• NNDL:作业3:分别使用numpy和pytorch实现FNN例题


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

    一、过程推导 - 了解BP原理和数值计算 - 手动计算,掌握细节

    这两个写到一起了。

    (一)、目前已知

    x1=0.5   x2=0.3   y1=0.23   y2=-0.07 以及权重w1~w8

    相关导的公式:
     

    为方便计算又画了一遍图【图中变量名就是后期推导的名称】:

    (二)、编程求解sigmoid

    由于sigmoid函数手工计算比较麻烦,所以使用编程求解h1、h2和o1、o2。

    (1)定义函数

    1. import numpy as np
    2. def sigmoid(x):
    3. return 1 / (1 + np.exp(-x))

    (2)求解

    1. x1=0.5
    2. x2=0.3
    3. y1,y2 = 0.23, -0.07
    4. w=[0.2,-0.4,0.5,0.6,0.1,-0.5,-0.3,0.8]
    5. #h
    6. in_h1=x1*w[0]+x2*w[2]
    7. in_h2=x2*w[3]+x1*w[1]
    8. out_h1=sigmoid(in_h1)
    9. out_h2=sigmoid(in_h2)
    10. in_h1 = round(in_h1, 2)
    11. in_h2 = round(in_h2, 2)
    12. out_h1 = round(out_h1, 2)
    13. out_h2 = round(out_h2, 2)
    14. print('in_h1',in_h1,'in_h2',in_h2,'out_h1',out_h1,'out_h2',out_h2)
    15. #o
    16. in_o1=out_h1*w[4]+out_h2*w[6]
    17. in_o2=out_h1*w[5]+out_h2*w[7]
    18. out_o1=sigmoid(in_o1)
    19. out_o2=sigmoid(in_o2)
    20. in_o1 = round(in_o1, 2)
    21. in_o2 = round(in_o2, 2)
    22. out_o1 = round(out_o1, 2)
    23. out_o2 = round(out_o2, 2)
    24. print('in_o1',in_o1,'in_o2',in_o2,'out_o1',out_o1,'out_o2',out_o2)

     (3)得到结果

    (三)、推导

     如此,就求出了w1~w8的导

    (四)、更新权重

    可以看到,更新后的权重与题中相同。 

    二、代码实现 - numpy手推 + pytorch自动

    (一)、numpy完全手推

    第一次写的numpy代码是直接按照前边手写公式推的,如下:

    1. import numpy as np
    2. def sigmoid(x):
    3. return 1 / (1 + np.exp(-x))
    4. x1=0.5
    5. x2=0.3
    6. y1,y2 = 0.23, -0.07
    7. w=[0.2,-0.4,0.5,0.6,0.1,-0.5,-0.3,0.8]
    8. in_h1=x1*w[0]+x2*w[2]
    9. in_h2=x2*w[3]+x1*w[1]
    10. out_h1=sigmoid(in_h1)
    11. out_h2=sigmoid(in_h2)
    12. in_h1 = round(in_h1, 2)
    13. in_h2 = round(in_h2, 2)
    14. out_h1 = round(out_h1, 2)
    15. out_h2 = round(out_h2, 2)
    16. in_o1=out_h1*w[4]+out_h2*w[6]
    17. in_o2=out_h1*w[5]+out_h2*w[7]
    18. out_o1=sigmoid(in_o1)
    19. out_o2=sigmoid(in_o2)
    20. in_o1 = round(in_o1, 2)
    21. in_o2 = round(in_o2, 2)
    22. out_o1 = round(out_o1, 2)
    23. out_o2 = round(out_o2, 2)
    24. #反向推导
    25. def sigmoid_dao(x):
    26. return x*(1-x)
    27. def error_dao(out_o,y):
    28. return out_o-y
    29. w_dao=[0]*8
    30. w_dao[4]=error_dao(out_o1,y1)*sigmoid_dao(out_o1)*out_h1
    31. w_dao[5]=error_dao(out_o2,y2)*sigmoid_dao(out_o2)*out_h1
    32. w_dao[6]=error_dao(out_o1,y1)*sigmoid_dao(out_o1)*out_h2
    33. w_dao[7]=error_dao(out_o2,y2)*sigmoid_dao(out_o2)*out_h2
    34. H1=error_dao(out_o1,y1)*sigmoid_dao(out_o1)
    35. H2=error_dao(out_o2,y2)*sigmoid_dao(out_o2)
    36. w_dao[0]=(H1*w[4]+H2*w[5])*sigmoid_dao(out_h1)*x1
    37. w_dao[1]=(H1*w[6]+H2*w[7])*sigmoid_dao(out_h2)*x1
    38. w_dao[2]=(H1*w[4]+H2*w[5])*sigmoid_dao(out_h1)*x2
    39. w_dao[3]=(H1*w[6]+H2*w[7])*sigmoid_dao(out_h2)*x2
    40. H1 = round(H1, 2)
    41. H2 = round(H2, 2)
    42. for i in range(8):
    43. w_dao[i]= round(w_dao[i], 2)
    44. for i in range(8):
    45. print('w_dao:',i+1,w_dao[i])
    46. n=1
    47. w_xin=[0]*8
    48. for i in range(8):
    49. w_xin[i]=w[i]-n*w_dao[i]
    50. w_xin[i]=round(w_xin[i],2)
    51. print(i+1,w_xin[i])

    得到的结果是:

    w的导:

    w的更新:

    可以看到上边的两张图,与我自己手写推导的结果相同。

    (二)、numpy改良

    由于后边问题中,要求更改步长、迭代次数,观察效果,需要把前边的代码改良一下。

    (1)封装的函数

    第一部分:小函数的封装【sigmoid函数、sigmoid导的函数、error对out_o求的导、Error】

    1. def sigmoid(x):
    2. return 1 / (1 + np.exp(-x))
    3. def sigmoid_dao(x):
    4. return x*(1-x)
    5. def error_dao(out_o,y):
    6. return out_o-y
    7. def Error(out_o1,out_o2,y1,y2):
    8. error = (1 / 2) * (out_o1 - y1) ** 2 + (1 / 2) * (out_o2 - y2) ** 2
    9. error=round(error,2)
    10. return error

    第二部分: 正向计算,求解out_*

    1. #正向计算
    2. def forward(w,x1,x2,y1,y2):
    3. in_h1=x1*w[0]+x2*w[2]
    4. in_h2=x2*w[3]+x1*w[1]
    5. out_h1=sigmoid(in_h1)
    6. out_h2=sigmoid(in_h2)
    7. in_h1 = round(in_h1, 2)
    8. in_h2 = round(in_h2, 2)
    9. out_h1 = round(out_h1, 2)
    10. out_h2 = round(out_h2, 2)
    11. in_o1=out_h1*w[4]+out_h2*w[6]
    12. in_o2=out_h1*w[5]+out_h2*w[7]
    13. out_o1=sigmoid(in_o1)
    14. out_o2=sigmoid(in_o2)
    15. in_o1 = round(in_o1, 2)
    16. in_o2 = round(in_o2, 2)
    17. out_o1 = round(out_o1, 2)
    18. out_o2 = round(out_o2, 2)
    19. return out_h1,out_h2,out_o1,out_o2

    第三部分:反向求解w的梯度

    1. #反向计算
    2. def back(out_h1,out_h2,out_o1,out_o2):
    3. w_dao=[0]*8
    4. w_dao[4]=error_dao(out_o1,y1)*sigmoid_dao(out_o1)*out_h1
    5. w_dao[5]=error_dao(out_o2,y2)*sigmoid_dao(out_o2)*out_h1
    6. w_dao[6]=error_dao(out_o1,y1)*sigmoid_dao(out_o1)*out_h2
    7. w_dao[7]=error_dao(out_o2,y2)*sigmoid_dao(out_o2)*out_h2
    8. H1=error_dao(out_o1,y1)*sigmoid_dao(out_o1)
    9. H2=error_dao(out_o2,y2)*sigmoid_dao(out_o2)
    10. w_dao[0]=(H1*w[4]+H2*w[5])*sigmoid_dao(out_h1)*x1
    11. w_dao[1]=(H1*w[6]+H2*w[7])*sigmoid_dao(out_h2)*x1
    12. w_dao[2]=(H1*w[4]+H2*w[5])*sigmoid_dao(out_h1)*x2
    13. w_dao[3]=(H1*w[6]+H2*w[7])*sigmoid_dao(out_h2)*x2
    14. for i in range(8):
    15. w_dao[i]= round(w_dao[i], 2)
    16. return w_dao

    第四部分:w更新

    1. #更新权重
    2. def updata(n,w,w_dao):
    3. w_xin=[0]*8
    4. for i in range(8):
    5. w_xin[i]=w[i]-n*w_dao[i]
    6. w_xin[i]=round(w_xin[i],2)
    7. return w_xin

    (2)实例化

    1. x1=0.5
    2. x2=0.3
    3. y1,y2 = 0.23, -0.07
    4. w=[0.2,-0.4,0.5,0.6,0.1,-0.5,-0.3,0.8]
    5. n=1
    6. print('更新前的权重',w)
    7. for i in range(51):
    8. out_h1,out_h2,out_o1,out_o2=forward(w,x1,x2,y1,y2)
    9. w_dao=back(out_h1,out_h2,out_o1,out_o2)
    10. w=updata(n,w,w_dao)
    11. error=Error(out_o1,out_o2,y1,y2)
    12. if i%10==0:
    13. print("第",i,"轮次")
    14. print("正向计算:",out_o1,out_o2)
    15. print("反向计算,得到w的导:",w_dao)
    16. print("损失函数(均方误差):",error)
    17. print("更新后的权值w",w)

    (3)结果

     更新五十次,得到以下结果(每十次的一次结果),并且保留两位小数

    1. 更新前的权重 [0.2, -0.4, 0.5, 0.6, 0.1, -0.5, -0.3, 0.8]
    2. 0 轮次
    3. 正向计算: 0.48 0.53
    4. 反向计算,得到w的导: [-0.01, 0.01, -0.01, 0.01, 0.03, 0.08, 0.03, 0.07]
    5. 损失函数(均方误差): 0.21
    6. 更新后的权值w [0.21, -0.41, 0.51, 0.59, 0.07, -0.58, -0.33, 0.73]
    7. 10 轮次
    8. 正向计算: 0.4 0.35
    9. 反向计算,得到w的导: [-0.02, -0.0, -0.01, -0.0, 0.02, 0.06, 0.02, 0.05]
    10. 损失函数(均方误差): 0.1
    11. 更新后的权值w [0.33, -0.45, 0.61, 0.57, -0.22, -1.27, -0.58, 0.14]
    12. 20 轮次
    13. 正向计算: 0.35 0.25
    14. 反向计算,得到w的导: [-0.01, -0.0, -0.01, -0.0, 0.02, 0.04, 0.01, 0.03]
    15. 损失函数(均方误差): 0.06
    16. 更新后的权值w [0.44, -0.45, 0.71, 0.57, -0.42, -1.71, -0.75, -0.22]
    17. 30 轮次
    18. 正向计算: 0.32 0.19
    19. 反向计算,得到w的导: [-0.01, -0.0, -0.01, -0.0, 0.01, 0.02, 0.01, 0.02]
    20. 损失函数(均方误差): 0.04
    21. 更新后的权值w [0.54, -0.45, 0.81, 0.57, -0.56, -2.0, -0.85, -0.45]
    22. 40 轮次
    23. 正向计算: 0.29 0.15
    24. 反向计算,得到w的导: [-0.01, -0.0, -0.0, -0.0, 0.01, 0.02, 0.01, 0.01]
    25. 损失函数(均方误差): 0.03
    26. 更新后的权值w [0.64, -0.45, 0.9, 0.57, -0.66, -2.21, -0.95, -0.64]
    27. 50 轮次
    28. 正向计算: 0.28 0.13
    29. 反向计算,得到w的导: [-0.01, -0.0, -0.0, -0.0, 0.01, 0.01, 0.0, 0.01]
    30. 损失函数(均方误差): 0.02
    31. 更新后的权值w [0.74, -0.45, 0.9, 0.57, -0.76, -2.37, -0.98, -0.74]

    (三)、改为pytorch

    (1)代码

    1. import torch
    2. def sigmoid(x):
    3. return 1 / (1 + torch.exp(-x))
    4. def sigmoid_derivative(x):
    5. return x * (1 - x)
    6. def forward(x1,x2):
    7. in_h1=x1*w[0]+x2*w[2]
    8. in_h2=x2*w[3]+x1*w[1]
    9. out_h1=sigmoid(in_h1)
    10. out_h2=sigmoid(in_h2)
    11. in_o1=out_h1*w[4]+out_h2*w[6]
    12. in_o2=out_h1*w[5]+out_h2*w[7]
    13. out_o1=sigmoid(in_o1)
    14. out_o2=sigmoid(in_o2)
    15. print("正向计算:h1 ,h2:",out_h1.data, out_h2.data)
    16. print("正向计算:o1 ,o2:",out_o1.data, out_o2.data)
    17. return out_o1, out_o2
    18. def update(n, w, w_dao):
    19. w_updated = []
    20. for i in range(8):
    21. w_updated.append(w[i] - n * w_dao[i])
    22. return w_updated
    23. def Error(x1,x2,y1,y2):
    24. y_pre = forward(x1,x2) # 前向传播
    25. loss_mse =(1/2)*(y_pre[0]-y1)**2+(1/2)*(y_pre[1]-y2)** 2 # 考虑 : t.nn.MSELoss()
    26. print("损失函数(均方误差):", loss_mse.item())
    27. return loss_mse
    28. # 定义x1, x2, y1, y2和初始权重w
    29. x1 = torch.tensor([0.5])
    30. x2 = torch.tensor([0.3])
    31. y1 = torch.tensor([0.23])
    32. y2 = torch.tensor([-0.07])
    33. 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])]
    34. n = 1 # 步长
    35. for i in range(0, 8):
    36. w[i].requires_grad = True
    37. print("权值w0-w7:",w[i].data)
    38. for j in range(51):
    39. print("\n=====第" + str(j+1) + "轮=====")
    40. L = Error(x1,x2,y1,y2) # 前向传播
    41. L.backward() # 反向传播,自动求梯度。
    42. print("w的梯度: ", end=" ")
    43. for i in range(0, 8):
    44. print(round(w[i].grad.item(), 2), end=" ")
    45. for i in range(0, 8):
    46. w[i].data = w[i].data-n * w[i].grad.data # 更新权值
    47. w[i].grad.data.zero_() # 注意:将w中所有梯度清零
    48. print("\n更新后的权值w:",w[i].data)

    (2)结果

    1. 0
    2. 损失函数(均方误差): 0.2097097933292389
    3. -0.01 0.01 -0.01 0.01 0.03 0.08 0.03 0.07 更新后的权值w: tensor([0.2084])
    4. 更新后的权值w: tensor([-0.4126])
    5. 更新后的权值w: tensor([0.5051])
    6. 更新后的权值w: tensor([0.5924])
    7. 更新后的权值w: tensor([0.0654])
    8. 更新后的权值w: tensor([-0.5839])
    9. 更新后的权值w: tensor([-0.3305])
    10. 更新后的权值w: tensor([0.7262])
    11. 10
    12. 损失函数(均方误差): 0.10375461727380753
    13. -0.02 -0.0 -0.01 -0.0 0.02 0.06 0.02 0.05 更新后的权值w: tensor([0.3425])
    14. 更新后的权值w: tensor([-0.4540])
    15. 更新后的权值w: tensor([0.5855])
    16. 更新后的权值w: tensor([0.5676])
    17. 更新后的权值w: tensor([-0.2230])
    18. 更新后的权值w: tensor([-1.2686])
    19. 更新后的权值w: tensor([-0.5765])
    20. 更新后的权值w: tensor([0.1418])
    21. 20
    22. 损失函数(均方误差): 0.05818723142147064
    23. -0.01 -0.0 -0.01 -0.0 0.02 0.04 0.01 0.03 更新后的权值w: tensor([0.4856])
    24. 更新后的权值w: tensor([-0.4246])
    25. 更新后的权值w: tensor([0.6714])
    26. 更新后的权值w: tensor([0.5853])
    27. 更新后的权值w: tensor([-0.4255])
    28. 更新后的权值w: tensor([-1.7091])
    29. 更新后的权值w: tensor([-0.7422])
    30. 更新后的权值w: tensor([-0.2187])
    31. 30
    32. 损失函数(均方误差): 0.03740861266851425
    33. -0.01 -0.0 -0.01 -0.0 0.01 0.02 0.01 0.02 更新后的权值w: tensor([0.6021])
    34. 更新后的权值w: tensor([-0.3828])
    35. 更新后的权值w: tensor([0.7413])
    36. 更新后的权值w: tensor([0.6103])
    37. 更新后的权值w: tensor([-0.5658])
    38. 更新后的权值w: tensor([-2.0030])
    39. 更新后的权值w: tensor([-0.8544])
    40. 更新后的权值w: tensor([-0.4537])
    41. 40
    42. 损失函数(均方误差): 0.026734821498394012
    43. -0.01 -0.0 -0.0 -0.0 0.01 0.02 0.01 0.01 更新后的权值w: tensor([0.6939])
    44. 更新后的权值w: tensor([-0.3438])
    45. 更新后的权值w: tensor([0.7963])
    46. 更新后的权值w: tensor([0.6337])
    47. 更新后的权值w: tensor([-0.6630])
    48. 更新后的权值w: tensor([-2.2142])
    49. 更新后的权值w: tensor([-0.9311])
    50. 更新后的权值w: tensor([-0.6204])
    51. 50
    52. 损失函数(均方误差): 0.02064337022602558
    53. -0.01 -0.0 -0.0 -0.0 0.01 0.01 0.0 0.01 更新后的权值w: tensor([0.7672])
    54. 更新后的权值w: tensor([-0.3102])
    55. 更新后的权值w: tensor([0.8403])
    56. 更新后的权值w: tensor([0.6539])
    57. 更新后的权值w: tensor([-0.7309])
    58. 更新后的权值w: tensor([-2.3755])
    59. 更新后的权值w: tensor([-0.9843])
    60. 更新后的权值w: tensor([-0.7468])

    三、问题解答

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

     其中numpy中所有函数都要自己写,但是在pytorch中可以直接调用backward函数,相对便利。但是我也遇到了问题:关于将数组转化为tensor类型的:

    我开始使用的是:

    w = torch.tensor([0.2000, -0.4000, 0.5000, 0.6000, 0.1000, -0.5000, -0.3000, 0.8000])

    但是不对,只能使用下边这种方式,才能成功调用backward函数:

    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

     因为:“requires_grad”属性用于标记该张量是否需要计算其梯度。如果一个张量的“requires_grad”属性为True,那么PyTorch会在该张量进行操作时自动计算其梯度,并将结果存储在“grad”属性中。这个自动计算梯度的过程是由PyTorch的autograd系统完成的。

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

    为对比sigmoid激活函数与torch自带sigmoid,代码如下:

    torch中sigmoid函数:

    1. def forward(x1,x2):
    2. in_h1 = w[0] * x[0] + w[2] * x[1]
    3. out_h1 = torch.sigmoid(in_h1)
    4. in_h2 = w[1] * x[0] + w[3] * x[1]
    5. out_h2 = torch.sigmoid(in_h2)
    6. in_o1 = w[4] * out_h1 + w[6] * out_h2
    7. out_o1 = torch.sigmoid(in_o1)
    8. in_o2 = w[5] * out_h1 + w[7] * out_h2
    9. out_o2 = torch.sigmoid(in_o2)
    10. return out_o1, out_o2
    11. out_o1, out_o2=forward(x1,x2)
    12. print(out_o1,out_o2)

    sigmoid自写函数:

    1. def sigmoid(x):
    2. return 1 / (1 + torch.exp(-x))
    3. def forward(x1,x2): # 计算图
    4. in_h1 = w[0] * x[0] + w[2] * x[1]
    5. out_h1 = sigmoid(in_h1)
    6. in_h2 = w[1] * x[0] + w[3] * x[1]
    7. out_h2 = sigmoid(in_h2)
    8. in_o1 = w[4] * out_h1 + w[6] * out_h2
    9. out_o1 = sigmoid(in_o1)
    10. in_o2 = w[5] * out_h1 + w[7] * out_h2
    11. out_o2 = sigmoid(in_o2)
    12. return out_o1, out_o2
    13. out_o1, out_o2=forward(x1,x2)
    14. print(out_o1,out_o2)

    对比结果:

     可以看到结果相同,并没有明显区别。

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

    以pytorch为例,使用ReLu激活函数代码如下:

    1. import torch
    2. def relu(x):
    3. return torch.nn.functional.relu(x)
    4. def forward(x1,x2):
    5. in_h1=x1*w[0]+x2*w[2]
    6. in_h2=x2*w[3]+x1*w[1]
    7. out_h1=relu(in_h1)
    8. out_h2=relu(in_h2)
    9. in_o1=out_h1*w[4]+out_h2*w[6]
    10. in_o2=out_h1*w[5]+out_h2*w[7]
    11. out_o1=relu(in_o1)
    12. out_o2=relu(in_o2)
    13. return out_o1, out_o2
    14. def update(n, w, w_dao):
    15. w_updated = []
    16. for i in range(8):
    17. w_updated.append(w[i] - n * w_dao[i])
    18. return w_updated
    19. def Error(x1,x2,y1,y2):
    20. y_pre = forward(x1,x2) # 前向传播
    21. loss_mse =(1/2)*(y_pre[0]-y1)**2+(1/2)*(y_pre[1]-y2)** 2 # 考虑 : t.nn.MSELoss()
    22. return loss_mse
    23. x1 = torch.tensor([0.5])
    24. x2 = torch.tensor([0.3])
    25. y1 = torch.tensor([0.23])
    26. y2 = torch.tensor([-0.07])
    27. 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])]
    28. n = 1 # 步长
    29. for i in range(0, 8):
    30. w[i].requires_grad = True
    31. for j in range(1):
    32. L = Error(x1,x2,y1,y2) # 前向传播
    33. L.backward() # 反向传播,自动求梯度。
    34. if j%10==0:
    35. print("\n第",j,"轮")
    36. print("损失函数(均方误差):", L.item())
    37. for i in range(0, 8):
    38. if j%10==0:
    39. print(round(w[i].grad.item(), 2), end=" ")
    40. for i in range(0, 8):
    41. w[i].data = w[i].data-n * w[i].grad.data # 更新权值
    42. w[i].grad.data.zero_() # 注意:将w中所有梯度清零
    43. if j%10==0:
    44. print("更新后的权值w:",w[i].data)

    对比两种激活函数结果:

    sigmoid:

    1. 1
    2. 损失函数(均方误差): 0.2097097933292389
    3. -0.01 0.01 -0.01 0.01 0.03 0.08 0.03 0.07 更新后的权值w: tensor([0.2084])
    4. 更新后的权值w: tensor([-0.4126])
    5. 更新后的权值w: tensor([0.5051])
    6. 更新后的权值w: tensor([0.5924])
    7. 更新后的权值w: tensor([0.0654])
    8. 更新后的权值w: tensor([-0.5839])
    9. 更新后的权值w: tensor([-0.3305])
    10. 更新后的权值w: tensor([0.7262])
    1. 11
    2. 损失函数(均方误差): 0.10375461727380753
    3. -0.02 -0.0 -0.01 -0.0 0.02 0.06 0.02 0.05 更新后的权值w: tensor([0.3425])
    4. 更新后的权值w: tensor([-0.4540])
    5. 更新后的权值w: tensor([0.5855])
    6. 更新后的权值w: tensor([0.5676])
    7. 更新后的权值w: tensor([-0.2230])
    8. 更新后的权值w: tensor([-1.2686])
    9. 更新后的权值w: tensor([-0.5765])
    10. 更新后的权值w: tensor([0.1418])
    1. 21
    2. 损失函数(均方误差): 0.05818723142147064
    3. -0.01 -0.0 -0.01 -0.0 0.02 0.04 0.01 0.03 更新后的权值w: tensor([0.4856])
    4. 更新后的权值w: tensor([-0.4246])
    5. 更新后的权值w: tensor([0.6714])
    6. 更新后的权值w: tensor([0.5853])
    7. 更新后的权值w: tensor([-0.4255])
    8. 更新后的权值w: tensor([-1.7091])
    9. 更新后的权值w: tensor([-0.7422])
    10. 更新后的权值w: tensor([-0.2187])

    ReLu:
     

    1. 1
    2. 损失函数(均方误差): 0.023462500423192978
    3. -0.01 0.0 -0.01 0.0 -0.05 0.0 -0.0 0.0 更新后的权值w: tensor([0.2103])
    4. 更新后的权值w: tensor([-0.4000])
    5. 更新后的权值w: tensor([0.5062])
    6. 更新后的权值w: tensor([0.6000])
    7. 更新后的权值w: tensor([0.1513])
    8. 更新后的权值w: tensor([-0.5000])
    9. 更新后的权值w: tensor([-0.3000])
    10. 更新后的权值w: tensor([0.8000])
    1. 11
    2. 损失函数(均方误差): 0.0036630069371312857
    3. -0.01 0.0 -0.01 0.0 -0.02 0.0 0.0 0.0 更新后的权值w: tensor([0.3877])
    4. 更新后的权值w: tensor([-0.4000])
    5. 更新后的权值w: tensor([0.6126])
    6. 更新后的权值w: tensor([0.6000])
    7. 更新后的权值w: tensor([0.5074])
    8. 更新后的权值w: tensor([-0.5000])
    9. 更新后的权值w: tensor([-0.3000])
    10. 更新后的权值w: tensor([0.8000])
    1. 21
    2. 损失函数(均方误差): 0.0024535225238651037
    3. -0.0 0.0 -0.0 0.0 -0.0 0.0 0.0 0.0 更新后的权值w: tensor([0.4266])
    4. 更新后的权值w: tensor([-0.4000])
    5. 更新后的权值w: tensor([0.6360])
    6. 更新后的权值w: tensor([0.6000])
    7. 更新后的权值w: tensor([0.5644])
    8. 更新后的权值w: tensor([-0.5000])
    9. 更新后的权值w: tensor([-0.3000])
    10. 更新后的权值w: tensor([0.8000])

    对比两组结果,可以发现ReLu的计算效率比sigmoid效率高【均方误差下降的快】,他的收敛速度也比较快。

    同时搜集资料得知:当输入值为负时,ReLU输出为0,激活函数非常简单和稀疏。相比之下,Sigmoid函数在所有输入值上都有一个非零输出。这种稀疏性可以减少神经网络中的冗余和过拟合现象。

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

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

    1. import torch
    2. def sigmoid(x):
    3. return 1 / (1 + torch.exp(-x))
    4. def forward(x1,x2):
    5. in_h1=x1*w[0]+x2*w[2]
    6. in_h2=x2*w[3]+x1*w[1]
    7. out_h1=sigmoid(in_h1)
    8. out_h2=sigmoid(in_h2)
    9. in_o1=out_h1*w[4]+out_h2*w[6]
    10. in_o2=out_h1*w[5]+out_h2*w[7]
    11. out_o1=sigmoid(in_o1)
    12. out_o2=sigmoid(in_o2)
    13. return out_o1, out_o2
    14. def update(n, w, w_dao):
    15. w_updated = []
    16. for i in range(8):
    17. w_updated.append(w[i] - n * w_dao[i])
    18. return w_updated
    19. def Error(x1, x2, y1, y2):
    20. y_pre= forward(x1, x2)
    21. mse = torch.nn.MSELoss()
    22. loss_mse =mse(y_pre[0],y1) + mse(y_pre[1],y2)
    23. return loss_mse
    24. # 定义x1, x2, y1, y2和初始权重w
    25. x1 = torch.tensor([0.5])
    26. x2 = torch.tensor([0.3])
    27. y1 = torch.tensor([0.23])
    28. y2 = torch.tensor([-0.07])
    29. 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])]
    30. n = 1 # 步长
    31. for i in range(0, 8):
    32. w[i].requires_grad = True
    33. for j in range(1001):
    34. L = Error(x1,x2,y1,y2) # 前向传播
    35. L.backward() # 反向传播,自动求梯度。
    36. if j%10==0:
    37. print("\n第",j+1,"轮")
    38. print("损失函数(均方误差):", L.item())
    39. for i in range(0, 8):
    40. if j%10==0:
    41. print(round(w[i].grad.item(), 2), end=" ")
    42. for i in range(0, 8):
    43. w[i].data = w[i].data-n * w[i].grad.data # 更新权值
    44. w[i].grad.data.zero_() # 注意:将w中所有梯度清零
    45. if j%10==0:
    46. print("更新后的权值w:",w[i].data)

    对比手写与torch自带函数:

    手写MSE:

    1. 1
    2. 损失函数(均方误差): 0.2097097933292389
    3. -0.01 0.01 -0.01 0.01 0.03 0.08 0.03 0.07 更新后的权值w: tensor([0.2084])
    4. 更新后的权值w: tensor([-0.4126])
    5. 更新后的权值w: tensor([0.5051])
    6. 更新后的权值w: tensor([0.5924])
    7. 更新后的权值w: tensor([0.0654])
    8. 更新后的权值w: tensor([-0.5839])
    9. 更新后的权值w: tensor([-0.3305])
    10. 更新后的权值w: tensor([0.7262])
    1. 51
    2. 损失函数(均方误差): 0.02064337022602558
    3. -0.01 -0.0 -0.0 -0.0 0.01 0.01 0.0 0.01 更新后的权值w: tensor([0.7672])
    4. 更新后的权值w: tensor([-0.3102])
    5. 更新后的权值w: tensor([0.8403])
    6. 更新后的权值w: tensor([0.6539])
    7. 更新后的权值w: tensor([-0.7309])
    8. 更新后的权值w: tensor([-2.3755])
    9. 更新后的权值w: tensor([-0.9843])
    10. 更新后的权值w: tensor([-0.7468])
    1. 101
    2. 损失函数(均方误差): 0.010253453627228737
    3. -0.0 -0.0 -0.0 -0.0 0.0 0.01 0.0 0.01 更新后的权值w: tensor([0.9896])
    4. 更新后的权值w: tensor([-0.2022])
    5. 更新后的权值w: tensor([0.9738])
    6. 更新后的权值w: tensor([0.7187])
    7. 更新后的权值w: tensor([-0.8637])
    8. 更新后的权值w: tensor([-2.8525])
    9. 更新后的权值w: tensor([-1.0873])
    10. 更新后的权值w: tensor([-1.1163])

    t.nn.MSELoss():

    1. 1
    2. 损失函数(均方误差): 0.4194195866584778
    3. -0.02 0.03 -0.01 0.02 0.07 0.17 0.06 0.15 更新后的权值w: tensor([0.2168])
    4. 更新后的权值w: tensor([-0.4252])
    5. 更新后的权值w: tensor([0.5101])
    6. 更新后的权值w: tensor([0.5849])
    7. 更新后的权值w: tensor([0.0307])
    8. 更新后的权值w: tensor([-0.6677])
    9. 更新后的权值w: tensor([-0.3610])
    10. 更新后的权值w: tensor([0.6523])
    1. 51
    2. 损失函数(均方误差): 0.020327573642134666
    3. -0.01 -0.0 -0.0 -0.0 0.0 0.01 0.0 0.01 更新后的权值w: tensor([0.9895])
    4. 更新后的权值w: tensor([-0.2069])
    5. 更新后的权值w: tensor([0.9737])
    6. 更新后的权值w: tensor([0.7159])
    7. 更新后的权值w: tensor([-0.8678])
    8. 更新后的权值w: tensor([-2.8686])
    9. 更新后的权值w: tensor([-1.0913])
    10. 更新后的权值w: tensor([-1.1305])
    1. 101
    2. 损失函数(均方误差): 0.012286863289773464
    3. -0.0 -0.0 -0.0 -0.0 -0.0 0.01 -0.0 0.0 更新后的权值w: tensor([1.1914])
    4. 更新后的权值w: tensor([-0.1058])
    5. 更新后的权值w: tensor([1.0948])
    6. 更新后的权值w: tensor([0.7765])
    7. 更新后的权值w: tensor([-0.8710])
    8. 更新后的权值w: tensor([-3.3064])
    9. 更新后的权值w: tensor([-1.0937])
    10. 更新后的权值w: tensor([-1.4652])

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

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

    交叉熵函数代码【其他地方不变】:

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

    得到结果:

    1. 1
    2. 损失函数(均方误差): 0.11871970444917679
    3. -0.0 0.01 -0.0 0.0 -0.02 0.02 -0.02 0.02 更新后的权值w: tensor([0.2028])
    4. 更新后的权值w: tensor([-0.4052])
    5. 更新后的权值w: tensor([0.5017])
    6. 更新后的权值w: tensor([0.5969])
    7. 更新后的权值w: tensor([0.1213])
    8. 更新后的权值w: tensor([-0.5213])
    9. 更新后的权值w: tensor([-0.2812])
    10. 更新后的权值w: tensor([0.7812])
    1. 51
    2. 损失函数(均方误差): 0.05054658651351929
    3. -0.01 -0.0 -0.0 -0.0 -0.02 0.02 -0.01 0.01 更新后的权值w: tensor([0.5215])
    4. 更新后的权值w: tensor([-0.4611])
    5. 更新后的权值w: tensor([0.6929])
    6. 更新后的权值w: tensor([0.5633])
    7. 更新后的权值w: tensor([1.0931])
    8. 更新后的权值w: tensor([-1.4937])
    9. 更新后的权值w: tensor([0.5264])
    10. 更新后的权值w: tensor([-0.0268])
    1. 101
    2. 损失函数(均方误差): 0.015986934304237366
    3. -0.01 -0.0 -0.0 -0.0 -0.01 0.01 -0.01 0.01 更新后的权值w: tensor([0.9094])
    4. 更新后的权值w: tensor([-0.3223])
    5. 更新后的权值w: tensor([0.9257])
    6. 更新后的权值w: tensor([0.6466])
    7. 更新后的权值w: tensor([1.7698])
    8. 更新后的权值w: tensor([-2.1666])
    9. 更新后的权值w: tensor([1.0463])
    10. 更新后的权值w: tensor([-0.5439])

    可以发现,与自带MSE相比,交叉熵损失函数在第100轮次的梯度下降较慢。

    可以知道交叉熵的梯度下降速率没有MSE快。同时在本次实验中,我们关注的是预测值与真实值之间的差异,想要降低这差值。而交叉熵关注的是概率的差异更敏感,它更加关注样本的分类错误程度。

    在以前的学习过程中,知道MSE适用于回归任务,交叉熵适用于分类任务。

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

    在前边代码中改变训练次数,可以看到,训练次数越多,梯度越接近于0,损失值越小,神经网络效果越好。

    改变步长:
    n=1:

    1. 1
    2. 损失函数(均方误差): 0.2097097933292389
    3. -0.01 0.01 -0.01 0.01 0.03 0.08 0.03 0.07 更新后的权值w: tensor([0.2084])
    4. 更新后的权值w: tensor([-0.4126])
    5. 更新后的权值w: tensor([0.5051])
    6. 更新后的权值w: tensor([0.5924])
    7. 更新后的权值w: tensor([0.0654])
    8. 更新后的权值w: tensor([-0.5839])
    9. 更新后的权值w: tensor([-0.3305])
    10. 更新后的权值w: tensor([0.7262])
    11. 51
    12. 损失函数(均方误差): 0.02064337022602558
    13. -0.01 -0.0 -0.0 -0.0 0.01 0.01 0.0 0.01 更新后的权值w: tensor([0.7672])
    14. 更新后的权值w: tensor([-0.3102])
    15. 更新后的权值w: tensor([0.8403])
    16. 更新后的权值w: tensor([0.6539])
    17. 更新后的权值w: tensor([-0.7309])
    18. 更新后的权值w: tensor([-2.3755])
    19. 更新后的权值w: tensor([-0.9843])
    20. 更新后的权值w: tensor([-0.7468])
    21. 101
    22. 损失函数(均方误差): 0.010253453627228737
    23. -0.0 -0.0 -0.0 -0.0 0.0 0.01 0.0 0.01 更新后的权值w: tensor([0.9896])
    24. 更新后的权值w: tensor([-0.2022])
    25. 更新后的权值w: tensor([0.9738])
    26. 更新后的权值w: tensor([0.7187])
    27. 更新后的权值w: tensor([-0.8637])
    28. 更新后的权值w: tensor([-2.8525])
    29. 更新后的权值w: tensor([-1.0873])
    30. 更新后的权值w: tensor([-1.1163])

    n=0.1:

    1. 1
    2. 损失函数(均方误差): 0.2097097933292389
    3. -0.01 0.01 -0.01 0.01 0.03 0.08 0.03 0.07 更新后的权值w: tensor([0.2008])
    4. 更新后的权值w: tensor([-0.4013])
    5. 更新后的权值w: tensor([0.5005])
    6. 更新后的权值w: tensor([0.5992])
    7. 更新后的权值w: tensor([0.0965])
    8. 更新后的权值w: tensor([-0.5084])
    9. 更新后的权值w: tensor([-0.3030])
    10. 更新后的权值w: tensor([0.7926])
    11. 51
    12. 损失函数(均方误差): 0.1468939185142517
    13. -0.01 0.0 -0.01 0.0 0.03 0.07 0.03 0.06 更新后的权值w: tensor([0.2584])
    14. 更新后的权值w: tensor([-0.4417])
    15. 更新后的权值w: tensor([0.5350])
    16. 更新后的权值w: tensor([0.5750])
    17. 更新后的权值w: tensor([-0.0627])
    18. 更新后的权值w: tensor([-0.8937])
    19. 更新后的权值w: tensor([-0.4411])
    20. 更新后的权值w: tensor([0.4587])
    21. 101
    22. 损失函数(均方误差): 0.10485432296991348
    23. -0.02 -0.0 -0.01 -0.0 0.02 0.06 0.02 0.05 更新后的权值w: tensor([0.3317])
    24. 更新后的权值w: tensor([-0.4496])
    25. 更新后的权值w: tensor([0.5790])
    26. 更新后的权值w: tensor([0.5702])
    27. 更新后的权值w: tensor([-0.1973])
    28. 更新后的权值w: tensor([-1.2086])
    29. 更新后的权值w: tensor([-0.5545])
    30. 更新后的权值w: tensor([0.1932])

    n=10:

    1. 1
    2. 损失函数(均方误差): 0.2097097933292389
    3. -0.01 0.01 -0.01 0.01 0.03 0.08 0.03 0.07 更新后的权值w: tensor([0.2842])
    4. 更新后的权值w: tensor([-0.5261])
    5. 更新后的权值w: tensor([0.5505])
    6. 更新后的权值w: tensor([0.5244])
    7. 更新后的权值w: tensor([-0.2463])
    8. 更新后的权值w: tensor([-1.3387])
    9. 更新后的权值w: tensor([-0.6049])
    10. 更新后的权值w: tensor([0.0616])
    11. 51
    12. 损失函数(均方误差): 0.003888471983373165
    13. -0.0 -0.0 -0.0 -0.0 -0.0 0.0 -0.0 0.0 更新后的权值w: tensor([1.4146])
    14. 更新后的权值w: tensor([-0.0209])
    15. 更新后的权值w: tensor([1.2287])
    16. 更新后的权值w: tensor([0.8275])
    17. 更新后的权值w: tensor([-0.8256])
    18. 更新后的权值w: tensor([-3.9177])
    19. 更新后的权值w: tensor([-1.0687])
    20. 更新后的权值w: tensor([-1.9406])
    21. 101
    22. 损失函数(均方误差): 0.003177047474309802
    23. -0.0 -0.0 -0.0 -0.0 -0.0 0.0 -0.0 0.0 更新后的权值w: tensor([1.6124])
    24. 更新后的权值w: tensor([0.1093])
    25. 更新后的权值w: tensor([1.3474])
    26. 更新后的权值w: tensor([0.9056])
    27. 更新后的权值w: tensor([-0.7873])
    28. 更新后的权值w: tensor([-4.3226])
    29. 更新后的权值w: tensor([-1.0398])
    30. 更新后的权值w: tensor([-2.2449])

    我将n=1,n=0.1,n=10分别对比输出结果与点图可以看到:当n=0.1时梯度下降速度、收敛速度太慢,但是n=10时在15轮次左右的时候梯度都已经下降到0,速度太快,不便于观察。

    由此可知:将步长适当调大可以提高梯度下降速度。

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

     使用以下代码初始化:

    1. w = [torch.randn(1) for _ in range(8)]
    2. print("随机初始化的 w:", w)

    其中随机化后的w为:

    随机初始化的 w: [tensor([0.5343]), tensor([-0.0909]), tensor([0.0911]), tensor([0.6306]), tensor([1.5516]), tensor([0.0103]), tensor([-0.0712]), tensor([-0.1518])]

    得到的结果为:

    1. 0.02 -0.0 0.01 -0.0 0.06 0.08 0.05 0.07
    2. 1
    3. 损失函数(均方误差): 0.2627074718475342
    4. 更新后的权值w: tensor([0.5154])
    5. 更新后的权值w: tensor([-0.0874])
    6. 更新后的权值w: tensor([0.0797])
    7. 更新后的权值w: tensor([0.6327])
    8. 更新后的权值w: tensor([1.4951])
    9. 更新后的权值w: tensor([-0.0686])
    10. 更新后的权值w: tensor([-0.1241])
    11. 更新后的权值w: tensor([-0.2255])
    12. -0.0 -0.01 -0.0 -0.0 0.01 0.01 0.01 0.01
    13. 51
    14. 损失函数(均方误差): 0.020640043541789055
    15. 更新后的权值w: tensor([0.5296])
    16. 更新后的权值w: tensor([0.4536])
    17. 更新后的权值w: tensor([0.0882])
    18. 更新后的权值w: tensor([0.9573])
    19. 更新后的权值w: tensor([0.1301])
    20. 更新后的权值w: tensor([-1.5691])
    21. 更新后的权值w: tensor([-1.5129])
    22. 更新后的权值w: tensor([-1.7508])
    23. -0.0 -0.0 -0.0 -0.0 0.0 0.01 0.0 0.01
    24. 101
    25. 损失函数(均方误差): 0.00984925590455532
    26. 更新后的权值w: tensor([0.6697])
    27. 更新后的权值w: tensor([0.6586])
    28. 更新后的权值w: tensor([0.1723])
    29. 更新后的权值w: tensor([1.0803])
    30. 更新后的权值w: tensor([-0.0452])
    31. 更新后的权值w: tensor([-1.9563])
    32. 更新后的权值w: tensor([-1.7057])
    33. 更新后的权值w: tensor([-2.1770])

    可以看到,初始权值对对权值w收敛影响不大,只会影响收敛的速度。

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

    w = [torch.zeros(1) for i in range(8)]

    得到结果为:

    1. 0.0 0.0 0.0 0.0 0.03 0.07 0.03 0.07
    2. 1
    3. 损失函数(均方误差): 0.1988999992609024
    4. 更新后的权值w: tensor([0.])
    5. 更新后的权值w: tensor([0.])
    6. 更新后的权值w: tensor([0.])
    7. 更新后的权值w: tensor([0.])
    8. 更新后的权值w: tensor([-0.0337])
    9. 更新后的权值w: tensor([-0.0712])
    10. 更新后的权值w: tensor([-0.0337])
    11. 更新后的权值w: tensor([-0.0712])
    12. -0.01 -0.01 -0.0 -0.0 0.01 0.01 0.01 0.01
    13. 51
    14. 损失函数(均方误差): 0.022186635062098503
    15. 更新后的权值w: tensor([0.3932])
    16. 更新后的权值w: tensor([0.3932])
    17. 更新后的权值w: tensor([0.2359])
    18. 更新后的权值w: tensor([0.2359])
    19. 更新后的权值w: tensor([-0.8327])
    20. 更新后的权值w: tensor([-1.6622])
    21. 更新后的权值w: tensor([-0.8327])
    22. 更新后的权值w: tensor([-1.6622])
    23. -0.0 -0.0 -0.0 -0.0 0.0 0.01 0.0 0.01
    24. 101
    25. 损失函数(均方误差): 0.010592692531645298
    26. 更新后的权值w: tensor([0.5907])
    27. 更新后的权值w: tensor([0.5907])
    28. 更新后的权值w: tensor([0.3544])
    29. 更新后的权值w: tensor([0.3544])
    30. 更新后的权值w: tensor([-0.9696])
    31. 更新后的权值w: tensor([-2.1009])
    32. 更新后的权值w: tensor([-0.9696])
    33. 更新后的权值w: tensor([-2.1009])

    可以发现,类似w权值随机化,对神经网络的收敛结果没影响,只影响收敛速度。

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

    1)在写numpy代码实现时,很麻烦,因为每一个函数都要自己手写实现;这时候也比较理解为什么做神经网络要使用pytorch了,因为torch里边有现成的函数,比较方便。

    2)在手写求导的过程中,感觉不是很难,主要是要理清每个权值求解导数的公式。

    3)numpy转为tensor过程中遇到了很多问题,比如转换,前边提到了:

    发现pytorch对tensor的格式要求很严【一直报错】,还有在用ReLu时,看到学长【NNDL 作业3:分别使用numpy和pytorch实现FNN例题_。没有用n,nly,kkn3_笼子里的薛定谔的博客-CSDN博客】遇到的问题(我没想到那种错误写法,不过也长知识了):错误写法如下:

    正确的是:

    错误原因是因为y1_pred和y1本身是tensor类型的,不用再使用torch.tensor()。

    其实我还是不太理解,tensor类型的数据再使用torch.tensor转出的不是tensor类型吗?为什么会报错呢?

    3)在问题解答中,感觉学习到了很多:在以后的神经网络学习中,感到激活函数效果不好时,考虑更换别的激活函数;还有损失函数。

    4)在多次改变权值初始值后 可以发现初始权值对对权值w收敛影响不大,只会影响收敛的速度。

    5)适当的调整步长可以加快收敛速度,但是不能过大,会使得变化过大不方便观察,也可能结果不再收敛。

  • 相关阅读:
    套接字选项
    逆向案例二:关键字密文解密,自定义的加密解密。基于企名片科技的爬取。
    Celery笔记二之celery项目建立、配置及加载方式介绍
    Java -数字金字塔-代码
    Verilog 过程赋值(阻塞赋值,非阻塞赋值,并行)
    前端开发报错:Cannot find module ‘@angular-devkit/schematics‘ Require stack:
    The-MIFARE-Hack-1 -mifare技术
    TiFlash 常见问题
    乐优商城_第3章_-认识微服务(Feign+Zuul)
    微控制器通信1 -基础(微控制器与模块化设计)
  • 原文地址:https://blog.csdn.net/weixin_61838030/article/details/133827550