2010.11. 7.
石立 喬
Visual C++ 2010 Express の易しい使い方(18)
―――― 最小二乗法により、測定データに多項式を当てはめる ――――
概要
実験結果をグラフに表示する時、測定点をプロットしてから、測定点に最も近い直線や二次曲線を描くことがある。これを自動的に行うプログラムを紹介する。この作業は「曲線の当てはめ」と呼ばれ、多元連立方程式を解く必要がある。
当てはめ
「曲線の当てはめ(Curve Fitting)」とは、測定データとの誤差の二乗の和が最小になるような、与えられた形式の式(実験式)を求めることを言う。「易しい使い方(17)」で紹介した「補間」が測定データを絶対視し、それを必ず通る曲線を描き、中間の値を推定したのに対し、「当てはめ」は、測定データからの誤差を最小にする式を求める。
一番単純な当てはめは直線(一次式)で近似するもので、その直線は「回帰直線」と呼ばれ、統計で良く用いられる。二次式や三次式を当てはめる方法もあるが、測定データは理論的に平方根、指数、冪などの形をとるべきものも多く、無理に高次の多項式に当てはめるのはあまり意味がない。
最小二乗法による多項式の当てはめ
当てはめは下記の手順による。
1) 目的とする一次式、二次式、三次式などの式を仮定する。
2) 二乗誤差の和を最小にするために、式の各係数a、b、c、dなどについて偏微分を求め、それを0と置く。
3) 二元(一次式の場合)、三元(二次式)、四元(三次式)などの連立方程式が得られる。
4) 上記の連立方程式を解く。ここでは、ピボット選択付きのGauss-Jordan法を利用する。
5) 連立方程式の解として得た各係数を用いてグラフを描く。
6) 式と二乗平均誤差を表示する。
当てはめに用いる連立方程式
◎一次式による当てはめ(回帰直線)の場合
当てはめたい式を
とすると、二乗誤差の和は、データをxi、yiとし、データ点の数を n として、
である。これを最小にする a および b の値は、

で求められる。したがって、解くべき連立方程式は、
である。
◎二次式による当てはめの場合
当てはめたい式を
とすると、途中を省略して、解くべき連立方程式は次のようになる。
◎三次式による当てはめの場合
当てはめたい式を

とすると、解くべき連立方程式は次のようになる。

連立方程式を解くプログラムと使用する配列
ここでは、一般に良く知られているGauss-Jordanの方法を用いた。なるべく精度を高め、0割りなどのエラーが生じないように、ピボット選択方式を採用した。
三次式による当てはめ、すなわち四元連立方程式を解く場合を例として下表に示す。上の式と比較して欲しい。sx6などは、xの6乗の累計の意味である。
a*aa[0,0] + b*aa[0,1] + c*aa[0,2] + d*aa[0,3] = aa[0,4]
a*aa[1,0] + b*aa[1,1] + c*aa[1,2] + d*aa[1,3] = aa[1,4]
a*aa[2,0] + b*aa[2,1] + c*aa[2,2] + d*aa[2,3] = aa[2,4]
a*aa[3,0] + b*aa[3,1] + c*aa[3,2] + d*aa[3,3] = aa[3,4]
のように配列aa[i,j]を割り当て、aa[0,1]=sx6; ----- aa[0,4]=sx3y; ----- aa[3,3]=data_number;
などと代入して、a、b、c、dを求める。
配列のみを取り出すと、下記のような行列ができる。
aa[0,0] aa[0,1] aa[0,2] aa[0,3] aa[0,4]
aa[1,0] aa[1,1] aa[1,2] aa[1,3] aa[1,4]
aa[2,0] aa[2,1] aa[2,2] aa[2,3] aa[2,4]
aa[3,0] aa[3,1] aa[3,2] aa[3,3] aa[3,4]
これを変形して
1 0 0 0 aa[0,4] (=b[0])
0 1 0 0 aa[1,4] (=b[1])
0 0 1 0 aa[2,4] (=b[2])
0 0 0 1 aa[3,4] (=b[3])
のようにするのが、Gauss-Jordan法であり、解は b[0] ~ b[3] に得られる。
なお、Gauss-Jordan法の原理を説明するために、プログラム中にコメント文をやや詳しく付けた。
プログラムの概要
◎「易しい使い方(17)」の補間で用いたプログラムを流用する。
下記に示すメソッドを流用した。これらについては、説明を省略する。
Form1_Paint()
Form1_MouseMove()
Form1_MouseDown()
buttonFittingCancel_Click() ---- buttonInterCancel_Click() と名称が異なるが、内容は同一
buttonDataCancel_Click()
◎あてはめプログラム
1)下表の関係に従って、配列を用意し、各係数を計算する。
あてはめる式 |
解くべき連立方程式 |
各係数の配列 |
解を受け取る配列 |
一次式 |
二元連立方程式 |
aa[0,0]~aa[1,2] |
bb[0]~bb[1] |
二次式 |
三元連立方程式 |
aa[0,0]~aa[2,3] |
bb[0]~bb[2] |
三次式 |
四元連立方程式 |
aa[0,0]~aa[3,4] |
bb[0]~bb[3] |
2)ユーザ定義メソッドcalculateGaussJordanを呼び出し、結果をa、b、cなどに代入する。
3)a、b、cなどを用いて、対応する直線(曲線)を描く。
4)式を表示する。
5)二乗誤差を計算して表示する。
プログラム
static int X0=50,X1=450;
static int Y0=60,Y1=500,Y2=540,Y3=560,Y4=580;
static int data_number=0;
static array<float>^ xdata=gcnew array<float>(20);
static array<float>^ ydata=gcnew array<float>(20);
private: System::Void buttonFirst_Click(System::Object^ sender, System::EventArgs^
e) {
Graphics^ g=this->CreateGraphics();
int i;
float x1,y1,x2,y2;
float y;
float sx=0.0f,sy=0.0f,sx2=0.0f,sxy=0.0f;
float error=0.0f;
float a,b;
array<float,2>^ aa=gcnew array<float,2>(2,3);
array<float>^ bb=gcnew array<float>(2);
//二元連立方程式の係数を計算
for(i=0;i<data_number;i++){
sx+=xdata[i];
sy+=ydata[i];
sx2+=xdata[i]*xdata[i];
sxy+=xdata[i]*ydata[i];
}
aa[0,0]=sx2; aa[0,1]=sx; aa[0,2]=sxy;
aa[1,0]=sx; aa[1,1]=data_number; aa[1,2]=sy;
//二元連立方程式を解く
bb=calculateGaussJordan(2,aa);
a=bb[0];
b=bb[1];
//直線式を描画
x1=xdata[0];
x2=xdata[data_number-1];
y1=a*x1+b;
y2=a*x2+b;
g->DrawLine(Pens::Blue,X0+10*x1,Y0+10*(40-y1),X0+10*x2,Y0+10*(40-y2));
if(b>=0)
g->DrawString("一次式 y=" +a+ "*x + " +b,Font,Brushes::Blue,X0,Y2);
else
g->DrawString("一次式 y=" +a+ "*x - " +(-b),Font,Brushes::Blue,X0,Y2);
//二乗誤差を表示
for(i=0;i<data_number;i++){
y=a*xdata[i]+b;
error+=(float)Math::Pow(ydata[i]-y,2);
}
g->DrawString("二乗誤差の合計=" +error,Font,Brushes::Blue,X1,Y2);
}
private: System::Void buttonSecond_Click(System::Object^ sender, System::EventArgs^
e) {
Graphics^ g=this->CreateGraphics();
int i;
float x,y;
float xx,yy,xx_old,yy_old;
float x1,x2,x3,x4;
float sx=0.0f,sx2=0.0f,sx3=0.0f,sx4=0.0f,sy=0.0f,sxy=0.0f,sx2y=0.0f,error=0.0f;
array<float,2>^ aa=gcnew array<float,2>(3,4);
array<float>^ bb=gcnew array<float>(4);
float a,b,c;
//三元連立方程式の係数を計算
for(i=0;i<data_number;i++){
x1=xdata[i];
x2=x1*xdata[i];
x3=x2*xdata[i];
x4=x3*xdata[i];
sx+=x1;
sx2+=x2;
sx3+=x3;
sx4+=x4;
sy+=ydata[i];
sxy+=x1*ydata[i];
sx2y+=x2*ydata[i];
}
aa[0,0]=sx4; aa[0,1]=sx3; aa[0,2]=sx2; aa[0,3]=sx2y;
aa[1,0]=sx3; aa[1,1]=sx2; aa[1,2]=sx; aa[1,3]=sxy;
aa[2,0]=sx2; aa[2,1]=sx; aa[2,2]=data_number; aa[2,3]=sy;
//三元連立方程式を解く
bb=calculateGaussJordan(3,aa);
a=bb[0];
b=bb[1];
c=bb[2];
//二次曲線を描画
for(x=xdata[0];x<=xdata[data_number-1];x+=0.01f){
y=a*x*x+b*x+c;
xx=X0+x*10;
yy=Y0+(40-y)*10;
if(x==xdata[0]){
xx_old=xx;
yy_old=yy;
}
else{
g->DrawLine(Pens::YellowGreen,xx_old,yy_old,xx,yy);
xx_old=xx;
yy_old=yy;
}
}
//二次式を表示
String^ string1;
if(b>=0){
if(c>=0) string1="二次式 y=" +a+ "x*x + " + b + "x + " + c;
else string1="二次式 y=" +a+ "x*x + " + b + "x - " +(-c);
}
else{
if(c>=0) string1="二次式 y=" +a+ "x*x - " +(-b)+ "x + " + c;
else string1="二次式 y=" +a+ "x*x - " +(-b)+ "x - " +(-c);
}
g->DrawString(string1,Font,Brushes::Green,X0,Y3);
//二乗誤差を表示
for(i=0;i<data_number;i++){
y=a*xdata[i]*xdata[i]+b*xdata[i]+c;
error+=(float)Math::Pow(ydata[i]-y,2);
}
g->DrawString("二乗誤差の合計=" +error,Font,Brushes::Green,X1,Y3);
}
private: System::Void buttonThird_Click(System::Object^ sender, System::EventArgs^
e) {
Graphics^ g=this->CreateGraphics();
int i;
float x,y;
float xx,yy,xx_old,yy_old;
float x1,x2,x3,x4,x5,x6;
float sx=0.0f,sx2=0.0f,sx3=0.0f,sx4=0.0f,sx5=0.0f,sx6=0.0f;
float sy=0.0f,sxy=0.0f,sx2y=0.0f,sx3y=0.0f,error=0.0f;
array<float,2>^ aa=gcnew array<float,2>(4,5);
array<float>^ bb=gcnew array<float>(5);
float a,b,c,d;
//四元連立方程式の係数を計算
for(i=0;i<data_number;i++){
x1=xdata[i];
x2=x1*xdata[i];
x3=x2*xdata[i];
x4=x3*xdata[i];
x5=x4*xdata[i];
x6=x5*xdata[i];
sx+=x1;
sx2+=x2;
sx3+=x3;
sx4+=x4;
sx5+=x5;
sx6+=x6;
sy+=ydata[i];
sxy+=x1*ydata[i];
sx2y+=x2*ydata[i];
sx3y+=x3*ydata[i];
}
aa[0,0]=sx6; aa[0,1]=sx5; aa[0,2]=sx4; aa[0,3]=sx3; aa[0,4]=sx3y;
aa[1,0]=sx5; aa[1,1]=sx4; aa[1,2]=sx3; aa[1,3]=sx2; aa[1,4]=sx2y;
aa[2,0]=sx4; aa[2,1]=sx3; aa[2,2]=sx2; aa[2,3]=sx; aa[2,4]=sxy;
aa[3,0]=sx3; aa[3,1]=sx2; aa[3,2]=sx; aa[3,3]=data_number; aa[3,4]=sy;
//四元連立方程式を解く
bb=calculateGaussJordan(4,aa);
a=bb[0];
b=bb[1];
c=bb[2];
d=bb[3];
//三次曲線を描画
for(x=xdata[0];x<=xdata[data_number-1];x+=0.01f){
y=a*x*x*x+b*x*x+c*x+d;
xx=X0+x*10;
yy=Y0+(40-y)*10;
if(x==xdata[0]){
xx_old=xx;
yy_old=yy;
}
else{
g->DrawLine(Pens::OrangeRed,xx_old,yy_old,xx,yy);
xx_old=xx;
yy_old=yy;
}
}
//三次式を表示
String^ string1;
if(b>=0){
if(c>=0){
if(d>=0) string1="三次式 y=" +a+ "x*x*x + " + b + "x*x + " + c + "x + " +d;
else string1="三次式 y=" +a+ "x*x*x + " + b + "x*x + " + c + "x - " +(-d);
}
else{
if(d>=0) string1="三次式 y=" +a+ "x*x*x + " + b + "x*x - " +(-c)+ "x + " +d;
else string1="三次式 y=" +a+ "x*x*x + " + b + "x*x - " +(-c)+ "x - " +(-d);
}
}
else{
if(c>=0){
if(d>=0) string1="三次式 y=" +a+ "x*x*x - " +(-b)+ "x*x + " + c + "x + " +d;
else string1="三次式 y=" +a+ "x*x*x - " +(-b)+ "x*x + " + c + "x - " +(-d);
}
else{
if(d>=0) string1="三次式 y=" +a+ "x*x*x - " +(-b)+ "x*x - " +(-c)+ "x + " +d;
else string1="三次式 y=" +a+ "x*x*x - " +(-b)+ "x*x - " +(-c)+ "x - " +(-d);
}
}
g->DrawString(string1,Font,Brushes::OrangeRed,X0,Y4);
//二乗誤差を表示
for(i=0;i<data_number;i++){
y=a*xdata[i]*xdata[i]*xdata[i]+b*xdata[i]*xdata[i]+c*xdata[i]+d;
error+=(float)Math::Pow(ydata[i]-y,2);
}
g->DrawString("二乗誤差の合計=" +error,Font,Brushes::OrangeRed,X1,Y4);
}
private: array<float>^ calculateGaussJordan(int n,array<float,2>^ aa){
float p,d,coef_max,temp;
int i,j,k;
int pivot;
for(k=0;k<n;k++){ //係数配列aa[i,j]の列(column)を横方向にkでスキャン
//k列で係数の絶対値が最大となる行をさがし、pivotに設定
coef_max=0.0;
for(i=k;i<n;i++){ //k番目からはじめて、行(row)を縦方向にiでスキャン
if(Math::Abs(aa[i,k])>coef_max){
coef_max=Math::Abs(aa[i,k]);
pivot=i;
}
}
for(j=0;j<=n;j++){ //横方向にjでスキャンして、aa[k,j]とaa[pivot,j]を交換
temp=aa[k,j];
aa[k,j]=aa[pivot,j];
aa[pivot,j]=temp;
}
//aa[k,k]が1になるように、横方向にjでスキャンして、aa[k,k]~aa[k,n]をaa[k,k]で割る
p=aa[k,k];
for(j=k;j<=n;j++)
aa[k,j]/=p;
//列を縦方向にiでスキャンして、aa[i,k]が0になるようにする
for(i=0;i<n;i++){
if(i!=k){
d=aa[i,k];
for(j=k;j<=n;j++)
aa[i,j]-=d*aa[k,j];
}
}
}
//解を配列bb[n]に入れて返す
array<float>^ bb=gcnew array<float>(n);
for(i=0;i<n;i++)
bb[i]=aa[i,n];
return bb;
}
プログラムの実行結果の一例
図1において、黒丸はマウスで入力したデータ、青は「一次式」、緑は「二次式」、赤は「三次式」のボタンをそれぞれクリックした結果である。「方眼紙」の下は、各線の式と二乗誤差が表示されている。一次式から三次式に次数が高くなるほど誤差が減少している。「方眼紙」の下にはマウスが指している座標が表示されるが、この例では、マウスが「方眼紙」外にあったため「Out of Range」になっている。
図1 得られた結果の一例
「Visual C++ の勉強部屋」(目次)へ