Разработан новый алгоритм классификации без отрицательных данных в датасете

29 ноября 2018
алгоритм классификации без отрицательных данных в датасете

Разработан новый алгоритм классификации без отрицательных данных в датасете

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

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

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

В новом подходе ученые работают только с положительными данными. Такой метод будет полезен в случае, если отрицательные данные для модели невозможно получить. Команда использует показатель доверия (Pconf), который математически высчитывает соответствие вероятности, относится ли результат к положительному классу.

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

Исследователи протестировали работу метода на датасетах MNIST и The CIFAR-10. Результаты Pconf сопоставимы с другими современными методами классификации, использующими отрицательные данные для обучения. Подход поможет улучшить работу моделей классификации и минимизировать ошибки в случаях, когда невозможно получить отрицательные данные для подготовки.

Градиентый бустинг — просто о сложном

27 ноября 2018
градиентный бустинг

Градиентый бустинг — просто о сложном

Хотя большинство победителей соревнований на Kaggle используют композицию разных моделей, одна из них заслуживает особого внимания, так как является почти обязательной частью. Речь, конечно, про Градиентный бустинг (GBM) и его…

Хотя большинство победителей соревнований на Kaggle используют композицию разных моделей, одна из них заслуживает особого внимания, так как является почти обязательной частью. Речь, конечно, про Градиентный бустинг (GBM) и его вариации. Возьмем, например. победителя Safe Driver Prediction, Michael Jahrer. Его решение — это комбинация шести моделей. Одна LightGBM (вариация GBM) и пять нейронных сетей. Хотя его успех в большей мере принадлежит полуконтролируемому обучению, которое он использовал для упорядочивания данных, градиентный бустинг сыграл свою роль.

Даже несмотря на то, что градиентный бустинг используется повсеместно, многие практики до сих пор относятся к нему, как к сложному алгоритму в черном ящике и просто запускают готовые модели из предустановленных библиотек. Цель этой статьи — дать понимание как же работает градиентный бустинг. Разбор будет посвящен чистому “vanilla” GMB.

Ансамбли, бэггинг и бустинг

Когда мы пытаемся предсказать целевую переменную с помощью любого алгоритма машинного обучения, главные причины отличий реальной и предсказанной переменной — это noise, variance и bias. Ансамбль помогает уменьшить эти факторы (за исключением noise — это неуменьшаемая величина).

Ансамбль

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

Бэггинг

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

Обычно берут случайную подвыборку данных для каждой модели, так все модели немного отличаются друг от друга. Выборка строится по модели выбора с возвращением. Из-за того что данная техника использует множество некореллириющих моделей для построения итоговой модели, это уменьшает variance. Примером бэггинга служит модель случайного леса (Random Forest, RF)

Бустинг

Бустинг — это техника построения ансамблей, в которой предсказатели построены не независимо, а последовательно

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

Алгоритм градиентного бустинга

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

Цель любого алгоритма обучения с учителем — определить функцию потерь и минимизировать её. Давайте обратимся к математике градиентного бустинга. Пусть, например, в качестве функции потерь будет среднеквадратичная ошибка (MSE):

градиентный бустинг

Мы хотим, чтобы построить наши предсказания таким образом, чтобы MSE была минимальна. Используя градиентный спуск и обновляя предсказания, основанные на скорости обучения (learning rate), ищем значения, на которых MSE минимальна.

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

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

Интуиция за градиентным бустингом

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

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

Нормальное распределение выборки отклонений со средним 0
Нормальное распределение выборки отклонений со средним 0

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

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

В итоге,

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

Шаги построения модели градиентного спуска

Рассмотрим смоделированные данные, как показано на диаграмме рассеивания ниже с 1 входным (x) и 1 выходной (y) переменными.

gradient boosting

Данные для показанного выше графика генерируются с использованием кода python:

1. Установите линейную регрессию или дерево решений на данные (здесь выбрано дерево решений в коде) [вызов x как input и y в качестве output]

2. Вычислите погрешности ошибок. Фактическое целевое значение, минус прогнозируемое целевое значение [e1 = y — y_predicted1]

3. Установите новую модель для отклонений в качестве целевой переменной с одинаковыми входными переменными [назовите ее e1_predicted]

4. Добавьте предсказанные отклонения к предыдущим прогнозам
[y_predicted2 = y_predicted1 + e1_predicted]

5. Установите еще одну модель оставшихся отклонений. т.е. [e2 = y — y_predicted2], и повторите шаги с 2 по 5, пока они не начнутся overfitting, или сумма не станет постоянной. Управление overfitting-ом может контролироваться путем постоянной проверки точности на данных для валидации.

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

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

Визуализация работы Gradient Boosting Tree:

  • Синие точки (слева) отображаются как вход (x) по сравнению с выходом (y);
  • Красная линия (слева) показывает значения, предсказанные деревом решений;
  • Зеленые точки (справа) показывают остатки по сравнению с вводом (x) для i-й итерации;
  • Итерация представляет собой последовательное заполнения дерева Gradient Boosting.
предсказания градиентного бустинга
Визуализация предсказаний (18-20 итерации)

Заметим, что после 20-й итерации отклонения распределены случайным образом (здесь не говорим о случайной норме) около 0, и наши прогнозы очень близки к истинным значениям (итерации называются n_estimators в реализации sklearn). Возможно, это хороший момент для остановки, или наша модель начнет переобучаться.

Посмотрим, как выглядит наша модель после 50-й итерации.

Визуализация градиентного бустинга после 50 предсказаний
Визуализация градиентного бустинга после 50 итераций

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

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

Видео Александра Ихлера:

Метод обратного распространения ошибки: математика, примеры, код

26 ноября 2018
обратное распространение

Метод обратного распространения ошибки: математика, примеры, код

Обратное распространение ошибки — способ обучения нейронной сети. Цели обратного распространения просты: отрегулировать каждый вес в нейросети пропорционально тому, насколько он способствует общей ошибке. Если мы будем итеративно уменьшать ошибку каждого веса,…

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

Обновление правила цепочки

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

f(x)=A(B(C(x)))

A, B, и C — функции активации на различных слоях. Пользуясь правилом цепочки, мы легко вычисляем производную f(x) по x:

f′(x)=f′(A)⋅A′(B)⋅B′(C)⋅C′(x)

Что насчёт производной относительно B? Чтобы найти производную по B, вы можете сделать вид, что B (C(x)) является константой, заменить ее переменной-заполнителем B, и продолжить поиск производной по B стандартно.

f′(B)=f′(A)⋅A′(B)

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

Применение правила цепочки

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

обратное распространение ошибки

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

Cost=C(R(Z(XW)))

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

C′(W)=C′(R)⋅R′(Z)⋅Z′(W)=(y^−y)⋅R′(Z)⋅X

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

обратное распространение ошибки нейронная сеть

Какова производная от потери по Wo?

C′(WO)=C′(y^)⋅y^′(ZO)⋅Z′O(WO)=(y^−y)⋅R′(ZO)⋅H

А что насчет Wh? Чтобы узнать это, мы просто продолжаем возвращаться в нашу функцию, рекурсивно применяя правило цепочки, пока не доберемся до функции, которая имеет элемент Wh.

C′(Wh)=C′(y^)⋅O′(Zo)⋅Z′o(H)⋅H′(Zh)⋅Z′h(Wh)=(y^−y)⋅R′(Zo)⋅Wo⋅R′(Zh)⋅X

И просто забавы ради, что, если в нашей сети было бы 10 скрытых слоев. Что такое производная потери для первого веса w1?

C(w1)=(dC/dy^)⋅(dy^/dZ11)⋅(dZ11/dH10)⋅(dH10/dZ10)⋅(dZ10/dH9)⋅(dH9/dZ9)⋅(dZ9/dH8)⋅(dH8/dZ8)⋅(dZ8/dH7)⋅(dH7/dZ7)⋅(dZ7/dH6)⋅(dH6/dZ6)⋅(dZ6/dH5)⋅(dH5/dZ5)⋅(dZ5/dH4)⋅(dH4/dZ4)⋅(dZ4/dH3)⋅(dH3/dZ3)⋅(dZ3/dH2)⋅(dH2/dZ2)⋅(dZ2/dH1)⋅(dH1/dZ1)⋅(dZ1/dW1)

Заметили закономерность? Количество вычислений, необходимых для расчёта производных потерь, увеличивается по мере углубления нашей сети. Также обратите внимание на избыточность в наших расчетах производных. Производная потерь каждого слоя добавляет два новых элемента к элементам, которые уже были вычислены слоями над ним. Что, если бы был какой-то способ сохранить нашу работу и избежать этих повторяющихся вычислений?

Сохранение работы с мемоизацией

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

уравнение обратного распространения

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

уравнение

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

Примечание: термин ошибка слоя относится к производной потерь по входу в слой. Он отвечает на вопрос: как изменяется выход функции потерь при изменении входа в этот слой?

Ошибка выходного слоя

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

C′(Zo)=(y^−y)⋅R′(Zo)

Чтобы упростить запись, практикующие МО обычно заменяют последовательность (y^−y)∗R'(Zo) термином Eo. Итак, наша формула для ошибки выходного слоя равна:

Eo=(y^−y)⋅R′(Zo)

Ошибка скрытого слоя

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

C′(Zh)=(y^−y)⋅R′(Zo)⋅Wo⋅R′(Zh)

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

Eh=Eo⋅Wo⋅R′(Zh)

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

Производная потерь по любому весу

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

C′(WO)=(y^−y)⋅R′(ZO)⋅H

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

C′(Wo)=Eo⋅H

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

C′(w)=CurrentLayerError⋅CurrentLayerInput

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

Подводя итог

Вот последние 3 уравнения, которые вместе образуют основу обратного распространения.

основа обратного распространения

Вот процесс, визуализированный с использованием нашего примера нейронной сети выше:

_images/backprop_visually.png

Обратное распространение: пример кода

def relu_prime(z):
if z > 0:
return 1
return 0

def cost(yHat, y):
return 0.5 * (yHat - y)**2

def cost_prime(yHat, y):
return yHat - y

def backprop(x, y, Wh, Wo, lr):
yHat = feed_forward(x, Wh, Wo)

# Layer Error
Eo = (yHat - y) * relu_prime(Zo)
Eh = Eo * Wo * relu_prime(Zh)

# Cost derivative for weights
dWo = Eo * H
dWh = Eh * x

# Update weights
Wh -= lr * dWh
Wo -= lr * dWo

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

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.

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

Сверточная нейронная сеть на 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.


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

Обучение нейронных сетей: проклятие размерности

3 октября 2018
обучение нейронной сети - проклятие размерности

Обучение нейронных сетей: проклятие размерности

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

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

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

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

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

Обучение глубокой нейронной сети

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

обучение нейронных сетей глубокая сеть DNN
Глубокая нейронная сеть (DNN)

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

Давайте начнем с самого наивного метода: случайный перебор. Установим веса случайным образом и подсчитаем результаты. Повторить много раз, сохранить все результаты, выбрать наиболее точные. На первый взгляд это кажется не таким уж плохим подходом. И действительно, компьютеры уже очень мощные, может быть мы можем получить решение брутфорсом? Для небольших нейронок (~30 нейронов) это и вправду работает. Но для задач из реального мира, где нейронов под 12,000 , случайный перебор теряет свою актуальность. Попробуйте найти иголку в 12000-мерном стоге сена методом простого перебора.

N-мерное пространство — одинокое место

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

Чтобы проиллюстрировать это, давайте рассмотрим несколько простых однослойных нейросетей: с тремя и с двумя нейронами.

Пространства 10х10(слева) и 10х10х10(справа) точек
Пространства 10х10(слева) и 10х10х10(справа) точек

В первом случае нужно найти всего два веса. Как много попыток нужно сделать, чтобы угадать идеальный набор? Один из подходов — представить двумерное пространство возможных весов и до изнеможения искать нужную комбинацию при некотором разбиении. Возможно, мы можем разбить каждую ось на 10 частей. Тогда попыток всего будет 10^2 = 100. Не так уж и плохо, но давайте сделаем разбиение более мелким, чтобы подобрать веса корректнее, например на 100 частей. Тогда уже комбинаций может быть 10,000. Достаточно маленькое число, компьютер сможет обработать его меньше, чем за секунду.

Так, а что же со второй сетью? Теперь нам нужно подобрать уже 3 веса и, следовательно, работать в трехмерном пространстве. Проделаем такие же операции 10*10*10 = 1000 вариантов и 100*100*100 = 1,000,000 вариантов соответственно при разных вариантах разбиения. Миллион вариантов — не так уж и страшно, но рост пугающий. И что же произойдет, когда мы перейдем к реальным сеткам?

Проклятие размерности

Что же будет, когад мы попробуем классифицировать цифры из датасета MNIST? Cеть будет содержать 784 входных нейронов, 15 нейронов в 1 скрытом слое и 10 выходных нейронов. Итого 784*15 + 15*10 = 11910 весов. Получается, чтобы угадать, нужно сделать 10^11910 попыток… Это 1 почти с 12,000 нулями. Для сравнения атомов в видимой Вселенной всего 10^80. Никакой суперкомпьютер не справится с таким перебором. И эта простая сетка даже близко не сравнится с самыми современными, где десятки и тысячи миллионов весов!

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

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

МРТ, чип в мозге и нейросети вернули подвижность парализованному человеку

3 октября 2018

МРТ, чип в мозге и нейросети вернули подвижность парализованному человеку

Исследователи много лет работают над методами на основе электрических импульсов, чтобы вернуть возможность двигаться людям с травмами спинного мозга и парализованными конечностями. Учёные из Университета Огайо и компании Batelle объявили, что…

Исследователи много лет работают над методами на основе электрических импульсов, чтобы вернуть возможность двигаться людям с травмами спинного мозга и парализованными конечностями. Учёные из Университета Огайо и компании Batelle объявили, что им удалось сделать прорыв в технологии с помощью нейронных сетей.

Инвазивный метод

Участником эксперимента стал Йен Буркхарт. Молодой человек парализован ниже плеч в результате несчастного случая. Учёные применили метод нейромышечной электростимуляции, чтобы вернуть подвижность руке Йена. С помощью функциональной МРТ исследователи вычислили области коры головного мозга, отвечающую за двигательную активность, и вживили туда чип, который реагирует на электрическую активность мозга. Когда Йен думает о перемещении руки, импульсы передаются через кабель к компьютеру. Затем алгоритмы машинного обучения кодируют данные и подают сигнал в гибкий рукав. Рукав стимулирует мышцы, в результате рука совершает нужное движение.

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

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

Новый подход

В сентябре команда опубликовала в журнале Nature Medicine результаты разработки новой модели. Была подключена вторая нейросеть, которая следила за движениями Йена, когда он свободно использовал механизм, и сама выявляла закономерности. Учёные использовали обучение без учителя и transfer learning для улучшения функциональности. Метод позволил уменьшить время обучения одному движению до 7 минут без необходимости переобучения — новая модель в течение года правильно интерпретировала движения Йена с 90% точностью.

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


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

Глубокое обучение (Deep Learning): краткий туториал

2 октября 2018
глубокое обучение

Глубокое обучение (Deep Learning): краткий туториал

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

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

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

Искусственный интеллект — способность машины или программы находить решения при помощи вычислений.

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

Машинное обучение — это попытка научить компьютеры самостоятельно обучаться на большом количестве данных вместо жестко постулированных правил.

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

deep learning и машинное обучение различие
Разница между машинным и глубоким обучением

Контролируемое и неконтролируемое обучение

Контролируемое обучение (обучение с учителем, supervised learning) подразумевает использование помеченных наборов данных, содержащих входные данные и ожидаемые выходные результаты. Когда вы обучаете ИИ с помощью контролируемого обучения, вы подаете как входные данные, так и ожидаемые выходные результаты.

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

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

Неконтролируемое обучение (обучение без учителя, unsupervised learning) — это машинное обучение с использованием наборов данных без определенной структуры.

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

Как работает глубокое обучение

Теперь вы подготовлены к изучению того, что такое глубокое обучение и как оно работает.

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

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

Мы хотим, чтобы наш сервис предсказывал цену на авиабилет по следующим входным данным:

  • аэропорт вылета;
  • аэропорт назначения;
  • дата отбытия;
  • авиакомпания.

Нейронные сети

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

глубокая нейронная сеть
Глубокая нейронная сеть (с двумя скрытыми слоями)

Нейроны сгруппированы в три различных типа слоев:

  • входной слой;
  • скрытый слой (слои);
  • выходной слой.

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

Скрытые слои выполняют математические вычисления со входными данными. Одна из задач при создании нейронных сетей — определение количества скрытых слоев и нейронов на каждом слое.

Слово «глубина» в термине «глубокое обучение» означает наличие более чем одного скрытого слоя.

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


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

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

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

глубокое обучение нейросети с тензофлоу

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

Обучение глубокой нейронной сети

Обучение нейросети — самая сложная часть глубокого обучения. Почему?

  • Вам нужен большой набор данных.
  • Вам нужно большое количество вычислительной мощности.

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

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

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

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

Как уменьшить значение функции потерь?

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

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

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

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

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

В сухом остатке:

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

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