ML.NET Несоответствие схемы: ожидаемый переменный вектор получил вектор

#c# #schema #ml.net

#c# #схема #ml.net

Вопрос:

Я пытался изменить ML.NET учебное пособие по классификации изображений и столкнулись с проблемой несоответствия схемы, когда модель ожидает переменный вектор, и ей передается вектор.

В исходном примере создается IDataView со столбцом с именем «ImagePath», заполненным путями к изображениям, а затем генерируется столбец «Image» с байтовыми данными, используя MLContext.Преобразует.LoadRawImageBytes(). Полученный IDataView затем передается в мультиклассификацию.Кроссовки.Объект ImageClassification(). Это очень просто.

Я хочу обобщить его, чтобы включить немного больше предварительной обработки изображения. Я хотел бы, чтобы конвейер предварительной обработки уменьшил разрешение изображения, а затем также обрезал изображение до правильного размера перед отправкой его в тренажер ImageClassification(). Он следует тому же потоку, что и в примере, но изменяет конвейер обработки для использования оценщиков LoadImages(), resizeImage() и ExtractPixels() в конвейере обработки изображений. Ниже приведен фрагмент кода, с которым я работаю.

     // Fill an IDataView with the Label and ImagePath information:
    // The ImageData class is defined for this purpose and it is just:
    // 
    //  public class ImageData {
    //       public string ImagePath { get; set; }
    //       public string Label { get; set; }
    //  }
    // 
    // Create a bunch of image names and their labels into a list of ImageData objects.
    IEnumerable<ImageData> images = LoadImagesFromDirectory(folder: assetsRelativePath, 
                                                            useFolderNameAsLabel: true);
    IDataView imageData = mlContext.Data.LoadFromEnumerable(images);

    // Define the preprocessing pipeline to:
    // 1. Map the Label to a unique key
    // 2. Load the image data using the ImagePath column
    // 3. Reduce the resolution of the image data
    // 4. Crop the reduced resolution image data
    // 5. Transform the image data from type "Image" to a byte array.
    var preprocessingPipeline = mlContext.Transforms.Conversion.MapValueToKey(
                    inputColumnName: "Label",
                    outputColumnName: "LabelAsKey")
                .Append(mlContext.Transforms.LoadImages(
                    inputColumnName: "ImagePath",
                    outputColumnName: "RawImage",
                    imageFolder: assetsRelativePath))
                .Append(mlContext.Transforms.ResizeImages(
                    inputColumnName: "RawImage",
                    outputColumnName: "ResReduxImage",
                    imageWidth: 512,
                    imageHeight: 459,
                    resizing: Microsoft.ML.Transforms.Image.ImageResizingEstimator.ResizingKind.Fill))
                .Append(mlContext.Transforms.ResizeImages(
                    inputColumnName: "ResReduxImage",
                    outputColumnName: "CroppedImage",
                    imageWidth: 512,
                    imageHeight: 459,
                    resizing: Microsoft.ML.Transforms.Image.ImageResizingEstimator.ResizingKind.IsoCrop,
                    cropAnchor: Microsoft.ML.Transforms.Image.ImageResizingEstimator.Anchor.Center))
                .Append(mlContext.Transforms.ExtractPixels(
                    inputColumnName: "CroppedImage",
                    outputColumnName: "Image",
                    outputAsFloatArray: false,
                    colorsToExtract: ImagePixelExtractingEstimator.ColorBits.Red));

     // Transform the raw ImageData into the TransformedData that the Model will train on
     IDataView preProcessedData = preprocessingPipeline.Fit(imageData).Transform(imageData);

     // Partition the data set for training and validation
     TrainTestData trainSplit = mlContext.Data.TrainTestSplit(data: preProcessedData, testFraction: 0.4);
     TrainTestData validationTestSplit = mlContext.Data.TrainTestSplit(trainSplit.TestSet);

     IDataView trainSet = trainSplit.TrainSet;
     IDataView validationSet = validationTestSplit.TrainSet;
     IDataView testSet = validationTestSplit.TestSet;

     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
     };

     var trainingPipeline = 
           mlContext.MulticlassClassification.Trainers.ImageClassification(classifierOptions)
           .Append(mlContext.Transforms.Conversion.MapKeyToValue("PredictedLabel"));

     // When I call Fit() in the next line of code, the following runtime error occurs: 
     // System.ArgumentOutOfRangeException: 'Schema mismatch for feature column 'Image': 
     // expected VarVector<Byte>, got Vector<Byte> Parameter name: inputSchema'
     ITransformer trainedModel = trainingPipeline.Fit(trainSet);
  

Я озадачен несоответствием переменных и векторов. За последние два дня я пробовал следующее, но безуспешно:

  • чтобы определить пользовательскую DataViewSchema для передачи преобразованию
  • для определения пользовательского представления данных путем вывода из IDataView
  • определить пользовательское отображение с помощью пользовательского отображения<> для изменения типа

Кто-нибудь может дать некоторые рекомендации о том, как выбраться из этой передряги? Я бы действительно хотел избежать чтения в изображении, предварительной обработки его, записи в файл, а затем вызова исходной функции LoadRawImageBytes(). Я не хочу, чтобы эта дополнительная файловая система работала.

Ответ №1:

Я понял, как заставить это работать. Проблема здесь заключалась в том, что базовые типы были правильными, но схема была неправильной. Я смог решить свою проблему с помощью пользовательского сопоставления и некоторой магии атрибутов.Смотрите документацию по пользовательскому отображению о том, как работает пользовательское отображение.

Исправленный фрагмент кода приведен ниже.

     // SOMEWHERE OUTSIDE OF THE FUNCTION DEFINE:
    // Define a class to represent the input column type for the custom transform
    class InputData 
    {
        [VectorType(1)] // attribute specifies vector type of known length
        public VBuffer<Byte> Image1; // the VBuffer<> type actually represents the data
    }

    // Define a class to represent the output column type for the custom transform
    class OutputData 
    {
        // THE MAGICAL FIX: attribute specifies vector type of unknown length (i.e. VarVector)
        [VectorType()]
        public VBuffer<Byte> Image; // the VBuffer<> type actually represents the data
    }

    // ------------------------------------------------------------------
    // INSIDE THE FUNCTION THAT WILL DO THE TRAINING
    // ------------------------------------------------------------------

    // Fill an IDataView with the Label and ImagePath information:
    // The ImageData class is defined for this purpose and it is just:
    // 
    //  public class ImageData {
    //       public string ImagePath { get; set; }
    //       public string Label { get; set; }
    //  }
    // 
    // Create a bunch of image names and their labels into a list of ImageData objects.
    IEnumerable<ImageData> images = LoadImagesFromDirectory(folder: assetsRelativePath, 
                                                            useFolderNameAsLabel: true);
    IDataView imageData = mlContext.Data.LoadFromEnumerable(images);

    // Define the preprocessing pipeline to:
    // 1. Map the Label to a unique key
    // 2. Load the image data using the ImagePath column
    // 3. Reduce the resolution of the image data
    // 4. Crop the reduced resolution image data
    // 5. Transform the image data from type "Image" to a byte array.
    var preprocessingPipeline = mlContext.Transforms.Conversion.MapValueToKey(
                    inputColumnName: "Label",
                    outputColumnName: "LabelAsKey")
                .Append(mlContext.Transforms.LoadImages(
                    inputColumnName: "ImagePath",
                    outputColumnName: "RawImage",
                    imageFolder: assetsRelativePath))
                .Append(mlContext.Transforms.ResizeImages(
                    inputColumnName: "RawImage",
                    outputColumnName: "ResReduxImage",
                    imageWidth: 512,
                    imageHeight: 459,
                    resizing: Microsoft.ML.Transforms.Image.ImageResizingEstimator.ResizingKind.Fill))
                .Append(mlContext.Transforms.ResizeImages(
                    inputColumnName: "ResReduxImage",
                    outputColumnName: "CroppedImage",
                    imageWidth: 512,
                    imageHeight: 459,
                    resizing: Microsoft.ML.Transforms.Image.ImageResizingEstimator.ResizingKind.IsoCrop,
                    cropAnchor: Microsoft.ML.Transforms.Image.ImageResizingEstimator.Anchor.Center))
                .Append(mlContext.Transforms.ExtractPixels(
                    inputColumnName: "CroppedImage",
                    outputColumnName: "Image1",
                    outputAsFloatArray: false,
                    colorsToExtract: ImagePixelExtractingEstimator.ColorBits.Red));

     // Transform the raw ImageData into the TransformedData that the Model will train on
     IDataView preProcessedData = 
           preprocessingPipeline.Fit(imageData).Transform(imageData);

     // Create an action representing the custom transform...
     // The data itself does not need to be changed at all, so this is just an 
     // identity transform
     Action<InputData, OutputData> convertVecType 
               = (input, output) => output.Image = input.Image1;
 
     var convertTypePipeline = mlContext.Transforms.CustomMapping(convertVecType, 
                                                                 "convertVecType");
     preProcessedData = 
              convertTypePipeline.Fit(preProcessedData).Transform(preProcessedData);

     // Partition the data set for training and validation
     TrainTestData trainSplit = mlContext.Data.TrainTestSplit(data: preProcessedData, testFraction: 0.4);
     TrainTestData validationTestSplit = mlContext.Data.TrainTestSplit(trainSplit.TestSet);

     IDataView trainSet = trainSplit.TrainSet;
     IDataView validationSet = validationTestSplit.TrainSet;
     IDataView testSet = validationTestSplit.TestSet;

     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
     };

     var trainingPipeline = 
           mlContext.MulticlassClassification.Trainers.ImageClassification(classifierOptions)
           .Append(mlContext.Transforms.Conversion.MapKeyToValue("PredictedLabel"));

     // When I call Fit() in the next line of code, the following runtime error occurs: 
     // System.ArgumentOutOfRangeException: 'Schema mismatch for feature column 'Image': 
     // expected VarVector<Byte>, got Vector<Byte> Parameter name: inputSchema'
     ITransformer trainedModel = trainingPipeline.Fit(trainSet);
  

Комментарии:

1. Привет, спасибо за сообщение и ответ! Я столкнулся с точно такой же проблемой. Хотя один вопрос… Ваше исправление помогло мне пройти этап обучения, но теперь я всегда получаю точность NaN в конце обучения. Я только применяю изменение размера и извлекаю пиксели. Если я удалю изменение размера и извлечение пикселей и вернусь к использованию LoadImagesRawBytes, все снова будет работать нормально. Случайно, есть идея? Спасибо

Ответ №2:

Я два года ломал голову над этим. Я действительно хотел указать размер вектора во время выполнения. Наконец, я нашел элегантный способ справиться с этим. Короткий ответ таков: создайте схему ввода на основе вашего класса inputData, измените соответствующие столбцы, чтобы они содержали правильный размер вектора, а затем передайте эту схему при создании DataView.

Более подробная версия выглядит следующим образом:

Я начинаю с моего класса ModelInput, который имеет подобные свойства. (У него также есть функции для загрузки изображений и получения данных …)

 public class ModelInput
{
    //This is just used for internal identification
    public string ImagePath { get; private set; }
    //An array of loaded bytes if needed.
    public byte[] ImageBytes { get; private set; }
    //An array of floats that took the ImageBytes and normalized them to floats. Normally the values are between 0 and 1.
    public float[] ImageFloats { get; private set; }
    //This is just used for internal identification
    public string ImageName { get; private set; }
    //The label
    public string Label { get; private set; }
    //An array of vector dimensions like [150,150,3]. This is set when the image is loaded and 
    //converted to ImageFloats
    public int[] VectorDimensions { get; private set; }
}
  

Я продолжаю и загружаю изображение, преобразую его в 3-канальный байтовый массив, а затем в массив с плавающей точкой, используя инструменты SixLabors image tools. В процессе я получаю размеры изображения и сохраняю его в векторных размерах.
Я удаляю всю предварительную обработку изображения в конвейере модели и делаю это в классе ModelInput, чтобы ML.Net модель ожидает массив значений с плавающей точкой.

Я загружаю первый ModelInput, который заполнит поле VectorDimensions. Затем я использую это, чтобы получить SchemaDef

 public SchemaDefinition GetSchemaDefinition()
{
        var inputSchemaDefinition = SchemaDefinition.Create(typeof(ModelInput), SchemaDefinition.Direction.Both);
        inputSchemaDefinition["ImageFloats"].ColumnType = new VectorDataViewType(NumberDataViewType.Single, VectorDimensions);
        inputSchemaDefinition["ImageBytes"].ColumnType = new VectorDataViewType(NumberDataViewType.Byte, VectorDimensions);
        return inputSchemaDefinition;
}
  

Загрузите свою модель как обычно.

Создайте IEnumerable из желаемых файлов

     internal IEnumerable<ModelInput> LoadData(string folderPath, bool shouldShuffle)
    {
        
        var images = LoadFileNames(folderPath, shouldShuffle);
        
        foreach (var item in images)
        {
//You can figure out how to load the image into ModelInput and create
//the byte or float arrays, and fill in the Label and VectorDimension fields
//I leave my code here as an example to give you ideas.
            //var options = new ModelInputOptions(item, batchOptions.LabelClass, batchOptions.ResnetType,
                //batchOptions.ResizeType, batchOptions.ChannelsDesired, batchOptions.Offset);
            ModelInput modelInput = new ModelInput();
            //You need to do something here to load the image and fill in the properties.
            //modelInput.LoadData(options);
            //Yield return is important here. This makes the images lazy loading.
            yield return modelInput;

        }

    }
    
    /// <summary>
    /// Loads file names and applies and ordering
    /// </summary>
    /// <param name="folder">the folder that contains all the categories and images</param>
    /// <param name="sortByFilename">false if the filename is used as the sort key, true to randomly shuffle</param>
    /// <returns>A sorted array of file names</returns>
    private string[] LoadFileNames(string folder, bool shouldShuffle)
    {
        var files = Directory.GetFiles(folder, "*",
           searchOption: SearchOption.AllDirectories);
        if (files == null || !files.Any())
        {
            return new string[0];
        }
        

        if (shouldShuffle)
        {
            return Shuffle(files.ToList());
        }
        else
        {
            //Use this if you want to order them by image name
            //return files.OrderBy(x=>Path.GetFileName(x)).ToArray();
            return files.Select(x=>Path.GetFileName(x)).ToArray();
        }
    }

    private string[] Shuffle(List<string> fileNames)
    {
      
        var result = fileNames.ToDictionary(k => Guid.NewGuid().ToString()).OrderBy(k=>k.Key);
       return result.Select(k => k.Value).ToArray();
    }
  

Загрузите свои изображения. Это отложенная загрузка из-за возврата yield.

 var images = LoadData(folderPath, shouldShuffle);
  

Вот волшебство, которого вы так долго ждали. Передайте изображения и измененную схему в dataview

 IDataView imageData = mlContext.Data.LoadFromEnumerable(images, schema);
  

Все должно работать!!!!