前言
非線性方程組,顧名思義就是未知數的冪除了不是1,其他都有可能!線性方程組其實只是非常小的一類,非線性方程組才是大類!也正因此非線性方程組包含各種各樣的方程形式,所以它的解和對應的求解方法不可能像線性方程組那樣完美,即都是局部收斂的。先給出一個直觀的非線性方程組例子:
個人對兩個問題的理解:
1. 非線性方程組如果有解,一般都有很多解!如何理解:
把方程組的解看成是各個函數圖像的交點的。我們知道非線性方程組的各個函數就都是復雜曲線/面,甚至是高緯空間里的復雜東西;線性方程組的各個函數就是最簡單的直線/面!各個復雜函數圖像間的相交機會很多,并且只要相交,就是多個交點(因為交線、交面里有無數的交點),也就是有多個解~
可以想象,非線性方程組有多解是很平常的一件事~ 對于復雜的非線性函數沒解才不正常!
可以想象,這些解是等價的!沒有說是等級更高,誰等級低一些。都是解!因為:只要是解,它就只滿足一個條件:讓方程組中的各個方程=0。所以無法用什么評判標準(比如范數)來說哪個解的等級高一些或者效果更好一些。
注意:這里的解等價和欠定線性方程組通解中的唯一極小范數解不一樣!可以想象二者的區別:非線性方程組中的解都是實打實存在的;而欠定線性方程組中除了特解,其他通解中的解說存在也行,說不存在那就是因為方程條件(個數)都不夠!這些是啥都行的通解和非線性方程組中實打實存在的解肯定不能比!
這樣的話各個非線性方程組的局部收斂性就可以理解,即:空間中有很多解時,我每次只能找一個,那我找誰?找離我出發點最近的那個解唄。所以不同的出發點,就有可能找到不同的解,這就是局部收斂性。
2. 為什么不能用簡單的替換先變成線性方程組求解,然后再解每一個非線性方程?
意思是:每個方程中把所有和有關的用一個變量代替,所有
有關的用一個變量代替,即方程1中用:
,
但是很明顯方程2的第一項兩個變量相乘,沒法用變量代替~
并且,即使在方程2中能代替,那么就會有和
,這樣總未知數變成4個而方程只有2個,還是解不了。
所以,非線性方程組不可能用簡單的線性變量代換來解。
本文介紹最常用、最好用的非線性方程組解法,包括牛頓法和擬牛頓法(割線法)兩大類:
- 牛頓法:原始牛頓法、修正牛頓法;
- 擬牛頓法:逆Broyden秩1法、逆Broyden秩1第二公式、BFS秩2法;
上面這5種方法的函數表達式、計算流程、代碼都會詳細說明和展示。并且,這5種方法都很經濟(算的快)、實用(適用各種非刁鉆的函數)、易用(計算流程好理解,便于編程)。
本文側重的是方法的使用,不提推導和太具體的其他細節。
非線性方程組解法 —— 牛頓法
本文要介紹的5種方法可分為兩大類:牛頓法、擬牛頓法。
先把兩類方法的優缺點列出來:
牛頓法:用jacobi矩陣(導數)
- 優點:導數法收斂速度巨快(平方收斂);自校正誤差不會傳遞;
- 缺點:求導稍費事;只要賦值后的jacobi矩陣存在稀疏性、奇異性、病態等,就跪了;
擬牛頓法:割線法思想,用近似矩陣趨近jacobi矩陣
- 優點:jacobi矩陣的問題在這里都不是問題!這個優點極大提高解法的穩定性?。?!
- 缺點:收斂速度介于平方收斂和直線收斂之間,稍慢一丟丟。
其實,牛頓法看上去要求導很麻煩,其實導數就求了一次而已!剩下都是在循環里對jacobi矩陣的賦值!所以去求導其實不是大問題。大問題就是每次賦值后的jacobi矩陣要求逆?。∵@就對數值矩陣的要求很高了!并且實際問題中經常出現賦值后的jacobi矩陣是稀疏的。
所以,如果原函數們不刁鉆,兩種方法都可勝任。但如果稍微感覺原函數有些復雜時,建議犧牲一丟的速度選擇擬牛頓法!擬牛頓法的穩定性真的提高一個量級。
下面我們將正式開始方法的介紹,給定一個的非線性方程組的通式:
對應的jacobi矩陣為:
原始牛頓法
自己給定,然后進行迭代:
上述迭代可以用了,但是每次循環都要求逆很慢!所以換成下面這種形式:
設真實解為,實現流程如下:
步1:給出初始近似
及計算精度
和
;
步2:假定已進行了k次迭代,已求出
和
,計算
并做賦值:
步3:解上面的線性方程組(用預處理后萬能高斯-賽德爾迭代):
步4:求下面兩個式子:
步5:若
或
,則置:
并轉步6;否則進行如下賦值后回到步2:
步6:結束。
針對最開始的例子,下面給出matlab程序:
clear ; clc
% 未知數:
syms x1 x2;
% 方程組: 統一用列向量
f1 = x1^2 - 10*x1 + x2^2 + 8;
f2 = x1*x2^2 + x1 - 10*x2 + 8;
% f1 = 4*x1 - x2 + 0.1*exp(x1)-1;
% f2 = -x1 + 4*x2 + 1/8*x1^2;
x = [x1;x2];
f = [f1;f2];
% 初始值: 統一用列向量
x0 = [2;3];
error_dxk = double( input('dxk范數的精度:') );
error_fkk = double( input('fkk范數的精度:') );
num = input('停止迭代次數:');
% jacobi1 = [diff(f1,x1) diff(f1,x2);diff(f2,x1) diff(f2,x2)]
% 直接用自帶函數求雅克比矩陣:
jacobi = jacobian([f1,f2],[x1,x2]);
for k = 1:num
Ak = double( subs(jacobi, x, x0) );
bk = double( subs(f, x, x0) );
dxk = pre_seidel(Ak,-bk,k); % 步長
x0 = x0 + dxk;
fkk = double( subs(f, x, x0) ); % fk+1單純用來判斷
if norm(dxk) < error_dxk | norm(fkk) < error_fkk
break;
end
end
if k < num
x_result = x0;
fprintf('精度已達要求,迭代提前結束!\n');
fprintf('%d次迭代后, 近似解為:\n',k);
x_result
else
x_result = x0;
fprintf('迭代次數已達上限!\n');
fprintf('似解為:\n',k);
x_result
end
fprintf('f1結果為:%f\n',double( subs(f1,x,x0) ));
fprintf('f2結果為:%f\n',double( subs(f2,x,x0) ));
fprintf('dxk范數:%f\n',norm(dxk));
fprintf('fkk范數:%f\n',norm(fkk));
結果:
dxk范數的精度:0.0001
fkk范數的精度:0.0001
停止迭代次數:200
第1次求解線性方程組, 當前萬能賽德爾迭代矩陣譜半徑為(越小越好): 0.8306
第2次求解線性方程組, 當前萬能賽德爾迭代矩陣譜半徑為(越小越好): 0.0668
第3次求解線性方程組, 當前萬能賽德爾迭代矩陣譜半徑為(越小越好): 0.1969
第4次求解線性方程組, 當前萬能賽德爾迭代矩陣譜半徑為(越小越好): 0.2209
第5次求解線性方程組, 當前萬能賽德爾迭代矩陣譜半徑為(越小越好): 0.2214
精度已達要求,迭代提前結束!
5次迭代后, 近似解為:
x_result =
0.9999
1.0000
f1結果為:0.000728
f2結果為:0.000184
dxk范數:0.000000
fkk范數:0.000751
說明:完全按照上面的流程編寫的程序,非常好理解。
對應的pre_seidel自定義函數在下載地址里面都有。
修正牛頓法
對于上面的原始牛頓法,如果每步計算改為固定的
可得:
這樣就變成了"簡化牛頓法"。很明顯可以看到簡化牛頓法是線性收斂的!
計算量小,但是收斂速度非常慢。
如果既擁有"原始牛頓法"的收斂速度,又擁有"簡化牛頓法"的工作量???—— 修正牛頓法。
對應的迭代格式為:
從迭代公式可以得知,在每一個k的大循環內都有一個從1到m的小循環來求,下面同樣給出實現流程:
步1:給出初始近似
及計算精度
和
;
步2:根據方程階數/個數n,求出效率最大化的m值或人為給定一個m值;
步3:假定已進行k此迭代,已算出
和
,計算并賦值:
-
:進入每次的內層循環,假設已內層循環i次,已算出
和
,先做1個賦值,再做2個計算:
-
:先做3個賦值
然后做一個計算:
然后再一次判斷:如果,轉到步6(出小循環),否則回到步4(沒出小循環);
- 步6:若
或
,則賦值并結束:
否則,然后重回步3(大循環)。
仍針對最開始的例子,給出matlab程序:
clear ; clc;
% 未知數:
syms x1 x2;
% 方程組: 統一用列向量
f1 = x1^2 - 10*x1 + x2^2 + 8;
f2 = x1*x2^2 + x1 - 10*x2 + 8;
x = [x1;x2];
f = [f1;f2];
% 初始值: 統一用列向量
x0 = [5;4];
error_z = double( input('z范數的精度:') );
error_fk = double( input('fk范數的精度:') );
num = input('停止迭代次數:');
% 直接用自帶函數求雅克比矩陣:
jacobi = jacobian([f1,f2],[x1,x2]);
% 小循環m取多少的判斷: 和方程個數N有關,求w的最大值
syms M N;
mn0 = [N;M];
% 參數xzn是方程的個數:
xzn = length(x);
% 原始的效率對比方程:
w = (N+1)*log(M+1)/( (N+M)*log(2) );
% 讓w方程求最大值的xzm值:
xzm = double( solve( diff( subs(w,N,xzn),M ) ) );
mn1 = [xzn;xzm];
% 最高效率值:
wax = double( subs(w,mn0,mn1) );
% 開始修正牛頓迭代:
xki = x0;
for k = 1:num
fk = double( subs(f,x,x0) );
Ak = inv( double( subs(jacobi, x, x0) ) );
for m = 1:round(xzm)
b = fk;
x0 = x0 - Ak*b;
fki = double( subs(f,x,x0) );
fk = fki;
z = Ak*b;
end
if norm(z) < error_z | norm(fk) < error_fk
break;
end
end
if k < num
x_result = x0;
fprintf('精度已達要求,迭代提前結束!\n');
fprintf('%d次迭代后, 近似解為:\n',k);
x_result
else
x_result = x0;
fprintf('迭代次數已達上限!\n');
fprintf('似解為:\n',k);
x_result
end
fprintf('f1結果為:%f\n',double( subs(f1,x,x0) ));
fprintf('f2結果為:%f\n',double( subs(f2,x,x0) ));
fprintf('z范數:%f\n',norm(z));
fprintf('fk范數:%f\n',norm(fk));
效果和原始牛頓法一樣,可以感覺到速度提升了!!
這里用到了求最效率的m值,下面對它的求法加以補充(完全可以不求手動給):
修正牛頓法與原始牛頓法
的效率之比為:
其中n就是方程組中方程的個數,這對于每個方程組都是固定常數。所以要想效率最高,那就讓上式中右邊含m參數的表達式取最大值即可(求導讓導函數=0),即如程序中所示。m求出若不是整數,就4舍5入。
至此,牛頓法的兩種方法介紹完畢。下面簡單對比一些二者的不同:
? | 原始牛頓法 | 修正牛頓法 |
---|---|---|
區別 | 不求jacobi矩陣的逆 每次要求線性方程組 |
不求線性方程組 每次要求jacobi的逆 |
前文已經說明:每次要對矩陣求逆的話就很不穩定!
所以:其實原始牛頓法不錯!好編程、速度快、不求逆、線性方程組有萬能解法!修正牛頓法單純提高了一丟丟效率,但是穩定性個人覺得變差了。
故,牛頓法中我推薦使用原始牛頓法。
非線性方程組解法 —— 擬牛頓法
牛頓法中要求導的jacobi矩陣,自然可以想到能否用差商來代替求導?
肯定是可以的,這就是割線法的思想,
即牛頓法是用超切平面趨近,割線法是用超割平面去趨近。
常用的割線法有:2點割線法、n點割線法、n+1點割線法;(n是方程個數)
其中n+1點割線法效率是最高的,但是穩定性沒有n點割線法好!
所以,能不能構造一種算法,它具備n+1點割線法效率高的優點同時又增加穩定性?
這就是擬牛頓法。
所以:擬牛頓法是基于割線法,并做的比割線法更好的算法!
所以:擬牛頓法和割線法的"收斂速度"介于線性收斂和平方收斂之間;
所以:本文就不介紹割線法怎么搞,直接上最好的割線法 —— 擬牛頓法。
擬牛頓法中最好的是Broyden提出的操作,統稱為Broyden方法。
思想:不斷用一個低秩矩陣對進行修正;不同方法的區別就是那個低秩矩陣不同~
可以想象:低秩矩陣不同的秩數,就能得到一系列方法。
最常用:Broyden秩1、Broyden秩1第二方法、逆Broyden秩1,逆Broyden秩1第二方法。
另外還有秩2的方法,比如比較好的BFS。
秩1相關方法
1. Broyden秩1迭代公式:
其中:
2. Broyden秩1第二方法迭代公式:
上面的兩種方法中的第一個式子都要求逆,不好看!我們用代替,然后對上面的兩個方法分別做改寫,就可以得到它們的逆方法!
3. 逆Broyden秩1迭代公式:√
4. 逆Broyden秩1第二方法迭代公式:√
對于上面4個方法/迭代公式,有下面3點說明:
- 文獻中說"第二方法"類只適用于jacobi矩陣對稱!但其實不對稱的時候也可以用!不過穩定性大幅變差;
- "逆方法"類的穩定性會提高!所以實際中/本文使用逆方法類。
- 方法間的區別只在第2個公式中右邊那個項。故編程只用換那個表達式即可。
下面給出"逆Broyden秩1方法"實現流程:
步1:給出初始近似
及計算精度
和
;
步2:計算初始矩陣
,一般?。?/p>
步3:k = 0;計算
步4:計算
和
:
步5:計算
;檢驗若若
或
則轉步8,否則轉步6;
步6:計算
,并根據上面公式計算
:
- 步7:做4個賦值,然后回到步4:
- 步8:
,結束。
仍對于最開始的例子,給出逆Broyden秩1法的matlab程序:
clear; clc;
% 未知數:
syms x1 x2;
% 方程組: 統一用列向量
f1 = x1^2 - 10*x1 + x2^2 + 8;
f2 = x1*x2^2 + x1 - 10*x2 + 8;
x = [x1;x2];
f = [f1;f2];
% 初始值: 統一用列向量
x0 = [2;3];
error_sk = double( input('sk范數的精度:') );
error_fkk = double( input('fkk范數的精度:') );
num = input('停止迭代次數:');
% 直接用自帶函數求雅克比矩陣:
jacobi = jacobian([f1,f2],[x1,x2]);
% 開始求解前的初值:
Bk = inv( double( subs(jacobi,x,x0) ) );
fk = double( subs(f,x,x0) );
% 循環完全按照流程來
for k = 1:num
sk = -Bk*fk;
x0 = x0 + sk;
fkk = double( subs(f,x,x0) );
if norm(sk) < error_sk | norm(fkk) < error_fkk
break;
end
yk = fkk - fk;
Bkk = Bk + (sk-Bk*yk)*(sk')*Bk/( sk'*Bk*yk ); % 不同方法改這里表達式即可
fk = fkk;
Bk = Bkk;
end
if k < num
x_result = x0;
fprintf('精度已達要求,迭代提前結束!\n');
fprintf('%d次迭代后, 近似解為:\n',k);
x_result
else
x_result = x0;
fprintf('迭代次數已達上限!\n');
fprintf('似解為:\n',k);
x_result
end
fprintf('f1結果為:%f\n',double( subs(f1,x,x0) ));
fprintf('f2結果為:%f\n',double( subs(f2,x,x0) ));
fprintf('sk范數:%f\n',norm(sk));
fprintf('fkk范數:%f\n',norm(fkk));
效果就是比牛頓法多迭代幾次而已。
對應的逆Broyden秩1第二方法只用改程序中的Bkk表達式即可~ 故不多展示。
秩2相關方法
個人感覺:秩越高,"局部收斂"的范圍更廣一些(是好事)!后面會舉例子說明這一點。
個人感覺穩定、效率高、效果好的秩2方法是:BFS(Broyden-Fletcher-Shanme),迭代公式為:
其中的表達式為:
實現上和秩1法基本上就是一樣的,只不過每次循環多算一個就行。給出程序:
clear; clc;
% 未知數:
syms x1 x2;
% 方程組: 統一用列向量
f1 = x1^2 - 10*x1 + x2^2 + 8;
f2 = x1*x2^2 + x1 - 10*x2 + 8;
x = [x1;x2];
f = [f1;f2];
% 初始值: 統一用列向量
x0 = [5;4];
error_sk = double( input('sk范數的精度:') );
error_fkk = double( input('fkk范數的精度:') );
num = input('停止迭代次數:');
% 直接用自帶函數求雅克比矩陣:
jacobi = jacobian([f1,f2],[x1,x2]);
% 開始求解前的初值:
Bk = inv( double( subs(jacobi,x,x0) ) );
fk = double( subs(f,x,x0) );
for k = 1:num
sk = -Bk*fk;
x0 = x0 + sk;
fkk = double( subs(f,x,x0) );
if norm(sk) < error_sk | norm(fkk) < error_fkk
break;
end
yk = fkk - fk;
miuk = 1 + yk'*Bk*yk/(sk'*yk); % 就多算一個這個而已
Bkk = Bk + (miuk*sk*sk' - sk*yk'*Bk - Bk*yk*sk')/(sk'*yk);
fk = fkk;
Bk = Bkk;
end
if k < num
x_result = x0;
fprintf('精度已達要求,迭代提前結束!\n');
fprintf('%d次迭代后, 近似解為:\n',k);
x_result
else
x_result = x0;
fprintf('迭代次數已達上限!\n');
fprintf('似解為:\n',k);
x_result
end
fprintf('f1結果為:%f\n',double( subs(f1,x,x0) ));
fprintf('f2結果為:%f\n',double( subs(f2,x,x0) ));
fprintf('sk范數:%f\n',norm(sk));
fprintf('fkk范數:%f\n',norm(fkk));
至此,所有的方法就介紹完畢了。
總結
5種方法都在上面介紹并給出了程序,下面還有3點自己的心得體會(不一定正確!)。
1. 關于兩套精度判斷:和
:
不同方法衡量的對象稍有不同,大體可歸納為:
衡量是前后兩次解的差值的范數;
衡量是矩陣數值的范數;
可以認為是2套獨立的衡量標準,只不過我們在線性方程組里常用的是第一種而已罷了;
只要迭代在收斂,這兩個范數都是在不斷縮小的往0趨近的!故其實都可設置成0.0001這種形式;
誰先到,意味著有一個衡量標準已經達到了,就可以結束了!
當然讓兩個都達到了再結束也可以。一句話:只要迭代在收斂,兩套范數標準都是在下降趨向0的。
2. 擬牛頓法秩越高的方法"局部收斂"的范圍會廣一些!
對應上面同樣的例子:和
都是0.0001;5種方法的初值都是[5;4]時:
只有BFS這個秩2方法的最終解是[1;1]
其他4種方法全找的是[2.1934;3.0205]
雖然這兩個都是非線性方程組的解,但是為什么只有秩2的方法能搜到離起始點較遠的解?
個人猜測是:秩2法的"局部收斂"范圍比秩1法更廣!
但不管是秩幾,只要是牛頓法的大類(牛頓+割線+擬牛頓),都是局部收斂的!
3. 使用推薦:
若方程組很大,看重求解速度:使用原始牛頓法;
若看重求解的穩定性:擬牛頓法BFS秩2;
補充說明:牛頓法最有可能出問題的地方就是jacobi矩陣每次賦完值之后的數值矩陣!穩定性太依賴這個導函數矩陣。擬牛頓法穩定性非常好!并且個人感覺:秩越高穩定性越好、收斂速度越趨于平方收斂!
參考寶書
- 李慶揚, 莫孜中, 祁力群. 非線性方程組的數值解法[M]. 科學出版社, 2005.
- 范金燕, 袁亞湘. 非線性方程組數值方法[M]. 科學出版社, 2018.
- 李星. 數值分析[M]. 科學出版社, 2014.