Содержание страницы
Кросс-энтропийная функция и расстояние Кульбака-Лейблера
Здравствуйте и вновь добро пожаловать на занятия по теме «Глубокое обучение без учителя: глубокое обучение на языке Python, часть 4».
Ранее мы использовали кросс-энтропийную функцию в качестве функции ошибок для задач классификации, а также расстояние Кульбака-Лейблера в качестве функции ошибок для сравнения двух распределений вероятностей, как в методе t-SNE. В лекциях, посвящённых автокодировщикам, мы использовали кросс-энтропийную функцию ошибок даже несмотря на то, что целевые переменные не были строго разделены на классы из нулей или единиц; при этом результат показывал, что она работает. Эта лекция и даст вам понимание, почему же она работает, а также покажет связь между кросс-энтропийной функцией и функцией ошибок, использующейся в методе t-SNE, то есть с расстоянием Кульбака-Лейблера. Но чтобы всё это понять, нам необходимо сначала углубиться в теорию информации.
Итак, что такое энтропия?
Если вы изучали физику, то можете сказать, что энтропия – это мера беспорядка:
где i – состояние системы, pi – вероятность пребывания в состоянии i.
Информационная энтропия измеряет то же самое – меру беспорядка:
Обратите внимание, что информационная энтропия на самом деле представляет собой ожидаемое значение отрицательного логарифма p в распределении p. Это очень похоже на то, что мы узнали при изучении метода главных компонент. Напомню, что в методе главных компонент мы также пытаемся упорядочить исходящую информацию, с тем разве что исключением, что для измерения информации в нём используется дисперсия.
При этом важно иметь в виду, что в обоих случаях наши меры равны нулю, когда переменная является не случайной, а заданной, и в обоих случаях они возрастают по мере увеличения непредсказуемости случайной величины.
Чтобы продемонстрировать этот факт, максимизируем дисперсию в распределении Бернулли, а заодно и информационную энтропию. Напомню, что при распределении Бернулли случайная переменная может принимать только два значения – 0 и 1. Обозначим вероятность единицы через p, тогда вероятность нуля будет равна 1 – p:
Тогда дисперсия будет равна
Чтобы найти p, при котором дисперсия будет максимальной, возьмём производную относительно p и приравняем к нулю:
Отсюда находим, что p = 1/2.
Теперь давайте вычислим p, при котором энтропия будет оптимальной. Подставив p в выражение для энтропии, получим:
Найдём решение тем же способом – взяв производную и приравняв её к нулю:
И снова найдём, что искомое значение p = 1/2.
Чудесное обобщение этой задачи – определить оптимальное распределение, когда случайная переменная может принимать более одного значения, скажем, K значений. Вы можете также попробовать решить эту задачу для непрерывной случайной переменной. Если хотите попробовать свои силы в решении такого рода теоретических задач, попробуйте отыскать их самостоятельно. Вы обнаружите, что оптимальный результат даёт равномерное распределение.
Итак, теперь когда вы знаете, что такое энтропия, вернёмся к понятию кросс-энтропии. Неудивительно, что это то же самое выражение, но с двумя разными распределениями вероятностей:
Это естественный способ обобщения задачи. С помощью кросс-энтропийной функции ошибок вместо одного распределения вероятностей, являющихся целевыми переменными и принимающими значения только 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()
Итак, запустим программу и посмотрим, что получится.