從零開始學keras之kaggle貓狗識別分類器
使用很少的數據來訓練一個圖像分類模型,這是很常見的情況,如果你要從事計算機視覺方面的職業,很可能會在實踐中遇到這種情況。“很少的”樣本可能是幾百張圖像,也可能是幾萬張圖像。來看一個實例,我們將重點討論貓狗圖像分類,數據集中包含 4000 張貓和狗的圖像(2000 張貓的圖像,2000 張狗的圖像)。我們將 2000 張圖像用于訓練,1000 張用于驗證,1000張用于測試。本節將介紹解決這一問題的基本策略,即使用已有的少量數據從頭開始訓練一個新模型。
首先,在 2000 個訓練樣本上訓練一個簡單的小型卷積神經網絡,不做任何正則化,為模型目標設定一個基準。這會得到 71% 的分類精度。此時主要的問題在于過擬合。然后,我們會介紹數據增強(data augmentation),它在計算機視覺領域是一種非常強大的降低過擬合的技術。使用數據增強之后,網絡精度將提高到 82%。
下一節會介紹將深度學習應用于小型數據集的另外兩個重要技巧:用預訓練的網絡做特征提 取(得到的精度范圍在 90%~93%),對預訓練的網絡進行微調(最終精度為 95%)。總而言之,這三種策略——從頭開始訓練一個小型模型、使用預訓練的網絡做特征提取、對預訓練的網絡進行微調——構成了你的工具箱,未來可用于解決小型數據集的圖像分類問題。
深度學習與小數據問題的相關性
有時你會聽人說,僅在有大量數據可用時,深度學習才有效。這種說法部分正確:深度學習的一個基本特性就是能夠獨立地在訓練數據中找到有趣的特征,無須人為的特征工程,而這只在擁有大量訓練樣本時才能實現。對于輸入樣本的維度非常高(比如圖像)的問題尤其如此。
但對于初學者來說,所謂“大量”樣本是相對的,即相對于你所要訓練網絡的大小和深度而言。只用幾十個樣本訓練卷積神經網絡就解決一個復雜問題是不可能的,但如果模型很小, 并做了很好的正則化,同時任務非常簡單,那么幾百個樣本可能就足夠了。由于卷積神經網絡 學到的是局部的、平移不變的特征,它對于感知問題可以高效地利用數據。雖然數據相對較少,但在非常小的圖像數據集上從頭開始訓練一個卷積神經網絡,仍然可以得到不錯的結果,而且無須任何自定義的特征工程。本節你將看到其效果。
此外,深度學習模型本質上具有高度的可復用性,比如,已有一個在大規模數據集上訓練的圖像分類模型或語音轉文本模型,你只需做很小的修改就能將其復用于完全不同的問題。特 別是在計算機視覺領域,許多預訓練的模型(通常都是在 ImageNet 數據集上訓練得到的)現在都可以公開下載,并可以用于在數據很少的情況下構建強大的視覺模型。這是下一節的內容。
我們先來看一下數據。
下載數據
本節用到的貓狗分類數據集不包含在 Keras 中。它由 Kaggle 在 2013 年末公開并作為一項 計算視覺競賽的一部分,當時卷積神經網絡還不是主流算法。你可以從?https://www.kaggle.com/c/dogs-vs-cats/data?下載原始數據集(如果沒有 Kaggle 賬號的話,你需要注冊一個,別擔心,很簡單)。
這些圖像都是中等分辨率的彩色 JPEG 圖像。下圖給出了一些樣本示例。
不出所料,2013 年的貓狗分類 Kaggle 競賽的優勝者使用的是卷積神經網絡。最佳結果達到了 95% 的精度。本例中,雖然你只在不到參賽選手所用的 10% 的數據上訓練模型,但結果也和這個精度相當接近。
這個數據集包含 25 000 張貓狗圖像(每個類別都有 12 500 張),大小為 543MB(壓縮后)。 下載數據并解壓之后,你需要創建一個新數據集,其中包含三個子集:每個類別各 1000 個樣本 的訓練集、每個類別各 500 個樣本的驗證集和每個類別各 500 個樣本的測試集。
創建新數據集的代碼如下所示。
備注:數據已經下載好了,放在data/kaggle_original_data目錄
import os, shutil # The path to the directory where the original # dataset was uncompressed(原始數據集解壓目錄的路徑) original_dataset_dir = 'data/kaggle_original_data' # The directory where we will # store our smaller dataset(保存較小數據集的目錄) base_dir = 'data/cats_and_dogs_small' if not os.path.exists(base_dir): os.mkdir(base_dir) # Directories for our training, validation and test splits #(分別對應劃分后的訓練、 驗證和測試的目錄) train_dir = os.path.join(base_dir, 'train') if not os.path.exists(train_dir): os.mkdir(train_dir) # Directory with our training cat pictures(貓的訓練圖像目錄) train_cats_dir = os.path.join(train_dir, 'cats') if not os.path.exists(train_cats_dir): os.mkdir(train_cats_dir) # Directory with our training dog pictures(狗的訓練圖像目錄) train_dogs_dir = os.path.join(train_dir, 'dogs') if not os.path.exists(train_dogs_dir): os.mkdir(train_dogs_dir) validation_dir = os.path.join(base_dir, 'validation') if not os.path.exists(validation_dir): os.mkdir(validation_dir) # Directory with our validation cat pictures(貓的驗證圖像目錄) validation_cats_dir = os.path.join(validation_dir, 'cats') if not os.path.exists(validation_cats_dir): os.mkdir(validation_cats_dir) # Directory with our validation dog pictures(狗的驗證圖像目錄) validation_dogs_dir = os.path.join(validation_dir, 'dogs') if not os.path.exists(validation_dogs_dir): os.mkdir(validation_dogs_dir) test_dir = os.path.join(base_dir, 'test') if not os.path.exists(test_dir): os.mkdir(test_dir) # Directory with our test cat pictures(貓的測試圖像目錄) test_cats_dir = os.path.join(test_dir, 'cats') if not os.path.exists(test_cats_dir): os.mkdir(test_cats_dir) # Directory with our test dog pictures(狗的測試圖像目錄) test_dogs_dir = os.path.join(test_dir, 'dogs') if not os.path.exists(test_dogs_dir): os.mkdir(test_dogs_dir) # Copy first 1000 cat images to train_cats_dir(將前 1000 張貓的圖像復制 到 train_cats_dir) fnames = ['cat.{}.jpg'.format(i) for i in range(1000)] for fname in fnames: src = os.path.join(original_dataset_dir, fname) dst = os.path.join(train_cats_dir, fname) shutil.copyfile(src, dst) # Copy next 500 cat images to validation_cats_dir(將 接 下 來 500 張 貓 的 圖像 復 制到 validation_cats_dir) fnames = ['cat.{}.jpg'.format(i) for i in range(1000, 1500)] for fname in fnames: src = os.path.join(original_dataset_dir, fname) dst = os.path.join(validation_cats_dir, fname) shutil.copyfile(src, dst) # Copy next 500 cat images to test_cats_dir(將 接 下 來的 500 張 貓 的 圖像 復制到 test_cats_dir) fnames = ['cat.{}.jpg'.format(i) for i in range(1500, 2000)] for fname in fnames: src = os.path.join(original_dataset_dir, fname) dst = os.path.join(test_cats_dir, fname) shutil.copyfile(src, dst) # Copy first 1000 dog images to train_dogs_dir(將前 1000 張狗的圖像復制 到 train_dogs_dir) fnames = ['dog.{}.jpg'.format(i) for i in range(1000)] for fname in fnames: src = os.path.join(original_dataset_dir, fname) dst = os.path.join(train_dogs_dir, fname) shutil.copyfile(src, dst) # Copy next 500 dog images to validation_dogs_dir(將接下來 500 張狗的圖像復 制到 validation_dogs_dir) fnames = ['dog.{}.jpg'.format(i) for i in range(1000, 1500)] for fname in fnames: src = os.path.join(original_dataset_dir, fname) dst = os.path.join(validation_dogs_dir, fname) shutil.copyfile(src, dst) # Copy next 500 dog images to test_dogs_dir(將接下來 500 張狗的圖像復制到 test_dogs_dir) fnames = ['dog.{}.jpg'.format(i) for i in range(1500, 2000)] for fname in fnames: src = os.path.join(original_dataset_dir, fname) dst = os.path.join(test_dogs_dir, fname) shutil.copyfile(src, dst)
我們來檢查一下,看看每個分組(訓練 / 驗證 / 測試)中分別包含多少張圖像。
print('total training cat images:', len(os.listdir(train_cats_dir))) 輸出:total training cat images: 1000 print('total training dog images:', len(os.listdir(train_dogs_dir))) 輸出:total training dog images: 1000 print('total validation cat images:', len(os.listdir(validation_cats_dir))) 輸出為:total validation cat images: 500 print('total validation dog images:', len(os.listdir(validation_dogs_dir))) 輸出:total validation dog images: 500 print('total test cat images:', len(os.listdir(test_cats_dir))) 輸出:total test cat images: 500 print('total test dog images:', len(os.listdir(test_dogs_dir))) 輸出:total test dog images: 500
所以我們的確有 2000 張訓練圖像、1000 張驗證圖像和 1000 張測試圖像。每個分組中兩個類別的樣本數相同,這是一個平衡的二分類問題,分類精度可作為衡量成功的指標。
構建網絡
在前一個 MNIST 示例中,我們構建了一個小型卷積神經網絡,所以你應該已經熟悉這 種網絡。我們將復用相同的總體結構,即卷積神經網絡由 Conv2D 層(使用 relu 激活)和 MaxPooling2D 層交替堆疊構成。
但由于這里要處理的是更大的圖像和更復雜的問題,你需要相應地增大網絡,即再增加一 個 Conv2D+MaxPooling2D 的組合。這既可以增大網絡容量,也可以進一步減小特征圖的尺寸, 使其在連接 Flatten 層時尺寸不會太大。本例中初始輸入的尺寸為 150×150(有些隨意的選 擇),所以最后在 Flatten 層之前的特征圖大小為 7×7。
注意 網絡中特征圖的深度在逐漸增大(從 32 增大到 128),而特征圖的尺寸在逐漸減小(從148×148 減小到 7×7)。這幾乎是所有卷積神經網絡的模式。
你面對的是一個二分類問題,所以網絡最后一層是使用 sigmoid 激活的單一單元(大小為1 的 Dense 層)。這個單元將對某個類別的概率進行編碼。
from Keras import models from keras import layers model = models.Sequential() model.add(layers.Conv2D(32, (3, 3), activation='relu', input_shape=(150, 150, 3))) model.add(layers.MaxPooling2D((2, 2))) model.add(layers.Conv2D(64, (3, 3), activation='relu')) model.add(layers.MaxPooling2D((2, 2))) model.add(layers.Conv2D(128, (3, 3), activation='relu')) model.add(layers.MaxPooling2D((2, 2))) model.add(layers.Conv2D(128, (3, 3), activation='relu')) model.add(layers.MaxPooling2D((2, 2))) model.add(layers.Flatten()) model.add(layers.Dense(512, activation='relu')) model.add(layers.Dense(1, activation='sigmoid'))
我們來看一下特征圖的維度如何隨著每層變化。
model.summary()
在編譯這一步,和前面一樣,我們將使用 RMSprop 優化器。因為網絡最后一層是單一 sigmoid 單元,所以我們將使用二元交叉熵作為損失函數(提醒一下,第四章第五節中的表列出了各種情況下應該使用的損失函數)。
from keras import optimizers model.compile(loss='binary_crossentropy', optimizer=optimizers.RMSprop(lr=1e-4), metrics=['acc'])
數據預處理
你現在已經知道,將數據輸入神經網絡之前,應該將數據格式化為經過預處理的浮點數張量。 現在,數據以 JPEG 文件的形式保存在硬盤中,所以數據預處理步驟大致如下。
(1) 讀取圖像文件。
(2) 將 JPEG 文件解碼為 RGB 像素網格。
(3) 將這些像素網格轉換為浮點數張量。
(4) 將像素值(0~255 范圍內)縮放到 [0, 1] 區間(正如你所知,神經網絡喜歡處理較小的輸入值)。
這些步驟可能看起來有點嚇人,但幸運的是,Keras 擁有自動完成這些步驟的工具。Keras 有一個圖像處理輔助工具的模塊,位于keras.preprocessing.image。特別地,它包含 ImageDataGenerator 類,可以快速創建 Python 生成器,能夠將硬盤上的圖像文件自動轉換 為預處理好的張量批量。下面我們將用到這個類。
from keras.preprocessing.image import ImageDataGenerator # All images will be rescaled by 1./255(將所有圖像乘以 1/255 縮放) train_datagen = ImageDataGenerator(rescale=1./255) test_datagen = ImageDataGenerator(rescale=1./255) train_generator = train_datagen.flow_from_directory( # This is the target directory(目標目錄) train_dir, # All images will be resized to 150x150(將所有圖像的大小調整為 150×150) target_size=(150, 150), batch_size=20, # Since we use binary_crossentropy loss, we need binary labels(因為使用了 binary_crossentropy損失,所以需要用二進制標簽) class_mode='binary') validation_generator = test_datagen.flow_from_directory( validation_dir, target_size=(150, 150), batch_size=20, class_mode='binary')
我們來看一下其中一個生成器的輸出:它生成了 150×150 的 RGB 圖像[形狀為 (20,150, 150, 3)]與二進制標簽[形狀為 (20,)]組成的批量。每個批量中包含20個樣本(批量大小)。注意,生成器會不停地生成這些批量,它會不斷循環目標文件夾中的圖像。因此,你需要在某個時刻終止(break)迭代循環。
for data_batch, labels_batch in train_generator: print('data batch shape:', data_batch.shape) print('labels batch shape:', labels_batch.shape) break 輸出:data batch shape: (20, 150, 150, 3) labels batch shape: (20,)
利用生成器,我們讓模型對數據進行擬合。我們將使用 fit_generator 方法來擬合,它在數據生成器上的效果和 fit 相同。它的第一個參數應該是一個 Python 生成器,可以不停地生成輸入和目標組成的批量,比如 train_generator。因為數據是不斷生成的,所以 Keras 模型 要知道每一輪需要從生成器中抽取多少個樣本。這是 steps_per_epoch 參數的作用:從生成器中抽取 steps_per_epoch 個批量后(即運行了 steps_per_epoch 次梯度下降),擬合過程將進入下一個輪次。本例中,每個批量包含 20 個樣本,所以讀取完所有 2000 個樣本需要 100 個批量。
使用 fit_generator 時,你可以傳入一個 validation_data 參數,其作用和在 fit 方法中類似。值得注意的是,這個參數可以是一個數據生成器,但也可以是 Numpy 數組組成的元組。如果向 validation_data 傳入一個生成器,那么這個生成器應該能夠不停地生成驗證數據批量,因此你還需要指定 validation_steps 參數,說明需要從驗-中抽取多少個批次用于評估。
history = model.fit_generator( train_generator, steps_per_epoch=100, epochs=30, validation_data=validation_generator, validation_steps=50)
取部分訓練輪數:
始終在訓練完成后保存模型,這是一種良好實踐。
model.save('cats_and_dogs_small_1.h5')
我們來分別繪制訓練過程中模型在訓練數據和驗證數據上的損失和精度。
%matplotlib inline import matplotlib.pyplot as plt acc = history.history['acc'] val_acc = history.history['val_acc'] loss = history.history['loss'] val_loss = history.history['val_loss'] epochs = range(len(acc)) plt.plot(epochs, acc, 'bo', label='Training acc') plt.plot(epochs, val_acc, 'b', label='Validation acc') plt.title('Training and validation accuracy') plt.legend() plt.figure() plt.plot(epochs, loss, 'bo', label='Training loss') plt.plot(epochs, val_loss, 'b', label='Validation loss') plt.title('Training and validation loss') plt.legend() plt.show()
這些圖像中都能看出過擬合的特征。訓練精度隨著時間線性增加,直到接近 100%,而驗證精度則停留在 70%~72%。驗證損失僅在 5 輪后就達到最小值,然后保持不變,而訓練損失則一直線性下降,直到接近于 0。
因為訓練樣本相對較少(2000 個),所以過擬合是你最關心的問題。前面已經介紹過幾種降低過擬合的技巧,比如 dropout 和權重衰減(L2 正則化)。現在我們將使用一種針對于計算機視覺領域的新方法,在用深度學習模型處理圖像時幾乎都會用到這種方法,它就是數據增強(data augmentation)。
使用數據增強
過擬合的原因是學習樣本太少,導致無法訓練出能夠泛化到新數據的模型。如果擁有無限的數據,那么模型能夠觀察到數據分布的所有內容,這樣就永遠不會過擬合。數據增強是從現有的訓練樣本中生成更多的訓練數據,其方法是利用多種能夠生成可信圖像的隨機變換來增加(augment)樣本。其目標是,模型在訓練時不會兩次查看完全相同的圖像。這讓模型能夠觀察到數據的更多內容,從而具有更好的泛化能力。
在 Keras中,這可以通過對 ImageDataGenerator實例讀取的圖像執行多次隨機變換來實現。我們先來看一個例子。
datagen = 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')
這里只選擇了幾個參數(想了解更多參數,請查閱 Keras 文檔)。我們來快速介紹一下這些參數的含義。
rotation_range 是角度值(在 0~180 范圍內),表示圖像隨機旋轉的角度范圍。
width_shift 和 height_shift 是圖像在水平或垂直方向上平移的范圍(相對于總寬度或總高度的比例)。
shear_range 是隨機錯切變換的角度。
zoom_range 是圖像隨機縮放的范圍。
horizontal_flip 是隨機將一半圖像水平翻轉。如果沒有水平不對稱的假設(比如真實世界的圖像),這種做法是有意義的。
fill_mode是用于填充新創建像素的方法,這些新像素可能來自于旋轉或寬度 / 高度平移。
我們來看一下增強后的圖像:
from keras.preprocessing import image fnames = [os.path.join(train_cats_dir, fname) for fname in os.listdir(train_cats_dir)] # We pick one image to "augment"(選擇一張圖像進行增強) img_path = fnames[3] # Read the image and resize it(讀取圖像并調整大小) img = image.load_img(img_path, target_size=(150, 150)) # Convert it to a Numpy array with shape (150, 150, 3)(將其轉換為形狀 (150, 150, 3) 的 Numpy 數組) x = image.img_to_array(img) # Reshape it to (1, 150, 150, 3)(將其形狀改變為 (1, 150, 150, 3)) x = x.reshape((1,) + x.shape) # The .flow() command below generates batches of randomly transformed images. # It will loop indefinitely, so we need to `break` the loop at some point! #生成隨機變換后的圖像批量。 循環是無限的,因此你需要在某個時刻終止循環 i = 0 for batch in datagen.flow(x, batch_size=1): plt.figure(i) imgplot = plt.imshow(image.array_to_img(batch[0])) i += 1 if i % 4 == 0: break plt.show()
model = models.Sequential() model.add(layers.Conv2D(32, (3, 3), activation='relu', input_shape=(150, 150, 3))) model.add(layers.MaxPooling2D((2, 2))) model.add(layers.Conv2D(64, (3, 3), activation='relu')) model.add(layers.MaxPooling2D((2, 2))) model.add(layers.Conv2D(128, (3, 3), activation='relu')) model.add(layers.MaxPooling2D((2, 2))) model.add(layers.Conv2D(128, (3, 3), activation='relu')) model.add(layers.MaxPooling2D((2, 2))) model.add(layers.Flatten()) model.add(layers.Dropout(0.5)) model.add(layers.Dense(512, activation='relu')) model.add(layers.Dense(1, activation='sigmoid')) model.compile(loss='binary_crossentropy', optimizer=optimizers.RMSprop(lr=1e-4), metrics=['acc'])
我們來訓練這個使用了數據增強和 dropout 的網絡。
train_datagen = ImageDataGenerator( rescale=1./255, rotation_range=40, width_shift_range=0.2, height_shift_range=0.2, shear_range=0.2, zoom_range=0.2, horizontal_flip=True,) # Note that the validation data should not be augmented!(注意,不能增強驗證數據) test_datagen = ImageDataGenerator(rescale=1./255) train_generator = train_datagen.flow_from_directory( # This is the target directory(目標目錄) train_dir, # All images will be resized to 150x150(將所有圖像的大小調整為 150×150) target_size=(150, 150), batch_size=32, # Since we use binary_crossentropy loss, we need binary labels #(因為使用了 binary_crossentropy損失,所以需要用二進制標簽) class_mode='binary') validation_generator = test_datagen.flow_from_directory( validation_dir, target_size=(150, 150), batch_size=32, class_mode='binary') history = model.fit_generator( train_generator, steps_per_epoch=100, epochs=100, validation_data=validation_generator, validation_steps=50)
部分訓練結果:
我們把模型保存下來,你會在卷積可視化這節用到它。
model.save('cats_and_dogs_small_2.h5')
再次畫下我們的結果:
acc = history.history['acc'] val_acc = history.history['val_acc'] loss = history.history['loss'] val_loss = history.history['val_loss'] epochs = range(len(acc)) plt.plot(epochs, acc, 'bo', label='Training acc') plt.plot(epochs, val_acc, 'b', label='Validation acc') plt.title('Training and validation accuracy') plt.legend() plt.figure() plt.plot(epochs, loss, 'bo', label='Training loss') plt.plot(epochs, val_loss, 'b', label='Validation loss') plt.title('Training and validation loss') plt.legend() plt.show()
使用了數據增強和 dropout 之后,模型不再過擬合:訓練曲線緊緊跟隨著驗證曲線。現在的精度為 82%,比未正則化的模型提高了 15%(相對比例)。
通過進一步使用正則化方法以及調節網絡參數(比如每個卷積層的過濾器個數或網絡中的層數),你可以得到更高的精度,可以達到 86% 到 87%。但只靠從頭開始訓練自己的卷積神經網絡,再想提高精度就十分困難,因為可用的數據太少。想要在這個問題上進一步提高精度,下一步需要使用預訓練的模型,這是接下來的重點。
Keras 機器學習 神經網絡
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。