35 KiB
ЛАБОРАТОРНАЯ РАБОТА №1.2
ИЗУЧЕНИЕ ОСНОВНЫХ ПОНЯТИЙ ТЕОРИИ ИСКУССТВЕННЫХ НЕЙРОННЫХ СЕТЕЙ
Цель работы: изучение основных понятий теории искусственных нейронных сетей на примере простых задач распознавания логических функций («И», «ИЛИ», «исключающее ИЛИ»).
Импорт библиотек:
import numpy as np
import torch
import matplotlib.pyplot as plt
from IPython.display import clear_output
from torch import nn
%matplotlib inlineВ заключительной части данной лабораторной работы мы применяем PyTorch — современный фреймворк для научных вычислений и глубокого обучения в Python.
PyTorch во многом напоминает NumPy по принципам работы и синтаксису:
- оперирует многомерными массивами (в PyTorch они называются тензорами, или Tensors), которые являются прямым аналогом
ndarrayиз NumPy; - поддерживает похожий синтаксис для базовых операций: индексация, срезы, арифметические действия;
- реализует векторизованные вычисления и механизм broadcasting (широковещательное сложение);
- позволяет выполнять стандартные математические операции с интуитивно понятными методами (например,
tensor.mean()в PyTorch аналогиченarray.mean()в NumPy).
Некоторые основные компоненты PyTorch:
- Тензоры (Tensors)
- многомерные массивы с поддержкой вычислений на GPU;
- аналог
numpy.ndarray, но с расширенными возможностями для машинного обучения.
- Автоматическое дифференцирование (Autograd)
- механизм для автоматического вычисления градиентов;
- критически важен для обучения нейронных сетей (реализует алгоритм обратного распространения ошибки).
- Модули (nn.Module)
- базовые строительные блоки для создания нейронных сетей;
- включают слои, функции активации, функции потерь и др.
- Оптимизаторы (optim)
- реализации популярных алгоритмов оптимизации: SGD, Adam, RMSprop и др.;
- упрощают обновление параметров модели.
Ключевые возможности PyTorch:
- Создание и обучение нейронных сетей любой сложности — от простых перцептронов до трансформеров.
- Вычисления на CPU и GPU с минимальным изменением кода (достаточно переместить тензоры на устройство
cuda). - Динамические вычислительные графы — в отличие от статических графов в некоторых других фреймворках, PyTorch строит граф операций «на лету», что упрощает отладку и эксперименты.
- Гибкость в проектировании архитектур — легко создавать кастомные слои и модели, переопределять методы обратного прохода.
Преимущества для исследований и разработки:
- Интуитивный интерфейс, близкий к NumPy — низкий порог входа для тех, кто знаком с NumPy.
- Гибкая система наследования — возможность создавать собственные модули и слои.
- Обширное сообщество — множество туториалов, примеров, библиотек и форумов.
- Совместимость с другими библиотеками — лёгкая интеграция с NumPy, SciPy, Matplotlib и др.
- Производительность — ускорение вычислений на GPU (в 10–100 раз по сравнению с CPU для крупных задач).
Официальная документация: https://pytorch.org/docs/stable/index.html
В качестве примера вышесказанного переписанная на PyTorch метрика accuracy:
def accuracy(y_pred, y_true):
return torch.sum(y_pred == y_true) / len(y_true)Содержание:
1. Решение задачи логического «И»
2. Решение задачи логического «ИЛИ»
3. Решение задачи логического «исключающего ИЛИ»
1. Решение задачи логического «И»
Представим входные данные для решения задачи логического «И» в виде тензора X_data.
Операции в нейронных сетях (умножение весов, активация, вычисление градиентов) выполняются только с вещественными числами, поэтому через .float() приводим тензор к вещественному типу данных — иначе далее возможна ошибка типа данных.
X_data = torch.tensor([[0, 0], [0, 1], [1, 0], [1, 1]]).float()
print(X_data)Аналогичная операция с выходным вектором:
y_and_data = torch.tensor([0, 0, 0, 1]).reshape(-1, 1).float()
print(y_and_data)Создание нейронной сети в PyTorch: последовательная модель
Один из самых простых и наглядных способов создать нейронную сеть в PyTorch — использовать nn.Sequential. Это контейнер, который организует слои в последовательный пайплайн: данные проходят через каждый слой по порядку, от входа к выходу — выход предыдущего слоя автоматически становится входом следующего.
Создадим данным способом нейронную сеть model_seq с одним слоем нейронов.
Пояснения к выбранным слоям в её архитектуре:
nn.Linear(in_features=2, out_features=1)— полностью соединённый (плотный, полносвязный) слой.
Параметры:
in_features=2— размерность входного вектора (2 признака, соответствующие входам логической операции);out_features=1— размерность выхода (1 нейрон для итогового результата).
Математическая операция:
\[
y = W \cdot X + b,
\]
nn.Sigmoid()— функция активации (сигмоида).
После создания экземпляр model_seq можно:
- вызывать как функцию:
model_seq(X_data)— прямой проход через сеть; - получать параметры:
model_seq.parameters()— для оптимизации; - выводить структуру: печать
model_seqпокажет последовательность слоёв.
model_seq = nn.Sequential(
nn.Linear(in_features=2, out_features=1),
nn.Sigmoid()
)
model_seqПереберём все обучаемые параметры сети model_seq — результатом будет список тензоров, содержащих исходные веса и смещения сети:
[x for x in model_seq.parameters()]Обратите внимание в выводе на флаг requires_grad=True.
Он указывает, что для этого тензора в процессе обратного распространения ошибки нужно будет вычислять градиенты (производные) всех математических операций, через которые далее в коде этот тензор проходит. Без этого флага параметры не будут обновляться при обучении. Флаг при задании тензора также можно переключать на False.
Как это работает:
- Во время прямого прохода (
forward) PyTorch строит вычислительный граф, запоминая операции с тензорами, у которыхrequires_grad=True. - При вызове
loss.backward()автоматически вычисляются градиенты по всем параметрам сrequires_grad=True, при этом значение каждое градиенты сохраняются в атрибуте.gradтензора. - Оптимизатор (объект, реализующий алгоритм обновления параметров модели на основе вычисленных градиентов — например, SGD) использует эти градиенты для обновления параметров.
Рассмотрим всё на примере.
Выберем в качестве оптимизатора параметров model_seq SGD (с достаточно большим шагом обучения lr для наглядности) и среднеквадратичную функцию потерь:
optimizer = torch.optim.SGD(model_seq.parameters(), lr=1.5)
criterion = nn.MSELoss()z = model_seq(X_data)
zАтрибут grad_fn=<SigmoidBackward0> в выводе обозначает:
- Тензор
z— результат работы сигмоиды в последнем слое сети; - PyTorch «запомнил» эту операцию в своём вычислительном графе;
- При обратном распространении ошибки градиенты будут корректно рассчитаны по формуле производной сигмоиды и переданы дальше в сеть.
Рассчитаем значение функции потерь:
loss = criterion(z, y_and_data)
lossВ атрибуте grad_fn=<SigmoidBackward0> аналогично видно, что в вычислительном графе к тензорам применена функция среднеквадратичной ошибки.
Вычислим градиенты функции потерь по всем параметрам модели согласно вычислительному графу:
loss.backward()Обновим с помошью оптимизатора все параметры сети:
optimizer.step()Посмотрим значения обновлённых параметров:
[x for x in model_seq.parameters()]Метод optimizer.zero_grad() вызывается обязательно перед каждым последующим обратным проходом (loss.backward()) в цикле обучения для обнуления градиентов всех параметров модели, управляемых этим оптимизатором.
Почему это необходимо?
В PyTorch градиенты по умолчанию накапливаются (суммируются) при каждом вызове loss.backward(). Если не обнулять градиенты:
- значения градиентов будут расти с каждой итерацией;
- веса модели начнут обновляться некорректно (с учётом «старых» градиентов);
- возможен эффект «взрывных градиентов» (gradients explode), когда значения становятся настолько большими, что превращаются в
NaN(не число).
optimizer.zero_grad()Сделайте несколько проходов (итераций) от первого шага обратного распространения ошибки до данной ячейки. Убедитесь, что ошибка сети loss понижается.
Рассмотрим ещё один способ создания сети в PyTorch.
Создание нейронной сети в PyTorch: через ООП
В PyTorch принято определять архитектуры нейронных сетей как классы, наследующие от nn.Module. Это позволяет:
- структурировать код в виде повторно используемых компонентов;
- чётко разделять инициализацию параметров (
__init__) и прямой проход (forward); - легко расширять и модифицировать модели;
- использовать встроенные механизмы PyTorch (оптимизаторы, сохранение/загрузка моделей).
Пример сети с одним полносвязным слоем и сигмоидой в качестве функции активации:
class OneLayerNetwork(nn.Module):
def __init__(self, n_inputs=2, n_outputs=1):
super().__init__()
self.model = nn.Sequential(
nn.Linear(in_features=n_inputs, out_features=n_outputs),
nn.Sigmoid()
)
def forward(self, X):
return self.model(X)Сейчас все слои для удобства по-прежнему собраны в nn.Sequential. Чтобы повысить гибкость модели, в методе __init__ можно по отдельности задать все необходимые слои, а в forward — определять порядок их применения. Такой способ даёт больше контроля над архитектурой (пример разберём в одной из следующих лабораторных работ).
Сигмоиду здесь можно также заменить на nn.ReLU или другую функцию активации — но для отделения от экземплятор с исходной архитектурой создайте другой класс (например, OneLayerReLUNetwork())
Создадим экземпляр однослойной сети этого класса для решения задачи логического «И»:
model_one_layer = OneLayerNetwork()
model_one_layerПо аналогии с прошлым ноутбуком зададим
- данные для обучения (входные и выходные);
- экземпляр нейронной сети (через последовательность
nn.Sequential()или классOneLayerNetwork()); - количество эпох или итераций в процессе обучения (желательно больше 5);
- скорость обучение и коэффициент импульса;
- оптимизатор (попробуйте разные алгоритмы —
torch.optim.SGD(),torch.optim.Adam()и другие) (значениеlearning_rateпередаётся вlr) (учтите, что вtorch.optim.Adam()нет импульса); - функцию потерь (можно оставить
nn.MSELoss(), однако поскольку решается задача бинарной классификации, можно выбрать бинарную кросс-энтропиюnn.BCELoss()).
Примечание. Вы можете использовать для обучения сети либо код ниже, либо модифицированный на его основе — например, реализовать функцию обучения нейронной сети и проверки её работы.
y_true = y_and_data
# Перебор seed для инициализации параметров
torch.manual_seed(seed=42)
model = # Ваш код здесь
epochs = # Ваш код здесь
learning_rate = # Ваш код здесь
momentum = # Ваш код здесь
optimizer = # Ваш код здесь
criterion = # Ваш код здесьОбучение нейронной сети для решения задачи логического «И»:
loss_history = []
for epoch in range(epochs):
# Метод .train() переводит модель в режим обучения
model.train()
optimizer.zero_grad()
z = model(X_data)
model_answer_interpretation = (z >= 0.5).float()
loss = criterion(z, y_true)
# Метод .item() извлекает скалярное значение из тензора loss
loss_history.append(loss.item())
loss.backward()
optimizer.step()
# Метод .eval() переводит модель в режим валидации/тестирования (об этом в следующей лабораторной)
model.eval()
if (epoch + 1) % 5 == 0:
clear_output(True)
plt.plot(range(1, epoch+2), loss_history, label='Loss')
plt.title(f'Epoch: {epoch + 1}, Loss: {loss:.6f}')
plt.grid(True, alpha=0.3)
plt.legend(loc='best')
plt.show()
print('Test:')
for i in range(len(X_data)):
# Метод .detach() создаёт копию тензора, отсоединённую от вычислительного графа,
# чтобы избежать ненужного вычисления градиентов для исходного тензора
print(f'Input: {X_data.numpy()[i]}, Output: {torch.round(z[i].detach() * 100).numpy() / 100}, Prediction: {model_answer_interpretation[i].numpy()}, True: {y_true[i].numpy()}')
print(f'Accuracy: {accuracy(model_answer_interpretation, y_true):.2f}')Проверим качество обучения сети:
assert accuracy(model_answer_interpretation, y_and_data) == 1
assert np.mean(loss_history[:5]) > np.mean(loss_history[-5:])Поэкспериметируйте с параметрами обучения — сделайте несколько вариантов с разными функциями потерь, оптимизаторами, значениями скорости обучения и коэффициента импульса. Выберите лучшие по качеству обучения и количеству эпох.
2. Решение задачи логического «ИЛИ»
По аналогии с задачей логического «И» решите задачу логического «ИЛИ».
Выходные данные:
y_or_data = torch.tensor([0, 1, 1, 1]).reshape(-1, 1).float()
print(y_or_data)# Ваш код здесь# Ваш код здесьПроверьте качество обучения сети:
assert accuracy(model_answer_interpretation, y_or_data) == 1
assert np.mean(loss_history[:5]) > np.mean(loss_history[-5:])Поэкспериметируйте с параметрами обучения — сделайте несколько вариантов с разными функциями потерь, оптимизаторами, значениями скорости обучения и коэффициента импульса. Выберите лучшие по качеству обучения и количеству эпох.
3. Решение задачи логического «исключающего ИЛИ»
Выходные данные:
y_xor_data = torch.tensor([0, 1, 1, 0]).reshape(-1, 1).float()
print(y_xor_data)Напомним, что однослойной сетью задачу «исключающего ИЛИ» решить нельзя. В нейронную сеть требуется добавить ещё один слой с как минимум двумя нейронами.
Модифицируем для этого класс OneLayerNetwork() в класс TwoLayersNetwork() — в последовательсти в self.model добавим ещё один полносвязный слой с сигмоидой для активации. Добавлен также параметр n_hiddens — количество нейронов в скрытом слое. Обратите внимание на задание размерности в последовательности слоёв.
class TwoLayersNetwork(nn.Module):
def __init__(self, n_inputs=2, n_hiddens=2, n_outputs=1):
super().__init__()
self.model = nn.Sequential(
nn.Linear(in_features=n_inputs, out_features=n_hiddens),
nn.Sigmoid(),
nn.Linear(in_features=n_hiddens, out_features=n_outputs),
nn.Sigmoid()
)
def forward(self, X):
return self.model(X)Создадим экземпляр двухслойной сети этого класса для решения задачи «исключающего ИЛИ»:
model_two_layers = TwoLayersNetwork()
model_two_layersПо аналогии с задачами логического «И» и логического «ИЛИ» решите «задачу исключающего ИЛИ»:
# Ваш код здесь
# Вариант задания сети с разным количество нейронов в скрытом слое
n_hiddens = 2
model = TwoLayersNetwork(n_hiddens=n_hiddens)
# Ваш код здесьПроверьте качество обучения сети:
assert accuracy(model_answer_interpretation, y_xor_data) == 1
assert np.mean(loss_history[:5]) > np.mean(loss_history[-5:])Поэкспериметируйте с параметрами обучения — сделайте несколько вариантов с разными функциями потерь, оптимизаторами, значениями скорости обучения и коэффициента импульса. Выберите лучшие по качеству обучения и количеству эпох.
Литература:
- Бородкин А.А., Елисеев В.Л. Основы и применение искусственных нейронных сетей. Сборник лабораторных работ: методическое пособие. – М.: Издательский дом МЭИ, 2017.
- MachineLearning.ru — профессиональный информационно-аналитический ресурс, посвященный машинному обучению, распознаванию образов и интеллектуальному анализу данных: http://www.machinelearning.ru
- Modern State of Artificial Intelligence — Online Masters program at MIPT: https://girafe.ai/