☰
CAS-ViT图像分类实战:轻量Transformer的加性自注意力与训练优化
2026/10/7 5:43:48 网站建设 项目流程

简介:面向图像分类任务开发者,这份实战资源围绕CAS-ViT(卷积加性自注意力视觉Transformer)展开,帮助解决传统Transformer计算开销大、难以高效部署的问题。压缩包共2000个文件,约736.89MB,其中包含1990个png图像(数据集样本及训练/验证可视化结果)、6个Python脚本(模型构建、训练与评估代码)、2个pyc编译文件、1个json类别映射及1个txt说明文件,目录结构清晰。已有745人学习下载。资源重点覆盖了CAS-ViT的两大核心设计——加性相似度函数与卷积加性标记混合器(CATM),可让读者从数据准备、模型实现到训练评估完整走通图像分类流程,并借助可视化输出直观理解低计算开销下的特征提取效果,适合作为Transformer轻量化的入门与复现参考。

1. CAS-ViT 实战:轻量 Transformer 做图像分类,先别急着上 Swin

做图像分类时,我一开始习惯直接搬 Swin Transformer,直到在一台只有 4GB 显存的旧卡上跑训练,才意识到轻量化不是“少两个 block”这么简单。CAS-ViT(Convolutional Additive Self-attention Vision Transformer)用加性自注意力替代经典矩阵乘注意力,配合卷积加性标记混合器 CATM,把计算复杂度从平方级压到线性。这份实战资源提供了 class.json 类别文件和一批 PNG 样例图,可以在不改动模型结构的情况下,快速跑通“数据准备→训练→验证”完整流程。它适合正在做图像分类,又不想在硬件上妥协的 PyTorch 用户,也适合想对比 Transformer 与 CNN 计算差异的算法工程师。如果你上个月还在调 ResNet,这周想试试最新的图像分类模型,这个资源正好接得住。

2. 拆解 CAS-ViT 的核心结构:加性自注意力与 CATM 怎么省算力

很多人看 Transformer 做视觉时,只盯着“有没有多头注意力”。但 CAS-ViT 的关键不在多头,而在把自注意力里的 QK^T 矩阵乘法换掉。这个替换让计算量从 token 数的平方级降到线性,是它在图像分类任务里“够轻”的根本原因。下面我用一个能实际跑起来的最小实现说清楚,再看哪些地方容易踩坑。

2.1 从标准自注意力到加性相似度:计算图发生了什么变化

标准 ViT 的自注意力可以简写为:Attention(Q,K,V) = softmax(QK^T / √d) V。这里的 Q、K、V 都是 token 序列的线性投影,形状为 [B, N, d]。QK^T 会产生一个 [B, N, N] 的注意力矩阵。当输入是 224×224 分辨率、patch=16 时,N=196,这个矩阵还勉强能看;可一旦把 patch 调到 4,N 直接变成 3136,矩阵就是上千万个元素,显存和延迟一起爆炸。

CAS-ViT 提出的加性相似度函数,核心思路是不再让两个 token 的向量做内积,而是把 query 和 key 做逐元素的相加或相减,再接一个非线性激活。这样不需要显式求出完整的 N×N 矩阵,token 之间的交互可以用卷积在空间邻域内近似完成。CATM(卷积加性标记混合器)就是在这个思路下把“注意力”和“token 混合”合并成一个算子。下面这段代码演示了两种注意力的形状差异:

import torch import torch.nn.functional as F def standard_attention(q, k, v): # q,k,v: [B, N, d] scores = torch.bmm(q, k.transpose(1, 2)) / (q.size(-1) ** 0.5) attn = F.softmax(scores, dim=-1) return torch.bmm(attn, v), attn.shape # attn 是 [B,N,N] def additive_similarity(q, k): # 加性相似度:q-k 后走一个可学习的缩放,不产生 [B,N,N] 的完整矩阵 diff = q.unsqueeze(2) - k.unsqueeze(1) # 演示用,实际实现会用卷积聚合 return torch.tanh(diff.mean(-1)), diff.mean(-1).shape

这段代码的逻辑是:standard_attention 里 torch.bmm 直接计算了 [B,N,N] 的注意力分数图,第二个维度会随 token 数平方增长;additive_similarity 则先构造一个差值张量,再在最后一维上做归约。实际部署时,CAS-ViT 不会真的展开 N×N 的 diff,而是用一个 depthwise 卷积在邻域窗口内做同样的加性混合,所以内存占用是线性的。这里可以看到,标准注意力的返回 shape 里 N 占两个维度,而加性相似度只产生一个 N 维度。

真正的 CATM 实现会把加性相似度封装成卷积算子,并带一个可学习的 temperature 参数,控制激活前的缩放。你不需要自己重写,下载的资源包里有完整模型定义。为了确认模型确实走的是加性注意力分支而不是回了标准 MHSA 的 fallback,我一般会在模型上挂钩子打印每一层的输出形状:

python -c " from casvit import build_cas_vit import torch model = build_cas_vit('casvit_tiny', num_classes=4) x = torch.randn(1, 3, 224, 224) hooks = {} for name, m in model.named_modules(): m.register_forward_hook(lambda mod, inp, out, name=name: hooks.update({name: out[0].shape if isinstance(out, tuple) else out.shape})) model(x) for k, v in hooks.items(): if 'catm' in k.lower() or 'add' in k.lower(): print(k, v) "

这里的 build_cas_vit 导入路径以你下载的代码包为准,文件名可能不同,但搜索关键字一般是 catm 或 token_mixer。看到输出里有 [1, 192, 56, 56] 之类的形状,说明 CATM 是拿四维图像特征在干活,而不是把特征拉平成一维 token 序列做矩阵乘。这也是它省算力的证据之一。

2.2 CATM 模块的输入输出与参数设计

CATM 的直观理解是:先用卷积把相邻 token 的信息“预混合”,再用加性注意力权重在通道间融合。下面这个简化版是我在实际项目里用来复现 CATM 行为的最小实现,保留了残差和通道混合两个关键设计:

import torch import torch.nn as nn class AdditiveTokenMixer(nn.Module): def __init__(self, dim, kernel_size=3, temperature=0.1): super().__init__() self.dwconv = nn.Conv2d(dim, dim, kernel_size, padding=kernel_size // 2, groups=dim) self.norm = nn.InstanceNorm2d(dim) self.temperature = nn.Parameter(torch.tensor(temperature)) self.channel_mix = nn.Conv1d(dim, dim, 1) def forward(self, x): B, C, H, W = x.shape out = self.dwconv(x) # 空间邻域加性混合 out = self.norm(out) * self.temperature out = out.tanh() out = out + x # 残差,保持原始语义 out = self.channel_mix(out.flatten(2)).reshape(B, C, H, W) return out

这段代码的参数含义比较关键:

  • dwconv 是分组卷积,groups=dim 表示每个通道独立做 3×3 卷积,参数量只有 3×3×C,和 token 数完全无关。
  • temperature 是一个可学习标量,初始值设 0.1,防止 tanh 在训练初期就饱和。
  • channel_mix 是 1×1 卷积,等价于对每个位置上的 C 维向量做线性变换,相当于标准 FFN 的通道混合层。
  • 残差连接放在 tanh 之后,避免非线性把原始语义冲刷掉。

为什么这里用 InstanceNorm 而不是 BatchNorm?因为 CATM 经常在不同 Batch Size 下训练,InstanceNorm 对单样本也稳定;而且加性注意力希望在空间上保持局部统计,BatchNorm 会引入跨样本统计噪声。如果你在自己的实现里发现训练和验证表现不一致,先检查是不是这里用了 BatchNorm。

再放一个直观的参数对比表,帮你理解 CATM 为什么比 MHSA 轻:

模块参数量(dim=192, N=196, h=3 估算)计算量特点
MHSA4×192×192 ≈ 147K随 N 平方增长
CATM3×3×192 + 192×192 ≈ 37K随 N 线性增长

这里的数量级是粗略估算,实际还要算上 LayerNorm 和 FFN 部分,但趋势很清楚:CATM 把大头省在矩阵乘上。读源码时,先找 class TokenMixer,再看 forward 里是否有一个 else 分支回到标准注意力。很多项目为了兼容旧权重会保留双分支,训练时如果用错分支,显存表现会跟论文对不上,这是后话。

结构清楚之后,下一步就是把数据送进模型。资源里的 class.json 和样例 PNG 正好在数据准备这一步派上用场。

3. 用 class.json 组织数据:CAS-ViT 图像分类训练全流程

下载的资源里有一个 class.json 和若干张 PNG 样例图。class.json 的作用是告诉模型“类别索引 0 对应哪个名称”。如果你的目标数据集也是散装图片,第一步就是把这张映射表变成磁盘目录。否则 ImageFolder 会按目录名排序重新编号,和 class.json 对不上,训练出来的模型标签永远是错位的。

3.1 把 class.json 变成 ImageFolder:目录结构与映射检查

class.json 常见的格式有两种:{"0": "cat", "1": "dog"}或者{"cat": 0, "dog": 1}。加载后先判断方向,再做统一处理:

import json with open("class.json", "r", encoding="utf-8") as f: class_map = json.load(f) print(type(class_map), len(class_map)) # 如果键是字符串数字,说明是 id -> 名称 if all(k.isdigit() for k in class_map.keys()): class_names = [class_map[str(i)] for i in range(len(class_map))] else: class_names = list(class_map.keys()) print(class_names)

这段代码的逻辑是:先检查所有 key 是否是数字字符串,是的话就按索引 0,1,2... 取出对应的类别名称。这样不管原始字典的插入顺序多乱,最终 class_names 的排列顺序一定和索引一致。需要注意,Python 字典的插入顺序不等于排序顺序,所以千万不要直接list(class_map.keys())拿类别列表,除非你能确认 class.json 是严格按照 0,1,2,3 顺序写入的。

拿到类别列表后,按类别建立 train/val 目录,并把 PNG 样例图复制进去。资源自带的几张图只是用来验证流程的,真实项目里你需要把自己的图片分好类:

import shutil from pathlib import Path root = Path("data") for split in ["train", "val"]: for name in class_names: (root / split / name).mkdir(parents=True, exist_ok=True) # 将每张 png 按实际标签移动,这里以手动列表示意 for img_name, label in [("5e4d1ee0d.png", 0), ("77291b3ad.png", 1)]: dst = root / "train" / class_names[label] / img_name shutil.copy(f"images/{img_name}", dst)

这里建议手动维护一个img_name -> label的映射文件,比写死 if 判断更可靠。我一般会在项目根目录放一个 labels.txt,每行是“文件名 类别索引”,然后统一读进来复制。样本量上百之后,手动改代码很容易漏。

接下来用 ImageFolder 读取,并断言顺序:

from torchvision.datasets import ImageFolder from torchvision import transforms train_transform = transforms.Compose([ transforms.RandomResizedCrop(224, scale=(0.7, 1.0)), transforms.RandomHorizontalFlip(), transforms.ColorJitter(0.2, 0.2, 0.2), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), ]) train_set = ImageFolder(root / "train", transform=train_transform) print(train_set.classes, train_set.class_to_idx) # 关键:验证与 class.json 一致 assert train_set.classes == class_names, "类别顺序不匹配!"

这里必须用断言的场景是:ImageFolder 自动扫描子目录并按名称排序生成 classes,而 class_names 来自 class.json。两者只要有一个字符不同(比如大小写、空格),后面的训练就全乱了。这个断言能让你在训练第一轮就发现错误,而不是等几个小时后看准确率才发现。

3.2 训练主循环:AdamW、Cosine 与 AMP

CAS-ViT 是轻量模型,但不代表可以用 ResNet 的老一套训练参数。ViT 系模型对优化器很敏感,我在这类任务上默认用 AdamW,加线性 warmup 和余弦退火。下面是一份完整的训练循环核心代码,拿过去改改数据路径就能跑:

import math import torch from torch import nn from torch.cuda.amp import autocast, GradScaler from casvit import build_cas_vit model = build_cas_vit("casvit_tiny", num_classes=len(class_names)) model = model.cuda() criterion = nn.CrossEntropyLoss(label_smoothing=0.1) optimizer = torch.optim.AdamW(model.parameters(), lr=1e-3, weight_decay=0.05) iters_per_epoch = len(train_loader) total_iters = iters_per_epoch * 100 # 假设训练 100 epoch warmup_iters = 5 * iters_per_epoch # 前 5 epoch 线性升 lr def lr_schedule(step): if step < warmup_iters: return step / warmup_iters prog = (step - warmup_iters) / (total_iters - warmup_iters) return 0.5 * (1 + math.cos(math.pi * prog)) scaler = GradScaler() for epoch in range(100): for i, (x, y) in enumerate(train_loader): step = epoch * iters_per_epoch + i lr = lr_schedule(step) for g in optimizer.param_groups: g["lr"] = lr x, y = x.cuda(), y.cuda() with autocast(): logits = model(x) loss = criterion(logits, y) scaler.scale(loss).backward() scaler.step(optimizer) scaler.update() optimizer.zero_grad(set_to_none=True)

这段代码里值得注意的参数有几个:

  • label_smoothing=0.1,防止轻量模型在类别少时过度自信。如果你只有 4 类,这个值很关键。
  • warmup_iters 设成 5 个 epoch,如果数据集很小,可以缩到 1 个 epoch,否则训练初期 loss 会冲高。
  • weight_decay=0.05 是 ViT 系常用配置,不要照搬 CNN 的 0.0001。
  • AMP 混合精度用了 GradScaler,CAS-ViT 本身很轻,但 AMP 在边缘卡上更稳,显存约降一半。

如果你想知道 lr 在每个 step 的具体值,可以在for g in optimizer.param_groups后打印一下,确保 warmup 阶段 lr 从 0 平滑涨到目标值。余弦退火阶段 lr 会降到接近 0,这是正常现象,不要中途以为代码 bug 而手动拉高。

3.3 训练日志记录:loss、acc 和梯度范数

训练时只盯着 loss 容易漏掉问题。我一般会额外记录梯度范数,它是判断模型有没有爆掉的最早信号:

total_norm = 0.0 for p in model.parameters(): if p.grad is not None: total_norm += p.grad.norm().item() ** 2 total_norm = total_norm ** 0.5 if step % 50 == 0: print(f"epoch {epoch} step {i} loss {loss.item():.4f} " f"lr {lr:.2e} grad_norm {total_norm:.2f}")

梯度范数正常范围在 1 到 10 之间。如果某一步突然冲到 100 以上,说明学习率偏大或者 CATM 里的 temperature 饱和了。轻量模型对梯度范数比较敏感,建议把 grad_clip 加上,torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0),能避免大部分训练中期崩溃。

另外我习惯维护一个 EMA 权重。ViT 系模型训练后期波动大,EMA 平均后的权重通常比最后一轮权重涨 1 到 2 个点。这份资源跑出来的基线结果,用 EMA 后一般还能再稳定一点。训练日志里有没有 EMA 指标,是判断一个项目后半程质量的标准。

4. 参数调优:输入分辨率、Batch Size 与学习率联动

CAS-ViT 轻,但不代表所有超参都能照搬 Swin。我在这个资源上跑分类时总结了一套超参联动规则,核心是:分辨率决定 token 数,batch size 决定学习率上限,正则化强度决定最终准确率的天花板。

4.1 分辨率与 Patch Embedding 的 token 数边界

CAS-ViT 仍然有 Patch Embedding,token 数 N 由 N=(输入尺寸 / patch_size)² 决定。patch=16 时,224 输入得到 196 个 token;384 输入得到 576 个 token。虽然 CAS-ViT 的注意力部分是线性的,但 token 变多后,后面的 FFN、LayerNorm 以及最后的分类头计算量都会同步上涨,所以不是“可以无限放大分辨率”。

我的一般做法是:普通场景用 224;如果图像里的小目标多,比如零件缺陷分类,用 320 或 384。如果你想试不同分辨率,不需要改模型定义,因为 CATM 本身没有绝对位置编码依赖。验证一下:

model.eval() for size in [224, 288, 320, 384]: x = torch.randn(1, 3, size, size) with torch.inference_mode(): out = model(x) print(size, out.shape)

这段代码能直接确认你的模型能不能接受任意输入尺寸。如果报错说 position embedding 维度不匹配,说明代码里还带了可学习位置编码;那就固定 224 训练,推理时同样 resize 到 224,别搞多尺寸输入。

4.2 Batch Size 与学习率的线性缩放法则

轻量模型在单卡上能塞下较大 batch,但学习率不是固定值。Transformer 训练里一个经典经验是:batch size 翻倍,学习率近似翻倍。我在这份资源上从 bs=64 调到 bs=256 时,lr 从 1e-3 提到 3e-3 反而更稳。超过 3e-3 后,即使有 warmup,第一个 epoch 的 loss 也会出现明显震荡。

用脚本自动算学习率最稳:

base_batch_size = 64 base_lr = 1e-3 current_bs = 256 lr = base_lr * (current_bs / base_batch_size) ** 0.9 print(f"建议 lr = {lr:.2e}")

指数取 0.9 而不是 1.0,是我个人的经验:全量线性缩放对轻量模型来说过于激进,取 0.9 左右相当于在保守和激进之间折中。你可以先按这个值跑 10 轮,观察 loss 下降曲线是平稳还是抖动,再微调 0.2 以内。

4.3 轻量 Transformer 的正则化:DropPath、Weight Decay、MixUp

轻量模型本身参数少,但正则化不能省。真正影响泛化的往往不是参数量,而是训练策略。下面这张表是我在这类 4~10 类小数据集上的默认配置:

配置项推荐值备注
DropPath0.1小数据集从 0.05 起调
Weight Decay0.05与 AdamW 搭配
Label Smoothing0.1防止 over-confidence
RandomResizedCrop scale(0.7, 1.0)减少背景噪声
MixUp alpha0.24 类小数据建议 0.2
RandAugment2, 10幅度和数量

DropPath 是 Transformer 里最有用的正则化手段,它的作用相当于给每个 block 的残差路径随机置零。实现很简单:

import torch.nn as nn class DropPath(nn.Module): def __init__(self, p=0.1): super().__init__() self.p = p def forward(self, x): if not self.training or self.p == 0: return x keep_prob = 1 - self.p shape = (x.shape[0],) + (1,) * (x.ndim - 1) mask = torch.rand(shape, device=x.device) < keep_prob return x * mask / keep_prob

这里的 mask 是按 batch 维度生成的,同一个 batch 里的所有 token 共享同一块 mask,DropPath 才有效。如果你把它做成每个 token 独立随机,效果和 Dropout 一样,就失去“路径丢弃”的意义了。使用时接在 attention 输出和 FNN 输出之后:

x = x + self.drop_path(self.attn(self.norm1(x)))

如果数据集只有几百张图,RandAugment 的幅度别开太大,2 级幅度加 10 次操作数足够。幅度再大,模型会把增强后的噪声当成语义,验证集上的损失反而提前上升。

5. CAS-ViT 实战常见问题排查:五个踩坑记录

这部分记录的是我在轻量 Transformer 分类项目里实际撞过的坑。每个都按现象→原因→解决写,你可以直接对着序号排查。

5.1 坑位一:class.json 读出来的顺序和 ImageFolder 不一致

现象:训练 loss 正常下降,甚至降得很漂亮,但验证准确率一直停留在 25% 附近(4 类任务),像完全没学过一样。

原因:class_map 的键是字符串数字,但遍历for k in class_map得到的顺序是字典插入顺序,不是 0,1,2,3 的排序。ImageFolder 则按目录名字典序排列。两者顺序错位后,模型半路把“猫”的图片学成了“狗”的标签。

解决:先sorted(class_map.items(), key=lambda x: int(x[0]))强制按索引排序,再用assert train_set.classes == class_names兜底。从那以后,我每次数据准备脚本都强制走一遍这段断言,再也没有犯过类似错误。

5.2 坑位二:相同 Batch Size 下显存比 Swin 还高

现象:换用 CAS-ViT 后显存没有明显下降,甚至偶尔 OOM。

原因:模型权重轻,但代码里保留了标准多头注意力的备用实现。很多仓库为了兼容旧权重,会在 Block 里写if use_linear_attn: ... else: self.attn = nn.MultiheadAttention(...)。PyTorch 即使走了 if 分支,else 分支里已经实例化的 Module 仍会占用显存。

解决:先打印模型里所有MultiheadAttention实例的数量。确认不需要后,把无关的 attn 分支整个删掉,而不是置空。同时开启 AMP,这样 Batch Size 可以直接乘 1.5。真实项目里这一步省下的显存比你调参更可观。

5.3 坑位三:训练一开始 loss 就卡在 log(类别数) 附近

现象:前 10 个 epoch loss 稳定在 1.0 左右(4 类任务),梯度范数降到 0.01 以下,几乎没有训练迹象。

原因:加性注意力里的 temperature 初始值太大,tanh 输入饱和,梯度传不回去。另一个常见原因是模型 stem 部分的参数被误冻结了,或者 optimizer 只拿到了部分参数。

解决:把 temperature 初始化为 0.05~0.1,并确认它是 nn.Parameter 而不是普通张量。我一般在构造 optimizer 后打印第一个 block 的 temperature 值一眼确认:

print(model.token_mixer.temperature.item())

输出不是 0.1 就说明权重初始化被覆盖了。另外检查[p.requires_grad for p in model.parameters()]里有没有大量 False。

5.4 坑位四:Top-1 不错但某个类召回率极低

现象:总体准确率 92%,但第 2 类预测几乎全部落到第 3 类。

原因:train 和 val 的目录内容有重叠,模型在训练时已经见过验证图的增强版本;或者第 2 类样本太少,RandomResizedCrop 的 scale 下限 0.7 经常把关键区域裁掉。

解决:先用文件名的集合差运算对比 train/val 目录,去掉交集。再按类别统计样本数,少于 50 张的类别不要用过强的空间增强,把 scale 改成 (0.9, 1.0)。这一条在拿公开数据集做分类时尤其常见,很多数据集本身就有重复图片。

5.5 坑位五:导出 ONNX 时 CATM 算子不支持动态 shape

现象:torch.onnx.export 报错,提示 InstanceNorm2d 的输入维度或 view 操作无法推导。

原因:CATM 里为了省显存用了 flatten 和 reshape,导出 ONNX 时动态形状下 view 的 shape 推导失败。InstanceNorm2d 在导出一层时也会遇到不支持的属性。

解决:导出前固定 H/W 尺寸,只放开 batch 维度。把代码里的view改成permute加contiguous的组合,并在导出时使用dynamic_axes={"input": {0: "batch"}, "output": {0: "batch"}}。这样导出的 ONNX 在带 shape 约束时稳定,不会在推理引擎里报维度错误。

6. 用推理脚本跑通单张图:从 Top-1 到置信度输出

最后一步是把训练好的权重接进推理流程。这里我给一份可复制的单图推理脚本,重点关注预处理和权重加载细节:

import json import torch import torchvision.transforms as T from PIL import Image from casvit import build_cas_vit device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model = build_cas_vit("casvit_tiny", num_classes=4).to(device) model.load_state_dict(torch.load("best.pth", map_location=device)) model.eval() with open("class.json", encoding="utf-8") as f: class_map = json.load(f) class_names = [class_map[str(i)] for i in range(len(class_map))] transform = T.Compose([ T.Resize(256), T.CenterCrop(224), T.ToTensor(), T.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), ]) img = Image.open("77291b3ad.png").convert("RGB") x = transform(img).unsqueeze(0).to(device) with torch.inference_mode(): logits = model(x) probs = torch.softmax(logits, dim=1)[0] topk = torch.topk(probs, k=3) print("Top-3:") for val, idx in zip(topk.values, topk.indices): print(f"{class_names[idx]}: {val:.4f}")

这段代码有两点值得说明:.convert("RGB")能处理 RGBA 或单通道 PNG,漏掉它会得到 4 通道输入报错;torch.inference_mode()比no_grad()更快,因为它同时关闭了梯度记录和自动求导的图跟踪。注意:如果加载权重时报 key 不匹配,先检查 build_cas_vit 的 num_classes 是否和训练时一致。

验证模型是否正常,不能只看单张图输出。我一般会准备一张不属于任何类别的负样本,喂进去看最高置信度。如果模型对负样本也给出 0.9 以上的置信度,说明特征空间没有收敛,需要回到训练阶段调正则化。提示:可以加一个阈值逻辑,if probs.max() < 0.6: print("unknown"),对真实场景更友好。

这次拆 CAS-ViT 让我养成一个习惯:不管模型多轻,我都会在训练前打印一次数据目录顺序和类别顺序,训练一轮后看一眼 loss 能不能降到 1.0 以下,再丢到后台跑长训。从那以后,我在四五个轻量分类项目里都没有再犯标签错位的低级错误。希望这个流程能帮你在 CAS-ViT 实战里少走一步弯路,下载后直接拿 class.json 和样例图对一遍。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询