这段时间忙游戏项目之外,继续花了几天学习神经网络。
上一篇 blog 发布以后,收到了图灵的朋友的 email 。然后,我收到了一大堆(九本)图灵出版的关于人工智能的中文书。通常出版社的同学给我邮寄新书,或多或少是希望我能写点什么。我不想刻意帮人宣传新书,但倒是不介意推荐一些我自己读过的不少的东西。
这次收到的书太多,当然没有全部读完。但书的内容相关性都很强(差不多一个主题)。去年的时候,我读了《如何阅读一本书》,里面提到读书的四个层次。第四个层次就是就一个主题同时读多本书是非常有意义的。这次,我也颇有收获。
在这堆书中,关于神经网络的入门。除了上一篇 blog 中提到的那本免费英文书(已经有中文版《深入浅出神经网络与深度学习》了),我还推荐日本人写的《深度学习入门》。我觉得日本的科普入门书都讲的特别细致,并配了大量图解。果然这一本补充了许多我在阅读前本书中忽略的细节。尤其是误差反向传播算法,我觉得读了之后脉络更清楚一些。btw, 我在 twitter 上推荐这本书时(并没有写出书名,只有一张照片),还收到了作者斋藤康毅的回复。世界真是小啊 :)
在实作环节、我给自己定的小目标是实践一下卷积网络。但几本书的实践代码都不再像之前那个简单的全连接网络那样有非常简单的实现了。我还是打算完全自己从零写(不借助任何包括数学向量计算在内的库)。这次没有太多可以参考的代码,所以难度更大。倒不是说代码会特别复杂,而是万一哪个细节出了 bug ,没有参照物很难追溯。需要把原理弄得再清楚一点,才能在编写代码的时候少犯错,犯了错也容易触发自己写代码的直觉快速定位 bug 。
我首先发现的是自己之前对一些基本概念理解的不够。导致老版本有好些错误的抽象。想在已有的抽象上扩展出一个卷积网络很困难。
比如、全连接层之间每个连接上都有一个影响信号传播的权重、还有一个偏置。从计算角度看,其实对信号做了一次仿射变换。我觉得这个权重和偏置看起来是一个整体。但实际它们应该是两个东西。
如果只看全连接层的一个神经元,权重配置在它上游的神经元过来的连接上;而偏置是放在自己身上的。偏置更像是另外一个独立的常量信号源。这样拆解开,可以更好的理解误差反向传播:权重和偏置的影响是分开传播的。
激活函数也是一样:不同的场合,我们需要不同的激活函数。sigmoid 、relu 、tanh 各有适用的场合。它们会反向传播误差,但没有需要调整的参数。
我在实现卷积网络之前,先花了点时间把旧代码重写。重写的版本也没有做太好的抽象,只是恰恰能用。因为我觉得随着学习的进展,随时可能再改动。没必要太早引入过多的复杂度。
关于卷积网络。其实和生物学上神经网络的结构没有太大关系。它只是利用信号处理中已经成熟的技术,通过对图像卷积运算,提取出 2D 图像上的特征。这样,神经网络的输入参数会更合理(而不是一个个孤立的像素)。可以理解为从更高维上提取出图像的信息,交给神经网络分类。
但卷积层的信号传播以及利用误差反向传播去调整参数这部分的原理和神经网络是差不多的。所以我们还是把它归到神经网络中去。对于实现、正向推理就是图形学中常见的图像卷积运算;这次我需要的是求得正确的误差反向传播算法。
理解链式法则的话,自己推导不是难事。我在自己推导了一遍后,为避免错误,又 google 了不少相关的文章。我觉得写的比较好的是微软研究院的这一篇:《卷积的反向传播原理》。至于池化层的误差反向传播更简单,也可以参考:Forward and Backward propagation of Max Pooling Layer in Convolutional Neural Networks 。
简单说来,对于 mnist 手写数字识别来说。我们期待用卷积层找到这些图片中的不同特征。横竖笔划、交叉、弯折等等。卷积有很好的平移不变性,只要找到了某个特征,据无所谓特征出现在图像的哪个位置。最后用这些特征作为信号源去判断具体数字是什么,这样比基于像素点的判断要高效。
但用怎样的滤波器(卷积核)找什么图像特征我们事先并不知道。循环卷积层的工作就是从随机数开始去寻找怎样的卷积核更合适。
ps. 说起为什么要从随机数(而不是零)初始化学习的起点。这几天我在书中找到了答案:如果我们要训练的那些参数一开始都是一致的,它们就很难分出彼此。因为本质上神经网络的同一层中的神经元都是等价的。一开始并没有规定哪个神经元负责什么工作。是训练让它们偏向不同的职责。如果一开始初始值都一样的话,就训练路径就很难分开了。如果是多层网络,我们还需要调整不同层次的随机初始值的分布参数才能获得更好的效果。
从零编写卷积层很容易犯错误。我并没有指望一次做对。所以单独写了一段测试代码测试卷积运算的误差反向传播算法。即求解问题:如果我有一张图片以及通过某个过滤器进行图像卷积运算得到的目标图案,如何反过来求得过滤器是什么。通过这段测试,找出了这些代码中的几处小 bug 。如果没有这个测试步骤,恐怕直接把新写的卷积层并入神经网络,错误都无从查起。
最终的卷积神经网络效果很好。把之前 95% 左右的正确率提升到了接近 99% 。根据从书中了解到的知识,下一步改进一些学习过程的细节,应该还能进一步提高到 99% 以上。
不过,加入卷积层后,训练成本也大幅度增加了。之前那个全连接的前馈神经网络,训练一次只需要一两分钟。而现在需要 20 分钟左右。学习过程如果有快速的数据反馈自然是效果最好。半个小时恐怕是让人心烦的极限了。
目前我的实现中采用的 mini batch 方法,也就是一个周期训练一小批数据,这一小批每组数据的训练都是完全独立的。一批完成后才累积结构迭代到要训练的模型中去。这天然就是可以拆分到不同的线程中去的。如果花一点功夫,在 skynet 或 ltask 下实现一个并行版本的话,我想还能把速度提升一个数量级。不过这似乎对学习神经网络本身意义不大。毕竟把运算移到 gpu 中收益更大。