Практикум на ЭВМ (317)/Autoencoder

Материал из MachineLearning.

Перейти к: навигация, поиск
Основная статья: Практикум на ЭВМ (317)

В этом задании вам предстоит реализовать нейросетевой алгоритм сокращения размерности данных, известный как автокодировщик (autoencoder). Его нужно настроить на базе изображений рукописных цифр MNIST. Выполненное задание состоит из кода на Matlab и отчёта в формате pdf, в который нужно поместить ответы на вопросы, сформулированные в тексте. Пункты, помеченные как бонусные, выполнять необязательно, но полезно.

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

Содержание

Цели задания

  1. Познакомиться с задачей сокращения размерности данных (сжатия с потерями).
  2. Понять, какие практические проблемы возникают при обучении искусственных нейронных сетей.
  3. Усвоить некоторые принципы глубинного обучения (deep learning).
  4. Закрепить навыки манипуляции с матрицами.

Бэкграунд

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

В течение последних нескольких лет в рамках парадигмы глубинного обучения (deep learning) появились методы стабильной инициализации весов. Другая идея из глубинного обучения заключается в следующем. Чтобы избежать переобучения, нужно сначала сократить размерность данных (игнорируя метки классов), а затем уже обучаться на данных низкой размерности. Первый этап может заменять ручное извлечение признаков, поэтому эту идею называют обучением признаков (feature learning). Автокодировщик — это нейросеть, которая преобразует данные высокой размерности в данные низкой размерности, как и метод главных компонент (PCA), но при этом преобразование является нелинейным.

Теория

Этот раздел описывает архитектуру n-слойного нейросетевого автокодировщика. Пусть на вход подаётся вектор признаков \mathbf{x}, который в случае MNIST представляет собой вытянутое в строку изображение символа. На выходе автокодировщика имеем вектор \mathbf{y}, который должен быть близок к \mathbf{x}. Выход i-го слоя обозначим \mathbf{h}_i и положим \mathbf{h}_0 = \mathbf{x}, \mathbf{y} = \mathbf{h}_n. Формулы пересчёта значений на слоях следующие:

\mathbf{h}_i = \zeta_i(\mathbf{h}_{i-1} W_i + \mathbf{b}_{i}), \quad i \in \{1, \dots, n \},

где W_i — матрица весов i-го слоя, а \mathbf{b}_{i} — вектор смещений (bias), их необходимо настроить по обучающей выборке. \zeta_i(\cdot) — функция активации i-го слоя, которая применяется к векторам поэлементно и обеспечивает нелинейное преобразование. Мы предлагаем использовать архитектуру из статьи Хинтона и Салахутдинова (2006), в которой было предложено глубинное обучение для автокодировщика. Размер слоёв следующий: [784, 1000, 500, 250, 30, 250, 500, 1000, 784]. На входном и выходном слоях размерность совпадает с числом пикселей в изображении символа (28×28). Средний слой — 30-мерный вектор, именно на нём получается сжатое представление символа. На всех слоях используются сигмоидальные функции активации: \sigma(x) = \frac{1}{1+\exp(-x)}, кроме среднего, где используется линейная активация: \zeta_4(x) = x.

При обучении необходимо настроить матрицы W_i и векторы \mathbf{b}_{i} так, чтобы минимизировать функцию потерь L(\mathbf{y},\mathbf{x}). Её можно задавать по-разному, лишь бы она отражала расстояние между ответами и желательно легко дифференцировалась. Мы предлагаем в этом задании использовать среднеквадратичную ошибку (mean squared error, MSE): L(\mathbf{y},\mathbf{x}) = || \mathbf{y} - \mathbf{x} ||_2^2.

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

Хинтон и Салахутдинов предложили для инициализации параметров автокодировщика использовать этап предобучения, на котором последовательно настраивать параметры отдельных слоёв. Для этого используется простая графическая вероятностная модель — restricted Boltzmann machine (RBM). Мы предлагаем Вам более простую альтернативу: настраивать параметры автокодировщиков с одним внутренним слоем с помощью алгоритма обратного распространения ошибки. Идея в том, что двуслойные нейросети более устойчивы к инициализации параметров, то есть, их проще настраивать. При этом помогает использовать т.н. связанные веса (tied weights), когда матрица весов на втором слое является транспонированной матрицей весов первого слоя: W_1 = W_2^{\mathrm{T}}. Смещения же настраиваются по отдельности. Функции активации используются те же, что использовались на этих слоях в исходном автокодировщике. Таким образом, на этапе предобучения вам необходимо обучить 4 автокодировщика с одним внутренним слоем. Например, первый представляет собой нейросеть со слоями размерностей [784, 1000, 784], на выходах слоёв используются сигмоидальные функции активации. Его нужно обучить на исходных данных. Выход его скрытого слоя даёт данные для обучения второго автокодировщика. Четвёртый автокодировщик содержит слои размерностей [250, 30, 250], на скрытом слое используется линейная функция активации, на выходном — сигмоида. Полученные параметры используются для инициализации соответствующих слоёв «большого» автокодировщика, при его настройке ограничение на связанность весов снимается.

Практика

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

Выведите рекуррентные формулы градиентов по группам параметров. Обозначим линейный выход i-го слоя \mathbf{o}_i = \mathbf{h}_{i-1} W_i + \mathbf{b}_{i}, тогда \mathbf{h}_i = \zeta_i(\mathbf{o}_i). Вычислите и поместите в отчёт следующие функции:

  • \frac{dL}{d\mathbf{h}_n} как функцию от \mathbf{h}_n,
  • \frac{d\mathbf{h}_i^k}{d\mathbf{o}_i^k} как функцию от \mathbf{h}_i для поэлементных сигмоиды и линейной функции (для всех компонент k),
  • \frac{d\mathbf{o}_{i+1}^k}{d\mathbf{o}_{i}},
  • \frac{dL}{d\mathbf{o}_i} как функцию от \frac{dL}{d\mathbf{o}_{i+1}},
  • \frac{dL}{dW_i} и \frac{dL}{d\mathbf{b}_i} как функцию от \frac{dL}{d\mathbf{o}_{i}}.

Как изменятся формулы в случае связанных весов?

Реализация алгоритмов

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

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

  1. Опишите вычисление переменных val{layer}, аналогов h_{i+1}.
  2. Опишите вычисление функции потерь, в нашем случае MSE.
  3. Опишите вычисление градиентов функции потерь.
  4. Скорректируйте градиент для случая связанных весов. В этом случае для второй половины матриц весов градиенты должны остаться пустыми матрицами.

В функции minimize опишите вычисление шага градиентного спуска на эпохе k как s_k = \frac{l}{k^{1/4}}, где l — параметр функции learning_rate. Остальные изменения нужно будет внести позже.

В функции compute_error_and_plot опишите вычисление MSE между вычисленным ответом и верным. Эта функция вызывается в процессе оптимизации, чтобы отслеживать текущее качество модели. Если на вход подаются символы в формате MNIST, можно включить их отображение (вызывается код Руслана Салахутдинова).

Подбор параметров и обучение

Подбор параметров на сокращённой выборке

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

1. Архитектура сети передаётся в minimize как массив структур arch длины на 1 больше, чем число слоёв, с полями numw — число весов на данном слое, actfun и dactfun — функция активации и её производная как функция выходного значения самой функции. Опишите в начале test_pretrain.m вычисление сигмоиды и её производной: такие функции активации мы будем использовать для обоих слоёв. Запустите обучение из нулевого начального приближения на 20 эпох (это может занять пару минут). Какой результат вы наблюдаете визуально? Чему равна MSE в конце обучения?

2. Чтобы избежать такого эффекта, попробуйте инициализировать веса случайно, генерируя их из одномерных нормальных распределений с нулевым матожиданием и среднеквадратичным отклонением 0.3 независимо друг от друга. Чему равна MSE после 20 эпох? Бонус: попробуйте довести обучение до сходимости. Сколько эпох и сколько времени это занимает, какая финальная ошибка?

3. Такая скорость обучения может быть приемлемой для сокращённой выборки, но перед переходом на полный набор желательно ускорить оптимизацию. Один из способов — использовать стохастический градиентный спуск. Его идея в том, чтобы оценивать градиент не по всей выборке, а по одному случайному объекту (on-line training) или небольшой группе объектов (mini-batch training). Несмотря на то что индивидуальные оценки градиента становятся менее точными, за счёт ускорения оценок градиента и компенсации ошибок удаётся увеличить скорость оптимизации. На практике при on-line оптимизации не выбирают каждый раз случайный объект, а в начале каждой эпохи упорядочивают их в случайном порядке и затем выбирают последовательно. В такой реализации on-line training является частным случаем mini-batch с размером порции равным 1.

Реализуйте в функции minimize поддержку параметров shuffle и nbatches. Если первый равен true, то в начале каждой эпохи нужно случайно переупорядочить объекты и поделить выборку на nbatches частей. Внутри каждой эпохи градиент должен оцениваться nbatches раз, при этом счётчик номера итерации (для вычисления шага) растёт каждый раз на 1. Раскомментируйте соответствующий цикл и перенесите туда вычисление градиента.

Чем больше раз вычисляется градиент на каждой эпохе, тем она дольше, но в этом случае для обучения нужно меньше эпох. Попробуйте запустить оптимизацию на минуту (установите достаточно большое число эпох и останавливайте внучную) при следующих значениниях nbatches: 1 (предыдущий эксперимент), 150, 1500, −1 (on-line оптимизация). Чему равна ошибка через минуту? При каком значении оптимизация идёт быстрее?

4. При маленьких размерах порций оценка градиента становится ненадёжной. Для её стабилизации иногда используют момент — метод усреднения градиента на последних итерациях (также называется damping). Вместо того, чтобы делать шаг по градиенту, на итерациях оптимизации поддерживается вектор \Delta. Если на k-й итерации оценен градиент \nabla^{k}L, то \Delta^k = (1-m)\nabla^{k}L + m\Delta^{k-1}, где константа m \in [0,1). Этот вектор вычитается из вектора параметров на каждой итерации вместо градиента. Реализуйте эту возможность. Запустите 2 эпохи оптимизации на 1500 порциях при значениях коэффициента момента 0.0 (предыдущий эксперимент), 0.8, 0.9. Какие ошибки получаются после двух эпох?

5. (Бонус) попробуйте подобрать параметры алгоритма оптимизации, чтобы ошибка на обучении падала скорее всего.

Обучение многослойного автокодировщика

1. В скрипте test.m опишите функции активации и их производные для всех слоёв. В функции pretrain.m внутри цикла опишите: 1) создание архитектуры двуслойного автокодировщика для данного слоя, 2) случайную инициализацию параметров, 3) преобразование данных в соответствии с обученными параметрами для получения обучающей выборки следующего слоя. Обратите внимание на используемые параметры: 3 эпохи по 1500 порций, коэффициент момента 0.97. Это эмпирически установленные значения, которые неплохо походят для предобучения всех слоёв. Вы можете добиться лучшего результата, установив индивидуальные параметры для каждого слоя, а также регулируя скорость обучения. Визуализация параметров включена только для первого слоя, так как только для него она имеет смысл. Запустите test.m и убедитесь, что обучение идёт.

2. Раскомментируйте в test.m тонкую подстройку весов градиентным спуском. Подождите 3–4 эпохи обучения, и остановите вручную. Чему равна ошибка на обучающей выборке? Сильно ли градиентный спуск улучшает результат предобучения? Загрузите тестовую выборку (файлы testX.mat) и протестируйте обученный автокодировщик на ней. Сделайте вывод о степени переобучения.

3. Предлагается проверить, есть ли смысл в этапе предобучения. Отключите предобучение в начале test.m, сделайте инициализацию параметров из нормального распределения с центром в нуле и среднеквадратичным отклонением 0.3. Запустите градиентный спуск на час (или больше). Может ли он достигнуть той же точности, что и с предобучением? Бонус: попробуйте модифицировать инициализацию и параметры, чтобы улучшить точность градиентного спуска без предобучения.

4. (Бонус) Реализуйте метод главных компонент (PCA) и сократите размерность данных до 30 главных компонент. Может ли автокодировщик извлечь пользу из нелинейности преобразования?

Обучение классификатора цифр на данных сокращённой размерности

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

1. Попробуйте обучить однослойную нейросеть, на выходном слое которой используется функция активации softmax. Это естественная функция для задачи многоклассовой классификации. Обратите внимание, что она применяется уже не поэлементно. Какую точность (доля верно распознанных цифр) удаётся получить?

2. Добавьте скрытый слой размерности 20 с сигмоидальной функцией активации. Позволяет ли это поднять точность?

3. Сравните полученную точность с точностью SVM из первого задания. Опишите сравнение в отчёте.