逼乎原贴 zhuanlan.zhihu.com/p/191455445…
概述
本文从开发的视角, 以较直觉的形式展示了 函数可以是数组,函数即向量。
略为严谨的推出数组可以等价函数,函数等价向量的结论。.
看懂所需前置
看懂本文大概只需要一点点微积分,和线性代数坐标变换的储备,和一点点程序基础。
尽可能的引入一些富含直觉的例子来解释。
严谨性就没那么有保证了。
收益
把函数当成数组或者向量,看公式的时候啊,看代码的时候,灵活切换视角,可以提供更直观、更贴近本质的理解。
数据压缩,图片卷积,傅里叶变换,brdf求解,球谐光照 积分问题 导数问题 函数拟合问 等等各种问题都可以切换视角让他变得更容易理解
吐槽和碎碎念
他喵的, 这类文章 通篇是证明和公式,而且还跳步,还有一堆数学黑话,什么算子 无限维的完备内积空间 哈哈哈 懂的就懂 不懂的就不懂
看的人都麻了 不是说不好,是要看懂,得学完好多本书 。虽然理解数学为了严谨,就需要无歧义的用数学语言定义一堆东西。
但是工程上 不需要这么严谨, 工程各种处理手段的不严谨远比这影响要大得多
1 菜菜的开发的奇妙幻想,所有函数可以用数组描述
自己实现一个方法,计算sin(x)的值。可以这么做
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,没任何区别。(现实情况下不行,浮点数精度和内存会引入新的问题,这里暂时视为理想状态)
极端一点你可以吧常用函数都这么定义了,然后你拿这些基本函数来复合,积分,求导,你会发现和之前的没任何区别。简直妙不可言
直觉上,这似乎在暗示着
函数本质上或许可以用类似数组的数学结构表示。精度取无穷小时,数组应该就能完美重建需要表示函数。
另一个角度(用无数个点来表示函数)
对于不是开发的同学
从另一个很自然的角度去思考,我用一个个点去描述函数。如果我点越多,那我就和函数越接近,如果有无穷多个点,每个点之间的距离又无穷近,那就能完美的构造函数。


游戏开发中也这么玩
在游戏开发里真就这么干。游戏并不需要高精度数值计算,他只要在有限精度内骗的过人的眼就行。坐标0..0001,几乎是 已经是肉眼无法察觉的精度了。
特别是在做帧同步的情景下,我们需要多端的运算结果一致。浮点数的性质带来一系列运算不一致。所以我们会采用 固定精度的 定点数去做计算。
定点数性能比浮点数差的多,所以查数组比你去泰勒展开牛顿迭代,进行一堆加减乘除算起来快的多。一些 数sin 1/√x 用的就是以上方法,打表,以及结合其他手段来提高精度降低内存开销。
一些无解析解的非基本函数也会用数组替代。例如 有时候需要再曲线上做匀速运动,就需要求解弧长积分。
如果你做游戏的话,多多少少也有这种从应用场景磨练出来的直觉
2 函数可以表示成数组的理论支持
在第一节,我们用代码提取出来sin(x)各个位置的值,储存到数组。然后再用这个数组在一定精度内重建函数。直觉上函数应该可以等价数组。
以下会从数学的角度, 用数学语言完成以上的操作。
引入δ函数和δ函数系。用δ函数创建一个像数组一样的函数,并且在精度取无穷小时,
会发现这个数组等价于函数。
以下依旧沿用第一部分的直觉,把函数继续表示成数组。
2.1 δ函数的引入
并定义以下函数 (离散情况下的定义,连续不可用)
δ(x)={10if x=0if x=0
这个函数仅在x=0位置取1,其他位置均为0。
第一眼你会觉得,看起来似乎没什么用,而且这他喵叫函数?我看你才像个函数。
但是表示成数组的话,那好像也说的过去。
δ(x)=[...,0,0,x=0处1,0,0,...]
(这个函数长的有点离谱,他是广义函数。
在连续情况下他的本身,积分,微分都被定义出来的。
这里抛开一些数学细节,使用他在离散情况下的定义。它非常有用,他沟通离散和连续。
)
表示起来有点麻烦,之后函数都截取(0,+∞) 。并且我们已经知道
δ(x)=[x=0处1,0,0,...]
2.2 平移δ函数构造函数系
接着,平移这个函数,引出δ函数系。
通过平移函数,我们获得了一系列函数,并且可以用这些函数获得用来充满数轴的连续点集。
δ(x−1)也就是只有x=1为1其他位置为0δ(x−1.5)也就是只有x=1.5为1其他位置为0δ(x−2)也就是只有x=2为1其他位置为0...δ(x−n)也就是只有x=n为1其他位置为0
例如 δ(x-1)
δ(x−1)=[0,...,x=1处1,0,...]

2.3 使用δ函数系的重建函数
一个δ函数可以在数组创建一个元素,无穷多个就能创建无穷多个元素。
f(x)=a1δ(x−Δt)+a2δ(x−2Δt)+a3δ3(x−3Δt)+⋯+anδndt(x−nΔt)(Δt=0.1)

f(x)=[1,1,1,1,1,1,1,1...]
在各个δ函数前取上合适的系数,就能用δ函数系组合出想要的函数数组。
sin(x)≈a1δ(x−Δt)+a2δ(x−2Δt)+a3δ3(x−3Δt)+⋯+anδndt(x−nΔt)(Δt=0.1)

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(2)处的值,准备好δ(x-2)
δ(x−2)=[0,0,1,0,0,...]
相乘一下试试
f(x)δ(x−2)=[0,0,4,0,0,...]
好像很接近了? 但是返回的是一个新函数,不是一个值。 只有输入2的时候才是4,输入其他的得到的是0。不符合要求
这么多个0,那就把他全加起来。全加起来数学上操作对应定积分,定积分能让函数返回一个数值,而不是返回一个函数。积分本质在于求和,离散的求和就是Σ,连续的求和就是 ∫
x=−∞∑∞f(x)δ(x−2)=x=−∞∑∞[0,0,4,0,0,...]=0+0+4+0+0+...=4
好,good。用纯数学语言,使用两个函数捣鼓处来一个值。
总结下就是
a1=x=−∞∑∞f(x)δ(x−1)=f(1)a2=x=−∞∑∞f(x)δ(x−2)=f(2).....(2.4-1)
枚举这么多系数要死人了,忍不住抽了个求系数的函数。
an=f(n)=x=−∞∑∞f(x)δ(x−n)(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=−max∑maxδ(x−max+0)∗sin(x)a[−max+1]=x=−max∑maxδ(x−max+1)∗sin(x)......a[max−1]=x=−max∑maxδ(x+max−1)∗sin(x)a[max]=x=−max∑maxδ(x+max−0)∗sin(x)=sin(−max)=sin(−max+1)=sin(max−1)=sin(max)
利用离散的点重建函数的时候,数学上的表达如下。
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)
2.6 步长无限小时可以重建出任意函数
整合一下以上的例子,发现可以用以下公式在有限精度内表示任意一个函数!!
f(x)≈f≈(x)=a1δ(x−Δn)+a2δ(x−2Δn)+a3δ(x−3Δn)+⋯+anδ(x−nΔn)(2.6-1)
f≈(x)=n=−∞∑∞anδ(x−n)
其中系数a就来源于
a(n)=f(n)=x=−∞∑∞f(x)δ(x−n)(2.6-2)
【2.6-1式】的意义:函数可以近似的由一系列不同比例δ函数混合出来(用δ函数系线性组合出来),直观上就是可以用无数个点来近似。
【2.6-2式】的意义:用来混合的比例可以直接通过函数和δ函数的积分确定,但他也等价于函数在目标处的值来确定。
例 精度Δt取1时
f(x)=a1δ(x−1)+a2δ(x−2)+a3δ3(x−3)+⋯+anδndt(x−n)
代入x=1
?=a1∗1+a2∗0+a3∗0+⋯+an∗0=a1
a(1)=x=−∞∑∞f(x)δ(x−1)=f(1)
嘛 带进去一顿算就是,取1时会获得f(1) ~绕了一大圈,说明重建是成功的。
当精度无限小时,我们有无穷个间隔无限小的系数。 2.6-2变成如下。
(δ函数使用连续形式的定义,让他变得可以积分,不过他和离散下的表现是一样的)
a(n)=f(n)=∫−∞∞f(x)δ(x−n)dx(2.6-3)
可以看到,系数函数a(n)变的连续,并且已经完全等价于原函数(似乎已经重建)。
使用无穷连续系数去重建函数,求和也变成的积分,变成如下。
f(t)=∫−∞∞a(n)δ(t−n)dn(2.6-4)
整合一下就是
f(t)=∫−∞∞ (∫−∞∞f(x)δ(x−n)dx) δ(t−n)dn(2.6-5)
注意 2.6-3式的意义是重建 ,2.6-4式的意义是分离系数
分离系数和重建背后的意义是不一样的,但从推导结果上看,他们的公式是一样的,这在第三部分会得到解释。
总结
至此,用δ函数重建了函数。δ函数沟通了连续和离散
当有限精度时,用捕获的函数值重建的函数是近似的。
当有精度无穷大时,用捕获的函数值重建函数的即是精确的。
所以函数是一个数组,完全是合理的直觉。当取精确无穷大时候,他就是函数。
接下来,会在有限精度的情况继续分析。(有限精度时候分析会比较方便,比较符合直觉)
分析出来的结论,仅需取精度无穷小就可以直接推广到连续形式。
总之说了一大堆
一方面是从理论肯定了用数组替代函数的行为。
另一方面,为了可以在连续和离散之间反复横跳。离散的情况好分析就去分析离散,连续的形式好分析就去分析连续。 只要离散的处理手段和连续的处理手段有对应关系,最后分析出来的结论,两边共享。
3 函数即数组,函数即向量(Hilbert空间视角)
函数如果是向量的话,那我们就可以用线性代数的工具去拷打他。
函数可以用数组表示,向量可以用数组表示。这似乎再暗示着他们的同个东西
3.1 δ函数重建函数的意义
回到2.6-1 2.6-2,之前先求得系数,然后再用系数重建函数。接下来会从向量的角度去解释
(令Δn=1,忽视掉映射数组的过程。结论成立的话最后只需要取步长无限小就可以推广到连续)
f(x)=a1δ(x−1)+a2δ(x−2)+a3δ(x−3)+⋯+anδ(x−n)(3.1-1)
a(n)=f(n)=x=−∞∑∞f(x)δ(x−n)(3.1-2)
将f(x)函数,δ(x-n)也写成向量,看看会发生什么。
f(x)=f(0)f(1)f(2)⋮δ(x−1)=100⋮0,δ(x−2)=010⋮0,δ(x−3)=001⋮0
3.1-1 用向量来表示
f(x)=a1δ(x−1)+a2δ(x−2)+a3δ(x−3)+⋯+anδ(x−n)
整合系数a
f(x)=[δ(x−1),δ(x−2),...,δn(x−n)]a(n)
令人惊讶,δ函数系构成了坐标系。
重构函数,只是简单的 把坐标系下的向量转化到世界坐标系下。
(不懂的可以看坐标变换部分 # 【矩阵乘法】一眼秒懂的矩阵乘法,从空间变换到图像压缩:矩阵乘法的四种视角)
3.2 δ函数分离获得系数的意义
分离系数的过程,能猜到 其实只是在把世界空间下的向量转换到δ函数系构成的本地坐标系下。
在第2.4节分离系数部分,谈到了分离系数期望输出一个值,所以将两个数组相乘后相加。仔细想想 这不是在做点乘嘛。 把2.4-1改写成向量形式
a(1)=δ(x−1)Tf(x)a(2)=δ(x−2)Tf(x)...a(n)=δ(x−n)Tf(x)
整理成矩阵形式。(点乘 需要写成行向量的形式)
a(n)=δ(x−1)Tδ(x−2)T...δn(x−n)Tf(x)
整体转置下,好看一点。
a(n)=[δ(x−1),δ(x−2),...,δn(x−n)]Tf(x)
δ函数系的向量组用矩阵A替代
A=[δ(x−1),δ(x−2),...,δn(x−n)]
把重建和分离系数写在一起的话
f(x)=AATf(x)
由于δ是正交矩阵。正交矩阵有性质 矩阵逆 = 矩阵转置
所以分离系数和重建抵消掉了。
f(x)=f(x)
这也就解释了分离系数后重建,重建出来的函数和原本的一样。
也是2.6-5式看着像脱了裤子放屁的原因。
执行分离系数,和重构相当于进行了一个逆变换,在执行一次正变换。结果不会发生改变
3.3 δ函数系 构成的坐标系是单位矩阵
展开δ函数系矩阵
A=[δ(x−1),δ(x−2),...,δn(x−n)]=100⋮010⋮001⋮⋯⋯⋯⋱
他喵的是个单位矩阵。单位矩阵乘任何向量都是原向量。
这是 2.4 提到的a(n) = f(n) ,分离系数的行为看起来像脱了裤子放屁的原因。
执行变换的意义不大,所以我们可以直接取函数值进行使用。
3.4 离散到连续,获得解释积分的新视角
2.6-3 2.6-4 2.6-5 可以看到,当取精度无穷大时候,∑变成了∫。
结合3.3求系数的本质实际是在做点乘,可以获得以下视角。
n=−∞∑∞f(x)δ(x−n)∫−∞∞f(x)δ(x−n)dn(离散的点乘)(连续的点乘)
(注意坐标系变换的点乘意义,仅在变换时候坐标系标准正交才有效)
结合2.2重建函数的本质,是在做对基向量的线性组合,可以获得以下视角
f≈(x)=n=−∞∑∞anδ(x−n)f(x)=∫−∞∞a(n)δ(x−n)dn(离散的线性组合)(连续的线性组合)
基本上线性代数坐标变换等解释,在连续下也同样适用。
4 变换到其他坐标系
函数是表示在标准坐标系下的向量。
那么,自然会联想有没有其他坐标系也能用来表示函数。
利用坐标变换去思考。
我们仅需随便找一组线性无关的函数系,来做坐标系即可。
用逆变换取得系数,用系数乘正变换在吧函数变回去。(连续形式无逆变换,所以要求正交性)
其他函数系
有非常多的函数系,用来做函数变换。
意义的话还是具体情况具体分析。
例如
- 当行为是周期的,我们对他建模使用傅里叶变换就可以获得他周期的特性。
- 微分方程不好解,三角函数导4次变成他自己。所以用傅里叶变换。
- 图片压缩,用的余弦变换,用正弦变换,傅里叶变换会导致接缝处有瑕疵。
- 环境光记录用球谐展开。
- 数据压缩,会挑选和数据模式接近的函数系,这样子能更好的解耦数据,丢弃影响小的分量。
如
- 傅里叶变换
- 离散正弦变换
- 幂级数(实际直接变换变换不了,幂函数系不正交)
不正交无法通过点乘(积分)获得系数,在离散情况下可以通过逆矩阵求得。但是连续情况下就无法通过这种形式求得系数。
有其他技术去获得不一样的幂级数系数。
例如 泰勒展开
施密特正交化,让函数系正交后勒让德展开。
- 小波变换
- 球谐函数
- 等等
具体应用之后再开新文章写吧