В чем отличие реализации MLP с нуля и PyTorch? - PullRequest
0 голосов
/ 18 января 2019

В ответ на вопрос из Как обновить скорость обучения в двухслойном многослойном персептроне?

Учитывая проблему XOR:

X = xor_input = np.array([[0,0], [0,1], [1,0], [1,1]])
Y = xor_output = np.array([[0,1,1,0]]).T

и простой

  • двухслойный многослойный персептрон (MLP) с
  • сигмовидная активация между ними и
  • Среднеквадратичная ошибка (MSE) как функция потерь / критерий оптимизации

Если мы обучаем модель с нуля как таковую:

from itertools import chain
import matplotlib.pyplot as plt
import numpy as np
np.random.seed(0)

def sigmoid(x): # Returns values that sums to one.
    return 1 / (1 + np.exp(-x))

def sigmoid_derivative(sx):
    # See https://math.stackexchange.com/a/1225116
    return sx * (1 - sx)

# Cost functions.
def mse(predicted, truth):
    return 0.5 * np.mean(np.square(predicted - truth))

def mse_derivative(predicted, truth):
    return predicted - truth

X = xor_input = np.array([[0,0], [0,1], [1,0], [1,1]])
Y = xor_output = np.array([[0,1,1,0]]).T

# Define the shape of the weight vector.
num_data, input_dim = X.shape
# Lets set the dimensions for the intermediate layer.
hidden_dim = 5
# Initialize weights between the input layers and the hidden layer.
W1 = np.random.random((input_dim, hidden_dim))

# Define the shape of the output vector. 
output_dim = len(Y.T)
# Initialize weights between the hidden layers and the output layer.
W2 = np.random.random((hidden_dim, output_dim))

# Initialize weigh
num_epochs = 5000
learning_rate = 0.3

losses = []

for epoch_n in range(num_epochs):
    layer0 = X
    # Forward propagation.

    # Inside the perceptron, Step 2. 
    layer1 = sigmoid(np.dot(layer0, W1))
    layer2 = sigmoid(np.dot(layer1, W2))

    # Back propagation (Y -> layer2)

    # How much did we miss in the predictions?
    cost_error = mse(layer2, Y)
    cost_delta = mse_derivative(layer2, Y)

    #print(layer2_error)
    # In what direction is the target value?
    # Were we really close? If so, don't change too much.
    layer2_error = np.dot(cost_delta, cost_error)
    layer2_delta = cost_delta *  sigmoid_derivative(layer2)

    # Back propagation (layer2 -> layer1)
    # How much did each layer1 value contribute to the layer2 error (according to the weights)?
    layer1_error = np.dot(layer2_delta, W2.T)
    layer1_delta = layer1_error * sigmoid_derivative(layer1)

    # update weights
    W2 += - learning_rate * np.dot(layer1.T, layer2_delta)
    W1 += - learning_rate * np.dot(layer0.T, layer1_delta)
    #print(np.dot(layer0.T, layer1_delta))
    #print(epoch_n, list((layer2)))

    # Log the loss value as we proceed through the epochs.
    losses.append(layer2_error.mean())
    #print(cost_delta)


# Visualize the losses
plt.plot(losses)
plt.show()

Мы получаем резкое погружение в потерю из эпохи 0, а затем быстро насыщаемся:

enter image description here

Но если мы обучаем аналогичную модель с pytorch, тренировочная кривая имеет постепенное падение потерь перед насыщением:

enter image description here

В чем разница между MLP с нуля и кодом PyTorch?

Почему достигается конвергенция в другой точке?

За исключением инициализации весов, np.random.rand() в коде с нуля и инициализации факела по умолчанию, я не вижу различия в модели.

Код для PyTorch:

from tqdm import tqdm
import numpy as np

import torch
from torch import nn
from torch import tensor
from torch import optim

import matplotlib.pyplot as plt

torch.manual_seed(0)
device = 'gpu' if torch.cuda.is_available() else 'cpu'

# XOR gate inputs and outputs.
X = xor_input = tensor([[0,0], [0,1], [1,0], [1,1]]).float().to(device)
Y = xor_output = tensor([[0],[1],[1],[0]]).float().to(device)


# Use tensor.shape to get the shape of the matrix/tensor.
num_data, input_dim = X.shape
print('Inputs Dim:', input_dim) # i.e. n=2 

num_data, output_dim = Y.shape
print('Output Dim:', output_dim) 
print('No. of Data:', num_data) # i.e. n=4

# Step 1: Initialization. 

# Initialize the model.
# Set the hidden dimension size.
hidden_dim = 5
# Use Sequential to define a simple feed-forward network.
model = nn.Sequential(
            # Use nn.Linear to get our simple perceptron.
            nn.Linear(input_dim, hidden_dim),
            # Use nn.Sigmoid to get our sigmoid non-linearity.
            nn.Sigmoid(),
            # Second layer neurons.
            nn.Linear(hidden_dim, output_dim),
            nn.Sigmoid()
        )
model

# Initialize the optimizer
learning_rate = 0.3
optimizer = optim.SGD(model.parameters(), lr=learning_rate)

# Initialize the loss function.
criterion = nn.MSELoss()

# Initialize the stopping criteria
# For simplicity, just stop training after certain no. of epochs.
num_epochs = 5000 

losses = [] # Keeps track of the loses.

# Step 2-4 of training routine.

for _e in tqdm(range(num_epochs)):
    # Reset the gradient after every epoch. 
    optimizer.zero_grad() 
    # Step 2: Foward Propagation
    predictions = model(X)

    # Step 3: Back Propagation 
    # Calculate the cost between the predictions and the truth.
    loss = criterion(predictions, Y)
    # Remember to back propagate the loss you've computed above.
    loss.backward()

    # Step 4: Optimizer take a step and update the weights.
    optimizer.step()

    # Log the loss value as we proceed through the epochs.
    losses.append(loss.data.item())


plt.plot(losses)

1 Ответ

0 голосов
/ 20 января 2019

Список различий между кодом, написанным вручную, и кодом PyTorch

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

  • Ваш код и код PyTorch используют две разные функции для сообщения о потере.
  • Ваш код и код PyTorch устанавливают начальные веса по-разному. Вы упоминаете об этом в своем вопросе, но оказывается, что это оказывает довольно значительное влияние на результаты.
  • По умолчанию слои torch.nn.Linear добавляют дополнительный набор весов смещения к модели. Таким образом, первый слой модели Pytorch эффективно имеет веса 3x5, а второй слой имеет веса 6x1. Слои в свернутом вручную коде имеют веса 2x5 и 5x1 соответственно.
    • Смещение, похоже, помогает модели учиться и адаптироваться несколько быстрее. Если вы отключите смещение, для модели Pytorch потребуется примерно вдвое больше тренировочных эпох, чтобы достичь почти 0 потерь.
  • Любопытно, что кажется, что модель Pytorch использует скорость обучения, которая фактически составляет половину от того, что вы указали. В качестве альтернативы может быть случайный фактор 2, который где-то попал в вашу свернутую вручную математику / код.

Как получить идентичные результаты из свернутого вручную кода и кода Pytorch

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

enter image description here

Самый важный твик - сделать так, чтобы функции отчетов о потерях соответствовали

Критическим отличием является то, что в итоге вы используете две совершенно разные функции для измерения потерь в двух фрагментах кода:

  • В ручном коде вы измеряете потери как layer2_error.mean(). Если вы распакуете переменную, вы увидите, что layer2_error.mean() является довольно странным и бессмысленным значением:

    layer2_error.mean()
    == np.dot(cost_delta, cost_error).mean()
    == np.dot(mse_derivative(layer2, Y), mse(layer2, Y)).mean()
    == np.sum(.5 * (layer2 - Y) * ((layer2 - Y)**2).mean()).mean()
    
  • С другой стороны, в коде PyTorch потери измеряются в терминах традиционного определения mse, то есть как эквивалент np.mean((layer2 - Y)**2). Вы можете доказать это себе, изменив цикл PyTorch следующим образом:

    def mse(x, y):
        return np.mean((x - y)**2)
    
    torch_losses = [] # Keeps track of the loses.
    torch_losses_manual = [] # for comparison
    
    # Step 2-4 of training routine.
    
    for _e in tqdm(range(num_epochs)):
        # Reset the gradient after every epoch. 
        optimizer.zero_grad() 
        # Step 2: Foward Propagation
        predictions = model(X)
    
        # Step 3: Back Propagation 
        # Calculate the cost between the predictions and the truth.
        loss = criterion(predictions, Y)
        # Remember to back propagate the loss you've computed above.
        loss.backward()
    
        # Step 4: Optimizer take a step and update the weights.
        optimizer.step()
    
        # Log the loss value as we proceed through the epochs.
        torch_losses.append(loss.data.item())
        torch_losses_manual.append(mse(predictions.detach().numpy(), Y.detach().numpy()))
    
    plt.plot(torch_losses, lw=5, label='torch_losses')
    plt.plot(torch_losses_manual, lw=2, label='torch_losses_manual')
    plt.legend()
    

Выход:

enter image description here

Также важно - используйте те же начальные веса

PyTorch использует свою собственную специальную процедуру для установки начальных весов, которая дает очень отличные результаты от np.random.rand. Я еще не смог точно воспроизвести это, но для следующего лучшего, что мы можем просто захватить Pytorch. Вот функция, которая получит те же начальные веса, которые использует модель Pytorch:

import torch
from torch import nn
torch.manual_seed(0)

def torch_weights(nodes_in, nodes_hidden, nodes_out, bias=None):
    model = nn.Sequential(
        nn.Linear(nodes_in, nodes_hidden, bias=bias),
        nn.Sigmoid(),
        nn.Linear(nodes_hidden, nodes_out, bias=bias),
        nn.Sigmoid()
    )

    return [t.detach().numpy() for t in model.parameters()]

Наконец - в Pytorch отключите все веса смещения и удвойте скорость обучения

В конце концов, вы можете захотеть реализовать веса смещения в своем собственном коде. Сейчас мы просто отключим смещение в модели Pytorch и сравним результаты свернутой вручную модели с результатами объективной модели Pytorch.

Кроме того, чтобы результаты совпали, вам нужно удвоить скорость обучения модели Pytorch. Это эффективно масштабирует результаты вдоль оси x (то есть удвоение скорости означает, что для достижения какого-то определенного признака на кривой потерь требуется вдвое меньше эпох).

Собираем все вместе

Чтобы воспроизвести данные hand_rolled_losses из сюжета в начале моего поста, все, что вам нужно сделать, - это взять свой свернутый вручную код и заменить функцию mse на:

def mse(predicted, truth):
    return np.mean(np.square(predicted - truth))

строки, которые инициализируют веса:

W1,W2 = [w.T for w in torch_weights(input_dim, hidden_dim, output_dim)]

и строка, которая отслеживает потери с:

losses.append(cost_error)

и тебе надо идти.

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

model = nn.Sequential(
    # Use nn.Linear to get our simple perceptron.
    nn.Linear(input_dim, hidden_dim, bias=None),
    # Use nn.Sigmoid to get our sigmoid non-linearity.
    nn.Sigmoid(),
    # Second layer neurons.
    nn.Linear(hidden_dim, output_dim, bias=None),
    nn.Sigmoid()
)

Вам также необходимо изменить строку, определяющую learning_rate:

learning_rate = 0.3 * 2

Полный список кодов

Код, свернутый вручную

Вот полный список моей версии свернутого вручную кода нейронной сети, чтобы помочь с воспроизведением моих результатов:

from itertools import chain
import matplotlib.pyplot as plt
import numpy as np
import scipy as sp
import scipy.stats
import torch
from torch import nn

np.random.seed(0)
torch.manual_seed(0)

def torch_weights(nodes_in, nodes_hidden, nodes_out, bias=None):
    model = nn.Sequential(
        nn.Linear(nodes_in, nodes_hidden, bias=bias),
        nn.Sigmoid(),
        nn.Linear(nodes_hidden, nodes_out, bias=bias),
        nn.Sigmoid()
    )

    return [t.detach().numpy() for t in model.parameters()]

def sigmoid(x): # Returns values that sums to one.
    return 1 / (1 + np.exp(-x))

def sigmoid_derivative(sx):
    # See https://math.stackexchange.com/a/1225116
    return sx * (1 - sx)

# Cost functions.
def mse(predicted, truth):
    return np.mean(np.square(predicted - truth))

def mse_derivative(predicted, truth):
    return predicted - truth

X = xor_input = np.array([[0,0], [0,1], [1,0], [1,1]])
Y = xor_output = np.array([[0,1,1,0]]).T

# Define the shape of the weight vector.
num_data, input_dim = X.shape
# Lets set the dimensions for the intermediate layer.
hidden_dim = 5
# Define the shape of the output vector. 
output_dim = len(Y.T)

W1,W2 = [w.T for w in torch_weights(input_dim, hidden_dim, output_dim)]

num_epochs = 5000
learning_rate = 0.3
losses = []

for epoch_n in range(num_epochs):
    layer0 = X
    # Forward propagation.

    # Inside the perceptron, Step 2. 
    layer1 = sigmoid(np.dot(layer0, W1))
    layer2 = sigmoid(np.dot(layer1, W2))

    # Back propagation (Y -> layer2)

    # In what direction is the target value?
    # Were we really close? If so, don't change too much.
    cost_delta = mse_derivative(layer2, Y)
    layer2_delta = cost_delta *  sigmoid_derivative(layer2)

    # Back propagation (layer2 -> layer1)
    # How much did each layer1 value contribute to the layer2 error (according to the weights)?
    layer1_error = np.dot(layer2_delta, W2.T)
    layer1_delta = layer1_error * sigmoid_derivative(layer1)

    # update weights
    W2 += - learning_rate * np.dot(layer1.T, layer2_delta)
    W1 += - learning_rate * np.dot(layer0.T, layer1_delta)

    # Log the loss value as we proceed through the epochs.
    losses.append(mse(layer2, Y))

# Visualize the losses
plt.plot(losses)
plt.show()

Код Pytorch

import matplotlib.pyplot as plt
from tqdm import tqdm
import numpy as np

import torch
from torch import nn
from torch import tensor
from torch import optim

torch.manual_seed(0)
device = 'gpu' if torch.cuda.is_available() else 'cpu'

num_epochs = 5000
learning_rate = 0.3 * 2

# XOR gate inputs and outputs.
X = tensor([[0,0], [0,1], [1,0], [1,1]]).float().to(device)
Y = tensor([[0],[1],[1],[0]]).float().to(device)

# Use tensor.shape to get the shape of the matrix/tensor.
num_data, input_dim = X.shape
num_data, output_dim = Y.shape

# Step 1: Initialization. 

# Initialize the model.
# Set the hidden dimension size.
hidden_dim = 5
# Use Sequential to define a simple feed-forward network.
model = nn.Sequential(
    # Use nn.Linear to get our simple perceptron.
    nn.Linear(input_dim, hidden_dim, bias=None),
    # Use nn.Sigmoid to get our sigmoid non-linearity.
    nn.Sigmoid(),
    # Second layer neurons.
    nn.Linear(hidden_dim, output_dim, bias=None),
    nn.Sigmoid()
)

# Initialize the optimizer
optimizer = optim.SGD(model.parameters(), lr=learning_rate)

# Initialize the loss function.
criterion = nn.MSELoss()

def mse(x, y):
    return np.mean((x - y)**2)

torch_losses = [] # Keeps track of the loses.
torch_losses_manual = [] # for comparison

# Step 2-4 of training routine.

for _e in tqdm(range(num_epochs)):
    # Reset the gradient after every epoch. 
    optimizer.zero_grad() 
    # Step 2: Foward Propagation
    predictions = model(X)

    # Step 3: Back Propagation 
    # Calculate the cost between the predictions and the truth.
    loss = criterion(predictions, Y)
    # Remember to back propagate the loss you've computed above.
    loss.backward()

    # Step 4: Optimizer take a step and update the weights.
    optimizer.step()

    # Log the loss value as we proceed through the epochs.
    torch_losses.append(loss.data.item())
    torch_losses_manual.append(mse(predictions.detach().numpy(), Y.detach().numpy()))

plt.plot(torch_losses, lw=5, c='C1', label='torch_losses')
plt.plot(torch_losses_manual, lw=2, c='C2', label='torch_losses_manual')
plt.legend()

Примечания

Смещения веса

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

Функции для получения начального предположения о весах

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

import scipy as sp
import scipy.stats

def tnorm_weights(nodes_in, nodes_out, bias_node=0):
    # see https://www.python-course.eu/neural_network_mnist.php
    wshape = (nodes_out, nodes_in + bias_node)
    bound = 1 / np.sqrt(nodes_in)
    X = sp.stats.truncnorm(-bound, bound)
    return X.rvs(np.prod(wshape)).reshape(wshape) 
...