【非線形回帰分析】信頼領域法について
はじめに
今回は非線形な関数による回帰分析(モデルとなる関数のパラメータの計算)の手法の中で信頼領域法について解説をします。信頼領域法は ”対象となる関数(以下、目的関数)を二次式で近似し、その近似関数を最小とするパラメータを求める” という動作を反復します。従来の方法と異なる点は更新の際に加算するベクトルの大きさに制限を設ける点です。制限する大きさについては、目的関数とその近似関数のパラメータ更新による減少量の比によって決定します。最急降下法やガウス・ニュートン法など従来の計算方法に関する知識が必要ですので、こちらの記事を予めご一読頂ければ幸いです。
1. 前置き
また、残差平方和
2. 概要
信頼領域法は従来の方法と同様に漸化式の反復計算により求めます。
漸化式については、
信頼領域法では、探索ベクトル
信頼領域法のおおまかな手順は、以下のようになります。
- パラメータの初期値
、信頼半径の初期値\boldsymbol{a^{(0)}} 、閾値r_{0} を設定する\eta -
を二次式で近似した関数S(\boldsymbol{a}) を最小とする探索ベクトルS_{(2)}(\boldsymbol{a}) を\boldsymbol{d^{(k)}} の範囲で計算する\boldsymbol{d^{(k)}} \leq r_{k}
ドッグレッグ法により計算\to - 目的関数とその近似関数のズレを評価する関数
を計算\rho_{k} - 信頼半径を更新
[1] の場合、\rho_{k} \lt 0.25 r_{k+1}=0.25 r_{k}
[2] かつ\rho_{k} \gt 0.75 の場合、||\boldsymbol{d^{(k)}}|| = r_{k} r_{k+1}=2r_{k}
[3] 1,2を満たさない場合、r_{k+1}=r_{k} - パラメータの値を更新
[1] の場合、\rho_{k} \geq \eta よりパラメータの値を更新\boldsymbol{a^{(k+1)}} = \boldsymbol{a^{(k)}} + \boldsymbol{d^{(k)}}
[2] の場合、\rho_{k} \lt \eta ⇒パラメータはそのまま\boldsymbol{a^{(k+1)}} = \boldsymbol{a^{(k)}} -
を計算し、許容値以下なら終了、そうでない場合は\nabla S(\boldsymbol{a^{(k+1)}}) として 2 へ戻るk+1 \to k
3. ドッグレッグ法
信頼領域法は上記の手順2で示したように、"二次近似関数が最小値となる探索ベクトルを信頼領域の範囲内で求める" といった制限付きの最適化問題が存在します。これを解くにあたってはドッグレッグ法と呼ばれる手法を用います。
まず、
ヘッセ行列については二階偏微分の計算の複雑さからガウス・ニュートン法同様に、関数
ヤコビ行列
以上をまとめると、二次近似関数
ドッグレッグ法の簡易手順を以下に示します。
- ガウス・ニュートンステップ
を計算\boldsymbol{d_{gn}} - 1 で求めたガウス・ニュートンステップ
と信頼半径\boldsymbol{d_{gn}} の大小を比較し、r_{k}
[1] の場合 ⇒||\boldsymbol{d_{gn}}|| \leq r_{k} \boldsymbol{d^{(k)}}=\boldsymbol{d_{gn}}
[2] の場合 ⇒ 3 へ||\boldsymbol{d_{gn}}|| \gt r_{k} - コーシー点
を計算\boldsymbol{d_{c}} - 3 で求めたコーシー点
の大きさと信頼半径\boldsymbol{d_{c}} の大小を比較し、r_{k}
[1] の場合 ⇒||\boldsymbol{d_{c}}|| \lt r_{k} ⇒ドッグレッグステップ\boldsymbol{d^{(k)}}=\boldsymbol{d_{dl}}
[2] の場合 ⇒||\boldsymbol{d_{c}}|| \geq r_{k} (方向はコーシー点と同じで大きさが\boldsymbol{d^{(k)}}=\dfrac{r_{k}}{||\boldsymbol{d_{c}}||}\boldsymbol{d_{c}} )r_{k}
ガウス・ニュートンステップ
3.1 ガウス・ニュートンステップ
まず最初に、ガウス・ニュートンステップを計算します。ガウス・ニュートンステップは二次近似関数
よって、勾配ベクトル
以上より、ガウス・ニュートンステップ
3.2 コーシー点
ガウス・ニュートンステップが信頼領域外に存在する場合は、コーシー点を計算します。コーシー点は最急降下法と同様に探索ベクトルを
上式を最小とする
となります。以上より、コーシー点は以下のように表せます。
3.3 ドッグレッグステップ
コーシー点が信頼領域内にある場合、 ガウス・ニュートンステップ、コーシー点を結ぶ直線と信頼領域の境界との交点を使用します。この交点をドッグレッグステップと呼びます。数式で表すと以下のようになります。
上式
まず、
となります。
解は2つ存在しますが、
コーシー点ではなくドッグレッグステップを使う理由はドッグレッグステップにおける二次近似関数
4. 信頼半径・パラメータの更新
信頼半径の更新には前章にて計算した探索ベクトル
上記
[1] 信頼半径の更新
-
のとき\rho_{k} \lt 0.25
r_{k+1} = 0.25 r_{k} -
かつ\rho_{k} \gt 0.75 ||\boldsymbol{d^{(k)}}||=r_{k}
r_{k+1} = 2r_{k}
※信頼半径の上限値 超える場合は上限値を使用r_{max} -
1 および 2 の条件を満たさないとき
r_{k+1} = r_{k}
目的関数とその近似関数のズレが大きい場合には信頼半径を小さく、ズレが小さくガウス・ニュートンステップが信頼領域外にある場合に信頼半径を大きくします。
[2] パラメータの更新
-
のとき\rho_{k} \geq \eta
\boldsymbol{a^{(k+1)}} = \boldsymbol{a^{(k)}} +\boldsymbol{d^{(k)}} -
のとき\rho_{k} \lt \eta
\boldsymbol{a^{(k+1)}} = \boldsymbol{a^{(k)}}
目的関数とその近似関数のズレが大きい場合には、パラメータ
5. Python ソースコード
csv(カンマ区切)、txt(タブ区切)のファイルを読み込み、
バージョン情報
python : 3.12.0
numpy : 2.1.0
pandas : 2.2.2
matplotlib: 3.8.4
import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
import time
import warnings
import os
import glob
#関数定義
# f, fa , fb , fc , S , Sa , Sb , Sc
def f(a,x):
y = 1/(a[0]*x + a[1]) + a[2]
return y
def f_a(a,x):
y = - x/(a[0]*x + a[1]) **2
return y
def f_b(a,x):
y = -1/(a[0]*x + a[1])**2
return y
def f_c(a,x):
y = np.ones(len(x))
return y
def S(a,x,y):
r = y - 1/(a[0]*x + a[1]) -a[2]
return 0.5*np.sum(r**2)
def S_a(a,x,y):
r = x/(a[0]*x + a[1]) **2 * (y - 1/(a[0]*x+a[1]) -a[2])
return np.sum(r)
def S_b(a,x,y):
r = 1/(a[0]*x + a[1]) **2 * (y - 1/(a[0]*x+a[1]) -a[2] )
return np.sum(r)
def S_c(a,x,y):
r = 1/(a[0]*x+a[1]) + a[2] -y
return np.sum(r)
#数字入力用関数 数字以外は除外
def ENTER_NUM(phrase):
while(1): #数字が入力されるまでループ
#数字を入力
num = input(phrase)
if num.isdecimal() : #数値判定
num = int(num)
break
print('数値を入力してください')
return num
#入力された文字を含むファイル名を出力する関数
# 検索結果が複数の場合は数字を入力
def SEARCH(phrase,exts):
while(1): #ファイル選択完了までループ
word = input(phrase) #検索するファイル名の入力
word = word.strip() #strip()⇒両端の空白文字除去
if word == '':
continue
#glob でファイルの検索結果(ファイル名)を格納するlist
filelist = []
# 入力となる拡張子すべてに対し検索を実施
for ext in exts:
filelist.extend(glob.glob(f'*{word}*{ext}'))
#ファイルが複数存在する場合は、”番号:ファイル名”を出力し、番号を選択する
if len(filelist) >= 2:
print('指定のファイルが複数存在します。')
for i in range(len(filelist)):
print(str(i+1)+ ':' +filelist[i])
print('0:再選択')
while(1): #適切なファイル番号が入力されるまでループ
file_no = ENTER_NUM('番号を選択してください:')
if file_no == 0:
break #0が選択されたら、検索の文字入力まで戻る
elif 1 <= file_no <= len(filelist):
return filelist[file_no - 1]
else:
continue
continue # 0が入力され、文字検索をやり直す用
#ファイルが見つからない場合は再度文字の入力から
elif len(filelist) == 0:
print('ファイルが見つかりません')
continue
break
return filelist[0]
# メインの関数
def main():
# ファイル名の入力
exts = ['csv','txt'] #検索するファイルの拡張子
#入力された文字を含むファイルを検索
infilename = SEARCH('点列データのリンクを入力してください(txt(タブ区切) / csv(カンマ区切))',exts)
# 選択されたファイルの表示
print(f'ファイル:"{infilename}"を選択しました。')
# ファイル名から拡張子の抽出
ext = infilename.split('.')[-1]
#拡張子によって区切り文字を選択
if ext == 'txt':
delim = '\t'
elif ext == 'csv':
delim = ','
else:
print(f'拡張子:{ext}は使用できません。txt(タブ区切) / csv(カンマ区切)のファイルを選択してください。')
return
# 行 / 列番号の選択
col_x = ENTER_NUM('x:説明変数の列番号を入力:')
col_y = ENTER_NUM('y:目的変数の列番号を入力:')
start_row = ENTER_NUM('データが始まる行の番号を入力:')
#ファイルからx,yのデータを読み込み
#エラー発生時はmain関数終了
try:
x = np.loadtxt(infilename,delimiter=delim,skiprows=start_row-1, usecols=col_x-1)
y = np.loadtxt(infilename,delimiter=delim,skiprows=start_row-1, usecols=col_y-1)
except Exception as e:
print(f'ファイルのデータに数字以外が含まれているか、データの数が一致しません。\n'\
f'ファイルの中身を確認ください。最初に戻ります。\n' \
f'ERROR:{e}')
return
# x,y の平均値を定義
x_bar = np.average(x)
y_bar = np.average(y)
# x,yを 二次式で近似し、二次の項の係数で下に凸か下に凸かを判定
# yi=β2xi^2 + β0xi +β0 をまとめて 行列 y=Xβ として X の作成
X = []
for i in range(len(x)):
x_i = [1,x[i],x[i]**2]
X.append(x_i)
X =np.array(X)
# 正規方程式より近似係数を計算 二次項の係数 = beta[2]
beta = np.linalg.solve(X.T @ X, X.T @ y)
# 共分散により増加関数か減少関数か判定
cov_xy = np.sum((x-x_bar)*y)
# 減少関数、下に凸となるようにx,yを変換
sgn_x = -np.sign(cov_xy)*np.sign(beta[2])
sgn_y = np.sign(beta[2])
x1 = sgn_x * x
y1 = sgn_y * y
# c, g2(c) , g2'(c) の初期値の設定
c = min(y1)-0.001 # c の初期値は最小値より若干小さい値とする
g2 =1 #g2(c)の初期値
g2_prime = -1 #g2'(c) の初期値
g2_tol = 1e-3 #g2の許容値 許容値以下になるまで反復繰返し
rep =0 #反復回数
rep_limit = 200 #反復回数の上限
# g2'(c)が負, g2(c) の値が許容値以下になるまで、ニュートン法による反復計算を繰り返す。
while( (abs(g2) >= g2_tol) or (g2_prime>0) ):
# y1 -> z に変換
z = 1/(y1-c)
#平均,二乗平均等を計算
x1_bar = np.average(x1)
z_bar = np.average(z)
z_sqbar = np.average(z**2)
#g2(c)の計算
g2 = np.sum(z**2*(x1-x1_bar)) * np.sum((z-z_bar)**2) \
- np.sum(z*(x1-x1_bar)) * np.sum(z**2*(z-z_bar))
#g2'(c)の計算
g2_prime = 2*np.sum(z**3*(x1-x1_bar)) * np.sum((z-z_bar)**2) \
+np.sum(z**2*(x1-x1_bar)) * np.sum(z**2*(z-z_bar)) \
- np.sum(z*(x1-x1_bar)) * np.sum(z**2*(2*z*(z-z_bar)+z**2-z_sqbar))
#ニュートン法により値を更新
c = c + g2/abs(g2_prime)
# c が y1 の最小値以上の場合、無限大か負値となるため、下限を設定
if c >= min(y1):
c = min(y1)-0.001
#反復回数の更新
rep +=1
# 反復回数が上限に達した場合は計算終了
if rep == rep_limit:
print('反復回数の上限に達しました。計算を終了します。')
break
# cの計算が終了したら、反復回数と、c,g2(c)の値を表示
#print(f'{rep}回目:g2={g2}\nc={c}')
# z=1/(y-c) と x1 を直線近似し、計算結果をa,bの初期値とする
c0 = c
z = 1/(y1-c)
# x1,z の平均値計算
x1_bar = np.average(x1)
z_bar = np.average(z)
# a,bの初期値a0,b0の計算
# 最小二乗法
a0 = (np.dot(x1-x1_bar,z) / np.sum((x1-x1_bar) ** 2))
b0 = z_bar - x1_bar * a0
# 符号の修正
a0 = sgn_x*sgn_y * a0
b0 = sgn_y * b0
c0 = sgn_y * c0
# 3係数を配列にまとめる
a = np.array([a0,b0,c0])
#a = np.array([-3,1,1]) #初期値を手動設定する場合
# 残差平方和の計算
sq_err = S(a,x,y)
# Sの勾配 ∇S(a)の計算
nabla_S = np.array([S_a(a,x,y),S_b(a,x,y),S_c(a,x,y)])
#初期値計算結果の表示
print(f'\n--------初期値の計算結果--------\n'+\
f'[a,b,c]={a}\n'+\
f'∇S(a,b,c)={nabla_S}\n'+ \
f'二乗誤差:{sq_err}\n'+\
f'--------------------------------')
# 信頼領域法による係数の計算、∇S(a)=(Sa,Sb,Sc)が許容値以下になるまで反復計算
rep = 1 #反復回数
rep_limit = 1000 #反復回数の上限
error_limit = 1e-6 #許容値
r_tr = 10 #信頼半径の初期値
eta = 0.2 #閾値 η
r_max = 100 #信頼半径の上限値の設定
lamb_min = 1e-10 #ガウス・ニュートン点が求められない(ヘッセ行列の近似列が正則でない)場合に加える単位行列の係数
# ∇S(a)の全要素が許容値以下になるまで反復計算
while ( (abs(nabla_S[0]) >= error_limit) or (abs(nabla_S[1]) >= error_limit) or (abs(nabla_S[2]) >= error_limit) ):
# ドッグレッグ法による計算
#ヤコビ行列の計算
Jf = np.array([f_a(a,x),f_b(a,x),f_c(a,x)]).T
#print(f'ヤコビ行列Jf\n{Jf}')
# ヘッセ行列 =Jf^{T}Jf の計算
Hs = Jf.T @ Jf
#print(f'ヘッセ近似行列Hs\n{Hs}')
lamb = lamb_min
M = Hs
# ガウス・ニュートン点の計算
while(1):
try:
d_gn = np.linalg.solve(M,-nabla_S)
break
except Exception as e:
#ヘッセ近似行列が正則でない場合、単位行列を足して正則化
M = Hs + lamb * np.eye(3)
lamb = lamb * 2
print(f'ヘッセ近似行列が正則でない可能性があります。正則化して再計算します。\n' +\
f'ERROR:{e}')
# ガウス・ニュートンステップの大きさと信頼半径を比較
dgn_norm = np.linalg.norm(d_gn)
if dgn_norm <= r_tr:
dk = d_gn
d_c = [0,0,0]
dc_norm=0
# 探索ベクトルの大きさ=信頼半径の場合に1となるフラグ
tr_flag = 0
# ガウス・ニュートンステップが信頼領域外の場合、コーシー点を計算
else:
#コーシー点の計算
alpha0 = np.sum(nabla_S**2)/ np.sum( (Jf @ nabla_S)**2)
d_c = -alpha0 * nabla_S
#コーシー点の大きさと信頼半径を比較
dc_norm = np.linalg.norm(d_c)
# コーシー点が信頼領域内の場合
if dc_norm <= r_tr:
# 係数βの計算
a_quadeq = np.sum((d_gn-d_c)**2)
b_quadeq = np.dot(d_c,(d_gn-d_c))
c_quadeq = dc_norm**2 -r_tr**2
D_quadeq = b_quadeq **2 - a_quadeq * c_quadeq
beta = (-b_quadeq+np.sqrt(D_quadeq))/a_quadeq
if (beta<0) or (beta>1):
beta = (-b_quadeq-np.sqrt(D_quadeq))/a_quadeq
# ドッグレッグステップの計算
d_dl = beta * d_gn + (1-beta)*d_c
ddl_norm = np.linalg.norm(d_dl)
# 探索ベクトルをドッグレッグステップに設定
dk = d_dl
else:
#コーシー点が信頼領域外の場合は、コーシー点と同じ方向で大きさ=信頼半径の点を探索ベクトルとする
dk = r_tr/dc_norm * d_c
# 探索ベクトルの大きさ=信頼半径の場合1となるフラグ (ガウス・ニュートンステップが信頼領域外の場合)
tr_flag = 1
# ρk の計算 残差平方和S(a) の二次近似関数
rho = (S(a+dk,x,y)-S(a,x,y))/(np.dot(nabla_S,dk) + 1/2*np.sum((Jf @ dk)**2))
if rho < 0.25:
r_tr = 0.25*r_tr
elif (rho > 0.75) and (tr_flag == 1):
r_tr = np.amin([2*r_tr,r_max])
#パラメータの更新 評価関数ρkが閾値ηを超えた場合のみ更新
if rho >= eta:
a = a + dk
#Sの勾配ベクトル ∇S(a,b,c) の値の計算
nabla_S = np.array([S_a(a,x,y),S_b(a,x,y),S_c(a,x,y)])
# 残差平方和 S(a,b,c) の計算
sq_err = S(a,x,y)
#途中経過の観察用
'''
print(f'\n----{rep}回目計算途中結果-----\n'+\
f'パラメータ[a,b,c]={a}\n'+\
f'∇S(a,b,c)={nabla_S}\n'+ \
f'二乗誤差:{sq_err}\n'+\
f'ガウス・ニュートン点{d_gn}\n大きさ:{dgn_norm}\n' +\
f'コーシー点{d_c}\n大きさ:{dc_norm}\n' +\
f'信頼半径:{r_tr}\n'+\
f'dk大きさ:{np.linalg.norm(dk)}\n'+\
f'評価関数ρ:{rho}\n'+\
f'--------------------------------')
time.sleep(0.3)#'''
# 反復回数の更新
rep +=1
# 反復回数が上限に達した場合は計算終了
if rep >= rep_limit:
print('反復回数の上限に達しました。計算を終了します。')
break
#出力結果格納用配列
dataset = [a[0],a[1],a[2],nabla_S,sq_err]
print(f'\n------------計算結果------------\n'+\
f'反復回数:{rep}\n'+\
f'[a,b,c]={a}\n'+\
f'∇S(a,b,c)={nabla_S}\n'+ \
f'二乗誤差:{sq_err}\n'+\
f'--------------------------------')
#'''グラフ出力(黒実線:近似曲線、●:入力データ) グラフ出力が不要の場合は行頭の#を削除
dx = (max(x)-min(x))/1000
x1=np.arange(min(x),max(x),dx)
y_NLLSM = f(a,x1)
plt.scatter(x,y)
plt.plot(x1,y_NLLSM,color='black')
plt.show()
#'''
# ファイル出力
## 表題作成
index1 = ['a', 'b','c','∇S','残差平方和']
pd_out = pd.DataFrame(dataset, index=index1)
# 名前の設定
outname0 = f'x={int(min(x))}-{int(max(x))}_Non_Linear_LSM'
outname = outname0 + '.csv'
#同名ファイルが存在する場合の名前変更
filenum = 1
while( os.path.exists(outname) ):
outname = outname0 + f'_{filenum}.csv'
filenum += 1
# csvデータ出力 #shift-jis でないとエクセルで文字化け
pd.DataFrame(pd_out).to_csv(outname,header=None, encoding='shift-jis')
print(f'ファイルを保存しました。ファイル名:{outname}')
if __name__ == "__main__":
while(1):
main()
#input('ENTERキーで最初に戻ります。')
6.【補足】ドッグレッグステップとコーシー点の大小
本章では、ドッグレッグステップにおける二次近似関数
方針は
まず
よって
となります。
Discussion