引言
KAN源码中KANlayer
模块涉及到了许多coef2curve
和coef2curve
操作,这里实际就是在利用Bspline曲线进行插值或者说拟合,以下介绍相关的BSpline插值知识。后一篇中我们将结合相关代码详细解释Bspline是如何融入到KAN的计算过程中的。
Bspline曲线
对于Bspline曲线的构造我们只需要给定一系列的控制点
p
0
,
.
.
.
,
p
n
p_0,...,p_n
p0,...,pn(control points)以及节点向量
U
=
{
u
0
,
.
.
,
u
m
+
1
}
U=\{u_0,..,u_{m+1}\}
U={u0,..,um+1}(knot vector),以及次数
k
k
k即可进行计算。
首先,BSpline曲线的基础计算公式是:
C
(
u
)
=
∑
i
=
0
n
p
i
N
i
,
k
(
u
)
C(u)=\sum_{i=0}^{n} p_iN_{i,k}(u)
C(u)=i=0∑npiNi,k(u)
其中
p
i
p_{i}
pi是人为选取或者求取的控制点,
N
i
,
k
(
t
)
N_{i,k}(t)
Ni,k(t)是
k
k
k次(
k
+
1
k+1
k+1阶)基函数(这里,
k
k
k次的意思是
N
i
,
k
(
t
)
N_{i,k}(t)
Ni,k(t)中包含了
t
t
t的
k
k
k次项
t
k
t^k
tk,这里我们假定
k
k
k从0开始取值)。
在这一公式的基础上我们只需要确定控制点以及基函数,就可以计算给定
t
t
t时的曲线点位置。后者可以通过Cox-de Boor递归公式来确定:
N i , 0 ( u ) = { 1 i f u i ≤ u < u i + 1 a n d u i < u i + 1 0 o t h e r w i s e N i , j ( u ) = u − u i u i + j − u i N i , j − 1 ( u ) + u i + j + 1 − u u i + j + 1 − u i + 1 N i + 1 , j − 1 ( u ) , \begin{aligned} &N_{i,0}\left(u\right) \left.=\left\{\begin{matrix}{1}&{\mathrm{if~}u_i\leq u<u_{i+1} \mathrm{and} u_i<u_{i+1}}\\\\{0}&{\mathrm{otherwise}}\\\end{matrix}\right.\right. \\ &N_{i,j}(u) =\frac{u-u_{i}}{u_{i+j}-u_{i}} N_{i,j-1} (u)+\frac{u_{i+j+1}-u}{u_{i+j+1}-u_{i+1}} N_{i+1,j-1} (u), \end{aligned} Ni,0(u)=⎩ ⎨ ⎧10if ui≤u<ui+1andui<ui+1otherwiseNi,j(u)=ui+j−uiu−uiNi,j−1(u)+ui+j+1−ui+1ui+j+1−uNi+1,j−1(u),
节点向量
这里需要注意的是,节点向量的设置是比较有讲究的,假定需要 m + 1 m+1 m+1个节点,通常有以下几个方案
- 在 [ 0 , 1 ] [0,1] [0,1]范围内进行划分(通常就是等距);这样形成了open Bspline曲线,不经过控制点折线的第一段和最后一段;
- 前 k + 1 k+1 k+1个节点设置为0, u 0 = u 1 = , . . , = u k = 0 u_0=u_1=,..,=u_k=0 u0=u1=,..,=uk=0,中间进行采样(通常就是等距),后 k + 1 k+1 k+1个节点设置为1, u m − k − 1 = , . . = u m = 1 u_{m-k-1}=,..=u_{m}=1 um−k−1=,..=um=1;这样形成了 clamped B-spline曲线,它会穿过第一个和最后一个控制点,并且和第一段最后一段折线相切;
- 重复多个节点。这样形成closed B-spline曲线.
次数、控制点数目和节点数目的关系1
次数
k
k
k,控制点数目
n
+
1
n+1
n+1和节点数目
m
+
1
m+1
m+1需要满足
m
=
n
+
k
+
1
m=n+k+1
m=n+k+1,也就是说如果给出了5个控制点,要求计算次数为3的B样条曲线,我们需要自行设置长度为9的节点向量。这是因为由Cox-de Boor递归公式我们可以知道计算
N
i
,
k
(
u
)
N_{i,k}(u)
Ni,k(u),我们需要计算
N
i
,
k
−
1
(
u
)
N_{i,k-1}(u)
Ni,k−1(u) 和
N
i
+
1
,
p
−
1
(
u
)
N_{i+1,p-1}(u)
Ni+1,p−1(u),可以画出如下计算流图:
从而得到
m
=
n
+
k
+
1
m=n+k+1
m=n+k+1。
通过推导我们也可以进一步得到以下两个性质:
- 基函数 N i , k ( u ) N_{i,k}(u) Ni,k(u) 在区间 [ u i , u i + k + 1 ) [u_i, u_{i+k+1}) [ui,ui+k+1) 上非零。换句话说, N i , k ( u ) N_{i,k}(u) Ni,k(u) 在 k + 1 k+1 k+1 个结节区间 [ u i , u i + 1 ) , [ u i + 1 , u i + 2 ) , … , [ u i + k , u i + k + 1 ) [u_i, u_{i+1}), [u_{i+1}, u_{i+2}), \ldots, [u_{i+k}, u_{i+k+1}) [ui,ui+1),[ui+1,ui+2),…,[ui+k,ui+k+1) 上非零。
- 在任意结节区间 [ u i , u i + 1 ) [u_i, u_{i+1}) [ui,ui+1) 上,最多有 k + 1 k+1 k+1 个度数为 k k k 的基函数是非零的,即: N i − k , k ( u ) , N i − k + 1 , k ( u ) , N i − k + 2 , k ( u ) , … , N i − 1 , k ( u ) N_{i-k,k}(u), N_{i-k+1,k}(u), N_{i-k+2,k}(u), \ldots, N_{i-1,k}(u) Ni−k,k(u),Ni−k+1,k(u),Ni−k+2,k(u),…,Ni−1,k(u) 和 N i , k ( u ) N_{i,k}(u) Ni,k(u)。
Bspline插值
KAN中首先使用到的技术就是BSpline插值,整体而言,使用Bspline进行曲线插值的步骤可以总结为如下:
步骤1:数据准备
准备需要拟合的数据点集合 D = { D 0 , D 1 , . . , D n } \mathbb{D}=\{\mathbf{D_0},\mathbf{D_1},..,\mathbf{D_n}\} D={D0,D1,..,Dn},通常是二维或三维空间中的点。
步骤2:节点向量的选择
选择节点向量,这决定了B样条基函数的形状和数量。节点向量可以是等距的或非等距的,通常情况下选择非等距节点能够更灵活地控制曲线的形状,简单情况下,我们进行等距采样。
而在节点向量结构方面,我们会选用clamped结构(为什么这么选,其实是没懂的,感觉插值下反正都会经过第一个和最后一个控制点,使用open好像也没差),有
m
+
1
m+1
m+1 个结节
u
0
=
u
1
=
…
=
u
p
=
0
u_0=u_1=\ldots=u_p=0
u0=u1=…=up=0,
u
p
+
1
u_{p+1}
up+1,
u
p
+
2
u_{p+2}
up+2,
…
\ldots
…,
u
m
−
p
−
1
u_{m-p-1}
um−p−1,
u
m
−
p
=
u
m
−
p
+
1
=
…
=
u
m
=
1
u_{m-p}=u_{m-p+1}=\ldots=u_m=1
um−p=um−p+1=…=um=1。
步骤3:构建插值矩阵
根据数据点和节点向量构建插值矩阵。插值矩阵的每一行对应一个数据点,每一列对应一个基函数在该数据点处的取值。插值矩阵每个元素的求取是通过Codx算法得到的,流程如下:
算法描述
输入:n、k、m、u 和 m+1 个闭合结节
{
u
0
,
.
.
.
,
u
m
}
\{ u_0, ..., u_m \}
{u0,...,um}
输出:系数
N
0
,
k
(
u
)
N_{0,k}(u)
N0,k(u)、
N
1
,
k
(
u
)
N_{1,k}(u)
N1,k(u)、…、
N
n
,
k
(
u
)
N_{n,k}(u)
Nn,k(u) 存储在
N
[
0
]
N[0]
N[0]、
N
[
1
]
N[1]
N[1]、…、
N
[
n
]
N[n]
N[n] 中
算法步骤2
-
将 N [ 0.. n ] N[0..n] N[0..n] 初始化为 0; // 初始化
-
如果 u = u 0 u = u_0 u=u0,则 // 排除特殊情况
- N [ 0 ] = 1.0 N[0] = 1.0 N[0]=1.0;
- 返回
-
否则如果 u = u m u = u_m u=um,则
- N [ n ] = 1.0 N[n] = 1.0 N[n]=1.0;
- 返回
-
结束如果
-
// 现在 u u u 在 u 0 u_0 u0 和 u m u_m um 之间
-
令 u u u 在结节区间 [ u k , u k + 1 ) [u_k, u_{k+1}) [uk,uk+1) 中;
-
N [ k ] : = 1.0 N[k] := 1.0 N[k]:=1.0;// 度数 0 的系数
-
对于 d d d 从 1 到 k k k 循环执行:// 度数 d d d 从 1 到 k k k
- 开始
- N [ k − d ] = u k + 1 − u u k + 1 − u k − d + 1 ∗ N [ ( k − d ) + 1 ] N[k-d] = \frac{u_{k+1}-u}{u_{k+1}-u_{k-d+1}}* N[(k-d)+1] N[k−d]=uk+1−uk−d+1uk+1−u∗N[(k−d)+1]; // 右侧(西南角)项
- 对于
i
i
i 从
k
−
d
+
1
k-d+1
k−d+1 到
k
−
1
k-1
k−1 循环执行:// 计算内部项
- N [ i ] : = u − u i u i + d − u i ∗ N [ i ] + u i + d + 1 − u u i + d + 1 − u i + 1 ∗ N [ i + 1 ] N[i] := \frac{u-u_i}{u_{i+d}-u_{i}} * N[i] +\frac{u_{i+d+1}-u}{u_{i+d+1}-u_{i+1}} * N[i+1] N[i]:=ui+d−uiu−ui∗N[i]+ui+d+1−ui+1ui+d+1−u∗N[i+1];
- N [ k ] = u − u k u k + d − u i + k ∗ N [ k ] N[k] = \frac{u-u_k}{u_{k+d}-u_{i+k}} * N[k] N[k]=uk+d−ui+ku−uk∗N[k]; // 左上角项
- 结束
-
// 数组 N [ 0.. n ] N[0..n] N[0..n] 中存储了系数。
对每前
n
+
1
n+1
n+1节点都进行这样的操作(后面的节点会因为计算项数不足被舍掉,所以只会有n+1个这样的求系数操作),我们就能得到插值矩阵:
[
M_k = \begin{bmatrix}
N_{0,k}(u_0) & N_{1,k}(u_0) & \cdots & N_{n,k}(u_0) \
N_{0,k}(u_1) & N_{1,k}(u_1) & \cdots & N_{n,k}(u_1) \
\vdots & \vdots & \ddots & \vdots \
N_{0,k}(u_n) & N_{1,k}(u_n) & \cdots & N_{n,k}(u_n)
\end{bmatrix}
]
其中,
u
0
,
u
1
,
…
,
u
n
u_0, u_1, \ldots, u_n
u0,u1,…,un 是插值点,
N
i
,
k
(
u
)
N_{i,k}(u)
Ni,k(u) 是
k
k
k次B样条基函数在该节点处的取值。
步骤4:控制点的计算
使用最小二乘法,求解线性方程组 D = N P D=NP D=NP其中 N N N是插值矩阵, P P P是控制点的向量, D D D是数据点坐标向量。
步骤5:B样条曲线的计算
对于给出的任意待插值坐标序列 u \mathbf{u} u,使用Cox-de Boor递归公式得到在 n n n个基函数在该位置上的取值,使用插值点和计算得到的控制点 p p p进行加权求和,计算code构建B样条曲线。
总结
使用Bspline进行插值实际只有两个关键步骤:
- 构造节点向量
- 计算控制点
然后基于Cox-de Boor递归公式就可以计算出插值曲线在任意位置的值了。