ML.NET часть 2 —распознавание рукописных чисел

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

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

Файл представляет из себя строки по 65 чисел, первые 64 числа — это описание изображения, о котором я расскажу немного ниже, а 65 число — это непосредственно значение, которое должно быть предсказано по этим данным.

Как не трудно догадаться 64 числа — это массив данных 8x8 элементов, каждый элемент — это число от 0 до 16. Получается этот набор чисел из изображения размером 32x32 пикселей. Если изображение разбить на области размером 4x4 и посчитать количество закрашенных пикселей в этой области, то это и будет одно из чисел нашего датасета. Процесс можно описать как сжатие изображения 32x32 до размеров 8x8, с дополнительными условиями для большей точности. В самом датасете значения массива записаны в одну строку через запятую.

Для наглядности приведу это изображение, демонстрирующее процесс:

Демонстрация процесса сжатия изображения до размеров 8x8

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

Для удобства рисования новых чисел я добавил простую web страницу, в которой на canvas можно что-то нарисовать, а потом отправить полученное изображение для анализа.

Выглядеть страница будет следующим образом:

Внешний вид созданной страницы

При нажатии кнопки Send будет происходить передача изображение, а полученный ответ с предсказанием будет записываться вместо звёздочки. Кнопка Clear Area будет очищать область для рисования.

Теперь нужно добавить модели данных и функционал обучения. Сделать это можно через ML.NET Model Builder, выбрав Issue Classification.

Использование Issue Classificatin в ML.NET Model Builder

Остальной процесс будет аналогичен тому, что было показано в первой статье. Но уже после добавления модели в проект нужно внести небольшие изменения, чтобы для значения пикселей датасета подгружало в массив, а не в 64 отдельные переменные, добавлю массив float значений PixelValues и в атрибуте укажу какие столбцы будет в его загружены.

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

public class ModelInput
{
    [ColumnName("PixelValues"), LoadColumn(0,63)]
    [VectorType(64)]
    public float[] PixelValues;


    [ColumnName("Number"), LoadColumn(64)]
    public float Number { get; set; }

}

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

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

Исходный код контроллера:

public class HomeController : Controller
{
    private const int SizeOfImage = 32;
    private const int SizeOfArea = 4;
    private const string ModelName = "MLModel.zip";

    public IActionResult Index()
    {
        return View();
    }

    [HttpPost]
    public IActionResult Upload(string imgBase64)
    {
        if (string.IsNullOrEmpty(imgBase64))
        {
            return BadRequest(new { prediction = "-", dataset = string.Empty });
        }
        MLContext mlContext = new MLContext();
        ITransformer mlModel = mlContext.Model.Load(ModelName, out var modelInputSchema);
        var predEngine = mlContext.Model.CreatePredictionEngine<ModelInput, ModelOutput>(mlModel);

        var datasetValue = GetDatasetValuesFromImage(imgBase64);

        var input = new ModelInput();
        input.PixelValues = datasetValue.ToArray();
        ModelOutput result = predEngine.Predict(input);

        return Ok(new { prediction = result.Prediction, dataset = string.Join(",", datasetValue) });
    }

    private List<float> GetDatasetValuesFromImage(string base64Image)
    {
        base64Image = base64Image.Replace("data:image/png;base64,", "");
        var imageBytes = Convert.FromBase64String(base64Image).ToArray();

        Image image;

        using (var stream = new MemoryStream(imageBytes))
        {
            image = Image.FromStream(stream);
        }

        var res = new Bitmap(SizeOfImage, SizeOfImage);
        using (var g = Graphics.FromImage(res))
        {
            g.Clear(Color.White);
            g.DrawImage(image, 0, 0, SizeOfImage, SizeOfImage);
        }

        var datasetValue = new List<float>();

        for (int i = 0; i < SizeOfImage; i += SizeOfArea)
        {
            for (int j = 0; j < SizeOfImage; j += SizeOfArea)
            {
                var sum = 0;
                for (int k = i; k < i + SizeOfArea; k++)
                {
                    for (int l = j; l < j + SizeOfArea; l++)
                    {
                        sum += res.GetPixel(l, k).Name == "ffffffff" ? 0 : 1;
                    }
                }
                datasetValue.Add(sum);
            }
        }

        return datasetValue;
    }
}

В функцию GetDatasetValuesFromImage передаётся изображение, закодированное в base64 (это самый удобный способ передать изображение с web страницы используя JavaScript). Оно масштабируется до размера 32x32 пикселей, а замет вычисляются значения для размеров 8x8. Полученные в результате работы функции данные передаются в качестве входных данных для предсказания. В ответ возвращается предсказанное значение и строка с вычисленными значениями, которые можно использовать для пополнения датасета.

Работает всё следующим образом:

Окончательная работа проекта

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

Вывод данных для пополнения датасета в консоле браузера

Исходные код проекта здесь: https://github.com/flash2048/Handwriting_recognition_ML_NET

Задачи с предсказанием из нескольких вариантов при наличии хорошего датасета решаются с использованием ML.NET без особых проблем. Результаты предсказания всегда можно улучшить, дополнив датасет и натренировав модель заново. Также следует иметь ввиду, что передаваемые для предсказания данные должны соответствовать данным из датасета, на котором происходило обучение. Именно для этого исходное изображение сперва масштабировалось до размеров 32x32, а замет вычислялись значения для размеров 8x8.

Приятного программирования.

Добавить комментарий