【一起学NLP】Chapter2-学习神经网络

news2024/11/15 17:27:44

目录

  • 学习神经网络
      • 损失函数
      • Tip:One-hot向量
      • 导数与梯度
      • Tip:严格地说
      • 链式法则
      • 计算图
      • 反向传播
      • 其他典型的运算结点
        • 乘法结点
        • 分支节点
        • Repeat节点
        • Sum节点
        • MatMul节点
      • Tip:浅拷贝和深拷贝的差异
      • 梯度的推导和反向传播的实现
        • Sigmoid层
        • Affine层
        • Softmax with Loss层
      • 权重的更新——随机梯度下降算法

学习神经网络

不进行神经网络的学习,就做不到“好的推理”​。所谓推理,就是对上一节介绍的多类别分类等问题给出回答的任务。而神经网络的学习的任务是寻找最优参数。不进行神经网络的学习,就做不到“好的推理”​。因此,常规的流程是,首先进行学习,然后再利用学习好的参数进行推理。

损失是衡量神经网络学习好坏的一种指标,是指示学习阶段中某个时间点的神经网络的性能。基于监督数据(学习阶段获得的正确解数据)和神经网络的预测结果,将模型的恶劣程度作为标量(单一数值)计算出来,得到的就是损失。

损失函数

损失函数是计算神经网络的一种方法。损失函数有多种,当进行多类别分类的神经网络通常使用交叉熵误差(cross entropy error)作为损失函数。此时,交叉熵误差由神经网络输出的各类别的概率和监督标签求得。

根据之前介绍过的神经网络,从结果的三输出来看,可以视作为一种多分类神经网络。所以在原有的层基础上我们再加上交叉熵误差作为损失函数。
在这里插入图片描述

在上图中,X是输入数据,t是监督标签,L是损失。此时,Softmax层的输出是概率,该概率和监督标签被输入Cross Entropy Error层。

SoftMax函数:

y k = e s k ∑ j = 1 n e s i y_k = \frac{e^{s_k}}{\sum_{j=1}^{n} e^{s_i}} yk=j=1nesiesk

当输出总共有n个时,计算第k个输出yk时的算式。这个yk是对应于第k个类别的Softmax函数的输出。如上式所示,Softmax函数的分子是得分sk的指数函数,分母是所有输入信号的指数函数的和。Softmax函数输出的各个元素是0.0~1.0的实数。另外,如果将这些元素全部加起来,则和为1。因此,Softmax的输出可以解释为概率。之后,这个概率会被输入交叉熵误差。此时,

交叉熵误差可由下式表示:

L = − ∑ i = 1 n t k log ⁡ ( y ^ k ) L = - \sum_{i=1}^{n} t_k \log(\hat{y}_k) L=i=1ntklog(y^k)

这里,tk是对应于第k个类别的监督标签。log是以纳皮尔数e为底的对数(严格地说,应该记为loge)​。监督标签以one-hot向量的形式表示,比如t=(0, 0,1)​。

Tip:One-hot向量

one-hot向量是一个元素为1,其他元素为0的向量。因为元素1对应正确解的类,所以式(1.7)实际上只是在计算正确解标签为1的元素所对应的输出的自然对数(log)​。

在考虑了mini-batch处理的情况下,交叉熵误差可以由下式表示:

L = − 1 N ∑ n ∑ k t n k log ⁡ ( y ^ n k ) L = - \frac{1}{N} \sum_{n} \sum_{k} t_{nk} \log(\hat{y}_{nk}) L=N1nktnklog(y^nk)

这里假设数据有N笔,tnk表示第n笔数据的第k维元素的值,ynk表示神经网络的输出,tnk表示监督标签。式(1.8)看上去有些复杂,其实只是将表示单笔数据的损失函数的公式扩展到了N笔数据的情况。用上式除以N,可以求单笔数据的平均损失。通过这样的平均化,无论mini-batch的大小如何,都始终可以获得一致的指标。

为了使学习变得简单愉快,下面我们将计算Softmax函数和交叉熵误差的层实现为Softmax with Loss层(通过整合这两个层,反向传播的计算会变简单)​。
在这里插入图片描述

导数与梯度

神经网络的学习目标是找到尽可能小的参数。而想找到这些参数,导数和梯度非常重要。

导数定义:导数是数学中微积分的一个重要概念,描述了一个函数在某一点的斜率或者一个函数的变化率。

假设有一个函数$ y=f(x) , 此时, ,此时, ,此时,y 关于 关于 关于x 的导数记为 的导数记为 的导数记为 \frac{dy}{dx} $,这个式子的含义是随着x的微小变化y的变化程度。
在这里插入图片描述

比如, y = x 2 y = x^2 y=x2,其导数为 d y d x = 2 x \frac{dy}{dx} = 2x dxdy=2x,这个导数结果表示x在各处的变化程度。实际上就相当于函数的斜率。
在这里插入图片描述

在上图中,我们求了关于x这一个变量的导数,其实同样可以求关于多个变量(多变量)的导数。假设有函数$ L=f(x) ​,其中 L 是标量, x 是向量。此时, L 关于 ​,其中L是标量,x是向量。此时,L关于 ,其中L是标量,x是向量。此时,L关于x_i ( x 的第 i 个元素)的导数可以写成 (x的第i个元素)的导数可以写成 x的第i个元素)的导数可以写成\frac{\partial L}{\partial {x_i}}$。另外,也可以求关于向量的其他元素的导数,我们将其整理如下:
∂ L ∂ x = ( ∂ L ∂ x 1 , ∂ L ∂ x 2 , . . . . , ∂ L ∂ x n ) \frac{\partial L}{\partial x} = (\frac{\partial L}{\partial x_1},\frac{\partial L}{\partial x_2},....,\frac{\partial L}{\partial x_n}) xL=(x1L,x2L,....,xnL)

像这样,将关于向量各个元素的导数罗列到一起,就得到了梯度(gradient)​。

梯度的定义:梯度一词有时用于斜度,也就是一个曲面沿着给定方向的倾斜程度。 可以通过取向量梯度和所研究的方向的点积来得到斜度。 梯度的数值有时也被称为梯度。 梯度的本意是一个向量(矢量),表示某一函数在该点处的方向导数沿着该方向取得最大值,即函数在该点处沿着该方向(此梯度的方向)变化最快,变化率最大(为该梯度的模)。
在这里插入图片描述

另外,矩阵也可以像向量一样求梯度。假设W是一个m×n的矩阵,则函数L=g(W)的梯度如下所示:
在这里插入图片描述

如上式所示,L关于W的梯度可以写成矩阵(准确地说,矩阵的梯度的定义如上所示)​。这里的重点是,W和 ∂ L ∂ W \frac{\partial L}{\partial W} WL具有相同的形状。利用“矩阵和其梯度具有相同形状”这一性质,可以轻松地进行参数的更新和链式法则(后述)的实现。

Tip:严格地说

本书使用的“梯度”一词与数学中的“梯度”是不同的。数学中的梯度仅限于关于向量的导数。而在深度学习领域,一般也会定义关于矩阵和张量的导数,称为“梯度”​。

链式法则

实际上,神经网络除了第1层和第n层的每一层(n-1层)都有前接后继的关系,其中隐藏层中的每个神经元都含有一个激活函数,输入经过全连接层的仿射关系($ H = WX+B )后 , 在进入隐藏层神经元的激活函数中得到 )后,在进入隐藏层神经元的激活函数中得到 )后,在进入隐藏层神经元的激活函数中得到a(H) , 再经过后面的 ,再经过后面的 ,再经过后面的Sigmoid 函数进行非线性拟合,在得到一个新的函数 函数进行非线性拟合,在得到一个新的函数 函数进行非线性拟合,在得到一个新的函数Sigmoid(a(H))$,以此类推,再仿射,再激活,再非线性拟合……这个过程实际上就是一个不断地函数复合的过程。
在这里插入图片描述

(分享一个好玩的神经网络可视化网站:)[https://playground.tensorflow.org/#activation=tanh&batchSize=10&dataset=circle&regDataset=reg-plane&learningRate=0.03&regularizationRate=0&noise=0&networkShape=4,2&seed=0.40401&showTestData=false&discretize=false&percTrainData=50&x=true&y=true&xTimesY=false&xSquared=false&ySquared=false&cosX=false&sinX=false&cosY=false&sinY=false&collectStats=false&problem=classification&initZero=false&hideText=false]

在这里插入图片描述

学习阶段的神经网络在给定学习数据后会输出损失。这里我们想得到的是损失关于各个参数的梯度。只要得到了它们的梯度,就可以使用这些梯度进行参数更新。那么,神经网络的梯度怎么求呢?这就轮到误差反向传播法出场了。

理解误差反向传播法的关键是链式法则。链式法则是复合函数的求导法则,其中复合函数是由多个函数构成的函数。

现在,我们来学习链式法则。这里考虑y=f(x)和z=g(y)这两个函数。如z=g(f(x)​)所示,最终的输出z由两个函数计算而来。此时,z关于x的导数可以按下式求得:

∂ z ∂ x = ∂ z ∂ y ⋅ ∂ y ∂ x \frac{\partial z}{\partial x} = \frac{\partial z}{\partial y}·\frac{\partial y}{\partial x} xz=yzxy

如式所示,z关于x的导数由y=f(x)的导数和z=g(y)的导数之积求得,这就是链式法则。链式法则的重要之处在于,无论我们要处理的函数有多复杂(无论复合了多少个函数)​,都可以根据它们各自的导数来求复合函数的导数。也就是说,只要能够计算各个函数的局部的导数,就能基于它们的积计算最终的整体的导数。

计算图

使用计算图能更加直观的展示计算过程。
在这里插入图片描述

计算图通过节点和箭头来表示。这里,​“+”表示加法,变量x和y写在各自的箭头上。像这样,在计算图中,用节点表示计算,处理结果有序(本例中是从左到右)流动。这就是计算图的正向传播。

使用计算图,可以直观地把握计算过程。另外,这样也可以直观地求梯度。这里重要的是,梯度沿与正向传播相反的方向传播,这个反方向的传播称为反向传播。

反向传播

虽然我们处理的是z=x+y这一计算,但是在该计算的前后,还存在其他的“某种计算”​(如下图)​。另外,假设最终输出的是标量L(在神经网络的学习阶段,计算图的最终输出是损失,它是一个标量)​。
在这里插入图片描述

我们的目标是求L关于各个变量的导数(梯度)​。这样一来,计算图的反向传播就可以绘制成下图。
在这里插入图片描述

如图所示,反向传播用蓝色的粗箭头表示,在箭头的下方标注传播的值。此时,传播的值是指最终的输出L关于各个变量的导数。在这个例子中,关于z的导数是$ \frac{\partial L}{\partial z} ,关于 x 和 y 的导数分别是 ,关于x和y的导数分别是 ,关于xy的导数分别是 \frac{\partial L}{\partial x} 和 和 \frac{\partial L}{\partial y} $。

根据刚才复习的链式法则,反向传播中流动的导数的值是根据从上游(输出侧)传来的导数和各个运算节点的局部导数之积求得的。因此,在上面的例子中,$ \frac{\partial L}{\partial x} = \frac{\partial L}{\partial z}·\frac{\partial z}{\partial x} , , , \frac{\partial L}{\partial y} = \frac{\partial L}{\partial z}·\frac{\partial z}{\partial y}$

这里,我们来处理z=x+y这个基于加法节点的运算。此时,分别解析性地求得$ \frac{\partial z}{\partial x} = 1 , , , \frac{\partial z}{\partial y}=1 $。因此,如图1-18所示,加法节点将上游传来的值乘以1,再将该梯度向下游传播。也就是说,只是原样地将从上游传来的梯度传播出去。
在这里插入图片描述

图1-18 加法节点的正向传播(左图)和反向传播(右图)

其他典型的运算结点

乘法结点

乘法节点是z=x×y这样的计算。此时,导数可以分别求出,即 ∂ z ∂ x = y \frac{\partial z}{\partial x} = y xz=y ∂ z ∂ y = x \frac{\partial z}{\partial y} = x yz=x。因此,如图1-19所示,乘法节点的反向传播会将“上游传来的梯度”乘以“将正向传播时的输入替换后的值”​。
在这里插入图片描述

图1-19 乘法节点的正向传播(左图)和反向传播(右图)

分支节点

如图1-20所示,分支节点是有分支的节点。
在这里插入图片描述

图1-20 分支节点的正向传播(左图)和反向传播(右图)

严格来说,分支节点并没有节点,只有两根分开的线。此时,相同的值被复制并分叉。因此,分支节点也称为复制节点。如图1-20所示,它的反向传播是上游传来的梯度之和。

Repeat节点

分支节点有两个分支,但也可以扩展为N个分支(副本)​,这里称为Repeat节点。现在,我们尝试用计算图绘制一个Repeat节点(图1-21)​。
在这里插入图片描述

图1-21 Repeat节点的正向传播(上图)和反向传播(下图)

如图1-21所示,这个例子中将长度为D的数组复制了N份。因为这个Repeat节点可以视为N个分支节点,所以它的反向传播可以通过N个梯度的总和求出,如下所示。

import numpy as np

D,N = 8,7 # D-特征数量(数据点维度),N-数据点数量
# 输入
x = np.random.randn(1,D) # x(1,8)
# 正向传播
y = np.repeat(x,N,axis=0) # y(7,8),将x沿着行方向重复N次
# 假设的梯度
dy = np.random.randn(N,D) # dy(7,8),每一行都代表了与数据点相关的梯度或扰动。
# 反向传播
dx = np.sum(dy,axis=0,keepdims=True) # 对 dy 的所有行(axis=0)进行求和,得到一个形状为 (1, D) 的数组(1x8)。
# keepdims=True 的作用是保持结果的二维结构,即结果仍然是 (1, D),而不是将其压缩成一维 (D,)。
print(x,y,dy,dx)
[[-0.65398062  0.17582954  1.40937608 -0.96070201 -1.26623082  0.73848146
  -1.46208389 -0.929922  ]] [[-0.65398062  0.17582954  1.40937608 -0.96070201 -1.26623082  0.73848146
  -1.46208389 -0.929922  ]
 [-0.65398062  0.17582954  1.40937608 -0.96070201 -1.26623082  0.73848146
  -1.46208389 -0.929922  ]
 [-0.65398062  0.17582954  1.40937608 -0.96070201 -1.26623082  0.73848146
  -1.46208389 -0.929922  ]
 [-0.65398062  0.17582954  1.40937608 -0.96070201 -1.26623082  0.73848146
  -1.46208389 -0.929922  ]
 [-0.65398062  0.17582954  1.40937608 -0.96070201 -1.26623082  0.73848146
  -1.46208389 -0.929922  ]
 [-0.65398062  0.17582954  1.40937608 -0.96070201 -1.26623082  0.73848146
  -1.46208389 -0.929922  ]
 [-0.65398062  0.17582954  1.40937608 -0.96070201 -1.26623082  0.73848146
  -1.46208389 -0.929922  ]] [[-0.90802981  1.29492835 -0.08913975 -1.05449559  1.52160534  1.66412136
   1.28437637  0.10368702]
 [ 0.71989271 -1.0449534   0.50564523 -1.28796732 -1.16791928 -2.67012599
  -0.36123667 -0.32709917]
 [-0.25298264  1.47424981  0.03609116  0.85514997  0.81946986 -0.17173151
   0.26644988  1.52564421]
 [-2.20924914  0.3320471  -1.11236776 -0.23800763  0.8486034   0.18278592
   0.79370296  0.42604111]
 [-0.59119096  0.14343845  1.62747892  0.64882914  0.09213585  0.51629164
   0.28035387 -0.39534521]
 [ 0.30125875  0.46648204 -0.9340739  -1.10590717 -1.25901507 -1.15055799
  -0.95753432  1.40269176]
 [-0.05479657  0.67668708  1.30038377 -0.23583369 -2.00146352 -0.9221725
  -0.552145   -0.5606353 ]] [[-2.99509766  3.34287943  1.33401767 -2.41823229 -1.14658342 -2.55138908
   0.7539671   2.17498441]]
Sum节点

Sum节点是通用的加法节点。这里考虑对一个N×D的数组沿第0个轴求和。此时,Sum节点的正向传播和反向传播如图1-22所示。
在这里插入图片描述

图1-22 Sum节点的正向传播(上图)和反向传播(下图)

如图1-22所示,Sum节点的反向传播将上游传来的梯度分配到所有箭头上。这是加法节点的反向传播的自然扩展。下面,和Repeat节点一样,我们也来展示一下Sum节点的实现示例,如下所示。

import numpy as np
D,N = 8,7 
x = np.random.randn(N,D) # 输入
y = np.sum(x,axis=0,keepdims=True) # 正向传播
dy = np.random.randn(1,D) # 假设的梯度
dx = np.repeat(dy,N,axis=0) # 反向传播
print(x,y,dy,dx)
[[ 0.89537097  0.22432744  0.94135199 -2.2696392  -0.72565332 -0.79235767
   0.05426546 -0.5941178 ]
 [ 0.68011678  0.49310042  0.81607618 -1.09238102  0.82414314 -0.62151216
   1.02772305  1.09321035]
 [-0.46208365  0.59195862  0.67912749 -1.92346964  0.56483658  0.87066596
  -1.47871334  0.64403717]
 [ 0.42761419  0.10318995 -0.80747833  0.01638887  1.24014633 -0.68491584
  -1.8335278  -2.06326384]
 [-0.25659482  0.24308573  1.00591128 -0.65020313 -0.04504154 -0.26665785
  -1.62130109  1.21561124]
 [ 1.04537524 -0.60882174  0.79904833 -1.74358711 -0.22525197 -0.20221027
   1.31600569 -0.29160988]
 [ 0.37403777  0.95148592  0.64760357  0.91545427  0.27634025 -0.27963649
   0.2418728  -0.43913995]] [[ 2.70383648  1.99832633  4.08164051 -6.74743696  1.90951947 -1.97662431
  -2.29367523 -0.43527271]] [[ 0.18801312 -0.82046416 -1.15742412  0.00263969  1.96304172  1.14687676
   0.19588645 -2.20671411]] [[ 0.18801312 -0.82046416 -1.15742412  0.00263969  1.96304172  1.14687676
   0.19588645 -2.20671411]
 [ 0.18801312 -0.82046416 -1.15742412  0.00263969  1.96304172  1.14687676
   0.19588645 -2.20671411]
 [ 0.18801312 -0.82046416 -1.15742412  0.00263969  1.96304172  1.14687676
   0.19588645 -2.20671411]
 [ 0.18801312 -0.82046416 -1.15742412  0.00263969  1.96304172  1.14687676
   0.19588645 -2.20671411]
 [ 0.18801312 -0.82046416 -1.15742412  0.00263969  1.96304172  1.14687676
   0.19588645 -2.20671411]
 [ 0.18801312 -0.82046416 -1.15742412  0.00263969  1.96304172  1.14687676
   0.19588645 -2.20671411]
 [ 0.18801312 -0.82046416 -1.15742412  0.00263969  1.96304172  1.14687676
   0.19588645 -2.20671411]]

如上所示,Sum节点的正向传播通过np.sum(​)方法实现,反向传播通过np.repeat(​)方法实现。有趣的是,Sum节点和Repeat节点存在逆向关系。所谓逆向关系,是指Sum节点的正向传播相当于Repeat节点的反向传播,Sum节点的反向传播相当于Repeat节点的正向传播。

MatMul节点

本书将矩阵乘积称为MatMul节点。MatMul是Matrix Multiply的缩写。因为MatMul节点的反向传播稍微有些复杂,所以这里我们先进行一般性的介绍,再进行直观的解释。

为了解释MatMul节点,我们来考虑y=xW这个计算。这里,x、W、y的形状分别是1×D、D×H、1×H(图1-23)​。

在这里插入图片描述

图1-23 MatMul节点的正向传播:矩阵的形状显示在各个变量的上方

此时,可以按如下方式求得关于x的第i个元素的导数 ∂ L ∂ x i \frac{\partial L}{\partial x_i} xiL
在这里插入图片描述

式(1.12)的 ∂ L ∂ x i \frac{\partial L}{\partial x_i} xiL表示变化程度,即当xi发生微小的变化时,L会有多大程度的变化。如果此时改变xi,则向量y的所有元素都会发生变化。另外,因为y的各个元素会发生变化,所以最终L也会发生变化。因此,从xi到L的链式法则的路径有多个,它们的和是 ∂ L ∂ x i \frac{\partial L}{\partial x_i} xiL

式(1.12)仍可进一步简化。利用 ∂ y i ∂ x i = W i j \frac{\partial y_i}{\partial x_i}=W_{ij} xiyi=Wij,将其代入式(1.12)​:
在这里插入图片描述

由式(1.13)可知, ∂ L ∂ x i \frac{\partial L}{\partial x_i} xiL由向量 ∂ L ∂ y \frac{\partial L}{\partial y} yL和W的第i行向量的内积求得。从这个关系可以导出下式:
在这里插入图片描述

如式(1.14)所示, ∂ L ∂ x \frac{\partial L}{\partial x} xL可由矩阵乘积一次求得。这里, W T W^T WT的T表示转置矩阵。对式(1.14)进行形状检查,结果如图1-24所示。
在这里插入图片描述

如图1-24所示,矩阵形状的演变是正确的。由此,可以确认式(1.14)的计算是正确的。然后,我们可以反过来利用它(为了保持形状合规)来推导出反向传播的数学式(及其实现)​。为了说明这个方法,我们再次考虑矩阵乘积的计算y=xW。不过,这次考虑mini-batch处理,假设x中保存了N笔数据。此时,x、W、y的形状分别是N×D、D×H、N×H,反向传播的计算图如图1-25所示。
在这里插入图片描述

那么, ∂ L ∂ x \frac{\partial L}{\partial x} xL将如何计算呢?此时,和 ∂ L ∂ x \frac{\partial L}{\partial x} xL相关的变量(矩阵)是上游传来的 ∂ L ∂ y \frac{\partial L}{\partial y} yL和W。为什么说和W有关系呢?考虑到乘法的反向传播的话,就容易理解了。因为乘法的反向传播中使用了“将正向传播时的输入替换后的值”​。同理,矩阵乘积的反向传播也使用“将正向传播时的输入替换后的矩阵”​。之后,留意各个矩阵的形状求矩阵乘积,使它们的形状保持合规。如此,就可以导出矩阵乘积的反向传播,如图1-26所示。

如图1-26所示,通过确认矩阵的形状,可以推导矩阵乘积的反向传播的数学式。这样一来,我们就推导出了MatMul节点的反向传播。现在我们将MatMul节点实现为层,如下所示
在这里插入图片描述

import numpy as np

class MatMul:
    def __init__(self, W):
        # 初始化权重矩阵 W 和梯度
        self.params = [W]  # 存储参数(权重)
        self.grads = [np.zeros_like(W)]  # 初始化梯度为零矩阵
        self.x = None  # 存储输入

    def forward(self, x):
        # 前向传播:计算输出
        W, = self.params  # 解包权重
        out = np.dot(x, W)  # 计算 x 与 W 的点乘
        self.x = x  # 保存输入 x 以备反向传播使用
        return out  # 返回输出

    def backward(self, dout):
        # 反向传播:计算梯度
        W, = self.params  # 解包权重
        dx = np.dot(dout, W.T)  # 计算损失相对于输入的梯度
        dW = np.dot(self.x.T, dout)  # 计算损失相对于权重的梯度
        self.grads[0][...] = dW  # 更新权重的梯度
        return dx  # 返回输入的梯度

MatMul层在params中保存要学习的参数。另外,以与其对应的形式,将梯度保存在grads中。在反向传播时求dx和dw,并在实例变量grads中设置权重的梯度。另外,在设置梯度的值时,像grads[0]​[…] = dW这样,使用了省略号。由此,可以固定NumPy数组的内存地址,覆盖NumPy数组的元素。

Tip:浅拷贝和深拷贝的差异

和省略号一样,这里也可以进行基于grads[0] = dW的赋值。不同的是,在使用省略号的情况下会覆盖掉NumPy数组。这是浅复制(shallow copy)和深复制(deep copy)的差异。grads[0] = dW的赋值相当于浅复制,grads[0]​[…] = dW的覆盖相当于深复制。

省略号的话题稍微有些复杂,我们举个例子来说明。假设有a和b两个NumPy数组。

a = np.array([1,2,3])
b = np.array([4,5,6])

这里,不管是a = b,还是a[…] = b,a都被赋值[4,5,6]​。但是,此时a指向的内存地址不同。我们将内存(简化版)可视化,如图1-27所示。
在这里插入图片描述

如图1-27所示,在a = b的情况下,a指向的内存地址和b一样。由于实际的数据(4,5,6)没有被复制,所以这可以说是浅复制。而在a[…] = b时,a的内存地址保持不变,b的元素被复制到a指向的内存上。这时,因为实际的数据被复制了,所以称为深复制。

由此可知,使用省略号可以固定变量的内存地址(在上面的例子中,a的地址是固定的)​。通过固定这个内存地址,实例变量grads的处理会变简单。

在grads列表中保存各个参数的梯度。此时,grads列表中的各个元素是NumPy数组,仅在生成层时生成一次。然后,使用省略号,在不改变NumPy数组的内存地址的情况下覆盖数据。这样一来,将梯度汇总在一起的工作就只需要在开始时进行一次即可。

梯度的推导和反向传播的实现

下面我们来实现一些实用的层。这里,我们将实现Sigmoid层、全连接层Affine层和Softmax with Loss层。

Sigmoid层

sigmoid函数由 y = 1 1 + e − x y = \frac{1}{1+e^{-x}} y=1+ex1表示,sigmoid函数的导数由下式表示。
在这里插入图片描述

根据式(1.15),Sigmoid层的计算图可以绘制成图1-28。这里,将输出侧的层传来的梯度( ∂ L ∂ y \frac{\partial L}{\partial y} yL)乘以sigmoid函数的导数( ∂ L ∂ x \frac{\partial L}{\partial x} xL)​,然后将这个值传递给输入侧的层。
在这里插入图片描述

使用Python来实现Sigmoid层。

import numpy as np

class Sigmoid:
    def __init__(self):
        # Sigmoid 层不包含参数和梯度
        self.params = []  # 没有可训练的参数
        self.grads = []   # 没有参数的梯度
        self.out = None   # 保存 Sigmoid 函数的输出,用于反向传播

    def forward(self, x):
        # 前向传播:计算 Sigmoid 函数的输出
        # Sigmoid 函数:f(x) = 1 / (1 + exp(-x))
        out = 1 / (1 + np.exp(-x))  # 计算 Sigmoid 激活
        self.out = out  # 保存输出,用于反向传播
        return out  # 返回前向传播的结果

    def backward(self, dout):
        # 反向传播:计算梯度
        # Sigmoid 函数的导数:f'(x) = f(x) * (1 - f(x))
        dx = dout * (1.0 - self.out) * self.out  # 链式法则:dL/dx = dL/dout * dout/dx
        return dx  # 返回输入的梯度

这里将正向传播的输出保存在实例变量out中。然后,在反向传播中,使用这个out变量进行计算。

Affine层

如前所示,我们通过y = np.dot(x, W) + b实现了Affine层的正向传播。此时,在偏置的加法中,使用了NumPy的广播功能。如果明示这一点,则Affine层的计算图如图1-29所示。

如图1-29所示,通过MatMul节点进行矩阵乘积的计算。偏置被Repeat节点复制,然后进行加法运算(可以认为NumPy的广播功能在内部进行了Repeat节点的计算)​。下面是Affine层的实现
在这里插入图片描述

import numpy as np

class Affine:
    def __init__(self, W, b):
        # 初始化权重矩阵 W 和偏置 b
        self.params = [W, b]  # 存储权重和偏置
        # 初始化梯度为与 W 和 b 形状相同的零矩阵
        self.grads = [np.zeros_like(W), np.zeros_like(b)]
        self.x = None  # 用于存储输入,供反向传播时使用

    def forward(self, x):
        # 前向传播:计算仿射变换 (线性变换 + 偏置)
        W, b = self.params  # 获取权重和偏置
        # 仿射变换公式:out = xW + b
        out = np.dot(x, W) + b  # 矩阵乘法 xW 加上偏置 b
        self.x = x  # 保存输入 x,以备反向传播使用
        return out  # 返回仿射层的输出

    def backward(self, dout):
        # 反向传播:计算输入和参数的梯度
        W, b = self.params  # 获取权重和偏置
        # 计算输入的梯度:dx = dout * W^T
        dx = np.dot(dout, W.T)
        # 计算权重的梯度:dW = x^T * dout
        dW = np.dot(self.x.T, dout)
        # 计算偏置的梯度:db = dout 在每个样本上按列求和
        db = np.sum(dout, axis=0)

        # 更新权重和偏置的梯度
        self.grads[0][...] = dW  # 存储权重的梯度
        self.grads[1][...] = db  # 存储偏置的梯度
        return dx  # 返回输入的梯度

根据本书的代码规范,Affine层将参数保存在实例变量params中,将梯度保存在实例变量grads中。它的反向传播可以通过执行MatMul节点和Repeat节点的反向传播来实现。Repeat节点的反向传播可以通过np.sum(​)计算出来,此时注意矩阵的形状,就可以清楚地知道应该对哪个轴(axis)求和。最后,将权重参数的梯度设置给实例变量grads。以上就是Affine层的实现。

使用已经实现的MatMul层,可以更轻松地实现Affine层。这里出于复习的目的,没有使用MatMul层,而是使用NumPy的方法进行了实现。

Softmax with Loss层

我们将Softmax函数和交叉熵误差一起实现为Softmax with Loss层。此时,计算图如图1-30所示。
在这里插入图片描述

图1-30的计算图将Softmax函数记为Softmax层,将交叉熵误差记为Cross Entropy Error层。这里假设要执行3类别分类的任务,从前一层(靠近输入的层)接收3个输入。如图1-30所示,Softmax层对输入a1, a2, a3进行正规化,输出y1, y2, y3。Cross Entropy Error层接收Softmax的输出y1, y2, y3和监督标签t1, t2, t3,并基于这些数据输出损失L。

在图1-30中,需要注意的是反向传播的结果。从Softmax层传来的反向传播有y1-t1, y2-t2, y3-t3这样一个很“漂亮”的结果。因为y1, y2, y3是Softmax层的输出,t1, t2, t3是监督标签,所以y1-t1, y2-t2, y3-t3是Softmax层的输出和监督标签的差分。神经网络的反向传播将这个差分(误差)传给前面的层。这是神经网络的学习中的一个重要性质。

权重的更新——随机梯度下降算法

通过误差反向传播法求出梯度后,就可以使用该梯度更新神经网络的参数。此时,神经网络的学习按如下步骤进行。

  • step1:mini-batch

    从训练数据中随机选出多笔数据。

  • step2:计算梯度

    基于误差反向传播法,计算损失函数关于各个权重参数的梯度。

  • step3:更新参数

    使用梯度更新权重参数。

  • step4:重复

    根据需要重复多次步骤1、步骤2和步骤3。

我们按照上面的步骤进行神经网络的学习。首先,选择mini-batch数据,根据误差反向传播法获得权重的梯度。这个梯度指向当前的权重参数所处位置中损失增加最多的方向。因此,通过将参数向该梯度的反方向更新,可以降低损失。这就是梯度下降法(gradient descent)​。之后,根据需要将这一操作重复多次即可。

我们在上面的步骤3中更新权重。权重更新方法有很多,这里我们来实现其中最简单的随机梯度下降法(Stochastic Gradient Descent,SGD)​。其中,​“随机”是指使用随机选择的数据(mini-batch)的梯度。

SGD是一个很简单的方法。它将(当前的)权重朝梯度的(反)方向更新一定距离。如果用数学式表示,则有:
在这里插入图片描述

这里将要更新的权重参数记为W,损失函数关于W的梯度记为 ∂ L ∂ W \frac{\partial L}{\partial W} WL。η表示学习率,实际上使用0.01、0.001等预先定好的值。

现在,我们来进行SGD的实现。这里考虑到模块化,将进行参数更新的类实现在common/optimizer.py中。除了SGD之外,这个文件中还有AdaGrad和Adam等的实现。

进行参数更新的类的实现拥有通用方法update(params, grads)​。这里,在参数params和grads中分别以列表形式保存了神经网络的权重和梯度。此外,假定params和grads在相同索引处分别保存了对应的参数和梯度。这样一来,SGD就可以像下面这样实现(common/optimizer.py)​。

class SGD:
    def __init__(self, lr=0.01):
        # 初始化优化器,设定学习率(lr)
        self.lr = lr  # 学习率,控制每次参数更新的步长

    def update(self, params, grads):
        # 更新模型参数
        # params:模型的参数列表(如权重和偏置)
        # grads:与 params 对应的梯度列表
        for i in range(len(params)):  # 遍历每个参数
            # 使用随机梯度下降法更新参数
            # 更新公式:参数 = 参数 - 学习率 * 梯度
            params[i] -= self.lr * grads[i]  # 更新参数

初始化参数lr表示学习率(learning rate)​。这里将学习率保存为实例变量。然后,在update(params, grads)方法中实现参数的更新处理。

使用这个SGD类,神经网络的参数更新可按如下方式进行(下面的代码是不能实际运行的伪代码)​。

# 初始化两层神经网络模型
model = TwoLayerNet(...)  # 创建一个包含两层(比如:Affine + ReLU + Affine + Softmax)的神经网络

# 初始化优化器,使用随机梯度下降法(SGD)
optimizer = SGD()  # 创建一个SGD优化器实例,用于更新模型参数

# 进行10000次训练迭代
for i in range(10000):
    # 获取一个 mini-batch 样本及对应的标签
    x_batch, t_batch = get_mini_batch(...)  # 从训练数据中提取一小批样本 (mini-batch),提高训练效率并减少内存占用

    # 进行前向传播,计算当前 mini-batch 的损失
    loss = model.forward(x_batch, t_batch)  # 将 mini-batch 输入模型,并计算损失函数值 (比如交叉熵损失)

    # 进行反向传播,计算梯度
    model.backward()  # 基于损失函数的输出,计算模型参数(权重和偏置)的梯度

    # 使用优化器更新模型的参数
    optimizer.update(model.params, model.grads)  # 利用计算得到的梯度,更新模型的参数(比如权重、偏置)

    # 可以在这里添加额外的代码,如:记录损失值,打印训练进度等


像这样,通过独立实现进行最优化的类,系统的模块化会变得更加容易。除了SGD外,本书还实现了AdaGrad和Adam等方法

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2159354.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

[PICO VR]Unity如何往PICO VR眼镜里写持久化数据txt/json文本

前言 最近在用PICO VR做用户实验,需要将用户实验的数据记录到PICO头盔的存储空间里,记录一下整个过程 流程 1.开启写入权限 首先开启写入权限:Unity->Edit->Player->安卓小机器人->Other Settings->Configuration->Wri…

大数据毕业设计选题推荐-网络电视剧收视率分析系统-Hive-Hadoop-Spark

✨作者主页:IT毕设梦工厂✨ 个人简介:曾从事计算机专业培训教学,擅长Java、Python、PHP、.NET、Node.js、GO、微信小程序、安卓Android等项目实战。接项目定制开发、代码讲解、答辩教学、文档编写、降重等。 ☑文末获取源码☑ 精彩专栏推荐⬇…

数据库主备副本物理复制和逻辑复制对比

数据库主从节点的数据一致性是保证数据库高可用的基本要求,各个数据库在实现方式上也各有异同。而主备复制的方式无外乎两种:物理复制和逻辑复制,本文简要对比下两种方式的不同,并分析下国产数据库是如何实现的。 1、数据库复制基…

中国中车在线测评考的啥?大易题库如何通过|附真题型国企题库通关秘籍和攻略

言语理解题目:这类题目主要考察你的语言理解和表达能力,例如,给你一个段落,让你根据段落内容选择最合适的答案。要点是快速捕捉文段中的关键信息,理解作者的意图和观点 逻辑推理题目:这类题目需要你从一组…

Java面试篇基础部分- 锁详解

可重入锁 可重入锁也叫作递归锁,是指在同一个线程中,在外层函数获取到该锁之后,内存的递归函数还可以获取到该锁。在Java语言环境下,ReentrantLock和Synchroinzed都是可重入锁的代表。 公平锁与非公平锁 公平锁(Fair Lock)是指在分配锁之前检查是否有线程在排队等待获取…

CICD从无到会

一 CICD是什么 CI/CD 是指持续集成(Continuous Integration)和持续部署(Continuous Deployment)或持续交付(Continuous Delivery) 1.1 持续集成(Continuous Integration) 持续集成是…

【每天学个新注解】Day 4 Lombok注解简解(三)—@NonNull

我们在之前的三天学了Lombok常用的注解: 【每天学个新注解】Day 1 Lombok注解简解(〇)—Getter、Setter、ToString、EqualsAndHashCode、Constructor 【每天学个新注解】Day 2 Lombok注解简解(一)—Data、Build、Value…

【威领,德新,中达安】9.23复盘

威领这次的底部是4个月 所以这种跳空高开,远离5日均线的,如果不是近期的利好板块,那么第二天可能要回调5日均线。所以按照我的收益准则,吃一个板可以出一半了。 到顶部十字剩下一半也出掉了。 如果做长期,我依旧认为威…

git学习报告

文章目录 git学习报告如何配置vscode终端安装PowerShell安装 Microsoft.Powershell.Preview使用 git的使用关于团队合作 git指令本地命令:云端指令 git学习报告 如何配置vscode 安装powershell调教window终端,使其像Linux一样,通过Linux命令…

C语言初识(一)

目录 前言 一、什么是C语言? 二、第一个C语言程序 (1)创建新项目 (2)编写代码 (3)main函数 三、数据类型 四、变量、常量 (1)变量的命名 (2&#x…

mysql复合查询 -- 合并查询(union,union all)

目录 合并查询 介绍 表数据 union 使用场景 ​编辑 示例 union all 合并查询 介绍 它不像笛卡尔积那种,将行信息做乘法 合并只是单纯地合在一起求的是两个结果集的并集,并且会自动去掉并集中的重复行 注意,因为是求并集,会将两个结果进行拼接 所以要保证列信息相同 表…

13.第二阶段x86游戏实战2-动态模块地址

免责声明:内容仅供学习参考,请合法利用知识,禁止进行违法犯罪活动! 本次游戏没法给 内容参考于:微尘网络安全 本人写的内容纯属胡编乱造,全都是合成造假,仅仅只是为了娱乐,请不要…

基于Python+SQLServer实现(界面)书店销售管理管理子系统

书店销售管理管理子系统 一、设 计 总 说 明 现在社会随着计算机技术迅速发展与技术的逐渐成熟,信息技术已经使人们的生活发生深刻的变化。生活中的各种服务系统也使人们在生活中的联系日常销售活动方式发生了很大的变化,让效率较低的手工操作成为过去…

大数据新视界 --大数据大厂之 Reactjs 在大数据应用开发中的优势与实践

💖💖💖亲爱的朋友们,热烈欢迎你们来到 青云交的博客!能与你们在此邂逅,我满心欢喜,深感无比荣幸。在这个瞬息万变的时代,我们每个人都在苦苦追寻一处能让心灵安然栖息的港湾。而 我的…

OpenHarmony(鸿蒙南向开发)——小型系统内核(LiteOS-A)【Perf调测】

往期知识点记录: 鸿蒙(HarmonyOS)应用层开发(北向)知识点汇总 鸿蒙(OpenHarmony)南向开发保姆级知识点汇总~ 持续更新中…… 基本概念 Perf为性能分析工具,依赖PMU(Per…

UE学习篇ContentExample解读-----------Blueprint_Mouse_Interaction

文章目录 总览描述(Blueprint_Mouse_Interaction)阅览解析1、PlayerControler分析2、拖拽球蓝图分析:3、移动的立方体分析: 新概念总结致谢: 总览描述(Blueprint_Mouse_Interaction) 打开关卡后…

MySQL tinyint(1)类型数据在经过flink cdc同步到doris后只有0/1问题定位与解决

背景: 近期在负责公司数据仓库搭建事宜,踩了一些坑后,终于通了,目标报表也成功迁移到了新方案上,可在数据验收的时候发现,同一个订单查询出了多条记录,原本以为只是简单的left join出多条记录问…

植物检测系统源码分享

植物检测检测系统源码分享 [一条龙教学YOLOV8标注好的数据集一键训练_70全套改进创新点发刊_Web前端展示] 1.研究背景与意义 项目参考AAAI Association for the Advancement of Artificial Intelligence 项目来源AACV Association for the Advancement of Computer Vision …

Kubernetes调度单位Pod

Kubernetes调度单位Pod 1 Pod简介 不直接操作容器container。 一个 pod 可包含一或多个容器(container),它们共享一个 namespace(用户,网络,存储等),其中进程之间通过 localhost 本地…

Linux环境下安装部署MySQL8.0以上(内置保姆级教程) C语言

一、环境搭建、 1 、安装MySQL服务端与客户端 sudo apt-get install mysql-server //mysql服务端安装 。 (现在只安装这一个就够了,包含了客户端的) sudo apt-get install mysql-client //mysql客户端安装。 mysql服务器端程序&…