Написание кода для класса автокодировщиков. Часть 2

Кросс-энтропийная функция и расстояние Кульбака-Лейблера

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

Ранее мы использовали кросс-энтропийную функцию в качестве функции ошибок для задач классификации, а также расстояние Кульбака-Лейблера в качестве функции ошибок для сравнения двух распределений вероятностей, как в методе t-SNE. В лекциях, посвящённых автокодировщикам, мы использовали кросс-энтропийную функцию ошибок даже несмотря на то, что целевые переменные не были строго разделены на классы из нулей или единиц; при этом результат показывал, что она работает. Эта лекция и даст вам понимание, почему же она работает, а также покажет связь между кросс-энтропийной функцией и функцией ошибок, использующейся в методе t-SNE, то есть с расстоянием Кульбака-Лейблера. Но чтобы всё это понять, нам необходимо сначала углубиться в теорию информации.

Итак, что такое энтропия?

Если вы изучали физику, то можете сказать, что энтропия – это мера беспорядка:

S=-k_B \sum_i p_i \;log \;p_i,

где i – состояние системы, pi – вероятность пребывания в состоянии i.

Информационная энтропия измеряет то же самое – меру беспорядка:

H(P)=-\sum_i p_i \;log \;p_i=E_p[-log P].

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

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

Чтобы продемонстрировать этот факт, максимизируем дисперсию в распределении Бернулли, а заодно и информационную энтропию. Напомню, что при распределении Бернулли случайная переменная может принимать только два значения – 0 и 1. Обозначим вероятность единицы через p, тогда вероятность нуля будет равна 1 – p:

P(X=1)=p, P (X=0)=1-p.

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

\sigma^{2}=\sum_x (X-\mu)^2 P (X)=p^2(1-p)+(1-p)^2p.

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

\frac{\partial \sigma^2}{\partial p}=2p(1-p)-p^2-2(1-p)p+1(1-p^2)=2p-2p^2-p^2-2p+2p^2+1-2p+p^2=1-2p=0.

Отсюда находим, что p = 1/2.

Теперь давайте вычислим p, при котором энтропия будет оптимальной. Подставив p в выражение для энтропии, получим:

H=-p \;log \; p-(1-p) \;log(1-p).

Найдём решение тем же способом – взяв производную и приравняв её к нулю:

И снова найдём, что искомое значение p = 1/2.

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

Итак, теперь когда вы знаете, что такое энтропия, вернёмся к понятию кросс-энтропии. Неудивительно, что это то же самое выражение, но с двумя разными распределениями вероятностей:

H(P,Q)=-\sum_i p_i \;log \;q_i=E_p(-log \;Q).

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

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

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

Другими словами, кросс-энтропия и расстояние Кульбака-Лейблера эквивалентны с точностью до добавочной константы.

Чем это может нас заинтересовать?

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

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

Глубокий автокодировщик. Описание визуализации

Теперь я дам вам краткое описание следующего упражнения по написанию кода. Мы создадим, по сути, глубокий автокодировщик, но «глубокий» не в том смысле, что будем создавать вложенные автокодировщики, а в том смысле, что будет много слоёв между входом и выходом. Архитектура сети, которую мы постараемся построить, похожа на букву «X», так что назовём её X-образной.

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

Код для этого примера можно найти по адресу github.com/lazyprogrammer/machine_learning_examples в папке unsupervised_class2. Нужный файл называется xwing.py.

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

Визуализация глубокого автокодировщика в коде

В заключении мы напишем код для X-образного автокодировщика.

Итак, начинаем с импорта обычных библиотек, а кроме того, из файла util импортируем функции import relu, error_rate, getKaggleMNIST, init_weights.

import numpy as np

import theano

import theano.tensor as T

import matplotlib.pyplot as plt

from sklearn.utils import shuffle

from util import relu, error_rate, getKaggleMNIST, init_weights

Далее определим класс слоя для нашего многослойного персептрона.

class Layer(object):

    def __init__(self, m1, m2):

        W = init_weights((m1, m2))

        bi = np.zeros(m2)

        bo = np.zeros(m1)

        self.W = theano.shared(W)

        self.bi = theano.shared(bi)

        self.bo = theano.shared(bo)

        self.params = [self.W, self.bi, self.bo]

Теперь напишем знакомую нам функцию forward и функцию forwardT для транспонированных величин. Она будет вызываться чуть дальше.

    def forward(self, X):

        return T.nnet.sigmoid(X.dot(self.W) + self.bi)

    def forwardT(ш):

        return T.nnet.sigmoid(X.dot(self.W.T) + self.bo)

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

class DeepAutoEncoder(object):

    def __init__(self, hidden_layer_sizes):

        self.hidden_layer_sizes = hidden_layer_sizes

Для функции fit используем коэффициент обучения 0,5, импульс 0,99, количество эпох 50, количество пакетов 100.

    def fit(self, X, learning_rate=0.5, mu=0.99, epochs=50, batch_sz=100, show_fig=False):

        N, D = X.shape

        n_batches = N / batch_sz

        mi = D

        self.layers = []

        self.params = []

        for mo in self.hidden_layer_sizes:

            layer = Layer(mi, mo)

            self.layers.append(layer)

            self.params += layer.params

            mi = mo

        X_in = T.matrix(‘X’)

        X_hat = self.forward(X_in)

В качестве функции затрат используем кросс-энтропийную функцию.

cost = -(X_in * T.log(X_hat) + (1 – X_in) * T.log(1 – X_hat)).mean()

cost_op = theano.function(

     inputs=[X_in],

     outputs=cost,

 )

Следующая задача – обновления для градиентного спуска.

dparams = [theano.shared(p.get_value()*0) for p in self.params]

grads = T.grad(cost, self.params)

    updates = [

        (p, p + mu*dp – learning_rate*g) for p, dp, g in zip(self.params, self.dparams, grads)

    ] + [

        (dp, mu*dp – learning_rate*g) for for dp, g in zip(dparams, grads)

    ]

        train_op = theano.function(

            inputs=[X_in],

            outputs=cost,

            updates=updates,

        )

Теперь мы готовы перейти к основному обучающему циклу.

  costs = []

  for i in xrange(epochs):

            print “epoch:”, i

            X = shuffle(X)

            for j in xrange(n_batches):

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

                c = train_op(batch)

                if j % 100 == 0:

                    print “j / n_batches:”, j, “/”, n_batches, “cost:”, c

                costs.append(c)

        if show_fig:

            plt.plot(costs)

            plt.show()

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

def forward(self, X):

       Z = X

  for layer in self.layers:

       Z = layer.forward(Z)

И напишем специальную функцию Theano, которую назовём self.map2center. В качестве аргумента она принимает наши X, а выдаёт наше текущее Z. Это будет как раз посередине нашей нейронной сети.

 self.map2center = theano.function(

      inputs=[X],

      outputs=Z,

 )

Теперь можно переходить ко второй части нашей сети.

  for i in xrange(len(self.layers) -1, -1, -1):

        Z = self.layers[i].forwardT(Z)

  return Z

Теперь мы готовы написать функцию main.

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

def main():

    Xtrain, Ytrain, Xtest, Ytest = getKaggleMNIST()

    dae = DeepAutoEncoder([500, 300, 2])

    dae.fit(Xtrain)

    mapping = dae.map2center(Xtrain)

    plt.scatter(mapping[:,0], mapping[:,1], c=Ytrain, s=100, alpha=0.5)

    plt.show()

if __name__ == ‘__main__’:

    main()

Итак, запустим программу и посмотрим, что получится.

Визуализация глубокого автокодировщика в коде

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

;-) :| :x :twisted: :smile: :shock: :sad: :roll: :razz: :oops: :o :mrgreen: :lol: :idea: :grin: :evil: :cry: :cool: :arrow: :???: :?: :!:

Share via
Copy link