Обучение многослойного персептрона операции XOR
Обучение многослойного персептрона на примере логической операции XOR.
Обязательно изучите введение в нейронные сети.
В данном примере будем обучать нейронную сеть решать логическую операцию Xor. Xor имеет таблицу истинности.
x1 | x2 | x1 xor x2 |
0 | 0 | 0 |
0 | 1 | 1 |
1 | 0 | 1 |
1 | 1 | 0 |
Чтобы решить данную задачу нужно будет создать многослойный персепрон следующего вида:
Перед тем как начать работу, установите одну из двух библиотек:
Решение для PyTorch
Проект perceptron-xor на гитхабе
Подключение библиотек и определение устройства, на котором будут выполняться вычисления:
import torch
from torch import nn
from torchsummary import summary
# Detect device
tensor_device = torch.device('cuda') if torch.cuda.is_available() else torch.device('cpu')
Подготовка обучающих данных
Перед тем, как начать обучать нейронную сеть, нужно создать обучающий набор данных и нормализировать его. Числа входного и выходного векторов должны быть в пределах от 0 до 1 включительно.
Определяем данные:
data_train = [
{ "in": [0, 0], "out": [0] },
{ "in": [0, 1], "out": [1] },
{ "in": [1, 0], "out": [1] },
{ "in": [1, 1], "out": [0] },
]
Это массив вопросов и правильных ответов. Обратите внимание, что входом и выходом являются вектора. Например, вход 2-мерный вектор (0, 0), выход 1-мерный вектор (0).
Сделаем две операции map, которые преобразуют массив data_train отдельно в массив вопросов и ответов. Массив вопросов будет состоять из значения поля "in". Массив ответов будет состоять из значений поля "out".
# Convert to question and answer
tensor_train_x = list(map(lambda item: item["in"], data_train))
tensor_train_y = list(map(lambda item: item["out"], data_train))
Преобразуем в тензор float32 на устройстве tensor_device.
# Convert to tensor
tensor_train_x = torch.tensor(tensor_train_x).to(torch.float32).to(tensor_device)
tensor_train_y = torch.tensor(tensor_train_y).to(torch.float32).to(tensor_device)
Выведем на экран полученный результат:
print ("Input:")
print (tensor_train_x)
print ("Shape:", tensor_train_x.shape)
print ("")
print ("Answers:")
print (tensor_train_y)
print ("Shape:", tensor_train_y.shape)
Должно получится:
Input:
tensor([[0., 0.],
[0., 1.],
[1., 0.],
[1., 1.]], device='cuda:0')
Shape: torch.Size([4, 2])
Answers:
tensor([[0.],
[1.],
[1.],
[0.]], device='cuda:0')
Shape: torch.Size([4, 1])
Shape означается размерность вектора. Выражение (4, 2) означает, что дан массив из 4х 2-мерных векторов. Или другими словами двумерный массив с 4 строчками и 2 колонками.
На выходе получаем массив из 4х 1-мерных векторов. Обратите внимание, что количество строчек на входе и на выходе одинаковое и равно 4м. У нас в обучающей выборке 4 варианта вопросов и на каждый вопрос есть по одному ответу. В итоге на 4 вопроса, 4 ответа.
Данные подготовили. Сформировали два тензора вопросов и ответов, формата float32. Числа находятся в переделах от 0 до 1.
Можно приступать к созданию нейронной сети.
Создание модели нейронной сети
Архитектура нейронной сети будет следующая.
Почему такая? Опытным путем было выяснено, что другие архитектуры неработают. Создание архитектуры нейронной сети напоминает танец с бубном, когда нужно пробовать разные варианты, до тех пор, пока значение функции ошибки не станет минимальным.
Что можно менять:
- количество слоев.
- количество нейронов в слое.
- функции активации.
- функцию ошибки.
Методом тыка было выяснено, что архитектура нейронной сети должна состоять из 3х слоев.
- Входом в нейронную сеть будет являться 2-мерный вектор (x1,x2)
- Скрытый слой из 16 нейронов с функцией активации Relu.
- Выходной слой из одного нейрона с функцией активации Softmax. В большинстве случаев выход классификатора активируется функцией Softmax. Это важно!
- Выходом будет являться 1-мерный вектор, результат операции x1 xor x2
Создаем модель:
input_shape = 2
output_shape = 1
model = nn.Sequential(
nn.Linear(input_shape, 16),
nn.ReLU(),
nn.Linear(16, output_shape)
)
summary(model, (input_shape,))
Параметры:
- Размер входного тензора input_shape - 2
- Размер выходного тензора output_shape - 1
- Количество нейронов на скрытом слое - 16
Зададим параметры оптимизации для модели:
# Adam optimizer
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3, betas=(0.9, 0.99))
# mean squared error
loss = nn.MSELoss()
# Batch size
batch_size = 2
# Epochs
epochs = 1000
Параметры:
- Оптимизатор: Adam
- Функция ошибка: средне-квадратическая
Получается данная модель создает многослойный персептрон.
Выведем модель на экран:
summary(model, (input_shape,))
Результат выполнения команды:
==========================================================================================
Layer (type:depth-idx) Output Shape Param #
==========================================================================================
├─Linear: 1-1 [-1, 16] 48
├─ReLU: 1-2 [-1, 16] --
├─Linear: 1-3 [-1, 1] 17
==========================================================================================
Total params: 65
Trainable params: 65
Non-trainable params: 0
Число 65 - это количество весов, которые будут в нейронной сети. Это число получается следующим образом:
- На входном слое 2 нейрона + 1 нейрон для bias. Итого 3 нейрона
- На скрытом слое 16 нейронов. Нужно (2 + 1) * 16 связей, чтобы соединить первый слой со вторым. Получаем число 48. Это число указано в графе параметр
- На выходном слое 1 нейрон. Его надо соединить с 16ю нейронами и одним bias нейроном. Итого нужно 17 связей
- Получаем всего нужно 48 + 17 = 65 связей и 65 весов для каждой связи.
Обучение нейронной сети
Обучение происходит через метод fit. Передаем в функцию вопросы и правильные ответы. Делаем 250 эпох по 4 обучения в каждой эпохе.
history = []
# Переместим модель на устройство
model = model.to(tensor_device)
for i in range(epochs):
# Вычислим результат модели
model_res = model(tensor_train_x)
# Найдем значение ошибки между ответом модели и правильными ответами
loss_value = loss(model_res, tensor_train_y)
# Добавим значение ошибки в историю, для дальнейшего отображения на графике
loss_value_item = loss_value.item()
history.append(loss_value_item)
# Вычислим градиент
optimizer.zero_grad()
loss_value.backward()
# Оптимизируем
optimizer.step()
# Остановим обучение, если ошибка меньше чем 0.01
if loss_value_item < 0.01:
break
# Отладочная информация
if i % 10 == 0:
print (f"{i+1},\t loss: {loss_value_item}")
# Очистим кэш CUDA
if torch.cuda.is_available():
torch.cuda.empty_cache()
Выведем красивый график. Это обязательно, чтобы понять правильно обучилась нейронная сеть или нет.
import matplotlib.pyplot as plt
plt.plot(history)
plt.title('Loss')
plt.savefig('xor_torch.png')
plt.show()
Как видно из графика, ошибка с каждой эпохи стремится к нулю. Делаем вывод, что нейронная сеть прошла обучение успешно.
Внедрение нейронной сети
Напишем небольшой тест для нейронной сети и проверим как она освоила операцию XOR.
control_x = [
[0, 0],
[0, 1],
[1, 0],
[1, 1],
]
control_x = torch.tensor(control_x).to(torch.float32).to(tensor_device)
print ("Shape:", control_x.shape)
answer = model( control_x )
for i in range(len(answer)):
print(control_x[i].tolist(), "->", answer[i].round().tolist())
Выводит результат:
Shape: torch.Size([4, 2])
[0.0, 0.0] -> [0.0]
[0.0, 1.0] -> [1.0]
[1.0, 0.0] -> [1.0]
[1.0, 1.0] -> [0.0]
Видно, что нейронная сеть отвечает правильно.
Исходный код нейронной сети на TensorFlow
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
##
# Copyright (с) Ildar Bikmamatov 2022
# License: MIT
# Source:
# https://blog.bayrell.org/ru/iskusstvennyj-intellekt/411-obuchenie-mnogoslojnogo-perseptrona-operaczii-xor.html
##
import numpy as np
import tensorflow as tf
import matplotlib.pyplot as plt
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense, Input
# Step 1. Prepare DataSet
data_train = [
{ "in": [0, 0], "out": [0] },
{ "in": [0, 1], "out": [1] },
{ "in": [1, 0], "out": [1] },
{ "in": [1, 1], "out": [0] },
]
# Convert to question and answer DataSet
data_train_question = list(map(lambda item: item["in"], data_train))
data_train_answer = list(map(lambda item: item["out"], data_train))
# Normalize
data_train_question = np.array(data_train_question, "float32")
data_train_answer = np.array(data_train_answer, "float32")
# Print info
print ("Input:")
print (data_train_question)
print ("Shape:", data_train_question.shape)
print ("")
print ("Answers:")
print (data_train_answer)
print ("Shape:", data_train_answer.shape)
# Wait
print ("Press Enter to continue")
input()
# Step 2. Create tensorflow model
model = Sequential(name='XOR_Model')
model.add(Input(shape=(2), name='input'))
model.add(Dense(16, name='hidden', activation='relu'))
model.add(Dense(1, name='output', activation='softmax'))
# Compile
model.compile(loss='mean_squared_error',
optimizer='adam',
metrics=['accuracy'])
# Output model info to the screen
model.summary()
# Wait
print ("Press Enter to continue")
input()
# Step 3. Train model
history = model.fit(data_train_question, # Input
data_train_answer, # Output
batch_size=4,
epochs=250,
verbose=1)
plt.plot( np.multiply(history.history['accuracy'], 100), label='Correct answers')
plt.plot( np.multiply(history.history['loss'], 100), label='Error')
plt.ylabel('%')
plt.xlabel('Epochs')
plt.legend()
plt.savefig('xor_model.png')
plt.show()
# Step 3. Test model
test = [
[0, 0],
[0, 1],
[1, 0],
[1, 1],
]
test = np.asarray(test)
print ("Shape:", test.shape)
answer = model.predict( test )
for i in range(0,len(answer)):
print(test[i], "->", answer[i].round())