自学内容网 自学内容网

matplotlib可视化梯度下降

引言

本文主要基于numpy来进行梯度下降的可视化观察,梯度下降本质上是一种迭代技术,它试图从随机猜测开始,为给定模型和数据点找到最佳可能的参数集。

为什么要基于numpy而不直接使用pytorch?

主要是因为pytorch是一个高度封装的框架,对于初学者来说编写代码可能高效,但并不方便理解。 像误差、损失、梯度、参数更新等可能就是一句代码调用,但要真正理解它背后怎么做到的以及为什么这么做,却并不容易。
我们基于numpy来纯手工实现,就可以对梯度下降过程中的每一步都有更加直观的认识。

1. 模型

为了专注于梯度下降的内部工作原理,具有单个特征x的线性回归最适合初学者来练手。

因为模型足够简单,所以我们就能把注意力放在误差、损失、梯度下降、学习率等更基础重要的概念上。

线性回归本质上就是个简单的一元一次方程:

y = b + w x + ϵ \Large y = b + w x + \epsilon y=b+wx+ϵ
在这个模型中,使用特征x来预测y的值,该模型包含3个元素:

  • 参数b:偏差(或截矩),它告诉我们当x为0时y的预期平均值。
  • 参数w:权重,它告诉我们当x增加1时y平均增加了多少。
  • 噪声 ϵ \epsilon ϵ:它表示我们预测y时无法预料到的随机误差。

引入误差是为了模拟真实世界的数据,因为真实情况下,所有的测量数据都会有误差。

2. 数据准备

2.1 数据生成

为了简单,这里就不引入什么语料库了,直接采用随机数生成特征x和标签y。

  • 定义真实的参数值true_w和true_b用以生成训练数据。
  • x采用均匀分布,生成在[0, 1]之间。
  • 指定随机数种子seed的目的是为了能复现结果,只要使用相同的seed,再次运行或其他人运行能够生成相同的数。
  • epsilon: 引入误差,采用均值为0标准差为1的标准正态分布,0.1为噪声因子可以用来调整噪声程度。
  • 将特征x和参数b、w代入方程,就可以得到标签y。

实际业务中并不知道真实的参数值,我们这个场景由于模型足够简单,所以可以预先知道真实的参数值。

import numpy as np

# 定义真实参数值和数据集大小
true_w = 2
true_b = 1
N = 100

np.random.seed(42)
x = np.random.rand(N, 1)
epsilon = 0.1 * np.random.randn(N, 1)
y = true_b + true_w * x + epsilon
x.shape, y.shape, x[0], y[0], epsilon[0]

((100, 1),
 (100, 1),
 array([0.37454012]),
 array([1.75778494]),
 array([0.00870471]))
2.2 数据预处理

需要对数据进行两方面的处理:

  1. 打乱数据集:对数据集打乱顺序,确保数据随机,目的是提高梯度下降的性能。
  2. 数据集拆分:前80%为训练集,后20%为测试集,拆分数据集一定要放在数据预处理和转换之前,原则是尽早拆分,防止测试数据提前泄漏给模型。
# 定义一系列索引数据的下标,并打乱索引顺序
idx = np.arange(N)
np.random.shuffle(idx)

# 数据集拆分,80%用于训练,20%用于测试
ratio = int(0.8 * N)
train_idx = idx[:ratio]
test_idx = idx[ratio:]
x_train, y_train = x[train_idx], y[train_idx]
x_test, y_test = x[test_idx], y[test_idx]

idx, train_idx, test_idx, x_train.shape, x_test.shape, y_train.shape, y_test.shape
(array([30, 65, 64, 53, 45, 93, 91, 47, 10,  0, 18, 31, 88, 95, 77,  4, 80,
        33, 12, 26, 98, 55, 22, 76, 44, 72, 15, 42, 40,  9, 85, 11, 51, 78,
        28, 79,  5, 62, 56, 39, 35, 16, 66, 34,  7, 43, 68, 69, 27, 19, 84,
        25, 73, 49, 13, 24,  3, 17, 38,  8, 81,  6, 67, 36, 90, 83, 54, 50,
        70, 46, 99, 61, 14, 96, 41, 58, 48, 89, 57, 75, 32, 97, 59, 63, 92,
        37, 29,  1, 52, 21,  2, 23, 87, 94, 74, 86, 82, 20, 60, 71]),
 array([30, 65, 64, 53, 45, 93, 91, 47, 10,  0, 18, 31, 88, 95, 77,  4, 80,
        33, 12, 26, 98, 55, 22, 76, 44, 72, 15, 42, 40,  9, 85, 11, 51, 78,
        28, 79,  5, 62, 56, 39, 35, 16, 66, 34,  7, 43, 68, 69, 27, 19, 84,
        25, 73, 49, 13, 24,  3, 17, 38,  8, 81,  6, 67, 36, 90, 83, 54, 50,
        70, 46, 99, 61, 14, 96, 41, 58, 48, 89, 57, 75]),
 array([32, 97, 59, 63, 92, 37, 29,  1, 52, 21,  2, 23, 87, 94, 74, 86, 82,
        20, 60, 71]),
 (80, 1),
 (20, 1),
 (80, 1),
 (20, 1))
2.3 可视化数据集

使用matplotlib的散点图来可视化数据集,这样我们就能直观地看到数据集的分布情况。

相关matplotlib函数说明如下:

  • plt.subplots()函数用于创建子图,它返回一个包含两个元素的元组。第一个元素是figure对象,第二个元素是axes对象。
  • plt.scatter()函数用于绘制散点图,它接收两个参数:第一个参数是x轴上的数据,第二个参数是y轴上的数据。
  • plt.show()函数用于显示图像。
import matplotlib.pyplot as plt

def show_data_distribution(x_train, y_train, x_test, y_test):
    fig, ax = plt.subplots(1, 2, figsize=(12,6))

    ax[0].scatter(x_train, y_train)
    ax[0].set_xlabel("x")
    ax[0].set_ylabel("y")
    ax[0].set_ylim([0, 3.1])
    ax[0].set_title("train data generation")

    ax[1].scatter(x_test, y_test, color='r')
    ax[1].set_xlabel("x")
    ax[1].set_ylabel("y")
    ax[1].set_ylim([0, 3.1])
    ax[1].set_title("test data generation")
    fig.tight_layout()
    plt.show()

show_data_distribution(x_train, y_train, x_test, y_test)

在这里插入图片描述

3. 计算预测值

3.1 参数初始化

在我们的例子中已经知道了参数的真实值,但这永远不会发生在现实世界中。我们希望通过训练数据来学习参数,然后使用参数来预测未来的值。

假设永远不知道参数的真实值,在训练之前首先需要为这些参数设置初始值,这里使用N(0,1)标准正态分布来随机初始化参数。


np.random.seed(42)
b = np.random.randn(1)
w = np.random.randn(1)
b, w
(array([0.49671415]), array([-0.1382643]))
3.2 计算模型预测

使用上一步初始化的参数来预测结果,本质上就是将特征x、参数b、参数w代入方程来计算标签y_hat, 并计算与标签值y之间的误差,这个过程也被称为前向传播

y_hat = b + w * x_train
error = y_hat - y_train   # 误差
y_hat.shape, error[0], y_hat[0], y_train[0]
((80, 1), array([-1.99099591]), array([0.41271239]), array([2.40370829]))
3.3 可视化误差

我们将误差在图上绘制出来,以便更直观感受预测值与真实值的误差有多少。

由于前面是随机初始化的参数,这里还未经过训练,所以误差应该比较大。

用到的matplotlib函数如下:

  1. ax.annotate: 在指定坐标显示注释文本
  2. ax.plot: 绘制坐标点构成的线图
  3. ax.legend: 在坐标轴上给各个线条添加Label标签
  4. ax.arrow: 在指定位置绘制箭头
def show_gradient_descent(x_train, y_train, y_hat):
    fig, ax = plt.subplots(1,1, figsize=(6,6))

    ax.set_xlabel("x")
    ax.set_ylabel("y")
    ax.set_title("Prediction before training")
    ax.scatter(x_train, y_train, color='b', label='labels', marker='.')
    ax.scatter(x_train, y_hat, color='g', marker='.')
    ax.plot(x_train, y_hat, label="model's prediction", color='g', linestyle='--')
    ax.annotate(f"w={w[0]:.4f}, b={b[0]:.4f}", xy=(0.2, 0.55), color='g')
    
    x0, y0, y_hat0 = x_train[0][0], y_train[0][0], y_hat[0][0]
    ax.plot([x0, x0], [y0, y_hat0], color='r', linestyle='--', linewidth=1)
    ax.arrow(x0, y0-0.03, 0, 0.03, color='r', shape='full', length_includes_head=True, head_width=.03, lw=0)
    ax.arrow(x0, y_hat0+0.03, 0, -0.03, color='r', shape='full', length_includes_head=True, head_width=.03, lw=0)
    ax.annotate(f"error={y0-y_hat0:.4f}", xy=(x0+0.02, (y0+y_hat0)/2), color='r')
    ax.annotate(f"({x0:.4f},{y0:.4f})", xy=(x0+0.02, y0), color='b')
    ax.annotate(f"({x0:.4f},{y_hat0:.4f})", xy=(x0-0.1, y_hat0-0.1), color='g')

    ax.legend(loc=0)
    fig.tight_layout()
    plt.show()

show_gradient_descent(x_train, y_train, y_hat)

在这里插入图片描述

图中红线只标注了x=0.7713这一个点上预测值和真实值之间的误差,其实每个点上都有误差,并且不同点的误差各不相同,例如:x=0.2这个点上的误差就要比x=0.7713这个点上的误差小很多。

因此,单个点的误差并不能用来指导我们进行参数更新,我们需要计算一个整体误差,而这个整体误差就叫损失。

4. 损失

损失与误差之间有一些本质的差别和联系,误差是单个数据预测值与标签值的差异,而损失是一组数据点的误差聚合,数学上采用所有点误差平方的均值来作为线性回归的损失。

MSE = 1 n ∑ i = 1 n error i 2 = 1 n ∑ i = 1 n ( y i ^ − y i ) 2 = 1 n ∑ i = 1 n ( b + w x i − y i ) 2 \Large \begin{aligned} \text{MSE} &= \frac{1}{n} \sum_{i=1}^n{\text{error}_i}^2 \\ &= \frac{1}{n} \sum_{i=1}^n{(\hat{y_i} - y_i)}^2 \\ &= \frac{1}{n} \sum_{i=1}^n{(b + w x_i - y_i)}^2 \end{aligned} MSE=n1i=1nerrori2=n1i=1n(yi^yi)2=n1i=1n(b+wxiyi)2

基于上述公式来计算模型在当前w和b下的损失,需要计算所有数据点误差的平方和,然后求平均。

loss = ((y_hat - y_train)**2).mean()
loss
2.7421577700550976
4.1 生成参数集

单个损失值不太容易直观感受大小,但如果能将大量参数集计算的损失值汇聚到一张图上显示,通过对比不同参数下的损失差异则更容易对损失有直观感觉。

为此,我们需要先生成一批参数,由于我们已知正确参数值为w=2, b=1, 那以正确参数值为中心,来生成一批随机参数集bs和ws,就能满足我们的对比观测需求。

用到了numpy库中的以下函数:

  1. np.linespace: 生成指定数量的等间隔序列(下面示例中数量是101),返回一个一维数组
  2. np.meshgrid: 根据两个一维数组来生成指定数量的网格序列,返回两个二维数组,这两个二维数组有以下特征:
    • bs: 其实是b_range序列不断按行重复。
    • ws: 是w_range序列不断按列重复。
def generate_param_sets(true_b, true_w):
    b_range = np.linspace(true_b - 3, true_b + 3, 101)
    w_range = np.linspace(true_w - 3, true_w + 3, 101)

    bs, ws = np.meshgrid(b_range, w_range)
    return bs, ws

bs, ws = generate_param_sets(true_b, true_w)
ws.shape, bs.shape, bs[0, :], ws[:, 0]

((101, 101),
 (101, 101),
 array([-2.  , -1.94, -1.88, -1.82, -1.76, -1.7 , -1.64, -1.58, -1.52,
        -1.46, -1.4 , -1.34, -1.28, -1.22, -1.16, -1.1 , -1.04, -0.98,
        -0.92, -0.86, -0.8 , -0.74, -0.68, -0.62, -0.56, -0.5 , -0.44,
        -0.38, -0.32, -0.26, -0.2 , -0.14, -0.08, -0.02,  0.04,  0.1 ,
         0.16,  0.22,  0.28,  0.34,  0.4 ,  0.46,  0.52,  0.58,  0.64,
         0.7 ,  0.76,  0.82,  0.88,  0.94,  1.  ,  1.06,  1.12,  1.18,
         1.24,  1.3 ,  1.36,  1.42,  1.48,  1.54,  1.6 ,  1.66,  1.72,
         1.78,  1.84,  1.9 ,  1.96,  2.02,  2.08,  2.14,  2.2 ,  2.26,
         2.32,  2.38,  2.44,  2.5 ,  2.56,  2.62,  2.68,  2.74,  2.8 ,
         2.86,  2.92,  2.98,  3.04,  3.1 ,  3.16,  3.22,  3.28,  3.34,
         3.4 ,  3.46,  3.52,  3.58,  3.64,  3.7 ,  3.76,  3.82,  3.88,
         3.94,  4.  ]),
 array([-1.  , -0.94, -0.88, -0.82, -0.76, -0.7 , -0.64, -0.58, -0.52,
        -0.46, -0.4 , -0.34, -0.28, -0.22, -0.16, -0.1 , -0.04,  0.02,
         0.08,  0.14,  0.2 ,  0.26,  0.32,  0.38,  0.44,  0.5 ,  0.56,
         0.62,  0.68,  0.74,  0.8 ,  0.86,  0.92,  0.98,  1.04,  1.1 ,
         1.16,  1.22,  1.28,  1.34,  1.4 ,  1.46,  1.52,  1.58,  1.64,
         1.7 ,  1.76,  1.82,  1.88,  1.94,  2.  ,  2.06,  2.12,  2.18,
         2.24,  2.3 ,  2.36,  2.42,  2.48,  2.54,  2.6 ,  2.66,  2.72,
         2.78,  2.84,  2.9 ,  2.96,  3.02,  3.08,  3.14,  3.2 ,  3.26,
         3.32,  3.38,  3.44,  3.5 ,  3.56,  3.62,  3.68,  3.74,  3.8 ,
         3.86,  3.92,  3.98,  4.04,  4.1 ,  4.16,  4.22,  4.28,  4.34,
         4.4 ,  4.46,  4.52,  4.58,  4.64,  4.7 ,  4.76,  4.82,  4.88,
         4.94,  5.  ]))

从训练集中提取单个数据点,并计算该数据点在网格中每个b、w组合上的预测。

sample_x虽然是单个数据点,但由于广播特性的缘故,numpy能够理解我们要将x值乘以ws矩阵中的每一项,最终生成的sample_y也是(101,101)的矩阵。这里(101,101)形状的sample_y表示单个数据点sample_x在不同b、w组合下的预测值。

sample_x = x_train[0][0]   # 单个数据点
sample_y = bs + ws * sample_x
sample_y.shape, sample_x, sample_y[0][0]
((101, 101), 0.7712703466859457, -2.7712703466859456)
4.2 计算损失

现在对x_train中的每一项都执行此运算,最终得到80个形状为(101,101)的矩阵,也就是80条数据在每个w、b组合上的预测结果,形状为(80, 101, 101)。

然后对这80条预测数据计算误差和损失,就可以得到形状为(101,101)的损失矩阵,它表示80条数据在每个w、b组合上的损失。

计算中间变量解释:

  • all_predictions:80条数据在每个w、b组合上的预测结果
  • y_train:80条数据对应的标签
  • all_errors:80条数据在每个w、b组合上的误差
  • all_losses:每个w、b组合上的损失

关键代码解释:

  1. y_train.reshape(-1, 1, 1):用于对标签y变换形状,由于标签y_train是长度为80的一维数组,在与all_predictions计算误差之前需要先将其变换形状以便两者进行减法计算。
  2. np.apply_along_axis: 在 NumPy 数组的指定轴上执行自定义计算
    • func1d: 需要应用的函数。这个函数应该是一个可以作用于 1D 数组的函数。
    • axis: 指定要应用函数的轴。数组将在这个轴上被切片,并对每个切片应用 func1d 函数。
    • arr: 要操作的输入数组。
def compute_losses(x_train, y_train, bs, ws):
    all_predictions = np.apply_along_axis(
        func1d=lambda x: bs + ws *x, 
        axis=1,
        arr=x_train,
    )
    all_labels = y_train.reshape(-1, 1, 1)
    all_errors = all_predictions - all_labels
    all_losses = (all_errors **2).mean(axis=0)
    return all_losses

all_losses = compute_losses(x_train, y_train, bs, ws)
all_losses.shape, all_losses
((101, 101),
 array([[20.42636615, 19.8988156 , 19.37846505, ...,  2.94801224,
          3.12606169,  3.31131114],
        [20.14315119, 19.61900235, 19.1020535 , ...,  2.99816431,
          3.17961547,  3.36826662],
        [19.86221857, 19.34147143, 18.82792428, ...,  3.05059872,
          3.23545158,  3.42750444],
        ...,
        [ 3.51924506,  3.32506154,  3.13807803, ..., 18.71086044,
         19.22227692, 19.7408934 ],
        [ 3.45969907,  3.26891726,  3.08533545, ..., 18.98468148,
         19.49949967, 20.02151785],
        [ 3.40243542,  3.21505531,  3.0348752 , ..., 19.26078486,
         19.77900475, 20.30442464]]))

通过上面可以看到, 将参数b和w网格化的目的,是为了建立预测值、误差值、损失值与参数b、w的三维立体关系,这样通过简单的all_losses[b,w]就能得到模型在任意b、w参数组合上的损失,极大的方便了可视化显示。

4.3 损失面

下面需要将all_losses可视化,在这之前,需要先定义两个辅助函数:

  • fit_model: 使用sklearn.LinearRegression根据数据集来自动拟合出线性回归方程,得出最佳参数值w和b。
  • find_index: 在所有参数庥中找到与随机参数最接近的参数,并返回其索引。
from sklearn.linear_model import LinearRegression

def fit_model(x_train, y_train):
    # Fits a linear regression to find the actual b and w that minimize the loss
    regression = LinearRegression()
    regression.fit(x_train, y_train)
    b_minimum, w_minimum = regression.intercept_[0], regression.coef_[0][0]
    return b_minimum, w_minimum

def find_index(b, w, bs, ws):
    b_idx = np.argmin(np.abs(bs[0, :]-b))
    w_idx = np.argmin(np.abs(ws[:,0]-w))

    fixedb, fixedw = bs[0, b_idx], ws[w_idx, 0]
    return b_idx, w_idx, fixedb, fixedw
(array([-2.  , -1.94, -1.88, -1.82, -1.76, -1.7 , -1.64, -1.58, -1.52,
        -1.46, -1.4 , -1.34, -1.28, -1.22, -1.16, -1.1 , -1.04, -0.98,
        -0.92, -0.86, -0.8 , -0.74, -0.68, -0.62, -0.56, -0.5 , -0.44,
        -0.38, -0.32, -0.26, -0.2 , -0.14, -0.08, -0.02,  0.04,  0.1 ,
         0.16,  0.22,  0.28,  0.34,  0.4 ,  0.46,  0.52,  0.58,  0.64,
         0.7 ,  0.76,  0.82,  0.88,  0.94,  1.  ,  1.06,  1.12,  1.18,
         1.24,  1.3 ,  1.36,  1.42,  1.48,  1.54,  1.6 ,  1.66,  1.72,
         1.78,  1.84,  1.9 ,  1.96,  2.02,  2.08,  2.14,  2.2 ,  2.26,
         2.32,  2.38,  2.44,  2.5 ,  2.56,  2.62,  2.68,  2.74,  2.8 ,
         2.86,  2.92,  2.98,  3.04,  3.1 ,  3.16,  3.22,  3.28,  3.34,
         3.4 ,  3.46,  3.52,  3.58,  3.64,  3.7 ,  3.76,  3.82,  3.88,
         3.94,  4.  ]),
 array([-1.  , -0.94, -0.88, -0.82, -0.76, -0.7 , -0.64, -0.58, -0.52,
        -0.46, -0.4 , -0.34, -0.28, -0.22, -0.16, -0.1 , -0.04,  0.02,
         0.08,  0.14,  0.2 ,  0.26,  0.32,  0.38,  0.44,  0.5 ,  0.56,
         0.62,  0.68,  0.74,  0.8 ,  0.86,  0.92,  0.98,  1.04,  1.1 ,
         1.16,  1.22,  1.28,  1.34,  1.4 ,  1.46,  1.52,  1.58,  1.64,
         1.7 ,  1.76,  1.82,  1.88,  1.94,  2.  ,  2.06,  2.12,  2.18,
         2.24,  2.3 ,  2.36,  2.42,  2.48,  2.54,  2.6 ,  2.66,  2.72,
         2.78,  2.84,  2.9 ,  2.96,  3.02,  3.08,  3.14,  3.2 ,  3.26,
         3.32,  3.38,  3.44,  3.5 ,  3.56,  3.62,  3.68,  3.74,  3.8 ,
         3.86,  3.92,  3.98,  4.04,  4.1 ,  4.16,  4.22,  4.28,  4.34,
         4.4 ,  4.46,  4.52,  4.58,  4.64,  4.7 ,  4.76,  4.82,  4.88,
         4.94,  5.  ]))

绘制等高线的思路:由于损失矩阵中的每个值都对应于参数b和w的不同组合,因此连接产生相同损失值的b和w组合就能得到一个椭圆,然后,不同损失值的椭圆叠加就能够得到等高线图。

而损失面则本质上是不同参数(b和w)的损失值在三维空间中的投影,我们可以通过损失面来观察不同参数组合下损失的变化。

用到的matplotlib函数:

  1. ax.annotate: 适用于添加带有箭头和多种格式的注释,主要用于2d场景,但是在3d场景中这些格式可能无法正常显示。
  2. ax.text:适用于添加简单文本注释,可以用于3d场景(指定x\y\z三个坐标)
    • zdir=(1, 0, 0): 文本方向相对于x轴对齐
  3. ax.plot_surface: 在3d坐标轴中绘制曲面图,有以下参数:
    • rstride 和 cstride 分别指定行和列之间的步长,用于定义在绘制曲面时跳过的行数和列数。增加这些值可以减少渲染曲面所需的顶点数量,从而提高渲染速度,但也可能降低图形的清晰度。
    • cmap: 这是一个 Colormap 对象或注册的名称,用于将 Z 值映射到颜色。它决定了曲面上的颜色如何随 Z 值的变化而变化。
    • norm: Normalize 对象,用于将数据值缩放到标准化范围以供 cmap 使用。
    • shade: 一个布尔值,指定是否对曲面进行着色。如果为 True,则使用光源和阴影来增强曲面的三维效果。
    • antialiased: 一个布尔值,指定是否对边缘进行抗锯齿处理。这通常用于改善图形的视觉质量。
  4. ax.contour: 在3d坐标轴中绘制等高线图
  5. ax.clabel: 在等高线图上添加标签
def show_loss_surface(x_train, y_train, bs, ws, all_losses):
    b_minimum, w_minimum = fit_model(x_train, y_train)
    b_range, w_range = bs[0, :], ws[:, 0]
    print(b_minimum, w_minimum)
    
    fig = plt.figure(figsize=(10,4))
    ax1 = fig.add_subplot(121, projection='3d')
    ax1.set_xlabel("b")
    ax1.set_ylabel("w")
    ax1.set_title("Loss 3d surface")
    ax1.plot_surface(bs, ws, all_losses, rstride=1, cstride=1, alpha=.2, cmap=plt.cm.jet, linewidth=0, antialiased=True)
    cs1 = ax1.contour(bs[0, :], ws[:, 0], all_losses, cmap=plt.cm.jet)
    ax1.clabel(cs1, fontsize=10, inline=1)   # 不能用于3D空间?
    mb_idx, mw_idx, _, _ = find_index(b_minimum, w_minimum, bs, ws)
    ax1.scatter(b_minimum, w_minimum, c='k')
    print(f"minimum loss: {all_losses[mb_idx, mw_idx]}")
    ax1.text(3, 0, 0, f"Minimum", zdir=(1, 0, 0))
    b_idx, w_idx, fixedb, fixedw = find_index(b, w, bs, ws)
    print(f"random start: {fixedb}, {fixedw}, {all_losses[b_idx, w_idx]}")
    ax1.scatter(fixedb, fixedw, c = 'k')
    ax1.text(0, -2.5, 0, f"Random start", zdir=(1, 0, 0))
    
    ax2 = fig.add_subplot(122)
    ax2.set_xlabel("b")
    ax2.set_ylabel("w")
    ax2.set_title("Loss 2D surface")
    cs2 = ax2.contour(bs[0, :], ws[:, 0], all_losses, cmap=plt.cm.jet)  # 绘制等高线
    ax2.clabel(cs2, fontsize=10, inline=1)   # 给等高级添加标签
    ax2.scatter(b_minimum, w_minimum, c = 'k')
    ax2.scatter(fixedb, fixedw, c = 'k')
    ax2.annotate(f"Minimum({b_minimum:.2f},{w_minimum:.2f})", xy=(b_minimum+.1, w_minimum+.1), c='k')
    ax2.annotate(f"Random start({fixedb:.2f}, {fixedw:.2f})", xy=(fixedb+.1, fixedw + .1), c='k')
    ax2.plot([fixedb, fixedb], w_range[[0, -1]], linewidth=1, linestyle='--', color='r')
    ax2.plot(b_range[[0, -1]], [fixedw, fixedw], linewidth=1, linestyle='--', color='b')

    fig.tight_layout()
    plt.show()

show_loss_surface(x_train, y_train, bs, ws, all_losses)

在这里插入图片描述

  1. 这里引入损失面纯粹是为了学习需要,帮助我们更直观理解不同参数下损失的变化。实际中绝大多数情况计算损失面都不太可行,因为我们大概率无法知道模型真实的参数值。
  2. 右图中心位置的Minimum,是损失的最小点,也是使用梯度下降要达到的点。
  3. 右图左下角的RandomStart对应于随机初始化参数的起点。
4.4 横截面图

上面右图中有两根虚线是为了切割横截面,意义在于:如果其它参数保持不变,就可以通过横截面来单独观察单个参数更改对损失的影响程度。

  • 红色虚线表示沿着b=0.52进行垂直切割,相当于保持b不变,单独增加w(达到2-3之间的某个值),则损失可以最小化
  • 蓝色虚线表示沿着w=-0.16进行水平切割,相当于保持w不变,单独增加b(达到接近2的某个值,损失可以最小化

最终得到两个横截面可视化如下。

def show_cross_surface(b, w, bs, ws, all_losses):
    b_range, w_range = bs[0, :], ws[:, 0]
    
    fig = plt.figure(figsize=(10,4))

    b_idx, w_idx, fixedb, fixedw = find_index(b, w, bs, ws)
    print(f"{all_losses[b_idx, w_idx]} and {all_losses[w_idx, b_idx]}")
    fixed_loss = all_losses[w_idx, b_idx]
    ax1 = fig.add_subplot(121)
    ax1.set_title(f"cross surface fixedb = {fixedb:.2f}")
    ax1.set_xlabel("w")
    ax1.set_ylabel("Loss")
    ax1.set_ylim([-.1, 6.1])
    ax1.plot(w_range, all_losses[:, b_idx], color='r', linestyle='--', linewidth=1)
    ax1.scatter(fixedw, fixed_loss, c='k')
    ax1.annotate(f"Random start({fixedw:.2f}, {fixed_loss:.2f})", xy=(fixedw-0.5, fixed_loss-0.3), c='k')

    ax4 = fig.add_subplot(122)
    ax4.set_title(f"cross surface fixedw = {fixedw:.2f}")
    ax4.set_xlabel("b")
    ax4.set_ylabel("Loss")
    ax4.set_ylim([-.1, 6.1])
    # ax4.set_xlim([-2, 8])
    ax4.plot(b_range, all_losses[w_idx, :], color='b', linestyle='--', linewidth=1)
    ax4.scatter(fixedb, fixed_loss, c = 'k')
    ax4.annotate(f"Random start({fixedb:.2f}, {fixed_loss:.2f})", xy=(fixedb-0.5, fixed_loss-1), c='k')

    fig.tight_layout()
    plt.show()
    
show_cross_surface(b, w, bs, ws, all_losses)
5.766123965773524 and 2.7113284116288243

在这里插入图片描述

这两个参数的横截面形状不同,而左边更平缓,相当于沿着参数w损失下降更慢; 右边更陡峭,相当于沿着参数b损失下降更快。

有了损失后,接下来就要找到使损失下降最快的参数更新方向,也就是梯度。

5. 梯度下降

梯度就是损失函数对参数的偏导数, 梯度的含义在于表达:当一个参数(如w)稍有变化时,损失会变化多少。

之所以使用偏导数而不是导数,是因为存在两个参数b和w,我们要分别知道参数b变化对损失的影响,以及参数w变化对损失的影响。

在这个线性回归的例子中,损失对参数b和w的偏导数可推导为:

∂ MSE ∂ b = ∂ MSE ∂ y i ^ ∂ y i ^ ∂ b = 1 n ∑ i = 1 n 2 ( b + w x i − y i ) = 2 1 n ∑ i = 1 n ( y i ^ − y i ) ∂ MSE ∂ w = ∂ MSE ∂ y i ^ ∂ y i ^ ∂ w = 1 n ∑ i = 1 n 2 ( b + w x i − y i ) x i = 2 1 n ∑ i = 1 n x i ( y i ^ − y i ) \Large \begin{aligned} \frac{\partial{\text{MSE}}}{\partial{b}} = \frac{\partial{\text{MSE}}}{\partial{\hat{y_i}}} \frac{\partial{\hat{y_i}}}{\partial{b}} &= \frac{1}{n} \sum_{i=1}^n{2(b + w x_i - y_i)} \\ &= 2 \frac{1}{n} \sum_{i=1}^n{(\hat{y_i} - y_i)} \\ \frac{\partial{\text{MSE}}}{\partial{w}} = \frac{\partial{\text{MSE}}}{\partial{\hat{y_i}}} \frac{\partial{\hat{y_i}}}{\partial{w}} &= \frac{1}{n} \sum_{i=1}^n{2(b + w x_i - y_i) x_i} \\ &= 2 \frac{1}{n} \sum_{i=1}^n{x_i (\hat{y_i} - y_i)} \end{aligned} bMSE=yi^MSEbyi^wMSE=yi^MSEwyi^=n1i=1n2(b+wxiyi)=2n1i=1n(yi^yi)=n1i=1n2(b+wxiyi)xi=2n1i=1nxi(yi^yi)

用上面的公式分别计算参数b和w的梯度。

b_grad = 2 * error.mean()
w_grad = 2 * (x_train * error).mean()
b_grad, w_grad
(-4.168926447402798, -1.969646602684886)

这里是对整个训练集一次性计算梯度,相当于是批量梯度下降。

6. 参数更新

有了梯度后,就可以用它来更新参数,梯度下降法中,每次参数更新时,都会减去学习率乘以梯度。公式定义如下:
b = b − η ∂ MSE ∂ b w = w − η ∂ MSE ∂ w \Large \begin{aligned} b &= b - \eta \frac{\partial{\text{MSE}}}{\partial{b}} \\ w &= w - \eta \frac{\partial{\text{MSE}}}{\partial{w}} \end{aligned} bw=bηbMSE=wηwMSE

lr=0.2
b_new = b - lr * b_grad
w_new = w - lr * w_grad
b, w, b_new, w_new
(array([0.49671415]),
 array([-0.1382643]),
 array([1.33049944]),
 array([0.25566502]))

梯度下降可以理解成: 您从山顶徒步下山,您可以选择从平坦的大路走,这样您会下得很慢,但最终一定会到达山底。您也可以选择从山脊的小路走,这样您会下得很快。

学习率 η \eta η 则可以理解成:您下山时迈的步子大小,它反映的是每次参数更新的幅度,为了进一步理解学习率对梯度下降的影响,我们会尝试不同学习率,观察参数更新后的变化。

首先定义一个函数来可视化参数更新后的损失变化。

def show_param_update(b, w, bs, ws, all_losses, lr, b_grad, w_grad):
    b_new = b - lr * b_grad
    w_new = w - lr * w_grad
    # bs是行与行重复,所以取第0行就是所有b参数的取值范围
    # ws是列与列重复,所以取第0列就是所有w参数的取值范围
    b_range, w_range = bs[0, :], ws[:, 0]   
    # 检查所有预测值中与当前b、w最相近的参数值 
    b_idx, w_idx, fixedb, fixedw = find_index(b, w, bs, ws)
    # 检查所有预测值中与最新b、w最相近的参数值 
    b_idx_new, w_idx_new, fixedb_new, fixedw_new = find_index(b_new, w_new, bs, ws)

    print(fixedb_new, fixedw_new, all_losses[w_idx_new, b_idx_new])
    print(b_idx, w_idx, b_idx_new, w_idx_new)

    fig, ax = plt.subplots(1, 2, figsize=(10, 4))
    
    fixed_loss = all_losses[w_idx, b_idx]
    fixedb_loss_new = all_losses[w_idx_new, b_idx]
    fixedw_loss_new = all_losses[w_idx, b_idx_new]
    ax[0].set_title(f"Fixedb = {fixedb:.4f}")
    ax[0].set_xlabel("w")
    ax[0].set_ylabel("Loss")
    ax[0].set_ylim(0, 6)
    ax[0].plot(w_range, all_losses[:, b_idx], color='r', linewidth=1, linestyle='--')
    ax[0].plot(fixedw, fixed_loss, 'or')   # 圆点、红色
    # fixedw_new = ws[w_idx_new+5, 0]
    ax[0].plot(fixedw_new, fixedb_loss_new, 'or')
    ax[0].plot([fixedw, fixedw_new], [fixed_loss, fixed_loss], linestyle='--', color='r')
    ax[0].arrow(fixedw_new, fixed_loss, .3, 0, color='r', shape='full', lw=0, length_includes_head=True, head_width=.2)
    # Annotations
    ax[0].annotate(r'$\eta = {:.2f}$'.format(lr), xy=(1, 9.5), c='k', fontsize=17)
    ax[0].annotate(r'$-\eta \frac{\delta MSE}{\delta w} \approx' + f'{-lr * w_grad:.2f}$', xy=(1, 3), c='k', fontsize=17)

    ax[1].set_title(f"Fixedw = {fixedw:.4f}")
    ax[1].set_xlabel("b")
    ax[1].set_ylabel("Loss")
    ax[1].set_ylim(0, 6)
    ax[1].plot(b_range, all_losses[w_idx, :], color='b', linewidth=1, linestyle='--')
    ax[1].plot(fixedb, fixed_loss, 'ob')   # 圆点、蓝色
    ax[1].plot(fixedb_new, fixedw_loss_new, 'ob')
    ax[1].plot([fixedb, fixedb_new], [fixed_loss, fixed_loss], linestyle='--', color='b')
    ax[1].arrow(fixedb_new, fixed_loss, .3, 0, color='b', shape='full', lw=0, length_includes_head=True, head_width=.2)
   
    ax[1].annotate(r'$\eta = {:.2f}$'.format(lr), xy=(0.6, 12.5), c='k', fontsize=17)
    ax[1].annotate(r'$-\eta \frac{\delta MSE}{\delta b} \approx' + f'{-lr * b_grad:.2f}$', xy=(1, 3), c='k', fontsize=17)

    plt.show()

首先尝试一个比较小的学习率:0.2,小的学习率总是安全的,这两个参数w、b的损失都将接近最小值,但右侧曲线b接近的更快,因为它更陡峭。

show_param_update(b, w, bs, ws, all_losses, 0.2, b_grad, w_grad)

在这里插入图片描述

尝试增大学习率到0.6,看看会发生什么?

show_param_update(b, w, bs, ws, all_losses, 0.6, b_grad, w_grad)

在这里插入图片描述

  1. 左边红色曲线的损失依旧在稳定的下降,但右侧曲蓝色曲线的损失有些出乎意料,它越过最小值开始向着山的另一边向上爬,相当于下山回家时步子迈的有点大,走过了。
  2. 蓝色曲线这种情况,会在左右来回震荡中收敛,最终也会达到最小值,但速度很慢。

如果继续加大学习率到0.8,会发生什么?

show_param_update(b, w, bs, ws, all_losses, 0.8, b_grad, w_grad)

在这里插入图片描述

这次蓝色曲线的表现更糟糕,不仅再次爬上了山的另一边,而且这次爬得更高,比下山时还要高,相当于损失不仅没下降,反而上升了。

值得注意的是: 在这三次学习率尝试期间,左图一切正常,损失始终在下降。这意味着左边曲线适应更大的学习率,而右边曲线则只能适应较小的学习率。之所以会出现这个差异,是因为右边曲线比左边更陡峭。

这说明: 学习率太大或太小都是一个相对概念,它取决于曲线有多陡峭,也就是梯度有多大。

不幸的是, 我们只有一个学习率可供选择,这意味着学习率大小要受到最陡峭曲线的限制,而其它平滑曲线则必须降低学习速度,相当于不得不使用次优的学习率。

不过,如果所有的曲线都同样陡峭,则所有曲线的学习率就都能接近最优值。

x_train.shape, y_train.shape
((80, 1), (80, 1))

7. 循环迭代训练

前面介绍的这些步骤 计算预测值、计算损失、计算梯度、更新参数 相当于一个训练周期。但仅靠一个训练周期很难将模型损失降到最低,因此需要循环迭代训练。
循环迭代训练就是在多个周期中一遍又一遍的重复这个过程,这就是在训练一个模型。

为了完成模型的训练,我们需要为这个训练过程定义几个函数:

  1. 单次训练函数,也就是完成预测、损失、梯度的计算以及参数更新这一个训练周期。
  2. 训练循环函数,也就是用不同的数据集重复调用单次训练函数多次。
  3. 可视化函数,用于观察训练过程中模型参数的变化。

下面首先定义单批次训练函数

  • 使用当前参数计算预测值
  • 预测值减真实值得到损失
  • 通过之前的公式分别计算参数w和参数b的梯度
  • 更新参数w和参数b
def train_epoch(x_train, y_train, w, b, lr):
    yhat = b + w * x_train
    # print(f"yhat.shape:{yhat.shape}, yhat:{yhat}")
    error = yhat - y_train
    # print(f"error.shape:{error}")
    b_grad = 2 * error.mean()
    w_grad = 2 * (x_train * error).mean()
    # print(f"b_grad:{b_grad}, w_grad:{w_grad}")
    w = w - lr * w_grad
    b = b - lr * b_grad
    return w, b

定义一个可视化函数,用于更好的观察训练过程中的拟合效果和梯度下降过程。

def show_gradient_descent(x_train, y_train, y_hat):
    min_b, min_w = fit_model(x_train, y_train)
    fig, ax = plt.subplots(1,2, figsize=(10,5))

    ax[0].set_xlabel("x")
    ax[0].set_ylabel("y")
    ax[0].set_title("Prediction ")
    ax[0].scatter(x_train, y_train, color='b', marker='.')
    ax[0].plot(x_train, y_hat, label="model's old prediction", color='r', linestyle='--')
    ax[0].annotate(f"w={w[0]:.4f}, b={b[0]:.4f}", xy=(0.2, 0.5), color='r')
    ax[0].legend(loc=0)

    ax[1].set_xlabel('b')
    ax[1].set_ylabel('w')
    ax[1].set_xlim([0, 3])
    ax[1].set_title('Gradient descent path')
    # ax[1].plot(bs1, ws1, marker='o', linestyle='--', color='y')
    ax[1].plot(min_b, min_w, 'ko')
    ax[1].annotate('Minimum', xy=(min_b+.1, min_w-.05), fontsize=10)
    # ax[1].annotate('Random Start', xy=(bs1[0]+.1, ws1[0]), fontsize=10)

    fig.tight_layout()
    # plt.show()
    return ax[0], ax[1]

定义一个循环迭代的训练方法,来实现小批量随机梯度下降,并可视化展示整个训练过程。

def train_and_show(w_initial, b_initial, min_batch, lr, x_train, y_train, y_hat, show_epoch_text=True):
    ax0, ax1 = show_gradient_descent(x_train, y_train, y_hat)
    all_w, all_b = [w_initial], [b_initial]

    w_new, b_new = w_initial, b_initial
    for i in range(len(x_train)//min_batch):
        start = i * min_batch
        end = (i+1)*min_batch
        w_new, b_new = train_epoch(x_train[start:end], y_train[start:end], w_new, b_new, lr)
        y_hat_new = b_new + w_new * x_train
        ax0.plot(x_train, y_hat_new, label=f"model's new prediction:{i+1}", color='g', linewidth=0.3, linestyle='--')
        if show_epoch_text:
            ax0.annotate(f"epoch {i+1}", xy=(x_train[0, 0], y_hat_new[0, 0]), color='k')
        all_w.append(w_new)
        all_b.append(b_new)

    ax1.plot(all_b, all_w, marker='o', linestyle='--', color='y')
    ax1.annotate('Random Start', xy=(all_b[0]+.1, all_w[0]), fontsize=10)
    for i in range(len(all_w)): print(f"w_new:{all_w[i]}, b_new:{all_b[i]}") 

前面有讨论学习率大小对梯度下降的影响,这里我们会讨论另一个超参数——小批量数量对梯度下降的影响。

所谓小批量数量,就是每次训练时,取多少个特征样本用于训练,这也决定了会执行多少次参数更新,可分为三类:

  • 随机梯度下降,每次取一个样本进行训练,80条数据执行80次参数更新。
  • 批量梯度下降,每次取全部样本进行训练,80条数据只执行一次参数更新。
  • 小批量梯度下降,每次取一部分样本进行训练,参数更新次数介于批量梯度和随机梯度两者之间。

定义超参数

  • 小批量的数量设为10,意味着训练为80/10=8次
  • 学习率为0.20

    这个学习率如果放在实际应用中会很大,但我们的模型非常简单,所以这个学习率并不算大。

开始训练。

w_new, b_new, min_batch, lr = w, b, 10, 0.20
train_and_show(w_new, b_new, min_batch, lr, x_train, y_train, y_hat)

w_new:[-0.1382643], b_new:[0.49671415]
w_new:[0.22391513], b_new:[1.07956181]
w_new:[0.44752515], b_new:[1.39670002]
w_new:[0.60827456], b_new:[1.60360132]
w_new:[0.64546871], b_new:[1.59991547]
w_new:[0.72646039], b_new:[1.64620925]
w_new:[0.7632322], b_new:[1.65686899]
w_new:[0.78698009], b_new:[1.59736361]
w_new:[0.80618527], b_new:[1.55216515]

在这里插入图片描述

  1. 上面左图可以看到:后面几次迭代(epoch3-epoch8)预测曲线(绿色)向训练数据(蓝色点)的拟合进度几乎停滞。
  2. 上面右图可以看到:后面几次迭代的参数几乎不再更新。

不确定是与学习率和小批量数量哪个有关系,我们先增大学习率到0.70看看效果。

w_new, b_new, min_batch, lr = w, b, 10, 0.70
train_and_show(w_new, b_new, min_batch, lr, x_train, y_train, y_hat)
w_new:[-0.1382643], b_new:[0.49671415]
w_new:[1.12936372], b_new:[2.53668097]
w_new:[0.58587058], b_new:[1.03648217]
w_new:[1.36255064], b_new:[2.15240862]
w_new:[0.87328498], b_new:[0.90592657]
w_new:[1.54979536], b_new:[1.87013457]
w_new:[1.13417631], b_new:[0.99797719]
w_new:[1.4493271], b_new:[1.50590366]
w_new:[1.34195945], b_new:[1.09110064]

在这里插入图片描述

当学习率增大到0.7时,震荡很剧烈,这种情况并不利于模型快速收敛,实际情况中要避免。

尝试将小批量数量调小为1,观察损失下降的效果。

小批量为1时,就等于随机梯度下降,每次只用1条数据来训练损失和计算梯度,80条数据就会将参数更新80次。

w_new, b_new, min_batch, lr = w, b, 1, 0.20
train_and_show(w_new, b_new, min_batch, lr, x_train, y_train, y_hat, False)
    w_new:[-0.1382643], b_new:[0.49671415]
    w_new:[0.34558342], b_new:[1.29311252]
    w_new:[0.50523314], b_new:[1.58729138]
    w_new:[0.48898234], b_new:[1.52944586]
   ……
    w_new:[1.83501995], b_new:[1.06041805]
    w_new:[1.84612004], b_new:[1.08392448]
    w_new:[1.84561774], b_new:[1.08136152]
    w_new:[1.86481369], b_new:[1.10769315]

在这里插入图片描述

  1. 损失在反复振荡后能够接近最小值,但需要的训练批次和训练时间明显增加。
  2. 之所以会反复振荡,是因为当小批量太小时,单个数据点所反应的损失永远不会稳定,这也决定了它只能徘徊在最小值附近却无法到达。

上面几个参数更新的过程都不太理想,下面尝试对数据特征进行变换,看看效果如何。

8. 数据缩放影响

前面有提到,因为全局只有一个学习率,即使不同参数的梯度不同,也必须采用最陡峭曲线的学习率进行训练,导致不同参数的收敛速度有很大差异,进而导致整体训练速度慢。

那如果让每个参数的梯度都接近相同,是否会提高训练速度?

8.1 数据归一化

这里要用到一个组件——StandardScaler,用来缩放数据使其标准化,它能使每个特征的均值变为0,标准差变为1,这个操作称为归一化。这样做的目的是使所有数据特征具有相似比例,提高梯度下降的性能。

需要注意的是:缩放数据必须在训练集、测试集拆分之后进行,否则会把测试集的信息提前透露给模型。

from sklearn.preprocessing import StandardScaler

scaler = StandardScaler(with_mean=True, with_std=True)
# 只对x计算均值和方差
scaler.fit(x_train)
# 标准化x_train和x_test
x_train_scaled = scaler.transform(x_train)
x_test_scaled = scaler.transform(x_test)
def show_scaled_data(x, y, scaled_x):
    fig, ax = plt.subplots(1, 2, figsize=(10, 4))
    ax[0].scatter(x, y, c='b')
    ax[0].set_xlabel('x')
    ax[0].set_ylabel('y')
    ax[0].set_title('Train original data')
    ax[0].label_outer()  # 用于多个子图共享坐标轴标签。

    ax[1].scatter(scaled_x, y, c = 'g')
    ax[1].set_xlabel('scaled x')
    ax[1].set_ylabel('y')
    ax[1].set_title('Train scaled data')
    ax[1].label_outer() # 用于多个子图共享坐标轴标签。
    plt.show()

show_scaled_data(x_train, y_train, x_train_scaled)

在这里插入图片描述

可视化缩放后的数据与原数据, 两个图形之间唯一的区别是特征x的比例,原来的x值范围是[0,1],缩放后的x值范围是[-1.5,1.5]

8.2 归一化数据损失面

下面来检查下数据归一化后损失面和横截面的变化。

def show_scaled_loss(x, y, scaled_x, bs, ws):
    original_losses = compute_losses(x, y, bs, ws)
    b_minimum, w_minimum = fit_model(x, y)
    
    scaled_losses = compute_losses(scaled_x, y, bs, ws)
    scaled_b_minimum, scaled_w_minimum = fit_model(scaled_x, y)
    b_idx, w_idx, fixedb, fixedw = find_index(b, w, bs, ws)
    b_range, w_range = bs[0, :], ws[:, 0]

    fig = plt.figure(figsize=(12, 10))
    ax1 = fig.add_subplot(2, 2, 1)
    ax1.set_xlabel('b')
    ax1.set_ylabel('w')
    ax1.set_title(f'Original loss surface')
    cs1 = ax1.contour(bs[0, :], ws[:, 0], original_losses)
    ax1.clabel(cs1, inline=True, fontsize=8)
    ax1.plot(b_minimum, w_minimum, 'ko')
    ax1.annotate(f'Minimum({b_minimum:.2f}, {w_minimum:.2f})', (b_minimum+.1, w_minimum+.1))
    ax1.plot(fixedb, fixedw, 'ko')
    ax1.annotate(f'Random Start({fixedb:.2f}, {fixedw:.2f})', (fixedb+.1, fixedw+.1))
    ax1.plot([fixedb, fixedb], w_range[[0, -1]], linewidth=1, linestyle='--', color='r')
    ax1.plot(b_range[[0, -1]], [fixedw, fixedw], linewidth=1, linestyle='--', color='b')

    ax2 = fig.add_subplot(2, 2, 2)
    ax2.set_xlabel('b')
    ax2.set_ylabel('w')
    ax2.set_title(f'Scaled loss surface')
    cs2 = ax2.contour(bs[0, :], ws[:, 0], scaled_losses)
    ax2.clabel(cs2, inline=True, fontsize=8)
    ax2.plot(scaled_b_minimum, scaled_w_minimum, 'ko')
    ax2.annotate(f'Minimum({scaled_b_minimum:.2f}, {scaled_w_minimum:.2f})', (scaled_b_minimum+.1, scaled_w_minimum+.1))
    ax2.plot(fixedb, fixedw, 'ko')
    ax2.annotate(f'Random Start({fixedb:.2f}, {fixedw:.2f})', (fixedb+.1, fixedw+.1))
    ax2.plot([fixedb, fixedb], w_range[[0, -1]], linewidth=1, linestyle='--', color='r')
    ax2.plot(b_range[[0, -1]], [fixedw, fixedw], linewidth=1, linestyle='--', color='b')
    
    ax3 = fig.add_subplot(2, 2, 3)
    ax3.set_xlabel('w')
    ax3.set_ylabel('Loss')
    ax3.set_title(f'Fixedb={fixedb:.4f}')
    ax3.plot(ws[:, 0], original_losses[b_idx, :], color='r', linestyle=':',  linewidth=1, label='Original')
    ax3.plot(ws[:, 0], scaled_losses[b_idx, :], color='r', linestyle='--', linewidth=2, label='Scaled')
    ax3.plot(fixedw, scaled_losses[b_idx, w_idx],'ko')
    ax3.legend(loc=0)

    ax4 = fig.add_subplot(2, 2, 4)
    ax4.set_xlabel('b')
    ax4.set_ylabel('Loss')
    ax4.set_title(f'Fixedw={fixedw:.4f}')
    ax4.plot(bs[0, :], original_losses[:, w_idx], color='b', linestyle=':', linewidth=1, label='Original')
    ax4.plot(bs[0, :], scaled_losses[:, w_idx], color='b', linestyle='--', linewidth=2, label='Scaled')
    ax4.plot(fixedb, scaled_losses[b_idx, w_idx], 'ko')
    ax4.legend(loc=0)

    fig.tight_layout()
    plt.show()

bs_scaled, ws_scaled = generate_param_sets(2, 0.6)
show_scaled_loss(x_train, y_train, x_train_scaled, bs_scaled, ws_scaled)

在这里插入图片描述
其中:

  • 第一个图为参数集在数据缩放前的损失面
  • 第二个图为参数集在归一化后的损失面
  • 第三个图为当参数b固定为0.50时,参数w在数据缩放前和归一化后的横截面对比。
  • 第四个图为当参数w固定为-0.12时,参数b在数据缩放前和归一化后的横截面对比。

可以观察到:

  • 归一化后的损失面近乎像标准的圆,实际上这正是我们期望的理想损失面。
  • 参数w和b的横截面在归一化后形状接近相同,这意味着一个横截面的良好学习率对另一个面同样有效。
8.3 训练归一化数据

使用归一化后的数据进行训练,观察训练结果的收敛情况。

yhat_scaled = b + w * x_train_scaled
w_new, b_new, min_batch, lr = w, b, 10, 0.20
train_and_show(w_new, b_new, min_batch, lr, x_train_scaled, y_train, yhat_scaled)  
w_new:[-0.1382643], b_new:[0.49671415]
w_new:[0.19570483], b_new:[1.05037857]
w_new:[0.36733079], b_new:[1.42526451]
w_new:[0.51859287], b_new:[1.67266035]
w_new:[0.53325665], b_new:[1.77014119]
w_new:[0.58344743], b_new:[1.84460182]
w_new:[0.59347544], b_new:[1.89217424]
w_new:[0.58694027], b_new:[1.9181703]
w_new:[0.60220844], b_new:[1.91942387]

在这里插入图片描述

可以看到,使用归一化后的数据训练收敛速度明显加快,已经逼近了真实值,同时参数更新过程稳定,没有震荡的情况,这几乎就是模型训练的理想状态。

如果将小批量设小一点,会发生什么?

w_new, b_new, min_batch, lr = w, b, 5, 0.20
train_and_show(w_new, b_new, min_batch, lr, x_train_scaled, y_train, yhat_scaled, False)
w_new:[-0.1382643], b_new:[0.49671415]
w_new:[0.33418071], b_new:[1.11193084]
w_new:[0.37950001], b_new:[1.39951975]
w_new:[0.48928685], b_new:[1.65332468]
w_new:[0.51731784], b_new:[1.79052714]
w_new:[0.56426217], b_new:[1.85115254]
w_new:[0.58748889], b_new:[1.92262467]
w_new:[0.59929455], b_new:[1.92057126]
w_new:[0.59101636], b_new:[1.92551366]
w_new:[0.57512316], b_new:[1.94485826]
w_new:[0.62278016], b_new:[1.93327507]
w_new:[0.61866432], b_new:[1.95991615]
w_new:[0.60983028], b_new:[1.94246341]
w_new:[0.60047933], b_new:[1.95643523]
w_new:[0.59956041], b_new:[1.94991891]
w_new:[0.61490553], b_new:[1.92118735]
w_new:[0.62461468], b_new:[1.93870235]

在这里插入图片描述

模型拟合程度有肉眼看不出差异,参数向损失最小值靠近的比之前更近一点(1.91->1.93),但训练时间也会更长一点。

如果将小批量调大呢?

w_new, b_new, min_batch, lr = w, b, 16, 0.20
train_and_show(w_new, b_new, min_batch, lr, x_train_scaled, y_train, yhat_scaled)
w_new:[-0.1382643], b_new:[0.49671415]
w_new:[0.16741696], b_new:[1.06592087]
w_new:[0.40949256], b_new:[1.46115881]
w_new:[0.49592256], b_new:[1.65094415]
w_new:[0.53795035], b_new:[1.78020678]
w_new:[0.55429704], b_new:[1.83529641]

在这里插入图片描述

依然稳定的超着最小值迈进,但由于训练批次太少,还没有到达最小值,训练已经停止。

小结: 综上可以看出,不论学习率还是批量大小,它们的设置都是一个权衡的结果,而数据特征则对训练性能有非常大的影响,应该始终标准化我们的训练特征数据。

参考资料


原文地址:https://blog.csdn.net/xiaojia1001/article/details/140536692

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!