从0构建线性回归模型
参考:《动手学深度学习》——李沐
模型原理
模型定义:(以二维变量为例)
构建的模型得到的是对真实数据的估计,假设采集的样本数为,索引为的样本特征为和,标签为则有:
定义损失函数:平方误差
在模型训练中,我们希望找出⼀组模型参数,记为,,来使训练样本平均损失最小:
如上述公式可知,通常,我们用训练数据集中所有样本误差的平均来衡量模型预测的质量
但对于大量的数据时,采用所有样本往往使得计算量变大。因此将采用小批量随机梯度下降法(BGD)。
优化方法
此处需要一定的凸优化基础,可以参考《convex Optimization》—Boyd and L. Vandenberghe
梯度下降法(批量梯度下降法)
我们考虑如下优化目标。
其中为给定的数据,其中可以为任意损失函数。
因此由梯度下降法可以得到,下一个迭代点为:
可以看出,此处用了所有的数据,由于可以改写为,其中,所以我们将考虑优化函数:(此处只是为了最终求解的梯度较为好看)
可以看出此问题是一个凸问题,存在全局最优解,采用梯度下降法是可以得到最优解的,求梯度可得:
可以看出,上述的下降梯度用上了所有的数据,此方法存在的缺点则是:计算量大。存在的优点则是:相比于SGD和BGD下降速度较快。
随机梯度下降法(SGD)
算法原理:(用随机一个梯度近似全体梯度)
随机梯度下降法则是随机选取一个样本,进行梯度计算,进行参数更新。
即将上述改为:只用一个样本进行计算梯度
收敛性:(我还没理解透,凸优化和概率论之间的联系还没搞清楚,听说是依期望收敛)
参考:浅谈随机梯度下降&小批量梯度下降 - 知乎 (zhihu.com)
假设我们样本量为,则优化函数可以写出:
其中为损失函数,此处有点滥用符号,但此式子不难理解,即对所有的损失加起来的最终值进行求最小。
例如:是平方误差损失即,则。
则有:
随机梯度下降法则随机选取一个:
代替进行参数更新。
优缺点:
优点:计算量小。
缺点:参数更新慢。
小批量随机梯度下降法(BGD)
算法原理:是随机梯度缺点的改进,优点的牺牲。利用小批量样本进行梯度更新。
小批量随机梯度下降法则是随机选取小批量的样本,进行梯度计算,进行参数更新。
即将上述改为:用小批量样本进行计算梯度。
收敛性:和随机梯度下降原理一样。
优缺点:是梯度下降和随机梯度下降的中和。
代码实现
先上代码
# 导入库
import torch
from IPython import display
import matplotlib.pyplot as plt
import numpy as np
import random
# 创建数据
num_inputs = 2
num_examples = 1000
true_w = [2, -3.4]# 给定数据权重
true_b = 4.2# 给定参数b
features = torch.randn(num_examples,num_inputs
,dtype=torch.float32)# 利用randn创建随机数据
labels = true_w[0] * features[:, 0] + true_w[1] * features[:, 1]# 获取回归数据
labels += torch.tensor(np.random.normal(0, 0.01, size=labels.size()),
dtype=torch.float32)# 加入扰动
# 对创建的数据进行可视化
def use_svg_display():
# 用矢量图显示
display.set_matplotlib_formats('svg')
def set_figsize(figsize=(3.5, 2.5)):
use_svg_display()
# 设置图的尺寸
plt.rcParams['figure.figsize'] = figsize
set_figsize()
plt.scatter(features[:, 1].numpy(), labels.numpy(), 1)
# 创建数据迭代器,进行批量学习
def data_iter(batch_size, features, labels):
# batch_size是选取的每次学习的数据长度
# features是数据特征
# labels是数据标签
num_examples = len(features)
indices = list(range(num_examples))# 获取索引
random.shuffle(indices)# 打乱索引
for i in range(0, num_examples, batch_size):
# 最后⼀次可能不⾜⼀个batch可利用min(i + batch_size,num_examples)进行拆解
# 将indices[i:min(i + batch_size,num_examples)]转为LongTensor类型
j = torch.LongTensor(indices[i:min(i + batch_size,num_examples)])
# 利用yield关键字生成迭代器
yield features.index_select(0, j), labels.index_select(0, j)
# 定义线性回归模型
def linreg(X,w,b):
# 利用torch.mm进行矩阵相乘
return torch.mm(X,w) + b
# 定义平方误差
def squared_loss(y_hat, y):
return (y_hat - y.view(y_hat.size())) ** 2 / 2
# 随机小批量梯度下降法
def sgd(params, lr, batch_size):
for param in params:
param.data -= lr * param.grad / batch_size
if __name__ == "__main__":
# 利用random初始化w
w = torch.tensor(np.random.normal(0,0.01,(num_inputs,1)), dtype=torch.float32)
# 初始化b
b = torch.zeros(1, dtype=torch.float32)
# 将w和b设置为可追踪操作,requires_grad=True
w.requires_grad_(requires_grad=True)
b.requires_grad_(requires_grad=True)
lr = 0.03 # 设置学习率
num_epochs = 3 # 设置学习轮数
net = linreg # 定义线性模型
loss = squared_loss # 定义损失函数
for epoch in range(num_epochs):
for X,y in data_iter(batch_size, features, labels): # 从迭代器中选取每次batch的训练数据
l = loss(net(X,w,b),y).sum() # 求损失函数值
l.backward() # 利用backward进行求导,得到的梯度会保存再w,b的grad属性中
sgd([w,b],lr,batch_size) # 利用小批量梯度下降法进行更新
w.grad.data.zero_() # grad.data.zero_()将梯度清零,不然梯度会累加
b.grad.data.zero_() # grad.data.zero_()将梯度清零,不然梯度会累加
train_1 = loss(net(features,w,b),labels)
print('epoch %d, loss %f' % (epoch + 1, train_1.mean().item()))
输出:
代码解读
只对主体代码进行解读。
# 创建数据迭代器,进行批量学习
def data_iter(batch_size, features, labels):
# batch_size是选取的每次学习的数据长度
# features是数据特征
# labels是数据标签
num_examples = len(features)
indices = list(range(num_examples))# 获取索引
random.shuffle(indices)# 打乱索引
for i in range(0, num_examples, batch_size):
# 最后⼀次可能不⾜⼀个batch可利用min(i + batch_size,num_examples)进行拆解
# 将indices[i:min(i + batch_size,num_examples)]转为LongTensor类型
j = torch.LongTensor(indices[i:min(i + batch_size,num_examples)])
# 利用yield关键字生成迭代器
yield features.index_select(0, j), labels.index_select(0, j)
data_iter是从原始数据中产生小批量数据迭代器的函数。
其中yield关键字是产生迭代器的关键字,具体可以查看python基础查漏补缺 - 掘金 (juejin.cn)
# 定义线性回归模型
def linreg(X,w,b):
# 利用torch.mm进行矩阵相乘
return torch.mm(X,w) + b
# 定义平方误差
def squared_loss(y_hat, y):
return (y_hat - y.view(y_hat.size())) ** 2 / 2
linreg,squared_loss是进行前向传播的神经网络层
for epoch in range(num_epochs):
for X,y in data_iter(batch_size, features, labels): # 从迭代器中选取每次batch的训练数据
l = loss(net(X,w,b),y).sum() # 求损失函数值
l.backward() # 利用backward进行求导,得到的梯度会保存再w,b的grad属性中
sgd([w,b],lr,batch_size) # 利用小批量梯度下降法进行更新
w.grad.data.zero_() # grad.data.zero_()将梯度清零,不然梯度会累加
b.grad.data.zero_() # grad.data.zero_()将梯度清零,不然梯度会累加
train_1 = loss(net(features,w,b),labels)
print('epoch %d, loss %f' % (epoch + 1, train_1.mean().item()))
上述流程是关键。
Step1:(正向传播)用loss(net(X,w,b),y).sum()求得最终损失值。
Step2:(反向传播)根据w,b定义了requires_grad=True可以进行梯度计算,并把计算储存在w,b中的grad属性。
Step3:利用小批量随机梯度下降法进行参数更新。
Step4:对w,b进行梯度清0,否则梯度会累加。
Step5:对选取的小批量数据完成Step1到Step4循环后,进行下一个epoch。
线性回归的简洁实现
torch.utils.data 模块提供了有关数据处理的⼯具
torch.nn 模块定义了⼤量神经⽹络的层
torch.nn.init 模块定义了各种初始化⽅法
torch.optim 模块提供了模型参数初始化的各种⽅法
先上代码
import torch
from torch import nn
import numpy as np
from torch.nn import init# 初始化模型参数
import torch.utils.data as Data # 导入生成迭代器的库
import torch.optim as optim
torch.manual_seed(1) # 设置随机数种子
torch.set_default_tensor_type('torch.FloatTensor')# 修改默认tensor类型
# 生成数据
num_inputs = 2
num_examples = 1000
true_w = [2, -3.4]
true_b = 4.2
features = torch.tensor(np.random.normal(0,1,(num_examples,num_inputs)),dtype=torch.float)
labels = true_w[0] * features[:, 0] + true_w[1] * features[:, 1] + true_b
labels += torch.tensor(np.random.normal(0, 0.01, size=labels.size()), dtype=torch.float)
batch_size = 10 # 设置
dataset = Data.TensorDataset(features, labels)
# 生成迭代器,shuffle为是否打乱,num_workers多线程
data_iter= Data.DataLoader(dataset=dataset,batch_size=batch_size,shuffle=True,num_workers=2)
# 定义模型
class LinearNet(nn.Module):
def __init__(self, n_feature):
super(LinearNet , self).__init__()# 继承LinearNet中的初始化方式
self.linear= nn.Linear(n_feature,1)
def forward(self ,x):# 正向传播
y= self.linear(x)
return y
net = LinearNet(num_inputs)
# 初始化模型参数
init.normal(net.linear.weight, mean=0.0, std=0.01)
init.constant(net.linear.bias, val=0.0)
# 定义损失函数
loss = nn.MSELoss()
# 定义优化方式
optimizer= optim.SGD(net.parameters(), lr=0.01)
# 训练
num_epochs = 3
for epoch in range(1,num_epochs + 1):
for X,y in data_iter:
output = net(X)
l = loss(output , y.view(-1,1))# 正向传播
optimizer.zero_grad()# 梯度清0
l.backward()# 计算梯度
optimizer.step()# 更新参数
print('epoch %d, loss: %f' %(epoch, l.item()))
代码解读:
优化器
optimizer= optim.SGD(net.parameters(), lr=0.01)
optimizer中保存了net中的参数,因此当l.backward()执行的时候,梯度会存储在grad中,即在optimizer中,因此可以用step进行更新,注意此处的lr是对全局全部参数更新的学习率
如果想对某层网络的学习率进行调整,可以以下方式。
optimizer =optim.SGD([
# 如果对某个参数不指定学习率,就使⽤最外层的默认学习率
{'params': net.subnet1.parameters()}, # lr=0.03
{'params': net.subnet2.parameters(), 'lr': 0.01}
], lr=0.03)
模型构建
# 定义模型
class LinearNet(nn.Module):
def __init__(self, n_feature):
super(LinearNet , self).__init__()# 继承LinearNet中的初始化方式
self.linear= nn.Linear(n_feature,1)
def forward(self ,x):# 正向传播
y= self.linear(x)
return y
此处模型的定义用了继承的方式,定义了类继承了线性模型。
其中forward会在net(X)执行的时候自动调用,因为nn.Module中定义了__call__,当net(X)执行时候会自动调用。
除了上述定义模型的方法还可以创建Sequential
# 写法一
net = nn.Sequential(
nn.Linear(num_inputs, 1)
# 此处还可以传入其他层
)
# 写法二
net = nn.Sequential()
net.add_module('linear', nn.Linear(num_inputs, 1))
# net.add_module ......
# 写法三
from collections import OrderedDict
net = nn.Sequential(OrderedDict([
('linear', nn.Linear(num_inputs, 1))
# ......
]))
print(net)
print(net[0])
从上面的过程可以看出,一个完整的流程如下:
Step1:定义模型
Step2:传入数据进行前向传播,得到误差
Step3:反向传播计算梯度,更新梯度,回到Step2