Tensorflow实现卷积神经网络,用于人脸关键点识别

时间:2023-02-07 09:27:23

今年来人工智能的概念越来越火,AlphaGo以4:1击败李世石更是起到推波助澜的作用。作为一个开挖掘机的菜鸟,深深感到不学习一下deep learning早晚要被淘汰。

既然要开始学,当然是搭一个深度神经网络跑几个数据集感受一下作为入门最直观了。自己写代码实现的话debug的过程和运行效率都会很忧伤,我也不知道怎么调用GPU… 所以还是站在巨人的肩膀上,用现成的框架吧。粗略了解一下,现在比较知名的有caffe、mxnet、tensorflow等等。选哪个呢?对我来说选择的标准就两个,第一要容易安装(想尽快入门的迫切心情实在难以忍受一大堆的配置安装…);第二文档要齐全(这应该是废话 - -)。这几个大名鼎鼎的框架文档都是比较齐全的,那就看最容易安装的。看了几个文档,tensorflow算是最容易安装的了。基本就是pip intall 给定的URL就可以了。安装方式的文档可以在tensorflow安装教程上查看。

tensorflow基本概念与用法

tensorflow直译过来就是张量流。去年google刚推出tensorflow的时候我就纳闷,为什么深度学习会牵扯到张量,以前学弹塑性力学的时候就是一大堆张量看的很烦…不过还好要理解tensorflow里的tensor完全不用理会那些。先来看一下官方文档的说明:

class tf.Tensor

Represents a value produced by an Operation.

A Tensor is a symbolic handle to one of the outputs of an Operation. It does not hold the values of that operation’s output, but instead provides a means of computing those values in a TensorFlow Session.

首先,Tensor代表了执行一个操作(运算)所产生的值。其次,一个Tensor实例并不会保存具体的值,而只是代表了产生这些值的运算方式。好像有些拗口,也就是说假如有一个加法操作add,令c = add(1,1)。那么c就是一个tensor实例了,代表了1+1的结果,但是它并没有存储2这个具体的值,它只知道它代表1+1这个运算。从这里也可以看出,tensorflow里的api都是惰性求值,等真正需要知道具体的值的时候,才会执行计算,其他时候都是在定义计算的过程。

Tensor可以代表从常数一直到N维数组的值。

Flow指的是,指的是tensorflow这套框架里的数据传递全部都是tensor,也就是运算的输入,输出都是tensor。

常用操作

这里只是简单介绍一下在后面定义卷积神经网络的时候会用到的东西。想要了解更详细的内容还得参考官网上的文档。

首先import tensorflow as tf,后面的tf就代表tensorflow啦。

常数

tf.constant 是一个Operation,用来产生常数,可以产生scalar与N-D array. a是一个tensor,代表了由constant这个Operation所产生的标量常数值的过程。 b就是代表了产生一个2*2的array的过程。

a = tf.constant(3)
b = tf.constant(3,shape=[2,2])

变量

变量代表了神经网络中的参数,在优化计算的过程中需要被改变。tf.Variable当然也是一个Operation,用来产生一个变量,构造函数需要传入一个Tensor对象,传入的这个Tensor对象就决定了这个变量的值的类型(float 或 int)与shape。

变量虽然与Tensor有不同的类型,但是在计算过程中是与Tensor一样可以作为输入输出的。(可以理解为Tensor的派生类,但是实际上可能并不是这样,我还没有看源码)

变量在使用前都必须初始化。

w = tf.Variable(b)

Operation

其实Operation不应该单独拿出来说,因为之前的tf.constant和tf.Variable都是Op,不过还是说一下常规的操作,比如tf.matmul执行矩阵计算,tf.conv2d用于卷积计算,Op的详细用法以及其他的Op可以参考api文档。

tf.matmul(m,n)
tf.conv2d(...)

TensorFlow的计算由不同的Operation组成,比如下图
Tensorflow实现卷积神经网络,用于人脸关键点识别

定义了6*(3+5)这个计算过程。6、3、5其实也是Op,这在前面介绍过了。

卷积神经网络用于人脸关键点识别

写到这里终于要开始进入正题了,先从CNN做起吧。Tensorflow的tutorial里面有介绍用CNN(卷积神经网络)来识别手写数字,直接把那里的代码copy下来跑一遍也是可以的。但是那比较没有意思,kaggle上有一个人脸关键点识别的比赛,有数据集也比较有意思,就拿这个来练手了。

定义卷积神经网络

首先是定义网络结构,在这个例子里我用了3个卷积层,第一个卷积层用 33 的卷积核,后面两个用 22 的卷积核。每个卷积层后面都跟max_pool池化层,之后再跟3个全连接层(两个隐层一个输出层)。每个卷积层的feature_map分别用32、64、128。

产生权值的函数代码如下

#根据给定的shape定义并初始化卷积核的权值变量
def weight_variable(shape):
initial = tf.truncated_normal(shape, stddev=0.1)
return tf.Variable(initial)

#根据shape初始化bias变量
def bias_variable(shape):
initial = tf.constant(0.1, shape=shape)
return tf.Variable(initial)

定义卷积运算的代码如下。对tf.nn.con2d()的参数还是要说明一下
1. x是输入的样本,在这里就是图像。x的shape=[batch, height, width, channels]。
- batch是输入样本的数量
- height, width是每张图像的高和宽
- channels是输入的通道,比如初始输入的图像是灰度图,那么channels=1,如果是rgb,那么channels=3。对于第二层卷积层,channels=32。
2. W表示卷积核的参数,shape的含义是[height,width,in_channels,out_channels]。
3. strides参数表示的是卷积核在输入x的各个维度下移动的步长。了解CNN的都知道,在宽和高方向stride的大小决定了卷积后图像的size。这里为什么有4个维度呢?因为strides对应的是输入x的维度,所以strides第一个参数表示在batch方向移动的步长,第四个参数表示在channels上移动的步长,这两个参数都设置为1就好。重点就是第二个,第三个参数的意义,也就是在height于width方向上的步长,这里也都设置为1。
4. padding参数用来控制图片的边距,’SAME’表示卷积后的图片与原图片大小相同,’VALID’的话卷积以后图像的高为 Heightout=HeightHeight+1StrideHeight , 宽也同理。

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

接着是定义池化层的代码,这里用 22 的max_pool。参数ksize定义pool窗口的大小,每个维度的意义与之前的strides相同,所以实际上我们设置第二个,第三个维度就可以了。

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

定义好产生权重、卷积、池化的函数以后就要开始组装这个卷积神经网络了。定义之前再定义一下输入样本x与对应的目标值y_。这里用了tf.placeholder表示此时的xy_是指定shape的站位符,之后在定义网络结构的时候并不需要真的输入了具体的样本,只要在求值的时候feed进去就可以了。激活函数用relu,api也就是tf.nn.relu
keep_prob是最后dropout的参数,dropout的目的是为了抗过拟合。

rmse是损失函数,因为这里的目的是为了检测人脸关键点的位置,是回归问题,所以用root-mean-square-error。并且最后的输出层不需要套softmax,直接输出y值就可以了。

这样就组装好了一个卷积神经网络。后续的步骤就是根据输入样本来train这些参数啦。

    x = tf.placeholder("float", shape=[None, 96, 96, 1])
y_ = tf.placeholder("float", shape=[None, 30])
keep_prob = tf.placeholder("float")

def model():
W_conv1 = weight_variable([3, 3, 1, 32])
b_conv1 = bias_variable([32])

h_conv1 = tf.nn.relu(conv2d(x, W_conv1) + b_conv1)
h_pool1 = max_pool_2x2(h_conv1)

W_conv2 = weight_variable([2, 2, 32, 64])
b_conv2 = bias_variable([64])

h_conv2 = tf.nn.relu(conv2d(h_pool1, W_conv2) + b_conv2)
h_pool2 = max_pool_2x2(h_conv2)

W_conv3 = weight_variable([2, 2, 64, 128])
b_conv3 = bias_variable([128])

h_conv3 = tf.nn.relu(conv2d(h_pool2, W_conv3) + b_conv3)
h_pool3 = max_pool_2x2(h_conv3)

W_fc1 = weight_variable([11 * 11 * 128, 500])
b_fc1 = bias_variable([500])

h_pool3_flat = tf.reshape(h_pool3, [-1, 11 * 11 * 128])
h_fc1 = tf.nn.relu(tf.matmul(h_pool3_flat, W_fc1) + b_fc1)

W_fc2 = weight_variable([500, 500])
b_fc2 = bias_variable([500])

h_fc2 = tf.nn.relu(tf.matmul(h_fc1, W_fc2) + b_fc2)
h_fc2_drop = tf.nn.dropout(h_fc2, keep_prob)

W_fc3 = weight_variable([500, 30])
b_fc3 = bias_variable([30])

y_conv = tf.matmul(h_fc2_drop, W_fc3) + b_fc3
rmse = tf.sqrt(tf.reduce_mean(tf.square(y_ - y_conv)))
return y_conv, rmse

训练卷积神经网络

读取训练数据

定义好卷积神经网络的结构之后,就要开始训练。训练首先是要读取训练样本。下面的代码用于读取样本。

    import pandas as pd
import numpy as np

TRAIN_FILE = 'training.csv'
TEST_FILE = 'test.csv'
SAVE_PATH = 'model'


VALIDATION_SIZE = 100 #验证集大小
EPOCHS = 100 #迭代次数
BATCH_SIZE = 64 #每个batch大小,稍微大一点的batch会更稳定
EARLY_STOP_PATIENCE = 10 #控制early stopping的参数


def input_data(test=False):
file_name = TEST_FILE if test else TRAIN_FILE
df = pd.read_csv(file_name)
cols = df.columns[:-1]

#dropna()是丢弃有缺失数据的样本,这样最后7000多个样本只剩2140个可用的。
df = df.dropna()
df['Image'] = df['Image'].apply(lambda img: np.fromstring(img, sep=' ') / 255.0)

X = np.vstack(df['Image'])
X = X.reshape((-1,96,96,1))

if test:
y = None
else:
y = df[cols].values / 96.0 #将y值缩放到[0,1]区间

return X, y

#最后生成提交结果的时候要用到
keypoint_index = {
'left_eye_center_x':0,
'left_eye_center_y':1,
'right_eye_center_x':2,
'right_eye_center_y':3,
'left_eye_inner_corner_x':4,
'left_eye_inner_corner_y':5,
'left_eye_outer_corner_x':6,
'left_eye_outer_corner_y':7,
'right_eye_inner_corner_x':8,
'right_eye_inner_corner_y':9,
'right_eye_outer_corner_x':10,
'right_eye_outer_corner_y':11,
'left_eyebrow_inner_end_x':12,
'left_eyebrow_inner_end_y':13,
'left_eyebrow_outer_end_x':14,
'left_eyebrow_outer_end_y':15,
'right_eyebrow_inner_end_x':16,
'right_eyebrow_inner_end_y':17,
'right_eyebrow_outer_end_x':18,
'right_eyebrow_outer_end_y':19,
'nose_tip_x':20,
'nose_tip_y':21,
'mouth_left_corner_x':22,
'mouth_left_corner_y':23,
'mouth_right_corner_x':24,
'mouth_right_corner_y':25,
'mouth_center_top_lip_x':26,
'mouth_center_top_lip_y':27,
'mouth_center_bottom_lip_x':28,
'mouth_center_bottom_lip_y':29
}

开始训练

执行训练的代码如下,save_model用于保存当前训练得到在验证集上loss最小的模型,方便以后直接拿来用。

tf.InteractiveSession()用来生成一个Session,(好像是废话…)。Session相当于一个引擎,TensorFlow框架要真正的进行计算,都要通过Session引擎来启动。

tf.train.AdamOptimizer是优化的算法,Adam的收敛速度会比较快,1e-3是learning rate,这里先简单的用固定的。minimize就是要最小化的目标,当然是最小化均方根误差了。

    def save_model(saver,sess,save_path):
path = saver.save(sess, save_path)
print 'model save in :{0}'.format(path)

if __name__ == '__main__':
sess = tf.InteractiveSession()
y_conv, rmse = model()
train_step = tf.train.AdamOptimizer(1e-3).minimize(rmse)

#变量都要初始化
sess.run(tf.initialize_all_variables())
X,y = input_data()
X_valid, y_valid = X[:VALIDATION_SIZE], y[:VALIDATION_SIZE]
X_train, y_train = X[VALIDATION_SIZE:], y[VALIDATION_SIZE:]

best_validation_loss = 1000000.0
current_epoch = 0
TRAIN_SIZE = X_train.shape[0]
train_index = range(TRAIN_SIZE)
random.shuffle(train_index)
X_train, y_train = X_train[train_index], y_train[train_index]

saver = tf.train.Saver()

print 'begin training..., train dataset size:{0}'.format(TRAIN_SIZE)
for i in xrange(EPOCHS):
random.shuffle(train_index) #每个epoch都shuffle一下效果更好
X_train, y_train = X_train[train_index], y_train[train_index]

for j in xrange(0,TRAIN_SIZE,BATCH_SIZE):
print 'epoch {0}, train {1} samples done...'.format(i,j)

train_step.run(feed_dict={x:X_train[j:j+BATCH_SIZE],
y_:y_train[j:j+BATCH_SIZE], keep_prob:0.5})

#电脑太渣,用所有训练样本计算train_loss居然死机,只好注释了。
#train_loss = rmse.eval(feed_dict={x:X_train, y_:y_train, keep_prob: 1.0})
validation_loss = rmse.eval(feed_dict={x:X_valid, y_:y_valid, keep_prob: 1.0})

print 'epoch {0} done! validation loss:{1}'.format(i, validation_loss*96.0)
if validation_loss < best_validation_loss:
best_validation_loss = validation_loss
current_epoch = i
save_model(saver,sess,SAVE_PATH) #即时保存最好的结果
elif (i - current_epoch) >= EARLY_STOP_PATIENCE:
print 'early stopping'
break

在测试集上预测

下面的代码用于预测test.csv里面的人脸关键点,最后的y值要乘以96,因为之前缩放到[0,1]区间了。

    X,y = input_data(test=True)
y_pred = []

TEST_SIZE = X.shape[0]
for j in xrange(0,TEST_SIZE,BATCH_SIZE):
y_batch = y_conv.eval(feed_dict={x:X[j:j+BATCH_SIZE], keep_prob:1.0})
y_pred.extend(y_batch)

print 'predict test image done!'

output_file = open('submit.csv','w')
output_file.write('RowId,Location\n')

IdLookupTable = open('IdLookupTable.csv')
IdLookupTable.readline()

for line in IdLookupTable:
RowId,ImageId,FeatureName = line.rstrip().split(',')
image_index = int(ImageId) - 1
feature_index = keypoint_index[FeatureName]
feature_location = y_pred[image_index][feature_index] * 96
output_file.write('{0},{1}\n'.format(RowId,feature_location))

output_file.close()
IdLookupTable.close()

结果

用这个结构的卷积神经网络训练出来的模型,在测试集上预测的结果提交以后的成绩是3.4144,在kaggle的leaderboard上是41名,初试CNN,感觉还可以了。这只是数据,还是找一些现实的照片来试试这个模型如何,所以我找了一张anglababy的,标识出来的关键点感觉还算靠谱。基于TensorFlow的卷积神经网络先写到这了,有什么遗漏的想起来再补充,之后对深度学习更了解了,再写写CNN的原理,bp的推导过程之类的。

Tensorflow实现卷积神经网络,用于人脸关键点识别