资讯详情

ViT图像分类从原理到实战:Patch Embedding、训练与排坑指南

📅 2026/10/22 8:06:11 | 华诺云谱 👁 阅读
ViT图像分类从原理到实战:Patch Embedding、训练与排坑指南
图像分类这个任务放在几年前大家第一反应基本就是CNN那一套ResNet、EfficientNet、MobileNet各种卷积结构轮番上场。但自从Vision Transformer简称ViT出现之后整个视觉分类的思路被彻底翻了一遍——把图像切成一个个小方块当成一串token丢给Transformer去跑居然也能达到甚至超过CNN的效果。这篇就围绕ViT在图像分类任务上的原理、实现和排坑过程展开结合我在实际项目里的使用心得把每个环节拆开讲透。想搞懂ViT为什么能work、想自己从零写一个去训练、想复现这类结构的读者这篇应该能帮上忙。1. 项目思路拆解为什么图像分类也能用Transformer1.1 从CNN到Transformer分类范式的变化CNN统治图像分类的时间太长以至于很多人默认“图像任务就该用卷积”。CNN之所以能work靠的是三个非常重要的归纳偏置局部性卷积核只看相邻像素、权值共享同一卷积核滑遍全图以及平移等变性目标位置变化不影响识别结果。这些先验让CNN在有限数据下也能学到不错的特征本质上是在用结构约束降低模型复杂度。但Transformer最开始是为NLP设计的它压根没有这些先验。自注意力机制允许序列中的任意两个位置直接交互位置信息完全靠额外加进去的位置编码来提供。当研究者把这种结构搬来处理图像时等于是说我不预设特征应该怎么组织全靠数据自己说话。这在当时是一个非常大胆的选择因为纯Transformer模型没有局部性约束如果直接用在小型数据集上很容易因为模型容量大、数据量不够而陷入过拟合。ViT的意义在于它证明了纯Transformer在视觉任务上的可行性。不是用CNN提取特征后再接Transformer做融合而是从原始像素直接切patch、直接建模全局关系。结果在大规模数据集上ViT的表现追平甚至超过了当时的CNN结构。这件事当时确实让不少做视觉的人重新思考我们到底需要多少“人工设计”的先验后来大量实验也表明只要数据量足够大、训练策略正确Transformer完全能自己学会图像中的空间结构。1.2 ViT的核心设计思路把图像当“句子”读理解ViT最简单的方式就是把它和NLP里的句子处理对标。你在读一句话时词和词的顺序本身有意义而语义需要结合上下文来理解。ViT把一张图像当作一段“句子”把图像划分成固定大小的patch比如16x16像素一块每一块相当于句子里的一个“词”。每个patch有三个通道RGB拉直成一个向量后通过一个线性映射变成定长的embedding向量。这个映射过程就叫Patch Embedding。随后所有patch的embedding拼成序列加上一个专门用于分类的CLS token再叠加上位置编码送入标准的Transformer Encoder。Encoder里的自注意力机制会让每个patch去关注图像里其他所有patch的信息从而构建全局上下文。最后一层取CLS token的输出向量过一层分类头就能得到类别概率。这个思路最大的变化在于图像不再是一张二维矩阵而是一组一维token。空间结构不再由卷积核的局部窗口隐式表达而是由位置编码显式告诉模型“你原来在哪”。这种设计的好处是任何位置的patch都能直接与其他位置的patch交互长距离依赖问题天然被解决不需要像CNN那样通过堆叠几十层来扩大感受野。1.3 什么时候选ViT什么时候继续用CNN在实际项目里选型千万别抱着“ViT是新东西所以一定更好”的心态。CNN和ViT的取舍核心是两个变量数据规模和计算资源。先看数据规模。ViT没有卷积那种强归纳偏置从零训练的条件下它对数据量的需求比CNN高不少。业界普遍经验是如果你只有几万张图甚至更少从零训一个ViT效果大概率不如同量级CNN。反过来如果你手里有百万级别的数据或者能从大规模预训练权重出发做迁移学习那ViT的优势会被明显放大。再看计算资源。ViT在相同参数量下计算量通常高于CNN而且对训练配置更敏感。很多情况下同样的预算和时间CNN已经训完了ViT还在调参。下列表格可以辅助判断维度CNNViT归纳偏置强局部性、平移等变弱全靠数据学习数据需求少样本也能凑合大数据或预训练才香长距离依赖靠深层堆叠间接获取天然全局建模计算效率小数据集上更省大规模场景更划算训练难度相对低、好调对学习率、正则化敏感迁移能力成熟但特征偏局部预训练后泛化性很强日常项目里如果任务是中小规模的图像分类我通常先用CNN打底快速出一版结果如果发现数据量足够、硬件也够好再换成ViT尝试涨点。或者反过来直接加载预训练ViT做微调这样能绕开从零训练的坑通常效果也不错。2. 核心细节解析ViT的每一块拼图2.1 Patch Embedding图像如何变成TokenPatch Embedding是整个ViT里最容易被人忽略、却最基础的一步。假设输入图像大小是224x224通道数是3选定的patch大小是16x16。这时图像被均匀切分成14x14196个patch每个patch展开后的维度是16x16x3768。这196个patch就是输入序列的长度每个patch经过一次线性变换后得到的768维向量就对应Transformer里token的embedding维度。实际工程实现上这个线性变换不会真的逐patch做矩阵乘法而是直接用一层卷积核大小等于patch_size、步长也等于patch_size的卷积层来完成。比如nn.Conv2d(in_chans3, embed_dim768, kernel_size16, stride16)。这层卷积的输出正好是[B, 768, 14, 14]再展平成[B, 196, 768]。很多初看ViT代码的人会困惑“为什么用卷积实现embedding”其实就是因为这种操作等价于对每个patch做线性映射而且卷积在GPU上的效率更高。关于patch_size的选择它直接决定了序列长度和每个token的信息粒度。patch越小序列越长计算量越大但能保留更细的空间细节patch越大序列越短计算量少但可能丢失局部细节。ViT-Base/16用的是16x16ViT-Large/14用的14x14实践下来在ImageNet级别的数据上都是合理的。如果换到小图数据集比如32x32的CIFAR直接用16x16的patch会产生很尴尬的情况整张图只有2x24个patch序列太短Transformer很难发挥全局建模能力。这时候就要把patch_size调小比如4x4或者先把图resize到224x224再训练。2.2 Position Embedding让模型知道“顺序”Transformer的自注意力机制本身是置换不变的——你把token的顺序打乱计算出来的结果在数学上是一样的。这对NLP来说不可接受对图像来说更不能接受因为图像的空间结构本身就是重要信息。猫的眼睛长在鼻子上面轮子在车身下面这类位置关系必须被模型感知到。ViT采用的做法非常直接在输入序列进入Transformer之前给每个token的embedding加上一个可学习的位置向量。因为序列里除了196个patch token还有一个CLS token所以位置编码的shape是[1, 197, 768]。这个位置编码是随机初始化、随训练更新的模型会在训练过程中自己学会利用位置信息。很多人问过一个问题2D位置编码会不会比1D位置编码更好毕竟图像是二维结构。我在实际实验里对比过这两种方案结论是效果差别很小。原因在于ViT的位置编码是加在patch embedding上的后续的多头注意力可以自行从这个向量的不同分量中解码出相对位置关系。既然1D够用就省去了构造2D坐标映射表的麻烦。顺带一提如果训练时固定输入分辨率但推理时想用更高分辨率位置编码就需要做插值处理这是很多“换分辨率就变差”问题的根源。2.3 Transformer Encoder自注意力到底在看什么ViT的主干就是堆叠多个结构相同的Transformer Encoder Block。每个Block包含两个核心子层多头自注意力Multi-Head Self-Attention和MLP每个子层外面都套了LayerNorm和残差连接。自注意力的计算流程可以这么理解对于序列里的每个token我们生成三个向量分别叫Query、Key、Value。其中Query是“我当前这个patch想找谁”Key是“我是谁我能提供什么信息”Value是“我携带的实际内容”。注意力权重的计算方式是对所有token做Query和Key的点积值越大说明相关性越强再经过softmax归一化后作为加权系数对Value做加权求和。这个过程的直觉很像在图书馆检索你带着自己的问题Query去匹配每本书的标签Key然后从匹配到的书里提取内容Value。具体公式是Attention(Q, K, V) softmax(Q K^T / sqrt(d_k)) V其中d_k是每个注意力头的维度除以sqrt(d_k)非常关键。如果不除以这个缩放因子当维度变大时点积的数值会随之增大落入softmax的饱和区梯度会变得极小训练几乎推不动。这也是Transformer训练里一个容易被忽略的细节。多头注意力则是把768维的embedding分成12个64维的“头”每个头独立做注意力计算最后拼接起来。不同头会关注不同的关系比如有的头倾向于关注颜色相似的区域有的头关注形状特征有的头关注目标边缘。这个机制给了模型更强的表征能力。MLP部分是每个token独立经过的两层全连接网络通常是4倍隐藏维度扩张再加GELU激活。它的作用是给每个token单独做特征变换和非线性映射。LayerNorm和残差连接保证了深层模型训练的稳定性让ViT可以堆到12层甚至24层而不至于梯度消失。2.4 CLS Token与分类头怎么输出分类结果ViT的分类头设计沿用了BERT里的做法在输入序列的最前面拼接一个特殊的CLS token它的初始向量是随机初始化的可学习参数。经过整个Transformer Encoder之后把这个CLS token对应位置的输出向量取出来作为整张图像的全局特征表示再过一层线性分类器得到类别概率。这种设计的巧妙之处在于CLS token的初始向量不包含任何图像信息但它是序列中的一个位置因此会在注意力计算过程中不断聚合其他patch token的信息。训练完成后CLS token的输出可以被理解为模型对整张图语义的浓缩表示。我实际还试过另一种更朴素的方案不用CLS token而是把所有patch token的最后一层输出做全局平均池化GAP再送入分类头。这个方案在某些数据集上的表现甚至更稳定原因是GAP强制模型利用所有patch的信息而不是依赖某个特定位置。开源代码里也常见这种变体叫作ViT-GAP效果不比CLS方案差。分类头通常就是一个线性层输入维度是768输出维度是类别数。如果类别数很多也可以先接一个Dropout再加线性层防止过拟合。整个ViT的核心组件加起来就这么点东西Patch Embedding、Position Embedding、Transformer Block、CLS Token、分类头。逻辑并不复杂真正难的是把训练跑稳。3. 实操过程从零实现ViT图像分类器3.1 环境准备与数据准备实操部分我用PyTorch来实现因为它在动态图和生态上最顺手。官方实现是JAX写的但对大多数人来说PyTorch版本更容易修改和调试。环境方面只需要torch和torchvision配合GPU训练即可。数据方面我用一个自建的图像分类数据集来演示目录结构就是常见的train/类别名/图片.jpg和val/类别名/图片.jpg。直接调用torchvision.datasets.ImageFolder就能读进来非常省事。数据增强部分和CNN训练时类似但ViT对数据增强的依赖更强所以会多加几项from torchvision import datasets, transforms train_transform transforms.Compose([ transforms.RandomResizedCrop(224, scale(0.8, 1.0)), transforms.RandomHorizontalFlip(), transforms.RandAugment(num_ops2, magnitude9), transforms.ColorJitter(0.3, 0.3, 0.3), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) val_transform transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) train_ds datasets.ImageFolder(data/train, transformtrain_transform) val_ds datasets.ImageFolder(data/val, transformval_transform)这里有一个实操经验RandAugment对ViT很有用。CNN有时候光靠随机裁剪和翻转就能训得不错但ViT因为缺少归纳偏置更需要多样化的输入来弥补这一点。数据增强是ViT从零训练时最重要的“免费午餐”之一。3.2 模型实现一个可运行的ViT完整结构下面是我在实际项目里使用并踩过不少坑之后整理出的完整实现。为了讲清楚结构我把整个网络拆成了四个部分PatchEmbed、Attention、Mlp、Block最后用VisionTransformer把它们组合起来。import torch import torch.nn as nn import torch.nn.functional as F class PatchEmbed(nn.Module): 图像切块并映射到embedding维度 def __init__(self, img_size224, patch_size16, in_chans3, embed_dim768): super().__init__() self.num_patches (img_size // patch_size) ** 2 # 用卷积实现等价线性映射 self.proj nn.Conv2d(in_chans, embed_dim, kernel_sizepatch_size, stridepatch_size) def forward(self, x): # x: [B, C, H, W] x self.proj(x) # [B, embed_dim, H/p, W/p] x x.flatten(2) # [B, embed_dim, num_patches] x x.transpose(1, 2) # [B, num_patches, embed_dim] return x class Attention(nn.Module): 多头自注意力 def __init__(self, dim, num_heads8, qkv_biasTrue, attn_drop0.0, proj_drop0.0): super().__init__() self.num_heads num_heads head_dim dim // num_heads self.scale head_dim ** -0.5 self.qkv nn.Linear(dim, dim * 3, biasqkv_bias) self.attn_drop nn.Dropout(attn_drop) self.proj nn.Linear(dim, dim) self.proj_drop nn.Dropout(proj_drop) def forward(self, x): B, N, C x.shape qkv self.qkv(x).reshape(B, N, 3, self.num_heads, C // self.num_heads) qkv qkv.permute(2, 0, 3, 1, 4) # [3, B, heads, N, head_dim] q, k, v qkv[0], qkv[1], qkv[2] attn (q k.transpose(-2, -1)) * self.scale attn attn.softmax(dim-1) attn self.attn_drop(attn) x (attn v).transpose(1, 2).reshape(B, N, C) x self.proj(x) x self.proj_drop(x) return x class Mlp(nn.Module): 每个token独立过MLP def __init__(self, in_features, hidden_featuresNone, out_featuresNone, drop0.0): super().__init__() out_features out_features or in_features hidden_features hidden_features or in_features * 4 self.fc1 nn.Linear(in_features, hidden_features) self.act nn.GELU() self.fc2 nn.Linear(hidden_features, out_features) self.drop nn.Dropout(drop) def forward(self, x): x self.fc1(x) x self.act(x) x self.drop(x) x self.fc2(x) x self.drop(x) return x class Block(nn.Module): 一个Transformer Encoder Block def __init__(self, dim, num_heads, mlp_ratio4.0, drop0.0, attn_drop0.0): super().__init__() self.norm1 nn.LayerNorm(dim) self.attn Attention(dim, num_headsnum_heads, attn_dropattn_drop, proj_dropdrop) self.norm2 nn.LayerNorm(dim) self.mlp Mlp(in_featuresdim, hidden_featuresint(dim * mlp_ratio), dropdrop) def forward(self, x): # Pre-LN结构先做LayerNorm再进Attention/MLP x x self.attn(self.norm1(x)) x x self.mlp(self.norm2(x)) return x class VisionTransformer(nn.Module): ViT完整结构 def __init__(self, img_size224, patch_size16, in_chans3, num_classes1000, embed_dim768, depth12, num_heads12, mlp_ratio4.0, drop0.1, attn_drop0.0): super().__init__() self.patch_embed PatchEmbed(img_size, patch_size, in_chans, embed_dim) num_patches self.patch_embed.num_patches self.cls_token nn.Parameter(torch.zeros(1, 1, embed_dim)) self.pos_embed nn.Parameter(torch.zeros(1, num_patches 1, embed_dim)) self.pos_drop nn.Dropout(drop) self.blocks nn.Sequential(*[ Block(embed_dim, num_heads, mlp_ratio, drop, attn_drop) for _ in range(depth) ]) self.norm nn.LayerNorm(embed_dim) self.head nn.Linear(embed_dim, num_classes) # 初始化策略正态分布初始化LayerNorm置为1和0 nn.init.trunc_normal_(self.pos_embed, std0.02) nn.init.trunc_normal_(self.cls_token, std0.02) self.apply(self._init_weights) def _init_weights(self, m): if isinstance(m, nn.Linear): nn.init.trunc_normal_(m.weight, std0.02) if m.bias is not None: nn.init.zeros_(m.bias) elif isinstance(m, nn.LayerNorm): nn.init.ones_(m.weight) nn.init.zeros_(m.bias) def forward(self, x): B x.shape[0] x self.patch_embed(x) # [B, N, embed_dim] cls_token self.cls_token.expand(B, -1, -1) x torch.cat((cls_token, x), dim1) # [B, N1, embed_dim] x x self.pos_embed x self.pos_drop(x) x self.blocks(x) x self.norm(x) cls x[:, 0] # 取CLS token输出 return self.head(cls)这个实现用的结构是Pre-LN也就是在进入注意力层之前先做LayerNorm而不是在子层之后。千万别小看这个顺序我一开始按Post-LN先注意力再LayerNorm写结果12层Transformer很难训稳换到Pre-LN后收敛速度明显提升。这是一个非常实用的经验。3.3 训练配置与参数计算训练配置上我也踩过不少坑。ViT对学习率异常敏感直接搬CNN那套SGD或者偏大的学习率基本都会让loss像过山车一样震荡。推荐使用AdamW优化器配合学习率warmup和cosine退火。import torch.optim as optim from torch.optim.lr_scheduler import CosineAnnealingLR, LinearLR, SequentialLR model VisionTransformer(img_size224, patch_size16, in_chans3, num_classes10, embed_dim768, depth12, num_heads12, drop0.1, attn_drop0.0) optimizer optim.AdamW(model.parameters(), lr5e-4, weight_decay0.05) # warmup 3个epoch然后cosine退火到很低的lr warmup_epochs 3 total_epochs 100 warmup_scheduler LinearLR(optimizer, start_factor0.01, end_factor1.0, total_iterswarmup_epochs) cosine_scheduler CosineAnnealingLR(optimizer, T_maxtotal_epochs - warmup_epochs, eta_min1e-6) scheduler SequentialLR(optimizer, schedulers[warmup_scheduler, cosine_scheduler], milestones[warmup_epochs])这里有几个参数值得说道一下学习率峰值5e-4是ViT-Base/16配合batch size 256时的常见配置。如果你的batch size明显更大或者更小学习率可以按线性缩放原则调整比如batch翻倍学习率大致翻倍。weight_decay设成0.05是Transformer系模型的常用做法比CNN常用的1e-4大不少。它对抑制过拟合很有帮助但也不能设太大否则会欠拟合。warmup的作用是让模型在训练初期不要剧烈更新参数。Transformer的embedding初始数值范围比较大直接在峰值学习率下更新很容易导致早期不稳定先用小学习率跑几个epoch等分布稳定后再涨到峰值就顺滑很多。参数计算方面ViT-Base/16的结构是一目了然的输入224x224patch16序列长度196。embedding维度768Transformer层数12注意力头数12每个头64维MLP隐层3072。整体参数量大约8600万这个体量跟ResNet101是一个级别。计算量大概在17.6 GFLOPs左右比ResNet50的4.1 GFLOPs大了四倍多。所以在硬件不太够的前提下往往不是模型参数量卡你而是计算量和显存占用卡你。3.4 训练循环与评估训练循环本身不复杂关键在准确保留验证集评估逻辑。下面是我项目里的训练主循环import time from tqdm import tqdm def train_one_epoch(model, loader, optimizer, criterion, device, grad_clip1.0): model.train() total_loss, correct, total 0.0, 0, 0 for images, labels in tqdm(loader, leaveFalse): images, labels images.to(device), labels.to(device) outputs model(images) loss criterion(outputs, labels) optimizer.zero_grad() loss.backward() nn.utils.clip_grad_norm_(model.parameters(), grad_clip) optimizer.step() total_loss loss.item() * images.size(0) correct outputs.argmax(dim1).eq(labels).sum().item() total images.size(0) return total_loss / total, correct / total def evaluate(model, loader, criterion, device): model.eval() total_loss, correct, total 0.0, 0, 0 with torch.no_grad(): for images, labels in loader: images, labels images.to(device), labels.to(device) outputs model(images) loss criterion(outputs, labels) total_loss loss.item() * images.size(0) correct outputs.argmax(dim1).eq(labels).sum().item() total images.size(0) return total_loss / total, correct / total criterion nn.CrossEntropyLoss() device torch.device(cuda if torch.cuda.is_available() else cpu) model.to(device) for epoch in range(total_epochs): train_loss, train_acc train_one_epoch(model, train_loader, optimizer, criterion, device) val_loss, val_acc evaluate(model, val_loader, criterion, device) scheduler.step() print(fEpoch {epoch1}/{total_epochs} | ftrain_loss {train_loss:.4f} | train_acc {train_acc:.4f} | fval_loss {val_loss:.4f} | val_acc {val_acc:.4f})梯度剪裁这一行是我强烈建议保留的。ViT在训练后期偶发会出现某个batch的loss突然飙高的情况如果不做梯度剪裁一次异常大幅更新就可能把整个模型参数推离正常区域后续怎么训都回不来。clip_grad_norm设为1.0在多数情况下能在不影响收敛速度的同时避免这种灾难。4. 常见问题与排查技巧实录4.1 数据集太小ViT直接翻车很多第一次跑ViT的人都会遇到这个现象同样是从零训练ViT在几千张图上训了半天准确率依然不如一个小ResNet。我一开始也中招了以为是自己代码写错了后来才意识到这是ViT的特性——缺乏归纳偏置小数据集根本撑不起它的容量。解决办法有三个方向。第一个是使用预训练权重微调这是最有效的一条路。第二个是改用patch_size更小的变体降低序列粒度让模型更容易捕捉小尺寸数据里的局部特征比如在CIFAR-10上把patch_size从16降到4。第三个是增大数据增强强度用更激进的RandAugment和MixUp来弥补数据量不足。这些手段配合下来小数据集上的表现可以提升不少。4.2 训练loss震荡、不收敛ViT最典型的训练问题就是loss震荡。表现是前几个epoch loss下降正常到某个节点突然冲高之后再降回来反复几次最终效果很差。这个问题我排查过很多次最常见的根源有三个学习率太高注意力机制的softmax对梯度幅值很敏感。warmup期太短模型分布还没稳定就直接进入峰值学习率。某个batch的数据没处理好比如存在全黑的图或者异常像素值。处理方式就是刚才代码里的三板斧AdamW、warmup、梯度裁剪。如果做了这三件事还震荡可以继续降低峰值学习率或者把batch size调大。batch size在Transformer里往往比学习率更影响稳定性条件允许时优先扩大batch。4.3 显存不足ViT的显存占用比同参数量CNN要高其中一个原因是自注意力机制需要保存QK^T的完整注意力矩阵这个矩阵的大小是batch size乘以序列长度的平方。序列长度196时还好但如果你把patch调小或者输入分辨率调大序列长度会迅速膨胀显存随之爆掉。解决办法也有几条路一是梯度累积用时间换空间二是混合精度训练我用AMP之后显存基本能省一半三是降低输入分辨率比如把224改成160序列长度保持不变但每个patch的embedding计算量会下降。如果必须用高分辨率就要认真考虑换更高效的变体了。4.4 过拟合验证集准确率上不去ViT在小数据集上过拟合比CNN来得更快具体表现是训练准确率很快逼近100%验证集准确率却停滞不前。除了常规的Dropout和Weight Decay我推荐重点检查两个位置位置编码和CLS token的Dropout是不是默认太小。pre-training的时候dropout一般设0.1但从零训练时我会把drop调到0.2甚至0.3。MLP模块里的Dropout有没有真的加在两层之间。很多简化实现只在输出层drop一次实际两层都drop效果更稳。还有一个容易被忽略的细节LayerNorm的初始化不要动保持weight1、bias0。有人为了“提升表达能力”去调整LayerNorm的初始化结果往往适得其反。4.5 位置编码和CLS token的坑自己手写ViT时坑最多的地方就是位置编码和CLS token的初始化。初次接触时我踩过一个很经典的坑忘记初始化cls_token和pos_embed它们保持全0训练出的模型和随机初始化差不了多少。还有一个坑是预训练模型是在固定分辨率下做的位置编码推理时如果你改成更大的分辨率必须对位置编码做插值否则位置信息错乱准确率直接雪崩。这里贴一段插值代码的核心逻辑def interpolate_pos_embed(pos_embed, new_num_patches): # pos_embed: [1, old_N1, embed_dim]去掉cls token后插值 old_pos pos_embed[:, 1:, :] # [1, old_N, embed_dim] old_h old_w int(old_pos.shape[1] ** 0.5) new_h new_w int(new_num_patches ** 0.5) old_pos old_pos.reshape(1, old_h, old_w, -1).permute(0, 3, 1, 2) # [1, C, h, w] new_pos F.interpolate(old_pos, size(new_h, new_w), modebilinear, align_cornersFalse) new_pos new_pos.permute(0, 2, 3, 1).flatten(1, 2) # [1, new_N, C] return torch.cat([pos_embed[:, :1], new_pos], dim1)这个思路也适用于从预训练权重微调时输入分辨率比预训练分辨率大的情况。如果不做插值模型在更高分辨率输入下的表现会明显退化。别问我是怎么知道的问就是深夜调参的眼泪。5. 一点个人体会最后说点我自己实际跑下来的感受。ViT这东西很挑训练配置绝不是把CNN那套训练流程搬过来就能跑出效果。如果你手头数据不多别急着从零训练先去找预训练权重微调很多时候你花几天调一个从零训练的ViT还不如加载预训练模型跑一个晚上。再说一个小技巧先用小patch、小分辨率把网络结构和训练流程跑通再逐步放大输入尺寸迁移到目标分辨率调试周期能缩短一大截。ViT虽然坑多但当你第一次把注意力权重可视化出来看到它真的能学会关注图像里那些关键区域的时候你会觉得这些折腾都值了。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑