@作者: 研发中心算法平台 @迪吉老农
假冒大V的头像识别
一. 问题的定义
热门微博的评论中出现了一批假冒名人头像的用户,在头像右下角仿造了一个V字图形。如图,
热门微博的产品希望通过图片识别这些用户。
二. 基于相似图片的方法
2.1. 算法
第一版模型,是基于相似图片搜索。步骤如下,
- 首先选出来一些V字图片作为模板,提取图片的指纹特征。
- 对于给定的一张要判定的图片,按像素和不同放大比例来扫描,如果发现和模板中的指纹相似性较高,则认为含有V字形在图片里面。
图片指纹的计算,参考的是图片搜索引擎这篇文章。简单来说,对于一张给定的方块图,我们把它按照下图所示分成5个区块,
每个区块计算HSV(Hue, Saturation, Value)三维颜色空间的直方图分布,例如,
将这个分布的形状作为图片的指纹特征。通过比较每个区块中的颜色分布与模板的颜色分布是否相似(计算$\chi^2$距离),来判定是否含有大V图标。
2.2. 效果
我们手工收集了15张含有V字的图片,将V字部分抠出来,计算指纹作为模板保存起来。
通过上面的算法,成功识别了大V图形的部分。如下图所示,
算法的计算时间比较长,因为要考虑不同位置和不同缩放比例。平均完成一整张图的扫描的时间为1s。
当我们把这个算法应用到100万个测试用的头像集的时候,更严重的问题出现了:误判。很多正常的图片(比如一些纯色图、漫画)都被错误地识别成大V了。我们试图通过调整直方图的划分方式来过滤误伤,但是,新的误伤类型又会产生,此消彼长。一些误判的例子如下,
最终,从100万张测试图像中扫出300张图,竟然没有一张是大V图片,100%都是误判。我们意识到,大V图片的在总比例是相当低的,需要更为精确的算法才能避免误伤。
三. 卷积神经网络
之前的算法是,采用直方图提取特征值,用扫描和缩放来定位。我们考虑,这两步可以统一到卷积神经网络的框架中,特征可以提取得更细腻,扫描和缩放也可以通过卷积和池化的操作来实现,避免了扫描带来的大量重复计算,可以把精度和速度都提升一个量级。卷积神经网络应用于手写识别已经很成功,解决类似的问题,我们猜测只需要一个简单的网络就可以了。只是难点在于,没有训练数据。
3.1. 数据生成
构建深度网络模型需要大量的标注数据,这在我们的项目中是无法获得的,只能我们人为构造数据集,但是需要加入足够丰富的扰动项,以避免模型抓住了单一维度,过拟合。
数据的生成方式如下,
- 首先,从之前的模板中将V字图形抠出。
- 然后,对V字图形进行一定的随机扰动,包括
- 一些photoshop中的手动操作(比如增加水波纹理,灰色图,特效等)
- 机器自动生成的缩放、旋转、色阶移动等。
- 将扰动后的大V附加到原图右下角的位置上。
- 模仿之前的假大V模式,按一定比例地用圆圈切除四个边角,用灰白色填充空白。
生成器生成的负样本如下(只显示了右下角),
正样本如下(只显示右下角),
最终,读取一个大概60000张的非大V的头像图库,把按照1:1的比例持续输出正负样本给模型进行训练。
3.2. 网络结构和训练参数
之前很多次用卷积神经网络,但是没有自己写过模型,都是直接用前人写好的网络结构跑一下。很多时候,好的结果自动就出来了,也没有深究每一步的原因。这次干脆从零开始写,实验了一下各种模型结构。
最后发现,如果随便弄上去一个模型,也许可以都达到差不多99%的准确率;但是,这样的准确率并不够,1%的误差意味着,一百万用户就误伤1万用户。我们要看哪个模型可以达到99.90%,甚至接近99.99%,才能应对这种正负样本偏差极大的情况。
3.2.1. LeNet结构
开始试验的是类似LeNet的结构,基本结构是Convolution2D + MaxPooling,然后不断循环加深。
Layer (type) Output Shape Param #
=================================================================
image_input (InputLayer) (None, 90, 90, 3) 0
_________________________________________________________________
conv2d_1 (Conv2D) (None, 90, 90, 8) 224
_________________________________________________________________
batch_normalization_1 (Batch (None, 90, 90, 8) 32
_________________________________________________________________
activation_1 (Activation) (None, 90, 90, 8) 0
_________________________________________________________________
max_pooling2d_1 (MaxPooling2 (None, 45, 45, 8) 0
_________________________________________________________________
conv2d_2 (Conv2D) (None, 45, 45, 8) 584
_________________________________________________________________
batch_normalization_2 (Batch (None, 45, 45, 8) 32
_________________________________________________________________
activation_2 (Activation) (None, 45, 45, 8) 0
_________________________________________________________________
max_pooling2d_2 (MaxPooling2 (None, 22, 22, 8) 0
_________________________________________________________________
conv2d_3 (Conv2D) (None, 22, 22, 16) 1168
_________________________________________________________________
batch_normalization_3 (Batch (None, 22, 22, 16) 64
_________________________________________________________________
activation_3 (Activation) (None, 22, 22, 16) 0
_________________________________________________________________
max_pooling2d_3 (MaxPooling2 (None, 11, 11, 16) 0
_________________________________________________________________
conv2d_4 (Conv2D) (None, 11, 11, 32) 4640
_________________________________________________________________
batch_normalization_4 (Batch (None, 11, 11, 32) 128
_________________________________________________________________
activation_4 (Activation) (None, 11, 11, 32) 0
_________________________________________________________________
max_pooling2d_4 (MaxPooling2 (None, 5, 5, 32) 0
_________________________________________________________________
flatten_1 (Flatten) (None, 800) 0
_________________________________________________________________
dense_1 (Dense) (None, 16) 12816
_________________________________________________________________
batch_normalization_5 (Batch (None, 16) 64
_________________________________________________________________
activation_5 (Activation) (None, 16) 0
_________________________________________________________________
output (Dense) (None, 2) 34
=================================================================
Total params: 19,786
Trainable params: 19,626
Non-trainable params: 160
这个模型在测试集上的效果如下,感觉对V字的识别能力还是不错的,误伤在30%以上,略大了一些。
Precision-recall at threshold 0.500000
precision recall f1-score support
0 0.99 0.89 0.94 373
1 0.76 0.97 0.85 131
avg / total 0.93 0.91 0.91 504
Precision-recall at threshold 0.700000
precision recall f1-score support
0 0.99 0.88 0.93 373
1 0.80 0.95 0.87 131
avg / total 0.94 0.90 0.92 504
Precision-recall at threshold 0.800000
precision recall f1-score support
0 0.99 0.86 0.92 373
1 0.83 0.95 0.88 131
avg / total 0.95 0.88 0.91 504
Precision-recall at threshold 0.900000
precision recall f1-score support
0 0.99 0.83 0.90 373
1 0.87 0.94 0.90 131
avg / total 0.96 0.86 0.90 504
Precision-recall at threshold 0.950000
precision recall f1-score support
0 0.99 0.82 0.89 373
1 0.89 0.94 0.91 131
avg / total 0.96 0.85 0.90 504
AUC score:
0.983238851483
confusion_matrix
[332 41] (0) 0
[ 4 127] (1) 1
(0) (1)
3.2.2 全卷积的LeNet结构
LeNet结构的模型,经过Flat层以后,变成了800,全链接层带来了比较多的参数,有过拟合的风险,于是尝试用GlobalMaxPooling替代Flat层和隐层Dense(16)。
Layer (type) Output Shape Param #
=================================================================
image_input (InputLayer) (None, 90, 90, 3) 0
_________________________________________________________________
conv2d_1 (Conv2D) (None, 90, 90, 8) 224
_________________________________________________________________
batch_normalization_1 (Batch (None, 90, 90, 8) 32
_________________________________________________________________
activation_1 (Activation) (None, 90, 90, 8) 0
_________________________________________________________________
max_pooling2d_1 (MaxPooling2 (None, 45, 45, 8) 0
_________________________________________________________________
conv2d_2 (Conv2D) (None, 45, 45, 8) 584
_________________________________________________________________
batch_normalization_2 (Batch (None, 45, 45, 8) 32
_________________________________________________________________
activation_2 (Activation) (None, 45, 45, 8) 0
_________________________________________________________________
max_pooling2d_2 (MaxPooling2 (None, 22, 22, 8) 0
_________________________________________________________________
conv2d_3 (Conv2D) (None, 22, 22, 16) 1168
_________________________________________________________________
batch_normalization_3 (Batch (None, 22, 22, 16) 64
_________________________________________________________________
activation_3 (Activation) (None, 22, 22, 16) 0
_________________________________________________________________
max_pooling2d_3 (MaxPooling2 (None, 11, 11, 16) 0
_________________________________________________________________
conv2d_4 (Conv2D) (None, 11, 11, 32) 12832
_________________________________________________________________
batch_normalization_4 (Batch (None, 11, 11, 32) 128
_________________________________________________________________
activation_4 (Activation) (None, 11, 11, 32) 0
_________________________________________________________________
max_pooling2d_4 (MaxPooling2 (None, 5, 5, 32) 0
_________________________________________________________________
global_max_pooling2d_1 (Glob (None, 32) 0
_________________________________________________________________
dropout_1 (Dropout) (None, 32) 0
_________________________________________________________________
output (Dense) (None, 2) 66
=================================================================
Total params: 15,130
Trainable params: 15,002
Non-trainable params: 128
这个模型在测试集的效果如下,
Precision-recall at threshold 0.500000
precision recall f1-score support
0 0.95 0.94 0.94 373
1 0.82 0.85 0.84 131
avg / total 0.92 0.91 0.92 504
Precision-recall at threshold 0.700000
precision recall f1-score support
0 0.96 0.92 0.94 373
1 0.87 0.80 0.83 131
avg / total 0.93 0.89 0.91 504
Precision-recall at threshold 0.800000
precision recall f1-score support
0 0.97 0.91 0.94 373
1 0.88 0.78 0.83 131
avg / total 0.95 0.88 0.91 504
Precision-recall at threshold 0.900000
precision recall f1-score support
0 0.99 0.90 0.94 373
1 0.89 0.71 0.79 131
avg / total 0.96 0.85 0.90 504
Precision-recall at threshold 0.950000
precision recall f1-score support
0 0.99 0.88 0.93 373
1 0.91 0.64 0.75 131
avg / total 0.97 0.82 0.89 504
AUC score:
0.97599410597
confusion_matrix
[349 24] (0) 0
[ 19 112] (1) 1
(0) (1)
观察分错的例子,我们发现对于红黄交替的图片会分不清,对于圆形标记内部的V字形没有分辨能力。
我感觉模型并没有学习到V的形状,对于含有V字的变体,没有任何识别能力。
为了解决这个问题,我曾一度尝试在网络后层加深,或者增加卷积的输出频道个数;但是除了模型过拟合变的更严重外,效果没有改进。
3.2.3. 网络结构分析
在之前的建模中,我一直有一个误区:注意到V字图形的边长是右下角$\frac{1}{3}$左右,于是希望卷积后的图片大小能越来越小,到最后一层能到$5\times5$或$3\times3$,以为这样以后每个点控制的就是$\frac{1}{5}$或者$\frac{1}{3}$的图像区域,就可以覆盖到完整的V字。实际上这样理解卷积是错误的。
实际上应该怎么计算,我们看下图,
我们先看左边的卷积。如果上层每个结点覆盖了$c_n$个像素点,结点之间的步长跨度为$step_n$像素,那么一个步长为$k$的卷积生成的新结点,将覆盖像素点个数为
$$
c_{n+1} = c_n + (k-1)\cdot step_n
$$
再看右边的池化层。池化层的作用是修改了结点之间步长跨度的像素,即
$$
step_{n+1} = 2 step_n
$$
拿之前的两层网络分析一下,
Layer (type) Output Shape Param #
=================================================================
image_input (InputLayer) (None, 90, 90, 3) 0
_________________________________________________________________
conv2d_1 (Conv2D) (None, 90, 90, 8) 224
_________________________________________________________________
max_pooling2d_1 (MaxPooling2 (None, 45, 45, 8) 0
_________________________________________________________________
conv2d_2 (Conv2D) (None, 45, 45, 8) 584
_________________________________________________________________
max_pooling2d_2 (MaxPooling2 (None, 22, 22, 8) 0
______________________________________________________________
conv2d_3 (Conv2D) (None, 22, 22, 16) 1168
_________________________________________________________________
activation_3 (Activation) (None, 22, 22, 16) 0
_________________________________________________________________
max_pooling2d_3 (MaxPooling2 (None, 11, 11, 16) 0
_________________________________________________________________
conv2d_4 (Conv2D) (None, 11, 11, 32) 12832
_________________________________________________________________
- 第1层卷积,核是3,步长是1个像素,每个上层结点对应1个像素,所以每个下层结点覆盖像素点个数为$1 + (3-1) \cdot 1 = 3$
- 第1层池化,将2个像素点合成一个步长。
- 第2层卷积,核是3,步长是2个像素,每个上层结点对应3个像素,所以下层结点覆盖$3 + (3-1)\cdot2 = 7$
- 第2层池化,将4个像素点合成一个步长。
- 第3层卷积,核是3,步长是4个像素,下层结点覆盖$7 + (3-1)\cdot 4 = 15$
- 第3层池化,将8个像素点合成一个步长。
- 第4层卷积,核是3,步长是8个像素,下层结点覆盖$15 + (3-1)\cdot 8 = 31$
我们看到,经过这些步骤以后,图像抽象到了$11\times11$个结点,每个结点覆盖31个像素点。已经达到了我们的预期的V字图片的大小,如果再做一次卷积,将结点个数缩小到$5\times5$,那么每个点将覆盖$31 + (3-1)\cdot16 = 63$了,已经够大了,并不是之前想的,只覆盖了$90 \times \frac{1}{5} = 16$的像素点。
3.2.4. VGG结构
最终选用VGG类似的结构,主要的结构模式是把两次$3\times3$的卷积叠在一起,再做池化。相当于增加了抽象的程度。
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
image_input (InputLayer) (None, 90, 90, 3) 0
_________________________________________________________________
conv2d_1 (Conv2D) (None, 90, 90, 16) 448 # 1 + 2 * 1 = 3
_________________________________________________________________
conv2d_2 (Conv2D) (None, 90, 90, 16) 2320 # 3 + 2 * 1 = 5
_________________________________________________________________
batch_normalization_1 (Batch (None, 90, 90, 16) 64
_________________________________________________________________
activation_1 (Activation) (None, 90, 90, 16) 0
_________________________________________________________________
max_pooling2d_1 (MaxPooling2 (None, 45, 45, 16) 0 # step = 2
_________________________________________________________________
conv2d_3 (Conv2D) (None, 45, 45, 16) 2320 # 5 + 2 * 2 = 9
_________________________________________________________________
conv2d_4 (Conv2D) (None, 45, 45, 16) 2320 # 9 + 2 * 2 = 13
_________________________________________________________________
batch_normalization_2 (Batch (None, 45, 45, 16) 64
_________________________________________________________________
activation_2 (Activation) (None, 45, 45, 16) 0
_________________________________________________________________
max_pooling2d_2 (MaxPooling2 (None, 22, 22, 16) 0 # step = 4
_________________________________________________________________
conv2d_5 (Conv2D) (None, 22, 22, 32) 4640 # 13 + 2 * 4 = 21
_________________________________________________________________
conv2d_6 (Conv2D) (None, 22, 22, 32) 9248 # 21 + 2 * 4 = 29
_________________________________________________________________
batch_normalization_3 (Batch (None, 22, 22, 32) 128
_________________________________________________________________
activation_3 (Activation) (None, 22, 22, 32) 0
_________________________________________________________________
max_pooling2d_3 (MaxPooling2 (None, 11, 11, 32) 0 # step = 8
_________________________________________________________________
conv2d_7 (Conv2D) (None, 11, 11, 48) 13872 # 29 + 2 * 8 = 45
_________________________________________________________________
conv2d_8 (Conv2D) (None, 11, 11, 48) 20784 # 45 + 2 * 8 = 61
_________________________________________________________________
batch_normalization_4 (Batch (None, 11, 11, 48) 192
_________________________________________________________________
activation_4 (Activation) (None, 11, 11, 48) 0
_________________________________________________________________
global_max_pooling2d_1 (Glob (None, 48) 0
_________________________________________________________________
dropout_1 (Dropout) (None, 48) 0
_________________________________________________________________
output (Dense) (None, 2) 98
=================================================================
Total params: 56,498
Trainable params: 56,274
Non-trainable params: 224
最后每个点覆盖的像素点个数,可以按照上表中的注释部分来计算,得到最终$11\times11$个结点,每个结点覆盖61个像素,比较好的可以覆盖各种尺寸的V字图标。
我们将之前模型预测错误的样本加入模型,不断迭代训练。这过程不仅包含增加训练集,而是在测试集和验证集中也都增加了一批模型分错的图片。目前的效果是,
classification report:
Precision-recall at threshold 0.500000
precision recall f1-score support
0 1.00 0.99 0.99 6820
1 0.94 0.99 0.97 1124
avg / total 0.99 0.99 0.99 7944
Precision-recall at threshold 0.700000
precision recall f1-score support
0 1.00 0.98 0.99 6820
1 0.96 0.99 0.97 1124
avg / total 0.99 0.98 0.99 7944
Precision-recall at threshold 0.800000
precision recall f1-score support
0 1.00 0.98 0.99 6820
1 0.97 0.99 0.98 1124
avg / total 0.99 0.98 0.99 7944
Precision-recall at threshold 0.900000
precision recall f1-score support
0 1.00 0.97 0.98 6820
1 0.97 0.98 0.98 1124
avg / total 1.00 0.97 0.98 7944
Precision-recall at threshold 0.950000
precision recall f1-score support
0 1.00 0.96 0.98 6820
1 0.98 0.98 0.98 1124
avg / total 1.00 0.96 0.98 7944
AUC score:
0.998549835109
confusion_matrix
[6747 73] (0) 0
[ 6 1118] (1) 1
(0) (1)
3.3. 实际效果
几轮迭代以后,0.9分数下实际分错的比例从90%降低到10%以内,召回保持在90%以上。
四. 持续训练架构
我将模型的训练抽象出来,方便模型持续接受新的输入来迭代。
- 启动
vreco_train_booter.sh
,程序会每隔30秒搜索一次log/start.pid
存在与否,判断是否启动训练。 - 将新增的图片上传到
upload
文件夹中对应的类目中,比如将模型分错的图片传到upload/train/0/
目录下,将含有大V标识的图片传到upload/train/1/
目录下。 - 在
log
目录下新建start.pid
来通知vreco_train_booter.sh
启动模型训练。 - 启动模型后,程序将
upload
目录下的图片合并到stage
目录下。 - 训练时间大概是不到一个小时,平台会生成一个版本编号,生成这个版本对应的结果日志,保存到
backup/result/
目录下。模型文件保存到backup/model/
。最后会将这次训练用到的数据stage
打包放在backup/data/
目录中。所有这些都会以版本号来区分,例如,
├── data
│ ├── stage_20170516-154945.tar.gz
│ ├── stage_20170516-163435.tar.gz
│ └── stage_20170516-174022.tar.gz
├── model
│ ├── cnn_20170516-154945.pkl
│ ├── cnn_20170516-154945.pkl.hdf5
│ ├── cnn_20170516-163435.pkl
│ ├── cnn_20170516-163435.pkl.hdf5
│ ├── cnn_20170516-174022.pkl
│ ├── cnn_20170516-174022.pkl.hdf5
│ └── nn_checkpoint.hdf5
└── result
├── result_20170516-154945
├── result_20170516-163435
└── result_20170516-174022
- 训练结束后会通过邮件,把结果result发送到指定的邮箱中。
我们可以通过报告来查看是否新的模型有合理的表现,是否需要上线等。如果需要恢复上一次版本的数据重新训练,可以从backup/data/
中找到对应的stage
解压回去。
注意,GPU内存需要保持将近3G空余用来训练,否则会报错,发送带有错误标题的空邮件报告。
版权声明
以上文章为本人@迪吉老农原创,首发于简书,文责自负。文中如有引用他人内容的部分(包括文字或图片),均已明文指出,或做出明确的引用标记。如需转载,请联系作者,并取得作者的明示同意。感谢。