卷积神经网络-卷积层

版权声明:署名-非商业性使用-相同方式共享

@@ Tags: 卷积神经网络;卷积层
@@ Date: 12/25/2025, 6:15:47

从数据以及算法层面上对卷积层做一个演示,理解卷积的过程以及反向传播的原理。

首先准备数据

import numpy as np
from termcolor import cprint

# 输入样本、卷积核、卷积层偏置项
sample = np.arange(16).reshape(4, 4)
filter = np.array([[-1, -2, 0], [1, 0, -2], [0, -1, 2]])
biases = np.array([5])

# 预设参数
N, channels = (1, 1)
H, W = sample.shape
FH, FW = filter.shape
padding = 0
stride = 1
out_h = (H + 2 * padding - FH) // stride + 1
out_w = (W + 2 * padding - FW) // stride + 1

cprint(f'sample:\n{sample}\n')
cprint(f'filter:\n{filter}\n', 'magenta')

输出:

sample:
[[ 0  1  2  3]
 [ 4  5  6  7]
 [ 8  9 10 11]
 [12 13 14 15]]

filter:
[[-1 -2  0]
 [ 1  0 -2]
 [ 0 -1  2]]

前向传播(卷积)过程

卷积核在输入图像上滑动,将每次滑动产生卷积窗口的数据拷贝出来,以便后面矩阵乘法计算卷积。

# 滑动步数(输出特征图的每个元素都是一个滑动的卷积窗口) * 卷积窗口
matrix = np.zeros((out_h, out_w,
                   FH, FW), np.int64)

# 在输入数据上滑动, 拷贝每个滑动窗口的数据到矩阵中
for i in range(out_h):
    for j in range(out_w):
        y = i * stride
        x = j * stride
        y_end = y + FH
        x_end = x + FW

        matrix[i, j] = sample[y:y_end, x:x_end]

cprint(f'matrix:\n{matrix}\n', 'green')

输出:

32mmatrix:
[[[[ 0  1  2]
   [ 4  5  6]
   [ 8  9 10]]

  [[ 1  2  3]
   [ 5  6  7]
   [ 9 10 11]]]


 [[[ 4  5  6]
   [ 8  9 10]
   [12 13 14]]

  [[ 5  6  7]
   [ 9 10 11]
   [13 14 15]]]]

将卷积窗口中的像素值与卷积核的权重执行点积,得到输出特征图

# 将矩阵表示的滑动窗口的数据重塑为行格式, 以便进行矩阵乘法
matrix = matrix.reshape(-1, FH * FW)
cprint(f'matrix:\n{matrix}\n', 'cyan')

# 将卷积核矩阵重塑为列格式
weight = filter.reshape(-1).T
cprint(f'weight:\n{weight}\n', 'magenta')

# 执行矩阵乘法实现卷积
output = np.dot(matrix, weight) + biases
cprint(f'biases:\n{biases}\n', 'green')
cprint(f'output:\n{output}\n', 'green')

# 将输出重塑为特征图的形状
output = output.reshape(-1, out_h, out_w)
cprint(f'output.reshape:\n{output}\n', 'green')

输出:

matrix:
[[ 0  1  2  4  5  6  8  9 10]
 [ 1  2  3  5  6  7  9 10 11]
 [ 4  5  6  8  9 10 12 13 14]
 [ 5  6  7  9 10 11 13 14 15]]

weight:
[-1 -2  0  1  0 -2  0 -1  2]

biases:
[5]

output:
[ 6  3 -6 -9]

output.reshape:
[[[ 6  3]
  [-6 -9]]]

以上就是卷积到输出特征图的过程。为了便于理解,我们忽略了 多样本、多通道的数据维度。

前向传播的数学表达

单通道卷积(基础形式)

设输入 sample 是一个二维矩阵 $X$,卷积核为 $W$,偏置为 $b$。输出特征图 $Y$ 的每一个元素 $y_{i,j}$ 的计算公式为(步幅=1,填充=0):

$$
y_{i,j} = \sum_{m=0}^{k-1} \sum_{n=0}^{k-1} (x_{i+m, j+n} \cdot w_{m,n}) + b
$$

  • $x_{i+m, j+n}$:输入图像在卷积窗口内的每一个像素。
  • $w_{m,n}$:卷积核在 $(m,n)$ 位置的权重。
  • $k$:卷积核的大小(假设为 $k \times k$)。

多通道卷积(实际工程形式)

在深度学习中,输入通常是三维的 通道x高x宽($C \times H \times W$),前向传播会涉及多个输入通道的累加。

假设:

  • 输入 $X$ 的维度为 $(C_{in}, H_{in}, W_{in})$
  • 卷积核 $W$ 的维度为 $(C_{out}, C_{in}, k, k)$
  • 输出 $Y$ 的维度为 $(C_{out}, H_{out}, W_{out})$

第 $f$ 个输出特征图(Feature Map)的计算公式为:

$$
Y_f = \sum_{c=1}^{C_{in}} (X_c \star W_{f,c}) + b_f
$$

其中 $\star$ 表示二维卷积操作。这意味着:

  • 每一个输出通道都是所有输入通道与对应卷积核参数卷积后的总和。
  • $C_{out}$(输出通道数)本质上就是这一层所使用的卷积核(Filters/Kernels)的数量。

或者更详细的公式:

$$
Y[c_{\text{out}}, i, j] = \sum_{c_{\text{in}}=0}^{C_{\text{in}}-1} \sum_{m=0}^{k-1} \sum_{n=0}^{k-1} X[c_{\text{in}}, i+m, j+n] \cdot W[c_{\text{out}}, c_{\text{in}}, m, n] + B[c_{\text{out}}]
$$

im2col、col2im函数

经过上面的步骤,可知卷积算法的过程实际上分为两步:

  1. 将输入图片按照卷积窗口展开
  2. 将展开的数据矩阵 与 权重矩阵进行矩阵乘法

因此,提供了 im2colcol2im 函数,用于将输入数据进行展开和还原。

im2col函数和col2im函数的实现如下:

def im2col(input_data, filter_h, filter_w, stride=1, pad=0):
    """
    将输入图像转换为列矩阵,以便于后续的矩阵乘法运算。

    参数:
    input_data: 输入数据,形状为(N, C, H, W)
    stride : 卷积步长
    pad : 零填充大小
    return: 转换后的二维矩阵, 形状为(N * out_h * out_w, C * filter_h * filter_w)
    """
    N, C, H, W = input_data.shape
    out_h = (H + 2 * pad - filter_h) // stride + 1
    out_w = (W + 2 * pad - filter_w) // stride + 1

    # 填充 (Padding)
    img = np.pad(input_data, [(0, 0), (0, 0),
                 (pad, pad), (pad, pad)], 'constant')

    # 每一行的大小是: 通道数 * 卷积核高 * 卷积核宽
    # 这里的形状定义只是为了方面下面的拷贝操作, 最后会进行维度重排
    col = np.zeros((N, C, filter_h, filter_w, out_h, out_w))

    for y in range(filter_h):
        y_max = y + stride * out_h
        for x in range(filter_w):
            x_max = x + stride * out_w
            col[:, :, y, x, :, :] = img[:, :, y:y_max:stride, x:x_max:stride]

    # 转置并重塑
    return col.transpose(0, 4, 5, 1, 2, 3).reshape(N * out_h * out_w, -1)


def col2im(col, input_shape, filter_h, filter_w, stride=1, pad=0):
    """
    将列矩阵转换回图像格式,用于反向传播

    参数:
    col: 展开后的列矩阵, 形状为 (N * out_h * out_w, C * filter_h * filter_w)
    input_shape: 原始数据的形状 (N, C, H, W)

    返回: 重建的图像(把col中卷积窗口的值“累加”回来, 因此输出与输入是不同的: col2im(im2col(x)) ≠ x)
    """
    N, C, H, W = input_shape
    out_h = (H + 2 * pad - filter_h) // stride + 1
    out_w = (W + 2 * pad - filter_w) // stride + 1

    # 重塑并转置回原始维度顺序
    col = col.reshape(N, out_h, out_w, C, filter_h,
                      filter_w).transpose(0, 3, 4, 5, 1, 2)

    img = np.zeros((N, C, H + 2 * pad + stride - 1, W + 2 * pad + stride - 1))

    for y in range(filter_h):
        y_max = y + stride * out_h
        for x in range(filter_w):
            x_max = x + stride * out_w
            # 关键:累加重叠部分的梯度
            img[:, :, y:y_max:stride, x:x_max:stride] += col[:, :, y, x, :, :]

    # 切除 padding 部分
    return img[:, :, pad:H + pad, pad:W + pad]
images = sample.reshape(1, 1, H, W)
cprint(f'images:\n{images}\n')

col = im2col(images, FH, FW)
cprint(f'col:\n{col}\n')

img = col2im(col, images.shape, FH, FW)
cprint(f'img:\n{img}\n', 'yellow')

输出:

images:
[[[[ 0  1  2  3]
   [ 4  5  6  7]
   [ 8  9 10 11]
   [12 13 14 15]]]]

col:
[[ 0.  1.  2.  4.  5.  6.  8.  9. 10.]
 [ 1.  2.  3.  5.  6.  7.  9. 10. 11.]
 [ 4.  5.  6.  8.  9. 10. 12. 13. 14.]
 [ 5.  6.  7.  9. 10. 11. 13. 14. 15.]]

img:
[[[[ 0.  2.  4.  3.]
   [ 8. 20. 24. 14.]
   [16. 36. 40. 22.]
   [12. 26. 28. 15.]]]]

观察imagesimg的结果可以发现:

中间像素被卷积窗口覆盖了多次,在 col2im 中被多次累加了。 而边角像素只被覆盖一次,所以它们是一样的。

因此, 对于边缘非边角元素被累加两次, 中间元素被累加四次。

关于输出的形状, 可能与其他实现存在差异,但不影响逻辑。


再看看完整的RGB图像的数据样本:

cprint(f'RGB三通道图片:\n')
C = 3  # RGB 三通道
img = np.arange(1*C*H*W).reshape(1, C, H, W)
col = im2col(img, FH, FW)
# cprint(f'原始数据:\n{img}\n')
cprint(f'卷积窗口数据:\n{col}\n', 'green')

FN = 2  # 卷积核个数
kernel = np.arange(FN*C*FH*FW).reshape(FN, C, FH, FW)
weight = kernel.reshape(FN, -1).T
# cprint(f'卷积核:\n{kernel}\n', 'red')
# cprint(f'权重值:\n{weight}\n', 'red')
output = np.dot(col, weight)
cprint(f'特征数据:\n{output}\n', 'yellow')

# 转换为特征图
out_h, out_w = (2, 2)  # 特征图大小
output = output.reshape(N, out_h, out_w, -1)
output = output.transpose(0, 3, 1, 2)
cprint(f'特征图:\n{output}\n', 'red')

输出:

RGB三通道图片:

卷积窗口数据:
[[ 0.  1.  2.  4.  5.  6.  8.  9. 10. 16. 17. 18. 20. 21. 22. 24. 25. 26.
  32. 33. 34. 36. 37. 38. 40. 41. 42.]
 [ 1.  2.  3.  5.  6.  7.  9. 10. 11. 17. 18. 19. 21. 22. 23. 25. 26. 27.
  33. 34. 35. 37. 38. 39. 41. 42. 43.]
 [ 4.  5.  6.  8.  9. 10. 12. 13. 14. 20. 21. 22. 24. 25. 26. 28. 29. 30.
  36. 37. 38. 40. 41. 42. 44. 45. 46.]
 [ 5.  6.  7.  9. 10. 11. 13. 14. 15. 21. 22. 23. 25. 26. 27. 29. 30. 31.
  37. 38. 39. 41. 42. 43. 45. 46. 47.]]

特征数据:
[[10197. 25506.]
 [10548. 26586.]
 [11601. 29826.]
 [11952. 30906.]]

特征图:
[[[[10197. 10548.]
   [11601. 11952.]]

  [[25506. 26586.]
   [29826. 30906.]]]]

对比上一个单通道数据, 可以看出 im2col 函数输出的二维数据中, 每一行 表示样本在卷积核中滑动时的窗口数据

如果有多个通道, 则同时包含多个通道的窗口数据。 本质上就是为了满足后面的矩阵运算, 因此需要先构建数据。

再看特征数据部分, 每一行是卷积核在当前滑动的卷积窗口中的特征值, 列是不同的卷积核。

通过特征数据变形再重排后, 就可以得到卷积核对应的特征图了。

特征图浓缩了整个输入样本相对于卷积核的特性信息,相当于数据被卷积核筛选出来的结果,因此卷积核又叫过滤器

卷积层的实现

class ConvolutionalLayer:
    """卷积层实现"""

    def __init__(self, weights, biases, stride=1, padding=0):
        """
        初始化卷积层

        参数:
        weights : 卷积核权重,形状为(输出通道数, 输入通道数, 高度, 宽度)
        biases : 偏置项,形状为(输出通道数,)
        stride : 卷积步长
        padding : 零填充大小
        """
        self.weights = weights
        self.biases = biases
        self.stride = stride
        self.padding = padding

        # 中间变量(用于反向传播)
        self.x = None
        self.col = None
        self.colWeights = None

        # 参数的梯度
        self.dweights = None
        self.dbiases = None

    def forward(self, input_data):
        """
        前向传播

        参数:
        input_data : 输入数据,形状为(样本大小, 通道数, 高度, 宽度)

        返回:
        output : 卷积核对应的特征图,形状为(N, FN, out_h, out_w)
        """
        FN, C, FH, FW = self.weights.shape
        N, C, H, W = input_data.shape

        # 计算输出特征图的大小
        out_h = (H + 2 * self.padding - FH) // self.stride + 1
        out_w = (W + 2 * self.padding - FW) // self.stride + 1

        # 将输入图像转换为行矩阵
        row_matrix = im2col(input_data, FH, FW, self.stride, self.padding)

        # 将权重矩阵重塑为列格式
        column_matrix = self.weights.reshape(FN, -1).T

        # 执行矩阵乘法实现卷积
        output = np.dot(row_matrix, column_matrix) + self.biases

        # 将输出重塑为正确的4维格式
        output = output.reshape(N, out_h, out_w, -1)
        output = output.transpose(0, 3, 1, 2)

        # 保存中间变量用于反向传播
        self.x = input_data
        self.col = row_matrix
        self.colWeights = column_matrix

        return output

    def backward(self, dout):
        """
        反向传播

        参数:
        dout : 输出层的梯度, 形状(N, FN, out_h, out_w)

        返回:
        gradient_input : 输入层的梯度
        """
        FN, C, FH, HW = self.weights.shape

        # 调整梯度输出的形状, 通道调整到最后
        dout = dout.transpose(0, 2, 3, 1).reshape(-1, FN)

        # 计算偏置项的梯度, 每个特征图的梯度求和作为该卷积核的偏置
        self.dbiases = np.sum(dout, axis=0)

        # 计算权重的梯度
        self.dweights = np.dot(self.col.T, dout)
        self.dweights = self.dweights.transpose(1, 0).reshape(FN,
                                                              C,
                                                              FH,
                                                              HW)

        # 计算输入数据的梯度
        dcol = np.dot(dout, self.colWeights.T)
        dx = col2im(dcol,
                    self.x.shape,
                    FH,
                    HW,
                    self.stride,
                    self.padding)
        return dx


# -------------------------------------------------------------------
# 构造输入
x = np.random.randn(2, 3, 5, 5)        # N=2, C=3
w = np.random.randn(4, 3, 3, 3)        # FN=4
b = np.random.randn(4)

conv = ConvolutionalLayer(w, b, stride=1, padding=1)

# 前向传播
out = conv.forward(x)

# 注意输出格式为: (N, FN, out_h, out_w)
# 这表示每个样本的
print("output shape:", out.shape)  # (2, 4, 5, 5)

# 构造上游梯度
dout = np.random.randn(*out.shape)

# backward
dx = conv.backward(dout)
print("dx shape:", dx.shape)
print("dw shape:", conv.dweights.shape)
print("db shape:", conv.dbiases.shape)

输出:

output shape: (2, 4, 5, 5)
dx shape: (2, 3, 5, 5)
dw shape: (4, 3, 3, 3)
db shape: (4,)

def numerical_gradient(conv, x, dout, eps=1e-5):
    grad_w = np.zeros_like(conv.weights)

    it = np.nditer(conv.weights, flags=['multi_index'], op_flags=['readwrite'])
    while not it.finished:
        idx = it.multi_index
        old = conv.weights[idx]

        conv.weights[idx] = old + eps
        out1 = conv.forward(x)
        loss1 = np.sum(out1 * dout)

        conv.weights[idx] = old - eps
        out2 = conv.forward(x)
        loss2 = np.sum(out2 * dout)

        grad_w[idx] = (loss1 - loss2) / (2 * eps)
        conv.weights[idx] = old
        it.iternext()

    return grad_w


# 数值梯度 vs 反向传播
num_dw = numerical_gradient(conv, x, dout)
max_diff = np.max(np.abs(num_dw - conv.dweights))
print("max diff:", max_diff)

输出:

max diff: 2.0845973836003395e-09

反向传播的数学表达

反向传播的主要目标是计算两个梯度:

  • 损失函数对输入的梯度(用于将误差传递给前一层)。
  • 损失函数对卷积核参数的梯度(用于更新当前层的权重)。

在卷积层的前向传播中,输入矩阵 $X \in \mathbb{R}^{C_{\text{in}} \times H_{\text{in}} \times W_{\text{in}}}$ 与卷积核 $W \in \mathbb{R}^{C_{\text{out}} \times C_{\text{in}} \times k_H \times k_W}$ 进行卷积得到输出 特征图 $Y \in \mathbb{R}^{C_{\text{out}} \times H_{\text{out}} \times W_{\text{out}}}$。

在反向传播时,我们已经从下一层拿到了输出梯度($\frac{\partial L}{\partial Y}$),通常被称为“误差项”或 $\delta$。

$$
\frac{\partial L}{\partial Y} \in \mathbb{R}^{C_{\text{out}} \times H_{\text{out}} \times W_{\text{out}}}
$$

误差或损失函数为:

$$
L = Loss(Y)
$$

根据 前向传播的公式:

$$
Y = \sum_{c=1}^{C_{in}} (X \star W) + b
$$

分别计算 $\frac{\partial L}{\partial W}$ 、$\frac{\partial L}{\partial X}$ 和 $\frac{\partial L}{\partial b}$。

对权重 $W$ 求导(更新参数)

根据链式法则,对于权重 $W[c_{\text{out}}, c_{\text{in}}, i, j]$:

$$
\frac{\partial L}{\partial W[c_{\text{out}}, c_{\text{in}}, i, j]} = \sum_{m=0}^{H_{\text{out}}-1} \sum_{n=0}^{W_{\text{out}}-1} \frac{\partial L}{\partial Y[c_{\text{out}}, m, n]} \cdot \frac{\partial Y[c_{\text{out}}, m, n]}{\partial W[c_{\text{out}}, c_{\text{in}}, i, j]}
$$

从前向公式可知:

$$
\frac{\partial Y[c_{\text{out}}, m, n]}{\partial W[c_{\text{out}}, c_{\text{in}}, i, j]} = X[c_{\text{in}}, m+i, n+j]
$$

因此:

$$
\frac{\partial L}{\partial W[c_{\text{out}}, c_{\text{in}}, i, j]} = \sum_{m=0}^{H_{\text{out}}-1} \sum_{n=0}^{W_{\text{out}}-1} \frac{\partial L}{\partial Y[c_{\text{out}}, m, n]} \cdot X[c_{\text{in}}, m+i, n+j]
$$

3.2 向量化形式(互相关运算)

这等价于对每个输出通道 $c_{\text{out}}$ 和输入通道 $c_{\text{in}}$,执行以下互相关运算:

$$
\frac{\partial L}{\partial W[c_{\text{out}}, c_{\text{in}}]} = X[c_{\text{in}}] \star \frac{\partial L}{\partial Y[c_{\text{out}}]}
$$

完整公式
$$
\boxed{\frac{\partial L}{\partial W} = X \star \frac{\partial L}{\partial Y}}
$$

其中 $\star$ 是互相关运算,维度变化:

  • $X$: $(C_{\text{in}}, H_{\text{in}}, W_{\text{in}})$
  • $\frac{\partial L}{\partial Y}$: $(C_{\text{out}}, H_{\text{out}}, W_{\text{out}})$
  • $\frac{\partial L}{\partial W}$: $(C_{\text{out}}, C_{\text{in}}, k_H, k_W)$

对输入 $X$ 求导(传递误差)

为了让误差继续向前传播,我们需要计算 $\frac{\partial L}{\partial X}$。

对于输入元素 $X[c_{\text{in}}, h_x, w_x]$:

$$
\frac{\partial L}{\partial X[c_{\text{in}}, h_x, w_x]} = \sum_{c_{\text{out}}=0}^{C_{\text{out}}-1} \sum_{h=0}^{H_{\text{out}}-1} \sum_{w=0}^{W_{\text{out}}-1} \frac{\partial L}{\partial Y[c_{\text{out}}, h, w]} \cdot \frac{\partial Y[c_{\text{out}}, h, w]}{\partial X[c_{\text{in}}, h_x, w_x]}
$$

注意

  • $h$、$w$ 表示输出特征图的行、列,也可以视为滑动卷积窗口在输入元素内部的偏移量
  • $h_x$、$w_x$ 表示输入元素内的实际坐标。
  • $k_H$、$k_W$ 表示卷积核的宽、高。

从前向公式可知,只有当满足以下条件时,偏导数非零:

$$
\frac{\partial Y[c_{\text{out}}, h, w]}{\partial X[c_{\text{in}}, h_x, w_x]} =
\begin{cases}
W[c_{\text{out}}, c_{\text{in}}, h_x-h, w_x-w] & \text{if } 0 \leq h_x-h < k_H \text{ and } 0 \leq w_x-w < k_W \\
0 & \text{otherwise}
\end{cases}
$$

令 $i = h_x - h$, $j = w_x - w$,则:
$$
\frac{\partial L}{\partial X[c_{\text{in}}, h_x, w_x]} = \sum_{c_{\text{out}}=0}^{C_{\text{out}}-1} \sum_{i=0}^{k_H-1} \sum_{j=0}^{k_W-1} \frac{\partial L}{\partial Y[c_{\text{out}}, h_x-i, w_x-j]} \cdot W[c_{\text{out}}, c_{\text{in}}, i, j]
$$

注意:$i$、$j$ 可以看作是滑动卷积窗口内部元素距离窗口左上角的偏移,因此它们的取值范围是 $[0, K]$。

4.2 向量化形式(全卷积运算)

这等价于一个全卷积(full convolution)运算:
$$
\frac{\partial L}{\partial X[c_{\text{in}}]} = \sum_{c_{\text{out}}=0}^{C_{\text{out}}-1} \text{full_conv}\left( \frac{\partial L}{\partial Y[c_{\text{out}}]}, \text{rot180}(W[c_{\text{out}}, c_{\text{in}}]) \right)
$$

其中 $\text{rot180}(\cdot)$ 表示将矩阵旋转180度(即上下翻转+左右翻转),$\text{full_conv}$ 表示全卷积。

等价形式(使用互相关和填充)
$$
\boxed{\frac{\partial L}{\partial X} = \text{pad}\left(\frac{\partial L}{\partial Y}\right) \star \text{rot180}(W)}
$$

或者使用转置卷积表示:
$$
\boxed{\frac{\partial L}{\partial X} = \text{transposed_conv}\left(\frac{\partial L}{\partial Y}, W\right)}
$$

维度变化:

  • $\frac{\partial L}{\partial Y}$: $(C_{\text{out}}, H_{\text{out}}, W_{\text{out}})$
  • $W$: $(C_{\text{out}}, C_{\text{in}}, k_H, k_W)$
  • $\frac{\partial L}{\partial X}$: $(C_{\text{in}}, H_{\text{in}}, W_{\text{in}})$

对偏置 $b$ 求导

偏置项的计算最简单。由于一个卷积核通常共享一个偏置,因此偏置的梯度就是对应输出梯度图(Feature Map)中所有元素的总和。

如果前向传播包含偏置:$Y = X \star W + b$,其中 $b \in \mathbb{R}^{C_{\text{out}}}$,则:

$$
\frac{\partial L}{\partial b[c_{\text{out}}]} = \sum_{m=0}^{H_{\text{out}}-1} \sum_{n=0}^{W_{\text{out}}-1} \frac{\partial L}{\partial Y[c_{\text{out}}, m, n]} \cdot \frac{\partial Y[c_{\text{out}}, m, n]}{\partial b[c_{\text{out}}]}
$$

由于 $\frac{\partial Y[c_{\text{out}}, m, n]}{\partial b[c_{\text{out}}]} = 1$:

$$
\boxed{\frac{\partial L}{\partial b[c_{\text{out}}]} = \sum_{m=0}^{H_{\text{out}}-1} \sum_{n=0}^{W_{\text{out}}-1} \frac{\partial L}{\partial Y[c_{\text{out}}, m, n]}}
$$

向量化形式:

$$
\boxed{\frac{\partial L}{\partial b} = \sum_{m,n} \frac{\partial L}{\partial Y[:, m, n]}}
$$