综述

爬坑一天,整出来一套还行的方案,特此分享爬坑心得~

因为整体代码结构和上篇手写数字的文章里代码结构比较相似,所以这里只贴出部分代码

 

网络结构

目前采用的两套模型是普通cnn以及vgg,效果不错,其他模型可能后续还会尝试。

CNN

paddle释放gpu paddle dropout_paddle释放gpu

# cnn
def convolutional_neural_network(image, type_size):
    # 第一个卷积--池化层
    conv_pool_1 = fluid.nets.simple_img_conv_pool(input=image,# 输入图像
                                                       filter_size=3,# 滤波器的大小
                                                       num_filters=32,# filter 的数量。它与输出的通道相同
                                                       pool_size=2,# 池化层大小2*2
                                                       pool_stride=2,# 池化层步长
                                                       act='relu') # 激活类型
    
    # Dropout主要作用是减少过拟合,随机让某些权重不更新  
    # Dropout是一种正则化技术,通过在训练过程中阻止神经元节点间的联合适应性来减少过拟合。
    # 根据给定的丢弃概率dropout随机将一些神经元输出设置为0,其他的仍保持不变。
    drop = fluid.layers.dropout(x=conv_pool_1, dropout_prob=0.5)
    
    # 第二个卷积--池化层
    conv_pool_2 = fluid.nets.simple_img_conv_pool(input=drop,
                                                       filter_size=3,
                                                       num_filters=64,
                                                       pool_size=2,
                                                       pool_stride=2,
                                                       act='relu')
    # 减少过拟合,随机让某些权重不更新                                                   
    drop = fluid.layers.dropout(x=conv_pool_2, dropout_prob=0.5)
    
    # 第三个卷积--池化层
    conv_pool_3 = fluid.nets.simple_img_conv_pool(input=drop,
                                                       filter_size=3,
                                                       num_filters=64,
                                                       pool_size=2,
                                                       pool_stride=2,
                                                       act='relu')
    # 减少过拟合,随机让某些权重不更新                                                   
    drop = fluid.layers.dropout(x=conv_pool_3, dropout_prob=0.5)
    
    # 全连接层
    fc = fluid.layers.fc(input=drop, size=512, act='relu')
    # 减少过拟合,随机让某些权重不更新                                                   
    drop =  fluid.layers.dropout(x=fc, dropout_prob=0.5)                                                   
    # 输出层 以softmax为激活函数的全连接输出层,输出层的大小为图像类别type_size个数
    predict = fluid.layers.fc(input=drop,size=type_size,act='softmax')
    
    return predict

VGG

1.首先定义了一组卷积网络,即conv_block。卷积核大小为3x3,池化窗口大小为2x2,窗口滑动大小为2,groups决定每组VGG模块是几次连续的卷积操作,dropouts指定Dropout操作的概率。所使用的img_conv_group是在paddle.networks中预定义的模块,由若干组 Conv->BN->ReLu->Dropout 和 一组 Pooling 组成。

2.五组卷积操作,即 5个conv_block。 第一、二组采用两次连续的卷积操作。第三、四、五组采用三次连续的卷积操作。每组最后一个卷积后面Dropout概率为0,即不使用Dropout操作。

3.最后接两层512维的全连接。

4.通过上面VGG网络提取高层特征,然后经过全连接层映射到类别维度大小的向量,再通过Softmax归一化得到每个类别的概率,也可称作分类器。

# vgg
def vgg_bn_drop(image, type_size):  
    def conv_block(ipt, num_filter, groups, dropouts):
        return fluid.nets.img_conv_group(
            input=ipt, # 具有[N,C,H,W]格式的输入图像
            pool_size=2,
            pool_stride=2,
            conv_num_filter=[num_filter] * groups, # 过滤器个数
            conv_filter_size=3, # 过滤器大小
            conv_act='relu',
            conv_with_batchnorm=True, # 表示在 Conv2d Layer 之后是否使用 BatchNorm
            conv_batchnorm_drop_rate=dropouts,# 表示 BatchNorm 之后的 Dropout Layer 的丢弃概率
            pool_type='max') # 最大池化

    conv1 = conv_block(image, 64, 2, [0.0, 0])
    conv2 = conv_block(conv1, 128, 2, [0.0, 0])
    conv3 = conv_block(conv2, 256, 3, [0.0, 0.0, 0])
    conv4 = conv_block(conv3, 512, 3, [0.0, 0.0, 0])
    conv5 = conv_block(conv4, 512, 3, [0.0, 0.0, 0])

    drop = fluid.layers.dropout(x=conv5, dropout_prob=0.5)
    fc1 = fluid.layers.fc(input=drop, size=512, act=None)
    
    bn = fluid.layers.batch_norm(input=fc1, act='relu')
    drop2 = fluid.layers.dropout(x=bn, dropout_prob=0.0)
    fc2 = fluid.layers.fc(input=drop2, size=512, act=None)
    predict = fluid.layers.fc(input=fc2, size=type_size, act='softmax')
    return predict

数据集

本次实验数据集的基础是paddlepaddle人脸识别公开学习项目的数据集,在此基础上作者加入了作者本人大头自拍100余张,共组成四组图片集。由此训练集训练的模型训练大概可以到70-80%左右的准确率,更多并没有进行尝试。

不过作者直接用该数据集训练得到的结果更像是对整张照片进行分类,即人脸+背景,有点偏离了实验的主题——“人脸识别”,由此决定对数据集进行增强,即在原照片的基础上把人脸抠出来作为新的数据集。

实现这一目标的手法可以有很多,比如自己实现一个网络进行人脸检测,或者直接调用第三方接口直接获取请求结果。室友某段姓玩家就采用前者用了一个自己以前写的检测网络……

作者这里采用了百度AI平台的人脸检测开放接口:http://ai.baidu.com/tech/face/detect

没有用过的同学可以查阅一下官网教程自己试一下,这里贴出关键代码:

# 调用百度AI人脸检测API用于检测人脸box,方便裁剪以增强数据集

host = 'https://aip.baidubce.com/oauth/2.0/token?grant_type=client_credentials&client_id=【你的ak】&client_secret=【你的sk】'
request = urllib.request.Request(host)
request.add_header('Content-Type', 'application/json; charset=UTF-8')
response = urllib.request.urlopen(request)
content = response.read()
content = json.loads(bytes.decode(content).strip())
access_token = content['access_token']

# 获取路径为img的人脸box
def get_face(img):
    request_url = "https://aip.baidubce.com/rest/2.0/face/v3/detect" #请求地址
    with open(img,"rb") as f:
        base64_data = base64.b64encode(f.read())
        params = {
            "image":str(base64_data,encoding='UTF8'),
            "image_type":"BASE64"
        }
        params = urllib.parse.urlencode(params).encode(encoding='UTF8')
        request_url = request_url + "?access_token=" + access_token
        request = urllib.request.Request(url=request_url, data=params)
        request.add_header('Content-Type', 'application/json')
        response = urllib.request.urlopen(request)
        content = response.read()
        content = json.loads(str(content,encoding='UTF8')) #解析请求结果
        face_location = content['result']['face_list'][0]['location']
        return(face_location)

这里封装了get_face以为指定路径的图片提供人脸box

后续调用时识别完不要忘记裁剪及保存

face_location = get_face(name_path)
# (left, upper, right, lower)
box = (face_location['left'],face_location['top'],face_location['left']+face_location['width'],face_location['top']+face_location['height'])
img = img.crop(box)
img.save(face_path) #保存裁剪完的人脸图片

为了制作数据集这里每10张图片取1张图片塞进测试集,其余塞进训练集

if class_sum % 10 == 0:   # 每10张图片取一个做测试数据
    test_sum += 1                                       #test_sum测试数据的数目
    with open(data_list_path + "test.list", 'a') as f:
        f.write(face_path + "\t%d" % class_label + "\n") #class_label 标签:0,1,2,3
else:
    trainer_sum += 1        #trainer_sum测试数据的数目
    with open(data_list_path + "trainer.list", 'a') as f:
        f.write(face_path + "\t%d" % class_label + "\n") #class_label 标签:0,1,2,3
class_sum += 1                                          #每类图片的数目
all_class_images += 1                                   #所有类图片的数目

 最终人脸裁剪完的数据集部分效果如下:

paddle释放gpu paddle dropout_神经网络_02

结果

增强数据集后cnn的效果可以得到明显提升

不过vgg的效果并不尽如人意,同样的训练轮数下表现的远不如cnn理想,有待调参或使用其他增强数据集的手段

 

爬坑指南

其实还是有一些可能会浪费很多时间的坑,举几个例子:

将自己的照片做进数据集中再上传、解压、使用,整个过程中大家一定要注意路径(如果你并不想改原代码的一些路径的话)

苹果用户拍出来的照片格式为heic,即使用一些网盘之类的工具传输到win本后格式变成了jpg,实际上也是打不开的,编码格式根本不是jpg,所以也就导致代码会抛出异常,一些函数并不能处理这种图片。建议使用微信传输一下,再右键保存,微信传图可以自动把heic转换为正确的jpg。

此外百度AI开放平台有些教程描述的其实是不太清晰的,这里贴出了请求的代码可以帮助大家少走弯路,中间涉及json封装、base64转换、response里正确关键字的提取等官方文档并没有做详细说明的坑点。

以及数据集里有一张图片是gif,强烈建议大家用函数转换为其他格式图片或者直接忽略掉这张,因为代码里的paddle.dataset.image.simple_transform函数并不支持对gif格式的动图进行处理。这个巨坑浪费了我四个小时debug!!!(不过我现在并不太想的明白为什么修改数据集前不会导致这个问题)

其他并没有什么特别的地方,基本上大家完成解压后都可以直接把fork的项目顺利的跑起来。这里基本是完成一些工作后简单写的一篇博客,如果有精力的话还可以有大量工作可以做,比如尝试更多的模型、调参以及对数据集进一步增强等~