pytorch从0构建线性回归模型

324 阅读8分钟

从0构建线性回归模型

参考:《动手学深度学习》——李沐

模型原理

模型定义:(以二维变量为例)

y^=w1x1+w2x2+b\hat{y}=w_1x_1+w_2x_2+b

​ 构建的模型得到的y^\hat{y}是对真实数据yy的估计,假设采集的样本数为nn,索引为ii的样本特征为x1(i)x_1^{(i)}x2(i)x_2^{(i)},标签为y(i)y^{(i)}则有:

y^(i)=w1x1(i)+w2x2(i)+b\hat{y}^{(i)}=w_1x_1^{(i)}+w_2x_2^{(i)}+b

定义损失函数:平方误差

l(w1,w2,b)=1ni=1nl(i)(w1,w2,b)=1ni=1n(w1x1(i)+w2x2(i)+by(i))2l(w_1,w_2,b)=\frac{1}{n}\sum_{i=1}^{n}{l^{(i)}(w_1,w_2,b)}=\frac{1}{n}\sum_{i=1}^{n}{(w_1x_1^{(i)}+w_2x_2^{(i)}+b-y^{(i)})^2}

​ 在模型训练中,我们希望找出⼀组模型参数,记为w1w_1^*w2w_2^*bb^*来使训练样本平均损失最小:

w1,w2,b=argminw1,w2,bl(w1,w2,b)w_1^*,w_2^*,b=\underset{w_1,w_2,b}{arg\min} l\left( w_1,w_2,b \right)

如上述公式可知,通常,我们用训练数据集中所有样本误差的平均来衡量模型预测的质量

​ 但对于大量的数据时,采用所有样本往往使得计算量变大。因此将采用小批量随机梯度下降法(BGD)。

优化方法

​ 此处需要一定的凸优化基础,可以参考《convex Optimization》—Boyd and L. Vandenberghe

梯度下降法(批量梯度下降法)

​ 我们考虑如下优化目标。

w=argminwf(w,x)w^*=\underset{w}{arg\min} f(w,x)

​ 其中xx为给定的数据,其中ff可以为任意损失函数。

​ 因此由梯度下降法可以得到,下一个迭代点为:

wn+1=wnαf(w,x)ww_{n+1}=w_n-\alpha\frac{\partial f(w,x)}{\partial w}

可以看出,此处用了所有的数据,由于y^\hat{y}可以改写为y^=[x1,x2,1][w1,w2,b]T=xwT\hat{y}=[x_1,x_2,1]*[w_1,w_2,b]^T=x*w^T,其中w=[w1,w2,b]w=[w_1,w_2,b],所以我们将考虑优化函数:(此处12\frac{1}{2}只是为了最终求解的梯度较为好看)

w=argminwf(w,x)w=argminw1ni=1n12(x(i)wTy(i))2w^*=\underset{w}{arg\min}\frac{\partial f(w,x)}{\partial w}=\underset{w}{arg\min}\frac{1}{n}\sum_{i=1}^{n}{\frac{1}{2}(x^{(i)}*w^T-y^{(i)})^2}

​ 可以看出此问题是一个凸问题,存在全局最优解,采用梯度下降法是可以得到最优解的,求梯度可得:

f(w,x)w=1ni=1n12(x(i)wTy(i))2w=1ni=1nx(i)(x(i)wTy(i))\frac{\partial f(w,x)}{\partial w}=\frac{\partial \frac{1}{n}\sum_{i=1}^{n}{\frac{1}{2}(x^{(i)}*w^T-y^{(i)})^2}}{\partial w}=\frac{1}{n}\sum_{i=1}^{n}x^{(i)}(x^{(i)}*w^T-y^{(i)})

​ 可以看出,上述的下降梯度用上了所有的数据,此方法存在的缺点则是:计算量大。存在的优点则是:相比于SGD和BGD下降速度较快

随机梯度下降法(SGD)

算法原理:(用随机一个梯度近似全体梯度)

​ 随机梯度下降法则是随机选取一个样本,进行梯度计算,进行参数更新。

​ 即将上述f(w,x)w\frac{\partial f(w,x)}{\partial w}改为:只用一个样本进行计算梯度

f(w,x)w=x(i)(x(i)wTy(i))\frac{\partial f(w,x)}{\partial w}=x^{(i)}(x^{(i)}*w^T-y^{(i)})

收敛性:(我还没理解透,凸优化和概率论之间的联系还没搞清楚,听说是依期望收敛)

参考:浅谈随机梯度下降&小批量梯度下降 - 知乎 (zhihu.com)

​ 假设我们样本量为nn,则优化函数可以写出:

w=argminwf(w,x)=argminw1n(f1(w,x(1))+f2(w,x(2))++fn(w,x(n)))w^*=\underset{w}{arg\min} f(w,x)=\underset{w}{arg\min} \frac{1}{n}(f_1(w,x^{(1)})+f_2(w,x^{(2)})+\cdots+f_n(w,x^{(n)}))

​ 其中ff为损失函数,此处有点滥用符号,但此式子不难理解,即对所有的损失加起来的最终值进行求最小

​ 例如:ff是平方误差损失即f=1ni=1n12(x(i)wy(i))2f=\frac{1}{n}\sum_{i=1}^{n}{\frac{1}{2}(x^{(i)}*w-y^{(i)})^2},则fi=12(x(i)wy(i))2f_i={\frac{1}{2}(x^{(i)}*w-y^{(i)})^2}

​ 则有:

f(w,x)w=1n(f1(w,x(1))w+f2(w,x(2))w++fn(w,x(n))w)\frac{\partial f(w,x)}{\partial w}=\frac{1}{n}(\frac{\partial f_1(w,x^{(1)})}{\partial w}+\frac{\partial f_2(w,x^{(2)})}{\partial w}+\cdots+\frac{\partial f_n(w,x^{(n)})}{\partial w})

​ 随机梯度下降法则随机选取一个:

fi(w,x(i))w\frac{\partial f_i(w,x^{(i)})}{\partial w}

​ 代替f(w,x)w\frac{\partial f(w,x)}{\partial w}进行参数更新。

优缺点:

​ 优点:计算量小。

​ 缺点:参数更新慢。

小批量随机梯度下降法(BGD)

算法原理:是随机梯度缺点的改进,优点的牺牲。利用小批量样本进行梯度更新。

​ 小批量随机梯度下降法则是随机选取小批量的样本,进行梯度计算,进行参数更新。

​ 即将上述f(w,x)w\frac{\partial f(w,x)}{\partial w}改为:用小批量样本进行计算梯度。

f(w,x)w=1BiBx(i)(x(i)wy(i))\frac{\partial f(w,x)}{\partial w}=\frac{1}{|\mathcal{B}|}\sum_{i\in \mathcal{B}}x^{(i)}(x^{(i)}*w-y^{(i)})

收敛性:和随机梯度下降原理一样。

优缺点:是梯度下降和随机梯度下降的中和。

代码实现

先上代码

# 导入库
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()))

输出:

9699cc708a98e2ebcdef01e5ad0e9d3.jpg

代码解读

​ 只对主体代码进行解读。

# 创建数据迭代器,进行批量学习
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是从原始数据中产生小批量B\mathcal{B}数据迭代器的函数。

​ 其中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

linregsquared_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