Наши партнеры

UnixForum





Библиотека сайта rus-linux.net

Система распознавания символов (OCR)

Оригинал: Optical Character Recognition (OCR)
Автор: Marina Samuel
Дата публикации: July 12, 2016
Перевод: Н.Ромоданов
Дата перевода: февраль 2017 г.

Это вторая часть статьи "Система распознавания символов (OCR)".
Перейти к
началу.

Принятие решений в простой системе распознавания символов

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

Во-первых, мы должны решить, что мы хотим, чтобы могла делать наша система. Для того, чтобы система была простой, давайте позволим пользователям рисовать одну цифру и иметь возможность обучать систему распознавать эту цифрой или потребовать, чтобы система предсказала, что нарисована именно эта цифра. Хотя система распознавания символов может работать локально на отдельной взятой машине, использование архитектуры типа «клиент-сервер» предоставляет нам большую гибкость. Она позволяет применять краудсорсинговый вариант обучения нейронной сети, а также появляется возможность использовать для обработки интенсивных вычислений мощные серверы.

Наша система распознавания символов будет состоять из пяти основных компонентов, разделенных на пять отдельных файлов. Это следующем компоненты и файлы:

  • клиентская часть (ocr.js)
  • серверная часть (server.py)
  • простой пользовательский интерфейс (ocr.html)
  • нейронная сеть, обученная с помощью алгоритма обратного распространения (ocr.py)
  • скрипт, настраивающий нейронную сеть (neural_network_design.py)

Пользовательский интерфейс будет простым: холст (canvas) для рисования на нем цифры и кнопок, при нажатии которых либо происходит обучение нейронной сети, либо запрашивается прогноз. Клиентская часть нашей системы будет брать нарисованную цифру, преобразовывать ее в массив и передать массив в серверную часть, на корой этот массив будет использован либо в качестве обучающей выборки, либо в качестве запроса предсказания. Серверная часть будет просто с помощью вызовов API перенаправлять обучающий запрос или запрос предсказания в модуль нейронной сети. Модуль искусственной нейронной сети будет при первой инициализации сети обучать ее на существующем наборе данных. Затем он сохранит веса нейронной сети в файле и при последующих запусках будет повторно их загружать. Это тот самый модуль, где реализована основная логика обучения и прогнозирования. И, наконец, скрипт, настраивающий нейронную сеть, предназначен для экспериментирования с различным количеством узлов сети и принятия решений относительно того, что же будет работать лучше. Вместе все эти части формируют очень упрощенную, но функционально полную систему распознавания символов.

Теперь, когда мы на высоком уровне продумали то, как система будет работать, пришло время превратить эти мысли в код!

Простой интерфейс (ocr.html)

Как уже упоминалось ранее, первый шаг заключается в сборе данных для обучения сети. Мы могли бы загрузить последовательность рукописных цифр на сервер, но это было бы неудобно. Вместо этого мы можем предоставить пользователю возможность от руки писать цифры на странице, используя для этого холст (canvas) языка HTML. Затем можем дать возможность либо выполнить обучение, либо проверить сеть, причем в случае обучения также нужно будет указать, какая была нарисована цифра. Если сообщить о том, что есть такой веб-сайт, на котором посетители могли бы внести в наше исследование свой вклад, то можно достаточно просто собрать много исходных данных. Ниже показан небольшой фрагмент кода на языке HTML, с которого мы начинаем разработку.

<html>
<head>
    <script src="/images/BOOKS/AOSA/15/ocr.js"></script>
    <link rel="stylesheet" type="text/css" href="ocr.css">
</head>
<body onload="ocrDemo.onLoadFunction()">
    <div id="main-container" style="text-align: center;">
        <h1>OCR Demo</h1>
        <canvas id="canvas" width="200" height="200"></canvas>
        <form name="input">
            <p>Digit: <input id="digit" type="text"> 7lt;/p>
            <input type="button" value="Train" onclick="ocrDemo.train()">
            <input type="button" value="Test" onclick="ocrDemo.test()">
            <input type="button" value="Reset" onclick="ocrDemo.resetCanvas();"/>
        </form> 
    </div>
</body>
</html>

Клиент системы распознавания символов (ocr.js)

Поскольку отдельный пиксель на холсте HTML различить трудно, мы можем представлять один пиксель для ввода в искусственную нейронную сеть в виде квадрата размером в 10х10 реальных пикселей. Таким образом, реальный холст размером в 200x200 пикселей будет с точки зрения нейронной сети представлен в виде 20х20 холстов. Переменные, приводимые ниже, помогут нам следить за этими измерениями.

var ocrDemo = {
    CANVAS_WIDTH: 200,
    TRANSLATED_WIDTH: 20,
    PIXEL_WIDTH: 10, // TRANSLATED_WIDTH = CANVAS_WIDTH / PIXEL_WIDTH

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

    drawGrid: function(ctx) {
        for (var x = this.PIXEL_WIDTH, y = this.PIXEL_WIDTH; 
                 x < this.CANVAS_WIDTH; x += this.PIXEL_WIDTH, 
                 y += this.PIXEL_WIDTH) {
            ctx.strokeStyle = this.BLUE;
            ctx.beginPath();
            ctx.moveTo(x, 0);
            ctx.lineTo(x, this.CANVAS_WIDTH);
            ctx.stroke();

            ctx.beginPath();
            ctx.moveTo(0, y);
            ctx.lineTo(this.CANVAS_WIDTH, y);
            ctx.stroke();
        }
    },

Нам также необходимо хранить данные, полученные из сетки, в некоторой форме, которая может быть отправлена на сервер. Для простоты, мы можем использовать массив данных data, черный пиксель в котором будет обозначаться как 0, а белый пиксель - как 1. Мы будем считать, что пиксели у нас неокрашенные, т.е. черно-белые. Нам также потребуется несколько слушателей событий мыши на холсте, с помощью которых мы будем знать, когда нужно вызывать функцию fillSquare(), которая в процессе рисования пользователем цифр будет закрашивать пиксели белым цветом. Эти слушатели должны отслеживать находимся ли мы в состоянии рисования и, после этого, вызвать функцию fillSquare(), реализующую некоторая простую математика и принятие решение о том, какие пиксели следует закрашивать.

    onMouseMove: function(e, ctx, canvas) {
        if (!canvas.isDrawing) {
            return;
        }
        this.fillSquare(ctx, 
            e.clientX - canvas.offsetLeft, e.clientY - canvas.offsetTop);
    },

    onMouseDown: function(e, ctx, canvas) {
        canvas.isDrawing = true;
        this.fillSquare(ctx, 
            e.clientX - canvas.offsetLeft, e.clientY - canvas.offsetTop);
    },

    onMouseUp: function(e) {
        canvas.isDrawing = false;
    },

    fillSquare: function(ctx, x, y) {
        var xPixel = Math.floor(x / this.PIXEL_WIDTH);
        var yPixel = Math.floor(y / this.PIXEL_WIDTH);
        this.data[((xPixel - 1)  * this.TRANSLATED_WIDTH + yPixel) - 1] = 1;

        ctx.fillStyle = '#ffffff';
        ctx.fillRect(xPixel * this.PIXEL_WIDTH, yPixel * this.PIXEL_WIDTH, 
            this.PIXEL_WIDTH, this.PIXEL_WIDTH);
    },

Теперь мы приближаемся к самому интересному! Нам нужна функция, которая готовит для отправки на сервер данные, предназначенные для обучения. Здесь у нас есть относительно понятная функция перенаправления train(), который проверяет наличие некоторых ошибок в данных, подлежащих передаче, добавляет их в массив trainArray и отправляет их с помощью обращения к функции sendData().

    train: function() {
        var digitVal = document.getElementById("digit").value;
        if (!digitVal || this.data.indexOf(1) < 0) {
            alert("Please type and draw a digit value in order to train the network");
            return;
        }
        this.trainArray.push({"y0": this.data, "label": parseInt(digitVal)});
        this.trainingRequestCount++;

        // Time to send a training batch to the server.
        if (this.trainingRequestCount == this.BATCH_SIZE) {
            alert("Sending training data to server...");
            var json = {
                trainArray: this.trainArray,
                train: true
            };

            this.sendData(json);
            this.trainingRequestCount = 0;
            this.trainArray = [];
        }
    },

Из интересного здесь следует отметить использование объектов trainingRequestCount, trainArray и BATCH_SIZE. Здесь BATCH_SIZE является некоторой предопределенной константой, указывающей, какое количество данных, предназначенных для обучения, клиент соберет перед тем, как отошлет пакет с запросами на сервер, выполняющий распознавание символа. Основная причина использования пакетов запросов в том, что они позволяют избежать проблем на сервере, которые возникают при большом количестве одновременных запросов. При наличии большого количества клиентов (например, большое количество пользователей на странице ocr.html, на которой происходит обучение системы), или при наличии в клиентской части еще одного уровня, на котором отсканированные изображения цифр преобразуются в пиксели используемые для обучения сети, значение BATCH_SIZE, установленное равным 1, будет причиной возникновения большого количества ненужных запросов. Такой подход удобен для клиента, но, на практике, если это потребуется, похожая пакетная обработка должна также присутствовать и на сервере. В случае, если злоумышленник намеренно отправляет большое количество запросов к серверу, то может возникнуть состояние атаки вида «отказ в обслуживании" (DoS), нагрузка на сервер превысит его возможности и он выйдет из строя.

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

    test: function() {
        if (this.data.indexOf(1) < 0) {
            alert("Please draw a digit in order to test the network");
            return;
        }
        var json = {
            image: this.data,
            predict: true
        };
        this.sendData(json);
    },

И, наконец, нам потребуются некоторые функции, которые выполняют запросы HTTP POST, получают ответы и обрабатывают любые возможные ошибки, которые при этом возникают.

    receiveResponse: function(xmlHttp) {
        if (xmlHttp.status != 200) {
            alert("Server returned status " + xmlHttp.status);
            return;
        }
        var responseJSON = JSON.parse(xmlHttp.responseText);
        if (xmlHttp.responseText && responseJSON.type == "test") {
            alert("The neural network predicts you wrote a \'" 
                   + responseJSON.result + '\'');
        }
    },

    onError: function(e) {
        alert("Error occurred while connecting to server: " + e.target.statusText);
    },

    sendData: function(json) {
        var xmlHttp = new XMLHttpRequest();
        xmlHttp.open('POST', this.HOST + ":" + this.PORT, false);
        xmlHttp.onload = function() { this.receiveResponse(xmlHttp); }.bind(this);
        xmlHttp.onerror = function() { this.onError(xmlHttp) }.bind(this);
        var msg = JSON.stringify(json);
        xmlHttp.setRequestHeader('Content-length', msg.length);
        xmlHttp.setRequestHeader("Connection", "close");
        xmlHttp.send(msg);
    }

Перейти к следующей части статьи.

Перейти к началу статьи.