图神经网络版本的Kolmogorov Arnold(KAN)代码实现和效果对比
MLP是多层感知器(Multilayer Perceptron)的缩写,它是一种前馈人工神经网络。MLP由至少三层节点组成:一个输入层、一个或多个隐藏层以及一个输出层。每一层的节点都与下一层的每个节点相连,并且每个连接都有一个权重。MLP通过这些权重和节点的激活函数来学习输入数据的模式。
Kolmogorov-Arnold Networks(KAN) 是一种新型的神经网络架构,它受到Kolmogorov-Arnold表示定理的启发。与传统的多层感知器(MLP)不同,MLP在节点上使用固定的激活函数,而KAN在网络的边(即权重)上使用可学习的激活函数。KAN没有使用线性权重,而是将每个权重参数替换为一个参数化的单变量函数,通常是样条函数。
这种设计让KAN在准确性和可解释性方面相比于MLP有显著提升。理论上和实证上,KAN比MLP拥有更快的神经扩展规律。在可解释性方面,KAN可以直观地被可视化,并且能够容易地与人类用户交互。通过数学和物理中的两个例子,KAN被展示为有用的合作伙伴,帮助科学家(重新)发现数学和物理定律。
此外,KAN还提出了一些网络简化的方法,如符号化和修剪,以提供可解释的数学公式和优化网络架构。KAN的创新性在于它调整了Kolmogorov-Arnold表示定理的理念,并将其应用于当前的机器学习环境中,使用任意的网络架构并采用反向传播和修剪等技术,使得KAN比以往的研究更接近实际应用。
KAN的提出为深度学习模型提供了一种有前景的替代方案,这些模型目前严重依赖于MLP。
原文地址:图神经网络版本的Kolmogorov Arnold(KAN)代码实现和效果对比 (qq.com)
Kolmogorov Arnold Networks (KAN)最近作为MLP的替代而流行起来,KANs使用Kolmogorov-Arnold表示定理的属性,该定理允许神经网络的激活函数在边缘上执行,这使得激活函数“可学习”并改进它们。
目前我们看到有很多使用KAN替代MLP的实验,但是目前来说对于图神经网络来说还没有类似的实验,今天我们就来使用KAN创建一个图神经网络Graph Kolmogorov Arnold(GKAN),来测试下KAN是否可以在图神经网络方面有所作为。
首先声明GKAN类,它是一个图神经网络,用于捕获图数据集中的复杂模式。模型将计算Cora图数据集之间的关系,并训练节点分类模型。由于Cora数据集中的节点代表学术论文,边缘代表引用,因此该模型将根据论文引用检测到的模式对学术论文进行分组。
代码中最主要的是NaiveFourierKANLayer层。每个NaiveFourierKANLayer对特征进行傅里叶变换,捕获数据中的复杂模式,同时改进NaiveFourierKANLayer中的激活函数。序列中的最后一层是一个标准的线性层,它将隐藏的特征映射到由hidden_feat和out_feat定义的输出特征空间,降低特征的维数,使分类更容易。
在最后一个KAN层之后,线性层对特征进行处理以产生输出特征。结果输出使用log-softmax激活函数原始输出分数转换为用于分类的概率。
通过整合傅里叶变换,模型通过捕获数据中的高频成分和复杂模式而成为真正的KAN,同时使用基于傅里叶的转换,该转换是可学习的,并随着模型的训练而改进。
class GKAN(torch.nn.Module):
def __init__(self, in_feat, hidden_feat, out_feat, grid_feat, num_layers, use_bias=False):
super().__init__()
self.num_layers = num_layers
self.lin_in = nn.Linear(in_feat, hidden_feat, bias=use_bias)
self.lins = torch.nn.ModuleList()
for i in range(num_layers):
self.lins.append(NaiveFourierKANLayer(hidden_feat, hidden_feat, grid_feat, addbias=use_bias))
self.lins.append(nn.Linear(hidden_feat, out_feat, bias=False))
def forward(self, x, adj):
x = self.lin_in(x)
for layer in self.lins[:self.num_layers - 1]:
x = layer(spmm(adj, x))
x = self.lins[-1](x)
return x.log_softmax(dim=-1)
NaiveFourierKANLayer类实现了一个自定义的神经网络层,使用傅里叶特征(模型中的正弦和余弦变换是“激活函数”)来转换输入数据,增强模型捕获复杂模式的能力。
在init方法初始化关键参数,包括输入和输出尺寸,网格大小和可选的偏差项。gridsize影响输入数据转换成其傅立叶分量的精细程度,从而影响转换的细节和分辨率。
在forward方法中,输入张量x被重塑为二维张量。创建频率k的网格,重塑的输入xrshp用于计算余弦和正弦变换,以找到输入数据中的模式,从而产生两个张量c和s,表示输入的傅里叶特征。然后将这些张量连接并重塑以匹配后面计算需要的维度。
einsum函数用于在连接的傅立叶特征和傅立叶系数之间执行广义矩阵乘法,产生转换后的输出y。einsum函数中使用的字符串“dbik,djik->bj”是一个指示如何运行矩阵乘法的einsum字符串(在本例中为一般矩阵乘法)。矩阵乘法通过将变换后的输入数据投影到由傅里叶系数定义的新特征空间中,将输入数据的正弦和余弦变换组合成邻接矩阵。
fouriercoeffs参数是一个可学习的傅立叶系数张量,初始化为正态分布,并根据输入维度和网格大小进行缩放。傅里叶系数作为可调节的权重,决定了每个傅里叶分量对最终输出的影响程度,作为使该模型中的激活函数“可学习”的分量。在NaiveFourierKANLayer中,fouriercoeffs被列为参数,因此优化器将改进该变量。
最后,使用输出特征大小将输出y重塑回其原始维度并返回。
class NaiveFourierKANLayer(nn.Module):
def __init__(self, inputdim, outdim, gridsize=300, addbias=True):
super(NaiveFourierKANLayer, self).__init__()
self.gridsize = gridsize
self.addbias = addbias
self.inputdim = inputdim
self.outdim = outdim
self.fouriercoeffs = nn.Parameter(torch.randn(2, outdim, inputdim, gridsize) /
(np.sqrt(inputdim) * np.sqrt(self.gridsize)))
if self.addbias:
self.bias = nn.Parameter(torch.zeros(1, outdim))
def forward(self, x):
xshp = x.shape
outshape = xshp[0:-1] + (self.outdim,)
x = x.view(-1, self.inputdim)
k = torch.reshape(torch.arange(1, self.gridsize + 1, device=x.device), (1, 1, 1, self.gridsize))
xrshp = x.view(x.shape[0], 1, x.shape[1], 1)
c = torch.cos(k * xrshp)
s = torch.sin(k * xrshp)
c = torch.reshape(c, (1, x.shape[0], x.shape[1], self.gridsize))
s = torch.reshape(s, (1, x.shape[0], x.shape[1], self.gridsize))
y = torch.einsum("dbik,djik->bj", torch.concat([c, s], axis=0), self.fouriercoeffs)
if self.addbias:
y += self.bias
y = y.view(outshape)
return y
train函数训练神经网络模型。它基于输入特征(feat)和邻接矩阵(adj)计算预测(out),使用标记数据(label和mask)计算损失和精度,使用反向传播更新模型的参数,并返回精度和损失值。
eval函数对训练好的模型求值。它在不更新模型的情况下计算输入特征和邻接矩阵的预测(pred),并返回预测的类标签。
Args类定义了各种配置参数,如文件路径,数据集名称,日志路径,辍学率,隐藏层大小,傅立叶基函数的大小,模型中的层数,训练轮数,早期停止标准,随机种子和学习率,等等
最后还有设置函数index_to_mask和random_disassortative_splits将数据集划分为训练、验证和测试数据,以便每个阶段捕获来自Cora数据集的各种各样的类。random_disassortative_splits函数通过变换每个类中的索引并确保每个集合的指定比例来划分数据集。然后使用index_to_mask函数将这些索引转换为布尔掩码,以便对原始数据集进行索引。
def train(args, feat, adj, label, mask, model, optimizer):
model.train()
optimizer.zero_grad()
out = model(feat, adj)
pred, true = out[mask], label[mask]
loss = F.nll_loss(pred, true)
acc = int((pred.argmax(dim=-1) == true).sum()) / int(mask.sum())
loss.backward()
optimizer.step()
return acc, loss.item()
@torch.no_grad()
def eval(args, feat, adj, model):
model.eval()
with torch.no_grad():
pred = model(feat, adj)
pred = pred.argmax(dim=-1)
return pred
class Args:
path = './data/'
name = 'Cora'
logger_path = 'logger/esm'
dropout = 0.0
hidden_size = 256
grid_size = 200
n_layers = 2
epochs = 1000
early_stopping = 100
seed = 42
lr = 5e-4
def index_to_mask(index, size):
mask = torch.zeros(size, dtype=torch.bool, device=index.device)
mask[index] = 1
return mask
def random_disassortative_splits(labels, num_classes, trn_percent=0.6, val_percent=0.2):
labels, num_classes = labels.cpu(), num_classes.cpu().numpy()
indices = []
for i in range(num_classes):
index = torch.nonzero((labels == i)).view(-1)
index = index[torch.randperm(index.size(0))]
indices.append(index)
percls_trn = int(round(trn_percent * (labels.size()[0] / num_classes)))
val_lb = int(round(val_percent * labels.size()[0]))
train_index = torch.cat([i[:percls_trn] for i in indices], dim=0)
rest_index = torch.cat([i[percls_trn:] for i in indices], dim=0)
rest_index = rest_index[torch.randperm(rest_index.size(0))]
train_mask = index_to_mask(train_index, size=labels.size()[0])
val_mask = index_to_mask(rest_index[:val_lb], size=labels.size()[0])
test_mask = index_to_mask(rest_index[val_lb:], size=labels.size()[0])
return train_mask, val_mask, test_mask
对于上述代码中的random_disassortative_splits
进行分析:
这段代码是一个Python函数,用于创建随机的、非同配性(disassortative)的数据集划分,通常用于机器学习中的数据拆分。下面是对这段代码的详细语法分析:
-
函数定义:
def random_disassortative_splits(labels, num_classes, trn_percent=0.6, val_percent=0.2):
这是一个名为
random_disassortative_splits
的函数,它接受四个参数:labels
(标签),num_classes
(类别数量),trn_percent
(训练集所占百分比,默认为0.6),val_percent
(验证集所占百分比,默认为0.2)。 -
参数处理:
labels, num_classes = labels.cpu(), num_classes.cpu().numpy()
这里将
labels
和num_classes
转换为CPU张量和NumPy数组,以便进行后续操作。 -
初始化索引列表:
indices = []
创建一个空列表
indices
,用于存储每个类别的索引。 -
类别索引分配:
for i in range(num_classes): index = torch.nonzero((labels == i)).view(-1) index = index[torch.randperm(index.size(0))] indices.append(index)
对于每个类别,找到所有属于该类别的索引,然后随机打乱这些索引,并将结果添加到
indices
列表中。 -
计算训练集大小:
percls_trn = int(round(trn_percent * (labels.size()[0] / num_classes)))
计算每个类别中训练集的大小。
-
计算验证集大小:
val_lb = int(round(val_percent * labels.size()[0]))
计算整个数据集中验证集的大小。
-
创建训练集索引:
train_index = torch.cat([i[:percls_trn] for i in indices], dim=0)
从每个类别的索引列表中取出训练集部分,并使用
torch.cat
将它们合并为一个张量。 -
创建剩余索引:
rest_index = torch.cat([i[percls_trn:] for i in indices], dim=0) rest_index = rest_index[torch.randperm(rest_index.size(0))]
取出每个类别的非训练集部分,合并它们,并随机打乱。
-
创建验证集和测试集掩码:
train_mask = index_to_mask(train_index, size=labels.size()[0]) val_mask = index_to_mask(rest_index[:val_lb], size=labels.size()[0]) test_mask = index_to_mask(rest_index[val_lb:], size=labels.size()[0])
使用
index_to_mask
函数(这个函数在代码中没有给出,可能是外部定义的)将索引转换为掩码。掩码是布尔张量,用于指示哪些数据点属于训练集、验证集或测试集。 -
返回结果:
return train_mask, val_mask, test_mask
函数返回三个掩码,分别对应训练集、验证集和测试集。
这段代码的目的是为每个类别生成随机的训练集、验证集和测试集索引,以确保数据的非同配性,即不同类别的数据点在各个集合中分布均匀。