简介:这份资源是一套基于机器学习的入侵检测系统完整项目源码,面向计算机、人工智能、通信工程等专业的在校学生与教师,也适合作为毕业设计、课程设计或项目立项的参考范例。项目围绕网络流量嗅探与分类检测展开,包含数据预处理、SVM等机器学习算法实现,以及抓包嗅探模块,帮助读者理解从原始流量采集到模型训练与检测的完整链路。压缩包共22个文件,以xml配置、py源码、md说明文档为主,另含gitignore、license等工程辅助文件,整体约18KB,结构轻量便于快速上手。目前已有457人学习下载,代码均经过运行测试,答辩评审平均分达96分。读者可据此掌握入侵检测系统的目录组织与核心算法实现思路,并在此基础上修改扩展,用于毕设、课设或作业等场景。
1. 基于机器学习的入侵检测系统:从流量特征到告警落地的完整路径
很多团队第一次做入侵检测,都会掉进同一个坑:把公开数据集上的准确率刷到 99%,部署到真实网络里却连一个扫描行为都报不出来。问题不在模型,而在从流量采集、特征工程到推理服务的整条链路没有对齐。基于机器学习的入侵检测系统,本质是把网络流量或主机日志转成数值特征,用分类模型判断这条记录是正常还是攻击,再输出告警。它适合两类人:一是安全运维想给现有设备加一层智能告警,二是算法工程师想把分类模型落到真实安全场景。这篇笔记按我实际搭过的一套流程展开,从数据到模型到服务,每一步都给可复现的命令和参数,最后讲清楚哪些地方最容易翻车。
2. 先定检测对象:流量特征和主机日志到底选哪个
2.1 两种数据源的检测能力和成本对比
做入侵检测,第一个决策不是选模型,而是选数据源。常见做法有两类:网络流量特征和主机行为日志。网络流量侧通常用 NetFlow、PCAP 或 Zeek 生成的连接记录,字段包括源目的 IP、端口、协议、包数、字节数、持续时间、TCP 标志位等。主机侧则采集系统调用序列、进程创建、文件访问、注册表改动等。两者不是替代关系,而是覆盖不同攻击面。
| 维度 | 网络流量特征 | 主机行为日志 |
|---|---|---|
| 典型数据 | NetFlow、Zeek conn.log | 系统调用、进程事件 |
| 检测强项 | 端口扫描、DDoS、C2 通信 | 提权、持久化、横向移动 |
| 采集成本 | 中,需镜像或流量导出 | 高,需端点代理 |
| 隐私风险 | 较低,多为元数据 | 较高,含命令与路径 |
| 模型输入维度 | 几十到上百维 | 可达数百维序列 |
我一般建议第一版从网络流量特征切入,原因是采集链路成熟、公开数据多、特征含义直观,团队能快速跑通闭环。等告警链路稳定后,再补主机侧做纵深覆盖。
2.2 用 CICFlowMeter 风格特征做最小闭环
公开流量数据集里,CICIDS 系列和 UNSW-NB15 是绕不开的。它们已经把 PCAP 转成了连接级特征,省去自己写解析器的麻烦。下面这段 Python 读取 CSV、做标签映射和基础清洗,是整个流程的起点。
import pandas as pd import numpy as np # 读取连接级特征数据,低内存模式避免大文件爆内存 df = pd.read_csv("traffic_features.csv", low_memory=False) # 去掉前后空格,防止列名带空格导致后续 KeyError df.columns = df.columns.str.strip() # 标签列通常叫 Label 或 attack_cat,统一成 label label_col = "Label" if "Label" in df.columns else "attack_cat" df["label"] = df[label_col].astype(str).str.strip() # 二分类:BENIGN 为 0,其余攻击类型为 1 df["binary_label"] = np.where(df["label"].str.upper() == "BENIGN", 0, 1) # 丢弃全空列和常数列,它们对模型没有信息量 df = df.dropna(axis=1, how="all") nunique = df.nunique() constant_cols = nunique[nunique <= 1].index.tolist() df = df.drop(columns=constant_cols) print("样本数:", len(df), "特征数:", df.shape[1]) print(df["binary_label"].value_counts())这段代码做了三件事:统一标签列名、生成二分类标签、清理无效列。参数上,low_memory=False会让 pandas 一次性推断类型,适合内存充足的机器;如果数据超过内存,改用chunksize分块读取。nunique() <= 1过滤常数列,是因为这类列在树模型里分裂增益为零,留着只会拖慢训练。标签映射时注意大小写,不同数据集里 BENIGN 可能写成 Benign 或 benign,统一转大写再比较最稳。
2.3 特征里哪些列必须删掉
连接级特征里混着不少“作弊列”。比如 Flow ID、Source IP、Destination IP、Timestamp,这些列在训练集里能帮模型记住具体主机,但换一个网络就完全失效。我的做法是维护一个黑名单,训练前直接剔除。
drop_cols = [ "Flow ID", "Source IP", "Destination IP", "Src IP", "Dst IP", "Timestamp", "SimillarHTTP" ] df = df.drop(columns=[c for c in drop_cols if c in df.columns])SimillarHTTP这类列在不同版本里拼写不一致,用列表推导式加存在性判断,避免 KeyError。IP 和端口要不要全删?端口可以保留,因为某些攻击确实偏好特定端口;但源目的 IP 必须删,否则模型学到的只是“这个 IP 出现过就是攻击”。这一步做完,特征维度通常从 80 多降到 70 左右,模型泛化会明显改善。
3. 训练一个能落地的二分类模型:从划分到评估
3.1 分层划分与类别不平衡处理
安全数据天然不平衡,正常流量占九成以上。直接随机划分会导致测试集里攻击样本太少,评估结果波动大。用分层抽样保证训练集和测试集里攻击比例一致,再在训练时给少数类加权。
from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import classification_report, confusion_matrix feature_cols = [c for c in df.columns if c not in ("label", "binary_label")] X = df[feature_cols].replace([np.inf, -np.inf], np.nan).fillna(0) y = df["binary_label"] # stratify=y 保证训练测试集攻击比例一致 X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.3, random_state=42, stratify=y ) scaler = StandardScaler() X_train_scaled = scaler.fit_transform(X_train) X_test_scaled = scaler.transform(X_test) # class_weight="balanced" 自动按类别频率反比加权 clf = RandomForestClassifier( n_estimators=200, max_depth=20, class_weight="balanced", n_jobs=-1, random_state=42 ) clf.fit(X_train_scaled, y_train) y_pred = clf.predict(X_test_scaled) print(confusion_matrix(y_test, y_pred)) print(classification_report(y_test, y_pred, digits=4))replace([np.inf, -np.inf], np.nan).fillna(0)处理的是流量特征里常见的无穷值,比如平均包长在零包连接上会算出 inf。class_weight="balanced"让模型对少数类更敏感,代价是误报可能上升,这个后面调阈值时再平衡。n_estimators=200和max_depth=20是我在几十万样本上的常用起点,再大收益递减,再小容易欠拟合。
3.2 别只看准确率:召回率和误报率怎么权衡
安全场景里,准确率是最没用的指标。攻击样本只占 5%,全判正常也有 95% 准确率。真正要看的是攻击类的召回率(漏报)和正常类的召回率(误报)。下面这段输出按类别看指标,并画出阈值和召回的关系。
from sklearn.metrics import roc_curve, auc import matplotlib.pyplot as plt # 取攻击类概率,用于调阈值 proba = clf.predict_proba(X_test_scaled)[:, 1] fpr, tpr, thresholds = roc_curve(y_test, proba) roc_auc = auc(fpr, tpr) print("AUC:", round(roc_auc, 4)) # 找使 F1 最大的阈值,作为默认告警线 f1_scores = 2 * tpr * (1 - fpr) / (tpr + (1 - fpr) + 1e-9) best_idx = f1_scores.argmax() print("最佳阈值:", round(thresholds[best_idx], 4), "召回:", round(tpr[best_idx], 4), "误报率:", round(fpr[best_idx], 4))roc_curve返回的 thresholds 是递减的,argmax找到 F1 最大点。实际部署时,我通常把阈值定在召回 0.95 以上、误报率 0.05 以下的位置,宁可多报也不漏报,因为漏掉一次入侵的代价远高于多看一眼告警。如果误报压不下来,先检查特征里有没有残留的 IP 类列,再考虑换模型。
3.3 模型持久化与推理接口
训练完的模型要能存下来给服务调用。用 joblib 保存模型和标准化器,两者必须一起存,否则推理时特征尺度对不上。
import joblib joblib.dump({"model": clf, "scaler": scaler, "features": feature_cols}, "ids_model.joblib") # 推理时加载 bundle = joblib.load("ids_model.joblib") model, scaler, features = bundle["model"], bundle["scaler"], bundle["features"] def predict_one(record: dict) -> dict: # record 是单条连接特征字典 row = pd.DataFrame([record])[features].replace([np.inf, -np.inf], np.nan).fillna(0) x = scaler.transform(row) proba = model.predict_proba(x)[0, 1] return {"attack_prob": float(proba), "is_attack": bool(proba > 0.5)}features列表必须和训练时顺序一致,DataFrame 按列名选取能自动对齐,但如果有缺失列会直接报错,这比静默填错值要好。predict_one返回概率而不是硬标签,方便上层按业务调阈值。这个接口每秒能处理几千条,单机足够中小规模网络使用。
4. 避坑与排查:上线后最容易翻车的五个地方
4.1 训练集和线上特征分布不一致
现象:离线评估 AUC 0.99,上线后攻击告警几乎为零。原因:训练用的公开数据集和线上流量在协议分布、包长、连接时长上差异巨大,模型把训练集的统计规律当成了攻击特征。解决:上线前用线上正常流量跑一遍,看攻击概率分布是否集中在低分段;如果整体偏移,先用线上数据做无监督异常检测打底,再逐步补充标注样本重训。
4.2 无穷值和缺失值导致推理报错
现象:服务运行几小时后开始抛 ValueError,提示输入包含 NaN 或 inf。原因:某些连接在统计时除零,产生 inf,训练时被 fillna 掩盖,推理时没做同样处理。解决:把清洗逻辑封装成函数,训练和推理共用同一份代码,不要在两处各写一遍。下面这个函数就是统一入口。
def clean_features(df: pd.DataFrame, feature_cols: list) -> pd.DataFrame: df = df[feature_cols].replace([np.inf, -np.inf], np.nan) return df.fillna(0)4.3 类别权重设太高导致误报爆炸
现象:调大class_weight后召回上去了,但正常业务流量被大量误判。原因:权重让模型过度偏向攻击类,把边缘正常样本也划成攻击。解决:不要只调权重,配合阈值调整。先固定权重,画 ROC 找业务能接受的误报率对应阈值,再微调权重。我的经验是权重和阈值一起调,单动一个都会失衡。
4.4 特征顺序在保存和加载时错位
现象:模型加载后预测结果和训练时对不上,但代码没报错。原因:保存时用 DataFrame 列顺序,加载时用字典构造,列顺序变了,标准化器作用到了错误的列上。解决:保存feature_cols列表,推理时强制df[feature_cols]重排。这个坑很隐蔽,因为数值都在合理范围,只是对应关系错了。
4.5 告警没有去重和聚合
现象:一次端口扫描产生上万条告警,运维直接忽略。原因:模型对每条连接独立判断,同一攻击源的多条连接被重复告警。解决:在告警层按源 IP 和时间窗口聚合,比如 60 秒内同一源 IP 超过 20 条攻击告警才升级为事件。这个逻辑放在推理服务之后,不污染模型。
5. 把检测率再提一档:特征交叉与在线学习的取舍
模型上线只是开始,真正拉开差距的是特征工程和更新机制。我在第一版跑通后,做了两件事让召回率从 0.91 提到 0.96。第一是加特征交叉,比如“目的端口是否小于 1024”和“包数是否小于 3”组合,能抓住短连接扫描;第二是引入时间窗口统计,比如同一源 IP 在过去 5 分钟的连接数,这个特征对 DDoS 和暴力破解特别有效。
# 时间窗口聚合特征示例 df["is_low_port"] = (df["Destination Port"] < 1024).astype(int) df["is_short_conn"] = (df["Total Fwd Packets"] < 3).astype(int) df["scan_like"] = df["is_low_port"] * df["is_short_conn"] # 按源 IP 统计窗口内连接数(需先按时间排序) df = df.sort_values("Timestamp") df["conn_count_5min"] = ( df.groupby("Source IP")["Timestamp"] .transform(lambda s: s.rolling("5min").count()) )scan_like是布尔交叉,树模型能自己学,但显式给出来能减少分裂次数、加快收敛。rolling("5min")依赖时间索引,如果数据里 Timestamp 是字符串要先转 datetime。这个特征在线上要用流式窗口计算,不能等全量数据,常见做法是用 Redis 或本地滑动窗口维护计数。
在线学习要不要上?我的建议是谨慎。安全数据的标签来得慢,攻击样本少,在线更新容易被噪声带偏。更稳的做法是每月用新标注数据重训一次,中间用阈值微调应对分布漂移。如果一定要在线,至少保留一个离线基线模型做对照,一旦在线模型指标异常就切回去。
最后说个习惯:每次重训后,我都会拿一批历史告警人工过一遍,看新模型把哪些旧告警判成了正常。这些“消失的告警”里往往藏着真正的漏报,比看指标更能发现问题。希望帮到你。
本文还有配套的精品资源,点击获取