Решение проблемы четкости

Проблема чётности – навороченное XOR

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

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

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

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

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

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

Теперь подумаем о том, как можно решить эту задачу с помощью рекуррентных нейронных сетей. Основная идея заключается в том, что нам нужно, чтобы нейронная сеть отслеживала своё состояние. То есть, если на выходе нуль, а на входе мы получаем очередную единицу, то она должна переключиться на единицу. Если же на выходе единица, а на входе у нас опять очередная единица, то она должна переключиться на нуль. Ну, а если на входе нуль, то на выходе сохраняется текущее значение. Нам нужно, чтобы получилось именно так. Но сможет ли нейронная сеть действительно научиться этому? Я покажу, что да, сможет.

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

Рассмотрим пример. Пусть наши данные имеют размерность 4, так что возможными входными данными будут 0000, 0001, 0010, 0011 и т. д. Целевая переменная будет принимать значения 0, 1, 1, 0 и так далее. Преобразовав эти целевые переменные в целевые последовательности, которые нам нужны для рекуррентной сети, получим 0000, 0001, 0011, 0010 и т. д. Если вы не можете этого понять, поставьте видео на паузу, пока не убедитесь, что точно поняли, почему каждая целевая последовательность именно такова. Помните, что целью является отслеживание количества получаемых единиц – чётное или нечётное.

Проблема четности в коде с использованием ИНН прямого распространения

Мы используем обычную нейронную сеть прямого распространения для решения проблемы чётности. Если вы не хотите писать код сами или где-то допустили ошибку, найдите файл mlp_parity.py в репозитарии Github.

И первое, что мы делаем, – это, как обычно, импорт библиотек.

import numpy as np

import theano

import theano.tensor as T

import matplotlib.pyplot as plt

from util import init_weight, all_parity_pairs

from sklearn.utils import shuffle

Тут я сделаю небольшое отступление и покажу, что находится в файле util.py. Функцию init_weight вы уже видели, она всего лишь инициирует значения весовых коэффициентов. Коэффициенты имеют случайное распределение и достаточно малы, чтобы градиентный спуск работал корректно. В качестве аргументов используются входящий размер (Mi) и исходящий (Mo):

def init_weight(Mi, Mo):

    return np.random.randn(Mi, Mo) / np.sqrt(Mi + Mo)

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

def all_parity_pairs(nbit):

    # total number of samples (Ntotal) will be a multiple of 100

    # why did I make it this way? I don’t remember.

    N = 2**nbit

    remainder = 100 – (N % 100)

    Ntotal = N + remainder

    X = np.zeros((Ntotal, nbit))

    Y = np.zeros(Ntotal)

    for ii in xrange(Ntotal):

        i = ii % N

        # now generate the ith sample

        for j in xrange(nbit):

            if i % (2**(j+1)) != 0:

                i -= 2**j

                X[ii,j] = 1

        Y[ii] = X[ii].sum() % 2

    return X, Y

Возвращаемся к файлу mlp_parity. Для начала инициируем класс HiddenLayer.

class HiddenLayer:

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

        self.id = an_id

        self.M1 = M1

        self.M2 = M2

        W = init_weight(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]

Единственная функция, которая здесь используется, – функция forward. В качестве функции активации используем relu.

    def forward(self, X):

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

Класс ANN:

class ANN:

    def __init__(self, hidden_layer_sizes):

        self.hidden_layer_sizes = hidden_layer_sizes

    def fit(self, X, Y, learning_rate=10e-3, mu=0.99, reg=10e-12, eps=10e-10, epochs=400, batch_sz=20, print_period=1, show_fig=False):

        Y = Y.astype(np.int32)

        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 = init_weight(M1, K)

        b = np.zeros(K)

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

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

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

        for h in self.hidden_layers:

            self.params += h.params

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

        thX = T.matrix(‘X’)

        thY = T.ivector(‘Y’)

        pY = self.forward(thX)

        rcost = reg*T.sum([(p*p).sum() for p in self.params])

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

        prediction = self.predict(thX)

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

        updates = [

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

        ] + [

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

        ]

        train_op = theano.function(

            inputs=[thX, thY],

            outputs=[cost, prediction],

            updates=updates,

        )

        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)]

                c, p = train_op(Xbatch, Ybatch)

                if j % print_period == 0:

                    costs.append(c)

                    e = np.mean(Ybatch != p)

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

        if show_fig:

            plt.plot(costs)

            plt.show()

    def forward(self, X):

        Z = X

        for h in self.hidden_layers:

            Z = h.forward(Z)

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

    def predict(self, X):

        pY = self.forward(X)

        return T.argmax(pY, axis=1)

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

Итак, функция wide для 12-битного набора данных с 2048 скрытыми узлами.

def wide():

    X, Y = all_parity_pairs(12)

    model = ANN([2048])

    model.fit(X, Y, learning_rate=10e-5, print_period=10, epochs=300, show_fig=True)

И другая функция deep со всего 1024 скрытыми узлами в слое, зато с двумя слоями.

def deep():

    # Challenge – find a deeper, slimmer network to solve the problem

    X, Y = all_parity_pairs(12)

    model = ANN([1024]*2)

    model.fit(X, Y, learning_rate=10e-4, print_period=10, epochs=100, show_fig=True)

Попробуем сначала с функцией wide.

if __name__ == ‘__main__’:

    wide()

# deep()

Запускаем файл.

Теперь запустим с более глубокой нейронной сетью.

if __name__ == ‘__main__’:

#   wide()

deep()

Введение в Theano Scan

Нам стоит не забыть поговорить об очень важной функцией scan из библиотеки Theano.

Чем она так важна? Задумаемся о том, как работает библиотека Theano. Мы создаём переменные и функционально их связываем, хотя они не имеют никаких значений, пока мы не запустим функцию. Поэтому когда мы создаём матрицу X, мы не указываем её размерность, а просто сообщаем, что это матрица, и Theano понимает, что это объект с двумя размерностями. Что будет, если мы захотим просмотреть все элементы X? Theano «не знает», где конец, поскольку мы ещё не сообщили ей этого. X всегда должен иметь определённую длину N; в таком случае мы можем использовать цикл for от 0 до N. В общем случае мы не можем точно знать длину наших обучающих последовательностей, поэтому если попробовать написать что-то вроде «for i in xrange(X.shape[0])», то ничего не получится, поскольку X.shape[0] пока не имеет значения. Вот тут-то и вступает в игру функция scan. Функция scan позволяет передавать длину конечной переменной как количество выполняемых циклов.

В некоторых случаях цикл for можно использовать, но всё равно лучше пользоваться функцией scan. Вот некоторые преимущества scan перед циклом for:

  • во-первых, позволяет количеству проходов цикла быть частью символьного графа Theano;
  • во-вторых, минимизирует количество обращений к графическому процессору, если тот задействован в вычислениях;
  • в-третьих, она может вычислять градиенты при помощи последовательных этапов вычислений;
  • в-четвёртых, она работает значительно быстрее циклa for для скомпилированной функции Theano;
  • в-пятых, она может снизить общее количество используемой памяти, определяя необходимый её объём.

Итак, рассмотрим строение функции scan. Вот она в своей простейшей форме:

outputs, updates = theano.scan(

    fn=some_function,

    sequences=thing_to_loop_over,

    n_steps=number_of_times_to_iterate,

)

def some_function(element):

    return do_something_to(element)

Первый аргумент – это некоторая функция, которая будет применяться к каждому элементу последовательности. Второй аргумент – фактическая последовательность. Таким образом, к каждому отдельному элементу последовательности будет применена одна и та же функция fn. n_steps – это количество повторов; как правило, это просто длина последовательности.

Сама функция scan возвращает две вещи – outputs и updates. Updates – это объект Theano типа упорядоченных обновлений. Обычно он просто игнорируется, так что больше не будем о нём говорить. Действительно интересным является outputs. Предполагается, что некоторая функция в результате работы возвращает некоторое значение функции. Так вот, выходом в данном случае будут все значения, объединённые вместе. Например, если в качестве аргумента будут числа 1, 2, 3, а функцией будет квадрат числа, то выходом будут числа 1, 4, 9.

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

outputs, updates = theano.scan(

    fn=reccurence,

    sequences=thing_to_loop_over,

    n_steps=number_of_times_to_iterate,

    outputs_info=[initial_value]

)

def reccurence(element, recurring_variable):

    return do_something_to(element, recurring_variable)

Аргумент outputs_info представляет начальные значения рекуррентных переменных. Обратите внимание, что они включены в список – это потому, что их может быть более одной, поэтому Theano ожидает здесь именно списка. В результате мы можем вычислить, например, последовательность Фибоначчи, в которой текущий член последовательности зависит от двух предыдущих. Заметьте, что при этом, когда рекурсия возвращает более одного элемента, исходящие переменные функции scan возвращают все значения в списке. На самом деле сколько аргументов вы укажете для рекурсии, столько и получите на выходе.

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

outputs, updates = theano.scan(

    fn=reccurence,

    sequences=thing_to_loop_over,

    n_steps=number_of_times_to_iterate,

    outputs_info=[initial_value]

    non_sequences=decay

)

def reccurence(x, last, decay):

    return (1-decay)*x + decay*last

Теперь попробуем реализовать это в коде и посмотрим, что получится. Сначала – файл scan1.py, который находится репозитарии github, на случай, если вам не хочется самим писать код.

Импортируем Numpy, Theano и Theano.tensor

import numpy as np

import theano

import theano.tensor as T

Назначим x в качестве вектора, что значит, что каждый элемент в списке будет скаляром.

x = T.vector(‘x’)

Определим функцию square, возвращающую x2.

def square(x):

  return x*x

И вызываем функцию scan.

outputs, updates = theano.scan(

  fn=square,

  sequences=x,

  n_steps=x.shape[0],

)

Теперь собственно функция Theano.

square_op = theano.function(

  inputs=[x],

  outputs=[outputs],

)

o_val = square_op(np.array([1, 2, 3, 4, 5]))

print(“output:”, o_val)

Запустим код и получим ожидаемый ответ.

Теперь перейдем к файлу scan2.py. Импорт библиотек остаётся прежним, но на этот раз мы попробуем вычислить последовательность Фибоначчи.

import numpy as np

import theano

import theano.tensor as T

N = T.iscalar(‘N’)

def recurrence(n, fn_1, fn_2):

  return fn_1 + fn_2, fn_1

outputs, updates = theano.scan(

  fn=recurrence,

  sequences=T.arange(N),

  n_steps=N,

  outputs_info=[1., 1.]

)

Создаём функцию Theano

fibonacci = theano.function(

  inputs=[N],

  outputs=outputs,

)

o_val = fibonacci(8)

print(“output:”, o_val)

Запустим код и получим ожидаемый результат. Обратите внимание, что выводится две последовательности. Это потому, что в функции recurrence мы указали два параметра – текущее значение и предыдущее значение.

Переходим к следующему примеру. Теперь будем работать с файлом scan3.py. Импортируем библиотеки:

import numpy as np

import matplotlib.pyplot as plt

import theano

import theano.tensor as T

В этом примере мы создадим фильтр нижних частот. Вначале создадим сигнал с большим количеством шума и длиной 300. Основой будет синусоида. Сразу и выведем его на экран, чтобы видеть сигнал с шумом.

X = 2*np.random.randn(300) + np.sin(np.linspace(0, 3*np.pi, 300))

plt.plot(X)

plt.title(“original”)

plt.show()

Определим переменную decay и вектор sequence. Это и будет наш зашумленный сигнал.

decay = T.scalar(‘decay’)

sequence = T.vector(‘sequence’)

Теперь функция reccurence.

def recurrence(x, last, decay):

  return (1-decay)*x + decay*last

В функции scan на этот раз просто пропустим updates:

outputs, _ = theano.scan(

  fn=recurrence,

  sequences=sequence,

  n_steps=sequence.shape[0],

  outputs_info=[np.float64(0)],

  non_sequences=[decay]

)

Теперь создадим функцию Theano с названием lpf.

lpf = theano.function(

  inputs=[sequence, decay],

  outputs=outputs,

)

Y = lpf(X, 0.99)

plt.plot(Y)

plt.title(“filtered”)

plt.show()

Запустим код и посмотрим, что выйдет.

Сначала мы видим первоначальный сигнал, весьма зашумленный. Далее мы видим очищенный сигнал.

Проблема чётности в коде с использованием рекуррентной нейронной сети

Наконец-то настало время, чтобы мы написали в коде решение проблемы чётности с помощью рекуррентной нейронной сети.

Итак, начинаем с импорта библиотек.

import theano

import theano.tensor as T

import numpy as np

import matplotlib.pyplot as plt

from sklearn.utils import shuffle

from util import init_weight, all_parity_pairs

Создадим класс SimpleRNN. M – размер скрытого слоя.

class SimpleRNN:

    def __init__(self, M):

        self.M = M

Функция fit.

    def fit(self, X, Y, learning_rate=10e-1, mu=0.99, reg=1.0, activation=T.tanh, epochs=100, show_fig=False):

        D = X[0].shape[1]

        K = len(set(Y.flatten()))

        N = len(Y)

        M = self.M

        self.f = activation

Следующее – инициация весовых коэффициентов.

        # initial weights

        Wx = init_weight(D, M)

        Wh = init_weight(M, M)

        bh = np.zeros(M)

        h0 = np.zeros(M)

        Wo = init_weight(M, K)

        bo = np.zeros(K)

Преобразуем их в переменные Theano.

        self.Wx = theano.shared(Wx)

        self.Wh = theano.shared(Wh)

        self.bh = theano.shared(bh)

        self.h0 = theano.shared(h0)

        self.Wo = theano.shared(Wo)

        self.bo = theano.shared(bo)

        self.params = [self.Wx, self.Wh, self.bh, self.h0, self.Wo, self.bo]

        thX = T.fmatrix(‘X’)

        thY = T.ivector(‘Y’)

Теперь можем определить фунцию recurrence, возращающую наши h(t) и y(t).

        def recurrence(x_t, h_t1):

            # returns h(t), y(t)

            h_t = self.f(x_t.dot(self.Wx) + h_t1.dot(self.Wh) + self.bh)

            y_t = T.nnet.softmax(h_t.dot(self.Wo) + self.bo)

            return h_t, y_t

Используем функцию scan библиотеки Theano.

        [h, y], _ = theano.scan(

            fn=recurrence,

            outputs_info=[self.h0, None],

            sequences=thX,

            n_steps=thX.shape[0],

        )

        py_x = y[:, 0, :]

        prediction = T.argmax(py_x, axis=1)

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

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

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

        updates = [

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

        ] + [

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

        ]

После этого можем определить функции Theano.

        self.predict_op = theano.function(inputs=[thX], outputs=prediction)

        self.train_op = theano.function(

            inputs=[thX, thY],

            outputs=[cost, prediction, y],

            updates=updates

        )

Затем можем переходить к основному учебному циклу.

        costs = []

        for i in xrange(epochs):

            X, Y = shuffle(X, Y)

            n_correct = 0

            cost = 0

            for j in xrange(N):

                c, p, rout = self.train_op(X[j], Y[j])

                cost += c

                if p[-1] == Y[j,-1]:

                    n_correct += 1

            print(“shape y:”, rout.shape)

            print(“i:”, i, “cost:”, cost, “classification rate:”, (float(n_correct)/N))

            costs.append(cost)

        if show_fig:

            plt.plot(costs)

            plt.show()

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

def parity(B=12, learning_rate=10e-5, epochs=200):

    X, Y = all_parity_pairs (B)

    N, t = X.shape

    Y_t = np.zeros(X.shape, dtype=np.int32)

    for n in xrange(N):

        ones_count = 0

        for i in xrange(t):

            if X[n,i] == 1:

                ones_count += 1

            if ones_count % 2 == 1:

                Y_t[n,i] = 1

    X = X.reshape(N, t, 1).astype(np.float32)

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

    rnn = SimpleRNN(4)

    rnn.fit(X, Y_t, learning_rate=learning_rate, epochs=epochs, activation=T.nnet.sigmoid, show_fig=True)

if __name__ == ‘__main__’:

parity()

И запустим наш файл.

О повышенной сложности

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

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

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

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

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

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