图像的像素这么多(维度这么高),现实的情况如此复杂,我们没有办法手工地定义好规则然后让计算机执行,因此需要准备足够的训练数据,设计算法让机器自己寻找其中的统计规律,然后对训练数据以外的测试样例进行预测,所谓的机器学习。可是如何表示这些训练数据,怎样描述所谓的规律,设计什么样的算法让机器去寻找规律,这些都不是容易的问题。一篇典型的计算机视觉的论文经常就这几个问题提出一两个新的方法。例如对于表情识别这个问题,在数据的表示方面,是直接使用像素值,还是图像的梯度或其他滤波结果,或是其他手工定义的特征,例如HOG?LBP?是在整张图上提取特征来表示,还是在人脸的关键点附近?是人工定义区域来提取还是让机器自己寻找和表情有关的区域?对于规律的描述,是使用一个生成(generative)模型,例如基于PCA的ASM,AAM来描述人脸的动作?还是使用判别(discriminative)模型,例如random forest,深度卷积网络(CNN)选取或学习有意义的特征来预测分类结果?如何让机器自动地寻找规律,即这些模型的参数?是通过添加约束,使得问题存在一个closed form 的解,还是直接使用梯度下降法获得有个局部最优解?
因此,这里头的学问也是五花八门。在此,我们使用深度卷积网络(deep convolutional network, CNN) 直接从整张图像的像素值学习特征,同时通过线性分类器进行表情的分类。
表情分类在计算机视觉领域一直是个不冷不热的话题。怎么说呢?情感分析应该是人机交互里面一个非常重要的问题,可是情感分析简单地表示成表情分类会带来以下问题。
话虽如此,即便表情分类这个提法有自己的限制,能学习出一个表情分类器仍然是有作用的,例如至少我们能判断一个人是否开心或者伤心啊(否则Microsoft Oxford Project 和indico的表情接口就白做啦:))。
这里我们采用的训练数据来自Kaggle的一个表情分类比赛。其中包含三万多张来自网上的人脸图片,每张图片被标注为“无表情、生气、厌恶、惊讶、开心、伤心、害怕”的其中一种。
有了数据,我们还需要一个快速的深度卷积网络的库(否则自己实现可是一大工程的)。目前我所知道的流行的库有Caffe, Torch, Theano, MXNet, Cuda-convnet2,CNTK, TensorFlow. 下面简单说说我用过的几个:
对于训练图像,MXNet可以接受三种形式的输入:CSV文件、Record 文件,NDArray (类似于numpy的array)。其中Record文件是MXNet把图像数据以二进制的方式存储,可以实现多线程读取和解码。相比于CSV文件,更利于读取,相比于NDArray,适合数据量大内存放不下的情况。而且MXNet对于这种数据输入形式,自带实现了训练时的数据增广(augmentation),例如旋转、平移。因此,这里需要把图像准备成Record文件。
转换步骤分为两步,首先生成一个列表文件,文件中每一行记录了图像路径以及label.第二步调用MXNet的im2rec,读入列表文件生成record文件。代码如下:
首先,因为表情数据库有自己的格式,我们先将其准备成方便进一步处理的形式。包括:a)把每个图片文件单独存储出来 b)使用一个pickle文件记录每个图片的路径和label
import csv
import numpy as np
import skimage.io as io
import skimage.transform as transform
import matplotlib as plt
import h5py
import os
import pickle
#fer2013是数据集本来的存储形式
fer_csv_file = 'fer2013.csv'
fi = open(fer_csv_file,'r')
image_directory = 'image'
save_file_name = 'FER.pkl'
if not os.path.exists(image_directory):
os.mkdir(image_directory)
cnt = 0
faces = []
for row in csv.DictReader(fi):
usage = str(row['Usage'])
if usage=='PrivateTest':
continue
cnt+=1
emotion = int(row['emotion'])
#raw_input('test')
pixels = row['pixels']
pixels = (pixels.split(' '))
pixels = [int(x) for x in pixels]
pixels = np.asarray(pixels).reshape((48,48)).astype(np.float32)
pixels = transform.resize(pixels/float(255),(96,96))
image_path=os.path.join(image_directory,str(cnt).zfill(5)+'.bmp')
io.imsave(image_path,(pixels*255).astype(np.uint8))
face_strcture = {}
face_strcture['file_path'] = image_path
face_strcture['expression'] = emotion
faces.append(face_strcture)
print cnt
pickle.dump(faces,open(save_file_name,'w'))
然后,我们读取刚才存储的pickle文件,生成MXNet中im2rec能接收的列表文件,然后调用im2rec生成二进制的record文件。至此,机器的教材准备完毕,代码如下:
import pickle
import random
import csv
import os
data_type = 'train'
lst_file = 'lst/fer_train.lst'
IM2REC_PATH = '../../lib/mxnet/bin/im2rec'
output_bin_file = 'bin/fer_train.bin'
img_root_dir = '../../FER/'
fo = open(lst_file,'w')
fwtr = csv.writer(fo,delimiter='\t',lineterminator='\n')
if data_type=='train':
pkl_file = '../../FER/FER.pkl'
else:
pkl_file = '../../FER/FER_test.pkl'
faces = pickle.load(open(pkl_file,'r'))
sample_lst=[]
for idx,face in enumerate(faces):
sample_lst.append((idx,face['expression'],face['file_path']))
random.shuffle(sample_lst)
for item in sample_lst:
fwtr.writerow(item)
fo.close()
os.system(IM2REC_PATH+' '+lst_file+' '+img_root_dir+' '+output_bin_file+' color=0')
代码托管于Github: https://github.com/zhzhanp/facial_expression_recognition/blob/master/FER/gather_fer.py
https://github.com/zhzhanp/facial_expression_recognition/blob/master/mxnet/data_preparation/gen_fer_bin.py