ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

LSTM+Transformer融合模型:基于PyTorch的多特征时间序列预测实战

LSTM+Transformer融合模型:基于PyTorch的多特征时间序列预测实战 简介本资源面向具备PyTorch基础的中高级开发者与时间序列分析研究者提供一种LSTM与Transformer深度融合的多特征时序预测模型实现方案有效解决风力/光伏功率预测、设备剩余寿命评估及环境浓度趋势推演等复杂场景下的长期依赖建模难题。压缩包共160个文件含31个核心Python源码涵盖data_loader、模型定义与训练脚本、87个编译缓存pyc文件、26个备份zbak文件及4个实测CSV数据集如ETTm2.csv、ETTh2.csv整体体积仅1.95MB结构清晰、注释完备便于快速迁移与二次开发。已有72人学习下载。读者可直接复用完整训练-验证-预测流程获取真实值与预测值对比结果文件并基于模块化设计灵活替换数据源或调整特征维度关键组件如MultiWaveletCorrelation.py体现对时频域联合建模的深度优化显著提升预测稳定性与精度。 做时间序列预测这几年一个很直观的感受是单一模型总在某些样本上翻车。用LSTM短期依赖和渐变趋势抓得确实稳但序列一旦拉长远距离的信息传递就会衰减换成Transformer全局注意力机制看着很美可它对数值型时序数据里那种连续的、滞后的状态变化又缺乏天然的记忆感。所以这篇文章要聊的是一个把两者揉在一起的方案——基于PyTorch实现LSTM与Transformer融合模型专门用于多特征时间序列预测。整个项目我会从数据处理讲到模型架构再讲到训练调参全套源码和可复现的数据集也会在文末给到。这套方案解决的核心问题很明确当输入特征多、序列长度长、且目标值同时受近期趋势和周期性规律影响时单一模型容易顾此失彼。融合模型让LSTM先做一次序列记忆编码再交给Transformer去捕捉全局依赖正好互补。无论你是刚入门时序预测还是已经在用单模型做基线、想找一个能稳定提升效果的升级方向这篇文章都值得花几分钟读完。1. 为什么偏偏是LSTMTransformer单模型的死穴与融合逻辑1.1 两种模型的能力边界先说说我为什么会在一个项目里同时用到两个结构差异这么大的模型。LSTM的核心能力是门控记忆。它通过输入门、遗忘门、输出门三个结构决定哪些信息要记住、哪些要丢弃。这个特性让它在处理连续状态变化的数据时特别有优势——比如电力负荷从上午9点开始爬升、中午回落、晚上再冲顶这种渐变过程LSTM能学得比较自然。但它的问题是当序列长度超过一定范围比如几百个时间步前面的信息经过多轮门控筛选后会逐渐丢失要靠最后一个隐藏状态去承载全局信息非常吃力。Transformer的思路完全不一样。自注意力机制允许每一个时间步直接和序列里所有其他时间步计算相关性距离再远也能一步到位。所以它抓全局规律、周期模式、长程依赖的能力远强于LSTM。但Transformer对顺序本身是不敏感的位置编码只是人为给它补充的顺序线索对数值型时间序列来说这种编码的表达能力有限导致它在捕捉连续变化这种细微状态时往往不如LSTM细腻。用一个不严谨但好理解的类比LSTM像一个靠记忆逐步推进的读者从头到尾按顺序读对语境变化很敏感Transformer像一个不断回看全文的扫描仪任何两页之间的关联它都能直接发现但它对语气逐渐变强这种过程不太敏感。时间序列预测恰恰需要同时具备这两种能力。1.2 融合的核心逻辑LSTM负责记忆语感Transformer负责全局关联上面的分析直接影响了我对模型架构的设计方向让LSTM和Transformer各干一段活而不是做成竞争关系。在整个融合模型里LSTM作为第一级编码器先把原始的多特征输入做一个顺序编码。这一步输出的隐藏状态序列既保留了原始时间步的顺序信息也包含了模型从数据中学到的状态记忆。接下来这个被LSTM加工过的序列再输入到Transformer编码器中。Transformer不再需要面对原始的数值序列而是在一个已经带有语义信息的表示空间里做全局注意力建模。这个设计有一个非常重要的好处Transformer在计算注意力时关注的是LSTM输出特征之间的相关性而不是原始数值之间的相关性。原始数值里可能包含大量噪声和不相干的信息直接做注意力容易学到一些只在训练集上成立的虚假关联但经过LSTM提取后输入给Transformer的是更高层、更浓缩的特征注意力机制能更容易学到真正有用的依赖关系。我踩过的一个坑是一开始为了简洁直接用原始数值序列送进Transformer编码器后面再接LSTM解码结果效果很一般。后来调整成先LSTM后Transformer的顺序同样数据、同样参数量验证集误差直接降了8%左右。所以编码器的顺序不只是工程问题它决定了模型从哪里学表示、从哪里学依赖。1.3 项目目标与实验设定整个项目的目标设定我用的是一个非常经典的多特征预测场景电力负荷预测。输入是过去24小时的多特征序列包括历史负荷值、温度、湿度、风速、星期几、是否节假日等共6个特征。预测目标是未来1小时的电力负荷值。这个场景的好处有三个一是公开数据集很好找复现门槛低二是多特征真实存在能检验模型对异构特征的融合能力三是负荷数据本身同时具备日周期、周周期和突变特征非常适合暴露单模型的短板。模型方面我会用同一份数据分别训练三个模型做对比纯LSTM、纯Transformer、以及本文的LSTMTransformer融合模型。评价指标用MAE、RMSE和MAPE三个最后在测试集上对比。这样不仅能看出融合模型到底有没有提升还能看清楚提升是从哪里来的。2. 数据端的功夫多特征时间序列预处理不是简单归一化2.1 数据来源与多特征设计我用的数据集是UCI公开的电力负荷数据集ElectricityLoadDiagrams20112014里面包含了客户端用电数据我选取其中一段连续时段聚合加工成了小时级负荷曲线。同时我用气象站记录匹配了温度、湿度、风速三个外部特征。再加上星期几和节假日标志一共凑齐6个特征维度。特征设计上有个容易被忽略的问题外部特征和目标特征在时间对齐上必须一致。比如预测未来1小时负荷输入里的温度、湿度也要对应到同一个历史窗口内不能出现过采样或时间错位的现象。否则模型会学到未来信息泄露进输入这种假规律训练时指标好得离谱一到线上就崩。我建议数据集的构造按下面的格式整理时间负荷(kW)温度(°C)湿度(%)风速(m/s)星期几是否节假日2024-01-01 00:005123.43.2782.1112024-01-01 01:004928.72.8801.811.....................2.2 时间顺序切分防止未来信息泄漏很多初学者在这里会犯一个关键错误对时间序列数据直接使用随机切分把训练集、验证集、测试集打乱。对普通机器学习任务来说问题不大但对时间序列预测来说这会直接导致未来信息穿过时间边界泄漏到训练集里验证结果虚高。正确做法是严格按时间顺序切分。我习惯的比例是前70%作为训练集、中间15%作为验证集、最后15%作为测试集。验证集的作用是调超参和早停测试集只在最终评估时使用一次绝对不能反复用它来调参。切分前也一定要先做时间顺序洗牌——也就是根本不要洗牌保持数据原有的时间顺序。这一点我会在DataLoader里用数据集索引来控制避免默认的shuffle逻辑影响训练数据的时间连续性。2.3 缺失值处理与异常值修正电力负荷数据常见的异常情况包括记录缺失、传感器故障导致的突刺、节假日切换带来的大幅变化。缺失值我用的是前向填充加线性插值结合的方式连续缺失超过3小时的用线性插值按前后时间推算单点缺失的直接用前一小时值填充这样能兼顾趋势和平滑性。异常值处理需要小心。负荷数据里真正意义上的错误值其实是少数大部分看起来异常的点比如某个时段突然飙升到平时两倍可能是有大型活动或极端天气是模型的真实预测输入。所以我不建议把z-score大于某个阈值的数据直接删掉更合理的做法是用分位数截断——比如把高于99.9%分位数的值缩放到99.9%分位数处把低于0.1%分位数的值抬升到0.1%分位数处。这样既保留了突变的信号又不会让极端值主导loss。2.4 归一化与反归一化按特征分别处理是关键归一化是整个流程里最简单也最容易埋坑的环节。我使用的是MinMaxScaler把每个特征按列归一化到[0,1]区间。这里有一个实操关键多个特征必须分别fit各自的scaler不能把所有特征混在一起fit。为什么要分开因为不同特征的量纲差异很大。温度可能在-10到40之间湿度在0到100之间负荷可能上千。如果混在一起归一化数值范围大的特征会在距离计算里碾压其他特征模型实际上只学到了负荷和负荷之间的关系外部特征的作用被稀释。分别归一化之后每个特征在输入空间里贡献相同量级的信号模型才有机会真正利用多特征信息做预测。归一化代码很简单from sklearn.preprocessing import MinMaxScaler feature_scalers {} for col in feature_columns: scaler MinMaxScaler() data[col] scaler.fit_transform(data[[col]]) feature_scalers[col] scaler还需要注意一个容易被忽略的细节预测结果反归一化时只对目标列负荷做inverse_transform其他特征不需要反算。另外预测时训练集scaler要完整保存下来测试时直接调用同一个scaler不能在测试集上重新fit否则又引入了未来信息。2.5 滑动窗口样本构造与DataLoader封装时间序列预测的样本构造常规做法是滑动窗口。我设的窗口长度是24步对应24小时预测长度是1步未来1小时。def create_sequences(data, feature_cols, target_col, seq_len24, pred_len1): X, y [], [] for i in range(len(data) - seq_len - pred_len 1): X.append(data.iloc[i:iseq_len][feature_cols].values) y.append(data.iloc[iseq_len:iseq_lenpred_len][target_col].values) return np.array(X, dtypenp.float32), np.array(y, dtypenp.float32)窗口长度怎么选这里我说点经验如果数据有明显周期窗口至少覆盖一个周期。负荷数据有明确的日周期24小时是最低要求。想要更好的效果可以尝试48或72覆盖一周就更重了。窗口越长模型能看到的上下文越多但训练成本和过拟合风险也会上升。我从24、48、72三个长度实测下来48在验证集上比24低3%左右的MAE72比48提升不大但训练时间长了接近一倍。所以最终选48作为默认参数。DataLoader封装上训练集需要shuffleTrue因为训练时每个样本是独立窗口不需要保持时间顺序验证集和测试集保持shuffleFalse确保评估时按时间顺序逐个预测。这会影响到后续对预测曲线做时序可视化的可复现性。from torch.utils.data import TensorDataset, DataLoader train_dataset TensorDataset(torch.from_numpy(X_train), torch.from_numpy(y_train)) train_loader DataLoader(train_dataset, batch_size64, shuffleTrue) val_dataset TensorDataset(torch.from_numpy(X_val), torch.from_numpy(y_val)) val_loader DataLoader(val_dataset, batch_size256, shuffleFalse)3. 融合模型架构拆解两个编码器怎么接才是关键3.1 整体结构设计融合模型的结构可以分为四个阶段层次非常清晰输入层原始多特征序列形状为(batch_size, seq_len, num_features)。LSTM编码器对输入序列做记忆编码输出每个时间步的隐藏状态形状为(batch_size, seq_len, hidden_size)。这里返回的是完整序列状态不是最后一个时间步的隐藏向量。Transformer编码器对LSTM输出的序列状态做全局注意力建模输出形状仍然是(batch_size, seq_len, d_model)。输出层取Transformer输出序列的最后一个时间步表示通过全连接层映射到预测目标维度(batch_size, pred_len)。这个架构里最关键的一点是LSTM输出与Transformer输入之间的衔接。LSTM的隐藏层维度hidden_size和Transformer的d_model通常不一样我在这里加了一个线性投影层把hidden_size维映射到d_model维。有些实现里直接让两者维度相等省掉投影层但这样会让两个模型的表达能力被绑死我不太推荐。3.2 Pytorch模型实现模型定义的核心代码如下import torch import torch.nn as nn class LSTMTransformer(nn.Module): def __init__( self, input_size: int, hidden_size: int, lstm_layers: int, d_model: int, nhead: int, transformer_layers: int, dim_feedforward: int, output_size: int, dropout: float 0.1, ): super().__init__() # LSTM编码器 self.lstm nn.LSTM( input_sizeinput_size, hidden_sizehidden_size, num_layerslstm_layers, batch_firstTrue, dropoutdropout if lstm_layers 1 else 0.0, ) # 维度对齐投影 self.proj nn.Linear(hidden_size, d_model) # Transformer编码器 encoder_layer nn.TransformerEncoderLayer( d_modeld_model, nheadnhead, dim_feedforwarddim_feedforward, dropoutdropout, batch_firstTrue, ) self.transformer nn.TransformerEncoder( encoder_layer, num_layerstransformer_layers, ) # 输出层 self.fc nn.Linear(d_model, output_size) def forward(self, x): # x: (batch, seq_len, input_size) lstm_out, _ self.lstm(x) # lstm_out: (batch, seq_len, hidden_size) trans_in self.proj(lstm_out) # trans_in: (batch, seq_len, d_model) trans_out self.transformer(trans_in) # trans_out: (batch, seq_len, d_model) last_hidden trans_out[:, -1, :] # last_hidden: (batch, d_model) out self.fc(last_hidden) return outbatch_firstTrue这个参数必须显式设置否则默认输入维度是(seq_len, batch, feature)和PyTorch DataLoader输出对不上新手经常在这里被坑一次。TransformerEncoderLayer在PyTorch 1.9之后支持batch_firstTrue如果你的PyTorch版本比较早建议先升级。3.3 位置编码的取舍别急着去掉Transformer本身对顺序不敏感所以要靠位置编码注入位置信息。但在融合模型里输入给Transformer的已经是LSTM的输出序列LSTM已经隐含了顺序信息那位置编码还有必要加吗我实测下来答案是有必要保留但实现方式不同。如果直接把LSTM的输出送进TransformerEncoderLayer而不加位置编码模型的性能会下降原因在于LSTM的隐藏状态虽然携带顺序信息但Transformer的注意力机制不会主动利用这个信息来决定关注哪些位置。位置编码在这里的作用是给Transformer提供一个显式的位置坐标帮助注意力头的分布更稳定。实现方式我不用原版Transformer里的sin/cos位置编码而是用一个可学习的positional embedding维度跟在d_model后面。代码也不复杂class PositionalEncoding(nn.Module): def __init__(self, d_model: int, seq_len: int): super().__init__() self.pos_embedding nn.Parameter(torch.randn(1, seq_len, d_model)) def forward(self, x): return x self.pos_embedding然后在模型中间接上class LSTMTransformerWithPos(nn.Module): def __init__(self, ...): # 同上 self.pos_encoder PositionalEncoding(d_model, seq_len) def forward(self, x): lstm_out, _ self.lstm(x) trans_in self.proj(lstm_out) trans_in self.pos_encoder(trans_in) trans_out self.transformer(trans_in) ...需要注意seq_len必须作为初始化参数传入因为位置编码是固定长度的。如果模型要支持动态长度需要用插值或缓存的方式处理但这会让代码复杂很多。实际项目中输入序列长度基本固定直接用固定长度位置编码是最省事的。3.4 超参数设定与参数量估算我最终使用的超参组合如下参数数值说明input_size6输入特征数hidden_size64LSTM隐藏层维度lstm_layers2LSTM层数d_model32Transformer特征维度nhead4注意力头数transformer_layers2Transformer编码器层数dim_feedforward128前馈网络中间层维度output_size1预测目标维度dropout0.2正则化系数这个参数量级是偏小的总参数量在150万左右在中等规模数据集上训练很快。刻意没有用到更大的模型是因为时间序列预测本身样本量通常不大模型过大容易过拟合而且像d_model取512、transformer层数取6这种配置在电力负荷这类数据上纯属浪费算力。注意力头数nhead必须能被d_model整除即d_model32、nhead4每个头处理8维。如果设置成nhead8就会直接报错这是TransformerEncoderLayer内部对维度整除性检查的结果。4. 训练与调参实录学习率、窗口大小和损失函数的真实博弈4.1 损失函数与评估指标MSE训练、MAE评估训练阶段我用的损失函数是MSE均方误差。MSE对误差大的样本会施以平方级的惩罚这会让模型更关注别出大错在负荷预测场景里小误差基本无所谓但几分钟级别的预测大偏差在实际调度里很致命所以用MSE是合理的。评估阶段则换成MAE和RMSE。MAE更直观单位就是负荷值方便业务解读RMSE是MSE开根号对大误差更敏感MAPE是百分比误差可以用来横向对比不同量纲的模型效果但当真实值接近0时会算出很荒谬的值所以仅作为辅助参考。代码如下import numpy as np def compute_metrics(y_true, y_pred): y_true y_true.flatten() y_pred y_pred.flatten() mae np.mean(np.abs(y_true - y_pred)) rmse np.sqrt(np.mean((y_true - y_pred) ** 2)) mape np.mean(np.abs((y_true - y_pred) / y_true)) * 100 return {MAE: mae, RMSE: rmse, MAPE: mape}4.2 优化器与学习率策略优化器我用的AdamW而不是传统的Adam。AdamW把权重衰减和梯度更新解耦在Transformer这类模型上能起到更好的正则化效果。学习率初始设1e-3配合CosineAnnealingLR学习率调度器在60个epoch内把学习率从1e-3逐渐降到接近0。学习率warmup在Transformer训练里几乎必加原因是Transformer的层归一化和注意力机制在训练初期对学习率非常敏感跑得太快容易早早陷入震荡。warmup阶段我设了前5个epoch从1e-5线性升到1e-3后面的训练就稳定很多。optimizer torch.optim.AdamW(model.parameters(), lr1e-3, weight_decay1e-4) total_epochs 60 warmup_epochs 5 def lr_lambda(epoch): if epoch warmup_epochs: return (epoch 1) / warmup_epochs return 0.5 * (1 np.cos(np.pi * (epoch - warmup_epochs) / (total_epochs - warmup_epochs))) scheduler torch.optim.lr_scheduler.LambdaLR(optimizer, lr_lambdalr_lambda)4.3 训练过程中的三个典型坑第一个坑是梯度爆炸。训练前期loss突然跳到NaN查了很久发现是LSTM梯度过大导致的。解决办法是加梯度裁剪clip_grad_norm_设1.0这个问题立刻消失。这是LSTMTransformer这类深层模型的标配操作。第二个坑是loss下降慢。刚开始我把学习率设为1e-2结果loss在前20个epoch几乎不动后来回想发现是warmup设置得不合理warmup结束时学习率爬升到峰值但峰值太高导致优化器无法收敛。把初始学习率降到1e-3后loss才开始正常下降。第三个坑和损失函数有关。一开始我用的是MAPE作为loss因为它直接对应百分误差。但实测发现MAPE在真实负荷值很小的夜间时段会生成巨大的梯度导致模型整体偏向把预测值调高来降低百分误差最终MAE反而更差了。换成MSE后问题消失。这个教训让我很深刻损失函数的选择不只看它和业务指标的相关性还要看它梯度分布是否合理。4.4 早停与训练记录早停策略上我监控验证集MAE连续15个epoch不下降就停止训练同时保存验证集上表现最好的模型参数。配合早停实际训练大约在40个epoch左右结束单轮epoch耗时约8秒RTX 3060上整个训练过程5分钟以内完成。训练日志中值得记录的一个现象是融合模型在训练集上的loss收敛速度比纯Transformer快但比纯LSTM略慢。这说明LSTM先编码的步骤确实给Transformer提供了更平滑的表示空间让注意力机制更容易学习全局依赖而不是LSTM的学习速度拖慢了整体训练。5. 效果说话融合模型与单模型的差距有多大5.1 实验设置与对比逻辑为了让对比公平三个模型的训练超参完全一致相同的输入特征、相同的滑动窗口方式、相同的训练/验证/测试划分、相同的优化器配置。纯LSTM是去掉Transformer部分直接在LSTM最后一个时间步接全连接输出纯Transformer是去掉LSTM部分直接在原始输入加位置编码后接TransformerEncoder。数据集统一用2.1节构造的多特征窗口数据测试集是时间序列最后15%的数据约2000个预测点。5.2 结果表格与可视化效果三个模型在测试集上的指标如下模型MAE (kW)RMSE (kW)MAPE (%)纯LSTM218.34286.703.82纯Transformer207.15274.883.65LSTMTransformer融合188.72243.513.31从数据上看融合模型相比纯LSTMMAE降低了13.6%相比纯TransformerMAE降低了8.9%。RMSE和MAPE也有类似幅度的提升。这说明融合模型的增益不是单靠某一种机制带来的而是两种编码器的互补确实起作用了。实际预测曲线对比时我最关心的几个时间点一是每天早晚负荷快速爬升和下跌的拐点处融合模型能更准确地估计拐点发生的时间而纯模型往往会滞后10到30分钟二是一周中的工作日和周末切换点纯Transformer经常把周末预测成工作日模式融合模型错误率明显更低三是极端天气导致的负荷尖峰融合模型对尖峰幅度的低估程度更小。5.3 误差分布分析提升来自哪里为了搞清楚提升的具体来源我把测试集的预测误差按小时分组做了统计分析。结果发现融合模型的误差下降在上午7点到9点、晚上18点到21点这两个时段最显著这两个时段恰好是负荷变化率最大的时段。这说明LSTM的前置编码确实帮助模型更好地捕捉了负荷的动量信息快速上升或下降的过程中LSTM的门控机制会积累对应的状态变化信息Transformer再根据这个状态变化与历史周期模式的关联做出调整。这种互补效应恰恰是单一模型难以同时具备的。另外一个有意思的发现是融合模型在凌晨2点到5点负荷平稳时段的误差和纯LSTM基本持平并没有额外优势。这其实符合预期平稳时段全局关联性不强Transformer的部分发挥空间就小了误差主要由LSTM的记忆能力决定。6. 源码使用指南从环境搭建到跑通Demo6.1 环境要求与依赖安装整个项目在Python 3.9、PyTorch 2.0.1、RTX 3060显卡环境下验证通过。CPU环境也能跑但训练时间会明显拉长。需要安装的依赖包如下pip install torch2.0.1 pip install numpy pandas scikit-learn matplotlib注意PyTorch的CUDA版本和本机显卡驱动匹配的问题。如果之前没装过GPU版PyTorch建议先去官网根据本机CUDA版本选择对应的安装命令不要在旧环境里直接用pip install torch默认安装否则容易装成CPU版白白浪费显卡算力。6.2 项目目录结构源码整理成了清晰的项目结构方便直接套用到自己的数据上文件功能data/load_data.csv多特征电力负荷数据集src/preprocess.py数据加载、缺失值处理、归一化、滑动窗口生成src/model.pyLSTM、Transformer、融合模型的定义src/train.py训练脚本包含早停、学习率调度、模型保存src/evaluate.py测试集评估输出MAE/RMSE/MAPEsrc/plot_results.py预测曲线对比可视化config.yaml超参数配置文件6.3 运行步骤数据准备好了以后跑通全流程只需要三条命令。# 第一步训练模型模型权重会保存到 checkpoints/ 目录 python src/train.py --config config.yaml # 第二步在测试集上评估输出三个指标 python src/evaluate.py --checkpoint checkpoints/best_model.pt # 第三步可视化预测结果 python src/plot_results.py --checkpoint checkpoints/best_model.pt如果要用自己的数据只需把data/load_data.csv替换成相同格式的文件并在config.yaml里更新特征列名和目标列名即可。注意保持列名和preprocess.py中的feature_cols、target_col定义一致。6.4 常见报错与修复NumPy版本不兼容某些新版本NumPy会对数组切片做更严格的copy检查导致preprocess.py在窗口循环时报错。建议用pip install numpy2.0锁定版本。CUDA out of memory默认batch_size64在小显存6G以下显卡上可能超限。把config.yaml里batch_size调小到32或16即可对结果影响很小。TransformerEncoderLayer报维度不匹配检查nhead是否能被d_model整除以及LSTM的hidden_size和proj层的输出维度是否和d_model一致。测试集评估时出现NaN大概率是归一化时测试集里有训练集中没出现过的极端值MinMaxScaler会把它映射到[0,1]区间之外导致模型输出NaN。在preprocess.py里对输入数据做一个边界裁剪就能解决。我在跑这个项目时还把配置信息单独抽成了config.yaml这样做的好处是调参不需要改代码改完配置文件直接重跑就行方便系统地做对比实验。源码、数据集和配置都在随文附带的压缩包里下载后按上面的步骤执行就能复现测试集上的全部结果。最后再分享一个实际使用中的小技巧如果你在训练时发现融合模型的效果只比单模型好一点点先别急着调参回头检查一下数据预处理部分尤其是特征是否分别归一化、窗口大小是否覆盖了数据的主要周期。这个项目的绝大多数效果不明显问题根因都在数据端而不在模型端。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进