通過 ML.NET 使用預訓練殘差網路 ResNet 模型實現手勢識別
之前我寫過的一篇《基於 ONNX 在 ML.NET 中使用 Pytorch 訓練的垃圾分類模型》,介紹到了 ML.NET 是如何實現影象分類的,此後我收到好多留言提出了更多的場景,比如某個線上學習應用,希望學生按照視訊的要求做一個指定的動作,完成形體訓練,又比如某個內部排程系統,希望通過某種肢體動作執行特定的命令,例如比個“OK”確認Job觸發,又或者是想實現一個猜拳的人機遊戲等等。不難發現這些場景相似性很高,從技術上我們可以分解為幾個過程,首先是通過opencv一類的工具捕獲影象,然後通過與目標影象的比對得到是否一致的分析結果,最後根據這個分析結果對場景的實際意義進行反饋,機器學習模型能夠解決第二個過程的需求。本文就摘取跟手勢有關的場景,介紹如何通過 ML.NET 使用預訓練殘差網路 ResNet 模型實現手勢識別。
為了展現 ML.NET 在普通機器上的 GPU 訓練能力,我準備的軟、硬體環境如下:
- Windows 10
- cuDNN 7.6 以及 CUDA10
- CPU Intel(R) Core(TM) i7-6700HQ
- GPU NVIDIA GeForce 940MX
準備階段
從https://aka.ms/mlnet-resources/meta/resnet_v2_101_299.meta下載 ResNet模型檔案,然後放置到 C:\Users\<Your Name>\AppData\Local\Temp\MLNET 下,否則專案執行時會出現如下圖的異常。
從https://cloud.tsinghua.edu.cn/f/787490e187714336aae2/?dl=1
建立專案
使用 Visual Studio 2017/2019 建立一個 .NET Core 控制檯應用專案,建立 assets 和 workspace 目錄,將資料集檔案 hand_dataset.tar 解壓到 assets目錄中。
新增如下 Nuget 包的引用:
- Microsoft.ML
- Microsoft.ML.ImageAnalytics
- Microsoft.ML.Vision
- SciSharp.TensorFlow.Redist-Windows-GPU
程式碼部分
在預處理資料階段,使用了mlContext.Data.ShuffleRows 混淆了順序,引數shufflePoolSize 的大小由資料集的大小決定,過小的值會導致管道的非同步執行緒拋異常,本例中訓練集有4500多張圖片,所以我定義的值為5000。
在建立ImageClassificationTrainer.Options 物件時,BatchSize 和Epoch 也要根據 GPU 的處理能力,以及前一次模型正確率調整,過高的值使得訓練過程加長且模型高度擬合,對未知資料的適應性會差。
完整的程式碼如下。
using System; using System.Collections.Generic; using System.Linq; using System.IO; using Microsoft.ML; using static Microsoft.ML.DataOperationsCatalog; using Microsoft.ML.Vision; namespace Gesture { class Program { static void Main(string[] args) { var projectDirectory = AppContext.BaseDirectory; var workspaceRelativePath = Path.Combine(projectDirectory, "workspace"); var assetsRelativePath = Path.Combine(projectDirectory, "assets"); MLContext mlContext = new MLContext(); /** * Train and Validate Data **/ IEnumerable<ImageData> images = LoadImagesFromFile(path: assetsRelativePath, category: "images/train.txt"); IDataView imageData = mlContext.Data.LoadFromEnumerable(images); IDataView shuffledData = mlContext.Data.ShuffleRows(imageData, seed: 123, shufflePoolSize: 5000); /** * Test Data **/ IEnumerable<ImageData> testImages = LoadImagesFromFile(path: assetsRelativePath, category: "images/test.txt"); IDataView testImageData = mlContext.Data.LoadFromEnumerable(testImages); var preprocessingPipeline = mlContext.Transforms.Conversion.MapValueToKey(inputColumnName: "Label", outputColumnName: "LabelAsKey") .Append(mlContext.Transforms.LoadRawImageBytes(outputColumnName: "Image", imageFolder: assetsRelativePath, inputColumnName: "ImagePath")); IDataView preProcessedData = preprocessingPipeline.Fit(shuffledData) .Transform(shuffledData); TrainTestData trainSplit = mlContext.Data.TrainTestSplit(data: preProcessedData, testFraction: 0.3); IDataView trainSet = trainSplit.TrainSet; IDataView validationSet = trainSplit.TestSet; IDataView testSet = preprocessingPipeline.Fit(testImageData) .Transform(testImageData); var classifierOptions = new ImageClassificationTrainer.Options() { FeatureColumnName = "Image", LabelColumnName = "LabelAsKey", ValidationSet = validationSet, Arch = ImageClassificationTrainer.Architecture.ResnetV2101, MetricsCallback = (metrics) => Console.WriteLine(metrics), TestOnTrainSet = false, ReuseTrainSetBottleneckCachedValues = true, ReuseValidationSetBottleneckCachedValues = true, WorkspacePath = workspaceRelativePath, BatchSize = 10, Epoch = 2000 }; var trainingPipeline = mlContext.MulticlassClassification.Trainers.ImageClassification(classifierOptions) .Append(mlContext.Transforms.Conversion.MapKeyToValue("PredictedLabel")); ITransformer trainedModel = trainingPipeline.Fit(trainSet); ClassifySingleImage(mlContext, testSet, trainedModel); ClassifyImages(mlContext, testSet, trainedModel); } public static IEnumerable<ImageData> LoadImagesFromFile(string path, string category = "images/train.txt") { var fullPath = Path.Combine(path, category); return File.ReadAllLines(fullPath) .Select(line => line.Split(' ')) .Select(line => new ImageData() { ImagePath = Path.Combine(path, line[0]), Label = line[1] }); }private static void OutputPrediction(ModelOutput prediction) { string imageName = Path.GetFileName(prediction.ImagePath); Console.WriteLine($"Image: {imageName} | Actual Value: {prediction.Label} | Predicted Value: {prediction.PredictedLabel}"); } public static void ClassifySingleImage(MLContext mlContext, IDataView data, ITransformer trainedModel) { PredictionEngine<ModelInput, ModelOutput> predictionEngine = mlContext.Model.CreatePredictionEngine<ModelInput, ModelOutput>(trainedModel); ModelInput image = mlContext.Data.CreateEnumerable<ModelInput>(data, reuseRowObject: true).First(); ModelOutput prediction = predictionEngine.Predict(image); Console.WriteLine("Classifying single image"); OutputPrediction(prediction); } public static void ClassifyImages(MLContext mlContext, IDataView data, ITransformer trainedModel) { IDataView predictionData = trainedModel.Transform(data); IEnumerable<ModelOutput> predictions = mlContext.Data.CreateEnumerable<ModelOutput>(predictionData, reuseRowObject: true).Take(10); Console.WriteLine("Classifying multiple images"); foreach (var prediction in predictions) { OutputPrediction(prediction); } } } class ModelInput { public byte[] Image { get; set; } public UInt32 LabelAsKey { get; set; } public string ImagePath { get; set; } public string Label { get; set; } } class ModelOutput { public string ImagePath { get; set; } public string Label { get; set; } public string PredictedLabel { get; set; } } class ImageData { public string ImagePath { get; set; } public string Label { get; set; } } }
執行結果
載入顯示卡啟用CUDA的過程如下。
訓練資料的過程如下。
單個預測和批量預測的過程如下。
從結果上看,無論單個預測還是批量預測,正確率都得到了很好的保證,當然也要注意適當地調整超參,防止模型過擬合。好訊息是,手勢的預測模型訓練好了,可以放心地整合到其他應用中使用。