在KAN学习Day1——模型框架解析及HelloKAN中,我对KAN模型的基本原理进行了简单说明,并将作者团队给出的入门教程hellokan跑了一遍;
在KAN 学习 Day2 —— utils.py及spline.py 代码解读及测试中,我对项目的基本模块代码进行了解释,并以单元测试的形式深入理解模块功能,其中还发现了一个细小的错误。
今天直接上强度,开始对KANLayer进行剖析,与常见的深度学习模型类似,KAN也是由“层”堆叠起来的,因此KANLayer是整个项目的基础,务必理解透彻。
一、kan目录
kan目录结构如下,包括了模型源码、检查点、实验以及assets等
先了解一下这些文件/文件夹的大致信息:
- kan\__init__.py:用于初始化Python包,方便使用时导入模块
- kan\compiler.py:用于编译模型
- kan\experiment.py:实验代码
- kan\feynman.py:费曼函数,根据传入“name”的值确定函数,暂时没找到这个在哪里用到
- kan\hypothesis.py:将函数进行线性分离,还包含一些画图函数
- kan\KANLayer.py:KAN层的实现,使用B样条曲线作为激活函数
- kan\LBFGS.py:这个文件名似乎昨天见过,训练时的opt参数。L-BFGS是一种用于无约束优化问题的算法,它是一种拟牛顿方法,特别适用于大型稀疏问题。
- kan\MLP.py:作者自己实现了一个MLP,应该使来与KAN做对比的
- kan\MultKAN.py:在KANLayer的基础上实现的KAN类的定义,提供了关于构建和配置这种网络的详细信息。
- kan\spline.py:样条函数的实现
- kan\Symbolic_KANLayer.py:符号化的KAN层,使用四参线性函数作为激活函数
- kan\utils.py:通用模块
- kan\.ipynb_checkpoints:看目录名,这个文件夹下存放的应该是检查点文件,但是似乎和模型的实现代码区别不大,没遇到过,还不知道有什么用。
- kan\assets:这个目录下存放了两张图片,一张加号一张乘号,应该是对函数进行线性分离后,可视化时用的
- kan\experiments:这个目录下是experiment1.ipynb,和昨天跑的hellokan差不多,今天再跑一下
二、KANLayer.py
import torch
import torch.nn as nn
import numpy as np
from .spline import *
from .utils import sparse_mask
torch
:PyTorch的主要库,用于深度学习。torch.nn
:PyTorch神经网络模块,用于构建神经网络模型。numpy
:一个强大的Python库,用于数值计算。.spline
:自定义模块,包含与样条插值相关的函数。.utils
:自定义模块,包含一些工具函数,其中sparse_mask
是用于创建稀疏掩码矩阵的函数。
整个文件只有 class KANLayer(nn.Module) 一个类,实现了 函数的细节以及层与层之间的某些操作。
1.1 注释说明
class KANLayer(nn.Module):
"""
KANLayer class
Attributes:
-----------
in_dim: int
input dimension
out_dim: int
output dimension
num: int
the number of grid intervals
k: int
the piecewise polynomial order of splines
noise_scale: float
spline scale at initialization
coef: 2D torch.tensor
coefficients of B-spline bases
scale_base_mu: float
magnitude of the residual function b(x) is drawn from N(mu, sigma^2), mu = sigma_base_mu
scale_base_sigma: float
magnitude of the residual function b(x) is drawn from N(mu, sigma^2), mu = sigma_base_sigma
scale_sp: float
mangitude of the spline function spline(x)
base_fun: fun
residual function b(x)
mask: 1D torch.float
mask of spline functions. setting some element of the mask to zero means setting the corresponding activation to zero function.
grid_eps: float in [0,1]
a hyperparameter used in update_grid_from_samples. When grid_eps = 1, the grid is uniform; when grid_eps = 0, the grid is partitioned using percentiles of samples. 0 < grid_eps < 1 interpolates between the two extremes.
the id of activation functions that are locked
device: str
device
"""
KANLayer类的属性说明:
in_dim
:这个属性表示层的输入维度。它表明层期望接收多少个特征或输入。out_dim
:这个属性表示层的输出维度。它表明层将产生多少个特征或输出。num
:这个属性指定了网格间隔的数量。在样条函数的上下文中,这个参数定义了样条定义的网格的粒度。更多的间隔通常意味着对函数的更详细表示。k
:这个属性表示分段多项式样条的单调阶数。它表明组成样条函数的多项式片段的阶数。noise_scale
:这个属性是一个浮点数,表示样条函数的初始化值。它可以用来调整样条函数的初始幅度,可能影响结果的样条函数的复杂性或平滑性。coef
:这个属性是一个 2Dtorch.tensor
,包含 B样条基的系数。B样条是定义为控制点和基函数集合的一种样条。这里的系数用于构建样条函数。scale_base_mu
和scale_base_sigma
:这两个属性定义了剩余函数b(x)
的均值和标准差。剩余函数是从正态分布中抽取的,这些参数控制了剩余的幅度和可变性,可能影响样条函数的行为。scale_sp
:这个属性表示样条函数本身的幅度。它可以用来调整样条函数的整体尺度。base_fun
:这个属性是一个函数(fun
),表示剩余函数b(x)
。剩余函数用于样条函数的上下文中,以建模某种形式的偏差或误差项。mask
:这个属性是一个 1Dtorch.float
张量,作为样条函数的掩码。掩码中的某些元素设置为0对应于0函数,有效地关闭了某些样条组件。grid_eps
:这个属性是一个浮点数,控制网格细化策略。它影响如何对输入空间进行网格分区。当grid_eps = 1
时,网格是均匀的;当grid_eps = 0
时,它使用样本的百分位数进行分区。0 < grid_eps < 1 在这两种极端之间插值。锁定激活函数的IDdevice
:这个属性指定了设备(CPU或GPU),层的计算将在其上执行。
1.2 __init__
def __init__(self, in_dim=3, out_dim=2, num=5, k=3, noise_scale=0.5, scale_base_mu=0.0, scale_base_sigma=1.0, scale_sp=1.0, base_fun=torch.nn.SiLU(), grid_eps=0.02, grid_range=[-1, 1], sp_trainable=True, sb_trainable=True, save_plot_data = True, device='cpu', sparse_init=False):
''''
initialize a KANLayer
Args:
-----
in_dim : int
input dimension. Default: 2.
out_dim : int
output dimension. Default: 3.
num : int
the number of grid intervals = G. Default: 5.
k : int
the order of piecewise polynomial. Default: 3.
noise_scale : float
the scale of noise injected at initialization. Default: 0.1.
scale_base_mu : float
the scale of the residual function b(x) is intialized to be N(scale_base_mu, scale_base_sigma^2).
scale_base_sigma : float
the scale of the residual function b(x) is intialized to be N(scale_base_mu, scale_base_sigma^2).
scale_sp : float
the scale of the base function spline(x).
base_fun : function
residual function b(x). Default: torch.nn.SiLU()
grid_eps : float
When grid_eps = 1, the grid is uniform; when grid_eps = 0, the grid is partitioned using percentiles of samples. 0 < grid_eps < 1 interpolates between the two extremes.
grid_range : list/np.array of shape (2,)
setting the range of grids. Default: [-1,1].
sp_trainable : bool
If true, scale_sp is trainable
sb_trainable : bool
If true, scale_base is trainable
device : str
device
sparse_init : bool
if sparse_init = True, sparse initialization is applied.
Returns:
--------
self
Example
-------
>>> from kan.KANLayer import *
>>> model = KANLayer(in_dim=3, out_dim=5)
>>> (model.in_dim, model.out_dim)
'''
KANLayer
类的初始化函数接收多个参数,以下是这些参数的详细说明:
in_dim
:输入维度,默认为3。这表示层接收的输入特征数量。out_dim
:输出维度,默认为2。这表示层产生的输出特征数量。num
:网格间隔的数量,默认为5。这定义了样条函数定义的网格的粒度。k
:分段多项式样条的阶数,默认为3。这表示组成样条函数的多项式片段的阶数。noise_scale
:初始化时注入的噪声的尺度,默认为0.5。这可以调整样条函数的初始幅度。scale_base_mu
和scale_base_sigma
:剩余函数b(x)
的均值和标准差,默认分别为0.0和1.0。剩余函数是从正态分布中抽取的,这些参数控制了剩余的幅度和可变性。scale_sp
:样条函数spline(x)
的尺度,默认为1.0。这调整样条函数的整体幅度。base_fun
:剩余函数b(x)
的函数,默认为torch.nn.SiLU()
。这定义了样条函数的偏差或误差项。grid_eps
:控制网格细化策略的浮点数,默认为0.02。当grid_eps = 1
时,网格是均匀的;当grid_eps = 0
时,它使用样本的百分位数进行分区。0 < grid_eps < 1 插值在两种极端之间。grid_range
:网格范围的列表或数组,默认为[-1, 1]。这设置了网格的上下限。sp_trainable
和sb_trainable
:布尔值,分别表示scale_sp
和scale_base
是否可训练,默认分别为True和True。save_plot_data
:布尔值,表示是否保存绘图数据,默认为True。device
:设备,默认为'cpu'。层的计算将在其上执行。sparse_init
:布尔值,表示是否进行稀疏初始化,默认为False。
在初始化函数中,执行以下操作:
- 继承父类,将输入和输出维度、网格间隔数量、样条阶数等参数赋值给类的成员变量。
super(KANLayer, self).__init__()
# size
self.out_dim = out_dim
self.in_dim = in_dim
self.num = num
self.k = k
- 创建一个线性间隔的网格,并使用
extend_grid
函数扩展网格,以适应样条函数的需求。torch.nn.Parameter
将一个张量转换为模型的可训练参数。.requires_grad_(False)
设置了这个参数的梯度计算为False
,意味着在训练过程中,这个参数的值不会通过梯度下降来更新。
grid = torch.linspace(grid_range[0], grid_range[1], steps=num + 1)[None,:].expand(self.in_dim, num+1)
grid = extend_grid(grid, k_extend=k)
self.grid = torch.nn.Parameter(grid).requires_grad_(False)
- 计算噪声,用于初始化样条函数的系数。
noises = (torch.rand(self.num+1, self.in_dim, self.out_dim) - 1/2) * noise_scale / num
- 使用
curve2coef
函数计算样条函数的系数。- 在机器学习中,“coef” 通常是指模型的系数(coefficients)。这些系数用于线性模型(如线性回归或逻辑回归)中的特征权重。所以这个参数也就是公式中的
- 在机器学习中,“curve” 是通过绘制训练集和测试集的准确率来评估模型在不同训练样本量下的表现,它可以帮助识别过拟合或欠拟合问题。
- 本项目中使用这两个量进行 y 的计算和 grid 的更新。
- 在机器学习中,“coef” 通常是指模型的系数(coefficients)。这些系数用于线性模型(如线性回归或逻辑回归)中的特征权重。所以这个参数也就是公式中的
self.coef = torch.nn.Parameter(curve2coef(self.grid[:,k:-k].permute(1,0), noises, self.grid, k))
- 如果
sparse_init
为真,则创建一个稀疏初始化的掩码参数。
if sparse_init:
self.mask = torch.nn.Parameter(sparse_mask(in_dim, out_dim)).requires_grad_(False)
else:
self.mask = torch.nn.Parameter(torch.ones(in_dim, out_dim)).requires_grad_(False)
- 创建可训练参数
scale_base
和scale_sp
,并根据sp_trainable
和sb_trainable
的值设置它们是否可训练。- 分别是公式中的
、
- 分别是公式中的
self.scale_base = torch.nn.Parameter(scale_base_mu * 1 / np.sqrt(in_dim) + \
scale_base_sigma * (torch.rand(in_dim, out_dim)*2-1) * 1/np.sqrt(in_dim)).requires_grad_(sb_trainable)
self.scale_sp = torch.nn.Parameter(torch.ones(in_dim, out_dim) * scale_sp * self.mask).requires_grad_(sp_trainable) # make scale trainable
- 将
base_fun
设置为剩余函数,就是公式中的
self.base_fun = base_fun
- 设置网格细化策略的参数
grid_eps
。
self.grid_eps = grid_eps
- 将模型移动到指定的设备上。
self.to(device)
def to(self, device):
super(KANLayer, self).to(device)
self.device = device
return self
to
方法接受一个参数device
,它的目的是将类的实例(以及可能的所有子模块和参数)移动到指定的设备上,比如CPU或GPU。
测试:
from kan.KANLayer import *
# 创建 KANLayer 实例
model = KANLayer()
# 打印属性值
print(f"输入维度 (in_dim): {model.in_dim}")
print(f"输出维度 (out_dim): {model.out_dim}")
print(f"网格间隔数量 (num): {model.num}")
print(f"样条函数阶数 (k): {model.k}")
print(f"网格 (grid): {model.grid}")
print(f"样条系数 (coef): {model.coef}")
print(f"尺度基函数 (scale_base): {model.scale_base}")
print(f"尺度样条函数 (scale_sp): {model.scale_sp}")
print(f"剩余函数 (base_fun): {model.base_fun}")
print(f"网格细化策略参数 (grid_eps): {model.grid_eps}")
#print(f"网格范围 (grid_range): {model.grid_range}")
#print(f"尺度样条函数是否可训练 (sp_trainable): {model.sp_trainable}")
#print(f"尺度基函数是否可训练 (sb_trainable): {model.sb_trainable}")
#print(f"是否保存绘图数据 (save_plot_data): {model.save_plot_data}")
print(f"设备 (device): {model.device}")
print(f"稀疏初始化掩码 (mask): {model.mask}")
输入维度 (in_dim): 3
输出维度 (out_dim): 2
网格间隔数量 (num): 5
样条函数阶数 (k): 3
网格 (grid): Parameter containing:
tensor([[-2.2000, -1.8000, -1.4000, -1.0000, -0.6000, -0.2000, 0.2000, 0.6000,
1.0000, 1.4000, 1.8000, 2.2000],
[-2.2000, -1.8000, -1.4000, -1.0000, -0.6000, -0.2000, 0.2000, 0.6000,
1.0000, 1.4000, 1.8000, 2.2000],
[-2.2000, -1.8000, -1.4000, -1.0000, -0.6000, -0.2000, 0.2000, 0.6000,
1.0000, 1.4000, 1.8000, 2.2000]])
样条系数 (coef): Parameter containing:
tensor([[[-0.1270, -0.0024, -0.0054, 0.0247, -0.0781, 0.0293, -0.0078,
-0.1250],
[-0.1152, -0.0171, 0.0319, -0.0687, 0.0366, -0.0332, -0.0352,
0.0625]],[[-0.1624, -0.0123, -0.0430, 0.0217, -0.0357, -0.0408, 0.0068,
-0.0234],
[-0.2317, -0.0157, 0.0621, 0.0537, -0.0087, 0.0471, 0.0034,
-0.0625]],[[-0.1143, 0.0051, -0.0782, 0.0807, -0.0725, 0.0010, 0.0098,
-0.0859],
[ 0.1318, -0.0132, 0.0538, -0.0962, 0.0729, -0.0930, 0.0000,
0.1211]]], requires_grad=True)
尺度基函数 (scale_base): Parameter containing:
tensor([[-0.4503, -0.2924],
[ 0.0544, -0.1100],
[-0.3299, -0.0852]], requires_grad=True)
尺度样条函数 (scale_sp): Parameter containing:
tensor([[1., 1.],
[1., 1.],
[1., 1.]], requires_grad=True)
剩余函数 (base_fun): SiLU()
网格细化策略参数 (grid_eps): 0.02
设备 (device): cpu
稀疏初始化掩码 (mask): Parameter containing:
tensor([[1., 1.],
[1., 1.],
[1., 1.]])
其中 grid_range=[-1, 1], sp_trainable=True, sb_trainable=True, save_plot_data = True这几个参数虽然有默认值,但是并不属于self的属性,无法直接打印出值,他们的作用是控制可训练参数是否进行更新。
在这个初始化函数中,使用了extend_grid、curve2coef、sparse_mask等模块,如果已经忘记请回看Day2讲解。
看到这那就要恭喜你了,你已经知晓了核心公式的全部组成部分,在__init__中,分别实现了、
、
的初始化,以及
的选择。而在spline.py中的B_batch函数即为公式中的
,其他函数则为参数更新的方法。至于如何更新,请接着往后看。
1.3 forward
def forward(self, x):
'''
KANLayer forward given input x
Args:
-----
x : 2D torch.float
inputs, shape (number of samples, input dimension)
Returns:
--------
y : 2D torch.float
outputs, shape (number of samples, output dimension)
preacts : 3D torch.float
fan out x into activations, shape (number of sampels, output dimension, input dimension)
postacts : 3D torch.float
the outputs of activation functions with preacts as inputs
postspline : 3D torch.float
the outputs of spline functions with preacts as inputs
输入和输出:
x
:输入数据,形状为(batch_size, input_dim)
,其中batch_size
是样本数量,input_dim
是输入维度。y
:输出数据,形状为(batch_size, output_dim)
,其中output_dim
是输出维度。preacts
:激活前的数据,形状为(batch_size, output_dim, input_dim)
。postacts
:激活后的数据,形状为(batch_size, output_dim, input_dim)
。postspline
:样条函数后的数据,形状为(batch_size, output_dim, input_dim)
。
计算过程:
base = self.base_fun(x) # (batch, in_dim)
preacts
:通过将输入x
扩展为三维张量,形状为(batch_size, output_dim, input_dim)
。
batch = x.shape[0]
preacts = x[:,None,:].clone().expand(batch, self.out_dim, self.in_dim)
base
:调用self.base_fun
函数,计算的输出,形状为
(batch_size, input_dim)。
公式2.11
base = self.base_fun(x) # (batch, in_dim)
y
:调用coef2curve
函数,计算样条曲线,形状为(batch_size, input_dim, output_dim).
公式2.12
y = coef2curve(x_eval=x, grid=self.grid, coef=self.coef, k=self.k)
postspline
:复制y
并进行维度置换,形状变为(batch_size, output_dim, input_dim)
。
postspline = y.clone().permute(0,2,1)
y
:对y
进行缩放和加法操作,然后进行掩码操作,形状保持为(batch_size, input_dim, output_dim)
。- 公式2.10
y = self.scale_base[None,:,:] * base[:,:,None] + self.scale_sp[None,:,:] * y
y = self.mask[None,:,:] * y
postacts
:复制y
并进行维度置换,形状变为(batch_size, output_dim, input_dim)
。y
:对y
在维度1上求和,形状变为(batch_size, output_dim)
。- 并返回计算结果
postacts = y.clone().permute(0,2,1)
y = torch.sum(y, dim=1)
return y, preacts, postacts, postspline
测试:
model = KANLayer(in_dim=3, out_dim=5)
x = torch.normal(0,1,size=(100,3))
y, preacts, postacts, postspline = model(x)
y.shape, preacts.shape, postacts.shape, postspline.shape
(torch.Size([100, 5]),
torch.Size([100, 5, 3]),
torch.Size([100, 5, 3]),
torch.Size([100, 5, 3]))
好了,你已经掌握了前向传播的计算过程,接下来进行一些参数更新的内容,对象是grid和。而参数
、
作为权重,更新过程应该在 MultKAN.py的 fit 方法中,届时根据loss更新所有KANLayer的参数。
1.4 update_grid_from_samples
def update_grid_from_samples(self, x, mode='sample'):
'''
update grid from samples
Args:
-----
x : 2D torch.float
inputs, shape (number of samples, input dimension)
Returns:
--------
None
Example
-------
>>> model = KANLayer(in_dim=1, out_dim=1, num=5, k=3)
>>> print(model.grid.data)
>>> x = torch.linspace(-3,3,steps=100)[:,None]
>>> model.update_grid_from_samples(x)
>>> print(model.grid.data)
'''
batch = x.shape[0]
#x = torch.einsum('ij,k->ikj', x, torch.ones(self.out_dim, ).to(self.device)).reshape(batch, self.size).permute(1, 0)
x_pos = torch.sort(x, dim=0)[0]
y_eval = coef2curve(x_pos, self.grid, self.coef, self.k)
num_interval = self.grid.shape[1] - 1 - 2*self.k
def get_grid(num_interval):
ids = [int(batch / num_interval * i) for i in range(num_interval)] + [-1]
grid_adaptive = x_pos[ids, :].permute(1,0)
h = (grid_adaptive[:,[-1]] - grid_adaptive[:,[0]])/num_interval
grid_uniform = grid_adaptive[:,[0]] + h * torch.arange(num_interval+1,)[None, :].to(x.device)
grid = self.grid_eps * grid_uniform + (1 - self.grid_eps) * grid_adaptive
return grid
grid = get_grid(num_interval)
if mode == 'grid':
sample_grid = get_grid(2*num_interval)
x_pos = sample_grid.permute(1,0)
y_eval = coef2curve(x_pos, self.grid, self.coef, self.k)
self.grid.data = extend_grid(grid, k_extend=self.k)
self.coef.data = curve2coef(x_pos, y_eval, self.grid, self.k)
这段代码是一个用于更新网格(grid)的函数,它基于输入的样本数据(x
)进行操作。函数接受两个参数:
x
:一个形状为(number of samples, input dimension)
的2Dtorch.float
张量,表示输入样本。mode
:一个字符串参数,表示更新模式,默认为'sample'
。
函数的主要目标是更新网格和系数(coef
),使得网格能够更好地适应输入样本的分布。以下是函数的主要步骤:
- 计算批次大小:
batch = x.shape[0]
,获取样本的数量。 - 对输入样本进行排序:
x_pos = torch.sort(x, dim=0)[0]
,将输入样本按照升序排列。 - 计算评估值:通过
coef2curve
函数,将排序后的样本值映射到当前网格(grid
)上,得到评估值(y_eval
)。 - 计算网格间隔数:
num_interval = self.grid.shape[1] - 1 - 2*self.k
,计算当前网格的间隔数。 - 生成网格:
- 使用
get_grid
函数生成适应性网格(grid_adaptive
)和均匀网格(grid_uniform
)。 grid = get_grid(num_interval)
,生成最终的网格,结合适应性和均匀性。
- 使用
- 根据模式更新网格:
- 如果
mode
为'grid'
,则从适应性网格中生成更多的样本网格(sample_grid
),并更新网格和系数。
- 如果
- 扩展网格:使用
extend_grid
函数扩展网格,以适应输入样本的分布。 - 更新系数:使用
curve2coef
函数,根据更新后的网格和评估值,更新系数。
测试:
model = KANLayer(in_dim=1, out_dim=1, num=5, k=3)
print("更新前:")
print(model.coef)
print(model.grid.data)
x = torch.linspace(-3,3,steps=100)[:,None]
model.update_grid_from_samples(x)
print("更新后:")
print(model.coef)
print(model.grid.data)
更新前:
Parameter containing:
tensor([[[-0.2227, 0.0020, 0.0291, 0.0040, -0.0469, 0.0273, 0.1094,
-0.1562]]], requires_grad=True)
tensor([[-2.2000, -1.8000, -1.4000, -1.0000, -0.6000, -0.2000, 0.2000, 0.6000,
1.0000, 1.4000, 1.8000, 2.2000]])
更新后:
Parameter containing:
tensor([[[-0.9262, 0.2282, -0.1677, 0.0103, 0.0412, -0.0915, 0.1161,
-0.4794]]], requires_grad=True)
tensor([[-6.6000, -5.4000, -4.2000, -3.0000, -1.7881, -0.5762, 0.6356, 1.8475,
3.0000, 4.2000, 5.4000, 6.6000]])
在KANLayer的初始化函数中,grid属性初始化是由参数传入的,但是既然KAN是一个“神经网络”,前后层之间必然存在联系,于是乎作者根据父层的grid初始化子层的grid,我感觉在构建网络时多少有点作用,比如减少迭代次数之类的。
1.5 initialize_grid_from_parent
def initialize_grid_from_parent(self, parent, x, mode='sample'):
'''
update grid from a parent KANLayer & samples
Args:
-----
parent : KANLayer
a parent KANLayer (whose grid is usually coarser than the current model)
x : 2D torch.float
inputs, shape (number of samples, input dimension)
Returns:
--------
None
Example
-------
>>> batch = 100
>>> parent_model = KANLayer(in_dim=1, out_dim=1, num=5, k=3)
>>> print(parent_model.grid.data)
>>> model = KANLayer(in_dim=1, out_dim=1, num=10, k=3)
>>> x = torch.normal(0,1,size=(batch, 1))
>>> model.initialize_grid_from_parent(parent_model, x)
>>> print(model.grid.data)
'''
batch = x.shape[0]
x_pos = torch.sort(x, dim=0)[0]
y_eval = coef2curve(x_pos, parent.grid, parent.coef, parent.k)
num_interval = self.grid.shape[1] - 1 - 2*self.k
def get_grid(num_interval):
ids = [int(batch / num_interval * i) for i in range(num_interval)] + [-1]
grid_adaptive = x_pos[ids, :].permute(1,0)
h = (grid_adaptive[:,[-1]] - grid_adaptive[:,[0]])/num_interval
grid_uniform = grid_adaptive[:,[0]] + h * torch.arange(num_interval+1,)[None, :].to(x.device)
grid = self.grid_eps * grid_uniform + (1 - self.grid_eps) * grid_adaptive
return grid
grid = get_grid(num_interval)
if mode == 'grid':
sample_grid = get_grid(2*num_interval)
x_pos = sample_grid.permute(1,0)
y_eval = coef2curve(x_pos, parent.grid, parent.coef, parent.k)
grid = extend_grid(grid, k_extend=self.k)
self.grid.data = grid
self.coef.data = curve2coef(x_pos, y_eval, self.grid, self.k)
这个方法initialize_grid_from_parent
用于从父KANLayer
实例中更新当前KANLayer
实例的网格(grid
)和系数(coef
)。它基于输入样本x
和一个已存在的parent``KANLayer
实例。
参数说明:
parent
:KANLayer
实例,通常网格比当前模型的网格更粗。x
: 2Dtorch.float
张量,形状为(number of samples, input dimension)
,表示输入样本。mode
: 字符串,可选参数,用于指定更新方式。默认为'sample'
。
返回值
None
:方法不返回任何值,而是直接更新KANLayer
实例中的grid
和coef
。
方法步骤
- 初始化参数:
- 获取输入样本
x
的批大小batch
。 - 对于每个输入样本,执行以下操作。
- 获取输入样本
- 排序输入样本:
- 对输入样本
x
进行排序,得到排序后的x_pos
。
- 对输入样本
- 评估曲线:
- 使用
coef2curve
函数评估排序后的x_pos
,得到y_eval
。
- 使用
- 计算间隔数:
- 计算
grid
的间隔数num_interval
,这是grid
的宽度减去两端的额外间隔数。
- 计算
- 生成网格:
- 使用
get_grid
函数生成适应性网格grid_adaptive
和均匀网格grid_uniform
。 - 根据
num_interval
计算网格点,然后结合grid_adaptive
和grid_uniform
生成最终的网格grid
。
- 使用
- 更新网格和系数:
- 根据
mode
参数决定是否使用额外的样本网格进行更新。 - 使用
extend_grid
和curve2coef
函数更新grid
和coef
。
- 根据
只能说和更新层自身的参数一模一样!
测试:
batch = 100
parent_model = KANLayer(in_dim=1, out_dim=1, num=5, k=3)
print("父节点:")
print(parent_model.coef)
print(parent_model.grid.data)
model = KANLayer(in_dim=1, out_dim=1, num=10, k=3)
x = torch.normal(0,1,size=(batch, 1))
model.initialize_grid_from_parent(parent_model, x)
print("子节点:")
print(model.coef)
print(model.grid.data)
父节点:
Parameter containing:
tensor([[[-0.0117, 0.0352, -0.0894, 0.0646, -0.0684, 0.0664, 0.0625,
-0.0625]]], requires_grad=True)
tensor([[-2.2000, -1.8000, -1.4000, -1.0000, -0.6000, -0.2000, 0.2000, 0.6000,
1.0000, 1.4000, 1.8000, 2.2000]])
子节点:
Parameter containing:
tensor([[[ 0.0952, -0.0599, 0.0467, -0.0272, -0.0517, 0.0529, -0.0438,
-0.0055, 0.0788, 0.0309, -0.0930, 0.1110, -0.1484]]],
requires_grad=True)
tensor([[-3.9502, -3.4248, -2.8994, -2.3741, -1.1085, -0.7264, -0.4975, -0.1283,
0.0768, 0.4406, 0.6853, 1.0040, 1.4111, 2.8797, 3.4050, 3.9304,
4.4558]])
1.6 get_subset
def get_subset(self, in_id, out_id):
'''
get a smaller KANLayer from a larger KANLayer (used for pruning)
Args:
-----
in_id : list
id of selected input neurons
out_id : list
id of selected output neurons
Returns:
--------
spb : KANLayer
Example
-------
>>> kanlayer_large = KANLayer(in_dim=10, out_dim=10, num=5, k=3)
>>> kanlayer_small = kanlayer_large.get_subset([0,9],[1,2,3])
>>> kanlayer_small.in_dim, kanlayer_small.out_dim
(2, 3)
'''
spb = KANLayer(len(in_id), len(out_id), self.num, self.k, base_fun=self.base_fun)
spb.grid.data = self.grid[in_id]
spb.coef.data = self.coef[in_id][:,out_id]
spb.scale_base.data = self.scale_base[in_id][:,out_id]
spb.scale_sp.data = self.scale_sp[in_id][:,out_id]
spb.mask.data = self.mask[in_id][:,out_id]
spb.in_dim = len(in_id)
spb.out_dim = len(out_id)
return spb
这个方法get_subset
用于从一个较大的KANLayer
实例中获取一个较小的KANLayer
实例,通常用于剪枝操作。它根据指定的输入神经元in_id
和输出神经元out_id
的索引来选择子集。
参数说明:
in_id
:列表,包含所选输入神经元的索引。out_id
:列表,包含所选输出神经元的索引。
返回值
spb
:KANLayer
实例,是从原始KANLayer
中选择的子集。
方法步骤:
- 创建一个新的
KANLayer
实例spb
,其输入维度为len(in_id)
,输出维度为len(out_id)
,其他参数与原始KANLayer
相同。 - 将原始
KANLayer
的grid
、coef
、scale_base
、scale_sp
和mask
数据根据in_id
和out_id
进行选择,并赋值给spb
。 - 更新
spb
的in_dim
和out_dim
属性。 - 返回
spb
。
其实剪枝的操作我们在hellokan中已经见识过了,如下图:
测试:
kanlayer_large = KANLayer(in_dim=10, out_dim=10, num=5, k=3)
kanlayer_small = kanlayer_large.get_subset([0,9],[1,2,3])
kanlayer_small.in_dim, kanlayer_small.out_dim
(2, 3)
这是作者给的测试用例,意思是先创建了一个层,输入10个节点,输出9个节点,然后剪枝操作时选择了输入的0、9节点,输出的1、2、3节点,最后剩下一个输入2个节点、输出3个节点的小层。
1.7 swap
def swap(self, i1, i2, mode='in'):
'''
swap the i1 neuron with the i2 neuron in input (if mode == 'in') or output (if mode == 'out')
Args:
-----
i1 : int
i2 : int
mode : str
mode = 'in' or 'out'
Returns:
--------
None
Example
-------
>>> from kan.KANLayer import *
>>> model = KANLayer(in_dim=2, out_dim=2, num=5, k=3)
>>> print(model.coef)
>>> model.swap(0,1,mode='in')
>>> print(model.coef)
'''
with torch.no_grad():
def swap_(data, i1, i2, mode='in'):
if mode == 'in':
data[i1], data[i2] = data[i2].clone(), data[i1].clone()
elif mode == 'out':
data[:,i1], data[:,i2] = data[:,i2].clone(), data[:,i1].clone()
if mode == 'in':
swap_(self.grid.data, i1, i2, mode='in')
swap_(self.coef.data, i1, i2, mode=mode)
swap_(self.scale_base.data, i1, i2, mode=mode)
swap_(self.scale_sp.data, i1, i2, mode=mode)
swap_(self.mask.data, i1, i2, mode=mode)
这个swap
方法用于在KANLayer
实例中交换输入或输出神经元的位置。具体操作取决于mode
参数,其可以是'in'
或'out'
,分别表示在输入或输出神经元之间进行交换。以下是方法的详细解释:
参数说明:
i1
:整数,表示要交换的神经元的索引。i2
:整数,表示要交换的神经元的索引。mode
:字符串,表示交换是在输入('in'
)还是输出('out'
)神经元之间进行。
返回值:
- 该方法没有返回值,其作用是修改
KANLayer
实例的内部数据结构。
方法步骤:
- 使用
torch.no_grad()
上下文管理器来避免在执行交换操作时计算梯度,这对于不需要梯度的交换操作是必要的。 - 定义一个内部函数
swap_
,该函数接受一个数据列表(如grid.data
、coef.data
等)和两个索引i1
和i2
,以及一个模式参数mode
。 - 根据
mode
参数,调用swap_
函数来交换对应的数据。如果是输入神经元交换(mode == 'in'
),则交换grid.data
中的元素;如果是输出神经元交换,则交换coef.data
、scale_base.data
、scale_sp.data
和mask.data
中的元素。 - 最后,调用
swap_
函数来完成所有相关数据的交换。
测试:
model = KANLayer(in_dim=2, out_dim=2, num=5, k=3)
print(model.coef)
model.swap(0,1,mode='in')
print(model.coef)
Parameter containing:
tensor([[[ 0.0078, -0.0166, 0.0872, -0.1138, 0.1084, -0.0938, 0.0469,
0.1562],
[ 0.2231, 0.0074, 0.0042, -0.0034, -0.0464, -0.0615, 0.0293,
-0.0547]],[[ 0.1914, -0.0283, 0.1116, -0.1251, 0.0928, -0.0566, -0.0859,
0.1250],
[ 0.0859, 0.0068, -0.0500, -0.0283, 0.0156, 0.0430, -0.0859,
0.0625]]], requires_grad=True)
Parameter containing:
tensor([[[ 0.1914, -0.0283, 0.1116, -0.1251, 0.0928, -0.0566, -0.0859,
0.1250],
[ 0.0859, 0.0068, -0.0500, -0.0283, 0.0156, 0.0430, -0.0859,
0.0625]],[[ 0.0078, -0.0166, 0.0872, -0.1138, 0.1084, -0.0938, 0.0469,
0.1562],
[ 0.2231, 0.0074, 0.0042, -0.0034, -0.0464, -0.0615, 0.0293,
-0.0547]]], requires_grad=True)
在神经网络模型中,交换节点(或神经元)的主要好处有以下几个方面:
-
增强模型的灵活性:通过交换节点,模型可以探索不同的连接模式,从而增强其适应性和灵活性。这有助于模型在面对不同数据集或任务时,能够以更灵活的方式进行学习和调整。
-
优化性能:交换节点可能有助于优化模型的性能。通过改变节点之间的连接,模型可能能够找到更有效的路径来传递信息,从而提高计算效率和预测准确性。
-
避免过拟合:在某些情况下,通过改变节点连接,可以减少模型对训练数据的过度依赖,从而帮助减少过拟合现象。这通常与增加模型的泛化能力相关。
-
增强解释性:在某些神经网络架构中,特定节点的连接模式可能影响模型的决策过程。通过调整这些连接,可以增加模型决策的透明度和可解释性。
-
促进特征学习:节点之间的连接模式直接影响特征的提取和学习过程。通过调整这些连接,模型可能能够更有效地学习和表示输入数据的特征,从而提高模型的性能。
-
适应性增强:在处理动态或变化的输入数据时,能够快速调整节点连接的模型可能具有更好的适应性。这在处理时间序列数据、图像识别或自然语言处理等任务中尤为重要。
-
减少冗余:在某些情况下,通过交换节点,可以减少模型中的冗余连接,从而减少计算成本和内存使用,提高模型的效率。
-
探索不同架构:交换节点可以作为探索不同神经网络架构的一种手段。通过尝试不同的连接模式,研究者和开发者可以发现新的有效架构,这可能对特定任务或数据集特别有利。
-
提高模型的鲁棒性:通过改变节点的连接,模型可能能够更好地抵抗输入数据的微小变化,从而提高其在面对噪声或不完整数据时的鲁棒性。
结论来自gpt-4o mini
看完了KANLayer,我们已经知道了KAN每一层的实现方式。但是,还有Symbolic_KANLayer,让我们继续探索他们的区别所在。
三、Symbolic_KANLayer.py
导入包就不再说了
import torch
import torch.nn as nn
import numpy as np
import sympy
from .utils import *
2.1 注释说明
class Symbolic_KANLayer(nn.Module):
'''
KANLayer class
Attributes:
-----------
in_dim : int
input dimension
out_dim : int
output dimension
funs : 2D array of torch functions (or lambda functions)
symbolic functions (torch)
funs_avoid_singularity : 2D array of torch functions (or lambda functions) with singularity avoiding
funs_name : 2D arry of str
names of symbolic functions
funs_sympy : 2D array of sympy functions (or lambda functions)
symbolic functions (sympy)
affine : 3D array of floats
affine transformations of inputs and outputs
类属性
-
in_dim
: 输入特征的维度。这是一个整数,表示输入数据的特征数量。 -
out_dim
: 输出特征的维度。这也是一个整数,表示输出数据的特征数量。 -
funs
: 这是一个二维数组,其中每个子数组包含输入维度个数的函数,这些函数是 PyTorch 的函数或 lambda 函数。这些函数用于执行符号计算或激活操作。 -
funs_avoid_singularity
: 这是一个二维数组,每个子数组包含输入维度个数的函数,这些函数旨在避免奇点(singularity)。奇点是指可能导致函数不连续或未定义的点。 -
funs_name
: 这是一个二维数组,包含字符串,代表funs
中每个函数的名称。这有助于在调试或可视化时识别不同的函数。 -
funs_sympy
: 这是一个二维数组,包含 Sympy 的函数或 lambda 函数。Sympy 是一个用于符号数学的 Python 库,可以用来进行符号计算。 -
affine
: 这是一个三维数组,包含浮点数,代表输入和输出的仿射变换。仿射变换是一种几何变换,它包括线性变换和均匀缩放,但不包括旋转或反射。
2.2 __init__
def __init__(self, in_dim=3, out_dim=2, device='cpu'):
'''
initialize a Symbolic_KANLayer (activation functions are initialized to be identity functions)
Args:
-----
in_dim : int
input dimension
out_dim : int
output dimension
device : str
device
Returns:
--------
self
Example
-------
>>> sb = Symbolic_KANLayer(in_dim=3, out_dim=3)
>>> len(sb.funs), len(sb.funs[0])
'''
方法描述
__init__
是 Symbolic_KANLayer
类的构造函数,用于初始化一个 Symbolic_KANLayer
实例。这个构造函数接受以下参数:
in_dim
(默认值 3):输入特征的维度。out_dim
(默认值 2):输出特征的维度。device
(默认值 'cpu'):指定模型运行在哪个设备上,可以是'cpu'
或'cuda'
。
构造函数的目的是设置 Symbolic_KANLayer
实例的属性,并初始化其内部状态。
返回值
构造函数返回 self
,即当前类的实例。这是 Python 中构造函数的常规做法,允许在构造函数中设置实例属性。
super(Symbolic_KANLayer, self).__init__()
self.out_dim = out_dim
self.in_dim = in_dim
self.mask = torch.nn.Parameter(torch.zeros(out_dim, in_dim, device=device)).requires_grad_(False)
# torch
self.funs = [[lambda x: x*0. for i in range(self.in_dim)] for j in range(self.out_dim)]
self.funs_avoid_singularity = [[lambda x, y_th: ((), x*0.) for i in range(self.in_dim)] for j in range(self.out_dim)]
# name
self.funs_name = [['0' for i in range(self.in_dim)] for j in range(self.out_dim)]
# sympy
self.funs_sympy = [[lambda x: x*0. for i in range(self.in_dim)] for j in range(self.out_dim)]
### make funs_name the only parameter, and make others as the properties of funs_name?
self.affine = torch.nn.Parameter(torch.zeros(out_dim, in_dim, 4, device=device))
# c*f(a*x+b)+d
self.device = device
self.to(device)
构造函数内部实现
super(Symbolic_KANLayer, self).__init__()
:调用父类的构造函数。这通常用于初始化父类属性。self.out_dim = out_dim
和self.in_dim = in_dim
:设置实例属性out_dim
和in_dim
,分别表示输出和输入特征的维度。self.mask
:创建一个torch.nn.Parameter
,它是一个可训练的参数,初始化为零张量,维度为(out_dim, in_dim)
,并指定设备。requires_grad_(False)
表示这个参数不应该跟踪梯度,即它不会被优化。self.funs
:初始化一个二维列表,每个子列表包含in_dim
个 lambda 函数。每个 lambda 函数将输入乘以 0.,即恒等函数乘以一个非常小的数,这可能是为了避免数值问题。self.funs_avoid_singularity
:类似于funs
,但包含一个额外的参数y_th
,用于避免奇异情况。每个 lambda 函数返回一个元组和一个乘以 0. 的输入。self.funs_name
:初始化一个二维列表,每个子列表包含in_dim
个字符串 '0'。这可能是用于存储函数名称或其他标识。self.funs_sympy
:初始化与funs
相同的二维列表,但每个 lambda 函数乘以 0.。这可能是用于存储符号表示的函数。self.affine
:创建一个torch.nn.Parameter
,它是一个可训练的参数,初始化为零张量,维度为(out_dim, in_dim, 4)
,并指定设备。这用于表示线性变换,affine每一个分向量依次存储着
self.device
和self.to(device)
:- 设置实例属性
device
,表示模型运行在哪个设备上。 self.to(device)
方法将模型移动到指定的设备,实习如下:
- 设置实例属性
def to(self, device):
'''
move to device
'''
super(Symbolic_KANLayer, self).to(device)
self.device = device
return self
根据这个构造函数,我们大致可以推断出,我们可以通过这个类自定义函数,并且函数具有可训练参数,包括了函数名、函数符号等等。
测试:
from kan.Symbolic_KANLayer import *
sb = Symbolic_KANLayer(in_dim=3, out_dim=3)
len(sb.funs), len(sb.funs[0])
(3, 3)
这个Symbolic_KANLayer层有三个输入节点、三个输出节点,由于是全连接,所以共产生了3*3=9个函数。
2.3 forward
def forward(self, x, singularity_avoiding=False, y_th=10.):
'''
forward
Args:
-----
x : 2D array
inputs, shape (batch, input dimension)
singularity_avoiding : bool
if True, funs_avoid_singularity is used; if False, funs is used.
y_th : float
the singularity threshold
Returns:
--------
y : 2D array
outputs, shape (batch, output dimension)
postacts : 3D array
activations after activation functions but before being summed on nodes
Example
-------
>>> sb = Symbolic_KANLayer(in_dim=3, out_dim=5)
>>> x = torch.normal(0,1,size=(100,3))
>>> y, postacts = sb(x)
>>> y.shape, postacts.shape
(torch.Size([100, 5]), torch.Size([100, 5, 3]))
'''
batch = x.shape[0]
postacts = []
for i in range(self.in_dim):
postacts_ = []
for j in range(self.out_dim):
if singularity_avoiding:
xij = self.affine[j,i,2]*self.funs_avoid_singularity[j][i](self.affine[j,i,0]*x[:,[i]]+self.affine[j,i,1], torch.tensor(y_th))[1]+self.affine[j,i,3]
else:
xij = self.affine[j,i,2]*self.funs[j][i](self.affine[j,i,0]*x[:,[i]]+self.affine[j,i,1])+self.affine[j,i,3]
postacts_.append(self.mask[j][i]*xij)
postacts.append(torch.stack(postacts_))
postacts = torch.stack(postacts)
postacts = postacts.permute(2,1,0,3)[:,:,:,0]
y = torch.sum(postacts, dim=2)
return y, postacts
这段代码定义了 Symbolic_KANLayer
类的 forward
方法,该方法用于前向传播输入数据并通过层生成输出。
参数说明:
x
:输入数据,形状为(batch, input dimension)
,即批量和输入特征维度。singularity_avoiding
:布尔值,如果为True
,则使用funs_avoid_singularity
;如果为False
,则使用funs
。y_th
:奇异性的阈值。
返回值:
y
:输出数据,形状为(batch, output dimension)
,即批量和输出特征维度。postacts
:激活函数之后的激活,形状为(batch, output dimension, input dimension)
。
方法实现:
- 获取输入数据的批次大小
batch
和输入特征维度。 - 初始化一个空列表
postacts
,用于存储每个节点的激活。 - 对每个输入特征
i
和输出特征j
进行迭代:- 如果
singularity_avoiding
为True
,则使用funs_avoid_singularity
,否则使用funs
。 - 根据选择的函数,计算
xij
,这是通过线性变换和可能的奇异值避免操作得到的。
- 将
xij
与mask
相乘,然后添加到postacts_
列表中。
- 如果
- 将
postacts_
列表中的所有元素堆叠成一个张量,并将其添加到postacts
列表中。 - 将
postacts
列表中的所有张量堆叠成一个三维张量,然后将其转置,以便输出特征的维度在第二个位置。 - 对
postacts
的最后一个维度进行求和,得到最终的输出y
。
所以看到这里你就懂了吧,KANLayer使用的是B样条曲线激活,而Symbolic_KANLayer使用的是线性变换进行激活。所以KANLayer进行的是矩阵运算,而Symbolic_KANLayer对节点进行遍历,依次运算。
2.4 get_subset
def get_subset(self, in_id, out_id):
'''
get a smaller Symbolic_KANLayer from a larger Symbolic_KANLayer (used for pruning)
Args:
-----
in_id : list
id of selected input neurons
out_id : list
id of selected output neurons
Returns:
--------
spb : Symbolic_KANLayer
Example
-------
>>> sb_large = Symbolic_KANLayer(in_dim=10, out_dim=10)
>>> sb_small = sb_large.get_subset([0,9],[1,2,3])
>>> sb_small.in_dim, sb_small.out_dim
'''
sbb = Symbolic_KANLayer(self.in_dim, self.out_dim, device=self.device)
sbb.in_dim = len(in_id)
sbb.out_dim = len(out_id)
sbb.mask.data = self.mask.data[out_id][:,in_id]
sbb.funs = [[self.funs[j][i] for i in in_id] for j in out_id]
sbb.funs_avoid_singularity = [[self.funs_avoid_singularity[j][i] for i in in_id] for j in out_id]
sbb.funs_sympy = [[self.funs_sympy[j][i] for i in in_id] for j in out_id]
sbb.funs_name = [[self.funs_name[j][i] for i in in_id] for j in out_id]
sbb.affine.data = self.affine.data[out_id][:,in_id]
return sbb
这段代码定义了 Symbolic_KANLayer
类的 get_subset
方法,用于从较大的 Symbolic_KANLayer
实例中创建一个较小的实例。这个方法通常用于神经网络的剪枝(pruning),即删除不需要的神经元以减少模型的复杂性和计算成本。与KANLayer做法相同。
参数说明:
in_id
:一个列表,包含选择的输入神经元的索引。out_id
:一个列表,包含选择的输出神经元的索引。
返回值:
spb
:一个Symbolic_KANLayer
实例,表示从较大实例中提取的子集。
方法实现:
- 创建一个新的
Symbolic_KANLayer
实例sbb
,并初始化其参数以匹配in_id
和out_id
的大小。 - 将
sbb
的in_dim
设置为in_id
的长度(即选择的输入神经元的数量)。 - 将
sbb
的out_dim
设置为out_id
的长度(即选择的输出神经元的数量)。 - 将
sbb
的mask
属性设置为self.mask
属性的子集,该子集由out_id
和in_id
确定。这表示在新层中,只有在out_id
和in_id
中的对应位置为True
的连接是有效的。 - 将
sbb
的funs
、funs_avoid_singularity
、funs_sympy
和funs_name
属性分别设置为self
属性的子集,这些子集同样由out_id
和in_id
确定。这些属性通常包含激活函数、奇异值避免函数、符号表示和函数名称等信息。 - 将
sbb
的affine
属性设置为self.affine
属性的子集,该子集由out_id
和in_id
确定。affine
属性通常包含线性变换的权重。 - 返回
sbb
,即从较大实例中提取的子集。
2.5 fix_symbolic
def fix_symbolic(self, i, j, fun_name, x=None, y=None, random=False, a_range=(-10,10), b_range=(-10,10), verbose=True):
'''
fix an activation function to be symbolic
Args:
-----
i : int
the id of input neuron
j : int
the id of output neuron
fun_name : str
the name of the symbolic functions
x : 1D array
preactivations
y : 1D array
postactivations
a_range : tuple
sweeping range of a
b_range : tuple
sweeping range of a
verbose : bool
print more information if True
Returns:
--------
r2 (coefficient of determination)
Example 1
---------
>>> # when x & y are not provided. Affine parameters are set to a = 1, b = 0, c = 1, d = 0
>>> sb = Symbolic_KANLayer(in_dim=3, out_dim=2)
>>> sb.fix_symbolic(2,1,'sin')
>>> print(sb.funs_name)
>>> print(sb.affine)
Example 2
---------
>>> # when x & y are provided, fit_params() is called to find the best fit coefficients
>>> sb = Symbolic_KANLayer(in_dim=3, out_dim=2)
>>> batch = 100
>>> x = torch.linspace(-1,1,steps=batch)
>>> noises = torch.normal(0,1,(batch,)) * 0.02
>>> y = 5.0*torch.sin(3.0*x + 2.0) + 0.7 + noises
>>> sb.fix_symbolic(2,1,'sin',x,y)
>>> print(sb.funs_name)
>>> print(sb.affine[1,2,:].data)
'''
Symbolic_KANLayer
类的 fix_symbolic
方法,用于将激活函数固定为符号形式。这个方法通常用于神经网络的符号学习阶段,其中激活函数被拟合为符号表达式,例如线性组合、多项式、三角函数等。这种方法允许模型学习更复杂的函数关系,而不仅仅是简单的线性组合。
参数说明:
i
:输入神经元的索引。j
:输出神经元的索引。fun_name
:拟合的符号函数的名称。x
:预激活值的1D数组。y
:激活值的1D数组。random
:布尔值,表示是否使用随机参数进行初始化。a_range
:元组,表示参数a的搜索范围。b_range
:元组,表示参数b的搜索范围。verbose
:布尔值,表示是否打印更多详细信息。
返回值:
r2
:决定系数(coefficient of determination),衡量拟合函数与实际激活值之间的相关性
当 fun_name
是一个字符串时
if isinstance(fun_name,str):
fun = SYMBOLIC_LIB[fun_name][0]
fun_sympy = SYMBOLIC_LIB[fun_name][1]
fun_avoid_singularity = SYMBOLIC_LIB[fun_name][3]
self.funs_sympy[j][i] = fun_sympy
self.funs_name[j][i] = fun_name
if x == None or y == None:
#initialzie from just fun
self.funs[j][i] = fun
self.funs_avoid_singularity[j][i] = fun_avoid_singularity
if random == False:
self.affine.data[j][i] = torch.tensor([1.,0.,1.,0.], device=self.device)
else:
self.affine.data[j][i] = torch.rand(4, device=self.device) * 2 - 1
return None
else:
#initialize from x & y and fun
params, r2 = fit_params(x,y,fun, a_range=a_range, b_range=b_range, verbose=verbose, device=self.device)
self.funs[j][i] = fun
self.funs_avoid_singularity[j][i] = fun_avoid_singularity
self.affine.data[j][i] = params
return r2
-
类型检查:
- 首先检查
fun_name
是否为字符串类型。
- 首先检查
-
获取函数和属性:
- 从
SYMBOLIC_LIB
字典中获取fun_name
对应的函数、符号表示和避免奇点的版本。
- 从
-
设置属性:
- 将符号函数和函数名称存储在实例变量中。
-
处理
x
和y
:- 如果
x
或y
为None
,则仅使用函数进行初始化。 - 如果
x
和y
都提供了,则使用fit_params
函数拟合参数并计算决定系数r2
。
- 如果
当 fun_name
是一个函数时
else:
# if fun_name itself is a function
fun = fun_name
fun_sympy = fun_name
self.funs_sympy[j][i] = fun_sympy
self.funs_name[j][i] = "anonymous"
self.funs[j][i] = fun
self.funs_avoid_singularity[j][i] = fun
if random == False:
self.affine.data[j][i] = torch.tensor([1.,0.,1.,0.], device=self.device)
else:
self.affine.data[j][i] = torch.rand(4, device=self.device) * 2 - 1
return None
-
处理函数:
- 如果
fun_name
是一个函数,则直接使用该函数。 - 将函数和匿名函数名称存储在实例变量中。
- 如果
-
设置属性:
- 将函数和避免奇点的版本设置为相同的值。
- 根据随机参数的值初始化
affine
。
测试:
# when x & y are not provided. Affine parameters are set to a = 1, b = 0, c = 1, d = 0
sb = Symbolic_KANLayer(in_dim=3, out_dim=2)
sb.fix_symbolic(2,1,'sin')
print(sb.funs_name)
print(sb.affine)
[['0', '0', '0'], ['0', '0', 'sin']]
Parameter containing:
tensor([[[0., 0., 0., 0.],
[0., 0., 0., 0.],
[0., 0., 0., 0.]],[[0., 0., 0., 0.],
[0., 0., 0., 0.],
[1., 0., 1., 0.]]], requires_grad=True)
# when x & y are provided, fit_params() is called to find the best fit coefficients
sb = Symbolic_KANLayer(in_dim=3, out_dim=2)
batch = 100
x = torch.linspace(-1,1,steps=batch)
noises = torch.normal(0,1,(batch,)) * 0.02
y = 5.0*torch.sin(3.0*x + 2.0) + 0.7 + noises
sb.fix_symbolic(2,1,'sin',x,y)
print(sb.funs_name)
print(sb.affine[1,2,:].data)
r2 is 0.9999717473983765
[['0', '0', '0'], ['0', '0', 'sin']]
tensor([-3.0000, -2.0000, -4.9992, 0.6980])
其实这个函数我们在hellokan中也见过:
mode = "auto" # "manual"
if mode == "manual":
# manual mode
model.fix_symbolic(0,0,0,'sin');
model.fix_symbolic(0,1,0,'x^2');
model.fix_symbolic(1,0,0,'exp');
elif mode == "auto":
# automatic mode
lib = ['x','x^2','x^3','x^4','exp','log','sqrt','tanh','sin','abs']
model.auto_symbolic(lib=lib)
fixing (0,0,0) with sin, r2=0.9999999186941056, c=2
fixing (0,1,0) with x^2, r2=0.9999999824464364, c=2
fixing (1,0,0) with exp, r2=0.9999999908233412, c=2
saving model version 0.6
2.6 swap
def swap(self, i1, i2, mode='in'):
'''
swap the i1 neuron with the i2 neuron in input (if mode == 'in') or output (if mode == 'out')
'''
with torch.no_grad():
def swap_list_(data, i1, i2, mode='in'):
if mode == 'in':
for j in range(self.out_dim):
data[j][i1], data[j][i2] = data[j][i2], data[j][i1]
elif mode == 'out':
data[i1], data[i2] = data[i2], data[i1]
def swap_(data, i1, i2, mode='in'):
if mode == 'in':
data[:,i1], data[:,i2] = data[:,i2].clone(), data[:,i1].clone()
elif mode == 'out':
data[i1], data[i2] = data[i2].clone(), data[i1].clone()
swap_list_(self.funs_name,i1,i2,mode)
swap_list_(self.funs_sympy,i1,i2,mode)
swap_list_(self.funs_avoid_singularity,i1,i2,mode)
swap_(self.affine.data,i1,i2,mode)
swap_(self.mask.data,i1,i2,mode)
这段代码定义了一个名为 swap
的方法,它接受一个神经网络模型的一部分参数,并将输入或输出层中的两个神经元(索引为 i1
和 i2
)进行交换。根据 mode
参数的值,交换发生在输入层(mode == 'in'
)或输出层(mode == 'out'
)。
代码详解:
- 使用
with torch.no_grad()
上下文管理器:这确保在执行交换操作时,不会计算梯度,这对于不需要梯度的交换操作(如数据预处理或模型初始化)是很有用的。 - 交换列表元素:
swap_list_
函数用于交换列表中的元素。对于输入和输出层,它会遍历列表(data
),并交换索引为i1
和i2
的元素。- 对于输入层(
mode == 'in'
),它会逐个元素地交换列表data
中对应输入维度的所有列表(即self.funs_name
,self.funs_sympy
,self.funs_avoid_singularity
)中索引为i1
和i2
的元素。 - 对于输出层(
mode == 'out'
),它直接交换data
中的i1
和i2
位置的元素。
- 交换张量元素:
swap_
函数用于交换张量中的元素。对于输入和输出层,它会使用.clone()
方法来创建元素的副本,然后交换这些元素。- 对于输入层(
mode == 'in'
),它会交换self.affine.data
和self.mask.data
中的i1
和i2
位置的元素。 - 对于输出层(
mode == 'out'
),它同样交换self.affine.data
和self.mask.data
中的i1
和i2
位置的元素。
四、总结
KANLayer
的类,用于构建和操作一种特殊的神经网络层,该层使用分段多项式(B-spline)基函数和残差函数来实现非线性变换。KANLayer
类实现了以下关键功能:
- 初始化
- 参数:初始化函数接受多个参数,包括输入维度、输出维度、网格间隔数、多项式阶数、噪声尺度、基函数参数、网格范围、网格更新策略等。
- 网格生成:生成一个等间距的网格,用于定义B-spline基函数的节点。网格的间距可以通过
grid_eps
参数进行调整,以在均匀网格和基于样本的非均匀网格之间进行平衡。 - 系数初始化:使用给定的网格和噪声初始化B-spline基函数的系数。系数通过曲线到系数的转换函数
curve2coef
生成。 - 激活函数参数:设置激活函数的基参数,如基函数的尺度和残差函数的尺度。
- 前向传播
- 前向计算:在前向传播中,首先通过基函数和系数计算输出。然后,通过激活函数处理输出,最后应用可训练的尺度参数。
- 网格更新
- 从样本更新网格:根据输入样本更新网格,以适应数据分布。这可以通过两种策略实现:基于样本的网格更新或基于网格的网格更新。
- 网络修剪和子集提取
- 网络修剪:通过选择特定的输入和输出节点来创建一个更小的
KANLayer
实例,用于网络修剪或模型压缩。
- 网络修剪:通过选择特定的输入和输出节点来创建一个更小的
- 层操作
- 层交换:允许交换输入或输出层中两个神经元的位置,这可能用于模型的正则化或优化策略。
Symbolic_KANLayer
的类,用于构建和操作一种特殊的神经网络层,该层使用符号计算来定义激活函数。Symbolic_KANLayer
类实现了以下关键功能:
-
初始化
- 参数:初始化函数接受输入维度、输出维度和设备参数。初始化时,激活函数默认设置为恒等函数。
- 属性:包括输入输出维度、激活函数列表、避免奇异性的激活函数列表、激活函数名称列表、符号表示的激活函数列表、以及线性变换参数(affine)。
-
前向传播
- 前向计算:在前向传播中,首先通过线性变换和激活函数计算输出。激活函数可以是符号计算函数,也可以是通过数据拟合得到的函数。
-
层操作
- 子集提取:允许从较大的
Symbolic_KANLayer
实例中提取较小的实例,用于网络修剪或模型压缩。 - 激活函数符号化:允许用户将特定激活函数(如
sin
、exp
等)应用到特定的输入输出节点。 - 层交换:允许交换输入或输出层中两个神经元的位置,这可能用于模型的正则化或优化策略。
- 子集提取:允许从较大的
下面是MultKAN的依赖包,LBFGS和plot_tree分别是模型的优化器和模型可视化工具,其他的依赖基本上已经学完了,所以我打算先研究如何构建完整的KAN,再研究优化器和可视化。
import torch
import torch.nn as nn
import numpy as np
from .KANLayer import KANLayer
#from .Symbolic_MultKANLayer import *
from .Symbolic_KANLayer import Symbolic_KANLayer
from .LBFGS import *
import os
import glob
import matplotlib.pyplot as plt
from tqdm import tqdm
import random
import copy
#from .MultKANLayer import MultKANLayer
import pandas as pd
from sympy.printing import latex
from sympy import *
import sympy
import yaml
from .spline import curve2coef
from .utils import SYMBOLIC_LIB
from .hypothesis import plot_tree
创作不易,请各位观众老爷多多支持!