はじめに
『パターン認識と機械学習』の独学時のまとめです。一連の記事は「数式の行間埋め」または「R・Pythonでのスクラッチ実装」からアルゴリズムの理解を補助することを目的としています。本とあわせて読んでください。
この記事は、3.1節「線形基底関数モデル」を補助する内容です。3つの基底関数(多項式基底関数・ガウス基底関数・シグモイド基底関数)をPythonで実装してグラフを作成します。
【他の節一覧】
【この節の内容】
3.1.0 基底関数
入力変数に非線形な変形を行う基底関数を可視化します。
利用するライブラリを読み込みます。
# 3.1.0項で利用するライブラリ import numpy as np import matplotlib.pyplot as plt from matplotlib.animation import FuncAnimation
パラメータとグラフの形状の関係をアニメーションで確認するのにanimation
モジュールを利用します。
線形回帰モデルを簡単に確認しておきます。
入力$x$に非線形な変換を与える関数を基底関数と呼びます。ここでは、基底関数を$\phi_j(x)$で表します。
$M$個の基底関数をまとめて次のように表記します。
ただし、0番目については$\phi_0(x) = 1$とします。
また、$M$個のパラメータをまとめて次のように表記します。
ただし、0番目の要素$w_0$はバイアスです。
基底関数によって変換を行った入力$\boldsymbol{\phi}(x)$とパラメータ$\mathbf{w}$との内積(積の和)を出力$y(x, \mathbf{w})$とします。
ただし、0番目の項は、$w_0 \phi_0(x) = w_0$になります。
この例では入力をスカラ$x$としますが、ベクトル$\mathbf{x} = (x_1, x_2, \cdots, x_n)^{\top}$の場合もあります。
この記事では、基底関数$\phi_j(x)$として用いられる3つの関数を確認します。
・1次元の場合
作図用の入力値$x$の点を作成します。
# 作図用のxの点を作成 x_vals = np.arange(-1.0, 1.01, 0.01) x_vals = np.linspace(-1.0, 1.0, 201)
np.arange()
またはnp.linspace()
を使ってx軸の値を作成します。
・多項式基底関数
多項式基底関数を実装して可視化します。
多項式基底関数を作成します。
# 多項式基底関数を作成 def phi_poly(x, j): return x**j
多項式基底関数は次の式で定義されます。
$j$を指定して、多項式基底関数のグラフを作成します。
# 値を指定 j = 3 # 多項式基底関数を作図 plt.figure(figsize=(12, 9)) plt.plot(x_vals, phi_poly(x_vals, j), label='j=' + str(j)) plt.xlabel('$x$') plt.ylabel('$\phi_j(x)$') plt.suptitle('Polynomial Basis Function', fontsize=20) plt.legend() plt.grid() plt.show()
$j$とグラフの形状の関係を確認します。
・作図コード(クリックで展開)
# 基底関数の数を指定 M = 10 # 多項式基底関数を作図 plt.figure(figsize=(12, 9)) for j in range(M): plt.plot(x_vals, phi_poly(x_vals, j), label='j=' + str(j)) plt.xlabel('$x$') plt.ylabel('$\phi_j(x)$') plt.suptitle('Polynomial Basis Function', fontsize=20) plt.legend() plt.grid() plt.show()
# 図を初期化 fig = plt.figure(figsize=(12, 9)) fig.suptitle('Polynomial Basis Function', fontsize=20) # 作図処理を関数として定義 def update(j): # 前フレームのグラフを初期化 plt.cla() # 多項式基底関数を作図 plt.plot(x_vals, phi_poly(x_vals, j), label='j=' + str(j)) plt.xlabel('$x$') plt.ylabel('$\phi_j(x)$') plt.legend(loc='lower right') plt.grid() plt.ylim(-1.1, 1.1) # gif画像を作成 anime_basis = FuncAnimation(fig, update, frames=M, interval=1000) # gif画像を保存 anime_basis.save('PolynomialBasisFunction1D.gif')
$j$が偶数のとき、マイナスが打ち消されるので、全ての範囲で$y \geq 0$となります。(いい感じにy軸の値が-1から1の値になっているのは、x軸の範囲が-1から1の値なためです。)
・ガウス基底関数
ガウス基底関数を実装して可視化します。
ガウス基底関数を作成します。
# ガウス基底関数を作成 def phi_gauss(x, mu, s): return np.exp(-(x - mu)**2 / (2.0 * s**2))
ガウス基底関数は次の式で定義されます。
指数関数$\exp(x)$について、$x < 0$のとき$0 < \exp(x) < 1$で、$\exp(0) = 1$です。また、分母分子がどちらも2乗なので、分数の項は常に0以上の値になります。よって、$\exp(\cdot)$の中は常に0以下の値になり、ガウス基底関数の出力は$0 < \phi_j(x) \leq 1$になります。
パラメータを指定して、ガウス基底関数のグラフを作成します。
# パラメータを指定 mu = 0.0 s = 0.2 # ガウス基底関数を作図 plt.figure(figsize=(12, 9)) plt.plot(x_vals, phi_gauss(x_vals, mu, s), label='$\mu=' + str(mu) + ', s=' + str(s) + '$') plt.xlabel('$x$') plt.ylabel('$\phi_j(x)$') plt.suptitle('Gaussian Basis Function', fontsize=20) plt.legend() plt.grid() plt.show()
正規化していないガウス分布の形になります。
パラメータとグラフの形状の関係を確認します。
・作図コード(クリックで展開)
# 基底関数の数を指定 M = 10 # パラメータを指定 mu_vals = np.linspace(-1.0, 1.0, M) #mu_vals = np.repeat(0.0, M) #s_vals = np.linspace(0.1, 1.0, M) s_vals = np.repeat(0.2, M) # ガウス基底関数を作図 plt.figure(figsize=(12, 9)) for j in range(M): plt.plot(x_vals, phi_gauss(x_vals, mu_vals[j], s_vals[j]), label='$\mu=' + str(np.round(mu_vals[j], 1)) + ', s=' + str(np.round(s_vals[j], 1)) + '$') plt.xlabel('$x$') plt.ylabel('$\phi_j(x)$') plt.suptitle('Gaussian Basis Function', fontsize=20) plt.legend(loc='upper right') plt.grid() plt.show()
# フレーム数を指定 M = 101 # パラメータを指定 #mu_vals = np.linspace(-1.0, 1.0, M) mu_vals = np.repeat(0.0, M) s_vals = np.linspace(0.1, 1.0, M) #s_vals = np.repeat(0.2, M) # 図を初期化 fig = plt.figure(figsize=(12, 9)) fig.suptitle('Gaussian Basis Function', fontsize=20) # 作図処理を関数として定義 def update(j): # 前フレームのグラフを初期化 plt.cla() # i回目のパラメータを取得 mu = mu_vals[j] s = s_vals[j] # ガウス基底関数を作図 plt.plot(x_vals, phi_gauss(x_vals, mu, s), label='$\mu=' + str(np.round(mu, 1)) + ', s=' + str(np.round(s, 1)) + '$') plt.xlabel('$x$') plt.ylabel('$\phi_j(x)$') plt.legend() plt.grid() plt.ylim(-0.1, 1.1) # gif画像を作成 anime_basis = FuncAnimation(fig, update, frames=M, interval=1000) # gif画像を保存 anime_basis.save('GaussianBasisFunction1D.gif')
$\mu_j$は、形状に影響せず、山の位置に影響します。$s_j$は、山の広がり具合に影響します。
・シグモイド基底関数
シグモイド基底関数を実装して可視化します。
シグモイド基底関数を作成します。
# シグモイド基底関数を作成 def phi_sigmoid(x, mu, s): a = (x - mu) / s return 1.0 / (1.0 + np.exp(-a))
シグモイド基底関数は、ロジスティックシグモイド関数
を用いて、次の式で定義されます。
ロジスティックシグモイド関数によって、シグモイド基底関数の出力は$0 \leq \phi_j(x) \leq 1$になります。
パラメータを指定して、シグモイド基底関数のグラフを作成します。
# パラメータを指定 mu = 0.0 s = 0.1 # シグモイド基底関数を作図 plt.figure(figsize=(12, 9)) plt.plot(x_vals, phi_sigmoid(x_vals, mu, s), label='$\mu=' + str(mu) + ', s=' + str(s) + '$') plt.xlabel('$x$') plt.ylabel('$\phi_j(x)$') plt.suptitle('Sigmoid Basis Function', fontsize=20) plt.legend() plt.grid() plt.show()
ロジスティッシグモイド関数の形になります。
パラメータとグラフの形状の関係を確認します。
・作図コード(クリックで展開)
# 基底関数の数を指定 M = 10 # パラメータを指定 mu_vals = np.linspace(-1.0, 1.0, M) #mu_vals = np.repeat(0.0, M) #s_vals = np.linspace(0.1, 1.0, M) s_vals = np.repeat(0.2, M) # シグモイド基底関数を作図 plt.figure(figsize=(12, 9)) for j in range(M): plt.plot(x_vals, phi_sigmoid(x_vals, mu_vals[j], s_vals[j]), label='$\mu=' + str(np.round(mu_vals[j], 1)) + ', s=' + str(np.round(s_vals[j], 1)) + '$') plt.xlabel('$x$') plt.ylabel('$\phi_j(x)$') plt.suptitle('Sigmoid Basis Function', fontsize=20) plt.legend() plt.grid() plt.show()
# フレーム数を指定 M = 101 # パラメータを指定 #mu_vals = np.linspace(-1.0, 1.0, M) mu_vals = np.repeat(0.0, M) s_vals = np.linspace(0.1, 1.0, M) #s_vals = np.repeat(0.2, M) # 図を初期化 fig = plt.figure(figsize=(12, 9)) fig.suptitle('Sigmoid Basis Function', fontsize=20) # 作図処理を関数として定義 def update(j): # 前フレームのグラフを初期化 plt.cla() # i回目のパラメータを取得 mu = mu_vals[j] s = s_vals[j] # シグモイド基底関数を作図 plt.plot(x_vals, phi_sigmoid(x_vals, mu, s), label='$\mu=' + str(np.round(mu, 1)) + ', s=' + str(np.round(s, 1)) + '$') plt.xlabel('$x$') plt.ylabel('$\phi_j(x)$') plt.legend() plt.grid() plt.ylim(-0.1, 1.1) # gif画像を作成 anime_basis = FuncAnimation(fig, update, frames=M, interval=1000) # gif画像を保存 anime_basis.save('SigmoidFunction1D.gif')
こちらも、$\mu_j$は山の位置に影響し、$s_j$はS字の曲がり具合に影響します。
・2次元の場合
続いて、2次元の場合を考えます。ただし、調べても見付けられなかったので以降はただの妄想です。誰か教えてください。
作図用の入力値$\mathbf{x} = (x_1, x_2)^{\top}$を作成します。3次元のグラフで描画するため$D = 2$とします。
# 作図用のxの値を指定 x_vals = np.linspace(-1.0, 1.0, 201) # 作図用のxの点を作成 X1, X2 = np.meshgrid(x_vals, x_vals) # 計算用のxの点を作成 x_points = np.stack([X1.flatten(), X2.flatten()], axis=1) x_dims = X1.shape print(x_points.shape) print(x_dims)
(40401, 2)
(201, 201)
np.meshgrid()
を使って、x_vals
の値の全ての組み合わせを持つX1, X2
を作成します。
また、X1, X2
をそれぞれ1列に並べたx_points
を作成します。
X1, X2
は作図に、x_points
は計算に利用します。
・多項式基底関数
2次元の多項式基底関数を実装して可視化します。
2次元多項式基底関数を作成します。
# 2次元多項式基底関数を作成 def phi_poly2d(x, j): # 全ての次元の和をとる return np.sum(x**j, axis=1)
2次元の多項式基底関数を次の式とします。
2次元多項式基底関数のグラフを作成します。
# 値を指定 j = 3 # 2次元多項式基底関数を計算 Z = phi_poly2d(x_points, j) # 2次元多項式基底関数を作図 fig = plt.figure(figsize=(12, 9)) ax = fig.add_subplot(projection='3d') # 3D用の設定 ax.plot_surface(X1, X2, Z.reshape(x_dims), cmap='jet') # 曲面 ax.contour(X1, X2, Z.reshape(x_dims), cmap='jet', offset=np.min(Z)) # 等高線 ax.set_xlabel('$x_1$') ax.set_ylabel('$x_2$') ax.set_zlabel('$\phi_j(x)$') ax.set_title('j=' + str(j), loc='left') fig.suptitle('Polynomial Basis Function', fontsize=20) plt.show()
$j$とグラフの形状の関係をアニメーションで確認します。
・作図コード(クリックで展開)
# 基底関数の数を指定 M = 10 # 図を初期化 fig = plt.figure(figsize=(12, 9)) ax = fig.add_subplot(projection='3d') # 3D用の設定 fig.suptitle('Polynomial Basis Function', fontsize=20) # 作図処理を関数として定義 def update(j): # 前フレームのグラフを初期化 plt.cla() # j番目の基底関数を計算 Z = phi_poly2d(x_points, j) # 2次元多項式基底関数を作図 ax.plot_surface(X1, X2, Z.reshape(x_dims), cmap='jet') # 曲面 ax.contour(X1, X2, Z.reshape(x_dims), cmap='jet', offset=-2.1) # 等高線 ax.set_xlabel('$x_1$') ax.set_ylabel('$x_2$') ax.set_zlabel('$\phi_j(x)$') ax.set_title('j=' + str(j), loc='left') ax.set_zlim(-2.1, 2.1) # gif画像を作成 anime_basis = FuncAnimation(fig, update, frames=M, interval=1000) # gif画像を保存 anime_basis.save('PolynomialBasisFunction2D.gif')
これはまぁこれでいいんじゃないでしょうか、知らんけど。
・ガウス基底関数
2次元のガウス基底関数を実装して可視化します。
2次元ガウス基底関数を作成します。
# 2次元ガウス基底関数を作成 def phi_gauss2d(x_d, mu_d, s_d): # 入力をパラメータで調整 a_d = -(x_d - mu_d)**2 / (2.0 * s_d**2) # 全ての次元の和をとる s = np.sum(a_d, axis=1) return np.exp(s)
2次元のガウス基底関数を次の式とします。
2次元多項式基底関数のグラフを作成します。
# パラメータを指定 mu_d = np.array([0.0, 0.0]) s_d = np.array([0.2, 0.2]) # 2次元ガウス基底関数を計算 Z = phi_gauss2d(x_points, mu_d, s_d) # 2次元ガウス基底関数を作図 fig = plt.figure(figsize=(12, 9)) ax = fig.add_subplot(projection='3d') # 3D用の設定 ax.plot_surface(X1, X2, Z.reshape(x_dims), cmap='jet') # 曲面 ax.contour(X1, X2, Z.reshape(x_dims), cmap='jet', offset=0.0) # 等高線 ax.set_xlabel('$x_1$') ax.set_ylabel('$x_2$') ax.set_zlabel('$\phi_j(x)$') ax.set_title('$\mu=(' + ', '.join([str(mu) for mu in mu_d]) + ')' + ', s=(' + ', '.join([str(s) for s in s_d]) + ')$', loc='left') fig.suptitle('Gaussian Basis Function', fontsize=20) plt.show()
パラメータとグラフの形状の関係をアニメーションで確認します。
・作図コード(クリックで展開)
# フレーム数を指定 M = 101 # パラメータを指定 #mu_vals = np.stack([np.linspace(-1.0, 1.0, M), np.linspace(-1.0, 1.0, M)], axis=1) mu_vals = np.stack([np.repeat(0.0, M), np.repeat(0.0, M)], axis=1) s_vals = np.stack([np.linspace(0.1, 1.0, M), np.linspace(0.1, 1.0, M)], axis=1) #s_vals = np.stack([np.repeat(0.2, M), np.repeat(0.2, M)], axis=1) # 図を初期化 fig = plt.figure(figsize=(12, 9)) ax = fig.add_subplot(projection='3d') # 3D用の設定 fig.suptitle('Gaussian Basis Function', fontsize=20) # 作図処理を関数として定義 def update(j): # 前フレームのグラフを初期化 plt.cla() # j番目のパラメータを取得 mu_d = mu_vals[j] s_d = s_vals[j] # j番目の基底関数を計算 Z = phi_gauss2d(x_points, mu_d, s_d) # 2次元ガウス基底関数を作図 ax.plot_surface(X1, X2, Z.reshape(x_dims), cmap='jet') # 曲面 ax.contour(X1, X2, Z.reshape(x_dims), cmap='jet', offset=-0.1) # 等高線 ax.set_xlabel('$x_1$') ax.set_ylabel('$x_2$') ax.set_zlabel('$\phi_j(x)$') ax.set_title('$\mu=(' + ', '.join([str(mu) for mu in np.round(mu_d, 1)]) + ')' + ', s=(' + ', '.join([str(s) for s in np.round(s_d, 1)]) + ')$', loc='left') ax.set_zlim(-0.1, 1.0) # gif画像を作成 anime_basis = FuncAnimation(fig, update, frames=M, interval=1000) # gif画像を保存 anime_basis.save('GaussianBasisFunction2D.gif')
上の図は、次元ごとに指定した範囲で最小値から最大値に変化するように$\mu_{j,d}$を設定しました。そのため、対角線に移動します。
次のようにすると、指定した範囲内で満遍なく設定できます。
・作図コード(クリックで展開)
# 基底関数の数の平方根を指定 M_sqrt = 3 # √M個のパラメータの値を指定 mu_vals = np.linspace(-1.0, 1.0, M_sqrt) s_vals = np.repeat(0.2, M_sqrt) # 格子点を作成 Mu1, Mu2 = np.meshgrid(mu_vals, mu_vals) S1, S2 = np.meshgrid(s_vals, s_vals) # 計算用のパラメータを作成 mu_jd = np.stack([Mu1.flatten(), Mu2.flatten()], axis=1) s_jd = np.stack([S1.flatten(), S2.flatten()], axis=1) # M個の2次元ガウス基底関数を作図 fig = plt.figure(figsize=(12, 9)) ax = fig.add_subplot(projection='3d') # 3D用の設定 for j in range(M_sqrt**2): # j番目のパラメータを取得 mu_d = mu_jd[j] s_d = s_jd[j] # j番目の基底関数を計算 Z = phi_gauss2d(x_points, mu_d, s_d) # 2次元ガウス基底関数を作図 ax.plot_surface(X1, X2, Z.reshape(x_dims), cmap='jet', alpha=0.5) # 曲面 ax.contour(X1, X2, Z.reshape(x_dims), cmap='jet', offset=0.0) # 等高線 ax.set_xlabel('$x_1$') ax.set_ylabel('$x_2$') ax.set_zlabel('$\phi_j(x)$') ax.set_title('$s=(' + ', '.join([str(s) for s in np.round(s_d, 1)]) + ')$', loc='left') fig.suptitle('Gaussian Basis Function', fontsize=20) plt.show()
(9枚のグラフを重ねて描画されているので残念な感じになっています。)
・シグモイド基底関数
2次元のシグモイド基底関数を実装して可視化します。
2次元シグモイド基底関数を作成します。
# 2次元シグモイド基底関数を作成 def phi_sigmoid2d(x_d, mu_d, s_d): # 入力をパラメータで調整 a_d = (x_d - mu_d) / s_d # ロジスティックシグモイド関数の計算 y_d = 1.0 / (1.0 + np.exp(-a_d)) # 全ての次元の平均を計算 return np.sum(y_d, axis=1) / 2.0
2次元のシグモイド基底関数を次の式とします。
2次元多項式基底関数のグラフを作成します。
# パラメータを指定 mu_d = np.array([0.0, 0.0]) s_d = np.array([0.1, 0.1]) # 2次元シグモイド基底関数を計算 Z = phi_sigmoid2d(x_points, mu_d, s_d) # 2次元シグモイド基底関数を作図 fig = plt.figure(figsize=(12, 9)) ax = fig.add_subplot(projection='3d') # 3D用の設定 ax.plot_surface(X1, X2, Z.reshape(x_dims), cmap='jet') # 曲面 ax.contour(X1, X2, Z.reshape(x_dims), cmap='jet', offset=0.0) # 等高線 ax.set_xlabel('$x_1$') ax.set_ylabel('$x_2$') ax.set_zlabel('$\phi_j(x)$') ax.set_title('$\mu=(' + ', '.join([str(mu) for mu in mu_d]) + ')' + ', s=(' + ', '.join([str(s) for s in s_d]) + ')$', loc='left') fig.suptitle('Sigmoid Basis Function', fontsize=20) plt.show()
パラメータとグラフの形状の関係をアニメーションで確認します。
・作図コード(クリックで展開)
# フレーム数を指定 M = 101 # パラメータを指定 mu_vals = np.stack([np.linspace(-1.0, 1.0, M), np.linspace(-1.0, 1.0, M)], axis=1) #mu_vals = np.stack([np.repeat(0.0, M), np.repeat(0.0, M)], axis=1) #s_vals = np.stack([np.linspace(0.1, 1.0, M), np.linspace(0.1, 1.0, M)], axis=1) s_vals = np.stack([np.repeat(0.1, M), np.repeat(0.1, M)], axis=1) # 図を初期化 fig = plt.figure(figsize=(12, 9)) ax = fig.add_subplot(projection='3d') # 3D用の設定 fig.suptitle('Sigmoid Basis Function', fontsize=20) # 作図処理を関数として定義 def update(j): # 前フレームのグラフを初期化 plt.cla() # j番目のパラメータを取得 mu_d = mu_vals[j] s_d = s_vals[j] # j番目の基底関数を計算 Z = phi_sigmoid2d(x_points, mu_d, s_d) # 2次元シグモイド基底関数を作図 ax.plot_surface(X1, X2, Z.reshape(x_dims), cmap='jet') # 曲面 ax.contour(X1, X2, Z.reshape(x_dims), cmap='jet', offset=-0.1) # 等高線 ax.set_xlabel('$x_1$') ax.set_ylabel('$x_2$') ax.set_zlabel('$\phi_j(x)$') ax.set_title('$\mu=(' + ', '.join([str(mu) for mu in np.round(mu_d, 1)]) + ')' + ', s=(' + ', '.join([str(s) for s in np.round(s_d, 1)]) + ')$', loc='left') ax.set_zlim(-0.1, 1.0) # gif画像を作成 anime_basis = FuncAnimation(fig, update, frames=M, interval=1000) # gif画像を保存 anime_basis.save('SigmoidBasisFunction2D.gif')
これが一番謎。
参考文献
- C.M.ビショップ著,元田 浩・他訳『パターン認識と機械学習 上下』,丸善出版,2012年.
おわりに
3次元のグラフにした方が良さそうな多クラスロジスティック回帰などをいくつかPythonでやろうと思ったら、基底関数を実装しなきゃいけないし、じゃもう丁寧にやってくかとスタートに戻ってきました。三歩進んで二歩下がる~~は理解を深めるにはたぶん必要なことだしこれまでもよくあったこと。
で、入力が多次元の回帰をやってみて詰まったのが、多次元の基底関数ってどう調べたらいいんだ?ガウス関数は少し出てきたけどシグモイドの方は見付けられなかった。シグモイド関数を多変量化したらソフトマックス関数だろうけど、次元間で操作する必要はないだろうし、、
雰囲気でやるよりは、基底関数を挟まずにやる方がいいかもしれません。
2022年1月7日は、モーニング娘。'22のサブリーダー石田亜佑美さんの25歳のお誕生日です!
サムネ左から3人目の方です。いつかあるかもしれないあゆみんリーダー体制楽しみだなぁ。
【次節の内容】
今組んでる明日上げたい。