教程:创建 Windows 机器学习桌面应用程序(C++)

可以利用 Windows ML API 轻松与C++桌面(Win32)应用程序中的机器学习模型进行交互。 使用加载、绑定和评估的三个步骤,应用程序可以从机器学习的强大功能中获益。

加载 -> 绑定 -> 评估

我们将创建一个稍微简化版本的 SqueezeNet 对象检测示例,该示例在 GitHub 上可用。 如果想要查看完成时的外观,可以下载完整的示例。

我们将使用 C++/WinRT 访问 WinML API。 有关详细信息 ,请参阅 C++/WinRT

本教程介绍以下操作:

  • 加载机器学习模型
  • 将图像加载为 视频帧
  • 绑定模型的输入和输出
  • 评估模型并打印有意义的结果

先决条件

创建项目

首先,我们将在 Visual Studio 中创建项目:

  1. 选择 “文件 > 新建 > 项目 ”以打开 “新建项目” 窗口。
  2. 在左窗格中,选择“已安装>的 Visual C++ > Windows 桌面”,然后在中间选择“Windows 控制台应用程序”(C++/WinRT)。
  3. 为项目指定名称和位置,然后单击“确定”。
  4. 在“新建通用 Windows 平台项目”窗口中,将“目标”和“最低版本”都设置为版本 17763 或更高版本,然后单击“确定”
  5. 请确保顶部工具栏中的下拉菜单设置为 “调试 ”,并根据计算机的体系结构选择 x64x86
  6. Ctrl+F5 在不调试的情况下运行程序。 应当会打开一个终端,其中显示“Hello world”文本。 按任意键将其关闭。

加载模型

接下来,我们将使用 LearningModel.LoadFromFilePath 将 ONNX 模型加载到程序中:

  1. pch.h在头文件 文件夹中),添加以下 include 语句(这些语句允许我们访问我们需要的所有 API):

    #include <winrt/Windows.AI.MachineLearning.h>
    #include <winrt/Windows.Foundation.Collections.h>
    #include <winrt/Windows.Graphics.Imaging.h>
    #include <winrt/Windows.Media.h>
    #include <winrt/Windows.Storage.h>
    
    #include <string>
    #include <fstream>
    
    #include <Windows.h>
    
  2. main.cpp (在 源文件 文件夹中),添加以下 using 语句:

    using namespace Windows::AI::MachineLearning;
    using namespace Windows::Foundation::Collections;
    using namespace Windows::Graphics::Imaging;
    using namespace Windows::Media;
    using namespace Windows::Storage;
    
    using namespace std;
    
  3. 在语句后面 using 添加以下变量声明:

    // Global variables
    hstring modelPath;
    string deviceName = "default";
    hstring imagePath;
    LearningModel model = nullptr;
    LearningModelDeviceKind deviceKind = LearningModelDeviceKind::Default;
    LearningModelSession session = nullptr;
    LearningModelBinding binding = nullptr;
    VideoFrame imageFrame = nullptr;
    string labelsFilePath;
    vector<string> labels;
    
  4. 在全局变量后面添加以下向前声明:

    // Forward declarations
    void LoadModel();
    VideoFrame LoadImageFile(hstring filePath);
    void BindModel();
    void EvaluateModel();
    void PrintResults(IVectorView<float> results);
    void LoadLabels();
    
  5. main.cpp中,删除“Hello world”代码(函数后面的maininit_apartment所有内容)。

  6. 在本地 Windows-Machine-Learning 存储库克隆中找到 SqueezeNet.onnx 文件。 它应位于 \Windows-Machine-Learning\SharedContent\models 中

  7. 复制文件路径,并将其指定给我们在顶部定义的 modelPath 变量。 记得要使用 L 作为字符串前缀,使其成为宽字符串,以便它能够正常用于 hstring,并使用额外的反斜杠来对任何反斜杠 (\) 进行转义。 例如:

    hstring modelPath = L"C:\\Repos\\Windows-Machine-Learning\\SharedContent\\models\\SqueezeNet.onnx";
    
  8. 首先,我们将实现该方法 LoadModel 。 在main方法后添加以下方法。 此方法加载模型并输出花费的时间:

    void LoadModel()
    {
         // load the model
         printf("Loading modelfile '%ws' on the '%s' device\n", modelPath.c_str(), deviceName.c_str());
         DWORD ticks = GetTickCount();
         model = LearningModel::LoadFromFilePath(modelPath);
         ticks = GetTickCount() - ticks;
         printf("model file loaded in %d ticks\n", ticks);
    }
    
  9. 最后,从 main 方法调用此方法:

    LoadModel();
    
  10. 在不调试的情况下运行程序。 你应该会看到模型已成功加载!

加载映像

接下来,我们将图像文件加载到程序中:

  1. 添加以下方法。 此方法将从给定路径加载图像并从中创建 VideoFrame

    VideoFrame LoadImageFile(hstring filePath)
    {
        printf("Loading the image...\n");
        DWORD ticks = GetTickCount();
        VideoFrame inputImage = nullptr;
    
        try
        {
            // open the file
            StorageFile file = StorageFile::GetFileFromPathAsync(filePath).get();
            // get a stream on it
            auto stream = file.OpenAsync(FileAccessMode::Read).get();
            // Create the decoder from the stream
            BitmapDecoder decoder = BitmapDecoder::CreateAsync(stream).get();
            // get the bitmap
            SoftwareBitmap softwareBitmap = decoder.GetSoftwareBitmapAsync().get();
            // load a videoframe from it
            inputImage = VideoFrame::CreateWithSoftwareBitmap(softwareBitmap);
        }
        catch (...)
        {
            printf("failed to load the image file, make sure you are using fully qualified paths\r\n");
            exit(EXIT_FAILURE);
        }
    
        ticks = GetTickCount() - ticks;
        printf("image file loaded in %d ticks\n", ticks);
        // all done
        return inputImage;
    }
    
  2. main方法中添加对此方法的调用:

    imageFrame = LoadImageFile(imagePath);
    
  3. 在本地 Windows-Machine-Learning 存储库克隆中找到媒体文件夹。 它应位于 \Windows-Machine-Learning\SharedContent\media

  4. 在该文件夹中选择一个图像,并将它的文件路径分配给我们在顶部定义的 imagePath 变量。 记得要使用 L 作为其前缀,使其成为宽字符串,并使用另一个反斜杠来对任何反斜杠进行转义。 例如:

    hstring imagePath = L"C:\\Repos\\Windows-Machine-Learning\\SharedContent\\media\\kitten_224.png";
    
  5. 在不调试的情况下运行程序。 应会看到已成功加载的图像!

绑定输入和输出

接下来,我们将基于模型创建会话,并使用 LearningModelBinding.Bind 绑定会话中的输入和输出。 有关绑定的详细信息,请参阅 绑定模型

  1. 实现 BindModel 方法。 这会创建一个基于模型和设备的会话,并在该会话基础上创建一个绑定。 然后,我们将输入和输出绑定到使用它们的名称创建的变量。 我们事先知道输入功能名为“data_0”,输出功能名为“softmaxout_1”。 可以通过在 Netron(联机模型可视化工具)中打开这些属性来查看任何模型的这些属性。

    void BindModel()
    {
        printf("Binding the model...\n");
        DWORD ticks = GetTickCount();
    
        // now create a session and binding
        session = LearningModelSession{ model, LearningModelDevice(deviceKind) };
        binding = LearningModelBinding{ session };
        // bind the intput image
        binding.Bind(L"data_0", ImageFeatureValue::CreateFromVideoFrame(imageFrame));
        // bind the output
        vector<int64_t> shape({ 1, 1000, 1, 1 });
        binding.Bind(L"softmaxout_1", TensorFloat::Create(shape));
    
        ticks = GetTickCount() - ticks;
        printf("Model bound in %d ticks\n", ticks);
    }
    
  2. BindModel 方法添加对 main 的调用:

    BindModel();
    
  3. 在不调试的情况下运行程序。 应成功绑定模型的输入和输出。 我们快到了!

评估模型

现在,我们处于本教程开头图表的最后一步:评估。 我们将使用 LearningModelSession.Evaluate 评估模型:

  1. 实现 EvaluateModel 方法。 此方法获取我们的会话,并使用我们的绑定和相关 ID 对其进行评估。 关联 ID 是我们以后可能会用来匹配特定评估调用与输出结果的工具。 同样,我们提前知道输出的名称为“softmaxout_1”。

    void EvaluateModel()
    {
        // now run the model
        printf("Running the model...\n");
        DWORD ticks = GetTickCount();
    
        auto results = session.Evaluate(binding, L"RunId");
    
        ticks = GetTickCount() - ticks;
        printf("model run took %d ticks\n", ticks);
    
        // get the output
        auto resultTensor = results.Outputs().Lookup(L"softmaxout_1").as<TensorFloat>();
        auto resultVector = resultTensor.GetAsVectorView();
        PrintResults(resultVector);
    }
    
  2. 现在,让我们实现 PrintResults。 此方法获取图像中对象可能的前三个概率,并输出它们:

    void PrintResults(IVectorView<float> results)
    {
        // load the labels
        LoadLabels();
        // Find the top 3 probabilities
        vector<float> topProbabilities(3);
        vector<int> topProbabilityLabelIndexes(3);
        // SqueezeNet returns a list of 1000 options, with probabilities for each, loop through all
        for (uint32_t i = 0; i < results.Size(); i++)
        {
            // is it one of the top 3?
            for (int j = 0; j < 3; j++)
            {
                if (results.GetAt(i) > topProbabilities[j])
                {
                    topProbabilityLabelIndexes[j] = i;
                    topProbabilities[j] = results.GetAt(i);
                    break;
                }
            }
        }
        // Display the result
        for (int i = 0; i < 3; i++)
        {
            printf("%s with confidence of %f\n", labels[topProbabilityLabelIndexes[i]].c_str(), topProbabilities[i]);
        }
    }
    
  3. 我们还需要实现 LoadLabels。 此方法打开标签文件,其中包含模型可以识别的所有不同对象,并对其进行分析:

    void LoadLabels()
    {
        // Parse labels from labels file.  We know the file's entries are already sorted in order.
        ifstream labelFile{ labelsFilePath, ifstream::in };
        if (labelFile.fail())
        {
            printf("failed to load the %s file.  Make sure it exists in the same folder as the app\r\n", labelsFilePath.c_str());
            exit(EXIT_FAILURE);
        }
    
        std::string s;
        while (std::getline(labelFile, s, ','))
        {
            int labelValue = atoi(s.c_str());
            if (labelValue >= labels.size())
            {
                labels.resize(labelValue + 1);
            }
            std::getline(labelFile, s);
            labels[labelValue] = s;
        }
    }
    
  4. Windows-Machine-Learning 存储库的本地克隆中找到 Labels.txt 文件。 它应位于 \Windows-Machine-Learning\Samples\SqueezeNetObjectDetection\Desktop\cpp 中。

  5. 将此文件路径指定给我们在顶部定义的 labelsFilePath 变量。 请确保用另一反斜杠来对任何反斜杠进行转义。 例如:

    string labelsFilePath = "C:\\Repos\\Windows-Machine-Learning\\Samples\\SqueezeNetObjectDetection\\Desktop\\cpp\\Labels.txt";
    
  6. EvaluateModel 方法中添加对 main 的调用:

    EvaluateModel();
    
  7. 在不调试的情况下运行程序。 它现在应该能够正确识别图像中的内容! 下面是它可能输出的示例:

    Loading modelfile 'C:\Repos\Windows-Machine-Learning\SharedContent\models\SqueezeNet.onnx' on the 'default' device
    model file loaded in 250 ticks
    Loading the image...
    image file loaded in 78 ticks
    Binding the model...Model bound in 15 ticks
    Running the model...
    model run took 16 ticks
    tabby, tabby cat with confidence of 0.931461
    Egyptian cat with confidence of 0.065307
    Persian cat with confidence of 0.000193
    

后续步骤

太好了,你在C++桌面应用程序中成功实现了对象检测! 接下来,可以尝试使用命令行参数来输入模型和图像文件,而不是对其进行硬编码,这类似于 GitHub 上的示例。 还可以尝试在不同的设备(如 GPU)上运行评估,以了解性能的差异。

琢磨 GitHub 上的其他示例并任意扩展它们!

另请参阅

注释

使用以下资源获取有关 Windows ML 的帮助:

  • 若要询问或回答有关 Windows ML 的技术问题,请使用 Stack Overflow上的 windows-machine-learning 标记。
  • 若要报告 bug,请在 gitHub 提交问题。