歡迎您光臨本站 註冊首頁

generator訓練模型(節省記憶體)

←手機掃碼閱讀     zhang3221994 @ 2020-06-18 , reply:0

前言

前段時間在訓練模型的時候,發現當訓練集的數量過大,並且輸入的圖片維度過大時,很容易就超記憶體了,舉個簡單例子,如果我們有20000個樣本,輸入圖片的維度是224x224x3,用float32儲存,那麼如果我們一次性將全部資料載入記憶體的話,總共就需要20000x224x224x3x32bit/8=11.2GB 這麼大的記憶體,所以如果一次性要載入全部資料集的話是需要很大記憶體的。

如果我們直接用keras的fit函式來訓練模型的話,是需要傳入全部訓練資料,但是好在提供了fit_generator,可以分批次的讀取資料,節省了我們的記憶體,我們唯一要做的就是實現一個生成器(generator)。

1.fit_generator函式簡介

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

 

引數:

generator:一個生成器,或者一個 Sequence (keras.utils.Sequence) 物件的例項。這是我們實現的重點,後面會著介紹生成器和sequence的兩種實現方式。

steps_per_epoch:這個是我們在每個epoch中需要執行多少次生成器來生產資料,fit_generator函式沒有batch_size這個引數,是透過steps_per_epoch來實現的,每次生產的資料就是一個batch,因此steps_per_epoch的值我們透過會設為(樣本數/batch_size)。如果我們的generator是sequence型別,那麼這個引數是可選的,預設使用len(generator) 。

epochs:即我們訓練的迭代次數。

verbose:0, 1 或 2。日誌顯示模式。 0 = 安靜模式, 1 = 進度條, 2 = 每輪一行

callbacks:在訓練時呼叫的一系列回撥函式。

validation_data:和我們的generator類似,只是這個使用於驗證的,不參與訓練。

validation_steps:和前面的steps_per_epoch類似。

class_weight:可選的將類索引(整數)對映到權重(浮點)值的字典,用於加權損失函式(僅在訓練期間)。 這可以用來告訴模型「更多地關注」來自代表性不足的類的樣本。(感覺這個引數用的比較少)

max_queue_size:整數。生成器佇列的最大尺寸。預設為10.

workers:整數。使用的最大程序數量,如果使用基於程序的多執行緒。 如未指定,workers 將預設為 1。如果為 0,將在主執行緒上執行生成器。

use_multiprocessing:布林值。如果 True,則使用基於程序的多執行緒。預設為False。

shuffle:是否在每輪迭代之前打亂 batch 的順序。 只能與Sequence(keras.utils.Sequence) 例項同用。

initial_epoch: 開始訓練的輪次(有助於恢復之前的訓練)

2.generator實現

2.1生成器的實現方式

樣常式式碼:

  import keras  from keras.models import Sequential  from keras.layers import Dense  import numpy as np  from sklearn.model_selection import train_test_split  from PIL import Image    def process_x(path):   img = Image.open(path)   img = img.resize((96,96))   img = img.convert('RGB')   img = np.array(img)     img = np.asarray(img, np.float32) / 255.0   #也可以進行進行一些資料資料增強的處理   return img    count =1  def generate_arrays_from_file(x_y):   #x_y 是我們的訓練集包括標籤,每一行的第一個是我們的圖片路徑,後面的是我們的獨熱化後的標籤     global count   batch_size = 8   while 1:    batch_x = x_y[(count - 1) * batch_size:count * batch_size, 0]    batch_y = x_y[(count - 1) * batch_size:count * batch_size, 1:]      batch_x = np.array([process_x(img_path) for img_path in batch_x])    batch_y = np.array(batch_y).astype(np.float32)    print("count:"+str(count))    count = count+1    yield (batch_x, batch_y)    model = Sequential()  model.add(Dense(units=1000, activation='relu', input_dim=2))  model.add(Dense(units=2, activation='softmax'))  model.compile(loss='categorical_crossentropy',optimizer='sgd',metrics=['accuracy'])    x_y = []  model.fit_generator(generate_arrays_from_file(x_y),steps_per_epoch=10, epochs=2,max_queue_size=1,workers=1)

 

在理解上面程式碼之前我們需要首先了解yield的用法。

yield關鍵字:

我們先透過一個例子看一下yield的用法:

  def foo():   print("starting...")   while True:    res = yield 4    print("res:",res)  g = foo()  print(next(g))  print("----------")  print(next(g))

 

執行結果:

  starting...  4  ----------  res: None  4

 

帶yield的函式是一個生成器,而不是一個函式。因為foo函式中有yield關鍵字,所以foo函式並不會真的執行,而是先得到一個生成器的例項,當我們第一次呼叫next函式的時候,foo函式才開始行,首先先執行foo函式中的print方法,然後進入while迴圈,迴圈執行到yield時,yield其實相當於return,函式返回4,程式停止。所以我們第一次呼叫next(g)的輸出結果是前面兩行。

然後當我們再次呼叫next(g)時,這個時候是從上一次停止的地方繼續執行,也就是要執行res的賦值操作,因為4已經在上一次執行被return了,隨意賦值res為None,然後執行print(「res:」,res)列印res: None,再次迴圈到yield返回4,程式停止。

所以yield關鍵字的作用就是我們能夠從上一次程式停止的地方繼續執行,這樣我們用作生成器的時候,就避免一次性讀入資料造成記憶體不足的情況。

現在看到上面的示常式式碼:

generate_arrays_from_file函式就是我們的生成器,每次迴圈讀取一個batch大小的資料,然後處理資料,並返回。x_y是我們的把路徑和標籤合併後的訓練集,類似於如下形式:

['data/imgfimg_4092.jpg' '0' '1' '0' '0' '0' ]

至於格式不一定要這樣,可以是自己的格式,至於怎麼處理,根於自己的格式,在process_x進行處理,這裡因為是存放的圖片路徑,所以在process_x函式的主要作用就是讀取圖片並進行歸一化等操作,也可以在這裡定義自己需要進行的操作,例如對影象進行實時資料增強。

2.2使用Sequence實現generator

示常式式碼:

  class BaseSequence(Sequence):   """   基礎的資料流生成器,每次迭代返回一個batch   BaseSequence可直接用於fit_generator的generator引數   fit_generator會將BaseSequence再次封裝為一個多程序的資料流生成器   而且能保證在多程序下的一個epoch中不會重複取相同的樣本   """   def __init__(self, img_paths, labels, batch_size, img_size):    #np.hstack在水平方向上平鋪    self.x_y = np.hstack((np.array(img_paths).reshape(len(img_paths), 1), np.array(labels)))    self.batch_size = batch_size    self.img_size = img_size     def __len__(self):    #math.ceil表示向上取整    #呼叫len(BaseSequence)時返回,返回的是每個epoch我們需要讀取資料的次數    return math.ceil(len(self.x_y) / self.batch_size)     def preprocess_img(self, img_path):      img = Image.open(img_path)    resize_scale = self.img_size[0] / max(img.size[:2])    img = img.resize((self.img_size[0], self.img_size[0]))    img = img.convert('RGB')    img = np.array(img)      # 資料歸一化    img = np.asarray(img, np.float32) / 255.0    return img     def __getitem__(self, idx):    batch_x = self.x_y[idx * self.batch_size: (idx + 1) * self.batch_size, 0]    batch_y = self.x_y[idx * self.batch_size: (idx + 1) * self.batch_size, 1:]    batch_x = np.array([self.preprocess_img(img_path) for img_path in batch_x])    batch_y = np.array(batch_y).astype(np.float32)    print(batch_x.shape)    return batch_x, batch_y   #重寫的父類Sequence中的on_epoch_end方法,在每次迭代完後呼叫。   def on_epoch_end(self):    #每次迭代後重新打亂訓練集資料    np.random.shuffle(self.x_y)

 

在上面程式碼中,__len __和__getitem __,是我們重寫的魔法方法,__len __是當我們呼叫len(BaseSequence)函式時呼叫,這裡我們返回(樣本總量/batch_size),供我們傳入fit_generator中的steps_per_epoch引數;__getitem __可以讓物件實現迭代功能,這樣在將BaseSequence的物件傳入fit_generator中後,不斷執行generator就可迴圈的讀取資料了。

舉個例子說明一下getitem的作用:

  class Animal:   def __init__(self, animal_list):    self.animals_name = animal_list     def __getitem__(self, index):    return self.animals_name[index]    animals = Animal(["dog","cat","fish"])  for animal in animals:   print(animal)

 

輸出結果:

  dog  cat  fish

 

並且使用Sequence類可以保證在多程序的情況下,每個epoch中的樣本只會被訓練一次。



[zhang3221994 ] generator訓練模型(節省記憶體)已經有370次圍觀

http://coctec.com/docs/program/show-post-238978.html