Вариационные автокодировщики

Раздел вариационных автокодировщиков. Введение

Здравствуйте и вновь добро пожаловать на занятия по теме «Глубокое обучение без учителя, часть 2».

В этой статье мы обсудим некоторые основы, на которых покоятся вариационные автокодировщики.

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

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

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

Что под этим понимается?

Предположим, мы можем обучить автокодировщик в точности воспроизводить свои входные данные. Допустим, размер входных данных для автокодировщика составляет 784 размерности, а размер скрытого слоя – 200 размерностей. Это значит, что мы научились представлять любое переданное изображение, состоящее из 784 чисел, в виде значительно меньшей величины – 200 чисел. А что это значит? Это значит, что среди первоначальных 784 чисел многие из них были избыточны – истинное количество информации, содержавшееся в исходных данных, должно быть куда меньше 784. Обычно эти 200 чисел скрытого слоя называются представлением скрытых переменных и обозначаются z. Это и неудивительно, ведь в прошлом мы постоянно использовали букву z для представления скрытых слоёв нейронной сети и скрытых переменных. Позже мы вновь вернёмся к этой концепции.

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

Как вы, возможно, помните, алгоритм максимизации ожиданий используется в случае, когда у нас есть модель со скрытой переменной и мы не можем напрямую максимизировать p(x). Примером его может служить уже встречавшаяся нам гауссова смесь распределений. Алгоритм максимизации ожиданий даёт нам точечную оценку параметров; другими словами, его можно рассматривать как частотный статистический метод. Вариационный же вывод расширяет эту идею до байесовского обучения, когда вместо изучения точечных оценок мы изучаем их распределения.

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

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

Архитектура вариационного автокодировщика

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

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

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

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

И наконец мы добираемся до конца декодировщика. Что здесь происходит? Вряд ли удивительно, что тут тоже имеет место распределение. Допустим, нашими входными данными являются двоичные переменные, так что выходными данными также будут двоичные переменные. Другими словами, данные принимают значения только 0 и 1. Разумеется, это распределение Бернулли, которое характеризуется одним параметром, показывающим вероятность получения 1. Как вам известно, сигмоида даёт значения в диапазоне между 0 и 1, а значит, является вполне подходящей функцией активации, а выход декодировщика может представлять распределения Бернулли.

На самом деле это мало чем отличается от того, что мы делали в случае двоичной классификации. Как мы знаем, для неё используется сигмоида, а выход представляет собой вероятность p(y = 1|x). Впрочем, мы рассматривали её как прогноз, а не как распределение вероятности. Другими словами, мы округляли его, чтобы получить прогнозируемую метку. Здесь же, так как мы стараемся стать на байесовскую точку зрения, то явно указываем на это различие: выход декодировщика представляет собой распределение вероятности.

Следует иметь в виду, что поскольку выход является распределением, это влияет на то, как мы будем его использовать. В обычной нейронной сети, выполняя двоичную классификацию, мы получаем p(y = 1|x) и округляем его, чтобы получить прогноз. В случае же вариационного автокодировщика парадигма несколько другая. На выходе декодировщика мы получаем распределение, а затем из этого распределения генерируем образцы.

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

Далее идёт декодировщик. Чтобы получить для него входные данные, мы берём выборку из q(z), являющееся, как уже обсуждалось, гауссовым распределением, после чего обычным путём пропускаем её через нейронную сеть декодировщика. В данном случае мы рассматриваем входные и выходные данные в виде двоичных переменных, что означает, что распределение будет распределением Бернулли, для получения параметров которого можно использовать сигмоиду. И, кстати, поскольку выходные данные на самом деле представляют собой распределение, их можно использовать для генерации образцов, а не просто для двоичной классификации, где мы лишь округляли выходные значения.

Параметризация гауссового распределения при помощи нейронной сети

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

Предположим, у нас есть нейронная сеть, представляющая собой кодировщик, с размерами слоёв 4, 3 и 2. Это означает, что входные данные имеют размерность 4, размер скрытого слоя равен 3, а размер скрытого вектора z равен 2. Это, в свою очередь, значит, что в конце мы получим представление двухмерного гауссового распределения. Заметьте, что для простоты мы будем представлять выровненное по оси распределение, а не распределение с полной ковариацией, что потребовало бы даже большего количества параметров. Другими словами, мы можем представить дисперсию с помощью единичного вектора вместо матрицы с одним элементом на каждую размерность.

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

Тут всё довольно просто, однако возникает другая проблема. Мы знаем, что стандартное отклонение должно быть строго положительным числом, но нейронная сеть может на выходе выдавать любое число – и положительное, и отрицательное. Как же нам это исправить?

Простейшим решением будет использовать в качестве функции активации softplus:

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

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

И наконец мы добираемся до конца декодировщика. Что здесь происходит? Вряд ли удивительно, что тут тоже имеет место распределение. Допустим, нашими входными данными являются двоичные переменные, так что выходными данными также будут двоичные переменные. Другими словами, данные принимают значения только 0 и 1. Разумеется, это распределение Бернулли, которое характеризуется одним параметром, показывающим вероятность получения 1. Как вам известно, сигмоида даёт значения в диапазоне между 0 и 1, а значит, является вполне подходящей функцией активации, а выход декодировщика может представлять распределения Бернулли.

На самом деле это мало чем отличается от того, что мы делали в случае двоичной классификации. Как мы знаем, для неё используется сигмоида, а выход представляет собой вероятность p(y = 1|x). Впрочем, мы рассматривали её как прогноз, а не как распределение вероятности. Другими словами, мы округляли его, чтобы получить прогнозируемую метку. Здесь же, так как мы стараемся стать на байесовскую точку зрения, то явно указываем на это различие: выход декодировщика представляет собой распределение вероятности.

Следует иметь в виду, что поскольку выход является распределением, это влияет на то, как мы будем его использовать. В обычной нейронной сети, выполняя двоичную классификацию, мы получаем p(y = 1|x) и округляем его, чтобы получить прогноз. В случае же вариационного автокодировщика парадигма несколько другая. На выходе декодировщика мы получаем распределение, а затем из этого распределения генерируем образцы.

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

Далее идёт декодировщик. Чтобы получить для него входные данные, мы берём выборку из q(z), являющееся, как уже обсуждалось, гауссовым распределением, после чего обычным путём пропускаем её через нейронную сеть декодировщика. В данном случае мы рассматриваем входные и выходные данные в виде двоичных переменных, что означает, что распределение будет распределением Бернулли, для получения параметров которого можно использовать сигмоиду. И, кстати, поскольку выходные данные на самом деле представляют собой распределение, их можно использовать для генерации образцов, а не просто для двоичной классификации, где мы лишь округляли выходные значения.

Параметризация гауссового распределения при помощи нейронной сети

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

Предположим, у нас есть нейронная сеть, представляющая собой кодировщик, с размерами слоёв 4, 3 и 2. Это означает, что входные данные имеют размерность 4, размер скрытого слоя равен 3, а размер скрытого вектора z равен 2. Это, в свою очередь, значит, что в конце мы получим представление двухмерного гауссового распределения. Заметьте, что для простоты мы будем представлять выровненное по оси распределение, а не распределение с полной ковариацией, что потребовало бы даже большего количества параметров. Другими словами, мы можем представить дисперсию с помощью единичного вектора вместо матрицы с одним элементом на каждую размерность.

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

Тут всё довольно просто, однако возникает другая проблема. Мы знаем, что стандартное отклонение должно быть строго положительным числом, но нейронная сеть может на выходе выдавать любое число – и положительное, и отрицательное. Как же нам это исправить?

Простейшим решением будет использовать в качестве функции активации softplus:

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

А теперь давайте взглянем на код. Соответствующий файл в репозитарии курса называется parameterize_guassian.py.

Вначале у нас идут несколько стандартных импортов – Numpy, Matplotlib и функция multivariate_normal из библиотеки Scipy:

from __future__ import print_function, division

from builtins import range, input

# Note: you may need to update your version of future

# sudo pip install -U future

import numpy as np

import matplotlib.pyplot as plt

from scipy.stats import multivariate_normal as mvn

Далее мы определяем функцию softplus. Обратите внимание, что тут используется специальная функция Numpy, которая называется log1p, которая автоматически добавляет единицу к своему аргументу. Причина, по которой мы её используем, заключается в том, что она более численно стабильна:

def softplus(x):

  # log1p(x) == log(1 + x)

  return np.log1p(np.exp(x))

Затем мы определяем весовые коэффициенты нашей нейронной сети, являющейся просто нейронной сетью с одним скрытым слоем. Входной размер равен 4, скрытый слой равен 3, а выходной размер равен 2 – такой размер выходного слоя нам нужен, чтобы можно было отобразить примеры на диаграмме. Обратите внимание, что размерность первого весового коэффициента равна 4×3, а вот второго – 3×4 вместо обычного 3×2. Это связано с тем, что нам нужно две исходящие величины для представления среднего значения исходящего распределения и ещё две величины – для представления стандартного отклонения этого же распределения.

# we’re going to make a neural network

# with the layer sizes (4, 3, 2)

# like a toy version of a decoder

W1 = np.random.randn(4, 3)

W2 = np.random.randn(3, 2*2)

# why 2 * 2?

# we need 2 components for the mean,

# and 2 components for the standard deviation!

# ignore bias terms for simplicity.

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

def forward(x, W1, W2):

  hidden = np.tanh(x.dot(W1))

  output = hidden.dot(W2) # no activation!

  mean = output[:2]

  stddev = softplus(output[2:])

  return mean, stddev

Далее мы случайным образом создаём входной вектор размером 4 и подставляем его в нейронную сеть, чтобы получить среднее значение и стандартное отклонение. Не забывайте, что это представляет нечто вроде модели распределения q(z|x).

# make a random input

x = np.random.randn(4)

# get the parameters of the Gaussian

mean, stddev = forward(x, W1, W2)

print(“mean:”, mean)

print(“stddev:”, stddev)

Затем мы делаем 10 000 выборок из гауссового распределения с этими средним значением и стандартным отклонением. Обратите внимание, что модуль mvn в качестве аргумента принимает ковариацию – дисперсии вдоль диагонали, поэтому нам необходимо возвести стандартное отклонение в квадрат. И наконец, получив образцы выборок, мы наносим их на диаграмму и выводим на экран. Такое наглядное представление данных можно использовать, чтобы убедиться, что они имеют среднее значение и стандартное отклонение, вычисленные нейронной сетью.

# draw samples

samples = mvn.rvs(mean=mean, cov=stddev**2, size=10000)

# plot the samples

plt.scatter(samples[:,0], samples[:,1], alpha=0.5)

plt.show()

Запустим нашу программу и посмотрим, что у нас получится.

Вычисленное нами среднее значение равно -2,4 и -1,9, а стандартное отклонение равно 2,2 и 2,16. На рисунке мы видим образцы, взятые из этого распределения.

Хотя этот код был довольно-таки простым, концептуально он отличается от того, что мы обычно делали, поэтому давайте пройдёмся по отдельным этапам. Первое: мы выполняем прямое прохождение по нейронной сети – всё как обычно. Второе: выход нейронной сети является не значением, а распределением, и в этом отличие. Третье: из этого распределения мы можем генерировать образцы, что и будем делать при помощи вариационного автокодировщика. И четвёртое: мы ещё не поговорили о том, как обучать эти весовые коэффициенты. Действительно, возникает вопрос: как выполнить градиентный спуск, если в итоге мы получаем образец из распределения, а не значение, являющееся функцией обновляемых параметров? Об этом надо задуматься.

Ключевым моментом является то, что выходное распределение кодировщика является функцией входных данных и обозначается q(z), хотя зависимость от x не совсем ясна. Конечно, мы можем просто обозначить её q(z|x), чтобы явно показать зависимость. Однако полезно уяснить, что именно это q(z) пытается приближённо представить. Если конкретно, то оно приближённо представляет p(z|x) – распределение z, зависящее от того, чем является x. p(z|x) можно рассматривать как «истинное» условное распределение, а q(z|x) – это приближённое представление нейронной сетью этого p. Это окажется полезным позже, когда мы начнём обсуждать функцию затрат и как её оптимизировать.

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

Важнейшей концепцией, применимой как к вариационным, так и обычным автокодировщикам, является идея скрытого пространства. Не забывайте, что наша нейронная сеть состоит из двух частей: кодировщика и декодировщика. Кодировщик отвечает за преобразование входных данных x в отличающийся от них вектор z – кодированную версию x. Это кодированная версия x и находится в скрытом пространстве. Впоследствии, взяв вектор из этого скрытого пространства, мы можем пропустить его через декодировщик и получить обратно изображение, выглядящее как исходное. Полезно наглядно представить этот процесс, поскольку он приводит к различным операциям, которые мы можем совершить.

Чему же, по сути, должен научиться наш автокодировщик? Он должен взять «сгусток» этого скрытого пространства и, по сути говоря, отделить его. Для простоты сделаем вид, что мы используем базу MNIST. У нас есть цифры от 0 до 9. Это означает, что данные 10 цифр должны находиться где-то в этом сгустке. Что же должен научиться делать автокодировщик? Он учится брать изображение и находить, где в этом сгустке оно находится, а также учится брать любую цифру из её местопребывания в сгустке и преобразовывать обратно в изображение этой цифры. Сгусток подобен крошечной частице пространства, представляющей цифру, однако заполняющий лишь часть пространства, занимаемым полным изображением. Это можно рассматривать как операции сжатия и распаковки данных: чтобы сжать, мы преобразовываем изображение в некоторый код, а чтобы распаковать – преобразовываем этот код обратно в изображение. Ключевым тут является сжатие, что означает уменьшение. Таким образом, вариационный автокодировщик в некотором смысле упаковывает учебные данные в малый объём пространства, а впоследствии их распаковывает, когда мы запрашиваем образец.

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

Что касается апостериорной прогнозируемой выборки, то процесс в точности заключается в том, что мы описывали ранее в качестве прямого механизма получения восстановленных входных данных: мы подставляем изображение – скажем, изображение восьмёрки, – а оно получает отображение в гауссовом распределении q(z|x). Затем мы берём выборку z из этого распределения и пропускаем её через декодировщик, чтобы получить восстановленную версию изображения. После этого мы можем выбрать это изображение из данного выходного распределения. Это и называется апостериорной прогнозируемой выборкой.

Но мы можем сделать и кое-что ещё. Вместо получения z из входных данных x, мы можем получить его из стандартного нормального распределения. Это N(0, 1) в случае одной размерности и N(0, единичная_матрица) в случае нескольких размерностей. Позже вы поймёте, почему стандартное нормальное распределение имеет особое значение. Получив это z, мы можем, как обычно, пропустить его через декодировщик. Сделав таким образом, можно получить изображение, выглядящее так, словно оно получено из учебных данных, скажем, изображения восьмёрки. Это называется априорной прогнозируемой выборкой. Разумеется, поскольку мы выбираем лишь из стандартного нормального распределения, то не знаем, какую цифру получим.

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

Функция затрат

Теперь начнём разговор о том, как обучать автокодировщик. Не забывайте, что во всех задачах машинного обучения у нас обычно есть два режима работы: вывод и обучение. Вывод – широкое и, вероятно, чересчур широко используемое понятие, но в контексте глубокого обучения оно обычно относится к направлению вперёд, то есть, другими словами, созданию прогнозов, если речь идёт об обучении с учителем, и преобразованию данных в скрытое представление, если речь идёт об обучении без учителя. В библиотеке Sci-Kit Learn это обычно совершается функциями fit и predict или функциями fit и transform. В любом случае следующий этап – обсудить приспособление, или обучение. Обычно мы хотим определить функцию затрат, а затем её минимизировать.

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

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

А теперь – большое откровение! Целевая функция, которую нам нужно оптимизировать, называется ELBO – evidence lower bound, то есть нижняя граница доказательства:

Смысл такого названия станет яснее чуть позже. В библиотеке Tensorflow, так как оптимизатор имеет только функцию минимизации, но не имеет функции максимизации, мы будем минимизировать это выражение со знаком «минус». То есть нашей функцией затрат будет –ELBO, хотя на самом деле мы будем стараться максимизировать ELBO.

Функция ELBO состоит из двух членов: ожидаемого логарифма правдоподобия данных минус KL-расхождение между q(z|x) и p(z). Что это означает, мы поясним в течение следующих нескольких минут.

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

Это можно сделать, потому что x-ы сами по себе представляют вероятности.

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

Это можно сделать, потому что x-ы сами по себе представляют вероятности.

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

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

Следующим идёт член с KL-расхождением между q(z|x) и p(z). В байесовском машинном обучении p(z) называется априорной вероятностью и нам нужно его выбрать. Для удобства выбирается N(0, 1) – стандартное нормальное распределение. Пожалуйста, обратите внимание, что нет никакого теоретического обоснования для выбора именно (0, 1) – это делается лишь потому, что это удобно. В действительности в этом иногда проявляется слабость байесовского машинного обучения: если выбрать неудачную априорную вероятность, это приведёт к неудовлетворительным результатам, даже если модель хороша.

Что же делает KL-расхождение? Мы это ранее уже обсуждали, но на случай, если вы подзабыли, KL-расхождение – это способ сравнения двух распределений вероятностей:

Если два распределения вероятностей в точности одинаковы, то мы получим 0, если же они различны, то получим нечто больше нуля, и чем больше различие между распределениями вероятностей, тем больше значение KL-расхождения. Таким образом, это лишь член, который побуждает q(z) стремиться к стандартному нормальному распределению – не забывайте, что нам нужно, чтобы скрытые векторы были именно там.

Теперь поговорим о том, почему ELBO и два составляющих его члена имеют смысл. Один из способов их интерпретации – полагать, что функция затрат обычно состоит из двух таких частей: первая часть показывает, насколько близок наш выход к целевой переменной (в случае, если мы выполняем регрессию, классификацию или восстановление входных данных); вторая часть – регуляризатор. Это общераспространённая конструкция во многих моделях машинного обучения, включая, как мы видели ранее, и глубокое обучение, и даже метод опорных векторов. Действительно, во многих статьях просто исходят из этой предпосылки. Так что это один из способов убедиться, что данная функция затрат подходит, – удостовериться, что она состоит из двух обычных частей: один член показывает, насколько мы близки к цели, а другой представляет собой регуляризатор.

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

Андрей Никитенко
Андрей Никитенко
Задать вопрос эксперту