。import numpy as np def kernel(X1, X2, l=1.0, sigma_f=1.0): ''' 各向同性平方指數內核. 計算點X1與點X2的協方差矩陣. Args: X1: ndArray, m個點 (m x d). X2: ndArray, n個點 (n x d). 返回: 協方差矩陣 (m x n). ''' sqdist = np.sum(X1**2, 1).reshape(-1, 1) + np.sum(X2**2, 1) - 2 * np.dot(X1, X2.T) return sigma_f**2 * np.exp(-0.5 / l**2 * sqdist) 定義先驗我們首先定義一個均值 0 和一個用內核參數 和 計算的協方差矩陣的先驗函數。
為了從該GP提取隨機函數,我們從相應的多元高斯分布 抽取隨機樣本。
以下示例繪製了三個隨機樣本,並將其與 0 均值和95%置信區間(根據協方差矩陣的對角線計算)一起繪製。
# 編寫繪圖函數 import matplotlib.pyplot as plt from matplotlib import cm from mpl_toolkits.mplot3d import Axes3D def plot_gp(mu, cov, X, X_train=None, Y_train=None, samples=[]): X = X.ravel() mu = mu.ravel() uncertainty = 1.96 * np.sqrt(np.diag(cov)) plt.fill_between(X, mu + uncertainty, mu - uncertainty, alpha=0.1) plt.plot(X, mu, label='均值') for i, sample in enumerate(samples): plt.plot(X, sample, lw=1, ls='--', label=f'樣本 {i+1}') if X_train is not None: plt.plot(X_train, Y_train, 'rx') plt.legend(bbox_to_anchor=(1.04,0.5), loc="center left") def plot_gp_2D(gx, gy, mu, X_train, Y_train, title, i): ax = plt.gcf().add_subplot(1, 2, i, projection='3d') ax.plot_surface(gx, gy, mu.reshape(gx.shape), cmap=cm.coolwarm, linewidth=0, alpha=0.2, antialiased=False) ax.scatter(X_train[:,0], X_train[:,1], Y_train, c=Y_train, cmap=cm.coolwarm) ax.set_title(title)%matplotlib inline # 有限個輸入數據點 X = np.arange(-5, 5, 0.2).reshape(-1, 1) # 先驗的均值與方差 mu = np.zeros(X.shape) cov = kernel(X, X) # 從先驗分布(多元高斯分布)中抽取樣本點 samples = np.random.multivariate_normal(mu.ravel(), cov, 3) # 畫出GP的均值, 置信區間 plot_gp(mu, cov, X, samples=samples) 基於無噪聲訓練數據進行預測為了計算充分統計量,即後驗預測分布的均值和協方差矩陣,我們用下面代碼實現公式(4)和(5)
# 倒入計算逆矩陣的函數inv() from numpy.linalg import inv def posterior_predictive(X_s, X_train, Y_train, l=1.0, sigma_f=1.0, sigma_y=1e-8): ''' 計算後驗分布的充分統計量 給定 m 個訓練數據 X_train 與 Y_train 給定 n 個新輸入數據 inputs X_s. Args: X_s: 新輸入數據 (n x d). X_train: 訓練輸入數據 (m x d). Y_train: 訓練輸出數據 (m x 1). l: 核函數的長度參數. sigma_f: 核函數的縱向波動參數. sigma_y: 噪音參數. 返回: 後驗均值向量 (n x d) 與協方差矩陣 (n x n). ''' K = kernel(X_train, X_train, l, sigma_f) + sigma_y**2 * np.eye(len(X_train)) K_s = kernel(X_train, X_s, l, sigma_f) K_ss = kernel(X_s, X_s, l, sigma_f) + 1e-8 * np.eye(len(X_s)) K_inv = inv(K) # 公式 (4) mu_s = K_s.T.dot(K_inv).dot(Y_train) # 公式 (5) cov_s = K_ss - K_s.T.dot(K_inv).dot(K_s) return mu_s, cov_s並將它們應用於無噪聲訓練數據X_train和Y_train。以下示例從後驗預測中提取3個樣本,並將它們與均值,置信區間和訓練數據一起繪製。在無噪聲模型中,訓練點的方差為 0
註: 從後驗預測分布提取的所有隨機函數都經過訓練點。
# 無噪音的5個輸入數據 X_train = np.array([-4, -3, -2, -1, 1]).reshape(-1, 1) # y=sin(x) Y_train = np.sin(X_train) # 計算後驗預測分布的均值向量與協方差矩陣 mu_s, cov_s = posterior_predictive(X, X_train, Y_train) # 從後驗預測分布中抽取3個樣本 samples = np.random.multivariate_normal(mu_s.ravel(), cov_s, 3) plot_gp(mu_s, cov_s, X, X_train=X_train, Y_train=Y_train, samples=samples) 基於帶有噪音的訓練數據進行預測如果模型中包含一些噪聲,則僅對訓練點進行近似,並且訓練點的方差不為零。
# 定義噪音參數 noise = 0.4 # 帶有噪音的訓練數據 X_train = np.arange(-3, 4, 1).reshape(-1, 1) Y_train = np.sin(X_train) + noise * np.random.randn(*X_train.shape) # 可以對比地看一下帶噪音的訓練數據與不帶噪音的訓練數據的區別 plt.figure() plt.plot(X_train, np.sin(X_train) + noise * np.random.randn(*X_train.shape), lw=1, ls='-.', label='有噪音') plt.plot(X_train, np.sin(X_train) + 0.0 * np.random.randn(*X_train.shape), lw=2.5, ls='-', label='無噪音') plt.plot(X_train, np.sin(X_train) + 0.0 * np.random.randn(*X_train.shape), 'rx') plt.legend(bbox_to_anchor=(1.04,0.5), loc="center left") plt.show()# 計算後驗預測分布的均值向量以及方差矩陣 mu_s, cov_s = posterior_predictive(X, X_train, Y_train, sigma_y=noise) # 從後驗預測分布中抽取3個樣本點 samples = np.random.multivariate_normal(mu_s.ravel(), cov_s, 3) plot_gp(mu_s, cov_s, X, X_train=X_train, Y_train=Y_train, samples=samples) 核函數參數和噪聲參數的影響以下示例顯示了核函數參數 和 以及噪聲參數 的影響。
值越高,函數越平滑,因此訓練數據的近似值越粗糙(與真實值誤差更大)。較低的 值使訓練數據點之間的置信區間較寬,使函數更不平穩,波動更大。(下圖第一行)
控制從GP提取的函數的垂直方向的波動性。 (下圖第二行)
表示訓練數據中的噪聲量。較高的 值會做出更粗略的近似,從而避免過度擬合噪聲數據。(下圖第三行)
params = [ (0.3, 1.0, 0.2), (3.0, 1.0, 0.2), (1.0, 0.3, 0.2), (1.0, 3.0, 0.2), (1.0, 1.0, 0.05), (1.0, 1.0, 1.5), ] plt.figure(figsize=(12, 5)) for i, (l, sigma_f, sigma_y) in enumerate(params): mu_s, cov_s = posterior_predictive(X, X_train, Y_train, l=l, sigma_f=sigma_f, sigma_y=sigma_y) plt.subplot(3, 2, i + 1) plt.subplots_adjust(top=2) plt.title(f'l = {l}, sigma_f = {sigma_f}, sigma_y = {sigma_y}') plot_gp(mu_s, cov_s, X, X_train=X_train, Y_train=Y_train)這些參數的最優值可以通過最大化由[1] [3]給出的邊際對數似然來得到:
在下面的代碼中,我們將最小化負邊際對數似然來獲得核函數參數 和 的參數估計。注意,這裡我們假設噪聲參數 是已知參數。from numpy.linalg import cholesky, det, lstsq from scipy.optimize import minimize def nll_fn(X_train, Y_train, noise, naive=True): ''' 基於給定數據X_train和Y_train以及噪聲水平 返回一個可以計算負邊際對數似然的函數 Args: X_train: 訓練輸入 (m x d). Y_train: 訓練輸出 (m x 1). noise: Y_train的噪聲水平. naive: 如果 True 那麼使用公式(7)來實現 如果 False 那麼使用數值方法來實現 返回: 最小的目標對象 ''' def nll_naive(theta): # 使用公式(7)來實現 # 與下面的nll_stable的實現相比在數值上不穩定 K = kernel(X_train, X_train, l=theta[0], sigma_f=theta[1]) + \ noise**2 * np.eye(len(X_train)) return 0.5 * np.log(det(K)) + \ 0.5 * Y_train.T.dot(inv(K).dot(Y_train)) + \ 0.5 * len(X_train) * np.log(2*np.pi) def nll_stable(theta): # 數值上更穩定,相比於nll_naive K = kernel(X_train, X_train, l=theta[0], sigma_f=theta[1]) + \ noise**2 * np.eye(len(X_train)) L = cholesky(K) return np.sum(np.log(np.diagonal(L))) + \ 0.5 * Y_train.T.dot(lstsq(L.T, lstsq(L, Y_train)[0])[0]) + \ 0.5 * len(X_train) * np.log(2*np.pi) if naive: return nll_naive else: return nll_stable # 求解可滿足最小化目標函數的參數 l 及 sigma_f. # 實際上,我們應該使用不同的初始化多次運行最小化,以避免局部最小化, # 但是為了簡單起見,此處將其跳過 res = minimize(nll_fn(X_train, Y_train, noise), [1, 1], bounds=((1e-5, None), (1e-5, None)), method='L-BFGS-B') # 將優化結果存儲在全局變量中,以便我們以後可以將其與其他實現的結果進行比較 l_opt, sigma_f_opt = res.x # 使用優化的核函數參數計算後驗預測分布的參數,並繪製結果圖 mu_s, cov_s = posterior_predictive(X, X_train, Y_train, l=l_opt, sigma_f=sigma_f_opt, sigma_y=noise) plot_gp(mu_s, cov_s, X, X_train=X_train, Y_train=Y_train)其最優化的核函數參數 和 的參數估計為
print(l_opt, sigma_f_opt)0.9872536793237083 0.8613778055591963 更高維的高斯過程以上實現也可以用於更高的輸入數據維度。此處,GP用於擬合在 二維平面上擴展的正弦波的帶噪聲樣本。
下圖顯示了核函數參數優化前後的帶噪聲樣本和後驗預測分布的均值向量。
#噪音參數 noise_2D = 0.1 rx, ry = np.arange(-5, 5, 0.3), np.arange(-5, 5, 0.3) gx, gy = np.meshgrid(rx, rx) X_2D = np.c_[gx.ravel(), gy.ravel()] X_2D_train = np.random.uniform(-4, 4, (100, 2)) Y_2D_train = np.sin(0.5 * np.linalg.norm(X_2D_train, axis=1)) + \ noise_2D * np.random.randn(len(X_2D_train)) plt.figure(figsize=(14,7)) mu_s, _ = posterior_predictive(X_2D, X_2D_train, Y_2D_train, sigma_y=noise_2D) plot_gp_2D(gx, gy, mu_s, X_2D_train, Y_2D_train, f'參數優化前: l={1.00} sigma_f={1.00}', 1) res = minimize(nll_fn(X_2D_train, Y_2D_train, noise_2D), [1, 1], bounds=((1e-5, None), (1e-5, None)), method='L-BFGS-B') mu_s, _ = posterior_predictive(X_2D, X_2D_train, Y_2D_train, *res.x, sigma_y=noise_2D) plot_gp_2D(gx, gy, mu_s, X_2D_train, Y_2D_train, f'參數優化後: l={res.x[0]:.2f} sigma_f={res.x[1]:.2f}', 2) 使用Scikit-learn實現高斯過程scikit-learn 提供了 GaussianProcessRegressor方法來實現GP回歸模型. 你可以自定義核函數,或者使用它內置的核函數. 核函數也可以疊加. Squared exponential 核函數在scikit-learn中也就是RBF. scikit-learn中的RBF只有長度參數 。為了引入參數 ,我們需要將 RBF 與 ConstantKernel 複合。
from sklearn.gaussian_process import GaussianProcessRegressor from sklearn.gaussian_process.kernels import ConstantKernel, RBF rbf = ConstantKernel(1.0) * RBF(length_scale=1.0) gpr = GaussianProcessRegressor(kernel=rbf, alpha=noise**2) # 1D 訓練樣本 gpr.fit(X_train, Y_train) # 計算後驗預測分布的均值向量與協方差矩陣 mu_s, cov_s = gpr.predict(X, return_cov=True) # 獲得最優核函數參數 l = gpr.kernel_.k2.get_params()['length_scale'] sigma_f = np.sqrt(gpr.kernel_.k1.get_params()['constant_value']) # 與前面手寫的結果比對 assert(np.isclose(l_opt, l)) assert(np.isclose(sigma_f_opt, sigma_f)) # 繪製結果 plot_gp(mu_s, cov_s, X, X_train=X_train, Y_train=Y_train) 小結從前面我們可以看出,與常見的機器學習模型不同,用高斯過程做預測的方法是直接生成一個後驗預測分布(依然是高斯分布)。
這也決定了我們可以不僅僅得到一個「光禿禿」的預測值,還可以得到關於這個預測值的不確定性信息 ,可以利用這些信息繪製error-bar等等!
從統計學的角度上來看,利用高斯過程模型做預測具有很高的價值。