使用 tf.keras 進(jìn)行圖片分類
既然 tf.keras 是我們?cè)趯?shí)際工作中使用最多的工具,那么這節(jié)課我們首先來學(xué)習(xí)如何使用 tf.keras 進(jìn)行圖片的分類。
這節(jié)課會(huì)介紹圖像分類網(wǎng)絡(luò)的常用的網(wǎng)絡(luò)層、如何處理圖片數(shù)據(jù)以及整體程序的結(jié)構(gòu)。
在這里我們采用的是 Kaggle 上的貓狗分類數(shù)據(jù)集,該數(shù)據(jù)集合的數(shù)據(jù)的標(biāo)簽分為貓和狗兩個(gè)類別,他們的數(shù)量分別為:
- 訓(xùn)練集合 2000 張圖片,包括 1000 張狗圖片和 1000 張貓圖片;
- 驗(yàn)證集合 1000 張圖片,包括 500 張狗圖片和 500 張貓圖片。
我們會(huì)按照獲取數(shù)據(jù)、準(zhǔn)備數(shù)據(jù)、訓(xùn)練數(shù)據(jù)、數(shù)據(jù)可視化的順序幫助大家理解圖片分類的過程。
通過學(xué)習(xí)該節(jié)課程,我們會(huì)得到一個(gè)能夠分辨貓圖片和狗圖片、正確率在 70% 以上的分類模型。
1. 獲取數(shù)據(jù)
首先我們要引入我們所需要的程序包:
import tensorflow as tf
import os
import matplotlib.pyplot as plt
其中 os 用于目錄的相關(guān)操作,而 plt 用于我們的圖片繪制工作,然后我們進(jìn)行數(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 內(nèi)置的下載工具進(jìn)行下載,返回的是數(shù)據(jù)集所在的路徑。
我們最后是結(jié)合數(shù)據(jù)集的下載位置與其內(nèi)部文件建構(gòu)得到訓(xùn)練數(shù)據(jù)集與測(cè)試數(shù)據(jù)集所在的目錄,
其中 train_dataset_dir 是訓(xùn)練集的目錄,而 valid_dataset_dir 是測(cè)試集合所在的目錄。
可以通過下面的方法來查看數(shù)據(jù)文件所在的目錄。
print(cat_train_dir, cat_validation_dir, dog_train_dir, dog_validation_dir)
然后我們定義一些數(shù)據(jù)集合與訓(xùn)練的基本參數(shù),留作后面訓(xùn)練之用:
BATCH_SIZE = 64
TRAIN_NUM = 2000
VALID_NUM = 1000
EPOCHS = 15
Height = 128
Width = 128
這些值的具體含義為:
- BATCH_SIZE: 批次的大小,我們會(huì)將 BATCH_SIZE 個(gè)圖片數(shù)據(jù)同時(shí)送給模型進(jìn)行訓(xùn)練,這個(gè)數(shù)值大家可以根據(jù)自己的內(nèi)存大小或者顯存大小進(jìn)行調(diào)整;
- TRAIN_NUM: 訓(xùn)練集的數(shù)量,這里是 2000;
- VALID_NUM: 驗(yàn)證集的數(shù)量,這里是 1000;
- EPOCHS: 訓(xùn)練的周期數(shù),將所有的數(shù)據(jù)通用模型訓(xùn)練一遍稱為一個(gè) EPOCH,一般來說,EPOCH 的數(shù)值越大,模型在訓(xùn)練集合上的準(zhǔn)確率越高,相應(yīng)的所需要的時(shí)間也更長;
- Height: 圖片的高度;
- Width: 圖片的長度。
2. 數(shù)據(jù)預(yù)處理
這里我們要使用 Keras 內(nèi)部內(nèi)置的 ImageDataGenerator 來作為迭代器產(chǎn)生數(shù)據(jù)。
train_image_generator = tf.keras.preprocessing.image.ImageDataGenerator(rescale=1./255)
valid_image_generator = tf.keras.preprocessing.image.ImageDataGenerator(rescale=1./255)
在這里我們定義了兩個(gè)圖片數(shù)據(jù)迭代器,同時(shí)我們定義了 rescale 參數(shù),在函數(shù)內(nèi)部每張圖片的每個(gè)像素?cái)?shù)據(jù)都會(huì)乘以 1./255,以將其歸一化到 [0, 1] 之間,因?yàn)闄C(jī)器學(xué)習(xí)之中模型的最好輸入數(shù)據(jù)是在 [0, 1] 之間。
然后我們使用文件夾中的數(shù)據(jù)來初始化這兩個(gè)圖片數(shù)據(jù)迭代器,這個(gè)功能是采用 flow_from_directory 函數(shù)來實(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')
其中的幾個(gè)參數(shù)的解釋如下:
- batch_size:數(shù)據(jù)批次的大小,我們之前定義為 64;
- directory:數(shù)據(jù)存放的文件夾;
- shuffle:數(shù)據(jù)是否打亂,這里我們選擇進(jìn)行打亂;
- target_size:輸出圖片的大小,這里我們將長寬都定義為 128;
- class_mode:分類模式,這里我們選擇 “binary” 表示這是一個(gè)二分類問題,如果是多分類問題,我們可以選擇 “categorical”。
至此我們就完成了數(shù)據(jù)的預(yù)處理的工作。
3. 構(gòu)建模型
這里我們采用傳統(tǒng)的順序結(jié)構(gòu)來定義我們的網(wǎng)絡(luò)結(jié)構(gòu),具體如下:
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)絡(luò)層結(jié)構(gòu),分別是 Conv2D 層與 MaxPooling2D 層,這兩層在后面我們都會(huì)詳細(xì)學(xué)習(xí),這里我們可以暫且理解為:
- Conv2D:卷積層,用來提圖片的特征;
- MaxPooling2D:池化層,用來降低計(jì)算量;
- 一般來說,池化層都會(huì)跟在卷積層之后。
然后我們對(duì)模型進(jìn)行編譯:
model.compile(optimizer='adam',
loss=tf.keras.losses.BinaryCrossentropy(from_logits=True),
metrics=['accuracy'])
model.summary()
在這里我們需要注意的有:
- 優(yōu)化器我們選擇最常用的 adam 優(yōu)化器,同時(shí)我們?cè)谟?xùn)練的過程中記錄準(zhǔn)確率(accuracy);
- 因?yàn)槿蝿?wù)是二元分類,因此我們采用的是二元交叉熵(BinaryCrossentropy);
- 因?yàn)榫W(wǎng)絡(luò)最后一層沒有激活函數(shù)而直接輸出,因此模型產(chǎn)生的數(shù)據(jù)實(shí)際是兩個(gè)類別的可能性的大?。凰詢?yōu)化器會(huì)加上參數(shù)
from_logits=True
,以表示輸出為可能性大小,而不是具體類別。
運(yùn)行上面代碼我們可以得到網(wǎng)絡(luò)的結(jié)構(gòu):
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. 訓(xùn)練模型
在訓(xùn)練模型的時(shí)候,我們會(huì)使用之前定義好的圖片數(shù)據(jù)迭代器,同時(shí)將訓(xùn)練數(shù)據(jù)保存在 history 對(duì)象之中:
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)
通過訓(xùn)練,我們可以得到以下的輸出:
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
輸出準(zhǔn)確率與損失值因人而異,在這里我們?cè)谟?xùn)練集合上得到了 99.54% 的準(zhǔn)確率,在驗(yàn)證集上得到了 71.56% 的準(zhǔn)確率。
5. 可視化訓(xùn)練過程
在上一步之中,我們特地將訓(xùn)練過程的數(shù)據(jù)記錄進(jìn)了 history 對(duì)象之中;history 對(duì)象中的 history 數(shù)據(jù)對(duì)象是一個(gè)字典型的結(jié)構(gòu),其中包含了我們?cè)谟?xùn)練過程中的準(zhǔn)確率與損失值等等。
于是我們將其可視化:
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()
在這里我們使用了兩個(gè)圖表,第一個(gè)圖片展示準(zhǔn)確率的變化,第二個(gè)圖片展示損失值的變化。
由此我們可以得到以下兩張圖片:
由此可以看出,隨著訓(xùn)練的不斷迭代,訓(xùn)練集合上的準(zhǔn)確率不斷上升,損失值不斷下降;但是驗(yàn)證集上的準(zhǔn)確率在第 3 個(gè) Epoch 以后便趨于平穩(wěn),而損失值卻在第 3 個(gè) Epoch 之后逐漸上升。這就是我們?cè)谟?xùn)練過程中遇到的過擬合,我們以后會(huì)有課程詳細(xì)介紹過擬合。
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é)
在這節(jié)之中,我們學(xué)習(xí)了如何對(duì)圖片數(shù)據(jù)進(jìn)行預(yù)處理,同時(shí)也了解了圖片處理中常用的兩種層結(jié)構(gòu):卷積層和池化層。于此同時(shí),我們也對(duì)訓(xùn)練過程進(jìn)行了可視化操作,并親眼見證了過擬合的發(fā)生。
我們這節(jié)課所學(xué)的大致內(nèi)容包括: