☰
人脸表情识别消融实验:ResNet50与注意力模块实战解析
2026/10/2 2:40:22 网站建设 项目流程

简介:面向计算机视觉与深度学习初学者及研究者,这是一套基于多模型消融实验的人脸表情识别项目完整源码。项目基于PyTorch 1.8搭建,集成了VGG16、ResNet50、InceptionV3以及自定义CNN等四种主干网络,并将CBAM、SE、ECA三种注意力机制分别嵌入上述模型,在FER2013与RAF数据集上开展系统性对比消融实验,最终验证ResNet50结合CBAM的配置在识别精度与模型拟合程度上均达到最优,相关结论具备较强的参考价值。资源包共18个文件,以7个Python源码为核心,覆盖数据加载、模型构建、注意力模块、训练与测试等完整流程;另含5个keep占位文件、3张实验图片、2份说明文档及1个内嵌压缩包,整体大小仅244KB,目录结构清晰,便于直接对照学习。已有443人浏览学习,适合复现实验、完成课程设计,或深入理解注意力机制对人脸表情识别任务的具体影响。

1. 人脸表情识别项目的标题拆解:消融实验才是这套源码的核心价值

在FER2013这类公开数据集上,把ResNet50的层数堆到152层,识别准确率并不会比ResNet50加一个Coordinate Attention更高,这是很多人脸表情识别项目里最反直觉的结论。这套Python源码要解决的不是“怎么把表情识别准确率刷到99%”,而是给你一套严谨的多模型横向对比框架——以ResNet50为主干,分别接入SE、CBAM、Coordinate Attention等注意力模块,用消融实验量化每个模块对表情识别效果的贡献。适合正在做毕业设计、准备论文实验章节、或者要验证一个新注意力模块是否值得上线的工程师。它不追求模型最前沿,追求的是变量可控、结果可复现、每个参数改动都能说清楚原因。

2. 人脸表情识别先解决数据问题:FER2013预处理与PyTorch数据流水线

2.1 为什么选FER2013做消融实验基准数据集

消融实验最怕数据集本身波动太大,模型还没开始比,数据就把结果搞乱了。常见做法是用FER2013作为基准数据集,它有28709张训练图、3589张公共验证图和3589张测试图,图片统一48x48灰度,分7类:生气、厌恶、恐惧、开心、难过、惊讶、中性。类别不全但足够做模块对比。AffectNet数据量更大但标注噪声高,用来做最终效果展示可以,做消融实验对比基线时反而容易引入额外变量。

这个项目源码里对数据集的约定一般是:训练集/验证集按原始划分不动,测试集只在最终评估时碰一次。表情识别有个经典痛点就是类别不均衡,“厌恶”类样本常年只有几百张,“开心”“中性”各有几千张,所以预处理阶段就要把样本分布统计好。

# dataset_stats.py import numpy as np from collections import Counter import pandas as pd df = pd.read_csv('fer2013.csv') # 原始csv三列:emotion, pixels, Usage df['pixels_list'] = df['pixels'].apply(lambda x: np.array(x.split(), dtype=np.float32)) # 统计各类别样本数,用于后续加权采样或loss权重设置 train_df = df[df['Usage'] == 'Training'] counter = Counter(train_df['emotion']) print(counter) # 输出示例:Counter({6: 4965, 3: 7215, 4: 4830, ...}) # emotion映射:0=生气,1=厌恶,2=恐惧,3=开心,4=难过,5=惊讶,6=中性

这段代码逻辑很简单但很关键,pixels列是一串用空格分隔的灰度值字符串,必须转成float32数组才能进模型。如果不先统计分布,后面做数据增强时可能会对“厌恶”类做过度增强导致过拟合。参数上注意dtype必须用float32,PyTorch默认float32,如果用float64会增加显存占用且无精度收益。

2.2 用PyTorch搭一套训练数据流水线,把灰度图归一化做对

表情识别和ImageNet分类有个重要差别:输入是单通道灰度图,而ResNet50预训练权重是在三通道ImageNet上训出来的。这里有两个派别:一种是把灰度图复制三遍变成伪三通道,另一种是只用第一通道卷积核。项目源码里常见的是第一种,因为可以直接加载torchvision的ResNet50预训练权重做迁移学习,省去改网络结构的麻烦。

# fer_data_loader.py import torch from torch.utils.data import Dataset, DataLoader from torchvision import transforms from PIL import Image import numpy as np class Fer2013Dataset(Dataset): def __init__(self, df, transform=None): self.df = df self.transform = transform def __len__(self): return len(self.df) def __getitem__(self, idx): row = self.df.iloc[idx] pixels = np.array(row['pixels_list']).reshape(48, 48).astype(np.uint8) img = Image.fromarray(pixels, mode='L') # 'L'表示灰度模式 img = img.convert('RGB') # 灰度--->伪三通道 label = int(row['emotion']) if self.transform: img = self.transform(img) return img, label # 训练transform:随机水平翻转+随机遮挡+归一化 train_transform = transforms.Compose([ transforms.RandomHorizontalFlip(p=0.5), transforms.RandomAffine(degrees=15, translate=(0.1, 0.1)), transforms.ColorJitter(brightness=0.2, contrast=0.2), transforms.ToTensor(), transforms.Normalize(mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5]) ])

这里有个容易翻车的地方:Image.fromarray接收的dtype必须是uint8,如果上游pixels转成了float32,这里会直接报错或生成全黑图。Normalize用0.5/0.5是因为灰度图复制三遍后像素范围是0-255,ToTensor后变成0-1,减0.5除0.5后数据落在-1到1之间,这和ImageNet预训练用的mean=[0.485,0.456,0.406]不一样,但实测在表情识别上0.5均值效果更好,因为灰度图没有彩色分布偏移。

2.3 数据增强参数怎么设:RandomAffine和RandomErasing的边界

消融实验里数据增强策略必须固定,否则你没法说清是注意力模块起作用还是增强策略起作用。项目源码里一般会固定一套增强参数:RandomHorizontalFlip概率0.5、RandomAffine旋转15度、平移0.1、ColorJitter亮度对比度各0.2。这些参数来自人脸对齐后的经验值,旋转超过20度会让表情语义发生变化,比如“惊讶”在过度旋转后可能被人类标注者看成“恐惧”。

这里要特别提醒,验证集和测试集不要加RandomAffine和ColorJitter,只做ToTensor和Normalize。很多人会把训练增强直接套到验证集上,结果验证loss抖动得像心跳图,模型好坏根本看不出来。数据流水线固定后,下一步才是模型主干和注意力模块的嫁接,这是这套源码的核心结构设计。

3. ResNet50主干与Attention模块的实现:SE、CBAM、Coordinate Attention拆开讲

3.1 为什么ResNet50适合做消融实验的骨架,而不是ResNet18或EfficientNet

选ResNet50做消融实验主干是综合考虑深度、预训练权重的可获得性、以及模块插入的便利性。ResNet18太浅,注意力模块加上去准确率提升不明显,分辨不出模块好坏;ResNet101训练时间太长,消融实验要跑四个以上模型变体,时间成本翻倍。ResNet50在ImageNet上的预训练权重质量高,且torchvision里直接torchvision.models.resnet50(pretrained=True)就能拿到,不用自己训一个backbone。

更关键的是结构解耦性。ResNet50分成stem(首层卷积)、layer1到layer4四个残差阶段,注意力模块可以很方便地插在任意一个阶段后面。项目源码里最常见的做法是插在layer3和layer4之间,因为48x48输入经过四次下采样后,layer3输出的特征图是6x6,空间分辨率还在,注意力能学到空间位置信息;layer4输出是3x3,分辨率太低,注意力模块施展不开。

3.2 写一个Coordinate Attention模块,吃透SENet的通道注意力原理

很多人在源码里看到Attention第一反应是NLP里的Transformer Self-Attention,但人脸表情识别里的Attention几乎都是轻量级即插即用模块,最常见的是SENet的SeBlock和Coordinate Attention。SENet做的是通道注意力:把特征图压缩成一个通道描述符,学习每个通道的重要性权重,然后乘回去。Coordinate Attention是SENet的一个变体,它在压缩时保留了空间方向信息,把特征图沿高度和宽度方向分别做池化,再进行卷积融合。

# attention_modules.py import torch import torch.nn as nn import torch.nn.functional as F class CoordAtt(nn.Module): def __init__(self, inp, oup, reduction=32): super().__init__() # 高度方向与宽度方向的池化,保留位置信息 self.pool_h = nn.AdaptiveAvgPool2d((None, 1)) self.pool_w = nn.AdaptiveAvgPool2d((1, None)) mip = max(8, inp // reduction) # 隐藏层通道数,reduction是压缩比 self.conv1 = nn.Conv2d(inp, mip, kernel_size=1, stride=1, padding=0) self.bn1 = nn.BatchNorm2d(mip) self.act = nn.Hardswish() self.conv_h = nn.Conv2d(mip, oup, kernel_size=1, stride=1, padding=0) self.conv_w = nn.Conv2d(mip, oup, kernel_size=1, stride=1, padding=0) def forward(self, x): identity = x n, c, h, w = x.size() x_h = self.pool_h(x) # (n, c, h, 1) x_w = self.pool_w(x).permute(0, 1, 3, 2) # (n, c, 1, w) 转成 (n, c, w, 1) y = torch.cat([x_h, x_w], dim=2) # 沿高度维度拼接 y = self.conv1(y) y = self.bn1(y) y = self.act(y) x_h, x_w = torch.split(y, [h, w], dim=2) x_w = x_w.permute(0, 1, 3, 2) a_h = self.conv_h(x_h).sigmoid() a_w = self.conv_w(x_w).sigmoid() out = identity * a_w * a_h return out

CoordAtt的forward里有几个细节容易被忽略。池化时x_w的shape是(n, c, 1, w),要permute成(n, c, w, 1)才能和x_h在dim=2上拼接。拼接后的y经过卷积融合,再split回h和w两个方向。最后每个方向各自过sigmoid得到注意力权重,a_w和a_h广播相乘后乘回输入。reduction=32是常见默认值,输入通道2048时隐藏层是64;reduction改太小参数量涨得快,在消融实验里要统一所有注意力模块的reduction,否则不公平。

如果你想把NLP里的multi-head注意力拿来做表情识别,也可以,但效果通常不如这种轻量模块,因为特征图只有6x6,自注意力学到的全局关系有限,还容易过拟合。这里踩过的坑是,有人把attention is all you need里的公式硬搬过来,训练速度慢一倍,准确率反而掉了两个点。所以源码里用Coordinate Attention这类模块,不是因为它名字里有attention,而是它参数量小、插进去稳定、可解释性强。

3.3 用一行代码改造ResNet50:替换layer4前的Bottleneck输出

把注意力模块接进ResNet50,不需要重写整个网络,只需要在torchvision的ResNet基础上做外科手术式的修改。核心思路是拿到resnet50的children列表,在layer4之前插入CoordAtt模块,或者在layer4的每个Bottleneck后都插入。项目源码里一般两种都支持,通过config控制。

# resnet50_attention.py import torchvision.models as models import torch.nn as nn def build_backbone(arch='resnet50', attention='ca', pretrained=True): model = getattr(models, arch)(pretrained=pretrained) # 冻结stem和layer1的bn,加快收敛并保留预训练特征 for name, param in model.named_parameters(): if 'layer1' in name or 'conv1' in name or 'bn1' in name: param.requires_grad = False if attention == 'ca': attn = CoordAtt(inp=1024, oup=1024, reduction=16) elif attention == 'se': attn = SEBlock(inp=1024, reduction=16) elif attention == 'cbam': attn = CBAMBlock(inp=1024, reduction=16) else: attn = nn.Identity() # 在layer3输出之后、layer4之前插入注意力模块 # layer3输出的通道数是1024,layer4输入期望也是1024 old_layer4 = model.layer4 model.layer4 = nn.Sequential(attn, old_layer4) return model

这里有个隐藏的坑:torchvision里的ResNet50前向是stem -> layer1 -> layer2 -> layer3 -> layer4 -> avgpool -> fc。layer4本身是Sequential容器,你直接把它替换成nn.Sequential(attn, old_layer4)是可行的。但注意CoordAtt的oup必须是1024,因为layer3输出是1024通道,layer4第一个Bottleneck的conv1接收的输入通道也是1024。如果这里通道数写错了,前向跑起来会直接报shape mismatch,而且报错信息会指向layer4内部,排查起来很费劲。

另外,冻结stem和layer1的参数是个实践技巧。48x48的输入比ImageNet的224x224小很多,浅层特征差异大,不冻结的话微调前期容易把预训练权重的底层纹理特征破坏掉。layer2和layer3保持可训练,layer4和注意力模块重点更新,这能显著缩短收敛时间。项目里一般用这种配置跑30个epoch就能达到基线水平。

4. 多模型消融实验设计:四组对照实验与训练脚本实现

4.1 消融实验的矩阵怎么定:Baseline、+SE、+CBAM、+CA、+CA_SE叠加

消融实验的核心是控制变量。这套源码的模型对比表一般长这样:

模型编号主干注意力模块参数量(约)说明
M0ResNet50无25.6M基线
M1ResNet50SEBlock25.7M只加通道注意力
M2ResNet50CBAM25.8M通道+空间注意力
M3ResNet50CoordAtt25.7M通道+方向位置注意力
M4ResNet50CA + SE25.9M双注意力叠加测试

参数量差距只有0.1M级别,这说明注意力模块是轻量的,准确率差异不是参数量带来的,而是注意力机制本身。M4最容易出现的现象是准确率反而比M3低,因为两层注意力对6x6特征图来说冗余度过高,梯度传播路径变长。源码里一般会把这五个模型都跑一遍,但训练资源配置成一致——同一个GPU、同batch size、同lr、同epoch数。

4.2 训练脚本怎么写才不混乱:config字典驱动多组实验

多模型训练最怕的是代码里到处硬编码模型名,跑完M1忘了M0,结果一比对发现baseline的lr和M1不一样。项目源码里会用单个config字典管理所有超参数,每次训练只改model_name和attention_type两个字段。

# train_ablation.py import torch import torch.nn as nn import torch.optim as optim from torch.optim.lr_scheduler import CosineAnnealingLR from torch.utils.tensorboard import SummaryWriter import copy, os, json, time def train_model(cfg): torch.manual_seed(cfg['seed']) torch.cuda.manual_seed_all(cfg['seed']) model = build_backbone( arch=cfg['arch'], attention=cfg['attention'], pretrained=True ).cuda() model.fc = nn.Linear(model.fc.in_features, 7).cuda() criterion = nn.CrossEntropyLoss( weight=torch.tensor(cfg['class_weight']).cuda() ) optimizer = optim.AdamW( model.parameters(), lr=cfg['lr'], weight_decay=cfg['weight_decay'] ) scheduler = CosineAnnealingLR(optimizer, T_max=cfg['epochs']) train_loader = build_dataloader(split='Training', cfg=cfg) val_loader = build_dataloader(split='PublicTest', cfg=cfg) best_acc = 0.0 for epoch in range(cfg['epochs']): model.train() running_loss = 0.0 for imgs, labels in train_loader: imgs, labels = imgs.cuda(), labels.cuda() optimizer.zero_grad() outputs = model(imgs) loss = criterion(outputs, labels) loss.backward() optimizer.step() running_loss += loss.item() model.eval() correct = 0 total = 0 with torch.no_grad(): for imgs, labels in val_loader: imgs, labels = imgs.cuda(), labels.cuda() outputs = model(imgs) _, predicted = torch.max(outputs, 1) total += labels.size(0) correct += (predicted == labels).sum().item() val_acc = 100.0 * correct / total if val_acc > best_acc: best_acc = val_acc best_model = copy.deepcopy(model.state_dict()) writer.add_scalar(f'{cfg["exp_name"]}/loss', running_loss / len(train_loader), epoch) writer.add_scalar(f'{cfg["exp_name"]}/val_acc', val_acc, epoch) print(f'Epoch {epoch+1}/{cfg["epochs"]} | Loss: {running_loss/len(train_loader):.4f} | Val Acc: {val_acc:.2f}%') torch.save(best_model, f'{cfg["exp_name"]}_best.pth') return best_acc if __name__ == '__main__': base_cfg = { 'arch': 'resnet50', 'epochs': 30, 'lr': 3e-4, 'weight_decay': 1e-4, 'batch_size': 64, 'seed': 42, 'num_workers': 4, } # 跑4组消融,只改attention字段和exp_name for attn_name in ['none', 'se', 'cbam', 'ca']: cfg = copy.deepcopy(base_cfg) cfg['attention'] = attn_name cfg['exp_name'] = f'ablation_{attn_name}' best = train_model(cfg) print(f'{attn_name} best val_acc: {best:.2f}%')

训练脚本里有几个参数值得较真。lr=3e-4搭配AdamW是微调ResNet50的常用起手式,如果是从头训练要提到1e-3,但消融实验必须用迁移学习,否则训练时间翻三倍而且结果不稳定。class_weight这里要传长度为7的Tensor,数值按训练集类别频率的倒数归一化,类别不均衡时这个参数比任何注意力模块都管用,但它不属于消融变量,所有模型都要用同一套class_weight。num_workers=4在Windows上有时会报BrokenPipeError,改成0能跑但速度慢,Linux上设4或8都行。

4.3 消融实验的评估协议:只信测试集,验证集结果不能写进结论

评估阶段有两条铁律。第一条:所有模型选checkpoint的原则一致,都用验证集准确率最高的那个epoch,不能M0用epoch25、M3用epoch15。第二条:最终结论只用PrivateTest(测试集)准确率,验证集准确率只用来选模型。为了做到这一点,源码里会在训练完成后单独写一个eval_test.py脚本,统一加载各模型的best.pth,在相同的预处理下跑测试集,输出每个类别的分类准确率和macro-F1。

这里要提醒一个血泪坑:FER2013的PrivateTest一定要警惕标签错位,因为csv原始文件是按行存储的,如果有人用pandas做过dropna或reset_index,行顺序一变,结果就全错了。源码里加载测试集时不要重新shuffle,不要过滤脏数据,加载顺序严格按csv原始行号来,否则你报的准确率对不上别人的结果。

5. 消融实验避坑指南:五个让准确率对不上的常见问题与排查方法

5.1 梯度更新了但训练loss不降:注意力模块把BatchNorm位置放错了

现象:加入SEBlock或CoordAtt后,训练loss从1.9降到1.7就卡住,验证准确率在30%左右徘徊,但单独跑ResNet50就能正常收敛到65%以上。

原因:注意力模块里的Conv2d后面跟了BatchNorm,但插入位置在ResNet50的layer3和layer4之间时,BatchNorm的moving_mean和moving_var没有初始化好,而且如果模型里其他层被冻结,梯度更新到注意力模块后,BN的统计量抖动很大。

解决:先把注意力模块里的BatchNorm换成GroupNorm(num_groups=32),或者在训练前用一小批数据跑几次前向,让BN统计量预热。更快的方法是把reduction调大,从16改成32,隐藏层通道数变少,BN的方差波动自然变小。检查看loss曲线如果前10个epoch是缓慢下降然后平台,十有八九是这个问题。

5.2 验证准确率比训练集高:数据泄漏发生在预处理阶段

现象:训练准确率85%,验证准确率却92%,明显违反直觉。仔细一看,原来是训练集里做了RandomAffine,但验证集的pixels数组在读取时没有做归一化,像素值范围还是0-255,模型输出的logits分布完全不对。

原因:验证集和测试集的transform里,Normalize的mean和std参数复制错了,或者ToTensor没有调用,导致输入分布和训练时不一致。这个问题隐蔽在验证代码里,前向推理一般不报错,只是结果悄悄变差或变好。

解决:写一个断言检查,在第一轮迭代时打印输入Tensor的均值,训练集和验证集都应该在-0.5到0.5附近。源码里可以加一行代码:assert abs(imgs.mean().item()) < 0.6, "normalize可能写错了",训练阶段直接让程序报错而不是等着结果对不上。

5.3 ResNet50加载预训练权重后发现fc层维度对不上:源码里改了分类头

现象:报错Error(s) in loading state_dict ... size mismatch for fc.weight,模型能训练但准确率一直很低。

原因:torchvision的ResNet50默认fc输出1000类,要做7类表情分类必须替换fc。有人在build_backbone里改了fc,但又忘了改load_state_dict的strict参数,导致预训练权重里fc相关的键全部加载失败。

解决:加载预训练时显式忽略fc层:model.load_state_dict(torch.load('resnet50.pth'), strict=False),然后再替换fc。或者在build_backbone里先替换fc,再加载权重。项目源码里建议的顺序是:建模型 -> 替换fc -> load_state_dict(strict=False) -> 冻结浅层。如果把顺序写成加载再替换,fc那层的random init权重会保留,消融实验里所有模型的fc都必须用同一个随机种子初始化,否则fc层初始化的差异会盖过注意力模块的差异。

5.4 双注意力叠加效果反而变差:不是模块写错,是特征图太小

现象:M4(CA+SE叠加)验证准确率比M3低,单独看两个模块的代码逻辑都没问题,前向也能跑通。

原因:48x48输入经过ResNet50下采样后,layer3输出的特征图只有6x6。SE做通道压缩时丢掉了空间信息,CA虽然保留方向信息但和SE叠加时,两个注意力权重相乘相当于在36个空间位置上做了两轮重标定,信息被过度压缩,梯度回传时注意力权重趋近于饱和。

解决:这种情况不是bug,是模型的特性。如果要叠加,把注意力模块分别插到不同位置,比如SE插在layer2后,CA插在layer4前,让它们在不同尺度上各管一段。不要两个都插在layer3和layer4之间。消融实验的价值就在这里——不是为了证明模块越多越好,而是找出边界。

5.5 训练日志显示准确率有波动但最终结果对不上GitHub上的开源数字

现象:自己复现的FER2013准确率在68%左右,别人项目README上写72%,怎么调都差一截。

原因:最常见的差异来自数据集的划分方式。FER2013官方是Training/PublicTest/PrivateTest三份,但有些博客把Training和PublicTest合并再重新随机划分,这样训练集变大,验证集变小,结果自然偏高。还有一种情况是有人做了人脸对齐预处理,把48x48的原始图用OpenCV的landmark对齐到44x44,相当于额外用了外部数据信息。

解决:源码关键要写清楚评测协议。项目仓库里一般会有一个README或实验记录文档,描述数据和划分方式。自己复现时先跑一次官方baseline,如果ResNet50不加任何注意力模块能跑到70%,说明数据流水线正常;如果只有65%,多半是增强策略或预训练权重加载有问题。消融实验追求的是相对差异,不是绝对数值,M3比M0高2.5个百分点,这个相对收益才是论文里要写的。

6. 用Grad-CAM热力图验证注意力模块到底关注了哪些区域

训练结束后,除了看准确率表格,还要回答一个问题:注意力模块是不是真的帮模型把注意力放在了眼睛和嘴巴上。表情识别的关键区域基本集中在眼部、眉部和嘴部,如果模型学到的特征是背景或头发纹理,acc再高也是过拟合。项目源码里一般会写一个可视化脚本,用Grad-CAM生成热力图叠加到输入图上,每组模型各选几张典型样本做对比。

# grad_cam_visualize.py import cv2 import numpy as np import torch from torchvision import transforms def grad_cam(model, img_tensor, target_layer): activations = {} gradients = {} def forward_hook(module, input, output): activations['value'] = output def backward_hook(module, grad_input, grad_output): gradients['value'] = grad_output[0] hook = target_layer.register_forward_hook(forward_hook) hook_b = target_layer.register_full_backward_hook(backward_hook) output = model(img_tensor.unsqueeze(0).cuda()) score = output[0, output.argmax(dim=1)[0]] model.zero_grad() score.backward() weights = gradients['value'].mean(dim=(2, 3), keepdim=True) cam = (weights * activations['value']).sum(dim=1, keepdim=True) cam = F.relu(cam).squeeze().cpu().data.numpy() cam = cv2.resize(cam, (48, 48)) cam = (cam - cam.min()) / (cam.max() - cam.min() + 1e-8) return cam

这段代码挂了两个hook,前向hook拿特征图,反向hook拿梯度,然后对特征图的通道维做梯度加权平均。target_layer在ResNet50上一般选layer4的最后一个Bottleneck,也就是model.layer4[-1],太浅的层热力图会贴着边缘,看不出语义区域。cam图resize回48x48后直接用OpenCV的applyColorMap转伪彩色叠加到原图上。

可视化结果通常能看到一个规律:baseline模型的热力图弥散在整个脸部,而加了CA模块的模型热力图更集中,亮区会聚在眼睛和嘴角附近。这就是消融实验在定性层面的价值——不光告诉你模块带来了0.8%的提升,还告诉你提升来自哪里。另外,热力图的对比要和准确率对比一起看,如果M3准确率高但热力图看不出明显区域集中,那可能是class_weight的影响,不是注意力模块的功劳。

最后再提一个工程习惯:每次跑完一组消融实验,把模型名、seed、验证准确率、测试准确率、热力图路径、以及训练时用的GPU型号和PyTorch版本写进一个results.json,别只存在终端输出里。很多项目过了三个月回来看不懂自己当时为什么M2比M1高,就是因为没留版本记录。这组源码的最终价值不在于跑出多高的准确率,而在于你随时能把每个模块的贡献用数字和热力图讲清楚。希望帮到你。

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

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

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

立即咨询