Как построить нейронную сеть для распознавания рукописных цифр с TensorFlow

Вступление

Нейронные сети используются как метод глубокого обучения, одно из многих подразделов искусственного интеллекта. Они были впервые предложены около 70 лет назад как попытка смоделировать работу человеческого мозга, хотя и в гораздо более упрощенной форме. Отдельные «нейроны» соединены в слои, причем веса назначены для определения того, как нейрон реагирует, когда сигналы распространяются через сеть. Ранее нейронные сети были ограничены числом нейронов, которые они могли имитировать, и, следовательно, сложностью обучения, которую они могли достичь. Но в последние годы благодаря прогрессу в разработке аппаратного обеспечения мы смогли построить очень глубокие сети и обучить их огромным наборам данных для достижения прорывов в области машинного интеллекта.

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

В этом уроке вы реализуете небольшой подраздел распознавания и распознавания цифр. Используя TensorFlow, библиотеку Python с открытым исходным кодом, разработанную лабораториями Google Brain для глубоких исследований, вы будете рисовать нарисованные от руки изображения с номерами 0-9, строить и обучать нейронная сеть для распознавания и прогнозирования правильной метки для отображаемой цифры.

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

Предпосылки

Для завершения этого урока вам понадобится:

  • Локальная Python 3 среда разработки, включая https : //pypi.org/project/pip/ [pip], инструмент для установки пакетов Python и venv, для создания виртуальных сред.

Шаг 1 - Настройка проекта

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

Мы будем использовать виртуальную среду Python 3 для управления зависимостями нашего проекта. Создайте новый каталог для вашего проекта и перейдите к новому каталогу:

mkdir tensorflow-demo
cd tensorflow-demo

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

python3 -m venv tensorflow-demo
source tensorflow-demo/bin/activate

Затем установите библиотеки, которые вы будете использовать в этом руководстве. Мы будем использовать конкретные версии этих библиотек, создав в каталоге проекта файл + needs.txt +, в котором указаны требование и версия, которая нам нужна. Создайте файл + needs.txt +:

touch requirements.txt

Откройте файл в текстовом редакторе и добавьте следующие строки, чтобы указать библиотеки Image, NumPy и TensorFlow и их версии:

requirements.txt

image==1.5.20
numpy==1.14.3
tensorflow==1.4.0

Сохраните файл и выйдите из редактора. Затем установите эти библиотеки с помощью следующей команды:

pip install -r requirements.txt

Установив зависимости, мы можем начать работу над нашим проектом.

Шаг 2 - Импорт набора данных MNIST

Набор данных, который мы будем использовать в этом руководстве, называется набором данных MNIST и является классическим в сообществе машинного обучения. Этот набор данных состоит из изображений рукописных цифр размером 28x28 пикселей. Вот несколько примеров цифр, включенных в набор данных:

изображение: https: //assets.digitalocean.com/articles/handwriting_tensorflow_python3/wBCHXId.png [Примеры изображений MNIST]

Давайте создадим программу Python для работы с этим набором данных. Мы будем использовать один файл для всей нашей работы в этом уроке. Создайте новый файл с именем + main.py +:

touch main.py

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

main.py

import tensorflow as tf

Добавьте следующие строки кода в ваш файл, чтобы импортировать набор данных MNIST и сохранить данные изображения в переменной + mnist +:

main.py

...
from tensorflow.examples.tutorials.mnist import input_data


mnist = input_data.read_data_sets("MNIST_data/", one_hot=True)  # y labels are oh-encoded

При чтении данных мы используем горячее кодирование для представления меток (фактически нарисованная цифра, например, «3») изображений. Горячее кодирование использует вектор двоичных значений для представления числовых или категориальных значений. Поскольку наши метки предназначены для цифр 0-9, вектор содержит десять значений, по одному на каждую возможную цифру. Одно из этих значений установлено в 1, чтобы представить цифру в этом индексе вектора, а остальные установлены в 0. Например, цифра 3 представлена ​​с помощью вектора + [0, 0, 0, 1, 0, 0, 0, 0, 0, 0] +. Поскольку значение в индексе 3 хранится как 1, вектор представляет цифру 3.

Чтобы представить сами фактические изображения, 28x28 пикселей сведены в одномерный вектор, который имеет размер 784 пикселей. Каждый из 784 пикселей, составляющих изображение, сохраняется как значение от 0 до 255. Это определяет оттенки серого пикселя, поскольку наши изображения представлены только в черно-белом режиме. Таким образом, черный пиксель представлен 255, а белый пиксель - 0 с различными оттенками серого где-то посередине.

Мы можем использовать переменную + mnist +, чтобы узнать размер набора данных, который мы только что импортировали. Глядя на + num_examples + для каждого из трех подмножеств, мы можем определить, что набор данных был разбит на 55 000 изображений для обучения, 5000 для проверки и 10 000 для тестирования. Добавьте следующие строки в ваш файл:

main.py

...
n_train = mnist.train.num_examples  # 55,000
n_validation = mnist.validation.num_examples  # 5000
n_test = mnist.test.num_examples  # 10,000

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

Шаг 3 - Определение архитектуры нейронной сети

Архитектура нейронной сети относится к таким элементам, как количество уровней в сети, количество блоков на каждом уровне и то, как эти блоки связаны между уровнями. Поскольку нейронные сети слабо вдохновлены работой человеческого мозга, здесь термин единица используется для представления того, что мы биологически считаем нейроном. Подобно нейронам, передающим сигналы по всему мозгу, единицы принимают некоторые значения из предыдущих единиц в качестве входных данных, выполняют вычисления, а затем передают новое значение в качестве вывода другим единицам. Эти единицы имеют многоуровневую структуру, начиная как минимум с одного слоя для ввода значений и одного слоя для вывода значений. Термин «скрытый слой» используется для всех слоев между входным и выходным слоями, т.е. те, «скрытые» от реального мира.

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

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

main.py

...
n_input = 784  # input layer (28x28 pixels)
n_hidden1 = 512  # 1st hidden layer
n_hidden2 = 256  # 2nd hidden layer
n_hidden3 = 128  # 3rd hidden layer
n_output = 10  # output layer (0-9 digits)

Следующая диаграмма показывает визуализацию архитектуры, которую мы разработали, с каждым слоем, полностью связанным с окружающими слоями:

изображение: https: //assets.digitalocean.com/articles/handwriting_tensorflow_python3/cnwitLM.png [Диаграмма нейронной сети]

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

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

main.py

...
learning_rate = 1e-4
n_iterations = 1000
batch_size = 128
dropout = 0.5

Скорость обучения показывает, насколько параметры будут корректироваться на каждом этапе процесса обучения. Эти корректировки являются ключевым компонентом обучения: после каждого прохода по сети мы слегка настраиваем веса, чтобы попытаться уменьшить потери. Более высокие скорости обучения могут сходиться быстрее, но также могут привести к превышению оптимальных значений при их обновлении. Количество итераций относится к тому, сколько раз мы пройдем этап обучения, а размер пакета относится к тому, сколько примеров обучения мы используем на каждом этапе. Переменная + dropout + представляет собой порог, при котором мы исключаем некоторые единицы случайным образом. Мы будем использовать + dropout + в нашем последнем скрытом слое, чтобы дать каждому юниту 50% шанс быть уничтоженным на каждом этапе обучения. Это помогает предотвратить переоснащение.

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

Шаг 4 - Построение графика TensorFlow

Чтобы построить нашу сеть, мы настроим сеть как вычислительный граф для выполнения TensorFlow. Основная концепция TensorFlow - tensor, структура данных, похожая на массив или список. инициализируются, обрабатываются по мере их прохождения через граф и обновляются в процессе обучения.

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

main.py

...
X = tf.placeholder("float", [None, n_input])
Y = tf.placeholder("float", [None, n_output])
keep_prob = tf.placeholder(tf.float32)

Единственный параметр, который необходимо указать при его объявлении, - это размер данных, которые мы будем вводить. Для + X + мы используем форму + [None, 784] +, где + None + представляет любое количество, так как мы будем вводить неопределенное количество 784-пиксельных изображений. Форма + Y + имеет вид + [None, 10] +, так как мы будем использовать его для неопределенного числа выходных меток с 10 возможными классами. Тензор + keep_prob + используется для управления частотой отсева, и мы инициализируем его как заполнитель, а не как неизменную переменную, потому что мы хотим использовать один и тот же тензор для обучения (когда для + dropout + установлено значение +0.5+ `) и тестирование (когда + dropout + установлено в + 1.0 + `).

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

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

main.py

...
weights = {
   'w1': tf.Variable(tf.truncated_normal([n_input, n_hidden1], stddev=0.1)),
   'w2': tf.Variable(tf.truncated_normal([n_hidden1, n_hidden2], stddev=0.1)),
   'w3': tf.Variable(tf.truncated_normal([n_hidden2, n_hidden3], stddev=0.1)),
   'out': tf.Variable(tf.truncated_normal([n_hidden3, n_output], stddev=0.1)),
}

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

main.py

...
biases = {
   'b1': tf.Variable(tf.constant(0.1, shape=[n_hidden1])),
   'b2': tf.Variable(tf.constant(0.1, shape=[n_hidden2])),
   'b3': tf.Variable(tf.constant(0.1, shape=[n_hidden3])),
   'out': tf.Variable(tf.constant(0.1, shape=[n_output]))
}

Затем настройте слои сети, определив операции, которые будут манипулировать тензорами. Добавьте эти строки в ваш файл:

main.py

...
layer_1 = tf.add(tf.matmul(X, weights['w1']), biases['b1'])
layer_2 = tf.add(tf.matmul(layer_1, weights['w2']), biases['b2'])
layer_3 = tf.add(tf.matmul(layer_2, weights['w3']), biases['b3'])
layer_drop = tf.nn.dropout(layer_3, keep_prob)
output_layer = tf.matmul(layer_3, weights['out']) + biases['out']

Каждый скрытый слой будет выполнять матричное умножение на выходах предыдущего слоя и весах текущего слоя и добавлять смещение к этим значениям. На последнем скрытом слое мы применим операцию удаления, используя наше значение + keep_prob +, равное 0,5.

Последний шаг в построении графика - определить функцию потерь, которую мы хотим оптимизировать. Популярный выбор функции потерь в программах TensorFlow - это cross-entropy, также известная как log-loss, которая количественно определяет разницу между двумя распределениями вероятностей (предсказаниями и метками). Идеальная классификация привела бы к кросс-энтропии 0, а потери были бы полностью сведены к минимуму.

Нам также нужно выбрать алгоритм оптимизации, который будет использоваться для минимизации функции потерь. Процесс с именем gradient Descent Optimizer является распространенным методом для нахождения (локального) минимума функции путем выполнения итерационных шагов вдоль градиента в отрицательном (нисходящем) направлении. В TensorFlow уже реализовано несколько вариантов алгоритмов оптимизации градиентного спуска, и в этом руководстве мы будем использовать Adam optimizer. Это распространяется на оптимизацию градиентного спуска путем использования импульса для ускорения процесса посредством вычисления экспоненциально взвешенного среднего значения градиентов и использования его в корректировках. Добавьте следующий код в ваш файл:

main.py

...
cross_entropy = tf.reduce_mean(
   tf.nn.softmax_cross_entropy_with_logits(
       labels=Y, logits=output_layer
       ))
train_step = tf.train.AdamOptimizer(1e-4).minimize(cross_entropy)

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

Шаг 5 - Обучение и тестирование

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

Прежде чем начать процесс обучения, мы определим наш метод оценки точности, чтобы мы могли распечатать его на мини-пакетах данных во время обучения. Эти печатные утверждения позволят нам проверить, что от первой итерации до последней потери уменьшаются, а точность увеличивается; они также позволят нам отслеживать, достаточно ли мы выполнили итераций для достижения согласованного и оптимального результата:

main.py

...
correct_pred = tf.equal(tf.argmax(output_layer, 1), tf.argmax(Y, 1))
accuracy = tf.reduce_mean(tf.cast(correct_pred, tf.float32))

В + correct_pred + мы используем функцию + arg_max +, чтобы сравнить, какие изображения предсказываются правильно, посмотрев на + output_layer + (предсказания) и + Y + (метки), и мы используем + равно + функция, чтобы возвратить это как список Booleans. Затем мы можем привести этот список к плавающим и вычислить среднее значение, чтобы получить общую оценку точности.

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

main.py

...
init = tf.global_variables_initializer()
sess = tf.Session()
sess.run(init)

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

  • Распространять значения вперед через сеть

  • Вычислить потери

  • Распространять значения в обратном направлении через сеть

  • Обновить параметры

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

Добавьте этот код в файл:

main.py

...
# train on mini batches
for i in range(n_iterations):
   batch_x, batch_y = mnist.train.next_batch(batch_size)
   sess.run(train_step, feed_dict={
       X: batch_x, Y: batch_y, keep_prob: dropout
       })

   # print loss and accuracy (per minibatch)
   if i % 100 == 0:
       minibatch_loss, minibatch_accuracy = sess.run(
           [cross_entropy, accuracy],
           feed_dict={X: batch_x, Y: batch_y, keep_prob: 1.0}
           )
       print(
           "Iteration",
           str(i),
           "\t| Loss =",
           str(minibatch_loss),
           "\t| Accuracy =",
           str(minibatch_accuracy)
           )

После 100 итераций каждого шага обучения, на котором мы передаем мини-серию изображений через сеть, мы распечатываем потери и точность этого пакета. Обратите внимание, что здесь не следует ожидать снижения потерь и повышения точности, поскольку значения приведены для каждой партии, а не для всей модели. Мы используем мини-пакеты изображений вместо того, чтобы передавать их по отдельности, чтобы ускорить процесс обучения и позволить сети увидеть множество различных примеров перед обновлением параметров.

После завершения обучения мы можем запустить сеанс на тестовых изображениях. На этот раз мы используем коэффициент отсева + keep_prob + + 1.0 +, чтобы гарантировать, что все юниты активны в процессе тестирования.

Добавьте этот код в файл:

main.py

...
test_accuracy = sess.run(accuracy, feed_dict={X: mnist.test.images, Y: mnist.test.labels, keep_prob: 1.0})
print("\nAccuracy on test set:", test_accuracy)

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

python main.py

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

OutputIteration 0     | Loss = 3.67079    | Accuracy = 0.140625
Iteration 100   | Loss = 0.492122   | Accuracy = 0.84375
Iteration 200   | Loss = 0.421595   | Accuracy = 0.882812
Iteration 300   | Loss = 0.307726   | Accuracy = 0.921875
Iteration 400   | Loss = 0.392948   | Accuracy = 0.882812
Iteration 500   | Loss = 0.371461   | Accuracy = 0.90625
Iteration 600   | Loss = 0.378425   | Accuracy = 0.882812
Iteration 700   | Loss = 0.338605   | Accuracy = 0.914062
Iteration 800   | Loss = 0.379697   | Accuracy = 0.875
Iteration 900   | Loss = 0.444303   | Accuracy = 0.90625

Accuracy on test set: 0.9206

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

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

Если вы находитесь на локальном компьютере и хотите использовать собственный нарисованный от руки номер, вы можете использовать графический редактор для создания собственного изображения цифры размером 28x28 пикселей. В противном случае вы можете использовать + curl + для загрузки следующего примера тестового изображения на ваш сервер или компьютер:

curl -O https://raw.githubusercontent.com/do-community/tensorflow-digit-recognition/master/test_img.png

Откройте файл + main.py + в вашем редакторе и добавьте следующие строки кода в начало файла, чтобы импортировать две библиотеки, необходимые для работы с изображениями.

main.py

import numpy as np
from PIL import Image
...

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

main.py

...
img = np.invert(Image.open("test_img.png").convert('L')).ravel()

Функция + open + библиотеки + Image + загружает тестовое изображение в виде 4D-массива, содержащего три цветовых канала RGB и альфа-прозрачность. Это не то представление, которое мы использовали ранее при чтении в наборе данных с TensorFlow, поэтому нам потребуется проделать дополнительную работу, чтобы соответствовать формату.

Во-первых, мы используем функцию + convert + с параметром + L +, чтобы уменьшить представление 4D RGBA до одного цветового канала в градациях серого. Мы сохраняем это как массив + numpy + и инвертируем его, используя + np.invert +, потому что текущая матрица представляет черный как 0 и белый как 255, тогда как нам нужно обратное. Наконец, мы вызываем + ravel +, чтобы сгладить массив.

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

Добавьте следующий код в ваш файл, чтобы проверить изображение и распечатать выведенную этикетку.

main.py

...
prediction = sess.run(tf.argmax(output_layer, 1), feed_dict={X: [img]})
print ("Prediction for test image:", np.squeeze(prediction))

Функция + np.squeeze + вызывается для предсказания, чтобы вернуть единственное целое число из массива (т.е. перейти от [2] к 2). Полученный результат демонстрирует, что сеть распознала это изображение как цифру 2.

OutputPrediction for test image: 2

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

Заключение

В этом руководстве вы успешно обучили нейронную сеть классифицировать набор данных MNIST с точностью около 92% и протестировали ее на своем собственном изображении. Современные исследования позволяют решить эту проблему примерно на 99%, используя более сложные сетевые архитектуры, включающие сверточные уровни. Они используют 2D-структуру изображения для лучшего представления содержимого, в отличие от нашего метода, который сглаживает все пиксели в один вектор из 784 единиц. Вы можете прочитать больше об этой теме на веб-сайте TensorFlow, а также ознакомиться с научными статьями, подробно описывающими наиболее точные результаты, на http: // yann. .lecun.com / exdb / mnist / [веб-сайт MNIST].

Теперь, когда вы знаете, как построить и обучить нейронную сеть, вы можете попробовать и использовать эту реализацию на своих собственных данных или протестировать ее на других популярных наборах данных, таких как Google StreetView Номера домов, или CIFAR-10 набор данных для более общего распознавания изображений.

Related