监督学习






2.33/5 (2投票s)
一篇关于监督学习的文章。
监督学习是机器学习的两个主要分支之一。从某种意义上说,它与人类学习新技能的方式相似:有人向我们展示如何做,然后我们就能通过模仿来学习。在监督学习算法的情况下,我们通常需要大量的示例,也就是说,大量提供算法输入和预期输出的数据。算法将从这些数据中学习,然后根据它以前未见过的新输入来预测输出。
令人惊讶的是,有许多问题都可以通过监督学习来解决。许多电子邮件系统都使用它来自动将电子邮件分类为重要或不重要,只要新邮件到达收件箱。更复杂的例子包括图像识别系统,它们可以仅从输入像素值中识别图像包含的内容。
这些系统首先从人类手动标记的大量图像数据集中学习,然后能够自动对全新的图像进行分类。甚至可以使用监督学习来自动驾驶汽车绕赛道行驶:算法首先学习人类驾驶员如何控制车辆,然后最终能够复制这种行为。
到本文结束时,您将能够使用 Go 实现两种类型的监督学习:
- 分类,其中算法必须学会将输入分类为两个或多个离散类别。我们将构建一个简单的图像识别系统来演示其工作原理。
- 回归,其中算法必须学会预测一个连续变量,例如网站上待售商品的價格。在我们的示例中,我们将根据房屋的位置、大小和年龄等输入来预测房价。
在本文中,我们将涵盖以下主题:
- 何时使用回归和分类
- 如何使用 Go 机器学习库实现回归和分类
- 如何衡量算法的性能
我们将涵盖构建监督学习系统涉及的两个阶段:
- 训练,这是学习阶段,我们使用带标签的数据来校准算法。
- 推理或预测,我们使用训练好的算法来实现其预期目的:从输入数据中进行预测。
分类
在开始任何监督学习问题时,第一步是加载和准备数据。我们将从加载MNIST时尚数据集开始,该数据集包含各种服装的小型灰度图像。我们的任务是构建一个系统,能够识别每张图像中的内容;也就是说,它是否包含连衣裙、鞋子、外套等?
首先,我们需要通过在代码仓库中运行 *download-fashion-mnist.sh* 脚本来下载数据集。然后,我们将把它加载到 Go 中。
import (
"fmt"
mnist "github.com/petar/GoMNIST"
"github.com/kniren/gota/dataframe"
"github.com/kniren/gota/series"
"math/rand"
"github.com/cdipaolo/goml/linear"
"github.com/cdipaolo/goml/base"
"image"
"bytes"
"math"
"github.com/gonum/stat"
"github.com/gonum/integrate"
)
set, err := mnist.ReadSet("../datasets/mnist/images.gz", "../datasets/mnist/labels.gz")
让我们先看看一些图像样本。每张图像的大小为 28 x 28 像素,每个像素的值在 0 到 255 之间。我们将使用这些像素值作为算法的输入:我们的系统将接受来自图像的 784 个输入,并使用它们根据图像包含的服装类别对其进行分类。在 Jupyter 中,您可以如下查看图像:
set.Images[1]
这将显示数据集中的一张 28 x 28 图像,如下图所示:
为了使数据适合机器学习算法,我们需要将其转换为数据框格式。首先,我们将从数据集中加载前 1,000 张图像。
func MNISTSetToDataframe(st *mnist.Set, maxExamples int) dataframe.DataFrame {
length := maxExamples
if length > len(st.Images) {
length = len(st.Images)
}
s := make([]string, length, length)
l := make([]int, length, length)
for i := 0; i < length; i++ {
s[i] = string(st.Images[i])
l[i] = int(st.Labels[i])
}
var df dataframe.DataFrame
images := series.Strings(s)
images.Name = "Image"
labels := series.Ints(l)
labels.Name = "Label"
df = dataframe.New(images, labels)
return df
}
df := MNISTSetToDataframe(set, 1000)
我们还需要一个 `string` 数组,其中包含每张图像的可能标签。
categories := []string{"tshirt", "trouser", "pullover",
"dress", "coat", "sandal", "shirt", "shoe", "bag", "boot"}
开始时,预留一小部分数据以测试完成的算法非常重要。这使我们能够衡量算法在新数据上的表现如何,而这些数据并未在训练中使用。如果不这样做,您很可能会构建一个在训练期间表现良好但在面对新数据时表现不佳的系统。首先,我们将使用 75% 的图像来训练我们的模型,25% 的图像来测试它。
请注意,在使用监督学习时,将数据拆分为训练集和测试集是一个至关重要的步骤。通常会预留 20-30% 的数据用于测试,但如果您的数据集非常大,则可能可以使用更少的比例。
使用上一章中的 `Split (df dataframe.DataFrame, valFraction float64)` 函数来准备这两个数据集。
training, validation := Split(df, 0.75)
一个简单的模型——逻辑分类器
解决我们问题的最简单的算法之一是逻辑分类器。这就是数学家所说的线性模型,我们可以通过一个简单的例子来理解它:我们试图将下面两个图上的点分类为圆形或方形。线性模型将尝试通过画一条直线来分离这两种点。这在左侧的图表中效果很好,其中输入(图表轴)和输出(圆形或方形)之间的关系很简单。然而,在右侧的图表中,它不起作用,因为无法用一条直线将点正确地分成两组。
在面对新的机器学习问题时,建议您从线性模型作为基线开始,然后将其与其他模型进行比较。虽然线性模型无法捕捉输入数据中的复杂关系,但它们易于理解,并且通常可以快速实现和训练。您可能会发现线性模型对您正在处理的问题来说已经足够了,从而节省了实现更复杂模型的时间。如果没有,您可以尝试不同的算法,并使用线性模型来了解它们的效果有多好。
请注意,基线是一个简单的模型,您可以在比较不同的机器学习算法时将其用作参考点。
回到我们的图像数据集,我们将使用逻辑分类器来决定图像是否包含裤子。首先,让我们做一些最终的数据准备:将标签简化为“裤子”(真)或“不是裤子”(假)。
func EqualsInt(s series.Series, to int) (*series.Series, error) {
eq := make([]int, s.Len(), s.Len())
ints, err := s.Int()
if err != nil {
return nil, err
}
for i := range ints {
if ints[i] == to {
eq[i] = 1
}
}
ret := series.Ints(eq)
return &ret, nil
}
trainingIsTrouser, err1 := EqualsInt(training.Col("Label"), 1)
validationIsTrouser, err2 := EqualsInt(validation.Col("Label"), 1)
if err1 != nil || err2 != nil {
fmt.Println("Error", err1, err2)
}
我们还将对像素数据进行归一化,因此,它将不再存储为 0 到 255 之间的整数,而是表示为 0 到 1 之间的浮点数。
请注意,许多监督机器学习算法只有在数据经过归一化(即缩放到 0 到 1 之间)后才能正常工作。如果您在让算法正确训练时遇到问题,请确保您已正确归一化数据。
func NormalizeBytes(bs []byte) []float64 {
ret := make([]float64, len(bs), len(bs))
for i := range bs {
ret[i] = float64(bs[i])/255.
}
return ret
}
func ImageSeriesToFloats(df dataframe.DataFrame, col string) [][]float64 {
s := df.Col(col)
ret := make([][]float64, s.Len(), s.Len())
for i := 0; i < s.Len(); i++ {
b := []byte(s.Elem(i).String())
ret[i] = NormalizeBytes(b)
}
return ret
}
trainingImages := ImageSeriesToFloats(training, "Image")
validationImages := ImageSeriesToFloats(validation, "Image")
在正确准备好数据后,终于可以创建逻辑分类器并对其进行训练了。
model := linear.NewLogistic(base.BatchGA, 1e-4, 1, 150,
trainingImages, trainingIsTrouser.Float())
//Train
err := model.Learn()
if err != nil {
fmt.Println(err)
}
性能衡量
现在我们有了训练好的模型,我们需要通过将模型对每张图像的预测与真实情况(图像是否为裤子)进行比较来衡量其性能。一种简单的方法是衡量准确率。
准确率衡量算法可以正确分类的输入数据的比例,例如,90%,如果算法的 100 次预测中有 90 次是正确的。
在我们的 Go 代码示例中,我们可以通过循环遍历验证数据集并计算正确分类的图像数量来测试模型。这将输出 98.8% 的模型准确率。
//Count correct classifications
var correct = 0.
for i := range validationImages {
prediction, err := model.Predict(validationImages[i])
if err != nil {
panic(err)
}
if math.Round(prediction[0]) == validationIsTrouser.Elem(i).Float() {
correct++
}
}
//accuracy
correct / float64(len(validationImages))
精确率和召回率
衡量准确率可能具有误导性。假设您正在构建一个系统来分类医学患者是否会检测出一种罕见疾病的阳性,而数据集中只有 0.1% 的示例实际上是阳性的。一个非常糟糕的算法可能会预测没有人会检测出阳性,但它仍然具有 99.9% 的准确率,仅仅因为这种疾病很罕见。
请注意,一个分类比另一个分类具有更多示例的数据集被称为不平衡。在衡量算法性能时,需要谨慎处理不平衡数据集。
衡量性能的更好方法是将算法的每个预测放入以下四类之一:
现在我们可以定义一些新的性能指标:
- 精确率衡量模型预测的正确预测中有多少比例实际上是正确的。在下图表中,它是模型预测的真阳性(圆的左侧)除以模型的所有阳性预测(圆中的所有内容)。
- 召回率衡量模型识别所有阳性示例的能力。换句话说,它是真阳性(圆的左侧)除以所有实际为阳性的数据点(整个左侧)。
前面的图表显示了模型在中心圆中预测为真的数据点。实际为真的点位于图表的左半部分。
请注意,在处理不平衡数据集时,精确率和召回率是更稳健的性能指标。两者都在 0 到 1 之间,其中 1 表示完美性能。
以下是计算真阳性和假阴性总数的代码。
//Count true positives and false negatives
var truePositives = 0.
var falsePositives = 0.
var falseNegatives = 0.
for i := range validationImages {
prediction, err := model.Predict(validationImages[i])
if err != nil {
panic(err)
}
if validationIsTrouser.Elem(i).Float() == 1 {
if math.Round(prediction[0]) == 0 {
// Predicted false, but actually true
falseNegatives++
} else {
// Predicted true, correctly
truePositives++
}
} else {
if math.Round(prediction[0]) == 1 {
// Predicted true, but actually false
falsePositives++
}
}
}
现在我们可以使用以下代码计算精确率和召回率:
//precision
truePositives / (truePositives + falsePositives)
//recall
truePositives / (truePositives + falseNegatives)
对于我们的线性模型,我们得到 100% 的精确率,这意味着没有假阳性,召回率为 90.3%。
ROC 曲线
衡量性能的另一种方法是更详细地了解分类器的工作方式。在我们的模型内部,会发生两件事:
- 首先,模型计算一个介于 0 和 1 之间的值,表示给定图像被分类为裤子的可能性。
- 设置一个阈值,以便只有得分高于阈值的图像才会被分类为裤子。设置不同的阈值可以以召回率为代价提高精确率,反之亦然。
如果我们查看模型在所有 0 到 1 之间的阈值上的输出,我们可以更深入地了解其有用性。我们使用称为接收者操作特征 (ROC) 曲线的东西来实现这一点,它是在不同阈值下,整个数据集上的真阳性率与假阳性率的图。以下三个示例显示了坏、中等和非常好分类器的 ROC 曲线:
通过测量这些 ROC 曲线下的阴影区域,我们可以获得模型有多好的简单度量,称为曲线下面积 (AUC)。对于差的模型,它接近0.5,但对于非常好的模型,它接近1.0,表明模型可以同时实现高真阳性率和低假阳性率。
gonum/stat 包提供了一个有用的函数来计算 ROC 曲线,一旦我们将模型扩展到处理数据集中每种不同的服装,我们就会使用它。
请注意,接收者操作特征,或 ROC 曲线,是不同阈值下的真阳性率与假阳性率的图。它使我们能够可视化模型在分类方面的能力。AUC 提供了分类器有多好的简单度量。
多类别模型
到目前为止,我们一直在使用二元分类;也就是说,如果图像显示裤子,则输出真,否则输出假。对于某些问题,例如判断电子邮件是否重要,这已经足够了。但在本例中,我们真正想要的是一个能够识别数据集中所有不同类型服装的模型,即衬衫、靴子、连衣裙等。
对于某些算法实现,我们将需要首先对输出应用独热编码。但是,在我们的示例中,我们将使用 softmax 回归在 *goml*/ *linear* 中,它会自动执行此步骤。我们可以通过简单地将输入(像素值)和整数输出(0、1、2... 代表 T 恤、裤子、套头衫等)馈送到模型来训练模型。
model2 := linear.NewSoftmax(base.BatchGA, 1e-4, 1, 10, 100,
trainingImages, training.Col("Label").Float())
//Train
err := model2.Learn()
if err != nil {
fmt.Println(err)
}
当使用此模型进行推理时,它将为每个类别输出一个概率向量;也就是说,它告诉我们输入图像是 T 恤、裤子等等。这正是我们进行 ROC 分析所需要的,但如果我们想要每个图像的单一预测,我们可以使用以下函数来找到具有最高概率的类别。
func MaxIndex(f []float64) (i int) {
var (
curr float64
ix int = -1
)
for i := range f {
if f[i] > curr {
curr = f[i]
ix = i
}
}
return ix
}
接下来,我们可以绘制每个类别的 ROC 曲线和 AUC。以下代码将循环遍历验证数据集中的每个示例,并使用新模型为每个类别预测概率。
//create objects for ROC generation
//as per https://godoc.org/github.com/gonum/stat#ROC
y := make([][]float64, len(categories), len(categories))
classes := make([][]bool, len(categories), len(categories))
//Validate
for i := 0; i < validation.Col("Image").Len(); i++ {
prediction, err := model2.Predict(validationImages[i])
if err != nil {
panic(err)
}
for j := range categories {
y[j] = append(y[j], prediction[j])
classes[j] = append(classes[j],
validation.Col("Label").Elem(i).Float() != float64(j))
}
}
//Calculate ROC
tprs := make([][]float64, len(categories), len(categories))
fprs := make([][]float64, len(categories), len(categories))
for i := range categories {
stat.SortWeightedLabeled(y[i], classes[i], nil)
tprs[i], fprs[i] = stat.ROC(0, y[i], classes[i], nil)
}
现在我们可以计算每个类别的 AUC 值,这表明我们的模型在某些类别上的表现优于其他类别。
for i := range categories {
fmt.Println(categories[i])
auc := integrate.Trapezoidal(fprs[i], tprs[i])
fmt.Println(auc)
}
对于裤子,AUC 值为 0.96,表明即使是简单的线性模型在这种情况下也工作得很好。然而,衬衫和套头衫的得分都接近 0.6。这在直观上是有意义的:衬衫和套头衫看起来非常相似,因此模型很难正确识别它们。通过绘制每个类别的 ROC 曲线作为单独的线,我们可以更清楚地看到这一点:模型在衬衫和套头衫上的表现明显最差,而在形状非常独特的服装(靴子、裤子、凉鞋等)上的表现最好。
以下代码加载 gonum 的绘图库,创建 ROC 图,并将其另存为 JPEG 图像。
import (
"gonum.org/v1/plot"
"gonum.org/v1/plot/plotter"
"gonum.org/v1/plot/plotutil"
"gonum.org/v1/plot/vg"
"bufio"
)
func plotROCBytes(fprs, tprs [][]float64, labels []string) []byte {
p, err := plot.New()
if err != nil {
panic(err)
}
p.Title.Text = "ROC Curves"
p.X.Label.Text = "False Positive Rate"
p.Y.Label.Text = "True Positive Rate"
for i := range labels {
pts := make(plotter.XYs, len(fprs[i]))
for j := range fprs[i] {
pts[j].X = fprs[i][j]
pts[j].Y = tprs[i][j]
}
lines, points, err := plotter.NewLinePoints(pts)
if err != nil {
panic(err)
}
lines.Color = plotutil.Color(i)
lines.Width = 2
points.Shape = nil
p.Add(lines, points)
p.Legend.Add(labels[i], lines, points)
}
w, err := p.WriterTo(5*vg.Inch, 4*vg.Inch, "jpg")
if err != nil {
panic(err)
}
if err := p.Save(5*vg.Inch, 4*vg.Inch, "Multi-class ROC.jpg"); err != nil {
panic(err)
}
var b bytes.Buffer
writer := bufio.NewWriter(&b)
w.WriteTo(writer)
return b.Bytes()
}
如果我们查看 Jupyter 中的图表,我们可以看到最差的类别紧随对角线附近的线,这再次表明 AUC 接近 0.5。
非线性模型——支持向量机
为了继续前进,我们需要使用一种不同的机器学习算法:一种能够模拟像素输入和输出类别之间更复杂、非线性关系的算法。虽然一些主流的 Go 机器学习库(如 Golearn)支持局部最小二乘等基本算法,但没有一个库支持像 Python 的 scikit-learn 或 R 的标准库那样广泛的算法集。因此,通常有必要寻找实现绑定到广泛使用的 C 库的替代库,或者包含适用于特定问题的算法的可配置实现的库。
在本例中,我们将使用一种称为支持向量机 (SVM) 的算法。SVM 比线性模型更难使用,它们有更多的参数需要调整,但它们能够模拟数据中更复杂的模式。
请注意,SVM 是一种更高级的机器学习方法,可用于分类和回归。它们允许我们对输入数据应用核函数,这意味着它们可以模拟输入/输出之间的非线性关系。
SVM 模型的一个重要特性是它们使用核函数的能力。简单来说,这意味着算法可以对输入数据进行转换,从而找到非线性模式。在我们的示例中,我们将使用LIBSVM 库在图像数据上训练 SVM。LIBSVM 是一个开源库,支持多种语言的绑定,这意味着如果您想移植在 Python 流行库 scikit-learn 中构建的模型,它也很有用。首先,我们需要进行一些数据准备,使我们的输入/输出数据适合馈送到 Go 库。
trainingOutputs := make([]float64, len(trainingImages))
validationOutputs := make([]float64, len(validationImages))
ltCol:= training.Col("Label")
for i := range trainingImages {
trainingOutputs[i] = ltCol.Elem(i).Float()
}
lvCol:= validation.Col("Label")
for i := range validationImages {
validationOutputs[i] = lvCol.Elem(i).Float()
}
// FloatstoSVMNode converts a slice of float64 to SVMNode
// with sequential indices starting at 1
func FloatsToSVMNode(f []float64) []libsvm.SVMNode {
ret := make([]libsvm.SVMNode, len(f), len(f))
for i := range f {
ret[i] = libsvm.SVMNode{
Index: i+1,
Value: f[i],
}
}
//End of Vector
ret = append(ret, libsvm.SVMNode{
Index: -1,
Value: 0,
})
return ret
}
接下来,我们可以设置 SVM 模型并使用径向基函数 (RBF) 核进行配置。RBF 核是使用 SVM 的常见选择,但训练时间比线性模型长。
var (
trainingProblem libsvm.SVMProblem
validationProblem libsvm.SVMProblem
)
trainingProblem.L = len(trainingImages)
validationProblem.L = len(validationImages)
for i := range trainingImages {
trainingProblem.X = append(trainingProblem.X, FloatsToSVMNode(trainingImages[i]))
}
trainingProblem.Y = trainingOutputs
for i := range validationImages {
validationProblem.X = append(validationProblem.X, FloatsToSVMNode(validationImages[i]))
}
validationProblem.Y = validationOutputs
// configure SVM
svm := libsvm.NewSvm()
param := libsvm.SVMParameter{
SvmType: libsvm.CSVC,
KernelType: libsvm.RBF,
C: 100,
Gamma: 0.01,
Coef0: 0,
Degree: 3,
Eps: 0.001,
Probability: 1,
}
最后,我们可以将模型拟合到 750 张图像的训练数据,然后使用 `svm.SVMPredictProbability` 来预测概率,正如我们在多类别线性模型中所做的那样。
model := svm.SVMTrain(&trainingProblem, ¶m)
正如我们之前所做的那样,我们计算 AUC 和 ROC 曲线,这表明该模型在各个类别上的表现都更好,包括像衬衫和套头衫这样的困难类别。
过拟合和欠拟合
SVM 模型在验证数据集上的表现比线性模型好得多,但是,为了了解下一步该怎么做,我们需要介绍两个重要的机器学习概念:过拟合和欠拟合。它们都指的是在训练模型时可能出现的问题。
如果模型欠拟合数据,则它过于简单而无法解释输入数据中的模式,因此在针对训练数据集和验证数据集进行评估时表现不佳。这种情况的另一个术语是模型具有高偏差。如果模型过拟合数据,则它过于复杂,无法很好地泛化到未包含在训练中的新数据点。这意味着模型在针对训练数据进行评估时表现良好,但在针对验证数据集进行评估时表现不佳。这种情况的另一个术语是模型具有高方差。
理解过拟合和欠拟合之间差异的一个简单方法是查看以下简单示例:在构建模型时,我们的目标是构建一个适合数据集的东西。左侧的示例欠拟合,因为直线模型无法准确区分圆形和方形。右侧的模型过于复杂:它正确地分离了所有圆形和方形,但不太可能在新数据上表现良好。
我们的线性模型出现了欠拟合:它过于简单,无法模拟所有类别之间的差异。查看 SVM 的准确率,我们可以看到它在训练数据上得分为 100%,但在验证数据上仅得分为 82%。这是一个明确的迹象表明它正在过拟合:与训练时相比,它在新图像的分类能力要差得多。
处理过拟合的一种方法是使用更多训练数据:即使是复杂的模型,如果训练数据集足够大,也不可能过拟合。另一种方法是引入正则化:许多机器学习模型都有一个您可以调整以减少过拟合的参数。
深度学习
到目前为止,我们通过使用 SVM 改进了模型的性能,但仍然面临两个问题:
- 我们的 SVM 过拟合了训练数据。
- 它也很难扩展到完整的 60,000 张图像数据集:尝试用更多图像训练最后一个示例,您会发现它变得慢得多。如果我们使数据点数量加倍,SVM 算法需要两倍以上的时间。
在本节中,我们将使用深度神经网络来解决这个问题。这些类型的模型在图像分类任务以及许多其他机器学习问题上取得了最先进的性能。它们能够模拟复杂的非线性模式,并且能够很好地扩展到大型数据集。
数据科学家通常使用 Python 来开发和训练神经网络,因为它能够访问像TensorFlow和Keras这样支持极好的深度学习框架。这些框架使构建复杂的神经网络并在大型数据集上训练它们比以往任何时候都更容易。它们通常是构建复杂的深度学习模型的最佳选择。在本节中,我们将使用 go-deep 库从头开始构建一个更简单的神经网络来演示关键概念。
神经网络
神经网络的基本构建块是神经元(也称为感知器)。它实际上与我们简单的线性模型相同:它根据以下公式将所有输入(即 x1、x2、x3... 等)组合成一个单一输出 y:
神经网络的魔力来自于这些简单的神经元组合在一起时发生的事情。
- 首先,我们创建许多神经元的层,我们将输入数据馈送到其中。
- 在每个神经元的输出处,我们引入一个激活函数。
- 然后,此输入层的输出被馈送到另一层神经元和激活,称为隐藏层。
- 这个过程会重复多次隐藏层——层越多,网络就越深。
- 最终的输出层神经元将网络的结果合并为最终输出。
- 使用一种称为 `backpropagation` 的技术,我们可以通过查找每个神经网络的权重 `w0, w1, w2`... 来训练网络,这些权重允许整个网络拟合训练数据。
下图显示了这种布局:箭头表示每个神经元的输出,这些输出馈送到下一层神经元的输入。
该网络中的神经元被认为是全连接或密集层。计算能力和软件的最新进展使研究人员能够构建和训练比以往任何时候都更复杂的神经网络架构。例如,最先进的图像识别系统可能包含数百万个单独的权重,并且需要许多天的计算时间来训练所有这些参数以拟合大型数据集。它们通常包含不同的神经元排列,例如,在卷积层中,这些层在这些类型的系统中执行更专业的学习。
在实践中成功使用深度学习所需的大部分技能涉及对如何选择和调整网络以获得良好性能的广泛理解。有许多博客和在线资源提供了有关这些网络工作方式以及它们已应用于的问题类型的更多详细信息。
神经网络中的全连接层是指每个神经元的输入连接到前一层所有神经元的输出。
一个简单的深度学习模型架构
构建成功的深度学习模型的大部分技能在于选择正确的模型架构:层的数量/大小/类型以及每个神经元的激活函数。在开始之前,值得研究一下是否有人已经使用深度学习解决了与您类似的问题,并发布了一个效果良好的架构。一如既往,最好从简单开始,然后迭代地修改网络以提高其性能。
在我们的示例中,我们将从以下架构开始:
- 一个输入层
- 两个隐藏层,每个包含 128 个神经元
- 一个输出层,包含 10 个神经元(对应于数据集中每个输出类别一个)
- 隐藏层中的每个神经元将使用整流线性单元 (ReLU) 作为其输出函数。
ReLU 是神经网络中激活函数的常见选择。它们是引入模型非线性的一种非常简单的方法。其他常见的激活函数包括 `logistic` 函数和 `tanh` 函数。
go-deep 库让我们能够非常快速地构建此架构。
import (
"github.com/patrikeh/go-deep"
"github.com/patrikeh/go-deep/training"
)
network := deep.NewNeural(&deep.Config{
// Input size: 784 in our case (number of pixels in each image)
Inputs: len(trainingImages[0]),
// Two hidden layers of 128 neurons each, and an output layer 10 neurons
// (one for each class)
Layout: []int{128, 128, len(categories)},
// ReLU activation to introduce some additional non-linearity
Activation: deep.ActivationReLU,
// We need a multi-class model
Mode: deep.ModeMultiClass,
// Initialise the weights of each neuron using normally distributed random numbers
Weight: deep.NewNormal(0.5, 0.1),
Bias: true,
})
神经网络训练
训练神经网络是另一个需要进行技巧性调整以获得良好结果的领域。训练算法通过计算模型在小批量训练数据上的拟合程度(称为损失),然后对权重进行微小调整以改进拟合。然后,此过程会在不同的训练数据批次上一次又一次地重复。学习率是一个重要的参数,它控制算法调整神经元权重的速度。
在训练神经网络时,算法会反复将所有输入数据馈送到网络中,并在进行过程中调整网络权重。每次完整地通过数据称为一个epoch。
在训练神经网络时,请在每个 epoch 后监视网络的准确率和损失(准确率应增加,损失应减少)。如果准确率没有提高,请尝试降低学习率。继续训练网络,直到准确率不再提高:此时,网络据说已经收敛。
以下代码使用 0.006 的学习率进行 500 次迭代来训练我们的模型,并打印每个 epoch 后的准确率。
// Parameters: learning rate, momentum, alpha decay, nesterov
optimizer := training.NewSGD(0.006, 0.1, 1e-6, true)
trainer := training.NewTrainer(optimizer, 1)
trainer.Train(network, trainingExamples, validationExamples, 500)
// training, validation, iterations
该神经网络在训练集和验证集上提供了 80% 的准确率,这是一个好迹象,表明模型没有过拟合。
在本文中,我们涵盖了许多内容,并探索了许多重要的机器学习概念。处理监督学习问题的第一步是收集和预处理数据,确保其已归一化并拆分为训练集和验证集。在这里,我们涵盖了一系列不同的分类算法。
如果您觉得本文有用,您可能还会发现《使用 Go 进行机器学习快速入门指南》一书很有帮助。本书可以帮助您在 Go 中高效地开发机器学习应用程序。通过它,您将能够理解 ML 所解决的问题类型以及各种方法。您还可以使用 gonum/plot 和 Gophernotes 进行数据可视化。如果您想学习如何为成功设置机器学习项目,这是您的正确选择。