用Go进行基本的逻辑回归

511 阅读9分钟

我们将使用Logistic回归来创建一个非常基本的模型,这可以帮助我们解决分类问题

好吧,首先,分类问题是一个简单的问题,在这个问题中,我们试图根据现有的观察(数据)找出一个新的观察将(最可能)属于哪个类别。例如,你可以想一想,我们想把一颗痣或胎记归类为恶性或良性。我们可以使用过去见过的、我们知道是恶性还是良性的痣的图像来训练一个模型,并尝试预测一个新的胎记是否可能是癌症。

Logistic回归是我们将用来创建这个模型的算法。我只想说,这是一种非常适用于分类问题的算法。我们将在后面的代码示例中讨论该算法的几个方面,因为我们需要提供一些参数以使该算法正常工作,但现在我们知道这适合我们的问题,并且我们要使用的库正确地实现了它,这就足够了。

如上所述,这里的目标不是要完全理解Logistic回归的工作原理,而是要了解在试图用机器学习来解决问题时涉及到哪些一般步骤。我们也不会以任何方式处理大数据,而是与一个非常小的数据集(大约100个条目)合作,所以对于我们这个小编编造的例子来说,解释我们得到的确切结果将没有什么价值。但是,正如你可以想象的那样,这个例子中显示的代码和技术也会在其他更大的数据集上发挥作用。

我们将使用Go来做这个例子,但Go绝不是机器学习问题的首选语言。在这一领域,R,Matlab,python 和其他一些语言确实很出色。然而,Go通常与这些语言结合使用,对数据进行预处理和后处理,并对这些模型进行扩展,这正是Go所擅长的。

Go中也有一些机器学习库,但它们当然远远没有达到scikit-learn那样的成熟度。但对于我们的简单目的来说,它们是可以胜任的。在这个例子中,我使用了goml。我还看了一下golearn,它看起来也很有前途。

好了,让我们开始吧!

数据

首先,我们需要一些数据。在这个例子中,我们使用一个非常简单的、基于csv的数据集,看起来像这样。

exam1Score;exam2Score;accepted
45.3;38.2;1
99.1;88.1;0
...

这个数据集应该是代表一些学生在两次考试中的考试成绩,以及他们是否被他们参加考试的机构所录取。在这个小例子中,数据的意义并不重要,但在现实世界的应用中,理解数据并对其进行相应的处理往往是最重要的步骤之一。

现在,如果我们只是在我们训练的相同数据上测试我们的模型,它很可能会工作得很好,但这并不能告诉我们它在现实世界中的表现如何。出于这个原因,我们将数据分为训练集(约70%)和测试集(约30%)。我们将使用训练集来训练我们的模型,然后使用测试集来评估其性能。

在现实世界的例子中,我们会随机化我们的数据,并尝试不同的分割,但对于我们微薄的100条数据集来说,这并不重要,所以我们只是在两个文件中手动分离条目,并按如下方式加载它们。

xTrain, yTrain, err := base.LoadDataFromCSV("./data/studentsTrain.csv")
if err != nil {
    return err
}
xTest, yTest, err := base.LoadDataFromCSV("./data/studentsTest.csv")
if err != nil {
    return err
}

另外,在现实世界中,我们会进一步处理数据,例如,去除异常值或将数据正常化,但在我们的简单案例中,数据已经正常化了,我们可以直接继续。

我们也可以用gonum的图来创建一个数据的散点图来感受一下

func plotData(xTest [][]float64, yTest []float64) error {
    p, err := plot.New()
    if err != nil {
        return err
    }
    p.Title.Text = "Exam Results"
    p.X.Label.Text = "X"
    p.Y.Label.Text = "Y"
    p.X.Max = 120
    p.Y.Max = 120

    positives := make(plotter.XYs, len(yTest))
    negatives := make(plotter.XYs, len(yTest))
    for i := range xTest {
        if yTest[i] == 1.0 {
            positives[i].X = xTest[i][0]
                positives[i].Y = xTest[i][1]
        }
        if yTest[i] == 0.0 {
            negatives[i].X = xTest[i][0]
                negatives[i].Y = xTest[i][1]
        }
    }

    err = plotutil.AddScatters(p, "Negatives", negatives, "Positives", positives)
    if err != nil {
        return err
    }
    if err := p.Save(10*vg.Inch, 10*vg.Inch, "exams.png"); err != nil {
        return err
    }
    return nil
}

我们只是用我们的x 值来绘制不同的点,并根据它们相应的y 值给它们一个颜色。这样我们就可以很好地看到积极和消极的结果在哪里,以及它们是如何分布的。

这让我们看到了下面的图片(点击放大)。

image.png

该模型

使用goml ,学习模型是非常简单的。我们只需调用linear.NewLogistic ,它需要以下参数。

  • 优化方法 (base.BatchGA)
    • goml 有两种Logistic回归的优化算法,称为随机梯度上升分段梯度上升,基本上是梯度下降,只是倒置了。这些优化算法的目标是通过最小化误差("距离"),使模型与我们的训练数据尽可能地贴合。
  • 学习率和最大迭代 (0.00001 &1000)
    • 梯度下降法需要这两个参数,它们规定了算法走向最小值的步骤应该有多大(学习率),以及如果算法不能自行收敛,它应该使用的最大迭代量(最大迭代数)。如果我们将学习率设置得太高,可能会发生我们永远无法接近任何最小值的情况,所以在这里采取一个非常小的数字是安全的做法(尽管会有性能上的损失,因为我们需要更多的步骤)。
  • 正则化 (0)
    • 这是一个参数,我们可以用来防止过度拟合(将模型与我们的训练数据过于接近),但在这个例子中我们将忽略它。
  • 输入值 (xTrain - 考试分数)
  • 分类属性 (yTrain --录取的0和1的值)

一旦我们以后有了评估模型的方法,我们就可以开始试验这些参数来改进我们的模型,但现在我们只是按照以下方法训练我们的模型。

model := linear.NewLogistic(base.BatchGA, 0.00001, 0, 1000, xTrain, yTrain)
err := model.Learn()
if err != nil {
    return nil, nil, err
}

好了,现在我们有了训练好的模型--很简单,对吗?我们现在可以使用这个模型来预测新数据的分类,使用model.Predict(input []float) 。例如,如果我们训练了一个模型,根据图片中是否有猫来进行分类,那么对于一张新的图片,Predict 方法可以告诉我们,我们的模型是否认为其中有猫(概率)。

我们还可以使用这个Predict 方法来评估我们训练的模型在测试集上的表现。为了这个目的,我们将创建一个所谓的混淆矩阵,其数据结构如下。

// ConfusionMatrix describes a confusion matrix
type ConfusionMatrix struct {
    positive      int
    negative      int
    truePositive  int
    trueNegative  int
    falsePositive int
    falseNegative int
    accuracy      float64
}

混淆矩阵是一个误差矩阵--一个可以用来评估分类算法性能的表格。为了做到这一点,我们在评估期间记录一些数值。

  • positive - 正面例子的数量
  • negative - 负面例子的数量
  • truePositive - 我们预测正确的正面例子的数量
  • trueNegative - 我们预测正确的负面例子的数量
  • falsePositive - 我们错误地预测为正面的例子的数量
  • falseNegative - 我们错误地预测为负数的例子数量
  • accuracy - 衡量模型的准确性,定义为 ( + ) / ( + )truePositive trueNegative``positive negative

还有其他的衡量标准,如F1 score ,也经常被用来评估一个模型的性能,但在我们的案例中,我们将只使用accuracy

为了实现这一点,我们首先收集我们测试集的所有正负值。

cm := ConfusionMatrix{}
for _, y := range yTest {
    if y == 1.0 {
        cm.positive++
    }
    if y == 0.0 {
        cm.negative++
    }
}

然后,我们对测试集进行迭代,预测每个数据点的结果,并记录预测结果在混淆矩阵中的位置。有了这些记录,我们可以计算出我们模型的准确性。

(关于decisionBoundary :我们的模型的Predict() 方法产生的概率在0和1之间,我们需要指定在哪一点(超过哪一个值)一个例子应该被归类为positive 。我们可以先简单地选择50%作为例子,一旦我们能够评估这个模型,以后再加以改进)。

// Evaluate the Model on the Test data
for i := range xTest {
    prediction, err := model.Predict(xTest[i])
    if err != nil {
        return nil, nil, err
    }
    y := int(yTest[i])
    positive := prediction[0] >= decisionBoundary

   if y == 1 && positive {
       cm.truePositive++
   }
   if y == 1 && !positive {
       cm.falseNegative++
   }
   if y == 0 && positive {
       cm.falsePositive++
   }
   if y == 0 && !positive {
       cm.trueNegative++
   }
}

// Calculate Evaluation Metrics
cm.accuracy = (float64(cm.truePositive) + float64(cm.trueNegative)) /
    (float64(cm.positive) + float64(cm.negative))

现在我们能够在测试数据上评估我们的模型,我们实际上可以尝试修改这些值,以进一步提高我们的准确性。在实践中,我们当然会自动进行,测试每个相关参数的不同取值范围,直到我们找到最佳参数。这个步骤也可以很容易地并行化。

一个没有并行化的非常天真的实现,为决策边界尝试不同的值,可以是这样的。

var maxAccuracy float64
var maxAccuracyCM *ConfusionMatrix
var maxAccuracyDb float64
var maxAccuracyModel *linear.Logistic

//Try different parameters to get the best model
for db := 0.05; db < 1.0; db += 0.01 {
    // Learn Model and Calculate ConfusionMatrix for given values
    cm, model, err := tryValues(0.0001, 0.0, 1000, db, xTrain, xTest, yTrain, yTest)
    if err != nil {
        return err
    }
    if cm.accuracy > maxAccuracy {
        maxAccuracy = cm.accuracy
        maxAccuracyCM = cm
        maxAccuracyDb = db
        maxAccuracyModel = model
    }
}

fmt.Printf("Maximum accuracy: %.2f\n\n", maxAccuracy)
fmt.Printf("with Model: %s\n\n", maxAccuracyModel)
fmt.Printf("with Confusion Matrix:\n%s\n\n", maxAccuracyCM)
fmt.Printf("with Decision Boundary: %.2f\n", maxAccuracyDb)
fmt.Printf("with Num Iterations: %d\n", maxAccuracyIter)

我们可以对所有与模型相关的值进行测试,直到我们找到一个我们满意的模型。当然,在机器学习理论的世界里,也有不那么粗暴的方法来改进你的模型。

这就是了!这个简单程序的输出样本可以是这样的。

Running Logistic Regression...
Maximum accuracy: 0.91

with Model: h(θ,x) = 1 / (1 + exp(-θx))
θx = -1.286 + 0.04981(x[1]) + 0.01461(x[2])

with Confusion Matrix:
Positives: 24
Negatives: 11
True Positives: 23
True Negatives: 9
False Positives: 2
False Negatives: 1

Recall: 0.96
Precision: 0.92
Accuracy: 0.91


with Decision Boundary: 0.91
with Num Iterations: 2600

结论

在这篇文章中,我们只是触及了Go中机器学习的表面,但在我看来,这是一个有趣的小例子,人们可以在其中进行修补,看看不同的输入变量会带来怎样的结果。你也可以把这个代码例子作为一个模板,插入其他免费的数据集。

随着时间的推移,机器学习很可能会变得更加重要,所以学习它或者至少深入了解它背后的基本原理可能是一项不错的投资。除此之外,我还非常喜欢研究机器学习的问题,所以也是如此。)

正如介绍中提到的,Go不是机器学习的传统语言(还没有?)Go在未来是否会被更多地用于模型训练/评估,还有待观察,但这当然不是不可能。