MegEngine Python 層模塊串講(上)
在前面的文章中,我們簡單介紹了在?MegEngine imperative
?中的各模塊以及它們的作用。對于新用戶而言可能不太了解各個模塊的使用方法,對于模塊的結(jié)構(gòu)和原理也是一頭霧水。Python
?作為現(xiàn)在深度學習領域的主流編程語言,其相關(guān)的模塊自然也是深度學習框架的重中之重。
模塊串講將對?MegEngine
?的?Python
?層相關(guān)模塊分別進行更加深入的介紹,會涉及到一些原理的解釋和代碼解讀。Python
?層模塊串講共分為上、中、下三個部分,本文介紹?Python
?層的?data
?模塊。讀者將通過本文了解到要構(gòu)建數(shù)據(jù)?pipeline
?所需要的對象,以及如何高效地構(gòu)建?pipeline
。
構(gòu)建數(shù)據(jù)處理 pipeline —— data 模塊
神經(jīng)網(wǎng)絡需要數(shù)據(jù)才可以訓練,數(shù)據(jù)源文件可能是各種格式,讀取數(shù)據(jù)需要定義采樣規(guī)則、數(shù)據(jù)變換規(guī)則、數(shù)據(jù)合并策略等,這些和數(shù)據(jù)相關(guān)的模塊都封裝在?data?下。
在?MegEngine
?中訓練模型讀取數(shù)據(jù)的?pipeline
?一般是:
創(chuàng)建一個?Dataset?對象;
按照訓練場景的需求可能需要對數(shù)據(jù)做一些變換或合并的處理,這里可能需要創(chuàng)建?Sampler、Transform、Collator?等對象來完成相應操作;
創(chuàng)建一個?DataLoader?對象;
將數(shù)據(jù)分批加載到?
DataLoader
?里,迭代?DataLoader
?對象進行訓練。
下面我們看一下這幾個對象的實現(xiàn)。
Dataset
在?MegEngine
?中,數(shù)據(jù)集是一個可迭代的對象,所有的?Dataset
?對象都繼承自?class Dataset,都需要實現(xiàn)自己的?__getitem__()
?方法和?__len__()
?方法,這兩個方法分別是用來獲取給定索引的對應的數(shù)據(jù)樣本和返回數(shù)據(jù)集的大小。
根據(jù)對數(shù)據(jù)集訪問方式的區(qū)別,MegEngine
?中的數(shù)據(jù)集類型主要分為兩種:ArrayDataset?和?StreamDataset,前者支持隨機訪問數(shù)據(jù)樣本,而后者只可以順序訪問。二者的主要區(qū)別見下表:

Dataset
Dataset
?支持對數(shù)據(jù)集的隨機訪問,訪問類型是?Map-style
?的,也就是可以從索引映射到數(shù)據(jù)樣本,使用時需要實現(xiàn)?__getitem__()
?方法和?__len__()
?方法。
下面是一個使用?Dataset
?生成一個由?0
?到?5
?的數(shù)組成的數(shù)據(jù)集的例子:
使用起來如下:
可以發(fā)現(xiàn)?Dataset
?最大的特點就是可以根據(jù)給定索引隨機訪問數(shù)據(jù)集中對應下標的數(shù)據(jù)。
ArrayDataset
對于?Numpy ndarray
?類型的數(shù)據(jù),MegEngine
?中對?Dataset
?進一步封裝實現(xiàn)了?ArrayDataset
,使用?ArrayDataset
?無需實現(xiàn)?__getitem__()
?方法和?__len__()
?方法。
下面的例子隨機生成了一個具有?100
?個樣本、每張樣本為?32 × 32
?像素的?RGB
?圖片的數(shù)據(jù)集:
由于需要支持隨機訪問,因此對于支持順序訪問的?Dataset
?需要將索引等信息加載進內(nèi)存,如果數(shù)據(jù)集規(guī)模較大導致內(nèi)存無法存放從而發(fā)生?OOM(Out Of Memory)
,我們需要考慮使用流式數(shù)據(jù)?StreamDataset
。
StreamDataset
當數(shù)據(jù)集規(guī)模較大時,使用流失數(shù)據(jù)迭代訪問數(shù)據(jù)對象是比較主流的做法。從類的定義可以看到:由于無法根據(jù)索引獲取數(shù)據(jù),因此?StreamDataset
?無需實現(xiàn)?__getitem__()
?方法和?__len__()
?方法,但是需要實現(xiàn)一個?__iter__()
?方法定義流式獲取數(shù)據(jù)的規(guī)則:
StreamDataset
?適用的場景主要是:
隨機讀取成本過高,或者數(shù)據(jù)規(guī)模太大,無法支持;
必須根據(jù)流數(shù)據(jù)才能判斷當前批是否已經(jīng)完整。
可以使用流數(shù)據(jù)返回從數(shù)據(jù)庫、遠程服務器甚至實時生成的日志中讀取的數(shù)據(jù)流。
下面的例子展示了如何生成一個由?0
?到?5
?這五個數(shù)組成的數(shù)據(jù)集:
Sampler
有了?DataSet
?之后,DataLoader
?可以從數(shù)據(jù)集加載數(shù)據(jù)到內(nèi)存,但是對每批數(shù)據(jù)有時候需要規(guī)定規(guī)模的大小,還有定義抽樣規(guī)則等需求,使用?Sampler
?可以對每批數(shù)據(jù)的抽樣規(guī)則進行自定義。
準確來說,抽樣器的職責是決定數(shù)據(jù)的獲取順序,方便為?DataLoader
?提供一個可供迭代的多批數(shù)據(jù)的索引:
在 MegEngine 中,Sampler
?是所有抽樣器的抽象基類,在大部分情況下用戶無需對抽樣器進行自定義實現(xiàn), 因為在 MegEngine 中已經(jīng)實現(xiàn)了常見的各種抽樣器,比如上面示例代碼中的?RandomSampler?抽樣器。
下面介紹?MegEngine
?中幾種常見的?Sampler
。
SequentialSampler
SequentialSampler(https://github.com/MegEngine/MegEngine/blob/master/imperative/python/megengine/data/sampler.py#L178)?也叫?MapSampler
, 顧名思義就是對數(shù)據(jù)集進行順序抽樣的抽樣器。
對一個含有?100
?個數(shù)據(jù)樣本的數(shù)據(jù)集,batch_size
?為?10
,可以得到?10
?批順序索引:
默認情況下?batch_size
?為?1
,表示逐個遍歷數(shù)據(jù)集中的樣本,drop_last
?為?False
。
RandomSampler
RandomSampler?用來對數(shù)據(jù)集進行無放回隨機抽樣(也叫簡單隨機抽樣)。
直接看例子:
ReplacementSampler
ReplacementSampler?是有放回隨機抽樣,也就是可能抽樣到之前已經(jīng)抽樣過的數(shù)據(jù)。
使用方法和無放回隨機抽樣類似:
Infinite
通常數(shù)據(jù)集在給定?batch_size
?的情況下,只能劃分為有限個?batch
。 這意味著抽樣所能得到的數(shù)據(jù)批數(shù)是有限的,想要重復利用數(shù)據(jù), 最常見的做法是循環(huán)多個周期?epochs
?來反復遍歷數(shù)據(jù)集:
、但在一些情況下,我們希望能夠直接從數(shù)據(jù)集中無限進行抽樣, 因此MegEngine
提供了?Infinite?包裝類用來進行無限抽樣:
以上就是常見的?Sampler
?的使用方法,有時候?qū)τ跀?shù)據(jù)集中的數(shù)據(jù)還需要做一些變換以滿足業(yè)務需要,這就是我們接下來要說的?transform
。
Transform
在深度學習中對數(shù)據(jù)進行變換(Transformation
)以滿足業(yè)務需求和增強模型性能是很常見的操作。
在?megengine.data.transform
?中提供的各種數(shù)據(jù)變換都是基于?Transform?抽象類實現(xiàn)的,其中:
apply
?抽象方法可用于單個的數(shù)據(jù)樣本, 需要在子類中實現(xiàn);各種變換操作可以通過?Compose?進行組合,這樣使用起來更加方便。
我們能夠很方便地在?DataLoader
?加載數(shù)據(jù)時進行相應地變換操作。例如:
上面就是將兩個?transform
?操作?Resize()
?和?ToMode()
?組合起來對數(shù)據(jù)進行變換。
下面舉個例子如何實現(xiàn)自己的?Transform
:
上面這個?Transform
?實現(xiàn)了自己的?apply()
?方法,對數(shù)據(jù)集中的所有樣本做了一個?+1
?操作。
可以使用?Compose
?對數(shù)據(jù)變換進行組合:
最終,我們的各種Transform
實現(xiàn)應當被應用于DataLoader
:
實際使用時對數(shù)據(jù)做的操作往往比上面的例子要復雜許多,MegEngine
?在?VisionTransform?中已經(jīng)實現(xiàn)了很多轉(zhuǎn)換方法供用戶使用。用戶也可以根據(jù)需要實現(xiàn)自己的數(shù)據(jù)變換方法。
當我們從?DataLoader
?中獲取批數(shù)據(jù)時,如果定義了?Transform
, 則會在每次加載完樣本后立即對其進行變換。
數(shù)據(jù)變換操作也是有計算開銷的,且該流程通常在?CPU
?設備上進行,以及有些操作會調(diào)用類似?OpenCV
?的庫。 如果我們對每個樣本進行多次加載(比如訓練多個周期),那么變換操作也會被執(zhí)行多次,這可能會帶來額外的開銷。 因此在有些時候,我們會選擇將預處理操作在更早的流程中進行,即直接對原始數(shù)據(jù)先進行一次預處理操作, 這樣在?DataLoader
?中獲取的輸入便已經(jīng)是經(jīng)過預處理的數(shù)據(jù)了,這樣可以盡可能地減少?Transform
?操作。
用戶應當考慮到,原始數(shù)據(jù)相關(guān)的?I/O
?和處理也有可能成為模型訓練整體流程中的瓶頸。
Collator
在使用?DataLoader
?獲取批數(shù)據(jù)的整個流程中,?Collator?負責合并樣本,最終得到批數(shù)據(jù)。
Collator
?僅適用于?Map-style
?的數(shù)據(jù)集,因為?Iterable-style
?數(shù)據(jù)集的批數(shù)據(jù)必然是逐個合并的。
經(jīng)過?DataSet
?和?Transform
?的處理后,?Collator
?通常會接收到一個列表:
如果你的?
Dataset
?子類的?__getitem__
?方法返回的是單個元素,則?Collator
?得到一個普通列表;如果你的?
Dataset
?子類的?__getitem__
?方法返回的是一個元組,則?Collator
?得到一個元組列表。
MegEngine
?中使用?Collator?作為默認實現(xiàn),通過調(diào)用?apply
?方法來將列表數(shù)據(jù)合并成批數(shù)據(jù):
默認的?Collator
?支持?NumPy ndarray
,?Numbers
,?Unicode strings
,?bytes
,?dicts
?或?lists
?數(shù)據(jù)類型。 要求輸入必須包含至少一種上述數(shù)據(jù)類型,否則用戶需要使用自己定義的?Collator
。
Collator
?的作用是合并數(shù)據(jù),比如每個數(shù)據(jù)樣本是?shape
?為?(C, H, W)
?的圖片,如果我們在?Sampler
?中指定了?batch_size
?為?N
。那么?Collator
?就會將獲得的樣本列表合并成一個?shape
?為?(N, C, H, W)
?的批樣本結(jié)構(gòu)。
我們可以模擬得到這樣一個?image_list
?數(shù)據(jù),并借助?Collator
?得到?batch_image
:
DataLoader
前面介紹的?Dataset
、Sampler
、Transform
、Collator
?等對象都是為了更靈活地配置?DataLoader
?對象的。
當單進程運行?DataLoader
?時(設置?num_workers=0
),每當我們向?DataLoader
?索要一批數(shù)據(jù)時,DataLoader
?將從 Sampler 獲得下一批數(shù)據(jù)的索引, 根據(jù)?Dataset
?提供的?__getitem__()
?方法將對應的數(shù)據(jù)逐個加載到內(nèi)存, 加載進來的數(shù)據(jù)可以通過指定的?Transform
?做一些處理,再通過?Collator
?將單獨的數(shù)據(jù)組織成批數(shù)據(jù)。
DataLoader
?也支持多進程加載以提升數(shù)據(jù)加載處理速度(提高?num_workers
?數(shù)量)。 一般?worker
?數(shù)量越多,數(shù)據(jù)加載處理的速度會越快。不過如果?worker
?數(shù)過多, 并大大超出了系統(tǒng)中 cpu 的數(shù)量,這些子進程可能會存在競爭?cpu
?資源的情況,反而導致效率的降低。
一般來說,我們建議根據(jù)系統(tǒng)中?cpu
?的數(shù)量設置?worker
?的值。 比如在一臺?64 cpu
,?8 gpu
?的機器上,預期中每個?gpu
?會對應?8
?個?cpu
, 那么我們在使用時對應的把?worker
?數(shù)設置在?8
?左右就是個不錯的選擇。
下面以一個加載圖像分類數(shù)據(jù)的流程來舉例說明如何創(chuàng)建一個加載數(shù)據(jù)的?pipeline
。
1、假設圖像數(shù)據(jù)按照一定的規(guī)則放置于同一目錄下(通常數(shù)據(jù)集主頁會對目錄組織和文件命名規(guī)則進行介紹)。 要創(chuàng)建對應的數(shù)據(jù)加載器,首先需要一個繼承自?Dataset
?的類。 我們可以創(chuàng)建一個自定義的數(shù)據(jù)集:
要獲取示例圖像,可以創(chuàng)建一個數(shù)據(jù)集對象,并將示例索引傳遞給__getitem__()
方法, 然后將返回圖像數(shù)組和對應的標簽,例如:
2、現(xiàn)在我們已經(jīng)預先創(chuàng)建了能夠返回一個樣本及其標簽的類CustomImageDataset
, 但僅依賴Dataset
本身還無法實現(xiàn)自動分批、亂序、并行等功能; 我們必須接著創(chuàng)建DataLoader
, 它通過其它的參數(shù)配置項圍繞這個類“包裝”, 可以按照我們的要求從數(shù)據(jù)集類中返回整批樣本。
3、現(xiàn)在可以加載數(shù)據(jù)并進行訓練了:
更多 MegEngine 信息獲取,您可以:查看文檔:https://www.megengine.org.cn/doc/stable/zh/
GitHub 項目: https://github.com/MegEngine
加入 MegEngine 用戶交流 QQ 群:1029741705
歡迎參與 MegEngine 社區(qū)貢獻,成為?Awesome MegEngineer:https://www.megengine.org.cn/community-AMGE,榮譽證書、定制禮品享不停。