一、把数据集划分成训练集、验证集和测试集我们通常将数据集划分为训练集、验证集和测试集每个部分都有其特定的作用训练集Training Set模型直接从这个数据集学习特征和模式。用于训练模型即通过训练集的数据来学习模型的参数如权重和偏置。模型在训练集上通过反向传播和优化算法如Adam来最小化损失函数。验证集Validation Set模型不直接从这个数据集学习但影响训练决策。用于在训练过程中评估模型的表现以便进行模型选择和调参。通过验证集我们可以监控模型在未见过的数据上的表现避免过拟合。例如我们可以根据验证集上的表现来决定早停Early Stopping的时机或者调整超参数如学习率、隐藏层维度等。测试集Test Set在整个训练过程中完全不被使用确保评估的客观性。用于最终评估模型的泛化能力。测试集在训练过程中完全不被使用直到训练结束。测试集上的表现反映了模型在真实世界中未知数据上的表现。注意由于在训练时需要前向传播求损失然后再反向传播更新模型参数而在验证时仅仅通过前向传播求验证损失。在下面的任务中我们就是为了设置早停策略通过比较验证损失val_loss来保存最佳为什么要把训练集再分成训练集和验证集如果我们只使用训练集和测试集那么我们在训练过程中无法知道模型是否过拟合了训练集。因为测试集只能用于最终评估我们不能使用测试集来调整模型或选择模型否则会导致测试集被间接地用于训练从而高估模型的泛化能力。只有训练损失与验证损失同时递减才能说明模型泛化能力好如果训练损失在下降而验证损失一直不下降说明如果再继续训练就会出现过拟合了这个时候要提前停止训练并保存最优模型验证损失最低的。使用验证集我们可以在训练过程中定期评估模型根据验证集上的表现来调整超参数并选择最佳的模型。然后我们使用测试集来评估最终模型的泛化能力。训练集验证集测试集 VS 训练集测试集的区别训练集验证集测试集我们将原始数据分成三部分训练集用于训练验证集用于模型选择和调参测试集用于最终评估。这是一种标准的做法可以更好地评估模型的泛化能力。训练集测试集我们只将数据分成两部分训练集用于训练测试集用于评估。这种情况下我们无法在训练过程中使用验证集来监控模型和调参容易导致过拟合并且无法进行模型选择比如早停。二、LSTM实战案例模型逆向攻击日志数据集0.导入相关的库这里面有一个新见的库seaborn 用于绘制混淆矩阵的热力地图。Seaborn 是一个基于 matplotlib 且数据结构与 pandas 统一的统计图制作库。1.加载数据集上面是图片是我的特征向量和标签的json格式的数据集文件由于数据集加载这部分不是本章重点所以不再赘述。假设现在已经通过一个封装好的load_data()方法已经返回了特征向量X和标签y。其中X.shape(2001507)、y.shape(200)2.划分数据集80%训练20%验证, 30%测试一次划分先划分出来训练集和测试集73二次划分再从训练集中划分出验证集82返回X_train、y_trainX_val、y_valX_test、y_test3.特征缩放标准化对X_train、X_val、X_test分别进行标准化处理返回X_train_scaled、X_val_scaled、X_test_scaled。具体过程以X_train为例X_train.shape为11215071.首先先将三维的X_train通过reshape(-1,7)方法转换为2D数组。(-17) 等价于112*15072.使用fit_transform()方法计算均值和标准差并转换数据。fit_transform()方法 先使用fit()方法 再使用transform()方法fit()方法计算均值和标准差以便后续的缩放操作。它不会对数据进行转换只是计算并存储这些参数。transform()方法转换数据。记住1、先在训练集上fit()后在训练集、验证集、测试集上transform()2、3D数据在标准化之前需要先使用reshape(-1,features_size)转成2D数据然后经过标准化处理后再转回来。3.再通过reshape(X_train.shape)方法还原回来特征形状。注意在训练集上使用fit_transform()在测试集和验证集上只使用transform()4.创建 DataLoader批次加载器接下来我们需要将(X_train_scaled, y_train)、(X_val_scaled, y_val)、(X_test_scaled, y_test)分别传入我们自定义的DataSet类分别返回train_dataset、val_dataset、test_dataset再将train_dataset、val_dataset、test_dataset传入torch.utils.data.DataLoader类分别返回train_loader、val_loader、test_loader。为什么要这样做这个知识点详细请跳转到下面这篇博客中的三、小批量梯度下降章节https://blog.csdn.net/m0_59777389/article/details/153636175?spm1011.2124.3001.6209在封装我们的数据集时必须继承实用工具utils中的 DataSet 的类这个过程需要重写__init__、__getitem__、__len__三个方法分别是为了加载数据集、获取数据索引、获取数据总量。5.构建模型重点自定义LSTM分类模型参数:- input_dim: 输入特征的维度- hidden_dim: 隐藏层的维度- output_dim: 输出层的维度通常为1因为这是一个二分类问题- n_layers: LSTM层的数量- dropout: Dropout层的dropout概率模型的__init__()方法主要是用于初始化模型参数的并且搭建整个神经网络模型的。如下图所示这里的神经网络模型结构为-nn.LSTMinput_dim, hidden_dim, n_layers,batch_firstTrue, dropoutdropout) # 定义LSTM层-nn.Dropout(dropout) # 定义Dropout层作用防止过拟合-nn.Linear(hidden_dim, output_dim) # 定义全连接层将hidden_dim映射到output_dim-nn.Sigmoid() # 定义Sigmoid激活函数将输出映射到(0,1)区间在定义LSTM层时我将batch_firstTrue了但是__init__()的参数中我并没有指定batch_first参数这样写对吗另外你再解释一下batch_firstTrue是什么意思有什么作用答1.batch_firstTrue是什么意思在PyTorch中LSTM层的输入默认维度顺序是(seq_len, batch, input_size)即序列长度在前批次大小在后。但是当我们设置batch_firstTrue时输入和输出的维度顺序将变为(batch, seq_len, input_size)即批次大小在前。当batch_firstFalse默认值时输入形状: (seq_len, batch, input_size)输出形状: (seq_len, batch, hidden_size)当batch_firstTrue时输入形状: (batch, seq_len, input_size)输出形状: (batch, seq_len, hidden_size)2.为什么我们要使用batch_firstTrue呢通常我们的数据组织方式是以批次为第一个维度的比如我们有一个张量形状为(batch_size, sequence_length, features)。这样更符合我们的直观理解因为我们在处理数据时通常先按批次处理。例如我们有一个包含32个样本的批次每个样本有10个时间步每个时间步有5个特征那么我们的输入张量形状就是(32, 10, 5)。如果我们使用默认的batch_firstFalse那么我们需要将输入张量转换为(10, 32, 5)才能输入LSTM层这可能会带来一些不便。因此设置batch_firstTrue可以让我们避免在输入LSTM之前调整维度顺序使得代码更加直观。3.在定义LSTM层时我将batch_firstTrue了但是__init__()方法的参数中我并没有指定batch_first参数这样写是正确的本来就应该这样写。不需要在__init__方法参数中添加batch_first因为batch_first是LSTM层的参数不是模型类的参数用于外部传参可以改变的你在创建LSTM层时已经直接设置了它内部固定写死的总之一般使用DataLoader批量加载数据的时候都要在定义nn.LSTM层时设置batch_firstTrue模型的forward()方法用于前向传播过程中具体的操作。在上图中我们可以看到首先需要初始化隐藏状态h0和细胞状态c0这里是用0填充的并将h0和c0放到GPU上h0 和 c0 的形状: (n_layers, batch_size, hidden_dim)然后将x, (h0, c0)输入到LSTM返回lstm_out, (hn, cn)这个过程比较复杂如果感兴趣请去看LSTM的原理这里我们不赘述接着我们需要通过lstm_out[:, -1, :]操作取每个序列的最后一个时间步的输出last_lstm_out-1表示序列的最后一个时间步的下标通过这个操作相当于降维了lstm_out(16,150,64) - last_lstm_out(16,64)。注意取每个序列的最后一个时间步的输出last_lstm_out之后要记得对last_lstm_out使用dropout防止过拟合。最后就是分别通过全连接层、再通过Sigmoid激活函数此时返回形状为 (batch_size, 1)我在这里做了一个squeeze操作移除了最后一个维度形状变为 (batch_size,)。这是因为真实标签是一维数组为了后面在做测试的时候让模型输出的pred_y与真实标签y维度一致。当然这里也可以直接返回通过Sigmoid激活函数之后的输出不做squeeze操作不过后面在做测试的时候需要把真实标签y的形状reshape成二维的列向量就行了测试只取output的最后一个时间步的隐藏状态lstm_out torch.randn(16, 150, 64) # 16个样本每个样本150个时间步每个时间步64个特征 print(lstm_out) print(lstm_out.shape)输出tensor([[[-1.8707, -0.6794, -0.1703, ..., 0.1776, -0.1101, -0.1647],[-0.2021, 0.7888, -0.9574, ..., -0.9473, -0.7599, 1.2282],[-0.0662, -0.3341, 0.7425, ..., -0.6443, -0.1219, 1.1780],...,[ 0.7821, 0.2164, 0.4434, ..., 0.3323, 0.0520, 2.1495],[ 0.3234, 0.4131, -1.0398, ..., 1.1657, 0.0976, 0.4193],[-1.2332, 0.0785, -1.0180, ..., 1.2127, 0.0754, 0.5740]],[[ 0.4871, 0.6760, -0.0452, ..., -0.0956, 0.7223, 0.2410],[ 0.9383, -0.9841, -0.3879, ..., -0.7236, -0.6700, -0.4763],[ 0.9720, -0.3246, -0.2011, ..., -0.1065, 0.2752, 0.7799],...,[ 3.3505, 0.4136, 0.7960, ..., -0.4927, 1.6054, -0.4607],[ 1.4111, -0.0840, 0.4895, ..., 0.8858, -1.4625, 0.1890],[ 0.0132, 0.8347, 0.5775, ..., -1.1897, -0.3169, -0.0996]],[[-1.5659, 0.5573, -0.1124, ..., 0.4236, -0.3952, -0.0877],[-1.6665, -1.3872, -0.1420, ..., 0.1521, 2.0090, 0.6887],[ 0.7853, 1.0151, -0.6576, ..., -0.2066, 1.6503, -0.2559],...,[ 0.2859, 0.1691, -0.1844, ..., -0.5393, 0.4212, -0.6696],[-0.2669, -0.6699, 0.9827, ..., 0.9614, 0.0540, 0.8014],[-1.2266, -0.0785, 2.2120, ..., -0.2371, 1.7394, -1.3353]],...,[[-1.6111, -0.1952, -0.0727, ..., 2.3489, -1.0382, 0.7262],[ 2.2573, 0.9253, 0.2724, ..., 1.1263, 0.5576, -0.5277],[-0.1499, -0.8148, -0.1669, ..., 0.7669, -0.1836, -0.7848],...,[ 1.2975, 0.8881, 0.4835, ..., 0.1819, 1.5072, 1.1313],[ 0.6793, 1.8737, 1.1889, ..., -0.0768, 0.4340, 0.5262],[ 0.7356, 0.7803, 1.3421, ..., 0.1636, -0.6233, 0.4539]],[[-0.0290, -0.1030, 1.1579, ..., -1.8016, -0.1866, 0.0061],[-0.9076, 0.6781, 0.7278, ..., 0.5644, 1.4593, 0.0571],[ 1.0055, -1.2006, 0.9629, ..., -1.0520, -0.8257, 0.8989],...,[-0.6574, 2.4848, 0.0490, ..., 0.7014, -1.0352, -0.4521],[-0.6112, -0.0974, 0.7430, ..., -0.2740, 2.9617, 0.1575],[ 1.3955, 2.2767, -1.4854, ..., 0.7403, 0.9316, 2.0881]],[[ 0.7596, -1.3041, -0.1047, ..., -0.8039, 0.3766, -0.4940],[-1.1452, -0.1129, 1.4303, ..., -1.1768, -0.8231, 0.3138],[ 0.4168, 0.0711, 0.6345, ..., -0.3294, -1.4570, 0.3510],...,[ 0.6431, 0.2386, -1.4828, ..., -0.1227, -0.0887, 0.2915],[ 0.2548, 0.6102, 1.8154, ..., -1.5753, -0.0761, -0.1968],[ 1.1818, -0.3178, 0.5499, ..., -0.2600, 0.5431, -0.8910]]])torch.Size([16, 150, 64])out lstm_out[:, -1, :] # 取每个样本的最后一个时间步的输出形状为 (16, 64) print(out) print(out.shape)输出tensor([[-1.2332, 0.0785, -1.0180, ..., 1.2127, 0.0754, 0.5740],[ 0.0132, 0.8347, 0.5775, ..., -1.1897, -0.3169, -0.0996],[-1.2266, -0.0785, 2.2120, ..., -0.2371, 1.7394, -1.3353],...,[ 0.7356, 0.7803, 1.3421, ..., 0.1636, -0.6233, 0.4539],[ 1.3955, 2.2767, -1.4854, ..., 0.7403, 0.9316, 2.0881],[ 1.1818, -0.3178, 0.5499, ..., -0.2600, 0.5431, -0.8910]])torch.Size([16, 64])dropout nn.Dropout(p0.5) # 50%的概率将元素设置为0每执行一次dropout就有50%的元素会被设置为0 out dropout(out) print(out) print(out.size(0) * out.size(1)) # 总元素数 print((out 0).sum()) # 统计有多少个元素被设置为0tensor([[-2.4665, 0.1571, -0.0000, ..., 0.0000, 0.1507, 0.0000],[ 0.0264, 0.0000, 1.1550, ..., -0.0000, -0.6339, -0.1992],[-0.0000, -0.0000, 0.0000, ..., -0.4742, 3.4789, -2.6707],...,[ 1.4713, 0.0000, 0.0000, ..., 0.0000, -0.0000, 0.0000],[ 2.7911, 4.5535, -2.9709, ..., 0.0000, 0.0000, 4.1762],[ 0.0000, -0.6357, 0.0000, ..., -0.5200, 1.0861, -0.0000]])1024tensor(522)# 对上一步操作返回的out进行全连接层映射 【注意】操作可叠加 fc nn.Linear(64, 1) # 64个特征映射到1个输出(全连接层,输出层) out fc(out) out输出tensor([[ 1.0132],[ 1.3960],[ 0.9772],[ 1.3545],[-0.0395],[ 0.0946],[-1.0372],[-0.3325],[ 0.1005],[ 0.7154],[-1.1141],[-0.5344],[ 0.2924],[ 1.8825],[-2.3383],[-0.2945]], grad_fnAddmmBackward0)# 对全连接层的输出应用Sigmoid激活函数 【注意】操作可叠加 sigmoid nn.Sigmoid() out sigmoid(out) # 将输出映射到(0,1)区间 print(out) print(out.shape)tensor([[0.7336],[0.8016],[0.7265],[0.7949],[0.4901],[0.5236],[0.2617],[0.4176],[0.5251],[0.6716],[0.2471],[0.3695],[0.5726],[0.8679],[0.0880],[0.4269]], grad_fnSigmoidBackward0)torch.Size([16, 1])# squeeze移除最后一个维度形状为 (batch_size,) 【注意】操作可叠加 out out.squeeze(1) print(out) print(out.shape)tensor([0.7336, 0.8016, 0.7265, 0.7949, 0.4901, 0.5236, 0.2617, 0.4176, 0.5251, 0.6716, 0.2471, 0.3695, 0.5726, 0.8679, 0.0880, 0.4269], grad_fnSqueezeBackward1) torch.Size([16])想要深入了解LSTM模型构建的入门案例请跳转https://blog.csdn.net/m0_59777389/article/details/149350040?spm1011.2124.3001.62096.模型训练验证同一个epoch下训练模型前需要提前初始化模型、并指定损失函数和优化器。并将模型移动到GPU上。LSTMClassifier分类器结构如下图由上图可以看出1.训练与验证是在同一个epoch中进行的顺序是先训练后验证。但是训练和验证又分别在各自的DataLoader中分批进行的。2.训练过程需要进行前向传播求损失又要反向传播更新梯度但是验证过程仅仅只需要前向传播求损失。3.以往的案例中我都是直接losses.append(loss.item()) 将损失添加到列表中为了后面打印损失变化曲线。在这里为啥要做累加每个批次的损失的操作train_loss loss.item() * inputs.size(0) # 累加每个批次的损失乘以样本数这行代码没看懂为什么要累加每个批次的损失乘以样本数这样做的目的是啥答在PyTorch中nn.BCELoss()默认返回的是批次内所有样本的平均损失而我们通常计算一个epoch的平均损失时是用该epoch的总损失除以样本总数。但是由于我们每个批次的样本数可能不同最后一个批次可能小于batch_size所以我们需要记录每个批次的损失乘以该批次的样本数这样累加后得到的就是整个epoch的总损失。然后在计算平均损失时我们用总损失除以总样本数即len(train_loader.dataset)。train_loss loss.item() * inputs.size(0) # 累加每个批次的损失乘以样本数loss.item(): 获取当前批次的平均损失值inputs.size(0): 获取当前批次的样本数量loss.item() * inputs.size(0): 将平均损失转换为批次总损失这样操作是为了正确处理不同批次大小特别是最后一个批次可能不满准确计算整个数据集的平均损失确保损失统计的数学正确性这是一种标准的PyTorch训练模式确保损失计算的准确性7.测试模型1.之前的案例中我们在测试过程中将概率转换为类别 (0或1)的时候都是将二维数组先进行布尔判断然后再赋值的方式将模型的输出结果转换成0或1的如下但是在这里有一个更加巧妙的方法直接通过float()方法可以将为True的位置直接转成1为False的位置转成02.all_preds.extend(preds.cpu().numpy()) # 转换为 numpy 数组并添加到列表中这行代码我没看懂请一步一步的拆解。答我们一步步拆解这行代码all_preds.extend(preds.cpu().numpy())preds这是一个PyTorch张量包含了一个批次的预测结果0或1。cpu()将张量从当前设备可能是GPU移动到CPU。如果张量已经在CPU上这个操作不会改变什么但为了安全起见我们通常这样做以确保张量在CPU上因为numpy数组只能在CPU上处理。numpy()将PyTorch张量转换为NumPy数组。preds.cpu().numpy()的结果是一个NumPy数组其形状为(batch_size,)因为每个样本有一个预测值。all_preds是一个Python列表用于收集所有批次的预测结果。.extend()这是Python列表的一个方法用于将可迭代对象如数组中的每个元素添加到列表的末尾。所以这行代码的意思是将当前批次的预测结果一个NumPy数组中的每个元素依次添加到all_preds列表中。举例说明假设一个批次有3个样本preds.cpu().numpy()得到数组[0, 1, 0]如果all_preds原来是[1, 0]那么执行后all_preds变为[1, 0, 0, 1, 0]extend()与append()的区别append()方法是将整个对象作为单个元素添加到列表末尾。如果使用append那么上述例子就会变成all_preds.append([0,1,0]) - [1, 0, [0, 1, 0]]这显然不是我们想要的。因此当我们想要将一个可迭代对象中的每个元素单独添加到列表中时使用extend当我们想要将整个对象作为一个元素添加时使用append。在机器学习中我们通常使用extend来收集所有批次的预测结果以便最后形成一个一维的列表包含所有样本的预测。最后就是打印评价指标了附录1. 训练集shuffleTrue验证/测试集shuffleFalse在深度学习的实践中这种设置训练集shuffleTrue验证/测试集shuffleFalse已经成为了标准规范。这背后的原因涉及模型泛化能力、训练效率以及评估的严谨性。以下是详细的深度解析1. 为什么训练集Train需要打乱核心目的防止模型学到“虚假相关性”或“记忆顺序”。打破数据间的相关性在原始数据集中样本往往是按某种顺序排列的例如按类别、采集时间或特定传感器 ID。如果模型按固定顺序学习它可能会捕获到序列中的统计偏差而不是特征本身的规律。平滑梯度方向深度学习使用随机梯度下降SGD。如果每个 Batch 的数据构成非常单一比如一个 Batch 全是 A 类下一个全是 B 类会导致梯度在更新时剧烈波动。打乱数据能确保每个 Batch 的数据分布更接近整体分布使训练过程更平稳、收敛更快。防止过拟合固定的顺序会让模型产生某种“记忆”它可能学会“第 10 个样本后通常会出现类别 X”这种无意义的规律。2. 为什么验证/测试集Val/Test通常不打乱核心目的保证评估的一致性、可重复性以及效率。结果的可复现性验证集的作用是作为一个“标尺”来衡量模型性能。如果每次验证时数据顺序都变虽然理论上最终指标如平均准确率不变但在调试模型或对比不同版本时这种不必要的随机性会增加分析难度。不需要梯度的更新验证和测试阶段只是进行前向传播Forward Pass。由于不涉及权重更新样本之间的顺序完全不会影响模型当前的预测性能。便于排查错误如果不打乱你可以很容易地将模型的预测结果与原始标签一一对应。比如你发现模型在第 500 到 600 个样本上表现极差你可以直接定位到原始数据中这部分样本分析其特征。效率考量内存读取和预取Prefetching在顺序读取时通常比随机读取效率更高虽然在 GPU 训练中这点差异微乎其微但也是一种良好的工程实践。3. 如果将验证/测试集也打乱是错误的做法吗简单来说不违法但没必要且可能带来不便。数学结果上只要你完整地跑完一遍验证集无论顺序如何得到的Loss 和 Accuracy 是完全一致的。因此从纯粹的数学评估角度看打乱并不是“错误”的。工程逻辑上计算浪费shuffleTrue会在每个 Epoch 开始时消耗额外的 CPU 算力来重新生成随机索引序列。无法通过索引追踪如果你在训练日志中想查看某个特定 Bad Case坏样本打乱顺序会让你很难通过 Batch 序号快速找到对应的原始数据。总结与对比数据集类型是否 Shuffle主要原因训练集 (Train)必须 (True)增强泛化能力平滑梯度防止模型学习样本顺序。验证集 (Val)不建议 (False)确保评估结果可复现方便对比不同 Epoch 的模型表现。测试集 (Test)不建议 (False)方便进行最终结果分析、生成预测报告以及进行样本对照。进阶建议如果你需要对模型进行非常严格的性能分析例如计算每个样本的置信度或者将模型预测结果导出为 CSV 进行后续分析请务必保持测试集不打乱这样预测结果的索引才能与你原始数据的 CSV/数据库索引完美对应。2. 3维特征数据如何进行标准化# --- B. 特征缩放 (与原脚本相同) --- scaler StandardScaler() X_train_reshaped X_train.reshape(-1, num_features) # 先将训练集转换为2D数组进行缩放 num_features每个时间步的特征数量 scaler.fit(X_train_reshaped) # 用于计算数据集的均值和标准差等统计量以便后续的缩放操作。它不会对数据进行转换只是计算并存储这些参数。 # scaler.transform使用fit方法计算得到的均值和标准差来对数据进行标准化即缩放 X_train_scaled scaler.transform(X_train_reshaped).reshape(X_train.shape) # fit() transform() 与 fit_transform() 在结果上是等效的 # 在训练集上使用fit_transform()在测试集和验证集上使用transform() X_val_scaled scaler.transform(X_val.reshape(-1, num_features)).reshape(X_val.shape) X_test_scaled scaler.transform(X_test.reshape(-1, num_features)).reshape(X_test.shape) print(特征缩放完成。) print(- * 50)记住1、先在训练集上fit()后在训练集、验证集、测试集上transform()2、3D数据在标准化之前需要先使用reshape(-1,features_size)转成2D数据然后经过标准化处理后再转回来。总结得非常到位你提到的这两点是深度学习数据预处理中极其关键的“工程细节”。为了帮你加深理解并防止踩坑我针对这两点补充一些深度解析和为什么必须这样做的原因1. 为什么必须先在训练集上fit再对所有集transform核心原则严禁“数据泄露 (Data Leakage)”。保持一致的“标尺”StandardScaler的本质是计算均值 $\mu$ 和标准差 $\sigma$。如果验证集或测试集参与了fit就相当于模型在训练之前通过预处理过程“偷看”了未来数据的分布信息。模拟真实场景在实际部署模型时新产生的数据推理数据是单条或小批量的你无法计算新数据的均值和标准差。因此模型必须习惯使用“训练时学到的分布”来处理所有未来的输入。结果对比如果每个集都独立fit那么同一个数值比如“100”在不同集合中标准化后的结果可能不同因为均值变了这会导致模型逻辑混乱。2. 为什么 3D 数据必须reshape到 2D 才能标准化核心原因StandardScaler的设计逻辑是基于“特征列”的。API 限制Scikit-learn 的StandardScaler期望接收的输入是。对于 LSTM 常用的 3D 形状它无法直接识别哪一维是特征。跨时间步的一致性* 通过reshape(-1, num_features)你实际上是将所有样本的所有时间步都堆叠在了一起。这样做能确保模型学习到的是该特征在整个时间维度上的全局统计规律。例子如果你在预测气温你希望“气温”这个特征在所有时间步第1小时到第24小时都使用同一个均值和标准差进行缩放而不是每个小时都有一套不同的缩放标准。 补充一个代码层面的小技巧在 PyTorch 的 pipeline 中这种“转 2D - 缩放 - 转回 3D”的操作非常常见。为了确保代码健壮你可以这样写# 记录原始形状方便后续 reshape 还原 original_shape X_train.shape # (N, L, C) # 转换、缩放、还原 X_train_scaled scaler.fit_transform(X_train.reshape(-1, num_features)).reshape(original_shape)延伸思考如果你的数据中存在异常值OutliersStandardScaler可能会受到很大影响因为它对均值和方差敏感。在这种情况下你是否考虑过使用RobustScaler基于分位数来替代它3. LSTM模型的参数、input和output针对nn.LSTM的参数、输入Input和输出Output我为你梳理了一份清晰的指南。理解这些维度的关键在于区分模型初始化参数决定网络结构与转发数据形状决定数据流动。1. 模型初始化参数 (Initialization Parameters)当你定义model nn.LSTM(...)时主要设置以下参数参数名含义说明input_size输入特征维度每个时间步输入的向量长度如词向量维度。hidden_size隐状态维度隐藏层神经元数量决定了模型的“记忆容量”。num_layers层数堆叠 LSTM 的层数默认 1。batch_firstBatch 优先若为True输入形状为默认为False。bidirectional双向若为True则为双向 LSTM输出维度会翻倍。dropout丢弃率除最后一层外层与层之间的 Dropout 概率。2. 输入数据 (Input)LSTM 的forward函数通常接收两个输入数据 (input)和初始状态 (h0, c0)。input:如果batch_firstFalse(默认): 形状为如果batch_firstTrue: 形状为其中 序列长度 Batch 大小input_sizeh_0(Hidden State): 形状为。c_0(Cell State): 形状与h_0一致。其中(双向) 或(单向)hidden_size注如果不提供默认为全 0。3. 输出数据 (Output)LSTM 会返回两个部分所有时间步的输出和最后一个时间步的状态。output: 包含序列中每个时间步的隐状态。形状若batch_firstTrue则在首位。通常用于后续的序列标注或连接全连接层。(h_n, c_n):h_n: 最后一个时间步的隐状态。形状为。c_n: 最后一个时间步的细胞状态。形状同上。如果你只需要整个序列的“语义向量”通常取h_n的最后一层。4. 关键维度关系总结 (Cheat Sheet)假设你有一个单向、单层的 LSTM且batch_firstTrue特别注意如果开启了bidirectionalTrueoutput的最后一个维度大小将变为。请真正搞懂LSTM模型参数请见https://blog.csdn.net/m0_59777389/article/details/154486864?spm1011.2415.3001.53314. LSTM模型训练完之后需要我们知道的几个模型属性对于nn.LSTM()模型而言模型训练完之后模型会把学到的知识存哪几个变量里有哪些重要的模型属性需要我们知道在nn.LSTM训练完成后它所学到的“知识”即网络权重主要存储在模型的参数变量中。你可以通过state_dict()方便地查看它们。以下是知识存储的载体和你需要掌握的关键属性1. 知识存放在哪些变量里LSTM 的知识本质上是四种门控遗忘门、输入门、细胞门、输出门的线性变换权重。它们被合并存储在以下变量中权重 (Weights)weight_ih_l[k]: 第层输入到隐藏层Input-to-Hidden的权重。形状。它包含了四个门的权重矩阵拼接在一起。weight_hh_l[k]: 第层隐藏层到隐藏层Hidden-to-Hidden的权重。形状。这是 LSTM “记忆”上一个时刻信息的关键。偏置 (Biases)bias_ih_l[k]和bias_hh_l[k]: 对应输入和隐藏层的偏置项。形状。注意PyTorch 为了计算效率将四个门的权重拼接在一个大矩阵里。如果你有 128 个隐藏单元weight_ih的形状通常是(512, input_size)即。2. 必须掌握的重要属性除了权重变量在定义或调用模型时这几个属性决定了模型的结构和数据流向结构属性input_size: 输入特征的维度。hidden_size: 隐藏状态的维度。这决定了模型“记忆力”的容量。num_layers: LSTM 的层数堆叠深度。默认为 1。batch_first: 极其重要。如果为True输入形状为(batch, seq, feature)。如果为False默认形状为(seq, batch, feature)。bidirectional: 布尔值。如果为True模型会同时学习正向和反向的信息输出维度会翻倍2 * hidden_size。dropout: 层间失活概率。注意它只在num_layers 1时对中间层有效单层 LSTM 无效。运行属性training: 布尔值。当执行model.train()时为True执行model.eval()时为False。这会影响dropout是否生效。3. 运行时的关键状态非模型属性虽然这些不是存储在模型里的固定知识但理解它们对推理至关重要h_n(Hidden State): 最后一时刻的隐藏状态通常被当作整个序列的“特征向量”。c_n(Cell State): 最后一时刻的细胞状态携带了长期记忆信息。4. 如何访问这些“知识”如果你想获取特定的参数进行可视化例如分析权重分布可以使用以下方法# 1. 查看所有参数名称和形状 for name, param in lstm.named_parameters(): print(f参数名: {name}, 形状: {param.shape}) # 2. 只获取第一层的输入权重 w_ih lstm.weight_ih_l0.data5. 易混淆概念知识 vs. 状态这是一个非常关键的区别很多初学者会混淆知识 (Parameters): 即上面提到的weight和bias。它们在训练结束后是固定的存在模型文件中.pth或.pt。状态 (States): 即h_n(Hidden State) 和c_n(Cell State)。它们是模型在处理特定输入序列时产生的临时记忆。当你处理完一个 Batch这些状态通常会被清空或重置。它们不属于模型训练学到的永久知识而是模型对当前输入的“瞬时反应”。 实用建议如果你发现模型在训练集上表现很好但在测试集上很差可以检查weight_hh_l[k]的梯度或数值分布。如果数值爆炸或全为 0通常意味着长序列训练出现了梯度消失或梯度爆炸问题。