源代码请参考:实验一 GitHub 仓库 运行效果请参考:主程序
哈尔滨工业大学计算学部 实验报告 《机器学习》 实验一:多项式拟合正弦函数 学号:1183710109 姓名:郭茁宁掌握最小二乘法求解(无惩罚项的损失函数),掌握增加惩罚项(2 范数)的损失函数优化,梯度下降法、共轭梯度法,理解过拟合、客服过拟合的方法(如增加惩罚项、增加样本)。
主要是利用 s i n ( 2 π x ) sin(2\pi x) sin(2πx)函数产生样本,其中 x x x均匀分布在 [ 0 , 1 ] [0, 1] [0,1]之间,对于每一个目标值 t = s i n ( 2 π x ) t=sin(2\pi x) t=sin(2πx)增加一个 0 0 0均值,方差为 0.25 0.25 0.25的高斯噪声。
def get_data( x_range: (float, float) = (0, 1), sample_num: int = 10, base_func=lambda x: np.sin(2 * np.pi * x), noise_scale=0.25, ) -> "pd.DataFrame": X = np.linspace(x_range[0], x_range[1], num=sample_num) Y = base_func(X) + np.random.normal(scale=noise_scale, size=X.shape) data = pd.DataFrame(data=np.dstack((X, Y))[0], columns=["X", "Y"]) return data利用训练集合,对于每个新的 x ^ \hat{x} x^,预测目标值 t ^ \hat{t} t^。采用多项式函数进行学习,即利用式 ( 1 ) (1) (1)来确定参数 w w w,假设阶数 m m m已知。
y ( x , w ) = w 0 + w 1 x + ⋯ + w m x m = ∑ i = 0 m w i x i (1) y(x, w) = w_0 + w_1x + \dots + w_mx^m = \sum_{i = 0}^{m}w_ix^i \tag{1} y(x,w)=w0+w1x+⋯+wmxm=i=0∑mwixi(1)
采用最小二乘法,即建立误差函数来测量每个样本点目标值 t t t与预测函数 y ( x , w ) y(x, w) y(x,w)之间的误差,误差函数即式 ( 2 ) (2) (2)
E ( w ) = 1 2 ∑ i = 1 N { y ( x i , w ) − t i } 2 (2) E(\bold{w}) = \frac{1}{2} \sum_{i = 1}^{N} \{y(x_i, \bold{w}) - t_i\}^2 \tag{2} E(w)=21i=1∑N{y(xi,w)−ti}2(2)
将上式写成矩阵形式如式 ( 3 ) (3) (3)
E ( w ) = 1 2 ( X w − T ) ′ ( X w − T ) (3) E(\bold{w}) = \frac{1}{2} (\bold{Xw} - \bold{T})'(\bold{Xw} - \bold{T})\tag{3} E(w)=21(Xw−T)′(Xw−T)(3)
其中 X = [ 1 x 1 ⋯ x 1 m 1 x 2 ⋯ x 2 m ⋮ ⋮ ⋱ ⋮ 1 x N ⋯ x N m ] , w = [ w 0 w 1 ⋮ w m ] , T = [ t 1 t 2 ⋮ t N ] \bold{X} = \left[ \begin{matrix} 1 & x_1 & \cdots & x_1^m \\ 1 & x_2 & \cdots & x_2^m \\ \vdots & \vdots & \ddots & \vdots \\ 1 & x_N & \cdots & x_N^m \\ \end{matrix} \right], \bold{w} = \left[ \begin{matrix} w_0 \\ w_1 \\ \vdots \\ w_m \end{matrix} \right], \bold{T} = \left[ \begin{matrix} t_1 \\ t_2 \\ \vdots \\ t_N \end{matrix} \right] X=⎣⎢⎢⎢⎡11⋮1x1x2⋮xN⋯⋯⋱⋯x1mx2m⋮xNm⎦⎥⎥⎥⎤,w=⎣⎢⎢⎢⎡w0w1⋮wm⎦⎥⎥⎥⎤,T=⎣⎢⎢⎢⎡t1t2⋮tN⎦⎥⎥⎥⎤ 通过将上式求导我们可以得到式 ( 4 ) (4) (4)
∂ E ∂ w = X ′ X w − X ′ T (4) \cfrac{\partial E}{\partial \bold{w}} = \bold{X'Xw} - \bold{X'T} \tag{4} ∂w∂E=X′Xw−X′T(4)
令 ∂ E ∂ w = 0 \cfrac{\partial E}{\partial \bold{w}}=0 ∂w∂E=0 我们有式 ( 5 ) (5) (5)即为 w ∗ \bold{w^*} w∗
w ∗ = ( X ′ X ) − 1 X ′ T (5) \bold{w^*} = (\bold{X'X})^{-1}\bold{X'T} \tag{5} w∗=(X′X)−1X′T(5)
由 ( 5 ) (5) (5)实现
def get_params(x_matrix, t_vec) -> [float]: return np.linalg.pinv(x_matrix.T @ x_matrix) @ x_matrix.T @ t_vec在不带惩罚项的多项式拟合曲线时,在参数多时 w ∗ \bold{w}^* w∗具有较大的绝对值,本质就是发生了过拟合。对于这种过拟合,我们可以通过在优化目标函数式 ( 3 ) (3) (3)中增加 w \bold{w} w的惩罚项,因此我们得到了式 ( 6 ) (6) (6)。
E ~ ( w ) = 1 2 ∑ i = 1 N { y ( x i , w ) − t i } 2 + λ 2 ∣ ∣ w ∣ ∣ 2 (6) \widetilde{E}(\bold{w}) = \frac{1}{2} \sum_{i=1}^{N} \{y(x_i, \bold{w}) - t_i\}^2 + \cfrac{\lambda}{2}|| \bold{w} || ^ 2 \tag{6} E (w)=21i=1∑N{y(xi,w)−ti}2+2λ∣∣w∣∣2(6)
同样我们可以将式 ( 6 ) (6) (6)写成矩阵形式, 我们得到式 ( 7 ) (7) (7)
E ~ ( w ) = 1 2 [ ( X w − T ) ′ ( X w − T ) + λ w ′ w ] (7) \widetilde{E}(\bold{w}) = \frac{1}{2}[(\bold{Xw} - \bold{T})'(\bold{Xw} - \bold{T}) + \lambda \bold{w'w}]\tag{7} E (w)=21[(Xw−T)′(Xw−T)+λw′w](7)
对式 ( 7 ) (7) (7)求导我们得到式 ( 8 ) (8) (8)
∂ E ~ ∂ w = X ′ X w − X ′ T + λ w (8) \cfrac{\partial \widetilde{E}}{\partial \bold{w}} = \bold{X'Xw} - \bold{X'T} + \lambda\bold{w} \tag{8} ∂w∂E =X′Xw−X′T+λw(8)
令 ∂ E ~ ∂ w = 0 \cfrac{\partial \widetilde{E}}{\partial \bold{w}} = 0 ∂w∂E =0 我们得到 w ∗ \bold{w^*} w∗即式 ( 9 ) (9) (9),其中 I \bold{I} I为单位阵。
w ∗ = ( X ′ X + λ I ) − 1 X ′ T (9) \bold{w^*} = (\bold{X'X} + \lambda\bold{I})^{-1}\bold{X'T}\tag{9} w∗=(X′X+λI)−1X′T(9)
由 ( 9 ) (9) (9)实现
def get_params_with_penalty(x_matrix, t_vec, lambda_penalty): return ( np.linalg.pinv( x_matrix.T @ x_matrix + lambda_penalty * np.identity(x_matrix.shape[1]) ) @ x_matrix.T @ t_vec )对于 f ( x ) f(\bold{x}) f(x)如果在 x i \bold{x_i} xi点可微且有定义,我们知道顺着梯度 ∇ f ( x i ) \nabla f(\bold{x_i}) ∇f(xi)为增长最快的方向,因此梯度的反方向 − ∇ f ( x i ) -\nabla f(\bold{x_i}) −∇f(xi) 即为下降最快的方向。因而如果有式 ( 10 ) (10) (10)对于 α > 0 \alpha > 0 α>0 成立,
x i + 1 = x i − α ∇ f ( x i ) (10) \bold{x_{i+1}}= \bold{x_i} - \alpha \nabla f(\bold{x_i}) \tag{10} xi+1=xi−α∇f(xi)(10)
那么对于序列 x 0 , x 1 , … \bold{x_0}, \bold{x_1}, \dots x0,x1,… 我们有 f ( x 0 ) ≥ f ( x 1 ) ≥ … f(\bold{x_0}) \ge f(\bold{x_1}) \ge \dots f(x0)≥f(x1)≥…
因此,如果顺利我们可以得到一个 f ( x n ) f(\bold{x_n}) f(xn) 收敛到期望的最小值,对于此次实验很大可能性可以收敛到最小值。
进而我们实现算法如下,其中 δ \delta δ为精度要求,通常可以设置为 δ = 1 × 1 0 − 6 \delta = 1\times10^{-6} δ=1×10−6:
def gradient_descent_fit(x_matrix, t_vec, lambda_penalty, w_vec_0, learning_rate=0.1, delta=1e-6): loss_0 = calc_loss(x_matrix, t_vec, lambda_penalty, w_vec_0) k = 0 w = w_vec_0 while True: w_ = w - learning_rate * calc_derivative(x_matrix, t_vec, lambda_penalty, w) loss = calc_loss(x_matrix, t_vec, lambda_penalty, w_) if np.abs(loss - loss_0) < delta: break else: k += 1 if loss > loss_0: learning_rate *= 0.5 loss_0 = loss w = w_ return k, w共轭梯度法解决的主要是形如 A x = b \bold{Ax} = \bold{b} Ax=b的线性方程组解的问题,其中 A \bold{A} A必须是对称的、正定的。大概来说,共轭梯度下降就是在解空间的每一个维度分别取求解最优解的,每一维单独去做的时候不会影响到其他维,这与梯度下降方法,每次都选泽梯度的反方向去迭代,梯度下降不能保障每次在每个维度上都是靠近最优解的,这就是共轭梯度优于梯度下降的原因。
对于第 k 步的残差 r k = b − A x k \bold{r_k = b - Ax_k} rk=b−Axk,我们根据残差去构造下一步的搜索方向 p k \bold{p_k} pk,初始时我们令 p 0 = r 0 \bold{p_0 = r_0} p0=r0。然后利用 G r a m − S c h m i d t Gram-Schmidt Gram−Schmidt方法依次构造互相共轭的搜素方向 p k \bold{p_k} pk,具体构造的时候需要先得到第 k+1 步的残差,即 r k + 1 = r k − α k A p k \bold{r_{k+1} = r_k - \alpha_kAp_k} rk+1=rk−αkApk,其中 α k \alpha_k αk如后面的式 ( 11 ) (11) (11)。
根据第 k+1 步的残差构造下一步的搜索方向 p k + 1 = r k + 1 + β k + 1 p k \bold{p_{k+1} = r_{k+1} + \beta_{k+1}p_k} pk+1=rk+1+βk+1pk,其中 β k + 1 = r k + 1 T r k + 1 r k T r k \beta_{k+1} = \bold{\cfrac{r_{k+1}^Tr_{k+1}}{r_k^Tr_k}} βk+1=rkTrkrk+1Trk+1。
然后可以得到 x k + 1 = x k + α k p k \bold{x_{k+1} = x_k + \alpha_kp_k} xk+1=xk+αkpk,其中 α k = p k T ( b − A x k ) p k T A p k = p k T r k p k T A p k \alpha_k = \cfrac{\bold{p_k}^T(\bold{b} - \bold{Ax_k})}{\bold{p_k}^T\bold{Ap_k}} = \cfrac{\bold{p_k}^T\bold{r_k}}{{\bold{p_k}^T\bold{Ap_k}}} αk=pkTApkpkT(b−Axk)=pkTApkpkTrk
对于第 k 步的残差 r k = b − A x \bold{r_k} = \bold{b} - \bold{Ax} rk=b−Ax, r k \bold{r_k} rk为 x = x k \bold{x} = \bold{x_k} x=xk时的梯度反方向。由于我们仍然需要保证 p k \bold{p_k} pk 彼此共轭。因此我们通过当前的残差和之前所有的搜索方向来构建 p k \bold{p_k} pk,得到式 ( 11 ) (11) (11)
p k = r k − ∑ i < k p i T A r k p i T A p i p i (11) \bold{p_k} = \bold{r_k} - \sum_{i < k}\cfrac{\bold{p_i}^T\bold{Ar_k}}{\bold{p_i}^T\bold{Ap_i}}\bold{p_i}\tag{11} pk=rk−i<k∑piTApipiTArkpi(11)
进而通过当前的搜索方向 p k \bold{p_k} pk得到下一步优化解 x k + 1 = x k + α k p k \bold{x_{k+1}} = \bold{x_k} + \alpha_k\bold{p_k} xk+1=xk+αkpk,其中 α k = p k T ( b − A x k ) p k T A p k = p k T r k p k T A p k \alpha_k = \cfrac{\bold{p_k}^T(\bold{b} - \bold{Ax_k})}{\bold{p_k}^T\bold{Ap_k}} = \cfrac{\bold{p_k}^T\bold{r_k}}{{\bold{p_k}^T\bold{Ap_k}}} αk=pkTApkpkT(b−Axk)=pkTApkpkTrk
求解的方法就是我们先猜一个解 x 0 \bold{x_0} x0,然后取梯度的反方向 p 0 = b − A x \bold{p_0} = \bold{b} - \bold{Ax} p0=b−Ax,在 n 维空间的基中 p 0 \bold{p_0} p0要与其与的基共轭并且为初始残差。
对于共轭梯度下降,算法实现如下,初始时取 w 0 = [ 0 0 ⋮ 0 ] \bold{w_0} = \left[ \begin{matrix} 0 \\ 0 \\ \vdots \\ 0 \end{matrix} \right] w0=⎣⎢⎢⎢⎡00⋮0⎦⎥⎥⎥⎤
由上文 ( 8 ) ( 9 ) (8)(9) (8)(9)得
( X ′ X + λ I ) w ∗ = X ′ T (12) (\bold{X'X} + \lambda\bold{I})\bold{w^*} = \bold{X'T}\tag{12} (X′X+λI)w∗=X′T(12)
令
{ A = X ′ X + λ I b = X ′ T (13) \left\{ \begin{array}{lr} \bold{A} = \bold{X'X} + \lambda\bold{I} & \\ \bold{b} = \bold{X'T} \end{array} \right.\tag{13} {A=X′X+λIb=X′T(13)
def switch_deri_func_for_conjugate_gradient(x_matrix, t_vec, lambda_penalty, w_vec): A = x_matrix.T @ x_matrix - lambda_penalty * np.identity(len(x_matrix.T)) x = w_vec b = x_matrix.T @ t_vec return A, x, b通过共轭梯度下降法求解 A x = b Ax=b Ax=b
def conjugate_gradient_fit(A, x_0, b, delta=1e-6): x = x_0 r_0 = b - A @ x p = r_0 k = 0 while True: alpha = (r_0.T @ r_0) / (p.T @ A @ p) x = x + alpha * p r = r_0 - alpha * A @ p if r_0.T @ r_0 < delta: break beta = (r.T @ r) / (r_0.T @ r_0) p = r + beta * p r_0 = r k += 1 return k, x解得 x x x,并记录迭代次数 k k k
固定训练样本的大小为 15,分别使用不同多项式阶数,测试:
我们可以看到在固定训练样本的大小之后,在多项式阶数为 3 时的拟合效果已经很好。继续提高多项式的阶数,尤其在阶数为 14 的时候曲线“完美的”经过了所有的节点,这种剧烈的震荡并没有很好的拟合真实的背后的函数 s i n ( 2 π x ) sin(2\pi x) sin(2πx),反而将所有噪声均很好的拟合,即表现出来一种过拟合的情况。
其出现过拟合的本质原因是,在阶数过大的情况下,模型的复杂度和拟合的能力都增强,因此可以通过过大或者过小的系数来实现震荡以拟合所有的数据点,以至于甚至拟合了所有的噪声。在这里由于我们的数据样本大小只有 15,所以在阶数为 14 的时候,其对应的系数向量 w \bold{w} w恰好有唯一解,因此可以穿过所有的样本点。
对于过拟合我们可以通过增加样本的数据或者通过增加惩罚项的方式来解决。增加数据集样本数量,使其超过参数向量的大小,就会在一定程度上解决过拟合问题。
固定多项式阶数,使用不同数量的样本数据,测试
可以看到在固定多项式阶数为 7 的情况下,随着样本数量逐渐增加,过拟合的现象有所解决。特别是对比左上图与右下图的差距,可以看到样本数量对于过拟合问题是有影响的。
首先根据式 ( 6 ) (6) (6)我们需要确定最佳的超参数 λ \lambda λ,因此我们通过根均方(RMS)误差来确定
观察上面的四张图, 我们可以发现对于超参数 λ \lambda λ的选择,在 ( e − 50 , e − 30 ) (e^{-50}, e^{-30}) (e−50,e−30)左右保持一种相对稳定的错误率;但是在 ( e − 30 , e − 5 ) (e^{-30}, e^{-5}) (e−30,e−5)错误率有一个明显的下降,所以下面在下面的完整 100 次实验中我们可以看到最佳参数的分布区间也大都在这个范围内;在大于 e − 5 e^{-5} e−5的区间内,错误率有一个急剧的升高。
对于不同训练集,超参数 λ \lambda λ的选择不同。 因此每生成新训练集,则通过带惩罚项的解析解计算出当前的 b e s t _ l a m b d a best\_lambda best_lambda,用于带惩罚项的解析解、梯度下降和共轭梯度下降。
一般来说,最佳的超参数范围在 ( e − 10 , e − 6 ) (e^{-10}, e^{-6}) (e−10,e−6)之间。
比较是否带惩罚项的拟合曲线
实验利用梯度下降(Gradient descent)和共轭梯度法(Conjugate gradient)两种方法来求优化解。由于该问题是有解析解存在,并且在之前的实验报告部分已经列出,所以在此处直接利用上面的解析解进行对比。此处使用的学习率固定为 l e a r n i n g _ r a t e = 0.01 learning\_rate = 0.01 learning_rate=0.01,停止的精度要求为 1 × 1 0 − 6 1 \times 10 ^ {-6} 1×10−6。
阶数训练集规模梯度下降迭代次数共轭梯度下降迭代次数31011093943209255143505603745103803845202286755501187885810813875820717437850474458首先在固定多项式阶数的情况下,随着训练样本的增加,梯度下降的迭代次数均有所下降,但是对于共轭梯度迭代次数变化不大。
其次在固定训练样本的情况下,梯度下降迭代次数的变化,对于 3 阶的情况下多于 8 阶的情况对于共轭梯度的而言,迭代次数仍然较少。
总的来说,对于梯度下降法,迭代次数在 10000 次以上;而对于共轭梯度下降,则需要的迭代次数均不超过 10 次(即小于解空间的维度 M)。
下面是不同阶数和不同训练集规模上,两种方法的比较
梯度下降和共轭梯度下降两种方法拟合结果相似,但共轭梯度下降收敛速度却远优于梯度下降、梯度下降稳定程度略优于共轭梯度下降。
3 阶,训练集规模 10
w w wAnalytical_Without_PenaltyAnalytical_With_PenaltyGradient_DescentConjugate_Gradient00.0868790.0868790.2214660.086879110.03064110.0306418.03774510.0306412-29.339163-29.339163-24.321837-29.339163318.91148018.91148015.65897718.9114805 阶,训练集规模 15
w w wAnalytical_Without_PenaltyAnalytical_With_PenaltyGradient_DescentConjugate_Gradient00.1790980.1790980.2529700.12728016.1866796.1866796.1209218.8025752-4.197192-4.197192-14.547026-24.0930283-47.562082-47.562082-0.7204646.293567473.58170873.5817086.17908413.4168705-28.074476-28.0744762.945509-4.3987288 阶,训练集规模 15
w w wAnalytical_Without_PenaltyAnalytical_With_PenaltyGradient_DescentConjugate_Gradient00.106875-0.090665-0.015363-0.3940361-3.3846959.2919017.88961218.583773237.377871-19.656246-16.016235-65.8705233451.762357-6.583293-3.96843854.9413024-3775.17251416.0873705.88567025.695923510453.33001110.9961647.857581-17.6301516-13828.893913-3.5538334.750929-30.26769078931.020178-9.096546-0.458928-12.0723428-2266.1318412.553330-5.97053327.07404115 阶,训练集规模 20
w w wAnalytical_Without_PenaltyAnalytical_With_PenaltyGradient_DescentConjugate_Gradient00.3331710.0567380.132317-0.3360801-43.8805597.4493536.43357214.72254521033.334292-13.398546-12.067511-36.8034283-8505.117304-8.201093-5.2564893.989214434811.0216603.0485362.14161817.2448915-75466.0685948.6125575.26895813.535997675267.4041538.5297045.1505085.0898057786.3041405.2208443.410453-2.2346878-53324.7242190.8841391.230193-6.6032449-2109.290031-2.981579-0.689982-8.0389831042045.345941-5.509223-1.994826-7.2250301114623.791181-6.291021-2.546300-4.95221712-33191.453129-5.219866-2.332749-1.90437013-22428.414650-2.369747-1.4102701.3994831438870.2022342.0866730.1332194.59806415-12368.8291717.9316682.1990927.459104源代码:实验一 GitHub 仓库
Jupyter Notebook 运行效果:主程序
import numpy as np import pandas as pd import seaborn as sns from matplotlib import pyplot as plt from DataGenerator import * from AnalyticalSolution import * from Switcher import * from Calculator import * from GradientDescent import * from ConjugateGradient import * %matplotlib inline sns.set(palette="Set2") # 超参数 TRAIN_NUM = 10 # 训练集规模 VALIDATION_NUM = 100 # 验证集规模 TEST_NUM = 1000 # 测试集规模 ORDER = 7 # 阶数 X_LEFT = 0 # 左界限 X_RIGHT = 1 # 右界限 NOISE_SCALE = 0.25 # 噪音标准差 LEARNING_RATE = 0.01 # 梯度下降学习率 DELTA = 1e-6 # 优化残差界限 W_SOLUTION = pd.DataFrame() # 不同方法的w解 LAMBDA = np.power(np.e, -7) def base_func(x): """原始函数 """ return np.sin(2 * np.pi * x) # 训练集 train_data = get_data( x_range=(X_LEFT, X_RIGHT), sample_num=TRAIN_NUM, base_func=base_func, noise_scale=NOISE_SCALE, ) X_TRAIN = train_data["X"] Y_TRAIN = train_data["Y"] # 验证集 X_VALIDATION = np.linspace(X_LEFT, X_RIGHT, VALIDATION_NUM) Y_VALIDATION = base_func(X_VALIDATION) # 测试集 X_TEST = np.linspace(X_LEFT, X_RIGHT, TEST_NUM) Y_TEST = base_func(X_TEST) train_data XY00.0000000.08744410.1111110.66782220.2222220.82602530.3333330.56355040.4444440.54817050.555556-0.38868760.666667-0.80397270.777778-1.04936880.888889-0.64851691.0000000.234352