dataloader 源码_带你从零掌握迭代器及构建最简DataLoader
點藍色字關注“機器學習算法工程師”
設為星標,干貨直達!
AI編輯:深度眸
0 摘要
? ? 本文本意是寫 pytorch 中 DataLoader 源碼學習心得,但是發現自己對迭代器和生成器的掌握比較水,不夠牢固,而我也沒有搜到能夠解決我所有疑問的解答文章,因此誕生了這篇文章。通過本文你將能夠零基礎深入掌握 python 迭代器相關知識、并且能夠一步步理解 DataLoader 的實現原理以及背后涉及的設計模式。
? ? 本文最終目的是通過源碼學習自己實現一個功能比較完善的 DataLoader 類,為了達到這個目的,本文寫作流程是:
先深入淺出分析 python 中迭代器、生成器等實現原理,包括 Iterable、Iterator、for .. in ..、__getitem__、yield 生成器?5個部分
再實現了一個最簡單版本的 DataLoader,目的是理解 DataLoader 與 Dataset、Sampler、BatchSampler和 collate_fn 之間的調用關系
最后對該實現進行深入全面分析,讀者可以清晰的理解每個類的作用
? ? 但是 DataLoader 功能其實非常復雜,故本文屬于系列文章的第一篇,后面文章會不斷完善、調整,最終實現 DataLoader 所有功能?;蛘哒f本文是后續文章的基礎,如果基礎內容沒有理解非常透徹,后面的多進程、分布式版本就更難以理解了。
? ? 雖然本文比較簡單,但是由于涉及到代碼,故為了方便,有必要的讀者可以 clone rep 進行學習(需要特意說明的是:rep 里面代碼是學習目的的,質量不高,不要要求那么多)
github:? https://github.com/hhaAndroid/miniloader
由于本人水平有限,某些環節理解可能有偏頗,歡迎指正。手機對于代碼顯示效果不太好,建議電腦端閱讀。
1 python 迭代器深入淺出理解
1.1 可迭代對象 Iterable
? ? 可迭代對象 Iterable:表示該對象可迭代,其并不是指某種具體數據類型。簡單來說只要是實現了 `__iter__` 方法的類就是可迭代對象。
from collections.abc import Iterable, Iteratorclass A(object): def __init__(self): self.a = [1, 2, 3] def __iter__(self): # 此處返回啥無所謂 return self.acls_a = A()# Trueprint(isinstance(cls_a, Iterable))? ? 但是對象如果是 Iterable 的,看起來好像也沒有特別大的用途,因為你依然無法迭代,實際上 Iterable 僅僅是提供了一種抽象規范接口:
for a in cls_a: print(a)# 程序報錯,要理解這個錯誤的含義TypeError: iter() returned non-iterator of type 'list'? ? 我們可以檢查下 Iterable 接口:
class Iterable(metaclass=ABCMeta): # 如果實現了這個方法,那么就是 Iterable @abstractmethod def __iter__(self): while False: yield None @classmethod def __subclasshook__(cls, C): if cls is Iterable: return _check_methods(C, "__iter__") return NotImplemented看起來實現 Iterable 接口用途不大,其實不是的,其有很多用途的,例如簡化代碼等,在后面的高級語法糖中會頻繁用到,后面會分析。
1.2 迭代器 Iterator
? ? 迭代器 Iterator:其和 Iterable 之間是一個包含與被包含的關系,如果一個對象是迭代器 Iterator,那么這個對象肯定是可迭代 Iterable;但是反過來,如果一個對象是可迭代 Iterable,那么這個對象不一定是迭代器 Iterator,可以通過接口協議看出:
class Iterator(Iterable): # 迭代具體實現 @abstractmethod def __next__(self): 'Return the next item from the iterator. When exhausted, raise StopIteration' raise StopIteration????# 返回自身,因為自身有?__next__?方法(如果自身沒有?__next__,那么返回自身沒有意義) def __iter__(self):????????return?self???????? @classmethod def __subclasshook__(cls, C): if cls is Iterator: return _check_methods(C, '__iter__', '__next__') return NotImplemented可以發現:實現了 `__next__` 和 `__iter__` 方法的類才能稱為迭代器,就可以被 for 遍歷了。
class A(object): def __init__(self): self.index = -1 self.a = [1, 2, 3]????#必須要返回一個實現了?__next__?方法的對象,否則后面無法?for?遍歷????#因為本類自身實現了?__next__,所以通常都是返回?self?對象即可 def __iter__(self): return self def __next__(self): self.index += 1 if self.index < len(self.a): return self.a[self.index] else:????????????#拋異常,for?內部會自動捕獲,表示迭代完成 raise StopIteration("遍歷完了")cls_a = A()print(isinstance(cls_a, Iterable)) # Trueprint(isinstance(cls_a, Iterator)) # Trueprint(isinstance(iter(cls_a), Iterator)) # Truefor a in cls_a: print(a)# 打印 1 2 3再次明確,一個對象如果要是 Iterator ,那么必須要實現 `__next__` 和 `__iter__` 方法,但是要理解其內部迭代流程,還需要理解 for .. in .. 流程。
1.3 for .. in .. 本質流程
? ? for .. in .. 也就是常見的迭代操作了,其被 python 編譯器編譯后,實際上代碼是:
# 實際調用了?__iter__?方法返回自身,包括了?__next__?方法的對象cls_a = iter(cls_a)while True: try: # 然后調用對象的 __next__ 方法,不斷返回元素 value = next(cls_a) print(value) # 如果迭代完成,則捕獲異常即可 except StopIteration: break可以看出,任何一個對象如果要能夠被 for 遍歷,必須要實現? `__iter__` 和 `__next__` 方法,缺一不可。
? ? 明白了上述流程,那么迭代器對象 A,我們可以采用如下方式進行遍歷:
myiter = iter(cls_a)print(next(myiter))print(next(myiter))print(next(myiter))# 因為遍歷完了,故此時會出現錯誤:StopIteration: 遍歷完了print(next(myiter))我們再來思考 python 內置對象 list 為啥可以被迭代?
b=list([1,2,3])print(isinstance(b, Iterable)) # Trueprint(isinstance(b, Iterator)) # False? ? 可以發現 list 類型是可迭代對象,但是其不是迭代器(即 list 沒有 `__next__` 方法),那為啥 for .. in .. 可以迭代呢?
? ? 原因是 list 內部的 `__iter__` 方法內部返回了具備 `__next__` 方法的類,或者說調用 iter() 后返回的對象本身就是一個迭代器,當然可以 for 循環了。
b=list([1,2,3])print(dir(b)) # 可以發現其存在 __iter__ 方法,不存在 __next__b=iter(b) # 調用 list 內部的 __iter__,返回了具備 __next__ 的對象print(isinstance(b, Iterable)) # Trueprint(isinstance(b, Iterator)) # Trueprint(dir(b)) # 同時具備 __iter__ 和 __next__ 方法基于上述理解我們可以對 A 類代碼進行改造,使其更加簡單:
class A(object): def __init__(self): self.a = [1, 2, 3] # 我們內部又調用了 list 對象的 __iter__ 方法,故此時返回的對象是迭代器對象 def __iter__(self): return iter(self.a)cls_a = A()print(isinstance(cls_a, Iterable)) # Trueprint(isinstance(cls_a, Iterator)) # Falsefor a in cls_a: print(a)# 輸出:1 2 3? ? 此時我們就實現了僅僅實現 Iterable 規范接口,但是又具備了 for .. in .. 功能,代碼是不是比最開始的實現簡單很多?這種寫法應用也非常廣泛,因為其不需要自己再次實現 `__next__` 方法。
? ? 如果你想理解的更加透徹,那么可以看下面例子:
# 僅僅實現 __iter__ class A(object): def __init__(self): self.b = B() def __iter__(self): return self.b# 僅僅實現 __next__class B(object): def __init__(self): self.index = -1 self.a = [1, 2, 3] def __next__(self): self.index += 1 if self.index < len(self.a): return self.a[self.index] else: # 內部會自動捕獲,表示迭代完成 raise StopIteration("遍歷完了")cls_a = A()cls_b = B()print(isinstance(cls_a, Iterable)) # Trueprint(isinstance(cls_a, Iterator)) # Falseprint(isinstance(cls_b, Iterable)) # Falseprint(isinstance(cls_b, Iterator)) # Falseprint(type(iter(cls_a))) # B 對象print(isinstance(iter(cls_a), Iterator)) # Falsefor a in cls_a: print(a)# 輸出:1 2 3? ? 自此我們知道了:一個對象要能夠被 for .. in .. 迭代,那么不管你是直接實現 `__iter__` 和 `__next__` 方法(對象必然是 Iterator),還是只實現 `__iter__`(不是 Iterator),但是內部間接返回了具備 `__next__` 對象的類,都是可行的。
? ? 但是除了這兩種實現,還有其他高級語法糖,可以進一步精簡代碼。
1.4? __ getitem__ 理解
? ? 上面說過 for .. in .. 的本質就是調用對象的 `__iter__` 和 `__next__` 方法,但是有一種更加簡單的寫法,你通過僅僅實現 `__getitem__` 方法就可以讓對象實現迭代功能。實際上任何一個類,如果實現了`__getitem__` 方法,那么當調用 iter(類實例) 時候會自動具備`__iter__` 和 `__next__`方法,從而可迭代了。
? ? 通過下面例子可以看出,`__getitem__` 實際上是屬于 __iter__` 和 `__next__` 方法的高級封裝,也就是我們常說的語法糖,只不過這個轉化是通過編譯器完成,內部自動轉化,非常方便。
class?A(object): def __init__(self): self.a = [1, 2, 3] def __getitem__(self, item):????????return?self.a[item]????????cls_a = A()print(isinstance(cls_a, Iterable)) # Falseprint(isinstance(cls_a, Iterator)) # Falseprint(dir(cls_a)) # 僅僅具備 __getitem__ 方法cls_a = iter(cls_a)print(dir(cls_a)) # 具備 __iter__ 和 __next__ 方法print(isinstance(cls_a, Iterable)) # Trueprint(isinstance(cls_a, Iterator)) # True# 等價于?for?..?in?..while True: try:????????# 然后調用對象的?__next__?方法,不斷返回元素 value = next(cls_a) print(value) # 如果迭代完成,則捕獲異常即可 except StopIteration: break# 輸出:1 2 3而且 `__getitem__` 還可以通過索引直接訪問元素,非常方便
a[0]?#?1??a[4] # 錯誤,索引越界如果你想該對象具備 list 等對象一樣的長度屬性,則只需要實現 `__len__` 方法即可
class A(object): def __init__(self): self.a = [1, 2, 3] def __getitem__(self, item): return self.a[item] def __len__(self): return len(self.a)cls_a = A() print(len(cls_a)) # 3? ? 到目前為止,我們已經知道了第一種高級語法糖實現迭代器功能,下面分析另一個更簡單的可以直接作用于函數的語法糖。
1.5 yield 生成器
? ? 生成器是一個在行為上和迭代器非常類似的對象,二者功能上差不多,但是生成器更優雅,只需要用關鍵字 yield 來返回,作用于函數上叫生成器函數,函數被調用時會返回一個生成器對象,生成器本質就是迭代器,其最大特點是代碼簡潔。
def func(): for a in [1, 2, 3]: yield acls_g = func()print(isinstance(cls_g, Iterator)) # Trueprint(dir(cls_g)) # 自動具備 __iter__ 和 __next__ 方法for a in cls_g: print(a)# 輸出: 1 2 3# 一種更簡單的寫法是用 ()cls_g = (i for i in [1,2,3])? ? 直觀感覺和 `__getitem__` 一樣,也是高級語法糖,但是比 `__getitem__` 更加簡單,更加好用。
? ? 使用 yield 函數與使用 return 函數,在執行時差別在于:包含 yield 的方法一般用于迭代,每次執行時遇到 yield 就返回 yield 后的結果,但內部會保留上次執行的狀態,下次繼續迭代時,會繼續執行 yield 之后的代碼,直到再次遇到 yield 后返回。生成器是懶加載模式,特別適合解決內存占用大的集合問題。假設創建一個包含10萬個元素的列表,如果用 list 返回不僅占用很大的存儲空間,如果我們僅僅需要訪問前面幾個元素,那后面絕大多數元素占用的空間都白白浪費了,這種場景就適合采用生成器,在迭代過程中推算出后續元素,而不需要一次性全部算出。
1.6 小結
?list set dict等內置對象都是容器 container 對象,容器是一種把多個元素組織在一起的數據結構,可以逐個迭代獲取其中的元素。容器可以用 in 來判斷容器中是否包含某個元素。大多數容器都是可迭代對象,可以使用某種方式訪問容器中的每一個元素。
在迭代對象基礎上,如果實現了 `__next__`? 方法則是迭代器對象,該對象在調用 next()? 的時候返回下一個值,如果容器中沒有更多元素了,則拋出 StopIteration 異常。
對于采用語法糖 `__getitem__` 實現的迭代器對象,其本身實例既不是可迭代對象,更不是迭代器,但是其可以被 for in 迭代,原因是對該對象采用 iter(類實例) 操作后就會自動變成迭代器。
生成器是一種特殊迭代器,但是不需要像迭代器一樣實現`__iter__`和`__next__`方法,只需要使用關鍵字 yield 就可以,生成器的構造可以通過生成器表達式 (),或者對函數返回值加入 yield 關鍵字實現。
對于在類的 `__iter__` 方法中采用語法糖 yield 實現的迭代器對象,其本身實例是可迭代對象,但不是迭代器,但是其可以被 for .. in .. 迭代,原因是對該對象采用 iter(類實例) 操作后就會自動變成迭代器。
2 DataLoader 最簡版本 V1
? ? 這里說的最簡版本是指:沒有任何花哨、高級實現技巧,僅僅以實現最基礎功能為目的。具體來說是包括必備的5個對象:Dataset、Sampler、BatchSampler、DataLoader 和 collate_fn。其作用可以簡要描述為如下:
Dataset 提供整個數據集的隨機訪問功能,每次調用都返回單個對象,例如一張圖片和對應 target 等等
Sampler 提供整個數據集隨機訪問的索引列表,每次調用都返回所有列表中的單個索引,常用子類是 SequentialSampler 用于提供順序輸出的索引 和 RandomSampler 用于提供隨機輸出的索引
BatchSampler 內部調用 Sampler 實例,輸出指定 `batch_size` 個索引,然后將索引作用于 Dataset 上從而輸出 `batch_size` 個數據對象,例如 batch 張圖片和 batch 個 target
collate_fn 用于將 batch 個數據對象在 batch 維度進行聚合,生成 (b,...) 格式的數據輸出,如果待聚合對象是 numpy,則會自動轉化為 tensor,此時就可以輸入到網絡中了
迭代一次偽代碼如下(非迭代器版本):
class DataLoader(object): def __init__(self): # 假設數據長度是100,batch_size 是4 self.dataset = [[img0, target0], [img1, target1], ..., [img99, target99]] # 假設 sampler 是 SequentialSampler,那么實際上就是 [0,1,...,99] 列表而已 # 如果 sampler 是 RandomSampler,那么可能是 [30,1,34,2,6,...,0] 列表 self.sampler = [0, 1, 2, 3, 4, ..., 99] self.batch_size = 4 self.index = 0 def collate_fn(self, data): # batch 維度聚合數據 batch_img = torch.stack(data[0], 0) batch_target = torch.stack(data[1], 0) return batch_img, batch_target def __next__(self): # 0.batch_index 輸出,實際上就是 BatchSampler 做的事情 i = 0 batch_index = [] while i < self.batch_size: # 內部會調用 sampler 對象取單個索引 batch_index.append(self.sampler[self.index]) self.index += 1 i += 1 # 1.得到 batch 個數據了,調用 dataset 對象 data = [self.dataset[idx] for idx in batch_index] # 2. 調用 collate_fn 在 batch 維度拼接輸出 batch_data = self.collate_fn(data) return batch_data def __iter__(self): return self? ? 以上就是最抽象的 DataLoader 運行流程以及和 Dataset、Sampler、BatchSampler、collate_fn 的關系。
2.1 整體對象理解
? ? 首先需要強調的是 Dataset、Sampler、BatchSampler 和 DataLoader 都直接或間接實現了迭代器,你必須要先理解第一小節內容,否則本節內容會比較難理解,具體為:
?Dataset 通過實現 `__getitem__` 方法使其可迭代
?Sampler 對象是一個可迭代的基類對象,其常用子類 SequentialSampler 在 `__iter__` 內部返回迭代器,RandomSampler 在 `__iter__` 內部通過 yield 關鍵字返回迭代器
?BatchSampler 也是在 `__iter__` 內部通過 yield 關鍵字返回迭代器
?DataLoader 通過直接實現 `__next__` 和 `__iter__` 變成迭代器
? ? 注意除了 DataLoader 本身是迭代器外,其余對象本身不是迭代器,但是都能被 for .. in .. 迭代。下面一個簡單例子證明:
from?simplev1_datatset?import?SimpleV1Datasetfrom libv1 import SequentialSampler, RandomSampler from?collections?import?Iterator,?Iterable???simple_dataset?=?SimpleV1Dataset()? dataloader?=?DataLoader(simple_dataset,?batch_size=2,?collate_fn=default_collate)print(isinstance(simple_dataset, Iterable)) # Falseprint(isinstance(simple_dataset, Iterator)) # Falseprint(isinstance(iter(simple_dataset), Iterator)) # Trueprint(isinstance(SequentialSampler(simple_dataset), Iterable)) # Trueprint(isinstance(SequentialSampler(simple_dataset), Iterator)) # Falseprint(isinstance(iter(SequentialSampler(simple_dataset)), Iterator)) # True# BatchSampler?和?RandomSampler?內部實現結構一樣,結果也是一樣print(isinstance(RandomSampler(simple_dataset), Iterable)) # Trueprint(isinstance(RandomSampler(simple_dataset), Iterator)) # Falseprint(isinstance(iter(RandomSampler(simple_dataset)),?Iterator))?#?Trueprint(isinstance(dataloader, Iterator)) # True? ? 在 DataLoader 中主要涉及3個類,其內部實例傳遞關系如下:
? ? 由于 DataLoader 類寫的非常通用,故 Dataset、Sampler、BatchSampler 都可以外部傳入,除了 Dataset 必須輸入外,其余兩個類都有默認實現,最典型的 Sampler 就是 SequentialSampler 和 RandomSampler。
? ? 需要注意的是 Sampler 對象其實在大部分時候都不需要傳入 Dataset 實例對象,因為其功能僅僅是返回索引而已,并沒有直接接觸數據。
2.2 DataLoader 運行流程
? ? 最簡單版本 DataLoader,具備如下功能:
Dataset 內部返回需要是 numpy 或者 tensor 對象
Sampler 直接 SequentialSampler 和 RandomSampler
BatchSampler 已經實現
collate_fn 僅僅考慮了 numpy 或者 tensor 對象
僅僅支持 num_works=0 即單進程
看起來功能非常單一,但是其實已經搭建起了整個框架,理解了這個最簡框架才能去理解高級實現,其核心運行邏輯為:
def?__next__(self):????# 返回?batch?個索引????index?=?next(self.batch_sampler)????# 利用索引去取數據????data?=?[self.dataset[idx]?for?idx?in?index????# batch?維度聚合????data?=?self.collate_fn(data)????return?data然后為了方便大家理解,特意繪制了如下代碼運行流程圖:
? ? 還是那句話:一定要對第1小節內容非常熟悉,否則里面這么多迭代器、生成器的調用,可能會把你繞暈。詳細代碼描述如下:
`self.batch_sampler = iter(batch_sampler)`。在 DataLoader 的類初始化,需要得到 BatchSampler 的迭代器對象
`index = next(self.batch_sampler)`。對于每次迭代,DataLoader 對象首先會調用 BatchSampler 的迭代器進行下一次迭代,具體是調用 BatchSampler 對象的? `__iter__`? 方法
而 BatchSampler 對象的 `__iter__` 方法實際上是需要依靠 Sampler 對象進行迭代輸出索引,Sampler 對象也是一個迭代器,當迭代 `batch_size` 次后就可以得到 `batch_size` 個數據索引
`data = [self.dataset[idx] for idx in index]`。有了 batch 個索引就可以通過不斷調用? dataset 的 `__getitem__` 方法返回數據對象,此時 data 就包含了 batch 個對象
`data = self.collate_fn(data)`。將 batch 個對象輸入給聚合函數,在第0個維度也就是 batch 維度進行聚合,得到類似 (b,...) 的對象
不斷重復1-5步,就可以不斷的輸出一個一個 batch 的數據了
以上就是完整流程,如果理解有困難,你可以先看下一小結的代碼實現,然后再返回去理解。
2.3 最簡V1版本源代碼
(1) Dataset
class?Dataset(object):????# 只要實現了?__getitem__?方法就可以變成迭代器 def __getitem__(self, index): raise NotImplementedError # 用于獲取數據集長度 def __len__(self): ????????raise?NotImplementedError(2) Sampler
class?Sampler(object):????def?__init__(self,?data_source): pass def __iter__(self): raise NotImplementedError def __len__(self): raise NotImplementedErrorclass?SequentialSampler(Sampler): def __init__(self, data_source): super(SequentialSampler, self).__init__(data_source) self.data_source = data_source def __iter__(self): # 返回迭代器,不然無法 for .. in .. return iter(range(len(self.data_source))) def __len__(self): return len(self.data_source)class BatchSampler(Sampler): def __init__(self, sampler, batch_size, drop_last): self.sampler = sampler self.batch_size = batch_size self.drop_last = drop_last def __iter__(self): batch = [] # 調用 sampler 內部的迭代器對象 for idx in self.sampler: batch.append(idx) # 如果已經得到了 batch 個 索引,則可以通過 yield # 關鍵字生成生成器返回,得到迭代器對象 if len(batch) == self.batch_size: yield batch batch = [] if len(batch) > 0 and not self.drop_last: yield batch def __len__(self): if self.drop_last: # 如果最后的索引數不夠一個 batch,則拋棄 return len(self.sampler) // self.batch_size ????????else:??????????????return? (len(self.sampler)?+?self.batch_size?-?1)?//?self.batch_size(3) DataLoader
class DataLoader(object): def __init__(self, dataset, batch_size=1, shuffle=False, sampler=None,?????????????????batch_sampler=None,?collate_fn=None,?drop_last=False) self.dataset = dataset # 因為這兩個功能是沖突的,假設 shuffle=True, # 但是 sampler 里面是 SequentialSampler,那么就違背設計思想了 if sampler is not None and shuffle: raise ValueError('sampler option is mutually exclusive with ' 'shuffle') if batch_sampler is not None: # 一旦設置了 batch_sampler,那么 batch_size、shuffle、sampler # 和 drop_last 四個參數就不能傳入 # 因為這4個參數功能和 batch_sampler 功能沖突了 if batch_size != 1 or shuffle or sampler is not None or drop_last: raise ValueError('batch_sampler option is mutually exclusive ' 'with batch_size, shuffle, sampler, and ' 'drop_last') batch_size = None drop_last = False if sampler is None: if shuffle: sampler = RandomSampler(dataset) else: sampler = SequentialSampler(dataset) # 也就是說 batch_sampler 必須要存在,你如果沒有設置,那么采用默認類 if batch_sampler is None: batch_sampler = BatchSampler(sampler, batch_size, drop_last) self.batch_size = batch_size self.drop_last = drop_last self.sampler = sampler self.batch_sampler = iter(batch_sampler) if collate_fn is None: collate_fn = default_collate self.collate_fn = collate_fn # 核心代碼 def __next__(self): index = next(self.batch_sampler) data = [self.dataset[idx] for idx in index] data = self.collate_fn(data) return data # 返回自身,因為自身實現了 __next__ def __iter__(self): return self(4) collate_fn
def default_collate(batch): elem = batch[0] elem_type = type(elem) if isinstance(elem, torch.Tensor): return torch.stack(batch, 0) elif elem_type.__module__ == 'numpy': return default_collate([torch.as_tensor(b) for b in batch]) else: raise NotImplementedError(5) 調用完整例子
class SimpleV1Dataset(Dataset): def __init__(self): # 偽造數據 self.imgs = np.arange(0, 16).reshape(8, 2) def __getitem__(self, index): return self.imgs[index] def __len__(self): return self.imgs.shape[0] from simplev1_datatset import SimpleV1Dataset simple_dataset = SimpleV1Dataset() dataloader = DataLoader(simple_dataset, batch_size=2, collate_fn=default_collate)for data in dataloader: print(data)3 總結
? ? 本文是最小 DataLoader 系列文章的第一篇,重點是分析了 python 中迭代器相關知識,然后構建一個最簡單的 DataLoader 類,用于加深到 DataLoader 流程的理解,功能比較簡單。
? ? 后面慢慢完善,希望最終能實現完整功能。
github: https://github.com/hhaAndroid/miniloader
推薦閱讀
PyTorch 源碼解讀之 torch.autograd
PyTorch 源碼解讀之 BN & SyncBN
機器學習算法工程師
? ??? ? ? ? ? ? ? ? ? ? ? ??????????????????一個用心的公眾號
總結
以上是生活随笔為你收集整理的dataloader 源码_带你从零掌握迭代器及构建最简DataLoader的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: er图转化为关系模式题_“助你在家自学”
- 下一篇: ubuntu开启客户端nfs服务_LIN