Физтех.Статистика
Информация не актуальна. Сайт переехал на miptstats.github.io
Скачать ipynb
Введение в анализ данных¶
PyTorch и полносвязные нейронные сети¶
1. Введение¶
В данном ноутбуке мы будем пользоваться фреймворком PyTorch, который предназначен для работы с нейронными сетями. Как установить pytorch
можно прочитать на официальном сайте PyTorch. Для этого выберите свою OS и вам будет показана нужная команда для ввода в терминале. Больше подробностей о том, как pytorch
работает будет рассказно на 3 курсе.
import numpy as np
from sklearn.datasets import load_boston
import matplotlib.pyplot as plt
import seaborn as sns
from IPython.display import clear_output
sns.set(palette='Set2', font_scale=1.5)
import torch
from torch import nn
import torch.nn.functional as F
print(torch.__version__)
1.1 Сравнение NumPy и PyTorch-синтаксиса¶
Интерфейс pytorch
написан подобно интерфесу numpy
для удобства использования. Главное различие между ними, что numpy
оперрирует numpy.ndarray
массивами, а pytorch
— тензорами pytorch.Tensor
. Напишем одни и те же операции на numpy
и pytorch
.
numpy
x = np.arange(16).reshape(4, 4)
print("Матрица X:\n{}\n".format(x))
print("Размер: {}\n".format(x.shape))
print("Добавление константы:\n{}\n".format(x + 5))
print("X*X^T:\n{}\n".format(np.dot(x, x.T)))
print("Среднее по колонкам:\n{}\n".format(x.mean(axis=-1)))
print("Кумулятивная сумма по колонкам:\n{}\n".format(np.cumsum(x, axis=0)))
pytorch
x = np.arange(16).reshape(4, 4)
x = torch.tensor(x, dtype=torch.float32) # или torch.arange(0,16).view(4,4)
print("Матрица X:\n{}".format(x))
print("Размер: {}\n".format(x.shape))
print("Добавление константы:\n{}".format(x + 5))
print("X*X^T:\n{}".format(torch.matmul(x, x.transpose(1, 0)))) # кратко: x.mm(x.t())
print("Среднее по колонкам:\n{}".format(torch.mean(x, dim=-1)))
print("Кумулятивная сумма по колонкам:\n{}".format(torch.cumsum(x, dim=0)))
Всё же некоторые названия методов отличаются от numpy-евских. Полной совместимости с numpy пока нет, но от верчии к версии она увеличвается, и придется сново запоминать новые названия для некоторых методов.
Например, Pytorch имеет другое написание стандартных типов
x.astype('int64') -> x.type(torch.LongTensor)
Для более подробного ознакомления можно посмотреть на табличку перевода методов из numpy в pytorch, а также заглянуть в документацию. Также при возникновении проблем часто помогает зайти на pytorch forumns.
1.2 NumPy <-> PyTorch¶
Можно переводить numpy-массив в torch-тензор и наоборот. Например, чтобы сделать из numpy-массива torch-тензор, можно сделать так:
# зададим numpy массив
x_np = np.array([2, 5, 7, 1])
# 1-й способ
x_torch = torch.tensor(x_np)
print(type(x_torch), x_torch)
# 2-й способ
x_torch = torch.from_numpy(x_np)
print(type(x_torch), x_torch)
Аналогично и с переводом обратно: функция x.numpy()
переведет torch-тензор x в numpy-массив, причем типы переведутся соответственно табличке.
x_np = x_torch.numpy()
print(type(x_np), x_np)
1.3 Еще один пример¶
Давайте нарисуем по сетке данную кривую на графике, используя pytorch:
$$x(t) = 2 \cos t + \sin 2t \cos 60t,$$$$y(t) = \sin 2t + \sin 60t.$$t = torch.linspace(-10, 10, steps=10000)
x = 2 * torch.cos(t) + torch.sin(2 * t) * torch.cos(60 * t)
y = torch.sin(2 * t) + torch.sin(60 * t)
plt.plot(x, y)
plt.xlabel('x')
plt.ylabel('y')
plt.show()
Заметим, что matplotlib
справляется с отображением pytorch
-тензоров, и дополнительных преобразований делать не нужно.
2. Простой пример обучения нейронной сети¶
2.1 Цикл обучения модели¶
Пусть у нас есть вход $x \in \mathbb{R}^d$. Мы построили нейронную сеть $model$, состоящую из обучаемых параметров $w$ и $b$. На выходе она возвращает некоторый ответ $\widehat{y} = model(x)$. Для обучения такой сети мы задаем функцию, которую будем минимизировать. Тогда процесс обучения задается так:
- Прямой проход / Forward pass
Считаем $\widehat{y}$ и также запоминаем значения выходов всех слоев; - Вычисление оптимизируемой функции
Вычисляем оптимизируемую функцию на текущем наборе объектов; - Обратный проход / Backward pass
Считаем градиенты по всем обучаемым параметрам и запоминаем их; - Шаг оптимизации
Делаем шаг градиентного спуска, обновляя все обучаемые веса.
2.2 Линейная регрессия¶
Сделаем одномерную линейную регрессию на датасете boston.
Скачиваем данные.
boston = load_boston()
Будем рассматривать зависимость таргета от последнего признака в данных.
plt.figure(figsize=(10,7))
plt.scatter(boston.data[:, -1], boston.target, alpha=0.7)
plt.xlabel('Признак')
plt.ylabel('Таргет');
В данном случае ответ модели задается следующим образом: $$\widehat{y}(x) = wx + b.$$
Объявляем обучаемые параметры, в данном случае у нас всего 2 скалярных параметра $w, b$. Также задаем вход $x$ и таргеты $y$ в виде torch-тензоров.
# создаем два тензора размера 1 с заполнением нулями,
# для которых будут вычисляться градиенты
w = torch.zeros(1, requires_grad=True)
b = torch.zeros(1, requires_grad=True)
# Данные оборачиваем в тензоры, по которым не требуем вычисления градиента
x = torch.FloatTensor(boston.data[:, -1] / 10)
y = torch.FloatTensor(boston.target)
# по-другому:
# x = torch.tensor(boston.data[:, -1] / 10, dtype=torch.float32)
# y = torch.tensor(boston.target, dtype=torch.float32)
print(x.shape)
print(y.shape)
Задалим оптимизируемую функцию — MSE, и сделаем обратный проход loss.backward()
:
def optim_func(y_pred, y_true):
return torch.mean((y_pred - y_true) ** 2)
# Прямой проход
y_pred = w * x + b
# Подсчет лосса
loss = optim_func(y_pred, y)
# Вычисление градиентов
# с помощью обратного прохода по сети
# и сохранение их в памяти сети
loss.backward()
Здесь loss
— значение функции MSE, вычисленное на этой итерации.
loss
К градиентам для параметров, которые требуют градиента (requires_grad=True
), теперь можно обратиться следующим образом:
print("dL/dw =", w.grad)
print("dL/b =", b.grad)
Если мы посчитаем градиент $M$ раз, то есть $M$ раз вызовем loss.backward()
, то градиент будет накапливаться (суммироваться) в параметрах, требующих градиента. Иногда это бывает удобно.
Убедимся на примере, что именно так все и работает.
y_pred = w * x + b
loss = optim_func(y_pred, y)
loss.backward()
print("dL/dw =", w.grad)
print("dL/b =", b.grad)
Видим, что значения градиентов стали в 2 раза больше, за счет того, что мы сложили одни и те же градиенты 2 раза.
Если же мы не хотим, чтобы градиенты суммировались, то нужно занулять
градиенты между итерациями после того как сделали шаг градиентного спуска.
Это можно сделать с помощью функции zero_
для градиентов.
w.grad.zero_()
b.grad.zero_()
w.grad, b.grad
Напишем код, обучающий нашу модель.
def show_progress(x, y, y_pred, loss):
'''
Визуализация процесса обучения.
x, y -- объекты и таргеты обучающей выборки;
y_pred -- предсказания модели;
loss -- текущее значение ошибки модели.
'''
# Избавимся от градиентов перед отрисовкой графика
y_pred = y_pred.detach()
# Превратим тензор размерности 0 в число, для краисивого отображения
loss = loss.item()
# Стираем предыдущий вывод в тот момент, когда появится следующий
clear_output(wait=True)
# Строим новый график
plt.figure(figsize=(10, 7))
plt.scatter(x, y, alpha=0.75)
plt.scatter(x, y_pred, color='orange', linewidth=5)
plt.xlabel('Признак')
plt.ylabel('Таргет')
plt.show()
print(f"MSE = {loss:.3f}")
# Инициализация параметров
w = torch.zeros(1, requires_grad=True)
b = torch.zeros(1, requires_grad=True)
# Количество итераций
num_iter = 1000
# Скорость обучения для параметров
lr_w = 0.01
lr_b = 0.05
for i in range(num_iter):
# Forward pass: предсказание модели
y_pred = w * x + b
# Подсчет оптимизируемой функции (MSE)
loss = optim_func(y_pred, y)
# Обратный проход: подсчет градиентов
loss.backward()
# Оптимизация: обновение параметров
w.data -= lr_w * w.grad.data
b.data -= lr_b * b.grad.data
# Зануление градиентов
w.grad.zero_()
b.grad.zero_()
# График + вывод MSE через каждые 5 итераций
if (i + 1) % 5 == 0:
show_progress(x, y, y_pred, loss)
if loss.item() < 39:
print("Готово!")
break
2.3 Улучшение модели¶
Попробуем усложнить модель. Сделаем еще один слой.
# Инициализация параметров
w0 = torch.ones(1, requires_grad=True)
b0 = torch.ones(1, requires_grad=True)
w1 = torch.ones(1, requires_grad=True)
b1 = torch.ones(1, requires_grad=True)
# Функция активации
def act_func(x):
return x * (x >= 0)
# Количество итераций
num_iter = 1000
# Скорость обучения для параметров
lr_w = 0.01
lr_b = 0.05
for i in range(num_iter):
# Forward pass: предсказание модели
y_pred = w1 * act_func(w0 * x + b0) + b1
# Подсчет оптимизируемой функции (MSE)
loss = optim_func(y_pred, y)
# Bakcward pass: подсчет градиентов
loss.backward()
# Оптимизация: обновление параметров
w0.data -= lr_w * w0.grad.data
b0.data -= lr_b * b0.grad.data
w1.data -= lr_w * w1.grad.data
b1.data -= lr_b * b1.grad.data
# Зануление градиентов
w0.grad.zero_()
b0.grad.zero_()
w1.grad.zero_()
b1.grad.zero_()
# График + вывод MSE через каждые 5 итераций
if (i + 1) % 5 == 0:
show_progress(x, y, y_pred, loss)
if loss.item() < 33:
print("Готово!")
break
Полученная модель лучше описывает данные.
3. Готовые модули из PyTorch¶
На практике нейронные сети так не пишут, пользуются готовыми модулями. Напишем такую же нейросеть, но теперь с помощью pytorch. Для этого будем пользоваться torch.nn
.
model = nn.Sequential( # собираем модули в последовательность
nn.Linear(in_features=1, out_features=1), # кол-во признаков во входном слое 1, в выходном тоже 1
nn.ReLU(), # та же ф-ция активации, что и раньше, только из pytorch
nn.Linear(in_features=1, out_features=1) # кол-во признаков во входном слое 1, в выходном тоже 1
)
model
Для того, чтобы работать с данной моделью, нам понадобится поменять размерность x и y.
x_new = x.reshape(-1, 1)
y_new = y.reshape(-1, 1)
Применим модель к нашим данным и посмотрим на результаты для первых 10 элементов.
model(x_new)[:10]
Посмотрим на параметры модели с помощью функции named_parameters
, которая кроме параметров, выдает также их названия.
for name, param in model.named_parameters():
print(name)
print(param.data)
Инициализируем параметры так же, как мы делали для подобной модели ранее. На этот раз воспользуемся функцией parameters
, она возвращает только параметры.
for p in model.parameters():
p.data = torch.FloatTensor([[1]])
print(p.data)
Ранее мы оптимизацию производили самостоятельно. Теперь же сделаем это с помощью оптимизатора SGD из pytorch. Установим скорость обучения на уровне 0.01 для всех параметров сразу. Также заменим нашу написанную MSE функцию на соответствуюшую из pytorch.
optimizer = torch.optim.SGD(model.parameters(), lr=0.01)
optim_func = nn.MSELoss()
Обучим полученную модель на наших данных. Теперь обновления значений параметров происходят с помощью вызова optimzer.step()
, а зануление градиентов — optimizer.zero_grad()
.
# Количество итераций
num_iter = 10000
for i in range(num_iter):
# Forward pass: предсказание модели
y_pred = model(x_new)
# Подсчет оптимизируемой функции (MSE)
loss = optim_func(y_pred, y_new)
# Bakcward pass: подсчет градиентов
loss.backward()
# Оптимизация: обновление параметров
optimizer.step()
# Зануление градиентов
optimizer.zero_grad()
# График + вывод MSE через каждые 5 итераций
if (i + 1) % 5 == 0:
show_progress(x, y, y_pred, loss)
if loss.item() < 35:
print("Готово!")
break
Полученная модель довольно хорошо приближает данные, однако дольше сходится к оптимумум за счет меньшей скорости обучения для параметров сдвига.