一、前期准备
1.PaddlePaddle-gpu安装
如果是在windows上安装,需要注意以下几点:
(1)win7系统上PaddlePaddle最高只支持CUDA 8.0,可点击这里前往官网下载CUDA,以及点击这里官网下载cuDNN(或者前往百度网盘下载CUDA 8.0和相应的cuDNN)。
(2)win10家庭版无法使用PaddlePaddle-gpu,虽然可以通过pip install命令正常安装PaddlePaddle,但是无法使用,下图官方文档给的说明中不包含有win10家庭版。
(3)如果是win10专业版或企业版安装CUDA,推荐安装CUDA 9.0,这里小编亲测可用,点击这里前往下载win10系统的CUDA 9.0和相应的cuDNN吧。最后关于CUDA的安装过程很简单,运行安装程序默认安装就行,然后把cuDNN中的库放到CUDA对应的安装目录下。
安装paddlepaddle-gpu时注意,需要以管理员权限运行cmd,然后执行pip install paddlepaddle-gpu下载安装即可,如果想指定安装的版本,则执行pip install paddlepaddle-gpu==x.x.x(现有的paddlepaddle-gpu versions如下: 1.3.0, 1.3.1, 1.3.2, 1.4.0, 1.4.1, 1.5.0.post87, 1.5.0.post97, 1.5.1.post87, 1.5.1.post97)。
2.CK+数据集下载
官网下载链接请点这里:http://www.consortium.ri.cmu.edu/ckagree/
不过需要提交一些信息,邮箱很重要,因为它会把下载链接、用户名和口令发送到你的邮箱。
当然,为了给看到此文的小伙伴们提供便利,我已经将CK+数据集上传至了百度网盘,点击这里前往网盘下载CK+数据集吧。
二、CK+数据集的预处理
1.数据集结构介绍
首先来看一下CK+数据集的文件,主要包含cohn-kanade-images.zip和Emotion_labels.zip这两部分,这两个大文件夹里面的子文件夹是相同的(如下图所示),其实每个子文件夹(如’S005’)是表示一个人的表情图片,因此该数据集是采集了123人的表情图片。
然后进入第一层子文件夹(比如’S014’)中,可以看到其包含有若干个子文件夹(’001’ ~ ‘006’不等),再进入第二层子文件(比如’001’)中,可以看到里面是一个人所做的若干表情图片,这些图片连续起来其实是这个人做这个表情从平静到激动的变化过程。同样Emotion_labels文件夹中也是这样的结构,只是最里层的是存储标签值的txt文件,这里提醒下原始的标签值是1~7。
2. 表情图片以及标签的读取
源代码如下所示,首先是获取二级子目录的列表,得到这样的list: [‘S005\001’, ‘S010\001’, ‘S010\002’, …];然后就是遍历子目录去获取表情图片和对应的标签文件;最后将表情数据和标签值绑定在一起得到这样的list: [[data1, label1], [data2, label2], …]。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28# 获取子目录列表, like ['S005\001', 'S010\001', 'S010\002', ...]
dir_list = []
for root, dirs, _ in os.walk(image_dir):
    for rdir in dirs:
        for _, sub_dirs, _ in os.walk(root + '\\' + rdir):
            for sub_dir in sub_dirs:
                dir_list.append(rdir + '\\' + sub_dir)
            break
    break
# like [[data1, label1], [data2, label2], ...]
data_label = []
# 遍历子目录获取文件
for path in dir_list:
    # 处理 images
    for root, _, files in os.walk(image_dir + '\\' + path):
        for i in range(0, len(files)):
            if files[i].split('.')[1] == 'png':
                # 裁剪图片,并将其转为数据矩阵
                img_data = image_to_matrix(root + '\\' + files[i])
                # 处理相应的 label
                for lroot, _, lfiles in os.walk(label_dir + '\\' + path):
                    if len(lfiles) > 0:  # picture has label
                        label = get_label(lroot + '\\' + lfiles[0])
                        data_label.append([img_data, label])
                    break
        break
3. 表情图片的处理
首先是进行脸部中心区域的检测,这里使用了opencv进行人脸识别;其次就是进行图片灰度化,然后对识别出的脸部中心区域进行裁剪,实现的代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25# 裁剪人脸部分
def image_cut(file_name):
    # cv2读取图片
    im = cv2.imread(file_name)
    gray = cv2.cvtColor(im, cv2.COLOR_BGR2GRAY)
    # cv2检测人脸中心区域
    faces = face_cascade.detectMultiScale(
        gray,
        scaleFactor=1.15,
        minNeighbors=5,
        minSize=(5, 5)
    )
    if len(faces) > 0:
        for (x, y, w, h) in faces:
            # PIL读取图片
            img = Image.open(file_name)
            # 转换为灰度图片
            img = img.convert("L")
            # 裁剪人脸核心部分
            crop = img.crop((x, y, x + w, y + h))
            # 缩小为120*120
            crop = crop.resize((purpose_size, purpose_size))
            return crop
    return None
裁剪完成之后,还需要把图片转换为数据矩阵,实现的方法如下所示,需要注意的是这里把数据进行归一化到0~1之间,为的是让后面模型训练的时候能够尽快提取到特征,从而加速模型的收敛。1
2
3
4
5
6
7# 将图片转换为数据矩阵
def image_to_matrix(filename):
    # 裁剪并缩小
    img = image_cut(filename)
    data = img.getdata()
    # 归一化到(0,1)
    return np.array(data, dtype=float) / 255.0
4. 标签值的处理
标签值的处理比较简单,首先就是读取txt文件中的标签值,然后就是将原始的标签值1-7转换为0-6,因为多元分类问题标签一般都是从0开始,这也符合平时对list、set等元素的索引顺序。1
2
3
4
5
6
7
8
9# 获取文件中的label值
def get_label(file_name):
    f = open(file_name, 'r+')
    line = f.readline()  # only one row
    line_data = line.split(' ')
    label = float(line_data[3])
    f.close()
    # 1-7 的标签值转为 0-6
    return int(label) - 1
5. 数据的保存
将第1步中得到的data_label(形如 [[data1, label1], [data2, label2], …] ,list类型)保存为pickle文件,因为pickle本身是用来存大量的数据的,而且通过pickle文件读取数据的效率比较高。1
2
3
4
5# 写入数据到pkl文件
pkl_file = '../data/data_label_list_120.pkl'
with open(pkl_file, 'wb') as f:
    pickle.dump(data_label, f)
    f.close()
三、构建神经网络模型
该神经网络模型通过paddlepaddle中的fluid来实现,主要包含两个卷积层、两个池化层、一个全连接层和一个softmax输出层,具体的实现代码如下。关于每个层的参数在代码中都有注释介绍,就不赘述了。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32def convolutional_neural_network(img):
    """
    定义卷积神经网络分类器:
        输入的二维图像,经过两个卷积-池化层,一个全连接层,使用以softmax为激活函数的输出层
    Return:
        prediction -- 分类的结果
    """
    # 第一个卷积-池化层
    # 使用32个5*5的滤波器,池化大小为3,池化步长为2,激活函数为Relu
    conv_pool_1 = fluid.nets.simple_img_conv_pool(
        input=img,
        filter_size=5,
        num_filters=32,
        pool_size=3,
        pool_stride=2,
        act='relu')
    conv_pool_1 = fluid.layers.batch_norm(conv_pool_1)
    # 第二个卷积-池化层
    # 使用64个5*5的滤波器,池化大小为3,池化步长为2,激活函数为Relu
    conv_pool_2 = fluid.nets.simple_img_conv_pool(
        input=conv_pool_1,
        filter_size=5,
        num_filters=64,
        pool_size=3,
        pool_stride=2,
        act='relu')
    # 全连接层,输出为512维特征
    fc1 = fluid.layers.fc(input=conv_pool_2, size=512, act=None)
    # 以softmax为激活函数的全连接输出层,输出层的大小必须为类别数7
    prediction = fluid.layers.fc(input=fc1, size=7, act='softmax')
    return prediction
四、模型的训练和测试
1. 数据的读取
主要就是读取之前保存好的pickle文件,然后将所有数据打乱后按照8:2来划分训练集和测试集,读取pickle文件借助的是pandas.read_pickle()方法,读出来后的数据类型与存储的一致,即依然是最初的data_label(list类型)。1
2
3
4
5
6# 读取pkl文件的数据
def read_data(file_name):
    data = pd.read_pickle(file_name)
    # 随机打乱后划分数据集,测试集占0.2
    train_data, test_data = train_test_split(data, shuffle=True, test_size=0.2, random_state=42)
    return train_data, test_data
2. batch数据的生成
为了提高程序执行的效率,这里生成batch数据借助了yield函数,它能够返回一个generator迭代器,不仅能够降低数据对内存的消耗,还能够加速程序的运行。1
2
3
4
5
6
7
8
9
10
11def generator_batches(x, bsize=50):
    """
    generator batch data from x
    :param x: list x, like [[train_data, train_label], ...]
    :param bsize: batch size
    :return: generator batch data
    """
    n = len(x) // bsize
    x = x[:n * bsize]
    for i in range(0, len(x), bsize):
        yield x[i:i + bsize]
3. 配置训练程序
训练程序主要是调用自定义的神经网络来训练,得到预测的结果,其中主要是使用交叉熵作为损失函数,最后返回的预测结果、平均损失和正确率这些信息。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17def train_program(img, label):
    """
    配置train_program
    Return:
        prediction -- 分类的结果
        avg_cost -- 平均损失
        acc -- 分类的准确率
    """
    prediction = convolutional_neural_network(img)  # 使用自定义的卷积神经网络
    # 使用类交叉熵函数计算predict和label之间的损失函数
    cost = fluid.layers.cross_entropy(input=prediction, label=label)
    # 计算平均损失
    avg_cost = fluid.layers.mean(cost)
    # 计算分类准确率
    acc = fluid.layers.accuracy(input=prediction, label=label)
    return prediction, [avg_cost, acc]
4. 构建测试程序
测试程序主要是在评估训练的模型的好坏,并返回模型在测试集上的好坏评估指标(平均损失值和平均准确率),神经网络中一般需要在模型训练的过程中进行多轮测试。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19# 训练过程中测试模型的好坏
def train_test(test_program, feeder, test_data, batch_size):
    # 将分类准确率存储在acc_set中
    acc_set = []
    # 将平均损失存储在avg_loss_set中
    avg_loss_set = []
    # 将测试集 yield 出的每一个数据传入网络中进行训练
    for data in generator_batches(test_data, batch_size):
        acc_np, avg_loss_np = exe.run(
            program=test_program,
            feed=feeder.feed(data),
            fetch_list=[acc, avg_loss])
        acc_set.append(float(acc_np))
        avg_loss_set.append(float(avg_loss_np))
    # 获得测试数据上的准确率和损失值
    acc_val_mean = np.array(acc_set).mean()
    avg_loss_val_mean = np.array(avg_loss_set).mean()
    # 返回平均损失值,平均准确率
    return avg_loss_val_mean, acc_val_mean
五、模型的运行、保存和加载
1. 主运行程序和模型的保存
主程序的功能就是读取数据,定义神经网络的输入和输出、一些超参数(epochs、batch_size、learning_rate等)、fluid的执行器等,以及设置模型的训练过程、保存模型等。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68if __name__ == '__main__':
    pkl_file = '../data/data_label_list_120.pkl'
    train_data, test_data = read_data(pkl_file)
    # 一个minibatch中有64个数据
    batch_size = 52
    # 该模型运行在CPU上
    use_cuda = True  # 如想使用GPU,请设置为 True
    place = fluid.CUDAPlace(0) if use_cuda else fluid.CPUPlace()
    # 创建执行器
    exe = fluid.Executor(place)
    # 输入的原始图像数据,大小为120*120*1
    img = fluid.layers.data(name='img', shape=[1, image_size, image_size], dtype='float32')
    # 标签层,名称为label,对应输入图片的类别标签
    label = fluid.layers.data(name='label', shape=[1], dtype='int64')
    # 告知网络传入的数据分为两部分,第一部分是img值,第二部分是label值
    feeder = fluid.DataFeeder(feed_list=[img, label], place=place)
    # 调用train_program 获取预测值,损失值,
    prediction, [avg_loss, acc] = train_program(img, label)
    # 选择Adam优化器
    optimizer = fluid.optimizer.Adam(learning_rate=0.001)
    optimizer.minimize(avg_loss)
    # 训练的轮数
    epochs = 10
    # 将模型参数存储在名为 save_dirname 的文件中
    save_dirname = '../model/cnn_paddle.model'
    # 设置 main_program 和 test_program
    main_program = fluid.default_main_program()
    test_program = fluid.default_main_program().clone(for_test=True)
    exe.run(fluid.default_startup_program())
    # 开始训练
    lists = []
    step = 0
    for epoch_id in range(0, epochs):
        for data in generator_batches(train_data, batch_size):
            metrics = exe.run(main_program,
                              feed=feeder.feed(data),
                              fetch_list=[avg_loss, acc])
            if step % 50 == 0:  # 每训练50次 打印一次log
                print("Pass %d, Batch %d, Cost %f" % (step, epoch_id, metrics[0]))
            step += 1
        # 测试每个epoch的分类效果
        avg_loss_val, acc_val = train_test(test_program, feeder, test_data, batch_size)
        print("Test with Epoch %d, avg_cost: %s, acc: %s" % (epoch_id, avg_loss_val, acc_val))
        lists.append((epoch_id, avg_loss_val, acc_val))
        # 保存训练好的模型参数用于预测
        if save_dirname is not None:
            fluid.io.save_inference_model(save_dirname,
                                          ['img'], [prediction], exe,
                                          model_filename=None,
                                          params_filename=None)
    # 选择效果最好的pass
    best = sorted(lists, key=lambda lt: float(lt[1]))[0]
    print('Best pass is %s, testing Avgcost is %s' % (best[0], best[1]))
    print('The classification accuracy is %.2f%%' % (float(best[2]) * 100))
    # 训练完成后可进行预测
    # predict()
    print('\n--------------------------Program Finished---------------------------\n')
2. 加载模型和预测
当训练好的模型保存成功后,调用该函数能够加载保存好的模型,然后对输入的表情图片进行预测。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33def predict(save_dirname, image_file):
    """
    加载保存的模型进行预测
    :param save_dirname: 模型保存的路径
    :param image_file: 要预测的表情图片路径
    :return:
    """
    img_data = image_to_matrix(image_file)
    # 转为输入所需的大小,注意需要转换为float32类型
    img_data = img_data.reshape((1, 1, image_size, image_size)).astype(np.float32)
    use_cuda = False  # 如想使用GPU,请设置为 True
    place = fluid.CUDAPlace(0) if use_cuda else fluid.CPUPlace()
    # 创建执行器
    exe = fluid.Executor(place)
    exe.run(fluid.default_startup_program())
    inference_scope = fluid.core.Scope()
    with fluid.scope_guard(inference_scope):
        # 使用 fluid.io.load_inference_model 获取 inference program desc,
        # feed_target_names 用于指定需要传入网络的变量名
        # fetch_targets 指定希望从网络中fetch出的变量名
        [inference_program, feed_target_names,
         fetch_targets] = fluid.io.load_inference_model(
            save_dirname, exe, None, None)
        # 将feed构建成字典 {feed_target_name: feed_target_data}
        # 结果将包含一个与fetch_targets对应的数据列表
        results = exe.run(inference_program,
                          feed={feed_target_names[0]: img_data},
                          fetch_list=fetch_targets)
        lab = np.argsort(results)
        # 打印图片的预测结果,结果 +1 转为最初的 1-7
        print('Inference result of ' + image_file + ' is: %d' % (lab[0][0][-1] + 1))
六、 Github链接
本博文中讲述的所有代码,以及整个项目代码都已在github上公开,欢迎大家下载学习,点击这里前往github仓库。如果你觉得这篇博客不错,请记得点赞哦~ 当然如果有任何问题,也欢迎大家提问,感谢您的阅读!

...
...