如何用Keras從頭開始訓練一個在CIFAR10上準確率達到89%的模型
CIFAR10 是一個用於影象識別的經典資料集,包含了10個型別的圖片。該資料集有60000張尺寸為 32 x 32 的彩色圖片,其中50000張用於訓練,10000張用於測試。[CIFAR10]
在幾大經典影象識別資料集(MNIST / CIFAR10 / CIFAR100 / STL-10 / SVHN / ImageNet)中,對於 CIFAR10 資料集而言,目前業內 State-of-Art 級別的模型所能達到的最高準確率是 96.53%(詳細排名及論文連結)。
現在就讓我們用 Keras 從頭開始訓練一個CNN模型,目標是讓模型能夠在 CIFAR10 上達到將近 89% 的準確率。
1. 資料匯入和預處理
- 使用 keras.datasets 可以很方便的匯入 CIFAR10 的資料。
- 正規化:將畫素點的取值範圍從 [0, 255] 歸一化至 [0, 1]。實際上,對於不同的經典模型而言,有多種正規化方法,例如 [0, 1], [-1, 1], Mean Subtraction 等等。
- 使用 keras.utils.to_categorical 對十類標籤進行 One-hot 編碼,以供後來Softmax分類。
nb_classes = 10
(X_train, y_train), (X_test, y_test) = cifar10.load_data()
y_train = y_train .reshape(y_train.shape[0])
y_test = y_test.reshape(y_test.shape[0])
X_train = X_train.astype('float32')
X_test = X_test.astype('float32')
X_train /= 255
X_test /= 255
y_train = to_categorical(y_train, nb_classes)
y_test = to_categorical(y_test, nb_classes)
2. 建立模型
這裡我們採用類似於 VGG16 的結構建立模型。
- 使用固定尺寸的小卷積核 3 x 3
- 兩層卷積搭配一層池化
- 使用 VGG16 的前三個卷積池化結構:以2的冪次遞增卷積核數量 (64, 128, 256)
- 模型的全連線層沒有采用 VGG16 龐大的三層結構(VGG16 的全連線層引數數量佔整個模型的90%以上)
- 卷積層輸出直接上 10 分類的 Softmax Classifier(測試過新增一層128個節點的全連線層節點在輸出層之前,但是訓練100代只能到79%的準確率,比不加還要差)
- 權重初始化全部採用 He Normal
x = Input(shape=(32, 32, 3))
y = x
y = Convolution2D(filters=64, kernel_size=3, strides=1, padding='same', activation='relu', kernel_initializer='he_normal')(y)
y = Convolution2D(filters=64, kernel_size=3, strides=1, padding='same', activation='relu', kernel_initializer='he_normal')(y)
y = MaxPooling2D(pool_size=2, strides=2, padding='same')(y)
y = Convolution2D(filters=128, kernel_size=3, strides=1, padding='same', activation='relu', kernel_initializer='he_normal')(y)
y = Convolution2D(filters=128, kernel_size=3, strides=1, padding='same', activation='relu', kernel_initializer='he_normal')(y)
y = MaxPooling2D(pool_size=2, strides=2, padding='same')(y)
y = Convolution2D(filters=256, kernel_size=3, strides=1, padding='same', activation='relu', kernel_initializer='he_normal')(y)
y = Convolution2D(filters=256, kernel_size=3, strides=1, padding='same', activation='relu', kernel_initializer='he_normal')(y)
y = MaxPooling2D(pool_size=2, strides=2, padding='same')(y)
y = Flatten()(y)
y = Dropout(0.5)(y)
y = Dense(units=nb_classes, activation='softmax', kernel_initializer='he_normal')(y)
model1 = Model(inputs=x, outputs=y, name='model1')
model1.compile(loss='categorical_crossentropy', optimizer='adadelta', metrics=['accuracy'])
用 summary() 檢視一下模型的完整結構。
同時可以注意到,模型的總引數量為 118 萬左右 (這還是簡化後的模型)
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
input_4 (InputLayer) (None, 32, 32, 3) 0
_________________________________________________________________
conv2d_19 (Conv2D) (None, 32, 32, 64) 1792
_________________________________________________________________
conv2d_20 (Conv2D) (None, 32, 32, 64) 36928
_________________________________________________________________
max_pooling2d_10 (MaxPooling (None, 16, 16, 64) 0
_________________________________________________________________
conv2d_21 (Conv2D) (None, 16, 16, 128) 73856
_________________________________________________________________
conv2d_22 (Conv2D) (None, 16, 16, 128) 147584
_________________________________________________________________
max_pooling2d_11 (MaxPooling (None, 8, 8, 128) 0
_________________________________________________________________
conv2d_23 (Conv2D) (None, 8, 8, 256) 295168
_________________________________________________________________
conv2d_24 (Conv2D) (None, 8, 8, 256) 590080
_________________________________________________________________
max_pooling2d_12 (MaxPooling (None, 4, 4, 256) 0
_________________________________________________________________
flatten_4 (Flatten) (None, 4096) 0
_________________________________________________________________
dropout_4 (Dropout) (None, 4096) 0
_________________________________________________________________
dense_6 (Dense) (None, 10) 40970
=================================================================
Total params: 1,186,378
Trainable params: 1,186,378
Non-trainable params: 0
_________________________________________________________________
3. 定義訓練方式
Keras比較有意思的一點就是你可以使用各種回撥函式來讓你的訓練過程更輕鬆快捷。
- Early Stopping:通過監測效能指標的變化趨勢來及時停止訓練,避免過擬合。
- Model Checkpoint:自動儲存每一代訓練後的模型快照,可以只記錄效能提升的代。
- Tensor Board:自動配置強大的視覺化工具 TensorBoard,將每代訓練資料都儲存並繪製在 TensorBoard 上,方便後期對比模型的效能。
4. 模型效能提升的幾個方向
為了提升模型的效能,我目前瞭解到通常可以在以下幾種方向上下功夫:
- Data Augmentation
- Weight Initialization
- Transfer Learning + Fine-tune
- Ensemble / Model Fusion
下面用的是第一個:影象增強。Keras 很方便的自帶了影象增強的生成器。這個生成器函式提供了很多影象變換的引數,例如旋轉、平移、白化等等。可以直接定義影象增強生成器物件,然後用 flow 函式生成真正可以用於訓練的生成器。我定義的完整訓練函式如下。
import os
from datetime import datetime
def train(model, batch, epoch, data_augmentation=True):
start = time()
log_dir = datetime.now().strftime('model_%Y%m%d_%H%M')
os.mkdir(log_dir)
es = EarlyStopping(monitor='val_acc', patience=20)
mc = ModelCheckpoint(log_dir + '\\CIFAR10-EP{epoch:02d}-ACC{val_acc:.4f}.h5',
monitor='val_acc', save_best_only=True)
tb = TensorBoard(log_dir=log_dir, histogram_freq=0)
if data_augmentation:
aug = ImageDataGenerator(width_shift_range = 0.125, height_shift_range = 0.125, horizontal_flip = True)
aug.fit(X_train)
gen = aug.flow(X_train, y_train, batch_size=batch)
h = model.fit_generator(generator=gen,
steps_per_epoch=50000/batch,
epochs=epoch,
validation_data=(X_test, y_test),
callbacks=[es, mc, tb])
else:
start = time()
h = model.fit(x=X_train,
y=y_train,
batch_size=batch,
epochs=epoch,
validation_data=(X_test, y_test),
callbacks=[es, mc, tb])
print('\n@ Total Time Spent: %.2f seconds' % (time() - start))
acc, val_acc = h.history['acc'], h.history['val_acc']
m_acc, m_val_acc = np.argmax(acc), np.argmax(val_acc)
print("@ Best Training Accuracy: %.2f%% achieved at EP #%d." % (acc[m_acc] * 100, m_acc + 1))
print("@ Best Testing Accuracy: %.2f%% achieved at EP #%d." % (val_acc[m_val_acc] * 100, m_val_acc + 1))
return h
5. 訓練模型
我們首先使用 batch size = 64 對模型進行訓練。
accuracy_curve 是通過訓練結束返回的歷史資訊繪製出的 accuracy 和 loss 曲線。程式碼見:utils.py。如果用TensorBoard的話,看到同樣的圖表資訊。
epoch = 200
batch = 64
h = train(model1, batch, epoch)
accuracy_curve(h)
-----------------------
........
Epoch 105/200
16s - loss: 0.0656 - acc: 0.9782 - val_loss: 0.5996 - val_acc: 0.8947
Epoch 106/200
16s - loss: 0.0661 - acc: 0.9782 - val_loss: 0.6633 - val_acc: 0.8938
@ Total Time Spent: 1875.35 seconds
@ Best Training Accuracy: 97.89 % achieved at EP #98.
@ Best Testing Accuracy: 90.00 % achieved at EP #85.
<img src="https://pic3.zhimg.com/v2-5c7f22d9bcf64d0ccab87b01e38fa5f6_b.jpg" data-rawwidth="989" data-rawheight="321" class="origin_image zh-lightbox-thumb" width="989" data-original="https://pic3.zhimg.com/v2-5c7f22d9bcf64d0ccab87b01e38fa5f6_r.jpg">6. 分析訓練結果
訓練結果顯示模型最高在第85代達到了90%的測試準確率。
但是,我們可以觀察到,從第40代開始模型的 Testing Loss 曲線就不再下降而是開始上升,這說明模型已經進入過擬合狀態,不應繼續訓練。這是由於我在 Early Stopping 檢測的效能指標不是 val_loss 而是 val_acc,因此並不能夠在模型的 Loss 停止下降時就及時結束訓練。
所以實際上模型正常收斂的測試準確率應該在20 - 40代之間,即準確率在 88% - 89% 左右。
7. 使用 TensorBoard 對比不同 Batch Size 下的模型訓練過程
我對上面的模型嘗試了四種不同的 Batch Size = 64 / 128 / 256 / 512,並通過 TensorBoard 將這四個模型的訓練準確度和Loss曲線繪製在一張圖上,結果很有趣。
- 橫軸是訓練代數
- 半透明的折線是原始資料
- 實線是經過平滑後的資料,能夠更清晰的看到每個模型在同一個訓練指標上的變化趨勢
- 放大細節圖
通過這個簡單的對比試驗,可以得到以下幾條關於 Batch Size 對訓練模型影響的結論:
- Batch Size 越大,每代訓練的訓練時間越短,但縮短到一定程度就不再下降
- Batch Size 越大,模型收斂的越慢,直至不能收斂
- Batch Size 越大,過擬合會越晚體現出來