深度学习2:线性表示代码

本次内容比较简单,是李哥深度学习课程的第一个模型代码。博客写作思路是先介绍了数据生成函数以及如何简单调用,介绍了取数据函数以及如何简单调用,而后才开始模型实战并在实战中调用这两个函数。

任务


根据真实的数据来推测出【恋爱次数】和【外貌、性格、财富、内涵】四个维度之间的关系,模型实现的是y=x*w+b的线性关系。

引用库


import torch
import matplotlib.pyplot as plt #画图用
import random #随机

torch库最重要的作用就是为用户实现张量的计算,包括前向传播时的矩阵相乘、相加,反向传播时的求导等,都会在后面的代码中用到。

matplotlib.pyplot是用来画图的一个库,后面用来进行可视化。

random是随机库,在后面数据的打乱中会用到。 

生成数据


       因为是模拟,并没有真实的数据,因此我们要先自己生成一组数据。

       如何生成数据?

       根据已知的真实的恋爱次数和四个方面间的真实模型,基于正态分布生成一组符合真实情况的数据,生成后我们再拿这笔数据来开始一个正常且完整的模型的构建与训练过程。

1.数据生成函数:

        x1,x2,x3,x4对应的是四个方面的维度,如果样本数为1就是1×4矩阵,这里生成500个样本,最终会形成一个500×4的矩阵。这里的w,b是待传入的真实模型的参数,data_num为想要生成的样本数。

def creat_data(w,b,data_num):
    x = torch.normal(0,1,(data_num,len(w)))#这里len(w)是求容器一维长度。
    y = torch.matmul(x,w) + b #y=xw+b,得到的y是一个500×1的张量
    noise = torch.normal(0,0.01,y.shape) #噪声要加到y上
    y += noise
    return x,y # 返回x,y,其中x是一个500×4的tensor,y是一个500×1的tensor

第2行:这里len(w)是python自带的求Python 容器(如列表、元组、字典、张量等)的一维长度。

第3行:这里的这个公式是y=xw+b因为最终要得到的y是一个(500,1)的张量,注意矩阵相乘是x*w,不要反了,起初我以为w是一个(,4)张量实际上是(4,)。x为(500,4),w为(4,  ),w又被称为 1D向量,后面我们在传入w后pytorch会自动将其视作列向量进行处理而不是单一的数字,所以与之前所写的有所不同

第4行:加入噪声,因为实际数据不会这么精准,会有一点误差

2.调用数据生成函数来生成数据:

通过真实的模型参数调用数据生成函数,生成500个样本。

num = 500 # 实际的参数值,要转换成张量
true_w = torch.tensor([8.1,2,2,4])
true_b = torch.tensor(1.1)
X,Y = creat_data(true_w,true_b,num) # 调用生成数据函数

3.通过画图库对生成的数据可视化:

画图代码如下:

 plt.scatter(X[:,0],Y,1)
 plt.show() 

第1行: 画图,画一个散点图,1表示每一个点的大小,Y表示Y张量,他是(500,1)的,但是X是(500,4)的张量我们没有办法直接拿X,Y进行对比找二者关系,因此我们只选取X的其中一列来与Y进行对比找关系即X[:,3]这样的话每一次对比一共是500个数据。而由前面我们知道X的每一列代表着不同的维度如果我们选择第0列就是看颜值和恋爱次数关系,第一列就是性格和恋爱次数的关系,其他同理

我们得到了真实的数据,现在我们要通过这些真实的数据来推测出真实的w和b从而得到模型

怎么取数据


1.构建取数据函数

函数的作用:每次返回1batch的数据即16个数据 

先上代码:

def data_provider(data,lable,batchsize):  #数据(x)  标签(y)  步长
    length = len(lable)    #取得标签的长度
    indices = list(range(length))  #按把标签长度的范围生成一个列表,命名为indices
    random.shuffle(indices) #打乱

    for each in range(0,length,batchsize):
        get_indices = indices[each:each+batchsize] #列表切片
        get_data = data[get_indices] #张量切片
        get_lable = lable[get_indices] #张量切片

        yield get_data,get_lable #有存档点的return

第1行: 取数据函数,每次访问这个函数就能提供一批数据,其中x就是数据,y就是数据x对应的标签,参数依次是:数据(x)  标签(y)  步长

第4行:不能按顺序取数据,要把数据打乱,这样才符合模型训练的随机性。这样打乱后下标的位置被打乱了,但是后面取数据的时候还是取的前十六个.....以此类推所以在整个的取数过程中,每个indices即下标对应的data和lable都是不变的,只是他们摆放的位置变了而我们在下面的for中取data和lable时是按照位置取的,从而实现随机取

第7行:列表的切片,步长为16,每次取16个数据为一组,如0-15,16-31,我们将其用作下表来取数据和标签

第8、9行:data和lable都是tensor,故以get_indices为下标,取若干组data和lable。get_data和get_lable的格式都是tensor

第11行:在后面调用取数据函数时解释 

2.如何调用取数据函数

batchsize = 16
for batch_x,batch_y in data_provider(X,Y,batchsize):
    print(batch_x,batch_y)

       这里的循环是这样的:我们使用这个循环的目的是从data_provider这个函数中不断的获取16个一组的data和lable数据。

       首先表明yeild get_data,get_lable是一个有存档点的返回函数,即保留each的值。

       传入的参数是我们已有的数据X和Y以及步长batchsize由此进入函数,进入函数后我们得到了在位置上排前16的下标对应的data和lable,返回这组tensor,但是保留了存档即记录了each的值为0,然后打印该组tensor。

       打印后又回到for batch_x,batch_y in data_provider(X,Y,batchsize):循坏开始处,重新进入函数,并且直接进入函数内的循环,此时each=16,最终返回一组tensor,保存each=16,打印该组tensor后重复上述过程,这个通过循环调用取数函数的代码后面训练模型时会有用到

关于batchsize(步长)有一个小点:

       关于batchsize(步长)的设置对模型训练有什么影响,这叫做Mini-batch 训练,当batchsize设置过小时,比较容易跳出局部最优解,可以提高泛化能力,但是会导致梯度不稳定,不易收敛。当batchsize设置过大时,梯度稳定,收敛稳定快速,但是泛化能力差且容易进入局部最优解。

       关于局部最优解:和最优解的区别:最小值和极小值的区别。解决方法:使用 Mini-Batch 训练;增加学习率(Learning Rate),学习率小收敛慢;添加噪声(Noise)

定义模型


def fun(x,w,b):
    pred_y = torch.matmul(x,w)+b
    return pred_y

预测的模型结构为:

代码中的pred_y和x,w,b都是张量的形式 

求损失函数


def maeLoss(pred_y,y):
    return torch.sum(abs(pred_y-y))/len(y) #abs是取绝对值的意思

计算损失函数用到的公式为: 

先求出来每一个预测值和实际值差的绝对值

再将这些绝对值加和求平均,其中下面的公式的小写字母l就是上面公式中的L(w,b) 

随机梯度下降,更新参数


def sgd(paras,lr): #paras表示所有参数的集合,在该模型中就是[w,b]
    with torch.no_grad(): #属于这句代码的部分不计算梯度
        for para in paras:
            para -=para.grad * lr  #不能写成para = para - para.grad*lr
            para.grad.zero_() #将使用过的梯度置为0,防止梯度的累加

第2行:默认情况下,所有涉及梯度计算的操作都会被记录到计算图中,以便反向传播,但模型参数的更新(para -= para.grad * lr)并不属于梯度计算的一部分,而是一个赋值操作,不应该被计算图追踪。

第4行,不能写成para = para - para.grad*lr。代码中的写法不会创建新的para张量,而是 在原地(in-place)修改para的值,不会影响计算图的构建,后者会创建一个新的张量,并将这个新张量赋值给para,这样para不再是原来的模型参数,而是一个新的张量,不会被梯度计算跟踪。

第6行:梯度清零。PyTorch 的计算图(computational graph)默认会累积梯度,也就是说,loss.backward()计算的梯度并不会覆盖para.grad,而是会累加到之前的梯度值上。如果不手动清零,每次backward() 计算的梯度都会累积,导致错误的参数更新。当然,在这段代码中没有用到求梯度backward(),但是这个函数不是独立运行的,在训练模型时会对w,b求梯度,如果在调用sgd()时不清零,则在更新参数的时候会出现错误。

比如下面的代码:

import torch

w = torch.tensor(2.0, requires_grad=True)
lr = 0.1

for i in range(3):  
    loss = w**2  
    loss.backward()  # 计算梯度

    print(f"Step {i}, Gradient: {w.grad.item()}")  # 梯度不会累加

    with torch.no_grad():
        w -= lr * w.grad  
    w.grad.zero_()  # 清零梯度,避免累加

print("Final w:", w)

如果没有梯度清零的那一步,最终w的梯度会是4—8—12;如果正常清零则会是:4—4—4。 

模型训练


1.设置学习率,初始化参数,设置训练的轮数

lr = 0.01 #取一个学习率,认为调整,属于超参数
w_0 = torch.normal(0,0.01,true_w.shape,requires_grad=True) #这个w需要计算梯度
b_0 = torch.tensor(0.01,requires_grad=True)
epochs = 50 # 训练的轮数,一共50轮,每一轮是500个数据

第2,3行:这里是训练模型之前的初始化参数,采用的是随机初始化。可以避免对称性问题——所有参数(权重 w)都初始化为相同的值(如 0),那么在反向传播时,它们会计算出相同的梯度,导致每个神经元执行相同的更新,这样的话,所有神经元在训练过程中都保持相同的行为,模型就学不到复杂特征。

2.开始训练

for epoch in range(epochs):
    data_loss = 0 #统计每一轮的Loss值 。虽然是统计每一轮的loss,但我们每一个批次都更新一次
    for batch_x,batch_y in data_provider(X,Y,batchsize):
        pred_y = fun(batch_x,w_0,b_0) #预测的y
        loss = maeLoss(pred_y,batch_y) #求损失函数
        loss.backward() #求损失函数的梯度
        sgd([w_0,b_0],lr) #更新参数
        data_loss += loss #把每一批的数据 loss 累加,作为这一轮(epoch)所有 batch 的总 loss
    #后面小括号内两个数据会替换掉前面引号内两个百分数,第一个是保留3位整数,第二个是小数点后保留6位
    print("epoch %03d: loss: %.6f"%(epoch,data_loss))

print("真实的函数值是",true_w,true_b)
print("训练得到的参数值是",w_0,b_0)

       关于训练过程的解释基本在注释中给出了,在这个训练过程中,每一轮有500个数据,每16个数据作为1批进行训练,也就是每一轮进行32批次训练。

       关于整个模型训练的前向传播反向传播:向前传播是指从输入数据开始,经过模型计算,最终得到预测结果(pred_y),然后计算损失值(loss),即代码的4、5行。向后传播是指从损失函数loss计算相对于模型参数的梯度,并用于更新参数,即代码的6、7行。同时因为这里对w、b的求梯度使得sgd()中要加入梯度清零语句。

       关于loss.backward():用来对loss计算梯度,即自动求偏导,公式如下:

       注意一下打印函数的格式

3.模型可视化

idx = 0 #取张量X的哪一列
plt.plot(X[:,idx].detach().numpy(),X[:,idx].detach().numpy()*w_0[idx].detach().numpy()+b_0.detach().numpy())
plt.scatter(X[:,idx],Y,1)
plt.show()

       X是一个4维的,我们没有办法画出来四个维度上的关系,因此我们只能取X的其中一列,同样的,x要和w进行相乘计算,既然只取了X的其中一列,那么w只能取1个值即第一行,这样x为(500,1),w为(1,1),相乘后为(500,1)。此外x,w都是在张量网上的我们要将其取下,使用到的是.detach().numpy()。

4.调试参数

根据损失函数以及可视图中的拟合效果对学习率进行调整最终发现lr=0.01时拟合效果最好。如果对batchsize(批次大小)或者训练轮数进行调整学习率也要调整。最终效果如下:

我的想法


       通过这个简单的线性表示问题,大致搞明白了深度学习是要干嘛的,对于一些最基本的框架有了大致的了解。中间遇到了不少不理解的地方都通过大模型解决了,确实也体会到了大模型对理解代码的帮助。作为一个跨考生,过完年后把李哥的前几节课过了一遍,配了环境学了学python基础,调用取数据函数的循环和取数据函数的循环有点懵调试了好一会,中间又流感发烧吃药在家躺了三天,一直到昨天晚上才把代码写出来。这是我第一次接触深度学习,第一次动手在计算机上写项目,当然太过于简单都称不上项目,也是第一次写csdn博客。自我感觉写的还是蛮细致的,花费了不少时间,会坚持下去,但可能不会像这篇这么细致了,太耗时间了。这次也是把自己能想到的东西都写上了,希望有一个好的开始!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值