Определение дорожных знаков с помощью TensorFlow

машинное обучение

Этот блог является переводомWaleed AbdullaНаписано с использованием TensorFlow для определения дорожных знаков, автор разрешил перевод, этооригинальный.


Я видел знак ограничения скорости, но я просто не видел тебя

Это использует модель глубокого обучения для распознавания дорожных знаков.первая часть. Цель этой серии — научиться использовать глубокие модели для построения системы, как вам тоже интересно учиться вместе со мной. В Интернете можно найти множество ресурсов, объясняющих математическую теорию нейронных сетей, поэтому я сосредоточусь на том, чтобы поделиться практическими аспектами. Далее я расскажу о своем опыте создания этой модели и поделюсьисходный коди сопутствующие материалы. Если вы уже владеете базовым синтаксисом Python и простыми техниками машинного обучения, то эта серия будет для вас. Но если вы хотите по-настоящему понять машинное обучение, создание реальной системы самостоятельно — отличный способ.

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

запустить код

исходный код здесьJupyterв блокноте. Версия Python, которую я использую,3.5, версия TensorFlow0.12. Если вы хотите запустить этот код в Docker, вы можете использовать мойDockertools. Запустите следующую командную строку:

docker run -it -p 8888:8888 -p 6006:6006 -v ~/traffic:/traffic waleedka/modern-deep-learning

Из исходного кода вы обнаружите, что каталог моего проекта находится в~/trafficНиже я сопоставил его сDockerконтейнер/trafficниже каталога. Если вы используете другой каталог проекта, вы можете изменить его.

Найти тренировочные данные

Моей первой задачей было найти хороший набор данных для обучения. Распознавание дорожных знаков — хорошая тема для исследования, поэтому мы можем найти много вещей в Интернете.

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


Ты сможешькликните сюдаЗагрузите набор данных. На странице загрузки много наборов данных, но вам нужно только скачатьBelgiumTS for Classification (cropped images)Два файла ниже каталога:

  • BelgiumTSC_Training (171.3MBytes)
  • BelgiumTSC_Testing (76.5MBytes)

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

/traffic/datasets/BelgiumTS/Training/ 
/traffic/datasets/BelgiumTS/Testing/

Обе эти папки имеют 62 подпапки, пронумерованные от00000прибыть00061. Имя подпапки идентифицирует метку изображений внутри.

Исследуйте наборы данных

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

Изображения в этом наборе данных используют.ppmформат сохранен. На самом деле это старый формат сохранения изображений, и многие инструменты его больше не поддерживают. Это также означает, что я не могу легко просматривать изображения в этих папках. К счастью,Scikit Image libraryКартины этой формы можно узнать. Следующий код используется для загрузки данных и возврата двух списков: изображений и меток.

def load_data(data_dir):
    # Get all subdirectories of data_dir. Each represents a label.
    directories = [d for d in os.listdir(data_dir) 
                   if os.path.isdir(os.path.join(data_dir, d))]
    # Loop through the label directories and collect the data in
    # two lists, labels and images.
    labels = []
    images = []
    for d in directories:
        label_dir = os.path.join(data_dir, d)
        file_names = [os.path.join(label_dir, f) 
                      for f in os.listdir(label_dir) 
                      if f.endswith(".ppm")]
        for f in file_names:
            images.append(skimage.data.imread(f))
            labels.append(int(d))
    return images, labels

images, labels = load_data(train_data_dir)

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

После загрузки данных и преобразования их вNumpyФормат. Я написал демонстрационную программу, которая отображает образец изображения для каждой этикетки. кодэто здесь, следующий наш набор данных:


The training set. consists of 62 classes. The numbers in parentheses are the count of images of each class.

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

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


Several sample images of label 32

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

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

Работа с изображениями разных размеров

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

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


Resizing images to a similar size and aspect ratio

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

for image in images[:5]:
    print("shape: {0}, min: {1}, max: {2}".format(
          image.shape, image.min(), image.max()))
Output:
shape: (141, 142, 3), min: 0, max: 255
shape: (120, 123, 3), min: 0, max: 255
shape: (105, 107, 3), min: 0, max: 255
shape: (94, 105, 3), min: 7, max: 255
shape: (128, 139, 3), min: 0, max: 255

Размер картины примерно128 * 128или около того, тогда мы можем использовать этот размер для сохранения изображения, что может сохранить как можно больше информации. Однако на ранних этапах разработки я предпочитаю использовать меньший размер, потому что тогда обучение модели будет быстрым, что позволит мне выполнять итерации быстрее. я тестировал16 * 16и20 * 20размер, но они все слишком малы. Наконец, я выбрал32 * 32размер, который легко определить на картинке невооруженным глазом (см. рисунок ниже), и мы хотим убедиться, что коэффициент уменьшения128 * 128кратно .

Еще у меня есть привычка распечатывать минимальное и максимальное значения в данных. Это простой способ проверить диапазоны данных и заблаговременно отловить программные ошибки. В этом наборе данных он говорит мне, что цвет изображения находится в стандартном диапазоне.0~255.


Images resized to 32x32

минимально жизнеспособная модель

Наконец мы подошли к самой интересной части, продолжая наш простой стиль. Мы начнем с самой простой модели: однослойной сети, в которой каждый нейрон представляет собой метку.


В сети 62 нейрона, каждый из которых принимает на вход значения RGB всех пикселей на картинке. Фактически каждый нейрон получает32 * 32 * 3 = 3072вход. Это полностью связанный слой, потому что каждый нейрон связан с входным слоем. Возможно, вам уже знакомо следующее уравнение:

y = xW + b

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

Построить граф TensorFlow

TensorFlow инкапсулирует архитектуру нейронной сети в граф выполнения. Построенный граф содержит операции (называемые Ops), такие как Add, Multiply, Reshape, ..... и так далее. Эти операции выполняют операции над данными в тензорах (многомерных массивах).


Visualization of a part of a TensorFlow graph

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

# Create a graph to hold the model.
graph = tf.Graph()

# Create model in the graph.
with graph.as_default():
    # Placeholders for inputs and labels.
    images_ph = tf.placeholder(tf.float32, [None, 32, 32, 3])
    labels_ph = tf.placeholder(tf.int32, [None])

    # Flatten input from: [None, height, width, channels]
    # To: [None, height * width * channels] == [None, 3072]
    images_flat = tf.contrib.layers.flatten(images_ph)

    # Fully connected layer. 
    # Generates logits of size [None, 62]
    logits = tf.contrib.layers.fully_connected(images_flat, 62, tf.nn.relu)

    # Convert logits to label indexes (int).
    # Shape [None], which is a 1D vector of length == batch_size.
    predicted_labels = tf.argmax(logits, 1)

    # Define the loss function. 
    # Cross-entropy is a good choice for classification.
    loss = tf.reduce_mean(tf.nn.sparse_softmax_cross_entropy_with_logits(
        logits, labels_ph))

    # Create training op.
    train = tf.train.AdamOptimizer(learning_rate=0.001).minimize(loss)

    # And, finally, an initialization op to execute before training.
    # TODO: rename to tf.global_variables_initializer() on TF 0.12.
    init = tf.initialize_all_variables()

Сначала я создаю объект Graph. В TensorFlow есть глобальный граф по умолчанию, но я не рекомендую его использовать. Установка глобальных переменных обычно является плохой привычкой, потому что слишком легко внести ошибки. Я предпочитаю явно создавать граф самостоятельно.

graph = tf.Graph()

Затем я установил заполнитель (Placeholder) для размещения изображений и меток. Заполнители — это то, как TensorFlow получает входные данные от основной программы. Обратите внимание, что я создаю заполнители (и все остальные операции) в graph.as_default(). Преимущество этого в том, что они становятся частью создаваемого мной графа, а не глобального графа.

with graph.as_default():
    images_ph = tf.placeholder(tf.float32, [None, 32, 32, 3])
    labels_ph = tf.placeholder(tf.int32, [None])

параметрimages_phРазмерность[None, 32, 32, 3], четыре параметра представляют[批量大小,高度,宽度,通道](часто сокращенно NHWC). размер партииNoneПредставление означает, что размер пакета является гибким, то есть мы можем импортировать в модель данные любого размера пакета без изменения кода. Обратите внимание на порядок, в котором вы вводите данные, так как в некоторых моделях и платформах, таких как NCHW, может использоваться другой порядок.

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


The ReLU function

Здесь я использую функцию ReLU в качестве функции активации следующим образом:

f(x) = max(0, x)

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

# Flatten input from: [None, height, width, channels]
# To: [None, height * width * channels] == [None, 3072]
images_flat = tf.contrib.layers.flatten(images_ph)
# Fully connected layer. 
# Generates logits of size [None, 62]
logits = tf.contrib.layers.fully_connected(images_flat, 62,
    tf.nn.relu)

Выход полносвязного слоя — логарифмический вектор длины 62 (технически его выходная размерность должна быть [None, 62], так как мы обрабатываем пакет за пакетом).

Выходные данные могут выглядеть так: [0,3, 0, 0, 1,2, 2,1, 0,01, 0,4, ... ..., 0, 0]. Чем выше значение, тем больше вероятность того, что изображение представляет метку. Выход не является вероятностью, они могут быть любыми значениями, а результат сложения не равен 1. Фактическое значение выходного нейрона не имеет значения, потому что это просто относительное значение относительно 62 нейронов. При необходимости мы можем легко преобразовать в вероятности, используя softmax или другие функции (здесь они не нужны).


Bar chart visualization of a logits vector

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

# Convert logits to label indexes.
# Shape [None], which is a 1D vector of length == batch_size.
predicted_labels = tf.argmax(logits, 1)

Выходом функции argmax будет целое число в диапазоне [0, 61].

Функция потерь и градиентный спуск

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


Credit: Wikipedia

Кросс-энтропия — это мера разницы между двумя векторами вероятности. Поэтому нам нужно преобразовать метки и выходные данные нейронной сети в векторы вероятности. В TensorFlow есть функция sparse_softmax_cross_entropy_with_logits, которая делает это. Эта функция принимает метку и выходные данные нейронной сети в качестве входных параметров и делает три вещи: во-первых, преобразует размерность метки в[None, 62](это вектор 0-1); во-вторых, используйте функцию softmax для преобразования данных метки и выходных данных нейронной сети в значения вероятности; в-третьих, вычислите перекрестную энтропию между ними. Эта функция вернет размер, который[None](длина вектора — это размер пакета), затем мы передаем функцию reduce_mean, чтобы получить значение, представляющее окончательное значение потерь.

loss = tf.reduce_mean(
        tf.nn.sparse_softmax_cross_entropy_with_logits(
            logits, labels_ph))

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

train = tf.train.AdamOptimizer(learning_rate=0.001).minimize(loss)

Последний узел в графе инициализирует все операции, он просто устанавливает значение всех переменных равными нулю (или случайным значениям).

init = tf.initialize_all_variables()

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

тренировочный цикл

Здесь мы итеративно обучаем модель. Прежде чем мы начнем обучение, нам нужно создать объект Session.


Вспомните объект Graph, о котором мы упоминали ранее, и то, как он содержит все операции (Ops) в модели. С другой стороны, сессия (Session) также содержит значения всех переменных. Если граф содержит уравненияy = xW + b, то сессия сохраняет актуальные значения этих переменных.

session = tf.Session(graph=graph)

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

session.run(init)

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

for i in range(201):
    _, loss_value = session.run(
        [train, loss], 
        feed_dict={images_ph: images_a, labels_ph: labels_a})
    if i % 10 == 0:
        print("Loss: ", loss_value)

Как видите, я установил количество циклов на 201 и распечатал значение потерь, когда количество циклов кратно 10. Окончательный вывод выглядит следующим образом:

Loss:  4.2588
Loss:  2.88972
Loss:  2.42234
Loss:  2.20074
Loss:  2.06985
Loss:  1.98126
Loss:  1.91674
Loss:  1.86652
Loss:  1.82595
...

использование модели

Теперь у нас есть обученная модель, хранящаяся в памяти в объекте Session. Если мы хотим использовать его, мы можем вызвать функцию session.run(), чтобы использовать его. Операция predicted_labels возвращает результат функции argmax, что нам и нужно. Ниже я случайным образом взял 10 изображений для классификации и одновременно распечатал результаты маркировки и прогнозирования.

# Pick 10 random images
sample_indexes = random.sample(range(len(images32)), 10)
sample_images = [images32[i] for i in sample_indexes]
sample_labels = [labels[i] for i in sample_indexes]
# Run the "predicted_labels" op.
predicted = session.run(predicted_labels,
                        {images_ph: sample_images})
print(sample_labels)
print(predicted)

Output:
[15, 22, 61, 44, 32, 22, 57, 38, 56, 38]
[14  22  61  44  32  22  56  38  56  38]

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


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

оценивать

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

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

# Run predictions against the full test set.
predicted = session.run(predicted_labels, 
                        feed_dict={images_ph: test_images32})
# Calculate how many matches we got.
match_count = sum([int(y == y_) 
                   for y, y_ in zip(test_labels, predicted)])
accuracy = match_count / len(test_labels)
print("Accuracy: {:.3f}".format(accuracy))

В каждом прогоне мы получаем правильную скорость от 0,40 до 0,70, в зависимости от того, попадает ли модель в локальный или глобальный минимум. Это также неизбежная проблема запуска такой простой модели. В следующей статье я расскажу, как улучшить согласованность результатов.

закрыть сеанс

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

# Close the session. This will destroy the trained model.
session.close()