使用 tf.keras 進行圖片分類
既然 tf.keras 是我們在實際工作中使用最多的工具,那么這節(jié)課我們首先來學習如何使用 tf.keras 進行圖片的分類。
這節(jié)課會介紹圖像分類網(wǎng)絡的常用的網(wǎng)絡層、如何處理圖片數(shù)據(jù)以及整體程序的結構。
在這里我們采用的是 Kaggle 上的貓狗分類數(shù)據(jù)集,該數(shù)據(jù)集合的數(shù)據(jù)的標簽分為貓和狗兩個類別,他們的數(shù)量分別為:
- 訓練集合 2000 張圖片,包括 1000 張狗圖片和 1000 張貓圖片;
- 驗證集合 1000 張圖片,包括 500 張狗圖片和 500 張貓圖片。
我們會按照獲取數(shù)據(jù)、準備數(shù)據(jù)、訓練數(shù)據(jù)、數(shù)據(jù)可視化的順序幫助大家理解圖片分類的過程。
通過學習該節(jié)課程,我們會得到一個能夠分辨貓圖片和狗圖片、正確率在 70% 以上的分類模型。
1. 獲取數(shù)據(jù)
首先我們要引入我們所需要的程序包:
import tensorflow as tf
import os
import matplotlib.pyplot as plt
其中 os 用于目錄的相關操作,而 plt 用于我們的圖片繪制工作,然后我們進行數(shù)據(jù)的下載與解壓:
dataset_url = 'https://storage.googleapis.com/mledu-datasets/cats_and_dogs_filtered.zip'
path_download = os.path.dirname(tf.keras.utils.get_file('cats_and_dogs.zip',
origin=dataset_url,
extract=True))
train_dataset_dir = path_download + '/cats_and_dogs_filtered/train'
valid_dataset_dir = path_download + '/cats_and_dogs_filtered/validation'
這里的第二行操作是使用 Keras 內置的下載工具進行下載,返回的是數(shù)據(jù)集所在的路徑。
我們最后是結合數(shù)據(jù)集的下載位置與其內部文件建構得到訓練數(shù)據(jù)集與測試數(shù)據(jù)集所在的目錄,
其中 train_dataset_dir 是訓練集的目錄,而 valid_dataset_dir 是測試集合所在的目錄。
可以通過下面的方法來查看數(shù)據(jù)文件所在的目錄。
print(cat_train_dir, cat_validation_dir, dog_train_dir, dog_validation_dir)
然后我們定義一些數(shù)據(jù)集合與訓練的基本參數(shù),留作后面訓練之用:
BATCH_SIZE = 64
TRAIN_NUM = 2000
VALID_NUM = 1000
EPOCHS = 15
Height = 128
Width = 128
這些值的具體含義為:
- BATCH_SIZE: 批次的大小,我們會將 BATCH_SIZE 個圖片數(shù)據(jù)同時送給模型進行訓練,這個數(shù)值大家可以根據(jù)自己的內存大小或者顯存大小進行調整;
- TRAIN_NUM: 訓練集的數(shù)量,這里是 2000;
- VALID_NUM: 驗證集的數(shù)量,這里是 1000;
- EPOCHS: 訓練的周期數(shù),將所有的數(shù)據(jù)通用模型訓練一遍稱為一個 EPOCH,一般來說,EPOCH 的數(shù)值越大,模型在訓練集合上的準確率越高,相應的所需要的時間也更長;
- Height: 圖片的高度;
- Width: 圖片的長度。
2. 數(shù)據(jù)預處理
這里我們要使用 Keras 內部內置的 ImageDataGenerator 來作為迭代器產生數(shù)據(jù)。
train_image_generator = tf.keras.preprocessing.image.ImageDataGenerator(rescale=1./255)
valid_image_generator = tf.keras.preprocessing.image.ImageDataGenerator(rescale=1./255)
在這里我們定義了兩個圖片數(shù)據(jù)迭代器,同時我們定義了 rescale 參數(shù),在函數(shù)內部每張圖片的每個像素數(shù)據(jù)都會乘以 1./255,以將其歸一化到 [0, 1] 之間,因為機器學習之中模型的最好輸入數(shù)據(jù)是在 [0, 1] 之間。
然后我們使用文件夾中的數(shù)據(jù)來初始化這兩個圖片數(shù)據(jù)迭代器,這個功能是采用 flow_from_directory 函數(shù)來實現(xiàn)的:
train_data_generator = train_image_generator.flow_from_directory(
batch_size=BATCH_SIZE,
directory=train_dataset_dir,
shuffle=True,
target_size=(Height, Width),
class_mode='binary')
valid_data_generator = valid_image_generator.flow_from_directory(
batch_size=BATCH_SIZE,
directory=valid_dataset_dir,
shuffle=True,
target_size=(Height, Width),
class_mode='binary')
其中的幾個參數(shù)的解釋如下:
- batch_size:數(shù)據(jù)批次的大小,我們之前定義為 64;
- directory:數(shù)據(jù)存放的文件夾;
- shuffle:數(shù)據(jù)是否打亂,這里我們選擇進行打亂;
- target_size:輸出圖片的大小,這里我們將長寬都定義為 128;
- class_mode:分類模式,這里我們選擇 “binary” 表示這是一個二分類問題,如果是多分類問題,我們可以選擇 “categorical”。
至此我們就完成了數(shù)據(jù)的預處理的工作。
3. 構建模型
這里我們采用傳統(tǒng)的順序結構來定義我們的網(wǎng)絡結構,具體如下:
model = tf.keras.models.Sequential([
tf.keras.layers.Conv2D(16, 3, padding='same', activation='relu',
input_shape=(Height, Width ,3)),
tf.keras.layers.MaxPooling2D(),
tf.keras.layers.Conv2D(32, 3, padding='same', activation='relu'),
tf.keras.layers.MaxPooling2D(),
tf.keras.layers.Conv2D(64, 3, padding='same', activation='relu'),
tf.keras.layers.MaxPooling2D(),
tf.keras.layers.Flatten(),
tf.keras.layers.Dense(512, activation='relu'),
tf.keras.layers.Dense(1)
])
在這里我們采用了兩種新的網(wǎng)絡層結構,分別是 Conv2D 層與 MaxPooling2D 層,這兩層在后面我們都會詳細學習,這里我們可以暫且理解為:
- Conv2D:卷積層,用來提圖片的特征;
- MaxPooling2D:池化層,用來降低計算量;
- 一般來說,池化層都會跟在卷積層之后。
然后我們對模型進行編譯:
model.compile(optimizer='adam',
loss=tf.keras.losses.BinaryCrossentropy(from_logits=True),
metrics=['accuracy'])
model.summary()
在這里我們需要注意的有:
- 優(yōu)化器我們選擇最常用的 adam 優(yōu)化器,同時我們在訓練的過程中記錄準確率(accuracy);
- 因為任務是二元分類,因此我們采用的是二元交叉熵(BinaryCrossentropy);
- 因為網(wǎng)絡最后一層沒有激活函數(shù)而直接輸出,因此模型產生的數(shù)據(jù)實際是兩個類別的可能性的大小;所以優(yōu)化器會加上參數(shù)
from_logits=True
,以表示輸出為可能性大小,而不是具體類別。
運行上面代碼我們可以得到網(wǎng)絡的結構:
Model: "sequential_3"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
conv2d_9 (Conv2D) (None, 128, 128, 16) 448
_________________________________________________________________
max_pooling2d_9 (MaxPooling2 (None, 64, 64, 16) 0
_________________________________________________________________
conv2d_10 (Conv2D) (None, 64, 64, 32) 4640
_________________________________________________________________
max_pooling2d_10 (MaxPooling (None, 32, 32, 32) 0
_________________________________________________________________
conv2d_11 (Conv2D) (None, 32, 32, 64) 18496
_________________________________________________________________
max_pooling2d_11 (MaxPooling (None, 16, 16, 64) 0
_________________________________________________________________
flatten_3 (Flatten) (None, 16384) 0
_________________________________________________________________
dense_6 (Dense) (None, 512) 8389120
_________________________________________________________________
dense_7 (Dense) (None, 1) 513
=================================================================
Total params: 8,413,217
Trainable params: 8,413,217
Non-trainable params: 0
4. 訓練模型
在訓練模型的時候,我們會使用之前定義好的圖片數(shù)據(jù)迭代器,同時將訓練數(shù)據(jù)保存在 history 對象之中:
history = model.fit_generator(
train_data_generator,
steps_per_epoch=TRAIN_NUM // BATCH_SIZE,
epochs=EPOCHS,
validation_data=valid_data_generator,
validation_steps=VALID_NUM // BATCH_SIZE)
通過訓練,我們可以得到以下的輸出:
Epoch 1/15
31/31 [==============================] - 41s 1s/step - loss: 0.7072 - accuracy: 0.5134 - val_loss: 0.6650 - val_accuracy: 0.5167
Epoch 2/15
31/31 [==============================] - 40s 1s/step - loss: 0.6540 - accuracy: 0.5826 - val_loss: 0.6381 - val_accuracy: 0.5448
Epoch 3/15
31/31 [==============================] - 39s 1s/step - loss: 0.5780 - accuracy: 0.6844 - val_loss: 0.5859 - val_accuracy: 0.7208
Epoch 4/15
31/31 [==============================] - 40s 1s/step - loss: 0.5245 - accuracy: 0.7485 - val_loss: 0.5550 - val_accuracy: 0.6719
Epoch 5/15
31/31 [==============================] - 40s 1s/step - loss: 0.4673 - accuracy: 0.7645 - val_loss: 0.5654 - val_accuracy: 0.6865
Epoch 6/15
31/31 [==============================] - 40s 1s/step - loss: 0.3968 - accuracy: 0.8110 - val_loss: 0.5929 - val_accuracy: 0.7208
Epoch 7/15
31/31 [==============================] - 40s 1s/step - loss: 0.3216 - accuracy: 0.8492 - val_loss: 0.6224 - val_accuracy: 0.7104
Epoch 8/15
31/31 [==============================] - 40s 1s/step - loss: 0.2577 - accuracy: 0.8879 - val_loss: 0.6871 - val_accuracy: 0.7115
Epoch 9/15
31/31 [==============================] - 40s 1s/step - loss: 0.2204 - accuracy: 0.9060 - val_loss: 0.6982 - val_accuracy: 0.7250
Epoch 10/15
31/31 [==============================] - 40s 1s/step - loss: 0.1633 - accuracy: 0.9329 - val_loss: 0.9962 - val_accuracy: 0.6896
Epoch 11/15
31/31 [==============================] - 40s 1s/step - loss: 0.1371 - accuracy: 0.9489 - val_loss: 0.8724 - val_accuracy: 0.6990
Epoch 12/15
31/31 [==============================] - 40s 1s/step - loss: 0.0937 - accuracy: 0.9654 - val_loss: 1.1101 - val_accuracy: 0.7052
Epoch 13/15
31/31 [==============================] - 40s 1s/step - loss: 0.0640 - accuracy: 0.9742 - val_loss: 1.0343 - val_accuracy: 0.7083
Epoch 14/15
31/31 [==============================] - 40s 1s/step - loss: 0.0449 - accuracy: 0.9866 - val_loss: 1.1627 - val_accuracy: 0.7167
Epoch 15/15
31/31 [==============================] - 40s 1s/step - loss: 0.0199 - accuracy: 0.9954 - val_loss: 1.2627 - val_accuracy: 0.7156
輸出準確率與損失值因人而異,在這里我們在訓練集合上得到了 99.54% 的準確率,在驗證集上得到了 71.56% 的準確率。
5. 可視化訓練過程
在上一步之中,我們特地將訓練過程的數(shù)據(jù)記錄進了 history 對象之中;history 對象中的 history 數(shù)據(jù)對象是一個字典型的結構,其中包含了我們在訓練過程中的準確率與損失值等等。
于是我們將其可視化:
acc = history.history['accuracy']
loss = history.history['loss']
val_acc = history.history['val_accuracy']
val_loss = history.history['val_loss']
plt.plot(range(EPOCHS), acc, label='Train Acc')
plt.plot(range(EPOCHS), val_acc, label='Valid Acc')
plt.show()
plt.plot(range(EPOCHS), loss, label='Train Loss')
plt.plot(range(EPOCHS), val_loss, label='Valid Loss')
plt.show()
在這里我們使用了兩個圖表,第一個圖片展示準確率的變化,第二個圖片展示損失值的變化。
由此我們可以得到以下兩張圖片:
由此可以看出,隨著訓練的不斷迭代,訓練集合上的準確率不斷上升,損失值不斷下降;但是驗證集上的準確率在第 3 個 Epoch 以后便趨于平穩(wěn),而損失值卻在第 3 個 Epoch 之后逐漸上升。這就是我們在訓練過程中遇到的過擬合,我們以后會有課程詳細介紹過擬合。
6. 完整代碼案例代碼
import tensorflow as tf
import os
import matplotlib.pyplot as plt
dataset_url = 'https://storage.googleapis.com/mledu-datasets/cats_and_dogs_filtered.zip'
path_download = os.path.dirname(tf.keras.utils.get_file('cats_and_dogs.zip', origin=dataset_url, extract=True))
train_dataset_dir = path_download + '/cats_and_dogs_filtered/train'
valid_dataset_dir = path_download + '/cats_and_dogs_filtered/validation'
cat_train_dir = path_download + '/cats_and_dogs_filtered/train/cats'
cat_validation_dir = path_download + '/cats_and_dogs_filtered/validation/cats'
dog_train_dir = path_download + '/cats_and_dogs_filtered/train/dogss'
dog_validation_dir = path_download + '/cats_and_dogs_filtered/validation/dogs'
BATCH_SIZE = 64
TRAIN_NUM = 2000
VALID_NUM = 1000
EPOCHS = 15
Height = 128
Width = 128
train_image_generator = tf.keras.preprocessing.image.ImageDataGenerator(rescale=1./255)
valid_image_generator = tf.keras.preprocessing.image.ImageDataGenerator(rescale=1./255)
train_data_generator = train_image_generator.flow_from_directory(batch_size=BATCH_SIZE,
directory=train_dataset_dir,
shuffle=True,
target_size=(Height, Width),
class_mode='binary')
valid_data_generator = valid_image_generator.flow_from_directory(batch_size=BATCH_SIZE,
directory=valid_dataset_dir,
shuffle=True,
target_size=(Height, Width),
class_mode='binary')
model = tf.keras.models.Sequential([
tf.keras.layers.Conv2D(16, 3, padding='same', activation='relu',
input_shape=(Height, Width ,3)),
tf.keras.layers.MaxPooling2D(),
tf.keras.layers.Conv2D(32, 3, padding='same', activation='relu'),
tf.keras.layers.MaxPooling2D(),
tf.keras.layers.Conv2D(64, 3, padding='same', activation='relu'),
tf.keras.layers.MaxPooling2D(),
tf.keras.layers.Flatten(),
tf.keras.layers.Dense(512, activation='relu'),
tf.keras.layers.Dense(1)
])
model.compile(optimizer='adam',
loss=tf.keras.losses.BinaryCrossentropy(from_logits=True),
metrics=['accuracy'])
model.summary()
history = model.fit_generator(
train_data_generator,
steps_per_epoch=TRAIN_NUM // BATCH_SIZE,
epochs=EPOCHS,
validation_data=valid_data_generator,
validation_steps=VALID_NUM // BATCH_SIZE)
acc = history.history['accuracy']
loss=history.history['loss']
val_acc = history.history['val_accuracy']
val_loss=history.history['val_loss']
epochs_ran = range(EPOCHS)
plt.plot(epochs_ran, acc, label='Train Acc')
plt.plot(epochs_ran, val_acc, label='Valid Acc')
plt.show()
plt.plot(epochs_ran, loss, label='Train Loss')
plt.plot(epochs_ran, val_loss, label='Valid Loss')
plt.show()
7. 小結
在這節(jié)之中,我們學習了如何對圖片數(shù)據(jù)進行預處理,同時也了解了圖片處理中常用的兩種層結構:卷積層和池化層。于此同時,我們也對訓練過程進行了可視化操作,并親眼見證了過擬合的發(fā)生。
我們這節(jié)課所學的大致內容包括: