最小二乘法:程序员为程序员编写的文本

我继续发表我的演讲,最初是为那些学习“数字地质”专业的学生而准备的。 这是该中心出版的第三本出版物,第一篇是介绍性文章,无需阅读。 但是,要理解本文,有必要阅读线性方程组的介绍 ,即使您知道它是什么,因为我将大量参考该介绍中的示例。

因此,今天的任务是:学习最简单的几何处理,例如,能够将我的头变成复活节岛的偶像:



当前的演讲计划:


官方课程资料库位于此处 。 这本书还没有读完,我正在慢慢地编辑哈布雷出版的文章。

在本文中,我的主要工具将是寻找最小二次函数; 但是在我们开始使用此工具之前,您至少需要了解他在哪里拥有“打开/关闭”按钮。 首先,刷新内存,然后回忆一下什么是矩阵,什么是正数以及什么是导数。

矩阵和数字


在本文中,我将大量使用矩阵表示法,因此让我们回顾一下它是什么。 不要在文本中进一步看,要暂停几秒钟,然后尝试公式化什么是矩阵。

不同的矩阵解释


答案很简单。 矩阵只是一个存放辣根的柜子。 每个狗屎都位于其单元格中,单元格按行和列按行分组。 在我们的特殊情况下,通常的实数是废话; 对于程序员而言,最容易呈现矩阵 喜欢的东西

float A[m][n]; 

为什么需要这种存储? 他们描述什么? 也许我会让你不高兴,但是矩阵本身并没有描述任何东西,它存储着。 例如,您可以在其中存储任何函数的系数。 让我们忘记矩阵一秒钟,然后想象我们有一个数字 。 什么意思 是的,魔鬼知道...例如,它可能是函数内部的一个系数,该系数以一个数字作为输入,并以另一个数字作为输出:

f x m a t h b b R r i g h t a r r o w m a t h b b R   



数学家可以写这样一个函数的一个版本:

f x =


好吧,在程序员的世界里,它看起来像这样:

 float f(float x) { return a*x; } 

另一方面,为什么要这样的功能,而又不完全是另一个? 让我们再来一个!

f x = 2


自从我开始学习程序员以来,我必须写她的代码:)

 float f(float x) { return x*a*x; } 

这些函数之一是线性的,第二个函数是二次的。 哪一个是正确的? 不,数字本身 没有定义它,只是存储了一些价值! 您需要什么功能,像这样构建一个。

矩阵也会发生同样的事情,如果没有足够的单个数字(标量),这是数字的一种扩展,这些就是需要的存储。 就像在数字上一样,在矩阵上方也定义了加法和乘法运算。

假设我们有一个矩阵 例如,尺寸为2x2:

= \开b 中号- [R X 11一个12a21a22\结bmatrix



这个矩阵本身并不意味着任何东西,例如,它可以解释为一个函数

fx mathbbR2 rightarrow mathbbR2 quadfx=Ax



 vector<float> f(vector<float> x) { return vector<float>{a11*x[0] + a12*x[1], a21*x[0] + a22*x[1]}; } 

此函数将二维向量转换为二维向量。 在图形上,将其表示为图像转换很方便:我们将图像提供给输入,在输出处得到其拉伸和/或旋转(甚至可以镜像!)版本。 很快我将给出带有矩阵解释的各种示例的图片。

并且可以矩阵 想象一下将二维向量转换为标量的函数:

fx mathbbR2 rightarrow mathbbR quadfx=x topAx= sum limitsi sum limitsjaijxixj



请注意,矢量的乘幂不是很明确,所以我不能写 x2 ,就像他在写普通数字时一样。 我强烈建议那些不习惯处理矩阵乘法的人,再次回忆一下矩阵乘法规则,并验证表达式 x topAx 通常允许,并且确实在出口处给出了标量。 为此,例如,您可以明确地放在方括号中 x topAx=x topAx 。 我提醒你,我们有 x 是维度2的向量(存储在维度2x1的矩阵中),我们明确编写所有维度:

 underbrace underbrace left underbracex top1 times2 times underbraceA2 times2 right1 times2 times underbracex2 times11 times1



回到温暖蓬松的程序员世界,我们可以编写相同的二次函数,如下所示:

 float f(vector<float> x) { return x[0]*a11*x[0] + x[0]*a12*x[1] + x[1]*a21*x[0] + x[1]*a22*x[1]; } 

什么是正数?


现在我将问一个非常愚蠢的问题:什么是正数? 我们有了一个称为谓词的强大工具 > 。 但是花点时间回答这个数字 当且仅当 a>0 那太容易了。 让我们定义正性如下:

定义1


编号 当且仅当对于所有非零实数为正 x in mathbbR x neq0 条件满足 ax2>0

它看起来很棘手,但它完全适用于矩阵:

定义2


方阵 如果为非零,则称为正定 x 条件满足 x topAx>0 也就是说,除了原点外,其他所有地方的对应二次形式都严格为正。

为什么我需要积极性? 正如我在文章开头提到的那样,我的主要工具将是搜索二次函数的最小值。 但是至少要进行搜索,如果它存在的话也不错! 例如一个功能 fx=x2 因为数字-1不是正数,所以最小值显然不存在,并且 fx 随着增长而不断减少 x :抛物线的分支 fx 往下看 正定矩阵是好的,因为相应的二次形式形成了具有(唯一)最小值的抛物面。 下图显示了七个不同的矩阵示例。 2\乘2 ,无论是正定的还是对称的,事实并非如此。 上排:矩阵作为函数的解释 fx mathbbR2 rightarrow mathbbR2 。 中排:将矩阵解释为函数 fx mathbbR2 rightarrow mathbbR



因此,我将使用正定矩阵,它们是正数的推广。 而且,在我的情况下,矩阵也将对称! 很好奇的是,当人们谈论积极确定性时,它们也常常意味着对称,而对称性可以通过以下观察(可以理解以下文字)来间接解释。

二次形式矩阵对称性的抒情离题


让我们看一下二次形式 x\前Mx 对于任意矩阵 M ; 然后 M 添加并立即减去其转置版本的一半:

M=\底线 frac12M+M top=Ms+\底线 frac12MM top=Ma=Ms+Ma



矩阵 Ms 对称的: M stop=Ms ; 矩阵 Ma 反对称: M atop=Ma 。 一个显着的事实是,对于任何反对称矩阵,对应的二次形式都等于零。 这来自以下观察:

q=x topMax=x topM atopx top=x topMax top=q


也就是说,二次形式 x topMax 同时等于 qq ,仅当 q\等0 。 从这个事实可以得出,对于任意矩阵 M 对应的二次形式 x\前Mx 可以通过对称矩阵表示 Ms

x topMx=x topMs+Max=x topMsx+x topMax=x topMsx



我们正在寻找二次函数的最小值


让我们简短地回到一维世界。 我想找到最少的功能 fx=22bx 。 编号 肯定地,因此存在一个最小值; 为了找到它,我们将相应的导数等于零:  fracddxfx=0 。 区分劳动的一维二次函数不是:  fracddxfx=2ax2b=0 ; 所以我们的问题归结为求解方程式 axb=0 ,通过艰苦的努力,我们找到了解决方案 x=b/a 。 下图说明了两个问题的等效性:解决方案 x 方程式 axb=0 与方程的解一致  mathrmargminxax22bx



我倾向于这样一个事实,即我们的总体目标是最小化二次函数,但是我们正在谈论最小二乘。 但是与此同时,我们真正知道如何做得很好的是求解线性方程(以及线性方程组)。 而且一个等于另一个很酷!

给我们留下的唯一一件事就是确保一维世界中的等价性扩展到案例 n 变量。 为了验证这一点,我们首先证明三个定理。

三个定理或微分矩阵表达式


第一个定理指出矩阵 1\乘1 关于换位的不变式:

定理1


x in mathbbR Rightarrowx top=x

证明太复杂了,为了简洁起见,我不得不省略它,但是请尝试自己找到它。

第二定理使我们能够区分线性函数。 对于一个变量的实函数,我们知道  fracddxbx=b 但是在实函数的情况下会发生什么 n 变量?

定理2


 nablab topx= nablax topb=b

也就是说,不出意外,只是记录同一学校成绩的矩阵记录。 证明非常简单,只需编写渐变的定义即可:

 nablab topx=\开bmatrix frac\部b topx\部x1 vdots frac\部b topx\部xn\结bmatrix=\开bmatrix frac\部b1x1+\点+bnxn\部x1 vdots frac\部b1x1+\点+bnxn\部xn\结bmatrix=\开bmatrixb1 vdotsbn\结bmatrix=b


应用第一个定理 b topx=x topb ,我们完成了证明。

现在我们传递给二次形式。 我们知道在一个变量的实函数的情况下  fracddxax2=2ax ,并且在二次形式的情况下会发生什么?

定理3


 nablax topAx=A+A topx

顺便提一下,如果矩阵 对称,那么定理说  nablax topAx=2Ax

该证明也很简单,只需将二次形式写为双倍和:

x topAx= sum limitsi sum limitsjaijxixj


然后,让我们看看这个双重和关于变量的偏导数是什么 xi
\开始{align *}
\ frac {\部分(x ^ \ top A x)} {\部分x_i}
&= \ frac {\部分} {\部分x_i} \左(\ sum \极限_ {k_1} \ sum \极限_ {k_2} a_ {k_1 k_2} x_ {k_1} x_ {k_2} \右)= \\
&= \ frac {\部分} {\部分x_i} \左(
\ underbrace {\ sum \ limit_ {k_1 \ neq i} \ sum \ limits_ {k_2 \ neq i} a_ {ik_2} x_ {k_1} x_ {k_2}} _ _ {k_1 \ neq i,k_2 \ neq i} + \底线{\ sum \ limits_ {k_2 \ neq i} a_ {ik_2} x_i x_ {k_2}} _ {k_1 = i,k_2 \ neq i} +
\ underbrace {\ sum \ limits_ {k_1 \ neq i} a_ {k_1 i} x_ {k_1} x_i} _ {k_1 \ neq i,k_2 = i} +
\ underbrace {a_ {ii} x_i ^ 2} _ {k_1 = i,k_2 = i} \ right)= \\
&= \ sum \ limits_ {k_2 \ neq i} a_ {ik_2} x_ {k_2} + \ sum \ limits_ {k_1 \ neq i} a_ {k_1 i} x_ {k_1} + 2 a_ {ii} x_i = \\
&= \ sum \ limits_ {k_2} a_ {ik_2} x_ {k_2} + \ sum \ limits_ {k_1} a_ {k_1 i} x_ {k_1} = \\
&= \ sum \ limits_ {j}(a_ {ij} + a_ {ji})x_j \\
\结束{align *}
我将双和分为四个案例,以大括号突出显示。 这四种情况均微不足道。 它仍然要做最后的动作,收集梯度向量中的偏导数:

 nablax topAx=\开bmatrix frac\部x topAx\部x1 vdots frac\部x\顶Ax\部xn\结bmatrix=\开bmatrix\和 limitja1j+aj1xj vdots sum limitsjanj+ajnxj endbmatrix=A+A topx



二次函数的最小值和线性系统的解


让我们不要忘记旅行的方向。 我们看到一个正数 对于一个变量的实函数,求解方程 ax=b 或最小化二次函数  mathrmargminxax22bx 是一回事。

我们想在对称正定矩阵的情况下显示相应的连接
所以我们想找到二次函数的最小值

 mathrmargminx in mathbbRnx topAx2b topx


就像在学校一样,我们将导数等于零:

 nablax topAx2b topx=\开bmatrix0 vdots0 endbmatrix


Hamilton算子是线性的,因此我们可以按以下形式重写方程式:

 nablax topAx2 nablab topx=\开bmatrix0 vdots0 endbmatrix


现在我们应用第二和第三阶微分定理:

A+A topx2b=\开bmatrix0 vdots0\结bmatrix


回想一下 对称,并将方程分为两部分,我们得到所需的线性方程组:

=b


Hooray从一个变量变为多个变量,我们什么都没有丢失,并且我们可以有效地最小化二次函数!

我们传递到最小二乘


最后,我们可以继续本讲座的主要内容。 想象一下,我们在飞机上有两个点 x1y1x2y2 ,我们想找到穿过这两个点的直线方程。 这条线的方程可以写成 y= alphax+ beta ,也就是说,我们的任务是找到系数  alpha beta 。 此练习是针对该学校的七年级学生,我们写下了方程组:

\左\ {\开始{拆分} \ alpha x_1 + \ beta&= y_1 \\ \ alpha x_2 + \ beta&= y_2 \\ \结束{split} \ right。



我们有两个带有两个未知数的方程(  alpha beta ),其余的都是已知的。

在一般情况下,解决方案是存在的并且是唯一的。 为了方便起见,我们以矩阵形式重写相同的系统:

 underbrace beginbmatrixx11x21 endbmatrix=A underbrace beginbmatrix alpha beta endbmatrix=x=\底线\开bmatrixy1y2\结bmatrix=b



我们得到一个类型的方程 =b 琐碎地解决了 x=A1b

现在想象一下,我们有三个点需要画一条直线:

\左\ {\开始{拆分} \ alpha x_1 + \ beta&= y_1 \\ \ alpha x_2 + \ beta&= y_2 \\ \ alpha x_3 + \ beta&= y_3 \\ \结束{split} \ right 。



该系统以矩阵形式编写如下:

 underbrace beginbmatrixx11x21x31 endbmatrix=A\,3 2 underbrace beginbmatrix alpha beta endbmatrix=x\,2 times1=\底线\开bmatrixy1y2y3 endbmatrix=b\,3\乘1


现在我们有了一个矩阵 矩形,它根本不存在反向! 这是完全正常的,因为我们只有两个变量和三个方程,在一般情况下,该系统没有解。 这是现实世界中的一种完全正常的情况,当这些点是来自噪声测量的数据时,我们需要找到参数  alpha beta 最接近的测量数据。 在第一个演讲中,当我们对杆秤进行校准时,我们已经考虑了这个示例。 但是那时我们的解决方案纯粹是代数的,非常麻烦。 让我们尝试一种更直观的方法。

我们的系统可以编写如下:

 alpha underbrace\开bmatrixx1x2x3 endbmatrix= veci+ beta underbrace\开bmatrix11  1\结bmatrix= vecj=\开bmatrixy1y2y3\结bmatrix


或者,更简单地说,

 alpha veci+ beta vecj= vecb


向量  vec vecj vecb 已知,需要找到标量  alpha beta
显然是线性组合  alpha veci+ beta vecj 产生一架飞机,如果向量  vecb 不在这个平面上,那么就没有确切的解决方案; 但是,我们正在寻找一种近似的解决方案。

让我们将决策错误定义为  vece alpha beta= alpha veci+ beta vecj vecb 。 我们的目标是找到这样的参数值  alpha beta 最小化向量的长度  vece alpha beta 。 换句话说,问题写为  mathrmargmin alpha beta | vece alpha beta |
这是一个例证:



给定向量  vec vecj vecb ,我们尝试最小化误差向量的长度  vece 。 显然,如果它的长度垂直于在矢量上拉伸的平面,则其长度将最小化  vec vecj

但是,等等,最小的平方在哪里? 现在他们会的。 根提取功能  sqrt cdot 因此单调  mathrmargmin alpha beta | vece alpha beta | =  mathrmargmin alpha beta | vece alpha beta |2

很明显,如果误差向量的长度垂直于在向量上延伸的平面,则它的长度将最小化  vec vecj ,可以通过将对应的标量乘积等于零来编写:

\左\ {\开始{分割} \ vec {i} ^ \ top \ vec {e}(\ alpha,\ beta)&= 0 \\ \ vec {j} ^ \ top \ vec {e}(\ alpha,\ beta)&= 0 \ end {split} \ right。



以矩阵形式,该同一系统可以写成

\开bmatrixx1x2x3111\结bmatrix\向 alpha\开bmatrixx1x2x3\结bmatrix+ beta\开bmatrix111\结bmatrix\开bmatrixy1y2y3 endbmatrix right=\开bmatrix00 endbmatrix


否则

\开bmatrixx1x2x3111\结bmatrix\向\开bmatrixx11x21x31\结bmatrix\开bmatrix alpha beta\结bmatrix\开bmatrixy1y2y3\结bmatrix right=\开bmatrix00\结bmatrix


但是我们不会止步于此,因为可以进一步减少记录:

A topAxb=\开bmatrix00 endbmatrix


最近的转换:

A topAx=A topb


让我们计算尺寸。 矩阵 有大小 3\乘2 因此 A topA 有大小 2\乘2 。 矩阵 b 有大小 3\乘1 但是矢量 A topb 有大小 2\乘1 。 也就是说,在一般情况下,矩阵 A topA 可逆的! 而且, A topA 它具有几个不错的属性。

定理4


A topA 对称的。

这一点一点都不难显示:

A topA top=A topA top top=A topA.



定理5


A topA 正半定:  forallx in mathbbRn quadx topA topAx geq0

这是由于以下事实: x topA topAx=Ax topAx>0

A topA 正定如果 具有线性独立的列(排名 等于系统中的变量数)。

总之,对于具有两个未知数的系统,我们证明了二次函数的最小化  mathrmargmin alpha beta | vece alpha beta |2 等效于求解线性方程组 A topAx=A topb 。 当然,所有这些推理都适用于任何其他数量的变量,但让我们使用简单的代数计算将所有内容再次紧凑地写在一起。 我们从最小二乘问题开始,对熟悉形式的二次函数进行最小化,然后得出结论,这等效于求解线性方程组。 因此,我们开始:

\开始{拆分} \ Mathrm {argmin} \ | Ax-b \ | ^ 2&= \ mathrm {argmin}(Ax-b)^ \ top(Ax-b)= \\&= \ mathrm {argmin}(x ^ \ top A ^ \ top-b ^ \ top)(Ax-b)= \\&= \ mathrm {argmin}(x ^ \ top A ^ \ top A x-b ^ \ top Ax-x ^ \ top A ^ \ top b + \ underbrace {b ^ \ top b} _ {\ rm const})= \\&= \ mathrm {argmin}(x ^ \ top A ^ \ top A x-2b ^ \ top Ax)= \\&= \ mathrm {argmin}( x ^ \ top \ underbrace {(A ^ \ top A)} _ {:= A'} x-2 \ underbrace {{A ^ \ top b)} _ {:= b'} \ phantom {} ^ \ top x)\结束{拆分}


所以最小二乘问题  mathrmargmin |b |2 等效于最小化二次函数  ħ ř 一个ř Ñ X ö p X - 2 b  ö p X (一般情况下)有一个对称的正定矩阵 A ' ,又相当于求解线性方程组 ' X = b ' 。 ff 理论已经结束。

让我们继续练习


示例一,一维


让我们回想一下上一篇文章中的一个示例:

 # initialize the data array x = [0.] * 32 x[0] = x[31] = 1. x[18] = 2. # smooth the array values other than at indices 0,18,31 for iter in range(1000): for i in range(len(x)): if i in [0,18,31]: continue x[i] = (x[i-1] + x[i+1])/2. 

我们有32个元素的常规x数组,其初始化如下:



然后,我们将执行以下过程一千次:对于每个单元x [i],我们将邻近单元的平均值写入其中:x [i] =(x [i-1] + x [i + 1])/ 2。 唯一的例外:我们不会用数字0、18和31重写数组的元素。这就是数组在前一百五十次迭代中的样子:



正如我们在前一篇文章中发现的那样 ,事实证明,我们的四行python代码宏代码通过Gauss-Seidel方法解决了以下线性方程组:



他们发现了,但是这个系统是从哪里来的呢? 什么样的魔术? 让我们离题,尝试解决稍微不同的系统。 我仍然有32个变量,但是现在我希望它们全都相等。 写下这个并不难,写下一个方程式系统就足够了,该方程式指出所有相邻元素成对相等:



上面的Python代码原则上不会触及三个变量的值:x0,x18,x31,因此让我们将它们传递到方程组的右侧,即从未知数集中排除:



我固定初始值x0 = 1,第一个方程式说x1 = x0 = 1,第二个方程式说x2 = x1 = x0 = 1依此类推,直到我们得到方程式1 = x0 = x17 = x18 = 2哦 但是这个系统没有解决方案:((

不管,我们是程序员! 让我们叫最小平方来寻求帮助! 我们无法解决的系统可以用矩阵形式编写 = b ; 大概解决我们的系统,就足以解决系统 X = b然后事实证明,这正是上一篇文章中的一对一方程组!

凭直觉,可以想象创建系统矩阵的过程,如下所示:我们通过弹簧连接要实现其相等性的值。例如,在我们的情况下,我们想要X 是等于x i + 1,所以我们加上等式x i - x i + 1 = 0,在这些值之间悬挂一个弹簧,将它们拉在一起。但是,由于x0,x18和x31的值是固定不变的,因此除了均匀拉伸弹簧之外,没有其他任何自由值,从而产生(在这种情况下)分段线性解决方案。让我们编写一个解决该方程组的程序。在上一篇文章中,我们看到Gauss-Seidel方法的编程非常简单,但是收敛速度很慢,因此最好使用线性方程组的实数求解器。在最简单的情况下,对于我们的一维示例,代码可能看起来像这样:



 import numpy as np n = 32 # size of the vector to produce # fill the matrix with 2nd order finite differences A = np.matrix(np.zeros((n-1, n))) for i in range(0,n-1): A[i, i] = -1. A[i, i+1] = 1. # eliminate the constrained variables from the matrix A = A[:,[*range(1,18)] + [*range(19,31)]] b = np.matrix(np.zeros((n-1, 1))) b[0,0] = 1. b[17,0] = -2. b[18,0] = 2. b[n-2,0] = -1. # solve the system x = np.linalg.inv(A.transpose()*A)*A.transpose()*b x = x.transpose().tolist()[0] # re-introduce the constrained variables x.insert(0, 1.) x.insert(18, 2.) x.append(1.) 

该代码产生一个x的32个元素的列表,其中存储了解决方案。我们正在建立一个矩阵 建筑矢量 b,我们得到一个带有解的向量X = - 1 b,然后插入到该载体中提供了固定的值x0 = 1,X18 = X31 = 2和1。这段代码可以完成必要的工作,但是将固定变量的值传递到右侧是非常不愉快的。有可能作弊吗?可以!让我们看下面的代码:



 import numpy as np n = 32 # size of the vector to produce # fill the matrix with 2nd order finite differences A = np.matrix(np.zeros((n-1+3, n))) for i in range(0,n-1): A[i, i] = -1. A[i, i+1] = 1. # enforce the constraints through the quadratic penalty A[n-1+0, 0] = 100. A[n-1+1,18] = 100. A[n-1+2,31] = 100. b = np.matrix(np.zeros((n-1+3, 1))) b[n-1+0,0] = 100.*1. b[n-1+1,0] = 100.*2. b[n-1+2,0] = 100.*1. # solve the system x = np.linalg.inv(A.transpose()*A)*A.transpose()*b x = x.transpose().tolist()[0] 

从程序员的角度来看,这要好得多,可以立即获得所需尺寸的向量x,并且可以更轻松地填充矩阵A和b。但是,诀窍是什么?让我们来写一个求解的方程组:



仔细查看最后三个方程:
100 x0 = 100 *
1100 x18 = 100 * 2
100 x31 = 100 * 1

编写它们的方法是否容易如下?
x0 = 1
x18 = 2
x31 = 1

不,不容易。正如我已经说过的,每个方程式都悬挂着弹簧,在这种情况下,x0和值1之间的弹簧,x18和值2之间的弹簧,最后,变量x31也将被吸引。

但是存在100的系数是有原因的。我们记得这个系统只是无法求解,我们从最小二乘的意义上解决了它,从而最小化了相应的二次函数。将最后三个方程式的每个系数乘以100,我们引入了将x0,x18和x32偏离期望值的惩罚,超过了偏离等式的惩罚的一万倍(100 ^ 2)x i = x i + 1粗略地讲,后三个方程的弹簧比其他所有方程都难上万倍。这种二次惩罚方法是引入限制的非常方便的工具;比减少一组变量要简单得多(尽管并非没有缺点)。

实例二,三维


让我们继续进行一些比平滑一维数组的元素更有趣的事情!首先,我们将做完全一样的工作,但要使用更多有趣的数据和实际工具。我想获取一个三维曲面,固定其边界,并尽可能使曲面的其余部分平滑:此处提供了



代码,以防万一我将给出主文件的完整列表:

 #include <vector> #include <iostream> #include "OpenNL_psm.h" #include "geometry.h" #include "model.h" int main(int argc, char** argv) { if (argc<2) { std::cerr << "Usage: " << argv[0] << " obj/model.obj" << std::endl; return 1; } Model m(argv[1]); for (int d=0; d<3; d++) { // solve for x, y and z separately nlNewContext(); nlSolverParameteri(NL_NB_VARIABLES, m.nverts()); nlSolverParameteri(NL_LEAST_SQUARES, NL_TRUE); nlBegin(NL_SYSTEM); nlBegin(NL_MATRIX); for (int i=0; i<m.nhalfedges(); i++) { // fix the boundary vertices if (m.opp(i)!=-1) continue; int v = m.from(i); nlRowScaling(100.); nlBegin(NL_ROW); nlCoefficient(v, 1); nlRightHandSide(m.point(v)[d]); nlEnd(NL_ROW); } for (int i=0; i<m.nhalfedges(); i++) { // smooth the interior if (m.opp(i)==-1) continue; int v1 = m.from(i); int v2 = m.to(i); nlRowScaling(1.); nlBegin(NL_ROW); nlCoefficient(v1, 1); nlCoefficient(v2, -1); nlEnd(NL_ROW); } nlEnd(NL_MATRIX); nlEnd(NL_SYSTEM); nlSolve(); for (int i=0; i<m.nverts(); i++) { m.point(i)[d] = nlGetVariable(i); } } std::cout << m; return 0; } 

我编写了wavefront .obj文件的最简单的解析器,因此该模型在一行中加载。作为求解器,我将使用OpenNL,它是解决稀疏矩阵最小二乘问题的强大求解器。请注意,矩阵的尺寸可能是巨大的:例如,如果我们要获取1000 x 1000像素的黑白图片,则矩阵一个一个我们将有一百万的大小,以一百万!但是,实际上,此矩阵的绝大多数元素通常都是零,因此一切并不是那么可怕,但是您仍然需要使用特殊的求解器。因此,让我们看一下代码。首先,我向求解器宣布我要执行的操作:



  nlNewContext(); nlSolverParameteri(NL_NB_VARIABLES, m.nverts()); nlSolverParameteri(NL_LEAST_SQUARES, NL_TRUE); nlBegin(NL_SYSTEM); nlBegin(NL_MATRIX); 

我想要一个矩阵 大小(科尔维尔顶点)×(科尔维尔顶点)。注意,我们给求解器一个矩阵一个一个它会自身建设,这是非常方便!另外,请注意以下事实:我不是求解一个方程组,而是针对x,y和z串联求解三个方程组(我认为问题是三维的,但仍然是一维的!),然后逐行声明矩阵。首先,我修复曲面边缘上的顶点:



  for (int i=0; i<m.nhalfedges(); i++) { // fix the boundary vertices if (m.opp(i)!=-1) continue; int v = m.from(i); nlRowScaling(100.); nlBegin(NL_ROW); nlCoefficient(v, 1); nlRightHandSide(m.point(v)[d]); nlEnd(NL_ROW); } 

您可以看到我使用100 ^ 2的平方惩罚来修复边顶点。

好吧,然后为每对相邻的顶点在它们之间悬挂一个刚度为1的弹簧:

  for (int i=0; i<m.nhalfedges(); i++) { // smooth the interior if (m.opp(i)==-1) continue; int v1 = m.from(i); int v2 = m.to(i); nlRowScaling(1.); nlBegin(NL_ROW); nlCoefficient(v1, 1); nlCoefficient(v2, -1); nlEnd(NL_ROW); } 

我们执行代码,看看如何在弯曲的线上将表面平滑到肥皂膜的表面。我们刚刚解决了Dirichlet问题,获得了面积最小的表面。

我们提升特色


让我们将自己的脸变成一个怪诞的面具:



在上一篇文章中,我已经展示了如何在膝盖上执行此操作,在这里,我给出了一个使用最小二乘法求解器的真实代码:

  for (int d=0; d<3; d++) { // solve for x, y and z separately nlNewContext(); nlSolverParameteri(NL_NB_VARIABLES, m.nverts()); nlSolverParameteri(NL_LEAST_SQUARES, NL_TRUE); nlBegin(NL_SYSTEM); nlBegin(NL_MATRIX); for (int i=0; i<m.nhalfedges(); i++) { // fix the boundary vertices if (m.opp(i)!=-1) continue; int v = m.from(i); nlRowScaling(100.); nlBegin(NL_ROW); nlCoefficient(v, 1); nlRightHandSide(m.point(v)[d]); nlEnd(NL_ROW); } for (int i=0; i<m.nhalfedges(); i++) { // light attachment to the original geometry if (m.opp(i)==-1) continue; int v1 = m.from(i); int v2 = m.to(i); nlRowScaling(.2); nlBegin(NL_ROW); nlCoefficient(v1, 1); nlCoefficient(v2, -1); nlRightHandSide(m.point(v1)[d] - m.point(v2)[d]); nlEnd(NL_ROW); } for (int v=0; v<m.nverts(); v++) { // amplify the curvature nlRowScaling(1.); nlBegin(NL_ROW); int nneigh = m.incident_halfedges(v).size(); nlCoefficient(v, nneigh); Vec3f curvature = m.point(v)*nneigh; for (int hid : m.incident_halfedges(v)) { nlCoefficient(m.to(hid), -1); curvature = curvature - m.point(m.to(hid)); } nlRightHandSide(2.5*curvature[d]); nlEnd(NL_ROW); } nlEnd(NL_MATRIX); nlEnd(NL_SYSTEM); nlSolve(); for (int i=0; i<m.nverts(); i++) { m.point(i)[d] = nlGetVariable(i); } } 

完整的代码在这里他如何工作?与前面的示例一样,我从固定边顶点开始。然后,对于每条边缘我(安静地,系数为0.2)说,如果它的形状与原始曲面完全相同,那就太好了:

  for (int i=0; i<m.nhalfedges(); i++) { // light attachment to the original geometry if (m.opp(i)==-1) continue; int v1 = m.from(i); int v2 = m.to(i); nlRowScaling(.2); nlBegin(NL_ROW); nlCoefficient(v1, 1); nlCoefficient(v2, -1); nlRightHandSide(m.point(v1)[d] - m.point(v2)[d]); nlEnd(NL_ROW); } 

如果您什么都不做,并按原样解决问题,那么我们应该确切地了解输出的输出内容:输出边界等于输入边界,加上输出的差异等于输入的差异。

但是我们不会止步于此!

  for (int v=0; v<m.nverts(); v++) { // amplify the curvature nlRowScaling(1.); nlBegin(NL_ROW); int nneigh = m.incident_halfedges(v).size(); nlCoefficient(v, nneigh); Vec3f curvature = m.point(v)*nneigh; for (int hid : m.incident_halfedges(v)) { nlCoefficient(m.to(hid), -1); curvature = curvature - m.point(m.to(hid)); } nlRightHandSide(2.5*curvature[d]); nlEnd(NL_ROW); } 

我在系统中添加了更多方程式。对于每个顶点,我计算输入曲面中围绕它的曲面的曲率,然后将其乘以两个半,即输出曲面到处都应具有较大的曲率。

然后我们调用求解器并获得良好的讽刺画。

今天的最后一个例子


到目前为止,我们还没有看到一个新的示例,所有内容已在上一篇文章中显示。让我们尝试做一些比最简单的平滑更简单的事情,而无需使用任何求解器就可以很容易地获得它。该代码在此处可用,但让我给您列出:

 #include <vector> #include <iostream> #include "OpenNL_psm.h" #include "geometry.h" #include "model.h" const Vec3f axes[] = {Vec3f(1,0,0), Vec3f(-1,0,0), Vec3f(0,1,0), Vec3f(0,-1,0), Vec3f(0,0,1), Vec3f(0,0,-1)}; int snap(Vec3f n) { // returns the coordinate axis closest to the given normal double nmin = -2.0; int imin = -1; for (int i=0; i<6; i++) { double t = n*axes[i]; if (t>nmin) { nmin = t; imin = i; } } return imin; } int main(int argc, char** argv) { if (argc<2) { std::cerr << "Usage: " << argv[0] << " obj/model.obj" << std::endl; return 1; } Model m(argv[1]); std::vector<int> nearest_axis(m.nfaces()); for (int i=0; i<m.nfaces(); i++) { Vec3f v[3]; for (int j=0; j<3; j++) v[j] = m.point(m.vert(i, j)); Vec3f n = cross(v[1]-v[0], v[2]-v[0]).normalize(); nearest_axis[i] = snap(n); } for (int d=0; d<3; d++) { // solve for x, y and z separately nlNewContext(); nlSolverParameteri(NL_NB_VARIABLES, m.nverts()); nlSolverParameteri(NL_LEAST_SQUARES, NL_TRUE); nlBegin(NL_SYSTEM); nlBegin(NL_MATRIX); for (int i=0; i<m.nhalfedges(); i++) { int v1 = m.from(i); int v2 = m.to(i); nlRowScaling(1.); nlBegin(NL_ROW); nlCoefficient(v1, 1); nlCoefficient(v2, -1); nlRightHandSide(m.point(v1)[d] - m.point(v2)[d]); nlEnd(NL_ROW); int axis = nearest_axis[i/3]/2; if (d!=axis) continue; nlRowScaling(2.); nlBegin(NL_ROW); nlCoefficient(v1, 1); nlCoefficient(v2, -1); nlEnd(NL_ROW); } nlEnd(NL_MATRIX); nlEnd(NL_SYSTEM); nlSolve(); for (int i=0; i<m.nverts(); i++) { m.point(i)[d] = nlGetVariable(i); } } std::cout << m; return 0; } 

我们仍然在与以前一样的表面上进行工作;我们为每个三角形调用snap()函数。该函数表示坐标系的哪个轴最接近该三角形的法线。也就是说,我们想知道这个三角形更可能是垂直或水平的。好吧,那么我们要更改表面的几何形状,以使靠近水平面的东西变得更近!该图的左侧显示了snap()函数的结果:



因此,我们用三种颜色绘制了曲面,分别对应于坐标系的三个轴。好吧,那么对于我们系统的每个边缘,我们添加以下方程式:

  nlRowScaling(1.); nlBegin(NL_ROW); nlCoefficient(v1, 1); nlCoefficient(v2, -1); nlRightHandSide(m.point(v1)[d] - m.point(v2)[d]); nlEnd(NL_ROW); 

也就是说,我们指示输出表面的每个边缘都类似于输入表面的相应边缘,即弹簧1的刚度。然后,如果我们允许例如x尺寸,并且该边缘应垂直,那么我们简单地说一个顶点等于另一个顶点,并具有弹簧的刚度。 2:

  nlRowScaling(2.); nlBegin(NL_ROW); nlCoefficient(v1, 1); nlCoefficient(v2, -1); nlEnd(NL_ROW); 

好吧,这是我们程序的结果:一个



完全合理的问题:复活节岛的偶像当然很酷,但是地质与它有什么关系?是否有实际问题的例子?

当然可以让我们看一看地壳的一部分:



对于地质学家来说,不同岩石的层非常重要,它们之间的边界(水平)以绿色显示,而地质断层则以红色显示。我们在输入处有我们感兴趣的区域的三角形网格(3D中的四面体),但是需要四边形(3D中的六面体)对某些过程进行建模。我们对模型进行变形,以使断层变为垂直而水平(惊奇)变为水平:



然后,我们只取一个规则的正方形网格,将我们的域切成均匀的正方形,并对它们应用逆变换,以获得必要的四边形网格。

结论


最小二乘法是一种强大的数据处理工具,与Excel列中的统计信息相比,它们的使用范围广泛。由于将二次函数最小化和线性方程组求解是相同的事实,这些可能性为我们打开了大门,而且我们(人类)已经学会了非常非常好地求解线性方程组,甚至完全解决了具有数十亿个变量的非人类尺寸。

但是,有时我们可能没有足够的线性模型。例如,在这里,神经网络可以为您提供帮助。在下一篇文章中,我将尝试证明拒绝排放的OLS等效于神经网络的公式之一。保持在线!

Source: https://habr.com/ru/post/zh-CN477192/


All Articles