实践是检验真理的唯一标准,设置“实验室小试牛刀”环节正是为了检验学习的成果。通过将学习的理论和方法应用到实际问题中,加深对理论和方法的理解,巩固学习到的知识。这里设置了两个小项目:第一个是使用PyTorch中的Tensor完成复杂的科学计算;第二个是使用PyTorch完成线性拟合的项目。涉及的数据请在随书代码chapter1文件夹的datas文件夹中查找。
PyTorch中的Tensor对标的是NumPy科学计算库,提供了和NumPy类似的接口,并且支持CPU和GPU计算。我们要完成的科学计算叫作塔珀自指公式。塔珀自指公式是杰夫·塔珀(Jeff Tupper)发现的自指公式,此公式的二维图像与公式本身的外观一样。塔珀自指公式如下:
式中,符号[ x ]代表向下取整,如[4.2]=4;mod( a , b )表示 a 除以 b 得到的余数,如mod(11,6)=5。现在取一个常数 k ,具体如下。
在0≤ x ≤106和 k ≤ y ≤ k +16范围内会将满足此不等式的值用黑点绘制出来,不满足的就保持空白,这样许多组 x 和 y 的取值绘图就是塔珀自指公式绘图,如图1.15所示。
图1.15 塔珀自指公式绘图
此时出现了一个表示它本身的点图图像,当然只是取了 y 轴正半轴很小的一块区域。我们还可以沿 y 轴上下移动,就会看到塔珀自指公式能画出几乎所有的图形,如数学最基础的算式1+1=2,就是在 k 为如下所示的值时在 k ≤ y ≤ k +16范围内出现的,如图1.16所示。
图1.16 用塔珀自指公式绘制“1+1=2”
反之,如果已经有一张106px×17px的小图片,能找到相应的 k 值吗?应如何找到?其实这个过程并不复杂,按照下面几个步骤操作即可。
(1)从小图片最左下角的像素开始,如果空白则记为0,如果有着色则记为1。然后向上移动一行,按此规则计算出相应的0或1。
(2)第一列处理完后,向右移动再从第二列底部向上开始照此计算。然后计算第三列、第四列、第五列,直至处理完整个106×17范围内的像素。
(3)按照整个顺序将这串1802位的“0/1”字符,从二进制数转成十进制数,并且乘以17,得到就是 k 的值。
关于塔珀自指公式,国外已经做成了JS交互的工具,直接通过网页就可以访问。读者可以自行尝试。
对于任意的 x 和 y ,输入塔珀自指公式并判断等式是否成立。这实际上就是考验读者对Tensor上算子的使用和学习能力。虽然不是每个算子都有讲解,但是通过查字典及Help命令,完全可以完成上述任务。下面提供了样例代码,可以做完了再看。
这其实就是堆砌公式而已,您做出来了吗?
在很多为应届毕业生服务的微信公众号上可以看到“毕业薪酬预测”“职业生涯预测”等趣味性预测。这些预测实际上就是简单的回归模型,通常会让测试者输入毕业学校信息、专业信息、熟练使用的技能或做选择题,提交后就可以预测自己的薪酬。实际上,这些趣味性测试是有根据的。笔者了解到,一家教育行业公司所做的模型基于收集到的大量特征(如学校、区域及薪酬分布等信息),然后进行模型训练,最终能够做出比较符合实际的预测。
本节使用一份脱敏的数据来完成“迷你”版的“毕业薪酬预测”,该项测试会使用回归模型,并使用PyTorch完成模型搭建、模型训练及预测。本次实验的数据包含“专业”“城市”“薪酬”“综合能力”等字段,如图1.17所示。
图1.17 数据样本
这份脱敏的数据共有19个特征:专业、学历编码、专业编码、是否是高薪专业、是否是热门专业、工作所在地的纬度、工作所在地的经度、是否是专科、是否是本科、是否是“双一流”学校、是否是“211”学校、是否是“C9”名校、是否是“Top2”学校(指北京大学和清华大学)、是否是“985”学校、地区、地区编码、所在城市、薪酬、综合能力指标。有效数据共有255 516条记录,该数据已经脱敏,现在的业务需求是,“请设计一个趣味测试,学生输入专业、学历、毕业院校这3个维度的信息,就可以预测应届毕业生毕业后的薪酬水平”。
先分析业务,工作中业务优先,鲁莽的开始必定会影响到手的年终奖。显然,薪酬预测是一个连续的结果,因此属于回归问题。我们要做的是使用专业、学历和毕业院校等信息建立一个如下所示的方程:
薪酬= w 1 ·专业+ w 2 ·学历+ w 3 ·毕业院校+…
使用数据集训练模型可以得到 w 1 , w 2 , w 3 ,…的权重,训练好之后便可以在测试数据集上测试。首先需要考虑的是影响薪酬的因素,最重要的莫过于行业,不同的行业之间存在巨大的薪酬差别。而在学校学什么专业在很大程度上会决定学生毕业后从事的行业,专业的优势实际上就是毕业后行业的优势,并且数据集中有“专业”这一特征,这个点可以满足。影响薪酬的另一个因素是学历,从大样本分布来看,学历越高薪酬也就越高,这是当前的普遍事实,低学历者当老板而高学历者上街讨饭的例子毕竟很少见,学历可以作为薪酬预测的依据。另外,与稍差的学校毕业的学生相比,好学校毕业的学生更容易找到高薪的工作,学校的影响力不容忽视,因此也用于薪酬预测的依据。中国地大物博,区位差异很明显,同样性质的工作在东部发达地区和西部欠发达地区,薪酬的差异是很大的,因此在做模型的时候需要考虑区位差异,训练数据中的“经度”“纬度”“城区”“城市”等体现了区位的特征,可以考虑用于模型中。而从输入的“学校信息”能够扩展获取到学校的等级,如是否是“985/211”等,也可以获取到学校所在的区位信息,该区位信息正好可以用来提供“地区差异”特征,从而能够代入方程中进行薪酬的预测。
经过上述分析读者应该会有清晰的思路,由于篇幅有限,本实验只提供模型训练及测试的代码;模型的部署读者可自行完成,或许可以在微信公众号中实现这样一个有趣并且是有一定数据依据的趣味测试。读者还可以拓宽思路,加入性格测试、工作态度测试、左右脑测试、思维灵敏度测试等。
即使确定了是回归模型,也不能使用PyTorch神经网络直接做回归分析,这可以当作入门神经网络的“Hello Word”程序。先了解神经网络的结构及基本用法,后面章节再继续深入阐述。
首先回顾数据,从图1.17中可以发现,对训练有用的特征中“学历编码”“专业编码”“纬度”“经度”“地区编码”这5个特征都是类别型数据,类别型数据只对有序的对象编码,其大小没有意义,所以不能认为东经123°的薪酬比东经102°的高。像这种类别型数据在回归处理中一般做One-Hot编码处理。下面先介绍什么是One-Hot编码。
以“学历编码”为例,该数据中学历编码为{1,2,3,4,5,…,10,11,12},是一个从1到12的连续型序列。这里是脱敏的数据,我们完全可以认为“1”代表的是大学学历,“2”代表中学学历,因此编号的大小是不能作为高薪的依据的,需要对其做适当的编码。直观来说,One-Hot就是有多少个状态就有多少个比特,而且只有一个比特为1,其他全为0的一种码制。因此,“学历编码”的One-Hot编码如图1.18所示。
由图1.18可以看出,学历编码“10”的One-Hot编码为[0 0 0 0 0 0 0 0 0 1 0 0],只有一个位置上的元素为1,其余全部为0。One-Hot编码格式的转换可以借助Pandas等数据处理工具。
图1.18 “学历编码”的One-Hot编码
1.类别型数据的One-Hot编码
借助Pandas可以快速实现One-Hot编码,下面分别处理“学历编码”“专业编码”“纬度”“经度”“地区编码”,调用get_dummies方法进行One-Hot编码。
运行完成后得到的数据如图1.19所示。
图1.19 对类别型数据运用One-Hot编码后的数据
经过One-Hot编码后,特征从19个上升到328个。特征工程在算法应用的整个过程中是非常关键的,消耗的时间也是最多的,一份好的特征决定了模型能达到的上限,所有的模型都是在尽量逼近这个上限。
2.特征的归一化
类别型数据运用One-Hot编码后,特征工程的下一步往往是缺失值的处理。缺失值,顾名思义,就是空缺的、没有的值。对于缺失值一般采用“均值填充”或“中位数填充”,甚至可以训练一个简单的模型来拟合缺失值。幸运的是,这份处理后的数据中没有缺失值,可以不用处理。
另外,特征工程中一个非常重要的操作步骤是特征的归一化,即将数据进行均值为0、方差为1的转换。通常,特征的归一化处理有利于模型快速收敛,增加模型的稳定性。归一化的方法有Min-Max和Z-Score,本节采用Z-Score做归一化处理。Z-Score的计算方式如下:
式中,
x 1 , x 2 , x 3 ,…, x n 表示特征的原始值,而转换后的结果 y 1 , y 2 , y 3 ,…, y n 是均值为0、方差为1的新特征。这里借助Pandas完成“综合能力”特征的归一化,因为这个特征相比其他特征“个头较大”。权重即便是很小的变化,当乘以一个“大块头”都将带来很大的变化,为模型带来较大的波动,因此选择“大块头”进行归一化处理。这实际上也是一个堆砌公式的过程,下面是归一化的实现。
定义方法z_score,传入series对象,返回新的归一化之后的series对象,用该对象替换原来的“综合能力”特征即可。
从输出结果来看,均值非常接近0,而标准差非常接近1,这正是Z-Score所要达成的目标。用新的“综合能力”替换原来的,即完成了对数据的处理。通过to_csv保存为新的文件salary_handled.csv。数据预处理就到此为止,特征工程中有很多处理数据的小技巧,如多重共线性的分析、异常值的检测等。
下面先介绍神经网络结构,以及如何使用神经网络完成回归分析任务。神经网络包括输入层、隐藏层和输出层。现代神经网络的发展建立在早期的感知机模型上,涉及的几个关于感知机的概念如下。
(1)超平面。令 w 1 , w 2 , w 3 ,…, w n 和 x i 都是实数( R ),至少有一个 w i 不为0,由所有满足线性方程 w 1 x 1 + w 2 x 2 + w 3 x 3 +…+ w n x n = y 的点 X =[ x 1 , x 2 , x 3 ,…, x n ]组成的集合称为空间 R 的超平面。超平面就是集合中的一点 X 与向量 W =[ w 1 , w 2 , w 3 ,…, w n ]的内积。如果令 y =0,对于训练数据中某个点 X 做如下分类:
WX = w 1 x 1 + w 2 x 2 + w 3 x 3 +…+ w n x n >0,则将 X 标记为正类。
WX = w 1 x 1 + w 2 x 2 + w 3 x 3 +…+ w n x n <0,则将 X 标记为负类。
(2)线性可分。对于数据集 T ={( X 1 , y 1 ),( X 2 , y 2 ),…,( X n , y n )},其中, X i ∈ R n , y i ∈{-1,1}, i =1,2,…, N 。若存在某个超平面 S 满足 WX =0,能将数据集中的所有样本正确分类,则称数据集线性可分。所谓正确分类,就是 WX i >0,则 X i 对应的标签 y i =1;若 WX i <0,则 X i 对应的标签 y i =-1。因此,对于给定的超平面 WX =0,数据集 T 中任何一条数据( X i , y i ),如果满足 y i ( WX i )>0,则这些样本被正确分类;如果某个点( X i , y i )使 y i ( WX i )<0则称超平面对该点分类失败。
(3)感知机模型。感知机模型的思想很简单,就是在 WX + b 的基础上应用符号函数sign,使正类的预测标签为1,负类的预测标签为-1。其形式如下:
f ( x )=sign( WX + b )
式中, W 是超平面的法向量, b 是超平面的截距向量,优化的目标就是找到( W , b ),将线性可分数据集 T 尽量正确划分。找到 W 和 b 需要构建优化目标,该构建过程前面已有介绍,对于正确分类的点一定满足 y i ( WX i )>0,而对错误分类的点总是有 y i ( WX i )<0。我们将注意力集中在错误分类的点上,错误分类的点到超平面的距离可以表示为
补充一个“-”是为了使距离 d 是正数。假设所有错误分类的点都在集合 E 中,于是得到整个数据集的损失函数:
最终将寻找 W 和 b 的问题转化为损失函数最小化,即一个最优化问题。最后采用梯度下降等优化算法便可以找出 W 和 b 。
感知机模型如图1.20所示,output=sign( w 1 x 1 + w 2 x 2 + w 3 x 3 + w 4 x 4 )。
图1.20 感知机模型
感知机采用单层线性方程加上sign作为激活函数,使它对线性不可分数据无能为力,上面提到的感知机模型只适合二分类任务,因此这也是感知机模型在工业界无法使用的原因。1968年马文·明斯基提出感知机无法解决线性不可分的问题(异或问题),使以感知机模型为代表的神经网络技术发展处于“休眠”状态。
对于单层感知机无法处理非线性的问题,科学家提出了不同的解决方案。第一种解决方案是增加感知机的层数,并且在数学上证明多层感知机能够解决任意复杂度的问题,即通过增加网络的层数解决线性不可分的问题,这也成为后来神经网络的基础。第二种解决方案是引入核函数,将低维不可分的数据映射到高维,在核空间进行分类。这一流派成为神经网络流行之前的“网红”算法,即SVM。SVM通过不同的核函数将低维数据映射到更加高维的空间中,使样本变得可分,但这会造成维数过高、计算量过于庞大等问题。
两种不同的解决方案都取得了巨大的成功。下面介绍的深度神经网络便脱胎于感知机模型的改进。
3.深度神经网络
深度神经网络的前身是感知机模型,对感知机模型做了如下几方面扩展。
(1)增加隐藏层。隐藏层是输入层与输出层中间的网络层,并且可以有多个隐藏层。这增加了网络的深度,丰富了网络的表达能力。图1.21所示的多层神经网络有两个隐藏层(hidden1和hidden2),输入层有4个神经元,hidden1和hidden2各有3个神经元,最后的输出层也可以有多个输出,这样的改进使神经网络可以进行多分类任务,而感知机模型只适合二分类任务。
图1.21 多层神经网络
(2)修改输出层。使神经网络可以有多个输出,如进行100种图片的分类,它就是100个类别的多分类任务,因此输出层有100个结果,每个结果中的数值代表的是对应类别的概率,利用argmax取概率最大的索引对应的类别作为预测的类别。
(3)扩展激活函数。单层感知机的激活函数是sign信号函数,这是一个分段函数,且不连续,因此处理能力有限。在神经网络中对sign信号函数进行了扩展,引入了很多有效且连续的激活函数,如ReLU、tanh、Sigmoid、softmax等,通过使用不同的激活函数进一步增强神经网络的表达能力。
4.深度神经网络的数学表示
神经网络是一种模拟大脑神经元工作的数据结构,要参与到数据中进行计算,首先需要进行数学的表示。图1.22所示的深度神经网络可分为三大层,分别是输入层、隐藏层和输出层。层与层之间是全连接的,也就是说,第 i 层的任意神经元与第 i +1层任意神经元相连,连线表示的是神经元与神经元的权重。虽然神经元看起来很复杂,但是局部结构都是类似的,和前面的感知机一样,局部仍然是一个线性关系 z =∑ w i x i + b 和一个激活函数 σ ( z )。
图1.22 深度神经网络
以图1.22所示的深度神经网络神经元结构为例, b 3 到 o 2 的权重可以表示为 ,上标4表示第四层神经元,下标23表示第四层的第二个神经元与第三层的第三个神经元相连,将下标反过来表示(下标是23而不是32)是为了便于线性代数的矩阵运算,否则权重需要求一次转置。
由此可知, 表示第 k -1层的第 b 个神经元到第 k 层的第 a 个神经元之间的权重,输入层没有权重。偏置项 b 的表示与权重的表示类似。 a 3 的偏置项用 表示,上标是所在的网络层,下标是神经元在层中的索引,这里表示第二层的第三个神经元。
5.深度神经网络前向传播的数学原理
下面以图1.23所示的神经网络结构为例说明神经网络前向传播的数学原理。
图1.23 神经网络前向传播的数学原理
神经元的整体虽然很复杂,但是具有相同的局部特征,仍然是一个线性关系 z =∑ w i x i + b 和一个激活函数 σ ( z )的形式。神经网络的第一层没有权重,也没有偏置项,因此直接看第二层的权重与偏置项的计算过程。对于第二层中 a 1 、 a 2 神经元的输出,有如下所示的数学表达式:
而对于第三层的输出 o 1 而言,有如下所示的数学表达式:
上面数学表达式中的输入是第二层输出的结果,因此 x 被 a 替换。从上面的数学表达式可以发现一些规律:偏置项 b 的上下标与输出是一致的; σ 激活函数的输入上下标和偏置项、输出值的上下标是一致的;每个数学表达式中 w 的上标保持一致,而下标对应上层每个神经元。将上面的数学表达式一般化,假设第 L -1层有 m 个神经元,第 L 层有 n 个神经元,则对于 L 层的第 j 个神经元的输出可表示为如下形式:
上面的公式使用矩阵算法可以使表达式更加简捷。第 L -1层有 m 个神经元,第 L 层有 n 个神经元。将第 L 层的线性系数 w 组成一个 n × m 的矩阵 W L ,第 L 层的偏置项 b 组成一个 n ×1的向量 b L ,第 L -1层的输出 a 组成一个 m ×1的向量 a L -1 ,第 L 层未激活前的输入 z 组成一个 n ×1的向量 z L ,第 L 层的输出 a 组成一个 n ×1的向量 a L 。则用矩阵表示为如下形式:
a L = σ ( z L )= σ ( W L a L -1 + b L )
有了上面的矩阵表示,接下来就可以给出深度神经网络前向传播算法。
输入:总层数 L ,所有隐藏层和输出层对应的矩阵为 W ,偏置项对应的向量为 b ,输入值向量为 x 。
输出:输出层输出结果向量 a L 。
初始化过程如下。
对于输入层 a 1 = x 。
For i =2 to L do:
a i = σ ( z i )= σ ( W i a i -1 + b i )
计算最终的结果即为 a L 向量。
6.深度神经网络反向传播的数学原理
前向传播完成后会得到输出结果向量 a L ,通过与真实数据标签 y 进行对比可以构建损失函数,如常见的均方误差(Mean Squared Error, MSE)损失,由损失函数反向对各层 w 和 b 求偏导数,进而更新权重参数 w 和 b ,完成训练和优化。
深度神经网络反向传播算法只需要流程上的理解,具体的算法推导过程读者可以根据自己的精力和时间深入研究与学习。这里只对反向传播算法的数学原理进行简单的推导(优化算法如梯度下降等,这里只需要知道是用来更新参数的方法即可,具体内容参见第4章)。
由前面的介绍可知,神经网络的输出结果为向量 a L ,和具体的标签值 y 进行对比可以得到该次前向传播的损失。损失函数有很多,如MSELoss、CrossEntropyLoss等。本节使用MSE来说明BP算法的原理。对于每个样本,我们期望损失值最小,MSE损失函数如下:
有了损失函数,下面用梯度下降求解每层网络的权重矩阵 W 和偏置项向量 b 。对于输出层 a L = σ ( z L )= σ ( W L a L -1 + b L ),代入损失函数有如下形式:
现在只需要针对损失函数中的 W 和 b 求偏导数,然后更新权重和偏置项即可完成神经网络的训练。
7.使用PyTorch构建神经网络
前面介绍了神经网络结构的基础知识,下面使用PyTorch构建神经网络。PyTorch对神经网络中前向传播、反向传播、自动微分等复杂的操作进行了封装,并提供了简单的接口调用。下面先介绍PyTorch的nn模块,快速搭建神经网络。
PyTorch的nn模块提供了两种快速搭建神经网络的方式。第一种是nn.Sequential,将网络层以序列的方式进行组装,每个层使用前面层计算的输出作为输入,并且在内部会维护层与层之间的权重矩阵及偏置项向量。使用nn.Sequential方式定义模型非常简单,如下所示的代码片段定义了一个三层的神经网络。
输入层的维度为10,中间隐藏层的维度为20,输出层的维度为2。中间使用ReLU作为激活函数,与单层感知机中的sign符号函数一样,ReLU是激活函数的一种,后面章节会详细介绍各种不同的激活函数。
另一种常见的搭建神经网络的方式是继承nn.Module,需要实现__init__和forward前向传播两个方法。同样,实现上面的三层神经网络,可以采用继承nn.Module的方式,具体如下。
采用继承nn.Module的方式,需要我们自己实现forward方法,forward方法接收输入网络的数据x,经过网络中不同层函数的处理,最后返回线性函数linear2的处理结果作为最终的预测输出值。
8.定义神经网络结构
前面处理好的数据共有328列,其中“薪酬”为标签。将“薪酬”单独列出,并从数据集DataFrame中删除,混洗打乱后划分出训练集和测试集,使用前20万条数据作为训练集,剩下的数据作为测试集。
现在训练集中特征数量为327,则对应的输入神经元为327个,借助神经网络可以将过多的特征进行压缩,因此设计第一个隐藏层的神经元为100个,第二个隐藏层的神经元为20个,输出层的神经元为1个(因为是回归任务,输出只有一个值)。薪酬预测神经网络模型结构如图1.24所示。
图1.24 薪酬预测神经网络模型结构
神经网络实际上是线性方程的组合,相比直接使用327个特征作为线性方程的变量,神经网络使用多个隐藏层将特征进行压缩,压缩后的特征拥有20个维度,维度更低,便于线性方程的求解。
下面使用PyTorch中基于Module的神经网络的方式搭建该神经网络,代码实现如下。
四层神经网络搭建好之后直接使用SalaryNet便可以实例化一个模型,使用方式为salaryModel=SalaryNet(327,100,20,1)。
有了模型还需要定义损失函数,因为PyTorch优化的依据是使损失函数的值最小。这里我们使用MSE损失函数,PyTorch中已经定义好该损失函数,按如下方式使用即可。
有了损失,还需要根据损失进行优化,PyTorch实现了很多优化器,如梯度下降、Adam等,下面使用Adam优化器进行优化。
所有优化器的输入基本上都是一样的,即需要优化参数和学习率。这里直接通过salaryModel的parameters方法便可以获取网络层中所有需要优化的权重矩阵及偏置项,指定学习率为0.001。学习率的设置是很关键的,学习率过大,优化过程容易出现波动,学习率过小,又会使模型收敛缓慢。学习率设置为0.001是最常见的技巧,后面章节会介绍其他方式的学习率的设置。
下面开始编写训练过程。模型需要多次迭代,而每次完整的迭代被视为一个epoch。在迭代过程中,每次取一批数据进行训练,记录每次训练的损失,用于可视化展示。
训练过程的代码如下。
每训练10次,保留一次Loss,并判断模型的性能是否有所提升,如果比之前模型的性能有所提升,则使用torch.save将模型保存到model.ckpt文件中,模型训练过程截图如图1.25所示。
图1.25 模型训练过程截图
模型训练完成后,变量loss_holder所保存的训练步骤及损失的数据可以用于训练过程的可视化,借助Matplotlib可以方便快速地进行数据可视化。
xticks方法用于设置 x 轴上的刻度,整个训练过程的损失可视化结果如图1.26所示。
从可视化结果可以看出,大概迭代30 000(3000×10,因为每隔10次保留一次Loss)次,模型损失就趋于平缓,继续训练模型的性能已经没有太大的提升空间,这个时候其实就可以停止模型的训练。训练好的模型保存在model.ckpt文件中,接下来使用训练好的模型在测试数据集上进行测试,测试代码如下。
图1.26 Loss与训练步骤可视化
将预测值与目标值分别保存在results和targets数组中,用于可视化。如果预测结果results和targets接近,则以result和target分别作为 x 轴与 y 轴对应的值,可视化的结果接近于直线 y = x 。理论上,如果预测完全准确,所有的点都将落在直线 y = x 上。首先进行可视化数据准备。
借助scatter绘制散点图,代码如下。
预测值和目标值的可视化结果如图1.27所示。
从可视化结果可以看出,在薪酬为20 000之前的数据,预测值和目标值组成的点基本上在直线 y = x 附近。超过20 000的薪酬预测出现了散乱和波动,而根据实际情况,超过20 000的数据是很少见的,甚至是异常数据,因此模型下一步要优化的目标可以放到训练数据中的“高薪”离群数据进行清洗及整理。这是一个负反馈过程,这个任务读者可自行尝试。
至此,“薪酬预测模型”已经全部完成,相信读者对神经网络的认识会更加清晰。1.5节是轻松愉快的“补充营养时间”。
图1.27 预测值和目标值的可视化结果