前言
前段時間在訓練模型的時候,發現當訓練集的數量過大,并且輸入的圖片維度過大時,很容易就超內存了,舉個簡單例子,如果我們有20000個樣本,輸入圖片的維度是224x224x3,用float32存儲,那么如果我們一次性將全部數據載入內存的話,總共就需要20000x224x224x3x32bit/8=11.2GB 這么大的內存,所以如果一次性要加載全部數據集的話是需要很大內存的。
如果我們直接用keras的fit函數來訓練模型的話,是需要傳入全部訓練數據,但是好在提供了fit_generator,可以分批次的讀取數據,節省了我們的內存,我們唯一要做的就是實現一個生成器(generator)。
1.fit_generator函數簡介
1
2
3
4
5
6
7
8
9
10
11
12
13
|
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生成器的實現方式
樣例代碼:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
|
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的用法:
1
2
3
4
5
6
7
8
9
|
def foo(): print ( "starting..." ) while True : res = yield 4 print ( "res:" ,res) g = foo() print ( next (g)) print ( "----------" ) print ( next (g)) |
運行結果:
1
2
3
4
5
|
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/img\\fimg_4092.jpg' '0' '1' '0' '0' '0' ]
至于格式不一定要這樣,可以是自己的格式,至于怎么處理,根于自己的格式,在process_x進行處理,這里因為是存放的圖片路徑,所以在process_x函數的主要作用就是讀取圖片并進行歸一化等操作,也可以在這里定義自己需要進行的操作,例如對圖像進行實時數據增強。
2.2使用Sequence實現generator
示例代碼:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
|
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的作用:
1
2
3
4
5
6
7
8
9
10
|
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) |
輸出結果:
1
2
3
|
dog cat fish |
并且使用Sequence類可以保證在多進程的情況下,每個epoch中的樣本只會被訓練一次。
以上這篇淺談keras通過model.fit_generator訓練模型(節省內存)就是小編分享給大家的全部內容了,希望能給大家一個參考,也希望大家多多支持服務器之家。
原文鏈接:https://blog.csdn.net/CarryLvan/article/details/103837093