简介:本资源为基于Python与CNN(卷积神经网络)的网络入侵检测算法完整项目,面向计算机相关专业正在开展毕业设计、课程设计或期末大作业的学生,也适合希望进行项目实战练习的学习者。项目经导师指导并评审通过,评分98分,具备较强的教学参考与复现价值。压缩包共33个文件,约21.59MB,核心包含4个Python源码文件(涵盖数据预处理、模型构建、训练与预测流程)、12个csv格式的数据集文件、模型权重文件pth,以及项目说明文档、训练与评估图表等,目录结构完整,可帮助读者快速理解从NSL-KDD数据加载、特征处理到模型训练、精度评估的完整流程。已有191人学习或下载,适合作为网络入侵检测方向入门与进阶的实践参考。
1. 网络入侵检测为什么要用CNN:NSL-KDD和卷积的适配逻辑
做基于python+CNN的网络入侵检测,第一步不是调参,而是先想清楚数据集和模型为什么是这么配的。NSL-KDD是KDD Cup 99的改进版,去掉了冗余重复记录,保留了41维特征加一个标签,训练集和测试集划分固定,是毕设和课程设计里最稳的基准数据集。而CNN在图像上成名,把一维特征序列搬到CNN上做入侵检测,反而是个被反复验证的路子——把每条连接记录当作长度为41的信号,用一维卷积提取局部特征,再通过全连接层分类。这份源码正好覆盖了从CSV原始数据到训练、预测的完整链路,适合正在做毕设,或者想快速跑通CNN入侵检测流程的人。
2. 数据预处理:把NSL-KDD的41维特征变成CNN能吃的张量
NSL-KDD的原始CSV里,41维特征不是都能直接丢进CNN的。protocol_type、service、flag这三列是符号型,Attack Type列有几十种攻击名,要先做两件事:符号特征数值化、攻击标签粗粒度化。预处理脚本在PreHandle.py里,输入是KDDTrain+.csv这类原始文件,输出是模型能直接读的数值特征文件。DataSet Change目录里一般放的是转换后的中间文件,对应社区常见的改动版NSL-KDD;如果你下载的是原始KDD数据集,直接跑PreHandle.py生成即可。
2.1 符号特征数值化:LabelEncoder和手动映射的差别
NSL-KDD的41维特征里有三列符号特征:protocol_type(tcp/udp/icmp,3种取值)、service(http/ftp/smtp等,约70种取值)、flag(SF/REJ/S0等,11种取值)。CNN不能直接吃字符串,标准做法是用LabelEncoder把它们编码成整数。
import pandas as pd from sklearn.preprocessing import LabelEncoder df = pd.read_csv('KDDTrain+.csv', header=None) symbol_cols = [1, 2, 3] # protocol_type, service, flag for col in symbol_cols: le = LabelEncoder() df[col] = le.fit_transform(df[col])逻辑说明:sklearn的LabelEncoder把字符串转成0到n-1的整数,tcp/udp/icmp会被映射成0/1/2,service按字母序编码。这里有个后面会踩的坑:预测阶段出现训练时没见过的service字符串,直接transform会报错,所以要在预处理阶段就固定全局映射,或者把encoder对象保存下来。
参数说明:header=None是因为NSL-KDD原始csv没有列名行,列索引从0开始,[1,2,3]正好对应41维特征的前三列。如果用的数据集带表头,要改成header=0并按字段名取列。符号列取值数量会影响后续编码结果,这个项目直接用整数编码没有做embedding,对网络结构是个简化,效果也足够。
2.2 攻击标签映射:从几十种攻击名到五分类
NSL-KDD标签列由正常(normal)和几十种攻击名组成,常见做法是映射成5大类:Normal、DoS、Probe、R2L、U2R。DoS包含neptune、smurf、pod等拒绝服务攻击,Probe包含portscan、satan等探测行为,R2L是远程到本地的未授权访问,U2R是本地提权。训练集里这五类的样本量差异极大,理解这个分布后面评估时才不会只看整体accuracy。
| 类别 | 含义 | 训练集样本量(约) |
|---|---|---|
| Normal | 正常连接 | 67343 |
| DoS | 拒绝服务攻击 | 45927 |
| Probe | 端口扫描与漏洞探测 | 11656 |
| R2L | 远程未授权访问 | 995 |
| U2R | 本地提权 | 52 |
attack_map = { 'normal': 'Normal', 'back': 'DoS', 'land': 'DoS', 'neptune': 'DoS', 'pod': 'DoS', 'smurf': 'DoS', 'teardrop': 'DoS', 'ipsweep': 'Probe', 'nmap': 'Probe', 'portscan': 'Probe', 'satan': 'Probe', 'ftp_write': 'R2L', 'guess_passwd': 'R2L', 'imap': 'R2L', 'multihop': 'R2L', 'phf': 'R2L', 'spy': 'R2L', 'warezclient': 'R2L', 'warezmaster': 'R2L', 'buffer_overflow': 'U2R', 'loadmodule': 'U2R', 'perl': 'U2R', 'rootkit': 'U2R' } df[41] = df[41].str.strip().str.lower().map(attack_map).fillna('Unknown')逻辑说明:先统一转小写去空格,再通过字典映射到五大类。KDDTrain+里的攻击类型基本都能映射,但KDDTest+里包含训练时没出现过的攻击变种,映射不到会被fillna成Unknown。跑评估的时候,Unknown样本建议单独统计,不要在训练阶段统一丢弃,否则测试集分布被改得不真实,指标也失去说服力。
参数说明:标签列是41列索引。map是pandas的字典映射,速度比apply加if-else快,12万行数据差异尤其明显。如果你做的是二分类入侵检测,把Normal以外的四类全部映射成Attack就行;五分类和二分类的多分类头、评估指标展示逻辑都要跟着改。
2.3 特征分组的意识:理解41维特征再谈CNN建模
41维特征大致分四组:基础连接特征(1到9列,duration、protocol_type、service、src_bytes、dst_bytes等)、内容特征(10到22列,logged_in、num_compromised等)、基于时间的流量特征(23到31列,count、srv_count等)、基于主机的流量特征(32到41列,dst_host_count等)。CNN把整条记录当一维序列来扫,卷积核会把相邻列组合出局部模式,比如src_bytes和dst_bytes的差值、count和srv_count的比例,这些是传统规则检测里专家会关注的高频组合。
做完这步再回头看归一化,就会有感觉:内容特征大部分是0/1标志位,基础特征跨度从1到几十万字节,不归一化的话,一个duration=10000的样本会把卷积层的梯度方向带偏。minMax.png这个图展示的就是归一化前后的特征分布变化,答辩时放这一张图就能解释清楚为什么CNN需要预处理,也顺便证明预处理步骤是成体系的,不是随便拼的。
2.4 归一化和数据集划分的顺序不能乱
41维特征里duration、src_bytes这类数值跨度极大,不归一化的话,CNN的梯度更新会被大数值列带偏。常见做法是逐特征列做min-max,把值压到[0,1]。
import numpy as np from sklearn.preprocessing import MinMaxScaler from sklearn.model_selection import train_test_split feature_cols = list(range(0, 41)) scaler = MinMaxScaler() X = scaler.fit_transform(df[feature_cols].values.astype(np.float32)) y = df[41].values X_train, X_val, y_train, y_val = train_test_split( X, y, test_size=0.2, stratify=y, random_state=42 )逻辑说明:MinMaxScaler逐列计算min和max,把每个特征压到[0,1]。stratif=y让切分后每一类的比例和全量一致,避免随机切分把占比本来就小的U2R全部切到验证集里。random_state固定切分结果,实验对比时才不会因为数据集不同引入额外变量。
但这段代码里有个顺序问题必须警惕:如果先对整个X做fit_transform再切分,验证集的min和max信息已经渗进训练过程,这就是特征泄漏。正确顺序是先切分,再对X_train做fit_transform,对X_val和测试集只做transform:
X_train, X_val, y_train, y_val = train_test_split( df[feature_cols].values.astype(np.float32), df[41].values, test_size=0.2, stratify=df[41].values, random_state=42 ) scaler = MinMaxScaler() X_train_scaled = scaler.fit_transform(X_train) X_val_scaled = scaler.transform(X_val)参数说明:注意第二个写法里scaler只对训练集fit,验证集用同一套min和max做transform,测试集同样只用transform,不许再fit。如果项目提供测试集专用的归一化文件就直接用;没有的话,用训练集保存下来的scaler参数。这份源码包里的PreHandle.py对这个顺序是处理过的,但如果你改成自己的数据集,这条顺序仍然是最容易翻车的地方。
提示:归一化必须遵循「先切分、再fit训练集、transform其他集」的顺序,这条检查清单能挡住大半精度虚高问题。
3. CNNMould.py拆解:一维卷积结构、通道数与Dropout怎么定
CNNMould.py是模型定义文件,也是这个项目里最值得一行行读的部分。很多毕设代码把模型和训练写在一个文件里,这个项目拆开了:CNNMould.py只负责网络结构,Train.py负责训练循环,Predict.py负责加载模型做预测。这种拆分的好处是结构清晰,调试时单独验证前向传播的输出维度很方便。下载包里还有Mould Acc.png,一般是模型结构和精度变化的对照图,答辩前整理思路用得上。
3.1 输入张量怎么从41维向量变成卷积能处理的形状
PyTorch的Conv1d期望输入是(batch, channels, length)。原始一条记录是(41,)的向量,需要先unsqueeze成(batch, 1, 41),再进卷积层。
import torch import torch.nn as nn class CNNMould(nn.Module): def __init__(self, input_dim=41, num_classes=5): super(CNNMould, self).__init__() self.conv1 = nn.Sequential( nn.Conv1d(1, 64, kernel_size=3, padding=1), nn.BatchNorm1d(64), nn.ReLU(), nn.MaxPool1d(kernel_size=2, stride=2) ) self.conv2 = nn.Sequential( nn.Conv1d(64, 128, kernel_size=3, padding=1), nn.BatchNorm1d(128), nn.ReLU(), nn.MaxPool1d(kernel_size=2, stride=2) ) self.dropout = nn.Dropout(0.5) self.fc = nn.Linear(128 * 10, num_classes) def forward(self, x): x = x.unsqueeze(1) # (batch, 1, 41) x = self.conv1(x) # (batch, 64, 20) x = self.conv2(x) # (batch, 128, 10) x = x.view(x.size(0), -1) # (batch, 1280) x = self.dropout(x) x = self.fc(x) # (batch, num_classes) return x逻辑说明:第一条Conv1d把通道数从1扩到64,kernel_size=3配合padding=1保持长度不变,MaxPool1d(2)把41压到20;第二条卷积把64通道扩到128,池化后长度变10。view把三维特征图展平成128*10=1280维向量,再进全连接层输出5分类logits。
参数说明:kernel_size=3在一维信号上是稳的选择,卷积窗口覆盖3个相邻特征,比如src_bytes、dst_bytes、协议类型这三个列会被组合成一个局部模式。MaxPool1d的核大小2和步长2把长度减半,连续两层后41变成了10,相当于把原本离散的特征列压缩成高层抽象。
如果你的数据集不是41维而是别的维度,比如以后换成CICIDS2017的80多维特征,要改两个点:一是unsqueeze(1)前的长度不再是41,二是fc层的输入维度要跟着第二次池化后的length重新算。常见做法是拿一批假数据跑一遍forward,在每一层后print(shape),而不是手推公式,省时间也更不容易错。
3.2 BatchNorm和Dropout在这里各自的职责
NSL-KDD的41维特征即使做了min-max归一化,深层网络中间层的激活值分布还是会漂移。BatchNorm按batch维度把激活值拉回均值0方差1附近,训练过程会稳很多。Dropout(0.5)在前向时随机屏蔽一半神经元,给网络加了正则约束。
我实际对比过:去掉BatchNorm只用Dropout,训练loss降得很快但验证精度上不去,典型的过拟合。两个都保留的时候,训练精度和验证精度的gap能控制住。Dropout的比率也可以调:0.5是默认值,如果训练数据只有一两万条,可以提到0.6;数据量充足时0.3到0.4反而保留更多信息。
BatchNorm还有一个容易忽略的行为:训练模式下它按batch统计均值和方差,eval模式下用训练阶段累计的全局统计量。所以Predict.py里加载模型后必须调model.eval(),否则推理结果会和你训练时看到的验证精度对不上,这种差距在单条样本预测时尤其明显。
3.3 选型对照:为什么CNN在这里比LSTM和Transformer更合适
网络入侵检测的输入特征是物理含义明确的字段集合,不像文本有强时序依赖。CNN在一维特征上的优势是局部感受野自动组合相邻特征;而LSTM把41维顺序当作时序来处理,但NSL-KDD的特征顺序本身没有严格时间先后含义,比如第5列是src_bytes、第6列是dst_bytes,换一种特征排列顺序,LSTM的结果就会明显变化,这种不稳定性放在毕设里是硬伤。
Transformer在这个数据量下也吃亏。NSL-KDD训练集约12万条,对注意力机制来说样本偏少,容易过拟合;要发挥多头注意力的优势往往需要更大规模数据和预训练。所以在固定特征表的入侵检测场景里,CNN是性价比最高的选择:代码量小、训练快、答辩解释起来直观。CNN在推理阶段的耗时也远低于LSTM,单条样本在CPU上毫秒级出结果,这对做成实时检测工具很关键。
4. Train.py训练全流程:损失曲线、精度曲线与最优模型保存
Train.py是项目的发动机,读预处理后的数据,实例化CNNMould,跑若干轮epoch,把验证集上精度最高的权重存成best_model.pth。文件里值得注意的不只是训练循环本身,还有模型保存策略和评估指标的可视化输出。accuracy.jpg和precision.jpg是训练结束后绘制的曲线图,Mould Acc.png是模型结构图,这些图答辩时直接放进PPT里就是有效证据。
4.1 训练主循环:CrossEntropyLoss、Adam与epoch设置
分类任务的标准组合是交叉熵损失加Adam优化器。训练循环按batch取数、清梯度、前向、算损失、反向传播、更新参数。
import torch import torch.nn as nn from torch.utils.data import DataLoader, TensorDataset from CNNMould import CNNMould def train_epoch(model, loader, criterion, optimizer, device): model.train() running_loss = 0.0 for xb, yb in loader: xb, yb = xb.to(device), yb.to(device) optimizer.zero_grad() out = model(xb) loss = criterion(out, yb) loss.backward() optimizer.step() running_loss += loss.item() * xb.size(0) return running_loss / len(loader.dataset) def evaluate(model, loader, device): model.eval() correct, total = 0, 0 with torch.no_grad(): for xb, yb in loader: xb, yb = xb.to(device), yb.to(device) out = model(xb) pred = torch.argmax(out, dim=1) correct += (pred == yb).sum().item() total += yb.size(0) return correct / total criterion = nn.CrossEntropyLoss() optimizer = torch.optim.Adam(model.parameters(), lr=0.001)逻辑说明:训练循环里最容易被忽略的是optimizer.zero_grad()必须在backward之前调用,否则梯度会在batch间累积,loss曲线周期性抖动。交叉熵内部自带softmax,模型输出层不需要额外接Softmax。evaluate函数里model.eval()和torch.no_grad()缺一不可,前者切换BatchNorm到推理模式,后者关闭梯度计算省显存。
参数说明:lr=0.001是Adam常用学习率,如果loss震荡不收敛,优先降到0.0005。batch_size选64或128,NSL-KDD训练集约12万条时64的batch一个epoch约2000次迭代,收敛速度和显存占用比较平衡。epoch建议30到50轮,盯着验证精度不提升就早停,不要盲目跑满。
4.2 训练过程可视化与accuracy、precision图表的判读方法
项目里的accuracy.jpg、precision.jpg是用matplotlib绘制的曲线图。训练时每完成一个epoch记录一次train_acc和val_acc,结束后画两条曲线。判读曲线时重点不是看曲线多平滑,而是看两条曲线的gap和趋势。
如果val_acc曲线贴着train_acc走,说明模型没有过拟合;如果train_acc冲到99%而val_acc停在80%以下,先怀疑数据泄漏,再怀疑网络过深。precision曲线比accuracy更能体现分类质量,因为Normal样本占比过半,模型全预测Normal都能拿50%以上accuracy,但per-class precision会立刻暴露这个病。
import matplotlib.pyplot as plt plt.figure(figsize=(8, 5)) plt.plot(range(1, len(train_acc_list) + 1), train_acc_list, label='train acc') plt.plot(range(1, len(val_acc_list) + 1), val_acc_list, label='val acc') plt.xlabel('epoch') plt.ylabel('accuracy') plt.legend() plt.savefig('accuracy.jpg', dpi=300)逻辑说明:train_acc_list和val_acc_list在训练循环里每个epoch追加一次当前精度,训练结束后一次性绘图。横坐标用range(1, len+1)让曲线从第1个epoch开始,而不是从0开始。savefig的dpi=300是给答辩PPT用的清晰度,直接插进论文也不糊。
参数说明:figsize控制画布尺寸,8x5英寸是常规比例。两条曲线画在同一张图上是为了对比train和val的gap;如果想单独看每个类别的precision,可以每类画一条线,也可以直接打印classification_report,后者信息更全。
训练结束后额外打印一份classification_report很值得:
from sklearn.metrics import classification_report y_true, y_pred = [], [] model.eval() with torch.no_grad(): for xb, yb in val_loader: out = model(xb.to(device)) y_pred.extend(torch.argmax(out, dim=1).cpu().numpy()) y_true.extend(yb.cpu().numpy()) print(classification_report(y_true, y_pred, digits=4))逻辑说明:classification_report输出每个类的precision、recall、f1-score和样本数。如果DoS的f1是0.99而U2R的f1是0.3,问题不在总体精度,而是少数类样本不足或特征区分度不够。这个表比loss曲线更早暴露模型短板,我一般每轮训练完都顺手打一次。
4.3 best_model.pth的保存与加载约定
文件里的best_model.pth保存的是验证精度最高的权重。训练过程中每个epoch结束都跑一遍验证集,当前精度超过历史最佳就覆盖保存。
best_val_acc = 0.0 epochs = 50 for epoch in range(epochs): train_loss = train_epoch(model, train_loader, criterion, optimizer, device) val_acc = evaluate(model, val_loader, device) if val_acc > best_val_acc: best_val_acc = val_acc torch.save(model.state_dict(), 'best_model.pth') print(f'epoch {epoch} save best model, val_acc={val_acc:.4f}')逻辑说明:只保存state_dict而不保存整个model对象,是因为state_dict体积小、不受PyTorch版本变化影响。加载时先实例化CNNMould再load_state_dict,这个模式在Predict.py里同样使用。保存条件用严格大于而不是大于等于,避免验证精度轻微波动时反复覆盖文件。
训练过程建议每保存一次就在控制台打印epoch和val_acc,跑完能清楚看到模型在第几轮收敛。后续复现实验时,best_model.pth加上固定的random_state,结果可以精确复现。
5. 避坑排查:预测全同类、特征泄漏与设备不匹配的五个真实现场
这一章写我在复现和调试CNN入侵检测项目时踩过的坑,每一条都是实际出现过的,不是理论推导。
5.1 预测脚本跑出来全是Normal类
现象:训练时精度很高,但Predict.py对任意新样本都输出Normal,换了输入也一样。
原因:两个常见来源。一是标签映射在训练和预测时不统一,预测脚本里把攻击类别全归到unknown,unknown又被当成Normal处理;二是归一化参数不匹配,预测时用了另一个scaler或者对全量数据重新fit了一次,输入分布偏移后,CNN输出偏向样本量最大的类别。
解决:检查Predict.py加载的是不是训练阶段保存的同一份scaler。规范做法是把scaler和LabelEncoder对象用joblib持久化:
import joblib joblib.dump(scaler, 'scaler.pkl') joblib.dump(label_encoder, 'label_encoder.pkl') # 预测时重新加载 scaler = joblib.load('scaler.pkl') label_encoder = joblib.load('label_encoder.pkl')逻辑说明:joblib保存的scaler对象里含每个特征列的min和max,预测时用同一份参数transform,输入分布才能和训练一致。这也顺便说明为什么预处理脚本里要单独拆出保存逻辑,而不是在训练脚本里顺手归一化。
5.2 训练集精度99%,测试集精度掉到70%,怀疑特征泄漏
现象:Train.py训练集accuracy图上到99%,换到KDDTest+.csv上预测,精度掉到70%出头。
原因:一方面,NSL-KDD测试集包含训练集没见过的攻击变种,这是数据集本身特点;但更常见的是代码层面的特征泄漏——先对全量X做了fit_transform再切分,验证集和测试集的信息提前进入训练。
解决:按第2章的顺序做两层检查。第一,train_test_split在归一化之前执行;第二,scaler只对训练集调用fit,验证集和测试集只调用transform。用pipeline把这两步锁死更稳:
from sklearn.pipeline import Pipeline from sklearn.preprocessing import MinMaxScaler preprocessor = Pipeline([('scaler', MinMaxScaler())]) X_train_scaled = preprocessor.fit_transform(X_train) X_val_scaled = preprocessor.transform(X_val) X_test_scaled = preprocessor.transform(X_test)参数说明:Pipeline把预处理流程封装成单对象,fit_transform和transform的区别在代码层面被强制区分,从根上堵住泄漏路径。注意pipeline的transform不会重新计算min和max,只复用训练阶段存下的参数。
注意:特征泄漏不像模型过拟合那么容易被发现,它表现为测试集指标虚高但换新数据立刻崩。查归一化是最容易犯错的位置。
5.3 GPU上训练好的模型在CPU上加载报错
现象:训练机有显卡,Predict.py换到没有显卡的机器上运行,torch.load时直接报错,报错信息通常包含Attempting to deserialize object on a CUDA device。
原因:保存的state_dict里tensor是cuda类型,CPU环境下无法直接反序列化。
解决:加载时指定map_location='cpu',或在训练保存前把权重转到CPU再存。
model = CNNMould(41, 5) model.load_state_dict(torch.load('best_model.pth', map_location=torch.device('cpu'))) model.eval()参数说明:map_location='cpu'把所有CUDA张量映射到CPU。反向情况是CPU训练保存的模型在GPU上加载,PyTorch默认能加载,但要记得把模型和输入tensor都.to('cuda')。毕设答辩演示时经常在教室电脑的CPU环境跑,所以这个坑很常见。
5.4 LabelEncoder在预测时遇到训练集没见过的字符串
现象:预测脚本输入一条新记录,service字段出现训练集70种取值之外的值,LabelEncoder抛ValueError,程序中断。
原因:sklearn的LabelEncoder对未知类别没有兜底机制,transform时发现陌生值直接报错。
解决:手动把类别字符串转成字典映射,未知值统一归到新下标:
service_map = {v: i for i, v in enumerate(le.classes_)} def safe_encode(val): return service_map.get(val, len(service_map))逻辑说明:先枚举训练集见过的所有取值,预测时遇到新值不再中断,而是落到额外的未知类别。这样至少保住整条预测流程不崩;后续还可以单独看这批未知样本的分类结果,判断是否真的属于新攻击模式。
5.5 U2R和R2L的recall常年接近0
现象:classification_report里U2R的f1在0到0.1之间,R2L也好不到哪去,DoS和Normal都在0.99。
原因:NSL-KDD类别分布极不均衡,U2R训练集只有几十条,R2L不到一千条,Normal和DoS都是几万条。CNN在交叉熵损失下优先拟合大类别,小类别梯度被稀释。
解决:给损失函数加类别权重,或对少数类过采样。最简单的方式是用样本占比的倒数作权重传进CrossEntropyLoss:
from sklearn.utils.class_weight import compute_class_weight class_weights = compute_class_weight('balanced', classes=np.unique(y_train), y=y_train) criterion = nn.CrossEntropyLoss( weight=torch.FloatTensor(class_weights).to(device))参数说明:balanced模式让大类的loss权重小、小类的loss权重大,相当于放大了U2R和R2L的梯度贡献。加权重后整体accuracy会略微下降,这是正常权衡,答辩时把加权前后的两套指标都展示出来反而更有说服力。
6. 把Predict.py改成检测工具:单样本封装、批量验证与分类报告
前面的章节把数据、模型、训练和坑都讲完了,这一章落到Predict.py的改造上。下载包里的Predict.py大概率是读一条CSV记录再输出一个类别,但实际用起来,我们要的是一个能反复调用的函数:输入一条41维特征,输出类别和置信度。模型从训练代码里彻底解耦出来,才算真正变成可用的检测工具。
6.1 单样本预测函数的封装
def predict_one(model, sample_41, scaler, label_map, device): arr = np.array(sample_41, dtype=np.float32).reshape(1, -1) arr = scaler.transform(arr) tensor = torch.FloatTensor(arr).to(device) model.eval() with torch.no_grad(): logits = model(tensor) prob = torch.softmax(logits, dim=1) pred_idx = torch.argmax(prob, dim=1).item() conf = prob[0, pred_idx].item() return label_map[pred_idx], conf逻辑说明:sample_41的每个位置必须和训练时的特征列顺序严格一致。scaler.transform用的是训练时保存的min和max,如果新样本某一列值超出训练范围,归一化后可能落在[0,1]区间外,模型仍能推理但置信度会下降;这个特性可以反过来用于异常样本初筛。返回的conf是softmax概率,可以设阈值,比如低于0.8就标记为低置信度待人工复核,比直接相信单次输出可靠。
6.2 批量验证:改完预测逻辑必跑的一致性检查
每改一次Predict.py的预处理链路,就取测试集前100条跑一遍,看输出类别分布。如果输出里95%都是Normal,说明预处理链路大概率断了;如果分布和训练集类别占比接近,再跑全量测试集算指标。
我现在的习惯是:每改一次Predict.py,强制跑一遍全量测试集,用classification_report对比改动前后的per-class f1,指标不低于原结果才继续往下做。这套验证流程跟了我好几个项目了。从那以后我每次做数据切分和归一化都会强制走一遍「切分在前、fit训练集、transform其他集」的检查清单,模型权重保存也固定加map_location='cpu'。希望帮到你。
本文还有配套的精品资源,点击获取