Python深度學習(在小型資料集上從頭開始訓練一個卷積神經網路)--學習筆記(十)

呆萌的小透明發表於2020-11-10

5.2 在小型資料集上從頭開始訓練一個卷積神經網路

  • 使用很少的資料來訓練一個影像分類模型,這是很常見的情況,如你要從事計算機視覺方面的職業,很可能會在實踐中遇到這種情況。“很少的”樣本可能是幾百張影像,也可能是幾萬張影像。

5.2.1 深度學習與小資料問題的相關性

  • 有時你會聽人說,僅在有大量資料可用時,深度學習才有效。這種說法部分正確:深度學習的一個基本特性就是能夠獨立地在訓練資料中找到有趣的特徵,無須認為的特徵工程,而這隻在擁有大量訓練樣本時才能實現。對於輸入樣本的維度非常高(比如影像)的問題尤其如此。
  • 但對於初學者來說,所謂“大量”樣本是相對的,即相對於你所要訓練網路的大小和深度而言。只用幾十個樣本訓練卷積神經網路就解決一個複雜問題是不可能的,但如果模型很小,並做了很好的正則化,同事任務非常簡單,那麼幾百個樣本可能就足夠了。由於卷積神經網路學到的是區域性的、平移不變的特徵,它對於感知問題可以高效地利用資料。雖然資料相對較少,但在非常小的影像資料集上從頭開始訓練一個卷積神經網路,仍然可以得到不錯的結果,而且無須任何自定義的特徵工程。
  • 此外,深度學習模型本質上具有高度的可複用性,比如,已有一個在大規模資料集上訓練的影像分類模型或語音轉文字模型,你只需做很小的修改就能將其複用於完全不同的問題。特別是在計算機視覺領域,許多預訓練的模型(通常都是在ImageNet資料集上訓練得到的)現在都可以公開下載,並可以用於在資料很少的情況下構建強大的視覺模型。

5.2.2 下載資料

  • 貓狗分類資料集包含25000張貓狗影像(每個類別都有12500張),大小為543MB。下載資料並解壓之後,你需要建立一個新資料集,其中包含三個子集:每個類別各100個樣本的訓練集、每個類別各500個樣本的驗證集和每個類別各500個樣本的測試集。
# 將影像複製到訓練、驗證和測試的目錄
import os, shutil

original_dataset_dir = "../data/train/train" # 原始資料集目錄
base_dir = "./data"

os.mkdir(base_dir)

train_dir = os.path.join(base_dir, "train")
os.mkdir(train_dir)

validation_dir = os.path.join(base_dir, "validation")
os.mkdir(validation_dir)

test_dir = os.path.join(base_dir, "test")
os.mkdir(test_dir)

train_cats_dir = os.path.join(train_dir, 'cats')
os.mkdir(train_cats_dir)

train_dogs_dir = os.path.join(train_dir, 'dogs')
os.mkdir(train_dogs_dir)

validation_cats_dir = os.path.join(validation_dir, 'cats')
os.mkdir(validation_cats_dir)
validation_dogs_dir = os.path.join(validation_dir, 'dogs')
os.mkdir(validation_dogs_dir)

test_cats_dir = os.path.join(test_dir, 'cats')
os.mkdir(test_cats_dir)
test_dogs_dir = os.path.join(test_dir, 'dogs')
os.mkdir(test_dogs_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)

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)

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)

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)

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)

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)))
print('total training dog images:', len(os.listdir(train_dogs_dir)))
print('total validation cat images:', len(os.listdir(validation_cats_dir)))
print('total validation dog images:', len(os.listdir(validation_dogs_dir)))
print('total test cat images', len(os.listdir(test_cats_dir)))
print('total test dog images', len(os.listdir(test_dogs_dir)))

5.2.3 構建網路

  • 網路中特徵圖的深度在逐漸增大(從32增大到128),而特徵圖的尺寸在逐漸減少(從150x150減小到7x7)。這幾乎是所有卷積神經網路的模式。
# 將貓狗分類的小型卷積神經網路例項化
from keras import layers
from keras import models

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'))

print(model.summary())
# 配置模型用於訓練
from keras import optimizers

model.compile(loss='binary_crossentropy', optimizer=optimizers.RMSprop(lr=1e-4), metrics=['acc'])

5.2.4 資料預處理

  • 你現在已經知道,將資料輸入神經網路之前,應該將資料格式化為經過預處理的浮點數張量。現在,資料以JPEG檔案的形式儲存在硬碟中,所以資料預處理步驟大致如下:(1)讀取影像檔案;(2)將JPEG檔案解碼為RGB畫素網路;(3)將這些畫素網路轉換為浮點數張量;(4)將畫素值(0-255範圍內)縮放到[0,1]區間(神經網路喜歡處理較小的輸入值)。
  • Keras有一個影像處理輔助工具的模組,位於keras.preprocessing.image。特別地,它包含ImageDataGenerator類,可以快速建立Python生成器,能夠將硬碟上的影像檔案自動轉換為預處理好的張量批量。
# 用ImageDataGenerator從目錄中讀取影像
from keras.preprocessing.image import ImageDataGenerator

# 將所有影像乘以1/255縮放
train_datagen = ImageDataGenerator(rescale=1./255)
test_datagen = ImageDataGenerator(rescale=1./255)

train_generator = train_datagen.flow_from_directory("./data/train", target_size=(150,150),batch_size=20,class_mode='binary')

validation_generator = test_datagen.flow_from_directory("./data/validation", target_size=(150, 150), batch_size=20, class_mode='binary')

# 利用批量生成器擬合模型
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')

# 繪製訓練過程中的損失曲線和精度曲線
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(1, len(acc) + 1)

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()
  • 利用生成器,我們讓模型對資料進行擬合。我們將是會用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)
  • 因為訓練樣本相對較少(2000個),所以過擬合是你最關心的問題。前面已經介紹過幾種降低過擬合的技巧,比如dropout和權重衰減(L2正則化)。現在我們將使用一種針對於計算機視覺領域的新方法,在用深度學習模型處理影像時機會都會用到這種方法,它就是資料增強(data augmentation)。

5.2.5 使用資料增強

  • 過擬合的原因是學習樣本太少,導致無法訓練出能夠泛化到新資料的模型。如果擁有無限的資料,那麼模型能夠觀察到資料分佈的所有內容,這樣就永遠不會過擬合。資料增強是從現有的訓練樣本中生成更多的訓練資料,其方法是利用多種能夠生成可信影像的隨機變換來增加(augment)樣本。其目標是,模型在訓練時不會兩次檢視完全相同的影像。這讓模型能夠觀察到資料的更多內容,從而具有更好的泛化能力。
  • 在Keras中,可以通過對ImageDataGenerator例項讀取的影像執行多次隨機變換來實現。
# 利用ImageDataGenerator來設定資料增強
datagen = ImageDataGenerator(rotation_range=40, width_shify_range=0.2, height_shift_range=0.2, shear_range=0.2, zoom_range=0.2, horizontal_filp=True, fill_mode='nearest')
  • rotation_range是角度值(在0~180範圍內),表示影像隨機旋轉的角度範圍。
  • width_shift和height_shift是影像在水平或垂直方向上平移的範圍(相對於總寬度或總高度的比例)。
  • shear_range:是隨機錯切變換的角度
  • zoom_range:是影像隨機縮放的範圍
  • horizontal_flip:是隨機將一般影像水平翻轉。如果沒有水平不對稱的假設(比如真實世界的影像),這種做法是有意義的。
  • fill_mode:是用於填充新建立畫素的方法,這些新畫素可能來自於旋轉或寬度/高度平移。
# 顯示幾個隨機增強後的訓練影像
from keras.preprocessing import image
from keras.preprocessing.image import ImageDataGenerator
import os
import matplotlib.pyplot as plt

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')

fnames = [os.path.join("./data/train/cats", fname) for fname in os.listdir("./data/train/cats")]

img_path = fnames[3]

img = image.load_img(img_path, target_size=(150, 150))
x = image.img_to_array(img)

x = x.reshape((1, ) + x.shape)
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()
  • 如果你使用這種資料增強來訓練一個新網路,那麼網路將不會兩次看到同樣的輸入。但網路看到的輸入仍然是高度相關的,因為這些輸入都來自於少量的原始影像。你無法生成新資訊,而只能混合現有資訊。因此,這種方法可能不足以完全消除過擬合。為了進一步降低過擬合,你還需要向模型中新增一個dropout層,新增到密集連線分類器之前。
# 定義一個包含dropout的新卷積神經網路
from keras import models
from keras import layers
from keras import optimizers
from keras.preprocessing.image import ImageDataGenerator

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'])

# 利用資料增強生成器訓練卷積神經網路
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)

test_datagen = ImageDataGenerator(rescale=1./255)

train_generator = train_datagen.flow_from_directory("./data/train", target_size=(150, 150), batch_size=20, class_mode='binary')

validation_generator = test_datagen.flow_from_directory("./data/validation", target_size=(150, 150), batch_size=20, 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')

# 繪製影像
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(1, len(acc) + 1)

plt.plot(epochs, acc, 'bo', label='Training acc')
plt.plot(epochs, val_acc, 'b', label='Validation acc')
plt.title('Training and validation accuracy')

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()

相關文章