深度学习作业四

1、过程推导 - 了解BP原理
2、数值计算 - 手动计算,掌握细节
3、代码实现 - numpy手推 + pytorch自动


对比【numpy】和【pytorch】程序,总结并陈述。
激活函数Sigmoid用PyTorch自带函数torch.sigmoid(),观察、总结并陈述。
激活函数Sigmoid改变为Relu,观察、总结并陈述。
损失函数MSE用PyTorch自带函数 t.nn.MSELoss()替代,观察、总结并陈述。
损失函数MSE改变为交叉熵,观察、总结并陈述。
改变步长,训练次数,观察、总结并陈述。
权值w1-w8初始值换为随机数,对比“指定权值”的结果,观察、总结并陈述。
权值w1-w8初始值换为0,观察、总结并陈述。
全面总结反向传播原理和编码实现,认真写心得体会。


数值计算部分

        为了方便书写,这里我把过程写在了纸上,这部分我先进行公式推导,方便实现numpy手推的过程,最后才把数值带入进行计算

1、前向传播计算h1,h2,o1,o2,以及均方误差error。

2、反向传播计算w1~w8的梯度

先计算w5~w8的梯度,再计算w1~w4的梯度

        在推导过程中,我想起来老师上课讲的时候提到了中国大学Mooc上的结果是用\delta 5\delta 6表示\delta 1\delta 2,我觉得应该存在一部分相似之处,只是在表示时有错误,于是我尝试了一下。这里我用含有\delta 5的表达式表示\delta 7,用含有\delta 6的表达式表示\delta 8\delta 1\, \delta 2\, \delta 3\, \delta 4 也同样可以表示出来。

这样表示可以方便计算,但也存在一个弊端就是:前面的计算结果会影响后面的结果。

计算w5~w8的梯度:

计算w1~w4的梯度:

3、进行参数更新

参数更新的迭代公式:


numpy手推实现部分

import numpy as np
def sigmoid(x):
    return 1/(1+np.exp(-x))
def forward(x1,x2,w1,w2,w3,w4,w5,w6,w7,w8,y1,y2):
    #前向传播
    inh1=w1*x1+w3*x2
    h1=sigmoid(inh1)
    inh2=w2*x1+w4*x2
    h2=sigmoid(inh2)
    print("隐藏层h1,h2:",round(h1,5),round(h2,5))
    ino1=w5*h1+w7*h2
    o1=sigmoid(ino1)
    ino2=w6*h1+w8*h2
    o2=sigmoid(ino2)
    print("预测值o1,o2:",round(o1,5),round(o2,5))
    error = 1 / 2 * ((o1 - y1)**2+(o2 - y2)**2)
    print("均方误差error:",round(error,2))
    return h1,h2,o1,o2
def BP(h1,h2,o1,o2):
    #反向传播
    a5=(o1-y1)*(o1*(1-o1))*h1
    a6=(o2-y2)*(o2*(1-o2))*h1
    a7=a5/h1*h2
    a8=a6/h1*h2
    a1=(a5*w5+a6*w6)*(1-h1)*x1
    a2=(a7*w7+a8*w8)*(1-h2)*x1
    a3=a1/x1*x2
    a4=a2/x1*x2
    print("误差传给每个权值(计算梯度):",round(a1,2), round(a2,2), round(a3,2), round(a4,2), round(a5,2), round(a6,2), round(a7,2),round(a8,2))
    return a1,a2,a3,a4,a5,a6,a7,a8
def up_w(w1,w2,w3,w4,w5,w6,w7,w8):
    step=1
    w1 = w1 - step * a1
    w2 = w2 - step * a2
    w3 = w3 - step * a3
    w4 = w4 - step * a4
    w5 = w5 - step * a5
    w6 = w6 - step * a6
    w7 = w7 - step * a7
    w8 = w8 - step * a8
    print("更改后的权重值w:",round(w1,2), round(w2,2), round(w3,2), round(w4,2), round(w5,2), round(w6,2), round(w7,2),round(w8,2))
    return  w1, w2, w3, w4, w5, w6, w7, w8


x1,x2,w1,w2,w3,w4,w5,w6,w7,w8,y1,y2=0.5,0.3,0.20,-0.40,0.50,0.60,0.10,-0.5,-0.30,0.80,0.23,-0.07
h1,h2,o1,o2=forward(x1,x2,w1,w2,w3,w4,w5,w6,w7,w8,y1,y2)
for i in range(1,101):
    print("========第", i, "轮更新:=========")
    h1,h2,o1,o2=forward(x1,x2,w1,w2,w3,w4,w5,w6,w7,w8,y1,y2)
    a1,a2,a3,a4,a5,a6,a7,a8=BP(h1,h2,o1,o2)
    w1,w2,w3,w4,w5,w6,w7,w8=up_w(w1,w2,w3,w4,w5,w6,w7,w8)

结果:

pytorch实现

import torch

x = [0.5, 0.3]
y = [0.23, -0.07]
print("输入值 x0, x1:", x[0], x[1])
print("输出值 y0, y1:", y[0], y[1])
w = [torch.Tensor([0.2]), torch.Tensor([-0.4]), torch.Tensor([0.5]), torch.Tensor(
    [0.6]), torch.Tensor([0.1]), torch.Tensor([-0.5]), torch.Tensor([-0.3]), torch.Tensor([0.8])]  # 权重初始值
for i in range(0, 8):
    w[i].requires_grad = True
print("权值w0-w7:")
for i in range(0, 8):
    print(w[i].data, end="  ")

def forward_propagate(x):  # 计算图
    in_h1 = w[0] * x[0] + w[2] * x[1]
    out_h1 = torch.sigmoid(in_h1)
    in_h2 = w[1] * x[0] + w[3] * x[1]
    out_h2 = torch.sigmoid(in_h2)

    in_o1 = w[4] * out_h1 + w[6] * out_h2
    out_o1 = torch.sigmoid(in_o1)
    in_o2 = w[5] * out_h1 + w[7] * out_h2
    out_o2 = torch.sigmoid(in_o2)

    print("正向计算,隐藏层h1 ,h2:", end="")
    print(out_h1.data, out_h2.data)
    print("正向计算,预测值o1 ,o2:", end="")
    print(out_o1.data, out_o2.data)

    return out_o1, out_o2

def loss(x, y):  # 损失函数
    y_pre = forward_propagate(x)  # 前向传播
    loss_mse = (1 / 2) * (y_pre[0] - y[0]) ** 2 + (1 / 2) * (y_pre[1] - y[1]) ** 2  # 考虑 : t.nn.MSELoss()
    print("损失函数(均方误差):", loss_mse.item())
    return loss_mse


for k in range(100):
    print("\n=====第" + str(k+1) + "轮=====")
    l = loss(x, y)  # 前向传播,求 Loss,构建计算图
    l.backward()  # 反向传播,求出计算图中所有梯度存入w中. 自动求梯度,不需要人工编程实现。
    print("w的梯度: ", end="  ")
    for i in range(0, 8):
        print(round(w[i].grad.item(), 2), end="  ")  # 查看梯度
    step = 1  # 步长
    for i in range(0, 8):
        w[i].data = w[i].data - step * w[i].grad.data  # 更新权值
        w[i].grad.data.zero_()  # 注意:将w中所有梯度清零
    print("\n更新后的权值w:")
    for i in range(0, 8):
        print(w[i].data, end="  ")

结果:

1、对比【numpy】和【pytorch】程序,总结并陈述。

       对比numpy和pytorch实现,可以看出两者实现的结果并没有明显差别。在numpy实现中,我主要对手推公式进行实现。pytorch则直接调用backward()函数实现反向传播,在pytorch实现中,需要把所有变量和值都设成张量

numpy:

pytorch:

可以看出两者结果基本相同。 

在pytorch实现中我遇到了一个问题:

把w以张量的形式输入时,我原本是这样写的:

w=torch.tensor([0.2,-0.4,0.5,0.6,0.1,-0.5,-0.3,0.8])

结果一直报错,于是我借鉴了 老师的博客 发现应该这样写:

w = [torch.Tensor([0.2]), torch.Tensor([-0.4]), torch.Tensor([0.5]), torch.Tensor(
    [0.6]), torch.Tensor([0.1]), torch.Tensor([-0.5]), torch.Tensor([-0.3]), torch.Tensor([0.8])]

经过查资料,询问同学我发现是因为w=torch.tensor([0.2,-0.4,0.5,0.6,0.1,-0.5,-0.3,0.8])的形式会导致0.2,-0.4,0.5,0.6,0.1,-0.5,-0.3,0.8是标量,而不是张量。

w[i].grad.data.zero_()的作用:

         将w[i]的梯度数据设置为零。这在每次更新模型参数之前是必要的,因为梯度是用于参数更新的,如果梯度不清零,那么梯度的累积可能会导致参数更新出现错误。在PyTorch中,每次计算都需要清零梯度,以便于下一次正确地计算新的梯度。

2、激活函数Sigmoid用PyTorch自带函数torch.sigmoid(),观察、总结并陈述。

 使用自带的torch.sigmoid()函数实现:

def forward_propagate(x):  # 计算图
    in_h1 = w[0] * x[0] + w[2] * x[1]
    out_h1 = torch.sigmoid(in_h1)
    in_h2 = w[1] * x[0] + w[3] * x[1]
    out_h2 = torch.sigmoid(in_h2)

    in_o1 = w[4] * out_h1 + w[6] * out_h2
    out_o1 = torch.sigmoid(in_o1)
    in_o2 = w[5] * out_h1 + w[7] * out_h2
    out_o2 = torch.sigmoid(in_o2)

    print("正向计算,隐藏层h1 ,h2:", end="")
    print(out_h1.data, out_h2.data)
    print("正向计算,预测值o1 ,o2:", end="")
    print(out_o1.data, out_o2.data)

    return out_o1, out_o2

 使用自己写的Sigmoid函数实现:

def sigmoid(x):
    return 1/(1+torch.exp(-x))
x = [0.5, 0.3]
y = [0.23, -0.07]
print("输入值 x0, x1:", x[0], x[1])
print("输出值 y0, y1:", y[0], y[1])
#w=torch.tensor([0.2,-0.4,0.5,0.6,0.1,-0.5,-0.3,0.8])
w = [torch.Tensor([0.2]), torch.Tensor([-0.4]), torch.Tensor([0.5]), torch.Tensor(
    [0.6]), torch.Tensor([0.1]), torch.Tensor([-0.5]), torch.Tensor([-0.3]), torch.Tensor([0.8])]  # 权重初始值
for i in range(0, 8):
    w[i].requires_grad = True
print("权值w0-w7:")
for i in range(0, 8):
    print(round(float(w[i].data),2), end="  ")

我发现两者基本上没什么差别。 

3、激活函数Sigmoid改变为Relu,观察、总结并陈述。

第一个图片是Sigmoid函数,第二个图片是ReLU函数:

第10轮:

第100轮:

第1000轮:

       通过对比,我发现ReLU函数收敛速度更快,在这个问题中ReLU函数更加合适。

通过查阅资料,这个问题我借鉴了https://www.zhihu.com/question/29021768

ReLU函数具有以下优点:

  • 解决了gradient vanishing问题 (在正区间)

  • 计算速度非常快,只需要判断输入是否大于0

  • 收敛速度远快于sigmoid和tanh

我认为ReLU函数优于Sigmoid函数的原因是:

1、采用sigmoid等函数,算激活函数时(指数运算),计算量大,反向传播求误差梯度时,求导涉及除法,计算量相对大,而采用Relu激活函数,整个过程的计算量节省很多。

2、对于深层网络,sigmoid函数反向传播时,很容易就会出现梯度消失的情况(在sigmoid接近饱和区时,变换太缓慢,导数趋于0,这种情况会造成信息丢失,从而无法完成深层网络的训练。

3、Relu会使一部分神经元的输出为0,这样就造成了网络的稀疏性,并且减少了参数的相互依存关系,缓解了过拟合问题的发生。

4、损失函数MSE用PyTorch自带函数 t.nn.MSELoss()替代,观察、总结并陈述。

使用函数t.nn.MSELoss()代码如下:

def loss(x, y):  # 损失函数
    y_pre = forward_propagate(x)  # 前向传播
    loss_mse =(1/2)*( torch.nn.MSELoss()(y_pre[0], y[0]) + torch.nn.MSELoss()(y_pre[1], y[1]))
    print("均方误差:", round(loss_mse.item(), 2))
    return loss_mse

 

       可以看到在到达100轮次后,手写的MSE值小于torch自带MSE值,可知手写情况比自带情况的收敛效果好。

5、损失函数MSE改变为交叉熵,观察、总结并陈述。

 要将损失函数MSE改变为交叉熵,需要定义相关loss函数:

在代码中,定义的交叉熵:使用二元交叉熵作为损失函数,这在多分类问题中较为常见:

def Error(x1, x2, y1, y2):
    y_pre = forward(x1, x2)  # 前向传播
 
    # 创建交叉熵损失函数
    loss = nn.CrossEntropyLoss()
 
    # 将预测结果和目标标签叠加在一起
    y_pred = torch.stack([y_pre[0], y_pre[1]], dim=1)
    y = torch.stack([y1, y2], dim=1)
 
    # 计算交叉熵损失
    loss_ce = loss(y_pred, y)
 
    return loss_ce

 

        可以看出,交叉熵损失函数逐渐下降,0.13~-0.31交叉熵损失函数有取负的情况。主要是因为交叉熵损失函数期望的输入是概率分布,而回归问题中的真实值通常不是概率分布。因此,将真实值直接用于交叉熵损失函数可能会导致不可预测的结果。

        交叉熵损失函数用于衡量两个概率分布之间的差距,而回归问题中通常关注的是预测值与真实值之间的差距,而不是它们之间的概率分布差异。所以,要计算回归问题的损失函数还是均方误差好一点。

6、改变步长,训练次数,观察、总结并陈述。

import torch

x = [0.5, 0.3]
y = [0.23, -0.07]
print("输入值 x0, x1:", x[0], x[1])
print("输出值 y0, y1:", y[0], y[1])
#w=torch.tensor([0.2,-0.4,0.5,0.6,0.1,-0.5,-0.3,0.8])
w = [torch.Tensor([0.2]), torch.Tensor([-0.4]), torch.Tensor([0.5]), torch.Tensor(
    [0.6]), torch.Tensor([0.1]), torch.Tensor([-0.5]), torch.Tensor([-0.3]), torch.Tensor([0.8])]  # 权重初始值
for i in range(0, 8):
    w[i].requires_grad = True
print("权值w0-w7:")
for i in range(0, 8):
    print(round(float(w[i].data),2), end="  ")

def forward_propagate(x):  # 计算图
    in_h1 = w[0] * x[0] + w[2] * x[1]
    out_h1 = torch.sigmoid(in_h1)
    in_h2 = w[1] * x[0] + w[3] * x[1]
    out_h2 = torch.sigmoid(in_h2)

    in_o1 = w[4] * out_h1 + w[6] * out_h2
    out_o1 = torch.sigmoid(in_o1)
    in_o2 = w[5] * out_h1 + w[7] * out_h2
    out_o2 = torch.sigmoid(in_o2)

    print("正向计算,隐藏层h1 ,h2:", end="")
    print(out_h1.data, out_h2.data)
    print("正向计算,预测值o1 ,o2:", end="")
    print(out_o1.data, out_o2.data)

    return out_o1, out_o2

def loss(x, y):  # 损失函数
    y_pre = forward_propagate(x)  # 前向传播
    loss_mse = (1 / 2) * (y_pre[0] - y[0]) ** 2 + (1 / 2) * (y_pre[1] - y[1]) ** 2  # 考虑 : t.nn.MSELoss()
    print("损失函数(均方误差):", loss_mse.item())
    return loss_mse
import matplotlib.pyplot as plt

# 初始化一个列表用于保存每次迭代的损失值
losses = []

for k in range(100):#设置训练轮数
    print("\n=====第" + str(k + 1) + "轮=====")
    l = loss(x, y) 
    l.backward() #反向传播函数
    print("w的梯度: ", end="  ")
    for i in range(0, 8):
        print(round(w[i].grad.item(), 2), end="  ")  # 查看梯度
    step = 100  # 步长
    for i in range(0, 8):
        w[i].data = w[i].data - step * w[i].grad.data  # 更新权值
        w[i].grad.data.zero_()  # 注意:将w中所有梯度清零

    # 将这次迭代的损失值添加到损失列表中
    losses.append(l.item())

    print("\n更新后的权值w:")
    for i in range(0, 8):
        print(round(float(w[i].data), 2), end="  ")
    print("\n")

# 使用matplotlib绘制损失值的图表
plt.plot(range(1, 101), losses)
plt.xlabel('Iteration')
plt.ylabel('Loss')
plt.title('Loss over Iterations')
plt.show()

step=1

更新10轮:

更新100轮:

更新1000轮:

可以看出,当step=1时,在更新150轮左右时达到最优。收敛速度比较快。

step=0.01

更新10轮:

更新100轮:

更新1000轮:

更新10000轮:

可以看出当step=0.01时收敛速度极慢,在更新10000轮左右时才会达到最优,在第10轮和第100轮时几乎是一条直线。

step=100

更新10轮:

更新100轮:

更新1000轮:

由上图可以看出step=100时,图像出现很大的波动,我觉得这是步长过大导致的。可以看出收敛速度极快,而且误差变化较大。但是经过多轮更新,误差仍然会达到最小。

       综上,我认为步长对计算来说很关键,步长过大可能会导致找不到最优解,步长过小则会导致收敛过于缓慢。


7、权值w1-w8初始值换为随机数,对比“指定权值”的结果,观察、总结并陈述。

w = [torch.randn(1,1), torch.randn(1,1), torch.randn(1,1), torch.randn(1,1), torch.randn(1,1), torch.randn(1,1), torch.randn(1,1), torch.randn(1,1)]

第一个图片是原本的,第二个图片是修改后的:

 第10轮:

第100轮:

第1000轮:

我发现修改权值为随机值后,只会改变收敛速度,不会改变最后的结果。

8、权值w1-w8初始值换为0,观察、总结并陈述。

第一个图片是原本的,第二个图片是修改后的:

 第10轮:

第100轮:

第1000轮:

我发现修改权值w为0后,只会改变收敛速度,不会改变最后的结果。

9、全面总结反向传播原理和编码实现,认真写心得体会。

1、我感觉直接调用backward()函数比自己手写的方便很多。而且代码量也少。但是backward()函数使用时,需要把常数以及变量都以张量的形式输入和计算。

2、(1)步长对反向传播算法来说很关键,步长过大会导致找不到最优解。步长过小时,收敛速度太慢。(2)w的初始值无论如何设置对结果都是没有影响的,只会影响收敛速度。(3)我认为ReLU函数在此次实验的算法中优于Sigmoid函数,Relu函数收敛速度优于sigmoid函数 。(4)交叉熵损失函数结果是有可能出现负数的情况的,需要结合softmax函数一起用。

3、在pytorch实现中我遇到了一个问题:

把w以张量的形式输入时,我原本是这样写的:

w=torch.tensor([0.2,-0.4,0.5,0.6,0.1,-0.5,-0.3,0.8])

结果一直报错,于是我借鉴了 老师的博客 发现应该这样写:

w = [torch.Tensor([0.2]), torch.Tensor([-0.4]), torch.Tensor([0.5]), torch.Tensor(
    [0.6]), torch.Tensor([0.1]), torch.Tensor([-0.5]), torch.Tensor([-0.3]), torch.Tensor([0.8])]

经过查资料,询问同学我发现是因为w=torch.tensor([0.2,-0.4,0.5,0.6,0.1,-0.5,-0.3,0.8])的形式会导致0.2,-0.4,0.5,0.6,0.1,-0.5,-0.3,0.8是标量,而不是张量。

4、w[i].grad.data.zero_()的作用:

         将w[i]的梯度数据设置为零。这在每次更新模型参数之前是必要的,因为梯度是用于参数更新的,如果梯度不清零,那么梯度的累积可能会导致参数更新出现错误。在PyTorch中,每次计算都需要清零梯度,以便于下一次正确地计算新的梯度。

5、写博客的过程中内容丢失,我觉得也算一次教训。  

       在写博客的过程中,我本来已经马上写完了,但是按了一下Ctrl+Z后我写的5~8题的回答全部消失了,在草稿箱里也找不到,可能是被覆盖了。以后写博客或者文档时,一定记得保存和备份,不然只会多做一些无用功,不仅浪费时间,还会导致作业交的晚。

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值