Революционный подход к нейросетям: рассказываем про KAN (Kolmogorov-Arnold Networks)
Эволюция архитектуры нейронных сетей уходит корнями в фундаментальные работы, заложенные в 1940-х годах Уорреном Маккаллохом и Уолтером Питcом, которые предложили концепцию искусственных нейронов и их взаимосвязь.
Однако значительные прорывы произошли только в 1980-х годах с разработкой алгоритмов обратного распространения ошибки: алгоритм Геоффри Хинтона и других — все это позволило создавать более глубокие нейронные сети и улучшить методы обучения.
В это время появились классические архитектуры, многослойные перцептроны (MLP, и сверточные нейронные сети (CNN), которые революционизировали различные области, включая компьютерное зрение, обработку естественного языка и распознавание образов — теперь мы говорим про своего рода инновационную архитектуру.
В основе этой новаторской концепции лежит теорема представления Колмогорова-Арнольда, математическая теория, разработанная Владимиром Арнольдом и Андреем Колмогоровым. Причем достаточно давно, вот только исследователи разработали архитектуру и небольшую библиотеку под работу недавно.
Теорема утверждает, что сложные многомерные функции могут быть разложены на более простые одномерные функции, полагая основу для уникальной структуры нейросети KAN.
Проклятие размерности MLP
Входной слой состоит из нейронов, каждый из которых представляет один признак входных данных. Если у вас есть d признаков, то входной слой будет состоять из d нейронов. Эти нейроны просто передают входные значения в первый скрытый слой.
Каждый признак соответствует одному измерению входного вектора. Входной вектор передается в MLP, где каждый элемент вектора (признак) умножается на соответствующий вес и передается на следующий слой.
Проклятие размерности (curse of dimensionality) — это термин, введенный Ричардом Беллманом в 1957 году, описывающий различные феномены, которые возникают при анализе и организации данных в пространствах высокой размерности.
Проклятие размерности в нейронных сетях — это проблема, возникающая при работе с данными, у которых очень много признаков или параметров.
Представьте, что у вас есть таблица с данными, и каждая строка — это набор характеристик какого-то объекта. Чем больше таких характеристик, тем больше измерений у вашего пространства данных.
В низкоразмерных пространствах (например, с двумя или тремя признаками) данные могут быть плотно расположены и легко анализируемые. С увеличением числа измерений (размерностей) объём пространства данных растет экспоненциально. Очень быстро.
Представьте, что у нас есть MLP, который должен классифицировать объекты по двум признакам (например, рост и вес).
Это можно представить как двухмерное пространство, где каждый объект описывается точкой с координатами (рост, вес). Допустим, что мы хотим разделить это пространство на области, которые соответствуют различным классам объектов.
Теперь предположим, что у нас появляется третий признак, например возраст. Теперь каждый объект описывается точкой в трёхмерном пространстве (рост, вес, возраст). Модель должна разделить это трёхмерное пространство на области, соответствующие классам объектов.
Для того чтобы обучить MLP эффективно классифицировать объекты в этом высокоразмерном пространстве, нам нужно достаточное количество данных, чтобы покрыть это пространство.
В двухмерном случае для точного представления данных может потребоваться несколько десятков или сотен точек. В пространстве, чтобы иметь такую же плотность данных, требуется экспоненциально больше точек — примерно , где — количество точек данных на одно измерение.
Рассмотрим пример с простым MLP, у которого 100 входных признаков и 50 нейронов в скрытом слое.
Пусть у нас есть 1000 тренировочных примеров. В этом случае соотношение данных к параметрам составляет 1000/5000 = 0.2, что очень мало для эффективного обучения.
Теперь увеличим количество входных признаков до 1000. Количество весов станет 1000×50=50000
Для того чтобы сохранить соотношение данных к параметрам на приемлемом уровне, скажем 10:1, нам потребуется 500000 тренировочных примеров, что существенно больше и может быть трудно достижимо на практике.
Большинство точек данных находятся далеко друг от друга, и статистические закономерности, наблюдаемые в низкоразмерных пространствах, становятся менее значимыми.
Представьте, что вы стараетесь найти иголку в стоге сена, но теперь сена в тысячу раз больше, чем раньше. Причем вы уже не различаете самые маленькие соломинки из-за их разреженности.
И это мы не говорим про переобучение. В высокоразмерных пространствах нейронные сети требуют большего количества параметров для эффективного обучения, что может привести к переобучению (overfitting). Хотя здесь подключаются «методы регуляризации».
Это приводит к нескольким сложностям.
Во-первых, для того чтобы модель могла эффективно учиться на таких данных, ей нужно гораздо больше примеров, чтобы покрыть всё это огромное пространство.
Во-вторых, различия между данными становятся менее заметными, и модель может с трудом находить закономерности.
В-третьих, вычислительные ресурсы, необходимые для обработки и анализа этих данных, сильно увеличиваются — в разы.
Для преодоления этих проблем используются различные методы, такие как понижение размерности (например, метод главных компонент (PCA), t-SNE), архитектуры, которые лучше работают с высокоразмерными данными и т.д. Но совсем недавно исследователи предложили новый способ работы. Радикально, но верно.
Поменять MLP на KAN. Сойти с нейронов к их связям.
Почему KAN обещает нам преодолеть надоедливый curse of dimensionality: теорема Колмогорова — Арнольда и B-spline.
Теорема представления Колмогорова-Арнольда звучит примерно так: сложные функции многих переменных могут быть представлены в виде суперпозиции более простых одномерных функций.
Короче говоря, любую сложную функцию можно представить в виде простых.
Представьте, что у вас есть сложная функция, которая зависит от нескольких переменных, например, . Это может быть что-то вроде — квадратичная функция двух переменных.
Теперь, вместо того чтобы рассматривать эту сложную функцию как одно целое, теорема Колмогорова-Арнольда говорит нам, что мы можем разложить эту функцию на более простые части, которые зависят только от одной переменной.
Другими словами, мы можем выразить как сумму нескольких функций, каждая из которых зависит только от одной переменной. Например, мы можем разложить:
на ,
где зависит только от зависит только от , а
Но хорошо, есть у нас разбитый полином на простую сумму функций, а зачем нам вообще это нужно? Чтобы при помощи сплайнов формировать гибкие функции активации, сократить число параметров и лучше контролировать процесс обучения.
Если мы можем найти все «мелкие функции» — нам больше не нужно переходить на высокие размерности — проблема curse of dimensionality решена.
А как вообще выглядят эти функции?
KAN параметризирует функции активации, чтобы сделать их обучаемыми. В этом кроется весь смысл архитектуры: переход от активации нейронов — к граням нейросети.
А как управлять локально параметрами и переобучать функции активации? — B-сплайнами.
Сплайны — это математические функции, которые позволяют создавать плавные кривые, соединяя серию управляющих точек.
Кусочные полиномы, также известные как сплайны, являются мощным инструментом в математике и науке о данных для приближения и интерполяции функций.
Они представляют собой полиномы, которые определены на отрезках (или «кусках») и объединены таким образом, чтобы обеспечить гладкость и непрерывность на всем интервале.
— сложная нейронная функция, которую мы хотим аппроксимировать или представить.
— количество базисных функций (B-сплайнов).
базисная функция (B-сплайн), зависящая от переменной (x).
— весовой коэффициент, который определяет вклад каждого B-сплайна в общую функцию .
Базисные функции обычно определяются рекурсивно, используя рекуррентные формулы, такие как формулы де Бура.
Эти функции могут быть локальными, что позволяет им вносить вклад только в определенные участки области определения функции — B-сплайны гибче и эффективнее для аппроксимации сложных функций.
Весовые коэффициенты wi — параметры модели, их можно настроить с использованием градиентного спуска или других алгоритмов оптимизации, чтобы минимизировать ошибку аппроксимации функции.
Благодаря суммированию B-spline, перемноженных на весовые коэффициенты и непременно приближаемся к основной функции — закономерности, которую мы и пытаемся выбить на выходе из нейронки.
Одна из наиболее распространенных форм сплайнов — кубические сплайны, полиномы третьей степени (три слагаемых) на каждом сегменте между узловыми точками.
Эти полиномы выбираются таким образом, чтобы они были гладкими на границах сегментов, то есть чтобы первая и вторая производные были непрерывны.
Для самых непосвящённых.
Производная отражает скорость изменения, как на картинке ниже. Если мы посчитаем производные на всем участке графика — мы получим новую функцию, которую также можно отразить визуально.
Если мы не видим в графике производной скачков, разрывов — она гладка.
Выбирают полиномы, в которых непрерывно растет или уменьшается скорость изменения их графика.
Это обеспечивает не только гладкость кривой, но и ее изменение, когда мы переходим с одного сегмента на другой.
Сплайны часто используются в восстановлении данных, когда нам нужно аппроксимировать или восстановить функцию по заданным точкам.
Мы можем настраивать их форму и поведение, изменяя положение узловых точек или условия на границах сегментов. Это позволяет нам создавать кривые, которые наилучшим образом соответствуют нашим потребностям и требованиям при анализе данных или визуализации.
Чтобы создать сплайн, обычно начинают с набора управляющих точек, определяющих траекторию кривой. Затем кривая конструируется путем интерполяции или аппроксимации пути между этими управляющими точками с использованием базисных функций: B-сплайны или кривые Безье.
Основное преимущество B-сплайнов — свойство локальности.
То есть изменения в одном сегменте сплайна оказывают непосредственное влияние только на этот сегмент, без существенного влияния на остальные части сплайна.
Короче говоря, сплайны можно изменять локально.
B-сплайны имеют структуру, основанную на узловых точках, которые определяются пользователем и влияют на форму сплайна.
Сразу хочется привести пример из программ Adobe, где мы можем поменять в настройках цвета кривыми интенсивность цвета на разных участках изображения или скорость движущегося объекта.
Каждый сегмент B-сплайна обычно определяется набором узловых точек и набором базисных функций, которые управляют формой сегмента. Их можно увидеть на гифке.
Эти базисные функции обладают свойством локальности и могут быть скомбинированы таким образом, чтобы обеспечить непрерывность и гладкость сплайна на всем интервале.
По итогу мы получаем контролируемый график, который можно локально изменять, двигая «ползунки» параметров. Если упрощенно. И соответственно мы можем управлять параметрами и функциями активации, но…
KAN отличается от традиционных MLP тем, что заменяет фиксированные функции активации на обучаемые функции (B-сплайны) вдоль ребер сети.
Эта адаптивная архитектура позволяет KAN эффективно моделировать сложные функции, сохраняя интерпретируемость и сокращая количество параметров, необходимых для работы. Используя локальную гибкость B-сплайнов, эти функции придают KAN адаптивность.
Хорошо видно, как в зависимости от получаемых данных нейрон регулируют фактически B-spline на участках. Получается, что нейроны «динамически» отвечают на получаемые данные, а не пассивно передают активации.
KAN демонстрирует неплохую масштабируемость по сравнению с MLP, особенно в сценариях с высокомерными данными. Ее способность декомпозировать сложные функции на более простые дает нам возможность работать с большими объемами данных.
Несмотря на использование меньшего количества параметров, KAN достигает более высокой точности и меньшего потери, чем традиционные MLP, на различных задачах.
Просто новая архитектура может гибко моделировать отношения в данных — у нас развязаны (частично руки).
Структура KAN облегчает интерпретируемость, позволяя исследователям выводить символьные формулы, эффективно представляющие выученные закономерности.
В отличие от черных ящиков и системы «скрытых слоев» без контролируемого полноценно результата — KAN предлагают понимание того, как входные признаки трансформируются во всей сети, делая нейронки «прозрачнее».
а) В классическом MLP у нас обучаемые веса, фиксированный функции активации на нейронах/нодах. Но вот на картинке b) в модели KAN — мы получаем обучаемые функции активации и суммирование операций над нейронами.
Проблемы архитектуры KAN.
Фрактальные функции. В череде слагаемых «полинома Колмогорова — Арнольда» можно отыскать фрактальные функции, которые создают… фракталы.
Фракталы — это сложные геометрические фигуры, которые выглядят одинаково на любом уровне увеличения. Это значит, что если вы возьмете часть фрактала и увеличите её, она будет выглядеть так же, как и вся фигура. Но вот только… такие функции достаточно сложные, чтобы работать с ними на вычислительных уровнях.
Не зря в машинном обучении функции активации линейно-кусочные. Короче говоря, простые по своей природе.
Формула геометрического фрактала с комплексными числами. Вполне возможно, что предлагаемые функции слагаемых в полиноме могут попросту не выполняться без выхода за пределы действительных чисел.
Эта проблема долго заставляла исследователей отвернуться от этой теоремы, как предвестника новой архитектуры нейронных сетей взамен полносвязных MLP.
Долгая обучаемость. Да, мы получаем 200 параметров в нейросети KAN вместо 3000 на архитектуре MLP. Мы получаем эффективность в 10 раз больше нежели у полносвязной модели. Но. Скорость обучения у такой нейронки — в те же 10 раз меньше…
Потенциальная неэффективность. Оба варианта архитектуры — оба аппроксиматора функции, поэтому эффективность KAN нужно будет еще показать на примере.
Простейший пример архитектуры KAN на PyTorch
import torch
import torch.nn as nn
import numpy as np
# Определяем инвариантные функции
class UniVariateFunction(nn.Module):
def __init__(self, output_size):
super(UniVariateFunction, self).__init__()
self.linear = nn.Linear(1, output_size)
def forward(self, x):
x = self.linear(x)
return torch.sin(x) # Используем синусоиду как функцию активации
# Определяем модель KAN
class KAN(nn.Module):
def __init__(self):
super(KAN, self).__init__()
self.phi = nn.ModuleList([UniVariateFunction(1) for _ in range(2)]) #Phi функции для переменных x и y
self.Phi = nn.Linear(2, 1) # Phi функция для комбинации вывода
def forward(self, x):
x1, x2 = x[:, 0], x[:, 1]
x1 = self.phi[0](x1.view(-1, 1))
x2 = self.phi[1](x2.view(-1, 1))
out = torch.cat((x1, x2), dim=1)
out = self.Phi(out)
return out
# Генерируем простой набор данных
x = torch.linspace(-np.pi, np.pi, 200)
y = torch.linspace(-np.pi, np.pi, 200)
X, Y = torch.meshgrid(x, y)
Z = torch.sin(X) + torch.cos(Y)
# Достаем "вход" модели
inputs = torch.stack([X.flatten(), Y.flatten()], dim=1)
model = KAN()
criterion = nn.MSELoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.01)
# Тренируем
for epoch in range(1000):
optimizer.zero_grad()
outputs = model(inputs)
loss = criterion(outputs, Z.flatten())
loss.backward()
optimizer.step()
if epoch % 100 == 0:
print(f'Epoch {epoch}, Loss: {loss.item()}')
Многим бы хотелось попробовать написать пример нейронки на TensorFlow и Keras, но пока что энтузиастов не нашлось. Базовая реализация KAN — смоделировать сеть на основе структуры теоремы, поэтому мы просто приложим сюда пример сложения косинуса-синуса: и напишем простую реализацию на PyTorch.
Сначала импортируются необходимые модули torch, torch.nn и numpy. Определяется класс UniVariateFunction, содержащий линейный слой и синусоидальную активационную функцию. Затем создаётся класс KAN, который использует два объекта UniVariateFunction для обработки входных переменных и линейный слой для их объединения.
Сначала создаются сетки данных с помощью torch.linspace и torch.meshgrid, а также вычисляется функция как сумма синуса и косинуса входных значений. Затем данные объединяются в тензор inputs. Создаётся модель KAN, задаётся критерий ошибки (nn.MSELoss) и оптимизатор Adam.
В цикле на 1000 эпох модель обучается: обнуляются градиенты, вычисляются выходные значения, рассчитывается ошибка, выполняется обратное распространение ошибки и обновление параметров модели. Каждые 100 эпох выводится текущая ошибка.
И так мы постепенно приближаемся к нужной нам функции.
Как поиграться с KAN в PyKAN?
На Гитхабе уже можно найти библиотеку PyKAN, на которой можно оформить свою нейронку. Основные требования такие:
python==3.9.7
matplotlib==3.6.2
numpy==1.24.4
scikit_learn==1.1.3
setuptools==65.5.0
sympy==1.11.1
torch==2.2.2
tqdm==4.66.2
Для наибольшей точности результатов нужно установить
torch.set_default_dtype(torch.float64)
И как минимум составить свой набор данных.
Вот пример создания синтетического набора данных с помощью make_classification:
from sklearn.datasets import make_classification
import matplotlib.pyplot as plt
X_test, y_test = make_classification(n_samples=100, n_features=2, n_informative=2,
n_redundant=0, n_clusters_per_class=1, random_state=84)
dataset['test_input'] = torch.tensor(X_test, dtype=torch.float32)
dataset['test_label'] = torch.tensor(y_test, dtype=torch.long)
plt.figure(figsize=(10, 6))
plt.scatter(X[:, 0], X[:, 1], c=y, cmap='viridis', edgecolor='k')
plt.title('Визуализация синтетического набора данных')
plt.xlabel('Признак 1')
plt.ylabel('Признак 2')
plt.show()
Обучается модель и выводится конечная формула.
from kan import KAN
model = KAN(width=[2,2], grid=3, k=3)
def train_acc():
return torch.mean((torch.argmax(model(dataset['train_input']),
dim=1) == dataset['train_label']).float())
def test_acc():
return torch.mean((torch.argmax(model(dataset['test_input']),
dim=1) == dataset['test_label']).float())
results = model.train(dataset, opt="LBFGS", steps=20,
metrics=(train_acc, test_acc),
loss_fn=torch.nn.CrossEntropyLoss())
Точность можно получить из полученной формулы и получить output.
formula1, formula2 = model.symbolic_formula()[0]
def acc(formula1, formula2, X, y):
batch = X.shape[0]
correct = 0
for i in range(batch):
logit1 = np.array(formula1.subs('x_1',
X[i,0]).subs('x_2', X[i,1])).astype(np.float64)
logit2 = np.array(formula2.subs('x_1', X[i,0]).subs('x_2',
X[i,1])).astype(np.float64)
correct += (logit2 > logit1) == y[i]
return correct/batch
# Print Accuracy
print('train acc of the formula:', acc(formula1,
formula2,
dataset['train_input'],
dataset['train_label']))
print('test acc of the formula:', acc(formula1,
formula2,
dataset['test_input'],
dataset['test_label']))
Если вам хочется сразу перейти к делу на основе примеров — заходите в репозиторий KAN на Гитхабе.
И сразу переходите к блокноту с кодом и объяснениями или читайте документацию.
Для математиков полный вариант статьи можно почитать на английском — тут.