Внедрение моделей TensorFlow с помощью ML.NET
Давайте посмотрим, как использовать предварительно обученную модель TensorFlow с ML.NET для создания важных прогнозов.
Получить модель
Сначала мы собираемся пойти дальше и выбрать предварительно обученную модель. Существует несколько хороших источников предварительно обученных моделей, таких как Hugging Face и tfhub. Мы собираемся использовать модель tfhub, которая предсказывает достопримечательности Северной Америки по изображениям — https://tfhub.dev/google/on_device_vision/classifier/landmarks_classifier_north_america_V1/1.
Преобразуйте и оптимизируйте модель
Теоретически мы могли бы начать использовать модель как есть, но мы можем добиться большего. Модель TensorFlow непросто использовать на всех возможных платформах, таких как Linux, macOS и Windows, а также на всех архитектурах ЦП, таких как ARM64.
ONNX — это формат портативного представления моделей машинного обучения. Кроме того, модели ONNX можно легко оптимизировать и, таким образом, сделать их меньше и быстрее.
Самый простой способ преобразовать загруженную модель TensorFlow в модель ONNX — использовать инструмент tf2onnx из https://github.com/onnx/tensorflow-onnx.
Следуйте инструкциям по его установке (используйте контейнер разработки с Python 3.10, чтобы поддерживать чистоту вашего компьютера), а затем запустите эту команду:
python -m tf2onnx.convert --opset 16 --tflite lite-model_on_device_vision_classifier_landmarks_classifier_north_america_V1_1.tflite --output lite-model_on_device_vision_classifier_landmarks_classifier_north_america_V1_1.onnx
Теперь у вас должен быть файл с именем lite-model_on_device_vision_classifier_landmarks_classifier_north_america_V1_1.onnx, содержащий оптимизированную модель ONNX. Размер файла уменьшился с 50,9 МБ до 42,7 МБ. Хороший!
Если вы начали с модели ONNX, которую все еще хотите оптимизировать, вы можете использовать официальный инструмент оптимизатора ONNX https://github.com/onnx/optimizer.
Сделайте модель ONNX доступной для ML.NET
На этом этапе мы сообщаем ML.NET, каковы входные и выходные данные модели, и упаковываем модель таким образом, чтобы ML.NET мог с ней работать.
Входы и выходы
В документации уже сказано, что «входные данные должны представлять собой 3-канальные цветные изображения RGB размером 321 x 321, масштабированные до [0, 1]», а выходные данные — «вектор из 99424 оценок сходства».
Нам нужно узнать точные имена входных и выходных тензоров. Такой инструмент, как Нетрон, делает это очень простым. Откройте исходную модель .tflite и/или ONNX в Netron и нажмите кнопку Свойства модели в левом нижнем углу.
Для нашей модели входные данные называются uint8_image_input, а выходные — transpose_1. Мы примем это к сведению.
Модель пакета для ML.NET
Создайте новое консольное приложение, добавьте ссылки на эти пакеты и восстановите их.
<PackageReference Include="Microsoft.ML" Version="2.0.1" /> <PackageReference Include="Microsoft.ML.ImageAnalytics" Version="2.0.1" /> <PackageReference Include="Microsoft.ML.OnnxRuntime" Version="1.15.1" /> <PackageReference Include="Microsoft.ML.OnnxTransformer" Version="2.0.1" />
Далее, чтобы упростить нашу жизнь и сделать код более аккуратным, давайте определим несколько констант и типов для ввода и вывода модели.
public static class LandmarkModelSettings { public const string OnnxModelName = "lite-model_on_device_vision_classifier_landmarks_classifier_north_america_V1_1.onnx"; public const string Input = "uint8_image_input"; public const string Output = "transpose_1"; public const string MlNetModelFileName = "landmark_classifier_onnx.zip"; public const string LabelFileName = "landmarks_classifier_north_america_V1_label_map.csv"; } public class LandmarkInput { public const int ImageWidth = 321; public const int ImageHeight = 321; public LandmarkInput(Stream imagesStream) { Image = MLImage.CreateFromStream(imagesStream); } [ImageType(width: ImageWidth, height: ImageHeight)] public MLImage Image { get; } } public class LandmarkOutput { [ColumnName(LandmarkModelSettings.Output)] public float[] Prediction { get; set; } }
Код относительно понятен. Я хотел бы отметить, что на странице tfhub, откуда мы скачали модель, также есть файл .csv с метками для каждого результата прогнозирования. Я сохранил его локально с именем из константы LabelFileName.
Теперь мы готовы описать входные и выходные данные ML.NET, мы также загружаем модель ONNX и сохраняем ее в собственном формате ML.NET.
// Configure ML model var mlCtx = new MLContext(); var pipeline = mlCtx .Transforms // Adjust the image to the required model input size .ResizeImages( inputColumnName: nameof(LandmarkInput.Image), imageWidth: LandmarkInput.ImageWidth, imageHeight: LandmarkInput.ImageHeight, outputColumnName: "resized" ) // Extract the pixels form the image as a 1D float array, but keep them in the same order as they appear in the image. .Append(mlCtx.Transforms.ExtractPixels( inputColumnName: "resized", interleavePixelColors: true, outputAsFloatArray: false, outputColumnName: LandmarkModelSettings.Input) ) // Perform the estimation .Append(mlCtx.Transforms.ApplyOnnxModel( modelFile: "./" + LandmarkModelSettings.OnnxModelName, inputColumnName: LandmarkModelSettings.Input, outputColumnName: LandmarkModelSettings.Output ) ); // Save ml model var transformer = pipeline.Fit(mlCtx.Data.LoadFromEnumerable(new List<LandmarkInput>())); mlCtx.Model.Save(transformer, null, LandmarkModelSettings.MlNetModelFileName);
Давайте пройдемся по коду.
Сначала мы говорим ML.NET изменить размер любого получаемого изображения до размера, ожидаемого загруженной моделью; в данном случае 321х321 пикселей. Изображение с измененным размером должно быть помещено в столбец «Измененный размер». Из столбца с измененным размером мы извлекаем пиксели изображения в одномерный массив чисел с плавающей запятой и выводим эти данные в столбец transpose_1, потому что именно этого ожидает модель. . На последнем шаге мы вызываем модель для прогнозирования.
Наконец, модель сохраняется под именем landmark_classifier_onnx.zip. Теперь он еще больше сократился до 39,6 МБ.
Загрузите модель ML.NET и сделайте прогноз
Прежде чем продолжить, нам следует убедиться, что модель ML.NET действительно работает так, как ожидалось. Для этого мы загружаем модель из файла landmark_classifier_onnx.zip и передаем ей файл .jpg со статуей свободы. Прогноз должен содержать несколько записей, но той, которая имеет наибольшую вероятность, должна быть наша статуя свободы.
// Load ml model var mlCtx2 = new MLContext(); var loadedModel = mlCtx2.Model.Load(LandmarkModelSettings.MlNetModelFileName, out var _); var predictionEngine = mlCtx2.Model.CreatePredictionEngine<LandmarkInput, LandmarkOutput>(loadedModel); // Predict var sw = new Stopwatch(); sw.Start(); await using var imagesStream = File.Open("Landmarks/Statue_of_Liberty_7.jpg", FileMode.Open); var prediction = predictionEngine.Predict(new LandmarkInput(imagesStream)); Console.WriteLine($"Prediction took: {sw.ElapsedMilliseconds}ms"); // Labels start from the second line and each contains the 0-based index, a comma and a name. var labels = await File.ReadAllLinesAsync(LandmarkModelSettings.LabelFileName) .ContinueWith(lineTask => { var lines = lineTask.Result; return lines .Skip(1) .Select(line => line.Split(",").Last()) .ToArray(); }); // Merge the prediction array with the labels. Produce tuples of landmark name and its probability. var predictions = prediction.Prediction .Select((val, index) => (index, probabiliy: val)) .Where(pair => pair.probabiliy > 0.55f) .Select(pair => (name: labels[pair.index], pair.probabiliy)) .GroupBy(pair => pair.name) .Select(group => (name: group.Key, probability: group.Select((p) => p.probabiliy).Max())) .OrderByDescending(pair => pair.probability) ; // Output var predictionsString = string.Join(Environment.NewLine, predictions.Select(pair => $"name: {pair.name}, probability: {pair.probability}")); Console.WriteLine(string.Join(Environment.NewLine, predictionsString));
Как вы могли заметить, у меня в папке Ориентиры было изображение под названием Statue_of_Liberty_7.jpg.
Особенностью этой модели является то, что прогноз содержит дубликаты, т. е. выходной массив чисел с плавающей запятой содержит несколько записей для одного и того же ориентира. На странице документации tfhub говорится, что нужно просто использовать прогноз ориентира, который имеет наибольшую вероятность. В зависимости от выбранной вами модели вам может не потребоваться это делать, и может быть достаточно просто присвоить метку каждой позиции из выходных данных.
На моем Macbook Pro M1 Pro результат выглядит так
Prediction took: 108ms name: Liberty Island, probability: 0,9176943 name: New York Harbor, probability: 0,798547 name: Liberty State Park, probability: 0,7981717 name: The Terminal Tower Residences, probability: 0,6972256
Поздравляем 🎉, вы готовы использовать модель! Продолжайте читать, если вы хотите интегрировать его в приложение AspNet.Core.
Предоставьте его как веб-API
Создайте новый проект веб-API AspNet.Core и добавьте следующие ссылки на пакеты.
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.9"/> <PackageReference Include="Microsoft.Extensions.ML" Version="2.0.1" /> <PackageReference Include="Microsoft.ML" Version="2.0.1" /> <PackageReference Include="Microsoft.ML.OnnxRuntime" Version="1.15.1" /> <PackageReference Include="Microsoft.ML.OnnxTransformer" Version="2.0.1" />
Последние необходимы только во время выполнения. Если вы пропустите их, вы получите такие ошибки, как
System.Reflection.TargetInvoctionException: исключение было создано целью вызова.
— -> System.IO.FileNotFoundException: не удалось загрузить файл или сборку «Microsoft.ML.OnnxTransformer, Version=1.0.0.0, Culture» =нейтрально, PublicKeyToken=cc7b13ffcd2ddd51'. Система не может найти указанный файл.
Имя файла: «Microsoft.ML.OnnxTransformer, Version=1.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51»
or
При выполнении запроса произошло необработанное исключение.
System.Reflection.TargetInvocationException: Исключение было создано целью вызова.
— -> System.Reflection.TargetInvocationException: Исключение было создано объектом вызова. цель вызова.
— -> System.TypeInitializationException: инициализатор типа для «Microsoft.ML.OnnxRuntime.NativeMethods» выдал исключение.
— —> System.DllNotFoundException: невозможно загрузить общую библиотеку ' onnxruntime» или одну из его зависимостей. Чтобы диагностировать проблемы с загрузкой, рассмотрите возможность установки переменной среды DYLD_PRINT_LIBRARIES:
dlopen(
Вы могли бы ожидать, что загрузка модели будет выглядеть точно так же, как мы делали раньше, когда делали наш первый прогноз, но Microsoft рекомендует нечто иное. Поскольку PredictionEngine не является потокобезопасным и его создание требует больших затрат, нам следует использовать PredictionEnginePool — ссылку на документы.
Убедитесь, что константы, типы ввода и вывода доступны, и добавьте эту строку в файл Program.cs.
builder.Services .AddPredictionEnginePool<LandmarkInput, LandmarkOutput>() .FromFile(Path.Combine( Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!, LandmarkModelSettings.MlNetModelFileName ));
Он добавляет PredictionEnginePool для наших типов ввода и вывода для модели ML.NET по указанному пути.
Вы можете организовать свой код так, как лучше всего подходит для вашего проекта, но я добавил специальный одноэлементный сервис, который загружает метки и сортирует их.
internal class NorthAmericanLabelProvider : INorthAmericanLabelProvider { private Lazy<string[]>? _lazyLabels; public string[] GetLabels() { _lazyLabels ??= new Lazy<string[]>(() => { var labelFilePath = Path.Combine( Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!, LandmarkModelSettings.LabelFileName); var labelLines = File.ReadAllLines(labelFilePath); return labelLines.Skip(1) .Select(line => line.Split(",")) .Select(lineTokens => (Index: int.Parse(lineTokens[0]), LandmarkName: lineTokens[1])) .OrderBy(tuple => tuple.Index) .Select(tuple => tuple.LandmarkName) .ToArray(); }); return _lazyLabels.Value; } }
Он считывает содержимое файла, анализирует и сортирует его только один раз, используя Lazy‹T›.
Затем я создал сервис под названием NorthAmericanLandmarkPredictor, который выполняет фактическое прогнозирование. Он использует PredictionEnginePool, который мы зарегистрировали ранее, и INorthAmericanLabelProvider.
internal class NorthAmericanLandmarkPredictor : INorthAmericanLandmarkPredictor { private readonly PredictionEnginePool<LandmarkInput, LandmarkOutput> _predictionEnginePool; private readonly INorthAmericanLabelProvider _northAmericanLabelProvider; public NorthAmericanLandmarkPredictor(PredictionEnginePool<LandmarkInput, LandmarkOutput> predictionEnginePool, INorthAmericanLabelProvider northAmericanLabelProvider) { _predictionEnginePool = predictionEnginePool; _northAmericanLabelProvider = northAmericanLabelProvider; } public List<LandmarkPrediction> PredictLandmark(Stream imageStream) { var labels = _northAmericanLabelProvider.GetLabels(); // Make prediction // Post process prediction - the output contains duplicates, so we should group by label and take the entry with the highest probability. // Docs - https://tfhub.dev/google/on_device_vision/classifier/landmarks_classifier_north_america_V1/1 var landmarkOutput = _predictionEnginePool.Predict(new LandmarkInput(imageStream)); return landmarkOutput.Prediction .Zip(labels, (probability, landmarkName) => (LandmarkName: landmarkName, Probability: probability)) .GroupBy(tuple => tuple.LandmarkName) .Select(group => new LandmarkPrediction( group.Key, group.MaxBy(tuple => tuple.Probability).Probability )) .OrderByDescending(prediction => prediction.Probability) .ToList(); } }
Наконец, в контроллере мы можем внедрить INorthAmericanLandmarkPredictor и делать прогнозы на основе загруженных изображений.
[ApiController] [Route("[controller]")] public class LandmarkPredictionController : ControllerBase { private readonly INorthAmericanLandmarkPredictor _northAmericanLandmarkPredictor; public LandmarkPredictionController(INorthAmericanLandmarkPredictor northAmericanLandmarkPredictor) { _northAmericanLandmarkPredictor = northAmericanLandmarkPredictor; } [HttpPost("NorthAmerica")] public async Task<List<LandmarkPrediction>> Get(IFormFile image) { var prediction = _northAmericanLandmarkPredictor.PredictLandmark(image.OpenReadStream()); return prediction; } }
Во встроенном пользовательском интерфейсе Swagger это выглядит так
Заключение
Вот и все, ребята!
Теперь вы готовы реализовать множество различных моделей машинного обучения с помощью ML.NET и представить их в удобном веб-API.
Вы можете найти весь исходный код по адресу https://github.com/mariusmuntean/Operationalize.ML.NET
Он находится под лицензией MIT, поэтому вы можете использовать его по своему усмотрению.
Если хотите купить мне кофе, напишите мне в Twitter/X 😁