最近两年,只要打开计算机视觉方向的论文、招聘 JD 或者开源项目,Transformer 几乎是绕不开的关键词。很多同学从 CNN 转向 Vision Transformer 时,最大的困惑往往不是模型本身有多复杂,而是概念断层:自注意力机制到底在算什么?Q、K、V 分别是什么?图片为什么要切成 Patch?ViT 和 Swin Transformer 的核心差异到底在哪里?
这篇文章就围绕“Transformer 在计算机视觉中的应用”这条主线,把注意力机制、位置编码、ViT、Swin Transformer 从前到后过一遍。每个核心模块都会配上可运行的代码片段和关键参数说明,你可以在阅读论文原文之后,再对照这篇文章做二次理解。无论你是刚入门计算机视觉的初学者,还是想快速掌握 ViT 与 Swin 核心原理的开发者,这篇文章都值得收藏备用。
1. 背景:Transformer 是怎么跨界进入计算机视觉的
1.1 从 NLP 到 CV:Transformer 的迁移逻辑
Transformer 最早是 2017 年 Google 团队在论文《Attention Is All You Need》中提出的序列建模架构,初衷是解决机器翻译这类 NLP 任务。它和传统 RNN/LSTM 最大的区别在于:完全摒弃了循环结构,只依靠注意力机制来捕捉序列中任意两个位置之间的关系。
在 NLP 中,一个句子可以看成一组词的集合,每个词都是一个 Token。Transformer 对整句话并行处理,因此训练效率远高于按时间步展开的 RNN。后来人们发现,如果把图像也看成一组“视觉 Token”,那么 Transformer 完全可以应用到计算机视觉任务中。这就是 Vision Transformer(ViT)的基本出发点。
1.2 计算机视觉中的注意力机制有哪几种
在进入 Transformer 之前,先明确一个概念:注意力机制并不是 Transformer 独有的。计算机视觉中一直存在很多注意力变体:
- 通道注意力:例如 SE 模块,通过全局池化得到每个通道的重要性权重,再对特征图加权。
- 空间注意力:例如 CBAM 中的空间注意力模块,关注特征图中哪些空间位置更重要。
- 通道-空间混合注意力:例如 CBAM,同时融合通道与空间两个维度的注意力。
- 自注意力:Transformer 使用的核心机制,通过输入特征本身计算 Q、K、V,建模任意两个位置之间的依赖关系。
SE 和 CBAM 更多是作为轻量插件插入到 CNN 中,而 Transformer 的自注意力则是模型的主干结构,二者适用范围和计算逻辑都不相同。后面会单独展开说明。
1.3 为什么不是直接拿 CNN 堆更深
CNN 通过卷积核的局部感受野提取特征,堆叠层数后可以扩大感受野,但这种扩张是隐式的、有限的。对于图像中距离较远的两个区域,CNN 需要经过很多层才能建立联系。而 Transformer 的自注意力机制在每一层都直接计算全局关系,理论上无视距离。
这种全局建模能力,让 Transformer 在图像分类、目标检测、语义分割等任务上表现出很强的上限。但代价是计算量较大,尤其当 Token 数量增多时,注意力矩阵的大小会平方增长。这个问题直接催生了 Swin Transformer 的窗口注意力设计。
2. 注意力机制核心原理与代码
2.1 Q、K、V:自注意力到底在算什么
自注意力可以通俗理解为:对一组输入向量,让每个向量都去和其他所有向量做相似度计算,再根据相似度加权聚合信息。
具体来说,每个输入向量 x 会通过三个可学习矩阵分别映射为:
- Query(Q):表示“我”想查询什么。
- Key(K):表示“我”能提供什么索引信息。
- Value(V):表示“我”实际携带的内容。
计算流程是:
- 对每一个 Query,计算它与所有 Key 的点积相似度。
- 将相似度除以缩放因子,一般是向量维度的平方根。
- 经过 Softmax 归一化,得到注意力权重。
- 用注意力权重对所有 Value 做加权求和,得到当前位置的输出。
2.2 多头注意力:从多个子空间并行学习
多头注意力就是把嵌入维度切成多份,每一份称为一个“头”,每个头独立执行自注意力计算,最后把多个头的输出拼接起来。
为什么要这样做?因为单头注意力只能学习一种注意力分布模式,而多头机制允许模型同时关注不同类型的依赖关系。例如一个头关注局部纹理,另一个头关注全局轮廓。ViT 中常见的配置是 12 个头,每个头的维度是 64,嵌入维度为 768。
2.3 PyTorch 实现一个自注意力模块
下面用 PyTorch 实现一个简化版的自注意力模块。这里给出的是教学版本,重点是演示 Q、K、V 的计算流程。
import torch import torch.nn as nn import torch.nn.functional as F class SelfAttention(nn.Module): def __init__(self, embed_dim, dropout=0.1): super().__init__() self.q_proj = nn.Linear(embed_dim, embed_dim) self.k_proj = nn.Linear(embed_dim, embed_dim) self.v_proj = nn.Linear(embed_dim, embed_dim) self.dropout = nn.Dropout(dropout) def forward(self, x): # x 形状: (batch_size, num_tokens, embed_dim) B, N, C = x.shape Q = self.q_proj(x) # (B, N, C) K = self.k_proj(x) # (B, N, C) V = self.v_proj(x) # (B, N, C) # 缩放点积注意力 scores = torch.matmul(Q, K.transpose(-2, -1)) / (C ** 0.5) attn_weights = F.softmax(scores, dim=-1) attn_weights = self.dropout(attn_weights) out = torch.matmul(attn_weights, V) return out多头注意力的实现思路是把最后一维拆成(num_heads, head_dim),然后交换维度,让每个头独立参与注意力计算。核心代码片段如下:
class MultiHeadAttention(nn.Module): def __init__(self, embed_dim, num_heads): super().__init__() self.num_heads = num_heads self.head_dim = embed_dim // num_heads self.qkv = nn.Linear(embed_dim, embed_dim * 3) self.proj = nn.Linear(embed_dim, embed_dim) def forward(self, x): B, N, C = x.shape # 一次性生成 Q、K、V qkv = self.qkv(x).reshape(B, N, 3, self.num_heads, self.head_dim) q, k, v = qkv.permute(2, 0, 3, 1, 4).unbind(0) # 后续每个头分别计算注意力,再拼接 return self.proj(out)这里需要注意一点:embed_dim必须能被num_heads整除。ViT-Base 中embed_dim=768,num_heads=12,每个头的维度是 64,刚好满足这个条件。
3. 位置编码:ViT 里为什么要加 Positional Embedding
3.1 Transformer 本身没有顺序信息
自注意力机制对输入是“无序”的。如果把输入 Token 的顺序打乱,模型计算出的注意力结果完全一致。对于文本来说,词序决定语义,所以必须在输入中加入位置信息。对于图像来说,Patch 的空间排列同样关键,位置编码就承担了这个作用。
3.2 ViT 使用的位置编码方案
ViT 论文原文使用的是可学习的位置编码(Learnable Positional Embedding),初始化方式一般是截断正态分布,标准差取 0.02。也就是说,位置编码是模型直接学习出来的参数,而不是像 Transformer 原始论文那样使用固定的三角公式。
一个值得注意的点是,ViT 的输入 Token 数量是固定的,例如输入图像是 224×224,Patch Size 是 16,那么 Patch 数量就是 14×14=196,再加上一个分类 Token,总共是 197 个 Token。位置编码的大小也是(1, 197, embedding_dim)。这意味着,如果推理时输入分辨率改变,Token 数量会发生变化,位置编码和输入特征就无法直接对齐。实际工程中一般会通过双线性插值或截断重采样来处理这个问题。
3.3 位置编码代码实现与分析
用 PyTorch 实现可学习位置编码非常直观:
import torch import torch.nn as nn class LearnablePositionalEncoding(nn.Module): def __init__(self, num_patches, embed_dim): super().__init__() # 可学习参数,形状为 (1, num_patches, embed_dim) self.pos_embed = nn.Parameter(torch.zeros(1, num_patches, embed_dim)) nn.init.trunc_normal_(self.pos_embed, std=0.02) def forward(self, x): # x 形状: (B, N, C) return x + self.pos_embedSwin Transformer 则改用相对位置偏置,这部分在后面会详细解释。用一句话概括:ViT 学习的是绝对位置编码,Swin Transformer 使用的是相对位置偏置,后者更利于处理不同分辨率的输入图像。
4. ViT(Vision Transformer)论文与代码拆解
4.1 ViT 核心思想:把图片看成一组 Token
ViT 的核心观点非常直接:把一张图像切分成固定大小的 Patch,例如 16×16,然后将每个 Patch 拉平并映射成向量,作为 Transformer 的输入 Token。
论文标题《An Image is Worth 16x16 Words》就点明了这个操作:一张图等于 16×16 个“单词”。这意味着 ViT 几乎没有引入图像特有的归纳偏置,而是把图像当成了“句子”来处理。
4.2 Patch Embedding 与分类 Token
Patch Embedding 在工程实现中通常不是真的“切图后拉平”,而是直接用一个大卷积核、大步长的卷积来实现。例如 Patch Size 为 16 时,卷积核大小和步长都设置为 16,输出通道设置为embed_dim。
同时,ViT 在输入序列最前面拼接了一个可学习的[class]Token,这个 Token 的输出经过分类头后作为图像分类结果。
import torch import torch.nn as nn class PatchEmbed(nn.Module): def __init__(self, in_channels=3, embed_dim=768, patch_size=16): super().__init__() self.patch_size = patch_size self.proj = nn.Conv2d( in_channels, embed_dim, kernel_size=patch_size, stride=patch_size ) def forward(self, x): # x 形状: (B, 3, H, W) B, C, H, W = x.shape assert H % self.patch_size == 0 and W % self.patch_size == 0 # 输出形状: (B, embed_dim, H/patch, W/patch) x = self.proj(x) # Flatten 后变成 (B, num_patches, embed_dim) x = x.flatten(2).transpose(1, 2) return x这里有一个容易出错的细节:x.flatten(2)是把最后两个维度展平,也就是(B, embed_dim, H/16 * W/16),再通过transpose(1, 2)变成(B, num_patches, embed_dim),这样才符合 Transformer 输入格式。
4.3 ViT 完整结构拆解
ViT 的整体结构可以分为几个部分:
- Patch Embedding:将图像转为 Token 序列。
- Position Embedding:加入可学习位置编码。
- 可选的 Distillation Token:DeiT 等改进模型中会加入。
- 多个 Transformer Encoder Block:每个 Block 包含多头自注意力、MLP、LayerNorm、残差连接。
- 分类头:取
[class]Token 的输出做分类。
一个完整的 Transformer Encoder Block 如下:
class TransformerBlock(nn.Module): def __init__(self, embed_dim, num_heads, mlp_ratio=4.0, dropout=0.1): super().__init__() self.norm1 = nn.LayerNorm(embed_dim) self.attn = MultiHeadAttention(embed_dim, num_heads) self.norm2 = nn.LayerNorm(embed_dim) self.mlp = nn.Sequential( nn.Linear(embed_dim, int(embed_dim * mlp_ratio)), nn.GELU(), nn.Dropout(dropout), nn.Linear(int(embed_dim * mlp_ratio), embed_dim), nn.Dropout(dropout), ) def forward(self, x): # 残差连接 + 前置 LayerNorm x = x + self.attn(self.norm1(x)) x = x + self.mlp(self.norm2(x)) return x这里使用的是 Pre-LN 结构,也就是先做 LayerNorm,再进注意力层,最后通过残差连接把输入加回来。这种设计可以显著稳定训练过程,也是后来 Swin Transformer、DeiT 等模型普遍采用的方式。
4.4 极简 ViT 代码示例
下面把上面几个模块拼起来,形成一个可以跑通的极简 ViT。这里只保留核心结构,不包含预训练逻辑。
import torch import torch.nn as nn class ViT(nn.Module): def __init__(self, img_size=224, patch_size=16, in_channels=3, num_classes=1000, embed_dim=768, depth=12, num_heads=12, mlp_ratio=4.0, dropout=0.1): super().__init__() num_patches = (img_size // patch_size) ** 2 self.patch_embed = PatchEmbed(in_channels, embed_dim, patch_size) self.cls_token = nn.Parameter(torch.zeros(1, 1, embed_dim)) self.pos_embed = nn.Parameter(torch.zeros(1, num_patches + 1, embed_dim)) nn.init.trunc_normal_(self.cls_token, std=0.02) nn.init.trunc_normal_(self.pos_embed, std=0.02) self.blocks = nn.Sequential(*[ TransformerBlock(embed_dim, num_heads, mlp_ratio, dropout) for _ in range(depth) ]) self.norm = nn.LayerNorm(embed_dim) self.head = nn.Linear(embed_dim, num_classes) def forward(self, x): B = x.shape[0] x = self.patch_embed(x) cls_tokens = self.cls_token.expand(B, -1, -1) x = torch.cat([cls_tokens, x], dim=1) x = x + self.pos_embed x = self.blocks(x) x = self.norm(x) # 取分类 Token x = x[:, 0] x = self.head(x) return x这个模型直接输入torch.randn(4, 3, 224, 224)就能跑通。不过需要注意的是,nn.Sequential中每个 Block 之间没有额外处理,而实际开源实现还会包含 LayerScale、DropPath 等训练稳定策略。
4.5 ViT 的关键局限
ViT 在 ImageNet 这样的大规模数据集上表现非常好,但直接在小数据集上从零训练,效果通常不如 ResNet。原因在于 ViT 缺少 CNN 的归纳偏置,如图像局部性、平移等变性等。要发挥 ViT 的能力,通常需要先在大型数据集上预训练,然后再在下游任务上微调。
5. Swin Transformer 论文与代码拆解
5.1 Swin 要解决 ViT 的什么问题
Swin Transformer 发表于 ICCV 2021,论文标题是《Swin Transformer: Hierarchical Vision Transformer using Shifted Windows》。
Swin 主要解决 ViT 的两个问题:
- 计算复杂度太高:ViT 的全局注意力中,Token 数量是 N,注意力矩阵是 N×N。对于高分辨率图像,Token 数量会非常大,显存和计算量都无法承受。
- 缺乏层级特征:ViT 在所有层中保持相同的 Token 数量,无法构建类似 FPN 的多尺度特征,这不利于目标检测和语义分割等任务。
Swin 的解决方案是:把注意力限制在局部窗口内,同时通过窗口移位来建立跨窗口连接。
5.2 窗口注意力与移位窗口机制
Swin Transformer 将特征图划分为多个不重叠的窗口,例如 7×7 个 Patch 为一个窗口,注意力只在窗口内部计算。假设特征图有 56×56 个 Patch,划分为 7×7 的窗口后,共有 8×8=64 个窗口。每个窗口内有 49 个 Token,注意力矩阵大小是 49×49,而不是全局的 3136×3136。计算量大幅下降。
但只做局部注意力会导致不同窗口之间信息无法交互。Swin 的做法是:在连续两个 Transformer Block 之间交替使用两种窗口划分方式。第一层使用常规窗口,第二层将窗口向对角线方向偏移,偏移量是窗口大小的一半。这样,原本在不同窗口中的 Patch 在新的划分下就能被分到同一窗口,从而实现跨窗口信息交互。
5.3 Swin 的层级结构与相对位置偏置
Swin Transformer 像 CNN 一样构建了层级特征。它包含四个 Stage,每个 Stage 内特征图分辨率逐步减半,而通道数逐步增加。例如 Swin-T 的配置是:第一个 Stage 输出 96 维特征,之后每个 Stage 通道翻倍,最终达到 768 维。
Patch Merging 模块负责下采样和通道拼接。它类似于 CNN 中的池化 + 通道扩展,但实现方式是每隔一个像素取样,得到 4 个子特征图,然后在通道维拼接。
相对位置偏置是 Swin 的另一个重要设计。窗口注意力计算相似度后,会在分数矩阵上加上一个可学习的相对位置偏置表索引,公式如下:
Attention(Q, K, V) = Softmax(QK^T / sqrt(d) + B) V
其中 B 来自相对位置偏置参数表。相对位置偏置让模型能感知窗口内 Token 之间的相对距离关系,同时可以适应不同分辨率的输入。
5.4 窗口划分代码思路
窗口划分与还原是 Swin 实现中的关键函数。下面给出教学版本的窗口划分代码:
def window_partition(x, window_size): """ x 形状: (B, H, W, C) 返回: (num_windows * B, window_size, window_size, C) """ B, H, W, C = x.shape x = x.view(B, H // window_size, window_size, W // window_size, window_size, C) x = x.permute(0, 1, 3, 2, 4, 5).contiguous() x = x.view(-1, window_size, window_size, C) return x def window_reverse(windows, window_size, H, W): """ windows 形状: (num_windows * B, window_size, window_size, C) 返回: (B, H, W, C) """ B = int(windows.shape[0] / (H * W / window_size / window_size)) x = windows.view(B, H // window_size, W // window_size, window_size, window_size, -1) x = x.permute(0, 1, 3, 2, 4, 5).contiguous() x = x.view(B, H, W, -1) return x与普通窗口注意力相比,移位窗口在实现时还需要处理 mask 矩阵,用来屏蔽不同窗口之间跨边界的 Patch。这个细节在源码实现中非常容易出错,也是 Swin 被问得最多的地方之一。
5.5 Swin 的关键参数说明
Swin Transformer 有 Tiny、Small、Base、Large 等不同配置。以 Swin-T 为例:
| 参数 | 值 |
|---|---|
| Embedding 维度 | 96 |
| 每个 Stage 的 Block 数 | [2, 2, 6, 2] |
| 多头注意力的头数 | [3, 6, 12, 24] |
| 窗口大小 | 7 |
| Patch 大小 | 4 |
| 输入分辨率 | 224×224 |
实际复现时,这些参数可以直接从timm或官方源码中读取,不需要手动维护。
6. 从 ViT 出发:注意力机制的扩展与研究方向
6.1 SE、CBAM 和 Transformer 注意力机制的区别
很多初学者容易把 SE、CBAM 与 Transformer 一起比较,但它们的定位完全不同。
SE 模块属于通道注意力,只关注通道之间的重要性,计算成本非常低,通常作为一个即插即用模块加入 ResNet 中。CBAM 则同时加入通道注意力和空间注意力,但空间注意力使用的是卷积操作,不是自注意力。
Transformer 的自注意力属于关系建模机制,它计算的是 Token 与 Token 之间的依赖关系,而非简单的特征加权。因此,SE、CBAM 更接近“特征重标定”,而 Transformer 是“关系建模”。在阅读理解论文时,建议留意作者讨论的是哪一层级的注意力。
6.2 Transformer 在各视觉方向上的扩展
ViT 和 Swin 之后,Transformer 在计算机视觉中的应用迅速扩展到了许多方向:
- 图像分类与检测:DeiT 通过 distillation token 和数据增强训练出无需外部数据即可媲美 CNN 的 ViT 变体;DETR 将 Transformer 引入目标检测;Swin 也被大量用于检测和分割的 backbone。
- 图像复原:Restormer 将 Transformer 用于图像去雨、去模糊,核心改进是多尺度和多头转置注意力,减少高分辨率图像的计算量。
- 高光谱图像处理:高光谱 Transformer 多用于光谱-空间联合特征提取。
- 点云处理:Point Transformer 将自注意力机制用于三维点云数据,处理点集的无序性和非均匀密度。
- 多模态感知:例如无人机多模态感知中,RGB 与热红外图像跨模态对齐也会用到注意力融合机制。
- 时序预测:基于 LSTM 与注意力机制的股票价格预测系统,本质上也是利用注意力对时序特征进行加权建模。虽然任务不同,但注意力机制的基本原理是一致的。
6.3 主流改进方向与近期趋势
从 ViT 到 Swin,再到后续的各种改进,主要围绕几个主题:
- 计算效率:窗口注意力、稀疏注意力、线性注意力、转置注意力。
- 层级特征:像 Swin 一样构建多尺度金字塔,增强对密集预测任务的适配性。
- 训练策略:更好的初始化、LayerScale、DropPath、更强的数据增强。
- 与 CNN 结合:使用卷积与注意力混合的架构,例如 MobileViT、ConvNeXt 等。
这些方向给实习、科研和比赛提供了大量选题空间。建议不要停留在“能跑通代码”的层面,而要深入理解每一步设计背后的动机。
7. 常见问题与排查思路
7.1 常见问题速查表
在实际训练和复现 Transformer 模型时,下面这些问题出现频率较高:
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| ViT 在小数据集上训练不收敛 | Transformer 缺少 CNN 归纳偏置 | 使用预训练权重,增大数据增强,减小模型规模 |
| 训练时显存不足 | 注意力矩阵随 Token 数量平方增长 | 减小输入分辨率,使用梯度累积,开启混合精度 |
| 位置编码与输入尺寸不匹配 | 预训练模型固定了 Token 数量 | 对位置编码做插值或截断处理 |
| Swin 窗口划分报错 | 特征图尺寸无法被窗口大小整除 | 增加 padding,或调整窗口大小和输入分辨率 |
| 注意力矩阵数值异常大 | QK 点积结果方差过大 | 检查是否加入了缩放因子 sqrt(d) |
| 模型收敛很慢 | 学习率设置不合理 | 使用 warmup + 余弦学习率调度器 |
7.2 典型报错分析与解决方案
报错一:输入 Patch Embedding 时维度对不上
如果你是直接修改输入分辨率,比如把 224 改成 384,但 Patch Size 仍然是 16,那么 Patch 数量就会从 196 变成 576,此时预训练位置编码只有 197 个 Token,拼接就会报错。
一个通用做法是对位置编码做双线性插值。timm中提供了一些成熟的实现方式。如果只做简单实验,也可以直接重新随机初始化位置编码,但效果会下降。
报错二:多头注意力中嵌入维度无法被头数整除
假设embed_dim=768,num_heads=5,那么768 / 5无法整除,reshape 时会直接报错。
这种做法很常见:先检查embed_dim % num_heads == 0,如果不能满足,就把num_heads调整为embed_dim的因子,或者修改embed_dim。
报错三:Swin 的 Batch 维度变化导致 shape mismatch
Swin 的核心代码中,window_reverse会根据窗口总数反推 Batch Size。当最后一个 Batch 的样本数量不是num_windows的整数倍时,很容易算错。建议在训练时固定 Batch Size,或者在 DataLoader 中设置drop_last=True。
8. 最佳实践与学习路线建议
8.1 建议的论文与代码阅读顺序
如果你想系统掌握 Transformer 在计算机视觉中的完整链路,建议按以下顺序学习:
- 先读《Attention Is All You Need》中关于自注意力和多头注意力的部分,这是基础。
- 阅读 ViT 论文,重点关注 Patch Embedding 和 Positional Embedding 的设计。
- 阅读 Swin Transformer 论文,重点关注窗口注意力、移位窗口、相对位置偏置和层级结构。
- 用开源代码跑通小模型训练,并尝试修改核心模块观察效果,例如把全局注意力改成窗口注意力。
- 选一个下游任务,例如目标检测或语义分割,将主干网络换成 ViT 或 Swin 进行对比实验。
8.2 复现论文的实操建议
复现 Transformer 模型时,不要一上来就写几百行的完整代码。推荐从最小子模块开始:
- 先实现 Self-Attention,并随机生成输入验证输出形状。
- 再实现 Transformer Block,与开源实现对比数值。
- 然后把 Patch Embedding 和位置编码拼接为完整模型,在小规模数据集上跑通。
- 最后再考虑正则化、预训练初始化、分布式训练等工程问题。
关键模块的形状变化可以先用print(x.shape)追踪,确认每一层的输出符合预期后再继续。
8.3 工程落地视角的注意事项
如果要把 ViT 或 Swin 用到实际项目中,以下几点值得提前规划:
- 预训练权重:几乎总是需要从 ImageNet 预训练模型开始微调,不建议从零训练。
- 动态分辨率:生产环境中输入图片尺寸可能不固定,要设计好位置编码的插值策略,或者统一做 Resize。
- 部署转换:Transformer 转 ONNX 或 TensorRT 时,某些动态形状算子可能不支持,需要提前验证兼容性。
- 推理加速:窗口注意力在并行化和算子融合方面比全局注意力更适合工程部署,这也是 Swin 的一大优势。
- 显存控制:如果超分辨率或检测任务中输入较大,可以把注意力计算中的 Tensor 显式声明为半精度,配合梯度累积稳定训练。
另外,在组自己的实验结果时,建议把注意力分布可视化出来,这有助于判断模型到底是关注了目标区域,还是出现了注意力涣散的问题。可视化工具可以选择 opencv、matplotlib 或直接保存注意力权重热力图。
到这里,关于注意力机制、ViT 和 Swin Transformer 的核心内容就完整梳理了一遍。你现在可以打开timm或者官方 GitHub 仓库,找到对应模型配置,对照这篇文章的模块拆分,一行一行读下去。读懂一个模型最快的方式,就是亲手改一个参数,然后观察训练结果的变化。