【火爐煉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()
複製程式碼
很明顯,由於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)上,歡迎下載。