線形基底関数モデル
前回までは 25歳以下に限定した年齢と体重のデータを使っていました。今回は年齢制限をなくして、幅広い年齢層から 20人を無作為抽出したデータを使用します。
# Linear_basis_function_models
# In[1]
import numpy as np
import matplotlib.pyplot as plt
# 配列の表示設定
np.set_printoptions(precision=3, floatmode="fixed")
# 20人の年齢と体重のデータセット
x = np.array([35, 16, 22, 43, 5,
66, 20, 13, 52, 1,
39, 62, 45, 33, 8,
28, 71, 24, 18, 3])
y = np.array([85.19, 58.93, 64.27, 68.91, 21.27,
68.88, 60.07, 55.18, 88.08, 8.89,
82.31, 81.18, 80.76, 78.98, 37.55,
75.9, 72.39, 69.51, 62.04, 12.47])
まずは与えられたデータをプロットしてみます。
# In[2]
# FigureとAxesの設定
fig = plt.figure(figsize=(8, 6))
ax = fig.add_subplot(111)
ax.set_title("Age vs. Weight", fontsize=16)
ax.grid()
ax.set_xlim([0, 70])
ax.set_ylim([0, 100])
ax.set_xlabel("Age", fontsize=14)
ax.set_ylabel("Weight [kg]", fontsize=14)
# 身長と体重データの散布図
ax.scatter(x, y, color="blue")
plt.show()
このグラフを直線で近似するのは明らかに無理があります。人間の身体的成長には限界があるので、ある年齢で体重の増加が頭打ちになることは常識的観点からもわかります。そこで、上のグラフを近似できるような別の関数を探すことにします。
基底関数モデル
とはいえ、「どのような関数をもってくればデータにフィットさせることができるのか」を判断することは難しいので、いくつかの関数を組合わせて近似曲線をつくることを考えます。すなわち、基底関数とよばれる
を用意して、各関数に重みをつけて足し合わせ、
という関数を近似関数とします。基底関数の選択に関しては、扱う問題ごとにプログラマーに委ねられますが、判断しにくい場合は多項式や ガウス関数 などの汎用性の高い関数を与えておきます。係数
パラメータ
ここでは記述を簡単にするために、
という関数を用意します。常に
と表すことができます。入力データベクトルに対しては、
となります。ここで、
というベクトルを定義すると、
と表せます。
を最小にするようにパラメータ
となります。
これを行列形式で書き直すと次のようになります。
記述を簡単にするために
基底関数行列
のように定義すると、式 (11) は
と表すことができます。両辺に左側から
となります。
多項式近似
曲線を
を選んで、データを二次関数
で近似するようなパラメータ
# In[3]
# 疑似逆行列関数
def pseudo_inverse(x):
return np.linalg.inv(x.T @ x) @ x.T
# φ0とφ1の設定
def phi_0(x):
return x ** 2
def phi_1(x):
return x
# 基底関数行列Φ
Phi = np.array([phi_0(x), phi_1(x), np.ones(len(x))]).T
# Φの疑似逆行列を求める
pi_Phi = pseudo_inverse(Phi)
# パラメータベクトルの決定
p = pi_Phi @ y
# 近似曲線の係数を表示
print("p = {}".format(p))
# 平均2乗誤差mseを計算
fx = p[0]*x**2 + p[1]*x + p[2]
mse = np.mean((fx-y)**2)
# 標準偏差SDを表示
print("SD = {0:.2f} kg".format(np.sqrt(mse)))
# FigureとAxesの設定
fig = plt.figure(figsize=(8, 6))
ax = fig.add_subplot(111)
ax.set_title("Age vs. Weight", fontsize=16)
ax.grid()
ax.set_xlim([0, 70])
ax.set_ylim([0, 100])
ax.set_xlabel("Age", fontsize=14)
ax.set_ylabel("Weight [kg]", fontsize=14)
# 身長と体重データの散布図
ax.scatter(x, y, color="blue")
# 近似曲線の描画
x2 = np.linspace(0, 70, 100)
y2 = p[0]*x2**2 + p[1]*x2 + p[2]
ax.plot(x2, y2, color="red")
plt.show()
# p = [-0.0343 3.2275 10.0832]
# SD = 5.68 kg
標準偏差 SD はフィッティング精度を示す値で、この値が大きいほど実際のデータと近似曲線のずれが大きいことを意味します。多項式近似からは少し離れてしまいますが、3 つめの基底関数として対数関数
# In[4]
# 3つめの基底関数
def phi_2(x):
return np.log(x)
# 基底関数行列Φ
Phi = np.array([phi_0(x), phi_1(x), phi_2(x), np.ones(len(x))]).T
# Φの疑似逆行列
pi_Phi = pseudo_inverse(Phi)
# パラメータベクトルの決定
p = pi_Phi @ y
# 係数を表示
print("p = {}".format(p))
# 平均2乗誤差mseを計算
fx = p[0] * phi_0(x) + p[1] * phi_1(x) + p[2] * phi_2(x) + p[3]
mse = np.mean((fx-y)**2)
# 標準偏差SDを表示
print("SD = {0:.2f} kg".format(np.sqrt(mse)))
# FigureとAxesの設定
fig = plt.figure(figsize=(8, 6))
ax = fig.add_subplot(111)
ax.set_title("Age vs. Weight", fontsize=16)
ax.grid()
ax.set_xlim([0, 70])
ax.set_ylim([0, 100])
ax.set_xlabel("Age", fontsize=14)
ax.set_ylabel("Weight [kg]", fontsize=14)
# 身長と体重データの散布図
ax.scatter(x, y, color="blue")
# 近似曲線を描画
x2 = np.linspace(1, 70, 100)
y2 = p[0] * phi_0(x2) + p[1] * phi_1(x2) + p[2] * phi_2(x2) + p[3]
ax.plot(x2, y2, color="red")
plt.show()
# p = [-0.0243 2.0948 9.5511 2.0261]
# SD = 4.90 kg
基底関数の組合わせ方は無数にあるので、最適な近似関数を見つけることは事実上不可能ですが、今後の記事でオーバーフィッテングの問題と絡めて、より良いモデルを選択する方法(ベストではなくベターなモデルに改良していく方法)について解説していく予定です。
コメント
下記は誤植と思われますので、ご確認ください。
(5) 式の右辺で、+a_0[ ]+ → +a_1[ ]+
(12) 式の左の式で、太字の「Φ」→ 細字の「Φ」
ありがとうございます。
確かに太字になってました。
訂正しておきました。
p[0], p[1], p[2] に 0.5~1.0 の係数をかけて、近似の様子がどのように変わるのかを見てみると、放物線と原点を通る直線とy切片の組み合わせを変えて近似している様子がわかって面白かったです。逆に (x , y) のデータとして3点 (両端とピーク値)しか与えなかったときに、SD = 0.00できれいな放物線が描かれたときにはびっくりしました。
3 点を与えると放物線は完全に形が定まります。言い方を変えると、異なる 3 点を通る放物線が必ず存在します。線形回帰は SD を最小にしようとするので、データが 3 点しかなければ、近似曲線として、与えられた 3 点を通る放物線を選択します。