ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

卷积神经网络入门:卷积核、步长与填充及PyTorch实现

卷积神经网络入门:卷积核、步长与填充及PyTorch实现 很多人第一次接触深度学习时最容易被吓退的地方就是“卷积”这个词。我印象特别深早几年在技术社区给新人解答问题十个里有八个看到卷积神经网络的示意图都会问这跟神经有什么关系卷积听着怎么像高数里的积分变换再翻到公式看到h(x)∫f(τ)g(x-τ)dτ这种符号基本就直接关页面了。但实际情况是如果你做图像识别、目标检测、语音处理这类任务卷积就是那个你绕不开的核心操作。它本质上做了一件非常简单的事用一个小窗口在数据上滑来滑去每滑到一个位置就做一次对应位置相乘再相加的计算从而提取出局部特征。就这么个事没有任何玄学。这篇文章我就跟你把卷积这件事彻底讲明白。我不打算用教科书式的方式开讲而是从我们实际写代码、调模型时会遇到的每个细节入手卷积核怎么理解、步长填充到底影响什么、输出特征图的尺寸怎么算再到转置卷积、膨胀卷积、深度可分离卷积这些进阶概念通通用大白话加真实代码过一遍。看完你至少能保证两件事一是别人再聊卷积你不慌二是自己用PyTorch搭一个卷积网络时每一层的输出形状你心里都有数。1. 先搞明白卷积到底在干什么从“盖章”说起1.1 卷积就是一个带着权重的小窗口在数据上滑动想象你手里有一个正方形印章章面上刻着3×3的数字。你的任务是把这枚章盖到一张大照片上但不是随便盖而是从照片左上角开始每盖一个位置就往右挪一格或者挪几格盖满整张照片。每盖一个位置你就把章上的9个数字和它正下方压住的照片区域里的9个像素值一对一做乘法然后把9个乘积加起来得到一个数填到输出结果对应的位置上。这个印章就是卷积核Kernel也叫滤波器。照片就是输入数据。印章盖出来的那一大堆结果就是特征图Feature Map。那为什么这种操作能提取特征关键就在印章上那9个数字。数字不是随便填的它们组合在一起就代表一种你要找的局部模式。举个例子如果这个卷积核的9个数字正好能识别出一条垂直边缘那它在照片上所有“有垂直边缘的地方”都会计算出很大的值在没边缘的地方算出接近0的值。这一路盖下来输出的特征图里亮点集中的地方就是垂直边缘的位置。这个过程跟我们在高中学过的矩阵点乘完全不同它不需要输入和卷积核尺寸完全一致而是核小得多走一步算一步像手电筒在黑屋子里扫一圈一样每次只看眼前那一小片。这是卷积的第一个核心直觉局部连接 权值共享。局部连接是说每次只连接输入的一小块权值共享是说那一个印章从头到尾用的都是同一组数字同一个卷积核不管盖到哪个位置。1.2 那个看起来很吓人的公式拆开就三步回到开头的那个积分公式。先别管积分符号卷积公式的本质就三句话翻转只在数学定义里有深度学习里一般不做这一步直接对应位置相乘。相乘把核里的权重和被覆盖区域内的像素一一相乘。求和把相乘的结果全加起来得到输出中的一个值。在信号处理里“翻转”是有物理意义的代表系统的脉冲响应反着扫。但深度学习的卷积和信号处理里的卷积严格说不是一回事深度学习用的是**互相关Cross-Correlation**操作只有乘加没有翻转。当初深度学习框架干脆沿用了“卷积”这个名字结果把很多人都绕晕了。所以在神经网络里你可以把卷积当成一个很单纯的数学操作输入一个小矩阵和另一个更小的矩阵做对应位置相乘再求和得到一个数。这个操作反复做直到覆盖完全部输入就得到一张特征图。仅此而已。1.3 用人话重新表述卷积的完整流程一个标准卷积操作从输入到输出实际上是这样一步步发生的假设输入是一张尺寸为 6×6 的单通道图像比如灰度图。准备一个 3×3 的卷积核里面的权重是模型训练时不断学习的初始化时随机给。把卷积核放在图像左上角覆盖 3×3 区域。把覆盖区域里的 9 个像素值和卷积核里的 9 个权重对应相乘再相加得到一个输出值。卷积核向右滑动一定距离这个距离叫步长后面细说重复步骤3直到扫完第一行。卷积核回到最左边向下滑动一定距离重复步骤3直到整个图像都扫完。把所有输出值拼在一起形成一个新的二维矩阵就是这一层的特征图。这个特征图再作为下一层的输入继续做同样的操作。很多层堆叠起来就构成了一个卷积神经网络。早期层能提取到边缘、颜色、纹理这类低级特征越往后层越能组合出眼睛、轮子、人脸这类高级语义特征。2. 卷积核、步长、填充、通道四个参数一次讲透接下来这四个概念是卷积里最基础也最容易搞混的四个参数。我把它们放在一起讲因为它们共同决定了卷积输出的形状。2.1 卷积核尺寸越大看越广但代价也越高卷积核大小是第一个要选的参数。最常见的是 3×3其次是 5×5、1×1。核越大每次能看到的输入区域就越大提取的特征范围也就越广。听起来似乎大核更好不是的。3×3 的核有两个巨大的优势。第一参数量少两个 3×3 卷积堆叠的感受野等于一个 5×5 卷积但参数量 2×3×318 小于 5×525还有更多非线性层表达能力反而更强。第二堆叠小核能让网络更深特征表达更丰富。所以现在的卷积网络基本默认 3×3。5×5 以上只在特定场景比如某些轻量网络里才用。1×1 卷积值得单独说一下。它不提取局部空间特征它的作用是跨通道的信息融合。比如你把一个 64 通道的特征图用 32 个 1×1 卷积核去卷输出的就是 32 通道的特征图。相当于在不改变空间尺寸的前提下对通道维度做了压缩或扩张这个操作在瓶颈结构里非常常用。2.2 步长决定输出图缩多小步长Stride是卷积核每次滑动的距离。步长为1就是一格一格地滑动输出特征图的尺寸几乎和输入一致再考虑一下填充。步长为2每次跳两格输出特征图的尺寸大约减半。步长带来的直观感受就是“下采样”。我们做图像分类时希望深层的特征图越来越小、通道越来越多这时用步长2的卷积就能比池化更优雅地实现降采样。因为池化没有可学习的参数而步长为2的卷积在下采样的同时还能学到该保留什么信息。我在实际项目中经常直接用步长2的卷积代替池化效果通常会好一点但代价是计算量会高一些。2.3 填充处理边缘信息的补偿方案填充Padding解决两个问题。第一个问题是卷积核盖不到边缘角落和边缘的像素参与计算的次数比中心少得多边缘信息等于天然被削弱了。第二个问题是每次卷积操作输出尺寸都会变小层数一多图就没了。填充的做法就是在输入矩阵的周围补一圈数字最常见的是补0Zero Padding。补一圈的效果是6×6 的输入配合 3×3 卷积核输出还是 6×6尺寸不缩水。如果步长为2通常适当填充后输出的尺寸正好是原来的一半这个操作就叫 SAME Padding输出尺寸等于输入尺寸除以步长。不填充的话叫 VALID Padding。这里有个小经验补0操作几乎没有成本所以 SAME Padding 在工程里是绝对主流。只有在某些特殊结构里比如需要精确控制特征图尺寸对齐时才会用 VALID。2.4 多通道卷积从灰度图到彩色图的关键一跃真实图像基本是彩色图也就是3通道RGB。那卷积核怎么处理多通道方法很简单卷积核的通道数必须和输入的通道数一致。比如输入是 6×6×3那每个卷积核的尺寸就是 3×3×3。计算时卷积核同时在3个通道上滑动每次把 3×3×327 个数和对应位置的像素相乘再求和得到一个输出值。那么这一层如果有 N 个卷积核就会得到 N 张特征图也就是输出的通道数为 N。所以有一条非常好记的公式输出特征图的通道数等于这一层卷积核的个数。我培养新同事时最常让他们反复默念的一句话就是“卷积核数量决定输出通道数卷积核大小和步长填充决定特征图尺寸。”2.5 输出尺寸计算公式全员背诵这是整个卷积操作里最实用的一条公式。假设输入尺寸为 W×H卷积核大小为 K填充为 P单边补的像素数步长为 S那么输出特征图的边长 W_out 为W_out ((W - K 2P) / S) 1如果除法除不尽就向下取整。H 同理。举个例子输入 224×224卷积核 3×3步长1填充1那输出就是 ((224 - 3 2)/1)1 224尺寸不变。输入 224×224卷积核 3×3步长2填充1那输出就是 ((224 - 3 2)/2)1 112正好减半。我建议你把这条公式抄在笔记本第一页后面所有跟图像尺寸有关的坑基本都是这条公式没算明白导致的。3. 用PyTorch亲手写一个卷积层从代码理解一切3.1 最小可运行示例一行代码创建卷积层纸上谈兵再多不如亲手跑一段代码。用PyTorch创建一个卷积层非常简单import torch import torch.nn as nn # 创建卷积层输入通道3RGB图像输出通道16卷积核3x3步长1填充1 conv_layer nn.Conv2d(in_channels3, out_channels16, kernel_size3, stride1, padding1) # 模拟一张输入图片batch_size43通道32x32 x torch.randn(4, 3, 32, 32) # 前向传播 out conv_layer(x) print(out.shape) # 输出torch.Size([4, 16, 32, 32])你看PyTorch里张量的形状约定是(batch_size, channels, height, width)。输入是 (4, 3, 32, 32)输出是 (4, 16, 32, 32)通道从3变成了16高度宽度没变因为 padding1 补回了一圈。第一次写代码的人最容易犯的错是把通道数放在最后像 TensorFlow 早期版本那样但PyTorch只认“通道在第二位”的布局。如果你报错的时候看到什么Expected 3D or 4D tensor八成就是维度顺序搞反了。3.2 从输入到输出的形状推导全过程我们拿一个稍微复杂点的例子练手输入还是 (4, 3, 32, 32)卷积核 5×5步长2填充0。套公式W_out ((32 - 5 0)/2) 1 (27/2) 1 13 1 14。注意27/2在整数除法里是13不是13.5。所以输出是 (4, 16, 14, 14)如果你的网络后面想再拼接一个反卷积或者其他结构这个14的奇偶性有时候会影响后续结构设计。再演示一下多通道的计算过程。输入 (4, 3, 32, 32)第一个卷积核的尺寸实际是 (3, 5, 5)也就是 3 个通道各自的 5×5 权重。这个卷积核滑过输入所有位置时每个位置都是在 3 个通道的对应局部区域上做乘加最终得到一个标量。这一层如果有 16 个这样的核就会得到 16 个特征图合并成 (4, 16, 14, 14)。3.3 自己动手看卷积核学到的到底是什么代码能跑通只是第一步。很多人跑完就完事了但从没看过卷积核里到底存了什么。这里我强烈建议你做一个可视化实验随便拿一个预训练好的卷积网络比如ResNet18PyTorch官方有现成的权重把第一层卷积的权重提取出来打印出来看。import torchvision.models as models model models.resnet18(pretrainedTrue) first_conv model.conv1 # 这是一个 nn.Conv2d7x7卷积核 weights first_conv.weight # 形状是 (64, 3, 7, 7) print(weights.shape)这64个7×7的卷积核每一组数字翻译成图像你会发现有些核专门检测水平纹理有些检测垂直边缘有些检测颜色差异。这就是卷积网络的“直觉”来源。训练好的网络第一层基本都长得像边缘检测器或者颜色块检测器这个现象在多个网络里都极为一致。3.4 反卷积视角为什么卷积层能叠加出抽象特征理解完单层卷积再看网络的整体行为就不难了。第一层卷积输出的是边缘、纹理第二层卷积在第一层的输出上继续滑动它看到的局部区域比第一层更广能组合边缘变成角、轮廓、局部图案第三层再往上就能组合成眼睛、轮子、窗户这类语义部件。这就是卷积网络从“像素”到“语义”的抽象过程。很多人误以为网络层数越深特征图里的像素越“高级”。其实更准确的说法是深层特征图里的每个像素对应着输入图像上更大的一块区域。这块区域的专业名词叫“感受野”。层数越深感受野越大看到的范围越广能理解的语义就越完整。4. 从卷积层到完整CNN池化、激活函数和网络整体结构4.1 池化把特征图浓缩提纯池化层在过去的CNN结构里几乎是标配现在虽然经常被步长卷积替代但理解它仍然很重要因为老模型和很多经典论文都绕不开。池化也是一种滑动窗口操作但和卷积的区别是池化没有任何可学习的参数只是简单地把窗口内的数值取最大值最大池化或平均值平均池化。最常用的是 2×2 窗口、步长2的最大池化它把 2×2 区域里的4个值浓缩成1个最大的值。最大池化保留了局部最强的特征给特征图带来了平移不变性物体稍微移动几个像素最大池化的结果基本不变。这个性质对分类任务非常有用。但是注意最大池化会丢掉精确的位置信息如果想做分割、检测这种需要空间定位的任务单纯的池化堆叠反而会坑了你这就是后来出现带孔卷积、跳过连接这类结构的原因之一。4.2 激活函数没有它多少层卷积都白搭如果卷积层后面直接接卷积层中间没有任何非线性变换那无论堆多少层数学上等价于一层线性变换。因为矩阵乘法叠加还是矩阵乘法你根本表达不了复杂的函数。所以每一层卷积之后都要接一个激活函数把线性变换的结果“掰弯”。现在最常用的还是 ReLUf(x) max(0, x)。正的保留负的全部归零。ReLU的好处是计算极快而且在正区间梯度恒定缓解了梯度消失的问题。它的变种 LeakyReLU 把负数部分也保留了一点点很小的斜率比如f(x) max(0.01x, x)防止负区间死亡。如果你搭的模型训练出来很多神经元的输出一直是0大概率就可以换成 LeakyReLU 试试。4.3 一个能跑的经典CNN结构长什么样把前面的模块拼起来一个标准的图像分类CNN大概长这样import torch.nn as nn class SimpleCNN(nn.Module): def __init__(self, num_classes10): super().__init__() self.features nn.Sequential( # 卷积块1 nn.Conv2d(3, 32, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.Conv2d(32, 32, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.MaxPool2d(kernel_size2, stride2), # 32x32 - 16x16 # 卷积块2 nn.Conv2d(32, 64, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.Conv2d(64, 64, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.MaxPool2d(kernel_size2, stride2), # 16x16 - 8x8 # 卷积块3 nn.Conv2d(64, 128, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.Conv2d(128, 128, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.MaxPool2d(kernel_size2, stride2), # 8x8 - 4x4 ) self.classifier nn.Sequential( nn.Flatten(), nn.Linear(128 * 4 * 4, 256), nn.ReLU(inplaceTrue), nn.Dropout(0.5), nn.Linear(256, num_classes), ) def forward(self, x): x self.features(x) x self.classifier(x) return x这个结构完全可以从一张 32×32 的CIFAR-10图片跑通输出10个类别的得分。观察一下每经过一次 MaxPool2d特征图的宽高减半而通道数翻倍。这是CNN最经典的“信道扩张、空间压缩”模式既能浓缩语义又不会丢失信息量。4.4 搭建网络时最容易忽略的尺寸匹配问题我给不少人看过代码最常见的报错就是mat1 and mat2 shapes cannot be multiplied。原因非常统一前面卷积部分的输出特征图经过 Flatten 展平后尺寸和全连接层的输入尺寸对不上。解决办法只有一个不要靠猜。在写全连接层之前先用一行代码把卷积部分的输出形状打印出来# 假设输入是 (3, 32, 32) dummy_input torch.randn(1, 3, 32, 32) dummy_out model.features(dummy_input) print(dummy_out.shape) # 看这里比如 torch.Size([1, 128, 4, 4])拿到128 * 4 * 4 2048再去写第一层全连接的输入维度。这个习惯看起来笨但在实际项目里能帮你省下大把排查时间。5. 转置卷积、膨胀卷积、深度可分离卷积变体们都在解决什么问题很多人学会基础卷积后看到各种“xx卷积”又开始慌了。其实这些变体一点都不神秘它们都是针对基础卷积的某个缺点做修补理解它们的动机比记住定义重要得多。5.1 转置卷积想把特征图变大怎么办普通卷积不断把图变小但有些任务需要把图变大。比如图像分割需要输出和原图一样大的逐像素分类图图像生成需要从一个随机向量先生成低分辨率特征图再放大成高清图。这时候就需要转置卷积Transposed Convolution也叫反卷积Deconvolution但“反卷积”这个叫法挺误导人的它不是卷积的逆运算而是尺寸上“反着来的卷积”。转置卷积的操作逻辑可以理解为输入特征图的每个像素值作为权重向外扩散映射到更大的特征图。它也有卷积核、步长、填充这些参数但计算方向相反。在PyTorch里对应的是nn.ConvTranspose2d。有个细节特别容易踩坑转置卷积的“棋盘格伪影”Checkboard Artifact。如果你用转置卷积做图像生成很容易在输出图上看到棋盘状的花纹尤其是卷积核尺寸能被步长整除时比如步长2、核4。解决方案通常是换核大小或用“先插值放大再用普通卷积”这种组合方式。我自己在实际项目里做上采样时更倾向于用nn.Upsample(modebilinear) nn.Conv2d而不是单独的转置卷积效果更稳。5.2 膨胀卷积在参数不变的情况下把视野做大膨胀卷积Dilated Convolution也叫空洞卷积。普通卷积核是挨个取点膨胀卷积则是在核的点与点之间插入空隙。比如膨胀率为2的 3×3 卷积核实际覆盖范围是 7×7但参与运算的只有9个点中间全是空的。这样一来感受野变大了但参数量和计算量几乎没变。这个特性非常适合需要多尺度上下文信息的任务比如语义分割、语音合成。它在PyTorch里只需要在nn.Conv2d里加一个参数dilation2。但它有个隐藏的坑膨胀率设太大卷积核之间的采样点太稀疏某些位置的特征根本采不到反而会伤害性能。常见做法是使用“混合膨胀卷积”例如1、2、5牛津组的HDC方案用一组不同的膨胀率叠加避免空洞重叠和局部信息缺失。5.3 深度可分离卷积移动端模型减算力的利器标准卷积的参数量是kernel_size × kernel_size × in_channels × out_channels。当通道数很大时这个量非常可观。深度可分离卷积Depthwise Separable Convolution的思路是把标准卷积拆成两步第一步逐通道卷积Depthwise Convolution每个输入通道用单独一个 2D 卷积核去卷输入有多少通道就做多少次独立的 2D 卷积。这一步不融合通道间信息。第二步逐点卷积Pointwise Convolution用 N 个 1×1 的卷积核把逐通道卷积的结果进行跨通道线性组合生成想要的输出通道数。这样做的参数量是kernel_size×kernel_size×in_channels in_channels×out_channels相比标准卷积少了很多倍。MobileNet 系列能火起来核心就是靠这个操作。在PyTorch里实现如下class DepthwiseSeparableConv(nn.Module): def __init__(self, in_channels, out_channels, kernel_size3): super().__init__() self.depthwise nn.Conv2d(in_channels, in_channels, kernel_size, paddingkernel_size//2, groupsin_channels) self.pointwise nn.Conv2d(in_channels, out_channels, kernel_size1) def forward(self, x): x self.depthwise(x) x self.pointwise(x) return x注意groupsin_channels这个参数它就是逐通道卷积的关键开关。这个结构在端侧设备、实时推理的模型设计里是躲不开的。5.4 三维卷积处理视频和医学影像的常规武器普通 2D 卷积处理的是 (高, 宽) 平面三维卷积3D Convolution则在 (高, 宽, 深度) 三个维度上滑动卷积核。这里的三维不是RGB通道而是真实的空间深度或者视频里的时间帧。比如一段长度为 T 帧的视频输入形状是 (C, T, H, W)3D卷积核的形状就是 (C, k_t, k_h, k_w)在时间维度上也会滑动。这样一次卷积就能同时提取空间特征和时间动态特征。视频行为识别、医学CT影像分割用到的就是这种结构。PyTorch 里对应nn.Conv3d。三维卷积的代价是参数量和计算量直接翻倍涨所以一般都会搭配更大的步长或者更小的输入分辨率否则显存瞬间爆掉。我第一次做视频分类时就是没注意这一点8G显存直接被一张 16帧 的 256×256 输入干爆后来老老实实把帧率降到4帧、分辨率降到112才跑起来。5.5 结构化数据上的卷积图卷积和球面卷积有时候数据不在规规矩矩的二维网格上。比如社交网络中每个人的朋友关系图、分子结构、街区的路网这些数据以图Graph的形式存在普通卷积没法直接处理。自适应图卷积Adaptive Graph Convolution就是在图的节点和边上定义“邻居聚合”操作把局部邻域的信息加权求和相当于在图上做卷积。球形卷积Spherical Convolution则是针对球面数据的卷积比如360度全景图、地球气象数据、蛋白质表面结构。由于球面没有规则的网格结构球面卷积需要重新定义“卷积核如何排列”和“邻居如何定义”比较有名的实现就是基于球谐函数展开的方法。这两个概念看起来离普通开发者很远但如果你做推荐系统、气象或者VR全景类的工作一定会遇上。6. 训练卷积网络时我踩过的坑和调参心得6.1 新手最常遇到的四个报错和对策维度不匹配就是前面说的全连接层输入和Flatten后尺寸对不上。解决打印shape。显存溢出OOM批量大小设太大或者图太大。解决调小 batch_size调小图片尺寸如果训练中途爆掉还可以用梯度累积。这里有个很实用的经验——把torch.cuda.empty_cache()在代码关键位置调用释放碎片显存虽然治标不治本但能让你在调试期多撑几个回合。学习率不合适损失函数变成NaN或者来回震荡不下降。解决先用lr1e-3起步观察损失下降速度如果是NAN就把学习率降到1e-4如果下降过于缓慢再调到3e-3。过拟合严重训练损失持续下降但验证损失开始上升。解决加数据增强随机裁剪、翻转、色彩抖动加Dropout减小模型容量提前停止Early Stopping。对于图像任务一个优秀的增强策略带来的提升通常比换模型架构还明显。6.2 关于卷积核尺寸和网络深度的个人经验我做图像分类和检测有一定年头了有两条配置方面的经验想单独分享。第一除非有明确的理由网络主体结构里统一用 3×3 卷积核下采样用 stride2 的卷积或池化上采样用双线性插值加卷积。这套组合是经过大量工作验证的无脑安全选择适合绝大多数任务。第二网络深度要跟着数据量走。数据量只有几千张的时候一个5层的卷积网络就敢充分拟合数据量几万张可以考虑ResNet18这个级别数据量百万级才轮到ResNet50以上。盲目加深网络在小数据集上表现往往更差不是因为你代码不对而是模型太复杂把噪声都学进去了。6.3 进阶初始化方式对收敛速度的影响这一条很多教程都不会讲但在实际项目中挺有用的。卷积层的权重初始化方式直接影响网络能不能顺利收敛。PyTorch里nn.Conv2d默认的初始化方式是Kaiming初始化也叫He初始化这是特意为配合ReLU激活函数设计的。不要自己做全0初始化那是绝对错误操作。如果你从别的框架迁移模型注意把初始化方式也对应过来。Xavier初始化和Kaiming初始化的区别就是前者假设激活函数是线性的适合tanh、sigmoid后者针对ReLU的非线性做了修正。用错初始化有时不会报错但训练速度会慢得让你怀疑人生。6.4 一张图理解卷积网络的“能力瓶颈”最后说一个我观察到的反直觉现象很多人以为卷积网络层数越深一定越好。但实际测试时你会发现超过一定深度后收益急剧下降甚至出现退化现象——训练集损失都降不下去。这不是梯度消失就是单纯的“优化困难”。ResNet通过残差连接解决了这个问题但你手写一个类似DeepNet的结构时还是有可能遇到。我的建议是先跑一个浅层模型作为基线确认它能过拟合到训练集比如准确率逼近100%再逐层加深。这样你能清楚地分辨模型不收敛是代码问题还是模型容量问题。这个排查技巧在我带新人的过程中几乎每次都派上用场。卷积这个东西当你把它拆成“一个小窗口滑来滑去乘乘加加”之后一切复杂名词都只是在这个基础上加了一点插空、跳步、拆分的花样。我做了这些年图像方向的工程回头看所有卷积变体心里只有一句话理解了窗口和滑动的本质花里胡哨的名字背后全是同一套逻辑。
返回列表