2017 年 4 月

第 32 卷,第 4 期

此文章由机器翻译。

测试运行 - 使用 C# 的内核感知器

通过James McCaffrey

James McCaffrey内核感知器是机器学习 (ML) 分类器可用于进行二元预测。例如,内核感知器可以预测人员的性别 (男性 =-1,女性 = + 1) 基于年龄、 收入、 高度和权重。内核感知普通感知高级变体,并且可以处理更复杂的数据。

请参阅本文所述观点的好方法是看一看中的演示程序图 1和中的关联的数据图 2。演示程序的目标是预测类,则为-1 或 + 1,但不包含两个预测器变量,x0 的虚拟输入数据和 x 1。例如,第一个定型数据项目是 (2.0、 3.0、-1),这意味着,如果输入的值 x0 = 2.0 和 x1 = 3.0 中,正确的类为-1。

内核感知器演示
图 1 内核感知器演示

内核感知器定型数据
图 2 内核感知器定型数据

21 定型数据点具有循环的几何,这就意味着简单的线性分类技术,如普通感知或逻辑回归中,较为低效。此类数据称为非线性可分离。

该演示程序使用训练数据创建预测模型使用内核感知器。与不同的是包含一组称为权重的数字值的多个预测模型,内核预测模型创建一组计数器的值,另一个用于每个训练数据项。该演示程序显示前两个数据项 (1 和 2) 和 (0 和 1) 的最后两个数据项目的计数器值。

训练后,内核感知器模型预测所有 21 数据项正确,这是预期会出现。经过训练的模型将应用于四个新测试数据项目︰ 定型期间未使用。第一个测试项目具有输入 (2.0、 4.0) 和-1 的类。预测模型正确预测该项目。总之,该模型将正确预测三个 0.75 准确性的四个测试项目。

在后台,内核感知器使用一个名为径向基函数 (RBF) 内核函数。在内核函数需要名为 sigma 的参数。Sigma 的值必须由试验和错误和 sigma = 1.5 演示中的。请注意,通过设置为 0.5 sigma,演示程序将达到 100%的精确度。我使用 sigma = 1.5 要指出的是,对于大多数数据集不会达到 100%的精确度。

本文假定您具有中级或更高版本编程技能,但并不假定您知道有关内核感知的任何信息。该演示程序使用 C# 中,进行编码,但您应该能够轻松重构为其他语言,如 Python 或 JavaScript 代码,如果您想。除硬编码数据和一些外的所有关键演示代码中显示语句,本文中介绍。完整的演示源代码中,其中包括数据是在随附下载中可用。

了解的内核函数

为了了解内核感知必须了解在内核函数在一般情况下。有很多的内核函数,但最常见,并且该类型由演示程序,称为 RBF 内核。RBF 内核函数接受两个向量 (也就是说,数组) 和一个名为 sigma,参数并返回单个值介于 0.0 和 1.0 是两个阵列之间的相似性的度量值。返回值为完全 1.0 表示两个数组都是相同的。两个数组越相似,越小 RBF 值,接近但从未完全达到 0.0。

计算公式为 RBF 是︰

K(x1, x2) = exp( - || x1 - x2 ||^2 / (2 * sigma^2) )

在这里,K 代表内核,x1 和 x2 是两个具有相同的长度的数组,sigma 为像 1.0 或 1.5,这样的值的可用参数,则 | |表示欧几里得距离和 exp 意味着欧拉数 (e) 的幂。

RBF,更好地示例进行说明。假设 x1 = (2.0、 1.0 和 3.0) 和 x2 = (0.0,1.0,5.0),和 sigma 为 1.0。首先,您可以计算的平方的欧几里得距离︰

|| x1 - x2 ||^2 = (2.0 - 0.0)^2 + (1.0 - 1.0)^2  + (3.0 - 5.0)^2
                           = 4.0 + 0.0 + 4.0
                           = 8.0

接下来,您可以平方的距离除以 2 倍 sigma 平方值︰

8.0 / (2 * (1.0)^2) = 8.0 / 2.0 = 4.0

最后,您采取欧拉数 (e = 大约 2.71828) 并将其提升到上面的结果中的负数︰

K = e^(-4.0) = 0.0183

请注意,如果 x1 等于 x2,平方欧几里得距离将为 0,并且再除以 2 倍 sigma 平方值将为 0,并 e 0 个幂为 1.0。不太相似 x1 和 x2 是,较大的平方的差将准备,并获取很小,接近 0.0 e 负的大值。越大 sigma 值、 越大 K 的值,但 K 仍从 0.0 排他 (却完全不同向量) 到 1.0 (等于向量) 而异。数组参数的顺序并不重要,因此 K (x1,x2) = K (x2,x1)。

演示计划定义为 RBF 内核函数︰

static double Kernel(double[] d1, double[] d2, double sigma) {
  double num = 0.0;
  for (int i = 0; i < d1.Length - 1; ++i)
    num += (d1[i] - d2[i]) * (d1[i] - d2[i]);
  double denom = 2.0 * sigma * sigma;
  double z = num / denom;
  double result = Math.Exp(-z);
  return result;
}

此函数假设每个数组的最后一个单元包含类标签 (+ 1 或-1),因此最后一个单元格不包含在计算中使用。有一些可与内核感知器,其中包括线性内核、 多项式内核、 Fisher 内核和 sigmoid 内核使用的其他内核函数。计算出内核函数是相对简单。新增功能并不显而易见,但是,是内核函数具有一些非同寻常的数学属性,允许简单分类器,如普通感知,若要转换为分类器可以处理非而呈线性增长可分离的数据。

了解普通感知

普通感知器可以执行简单、 可分离而呈线性增长的数据的二元分类。普通感知器包含的一组权重,其中的权重数等于预测器值的数目和额外的特殊重量调用偏置。例如,假设您遇到了二元分类问题,因为有三个预测器变量。同时假设这些感知器权重是 w0 = 1.5,w1 = 1.1 和 w2 =-3.0,并且三个预测器变量具有值 x0 = 4.0 中,x1 = 3.0 中,x2 = 5.0 和偏移是 b = 2.5。预测计算方式的权重和输入的值,以及偏差乘积的总和的符号 (肯定或否定) 为︰

sum = (w0)(x0) + (w1)(x1) +(w2)(x2) + b
          = (1.5)(4.0) + (1.1)(3.0) + (-3.0)(5.0) + 2.5
          = 6.0 + 3.3 + (-15.0) + 2.5
          = -3.2
prediction = sign(sum)
                      = -1

就这么简单。但是,权重和偏置值来自何处呢? 获取具有已知输入和正确类值的定型数据,然后使用数学优化算法查找的权重和偏置值,以便计算的类值能够非常接近已知的正确类值。

请注意,在感知器文献中偏置值通常被视为要与虚拟输入值为 1.0 关联的正则权重。使用该方案时,前面的示例将具有输入 x = (1.0,4.0,3.0,5.0) 和权重 w = (2.5、 1.5、 1.1、-3.0),生成有关以下内容 (其中都相同的结果和以前一样)︰

sum = (2.5)(1.0) + (1.5)(4.0) + (1.1)(3.0) + (-3.0)(5.0)
          = -3.2

换而言之,感知器偏置值可以是显式或隐式。不应低估这可能会导致的混淆。

现在,遗憾的是,普通感知可以仅以线性方式分离对数据进行分类,这使它们成为非常有限的做法。但是,感知器结合使用内核函数,就可以创建分类器,可以使用的非而呈线性增长可分离数据,如中所示的演示数据图 2

内核感知器算法

从表面看,内核感知器算法出现无关的普通感知器算法,但实际上,这两个都很深相关。假设有只有四个训练数据项︰ td [0] = (2.0、 3.0、-1)、 td [1] = (3.0、 2.0 和 + 1)、 td [2] = (3.0、 5.0 和 + 1)、 td [3] = (4.0,3.0,则为-1)。并假设训练的内核感知器模型会为您提供不正确的计数器值 = (1、 2、 3,4)。(我使用"a"对于错误计数器由于在研究文献中它们通常被提供希腊语 sigma 符号,它类似于小写英文"a。)

若要预测的类的新数据项 x = (3.0、 6.0),则计算应用于新的数据项和每个四个训练项的内核函数 (通过一个值和培训项正确类) 的加权的和︰

K(td[0], x) = 0.1083
K(td[1], x) = 0.0285
K(td[2], x) = 0.8007
K(td[3], x) = 0.1083
sum = (1)(0.1083)(-1) + (2)(0.0285)(+1) + (3)(0.8007)(+1) + (4)(0.1083)(-1)
          = +1.9175
prediction = sign(sum)
                      = +1

声明某种程度上宽泛一些,内核感知器审视对构造进行分类的数据项和所有训练数据项的相似性,并聚合相似性值 — 错误计数器用作权重 — 为单个值,该值指示预测的类。

因此,若要使内核感知器预测需要定型数据和相关联的错误计数器值。在训练普通感知器时,您将使用一个迭代过程。在每个迭代中,使用权重和偏置的当前值来计算预测的类。如果不正确预测的类 (不匹配定型数据中的类) 有点以便预测的类是更接近于已知的正确类值调整权重。

在内核感知器,使用类似迭代定型过程中,而调整权重值,如果某个计算的类是错了,您递增当前训练项的错误计数器。令您惊讶 ! 为什么此方法有效的数学证明是极为美观,并可在内核感知的 Wikipedia 条目中找到。

表示在高级伪代码,是内核感知器定型算法︰ 

loop a few times
  for each curr training item, i
    for each other training item, j
      sum += a[j] * K(td[i], td[j], sigma) * y[j];
    end-for
    pred = sign(sum)
    if (pred is wrong) increment a[i]
  end-for
end-loop

在定型算法中所需的两个参数是要循环的次数和 sigma 内核函数的值。这两种值必须通过执行少量试验和错误来确定。该演示程序将迭代 10 次并使用 sigma 1.5。

请注意,普通感知器定型算法使用训练数据生成的权重和偏置值,然后从概念上讲放弃定型数据。内核感知器定型算法生成与权重,从数学上相关的错误的计数器值,但该算法必须使定型数据,以便进行预测。

演示程序结构

在显示了演示程序使用少量小幅改动,以节省空间的整体结构图 3。我为简单起见使用静态方法样式,而不是面向对象的编程风格。Main 方法具有所有控制逻辑。有两个帮助器方法,内核和准确性。

图 3 内核感知器演示程序结构

using System;
namespace KernelPerceptron
{
  class KernelPerceptronProgram
  {
    static void Main(string[] args)
    {
      Console.WriteLine("Begin demo ");
      int numFeatures = 2;
      Console.WriteLine("Goal is classification(-1/+1) ");
      Console.WriteLine("Setting up 21 training items ");
      double[][] trainData = new double[21][];
      trainData[0] = new double[] { 2.0, 3.0, -1 };
      trainData[1] = new double[] { 2.0, 5.0, -1 };
      . . .
      trainData[20] = new double[] { 5.0, 6.0, +1 };
      int numTrain = trainData.Length;
      double[][] testData = new double[4][];
      testData[0] = new double[] { 2.0, 4.0, -1 };
      . .
      testData[3] = new double[] { 5.5, 5.5, +1 };
      int[] a = new int[trainData.Length];
      int maxEpoch = 10;
      int epoch = 0;
      double sigma = 1.5;  // for the kernel function
      Console.WriteLine("Starting train, sigma = 1.5 ");
      ...
      Console.WriteLine("Training complete ");
      double trainAcc = Accuracy(trainData, a,
        trainData, sigma, false);  // silent
      Console.WriteLine("Accuracy = " +
        trainAcc.ToString("F4"));
      Console.WriteLine("Analyzing test data: ");
      double testAcc = Accuracy(testData, a,
        trainData, sigma, true);  // verbose
      Console.WriteLine("End kernel perceptron demo ");
      Console.ReadLine();
    } // Main
    static double Kernel(double[] d1, double[] d2,
      double sigma) { . . }
    static double Accuracy(double[][] data, int[] a,
      double[][] trainData, double sigma, bool verbose)
    { . . }
  } // Program
} // ns

编写演示程序的代码,我启动了 Visual Studio 并创建一个新 C# 控制台应用程序并将其命名为 KernelPerceptron。我使用 Visual Studio 2015,但是演示程序有没有明显的.NET Framework 依赖性,因此,任何最新版本将正常运行。

模板代码加载到编辑器窗口后,我右键单击文件在解决方案资源管理器窗口中的 Program.cs 文件重命名为 KernelPerceptronProgram.cs,然后允许 Visual Studio 自动为我重新命名 Program 类。在模板生成代码的顶部,我删除所有不必要的 using 语句,保留只是一个引用顶级 System 命名空间。

训练和测试数据的 Main 方法集如下所示︰ 

int numFeatures = 2;
double[][] trainData = new double[21][];
trainData[0] = new double[] { 2.0, 3.0, -1 };
. .
trainData[20] = new double[] { 5.0, 6.0, +1 };
int numTrain = trainData.Length;
double[][] testData = new double[4][];
testData[0] = new double[] { 2.0, 4.0, -1 };
. .
testData[3] = new double[] { 5.5, 5.5, +1 };

该演示为简单起见,使用两个预测器变量 (也称为 ML 术语的功能) 但内核感知可以处理任意数量的预测器变量。数据被硬编码,但在非演示方案中您将可能数据加载从文本文件。该演示使用-1 和 + 1 表示两个可能的类。此编码是典型的感知,但类可被编码为 0 和 1 改为 (尽管这种编码需要对代码逻辑的某些更改)。

使用这些语句被准备培训︰ 

int[] a = new int[numTrain];  // "Wrong counters"
double sigma = 1.5;  // For the kernel function
int maxEpoch = 10;
int epoch = 0;

数组每个训练项,包含错误计数器并 sigma 是 RBF 内核函数的可用参数。试验和错误由确定 sigma 和 maxEpoch 循环控制的值。接下来,所有可能的内核函数值是预先计算并存储到矩阵︰ 

double[][] kernelMatrix = new double[numTrain][];
for (int i = 0; i < kernelMatrix.Length; ++i)
  kernelMatrix[i] = new double[numTrain];
for (int i = 0; i < numTrain; ++i) {
  for (int j = 0; j < numTrain; ++j) {
    double k = Kernel(trainData[i], trainData[j], sigma);
    kernelMatrix[i][j] = kernelMatrix[j][i] = k;
  }
}

其理念是,通过在定型过程之间所有训练项的成对的内核相似性都具有要使用几次,因此有必要以预先计算这些值。

训练循环是︰

while (epoch < maxEpoch) {
  for (int i = 0; i < numTrain; ++i) {
    // Get "desired" correct class into di
    for (int j = 0; j < numTrain; ++j) {
      // Get "other" desired class into dj
      // Compute y = weighted sum of products
    }
    if ((di == -1 && y >= 0.0) || (di == 1 && y <= 0.0))
      ++a[i]; // increment wrong counter
  }
  ++epoch;
}

(-1 或 + 1) 的所需,正确类来自当前的训练项替换为以下代码︰ 

int di = (int)trainData[i][numFeatures];

我使用 di 此处代表所需的值。其他两个常见的变量名称是 t (表示目标值) 和 y (这只是代表常规输出)。

嵌套的 for 循环的内核值进行加权求和的内部是内核感知器学习算法的核心︰ 

double y = 0.0; // Weighted sum of kernel results
for (int j = 0; j < numTrain;; ++j) {
  int dj = (int)trainData[j][numFeatures];
  double kern = kernelMatrix[i][j];
  y += a[j] * dj * kern;
}

演示代码计算所有训练项的成对的内核。但产品术语时关联的错误计数器具有值 0 时,将为 0。因此,一种重要可选优化培训项的数目很大时是在 [i] 或 [j] 的值为 0 时,跳过内核的计算。同样地,可以完全删除具有不正确的计数器值为 0 的培训内容。

我不计算显式预测的类,因为它是更轻松地检查是否直接预测的类为错误︰ 

if ((di == -1 && y >= 0.0) || (di == 1 && y <= 0.0))
  ++a[i]; // wrong counter for curr data

您必须进行仔细检查 y > = 0.0 或 y < = 0.0 而不是 y > 0.0 或 y < 0.0,因为第一次训练循环时,所有错误的计数器值数组均为零,因此加权乘积和将 0.0。

训练循环终止后,内核感知器由定型数据、 错误计数器数组和 RBF 内核参数 sigma 有效地定义。

进行预测

Helper 函数准确性进行预测。该方法的定义开头︰ 

static double Accuracy(double[][] data, int[] a,
  double[][] trainData, double sigma, bool verbose)
{
  int numFeatures = data[0].Length - 1;
  double[] x = new double[numFeatures]; 
  int numCorrect = 0;
  int numWrong = 0;
...

名为 data 的参数包含要计算数据的数组的数组样式矩阵。一个包含错误的计数器值生成的培训的参数数组。

该函数体内的键代码非常相似培训代码中,除了而不是递增的当前定型数据的错误计数器递增了错误的预测、 单个 numWrong 变量或 numCorrect 变量上的项︰ 

if ((di == -1 && y >= 0.0) || (di == 1 && y <= 0.0))
  ++numWrong;
else
  ++numCorrect;

检查了所有数据项目在调查后,将返回正确预测的百分比︰ 

return (1.0 * numCorrect) / (numCorrect + numWrong);

准确性方法有一个详细的参数,如果为 true,则会导致要显示的诊断信息︰ 

if (verbose == true)
{
  Console.Write("Input: ");
  for (int j = 0; j < data[i].Length - 1; ++j)
    Console.Write(data[i][j].ToString("F1") + " ");
  // Etc.
}

使用准确性方法中的代码,您可以编写专门的预测方法,它接受 x (不含已知的正确类) 的值、 定型数据、 错误计数器数组和返回 + 1 或-1 预测值的 RBF 内核 sigma 值的数组。

总结

内核感知不经常使用。这是神秘的由于在很大程度有的功能更强大的二进制分类方法可用,并且有大量的一般情况下在周围 ML 内核方法的事实。如果您做内核感知的 Internet 搜索,您会发现许多参考,以显示普通感知和内核感知,但很少的实际实现信息之间美观的数学关系。在我看来,了解内核感知的主要价值是,知识使其更易于理解较复杂的 ML 内核方法 (这在以后的专栏,我将介绍)。

内核感知的弱点之一是,要进行预测,以及整个定型数据集 (除外错误计数器的值为 0 的项) 必须检查。如果定型集非常大,这会使实时预测在某些情况下变得不可行。

内核感知无疑是最简单的内核方法类型。内核技巧可以应用于其他线性分类器。实际上,将内核技巧应用于最大边距线性分类器是支持向量机 (SVM) 分类,均在 90 年代末期中常见的基础。


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

衷心感谢以下 Microsoft 技术专家对本文的审阅: Ani Anirudh 和 Chris Lee


在 MSDN 杂志论坛讨论这篇文章