【火爐煉AI】深度學(xué)習(xí)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數(shù)據(jù)集作為深度學(xué)習(xí)屆的“Hello World”程序早芭,但是這個(gè)數(shù)據(jù)集有一個(gè)很大的特點(diǎn):它是一個(gè)典型的多分類問題(一共有10個(gè)分類),在我們剛剛開始接觸深度學(xué)習(xí)時(shí)妒茬,我倒是覺得應(yīng)該從最簡單的二分類問題著手阱持。
在深度學(xué)習(xí)框架方面撩嚼,目前比較流行的是Tensorflow捆憎,Keras按咒,PyTorch迟隅,Theano等,但是我建議新手入門励七,可以從Keras入手智袭,然后進(jìn)階時(shí)轉(zhuǎn)移到Tensorflow上,實(shí)際上掠抬,Keras的后端是可以支持Tensorflow和Theano吼野,可以說,Keras是在Tensorflow和Theano的基礎(chǔ)上進(jìn)一步封裝两波,更加的簡單實(shí)用瞳步,更容易入門闷哆,通常幾行簡單的代碼就可以解決一個(gè)小型的項(xiàng)目問題。
我這篇博文主要參考了:keras系列︱圖像多分類訓(xùn)練與利用bottleneck features進(jìn)行微調(diào)(三)单起,這篇博文也是參考的Building powerful image classification models using very little data抱怔,但我發(fā)現(xiàn)這兩篇博文有很多地方的代碼跑不起來,主要原因可能是Keras或Tensorflow升級造成的嘀倒,所以我做了一些必要的修改屈留。
1. 準(zhǔn)備數(shù)據(jù)集
最經(jīng)典的二分類數(shù)據(jù)集就是Kaggle競賽中的“貓狗大戰(zhàn)”數(shù)據(jù)集(train set有25K張圖片,test set: 12.5K)测蘑,此處按照原始博文的做法灌危,我從train_set中選取1000張Dog的照片+1000張Cat照片作為我們新的train set,選取400張Dog+400張Cat照片作為新的test set碳胳。所以train和test兩個(gè)文件夾下都有兩個(gè)子文件夾(cats和dogs子文件夾)勇蝙。當(dāng)然,選取是隨機(jī)的挨约,也是用代碼來實(shí)現(xiàn)的味混,準(zhǔn)備小數(shù)據(jù)集的代碼如下:
def dataset_prepare(raw_set_folder,dst_folder,train_num_per_class=1000,test_num_per_class=400):
'''
準(zhǔn)備小數(shù)據(jù)集,從原始的raw_set_folder數(shù)據(jù)集中提取train_num_per_class(每個(gè)類別)的照片放入train中烫罩,
提取val_num_per_class(每個(gè)類別)放入到validation文件夾中
:param raw_set_folder: 含有貓狗的照片惜傲,這些照片的名稱必須為cat.101.jpg或dog.102.jpg形式
:param dst_folder: 將選取之后的圖片放置到這個(gè)文件夾中
: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']]
# 下面的代碼可以進(jìn)一步優(yōu)化。贝攒。盗誊。。
for cat_img_path in cat_imgs[:train_num_per_class]: # 最開始的N個(gè)圖片作為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個(gè)圖片作為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個(gè)圖片作為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...')
運(yùn)行該函數(shù)即可完成小數(shù)據(jù)集的構(gòu)建隘弊,下面為Keras創(chuàng)建圖片數(shù)據(jù)流哈踱,為模型的構(gòu)建做準(zhǔn)備。
# 2梨熙,準(zhǔn)備訓(xùn)練集开镣,keras有很多Generator可以直接處理圖片的加載,增強(qiáng)等操作咽扇,封裝的非常好
from keras.preprocessing.image import ImageDataGenerator
train_datagen = ImageDataGenerator( # 單張圖片的處理方式邪财,train時(shí)一般都會進(jìn)行圖片增強(qiáng)
rescale=1. / 255, # 圖片像素值為0-255,此處都乘以1/255质欲,調(diào)整到0-1之間
shear_range=0.2, # 斜切
zoom_range=0.2, # 放大縮小范圍
horizontal_flip=True) # 水平翻轉(zhuǎn)
train_generator = train_datagen.flow_from_directory(# 從文件夾中產(chǎn)生數(shù)據(jù)流
train_data_dir, # 訓(xùn)練集圖片的文件夾
target_size=(IMG_W, IMG_H), # 調(diào)整后每張圖片的大小
batch_size=batch_size,
class_mode='binary') # 此處是二分類問題树埠,故而mode是binary
# 3,同樣的方式準(zhǔn)備測試集
val_datagen = ImageDataGenerator(rescale=1. / 255) # 只需要和trainset同樣的scale即可嘶伟,不需增強(qiáng)
val_generator = val_datagen.flow_from_directory(
val_data_dir,
target_size=(IMG_W, IMG_H),
batch_size=batch_size,
class_mode='binary')
上面構(gòu)建的generator就是keras需要的數(shù)據(jù)流怎憋,該數(shù)據(jù)流使用flow_from_directory首先從圖片文件夾(比如train_data_dir)中加載圖片到內(nèi)存中,然后使用train_datagen來對圖片進(jìn)行預(yù)處理和增強(qiáng),最終得到處理完成之后的batch size大小的數(shù)據(jù)流绊袋,這個(gè)數(shù)據(jù)流會無限循環(huán)的產(chǎn)生毕匀,直到達(dá)到一定的訓(xùn)練epoch數(shù)量為止。
上面用到了ImageDataGenerator來進(jìn)行圖片增強(qiáng)癌别,里面的參數(shù)說明為:(可以參考Keras的官方文檔)
rotation_range是一個(gè)0~180的度數(shù)皂岔,用來指定隨機(jī)選擇圖片的角度。
width_shift和height_shift用來指定水平和豎直方向隨機(jī)移動的程度规个,這是兩個(gè)0~1之間的比例凤薛。
rescale值將在執(zhí)行其他處理前乘到整個(gè)圖像上姓建,我們的圖像在RGB通道都是0255的整數(shù)诞仓,這樣的操作可能使圖像的值過高或過低,所以我們將這個(gè)值定為01之間的數(shù)速兔。
shear_range是用來進(jìn)行剪切變換的程度
zoom_range用來進(jìn)行隨機(jī)的放大
horizontal_flip隨機(jī)的對圖片進(jìn)行水平翻轉(zhuǎn)墅拭,這個(gè)參數(shù)適用于水平翻轉(zhuǎn)不影響圖片語義的時(shí)候
fill_mode用來指定當(dāng)需要進(jìn)行像素填充,如旋轉(zhuǎn)涣狗,水平和豎直位移時(shí)谍婉,如何填充新出現(xiàn)的像素
2. 構(gòu)建并訓(xùn)練Keras模型
由于Keras已經(jīng)封裝了很多Tensorflow的函數(shù),所以在使用上更加簡單容易镀钓,當(dāng)然穗熬,如果想調(diào)整里面的結(jié)構(gòu)和參數(shù)等,也比較麻煩一些丁溅,所以對于高手唤蔗,想要調(diào)整模型的結(jié)構(gòu)和自定義一些函數(shù),可以直接用Tensorflow.
2.1 Keras模型的構(gòu)建
不管是Keras模型還是Tensorflow模型窟赏,我個(gè)人認(rèn)為其構(gòu)建都包括兩個(gè)部分:模型的搭建和模型的配置妓柜,所以可以從這兩個(gè)方面來建立一個(gè)小型的模型。代碼如下:
# 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):
# 模型的搭建:此處構(gòu)建三個(gè)CNN層+2個(gè)全連接層的結(jié)構(gòu)
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)拷况,因?yàn)楹竺娴腶ctivation是sigmoid作煌,這個(gè)函數(shù)只能輸出一個(gè)值,即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'])# 主要優(yōu)化accuracy
# 二分類問題的loss function使用binary_crossentropy,此處使用準(zhǔn)確率作為優(yōu)化目標(biāo)
return model # 返回構(gòu)建好的模型
這個(gè)函數(shù)就搭建了模型的結(jié)構(gòu)蚤告,對模型進(jìn)行了配置努酸,主要配置了loss function, optimzer, 優(yōu)化目標(biāo)等,當(dāng)然可以做更多其他配置杜恰。
此處获诈,為了簡單說明仍源,只是建立了三層卷積層+兩層全連接層的小型網(wǎng)絡(luò)結(jié)構(gòu),當(dāng)然舔涎,對于一些比較簡單的圖像問題笼踩,這個(gè)小型模型也能解決。如果需要構(gòu)建更為復(fù)雜的模型亡嫌,只需要自定義這個(gè)函數(shù)嚎于,修改里面的模型構(gòu)建和配置方法即可。
2.2 模型的訓(xùn)練
由于此處我們使用generator來產(chǎn)生數(shù)據(jù)流挟冠,故而訓(xùn)練時(shí)要使用fit_generator函數(shù)于购。代碼如下:
model=build_model(input_shape=(IMG_W,IMG_H,IMG_CH)) # 輸入的圖片維度
# 模型的訓(xùn)練
model.fit_generator(train_generator, # 數(shù)據(jù)流
steps_per_epoch=train_samples_num // batch_size,
epochs=epochs,
validation_data=val_generator,
validation_steps=val_samples_num // batch_size)
由于我在自己的筆記本上訓(xùn)練,沒有獨(dú)立顯卡知染,更沒有英偉達(dá)那么NB的顯卡肋僧,故而速度很慢,但是的確能運(yùn)行下去控淡。運(yùn)行的具體結(jié)果可以去我的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
--------------------------------------------完-------------------------------------
從訓(xùn)練后的loss和acc上可以大致看出,loss在不斷減小涧狮,acc也不斷增大炕矮,趨勢比較平穩(wěn)。
此處我們可以將訓(xùn)練過程中的loss和acc繪圖勋篓,看看他們的變化趨勢吧享。
# 畫圖,將訓(xùn)練時(shí)的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次數(shù)太少钢颂,acc和loss都沒有達(dá)到平臺期,后續(xù)可以增大epoch次數(shù)來達(dá)到一個(gè)比較好的結(jié)果拜银。在原始博文中殊鞭,作者在50個(gè)epoch之后達(dá)到了約80%左右的準(zhǔn)確率,此處我20個(gè)epoch后的準(zhǔn)確率為74%尼桶。
2.3 預(yù)測新樣本
單張圖片的預(yù)測
模型訓(xùn)練好之后操灿,就需要用來預(yù)測新的圖片,看看它能不能準(zhǔn)確的給出結(jié)果泵督。預(yù)測函數(shù)為:
# 用訓(xùn)練好的模型來預(yù)測新樣本
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 # 相當(dāng)于ImageDataGenerator(rescale=1. / 255)
x = np.expand_dims(x, axis=0) # 調(diào)整圖片維度
preds = model.predict(x) # 預(yù)測
return preds[0]
用這個(gè)函數(shù)可以預(yù)測單張圖片:
predict(model,'E:\PyProjects\DataSet\FireAI\DeepLearning/FireAI005/cat11.jpg',(IMG_W,IMG_H))
predict(model,'E:\PyProjects\DataSet\FireAI\DeepLearning//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個(gè)類別是dog救鲤,第1個(gè)類別是cat久窟,模型能夠很好的區(qū)分開來。
多張圖片的預(yù)測
如果想用這個(gè)模型來預(yù)測一個(gè)文件夾中的所有圖片本缠,那么該怎么辦了斥扛?
# 預(yù)測一個(gè)文件夾中的所有圖片
new_sample_gen=ImageDataGenerator(rescale=1. / 255)
newsample_generator=new_sample_gen.flow_from_directory(
'E:\PyProjects\DataSet\FireAI\DeepLearning',
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 ]]
--------------------------------------------完-------------------------------------
上面的結(jié)果中第二個(gè)0.5149對應(yīng)的應(yīng)該是cat,應(yīng)該小于0.5丹锹,這個(gè)預(yù)測是錯(cuò)誤的稀颁,不過粗略估計(jì)正確率有3/4=75%。
2.4 模型的保存和加載
模型一般要及時(shí)保存到硬盤上楣黍,防止數(shù)據(jù)丟失匾灶,下面是保存的代碼:
# 模型保存
# model.save_weights('E:\PyProjects\DataSet\FireAI\DeepLearning//FireAI005/FireAI005_Model.h5') # 這個(gè)只保存weights,不保存模型的結(jié)構(gòu)
model.save('E:\PyProjects\DataSet\FireAI\DeepLearning//FireAI005/FireAI005_Model2.h5') # 對于一個(gè)完整的模型锡凝,應(yīng)該要保存這個(gè)
# 模型的加載粘昨,預(yù)測
from keras.models import load_model
saved_model=load_model('E:\PyProjects\DataSet\FireAI\DeepLearning//FireAI005/FireAI005_Model2.h5')
predicted=saved_model.predict_generator(newsample_generator)
print(predicted) # saved_model的結(jié)果和前面的model結(jié)果一致,表面模型正確保存和加載
此處得到的結(jié)果和上面model預(yù)測的結(jié)果一模一樣窜锯,表明模型被正確保存和加載。
########################小**********結(jié)###############################
1芭析,本篇文章講解了:準(zhǔn)備一個(gè)簡單的小數(shù)據(jù)集锚扎,從數(shù)據(jù)集中建立數(shù)據(jù)流,將該數(shù)據(jù)流引入到Keras的模型中進(jìn)行訓(xùn)練馁启,并使用訓(xùn)練后的模型進(jìn)行新圖片的預(yù)測驾孔,然后將模型進(jìn)行保存,加載保存好的模型到內(nèi)存中惯疙。
2翠勉,此處使用的模型是我們自己搭建的,結(jié)構(gòu)比較簡單霉颠,只有三層卷積層和兩層全連接層对碌,故而模型的準(zhǔn)確率不太高,而且此處由于時(shí)間關(guān)系蒿偎,我只訓(xùn)練了20個(gè)epoch朽们,訓(xùn)練并沒有達(dá)到平臺期。
#################################################################
注:本部分代碼已經(jīng)全部上傳到(我的github)上诉位,歡迎下載骑脱。