Files
neurocomputers-python/lab3/3.2_rbf.ipynb

36 KiB
Исходник Ответственный История

ЛАБОРАТОРНАЯ РАБОТА №3

Применение многослойного персептрона. Автоассоциативная ИНС

Цель работы: знакомство с применением многослойного персептрона для решения задач сжатия данных, прогнозирования временных рядов и распознавания образов.

Задание

  1. Открыть файл с данными по минеральной воде, который использовался при решении задач классификации в предыдущей лабораторной работе. Построить и обучить автоассоциативные нейронные сети с 2-мя и 3-мя нейронами в скрытом слое:
    а) для исходных данных из 5-ти классов;
    б) для исходных данных из 4-х классов.
    Провести визуализацию данных в скрытом слое каждой сети на плоскость и в 3-х мерное пространство. Проанализировать полученные результаты. Выбрать и сохранить автоассоциативные ИНС, обеспечивающие наилучшее сжатие исходных данных.
  2. Исследовать возможности ИНС по прогнозированию поведения нелинейных динамических систем (построение странного аттрактора) на примере отображения Хенона. Аттрактор Хенона может быть получен из уравнений \(x_{n+1} = 1 - \alpha x_{n}^2 + y_{n}\) и \(y_{n+1} = \beta x_{n}\), где \(α = 1.4\), \(β = 0.3\). Для прогнозирования предлагается использовать многослойный персептрон и сеть с радиально-базисными функциями. Постройте также прогноз курса доллара на один день вперед. В качестве исходных данных загрузить актуальные данные с сайта центрального банка России (http://www.cbr.ru).
  3. Решить задачу распознавания 9-ти изображений самолетов. Исходные данные (файлы avia1.bmp, …, avia9.bmp) необходимо предварительно преобразовать в набор векторов со значениями признаков 0 или 1. Обученная нейронная сеть должна правильно определять модель самолета и его класс (истребитель/бомбардировщик). Принадлежность модели к определенному классу выбирается студентом самостоятельно.

Импорт библиотек:

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import torch
from IPython.display import clear_output
from torch import nn
from sklearn.cluster import KMeans
from sklearn.linear_model import LinearRegression

%matplotlib inline

Среднеквадратическая ошибка:

def mse(y_pred, y_true):
    return np.mean((y_pred - y_true) ** 2)

Содержание:

1. Подготовка данных
2. Сеть с радиально-базисными функциями
3. Многослойный персептрон
4. Сравнение моделей
5. Прогнозирование курса доллара

1. Подготовка данных

Функция с реализацией отображения Хенона — уравнения \(x_{n+1} = 1 - \alpha x_{n}^2 + y_{n}\) и \(y_{n+1} = \beta x_{n}\), где \(α = 1.4\), \(β = 0.3\):

def xenon_map(x=0, y=0, alpha=1.4, beta=0.3):
    x_next = 1 - alpha * x ** 2 + y
    y_next = beta * x
    return x_next, y_next

Укажите количество точек во временном ряде, который будет получен из отображения Хенона:

n_points = # Ваш код здесь

Сгенерируем необходимое количество точек — помним, что нужна только переменная \(x\):

x, y = 0, 0
xenon_data = []
for i in range(n_points):
    x, y = xenon_map(x, y)
    xenon_data.append(x)

Поскольку исходная задача является задачей прогнозирования, то обучающая выборка должна включать в себя первый участок временного ряда, на котором будет обучаться модель. Валидационная выборка, на которой мы оцениваем качество предсказания модели, всегда должна идти после обучающей (аналогично с тестовой).

Выделим, к примеру, первые 70% данных временного ряда xenon_data для обучающей выборки, следующие данные по 15% отнесём к вылидационной и тестовой выборкам (вторая и третья части данных соответственно):

train_size = int(0.7 * n_points)
valid_size = int(0.15 * n_points)

Представим данные для выборок на графике:

plt.plot(xenon_data, color='k', label='Xenon Data')
plt.axvspan(0, train_size, alpha=0.35, color='blue', label='Train')
plt.axvspan(train_size, train_size+valid_size, alpha=0.35, color='orange', label='Valid')
plt.axvspan(train_size+valid_size, n_points, alpha=0.35, color='green', label='Test')
plt.legend(loc='best')
plt.grid(True, alpha=0.3)
plt.show()

Пройдём с единичным шагом по всем данным xenon_data скользящим окном длиною seq_length(например, длиною 10). Таким образом сформируем пары «вход‑выход» для модели:

  • X_data: последовательности из seq_length элементов (скользящее окно по xenon_data),
  • y_data: элемент, следующий сразу за каждой последовательностью в X_data.

Примечание. С помощью [:-1] мы убираем в X_data последнее окно, для которого нет «следующего значения» в y_data.

seq_length = 10

X_data = np.lib.stride_tricks.sliding_window_view(xenon_data, window_shape=seq_length)[:-1]
y_data = xenon_data[seq_length:]

Посмотрим конец полученных данных. Видно, что последнее значение y_data не включено в окна из X_data:

X_data[-2:]
y_data[-4:]

Длина наших данных — без учёта последнего значения:

assert len(X_data) == len(y_data) == n_points - seq_length

Разделим данные на обучающую, валидационную и тестовую выборку по заданному выше соотношению 70%/15%/15%:

X_train, X_valid, X_test = X_data[:train_size], X_data[train_size:train_size+valid_size], X_data[train_size+valid_size:]
y_train, y_valid, y_test = y_data[:train_size], y_data[train_size:train_size+valid_size], y_data[train_size+valid_size:]

Отнормируйте или отстанлартизируйте входные и выходные данные.

Поскольку нормирующие или стандартизирующие величины (минимум, максимум, среднее, СКО) всегда расчитываются только по значениям обучающей выборки, берём из исходных данных xenon_data также значения, которые попадут в последнее окно (и снова исключаем самое последнее значение):

X_mean = np.mean(xenon_data[:train_size+seq_length-1])
X_std = np.std(xenon_data[:train_size+seq_length-1], ddof=1)
y_mean = np.mean(y_data[:train_size])
y_std = np.std(y_data[:train_size], ddof=1)

X_train_scaled = # Ваш код здесь
X_valid_scaled = # Ваш код здесь
X_test_scaled = # Ваш код здесь

y_train_scaled = # Ваш код здесь
y_valid_scaled = # Ваш код здесь
y_test_scaled = # Ваш код здесь

2. Сеть с радиально-базисными функциями

РБФ-сеть (Radial Basis Function Network, RBF) — это нейронная сеть, которая использует радиально-базисные функции в качестве функций активации нейронов скрытого слоя. Архитектура такой сети обычно включает три слоя:

  • Входной слой — принимает входные данные (значения временного ряда).
  • Скрытый слой — содержит нейроны с радиальными базисными функциями. Каждый нейрон вычисляет расстояние между входным вектором и заранее определённым центром функции, а затем преобразует это расстояние с помощью радиальной функции.
  • Выходной слой — линейно комбинирует (взвешенно суммирует) выходы скрытых нейронов, чтобы получить итоговый результат.

Таким образом, выходной сигнал РБФ‑сети вычисляется по формуле:

\[ y(\mathbf{x}) = \sum_{j=1}^{H} w_j \cdot \phi(\|\mathbf{x} - \mathbf{c}_j\|_2) \] где:

  • \(y(\mathbf{x})\) — выходной сигнал сети для входного вектора \(\mathbf{x}\);
  • \(H\) — количество нейронов в скрытом слое;
  • \(w_j\) — вес связи от \(j\)-го нейрона скрытого слоя к выходному слою;
  • \(\mathbf{c}_j\) — центр \(j\)-й радиально-базисной функции;
  • \(\phi\) — радиально-базисная функция;
  • \(\|\mathbf{x} - \mathbf{c}_j\|_2\) — евклидово расстояние между входным вектором \(\mathbf{x}\) и центром \(\mathbf{c}_j\).

Радиально-базисная функция — это функция, которая зависит только от расстояния между входным вектором и центром функции. Чаще всего используется гауссова функция: \[ \phi(r) = \exp\left(-\frac{r^2}{2\sigma^2}\right) \] где \(r = \|\mathbf{x} - \mathbf{c}\|_2\), \(\sigma\) — параметр ширины окна.

Обучение РБФ‑сетей обычно проходит в два этапа:

  1. Определение параметров радиально-базисной функции (центров \(\mathbf{c}_i\) и ширин \(\sigma_i\)):
    • кластеризация (например, kmeans) для нахождения центров;
    • эвристические методы или кросс‑валидация для ширин.
  2. Обучение выходных весов \(w_j\):
    • решение задачи линейной регрессии (с использованием обучения с учителем, например, методом наименьших квадратов или градиентного спуска).

Таким образом, на первом этапе сеть определяет, где в пространстве признаков сосредоточены основные закономерности данных. Для этого, например, используются алгоритмы кластеризации: объекты (значения временного ряда) внутри одного кластера получатся максимально похожи друг на друга, а объекты разных классов — максимально различны (т.о. определим важные зоны или паттерны ряда).

После определения центров сеть анализирует, как каждая точка соотносится с ними. Это показывает гауссова РБФ — для каждой точки вычисляется расстояние до каждого из центров, и чем ближе точка к центру, тем выше значение функции (близко к 1), а также наоборот — чем дальше точка к центру, тем ниже (РБФ стремится к 0). Для каждой точки получается вектор активаций — набор чисел, показывающих, насколько она «похожа» на каждый из центров. Так, точка между двумя центрами даст средние значения для обоих, а точка рядом с одним центром — высокое значение для него и низкие для остальных.

На этих активациях для получения весов между скрытым и выходным слоями обучается линейная регрессия. Она находит оптимальные веса для каждого выхода скрытого слоя: какие локальные паттерны важнее для прогноза, какие — менее значимы.

Реализуем с помощью класса RBFPredictor РБФ-сеть с описанной выше архитектурой.

При инициализации (.__init__()) задаётся n_centers — число центров (нейронов скрытого слоя), устанавливается параметр ширины окна sigma для гауссовой функции, создаётся модель линейной регрессии linear_model для выходного слоя (взята из библиотеки sklearn).

Выбор n_centers центров из входных данных происходит в методе .fit(). Для этого используется алгоритм kmeans (взят из библиотеки sklearn).

Далее в скрытом слое (метод ._radial_basis()) для каждого входного вектора (объекта) вычисляются расстояния до всех центров. Расстояния преобразуются в значения гауссовой РБФ. На выходе получается матрица активаций размером (число объектов, число центров).

На данной матрице активаций скрытого слоя phi и целевых значениях y обучается модель линейной регрессии linear_model. Веса регрессии становятся весами связей от скрытого слоя к выходному.

При прогнозировании (метод .predict()) для новых данных вычисляются активации скрытого слоя (через гауссовы РБФ относительно тех же центров), а обученная линейная модель применяет веса к этим активациям и выдаёт финальный прогноз.

class RBFPredictor:
    def __init__(self, n_centers, sigma=1.0):
        self.n_centers = n_centers
        self.sigma = sigma
        self.centers = None
        self.linear_model = LinearRegression()


    def _radial_basis(self, X, centers):
        distances = np.zeros((X.shape[0], centers.shape[0]))
        for i, center in enumerate(centers):
            distances[:, i] = np.sqrt(np.sum((X - center) ** 2, axis=1))
        return np.exp(-(distances ** 2) / (2 * self.sigma ** 2))


    def fit(self, X, y):
        # Выбираем центры с помощью кластеризации
        kmeans = KMeans(n_clusters=self.n_centers, random_state=42)
        self.centers = kmeans.fit(X).cluster_centers_
        # Вычисляем выход скрытого слоя
        phi = self._radial_basis(X, self.centers)
        # Обучаем линейный выходной слой
        self.linear_model.fit(phi, y)


    def predict(self, X):
        phi = self._radial_basis(X, self.centers)
        return self.linear_model.predict(phi)

Обучите модель model_rbf. Для этого подберите подходящее количество центров n_centers и ширину окна sigma.

model_rbf = RBFPredictor(
    # Ваш код здесь
)

model_rbf.fit(X_train_scaled, y_train_scaled)

При подборе добейтесь примерно одинаковой (или хотя бы сопоставимой) ошибки на обучающей и вадилационной выборках:

pred_rbf_train = model_rbf.predict(X_train_scaled)
pred_rbf_valid = model_rbf.predict(X_valid_scaled)

print('Loss')
print(f'Train: {mse(pred_rbf_train, y_train_scaled):.6f}')
print(f'Valid: {mse(pred_rbf_valid, y_valid_scaled):.6f}')

Проверка model_rbf на тестовых данных будет ниже.

3. Многослойный персептрон

Представим входные и выходные данные в виде тензоров PyTorch:

X_train_tensor = torch.tensor(X_train_scaled).float()
y_train_tensor = torch.tensor(y_train_scaled).reshape(-1, 1).float()
X_valid_tensor = torch.tensor(X_valid_scaled).float()
y_valid_tensor = torch.tensor(y_valid_scaled).reshape(-1, 1).float()
X_test_tensor = torch.tensor(X_test_scaled).float()
y_test_tensor = torch.tensor(y_test_scaled).reshape(-1, 1).float()

Реализуйте в классе MLPPredictor с помощью полносвязных слоёв nn.Linear многослойный персептрон. В качестве промежуточных функций активации используйте nn.ReLU(), а поскольку решается задача прогнозирования вещественных данных, на выходе сети функцию активации можно не добавлять.

class MLPPredictor(nn.Module):
    def __init__(self, input_size):
        super().__init__()
        self.seq = nn.Sequential(
            # Ваш код здесь
        )

    def forward(self, x):
        return self.seq(x)

Создайте экземпляр модели:

model_mlp = MLPPredictor(input_size=seq_length)
model_mlp

Проверим, как модель обучается. Зададим оптимизатор и среднеквадратическую функцию потерь:

optimizer = torch.optim.SGD(model_mlp.parameters(), lr=0.01)
criterion = nn.MSELoss()

Рассчитаем значение функции потерь:

pred_mlp_train = model_mlp(X_train_tensor)

loss = criterion(pred_mlp_train, y_train_tensor)
loss

Выполните несколько раз эту и предыдущую ячейку, чтобы убедиться в уменьшении ошибки:

loss.backward()
optimizer.step()
optimizer.zero_grad()

Задайте параметры для обучения нейронной сети:

# Перебор seed для инициализации параметров
torch.manual_seed(seed=42)

model_mlp = # Ваш код здесь

epochs = # Ваш код здесь

learning_rate = # Ваш код здесь
momentum = # Ваш код здесь

optimizer = # Ваш код здесь
criterion = # Ваш код здесь

Обучение нейронной сети:

loss_train_history, loss_valid_history = [], []

for epoch in range(epochs):
    # Ваш код здесь

    if (epoch + 1) % 5 == 0:

        clear_output(True)
        plt.plot(range(1, epoch+2), loss_train_history, label='Train', color='green')
        plt.plot(range(1, epoch+2), loss_valid_history, label='Valid', color='red')
        plt.title(f'Epoch: {epoch + 1}, Loss Train: {loss_train_history[-1]:.6f}, Loss Valid: {loss_valid_history[-1]:.6f}')
        plt.grid(True, alpha=0.3)
        plt.legend(loc='best')
        plt.show()

4. Сравнение моделей

Получим прогнозы на тестовой выборке от РБФ-сети и многослойного персептрона:

pred_rbf_test = model_rbf.predict(X_test_scaled)

with torch.no_grad():
    pred_mlp_test = model_mlp(X_test_tensor).squeeze().numpy()

Поскольку обучение шло на нормированных или стандартизированных данных, приведём прогнозы к исходной шкале и рассчитаем среднеквадратическую ошибку:

pred_rbf_test_descaled = (pred_rbf_test  * y_std + y_mean)
pred_mlp_test_descaled = (pred_mlp_test  * y_std + y_mean)

print('Loss')
print(f'RBF: {mse(pred_rbf_test_descaled, y_test):.6f}')
print(f'MLP: {mse(pred_mlp_test_descaled, y_test):.6f}')

Построим графики прогнозов для сравнения с исходными тестовыми данными:

plt.figure(figsize=(12, 4))

plt.plot(y_test, label='True Values')
plt.plot(pred_rbf_test_descaled, label='RBF Pred')
plt.plot(pred_mlp_test_descaled, label='MLP Pred')
plt.grid(True, alpha=0.3)
plt.legend(loc='best')

plt.show()

5. Прогнозирование курса доллара

По аналогии с отображением Хенона самостоятельно реализуйте прогнозирование курса доллара.

Для этого скачайте актуальную информацию по курсу с сайта http://www.cbr.ru в формате .xlsx.

Введите в виже строки имя скачанного файла с расширением:

dollar_course_filename = # Ваш код здесь

Загрузим данные в массив NumPy:

dollar_data = pd.read_excel(dollar_course_filename).curs[::-1]

# По необходимости можно сгладить данные скользящим окном -
# в данном случае берём среднее за 7 дней
dollar_data = dollar_data.rolling(window=7).mean().dropna()

dollar_data = dollar_data.values
# Ваш код здесь
# Ваш код здесь
# Ваш код здесь

Литература:

  1. Бородкин А.А., Елисеев В.Л. Основы и применение искусственных нейронных сетей. Сборник лабораторных работ: методическое пособие. – М.: Издательский дом МЭИ, 2017.
  2. MachineLearning.ru — профессиональный информационно-аналитический ресурс, посвященный машинному обучению, распознаванию образов и интеллектуальному анализу данных: http://www.machinelearning.ru
  3. Modern State of Artificial Intelligence — Online Masters program at MIPT: https://girafe.ai/