從Google下載貓狗訓練集與驗證集的zip壓縮包,提取到項目目錄下。這個文件夾裏面包含訓練(train
)和驗證(validation
)數據集的子目錄,而且每個子目錄都包含貓和狗的子目錄。
可以直接在它這個目錄下創建一個python文件,就叫貓和狗(cats_and_dogs.py
),然後配置好訓練集、驗證集的目錄。
base_dir = '../cats_and_dogs_filtered'
train_dir = os.path.join(base_dir, 'train')
validation_dir = os.path.join(base_dir, 'validation')
# 用於訓練的貓圖目錄。
train_cats_dir = os.path.join(train_dir, 'cats')
# 用於訓練的狗圖目錄。
train_dogs_dir = os.path.join(train_dir, 'dogs')
# 用於驗證的貓圖目錄。
validation_cats_dir = os.path.join(validation_dir, 'cats')
# 用於驗證的狗圖目錄。
validation_dogs_dir = os.path.join(validation_dir, 'dogs')
現在可以看看貓(cats
)和狗(dogs
)在訓練(train
)和驗證(validation
)目錄中的文件命名是怎麼約定的。
train_cat_fnames = os.listdir(train_cats_dir)
print('貓的訓練、驗證目錄中的文件命名約定:\n%s' % (train_cat_fnames[:10]))
train_dog_fnames = os.listdir(train_dogs_dir)
train_dog_fnames.sort()
print('狗的訓練、驗證目錄中的文件命名約定:\n%s' % (train_dog_fnames[:10]))
'''
貓的訓練、驗證目錄中的文件命名約定:
['cat.952.jpg', 'cat.946.jpg', 'cat.6.jpg', 'cat.749.jpg', 'cat.991.jpg', 'cat.985.jpg', 'cat.775.jpg', 'cat.761.jpg', 'cat.588.jpg', 'cat.239.jpg']
狗的訓練、驗證目錄中的文件命名約定:
['dog.0.jpg', 'dog.1.jpg', 'dog.10.jpg', 'dog.100.jpg', 'dog.101.jpg', 'dog.102.jpg', 'dog.103.jpg', 'dog.104.jpg', 'dog.105.jpg', 'dog.106.jpg']
'''
還可以找出訓練和驗證目錄中貓和狗圖像的總數。
print('總共用於訓練的貓圖像:', len(os.listdir(train_cats_dir)))
print('總共用於訓練的狗圖像:', len(os.listdir(train_dogs_dir)))
print('總共用於驗證的貓圖像:', len(os.listdir(validation_cats_dir)))
print('總共用於驗證的狗圖像:', len(os.listdir(validation_dogs_dir)))
'''
總共用於訓練的貓圖像: 1000
總共用於訓練的狗圖像: 1000
總共用於驗證的貓圖像: 500
總共用於驗證的狗圖像: 500
'''
現在知道了,有1,000個貓狗的訓練圖片,還有500個貓狗的驗證圖片。現在可以再看幾張圖片,以便提前地瞭解一下貓狗數據集裏都是些什麼圖片。
import matplotlib.pyplot as plt
import matplotlib.image as mpimg
# 輸出圖表的參數,將以4x4的配置輸出貓狗數據集的部分圖片。
nrows = 4
ncols = 4
# 迭代圖像的當前索引。
pic_index = 0
# 設置matplotlib(Python的2D繪圖庫)圖,並將其設置爲適合4x4圖片大小。
fig = plt.gcf()
fig.set_size_inches(ncols * 4, nrows * 4)
pic_index += 8
next_cat_pix = [os.path.join(train_cats_dir, fname)
for fname in train_cat_fnames[pic_index-8:pic_index]]
next_dog_pix = [os.path.join(train_dogs_dir, fname)
for fname in train_dog_fnames[pic_index-8:pic_index]]
for i, img_path in enumerate(next_cat_pix+next_dog_pix):
# 設置子圖,子圖的索引從1開始。
sp = plt.subplot(nrows, ncols, i + 1)
# 不顯示軸(或者說網格線)。
sp.axis('Off')
img = mpimg.imread(img_path)
plt.imshow(img)
plt.show()
輸出的實際效果如下圖所示,每次重新運行都會查看到新的一批圖片。
現在準備從頭開始構建小型Convnet(開源的卷積神經網絡代碼),並期望能達到72%的準確率。我們要處理的圖片是150x150的彩色圖片,編寫代碼,將堆疊3個卷積層(convolution
)、修正線性激活函數(relu
)和最大池化層(maxpooling
)模塊。
我們的卷積層(convolution
)過濾器大小爲3x3,我們的最大池化層(maxpooling
)過濾器爲2x2。
- 第一個卷積層(
convolution
):設置16個3x3大小的過濾器,即識別16種輪廓及邊緣,最後通過修正線性激活函數(relu
)形成一個3x3x16的立方體。 - 第二個卷積層(
convolution
):設置32個3x3大小的過濾器,即識別32種輪廓及邊緣,最後通過修正線性激活函數(relu
)形成一個3x3x32的立方體。 - 第三個卷積層(
convolution
):設置64個3x3大小的過濾器,即識別64種輪廓及邊緣,最後通過修正線性激活函數(relu
)形成一個3x3x64的立方體。
每一個卷積層(convolution
)成形之後,都會追加一個最大池化層(maxpooling
),通過池化操作來降低卷積層輸出的特徵向量,同時改善結果,使其不易出現過擬合。
注意:這是一種廣泛使用的配置,並且已知可以很好地用於圖像分類。
此外,由於我們只有相對較少的訓練樣本(1,000張),僅使用三個卷積模塊就可以保持模型較小,從而降低過度擬合(overfitting
)的風險。過擬合(overfitting
)的意思是:對見過的數據,分類效果極好;而對沒見過的數據,表現很糟糕。
from tensorflow import keras
# 輸入特徵圖是150x150x3,其中150x150用於圖像像素,3用於三個顏色通道(R、G和B)。
img_input = keras.layers.Input(shape=(150, 150, 3))
# 第一個卷積層提取3x3x16的特徵,使用修正線性激活函數(`relu`),然後是具有2x2大小的最大池化層。
x = keras.layers.Conv2D(16, 3, activation='relu')(img_input)
x = keras.layers.MaxPooling2D(2)(x)
# 第二個卷積層提取3x3x32的特徵,使用修正線性激活函數(`relu`),然後是具有2x2大小的最大池化層。
x = keras.layers.Conv2D(32, 3, activation='relu')(x)
x = keras.layers.MaxPooling2D(2)(x)
# 第三個卷積層提取3x3x64的特徵,使用修正線性激活函數(`relu`),然後是具有2x2大小的最大池化層。
x = keras.layers.Conv2D(64, 3, activation='relu')(x)
x = keras.layers.MaxPooling2D(2)(x)
然後是最重要一步,創建兩個全連接層(dense
),因爲我們正面臨着兩個分類問題,即二元分類問題,我們將以sigmoid
函數(一個常用的神經網絡激勵函數)激活我們的網絡,以便我們的網絡輸出將是0~1之間的單個標量,當前編碼的概率圖像是一維(而不是零維)。
# 將特徵圖展平爲一維數據(`1-dim`)張量,以便添加全連接層。
x = keras.layers.Flatten()(x)
# 使用修正線性激活函數(`relu`)和512個隱藏單元(或神經元)創建全連接層。
x = keras.layers.Dense(512, activation='relu')(x)
# 使用單個節點(或神經元)和`sigmoid`激活函數創建輸出層。
output = keras.layers.Dense(1, activation='sigmoid')(x)
# 創建模型:
# input = 輸入特徵映射
# output = 輸入特徵映射 + 堆疊卷積層/最大池化層數 + 全連接層
# 全連接層 + `sigmoid`輸出層
model = keras.Model(img_input, output)
最後,可以總結一下整個模型的框架,欣賞一下我們創建的卷積神經網絡模型。
model.summary()
'''
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
input_1 (InputLayer) (None, 150, 150, 3) 0
_________________________________________________________________
conv2d (Conv2D) (None, 148, 148, 16) 448
_________________________________________________________________
max_pooling2d (MaxPooling2D) (None, 74, 74, 16) 0
_________________________________________________________________
conv2d_1 (Conv2D) (None, 72, 72, 32) 4640
_________________________________________________________________
max_pooling2d_1 (MaxPooling2 (None, 36, 36, 32) 0
_________________________________________________________________
conv2d_2 (Conv2D) (None, 34, 34, 64) 18496
_________________________________________________________________
max_pooling2d_2 (MaxPooling2 (None, 17, 17, 64) 0
_________________________________________________________________
flatten (Flatten) (None, 18496) 0
_________________________________________________________________
dense (Dense) (None, 512) 9470464
_________________________________________________________________
dense_1 (Dense) (None, 1) 513
=================================================================
Total params: 9,494,561
Trainable params: 9,494,561
Non-trainable params: 0
_________________________________________________________________
'''
上面輸出中的“Output Shape”列顯示特徵圖的大小如何在每個連續圖層中演變。很明顯,由於沒有設置填充(padding
),卷積層(conv
)會將特徵映射的大小減少一點,並且每個最大池化層(max_pooling
)將特徵映射減半。
接下來,我們要配置模型訓練的規範,我們將使用交叉熵損失函數(binary_crossentropy
)訓練我們的模型,因爲這是一個二進制分類問題,我們的最終激活函數是一個sigmoid
函數。
我們將使用學習率爲0.001的rmsprop
(一種常用的深度學習優化算法)優化器。同時,在訓練期間,我們想監控分類準確性。
注意:在當前情況下,使用rmsprop
優化算法優於隨機梯度下降(SGD
)算法,因爲rmsprop
會自動調整學習速率。
model.compile(loss='binary_crossentropy',
optimizer=keras.optimizers.RMSprop(lr=0.001),
metrics=['acc'])
現在,讓我們設置數據生成器,它將讀取源文件夾中的圖片,將它們轉換爲float32
的張量,並將它們附帶上標籤提供給我們的網絡。這樣的話,我們將有一個用於訓練圖像的生成器和一個用於驗證圖像的生成器,生成器將生產20個尺寸爲150x150的圖像及其標籤(二進制)。
通常進入神經網絡的數據應該以某種方式進行標準化,以使其更適合網絡處理。(將原始像素直接輸入網絡的情況並不常見)在這裏,我們通過將像素值歸一化爲0~1範圍來預處理圖像(最初所有值都在0~255範圍內)。
在Keras(用Python編寫的高級神經網絡API)中,可以使用rescale
參數通過keras.preprocessing.image.ImageDataGenerator
類完成此操作。此類允許我們通過.flow(data, labels)
或.flow_from_directory(directory)
實例化增強圖像批次(及其標籤)的生成器。然後,這些生成器可以與接受數據生成器作爲輸入的Keras模型方法一起使用:fit_generator
、evaluate_generator
和predict_generator
。
# 對所有圖像按照指定的尺度因子,進行放大或縮小,設置值在0~1之間,通常爲1./255。
train_datagen = keras.preprocessing.image.ImageDataGenerator(rescale=1./255)
test_datagen = keras.preprocessing.image.ImageDataGenerator(rescale=1./255)
# 使用train_datagen生成器分批輪流訓練20張圖像。
train_generator = train_datagen.flow_from_directory(
# 這是訓練圖像的源目錄。
train_dir,
# 所有圖像將調整爲150x150大小。
target_size=(150, 150),
batch_size=20,
# 由於我們使用binary_crossentropy損失算法,我們需要二進制標籤。
class_mode='binary')
# 使用test_datagen生成器批量生成20個流程驗證圖像。
validation_generator = test_datagen.flow_from_directory(
validation_dir,
target_size=(150, 150),
batch_size=20,
class_mode='binary')
現在讓我們訓練所有的圖像,一共2000個,分15個時期,並驗證所有驗證圖像,一個1000個。
# 開始訓練。
history = model.fit_generator(
train_generator,
# 2000張圖片 = 批量大小 * 步進。
steps_per_epoch=100,
epochs=15,
validation_data=validation_generator,
# 1000張圖片 = 批量大小 * 步進。
validation_steps=50,
verbose=2)
我們可以做一個有意思的事情,就是可視化在訓練時如何模型是怎麼變化的,讓我們從訓練集中選擇一個隨機的貓或狗圖像,然後生成一個圖形,其中每一行是圖層的輸出,並且該行中的每個圖像都是該輸出特徵圖中的特定濾鏡。
import numpy as np
import random
# 定義一個新的模型,它將圖像作爲輸入,並在第一個模型之後輸出前一個模型中所有圖層的中間表示。
successive_outputs = [layer.output for layer in model.layers[1:]]
visualization_model = keras.Model(img_input, successive_outputs)
# 從訓練集中準備一隻貓或狗的隨機輸入圖像。
cat_img_files = [os.path.join(train_cats_dir, f) for f in train_cat_fnames]
dog_img_files = [os.path.join(train_dogs_dir, f) for f in train_dog_fnames]
img_path = random.choice(cat_img_files + dog_img_files)
# 這是PIL圖像。
img = keras.preprocessing.image.load_img(img_path, target_size=(150, 150))
# Numpy數組形狀(150,150,3)。
x = keras.preprocessing.image.img_to_array(img)
# Numpy數組形狀(1,150,150,3)。
x = x.reshape((1,) + x.shape)
# 重新縮放1/255。
x /= 255
# 通過神經網絡運行我們的圖像,從而獲得該圖像的所有中間表示。
successive_feature_maps = visualization_model.predict(x)
# 這些是圖層的名稱,因此可以將它們作爲我們圖表的一部分。
layer_names = [layer.name for layer in model.layers]
# 現在展示一下。
for layer_name, feature_map in zip(layer_names, successive_feature_maps):
if len(feature_map.shape) == 4:
# 只需對conv/maxpool(卷積/最大化池)圖層執行此操作,而不是全連接層。
# 特徵圖中的要素數量。
n_features = feature_map.shape[-1]
# 特徵圖具有形狀(1,size,size,n_features)。
size = feature_map.shape[1]
# 我們將在此矩陣中平鋪圖像。
display_grid = np.zeros((size, size * n_features))
for i in range(n_features):
# 對特徵該進行後處理,使其在視覺上更加美觀。
x = feature_map[0, :, :, I]
x -= x.mean()
x /= x.std()
x *= 64
x += 128
x = np.clip(x, 0, 255).astype('uint8')
# 我們將每個過濾器平鋪到這個大的水平網格中。
display_grid[:, i * size : (i + 1) * size] = x
# 顯示網格。
scale = 20. / n_features
plt.figure(figsize=(scale * n_features, scale))
plt.title(layer_name)
plt.grid(False)
plt.imshow(display_grid, aspect='auto', cmap='viridis')
# 顯示圖像。
plt.show()
當上面的代碼運行完之前,會依次顯示下面的幾張圖片。
正如上面所顯示的,我們看到了圖像從原始像素變爲越來越抽象和緊湊的展示,下游的展示開始突出顯示神經網絡關注的內容,並且它們顯示越來越少的特徵被“激活”;大多數都設置爲零,這被稱爲“稀疏性”,稀疏性是深度學習的關鍵特徵。
這些展示關於圖像的原始像素的信息越來越少,但是關於圖像類別的信息越來越精細,我們可以將convnet(卷積神經或一般的深層網絡)視爲信息蒸餾管道。
接下來我們可以評估一下模型的準確性和損失,繪製訓練期間收集的訓練/驗證準確性和損失。
# 檢索每個訓練時期的訓練和驗證數據集的準確度結果列表。
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)
plt.plot(epochs, val_acc)
plt.title('Training and validation accuracy')
plt.figure()
# 繪製每個時期的訓練和驗證損失。
plt.plot(epochs, loss)
plt.plot(epochs, val_loss)
plt.title('Training and validation loss')
plt.show()
上面的代碼運行結束前,會顯示下面圖片。
正如上面圖片所展示的,我們的模型明顯過度擬合了,我們的訓練準確度(藍線)接近100%,而我們的驗證準確度(橙線)停滯在70%上下。我們的驗證損失在僅僅5個時期後達到最小值,這是因爲我們的訓練樣本數量相對較少(2000個)。
因此,過度擬合應成爲我們的首要關注點,當太少特徵樣本的模型,不推廣到新數據的模式時,即當模型開始使用不相關的特徵進行預測時,就會發生過度擬合。例如,如果我們作爲人類,只能看到三個伐木工人的圖像,以及三個水手的圖像,其中只有一個戴帽子的人是伐木工人,你可能會開始認爲戴着帽子是一名伐木工人而不是水手的標誌。
然後我們就會做出一個非常糟糕的伐木工人/水手分類器,過度擬合是機器學習中的核心問題:假設我們將模型的參數擬合到給定的數據集,我們如何確保模型適用於以前從未見過的數據?我們如何避免學習特定於訓練數據的內容?
最後清理一下數據,運行以下代碼以終止內核並釋放內存資源。
os.kill(os.getpid(), signal.SIGKILL)
通過以上內容,就建立了一個簡單的識別貓狗的模型。