November 2018

Band 33, Nummer 11

Testlauf: Einführung in die ML.NET-Bibliothek

Von James McCaffrey

James McCaffreyDie ML.NET-Bibliothek ist eine Open-Source-Sammlung von ML-Code (Machine Learning), der direkt in .NET-Anwendungen verwendet werden kann. Die meisten ML-Bibliotheken, etwa TensorFlow, Keras, CNTK und PyTorch, sind in Python geschrieben und rufen Low-Level-C++-Routinen auf. Wenn Sie jedoch eine Python-basierte Bibliothek verwenden, ist es für eine .NET-Anwendung nicht so einfach, auf ein trainiertes ML-Modell zuzugreifen. Glücklicherweise lässt sich die ML.NET-Bibliothek nahtlos in .NET-Anwendungen integrieren.

Wenn Sie erfahren möchten, womit sich dieser Artikel beschäftigt, werfen Sie am besten einen Blick auf das Demoprogramm in Abbildung 1. Das Demoprogramm erstellt ein ML-Modell, das basierend auf Alter, Geschlecht und Punktzahl des Patienten in einem medizinischen Test der Nieren vorhersagt, ob ein Patient sterben oder überleben wird. Da es nur zwei mögliche Ergebnisse gibt, nämlich sterben oder überleben, ist dies ein binäres Klassifizierungsproblem.

ML.NET-Demoprogramm in Aktion
Abbildung 1: ML.NET-Demoprogramm in Aktion

Hinter den Kulissen verwendet das Demoprogramm die ML.NET-Bibliothek, um ein logistisches Regressionsmodell zu erstellen und zu trainieren. Während ich diesen Artikel schreibe, befindet sich die ML.NET-Bibliothek noch im Vorschaumodus, sodass sich einige der hier vorgestellten Informationen geändert haben können, bis Sie dies lesen.

Das Demoprogramm verwendet einen Satz von Trainingsdaten mit 30 Elementen. Nach dem Trainieren des Modells wurde es auf die Quelldaten angewendet und erreichte eine Genauigkeit von 66,67 Prozent (20 richtige und 10 falsche Ergebnisse). Das Demoprogramm schließt mit der Verwendung des trainierten Modells ab, um das Ergebnis für einen 50-jährigen Mann mit einem Nierentestwert von 4,80 vorherzusagen: Die Vorhersage ist, dass der Patient überleben wird.

Dieser Artikel geht davon aus, dass Sie über mindestens mittlere Programmierkenntnisse in C# verfügen. Er geht aber nicht davon aus, dass Sie etwas über die ML.NET-Bibliothek wissen. Der vollständige Code und die Daten für das Demoprogramm werden in diesem Artikel vorgestellt und sind auch im zugehörigen Dateidownload verfügbar.

Das Demoprogramm

Um das Demoprogramm zu erstellen, habe ich Visual Studio 2017 gestartet. Die ML.NET-Bibliothek funktioniert mit der kostenlosen Community-Edition und den kommerziellen Editionen von Visual Studio 2017. Die ML.NET-Dokumentation besagt nicht ausdrücklich, dass Visual Studio 2017 erforderlich ist, aber ich konnte das Demoprogramm nicht dazu bewegen, dass es mit Visual Studio 2015 funktioniert. Ich habe ein neues C#-Konsolenanwendungsprojekt erstellt und ihm den Namen „Kidney“ gegeben. Die ML.NET-Bibliothek funktioniert mit einem klassischen .NET- oder einem .NET Core-Anwendungstyp.

Nachdem der Vorlagencode geladen wurde, habe ich mit der rechten Maustaste auf die Datei „Program.cs“ im Fenster des Projektmappen-Explorers geklickt und die Datei in „KidneyProgram.cs“ umbenannt, und ich habe Visual Studio erlaubt, die Klasse „Program“ automatisch für mich umzubenennen. Dann habe ich im Fenster des Projektmappen Explorers mit der rechten Maustaste auf das Kidney-Projekt geklickt und die Option „NuGet-Pakete verwalten“ ausgewählt. Im NuGet-Fenster habe ich die Registerkarte „Durchsuchen“ ausgewählt und dann „ML.NET“ in das Suchfeld eingegeben. Die ML.NET-Bibliothek befindet sich im Paket „Microsoft.ML“. Ich habe dieses Paket ausgewählt und auf die Schaltfläche „Installieren“ geklickt. Nach einigen Sekunden hat Visual Studio mit der Meldung „Microsoft.ML 0.3.0 für Kidney erfolgreich installiert“ geantwortet.

An diesem Punkt habe ich die Aktion „Erstellen > Projektmappe neu erstellen“ ausgeführt und eine Fehlermeldung „Unterstützt nur x64-Architekturen“ erhalten. Im Fenster des Projektmappen-Explorers habe ich mit der rechten Maustaste auf das Kidney-Projekt geklickt und den Eintrag „Eigenschaften“ ausgewählt. Im Eigenschaftenfenster habe ich auf der linken Seite die Registerkarte „Erstellen“ ausgewählt und dann den Eintrag „Plattformziel“ aus „Beliebige CPU“ in „x64“ geändert. Ich habe auch sichergestellt, dass ich die Version 4.7 von .NET Framework als Ziel verwende. Bei früheren Versionen erhielt ich einen Fehler im Zusammenhang mit einer der Abhängigkeiten der mathematischen Bibliothek und musste die globale CSPROJ-Datei manuell bearbeiten. Dumm gelaufen. Dann habe ich „Erstellen > Projektmappe neu erstellen“ ausgeführt und war erfolgreich. Wenn Sie mit Vorschaumodusbibliotheken wie ML.NET arbeiten, können Sie erwarten, dass solche Probleme eher die Regel als die Ausnahme sind.

Die Demodaten

Nach der Erstellung des Skeletts des Demoprogramms bestand der nächste Schritt darin, die Datei mit den Trainingsdaten zu erstellen. Die Daten werden in Abbildung 2 gezeigt. Im Fenster des Projektmappen-Explorers habe ich mit der rechten Maustaste auf das Kidney-Projekt geklickt und dann „Hinzufügen > Neues Element“ ausgewählt. Im Dialogfenster „Neues Element“ habe ich den Typ „Textdatei“ ausgewählt und die Datei „KidneyData.txt“ genannt. Wenn Sie dies nachvollziehen möchten, kopieren Sie die Daten aus Abbildung 2 und fügen sie in das Editor-Fenster ein, wobei Sie darauf achten müssen, dass keine zusätzlichen nachfolgenden Leerzeilen vorhanden sind.

Abbildung 2: Nierendaten des Kidney-Projekts

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

Das 30-teilige Dataset ist synthetisch und sollte weitgehend selbsterklärend sein. Das Feld „sex“ (Geschlecht) ist mit -1 als „Männlich“ und mit +1 als „Weiblich“ codiert. Da die Daten drei Dimensionen aufweisen (Alter, Geschlecht, Testergebnis), ist es nicht möglich, sie in einem zweidimensionalen Diagramm darzustellen. Aber Sie können sich eine gute Vorstellung von der Struktur der Daten machen, indem Sie das Diagramm nur der Alters- und Nierentestwerte in Abbildung 3 untersuchen. Das Diagramm deutet darauf hin, dass die Daten linear trennbar sein können. 

Nierendaten
Abbildung 3: Nierendaten

Der Programmcode

Der vollständige Democode (mit einigen kleinen Änderungen, um Platz zu sparen) wird in Abbildung 4 dargestellt. Oben im Editor-Fenster habe ich alle Namespaceverweise entfernt und durch die in der Codeliste angezeigten ersetzt. Die verschiedenen Microsoft.ML-Namespaces enthalten die gesamte ML.NET-Funktionalität. Der System.Threading.Tasks-Namespace ist erforderlich, um ein trainiertes ML.NET-Modell in einer Datei zu speichern oder es zu laden.

Abbildung 4: ML.NET-Beispielprogramm

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

Das Demoprogramm definiert eine Klasse namens „KidneyData“, die innerhalb der Hauptprogrammklasse geschachtelt ist und die interne Struktur der Trainingsdaten definiert. Die erste Spalte lautet beispielsweise folgendermaßen:

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

Beachten Sie, dass das Altersfeld als Typ „float“ und nicht als Typ „double“ deklariert ist. In ML ist der Typ „float“ der numerische Standardtyp, da die Erhöhung der Genauigkeit, die Sie durch die Verwendung des Typs „double“ erhalten, fast nie die sich daraus ergebenden Speicher- und Leistungskosten wert ist. Der vorherzusagende Wert muss den Namen „Label“ verwenden, aber Prädiktorfeldnamen können beliebige Namen aufweisen.

Das Demoprogramm definiert eine geschachtelte Klasse namens „KidneyPrediction“, um Modellvorhersagen zu speichern:

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

Der Spaltenname „PredictedLabel“ ist erforderlich. Wie gezeigt muss aber der zugehörige Zeichenfolgenbezeichner nicht übereinstimmen.

Erstellen und Trainieren des Modells

Das Demoprogramm erstellt ein ML-Modell mithilfe dieser sieben Anweisungen:

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" });

Sie können sich das Pipelineobjekt als untrainiertes ML-Modell plus die für das Training des Modells benötigten Daten vorstellen. Erinnern Sie sich daran, dass die zu prognostizierenden Werte in der Datendatei entweder „survive“ (überleben) oder „die“ (sterben) sind. Da ML-Modelle nur numerische Werte verstehen, wird die seltsam benannte Dictionarizer-Klasse verwendet, um zwei Zeichenfolgen in 0 oder 1 zu codieren. Der ColumnConcatenator-Konstruktor kombiniert die drei Prädiktorvariablen zu einem Aggregat. Die Verwendung eines Zeichenfolgen-Ergebnisparameters mit dem Namen „Features“ ist erforderlich.

Es gibt viele verschiedene ML-Techniken, die Sie für ein binäres Klassifizierungsproblem verwenden können. Ich benutze die logistische Regression im Demoprogramm, um die Grundideen so klar wie möglich zu halten, denn es ist wohl die einfachste und grundlegendste Form von ML. Andere von ML.NET unterstützte binäre Klassifikationsalgorithmen sind AveragedPerceptronBinaryClassifier, FastForestBinaryClassifier und LightGbmClassifier.

Das Demoprogramm trainiert und speichert das Modell mithilfe dieser Anweisungen:

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

Die Train-Methode der ML.NET-Bibliothek ist sehr anspruchsvoll. Wenn Sie sich an den Screenshot in Abbildung 1 erinnern, können Sie erkennen, dass Train eine automatische Normalisierung der Prädiktorvariablen durchführt, die die Methode so skaliert, dass große Prädiktorwerte (beispielsweise das Jahreseinkommen einer Person) nicht kleine Werte (z.B. die Anzahl der Kinder einer Person) überdecken. Die Train-Methode verwendet auch Regularisierung, eine erweiterte Technik, um die Genauigkeit eines Modells zu verbessern. Kurz gesagt, führt ML.NET alle Arten der erweiterten Verarbeitung durch, ohne dass Sie Parameterwerte explizit konfigurieren müssen.

Speichern und Auswerten des Modells

Nachdem das Modell trainiert wurde, wird es folgendermaßen auf dem Datenträger gespeichert:

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

Da die WriteAsync-Methode asynchron ist, ist es nicht so einfach, sie aufrufen. Der von mir bevorzugte Ansatz ist die gezeigte Wrappertechnik. Das Fehlen einer nicht asynchronen Methode zum Speichern eines ML.NET-Modells ist ein wenig überraschend, selbst für eine Bibliothek, die sich im Vorschaumodus befindet.

Das Demoprogramm geht davon aus, dass sich das ausführbare Programm zwei Verzeichnisse unterhalb des Projektstammverzeichnisses befindet. In einem Produktivsystem möchten Sie überprüfen, ob das Zielverzeichnis vorhanden ist. Normalerweise möchte ich in einem ML.NET-Projekt ein Unterverzeichnis „Data“ und ein Unterverzeichnis „Model“ im Stammordner des Projekts (in diesem Beispiel „Kidney“) erstellen und meine Daten und Modelle in diesen Verzeichnissen speichern.

Das Modell wird mit diesen Anweisungen ausgewertet:

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") + "%");

In den meisten ML-Szenarien verwenden Sie zwei Datendateien: ein Dataset, das nur für das Training verwendet wird, und ein zweites Testdataset, das nur für die Modellauswertung verwendet wird. Der Einfachheit halber verwendet das Demoprogramm die einzelne Datendatei mit 30 Elementen für die Modellauswertung erneut.

Die Evaluate-Methode gibt ein Objekt zurück, das mehrere Metriken enthält, darunter Protokollverlust, Genauigkeit, Abruf, F1-Bewertung usw. Das Rückgabeobjekt verfügt auch über ein praktisches ConfusionMatrix-Objekt, das verwendet werden kann, um Anzahlangaben wie die Anzahl der Patienten anzuzeigen, deren Überleben vorhergesagt wurde, die aber tatsächlich gestorben sind.

Verwenden des trainierten Modells

Das Demoprogramm zeigt, wie das trainierte Modell verwendet werden kann, um eine Vorhersage zu treffen:

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);

Beachten Sie, dass die numerischen Literale für Alter, Geschlecht und Nierenbewertung den Modifizierer „f“ verwenden, da das Modell Werte vom Typ „float“ erwartet. In diesem Beispiel war das trainierte Modell verfügbar, da das Programm soeben das Training beendet hat. Wenn Sie eine Vorhersage aus einem anderen Programm treffen möchten, würden Sie das trainierte Modell mit der ReadAsync-Methode mit den folgenden Zeilen laden:

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

Zusammenfassung

Obwohl die ML.NET-Bibliothek neu ist, reichen ihre Ursprünge viele Jahre zurück. Kurz nach der Einführung von Microsoft .NET Framework im Jahr 2002 startete Microsoft Research ein Projekt namens „Text Mining Search and Navigation“ (kurz TMSN), um Softwareentwicklern die Einbindung von ML-Code in Microsoft-Produkte und -Technologien zu ermöglichen. Das Projekt war sehr erfolgreich, und im Laufe der Jahre nahm die Größe und Nutzung intern bei Microsoft zu. Irgendwann um 2011 wurde die Bibliothek in „The Learning Code“ (TLC) umbenannt. TLC ist bei Microsoft weit verbreitet und befindet sich zurzeit in Version 3.9. Die ML.NET-Bibliothek ist ein direkter Ableger von TLC, wobei Microsoft-spezifische Funktionen entfernt wurden. 


Dr. James McCaffreyist in Redmond (Washington, USA) für Microsoft Research tätig. Er hat an verschiedenen Microsoft-Produkten mitgearbeitet, unter anderem an Internet Explorer und Bing. Dr. McCaffrey erreichen Sie unter jamccaff@microsoft.com.

Unser Dank gilt den folgenden technischen Experten von Microsoft für die Durchsicht dieses Artikels: Chris Lee und Ricky Loynd


Diesen Artikel im MSDN Magazine-Forum diskutieren