引言
本文主要基于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 数据预处理
需要对数据进行两方面的处理:
- 打乱数据集:对数据集打乱顺序,确保数据随机,目的是提高梯度下降的性能。
- 数据集拆分:前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函数如下:
- ax.annotate: 在指定坐标显示注释文本
- ax.plot: 绘制坐标点构成的线图
- ax.legend: 在坐标轴上给各个线条添加Label标签
- 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=1∑nerrori2=n1i=1∑n(yi^−yi)2=n1i=1∑n(b+wxi−yi)2
基于上述公式来计算模型在当前w和b下的损失,需要计算所有数据点误差的平方和,然后求平均。
loss = ((y_hat - y_train)**2).mean()
loss
2.7421577700550976
4.1 生成参数集
单个损失值不太容易直观感受大小,但如果能将大量参数集计算的损失值汇聚到一张图上显示,通过对比不同参数下的损失差异则更容易对损失有直观感觉。
为此,我们需要先生成一批参数,由于我们已知正确参数值为w=2, b=1, 那以正确参数值为中心,来生成一批随机参数集bs和ws,就能满足我们的对比观测需求。
用到了numpy库中的以下函数:
- np.linespace: 生成指定数量的等间隔序列(下面示例中数量是101),返回一个一维数组
- 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组合上的损失
关键代码解释:
- y_train.reshape(-1, 1, 1):用于对标签y变换形状,由于标签y_train是长度为80的一维数组,在与all_predictions计算误差之前需要先将其变换形状以便两者进行减法计算。
- 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函数:
- ax.annotate: 适用于添加带有箭头和多种格式的注释,主要用于2d场景,但是在3d场景中这些格式可能无法正常显示。
- ax.text:适用于添加简单文本注释,可以用于3d场景(指定x\y\z三个坐标)
- zdir=(1, 0, 0): 文本方向相对于x轴对齐
- ax.plot_surface: 在3d坐标轴中绘制曲面图,有以下参数:
- rstride 和 cstride 分别指定行和列之间的步长,用于定义在绘制曲面时跳过的行数和列数。增加这些值可以减少渲染曲面所需的顶点数量,从而提高渲染速度,但也可能降低图形的清晰度。
- cmap: 这是一个 Colormap 对象或注册的名称,用于将 Z 值映射到颜色。它决定了曲面上的颜色如何随 Z 值的变化而变化。
- norm: Normalize 对象,用于将数据值缩放到标准化范围以供 cmap 使用。
- shade: 一个布尔值,指定是否对曲面进行着色。如果为 True,则使用光源和阴影来增强曲面的三维效果。
- antialiased: 一个布尔值,指定是否对边缘进行抗锯齿处理。这通常用于改善图形的视觉质量。
- ax.contour: 在3d坐标轴中绘制等高线图
- 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)
- 这里引入损失面纯粹是为了学习需要,帮助我们更直观理解不同参数下损失的变化。实际中绝大多数情况计算损失面都不太可行,因为我们大概率无法知道模型真实的参数值。
- 右图中心位置的Minimum,是损失的最小点,也是使用梯度下降要达到的点。
- 右图左下角的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} ∂b∂MSE=∂yi^∂MSE∂b∂yi^∂w∂MSE=∂yi^∂MSE∂w∂yi^=n1i=1∑n2(b+wxi−yi)=2n1i=1∑n(yi^−yi)=n1i=1∑n2(b+wxi−yi)xi=2n1i=1∑nxi(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−η∂b∂MSE=w−η∂w∂MSE
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)
- 左边红色曲线的损失依旧在稳定的下降,但右侧曲蓝色曲线的损失有些出乎意料,它越过最小值开始向着山的另一边向上爬,相当于下山回家时步子迈的有点大,走过了。
- 蓝色曲线这种情况,会在左右来回震荡中收敛,最终也会达到最小值,但速度很慢。
如果继续加大学习率到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. 循环迭代训练
前面介绍的这些步骤 计算预测值、计算损失、计算梯度、更新参数 相当于一个训练周期。但仅靠一个训练周期很难将模型损失降到最低,因此需要循环迭代训练。
循环迭代训练就是在多个周期中一遍又一遍的重复这个过程,这就是在训练一个模型。
为了完成模型的训练,我们需要为这个训练过程定义几个函数:
- 单次训练函数,也就是完成预测、损失、梯度的计算以及参数更新这一个训练周期。
- 训练循环函数,也就是用不同的数据集重复调用单次训练函数多次。
- 可视化函数,用于观察训练过程中模型参数的变化。
下面首先定义单批次训练函数
- 使用当前参数计算预测值
- 预测值减真实值得到损失
- 通过之前的公式分别计算参数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]
- 上面左图可以看到:后面几次迭代(epoch3-epoch8)预测曲线(绿色)向训练数据(蓝色点)的拟合进度几乎停滞。
- 上面右图可以看到:后面几次迭代的参数几乎不再更新。
不确定是与学习率和小批量数量哪个有关系,我们先增大学习率到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]
- 损失在反复振荡后能够接近最小值,但需要的训练批次和训练时间明显增加。
- 之所以会反复振荡,是因为当小批量太小时,单个数据点所反应的损失永远不会稳定,这也决定了它只能徘徊在最小值附近却无法到达。
上面几个参数更新的过程都不太理想,下面尝试对数据特征进行变换,看看效果如何。
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绘图学习笔记
- 线性回归从零实现