☰
LSTM日志异常检测全流程实战:从HDFS数据预处理到模型调优
2026/10/1 7:29:57 网站建设 项目流程

简介:Python基于LSTM的日志异常检测系统是一份面向计算机专业课程设计与期末大作业的完整项目资源,包含可直接运行的源码和配套日志数据集,覆盖日志解析、特征提取、LSTM模型训练与异常检测全流程。资源包共115个文件,压缩包大小约82MB,主要包含Python源码、npy/csv/pkl数据文件、log原始日志,以及相关pdf和caj参考文献;HDFS日志数据与异常标签便于模型验证,说明文档辅助快速上手。已有232人学习下载,适合作为日志异常检测、智能运维方向课程设计的参考实现,也可用于毕业论文前期实验。通过研究源码与数据组织方式,可掌握时间序列特征构造、LSTM分类器训练和检测效果评估方法,为独立完成同类项目提供可复用的思路与代码基础。自带日志数据集可直接用于模型训练与测试。

1. 基于LSTM的日志异常检测系统:从期末大作业到可复现的检测方法

先把结论放在前面:这份资源解决的是「海量日志里怎么自动找出异常行为」这个经典问题,而它的做法是用LSTM对日志序列建模,把日志模板按时间窗口切分成序列,再训练一个二分类模型判断每条窗口是否异常。整套东西以HDFS公开日志数据集为实验对象,附带完整的CSV数据文件和标签,对正在做课程设计、期末大作业,或者想快速上手LSTM异常检测实战的人来说,读完这篇你就能知道项目结构、数据长什么样、模型怎么跑通,以及哪些地方最容易翻车。

我拆项目的习惯是先看数据再看代码,因为日志异常检测的坑往往不在模型而在数据预处理。这份资源里恰好把原始日志、结构化日志、标签文件都分好了,训练和验证可以直接在公开数据集上完成,不用自己满世界找日志数据。适合作业场景的原因也在这:数据是标准的,标签是人为标好的,你只需要把预处理、模型、评估这条链路跑通,就能在报告里写清楚每个环节。

下面按「资源里有什么 → 数据怎么理解 → 代码怎么跑 → 参数怎么调 → 坑在哪 → 还能怎么改进」的顺序拆,确保你拿到压缩包后不迷茫。

2. 资源结构与数据认知:先搞懂HDFS日志和异常标签,再碰LSTM

2.1 压缩包里都有什么:五个CSV和三篇参考文献的用途

打开压缩包,主要分两大部分:论文文献和数据集文件。先看文献,这部分是你写期末大作业「国内外研究现状」和「参考文献」章节的直接素材。压缩包里几篇.caj文件主题覆盖了网络流量分类、日志故障检测、日志记录语句级别推荐、服务器故障分析、网络异常行为检测、主机日志入侵检测,这些内容基本就是日志异常检测方向的常见切入点。写报告时可以直接引用这些论文的观点作为背景支撑,比如关于日志异常检测的主流方法演进、基于机器学习与深度学习的对比,但注意别去抄原文,只提炼论点就好。

再看数据文件,核心是三个CSV。第一个是HDFS_100k.log_structured.csv,这是HDFS日志经过日志解析后的结构化结果,每一行代表一条日志记录,包含时间戳、进程ID、日志级别、事件模板等字段。第二个anomaly_label.csv是异常标签文件,记录哪些日志块被标记为异常。第三个data_instances.csv是数据实例,通常是按日志块组织好的序列样本,里边有块ID、模板序列等字段,直接喂给模型训练用的就是它。

这里要明白HDFS日志数据集的构造逻辑:系统把一段时间的日志按block ID分组,同一个block的所有日志属于同一个执行流,如果某个block的日志流中出现了异常事件,这个block就被整体标记为异常。所以这个任务不是对单条日志做判断,而是对「一组日志序列」做二分类,这正是LSTM擅长的地方——它擅长捕捉时间序列中的前后依赖关系。

2.2 特征工程思路:从原始日志到模板序列再到大模型输入

日志异常检测的第一步永远是「解析日志」,把非结构化的日志文本变成结构化的模板。HDFS日志本身有一定格式,比如包含了日期、时间、PID、级别和具体消息内容。常见做法是用日志解析工具(比如Drain或SLCT)自动提取模板,把每个日志事件映射成一个模板ID。处理后的结果就是你看到的log_structured.csv,里面每条记录都已经带上了事件模板。

接下来要把日志按block ID聚合,形成序列数据。这一步的逻辑是:同一个block的日志记录按时间先后排列,得到一个模板ID序列,例如[8, 12, 34, 8, 12, 56, ...]。然后把这些序列按窗口大小切分,每个窗口对应一个训练样本。HDFS数据集的标签尽管是block级别的,但窗口级别也可以继承block的标签。data_instances.csv中保存的就是这种序列化之后的样本。

把序列转成LSTM可用的输入,需要做两个关键转换:一是把模板ID映射成稠密向量,通常用Embedding层完成,类似NLP中把词转成词向量的思路;二是把变长序列对齐成固定长度,比如设定窗口大小为10,不足部分补零或截断。这里注意,模板ID是离散的,不能直接作为数值特征输入LSTM,Embedding是必须的。

数据认知这块卡住了后面全都白搭。我见过不少同学直接拿模板ID当整数特征丢进模型训练,结果Loss一直不降,原因就是模型把这个离散值当成了有大小关系的连续值。所以预处理环节宁可慢一点,也要把每个字段的含义和ID映射关系的代码写清楚。

3. 环境搭建与数据预处理实战:从安装依赖到生成训练样本

3.1 环境配置与依赖清单:Python版本、PyTorch、sklearn缺一不可

这个项目用Python实现,依赖的核心库是PyTorch(LSTM模型搭建)、scikit-learn(评估指标计算)、pandas(CSV读取)、numpy(数组运算)和matplotlib(可视化)。Python版本建议3.8或3.9,太高或太低都可能遇到库兼容问题。创建虚拟环境后再安装依赖,避免把系统Python环境弄乱。

安装依赖最稳的方式是用requirements.txt,内容大致如下:

pip install torch==1.13.1 pip install pandas==1.5.3 pip install numpy==1.24.3 pip install scikit-learn==1.2.2 pip install matplotlib==3.7.1

版本号是我个人验证过的组合,PyTorch 1.13左右的版本对LSTM的支持很成熟,而且不会像2.x那样引入较多API改动。如果你用的是PyTorch 2.x,上面代码里的模型定义部分可能需要微调,尤其是torch.nn.LSTM相关的参数接口基本没变,但训练循环里的某些细节需要注意。装完依赖后跑一句python -c "import torch; print(torch.__version__)"确认安装成功,再做数据预处理。

这里有一个很容易被忽略的点:PyTorch的CPU版本和GPU版本安装命令不同。期末作业场景下CPU版本完全够用,HDFS 100k这个规模的数据在CPU上训练并不会等太久。如果电脑有NVIDIA显卡并且想用GPU加速,需要从PyTorch官网选择对应CUDA版本的安装命令,不要直接用默认的pip install torch,那会装成CPU版。

3.2 日志数据加载与标签合并:pandas操作的核心步骤

拿到三个CSV后,第一步是把数据加载进来并合并。核心逻辑是根据block ID把结构化日志和异常标签对齐。下面代码展示了加载与合并的过程:

import pandas as pd # 加载结构化日志数据 structured_df = pd.read_csv('HDFS_100k.log_structured.csv') # 加载异常标签 label_df = pd.read_csv('anomaly_label.csv') # 加载数据实例(序列样本) instances_df = pd.read_csv('data_instances.csv') print(structured_df.columns.tolist()) print(label_df.columns.tolist()) print(instances_df.columns.tolist()) # 合并标签到结构化日志:以BlockId为关联键 structured_df = structured_df.merge( label_df, on='BlockId', how='left', validate='many_to_one' ) print(structured_df['Label'].value_counts())

逻辑说明:merge操作的目的是把block级别的异常标签关联到每一条日志记录上,这样每个日志事件都知道了自己所属的block是不是异常的。validate='many_to_one'参数用于做校验,保证左表多行对应右表一行时不会因为重复键产生意外笛卡尔积。合并后检查Label的分布,正常情况下异常样本占比远低于正常样本,这是典型的类别不平衡场景,后面评估时不能只看准确率。

参数说明:

  • on='BlockId':指定关联键,两个DataFrame必须都有这一列。
  • how='left':保留左表全部记录,没有匹配标签的填NaN,HDFS数据集里标签是完整的,所以基本不会出现NaN。
  • validate='many_to_one':校验右表关联键唯一性,防止合并后行数膨胀。

3.3 构建模板序列与窗口切分:核心预处理代码

下一步是把日志记录按BlockId聚合成模板序列,然后切窗口。这一步决定了模型看到的每个样本长什么样,是预处理中最关键的一段逻辑:

import numpy as np from collections import defaultdict # 将每条日志的EventTemplate映射为模板ID template_list = structured_df['EventTemplate'].unique() template2id = {tpl: idx for idx, tpl in enumerate(template_list)} # 按BlockId聚合,形成序列 block_sequences = defaultdict(list) for _, row in structured_df.iterrows(): block_sequences[row['BlockId']].append(template2id[row['EventTemplate']]) # 按时间排序并保留原始顺序,这里假定CSV中记录已按时间排列 # 参数设置 window_size = 10 stride = 1 def sliding_window(seq, window_size=10, stride=1): windows = [] for i in range(0, max(1, len(seq) - window_size + 1), stride): w = seq[i:i + window_size] if len(w) < window_size: # 不足窗口长度的序列,用0填充(0预留为padding索引) w = w + [0] * (window_size - len(w)) windows.append(w) return windows # 为每个block生成窗口样本及标签 X, y = [], [] block_labels = dict(zip(label_df['BlockId'], label_df['Label'])) for bid, seq in block_sequences.items(): label = block_labels.get(bid, 0) for w in sliding_window(seq, window_size, stride): X.append(w) y.append(label) X = np.array(X, dtype=np.int64) y = np.array(y, dtype=np.int64) print(f'样本数量: {len(X)}, 窗口长度: {X.shape[1]}') print(f'异常样本占比: {y.mean():.4f}')

逻辑说明:先把所有日志事件模板映射成从0开始的整数ID,然后用defaultdict按BlockId聚合出每个block的模板序列。滑窗切分时,用stride控制相邻窗口的重叠程度,stride=1表示每次往前滑动一条日志,样本重叠度最高,数据量最大;stride=window_size则表示完全不重叠,样本会少很多。padding索引0单独预留,是因为我们后面会构建Embedding矩阵时让索引0对应的向量为全零,这样padding位置不会给LSTM传递有效信息。

参数说明:

  • window_size:控制LSTM记忆的时间长度,HDFS数据集上10~20之间效果都不错,太小抓不到异常事件前后的依赖,太大则引入大量无关噪声。
  • stride:控制样本生成密度,期末作业数据量不大,直接设为1多生成样本即可。
  • template2id:必须保持映射稳定,训练和测试阶段用同一份映射,不能重新生成,否则ID语义就变了。

预处理完成后,把数据划分成训练集、验证集和测试集。划分时有几个讲究:如果按block的边界划分,保证同一个block的所有窗口不会被同时分到训练集和测试集,避免信息泄漏;如果按窗口随机划分,训练和测试会共享同一block的上下文,模型评估的指标会偏乐观。我一般会用train_test_split按BlockId做分组划分,保证验证集和测试集的独立性强一些:

from sklearn.model_selection import train_test_split block_ids = list(block_sequences.keys()) train_blocks, test_blocks = train_test_split( block_ids, test_size=0.3, random_state=42, stratify=None )

这样做唯一的代价是数据量不均衡时某些类别在测试集中数量较少,但对异常检测任务来说,牺牲一点测试样本量换评估可信度是值得的。

4. LSTM模型设计与训练调参:Embedding、双向LSTM与类别不平衡处理

4.1 模型结构:为什么用Embedding加LSTM加全连接

日志模板序列本质上是离散符号序列,和NLP里的句子非常相似,所以模型结构也参考了文本分类的经典做法。Embedding层负责把每个模板ID转换为稠密向量,LSTM层负责捕捉序列的时序依赖,最后接全连接层输出二分类概率。

选LSTM而不是RNN或GRU的原因在于:LSTM通过门控机制缓解了长序列中的梯度消失问题,能记住更早之前的日志事件信息。日志异常检测场景下,异常往往不是单个事件就能确定的,而是前后多个事件组合才暴露异常特征,所以LSTM的记忆能力正好匹配这个需求。 模型定义代码如下:

import torch import torch.nn as nn class LogAnomalyLSTM(nn.Module): def __init__(self, vocab_size, embedding_dim=64, hidden_dim=128, num_layers=2, num_classes=2, dropout=0.3): super(LogAnomalyLSTM, self).__init__() # padding_idx=0,保证padding位置的embedding向量全零,不参与更新 self.embedding = nn.Embedding(vocab_size, embedding_dim, padding_idx=0) self.lstm = nn.LSTM( input_size=embedding_dim, hidden_size=hidden_dim, num_layers=num_layers, batch_first=True, dropout=dropout if num_layers > 1 else 0.0, bidirectional=True ) # 双向LSTM的hidden要乘以2 self.classifier = nn.Sequential( nn.Linear(hidden_dim * 2, 64), nn.ReLU(), nn.Dropout(dropout), nn.Linear(64, num_classes) ) def forward(self, x): embeds = self.embedding(x) # [batch, seq_len, embedding_dim] lstm_out, _ = self.lstm(embeds) # [batch, seq_len, hidden_dim*2] # 取序列最后一个有效位置(非padding位置)的输出 # 在实际训练中,简单做法是对所有时间步取平均或在最后一个时间步输出 # 这里采用最后一个时间步,注意padding可能影响该位置 last_out = lstm_out[:, -1, :] # [batch, hidden_dim*2] logits = self.classifier(last_out) return logits

逻辑说明:forward中把输入序列送入Embedding层之后,LSTM返回每个时间步的输出,这里取最后一个时间步的隐状态作为整条序列的语义表示。不过这种做法有个隐患:如果最后几个位置恰好是padding,那么取到的信息是padding对应的输出,不是真实日志的信息。更稳妥的做法是先计算序列长度,然后用torch.gather按真实长度索引取出对应位置的隐状态。

参数说明:

  • embedding_dim:模板向量的维度,64在中小规模数据集上够用,模板数量多时可以调到128。
  • hidden_dim:LSTM隐层维度,128是常见起步值,增大到256能提高拟合能力,但训练时间会变长。
  • num_layers:层数,两层比单层效果好一些,但超过三层在日志数据上收益很小,反而更容易过拟合。
  • bidirectional=True:双向LSTM能同时看到序列前向和后向的信息,日志序列中异常事件前后的上下文都重要,所以双向是更合适的选择。
  • dropout=0.3:常用的正则化手段,防止小数据集上过拟合。

4.2 训练脚本:损失函数、优化器与类别权重

异常检测任务中异常样本占比通常很低,直接训练会导致模型把所有样本都预测成正常类,准确率看起来很高但Recall几乎为0。解决办法是在损失函数中为少数类分配更高权重,PyTorch的CrossEntropyLoss自带这个参数:

from sklearn.utils.class_weight import compute_class_weight from torch.utils.data import DataLoader, TensorDataset # 计算类别权重 classes = np.array([0, 1]) class_weights = compute_class_weight( class_weight='balanced', classes=classes, y=y_train ) class_weights = torch.tensor(class_weights, dtype=torch.float32).to(device) criterion = nn.CrossEntropyLoss(weight=class_weights) # 构建DataLoader train_dataset = TensorDataset( torch.tensor(X_train, dtype=torch.long), torch.tensor(y_train, dtype=torch.long) ) train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True) # 优化器与学习率 optimizer = torch.optim.Adam(model.parameters(), lr=1e-3, weight_decay=1e-5) scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=5, gamma=0.5)

逻辑说明:compute_class_weight根据标签频率自动计算每个类别的权重,异常类别权重高、正常类别权重低,相当于在Loss计算时放大异常类别的误差贡献。Adam优化器加weight_decay正则项能抑制过拟合,配合StepLR每5个epoch把学习率减半,可以在训练后期更精细地逼近最优解。

参数说明:

  • weight_decay=1e-5:L2正则强度,设置太大模型欠拟合,太小正则效果不明显。
  • lr=1e-3:初始学习率,LSTM训练时学习率过大会导致Loss震荡,过小收敛太慢。
  • step_size=5:5个epoch衰减一次学习率,配合gamma=0.5,到第15个epoch时学习率已经是初始值的四分之一了。
  • batch_size=64:显存或内存够的话可以用128,小批量在训练后期更容易跳出局部极小值。

4.3 训练循环:早停机制与模型保存

训练过程要关注训练Loss和验证指标的双重变化,不能只看训练Loss下降就觉得万事大吉。我习惯在每个epoch结束时计算验证集的F1分数,并用F1分数决定是否保存当前模型:

best_f1 = 0.0 patience = 5 wait = 0 for epoch in range(30): model.train() total_loss = 0.0 for xb, yb in train_loader: xb, yb = xb.to(device), yb.to(device) optimizer.zero_grad() logits = model(xb) loss = criterion(logits, yb) loss.backward() # 梯度裁剪,防止LSTM训练中梯度爆炸 torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) optimizer.step() total_loss += loss.item() # 验证 model.eval() val_preds, val_labels = [], [] with torch.no_grad(): val_loader = DataLoader( TensorDataset( torch.tensor(X_val, dtype=torch.long), torch.tensor(y_val, dtype=torch.long) ), batch_size=64, shuffle=False ) for xb, yb in val_loader: xb = xb.to(device) logits = model(xb) preds = torch.argmax(logits, dim=1).cpu().numpy() val_preds.extend(preds) val_labels.extend(yb.numpy()) from sklearn.metrics import f1_score, precision_score, recall_score val_f1 = f1_score(val_labels, val_preds, average='binary') val_prec = precision_score(val_labels, val_preds, average='binary') val_rec = recall_score(val_labels, val_preds, average='binary') print(f'Epoch {epoch+1:02d} | Loss {total_loss/len(train_loader):.4f} | ' f'Precision {val_prec:.4f} | Recall {val_rec:.4f} | F1 {val_f1:.4f}') if val_f1 > best_f1: best_f1 = val_f1 wait = 0 torch.save(model.state_dict(), 'best_model.pth') else: wait += 1 if wait >= patience: print(f'Early stop at epoch {epoch+1}') break scheduler.step()

逻辑说明:训练循环中每一个epoch先跑训练集更新参数,然后切到eval模式在验证集上计算指标。梯度裁剪是关键操作,LSTM训练时梯度幅度可能突然变大导致训练发散,max_norm=1.0防止这个问题。早停机制通过patience参数控制:如果连续5个epoch验证集F1没有提升,就终止训练,防止在验证集上持续过拟合。

参数说明:

  • max_norm=1.0:梯度裁剪阈值,太小收敛变慢,太大起不到防护作用。
  • patience=5:连续5轮无提升才停,避免因为验证集波动误杀还在上升期的模型。
  • best_model.pth:保存的是验证集F1最好的那一版参数,不是最后一轮参数,这是防止过拟合的重要手段。

训练结束后,用测试集评估最终模型。这里要分清楚验证集和测试集的不同分工:验证集参与模型选择和早停判断,测试集只在全部训练结束后用一次。如果反复拿测试集调参,测试集的评估结果就没有说服力了。

5. 避坑与常见问题排查:从数据泄漏到Loss不降的四个典型案例

5.1 现象:训练集F1接近0.99,测试集F1只有0.5

这是我见过最多的翻车现场。原因几乎都是数据划分时按窗口随机切分,同一个block的日志窗口一部分进了训练集,另一部分进了测试集。LSTM模型在训练时已经见过同一个block的上下文规律,到测试集上再看到相似模式自然表现好,但这种「好」到了真实场景中完全不存在。

解决方法是把数据划分放在窗口切分之前,先按BlockId划出训练块和测试块,再对每个块分别做滑窗。代码上就是把train_test_split(X, y)改成train_test_split(block_ids),然后用块ID做过滤。从那次之后我再也没在日志数据上按样本直接切过分,因为跨block的上下文信息泄漏太隐蔽了。

5.2 现象:Loss一直不降,训练过程震荡严重

原因分两类:一类是Embedding层没设padding_idx=0,导致padding位置也在更新向量,模型把大量无关信息当成了特征;另一类是学习率偏大,LSTM对学习率比CNN更敏感,1e-3有时都偏高。

排查顺序是先检查模板ID的取值分布,确认padding位置的输入全是0;再尝试把学习率降到1e-4或3e-4,同时把梯度裁剪加上。我一般顺手把LSTM的初始化方式改成正交初始化,有时能解决深层次训练不稳的问题。

5.3 现象:模型一直在预测正常类,Recall是0

这是类别不平衡的典型症状。异常样本占比可能不到5%,模型发现把所有样本预测成正常类就能把Loss压得很低。解决方式有两条:一是在损失函数中设置class_weight,让异常类的错误预测代价更高;二是对异常类做过采样,复制异常样本让正负比例接近1:2左右。前者改动小,后者适合在异常样本实在太少时使用。两个方法可以同时上,但过采样时要注意同一个样本被复制多次后,模型容易对少数几种异常模式过拟合。

5.4 现象:测试集指标比验证集差一大截

原因通常在于训练过程中对验证集做了隐式的「使用」。比如每轮都看验证集指标来调整超参数,甚至验证集做得不满意就回头改预处理重新训练,这等于把验证集信息泄露到了模型选择过程中。正确做法是:定义好超参数组合后,只在验证集上做一次模型选择;全部定稿后,在测试集上只评估一次,记录结果,不回头调参。

我自己的习惯是,测试集评估结果出来后写进报告,哪怕分数低也如实记录,然后把验证集和测试集上的差距作为「泛化能力分析」素材写进作业里,老师反而觉得分析到位。

5.5 现象:程序报错IndexError: index out of range in self

这条错误基本可以锁定在Embedding层。原因是构造训练样本时模板ID的编号和Embedding矩阵的vocab_size不一致,某个ID超出了Embedding的合法范围。常见于预处理时用了pd.factorize自动编号,但模型定义里把vocab_size手动设成了模板数量加一,两边没对齐。

解决办法是在构建模型前先打印max(X_train),同时打印vocab_size,确保前者小于后者。我通常在预处理最后加一行断言:assert X.max() < vocab_size, "模板ID越界",这行断言救过我很多次。

6. 进阶技巧:把窗口预测结果映射回Block级别,让检测指标更有说服力

如果你的期末作业想拿高分,光给窗口级F1是不够的。真实的日志异常检测业务里,运维人员关心的是「哪些block是异常的」,而不是「哪些窗口是异常的」。窗口是人为切分的产物,不同窗口重叠程度不同,同一个block异常时可能同时触发多个窗口的异常预测。所以一种常见做法是:把block内所有窗口的预测结果聚合,只要有一个窗口被预测为异常,整个block就判定为异常。

聚合代码实现如下:

from collections import defaultdict # windows_to_blocks: 记录每个窗口所属的block window_to_block = [] for bid, seq in block_sequences.items(): for _ in sliding_window(seq, window_size, stride): window_to_block.append(bid) # 假设test_preds是模型在测试集上的预测结果 block_preds = defaultdict(list) block_true = {} for bid, pred, true_label in zip(window_to_block, y_test_preds, y_test): block_preds[bid].append(pred) block_true[bid] = true_label # block级别预测:任一窗口异常则block异常 block_pred_final = { bid: int(max(preds)) for bid, preds in block_preds.items() } from sklearn.metrics import classification_report block_labels = [block_true[bid] for bid in block_pred_final] block_predictions = [block_pred_final[bid] for bid in block_pred_final] print(classification_report(block_labels, block_predictions))

逻辑说明:聚合策略取最大值意味着只要该block的窗口序列中有任何一个被识别为异常,这个block的最终预测结果就是异常。这种做法背后的思想是异常事件往往只出现在部分窗口内,如果按多数投票,异常窗口会被正常窗口掩盖掉。对HDFS日志这种场景来说,异常事件触发的窗口数量本来就少,用max策略更合理。

这么做的好处是:作业报告中你可以同时呈现两个层面的指标——窗口级指标和block级指标,并解释两者的区别。block级指标往往比窗口级更高,因为聚合操作让模型的决策更加「保守但有效」,这本身就是可以写进分析报告的一大段内容。

另一个值得做的进阶方向是把LSTM的隐层输出可视化。你可以提取测试集样本经过LSTM后最后一个时间步的隐向量,用PCA降维到二维,然后用散点图把正常和异常样本画出来。如果两类样本在二维空间中有明显分离趋势,这张图放进报告里非常加分,远比贴一堆指标数字更有说服力。

from sklearn.decomposition import PCA import matplotlib.pyplot as plt model.eval() features = [] labels = [] with torch.no_grad(): for xb in test_loader: xb = xb.to(device) embeds = model.embedding(xb) lstm_out, _ = model.lstm(embeds) # 取最后时间步隐向量 feat = lstm_out[:, -1, :].cpu().numpy() features.extend(feat) features = np.array(features) pca = PCA(n_components=2) features_2d = pca.fit_transform(features) plt.figure(figsize=(8, 6)) plt.scatter(features_2d[test_labels == 0, 0], features_2d[test_labels == 0, 1], c='blue', label='Normal', alpha=0.5, s=10) plt.scatter(features_2d[test_labels == 1, 0], features_2d[test_labels == 1, 1], c='red', label='Anomaly', alpha=0.5, s=10) plt.xlabel('PC1') plt.ylabel('PC2') plt.legend() plt.title('LSTM Hidden State Visualization (PCA)') plt.savefig('lstm_pca.png', dpi=150)

这段代码的原理是:LSTM把输入序列压缩成了隐向量,这个向量的分布反映了模型对序列语义的理解,PCA降维只是把高维表示映射到二维方便观察。如果两类点明显分群,说明模型学到了具有区分度的特征;如果完全混在一起,则意味着模型表达能力不足或者特征提取有问题,需要回头检查预处理。

从那以后,我每次做日志异常检测作业都会强制自己走一遍「按block划分、类别加权、block级聚合评估」这套流程,哪怕项目内容变了,这套方法论也没变过。希望这份拆解能帮你在期末作业里少走弯路,把更多时间花在真正重要的模型分析和实验对比上。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询