Der von mir sehr geschätzte René Schulte publizierte vor einer Weile ein Video, in dem er demonstrierte, wie man per Webcamstream und AI erkennt, ob Menschen eine Schutzbrille tragen. Leider find ich das Beispiel nicht mehr. Jedenfalls ging mir die Idee nicht aus dem Kopf. Leicht abgewandelt, keine Klobrille.
Ich möchte eine Wohnung suchen, bei der das Klo nicht im Bad ist. Ist fast das gleiche wie eine Schutzbrille, in Bad Bildern Toiletten zu suchen.
Dataset
Zunächst braucht man Trainingsdaten. Ich habe wild per Google-Suche Bilder heruntergeladen. Getrennt nach “Mit” und “Ohne” und in entsprechende Ordner gespeichert. Sind nur rund 50 nötig pro Label. Format und Größe sind dabei ganz egal.
Und los geht's mit dem Programmieren. Ein neues Visual Studio Command Line Projekt. Wir brauchen eine Reihe von Nuget Paketen. Kann man per Paket Manager Console, UI oder direkt in der Projekt Datei (rechtsklick bearbeiten) einfügen.
Man erkennt, dass wir ML.NET verwenden und für Vision den Tensor Flow. Wichtig ist, nicht die neueste Version von TensorFLow, sondern maximal 2.3.1. Andernfalls wird eine Laufzeitfehlermeldung geworfen ala
System.EntryPointNotFoundException: "Unable to find an entry point named 'TF_StringEncodedSize' in DLL 'tensorflow'."
Dazu habe ich auch noch die Trainingsdaten ins Projekt eingefügt. Als Datei und beim Compile und Deploy.
1: <ItemGroup>
2: <PackageReference Include="Microsoft.ML" Version="3.0.1" />
3: <PackageReference Include="Microsoft.ML.ImageAnalytics" Version="3.0.1" />
4: <PackageReference Include="Microsoft.ML.OnnxRuntime" Version="1.17.1" />
5: <PackageReference Include="Microsoft.ML.OnnxTransformer" Version="3.0.1" />
6: <PackageReference Include="Microsoft.ML.Vision" Version="3.0.1" />
7: <PackageReference Include="Microsoft.Windows.Compatibility" Version="9.0.0-preview.1.24081.3" />
8: <PackageReference Include="SciSharp.TensorFlow.Redist" Version="2.3.1" />
9: </ItemGroup>
10: <ItemGroup>
11: <Folder Include="images\Mit\" />
12: <Folder Include="images\Ohne\" />
13: </ItemGroup>
Beim Build sollten sich die Pakete installieren.
Los geht es mit der programm.cs. Erst werden die nötigen Namespaces eingebunden. Das sollte Intellisense per Vorschlag auch automatisch erledigen können.
using Microsoft.ML;
using Microsoft.ML.Data;
Wir steigern den Level und laden die Bilder in einer generische Liste. Der Pfad und das Label das wir dem Bild zugewiesen haben.
public class BadDaten
{
public string ImagePath { get; set; }
public string Label { get; set; }
}
Noch ein kleines C# Schippchen drauf und je Verzeichnis die Datenamen und Label in die Liste.
1: var images = new List<BadDaten>();
2: foreach (var imagePath in Directory.GetFiles(@"images/mit"))
3: {
4: images.Add(new BadDaten { ImagePath = imagePath, Label = "Mit" });
5: }
6: foreach (var imagePath in Directory.GetFiles(@"images/ohne"))
7: {
8: images.Add(new BadDaten { ImagePath = imagePath, Label = "Ohne" });
9: }
Ähnlich wie ein DBContext hilft ein MLContext die Daten zu halten, zu ändern um z.B. Spalten auszuwählen und die Vorhersagemodelle mit Parametern zu füllen.
var mlContext = new MLContext();
Nun werden die Daten geladen und in einen Training- und Testsatz aufgeteilt. Hier wird eine 80/20 Teilung gewählt. Ideal wäre es, wenn man eigene Validierungsdaten benutzen kann. Das ist schon ein Teil der Machine Learning Kunst, gute und passende Daten aufzubereiten. Wir sind da lazy und werden trotzdem phänomenale Trainingsresultate erhalten.
1: var dataView = mlContext.Data.LoadFromEnumerable(images);
2: var trainTestData = mlContext.Data.TrainTestSplit(dataView, testFraction: 0.2);
3: var trainingData = trainTestData.TrainSet;
4: var testData = trainTestData.TestSet;
Als nächstes wird die Pipeline konfiguriert. Das finde ich aus zwei Gründen sehr mühsam. Ich komm mit dieser Fluent Api nicht klar. Ich bevorzuge es, ein Objekt pro Schritt zu haben, um das Ergebnis nachvollziehen zu können. Die Pipeline ist eine Black Box.
Noch viel schwerer finde ich Zeichenketten als Parameter zu verwenden. Die internen Typen matchen nicht direkt mit C# Typen. Man kann sehr viel falsch machen und erhält Fehlermeldungen in klingonisch.
1: var pipeline = mlContext.Transforms.Conversion.MapValueToKey(
inputColumnName: "Label", outputColumnName: "LabelAsKey")
2: .Append(mlContext.Transforms.LoadRawImageBytes(outputColumnName: "Image",
3: imageFolder: ".",
4: inputColumnName: nameof(BadDaten.ImagePath)))
5: .Append(mlContext.MulticlassClassification.Trainers.ImageClassification(
6: labelColumnName: "LabelAsKey",
7: featureColumnName: "Image"))
8: .Append(mlContext.Transforms.Conversion.MapKeyToValue("PredictedLabel", "PredictedLabel")); // Id zu string
In Zeile 1 werden Daten bzw. Felder vorbereitet. Das Label ist ein Text, aber intern werden nur Zahlen benutzt. Als aus Mit wird 1 (wild geraten).
Zeile 2 lädt das Bild als Byte Array. Input ist der volle Pfad inklusive Name und Extension jedes einzelnen Bilds. In der Pipeline wird das Objekt dann “Image” benannt und kann von folgenden Schritte damit angesprochen werden.
Zeile 5 startet den Trainer für Vision auf Basis von Tensorflow. Wir wissen das Label mit Key und das Feature Image aus dem vorigen Schritten
Zeile 8 wandelt die ID des Keys wieder zurück in eine Zeichenkette um.
Ich mach hier mal einen kurzen Break. Aus dem laufenden Projekt zeige ich hier den Debug View, wenn das Training durch ist. Den Code haben wir noch nicht.
Wir sehen verschiedene Eigenschaften der Ausgabe mit ganz speziellen Datentypen. Ein Teil der Eigenschaften sind einfach so vorhanden, weil Tensorflow das so will und ein andere Teil stamm aus der Pipeline wie PredictedLabel.
Um diese Daten in ein C# Objekt “abholen” zu können, braucht es eine Klasse. Hier hole ich nur zwei Felder. Am Ende der programm.cs einfügen
1: public class BadPrediction
2: {
3: [ColumnName("PredictedLabel")]
4: public string PredictedLabel { get; set; }
5: [ColumnName("Score")]
6: public float[] PredictedScore { get; set; }
7: }
Die Columname müssen dabei den Namen in dem Screenshot entsprechen. Die Typen auch. So wird Vector Single zum Float Array.
An den nächsten Zeilen habe ich stundenlang gekaut. Es geistern verschiedene Versionen durchs Web.
Falsch mlContext.MulticlassClassification.Evaluate(predictions, "LabelAsKey", "PredictedLabel");
erzeugt Schema mismatch for score column 'PredictedLabel': expected vector of two or more items of type Single, got String (Parameter 'schema')
1: var trainedModel = pipeline.Fit(trainingData);
2: var predictions = trainedModel.Transform(testData);
3: var metrics = mlContext.MulticlassClassification.Evaluate(predictions, labelColumnName:"LabelAsKey",
4: scoreColumnName:"Score",
5: predictedLabelColumnName:"PredictedLabel");
Zeile 1 Die Methode Fit benötigt Daten als IdataView Objekt. Diese wurden vorher per LoadFromIenumerable geladen. Es gibt für andere Daten auch andere Methoden. Diese Methode trainiert das Modell auf den angegebenen Trainingsdaten (trainingData). Die Trainingsdaten werden durch die Pipeline verarbeitet, und der Lernalgorithmus lernt aus diesen Daten, um Vorhersagen zu treffen.
An dieser Stelle könnte man das trainiert Modell per Save tatsächlich in eine ZIP Datei speichern und später verwenden. In der Regel wird eine Anwendung dies in der Praxis so auch tun. Solche Zip sind sehr groß.
Nun wird es Zeit zu prüfen wie gut das Model ist. In Zeile 2 wendet das trainierte Modell auf neue, unbekannte Daten an (testData), um Vorhersagen zu generieren. Das Ergebnis predictions enthält die ursprünglichen Testdaten zusammen mit den hinzugefügten Vorhersagespalten.
In Zeile 3 wird die Bewertung ausgeführt und das Label, der Score und das textuelle Label erzeugt und in metrics gefüllt. Siehe Screenshot des Debuggers.
Es gibt verschiedene Metriken um danach das Model zu bewerten. Die hier gewählten sollten sich in Richtung 1 bewegen.
Console.WriteLine($"Macro accuracy: {metrics.MacroAccuracy:F2}");
Console.WriteLine($"Micro accuracy: {metrics.MicroAccuracy:F2}");
Modell nutzen
Rein fiktiv haben wir das Model vorher per Save als Zip gesichert. Genauso würde ich das nun per mlContext.Model.Load wieder laden. Ist ja schon da und ich hänge den weiteren Code einfach so dran. In der der program.cs.
Es wird eine Vorhersage Engine erzeugt, ein Testbild geladen, die Vorhersage ausgeführt und in Zeile 7 das Ergebnis ausgegeben.
1: var predictionEngine=mlContext.Model.CreatePredictionEngine<BadDaten,BadPrediction>(trainedModel);
2: var TestBild = new BadDaten
3: {
4: ImagePath = @"c:\temp\badtest.jpg"
5: };
6: var prediction2 = predictionEngine.Predict(TestBild);
7: Console.WriteLine($"Ergebnis: {prediction2.PredictedLabel} {prediction2.PredictedScore.Max()}");
Zur Erinnerung, die Felder von Predection mussten von mir in der BadPredection Klasse erstellt und per Annotation dem Output entsprechen. Dann kann man auch typisiert damit arbeiten.
Das Bild
und das sagt die Vision AI Vorhersage dazu
Ist mit Klo mit Score 95%. Da ziehe ich nicht ein.