基于CNN的嬰兒睡覺狀態(tài)識別(上)—模型訓(xùn)練

前言

?? 最近在和同學(xué)做一個智能嬰兒床的創(chuàng)業(yè)項目,項目涉及到了對嬰兒的睡眠狀態(tài)的識別,整個流程下來相當(dāng)于一次簡單的CNN圖像分類,雖然只是個Toy Model,但作為練手也是很有收獲的,識別內(nèi)容主要有兩點:

1、很多嬰兒在睡覺時候會亂動踢掉被子,識別嬰兒有沒有蓋著被子反饋給溫控器,如果被子被踢掉了,就相應(yīng)升高溫度。

2、嬰兒長時間不換睡姿容易得扁頭癥,識別嬰兒的睡姿,若持續(xù)時間太長則輔助嬰兒翻身。

扁頭癥

以下主要介紹識別的實現(xiàn)流程,首先是采集訓(xùn)練集

訓(xùn)練集采集

訓(xùn)練集主要用模型娃娃,然后拿單反拍攝,效果如下

好像有點嚇人

??為了識別結(jié)果的魯棒性,訓(xùn)練集應(yīng)該盡量全面一些,因此我們對于不同被子褶皺紋理、不同被子傾斜角度、嬰兒不同睡姿:平躺、左側(cè)躺、右側(cè)躺、趴睡等不同睡姿,甚至于燈光的角度亮度都做了變化,不蓋被子狀態(tài)也是同理,最終共拍攝不蓋被子樣本+蓋被子樣本共277張。

??但是顯然,277張樣本對于CNN來說不夠多,過擬合的風(fēng)險很高,因此需要數(shù)據(jù)增強。

數(shù)據(jù)增強

??所謂數(shù)據(jù)增強,就是把圖片縮小一點,放大一點,旋轉(zhuǎn)一點,翻轉(zhuǎn)一下來擴充訓(xùn)練集,如果不這么做的話,過擬合的幾率是非常大的,直接上代碼

導(dǎo)入圖片

from PIL import Image
import numpy as np
import matplotlib.pyplot as plt
img = Image.open('./baby.png')
img = np.array(img)
plt.imshow(img)
plt.show()

翻轉(zhuǎn)

flipped_img = np.fliplr(img)
plt.imshow(flipped_img)
plt.show()

平移

for i in range(HEIGHT, 1, -1):
  for j in range(WIDTH):
     if (i < HEIGHT-20):
       img[j][i] = img[j][i-20]
     elif (i < HEIGHT-1):
       img[j][i] = 0
plt.imshow(img)
plt.show()

其余不再贅述,可自行查看相關(guān)博客。
最終圖像增強了大概4倍,得到訓(xùn)練集共1300張左右。

訓(xùn)練集預(yù)處理

前處理主要包括兩點:壓縮圖像,歸一化,做完前處理后才能丟進網(wǎng)絡(luò)訓(xùn)練。

圖像壓縮

??我們的單反非常牛啤,每張都有4K的分辨率(4096×2160)。。。 因此圖像壓縮是必要的,否則這么大的圖像不知道訓(xùn)練到猴年馬月了。

??通常訓(xùn)練的圖像會把圖像壓縮至32X32的像素大小( 李飛飛的Imagenet好像就是這么做的)不過我們本身樣本不多,而且網(wǎng)絡(luò)不大,壓到這么小會嚴重失真,因此我們最終把圖像壓縮到128X128像素大小,上代碼

from PIL import Image
import os
from glob import glob
 
fpath = filepath+"baby"
size = (128, 128)   # 要調(diào)整成為的尺寸
files = glob( fpath + "**/*.JPG", recursive=True) + glob(fpath + "**/*.jpg", recursive=True)
total = len(files) #總文件數(shù)
cur = 1 #當(dāng)前文件序號
print("共有" + str(total) + "個文件,開始處理")
print("-----------------------------------")
for infile in files:
    try:
       # 分離文件名和后綴
        print("進度:" + str(cur) + "/" + str(total) + "   " + infile)
        img = Image.open(infile) # 打開圖片文件
        mg.thumbnail(size, Image.ANTIALIAS) # 使用抗鋸齒模式生成縮略圖(壓縮圖片)
        img.save(infile, "JPEG") # 自動覆蓋源文件

        cur = cur + 1
 
    except OSError:
        print(infile + "文件錯誤,忽略")

我們來看下縮小后的效果

未壓縮.png
壓縮后.png

歸一化

??歸一化是通過特定操作,讓輸入等比例縮放或平移,最終保持在(0,1)的范圍內(nèi)。歸一化非常有必要,否則很難收斂甚至不能收斂,歸一化同時能有效增加收斂速度,提高模型精度。

未歸一化:

歸一化:


??可見歸一化能有效防止訓(xùn)練時梯度走之子型,步長也能比較均等,對于訓(xùn)練的收斂是有幫助的。

??對于RGB圖像類數(shù)據(jù),每個像素點有RGB三通道,每個通道的值為0~255之間,通常圖像歸一化有兩種方法:
?? 1.以0.5為中心的壓縮方式,將每個像素的RGB值簡單除以255,就能將所有值壓縮到0~1的范圍內(nèi)了。
?? 2.以0為中心的壓縮方式,將每個像素的RGB值除以127.5,這樣值在0~2之間,之后再減去一個1,使得值的分布為(-1—1)之間。

??我采用的是方法1,方法1與2的區(qū)別應(yīng)該只是表達習(xí)慣的區(qū)別,實際上對訓(xùn)練結(jié)果應(yīng)當(dāng)不影響。

Tensorflow下小型CNN模型建立

??以前做類似圖像二分類的時候用過遷移學(xué)習(xí),就是把VGG-16的網(wǎng)絡(luò)最后幾層包括softmax拆掉,補上兩層全連接,再對全連接層的參數(shù)做訓(xùn)練即可。

??然而因為此次需要把模型導(dǎo)入至樹莓派,內(nèi)存不能超過500M,而VGG-16光是參數(shù)就好幾個G了,所以需要自己寫小型CNN來實現(xiàn)。

1、讀取訓(xùn)練集

先前歸一化后的訓(xùn)練集用numpy保存為了train_x.npy與train_y.npy兩個文件,用Load指令加載到環(huán)境中,因為要采用Mini—batch的方式,對訓(xùn)練集作了打亂

train_x = np.load("C:/Users/Administrator/Desktop/fzdl/data/train_x.npy")
train_y = np.load("C:/Users/Administrator/Desktop/fzdl/data/train_y.npy")
index=np.arange(1108)
np.random.shuffle(index)
train_x = train_x[index]
train_y = train_y[index]
2、定義權(quán)重、偏置、卷積層、池化操作

唯一要注意的是權(quán)重要用tf.truncated_normal的方式來定義,以實現(xiàn)權(quán)重初始化的隨機性,否則可能造成梯度無法下降導(dǎo)致訓(xùn)練失敗。

卷積層與池化層都采用了same padding,即操作時會自動在圖像外圍補充一圈數(shù)字0,使得卷積或池化操作后圖像規(guī)格不會小一圈;池化采用了常用的最大池化max_pool

def weight_variable(shape, name):
    initializer = tf.truncated_normal(shape, stddev=0.1)
    return tf.Variable(initializer, name = name)

def bias_variable(shape, name):
    initializer = tf.constant(0.1, shape=shape)
    return tf.Variable(initializer, name = name)

def conv2d(x, W):
    
    return tf.nn.conv2d(x, W, strides=[1, 1, 1, 1], padding='SAME')

def max_pool_2x2(x, name):
    
    return tf.nn.max_pool(x, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME', name = name)

3、搭建網(wǎng)絡(luò)

來到最關(guān)鍵的部分,首先定義兩個placeholder做參數(shù)傳入,要記得給輸入xs命名,因為以后單片機模型轉(zhuǎn)化過程會需要輸入節(jié)點名字

第一維參數(shù)定義成None,代表可以為任意的值,這樣傳多大的batch進去都不用回頭修改,是比較方便的寫法

xs = tf.placeholder(tf.float32, [None, 128, 128,3], name = 'input')   # 128x128
ys = tf.placeholder(tf.float32, [None, 2])

來到正式搭建網(wǎng)絡(luò)步驟,我們將網(wǎng)絡(luò)寫成一個inference(x)的函數(shù),規(guī)格為兩層卷積,兩層全連接,最后softmax輸出。

激活函數(shù)統(tǒng)一采用relu,每層卷積操作后接最大池化,卷積層過度到全連接層要reshape一遍

def inference(x):
    
    W_conv1 = weight_variable([3, 3, 3, 16], 'W_conv1') # patch 5x5, in size 3, out size 16
    b_conv1 = bias_variable([16], 'b_conv1')
    h_conv1 = tf.nn.relu((conv2d(x, W_conv1) + b_conv1), name = 'conv1') # output size 128x128x16
    h_pool1 = max_pool_2x2(h_conv1, 'pool1')                                         # output size 64x64x16
    
    ## conv2 layer 
    W_conv2 = weight_variable([3, 3, 16, 32], 'W_conv2') # patch 5x5, in size 16, out size 32
    b_conv2 = bias_variable([32], 'b_conv2')
    h_conv2 = tf.nn.relu((conv2d(h_pool1, W_conv2) + b_conv2), name = 'conv2') # output size 64x64x32
    h_pool2 = max_pool_2x2(h_conv2, name = 'pool2')                                         # output size 32x32x32
    
    ## flatten
    h_pool_flat = tf.reshape(h_pool2, [-1, 32 * 32 * 32])
    
    ## fc1 layer
    W_fc1 = weight_variable([32 * 32 * 32, 64], 'W_fc1')
    b_fc1 = bias_variable([64], 'b_fc1')
    h_fc1 = tf.nn.relu((tf.matmul(h_pool_flat, W_fc1) + b_fc1), name = 'fc1')
    h_fc1 = tf.nn.dropout(h_fc1, keep_prob = 0.9, name = 'dropout')
    
    ## fc2 layer
    W_fc2 = weight_variable([64, 2], 'W_fc2')
    b_fc2 = bias_variable([2], 'b_fc2')
    prediction = tf.nn.bias_add(tf.matmul(h_fc1, W_fc2), b_fc2, name = 'output')  
    prediction = tf.nn.softmax(prediction, name = 'softmax_output')
    
    return prediction

4、損失函數(shù)、優(yōu)化器

損失函數(shù)為交叉熵,優(yōu)化器為adam,很平常的選擇,沒什么好說的

prediction = inference(xs)
cross_entropy = tf.nn.softmax_cross_entropy_with_logits(labels = ys, logits = prediction)
_loss = tf.reduce_mean(cross_entropy)
train_step = tf.train.AdamOptimizer(1e-4).minimize(_loss)

5、開始訓(xùn)練

首先定義準(zhǔn)確率acc,再定義saver以保存訓(xùn)練好的模型,然后就把定義好的各種東西直接run就好了。

bool_acc = tf.equal(tf.arg_max(prediction, 1), tf.arg_max(ys, 1))
acc = tf.reduce_mean(tf.cast(bool_acc, tf.float32))
saver = tf.train.Saver()

with tf.Session() as sess:
    
    init = tf.global_variables_initializer()
    summary_writer = tf.summary.FileWriter(log_dir, sess.graph)
    sess.run(init)
    
    loss_vec = []
    acc_vec = []
    
    for i in range(max_epoch):
        start = time.clock()
        loss, accuracy, _ = sess.run([_loss, acc, train_step],feed_dict={xs: train_x, ys: train_y})
        
        loss_vec.append(loss)

        time_cost = time.clock() - start
        print ('step %d, loss = %.4f, accuracy = %.4f, it costs %g' % (i + 1, loss, accuracy, time_cost),'s')
            
        if i + 1 == max_epoch:
            accc = sess.run(_acc, feed_dict={xs: train_x, ys:train_y})
            print(accc)
            saver.save(sess, os.path.join(model_save_path,model_name), global_step = i)

另外,本來打算用mini-batch的,然而發(fā)現(xiàn)運行的飛快,根本用不著了,最后還是用fullbatch完成了訓(xùn)練,共訓(xùn)練250步,約220步的時候就達到收斂,Loss降到了0.01左右,accuracy達到了100%


訓(xùn)練結(jié)果

結(jié)語

??實際上還是缺了一些步驟,做得還不夠講究:
??比如沒有防止過擬合的措施,應(yīng)該加入dropout;
??沒有分割出一部分交叉訓(xùn)練集,準(zhǔn)確率雖然在訓(xùn)練集上很高,但是可能是過擬合造成的假象;
??按Andrew ng的說法,還應(yīng)該調(diào)整各種參數(shù)去畫學(xué)習(xí)曲線,然后覺得下一步的優(yōu)化;

??不過由于項目時間有限,目前以能用就行作為唯一要求,而且在后續(xù)導(dǎo)入樹莓派后發(fā)現(xiàn)運氣很好的,并沒有過擬合,識別效果非常好~~~

??不過未來有時間總之還是得繼續(xù)優(yōu)化的。。
模型訓(xùn)練的代碼請移步至我的github:https://github.com/huangchuhccc/baby_recognition_by_Horned_sungem_using_tensorflow
后續(xù)嵌入樹莓派+角峰鳥的流程請移步至下篇
http://www.itdecent.cn/p/dd190c5dcbcd
后續(xù)又實現(xiàn)了利用嬰兒是否入睡的判定與微信的實時提醒,請移步這篇博客:
http://www.itdecent.cn/p/45918d2ed025

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容