资讯动态

从RNN到LSTM:彻底搞懂门控机制与时间序列预测实战

发布时间:2026/9/15 16:15:47 来源:尧图企业网站定制
我一度觉得LSTM就是个被神化了的黑盒子网上教程要么画一堆门结构让人看晕要么丢出公式让人劝退。后来自己在深度学习这条路上摸爬滚打拿循环神经网络做时间序列预测、做文本情感分析、跑水文数据才慢慢把LSTM的脾气摸清楚。说白了它确实是处理序列数据的一把好手但如果你不理解它内部的三个门到底在干什么调参的时候就是瞎试模型不收敛你都不知道该怪谁。这篇文章我打算用最直白的方式把LSTM讲透。适合两类人看一类是刚入门深度学习、已经学过CNN但一碰序列数据就懵的新手另一类是已经用LSTM跑过模型但训练不稳定、预测结果总滞后想搞清楚原理好对症下药的人。我会从RNN的先天缺陷讲起再拆解LSTM的门控机制最后用PyTorch从零搭一个时间序列预测的完整例子把我在实战里踩过的坑一并交代清楚。1. 为什么循环神经网络记不住“很久以前的事”1.1 RNN的核心思想把历史信息装进隐状态在进入LSTM之前得先弄明白循环神经网络RNN是怎么设计的。传统全连接网络和卷积神经网络处理每个样本时都是独立地看比如一张图片就是一堆像素一句话就是一堆词向量网络不关心这些输入之间有没有先后顺序。但现实世界有一类数据天生就是序列天气预报按小时记录、股票按分钟变动、语音按帧采样、文本按词排列。这些数据的特点是“当前时刻的状态通常依赖之前一段时间的状态”。你今天出门要不要带伞不能只看此刻的气温还得看过去几个小时的气压变化趋势。CNN处理这种带时序依赖的数据就很别扭因为它没有“记忆”机制卷积核在一个局部窗口内做加权求和窗口一划走前面的信息就扔了。RNN解决这个问题的方式非常朴素它引入了一个叫隐状态hidden state的东西。在每一个时间步tRNN会做这样一件事[ h_t \tanh(W_{hh} h_{t-1} W_{xh} x_t b) ]也就是说当前时刻的隐状态 ( h_t ) 是由上一时刻的隐状态 ( h_{t-1} ) 和当前输入 ( x_t ) 共同决定的。你可以把 ( h_t ) 理解成一个不断更新的笔记每来一个新时刻它就把上一页的内容和当前看到的新信息合并起来写在下一页。这样RNN在处理序列时整个历史信息理论上都压缩在这个隐状态里网络靠它“回忆”过去。1.2 长期依赖困境传着传着信息就丢了RNN这个思路听着很顺但工程上跑起来就露馅了。问题出在那个 (\tanh) 激活函数身上。(\tanh) 的导数是 (1 - \tanh^2)最大值是1大部分取值区间里都小于1。在反向传播时梯度要从最后一个时间步一路回传到第一个时间步。这个过程里梯度要连乘一串雅可比矩阵。如果每个时间步的导数都小于1连乘几十步之后梯度就会指数级衰减到接近0。这就是梯度消失vanishing gradient。梯度消失的直接后果是网络只能学会相邻几个时间步的关系离得远的信息对当前输出几乎不起作用RNN变成了“短期记忆”。反过来如果每个时间步的导数大于1连乘之后梯度又会指数级膨胀变成梯度爆炸exploding gradient。梯度爆炸反而好办做梯度裁剪gradient clipping就能压住真正麻烦的是梯度消失。我拿一个很常见的例子说明假设你在读一段文本“北京是中国的首都它有很多历史古迹”后面某处的代词“它”指代的是“北京”。中间隔了十来个词普通RNN在处理到“它”的时候前面关于“北京”的隐状态信息早就被后面一串词的 (\tanh) 压缩抹平了模型根本不知道“它”指的是谁。这个现象在学术上叫长期依赖long-term dependency问题。简单说RNN能记住的东西撑不过几个时间步时间一长记忆就像被水泡过的字迹模糊得看不清了。1.3 LSTM的破局思路给网络装一条“记忆传送带”LSTMLong Short-Term Memory长短期记忆网络在1997年由Hochreiter和Schmidhuber提出解决的就是上面这个“记不住”的问题。它的核心改动很巧妙在RNN那个每次都要被重写的隐状态之外额外增加了一条细胞状态cell state通道。你可以把细胞状态想象成一条贯穿整条序列的传送带。每经过一个时间步传送带上的信息可以被擦除一部分也可以写入一部分新信息但它的内容不会被当成普通数据反复做非线性变换因此梯度在反向传播时可以沿着这条“高速公路”一路畅通地流回去不再被 (\tanh) 一层层削弱。这个设计思路后来被广泛应用直到今天LSTM在语音识别、自然语言处理、时间序列预测这些任务上仍然是默认的强基线。很多人一上来就想用Transformer替代一切模型但LSTM在小规模数据、单变量时间序列、工业部署这些场景里反而更轻量、更稳。2. LSTM核心原理拆解三扇门和一个细胞状态2.1 从流程图到直觉门控机制到底是什么LSTM的结构比普通RNN复杂但核心逻辑就一句话用三个门来控制信息在细胞状态上的增删改查。三个门分别是遗忘门forget gate、输入门input gate、输出门output gate再加上一条旁路的候选记忆candidate memory。如果你想彻底看懂LSTM的计算过程最直观的方式是把它想成一个记账流程遗忘门决定过去账本里的哪些内容可以划掉。输入门决定新收到的账单里哪些内容值得记进账本。输出门决定根据当前账本我应该对外报告一个什么结果。每个门都输出一个0到1之间的数值表示“保留多少比例的信息”。这个0到1的输出由sigmoid激活函数完成。0表示全部丢弃1表示全部保留。用乘法和加法把这些门的结果组合起来就是LSTM一个时间步内做的全部事情。2.2 三个门的数学细节与直觉对应先看遗忘门[ f_t \sigma(W_f \cdot [h_{t-1}, x_t] b_f) ]这里 ([h_{t-1}, x_t]) 表示把上一时刻的隐状态和当前输入拼在一起经过一个全连接层后通过sigmoid映射到(0,1)区间。(f_t) 的向量长度和细胞状态 (C_{t-1}) 一致向量里每个元素对应细胞状态里相应位置的“保留比例”。再看输入门[ i_t \sigma(W_i \cdot [h_{t-1}, x_t] b_i) ]同时还要算一个候选记忆[ \tilde{C}t \tanh(W_C \cdot [h{t-1}, x_t] b_C) ]注意这里用 (\tanh) 而不是sigmoid因为 (\tilde{C}_t) 本身是要被写入细胞状态的实际信息它需要能取负数表示“负向信息”而门控系数只需要表示“保留比例”所以用sigmoid。然后更新细胞状态[ C_t f_t \odot C_{t-1} i_t \odot \tilde{C}_t ]这就是LSTM最关键的一条公式。(\odot) 表示逐元素相乘。细胞状态 (C_t) 等于“旧状态乘以遗忘比例”加上“新候选记忆乘以输入比例”。整个过程只有乘法和加法没有非线性激活这保证了梯度在 (C_t \to C_{t-1}) 这条路径上可以流畅回传。最后是输出门[ o_t \sigma(W_o \cdot [h_{t-1}, x_t] b_o) ] [ h_t o_t \odot \tanh(C_t) ]隐状态 (h_t) 是从细胞状态里挑一部分内容输出再做一次 (\tanh) 压缩。这样一来(h_t) 和 (C_t) 分工非常明确(C_t) 是长期记忆负责保存跨时间步的信息(h_t) 是短期记忆负责当前时刻的输出。2.3 为什么门控能缓解梯度消失高速路上的“直通通道”我把这个原理讲得再透一点。在普通RNN中(h_t) 是直接影响 (h_{t-1}) 经过非线性变换的结果所以任意一步的梯度都要穿过多层 (\tanh)每层都让梯度缩小一点。而在LSTM中(C_t) 这条路径上只经过 (f_t) 这个逐元素乘法。当遗忘门接近1时(\partial C_t / \partial C_{t-1}) 也接近1梯度可以近乎无损地传回很多个时间步之前。这就是LSTM能记住长期信息的原因。并不是说它梯度完全不衰减而是它提供了一条衰减可以受控的通道。遗忘门由网络自己学习如果某个任务里旧信息长期有用网络自然会把遗忘门学成接近1如果旧信息已经没用网络也会学会把它关掉。我用一个生活类比帮大家记住想象你在一家餐厅当服务员手里拿着一张点菜单细胞状态。每来一桌新客人新输入你决定划掉之前写错的、客人退掉的菜遗忘门记下刚加的菜输入门然后根据这张菜单给后厨报菜名输出门。菜单本身始终在你的手里不会被扔掉这就是长期记忆。3. LSTM的变体、关键参数与适用场景3.1 变体那么多先学会分清GRU、BiLSTM和Stacked LSTM很多人在实战里会遇到GRUGated Recurrent Unit。GRU是LSTM的简化版把遗忘门和输入门合并成了一个更新门update gate同时把细胞状态和隐状态合并成了一个状态向量。[ z_t \sigma(W_z \cdot [h_{t-1}, x_t]) ] [ r_t \sigma(W_r \cdot [h_{t-1}, x_t]) ] [ \tilde{h}t \tanh(W \cdot [r_t \odot h{t-1}, x_t]) ] [ h_t (1 - z_t) \odot h_{t-1} z_t \odot \tilde{h}_t ]GRU参数更少训练速度更快在小数据集上通常和LSTM效果非常接近。如果项目追求部署效率或者训练数据不太多GRU是值得优先试的方案。但也别迷信“GRU一定比LSTM好”我的经验是数据量充足、序列规律复杂的时候LSTM的表达能力还是略胜一筹。双向LSTMBidirectional LSTM简称BiLSTM是另一种常见做法核心思想是不只从前往后读序列还从后往前读一遍然后把两个方向的隐状态拼起来。这在自然语言处理里特别常用因为一句话里某个词的语义往往要结合后面的词才能确定。比如“他正在银行办贷款”和“他坐在河边的银行钓鱼”两个“银行”含义不同只看前面的词很难区分。BiLSTM能同时看到上下文预测准确率自然更高。堆叠LSTMStacked LSTM则是把多个LSTM层纵向叠起来。第一层输出的是低层特征第二层输入的是第一层的隐状态逐层提取更高层的语义。但层数不是越多越好一般2到3层就够。层数过多不仅参数量暴涨、训练时间拉长还会因为层间梯度传递路径过长而再次出现优化困难。3.2 实战前必须搞懂的5个超参数LSTM的超参数说白了就几个但每一个都直接影响模型能不能收敛、效果好不好。首先是隐藏单元数hidden_size。这个参数决定细胞状态和隐状态的维度相当于模型的“记忆容量”。太小了记不住复杂模式太大了容易过拟合而且训练慢。我的经验是从32或64起步用验证集误差做判断如果训练误差降不下来就加大如果验证误差涨了但训练误差还降说明过拟合了要回头减小或者加正则化。第二是序列长度sequence length / time step。这个参数非常关键。它决定了模型每次看多长的一段历史输入窗口太长模型学到的是噪声输入窗口太短历史信息不够。时间序列预测里可以先算一下数据的自相关系数看看滞后几阶还有明显的相关性把窗口长度设置在这个范围附近。第三是层数num_layers。前面提到了一般1到3层。层数越多模型的能力越强但优化也越难。我个人的做法是先跑单层确定baseline效果之后再加第二层看验证集效果是否有提升没提升就退回去。第四是学习率learning rate。LSTM对学习率比较敏感尤其是用Adam优化器时学习率设在1e-3附近通常比较稳如果loss震荡剧烈就降到1e-4或3e-4再跑几轮。第五是dropout率。注意PyTorch里的LSTM只有层数大于1时dropout才生效作用在层与层之间最后接全连接层时通常可以再单独加一个Dropout层。3.3 LSTM适合做什么不适合做什么LSTM最适合的是有顺序依赖、样本之间存在时间或逻辑先后关系的任务典型的有单变量时间序列预测如水文径流预报、气温预测、电力负荷预测、股价模拟。自然语言处理文本分类、情感分析、命名实体识别、机器翻译现在Transformer更强但传统管线里LSTM还是很常见。语音相关任务语音识别的前端特征建模、说话人识别。视频行为识别每一帧抽取空间特征后用LSTM对帧序列进行时序建模。LSTM不适合的任务也很明显如果输入之间没有顺序关系比如单张图片分类、普通表格数据回归没必要强行上LSTM用CNN或多层感知机MLP效果更好、训练更稳。另外超长序列比如几千上万个时间步用原生LSTM训练效率太低多数的做法是先用卷积对序列做下采样再接入LSTM或者直接换Transformer系列模型。4. 用PyTorch实现LSTM做时间序列预测从数据到结果4.1 造一份能复现的序列数据为了演示方便我用一个带噪声的正弦波作为“伪水文径流数据”。实际工程中用真实径流或气温数据时流程完全一样差别只在数据预处理环节需要更小心地处理缺失值和异常点。假设生成了1000个时间步的数据前80%做训练集后20%做验证集。这样设置的原因时间序列不能用普通的随机划分否则会导致未来信息泄漏验证集必须永远是时间上靠后的数据才能模拟真实预测场景。数据归一化这里我用MinMaxScaler把输入压到0到1之间。LSTM默认使用sigmoid、tanh这些饱和激活函数输入范围太大或太小都会让梯度不容易流通。import numpy as np import torch import torch.nn as nn from sklearn.preprocessing import MinMaxScaler # 构造一个频率和幅值都合理可控的合成序列 np.random.seed(42) t np.linspace(0, 100, 1000) data np.sin(t) 0.1 * np.random.randn(1000) # 归一化到 [0, 1] scaler MinMaxScaler() data_scaled scaler.fit_transform(data.reshape(-1, 1)).reshape(-1) # 用滑窗方式构造输入特征和标签 def create_sequences(data, seq_len): xs, ys [], [] for i in range(len(data) - seq_len): xs.append(data[i:i seq_len]) ys.append(data[i seq_len]) return np.array(xs), np.array(ys) seq_len 20 X, y create_sequences(data_scaled, seq_len) # 按时间顺序划分训练集 / 验证集 split int(len(X) * 0.8) X_train, y_train X[:split], y[:split] X_val, y_val X[split:], y[split:] # 转换成 PyTorch Tensor注意 LSTM 输入格式是 (batch, seq_len, input_size) X_train torch.tensor(X_train, dtypetorch.float32).unsqueeze(-1) y_train torch.tensor(y_train, dtypetorch.float32).unsqueeze(-1) X_val torch.tensor(X_val, dtypetorch.float32).unsqueeze(-1) y_val torch.tensor(y_val, dtypetorch.float32).unsqueeze(-1) print(f训练集形状: {X_train.shape}, 标签形状: {y_train.shape})这里有个新手容易搞混的点LSTM的输入维度是(batch_size, seq_len, input_size)而在很多教程里看到的是(seq_len, batch, input_size)那是因为PyTorch默认batch_firstFalse。我在代码里显式设置batch_firstTrue后面模型定义时也要对应省得数据变换绕来绕去。4.2 定义LSTM模型参数如何对应模型结构很直接一个LSTM层提取时序特征再经过一个全连接层把hidden_size维的隐状态映射到1维输出。为什么要加全连接层因为LSTM输出的隐状态是hidden_size维而我们要预测的目标只有1个数值中间必须有一个线性变换做维度压缩。class LSTMPredictor(nn.Module): def __init__(self, input_size1, hidden_size32, num_layers1): super(LSTMPredictor, self).__init__() self.lstm nn.LSTM( input_sizeinput_size, hidden_sizehidden_size, num_layersnum_layers, batch_firstTrue ) self.fc nn.Linear(hidden_size, 1) def forward(self, x): # x: (batch, seq_len, input_size) out, (h_n, c_n) self.lstm(x) # 取最后一个时间步的隐状态做预测 last_hidden out[:, -1, :] # (batch, hidden_size) y_pred self.fc(last_hidden) return y_pred注意取输出时用out[:, -1, :]表示取序列最后一个时间步的隐状态作为整条序列的摘要。这个选择是有讲究的时间序列预测任务里当前时刻的预测只依赖过去而不依赖未来所以只能取最后一个时刻的隐状态如果是序列标注任务比如给每个词打标签那要把所有时间步的输出都保留下来。训练部分我使用MSE损失和Adam优化器并且加了梯度裁剪。梯度裁剪的max_norm1.0是一个常见的保守值。为什么不设太大因为LSTM梯度爆炸虽然比RNN少见但长序列情况下仍然偶发裁剪一下能让训练过程更稳。model LSTMPredictor(input_size1, hidden_size32, num_layers1) criterion nn.MSELoss() optimizer torch.optim.Adam(model.parameters(), lr1e-3) epochs 200 batch_size 64 train_dataset torch.utils.data.TensorDataset(X_train, y_train) train_loader torch.utils.data.DataLoader(train_dataset, batch_sizebatch_size, shuffleTrue) for epoch in range(epochs): model.train() train_loss 0.0 for batch_x, batch_y in train_loader: optimizer.zero_grad() outputs model(batch_x) loss criterion(outputs, batch_y) loss.backward() # 梯度裁剪 torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) optimizer.step() train_loss loss.item() * batch_x.size(0) train_loss / len(train_dataset) if (epoch 1) % 20 0: model.eval() with torch.no_grad(): val_pred model(X_val) val_loss criterion(val_pred, y_val).item() print(fEpoch {epoch1:3d} | Train Loss: {train_loss:.5f} | Val Loss: {val_loss:.5f})4.3 预测结果怎么评估、怎么可视化模型训练完成后把验证集预测结果反归一化回原始尺度再和真实值对比。这里我顺手算了一下RMSE和MAE这两个指标是时间序列预测项目里最常汇报的。model.eval() with torch.no_grad(): y_pred_scaled model(X_val).numpy().reshape(-1, 1) y_true_scaled y_val.numpy().reshape(-1, 1) y_pred scaler.inverse_transform(y_pred_scaled).reshape(-1) y_true scaler.inverse_transform(y_true_scaled).reshape(-1) rmse np.sqrt(np.mean((y_pred - y_true) ** 2)) mae np.mean(np.abs(y_pred - y_true)) print(fRMSE: {rmse:.4f}, MAE: {mae:.4f})如果画出曲线正常情况下预测曲线和真实曲线整体趋势重合但在波峰、波谷这些变化剧烈的区域会有一定滞后。这个滞后现象是LSTM做时间序列预测时最常见也最让人头疼的问题下个小节我细说原因。我用这个结构在真实的水文径流数据上跑过类似实验只要预处理干净、序列长度合适LSTM通常能学到径流涨落的大致规律。但要注意水文数据往往有很强的季节性和非平稳性直接用原始流量序列训练效果通常不理想。行业里常见的做法是先做对数变换或者差分把非平稳序列转成平稳序列再交给LSTM。这一点和正弦波demo差别很大只用正弦波验证流程不代表真实数据也能这么顺利。4.4 训练LSTM的几条实操心得第一个心得是不要在训练集上追求loss降到极低要看验证集的loss。LSTM这类模型参数量不小在几百个样本的小数据集上很容易过拟合。如果训练loss持续下降但验证loss在第60轮就开始反弹那基本就是过拟合早停是最直接的办法。第二个心得是先跑小模型再放大。我每次拿到一个新任务会先设置hidden_size16、num_layers1跑通整个pipeline确认数据管道没bug再逐步加大容量。这样可以省下大量时间避免一上来就挂一个大模型训练半天发现数据预处理错了。第三个心得是状态初始化不能乱设。PyTorch的nn.LSTM默认会初始化隐状态为全零这个初始值适合大多数情况。但如果序列之间是连续的时间片段且你希望模型记住上一个片段的末尾状态那就要手动传递h_0、c_0。在batch_firstTrue模式下没有传入状态时PyTorch自动帮你建全零状态这个默认行为对我们多数单序列预测场景是够用的。第四个心得最重要预测时要用真实的观测值作为输入还是用上一轮的预测值作为输入这是两种截然不同的策略。训练时我们用的是真实历史观测值这叫teacher forcing推理时如果也用真实值那叫多步预测的“滚动预测”每一步都把上一步的预测值拼到输入窗口里。滚动预测的好处是更贴近实际部署场景坏处是误差会随着步数增加而累积越往后预测越偏。我在实际项目中通常会把这两条曲线放在一起对比用真实值输入的曲线往往很好看用预测值滚动输入的曲线才是真实水准。5. 常见问题与排查技巧实录5.1 问题速查表先定位再动手我把实战中碰到最多的问题整理成一张速查表。现象可能原因解决思路loss不下降或下降极慢学习率太小数据未归一化输入特征范围异常观察初始loss调大学习率到1e-2试跑确认归一化到0~1训练初期loss就爆炸学习率太大梯度爆炸数据里有极端异常值减小学习率加梯度裁剪clip_grad_norm_检查并清洗异常点训练loss降但验证loss升过拟合序列长度过长导致学到噪声加dropout减小hidden_size用早停缩小seq_len预测曲线比真实曲线滞后序列窗口不够长模型简单不够表达复杂动态多数是滚动预测误差累积增加seq_len使用seq2seq或注意力机制评估时区分teacher forcing和自回归两种模式预测结果几乎恒定等于训练集均值数据归一化后变化范围太小模型容量不足标签和特征错位检查滑窗代码标签是否取到了正确位置增大hidden_size验证集上误差远大于训练集数据划分不一致用了随机划分产生信息泄漏时间序列切分只能用按时间先后顺序切分的方法5.2 我踩过的几个关键坑第一个坑是数据泄漏。刚开始做时间序列预测时我图省事直接用train_test_split(random_state42)随机划分数据导致部分验证集样本和训练集样本在时间上重叠验证集效果虚高得离谱。后来改成按时间比例切割才得到真实水平。这件事给我留下的教训是时间序列任务里数据划分的严谨性直接决定你判断方向的准确性。第二个坑是忘记反归一化就在原始尺度上比较效果。有一次我训练完直接打印loss0.001感觉模型非常完美后来发现自己一直在0到1的归一化尺度上计算误差换算回原始数据误差其实大得不能看。现在我的代码模板里一定会保留一个反归一化步骤所有指标都在原始数据尺度上算。第三个坑是堆积LSTM层数。我曾经为了提升效果把num_layers调到4层训练时间翻了几倍验证loss反而比2层时更高。原因和梯度路径过长有关并不是层数越多越强。从那以后我给自己立了个规矩超过2层的LSTM必须先跑小规模对比实验有明确的精度收益才值得保留。第四个坑是处理缺失值。水文数据和工业传感器数据经常有断档如果直接拿0填充LSTM会把0当成真实的数值模式。一般做法是先用插值补全再训练。要是缺失比例太高就要考虑删掉整段连续数据避免模型学到错误的连续性。5.3 所有参数都调对了效果还是不行怎么办这是一个非常现实的问题。如果数据干净、窗口合适、模型结构正常但预测效果还是不理想我通常按下面几步排查。第一步看数据的平稳性。非平稳序列比如有趋势或季节性的数据LSTM直接学很容易偏向“记住最近的值”。经验做法是做一阶差分或者对数变换把趋势去掉后再建模预测结果再加回趋势项。我在做径流预报时还会额外加入月、日等周期性编码作为辅助特征效果往往比只用历史值好不少。第二步看问题是不是“不适合用纯序列模型”。有些强非平稳数据比如股价本质上接近随机游走不管用LSTM还是Transformer都难以预测。这不算模型失敗而是任务本身可预测性低。遇到这种情况可以减少对精度的执念把目标改成预测涨跌方向而不是具体数值。第三步考虑换模型。GRU在数据量小的时候往往更快更稳处理超长序列可以考虑TCN时间卷积网络它的感受野比LSTM更可控。再往下是Transformer和Informer系列这类模型做长序列预测有优势但需要更大数据量和更高显存。排查问题的顺序一定是从数据开始再到模型结构最后才是调参。我见过很多新手一上来就焦虑地调学习率、改hidden_size折腾半天发现是数据划分出了问题这点确实值得多提醒一句。最后分享一点个人体会回过头来看LSTM其实没那么玄乎。它给我的感觉更像是一个朴素而有效的工程工具结构清晰、任务适配度高、训练稳定放在今天的大模型时代它仍然是处理序列数据的合格基线。我现在每次接到一个新任务第一版模型通常都是从LSTM和GRU跑起不为别的就为了用最少的成本确认数据里到底有没有可学的模式。等确认了规律再换更重的模型事半功倍。如果你正在入门我建议先别急着背公式而是动手把这个正弦波案例跑通然后把数据换成你自己手头的时间序列把隐藏层维度、序列长度、层数都调一遍亲自观察loss曲线变化。这个过程比看十篇教程都管用。最后再分享一个小技巧训练LSTM时把每个epoch的训练loss和验证loss都打出来哪怕只跑50轮也能让你立刻看出模型是欠拟合、过拟合还是正常收敛这比盯着一个最终准确率要有效得多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价