-
Notifications
You must be signed in to change notification settings - Fork 0
OCR_modified
Welcome to the OCR_modified wiki!
https://barionleg.github.io/OCR_modified/ocr

https://aosabook.org/en/500L/optical-character-recognition-ocr.html

500 строк или менее Оптическое распознавание символов Марина Самуэль
Если вам понравились эти книги, вам также могут понравиться Разработка программного обеспечения по примерам на Python и Разработка программного обеспечения по примерам на JavaScript.
Введение Что, если бы ваш компьютер мог мыть посуду, стирать, готовить ужин и убираться в доме? Думаю, я могу с уверенностью сказать, что большинство людей были бы рады получить помощь! Но что нужно, чтобы компьютер мог выполнять эти задачи так же, как люди?
Известный ученый-компьютерщик Алан Тьюринг предложил тест Тьюринга как способ определить, может ли машина обладать интеллектом, неотличимым от человеческого. Тест заключается в том, что человек задает вопросы двум скрытым сущностям, одной из которых является человек, а другой — машина, и пытается определить, кто из них кто. Если опрашивающий не может идентифицировать машину, то считается, что машина обладает интеллектом человеческого уровня.
Хотя существует много споров о том, является ли тест Тьюринга допустимой оценкой интеллекта, и можем ли мы построить такие интеллектуальные машины, нет сомнений, что машины с некоторой степенью интеллекта уже существуют. В настоящее время существует программное обеспечение, которое помогает роботам ориентироваться в офисе и выполнять небольшие задачи или помогать тем, кто страдает болезнью Альцгеймера. Более распространенными примерами искусственного интеллекта (ИИ) являются способ, которым Google оценивает, что вы ищете, когда вы вводите некоторые ключевые слова, или способ, которым Facebook решает, что поместить в вашу новостную ленту.
Одним из известных приложений ИИ является оптическое распознавание символов (OCR). Система OCR — это часть программного обеспечения, которая может принимать изображения рукописных символов в качестве входных данных и интерпретировать их в машиночитаемый текст. Хотя вы, возможно, не задумываетесь дважды, внося рукописный чек в банковский автомат, в фоновом режиме происходит некоторая интересная работа. В этой главе будет рассмотрен рабочий пример простой системы OCR, которая распознает числовые цифры с помощью искусственной нейронной сети (ИНС). Но сначала давайте немного узнаем контекст.
Что такое искусственный интеллект? Хотя определение интеллекта Тьюрингом звучит разумно, в конечном итоге то, что составляет интеллект, по сути является философским спором. Однако специалисты по информатике классифицировали определенные типы систем и алгоритмов как ветви ИИ. Каждая ветвь используется для решения определенных наборов задач. Эти ветви включают следующие примеры, а также многие другие :
Логическая и вероятностная дедукция и вывод, основанные на некоторых предопределенных знаниях о мире. Например, нечеткий вывод может помочь термостату решить, когда включить кондиционер, если он обнаруживает, что температура высокая, а атмосфера влажная. Эвристический поиск. Например, поиск можно использовать для нахождения наилучшего возможного следующего хода в шахматной игре путем поиска всех возможных ходов и выбора того, который максимально улучшает вашу позицию. Машинное обучение (МО) с моделями обратной связи. Например, задачи распознавания образов, такие как OCR. В общем, МО подразумевает использование больших наборов данных для обучения системы выявлению закономерностей. Наборы обучающих данных могут быть маркированными, что означает, что ожидаемые выходные данные системы указаны для заданных входных данных, или немаркированными, что означает, что ожидаемые выходные данные не указаны. Алгоритмы, которые обучают системы с помощью немаркированных данных, называются неконтролируемыми алгоритмами, а те, которые обучают с помощью маркированных данных, называются контролируемыми . Существует множество алгоритмов и методов МО для создания систем OCR, одним из подходов к которым являются ИНС.
Искусственные нейронные сети Что такое ИНС? ANN — это структура, состоящая из взаимосвязанных узлов, которые взаимодействуют друг с другом. Структура и ее функциональность вдохновлены нейронными сетями, обнаруженными в биологическом мозге. Теория Хебба объясняет, как эти сети могут научиться распознавать закономерности, физически изменяя свою структуру и прочность связей. Аналогично, типичная ANN (показанная на рисунке 15.1 ) имеет связи между узлами, имеющими вес, который обновляется по мере обучения сети. Узлы, помеченные как «+1», называются смещениями . Самый левый синий столбец узлов — это входные узлы , средний столбец содержит скрытые узлы , а самый правый столбец содержит выходные узлы . Может быть много столбцов скрытых узлов, известных как скрытые слои .
Рисунок 15.1 — Искусственная нейронная сеть Рисунок 15.1 — Искусственная нейронная сеть
Значения внутри всех круглых узлов на рисунке 15.1 представляют выход узла. Если мы назовем выходн th узел сверху в слоеЛ какн ( Л ) и связь междуя th узел в слоеЛ идж th узел в слоеЛ + 1 какж( Л )джя , то выход узлаа( 2 )2 является:
а( 2 )2= ф(ж( 1 )21Икс1+ж( 1 )22Икс2+б( 1 )2)
гдеф( . ) известна как функция активации иб является смещением . Функция активации принимает решение о том, какой тип выходного сигнала будет у узла. Смещение — это дополнительный узел с фиксированным выходным сигналом 1, который может быть добавлен к ANN для повышения ее точности. Мы рассмотрим более подробную информацию об обоих из них в разделе Проектирование ANN прямого распространения ( neural_network_design.py) .
Этот тип топологии сети называется нейронной сетью прямого распространения , поскольку в сети нет циклов. ИНС с узлами, выходы которых подаются на их входы, называются рекуррентными нейронными сетями. Существует множество алгоритмов, которые можно применять для обучения ИНС прямого распространения; один из часто используемых алгоритмов называется обратным распространением . Система OCR, которую мы реализуем в этой главе, будет использовать обратное распространение.
Как мы используем ИНС? Как и в большинстве других подходов МО, первым шагом для использования обратного распространения является решение того, как преобразовать или свести нашу проблему к той, которую может решить ИНС. Другими словами, как мы можем манипулировать нашими входными данными, чтобы мы могли подать их в ИНС? В случае нашей системы OCR мы можем использовать позиции пикселей для заданной цифры в качестве входных данных. Стоит отметить, что зачастую выбор формата входных данных не так прост. Например, если бы мы анализировали большие изображения для определения на них форм, нам, возможно, потребовалась бы предварительная обработка изображения для определения контуров внутри него. Эти контуры были бы входными данными.
Как только мы определились с форматом входных данных, что дальше? Поскольку обратное распространение — это контролируемый алгоритм, его нужно будет обучить с помощью маркированных данных, как упоминалось в разделе Что такое искусственный интеллект?. Таким образом, при передаче позиций пикселей в качестве обучающих входных данных мы также должны передать связанную с ними цифру. Это означает, что мы должны найти или собрать большой набор данных из нарисованных цифр и связанных с ними значений.
Следующий шаг — разбиение набора данных на обучающий набор и проверочный набор. Обучающие данные используются для запуска алгоритма обратного распространения для установки весов ИНС. Проверочные данные используются для составления прогнозов с использованием обученной сети и вычисления ее точности. Если бы мы сравнивали производительность обратного распространения с другим алгоритмом на наших данных, мы бы разделили данные на 50% для обучения, 25% для сравнения производительности двух алгоритмов (проверочный набор) и последние 25% для проверки точности выбранного алгоритма (тестовый набор). Поскольку мы не сравниваем алгоритмы, мы можем сгруппировать один из 25% наборов как часть обучающего набора и использовать 75% данных для обучения сети и 25% для проверки того, что она была обучена хорошо.
Цель определения точности ИНС двояка. Во-первых, это избежание проблемы переобучения . Переобучение происходит, когда сеть имеет гораздо более высокую точность предсказания обучающего набора, чем проверочного набора. Переобучение говорит нам, что выбранные обучающие данные недостаточно хорошо обобщаются и нуждаются в уточнении. Во-вторых, тестирование точности нескольких различных количеств скрытых слоев и скрытых узлов помогает в проектировании наиболее оптимального размера ИНС. Оптимальный размер ИНС будет иметь достаточно скрытых узлов и слоев для точных предсказаний, но также как можно меньше узлов/соединений, чтобы сократить вычислительные издержки, которые могут замедлить обучение и предсказания. После того, как оптимальный размер будет определен и сеть обучена, она готова делать предсказания!
Проектные решения в простой системе OCR В последних нескольких параграфах мы рассмотрели некоторые основы ИНС прямого распространения и как их использовать. Теперь пришло время поговорить о том, как мы можем построить систему OCR.
Во-первых, мы должны решить, что мы хотим, чтобы наша система могла делать. Чтобы не усложнять, давайте позволим пользователям рисовать одну цифру и сможем обучать систему OCR с помощью этой нарисованной цифры или запросим, чтобы система предсказала, что это за нарисованная цифра. Хотя система OCR может работать локально на одной машине, наличие клиент-серверной конфигурации дает гораздо больше гибкости. Это делает возможным краудсорсинговое обучение ИНС и позволяет мощным серверам обрабатывать интенсивные вычисления.
Наша система OCR будет состоять из 5 основных компонентов, разделенных на 5 файлов. Будет:
клиент ( ocr.js) сервер ( server.py) простой пользовательский интерфейс ( ocr.html) ИНС, обученная методом обратного распространения ( ocr.py) сценарий проектирования ИНС ( neural_network_design.py) Пользовательский интерфейс будет простым: холст для рисования цифр и кнопки для обучения ANN или запроса прогноза. Клиент соберет нарисованную цифру, преобразует ее в массив и передаст на сервер для обработки либо как обучающий образец, либо как запрос прогноза. Сервер просто направит запрос на обучение или прогноз, выполнив вызовы API к модулю ANN. Модуль ANN обучит сеть с существующим набором данных при первой инициализации. Затем он сохранит веса ANN в файл и повторно загрузит их при последующих запусках. В этом модуле происходит ядро логики обучения и прогноза. Наконец, сценарий проектирования предназначен для экспериментов с различным количеством скрытых узлов и принятия решения о том, что работает лучше всего. Вместе эти части дают нам очень простую, но функциональную систему OCR.
Теперь, когда мы продумали, как система будет работать на высоком уровне, пришло время воплотить концепции в код!
Простой интерфейс ( ocr.html) Как упоминалось ранее, первым шагом является сбор данных для обучения сети. Мы могли бы загрузить последовательность рукописных цифр на сервер, но это было бы неудобно. Вместо этого мы могли бы заставить пользователей фактически вручную писать цифры на странице, используя HTML-холст. Затем мы могли бы дать им несколько вариантов либо для обучения, либо для тестирования сети, где обучение сети также включает указание того, какая цифра была нарисована. Таким образом, можно легко передать сбор данных на аутсорсинг, указав людям веб-сайт для получения их ввода. Вот немного HTML, чтобы начать.
<script src="ocr.js"></script> Клиент OCR ( ocr.js) Поскольку один пиксель на HTML-холсте может быть трудно увидеть, мы можем представить один пиксель для ввода ANN как квадрат 10x10 реальных пикселей. Таким образом, реальный холст имеет размер 200x200 пикселей и представляется холстом 20x20 с точки зрения ANN. Переменные ниже помогут нам отслеживать эти измерения.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. Основная причина пакетных запросов — избежать перегрузки сервера большим количеством запросов одновременно. Если существует много клиентов (например, много пользователей находятся на странице, ocr.htmlобучающей систему), или если в клиенте существует другой слой, который берет отсканированные нарисованные цифры и переводит их в пиксели для обучения сети, значение BATCH_SIZE1 приведет к большому количеству ненужных запросов. Этот подход хорош, поскольку он дает большую гибкость клиенту, однако на практике пакетная обработка также должна происходить на сервере, когда это необходимо. Может произойти атака типа «отказ в обслуживании» (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);
}
Сервер ( server.py) Несмотря на то, что это небольшой сервер, который просто передает информацию, нам все равно нужно подумать, как получать и обрабатывать HTTP-запросы. Сначала нам нужно решить, какой тип HTTP-запроса использовать. В последнем разделе клиент использует POST, но почему мы решили именно так? Поскольку данные отправляются на сервер, запрос PUT или POST имеет наибольший смысл. Нам нужно отправить только тело json и никаких параметров URL. Поэтому теоретически запрос GET мог бы работать так же хорошо, но не имел бы семантического смысла. Однако выбор между PUT и POST является предметом долгих и непрекращающихся споров среди программистов; KNPLabs с юмором суммирует эти проблемы .
Другое соображение заключается в том, следует ли отправлять запросы «train» и «predict» на разные конечные точки (например, http://localhost/trainи http://localhost/predict) или на одну и ту же конечную точку, которая затем обрабатывает данные по отдельности. В этом случае мы можем использовать последний подход, поскольку разница между тем, что делается с данными в каждом случае, достаточно мала, чтобы уместиться в коротком операторе if. На практике было бы лучше иметь их в качестве отдельных конечных точек, если бы сервер выполнял более подробную обработку для каждого типа запроса. Это решение, в свою очередь, повлияло на то, какие коды ошибок сервера использовались, когда. Например, ошибка 400 «Bad Request» отправляется, когда в полезной нагрузке не указано ни «train», ни «predict». Если бы вместо этого использовались отдельные конечные точки, это не было бы проблемой. Обработка, выполняемая в фоновом режиме системой OCR, может потерпеть неудачу по любой причине, и если она не обрабатывается правильно на сервере, отправляется 500 «Internal Server Error». Опять же, если бы конечные точки были разделены, было бы больше места для детализации, чтобы отправлять более подходящие ошибки. Например, выявление того, что внутренняя ошибка сервера на самом деле была вызвана неверным запросом.
Наконец, нам нужно решить, когда и где инициализировать систему OCR. Хорошим подходом будет инициализация внутри, server.pyно до запуска сервера. Это связано с тем, что при первом запуске система OCR должна обучить сеть на некоторых уже существующих данных при первом запуске, и это может занять несколько минут. Если сервер запустится до завершения этой обработки, любые запросы на обучение или прогнозирование выдадут исключение, поскольку объект OCR еще не будет инициализирован, учитывая текущую реализацию. Другая возможная реализация может создать некоторую неточную начальную ANN для использования в первых нескольких запросах, пока новая ANN асинхронно обучается в фоновом режиме. Этот альтернативный подход позволяет использовать ANN немедленно, но реализация более сложная и сэкономит время только при запуске сервера, если серверы будут сброшены. Этот тип реализации будет более выгодным для службы OCR, которая требует высокой доступности.
Здесь большая часть кода нашего сервера представлена в одной короткой функции, которая обрабатывает POST-запросы.
def do_POST(s):
response_code = 200
response = ""
var_len = int(s.headers.get('Content-Length'))
content = s.rfile.read(var_len);
payload = json.loads(content);
if payload.get('train'):
nn.train(payload['trainArray'])
nn.save()
elif payload.get('predict'):
try:
response = {
"type":"test",
"result":nn.predict(str(payload['image']))
}
except:
response_code = 500
else:
response_code = 400
s.send_response(response_code)
s.send_header("Content-type", "application/json")
s.send_header("Access-Control-Allow-Origin", "*")
s.end_headers()
if response:
s.wfile.write(json.dumps(response))
return
Проектирование нейронной сети прямого распространения ( neural_network_design.py) При проектировании ИНС прямого распространения необходимо учитывать несколько факторов. Во-первых, какую функцию активации использовать. Ранее мы упоминали функции активации как принимающие решения для выходных данных узла. Тип решения, принимаемого функцией активации, поможет нам решить, какую из них использовать. В нашем случае мы будем проектировать ИНС, которая выводит значение от 0 до 1 для каждой цифры (0-9). Значения, близкие к 1, будут означать, что ИНС предсказывает, что это нарисованная цифра, а значения, близкие к 0, будут означать, что это не нарисованная цифра. Таким образом, нам нужна функция активации, которая будет иметь выходные данные, близкие к 0 или близкие к 1. Нам также нужна дифференцируемая функция, поскольку нам понадобится производная для вычисления обратного распространения. Обычно используемой функцией в этом случае является сигмоид, поскольку она удовлетворяет обоим этим ограничениям. StatSoft предоставляет хороший список общих функций активации и их свойств.
Второй фактор, который следует рассмотреть, — хотим ли мы включить смещения. Мы уже упоминали смещения пару раз, но на самом деле не говорили о том, что это такое или почему мы их используем. Давайте попробуем понять это, вернувшись к тому, как вычисляется выход узла на рисунке 15.1 . Предположим, что у нас есть один входной узел и один выходной узел, наша выходная формула будету= ф( ш х ) , гдеу это выход,ф( ) это функция активации,ж это вес для связи между узлами, иИкс является переменным входом для узла. Смещение по сути является узлом, выход которого всегда1 . Это изменит выходную формулу нау= ф( ш х + б ) гдеб это вес связи между узлом смещения и следующим узлом. Если мы рассмотримж иб как константы иИкс как переменная, то добавление смещения добавляет константу к нашему линейному входу функцииф( . ) .
Добавление смещения, таким образом, позволяет сместитьу -intercept и в целом дает большую гибкость для выходных данных узла. Часто хорошей практикой является включение смещений, особенно для ИНС с небольшим количеством входов и выходов. Смещения обеспечивают большую гибкость выходных данных ИНС и, таким образом, предоставляют ИНС больше места для точности. Без смещений мы с меньшей вероятностью будем делать правильные прогнозы с помощью нашей ИНС или нам понадобится больше скрытых узлов для более точных прогнозов.
Другие факторы, которые следует учитывать, — это количество скрытых слоев и количество скрытых узлов на слой. Для более крупных ИНС с большим количеством входов и выходов эти числа определяются путем пробы различных значений и тестирования производительности сети. В этом случае производительность измеряется путем обучения ИНС заданного размера и определения того, какой процент проверочного набора классифицирован правильно. В большинстве случаев для приличной производительности достаточно одного скрытого слоя, поэтому здесь мы экспериментируем только с количеством скрытых узлов.
for i in xrange(5, 50, 5): nn = OCRNeuralNetwork(i, data_matrix, data_labels, train_indices, False) performance = str(test(data_matrix, data_labels, test_indices, nn)) print "{i} Hidden Nodes: {val}".format(i=i, val=performance) Здесь мы инициализируем ИНС с 5–50 скрытыми узлами с шагом 5. Затем мы вызываем функцию test().
def test(data_matrix, data_labels, test_indices, nn): avg_sum = 0 for j in xrange(100): correct_guess_count = 0 for i in test_indices: test = data_matrix[i] prediction = nn.predict(test) if data_labels[i] == prediction: correct_guess_count += 1
avg_sum += (correct_guess_count / float(len(test_indices)))
return avg_sum / 100
Внутренний цикл подсчитывает количество правильных классификаций, которые затем делятся на количество попыток классификации в конце. Это дает отношение или процентную точность для ANN. Поскольку каждый раз, когда ANN обучается, ее веса могут немного отличаться, мы повторяем этот процесс 100 раз во внешнем цикле, чтобы получить среднее значение точности этой конкретной конфигурации ANN. В нашем случае пример запуска выглядит neural_network_design.pyследующим образом:
5 Hidden Nodes: 0.7792 10 Hidden Nodes: 0.8704 15 Hidden Nodes: 0.8808 20 Hidden Nodes: 0.8864 25 Hidden Nodes: 0.8808 30 Hidden Nodes: 0.888 35 Hidden Nodes: 0.8904 40 Hidden Nodes: 0.8896 45 Hidden Nodes: 0.8928 Из этого вывода мы можем сделать вывод, что 15 скрытых узлов будут наиболее оптимальными. Добавление 5 узлов от 10 до 15 дает нам примерно на 1% больше точности, тогда как для повышения точности еще на 1% потребуется добавить еще 20 узлов. Увеличение количества скрытых узлов также увеличивает вычислительные издержки. Поэтому сетям с большим количеством скрытых узлов потребуется больше времени для обучения и составления прогнозов. Таким образом, мы решили использовать последнее скрытое количество узлов, что привело к резкому увеличению точности. Конечно, возможно, что при проектировании ИНС вычислительные издержки не являются проблемой, и главным приоритетом является получение максимально точной ИНС. В этом случае лучше выбрать 45 скрытых узлов вместо 15.
Основная функциональность OCR В этом разделе мы поговорим о том, как происходит фактическое обучение с помощью обратного распространения, как мы можем использовать сеть для составления прогнозов, а также о других ключевых решениях по проектированию основных функций.
Обучение методом обратного распространения ( ocr.py) Мы используем алгоритм обратного распространения для обучения нашей ИНС. Он состоит из 4 основных шагов, которые повторяются для каждого образца в обучающем наборе, обновляя веса ИНС каждый раз.
Сначала мы инициализируем веса небольшими (от -1 до 1) случайными значениями. В нашем случае мы инициализируем их значениями от -0,06 до 0,06 и сохраняем их в матрицах theta1, theta2, input_layer_bias, и hidden_layer_bias. Поскольку каждый узел в слое связан с каждым узлом в следующем слое, мы можем создать матрицу, которая имеет m строк и n столбцов, где n — количество узлов в одном слое, а m — количество узлов в соседнем слое. Эта матрица будет представлять все веса для связей между этими двумя слоями. Здесь theta1 имеет 400 столбцов для наших входов и num_hidden_nodesстрок размером 20x20 пикселей. Аналогично, theta2представляет связи между скрытым слоем и выходным слоем. Он имеет num_hidden_nodesстолбцы и NUM_DIGITS( 10) строк. Два других вектора (1 строка) input_layer_biasи hidden_layer_biasпредставляют смещения.
def _rand_initialize_weights(self, size_in, size_out):
return [((x * 0.12) - 0.06) for x in np.random.rand(size_out, size_in)]
self.theta1 = self._rand_initialize_weights(400, num_hidden_nodes)
self.theta2 = self._rand_initialize_weights(num_hidden_nodes, 10)
self.input_layer_bias = self._rand_initialize_weights(1,
num_hidden_nodes)
self.hidden_layer_bias = self._rand_initialize_weights(1, 10)
Второй шаг — прямое распространение , которое по сути вычисляет выходные данные узла, как описано в разделе Что такое ИНС?, слой за слоем, начиная с входных узлов. Здесь y0— массив размером 400 с входными данными, которые мы хотим использовать для обучения ИНС. Мы умножаем theta1на y0транспонируем, чтобы получить две матрицы с размерами (num_hidden_nodes x 400) * (400 x 1)и получить результирующий вектор выходных данных для скрытого слоя размером num_hidden_nodes. Затем мы добавляем вектор смещения и применяем векторизованную сигмоидальную функцию активации к этому выходному вектору, что дает нам y1. y1— выходной вектор нашего скрытого слоя. Тот же процесс повторяется снова для вычисления y2для выходных узлов. y2— теперь наш вектор выходного слоя со значениями, представляющими вероятность того, что их индекс — выпавшее число. Например, если кто-то вытащит 8, значение y2на 8-м индексе будет наибольшим, если ИНС сделала правильный прогноз. Однако вероятность того, что нарисованной цифрой будет 6, может быть выше, чем 1, поскольку она больше похожа на 8 и, скорее всего, будет использовать те же пиксели для рисования, что и 8. y2Точность становится выше с каждой дополнительной нарисованной цифрой, на которой обучается ИНС.
# The sigmoid activation function. Operates on scalars.
def _sigmoid_scalar(self, z):
return 1 / (1 + math.e ** -z)
y1 = np.dot(np.mat(self.theta1), np.mat(data['y0']).T)
sum1 = y1 + np.mat(self.input_layer_bias) # Add the bias
y1 = self.sigmoid(sum1)
y2 = np.dot(np.array(self.theta2), y1)
y2 = np.add(y2, self.hidden_layer_bias) # Add the bias
y2 = self.sigmoid(y2)
Третий шаг — обратное распространение , которое включает вычисление ошибок в выходных узлах, а затем в каждом промежуточном слое обратно к входу. Здесь мы начинаем с создания ожидаемого выходного вектора, , actual_valsс a 1в индексе цифры, которая представляет значение нарисованной цифры, и 0s в противном случае. Вектор ошибок в выходных узлах, output_errors, вычисляется путем вычитания фактического выходного вектора, y2, из actual_vals. Для каждого скрытого слоя впоследствии мы вычисляем два компонента. Во-первых, у нас есть транспонированная весовая матрица следующего слоя, умноженная на его выходные ошибки. Затем у нас есть производная функции активации, примененная к предыдущему слою. Затем мы выполняем поэлементное умножение этих двух компонентов, давая вектор ошибок для скрытого слоя. Здесь мы называем это hidden_errors.
actual_vals = [0] * 10
actual_vals[data['label']] = 1
output_errors = np.mat(actual_vals).T - np.mat(y2)
hidden_errors = np.multiply(np.dot(np.mat(self.theta2).T, output_errors),
self.sigmoid_prime(sum1))
Обновления веса, которые корректируют веса ИНС на основе ошибок, вычисленных ранее. Веса обновляются на каждом слое с помощью умножения матриц. Матрица ошибок на каждом слое умножается на выходную матрицу предыдущего слоя. Затем этот продукт умножается на скаляр, называемый скоростью обучения, и добавляется к матрице веса. Скорость обучения — это значение от 0 до 1, которое влияет на скорость и точность обучения в ИНС. Большие значения скорости обучения сгенерируют ИНС, которая обучается быстро, но менее точна, в то время как меньшие значения сгенерируют ИНС, которая обучается медленнее, но более точна. В нашем случае у нас есть относительно небольшое значение для скорости обучения, 0,1. Это хорошо работает, поскольку нам не нужно, чтобы ИНС была немедленно обучена, чтобы пользователь мог продолжать делать запросы на обучение или прогнозирование. Смещения обновляются простым умножением скорости обучения на вектор ошибок слоя.
self.theta1 += self.LEARNING_RATE * np.dot(np.mat(hidden_errors),
np.mat(data['y0']))
self.theta2 += self.LEARNING_RATE * np.dot(np.mat(output_errors),
np.mat(y1).T)
self.hidden_layer_bias += self.LEARNING_RATE * output_errors
self.input_layer_bias += self.LEARNING_RATE * hidden_errors
Тестирование обученной сети ( ocr.py) После того, как ANN обучена с помощью обратного распространения, ее довольно просто использовать для прогнозирования. Как мы видим здесь, мы начинаем с вычисления выходных данных ANN, y2точно так же, как мы делали на шаге 2 обратного распространения. Затем мы ищем индекс в векторе с максимальным значением. Этот индекс — цифра, предсказанная ANN.
def predict(self, test):
y1 = np.dot(np.mat(self.theta1), np.mat(test).T)
y1 = y1 + np.mat(self.input_layer_bias) # Add the bias
y1 = self.sigmoid(y1)
y2 = np.dot(np.array(self.theta2), y1)
y2 = np.add(y2, self.hidden_layer_bias) # Add the bias
y2 = self.sigmoid(y2)
results = y2.T.tolist()[0]
return results.index(max(results))
Другие решения дизайна ( ocr.py) В сети доступно множество ресурсов, которые более подробно описывают реализацию обратного распространения. Один хороший ресурс — из курса Университета Уилламетта . Он рассматривает шаги обратного распространения, а затем объясняет, как его можно перевести в матричную форму. Хотя объем вычислений с использованием матриц такой же, как и при использовании циклов, преимущество в том, что код проще и легче для чтения с меньшим количеством вложенных циклов. Как мы видим, весь процесс обучения написан менее чем в 25 строках кода с использованием матричной алгебры.
Как упоминалось во введении к разделу « Проектные решения в простой системе OCR» , сохранение весов ANN означает, что мы не теряем прогресс, достигнутый в ее обучении, когда сервер выключается или внезапно выходит из строя по какой-либо причине. Мы сохраняем веса, записывая их в файл в формате JSON. При запуске OCR загружает сохраненные веса ANN в память. Функция сохранения не вызывается внутренне OCR, а сервер решает, когда выполнять сохранение. В нашем случае сервер сохраняет веса после каждого обновления. Это быстрое и простое решение, но оно не оптимально, поскольку запись на диск занимает много времени. Это также не позволяет нам обрабатывать несколько одновременных запросов, поскольку нет механизма, предотвращающего одновременную запись в один и тот же файл. На более сложном сервере сохранения, возможно, можно было бы выполнять при выключении или раз в несколько минут с какой-либо формой блокировки или протокола временной метки, чтобы гарантировать отсутствие потери данных.
def save(self):
if not self._use_file:
return
json_neural_network = {
"theta1":[np_mat.tolist()[0] for np_mat in self.theta1],
"theta2":[np_mat.tolist()[0] for np_mat in self.theta2],
"b1":self.input_layer_bias[0].tolist()[0],
"b2":self.hidden_layer_bias[0].tolist()[0]
};
with open(OCRNeuralNetwork.NN_FILE_PATH,'w') as nnFile:
json.dump(json_neural_network, nnFile)
def _load(self):
if not self._use_file:
return
with open(OCRNeuralNetwork.NN_FILE_PATH) as nnFile:
nn = json.load(nnFile)
self.theta1 = [np.array(li) for li in nn['theta1']]
self.theta2 = [np.array(li) for li in nn['theta2']]
self.input_layer_bias = [np.array(nn['b1'][0])]
self.hidden_layer_bias = [np.array(nn['b2'][0])]
Заключение Теперь, когда мы узнали об искусственном интеллекте, искусственных нейронных сетях, обратном распространении ошибки и создании сквозной системы OCR, давайте вспомним основные моменты этой главы и общую картину.
Мы начали главу с описания ИИ, ИНС и того, что мы будем реализовывать. Мы обсудили, что такое ИИ и примеры его использования. Мы увидели, что ИИ по сути является набором алгоритмов или подходов к решению проблем, которые могут дать ответ на вопрос так же, как это сделал бы человек. Затем мы рассмотрели структуру ИНС с прямой связью. Мы узнали, что вычисление выходных данных в заданном узле так же просто, как суммирование произведений выходных данных предыдущих узлов и их соединительных весов. Мы рассказали о том, как использовать ИНС, сначала отформатировав входные данные и разделив данные на обучающие и проверочные наборы.
Получив некоторую базу, мы начали говорить о создании клиент-серверной веб-системы, которая будет обрабатывать запросы пользователей для обучения или тестирования OCR. Затем мы обсудили, как клиент будет интерпретировать нарисованные пиксели в массив и выполнять HTTP-запрос к серверу OCR для выполнения обучения или тестирования. Мы обсудили, как наш простой сервер считывает запросы и как проектировать ANN, проверяя производительность нескольких скрытых узлов. Мы закончили, пройдясь по основному коду обучения и тестирования для обратного распространения.
Хотя мы создали, казалось бы, функциональную систему OCR, эта глава просто царапает поверхность того, как может работать настоящая система OCR. Более сложные системы OCR могут иметь предварительно обработанные входные данные, использовать гибридные алгоритмы машинного обучения, иметь более обширные фазы проектирования или другие дополнительные оптимизации.