К сожалению, мир машинного обучения принадлежит python.

Он давно закрепился, как рабочий язык для Data Science , но Microsoft решила поспорить и представила свой инструмент, который легко можно интегрировать с экосистемой, которой сейчас пользуется весь мир. Так появился ML.NET, кросс-платформенная и открытая система машинного обучения для разработчиков .NET.

В данной статье, я хочу показать, что использовать ml.net - не сложнее, чем остальные варианты, которые есть, на реально работающем примере, ссылку на который оставлю внизу. Это канал в телеграмме, который в автоматическом режиме забирает данные, классифицирует их (это и будем рассматривать) и постит. Кому интересно, добро пожаловать.

Постановка задачи

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

Сбор данных

Для начала, я купил выгрузку данных твиттера по интересующему меня тегу, которую сервис отдает в формате csv(несколько разных файлов, которые различаются: сам твит, медиа, ссылки). Выбрав нужный мне файл, быстро пишем класс, чтобы распарсить данные, отсеять дубликаты. В итоге, в памяти, оставляются только ссылки на изображения, которые будут участвовать в обучении. Это хорошо, но все равно изображения нужно промаркировать, то есть разделить на категории. В моем случае, я выбрал: boys, girls, trash и other(вначале выбрал default, но, когда перешёл от строк к Enum, пришлось менять название категории). Все эти фото, я выгрузил, скрупулёзно разделил на папочки, которые отражали метку фотографии, так что настало время для самого интересного - код.

Обучение модели

Для определения того, что изображено на фото, используются алгоритмы классификации изображений.

Классификация изображений 

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

Конкретнее, я буду использовать глубокое обучение.

Глубокое обучение

Глубокое обучение (глубинное обучение; англ. Deep learning) — совокупность методов машинного обучения (с учителем, с частичным привлечением учителя, без учителя, с подкреплением), основанных на обучении представлениям (англ. feature/representation learning), а не специализированным алгоритмам под конкретные задачи.

Чтобы не тратить множество часов на обучение, проще всего взять готовую модель, которая уже содержит признаки изображений, и дообучить её для своих классов, чем обучать её с ноля. Я буду использовать TensorFlow Inception , которая уже обучена на популярном сете ImageNet.

Теперь можно добавить тип проекта "Библиотека классов", для более удобного переиспользования данной модели и наконец начать писать код (распределение 2000 картинок, у меня заняло около 2х часов, при условии, что мне требовалось +- равное количество изображений в каждой из категорий).

Оффтоп

Я немного экспериментировал с количеством изображений в наборах для обучения, но лучше всего себя показывал вариант, когда количество изображений, в каждой категории, примерно, равно. В данном примере используется 4 категории по 500 фото.

Сначала создадим класс. Например, model и после этого добавим нужные библиотеки через nuget и добавим их к файлу класса:

using Microsoft.ML; 
using Microsoft.ML.Data; 

Теперь добавим элементы, которые потребуются для работы основного функционала:

    private readonly string _inceptionTensorFlowModel; // путь к модели Inception 
    private MLContext mlContext;
    private ITransformer model;
    private DataViewSchema schema;
    private string modelName = "model.zip"; // название модели для её сохранения
    private string _setsPath = @"C:\datasets"; // путь к сетам и место, куда будет сложена модель после сохранения
    
    
        public Model(string inceptionTensorFlowModel)
        {
            mlContext = new MLContext();
            _inceptionTensorFlowModel = inceptionTensorFlowModel;
        }

MLContext - это отправная точка в мир машинного обучения в .NET. Этот класс "связывает" всю работу и все элементы, примерно, как DbContext в EntityFramework.

ITransformer - описывает то, как изменять данные, и то, как они будут выглядеть после трансформации.

DataViewSchema - схема данных колонок сета.

Теперь добавил классы, которые будут описывать "вход", то есть данные, которые мы будем подавать приложению.

public class ImageData
    {
        [LoadColumn(0)]
        public string ImagePath;

        [LoadColumn(1)]
        public string Label;

  		//метод, который я использую, чтобы забрать данные из папок и отмаркировать их
        public static (IEnumerable<ImageData> train, IEnumerable<ImageData> test) ReadData(string pathToFolder)
        {
            List<ImageData> list = new List<ImageData>();
            var directories = Directory.EnumerateDirectories(pathToFolder);
            foreach (var dir in directories)
            {
                if (!dir.Contains("girls") && !dir.Contains("boys") && !dir.Contains("trash") && !dir.Contains("other"))
                    continue;
                var label = dir.Split(@"\").Last();
                foreach (var file in Directory.GetFiles(dir))
                {
                    list.Add(new ImageData()
                    {
                        ImagePath = file,
                        Label = label
                    });
                }
            }
            list = list.Shuffle().ToList();
            return GetSets(list);
        }

				//Делим изображения на тестовую и основную выборки
        public static (IEnumerable<ImageData> train, IEnumerable<ImageData> test) GetSets(IEnumerable<ImageData> data)
        {
            var trainCount = data.Count() / 100 * 99;
            var train = data.Take(trainCount);
            var test = data.Skip(trainCount);
            return (train, test);
        }
    }
    public class ImagePrediction : ImageData
    {
        [ColumnName("Score")]
        public float[] Score;

        public string PredictedLabelValue;
    }

И расширение для IEnumerable для перемешивания данных:

Оффтоп по новому редактору

Попытался в спойлер вставить код, после чего у меня полностью зависла вкладка браузера

 public static IEnumerable<T> Shuffle<T>(this IEnumerable<T> source)
        {
            return source.Shuffle(new Random());
        }
        public static IEnumerable<T> Shuffle<T>(this IEnumerable<T> source, Random rng)
        {
            if (source == null) throw new ArgumentNullException("source");
            if (rng == null) throw new ArgumentNullException("rng");

            return source.ShuffleIterator(rng);
        }

        private static IEnumerable<T> ShuffleIterator<T>(
            this IEnumerable<T> source, Random rng)
        {
            var buffer = source.ToList();
            for (int i = 0; i < buffer.Count; i++)
            {
                int j = rng.Next(i, buffer.Count);
                yield return buffer[j];

                buffer[j] = buffer[i];
            }
        }

А также скруктуру, которая будет описывать настройки для модели:

private struct InceptionSettings
        {
            public const int ImageHeight = 224;
            public const int ImageWidth = 224;
            public const float Mean = 117;
            public const float Scale = 1;
            public const bool ChannelsLast = true;
        }

Она нужна, чтобы просто дать более понятные имена параметрам.

Наконец приготовления закончены и можно начинать писать метод обучения модели:

private double TrainModel()
        {
            IEstimator<ITransformer> pipeline = mlContext.Transforms.LoadImages(outputColumnName: "input", imageFolder: "", inputColumnName: nameof(ImageData.ImagePath))
                           .Append(mlContext.Transforms.ResizeImages(outputColumnName: "input", imageWidth: InceptionSettings.ImageWidth, imageHeight: InceptionSettings.ImageHeight, inputColumnName: "input"))
                           .Append(mlContext.Transforms.ExtractPixels(outputColumnName: "input", interleavePixelColors: InceptionSettings.ChannelsLast, offsetImage: InceptionSettings.Mean))
                           .Append(mlContext.Model.LoadTensorFlowModel(_inceptionTensorFlowModel).
                               ScoreTensorFlowModel(outputColumnNames: new[] { "softmax2_pre_activation" }, inputColumnNames: new[] { "input" }, addBatchDimensionInput: true))
                           .Append(mlContext.Transforms.Conversion.MapValueToKey(outputColumnName: "LabelKey", inputColumnName: "Label"))
                           .Append(mlContext.MulticlassClassification.Trainers.LbfgsMaximumEntropy(labelColumnName: "LabelKey", featureColumnName: "softmax2_pre_activation"))
                           .Append(mlContext.Transforms.Conversion.MapKeyToValue("PredictedLabelValue", "PredictedLabel"))
                           .AppendCacheCheckpoint(mlContext);

            var loadImages = ImageData.ReadData(_setsPath);
            IDataView trainingData = mlContext.Data.LoadFromEnumerable<ImageData>(loadImages.train);
            ITransformer model = pipeline.Fit(trainingData);
            IDataView testData = mlContext.Data.LoadFromEnumerable<ImageData>(loadImages.test);
            IDataView predictions = model.Transform(testData);
            List<ImagePrediction> imagePredictionData = mlContext.Data.CreateEnumerable<ImagePrediction>(predictions, true).ToList();
            MulticlassClassificationMetrics metrics =
                mlContext.MulticlassClassification.Evaluate(predictions,
                  labelColumnName: "LabelKey",
                  predictedLabelColumnName: "PredictedLabel");
            schema = trainingData.Schema;
            return metrics.LogLoss;
        }

Разберем по порядку:

IEstimator<ITransformer> pipeline = mlContext.Transforms.LoadImages(outputColumnName: "input", imageFolder: "", inputColumnName: nameof(ImageData.ImagePath))
     .Append(mlContext.Transforms.ResizeImages(outputColumnName: "input", imageWidth: InceptionSettings.ImageWidth, imageHeight: InceptionSettings.ImageHeight, inputColumnName: "input"))
     .Append(mlContext.Transforms.ExtractPixels(outputColumnName: "input", interleavePixelColors: InceptionSettings.ChannelsLast, offsetImage: InceptionSettings.Mean))
                           

Создаем пайплайн. Добавляем загрузку, изменение размера и извлечение пикселей из изображений:

.Append(mlContext.Model.LoadTensorFlowModel(_inceptionTensorFlowModel).
    ScoreTensorFlowModel(outputColumnNames: new[] { "softmax2_pre_activation" }, inputColumnNames: new[] { "input" }, addBatchDimensionInput: true))

Применение входных данных к модели глубокого обучения и формирование выходных данных с помощью модели называется оценкой. Добавляем в пайплайн модель по пути, заданному раннее и оцениваем модель:

.Append(mlContext.Transforms.Conversion.MapValueToKey(outputColumnName: "LabelKey", inputColumnName: "Label"))

Для работы моделей ml.net, метки должны быть в формате ключей, которые являются целочисленными значениями.

Добавляем алгоритм классификации:

.Append(mlContext.MulticlassClassification.Trainers.LbfgsMaximumEntropy(labelColumnName: "LabelKey", featureColumnName: "softmax2_pre_activation"))

И средство преобразования ключей обратно в строку:

.Append(mlContext.Transforms.Conversion.MapKeyToValue("PredictedLabelValue", "PredictedLabel"))
.AppendCacheCheckpoint(mlContext);

Теперь остальная часть метода:

var loadImages = ImageData.ReadData(_setsPath);
            IDataView trainingData = mlContext.Data.LoadFromEnumerable<ImageData>(loadImages.train);
            model = pipeline.Fit(trainingData);

Данный отрезок отвечает за получение данных, их загрузку и обучение модели.

						IDataView testData = mlContext.Data.LoadFromEnumerable<ImageData>(loadImages.test);
            IDataView predictions = model.Transform(testData);
            List<ImagePrediction> imagePredictionData = mlContext.Data.CreateEnumerable<ImagePrediction>(predictions, true).ToList();
            MulticlassClassificationMetrics metrics =
                mlContext.MulticlassClassification.Evaluate(predictions,
                  labelColumnName: "LabelKey",
                  predictedLabelColumnName: "PredictedLabel");

Сначала мы загружаем наш тестовый сет. Далее трансформируем его и пытаемся классифицировать. После чего, этот "классифицированный" список используем для оценки точности модели.

            schema = trainingData.Schema;
            return metrics.LogLoss;

Записываем схему данных в переменную класса и возвращаем LogLoss(метрика точности модели).

Наконец метод обучения модели готов, осталось только собрать все в одну кучу.

Сразу же создадим метод, которым будем сохранять модель на диске, чтобы её можно было в дальнейшем использовать:

  public void SaveModel() => mlContext.Model.Save(model, schema, Path.Combine(_setsPath, modelName));

И после добавим публичный метод, которым мы сразу и учим, и сохраняем модель:

    public void FitModel()
    {
        var LogLoss = TrainModel();
        Console.WriteLine($"LogLoss is {LogLoss}");
        SaveModel();
    }

Можно было после обучения сразу же сохранять модель, но данный метод удобнее будет расширить, если будет желание переучивать модель, записывать лог лосс и сохранять в том случае, если точность выше, а не ниже.

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

Под переменными класса добавим сам классификатор:

    private PredictionEngine<ImageData, ImagePrediction> predictor;

А теперь и метод, который его будет использовать(+ сразу же и загрузка модели):

        public ImagePrediction ClassifySingleImage(string filePath)
        {
            if (model == null)
                LoadModel();
            if (predictor == null)
                predictor = mlContext.Model.CreatePredictionEngine<ImageData, ImagePrediction>(model);
            var imageData = new ImageData()
            {
                ImagePath = filePath
            };
            return predictor.Predict(imageData);
        }
        public void LoadModel() =>
            model = mlContext.Model.Load(Path.Combine(_setsPath, modelName), out schema);

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

Для демострации работы, я добавил в проект приложение консольного типа и написал такой код:

 static void Main(string[] args)
        {
            Console.ForegroundColor = ConsoleColor.White;
            Stopwatch s = new Stopwatch();
            s.Start();

            Model model = new Model(@"C:\tensorflow_inception_graph.pb");
            model.FitModel();
            Console.WriteLine($"##### Model train ended for {s.Elapsed.Minutes}:{s.Elapsed.Seconds} #####");

            s.Restart();

            var res1 = model.ClassifySingleImage(@"C:\EugRqKFXUAYMTWz.jpg");
            Console.WriteLine($" > It's trash. Classification result is {res1.PredictedLabelValue} with score: {res1.Score.Max()}");
            Console.WriteLine($"##### Ended for {s.Elapsed.Minutes}:{s.Elapsed.Seconds} #####");

            s.Restart();

            var res2 = model.ClassifySingleImage(@"C:\EvpmOjIXcAMgj5r.jpg");
            Console.WriteLine($" > It's girl. Classification result is {res2.PredictedLabelValue} with score: {res1.Score.Max()}");
            Console.WriteLine($"##### Ended for {s.Elapsed.Minutes}:{s.Elapsed.Seconds} #####");
        }
Выбранные изображения

И получил такие результаты:

Несмотря на достаточно слабые метрики (я все таки использовал для тестов 20 изображений): 0.55, но модель отлично справилась со своими задачами. Именно такую модель, я использую для своего nsfw-бота, который получает данные из твиттера, а потом классифицирует и постит их.

Так что достаточно не сложно обучить модель и добавить в свой проект, главное желание разобраться. И никогда не стоит останавливаться в том, чтобы учиться чему-то новому.

P.s. ссылка на сеты