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