☰
无参考图像质量评价:基于RankIQA的Python实现与多任务训练
2026/9/28 14:16:57 网站建设 项目流程

简介:基于Python的无参考图像质量评价完整源码,主要面向图像质量评估、人脸分析方向的开发者与算法工程师,解决不依赖原始参考图的质量评分问题。项目以RankIQA思路为核心,既有用于生成不同等级失真图像的Matlab数据准备脚本,又提供了原版(224输入)与面向人脸尺寸重新训练(128输入)两套网络实现;regression_network可用小网络拟合RankIQA排序结果,train_pose_qua.py则为一人脸角度+图像质量的多任务联合学习脚本。包内还详细给出训练技巧:先分任务预训练观察loss下限,再做loss加权以避免任务间相互干扰,同时针对低质量图像效果差的问题,将质量label非线性拉伸到0-10区间。资源共274个文件、约12.37MB,以163个Python源码文件为主,配合prototxt网络结构、TensorFlow checkpoint模型、shell脚本及说明文档等,目录划分清楚。目前已有1280人学习下载,适合需要复现无参考质量评价流程、研究多任务训练策略或进行源码级调参的读者。

1. 无参考图像质量评价:一个Python源码包解决“没原图”的打分问题

做图像质量评价的从业者,大概率都遇到过这种困境:手里只有一批监控抓拍或人脸图片,原图不存在,判断哪张画质能用全靠肉眼。无参考图像质量评价(NR-IQA)解决的就是这个场景,不借助参考图,仅凭单张图像输出质量分。这个Python源码项目提供了一套完整的无参考图像质量评价方案,按RankIQA的两阶段思路实现:先用排序数据训练Rank网络学会比较图像清晰度,让网络在“这张比那张干净”的相对监督信号里学会质量表征;再用一个轻量回归网络把相对分拟合为绝对质量分。附带失真图像生成脚本和多任务训练代码,可直接用于人脸角度和画质联合预测。适合做人脸识别前处理、图像质量筛选的工程师,也适合拿NR-IQA做毕设的研究者;新手按步骤能复现,熟手能直接替换数据集。

2. 数据制造:用Matlab批量生成失真等级,把排序监督信号造出来

2.1 Rank网络为什么需要排序数据

先解决一个基础问题:为什么不直接让网络回归人工打分?质量分本身是主观的,两个人看同一张图给出的分差可能很大,标注一致性差;而且低质量样本在真实场景里占比小,直接回归时网络很容易把高质量区间学得很准、低质量区间学成一团浆糊。这一点在后续验证阶段会暴露得特别明显,模型对模糊图输出完全失控,根源就是训练阶段的标签分布没处理好。

排序监督信号则不同。给定同一张原图,高斯模糊sigma从1加到5,谁更清晰是确定的,不存在争议。把这种确定性的相对关系送进网络,网络不需要知道绝对分,只需要让输出分数满足质量高低顺序。RankIQA这类两阶段方案的核心就是利用自动生成的排序数据,把标注成本压到几乎为零。训练的时候,每次从同一张原图的失真版本里抽一对图,例如level=1和level=3,label记为1;再抽一对反序的,label记为0。数据对的数量远大于原图数量,模型见过的质量过渡样本更多。

这也是为什么这个项目把data目录下的Matlab失真脚本放在第一步。没有这些不同等级的图像,Rank网络完全没有监督信号可学。如果你手头已经有带MOS标注的数据集,也可以在后期把人工分数当作回归目标微调;但先跑排序阶段,模型初始化更好,收敛更稳。数据制造这一步决定后面所有训练能否成立,值得多花时间把目录和命名规则定清楚。

2.2 data目录下的distortion脚本说明

data目录里的distortion.m就是干这件事:读一张原图,按指定失真类型和强度生成一张降质图像。常见失真类型与参数区间如下表,实际使用时按项目面对的退化场景裁剪。这里的失真类型不是随意选的,宁少勿滥——没有业务场景对应的失真,训出来的排序能力换到线上等于白训。

失真类型 mode质量等级映射参数区间说明
blursigma = 0.5 + 0.9 * level0.5 ~ 5等级越高越模糊
jpegquality = 90 - 12 * level30 ~ 90压缩率逐级升高
noisesigma = 0.01 * level0.01 ~ 0.05高斯加性噪声
downsamplescale = 1 / (1 + 0.15 * level)0.5 ~ 0.87先缩小再放大回原尺寸

对应脚本的核心逻辑提炼如下,完整文件在data目录里:

% distort.m 按照失真类型和等级生成低质量图像 % img : 原始高清图,uint8 % mode : 'blur' | 'jpeg' | 'noise' | 'downsample' % level : 1~5,数字越大失真越重 function out = distort(img, mode, level) switch mode case 'blur' sigma = 0.5 + 0.9 * level; out = imgaussfilt(img, sigma); % 高斯模糊 case 'jpeg' quality = 90 - 12 * level; out = imwrite2jpg(img, quality); % 先落盘再读回,模拟JPEG编解码 case 'noise' sigma = 0.01 * level; out = imnoise(img, 'gaussian', 0, sigma); case 'downsample' scale = 1 / (1 + 0.15 * level); out = imresize(img, scale); out = imresize(out, [size(img, 1), size(img, 2)]); otherwise error('unknown distortion mode: %s', mode); end end % imwrite2jpg 用临时文件模拟JPEG有损编码 function out = imwrite2jpg(img, quality) tmp_path = fullfile(tempdir, sprintf('distort_q%d.jpg', quality)); imwrite(img, tmp_path, 'jpg', 'Quality', quality); out = imread(tmp_path); end

代码说明:blur分支用imgaussfilt,参数sigma随level线性增长,模拟对焦不准或运动模糊带来的观感下降;jpeg分支先写临时文件再读回,这一步会把压缩痕迹(块效应、振铃)真实地带回图像,比直接在矩阵上做DCT更省事;noise分支用imnoise加高斯噪声,level越高信噪比越低;downsample分支先缩小再放大回原尺寸,制造分辨率不足的模糊感。这些是质量评价任务里最常出现的真实退化类型。

注意临时文件路径里带上了quality参数。最早版本如果写死tmp.jpg,多级循环时不同等级的JPEG图像会互相覆盖,轻则生成数据错乱,重则imread直接读到损坏文件。批量生成时,我一般会对每张干净原图循环4种失真、5个等级,输出目录固定为data/clean和data/distorted/{mode}/level_N。这个结构后续Python读数据时直接按路径索引,不需要额外维护数据库;缺点是文件数量膨胀快,1张原图对应20张降质图,1000张原图就是2万张文件。磁盘不充裕就只生成level_1、level_3、level_5三个等级,排序间隔拉大,监督信号反而更强。

2.3 把图像整理成训练pair:目录与标签生成

Matlab负责把图制造出来,Python这边负责把图变成训练样本。我习惯先扫一遍目录,把所有pair预生成到一个文本里,而不是在Dataloader里实时随机组合。预生成的好处是训练中途排查某个batch时,可以直接定位是哪些图产生了异常loss,不用反查随机种子。另一个好处是pair列表可以提前做清洗,比如把文件缺失、尺寸异常的组合直接过滤掉,训练时少一半莫名其妙的报错。

# build_pairs.py import random from pathlib import Path root = Path("data/distorted") levels = [1, 3, 5] pairs = [] # 每种失真内部,从不同等级里抽高质量/低质量对 for mode in ["blur", "jpeg", "noise", "downsample"]: for level_high, level_low in zip(levels[:-1], levels[1:]): high_dir = root / mode / f"level_{level_high}" low_dir = root / mode / f"level_{level_low}" for high_path in sorted(high_dir.glob("*.jpg")): # 找到同一原图对应的低等级文件,命名规则:orig_001_level_1.jpg low_path = low_dir / (high_path.stem.replace(f"level_{level_high}", f"level_{level_low}") + ".jpg") if low_path.exists(): pairs.append((str(high_path), str(low_path), 1)) # 前清晰后模糊 pairs.append((str(low_path), str(high_path), 0)) # 反序 random.shuffle(pairs) with open("train_pairs.txt", "w", encoding="utf-8") as f: for p in pairs: f.write(f"{p[0]} {p[1]} {p[2]}\n") print(f"total pairs: {len(pairs)}")

逻辑说明:第一个循环遍历失真类型,第二个循环把等级1、3、5两两配对,生成“高等级图在前、低等级图在后”的正序pair,label为1;再把顺序反过来生成反序pair,label为0。这样每个清晰度关系都被模型同时学习正向和反向,Rank网络不会偏向某一个方向。文件命名规则要求Matlab导出时带上原图编号和等级,例如orig_001_level_3.jpg;如果实际命名不一致,把replace那段改成对应规则即可。

Dataloader读取train_pairs.txt后,使用RandomCrop从两张图里裁同一位置的小块来训练,这样既做了数据增强,也避免全图计算导致显存吃紧。人脸场景下裁剪尺寸一般就是128x128,和后面网络输入尺寸保持一致;车牌、遥感这类大幅面场景可以适当调到160或192。pair对内部的裁剪必须用同一个随机种子,否则两张图裁自不同区域,质量差异就不是纯粹由失真等级决定了,模型会学偏到内容差异上。

3. 训练链路:从Rank比较到绝对质量分,两阶段参数怎么设

3.1 网络输入从224降到128:结构改动与权重处理

项目里有_src和src两套代码,区别就在网络输入尺寸:_src里是224,src里为了适配人脸区域裁剪尺寸把输入降到了128。人脸检测框裁出来通常不到224像素,强行resize到224不仅损失细节,还会改变五官比例;降采样到128反而更接近真实输入分布。改动本身不复杂:网络前后端通常都是全卷积加全局池化,把数据预处理里的resize尺寸和随机裁剪尺寸从224改成128就行,最后全连接层的输入特征维度跟着全局池化走,不受分辨率影响。

但这里有个极易翻车的点:如果直接加载在224输入下预训练的权重,第一层卷积核的统计分布是在224尺度下习得的,喂128像素的图,早期特征的感受野和统计含义都变了,效果通常不是小幅下降而是明显变差。我一般会把预训练权重当作初始化,然后用128输入在小数据集上额外跑5到10轮finetune,让BN的均值和方差重新估计。如果项目里没有预训练权重,就老老实实从零训:Rank阶段对绝对精度要求不高,先训出来一个相对排序正确的模型,反而比硬套预训练权重更稳妥。

3.2 Rank阶段:pair采样与排序损失

Rank网络输出的是一个标量质量得分,训练目标让清晰图的得分高于模糊图。常见损失用hinge排序形式,PyTorch里实现很直接:

# rank_loss.py import torch def pairwise_rank_loss(score_high, score_low, margin=0.3): # score_high: 清晰图的网络输出, score_low: 模糊图的网络输出 # 目标: score_high > score_low + margin loss = torch.clamp(margin - (score_high - score_low), min=0) return loss.mean()

参数说明:margin控制两张图输出分数的间隔,取0.3是经验值。margin太小,比如0.1,模型只要把两张图分出微小差异就停止优化,学到的排序置信度不足,后续回归拟合时噪声很大;margin太大,比如1.0,网络对easy pair过度苛求,训练震荡,低等级差距小的pair反而长期无法收敛。一般我会先设0.3跑50轮,观察loss曲线平稳后再扫一轮margin在0.1到0.5之间的取值,看哪个让验证SRCC最高。

pair采样要均衡。如果数据生成时4种失真、每类5个等级,按2.3的预生成文本,每次迭代随机取一个batch;批次内最好保证同一失真类型的pair不超过一半。否则网络可能只学会分辨“模糊vs清晰”这一类差异,而对噪声和JPEG压缩无感。采样权重按失真类型等概率抽,再在类型内部随机选pair。训练日志里建议加上当前batch里各失真类型的pair数量,方便定位是哪一类数据拖慢了收敛。

3.3 回归网络:小网络拟合RankIQA的评价效果

Rank网络训练完成后,它的输出只是相对分,最大问题是分数尺度不确定:同一张图在两次forward里可能因为batch统计不同输出不同,没法直接当绝对质量分用。项目里regression_network做的工作就是把Rank输出拟合到一个稳定刻度。常见做法有两类:一类是拿Rank网络的倒数第二层特征接一个回归头直接训练;另一类是像项目里这样单独用一个更小的网络,输入原图,输出绝对分,用Rank网络打分当伪标签。第二种训练更快,显存占用低,便于部署时把质量分支嵌入人脸流水线。

一个可抄作业的小回归网络结构如下:

# regression_network/model.py import torch.nn as nn class SmallRegressionNet(nn.Module): def __init__(self): super().__init__() self.features = nn.Sequential( nn.Conv2d(3, 32, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(32, 64, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(64, 128, 3, padding=1), nn.ReLU(), nn.AdaptiveAvgPool2d(1), ) self.head = nn.Sequential( nn.Linear(128, 32), nn.ReLU(), nn.Linear(32, 1), ) def forward(self, x): feat = self.features(x).flatten(1) return self.head(feat).squeeze(1)

结构说明:三层卷积加全局平均池化,最后接一个32维隐藏层的回归头。比Rank主干网络小一个量级,因为这里只是把Rank网络已经编码好的相对质量信号重新标定到绝对分数,不需要再学复杂纹理。训练时输入用128x128的随机裁剪,优化器Adam,初始学习率1e-4,weight_decay设1e-5防止在小数据集上过拟合;loss用smooth L1而不是MSE,MSE对个别极端伪标签的惩罚太狠,一个小网络扛不住。

训练循环里要保存阶段性checkpoint,不能只留最后一轮:

# train_regression.py 核心片段 import torch import torch.nn.functional as F best = float('inf') for epoch in range(epochs): for img, rank_score in loader: pred = model(img) loss = F.smooth_l1_loss(pred, rank_score) optimizer.zero_grad() loss.backward() optimizer.step() val_loss = evaluate(model, val_loader) if val_loss < best: best = val_loss torch.save({ 'epoch': epoch, 'model': model.state_dict(), 'val_loss': val_loss, }, f"checkpoints/regression_epoch{epoch:03d}_val{val_loss:.4f}.pt")

这段代码里最容易被忽略的是保存文件名:带着epoch和val_loss,便于回滚。模型比较小,多存几个文件也不占空间;真正的大模型才需要考虑定期清理旧权重。val_loss用smooth L1在验证集上的均值,和训练loss同口径,直接比较才有意义。这个阶段要把Rank网络冻结,不要让回归阶段的反向传播回去扰动排序网络;冻结后Rank网络相当于一个固定的伪标签生成器,回归网络只负责拟合它。

4. 多任务训练:一个网络同时输出人脸角度与图像质量

4.1 先跑单任务:拿到这个网络结构的性能下限

项目里的train_pose_qua.py是重头戏,一个网络同时预测人脸角度和图像质量。多任务不是把两个loss加起来就完事,第一步应该先跑单任务:同一种网络结构,分别只用角度标签和只用质量标签各训练一遍,记录最终能达到的最低loss。这个值代表当前网络结构拟合单个任务的性能上限,也是后续多任务调优的基准线。如果没有这个基准,多任务训练时你根本不知道两个任务之间的互相干扰到底有没有被消化掉。

我在实际训练里会把单任务的结果写在一个小表里:pose loss最低多少、quality loss最低多少、分别是第几轮达到的。到了多任务阶段,每隔几个epoch就对比一次两个任务的当前loss和各自单任务下限之间的差距;如果多任务里pose loss长期比单任务下限高出一截,说明质量任务在跟角度任务抢梯度,不是单纯的权重设置问题,可能要从网络结构上分离两个分支。反过来,如果两个任务都能逼近单任务下限,说明共享特征提取部分学到的表征对两个任务都够用,就不需要拆分支。

4.2 loss加权与分别打印:观察两个loss是否逼近单任务

多任务训练容易掉进“总loss好看,单任务崩掉”的坑。总loss是加权和,一个任务loss特别低时总loss也会低,另一个任务可能早已卡死。所以train_pose_qua.py这类训练脚本里,正向传播后一定要分别打印两个loss,而不是只打一个加权和。下面是我常用的模板:

# 多任务训练主循环(角度+质量) import torch.nn.functional as F for epoch in range(epochs): for img, pose_gt, qua_gt in loader: pose_pred, qua_pred = model(img) loss_pose = F.smooth_l1_loss(pose_pred, pose_gt) loss_qua = F.smooth_l1_loss(qua_pred, qua_gt) w_pose, w_qua = 1.0, 1.0 # 先等权起跑 loss_total = w_pose * loss_pose + w_qua * loss_qua optimizer.zero_grad() loss_total.backward() optimizer.step() # 每条日志分开打,盯住与单任务基线的差距 if step % 50 == 0: print(f"epoch {epoch} step {step}: " f"pose={loss_pose.item():.4f} " f"qua={loss_qua.item():.4f} " f"total={loss_total.item():.4f}")

逻辑说明:w_pose和w_qua先把两个loss拉到同一数量级再相加。角度回归的数值如果是欧拉角,范围大概在-45到45度;质量分经过拉伸后落在0到10。两者数值范围差一个量级,不等权直接相加时,质量loss会主导梯度。打印两行单独loss后,和单任务时的最低loss对比:假设单任务qua最低是0.35,多任务跑到第10轮还在0.9以上,就说明质量任务没吃到足够梯度,要么把w_qua加大,要么检查两个任务是否共享了太多前端特征。

提示:权重微调范围建议控制在0.5到2.0之间,一次只动一个权重;频繁双向调整会让两个loss都在抖动里浪费时间。

权重的调法我一般遵循“一次只调一个”的原则:固定在0.1、0.5、1.0、2.0几个档位里扫,每个档位跑10到20轮看趋势,而不是每轮都改权重。多任务训练对权重变化很敏感,频繁改动会把模型推向一个不稳定区域,表现就是loss在小步低频震荡,怎么等也等不到收敛。调整权重时保留训练日志,观察两个loss的变化方向;如果增大w_qua后pose明显反弹,就得回落,寻找两边都能接受的折中点。

4.3 多任务的checkpoint策略

多任务的可复现难度比单任务高一截:两个loss的动态范围不同,最优epoch也不一定同步。单任务时看val loss选最优很容易,多任务时就要专门定义“什么叫最好”。我会把保存条件设为两个任务的val loss都低于各自单任务基线的1.1倍,且两者之和最小,才覆盖上一份checkpoint。达不到这个条件,宁可保留旧权重继续训,不盲目保存。

保存时把两个val loss、epoch数、权重文件都写进文件名,格式类似pose0.42_qua0.38_ep024.pt。这比只存一个best_model.pt多几个字符,但回滚时能一眼看出这份权重的质量构成。还有一个细节:多任务训练收敛慢,学习率调度上不要用StepLR那种固定步长衰减,常见做法是等两个loss都进入平缓期再手动降一个量级。降太早,网络还在适应两个任务的均衡点,梯度方向乱;降太晚,收敛速度太慢,等一轮要好几小时。

5. 避坑记录:低质量崩预测、loss失衡、checkpoint覆盖,五条踩坑经验

5.1 低质量图像预测崩掉:quality label要非线性拉伸到0到10

现象:训练时验证集里清晰图预测和真实分接近,一旦输入有雾、严重过曝或重度噪声的图,输出分数要么贴着0要么贴着10,完全乱套。loss在低质量区间长期降不下去。

原因:label分布不均衡,自然采集的图大部分落在中等偏高质量段,低质量样本稀少;原始分数尺度跨度大时,网络在同一组权重里很难同时拟合两个密度差异巨大的区间。这是回归任务的经典问题,不只是NR-IQA独有。

解决:对质量label做非线性拉伸到0到10,让低分区间占据更大的数值空间,典型映射加一个幂指数:

# label_mapping.py import numpy as np def stretch_label(q_raw, q_min=0, q_max=100, gamma=0.6): q_norm = (q_raw - q_min) / (q_max - q_min) return 10.0 * np.power(q_norm, gamma)

这段函数把原始分先归一化到0到1,再开gamma次方。gamma小于1时,低分段被拉伸;gamma越大越接近线性,低分段越挤。我一般先从gamma=0.6起步,画出拉伸前后label的直方图,确保低质量区间不再是一条细线。注意预测出来的是拉伸后的分数,对外输出时要么同时公布反变换公式,要么保留一份拉伸分数到真实MOS的映射表,否则业务方拿到的0到10分和他脑子里那个0到10不是一回事。

5.2 两个任务互相打架:总loss降得很好,单个loss不动

现象:多任务训练日志里total loss一路下降,看起来很美;单独看pose loss却停在某个数值上不挪窝,qua loss反而在降。

原因:两个loss数值量级不同,加权相加后梯度被较大的一方主导。反向传播时,共享卷积层收到的梯度几乎全来自质量任务,角度任务的分支只是陪跑。这就是多任务训练最常见的梯度“绑架”。

解决:先分别打印两个loss,记下等权条件下的量级差;根据量级差把权重修正为接近1比1的梯度贡献,再继续训练。如果修正后仍然不降,检查角度分支是否只有一个全连接头,分支容量太小拟合不了;常见做法是把共享特征层复制一份作为角度分支的输入,给角度任务独立的高层特征。做权重归一化时,我习惯每隔50个step输出一次两个分支的grad_norm,而不是只看loss。

5.3 checkpoint被覆盖:想回滚时只剩最后一轮

现象:训练到第30轮发现loss不稳,想回滚到第20轮的权重,打开checkpoints目录只剩一个文件。

原因:保存代码里文件名写死成checkpoint.pt,每轮覆盖;或者保存条件是“val_loss更低才保存”,而第20轮不是最优val,早被覆盖了。很多训练脚本默认带着这两个坏习惯。

解决:文件名带上epoch和两个关键指标,每轮都保存一份小文件;回归网络单模型几MB随便存,多任务网络参数量大就保留最近5份然后轮转清理。保存时同时存optimizer状态和loss,回滚时不只是模型权重,学习率状态也要一起恢复,只恢复模型不恢复optimizer等于半套后悔药。

torch.save({ 'epoch': epoch, 'model': model.state_dict(), 'optimizer': optimizer.state_dict(), 'loss_pose': loss_pose.item(), 'loss_qua': loss_qua.item(), }, save_dir / f"ep{epoch:03d}_pose{loss_pose:.3f}_qua{loss_qua:.3f}.pt")

5.4 输入尺寸降到128后精度掉一截:别直接拿224权重跑

现象:把resize尺寸从224改成128,在原有checkpoint上续训或直接推理,准确率下降明显,且下降幅度远超预期。

原因:卷积层对输入尺寸有一定弹性,但BN层统计量、第一层卷积的有效感受野都是按224尺度校准的;直接换输入尺寸,等于让模型在一个分布外输入上推理。

解决:如果已经训好了224的Rank网络,先把它当作初始化载入,再用128输入finetune 5到10轮,期间BN参数重新估计;不要从224训练好的模型直接跳到128继续用。如果两个尺寸都要作为输入,最省事的方式是在网络结构里强制加AdaptiveAvgPool,保证全连接层输入维度不随尺寸变化。

5.5 Matlab批量生成数据中途报错或文件错乱

现象:跑distort脚本时,生成的图文件大小明显异常,或者imread读回时报文件不存在;批量跑到一半直接error中断。

原因:多数情况是临时文件重名和路径问题。JPEG临时文件如果没带quality参数区分,两个level会互相覆盖;另一个常见原因是工程拷贝到Windows后有中文路径或带空格的路径,Matlab函数拼接路径时踩坑。

解决:临时文件命名带上失真类型和level,比如distort_blur_l3.jpg;把data目录统一放到英文无空格路径下;批处理前先对10张原图跑冒烟测试,确认四种失真的输出文件大小都落在合理区间(JPEG一般在几十KB到几百KB,不可能只有0字节),再铺开全量。

6. 验证评价效果:排序一致性和主观打分的对拍流程

6.1 用SRCC衡量排序能力,而不是只看MSE

质量评价模型验证的常用指标不只MSE。MSE衡量的是绝对分数接近程度,但业务上更关心排序是否可信:用户要挑出最差的一批图,模型误判顺序比分数偏差更致命。所以对拍时我会用Spearman秩相关系数(SRCC)来看预测分和人工分排序是否一致。

# eval_correlation.py from scipy.stats import spearmanr preds = [model.predict(img) for img in test_images] mos = get_mos(test_images) # 人工平均意见分 rho, p = spearmanr(preds, mos) print(f"SRCC={rho:.4f} p={p:.2e}")

操作步骤:挑30到50张覆盖清晰、模糊、噪声、压缩各等级的测试图,找3到5个人打分取平均,然后和模型预测算SRCC。结果分三档看待:SRCC 0.9以上说明排序基本可靠;0.8到0.9可以用于粗筛;0.8以下不要上生产,先回训练阶段找问题。对拍时注意预测分数是否经过了和人工分相同的单调变换,如果训练时做了非线性拉伸,评估时也要把预测分做逆变换再算相关,否则相关系数会被非线性关系干扰。

6.2 控制变量排查:先定位是谁崩了再改训练

多任务模型出现整体效果差时,最有效的排查方法是控制变量:把angle分支权重设成0,先让模型退化成纯质量分支,看单任务性能是否恢复到此前水平。如果退化了还不行,说明问题出在共享特征层或数据配比,而不是多任务权重;要么调整两个数据集的采样比例,要么把共享层拆开。我见过太多人一上来就调loss权重,调了两天发现是数据里把level_1和level_3的命名顺序搞反了,排序pair全错。所以每次改完数据生成脚本,都要先抽几对pair人工确认,再进训练。

从那以后,我每次换数据集训练NR-IQA模型前,都强制自己走一遍固定流程:先检查pair文本里五对样本的正确性,再跑单任务下限,再算一轮SRCC对拍,最后才轮到多任务权重。顺序不许乱,乱一次就多付一次翻车学费。希望帮到你。

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

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

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

立即咨询