吴恩达机器学习笔记(8)—神经网络:反向传播算法(附代码)

上一节介绍了神经网络模型可以利用前向传播做预测,并引入了一系列符号来描述神经网络模型:

  • x i x_i xi 表示输入层的第 i i i 个输入特征,其中 x 0 x_0 x0 偏置项省略;
  • a i ( j ) a_i^{(j)} ai(j) 表示第 j j j 层的第 i i i 个激活单元,其中 a 0 ( j ) a_0^{(j)} a0(j) 偏置单元省略;
  • s j s_j sj 表示第 j j j 层的神经元数量(不包含偏置项), a ( j ) ∈ R s j + 1 a^{(j)} \in \mathbb{R}^{s_j + 1} a(j)Rsj+1 表示加上偏置项后的 a ( j ) a^{(j)} a(j)
  • Θ ( j ) ∈ R s j + 1 × ( s j + 1 ) \Theta^{(j)} \in \mathbb{R}^{s_{j + 1} \times (s_j + 1)} Θ(j)Rsj+1×(sj+1) 表示第 j j j 层到第 j + 1 j + 1 j+1 层的权重矩阵 Θ \Theta Θ 表示所有权重矩阵的集合

其中的权重参数,与前面的回归问题类似,需要通过最小化代价函数来逼近。

一、代价函数

为了描述代价函数,我们引入新的符号:

  • L L L 表示神经网络的层数,包含输入层、隐藏层、输出层,因此共有 L − 1 L - 1 L1 次传播;
  • K ⩾ 3 K \geqslant 3 K3 表示多分类问题的类别数,相应的有 s L = K s_L = K sL=K h θ ( x ) ∈ R K h_\theta(x) \in \mathbb{R}^K hθ(x)RK
  • h θ ( x ( i ) ) ∈ R K h_\theta(x^{(i)}) \in \mathbb{R}^K hθ(x(i))RK,表示从第 i i i 个样本经过神经网络得到的输出结果, h θ ( x ( i ) ) k h_\theta(x^{(i)})_k hθ(x(i))k 表示其第 k k k 维。

回顾正则化逻辑回归中的交叉熵代价函数
J ( θ ) = − 1 m ∑ i = 1 m [ y ( i ) log ⁡ h θ ( x ( i ) ) + ( 1 − y ( i ) ) log ⁡ ( 1 − h θ ( x ( i ) ) ) ] + λ 2 m ∑ j = 1 n θ j 2 J(\theta) = -\frac{1}{m} \sum_{i=1}^{m} \left[ y^{(i)} \log h_\theta(x^{(i)}) + (1 - y^{(i)}) \log (1 - h_\theta(x^{(i)})) \right] + \frac{\lambda}{2m} \sum_{j=1}^{n} \theta_j^2 J(θ)=m1i=1m[y(i)loghθ(x(i))+(1y(i))log(1hθ(x(i)))]+2mλj=1nθj2

在神经网络中,我们沿用交叉熵代价函数而不是均方误差,原因如下:

  1. 我们使用了 sigmoid 函数作为激活函数引入非线性,其单增 S S S 形曲线使其在 y = 0 y = 0 y=0 1 1 1 附近导数较小;
  2. 通常我们会随机初始化参数后进行梯度下降,如果使用均方误差代价函数,对 θ \theta θ 求偏导后含有 sigmoid 的导数项,会使前期的梯度下降十分缓慢;
  3. 如果使用交叉熵代价函数,则求偏导后不含 sigmoid 的导数项,前期收敛更快。

具体地,对于一个 K K K 分类的神经网络而言,可以看作是 K K K 个二分类的交叉熵代价函数之和
J ( Θ ) = − 1 m [ ∑ i = 1 m ∑ k = 1 K y k ( i ) ln ⁡ ( h Θ ( x ( i ) ) ) k + ( 1 − y k ( i ) ) ln ⁡ ( 1 − h Θ ( x ( i ) ) ) k ] + λ 2 m ∑ l = 1 L − 1 ∑ i = 1 s l ∑ j = 1 s l + 1 ( Θ j i ( l ) ) 2 J(\Theta) = -\frac{1}{m}\left[ \sum_{i=1}^{m} \sum_{k=1}^{K} y_k^{(i)} \ln \left(h_\Theta(x^{(i)})\right)_k + (1 - y_k^{(i)}) \ln \left(1 - h_\Theta(x^{(i)})\right)_k \right] + \frac{\lambda}{2m} \sum_{l=1}^{L-1} \sum_{i=1}^{s_l} \sum_{j=1}^{s_{l+1}} \left(\Theta_{ji}^{(l)}\right)^2 J(Θ)=m1[i=1mk=1Kyk(i)ln(hΘ(x(i)))k+(1yk(i))ln(1hΘ(x(i)))k]+2mλl=1L1i=1slj=1sl+1(Θji(l))2

注意这里有一个符号混用的地方:上标 ( i ) (i) (i) x ( i ) x^{(i)} x(i) y ( i ) y^{(i)} y(i) 中表示第 i i i 个数据的输入 ( ∈ R n + 1 ) (\in \mathbb{R}^{n+1}) (Rn+1) 和输出 ( ∈ R K ) (\in \mathbb{R}^K) (RK) ,但是在 Θ ( l ) \Theta^{(l)} Θ(l) 中表示第 l l l 层的 Θ ( ∈ R s l + 1 × ( s l + 1 ) ) \Theta (\in \mathbb{R}^{s_{l+1} \times (s_l + 1)}) Θ(Rsl+1×(sl+1))

二、反向传播算法

现在我们的目标就是最小化代价函数 J ( Θ ) J(\Theta) J(Θ) ,并找到使之最小的参数 Θ \Theta Θ ,我们仍使用梯度下降法解决最小化问题(尽管 J ( Θ ) J(\Theta) J(Θ) 可能为非凸函数)。因此需要计算梯度 ∂ J ∂ Θ j i ( l ) \frac{\partial J}{\partial \Theta_{ji}^{(l)}} Θji(l)J ,而计算的关键就是「反向传播」:从输出层开始逐层向前计算。

  • 为什么需要「反向传播」?我们知道梯度反映的是 J J J Θ j i ( l ) \Theta_{ji}^{(l)} Θji(l) 方向的变化率,即:当参数发生微小增量时,代价函数的增量。对于前几层网络,参数的增量需要经过多层全连接传播,才会影响到输出(代价函数)。
  • 如果我们从输入层开始计算每个参数的梯度,则每个参数都要依赖其后续路径的传播,每个参数的计算都要先算一遍后续路径的梯度。而如果从输出层开始逐层向前计算并存储了梯度,则解耦了依赖关系,前一层可以利用后一层的计算结果。有点类似「动态规划」中「递归求解子问题」的思想。

1、数学推导:问题转化

为了推导方便,先假设只有一组数据 ( x , y ) (x, y) (x,y) ,这样可以省去上标和求和的麻烦。并且令 λ = 0 \lambda = 0 λ=0 省略正则化项,等到最后的偏导结果再加上。

我们首先对第 l + 1 l + 1 l+1 层的第 j j j 个神经元(偏置单元除外)定义一个 delta 误差:
δ j ( l + 1 ) = ∂ J ∂ z j ( l + 1 ) \delta_j^{(l + 1)} = \frac{\partial J}{\partial z_j^{(l + 1)}} δj(l+1)=zj(l+1)J

其中 z j ( l + 1 ) = ∑ k = 0 s l Θ j k ( l ) a k ( l ) z_j^{(l + 1)} = \sum_{k = 0}^{s_l} \Theta_{jk}^{(l)} a_k^{(l)} zj(l+1)=k=0slΘjk(l)ak(l) ,即 a j ( l + 1 ) a_j^{(l + 1)} aj(l+1) 神经元取 sigmoid 激活前的输出值。换句话说, a j ( l + 1 ) = g ( z j ( l + 1 ) ) a_j^{(l + 1)} = g\left(z_j^{(l + 1)}\right) aj(l+1)=g(zj(l+1))

于是根据链式求导法则,我们有:
∂ J ∂ Θ j i ( l ) = ∂ J ∂ z j ( l + 1 ) ⋅ ∂ z j ( l + 1 ) ∂ Θ j i ( l ) = δ j ( l + 1 ) a i ( l ) \frac{\partial J}{\partial \Theta_{ji}^{(l)}} = \frac{\partial J}{\partial z_j^{(l + 1)}} \cdot \frac{\partial z_j^{(l + 1)}}{\partial \Theta_{ji}^{(l)}} = \delta_j^{(l + 1)} a_i^{(l)} Θji(l)J=zj(l+1)JΘji(l)zj(l+1)=δj(l+1)ai(l)

写作矩阵形式:
Δ ( l ) = δ ( l + 1 ) ⋅ ( a ( l ) ) T \Delta^{(l)} = \delta^{(l + 1)} \cdot \left(a^{(l)}\right)^T Δ(l)=δ(l+1)(a(l))T

显然,一旦求出当前层的 d e l t a delta delta 误差,那么传入当前层的各参数的梯度也可求出。所以现在问题转化为求解 δ ( l + 1 ) \delta^{(l + 1)} δ(l+1)

2、数学推导:输出层误差

既然要递归求解子问题,首先就是要算输出层,即 δ j ( L ) \delta_j^{(L)} δj(L)
δ j ( L ) = ∂ J ∂ z j ( L ) = ∂ J ∂ a j ( L ) ⋅ ∂ a j ( L ) ∂ z j ( L ) = ∂ J ∂ a j ( L ) ⋅ g ′ ( z j ( L ) ) = − ( y j a j ( L ) − 1 − y j 1 − a j ( L ) ) ⋅ ( a j ( L ) ( 1 − a j ( L ) ) ) = a j ( L ) − y j \begin{aligned} \delta_j^{(L)} &= \frac{\partial J}{\partial z_j^{(L)}} = \frac{\partial J}{\partial a_j^{(L)}} \cdot \frac{\partial a_j^{(L)}}{\partial z_j^{(L)}} \\ &= \frac{\partial J}{\partial a_j^{(L)}} \cdot g'\left(z_j^{(L)}\right) \\ &= -\left( \frac{y_j}{a_j^{(L)}} - \frac{1 - y_j}{1 - a_j^{(L)}} \right) \cdot \left( a_j^{(L)} \left( 1 - a_j^{(L)} \right) \right) \\ &= a_j^{(L)} - y_j \end{aligned} δj(L)=zj(L)J=aj(L)Jzj(L)aj(L)=aj(L)Jg(zj(L))=(aj(L)yj1aj(L)1yj)(aj(L)(1aj(L)))=aj(L)yj

写作矩阵形式:
δ ( L ) = a ( L ) − y \delta^{(L)} = a^{(L)} - y δ(L)=a(L)y

  • 此处推导过程的一些注释:
  • 关于第二行,请注意: a ( L ) = g ( z ( L ) ) a^{(L)} = g(z^{(L)}) a(L)=g(z(L))
  • 关于第三行,请注意 sigmoid 函数的性质: g ′ ( z ) = g ( z ) ( 1 − g ( z ) ) g'(z) = g(z)(1 - g(z)) g(z)=g(z)(1g(z))
  • 以及第三行偏导项的计算,请注意 a ( L ) = h Θ ( x ) a^{(L)} = h_\Theta(x) a(L)=hΘ(x) ,所以 J ( Θ ) J(\Theta) J(Θ) 在现在的假设条件下可以写作:
    J ( Θ ) = − [ ∑ k = 1 K y k ln ⁡ ( a k ( L ) ) + ( 1 − y k ) ln ⁡ ( 1 − a k ( L ) ) ] J(\Theta) = -\left[ \sum_{k = 1}^{K} y_k \ln\left(a_k^{(L)}\right) + (1 - y_k)\ln\left(1 - a_k^{(L)}\right) \right] J(Θ)=[k=1Kykln(ak(L))+(1yk)ln(1ak(L))]

3、数学推导:隐藏层误差

下面计算第 l l l ( 2 ⩽ l < L ) (2 \leqslant l < L) (2l<L) δ j ( l ) \delta_j^{(l)} δj(l)
δ j ( l ) = ∂ J ∂ z j ( l ) = ∂ J ∂ z j ( l + 1 ) ⋅ ∂ z j ( l + 1 ) ∂ a j ( l ) ⋅ ∂ a j ( l ) ∂ z j ( l ) = δ ( l + 1 ) T ⋅ Θ j ( l ) ⋅ g ′ ( z j ( l ) ) = δ ( l + 1 ) T ⋅ Θ j ( l ) ⋅ ( a j ( l ) ( 1 − a j ( l ) ) ) \begin{aligned} \delta_j^{(l)} &= \frac{\partial J}{\partial z_j^{(l)}} \\ &= \frac{\partial J}{\partial z_j^{(l + 1)}} \cdot \frac{\partial z_j^{(l + 1)}}{\partial a_j^{(l)}} \cdot \frac{\partial a_j^{(l)}}{\partial z_j^{(l)}} \\ &= \delta^{(l + 1)T} \cdot \Theta_{j}^{(l)} \cdot g'\left(z_j^{(l)}\right) \\ &= \delta^{(l + 1)T} \cdot \Theta_{j}^{(l)} \cdot \left(a_j^{(l)} \left(1 - a_j^{(l)}\right)\right) \end{aligned} δj(l)=zj(l)J=zj(l+1)Jaj(l)zj(l+1)zj(l)aj(l)=δ(l+1)TΘj(l)g(zj(l))=δ(l+1)TΘj(l)(aj(l)(1aj(l)))

观察式子的前半部分,可以看出下一层所有结点的 d e l t a delta delta 误差均会影响当前层的任一结点,而影响则是通过该结点链出的权重参数反向传播。

转置后,写作矩阵形式:
δ ( l ) = ( Θ ( l ) T δ ( l + 1 ) ) ⊙ ( a ( l ) ⊙ ( 1 − a ( l ) ) ) \delta^{(l)} = \left( \Theta^{(l)T} \delta^{(l + 1)} \right) \odot \left( a^{(l)} \odot \left( 1 - a^{(l)} \right) \right) δ(l)=(Θ(l)Tδ(l+1))(a(l)(1a(l)))

其中 ⊙ \odot 表示 Hadamard 积,即两个向量对应位置相乘。

4、步骤总结

以上是对一组数据的推导,我们得到了三个重要的结果 (1)(2)(3):
{ Δ ( l ) = δ ( l + 1 ) ⋅ ( a ( l ) ) T 1 ⩽ l < L δ ( L ) = a ( L ) − y δ ( l ) = ( Θ ( l ) T δ ( l + 1 ) ) ⊙ ( a ( l ) ⊙ ( 1 − a ( l ) ) ) 2 ⩽ l < L \begin{cases} \Delta^{(l)} = \delta^{(l + 1)} \cdot \left( a^{(l)} \right)^T & 1 \leqslant l < L \\ \delta^{(L)} = a^{(L)} - y \\ \delta^{(l)} = \left( \Theta^{(l)T} \delta^{(l + 1)} \right) \odot \left( a^{(l)} \odot \left( 1 - a^{(l)} \right) \right) & 2 \leqslant l < L \end{cases} Δ(l)=δ(l+1)(a(l))Tδ(L)=a(L)yδ(l)=(Θ(l)Tδ(l+1))(a(l)(1a(l)))1l<L2l<L

m m m 组数据只需要在一些地方进行累加即可。设数据集为 { ( x ( i ) , y ( i ) ) ∣ 1 ⩽ i ⩽ m } \{ (x^{(i)}, y^{(i)}) \mid 1 \leqslant i \leqslant m \} {(x(i),y(i))1im} ,则反向传播算法的步骤如下:

  1. 所有 Δ ( l ) \Delta^{(l)} Δ(l) 置零;

  2. 遍历数据集,设当前数据为 ( x ( i ) , y ( i ) ) (x^{(i)}, y^{(i)}) (x(i),y(i))
    i. 以 x ( i ) x^{(i)} x(i) 为输入做前向传播,得到输出 a ( L ) a^{(L)} a(L)
    ii. 公式 (2) 计算输出层误差,公式 (3) 计算隐藏层误差;
    iii. 公式 (1) 更新各层的 Δ ( l ) \Delta^{(l)} Δ(l) 矩阵,进行累加 Δ ( l ) : = Δ ( l ) + δ ( l + 1 ) ⋅ ( a ( l ) ) T \Delta^{(l)} := \Delta^{(l)} + \delta^{(l + 1)} \cdot \left( a^{(l)} \right)^T Δ(l):=Δ(l)+δ(l+1)(a(l))T

  3. 计算 D D D 矩阵,对偏置项以外的参数进行正则化
    D i j ( l ) : = { 1 m ( Δ i j ( l ) + λ Θ i j ( l ) ) if  j ≠ 0 1 m Δ i j ( l ) if  j = 0 D_{ij}^{(l)} := \begin{cases} \frac{1}{m} \left( \Delta_{ij}^{(l)} + \lambda \Theta_{ij}^{(l)} \right) & \text{if } j \neq 0 \\ \frac{1}{m} \Delta_{ij}^{(l)} & \text{if } j = 0 \end{cases} Dij(l):={m1(Δij(l)+λΘij(l))m1Δij(l)if j=0if j=0

这就是最终的梯度矩阵: ∂ J ∂ Θ i j ( l ) = D i j ( l ) \frac{\partial J}{\partial \Theta_{ij}^{(l)}} = D_{ij}^{(l)} Θij(l)J=Dij(l)

现在,我们可以用 D i j ( l ) D_{ij}^{(l)} Dij(l) 做一次梯度下降了,整个步骤称为一代(epoch)。注意,我们的参数 Θ ( l ) \Theta^{(l)} Θ(l) 应该初始化为 [ − ϵ , ϵ ] [-\epsilon, \epsilon] [ϵ,ϵ] 中的随机值

三、参数展开

实现过程中,由于每一层都有一个 Θ ( l ) \Theta^{(l)} Θ(l) D ( l ) D^{(l)} D(l) 矩阵,总共有 2 × L 2 \times L 2×L 个矩阵,为了方便存储和计算,我们最好都转换为向量,并将各层的向量拼接成一个长向量。这样做可以方便调用封装好的高级优化函数接口,如 fminuncscipy.optimize.minimize

同样,函数输出的结果向量也可以通过 reshape 转为矩阵,再应用于前向传播。

四、梯度检验

当我们为一个较为复杂的模型(例如神经网络)实现梯度下降算法时,可能会出现一些不易察觉的错误,导致虽然代价看上去在不断减小,但最终的结果可能并不是最优解,误差可能高出一个量级。

为了避免这样的问题,可采取梯度的数值检验方法:通过数值计算得到梯度的近似值,再和反向传播的结果比对,检验是否接近。

在任意一代计算后,得到一组 θ \theta θ 向量及 D D D 矩阵,通过差商近似 J ( θ ) J(\theta) J(θ) 的各偏导:
∂ ∂ θ k J ( θ ) ≈ J ( θ 1 , ⋯   , θ k + ϵ , ⋯   , θ n ) − J ( θ 1 , ⋯   , θ k − ϵ , ⋯   , θ n ) 2 ϵ \frac{\partial}{\partial \theta_k} J(\theta) \approx \frac{J(\theta_1, \cdots, \theta_k + \epsilon, \cdots, \theta_n) - J(\theta_1, \cdots, \theta_k - \epsilon, \cdots, \theta_n)}{2\epsilon} θkJ(θ)2ϵJ(θ1,,θk+ϵ,,θn)J(θ1,,θkϵ,,θn)

其中, ϵ \epsilon ϵ 是非常小的常数,为了避免计算误差,通常选取 1 0 − 4 10^{-4} 104。现在我们可以比较这些偏导估计值与对应位置 D i j ( l ) D_{ij}^{(l)} Dij(l) 的值,它们应该非常接近。

注意:梯度检验耗时巨大,复杂度远大于反向传播,一旦验证了神经网络反向传播的代码正确后,不应进行梯度检验(删掉 or 注释掉)。

五、参数随机初始化

前文我们提到参数 Θ ( l ) \Theta^{(l)} Θ(l) 应该初始化为 [ − ϵ , ϵ ] [-\epsilon, \epsilon] [ϵ,ϵ] 中的随机值,而不是简单初始化为全零。如果将 Θ ( l ) \Theta^{(l)} Θ(l) 初始化为全零,则下一层的 z ( l + 1 ) z^{(l + 1)} z(l+1) 也为全零,则 a ( l + 1 ) a^{(l + 1)} a(l+1) 就全为 0.5 0.5 0.5,且所有隐藏层都会得到这个结果;与此同时,反向传播公式 (3) 中,由输出层到隐藏层时 d e l t a delta delta 误差也会清零。这将导致网络无法传播!

那么,如果将参数 Θ ( l ) \Theta^{(l)} Θ(l) 全都初始化为同一个非零常量呢?由于全连接,同一层的 a ( l + 1 ) a^{(l + 1)} a(l+1) 将为相同值;在反向传播公式 (3) 中,由输出层到隐藏层时 d e l t a delta delta 误差也会变成相同值,则所有的 D ( l ) D^{(l)} D(l) 矩阵也为常数矩阵,参数 Θ ( l ) \Theta^{(l)} Θ(l) 将再次被更新为新的相同常量。这样下去所有的激活单元都成了摆设,因为每一层都在重复计算着相同特征。

  1. 上述问题也被称为对称权重问题,解决方法就是随机初始化,实现时可以将 [ 0 , 1 ] [0, 1] [0,1] 的随机数映射到 [ − ϵ , ϵ ] [-\epsilon, \epsilon] [ϵ,ϵ] ,这里的 ϵ \epsilon ϵ 与梯度检验中的无关

  2. 实际应用中 ϵ \epsilon ϵ 的选取,通常与 Θ ( l ) \Theta^{(l)} Θ(l) 前后两层的神经元个数有关,一种常用的有效的取值是:
    ϵ ( l ) = 6 s ( l ) + s ( l + 1 ) \epsilon^{(l)} = \frac{\sqrt{6}}{\sqrt{s^{(l)} + s^{(l + 1)}}} ϵ(l)=s(l)+s(l+1) 6

六、代码实现

下面以 Coursera 上的多分类数据集 ex4data1.mat 为例,这是一个手写数字的数据集,与上一节的数据集类似。共 5000 组数据,每组数据输入是一个由 20 × 20 20 \times 20 20×20 灰度矩阵压缩而来的 400 维向量,输出是 0 到 9 之间的整数。

题目还提供一组训练好的权重参数 ex4weight.mat,用以检验前向传播和代价函数的正确性,此处跳过。采用题目推荐的网络层数 L = 3 L = 3 L=3,则输入层有 401 个单元(含偏置项),隐藏层有 26 个单元(含偏置项),输出层有 10 个单元(独热编码)。

实现反向传播算法并检验结果,大约耗时 20 分钟:

# 导入必要的库
import numpy as np                  # 用于数值计算和矩阵操作
import scipy.io as scio             # 用于读取MATLAB格式的数据文件
from sklearn.metrics import classification_report  # 用于生成分类结果的详细报告

# 加载数据集
# 从MAT文件中读取数据,该文件包含手写数字识别的训练样本
data = scio.loadmat('ex4data1.mat')
X = data['X']                       # 提取特征数据,形状为(5000, 400),每一行是一个20×20像素的手写数字展开成的向量
y = data['y'].flatten()             # 提取标签数据并将其展平为一维数组(5000, )
y[y==10] = 0                        # 原数据中数字0用10表示,这里将其转换为0以便统一处理

# 神经网络参数设置
(m, n) = (5000, 401)                # m: 样本数量,n: 特征数量+1(偏置项)
L = 3                               # 神经网络的层数(输入层+隐藏层+输出层)
s = [0, 400, 25, 10]                # 各层神经元数量,s[1]为输入层(400个特征),s[2]为隐藏层(25个神经元),s[3]为输出层(10个类别)
lmd = 1                             # 正则化参数,用于防止过拟合
alpha = 0.1                         # 梯度下降的学习率
num_iters = 5000                    # 梯度下降的迭代次数
#J_history = []                     # 用于记录每次迭代的损失值(当前注释掉)

# 对标签进行One-hot编码
# 将形状为(m, )的标签转换为(m, 10)的矩阵,每一行只有一个1,表示对应类别
Y = np.zeros((m, s[L]))
for i in range(m):
    Y[i][y[i]] = 1

# 随机初始化权重参数Theta
# Theta[l]表示从第l层到第l+1层的权重矩阵
Theta = [None] * L                  # 索引0未使用,符合层编号习惯
for l in range(1, L):
    # 生成(s[l+1], s[l]+1)形状的随机矩阵,+1是因为要包含偏置项的权重
    Theta[l] = np.random.rand(s[l+1], s[l]+1)
    # 使用Xavier初始化方法调整权重范围,有助于训练收敛
    eps = np.sqrt(6) / np.sqrt(s[l+1] + s[l])
    Theta[l] = Theta[l] * 2 * eps - eps  # 将权重调整到[-eps, eps]范围内

def sigmoid(z):
    """sigmoid激活函数,将输入映射到(0,1)区间"""
    return 1 / (1 + np.exp(-z))

def forwardProp():
    """前向传播计算
    返回各层的激活值a,其中a[l]表示第l层的输出
    """
    a = [None] * (L+1)  # a[0]未使用,索引从1开始对应各层
    
    # 输入层处理:添加偏置项(全为1的列)
    a[1] = np.c_[np.ones(m), X]  # 形状为(5000, 401)
    
    # 隐藏层计算
    for l in range(2, L):
        # 计算加权输入并应用sigmoid激活函数
        a[l] = sigmoid(a[l-1] @ Theta[l-1].T)  # 形状为(5000, 25)
        a[l] = np.c_[np.ones(m), a[l]]         # 添加偏置项,形状变为(5000, 26)
    
    # 输出层计算:最后一层不需要再添加偏置项
    a[L] = sigmoid(a[L-1] @ Theta[L-1].T)  # 形状为(5000, 10)
    
    return a

def J(a_L):
    """计算损失函数值
    参数:
        a_L: 输出层的激活值,形状为(m, s[L])
    返回:
        包含正则化项的交叉熵损失
    """
    # 计算交叉熵损失(不包含正则化项)
    res = - (1 / m) * np.sum(Y * np.log(a_L) + (1-Y) * np.log(1-a_L))
    
    # 添加正则化项(注意排除偏置项对应的权重)
    for l in range(1, L):
        # Theta[l][:, 1:]表示排除每一行的第一个元素(偏置项权重)
        res += lmd / (2 * m) * np.sum(np.power(Theta[l][:, 1:], 2))
    
    return res

def backProp():
    """反向传播算法,计算各权重矩阵的梯度
    返回:
        D: 与Theta结构相同的梯度矩阵
    """
    # 初始化Delta矩阵,用于累积梯度(与Theta形状相同)
    Delta = [None] * L
    for l in range(1, L):
        Delta[l] = np.zeros((s[l+1], s[l]+1))
    
    # 初始化delta,用于存储各层的误差项
    delta = [None] * (L+1)  # delta[0]和delta[1]未使用(输入层无误差)
    
    # 步骤1:执行前向传播,获取各层激活值
    a = forwardProp()
    #J_history.append(J(a[L]))  # 记录当前迭代的损失值(当前注释掉)
    
    # 步骤2:计算各层误差并累积梯度
    for i in range(m):  # 对每个样本计算
        # 输出层误差:预测值与真实值的差
        delta[L] = a[L][i, :] - Y[i, :]  # 形状为(s[L], )
        
        # 反向计算隐藏层误差(从倒数第二层开始到第二层)
        for l in range(L-1, 1, -1):
            # 误差计算:权重矩阵转置与上层误差的乘积,再乘以sigmoid函数的导数
            # [1:]表示排除偏置项对应的误差(因为偏置项没有输入来源)
            delta[l] = ((Theta[l].T @ delta[l+1]) * (a[l][i, :] * (1 - a[l][i, :])))[1:]
        
        # 累积梯度:误差项与激活值的外积
        for l in range(1, L):
            # delta[l+1][:,np.newaxis]将行向量转为列向量,a[l][i:i+1, :]保持为行向量
            # 外积结果为(s[l+1], s[l]+1),与Theta[l]形状一致
            Delta[l] += delta[l+1][:,np.newaxis] @ a[l][i:i+1, :]
    
    # 步骤3:计算平均梯度并添加正则化项
    D = [None] * L  # D的结构与Theta相同
    for l in range(1, L):
        # 偏置项的权重不参与正则化(第一列保持原样)
        D[l] = (1 / m) * (Delta[l] + lmd * np.c_[np.zeros(s[l+1]), Theta[l][:, 1:]])
    
    return D

# 梯度下降算法:迭代更新权重参数
for i in range(0, num_iters):
    print(i)  # 打印当前迭代次数,便于观察训练进度
    D = backProp()  # 计算梯度
    # 更新各层权重:Theta = Theta - alpha * 梯度
    for l in range(1, L):
        Theta[l] -= alpha * D[l]

# 模型预测与评估
# 执行前向传播获取输出层结果,取最大值所在索引作为预测类别
y_pred = np.argmax(forwardProp()[L], axis=1)  # 形状为(5000, )

# 生成并打印分类报告,包括精确率、召回率、F1分数等指标
print(classification_report(y, y_pred, digits=3))

5000 轮次后,最终的预测结果如下:

              precision    recall  f1-score   support

           0      0.950     0.984     0.967       500
           1      0.946     0.978     0.962       500
           2      0.944     0.914     0.929       500
           3      0.921     0.914     0.918       500
           4      0.948     0.940     0.944       500
           5      0.919     0.904     0.911       500
           6      0.949     0.964     0.956       500
           7      0.950     0.942     0.946       500
           8      0.935     0.924     0.930       500
           9      0.926     0.924     0.925       500

    accuracy                          0.939      5000
   macro avg      0.939     0.939     0.939      5000
weighted avg      0.939     0.939     0.939      5000

如果要进行梯度检验,只需要将下述函数插入到 D = backProp() 后即可,正式训练时删去:

# Gradient Check
def gradCheck():
	for l in range(1, L):
		for i in range(s[l+1]):
			for j in range(s[l]+1):
				Theta[l][i, j] -= 0.0001
				J1 = J(forwardProp()[L])
				Theta[l][i, j] += 0.0002
				J2 = J(forwardProp()[L])
				Theta[l][i, j] -= 0.0001
				print(D[l][i, j], (J2 - J1) / 0.0002)
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值