面向小數(shù)據(jù)集構(gòu)建圖像分類模型文章信息本文地址:http://blog./building-powerful-image-classification-models-using-very-little-data.html 本文作者:Francois Chollet 概述在本文中,我們將提供一些面向小數(shù)據(jù)集(幾百張到幾千張圖片)構(gòu)造高效、實(shí)用的圖像分類器的方法。 本文將探討如下幾種方法:
本文需要使用的Keras模塊有:
配置情況我們的實(shí)驗基于下面的配置
data/ train/ dogs/ dog001.jpg dog002.jpg ... cats/ cat001/jpg cat002.jpg ... validation/ dogs/ dog001.jpg dog002.jpg ... cats/ cat001/jpg cat002.jpg ... 這份數(shù)據(jù)集來源于Kaggle,原數(shù)據(jù)集有12500只貓和12500只狗,我們只取了各個類的前1000張圖片。另外我們還從各個類中取了400張額外圖片用于測試。 下面是數(shù)據(jù)集的一些示例圖片,圖片的數(shù)量非常少,這對于圖像分類來說是個大麻煩。但現(xiàn)實(shí)是,很多真實(shí)世界圖片獲取是很困難的,我們能得到的樣本數(shù)目確實(shí)很有限(比如醫(yī)學(xué)圖像,每張正樣本都意味著一個承受痛苦的病人:()。對數(shù)據(jù)科學(xué)家而言,我們應(yīng)該有能夠榨取少量數(shù)據(jù)的全部價值的能力,而不是簡單的伸手要更多的數(shù)據(jù)。 在Kaggle的貓狗大戰(zhàn)競賽種,參賽者通過使用現(xiàn)代的深度學(xué)習(xí)技術(shù)達(dá)到了98%的正確率,我們只使用了全部數(shù)據(jù)的8%,因此這個問題對我們來說更難。 針對小數(shù)據(jù)集的深度學(xué)習(xí)我經(jīng)常聽到的一種說法是,深度學(xué)習(xí)只有在你擁有海量數(shù)據(jù)時才有意義。雖然這種說法并不是完全不對,但卻具有較強(qiáng)的誤導(dǎo)性。當(dāng)然,深度學(xué)習(xí)強(qiáng)調(diào)從數(shù)據(jù)中自動學(xué)習(xí)特征的能力,沒有足夠的訓(xùn)練樣本,這幾乎是不可能的。尤其是當(dāng)輸入的數(shù)據(jù)維度很高(如圖片)時。然而,卷積神經(jīng)網(wǎng)絡(luò)作為深度學(xué)習(xí)的支柱,被設(shè)計為針對“感知”問題最好的模型之一(如圖像分類問題),即使只有很少的數(shù)據(jù),網(wǎng)絡(luò)也能把特征學(xué)的不錯。針對小數(shù)據(jù)集的神經(jīng)網(wǎng)絡(luò)依然能夠得到合理的結(jié)果,并不需要任何手工的特征工程。一言以蔽之,卷積神經(jīng)網(wǎng)絡(luò)大法好! 另一方面,深度學(xué)習(xí)模型天然就具有可重用的特性:比方說,你可以把一個在大規(guī)模數(shù)據(jù)上訓(xùn)練好的圖像分類或語音識別的模型重用在另一個很不一樣的問題上,而只需要做有限的一點(diǎn)改動。尤其在計算機(jī)視覺領(lǐng)域,許多預(yù)訓(xùn)練的模型現(xiàn)在都被公開下載,并被重用在其他問題上以提升在小數(shù)據(jù)集上的性能。 數(shù)據(jù)預(yù)處理與數(shù)據(jù)提升為了盡量利用我們有限的訓(xùn)練數(shù)據(jù),我們將通過一系列隨機(jī)變換堆數(shù)據(jù)進(jìn)行提升,這樣我們的模型將看不到任何兩張完全相同的圖片,這有利于我們抑制過擬合,使得模型的泛化能力更好。 在Keras中,這個步驟可以通過
現(xiàn)在讓我們看個例子: from keras.preprocessing.image import ImageDataGeneratordatagen = ImageDataGenerator( rotation_range=40, width_shift_range=0.2, height_shift_range=0.2, rescale=1./255, shear_range=0.2, zoom_range=0.2, horizontal_flip=True, fill_mode='nearest') 上面顯示的只是一部分選項,請閱讀文檔的相關(guān)部分來查看全部可用的選項。我們來快速的瀏覽一下這些選項的含義:
下面我們使用這個工具來生成圖片,并將它們保存在一個臨時文件夾中,這樣我們可以感覺一下數(shù)據(jù)提升究竟做了什么事。為了使圖片能夠展示出來,這里沒有使用 from keras.preprocessing.image import ImageDataGenerator, array_to_img, img_to_array, load_imgdatagen = ImageDataGenerator( rotation_range=40, width_shift_range=0.2, height_shift_range=0.2, shear_range=0.2, zoom_range=0.2, horizontal_flip=True, fill_mode='nearest')img = load_img('data/train/cats/cat.0.jpg') # this is a PIL imagex = img_to_array(img) # this is a Numpy array with shape (3, 150, 150)x = x.reshape((1,) + x.shape) # this is a Numpy array with shape (1, 3, 150, 150)# the .flow() command below generates batches of randomly transformed images# and saves the results to the `preview/` directoryi = 0for batch in datagen.flow(x, batch_size=1, save_to_dir='preview', save_prefix='cat', save_format='jpeg'): i += 1 if i > 20: break # otherwise the generator would loop indefinitely 下面是一張圖片被提升以后得到的多個結(jié)果: 在小數(shù)據(jù)集上訓(xùn)練神經(jīng)網(wǎng)絡(luò):40行代碼達(dá)到80%的準(zhǔn)確率進(jìn)行圖像分類的正確工具是卷積網(wǎng)絡(luò),所以我們來試試用卷積神經(jīng)網(wǎng)絡(luò)搭建一個初級的模型。因為我們的樣本數(shù)很少,所以我們應(yīng)該對過擬合的問題多加注意。當(dāng)一個模型從很少的樣本中學(xué)習(xí)到不能推廣到新數(shù)據(jù)的模式時,我們稱為出現(xiàn)了過擬合的問題。過擬合發(fā)生時,模型試圖使用不相關(guān)的特征來進(jìn)行預(yù)測。例如,你有三張伐木工人的照片,有三張水手的照片。六張照片中只有一個伐木工人戴了帽子,如果你認(rèn)為戴帽子是能將伐木工人與水手區(qū)別開的特征,那么此時你就是一個差勁的分類器。 數(shù)據(jù)提升是對抗過擬合問題的一個武器,但還不夠,因為提升過的數(shù)據(jù)仍然是高度相關(guān)的。對抗過擬合的你應(yīng)該主要關(guān)注的是模型的“熵容量”——模型允許存儲的信息量。能夠存儲更多信息的模型能夠利用更多的特征取得更好的性能,但也有存儲不相關(guān)特征的風(fēng)險。另一方面,只能存儲少量信息的模型會將存儲的特征主要集中在真正相關(guān)的特征上,并有更好的泛化性能。 有很多不同的方法來調(diào)整模型的“熵容量”,常見的一種選擇是調(diào)整模型的參數(shù)數(shù)目,即模型的層數(shù)和每層的規(guī)模。另一種方法是對權(quán)重進(jìn)行正則化約束,如L1或L2.這種約束會使模型的權(quán)重偏向較小的值。 在我們的模型里,我們使用了很小的卷積網(wǎng)絡(luò),只有很少的幾層,每層的濾波器數(shù)目也不多。再加上數(shù)據(jù)提升和Dropout,就差不多了。Dropout通過防止一層看到兩次完全一樣的模式來防止過擬合,相當(dāng)于也是一種數(shù)據(jù)提升的方法。(你可以說dropout和數(shù)據(jù)提升都在隨機(jī)擾亂數(shù)據(jù)的相關(guān)性) 下面展示的代碼是我們的第一個模型,一個很簡單的3層卷積加上ReLU激活函數(shù),再接max-pooling層。這個結(jié)構(gòu)和Yann LeCun在1990年發(fā)布的圖像分類器很相似(除了ReLU) 這個實(shí)驗的全部代碼在這里 from keras.models import Sequentialfrom keras.layers import Convolution2D, MaxPooling2Dfrom keras.layers import Activation, Dropout, Flatten, Densemodel = Sequential()model.add(Convolution2D(32, 3, 3, input_shape=(3, 150, 150)))model.add(Activation('relu'))model.add(MaxPooling2D(pool_size=(2, 2)))model.add(Convolution2D(32, 3, 3))model.add(Activation('relu'))model.add(MaxPooling2D(pool_size=(2, 2)))model.add(Convolution2D(64, 3, 3))model.add(Activation('relu'))model.add(MaxPooling2D(pool_size=(2, 2)))# the model so far outputs 3D feature maps (height, width, features) 然后我們接了兩個全連接網(wǎng)絡(luò),并以單個神經(jīng)元和sigmoid激活結(jié)束模型。這種選擇會產(chǎn)生二分類的結(jié)果,與這種配置相適應(yīng),我們使用 model.add(Flatten()) # this converts our 3D feature maps to 1D feature vectorsmodel.add(Dense(64))model.add(Activation('relu'))model.add(Dropout(0.5))model.add(Dense(1))model.add(Activation('sigmoid'))model.compile(loss='binary_crossentropy', optimizer='rmsprop', metrics=['accuracy']) 然后我們開始準(zhǔn)備數(shù)據(jù),使用 # this is the augmentation configuration we will use for trainingtrain_datagen = ImageDataGenerator( rescale=1./255, shear_range=0.2, zoom_range=0.2, horizontal_flip=True)# this is the augmentation configuration we will use for testing:# only rescalingtest_datagen = ImageDataGenerator(rescale=1./255)# this is a generator that will read pictures found in# subfolers of 'data/train', and indefinitely generate# batches of augmented image datatrain_generator = train_datagen.flow_from_directory( 'data/train', # this is the target directory target_size=(150, 150), # all images will be resized to 150x150 batch_size=32, class_mode='binary') # since we use binary_crossentropy loss, we need binary labels# this is a similar generator, for validation datavalidation_generator = test_datagen.flow_from_directory( 'data/validation', target_size=(150, 150), batch_size=32, class_mode='binary') 然后我們可以用這個生成器來訓(xùn)練網(wǎng)絡(luò)了,在GPU上每個epoch耗時20~30秒,在CPU上耗時300~400秒,所以如果你不是很著急,在CPU上跑這個模型也是完全可以的。 model.fit_generator( train_generator, samples_per_epoch=2000, nb_epoch=50, validation_data=validation_generator, nb_val_samples=800)model.save_weights('first_try.h5') # always save your weights after training or during training 這個模型在50個epoch后的準(zhǔn)確率為79%~81%,別忘了我們只用了8%的數(shù)據(jù),也沒有花時間來做模型和超參數(shù)的優(yōu)化。在Kaggle中,這個模型已經(jīng)可以進(jìn)前100名了(一共215隊參與),估計剩下的115隊都沒有用深度學(xué)習(xí):) 注意這個準(zhǔn)確率的變化可能會比較大,因為準(zhǔn)確率本來就是一個變化較高的評估參數(shù),而且我們只有800個樣本用來測試。比較好的驗證方法是使用K折交叉驗證,但每輪驗證中我們都要訓(xùn)練一個模型。 使用預(yù)訓(xùn)練網(wǎng)絡(luò)的bottleneck特征:一分鐘達(dá)到90%的正確率一個稍微講究一點(diǎn)的辦法是,利用在大規(guī)模數(shù)據(jù)集上預(yù)訓(xùn)練好的網(wǎng)絡(luò)。這樣的網(wǎng)絡(luò)在多數(shù)的計算機(jī)視覺問題上都能取得不錯的特征,利用這樣的特征可以讓我們獲得更高的準(zhǔn)確率。 我們將使用vgg-16網(wǎng)絡(luò),該網(wǎng)絡(luò)在ImageNet數(shù)據(jù)集上進(jìn)行訓(xùn)練,這個模型我們之前提到過了。因為ImageNet數(shù)據(jù)集包含多種“貓”類和多種“狗”類,這個模型已經(jīng)能夠?qū)W習(xí)與我們這個數(shù)據(jù)集相關(guān)的特征了。事實(shí)上,簡單的記錄原來網(wǎng)絡(luò)的輸出而不用bottleneck特征就已經(jīng)足夠把我們的問題解決的不錯了。不過我們這里講的方法對其他的類似問題有更好的推廣性,包括在ImageNet中沒有出現(xiàn)的類別的分類問題。 VGG-16的網(wǎng)絡(luò)結(jié)構(gòu)如下: 我們的方法是這樣的,我們將利用網(wǎng)絡(luò)的卷積層部分,把全連接以上的部分拋掉。然后在我們的訓(xùn)練集和測試集上跑一遍,將得到的輸出(即“bottleneck feature”,網(wǎng)絡(luò)在全連接之前的最后一層激活的feature map)記錄在兩個numpy array里。然后我們基于記錄下來的特征訓(xùn)練一個全連接網(wǎng)絡(luò)。 我們將這些特征保存為離線形式,而不是將我們的全連接模型直接加到網(wǎng)絡(luò)上并凍結(jié)之前的層參數(shù)進(jìn)行訓(xùn)練的原因是處于計算效率的考慮。運(yùn)行VGG網(wǎng)絡(luò)的代價是非常高昂的,尤其是在CPU上運(yùn)行,所以我們只想運(yùn)行一次。這也是我們不進(jìn)行數(shù)據(jù)提升的原因。 我們不再贅述如何搭建vgg-16網(wǎng)絡(luò)了,這件事之前已經(jīng)說過,在keras的example里也可以找到。但讓我們看看如何記錄bottleneck特征。 generator = datagen.flow_from_directory( 'data/train', target_size=(150, 150), batch_size=32, class_mode=None, # this means our generator will only yield batches of data, no labels shuffle=False) # our data will be in order, so all first 1000 images will be cats, then 1000 dogs# the predict_generator method returns the output of a model, given# a generator that yields batches of numpy databottleneck_features_train = model.predict_generator(generator, 2000)# save the output as a Numpy arraynp.save(open('bottleneck_features_train.npy', 'w'), bottleneck_features_train)generator = datagen.flow_from_directory( 'data/validation', target_size=(150, 150), batch_size=32, class_mode=None, shuffle=False)bottleneck_features_validation = model.predict_generator(generator, 800)np.save(open('bottleneck_features_validation.npy', 'w'), bottleneck_features_validation) 記錄完畢后我們可以將數(shù)據(jù)載入,用于訓(xùn)練我們的全連接網(wǎng)絡(luò): train_data = np.load(open('bottleneck_features_train.npy'))# the features were saved in order, so recreating the labels is easytrain_labels = np.array([0] * 1000 + [1] * 1000)validation_data = np.load(open('bottleneck_features_validation.npy'))validation_labels = np.array([0] * 400 + [1] * 400)model = Sequential()model.add(Flatten(input_shape=train_data.shape[1:]))model.add(Dense(256, activation='relu'))model.add(Dropout(0.5))model.add(Dense(1, activation='sigmoid'))model.compile(optimizer='rmsprop', loss='binary_crossentropy', metrics=['accuracy'])model.fit(train_data, train_labels, nb_epoch=50, batch_size=32, validation_data=(validation_data, validation_labels))model.save_weights('bottleneck_fc_model.h5') 因為特征的size很小,模型在CPU上跑的也會很快,大概1s一個epoch,最后我們的準(zhǔn)確率是90%~91%,這么好的結(jié)果多半歸功于預(yù)訓(xùn)練的vgg網(wǎng)絡(luò)幫助我們提取特征。 在預(yù)訓(xùn)練的網(wǎng)絡(luò)上fine-tune為了進(jìn)一步提高之前的結(jié)果,我們可以試著fine-tune網(wǎng)絡(luò)的后面幾層。Fine-tune以一個預(yù)訓(xùn)練好的網(wǎng)絡(luò)為基礎(chǔ),在新的數(shù)據(jù)集上重新訓(xùn)練一小部分權(quán)重。在這個實(shí)驗中,fine-tune分三個步驟
注意:
代碼如下,首先在初始化好的vgg網(wǎng)絡(luò)上添加我們預(yù)訓(xùn)練好的模型: # build a classifier model to put on top of the convolutional modeltop_model = Sequential()top_model.add(Flatten(input_shape=model.output_shape[1:]))top_model.add(Dense(256, activation='relu'))top_model.add(Dropout(0.5))top_model.add(Dense(1, activation='sigmoid'))# note that it is necessary to start with a fully-trained# classifier, including the top classifier,# in order to successfully do fine-tuningtop_model.load_weights(top_model_weights_path)# add the model on top of the convolutional basemodel.add(top_model) 然后將最后一個卷積塊前的卷積層參數(shù)凍結(jié): # set the first 25 layers (up to the last conv block)# to non-trainable (weights will not be updated)for layer in model.layers[:25]: layer.trainable = False# compile the model with a SGD/momentum optimizer# and a very slow learning rate.model.compile(loss='binary_crossentropy', optimizer=optimizers.SGD(lr=1e-4, momentum=0.9), metrics=['accuracy']) 然后以很低的學(xué)習(xí)率進(jìn)行訓(xùn)練: # prepare data augmentation configurationtrain_datagen = ImageDataGenerator( rescale=1./255, shear_range=0.2, zoom_range=0.2, horizontal_flip=True)test_datagen = ImageDataGenerator(rescale=1./255)train_generator = train_datagen.flow_from_directory( train_data_dir, target_size=(img_height, img_width), batch_size=32, class_mode='binary')validation_generator = test_datagen.flow_from_directory( validation_data_dir, target_size=(img_height, img_width), batch_size=32, class_mode='binary')# fine-tune the modelmodel.fit_generator( train_generator, samples_per_epoch=nb_train_samples, nb_epoch=nb_epoch, validation_data=validation_generator, nb_val_samples=nb_validation_samples) 在50個epoch之后該方法的準(zhǔn)確率為94%,非常成功 通過下面的方法你可以達(dá)到95%以上的正確率:
|
|