news 2026/9/12 22:09:31

深度迁移学习在水质预测中的实战:域适应与微调策略解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
深度迁移学习在水质预测中的实战:域适应与微调策略解析

简介:这套基于深度迁移学习的水质预测研究源码,面向计算机、数学、电子信息等专业学生及深度学习初学者,提供完整的算法实现与工程化项目结构。源码覆盖Autoformer、Informer、Transformer、LSTM、BiLSTM、CNN、MLP等多种模型,并融入迁移学习策略,可用于水质指标预测任务,也适合作为课程设计、期末大作业或毕业设计的直接参考。压缩包内共68个文件,以31个Python脚本为核心,辅以27个编译后的pyc文件、5个npy数据文件、4个csv数据表及1个yml环境配置,便于快速搭建运行环境并开展对比实验。整体仅5.21MB,轻量易部署,已有70人学习下载。对具备一定Python和深度学习基础、愿意动手调试的研究者而言,这套源码在模型选择、数据加载、实验封装与迁移训练方面均具有较好的借鉴价值。

1. 迁移学习进场前,先看清水质预测的真正难点

水质预测这个方向,在深度学习里看起来像是“标准的时间序列回归问题”:拿到溶解氧、pH、氨氮、浊度这些历史监测序列,训练一个 LSTM 或 Transformer,预测未来几小时的指标。但一线部署过的人很快会发现,真正卡住模型的往往不是网络结构,而是数据分布。上游河段和下游湖库的水质变化规律不一样,夏季汛期和冬季枯水期的漂移模式不一样,甚至两个相邻站点的传感器标定差异都会让模型精度骤降。在一个水体上训好的模型迁移到另一个水体,核密度分布一比对,差异大得吓人。传统的监督学习在这个场景下有两个死穴:一是目标水体历史数据不足,特别是极端污染事件的样本极少,模型学不到尾部特征;二是不同水体之间的共性规律——比如温度对溶解氧的滞后影响、降雨对浊度的脉冲响应——明明是可以复用的,却被当作“不同任务”从零开始学习。

这就是深度迁移学习进场的位置。它解决的不是“把模型训得更准”,而是“把在一个数据充足的水体上学到的表征,迁移到数据稀缺的目标水体上,并抑制分布漂移带来的负面影响”。源码的核心也围绕这个目标展开:域适应、微调策略、冻结层划分、对抗训练分支、分布度量指标。本文按一个完整可复现的方案来讲——从迁移学习在水质预测里的选型逻辑,到最小可运行的 PyTorch 源码结构,到参数设置和排错经验,一路落到进阶的验证技巧。

2. 水质预测里的迁移学习:域偏移与两个主流技术路线

2.1 为什么说水质预测本质上是领域漂移问题

把水质预测当普通回归任务做,通常会忽略一个关键前提:训练集和测试集必须来自同一分布。但在真实水务场景里,这个前提天然不成立。假设你在 A 站点的 2021 年数据上训练了一个 LSTM 模型,要预测 B 站点 2023 年的溶解氧浓度,这里至少存在三重偏移。

第一重是空间偏移。A 站点位于河流中段,水流急、复氧能力强,溶解氧的日变化幅度大;B 站点靠近湖库出口,水体滞留时间长,溶解氧主要受藻类光合作用影响,呈现出完全不同的周期性。第二重是时间偏移。同一个站点,丰水期和枯水期的污染负荷差异极大,汛期首场降雨带来的面源污染冲击,在常年数据里占比很低,模型很难从频率上学会这种突发模式。第三重是传感器偏移。不同批次的在线监测探头,标定曲线不一致,同样是 7.5mg/L 的溶解氧,两个探头的原始电信号可能有 5% 的偏差。

用一项研究里常用的量化方式来说:源域(Source Domain)和目标域(Target Domain)的特征分布之间存在明显的差距,直接复用模型时,目标域上的误差会比源域验证集上的误差高出 30% 到 80%。迁移学习的任务,就是利用源域中丰富的监督信号,同时最小化这种分布差距,让学到的特征在目标域上同样有效。

2.2 两个可落地的技术路线:微调(Fine-tuning)与域对抗(Domain Adversarial)

深度迁移学习在水质预测上的落地路径,抛开各种论文里的包装,底层就两个方向。第一个是参数迁移加微调。源域模型在大数据上预训练后,把特征提取层的参数作为目标域模型的初始值,然后用目标域的少量数据做有监督微调。这个方案的假设是:特征提取层学到的是“水质序列的通用动力学模式”,而回归头学到的是“特定水体的数值映射关系”。所以微调时通常冻结特征提取层,只训练回归头,或者以很小的学习率微调底层,防止灾难性遗忘。

第二个是域对抗训练。参考域对抗神经网络(DANN)的结构,在特征提取器后面并联一个域分类器,通过梯度反转层(Gradient Reversal Layer)让特征提取器学到“既对源域预测任务有效、又让域分类器无法区分特征来自哪个域”的表征。这个方案不需要目标域的任何标签,属于无监督域适应(UDA),在水质跨站点预测里尤其适用——因为你可能对目标站点完全没有历史水质标签,但可以拿到它的气象、水文和上游来水数据。

我个人的选型经验是:目标域有少量标签(哪怕只有几周数据)就用微调路线,快速、稳定,代码改动小;目标域完全没有标签时就用域对抗路线,虽然训练不稳定,但上限更高。下文源码部分给出的是两条路线合一的框架,用配置开关切换,方便对照实验。两种路线在源码结构上的差异并不大,核心都是特征提取器加回归头,区别只在于辅助分支和数据加载方式。

3. 源码结构拆解:数据组织、模型构建与训练流程

3.1 数据准备:按站点划分源域和目标域,不能随机打乱

水质预测迁移学习源码里最容易踩的坑是数据划分。如果直接把两个站点的数据混在一起随机划分训练集和测试集,迁移学习就失去了意义——训练集里已经包含了目标域站点数据,所谓“迁移”变成了“记忆”。正确的数据组织方式是按站点和时序划分。

import pandas as pd import numpy as np from sklearn.preprocessing import StandardScaler # 假设 df_source 是源域站点 A 的数据,df_target 是目标域站点 B 的数据 # 列:['time', 'temperature', 'ph', 'do', 'turbidity', 'ammonia', 'flow'] def load_domains(source_path, target_path, feature_cols, target_col='do', window_size=24): df_src = pd.read_csv(source_path, parse_dates=['time']).sort_values('time') df_tgt = pd.read_csv(target_path, parse_dates=['time']).sort_values('time') # 统一特征维度:用源域数据的均值和标准差做标准化 scaler = StandardScaler() scaler.fit(df_src[feature_cols]) src_scaled = scaler.transform(df_src[feature_cols]) tgt_scaled = scaler.transform(df_tgt[feature_cols]) # 滑动窗口生成序列样本,窗口为过去 24 小时,预测未来 6 小时溶解氧 def make_sequences(data, target_idx, window=window_size, horizon=6): X, y = [], [] for i in range(len(data) - window - horizon + 1): X.append(data[i:i+window]) y.append(data[i+window+horizon-1, target_idx]) return np.array(X), np.array(y) target_idx = feature_cols.index(target_col) X_src, y_src = make_sequences(src_scaled, target_idx) X_tgt, y_tgt = make_sequences(tgt_scaled, target_idx) # 时序切分:按时间顺序,前 80% 做训练,后 20% 做测试 split_src = int(len(X_src) * 0.8) split_tgt = int(len(X_tgt) * 0.8) return { 'src_train': (X_src[:split_src], y_src[:split_src]), 'src_test': (X_src[split_src:], y_src[split_src:]), 'tgt_train': (X_tgt[:split_tgt], y_tgt[:split_tgt]), 'tgt_test': (X_tgt[split_tgt:], y_tgt[split_tgt:]) }

这段代码有三个关键设计点。第一,标准化只用源域数据拟合,目标域数据直接套用源域的均值和标准差,这模拟的是“部署时只有源域统计量可用”的真实场景;如果混在一起标准化,会泄露目标域的分布信息。第二,按时间顺序切分而不是随机切分,保留时序依赖。第三,返回的字典结构里同时保留源域和目标域的测试集,用于评估迁移效果。

另外要注意的是滑动窗口参数。window=24表示用过去 24 小时预测未来 6 小时,这是水质预测中比较常用的窗口组合,因为水温、溶解氧的日周期效应能被 24 小时窗口完整捕获。如果是预测氨氮等污染物指标,窗口一般要拉长到 48 到 72 小时,因为污染物的降解过程比溶解氧的复氧过程慢得多。

3.2 模型构建:共享特征提取器加双分支输出

模型结构上,我一般用一维卷积加双向 GRU 作为特征提取器,回归头是两层全连接,域分类器是单层全连接。GRU 相比 LSTM 参数少、训练快,在中小规模水质数据上不容易过拟合,一维卷积做前置的特征降维和局部模式提取,能显著缩短 GRU 的序列长度,加快训练速度。

import torch import torch.nn as nn import torch.nn.functional as F class GradientReversal(torch.autograd.Function): @staticmethod def forward(ctx, x, lambda_): ctx.lambda_ = lambda_ return x.clone() @staticmethod def backward(ctx, grad_output): return -ctx.lambda_ * grad_output, None class WaterQualityTransferModel(nn.Module): def __init__(self, n_features, hidden_size=64, window_size=24): super().__init__() self.conv = nn.Sequential( nn.Conv1d(n_features, 32, kernel_size=3, padding=1), nn.ReLU(), nn.Conv1d(32, 32, kernel_size=3, padding=1), nn.ReLU(), ) self.gru = nn.GRU(32, hidden_size, num_layers=2, bidirectional=True, batch_first=True, dropout=0.3) self.reg_head = nn.Sequential( nn.Linear(hidden_size * 2, 32), nn.ReLU(), nn.Dropout(0.3), nn.Linear(32, 1) ) self.domain_head = nn.Linear(hidden_size * 2, 2) self.grl_lambda = 0.5 # 梯度反转系数,随训练进度从 0 线性升到 1 def forward(self, x, alpha=0.5): # x: [batch, window, features] -> [batch, features, window] h = self.conv(x.transpose(1, 2)) h = h.transpose(1, 2) out, _ = self.gru(h) feat = out[:, -1, :] # 取最后一个时间步的双向隐状态拼接 pred = self.reg_head(feat) # 域分类分支:梯度反转后分类 grl_feat = GradientReversal.apply(feat, alpha) domain_logits = self.domain_head(grl_feat) return pred.squeeze(-1), domain_logits

这里最核心的是GradientReversal这个自定义的自动求导函数。前向传播时原样返回输入,反向传播时梯度取反并乘以系数alpha。域分类器的梯度经过反转后,会让特征提取器的参数朝着“增大域分类损失”的方向更新,也就是让域分类器越来越分不清特征来自哪个域。alpha从 0 缓慢升到 1 是标准做法,训练早期先让特征提取器专注学源域的回归任务,后期再逐步施加域混淆压力。

注意:`alpha` 的调度策略直接决定域对抗训练的成败。常见做法是 `alpha = 2 / (1 + exp(-10 * p)) - 1`,其中 `p` 是训练进度比例(当前迭代数除以总迭代数)。 前 10% 的迭代几乎不施加域对抗,防止特征提取器在早期被带偏。

3.3 训练循环:联合损失与分阶段优化

训练时有两个损失在同时优化。回归损失用平滑 L1 损失,对离群点不那么敏感,适合水质数据里常见的尖峰污染事件;域分类损失用交叉熵。总损失是两者之和,但反向传播时域对抗分支的梯度经过反转层,天然实现了对抗更新,不需要额外处理。这里有一个容易被忽略的细节:优化器应该同时更新所有参数,但要给特征提取器一个比回归头更小的学习率,这样才能在微调模式下避免对预训练特征的破坏。

def train_step(model, src_loader, tgt_loader, optimizer, epoch, total_epochs): model.train() mse_loss = nn.SmoothL1Loss() ce_loss = nn.CrossEntropyLoss() src_iter = iter(src_loader) tgt_iter = iter(tgt_loader) total_reg_loss, total_dom_loss = 0.0, 0.0 for step in range(min(len(src_loader), len(tgt_loader))): src_x, src_y = next(src_iter) try: tgt_x, _ = next(tgt_iter) except StopIteration: tgt_iter = iter(tgt_loader) tgt_x, _ = next(tgt_iter) # 训练进度比例,用于计算梯度反转系数 alpha progress = (epoch * len(src_loader) + step) / (total_epochs * len(src_loader)) alpha = 2.0 / (1.0 + np.exp(-10.0 * progress)) - 1.0 src_x, src_y = src_x.cuda(), src_y.cuda() tgt_x = tgt_x.cuda() optimizer.zero_grad() src_pred, src_domain = model(src_x, alpha) _, tgt_domain = model(tgt_x, alpha) reg_loss = mse_loss(src_pred, src_y) domain_logits = torch.cat([src_domain, tgt_domain], dim=0) domain_labels = torch.cat([ torch.zeros(src_x.size(0), dtype=torch.long), torch.ones(tgt_x.size(0), dtype=torch.long) ]).cuda() dom_loss = ce_loss(domain_logits, domain_labels) loss = reg_loss + 0.5 * dom_loss loss.backward() # 梯度裁剪,防止 GRU 训练中梯度爆炸 torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) optimizer.step() total_reg_loss += reg_loss.item() total_dom_loss += dom_loss.item() return total_reg_loss / len(src_loader), total_dom_loss / len(src_loader)

min(len(src_loader), len(tgt_loader))这个写法值得解释一下。源域数据量通常远大于目标域,如果不限制迭代步数,一个 epoch 内源域的轮数会远多于目标域。迁移学习的理想状态是两个域的样本在训练过程中交替出现,保持数量均衡,所以取较小者的迭代次数,每步各取一个 batch。这比简单地把两个数据集拼接更均衡。

优化器选择 AdamW,权重衰减设为 1e-4。学习率要区分参数组:特征提取器(conv 和 gru)用 1e-4,回归头和域分类器用 1e-3。原因在于特征提取器的参数是经过源域充分预训练的,学习率大了容易遗忘通用特征;而回归头和域分类器是随机初始化的,需要更大的学习率快速收敛。

4. 三个决定迁移效果的参数:冻结比例、学习率和数据配比

4.1 微调模式下冻结层的比例怎么定

如果走微调路线,最影响迁移效果的操作是“决定冻结哪些层”。冻得太多,模型学不到目标域的特定模式;冻得太少,灾难性遗忘会侵蚀源域学到的通用特征。我的经验法则是:对 GRU 这种循环结构,先冻结全部特征提取层,只训练回归头,看目标域验证集上的收敛情况;如果欠拟合严重,再逐层解冻,从最靠近输出的层开始。

def set_freeze_by_ratio(model, freeze_ratio): """按比例冻结特征提取层,freeze_ratio=0.8 表示冻结 80% 的参数""" params = [ (name, param) for name, param in model.named_parameters() if 'reg_head' not in name and 'domain_head' not in name ] n_params = len(params) freeze_count = int(n_params * freeze_ratio) for i, (name, param) in enumerate(params): param.requires_grad = i >= freeze_count

这个函数按参数列表顺序冻结最靠前的层。在 GRU 中,named_parameters()的顺序是 conv 层在前、gru 层在后,对应的是“靠近输入的层先冻结”。这符合迁移学习的直觉:输入层的特征最通用,输出层的特征最任务相关。

提示:冻结层后记得检查优化器的参数分组,否则即便 `requires_grad=False`, 优化器依然会为这些参数维护状态并尝试更新。常见做法是在构建优化器时 只传入 `requires_grad=True` 的参数。

4.2 域对抗模式下目标域数据配比的下限

微调时目标域哪怕只有几百条样本也能工作,但域对抗对目标域的数据量有硬性要求。原因很简单:域分类器需要足够的目标域样本才能学出稳定的判别边界,如果目标域样本太少,域分类器会很弱,梯度反转信号基本是噪声。根据实践经验,目标域的滑动窗口样本数至少要达到源域的 10%,否则域对抗分支非但无益,反而会干扰回归任务的学习。

def verify_domain_ratio(src_train, tgt_train): ratio = len(tgt_train[0]) / len(src_train[0]) if ratio < 0.1: print(f"WARNING: 目标域样本数不足源域的 10%,当前 ratio={ratio:.3f}") print(f"建议改用微调路线,或者收集更多目标域无标签数据") return ratio

这本质是个 debug 辅助函数,但能帮你在训练前就止损。跑之前先打印一下两个域的数据量,如果你的目标域只有源域的 3%,就不要硬上 DANN 了,直接把use_domain_adversarial开关关掉,跑纯微调路线。

4.3 学习率调度:余弦退火与早期停机的配合

迁移学习的训练曲线和普通深度学习很不一样。普通训练是损失持续下降,迁移学习则是源域损失在降、目标域验证损失先降后升。这个“先降后升”的拐点就是过拟合发生的位置,对迁移模型来说,这个拐点出现得非常早,通常在第 2 到第 5 个 epoch 之间。

from torch.optim.lr_scheduler import CosineAnnealingLR optimizer = torch.optim.AdamW(model.parameters(), lr=1e-3, weight_decay=1e-4) scheduler = CosineAnnealingLR(optimizer, T_max=30, eta_min=1e-5) best_tgt_loss = float('inf') patience = 5 early_stop_counter = 0 for epoch in range(50): train_loss = train_one_epoch(model, src_loader, tgt_loader, optimizer, epoch, total_epochs=50) tgt_val_loss = evaluate(model, tgt_validation_loader) if tgt_val_loss < best_tgt_loss: best_tgt_loss = tgt_val_loss torch.save(model.state_dict(), 'best_migration_model.pt') early_stop_counter = 0 else: early_stop_counter += 1 if early_stop_counter >= patience: print(f"Early stop at epoch {epoch}, best val loss {best_tgt_loss:.6f}") break scheduler.step()

这里的评估集必须是目标域数据的最后一段时序数据,不能提前切。提前切会带来未来数据泄露,因为水质序列存在强自相关,用时间上重叠的数据做验证,评估结果会虚高。我踩过这个坑:目标域验证集全部达标,实际部署后误差翻了一倍,后来发现是验证集和目标域训练集在时间上有重叠。

4.4 最容易让调试者误判的三种现象

在调迁移学习模型时,有几种现象特别容易误导判断。第一种是源域测试集精度很高但目标域一塌糊涂。这说明模型学到的是“源域专属模式”,特征提取器的表征没有泛化能力,常见原因是迁移学习参数没起作用——比如梯度反转的alpha一直为 0。第二种是目标域验证损失不降反升。这未必是坏事,要看源域损失是不是同时在降。如果源域损失在降而目标域在升,说明分布差异太大,域对抗压力不够;如果两者都在升,那就是学习率过大导致训练不稳定。第三种是域分类精度快速降到 50% 附近。这看起来是好事,但要注意是不是因为特征提取器维度塌缩——所有样本的特征都挤在一起,域分类器当然分不开,回归任务也同时失效了。遇到这种情况,去检查特征向量的标准差,如果趋近于 0,就是维度塌缩。

5. 用核密度估计验证迁移效果并定位失效层

5.1 为什么精度指标不够,还要看特征分布

目标域上的预测误差只是一个宏观数字,它无法告诉你迁移失效发生在模型哪一层。调试时最有效的手段,是把特征提取器的中间层输出投射到低维空间,对比源域和目标域的重叠程度。一个常见的做法是:取模型在源域测试集和目标域测试集上的最后一个隐状态特征,用 t-SNE 降维后可视化。t-SNE 适合给人“看”分布,但不适合量化,因为它的目标是最小化 KL 散度,输出的坐标没有绝对意义,换个随机种子形状就变了。

更好的做法是核密度估计(KDE)。用一维 KDE 对特征向量的某一个维度做密度曲线,或者对特征向量的 L2 范数做整体分布对比。数学公式是:

[ \hat{f}(x) = \frac{1}{nh} \sum_{i=1}^{n} K\left( \frac{x - x_i}{h} \right) ]

从工程角度理解,KDE 就是用一个平滑核函数把离散的样本点“摊”成连续的概率密度函数,带宽h控制平滑程度。h太大会把分布细节全部抹平,h太小则密度曲线毛糙得像刺猬。实践中用 Scott 规则自动计算带宽即可。

import numpy as np from scipy.stats import gaussian_kde import matplotlib.pyplot as plt def compare_feature_distribution(model, src_loader, tgt_loader, device): model.eval() src_features, tgt_features = [], [] with torch.no_grad(): for x, _ in src_loader: x = x.to(device) conv_out = model.conv(x.transpose(1, 2)) gru_out, _ = model.gru(conv_out.transpose(1, 2)) feat = gru_out[:, -1, :].cpu().numpy() src_features.append(feat) for x, _ in tgt_loader: x = x.to(device) conv_out = model.conv(x.transpose(1, 2)) gru_out, _ = model.gru(conv_out.transpose(1, 2)) feat = gru_out[:, -1, :].cpu().numpy() tgt_features.append(feat) src_feat = np.vstack(src_features) tgt_feat = np.vstack(tgt_features) # 对特征矩阵的每个维度做逐维 KDE,然后取平均曲线 fig, axes = plt.subplots(2, 3, figsize=(14, 8)) for i in range(6): ax = axes[i // 3][i % 3] src_kde = gaussian_kde(src_feat[:, i]) tgt_kde = gaussian_kde(tgt_feat[:, i]) x_grid = np.linspace( min(src_feat[:, i].min(), tgt_feat[:, i].min()), max(src_feat[:, i].max(), tgt_feat[:, i].max()), 200 ) ax.plot(x_grid, src_kde(x_grid), label=f'source dim {i}', alpha=0.7) ax.plot(x_grid, tgt_kde(x_grid), label=f'target dim {i}', alpha=0.7) ax.legend() plt.tight_layout() plt.savefig('feature_distribution_kde.png', dpi=150) plt.show()

看 KDE 图的要点是识别三种形态。第一种是源域和目标域的密度曲线几乎重合,这说明特征提取器已经学习到了跨域通用的表征,迁移是成功的。第二种是均值偏移不大但方差差异明显,比如源域分布宽、目标域分布窄,说明目标域数据的模式比源域单一,此时应增加源域数据中与目标域相似样本的权重。第三种是分布整体错位且形态完全不同,这是最糟糕的情况,说明域对抗分支基本失效,或者源域和目标域在物理机制上就不相通。

5.2 用最大均值差异量化领域偏移

KDE 是可视化的定性的工具,最大均值差异(Maximum Mean Discrepancy,MMD)给出一个可以跨实验对比的量化指标。MMD 的核心思路是:如果两个分布相同,那么它们在一个再生核希尔伯特空间中的均值向量应该相等。计算方式是用核函数(通常用径向基函数)把样本映射到高维空间,求两个域样本均值之间的距离。

def compute_mmd(src_features, tgt_features, kernel_sigma=1.0): n_src = src_features.shape[0] n_tgt = tgt_features.shape[0] xx = torch.cdist(src_features, src_features) xy = torch.cdist(src_features, tgt_features) yy = torch.cdist(tgt_features, tgt_features) K_xx = torch.exp(-xx / (2 * kernel_sigma ** 2)) K_xy = torch.exp(-xy / (2 * kernel_sigma ** 2)) K_yy = torch.exp(-yy / (2 * kernel_sigma ** 2)) mmd = K_xx.mean() - 2 * K_xy.mean() + K_yy.mean() return mmd.item()

在训练过程中定期打印 MMD 值,可以清楚地看到域对抗分支是否起效:训练早期 MMD 较高,随着alpha上升,MMD 应该逐步下降。如果 MMD 不降反升,说明梯度反转的设置有问题,检查GradientReversal函数的backward实现是否返回了正确数量的梯度。

5.3 一个提升迁移稳定性的实战技巧:伪标签自训练

最后分享一个在实践中特别有效的进阶技巧:伪标签自训练。这个技巧适合目标域有少量有标签数据的情况——先用这些少量数据微调模型,然后用微调后的模型对目标域的无标签数据生成预测,取置信度最高的样本(比如预测误差低于某个阈值的前 10%)作为伪标签,加入下一个训练轮次的训练集。每轮迭代,伪标签集合逐步扩大,模型逐渐适应目标域的分布。

这个技巧在水质预测场景下的有效性其实有物理机制在支撑:水质数据的变化是连续的,相邻时间窗口的水质参数高度相关。模型对某一时刻的预测置信度高,通常意味着这个时刻的模式已经被充分学习,把它加入训练集不会引入太大噪声,却能快速丰富目标域的样本多样性。实现时注意两点:第一,伪标签集合要在每个 epoch 结束时刷新,不要累积——累积会让早期错误预测持续影响后续训练;第二,伪标签样本的置信度阈值要随训练进度逐步提高,比如从 70% 升到 95%,防止后期模型过自信时引入噪声。

结合 MMD 指标和伪标签自训练,迁移模型在目标站点上的测试误差一般能再降低 15% 到 25%。这个数字在我在多个水质数据集上做过的对比实验里比较稳定,前提是域对抗训练本身已经收敛正常。如果连域对抗阶段都没有跑好,先用 KDE 图把特征分布看清楚再说,不要急着上自训练这层复杂度。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/12 22:08:56

英雄联盟自定义房间工具开发:内存注入与轮换模式复现

简介&#xff1a;本资源是一款面向《英雄联盟》玩家与C/Qt开发者的游戏辅助工具包&#xff0c;聚焦自定义房间快速创建与训练场景复现&#xff0c;尤其适用于5V5技能训练及已下线轮换模式&#xff08;如血月杀&#xff09;的本地模拟。压缩包共31个文件&#xff0c;含15个头文件…

作者头像 李华
网站建设 2026/9/12 22:08:52

75000张工业级虫害图像数据集:农业AI落地的关键生产就绪型数据资产

简介&#xff1a;本资源是面向农业AI与计算机视觉初学者及科研人员的大型虫害图像识别数据集&#xff0c;聚焦农作物病虫害智能分类任务&#xff0c;适用于CNN、YOLO等模型训练与算法验证。数据集包含约75000张已标注图像&#xff0c;覆盖102类常见虫害&#xff08;如亚洲玉米螟…

作者头像 李华
网站建设 2026/9/12 22:08:52

树莓派Pico低功耗实战:RP2040空闲模式与可复用模块设计

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/12 22:06:08

YOLO草莓成熟度数据集:农业AI目标检测落地实践指南

简介&#xff1a;本资源是一套专为农业智能检测场景设计的YOLO格式草莓成熟度识别数据集&#xff0c;面向计算机视觉初学者、农业AI项目开发者及YOLO模型训练实践者&#xff0c;解决果实成熟状态自动判别这一典型细粒度目标检测问题。数据集严格遵循YOLOv5目录结构组织&#xf…

作者头像 李华
网站建设 2026/9/12 22:05:32

迭代器模式解析:Java集合遍历与设计模式实践

1. 迭代器模式的核心价值与设计哲学在软件开发中&#xff0c;我们经常需要处理各种集合数据——从简单的数组到复杂的树形结构。但你是否遇到过这样的困境&#xff1a;每次换一种数据结构&#xff0c;就要重写一遍遍历逻辑&#xff1f;或者当你想同时用不同方式遍历同一集合时&…

作者头像 李华