干貨!機器學(xué)習中,如何優(yōu)化數據性能
得益于覆蓋各種需求的第三方庫,Python在今天已經(jīng)成為了研究機器學(xué)習的主流工具。不過(guò)由于其解釋型語(yǔ)言的特性,在運行速度上往往和傳統編譯型語(yǔ)言有較大差距。特別是當訓練數據集非常龐大時(shí),很多時(shí)候處理數據本身就會(huì )占用大量的時(shí)間。
Python中自身提供了非常強大的數據存儲結構:numpy庫下的ndarry和pandas庫下的DataFrame。前者提供了很多list沒(méi)有實(shí)現的便利功能,而后者是最方便的column-row型數據的存儲方式,同樣提供了大量方便的隨機訪(fǎng)問(wèn)函數。
然而不正確的使用很多時(shí)候反而會(huì )適得其反,給人一種如此高級的三方庫性能還不如list手動(dòng)造輪子的錯覺(jué)。
本文主要通過(guò)優(yōu)化數據結構以及一些使用中的注意點(diǎn)來(lái)提高在大數據量下數據的處理速度。
避免使用append來(lái)逐行添加結果
很多人在逐行處理數據的時(shí)候,喜歡使用append來(lái)逐行將結果寫(xiě)入DataFrame或ndarry。類(lèi)似下面的寫(xiě)法:
這是非常不好的習慣,numpy或pandas在實(shí)現append的時(shí)候,實(shí)際上對內存塊進(jìn)行了拷貝——當數據塊逐漸變大的時(shí)候,這一操作的開(kāi)銷(xiāo)會(huì )非常大。
下面是官方文檔對此的描述:
Numpy:
Pandas.DataFrame:
實(shí)際上,受list的append操作的影響,開(kāi)發(fā)者會(huì )不假思索的認為numpy和pandas中的append也是簡(jiǎn)單的數組尾部拼接。這實(shí)際上是一個(gè)很?chē)乐氐恼`解,會(huì )產(chǎn)生很多不必要的拷貝開(kāi)銷(xiāo)。筆者沒(méi)有深入研究它們這么設計原因,猜測可能是為了保證拼接后的數組在內存中依然是連續區塊——這對于高性能的隨機查找和隨機訪(fǎng)問(wèn)是很有必要的。
解決辦法:
除非必須,在使用DataFrame的部分函數時(shí),考慮將inplace=True。出于保證原始數據的一致性,DataFrame的大部分方法都會(huì )返回一個(gè)原始數據的拷貝,如果要將返回結果寫(xiě)回,用這種方式效率更高。
除非必須,避免使用逐行處理。Numpy和pandas都提供了很多非常方便的區塊選取及區塊處理的辦法。這些功能非常強大,支持按條件的選取,能滿(mǎn)足大部分的需求。同時(shí)因為ndarry和DataFrame都具有良好的隨機訪(fǎng)問(wèn)的性能,使用條件選取執行的效率往往是高于條件判斷再執行的。
特殊情況下,使用預先聲明的數據塊而避免append。如果在某些特殊需求下(例如當前行的處理邏輯依賴(lài)于上一行的處理結果)并且需要構造新的數組,不能直接寫(xiě)入源數據時(shí)。這種情況下,建議提前聲明一個(gè)足夠大的數據塊,將自增的逐行添加改為逐行賦值。
這種寫(xiě)法本質(zhì)上是通過(guò)空間換取時(shí)間,即便數據量非常巨大,無(wú)法一次性寫(xiě)入內存,也可以通過(guò)數據塊的方式,減少不必要的拼接操作。需要注意的是,數據塊的邊界處理條件,以避免漏行。
避免鏈式賦值
鏈式賦值是幾乎所有pandas的新人都會(huì )在不知不覺(jué)中犯的錯誤,并且產(chǎn)生惱人而又意義不明的SettingWithCopyWarning警告。實(shí)際上這個(gè)警告是在提醒開(kāi)發(fā)者,你的代碼可能沒(méi)按你的預期運行,需要檢查——很多時(shí)候可能產(chǎn)生難以調試發(fā)現的錯誤。當使用DataFrame作為輸入的第三方庫時(shí),非常容易產(chǎn)生這類(lèi)錯誤,且難以判斷問(wèn)題到底出現在哪兒。
在繼續講解鏈式復制前,需要先了解pandas的方法有一部分是返回的是輸入數據的視圖(view)一部分返回的是輸入數據的拷貝(copy),還有少部分是直接修改源數據。
上圖很好的解釋了視圖與拷貝的關(guān)系。當需要對df2進(jìn)行修改時(shí),有時(shí)候我們希望df1也能被修改,有時(shí)候則不希望。而當使用鏈式賦值時(shí),則有可能產(chǎn)生歧義。這里的歧義指的是面向開(kāi)發(fā)人員的,代碼執行是不會(huì )有歧義的。
鏈式索引,就是對同一個(gè)數據連續的使用索引,形如data[1:5][2:3]這樣。而鏈式賦值,就是使用鏈式索引進(jìn)行賦值操作。下圖是一個(gè)鏈式賦值的例子,解釋器給出了SettingWithCopyWarning警告,同時(shí)對data的賦值操作也沒(méi)有成功。
解決辦法:上圖中的警告建議,當你想修改原始數據時(shí),使用loc來(lái)確保賦值操作被在原始數據上執行,這種寫(xiě)法對開(kāi)發(fā)人員是無(wú)歧義的(開(kāi)發(fā)人員往往會(huì )誤認為鏈式賦值修改的依然是源數據)。
反過(guò)來(lái)的情況并不會(huì )發(fā)生這種歧義。如果開(kāi)發(fā)人員想選取源數據的一部分,修改其中某列的值并賦給新的變量而不修改源數據,那么正常的寫(xiě)法就是無(wú)歧義的。
然而有些隱蔽的鏈式索引往往并不是簡(jiǎn)單的像上述情況那樣,有可能跨越多行代碼,甚至函數。下圖的例子中,data_part是對data的選取,而賦值操作又對data_part進(jìn)行了選取,此時(shí)構成了鏈式索引。
解決辦法:當你確定是要構造拷貝時(shí),明確指明構造拷貝。避免對有可能是視圖的中間變量進(jìn)行修改。
需要注意的是:DataFrame的索引操作到底是返回視圖還是返回拷貝,取決于數據本身。對于單類(lèi)型數據(全是某一類(lèi)型的DataFrame)出于效率的考慮,索引操作總是返回視圖,而對于多類(lèi)型數據(列與列的數據類(lèi)型不一樣)則總是返回拷貝。但也請不要依賴(lài)這一特性,因為根據內存布局,其行為未必總是一致。最好的方法還是明確指定——如果想要寫(xiě)入副本數據,就在索引時(shí)明確拷貝;如果想要修改源數據,就使用loc嚴格賦值。
總結
1.可以直接修改源數據就修改源數據,避免不必要的拷貝
2.使用條件索引替代逐行遍歷
3.構造數據塊替代逐行添加
4.想修改源數據時(shí)使用data.loc[row_index, col_index]替代鏈式賦值
5.想構造副本時(shí)嚴格使用copy消除****鏈式賦值
參考資料:
https://numpy.org/doc/stable/reference/generated/numpy.append.html
https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.append.html
https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#indexing-label
https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#indexing-view-versus-copy
https://zhuanlan.zhihu.com/p/41202576
*博客內容為網(wǎng)友個(gè)人發(fā)布,僅代表博主個(gè)人觀(guān)點(diǎn),如有侵權請聯(lián)系工作人員刪除。