Занятие 7
Лабораторная 7¶
1. Библиотека PyTorch¶
PyTorch - это популярная библиотека глубокого обучения, которая позволяет создавать и обучать различные модели и архитектуры нейронных сетей. Кроме того, данная библиотека используется для обработки данных и оптимизации.
# импортируем библиотеки
import numpy as np
import os
import torch
from torch import nn
from torch.nn import functional as F
from torch.utils.data import TensorDataset, DataLoader
from torchvision import transforms as tfs
from torchvision.datasets import MNIST
1.1 Тензоры в PyTorch¶
Тензоры в PyTorch - это основные структуры данных, которые используются для представления данных и выполнения операций в библиотеке PyTorch. Тензоры очень похожи на многомерные массивы в NumPy.
Можно выделить несколько особенностей тензоров в PyTorch:
Работа с данными: Тензоры могут хранить числовые данные и многомерные массивы, такие как изображения, звуковые сигналы и текстовые данные. Они предоставляют удобный способ представления и обработки различных типов данных в машинном обучении.
Автоматическое дифференцирование: в PyTorch присутствует автоматическое вычисление градиента функций. Это полезно при обучении нейронных сетей.
Гибкость: Тензоры могут быть созданы, преобразованы и использованы для выполнения различных математических операций, таких как сложение, умножение, свертка и много других, что делает их удобным и эффективным инструментом для работы с данными и моделями обучения.
# Пример создания тензора из списка:
l = [1, 2, 3, 4, 5, 6, 7, 8, 9]
tensor = torch.tensor(l)
print(tensor)
# Пример создания тензора из массива NumPy:
numpy_array = np.array([[1, 2, 3], [4, 5, 6]])
tensor = torch.tensor(numpy_array)
print(tensor)
# Пример создания нулевого тензора определенного размера:
zeros_tensor = torch.zeros(2, 3)
print(zeros_tensor)
# Пример создания тензора с единицами:
ones_tensor = torch.ones(3, 3)
print(ones_tensor)
# Пример создания тензора со случайными значениями:
random_tensor = torch.rand(3, 3)
print(random_tensor)
# Пример создания float тензора:
torch_float = torch.FloatTensor(20)
print(torch_float)
# Создание тензора
x = torch.tensor([[1, 2, 3], [4, 5, 6]])
# Узнать размер тензора
print(x.size())
# Узнать размер тензора
print(x.shape)
1.2 Функции с тензорами в PyTorch¶
Изменение формы¶
# Создание тензора
x = torch.tensor([[1, 2, 3], [4, 5, 6]])
# Изменение формы тензора
y = x.view(3, 2)
print(x)
print(y)
Обратите внимание, что .view оставляет старый тензор без изменений!
Арифметические операции¶
# Создание тензоров
x = torch.tensor([[1, 2], [3, 4]])
y = torch.tensor([[1, 2], [3, 4]])
# Сложение тензоров
tensor_add = torch.add(x, y)
print("Сложение тензоров:")
print(tensor_add)
# Вычитание тензоров
tensor_sub = torch.sub(x, y)
print("Вычитание тензоров:")
print(tensor_sub)
# Умножение тензоров поэлементно
tensor_mul = torch.mul(x, y)
print("Поэлементное умножение тензоров:")
print(tensor_mul)
# Деление тензоров поэлементно
tensor_div = torch.div(x, y)
print("Поэлементное деление тензоров:")
print(tensor_div)
# Создание тензоров
x = torch.tensor([[1, 2], [3, 4]])
y = torch.tensor([[1, 2], [3, 4]])
# Сложение тензоров
tensor_add = x + y
print("Сложение тензоров:")
print(tensor_add)
# Вычитание тензоров
tensor_sub = x - y
print("Вычитание тензоров:")
print(tensor_sub)
# Умножение тензоров поэлементно
tensor_mul = x * y
print("Поэлементное умножение тензоров:")
print(tensor_mul)
# Деление тензоров поэлементно
tensor_div = x / y
print("Поэлементное деление тензоров:")
print(tensor_div)
Операторы сравнения тензоров¶
# Создание тензоров
x = torch.tensor([[1, 2], [3, 4]])
y = torch.tensor([[1, 3], [3, 5]])
# Оператор сравнения (равенства)
print("Оператор сравнения (равенства):")
print(torch.eq(x, y))
# Оператор сравнения (больше или равно)
print("Оператор сравнения (больше или равно):")
print(torch.ge(x, y))
# Оператор сравнения (меньше или равно)
print("Оператор сравнения (меньше или равно):")
print(torch.le(x, y))
# Создание тензоров
x = torch.tensor([[1, 2], [3, 4]])
y = torch.tensor([[1, 3], [3, 5]])
# Оператор сравнения (равенства)
print("Оператор сравнения (равенства):")
print(x == y)
# Оператор сравнения (больше или равно)
print("Оператор сравнения (больше или равно):")
print(x >= y)
# Оператор сравнения (меньше или равно)
print("Оператор сравнения (меньше или равно):")
print(x <= y)
Применение математических функций¶
x = torch.tensor([0, 1, 2, 3, 4])
# Применение универсальной функции к тензору
y = torch.sin(x) # Применение синуса к каждому элементу тензора
print(y)
# Еще примеры универсальных функций
z = torch.exp(x) # Применение экспоненты к каждому элементу тензора
print(z)
w = torch.log(x) # Применение натурального логарифма к каждому элементу тензора
print(w)
Агрегация¶
# Создание тензора
x = torch.tensor([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]])
# Нахождение максимального значения в тензоре
max_value = torch.max(x)
print("Максимальное значение в тензоре:", max_value)
# Нахождение минимального значения в тензоре
min_value = torch.min(x)
print("Минимальное значение в тензоре:", min_value)
# Вычисление среднего значения тензора
mean_value = torch.mean(x)
print("Среднее значение тензора:", mean_value)
# Вычисление суммы значений тензора
sum_value = torch.sum(x)
print("Сумма значений тензора:", sum_value)
Матричные операции¶
A = torch.tensor([[1, 2], [3, 4]])
B = torch.tensor([[5, 6], [7, 8]])
# Сложение матриц
C = A + B
print("Сложение матриц:")
print(C)
# Умножение матриц
C = torch.matmul(A, B)
print("Результат умножения матриц:")
print(C)
print("Результат умножения матриц:")
print(A @ B)
# Транспонирование матрицы
D = torch.transpose(A, 0, 1)
print("Транспонированная матрица:")
print(D)
1.3 Работа с осями в PyTorch¶
# Создание тензора
x = torch.tensor([[1, 2, 3], [4, 5, 6]])
# Вычисление суммы значений по определенной оси
# По умолчанию ось равна -1 (последняя ось)
sum_along_axis_0 = torch.sum(x, dim=0)
print("Сумма значений по оси 0:", sum_along_axis_0)
sum_along_axis_0 = torch.sum(x, dim=1)
print("Сумма значений по оси 1:", sum_along_axis_0)
# Вычисление максимальных значений по определенной оси
max_along_axis_1 = torch.max(x, dim=0)
print("Максимальные значения по оси 0:", max_along_axis_1.values)
print("Индексы максимальных значений по оси 0:", max_along_axis_1.indices)
max_along_axis_1 = torch.max(x, dim=1)
print("Максимальные значения по оси 1:", max_along_axis_1.values)
print("Индексы максимальных значений по оси 1:", max_along_axis_1.indices)
Задача 1(3 балла)¶
- Создайте два тензор размера 4 на 2 и 2 на 4 из целых чисел.
- Умножьте два тензора поэлементно и матрично.
- Вычислить сумму значений тензоров, полученных в пункте 2.
- Создайте тензор размера 15 на 13 из случайных чисел. Транспонируйте данный тензор.
- Найти максимальное значение в тензоре, найдите максимальное значение в каждом столбце и строке.
- Умножить матрицу, созданную в пункте 4 на скаляр.
- Создайте тензор размера 15 на 1 из случайных чисел. Изменить форму тензора (преобразовать одномерный тензор в двумерный и наоборот).
- Найти сумму значений по определенной оси тензора пункта 6.
- Создайте тензор размера 100 на 20 из случайных чисел. Найдите среднее от максимального значения по каждому столбцу.
2 Полносвязная нейронная сеть¶
2.1 Модель нейрона¶
Модель нейрона - это математическая модель. Она обычно состоит из следующих элементов:
Входные данные (Input): На вход модели нейрона поступают признаки или значения, которые необходимо обработать. На картинке выше - это $x_1, x_2, ... x_N$.
Веса (Weights): Каждый входной признак соотносится с определенным весом, который отражает его важность для вычислений. В процессе обучения модель обучается и веса меняются таким образом, чтобы уменьшить функцию потерь. На картинке выше - это $w_0, w_1, w_2, ... w_N$.
Сумматор (Aggregator): В сумматоре происходит вычисление скалярного произведения $u = <x,w> = w_0 \cdot 1 + w_1 \cdot x_1 + ... w_N \cdot x_N$.
Функция активации (Activation Function): Результат сумматора $u$ проходит через функцию активации $f(u)$, которая добавляет нелинейность в модель.
Выход (Output): На основе результата функции активации нейрон генерирует свое значение - $y$, которое затем может передаваться другим нейронам.
Данная математическая модель имеет некоторую связь с биологической моделью:
В биологических терминах $x_1, x_2 ... x_N$ можно рассматривать как входные сигналы, поступающие к дендритам нейрона.
В биологических терминах функция активации $f(u)$ может отвечать за возбуждение или подавление сигналов, пришедших к нейрону.
2.2 Функции активации¶
Давайте теперь перечислим самые популярные функции активации:
Сигмоида (Sigmoid): Функция сигмоиды преобразует входное значение в диапазоне от 0 до 1. $$ \sigma(x) = \frac{1}{1 + e^{-x}} $$ Данная функция потерь может иметь проблему, связанную с затуханием градиента, но об этом мы поговорим позже.
Гиперболический тангенс (Tanh): Эта функция активации преобразует входное значение в диапазоне от -1 до 1. Ее формула: $$ \tanh(x) = \frac{e^{x} - e^{-x}}{e^{x} + e^{-x}}$$ Данная функция потерь может иметь проблему, связанную с затуханием градиента, но об этом мы поговорим позже.
ReLu (Rectified Linear Unit): Это одна из самых популярных функций активации. Он заменяет отрицательные значения на 0, а положительные значения оставляет без изменений: $$ f(x) = \max(0, x) $$
Leaky ReLU: Эта функция аналогична ReLU, но в случае отрицательных значений она допускает небольшие отрицательные значения: $$ f(x) = \max(ax, x) $$ где a - небольшая константа.
Softmax: Эта функция активации применяется обычно в последнем слое нейронной сети для задач классификации. Она преобразует вектор значений на входе в вероятностное распределение, сумма которых равна 1. Он обычно используется для многоклассовой классификации.
ELU (Exponential Linear Unit): Это функция активации, которая является вариантом ReLU с небольшим отрицательным смещением. Его формула: $$ f(x) = \begin{cases} x & \text{если } x > 0, \\ a(e^x - 1) & \text{если } x \leq 0, \end{cases} $$ где a - небольшая положительная константа.
Задача 2 (1 балл)¶
Реализуйте модель нейрона.
Реализуйте функцию, принимающую на вход N признаков $x_1, ... x_N$, а также N + 1 весов $w_0, ... w_N$, и вычисляющая $f(u)$. В качестве функции активации возьмите сигмойду.
Продемонстрируйте работу функции.
def neuron(x, weights):
###ВАШ КОД
2.3 Полносвязная нейронная сеть¶
Элементы полносвязной нейронной сети включают в себя несколько основных компонентов:
Входной слой: Этот слой принимает входные данные, например, значения признаков объекта. Количество нейронов в этом слое соответствует числу признаков объекта. На картинке это $x_1 ... x_n$.
Скрытые слои: Полносвязная нейронная сеть может содержать один или более скрытых слоев, где каждый нейрон в слое связан со всеми нейронами предыдущего и последующего слоев. Скрытые слои выполняют сложные нелинейные преобразования входных данных.
Выходной слой: Этот слой представляет собой окончательные выходы нейронной сети, которые могут быть связаны с конкретными классами (в случае классификации) или представлять числовые значения (в случае регрессии).
Веса и смещения: Каждая связь между нейронами имеет свой вес, который определяет степень важности связи.
Функции активации: Для внесения нелинейности в сеть используются функции активации.
Обучение нейронной сети осуществляется с помощью метода backpropagation. Предположим, у нас есть простая нейронная сеть с одним скрытым слоем. Мы хотим обучить эту сеть для решения задачи регрессии, поэтому функцией потерь будет среднеквадратичная ошибка (MSE).
Прямой проход (forward pass): Пусть у нас есть входной вектор данных $\mathbf{x}$, веса скрытого слоя $\mathbf{W}$, смещения скрытого слоя $\mathbf{b}$, активационная функция скрытого слоя $\sigma$, веса выходного слоя $ \mathbf{V}$ и смещение выходного слоя $c$. Выход скрытого слоя вычисляется как $\mathbf{h} = \sigma(\mathbf{Wx} + \mathbf{b})$, Затем происходит вычисление выхода сети $ y = \mathbf{Vh} + c $.
Функция потерь: Для задачи регрессии мы используем среднеквадратичную ошибку: $L(y, y_{true}) = \frac{1}{2} (y - y_{true})^2$.
Обратный проход (backward pass): Вернемся к выражению для выхода: $y = \mathbf{V\sigma(\mathbf{Wx} + \mathbf{b})} + c$. Для обновления весов сети в направлении уменьшения ошибки нам нужно вычислить градиенты функции потерь по параметрам сети, то есть по весам $\mathbf{W}$, $\mathbf{b}$, $\mathbf{V}$ и $ c $.
Обновление весов: Получив градиенты, мы используем их в алгоритме градиентного спуска (или других методах оптимизации) для обновления весов сети.
На данный момент может возникнуть вопрос, а как мы всё-таки считаем градиенты и обновляем веса. Этот вопрос мы рассмотрим позже в следующей лабораторной работе. Пока же достаточно понимать, что во время обучения происходит обновление весов с целью уменьшения функции потерь.
Теперь же давайте построим нейронную сеть и обучим её для классификации датасета MNIST.
Подготовка датасета¶
В PyTorch класс Compose из модуля torchvision.transforms представляет собой способ объединения нескольких преобразований данных (трансформаций) в одно целое.
tfs.ToTensor() преобразует данные в тензоры.
tfs.Normalize() нормализует данные.
data_tfs = tfs.Compose([
tfs.ToTensor(),
tfs.Normalize((0.5), (0.5))
])
root = './'
train_dataset = MNIST(root, train=True, transform=data_tfs, download=True)
val_dataset = MNIST(root, train=False, transform=data_tfs, download=True)
print(type(train_dataset[0]))
print(type(train_dataset[0][0]))
print(type(train_dataset[0][1]))
Таким образом теперь 'train_dataset' - это объект содержащий tuple. Каждый tuple содержит в себе тензор - картинку и саму цифру, которой соответствует данная картинка.
### тензор - картинка
train_dataset[0][0].shape
### сама цифра - класс
train_dataset[0][1]
DataLoader это инструмент в библиотеке PyTorch, который упрощает процесс подготовки данных для обучения нейронных сетей. Он позволяет эффективно загружать и обрабатывать данные в параллельных процессах во время обучения модели. DataLoader автоматически разделяет данные на батчи, выполняет преобразования и перемешивание данных.
Эпоха и batch в данном случае имеют тоже значение, что и в стохастическом градиентном спуске.
train_dataloader = DataLoader(train_dataset, batch_size=128)
valid_dataloader = DataLoader(val_dataset, batch_size=128)
В данном случае batch_size = 128, т.е мы делаем обновление весов на выборке из 128 элементов.
Архитектура сети¶
Теперь давайте же зададим архитектуру нашей сети:
activation = nn.ReLU()
model = nn.Sequential(
# nn.Flatten преобразует входные данные, двумерный тензор,
#в одномерный тензор путем "сплющивания" всех осей на входном тензоре, кроме первой.
nn.Flatten(),
# линейный слой со 784 признаками на входе и 256 на выходе
nn.Linear(28 * 28, 256),
# функция активации ReLU
activation,
# линейный слой со 256 признаками на входе и 128 на выходе
nn.Linear(256, 128),
# функция активации ReLU
activation,
# линейный слой со 128 признаками на входе и 64 на выходе
nn.Linear(128, 64),
# функция активации ReLU
activation,
# линейный слой со 64 признаками на входе и 10 на выходе
nn.Linear(64, 10)
)
nn.CrossEntropyLoss() - это функция потерь (loss function) в библиотеке PyTorch, которая часто используется для задач классификации.
torch.optim.Adam - очень популярный и эффективный оптимизатор. Как правило работает лучше, чем стохастический градиентный спуск: torch.optim.SGD
# функция потерь
criterion = nn.CrossEntropyLoss()
# оптимизатор - то что обновляет веса модели(model.parameters)
optimizer = torch.optim.Adam(model.parameters())
# словарь для удобства вывода результатов
loaders = {"train": train_dataloader, "valid": valid_dataloader}
device = "cuda" if torch.cuda.is_available() else "cpu"
Обучение¶
Метод model.train() используется в библиотеке PyTorch для установки модели в режим обучения. При вызове этого метода модель переходит в режим обучения, что означает, что PyTorch будет отслеживать градиенты всех параметров модели для обновления их в процессе обучения с помощью метода обратного распространения ошибки (backpropagation).
Метод model.eval() используется в библиотеке PyTorch для установки модели в режим оценки (evaluation mode) или режима инференса (inference mode). При вызове этого метода модель переходит из режима обучения в режим оценки.
В режиме оценки модель не отслеживает градиенты и не обновляет параметры в процессе обучения. Это позволяет ускорить вычисления и сэкономить ресурсы, поскольку нет необходимости хранить и вычислять градиенты во время оценки или предсказания.
# число эпох
max_epochs = 10
# словарь для хранения точности на обучающей и на тестовой выборке(valid)
accuracy = {"train": [], "valid": []}
# итерируемся по датасету столько раз, сколько число эпох
for epoch in range(max_epochs):
# k принимает два значения - train и valid
for k, dataloader in loaders.items():
epoch_correct = 0
epoch_all = 0
# выбираем batch из выборки, по нему и будем делать оптимизациооный шаг
for x_batch, y_batch in dataloader:
if k == "train":
# переходим в "режим обучения"
model.train()
# обнуляем градиенты модели
optimizer.zero_grad()
# выходные значения модели
outp = model(x_batch)
# считаем функцию потерь
loss = criterion(outp, y_batch)
# считаем градиенты
loss.backward()
# делаем шаг градиентного спуска
optimizer.step()
else:
# переходим в "режим тестирования"
model.eval()
# отключение вычисления и хранения градиентов
with torch.no_grad():
outp = model(x_batch)
# вычисляем точность
preds = outp.argmax(-1)
correct = (preds == y_batch).sum()
all = y_batch.shape[0]
epoch_correct += correct.item()
epoch_all += all
# выводим точность
if k == "train":
print(f"Epoch: {epoch+1}")
print(f"Loader: {k}. Accuracy: {epoch_correct/epoch_all}")
accuracy[k].append(epoch_correct/epoch_all)
У вас может возникнуть несколько вопросов, которые мы рассмотрим в следующей лабораторной работе:
- Как работает backpropagation с математической точки зрения?
- Как выглядит функция потерь с математической точки зрения?
- Есть ли какие - нибудь методы регуляризации нейронных сетей?
Задача 3(2 балла)¶
Попробуйте использовать разные функции активации: сигмоиду, tanh, и ELU для классификации датасета MNIST. Постройте график зависимости точности на valid выборке от числа эпох(до 10) для каждой функции активации. Какая даёт лучший результат?
Задача 4(3 балла)¶
Попробуйте использовать различное число слоёв nn.Linear. В сети выше их 4. Попробуйте 3, 5, 7. В качестве функции активации возьмите ту, что показала лучший результат в задаче 3. Постройте график зависимости точности на valid выборке от числа эпох(до 10) для разного числа слоёв. Какое число слоёв даёт лучший результат?
Задача 5(1 балл)¶
Постройте график зависимости функции потерь(loss) от числа итераций для лучшей модели в задаче 4.