尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

多任务空气质量预测实战:从LSTM建模到训练避坑指南

发布时间:2026/9/28 18:26:00

资讯中心
01
ARTICLE

多任务空气质量预测实战:从LSTM建模到训练避坑指南

多任务空气质量预测实战:从LSTM建模到训练避坑指南
简介本科毕业设计项目资源聚焦多任务深度学习框架下的空气质量预测建模面向需要完成毕业设计或相关课题的高校学生与初级研究者。资源包共49个文件大小5.08MB以36个CSV站点监测数据为主体配合7个Python脚本涵盖模型定义、训练、评估、数据处理等环节、4个Markdown说明文档及配置文件整体目录清晰便于定位与二次开发。当前已有83人学习。内容从站点数据切片、模型结构说明到训练与评估代码均有覆盖可帮助读者理解多任务预测的整体实现思路并可直接基于脚本进行实验复现或进一步优化。压缩包内还包含README、配置示例与需求说明适合作为课程设计、毕业论文或课题入门的参考基线尤其适用于希望快速搭建空气质量预测流程并验证模型效果的场景。1. 多任务空气质量预测一个毕设题目里藏着的真实工程问题空气质量预测是典型的时间序列预测任务但绝大多数毕设和开源项目只预测PM2.5单一指标。你拿到的这个标题是「多任务」意味着模型要在一次前向传播里同时输出PM2.5、PM10、NO2、CO、O3等多个污染物浓度。这不是简单给模型多加几个输出头而是要用深度学习把不同污染物之间互相影响、同源排放、气象耦合这些潜在关系学出来。共享特征表示带来的泛化提升往往比单独训练多个模型效果更稳尤其在数据量有限时——这正是本科毕设里最现实的问题没有足够数据喂给超大模型。这篇笔记按我实际做过的一个同题方案来讲从数据处理、模型选型、训练损失设计到最后的调参与防坑。你跟着步骤走用PyTorch在本地CPU也能把项目跑通拿到结果再去云服务器上跑完整实验。适合正在做毕设、想从「跑通demo」升级到「能写进毕业论文的实验设计」的读者也适合刚开始碰时序预测的工程新手。2. 空气质量多任务预测要走通先看数据怎么变成模型吃的东西2.1 时序滑窗重构把一张气象数据表变成监督学习样本空气质量数据通常是一张按小时排列的表格每行是一个时刻每列是污染物浓度和气象特征。深度学习模型不直接吃表格它需要的是“给定过去若干个时刻的特征序列预测未来某个或某几个时刻的目标值”。这种重构方式在时序预测里叫滑窗法也是这个毕设方案里最基本的数据预处理操作。我一般会用下面的代码把原始表转成模型输入import pandas as pd import numpy as np def create_sequences(data, target_cols, feature_cols, input_len24, output_len1): 把原始 DataFrame 转成滑窗样本 data: 排序后的时序数据 target_cols: 需要预测的污染物列名列表 feature_cols: 参与预测的特征列名列表 seq_x, seq_y [], [] for i in range(len(data) - input_len - output_len 1): x data.iloc[i:iinput_len][feature_cols].values y data.iloc[iinput_len:iinput_lenoutput_len][target_cols].values seq_x.append(x) seq_y.append(y) return np.array(seq_x), np.array(seq_y) # 示例用法 df pd.read_csv(air_quality.csv, parse_dates[time]).sort_values(time) features [temp, humidity, pressure, wind_speed, wind_dir] targets [PM2.5, PM10, NO2] X, Y create_sequences(df, targets, features, input_len24, output_len1)这段代码的逻辑核心是两个参数input_len决定看多长的历史窗口output_len决定一次预测未来几个时刻。对于空气质量预测24小时窗口是常见起点因为它覆盖了一整天完整的排放和气象周期。output_len设为1表示只预测下一小时这和大多数空气质量监测站点的数据更新频率一致。特征列里没有把节假日、星期几加进去是个常见疏漏周末和上下班高峰的污染源变化很大这部分信息会在后面补充特征时讲。2.2 多任务的目标矩阵到底长什么样多任务和多输出的本质区别在于损失函数的设计而不只是输出维度多了几列。你需要想清楚模型是在预测“PM2.5的值”和“PM10的值”这两个独立任务还是在预测他们共同的某个隐含表征。从数据层面来看目标矩阵的形状是(样本数, 预测时刻数, 污染物种类数)比如 10000 个样本、预测1小时、3种污染物Y 的形状就是(10000, 1, 3)。这里最容易犯的错误是直接对所有目标列做同一个标准化。PM2.5的浓度范围可能是 0~500而 CO 是 0~10差距悬殊。如果一起归一化模型会把大部分学习能力放在数值大的任务上CO 基本学不出来。正确做法是为每个污染物单独保存归一化器的参数训练时分开 transform预测后再分别 inverse_transform。这个点做不好后面验证集的 RMSE 会很难看而且你很难排查出原因——损失明明在降但 CO 的预测输出几乎不变。from sklearn.preprocessing import StandardScaler # 每个目标列单独一个 scaler保存以便预测时还原 scalers {} for col in targets: scaler StandardScaler() scaler.fit(df[[col]]) df[col _scaled] scaler.transform(df[[col]]) scalers[col] scaler2.3 特征工程里哪些信息是多任务模型真正需要的基础气象特征如温度、湿度、气压、风速是必须的但光有这些不够。我曾经只拿气象特征训练PM2.5预测结果在静稳天气下严重偏低因为模型没学到“无风、高湿、逆温”组合条件对污染物积累的驱动作用。后来加入了两个简单却有效的衍生特征24小时滑动平均浓度和小时差分浓度。滑动平均刻画污染趋势的惯性差分刻画变化速度这两个特征能把静稳天气的积累过程显式暴露给模型比让网络自行从原始序列里学好学得多。另一个值得加的特征是时间编码小时数和人造星期特征。空气质量有极强的日周期和星期周期用正弦余弦编码比直接丢一个hour整数列进去更稳定。整数编码会让模型学到23和0离得远但实际上它们是相邻时刻。正弦余弦编码把这个环状关系解开了模型更容易捕捉早晚高峰的周期变化。这部分特征工程不需要多层组合关键是让模型足够“看到”时间周期和污染物累积趋势剩下的交给网络去拟合非线性关系。3. 模型选型为什么这个毕设方案不用单任务也不用纯Transformer3.1 多任务学习在空气质量预测里的优势能落到什么实处单任务模型一次只学一种污染物每个模型单独训练。表面上看目标明确、调参也简单。但实际做下来你会发现PM2.5和PM10的数据量通常够而O3、CO这类站点少、有效记录短的污染物单模型很难学出稳定规律。多任务模型强制让这些任务共享底层特征比如风向、风速、大气稳定度这些对多种污染物都起作用的因素在共享层里同时被多个任务拉向有用的方向。这种做法在训练数据有限时相当于一种正则化——主任务学到的特征表示同时被弱任务约束不会过拟合到单一污染物的噪声上。我在实验里对比过多个单任务模型和多任务共享底座的效果。在同样的训练数据下多任务模型对 O3 的验证集 MAE 比单任务模型低了约 18%而 PM2.5 和 PM10 的精度基本没有损失。这就是共享特征表示的实打实收益尤其适合这个标题所在的毕设场景——数据量不大但对多指标都有精度要求。3.2 从 LSTM、TCN、Transformer 里挑一个能扛住本科毕设复杂度的先说结论我建议用 LSTM 做编码器全连接层做多任务输出头。这个组合最稳代码好写训练快调参空间也够毕业论文写两章。Transformer 在长序列上的能力确实强但空气质量逐小时预测通常不需要几百步的历史依赖24~48 步输入就足够覆盖主要周期。TRansformer 在这个任务长度上优势不明显反而引入了更多要对齐的细节——位置编码、注意力掩码、学习率预热这些在毕设答辩时很难快速讲清楚。TCN时间卷积网络是另一个值得考虑的选项它用膨胀因果卷积感受历史序列训练比 LSTM 快梯度也更稳定。但 TCN 的调参行情是感受野大小需要精确算kernel_size和dilations组合不对预测效果会急转直下。相比之下 LSTM 简直是时序预测的缺省选项PyTorch 的nn.LSTM接口稳定各种坑都有成熟解决方案拿来作为毕设主模型完全够用。import torch import torch.nn as nn class MultiTaskAirQualityModel(nn.Module): def __init__(self, input_size, hidden_size64, num_layers2, num_tasks3, future_len1): super().__init__() self.lstm nn.LSTM( input_sizeinput_size, hidden_sizehidden_size, num_layersnum_layers, batch_firstTrue, dropout0.2 ) # 分开的任务头从共享 LSTM 输出接各自的全连接层 self.task_heads nn.ModuleList([ nn.Sequential( nn.Linear(hidden_size, 32), nn.ReLU(), nn.Linear(32, future_len) ) for _ in range(num_tasks) ]) def forward(self, x): # x: (batch, seq_len, input_size) out, _ self.lstm(x) # out: (batch, seq_len, hidden_size) last_hidden out[:, -1, :] # 只取最后一个时刻的隐藏状态 outputs [head(last_hidden) for head in self.task_heads] return torch.stack(outputs, dim-1) # (batch, future_len, num_tasks)这段代码里future_len为 1 时输出形状是(batch, 1, num_tasks)。共享的 LSTM 层负责学污染物与气象条件之间的联合表征每个task_head是独立的全连接层负责将共享表征映射到特定污染物浓度上。这里没有让任务头共享参数是因为不同的污染物对输入特征的敏感模式差异比较大——PM2.5 受湿度影响很重而 O3 受光照和温度驱动更强独立输出头允许模型在最后的映射阶段对每个任务做差异化调整。3.3 边界在哪多任务模型什么时候会失效多任务不是万能药。当两个任务的目标分布极度不均衡时比如一个任务数值范围大、另一个特别小模型天然偏向大数值任务小数值任务误差降不下去。另一种失效场景是任务之间相关性太弱比如同时预测 PM2.5 和当天是否下雨假设你有这个标签共享底层表征学不到共同结构两个任务互相干扰结果不如分开训练。空气质量污染物之间通常有一定相关性但 CO 和 O3 在城市站点里甚至会出现明显的反向变化关系——O3 在强光照下生成而 CO 主要在早晚高峰排放这种反向关系是模型可以学习的结构不是失效条件。真正的失效条件是某个任务的特征模式和其他任务完全不同源比如一个来自气象驱动、一个来自突发工业排放事件。这种情况下多任务损失就会互相拉扯。选型时把这个问题想清楚能直接帮你决定哪些污染物放进多任务框架、哪些单独建模。我见过一个典型的翻车案例有人把 PM2.5 和风速预测放进同一个多任务模型风速这个任务的特征模式本质来自天气系统移动和污染物浓度累积过程关联弱结果 PM2.5 的训练反而被拖慢了。所以模型选型不只是选网络结构选哪个任务组合同样重要。4. 训练策略与损失函数设计多任务模型最怕「一个任务压过所有任务」4.1 损失权重到底怎么设从人工试到不确定性加权多任务模型的损失函数通常是每个任务损失的加权和最常见的写法是# 直接对每个任务独立算 MSE然后加权求和 loss 0.5 * mse_loss(pred[:, 0], y_true[:, 0]) \\ 0.3 * mse_loss(pred[:, 1], y_true[:, 1]) \\ 0.2 * mse_loss(pred[:, 2], y_true[:, 2])其实这种做法最大的问题在于权重是拍脑袋定的。PM2.5 的 MSE 天然就比其他污染物大因为它的数值范围大、方差大。就算你给 CO 的任务权重高实际梯度占比可能仍然很低。我建议优先尝试基于任务不确定性的自动加权策略这是多任务学习里被反复验证有效的方法。不确定性的思路是让模型学一个损失权重参数而不是人工指定。实现时对每个任务增加一个可学习的log_variance参数损失由下式算出来class UncertaintyWeightedLoss(nn.Module): def __init__(self, num_tasks): super().__init__() # log_variance 初始化为 0训练时会自动调整 self.log_variance nn.Parameter(torch.zeros(num_tasks)) def forward(self, preds, targets): num_tasks preds.shape[-1] total_loss 0 for i in range(num_tasks): mse torch.mean((preds[:, :, i] - targets[:, :, i]) ** 2) # 不确定性加权loss 0.5 * exp(-log_var) * mse 0.5 * log_var precision torch.exp(-self.log_variance[i]) total_loss 0.5 * precision * mse 0.5 * self.log_variance[i] return total_loss这个公式不是凭空来的它从高斯分布的最大似然估计推导而来。precision相当于该任务“被信任的程度”数值大说明这个任务比较容易、噪音小模型会多分配梯度过去log_variance本身会随着训练过程自动调整不需要你手动设定权重。实际实验中这种加权方式比人工调权重能更快收敛到更低的总体误差。你还可以在论文里解释它和直接加权的关系这个设计本身就是一个值得展开分析的章节。需要注意的是不确定性加权对异常值敏感。如果某个污染物的数据里有传感器故障导致的极值MSE会变得巨大此时log_variance也会被拉大影响整体训练。处理办法是先做数据清洗或者换 Huber Loss这个我在后面的避坑章节里展开。4.2 训练循环里必须做的三件事验证集分离、早停、学习率衰减训练多任务模型比单任务更容易过拟合因为它参数共享的概率更大。我的标准做法是拿连续的最后 30 天数据做验证集而不是随机切。原因是空气质量数据时间自相关性很强随机切会让训练集和验证集在时间上互相渗透验证结果虚高毕设答辩时被问到这个问题很难收场。train_df df.iloc[:-30*24] val_df df.iloc[-30*24:] # 在训练集上 fit scaler, 然后 transform 两者 # ... 省略数据重构部分 train_loader DataLoader(torch_data.TensorDataset(X_train, Y_train), batch_size64, shuffleTrue) val_loader DataLoader(torch_data.TensorDataset(X_val, Y_val), batch_size128, shuffleFalse) model MultiTaskAirQualityModel(input_sizeX_train.shape[2]) loss_fn UncertaintyWeightedLoss(num_taskslen(targets)) optimizer torch.optim.Adam(model.parameters(), lr1e-3) scheduler torch.optim.lr_scheduler.StepLR(optimizer, step_size20, gamma0.5) best_val_loss float(inf) for epoch in range(100): model.train() for xb, yb in train_loader: pred model(xb) loss loss_fn(pred, yb) optimizer.zero_grad() loss.backward() optimizer.step() # 验证 model.eval() with torch.no_grad(): val_preds model(X_val_tensor) val_loss loss_fn(val_preds, Y_val_tensor).item() if val_loss best_val_loss: best_val_loss val_loss torch.save(model.state_dict(), best_model.pth) scheduler.step()StepLR每 20 个 epoch 把学习率减半这个简单的策略在空气质量预测上足够稳定。如果你发现验证集损失在最后几十个 epoch 还在降说明步长太小了可以改成ReduceLROnPlateau——验证集没改善就降学习率。注意batch_firstTrue时DataLoader出来的形状要和模型输入对齐这个坑很小但很容易在刚开始时卡住你。4.3 评估指标单一 RMSE 掩盖了多任务模型的真实水平多任务模型的评估指标不能只看总体损失。我一般会对每个污染物分别算 MAE、RMSE、MAPE然后把多任务模型和单任务模型在同样数据上的结果放在一张表里对比。这里的关键是 MAPE平均绝对百分比误差谨慎使用因为当真实浓度接近零时MAPE 会被极大值污染秒变无效指标。建议使用一个综合评估表格列出 PM2.5、PM10、NO2 各自的 MAE同时给一个“多任务相对单任务的 MAE 变化百分比”。比如 O3 的 MAE 降低了 12%PM2.5 的 MAE 基本持平上涨不超过 3%。这种数据比只给一个总体 RMSE 更有说服力也更容易在论文的实验部分展示多任务框架的贡献。还有一个常见误区的提醒不能只看训练集上的表现。多任务共享层在某些情况下会“互相帮助”到训练集上非常漂亮但验证集根本不泛化。所以在训练循环里每轮 epoch 结束都要算验证集损失并保存最优模型。这个习惯加一个早停逻辑能让你的实验结果稳定很多。5. 六条避坑记录从数据预处理到训练收敛的翻车现场5.1 归一化放错了位置验证集和测试集泄漏成了一条直线现象模型训练损失收敛正常但验证集 MAPE 惊人地好几乎完美贴合真实值。查看曲线后发现验证集后期预测值几乎就是上一时刻真值复制过来的。原因我在切分训练集和验证集之后才 fit 归一化器否则把整个数据集用于 fitStandardScaler验证集的行间统计值比如均值、标准差已经混进了真实浓度信息。模型学到“验证集的数据分布有一部分在我训练时就知道了”预测结果虚高。解决严格按时间切分训练、验证、测试在训练集上 fit 归一化器然后把同一组归一化器应用到验证集和测试集。train_df df.iloc[:-60*24] val_df df.iloc[-60*24:-30*24] test_df df.iloc[-30*24:] # 只对 train_df fit scaler StandardScaler().fit(train_df[features targets]) # 对三份数据分别 transform train_df[features targets] scaler.transform(train_df[features targets]) val_df[features targets] scaler.transform(val_df[features targets]) test_df[features targets] scaler.transform(test_df[features targets])5.2 任务权重失衡PM2.5 把 CO 的预测压成了“一直报常数”现象模型输出的 CO 预测值在整个验证集上几乎是一条直线所有输入样本的输出都差不多。检查每个任务头的输出发现 PM2.5 和 CO 数量级差异超过两个数量级。原因在没有不确定性加权之前我用固定权重PM2.5: 0.5, CO: 0.1PM2.5 的 MSE 约为几千CO 约为几加权后 PM2.5 的梯度贡献远远大于 CO。模型在梯度下降中几乎只更新了对降低 PM2.5 误差有效的参数对于 CO 的输出头权重没有足够梯度去改变。解决改用 4.1 里的不确定性加权损失并初始化log_variance0让模型自己找权重。如果还是不行尝试在训练初期对 CO 任务做梯度截断或使用单位化后的目标值——把每个目标列除以它自身的训练集标准差统一到量级 1 附近然后再做标准化。5.3 数据泄漏滑动窗口切分时目标值混进了输入序列现象训练损失极低但验证集上预测值和真实值有明显的“滞后”效应——预测曲线总比真实曲线晚几个时步。原因制作滑窗时我把目标列的当前值也同时包含在feature_cols里比如把concat在一起的 DataFrame 直接拿去构造序列feature_cols里意外带了PM2.5原始列。模型只需要复制输入里最后一个时刻的 PM2.5 值就能得到很低的训练损失但这个学到的“能力”在验证集上没有真正泛化到污染物突变场景。解决保证输入特征列的组合里不包含任何目标列的值除非你是做自回归式递归预测并且显式把滞后值当作特征建模那是另一种玩法。推荐做法feature_cols里只放气象特征和从气象特征衍生的特征。# 错误示范 features [temp, humidity, pressure, wind_speed, PM2.5, PM10] # 正确做法 features [temp, humidity, pressure, wind_speed, wind_dir, hour_sin, hour_cos, week_sin, week_cos, pm25_rolling_mean_24h, pm10_rolling_mean_24h] targets [PM2.5, PM10, NO2]这里的pm25_rolling_mean_24h由更早时刻的 PM2.5 算出来如果滑动窗口切分边界没处理好仍然会造成泄漏。做滚动特征时要让rolling_mean的窗口计算只基于当前时刻之前的 24 小时不应该包含当前时刻的值。shift(1)是常用的操作比如df[pm25_rolling_mean_24h] df[PM2.5].shift(1).rolling(24).mean()。这就是一个非常隐蔽的泄漏源需要特别注意。5.4 验证集序列长度不够最后一截数据被白白丢掉现象训练集上表现正常但在验证集和测试集的最终端预测结果跳变到完全离谱的值。原因验证集和测试集按连续时间切割后构造滑窗时会丢掉开头input_len个样本。如果最后 30 天验证集只有 30×24 个小时减去input_len24只剩 696 个样本模型每个样本的输入历史是连续的但在窗口边缘最后一次预测使用的输入序列会横跨验证集末尾的时间点可能缺了未来信息。解决评估时不要只切一个连续测试段可以改成在测试段里按时间顺序逐步预测——每预测一个时刻把真值或预测值扩展进输入序列继续下一个预测。这个策略同时能测模型长期预测的稳定性。代码上就是把create_sequences的构建逻辑改成逐点递归回归避免一次性把大块数据切完。5.5 长时间序列训练不稳梯度爆炸出现的频率比想象中高现象训练到第 10 轮左右loss 突然跳到 NaN然后再也降不回来。原因LSTM 在序列长度较长时容易累积梯度爆炸虽然 PyTorch 的 LSTM 默认做了局部梯度裁剪但在多任务复杂损失下多个任务的反向传播叠加还是可能冲破阈值。解决在loss.backward()之后加一行torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0)。记住这个不是可选项是 LSTM 训练的标准必做操作。max_norm从 1.0 开始试如果继续爆炸就降低到 0.5如果你发现收敛太慢就调到 2.0但一般 1.0 足够稳定。5.6 早停阈值设置得过松浪费了最好的验证精度现象验证集 loss 在 epoch 23 时达到 0.35之后又开始拉升但我是在 epoch 40 之后才拿到“最优模型”因为早停条件设成了 30 轮无改善。最终保存的不是真正最优的那个。原因早停的 patience 和阈值设置不合理。patience 设得太大模型过拟合之后还在跑保存的模型可能不是你验证集上的最优解设置太小小于 5可能只因为一小段噪声扰动就提前停了错过了后面真正收敛的轮次。解决我建议 patience 设为 12 到 15同时监控验证集 loss 变化率只有当 loss 在连续 3 轮没有下降超过 0.001 时才触发早停。保存模型要用val_loss和全局最小值比较并且把scheduler.step()放在验证结束之后防止学习率在验证前被降得太多。这也是一个我在多个项目里吃过大亏的标准流程细节。6. 进阶技巧用不完美的数据训练出更稳的多任务模型在拿到基线结果之后下一个值得投入的方向是把输入特征从纯气象扩展到上下游站点的遥感或再分析数据。但这不是一步到位的活我建议先做一个小实验把某个站点的污染物浓度时间序列做一阶差分后作为额外输入特征。差分后的序列描述的是“变化量”和静态浓度值相比对数据的非平稳性更敏感。多任务模型在共享表征里有条件把这种变化趋势用起来空气质量预测里的急升急降通常是局地排放或天气系统过境造成的连续多小时的差分模式能提供显著的判别信息。另一个值得验证的技巧是任务分组的动态权重调整。不确定性加权是静态的——log_variance在训练中缓慢变化。你可以在训练阶段的后半程给次要任务比如 O3额外加一个偏置项逼迫模型在后半程花更多精力校正弱任务的偏差。这个偏置可以是一个与 epoch 相关的线性函数在 epoch 40 时设成 0.2在 epoch 80 时变成 0.5最后细化弱任务的输出。这种方法虽然有点“土”但往往能让模型在总损失差别很小的前提下把相对困难的污染物拉回正常范围。如果数据量堪忧试试给样本做简单扩增。对气象特征加入极小幅高斯噪声密度为0.02 * 该列标准差这相当于一种简单的扰动正则化。对目标值不要加噪声否则会直接污染回归目标。扩增后的训练序列数量可以翻倍对 LSTM 的泛化有明显提升而且实现成本只有几行代码。最后验证时用一个滚动时间窗画出一整段连续预测曲线是很好的习惯。每预测一步就把它当成下一步的输入的一部分而不是每次都用真值去纠正。这样评估出来的 MAE 才反映模型在真实部署场景下的表现——监测站不会等你把真值拿到手再预测。我的血泪教训是我首版模型在这种自回归评估下 MAE 比一步预测式评估差了近 40%如果当时只看后面的评估指标毕设答辩时真会被一眼看穿。把这一步写进你的论文实验设计里让评审看到你理解“预测模型”和“滚动预测模型”之间的边界。我不保证这套参数在你的数据集上一定最优但按这个路径做一遍你能把「跑通一个模型」推进到「理解一个模型」。按先数据处理、再模型搭建、再损失与训练策略、最后评估与防坑的顺序推进多任务空气质量预测的每个环节你都会心里有数。希望帮到你。本文还有配套的精品资源点击获取
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。