前言
本篇文章为李沐老师 线性神经网络 的学习笔记。使用Python编程语言及其强大的科学计算库(PyTorch)来实现线性回归模型。通过编写代码、运行实验和观察结果,我们将更加直观地理解线性回归模型的工作原理。
从零开始实现线性回归
本小节将从零开始实现线性回归,包括数据流水线、模型、损失函数和小批量随机梯度下降优化器。
Step1: 导包
import matplotlib.pyplot as plt
import torch
import random
from d2l import torch as d2l
Step2: 构造数据集
根据带有噪声的线性模型构造一个人造数据集。使用线性模型参数 w = [ 2 , − 3.4 ] T w=[2,-3.4]^T w=[2,−3.4]T、b=4.2和噪声项 ϵ \epsilon ϵ生成数据集及标签 y = X w + b + ϵ y=Xw+b+\epsilon y=Xw+b+ϵ
def synthetic_data(w,b,num_examples):
"""
构建带噪声的人造数据集
param w: 权重
param b: 偏差
param num_examples: 样本数目
"""
# 生成 X 均值为 0 ,方差为 1 ,num_examples个样本,列数为 w 的长度
X = torch.normal(0,1,(num_examples,len(w)))
# y = Xw+b
y = torch.matmul(X,w)+b
# 加入了一个均值为0,方差为0.01,形状同y相同的随机噪声
y += torch.normal(0,0.01,y.shape)
# 将X,y作为列向量返回
return X,y.reshape((-1,1))
调用函数生成数据集
# 线性模型参数赋值
true_w = torch.tensor([2,-3.4])
true_b = 4.2
# features中的每一行都包含一个二维数据样本,labels中的每一行都包含一个一维标签值
features, labels = synthetic_data(true_w, true_b, 1000)
print('features:', features[0],'\nlabel:', labels[0])
通过生成第二个特征features[:, 1]和labels的散点图,可以看出特征和标签线性相关
d2l.set_figsize()
d2l.plt.scatter(features[:,1].detach().numpy(),labels.detach().numpy(),1)
plt.show()
Step3: 读取数据集
定义data_iter函数,接受批量大小,特征矩阵和标签向量作为输入,生成大小为batch_size的小批量
def data_iter(batch_size,features,labels):
"""
生成小批量数据
param batch_size: 批量大小
param features: 特征矩阵
param labels: 标签向量
"""
num_examples = len(features)
# 生成每个样本的index
indices = list(range(num_examples))
# 随机读取样本,无特定的顺序,随机打乱这些下标
random.shuffle(indices)
# 从 0 开始到 num_examples 结束每次跳 batch_size 的步长
for i in range(0,num_examples,batch_size):
# 从i开始,不超出预定的样本数
batch_indices = torch.tensor(indices[i:min(i+batch_size,num_examples)])
# 通过下标返回随机顺序的特征和随机顺序的标签
yield features[batch_indices],labels[batch_indices]
读取第一个小批量数据样本并打印。 每个批量的特征维度显示批量大小和输入特征数。 同样的,批量的标签形状与batch_size相等。
# 批量大小
batch_size = 10
# 读取第一个小批量数据样本并打印。 每个批量的特征维度显示批量大小和输入特征数。 同样的,批量的标签形状与batch_size相等。
for X,y in data_iter(batch_size,features, labels):
print(X,'\n',y)
break
当我们运行迭代时,我们会连续地获得不同的小批量,直至遍历完整个数据集。
Step4: 定义初始化模型参数、模型、损失函数、优化算法
在开始用小批量随机梯度下降优化模型参数之前, 需要先有一些参数。 在下面的代码中,通过从均值为0、标准差为0.01的正态分布中采样随机数来初始化权重, 并将偏置初始化为0。
# 定义初始化模型参数,均需要计算梯度
w = torch.normal(0,0.01,size=(2,1),requires_grad=True)
b = torch.zeros(1,requires_grad=True)
接下来,必须定义模型,将模型的输入和参数同模型的输出关联起来。
def linreg(X,w,b):
"""
定义线性回归模型
param X: 输入
param w: 权重
param b: 偏差
y = Xw + b
"""
return torch.matmul(X,w)+b
紧接着,需要计算损失函数的梯度,所以应该先定义损失函数。 这里使用的是平方损失函数。 在实现中,需要将真实值 y 的形状转换为和预测值 y ^ \hat{y} y^ 的形状相同。
def squared_loss(y_hat,y):
"""
定义损失函数:均方误差
param y_hat: 预测值
param y: 真实值
"""
return(y_hat-y.reshape(y_hat.shape))**2/2
然后定义一个函数实现了小批量随机梯度下降(Stochastic Gradient Descent, SGD)。 该函数接受模型参数集合、学习速率和批量大小作为输入。每 一步更新的大小由学习速率 lr 决定。通过计算当前批次数据的梯度来更新网络参数,以最小化损失函数。
def sgd(params,lr,batch_size):
"""
定义优化算法:小批量随机梯度下降法
param params:包含w和b
param lr: 学习率
batch_size:批量大小,用于调整梯度以适配批量大小
"""
# 不需要计算梯度
with torch.no_grad():
# 更新参数
for param in params:
# 求均值
param -= lr*param.grad/batch_size
# 将梯度设为0,为了下次计算与其不相关
param.grad.zero_()
Step5: 训练
到目前为止,我们已经准备好了模型训练所有需要的要素,可以实现主要的训练过程部分了。 在每次迭代中,我们读取一小批量训练样本,并通过我们的模型来获得一组预测。 计算完损失后,我们开始反向传播,存储每个参数的梯度。 最后,我们调用优化算法sgd来更新模型参数。
# 学习率
lr = 0.03
# 迭代次数
num_epochs = 3
# 模型
net = linreg
# 损失函数
loss = squared_loss
# 训练过程
for epoch in range(num_epochs):
# 每次拿出一个批量大小的Xy
for X,y in data_iter(batch_size, features, labels):
# X,y的小批量损失,放进net作损失,预测的y和真实的y做损失
l = loss(net(X,w,b),y)
# 求和之后反向传播
l.sum().backward()
# 使用梯度更新w,b
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}')
线性回归的简洁实现
本小节将使用深度学习框架来简洁地实现线性回归模型,生成数据集。
Step1: 导包
import numpy as np
import torch
from torch.utils import data
from d2l import torch as d2l
Step2: 生成数据集
# 1.生成数据集
# 构造真实的w和b,然后通过人工数据合成函数生成我们需要的features和labels
true_w = torch.tensor([2,-3.4])
true_b = 4.2
features,labels = d2l.synthetic_data(true_w,true_b,1000)
Step3: 读取数据集
# 将features和labels作为API的参数传递,并通过数据迭代器指定batch_size。布尔值is_train表示是否希望数据迭代器对象在每个迭代周期内打乱数据。
def load_array(data_arrays,batch_size,is_train=True):
"""
构造一个pytorch数据迭代器
param data_arrays: features和labels
param batch_size: 批量大小
param is_train: 控制是否希望数据迭代器对象在每个迭代周期内打乱数据
"""
dataset = data.TensorDataset(*data_arrays)
# 使用 data.DataLoader 负责批量地、可选地打乱(如果 is_train=True)并加载数据集
return data.DataLoader(dataset,batch_size,shuffle=is_train)
接下来验证
batch_size = 10
data_iter = load_array((features,labels),batch_size)
#为了验证是否正常工作,让我们读取并打印第一个小批量样本。这里我们使用iter构造Python迭代器,并使用next从迭代器中获取第一项。
print(next(iter(data_iter)))
Step4: 定义模型
from torch import nn
# 定义一个模型变量net,它是一个Sequential类的实例。 Sequential类将多个层串联在一起。 当给定输入数据时,Sequential实例将数据传入到第一层, 然后将第一层的输出作为第二层的输入,以此类推。
# nn.Linear有两个参数:第一个指定输入特征形状,第二个指定输出特征形状。
net = nn.Sequential(nn.Linear(2,1))
在PyTorch中,nn.Sequential
是一个容器,用于按顺序封装一系列模块(如层)。当使用 nn.Sequential
来定义神经网络时,只需将层作为参数传递给它的构造函数,这些层就会按照提供的顺序被添加到网络中。
本小节定义了一个非常简单的神经网络 net,它只包含一个线性层(nn.Linear)。这个线性层接受输入特征维度为2(即每个输入样本有两个特征),并输出一个单一的值(输出特征维度为1)。
在构造nn.Linear
时指定输入和输出尺寸:将两个参数传递到nn.Linear
中。 第一个指定输入特征形状,即2,第二个指定输出特征形状,输出特征形状为单个标量,因此为1。
Step5: 初始化模型参数
在使用net之前,我们需要初始化模型参数.深度学习框架通常有预定义的方法来初始化参数。
nn.Linear
层有两个可学习的参数:权重(weight)和偏置(bias)。这些参数在模型初始化时是随机设置的,但我们可以通过直接访问它们来修改它们的初始值。
# 将第一个线性层的权重参数(weight)的初始值设置为均值为0,标准差为0.01的正态分布随机值。
net[0].weight.data.normal_(0,0.01)
# 将第一个线性层的偏置参数(bias)的初始值全部设置为0
net[0].bias.data.fill_(0)
Step5: 定义损失函数
计算均方误差使用的是MSELoss类,也称为平方 L 2 L_2 L2范数。 默认情况下,它返回所有样本损失的平均值。
# 实例化SGD实例,设置lr值,这里设置为0.03。
trainer = torch.optim.SGD(net.parameters(),lr=0.03)
Step6: 定义优化算法
# 使用MSELoss类定义均方误差
loss = nn.MSELoss()
Step7: 训练
# 训练
num_epochs = 3
for epoch in range(num_epochs):
for X,y in data_iter:
# net()这里本身带了模型参数,不需要把w和b放进去了,net(X)是预测值,y是真实值,拿到预测值和真实值做Loss
I = loss(net(X),y)
# 梯度清零
trainer.zero_grad()
# 计算反向传播,这里pytorch已经做了sum就不需要在做sum了(loss是一个张量,求sum之后是标量)
I.backward()
# 有了梯度之后调用step()函数来进行一次模型的更新。调用step函数,从而分别更新权重和偏差
trainer.step()
# 当扫完一遍数据之后,把所有的feature放进network中,和所有的Label作一次Loss
I = loss(net(features),labels)
print(f'epoch{epoch+1},loss{I:f}')
结语
本篇文章的完整代码可以查看完整代码