Реализация Transfer learning с библиотекой Keras

22 ноября 2018
transfer-learning-keras

Реализация Transfer learning с библиотекой Keras

Для большинства задач компьютерного зрения не существует больших датасетов (около 50 000 изображений). Даже при экстремальных стратегиях аугментации данных трудно добиться высокой точности. Обучение таких сетей с миллионами параметров обычно…

Для большинства задач компьютерного зрения не существует больших датасетов (около 50 000 изображений). Даже при экстремальных стратегиях аугментации данных трудно добиться высокой точности. Обучение таких сетей с миллионами параметров обычно имеет тенденцию перегружать модель. В этом случае Transfer learning готов прийти на помощь.

Inception-V3 Google Research

Что такое Transfer learning?

Transfer Learning (TL) — одно из направлений исследований в машинном обучении, которое изучает возможность применения знаний, полученных при решении одной задачи, к другой.

Зачем нужен Trasfer learning?

  • Лишь немногие обучают сверточные сети с нуля (с помощью случайной инициализации), потому что зачастую нужные датасеты отсутствуют. Таким образом, использование предварительно обученных весовых коэффициентов или фиксированного экстрактора свойств помогает разрешить большинство проблем.
  • Сети с большим количеством слоев обучать дорого и долго. Самые сложные модели требуют нескольких недель для обучения с использованием сотен дорогостоящих графических процессоров.
  • Определение структуры сети, методов и параметров обучения — настоящее искусство, так как соответствующей теории, которая поможет разобраться с этими проблемами, просто не существует.

Как может помочь Trasfer learning?

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

transfer learning
Inception V3 Google Research

Простая реализация с помощью Keras


Помня, что свойства СonvNet являются более примитивными на первых слоях и усложняются с каждым последующим слоем, рассмотрим четыре типичных сценария.

1. Новый датасет небольшой и аналогичен исходному датасету

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

Таким образом, давайте отбросим все слои VGG19 и обучим только классификатор:

for layer in model.layers:
   layer.trainable = False
#Now we will be training only the classifiers (FC layers)

2. Новый датасет большой и аналогичен исходному

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

for layer in model.layers:
   layer.trainable = True
#The default is already set to True. I have mentioned it here to make things clear.

Если вы хотите отбросить несколько первых слоев, формирующих самые примитивные свойства, это можно сделать с помощью следующего кода:

for layer in model.layers[:5]:
   layer.trainable = False.
# Here I am freezing the first 5 layers

3. Новый датасет небольшой и сильно отличается от исходного

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

Этот код должен помочь. Он будет извлекать свойства «block2_pool». Обычно это не очень полезно, поскольку слой имеет 64х64х128 свойств и обучение классификатора поверх них может и не помочь. Можно добавить несколько полносвязных слоев и обучить нейронную сеть поверх них. Это нужно делать в следующем порядке:

  1. Добавьте несколько FC слоев и выходной слой.
  2. Задайте весовые коэффициенты для более ранних слоев и отбросьте их.
  3. Обучите сеть.

4. Новый датасет большой и сильно отличается от исходного

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

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

Дерево решений: метод «белого ящика» в машинном обучении

20 ноября 2018
дерево решений

Дерево решений: метод «белого ящика» в машинном обучении

Дерево решений — логическая схема, позволяющие получить окончательное решение о классификации объекта после ответов на иерархически организованную систему вопросов. Стоит сказать, большинство высоко результативных решений на Kaggle — комбинация XGboost-ов,…

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

Один уровень

Стоящая за деревьями решений идея проста. Представим датасет, созданный путем записи времени ухода из дома и времени прихода на работу. Анализируя эти данные, можно увидеть, что в большинстве случаев выход из дома раньше 8:15 приводит к своевременному прибытию на работу, а выход после 8:15 — к опозданию.

дерево решений

Теперь этот паттерн можно выразить через дерево решений. В самой первой точке разветвления следует задать вопрос: “Выход из дома осуществляется раньше 8:15?”. Теперь есть две ветви — “да” и “нет”. Для согласованности будем считать положительный ответ левой веткой. Вводя такую границу решения, мы разбиваем данные на две группы. Хотя в таком случаем есть некоторые исключения и сложности, общее правило — разделение по времени с границей 8:15. Если вы выходите до 8:15, можете быть уверены, что попадете на работу вовремя. В противном случае — будьте уверены, что опоздаете.

решающие деревья

Это самое простое дерево решений, состоящее из одной пары ветвей.

Два уровня

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

Выход до 8:00 однозначно приведет к своевременному появлению на работе, тогда как с 8:00 до 8:15 — лишь к высокой вероятности прийти вовремя, но не к гарантии. Похожим образом ветвь с выходом после 8:15 делится на две ветви с решающим вопросом: “отправление до 8:30?”. Если ответ положительный, то есть большая вероятность опоздать, если же отрицательный —  вы гарантированно опоздаете.

дерево решений с двумя уровнями

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

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

Многомерное дерево решений

Можно расширить этот пример на случай нескольких предикторных переменных. Рассмотрим время выхода и день недели. Начнем собирать данные с понедельника (день 1), тогда суббота = 6, воскресенье = 7. Исследуя данные, можно видеть, что в субботу и воскресенье зеленые точки смещены в левую сторону. Это означает, выход в 8:10 является достаточным, чтобы успеть на работу вовремя в будний день, но не достаточным в выходные.

дерево решений пример

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

дерево решений пример задачи

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

решающее дерево с тремя уровнями

Можно еще сильнее уточнить оценку разделением ветки с отправлением до 8:15 в выходной день на отправление до 8:00 и после. Отправление до 8:00 скорее всего приведет к своевременному появлению на работе, а в интервале с 8:00 до 8:15 к опозданию с большой вероятностью. Получилось двумерное дерево решений, аккуратно поделенное на 4 различных региона. Два из них соответствуют прибытию вовремя, два — опозданию.

дерево решений с тремя уровнями

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

Регрессионное дерево решений

Рассмотрим случай с непрерывной целевой переменной, а не категориальной. В случае использования модели для предсказания непрерывных количественных переменных дерево называется регрессионным. Мы посмотрели на одномерные и двумерные классификационные деревья, теперь настало время взглянуть на регрессионные.

Перед нами стоит задача оценки времени пробуждения в зависимости от возраста человека. Корень нашего регрессионного дерева — оценка всего датасета. В этом случае, если требуется оценка без знания возраста конкретного человека, разумным предположением будет 6:25. Это и будет корнем нашего дерева.

возраст время подъема

Разумное первое разбиение — возраст 25 лет. В среднем, люди моложе 25 лет просыпаются в 7:05, а старше 25 — в 6 часов.

регрессионное дерево решений

Существует всё еще много вариаций разбиения на возрастные группы, поэтому мы можем разделить выборку еще раз. Теперь можно предположить, что люди младше 12 лет просыпаются в 7:45, а в возрасте от 12 до 25 лет — в 6:40.

Группа людей старше 25 лет тоже может быть разумно разделена. Люди в возрасте от 25 до 40 лет просыпаются в среднем в 6:10, а в возрасте от 40 до 70 — в 5:50.

Поскольку наблюдается большая неоднозначность для младшей группы, можем разделить её еще раз. Теперь границей решений будет возраст 8 лет, что позволит более точно подстроиться под данные. Также можно разделить возрастную группу в диапазоне от 40 до 70 лет на отметке 58 лет. Отметим, мы добиваемся того, чтобы в каждом листе дерева находилось только одно или два значения из данных. Но это условие опасно тем, что может приводить к переобучению, о котором мы поговорим в скором времени.

дерево принятия решений

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

  • “Младше 25 лет?” — Нет; идем вправо.
  • “Младше 40 лет?” — Да; идем влево.
  • Оценка для этого листа — 6:10.

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

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

Разветвленное дерево

Мы создаем дерево решений почти таким же образом, как и прошлое. Начинаем с корня — единичная оценка, которая грубо описывает весь набор данных — 6:30. (Здесь представлен код для визуализации с помощью библиотеки matplotlib).

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

  • популяция младше 35 лет с временем пробуждения 7:06
  • популяция старше 35 лет с временем пробуждения 6:12

Повторяем этот процесс, разделяя более молодую популяцию на два уровня — событие произошло до середины сентября и событие произошло до середины марта, соответственно. Такое разделение изолирует зимние месяцы от летних. Время пробуждения в зимние месяцы — 7:30 для людей младше 35 лет, а для летних — 6:56.

Теперь можем вернуться в узел с популяцией старше 35 лет и разделить его еще раз с границей в 48 лет для более точного представления.

Таким же образом разделим группу младше 35 лет для зимних месяцев добавлением границы в 18 лет. Человек младше 18 в зимние месяцы просыпается в 7:54, в противном случае, в 6:48.

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

Следующее разделение продолжает этот тренд, фокусируясь на группе младше 35 в летние месяцы, устанавливает границу в возрасте 13 лет. Форма модели становится всё более похожа на форму данных.

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

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

Когда мы дошли до этой стадии, дерево объясняет и описывает данные очень хорошо. Даже слишком хорошо. Такая модель не только находит лежащие в основе данных тренды (гладкая кривая, по которой следуют данные), но также реагирует и на шумы (несмоделированные отклонения), характерные для исследуемых данных. Если будет необходимо применить эту модель и предсказать время пробуждения на новых данных, шум из тренировочного сета будет делать предсказание менее точным. В идеале мы хотим, чтобы дерево решений находило только тренды, но не реагировало на шумы. Один из способов защититься от переобучения — убедиться, что в каждом листе нашего дерева находится больше чем один или несколько объектов. Такой способ позволяет усреднением избавиться от шума.

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

Чтобы визуализировать многомерные данные, используем прием, предложенный Джеффри Хинтоном — исследователем в области искусственных нейронных сетей. Он рекомендует следующее: “Чтобы иметь дело с гиперплоскостью в четырнадцатимерном пространстве, представьте себе трехмерное пространстве и скажите самому себе очень громко “четырнадцать.”

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

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

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

Градиентный спуск: всё, что нужно знать

20 ноября 2018
градиентный спуск метод

Градиентный спуск: всё, что нужно знать

Градиентный спуск — самый используемый алгоритм обучения, он применяется почти в каждой модели машинного обучения. Градиентный спуск — это, по сути, и есть то, как обучаются модели. Без ГС машинное обучение не…

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

В этом посте вы найдете объяснение градиентного спуска с небольшим количеством математики. Краткое содержание:

  • Смысл ГС — объяснение всего алгоритма;
  • Различные вариации алгоритма;
  • Реализация кода: написание кода на языке Phyton.

Что такое градиентный спуск

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

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

Суть алгоритма – процесс получения наименьшего значения ошибки. Аналогично это можно рассматривать как спуск во впадину в попытке найти золото на дне ущелья (самое низкое значение ошибки).

градиентный спуск пример
Поиск минимума функции

В дальнейшем, чтобы найти самую низкую ошибку (глубочайшую впадину) в функции потерь (по отношению к одному весу), нужно настроить параметры модели. Как мы их настраиваем? В этом поможет математический анализ. Благодаря анализу мы знаем, что наклон графика функции – производная от функции по переменной. Это наклон всегда указывает на ближайшую впадину!

На рисунке мы видим график функции потерь (названный «Ошибка» с символом «J») с одним весом. Теперь, если мы вычислим наклон (обозначим это dJ/dw) функции потерь относительно одного веса, то получим направление, в котором нужно двигаться, чтобы достичь локальных минимумов. Давайте пока представим, что наша модель имеет только один вес.

Функция потерь J
Функция потерь

Важно: когда мы перебираем все учебные данные, мы продолжаем добавлять значения  dJ/dw для каждого веса. Так как потери зависят от примера обучения, dJ/dw также продолжает меняться. Затем делим собранные значения на количество примеров обучения для получения среднего. Потом мы используем это среднее значение (каждого веса) для настройки каждого веса.

Также обратите внимание: Функция потерь предназначена для отслеживания ошибки с каждым примером обучениям, в то время как производная функции относительного одного веса – это то, куда нужно сместить вес, чтобы минимизировать ее для этого примера обучения. Вы можете создавать модели даже без применения функции потерь. Но вам придется использовать производную относительно каждого веса (dJ/dw).

Теперь, когда мы определили направление, в котором нужно подтолкнуть вес, нам нужно понять, как это сделать. Тут мы используем коэффициент скорости обучения, его называют гипер-параметром. Гипер-параметр – значение, требуемое вашей моделью, о котором мы действительно имеем очень смутное представление. Обычно эти значения могут быть изучены методом проб и ошибок. Здесь не так: одно подходит для всех гипер-параметров. Коэффициент скорости обучения можно рассматривать как «шаг в правильном направлении», где направление происходит от dJ/dw.

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

Подробнее о градиентах

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

Функция потерь среднеквадратической ошибки

Производная этой функции относительно любого веса (эта формула показывает вычисление градиента для линейной регрессии):

градиентный спуск формула

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

Коэффициент скорости обучения

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

Однако проблема у большинства моделей возникает с коэффициентом скорости обучения. Давайте взглянем на обновленное выражение для каждого веса (j лежит в диапазоне от 0 до количества весов, а Theta-j это j-й вес в векторе весов, k лежит в диапазоне от 0 до количества смещений, где B-k — это k-е смещение в векторе смещений). Здесь alpha – коэффициент скорости обучения. Из этого можно сказать, что мы вычисляем dJ/dTheta-j ( градиент веса Theta-j), и затем шаг размера alpha в этом направлении. Следовательно, мы спускаемся по градиенту. Чтобы обновить смещение, замените Theta-j на B-k.

формула градиентный спуск

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

Минимум функции - градиентный спуск

Использование градиентного спуска

Что ж, вот и всё. Это всё, что нужно знать про градиентный спуск. Давайте подытожим всё в псевдокоде.

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


От i = 0 до «количество примеров обучения»

1. Вычислите градиент функции потерь для i-го примера обучения по каждому весу и смещению. Теперь у вас есть вектор, который полон градиентами для каждого веса и переменной, содержащей градиент смещения.

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

3. Подобно ситуации с весами, добавьте градиент смещения к аккумулятивной переменной.


Теперь, ПОСЛЕ перебирания всех примеров обучения, выполните следующие действия:

1. Поделите аккумулятивные переменные весов и смещения на количество примеров обучения. Это даст нам средние градиенты для всех весов и средний градиент для смещения. Будем называть их обновленными аккумуляторами (ОА).

2. Затем, используя приведенную ниже формулу, обновите все веса и смещение. Вместо dJ / dTheta-j вы будете подставлять ОА (обновленный аккумулятор) для весов и ОА для смещения. Проделайте то же самое для смещения.

Это была только одна итерация градиентного спуска.

Повторите этот процесс от начала до конца для некоторого количества итераций. Это означает, что для 1-й итерации ГС вы перебираете все примеры обучения, вычисляете градиенты, потом обновляете веса и смещения.  Затем вы проделываете это для некоторого количества итераций ГС.

Различные типы градиентного спуска

Существует 3 варианта градиентного спуска:

1. Мini-batch: тут вместо перебирания всех примеров обучения и с каждой итерацией, выполняющей вычисления только на одном примере обучения, мы обрабатываем n учебных примеров сразу. Этот выбор хорош для очень больших наборов данных.

2. Стохастический градиентный спуск: в этом случае вместо использования и зацикливания на каждом примере обучения, мы ПРОСТО ИСПОЛЬЗУЕМ ОДИН РАЗ. Есть несколько вещей замечаний:

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

3. Серия ГС: это то, о чем написано в предыдущих разделах. Цикл на каждом примере обучения.

локальный минимум градиентный спуск
Картинка, сравнивающая 3 попадания в локальные минимумы

Пример кода на python

Применимо к cерии ГС, так будет выглядеть блок учебного кода на Python.


def train(X, y, W, B, alpha, max_iters):
'''
Performs GD on all training examples,
X: Training data set,
y: Labels for training data,
W: Weights vector,
B: Bias variable,
alpha: The learning rate,
max_iters: Maximum GD iterations.
'''
dW = 0 # Weights gradient accumulator
dB = 0 # Bias gradient accumulator
m = X.shape[0] # No. of training examples
for i in range(max_iters):
dW = 0 # Reseting the accumulators
dB = 0
for j in range(m):
# 1. Iterate over all examples,
# 2. Compute gradients of the weights and biases in w_grad and b_grad,
# 3. Update dW by adding w_grad and dB by adding b_grad,
W = W - alpha * (dW / m) # Update the weights
B = B - alpha * (dB / m) # Update the bias return W, B # Return the updated weights and bias.

Вот и всё. Теперь вы должны хорошо понимать, что такое метод градиентного спуска.

FaceNet — пример простой системы распознавания лиц с открытым кодом Github

16 ноября 2018

FaceNet — пример простой системы распознавания лиц с открытым кодом Github

Распознавание лица — последний тренд в авторизации пользователя. Apple использует Face ID, OnePlus — технологию Face Unlock. Baidu использует распознавание лица вместо ID-карт для обеспечения доступа в офис, а при…

Распознавание лица — последний тренд в авторизации пользователя. Apple использует Face ID, OnePlus — технологию Face Unlock. Baidu использует распознавание лица вместо ID-карт для обеспечения доступа в офис, а при повторном пересечении границы в ОАЭ вам нужно только посмотреть в камеру.

В статье разбираемся, как сделать простейшую сеть распознавания лиц самостоятельно с помощью FaceNet.

Ссылка на Гитхаб, кому нужен только код

Немного о FaceNet

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

Триплет потерь

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

  • f(a) это энкодинг якоря
  • f(p) это энкодинг похожих лиц (positive)
  • f(n) это энкодинг непохожих лиц (negative)
  • Альфа — это константа, которая позволяет быть уверенным, что сеть не будет пытаться оптимизировать напрямую f(a) — f(p) = f(a) — f(n) = 0
  • […]+ экиввалентено max(0, sum)

Сиамские сети

FaceNet — сиамская сеть. Сиамская сеть — тип архитектуры нейросети, который обучается диффиренцированию входных данных. То есть, позволяет научиться понимать какие изображения похожи, а какие нет.

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

В FaceNet это делается путем вычисления расстояния между двумя выходами.

Реализация

Переходим к практике.

В реализации мы будем использовать Keras и Tensorflow. Кроме того, мы используем два файла утилиты из репозитория deeplayning.ai, чтобы абстрагироваться от взаимодействий с сетью FaceNet.

  • fr_utils.py содержит функции для подачи изображений в сеть и получения кодирования изображений;
  • inception_blocks_v2.py содержит функции для подготовки и компиляции сети FaceNet.

Компиляция сети FaceNet

Первое, что нам нужно сделать, это собрать сеть FaceNet для нашей системы распознавания лиц.

import os
import glob
import numpy as np
import cv2
import tensorflow as tf
from fr_utils import *
from inception_blocks_v2 import *
from keras import backend as K
K.set_image_data_format('channels_first')
FRmodel = faceRecoModel(input_shape=(3, 96, 96))
def triplet_loss(y_true, y_pred, alpha = 0.3):
    anchor, positive, negative = y_pred[0], y_pred[1], y_pred[2]

    pos_dist = tf.reduce_sum(tf.square(tf.subtract(anchor,
               positive)), axis=-1)
    neg_dist = tf.reduce_sum(tf.square(tf.subtract(anchor, 
               negative)), axis=-1)
    basic_loss = tf.add(tf.subtract(pos_dist, neg_dist), alpha)
    loss = tf.reduce_sum(tf.maximum(basic_loss, 0.0))
   
    return loss
FRmodel.compile(optimizer = 'adam', loss = triplet_loss, metrics = ['accuracy'])
load_weights_from_FaceNet(FRmodel)

Мы начнем инициализпцию нашей сети со входа размерности (3, 96, 96). Это означает, что картинка передается в виде трех каналов RGB и размерности 96×96 пикселей.

Теперь давайте определим Triplet Loss функцию. Функция в сниппете кода выше удовлетворяет уравнению Triplet Loss, которое мы определили в предыдущей секции.

Если вы не знакомы с фреймворком TensorFlow, ознакомьтесь с документацией.

Сразу после того, как мы определили функцию потерь, мы можем скомпилировать нашу систему распознавания лиц с помощью Keras. Мы будем использовать Adam optimizer для минимизации потерь, подсчитанных с помощью функции Triplet Loss.

Подготовка базы данных

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

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

def prepare_database():
    database = {}
    for file in glob.glob("images/*"):
        identity = os.path.splitext(os.path.basename(file))[0]
        database[identity] = img_path_to_encoding(file, FRmodel)
    return database

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

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

Распознавание лиц

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

def who_is_it(image, database, model):
    encoding = img_to_encoding(image, model)
    
    min_dist = 100
    identity = None
    
    # Loop over the database dictionary's names and encodings.
    for (name, db_enc) in database.items():
        dist = np.linalg.norm(db_enc - encoding)
        print('distance for %s is %s' %(name, dist))
        if dist < min_dist:
            min_dist = dist
            identity = name
    
    if min_dist > 0.52:
        return None
    else:
        return identity

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

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

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

 if min_dist > 0.52: 
     return None 
 else:
     return identity

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

На GitHub есть демо работы полученной сети, с входом от простой вебкамеры.

Заключение

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


Интересные статьи:

Сверточная нейронная сеть на PyTorch: пошаговое руководство

26 октября 2018

Сверточная нейронная сеть на PyTorch: пошаговое руководство

В предыдущем вводном туториале по нейронным сетям была создана трехслойная архитектура для классификации рукописных символов датасета MNIST. В конце туториала была показана точность приблизительно 86%. Для простого датасета, как MNIST,…

В предыдущем вводном туториале по нейронным сетям была создана трехслойная архитектура для классификации рукописных символов датасета MNIST. В конце туториала была показана точность приблизительно 86%. Для простого датасета, как MNIST, это плохое качество. Дальнейшая оптимизация смогла улучшить результат плотно соединенной сети до 97-98% точности. Это уже намного лучше, но всё еще не достаточно для MNIST. Требуется более современный метод, который может действительно называться глубоким обучением. В данном туториале представлен такой метод — сверточная нейронная сеть (Convolutional Neural Network, CNN), который достигает высоких результатов в задачах классификации картинок. В частности, будет рассмотрена и теория, и практика реализации CNN при помощи PyTorch.

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

Особенности CNN

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

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

Принцип работы CNN

Свертка — фактически главное, что необходимо понять о сверточных нейронных сетях. Этот замысловатый математический термин нужен для движущегося окна или фильтра по исследуемому изображению. Перемещающееся окно применяется к определенному участку узлов, как показано ниже. Где примененный фильтр — ( 0.5 * значение в узле):

 

 

 

 

 

 

 

На диаграмме показаны только два выходных значения, каждое из которых отображает входной квадрат размера 2×2. Вес отображения для каждого входного квадрата, как ранее упоминалось, равен 0.5 для всех четырех входов (inputs). Поэтому выход может быть посчитан так:

 

 

 

 

 

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

 

 

 

 

 

 

 

 

 

 

 

Первое положение связей движущегося фильтра показано синей линией, второе — зеленой. Веса для каждых таких соединений равны 0.5.

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

  • Редкие связи — не каждый узел в первом (входном) слое соединен с каждым узлом во втором слое. Этим отличается архитектура CNN от полностью связанной нейронной сети, где каждый узел соединен со всем другими в следующем слое.
  • Постоянные параметры фильтра. Другими словами, при движении фильтра по изображению одинаковые веса применяются для каждого 2 х 2 набора узлов. Каждый фильтр может быть обучен для выполнения специфичных трансформаций входного пространства. Следовательно, каждый фильтр имеет определенный набор весов, которые применяются для каждой операции свертки. Этот процесс уменьшает количество параметров. Нельзя говорить, что любой вес постоянен внутри отдельного фильтра. В примере выше веса были [0.5, 0.5, 0.5, 0.5], но ничего не мешало им быть и [0.25, 0.1, 0.8, 0.001]. Выбор конкретных значений зависит от обучения каждого фильтра.

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

Следующий шаг в структуре CNN — прохождение выхода операции свертки через нелинейную активационную функцию. Речь идет о некотором подвиде ReLU, который обеспечивает известное нелинейное поведение этой нейронной сети.

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

Отображение признаков и мультиканальность

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

 

 

 

 

 

 

 

Теперь в правой части рисунка можно видеть несколько сложенных (stacked) выходов операции свертки. Их несколько, потому что существует несколько обучаемых фильтров, каждый из которых производит собственный 2D выход (в случае 2D изображения). Такое множество фильтров часто в глубоком обучении часто называют каналами. Каждый канал должен обучаться для выделения на изображении определенного ключевого признака. Выход сверточного слоя для черно-белого изображения, как в датасете MNIST, имеет 3 измерения — 2D для каждого из каналов и еще одно для их числа.

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

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

Объединение (Pooling)

Основными преимуществами для пулинга в сверточной нейронной сети являются:

  • Уменьшение количества параметров в вашей модели благодаря процессу даунсемплинга (down-sampling).
  • Детектирование признаков становится более правильным при изменении ориентации или размера объекта.

Пулинг — другой тип техники скользящего окна, где вместо применения обучаемых весов используется статистическая функция некоторого типа по содержимому этого окна. Наиболее частый тип пулинга — max pooling, который применяет функцию max(). Есть и другие варианты — mean pooling (который применяет функцию усреднения по содержимому окна), которые применяются в особых случаях. В этом туториале мы будем концентрироваться на max pooling. На рисунке ниже показано, как работает операция max pooling:

 

 

 

 

 

 

 

Давайте пройдемся по некоторым пунктам, связанным с диаграммой:

Основы

На диаграмме можно наблюдать действие max pooling. Для первого окна голубого цвета max pooling выдает значение 3.0, которое является максимальным значением узла в 2х2 окне. Таким же образом зеленое окно выводит максимальное значение, равно 5.0, а для красного окна максимальное значение — 7.0. Здесь всё просто и понятно.

Шаги и даунсемплинг

На диаграмме сверху можно заметить, что пулинговое окно каждый раз перемещается на 2 места. Можем говорить, что шаг равен 2. На диаграмме показаны шаги только вдоль оси x,  но для задачи предотвращения перекрытия окна, шаг должен быть также равен 2 и в направлении y. Другими словами, шаг обозначается как [2,2]. Следует упомянуть, если во время пулинга шаг больше 1, тогда размер выхода будет уменьшен. Как можно видеть на диаграмме, входной объект размера 5×5 уменьшается до 3х3 на выходе. И это хорошо — такое явление называется даунсемплингом и уменьшает количество обучаемых параметров в модели.

Padding

Важно отметить также, что в пулинговой диаграмме есть дополнительный столбец и строка, добавленные к входу размера 5х5, делающие эффективный размер пулингового пространства равным 6х6. Это делается для того, чтобы пулинговое окно размером 2х2 корректно работало с шагом [2,2]. Такой прием называется padding. Padding-узлы зачастую фиктивные, так как значения на них равны 0, и операция max pooling их не видит. Этот факт нужно будет учитывать при создании нашей сверточной сети на PyTorch.

Теперь мы разобрались в механизме работы пулинга в CNN,  выяснили его полезность в осуществлении даунсемплинга. Рассмотрим еще его некоторые функции и ответим на вопрос, почему max pooling используется так часто.

Использование пулинга в сверточных нейронных сетях

В дополнении к функции даунсемплинга пулинг используется в CNN, чтобы детектировать определенные признаки, инвариантные к изменениям размера или ориентации. Другой способ представить действие пулинга  — он обобщает низкоуровневую, сложно структурированную информацию. Представим случай, когда у нас есть сверточный фильтр, который во время тренировки обучается распознавать знак «9» в различных положениях на входном изображении. Чтобы сверточная сеть научилась корректно классифицировать появление «9» на картинке, требуется каким-то образом активировать сеть каждый раз, когда эта цифра появляется на изображении независимо от размера и ориентации (кроме случая, когда «9» напоминает «9»). Пулинг может помочь в такой задаче выбора высокоуровневых, обобщенных признаков. Этот процесс иллюстрирован ниже:

 

 

 

 

 

 

 

Диаграмма — стилизованное представление операции пулинга. Если мы считаем, что маленький участок входного изображения содержит цифру 9 (зеленый квадрат), и предполагаем, что пытаемся детектировать эту цифру на изображении. В таком случае несколько сверточных фильтров обучаются активироваться с помощью ReLU функции каждый раз, когда они видят «9» на картинке. Однако, они будут активироваться более или менее сильно в зависимости от того, как она расположена. Мы хотим научить сеть обнаруживать цифру на изображении независимо от ее ориентации. Здесь наступает черед пулинга. Он«смотрит» на выходы трех фильтров и берет настолько высокое значение, насколько высокий порог функций активации этих фильтров.

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

Общий взгляд

Ниже представлено изображение из Википедии, которое показывает структуру полностью разработанной сверточной нейронной сети:

 

 

 

 

 

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

Полносвязный слой

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

Рассматривая предыдущую диаграмму, на выходе имеем несколько каналов x x y тензоров/матриц. Эти каналы необходимо привести к одному (N x 1) тензору. Возьмем пример: имеем 100 каналов с 2х2 матрицами, отображающими выход последней пулинг операции в сети. В PyTorch можно легко осуществить преобразование в 2х2х100 = 400 строк, как будет показано ниже.

Теперь, когда основы сверточных нейронных сетей заложены, настало время реализовать CNN с помощью PyTorch.

Реализация CNN на PyTorch

Любой достойный фреймворк глубокого обучения может с легкостью справиться с операциями сверточной нейросети. PyTorch является таким фреймворком. В данном разделе будет показано, как создавать CNN с помощью PyTorch шаг за шагом. В идеале вы должны обладать некоторым представлением о PyTorch, но это не обязательно. Мы хотим разработать нейронную сеть для классификации символов в датасете MNIST. Полный код к этому туториалу находится в этом репозитории на GitHub.

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

 

 

 

 

 

 

В самом начале на вход подаются черно-белые представления символов размером 28х28 пикселей каждое. Первый слой состоит из 32 каналов сверточных фильтров размера 5х5 + активационная функция ReLU, затем идет 2х2 max pooling с даунсемплингом с шагом 2 (этот слой выводит данные размером 14х14). На следующий слой подается выход с первого слоя размера 14х14, который сканируется снова 5х5 сверточными фильтрами с 64 каналов, затем следует 2х2 max pooling с даунсемплингом для генерирования выхода размером 7х7.

После сверточной части сети следует:

  • операция выравнивания, которая создает 7х7х64=3164 узлов
  • средний слой из 1000 полносвязных улов
  • операция softmax над крайними 10 узлами для генерирования вероятностей классов.

Эти слои представлены в выходном классификаторе.

Загрузка датасета

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

num_epochs = 5 
num_classes = 10 
batch_size = 100 
learning_rate = 0.001
DATA_PATH = 'C:\\Users\Andy\PycharmProjects\MNISTData'
MODEL_STORE_PATH = 'C:\\Users\Andy\PycharmProjects\pytorch_models\\'

Прежде всего, устанавливаем тренировочные гиперпараметры. Далее идет спецификация пути папки для хранения датасета MNIST (PyTorch автоматически загрузит датасет в эту папку) и локации для тренировочных параметров модели после того, как обучение будет завершено.

Далее задаем преобразование для применения к MNIST и переменные для данных:

trans = transforms.Compose([transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,))]) 

train_dataset = torchvision.datasets.MNIST(root=DATA_PATH, train=True, transform=trans, download=True) 
test_dataset = torchvision.datasets.MNIST(root=DATA_PATH, train=False, transform=trans)

Первое, на что необходимо обратить внимание в коде сверху, это функция transform.Compose(). Функция находится в  пакете torchvision. Она позволяет разработчику делать различные манипуляции с указанным датасетом. Численные трансформации могут быть соединены вместе в список при использовании функции Compose(). В этом случае сначала устанавливается преобразование, которое конвертирует входной датасет в PyTorch тензор. PyTorch тензор — особый тип данных, используемый в библиотеке для всех различных операций с данными и весами внутри нейросети. По сути, такой тензор — простая многомерная матрица. Но в любом случае, PyTorch требует преобразования датасета в тензор таким образом, что его можно использовать для тренировки и тестирования сети.

Следующий аргумент в списке Compose() — нормализация. Нейронная сеть обучается лучше, когда входные данные нормализованы так, что их значения находятся в диапазоне от -1 до 1 или от 0 до 1. Чтобы это сделать с помощью нормализации PyTorch, необходимо указать среднее и стандартное отклонение MNIST датасета, которые в этом случае равны 0.1307 и 0.3081 соответственно. Отметим, среднее значение и стандартное отклонение должны быть установлены для каждого входного канала. У MNIST есть только один канал, но уже для датасета CIFAR c 3 каналами (по одному на каждый цвет из RGB спектра) надо указывать среднее и стандартное отклонение для каждого.

Далее необходимо создать объекты  train_dataset и test_dataset, которые будут последовательно проходить через загрузчик данных. Чтобы создать такие датасеты из данных MNIST, требуется задать несколько аргументов. Первый — путь до папки, где хранится файл с данными для тренировки и тестирования. Логический аргумент train показывает, какой файл из train.pt или  test.pt стоит брать в качестве тренировочного сета. Следующий аргумент — transform, в котором мы указываем ранее созданный объект trans, который осуществляет преобразования. Наконец, аргумент загрузки просит функцию датасета MNIST загрузить при необходимости данные из онлайн источника.

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

train_loader = DataLoader(dataset=train_dataset, batch_size=batch_size,shuffle=True) 
test_loader = DataLoader(dataset=test_dataset, batch_size=batch_size, shuffle=False)

Объект загрузчик данных в PyTorch обеспечивает несколько полезных функций при использовании тренировочных данных:

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

Как можно видеть, требуется задать три простых аргумента. Первый — данные, которые вы хотите загрузить; второй — желаемый размер партии; третий — перемешивать ли случайным образом датасет. Загрузчик может использоваться в качестве итератора. Поэтому для извлечения данных мы можем просто воспользоваться стандартным итератором Python, например, перечислением. Такая возможность будет позже продемонстрирована на практике в туториале.

Создание модели

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

class ConvNet(nn.Module): 
     def __init__(self): 
         super(ConvNet, self).__init__() 
         self.layer1 = nn.Sequential( nn.Conv2d(1, 32, kernel_size=5, stride=1, padding=2), 
            nn.ReLU(), nn.MaxPool2d(kernel_size=2, stride=2)) 
         self.layer2 = nn.Sequential( nn.Conv2d(32, 64, kernel_size=5, stride=1, padding=2), 
            nn.ReLU(), nn.MaxPool2d(kernel_size=2, stride=2)) 
         self.drop_out = nn.Dropout() 
         self.fc1 = nn.Linear(7 * 7 * 64, 1000) 
         self.fc2 = nn.Linear(1000, 10)

Это то место, где определяется модель. Наиболее простой способ создания структуры нейронной сети в PyTorch — создание наследственного класса от материнского nn.Module. Класс nn.Module очень полезен в PyTorch, он содержит всё необходимое для конструирования типичной глубокой нейронной сети. Кроме этого, класс имеет удобные функции: способы перемещения переменных и операций на GPU или обратно на CPU, применение рекурсивных функций через все свойства в классе (например, перенастройка всех весовых переменных), создание оптимизированных интерфейсов для тренировки и тому подобное. Все доступные методы можно посмотреть здесь.

Первый шаг — создание нескольких объектов последовательного слоя с помощью функции class _init_. Сначала создаем слой 1 (self.layer1) через создание объекта nn.Sequential. Этот метод позволяет создавать упорядоченные слои в сети и является удобным способом создания последовательности свертка + ReLu + пулинг. Как можно видеть, первый элемент в определении этой последовательности — метод Conv2d, который создает набор сверточных фильтров. В этом методе первый аргумент — количество входных каналов; в нашем случае изображения MNIST черно-белые и количество каналов равно 1. Второй аргумент в Conv2d — количество выходных каналов; как показано на диаграмме архитектуры модели, первый слой сверточных фильтров состоит из 32 каналов, поэтому значение второго аргумента равно 32.

Аргумент kernel_size отвечает за размер сверточного фильтра, в нашем случае мы хотим фильтр размеров 5х5, поэтому аргумент равен 5. Если бы мы хотели фильтры с разными размерными формами по оси х и y, необходимо было указать параметры в виде python tuple (размер по х, размер по y). Наконец, мы хотим установить padding, что делается несколько сложнее. Размер измерения на выходе от операции  сверточной фильтрации или пулинга может быть посчитан с помощью уравнения:

 

 

Где W(in) — ширина выхода, F — размер фильтра, P — паддинг, S — шаг. Такая же формула применима для расчета высоты.  В нашем случае изображение и фильтрация симметричны, поэтому формула применима и к ширине и к высоте. Если хотим оставить входные и выходные измерения без изменений с размером фильтра 5 и шагом 1, то исходя из верхней формулы паддинг нужно задать равным 2. Таким образом, аргумент для паддинга в Conv2d равен 2.

Следующий аргумент в последовательности — простая ReLU функция активации. Последний элемент в последовательном определении для self.layer1 — операция max pooling. Первый аргумент — размер объединения, который равен 2х2; следовательно, аргумент равен 2. Во втором аргументе мы хотим взять подвыборку из данных через уменьшение эффективного размера изображения с факторов 2. Чтобы это сделать используя формулу выше, устанавливаем шаг равным 2 и паддинг равным 0. Следовательно, шаговый аргумент равен 2. Паддинг аргумент по умолчанию равен 0, если не указано другое значение, что сделано в коде сверху. Из этих вычислений теперь понятно, что выход слоя self.layer1 имеет 32 канала и “изображения” размером 14х14.

Второй слой, self.layer2, определяется таким же образом, как и первый. Единственное отличие здесь — вход в функцию Conv2d теперь 32 канальный, а выход — 64 канальный. Следуя такой же логике и учитывая пулинг и даунсемплинг, выход из self.layer2 представляет из себя 64 канала изображения размера 7х7.

Далее определяем отсеивающий слой для предотвращения переобучения модели. В конце создаем два полносвязных слоя. Первый слой имеет размер 7х7х64 узла и соединяется со вторым слоем с 1000 узлами. Чтобы создать полносвязный слой в PyTorch, используем метод nn.Linear. Первый аргумент метода — число узлов в данном слое, второй аргумент — число узлов в следующем слое.

Определение слоев создано при помощи  _init_. Следующий шаг — определить потоки данных через слои при прямом прохождении через сеть:

def forward(self, x): 
     out = self.layer1(x) 
     out = self.layer2(out) 
     out = out.reshape(out.size(0), -1) 
     out = self.drop_out(out) 
     out = self.fc1(out) 
     out = self.fc2(out) 
     return out

Здесь важно назвать функцию “forward”, так как она будет использовать базовую функцию прямого распространения в nn.Module и позволит всему функционалу nn.Module работать корректно. Как можно видеть, функция принимает на вход аргумент х, представляющий собой данные, которые должны проходить через модель (например, партия данных). Направляем эти данные в первый слой (self.layer1) и возвращаем выходные данные как out. Эти выходные данные поступают на следующий слой и так далее. Отметим, после self.layer2 применяем функцию преобразования формы к out, которая разглаживает размеры данных с 7х7х64 до 3164х1. После двух полносвязных слоев применяется dropout-слой и из функции возвращается финальное предсказание.

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

Обучение модели

Перед тренировкой модели мы должны сначала создать экземпляр (instance) нашего класса ConvNet(), определить функцию потерь и оптимизатор:

model = ConvNet()
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)

Экземпляр класса ConvNet() создается под названием  model. Далее определяем операцию над потерями, которая будет использоваться для подсчета потерь. В нашем случае используем доступную в PyTorch функцию CrossEntropyLoss(). Вы уже могли заметить, что мы еще не задали активационную функцию SoftMax для последнего слоя, выполняющего классификацию. Это сделано потому, что функция  CrossEntropyLoss() объединяет и SoftMax и кросс-энтропийную функцию потерь в единую функцию. Далее определяем оптимизатор Adam. Первым аргументом функции являются параметры, которые мы хотим обучить оптимизатором. Это легко сделать с помощью класса nn.Module, из которого вы получается ConvNet. Всё что нужно сделать — снабдить функцию аргументом model.parameters() и PyTorch будет отслеживать все параметры нашей модели, которые необходимо обучить. Последним определяется скорость обучения.

Тренировочный цикл выглядит следующим образом:

total_step = len(train_loader)
loss_list = []
acc_list = []
for epoch in range(num_epochs):
    for i, (images, labels) in enumerate(train_loader):
        # Прямой запуск
        outputs = model(images)
        loss = criterion(outputs, labels)
        loss_list.append(loss.item())

        # Обратное распространение и оптимизатор
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

        # Отслеживание точности
        total = labels.size(0)
        _, predicted = torch.max(outputs.data, 1)
        correct = (predicted == labels).sum().item()
        acc_list.append(correct / total)

        if (i + 1) % 100 == 0:
            print('Epoch [{}/{}], Step [{}/{}], Loss: {:.4f}, Accuracy: {:.2f}%'
                  .format(epoch + 1, num_epochs, i + 1, total_step, loss.item(),
                          (correct / total) * 100))

Самыми важными частями для начала являются два цикла. Первый цикл проходит по количеству эпох, в нём итерации проходят по train_loader используя перечисление. Во внутреннем цикле сначала считаются выходы прямого прохождения изображений (которые представляют собой партии нормализованных MNIST изображений из train_loader). Отметим, нам не требуется вызывать model.forward(images), так как nn.Module знает, что нужно вызывать forward при выполнении model(images).

Следующий шаг — отправление выходов модели и настоящих меток в функцию CrossEntropyLoss, определенную как criterion. Потери добавляются в список, который будет использован позже для отслеживания прогресса обучения. Затем — выполнение обратного распространения ошибки и оптимизированного шага тренировки. Сначала градиенты должны быть обнулены, что легко делается вызовом zero_grad() на оптимизаторе. Далее вызываем функцию .backward() на переменной loss для выполнения обратного распространения. Теперь, когда градиенты посчитаны при обратном распространении, просто вызываем optimizer.step() для выполнения шага обучения оптимизатора Adam. PyTorch делает процесс обучения модели очень легким и интуитивным.

Следующий набор строк отвечает за отслеживание точности на тренировочном сете. Предсказания модели могут быть определены с помощью функции torch.max(), которая возвращает индекс максимального значения в тензоре. Первый аргумент этой функции — тензор, который мы хотим исследовать; второй — ось, по которой определяется максимум. Выходящий из модели тензор должен иметь размер (batch_size,10). Чтобы определить предсказание модели, необходимо для каждого примера в партии найти максимальные значения из 10 выходных узлов. Каждый из этих узлов будет соответствовать одному рукописному символу (например, выход 2 равен символу «2» и так далее). Выходной узел с наибольшим значением и будет предсказанием модели. Следовательно, надо задать второй аргумент в функции torch.max() равным 1, что указывает функции максимума проверить ось выходного узла (axis = 0 соответствует размерности batch_size).

На этом шаге возвращается список целочисленных предсказаний модели, а следующая строка сравнивает эти предсказания с настоящими метками (predicted == labels) и суммирует правильные предсказания. Отметим, что на этом шаге выход функции sum() всё еще тензор, поэтому для оценки его значений требуется вызвать .item(). Делим количество правильных предсказаний на размер партии batch_size (эквивалентно labels.size(0)) для подсчета точности. Наконец, во время тренировки после каждых 100 итераций внутреннего цикла выводим прогресс.

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

Epoch [1/6], Step [100/600], Loss: 0.2183, Accuracy: 95.00%
Epoch [1/6], Step [200/600], Loss: 0.1637, Accuracy: 95.00%
Epoch [1/6], Step [300/600], Loss: 0.0848, Accuracy: 98.00%
Epoch [1/6], Step [400/600], Loss: 0.1241, Accuracy: 97.00%
Epoch [1/6], Step [500/600], Loss: 0.2433, Accuracy: 95.00%
Epoch [1/6], Step [600/600], Loss: 0.0473, Accuracy: 98.00%
Epoch [2/6], Step [100/600], Loss: 0.1195, Accuracy: 97.00%

Теперь напишем код для определения точности на тестовом наборе.

Тестирование

Для тестирования модели используем следующий код:

model.eval()
with torch.no_grad():
    correct = 0
    total = 0
    for images, labels in test_loader:
        outputs = model(images)
        _, predicted = torch.max(outputs.data, 1)
        total += labels.size(0)
        correct += (predicted == labels).sum().item()

    print('Test Accuracy of the model on the 10000 test images: {} %'.format((correct / total) * 100))

# Сохраняем модель и строим график
torch.save(model.state_dict(), MODEL_STORE_PATH + 'conv_net_model.ckpt')

В первой строке устанавливаем режим оценки используя model.eval(). Это удобная функция запрещает любые исключения или слои нормализации партии в модели, которые будут мешать объективной оценке. Конструкция torch.no_grad() выключает функцию автоградиента в модели, так как она не нужна при тестировании/оценке модели и замедляет вычисления. Остальная часть кода совпадает с вычислением точности во время тренировки, за исключением того, что в этом случае итерации проходят по test_loader.

Наконец, результат выводится в консоль, а модель сохраняется при помощи функции torch.save().

В последней части кода в этом репозитории на GitHub дополнительно построен график отслеживания точности и потерь, используя библиотеку для рисования Bokeh. Финальный результат выглядит так:

Точность на тестовой выборке на 10000 картинках составляет 99.03%

 

 

 

 

 

 

 

 

 

 

 

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

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

 


Интересные статьи:

Туториал по PyTorch: от установки до готовой нейронной сети

22 октября 2018
pytorch туториал

Туториал по PyTorch: от установки до готовой нейронной сети

Если вы уже пробовали создавать собственные глубокие нейронные сети с помощью TensorFlow и Keras, то, вероятно, знакомы с чувством разочарования при отлаживании этих библиотек. Хотя они имеют API на Python,…

Если вы уже пробовали создавать собственные глубокие нейронные сети с помощью TensorFlow и Keras, то, вероятно, знакомы с чувством разочарования при отлаживании этих библиотек. Хотя они имеют API на Python, всё еще трудно выяснить, что именно пошло не так при ошибке. Они также плохо работают вместе с библиотеками numpy, scipy, scikit-learn, Cython и другими. Библиотека глубокого обучения PyTorch имеет заявленное преимущество — хорошо работает с Python и создана для апологетов Python. Кроме того, приятное свойство PyTorch — построение вычислительного динамического графа, противоположно статическим вычислительным графам, представленным в TensorFlow и Keras. PyTorch сейчас находится на подъеме и используется в разработке Facebook, Twitter, NVIDIA и другими компаниями. Давайте обратимся к туториалу по использованию PyTorch.

цукерберг pytorch facebook

Перед вами перевод статьи «A PyTorch tutorial – deep learning in Python», ссылка на оригинал — в подвале статьи.

Первый вопрос для рассмотрения — действительно ли PyTorch лучше TensorFlow? Это субъективно, так как с точки зрения производительности нет больших различий. В любом случае, PyTorch стал серьезным соперником в соревновании между библиотеками глубокого обучения. Давайте начнем изучать библиотеку, оставив для размышлений вопрос о том, что же лучше.

Основы PyTorch

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

Полносвязная нейронная сеть
Полносвязная нейронная сеть

Установка на Windows

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

Вычислительные графы

Первое, что необходимо понять о любой библиотеке глубокого обучения — идея вычислительных графов. Вычислительный граф — набор вычислений, которые называются узлами, и которые соединены в прямом порядке вычислений. Другими словами, выбранный узел зависит от узлов на входе, который в свою очередь производит вычисления для других узлов. Ниже представлен простой пример вычислительного графа для вычисления выражения a = (b + c) * (c + 2). Можно разбить вычисление на следующие шаги:

Вычислительный графы
Простой вычислительный граф

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

Тензоры

Тензоры — подобные матрице структуры данных, которые являются неотъемлемыми компонентами в библиотеках глубокого обучения и используются для эффективных вычислений. Графические процессоры (GPU) эффективны при вычислении операций между тензорами, что стимулировало волну возможностей в глубоком обучении. В PyTorch тензоры могут определяться несколькими способами:

import torch
x = torch.Tensor(2, 3)

Этот код создает тензор размера (2,3), заполненный нулями. В данном примере первое число — количество рядов, второе — количество столбцов:

0 0 0
0 0 0
[torch.FloatTensor of size 2x3]

Мы также можем создать тензор, заполненный случайными float-значениями:

x = torch.rand(2, 3)

Умножение тензоров, сложение друг с другом и другие алгебраические операции просты:

x = torch.ones(2,3)
y = torch.ones(2,3) * 2
x + y

Код возвращает:

3 3 3
3 3 3
[torch.FloatTensor of size 2x3]

Также доступна работа с функцией slice в numpy. Например y[:,1]:

y[:,1] = y[:,1] + 1

Которая возвращает:

2 3 2
2 3 2
[torch.FloatTensor of size 2x3]

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

Автоматическое дифференцирование в PyTorch

В библиотеках глубокого обучения есть механизмы вычисления градиента ошибки и обратного распространения ошибки через вычислительный граф. Этот механизм, называемый автоградиентом в PyTorch, легко доступен и интуитивно понятен. Переменный класс — главный компонент автоградиентной системы в PyTorch. Переменный класс обертывает тензор и позволяет автоматически вычислять градиент на тензоре при вызове функции .backward(). Объект содержит данные из тензора, градиент тензора (единожды посчитанный по отношению к некоторому другому значению, потеря) и содержит также ссылку на любую функцию, созданную переменной (если это функция созданная пользователем, ссылка будет пустой).

Создадим переменную из простого тензора:

x = Variable(torch.ones(2, 2) * 2, requires_grad=True)

В объявлении переменной используется двойной тензор размера 2х2 и дополнительно указывается, что переменной необходим градиент. При использовании этой переменной в нейронных сетях, она становится способна к обучению. Если последний параметр будет равен False, то переменная не может использоваться для обучения. В этом простом примере мы ничего не будем тренировать, но хотим запросить градиент для этой переменной, как будет показано ниже.

Далее, давайте создадим новую переменную на основе x.

z = 2 * (x * x) + 5 * x

Чтобы вычислить градиент этой операции по x, dz/dx, можно аналитически получить 4x + 5. Если все элементы x — двойки, то градиент dz/dx — тензор размерности (2,2), заполненный числами 13. Однако, сначала необходимо запустить операцию обратного распространения .backwards(), чтобы вычислить градиент относительно чего-либо. В нашем случае инициализируется единичный тензор (2,2), относительно которого считаем градиент. В таком случаем вычисление — просто операция d/dx:

z.backward(torch.ones(2, 2))
print(x.grad)

Результатом кода является следующее:

Variable containing:
13 13
13 13
[torch.FloatTensor of size 2x2]

Заметим, это в точности то, что мы предсказывали вначале. Отметим, градиент хранится в переменной x в свойстве .grad.

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

Создание нейронной сети в PyTorch

Этот раздел — основной в туториале. Полный код туториала лежит в этом репозитории на GitHub. Здесь мы создадим простую нейронную сеть с 4 слоями, включая входной и два скрытых слоя, для классификации рукописных символов в датасете MNIST. Архитектура, которую мы будем использовать, показана на картинке:

Архитектура полносвязной нейронной сети
Архитектура полносвязной нейронной сети

Входной слой состоит из 28 х 28 = 784 пикселей с оттенками серого, которые составляют входные данные в датасете MNIST. Входные данные далее проходят через два скрытых слоя, каждый из которых содержит 200 узлов, использующих линейную выпрямительную функцию активации (ReLU). Наконец, мы имеем выходной слой с десятью узлами, соответствующими десяти рукописным цифрам от 0 до 9. Для такой задачи классификации будем использовать выходной softmax-слой.

Класс для построения нейронной сети

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

import torch.nn as nn
import torch.nn.functional as F

class Net(nn.Module):
   def __init__(self):
       super(Net, self).__init__()
       self.fc1 = nn.Linear(28 * 28, 200)
       self.fc2 = nn.Linear(200, 200)
       self.fc3 = nn.Linear(200, 10)

В таком определении можно видеть наследование базового класса nn.Module. В первой строке инициализации класса def __init__(self)  мы имеем требуемую super() функцию языка Python, которая создает объект базового класса. В следующих трех строках создаем полностью соединенные слои как показано на диаграмме архитектуры. Полностью соединенный слой нейронной сети представлен объектом nn.Linear, в котором первый аргумент — определение количества узлов в i-том слое, а второй — количество узлов в i+1 слое. Из кода видно, первый слой принимает на входе 28×28 пикселей и соединяется с первым скрытым слоем с 200 узлами. Далее идет соединение с другим скрытым слоем с 200 узлами. И, наконец, соединение последнего скрытого слоя с выходным слоем с 10 узлами.

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

def forward(self, x):
   x = F.relu(self.fc1(x))
   x = F.relu(self.fc2(x))
   x = self.fc3(x)
   return F.log_softmax(x)

Для метода forward() берем входные данные x в качестве основного аргумента. Далее, загружаем всё в в первый полностью соединенный слой self.fc1(x) и применяем активационную функцию ReLU для узлов в этом слое, используя F.relu(). Из-за иерархической природы этой нейронной сети, заменяем x на каждой стадии и отправляем на следующий слой. Делаем эту процедуру на трех соединенных слоях, за исключением последнего. На последнем слое возвращаем не ReLU, а логарифмическую softmax активационную функцию. Это, в комбинации с функцией потери отрицательного логарифмического правдоподобия, дает многоклассовую на основе кросс-энтропии функцию потерь, которую мы будет использовать для тренировки сети.

Мы определили нейронную сеть. Следующим шагом будет создание экземпляра (instance) этой архитектуры:

net = Net()
print(net)

При выводе экземпляра класса Net получаем следующее:

Net (
(fc1): Linear (784 -> 200)
(fc2): Linear (200 -> 200)
(fc3): Linear (200 -> 10)
)

Что очень удобно, так как подтверждает структуру нашей нейронной сети.

Тренировка сети

Далее необходимо задать метод оптимизации и критерий качества:

# Осуществляем оптимизацию путем стохастического градиентного спуска
optimizer = optim.SGD(net.parameters(), lr=learning_rate, momentum=0.9)
# Создаем функцию потерь
criterion = nn.NLLLoss()

В первой строке создаем оптимизатор на основе стохастического градиентного спуска,  устанавливая скорость обучения (learning rate; в нашем случае определим этот показатель на уровне 0.01) и momentum. Еще в оптимизаторе необходимо определить все остальные параметры сети, но это делается легко в PyTorch благодаря методу .parameters() в базовом классе nn.Module, который наследуется из него в новый класс Net.

Далее устанавливается метрика контроля качества — функция потерь отрицательного логарифмического правдоподобия. Такой вид функции в комбинации с логарифмической softmax-функцией на выходе нейронной сети дает эквивалентную кросс-энтропийную потерю для 10 классов задачи классификации.

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

# запускаем главный тренировочный цикл
for epoch in range(epochs):
   for batch_idx, (data, target) in enumerate(train_loader):
       data, target = Variable(data), Variable(target)
# изменим размер с (batch_size, 1, 28, 28) на (batch_size, 28*28)
       data = data.view(-1, 28*28)
       optimizer.zero_grad()
       net_out = net(data)
       loss = criterion(net_out, target)
       loss.backward()
       optimizer.step()
       if batch_idx % log_interval == 0:
           print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format(
                   epoch, batch_idx * len(data), len(train_loader.dataset),
                          100. * batch_idx / len(train_loader), loss.data[0]))

Внешний тренировочный цикл проходит по количеству эпох, а внутренний тренировочный цикл проходит через все тренировочные данные в партиях, размер которых задается в коде как batch_size. На следующей линии конвертируем данные и целевую переменную в переменные PyTorch. Входной датасет MNIST, который находится в пакете torchvision (который вам необходимо установить при помощи pip), имеет размер (batch_size, 1, 28, 28) при извлечении из загрузчика данных. Такой четырехмерный тензор больше подходит для архитектуры сверточной нейронной сети, чем для нашей полностью соединенной сети. Тем не менее, необходимо уменьшить размерность данных с (1,28,28)  до одномерного случая для 28 х 28 = 784 входных узла.

Функция .view() работает с переменными PyTorch и преобразует их форму. Если мы точно не знаем размерность данного измерения, можно использовать ‘-1’ нотацию в определении размера. Поэтому при использование data.view(-1,28*28) можно сказать, что второе измерение должно быть равно 28 x 28, а первое измерение должно быть вычислено из размера переменной оригинальных данных. На практике это означает, что данные теперь будут размера (batch_size, 784). Мы можем пропустить эту партию входных данных в нашу нейросеть, и магический PyTorch сделает за нас тяжелую работу, эффективно выполняя необходимые вычисления с тензорами.

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

net_out = net(data)
loss = criterion(net_out, target)

Первая строка, в которой подаем порцию данных на вход нашей модели, вызывает метод forward() в классе Net. После запуска строки переменная net_out будет иметь логарифмический softmax-выход из нашей нейронной сети для заданной партии данных.  Это одна из самых замечательных особенностей PyTorch, так как можно активировать любой стандартный отладчик Python, который вы обычно используете, и мгновенно узнать, что происходит в нейронной сети. Это противоположно другим библиотекам глубокого обучения, TensorFlow и Keras, в которых требуется  производить сложные отладочные действия, чтобы узнать, что ваша нейронная сеть действительно создает. Надеюсь, вы поиграете с кодом для этого туториала и поймете, насколько в PyTorch удобный отладчик.

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

Давайте посмотрим на следующие две строки:

loss.backward()
optimizer.step()

Первая строка запускает операцию обратного распространения ошибки из переменной потери в обратном направлении через нейросеть. Если сравнить это с упомянутой выше операцией .backward(), которую мы рассматривали в туториале, видно, что не используется никакой аргумент в операции .backward(). Скалярные переменные при использовании на них .backward() не требуют аргумента; только тензорам необходим согласованный аргумент для передачи в операцию .backward().

В следующей строке мы просим PyTorch выполнить градиентный спуск по шагам на основе вычисленных во время операции .backward() градиентов.

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

if batch_idx % log_interval == 0:
   print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format(
                   epoch, batch_idx * len(data), len(train_loader.dataset),
                          100. * batch_idx / len(train_loader), loss.data[0]))

Эта функция выводит наш прогресс на протяжении эпох тренировки и показывает ошибку нейросети в этот момент. Отметим, что доступ к потерям находится в свойстве .data у переменной PyTorch, которая в данном случае будет массивом с единственным значением. Получаем скалярную потерю используя loss.data[0].

Запуская этот тренировочный цикл, получаем на выходе следующее:

Train Epoch: 9 [52000/60000 (87%)] Loss: 0.015086

Train Epoch: 9 [52000/60000 (87%)] Loss: 0.015086

Train Epoch: 9 [54000/60000 (90%)] Loss: 0.030631

Train Epoch: 9 [56000/60000 (93%)] Loss: 0.052631

Train Epoch: 9 [58000/60000 (97%)] Loss: 0.052678

После 10 эпох, значение потери по величине должно получиться меньше 0.05.

Тестирование сети

Чтобы проверить нашу обученную нейронную сеть на тестовом датасете MNIST, запустим следующий код:

test_loss = 0
correct = 0
for data, target in test_loader:
   data, target = Variable(data, volatile=True), Variable(target)
   data = data.view(-1, 28 * 28)
   net_out = net(data)
# Суммируем потери со всех партий
   test_loss += criterion(net_out, target).data[0]
   pred = net_out.data.max(1)[1]  # получаем индекс максимального значения
   correct += pred.eq(target.data).sum()

test_loss /= len(test_loader.dataset)
print('\nTest set: Average loss: {:.4f}, Accuracy: {}/{} ({:.0f}%)\n'.format(
       test_loss, correct, len(test_loader.dataset),
       100. * correct / len(test_loader.dataset)))

Этот цикл совпадает с тренировочным циклом до строки test_loss. Здесь мы извлекаем потери сети используя свойство .data[0] как и раньше, но только все в одной строке. Далее в строке pred используется метод data.max(1), который возвращает индекс наибольшего значения в определенном измерении тензора. Теперь выход нашей нейронной сети будет иметь размер (batch_size, 10), где каждое значение из второго измерения длины 10 — логарифмическая вероятность, которую нейросеть приписывает каждому выходному классу (то есть это логарифмическая вероятность принадлежности картинки к символу от 0 до 9). Поэтому для каждого входного образца в партии net_out.data будет выглядеть следующим образом:

[-1.3106e+01, -1.6731e+01, -1.1728e+01, -1.1995e+01, -1.5886e+01, -1.7700e+01, -2.4950e+01, -5.9817e-04, -1.3334e+01, -7.4527e+00]

Значение с наибольшей логарифмической вероятностью — цифра от 0 до 9, которую нейронная сеть распознает на входной картинке. Иначе говоря, это лучшее предсказание для заданного входного объекта. В примере net_out.data таким лучшим предсказанием является значение -5.9817e-04, которое соответствует цифре “7”. Поэтому для этого примера нейросеть предскажет знак  “7”. Функция .max(1) определяет это максимальное значение во втором пространстве (если мы хотим найти максимум в первом пространстве, мы должны аргумент функции изменить с 1 на 0) и возвращает сразу и максимальное найденное значение, и индекс ему соответствующий. Поэтому эта конструкция имеет размер (batch_size, 2). В данном случае, нас интересует индекс максимального найденного значения, к которому мы получаем доступ с помощью вызова .max(1)[1].

Теперь у нас есть предсказание нейронной сети для каждого примера в определенной партии входных данных, и можно сравнить его с настоящей меткой класса из тренировочного датасета. Это используется для подсчета количества правильных ответов. Чтобы сделать это в PyTorch, необходимо воспользоваться функцией .eq(), которая сравнивает значения в двух тензорах и при совпадении возвращает единицу. В противном случае, функция возвращает 0:

correct += pred.eq(target.data).sum()

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

test_loss /= len(test_loader.dataset)
print('\nTest set: Average loss: {:.4f}, Accuracy: {}/{} ({:.0f}%)\n'.format(
       test_loss, correct, len(test_loader.dataset),
       100. * correct / len(test_loader.dataset)))

После тренировки сети за 10 эпох получаем следующие результаты на тестовой выборке:

Test set: Average loss: 0.0003, Accuracy: 9783/10000 (98%)

Мы получили точность 98%. Весьма неплохо!

В туториале рассмотрены базовые принципы PyTorch, начиная c тензоров до функции автоматического дифференцирования (autograd) и заканчивая пошаговым руководством, как создать полностью соединенную нейронную сеть при помощи nn.Module.


Интересные статьи:

Как создать нейронную сеть c библиотекой Keras на Python: пример

9 октября 2018
keras нейронаня сеть на python

Как создать нейронную сеть c библиотекой Keras на Python: пример

Keras — популярная библиотека глубокого обучения, которая внесла большой вклад в коммерциализацию глубокого обучения. Библиотека Keras проста в использовании и позволяет создавать нейронные сети с помощью лишь нескольких строк кода…

Keras — популярная библиотека глубокого обучения, которая внесла большой вклад в коммерциализацию глубокого обучения. Библиотека Keras проста в использовании и позволяет создавать нейронные сети с помощью лишь нескольких строк кода Python.

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

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

Содержание:

  • Что такое Keras?
  • Что такое анализ настроений?
  • Датасет IMDB
  • Импорт зависимостей и получение данных
  • Изучение данных
  • Подготовка данных
  • Создание и обучение модели

Что такое Keras?

Keras — это библиотека для Python с открытым исходным кодом, которая позволяет легко создавать нейронные сети. Библиотека совместима с TensorFlow, Microsoft Cognitive Toolkit, Theano и MXNet. Tensorflow и Theano являются наиболее часто используемыми численными платформами на Python для разработки алгоритмов глубокого обучения, но они довольно сложны в использовании.

deep learning frameworks
Оценка популярности фреймворков машинного обучения по 7 категориям

Читайте: TensorFlow туториал. Часть 1: тензоры и векторы

Keras, наоборот, предоставляет простой и удобный способ создания моделей глубокого обучения. Ее создатель, François Chollet, разработал ее для того, чтобы максимально ускорить и упростить процесс создания нейронных сетей. Он сосредоточил свое внимание на расширяемости, модульности, минимализме и поддержке Python. Keras можно использовать с GPU и CPU; она поддерживает как Python 2, так и Python 3. Keras компании Google внесла большой вклад в коммерциализацию глубокого обучения и искусственного интеллекта, поскольку она содержит cовременные алгоритмы глубокого обучения, которые ранее были не только недоступными, но и непригодными для использования.

Что такое анализ настроений (сентимент-анализ)?

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

keras нейронная сеть анализ настроений
Пример шкалы анализа настроений

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

Датасет IMDb

imdb reviews sentiment
Рецензии на сайте IMDb

Датасет IMDb состоит из 50 000 обзоров фильмов от пользователей, помеченных как положительные (1) и отрицательные (0).

  • Рецензии предварительно обрабатываются, и каждая из них кодируется последовательностью индексов слов в виде целых чисел.
  • Слова в обзорах индексируются по их общей частоте появления в датасете. Например, целое число «2» кодирует второе наиболее частое используемое слово.
  • 50 000 обзоров разделены на два набора: 25 000 для обучения и 25 000 для тестирования.

Датасет был создан исследователями Стэнфордского университета и представлен в статье 2011 года, в котором достигнутая точность предсказаний была равна 88,89%. Датасет также использовался в рамках конкурса сообщества Keggle «Bag of Words Meets Bags of Popcorn» в 2011 году.

Импорт зависимостей и получение данных

Начнем с импорта необходимых зависимостей для предварительной обработки данных и построения модели.

%matplotlib inline
import matplotlib
import matplotlib.pyplot as plt
import numpy as np
from keras.utils import to_categorical
from keras import models
from keras import layers

Загрузим датесет IMDb, который уже встроен в Keras. Поскольку мы не хотим иметь данные обучения и тестирования в пропорции 50/50, мы сразу же объединим эти данные после загрузки для последующего разделения в пропорции 80/20:

from keras.datasets import imdb
(training_data, training_targets), (testing_data, testing_targets) = imdb.load_data(num_words=10000)
data = np.concatenate((training_data, testing_data), axis=0)
targets = np.concatenate((training_targets, testing_targets), axis=0)

Изучение данных

Изучим наш датасет:

print("Categories:", np.unique(targets))
print("Number of unique words:", len(np.unique(np.hstack(data))))
Categories: [0 1]
Number of unique words: 9998
length = [len(i) for i in data]
print("Average Review length:", np.mean(length))
print("Standard Deviation:", round(np.std(length)))
Average Review length: 234.75892
Standard Deviation: 173.0

Можно видеть, что все данные относятся к двум категориям: 0 или 1, что представляет собой настроение обзора. Весь датасет содержит 9998 уникальных слов, средний размер обзора составляет 234 слова со стандартным отклонением 173.

Рассмотрим простой способ обучения:

print("Label:", targets[0])
Label: 1
print(data[0])
[1, 14, 22, 16, 43, 530, 973, 1622, 1385, 65, 458, 4468, 66, 3941, 4, 173, 36, 256, 5, 25, 100, 43, 838, 112, 50, 670, 2, 9, 35, 480, 284, 5, 150, 4, 172, 112, 167, 2, 336, 385, 39, 4, 172, 4536, 1111, 17, 546, 38, 13, 447, 4, 192, 50, 16, 6, 147, 2025, 19, 14, 22, 4, 1920, 4613, 469, 4, 22, 71, 87, 12, 16, 43, 530, 38, 76, 15, 13, 1247, 4, 22, 17, 515, 17, 12, 16, 626, 18, 2, 5, 62, 386, 12, 8, 316, 8, 106, 5, 4, 2223, 5244, 16, 480, 66, 3785, 33, 4, 130, 12, 16, 38, 619, 5, 25, 124, 51, 36, 135, 48, 25, 1415, 33, 6, 22, 12, 215, 28, 77, 52, 5, 14, 407, 16, 82, 2, 8, 4, 107, 117, 5952, 15, 256, 4, 2, 7, 3766, 5, 723, 36, 71, 43, 530, 476, 26, 400, 317, 46, 7, 4, 2, 1029, 13, 104, 88, 4, 381, 15, 297, 98, 32, 2071, 56, 26, 141, 6, 194, 7486, 18, 4, 226, 22, 21, 134, 476, 26, 480, 5, 144, 30, 5535, 18, 51, 36, 28, 224, 92, 25, 104, 4, 226, 65, 16, 38, 1334, 88, 12, 16, 283, 5, 16, 4472, 113, 103, 32, 15, 16, 5345, 19, 178, 32]

Здесь вы видите первый обзор из датасета, который помечен как положительный (1). Нижеследующий код производит обратное преобразование индексов в слова, чтобы мы могли их прочесть. В нем каждое неизвестное слово заменяется на «#». Это делается с помощью функции get_word_index ().

index = imdb.get_word_index()
reverse_index = dict([(value, key) for (key, value) in index.items()]) 
decoded = " ".join( [reverse_index.get(i - 3, "#") for i in data[0]] )
print(decoded) 

# this film was just brilliant casting location scenery story direction everyone's really suited the part they played and you could just imagine being there robert # is an amazing actor and now the same being director # father came from the same scottish island as myself so i loved the fact there was a real connection with this film the witty remarks throughout the film were great it was just brilliant so much that i bought the film as soon as it was released for # and would recommend it to everyone to watch and the fly fishing was amazing really cried at the end it was so sad and you know what they say if you cry at a film it must have been good and this definitely was also # to the two little boy's that played the # of norman and paul they were just brilliant children are often left out of the # list i think because the stars that play them all grown up are such a big profile for the whole film but these children are amazing and should be praised for what they have done don't you think the whole story was so lovely because it was true and was someone's life after all that was shared with us all

Подготовка данных

Пришло время подготовить данные. Нужно векторизовать каждый обзор и заполнить его нулями, чтобы вектор содержал ровно 10 000 чисел. Это означает, что каждый обзор, который короче 10 000 слов, мы заполняем нулями. Это делается потому, что самый большой обзор имеет почти такой же размер, а каждый элемент входных данных нашей нейронной сети должен иметь одинаковый размер. Также нужно выполнить преобразование переменных в тип float.

def vectorize(sequences, dimension = 10000):
results = np.zeros((len(sequences), dimension))
for i, sequence in enumerate(sequences):
results[i, sequence] = 1
return results
 
data = vectorize(data)
targets = np.array(targets).astype("float32")

Разделим датасет на обучающий и тестировочный наборы. Обучающий набор будет состоять из 40 000 обзоров, тестировочный — из 10 000.

test_x = data[:10000]
test_y = targets[:10000]
train_x = data[10000:]
train_y = targets[10000:]

Создание и обучение модели

Теперь можно создать простую нейронную сеть. Начнем с определения типа модели, которую мы хотим создать. В Keras доступны два типа моделей: последовательные и с функциональным API.

Затем нужно добавить входные, скрытые и выходные слои. Для предотвращения переобучения будем использовать между ними исключение («dropout»). Обратите внимание, что вы всегда должны использовать коэффициент исключения в диапазоне от 20% до 50%. На каждом слое используется функция «dense» для полного соединения слоев друг с другом. В скрытых слоях будем используем функцию активации «relu», потому это практически всегда приводит к удовлетворительным результатам. Не бойтесь экспериментировать с другими функциями активации. На выходном слое используем сигмоидную функцию, которая выполняет перенормировку значений в диапазоне от 0 до 1. Обратите внимание, что мы устанавливаем размер входных элементов датасета равным 10 000, потому что наши обзоры имеют размер до 10 000 целых чисел. Входной слой принимает элементы с размером 10 000, а выдает — с размером 50.

Наконец, пусть Keras выведет краткое описание модели, которую мы только что создали.

# Input - Layer
model.add(layers.Dense(50, activation = "relu", input_shape=(10000, )))
# Hidden - Layers
model.add(layers.Dropout(0.3, noise_shape=None, seed=None))
model.add(layers.Dense(50, activation = "relu")
model.add(layers.Dropout(0.2, noise_shape=None, seed=None))
model.add(layers.Dense(50, activation = "relu"))
# Output- Layer
model.add(layers.Dense(1, activation = "sigmoid"))model.summary()
model.summary()

_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
dense_1 (Dense)              (None, 50)                500050    
_________________________________________________________________
dropout_1 (Dropout)          (None, 50)                0         
_________________________________________________________________
dense_2 (Dense)              (None, 50)                2550      
_________________________________________________________________
dropout_2 (Dropout)          (None, 50)                0         
_________________________________________________________________
dense_3 (Dense)              (None, 50)                2550      
_________________________________________________________________
dense_4 (Dense)              (None, 1)                 51        
=================================================================
Total params: 505,201
Trainable params: 505,201
Non-trainable params: 0
_________________________________________________________________

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

model.compile(
 optimizer = "adam",
 loss = "binary_crossentropy",
 metrics = ["accuracy"]
)

Теперь можно обучить нашу модель. Мы будем делать это с размером партии 500 и только двумя эпохами, поскольку я выяснил, что модель начинает переобучаться, если тренировать ее дольше. Размер партии определяет количество элементов, которые будут распространяться по сети, а эпоха — это один проход всех элементов датасета. Обычно больший размер партии приводит к более быстрому обучению, но не всегда — к быстрой сходимости. Меньший размер партии обучает медленнее, но может быстрее сходиться. Выбор того или иного варианта определенно зависит от типа решаемой задачи, и лучше попробовать каждый из них. Если вы новичок в этом вопросе, я бы посоветовал вам сначала использовать размер партии 32, что является своего рода стандартом.

results = model.fit(
 train_x, train_y,
 epochs= 2,
 batch_size = 500,
 validation_data = (test_x, test_y)
)

Train on 40000 samples, validate on 10000 samples
Epoch 1/2
40000/40000 [==============================] - 5s 129us/step - loss: 0.4051 - acc: 0.8212 - val_loss: 0.2635 - val_acc: 0.8945
Epoch 2/2
40000/40000 [==============================] - 4s 90us/step - loss: 0.2122 - acc: 0.9190 - val_loss: 0.2598 - val_acc: 0.8950

Проведем оценку работы модели:

print(np.mean(results.history["val_acc"]))
0.894750000536

Отлично! Наша простая модель уже побила рекорд точности в статье 2011 года, упомянутой в начале поста. Смело экспериментируйте с параметрами сети и количеством слоев.

Полный код модели приведен ниже:

import numpy as np
from keras.utils import to_categorical
from keras import models
from keras import layers
from keras.datasets import imdb
(training_data, training_targets), (testing_data, testing_targets) = imdb.load_data(num_words=10000)
data = np.concatenate((training_data, testing_data), axis=0)
targets = np.concatenate((training_targets, testing_targets), axis=0)
def vectorize(sequences, dimension = 10000):
 results = np.zeros((len(sequences), dimension))
 for i, sequence in enumerate(sequences):
  results[i, sequence] = 1
 return results
 
data = vectorize(data)
targets = np.array(targets).astype("float32")
test_x = data[:10000]
test_y = targets[:10000]
train_x = data[10000:]
train_y = targets[10000:]
model = models.Sequential()
# Input - Layer
model.add(layers.Dense(50, activation = "relu", input_shape=(10000, )))
# Hidden - Layers
model.add(layers.Dropout(0.3, noise_shape=None, seed=None))
model.add(layers.Dense(50, activation = "relu"))
model.add(layers.Dropout(0.2, noise_shape=None, seed=None))
model.add(layers.Dense(50, activation = "relu"))
# Output- Layer
model.add(layers.Dense(1, activation = "sigmoid"))
model.summary()
# compiling the model
model.compile(
 optimizer = "adam",
 loss = "binary_crossentropy",
 metrics = ["accuracy"]
)
results = model.fit(
 train_x, train_y,
 epochs= 2,
 batch_size = 500,
 validation_data = (test_x, test_y)
)
print("Test-Accuracy:", np.mean(results.history["val_acc"]))

Итоги

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

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

Эту модель (с небольшими изменениями) можно применить и для решения других задач машинного обучения.


Интересные статьи:

Как создать чат-бота с нуля на Python: подробная инструкция

4 октября 2018
как создать чат бота на python

Как создать чат-бота с нуля на Python: подробная инструкция

Аналитики Gartner утверждают, что к 2020 году 85% взаимодействий клиентов с сервисами сведется к общению с чат-ботами. В 2018 году они уже обрабатывают около 30% операций. В этой статье мы расскажем, как создать…

Аналитики Gartner утверждают, что к 2020 году 85% взаимодействий клиентов с сервисами сведется к общению с чат-ботами. В 2018 году они уже обрабатывают около 30% операций. В этой статье мы расскажем, как создать своего чат-бота на Python.

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

Несмотря на то что Duolingo позволяет изучить новый язык, у пользователей сервиса возникла проблема. Они почувствовали, что не развивают разговорные навыки, так как обучаются самостоятельно. Пользователи неохотно обучались в парах из-за смущения. Эта проблема не осталась незамеченной для разработчиков.

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

чат-бот пример

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

Итак, что такое чат-бот?

Чат-бот — это программа, которая выясняет потребности пользователей, а затем помогает удовлетворить их (денежная транзакция, бронирование отелей, составление документов). Сегодня почти каждая компания имеет чат-бота для взаимодействия с пользователями. Некоторые способы использования чат-ботов:

  • предоставление информации о рейсе;
  • предоставление пользователям доступа к информации об их финансах;
  • служба поддержки.

Возможности безграничны.

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

Как работает чат-бот?

Существует два типа ботов: работающие по правилам и самообучающиеся.

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

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

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

В этой статье мы научимся писать код простых поисковых чат-ботов на основе библиотеки NLTK.

Создание бота на Python

Предполагается, что вы умеете пользоваться библиотеками scikit и NLTK. Однако, если вы новичок в обработке естественного языка (NLP), вы все равно можете прочитать статью, а затем изучить соответствующую литературу.

Обработка естественного языка (NLP)

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

Краткое введение в NLKT

NLTK (Natural Language Toolkit) — платформа для создания программ на Python для работы с естественной речью. NLKT предоставляет простые в использовании интерфейсы для более чем 50 корпораций и лингвистических ресурсов, таких как WordNet, а также набор библиотек для обработки текста в целях классификации, токенизации, генерации, тегирования, синтаксического анализа и понимания семантики, создания оболочки библиотек NLP для коммерческого применения.

Книга Natural Language Processing with Python  — практическое введение в программирование для обработки языка. Рекомендуем ее прочитать, если вы владеете английским языком.

Загрузка и установка NLTK

  • Установите NLTK: запустите pip install nltk.
  • Тестовая установка: запустите python, затем введите import nltk.

Инструкции для конкретных платформ смотрите здесь.

Установка пакетов NLTK

Импортируйте NLTK и запустите nltk.download(). Это откроет загрузчик NLTK, где вы сможете выбрать версию кода и модели для загрузки. Вы также можете загрузить все пакеты сразу.

Предварительная обработка текста с помощью NLTK

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

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

Пакет NLTK включает в себя предварительно обученный токенизатор Punkt для английского языка.

  • Удаление шума, то есть всего, что не является цифрой или буквой;
  • Удаление стоп-слов. Иногда из словаря полностью исключаются некоторые крайне распространенные слова, которые, как считается, не имеют большого значения для формирования ответа на вопрос пользователя. Эти слова называются стоп-словами (междометия, артикли, некоторые вводные слова);
  • Cтемминг: приведение слова к коренному значению. Например, если нам нужно провести стемминг слов «стемы», «стемминг», «стемированный» и «стемизация», результатом будет одно слово — «стем».
  • Лемматизация. Лемматизация — немного отличающийся от стемминга метод. Основное различие между ними заключается в том, что стемминг часто создает несуществующие слова, тогда как лемма — это реально существующее слово. Таким образом, ваш исходный стем, то есть слово, которое получается после стемминга, не всегда можно найти в словаре, а лемму — можно. Пример лемматизации: «run» — основа для слов «running» или «ran», а «better» и «good» находятся в одной и той же лемме и потому считаются одинаковыми.

Набор слов

После первого этапа предварительной обработки нужно преобразовать текст в вектор (или массив) чисел. «Набор слов» — это представление текста, описывающего наличие слов в тексте. «Набор слов» состоит из:

  • словаря известных слов;
  • частот, с которыми каждое слово встречается в тексте.

Почему используется слово «набор»? Это связано с тем, что информация о порядке или структуре слов в тексте отбрасывается, и модель учитывает только то, как часто определенные слова встречаются в тексте, но не то, где именно они находятся.

Идея «набора слов» состоит в том, что тексты похожи по содержанию, если включают в себя похожие слова. Кроме того, кое-что узнать о содержании текста можно лишь по набору слов.

Например, если словарь содержит слова {Learning, is, the, not, great} и мы хотим составить вектор предложения “Learning is great”, получится вектор (1, 1, 0, 0, 1).

Метод TF-IDF

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

Один из подходов к решению этих проблем состоит в том, чтобы вычислять частоту появления слова не в одном тексте, а во всех сразу. За счет этого вклад, например, артиклей «a» и «the» будет нивелирован. Такой подход называется TF-IDF (Term Frequency-Inverse Document Frequency) и состоит из двух этапов:

  • TF — вычисление частоты появления слова в одном тексте
TF = (Число раз, когда слово "t" встречается в тексте)/(Количество слов в тексте)
  • IDF — вычисление того, на сколько редко слово встречается во всех текстах
IDF = 1+log(N/n), где N - общее количество текстов, n - во скольких текстах встречается "t"

Коэффициент TF-IDF — это вес, часто используемый для обработки информации и интеллектуального анализа текста. Он является статистической мерой, используемой для оценки важности слова для текста в некотором наборе текстов.

Пример

Рассмотрим текст, содержащий 100 слов, в котором слово «телефон» появляется 5 раз. Параметр TF для слова «телефон» равен (5/100) = 0,05.

Теперь предположим, что у нас 10 миллионов документов, и слово телефон появляется в тысяче из них. Коэффициент вычисляется как 1+log(10 000 000/1000) = 4. Таким образом, TD-IDF равен 0,05 * 4 = 0,20.

TF-IDF может быть реализован в scikit так:

from sklearn.feature_extraction.text import TfidfVectorizer

Коэффициент Отиаи

TF-IDF — это преобразование, применяемое к текстам для получения двух вещественных векторов в векторном пространстве. Тогда мы можем получить коэффициент Отиаи любой пары векторов, вычислив их поэлементное произведение и разделив его на произведение их норм. Таким образом, получается косинус угла между векторами. Коэффициент Отиаи является мерой сходства между двумя ненулевыми векторами. Используя эту формулу, можно вычислить схожесть между любыми двумя текстами d1 и d2.

Cosine Similarity (d1, d2) =  Dot product(d1, d2) / ||d1|| * ||d2||

Здесь d1, d2 — два ненулевых вектора.

Подробное объяснение и практический пример TF-IDF и коэффициента Отиаи приведены в посте по ссылке.

Пришло время перейти к решению нашей задачи, то есть созданию чат-бота. Назовем его «ROBO».


Обучение чат-бота

В нашем примере мы будем использовать страницу Википедии в качестве текста. Скопируйте содержимое страницы и поместите его в текстовый файл под названием «chatbot.txt». Можете сразу использовать другой текст.

Импорт необходимых библиотек

import nltk
import numpy as np
import random
import string # to process standard python strings

Чтение данных

Выполним чтение файла corpus.txt и преобразуем весь текст в список предложений и список слов для дальнейшей предварительной обработки.

f=open('chatbot.txt','r',errors = 'ignore')
raw=f.read()
raw=raw.lower()# converts to lowercase
nltk.download('punkt') # first-time use only
nltk.download('wordnet') # first-time use only
sent_tokens = nltk.sent_tokenize(raw)# converts to list of sentences 
word_tokens = nltk.word_tokenize(raw)# converts to list of words

Давайте рассмотрим пример файлов sent_tokens и word_tokens

sent_tokens[:2]
['a chatbot (also known as a talkbot, chatterbot, bot, im bot, interactive agent, or artificial conversational entity) is a computer program or an artificial intelligence which conducts a conversation via auditory or textual methods.',
 'such programs are often designed to convincingly simulate how a human would behave as a conversational partner, thereby passing the turing test.']
word_tokens[:2]
['a', 'chatbot', '(', 'also', 'known']

Предварительная обработка исходного текста

Теперь определим функцию LemTokens, которая примет в качестве входных параметров токены и выдаст нормированные токены.

lemmer = nltk.stem.WordNetLemmatizer()
#WordNet is a semantically-oriented dictionary of English included in NLTK.
def LemTokens(tokens):
    return [lemmer.lemmatize(token) for token in tokens]
remove_punct_dict = dict((ord(punct), None) for punct in string.punctuation)
def LemNormalize(text):
    return LemTokens(nltk.word_tokenize(text.lower().translate(remove_punct_dict)))

Подбор ключевых слов

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

GREETING_INPUTS = ("hello", "hi", "greetings", "sup", "what's up","hey",)
GREETING_RESPONSES = ["hi", "hey", "*nods*", "hi there", "hello", "I am glad! You are talking to me"]
def greeting(sentence):
 
    for word in sentence.split():
        if word.lower() in GREETING_INPUTS:
            return random.choice(GREETING_RESPONSES)

Генерация ответа

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

  • Импортируйте векторизатор TFidf из библиотеки, чтобы преобразовать набор необработанных текстов в матрицу свойств TF-IDF.
    from sklearn.feature_extraction.text import TfidfVectorizer
  • Кроме того, импортируйте модуль коэффициента Отиаи из библиотеки scikit
from sklearn.metrics.pairwise import cosine_similarity

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

Определим функцию отклика, которая возвращает один из нескольких возможных ответов. Если запрос не соответствует ни одному ключевому слову, бот выдает ответ «Извините! Я вас не понимаю».

def response(user_response):
    robo_response=''
TfidfVec = TfidfVectorizer(tokenizer=LemNormalize, stop_words='english')
    tfidf = TfidfVec.fit_transform(sent_tokens)
    vals = cosine_similarity(tfidf[-1], tfidf)
    idx=vals.argsort()[0][-2]
    flat = vals.flatten()
    flat.sort()
    req_tfidf = flat[-2]
    if(req_tfidf==0):
        robo_response=robo_response+"I am sorry! I don't understand you"
        return robo_response
    else:
        robo_response = robo_response+sent_tokens[idx]
        return robo_response

Наконец, мы задаем реплики бота в начале и конце переписки, в зависимости от реплик пользователя.

flag=True
print("ROBO: My name is Robo. I will answer your queries about Chatbots. If you want to exit, type Bye!")
while(flag==True):
    user_response = input()
    user_response=user_response.lower()
    if(user_response!='bye'):
        if(user_response=='thanks' or user_response=='thank you' ):
            flag=False
            print("ROBO: You are welcome..")
        else:
            if(greeting(user_response)!=None):
                print("ROBO: "+greeting(user_response))
            else:
                sent_tokens.append(user_response)
                word_tokens=word_tokens+nltk.word_tokenize(user_response)
                final_words=list(set(word_tokens))
                print("ROBO: ",end="")
                print(response(user_response))
                sent_tokens.remove(user_response)
    else:
        flag=False
        print("ROBO: Bye! take care..")

Вот и все. Мы написали код нашего первого бота в NLTK. Здесь вы можете найти весь код вместе с текстом. Теперь давайте посмотрим, как он взаимодействует с людьми:


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

Заключение

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


Интересные статьи:

TensorFlow туториал. Часть 3: работа с данными

26 сентября 2018
tensorflow анализ и работа с данными

TensorFlow туториал. Часть 3: работа с данными

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

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


Перед прочтением статьи рекомендуем изучить:


В этом разделе вы получите всю необходимую для продолжения изучения туториала информацию:

  • Текст на дорожных знаках в Бельгии обычно приведен на голландском и французском языках. Это полезно знать, но для датасета, с которым вы будете работать, это не слишком важно!
  • В Бельгии шесть категорий дорожных знаков: предупреждающие знаки, знаки приоритета, запрещающие знаки, предписывающие знаки, знаки, связанные с парковкой и стоянкой у дорог, и, наконец, обозначения.
  • 1 января 2017 года с бельгийских дорог было снято более 30 000 дорожных знаков. Это были запрещающие знаки, ограничивающие максимальную скорость вождения.
  • Снятие знаков связано с длительной дискуссией в Бельгии (и во всем Европейском Союзе) о чрезмерном количестве дорожных знаков.

Загрузка датасета

Теперь, когда вы знакомы со спецификой бельгийского трафика, пришло время скачать датасет. Вы должны скачать два zip-файла в разделе «BelgiumTS for Classification (cropped images)» с названиями «BelgiumTSC_Training» и «BelgiumTSC_Testing».

Совет. Если вы скачали файлы или сделаете это после прочтения туториала, взгляните на структуру папок, которые вы загрузили! Вы увидите, что папки тестирования и обучения содержат 61 подпапку, соответствующую 61 типам дорожных знаков, которые вы будете использовать для классификации в этом туториале. Кроме того, вы обнаружите, что файлы имеют расширение .ppm или Portable Pixmap Format. Вы скачали изображения дорожных знаков!

Давайте начнем с импорта данных в рабочую область. Начнем со строк кода, которые отображаются под пользовательской функцией load_data():

  • Сначала установите ROOT_PATH. Этот путь к каталогу, в котором находятся данные для обучения и тестирования.
  • Затем добавьте пути к вашему ROOT_PATH с помощью функции join(). Сохраните эту пути в train_data_directory и test_data_directory.
  • Вы увидите, что после этого можно вызвать функцию load_data() и передать в нее train_data_directory.
  • Теперь сама функция load_data() запускается путем сбора всех подкаталогов, присутствующих в train_data_directory; она делает это с помощью спискового включения, что является естественным способом составления списков — то есть, если вы найдете что-то в train_data_directory, вы проверяете, папка ли это и, если так и есть, добавляете ее в свой список. Помните, что каждый подкаталог представляет собой метку.
  • Затем вам нужно перебрать подкаталоги. Сначала вы инициализируете два списка, labels и images. Затем вы собираете пути подкаталогов и имена файлов изображений, которые хранятся в этих подкаталогах. После этого можно собрать данные в двух списках с помощью функции append().
def load_data(data_directory):

directories = [d for d in os.listdir(data_directory)

if os.path.isdir(os.path.join(data_directory, d))]

labels = []

images = [] for d in directories:

label_directory = os.path.join(data_directory, d)

file_names = [os.path.join(label_directory, f)

for f in os.listdir(label_directory)

if f.endswith(".ppm")]

for f in file_names:

images.append(skimage.data.imread(f))

labels.append(int(d))

return images, labels

ROOT_PATH = "/your/root/path"

train_data_directory = os.path.join(ROOT_PATH, "TrafficSigns/Training")

test_data_directory = os.path.join(ROOT_PATH, "TrafficSigns/Testing")

images, labels = load_data(train_data_directory)

Обратите внимание, что в приведенном выше блоке данные для обучения и тестирования находятся в папках с названиями «training» и «testing», которые являются подкаталогами другого каталога «TrafficSigns». На компьютере это может выглядеть примерно так: «/Users /Name /Downloads /TrafficSigns», а затем две подпапки под названием «training» и «testing».

Исследование данных

Когда данные загружены, пришло время для их исследования! Для начала можно провести элементарный анализ с помощью атрибутов ndim и size массива images:

Обратите внимание, что переменные images и labels являются списками, поэтому вам, возможно, придется воспользоваться np.array() для преобразования переменных в массив в рабочей области. Но здесь это уже сделано для вас!

# Вывести размерность 'images'
print(images.ndim)

# Вывести количество элементов в 'images'
print(images.size)

# Вывести первое значение 'images'
images[0]

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

Далее, обратимся к labels, но на этот раз никаких сюрпризов не будет:

# Вывести размерность 'labels'
print(labels.ndim)

# Вывести число элементов в 'labels'
print(labels.size)

# Вывести длину массива 'labels'
print(len(set(labels)))

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

Совет. Попробуйте добавить в свои массивы следующие атрибуты, чтобы получить дополнительную информацию о распределении памяти: длину одного элемента массива в байтах и ​​общее количество байтов, занимаемых элементами массива: flags, itemsize и nbytes.

Затем можно изучить распределение дорожных знаков по типам:

# Импортировать модуль 'pyplot'
import matplotlib.pyplot as plt

# Построить гистограмму с 64 точками - значениями 'labels'
plt.hist(labels, 62)

# Вывести график
plt.show()

Отлично! Давайте посмотрим на полученную гистограмму:

изучение данных в tensorflow

Хорошо видно, что не все типы дорожных знаков одинаково представлены в датасете. Это то, с чем вы столкнетесь позже, когда будете работать с данными, прежде чем приступать к моделированию нейронной сети. На первый взгляд видно: количество знаков типов 22, 32, 38 и 61 определенно преобладает над остальными. Запомните это, в следующем разделе вам пригодится эта информация.

Визуализация данных

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

Давайте посмотрим на несколько случайных знаков:

  • Во-первых, убедитесь, что вы импортируете модуль pyplot пакета matplotlib под общепринятым названием plt.
  • Затем создайте список из 4 случайных чисел. Они будут использоваться для выбора дорожных знаков из массива images, загруженного в прошлом разделе. В нижеследующем примере это будут числа 300, 2250, 3650 и 4000.
  • Затем для каждого элемента этого списка, от 0 до 4, вы создаете графики без осей (чтобы они не мешали сосредоточиться на изображениях). На этих графиках вы увидите конкретные изображения из массива images, которые соответствует номеру индекса i. На первом шаге цикла вы получите i = 300, во втором — 2250 и так далее. Наконец, нужно расположить графики так, чтобы между ними было достаточно пространства.
  • Последнее — выведите ваши графики с помощью функции show()!

Код:

# Импорт модуля 'pyplot' 'matplotlib`

import matplotlib.pyplot as plt

# Задание (случайных) номеров изображений, которые вы хотите вывести

traffic_signs = [300, 2250, 3650, 4000]

# Заполнение графиков изображениями

for i in range(len(traffic_signs)):

plt.subplot(1, 4, i+1)

plt.axis('off')

plt.imshow(images[traffic_signs[i]])

plt.subplots_adjust(wspace=0.5)

plt.show()

Как можно догадаться, знаки каждого из 62 типов отличаются друг от друга.

Но что еще можно заметить? Взглянем на изображения ниже:

работа с данными

Эти четыре изображения имеют разный размер!

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

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

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

# Импорт 'matplotlib'

import matplotlib.pyplot as plt

# Задание (случайных) номеров изображений, которые вы хотите вывести

traffic_signs = [300, 2250, 3650, 4000]

# Заполнение графиков изображениями и вывод размеров

for i in range(len(traffic_signs)):

plt.subplot(1, 4, i+1)

plt.axis('off')

plt.imshow(images[traffic_signs[i]])

plt.subplots_adjust(wspace=0.5)

plt.show()

print("shape: {0}, min: {1}, max: {2}".format(images[traffic_signs[i]].shape, images[traffic_signs[i]].min(), images[traffic_signs[i]].max()))

Обратите внимание на то, как вы используется метод format() в строке «shape: {0}, min: {1}, max: {2}», чтобы заполнить аргументы {0}, {1} и {2}.

визуализация данных tensorflow

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

# Импорт модуля 'pyplot' 'matplotlib'

import matplotlib.pyplot as plt

# Задание типов

unique_labels = set(labels)

# Инициализация графика

plt.figure(figsize=(15, 15))

# Задание счетчика

i = 1

# Для каждого типа:

for label in unique_labels:

# Выбирается первое изображение каждого типа:

image = images[labels.index(label)]

# Задание 64 графиков

plt.subplot(8, 8, i)

# Выключение осей

plt.axis('off')

# Добавление заголовка каждому графику

plt.title("Label {0} ({1})".format(label, labels.count(label)))

# Увеличить значение счетчика на 1

i += 1

# Вывод первого изображения

plt.imshow(image)

# Вывод всего графика

plt.show()

Обратите внимание, что даже если вы определяете 64 графика, не на всех из них будут изображения (так как есть всего 62 типа знаков!). Обратите также внимание на то, что опять же, вы не выводите оси, чтобы не отвлекаться на них.

анализ данных в Tensorflow

Как было видно из гистограммы, количество фотографий знаков с типами 22, 32, 38 и 61 значительно больше остальных. Эта видно из из графика выше: есть 375 снимков с меткой 22, 316 снимков с меткой 32, 285 снимков с меткой 38 и, наконец, 282 снимка с меткой 61.

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

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

Извлечение признаков

Теперь, когда вы тщательно изучили свои данные, пришло время засучить рукава! Давайте кратко отметим, что вы обнаружили, чтобы убедиться, что вы не забыли какие-либо моменты:

  • Изображения имеют разный размер;
  • Есть 62 метки (помним, что нумерация меток начинаются с 0 и заканчиваются на 61);
  • Распределение типов знаков трафика довольно неравномерно; между знаками, которые в большом количестве присутствовали в наборе данных, нет никакой связи.

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

Масштабирование изображений

Чтобы сделать размеры изображений одинаковыми, можно воспользоваться функцией skimage или библиотекой Scikit-Image, которая представляет собой набор алгоритмов для обработки изображений.

Во втором случае случае будет полезен модуль transform, так как в нем есть функция resize(); она снова использует списковое включение, чтобы сделать разрешение снимков равным 28×28 пикселей. Повторюсь: вы увидите, что фактически составляете список — для каждого изображения в массиве image вы выполните операцию преобразования, которую позаимствуете из библиотеки skimage. Наконец, вы сохраняете результат в переменной images28:

# Импорт модуля 'transform' из 'skimage'

import transform

# Масштабирование изображений в 'image'

array images28 = [transform.resize(image, (28, 28)) for image in images]

Выглядит очень просто, не так ли?

Обратите внимание, что изображения теперь четырехмерны: если вы конвертируете images28 в массив и привязываете атрибут shape, видно, что размеры imeges28 равны (4575, 28, 28, 3). Изображения 784-мерные (потому что ваши изображения имеют размер 28 на 28 пикселей).

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

Результат:

результат визуализации данных tensorflow

Обратите внимание, что, поскольку вы изменили масштаб, значения min и max также изменились; сейчас они все лежат в одном диапазоне, что действительно здорово, потому что теперь вам не нужно производить нормировку данных!

Преобразование изображений в оттенки серого

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

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

Как и при масштабировании, вы можете использовать библиотеку Scikit-Image; в этом случае вам понадобится модуль color с функцией rgb2gray().

Это будет просто!

Однако не забудьте преобразовать переменную images28 в массив, так как функция rgb2gray() в качестве аргумента принимает именно массивы.

# Импорт `rgb2gray` из`skimage.color`

from skimage.color import rgb2gray

# Конвертация `images28` в массив

images28 = np.array(images28)

# Конвертация `images28` в оттенки серого

images28 = rgb2gray(images28)

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

import matplotlib.pyplot as plt

traffic_signs = [300, 2250, 3650, 4000]

for i in range(len(traffic_signs)):

plt.subplot(1, 4, i+1)

plt.axis('off')

plt.imshow(images28[traffic_signs[i]], cmap="gray") plt.subplots_adjust(wspace=0.5)

# Вывод графика

plt.show()

Обратите внимание, что вам обязательно нужно указать цветовую карту или cmap и выставить значение ‘gray’ для вывода изображений в оттенках серого. Это связано с тем, что imshow() по умолчанию использует тепловую цветовую карту.


Совет. Поскольку этот блок используется в туториале несколько раз, будет полезно подумать, как можно сделать его функцией 🙂

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

В следующей частиглубокое обучение c TensorFlow.


Интересные статьи: