Flux 是一个机器学习框架。它最大的特点是配合 Julia 的语法,用编写可微分算法的思想设计机器学习算法,通过更为抽象的数学形式来代替嵌入操作。
Flux 与机器学习
Python 没有特地为科学计算或者说机器学习有过倾斜。Julia 却从一开始就是为了科学计算而生,在语法层面、编译器角度都为科学计算打好了坚实的基础,非常适合表达机器学习的程序。
Flux希望基于 Julia 语法,重新思考机器学习算法的编写。Flux 的切入点是目前的机器学习算法本质上其实是可微分算法,这和传统的离散数据结构是截然不同的。说到底,无非是在离散结构机器上模拟计算连续的算法。因此如果能更加数学化地表达可微分算法,也就可以更为高效简洁地编写机器学习算法。Flux 的开发者还发现,求微分的过程,本质上是符号转换,这其实很大程度上是编写编译器的时候需要解决的问题。因此如果能拓展编译器的实现,也就可以在语言层面解决问题。幸运地是,Julia 提供了这种可能,因此 Flux 的开发者开发出了 Zygote(21世纪的自动微分),作为 Flux 的基础。凭借 Julia 强大的基础设施,不再需要数十万行的 C++ 代码,Flux 仅仅只有数千行的 Julia 代码(当然,成熟程度另当别论)。
有关 Flux 设计的问题可以参考 Julia 官网博客文章Building a Language and Compiler for Machine Learning或者在 arxiv 上阅读论文Fashionable Modelling with Flux
机器学习介绍
去年,公众号已经推送了一篇通过纯粹的矩阵运算,没有优化地实现全连接的三层
神经网络
,并在最经典的 MNIST 集上进行训练,通过简单的多轮循环训练,达到了 MNIST 官网给出的水准(P.S. 训练速度相对于使用框架而言是非常慢的)。在那篇文章中,对于机器学习已经有比较粗浅的介绍,因此就不重复了。简而言之,机器学习还是相当的流水化,很有几分八股文的味道。步骤是固定的,无非是获取数据、洗数据,理解数据并搭建模型,测试集/训练集划分,训练,优化。
获取 MNIST 数据
Flux 已经内置了 MNIST 的数据加载器(),可以直接下载 MNIST 经典的 60000 测试图片,相当方便。
默认上,这是训练集。可以通过传入(元编程技术)获取 10000 的测试集图片。
得到的数据是一个嵌套的数组,为了方便操作和性能考虑,可以重新将其变形为一维度的向量。可以直接使用或者使用:
把转变成,也就是从一维的竖直向量转变为水平方向。接下来,获取每张图片对应的数字。
返回的 0 - 9 数字,为了提高效率,可以使用技术(独热编码)编码向量。举个例子:
可以这样理解,对于每一个特征,如果它有 m 个可能值,那么经过 one-hot 编码后,就变成了 m 个二元特征。并且,这些特征互斥,每次只有一个激活。因此,数据会变成稀疏的。这样就解决了分类器不好处理属性数据的问题,也一定程度上扩充了特征。因此,我们可以得到这样编写代码:
好了,将上面的代码封装一下,最终写成:
搭建模型
MNIST 数据当然不用洗了,可以开始搭建模型了,Flux 内置了一些层次模型,比如卷积、池化等等,我们这次用最普通的三层全链接神经网络。三层神经网络模型的中间隐藏层规模是可以随意的,根据矩阵运算性质可以屏蔽调该参数。
注意,28^2 和10 是不能变的。前者是 MNIST 图片是 28 × 28 大小的图片,后者是 0 - 9 这10 个数字。
是线性整流函数(Rectified Linear Unit, ReLU),作为激活函数。Flux 还提供了其它的激活函数,比如经典的 Sigmoid 激活函数,还有 Kaggle 中被首先提出并使用的泄漏随机线性整流函数,等等。
至于,可以理解为归一化的意思。识别0-9这10个手写数字,若最后一层的输出为[0,1,0, 0, 0, 0, 0, 0, 0, 0],则表明我们网络的识别结果为数字1。
划分训练集和测试集
训练过程要量化训练效果,就需要使用测试集来检验。因此我们将训练和测试的比例随机分成 9:1。
如果觉得太麻烦,那么直接重复放大数据集也是可以的。
训练模型
终于到了最核心的地方,在函数中,需要结合上面的准备工作。首先,准备训练数据,接着划分训练集和测试集。
定义模型、损失函数和准确率函数:
,就是交叉熵,可以衡量两者的相似度。是取平均值,值得一提的是,这和前的 onehot 显然是一个相反的过程,onehot 把特征进行编码,onecold 自然是把解码成原来的特征。便可以广播到所有的元素,得到一个布尔值的向量,经过平均值后便得到了准确度。
接下来,将数据以 64 个一组进行划分:
定义优化器,这里使用的 Flux 内置的 ADAM 算法,这是一个随机优化算法,可以替代传统的随机梯度下降的一阶优化算法,能够基于训练数据的迭代更新神经网络的权重。
Flux 中,ADAM 的原型是这样的:
在 ADAM 算法的论文中,也给出了在 MNIST 集上的运行效果:
关于 ADAM 算法的更多信息,可以到 https://arxiv.org/abs/1412.6980 查看相应的论文Adam: A Method for Stochastic Optimization。
损失函数有了,优化方法也有了,那么接下来就是训练了。为了获得比较好的效果,我们依然采取循环训练的手段,将每一轮得到的模型,重新进入到下一轮的训练当中,而这仅仅只需要一个。为了能切实地感受到训练的过程,不妨加上一个回调函数,每一轮训练之后,输出总体的损失值和准确度。借此我们可以看到哪里过拟合,指导后续的调优。除此之外,还能用来保存模型。
我们使用闭包来编写回调函数,这样可以封装一个私有的计数变量:
之前每 64 划分一组,就是用来计数是否达到了最后一组(即一轮结束)。这样,每一轮都会输出类似这样的结果:
保存模型
将每一轮模型保存下来,这样即使中断也会有一个模型,还能借此实现断点续训的功能。只需要在后面加上这样的代码:
OK,开始训练:
结束之后,保存下最终的模型:
这里采取两种保存模型的方法,一种是保存整个模型,另一种只是保留结点的权重。相对而言,前者所需的空间更大一点,但是后期使用的时候不再需要创建模型,再进行赋值。(可以只选择其中一种来保存模型)
效果
最终代码
如果需要使用显卡训练,可以使用 CUDA 配合,如果没有 CUDA,注释掉即可。以下代码支持在 GPU 下训练(和上面的代码稍微有一点增加,例如,把数据重定向到 GPU) :
手写数字实验
接下来看看效果怎么样,用鼠标绘制了28×28的手写数字。另外单独写一段代码调用一下训练好的模型:
最后
相对于之前纯粹的未经性能优化的原始三层神经网络,使用 Flux 的速度快得多。代码上,可以看到 Flux 在 Julia 的加持下还是很简洁地完成了搭建模型到训练的整个工作。最终也获得了 97.75 这样相当不错的成绩(可以进一步根据 LeCun 官网上的参数进行调整)。
领取专属 10元无门槛券
私享最新 技术干货