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