2018 年 11 月

第 33 卷,第 11 期

测试运行-ML.NET 库介绍

通过James McCaffrey

James McCaffreyML.NET 库是代码的一个开放源集合的机器学习 (ML) 可以直接在.NET 应用程序中使用。大多数机器学习库,如 TensorFlow、 Keras、 CNTK、 和 PyTorch,用 Python 和调用到低级别 c + + 例程。但是,如果使用基于 Python 的库,它不如此轻松地访问已训练的机器学习模型的.NET 应用程序。幸运的是,ML.NET 库无缝集成到.NET 应用程序。

了解本文所述观点的最佳方式是查看图 1 中的演示程序。演示创建预测患者是否将死或幸存根据患者的年龄、 性别和肾脏检测医疗测试分数上的机器学习模型。因为只有两个可能的结果,死或幸存,这是一个二元分类问题。

ML.NET 演示程序中操作
图 1 ML.NET 演示程序中操作

在后台,演示程序使用 ML.NET 库来创建并定型逻辑回归模型。在我写这篇文章,ML.NET 库仍处于预览模式时,所以此处提供的信息的一些可能正在阅读本文时已更改。

该演示含有 30 项使用定型数据集。对模型进行训练后,它已应用于源数据,并实现 66.67%的精确度 (20 正确和 10 个问题)。最后,演示程序会使用训练的模型来预测的 4.80 50 岁的男性肾脏测试的结果,该预测是病人可以幸存。

本文假定您有中等或更好地编程技能与C#,但并不假定您知道 ML.NET 库有关的任何信息。完整的代码和演示计划的数据在本文中显示和下载随附的文件中也会提供。

演示程序

若要创建演示程序,我启动了 Visual Studio 2017。ML.NET 库会使用免费的社区版或任何 Visual Studio 2017 的商业版本。Visual Studio 2017 是必需的但我无法获取演示程序以使用 Visual Studio 2015,ML.NET 文档不会显式声明。我创建了一个新C#控制台应用程序项目,然后将其命名为肾脏检测。ML.NET 库将使用经典版.NET 或.NET Core 应用程序类型。

加载模板代码后,我右键单击解决方案资源管理器窗口中的 Program.cs 文件和文件重命名为 KidneyProgram.cs 和我允许 Visual Studio 自动为我重命名类 Program。接下来,在解决方案资源管理器窗口中,右键单击肾脏检测项目,然后选择管理 NuGet 包选项。在 NuGet 窗口中,我选择浏览选项卡,然后在搜索字段中输入"ML.NET"。ML.NET 库位于 Microsoft.ML 包。我选择该包,并单击安装按钮。几秒钟后 Visual Studio 做出了"已成功安装的 Microsoft.ML 0.3.0 肾脏检测到"消息。

我在这里生成 |重新生成解决方案并收到了"支持 x64 体系结构"错误消息。在解决方案资源管理器窗口中,肾脏检测项目中,右键单击,然后选择属性项。在属性窗口中,我选择在左侧,生成选项卡,然后更改"x64"。 从"任何 CPU"的目标平台入口 我还已确保我所针对的.NET Framework 4.7 版本。与早期版本,我收到错误与一个数学库依赖项相关,且必须手动编辑全局.csproj 文件。呃。然后我未生成 |重新生成解决方案,并且已成功。当使用 ML.NET 等的预览模式库时,应像这样为规则而不是异常的问题。

演示数据

在创建后,演示程序的框架下, 一步是创建定型数据文件。数据显示在图 2。在解决方案资源管理器窗口中,右键单击肾脏检测项目,然后选择添加 |新项。从新项目对话框窗口中,我选择文本的文件类型,并将其命名为 KidneyData.txt。如果您要按照中的数据复制图 2并将其粘贴到编辑器窗口中,注意不要有任何额外尾随空行。

图 2 肾脏检测数据

48, +1, 4.40, survive
60, -1, 7.89, die
51, -1, 3.48, survive
66, -1, 8.41, die
40, +1, 3.05, survive
44, +1, 4.56, survive
80, -1, 6.91, die
52, -1, 5.69, survive
56, -1, 4.01, survive
55, -1, 4.48, survive
72, +1, 5.97, survive
57, -1, 6.71, die
50, -1, 6.40, survive
80, -1, 6.67, die
69, +1, 5.79, survive
39, -1, 5.42, survive
68, -1, 7.61, die
47, +1, 3.24, survive
45, +1, 4.29, survive
79, +1, 7.44, die
44, -1, 2.55, survive
52, +1, 3.71, survive
55, +1, 5.56, die
76, -1, 7.80, die
51, -1, 5.94, survive
46, +1, 5.52, survive
48, -1, 3.25, survive
58, +1, 4.71, survive
44, +1, 2.52, survive
68, -1, 8.38, die

30 项数据集是人工,大多数情况下应该浅显易懂。性别字段被编码为 male =-1 和女性 = + 1。数据具有三个维度 (年龄、 性别,测试分数),因为它不能在二维图中显示。但可以通过检查的关系图的只是年龄和肾脏检测测试分数中获取数据的结构的一个好主意图 3。在关系图所示的数据可能是非线性可分。 

肾脏检测数据
图 3 肾脏检测数据

程序代码

完整的演示代码,使用了少量小幅改动,以节省空间,如下所示图 4。在编辑器窗口的顶部,我删除命名空间的所有引用,并替换列的代码部分中所示。各种 Microsoft.ML 命名空间存放所有 ML.NET 功能。System.Threading.Tasks 命名空间需要保存或加载训练的 ML.NET 模型与文件。

图 4 ML.NET 示例程序

using System;
using Microsoft.ML;
using Microsoft.ML.Data;
using Microsoft.ML.Runtime.Api;
using Microsoft.ML.Trainers;
using Microsoft.ML.Transforms;
using Microsoft.ML.Models;
using System.Threading.Tasks;
namespace Kidney
{
  class KidneyProgram
  {
    public class KidneyData
    {
      [Column(ordinal: "0", name: "Age")]
      public float Age;
      [Column(ordinal: "1", name: "Sex")]
      public float Sex;
      [Column(ordinal: "2", name: "Kidney")]
      public float Kidney;
      [Column(ordinal: "3", name: "Label")]
      public string Label;
    }
    public class KidneyPrediction
    {
      [ColumnName("PredictedLabel")]
      public string PredictedLabels;
    }
    static void Main(string[] args)
    {
      Console.WriteLine("ML.NET (v0.3.0 preview) demo run");
      Console.WriteLine("Survival based on age, sex, kidney");
      var pipeline = new LearningPipeline();
      string dataPath = "..\\..\\KidneyData.txt";
      pipeline.Add(new TextLoader(dataPath).
        CreateFrom<KidneyData>(separator: ','));
      pipeline.Add(new Dictionarizer("Label"));
      pipeline.Add(new ColumnConcatenator("Features", "Age",
        "Sex", "Kidney"));
      pipeline.Add(new Logistic​Regression​Binary​Classifier());
      pipeline.Add(new
        PredictedLabelColumnOriginalValueConverter()
        { PredictedLabelColumn = "PredictedLabel" });
      Console.WriteLine("\nStarting training \n");
      var model = pipeline.Train<KidneyData,
        KidneyPrediction>();
      Console.WriteLine("\nTraining complete \n");
      string ModelPath = "..\\..\\KidneyModel.zip";
      Task.Run(async () =>
      {
        await model.WriteAsync(ModelPath);
      }).GetAwaiter().GetResult();
      var testData = new TextLoader(dataPath).
        CreateFrom<KidneyData>(separator: ',');
      var evaluator = new BinaryClassificationEvaluator();
      var metrics = evaluator.Evaluate(model, testData);
      double acc = metrics.Accuracy * 100;
      Console.WriteLine("Model accuracy = " +
        acc.ToString("F2") + "%");
      Console.WriteLine("Predict 50-year male, kidney 4.80:");
      KidneyData newPatient = new KidneyData()
        { Age = 50f, Sex = -1f, Kidney = 4.80f };
      KidneyPrediction prediction = model.Predict(newPatient);
      string result = prediction.PredictedLabels;
      Console.WriteLine("Prediction = " + result);
      Console.WriteLine("\nEnd ML.NET demo");
      Console.ReadLine();
    } // Main
  } // Program
} // ns

此示例程序定义一个名为 KidneyData,嵌套在主程序类,用于定义定型数据的内部结构。例如,第一列是:

[Column(ordinal: "0", name: "Age")]
public float Age;

请注意 age 字段声明类型 float 而不是类型 double。在机器学习,float 类型是默认数值类型,因为几乎从不需要生成内存和性能损失是提高获得使用双精度类型的精度。预测值必须使用名称"标签,"但预测值字段名称可以是任意命名。

演示程序定义了一个名为 KidneyPrediction 来保存模型预测的嵌套的类:

public class KidneyPrediction
{
  [ColumnName("PredictedLabel")]
  public string PredictedLabels;
}

列名称"PredictedLabel"是必需的但所示,关联的字符串标识符不必匹配。

创建和定型模型

此演示程序创建使用这些七个语句的机器学习模型:

var pipeline = new LearningPipeline();
string dataPath = "..\\..\\KidneyData.txt";
pipeline.Add(new TextLoader(dataPath).
  CreateFrom<KidneyData>(separator: ','));
pipeline.Add(new Dictionarizer("Label"));
pipeline.Add(new ColumnConcatenator("Features", "Age", "Sex", "Kidney"));
pipeline.Add(new Logistic​Regression​Binary​Classifier());
pipeline.Add(new PredictedLabelColumnOriginalValueConverter()
  { PredictedLabelColumn = "PredictedLabel" });

您可以将未训练的机器学习模型并加上训练该模型所需的数据的管道对象。请记住,值预测数据文件中是"保留"die"。 机器学习模型仅能理解数值,因为 weirdly 命名的 Dictionarizer 类用于编码为 0 或 1 的两个字符串。ColumnConcatenator 构造函数将三个预测因子变量组合到聚合;使用"功能"的名称的字符串结果参数是必需的。

有多种不同的机器学习技术可用于二元分类问题。演示程序中我使用逻辑回归来保持主要概念尽可能清楚明确,因为它可以说是机器学习的最简单且最基本形式。ML.NET 支持其他二元分类器算法包括 AveragedPerceptronBinaryClassifier、 FastForestBinaryClassifier 和 LightGbmClassifier。

演示程序训练,并将保存在模型中使用这些语句:

var model = pipeline.Train<KidneyData, KidneyPrediction>();
string ModelPath = "..\\..\\KidneyModel.zip";
Task.Run(async () =>
{
  await model.WriteAsync(ModelPath);
}).GetAwaiter().GetResult();

ML.NET 库 Train 方法是非常复杂。如果您回头参考中的屏幕截图图 1,可以看到训练执行自动缩放它们,以使大的预测因子值,例如一个人的年收入不反应较小的预测器变量正值,例如一个人的子项数。Train 方法还使用正则化,这是一项高级的技术以提高模型的准确性。简单地说,ML.NET 执行所有类型的高级处理,而无需显式配置参数值。

保存和评估模型

训练模型后,它保存到磁盘如下所示:

string ModelPath = "..\\..\\KidneyModel.zip";
Task.Run(async () =>
{
  await model.WriteAsync(ModelPath);
}).GetAwaiter().GetResult();

WriteAsync 方法是异步的因为它不是可以十分轻松地调用它。我更喜欢的方法是显示的包装器方法。若要保存 ML.NET 模型的非异步方法缺少是有点令人惊讶,即使对于在预览模式下的库。

演示程序假设程序可执行文件是项目根目录下的两个目录。在生产系统中,您想要验证的目标目录存在。通常情况下,在 ML.NET 项目中,我要创建数据的子目录,模型子目录关闭项目根文件夹 (在此示例中肾脏检测),并将我的数据和模型保存在这些目录。

使用以下语句评估模型:

var testData = new TextLoader(dataPath).
  CreateFrom<KidneyData>(separator: ',');
var evaluator = new BinaryClassificationEvaluator();
var metrics = evaluator.Evaluate(model, testData);
double acc = metrics.Accuracy * 100;
Console.WriteLine("Model accuracy = " +
  acc.ToString("F2") + "%");

在大多数机器学习方案中你将有两个数据文件 — 一个设置仅用于培训和第二个测试数据集仅用于模型评估。为简单起见,演示程序会重用模型评估的单个 30 项数据文件。

Evaluate 方法返回一个对象,包含多个指标,包括对数损失、 精度、 撤销、 F1 分数,等等。返回的对象也有一个巧妙的 ConfusionMatrix 对象,可以用于显示如患者的预测可以经受住,但实际上已停机数的计数。

使用已训练的模型

演示程序显示了如何使用经过训练的模型进行预测:

Console.WriteLine("Predict 50-year male kidney = 4.80:");
KidneyData newPatient = new KidneyData()
  { Age = 50f, Sex = -1f, Kidney = 4.80f };
KidneyPrediction prediction = model.Predict(newPatient);
string result = prediction.PredictedLabels;
Console.WriteLine("Prediction = " + result);

请注意,年龄、 性别和肾脏检测分数的数值文字使用"f"修饰符,因为该模型应为类型的浮点值。在此示例中,训练的模型不可用,因为该程序只需完成培训。如果你想要进行预测从其他程序,您将加载训练的模型使用 ReadAsync 方法的代码行:

PredictionModel<KidneyData,
  KidneyPrediction> model = null;
Task.Run(async () =>
{
  model2 = await PredictionModel.ReadAsync
  <KidneyData, KidneyPrediction>(ModelPath);
}).GetAwaiter().GetResult();

总结

即使 ML.NET 库是新的它的起源返回很多年。不久前才 2002 年的 Microsoft.NET Framework 引入,Microsoft Research 开始名为"文本挖掘搜索和导航"的项目或 TMSN,使软件开发人员能够在 Microsoft 产品和技术中包括机器学习代码。此项目非常成功,并在年增长的大小和在 Microsoft 内部使用情况。某处围绕 2011年库已重命名为"学习 code"(TLC)。TLC 广泛使用在 Microsoft 内部,并且当前处于 3.9 版本。ML.NET 库是 TLC,与删除的特定于 Microsoft 的功能直接分支。 


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

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


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