<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          帶你從零掌握迭代器及構(gòu)建最簡(jiǎn)DataLoader

          共 17143字,需瀏覽 35分鐘

           ·

          2020-12-31 12:46

          點(diǎn)藍(lán)色字關(guān)注“機(jī)器學(xué)習(xí)算法工程師

          設(shè)為星標(biāo),干貨直達(dá)!

          AI編輯:深度眸


          0 摘要

          ? ? 本文本意是寫(xiě) pytorch 中 DataLoader 源碼學(xué)習(xí)心得,但是發(fā)現(xiàn)自己對(duì)迭代器和生成器的掌握比較水,不夠牢固,而我也沒(méi)有搜到能夠解決我所有疑問(wèn)的解答文章,因此誕生了這篇文章。通過(guò)本文你將能夠零基礎(chǔ)深入掌握 python 迭代器相關(guān)知識(shí)、并且能夠一步步理解 DataLoader 的實(shí)現(xiàn)原理以及背后涉及的設(shè)計(jì)模式。

          ? ? 本文最終目的是通過(guò)源碼學(xué)習(xí)自己實(shí)現(xiàn)一個(gè)功能比較完善的 DataLoader 類(lèi),為了達(dá)到這個(gè)目的,本文寫(xiě)作流程是:


          • 先深入淺出分析 python 中迭代器、生成器等實(shí)現(xiàn)原理,包括 Iterable、Iterator、for .. in ..、__getitem__、yield 生成器?5個(gè)部分

          • 再實(shí)現(xiàn)了一個(gè)最簡(jiǎn)單版本的 DataLoader,目的是理解 DataLoader 與 Dataset、Sampler、BatchSampler和 collate_fn 之間的調(diào)用關(guān)系

          • 最后對(duì)該實(shí)現(xiàn)進(jìn)行深入全面分析,讀者可以清晰的理解每個(gè)類(lèi)的作用


          ? ? 但是 DataLoader 功能其實(shí)非常復(fù)雜,故本文屬于系列文章的第一篇,后面文章會(huì)不斷完善、調(diào)整,最終實(shí)現(xiàn) DataLoader 所有功能?;蛘哒f(shuō)本文是后續(xù)文章的基礎(chǔ),如果基礎(chǔ)內(nèi)容沒(méi)有理解非常透徹,后面的多進(jìn)程、分布式版本就更難以理解了。

          ? ? 雖然本文比較簡(jiǎn)單,但是由于涉及到代碼,故為了方便,有必要的讀者可以 clone rep 進(jìn)行學(xué)習(xí)(需要特意說(shuō)明的是:rep 里面代碼是學(xué)習(xí)目的的,質(zhì)量不高,不要要求那么多)


          github:? https://github.com/hhaAndroid/miniloader


          由于本人水平有限,某些環(huán)節(jié)理解可能有偏頗,歡迎指正。手機(jī)對(duì)于代碼顯示效果不太好,建議電腦端閱讀。


          1 python 迭代器深入淺出理解


          1.1 可迭代對(duì)象 Iterable

          ? ? 可迭代對(duì)象 Iterable:表示該對(duì)象可迭代,其并不是指某種具體數(shù)據(jù)類(lèi)型。簡(jiǎn)單來(lái)說(shuō)只要是實(shí)現(xiàn)了 `__iter__` 方法的類(lèi)就是可迭代對(duì)象。

          from collections.abc import Iterable, Iteratorclass A(object):    def __init__(self):        self.a = [1, 2, 3]    def __iter__(self):        # 此處返回啥無(wú)所謂        return self.acls_a = A()#  Trueprint(isinstance(cls_a, Iterable))

          ? ? 但是對(duì)象如果是 Iterable 的,看起來(lái)好像也沒(méi)有特別大的用途,因?yàn)槟阋廊粺o(wú)法迭代,實(shí)際上 Iterable 僅僅是提供了一種抽象規(guī)范接口:

          for a in cls_a:    print(a)
          # 程序報(bào)錯(cuò),要理解這個(gè)錯(cuò)誤的含義TypeError: iter() returned non-iterator of type 'list'

          ? ? 我們可以檢查下 Iterable 接口:

          class Iterable(metaclass=ABCMeta):
          # 如果實(shí)現(xiàn)了這個(gè)方法,那么就是 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

          看起來(lái)實(shí)現(xiàn) Iterable 接口用途不大,其實(shí)不是的,其有很多用途的,例如簡(jiǎn)化代碼等,在后面的高級(jí)語(yǔ)法糖中會(huì)頻繁用到,后面會(huì)分析。


          1.2 迭代器 Iterator

          ? ? 迭代器 Iterator:其和 Iterable 之間是一個(gè)包含與被包含的關(guān)系,如果一個(gè)對(duì)象是迭代器 Iterator,那么這個(gè)對(duì)象肯定是可迭代 Iterable;但是反過(guò)來(lái),如果一個(gè)對(duì)象是可迭代 Iterable,那么這個(gè)對(duì)象不一定是迭代器 Iterator,可以通過(guò)接口協(xié)議看出:

          class Iterator(Iterable):
          # 迭代具體實(shí)現(xiàn) @abstractmethod def __next__(self): 'Return the next item from the iterator. When exhausted, raise StopIteration' raise StopIteration
          ????# 返回自身,因?yàn)樽陨碛?__next__?方法(如果自身沒(méi)有?__next__,那么返回自身沒(méi)有意義) def __iter__(self):????????return?self???????? @classmethod def __subclasshook__(cls, C): if cls is Iterator: return _check_methods(C, '__iter__', '__next__') return NotImplemented

          可以發(fā)現(xiàn):實(shí)現(xiàn)了 `__next__` 和 `__iter__` 方法的類(lèi)才能稱(chēng)為迭代器,就可以被 for 遍歷了。

          class A(object):    def __init__(self):        self.index = -1        self.a = [1, 2, 3]
          ????#必須要返回一個(gè)實(shí)現(xiàn)了?__next__?方法的對(duì)象,否則后面無(wú)法?for?遍歷????#因?yàn)楸绢?lèi)自身實(shí)現(xiàn)了?__next__,所以通常都是返回?self?對(duì)象即可 def __iter__(self): return self
          def __next__(self): self.index += 1 if self.index < len(self.a): return self.a[self.index] else:????????????#拋異常,for?內(nèi)部會(huì)自動(dòng)捕獲,表示迭代完成 raise StopIteration("遍歷完了")cls_a = A()print(isinstance(cls_a, Iterable)) # Trueprint(isinstance(cls_a, Iterator)) # Trueprint(isinstance(iter(cls_a), Iterator)) # True
          for a in cls_a: print(a)# 打印 1 2 3

          再次明確,一個(gè)對(duì)象如果要是 Iterator ,那么必須要實(shí)現(xiàn) `__next__` 和 `__iter__` 方法,但是要理解其內(nèi)部迭代流程,還需要理解 for .. in .. 流程。


          1.3 for .. in .. 本質(zhì)流程

          ? ? for .. in .. 也就是常見(jiàn)的迭代操作了,其被 python 編譯器編譯后,實(shí)際上代碼是:

          # 實(shí)際調(diào)用了?__iter__?方法返回自身,包括了?__next__?方法的對(duì)象cls_a = iter(cls_a)while True:    try:        # 然后調(diào)用對(duì)象的 __next__ 方法,不斷返回元素        value = next(cls_a)        print(value)    # 如果迭代完成,則捕獲異常即可    except StopIteration:        break


          可以看出,任何一個(gè)對(duì)象如果要能夠被 for 遍歷,必須要實(shí)現(xiàn)? `__iter__` 和 `__next__` 方法,缺一不可。

          ? ? 明白了上述流程,那么迭代器對(duì)象 A,我們可以采用如下方式進(jìn)行遍歷:

          myiter = iter(cls_a)print(next(myiter))print(next(myiter))print(next(myiter))# 因?yàn)楸闅v完了,故此時(shí)會(huì)出現(xiàn)錯(cuò)誤:StopIteration: 遍歷完了print(next(myiter))

          我們?cè)賮?lái)思考 python 內(nèi)置對(duì)象 list 為啥可以被迭代?

          b=list([1,2,3])print(isinstance(b, Iterable)) # Trueprint(isinstance(b, Iterator)) # False

          ? ? 可以發(fā)現(xiàn) list 類(lèi)型是可迭代對(duì)象,但是其不是迭代器(即 list 沒(méi)有 `__next__` 方法),那為啥 for .. in .. 可以迭代呢?

          ? ? 原因是 list 內(nèi)部的 `__iter__` 方法內(nèi)部返回了具備 `__next__` 方法的類(lèi),或者說(shuō)調(diào)用 iter() 后返回的對(duì)象本身就是一個(gè)迭代器,當(dāng)然可以 for 循環(huán)了。

          b=list([1,2,3])print(dir(b)) # 可以發(fā)現(xiàn)其存在 __iter__ 方法,不存在 __next__
          b=iter(b) # 調(diào)用 list 內(nèi)部的 __iter__,返回了具備 __next__ 的對(duì)象print(isinstance(b, Iterable)) # Trueprint(isinstance(b, Iterator)) # Trueprint(dir(b)) # 同時(shí)具備 __iter__ 和 __next__ 方法

          基于上述理解我們可以對(duì) A 類(lèi)代碼進(jìn)行改造,使其更加簡(jiǎn)單:

          class A(object):    def __init__(self):        self.a = [1, 2, 3]    # 我們內(nèi)部又調(diào)用了 list 對(duì)象的 __iter__ 方法,故此時(shí)返回的對(duì)象是迭代器對(duì)象    def __iter__(self):        return iter(self.a)
          cls_a = A()print(isinstance(cls_a, Iterable)) # Trueprint(isinstance(cls_a, Iterator)) # False
          for a in cls_a: print(a)# 輸出:1 2 3

          ? ? 此時(shí)我們就實(shí)現(xiàn)了僅僅實(shí)現(xiàn) Iterable 規(guī)范接口,但是又具備了 for .. in .. 功能,代碼是不是比最開(kāi)始的實(shí)現(xiàn)簡(jiǎn)單很多?這種寫(xiě)法應(yīng)用也非常廣泛,因?yàn)槠洳恍枰约涸俅螌?shí)現(xiàn) `__next__` 方法。

          ? ? 如果你想理解的更加透徹,那么可以看下面例子:

          # 僅僅實(shí)現(xiàn) __iter__ class A(object):    def __init__(self):        self.b = B()
          def __iter__(self): return self.b
          # 僅僅實(shí)現(xiàn) __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: # 內(nèi)部會(huì)自動(dòng)捕獲,表示迭代完成 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)) # False
          print(type(iter(cls_a))) # B 對(duì)象print(isinstance(iter(cls_a), Iterator)) # False
          for a in cls_a: print(a)
          # 輸出:1 2 3

          ? ? 自此我們知道了:一個(gè)對(duì)象要能夠被 for .. in .. 迭代,那么不管你是直接實(shí)現(xiàn) `__iter__` 和 `__next__` 方法(對(duì)象必然是 Iterator),還是只實(shí)現(xiàn) `__iter__`(不是 Iterator),但是內(nèi)部間接返回了具備 `__next__` 對(duì)象的類(lèi),都是可行的。

          ? ? 但是除了這兩種實(shí)現(xiàn),還有其他高級(jí)語(yǔ)法糖,可以進(jìn)一步精簡(jiǎn)代碼。


          1.4? __ getitem__ 理解

          ? ? 上面說(shuō)過(guò) for .. in .. 的本質(zhì)就是調(diào)用對(duì)象的 `__iter__` 和 `__next__` 方法,但是有一種更加簡(jiǎn)單的寫(xiě)法,你通過(guò)僅僅實(shí)現(xiàn) `__getitem__` 方法就可以讓對(duì)象實(shí)現(xiàn)迭代功能。實(shí)際上任何一個(gè)類(lèi),如果實(shí)現(xiàn)了`__getitem__` 方法,那么當(dāng)調(diào)用 iter(類(lèi)實(shí)例) 時(shí)候會(huì)自動(dòng)具備`__iter__` 和 `__next__`方法,從而可迭代了。

          ? ? 通過(guò)下面例子可以看出,`__getitem__` 實(shí)際上是屬于 __iter__` 和 `__next__` 方法的高級(jí)封裝,也就是我們常說(shuō)的語(yǔ)法糖,只不過(guò)這個(gè)轉(zhuǎn)化是通過(guò)編譯器完成,內(nèi)部自動(dòng)轉(zhuǎn)化,非常方便。

          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
          # 等價(jià)于?for?..?in?..while True: try:????????# 然后調(diào)用對(duì)象的?__next__?方法,不斷返回元素 value = next(cls_a) print(value) # 如果迭代完成,則捕獲異常即可 except StopIteration: break
          # 輸出:1 2 3

          而且 `__getitem__` 還可以通過(guò)索引直接訪問(wèn)元素,非常方便

          a[0]?#?1??a[4] # 錯(cuò)誤,索引越界

          如果你想該對(duì)象具備 list 等對(duì)象一樣的長(zhǎng)度屬性,則只需要實(shí)現(xiàn) `__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

          ? ? 到目前為止,我們已經(jīng)知道了第一種高級(jí)語(yǔ)法糖實(shí)現(xiàn)迭代器功能,下面分析另一個(gè)更簡(jiǎn)單的可以直接作用于函數(shù)的語(yǔ)法糖。


          1.5 yield 生成器

          ? ? 生成器是一個(gè)在行為上和迭代器非常類(lèi)似的對(duì)象,二者功能上差不多,但是生成器更優(yōu)雅,只需要用關(guān)鍵字 yield 來(lái)返回,作用于函數(shù)上叫生成器函數(shù),函數(shù)被調(diào)用時(shí)會(huì)返回一個(gè)生成器對(duì)象,生成器本質(zhì)就是迭代器,其最大特點(diǎn)是代碼簡(jiǎn)潔。

          def func():    for a in [1, 2, 3]:        yield a
          cls_g = func()print(isinstance(cls_g, Iterator)) # Trueprint(dir(cls_g)) # 自動(dòng)具備 __iter__ 和 __next__ 方法
          for a in cls_g: print(a)
          # 輸出: 1 2 3
          # 一種更簡(jiǎn)單的寫(xiě)法是用 ()cls_g = (i for i in [1,2,3])

          ? ? 直觀感覺(jué)和 `__getitem__` 一樣,也是高級(jí)語(yǔ)法糖,但是比 `__getitem__` 更加簡(jiǎn)單,更加好用。

          ? ? 使用 yield 函數(shù)與使用 return 函數(shù),在執(zhí)行時(shí)差別在于:包含 yield 的方法一般用于迭代,每次執(zhí)行時(shí)遇到 yield 就返回 yield 后的結(jié)果,但內(nèi)部會(huì)保留上次執(zhí)行的狀態(tài),下次繼續(xù)迭代時(shí),會(huì)繼續(xù)執(zhí)行 yield 之后的代碼,直到再次遇到 yield 后返回。生成器是懶加載模式,特別適合解決內(nèi)存占用大的集合問(wèn)題。假設(shè)創(chuàng)建一個(gè)包含10萬(wàn)個(gè)元素的列表,如果用 list 返回不僅占用很大的存儲(chǔ)空間,如果我們僅僅需要訪問(wèn)前面幾個(gè)元素,那后面絕大多數(shù)元素占用的空間都白白浪費(fèi)了,這種場(chǎng)景就適合采用生成器,在迭代過(guò)程中推算出后續(xù)元素,而不需要一次性全部算出。


          1.6 小結(jié)

          • ?list set dict等內(nèi)置對(duì)象都是容器 container 對(duì)象,容器是一種把多個(gè)元素組織在一起的數(shù)據(jù)結(jié)構(gòu),可以逐個(gè)迭代獲取其中的元素。容器可以用 in 來(lái)判斷容器中是否包含某個(gè)元素。大多數(shù)容器都是可迭代對(duì)象,可以使用某種方式訪問(wèn)容器中的每一個(gè)元素。

          • 在迭代對(duì)象基礎(chǔ)上,如果實(shí)現(xiàn)了 `__next__`? 方法則是迭代器對(duì)象,該對(duì)象在調(diào)用 next()? 的時(shí)候返回下一個(gè)值,如果容器中沒(méi)有更多元素了,則拋出 StopIteration 異常。

          • 對(duì)于采用語(yǔ)法糖 `__getitem__` 實(shí)現(xiàn)的迭代器對(duì)象,其本身實(shí)例既不是可迭代對(duì)象,更不是迭代器,但是其可以被 for in 迭代,原因是對(duì)該對(duì)象采用 iter(類(lèi)實(shí)例) 操作后就會(huì)自動(dòng)變成迭代器。

          • 生成器是一種特殊迭代器,但是不需要像迭代器一樣實(shí)現(xiàn)`__iter__`和`__next__`方法,只需要使用關(guān)鍵字 yield 就可以,生成器的構(gòu)造可以通過(guò)生成器表達(dá)式 (),或者對(duì)函數(shù)返回值加入 yield 關(guān)鍵字實(shí)現(xiàn)。

          • 對(duì)于在類(lèi)的 `__iter__` 方法中采用語(yǔ)法糖 yield 實(shí)現(xiàn)的迭代器對(duì)象,其本身實(shí)例是可迭代對(duì)象,但不是迭代器,但是其可以被 for .. in .. 迭代,原因是對(duì)該對(duì)象采用 iter(類(lèi)實(shí)例) 操作后就會(huì)自動(dòng)變成迭代器。


          2 DataLoader 最簡(jiǎn)版本 V1

          ? ? 這里說(shuō)的最簡(jiǎn)版本是指:沒(méi)有任何花哨、高級(jí)實(shí)現(xiàn)技巧,僅僅以實(shí)現(xiàn)最基礎(chǔ)功能為目的。具體來(lái)說(shuō)是包括必備的5個(gè)對(duì)象:Dataset、Sampler、BatchSampler、DataLoader 和 collate_fn。其作用可以簡(jiǎn)要描述為如下:


          • Dataset 提供整個(gè)數(shù)據(jù)集的隨機(jī)訪問(wèn)功能,每次調(diào)用都返回單個(gè)對(duì)象,例如一張圖片和對(duì)應(yīng) target 等等

          • Sampler 提供整個(gè)數(shù)據(jù)集隨機(jī)訪問(wèn)的索引列表,每次調(diào)用都返回所有列表中的單個(gè)索引,常用子類(lèi)是 SequentialSampler 用于提供順序輸出的索引 和 RandomSampler 用于提供隨機(jī)輸出的索引

          • BatchSampler 內(nèi)部調(diào)用 Sampler 實(shí)例,輸出指定 `batch_size` 個(gè)索引,然后將索引作用于 Dataset 上從而輸出 `batch_size` 個(gè)數(shù)據(jù)對(duì)象,例如 batch 張圖片和 batch 個(gè) target

          • collate_fn 用于將 batch 個(gè)數(shù)據(jù)對(duì)象在 batch 維度進(jìn)行聚合,生成 (b,...) 格式的數(shù)據(jù)輸出,如果待聚合對(duì)象是 numpy,則會(huì)自動(dòng)轉(zhuǎn)化為 tensor,此時(shí)就可以輸入到網(wǎng)絡(luò)中了


          迭代一次偽代碼如下(非迭代器版本):

          class DataLoader(object):    def __init__(self):        # 假設(shè)數(shù)據(jù)長(zhǎng)度是100,batch_size 是4        self.dataset = [[img0, target0], [img1, target1], ..., [img99, target99]]        # 假設(shè) sampler 是 SequentialSampler,那么實(shí)際上就是 [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 維度聚合數(shù)據(jù) 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 輸出,實(shí)際上就是 BatchSampler 做的事情 i = 0 batch_index = [] while i < self.batch_size: # 內(nèi)部會(huì)調(diào)用 sampler 對(duì)象取單個(gè)索引 batch_index.append(self.sampler[self.index]) self.index += 1 i += 1
          # 1.得到 batch 個(gè)數(shù)據(jù)了,調(diào)用 dataset 對(duì)象 data = [self.dataset[idx] for idx in batch_index]
          # 2. 調(diào)用 collate_fn 在 batch 維度拼接輸出 batch_data = self.collate_fn(data) return batch_data
          def __iter__(self): return self

          ? ? 以上就是最抽象的 DataLoader 運(yùn)行流程以及和 Dataset、Sampler、BatchSampler、collate_fn 的關(guān)系。


          2.1 整體對(duì)象理解

          ? ? 首先需要強(qiáng)調(diào)的是 Dataset、Sampler、BatchSampler 和 DataLoader 都直接或間接實(shí)現(xiàn)了迭代器,你必須要先理解第一小節(jié)內(nèi)容,否則本節(jié)內(nèi)容會(huì)比較難理解,具體為:


          • ?Dataset 通過(guò)實(shí)現(xiàn) `__getitem__` 方法使其可迭代

          • ?Sampler 對(duì)象是一個(gè)可迭代的基類(lèi)對(duì)象,其常用子類(lèi) SequentialSampler 在 `__iter__` 內(nèi)部返回迭代器,RandomSampler 在 `__iter__` 內(nèi)部通過(guò) yield 關(guān)鍵字返回迭代器

          • ?BatchSampler 也是在 `__iter__` 內(nèi)部通過(guò) yield 關(guān)鍵字返回迭代器

          • ?DataLoader 通過(guò)直接實(shí)現(xiàn) `__next__` 和 `__iter__` 變成迭代器


          ? ? 注意除了 DataLoader 本身是迭代器外,其余對(duì)象本身不是迭代器,但是都能被 for .. in .. 迭代。下面一個(gè)簡(jiǎn)單例子證明:

          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)) # True
          print(isinstance(SequentialSampler(simple_dataset), Iterable)) # Trueprint(isinstance(SequentialSampler(simple_dataset), Iterator)) # Falseprint(isinstance(iter(SequentialSampler(simple_dataset)), Iterator)) # True
          # BatchSampler?和?RandomSampler?內(nèi)部實(shí)現(xiàn)結(jié)構(gòu)一樣,結(jié)果也是一樣print(isinstance(RandomSampler(simple_dataset), Iterable)) # Trueprint(isinstance(RandomSampler(simple_dataset), Iterator)) # Falseprint(isinstance(iter(RandomSampler(simple_dataset)),?Iterator))?#?True
          print(isinstance(dataloader, Iterator)) # True

          ? ? 在 DataLoader 中主要涉及3個(gè)類(lèi),其內(nèi)部實(shí)例傳遞關(guān)系如下:

          ? ? 由于 DataLoader 類(lèi)寫(xiě)的非常通用,故 Dataset、Sampler、BatchSampler 都可以外部傳入,除了 Dataset 必須輸入外,其余兩個(gè)類(lèi)都有默認(rèn)實(shí)現(xiàn),最典型的 Sampler 就是 SequentialSampler 和 RandomSampler。

          ? ? 需要注意的是 Sampler 對(duì)象其實(shí)在大部分時(shí)候都不需要傳入 Dataset 實(shí)例對(duì)象,因?yàn)槠涔δ軆H僅是返回索引而已,并沒(méi)有直接接觸數(shù)據(jù)。


          2.2 DataLoader 運(yùn)行流程

          ? ? 最簡(jiǎn)單版本 DataLoader,具備如下功能:


          • Dataset 內(nèi)部返回需要是 numpy 或者 tensor 對(duì)象

          • Sampler 直接 SequentialSampler 和 RandomSampler

          • BatchSampler 已經(jīng)實(shí)現(xiàn)

          • collate_fn 僅僅考慮了 numpy 或者 tensor 對(duì)象

          • 僅僅支持 num_works=0 即單進(jìn)程


          看起來(lái)功能非常單一,但是其實(shí)已經(jīng)搭建起了整個(gè)框架,理解了這個(gè)最簡(jiǎn)框架才能去理解高級(jí)實(shí)現(xiàn),其核心運(yùn)行邏輯為:

          def?__next__(self):????# 返回?batch?個(gè)索引????index?=?next(self.batch_sampler)????# 利用索引去取數(shù)據(jù)????data?=?[self.dataset[idx]?for?idx?in?index????# batch?維度聚合????data?=?self.collate_fn(data)????return?data

          然后為了方便大家理解,特意繪制了如下代碼運(yùn)行流程圖:

          ? ? 還是那句話:一定要對(duì)第1小節(jié)內(nèi)容非常熟悉,否則里面這么多迭代器、生成器的調(diào)用,可能會(huì)把你繞暈。詳細(xì)代碼描述如下:


          1. `self.batch_sampler = iter(batch_sampler)`。在 DataLoader 的類(lèi)初始化,需要得到 BatchSampler 的迭代器對(duì)象

          2. `index = next(self.batch_sampler)`。對(duì)于每次迭代,DataLoader 對(duì)象首先會(huì)調(diào)用 BatchSampler 的迭代器進(jìn)行下一次迭代,具體是調(diào)用 BatchSampler 對(duì)象的? `__iter__`? 方法

          3. 而 BatchSampler 對(duì)象的 `__iter__` 方法實(shí)際上是需要依靠 Sampler 對(duì)象進(jìn)行迭代輸出索引,Sampler 對(duì)象也是一個(gè)迭代器,當(dāng)?shù)?`batch_size` 次后就可以得到 `batch_size` 個(gè)數(shù)據(jù)索引

          4. `data = [self.dataset[idx] for idx in index]`。有了 batch 個(gè)索引就可以通過(guò)不斷調(diào)用? dataset 的 `__getitem__` 方法返回?cái)?shù)據(jù)對(duì)象,此時(shí) data 就包含了 batch 個(gè)對(duì)象

          5. `data = self.collate_fn(data)`。將 batch 個(gè)對(duì)象輸入給聚合函數(shù),在第0個(gè)維度也就是 batch 維度進(jìn)行聚合,得到類(lèi)似 (b,...) 的對(duì)象

          6. 不斷重復(fù)1-5步,就可以不斷的輸出一個(gè)一個(gè) batch 的數(shù)據(jù)了


          以上就是完整流程,如果理解有困難,你可以先看下一小結(jié)的代碼實(shí)現(xiàn),然后再返回去理解。


          2.3 最簡(jiǎn)V1版本源代碼


          (1) Dataset

          class?Dataset(object):????# 只要實(shí)現(xiàn)了?__getitem__?方法就可以變成迭代器    def __getitem__(self, index):          raise NotImplementedError     # 用于獲取數(shù)據(jù)集長(zhǎng)度     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 NotImplementedError


          class?SequentialSampler(Sampler): 
          def __init__(self, data_source): super(SequentialSampler, self).__init__(data_source) self.data_source = data_source
          def __iter__(self): # 返回迭代器,不然無(wú)法 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 = [] # 調(diào)用 sampler 內(nèi)部的迭代器對(duì)象 for idx in self.sampler: batch.append(idx) # 如果已經(jīng)得到了 batch 個(gè) 索引,則可以通過(guò) yield # 關(guān)鍵字生成生成器返回,得到迭代器對(duì)象 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: # 如果最后的索引數(shù)不夠一個(gè) 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    
          # 因?yàn)檫@兩個(gè)功能是沖突的,假設(shè) shuffle=True, # 但是 sampler 里面是 SequentialSampler,那么就違背設(shè)計(jì)思想了 if sampler is not None and shuffle: raise ValueError('sampler option is mutually exclusive with ' 'shuffle')
          if batch_sampler is not None: # 一旦設(shè)置了 batch_sampler,那么 batch_size、shuffle、sampler # 和 drop_last 四個(gè)參數(shù)就不能傳入 # 因?yàn)檫@4個(gè)參數(shù)功能和 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)
          # 也就是說(shuō) batch_sampler 必須要存在,你如果沒(méi)有設(shè)置,那么采用默認(rèn)類(lèi) 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
          # 返回自身,因?yàn)樽陨韺?shí)現(xiàn)了 __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) 調(diào)用完整例子

          class SimpleV1Dataset(Dataset):     def __init__(self):          # 偽造數(shù)據(jù)          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 總結(jié)

          ? ? 本文是最小 DataLoader 系列文章的第一篇,重點(diǎn)是分析了 python 中迭代器相關(guān)知識(shí),然后構(gòu)建一個(gè)最簡(jiǎn)單的 DataLoader 類(lèi),用于加深到 DataLoader 流程的理解,功能比較簡(jiǎn)單。

          ? ? 后面慢慢完善,希望最終能實(shí)現(xiàn)完整功能。


          github: https://github.com/hhaAndroid/miniloader


          推薦閱讀

          PyTorch 源碼解讀之 torch.autograd

          PyTorch 源碼解讀之 BN & SyncBN



          機(jī)器學(xué)習(xí)算法工程師


          ? ??? ? ? ? ? ? ? ? ? ? ? ??????????????????一個(gè)用心的公眾號(hào)


          ?


          瀏覽 43
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  开心五月天激情成人网 | 免费爱爱网址 | 学生妹毛片视频 | 中国性老太HD大全120 | 欧美日韩一级黄色片 |