用迭代计算平方根

求某个正数

x的平方根,
相当于求解方程

f(u)=u2−x=0。
利用一阶泰勒展开式

f(u)=f(u0)+f′(u0)(u−u0)+o(u−u0),
其中

f′(u)=2u,

f(u)=0得

u≈u0−f(u0)f′(u0),

将其作为迭代公式

un=un−1−f(un−1)f′(un−1)=un−1−un−12−x2un−1=12(un−1+xun−1),n=1,2,…

给定一个初始值

u0,
迭代直到

|un−un−1|<ϵ为止,

ϵ是预先给定的精度如

10−6。

程序如下:

function mysqrt(x, eps=1E-6)
    u = 1.0
    u1 = 0.0
    while abs(u - u1) >= eps
        u1 = u
        u = 0.5*(u + x/u)
    end
    return u
end
mysqrt(2)
## 1.414213562373095

圆周率

割圆术

公元3世纪北魏时期科学家刘徽在《九章算术注》中记载了割圆术估计圆周率的方法。

北京大学Julia语言入门讲义第21章: Julia编程示例–科学计算问题

很容易算出内接6六边形等低阶内接正

n边形的弦边长和面积。
比如,
设圆半径为1,
则内接正六边形可等分为6个正三角形,
其弦边的长为1,
面积为

24,
六边形面积为

322≈2.12。

设内接正

n边形的弦边(靠近圆周的边)长为

ln,
面积为

Sn,
可以分解为

n个等腰三角形,
腰长为1, 底边长为

ln。
如图中的三角形OAB。
将这个三角形的顶角等分,
设与圆周的交点为C,
则三角形OAC构成正

2n边形的一部分。
如此细分令

n→∞则

12nln→π,

Sn→π。

如何递推计算

l2n和

S2n?
设角平分线OC和弦AB的交点为G,
设CG=

x, 则OG=

1−x,
AG =

ln2,
AC =

l2n,
于是由勾股定理得

l2n2=(ln2)2+x2,1=(ln2)2+(1−x)2.

从第二个方程解出

x,代入第一个方程,得

l2n=(1−1−(ln2)2)2+(ln2)2.

递推过程仅需要使用开平方根运算,
所以在古代的研究条件下是可以手工计算的。

ln,
可知三角形OAB面积为

12ln1−(ln2)2,
从而正

n边形面积为

nln21−(ln2)2.

可以看出,

n→∞时

ln→0,
上述

l2n的递推公式出现相近数相减,
误差较大。
为避免相近数相减的误差,将

l2n公式转换为

l2n=ln2+4+ln2.

using Printf
area(s, n) = n*s/2*sqrt(1-s^2/4)
side_next(s) = s / sqrt(2 + sqrt(4 - s^2))
function pi_secant(niter = 10)
    n = 6
    s = big(1.0)
    ns = [n]
    sides = [s]
    for i=1:niter
        n *= 2
        s = side_next(s)
        push!(ns, n)
        push!(sides, s)
    end
    pi_vec_side = [0.5*n*s for (n,s) in zip(ns, sides)]
    pi_vec_area = [area(big(s), n) for (n,s) in zip(ns, sides)]
    println(" d         n        side         pi_side         pi_area")
    for i=1:(niter+1)
        println( @sprintf("%2d", i-1), 
        @sprintf("%10d", ns[i]),
        @sprintf("%12.8f", sides[i]),
        @sprintf("%16.12f", pi_vec_side[i]),
        @sprintf("%16.12f", pi_vec_area[i]))
    end
    [0:niter ns sides pi_vec_side pi_vec_area]
end
pi_secant(20);
 d         n        side         pi_side         pi_area
 0         6  1.00000000  3.000000000000  2.598076211353
 1        12  0.51763809  3.105828541230  3.000000000000
 2        24  0.26105238  3.132628613281  3.105828541230
 3        48  0.13080626  3.139350203047  3.132628613281
 4        96  0.06543817  3.141031950891  3.139350203047
 5       192  0.03272346  3.141452472285  3.141031950891
 6       384  0.01636228  3.141557607912  3.141452472285
 7       768  0.00818121  3.141583892148  3.141557607912
 8      1536  0.00409061  3.141590463228  3.141583892148
 9      3072  0.00204531  3.141592105999  3.141590463228
10      6144  0.00102265  3.141592516692  3.141592105999
11     12288  0.00051133  3.141592619365  3.141592516692
12     24576  0.00025566  3.141592645034  3.141592619365
13     49152  0.00012783  3.141592651451  3.141592645034
14     98304  0.00006392  3.141592653055  3.141592651451
15    196608  0.00003196  3.141592653456  3.141592653055
16    393216  0.00001598  3.141592653556  3.141592653456
17    786432  0.00000799  3.141592653581  3.141592653556
18   1572864  0.00000399  3.141592653588  3.141592653581
19   3145728  0.00000200  3.141592653589  3.141592653588
20   6291456  0.00000100  3.141592653590  3.141592653589

结果中最后的边数已经达到六百万以上,
用面积的方法精确到了第12位有效数字。
21.5用递推(滤波)的方法从面积序列进行了改进,
大大提高了精度。

用有理数近似π

π是一个无理数,
大家常用的近似值是小数表示的

3.1415926,
精度是小数点后7位小数,
误差为

5.4×10−8。

历史上多位数学家研究过用有理数近似圆周率。
我们在已知圆周率比较精确的近似值的基础上找到最优的有理数逼近。

function pi_rational(n=2)
    dens = 10^(n-1):10^n-1
    nums = round.(Int, pi .* dens)
    pirs = nums ./ dens 
    errs = abs.(pirs .- pi)
    i = argmin(errs)
    pir = pirs[i]

    # 返回近似有理数,近似实数,误差绝对值
    return nums[i]//dens[i], pir, errs[i]
end
pi_rational(2)
# (22//7, 3.142857142857143, 0.0012644892673496777)
pi_rational(2)
# (311//99, 3.1414141414141414, 0.00017851217565167943)
pi_rational(3)
# (355//113, 3.1415929203539825, 2.667641894049666e-7)
pi_rational(4)
# (355//113, 3.1415929203539825, 2.667641894049666e-7)
pi_rational(5)
(312689//99532, 3.1415926536189365, 2.914335439641036e-11)

其中分母为1位数的近似值

22/7是祖冲之得到的“约率”近似,
有小数点后2位精度;
分母为3位数的近似值

355/113是很好的近似值,
精确到了小数点后第6位,
这正是祖冲之得到的“密率”近似,
分母更多位数的近似已经是分母为5位数的

312689/99532,
其精度达到小数点后9位。

用级数计算π

此问题来自Ben Lauwens和Allen B. Downey(2019)
Julia语言编程入门》,中国电力出版社,
肖斌、王磊等译。

数学家Srinivasa Ramanujan提出了如下的计算

1π的级数:

1π=229801∑k=0∞(4k)!(1103+26390k)(k!)43964k

k = 0
x = 2*sqrt(2)/9801
y = 1103
z = x*y
s = z
while z > 1E-15
    k += 1
    k4 = 4*k
    x *= k4*(k4-1)*(k4-2)*(k4-3)/k^4/396^4
    y += 26390
    z = x*y
    s += z
end
println("k=", k, " estimate = ", s)
println("Error = ", s - 1/π)
## k=2 estimate = 0.3183098861837907
## Error = 0.0

只算了3项就达到了

10−15的精度。

卷积

离散卷积

Z表示整数集

{…,−2,−1,0,1,2,…},

x={xn,n∈Z}是一个数列。

x,y是两个数列,定义数列

z为

zn=∑k=−∞∞xkyn−k, n∈Z,

z={xn,n∈Z}为

x和

y的离散卷积,记作

x∗y。

交换律

x∗y=y∗x.

证明

(x∗y)n=∑kxkyn−k(令s=n−k, k=n−s)=∑sxn−sys=(y∗x)n.

结合律

(x∗y)∗z=x∗(y∗z).

证明

v=x∗y,

w=y∗z。

vn=(x∗y)n=∑kxkyn−k,wn=(y∗z)n=(z∗y)n=∑szsyn−s,

[(x∗y)∗z]m=[z∗(x∗y)]m=∑szsvm−s=∑szs∑kxkym−s−k=∑s∑kzsxkym−s−k,

[x∗(y∗z)]m=∑kxkwm−k=∑kxk∑szsym−k−s=∑k∑szsxkym−s−k.

在一定收敛性条件下(如上面的二重级数绝对收敛)结合律成立。

分配律

(x+y)∗z=x∗z+y∗z;x∗(y+z)=x∗y+x∗z.

证明

[(x+y)∗z]n=∑k(x+y)kzn−k=∑k(xk+yk)zn−k=∑kxkzn−k+∑kykzn−k=(x∗z)n+(y∗z)n,

第二式由交换律可得。

有限离散卷积

x仅有

A个元素,
记为

xn,n=0,1,…,A−1;

y仅有

B个元素,
记为

yn,n=0,1,…,B−1。

xn=0当

n<0或

n>A−1,

yn类似,
则可定义

zn=(x∗y)n, n∈Z.

显然,这样的卷积仍服从交换律、结合律、分配律。

zn=∑k=−∞∞xkyn−k,

为使得

xkyn−k≠0,
必须有

0≤k≤A−1, 0≤n−k≤B−1,

这等价于

0≤k≤A−1, n+1−B≤k≤n,

于是

zn=∑k=max(0,n+1−B)min(A−1,n)xkyn−k,

为使得

zn≠0必须有

max(0,n+1−B)≤min(A−1,n),

这等价于

0≤n≤A+B−2.

可见结果的非零元素最多有

A+B−1个。

在Julia中,
x的下标用1:A代替,
y的下标用1:B代替,
x*y的下标用1:(A+B-1)代替。

两个向量卷积的Julia程序:

function convolve_zero(x, y)
    A = length(x)
    B = length(y)
    z = similar(x, (A+B-1,))
    z .= 0
    i0 = firstindex(x)
    j0 = firstindex(y)
    for i in eachindex(x)
        for j in eachindex(y)
            z[1 + (i-i0) + (j-j0)] += x[i] * y[j]
        end
    end
    
    return z
end # function
convolve_zero([1, -1], [2, 3, 5, 11]) |> show
## [2, 1, 2, 6, -11]

滤波与推移算子

下标非有限情形

{aj,j∈Z}满足

∑j=−∞∞|aj|<∞,
称这个条件为

{aj}∈l1。

{xt,t∈Z}有界,
定义

yt=∑j=−∞∞ajxt−j=(a∗x)t, t∈Z,

{aj}为一个保时线性滤波器,

{yt}为

{xt}的滤波。
实际上是

a和

x的离散卷积。

C为复数域,
其元素

z为复平面上的点。
定义

P(z)=∑j=−∞∞ajzj,

则存在

0<ρ1<1<ρ2使得

P(z)在

ρ1<|z|<ρ2的

z∈C子集上收敛,
还是解析函数。

定义

Lxt=xt−1,

L为推移算子
定义

P(L)=∑j=−∞∞ajLj,yt=P(L)xt=∑j=−∞∞ajxt−j,

P(L)为推移算子多项式。

关于推移算子多项式,
有如下结论。
这也是下标取所有整数

Z情况下的离散卷积的性质。

性质1(交换律):对

{aj}∈l1,

{bj}∈l1,

P(z)=∑ajzj,

Q(z)=∑jbjzj,

U(z)=P(z)Q(z):

  1. c=a∗b, 则

    U(z)=∑j=−∞∞cjzj.

  2. {xt}有

    P(L)(Q(L)xt)=Q(L)(P(L)xt)=U(L)xt.

证明略。

性质2(线性):若

P(L),Q(L)为推移算子多项式,

a,b为实数或复数,

aP(L)+bQ(L)也是推移算子多项式。

性质3(分配律):设

P,Q,U是推移算子多项式,

P(Q+U)=PQ+PU,

(P+Q)U=PU+QU。

关于推移算子的性质,参见:

  • 何书元(2003) 《应用时间序列分析》, 北京大学出版社
  • 李东风的时间序列分析讲义

上面的做法将有限的离散卷积看成是无穷长数列卷积的特例。
在作为滤波计算时,

a0,a1,…,am−1看成滤波器,

x0,x1,…,xn−1看成信号,

m<n,
为了不使用

t<0和

t>n−1时的信号作为输入,
可定义滤波结果

yt=∑j=0m−1ajxt−j, t=m−1,m,…,n−1,

仅输出这

n−m+1个元素。
这样的卷积不再服从交换律。

下标有限情形

{aj}仅有有限个非零,
仅有

{aj,j=j0,j0+1,…,j1}。

输入序列无限情形

{xt}仍定义在

t∈Z,

P(z)=∑j=j0j1ajzj,yt=P(L)xt=∑j=j0j1ajxt−j, t∈Z.

这时卷积运算和推移算子多项式的性质与下标非有限时相同。

两侧补零的做法

如果

{aj}={aj,j=j0,j0+1,…,j1},

{xt}也仅有有限个观测值

{xt,t=1,2,…,n},
则情况比较复杂。

下面都定义

aj=0对

j<j0或

j>j1。

一种方法是定义

xt=0对

t<1或

t>n。
这时为了

yt≠0,
必须满足

j0+1≤t≤j1+n,
共输出

m=n+j1−j0个非零元素。
这时仍满足

yt=(a∗x)t, t=j0+1,j0+2,…,j1+n.

如果将

yt,t=j0+1,j0+2,…,j1+n仍放到下标为1:m的数组中,
这个结果与向量az的有限离散卷积的结果完全相同。

比如,

a=[−1,2,−1]。

j0=−1,j1=1,

yt=−xt+1+2xt−xt−1,

t=0,1,…,n+1。

using OffsetArrays
function test_finfilt1(;j0=-1, j1=1)
    a = [-1, 2, -1]
    a = OffsetArray(a, j0:j1)
    x = [2, 3, 7, 11, 13, 17]
    n = length(x)
    get(x, s) = 1 <= s <= n ? x[s] : 0
    y = [ sum(a[j] * get(x, t-j) for j=j0:j1) for t = j0+1:j1+n]
    return y
end
test_finfilt1(j0=-1, j1=1)'
## -2  1  -3  0  2  -2  21  -17
test_finfilt1(j0=0, j1=2)'
## -2  1  -3  0  2  -2  21  -17
test_finfilt1(j0=1, j1=3)'
## -2  1  -3  0  2  -2  21  -17
convolve_zero([-1,2,-1], [2, 3, 7, 11, 13, 17])'
## -2  1  -3  0  2  -2  21  -17

不补零的做法

如果要求

yt=∑j=j0j1ajxt−j中每一个

xt−j都是观测值而不是补的0,
就要求

1≤t−j≤n, ∀j0≤j≤j1.

这等价于

j1+1≤t≤j0+n,
滤波后元素个数减少到了

n−(j1−j0)个。
比如,

j0=0,j1=2,则元素个数减少了2个。
即减少的元素个数等于a的长度减1。

这时,具体使用的边界

t=1和

t=n处的值的个数不同,
这实际上是抛弃了补零的结果中

j0+1≤t≤j1部分和

n+j0+1≤t≤n+j1部分,
即在左端和右端都抛弃了

j1−j0个,
因此结果仍与

a的下标从几开始无关。

常用的

j0和

j1有两种,
一种是

j0=0,

j1=m−1, (

m是a的长度),
这时

yt=∑j=0j1ajxt−j=a0xt+a1xt−1+⋯+aj1xt−j1, t=m,m+1,…,n.

称为单边滤波,
输出

ym,ym+1,…,yn, 长度为

n−(m−1)。

另一种是

j0=−j1,

m=2j1+1,
这时

yt=∑j=−j1j1ajxt−j=a−j1xt+j1+⋯+a0xt+⋯+aj1xt−j1, t=j1+1,…,n−j1,

输出

n−(m−1)个值,
称为双侧滤波。

只要结果的下标仍统一为1到

n−(m−1),则不论

j0和

j1如何取,
这种方法得到的结果都是相同的。

例:

function test_finfilt2(;j0=-1, j1=1)
    a = [-1, 2, -1]
    a = OffsetArray(a, j0:j1)
    x = [2, 3, 7, 11, 13, 17]
    n = length(x)
    get(x, s) = 1 <= s <= n ? x[s] : 0
    y = [ sum(a[j] * x[t-j] for j=j0:j1) for t = j1+1:j0+n]
    return y
end

convolve_zero([-1,2,-1], [2, 3, 7, 11, 13, 17])'
## -2  1  -3  0  2  -2  21  -17
test_finfilt2(j0=-1, j1=1)'
##  -3  0  2  -2
test_finfilt2(j0=0, j1=2)'
##  -3  0  2  -2

结合律

对于补零的做法,
a, b是两个一维数组,
用作滤波器,
x是一个观测值向量,
由离散卷积的性质可知

a∗(b∗x)=(a∗b)∗x.

即分别进行两次滤波,
可以用两个滤波器的卷积(补零做法)

a∗b作为滤波器进行一次滤波。
如:

function test_finfilt3()
    a = [-1, 2, -1]
    b = [3, 1]
    x = [2, 3, 7, 11, 13, 17]
    y1 = convolve_zero(b, x)
    y2 = convolve_zero(a, y1)
    
    c = convolve_zero(a, b)
    y3 = convolve_zero(c, x)
    [y2 y3]
end
test_finfilt3()'
## -6  1  -8  -3  6  -4  61  -30  -17
## -6  1  -8  -3  6  -4  61  -30  -17

对于不补零的做法,
将不补零的卷积记为

a⋆x,
这个运算不满足交换律,
仍满足如下的结合律:

a⋆(b⋆x)=(a∗b)⋆x,

其中

a∗b仍是补零的卷积。

证明

a,b,x的长度分别为

na,nb,nx,

na+nb−1≤nx。
这三个向量的下标都从0开始计数。

z=b⋆x,

z的元素为

zt=∑s=0nb−1bsxt−s, t=nb−1,nb,…,nx−1,

共有

nx−nb+1个元素。

y=a⋆z,

yt=∑j=0na−1ajzt−j, t=na+nb−2,na+nb−1,…,nx−1,

nx−(na+nb−2)个元素。以

zt−j的公式代入得

yt=∑j=0na−1aj∑s=0nb−1bsxt−j−s=∑j=0na−1∑s=0nb−1ajbsxt−(j+s)(令k=j+s, s=k−j)=∑j=0na−1∑k=jj+nb−1ajbk−jxt−k=∑k=0na+nb−2(∑j=0na−1ajbk−j)xt−k, t=na+nb−2,na+nb−1,…,nx−1.

c=a∗b,则

c有

na+nb−1个元素,且

ck=∑j=0na−1ajbk−j, k=0,1,…,na+nb−2.

于是

yt=(c⋆x)t, t=na+nb−2,na+nb−1,…,nx−1.

y=a⋆(b⋆x)=c⋆x=(a∗b)⋆x,
得证。

为了验证,
先写一个一般的滤波函数,
注意这是不满足交换律的:

function convolve_nz(a, x)
    # a 是滤波器,x是输入信号
    m = length(a)
    # 两侧分别去掉m-1个
    convolve_zero(a, x)[m:end-m+1]
end

不补零做法结合律测试:

function test_finfilt4()
    a = [-1, 2, -1]
    b = [3, 1]
    x = [2, 3, 7, 11, 13, 17]
    y1 = convolve_nz(b, x)
    y2 = convolve_nz(a, y1)
    
    c = convolve_zero(a, b)
    y3 = convolve_nz(c, x)
    [y2 y3]
end
test_finfilt4()'
## -3  6  -4
## -3  6  -4

卷积与圆周率割圆法改进

推导

割圆法是用内接正多边形的面积估计圆面积从而逼近圆周率的方法,
从边数等于3或者等于6开始(可以很容易地仅用平方根计算面积),
然后每次从分

n块增加到分

2n块,
仍可以仅用开平方根递推地计算内接正多边形面积。
见21.2。

为了演示,
这里不使用初等的平方根递推方法,
而是直接用三角函数求面积,

f0(s)为半径等于1的内接正

s边形面积,

f0(s)=s2sin⁡2πs.

利用sin函数的泰勒展开,
可以从割圆法得到的逐次逼近序列仅用简单的卷积(滤波)方法得到高精度的圆周率逼近。
为了得到更多有效数字(Float64只有约16位有效数字),
下面的计算用了Julia的BigFloat类型,
只要用big()说明某个数,
则从这个数计算的结果也会用BigFloat保存。

sin⁡(x)的泰勒展开为

sin⁡(x)=x−13!x3+15!x5−⋯=∑j=0∞(−1)jx2j+1(2j+1)!.

于是

f0(s)=s2sin⁡2πs=π−2π33s−2+2π515s−4−4π7315s−6+⋯=π+∑k=1∞cks−2k=π+c1s−2+O(s−4).

所以当

s→∞时这个估计

π的近似公式有

O(s−2)阶精度。

如果使用内接正多边形周长来估计

2π,

π可估计为

p0(s)=12⋅s⋅2⋅sin⁡πs=ssin⁡πs.

作泰勒展开得

p0(s)=s[πs−13!(πs)3+15!(πs)5+⋯]=π−π36s−2+O(s−4).

考虑联合使用面积序列的

f0(s)和

f0(2s)来改进精度。

f0(2s)=π−(14)23π3s−2+(14)22π515s−4−(14)34π7315s−6+⋯=π+∑k=1∞(14)kcks−2k=π+14c1s−2+O(s−4),

注意

f0(s)和

f0(2s)误差部分的

s−2项系数分别为

c1和

14c1,
于是

4f0(2s)−f0(s)的

s−2系数为0,
常数项

π变成了

3π,
于是

4f0(2s)−f0(s)=3π+O(s−4),43f0(2s)−13f0(s)=π+O(s−4).

所以

43f0(2s)−13f0(s)可以获得

O(s−4)阶精度的

π近似公式。

f1(s)=43f0(2s)−13f0(s)=π+O(s−4)=π+c2(1)s−4+∑k=3∞ck(1)s−2k.

易见

f1(2s)与

f1(s)的

s−4项系数为

24=16倍关系,
所以

f2(s)=1615f1(2s)−115f1(s)=π+O(s−6)=π+c3(2)s−6+∑k=4∞ck(2)s−2k.

以此类推,

f2(2s)和

f2(s)的主要误差项系数相差

26=64倍,
于是

f3(s)=6463f2(2s)−163f2(s)=π+O(s−8)=π+c4(3)s−8+∑k=4∞ck(3)s−2k.

一般地,

fm(s)的精度为

s−(2m+2),

fm(2s)与

fm(s)的误差项的倍数为

22m+2,

fm+1(s)=22m+222m+2−1fm(2s)−122m+2−1fm(s), m=0,1,2,3,….

设从某个基础的

s(如

s=6)出发,
每次边数加倍计算精确的内接正多边形面积,
得到一个序列

Sn(0),n=0,1,2,…,M,
其中

Sn+1(0)对应的边数等于

Sn(0)对应的边数的二倍。
则令

a(m)=(22m+222m+2−1,−122m+2−1),m=0,1,2,…,

计算

S(1)=a(0)⋆S(0)就可以从

O(s−2)精度提高到

O(s−4)精度,
计算

S(2)=a(1)⋆S(1)就可以从

O(s−4)精度提高到

O(s−6)精度,
可以反复卷积,

S(m+1)=a(m)⋆S(m),m=0,1,…,M−1,

S(M)有

O(s−2M−2)精度。

这种改进方法也适用于用内接正多边形周长的二分之一近似估计

π的序列。

下面是改进的几种不同的编程实现。

递归方法

因为改进是逐次进行的,
所以如果需要进行

M次改进,
可以先生成第

M−1次改进,
如此递归调用,
直到第

0次计算为

f0(s)=s2sin⁡2πs。

using Printf

f0(s) = s/2*sin(big(2.0)*big(pi)/s)

function show_precision(pi_est)
    pi_str = @sprintf("%.50f", big(π))
    pi_est_str = @sprintf("%.50f", pi_est)
    ind = findfirst(x -> x[1] ≠ x[2], 
          collect(zip(collect(pi_est_str)[3:end], collect(pi_str)[3:end]) ))
    @sprintf("%2i : %.50f", ind, pi_est)
end

function test_pi01(s=6, m=6)
    # 递归算法
    function pi_sin_improve(s=6, m=0)
        # m: 改进次数
        # s: 从m=0开始所用的边数,每改进一次边数自动加倍
        if m==0
            return f0(s)
        else # m > 0
            m1 = m - 1
            a0 = 2^(2*m1 + 2)
            a1 = a0 - 1
            return (a0*pi_sin_improve(2*s, m1) -
                pi_sin_improve(s, m1)) / a1
        end
    end

    pi_str = @sprintf "%.50f" big(π)
    est_vec = map(i -> pi_sin_improve(s,i), 1:6)
    println("     ", pi_str)
    map(est_vec) do est
        println(show_precision(est))
    end
end # function test_pi01
test_pi01(6, 6);
     3.14159265358979323846264338327950288419716939937511
 2 : 3.13397459621556135323627682924706381652859737309481
 5 : 3.14158006333531692053878837350062054938886942432049
 9 : 3.14159265057324285166532423432989114988405248587089
13 : 3.14159265358967531032793304529670698783308976448951
18 : 3.14159265358979323765113602412171302806878023573296
24 : 3.14159265358979323846264234704427340360292693968997

第一行是

π的真值,
后面是

m=1,2,…,6的改进结果,
冒号前面是与

π真值首次在小数点后出现不同的位置。
注意改进了6次,
用到的最大边数就是

6×26=384,
精度是小数点后23位,
如果直接用384边的正多边形,
误差大得多:

@sprintf "%.50f" big(pi) 
@sprintf "%.50f" big(f0(384))
## "3.14159265358979323846264338327950288419716939937511"
## "3.14145247228546207545060930896122564524766230454968"

结果在小数点后第4位出现错误。

注意程序中用了big()使得计算用高精度浮点数进行计算,
否则会因为浮点误差使得迭代改进的效果无法显现。

m=0时边数改为

6×16,结果为:

结果:

     3.14159265358979323846264338327950288419716939937511
 7 : 3.14159253350505711510342162821007307966509942509625
13 : 3.14159265358902771719437964590074800765049986127300
18 : 3.14159265358979323775098181637016594094919795366774
25 : 3.14159265358979323846264327502032881036367294118752
31 : 3.14159265358979323846264338327949998110047544876416
41 : 3.14159265358979323846264338327950288419715494157489

最多用了6114正多边形,
有小数点后40位精度。

利用多次卷积

递归算法明显地有许多重复计算,
下面的程序直接生成多个卷积核(即

{aj}),
然后逐次对

S(0)序列进行不补零的卷积。

function test_pi02(s=6, m=6)
    # s: 最少边长,m:改进次数

    # 设边数取2的倍数:
    svec = s .* (2 .^ (0:m))

    # 求对应的内接正多边形逼近估计的pi
    # 注意这里用了`big(2)`保证精度
    x = [f0(s) for s in svec]

    # 产生多个卷积核:
    kervec = [ let
            a0 = big(2.0)^(2*m + 2)
            a1 = a0 - 1
            [a0, -1.0] ./ a1
        end
        for m=0:(m-1)]
    y = copy(x)

    for ker in kervec
        y = convolve_nz(ker, y)
    end

    pi_str = @sprintf "%.50f" big(π)
    println("     ", pi_str)
    println(show_precision(last(y)))
    return
end # function test_pi02
test_pi02(6, 6);

结果:

     3.14159265358979323846264338327950288419716939937511
24 : 3.14159265358979323846264234704427340360292693968997

96边开始改进:

结果:

     3.14159265358979323846264338327950288419716939937511
41 : 3.14159265358979323846264338327950288419715494157489

利用卷积结合律

上一小节用了多次卷积迭代。
前面证明了

a⋆(b⋆x)=(a∗b)⋆x,
所以多次卷积(滤波)可以先计算各个滤波器的补零卷积作为汇总的滤波器,
然后对输入序列

S(0)作不补零的卷积:

function test_pi03(s=6, m=6)
    # s: 最少边长,m:改进次数

    # 设边数取2的倍数:
    svec = s .* 2 .^ (0:m)

    # 求对应的内接多边形逼近
    # 注意这里用了`big(2)`保证精度
    x = [f0(s) for s in svec]

    # 产生多个卷积核:
    kervec = [ let
            a0 = big(2)^(2*m + 2)
            a1 = a0 - 1
            [a0, -1.0] ./ a1
        end
        for m=0:(m-1)]

    # 汇总的卷积
    ker = reduce(convolve_zero, kervec)
    y = convolve_nz(ker, x)

    pi_str = @sprintf "%.50f" big(π)
    println("     ", pi_str)
    println(show_precision(last(y)))
    return
end # function test_pi03
test_pi03(6*16, 6);
     3.14159265358979323846264338327950288419716939937511
41 : 3.14159265358979323846264338327950288419715494157489

这里用了reduceconvolve_zero配合,
获得了6个卷积核的卷积。
逐次卷积与先生成卷积核的卷积结果相同,
当输入序列很长时,
先生成所有卷积核的卷积会提高效率。

一维卷积在图像处理中应用

在信号处理、时间序列分析中经常对一个向量

{xt,t=1,2,…,n}计算这样的运算:

yt=∑jajxt−j,

其中

{aj}是一个较短的数列。
比如,

a0=1,a1=−1,

yt=−xt−1+xt,
这可以看成是

a的倒序

(−1,1)与

(xt−1,xt)的内积。

这样的卷积在

t=1和

t=n这样的位置可能会用到下标边界以外的值,
前面的做法是将正常下标以外的的值都用0代替,或不允许使用下标以外的值。

在图像处理中用0代替或者减小图像大小都不可取,
所以一般将

t≤0的

xt值用

x[1]代替,

t>n的

xt值用

x[n]代替。
写出如下的下标函数:

function extend(v::AbstractVector, i)
    i0 = firstindex(v)
    i1 = lastindex(v)
    ii = min(max(i, i0), i1)

    return v[ii]
end

卷积中的系数列

{aj}称为“核”,
常常是看成下标

−l:l的长度为

2l+1的数列。
这相当于上面的双侧滤波器。
针对这样的数列的卷积可以写成如下函数:

function convolve_win(v::AbstractVector, ker)
    l = (length(ker)-1) ÷ 2
    [ sum([extend(v, t+j) for j=(-l):l] .* reverse(ker)) for t in eachindex(v) ]
end

可以从一些函数产生核,程序如:

function make_kernel(kernel_fun, n)
    x = kernel_fun.(-n:n)
    x ./= sum(x)
    return x
end

高斯核,有参数sigma:

function make_gaussian_kernel(n; sigma=1)
    pdf(x; sigma=sigma) = 1/sqrt(2*pi)*exp(-0.5*x^2 / sigma^2)
    make_kernel(pdf, n)
end
ker_gau = make_gaussian_kernel(2)

x = rand(20)
y = convolve_win(x, ker_gau)

二维卷积在图像处理中应用

在图像处理中也普遍使用卷积,
各种滤镜往往是某种卷积,
比如高斯模糊、边界探查等。

Mij,i=1,…,m,j=1,…,n是矩阵,

Kij,i=−l,…,l,j=−l,…,l为

(2l+1)×(2l+1)矩阵,

Gij=∑s=−ll∑t=−llKstMi−s,j−t,

Gij称为

K和

M的二维卷积,

K称为“核”。

G=K∗M。

在下标边界处,
一般也用

M的四个边界的元素代替超出边界的值。
扩展下标的函数:

function extend(M::AbstractMatrix, i, j)
    i0 = firstindex(M, 1)
    i1 = lastindex(M,1)
    j0 = firstindex(M, 2)
    j1 = lastindex(M,2)

    ii = min(max(i, i0), i1)
    jj = min(max(j, j0), j1)
    
    return M[ii,jj]
end

计算卷积的函数为:

function convolve(M::AbstractMatrix, K::AbstractMatrix)
    l = (size(K,1)-1) ÷ 2
    [ sum([extend(M, s-i, t-j) for i=(-l):l, j=(-l):l] .* K) 
        for s in axes(M,1), t in axes(M,2)]
end

其中用了axes(M,1)获取矩阵M行下标迭代器,
用了axes(M,2)获取矩阵M列下标迭代器,
这是为了在M下标不从1开始的情况下也能正常工作。

仍可以用二维正态密度制作模糊滤镜,如:

gauss(x, y; σ=1) = gauss(x; σ=σ) * gauss(y; σ=σ)
function with_gaussian_blur(image; σ=3, l=5)
    K = [gauss(x, y; σ=σ) for x=-l:l, y=-l:l]
    K ./= sum(K)
    convolve(image, K)
end

可以用“Sobel”核进行边缘检测。
令:

Kx=[10−120−210−1];Ky=[121000−1−2−1]

Gx=Kx∗M, Gy=Ky∗M, Gij=Gx,ij2+Gy,ij2.

Gij的值越大,
这个点越有可能是边界点,
值越小,这个点与周围点越相近。

Sobel边界检测滤镜的函数:

function with_sobel_edge_detect(image)
    Kx = [1 0 -1; 2 0 -2; 1 0 -1]
    Ky = Kx'
    Gx = convolve(image, Kx)
    Gy = convolve(image, Ky)
    G = sqrt.(Gx .^2 + Gy .^2)
    
    return G
end