Современные методы регуляризации

Исключающая регуляризация

Здравствуйте и вновь добро пожаловать на занятия по теме «Обработка данных: практическое глубокое обучение в Theano и TensorFlow».

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

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

Что в данном случае понимается под «различными»? Для разных ансамблевых методов это может означать разные вещи. Один из простейших способов – обучать модель на случайных подмножествах данных. Этот метод хорош, когда алгоритм не масштабируется. К примеру, если у вас есть 1 миллион наблюдений, вы можете обучать 10 различных версий деревьев решений, каждая из которых обучается на 100 000 наблюдений. Получится ансамбль деревьев решений, а для прогноза можно взять наиболее часто встречающийся ответ этих 10 различных деревьев решений. Это первый способ.

Другой способ – использовать не все признаки. Например, если у нас есть 100 признаков, то каждое из 10 деревьев решений будет использовать лишь 10 признаков. Тогда вместо того, чтобы обучать одно древо решений на матрице размерностью [1 млн. x 100], мы будем обучать 10 деревьев решений с матрицами размерностью [100 тыс. x 10], взятых из исходной матрицы. Что замечательно, это приводит к лучшему результату, чем просто обучение одного древа решений. Как вы увидите, метод исключения более похоже на ансамбль с подмножествами признаков.

Итак, как именно работает метод исключения?

Современные методы регуляризации

Как я уже говорил, мы используем лишь часть признаков, но делаем это не только во входном слое, а в каждом. В каждом слое мы случайным образом выбираем, какой из узлов исключить. Для этого используется вероятность p(drop) или, иногда, противоположную вероятность p(keep), показывающие вероятность исключения или оставления узла. Обычными значениями для p(keep) являются 0,8 для входного слоя и 0,5 для скрытых слоёв.

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

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

Давайте подумаем, что представляет из себя получающийся «ансамбль». Пусть в нашей нейронной сети всего N узлов (это количество входных узлов плюс количество скрытых узлов в каждом слое). Каждый из них может иметь два состояния: вкл/выкл, исключён/оставлен. Это значит, что в общем у нас есть 2N различных возможных конфигураций. Таким образом, мы приближённо имеем ансамбль из 2N различных нейронных сетей.

Почему приближённо? Но давайте представим, что мы не делаем приближений. Возьмём весьма небольшую нейронную сеть – с 100 узлов. Это очень маленькая сеть. Для сравнения, при решении задачи вроде MNIST у нас может быть около 1 000 узлов –примерно 700 для входного слоя и примерно 300 для скрытого слоя – и это лишь в случае, если для MNIST мы используем только один скрытый слой. Тогда для нашей нейронной сети из 100 узлов – что в 10 раз меньше – мы имеем 2100 ≈ 1,3*1030 различных вариантов. Представьте себе обучение такого количества нейронных сетей! Очевидно, это совершенно невозможно.

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

Базовый подход в Theano состоит в том, что вместо того чтобы на самом деле исключать узел из нейронной сети, что привело бы к другому вычислительному графу, с которым библиотека не справится, Theano просто умножает его исходящую величину на нуль. Это приводит к тому же самому эффекту, так как исходящая величина, умноженная на нуль, даёт в результате нуль. Поскольку в каждом слое у нас будет матрица размерности NxD, где N равно величине пакета, нам потребуется создать случайную матрицу из нулей и единиц также размерности NxD, чтобы умножить её на слой. Такая матрица из нулей и единиц называется маской. Если вы когда-либо занимались низкоуровневым кодированием на C, вы, вероятно, уже знакомы с этой концепцией, когда часто приходится работать с побитовыми операциями и бит-маски оказываются очень полезной вещью.

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

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

Рассмотрим работающий код. Если вы желаете глянуть на него, зайдите на Github. Соответствующие файлы называются dropout_theano.py для версии с Theano и dropout_tensorflow.py для версии с TensorFlow. Использоваться будут те же данные, что и прежде в этом курсе.

Итак, в самом начале мы, кроме всего прочего, из theano.tensor.shared_randomstreams импортируем RandomStreams:

import numpy as np

import theano

import theano.tensor as T

import matplotlib.pyplot as plt

from theano.tensor.shared_randomstreams import RandomStreams

from util import get_normalized_data

from sklearn.utils import shuffle

Обратите внимание, что скрытый слой определён в точности так же, как всегда, поскольку я решил не вставлять внутрь метод исключения, а оставить «снаружи».

class HiddenLayer(object):

    def __init__(self, M1, M2, an_id):

        self.id = an_id

        self.M1 = M1

        self.M2 = M2

        W = np.random.randn(M1, M2) / np.sqrt(M1 + M2)

        b = np.zeros(M2)

        self.W = theano.shared(W, ‘W_%s’ % self.id)

        self.b = theano.shared(b, ‘b_%s’ % self.id)

        self.params = [self.W, self.b]

    def forward(self, X):

        return T.nnet.relu(X.dot(self.W) + self.b)

Конструктор теперь включает дополнительный параметр p_keep, который, разумеется, определяет вероятность того, что узел будет оставлен, а не исключён. В действительности это целый список вероятностей, поскольку каждый слой может иметь свою вероятность оставления. Большая часть кода остаётся прежней, но теперь мы создаёт новый объект типа RandomStreams.

class ANN(object):

    def __init__(self, hidden_layer_sizes, p_keep):

        self.hidden_layer_sizes = hidden_layer_sizes

        self.dropout_rates = p_keep

    def fit(self, X, Y, learning_rate=10e-7, mu=0.99, decay=0.999, epochs=300, batch_sz=100, show_fig=False):

        # make a validation set

        X, Y = shuffle(X, Y)

        X = X.astype(np.float32)

        Y = Y.astype(np.int32)

        Xvalid, Yvalid = X[-1000:], Y[-1000:]

        X, Y = X[:-1000], Y[:-1000]

        self.rng = RandomStreams()

Инициация скрытых слоёв и весовых коэффициентов остаётся прежней.

        N, D = X.shape

        K = len(set(Y))

        self.hidden_layers = []

        M1 = D

        count = 0

        for M2 in self.hidden_layer_sizes:

            h = HiddenLayer(M1, M2, count)

            self.hidden_layers.append(h)

            M1 = M2

            count += 1

        W = np.random.randn(M1, K) * np.sqrt(M1 + K)

        b = np.zeros(K)

        self.W = theano.shared(W, ‘W_logreg’)

        self.b = theano.shared(b, ‘b_logreg’)

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

        # collect params for later use

        self.params = [self.W, self.b]

        for h in self.hidden_layers:

            self.params += h.params

        # for momentum

        dparams = [theano.shared(np.zeros(p.get_value().shape)) for p in self.params]

        # for rmsprop

        cache = [theano.shared(np.zeros(p.get_value().shape)) for p in self.params]

        # set up theano functions and variables

        thX = T.matrix(‘X’)

        thY = T.ivector(‘Y’)

        pY_train = self.forward_train(thX)

Все обновления относятся к функции forward_train, поскольку исключение происходит именно во время обучения.

        # this cost is for training

        cost = -T.mean(T.log(pY_train[T.arange(thY.shape[0]), thY]))

        updates = [

            (c, decay*c + (1-decay)*T.grad(cost, p) *T.grad(cost, p)) for p, dp in zip(self.params, dparams)

        ] + [

            (dp, new_dp) for dp, new_dp in zip(dparams, new_dparams)

        ] + [

            (p, p + new_dp) for p, new_dp in zip(self.params, new_dparams)

        ]

        train_op = theano.function(

            inputs=[thX, thY],

            updates=updates

        )

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

        pY_predict = self.forward_predict(thX)

        cost_predict = -T.mean(T.log(pY_predict[T.arange(thY.shape[0]), thY]))

        prediction = self.predict(thX)

        cost_predict_op = theano.function(inputs=[thX, thY], outputs=[cost_predict, prediction])

        n_batches = N / batch_sz

        costs = []

        for i in xrange(epochs):

            X, Y = shuffle(X, Y)

            for j in xrange(n_batches):

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

                Ybatch = Y[j*batch_sz:(j*batch_sz+batch_sz)]

                train_op(Xbatch, Ybatch)

                if j % 20 == 0:

                    c, p = cost_predict_op(Xvalid, Yvalid)

                    costs.append(c)

                    e = error_rate(Yvalid, p)

                    print(“i:”, i, “j:”, j, “nb:”, n_batches, “cost:”, c, “error rate:”, e)

       

        if show_fig:

            plt.plot(costs)

            plt.show()

Прокрутив ещё чуть дальше, мы встретим функции forward_train и forward_predict. Здесь мы генерируем маску с использованием объекта RandomStreams. В функции forward_predict маски нет, мы лишь умножаем каждый слой на величину p_keep (вероятность того, что узел будет оставлен). Именно поэтому использовать вероятность оставления узла предпочтительнее, чем вероятность его исключения, поскольку p_keep во всех случаях можно прямо вставлять в код.

    def forward_train(self, X):

        Z = X

        for h, p in zip(self.hidden_layers, self.dropout_rates[:-1]):

            mask = self.rng.binomial(n=1, p=p, size=Z.shape)

            Z = mask * Z

            Z = h.forward(Z)

        mask = self.rng.binomial(n=1, p=self.dropout_rates[-1], size=Z.shape)

        Z = mask * Z

        return T.nnet.softmax(Z.dot(self.W) + self.b)

    def forward_predict(self, X):

        Z = X

        for h, p in zip(self.hidden_layers, self.dropout_rates[:-1]):

            Z = h.forward(p * Z)

        return T.nnet.softmax((self.dropout_rates[-1] * Z).dot(self.W) + self.b)

    def predict(self, X):

        pY = self.forward_predict(X)

        return T.argmax(pY, axis=1)

И наконец, в конце мы устанавливаем обычные значения для p_keep – 0,8 для входного слоя и 0,5 для остальных слоёв.

def error_rate(p, t):

    return np.mean(p != t)

def relu(a):

    return a * (a > 0)

def main():

    # step 1: get the data and define all the usual variables

    X, Y = get_normalized_data()

    ann = ANN([500, 300], [0.8, 0.5, 0.5])

    ann.fit(X, Y, show_fig=True)

if __name__ == ‘__main__’:

main()

Теперь обратимся к коду с TensorFlow. Структура кода остаётся точно такой же, а различия касаются лишь внутренних деталей. У нас есть класс HiddenLayer, ничего «не знающий» об исключении, и класс ANN, содержащий величину p_keep.

import numpy as np

import tensorflow as tf

import matplotlib.pyplot as plt

from util import get_normalized_data

from sklearn.utils import shuffle

class HiddenLayer(object):

    def __init__(self, M1, M2):

        self.M1 = M1

        self.M2 = M2

        W = np.random.randn(M1, M2) / np.sqrt(M1 + M2)

        b = np.zeros(M2)

        self.W = tf.Variable(W.astype(np.float32))

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

        self.params = [self.W, self.b]

    def forward(self, X):

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

class ANN(object):

    def __init__(self, hidden_layer_sizes, p_keep):

        self.hidden_layer_sizes = hidden_layer_sizes

        self.dropout_rates = p_keep

    def fit(self, X, Y, lr=10e-7, mu=0.99, decay=0.999, epochs=300, batch_sz=100):

        # make a validation set

        X, Y = shuffle(X, Y)

        X = X.astype(np.float32)

        Y = Y.astype(np.int64)

            Xvalid, Yvalid = X[-1000:], Y[-1000:]

            X, Y = X[:-1000], Y[:-1000]

        # initialize hidden layers

        N, D = X.shape

        K = len(set(Y))

        self.hidden_layers = []

        M1 = D

        for M2 in self.hidden_layer_sizes:

            h = HiddenLayer(M1, M2)

            self.hidden_layers.append(h)

            M1 = M2

        W = np.random.randn(M1, K) / np.sqrt(M1 + K)

        b = np.zeros(K)

        self.W = tf.Variable(W.astype(np.float32))

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

        # collect params for later use

        self.params = [self.W, self.b]

        for h in self.hidden_layers:

            self.params += h.params

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

        # set up theano functions and variables

        inputs = tf.placeholder(tf.float32, shape=(None, D), name=’inputs’)

        labels = tf.placeholder(tf.int64, shape=(None,), name=’labels’)

        logits = self.forward(inputs)

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

        cost = tf.reduce_mean(tf.nn.sparse_softmax_cross_entropy_with_logits(logits=logits, labels=labels))

        train_op = tf.train.RMSPropOptimizer(lr, decay=decay, momentum=mu).minimize(cost)

        prediction = self.predict(inputs)

И, конечно же, обучающий цикл остаётся обычным, поскольку функции train_op и predict_op уже определены.

        n_batches = N / batch_sz

        costs = []

        init = tf.initialize_all_variables ()

        with tf.Session() as session:

            session.run(init)

            for i in xrange(epochs):

                print(“epoch:”, i, “n_batches:”, n_batches)

                X, Y = shuffle(X, Y)

                for j in xrange(n_batches):

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

                    Ybatch = Y[j*batch_sz:(j*batch_sz+batch_sz)]

                    session.run(train_op, feed_dict={inputs: Xbatch, labels: Ybatch})

                    if j % print_every == 0:

                        c = session.run(cost, feed_dict={inputs: Xvalid, labels: Yvalid})

                        p = session.run(prediction, feed_dict={inputs: Xvalid})

                        costs.append(c)

                        e = error_rate(Yvalid, p)

                        print(“i:”, i, “j:”, j, “nb:”, n_batches, “cost:”, c, “error rate:”, e)

       

        plt.plot(costs)

        plt.show()

Прокрутив текст чуть далее, мы обнаружим функции forward_train и forward_predict. Forward_predict очень похожа на прежний вариант – мы лишь умножаем на p_keep в каждом слое. А вот forward_train весьма отличается от варианта в Theano – тут имеется функция dropout, принимающая в качестве аргументов матрицу входных данных и вероятность оставления узла.

    def forward_train(self, X):

        Z = X

        Z = tf.nn.dropout(Z, self.dropout_rates[0])

        for h, p in zip(self.hidden_layers, self.dropout_rates[1:]):

            Z = h.forward(Z)

            Z = tf.nn.dropout(Z, p)

        return tf.matmul(Z, self.W) + self.b

    def forward_predict(self, X):

        Z = X * self.dropout_rates[0]

        for h, p in zip(self.hidden_layers, self.dropout_rates[1:]):

            Z = h.forward(Z)

        return tf.matmul(Z, self.W) + self.b

Весь остальной код остаётся совершенно идентичным прежнему.

    def predict(self, X):

        pY = self.forward_test(X)

        return tf.argmax(pY, 1)

def error_rate(p, t):

    return np.mean(p != t)

def relu(a):

    return a * (a > 0)

def main():

    # step 1: get the data and define all the usual variables

    X, Y = get_normalized_data()

 

    ann = ANN([500, 300], [0.8, 0.5, 0.5])

    ann.fit(X, Y)

if __name__ == ‘__main__’:

main()

Запускаем программу и смотрим на результат:

Современные методы регуляризации

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

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