漫画-人脸识别:基于 SphereFace & CBAM 的一点改进

人脸解译课程期末总结。在Sphere Face的基础上加以改进的漫画-人脸识别方法。本篇主要记录学习过程中遇到的困难和解决的过程,以及一些学习心得。上一篇关于PyTorch的👉PyTorch 初尝试:MNIST 手写数字识别

学习的过程 & 遇到的困难

论文研读&复现

  学期初老师给了一份,我们小组讨论后决定研究人脸漫画合成这个方向的👇这三篇。

WebCaricature: a benchmark for caricature recognition
CariGAN: Caricature Generation through Weakly Paired Adversarial Learning
WarpGAN: Automatic Caricature Generation

  一开始选定了后两篇,第二篇为主,因为觉得第二篇的思路比较好理解一些。于是便开始着手想要复现论文中的方法。最初以为Github上肯定会有代码,但是也没有找到,还天真的想过自己造轮子,进度一直卡在开头原地踏步。

开个小差:互娱-漫画人脸识别

  因为进度一直停滞不前,所以老师建议我们从简单的人脸识别开始,做出来了再走下一步漫画人脸生成。我们觉得最好能给自己找个动力,所以搜了一下最近有没有相关的比赛,结果真的找到一个——华为的互娱-漫画人脸识别,题目和我们要学的方向差不多,最终需要判断一张真人照片和一张漫画图像是否为同一个人。
  老师推荐我们用WebCaricature的数据集入手,从研究生学姐那里要了份初始代码后,我们便把网络迁移到比赛数据集上开始尝试了。
  很遗憾的是,因为时间安排不当,导致最后没有足够的时间对网络进行调试就极限操作交了跑出来的predict.csv,最后得分也不是很理想。
这是前三名
这是第十名
这是我们_(´ཀ`」 ∠)_

重整装,再启程

  已经过去了小半个学期,总结了之前的经验后我们觉得应该先从简单的入手,一点点增加难度。老师则表示,虽然比赛结束了,但我们还是可以继续完善比赛的网络,完成这个方向的研究。建议我们在WebCaricature数据集用的SphereFace基础上加一点注意力机制,比如Channel attentionSpatial attention,尝试看能不能提高预测的准确度。

添加Channel attention和Spatial attention模块

论文地址:CBAM: Convolutional Block Attention Module
论文解读:用于卷积神经网络的注意力机制(Attention)
代码地址: Convolutional-Block-Attention-Module

  找了👆这篇论文希望可以参考其中关于 CA & SA 模块的网络结构。直接把运算加在net_sphere的forward里并不可行(是的毕竟是个菜鸡,就是这么乱来_(´ཀ`」 ∠)_),所以这一步暂时是在net_sphere.py文件里直接添加了两个函数def SpatialAttentiondef ChannelAttention。这样加吧,简单的运算是可以加进去的,但是fc、conv这样的就没办法加进去(因为没有定义)。
  后来把函数改为了类Class SpatialAttentionClass ChannelAttention。这样就可以在init里写定义然后在forward里调用了。

weight无法导入,因为class不存在

  在原先的net_sphere.py里新加了两个class之后,训练代码并跑不通。解决了一些很zz的bug(比如类不存在,发现是没有import。比如NotImplementedError,查了好久发现是forward拼错了😂)后,最后卡在weight权重上了。因为新加的层用了卷积,但是因为不是原先网络自带的所以没有预训练好的权重,在读取weight的时候一直会报错,说找不到这两个类。

迁移学习的弯路

  因为一开始的思路都是在网络的尾巴上进行修改即可,fc5之前的都可以沿用原先的。所以自然而然就想到了迁移学习。
  但是在看了一堆🐝和🐜之后,我们模糊地意识到可能我们想完成的功能不是迁移学习能做到的。大部分的迁移学习,都是在最后修改全连接层的输出维度,或者换训练集。基本没有在全连接层之后再加别的卷积层的操作。

修改预训练模型的新天地

CSDN博客:PyTorch中的经典模型的修改

  在不断碰壁中遇到了👆这篇博客,感觉打开了新天地。所以我们按照文中的步骤,新建了一个net_sphere_cbam.py,在其中添加了Class SpatialAttentionClass ChannelAttention,原先的net_sphere.py保持最开始的样子不做改动。
  训练的时候把相同层预训练好的权重从原先的网络复制过来(这样新加的层就不会报错说找不到了),然后让网络在这个基础上训练新加层的权重。
  在这一步我们调整了CA的位置,把这一层挪到了较前面的位置,因为考虑到前面几层的时候通道层数还比较少,运算量小一些。

网络中间结果可视化

  将这两个模块加进去训练之后,结果并不理想。应该说,是和原先的预测概率基本一致,没有什么较大的变化。
  于是我们合理怀疑 CA & SA 模块并没有起到我们预计的集中网络注意力的效果。所以便想到能不能将网络的中间计算结果可视化输出,看看这两个模块干了什么。在一通🔎之后,我们找到的方法是在网络需要输出的层之后用一个新的变量g = x然后修改return返回值return g.pow(2).mean(1)。最开始这个步骤是直接加在net_sphere_cbam.py里的(别问,问就是菜鸡😂),显然没跑通。
  最后是新建了一个notebook,把新的网络继承过来后,加了g = xreturn了。

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
class sphere20a_cbam_vis(sphere20a_cbam):
def __init__(self, classnum=10574, feature=False):
super().__init__(classnum, feature)
def forward(self, x, target=None):

x = self.relu1_1(self.conv1_1(x))
x = x + self.relu1_3(self.conv1_3(self.relu1_2(self.conv1_2(x))))
g0 = x
x=self.ca1(x)
g1 = x

x = self.relu2_1(self.conv2_1(x))
x = x + self.relu2_3(self.conv2_3(self.relu2_2(self.conv2_2(x))))
x = x + self.relu2_5(self.conv2_5(self.relu2_4(self.conv2_4(x))))
g2 = x

x = self.relu3_1(self.conv3_1(x))
x = x + self.relu3_3(self.conv3_3(self.relu3_2(self.conv3_2(x))))
x = x + self.relu3_5(self.conv3_5(self.relu3_4(self.conv3_4(x))))
x = x + self.relu3_7(self.conv3_7(self.relu3_6(self.conv3_6(x))))
x = x + self.relu3_9(self.conv3_9(self.relu3_8(self.conv3_8(x))))
g3 = x

x = self.relu4_1(self.conv4_1(x))
x = x + self.relu4_3(self.conv4_3(self.relu4_2(self.conv4_2(x))))
g4 = x
x= self.sa(x)
g5 = x

x = x.view(x.size(0), -1)
x = self.fc5(x) # 128*512

if self.feature: # self.feature=False
return x

x = self.fc6(x)

return [g.pow(2).mean(1) for g in (g0, g1, g2, g3, g4, g5)]

可视化后的输出的结果是这样的:
原图
g0-g5的输出
  感觉CA和SA并没有什么特别大的用处……主要的还是在网络中间层,也就是原先网络自带的几层。略感挫败后我们开始寻求新的思路。

数据预处理的插曲

  因为主要想要能提高准确度,所以想到先用dlib将数据集中的图片进行剪切,把人脸单独切出来,以便于初步排除一部分干扰。第一遍运完后发现训练时候说图片不存在,ls了一下报错路径发现图片是有的。最后找出来是因为图片虽然存在但大小是0B,所以读不到。原因是crop里读图用的Image.open,后面存图片用的是cv.imsave,所以没存成功。

1
2
3
4
5
6
7
8
det = dlib.get_frontal_face_detector()
def crop_face(path):
im = Image.open(path)
im_array = np.array(im)
for face in det(im_array, 1):
l, t, r, b = face.left(), face.top(), face.right(), face.bottom()
return im.crop((l, t, r, b))
return None

添加预测模块

  原先的网络主要的工作是分类,也即给一张图片,输出这张图片最可能是谁。但是在测试的时候,我们还会有给两张图片判断是否为同一个人的测试。按这个思路想下去,我们想到可以在训练的时候增加这样的预测模块,单独训练出来作为loss的一部分。
  最开始以为输进去的x是一个列表,里面有两张图片一张是真人照片一张是这个人的漫画图片,所以想到将两张512×7×6的图片矩阵乘后得到42×42的矩阵,再算全连接,相当于求两张图片的相似度。
  实施了之后发现有点不对,顺着代码读了dataset.py和之前以为是求两张图片夹角的AngleLinear,发现x每次输入的还是一张图片,AngleLinear求的是输入图片和自带weight的夹角。

曲线救国

  后来想到了一个batch有128张图片,可以将这128张图片组成64对进行之前预想的配对预测。这个思路大概算是比较行得通的,所以操作起来也比较思路清晰(大概是看了这么久的一个网络,多少也有些熟悉了😂)。网络的输出维持之前的想法,输出两个,然后分别计算loss。
  因为换成了随机两张图片的预测(之前想的是一张真人和一张漫画图像的预测)所以最后的全连接层添加了一层使最后输出1维,loss用MSEloss计算。labels对原先方法的输入不变,对新加的方法,则采用与网络中batch相同的处理方法,成对看是否为同一人,处理成0/1后用于loss计算。

意外收获

  用了一下TensorBoard,真好看!

未完待续

  值得开心的是这一次的改动是有效果的。可以看到虽然ACC和AUC下降了一点,但是两个下降了非常多:

结果对比
  其实误识率下降很有可能是因为训练的时候,很难配对到同一个人,所以基本上判0都会对,机器可能就会更偏向于判断两人不是同一个人,所以误识率就下去了。

  • 这个网络还可以改进的方向:
    1. 调整新Loss的占比。
    2. 单独添加 CA & SA 和预测模块,观察每个模块对最后结果的影响。
    3. 分别将 CA & SA 添加在不同的层之间,观察对结果的影响。

课程整理