线性回归算法从零开始实现(pytorch)

概要

回归是能为一个或多个自变量与因变量关系建模的一类方法。

分类和回归都是基于输入做预测,都属于监督学习,需要根据特征从输入中提取信息,勇于进行类别判断或者量值预测。

分类的输出是离散的,回归的输出是连续的(注意不是传统数学意义上的离散和连续,分类需要输出的是类别,不同类别明显有着区别,是一种定性的问题,而回归问题获得的是一个在某个合理范围内都可以回归出来的一个结果,是一个定量的值)

整体架构流程

一、生成数据集

二、读取数据集

三、初始化参数模型

四、模型定义

五、损失函数

六、优化算法定义

七、模型训练

关键函数解释

torch.normal()

返回一个张量,是pytorch中生成服从正态分布的随机数张量的函数。

主要有三种重载。

torch.normal(meanstd*generator=Noneout=None) → Tensor

torch.normal(mean,std,size,*,generator,out) → Tensor

torch.normal(meanstdsize*out=None) → Tensor

*意为将参数列表中的参数分开,*以前的参数可以按位传递或者按关键字传递,但是*之后的参数只能按照关键字传递

也就是说

而*之前的参数就不论方式。

  • mean:正态分布的均值。
  • std:正态分布的标准差。
  • size:生成张量的形状。
  • out:可选参数,用于指定输出张量。
  • generator :指定自定义的随机数生成器对象,以控制生成随机数的方式。
torch.normal(1,0,generator=....,out=out_tensor)
#想使用generator和out参数就必须要使用关键字
torch.normal(mean=1,std=0,size=(2,2),generator=custom_generator,out=out_tensor)
#控制out参数=out_tensor与调用torch.normal()函数将张量存储到out_tensor一样

torch.normal函数中,meanstd参数可以是标量(单个值)或者张量(多个值)。如果meanstd是张量,它们的形状不需要匹配,但是它们的元素个数需要相同。这意味着,即使meanstd的形状不同,只要它们包含的元素个数相同,就可以成功调用torch.normal函数。

例如,如果mean是形状为(2, 1)的张量,std是形状为(1, 2)的张量,只要它们各自包含两个元素,即可成功调用torch.normal函数。

torch.matual()

两个张量的矩阵乘积,支持广播机制,乘积结果和张量维度有关。

torch.matmul(input, other, *, out=None) → Tensor

 一:两个一维张量相乘,结果是一个标量(普通的向量点乘)

a = torch.tensor([1,2,4])
b = torch.tensor([2,5,6])
print(torch.matmul(a, b))
print(a.shape)
》》tensor(36)
》》torch.Size([3])
#张量a.shape显示的是torch.Size([3])这里表示只有一个维度,3指的是这个维度中的元素个数

二、如果两个二维张量相乘,就是普通的矩阵乘法 。

三、如果input是一维,other是二维,那么在计算时,在input处将升维,计算后再降维。

a = torch.tensor([1,2,4])
b = torch.tensor([
    [2,5],
    [1,2],
    [6,8]
])

print(a.shape)
print(b.shape)
print(torch.matmul(a, b))
》》torch.Size([3])
》》torch.Size([3, 2])
》》tensor([28, 41])
#a只有一个维度,在进行计算时,借助广播机制,a维度变为二维,形状变为(1,3)
即[1,2,4]->[[1,2,4]]然后再与维度为二维的b相乘(b形状为(3,2))。
获得一个二维形状为(1,2)的张量,最后降维
[[28,41]]->[28,41]

  • Bert
  • GPT 初代
  • GPT-2
  • GPT-3
  • ChatGPT

技术细节

一、生成数据集

import torch 
import random
from d2l import torch as d2l

 我们使用线性模型参数w=[2,-3.4]T、b=4.2和噪声项\epsilon生成数据集和标签。

                                                                y=Xw+b+\epsilon

 \epsilon作为模型预测和标签的潜在观测误差。在这里认为标准假设成立,即\epsilon服从均值为0的正态分布。

def synthetic_data(w,b,num_examples)
    """生成y=Xw+b+e"""
    X=torch.normal(0,1,(num_examples,len(w)))
    y=torch.matual(X,w)+b
    y+=torch.normal(0,0.01,y.shape)
    return X,y.reshape((-1,1))
    #返回生成的标签y和特征X,并将其reshape为二维张量。
    #-1表示在列数确定为1的情况下根据原始张量的大小自动计算行数
true_w=torch.tensor([2,-3,4])
true_b=4.2
features, labels=synthetic_data(true_w,true_b,1000)
#调用synthetic_data函数生成包含1000个样本的合成数据集
#并将特征赋值给features,标签赋值给labels
print('features:',features[0],'\nlabel:',labels[0])

d2l.set_figsize()
#d2l库中的一个功能,设置图像的尺寸和大小方便绘制散点图
d2l.plt.scatter(features[:,1].detach().numpy(),labels.detach().numpy(),1);
#.detach()用于切断梯度计算,便于将pytorch张量转化为numpy数组,1表示散点的大小为1

我们来逐行解释代码

X=torch.normal(0,1,(num_examples,len(w))
#调用normal函数生成一个均值为0标准差为1,大小为num_examples * len(w)的一个张量
#矩阵长为num_examplse,宽为len(w),就是为了随机生成一个tensor而已
y=torch.matmul(X,w)+b
#y=Xw+b
#接下来生成噪声
y+=torch.normal(0,0.01,y.shape)
#y.shape是labels的形状,是一个二维形状为(1000,1)的张量,表示有1000个样本,每个样本有1个特征
return X,y.reshape(-1,1)
#如果原始张量的大小为(m, n),那么reshape(-1, 1)操作将会将其重新塑形为一个(p, 1)的二维张量,其中p = m * n。

       pytorch中的索引和切片功能

与任何python数组一样,第一个元素的索引是0(顺着过去0,1,2,3.....),最后一个元素的索引是-1(从后往前-1,-2,-3....)。 

 pytorch风格的索引

import torch
 
a = torch.rand(4, 3, 28, 28)
print(a[0].shape) #取到第一个维度
print(a[0, 0].shape) # 取到二个维度
print(a[1, 2, 2, 4])  # 具体到某个元素

>>>torch.Size([3, 28, 28])
>>>torch.Size([28, 28])
>>>tensor(0.1076)

 a是一个四维张量,可以理解为四张三通道像素值为28x28的图片,第一个维度的元素是4,排列着四张图片,第二个维度上的元素是上,意味着三个通道,后面两个维度代表像素值。

print(a[0].shape)就是取第一个维度上的第一个元素,也就是取第一张图片

print(a[0, 0].shape)就是取到第一个维度上的第一个元素(第一张图片)与第二个维度上的第一个元素,也就是取到第一张图片上的第一个通道。

print(a[1, 2, 2, 4])就是取第一维上的第二个元素(第二张图片)与第二个维度上第三个元素(第二张图片上的第三个通道)与第三维上的第三个元素和第四维上的第五个元素。

也就是具体取到了第二张图片上的第三个通道上的第三行四列的像素值。

 二、读取数据集

训练模型时需要对数据集进行遍历,每次抽取小批量的样本,并使用他们来更新我们的模型,由于这个过程是训练机器学习模型的基础,所以我们有必要定义一个函数去进行打乱数据集中的数据并随机以小批量的方式获取数据。

def data_iter(batch_size,featrures,labels)
    num_examples=len(features)
    indices=list(range(num_examples))#随机读取样本,创建一个包含0到num_examples-1的索引列表
    random.shuffle(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]

定义一个data_iter函数,该函数接受批量大小,特征矩阵和标签向量作为输入,生成大小为batch_size的小批量。每个小批量包含一组特征和标签。 

batch_indices = torch.tensor(indices[i:min(i+batch_size, num_examples)])

这里是根据当前索引i和批次大小batch_size,创建一个包含当前批次索引样本的张量。

 而最后一行则是利用yield关键字生成器,每次迭代生成一个包含当前批次特征数据和标签数据的元组,实现按批次生成数据的功能。

三、初始化模型参数

我们在通过梯度下降算法更新这些参数之前需要对这些参数进行初始化,初始化之后才能便于我们更新这些参数去拟合数据。

w=torch.normal(0,0.01,size=(2,1),requires_grad=True)
b=torch.zeros(1,requires_grad=True)

 四、模型定义

这里我们是实现线性回归算法,故我们的模型当然也要定义为线性模型,这个没什么好说的,也就两行代码。

def liner(X,w,b):
    return torch.matmul(X,w)+b

 五、定义损失函数

def squared_loss(y_hat,y):
    #均方损失
    return (y_hat-y.reshape(y_hat.shape))**2/2
"""将真实值y转换为和预测值y_hat的形状相同"""

 六、定义优化算法

批量随机梯度下降算法是传统的梯度下降算法的变体,我们知道最简单的梯度下降算法实现简单且通俗易懂,只需要根据损失函数一步步更新参数就行了,但是他的缺点也很明显,就是他的超参数学习率非常敏感,学习率过小将导致收敛速度慢,学习率过大则可能训练中跳过极值点,而在梯度平滑的区域,可能因为导数(梯度)接近于0而提前终止训练。这无疑给我们带来了很多问题。

因此,选择一个合适的优化算法对于我们模型泛化性能的提升起到了至关重要的作用。

这里使用一个比较简单的优化算法,批量随机梯度下降算法,Mini-batch。

def sgd(params,lr,batch_size):
    """Mini_batch Stochastic Gradient Descent"""
    with torch.no_grad():
        for param in params:
            param-=lr*param.grad /batch_size
            param.grad.zero_()

 这种梯度下降算法有两个特点:随机性和批量性。

随机性意味着每次从样本中抽取样本做梯度下降是随机抽取的,这就意味着在每次迭代中能更好的拟合到新数据集上,也更能跳出局部最优点,从而找到全局最优点。

批量性意味着处理样本的时候不是一次性将全部样本处理更新,不是计算整个数据集的梯度,而是每次做分批次的梯度更新,更有助于处理大规模的数据集。

七、训练

lr=0.03
num_epochs=3
net=liner
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)#X和y的小批量损失
        """l的形状是(batch_size,1)而不是标量"""
        """l中所有元素做求和,计算关于[w,b]的梯度"""
        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}')

小结 

这样我们的整个流程就做完了,但是实际应用我们不可能从底层不适用任何框架实现,事实上目前的主流深度学习框架都非常成熟,实现线性回归不过是十几行的事情,但是我们有必要去剖析内部原理,这有助于我们之后的学习。

  • 17
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值