函数可以是数组,可以是向量的本质:直观的理解与实用的应用,从δ函数到向量空间的直觉推演

26 阅读13分钟

逼乎原贴 zhuanlan.zhihu.com/p/191455445…

概述

本文从开发的视角, 以较直觉的形式展示了 函数可以是数组,函数即向量。

略为严谨的推出数组可以等价函数,函数等价向量的结论。.

看懂所需前置

看懂本文大概只需要一点点微积分,和线性代数坐标变换的储备,和一点点程序基础。

尽可能的引入一些富含直觉的例子来解释。

严谨性就没那么有保证了。

收益

把函数当成数组或者向量,看公式的时候啊,看代码的时候,灵活切换视角,可以提供更直观、更贴近本质的理解。

数据压缩,图片卷积,傅里叶变换,brdf求解,球谐光照 积分问题 导数问题 函数拟合问 等等各种问题都可以切换视角让他变得更容易理解

吐槽和碎碎念

他喵的, 这类文章 通篇是证明和公式,而且还跳步,还有一堆数学黑话,什么算子 无限维的完备内积空间 哈哈哈 懂的就懂 不懂的就不懂

看的人都麻了 不是说不好,是要看懂,得学完好多本书 。虽然理解数学为了严谨,就需要无歧义的用数学语言定义一堆东西。

但是工程上 不需要这么严谨, 工程各种处理手段的不严谨远比这影响要大得多

1 菜菜的开发的奇妙幻想,所有函数可以用数组描述

自己实现一个方法,计算sin(x)的值。可以这么做

-- 在规定精度内,在可表达的最大值最小值之间记录下所有sin的取值

function calFun(x)
	return math.sin(x)
end

local min =  -double最大值
local max =  double最大值
local precisionStep = double最小值

local  funList = {}
for i = min,max,precisionStep do
	local mappIndex = (i + max)/ precisionStep
	funList[mappIndex] = calFun(i)
end

print(funList)  

然后把print的结果copy出来

-- 把记录到的值整理成数组。拿出来用

local min = -double最大值
local max =  double最大值
local precisionStep = double最小值

local cacheFun =  copy的数据贴进来
function sin(x)
	local mappIndex = (x + max)/ precisionStep 
	return cacheFun[mappIndex]
end

先不管你内存爆炸没爆炸嘛~嘿嘿 你会发现。从使用结果上看。

记录的函数值精度已经超过了double的精度你的sin和库函数的sin,没任何区别。(现实情况下不行,浮点数精度和内存会引入新的问题,这里暂时视为理想状态)

极端一点你可以吧常用函数都这么定义了,然后你拿这些基本函数来复合,积分,求导,你会发现和之前的没任何区别。简直妙不可言

直觉上,这似乎在暗示着 函数本质上或许可以用类似数组的数学结构表示。精度取无穷小时,数组应该就能完美重建需要表示函数。

另一个角度(用无数个点来表示函数)

对于不是开发的同学

从另一个很自然的角度去思考,我用一个个点去描述函数。如果我点越多,那我就和函数越接近,如果有无穷多个点,每个点之间的距离又无穷近,那就能完美的构造函数。

Pasted image 20250607021825.png

Pasted image 20250607021814.png

游戏开发中也这么玩

在游戏开发里真就这么干。游戏并不需要高精度数值计算,他只要在有限精度内骗的过人的眼就行。坐标0..0001,几乎是 已经是肉眼无法察觉的精度了。

特别是在做帧同步的情景下,我们需要多端的运算结果一致。浮点数的性质带来一系列运算不一致。所以我们会采用 固定精度的 定点数去做计算。

定点数性能比浮点数差的多,所以查数组比你去泰勒展开牛顿迭代,进行一堆加减乘除算起来快的多。一些 数sin 1/√x 用的就是以上方法,打表,以及结合其他手段来提高精度降低内存开销。

一些无解析解的非基本函数也会用数组替代。例如 有时候需要再曲线上做匀速运动,就需要求解弧长积分。 如果你做游戏的话,多多少少也有这种从应用场景磨练出来的直觉

2 函数可以表示成数组的理论支持

在第一节,我们用代码提取出来sin(x)各个位置的值,储存到数组。然后再用这个数组在一定精度内重建函数。直觉上函数应该可以等价数组。

以下会从数学的角度, 用数学语言完成以上的操作。

引入δ函数和δ函数系。用δ函数创建一个像数组一样的函数,并且在精度取无穷小时, 会发现这个数组等价于函数。

以下依旧沿用第一部分的直觉,把函数继续表示成数组。

2.1 δ函数的引入

并定义以下函数 (离散情况下的定义,连续不可用)

δ(x)={1if x=00if x0\delta(x) = \begin{cases} 1 & \text{if } x = 0 \\ 0 & \text{if } x ≠ 0 \end{cases}

Pasted image 20250606020241.png 这个函数仅在x=0位置取1,其他位置均为0。 第一眼你会觉得,看起来似乎没什么用,而且这他喵叫函数?我看你才像个函数。

但是表示成数组的话,那好像也说的过去。

δ(x)=[...,0,0,1x=0,0,0,...]\delta(x) = [...,0,0,\underbrace{1}_{x=0处},0,0,...]

(这个函数长的有点离谱,他是广义函数。

在连续情况下他的本身,积分,微分都被定义出来的。

这里抛开一些数学细节,使用他在离散情况下的定义。它非常有用,他沟通离散和连续。 )

表示起来有点麻烦,之后函数都截取(0,+∞) 。并且我们已经知道

δ(x)=[1x=0,0,0,...]\delta(x) = [\underbrace{1}_{x=0处},0,0,...]

2.2 平移δ函数构造函数系

接着,平移这个函数,引出δ函数系。 通过平移函数,我们获得了一系列函数,并且可以用这些函数获得用来充满数轴的连续点集。

δ(x1)也就是只有x=11其他位置为0δ(x1.5)也就是只有x=1.51其他位置为0δ(x2)也就是只有x=21其他位置为0...δ(xn)也就是只有x=n1其他位置为0\begin{align} & \delta(x - 1) 也就是只有x=1 为1 其他位置为0 \\ & \delta(x - 1.5) 也就是只有x=1.5 为1 其他位置为0\\ & \delta(x - 2) 也就是只有x=2 为1 其他位置为0\\ & ...\\ & \delta(x - n) 也就是只有x=n 为1 其他位置为0 \end{align}

例如 δ(x-1)

δ(x1)=[0,...,1x=1,0,...] δ(x-1) = [0,...,\underbrace{1}_{x=1处},0,...]

Pasted image 20250606020137.png

2.3 使用δ函数系的重建函数

一个δ函数可以在数组创建一个元素,无穷多个就能创建无穷多个元素。

f(x)=a1δ(xΔt)+a2δ(x2Δt)+a3δ3(x3Δt)++anδndt(xnΔt)(Δt=0.1)f(x) = a_1 \delta_{}(x-Δt) + a_2 \delta_(x-2Δt) + a_{3} \delta_{3}(x-3Δt) + \cdots + a_n \delta_{ndt}(x-nΔt) \tag{Δt=0.1}

Pasted image 20250606021549.png

f(x)=[1,1,1,1,1,1,1,1...]f(x) = [1,1,1,1,1,1,1,1...]

在各个δ函数前取上合适的系数,就能用δ函数系组合出想要的函数数组。

sin(x)a1δ(xΔt)+a2δ(x2Δt)+a3δ3(x3Δt)++anδndt(xnΔt)(Δt=0.1) sin(x) ≈ a_1 \delta_{}(x-Δt) + a_2 \delta_(x-2Δt) + a_{3} \delta_{3}(x-3Δt) + \cdots + a_n \delta_{ndt}(x-nΔt)\tag{Δt=0.1}

Pasted image 20250606021453.png

sin(x)=[0.00,0.10,0.20,0.30,0.39,0.48,0.56,0.64...]sin(x) = [0.00,0.10,0.20,0.30,0.39,0.48,0.56,0.64...]

2.4 使用δ函数的分离处系数

压力来到求系数这边来了,直觉上 an = f(n)。

如果直觉是对的,背后必然有理论可以用来解释他为什么是对的。(其理论是 δ函数系列的特殊性导致)

为了深挖一步,揭露更加本质的东西,不直接使用a(n)=f(n),而尝试使用δ函数去捕获f(x)在n处的一个值,并储存为系数an。

分析一下

显然an系数的值只和n有关,因此期望构造一个和自变量x无关的函数,在无论什么x取多少,能返回n处的值。

数学不像程序,没有if else 和数组索引。数学只有加,减,乘,除,积分,求导等运算。接下开始尝试用数学运算来分离目标处的函数值。

挑一个二次函数来做演示,f(x) = x^2,取精度为整数。方便表示

f(x)=[0,1,4,9,16,...] f(x) = [0,1,4,9,16,...]

期望分离处f(2)处的值,准备好δ(x-2)

δ(x2)=[0,0,1,0,0,...]\delta_(x-2) = [0,0,1,0,0,...]

相乘一下试试

f(x)δ(x2)=[0,0,4,0,0,...]f(x) \delta_(x-2) = [0,0,4,0,0,...]

好像很接近了? 但是返回的是一个新函数,不是一个值。 只有输入2的时候才是4,输入其他的得到的是0。不符合要求

这么多个0,那就把他全加起来。全加起来数学上操作对应定积分,定积分能让函数返回一个数值,而不是返回一个函数。积分本质在于求和,离散的求和就是Σ,连续的求和就是 ∫

x=f(x)δ(x2)=x=[0,0,4,0,0,...]=0+0+4+0+0+...=4\sum_{x=-∞}^{∞}f(x) \delta_(x-2) = \sum ^{∞}_{x=-∞}[0,0,4,0,0,...] = 0+0+4+0+0+... =4

好,good。用纯数学语言,使用两个函数捣鼓处来一个值。

总结下就是

a1=x=f(x)δ(x1)=f(1)a2=x=f(x)δ(x2)=f(2).....(2.4-1)\begin{align} a1 = \sum_{x=-∞}^{∞}f(x) δ(x-1) =f(1) \\ a2 = \sum_{x=-∞}^{∞}f(x) δ(x-2) =f(2) \\ .....\\ \end{align} \tag{2.4-1}

枚举这么多系数要死人了,忍不住抽了个求系数的函数。

an=f(n)=x=f(x)δ(xn)(2.4-2)an = f(n) = \sum_{x=-∞}^{∞}f(x) δ(x-n) \tag{2.4-2}

至此,系数问题已经完美解决。

2.5 sin函数表达成数组代码 转为 数学语言表达

嘛~准备工作都做完了,接下来用数学语言来解释代码了

之前的把函数值存成数组的代码,在数学上的表达如下。

for i = min,max,precisionStep do
	local mappIndex = (i + max)/ precisionStep
	funList[mappIndex] = calFun(i)
end

precisionStep 取1,不然太鬼畜了。

a[max]=x=maxmaxδ(xmax+0)sin(x)=sin(max)a[max+1]=x=maxmaxδ(xmax+1)sin(x)=sin(max+1)......a[max1]=x=maxmaxδ(x+max1)sin(x)=sin(max1)a[max]=x=maxmaxδ(x+max0)sin(x)=sin(max)\begin{align} a[-max] =\sum_{x=-max}^{max} \delta_{}(x-max+0) *sin(x) &= sin(-max) \\ a[-max+1] =\sum_{x=-max}^{max}\delta_(x-max+1) *sin(x) &= sin(-max+1) \\ ......\\ a[max-1] = \sum_{x=-max}^{max}\delta_(x+max-1) *sin(x) &= sin(max-1) \\ a[max] = \sum_{x=-max}^{max}\delta_(x+max-0) *sin(x) &= sin(max) \\ \end{align}

利用离散的点重建函数的时候,数学上的表达如下。

function sin(x)
	local mappIndex = (x + max)/ precisionStep 
	return cacheFun[mappIndex]
end
sin(x)=a[max]δ(max)+a[max+1]δ(max+1)+...a[max]δ(max)sin(x) = a[-max] \delta(-max) + a[-max+1] \delta(-max+1) + ... a[max] \delta(max)

2.6 步长无限小时可以重建出任意函数

整合一下以上的例子,发现可以用以下公式在有限精度内表示任意一个函数!!

f(x)f(x)=a1δ(xΔn)+a2δ(x2Δn)+a3δ(x3Δn)++anδ(xnΔn)(2.6-1) f_(x) ≈ f_{≈}(x) = a_1 \delta_{}(x-Δn) + a_2 \delta_(x-2Δn) + a_{3} \delta_(x-3Δn) + \cdots + a_n \delta_(x-nΔn) \tag{2.6-1}
f(x)=n=anδ(xn) f_{≈}(x) = \sum_{n=-∞}^{∞} a_n \delta_(x-n)

其中系数a就来源于

a(n)=f(n)=x=f(x)δ(xn)(2.6-2)a(n) = f(n) = \sum_{x=-∞}^{∞}f(x) δ(x-n) \tag{2.6-2}

【2.6-1式】的意义:函数可以近似的由一系列不同比例δ函数混合出来(用δ函数系线性组合出来),直观上就是可以用无数个点来近似。

【2.6-2式】的意义:用来混合的比例可以直接通过函数和δ函数的积分确定,但他也等价于函数在目标处的值来确定。

例 精度Δt取1时

f(x)=a1δ(x1)+a2δ(x2)+a3δ3(x3)++anδndt(xn) f(x) = a_1 \delta_{}(x-1) + a_2 \delta_(x-2) + a_{3} \delta_{3}(x-3) + \cdots + a_n \delta_{ndt}(x-n)

代入x=1

=a11+a20+a30++an0=a1 ? = a_1 *1 + a_2 *0 + a_{3} *0 + \cdots + a_n *0 = a_1
a(1)=x=f(x)δ(x1)=f(1)a(1) = \sum_{x=-∞}^{∞}f(x) δ(x-1) =f(1)

嘛 带进去一顿算就是,取1时会获得f(1) ~绕了一大圈,说明重建是成功的。

当精度无限小时,我们有无穷个间隔无限小的系数。 2.6-2变成如下。

(δ函数使用连续形式的定义,让他变得可以积分,不过他和离散下的表现是一样的)

a(n)=f(n)=f(x)δ(xn)dx(2.6-3)a(n) = f(n) = ∫_{-∞}^{∞} f(x)\delta_(x-n) dx \tag{2.6-3}

可以看到,系数函数a(n)变的连续,并且已经完全等价于原函数(似乎已经重建)。

使用无穷连续系数去重建函数,求和也变成的积分,变成如下。

f(t)=a(n)δ(tn)dn(2.6-4)f(t) = ∫_{-∞} ^ {∞} a(n)\delta(t-n) dn \tag{2.6-4}

整合一下就是

f(t)=  (f(x)δ(xn)dx)   δ(tn)dn(2.6-5) f(t) = ∫_{-∞} ^ {∞}\space\space (∫_{-∞}^{∞} f(x)\delta_(x-\textcolor{red}n) dx ) \space\space\space \delta(t-\textcolor{red}n)d\textcolor{red}n \tag{2.6-5}

注意 2.6-3式的意义是重建 ,2.6-4式的意义是分离系数

分离系数和重建背后的意义是不一样的,但从推导结果上看,他们的公式是一样的,这在第三部分会得到解释。

总结

至此,用δ函数重建了函数。δ函数沟通了连续和离散

当有限精度时,用捕获的函数值重建的函数是近似的。

当有精度无穷大时,用捕获的函数值重建函数的即是精确的。

所以函数是一个数组,完全是合理的直觉。当取精确无穷大时候,他就是函数。

接下来,会在有限精度的情况继续分析。(有限精度时候分析会比较方便,比较符合直觉)

分析出来的结论,仅需取精度无穷小就可以直接推广到连续形式。

总之说了一大堆

一方面是从理论肯定了用数组替代函数的行为。

另一方面,为了可以在连续和离散之间反复横跳。离散的情况好分析就去分析离散,连续的形式好分析就去分析连续。 只要离散的处理手段和连续的处理手段有对应关系,最后分析出来的结论,两边共享。

3 函数即数组,函数即向量(Hilbert空间视角)

函数如果是向量的话,那我们就可以用线性代数的工具去拷打他。

函数可以用数组表示,向量可以用数组表示。这似乎再暗示着他们的同个东西

3.1 δ函数重建函数的意义

回到2.6-1 2.6-2,之前先求得系数,然后再用系数重建函数。接下来会从向量的角度去解释
(令Δn=1,忽视掉映射数组的过程。结论成立的话最后只需要取步长无限小就可以推广到连续)

f(x)=a1δ(x1)+a2δ(x2)+a3δ(x3)++anδ(xn)(3.1-1) f_{}(x) = a_1 \delta_{}(x-1) + a_2 \delta_(x-2) + a_{3} \delta_(x-3) + \cdots + a_n \delta_(x-n) \tag{3.1-1}
a(n)=f(n)=x=f(x)δ(xn)(3.1-2)a(n) = f(n) = \sum_{x=-∞}^{∞}f(x) δ(x-n) \tag{3.1-2}

将f(x)函数,δ(x-n)也写成向量,看看会发生什么。

f(x)=[f(0)f(1)f(2)]δ(x1)=[1000],δ(x2)=[0100],δ(x3)=[0010]\vec{f(x)} = \begin{bmatrix} f(0) \\ f(1) \\ f(2) \\ \vdots \end{bmatrix} \vec\delta(x-1) = \begin{bmatrix} 1 \\ 0 \\ 0 \\ \vdots \\ 0 \end{bmatrix}, \quad \vec\delta(x-2) = \begin{bmatrix} 0 \\ 1 \\ 0 \\ \vdots \\ 0 \end{bmatrix}, \quad \vec\delta(x-3) = \begin{bmatrix} 0 \\ 0 \\ 1 \\ \vdots \\ 0 \end{bmatrix}

3.1-1 用向量来表示

f(x)=a1δ(x1)+a2δ(x2)+a3δ(x3)++anδ(xn)\begin{align} \vec f_{}(x) = a_1 \vec\delta_{}(x-1) + a_2 \vec\delta_(x-2) + a_{3} \vec\delta_(x-3) + \cdots + a_n \vec\delta_(x-n) \end{align}

整合系数a

f(x)=[δ(x1),δ(x2),...,δn(xn)]a(n) \vec{f(x)} = [\vec\delta_(x-1), \vec\delta(x-2) ,...,\vec\delta_n(x-n)] \vec a(n)

令人惊讶,δ函数系构成了坐标系。

重构函数,只是简单的 把坐标系下的向量转化到世界坐标系下。 (不懂的可以看坐标变换部分 # 【矩阵乘法】一眼秒懂的矩阵乘法,从空间变换到图像压缩:矩阵乘法的四种视角)

3.2 δ函数分离获得系数的意义

分离系数的过程,能猜到 其实只是在把世界空间下的向量转换到δ函数系构成的本地坐标系下。

在第2.4节分离系数部分,谈到了分离系数期望输出一个值,所以将两个数组相乘后相加。仔细想想 这不是在做点乘嘛。 把2.4-1改写成向量形式

a(1)=δ(x1)Tf(x)a(2)=δ(x2)Tf(x)...a(n)=δ(xn)Tf(x)\begin{align} a(1) = \vec\delta(x-1)^Tf(x) \\ a(2) = \vec\delta(x-2)^Tf(x) \\ ...\\ a(n) = \vec\delta(x-n)^Tf(x) \\ \end{align}

整理成矩阵形式。(点乘 需要写成行向量的形式)

a(n)=[δ(x1)Tδ(x2)T...δn(xn)T]f(x) \vec{a(n)} = \begin{bmatrix} \vec\delta_(x-1)^T\\ \vec\delta(x-2) ^T\\ ...\\ \vec\delta_n(x-n)^T \end{bmatrix}\vec f(x)

整体转置下,好看一点。

a(n)=[δ(x1),δ(x2),...,δn(xn)]Tf(x) \vec{a(n)} = [\vec\delta_(x-1), \vec\delta(x-2) ,...,\vec\delta_n(x-n)]^T\vec f(x)

δ函数系的向量组用矩阵A替代

A=[δ(x1),δ(x2),...,δn(xn)]A = [\vec\delta_(x-1), \vec\delta(x-2) ,...,\vec\delta_n(x-n)]

把重建和分离系数写在一起的话

f(x)=AATf(x) \vec{f(x)} = AA^Tf(x)

由于δ是正交矩阵。正交矩阵有性质 矩阵逆 = 矩阵转置

所以分离系数和重建抵消掉了。

f(x)=f(x) \vec{f(x)} = \vec f(x)

这也就解释了分离系数后重建,重建出来的函数和原本的一样。

也是2.6-5式看着像脱了裤子放屁的原因。

执行分离系数,和重构相当于进行了一个逆变换,在执行一次正变换。结果不会发生改变

3.3 δ函数系 构成的坐标系是单位矩阵

展开δ函数系矩阵

A=[δ(x1),δ(x2),...,δn(xn)]=[100010001]A = [\vec\delta_(x-1), \vec\delta(x-2) ,...,\vec\delta_n(x-n)] = \begin{bmatrix} 1 & 0 & 0 & \cdots \\ 0 & 1 & 0 & \cdots \\ 0 & 0 & 1 & \cdots \\ \vdots & \vdots & \vdots & \ddots \end{bmatrix}

他喵的是个单位矩阵。单位矩阵乘任何向量都是原向量。

这是 2.4 提到的a(n) = f(n) ,分离系数的行为看起来像脱了裤子放屁的原因。

执行变换的意义不大,所以我们可以直接取函数值进行使用。

3.4 离散到连续,获得解释积分的新视角

2.6-3 2.6-4 2.6-5 可以看到,当取精度无穷大时候,∑变成了∫。

结合3.3求系数的本质实际是在做点乘,可以获得以下视角。

n=f(x)δ(xn)f(x)δ(xn)dn\begin{align} \sum_{n=-∞}^{∞} f(x)\delta_(x-n) \tag{离散的点乘} \\\\ \int_{-∞}^{∞} f(x)\delta_(x-n)dn \tag{连续的点乘} \end{align}

(注意坐标系变换的点乘意义,仅在变换时候坐标系标准正交才有效)

结合2.2重建函数的本质,是在做对基向量的线性组合,可以获得以下视角

f(x)=n=anδ(xn)f(x)=a(n)δ(xn)dn\begin{align} f_{≈}(x) = \sum_{n=-∞}^{∞} a_n \delta_(x-n) \tag{离散的线性组合}\\ \\ f(x) = ∫_{-∞} ^ {∞} a(n)\delta(x-n) dn \tag{连续的线性组合} \end{align}

基本上线性代数坐标变换等解释,在连续下也同样适用。

4 变换到其他坐标系

函数是表示在标准坐标系下的向量。

那么,自然会联想有没有其他坐标系也能用来表示函数。

利用坐标变换去思考。

我们仅需随便找一组线性无关的函数系,来做坐标系即可。

用逆变换取得系数,用系数乘正变换在吧函数变回去。(连续形式无逆变换,所以要求正交性)

其他函数系

有非常多的函数系,用来做函数变换。

意义的话还是具体情况具体分析。

例如

  • 当行为是周期的,我们对他建模使用傅里叶变换就可以获得他周期的特性。
  • 微分方程不好解,三角函数导4次变成他自己。所以用傅里叶变换。
  • 图片压缩,用的余弦变换,用正弦变换,傅里叶变换会导致接缝处有瑕疵。
  • 环境光记录用球谐展开。
  • 数据压缩,会挑选和数据模式接近的函数系,这样子能更好的解耦数据,丢弃影响小的分量。

  • 傅里叶变换
  • 离散正弦变换
  • 幂级数(实际直接变换变换不了,幂函数系不正交) 不正交无法通过点乘(积分)获得系数,在离散情况下可以通过逆矩阵求得。但是连续情况下就无法通过这种形式求得系数。 有其他技术去获得不一样的幂级数系数。 例如 泰勒展开 施密特正交化,让函数系正交后勒让德展开。
  • 小波变换
  • 球谐函数
  • 等等

具体应用之后再开新文章写吧