文章代碼來源:《deep learning on keras》,非常好的一本書,大家如果英語好,推薦直接閱讀該書,如果時間不夠,可以看看此系列文章,文章為我自己翻譯的內容加上自己的一些思考,水平有限,多有不足,請多指正,翻譯版權所有,若有轉載,請先聯系本人。
個人方向為數值計算,日后會向深度學習和計算問題的融合方面靠近,若有相近專業人士,歡迎聯系。
系列文章:
一、搭建屬于你的第一個神經網絡
二、訓練完的網絡去哪里找
三、【keras實戰】波士頓房價預測
四、keras的function API
五、keras callbacks使用
六、機器學習基礎Ⅰ:機器學習的四個標簽
七、機器學習基礎Ⅱ:評估機器學習模型
八、機器學習基礎Ⅲ:數據預處理、特征工程和特征學習
九、機器學習基礎Ⅳ:過擬合和欠擬合
十、機器學習基礎Ⅴ:機器學習的一般流程十一、計算機視覺中的深度學習:卷積神經網絡介紹
十二、計算機視覺中的深度學習:從零開始訓練卷積網絡
十三、計算機視覺中的深度學習:使用預訓練網絡
十四、計算機視覺中的神經網絡:可視化卷積網絡所學到的東西
用很少的數據來訓練圖像分類模型在實踐中很常見,如果你是計算機視覺背景專業出身的。
有少量樣本意味著有幾百到幾萬張圖片。作為實例教學,我們將會集中注意力于貓狗分類,數據集中含有2000張貓,2000張狗。我們將會使用2000張用來訓練,1000張用來驗證,最后1000張用來測試。
在這部分,我們將回顧一個基本的解決這個問題的方法:從零訓練一個新的模型。我們從直接在我們的2000個訓練樣本上訓練一個小卷積網絡開始,沒有任何正則化,建立一個能達到的最低水平。我們的分類準確率達到了71%。這那一點,我們的主要問題在過擬合上。我們將會介紹數據增加,一種在計算機視覺中有效預防過擬合的方法。通過利用數據增加,我們把我們的網絡的準確率提升到了82%。
在接下來的部分,我們將會回顧兩個重要的應用在深度學習小樣本的方法:在預訓練的網絡上做特征提取(這將幫助我們達到90%到96%的準確率)以及調好參數的預訓練網絡(可以將準確率提升到97%)。一起來說,這三個方法——從零訓練小模型,使用預訓練模型來做特征提取,調節預處理模型的參數——將會組成你以后解決計算機視覺問題中的小數據集時的工具包。
小數據問題的深度學習關聯
你有的時候會聽到深度學習只有當有很多數據的時候才起作用。這在一定程度上是一個有效的點:一個深度學習的基本特征是它能找到訓練數據本身的有意思的特征,不需要任何人工特征工程,這也只能在有很多訓練樣本的時候是可行的。這對于輸入樣本有比較高的維數時尤為正確,比如說圖像。
然而,構成很多樣本的都是相關的。不可能通過十來個樣本就訓練一個網絡去解決復雜的問題,但是對于比較小的,正則化好的模型,數百個樣本也足夠了。因為卷積網絡學習局部,具有平移不變性的特征,具有很高的數據效率。在一個很小的圖像數據集上從零開始訓練一個卷積網絡,仍將產生合理的結果,盡管缺少數據,無需任何自定義的特征工程。你將在這一部分看到。
但是,深度學習模型是自然能高度重新設計的:你能將一個模型用到不同的數據集上,只需要一丁點的改動即可。特別的,很多訓練好的模型都能下載了,能夠用來引導小數據的情況。
下載數據
The cats vs. dogs dataset在keras里面沒有,但是在Kaggle里面的2013下載到。
下載后的數據如下所示:
不出意料的,在2013的Kaggle競賽中,使用convnets的贏得了比賽。達到了95%的準確率,接下來我們得到的會很接近這個準確率,我們實際使用的樣本還不足原本競賽給出數據的10%,競賽包含25000個貓狗圖,大小為543MB(壓縮后)。在下載和解壓后,我們將會生成一個新的數據集,包含三個子集:一個貓狗各有1000個樣本的訓練集,各有500個樣本的驗證集,和各有500個樣本的測試集。
接下來就是幾行做這個的代碼:
import os, shutil
# The path to the directory where the original
# dataset was uncompressed
original_dataset_dir = '/Users/fchollet/Downloads/kaggle_original_data'
# The directory where we will
# store our smaller dataset
base_dir = '/Users/fchollet/Downloads/cats_and_dogs_small'
os.mkdir(base_dir)
# Directories for our training,
# validation and test splits
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)
# Directory with our training cat pictures
train_cats_dir = os.path.join(train_dir, 'cats')
os.mkdir(train_cats_dir)
# Directory with our training dog pictures
train_dogs_dir = os.path.join(train_dir, 'dogs')
os.mkdir(train_dogs_dir)
# Directory with our validation cat pictures
validation_cats_dir = os.path.join(validation_dir, 'cats')
os.mkdir(validation_cats_dir)
# Directory with our validation dog pictures
validation_dogs_dir = os.path.join(validation_dir, 'dogs')
os.mkdir(validation_dogs_dir)
# Directory with our validation cat pictures
test_cats_dir = os.path.join(test_dir, 'cats')
os.mkdir(test_cats_dir)
# Directory with our validation dog pictures
test_dogs_dir = os.path.join(test_dir, 'dogs')
os.mkdir(test_dogs_dir)
# Copy first 1000 cat images to 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
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
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
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
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
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)
首先給出原始數據和新數據集的存放位置。
再在新數據集目錄下分別寫訓練集、驗證集、測試集的位置。
再分別在三個集合下面建立貓和狗的文件夾。
最后,把原數據集里面的前1000個數據放進新訓練集,接下來的500個放進驗證集,再接下來五百個放進測試集。
最后使用程序數一數我們放對了嗎?
>>> 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
這樣一來,我們就得到了所需小數據集。
構建我們的神經網絡
我們已經在MNIST中構建了一個小的卷積神經網絡,所以你應該對這個很熟。我們將會重復使用相同的生成框架:我們的卷積網絡就是一些卷積層和最大池化層的堆疊。
然而,由于我們在解決大點的圖像和更加復雜的問題,我們要讓我們的網絡相應的也更大:將會有更多的卷積層和最大池化層的組合。這將擴大網絡的容量,并減少特征映射的大小,使得他們在拉伸層不會過大。這里,由于我們輸入的大小從開始(隨便選的一個),我們最終得到了
的特征映射。
注意特征映射的深度從32提升到了128,同時特征映射的大小在下降(從到
)
由于我們在攻擊一個二分類問題,我們的網絡最終只需要一個單元。
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'))
看一下結構
>>> model.summary()
Layer (type) Output Shape Param #
================================================================
conv2d_1 (Conv2D) (None, 148, 148, 32) 896
________________________________________________________________
maxpooling2d_1 (MaxPooling2D) (None, 74, 74, 32) 0
________________________________________________________________
conv2d_2 (Conv2D) (None, 72, 72, 64) 18496
________________________________________________________________
maxpooling2d_2 (MaxPooling2D) (None, 36, 36, 64) 0
________________________________________________________________
conv2d_3 (Conv2D) (None, 34, 34, 128) 73856
________________________________________________________________
maxpooling2d_3 (MaxPooling2D) (None, 17, 17, 128) 0
________________________________________________________________
conv2d_4 (Conv2D) (None, 15, 15, 128) 147584
________________________________________________________________
maxpooling2d_4 (MaxPooling2D) (None, 7, 7, 128) 0
________________________________________________________________
flatten_1 (Flatten) (None, 6272) 0
________________________________________________________________
dense_1 (Dense) (None, 512) 3211776
________________________________________________________________
dense_2 (Dense) (None, 1) 513
================================================================
Total params: 3,453,121
Trainable params: 3,453,121
Non-trainable params: 0
在最后compilation的步驟,我們會像往常一樣,使用RMSprop優化器,因為我們使用一個sigmoid單元在我們的模型最后,我們將使用二進交叉熵作為損失函數,記住,你要是不知道怎么選這些東西了,可以翻一翻之前列的表。
from keras import optimizers
model.compile(loss='binary_crossentropy',
optimizer=optimizers.RMSprop(lr=1e-4),
metrics=['acc'])
數據預處理
你現在已經知道,數據在喂進網絡之前需要預處理成浮點數張量。目前我們的數據來自于JPEG文件,所以其處理步驟大致為:
- 讀圖片文件
- 將JPEG解碼為RBG
- 將它們轉化為浮點張量
- 將像素點的值歸一化
這看起來有點冗雜,但所幸,keras能夠自動做完上述步驟。keras有一個圖像處理幫助工具,位于keras.preprocessing.image。特別的,其包括ImageDataGenerator類,能夠快速設置Pyhon的生成器,從而快速將磁盤上圖片文件加入預處理張量批次。這就是我們將要使用的。
注意:理解Python中的生成器(generators)
Python的生成器是一個對象,像一個迭代器一樣工作,即一個對象可以使用for/in操作符。生成器使用yield操作符來建成。
這里有一個用生成器生成整數的例子:
def generator():
i = 0
while True:
i += 1
yield i
for item in generator():
print(item)
if item > 4:
break
1
2
3
4
5
使用圖像數據生成器來從目錄中讀取圖片
from keras.preprocessing.image import ImageDataGenerator
# All images will be rescaled by 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
target_size=(150, 150),
batch_size=20,
# Since we use binary_crossentropy loss, we need binary labels
class_mode='binary')
validation_generator = test_datagen.flow_from_directory(
validation_dir,
target_size=(150, 150),
batch_size=20,
class_mode='binary')
講解一下代碼:
首先定義train_datagen這個生成器,重置像素點大小的命令寫在括號中,然后validation_generator實際得到的是一個張量,張量形狀為(20,150,150,3),而生成這個所用的就是生成器的flow_from_directory屬性,第一個參數填文件目錄,第二個參數填將圖片重置的大小,第三個參數填每一批次取得個數,最后一個參數填標簽類別。
展示數據和標簽
>>> 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是等價的。先放代碼:
history = model.fit_generator(
train_generator,
steps_per_epoch=100,
epochs=30,
validation_data=validation_generator,
validation_steps=50)
第一個參數是我們生成器,第二個參數是每一批需要進行的步數,由于我們生成器每次生成20個數據,所以需要100步才能遍歷完2000個數據,驗證集的類似知道為什么是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()
這組圖表明過擬合了,我們的訓練準確率隨時間線性增加,直到最后接近100%,但我們的驗證集停滯在了70-72%。我們驗證損失在5批次達到最小值以后就停滯了,盡管訓練損失持續降低,直到最后接近0.
因為我們只用了很少的訓練數據2000個,過擬合是我們最關心的。你已經知道一系列方法去防止過擬合,比如dropout和權重衰減(L2正則化)。我們現在要介紹一種新的,特別針對于計算機視覺的,常常被用在深度學習模型中處理數據的:數據增加。
使用數據增加
過擬合是由于樣本太少造成的,導致我們無法訓練模型去泛化新數據。給定無限的數據,我們的模型就會暴露在各種可能的數據分布情況中:我們從不會過擬合。數據增加采用了從存在的訓練樣本中生成更多訓練數據的方法,通過一系列隨機的變換到可辨識的其它圖像,來增加樣本數量。目的是在訓練的時候,我們的模型不會重復看到同一張圖片兩次。這幫助模型暴露在更多數據面前,從而有更好的泛化性。
在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在新出來像素以后,我們選擇填充的策略。
讓我們看一看圖像增加:
# This is module with image preprocessing utilities
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)
x = image.img_to_array(img)
# Reshape it to (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()
雖然我們可以保證訓練過程中,模型不會看到相同的兩張圖,但是畢竟我們只是對原圖混合了一下,并沒有增加什么新的信息,所以無法完全避免過擬合,為了進一步抗擊過擬合,我們加入了dropout層:
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
target_size=(150, 150),
batch_size=32,
# Since we use binary_crossentropy loss, we need binary labels
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')
讓我們再畫出訓練和驗證的結果看看:
多虧了數據增強和dropout,我們不再過擬合了:訓練曲線和驗證曲線十分相近。我們現在能夠達到82%的準確率,比未正則化的模型要提高了15%。
通過利用正則化方法,或者更進一步:調參數,我們能達到更好的準確率,近乎86-87%。然而,這證明從零開始訓練我們的卷積網絡已經難以更好了,因為我們只有很少的數據來處理。下一步我們提高準確率的方法是利用預訓練的網絡,這將在接下來兩部分進行講解。