Реализация вариационного автокодировщика в Tensorflow

Реализация в Tensorflow (часть 1)

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

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

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

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

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

Конкретнее говоря, нужно обратить внимание на стохастический тензорный модуль Stochastic Tensor Module, который можно взять при помощи tf.contrib.bayesflow.stochastic_tensor, а использовать – при помощи следующего синтаксиса:

with st.value_type(st.SampleValue() ):

    self.Z = st. StochasticTensor(Normal (loc=means, scale=stddev))

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

Следующий вопрос – как создать функцию затрат. Не забывайте, что в ней есть две части – ожидаемый логарифм правдоподобия и KL-расхождение между q(z|x) и априорной вероятностью p(z). Вначале обсудим ожидаемое значение логарифма правдоподобия. Как вы знаете, это двоичная кросс-энтропийная функция или, более точно, отрицательное значение двоичной кросс-энтропийной функции между входными данными x и восстановленными данными , рассматривамое нами как средние значения в распределении Бернулли. К счастью, в Tensorflow уже есть двоичная кросс-энтропийная функция, которой можно воспользоваться. Прямое определение кросс-энтропийной функции использовать не рекомендуется ввиду проблем с численной стабильностью. Другой вариант – создать объект распределения Бернулли и вычислить логарифм плотности распределения вероятностей данных для восстановительного распределения. В коде я покажу, как использовать оба этих варианта. Если вы у вас возникнут вопросы, как или почему они идентичны, я бы рекомендовал выписать их на бумаге – это очень хорошее упражнение. Вы также можете написать код в Numpy, чтобы самому убедиться, что они одинаковы. Но вот чего определённо делать не стоит – так это использовать прямое определение, поскольку оно является численно нестабильным.

Далее идёт KL-расхождение. К счастью, Tensorflow идёт с функцией kl_divergence. Она принимает в качестве аргументов два распределения, а на выходе выдаёт KL-расхождение. Заметьте, что KL-расхождение между двумя гауссовыми распределениями можно получить и в аналитическом виде, и именно так мы поступим при реализации в Theano, поскольку, судя по всему, в этой библиотеке нет функции для KL-расхождения. Именно поэтому я всегда говорю, что использование библиотеки Tensorflow более похоже на упражнение по чтению документации, а не по машинному обучению. Как бы то ни было, имея KL-расхождение и ожидаемое значение логарифма правдоподобия, мы можем вычислить ELBO, а потом уже будет просто создать оптимизатор для выполнения градиентного спуска.

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

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

Реализация в Tensorflow (часть 2)

Давайте подробнее исследуем Stochastic Tensor, поскольку может оказаться не совсем ясно, что он делает. Здесь мы используем его лишь для того, чтобы убедиться, что он действительно выбирает значения из выборки со средним значением и стандартным отклонением, которые мы ему указываем. Если вы не хотите писать код самостоятельно, а лишь просмотреть его, соответствующий файл в репозитарии курса называется test_stochastic_tensor.py.

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

from __future__ import print_function, division

from builtins import range

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

# sudo pip install -U future

import numpy as np

import tensorflow as tf

import matplotlib.pyplot as plt

st = tf.contrib.bayesflow.stochastic_tensor

Normal = tf.contrib.distributions.Normal

Далее устанавливаем значение количества образцов N, определяя среднее значение равным 5, а стандартное отклонение – равным 3. Обратите внимание, что эти среднее значение и стандартное отклонение должны быть того же размера, что и желаемый результат, а следовательно, у нас будет N пятёрок для среднего значения и N троек для стандартного отклонения. Таким образом Stochastic Tensor узнаёт, какого размера должен быть выходной результат.

# sample N samples from N(5,3*3)

N = 10000

mean = np.ones(N)*5

scale = np.ones(N)*3

Затем мы создаём переменную из одних единиц. Обычно делать этого не нужно, но на деле мы должны умножить результат на 1, чтобы можно было запустить сессию. К сожалению, когда я пытался получить результат работы Stochastic Tensor напрямую, то получал ошибку, хотя и ожидалось, что всё будет работать.

I = tf.Variable(np.ones(N))

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

with st.value_type(st.SampleValue()):

  X = st.StochasticTensor(Normal(loc=mean, scale=scale))

# cannot session.run a stochastic tensor

# but we can session.run a tensor

Y = I * X

Далее мы запускаем Y в сессии и выводим на экран наши среднее значение и стандартное отклонение. Разумеется, нашей целью является убедиться, что мы получим нечто в районе 5 и 3.

init_op = tf.global_variables_initializer()

with tf.Session() as session:

  session.run(init_op)

  Y_val = session.run(Y)

  print(“Sample mean:”, Y_val.mean())

  print(“Sample std dev:”, Y_val.std())

  plt.hist(Y_val, bins=20)

  plt.show()

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

Гистограмма чётко центрирована по 5, а среднее значение и стандартное отклонение выборки очень близки к 5 и 3.

  Замечание: на данный момент новые версии Tensorflow выпускаются ежемесячно. Я бы не рекомендовал пытаться за ними угнаться. Не тратьте время на переучивание синтаксиса. — Прим. LazyProgrammer.

Реализация в Tensorflow (часть 3)

Пройдёмся по реализации вариационного автокодировщика в коде с помощью библиотеки Tensorflow. Если вы не хотите попробовать написать код самостоятельно, хотя я настоятельно рекомендую сделать именно так, соответствующий файл называется vae_tf.py.

Вначале у нас идёт ряд импортов. Файл util.py – просто локальный файл, помогающий нам загрузить данные. При желании вы можете его проверить, но там лишь стандартный код, который, как я полагаю, на данный момент вы можете уже написать с закрытыми глазами. Далее, как и следовало ожидать, мы импортируем библиотеки Numpy, Matplotlib и Tensorflow, после которых идут несколько модулей Tensorflow, настолько глубоко вложенных, что нужно дать им более удобные имена.

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 util

import numpy as np

import tensorflow as tf

import matplotlib.pyplot as plt

st = tf.contrib.bayesflow.stochastic_tensor

Normal = tf.contrib.distributions.Normal

Bernoulli = tf.contrib.distributions.Bernoulli

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

class DenseLayer(object):

  def __init__(self, M1, M2, f=tf.nn.relu):

    # self.M1 = M1

    # self.M2 = M2

    self.W = tf.Variable(tf.random_normal(shape=(M1, M2)) * 2 / np.sqrt(M1))

    self.b = tf.Variable(np.zeros(M2).astype(np.float32))

    self.f = f

  def forward(self, X):

    return self.f(tf.matmul(X, self.W) + self.b)

Затем идёт класс VariationalAutoencoder. Большая часть работы происходит в конструкторе, так что сейчас последует наиболее важная часть программы. Итак, в качестве аргумента он принимает D – входную размерность, и размеры скрытого слоя hidden_layer_sizes. Конечным элементом списка hidden_layer_sizes будет размер выхода кодировщика, одновременно являющийся размером входа декодировщика. Мы будем считать, что размеры слоёв декодировщика являются просто размерами кодировщика, размещённые в обратном порядке.

class VariationalAutoencoder:

  def __init__(self, D, hidden_layer_sizes):

    # hidden_layer_sizes specifies the size of every layer

    # in the encoder

    # up to the final hidden layer Z

    # the decoder will have the reverse shape

 После этого мы создаём заполнитель для данных X, а затем – создаём слои кодировщика, состоящие из объектов DenseLayer, но только до предпоследнего слоя. Это связано с тем, что в слоях мы по умолчанию используем функцию relu, однако для выхода кодировщика нам нужно использовать что-то другое. Не забывайте, что кодировщик должен давать на выходе и среднее значение, и стандартное отклонение гауссового распределения; средние значения не имеют никакой активации, а для стандартных отклонений для активации, как мы видели, используется функция softplus. Впрочем, мы можем определить их вне самих слоёв, что заодно и объясняет, почему выходной размер равен 2*M.

    # represents a batch of training data

    self.X = tf.placeholder(tf.float32, shape=(None, D))

    # encoder

    self.encoder_layers = []

    M_in = D

    for M_out in hidden_layer_sizes[:-1]:

      h = DenseLayer(M_in, M_out)

      self.encoder_layers.append(h)

      M_in = M_out

    # for convenience, we’ll refer to the final encoder size as M

    # also the input to the decoder size

    M = hidden_layer_sizes[-1]

    # the encoder’s final layer output is unbounded

    # so there is no activation function

    # we also need 2 times as many units as specified by M_out

    # since there needs to be M_out means + M_out variances

    h = DenseLayer(M_in, 2 * M, f=lambda x: x)

    self.encoder_layers.append(h)

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

    # get the mean and variance / std dev of Z.

    # note that the variance must be > 0

    # we can get a sigma (standard dev) > 0 from an unbounded variable by

    # passing it through the softplus function.

    # add a small amount for smoothing.

    current_layer_value = self.X

    for layer in self.encoder_layers:

      current_layer_value = layer.forward(current_layer_value)

    self.means = current_layer_value[:, :M]

    self.stddev = tf.nn.softplus(current_layer_value[:, M:]) + 1e-6

Затем мы делаем выборку из Z в виде стохастического тензора, работу которого вы видели ранее на простом примере.

    # get a sample of Z

    # we need to use a stochastic tensor

    # in order for the errors to be backpropagated past this point

    with st.value_type(st.SampleValue()):

      self.Z = st.StochasticTensor(Normal(loc=self.means, scale=self.stddev))

      # to get back Q(Z), the distribution of Z

      # we will later use self.Z.distribution

Это всё, что касается кодировщика.

Далее идёт декодировщик. Мы создаём объекты DenseLayer для каждого из скрытых слоёв, но в обратном порядке, поскольку нам нужно, чтобы они имели противоположную форму. Разумеется, это значит, что наш выходной слой будет иметь размерность D – входную размерность.

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

    # decoder

    self.decoder_layers = []

    M_in = M

    for M_out in reversed(hidden_layer_sizes[:-1]):

      h = DenseLayer(M_in, M_out)

      self.decoder_layers.append(h)

      M_in = M_out

    # the decoder’s final layer should technically go through a sigmoid

    # so that the final output is a binary probability (e.g. Bernoulli)

    # but Bernoulli accepts logits (pre-sigmoid) so we will take those

    # so no activation function is needed at the final layer

    h = DenseLayer(M_in, D, f=lambda x: x)

    self.decoder_layers.append(h)

Именно этим мы и займёмся следующим делом. У нас есть распределение Бернулли, соответствующее этим логитам.

    # get the logits

    current_layer_value = self.Z

    for layer in self.decoder_layers:

      current_layer_value = layer.forward(current_layer_value)

    logits = current_layer_value

    posterior_predictive_logits = logits # save for later

    # get the output

    self.X_hat_distribution = Bernoulli(logits=logits)

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

    # take samples from X_hat

    # we will call this the posterior predictive sample

    self.posterior_predictive = self.X_hat_distribution.sample()

    self.posterior_predictive_probs = tf.nn.sigmoid(logits)

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

    # take sample from a Z ~ N(0, 1)

    # and put it through the decoder

    # we will call this the prior predictive sample

    standard_normal = Normal(

      loc=np.zeros(M, dtype=np.float32),

      scale=np.ones(M, dtype=np.float32)

    )

    Z_std = standard_normal.sample(1)

    current_layer_value = Z_std

    for layer in self.decoder_layers:

      current_layer_value = layer.forward(current_layer_value)

    logits = current_layer_value

    prior_predictive_dist = Bernoulli(logits=logits)

    self.prior_predictive = prior_predictive_dist.sample()

    self.prior_predictive_probs = tf.nn.sigmoid(logits)

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

    # prior predictive from input

    # only used for generating visualization

    self.Z_input = tf.placeholder(tf.float32, shape=(None, M))

    current_layer_value = self.Z_input

    for layer in self.decoder_layers:

      current_layer_value = layer.forward(current_layer_value)

    logits = current_layer_value

    self.prior_predictive_from_input_probs = tf.nn.sigmoid(logits)

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

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

    # now build the cost

    kl = tf.reduce_sum(

      tf.contrib.distributions.kl_divergence(

        self.Z.distribution, standard_normal

      ),

      1

    )

    expected_log_likelihood = tf.reduce_sum(

      self.X_hat_distribution.log_prob(self.X),

      1

    )

    # equivalent

    # expected_log_likelihood = -tf.nn.sigmoid_cross_entropy_with_logits(

    #   labels=self.X,

    #   logits=posterior_predictive_logits

    # )

    # expected_log_likelihood = tf.reduce_sum(expected_log_likelihood, 1)

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

    self.elbo = tf.reduce_sum(expected_log_likelihood – kl)

    self.train_op = tf.train.RMSPropOptimizer(learning_rate=0.001).minimize(-self.elbo)

    # set up session and variables for later

    self.init_op = tf.global_variables_initializer()

    self.sess = tf.InteractiveSession()

    self.sess.run(self.init_op)

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

  def fit(self, X, epochs=30, batch_sz=64):

    costs = []

    n_batches = len(X) // batch_sz

    print(“n_batches:”, n_batches)

    for i in range(epochs):

      print(“epoch:”, i)

      np.random.shuffle(X)

      for j in range(n_batches):

        batch = X[j*batch_sz:(j+1)*batch_sz]

        _, c, = self.sess.run((self.train_op, self.elbo), feed_dict={self.X: batch})

        c /= batch_sz # just debugging

        costs.append(c)

        if j % 100 == 0:

          print(“iter: %d, cost: %.3f” % (j, c))

    plt.plot(costs)

    plt.show()

Затем идёт функция transform, которая преобразовывает входные данные X в соответствующие скрытые векторы Z. Обратите внимание, что здесь используются средние значения.

  def transform(self, X):

    return self.sess.run(

      self.means,

      feed_dict={self.X: X}

    )

И наконец, идут три функции для трёх операций, определённых нами ранее: одна для получения априорного прогнозного значения по заданному Z, одна – для получения априорного прогнозного значения из Z, выбранного из его априорной вероятности, и одна – для получения апостериорной прогнозной выборки.

  def prior_predictive_with_input(self, Z):

    return self.sess.run(

      self.prior_predictive_from_input_probs,

      feed_dict={self.Z_input: Z}

    )

  def posterior_predictive_sample(self, X):

    # returns a sample from p(x_new | X)

    return self.sess.run(self.posterior_predictive, feed_dict={self.X: X})

  def prior_predictive_sample_with_probs(self):

    # returns a sample from p(x_new | z), z ~ N(0, 1)

    return self.sess.run((self.prior_predictive, self.prior_predictive_probs))

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

Далее идёт цикл, выводящий на экран восстановленные данные из случайно выбранных данных учебного набора. Он написан таким образом, чтобы исходные и восстановленные данные выводились рядом друг с другом, а мы могли просто нажать букву «n», чтобы завершить цикл. Затем идёт цикл, генерирующий априорные прогнозные выборки.

Обратите внимание, что здесь нет входных данных, поскольку Z является лишь выборкой из стандартного нормального распределения; в нём также результаты выводятся рядом друг с другом, так что мы можем увидеть и образец, и среднее значение, из которого этот образец был сгенерирован. В этом цикле также можно нажать «n», чтобы выйти из него.

def main():

  X, Y = util.get_mnist()

  # convert X to binary variable

  X = (X > 0.5).astype(np.float32)

  vae = VariationalAutoencoder(784, [200, 100])

  vae.fit(X)

  # plot reconstruction

  done = False

  while not done:

    i = np.random.choice(len(X))

    x = X[i]

    im = vae.posterior_predictive_sample([x]).reshape(28, 28)

    plt.subplot(1,2,1)

    plt.imshow(x.reshape(28, 28), cmap=’gray’)

    plt.title(“Original”)

    plt.subplot(1,2,2)

    plt.imshow(im, cmap=’gray’)

    plt.title(“Sampled”)

    plt.show()

    ans = input(“Generate another?”)

    if ans and ans[0] in (‘n’ or ‘N’):

      done = True

  # plot output from random samples in latent space

  done = False

  while not done:

    im, probs = vae.prior_predictive_sample_with_probs()

    im = im.reshape(28, 28)

    probs = probs.reshape(28, 28)

    plt.subplot(1,2,1)

    plt.imshow(im, cmap=’gray’)

    plt.title(“Prior predictive sample”)

    plt.subplot(1,2,2)

    plt.imshow(probs, cmap=’gray’)

    plt.title(“Prior predictive probs”)

    plt.show()

    ans = input(“Generate another?”)

    if ans and ans[0] in (‘n’ or ‘N’):

      done = True

if __name__ == ‘__main__’:

  main()

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

Приём с репараметризацией

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

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

Решением является приём с репараметризацией. Нечто подобное мы видели ранее, в моём самом первом курсе цикла «Инструментарий Numpy на языке Python». Одно из упражнений, выполнявшихся в нём, было осуществление выборки из произвольного гауссового распределения с применением одной лишь библиотеки Numpy. Мы знаем, что в Numpy имеется функция np.random.randn, дающая выборку из распределения N(0, 1). Но что делать, если нам нужно сделать выборку из N(μ, σ2) и произвольными средним значением и дисперсией?

Мы всё также используем randn, но умножаем результат на σ и добавляем μ. Если вы сомневаетесь, что это даст выборку из желаемого распределения, предлагаю сгенерировать ряд выборок в коде, а затем вычислить их среднее значение и дисперсию. Вы должны обнаружить, что все они имеют ожидаемые значения.

Интуитивно понятным способом понять, почему это работает, – представить обратную операцию. Как вы знаете, мы любим стандартизовать, или нормализовать, данные, прежде чем использовать на них алгоритмы машинного обучения. Для этого мы вычитаем среднее значение и делим результат на стандартное отклонение, что преобразует данные из переменных с произвольным средним значением и дисперсией в переменные со средним значением 0 и дисперсией 1. Следовательно, если z имеет среднее значение 0 и дисперсию 1, можно переписать уравнение через x, и, разумеется, x имеет среднее значение μ и дисперсию σ2, как и было по определению:

Всё тут, наверное, кажется очень простым, зачем же это обсуждать? Для начала предположим, что кодировщик выдаёт на выходе μ и σ. Покажем в явном виде, что μ и σ являются функциями от x и θ – параметров кодировщика:

Всё тут, наверное, кажется очень простым, зачем же это обсуждать? Для начала предположим, что кодировщик выдаёт на выходе μ и σ. Покажем в явном виде, что μ и σ являются функциями от x и θ – параметров кодировщика:

А вот представление q(z|x):

Не забывайте, что после того, как мы найдём q(z|x), представляющее собой гауссово распределение со средним значением μ и стандартным отклонением σ, мы должны сделать выборку из этого распределения. Посмотрим, что получится, если использовать наш метод Numpy для получения этой выборки. Мы на самом деле возьмём выборку из N(0, 1), умноженную на σ и с добавлением μ:

Чем же это нам может помочь? Чтобы получить восстановленные данные, мы должны подставить z в декодировщик. Но тут важно то, что в такой формулировке z не является случайным относительно θ, которое представляет переменные в кодировщике, которые нужно обновлять. Другими словами, если у нас есть целевая функция J, то мы можем легко вычислить градиент относительно θ – это просто формула нахождения производной сложной функции:

где ε – число, сгенерированное при прямом прохождении, так что относительно θ являющееся константой.

Вот почему я всегда говорю, что использование библиотеки Theano позволяет напрямую реализовать изученное, и вы всегда можете творить с нуля, пользуясь лишь базовыми «строительными блоками». Использование же Tensorflow больше походит на упражнение по чтению документации: необходимо изучить документацию, чтобы отыскать то, что делает нужную вещь, или найти существующий код, который может послужить примером того, что вы хотели бы сделать. Так что временами библиотека Theano несколько менее лаконична, зато на самом деле в конечном счёте легче концептуально.

Андрей Никитенко
Андрей Никитенко
Задать вопрос эксперту
Понравилась статья? Поделить с друзьями: