1.线性方程和向量乘法
深度学习的基础就是从线性回归方程的理论进入的。简单的线性回归方程为
比如大家日常中买房子,价格受到哪些因素影响呢?
比如房龄、交通、是否是学区、有无配套超市、公园,这些基本是外部条件,内部条件诸如几梯几户、层高、容积率、面积、朝向等这些,这样一看如果使用上面的模型构建一个房屋价格预测的神经网络模型,参数非常多这么多个参数影响了房屋的价格。所谓线性回归(regression)是能为一个或者多个自变量与因变量之间关系建模的一类方法。回归在数学中经常用来表示输入和输出之间的关系。
所以上面的房屋价格如果只考虑面积和房龄的话,预测模型就变成了
称为偏执,或者截距;预测值所依据的自变量称为特征或者协变量,把试图预测的目标称为标签或者目标。。所以整体的预测值就可以描述为
我们用矩阵表示出来就是
向量是对于单个数据样本特征。使用符号表示的矩阵就是
。就可以很方便的引用整个数据集的
个样本。其中
的每一行就是一个样本,每一列就是一种特征。对于特征集合
,预测值通过矩阵-向量乘法表示为
2.正态分布和平方损失
正态分布和线性回归之间的关系很密切。 正态分布(normal distribution),也称为高斯分布(Gaussian distribution), 最早由德国数学家高斯(Gauss)应用于天文学研究。 我们定义正态分布的时候,就是有一个随机变量具有均值
和方差
(标准差
),其正态分布概率密度函数如下
在后面的方程中我们假设噪声服从正态分布。
如果使用函数来实现
def normal(x, mu, sigma):p = 1/ math.sqrt(2 * math.pi * sigma**2)return p* np.exp(-0.5 / sigma**2 * (x-mu)**2)# 注意torch.normal定义中
torch.normal(mean: _float, std: _float, size: Sequence[Union[_int, SymInt]]...)
mean: 正态分布的均值,可以是一个数值也可以是一个张量
std: 正态分布的标准差,可以是一个数值也可以是一个张量
size:输出随机数的形状,可以是一个整数用于生成size大小的一维张量;也可以是一个元组,用于生成相应形状的多维张量
3.样本刷选
现在我们从市场上获取了一批输入数据,即有一批特征值。因为我们最终是寻找参数。所以虽然开始不知道最终具体的
是多少,但是可以根据经验值填写一个
所以使用参数模型.其中
作为噪声项,服从均值为0的正态分布。这就是我们第一章节中的
import random # 导入random,用于需要随机化初始的权重,以及随机梯度下降
import torch
from d2l import torch as d2ldef synthetic_data(w, b, num_examples):"""生成y=xW +b+噪声:param w::param b::param num_examples: 指定生成样本的数量:return:"""X = torch.normal(0, 1, (num_examples, len(w))) # 生成均值为0,方差为1的,是num个样本,列数是wy = torch.matmul(X, w) + b # Matrix Multiplication 两个相乘y += torch.normal(0, 0.01, y.shape) # 加入一个随机噪音,均值为0方差为1return X, y.reshape((-1, 1)) # 做成一个列向量返回
📢注意:
features
中的每一行都包含一个二维数据样本,labels
中的每一行都包含一维标签值(一个标量)
x = torch.arange(12).reshape(2,6)print(x)y = x.reshape((-1,1))print(y)
这里还有一个就是,之前很多时候我们使用的时候都是
torch.arange(24).reshape(2,3,4)
reshape的参数都是整数,但是上面的y.reshape((-1,1))是什么意思呢?
-1作为一个维度参数时,表示该维度的大小由数组的实际大小和其它维度决定。换句话说NumPy会自动计算这个维度的大小以保持数组元素总数不变。1表示新形状中的另一个维度大小为1。因此,当你对一个数组或矩阵调用.reshape((-1, 1))时,你实际上是在告诉NumPy将原数组重塑为一个列向量(即每列只有一个元素的二维数组)。也就是说,无论输入数组有多少个元素,结果都将是一个两维数组,其中每个原始元素占据一行,且仅有一列。参考
x = torch.arange(12).reshape(2,6)print(x)y = x.reshape((-1,1))print(y)
输出如下所示:
我们调用上面的函数
true_w = torch.tensor([2, -3.4])true_b = 4.2features, labels = synthetic_data(true_w, true_b, 1000)print('output features:', features)print(f'output labels: {labels}')
输出如下所示,和上面的理解是一致的
即features特征值是一个每一行都包含一个二维数组的样本,labels即预测值每一行都包含一个一维标签值 。
我们看看特征值和标签之间的散点分布图。
d2l.set_figsize()# 这里的detach是从pytorch detach出来之后才能转到numpy中d2l.plt.scatter(features[:, 0].detach().numpy(), labels.detach().numpy(), 1);d2l.plt.show()
现在我们的基础数据已经构造完成。接下来一个重要的事情需要定义个函数,每次读取一个小批量。
4.小批量数据构造
为什么要使用小批量,而不是使用全部的样本呢,因为每次计算梯度需要对损失函数求导,损失函数是对我们样本平均损失,所以求解一次梯度函数需要把所有样本重新计算一遍,这个训练过程太贵了,所以在神经网络实际训练几乎不用全部的样本。同样一个神经网络模型的训练过程可能需要数分钟数个小时,甚至更久。因为要计算几百步,甚至几千步才能求解出最终的结果。所以我们定义损失函数之后,只要能求解得到一个“差不多”的解就可以,而且在多维复杂的模型中,很少能得到准确解。所以我们损失函数就是多个采样数据各自损失最后求平均得到最终的损失函数。所以我们采样个样本
来求解近似。
所以这里的采样的个样本,如果
很大,则计算出的结果相对比较精确但是计算复杂度很高;如果
比较小,计算比较容易但是精确度可能会很差。毕竟梯度的计算复杂度是和样本的个数线性相关的。
def data_iter(batch_size, features, labels):""":param batch_size::param features::param labels::return:"""num_examples = len(features)indices = list(range(num_examples))# shuffle这些样本是随机读取的,没有特定的顺序random.shuffle(indices) # 把indices列表中的元素随机打乱,这样就可以随机访问for i in range(0, num_examples, batch_size):batch_indices = torch.tensor(indices[i: min(i + batch_size, num_examples)])yield features[batch_indices], labels[batch_indices]
测试下这个函数
batch_size = 10for X, y in data_iter(batch_size, features, labels):print('小批量数据:', X, '\n', y)break
5.定义模型
def linreg(X, w, b):"""线性规划模型:param X::param w::param b::return:"""return torch.matmul(X, w) + b
w = torch.normal(0, 0.01, size=(2, 1), requires_grad=True)
b = torch.zeros(1, requires_grad=True)
这是一个均值为零,方差为0.01的正太分布函数。我们需要计算梯度requires_grad=True.对于偏差来说。我们直接给出一个0,当然也需要不断地调整,所以需要计算梯度。
6.定义损失函数
def squared_loss(y_hat, y):"""定义损失函数,就是均方误差:param y_hat: 预测值:param y: 真实值:return:"""# y.reshape(y_hat.shape) 确保两者大小一样return (y_hat - y.reshape(y_hat.shape)) **2 / 2
其中是预测值,
是我们的真实值。原则上两个个数是一样的,但是计算中可能是一个行向量,一个是列向量。所以使用.shape函数统一下。
7.定义优化算法
def sgd(params, lr, batch_size):"""小批量随机梯度下降:param params: params 包含了w和b:param lr:学习率:param batch_size::return:"""with torch.no_grad(): # 更新的时候不要更新梯度for param in params:param -= lr * param.grad / batch_sizeparam.grad.zero_() # 把梯度设置为0,下次计算梯度的时候就和上次不会相关了
with*作用:torch.no_grad() 是一个上下文管理器(context manager),它告诉 PyTorch 在这个块内不需要计算梯度。这意味着任何在此块内的操作都不会被加入到自动求导图中(即不会跟踪这些操作以进行反向传播),从而节省内存和计算资源。
原因:当确定某些操作(如参数更新)不需要参与梯度计算时,使用 torch.no_grad() 可以提高效率。特别是在推理阶段或手动更新参数时,我们通常不需要追踪这些操作的梯度。
将param.grad.zero_()的原因主要有:
- 将当前参数的梯度设置为零。这是因为在默认情况下,PyTorch 的反向传播会累加梯度而不是覆盖它们。如果不重置梯度,那么在下一次反向传播时,新计算出的梯度会被加到旧的梯度上,导致不正确的更新。
- 为什么需要这样做:每次参数更新后,我们应该清除之前的梯度信息,以便于下一轮迭代中正确地计算新的梯度并进行更新。
8.训练
现在结合上面的过程进行训练
if __name__ == '__main__':true_w = torch.tensor([2, -3.4])true_b = 4.2features, labels = synthetic_data(true_w, true_b, 1000)# print('output features:', features)# print(f'output labels: {labels}')## d2l.set_figsize()# # 这里的detach是从pytorch detach出来之后才能转到numpy中# d2l.plt.scatter(features[:, 0].detach().numpy(), labels.detach().numpy(), 1);## # d2l.plt.show()## # features就是自变量,输入值;labels是标签,这个就是因变量,输出值batch_size = 10# for X, y in data_iter(batch_size, features, labels):# print('小批量数据:', X, '\n', y)# break# #w = torch.normal(0, 0.01, size=(2, 1), requires_grad=True)b = torch.zeros(1, requires_grad=True)# 训练的过程lr = 0.03 # 学习率num_epochs = 3 # 把数据扫3遍net = linreg # 模型,就是我们linreg,之所以这样写,就是后续可以快速替换为其他模型loss = squared_loss # 均方损失for epoch in range(num_epochs):for X, y in data_iter(batch_size, features, labels):l = loss(net(X, w, b), y)l.sum().backward() # 求和之后计算梯度sgd([w,b], lr, batch_size)with torch.no_grad():train_l = loss(net(features, w, b), labels)print(f'epoch {epoch + 1}, loss {float(train_l.mean()):f}')print(f'w的估计误差: {true_w - w.reshape(true_w.shape)}')print(f'b的估计误差: {true_b - b}')
输出如下所示: