测试运行 - 神经网络回归

2016年3月21日 22:20 阅读 372
 

回归问题的目标是基于一个或多个预测变量(自变量)的值(它可以是数值也可以是某一分类值)预测数值变量(通常称为从属变量)的值。例如,您可能想要根据一个人的年龄、性别(男或女)以及受教育水平预测他/她的年收入。

回归的最简单形式被称为线性回归 (LR)。LR 预测方程可以是这样的:income = 17.53 + (5.11 * age) + (-2.02 * male) + (-1.32 * female) + (6.09 * education)。虽然 LR 对一些问题是有用的,但在许多情况下是无效的。还有其他一些常见的回归类型 - 多项式回归、一般线性模型回归和神经网络回归 (NNR)。最后那种回归类型可以说是功能最强大的回归形式。

神经网络 (NN) 最常见的类型是可预测分类变量的类型。例如,您可能要基于如年龄、收入和性别等一些要素预测一个人的政治倾向(保守派、温和派或自由派)。NN 分类器有 n 个输出节点,其中 n 是因变量可取值的个数。n 个输出节点的值总和为 1.0,可以笼统地称之为概率。因此,以预测政治倾向为例,NN 分类器将有三个输出节点。如果输出节点值分别为 (0.24, 0.61, 0.15),因为中间节点具有最大概率,所以 NN 分类器预测结果为“温和派”。

在 NN 回归中,NN 的单个输出节点会保留数值因变量的预测值。所以,以预测年收入为例,将有三个输入节点(分别对应:年龄、性别(其中 male = -1,female = + 1)和教育程度)和一个输出节点(年收入)。

要了解什么是 NN 回归以及了解本文要讨论的问题,较好的方法是查看图 1 中的演示程序。演示的目标是要创建一个可预测正弦函数值的 NN 模型,以尽可能清晰地阐述 NN 回归概念,而不是解决实际问题。如果您对正弦函数的知识有点生疏,您可以在图 2 中查看正弦函数的图像。正弦函数的单个实际输入值可以从负无穷大到正无穷大,返回值介于 -1.0 和 +1.0 之间。当 x = 0.0、x = pi (~3.14)、x = 2 * pi、x= 3 * pi 等取值时,正弦函数均返回 0。正弦函数是一个建模相当困难的函数。

 
图 1 神经网络回归演示

 
图 2 Sin(x) 函数

演示从以编程方式生成 80 个数据项开始,并使用它们来定型 NN 模型。这 80 个定型项提供介于 0 和 6.4(略微超过 2 * pi)之间的随机 x 输入值及其对应的 y 值,即 sin(x)。

演示创建 1-12-1 NN,即,这个 NN 包含一个输入节点 (x)、12 个隐藏处理节点(有效地定义预测方程)和一个输出节点(x 的预测正弦值)。使用 NN 时,总是伴有实验验证;隐藏节点的数量通过试验和错误来确定。

NN 分类器有两个激活功能,一个用于隐藏节点,一个用于输出节点。分类器的输出节点激活函数通常是 softmax 函数,这是因为 softmax 生成的值总和为 1.0。分类器的隐藏节点激活函数通常是 S 型逻辑函数或双曲正切函数(缩写为 tanh)。但在 NN 回归中,有隐藏节点激活函数,却没有输出节点激活函数。演示 NN 将 tanh 函数用做隐藏节点激活函数。

NN 的输出由它的输入值和一组被称为权重和偏差的常数确定。因为偏差实际上只是特殊类型的权重,所以术语“权重”有时用来泛指两者。一个包含 i 个输入节点、j 个隐藏节点和 k 个输出节点的神经网络总共有 (i * j) + j + (j * k) + k 个权重和偏差。所以 1-12-1 演示 NN 具有 (1 * 12) + 12 + (12 * 1) + 1 = 37 个权重和偏差。

确定权重值和偏差值的过程被称为定型模型。这样做是为了尝试不同的权重和偏差值,由此确定在哪些地方 NN 的计算输出值与定型数据已知正确的输出值接近一致。

有一些可用于定型 NN 的不同算法。到目前为止,最常用的方法是使用反向传播算法。反向传播是一个反复进行的过程,在此过程中权重和偏差值缓慢地发生变化,以便 NN 通常可以计算出更准确的输出值。

反向传播使用两个必需的参数(最大迭代次数和学习速率)和一个可选参数(动量率)。maxEpochs 参数对算法迭代次数设置限制。learnRate 参数控制每次迭代中发生改变的权重和偏差值数量。动量参数可加快定型速度,也有助于防止反向传播算法在求解不理想时卡住。该演示将 maxEpochs 的值设置为 10,000,将 learnRate 的值设置为 0.005,将动量的值设置为 0.001。这些值将通过试验和错误来确定。

当对 NN 定型使用反向传播算法时,可以使用三种变体。在批处理反向传播中,首先检查所有定型项目,然后调整所有的权重和偏差值。在随机反向传播(也称为联机反向传播)中,检查完每个定型项目后,要调整所有的权重和偏差值。在小批量反向传播中,检查完定型项目的指定部分后,将对所有权重和偏差值进行调整。演示程序使用了最常见的变体,即随机反向传播。

演示程序显示每 1,000 次定型过程中出现的错误情况。请注意,误差值有些许波动。定型结束后,该演示显示定义了该 NN 模型的 37 个权重和偏差的值。NN 权重和偏差的值没有什么确切的解释,但务必要检查这些值,以核对是否有糟糕的结果,例如,某一个权重具有极大值而其他所有权重的值均接近为 0 的情况。

演示程序以评估 NN 模型作为结束。x = pi、pi / 2 和 3 * pi / 2 时 NN 的 sin(x) 预测值与正确值之间的差值在 0.02 以内。sin(6 * pi) 的预测值与正确值的差距甚远。但是,这只是一个预期的结果,因为 NN 仅定型为针对 x 在 0 到2 * pi 之间进行取值时预测 sin(x) 的值。

本文假设您至少具有中级编程技能,但并不假定您精通神经网络回归。本演示程序使用 C# 编程,但您也应该能够顺利地将代码重构到其他语言(如 Visual Basic 或 Perl)。虽然该演示程序过长而无法在本文中全部展示,但在随附的代码下载中提供了完整的源代码。已将所有正常的错误检查从演示中删除,以使主要概念清晰明了并减少代码的大小。

演示程序结构

为了创建演示程序,我启动了 Visual Studio 并从“文件 | 新建 | 程序”菜单选择了 C# 控制台应用程序模板。我使用的是 Visual Studio 2015,但该演示对 .NET 的依赖程度并不明显,因此,任何 Visual Studio 版本都可以正常运行。我将该项目命名为 NeuralRegression。

当模板代码加载到编辑器窗口之后,在“解决方案资源管理器”窗口中,我选择了 Program.cs 文件并右键单击它,将它重命名为更具描述性的 NeuralRegressionProgram.cs。我允许 Visual Studio 为我自动重新命名 Program 类。在 Editor 代码的顶部,我删除了所有对未使用的命名空间的引用,只留下对顶级 System 命名空间的引用。

图 3 显示了演示程序的整体结构(为节省空间进行了一些较小的修改)。所有的控制语句都在 Main 方法中。所有的神经网络回归功能都包含在一个名为 NeuralNetwork 的以程序定义的类中。

图 3 神经网络回归程序结构
using System;namespace NeuralRegression{  class NeuralRegressionProgram  {    static void Main(string[] args)    {      Console.WriteLine("Begin NN regression demo");      Console.WriteLine("Goal is to predict sin(x)");      // Create training data      // Create neural network      // Train neural network      // Evaluate neural network      Console.WriteLine("End demo");      Console.ReadLine();    }    public static void ShowVector(double[] vector,      int decimals, int lineLen, bool newLine) { . . }    public static void ShowMatrix(double[][] matrix,      int numRows, int decimals, bool indices) { . . }  }  public class NeuralNetwork  {    private int numInput; // Number input nodes    private int numHidden;    private int numOutput;    private double[] inputs; // Input nodes    private double[] hiddens;    private double[] outputs;    private double[][] ihWeights; // Input-hidden    private double[] hBiases;    private double[][] hoWeights; // Hidden-output    private double[] oBiases;    private Random rnd;    public NeuralNetwork(int numInput, int numHidden,      int numOutput, int seed) { . . }    // Misc. private helper methods    public void SetWeights(double[] weights) { . . }    public double[] GetWeights() { . . }    public double[] ComputeOutputs(double[] xValues) { . . }    public double[] Train(double[][] trainData,      int maxEpochs, double learnRate,      double momentum) { . . }  } // class NeuralNetwork} // ns

在 Main 方法中,训练数据由以下语句创建:

int numItems = 80;double[][] trainData = new double[numItems][];Random rnd = new Random(1);for (int i = 0; i < numItems; ++i) {  double x = 6.4 * rnd.NextDouble();  double sx = Math.Sin(x);  trainData[i] = new double[] { x, sx };}

作为处理神经网络时的一般规则,训练数据越多,效果会越好。若是针对 x 在 0 到 2 * pi 之间取值建模正弦函数,我需要至少 80 个数据项才能得到比较好的结果。对随机数对象选择种子值 1 是任意选取的。定型数据存储在数组型阵列矩阵中。在现实情况下,您可能会从一个文本文件中读取定型数据。

神经网络由以下这些语句创建:

int numInput = 1;int numHidden = 12;int numOutput = 1;int rndSeed = 0;NeuralNetwork nn = new NeuralNetwork(numInput,  numHidden, numOutput, rndSeed);

这里只有一个输入节点,因为目标正弦函数只接受单一值。对于大多数神经网络回归问题,您可以有多个输入节点,每一个节点对应与预测无关的变量。在大多数神经网络回归问题中,只有一个输出节点,但它可以预测两个或多个数值。

NN 需要一个随机对象来初始化权重值,并参与到定型项目的处理顺序中。演示的 NeuralNetwork 构造函数接受内部随机对象的种子值。值使用 0 是任意的。

神经网络通过以下语句进行定型:

int maxEpochs = 10000;double learnRate = 0.005;double momentum  = 0.001;double[] weights = nn.Train(trainData, maxEpochs,  learnRate, momentum);ShowVector(weights, 4, 8, true);

NN 对定型参数值极为敏感。即使一个非常小的变化也可以产生截然不同的结果。

演示程序通过对三个标准值预测 sin(x) 来评估生成 NN 模型的质量。这些做过一些小修改的语句主要有:

double[] y = nn.ComputeOutputs(new double[] { Math.PI });Console.WriteLine("Predicted =  " + y[0]);y = nn.ComputeOutputs(new double[] { Math.PI / 2 });Console.WriteLine("Predicted =  " + y[0]);y = nn.ComputeOutputs(new double[] { 3 * Math.PI / 2.0 });Console.WriteLine("Predicted = " + y[0]);

请注意,演示 NN 将其输出存储在输出节点的数组中,即使只是这个例子中的一个单个输出值。返回一个数组使您可以在不改变源代码的情况下预测多个值。

演示以针对 x 在定型数据范围之外取值时预测其对应的 sin(x) 作为结束:

y = nn.ComputeOutputs(new double[] { 6 * Math.PI });Console.WriteLine("Predicted =  " + y[0]);Console.WriteLine("End demo");

在众多 NN 分类器方案中,您可以调用一个计算分类准确度的方法,也就是,由正确预测的个数除以预测的总个数。这种方法是可行的,因为分类输出值要么是正确的,要么就是不正确的。但处理 NN 回归时,没有定义准确度的标准方法。如果要计算准确度,将视具体问题而定。例如,要预测 sin(x),您可以任意地将正确的预测定义为与正确值误差在 0.01 之内的预测。

计算输出值

在用于分类的 NN 和用于回归的 NN 之间存在的主要区别大多数存在于计算输出和定型模型的方法中。对 NeuralNetwork 类 ComputeOutputs 方法的定义以以下内容作为开始:

public double[] ComputeOutputs(double[] xValues){  double[] hSums = new double[numHidden];  double[] oSums = new double[numOutput];...

该方法接受保存预测自变量值的数组。局部变量 hSums 和 oSums 是保存隐藏和输出节点的初始(激活之前)数值的临时数组。接着,将自变量的值复制到神经网络的输入节点中:

for (int i = 0; i < numInput; ++i)  this.inputs[i] = xValues[i];

然后,隐藏节点的初始值再通过将每个输入值与其相应的输入到隐藏的权重相乘,累积进行计算:

for (int j = 0; j < numHidden; ++j)  for (int i = 0; i < numInput; ++i)    hSums[j] += this.inputs[i] * this.ihWeights[i][j];

接下来,将隐藏节点的偏差值进行相加:

for (int j = 0; j < numHidden; ++j)  hSums[j] += this.hBiases[j];

隐藏节点的值通过将隐藏节点激活函数应用到每个初始总和进行确定:

for (int j = 0; j < numHidden; ++j)  this.hiddens[j] = HyperTan(hSums[j]);

然后,输出节点的初始值再通过将每个隐藏节点值与其相应的隐藏到输出的权重相乘,累积进行计算:

for (int k = 0; k < numOutput; ++k)  for (int j = 0; j < numHidden; ++j)    oSums[k] += hiddens[j] * hoWeights[j][k];

之后将隐藏节点的偏差值进行相加:

for (int k = 0; k < numOutput; ++k)  oSums[k] += oBiases[k];

直到回归网络的计算输出节点值与分类器网络的计算输出节点值完全相同为止。但是,在分类器中,最终的输出节点值会通过将 softmax 激活函数应用到每个累计和之后计算得到。而回归网络则不应用任何激活函数。因此,ComputeOutputs 方法只是将 oSums 初始数组中的值直接复制到输出节点而已:

...  Array.Copy(oSums, this.outputs, outputs.Length);  double[] retResult = new double[numOutput]; // Could define a GetOutputs  Array.Copy(this.outputs, retResult, retResult.Length);  return retResult;}

为方便起见,输出节点的值也会被复制到本地返回数组,以便无需调用 GetOutputs 方法也可以轻松访问这些值。

当使用反向传播算法定型神经网络分类器时,将使用两个激活函数的微积分导数。对于隐藏节点,代码如下所示:

for (int j = 0; j < numHidden; ++j) {  double sum = 0.0; // sums of output signals  for (int k = 0; k < numOutput; ++k)    sum += oSignals[k] * hoWeights[j][k];  double derivative = (1 + hiddens[j]) * (1 - hiddens[j]);  hSignals[j] = sum * derivative;}

名为导数的局部变量的值是 tanh 函数的微积分导数,它源自相当复杂的理论。在 NN 分类器中,涉及输出节点激活函数的导数的计算公式是:

for (int k = 0; k < numOutput; ++k) {  double derivative = (1 - outputs[k]) * outputs[k];  oSignals[k] = (tValues[k] - outputs[k]) * derivative;}

此处,局部变量导数的值是 softmax 函数的微积分导数。然而,由于 NN 回归对输出节点不使用激活函数,所以代码为:

for (int k = 0; k < numOutput; ++k) {  double derivative = 1.0;  oSignals[k] = (tValues[k] - outputs[k]) * derivative;}

当然,乘以 1.0 没有任何影响,所以您可以简单地拖放导数项。换种角度来思考,那就是,在 NN 回归中,输出节点激活函数是标识函数 f(x) = x。标识函数的微积分导数是常数 1.0。

总结

本文中的演示代码和解释应该足以满足您探索通过一个或多个数字预测变量了解神经网络回归的需求。如果您有一个可分类的预测变量,您需要给变量编码。如果这个可分类的预测变量有两种可能的取值,如性别(男或女),则可编码一个值为 -1,另一个值为 +1。

如果这个可分类的预测变量有三种或三种以上的可能值,则可以使用“(N-1) 择 1”类型的编码。例如,如果预测变量是一个有四种可能值的颜色(红、蓝、绿、黄),那么可将红色编码为 (1, 0, 0),蓝色编码为 (0, 1, 0),绿色编码为 (0, 0, 1),黄色编码为 (-1, -1, -1)。

Dr.James McCaffrey  供职于华盛顿地区雷蒙德市沃什湾的 Microsoft Research。他参与过多个 Microsoft 产品的工作,包括 Internet Explorer 和 Bing。Scripto可通过 jammc@microsoft.com 与 McCaffrey 取得联系。

衷心感谢以下 Microsoft 技术专家对本文的审阅: Gaz Iqbal 和 Umesh Madan

#MSDNget# 【测试运行 - 神经网络回归】神经式网络回归可以说是最强大的回归形式。如果您想探索如何使用这一功能强大的工具根据一个或多个自变量来预测特定变量的值,James McCaffrey 可以帮助您快速上手 [太开心] °测试运行 - 神经网络回归 ​​​​

Microsoft Developer Network探索桌面、Web、云和手机软件开发。 投稿、合作请私信我们。