【新智元导读】自动机器学习(AutoML)是近来很活跃的研究方向。KDnuggets 的主编 Matthew Mayo 写了一篇文章介绍了 AutoML 的概念,以及为什么要做自动机器学习。本文后附 AI 软件工程师 Thibault Neveu 发表在 Medium 的文章,介绍他如何训练神经网络自动编程。
在过去几年,自动机器学习(AutoML)成了一个研究的热点。在继续接下来的话题之前,我们先简单介绍什么是 AutoML,它为什么重要。然后,我们将介绍一个训练神经网络自动编程的项目,附上代码,你可以自己练习。
看着神经网络自己编程是件很令人兴奋的事情,我们强烈推荐你自己动手实践。
首先,什么是自动机器学习(AutoML)?
AutoML 不是自动数据科学(automated data science)。当然,两者有所重叠,但机器学习只是数据科学中众多工具的一种,而且机器学习用在预测很好,但描述性分析等任务里根本不会用到机器学习。
不过,就算是预测任务,数据科学涵盖的也不仅仅是实际的预测模型。数据科学家 Sandro Saitta 在讨论 AutoML 与自动数据科学间的潜在混淆时曾经说过:
误解来自于整个数据科学过程(参见例如CRISP-DM)与数据准备(特征提取等)和建模(算法选择、超参数调整等)的子任务之间的混淆。……
在阅读有关自动数据科学和数据科学竞赛的工具的消息时,没有行业经验的人可能会感到困惑,认为数据科学只是建模,并可以完全自动化。
他是对的,这不仅仅是一个语义问题。此外,数据科学家和自动机器学习支持者 Randy Olson 指出,有效的机器学习设计需要我们:
综上,我们可以认为 AutoML 是算法选择、超参数调优、迭代建模和模型评估的任务。当然,这个定义并不完全精准,但让我们先从这里开始。
为什么要做 AutoML?
AI 研究员和斯坦福大学博士生 S.Zayd Enam 在一篇名为《为什么机器学习“难”》的博客中写道(加粗强调部分是原文就有的):
机器学习仍然是一个相对“困难”的问题。毫无疑问,通过研究推进机器学习算法的科学是困难的。它需要创造力、实验和韧性。实现现有的算法和模型去适应新的应用程序时,机器学习仍然是一个难题。
注意,虽然 Enam 主要指的机器学习研究,但他也讲到现有算法在用例中的实现(见加粗部分)。
接下来,Enam 继续阐述了机器学习的难点,着重于算法的本质(强调也是原文就有的):
困难的一个方面涉及要靠直觉去想用什么工具解决问题。这需要知道可用的算法和模型以及每个算法和模型的利弊和约束。…… 困难在于机器学习在根本上是很难调试的问题。机器学习需要调试的话有两种情况:1)算法不起作用,或2)算法运行不够好……很少有算法一次就行,因此最终大部分时间都用于构建算法。
然后,Enam 从算法研究的角度阐述了这个问题。如果一个算法不起作用,或者做得不够好,而且选择和优化的过程是迭代的,这就给自动化提供了机会,从而有了自动机器学习上场的余地。
我在之前的一篇文章里曾经写过,AutoML 的本质是:
正如 Sebastian Raschka 所描述的那样,计算机编程是关于自动化的,而机器学习是“将自动化自动化”,然后自动机器学习是“自动化自动化自动化”。
编程通过管理重复的任务来减轻我们的劳动;机器学习让计算机学习如何最好地执行这些重复的任务;而自动机器学习让计算机学习如何优化学习如何执行这些任务的结果。
以前我们需要花大力气调整参数和超参数,而自动机器学习系统可以通过多种不同的方法,学习如何以最佳的方式来调整这些结果。
AutoML 源自于这个想法:如果必须建立大量机器学习模型,使用各种算法和多个不同的超参数配置,那么这种模型构建的过程,还有比较模型的性能和精度都能自动化。
简单吧?
实践:训练一个能自动编程的神经网络
要求
好,我们这就开始吧!
数据集
做任何监督学习都需要一个训练数据集,我们这个网络也一样。项目的代码全部基于 C 语言(用太简单的语言就没意思了)。因此,我们的训练数据集将是 Github Linux 库里的 C 语言脚本。我已经预先提取了相关的 .c 代码。
第一个问题:
神经网络只处理数字,其他信息都不知道。因此,数据集中的每个字符都需要表示成以下形式:
由上图可见,字符 “=” 被分配给了数字 7。稍后我们会用热编码表示每个数字,这样能在反向传播的过程中更好地收敛。
这里需要记住 3 个重要的变量:vocab_to_int、int_to_vocab 和 encoded。前两个让我们能够在字符和数字间自由转换,最后一个则是所有数据集的 encoder 格式。
第一个批次
先来生成一个简单的批次,含有两个序列,每个序列由 10 个数字组成。这个批次也将作为下面文章的样本。
这个批次看起来是这样的。也可以显示成:
好,现在我们有了需要处理的第一批值。我们的神经网络需要做的事情是,在知道已经输入的 n 个字符(而不是仅仅前一个字符)后,能够预测接下来要输入的字符是什么。比方说,如果我告诉网络,最后一个输入的字符是“e”,那么进化的可能有很多种。但是,如果我说最后的输入是“w”“h”“i”“l”和“e”,那么接下来要输入的字符是“(”就显而易见了。
因此,我们的神经网络必须要能考虑到字符类型的时空特征。
而为了实现这一点,我们需要用到一个循环神经网络。
循环神经网络
为了说明最后一个例子,一个经典的分类器(上图的左边)接收前面的字母;这个字母会被传入隐藏层(用蓝色表示),网络会推导出一个输出。一个循环的神经网络在结构上是不同的。每个单元(用红色表示)不仅连接到输入,还与时刻 t-1 的单元相连。为了解决我们的问题,我们将使用 LSTM(长短时内存)单元。
开始建模!
下面我们将详细介绍这篇文章的 5 个主要部分。占位符作为模型的一个 entry。LSTM 单元的初始化用于创建 RNN。
输出层与每个单元相连。相关运算用于测量模型的误差。最后,我们会定义训练的运算。
1)Graph 输入
批次由两个大小为 10 的输入组成,因此我们输入的形状大小为 [2,10],这个批次的每个 entry 与单个输出相关联,可以将我们的目标也定义为相同的形状。最后,我们定义一个占位符,用于将来 dropout 概率的值。
2)LSTM 输出
我们依次来看这段代码:
3)Graph 输出
单元的输出值存储在一个三维的表格中 [序列数,序列大小,神经元数],或者说是 [2,10,4]。我们不再需要按序列将输出间隔开来。我们之后会调整输出的大小,得到维数 [20,4] 的数组,存储在变量 seq_out_reshape 当中。
最后,用一个简单的线性运算:tf.matmul(..) + b。在最后整个再跟一个 softmax,把输出表示为概率的形式。
4)Loss
为了做误差运算,批处理的目标必须用与模型输出相同的方式和相同的维度来表示。我们使用 tf.one_hot 表示输出与输入有相同的编码。然后,将数组 (tf.reshape ()) 调整到与线性输出 tf.matmul(..) + b 相同的维度。现在,就可以使用这个函数来计算模型的误差了。
5)训练
用 AdamOptimize 将误差最小化即可。
训练结果!
好,终于走到结果了。这是最有成就感的一部分。我使用的参数是:
下面是在我的 GPU (GeForce GTX 1060)训练大约 2 个小时后获得的结果。
我们从误差演变开始:
最后,我们来看模型能够生成的代码:
看到模型能够清楚地了解一个程序的一般结构,感觉真是太酷了。
注意,在数据集中没有什么名为“super_fold”的函数。所以,我花了很长时间理解这个函数的功能。
不得不说,这个模型可能比我更聪明……
编译来源