1. 程式人生 > 實用技巧 >通過 ML.NET 使用預訓練殘差網路 ResNet 模型實現手勢識別

通過 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

下載訓練資料集,裡面是分類好的 0-5 的手勢圖片。

建立專案

使用 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的過程如下。

訓練資料的過程如下。

單個預測和批量預測的過程如下。

從結果上看,無論單個預測還是批量預測,正確率都得到了很好的保證,當然也要注意適當地調整超參,防止模型過擬合。好訊息是,手勢的預測模型訓練好了,可以放心地整合到其他應用中使用。