共用方式為


教學課程:建立 Windows 機器學習 Desktop 應用程式 (C++)

您可以利用 Windows ML API,輕鬆地與 C++ 傳統型應用程式 (Win32) 中的機器學習模型互動。 使用載入、繫結和評估的三個步驟,您的應用程式就能受益於機器學習的強大功能。

載入 - 系結 ->> 評估

我們將會建立簡化版本的 SqueezeNet 物件偵測範例,可在 GitHub 上取得。 如果您想要在完成時查看其內容,您可以下載完整的範例。

我們將使用 C++/WinRT 來存取 WinML API。 如需詳細資訊,請參閱 C++/WinRT

在本教學課程中,您將了解如何:

  • 載入機器學習模型
  • VideoFrame 格式載入影像
  • 繫結模型的輸入和輸出
  • 評估模型並列印有意義的結果

必要條件

建立專案

首先,我們會在 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" 程式碼 (main 函式中 init_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. main 方法新增對 BindModel 的呼叫:

    BindModel();
    
  3. 在不進行偵錯的情況下執行程式。 應該成功繫結模型的輸入和輸出。 即將完成!

評估模型

我們現在來到本教學課程開頭圖表中的最後一個步驟,評估。 我們將會使用 LearningModelSession.Evaluate 來評估模型:

  1. 實作 EvaluateModel 方法。 這個方法會採用我們的工作階段,並使用我們的繫結和相互關聯識別碼來進行評估。 相互關聯識別碼是我們稍後可能會用來比對特定評估呼叫與輸出結果的項目。 同樣地,我們事先知道輸出的名稱是 "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. main 方法中新增對 EvaluateModel 的呼叫:

    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 上提出問題。