使用 Windows ML API 在 Windows 应用中部署模型

注意

为了获得更大的功能, PyTorch 还可用于 Windows 上的 DirectML。

在本教程的上一部分中,你已了解如何构建并以 ONNX 格式导出模型。 接下来,我们将介绍如何将导出的模型嵌入到 Windows 应用程序中,并通过调用 WinML API 在设备的本地运行该模型。

完成此部分后,你将拥有一个可以工作的图像分类应用。

关于示例应用程序

在本教程的这一步中,你将创建一个应用,该应用可以使用 ML 模型对图像进行分类。 通过其基本 UI,你可以从本地设备中选择一个图像,然后使用你在上一部分构建并训练的分类 ONNX 模型对其进行分类。 随后,图像旁边会显示模型返回的标记。

在本部分中,我们将指导你完成该过程。

注意

如果选择使用预定义的代码示例,可以克隆解决方案文件。 克隆存储库,导航到此示例,然后使用 Visual Studio 打开 classifierPyTorch.sln 文件。跳到此页面的“启动应用程序”部分以查看它的使用情况

下面,我们将指导你如何创建应用并添加 Windows ML 代码。

创建 Windows ML UWP (C#)

若要创建可以工作的 Windows ML 应用,需要执行以下操作:

  • 加载机器学习模型。
  • 加载所需格式的图像。
  • 绑定模型的输入和输出。
  • 评估模型并显示有意义的结果。

还需要创建一个基本 UI,因为很难在命令行中创建一个令人满意的基于图像的应用。

在 Visual Studio 中打开新项目

  1. 现在就开始吧。 打开 Visual Studio 并选择“创建新项目”

创建新的 Visual Studio 项目

  1. 在搜索栏中,键入 UWP,然后选择 Blank APP (Universal Windows)。 这将为带有预定义控件或布局的单页通用 Windows 平台 (UWP) 应用打开一个 C# 项目。 选择 next 以打开项目的配置窗口。

创建新的 UWP 应用

  1. 在配置窗口中执行以下操作:
  • 为项目命名。 此处我们将其命名为 classifierPyTorch
  • 选择项目的位置。
  • 如果使用的是 VS 2019,请确保选中 Create directory for solution
  • 如果使用的是 VS2017,请确保未勾选 Place solution and project in the same directory

新的 UWP 应用设置

create 创建项目。 可能会弹出最低目标版本窗口。 请确保将最低版本设置为“Windows 10 版本 1809 (10.0;内部版本 17763)”或更高版本

  1. 创建项目后,导航到项目文件夹,打开资产文件夹 [….\classifierPyTorch \Assets],然后将 ImageClassifier.onnx 文件复制到该位置

探索项目解决方案

让我们探索项目解决方案。

Visual Studio 在解决方案资源管理器中自动创建了几个 cs 代码文件。 MainPage.xaml 包含 GUI 的 XAML 代码,MainPage.xaml.cs 包含应用程序代码(也称为代码隐藏)。 如果之前已创建 UWP 应用,你应该对这些文件非常熟悉。

UWP 应用解决方案

创建应用程序 GUI

首先,让我们为应用创建一个简单的 GUI。

  1. 双击 MainPage.xaml 代码文件。 在空白应用中,应用的 GUI 的 XAML模板为空,因此我们需要添加一些 UI 功能。

  2. 将以下代码添加到 MainPage.xaml,替换 <Grid></Grid> 标记。

    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"> 

        <StackPanel Margin="1,0,-1,0"> 
            <TextBlock x:Name="Menu"  
                       FontWeight="Bold"  
                       TextWrapping="Wrap" 
                       Margin="10,0,0,0" 
                       Text="Image Classification"/> 
            <TextBlock Name="space" /> 
            <Button Name="recognizeButton" 
                    Content="Pick Image" 
                    Click="OpenFileButton_Click"  
                    Width="110" 
                    Height="40" 
                    IsEnabled="True"  
                    HorizontalAlignment="Left"/> 
            <TextBlock Name="space3" /> 
            <Button Name="Output" 
                    Content="Result is:" 
                    Width="110" 
                    Height="40" 
                    IsEnabled="True"  
                    HorizontalAlignment="Left"  
                    VerticalAlignment="Top"> 
            </Button> 
            <!--Display the Result--> 
            <TextBlock Name="displayOutput"  
                       FontWeight="Bold"  
                       TextWrapping="Wrap" 
                       Margin="25,0,0,0" 
                       Text="" Width="1471" /> 
            <TextBlock Name="space2" /> 
            <!--Image preview --> 
            <Image Name="UIPreviewImage" Stretch="Uniform" MaxWidth="300" MaxHeight="300"/> 
        </StackPanel> 
    </Grid> 

使用 Windows ML 代码生成器 (mlgen) 将模型添加到项目中

Windows 机器学习代码生成器 (mlgen) 是一项 Visual Studio 扩展,有助于你在 UWP 应用中开始使用 WinML API。 将经过训练的 ONNX 文件添加到 UWP 项目时,它会生成模板代码。

Windows 机器学习的代码生成器 mlgen 会创建一个接口(用于 C#、C++/WinRT 和 C++/CX),其中包含为你调用 Windows ML API 的包装类。 这样,你便可以轻松地在项目中加载、绑定和评估模型。 在本教程中,我们将使用它来处理其中许多函数。

代码生成器适用于 Visual Studio 2017 及更高版本。 建议使用 Visual Studio。 请注意,在 Windows 10 版本 1903 和更高版本中,Windows 10 SDK 中不再包含 mlgen,因此必须下载并安装此扩展。 如果从简介开始就一直参加本教程,则已完成扩展的下载,否则,应下载适用于 VS 2019VS 2017 的扩展。

注意

若要详细了解 mlgen,请参阅 mlgen 文档

  1. 如果尚未安装 mlgen,请先安装。

  2. 右键单击 Visual Studio 解决方案资源管理器中的 Assets 文件夹,然后选择 Add > Existing Item

  3. 导航到 classifierPyTorch [….\classifierPyTorch \Assets] 中的资产文件夹,找到之前复制到该文件夹中的 ONNX 模型,然后选择 add

  4. 将 ONNX 模型添加到 VS 解决方案资源管理器的资产文件夹后,项目现在应该有两个新文件:

  • ImageClassifier.onnx - 这是采用 ONNX 格式的模型。
  • ImageClassifier.cs - 自动生成的 WinML 代码文件。

UWP 应用解决方案中的 ONNX 文件

  1. 若要确保在编译应用程序时生成模型,请选择 ImageClassifier.onnx 文件并选择 Properties。 对于 Build Action,请选择 Content

ONNX 文件代码

现在,让我们探索 ImageClassifier.cs 文件中新生成的代码。

生成的代码包括三个类:

  • ImageClassifierModel:此类包括用于模型实例化和模型评估的两种方法。 它有助于我们创建机器学习模型表示,在系统默认设备上创建一个会话,将特定输入和输出绑定到模型,并异步评估模型。
  • ImageClassifierInput:此类初始化模型预期的输入类型。 模型输入取决于输入数据的模型要求。
  • ImageClassifierOutput:此类初始化模型将输出的类型。 模型输出取决于模型对它的定义方式。

在本教程中,我们不希望处理张量化。 我们将对 ImageClassifierInput 类稍作更改,以更改输入数据类型并减轻工作负担。

  1. ImageClassifier.cs 文件中进行以下更改:

input 变量从 TensorFloat 更改为 ImageFeatureValue

public sealed class ImageClassifierInput 
    { 
        public ImageFeatureValue input; // shape(-1,3,32,32) 
    } 

加载模型

  1. 双击 MainPage.xaml.cs 文件以打开应用的代码隐藏。

  2. 将“using”语句替换为以下内容,以访问所需的全部 API:

// Specify all the using statements which give us the access to all the APIs that we'll need 
using System; 
using System.Threading.Tasks; 
using Windows.AI.MachineLearning; 
using Windows.Graphics.Imaging; 
using Windows.Media; 
using Windows.Storage; 
using Windows.Storage.Pickers; 
using Windows.Storage.Streams; 
using Windows.UI.Xaml; 
using Windows.UI.Xaml.Controls; 
using Windows.UI.Xaml.Media.Imaging; 
  1. MainPage 类中,在 public MainPage() 函数上方添加以下变量声明。
        // All the required fields declaration 
        private ImageClassifierModel modelGen; 
        private ImageClassifierInput image = new ImageClassifierInput(); 
        private ImageClassifierOutput results; 
        private StorageFile selectedStorageFile; 
        private string label = ""; 
        private float probability = 0; 
        private Helper helper = new Helper(); 

        public enum Labels 
        {             
            plane,
            car,
            bird,
            cat,
            deer,
            dog,
            frog,
            horse,
            ship,
            truck
        } 

接下来,实现 LoadModel 方法。 该方法将访问 ONNX 模型,并将其存储在内存中。 然后,使用 CreateFromStreamAsync 方法将模型实例化为 LearningModel 对象。 LearningModel 类表示已训练的机器学习模型。 实例化后,LearningModel 是用于与 Windows ML 交互的初始对象。

若要加载模型,可使用 LearningModel 类中的多个静态方法。 在本例中,将使用 CreateFromStreamAsync 方法。

CreateFromStreamAsync 方法是使用 mlgen 自动创建的,因此无需实现此方法。 可通过双击 mlgen 生成的 classifier.cs 文件来查看此方法。

注意

若要详细了解 LearningModel 类,请查看 LearningModel 类文档。 若要详细了解加载模型的其他方式,请查看加载模型文档

  1. 向主类的构造函数添加对 loadModel 方法的调用。
        // The main page to initialize and execute the model.
        public MainPage()
        {
            this.InitializeComponent();
            loadModel();
        }
  1. 在该 MainPage 类中添加 loadModel 方法的实现。
        private async Task loadModel()
        {
            // Get an access the ONNX model and save it in memory.
            StorageFile modelFile = await StorageFile.GetFileFromApplicationUriAsync(new Uri($"ms-appx:///Assets/ImageClassifier.onnx"));
            // Instantiate the model. 
            modelGen = await ImageClassifierModel.CreateFromStreamAsync(modelFile);
        }

加载图像

  1. 我们需要定义一个 click 事件来启动模型执行的四个方法调用序列 - 转换、绑定和评估、输出提取以及结果显示。 将以下方法添加到 MainPage 类中的 MainPage.xaml.cs 代码文件。
        // Waiting for a click event to select a file 
        private async void OpenFileButton_Click(object sender, RoutedEventArgs e)
        {
            if (!await getImage())
            {
                return;
            }
            // After the click event happened and an input selected, begin the model execution. 
            // Bind the model input
            await imageBind();
            // Model evaluation
            await evaluate();
            // Extract the results
            extractResult();
            // Display the results  
            await displayResult();
        }
  1. 接下来,实现 getImage() 方法。 此方法将选择一个输入图像文件并将其保存在内存中。 将以下方法添加到 MainPage 类中的 MainPage.xaml.cs 代码文件。
        // A method to select an input image file
        private async Task<bool> getImage()
        {
            try
            {
                // Trigger file picker to select an image file
                FileOpenPicker fileOpenPicker = new FileOpenPicker();
                fileOpenPicker.SuggestedStartLocation = PickerLocationId.PicturesLibrary;
                fileOpenPicker.FileTypeFilter.Add(".jpg");
                fileOpenPicker.FileTypeFilter.Add(".png");
                fileOpenPicker.ViewMode = PickerViewMode.Thumbnail;
                selectedStorageFile = await fileOpenPicker.PickSingleFileAsync();
                if (selectedStorageFile == null)
                {
                    return false;
                }
            }
            catch (Exception)
            {
                return false;
            }
            return true;
        }

接下来,你将实现图像 Bind() 方法,以获取位图 BGRA8 格式的文件表示。 但首先,你将创建帮助程序类来调整图像大小。

  1. 若要创建帮助程序文件,请右键单击解决方案名称 (ClassifierPyTorch),然后选择 Add a new item。 在打开的窗口中,选择 Class,然后为其指定一个名称。 此处我们将其命名为 Helper

添加帮助程序文件

  1. 你的项目中将出现一个新的类文件。 打开此类,并添加以下代码:
using System; 
using System.Threading.Tasks; 
using Windows.Graphics.Imaging; 
using Windows.Media; 

namespace classifierPyTorch 
{ 
    public class Helper 
    { 
        private const int SIZE = 32;  
        VideoFrame cropped_vf = null; 
 
        public async Task<VideoFrame> CropAndDisplayInputImageAsync(VideoFrame inputVideoFrame) 
        { 
            bool useDX = inputVideoFrame.SoftwareBitmap == null; 

            BitmapBounds cropBounds = new BitmapBounds(); 
            uint h = SIZE; 
            uint w = SIZE; 
            var frameHeight = useDX ? inputVideoFrame.Direct3DSurface.Description.Height : inputVideoFrame.SoftwareBitmap.PixelHeight; 
            var frameWidth = useDX ? inputVideoFrame.Direct3DSurface.Description.Width : inputVideoFrame.SoftwareBitmap.PixelWidth; 
 
            var requiredAR = ((float)SIZE / SIZE); 
            w = Math.Min((uint)(requiredAR * frameHeight), (uint)frameWidth); 
            h = Math.Min((uint)(frameWidth / requiredAR), (uint)frameHeight); 
            cropBounds.X = (uint)((frameWidth - w) / 2); 
            cropBounds.Y = 0; 
            cropBounds.Width = w; 
            cropBounds.Height = h; 
 
            cropped_vf = new VideoFrame(BitmapPixelFormat.Bgra8, SIZE, SIZE, BitmapAlphaMode.Ignore); 
 
            await inputVideoFrame.CopyToAsync(cropped_vf, cropBounds, null); 
            return cropped_vf; 
        } 
    } 
} 

现在,让我们将图像转换为适当的格式。

ImageClassifierInput 类初始化模型预期的输入类型。 在本例中,我们将代码配置为预计 ImageFeatureValue

ImageFeatureValue 类描述用于传递到模型的图像的属性。 若要创建 ImageFeatureValue,请使用 CreateFromVideoFrame 方法。 有关为什么选择这种方法以及这些类和方法如何工作的更具体详细信息,请参阅 ImageFeatureValue 类文档

注意

在本教程中,我们使用 ImageFeatureValue 类,而不是张量。 如果 Window ML 不支持模型的颜色格式,则不能使用此选项。 有关如何使用图像转换和张量化的示例,请参阅自定义张量化示例

  1. convert() 方法的实现添加到 MainPage 类中的 MainPage.xaml.cs 代码文件。 convert 方法将获取 BGRA8 格式的输入文件表示。
// A method to convert and bide the input image. 
private async Task imageBind () 
{
    UIPreviewImage.Source = null; 
    try
    { 
        SoftwareBitmap softwareBitmap;
        using (IRandomAccessStream stream = await selectedStorageFile.OpenAsync(FileAccessMode.Read)) 
        {
            // Create the decoder from the stream
            BitmapDecoder decoder = await BitmapDecoder.CreateAsync(stream);
            // Get the SoftwareBitmap representation of the file in BGRA8 format
            softwareBitmap = await decoder.GetSoftwareBitmapAsync();
            softwareBitmap = SoftwareBitmap.Convert(softwareBitmap, BitmapPixelFormat.Bgra8, BitmapAlphaMode.Premultiplied);
        }
        // Display the image 
        SoftwareBitmapSource imageSource = new SoftwareBitmapSource();
        await imageSource.SetBitmapAsync(softwareBitmap);
        UIPreviewImage.Source = imageSource;

        // Encapsulate the image within a VideoFrame to be bound and evaluated
        VideoFrame inputImage = VideoFrame.CreateWithSoftwareBitmap(softwareBitmap);
        // Resize the image size to 32x32  
        inputImage=await helper.CropAndDisplayInputImageAsync(inputImage); 
        // Bind the model input with image 
        ImageFeatureValue imageTensor = ImageFeatureValue.CreateFromVideoFrame(inputImage); 
        image.modelInput = imageTensor; 

        // Encapsulate the image within a VideoFrame to be bound and evaluated
        VideoFrame inputImage = VideoFrame.CreateWithSoftwareBitmap(softwareBitmap); 
        // bind the input image 
        ImageFeatureValue imageTensor = ImageFeatureValue.CreateFromVideoFrame(inputImage); 
        image.modelInput = imageTensor; 
    }
    catch (Exception e) 
    {
    }
} 

绑定并评估模型

接下来,需要基于模型创建一个会话,绑定会话的输入和输出,并评估模型。

创建用于绑定模型的会话:

若要创建会话,请使用 LearningModelSession 类。 此类用于评估机器学习模型,并将该模型绑定到一个设备上,然后运行和评估该模型。 在创建会话时,可以选择一个设备,以便在计算机的特定设备上执行模型。 默认设备是 CPU。

注意

若要详细了解如何选择设备,请参阅创建会话文档。

绑定模型输入和输出:

若要绑定输入和输出,请使用 LearningModelBinding 类。 机器学习模型具有输入和输出特征,用于将信息传入和传出模型。 请注意,Window ML API 必须支持必需的功能。 在 LearningModelSession 上应用 LearningModelBinding 类以将值绑定到已命名的输入和输出特征。

绑定的实现由 mlgen 自动生成,因此你无需执行此操作。 绑定通过调用 LearningModelBinding 类的预定义方法来实现。 在我们的示例中,它使用 Bind 方法将值绑定到已命名的功能类型。

评估模型:

创建会话以将模型和已绑定的值绑定到模型的输入和输出后,可以评估模型的输入并获取其预测。 要运行模型执行,应在 LearningModelSession 上调用任意预定义的评估方法。 在本例中,我们将使用 EvaluateAsync 方法。

CreateFromStreamAsync 类似,EvaluateAsync 方法也由 WinML 代码生成器自动生成,因此你无需实现此方法。 可在 ImageClassifier.cs 文件中查看此方法。

EvaluateAsync 方法将使用绑定中已绑定的特征值异步评估计算机学习模型。 它将通过 LearningModelSession 创建一个会话,通过 LearningModelBinding 绑定输入和输出,执行模型评估,并使用 LearningModelEvaluationResult 类获得模型的输出功能。

注意

要了解用于运行模型的其他评估方法,请通过查看 LearningModelSession 类文档,检查哪些方法可以在 LearningModelSession 上实现。

  1. 将以下方法添加到 MainPage 类内的 MainPage.xaml.cs 代码文件中,以创建会话、绑定和评估模型。
        // A method to evaluate the model
        private async Task evaluate()
        {
            results = await modelGen.EvaluateAsync(image);
        }

提取并显示结果

现在需要提取模型输出并显示正确的结果,这将通过 extractResultdisplayResult 方法来实现。 需要找出返回正确标签的最高概率。

  1. extractResult 方法添加到 MainPage 类中的 MainPage.xaml.cs 代码文件。
        // A method to extract output from the model 
        private void extractResult()
        {
            // Retrieve the results of evaluation
            var mResult = results.modelOutput as TensorFloat;
            // convert the result to vector format
            var resultVector = mResult.GetAsVectorView();
            
            probability = 0;
            int index = 0;
            // find the maximum probability
            for(int i=0; i<resultVector.Count; i++)
            {
                var elementProbability=resultVector[i];
                if (elementProbability > probability)
                {
                    index = i;
                }
            }
            label = ((Labels)index).ToString();
        }
  1. displayResult 方法添加到 MainPage 类中的 MainPage.xaml.cs 代码文件。
        private async Task displayResult() 
        {
            displayOutput.Text = label; 
        }

就是这样! 你已成功创建了包含基本 GUI 的 Windows 机器学习应用,以测试我们的分类模型。 下一步是启动应用程序并在 Windows 设备的本地运行该应用程序。

启动应用程序

完成应用程序接口、添加模型并生成 Windows ML 代码后,便可以测试应用程序了!

启用开发人员模式并从 Visual Studio 测试应用程序。 确保顶部工具栏中的下拉菜单设置为 Debug。 将解决方案平台更改为 x64(如果设备是 64 位的)或 x86(如果设备是 32 位的)以在你的本地计算机上运行该项目。

我们的模型已经过训练,可以对以下图像进行分类:飞机、汽车、鸟、猫、鹿、狗、青蛙、马、船、卡车。 为了测试应用,你将使用为此项目构建的乐高汽车的图像。 让我们看看应用如何对图像内容进行分类。

用于测试应用程序的图像

  1. 将此图像保存在本地设备上,以测试应用。 如果需要,将图像格式更改为 .jpg。 你还可以使用本地设备上 .jpg 或 .png 格式的任何其他相关图像。

  2. 要运行项目,请选择工具栏上的 Start Debugging 按钮,或按 F5

  3. 当应用程序启动时,按“选取图像”并从本地设备中选择图像。

应用程序接口

结果会立即显示在屏幕上。 如你所见,Windows ML 应用成功地将图像归类为汽车。

在应用中成功分类

总结

你刚刚完成了首个 Windows 机器学习应用从模型创建到成功执行的过程。

其他资源

若要详细了解本教程中所述的主题,请访问以下资源: