3.2 DataLoader
dataloader簡介
按照上圖的順序,本小節就來到pytorch資料載入最核心模組——DataLoader。
torch.utils.data.DataLoader(dataset, batch_size=1, shuffle=False, sampler=None, batch_sampler=None, num_workers=0, collate_fn=None, pin_memory=False, drop_last=False, timeout=0, worker_init_fn=None, multiprocessing_context=None, generator=None, *, prefetch_factor=2, persistent_workers=False)
從以上可以看到,DataLoader類有14個變數,因此成為最核心模組,一點不為過。
DataLoader功能繁多,這裡根據官方文檔可總結為以下五大功能:
- 支持兩種形式資料集讀取:map-style and iterable-style datasets,
- 自訂採樣策略:customizing data loading order,
- 自動組裝成批資料:automatic batching,
- 多進程資料載入:single- and multi-process data loading,
- 自動實現鎖頁記憶體(Pinning Memory):automatic memory pinning.
支持兩種形式資料集讀取
兩種形式的資料集分別是映射式(Map-style)與反覆運算式(Iterable-style),在3.1小結中講解的Dataset類就是映射式,因為它(getitem)提供了序號到資料的映射。反覆運算式則是編寫一個可反覆運算物件,從中依次獲取資料,此處不詳細展開,感興趣可以瞭解IterableDataset
注:往後不做特別說明,Dataset均表示映射式Dataset。
自訂採樣策略
DataLoader可借助Sampler自訂採樣策略,包括為每個類別設置採樣權重以實現1:1的均衡採樣,或者是自訂採樣策略,關於Sampler會在後面小結詳細展開,它是一個漲點神奇。
自動組裝成批資料
mini-batch形式的訓練成為了深度學習的標配,如何把資料組裝成一個batch資料?DataLoader內部自動實現了該功能,並且可以通過batch_sampler、collate_fn來自訂群組裝的策略,十分靈活。
多進程資料載入
通常GPU運算消耗資料會比CPU讀取載入資料要快,CPU“生產”跟不上GPU“消費”,因此需要多進程進行載入資料,以滿足GPU的消費需求。通常指要設置num_workers 為CPU核心數,如16核的CPU就設置為16。
自動實現鎖頁記憶體(Pinning Memory)
鎖頁記憶體的概念通常在作業系統課程裡才會涉及,非CS的同學可能不太瞭解,感興趣的可以去瞭解一下。Pinning Memory是空間換時間的做法,將指定的資料“鎖”住,不會被系統移動(交換)到磁片中的虛擬記憶體,因此可以加快資料的讀取速率。簡單的可以理解為常用的衣服就“鎖”在你的衣櫃裡,某些時候(如夏天),暫時不用的衣服——冬季大衣,則會移動到收納櫃裡,以騰出空間放其它常用的衣服,等到冬天來臨,需要用到大衣的時候,再從收納櫃裡把大衣放到衣櫃中。但是冬天拿大衣的時候就會慢一些,如果把它“鎖”在你的衣櫃,那麼冬天獲取它的時候自然快了,但佔用了你的空間。這就是空間換時間的一個例子。這裡的“鎖”就是固定的意思,大家可補充學習一下OS的內容。
DataLoader API
DataLoader提供了豐富的功能,下面介紹常用的功能,高階功能等到具體項目中再進行分析。
- dataset:它是一個Dataset實例,要能實現從索引(indices/keys)到樣本的映射。(即getitem函數)
- batch_size:每個batch的樣本量
- shuffle:是否對打亂樣本順序。訓練集通常要打亂它!驗證集和測試集無所謂。
- sampler:設置採樣策略。後面會詳細展開。
- batch_sampler:設置採樣策略, batch_sampler與sampler二選一,具體選中規則後面代碼會體現。
- num_workers: 設置多少個子進程進行資料載入(data loading)
- collate_fn:組裝資料的規則, 決定如何將一批資料組裝起來。
- pin_memory:是否使用鎖頁記憶體,具體行為是“the data loader will copy Tensors into CUDA pinned memory before returning them”
- drop_last:每個epoch是否放棄最後一批不足batchsize大小的資料,即無法被batchsize整除時,最後會有一小批資料,是否進行訓練,如果資料量足夠多,通常設置為True。這樣使模型訓練更為穩定,大家千萬不要理解為某些資料被捨棄了,因為每個epoch,dataloader的採樣都會重新shuffle,因此不會存在某些資料被真正的丟棄。
下面通過配套代碼加深dataloader的理解,並且觀察DataLoader 與 Dataset是如何配合使用的。
運行代碼,可看到輸出如下資訊:
0 torch.Size([2, 3, 224, 224]) torch.Size([2]) tensor([1, 0])
1 torch.Size([2, 3, 224, 224]) torch.Size([2]) tensor([0, 1])
2 torch.Size([1, 3, 224, 224]) torch.Size([1]) tensor([0])
0 torch.Size([3, 3, 224, 224]) torch.Size([3]) tensor([0, 0, 1])
1 torch.Size([2, 3, 224, 224]) torch.Size([2]) tensor([1, 0])
0 torch.Size([2, 3, 224, 224]) torch.Size([2]) tensor([0, 0])
1 torch.Size([2, 3, 224, 224]) torch.Size([2]) tensor([0, 1])
這裡主要觀察batch_size和drop_last的作用,以及圖片組裝成batch之後的shape。
這裡構建一個資料量為5的dataset,這樣可以採用batchsize=2和3來觀察drop_last的作用。
dataloader內部代碼
下一步,我們將採用debug模式,深入dataloader內部,觀察它是如何進行採樣的,如何調用dataset的getitem獲取資料,如何組裝一個batch的。這裡我們僅觀察單進程模式,因此大家的num_workers注意設置為0。
首先在for i, (inputs, target) in enumerate(train_loader_bs2) 設置一個中斷點,然後debug模式運行代碼,接著持續採用 Step Into方式運行代碼,下面就列出依次會進入的代碼:
第一步:初始化dataloader反覆運算器
for i, (inputs, target) in enumerate(train_loader_bs2)
DataLoader的iter()
DataLoader的_get_iterator()
SingleProcessDataLoaderIter的_init。
注:至此,僅完成了DataLoader的初始化,需要再一次進入dataloader才開始讀取資料。
第二步:依次迴圈該反覆運算器
來到 BaseDataLoaderIter的_next:進入521行:data = self._next_data()
來到 _SingleProcessDataLoaderIter的_next_data:此函式呼叫了兩個重要功能,第一個獲取一個batch的索引,第二個獲取此batch的資料。下面一個一個來看。
進入 _SingleProcessDataLoaderIter的_next_data:進入560行, index = self._next_index()
來到 _BaseDataLoaderIter的_next_index(): 這裡是對sampler的包裝,調用sampler獲取一批索引,進入512行
來到BatchSampler的iter():函數中有yield,這是一個反覆運算器,從這裡可以看到sampler是如何工作的。預設情況下,這裡用的是RandomSampler, 它會實現採樣的順序及頻率。在本函數中,對self.sampler依次反覆運算,拿到足夠一個batchsize的索引時,就yield。
回到 _SingleProcessDataLoaderIter的_next_data:第561行,經過index = self._next_index() ,已經獲得一個batch所對應的index,接著進入self._dataset_fetcher.fetch(index)
來到 _MapDatasetFetcher的fetch:mapdataset就是前面講到的map-style dataset。看到第49行,是一個列表生成式,在這裡,調用了我們自己寫的dataset,繼續進入。
來到 AntsBeesDataset的getitem:進入到這裡,大家就豁然開朗了吧,知道dataset是如何被dataloader使用的。下面,直接跳出去,回到 fetch看看如何組裝的。
來到 _MapDatasetFetcher的fetch:第52行self.collate_fn(data), 這裡採用collate_fn對資料進行組裝,繼續進入。
來到 collate.py的default_collate():這是pytorch預設的組裝函數,值得大家認真學習。這個函數通常是一個遞迴函數,第一次進入時可以發現會來到第84行:return [default_collate(samples) for samples in transposed]。會依次再進入一次default_collate()。
這裡的邏輯是這樣的:
首先將dataset返回的一系列資料解包再zip,為的是將相同資料放到一起。即getitem的return返回有img和label,這裡就是為了將多個img放到一起,多個label放到一起,當然大家可以在getitem返回其它有用資訊(例如圖片的路徑)。
接著再次進入default_collate函數時,會對實際的資料進行組裝。例如img的資料會進入if isinstance(elem, torch.Tensor),然後會看到img資料是這樣被組裝起來的:torch.stack(batch, 0, out=out),因此可知一個batch的圖像資料第一個維度是B,整體是BCH*W。
至此,一個batch資料已經讀取、載入完畢,依次跳出函數,可回到for i, (inputs, target) in enumerate(train_loader_bs2)。
這個時候,再觀察inputs, target,一定會更清晰了,知道它們是如何從硬碟到模型需要的形式。並且通過上述debug過程,我們可以知道sampler的機制、collate_fn的機制,方便今後進行高級的改造。希望大家一定要debug幾遍上述過程,並且記錄。
小結
以上就是關於DataLoader的概念的介紹,通過兩個小節相信大家對資料讀取有了初步認識,可pytorch的資料處理遠不止於此,它還提供了很多使用的方法,例如資料集的拼接,資料集的截取,資料的劃分等,想瞭解怎麼使用,請接著往下看。