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 = 100np.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 pltdef 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, wsbs, 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_labelsall_losses = (all_errors **2).mean(axis=0)return all_lossesall_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 LinearRegressiondef fit_model(x_train, y_train):# Fits a linear regression to find the actual b and w that minimize the lossregression = LinearRegression()regression.fit(x_train, y_train)b_minimum, w_minimum = regression.intercept_[0], regression.coef_[0][0]return b_minimum, w_minimumdef 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_gradw_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)# Annotationsax[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_gradb = b - lr * b_gradreturn 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_initialfor i in range(len(x_train)//min_batch):start = i * min_batchend = (i+1)*min_batchw_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_trainax0.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 StandardScalerscaler = 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]

在这里插入图片描述

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

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

参考资料

  • matplotlib绘图学习笔记
  • 线性回归从零实现

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://xiahunao.cn/news/3250590.html

如若内容造成侵权/违法违规/事实不符,请联系瞎胡闹网进行投诉反馈,一经查实,立即删除!

相关文章

去中心化技术的变革力量:探索Web3的潜力

随着区块链技术的发展和应用,去中心化技术正成为数字世界中的一股强大变革力量。Web3作为去中心化应用的新兴范式,正在重新定义人们对于数据、互联网和价值交换的认知。本文将探索去中心化技术的基本概念、Web3的核心特征及其潜力应用,展示其…

C语言 底层逻辑详细阐述指针(一)万字讲解 #指针是什么? #指针和指针类型 #指针的解引用 #野指针 #指针的运算 #指针和数组 #二级指针 #指针数组

文章目录 前言 序1:什么是内存? 序2:地址是怎么产生的? 一、指针是什么 1、指针变量的创建及其意义: 2、指针变量的大小 二、指针的解引用 三、指针类型存在的意义 四、野指针 1、什么是野指针 2、野指针的成因 a、指…

自定义注解 + Redis 实现业务的幂等性

1.实现幂等性思路 实现幂等性有两种方式: ⭐ 1. 在数据库层面进行幂等性处理(数据库添加唯一约束). 例如:新增用户幂等性处理,username 字段可以添加唯一约束. ⭐ 2. 在应用程序层面进行幂等性处理. 而在应用程序…

JVM(day2)经典垃圾收集器

经典垃圾收集器 Serial收集 使用一个处理器或一条收集线程去完成垃圾收集工作,更重要的是强调在它进行垃圾收集时,必须暂停其他所有工作线程,直到它收集结束。 ParNew收集器 ParNew 收集器除了支持多线程并行收集之外,其他与 …

博客园运营危机,我为了保护我的博客回到CSDN

文章目录 前言我与博客园程序员和创业后续更新计划 前言 博客园最近的运营危机大家应该也有所耳闻。我之前是因为CSDN的广告太多,所以换的博客园。但是我现在因为害怕博客园运营倒闭,我又来到了CSDN上面继续发博客。 我与博客园 首先,先上…

鸿道Intewell软件版本发布:Intewell-Hyper II_V2.2.0_实时操作系统

Intewell-Hyper II_V2.2.0 版本号:V2.2.0 版本特点 1.新增系统配置服务V1.0 2.新增系统配置工具V1.0 3.新增license ManagerV1.0 4.升级Tool Box至V1.1 5.升级Developer至V2.1.3 6.升级Intewell RTOS至V2.1.3 特殊说明 版本或修改说明: 1.增加系统配置服务&…

工时记录软件选型指南

国内外主流的10款工时计算软件对比:PingCode、Worktile、Tita、易企秀、奇鱼、Teambition、Timely、Toggl Track、RescueTime、ClickUp。 在忙碌的工作中,记录和管理工时常常是令人头疼的问题。工时记录软件的选择不仅能帮你省时省力,还能大幅…

Transformer是怎样处理序列数据的?

Transformer模型最初是一种广泛应用于自然语言处理(NLP)和其他序列建模任务的架构。它由编码器(encoder)和解码器(decoder)组成。 以下是Transformer模型输入和输出的详细介绍: 输入 1. 输入…

数据结构-java中链表的存储原理及使用方式

目录 链表(线性表的链式存储) 代码实例:(链表构建,头插尾插) LinkedList LinkedList的使用: 1、构造方法 2、操作方法 LinkedList 和 ArrayList 的区别 链表(线性表的链式存储…

C语言 ——— 输入两个正整数,求出最小公倍数

目录 何为最小公倍数 题目要求 代码实现 方法一:暴力求解法(不推荐) 方法二:递乘试摸法(推荐) 何为最小公倍数 最小公倍数是指两个或者多个正整数(除了0以外)的最小的公共倍数…

吴恩达深度学习笔记:机器学习策略(2)(ML Strategy (2)) 2.9-2.10

目录 第三门课 结构化机器学习项目(Structuring Machine Learning Projects)第二周:机器学习策略(2)(ML Strategy (2))2.9 什么是端到端的深度学习?(What is end-to-end deep learning?&#x…

【matlab 投影寻踪】基于PSO算法的最优投影方向优化

一 投影寻踪算法 投影寻踪是处理和分析高维数据的一类统计方法,其基本思想是将高维数据投影到低维(1~3维)子空间上,寻找出反映原高维数据的结构或特征的投影,以达到研究和分析高维数据的目的。1974年&…

深度学习中的正则化技术 - Dropout篇

序言 在深度学习的浩瀚领域中,模型过拟合一直是研究者们面临的挑战之一。当模型在训练集上表现得近乎完美,却难以在未见过的数据(测试集)上保持同样优异的性能时,过拟合现象便悄然发生。为了有效缓解这一问题&#xf…

java文本比较解决方案

参考资料 VBA计算页码和行号https://learn.microsoft.com/zh-cn/office/vba/api/word.wdinformation 概述: 最近在做word文档对比的,总结了几种解决方案,记录一下 在java中,常用的文本对比方案有如下几种: 差异比较…

Pycharm 报错 Environment location directory is not empty 解

删除项目中ven文件夹(已存在的),然后再添加新的ven虚拟环境就可以了

Richteck立锜科技电源管理芯片简介及器件选择指南

一、电源管理简介 电源管理组件的选择和应用本身的电源输入和输出条件是高度关联的。 输入电源是交流或直流?需求的输出电压比输入电压高或是低?负载电流多大?系统是否对噪讯非常敏感?也许系统需要的是恒流而不是稳压 (例如 LED…

入门C语言只需一个星期(星期三)

点击上方"蓝字"关注我们 01、基本数据类型 char 1 字节 −128 ~ 127 单个字符/字母/数字/ASCIIsigned char 1 字节 −128 ~ 127 -unsigned char 1 字节 0 ~ 255 -int…

【自学安全防御】三、企业双机热备和带宽管理的综合实验

实验拓扑: 实验任务: 12,对现有网络进行改造升级,将当个防火墙组网改成双机热备的组网形式,做负载分担模式,游客区和DMZ区走FW3,生产区和办公区的流量走FW1 13,办公区上网用户限制流…

JavaSE 知识梳理(上)

1. Java语言的特性 简单性、面向对象、分布式、健壮性、安全性、体系结构中立、可移植性、解释性、高能效、多线程、动态性 2. JDK、JRE、JVM之间的关系 JDK(Java Development Kit):Java开发工具包,提供给Java程序员使用,包含了JRE,同时还…

使用Pycharm画图展示在窗口的侧栏Plots中无图像问题

使用Pycharm画图展示在窗口的侧栏Plots中无图像问题 在运行一个python文件时,突然出现侧栏Plots处提供预览的哪里没有出现图片,只有空白。解决方法如下: 找到Tools -> Python Plots,下图,取消勾选use interactive…