非線形回帰分析(カーブフィッティング)の手法:PythonによるLM法の実装
はじめに
今回は非線形な関数による回帰分析の手法について解説をします。考え方は線形と非線形の場合で違いはなく、最小二乗法の原理で残差平方和 ( 観測値と予測値の差の二乗和 ) を最小にするパラメータを求めます。異なる点は非線形関数の場合には最小にするパラメータが解析的に求められない場合が多いため、数値解析により計算する点です。本記事では数値解析による主な手法である、最急降下法、ニュートン法、ガウス・ニュートン法、レーベンバーグ・マルカート(LM)法について解説します。また、
概要
残差平方和
微分したときに生じる
演算子
パラメータ
漸化式については上記のすべての方法について、以下の形で表されます。
1. 最急降下法
最急降下法の漸化式は
最急降下法は
計算量が少ないという利点がありますが、収束が非常に遅いという欠点があります。
2. ニュートン法
最急降下法の収束の遅さを改善したのがニュートン法です。
ニュートン法の漸化式は、
ニュートン法は
関数
上式
上式
よって、以下の式が導かれます。
以上より、項冒頭の漸化式
実際の計算では、逆行列を計算するよりも連立方程式を解く方が早いため、
また、ヘッセ行列
ニュートン法は最急降下法と比較して、収束が早いのが利点ですが、欠点として
以上より、ニュートン法により極小値を計算するには、ヘッセ行列が正定値行列である必要があります。さらに、2階偏微分の計算が必要で、計算量が多いという問題点もあります。
補足1:極大値、極小値とヘッセ行列
行列
-
が正定値行列P 固有値がすべて正\leftrightarrow \boldsymbol{x}^{T}P\boldsymbol{x} \gt 0 \leftrightarrow -
が半正定値行列P 固有値がすべて\leftrightarrow \boldsymbol{x}^{T}P\boldsymbol{x} \geq 0 \leftrightarrow 以上0 -
が負定値行列P 固有値がすべて負\leftrightarrow \boldsymbol{x}^{T}P\boldsymbol{x} \lt 0 \leftrightarrow -
が半負定値行列P 固有値がすべて\leftrightarrow \boldsymbol{x}^{T}P\boldsymbol{x} \leq 0 \leftrightarrow 以下0
多変数関数の停留点(勾配ベクトルが零ベクトルとなる点)について
- 極小値
- "
かつ\nabla S(\boldsymbol{a})=\boldsymbol{0} が半正定値行列" が必要条件H_{S} - 正定値行列であれば、必ず極小値
- すべてのパラメータ
について極小値となるa_{i} (i=1,\ ...\ ,m)
- 極大値
- "
かつ\nabla S(\boldsymbol{a})=\boldsymbol{0} が半負定値行列" が必要条件H_{S} - 負定値行列であれば、必ず極大値
- すべてのパラメータ
について極大値となるa_{i} (i=1,\ ...\ ,m)
- 鞍点
-
かつ\nabla S(\boldsymbol{a})=\boldsymbol{0} が半正定値行列でも半負定値行列でもなければ、必ず鞍点H_{S} - パラメータ
について極大値となるパラメータと極小値となるパラメータが混在 (固有値がすべてゼロでない場合)a_{i} (i=1,\ ...\ ,m)
式
正定値行列の場合
負定値行列の場合
補足2:正定値(負定値)行列とパラメータ更新後の S の増減
次に、
まず、
となります。
上式
となります。
以上より、
3. ガウス・ニュートン法
ニュートン法の問題点はヘッセ行列によって極値に収束しない場合があり、2階偏微分が必要で計算量が多いことでした。その2つの欠点を改善したのが、ガウス・ニュートン法です。
ガウス・ニュートン法の漸化式は、
ここで、
ガウス・ニュートン法は、ニュートン法で計算するヘッセ行列の
上式からガウス・ニュートン法では、前述のヤコビ行列
ガウス・ニュートン法においては、
以下、半正定値行列である証明です。
任意の実ベクトル
問題点として、逆行列が存在しない場合があります。その場合は漸化式により値を更新できなくなります。
また、
4. レーベンバーグ・マルカート法
ガウス・ニュートン法の問題点である "逆行列が存在しない場合がある" 点を改善したのがレーベンバーグ・マルカート法です。
レーベンバーグ・マルカート法は
以下、
任意の実ベクトル
また、以下の式から、レーベンバーグ・マルカート法はガウス・ニュートン法と最急降下法を混合したものといえます。
以下、
前提
-
を\lambda として漸化式から\lambda_{k}/\alpha を計算\boldsymbol{a^{(k+1)}} - 更新前の残差平方和
と更新後の残差平方和S(\boldsymbol{a^{(k)}}|\lambda_{k}) を比較S(\boldsymbol{a^{(k+1)}}|\lambda_{k+1}/\alpha) - 更新後の方が小さい
とき、\left( S(\boldsymbol{a^{(k+1)}}|\lambda_{k}/\alpha) \boldsymbol{\lt} S(\boldsymbol{a^{(k)}}|\lambda_{k}) \right)
更新後のパラメータは\to \lambda_{k+1}= \lambda_{k}/\alpha \boldsymbol{a^{(k+1)}}|\lambda_{k}/\alpha - 更新後の方が大きい
とき、\left( S(\boldsymbol{a^{(k+1)}}|\lambda_{k}/\alpha) \boldsymbol{\gt} S(\boldsymbol{a^{(k)}}|\lambda_{k})\right)
更新後の残差平方和の方が更新前よりも小さくなるまで以下の操作を繰り返す。\to
1. に\lambda をかける\alpha
2. を再計算\boldsymbol{a^{(k+1)}}
3. 更新前後の残差平方和を比較
を0以上の整数としてp の場合に初めて更新後の残差平方和の方が小さくなったとして\lambda_{k} \alpha^{p}
、更新後のパラメータは\lambda_{k+1} = \lambda_{k} \alpha^{p} \boldsymbol{a^{(k+1)}}|\lambda_{k}\alpha^{p}
簡潔に言えば、
まとめると、以下のようになります。
-
として計算したパラメータの残差平方和が、更新前よりも小さくなる場合は\lambda_{k}/\alpha を小さくする\lambda
⇒ ガウス・ニュートン法の比重を大きくする。 - 1.を満たさず
として計算したパラメータの残差平方和が更新前よりも小さくなる場合は\lambda_{k} はそのまま\lambda
⇒ 比重はそのまま -
として計算したパラメータの残差平方和が更新前よりも大きい場合は、\lambda_{k} を満たすまで、S(\boldsymbol{a^{(k+1)}}) \lt S(\boldsymbol{a^{(k)}}) に\lambda_{k} をかけ続け再計算\alpha を大きくする=\lambda
⇒ 最急降下法の比重を大きくする
例1. y = 1/(ax+b)+c の非線形回帰分析
例1として
以下の手順で計算を行います。
- パラメータ
の初期値の計算a,b,c -
の計算\nabla S(\boldsymbol{a^{(k)}}) -
を\lambda とする\lambda_{k}/\alpha - 漸化式
の行列(7) の計算M -
を計算\boldsymbol{a^{(k+1)}} -
の残差平方和\boldsymbol{a^{(k+1)}} を計算S(\boldsymbol{a^{(k+1)}}) -
となるまでS(\boldsymbol{a^{(k+1)}}) \lt S(\boldsymbol{a^{(k)}}) に\lambda をかけて 4-6 を繰り返す\alpha
を\to p 以上の整数として、-1 を満たす最小のS(\boldsymbol{a^{(k+1)}}|\lambda_{k}\alpha^{p}) \boldsymbol{\lt} S(\boldsymbol{a^{(k)}}) を求めるp -
、更新後のパラメータを\lambda_{k+1} = \lambda_{k} \alpha^{p} とする\boldsymbol{a^{(k+1)}}|\lambda_{k}\alpha^{p} -
を計算し、許容値以下なら終了、許容値より大きい場合は\nabla S(\boldsymbol{a^{(k+1)}}) をk+1 として、3 へ戻るk
1. パラメータ a,b,c の初期値の計算の補足
良好な計算結果を得るには、初期値を適切に設定する必要があります。初期値をそれぞれ
本例における非線形関数では、
手順は以下の通りです。
- 分数関数の形状は"上に凸 / 下に凸"、"増加 / 減少"で
通り存在2 \times 2 = 4
→ 減少、下に凸 ( の第1象限の形状) となるようy=1/x の符号を調整x,y -
とx の相関係数z=\dfrac{1}{y-c} が最大となる\rho_{xz} をニュートン法により求めるc_{0} -
とz=\dfrac{1}{y-c} の直線近似により線形の最小二乗法からx を求めるa_{0},b_{0}
詳細についてはこちらの記事をご覧ください。
相関係数を最大(実際には極大)にする
2. ∇S(a^(k)) の計算の補足
残差平方和
3. 漸化式の行列 M の計算の補足
以上より、
となります。
4. パラメータ更新の際の計算の補足
連立方程式の解の計算には numpy の linalg.slove 関数を使用します。
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-10 #許容値
lamb = 1 #λの初期値
alpha = 2 #αの値
lamb_lowlim = 1e-7 #λの下限値の設定
# ∇S(a)の全要素が許容値以下になるまで反復計算
while ( (abs(nabla_S[0]) >= error_limit) or (abs(nabla_S[1]) >= error_limit) or (abs(nabla_S[2]) >= error_limit) ):
# ヘッセ行列 =Jf^{T}Jf の各要素の計算
Haa = np.sum(f_a(a,x) **2)
Hab = np.sum(f_a(a,x) * f_b(a,x))
Hac = np.sum(f_a(a,x) * f_c(a,x))
Hbb = np.sum(f_b(a,x) **2)
Hbc = np.sum(f_b(a,x) * f_c(a,x))
Hcc = np.sum(f_c(a,x) **2)
# 行列化 = 二次元配列化
Hs = np.array([[Haa,Hab,Hac],[Hab,Hbb,Hbc],[Hac,Hbc,Hcc]])
# λをλ/αとする
lamb = lamb / alpha
# λI を加算
M = Hs + lamb * np.eye(3)
# 漸化式より係数 a の値を更新
a1 = a + np.linalg.solve(M,-nabla_S)
# 更新後、残差平方和が小さくなる場合、λと更新したパラメータはそのままλ=λ/α として、次の更新作業に
if sq_err > S(a1,x,y):
pass
# 更新後、残差平方和が大きくなる場合、
#更新後の残差平方和が更新前よりも小さくなるまでλにαをかけ続ける
else:
while(sq_err < S(a1,x,y)): #更新後の残差平方和が更新前よりも小さくなるまでλにαをかけ続ける
lamb = lamb * alpha
M = Hs + lamb * np.eye(3)
a1 = a + np.linalg.solve(M,-nabla_S)
a = a1
# 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)
# λが下限値以下になったら、下限値に再設定
if lamb <= lamb_lowlim:
lamb = lamb_lowlim
# 反復回数の更新
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キーで最初に戻ります。')
例2. y = a exp(bx) + c の回帰分析
例2として、
以下の手順で計算を行います。
- パラメータ
の初期値の計算a,b,c -
の計算\nabla S(\boldsymbol{a^{(k)}}) -
を\lambda とする\lambda_{k}/\alpha - 漸化式
の行列(7) の計算M -
を計算\boldsymbol{a^{(k+1)}} -
の残差平方和\boldsymbol{a^{(k+1)}} を計算S(\boldsymbol{a^{(k+1)}}) -
となるまでS(\boldsymbol{a^{(k+1)}}) \lt S(\boldsymbol{a^{(k)}}) に\lambda をかけて 4-6 を繰り返す\alpha
を\to p 以上の整数として、-1 を満たす最小のS(\boldsymbol{a^{(k+1)}}|\lambda_{k}\alpha^{p}) \boldsymbol{\lt} S(\boldsymbol{a^{(k)}}) を求めるp -
、更新後のパラメータを\lambda_{k+1} = \lambda_{k} \alpha^{p} とする\boldsymbol{a^{(k+1)}}|\lambda_{k}\alpha^{p} -
を計算し、許容値以下なら終了、許容値より大きい場合は\nabla S(\boldsymbol{a^{(k+1)}}) をk+1 として、3 へ戻るk
1. パラメータ a,b,c の初期値の計算の補足
例2では、
手順は以下の通りです。
- 指数関数の形状は "上に凸 / 下に凸"、"増加 / 減少"で
通り存在2 \times 2 = 4
→ 増加、下に凸 となるよう(a\gt 0,b \gt 0) の符号を調整x,y -
とx の相関係数z=\ln (y-c) が最大となる\rho_{xz} をニュートン法により求めるc_{0} -
とz=\ln (y-c) の直線近似により線形の最小二乗法からx を求めるa_{0},b_{0}
詳細についてはこちらの記事をご覧ください。
相関係数を最大(実際には極大)にする
2. ∇S(a^(k)) の計算の補足
残差平方和
3. 漸化式の行列 M の計算の補足
以上より、
となります。
4. パラメータ更新の際の計算の補足
例1と同様に、
連立方程式の解の計算には numpy の linalg.slove 関数を使用します。
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 math
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 = a[0]*np.exp(a[1]*x) + a[2]
return y
def f_a(a,x):
y = np.exp(a[1]*x)
return y
def f_b(a,x):
y = a[0]*x*np.exp(a[1]*x)
return y
def f_c(a,x):
y = np.ones(len(x))
return y
def S(a,x,y):
r = y - a[0]*np.exp(a[1]*x) - a[2]
return 0.5*np.sum(r**2)
def S_a(a,x,y):
r = np.exp(a[1]*x) * (a[0]*np.exp(a[1]*x) +a[2] -y)
return np.sum(r)
def S_b(a,x,y):
r = a[0]*x*np.exp(a[1]*x) * (a[0]*np.exp(a[1]*x) +a[2] - y)
return np.sum(r)
def S_c(a,x,y):
r = a[0]*np.exp(a[1]*x) +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)
# 増加関数、下に凸 (a>0,b>0)となるように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の計算
# 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 = np.log((y1-c))
#平均等を計算
x1_bar = np.average(x1)
z_bar = np.average(z)
exp_minusz = np.exp(-z)
exp_minusz_bar = np.average(np.exp(-z))
#g2(c)の計算
g2 = -np.sum(np.exp(-z)*(x1-x1_bar)) * np.sum((z-z_bar)**2) \
+ np.sum(z*(x1-x1_bar)) * np.sum(np.exp(-z)*(z-z_bar))
#g2'(c)の計算
g2_prime = -np.sum(np.exp(-2*z)*(x1-x1_bar)) * np.sum((z-z_bar)**2) \
+np.sum(np.exp(-z)*(x1-x1_bar)) * np.sum(np.exp(-z)*(z-z_bar)) \
+ np.sum(z*(x1-x1_bar)) * np.sum(np.exp(-2*z)*(z-z_bar -1+np.exp(z)*exp_minusz_bar))
#ニュートン法により値を更新
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=ln(y-c) と x1 を直線近似し、計算結果をa,bの初期値とする
c0 = c
z = np.log(y1-c)
# x1,z の平均値計算
x1_bar = np.average(x1)
z_bar = np.average(z)
# a,bの初期値a0,b0の計算
# 最小二乗法による係数の計算
slp = (np.dot(x1-x1_bar,z) / np.sum((x1-x1_bar) ** 2))
intcep = z_bar - x1_bar * slp
a0 = np.exp(intcep)
b0 = slp
# 符号の修正
a0 = sgn_y * a0
b0 = sgn_x * b0
c0 = sgn_y * c0
# 3係数を配列にまとめる
a = np.array([a0,b0,c0])
#a = np.array([-1,-1,-16]) #初期値を手動設定する場合
# 残差平方和の計算
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'反復回数:{rep}\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-8 #許容値
lamb = 1 #λの初期値
alpha = 2 #αの値
lamb_lowlim = 1e-6 #λの下限値の設定
# ∇S(a)の全要素が許容値以下になるまで反復計算
while ( (abs(nabla_S[0]) >= error_limit) or (abs(nabla_S[1]) >= error_limit) or (abs(nabla_S[2]) >= error_limit) ):
# ヘッセ行列 =Jf^{T}Jf の各要素の計算
Haa = np.sum(f_a(a,x) **2)
Hab = np.sum(f_a(a,x) * f_b(a,x))
Hac = np.sum(f_a(a,x) * f_c(a,x))
Hbb = np.sum(f_b(a,x) **2)
Hbc = np.sum(f_b(a,x) * f_c(a,x))
Hcc = np.sum(f_c(a,x) **2)
# 行列化 = 二次元配列化
Hs = np.array([[Haa,Hab,Hac],[Hab,Hbb,Hbc],[Hac,Hbc,Hcc]])
# λをλ/αとする
lamb = lamb / alpha
# λI を加算
M = Hs + lamb * np.eye(3)
# 漸化式より係数 a の値を更新
a1 = a + np.linalg.solve(M,-nabla_S)
# 更新後、残差平方和が小さくなる場合、λと更新したパラメータはそのままλ=λ/α として、次の更新作業に
if sq_err > S(a1,x,y):
pass
# 更新後、残差平方和が大きくなる場合、
# 更新後の残差平方和が更新前よりも小さくなるまでλにαをかけ続ける
else:
while(sq_err < S(a1,x,y)): #更新後の残差平方和が更新前よりも小さくなるまでλにαをかけ続ける
lamb = lamb * alpha
M = Hs + lamb * np.eye(3)
a1 = a + np.linalg.solve(M,-nabla_S)
a = a1
# 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)
# λが下限値以下になったら、下限値に再設定
if lamb <= lamb_lowlim:
lamb = lamb_lowlim
# 反復回数の更新
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キーで最初に戻ります。')
Discussion