【火爐煉AI】深度學習005-簡單幾行Keras程式碼解決二分類問題

煉丹老頑童發表於2019-02-28

【火爐煉AI】深度學習005-簡單幾行Keras程式碼解決二分類問題

(本文所使用的Python庫和版本號: Python 3.6, Numpy 1.14, scikit-learn 0.19, matplotlib 2.2, Keras 2.1.6, Tensorflow 1.9.0)

很多文章和教材都是用MNIST資料集作為深度學習屆的“Hello World”程式,但是這個資料集有一個很大的特點:它是一個典型的多分類問題(一共有10個分類),在我們剛剛開始接觸深度學習時,我倒是覺得應該從最簡單的二分類問題著手。

在深度學習框架方面,目前比較流行的是Tensorflow,Keras,PyTorch,Theano等,但是我建議新手入門,可以從Keras入手,然後進階時轉移到Tensorflow上,實際上,Keras的後端是可以支援Tensorflow和Theano,可以說,Keras是在Tensorflow和Theano的基礎上進一步封裝,更加的簡單實用,更容易入門,通常幾行簡單的程式碼就可以解決一個小型的專案問題。

我這篇博文主要參考了:keras系列︱影像多分類訓練與利用bottleneck features進行微調(三),這篇博文也是參考的Building powerful image classification models using very little data,但我發現這兩篇博文有很多地方的程式碼跑不起來,主要原因可能是Keras或Tensorflow升級造成的,所以我做了一些必要的修改。

1. 準備資料集

最經典的二分類資料集就是Kaggle競賽中的“貓狗大戰”資料集(train set有25K張圖片,test set: 12.5K),此處按照原始博文的做法,我從train_set中選取1000張Dog的照片+1000張Cat照片作為我們新的train set,選取400張Dog+400張Cat照片作為新的test set。所以train和test兩個資料夾下都有兩個子資料夾(cats和dogs子資料夾)。當然,選取是隨機的,也是用程式碼來實現的,準備小資料集的程式碼如下:

def dataset_prepare(raw_set_folder,dst_folder,train_num_per_class=1000,test_num_per_class=400):
    ```
    準備小資料集,從原始的raw_set_folder資料集中提取train_num_per_class(每個類別)的照片放入train中,
    提取val_num_per_class(每個類別)放入到validation資料夾中
    :param raw_set_folder: 含有貓狗的照片,這些照片的名稱必須為cat.101.jpg或dog.102.jpg形式
    :param dst_folder: 將選取之後的圖片放置到這個資料夾中
    :param train_num_per_class:
    :param test_num_per_class:
    :return:
    ```
    all_imgs=glob(os.path.join(raw_set_folder,`*.jpg`))
    img_len = len(all_imgs)
    assert img_len > 0, `{} has no jpg image file`.format(raw_set_folder)

    cat_imgs=[]
    dog_imgs=[]
    for img_path in all_imgs:
        img_name=os.path.split(img_path)[1]
        if img_name.startswith(`cat`):
            cat_imgs.append(img_path)
        elif img_name.startswith(`dog`):
            dog_imgs.append(img_path)
    random.shuffle(cat_imgs)
    random.shuffle(dog_imgs)
    [ensure_folder_exists(os.path.join(dst_folder,type_folder,class_folder)) for type_folder in [`train`,`test`]
        for class_folder in [`dogs`,`cats`]]
    # 下面的程式碼可以進一步優化。。。。
    for cat_img_path in cat_imgs[:train_num_per_class]: # 最開始的N個圖片作為train
        _, fname = os.path.split(cat_img_path)  # 獲取檔名和路徑
        shutil.copyfile(cat_img_path, os.path.join(dst_folder, `train`, `cats`,fname))
    print(`imgs saved to train/cats folder`)
    for dog_img_path in dog_imgs[:train_num_per_class]:
        _, fname = os.path.split(dog_img_path)  # 獲取檔名和路徑
        shutil.copyfile(dog_img_path, os.path.join(dst_folder, `train`, `dogs`,fname))
    print(`imgs saved to train/dogs folder`)
    for cat_img_path in cat_imgs[-test_num_per_class:]: # 最末的M個圖片作為test
        _, fname = os.path.split(cat_img_path)  # 獲取檔名和路徑
        shutil.copyfile(cat_img_path, os.path.join(dst_folder, `test`, `cats`,fname))
    print(`imgs saved to test/cats folder`)
    for dog_img_path in dog_imgs[-test_num_per_class:]: # 最末的M個圖片作為test
        _, fname = os.path.split(dog_img_path)  # 獲取檔名和路徑
        shutil.copyfile(dog_img_path, os.path.join(dst_folder, `test`, `dogs`,fname))
    print(`imgs saved to test/dogs folder`)
    print(`finished...`)
複製程式碼

執行該函式即可完成小資料集的構建,下面為Keras建立圖片資料流,為模型的構建做準備。

# 2,準備訓練集,keras有很多Generator可以直接處理圖片的載入,增強等操作,封裝的非常好
from keras.preprocessing.image import ImageDataGenerator
train_datagen = ImageDataGenerator( # 單張圖片的處理方式,train時一般都會進行圖片增強
        rescale=1. / 255, # 圖片畫素值為0-255,此處都乘以1/255,調整到0-1之間
        shear_range=0.2, # 斜切
        zoom_range=0.2, # 放大縮小範圍
        horizontal_flip=True) # 水平翻轉

train_generator = train_datagen.flow_from_directory(# 從資料夾中產生資料流
    train_data_dir, # 訓練集圖片的資料夾
    target_size=(IMG_W, IMG_H), # 調整後每張圖片的大小
    batch_size=batch_size,
    class_mode=`binary`) # 此處是二分類問題,故而mode是binary

# 3,同樣的方式準備測試集
val_datagen = ImageDataGenerator(rescale=1. / 255) # 只需要和trainset同樣的scale即可,不需增強
val_generator = val_datagen.flow_from_directory(
        val_data_dir,
        target_size=(IMG_W, IMG_H),
        batch_size=batch_size,
        class_mode=`binary`)
複製程式碼

上面構建的generator就是keras需要的資料流,該資料流使用flow_from_directory首先從圖片資料夾(比如train_data_dir)中載入圖片到記憶體中,然後使用train_datagen來對圖片進行預處理和增強,最終得到處理完成之後的batch size大小的資料流,這個資料流會無限迴圈的產生,直到達到一定的訓練epoch數量為止。

上面用到了ImageDataGenerator來進行圖片增強,裡面的引數說明為:(可以參考Keras的官方文件

rotation_range是一個0~180的度數,用來指定隨機選擇圖片的角度。

width_shift和height_shift用來指定水平和豎直方向隨機移動的程度,這是兩個0~1之間的比例。

rescale值將在執行其他處理前乘到整個影像上,我們的影像在RGB通道都是0~255的整數,這樣的操作可能使影像的值過高或過低,所以我們將這個值定為0~1之間的數。

shear_range是用來進行剪下變換的程度

zoom_range用來進行隨機的放大

horizontal_flip隨機的對圖片進行水平翻轉,這個引數適用於水平翻轉不影響圖片語義的時候

fill_mode用來指定當需要進行畫素填充,如旋轉,水平和豎直位移時,如何填充新出現的畫素

2. 構建並訓練Keras模型

由於Keras已經封裝了很多Tensorflow的函式,所以在使用上更加簡單容易,當然,如果想調整裡面的結構和引數等,也比較麻煩一些,所以對於高手,想要調整模型的結構和自定義一些函式,可以直接用Tensorflow.

2.1 Keras模型的構建

不管是Keras模型還是Tensorflow模型,我個人認為其構建都包括兩個部分:模型的搭建和模型的配置,所以可以從這兩個方面來建立一個小型的模型。程式碼如下:

# 4,建立Keras模型:模型的建立主要包括模型的搭建,模型的配置
from keras.models import Sequential
from keras.layers import Conv2D, MaxPooling2D
from keras.layers import Activation, Dropout, Flatten, Dense
from keras import optimizers
def build_model(input_shape):
    # 模型的搭建:此處構建三個CNN層+2個全連線層的結構
    model = Sequential()
    model.add(Conv2D(32, (3, 3), input_shape=input_shape))
    model.add(Activation(`relu`))
    model.add(MaxPooling2D(pool_size=(2, 2)))

    model.add(Conv2D(32, (3, 3)))
    model.add(Activation(`relu`))
    model.add(MaxPooling2D(pool_size=(2, 2)))

    model.add(Conv2D(64, (3, 3)))
    model.add(Activation(`relu`))
    model.add(MaxPooling2D(pool_size=(2, 2)))

    model.add(Flatten())
    model.add(Dense(64))
    model.add(Activation(`relu`))
    model.add(Dropout(0.5)) # Dropout防止過擬合
    model.add(Dense(1)) # 此處雖然是二分類,但是不能用Dense(2),因為後面的activation是sigmoid,這個函式只能輸出一個值,即class_0的概率
    model.add(Activation(`sigmoid`)) #二分類問題用sigmoid作為activation function
    
    # 模型的配置
    model.compile(loss=`binary_crossentropy`, # 定義模型的loss func,optimizer,
                  optimizer=optimizers.RMSprop(lr=0.0001),
                  metrics=[`accuracy`])# 主要優化accuracy
    # 二分類問題的loss function使用binary_crossentropy,此處使用準確率作為優化目標
    return model # 返回構建好的模型
複製程式碼

這個函式就搭建了模型的結構,對模型進行了配置,主要配置了loss function, optimzer, 優化目標等,當然可以做更多其他配置。

此處,為了簡單說明,只是建立了三層卷積層+兩層全連線層的小型網路結構,當然,對於一些比較簡單的影像問題,這個小型模型也能解決。如果需要構建更為複雜的模型,只需要自定義這個函式,修改裡面的模型構建和配置方法即可。

2.2 模型的訓練

由於此處我們使用generator來產生資料流,故而訓練時要使用fit_generator函式。程式碼如下:

model=build_model(input_shape=(IMG_W,IMG_H,IMG_CH)) # 輸入的圖片維度
# 模型的訓練
model.fit_generator(train_generator, # 資料流
                    steps_per_epoch=train_samples_num // batch_size, 
                    epochs=epochs,
                    validation_data=val_generator,
                    validation_steps=val_samples_num // batch_size)
複製程式碼

由於我在自己的筆記本上訓練,沒有獨立顯示卡,更沒有英偉達那麼NB的顯示卡,故而速度很慢,但是的確能執行下去。執行的具體結果可以去我的github上看。

————————————-輸———出——————————–

Epoch 1/20
62/62 [==============================] – 136s 2s/step – loss: 0.6976 – acc: 0.5015 – val_loss: 0.6937 – val_acc: 0.5000
Epoch 2/20
62/62 [==============================] – 137s 2s/step – loss: 0.6926 – acc: 0.5131 – val_loss: 0.6846 – val_acc: 0.5813
Epoch 3/20
62/62 [==============================] – 152s 2s/step – loss: 0.6821 – acc: 0.5544 – val_loss: 0.6735 – val_acc: 0.6100

。。。

Epoch 18/20
62/62 [==============================] – 140s 2s/step – loss: 0.5776 – acc: 0.6880 – val_loss: 0.5615 – val_acc: 0.7262
Epoch 19/20
62/62 [==============================] – 143s 2s/step – loss: 0.5766 – acc: 0.6971 – val_loss: 0.5852 – val_acc: 0.6800
Epoch 20/20
62/62 [==============================] – 140s 2s/step – loss: 0.5654 – acc: 0.7117 – val_loss: 0.5374 – val_acc: 0.7450

——————————————–完————————————-

從訓練後的loss和acc上可以大致看出,loss在不斷減小,acc也不斷增大,趨勢比較平穩。

此處我們可以將訓練過程中的loss和acc繪圖,看看他們的變化趨勢。

# 畫圖,將訓練時的acc和loss都繪製到圖上
import matplotlib.pyplot as plt
%matplotlib inline
def plot_training(history):
    plt.figure(12)
    
    plt.subplot(121)
    train_acc = history.history[`acc`]
    val_acc = history.history[`val_acc`]
    epochs = range(len(train_acc))
    plt.plot(epochs, train_acc, `b`,label=`train_acc`)
    plt.plot(epochs, val_acc, `r`,label=`test_acc`)
    plt.title(`Train and Test accuracy`)
    plt.legend()
    
    plt.subplot(122)
    train_loss = history.history[`loss`]
    val_loss = history.history[`val_loss`]
    epochs = range(len(train_loss))
    plt.plot(epochs, train_loss, `b`,label=`train_loss`)
    plt.plot(epochs, val_loss, `r`,label=`test_loss`)
    plt.title(`Train and Test loss`)
    plt.legend()
 
    plt.show()
複製程式碼
【火爐煉AI】深度學習005-簡單幾行Keras程式碼解決二分類問題

很明顯,由於epoch次數太少,acc和loss都沒有達到平臺期,後續可以增大epoch次數來達到一個比較好的結果。在原始博文中,作者在50個epoch之後達到了約80%左右的準確率,此處我20個epoch後的準確率為74%。

2.3 預測新樣本

單張圖片的預測

模型訓練好之後,就需要用來預測新的圖片,看看它能不能準確的給出結果。預測函式為:

# 用訓練好的模型來預測新樣本
from PIL import Image
from keras.preprocessing import image
def predict(model, img_path, target_size):
    img=Image.open(img_path) # 載入圖片
    if img.size != target_size:
        img = img.resize(target_size)

    x = image.img_to_array(img) 
    x *=1./255 # 相當於ImageDataGenerator(rescale=1. / 255)
    x = np.expand_dims(x, axis=0) # 調整圖片維度
    preds = model.predict(x) # 預測
    return preds[0]
複製程式碼

用這個函式可以預測單張圖片:

predict(model,`E:PyProjectsDataSetFireAIDeepLearning/FireAI005/cat11.jpg`,(IMG_W,IMG_H))

predict(model,`E:PyProjectsDataSetFireAIDeepLearning//FireAI005/dog4.jpg`,(IMG_W,IMG_H))
複製程式碼

————————————-輸———出——————————–

array([0.14361556], dtype=float32)

array([0.9942463], dtype=float32)

——————————————–完————————————-

可以看出,對於單張圖片cat11.jpg得到的概率為0.14,而dog4.jpg的概率為0.99,可以看出第0個類別是dog,第1個類別是cat,模型能夠很好的區分開來。

多張圖片的預測

如果想用這個模型來預測一個資料夾中的所有圖片,那麼該怎麼辦了?

# 預測一個資料夾中的所有圖片
new_sample_gen=ImageDataGenerator(rescale=1. / 255)
newsample_generator=new_sample_gen.flow_from_directory(
        `E:PyProjectsDataSetFireAIDeepLearning`,
        target_size=(IMG_W, IMG_H),
        batch_size=16,
        class_mode=None,
        shuffle=False)
predicted=model.predict_generator(newsample_generator)
print(predicted)
複製程式碼

————————————-輸———出——————————–

Found 4 images belonging to 2 classes.
[[0.14361556]
[0.5149474 ]
[0.71455824]
[0.9942463 ]]

——————————————–完————————————-

上面的結果中第二個0.5149對應的應該是cat,應該小於0.5,這個預測是錯誤的,不過粗略估計正確率有3/4=75%。

2.4 模型的儲存和載入

模型一般要及時儲存到硬碟上,防止資料丟失,下面是儲存的程式碼:

# 模型儲存
# model.save_weights(`E:PyProjectsDataSetFireAIDeepLearning//FireAI005/FireAI005_Model.h5`) # 這個只儲存weights,不儲存模型的結構
model.save(`E:PyProjectsDataSetFireAIDeepLearning//FireAI005/FireAI005_Model2.h5`) # 對於一個完整的模型,應該要儲存這個
複製程式碼
# 模型的載入,預測
from keras.models import load_model
saved_model=load_model(`E:PyProjectsDataSetFireAIDeepLearning//FireAI005/FireAI005_Model2.h5`)

predicted=saved_model.predict_generator(newsample_generator)
print(predicted) # saved_model的結果和前面的model結果一致,表面模型正確儲存和載入
複製程式碼

此處得到的結果和上面model預測的結果一模一樣,表明模型被正確儲存和載入。

########################小**********結###############################

1,本篇文章講解了:準備一個簡單的小資料集,從資料集中建立資料流,將該資料流引入到Keras的模型中進行訓練,並使用訓練後的模型進行新圖片的預測,然後將模型進行儲存,載入儲存好的模型到記憶體中。

2,此處使用的模型是我們自己搭建的,結構比較簡單,只有三層卷積層和兩層全連線層,故而模型的準確率不太高,而且此處由於時間關係,我只訓練了20個epoch,訓練並沒有達到平臺期。

#################################################################

注:本部分程式碼已經全部上傳到(我的github)上,歡迎下載。

相關文章