
简介这是一套面向高校计算机、人工智能与电子信息等专业学生的机器学习实践资源围绕恶意网站检测这一网络安全场景提供从特征提取到多算法验证的完整实现方案。压缩包共11个文件约3.32MB以Python脚本为主体辅以数据压缩包、备份文件与说明文档分别承担特征向量提取、特征分布可视化、模型训练与结果记录等职责。系统整合了支持向量机、随机森林与深度神经网络三类算法通过超平面分类、多决策树集成与多层感知机非线性学习进行对比验证交叉验证准确率均达95%以上并配有特征对照表作为标注依据。目前已有50人学习适合作为课程设计、毕业设计或算法对比实验的参考模板。读者可据此理解特征工程流程、复现三种模型的训练与评估并在此基础上调整网络参数或引入增量学习机制模块结构清晰便于扩展与优化。1. 三算法同台竞技恶意网站检测这套源码到底能跑出什么结果恶意网站检测这件事很多人第一反应是上规则库、上黑名单但规则永远追不上变种速度。这套create_experiment_research-main.zip走的是另一条路把 URL 和页面特征转成向量用 SVM、随机森林、DNN 三种算法分别训练分类器最后对比谁更稳。我拆完整个包之后的第一感受是——它不是那种跑个 demo 就完事的玩具代码translate.py负责特征提取typelist.py管类型映射forest_split.py做随机森林的切分逻辑SVM.py和DNN.py各自独立成文件结构上确实像一份能交实验报告的东西。适合谁用如果你是计算机、人工智能、电子信息方向的学生需要一份能跑通、能改参数、能写进论文实验章节的代码这套东西的完成度是够的。如果你是从业者想快速验证某个特征组合对分类效果的影响它的模块拆分也允许你只替换特征提取部分。摘要里说三种算法交叉验证准确率都在 95% 以上这个数字我不意外——恶意网站检测在特征选得好的情况下本来就是高准确率场景关键看泛化能力和特征工程的可扩展性。2. 数据管线拆解从 translate.py 到特征对照表的完整链路2.1 特征提取的入口为什么是 translate.py拿到包之后别急着跑模型先把数据流搞清楚。整个系统的起点是translate.py它的职责是把原始网站数据转成模型能吃的数值向量。我一般会先打开这个文件看它到底提取了哪些维度——常见做法是 URL 长度、特殊字符数量、数字占比、是否包含 IP 地址、路径层级深度、查询参数个数这类结构化特征。这些特征的好处是计算快、不依赖页面渲染适合大批量处理。# translate.py 的核心逻辑示意基于常见实现推断 import re import numpy as np def extract_features(url): 从单条 URL 提取数值特征向量 features [] # 特征1URL 总长度钓鱼网站常用超长 URL 混淆 features.append(len(url)) # 特征2特殊字符数量、-、_、、 等 special_chars len(re.findall(r[\-_?%], url)) features.append(special_chars) # 特征3数字字符占比 digit_ratio sum(c.isdigit() for c in url) / max(len(url), 1) features.append(digit_ratio) # 特征4是否直接使用 IP 地址而非域名 ip_pattern r\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3} features.append(1 if re.search(ip_pattern, url) else 0) # 特征5路径层级深度 path_depth url.count(/) - 2 if url.count(/) 2 else 0 features.append(path_depth) return np.array(features)这段代码的逻辑很直白每条 URL 变成一个固定长度的数值数组。参数方面特殊字符的正则集合你可以按数据集特点增删比如有些数据集里%出现频率极高那就值得单独拎出来做一个特征。数字占比这个特征对 DNN 特别友好因为归一化之后它天然在 0 到 1 之间。IP 地址判断是个二值特征SVM 对这种离散特征的处理需要留意核函数选择后面会讲。跑完translate.py之后你会得到一个特征矩阵。这时候别急着喂模型先跑typelist.py确认标签映射对不对——它管的是把原始标签转成 0/1 或者多分类的整数编码。我见过太多人在这里翻车标签顺序搞反了模型准确率 95% 但预测结果全是反的。2.2 特征分布可视化与 2.xlsx 对照表的使用typlot.py这个文件名大概率是 typo实际功能应该是特征分布绘图。它的价值在于让你在训练之前先看一眼数据长什么样。常见做法是用直方图看每个特征的分布用箱线图看正负样本在各特征上的差异。如果某个特征在两类样本上分布几乎重合那它大概率是噪声可以考虑剔除。# typlot.py 特征可视化核心逻辑示意 import matplotlib.pyplot as plt import pandas as pd # 读取 translate.py 输出的特征矩阵 df pd.read_csv(features.csv, headerNone) df.columns [url_len, special_cnt, digit_ratio, is_ip, path_depth, label] fig, axes plt.subplots(2, 3, figsize(15, 8)) for idx, col in enumerate(df.columns[:-1]): ax axes[idx // 3][idx % 3] # 按标签分组画直方图直观对比正负样本分布差异 df[df[label] 0][col].hist(axax, alpha0.5, labelnormal, bins30) df[df[label] 1][col].hist(axax, alpha0.5, labelmalicious, bins30) ax.set_title(col) ax.legend() plt.tight_layout() plt.savefig(feature_distribution.png, dpi150)这段代码跑完会生成一张六宫格图你能一眼看出哪些特征有区分度。比如is_ip这个二值特征如果恶意样本里 IP 直连的比例远高于正常样本那它在树模型里的分裂增益就会很高。2.xlsx是特征对照表里面记录了每个特征的含义和取值说明写实验报告的时候直接引用这张表就行不用自己再整理。注意typlot.py依赖 matplotlib如果你的环境里没有中文字体图上的中文标签会变成方块。要么在代码里指定字体路径要么把标签改成英文。2.3 三种算法的输入格式差异与适配SVM、随机森林、DNN 对输入的要求不一样这是很多人忽略的点。SVM 对特征尺度敏感url_len可能是几百digit_ratio在 0 到 1 之间如果不做归一化SVM 的超平面会被大数值特征主导。随机森林基于分裂增益对尺度不敏感但对特征的相关性敏感——如果两个特征高度线性相关树在分裂时会随机选一个导致结果不稳定。DNN 需要归一化而且对特征之间的交互关系学习能力最强但需要更多数据才能收敛。# 数据预处理针对不同算法的输入适配 from sklearn.preprocessing import StandardScaler, MinMaxScaler from sklearn.model_selection import train_test_split X, y load_features(features.csv) X_train, X_test, y_train, y_test train_test_split(X, y, test_size0.2, random_state42) # SVM 用 StandardScaler零均值单位方差适合 RBF 核 scaler_svm StandardScaler() X_train_svm scaler_svm.fit_transform(X_train) X_test_svm scaler_svm.transform(X_test) # DNN 用 MinMaxScaler压缩到 [0,1]配合 sigmoid/tanh 激活 scaler_dnn MinMaxScaler() X_train_dnn scaler_dnn.fit_transform(X_train) X_test_dnn scaler_dnn.transform(X_test) # 随机森林直接用原始特征但建议做一次方差过滤 from sklearn.feature_selection import VarianceThreshold selector VarianceThreshold(threshold0.01) X_train_rf selector.fit_transform(X_train) X_test_rf selector.transform(X_test)这里的关键参数是random_state42固定随机种子保证每次切分一致不然你调参的时候会怀疑人生。VarianceThreshold的阈值 0.01 是我一般会用的起点如果特征维度很高可以适当调大。SVM 的StandardScaler必须用训练集的均值和方差去 transform 测试集不能分别 fit否则数据泄露准确率虚高。3. 三算法实战SVM、随机森林、DNN 的训练脚本与参数调优3.1 SVM.py 的超平面构建与核函数选择SVM.py的核心是sklearn.svm.SVC或者LinearSVC。恶意网站检测的特征维度通常不高几十维样本量中等几千到几万这种场景下 RBF 核一般比线性核表现好因为特征之间可能存在非线性交互。但 RBF 核有两个关键参数C和gamma。C越大对误分类的惩罚越重容易过拟合gamma越大决策边界越复杂也容易过拟合。# SVM.py 核心训练与调参逻辑 from sklearn.svm import SVC from sklearn.model_selection import GridSearchCV from sklearn.metrics import classification_report, accuracy_score # 基础模型RBF 核先给一组经验参数 svm_clf SVC(kernelrbf, C1.0, gammascale, random_state42) svm_clf.fit(X_train_svm, y_train) y_pred_svm svm_clf.predict(X_test_svm) print(SVM 基础准确率:, accuracy_score(y_test, y_pred_svm)) # 网格搜索C 和 gamma 的经典组合 param_grid { C: [0.1, 1, 10, 100], gamma: [scale, auto, 0.01, 0.1, 1] } grid GridSearchCV(SVC(kernelrbf, random_state42), param_grid, cv5, n_jobs-1, scoringf1) grid.fit(X_train_svm, y_train) print(最优参数:, grid.best_params_) print(最优交叉验证 F1:, grid.best_score_) # 用最优参数在测试集上评估 best_svm grid.best_estimator_ y_pred_best best_svm.predict(X_test_svm) print(classification_report(y_test, y_pred_best))gammascale是 sklearn 的默认值等于1 / (n_features * X.var())大多数情况下够用。如果你发现模型在训练集上准确率很高但测试集掉得厉害先把C调小、gamma调小试试。GridSearchCV的cv5是五折交叉验证scoringf1是因为恶意网站检测里正负样本可能不均衡准确率会骗人F1 更靠谱。n_jobs-1用满所有 CPU 核心不然网格搜索会很慢。3.2 forest_split.py 的集成策略与树数量选择随机森林的核心参数是n_estimators树的数量和max_depth树的最大深度。树越多模型越稳定但训练时间线性增长。我一般会从 100 棵树起步看 OOBOut-of-Bag误差曲线是否已经平缓平缓了就说明树的数量够了。max_depth不设的话树会完全生长容易过拟合但随机森林的 bagging 机制本身有抗过拟合能力所以通常设一个较大的值比如 20 到 30就行。# forest_split.py 随机森林训练与特征重要性分析 from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import accuracy_score import matplotlib.pyplot as plt import numpy as np # 基础模型100 棵树OOB 评分开启 rf_clf RandomForestClassifier( n_estimators100, max_depth25, min_samples_split5, min_samples_leaf2, oob_scoreTrue, random_state42, n_jobs-1 ) rf_clf.fit(X_train_rf, y_train) y_pred_rf rf_clf.predict(X_test_rf) print(随机森林准确率:, accuracy_score(y_test, y_pred_rf)) print(OOB 评分:, rf_clf.oob_score_) # 特征重要性排序哪些特征对分类贡献最大 importances rf_clf.feature_importances_ indices np.argsort(importances)[::-1] feature_names [url_len, special_cnt, digit_ratio, is_ip, path_depth] for i in range(len(feature_names)): print(f{feature_names[indices[i]]}: {importances[indices[i]]:.4f}) # 树数量与 OOB 误差的关系曲线 oob_errors [] for n in range(10, 210, 10): rf_tmp RandomForestClassifier(n_estimatorsn, oob_scoreTrue, random_state42, n_jobs-1) rf_tmp.fit(X_train_rf, y_train) oob_errors.append(1 - rf_tmp.oob_score_) plt.plot(range(10, 210, 10), oob_errors, markero) plt.xlabel(n_estimators) plt.ylabel(OOB error) plt.savefig(rf_oob_curve.png, dpi150)min_samples_split5和min_samples_leaf2是我常用的防过拟合组合意思是节点至少 5 个样本才分裂叶节点至少 2 个样本。oob_scoreTrue让你不用单独划验证集就能评估模型省数据。特征重要性排序能告诉你哪些特征真正在起作用——如果url_len的重要性远高于其他那说明数据集里长 URL 的恶意样本占比很高这时候可以考虑对 URL 长度做分桶处理而不是直接用原始值。3.3 DNN.py 的多层感知机结构与训练技巧DNN.py大概率是用 Keras 或者 PyTorch 写的多层感知机。恶意网站检测的特征维度不高网络不需要太深两到三个隐藏层就够了。每层神经元数量从输入维度的 2 倍开始递减比如输入 30 维第一层 64第二层 32第三层 16输出层 1 个神经元配 sigmoid。激活函数隐藏层用 ReLU输出层用 sigmoid二分类或 softmax多分类。# DNN.py 基于 Keras 的多层感知机实现 import tensorflow as tf from tensorflow.keras import layers, models, callbacks from sklearn.metrics import accuracy_score def build_dnn(input_dim): model models.Sequential([ # 输入层 第一隐藏层 layers.Dense(64, activationrelu, input_shape(input_dim,)), layers.Dropout(0.3), # 丢弃 30% 神经元防过拟合 # 第二隐藏层 layers.Dense(32, activationrelu), layers.Dropout(0.2), # 第三隐藏层 layers.Dense(16, activationrelu), # 输出层二分类用 sigmoid layers.Dense(1, activationsigmoid) ]) model.compile( optimizertf.keras.optimizers.Adam(learning_rate0.001), lossbinary_crossentropy, metrics[accuracy] ) return model # 早停验证集 loss 连续 5 轮不下降就停恢复最优权重 early_stop callbacks.EarlyStopping( monitorval_loss, patience5, restore_best_weightsTrue ) model build_dnn(X_train_dnn.shape[1]) history model.fit( X_train_dnn, y_train, validation_split0.2, # 从训练集里再切 20% 做验证 epochs100, batch_size32, callbacks[early_stop], verbose1 ) y_pred_dnn (model.predict(X_test_dnn) 0.5).astype(int) print(DNN 准确率:, accuracy_score(y_test, y_pred_dnn))Dropout(0.3)和Dropout(0.2)是经验值特征维度低的时候 dropout 比例别太高不然欠拟合。Adam(learning_rate0.001)是默认起点如果 loss 震荡就降到 0.0005如果收敛太慢就升到 0.002。EarlyStopping的patience5意思是验证集 loss 连续 5 轮不降就停restore_best_weightsTrue保证你拿到的是验证集上最好的那组权重而不是最后一轮的。batch_size32对小数据集合适数据量上万之后可以调到 64 或 128。4. 避坑与排查跑这套代码时最容易翻车的五个地方4.1 标签编码顺序反了导致准确率虚高现象模型训练完准确率 95% 以上但拿几条已知恶意 URL 去预测全被判成正常。原因typelist.py里标签映射的顺序和2.xlsx对照表不一致比如正常网站被编码成 1恶意网站被编码成 0但评估代码里默认 1 是恶意。模型学到的决策边界完全反了准确率却因为正负样本均衡而看起来很高。解决跑完typelist.py之后手动打印前 10 条样本的标签和原始类别和2.xlsx逐条核对。或者在评估代码里加一行print(np.unique(y_test, return_countsTrue))确认标签分布符合预期。4.2 特征归一化在训练集和测试集上分别 fit现象SVM 和 DNN 的测试集准确率比交叉验证准确率低 10 个点以上。原因StandardScaler或MinMaxScaler在训练集和测试集上分别fit_transform导致测试集的均值和方差和训练集不一致模型看到的输入分布和训练时不同。解决永远只在训练集上fit然后transform测试集。代码里写成scaler.fit(X_train)然后scaler.transform(X_test)不要图省事用fit_transform一把梭。4.3 随机森林 n_estimators 设太大导致训练时间爆炸现象forest_split.py跑了半小时还没出结果CPU 风扇狂转。原因n_estimators设成了 1000 甚至更多而且max_depth没限制每棵树都完全生长。随机森林的训练时间随树数量和深度线性增长1000 棵深度 30 的树在几万条数据上确实要跑很久。解决先用 100 棵树跑一遍看 OOB 误差曲线是否已经平缓。如果 100 棵和 200 棵的 OOB 误差差距小于 0.5%就没必要再加树。max_depth设 20 到 30 之间配合min_samples_leaf2控制树的复杂度。4.4 DNN 训练时 loss 变成 NaN现象DNN.py跑了几轮之后 loss 突然变成 NaN准确率掉到 50%。原因学习率太大导致梯度爆炸或者输入特征没有归一化某些维度的值域过大。另外如果标签是 0/1 而输出层用了 softmax 配 categorical_crossentropy也会出问题。解决先把学习率降到 0.0001 试试。确认输入已经用MinMaxScaler归一化到 [0,1]。二分类输出层用Dense(1, activationsigmoid)配binary_crossentropy不要用 softmax。如果还不行加tf.keras.optimizers.Adam(clipnorm1.0)做梯度裁剪。4.5 交叉验证时数据泄露现象交叉验证准确率 98%但测试集只有 85%。原因在交叉验证之前就对整个数据集做了归一化或特征选择导致验证折的信息泄露到了训练折。比如先对整个 X 做StandardScaler再切分交叉验证验证折的均值和方差已经包含了训练折的信息。解决用sklearn.pipeline.Pipeline把归一化和模型串起来交叉验证时 pipeline 会在每个折内部分别 fit 归一化器。代码写成Pipeline([(scaler, StandardScaler()), (svm, SVC())])然后直接对 pipeline 做GridSearchCV。5. 进阶技巧用增量学习和特征扩展把准确率再往上推5.1 增量学习让模型跟上恶意网站的变化恶意网站的特征分布不是静态的今天有效的特征组合下个月可能就被绕过。这套代码里的模型都是批量训练训练完就固定了。如果你想让模型持续更新常见做法是用sklearn的partial_fit接口做增量学习。SVM 的SGDClassifier和 DNN 都支持partial_fit随机森林不支持但可以用warm_startTrue在原有树的基础上加新树。# 增量学习示例用 SGDClassifier 替代标准 SVM from sklearn.linear_model import SGDClassifier from sklearn.preprocessing import StandardScaler scaler StandardScaler() sgd_clf SGDClassifier(losshinge, penaltyl2, alpha0.0001, random_state42) # 第一批数据正常 fit X_batch1 scaler.fit_transform(X_train[:5000]) sgd_clf.fit(X_batch1, y_train[:5000]) # 后续批次partial_fit 增量更新 for i in range(5000, len(X_train), 2000): X_batch scaler.transform(X_train[i:i2000]) sgd_clf.partial_fit(X_batch, y_train[i:i2000])SGDClassifier的losshinge等价于线性 SVMalpha0.0001是正则化强度越小越容易过拟合。partial_fit的时候必须用同一个 scaler不能重新 fit否则分布对不上。增量学习的坑在于新数据的分布如果和旧数据差异太大模型会灾难性遗忘旧样本上的表现会骤降。常见做法是每次增量更新时混入一部分旧数据比例大概 1:1。5.2 特征扩展从 URL 特征到页面内容特征这套代码目前提取的是 URL 层面的结构化特征优点是快缺点是信息量有限。如果你想把准确率再往上推可以引入页面内容特征HTML 里的表单数量、iframe 数量、外部脚本域名数量、是否有混淆 JavaScript、页面文本的 TF-IDF 向量。这些特征需要实际抓取页面内容速度会慢很多但区分度更高。特征类别具体特征提取方式对模型的增益URL 结构长度、特殊字符数、数字占比正则匹配基础特征所有模型都需要域名特征域名年龄、注册商、是否 IP 直连WHOIS 查询对随机森林增益明显页面内容表单数、iframe 数、外部脚本数BeautifulSoup 解析对 DNN 增益明显文本特征TF-IDF 向量、关键词命中jieba 分词 TfidfVectorizer需要较大数据量才能体现扩展特征的时候注意维度灾难SVM 在维度超过样本量时表现会下降随机森林对高维稀疏特征的处理也不如 DNN。我一般会先用随机森林做一次特征重要性筛选保留 top 20 的特征再喂给 SVM 和 DNN。5.3 模型融合三算法投票比单算法更稳三种算法各有各的翻车场景SVM 对噪声敏感随机森林对相关性高的特征不稳定DNN 需要大量数据。把它们的结果做投票融合往往比单算法更稳。常见做法是软投票概率平均或硬投票多数表决。from sklearn.ensemble import VotingClassifier # 假设 svm_clf、rf_clf、dnn_model 已经训练好 # 这里用 sklearn 的 VotingClassifier 做软投票示例 voting_clf VotingClassifier( estimators[ (svm, SVC(kernelrbf, C10, gammascale, probabilityTrue, random_state42)), (rf, RandomForestClassifier(n_estimators100, random_state42)), (sgd, SGDClassifier(losslog_loss, random_state42)) ], votingsoft # 软投票按概率加权 ) voting_clf.fit(X_train_svm, y_train) y_pred_vote voting_clf.predict(X_test_svm) print(融合模型准确率:, accuracy_score(y_test, y_pred_vote))votingsoft要求每个基模型都能输出概率SVM 需要设probabilityTrue会慢一些随机森林天然有predict_probaSGDClassifier用losslog_loss也有概率输出。软投票比硬投票更稳因为它保留了模型的置信度信息。如果三个模型准确率差距很大比如 SVM 95%、随机森林 90%、DNN 85%那融合的时候可以给 SVM 更高的权重VotingClassifier的weights参数支持这个。从那以后我每次跑这类多算法对比实验都会先把标签映射和归一化流程单独写一个check_pipeline.py跑一遍确认数据没泄露、标签没反再开始训练。这套代码的模块拆分已经帮你省了很多事但数据管线上的坑还是得自己踩一遍才记得住。希望帮到你。本文还有配套的精品资源点击获取