2017 年 11 月

第 33 卷,第 11 期

测试运行 - 使用 C# 进行内核逻辑回归

作者 James McCaffrey

James McCaffrey

内核逻辑回归 (KLR) 是一种机器学习技术,可用于二进制预测。例如,KLR 可以根据预测变量(如年龄、收入和现有债务数额)预测一个人是否会偿还贷款(无法偿还 = 0,成功偿还 = 1)。KLR 是普通逻辑回归的高级变体。

了解本文所述观点的一个好方法是查看图 1 中的演示程序以及图 2 ****中的相关数据。演示程序的目标是预测具有两个预测变量(有时称为特征)x0 和 x1 的虚拟数据的类 0 或 1。例如,第一个定型数据项是 (2.0, 3.0, 0),这意味着如果预测值是 x0 = 2.0 和 x1 = 3.0,则正确的类是 0。KLR 可以使用任意数量的预测变量处理数据,但仅使用两个预测变量就可以轻松展示该技术。

21 个定型数据点具有圆形几何形状,这意味着简单的线性分类技术(如普通逻辑回归)是无效的。这种数据称为非线性可分数据。

在后台,KLR 使用称为径向基函数 (RBF) 核的函数。RBF 核函数有一个叫做 sigma 的参数。sigma 的值必须通过反复试验来确定,并且 sigma 在演示中设置为 1.0。定型 KLR 模型是一个迭代过程,演示将最大迭代次数设置为 1,000,并将学习率 eta 设置为 0.001。

定型 KLR 模型会为每个定型数据项创建一组“alpha 值”,以及一个附加“偏差”值。演示程序显示前三个定型项 (-0.3071, -0.3043, -0.3071) 和最后两个定型项 (0.8999, 0.6108) 的 alpha 值以及偏差值 (-1.0722)。

经过定型,KLR 模型能够正确预测所有 21个 数据项。然后将模型应用于四个测试数据项,在图 2 中显示为黑点。第一个测试项输入值为 (1.5, 4.5) 和正确类为 0。预测模型正确地预测出该项,以及其他三个测试项。

本文假定你具有中等或高级编程技能,但不一定了解 KLR。虽然演示程序使用 C# 进行编码,但如有必要,将代码重构为其他语言(如 Java 或 Python)应该不会遇到太多麻烦。因该演示程序过长,无法在此全部展示,但你可以在本文随附的 文件下载中获取完整的源代码。

RBF 核

内核函数测量两个向量或数组的相似度。我之前提到的 RBF 核函数是最常见的核函数,并且是演示程序所使用的类型。RBF 值为 1.0 表示两个向量是相同的。较小的 RBF 值表示两个向量不那么相似。

RBF 公式为:

K(v1, v2) = exp( - || v1 - v2 ||^2 / (2 * sigma^2) )

在公式中,K 表示内核;v1 和 v2 是两个具有相同长度的向量;sigma 是一个值为 1.0 或 1.5 的参数;|| 表示欧几里得距离;而 exp 函数是欧拉数 (e = 2.71828) 次方。

通过举例可以很好地解释 RBF 函数。假设 v1 = (3.0, 1.0, 2.0),v2 = (1.0, 0.0, 5.0),sigma 为 1.5。首先,计算平方欧几里得距离:

|| v1 - v2 ||^2 = (3.0 - 1.0)^2 + (1.0 - 0.0)^2  + (2.0 - 5.0)^2

                      = 4.0 + 1.0 + 9.0

                      = 14.0

接下来,将平方距离除以 sigma 平方的 2 倍:

14.0 / (2 * (1.5)^2) = 14.0 / 4.5 = 3.11

最后,找到欧拉数,并将其变为之前结果的负数:

K(v1, v2) = e^(-3.11) = 0.0446

小的内核值表示 v1 和 v2 不是很相似。演示程序将 RBF 核函数定义为:

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

该函数假定每个数组的最后一个单元格包含类标签(0 或 1),因此最后一个单元格不包含在计算过程中。KLR 使用内核函数将给定数据项与所有定型项进行比较,并使用该信息来确定预测的类标签。

普通逻辑回归

通过举例可以很好地解释 RBF 函数普通逻辑回归 (LR)。假设你具有三个预测变量:x0 = 2.5, x1 = 1.7, and x2 = 3.4。正则化 LR 模型创建一组称为权重 (wi) 的数值常量(可用于每个预测变量)以及一个称为偏差 (b) 的附加数值常量。请注意,正则化 LR 中的偏差与图 1 ****中显示的 KLR 偏差不同。 

内核逻辑回归演示

图 1 内核逻辑回归演示

假设 w0 = 0.11,w1 = 0.33,w2 = 0.22,b = 0.44。要预测输入数据 (2.5, 1.7, 3.4) 的类标签 0 或 1,首先要计算每个 x 及其相关 w 的乘积之和,然后加上偏差:

z = (2.5)(0.11) + (1.7)(0.33) + (3.4)(0.22) + 0.44

   = 2.024

接下来,计算 p = 1.0 / (1.0 + exp(-z)):

p = 1.0 / (1.0 + exp(-2.024))

   = 0.8833

p 值是数据项具有类标签 = 1 的概率,因此,如果 p 小于 0.5,则预测值为 0,如果 p 大于 0.5(如本例中那样),则预测值为 1。

好的,但正则化 LR 中的权重和偏差值是如何计算得出的?方法是通过使用已知输入值和已知的正确类标签的一组定型数据来确定权重和偏差值,然后使用优化算法来找到权重和偏差值,这样就可精确匹配预测的类标签与已知的正确标签值。有许多算法可用于寻找正则化 LR 的权重和偏差值,包括带有对数似然的梯度上升法、带有平方误差的梯度下降法、迭代牛顿-拉弗森方法、单纯形优化法、L-BFGS 和粒子群优化。

正则化 LR 的主要缺点是它只能处理线性可分的数据。正则化 LR 无法处理非线性可分的数据,如图 2 中显示的演示数据。

内核逻辑回归定型数据

图 2 内核逻辑回归定型数据

了解内核逻辑回归

通过举例可以很好地解释 KLR。首先我说明一下,乍看上去,KLR 与普通 LR 并没有很密切的关系。但是,这两种技术在数学上密切相关。

假设只有四个定型数据项:

td[0] = (2.0, 4.0, 0)

td[1] = (4.0, 1.0, 1)

td[2] = (5.0, 3.0, 0)

td[3] = (6.0, 7.0, 1)

你的目标是预测 x = (3.0, 5.0) 的类标签。假设经过定型的 KLR 模型给出 alpha 值和偏差值:alpha[0] = -0.3,alpha[1] = 0.4,alpha[2] = -0.2,alpha[3] =0.6,b = 0.1。

第一步是计算数据项之间的 RBF 相似度来预测每个定型项:

K(td[0], x) = 0.3679

K(td[1], x) = 0.0002

K(td[2], x) = 0.0183

K(td[3], x) = 0.0015

请注意,此时,x 与 td[0] 和 td[2] 最相似,它们都具有类标签 0。接下来,计算每个 K 值和关联 alpha 的乘积之和,然后加上偏差值:

z = (0.3679)(-0.3) + (0.0002)(0.4) + (0.0183)(-0.2) + (0.0015)(0.6) + 0.1

   = -0.1120

现在计算 p = 1.0 / (1.0 + exp(-z)):

p = 1.0 / (1.0 + exp(0.1120))

   = 0.4720

如果 p 值大于 0.5,则预测类为 1,并且如果 p 值小于 0.5,则预测类为 0,如此示例所示(几乎不存在)。

定型 KLR 模型

定型 KLR 模型是使用定型数据来查找 alpha 值和偏差值的过程。如果在高级伪代码中表达的话,KLR 定型算法如下所示:

compute K(td[i], td[j]) for all i, j
loop maxIter times
  for-each curr training item, i
    for-each j: sum += alphas[j] * K(i j)
    sum += bias
    y = 1.0 / (1.0 + exp(-sum))
    t = target class (0 or 1)
    for-each j:
      alpha[j] += eta * (t - y) * K(i, j)
    bias += eta * (t - y) * 1.0
end-loop

演示代码中的关键语句是 alphas[j] += eta * (t - y) * kernelMatrix[i][j],它基于索引 [i] 处的当前定型数据项来更新索引 [j] 处的定型数据项的 alpha 值。此处,t 是已知的正确目标类 0 或 1,y 是计算出的 [i] 中项具有类 1 的概率。

例如,假设定型项的 alpha 值目前是 0.1234,目标类是 1,则计算的概率是 0.60。目前预测是正确的,但是你希望 p 值更趋近于 1。假设两项之间的相似度为 K(i, j) = 0.70,学习率 eta 为 0.10。则新 alpha 值为:

alpha = 0.1234 + 0.10 * (1 - 0.60) * 0.70

          = 0.1234 + 0.0280

          = 0.1514

因为 alpha 是概率计算中的乘数值,所以稍大些的新 alpha 值会使 p 稍微变大,从而使得预测更准确。

演示程序

为了对演示程序进行编码,我启动了 Visual Studio,并新建了名为 KernelLogistic 的 C# 控制台应用程序。虽然我使用的是 Visual Studio 2015,但演示程序并不非常依赖 .NET Framework,因此任何 Visual Studio 的新发布版本都可以正常运行。

在编辑器窗口中加载模板代码后,我右键单击了“解决方案资源管理器”窗口中的 Program.cs 文件,并将此文件重命名为“KernelLogisticProgram.cs”,然后就是允许 Visual Studio 为我自动重命名类 Program。在模板生成的代码顶部,我删除了所有不必要的 using 语句,仅留下引用顶级 System 命名空间的语句。接下来,我实例化一个 Random 对象:

using System;
namespace KernelLogistic
{
  class KernelLogisticProgram
  {
    static Random rnd = new Random(0);
    static void Main(string[] args)
    {
      Console.WriteLine(“Begin KLR demo”);
      int numFeatures = 2;
...

简单起见,我使用静态方法而不是面向对象的编程来编写演示程序,并删除了所有正态误差检查。Main 方法设置 21 个定型项和 4 个测试项,如下所示:

double[][] trainData = new double[21][];
trainData[0] = new double[] { 2.0, 3.0, 0 };
...
trainData[20] = new double[] { 5.0, 6.0, 1 };
double[][] testData = new double[4][];
testData[0] = new double[] { 1.5, 4.5, 0 };
...
testData[3] = new double[] { 5.5, 5.5, 1 };

在非演示场景中,可能会从文本文件中读取数据。接下来,将初始化 alpha 值:

int numTrain = trainData.Length;
int numTest = testData.Length;
double[] alphas = new double[numTrain + 1];
for (int i = 0; i < alphas.Length; ++i)
  alphas[i] = 0.0;

在编码机器学习系统时,处理偏差值的方式通常有数种。在这里,我将 KLR 偏差存储在 alpha 数组的最后一个单元格中。另一种设计是创建单独的独立变量。接下来,计算所有定型项对之间的内核相似度:

double[][] kernelMatrix = new double[numTrain][];
for (int i = 0; i < kernelMatrix.Length; ++i)
  kernelMatrix[i] = new double[numTrain];
double sigma = 1.0;
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;
  }
}

因为只有 21 个数据项,为了简单起见,我选择了牺牲效率。通过使用 K(v1, v2) = K(v2, v1),而 K(v, v) = 1 的事实,我可以减少内核计算的次数。接下来,演示程序准备用于定型:

double eta = 0.001;
int iter = 0;
int maxIter = 1000;
int[] indices = new int[numTrain];
for (int i = 0; i < indices.Length; ++i)
  indices[i] = i;

eta 和 maxIter 的值将通过反复试验来确定。针对名为 index 的数组的设想是,在定型时,针对每次传递以随机顺序访问定型项目,从而避免陷入定型停顿或来回摆动的情况。主定型循环以如下程序开始:

while (iter < maxIter) {
  Shuffle(indices); 
  for (int idx = 0; idx < indices.Length; ++idx) {
    int i = indices[idx];

Shuffle 方法是一个帮助程序,可使用 Fisher-Yates 迷你算法对定型项目的顺序进行干扰。目标类标签和当前定型项目的预测概率计算如下:

double sum = 0.0;
for (int j = 0; j < alphas.Length-1; ++j)
  sum += alphas[j] * kernelMatrix[i][j];
sum += alphas[alphas.Length - 1]; 
double y = 1.0 / (1.0 + Math.Exp(-sum));
double t = trainData[i][numFeatures];

注意,此设计假定类标签在定型数据数组的最后一个单元格中。接下来,更新 alpha 和 beta 值:

for (int j = 0; j < alphas.Length - 1; ++j)
      alphas[j] = alphas[j] +
        (eta * (t - y) * kernelMatrix[i][j]);
    alphas[alphas.Length-1] = alphas[alphas.Length - 1] +
      (eta * (t - y)) * 1; 
  }
  ++iter;
} // While (train)

更新偏差值使用虚拟值 1 代替内核相似度值,只是为了使关系的对称度清晰。当然,你可以删除乘数是 1 的乘法,因为它没有任何作用了。定型之后,会显示一些 alpha 值和偏差值,如图 1 所示。

最后,演示程序根据定型数据和测试数据计算经定型的 KLR 模型的分类准确性:

double accTrain = Accuracy(trainData, trainData,
  alphas, sigma, false);
Console.WriteLine(“accuracy = “ +
  accTrain.ToString(“F4”) + “\n”);
double accTest = Accuracy(testData, trainData,
  alphas, sigma, true); // Verbose

传递给 Accuracy 方法的布尔参数指示是以详细模式(使用诊断消息)还是免打扰模式进行计算。

总结

内核逻辑回归不经常使用,至少我的同事是不经常使用的。其主要优势是简单。KLR 的主要缺点是它不能很好地适应大型数据集,因为必须预先计算所有项到项内核相似度值并保存它们,或者必须保留所有定型数据,然后针对每个预测计算所有运行的相似度值。

KLR 是为二元分类而设计的。对 KLR 进行扩展以处理具有三个或更多类值的分类问题是有可能的,但在我看来,还存在可使用的更好替代方案,特别是单个隐藏层前馈神经网络。KLR 与 K 最近的邻域 (K-NN) 分类算法有一些相似之处,也支持向量机 (SVM) 分类。


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

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


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