CNN基礎一:從頭開始訓練CNN進行圖像分類(貓狗大戰為例)


本文旨在總結一次從頭開始訓練CNN進行圖像分類的完整過程(貓狗大戰為例,使用Keras框架),免得經常遺忘。流程包括:

  • 從Kaggle下載貓狗數據集;
  • 利用python的os、shutil庫,制作訓練集和測試集;
  • 快速開發一個小模型作為基准;(只要效果比隨機猜略好即可,通常需要有一點過擬合)
  • 根據基准表現進行改進,比如針對過擬合的圖像增強、正則化等。

1 從Kaggle下載貓狗數據集

具體可參考

2 制作數據集

從Kaggle下載的貓狗數據集大概八百多兆,其中訓練集包含25000張貓狗圖,兩類數量各占一半。為了快速上手項目,可以從原始訓練集中抽取一部分數據,來制作本次項目的數據集D,其中包含三個子集:兩個類別各1000個樣本的訓練集(構造一個平衡的二分類問題),兩個類別各500個樣本的驗證集,兩個類別各500個樣本的測試集。代碼如下:

"""
代碼功能:在目標文件夾下建立三個子文件夾(train、validation、test),在每個子文件夾下再分別建立兩個子文件夾(cats、dogs),以存放從原始訓練集中抽取的圖像
"""
import os, shutil

original_dataset_dir = r'D:\KaggleDatasets\competitions\dogs-vs-cats\train' #在我磁盤上解壓的文件夾
base_dir = r'D:\KaggleDatasets\MyDatasets\dogs-vs-cats-small'  #在我磁盤上的目標文件夾

##############以下是創建文件夾並復制圖像#######################
#創建訓練集文件夾
train_cats_dir = os.path.join(base_dir, 'train\cats')
train_dogs_dir = os.path.join(base_dir, 'train\dogs')
os.makedirs(train_cats_dir)  
os.makedirs(train_dogs_dir) #與os.mkdir的區別是會自動創建中間路徑;若文件夾已存在,則都會報錯

#創建驗證集文件夾
validation_cats_dir = os.path.join(base_dir, 'validation\cats')
validation_dogs_dir = os.path.join(base_dir, 'validation\dogs')
os.makedirs(validation_cats_dir)
os.makedirs(validation_dogs_dir)

#創建測試集文件夾
test_cats_dir = os.path.join(base_dir, 'test\cats')
test_dogs_dir = os.path.join(base_dir, 'test\dogs')
os.makedirs(test_cats_dir)
os.makedirs(test_dogs_dir)

#將原始訓練集中前1000張貓圖復制到train_cats_dir
fnames = ['cat.{}.jpg'.format(i) for i in range(1000)] #前1000張貓圖名稱列表
for fname in fnames:
    scr = os.path.join(original_dataset_dir, fname)
    dst = os.path.join(train_cats_dir, fname)
    shutil.copyfile(scr, dst)
    
#將之后的500張貓圖復制到validation_cats_dir
fnames = ['cat.{}.jpg'.format(i) for i in range(1000, 1500)]
for fname in fnames:
    scr = os.path.join(original_dataset_dir, fname)
    dst = os.path.join(validation_cats_dir, fname)
    shutil.copyfile(scr, dst)
    
#將再之后的500張貓圖復制到test_cats_dir
fnames = ['cat.{}.jpg'.format(i) for i in range(1500, 2000)]
for fname in fnames:
    scr = os.path.join(original_dataset_dir, fname)
    dst = os.path.join(test_cats_dir, fname)
    shutil.copyfile(scr, dst)
    
#同理,對狗狗圖做同樣處理
fnames = ['dog.{}.jpg'.format(i) for i in range(1000)]
for fname in fnames:
    scr = os.path.join(original_dataset_dir, fname)
    dst = os.path.join(train_dogs_dir, fname)
    shutil.copyfile(scr, dst)
    
fnames = ['dog.{}.jpg'.format(i) for i in range(1000, 1500)]
for fname in fnames:
    scr = os.path.join(original_dataset_dir, fname)
    dst = os.path.join(validation_dogs_dir, fname)
    shutil.copyfile(scr, dst)
    
fnames = ['dog.{}.jpg'.format(i) for i in range(1500, 2000)]
for fname in fnames:
    scr = os.path.join(original_dataset_dir, fname)
    dst = os.path.join(test_dogs_dir, fname)
    shutil.copyfile(scr, dst)

3 快速開發基准模型

面對一個任務,通常需要快速驗證想法,並不斷迭代。因此開發基准模型通常需要快速,模型能跑起來,效果比隨機猜測好一些就行,不用太在意細節。至於正則化、圖像增強、參數選取等操作,后續會根據需要來進行。

####################模型搭建與編譯#################################
from keras.models import Model
from keras.layers import Input, Dense, Conv2D, MaxPooling2D, Flatten, Dropout

def build_model():
    input = Input(shape=(150, 150, 3))
    X = Conv2D(32, (3,3), activation='relu')(input)
    X = MaxPooling2D((2,2))(X)
    X = Conv2D(64, (3,3), activation='relu')(X)
    X = MaxPooling2D((2,2))(X)
    X = Conv2D(128, (3,3), activation='relu')(X)
    X = MaxPooling2D((2,2))(X)
    X = Conv2D(128, (3,3), activation='relu')(X)
    X = MaxPooling2D((2,2))(X)
    X = Flatten()(X)
    X = Dense(512, activation='relu')(X)
    X = Dense(1, activation='sigmoid')(X)
    model = Model(inputs=input, outputs=X)
    return model

model = build_model()
model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])
#model.summary()


#########使用圖像生成器讀取文件中數據(內存一次無法加載全部圖像)###########
#ImageDataGenerator就像一個把文件中圖像轉換成所需格式的轉接頭,通常先定制一個轉接頭train_datagen,它可以根據需要對圖像進行各種變換,然后再把它懟到文件中(flow方法是懟到array中),約定好出來數據的格式(比如圖像的大小、每次出來多少樣本、樣本標簽的格式等等)。這里出來的train_generator是個(X,y)元組,X的shape為(20,150,150,3),y的shape為(20,)
from keras.preprocessing.image import ImageDataGenerator

train_datagen = ImageDataGenerator(rescale=1./255) #之后可能會在這里進行圖像增強
test_datagen = ImageDataGenerator(rescale=1./255) #注意驗證集不可用圖像增強

batch_size = 20
train_dir = r'D:\KaggleDatasets\MyDatasets\dogs-vs-cats-small\train'
validation_dir = r'D:\KaggleDatasets\MyDatasets\dogs-vs-cats-small\validation'
train_generator = train_datagen.flow_from_directory(train_dir, target_size=(150,150),
           batch_size=batch_size,class_mode='binary')
validation_generator = test_datagen.flow_from_directory(validation_dir, 
           target_size=(150,150), batch_size=batch_size, class_mode='binary')


#######################開始訓練####################################
epochs = 100
steps_per_epoch = 2000 / batch_size
validation_steps = 1000 / batch_size
H = model.fit_generator(train_generator, 
                        epochs=epochs, 
                        steps_per_epoch=steps_per_epoch,
                        validation_data=validation_generator,
                        validation_steps=validation_steps)

#保存模型
model.save('cats_and_dogs_small_1.h5')
print("The trained model has been saved.")

##模型評估
test_dir = r'D:\KaggleDatasets\MyDatasets\dogs-vs-cats-small\test'
test_generator = test_datagen.flow_from_directory(test_dir,
                target_size=(150,150), batch_size=20, class_mode='binary')
score = model.evaluate_generator(test_generator, steps=50)
print("測試損失為:{:.4f}".format(score[0]))
print("測試准確率為:{:.4f}".format(score[1]))


######################結果可視化#############################
import matplotlib.pyplot as plt

loss = H.history['loss']
acc = H.history['acc']
val_loss = H.history['val_loss']
val_acc = H.history['val_acc']
epoch = range(1, len(loss)+1)

fig, ax = plt.subplots(1, 2, figsize=(10,4))
ax[0].plot(epoch, loss, label='Train loss')
ax[0].plot(epoch, val_loss, label='Validation loss')
ax[0].set_xlabel('Epochs')
ax[0].set_ylabel('Loss')
ax[0].legend()
ax[1].plot(epoch, acc, label='Train acc')
ax[1].plot(epoch, val_acc, label='Validation acc')
ax[1].set_xlabel('Epochs')
ax[1].set_ylabel('Accuracy')
ax[1].legend()
plt.show()

訓練結果如下圖所示,很明顯模型上來就過擬合了,主要原因是數據不夠,或者說相對於數據量,模型過復雜(訓練損失在第15個epoch就降為0了)。

4 根據基准模型進行調整

為了解決過擬合問題,可以減小模型復雜度,也可以用一系列手段去對沖,比如增加數據(圖像增強、人工合成或者多搜集真實數據)、L1/L2正則化、dropout正則化等。這里主要介紹CV中最常用的圖像增強。

4.1 圖像增強方法

在Keras中,可以利用圖像生成器很方便地定義一些常見的圖像變換。將變換后的圖像送入訓練之前,可以按變換方法逐個看看變換的效果。代碼如下:

#######################查看數據增強效果#########################
from keras.preprocessing import image
import numpy as np

#定義一個圖像生成器
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')

#生成所有貓圖的路徑列表
train_cats_dir = os.path.join(train_dir, 'cats')
fnames = [os.path.join(train_cats_dir, fname) for fname in os.listdir(train_cats_dir)]

#選一張圖片,包裝成(batches, 150, 150, 3)格式
img_path = fnames[1] 
img = image.load_img(img_path, target_size=(150,150)) #讀入一張圖像
x_aug = image.img_to_array(img) #將圖像格式轉為array格式
x_aug = np.expand_dims(x_aug, axis=0) #(1, 150, 150, 3) array格式

#對選定的圖片進行增強,並查看效果
fig = plt.figure(figsize=(8,8))
k = 1
for batch in datagen.flow(x_aug, batch_size=1):  #注意生成器的使用方式
    ax = fig.add_subplot(3, 3, k)
    ax.imshow(image.array_to_img(batch[0])) #當x_aug中樣本個數只有一個時,即便batch_size=4,也只能獲得一個樣本,所以batch[1]會出錯
    k += 1
    if k > 9:
        break

效果如下:

4.2 模型調整方法

這里暫時先采用兩種方法進行改進:一是將這幾種選定的圖像增強方法添加進訓練集的生成器中(train_datagen);二是在模型結構中加入一層Dropout(在Flatten層后加上 Dropout(0.5))。調整並重新訓練后發現30個epoch還不夠,損失函數還在持續下降,於是改為100個epoch。重新訓練后的結果如圖所示。可以看出,准確率由基准的67%提高到82%,進一步調整模型還可以提升到86%左右。但是進一步就再難以繼續提升了,因為數據太少,且模型比較粗糙,下一節我們會采取其他更有效的措施。

5 知識點小結

  • 如何從Kaggle上下載公開數據集;
  • 如何根據原始圖像制作小型數據集;
  • 如何使用圖像生成器讀取文件中圖片,並送入訓練;
  • 如何進行數據增強,並查看增強的效果;
  • 如何將訓練結果可視化。

另外,關於CNN模型的架構,通常有一些經驗:

  • 從前往后,特征圖需要適當的下采樣。一方面減少參數,另一方面也可以增大后側特征圖像素的感受野。
  • 下采樣的方法可以采用帶步長的卷積、平均池化或最大池化,但最大池化往往比前兩種方法好,因為CNN的本質就在於用卷積核中的特征去匹配前一層的圖像或特征圖,前兩種方法可能會錯過或淡化特征是否存在的信息。

Reference

Python深度學習


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM