精選文章

為何需要使用fit_generator?以及如何使用fit_generator? / 機器學習之Tensorflow的使用


衍皙打完一個模型的程式碼,準備用來判斷良惡性腫瘤。他吁了一口氣,輸入執行的指令後,起身活動一下筋骨,轉身走去泡杯咖啡,等熱水開的時間,衍皙走回電腦前面,想看一下這次的訓練情況,但走近一看,看到的卻不是預想中一排epoch的訓練結果。
他頓了一下,臉色糾結的看著螢幕上ResourceExhaustedError的錯誤訊息,嘆了口氣後坐下,喃喃自語道:「這樣的資料量就會記憶體爆炸了嗎?」,沉默了一下,移動滑鼠點開了瀏覽器視窗,開始搜尋關於「ResourceExhaustedError solution」的資料,其中有調小batch_size、清理顯示卡內存等方法,衍皙看到了關於keras的.fit_generator的介紹,他點了進去…

現在提到人工智慧中機器學習的領域,幾乎都會使用Tensorflow平台。其中有部分人在第一次接觸機器學習的時候,都會用keras程式庫,來降低撰寫程式的難度。
在訓練神經網路模型時,當網路的層數加深,節點(node)數目增多時,參數的量會達到非常驚人的地步;或者資料集(dataset)很龐大,單一樣本的尺寸過大,都有可能出現記憶體不足的情況。
這篇文章主要在探討當資料集過於龐大,如何有效的降低資料佔據記憶體的空間。
在閱讀完這篇文章後,你會了解到:
  • 產生器(generator)是一種副程式,不同於一般副程式執行到return後,會將副程式完全結束掉,當產生器執行到yield後,會將副程式狀態保留,暫停至下一次呼叫。
  • 使用python的產生器,可以簡單地拿到每回執行的數值。
  • 自行寫出一個樣本(sample)的產生器,除了需要寫出取得樣本的路徑的程式之外,還要自行寫出資料洗牌(shuffle)的方式,以及將樣本分批次(batch)的程式。

文章分段

這篇文章分成五個部分,分別是:
  1. Python的產生器(Generator)是什麼?
  2. 迥異於回傳(return)的產出(yield)用法
  3. 用於解決的函數( .fit_generator() )的參數意義
  4. 機器學習的資料集是如何產生的?
  5. 簡單可執行的程式碼(skeleton code)
  6. 產生器的資料在何時進行洗牌(shuffle)

Python的產生器(Generator)是什麼?

當我們在用副程式執行,運行到程式的最後一行時,會將最後算好的數值(value)回傳(return)。如果程式中有迴圈(loop)的話,回傳只會拿到一個值,而且是整個迴圈結束後,最終計算好的數值。
這時候,如果想要取得不同迴圈次數下的數值,最基本的想法是將每次迴圈的數值用陣列之類的變數儲存,而非單一變數。如果回傳數量不一定,就會需要動態宣告,並且可能會花太多空間儲存。
又或者可以自己寫一個函數,能夠在每次更新副程式內部的數值時,透過作業系統的中斷和恢復指令,達到取得每次更新的數值的目的,但這樣做的話,程式可能會很冗長,而且很複雜。
不過如果用Python語言的話,就不用這麼麻煩,因為他已經幫我們寫好這樣的機制了,只要將副程式的回傳(return)改成產出(yield),就可以取得每次更新的數值。
這樣含有產出(yield)陳述句(statement)的副程式即被稱為產生器(Generator)。

迥異於回傳(return)的產出(yield)用法

想要在程式中用到產生器(generator),只要在想呼叫的副程式中用到至少一個yield,就可以了。
yieldreturn一樣,副程式都有回傳值,不同的是,程式執行到return時,會將副程式完全結束掉;而執行到yield則是將副程式暫停,並且保留執行狀態,等待接續的呼叫
設想以下例子出來的結果,並實際執行看看。
random建立原始資料trainData,由10個數值為0~9的元素所組成的list。
import random trainData = random.sample(range(10), 10)

接著用產生器(generator),將trainData的資料預處理過。
處理的方式是判斷奇偶數,將資料傳入getProperty()產生器,經過判斷後,再將添加的性質與資料一併產出(yield)。
範例產生器傳回的是dict型態的變數,鍵值(key)的部分包含資料(Data)、奇偶數(Parity),透過呼叫getProperty()產生器,可取得原始資料及其奇偶性質。
若想回傳的是tuple型態的變數,將大括號與鍵值的部分刪去。
def getProperty(data): for el in data: if el % 2 == 0: yield {'Data': el, 'Parity': 'even'} else: yield {'Data': el, 'Parity': 'odd'}

第9行將變數p指向帶有引數(argument)的產生器,如此一來,p就變為有明確資料的物件(object),接著用Python語言本來就提供的產生器物件的next()方法(method),取得每回的資料。
p = getProperty(trainData) print(trainData)

注意,如果是採用互動式的python環境,只需重複執行下方程式碼。上方的程式碼,尤其是第9行,切勿再重複執行,以免覆蓋掉p物件的區域變數(local variable)和狀態(state)。
print(next(p)) print(next(p)) print(next(p)) ... print(next(p))
以下為範例輸出:
> [3, 1, 8, 5, 4, 6, 0, 9, 7, 2]
> {'Data': 3, 'Parity': 'odd'}
> {'Data': 1, 'Parity': 'odd'}
> {'Data': 8, 'Parity': 'even'}
> ...
> -----------------------------------------------
  StopIteration   Traceback (most recent call last)

直接呼叫會出現StopIteration,產生器輪過資料一圈即停止,想要再次取得資料的話,就要再次執行第9行。
也可以將第9行之後改寫,不用變數存產生器的物件(object),好處是能夠一直呼叫,不會出現StopIteration的情況。
for n in getProperty(trainData): print(n)
更多的範例以及詳細解釋可去 www.programiz.com 上查看。

了解了產生器的運作方式後,是不是覺得產生器就像扭蛋機一樣,裡面有許多球,只有你去扭它時,它才會丟一顆球給你。
依樣畫葫蘆,將資料集的樣本路徑都準備好,在需要的時候才透過路徑將實際的樣本載入,這樣占空間的只有那一個個字串(string)而已,大大的節省了許多空間。

用於解決的函數( .fit_generator() )的參數意義

接著我們來談談,在寫機器學習的程式時,實際使用產生器,來減輕運算時的資源消耗,所用到的方法(method)。
這裡是用keras框架的.fit_generator()做介紹,更多詳細說明都在官方的說明文件裡,如果有說明不清楚的部分,請以官方說明為準。
下一段會有實際的程式碼演練,在此之前,先來了解.fit_generator()的超參數(hyperparameter)意義。
接下來,如果內文會交錯提到模型在訓練時,模型的參數更新(update),或者模型經過學習(learn),兩者指的是同樣的意思,都是指模型的參數有調整,數值有變動的意思。

fit_generator(generator, steps_per_epoch=None, epochs=1, verbose=1, callbacks=None, validation_data=None, validation_steps=None, validation_freq=1, class_weight=None, max_queue_size=10, workers=1, use_multiprocessing=False, shuffle=True, initial_epoch=0)

引數(argument):

  • generator:
    這個引數要放之前指向好的生產器的物件(object),例如文章上一段的p
    要注意的是,用於模型參數更新的生產器,傳入模型的資料是樣本(sample),而且要按照tuple型態,通常含兩個元素,(輸入(inputs),目標(targets)),有時會有三個元素。
    (輸入,目標)的兩個元素的第一對即第一個樣本,第二對即第二個樣本,以此類推。(所以文件(doc)的變數有加s)。一個樣本就是機器學習的一個單位。
    以判斷肺腫瘤良惡性為例,輸入是肺部分的2維或3維影像。
    目標根據訓練需求會有不同型態:
    1. 如果單純分0為良性、1為惡性,則目標就是0或1的真值(ground truth);
    2. 而如果是用於判斷切割影像,則目標就也會跟輸入一樣,是2維或3維影像,不同的是它是配對輸入的已切割好的真值(ground truth)。
    在繼續下去之前,如果對batch、epoch等概念不是很熟的話,可以參考Jason Brownlee的What is the Difference Between a Batch and an Epoch in a Neural Network?,下面也會有簡單的說明。
    另外,生產器傳回的樣本(sample)即被視為一個批次(batch),模型在經過一個批次後參數會更新。
    產生器總回傳的樣本數量——或者說一個批次所含的樣本數——即為批次量(batch size)。這個"量"代表的是「模型(model)經過"幾個樣本"輸入後,模型的參數(parameter)會更新」。
    也就是說一個批次的樣本數量其實可以>=1,模型可以選擇在看到一個樣本就進行學習,也可以在看到多個樣本後綜合資訊再學習。
  • steps_per_epoch:
    這個引數放的是整數,是.fit()方法沒有的,這個引數比較不好理解,而且意思有調整過,現在的意思是:
    • 完成一次的epoch,總共要執行生產器中的yield"幾次"。
    也等同
    • 完成一次的epoch,總共要用next()呼叫生產器物件"幾次"。
    也是
    • 完成一次的epoch,總共會經過幾個批次(batches)。
    關於這個數字的調整,還需要解釋epoch的意義,現在就接著往下看吧。
  • epochs:
    期(epochs)引數放的也是整數。一期(epoch)代表「指定的訓練資料集(training dataset)中的每一個樣本(sample),都"參與過一次"模型參數的更新」的意思。
    也就是說,多期(epoch),表示資料集中的樣本,會參與"多次"更新模型參數的過程。

    小小的總結一下模型參數更新的時機。一至多個樣本(sample)組成一個批次(batch),一個批次經過模型,模型的參數就會更新,我們說這叫一次迭代(iteration);當所有的批次都經過模型一次,也就是進行過多次迭代後,我說這叫模型經過了一期(one epoch)的學習。
    [img]
    所以模型參數更新的次數,取決於:
    1. 樣本被分成多少個批次(batch)
    2. 有多少期(epoch)
    兩個超參數(hyperparameter),四種情況,多多、多少、少多、少少。我這裡不下結論說明,哪種情況是最好的,但我會給出三個網頁,讓你們對如何調整這兩個超參數有點概念。

epoch的意思也知道後,回過來說明steps_per_epoch就會比較明確,其實它跟.fit()的批次量(batch_size)參數是相輔的概念,因為一旦決定好批次量,就可以算出會有幾個批次;同理,決定好一期(epoch)中要有幾個批次,也就可以算出批次量是多少。
不過,如果你問我為什麼.fit()是用batch_size設定?為什麼.fit_generator()要用steps_per_epoch設定?我就不清楚了。
原本還有多個引數(argument),因為寫得有點累,因為礙於篇幅,所以先只取剛入門需要了解的部分做說明。
引數的部分就先說明到這裡為止。
基本用到的引數都說明過了,說了這麼多,如果沒有一個實際的例子,還是會有種摸不到邊的感覺。
前文有說,生產器傳回的樣本數可以>=1,問題是我要怎麼做?答案是:自己寫,自己訂!
不過在產生樣本之前,我想必須要給初入這個領域,還沒有感受到蒐集資料的困難度的人一些經驗。

機器學習的資料集是如何產生的?

程式在執行時,訓練模型的步驟就是:
  1. 向產生器拿一批次的樣本,產生器將這次要用的樣本載入記憶體
  2. 將批次中的樣本中的輸入(input)從輸入層(input layer)放入
  3. 經過隱藏層(hidden layer)
  4. 從輸出層(output layer)輸出結果(output)
  5. 用函數計算樣本中的目標(target)與輸出結果的誤差(loss)
  6. 再利用同一個函數的微分搭配連鎖律,由輸出層開始往輸入層的方向,由後往前,逐一更新參數
  7. 重複以上步驟直到訓練完成
訓練就是不停地拿樣本來上模型學習,所以在訓練開始之前,就要決定模型每次迭代時,樣本是如何提供的。
所以阿,機器學習除了要傷腦筋模型的架構,要搭幾層,每層要多少個filter或節點之外,最頭痛的部分還是處理資料的部分。
個人認為處理資料要顧到幾個面向:
  1. 將原始資料處理成能夠用來訓練的樣本
    類似原油要經過提煉才能變成汽車能夠使用的汽油、原木要經過加工可以選擇變成木板或打漿製造家具或紙張等,各類的資料依據自身的特性及目的,加工成樣本,最後形成能用的資料集、資料庫。
    會發生的災難:
    • 原始資料缺漏太多:公開資料庫常常發生,只能含淚補零。
    • 原始資料太複雜,而我要的很簡單:要特別寫程式抽出要的資料,如果這部分不能程式代勞…加油。
    • 別人的資料集很好,但跟我的目標不一樣:自己動手,豐衣足食,體會到自己建資料集超緩慢,從hello world等級的手寫資料蒐集就是一項艱鉅的任務,人臉辨識從手動標註人臉眉眼鼻嘴的區域的工作開始就很累,更不要提需要有專業知識背景的醫師才能標註的醫學影像。一線醫生是要去救人的,沒時間跟你慢慢一起用電腦畫圈圈,問題是可憐沒執照的我們,圈出來的資料可信度很低。
  2. 取得足夠代表真實世界的樣本:
    類似應試教育,大考就是真實世界,樣本是學生學習的知識點、小考、考古題。大考說國文會考中國文化教材,該做就是不只看古文三十,教材也要看才行。
    會發生的災難:
    • 樣本太少,覆蓋不了全部範圍:有些樣本得來不易,像醫學影像的取得需要經過倫理隱私的限制之外,雖然世界每天看病的人很多,但是病也分很多種,現在這個階段,很少人得的疾病能夠達到的樣本數能夠到5、600的數量已經很了不起了。
    • 樣本好多,但一看發現都好像:有些資料的性質很單一,但大部分不是,像是蒐集寶可夢,有時間和地區的因素,白天就是很難遇到鬼屬性的寶貝;你家附近有發電廠,就容易收集到雷電屬性的寶貝;又或者你家附近什麼都沒有,然後抓了一堆一般屬性的寶貝,都是有可能的。
    • 世界好大,範圍在哪我看不見:跟人生一樣難。
  3. 訓練模型時,樣本該以怎樣的順序進入:

簡單可執行的產生器的程式碼(skeleton code)

製作資料集的問題,大部分我都沒辦法幫助你,但是第3個面向,我想我可以提供你一些思路。

產生器的資料在何時進行洗牌(shuffle)

參考

留言

這個網誌中的熱門文章

COCO Dataset: 介紹、下載、取得方式、標註資料格式(key points)