Свёрточная нейронная сеть в Theano

Theano – создание компонентов свёрточной нейронной сети

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

Итак, в этой подробной лекции мы рассмотрим компоненты свёрточной нейронной сети на Theano, объединим их и проверим на наших данных.

После изучения свёртки и понижения дискретизации вас прежде всего может интересовать вопрос – есть ли в Theano соответствующие функции? Как вы догадываетесь – есть:

from theano.tensor.nnet import conv2d

from theano.tensor.signal import downsample

 

Правда, работать они будут не совсем так, как вы могли бы ожидать.

В модели LeNet свёртка и агрегирование всегда проводятся вместе, и потому данная операция называется ConvPool. Обратите внимание, что максимизационном агрегировании требуются некоторые дополнительные параметры:

def convpool (X, W, b, poolsize=(2, 2) ):

    conv_out = conv2d(input=X, filters=W)

    pooled_out = downsample.max_pool_2d(

        input=conv_out,

        ds=poolsize,

        ignore_border=True

    )

    return T.tanh(pooled_out + b.dimshuffle(‘x’, 0, ‘x’, ‘x’))

 

В результате исполнения последней строки мы получаем свободный член в виде четырёхмерного тензора. Как вы впоследствии увидите, TensorFlow всё это делает сразу.

Следующее – преобразование входных данных. Не забывайте, что Matlab делаёт всё несколько странным способом и помещает индекс в последней размерности каждого образца, тогда как в Theano предполагается, что он находится в первой размерности и что следующей размерностью идёт цвет. Поэтому мы должны соответствующим образом преобразовать данные. Кроме того, как вы уже знаете, при работе с нейронными сетями мы предпочитаем работать с небольшим диапазоном данных, так что мы делим максимальное значение образцов, то есть N, на 255.

def rearrange(X):

    # input is (32, 32, 3, N)

    # output is (N, 3, 32, 32)

    N = X.shape[-1]

    out = np.zeros( (N, 3, 32, 32), dtype=np.float32)

    for i in xrange(N):

        for j in xrange(3):

            out[i, j, :, :] = X[:, :, j, i]

    return out / 255

 

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

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

Таким образом, результат после первого ConvPool-слоя также будет четырёхмерным тензором, где первая размерность представляет, конечно же, количество образцов, а вторая размерность будет представлять уже не цвет, а количество карт признаков, равное, после первого этапа, 20. Следующие две размерности представляют размер нового изображения, равный после свёртывания и агрегирования 32 – 5 + 1 = 28 и делённый затем на два: 28/2 = 14.

W1_shape = (20, 3, 5, 5)

W1_init = init_filter(W1_shape, poolsz)

b1_init = np.zeros(W1_shape[0], dtype=np.float32)

 

Далее мы используем фильтр с размерностями (50, 20, 5, 5). Это значит, что теперь у нас есть 50 карт признаков. Таким образом, на выходе у нас будут первые две размерности, одна из которых равна количеству изображений, а вторая – 50, и вторые две размерности, характеризующие размер нового изображения, который после очередного свёртывания и агрегирования будет равен 14 – 5 + 1 = 10, делённый на 2: 10 / 2 = 5.

W2_shape = (50, 20, 5, 5)

W2_init = init_filter(W2_shape, poolsz)

b2_init = np.zeros(W2_shape[0], dtype=np.float32)

 

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

W3_init = np.random.randn(W2_shape[0]*5*5, M) / np.sqrt(W2_shape[0]*5*5 + M)

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

W4_init = np.random.randn(M, K) / np.sqrt(M + K)

b4_init = np.zeros(K, dtype=np.float32)

 

И наконец теперь, когда у нас есть инициированные весовые коэффициенты и необходимые операции, мы можем вычислить выход нейронной сети. Мы дважды делаем свёртку и агрегирование, а затем операцию сглаживания. Обратите внимание, что сглаживание производится до скалярного умножения. Это связано с тем, что Z2 после свёртки и агрегирования всё ещё остаётся изображением, а потому если пустить сглаживание на самотёк, оно превратится в одномерный массив, что нам вовсе не нужно. К счастью, Theano позволяет нам контролировать степень сглаживания массива. Поэтому мы указываем ndim=2 – это значит, что необходимо сгладить все размерности после второй.

Z1 = convpool(X, W1, b1)

Z2 = convpool(Z1, W2, b2)

Z3 = relu(Z2.flatten(ndim=2).dot(W3) + b3)

pY = T.nnet.softmax( Z3.dot(W4) + b4 )

 

Theano – полная свёрточная нейронная сеть и её проверка на SVHN

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

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

import numpy as np

import theano

import theano.tensor as T

import matplotlib.pyplot as plt

from theano.tensor.nnet import conv2d

from theano.tensor.signal import downsample

from scipy.io import loadmat

from sklearn.utils import shuffle

from datetime import datetime

 

Далее запишем все использовавшиеся ранее функции.

def error_rate(p, t):

    return np.mean(p != t)

def relu(a):

    return a * (a > 0)

def y2indicator(y):

    N = len(y)

    ind = np.zeros((N, 10))

    for i in xrange(N):

        ind[i, y[i]] = 1

    return ind

 

А теперь уже начинается нечто новое. Напишем функцию для проведения свёртки и агрегирования. Размер пула агрегирования оставим прежний, равный (2, 2), указав его в качестве параметра. Первая строка после объявления функции является ответственной за свёртку, а далее идёт понижение дискретизации; кроме того, не забываем о свободном члене.

def convpool(X, W, b, poolsize=(2, 2)):

    conv_out = conv2d(input=X, filters=W)

    pooled_out = downsample. max_pool_2d(

        input=conv_out,

        ds=poolsize,

        ignore_border=True

    )

    return T.tanh(pooled_out + b.dimshuffle(‘x’, 0, ‘x’, ‘x’))

 

Следующей идёт функция инициации весовых коэффициентов. Инициация производится на основе размерностей, так что и здесь в качестве параметра выступает размер пула. Тип возвращаемого функцией значения – float32.

def init_filter(shape, poolsz):

    w = np.random.randn(*shape) / np.sqrt(np.prod(shape[1:]) + shape[0]*np.prod(shape[2:]) / np.prod(poolsz))

    return w.astype(np.float32)

 

В данном случае нам ещё потребуется функция rearrange, так как Theano требует данные в формате (N, 3, 32, 32) и потому необходимо привести их в соответствующий вид.

def rearrange(X):

    N = X.shape[-1]

    out = np.zeros((N, 3, 32, 32), dtype=np.float32)

    for i in xrange(N):

        for j in xrange(3):

            out[i, j, :, :] = X[:, :, j, i]

    return out / 255

Теперь мы готовы написать функцию main. Значительная её часть идентична этой же функции из файла benchmark.py, так что можно её просто скопировать.

def main():

    train = loadmat(‘../large_files/train_32x32.mat’)

    test  = loadmat(‘../large_files/test_32x32.mat’)

    Xtrain = rearrange(train[‘X’])

    Ytrain = train[‘y’].flatten() – 1

    del train

    Xtrain, Ytrain = shuffle(Xtrain, Ytrain)

    Ytrain_ind = y2indicator(Ytrain)

    Xtest  = rearrange(test[‘X’])

    Ytest  = test[‘y’].flatten() – 1

    del test

    Ytest_ind = y2indicator(Ytest)

 

Установим теперь параметры обратного распространения – количество итераций, равное 20, коэффициент обучения, равный 0,0001, величину регуляризации 0,01 и импульс 0,99. Размер пакета оставим прежним, равным 500.

    max_iter = 20

    print_period = 10

    lr = np.float32(0.0001)

    reg = np.float32(0.01)

    mu = np.float32(0.99)

    N = Xtrain.shape[0]

    batch_sz = 500

    n_batches = N / batch_sz

И последние параметры нейронной сети – 500 скрытых узлов и 10 исходящих.

    M = 500

    K = 10

    poolsz = (2, 2)

 

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

    W1_shape = (20, 3, 5, 5)

    W1_init = init_filter(W1_shape, poolsz)

    b1_init = np.zeros(W1_shape[0], dtype=np.float32)

    W2_shape = (50, 20, 5, 5)

    W2_init = init_filter(W2_shape, poolsz)

    b2_init = np.zeros(W2_shape[0], dtype=np.float32)

 

Теперь инициируем весовые коэффициенты собственно обычной нейронной сети.

    W3_init = np.random.randn(W2_shape[0]*5*5, M) / np.sqrt(W2_shape[0]*5*5 + M)

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

    W4_init = np.random.randn(M, K) / np.sqrt(M + K)

    b4_init = np.zeros(K, dtype=np.float32)

 

Теперь определим наши переменные Theano. Не забывайте, что X у нас теперь является четырёхмерным тензором.

    X = T.tensor4(‘X’, dtype=’float32′)

    Y = T.matrix(‘T’)

    W1 = theano.shared(W1_init, ‘W1’)

    b1 = theano.shared(b1_init, ‘b1’)

    W2 = theano.shared(W2_init, ‘W2’)

    b2 = theano.shared(b2_init, ‘b2’)

    W3 = theano.shared(W3_init.astype(np.float32), ‘W3’)

    b3 = theano.shared(b3_init, ‘b3’)

    W4 = theano.shared(W4_init.astype(np.float32), ‘W4’)

    b4 = theano.shared(b4_init, ‘b4’)

 

И поскольку мы используем импульс, то необходимо указать и переменные для него в качестве переменных Theano.

    dW1 = theano.shared(np.zeros(W1_init.shape, dtype=np.float32), ‘dW1’)

    db1 = theano.shared(np.zeros(b1_init.shape, dtype=np.float32), ‘db1’)

    dW2 = theano.shared(np.zeros(W2_init.shape, dtype=np.float32), ‘dW2’)

    db2 = theano.shared(np.zeros(b2_init.shape, dtype=np.float32), ‘db2’)

    dW3 = theano.shared(np.zeros(W3_init.shape, dtype=np.float32), ‘dW3’)

    db3 = theano.shared(np.zeros(b3_init.shape, dtype=np.float32), ‘db3’)

    dW4 = theano.shared(np.zeros(W4_init.shape, dtype=np.float32), ‘dW4’)

    db4 = theano.shared(np.zeros(b4_init.shape, dtype=np.float32), ‘db4’)

 

Затем – часть, посвящённая прямому распространению.

    Z1 = convpool(X, W1, b1)

    Z2 = convpool(Z1, W2, b2)

    Z3 = relu(Z2.flatten(ndim=2).dot(W3) + b3)

    pY = T.nnet.softmax( Z3.dot(W4) + b4)

 

Далее определяем функции cost и prediction с использованием регуляризации.

    params = (W1, b1, W2, b2, W3, b3, W4, b4)

    reg_cost = reg*np.sum((param*param).sum() for param in params)

    cost = -(Y * T.log(pY)).sum() + reg_cost

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

 

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

    update_W1 = W1 + mu*dW1 – lr*T.grad(cost, W1)

    update_b1 = b1 + mu*db1 – lr*T.grad(cost, b1)

    update_W2 = W2 + mu*dW2 – lr*T.grad(cost, W2)

    update_b2 = b2 + mu*db2 – lr*T.grad(cost, b2)

    update_W3 = W3 + mu*dW3 – lr*T.grad(cost, W3)

    update_b3 = b3 + mu*db3 – lr*T.grad(cost, b3)

    update_W4 = W4 + mu*dW4 – lr*T.grad(cost, W4)

    update_b4 = b4 + mu*db4 – lr*T.grad(cost, b4)

    update_dW1 = mu*dW1 – lr*T.grad(cost, W1)

    update_db1 = mu*db1 – lr*T.grad(cost, b1)

    update_dW2 = mu*dW2 – lr*T.grad(cost, W2)

    update_db2 = mu*db2 – lr*T.grad(cost, b2)

    update_dW3 = mu*dW3 – lr*T.grad(cost, W3)

    update_db3 = mu*db3 – lr*T.grad(cost, b3)

    update_dW4 = mu*dW4 – lr*T.grad(cost, W4)

    update_db4 = mu*db4 – lr*T.grad(cost, b4)

 

Далее – определение функции обучения. Это функция Theano.

    train = theano.function(

        inputs=[X, Y],

        updates=[

            (W1, update_W1),

            (b1, update_b1),

            (W2, update_W2),

            (b2, update_b2),

            (W3, update_W3),

            (b3, update_b3),

            (W4, update_W4),

            (b4, update_b4),

            (dW1, update_dW1),

            (db1, update_db1),

            (dW2, update_dW2),

            (db2, update_db2),

            (dW3, update_dW3),

            (db3, update_db3),

            (dW4, update_dW4),

            (db4, update_db4),

        ]

    )

Следующая функция – get_prediction.

    get_prediction = theano.function(

        inputs=[X, Y],

        outputs=[cost, prediction],

    )

И последнее – написание цикла, в котором происходит собственно обучение. Код тут практически тот же, что и в предыдущих занятиях, так что его можно просто скопировать.

    t0 = datetime.now()

    LL = []

    for i in xrange(max_iter):

        for j in xrange(n_batches):

            Xbatch = Xtrain[j*batch_sz:(j*batch_sz + batch_sz),]

            Ybatch = Ytrain_ind[j*batch_sz:(j*batch_sz + batch_sz),]

            train(Xbatch, Ybatch)

            if j % print_period == 0:

                cost_val, prediction_val = get_prediction(Xtest, Ytest)

                err = error_rate(prediction_val, Ytest)

                print “Cost / err at iteration i=%d, j=%d: %.3f / %.3f” % (i, j, cost_val, err)

                LL.append(cost_val)

    print “Elapsed time:”, (datetime.now() – t0)

    plt.plot(LL)

    plt.show()

if __name__ == ‘__main__’:

main()

Итак, всё сделано. Запустим программу.

 

Наглядное представление фильтров после обучения

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

В данном продолжении мы научимся отвечать на этот вопрос. Мы просто возьмём уже написанный код для Theano и добавим кое-что в конце, так чтобы каждый фильтр отображался в координатной сетке, а затем выведем их все на экран. В этом деле есть некоторые тонкости, но само по себе оно несложно, ведь мы основываемся на уже изученном материале. Если это действительно вас заинтересовало, я настоятельно рекомендую попытаться сделать всё самостоятельно, до просмотра моего кода, поскольку, опять же, это не так уж и трудно. Так что если хотите попробовать свои силы – поставьте видео на паузу и приступайте к работе, в противном же случае продолжайте смотреть. Соответствующий файл из репозитария называется cnn_theano_plot_filters.py.

Итак, пройдёмся по добавленному коду, написанному внизу, сразу после вывода на экран функции затрат:

    W1_val = W1.get_value()

    grid = np.zeros((8*5, 8*5))

    m = 0

    n = 0

    for i in xrange(20):

        for j in xrange(3):

            filt = W1_val[i,j]

            grid[m*5:(m+1)*5,n*5:(n+1)*5] = filt

            m += 1

            if m >= 8:

                m = 0

                n += 1

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

    plt.title(“W1”)

    plt.show()

Итак, что у нас тут?

Во-первых, нам нужен действительный массив, а не Theano, для чего используем W1.get_value(). Далее, мы знаем, что в общей сложности у нас будет 60 фильтров размером 5×5, поскольку у нас 3 входных канала и 20 исходящих, а 3 * 20 = 60. Но поскольку мы хотим, чтобы рисунок имел более-менее квадратную форму то мы используем 8 * 8 = 64, причём последние 4 квадратика будут пустыми. Затем – циклы по всем 20 исходящим каналам и трём входящим с использованием координатной сетки размером 5×5, а получившийся результат присваиваем переменной filt.

Далее идёт фокус. Мы вводим две переменные m и n, которые показывают, в какой части координатной сетки мы рисуем. Как вы помните, у нас в общем есть 64 квадратика. Если m будет пробегать значения от 0 до 8, и n будет пробегать значения от 0 до 8, то их можно использовать в качестве индексов текущего квадрата. M в данном случае является первоочередной переменной цикла. Она увеличивается каждый раз на единицу, а достигнув 8, сбрасывается до нуля, а n увеличивается на единицу. Это можно рассматривать как сдвиг слева направо при каждом прохождении цикла.

Вот и всё, что касается W1 – первого фильтра.

Второй фильтр имеет 20 входных каналов и 50 исходящих. Это значит в общей сложности 1 000 квадратов – намного больше, чем перед этим. Тем не менее, мы всё равно можем поместить их в координатную сетку. Если квадратики будут 32×32, то в общей сложности у нас будет 1 024 квадратика. Циклы же строятся по тому же самому принципу, что и ранее.

    W2_val = W2.get_value()

    grid = np.zeros((32*5, 32*5))

    m = 0

    n = 0

    for i in xrange(50):

        for j in xrange(20):

            filt = W2_val[i,j]

            grid[m*5:(m+1)*5,n*5:(n+1)*5] = filt

            m += 1

            if m >= 32:

                m = 0

                n += 1

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

    plt.title(“W2”)

    plt.show()

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

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

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

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

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