#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);
Все должно работать!!!!