Вы находитесь на странице: 1из 11

ФЕДЕРАЛЬНОЕ АГЕНТСТВО СВЯЗИ

ФЕДЕРАЛЬНОЕ ГОСУДАРСТВЕННОЕ БЮДЖЕТНОЕ ОБРАЗОВАТЕЛЬНОЕ


УЧРЕЖДЕНИЕ ВЫСШЕГО ОБРАЗОВАНИЯ
«САНКТ-ПЕТЕРБУРГСКИЙ ГОСУДАРСТВЕННЫЙ УНИВЕРСИТЕТ ТЕЛЕКОММУНИКАЦИЙ ИМ. ПРОФ.
М.А. БОНЧ-БРУЕВИЧА»
(СПбГУТ)

ИНСТИТУТ НЕПРЕРЫВНОГО ОБРАЗОВАНИЯ

Лабораторная работа №7

по дисциплине
«Теория информации, данные, знания»

Фамилия: Алексеев
Имя: Егор
Отчество: Алексеевич
Курс: 3
№ зачетной книжки: 2010108
Группа №: ИБ – 04з

Проверил:______________

Санкт-Петербург
2023
Лабораторная работа №7
по дисциплине «Теория информации, данные, знания»
«Решение задачи регрессии с помощью нейронной сети»

Цель работы – исследование принципов разработки нейронной сети на


примере
задачи регрессии.

первая ячейка направлена на то, чтобы рисовать графики. Она не очень


интересная. Далее мы импортируем torch. И нам нужно составить "train
dataset". Как мы его будем составлять?

Возьмём точки из равномерного распределения от нуля до единицы,


100 штук таких, каждую точку домножим на 20, отнимем от неё 10, чтобы
график примерно по центру был -- это будут наши "X". А "y" -- это будут
синусы от данных точек.

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


в неё немного шума. Шум будет из нормального распределения, выглядит он
вот таким вот образом

нам хочется, чтобы они были правильной размерности. Ведь, по сути,


признаком объекта может быть не одно число, как здесь (координата "X", по
которой мы хотим предсказать координату "Y"), а может быть сразу
несколько чисел. Соответственно, для общности, нам нужно наш вектор X
(который сейчас строчка), превратить в столбец, у которого в каждой строчке
будет одно число X. 
Давайте это проверим, вообще проверим как работает метод unsqueeze.
Возьмём некоторый тензор одномерный (один, два, три). Назовём его "tmp" и
сделаем на нем метод unsqueeze(1). И видим, что до этого это была
действительно строчка, некоторый вектор одномерный, а теперь это столбец,
то есть двумерный тензор.
Кроме train dataset нам нужен будет отдельный validation dataset, то
есть обычно когда у нас есть какая-то задача машинного обучения, мы делим
наш dataset на тренировочные данные, и те на которых мы будем тестировать
или валидироваться, и сеть обучается на тренировочных данных и,
соответственно, валидируется на тех данных которые она не видела. 

Конечно это не очень жизненно, потому что у вас никогда такого не


будет --  что ваши данные будут не зашумлены,  там всегда будет некоторый
шум. Но в нашем тренировочном примере мы возьмём валидацию: обычный
синус. То есть создадим две переменные "x_validation" и "y_validation".
Посмотрим, как выглядит график этой функции.
И теперь нам нужно создать нейронную сеть. Вот мы и добрались до
самого интересного. Чтобы создать нейронную сеть, нам нужно создать
класс, назовём его "SineNet", предполагая, что это будет нейросеть, которая
решает задачу восстановления синуса. Её мы должны отнаследовать от
класса torch.nn.Module. Вот такое наследование внесет в наш объект
дополнительные функции, которые мы сейчас же будем использовать. Кроме
того, нам нужно проинициализировать те слои, которые будут
использоваться в сети. То есть мы пишем функцию "__init__", она на вход
может принимать что угодно, любые параметры, которые нам будет
интересно передать в эту сеть в момент конструирования. Нам, например,
интересно передать количество скрытых нейронов, которые будут храниться
в каждом слое, то есть мы предполагаем, что все слои будут одинакового
размера. Там будет N "hidden neurons". Соответственно инициируем
родительский объект. И давайте создавать слои: первый слой, который будет
называться fc1, это "fully connected" слой, полносвязанный слой. В PyTorch
"fully connected" слой называется "linear". Что мы передаём на вход? Мы
передаём количество входных нейронов и количество выходных нейронов.
Входных нейронов у нас будет один. То есть это, на самом деле, не нейрон
будет, а сам вход в нейрон. Это одно число "x", координата нашей точки, по
которой мы будем что-то предсказывать. Если бы у нас координата точки
была какая-то многомерная, если точка у нас бы задалась в многомерном
пространстве, то -- размерность того пространства, которое задаёт точку.
Выходных нейронов, у нас будет как раз "n_hidden_neurons".
После этого нам нужна функция активации. Кстати, попробуйте убрать
функцию активации, узнайте что получится. Функцию активации мы здесь
берем -- сигмоиду.  В принципе, нам бы подошла любая функция активации.
Но сигмоида-- самая простая, почему бы и нет. Кроме того, мы добавим ещё
один полносвязанный слой, но у него будет всего один нейрон. Этот нейрон
будет нашим ответом на вопрос. Так как у нас задача регрессии, нас
интересует ответ, который является одним числом, то на выходе нашей сети
должен быть один нейрон. В итоге, наша нейросеть будет выглядеть, как два
слоя, в одном из них будет несколько нейронов а во втором будет один. И
теперь нам нужно написать функцию forward, то есть то, как наши слои
последовательно применяются. Сначала мы применяем слой "fc1", на "x". То
что получилось мы передаём в функцию активации, то что вышло из
функций активации мы передаём в "fc2", и вот это же мы возвращаем. То
есть, в принципе, функция forward повторяет нашу инициализацию. И
давайте создадим такую сеть. Количество скрытых нейронов -- 50, чтобы
точно хватило. У нас теперь есть "SineNet" -- объект, который, в принципе,
можно обучать. Можно предсказывать.
Давайте не будем ничего обучать, а сразу предскажем. А вдруг уже
сразу работает? Напишем некоторую функцию "predict", внутри она будет
очень простая -- там будет вызов метода "forward", как раз метод forward у
нас написан (он, вот реализован). И если вы передадите туда некоторую
переменную X, на выходе у вас будет некоторый prediction (то что мы хотим:
одно число). И далее есть некоторый код, который рисует вот этот prediction.
Давайте посмотрим что происходит. Тут на самом деле, два графика, синим
обозначен groud truth, (то, что мы бы хотели на валидации увидеть), X -- это
то, что мы передаём в сеть а Y -- это то что мы бы хотели чтобы сеть вернула,
а красными точками обозначено то, что сеть нам предсказала. Нетрудно
догадаться, что, так как у нас сеть была инициирована случайными числами
(то есть, когда вы задаёте слои, они инициализируются некоторыми
случайными числами), то на выходе у нас получилась некоторая случайная
кривая (она может быть разная в зависимости от запуска).
Давайте теперь обучим эту нейросеть. Чтобы обучить нейросеть, нам
нужно несколько вещей, дополнительно. Во-первых нам нужен некоторый
оптимизатор -- некоторый объект, который будет совершать для нас шаги
градиентного спуска. Я взял torch.optim.adam. Вы можете попробовать что-то
другое. Пока что вы не знаете что такое ADAM (вы знаете, что такое SGD -- 
это то, что мы реализовывали -- обычный градиентный спуск, но в данной
задаче он работает не очень хорошо, собственно, поэтому мы и используем
другие градиентные спуски). Давайте пока просто используем ADAM, не
будем концентрировать внимание, "что он делает". Очень важно что, на вход
Adam передаются те параметры, которые мы хотим модифицировать -- те
параметры, которые мы хотим обучать в нейронной сети. Можно было
подумать, что это "x", но это не так, потому что "x" -- это наши точки, мы не
можем на них никак повлиять. Зато мы можем повлиять на веса нейронной
сети -- те веса, которые хранятся в нейронах (вот эти веса, они находятся в
sine_net.parameters). Это одна из тех причин, почему нейросеть мы
наследовали, а не создавали как класс, с нуля. Соответственно, если мы
передадим в Adam вот такой объект, то ADAM поймёт, что здесь лежат все
те переменные, которые он может модифицировать вследствие градиентного
спуска. И ещё необходимо передать "learning rate" -- шаг градиентного
спуска. Здесь у нас "0.01", можно тоже поиграться, разные взять, к чему это
приводит, интересно.

Создадим такой оптимизатор. И ещё, нам нужна функция потерь -- та


функция, которая нам говорит, насколько неправильно мы предсказали,
насколько мы ошиблись. Это та функция, по которой будет происходить
вычисление градиента и которая будет участвовать в градиентном спуске.
Скажем, что функция loss, это у нас вот такая функция. Если вы
присмотритесь, это на самом деле MSE (mean squared error). Мы берём
предсказания нейронной сети, мы берём target, то есть реальное значение,
которое соответствует данной точке, возводим это в квадрат и считаем
среднее значение этой суммы квадратов. Loss function у нас есть. Давайте
начинать тренировку. Если мы возьмём весь наш датасет, прогоним его через
нейросеть, получим некоторые предсказания. После этого, на этих
предсказаниях посчитаем функцию потерь, которую мы только что задали.
После этого у этой функции потерь посчитаем производную и сделаем
градиентный шаг. Это будет называться эпохой. То есть мы посмотрели на
все наши данные и сделали градиентный шаг. Забегая вперёд -- мы можем
делать градиентные шаги несколько раз, просматривая все данные. Эпоха --
это то, что мы посмотрели один раз датасет. Соответственно, может быть,
нам потребуется много эпох. Тут я взял две тысячи, чтобы точно хватало. Что
мы делаем внутри одной итерации, внутри одной эпохи? Сначала мы
зануляем градиенты. Можно было занулять градиенты в конце, но обычно
про это забываешь, поэтому лучше сразу делать это вначале. Каждая эпоха
начинается с того, что у нас у оптимайзера обнуляются градиенты. После
этого мы считаем forward, то есть мы берём наш весь X_train и передаём его
в функцию forward, считаем prediction (считаем предсказания нашей
нейросети), после этого мы считаем функцию потерь, получаем некоторое
число: это скаляр, по которому мы можем сделать backward. Делаем по этому
скаляру backward, то есть это некоторый тензор, который зависит от
параметров сети, то есть от весов нейросети, который обернут в оптимайзер,
и, соответственно, когда мы делаем loss_val.backward, оптимайзер понимает,
что там посчитались градиенты, и значит оптимайзер может сделать шаг. Вот
это весь цикл обучения на одной эпохе.  Давайте проведём обучение на 2000
эпохах и посмотрим, что получится. И мы видим что у нас ну очень хороший
результат. То есть мы обучались на зашёумлённых данных, которые совсем
не похожи на синусы.  А получили некоторые точки, которые здесь отмечены
красным. Оказывается, что у нас довольно хорошие результаты, то есть мы
обучались на зашумлённых данных, а получили действительно функцию,
которая очень близка к синусу. Она -- не идеальный, синус потому что
данные, которые приходили, объективно, они были не очень похожи на
идеальный синус.
Вывод
Давайте повторим основные этапы создания. Сперва мы взяли
трейновый датасет в правильном формате. У него должно быть количество
строчек -- это количество элементов, и колонки -- это некоторые признаки.
Здесь у нас был один признак, поэтому в колонке стояло одно число. Кроме
того, мы взяли валидационный датасет. Как правило, валидационный датасет
берётся как часть трейнового датасета, но здесь мы его сгенерировали
самостоятельно. После этого мы задали архитектуру нейросети в классе
SineNet. Далее нам потребовался оптимайзер. Это объект, который
оборачивает все параметры нейросети. И ещё понадобилась loss function, то
есть та функция потерь, которая будет считаться по выходам нейронной сети.
После этого мы смогли провести обучение нейросети.  Каждую эпоху мы
брали весь наш датасет, передавали в нейронную сеть и получали некоторые
предсказания. Эти предсказания мы передавали в функцию потерь, после
этого считали backward, то есть градиент этой функции потерь. Далее у
оптимайзера вызывали метод step, и надо ещё не забывать обнулять градиент.
И получается, что так, шаг за шагом, веса нашей нейросети приходят к
оптимальным значениям -- тем значениям, которые лучше всего
предсказывают целевую функцию.
import torch

import matplotlib.pyplot as plt
import matplotlib
# Задаем тренировочные данные
x_train = torch.rand(400)
x_train = x_train * 20.0 - 10.0
print (max(x_train))
y_train = torch.sin(x_train)

# Добавляем шум к данным
noise = torch.randn(y_train.shape) / 5.
y_train = y_train + noise

# Переворачиваем данные
x_train.unsqueeze_(1)
y_train.unsqueeze_(1)
print(x_train.shape)

# Данные для валидации
x_validation = torch.linspace(-10, 10, 400)
y_validation = torch.sin(x_validation.data)Ы
x_validation.unsqueeze_(1)
y_validation.unsqueeze_(1)

# Задаем нейронную сеть
class SineNet(torch.nn.Module):
    def __init__(self, n_hidden_neurons):
        super(SineNet, self).__init__()
        self.fc1 = torch.nn.Linear(1, n_hidden_neurons)
        self.act1 = torch.nn.Sigmoid()
        self.fc2 = torch.nn.Linear(n_hidden_neurons, 1)

    def forward(self, x):
        x = self.fc1(x)
        x = self.act1(x)
        x = self.fc2(x)
        return x

sine_net = SineNet(50)

# Расчет по нейронке и отрисовка сети
matplotlib.rcParams['figure.figsize'] = (13.0, 5.0)
def predict(net, x, y,num):
    y_pred = net.forward(x)
    plt.clf()
    plt.plot(x.numpy(), y.numpy(), 'o', label='Ground truth')
    plt.plot(x.numpy(), y_pred.data.numpy(), 'o', c='r', label='Prediction
')
    plt.legend(loc='upper left')
    plt.xlabel('$x$')
    plt.ylabel('$y$')
    plt.ylim(-1.1, 1.1)
    plt.text(7.1, -0.9, num, fontsize=20)
    num = num.replace(':', '-')
    print(num)
    plt.savefig(fname=f"{num}.png")
  #  plt.show()

#predict(sine_net, x_validation, y_validation,"Test")

# Оптимизатор
optimizer = torch.optim.Adam(sine_net.parameters(), lr=0.01)

# Лосс функция SGD
def loss(pred, target):
    squares = (pred - target) ** 2
    return squares.mean()

torch.cuda.is_available()
torch.device('cuda:0')
device = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')
print(device)

# Расчет Картинки
sine_net.fc1.reset_parameters()
sine_net.fc2.reset_parameters()
for epoch_index in range(2000):
    optimizer.zero_grad()
    y_pred = sine_net.forward(x_train)
    loss_val = loss(y_pred, y_train)
    loss_val.backward()
    optimizer.step()
    predict(sine_net, x_validation, y_validation,f"epoch:{epoch_index}")

Вам также может понравиться