Восстановление нелинейной функции нейронной сетью с активацией

Нейронная сеть с нелинейностью: теория

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

Схема сети с одним скрытым слоем и активацией
Рисунок 1. Схема сети с одним скрытым слоем и активацией

где

x_1 .. x_m - вход сети;
z_1 .. z_n - нейроны скрытого слоя;
h_1 .. h_n - активации с помощью функции f соответствующих нейронов скрытого слоя;
y - выход сети;
w_{ij}^{(l)} - веса соответствующих связей между нейронами; l - номер слоя, i - нейрон на l + 1 слое, j - нейрон на l слое;
b_i^{(l)} - смещения на соответствующих слоях;

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

y = \sum_{j=1}^{n} w_{1j}^{(2)}h_j + b_1^{(2)} = W^{(2)}h + b^{(2)} = w_{11}^{(2)}h_1 + w_{12}^{(2)}h_2 + w_{13}^{(2)}h_3 + b_{1}^{(2)}

где h_j = f(z_j).

Соответственно изменится и вычисление градиента для параметра

\begin{equation} \frac{\partial{J}}{\partial{w_{11}^{(1)}}} = \frac{\partial{J}}{\partial{y}} \frac{\partial{y}}{\partial{h_1}} \frac{\partial{h_1}}{\partial{z_1}} \frac{\partial{z_1}}{\partial{w_{11}^{(1)}}} \end{equation}

В качестве функции активации возьмём ReLU, для которой:

f'(x) = \begin{cases} 0 & \quad \text{if } x < 0 \\ 1 & \quad \text{if } x \geq 0 \end{cases}

Нейронная сеть с нелинейностью: код

Начало скрипта:

import numpy as np

from Utils import show, showSubplots
from NetLinearTest import Net, Linear


X = np.linspace(-3, 3, 1024, dtype=np.float32).reshape(-1, 1)

def func(x):
    from math import sin
    return 2 * sin(x) + 5

f = np.vectorize(func)
Y = f(X)

Добавляется новый класс - Activation, реализующий ReLU активацию:

class Activation:
    def __init__(self, name="relu"):
        self.name = name

        self.inData = None
        self.data = None

        self.grad = None


    def forward(self, data):
        self.inData = data

        self.data = data * (data > 0)

        return self.data


    def backward(self, grad):
        self.grad = (self.data > 0) * grad

        return self.grad


    def update(self, lr):
        pass

Обучение сети

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

def trainNet(size, steps=1000, batchsize=10, learnRate=1e-2):
    np.random.seed(1234)

    net = Net()
    net.append(Linear(insize=1, outsize=size, name="layer_1"))
    net.append(Activation(name="act_layer"))
    net.append(Linear(insize=size, outsize=1, name="layer_2"))

    predictedBT = net(X)

    XC = X.copy()
    perm = np.random.permutation(XC.shape[0])
    XC = XC[perm, :]

    for i in range(steps):
        idx = np.random.randint(0, 1000 - batchsize)
        x = XC[idx:idx + batchsize]
        y = f(x).astype(np.float32)

        net.optimize(x, y, learnRate)

    predictedAT = net(X)

    showSubplots(
        X,
        Y,
        {
            "y": predictedBT,
            "name": "Net results before training",
            "color": "orange"
        },
        {
            "y": predictedAT,
            "name": "Net results after training",
            "color": "orange"
        }
    )
trainNet(5, steps=1000, batchsize=100)

Сравнение результатов сети (5 нейронов в скрытом слое) до и после обучения
Рисунок 2. Сравнение результатов сети (5 нейронов в скрытом слое) до и после обучения

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

trainNet(100, steps=1000, batchsize=100)

Сравнение результатов сети (100 нейронов в скрытом слое) до и после обучения
Рисунок 3. Сравнение результатов сети (100 нейронов в скрытом слое) до и после обучения

Уже лучше, но всё равно есть погрешность. У нас есть возможность увеличить количество шагов, чтобы повысить качество, но это будет долго, так что самое время воспользоваться более мощными инструментами библиотеки PuzzleLib.

Реализация инструментами библиотеки

Заметьте, что в этот раз мы обучаем сеть не по шагам (прогон одного батча), а по эпохам (прогон всей выборки), и ещё выбираем более продвинутый оптимизатор:

def trainNetPL(size, epochs, batchsize=10, learnRate=1e-2):
    from PuzzleLib.Modules import Linear, Activation
    from PuzzleLib.Modules.Activation import relu
    from PuzzleLib.Containers import Sequential
    from PuzzleLib.Optimizers import MomentumSGD
    from PuzzleLib.Cost import MSE
    from PuzzleLib.Handlers import Trainer
    from PuzzleLib.Backend.gpuarray import to_gpu

    np.random.seed(1234)

    net = Sequential()
    net.append(Linear(insize=1, outsize=size, initscheme="gaussian"))
    net.append(Activation(activation=relu))
    net.append(Linear(insize=size, outsize=1, initscheme="gaussian"))

    predictedBT = net(to_gpu(X)).get()

    cost = MSE()
    optimizer = MomentumSGD(learnRate)
    optimizer.setupOn(net)

    trainer = Trainer(net, cost, optimizer, batchsize=batchsize)

    show(X, Y, net(to_gpu(X)).get())

    XC, YC = X.copy(), Y.copy()
    perm = np.random.permutation(XC.shape[0])
    XC = XC[perm, :]
    YC = YC[perm, :]

    for i in range(epochs):
        trainer.trainFromHost(XC.astype(np.float32), YC.astype(np.float32), macroBatchSize=1000,
                                onMacroBatchFinish=lambda train: print("PL module error: %s" % train.cost.getMeanError()))

    net.evalMode()

    predictedAT = net(to_gpu(X)).get()

    showSubplots(
        X,
        Y,
        {
            "y": predictedBT,
            "name": "PL net results before training",
            "color": "orange"
        },
        {
            "y": predictedAT,
            "name": "PL net results after training",
            "color": "orange"
        }
    )

Сравнение результатов сети PL до и после обучения
Рисунок 4. Сравнение результатов сети PL до и после обучения

Заключение

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