Tutorial: Creación de una aplicación de escritorio de Windows Machine Learning (C++)

Las API de Windows ML se pueden utilizar para interactuar fácilmente con los modelos de aprendizaje automático en aplicaciones de escritorio C++ (Win32). Con los tres pasos de carga, enlace y evaluación, la aplicación puede beneficiarse de la eficacia del aprendizaje automático.

Cargar - Enlazar ->> Evaluar

Vamos a crear una versión un poco simplificada del ejemplo SqueezeNet Object Detection, que está disponible en GitHub. Puedes descargar el ejemplo completo para ver el aspecto que tendrá al finalizar.

Usaremos C++/WinRT para acceder a las API de WinML. Para obtener más información, consulta C++/WinRT.

En este tutorial, aprenderá a:

  • Cargar un modelo de aprendizaje automático.
  • Cargar una imagen como un VideoFrame.
  • Enlazar las entradas y salidas del modelo.
  • Evaluar el modelo e imprimir resultados significativos.

Requisitos previos

Creación del proyecto

En primer lugar, crearemos el proyecto en Visual Studio:

  1. Seleccione Archivo > Nuevo > Proyecto para abrir la ventana Nuevo proyecto.
  2. En el panel izquierdo, seleccione Instalado > Visual C++ > Escritorio de Windows y, en el centro, seleccione Aplicación de consola de Windows (C++/WinRT).
  3. Asigna al proyecto valores de Nombre y Ubicación y, a continuación, haz clic en Aceptar.
  4. En la ventana Nuevo proyecto de la Plataforma universal de Windows, establece Destino y Versión mínima en la compilación 17763 o posterior, y haz clic en Aceptar.
  5. Asegúrate de que los menús desplegables de la barra de herramientas superior estén configurados en Depurar y x64 o x86 en función de la arquitectura del equipo.
  6. Presiona Ctrl + F5 para ejecutar el programa sin depuración. Un terminal debe abrirse con el texto "Hola mundo". Presiona cualquier tecla para cerrarlo.

Cargar el modelo

A continuación, cargaremos el modelo ONNX en nuestro programa con LearningModel.LoadFromFilePath:

  1. En pch.h (en la carpeta Archivos de encabezado), agrega las siguientes instrucciones include (que nos proporcionan acceso a todas las API que necesitamos):

    #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. En main.cpp (en la carpeta Archivos de origen), agrega las siguientes instrucciones 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. Agrega las siguientes declaraciones de variables detrás de las instrucciones 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. Agrega las siguientes declaraciones directas después de las variables globales:

    // Forward declarations
    void LoadModel();
    VideoFrame LoadImageFile(hstring filePath);
    void BindModel();
    void EvaluateModel();
    void PrintResults(IVectorView<float> results);
    void LoadLabels();
    
  5. En main.cpp, quita el código "Hola mundo" (todo en la función main después de init_apartment).

  6. Busca el archivo SqueezeNet.onnx en el clon local del repositorio Windows-Machine-Learning. Debe encontrarse en \Windows-Machine-Learning\SharedContent\models.

  7. Copia la ruta de acceso del archivo y asígnala a la variable modelPath donde la definimos en la parte superior. No olvides prefijar la cadena con L para convertirla en una cadena de caracteres anchos, de forma que funcione correctamente con hstring, y para que se escapen las barras diagonales inversas (\) con una barra diagonal inversa adicional. Por ejemplo:

    hstring modelPath = L"C:\\Repos\\Windows-Machine-Learning\\SharedContent\\models\\SqueezeNet.onnx";
    
  8. En primer lugar, implementaremos el método LoadModel. Agrega el siguiente código después del método main. Este método carga el modelo e indica cuánto ha tardado en ejecutarse:

    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. Por último, llama a este método desde el método main:

    LoadModel();
    
  10. Ejecuta el programa sin depuración. Deberías ver que el modelo se carga correctamente.

Cargar la imagen

A continuación, cargaremos el archivo de imagen en nuestro programa:

  1. Agregue el siguiente método . Este método cargará la imagen desde la ruta de acceso proporcionada y creará un VideoFrame a partir de esta:

    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. Agrega una llamada a este método en el método main:

    imageFrame = LoadImageFile(imagePath);
    
  3. Busca la carpeta media en el clon local del repositorio Windows-Machine-Learning. Debe encontrarse en \Windows-Machine-Learning\SharedContent\media.

  4. Elige una de las imágenes de la carpeta y asigna su ruta de acceso de archivo a la variable imagePath en la que se definió en la parte superior. No olvides prefijarla con L para convertirla en una cadena de caracteres anchos, y para que se escapen las barras diagonales inversas con otra barra diagonal inversa. Por ejemplo:

    hstring imagePath = L"C:\\Repos\\Windows-Machine-Learning\\SharedContent\\media\\kitten_224.png";
    
  5. Ejecuta el programa sin depuración. Deberías ver que la imagen cargada correctamente.

Enlazar la entrada y la salida

A continuación, crearemos una sesión basada en el modelo y enlazaremos la entrada y la salida de la sesión mediante LearningModelBinding.Bind. Para obtener más información sobre el enlace, consulta Enlazar un modelo.

  1. Implemente el método BindModel. Se crea una sesión basada en el modelo y el dispositivo, y un enlace basado en esa sesión. A continuación, enlazamos las entradas y salidas a las variables que hemos creado con sus nombres. Sabemos de antemano que la característica de entrada se denomina "data_0" y la característica de salida se denomina "softmaxout_1". Para ver estas propiedades de cualquier modelo, ábrelo en Netron, una herramienta de visualización de modelos en línea.

    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. Agrega una llamada a BindModel desde el método main:

    BindModel();
    
  3. Ejecuta el programa sin depuración. Las entradas y salidas del modelo deben estar correctamente enlazadas. Casi has terminado.

Evaluación del modelo

Hemos llegado al último paso del diagrama del principio de este tutorial, Evaluar. Evaluaremos el modelo con LearningModelSession.Evaluate:

  1. Implemente el método EvaluateModel. Este método toma nuestra sesión y la evalúa mediante nuestro enlace y un identificador de correlación. El identificador de correlación se podría usar más adelante para relacionar una llamada de evaluación determinada con los resultados de salida. Una vez más, sabemos de antemano que el nombre de la salida es "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. Ahora vamos a implementar PrintResults. Este método obtiene las tres principales probabilidades de qué objeto puede haber en la imagen y las imprime:

    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. También debemos implementar LoadLabels. Este método abre el archivo de etiquetas que contiene todos los objetos diferentes que puede reconocer el modelo y lo analiza:

    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. Busca el archivo Labels.txt en el clon local del repositorio Windows-Machine-Learning. Debe estar en \Windows-Machine-Learning\Samples\SqueezeNetObjectDetection\Desktop\cpp.

  5. Asigna esta ruta de acceso de archivo a la variable labelsFilePath en la que se definió en la parte superior. Asegúrate de que se escapen las barras diagonales inversas con otra barra diagonal inversa. Por ejemplo:

    string labelsFilePath = "C:\\Repos\\Windows-Machine-Learning\\Samples\\SqueezeNetObjectDetection\\Desktop\\cpp\\Labels.txt";
    
  6. Agrega una llamada a EvaluateModel en el método main:

    EvaluateModel();
    
  7. Ejecuta el programa sin depuración. Ahora debería reconocer correctamente lo que hay en la imagen. A continuación se muestra un ejemplo de salida posible:

    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
    

Pasos siguientes

Genial. Has logrado que la detección de objetos funcione en una aplicación de escritorio C++. A continuación, puedes intentar usar argumentos de la línea de comandos para especificar los archivos de modelo y de imagen en lugar de codificarlos de forma rígida, similar a lo que hace el ejemplo en GitHub. También puedes intentar ejecutar la evaluación en otro dispositivo, como la GPU, para ver cómo difiere el rendimiento.

Experimenta con los otros ejemplos en GitHub y amplíalos como quieras.

Consulte también

Nota:

Use los siguientes recursos para obtener ayuda con Windows ML:

  • Para formular o responder a preguntas técnicas sobre Windows Machine Learning, utilice la etiqueta windows-machine-learning en Stack Overflow.
  • Para notificar un error, registre un problema en GitHub.