人脸识别算法的小改进

Deeplearning.ai课程中第4门卷积神经网络里, 第4周的作业是做一个人脸识别的小应用.

这门课的作业设计都有些问题. 因为需要的数据量很大, 需要的算力也很高, 所以不大可能让学生从头做一个深度神经网络然后训练出结果. 所以作业基本是两头的, 一头是教如何建立这个深度神经网络, 一头是教如何应用这个建好的网络, 中间的部分就一笔带过说我们已经帮各位训练好了, 大家load就行了.

原有的人脸识别算法:

人脸识别的算法是所谓one-shot的方式, 不是直接去分类, 而是判断两个图片是否是属于统一类的, 或着说, 就是计算两个图片之间的距离.

人脸的照片(96*96像素)先经过一个深度神经网络编码器, 输出一个128位的编码.

FRmodel = faceRecoModel(input_shape=(3, 96, 96))
load_weights_from_FaceNet(FRmodel)

这个编码器是已经训练好的. 需要的话可以从这里下载到, (具体是那个文件我还没弄清楚. )

有了编码器, 就可以把任意人脸的照片进行编码, 例如:

database["danielle"] = img_to_encoding("images/danielle.png", FRmodel)

然后就是计算待测图片和目标图片编码的欧式距离. 一句话而已, 但属于作业内容, 我不能写出来.

算出距离以后, 跟一个阈值(! yu4 zhi2 !)比较一下, 如果距离小于阈值就认为两个图片是一个人的, 于是就"识别"了.

Andrew Ng老师给出的应用样例是百度的门禁. 人走过去, 照相, 与ID记录中保存的照片对比, 距离小于阈值, 放行.

原算法的问题

交付的时候编码器应该是已经完成的, 阈值也就是一个单一的数, 估计也是写死的. 所以这个人脸识别的应用在交付给用户应用的时候是固定的. 当然这也无可厚非, 毕竟大多数软件是这样的.

但神经网络毕竟得到的是个概率, 阈值也是人为设定的, 有可能出现一直把老板和扫地僧搞混的情况. 或者类似apple FaceID里的问题, 也许亲缘系数很近的亲属是可以互相解锁的. (10岁男儿解锁其母的FaceID, wired报道, 网易报道 )

(写到这的时候我意识到自己不小心开源了一个可以赚钱的算法, 算了, 开了就开了吧. 好像丢了一大笔钱 )

  • 理论上, 可以把编码器重新训练一遍, 比如把老板, 亲属, 扫地僧的照片加入到训练集里再重新修炼.
  • 理论上, 用户也可以调整阈值, 提高或者降低特异性敏感性.

但实际上, 显然让用户重新训练这么大而深的网络不现实, 没有数据集也没有那么强的算力. 调整阈值也可能会造成其他的连带问题.

综上, 赋予用户后期调整的能力很重要.

并联网络

之前的神经网络, 全连接也好, 卷积神经网络也好, 都是串联的, 一层接着一层, 直到ResNet出现, 在两层网络之间增加了短路的通道. 所谓短路, 就是直接把上一层的计算结果送到后面去. 被短路的几层网络只是负责训练"残差".

类似的思想也可以用在这里. 分两条路径: * 代数函数路径

def dist_path(x1,x2,threshold):
    d = tf.norm((x1-x2),axis=-1)
    # output=1-Activation('sigmoid')(d-threshold)
    output = 0.5 - (tf.sign(d-threshold))/2
    return output

这条路径上, 还是使用欧式距离来计算两个输入图片x1,x2之间的距离, 然后跟阈值比较. 如果想二值输出, 就用符号函数sign, 如果想得到连续输出就用一下sigmoid.

这条路径就是原来的计算方式, 参数都是写死的.

  • 修正神经网络

在这条路径之外, 我又并联了一条神经网络路径, 所用的神经网络其实要求并不高, 一是大部分的feature已经被编码器模型提取好了, 二是代数路径其实效果已经不错, 只需要微调一下. 所以并不一定需要多复杂的网络.

def tinker_path(x1,x2):
    X=tf.concat([x1,x2],axis=-1)
    X = Dense(128, activation='relu',
              kernel_initializer =
              RandomNormal(mean=0.0, stddev=0.05))(X)
    X = Dense(128, activation='relu',
              kernel_initializer =
              RandomNormal(mean=0.0, stddev=0.05))(X)
    X = Dense(1, activation='tanh',
              kernel_initializer =
              RandomNormal(mean=0.0, stddev=0.05))(X)
    return X

这里仅使用了一个简单的全连接网络示意, 实际中可能比这个层次稍微多一点, 但我觉得没必要太深. 卷积网络可以考虑, 而且可以考虑把输入的两份编码数据交叉排布成一个类似图片的东西.

由于并联上去的神经网络是为了修正误差用的, 所以通常来说它的输出值应该很小, 不影响主路径的结果. 所以我在初始化的时候使用的随机初始值是很集中于0附近的. 把标准差stddev改得很小.

  • 合并网络

将代数函数路径和修正神经网络并联在一起. 所谓并联就是将两个网络进行加权平均.

def face_tinker(x1,x2,threshold,alpha=[0.7,  0.3]):
    paths=[dist_path(x1,x2,threshold), tinker_path(x1,x2)]
    X=Add()([ a*path for (a,path) in zip(alpha,paths)])
    return X

主要的是代数函数路径, 给的权重应当高一点, 次要的是修正神经网络路径, 给的权重可以低一些, 但是修正神经网络要真的能够改变输出才行, 如果是[0.9, 0.1]这样的加权平均就没什么意义了.

完整的代码在github上

后续

我对tensorflow还不是很熟练, 还应当补充上loss, 训练之类的.

实际使用中如果两个人总是搞混, 可以把各自的照片多拍一些, 固定编码器的参数, 只去训练修正神经网络, 哪怕过拟合了也没什么关系, 毕竟是小范围使用, 不必太过追求泛化.

唉, 丢了一大笔钱.

并联网络推广

在已知代数函数上并联一个神经网络进行修正的模式很有意思.

现有的知识中其实已经有大量的线性拟合或者简单的非线性拟合工具, 比如计算人工晶体度数的SRK公式, 但是这些公式面对特殊问题的时候还是会出问题, 比如遇到高度近视或者准分子激光手术后的患者.

完全抛弃原有的公式, 使用深度神经网络重做一个算法当然可以, 但这需要大量的数据堆砌, 好像人们以前的经验也就浪费了.

如果在已知代数函数的基础上并联一个神经网络, 用神经网络来修正原有函数, 就好像Taylor展开那样, 一点一点近似, 近似到够用就可以了.

也许对数据集的大小, 对训练神经网络所需要的算力, 要求都会减低一些吧.

不过, 我估计肯定有很多人尝试过了, 而且失败了不少. 通常觉得自己有个新想法的时候, 只是因为文献阅读得不够多.

EOF( )