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

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

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


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

Содержание

Цели задания

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

Бэкграунд

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

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

В истории нейросетей всплески энтузиазма сменялись разочарованиями. Последнее разочаровние было во второй половине 90х и 00х годах, когда им на смену пришли модели, которые не требовали таких больших вычислительных ресурсов для оптимизации и аккуратной инициализации. Однако в конце 00х возрос интерес к большим данным и стали доступнее кластеры, что позволило обучать сложные модели. Также возникла парадигма глубинного обучения (deep learning), в рамках которой появились методы стабильной инициализации весов. Это возродило интерес к нейросетям.

Одна из идей глубинного обучения заключается в следующем. Чтобы избежать переобучения, нужно сначала сократить размерность данных (игнорируя метки классов), а затем уже обучаться на данных низкой размерности. Первый этап может заменять ручное извлечение признаков, поэтому эту идею называют обучением признаков (feature learning). Классические методы обучения без учителя либо линейные (например, метод главных компонент, PCA), либо локальные (кластеризация). Автокодировщик преобразует данные, учитывая глобальные зависимости, как 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} — вектор смещений, их необходимо настроить по обучающей выборке. \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}{d\mathbf{o}_i} как функцию от \mathbf{h}_i для поэлементных сигмоиды и линейной функции,
  • \frac{d\mathbf{o}_{i+1}}{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}}.

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

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

TODO: описать, откуда качать каркас кода и данные.

Реализуйте вычисление выхода нейросети, функции потерь и её градиента в функции 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. Запустите градиентный спуск на час (или больше). Может ли он достигнуть той же точности, что и с предобучением? Попробуйте модифицировать инициализацию и параметры, может, дело в них?


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

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

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

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

Данные

MNIST:

Детали задания

  • использование кросс-энтропии при обучении (бонус — MSE)
  • регуляризация — L2 (weight decay) и gaussian noise

Мои результаты

  • MNIST, only 0, PCA-30: MSE=9.0
  • MNIST, only 01, PCA-30: MSE=7.3
  • MNIST, all-dig, PCA-30: MSE=14.2
  • MNIST, only 0, PCA-18: MSE=13.0
  • MNIST, only 1, PCA-18: MSE=3.7
  • MNIST, only 01, PCA-18: MSE=10.6
  • MNIST, all-dig, PCA-18: MSE=20.0
  • MNIST, only 0, autoenc-st0b5e10: MSE=50.0 (averages everything) // 5 batches, 10 epochs (default: targ-CE, opt-CG)
  • MNIST, only 0, autoenc-stNorm(0,0.3)b5e1000: MSE=12.6 (continues optimizing)
  • MNIST, only 0, autoenc-stNorm(0,0.3)Tie-b5e400: MSE=16.0, 2­-3 hours // seems no difference from the previous case
  • MNIST, only 0, autoenc-stNorm(0,0.3[*2,4])Tie-b5e400: MSE=23.4
  • MNIST, only 0, autoenc-stNorm(0,0.2)-b5e100,targ-MSE: MSE=55.0, (continues optimizing, diverse) // slower, but okay
  • could not make stochastic gradient find non-trivial optimum
  • MNIST, only 0, autoenc-LeCunInitTanh(Last)Mean-b5e300,targCE: MSE=131.8
  • MNIST, only 0, autoenc-stLeCunInitx10-b5e300: MSE=28.7
  • MNIST, only 0, pretrain-tiedw-1-stNorm(0,0.3)-b6e50: MSE=2.7, 20 min
  • MNIST, all-dig, pretrain-tiedw-stNorm(0,0.3)-b6: MSE1(e200): 0.86, MSE2(e500): 3.1, MSE3(e200): 3.3, MSE4(e200): 9.7. Backprop, 300it: MSE=6.5
  • MNIST, only 0, pretrain-tiedw-stNorm(0,0.3)-stoch-b150: MSE1(e200): 1.6, MSE2(e500): 4.2, MSE3(e200): 2.3, MSE4(e200): 4.7. Backprop, 500it: MSE=3.0 // 2 hours
  • MNIST, all-dig, pretrain-tiedw-stNorm(0,0.3), targ-MSE, arch-sigm+1e-4(except-linear, last): // 630 s
    • level1: 3ep, momentum=0.97, nbatches=1500, step=(k+1)(-1/4), MSE=3.2.
    • level2: 3ep, momentum=0.97, nbatches=1500, step=(k+1)(-1/4), MSE=7.3.
    • level3: 3ep, momentum=0.97, nbatches=1500, step=(k+1)(-1/4), MSE=4.1.
    • level4: 3ep, momentum=0.97, nbatches=1500, step=(k+1)(-1/4), MSE=6.0.
    • backprop: 3ep, momentum=0.97, nbatches=1500, step=(k+1)(-1/4), MSE=8.0. // 630 s
  • MNIST, all-dig, pretrain-tiedw-stNorm(0,0.3), targ-Xentropy, arch-sigm: // 730 s
    • level1: 3ep, momentum=0.97, nbatches=1500, step=(k+1)(-1/4), MSE=1.7.
    • level2: 3ep, momentum=0.97, nbatches=1500, step=(k+1)(-1/4), MSE=5.3.
    • level3: 3ep, momentum=0.97, nbatches=1500, step=(k+1)(-1/4), MSE=2.5.
    • level4: 10ep, momentum=0.97, nbatches=1500, step=0.01*(k+1)(-1/4), MSE=7.4.
    • backprop: 3ep, momentum=0.97, nbatches=1500, step=0.1*(k+1)(-1/4), MSE=8.7. // 645 s
  • MNIST, all-dig, backprop train, stNorm(0,0.3), targ-Xentropy, arch-sigm, 30ep, momentum=0.97, nbatches=1500, step=0.1*(k+1)(-1/4), MSE=8.2, // 6280 s
Личные инструменты