文章目录
十一、二 模型选择与过拟合和欠拟合
1、模型的选择
-
训练误差:模型在训练数据上的误差
-
泛化误差:模型在新数据上的误差
-
验证数据集:一个用来评估模型好坏的数据集
-
测试数据集:只用一次的数据集。
训练集是近10年的高考题,验证集是最近一年的高考题,测试集就是要考的高考题
K-折交叉验证(K-Fold Cross-Validation)是一种常用的模型验证技术,主要用于评估统计分析或机器学习模型在独立数据集上的性能。其核心思想是将原始数据集分为K个大小相似的子集(或“折”),然后重复K次训练和验证过程。在每次迭代中,选择一个子集作为验证集,其余K-1个子集作为训练集。通过这K次迭代,每个子集都充当过一次验证集,从而可以得到K个评估指标。
在没有足够多数据时使用
K-折交叉验证的步骤大致如下:
- 将原始数据集随机分成K个大小相等的子集(或尽可能相等)。
- 对于每个子集i(i=1,2,…,K),将其作为验证集,其余K-1个子集作为训练集。
- 在训练集上训练模型,并在验证集上评估模型的性能(例如,计算分类准确率、回归误差等)。
- 重复步骤2和3,直到每个子集都充当过一次验证集,从而得到K个评估指标。
- 计算这K个评估指标的平均值,作为模型在该数据集上的最终性能评估结果。
训练数据集:训练模型参数
验证数据集:选择模型超参数
非大数据集上通常使用k-折交叉验证
2、过拟合和欠拟合
- 模型的容量(Capacity)或表达能力是指模型拟合复杂函数的能力,它从本质上描述了整个模型的拟合能力的大小。这个能力体现在模型可以表示的函数集的大小,即模型的假设空间的大小。
模型的容量不足,它可能无法很好地表示数据,这会导致欠拟合
;而如果模型的容量过大,它可能会过分拟合
数据
过拟合是指模型在训练数据上表现得非常好,但在测试数据性能较差的现象
欠拟合是指模型在训练数据上的表现就很差,更不用说在测试数据上的表现了
3、估计模型容量
难以在不同的种类算法之间比较,例如树模型和神经网络
给定一个模型种类,将有两个主要因素:参数的个数,参数值的选择范围
为了解释模型复杂度,我们以多项式函数拟合为例。给定一个由标量数据特征 x x x和对应的标量标签 y y y组成的训练数据集,多项式函数拟合的目标是找一个 K K K阶多项式函数
y ^ = b + ∑ k = 1 K x k w k \hat{y} = b + \sum_{k=1}^K x^k w_k y^=b+k=1∑Kxkwk
来近似 y y y。在上式中, w k w_k wk是模型的权重参数, b b b是偏差参数。与线性回归相同,多项式函数拟合也使用平方损失函数。特别地,一阶多项式函数拟合又叫线性函数拟合。
4、线性分类器的VC维
- VC维用于衡量一个模型的复杂度和学习能力。
传统的定义是:对一个指示函数集,如果存在H个样本能够被函数集中的函数按所有可能的2的H次方种形式分开,则称函数集能够把H个样本打散;函数集的VC维就是它能打散的最大样本数目H。若对任意数目的样本都有函数能将它们打散,则函数集的VC维是无穷大。
- VC维反映了函数集的学习能力,VC维越大则学习机器越复杂(容量越大)。
- 在机器学习中,VC维与偏差和方差之间存在关系,模型的VC维越大,其方差通常也越大。因此,在选择合适的模型时,需要考虑模型的复杂性对泛化性能的影响。
但是,尚没有通用的关于任意函数集VC维计算的理论,只对一些特殊的函数集知道其VC维。例如,在N维空间中线性分类器和线性实函数的VC维是N+1。
VC 维的用处:
- 提供为什么一个模型好的理论依据,它可以衡量训练误差和泛化误差之间的间隔。但深度学习中很少使用,衡量不是很准确,计算深度学习模型的VC维很困难
数据复杂度
多个重要因素,样本个数,每个样本的元素个数,时间、空间结构,多样性
总结:①模型容量需要匹配数据复杂度,否则可能导致欠拟合和过拟合②统计机器学习提供数学工具来衡量模型复杂度③实际中一般靠观察训练误差和验证误差
5、过拟合欠拟合的代码实现 🔥
①生成数据集
max_degree = 20 # 定义多项式的最大阶数,但实际上我们可能不需要这么高的阶数
n_train, n_test = 100, 100 # 训练和测试数据集的大小,各100个样本
true_w = np.zeros(max_degree) # 初始化一个长度为max_degree的零数组来存储多项式的系数
true_w[0:4] = np.array([5, 1.2, -3.4, 5.6]) # 只设置了前四个系数,其余为0
# 生成一个(n_train + n_test, 1)的二维数组,其中元素来自正态分布
features = np.random.normal(size=(n_train + n_test, 1))
# 打乱features数组的顺序,但这一步通常在实际应用中不是必需的
np.random.shuffle(features)
# 使用np.power生成多项式特征,但这里可能存在问题,因为我们没有按阶数来组织它们
poly_features = np.power(features, np.arange(max_degree).reshape(1, -1))
# 这里的问题在于gamma函数的使用。通常我们不需要用gamma函数来归一化多项式特征
for i in range(max_degree):
poly_features[:, i] /= math.gamma(i + 1) # gamma(n)=(n-1)!,但这不是归一化的常见方法
# 使用多项式特征和真实系数计算标签值
labels = np.dot(poly_features, true_w)
# 添加一些噪声到标签中,使其更接近真实世界的数据
labels += np.random.normal(scale=0.1, size=labels.shape)
max_degree = 20
定义多项式的最大阶数。但在实际应用中,阶数可能不需要这么高,因为这可能导致过拟合。
2-3. n_train, n_test = 100, 100
定义训练和测试数据集的大小。
-
true_w = np.zeros(max_degree)
初始化一个长度为max_degree的零数组,用于存储多项式的系数。 -
true_w[0:4] = np.array([5, 1.2, -3.4, 5.6])
只设置了前四个多项式系数,其余保持为零。这意味着我们实际上只关心四阶多项式。 -
features = np.random.normal(size=(n_train + n_test, 1))
生成一个(n_train + n_test, 1)的二维数组,其中元素来自标准正态分布。 -
np.random.shuffle(features)
打乱features数组的顺序。 -
poly_features = np.power(features, np.arange(max_degree).reshape(1, -1))
生成多项式特征。但是,这样生成的特征没有按阶数组织,即poly_features[:, 0]是所有x^0,
poly_features[:, 1]是所有x^1,依此类推。 -
labels = np.dot(poly_features, true_w)
使用多项式特征和真实系数计算标签值。但由于true_w
中只有前四个系数是非零的,其余的计算是多余的。 -
labels += np.random.normal(scale=0.1, size=labels.shape)
向标签中添加噪声,使其更接近真实世界的数据。
②定义评估损失
def evaluate_loss(net, data_iter, loss): #@save
"""评估给定数据集上模型的损失"""
metric = d2l.Accumulator(2) # 损失的总和,样本数量
for X, y in data_iter:
out = net(X)
y = y.reshape(out.shape)
l = loss(out, y)
metric.add(l.sum(), l.numel())
return metric[0] / metric[1]
- 函数定义:
def evaluate_loss(net, data_iter, loss):
这个函数接受三个参数:
net
: 一个神经网络模型。data_iter
: 一个迭代器,用于遍历数据集。每次迭代返回一批输入数据X
和对应的标签y
。loss
: 损失函数,用于计算预测输出out
和真实标签y
之间的差异。
- 初始化累加器:
metric = d2l.Accumulator(2)
- 遍历数据集:
for X, y in data_iter:
对于数据迭代器 data_iter
中的每一批数据,都会执行以下操作:
4. 前向传播:
out = net(X)
将输入数据 X
传递给神经网络模型 net
,得到预测输出 out
。
5. 处理标签的形状:
y = y.reshape(out.shape)
- 这里假设了
y
的形状可能需要调整以匹配out
的形状。这在某些情况下是必要的,尤其是当标签是多维的(例如,在多分类任务中,标签可能是独热编码的)并且模型输出了与这些标签形状相同的概率分布时。但请注意,如果y
和out
本来就具有相同的形状,这一步可能是不必要的。
- 计算损失:
l = loss(out, y)
- 使用损失函数
loss
计算预测输出out
和真实标签y
之间的损失。
- 累加损失和样本数量:
metric.add(l.sum(), l.numel())
- 将当前批次的损失总和(通过
l.sum()
计算)添加到累加器的第一个值中,并将当前批次的样本数量(通过l.numel()
计算,它返回张量中元素的数量)添加到累加器的第二个值中。
- 返回平均损失:
return metric[0] / metric[1]
最后,返回累加器中的损失总和除以样本数量,得到平均损失。
numel()函数:返回数组中元素的个数
③定义训练函数
def train(train_features, test_features, train_labels, test_labels,
num_epochs=400):
loss = nn.MSELoss(reduction='none')
input_shape = train_features.shape[-1]
# 不设置偏置,因为我们已经在多项式中实现了它
net = nn.Sequential(nn.Linear(input_shape, 1, bias=False))
batch_size = min(10, train_labels.shape[0])
train_iter = d2l.load_array((train_features, train_labels.reshape(-1,1)),
batch_size)
test_iter = d2l.load_array((test_features, test_labels.reshape(-1,1)),
batch_size, is_train=False)
trainer = torch.optim.SGD(net.parameters(), lr=0.01)
animator = d2l.Animator(xlabel='epoch', ylabel='loss', yscale='log',
xlim=[1, num_epochs], ylim=[1e-3, 1e2],
legend=['train', 'test'])
for epoch in range(num_epochs):
d2l.train_epoch_ch3(net, train_iter, loss, trainer)
if epoch == 0 or (epoch + 1) % 20 == 0:
animator.add(epoch + 1, (evaluate_loss(net, train_iter, loss),
evaluate_loss(net, test_iter, loss)))
print('weight:', net[0].weight.data.numpy())
定义 train
的函数,用于训练一个简单的神经网络模型。该函数使用均方误差(MSE)作为损失函数,并跟踪在训练集和测试集上的损失。
-
参数:
train_features
,test_features
: 训练集和测试集的特征。train_labels
,test_labels
: 训练集和测试集的标签。num_epochs
: 训练的轮数,默认为 400。
-
初始化:
loss
: 初始化一个均方误差损失函数,设置为'none'
,这意味着返回的是每个元素的损失,而不是批量中损失的平均值或总和。input_shape
: 从训练特征中获取输入的形状(最后一个维度)。net
: 创建一个简单的线性神经网络,没有偏置项(因为可能已经在多项式中实现了它)。batch_size
: 设置为训练标签数量的最小值或 10(取两者中的较小值)。train_iter
,test_iter
: 使用d2l.load_array
函数创建训练集和测试集的数据迭代器。trainer
: 初始化一个随机梯度下降(SGD)优化器,学习率设置为 0.01。animator
: 创建一个用于可视化损失随时间变化的动画对象。
-
训练循环:
- 使用
for
循环遍历每个训练轮次。 - 在每个轮次中,使用
d2l.train_epoch_ch3
函数进行一轮的训练(该函数的具体实现未在此代码中给出,但通常涉及前向传播、计算损失、反向传播和参数更新)。 - 如果当前轮次是 0 或每 20 轮,就计算当前轮次在训练集和测试集上的损失,并将损失添加到动画器中。
- 使用
④三阶多项式函数拟合
# 从多项式特征中选择前4个维度,即1,x,x^2/2!,x^3/3!
train(poly_features[:n_train, :4], poly_features[n_train:, :4],
labels[:n_train], labels[n_train:])
下面是我运行的拟合结果,拟合结果只能说还行😕😕
⑤线性函数拟合(欠拟合)
模拟下欠拟合的情况
# 从多项式特征中选择前2个维度,即1和x
train(poly_features[:n_train, :2], poly_features[n_train:, :2],
labels[:n_train], labels[n_train:])
欠拟合的情况,运行的非常好,达到预期 😃😃
⑤高阶多项式函数拟合(过拟合)
- 尝试使用一个阶数过高的多项式来训练模型。 在这种情况下,没有足够的数据用于学到高阶系数应该具有接近于零的值。 因此,这个过于复杂的模型会轻易受到训练数据中噪声的影响。 虽然训练损失可以有效地降低,但测试损失仍然很高。 结果表明,复杂模型对数据造成了过拟合。
# 从多项式特征中选取所有维度
train(poly_features[:n_train, :], poly_features[n_train:, :],
labels[:n_train], labels[n_train:], num_epochs=1500)
下面是我执行的结果,可以发现在epoch=600
左右开始发生过拟合💪💪
Tips: 迭代次数有点多,动图展示可能有点慢 🐟🐟
十三、权重衰退(Decay)
1、使用均方范数作为限制
- 现在介绍解决过拟合的常见方法
权重衰减等价于 L 2 L_2 L2 范数正则化(regularization)。正则化通过为模型损失函数添加惩罚项使学出的模型参数值较小,是应对过拟合的常用手段。我们先描述 L 2 L_2 L2范数正则化,再解释它为何又称权重衰减。
L 2 L_2 L2范数正则化在模型原损失函数基础上添加 L 2 L_2 L2范数惩罚项,从而得到训练所需要最小化的函数。 L 2 L_2 L2范数惩罚项指的是模型权重参数每个元素的平方和与一个正的常数的乘积。以3.1节(线性回归)中的线性回归损失函数
ℓ ( w 1 , w 2 , b ) = 1 n ∑ i = 1 n 1 2 ( x 1 ( i ) w 1 + x 2 ( i ) w 2 + b − y ( i ) ) 2 \ell(w_1, w_2, b) = \frac{1}{n} \sum_{i=1}^n \frac{1}{2}\left(x_1^{(i)} w_1 + x_2^{(i)} w_2 + b - y^{(i)}\right)^2 ℓ(w1,w2,b)=n1i=1∑n21(x1(i)w1+x2(i)w2+b−y(i))2
为例,其中 w 1 , w 2 w_1, w_2 w1,w2是权重参数, b b b是偏差参数,样本 i i i的输入为 x 1 ( i ) , x 2 ( i ) x_1^{(i)}, x_2^{(i)} x1(i),x2(i),标签为 y ( i ) y^{(i)} y(i),样本数为 n n n。将权重参数用向量 w = [ w 1 , w 2 ] \boldsymbol{w} = [w_1, w_2] w=[w1,w2]表示,带有 L 2 L_2 L2范数惩罚项的新损失函数为
ℓ ( w 1 , w 2 , b ) + λ 2 n ∥ w ∥ 2 , \ell(w_1, w_2, b) + \frac{\lambda}{2n} \|\boldsymbol{w}\|^2, ℓ(w1,w2,b)+2nλ∥w∥2,
其中超参数 λ > 0 \lambda > 0 λ>0。当权重参数均为0时,惩罚项最小。当 λ \lambda λ较大时,惩罚项在损失函数中的比重较大,这通常会使学到的权重参数的元素较接近0。当 λ \lambda λ设为0时,惩罚项完全不起作用。上式中 L 2 L_2 L2范数平方 ∥ w ∥ 2 \|\boldsymbol{w}\|^2 ∥w∥2展开后得到 w 1 2 + w 2 2 w_1^2 + w_2^2 w12+w22。有了 L 2 L_2 L2范数惩罚项后,在小批量随机梯度下降中,我们将线性回归一节中权重 w 1 w_1 w1和 w 2 w_2 w2的迭代方式更改为
w 1 ← ( 1 − η λ ∣ B ∣ ) w 1 − η ∣ B ∣ ∑ i ∈ B x 1 ( i ) ( x 1 ( i ) w 1 + x 2 ( i ) w 2 + b − y ( i ) ) , w 2 ← ( 1 − η λ ∣ B ∣ ) w 2 − η ∣ B ∣ ∑ i ∈ B x 2 ( i ) ( x 1 ( i ) w 1 + x 2 ( i ) w 2 + b − y ( i ) ) . \begin{aligned} w_1 &\leftarrow \left(1- \frac{\eta\lambda}{|\mathcal{B}|} \right)w_1 - \frac{\eta}{|\mathcal{B}|} \sum_{i \in \mathcal{B}}x_1^{(i)} \left(x_1^{(i)} w_1 + x_2^{(i)} w_2 + b - y^{(i)}\right),\\ w_2 &\leftarrow \left(1- \frac{\eta\lambda}{|\mathcal{B}|} \right)w_2 - \frac{\eta}{|\mathcal{B}|} \sum_{i \in \mathcal{B}}x_2^{(i)} \left(x_1^{(i)} w_1 + x_2^{(i)} w_2 + b - y^{(i)}\right). \end{aligned} w1w2←(1−∣B∣ηλ)w1−∣B∣ηi∈B∑x1(i)(x1(i)w1+x2(i)w2+b−y(i)),←(1−∣B∣ηλ)w2−∣B∣ηi∈B∑x2(i)(x1(i)w1+x2(i)w2+b−y(i)).
可见, L 2 L_2 L2范数正则化令权重 w 1 w_1 w1和 w 2 w_2 w2先自乘小于1的数,再减去不含惩罚项的梯度。因此, L 2 L_2 L2范数正则化又叫权重衰减。权重衰减通过惩罚绝对值较大的模型参数为需要学习的模型增加了限制,这可能对过拟合有效。实际场景中,我们有时也在惩罚项中添加偏差元素的平方和。
绿线是损失函数的取值, 黄线是惩罚项的取值, 两者都是圈越大取值越大
2、演示对最优解的情况
新的损失函数由两项组成,此时求导后,梯度有两项了,一项将w向绿线中心拉,一项将w向原点拉进,最后将在w*点达到一个平衡,中心点是最优解但是容易过拟合 所以要拉动ta,拉动的方法就是用这个平方项把ta往左下角拉
参数更新法则
知识串联:这里就是吴恩达老师讲的正则化
总结:
权重衰退通过L2正则项使得模型参数不会过大,从而控制模型复杂度
正则项权重是控制模型复杂度的超参数
3、权重衰减的实现 🔥
①造数据(合成数据集)
- 0.05为偏差,x为随机的数, c为噪音
n_train, n_test, num_inputs, batch_size = 20, 100, 200, 5
true_w, true_b = torch.ones((num_inputs, 1)) * 0.01, 0.05
train_data = d2l.synthetic_data(true_w, true_b, n_train)
train_iter = d2l.load_array(train_data, batch_size)
test_data = d2l.synthetic_data(true_w, true_b, n_test)
test_iter = d2l.load_array(test_data, batch_size, is_train=False)
-
定义变量:
n_train
: 训练数据集中的样本数量,设置为20。n_test
: 测试数据集中的样本数量,设置为100。num_inputs
: 特征的数量,设置为200。batch_size
: 批量大小,即每次迭代时从数据集中读取的样本数量,设置为5。
-
生成真实权重和偏置:
true_w
: 真实的权重向量,形状为(num_inputs, 1)
,每个元素的值都是0.01。true_b
: 真实的偏置项,值为0.05。
-
生成训练数据:
- 使用
d2l.synthetic_data
函数 基于给定的真实权重true_w
和偏置true_b
,以及训练样本数量n_train
,生成一个线性关系的数据集。 train_data
包含了特征和标签。
- 使用
-
创建训练迭代器:
- 使用
d2l.load_array
函数 加载训练数据,并返回一个迭代器train_iter
。这个迭代器可以在训练循环中使用,以批量地读取数据。 - 批量大小设置为
batch_size
。
- 使用
-
生成测试数据:
- 同样使用
d2l.synthetic_data
函数,但基于相同的真实权重和偏置,以及测试样本数量n_test
,生成测试数据集。 test_data
也包含了特征和标签。
- 同样使用
-
创建测试迭代器:
- 使用
d2l.load_array
函数加载测试数据,并返回一个迭代器test_iter
。与训练迭代器类似,但注意这里is_train=False
,这通常用于指示该迭代器是在测试模式下使用,可能影响数据的预处理或增强方式(尽管在这个简单的线性回归例子中可能没有影响)。
- 使用
最终,train_iter
和test_iter
可以用于训练循环和测试循环中,以批量地读取数据并评估模型的性能。
②初始化模型参数和范数惩罚
def init_params():
w = torch.normal(0, 1, size=(num_inputs, 1), requires_grad=True)
b = torch.zeros(1, requires_grad=True)
return [w, b]
定义 𝐿2 范数惩罚
- 实现这一惩罚最方便的方法是对所有项求平方后并将它们求和。
def l2_penalty(w):
return torch.sum(w.pow(2)) / 2
③定义训练代码实现
def train(lambd):
w, b = init_params()
net, loss = lambda X: d2l.linreg(X, w, b), d2l.squared_loss
num_epochs, lr = 100, 0.003
animator = d2l.Animator(xlabel='epochs', ylabel='loss', yscale='log',
xlim=[5, num_epochs], legend=['train', 'test'])
for epoch in range(num_epochs):
for X, y in train_iter:
# 增加了L2范数惩罚项,
# 广播机制使l2_penalty(w)成为一个长度为batch_size的向量
l = loss(net(X), y) + lambd * l2_penalty(w)
l.sum().backward()
d2l.sgd([w, b], lr, batch_size)
if (epoch + 1) % 5 == 0:
animator.add(epoch + 1, (d2l.evaluate_loss(net, train_iter, loss),
d2l.evaluate_loss(net, test_iter, loss)))
print('w的L2范数是:', torch.norm(w).item())
-
函数参数:
lambd
: L2正则化的系数(也称为λ或lambda)。作为超参数
-
初始化权重和偏置:
- 调用
init_params()
函数( 初始化权重w
和偏置b
为一些随机值或零)。
- 调用
-
定义模型和网络损失:
- 使用
lambda
表达式定义了线性回归模型net
,该模型接受输入X
、权重w
和偏置b
。 - 定义损失函数为均方误差损失
squared_loss
。
- 使用
-
设置超参数和动画器:
num_epochs
: 训练轮数,设置为100。lr
: 学习率,设置为0.003。animator
: 使用d2l.Animator
类创建一个动画器,用于可视化训练过程中训练和测试损失的变化。
-
训练循环:
- 遍历每一个训练轮次(
epoch
)。 - 在每个轮次内,遍历训练数据的每一个批次(
X, y
)。 - 计算损失,包括数据拟合损失和L2正则化项(
lambd * l2_penalty(w)
)。 - 使用
l.sum().backward()
计算梯度并反向传播。 - 使用随机梯度下降(SGD)更新权重和偏置。
- 遍历每一个训练轮次(
-
评估和可视化:
- 每5个轮次,计算并记录在训练和测试数据集上的损失。
- 使用
animator.add()
将损失值添加到动画器中,以便稍后可视化。
-
输出权重的L2范数:
- 在训练结束后,打印出权重
w
的L2范数,以检查正则化的效果。
- 在训练结束后,打印出权重
④开始训练
下面分三种情况进行描述
(1)忽略正则化直接训练
用lambd = 0禁用权重衰减后运行这个代码。 注意,这里训练误差有了减少,但测试误差没有减少, 这意味着出现了严重的过拟合。
w的L2范数是: 13.71298885345459
(2)使用权重衰减
下面,我们使用权重衰减来运行代码。 注意,在这里训练误差增大,但测试误差减小。 这正是我们期望从正则化中得到的效果
w的L2范数是: 0.387514591217041
lambd往大了调,是为了降低w的l2 norm,达到避免过拟合的效果
⑤简洁实现
下面给出简洁实现,以便以后学习直接调用
def train_concise(wd):
net = nn.Sequential(nn.Linear(num_inputs, 1))
for param in net.parameters():
param.data.normal_()
loss = nn.MSELoss(reduction='none')
num_epochs, lr = 100, 0.003
# 偏置参数没有衰减
trainer = torch.optim.SGD([
{"params":net[0].weight,'weight_decay': wd},
{"params":net[0].bias}], lr=lr)
animator = d2l.Animator(xlabel='epochs', ylabel='loss', yscale='log',
xlim=[5, num_epochs], legend=['train', 'test'])
for epoch in range(num_epochs):
for X, y in train_iter:
trainer.zero_grad()
l = loss(net(X), y)
l.mean().backward()
trainer.step()
if (epoch + 1) % 5 == 0:
animator.add(epoch + 1,
(d2l.evaluate_loss(net, train_iter, loss),
d2l.evaluate_loss(net, test_iter, loss)))
print('w的L2范数:', net[0].weight.norm().item())
net[0] 是 Linear(in_features=100, out_features=1, bias=True)
使用PyTorch的nn
(神经网络)和optim
(优化器)模块来训练一个带有L2正则化的线性回归模型。
-
定义网络:
- 使用
nn.Sequential
来定义一个简单的线性回归模型,该模型包含一个nn.Linear
层,它将num_inputs
个输入特征映射到一个输出。
- 使用
-
初始化参数:
- 使用
for
循环遍历网络中的所有参数(权重和偏置)。 - 使用
param.data.normal_()
将权重参数初始化为正态分布(均值为0,标准差为1)。注意,偏置参数通常初始化为0,但这里也使用了正态分布初始化,不过因为正态分布关于均值对称,所以初始化为0的效果和标准差很小的正态分布相近。
- 使用
-
定义损失函数:
- 使用
nn.MSELoss
定义均方误差损失函数,并设置reduction='none'
,这样损失函数会为每个样本输出一个损失值,而不是它们的平均值。
- 使用
-
设置优化器和超参数:
- 使用
torch.optim.SGD
定义随机梯度下降优化器。 - 为权重参数设置权重衰减(即L2正则化)的系数为
wd
。 - 偏置参数没有设置权重衰减(
'weight_decay': wd
只应用于权重)。 - 设置学习率为
lr
。
- 使用
-
设置动画器:
- 使用
d2l.Animator
创建一个动画器,用于可视化训练过程中的损失变化。
- 使用
-
训练循环:
- 遍历每一个训练轮次(
epoch
)。 - 在每个轮次内,遍历训练数据的每一个批次(
X, y
)。 - 在每次迭代开始时,使用
trainer.zero_grad()
清除之前累积的梯度。 - 计算损失值
l
。 - 对损失值的平均值进行反向传播,使用
l.mean().backward()
。 - 使用
trainer.step()
更新权重和偏置。
- 遍历每一个训练轮次(
-
评估和可视化:
- 每5个轮次,计算并记录在训练和测试数据集上的损失。
- 使用
animator.add()
将损失值添加到动画器中。
-
输出权重的L2范数:
- 在训练结束后,打印出第一个线性层(
net[0]
)的权重的L2范数。
- 在训练结束后,打印出第一个线性层(
十四、丢弃法(Dropout)
- 深度学习模型常常使用丢弃法(dropout)来应对过拟合问题
一个好的模型需要对输入数据的扰动鲁棒,使用有噪音的数据等价于
Tikhonov正则,
- 丢弃法:在层之间加入噪音
1、无差别加入噪音
E
(
X
i
)
=
0
∗
p
+
X
i
/
(
1
−
p
)
∗
(
1
−
p
)
=
X
i
E(X_i) = 0 * p + X_i/(1-p) * (1-p) = X _i
E(Xi)=0∗p+Xi/(1−p)∗(1−p)=Xi
2、丢弃法的使用
举例说明:
多层感知机描述了一个单隐藏层的多层感知机。其中输入个数为4,隐藏单元个数为5,且隐藏单元 h i h_i hi( i = 1 , … , 5 i=1, \ldots, 5 i=1,…,5)的计算表达式为
h i = ϕ ( x 1 w 1 i + x 2 w 2 i + x 3 w 3 i + x 4 w 4 i + b i ) h_i = \phi\left(x_1 w_{1i} + x_2 w_{2i} + x_3 w_{3i} + x_4 w_{4i} + b_i\right) hi=ϕ(x1w1i+x2w2i+x3w3i+x4w4i+bi)
这里 ϕ \phi ϕ是激活函数, x 1 , … , x 4 x_1, \ldots, x_4 x1,…,x4是输入,隐藏单元 i i i的权重参数为 w 1 i , … , w 4 i w_{1i}, \ldots, w_{4i} w1i,…,w4i,偏差参数为 b i b_i bi。当对该隐藏层使用丢弃法时,该层的隐藏单元将有一定概率被丢弃掉。设丢弃概率为 p p p,那么有 p p p的概率 h i h_i hi会被清零,有 1 − p 1-p 1−p的概率 h i h_i hi会除以 1 − p 1-p 1−p做拉伸。丢弃概率是丢弃法的超参数。具体来说,设随机变量 ξ i \xi_i ξi为0和1的概率分别为 p p p和 1 − p 1-p 1−p。使用丢弃法时我们计算新的隐藏单元 h i ′ h_i' hi′
h i ′ = ξ i 1 − p h i h_i' = \frac{\xi_i}{1-p} h_i hi′=1−pξihi
由于 E ( ξ i ) = 1 − p E(\xi_i) = 1-p E(ξi)=1−p,因此
E ( h i ′ ) = E ( ξ i ) 1 − p h i = h i E(h_i') = \frac{E(\xi_i)}{1-p}h_i = h_i E(hi′)=1−pE(ξi)hi=hi
即丢弃法不改变其输入的期望值
红色箭头表示使用丢弃法后
3、推理中的丢弃法
推理中的丢弃法并不是直接应用于推理或预测阶段的技术。实际上,丢弃法主要是在模型的训练阶段使用,以防止过拟合现象。
-
丢弃法的基本原理是在每次训练迭代中随机丢弃一部分神经元的输出,使得模型对输入数据有更好的鲁棒性。这通过减少每个神经元之间的依赖关系来增加模型的泛化能力。在训练时,丢弃法可以看作是一种集成学习的方法,因为每个训练迭代都在一个不同的、随机选择的神经网络子集上进行。
-
然而,在推理或预测阶段,我们通常不会使用丢弃法。
相反,我们会使用完整的、已经训练好的模型来进行预测
。这是因为在推理时,我们希望模型能够基于所有的神经元和连接来做出决策,而不是仅仅依赖于一部分神经元。 -
因此,虽然丢弃法在训练过程中是一个强大的工具,但在推理或预测阶段并不适用。
总结:
丢弃法将一些输出项随机置0来控制模型复杂度
常作用在多层感知机的隐藏层输出上
丢弃概率是控制模型复杂度的超参数
5、丢弃法的代码实现 🔥
- 要实现单层的暂退法函数, 我们从均匀分布 𝑈[0,1]中抽取样本,样本数与这层神经网络的维度一致。 然后我们保留那些对应样本大于 𝑝 的节点,把剩下的丢弃。
① 实现dropout_layer 函数
def dropout_layer(X, dropout):
assert 0 <= dropout <= 1
# 在本情况中,所有元素都被丢弃
if dropout == 1:
return torch.zeros_like(X)
# 在本情况中,所有元素都被保留
if dropout == 0:
return X
mask = (torch.rand(X.shape) > dropout).float()
return mask * X / (1.0 - dropout)
rand是产生0-1之间的均匀分布,randn是产生均值为0,方差为1的高斯分布
因为rand是在0到1均匀分布的,所以>dropout值的概率就是dropout
dropout_layer
函数是一个实现丢弃法(Dropout)的自定义层。这个函数接收两个参数:X
(输入数据)和 dropout
(丢弃率),然后基于丢弃率来随机丢弃X
中的一些元素。
-
首先,函数通过断言(
assert
)确保dropout
的值在[0, 1]
范围内。 -
如果
dropout
等于 1,则函数返回与X
形状相同且所有元素都为 0 的张量,因为这种情况下所有的元素都会被丢弃。 -
如果
dropout
等于 0,则函数直接返回X
,因为没有任何元素需要被丢弃。 -
对于
dropout
在(0, 1)
范围内的情况,函数首先生成一个与X
形状相同的随机数矩阵,其中每个元素都是从[0, 1)
范围内均匀采样的。然后,通过比较这个随机数矩阵和dropout
值,生成一个掩码(mask)。这个掩码中,值大于dropout
的元素被置为 1(保留),而值小于或等于dropout
的元素被置为 0(丢弃)。 -
最后,函数将掩码与
X
相乘,实现丢弃操作。但是,为了保持层的输出在训练阶段和推理阶段的期望相同(即不进行缩放),需要将保留的元素除以(1 - dropout)
。这是因为,在训练时,由于一部分元素被丢弃了,所以剩下的元素实际上被放大了(1 / (1 - dropout))
倍。而在推理时,我们通常不使用丢弃法,所以需要用这个缩放因子来确保输出的一致性。
注意:在训练神经网络时,通常会在每个训练迭代中调用此函数。然而,在推理或测试时,应该直接使用原始的、未经过丢弃法处理的模型,或者使用一个等效的缩放模型(即,将每个神经元的权重乘以
(1 - dropout)
),以避免在推理时引入额外的随机性。
② 测试dropout_layer 函数
X= torch.arange(16, dtype = torch.float32).reshape((2, 8))
print(X)
print(dropout_layer(X, 0.))
print(dropout_layer(X, 0.5))
print(dropout_layer(X, 1.))
③开始定义模型
下面给出代码并逐行注释
# 设定两个dropout的比率
dropout1, dropout2 = 0.2, 0.5
# 定义一个名为Net的神经网络类,继承自nn.Module
class Net(nn.Module):
# 初始化函数,当创建Net的实例时会自动调用
def __init__(self, num_inputs, num_outputs, num_hiddens1, num_hiddens2, is_training=True):
# 调用父类nn.Module的初始化函数
super(Net, self).__init__()
# 存储输入特征的数量
self.num_inputs = num_inputs
# 设置模型的训练模式标志(但这种做法并不常见,通常使用net.train()和net.eval())
self.training = is_training
# 定义第一个全连接层,从num_inputs到num_hiddens1个神经元
self.lin1 = nn.Linear(num_inputs, num_hiddens1)
# 定义第一个dropout层,但这里并没有直接使用nn.Dropout,而是后面会调用的自定义函数
# (注意:这里没有实际的dropout层定义,只是一个计划使用的变量名)
# (但通常我们会直接在forward方法中使用nn.Dropout)
# 定义第二个全连接层,从num_hiddens1到num_hiddens2个神经元
self.lin2 = nn.Linear(num_hiddens1, num_hiddens2)
# 同上,这是计划使用的第二个dropout层的变量名
# 定义第三个全连接层,从num_hiddens2到num_outputs个神经元(通常是输出层)
self.lin3 = nn.Linear(num_hiddens2, num_outputs)
# 定义ReLU激活函数(通常用于隐藏层)
self.relu = nn.ReLU()
# 前向传播函数,定义了数据通过网络的方式
def forward(self, X):
# 将输入数据X通过第一个全连接层,并应用ReLU激活函数
H1 = self.relu(self.lin1(X.reshape((-1, self.num_inputs))))
# 检查当前是否处于训练模式,如果是,则应用dropout
if self.training == True:
# 调用自定义的dropout_layer函数
H1 = dropout_layer(H1, dropout1)
# 将H1通过第二个全连接层,并再次应用ReLU激活函数
H2 = self.relu(self.lin2(H1))
# 如果在训练模式,再次应用dropout
if self.training == True:
H2 = dropout_layer(H2, dropout2)
# 将H2通过第三个全连接层(输出层),得到最终的输出
out = self.lin3(H2)
# 返回输出
return out
# 实例化Net类,创建一个神经网络对象
# 注意:这里并没有传递is_training参数(尽管它在__init__方法中定义了),所以它将默认为False
net = Net(num_inputs, num_outputs, num_hiddens1, num_hiddens2)
6、简洁实现
为了方便后续调用,给出简洁实现代码
net = nn.Sequential(nn.Flatten(),
nn.Linear(784, 256),
nn.ReLU(),
# 在第一个全连接层之后添加一个dropout层
nn.Dropout(dropout1),
nn.Linear(256, 256),
nn.ReLU(),
# 在第二个全连接层之后添加一个dropout层
nn.Dropout(dropout2),
nn.Linear(256, 10))
def init_weights(m):
if type(m) == nn.Linear:
nn.init.normal_(m.weight, std=0.01)
net.apply(init_weights);
十五、数值稳定性
神经网络的梯度
反向传播
1、衰减和爆炸
- 当神经网络的层数较多时,模型的数值稳定性容易变差。假设一个层数为 L L L的多层感知机的第 l l l层 H ( l ) \boldsymbol{H}^{(l)} H(l)的权重参数为 W ( l ) \boldsymbol{W}^{(l)} W(l),输出层 H ( L ) \boldsymbol{H}^{(L)} H(L)的权重参数为 W ( L ) \boldsymbol{W}^{(L)} W(L)。为了便于讨论,不考虑偏差参数,且设所有隐藏层的激活函数为恒等映射(identity mapping) ϕ ( x ) = x \phi(x) = x ϕ(x)=x。给定输入 X \boldsymbol{X} X,多层感知机的第 l l l层的输出 H ( l ) = X W ( 1 ) W ( 2 ) … W ( l ) \boldsymbol{H}^{(l)} = \boldsymbol{X} \boldsymbol{W}^{(1)} \boldsymbol{W}^{(2)} \ldots \boldsymbol{W}^{(l)} H(l)=XW(1)W(2)…W(l)。此时,如果层数 l l l较大, H ( l ) \boldsymbol{H}^{(l)} H(l)的计算可能会出现衰减或爆炸。举个例子,假设输入和所有层的权重参数都是标量,如权重参数为0.2和5,多层感知机的第30层输出为输入 X \boldsymbol{X} X分别与 0. 2 30 ≈ 1 × 1 0 − 21 0.2^{30} \approx 1 \times 10^{-21} 0.230≈1×10−21(衰减)和 5 30 ≈ 9 × 1 0 20 5^{30} \approx 9 \times 10^{20} 530≈9×1020(爆炸)的乘积。类似地,当层数较多时,梯度的计算也更容易出现衰减或爆炸。
2、梯度爆炸
梯度爆炸的问题
- 值超出值域(infinity)
- 对于16位浮点数尤为严重(数值区间6e-5-6e4)
- 对学习率敏感
- 如果学习率太大>大参数值->更大的梯度
- 如果学习率太小->训练无进展
- 我们可能需要在训练过程不断调整学习率
3、梯度消失
梯度消失的问题
- 梯度值变成0
- 对16位浮点数尤为严重
- 训练没有进展
- 不管如何选择学习率
- 对于底部层尤为严重
- 仅仅顶部层训练的较好
- 无法让神经网络更深
当数值过大或者过小时会导致数值问题 ·
常发生在深度模型中,因为其会对个数累乘
十六、模型初始化和激活函数
目标:
①让梯度值在合理的范围内 例如[1e-6,1e3]
②将乘法变加法 ResNet,LSTM
③归一化:梯度归一化,梯度裁剪
④合理的权重初始和激活函数
让每层的方差是一个常数
权重初始化
①在合理值区间里随机初始参数
②训练开始的时候更容易有数值不稳定
③远离最优解的地方损失函数表面可能很复杂
④最优解附近表面会比较平
⑤使用W(0,0.01)来初始可能对小网络没问题,但不能保证深度神经网络
在概率论中方差Var(X)=E(X2)-E(X)2^, 而由于E(X)=0,所以这边直接E(X^2)=Var(X)
正向方差
反向方差
1、随机初始化模型参数
在神经网络中,通常需要随机初始化模型参数。下面我们来解释这样做的原因。
上图多层感知机。为了方便解释,假设输出层只保留一个输出单元 o 1 o_1 o1(删去 o 2 o_2 o2和 o 3 o_3 o3以及指向它们的箭头),且隐藏层使用相同的激活函数。如果将每个隐藏单元的参数都初始化为相等的值,那么在正向传播时每个隐藏单元将根据相同的输入计算出相同的值,并传递至输出层。在反向传播中,每个隐藏单元的参数梯度值相等。因此,这些参数在使用基于梯度的优化算法迭代后值依然相等。之后的迭代也是如此。在这种情况下,无论隐藏单元有多少,隐藏层本质上只有1个隐藏单元在发挥作用。因此,正如在前面的实验中所做的那样,我们通常将神经网络的模型参数,特别是权重参数,进行随机初始化。
2、PyTorch的默认随机初始化
随机初始化模型参数的方法有很多。在3.3节(线性回归的简洁实现)中,使用torch.nn.init.normal_()
使模型net
的权重参数采用正态分布的随机初始化方式。不过,PyTorch中nn.Module
的模块参数都采取了较为合理的初始化策略(不同类型的layer具体采样的哪一种初始化方法的可参考源代码),因此一般不用我们考虑。
3 、Xavier随机初始化
还有一种比较常用的随机初始化方法叫作Xavier随机初始化。
假设某全连接层的输入个数为
a
a
a,输出个数为
b
b
b,Xavier随机初始化将使该层中权重参数的每个元素都随机采样于均匀分布
U ( − 6 a + b , 6 a + b ) . U\left(-\sqrt{\frac{6}{a+b}}, \sqrt{\frac{6}{a+b}}\right). U(−a+b6,a+b6).
它的设计主要考虑到,模型参数初始化后,每层输出的方差不该受该层输入个数影响,且每层梯度的方差也不该受该层输出个数影响。
4、线性激活函数
合理的权重初始值和激活函数的选取可以提升数值稳定性
十七、PyTorch 神经网络基础
1、模型构造
定义神经网络:
net = nn.Sequential(nn.Linear(20, 256), nn.ReLU(), nn.Linear(256, 10))
- 使用
nn.Sequential
, 定义了一个简单的序贯模型,它包含两个线性层(nn.Linear
)和一个ReLU激活函数。第一个线性层将输入从20个特征转换到256个特征,然后ReLU激活函数对这些特征进行非线性变换。最后,第二个线性层将256个特征转换到10个输出。
生成随机输入数据:
X = torch.rand(2, 20)
- 这里, 使用
torch.rand
函数生成了一个形状为(2, 20)
的张量,其中包含了从均匀分布(在0和1之间)中随机抽取的浮点数。这表示我们有两个样本,每个样本有20个特征。
通过神经网络传递输入数据:
net(X)
- 将输入数据
X
传递给神经网络net
时,它首先通过第一个线性层进行线性变换,然后应用ReLU激活函数,最后通过第二个线性层进行另一个线性变换。这个过程的结果是一个形状为(2, 10)
的张量,表示两个样本在10个输出类别上的得分或概率(如果这是一个分类任务的话)。
self=this指针
nn.ReLU()是构造了一个ReLU对象,并不是函数调用,而F.ReLU()是函数调用
class MySequential(nn.Module):
def __init__(self, *args):
super().__init__()
for idx, module in enumerate(args):
# 这里,module是Module子类的一个实例。我们把它保存在'Module'类的成员
# 变量_modules中。_module的类型是OrderedDict
self._modules[str(idx)] = module
def forward(self, X):
# OrderedDict保证了按照成员添加的顺序遍历它们
for block in self._modules.values():
X = block(X)
return X
MySequential
类试图模拟 PyTorch 内置的 nn.Sequential
的行为,但有一个小的不同:它使用模块在 args
中的索引(转换为字符串)作为其在 _modules
字典中的键。
下面是代码的详细解释:
-
初始化方法
__init__
:- 调用父类
nn.Module
的初始化方法super().__init__()
。 - 遍历传递给
__init__
的所有参数(这些参数应该是nn.Module
的子类实例)。 - 使用
enumerate
函数获取每个模块的索引(idx
)和模块本身(module
)。 - 将模块的索引转换为字符串,并作为键,将模块作为值存储在
_modules
字典中。在 PyTorch 中,_modules
是一个OrderedDict
,用于存储子模块,并确保它们在forward
方法中按照添加的顺序被遍历。
- 调用父类
-
前向传播方法
forward
:- 这个方法定义了数据如何通过网络传递。
- 遍历
_modules
字典中的所有值(即模块)。 - 将输入
X
传递给当前模块,并将输出重新赋值给X
,以便它在下一个模块中使用。 - 返回最终的输出
X
。
注意:虽然这段代码在功能上类似于 nn.Sequential
,但它使用索引作为键来存储模块,这可能会导致在后续的代码或调试中难以理解和跟踪模块。相比之下,nn.Sequential
使用简单的字符串(如 '0'
, '1'
, …)作为键,这更易于理解和跟踪。
X = block(X)
表示将输入数据X传递给一个名为block的神经网络层进行处理,并将处理后的结果重新赋值给X。
class FixedHiddenMLP(nn.Module):
def __init__(self):
super().__init__()
# 不计算梯度的随机权重参数。因此其在训练期间保持不变
self.rand_weight = torch.rand((20, 20), requires_grad=False)
self.linear = nn.Linear(20, 20)
def forward(self, X):
X = self.linear(X)
# 使用创建的常量参数以及relu和mm函数
X = F.relu(torch.mm(X, self.rand_weight) + 1)
# 复用全连接层。这相当于两个全连接层共享参数
X = self.linear(X)
# 控制流
while X.abs().sum() > 1:
X /= 2
return X.sum()
2、参数管理
print(*[(name, param.shape) for name, param in net[0].named_parameters()])
net[0]
:这假设net
是一个可以索引的对象(例如,一个列表或元组),并且它至少有一个元素,这个元素是一个PyTorch的神经网络模型(nn.Module
的子类)。net[0]
取出这个列表或元组中的第一个模型。net[0].named_parameters()
:这是PyTorch中nn.Module
类的一个方法,它返回模型中所有可训练参数的名称(name
)和参数本身(param
)的迭代器。- 列表推导
[(name, param.shape) for name, param in net[0].named_parameters()]
:对于模型net[0]
中的每一个参数,都创建一个元组,其中包含参数的名称和参数的形状(shape
)。 print(*...)
:这里使用了*
参数展开功能,它可以将列表或元组中的元素作为独立的参数传递给print
函数。所以,这段代码会打印出模型net[0]
中所有参数的名称和形状,每个参数占一行。
套娃开始 ,从嵌套块收集参数
内置初始化
def init_normal(m):
if type(m) == nn.Linear:
nn.init.normal_(m.weight, mean=0, std=0.01)
nn.init.zeros_(m.bias)
net.apply(init_normal)
net[0].weight.data[0], net[0].bias.data[0]
apply函数的功能是将传入的函数应用到指定的module上,不只是初始化,做什么都行
- 如果
m
的类型是nn.Linear
(即它是一个线性层),那么使用nn.init.normal_
函数来初始化权重m.weight
。这里,权重被初始化为均值为0、标准差为0.01的正态分布。 - 使用
nn.init.zeros_
函数来初始化偏置m.bias
。偏置被初始化为0。
net.apply(init_normal)
:
对net
中的每个子模块(如nn.Linear
层、nn.Conv2d
层等)应用init_normal
函数。这意味着,如果net
或其任何子模块包含nn.Linear
层,那么这些层的权重和偏置都会被上述方式初始化。
加下划线为原地操作,不加为复制后修改
参数绑定
# 我们需要给共享层一个名称,以便可以引用它的参数
shared = nn.Linear(8, 8)
net = nn.Sequential(nn.Linear(4, 8), nn.ReLU(),
shared, nn.ReLU(),
shared, nn.ReLU(),
nn.Linear(8, 1))
net(X)
# 检查参数是否相同
print(net[2].weight.data[0] == net[4].weight.data[0])
net[2].weight.data[0, 0] = 100
# 确保它们实际上是同一个对象,而不只是有相同的值
print(net[2].weight.data[0] == net[4].weight.data[0])
- 创建共享层并定义网络:
shared = nn.Linear(8, 8)
net = nn.Sequential(nn.Linear(4, 8), nn.ReLU(),
shared, nn.ReLU(),
shared, nn.ReLU(),
nn.Linear(8, 1))
这里,shared
被添加到net
两次。
- 检查参数是否相同:
print(net[2].weight.data[0] == net[4].weight.data[0])
由于net[2]
和net[4]
引用的是同一个nn.Linear
实例,它们的权重数据将是相同的,所以上述打印语句将输出全为True
的张量(假设权重数据的第一个元素是一个向量)。
- 修改共享层的权重并再次检查:
net[2].weight.data[0, 0] = 100
print(net[2].weight.data[0] == net[4].weight.data[0])
当 修改net[2].weight.data[0, 0]
时,由于net[2]
和net[4]
引用的是同一个对象,所以net[4].weight.data[0, 0]
也会被修改为100。
3、读写文件
加载和保存模型参数
首先定义MLP
class MLP(nn.Module):
def __init__(self):
super().__init__()
self.hidden = nn.Linear(20, 256)
self.output = nn.Linear(256, 10)
def forward(self, x):
return self.output(F.relu(self.hidden(x)))
net = MLP()
X = torch.randn(size=(2, 20))
Y = net(X)
存储文件
torch.save(net.state_dict(), 'mlp.params')
开始加载
clone = MLP()
clone.load_state_dict(torch.load('mlp.params'))
clone.eval()
eval()方法是将模型设置为评估模式
train模式(net.train())和eval模式(net.eval())。一般的神经网络中,这两种模式是一样的,只有当模型中存在dropout和batchnorm的时候才有区别。
十八、Kaggle房价预测实战 🔥
https://www.kaggle.com/c/house-prices-advanced-regression-techniques
沐神个人版房价预测竞赛链接如下:
https://www.kaggle.com/c/california-house-prices/discussion
1、数据集的获取
下载数据集, 将数据集缓存在本地目录(默认情况下为…/data)中, 并返回下载文件的名称。 如果缓存目录中已经存在此数据集文件,并且其sha-1与存储在DATA_HUB中的相匹配, 我们将使用缓存的文件,以避免重复的下载。
def download(name, cache_dir=os.path.join('..', 'data')): #@save
"""下载一个DATA_HUB中的文件,返回本地文件名"""
assert name in DATA_HUB, f"{name} 不存在于 {DATA_HUB}"
url, sha1_hash = DATA_HUB[name]
os.makedirs(cache_dir, exist_ok=True)
fname = os.path.join(cache_dir, url.split('/')[-1])
if os.path.exists(fname):
sha1 = hashlib.sha1()
with open(fname, 'rb') as f:
while True:
data = f.read(1048576)
if not data:
break
sha1.update(data)
if sha1.hexdigest() == sha1_hash:
return fname # 命中缓存
print(f'正在从{url}下载{fname}...')
r = requests.get(url, stream=True, verify=True)
with open(fname, 'wb') as f:
f.write(r.content)
return fname
(1)断言:使用 assert 语句确保 name 存在于 DATA_HUB 字典中。如果不存在,程序将抛出一个异常。
(2)使用 os.makedirs 创建缓存目录(如果它还不存在)。exist_ok=True 意味着如果目录已经存在,则不会抛出错误。
f.read(1048576)
:这是一个调用文件对象的 read 方法,用于读取文件内容。传递给 read 方法的参数 1048576 是一个整数,指定了读取的字节数。这个特定的数字是 1MB(因为 1048576 字节等于 1MB)
开始下载并解压如下
def download_extract(name, folder=None): #@save
"""下载并解压zip/tar文件"""
fname = download(name)
base_dir = os.path.dirname(fname)
data_dir, ext = os.path.splitext(fname)
if ext == '.zip':
fp = zipfile.ZipFile(fname, 'r')
elif ext in ('.tar', '.gz'):
fp = tarfile.open(fname, 'r')
else:
assert False, '只有zip/tar文件可以被解压缩'
fp.extractall(base_dir)
return os.path.join(base_dir, folder) if folder else data_dir
def download_all(): #@save
"""下载DATA_HUB中的所有文件"""
for name in DATA_HUB:
download(name)
2、查看数据便于后续处理
train_data
: 这是一个pandas DataFrame,通常用于存储表格型的数据。iloc[]
: 这是pandas DataFrame的一个基于整数位置的索引器,用于基于行和列的整数位置来选择数据。0:4
: 这选择了前4行(包括第0行,但不包括第4行)。在Python的切片语法中,结束索引是不包含的。[0, 1, 2, 3, -3, -2, -1]
: 这选择了特定的列。具体来说,它选择了第0列、第1列、第2列、第3列、倒数第3列、倒数第2列和最后一列。负索引是从后往前数的,所以-3
表示倒数第三列,-2
表示倒数第二列,-1
表示最后一列。
所以,train_data.iloc[0:4, [0, 1, 2, 3, -3, -2, -1]]
会返回一个新的DataFrame,它包含train_data
的前4行和指定的列(第0、1、2、3、倒数第3、倒数第2和最后一列)。
假设train_data
是这样的:
A B C D E F G
0 1 2 3 4 5 6 7
1 8 9 10 11 12 13 14
2 15 16 17 18 19 20 21
3 22 23 24 25 26 27 28
4 29 30 31 32 33 34 35
那么train_data.iloc[0:4, [0, 1, 2, 3, -3, -2, -1]]
会返回:
A B C D F G
0 1 2 3 4 6 7
1 8 9 10 11 13 14
2 15 16 17 18 20 21
3 22 23 24 25 27 28
3、数据预处理
进行简化处理,删除第一例(ID)
all_features = pd.concat((train_data.iloc[:, 1:-1], test_data.iloc[:, 1:]))
将train_data
DataFrame中除了第一列和最后一列的所有列与test_data
DataFrame中除了第一列的所有列合并在一起。
-
train_data.iloc[:, 1:-1]
: 这选择了train_data
DataFrame中从第二列开始到倒数第二列结束的所有列(不包括第一列和最后一列)。:
表示选择所有的行。1:-1
是列的切片,其中1
表示从第二列开始(因为索引是从0开始的),-1
表示到倒数第二列结束。
-
test_data.iloc[:, 1:]
: 这选择了test_data
DataFrame中从第二列开始到最后一列的所有列(不包括第一列)。:
表示选择所有的行。1:
是列的切片,其中1
表示从第二列开始,:
表示到最后一列结束。
-
pd.concat(...)
: 这将上面选择出来的两个DataFrame在纵向上(行方向上)进行合并。默认情况下,concat
会按照原始顺序合并DataFrame,但如果需要的话, 也可以指定一个axis
参数来改变合并的方向(axis=0
表示纵向合并,axis=1
表示横向合并)。
所以,all_features
将会是一个新的DataFrame,它包含了train_data
和test_data
中除了第一列之外的所有列,并且这些列是按照它们在原始DataFrame中的顺序排列的。
处理残缺数据
- 将所有缺失的值替换为相应特征的平均值 .然后,为了将所有特征放在一个共同的尺度上, 通过将特征重新缩放到零均值和单位方差来标准化数据
# 若无法获得测试数据,则可根据训练数据计算均值和标准差
numeric_features = all_features.dtypes[all_features.dtypes != 'object'].index
all_features[numeric_features] = all_features[numeric_features].apply(
lambda x: (x - x.mean()) / (x.std()))
# 在标准化数据之后,所有均值消失,因此我们可以将缺失值设置为0
all_features[numeric_features] = all_features[numeric_features].fillna(0)
- 确定数值型特征:
numeric_features = all_features.dtypes[all_features.dtypes != 'object'].index
首先通过 all_features.dtypes
获取DataFrame中每列的数据类型。然后,它筛选出非'object'
(即非字符串或混合类型)的列索引,这些列通常包含数值型数据(如整数、浮点数等)。这些索引被存储在 numeric_features
变量中。
2. 标准化数值型特征:
all_features[numeric_features] = all_features[numeric_features].apply(
lambda x: (x - x.mean()) / (x.std()))
对 numeric_features
中指定的列进行标准化处理。对于每一列 x
,它计算该列的均值 x.mean()
和标准差 x.std()
,然后用列中的每个值减去均值并除以标准差,从而实现标准化。这里使用了 apply
函数和 lambda
表达式来逐列应用标准化操作。
3. 处理缺失值:
all_features[numeric_features] = all_features[numeric_features].fillna(0)
在标准化之后,由于所有值都已经是相对于均值的偏移量(即,所有列的均值现在都是0),因此可以将缺失值(NaN)安全地替换为0。这是因为在标准化之后,0已经代表了原始数据的均值。使用 fillna(0)
方法可以将 numeric_features
中所有列的缺失值替换为0。
4、离散值的处理
# “Dummy_na=True”将“na”(缺失值)视为有效的特征值,并为其创建指示符特征
all_features = pd.get_dummies(all_features, dummy_na=True)
all_features.shape
使用独热编码进行替换
SalePrice - 房价,这也是我们预测的对象
MSSubClass: 建筑类别
MSZoning: The general zoning classification(空间分类)
-
pd.get_dummies()
函数是一个用于将分类变量(通常是字符串或布尔值)转换为one-hot编码(或称为独热编码)的函数。 -
One-hot编码是机器学习中的一个常用技巧,特别是当模型需要处理类别数据时。
-
dummy_na=True
参数是一个特殊选项,它告诉pd.get_dummies()
函数将NaN(即“缺失值”)也视为一个有效的类别,并为其创建一个额外的二进制列。 -
通常,默认情况下,
pd.get_dummies()
会忽略NaN值,但在设置dummy_na=True
后,它会为每一个原本包含NaN值的列都创建一个新的列,该列在原始列是NaN的位置为1,否则为0。
首先,pd.get_dummies(all_features, dummy_na=True)
会将 all_features
DataFrame 中的所有分类变量转换为one-hot编码,并且对于原本包含NaN的列,会为其创建一个新的列来表示这些NaN值。
然后,all_features.shape
会返回处理后的DataFrame的维度,即(行数, 列数)。
5、提取NumPy并转化为张量
-
n_train = train_data.shape[0]
- 从
train_data
DataFrame 中获取训练数据的行数(即样本数量),并将其存储在n_train
变量中。
- 从
-
train_features = torch.tensor(all_features[:n_train].values.astype(float), dtype=torch.float32)
- 这行代码首先从
all_features
中选取前n_train
个样本(即训练数据)。 .values
将这部分数据转换为NumPy数组。.astype(float)
确保这些数据是浮点数类型。- 最后,使用
torch.tensor
将这些数据转换为PyTorch张量,并指定数据类型为torch.float32
(即32位浮点数)。
- 这行代码首先从
-
test_features = torch.tensor(all_features[n_train:].values.astype(float), dtype=torch.float32)
- 这行代码与上一行类似,但它是从
n_train
之后选取all_features
中的样本(即测试数据)。 - 其余的处理步骤与训练特征相同。
- 这行代码与上一行类似,但它是从
-
train_labels = torch.tensor(train_data.SalePrice.values.astype(float).reshape(-1, 1), dtype=torch.float32)
- 这行代码从
train_data
中提取SalePrice
列作为训练标签。 .values
将其转换为NumPy数组。.astype(float)
确保这些标签是浮点数类型。.reshape(-1, 1)
将标签数组重新塑形为一个二维数组,其中每个样本的标签都是一个单独的行(尽管它可能只有一个元素)。这样做常常是为了使标签的形状与特征矩阵的形状兼容,特别是在进行某些类型的神经网络训练时。- 最后,使用
torch.tensor
将这些标签转换为PyTorch张量,并指定数据类型为torch.float32
。
- 这行代码从
总结:这段代码的目的是从原始数据集中提取特征和标签,并将它们转换为PyTorch张量,以便后续在PyTorch中进行模型训练。
6、训练模型
使用一个基本的线性回归模型和平方损失函数来训练模型。
loss = torch.nn.MSELoss()
def get_net(feature_num):
net = nn.Linear(feature_num, 1)
for param in net.parameters():
nn.init.normal_(param, mean=0, std=0.01)
return net
下面定义比赛用来评价模型的对数均方根误差。给定预测值 y ^ 1 , … , y ^ n \hat y_1, \ldots, \hat y_n y^1,…,y^n和对应的真实标签 y 1 , … , y n y_1,\ldots, y_n y1,…,yn,它的定义为
1 n ∑ i = 1 n ( log ( y i ) − log ( y ^ i ) ) 2 . \sqrt{\frac{1}{n}\sum_{i=1}^n\left(\log(y_i)-\log(\hat y_i)\right)^2}. n1i=1∑n(log(yi)−log(y^i))2.
对数均方根误差的实现如下。
def log_rmse(net, features, labels):
with torch.no_grad():
# 将小于1的值设成1,使得取对数时数值更稳定
clipped_preds = torch.max(net(features), torch.tensor(1.0))
rmse = torch.sqrt(loss(clipped_preds.log(), labels.log()))
return rmse.item()
下面的训练函数跟本章中前几节的不同在于使用了Adam优化算法。相对之前使用的小批量随机梯度下降,它对学习率相对不那么敏感。
def train(net, train_features, train_labels, test_features, test_labels,
num_epochs, learning_rate, weight_decay, batch_size):
train_ls, test_ls = [], []
train_iter = d2l.load_array((train_features, train_labels), batch_size)
# 这里使用的是Adam优化算法
optimizer = torch.optim.Adam(net.parameters(),
lr = learning_rate,
weight_decay = weight_decay)
for epoch in range(num_epochs):
for X, y in train_iter:
optimizer.zero_grad()
l = loss(net(X), y)
l.backward()
optimizer.step()
train_ls.append(log_rmse(net, train_features, train_labels))
if test_labels is not None:
test_ls.append(log_rmse(net, test_features, test_labels))
return train_ls, test_ls
-
参数:
net
: 要训练的神经网络模型。train_features
,train_labels
: 训练数据的特征和标签。test_features
,test_labels
: 测试数据的特征和标签(可选,如果提供则计算测试误差)。num_epochs
: 训练的总轮数(epoch)。learning_rate
: 学习率。weight_decay
: 权重衰减(也称为L2正则化项)。batch_size
: 批处理大小。
-
初始化:
train_ls
,test_ls
: 分别用于存储每一轮训练后的训练误差和测试误差(如果提供了测试数据)。train_iter
: 使用d2l 的load_array
函数将训练数据转换为可迭代的批处理数据。
-
优化器:
- 使用PyTorch的
torch.optim.Adam
优化器。这个优化器使用了Adam算法来更新网络的权重。 net.parameters()
: 获取神经网络中所有可训练的参数。lr
: 学习率。weight_decay
: 权重衰减。
- 使用PyTorch的
-
训练循环:
- 对于每一个epoch(即完整的训练数据集遍历一次):
- 对于训练数据集中的每一个batch(X, y):
optimizer.zero_grad()
: 清空之前累积的梯度。l = loss(net(X), y)
: 计算预测值与实际值之间的损失。这里假设loss
是一个已定义的损失函数(如均方误差)。l.backward()
: 反向传播,计算损失相对于模型参数的梯度。optimizer.step()
: 使用优化器更新模型的权重。
- 计算并记录当前epoch的训练误差。
- 如果提供了测试数据,则计算并记录当前epoch的测试误差。
- 对于训练数据集中的每一个batch(X, y):
- 对于每一个epoch(即完整的训练数据集遍历一次):
-
返回值:
train_ls
,test_ls
: 分别包含每一轮训练后的训练误差和测试误差的列表。
7 、𝐾折交叉验证
- 有助于模型选择和超参数调整。 我们首先需要定义一个函数,在 𝐾
折交叉验证过程中返回第 𝑖 折的数据。 具体地说,它选择第 𝑖个切片作为验证数据,其余部分作为训练数据
def get_k_fold_data(k, i, X, y):
assert k > 1
fold_size = X.shape[0] // k
X_train, y_train = None, None
for j in range(k):
idx = slice(j * fold_size, (j + 1) * fold_size)
X_part, y_part = X[idx, :], y[idx]
if j == i:
X_valid, y_valid = X_part, y_part
elif X_train is None:
X_train, y_train = X_part, y_part
else:
X_train = torch.cat([X_train, X_part], 0)
y_train = torch.cat([y_train, y_part], 0)
return X_train, y_train, X_valid, y_valid
def k_fold(k, X_train, y_train, num_epochs, learning_rate, weight_decay,
batch_size):
train_l_sum, valid_l_sum = 0, 0
for i in range(k):
data = get_k_fold_data(k, i, X_train, y_train)
net = get_net()
train_ls, valid_ls = train(net, *data, num_epochs, learning_rate,
weight_decay, batch_size)
train_l_sum += train_ls[-1]
valid_l_sum += valid_ls[-1]
if i == 0:
d2l.plot(list(range(1, num_epochs + 1)), [train_ls, valid_ls],
xlabel='epoch', ylabel='rmse', xlim=[1, num_epochs],
legend=['train', 'valid'], yscale='log')
print(f'折{i + 1},训练log rmse{float(train_ls[-1]):f}, '
f'验证log rmse{float(valid_ls[-1]):f}')
return train_l_sum / k, valid_l_sum / k
- 将数据集划分为k个大小相近的子集(称为“折”),并返回第i折作为验证集(validation set),其他k-1折作为训练集(training set)。
-
参数:
k
: 折数,即要将数据集划分为多少个子集。i
: 当前折的索引,用于指定哪一折作为验证集。X
: 特征数据,通常是一个二维数组或张量。y
: 标签数据,通常是一个一维数组或张量。
-
断言:
assert k > 1
: 确保k大于1,因为k折交叉验证至少需要两个折。
-
计算每个折的大小:
fold_size = X.shape[0] // k
: 计算每个折应包含多少样本。这里使用整数除法,确保样本能够均匀分配到各个折中。
-
初始化训练集和验证集:
X_train, y_train = None, None
: 初始化训练集的特征和标签为None。
-
循环遍历每个折:
- 使用for循环遍历从0到k-1的索引j。
idx = slice(j * fold_size, (j + 1) * fold_size)
: 计算当前折的索引范围。X_part, y_part = X[idx, :], y[idx]
: 从特征数据和标签数据中提取当前折的数据。
-
区分训练集和验证集:
- 如果当前折的索引j等于i,则将
X_part
和y_part
分别赋值给X_valid
和y_valid
,表示这一折是验证集。 - 如果
X_train
和y_train
尚未被初始化(即它们仍然是None),则将X_part
和y_part
分别赋值给X_train
和y_train
,表示这是第一个折,并且它将被用作训练集的开始部分。 - 如果
X_train
和y_train
已经被初始化(即它们不是None),则使用torch.cat
函数将当前折的数据与已有的训练集数据合并。
- 如果当前折的索引j等于i,则将
-
返回结果:
- 函数最后返回四个值:训练集的特征
X_train
、训练集的标签y_train
、验证集的特征X_valid
和验证集的标签y_valid
。
- 函数最后返回四个值:训练集的特征