KAN 学习 Day3 —— KANLayer.py 与 Symbolic_KANLayer.py 代码解读及测试

KAN学习Day1——模型框架解析及HelloKAN中,我对KAN模型的基本原理进行了简单说明,并将作者团队给出的入门教程hellokan跑了一遍;

KAN 学习 Day2 —— utils.py及spline.py 代码解读及测试中,我对项目的基本模块代码进行了解释,并以单元测试的形式深入理解模块功能,其中还发现了一个细小的错误。

今天直接上强度,开始对KANLayer进行剖析,与常见的深度学习模型类似,KAN也是由“层”堆叠起来的,因此KANLayer是整个项目的基础,务必理解透彻。

一、kan目录

kan目录结构如下,包括了模型源码、检查点、实验以及assets等

e12295be65d94b3381e647242dc51eba.pngcc0f7d4a5a5148c995fcd44ad9bbbab6.png

 先了解一下这些文件/文件夹的大致信息:

  • 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) 一个类,实现了 eq?%5Cphi 函数的细节以及层与层之间的某些操作。

f50d6221fdb74d20826626e4f29925ff.png

 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类的属性说明:

  1. in_dim:这个属性表示层的输入维度。它表明层期望接收多少个特征或输入。
  2. out_dim:这个属性表示层的输出维度。它表明层将产生多少个特征或输出。
  3. num:这个属性指定了网格间隔的数量。在样条函数的上下文中,这个参数定义了样条定义的网格的粒度。更多的间隔通常意味着对函数的更详细表示。
  4. k:这个属性表示分段多项式样条的单调阶数。它表明组成样条函数的多项式片段的阶数。
  5. noise_scale:这个属性是一个浮点数,表示样条函数的初始化值。它可以用来调整样条函数的初始幅度,可能影响结果的样条函数的复杂性或平滑性。
  6. coef:这个属性是一个 2D torch.tensor,包含 B样条基的系数。B样条是定义为控制点和基函数集合的一种样条。这里的系数用于构建样条函数。
  7. scale_base_mu 和 scale_base_sigma:这两个属性定义了剩余函数 b(x) 的均值和标准差。剩余函数是从正态分布中抽取的,这些参数控制了剩余的幅度和可变性,可能影响样条函数的行为。
  8. scale_sp:这个属性表示样条函数本身的幅度。它可以用来调整样条函数的整体尺度。
  9. base_fun:这个属性是一个函数(fun),表示剩余函数 b(x)。剩余函数用于样条函数的上下文中,以建模某种形式的偏差或误差项。
  10. mask:这个属性是一个 1D torch.float 张量,作为样条函数的掩码。掩码中的某些元素设置为0对应于0函数,有效地关闭了某些样条组件。
  11. grid_eps:这个属性是一个浮点数,控制网格细化策略。它影响如何对输入空间进行网格分区。当 grid_eps = 1 时,网格是均匀的;当 grid_eps = 0 时,它使用样本的百分位数进行分区。0 < grid_eps < 1 在这两种极端之间插值。锁定激活函数的ID
  12. device:这个属性指定了设备(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 类的初始化函数接收多个参数,以下是这些参数的详细说明:

  1. in_dim:输入维度,默认为3。这表示层接收的输入特征数量。
  2. out_dim:输出维度,默认为2。这表示层产生的输出特征数量。
  3. num:网格间隔的数量,默认为5。这定义了样条函数定义的网格的粒度。
  4. k:分段多项式样条的阶数,默认为3。这表示组成样条函数的多项式片段的阶数。
  5. noise_scale:初始化时注入的噪声的尺度,默认为0.5。这可以调整样条函数的初始幅度。
  6. scale_base_mu 和 scale_base_sigma:剩余函数 b(x) 的均值和标准差,默认分别为0.0和1.0。剩余函数是从正态分布中抽取的,这些参数控制了剩余的幅度和可变性。
  7. scale_sp:样条函数 spline(x) 的尺度,默认为1.0。这调整样条函数的整体幅度。
  8. base_fun:剩余函数 b(x) 的函数,默认为 torch.nn.SiLU()。这定义了样条函数的偏差或误差项。
  9. grid_eps:控制网格细化策略的浮点数,默认为0.02。当 grid_eps = 1 时,网格是均匀的;当 grid_eps = 0 时,它使用样本的百分位数进行分区。0 < grid_eps < 1 插值在两种极端之间。
  10. grid_range:网格范围的列表或数组,默认为[-1, 1]。这设置了网格的上下限。
  11. sp_trainable 和 sb_trainable:布尔值,分别表示 scale_sp 和 scale_base 是否可训练,默认分别为True和True。
  12. save_plot_data:布尔值,表示是否保存绘图数据,默认为True。
  13. device:设备,默认为'cpu'。层的计算将在其上执行。
  14. 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)。这些系数用于线性模型(如线性回归或逻辑回归)中的特征权重。所以这个参数也就是公式中的 eq?c_%7Bi%7D 
    • 在机器学习中,“curve” 是通过绘制训练集和测试集的准确率来评估模型在不同训练样本量下的表现,它可以帮助识别过拟合或欠拟合问题。
    • 本项目中使用这两个量进行 y 的计算和 grid 的更新。
        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 的值设置它们是否可训练。
    • 分别是公式中的eq?w_%7Bb%7Deq?w_%7Bs%7D
        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 设置为剩余函数,就是公式中的eq?b%28x%29
        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__中,分别实现了eq?w_%7Bb%7Deq?w_%7Bs%7Deq?c_%7Bi%7D 的初始化,以及 eq?b%28x%29 的选择。而在spline.py中的B_batch函数即为公式中的eq?B_%7Bi%7D,其他函数则为参数更新的方法。至于如何更新,请接着往后看。

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)

f50d6221fdb74d20826626e4f29925ff.png

  • 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函数,计算eq?b%28x%29的输出,形状为(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和eq?c_%7Bi%7D。而参数 eq?w_%7Bb%7Deq?w_%7Bs%7D 作为权重,更新过程应该在 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)的2D torch.float张量,表示输入样本。
  • mode:一个字符串参数,表示更新模式,默认为'sample'

函数的主要目标是更新网格和系数(coef),使得网格能够更好地适应输入样本的分布。以下是函数的主要步骤:

  1. 计算批次大小batch = x.shape[0],获取样本的数量。
  2. 对输入样本进行排序x_pos = torch.sort(x, dim=0)[0],将输入样本按照升序排列。
  3. 计算评估值:通过coef2curve函数,将排序后的样本值映射到当前网格(grid)上,得到评估值(y_eval)。
  4. 计算网格间隔数num_interval = self.grid.shape[1] - 1 - 2*self.k,计算当前网格的间隔数。
  5. 生成网格
    1. 使用get_grid函数生成适应性网格(grid_adaptive)和均匀网格(grid_uniform)。
    2. grid = get_grid(num_interval),生成最终的网格,结合适应性和均匀性。
  6. 根据模式更新网格
    1. 如果mode'grid',则从适应性网格中生成更多的样本网格(sample_grid),并更新网格和系数。
  7. 扩展网格:使用extend_grid函数扩展网格,以适应输入样本的分布。
  8. 更新系数:使用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实例。

参数说明:

  • parentKANLayer实例,通常网格比当前模型的网格更粗。
  • x: 2D torch.float张量,形状为(number of samples, input dimension),表示输入样本。
  • mode: 字符串,可选参数,用于指定更新方式。默认为'sample'

返回值

  • None:方法不返回任何值,而是直接更新KANLayer实例中的gridcoef

方法步骤

  1. 初始化参数
    1. 获取输入样本x的批大小batch
    2. 对于每个输入样本,执行以下操作。
  2. 排序输入样本
    1. 对输入样本x进行排序,得到排序后的x_pos
  3. 评估曲线
    1. 使用coef2curve函数评估排序后的x_pos,得到y_eval
  4. 计算间隔数
    1. 计算grid的间隔数num_interval,这是grid的宽度减去两端的额外间隔数。
  5. 生成网格
    1. 使用get_grid函数生成适应性网格grid_adaptive和均匀网格grid_uniform
    2. 根据num_interval计算网格点,然后结合grid_adaptivegrid_uniform生成最终的网格grid
  6. 更新网格和系数
    1. 根据mode参数决定是否使用额外的样本网格进行更新。
    2. 使用extend_gridcurve2coef函数更新gridcoef

只能说和更新层自身的参数一模一样!

测试:

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:列表,包含所选输出神经元的索引。

返回值

  • spbKANLayer实例,是从原始KANLayer中选择的子集。

方法步骤:

  1. 创建一个新的KANLayer实例spb,其输入维度为len(in_id),输出维度为len(out_id),其他参数与原始KANLayer相同。
  2. 将原始KANLayergridcoefscale_basescale_spmask数据根据in_idout_id进行选择,并赋值给spb
  3. 更新spbin_dimout_dim属性。
  4. 返回spb

其实剪枝的操作我们在hellokan中已经见识过了,如下图:

54020614e4c04e679b5e3a259cae177b.png49e05088a8d6474fa8ca00778d35a4ab.png

测试:

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实例的内部数据结构。

方法步骤:

  1. 使用torch.no_grad()上下文管理器来避免在执行交换操作时计算梯度,这对于不需要梯度的交换操作是必要的。
  2. 定义一个内部函数swap_,该函数接受一个数据列表(如grid.datacoef.data等)和两个索引i1i2,以及一个模式参数mode
  3. 根据mode参数,调用swap_函数来交换对应的数据。如果是输入神经元交换(mode == 'in'),则交换grid.data中的元素;如果是输出神经元交换,则交换coef.datascale_base.datascale_sp.datamask.data中的元素。
  4. 最后,调用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)

在神经网络模型中,交换节点(或神经元)的主要好处有以下几个方面:

  1. 增强模型的灵活性:通过交换节点,模型可以探索不同的连接模式,从而增强其适应性和灵活性。这有助于模型在面对不同数据集或任务时,能够以更灵活的方式进行学习和调整。

  2. 优化性能:交换节点可能有助于优化模型的性能。通过改变节点之间的连接,模型可能能够找到更有效的路径来传递信息,从而提高计算效率和预测准确性。

  3. 避免过拟合:在某些情况下,通过改变节点连接,可以减少模型对训练数据的过度依赖,从而帮助减少过拟合现象。这通常与增加模型的泛化能力相关。

  4. 增强解释性:在某些神经网络架构中,特定节点的连接模式可能影响模型的决策过程。通过调整这些连接,可以增加模型决策的透明度和可解释性。

  5. 促进特征学习:节点之间的连接模式直接影响特征的提取和学习过程。通过调整这些连接,模型可能能够更有效地学习和表示输入数据的特征,从而提高模型的性能。

  6. 适应性增强:在处理动态或变化的输入数据时,能够快速调整节点连接的模型可能具有更好的适应性。这在处理时间序列数据、图像识别或自然语言处理等任务中尤为重要。

  7. 减少冗余:在某些情况下,通过交换节点,可以减少模型中的冗余连接,从而减少计算成本和内存使用,提高模型的效率。

  8. 探索不同架构:交换节点可以作为探索不同神经网络架构的一种手段。通过尝试不同的连接模式,研究者和开发者可以发现新的有效架构,这可能对特定任务或数据集特别有利。

  9. 提高模型的鲁棒性:通过改变节点的连接,模型可能能够更好地抵抗输入数据的微小变化,从而提高其在面对噪声或不完整数据时的鲁棒性。

结论来自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)

构造函数内部实现

  1. super(Symbolic_KANLayer, self).__init__():调用父类的构造函数。这通常用于初始化父类属性。
  2. self.out_dim = out_dim 和 self.in_dim = in_dim:设置实例属性 out_dim 和 in_dim,分别表示输出和输入特征的维度。
  3. self.mask:创建一个 torch.nn.Parameter,它是一个可训练的参数,初始化为零张量,维度为 (out_dim, in_dim),并指定设备。requires_grad_(False) 表示这个参数不应该跟踪梯度,即它不会被优化。
  4. self.funs:初始化一个二维列表,每个子列表包含 in_dim 个 lambda 函数。每个 lambda 函数将输入乘以 0.,即恒等函数乘以一个非常小的数,这可能是为了避免数值问题。
  5. self.funs_avoid_singularity:类似于 funs,但包含一个额外的参数 y_th,用于避免奇异情况。每个 lambda 函数返回一个元组和一个乘以 0. 的输入。
  6. self.funs_name:初始化一个二维列表,每个子列表包含 in_dim 个字符串 '0'。这可能是用于存储函数名称或其他标识。
  7. self.funs_sympy:初始化与 funs 相同的二维列表,但每个 lambda 函数乘以 0.。这可能是用于存储符号表示的函数。
  8. self.affine:创建一个 torch.nn.Parameter,它是一个可训练的参数,初始化为零张量,维度为 (out_dim, in_dim, 4),并指定设备。这用于表示线性变换 eq?c*f%28a*x&plus;b%29&plus;d,affine每一个分向量依次存储着 eq?a%2Cb%2Cc%2Cd
  9. self.device 和 self.to(device)
    1. 设置实例属性 device,表示模型运行在哪个设备上。
    2. 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)

方法实现:

  1. 获取输入数据的批次大小 batch 和输入特征维度。
  2. 初始化一个空列表 postacts,用于存储每个节点的激活。
  3. 对每个输入特征 i 和输出特征 j 进行迭代:
    1. 如果 singularity_avoiding 为 True,则使用 funs_avoid_singularity,否则使用 funs
    2. 根据选择的函数,计算 xij,这是通过线性变换eq?c*f%28a*x&plus;b%29&plus;d和可能的奇异值避免操作得到的。
    3. 将 xij 与 mask 相乘,然后添加到 postacts_ 列表中。
  4. 将 postacts_ 列表中的所有元素堆叠成一个张量,并将其添加到 postacts 列表中。
  5. 将 postacts 列表中的所有张量堆叠成一个三维张量,然后将其转置,以便输出特征的维度在第二个位置。
  6. 对 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 实例,表示从较大实例中提取的子集。

方法实现:

  1. 创建一个新的 Symbolic_KANLayer 实例 sbb,并初始化其参数以匹配 in_id 和 out_id 的大小。
  2. 将 sbb 的 in_dim 设置为 in_id 的长度(即选择的输入神经元的数量)。
  3. 将 sbb 的 out_dim 设置为 out_id 的长度(即选择的输出神经元的数量)。
  4. 将 sbb 的 mask 属性设置为 self.mask 属性的子集,该子集由 out_id 和 in_id 确定。这表示在新层中,只有在 out_id 和 in_id 中的对应位置为 True 的连接是有效的。
  5. 将 sbb 的 funsfuns_avoid_singularityfuns_sympy 和 funs_name 属性分别设置为 self 属性的子集,这些子集同样由 out_id 和 in_id 确定。这些属性通常包含激活函数、奇异值避免函数、符号表示和函数名称等信息。
  6. 将 sbb 的 affine 属性设置为 self.affine 属性的子集,该子集由 out_id 和 in_id 确定。affine 属性通常包含线性变换的权重。
  7. 返回 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
  1. 类型检查

    • 首先检查 fun_name 是否为字符串类型。
  2. 获取函数和属性

    • 从 SYMBOLIC_LIB 字典中获取 fun_name 对应的函数、符号表示和避免奇点的版本。
  3. 设置属性

    • 将符号函数和函数名称存储在实例变量中。
  4. 处理 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
  1. 处理函数

    • 如果 fun_name 是一个函数,则直接使用该函数。
    • 将函数和匿名函数名称存储在实例变量中。
  2. 设置属性

    • 将函数和避免奇点的版本设置为相同的值。
    • 根据随机参数的值初始化 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')。

代码详解:

  1. 使用 with torch.no_grad() 上下文管理器:这确保在执行交换操作时,不会计算梯度,这对于不需要梯度的交换操作(如数据预处理或模型初始化)是很有用的。
  2. 交换列表元素
    1. swap_list_ 函数用于交换列表中的元素。对于输入和输出层,它会遍历列表(data),并交换索引为 i1 和 i2 的元素。
    2. 对于输入层(mode == 'in'),它会逐个元素地交换列表 data 中对应输入维度的所有列表(即 self.funs_nameself.funs_sympyself.funs_avoid_singularity)中索引为 i1 和 i2 的元素。
    3. 对于输出层(mode == 'out'),它直接交换 data 中的 i1 和 i2 位置的元素。
  3. 交换张量元素
    1. swap_ 函数用于交换张量中的元素。对于输入和输出层,它会使用 .clone() 方法来创建元素的副本,然后交换这些元素。
    2. 对于输入层(mode == 'in'),它会交换 self.affine.data 和 self.mask.data 中的 i1 和 i2 位置的元素。
    3. 对于输出层(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实例中提取较小的实例,用于网络修剪或模型压缩。
    • 激活函数符号化:允许用户将特定激活函数(如sinexp等)应用到特定的输入输出节点。
    • 层交换:允许交换输入或输出层中两个神经元的位置,这可能用于模型的正则化或优化策略。

下面是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

 创作不易,请各位观众老爷多多支持!

 

 

  • 13
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值