在對Keras框架的學(xué)習(xí)中,一個很大的難點(diǎn)就是數(shù)據(jù)的導(dǎo)入,尤其是當(dāng)數(shù)據(jù)不能一次放入內(nèi)存的時候,應(yīng)該如何導(dǎo)入的問題。在Keras的官網(wǎng),沒有章節(jié)特意講這個內(nèi)容,而專門去找資料,也很難找到相關(guān)的內(nèi)容。絕大多數(shù)的教程都是直接使用的Keras自帶的數(shù)據(jù)集。為了處理大量數(shù)據(jù)的情況,我還特意研究了Python的多線程。后來我還知道了導(dǎo)入數(shù)據(jù)的時候的隨機(jī)性的重要性等各種問題。這篇文章算是一個總結(jié)。
如果看過我前面的文章Keras入門與LeNet的實(shí)現(xiàn),應(yīng)該知道Keras里面有很多經(jīng)典的數(shù)據(jù)集。當(dāng)我們研究自己的模型的時候,只需要拿出其中與我們要研究的問題類似的數(shù)據(jù)集進(jìn)行試驗(yàn)就行了。比如之前提到的手寫數(shù)字識別的經(jīng)典數(shù)據(jù)集mnist。就只需要一行代碼:
(X_train, y_train), (X_test, y_test) = mnist.load_data()
就可以成功導(dǎo)入了。在Keras官網(wǎng)上面有各個常用數(shù)據(jù)庫的導(dǎo)入方法,這使得使用這些經(jīng)典的數(shù)據(jù)庫特別簡單。但是我們使用Keras是不完全是為了研究自己的model,還可能是為了解決實(shí)際問題。這個時候,我們就要創(chuàng)造自己的數(shù)據(jù)集,并且把數(shù)據(jù)集運(yùn)用到自己的模型之中。
創(chuàng)造數(shù)據(jù)集是一件比較難的事情。尤其是要創(chuàng)造大量的、靠譜的數(shù)據(jù)集。通常來說,很多數(shù)據(jù)集只能通過大公司去收集,而不能自己創(chuàng)造。但是凡事總有例外。有的時候我們還是能夠自己創(chuàng)造數(shù)據(jù)集的。例如我們的OCR。我們只需要把字符進(jìn)行變形就可以生成我們的數(shù)據(jù)集了。(當(dāng)然,如果要考慮手寫的順序等問題,這就不夠了。)關(guān)于生成字符識別數(shù)據(jù)集,請參考我之前的文章。
接著,我來說明我查到的第一種運(yùn)用自己的數(shù)據(jù)集的方法。這種方法需要一次性地把數(shù)據(jù)放入內(nèi)存,因此數(shù)據(jù)量不能過大。
原理也很簡單??紤]到Keras的輸入數(shù)據(jù)是numpy、float類型,因此我們只需要把圖片讀入,然后轉(zhuǎn)成numpy就行了。
首先,我們的目錄結(jié)構(gòu)是這樣的:
./words
./0
0.png
1.png
2.png
...
/1
0.png
1.png
2.png
...
/2
...
先寫一個讀圖片的函數(shù),我們用這個函數(shù)把Image類型轉(zhuǎn)成numpy類型。
def read_image(imageName):
im = Image.open(imageName).convert('L')
data = np.array(im)
return data
我們創(chuàng)造一個images的列表和labels的列表,用來存圖片和對應(yīng)的結(jié)果。接著,我們把圖片和它對應(yīng)的結(jié)果讀入:
# 讀取在words里面有幾個文件夾
text = os.listdir('./words')
# 把文件夾里面的圖片和其對應(yīng)的文件夾的名字也就是對應(yīng)的字
for textPath in text:
for fn in os.listdir(os.path.join('words', textPath)):
if fn.endswith('.png'):
fd = os.path.join('./words', textPath, fn)
images.append(read_image(fd))
labels.append(textPath)
接著我們把剛剛得到的images和labels也變成numpy類型。當(dāng)然,labels首先要變成int類型。
X = np.array(images)
y = np.array(list(map(int, labels)))
最后,我們按三七分把這些分為訓(xùn)練集和測試集。
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.30, random_state=30)
數(shù)據(jù)的導(dǎo)入工作就完成了。但是顯然,這樣做的話我們必須一次性把所有數(shù)據(jù)讀入內(nèi)存。當(dāng)我們的數(shù)據(jù)量特別大的時候,這肯定是行不通的。就算數(shù)據(jù)量不大,這樣也會浪費(fèi)很多時間在IO上面。我們的希望的是,在訓(xùn)練的時候拿數(shù)據(jù),一份一份地訓(xùn)練。
這是一個很難解決的問題,在網(wǎng)上很難找到對應(yīng)的教程專門提到這一點(diǎn)。大神們都默認(rèn)這是一個很簡單的問題。但是對于新手來說,這卻是一個很難的問題。我甚至考慮過用多線程去寫。但是甚至讀入的數(shù)據(jù)的順序,都會影響最后的結(jié)果。如果我們一批一批地訓(xùn)練,就容易使得最后的結(jié)果偏向我們最后導(dǎo)入的數(shù)據(jù),從而過擬合。最后我在查閱了Keras的官方文檔,并且查看了很多相關(guān)的內(nèi)容之后,找到了解決方法。
原來,Keras的訓(xùn)練不僅僅有fit,還有fit_generator,也就是一個一個訓(xùn)練。fit_generator的API如下:
fit_generator(self, generator, steps_per_epoch, epochs=1, verbose=1, callbacks=None, validation_data=None, validation_steps=None, class_weight=None, max_q_size=10, workers=1, pickle_safe=False, initial_epoch=0)
文檔是這樣寫的:
函數(shù)的參數(shù)是:
- generator:生成器函數(shù),生成器的輸出應(yīng)該為:
- 一個形如(inputs,targets)的tuple
- 一個形如(inputs, targets,sample_weight)的tuple。所有的返回值都應(yīng)該包含相同數(shù)目的樣本。生成器將無限在數(shù)據(jù)集上循環(huán)。每個epoch以經(jīng)過模型的樣本數(shù)達(dá)到samples_per_epoch時,記一個epoch結(jié)束
- steps_per_epoch:整數(shù),當(dāng)生成器返回steps_per_epoch次數(shù)據(jù)時計一個epoch結(jié)束,執(zhí)行下一個epoch
- epochs:整數(shù),數(shù)據(jù)迭代的輪數(shù)
- verbose:日志顯示,0為不在標(biāo)準(zhǔn)輸出流輸出日志信息,1為輸出進(jìn)度條記錄,2為每個epoch輸出一行記錄
- validation_data:具有以下三種形式之一
- 生成驗(yàn)證集的生成器
- 一個形如(inputs,targets)的tuple
- 一個形如(inputs,targets,sample_weights)的tuple
- validation_steps: 當(dāng)validation_data為生成器時,本參數(shù)指定驗(yàn)證集的生成器返回次數(shù)
- class_weight:規(guī)定類別權(quán)重的字典,將類別映射為權(quán)重,常用于處理樣本不均衡問題。
- sample_weight:權(quán)值的numpy array,用于在訓(xùn)練時調(diào)整損失函數(shù)(僅用于訓(xùn)練)??梢詡鬟f一個1D的與樣本等長的向量用于對樣本進(jìn)行1對1的加權(quán),或者在面對時序數(shù)據(jù)時,傳遞一個的形式為(samples,sequence_length)的矩陣來為每個時間步上的樣本賦不同的權(quán)。這種情況下請確定在編譯模型時添加了sample_weight_mode='temporal'。
- workers:最大進(jìn)程數(shù)
- max_q_size:生成器隊列的最大容量
- pickle_safe: 若為真,則使用基于進(jìn)程的線程。由于該實(shí)現(xiàn)依賴多進(jìn)程,不能傳遞non picklable(無法被pickle序列化)的參數(shù)到生成器中,因?yàn)闊o法輕易將它們傳入子進(jìn)程中。
- initial_epoch: 從該參數(shù)指定的epoch開始訓(xùn)練,在繼續(xù)之前的訓(xùn)練時有用。
這個里面,只有前面六個是比較重要的,其他的默認(rèn)就行了。甚至我們只需要前面三個就行了。steps_per_epoch和epochs都很好理解。這個generator,也就是生成器函數(shù),才是問題的關(guān)鍵。
接下來就非常簡單了。在keras系列︱利用fit_generator最小化顯存占用比率/數(shù)據(jù)Batch化這篇博客里面已經(jīng)講得很清楚了。
里面有個demo一般的生成器函數(shù):
def generate_batch_data_random(x, y, batch_size):
"""逐步提取batch數(shù)據(jù)到顯存,降低對顯存的占用"""
ylen = len(y)
loopcount = ylen // batch_size
while (True):
i = randint(0,loopcount)
yield x[i * batch_size:(i + 1) * batch_size], y[i * batch_size:(i + 1) * batch_size]
這里應(yīng)該注意的有兩點(diǎn),第一點(diǎn)就是數(shù)據(jù)必須是要打亂的,沒有規(guī)律的。第二點(diǎn)就是最后用的是yield。這也是Python的一個高級特性了。簡單地說,這就是一個return。但是你每調(diào)用一次,它就返回一次,而不像其他函數(shù)一樣,return了就出去了。這樣就成為了一個生成器。具體可以看廖雪峰的Python教程
剩下的就很簡單了。我們甚至可以直接在這個生成器函數(shù)里面寫圖片生成的算法。當(dāng)然,考慮到IO操作肯定比直接生成要快,直接生成肯定是不可取的。
當(dāng)然,這不是最好的導(dǎo)入數(shù)據(jù)的方法。Keras還有更快的方法。在介紹這個方法之前,我需要先介紹Keras的圖像預(yù)處理的方法。
為了防止圖像的過擬合,Keras里面自帶了圖片生成器用來對圖像進(jìn)行一些簡單的操作,例如平移,旋轉(zhuǎn),縮放等等。這樣我們就可以在有限的數(shù)據(jù)集上面生成無限的訓(xùn)練樣本。這樣可以擴(kuò)大訓(xùn)練集的大小,防止圖像的過擬合。具體的內(nèi)容可以查看圖片生成器的文章。
關(guān)鍵問題不在于這個圖片生成,而是這個圖片生成器的方法里面提供了一個函數(shù)——flow_from_directory(directory)
這個函數(shù)的參數(shù)如下:
flow_from_directory(directory): 以文件夾路徑為參數(shù),生成經(jīng)過數(shù)據(jù)提升/歸一化后的數(shù)據(jù),在一個無限循環(huán)中無限產(chǎn)生batch數(shù)據(jù)
- directory: 目標(biāo)文件夾路徑,對于每一個類,該文件夾都要包含一個子文件夾.子文件夾中任何JPG、PNG、BNP、PPM的圖片都會被生成器使用.詳情請查看此腳本
- target_size: 整數(shù)tuple,默認(rèn)為(256, 256). 圖像將被resize成該尺寸
- color_mode: 顏色模式,為"grayscale","rgb"之一,默認(rèn)為"rgb".代表這些圖片是否會被轉(zhuǎn)換為單通道或三通道的圖片.
- classes: 可選參數(shù),為子文件夾的列表,如['dogs','cats']默認(rèn)為None. 若未提供,則該類別列表將從
directory下的子文件夾名稱/結(jié)構(gòu)自動推斷。每一個子文件夾都會被認(rèn)為是一個新的類。(類別的順序?qū)凑兆帜副眄樞蛴成涞綐?biāo)簽值)。通過屬性class_indices可獲得文件夾名與類的序號的對應(yīng)字典。- class_mode: "categorical", "binary", "sparse"或None之一. 默認(rèn)為"categorical. 該參數(shù)決定了返回的標(biāo)簽數(shù)組的形式, "categorical"會返回2D的one-hot編碼標(biāo)簽,"binary"返回1D的二值標(biāo)簽."sparse"返回1D的整數(shù)標(biāo)簽,如果為None則不返回任何標(biāo)簽, 生成器將僅僅生成batch數(shù)據(jù), 這種情況在使用
model.predict_generator()和model.evaluate_generator()等函數(shù)時會用到.- batch_size: batch數(shù)據(jù)的大小,默認(rèn)32
- shuffle: 是否打亂數(shù)據(jù),默認(rèn)為True
- seed: 可選參數(shù),打亂數(shù)據(jù)和進(jìn)行變換時的隨機(jī)數(shù)種子
- save_to_dir: None或字符串,該參數(shù)能讓你將提升后的圖片保存起來,用以可視化
- save_prefix:字符串,保存提升后圖片時使用的前綴, 僅當(dāng)設(shè)置了
save_to_dir時生效- save_format:"png"或"jpeg"之一,指定保存圖片的數(shù)據(jù)格式,默認(rèn)"jpeg"
- flollow_links: 是否訪問子文件夾中的軟鏈接
這樣,我們導(dǎo)入數(shù)據(jù)就可以直接使用Keras自帶的導(dǎo)入數(shù)據(jù)的方法了,并且附帶了圖片的處理。
我們的代碼可以這樣寫:
datagen = ImageDataGenerator(...)
train_generator = datagen.flow_from_directory(
'./words',
target_size=(30, 30),
color_mode='grayscale',
batch_size=64)
model.fit_generator(train_generator, steps_per_epoch=500, epochs=50)
這樣我們就導(dǎo)入了數(shù)據(jù)了。自此Keras的簡單使用已經(jīng)不成問題。
最后,我們需要注意,最后的導(dǎo)入數(shù)據(jù)的時候,會自動搜索里面的文件夾,但是是按字典序排序的。這很自然。例如你的文件夾是分類問題,文件夾都是貓、狗、鼠這樣的漢字,它當(dāng)然得按字典序排序。但是如果是像我們用0、1、2、3…這樣的數(shù)字,就容易讓人崩潰。因此我們需要注意在生成文件夾的時候,前面補(bǔ)0,即000、001、003、…、999。