ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于DNN的恶意网站检测:Python特征工程与模型训练实战

基于DNN的恶意网站检测:Python特征工程与模型训练实战 简介这份资源面向计算机、信息安全及相关专业学生与入门开发者提供一套基于传统机器学习DNN算法的恶意网站检测完整项目可直接用于课程设计、期末大作业或自学练手。压缩包共7个文件以5个Python源码文件为主涵盖数据处理、特征转换、SVM与DNN模型实现及随机森林划分等模块另附1个数据压缩包和1份README说明文档整体约3.31MB结构清晰、便于按模块阅读与复现。项目已通过导师指导并获得97分评价下载后无需修改即可运行适合作为机器学习分类任务的实战参考。读者可从中了解恶意网站检测的完整流程包括数据准备、特征工程、模型搭建与对比实验并借鉴代码组织方式与调参思路。目前已有446人学习可作为课程作业或入门项目的可靠起点。1. 恶意网站检测的DNN方案从数据集到可复现的Python源码恶意网站检测这件事很多人第一反应是上深度学习大模型、上Transformer但真到期末大作业或者中小规模落地场景里传统机器学习配合DNN深度神经网络反而是性价比最高的选择。原因很直接URL和网页源码这类文本特征维度高、样本量往往在几万到几十万级别DNN在CPU上就能跑训练时间可控调参空间清晰答辩时也讲得清楚。这套方案要解决的问题是给定一批标注好的恶意/正常网站样本用Python构建特征工程管线训练一个DNN分类器最终输出可复用的模型文件和预测脚本。适合谁正在做机器学习期末大作业的学生、需要快速搭建网站安全过滤原型的后端工程师、以及想理解「传统特征工程神经网络」这条技术路线的从业者。下面从数据、特征、模型、训练、避坑到进阶一步步拆开讲。2. 数据集结构与特征工程URL里到底能挖出什么2.1 恶意网站数据集的典型字段与标签分布常见做法是找一份公开的恶意URL数据集比如URLhaus、PhishTank的导出数据或者Kaggle上标注好的malicious URLs数据集。这类数据集通常包含两列核心字段url完整URL字符串和label0表示正常1表示恶意。有些版本还会附带type字段区分钓鱼、恶意软件、垃圾邮件等子类。我一般会先做一次标签分布统计因为恶意样本和正常样本的比例往往不是1:1常见在1:3到1:10之间。如果直接用原始比例训练模型会偏向多数类准确率看着高但召回率惨不忍睹。import pandas as pd df pd.read_csv(malicious_urls.csv) print(df[label].value_counts(normalizeTrue)) # 典型输出0: 0.78, 1: 0.22逻辑说明value_counts(normalizeTrue)返回各类别占比。如果恶意类占比低于20%后续训练时必须用class_weight或过采样处理。参数上normalizeTrue比手动除以总数更省事也不会因为数据量大而精度丢失。2.2 从URL字符串提取数值特征的六个维度DNN不能直接吃字符串必须先把URL转成定长数值向量。我一般从六个维度提取特征长度类URL总长度、域名长度、路径长度、字符统计类数字个数、特殊字符个数、大写字母个数、结构类是否含IP地址、子域名层数、是否用短链接、协议类是否HTTPS、端口号是否非标准、关键词类是否含login、verify、update等敏感词、熵值类URL字符的香农熵。这六类加起来大约20到30维足够DNN学到有效模式。import re import math from urllib.parse import urlparse def extract_features(url): parsed urlparse(url) hostname parsed.hostname or path parsed.path or features {} features[url_len] len(url) features[host_len] len(hostname) features[path_len] len(path) features[digit_count] sum(c.isdigit() for c in url) features[special_count] len(re.findall(r[^a-zA-Z0-9], url)) features[upper_count] sum(c.isupper() for c in url) features[has_ip] 1 if re.match(r\d\.\d\.\d\.\d, hostname) else 0 features[subdomain_depth] hostname.count(.) - 1 if hostname else 0 features[is_https] 1 if parsed.scheme https else 0 features[sensitive_word] 1 if re.search(rlogin|verify|update|secure|account, url, re.I) else 0 prob [url.count(c) / len(url) for c in set(url)] if url else [0] features[entropy] -sum(p * math.log2(p) for p in prob if p 0) return features逻辑说明urlparse负责拆解URL结构re.findall统计非字母数字字符数量香农熵衡量字符分布的随机性——恶意URL常用随机字符串混淆熵值偏高。参数上re.I让敏感词匹配不区分大小写hostname.count(.) - 1计算子域名层数时要减掉主域名本身的那一个点。注意urlparse对畸形URL可能返回空hostname所以每处都要做空值保护。2.3 特征归一化与训练集划分的注意事项提取完特征后各维度量纲差异很大url_len可能上百has_ip只有0或1。DNN对输入尺度敏感必须做归一化。常见做法是StandardScaler或MinMaxScaler。我一般用StandardScaler因为它对异常值更稳健。划分训练集和测试集时务必用stratifyy保持标签比例一致否则小样本类可能在测试集里只剩个位数。from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler X df[url].apply(extract_features).apply(pd.Series) y df[label] X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42, stratifyy ) scaler StandardScaler() X_train_scaled scaler.fit_transform(X_train) X_test_scaled scaler.transform(X_test)逻辑说明apply(pd.Series)把字典列表展开成DataFrame。stratifyy保证训练集和测试集的标签比例与原始数据一致。关键点scaler只能在训练集上fit测试集用transform否则数据泄漏会让评估结果虚高。random_state42保证可复现答辩时别人跑你的代码能得到同样结果。3. DNN模型搭建与训练Keras还是PyTorch3.1 用Keras搭一个三隐层DNN的最小可用版本选Keras还是PyTorch期末大作业场景我推荐Keras因为代码量少、API直观、训练循环不用自己写。PyTorch更适合需要自定义损失函数或复杂训练逻辑的场景。这里给一个三隐层的DNN输入维度等于特征数三层全连接分别是64、32、16个神经元激活函数用ReLU输出层用Sigmoid做二分类。Dropout加在每层之后防止过拟合。import tensorflow as tf from tensorflow.keras import layers, models def build_dnn(input_dim): model models.Sequential([ layers.Dense(64, activationrelu, input_shape(input_dim,)), layers.Dropout(0.3), layers.Dense(32, activationrelu), layers.Dropout(0.3), layers.Dense(16, activationrelu), layers.Dense(1, activationsigmoid) ]) model.compile( optimizertf.keras.optimizers.Adam(learning_rate0.001), lossbinary_crossentropy, metrics[accuracy, tf.keras.metrics.AUC(nameauc)] ) return model model build_dnn(X_train_scaled.shape[1]) model.summary()逻辑说明input_shape(input_dim,)必须和特征数一致否则第一层就报错。Dropout(0.3)表示训练时随机丢弃30%神经元推理时关闭。Adam学习率设0.001是常见起点如果loss震荡就降到0.0005。binary_crossentropy是二分类标准损失。AUC指标比单纯accuracy更能反映类别不平衡下的表现。3.2 训练参数怎么设epoch、batch_size与早停训练参数没有万能值但有一套可靠的起点epochs50、batch_size64、加EarlyStopping监控val_loss、patience5。如果训练集超过10万条batch_size可以提到128或256。validation_split0.1从训练集里再切10%做验证不占用测试集。early_stop tf.keras.callbacks.EarlyStopping( monitorval_loss, patience5, restore_best_weightsTrue ) history model.fit( X_train_scaled, y_train, epochs50, batch_size64, validation_split0.1, class_weight{0: 1.0, 1: 3.0}, callbacks[early_stop], verbose1 )逻辑说明class_weight{0:1.0, 1:3.0}是应对类别不平衡的关键——恶意类权重设为正常类的3倍具体倍数按标签比例反比来算。restore_best_weightsTrue让训练结束后模型回到验证loss最低的那一轮省得手动保存。patience5表示验证loss连续5轮不降就停避免浪费时间。如果显存或内存吃紧把batch_size降到32但训练会慢一些。3.3 模型评估混淆矩阵比准确率更有说服力训练完别只看accuracy。恶意网站检测场景里漏报把恶意判成正常的代价远高于误报。所以评估要看召回率Recall和F1-score混淆矩阵能直观展示四类结果。from sklearn.metrics import classification_report, confusion_matrix y_pred_prob model.predict(X_test_scaled) y_pred (y_pred_prob 0.5).astype(int) print(confusion_matrix(y_test, y_pred)) print(classification_report(y_test, y_pred, digits4))逻辑说明y_pred_prob 0.5是默认阈值实际部署时可以调低到0.3来提高召回率代价是误报增多。classification_report输出precision、recall、f1-score重点看label1那一行的recall。如果recall低于0.85说明模型对恶意样本学得不够需要加特征或调class_weight。4. 避坑与排查训练过程中最容易翻车的五个点4.1 特征提取时URL解码不一致导致训练测试分布偏移现象训练集准确率0.95测试集掉到0.70。原因训练数据里的URL是原始编码测试数据经过了一次urllib.parse.unquote解码导致同一类URL提取出的特征值不同。解决在extract_features函数开头统一做一次unquote或者统一都不做关键是训练和推理走同一条管线。我一般把特征提取封装成单独模块训练和预测都import同一个函数。4.2 标签泄漏把URL本身当特征喂进模型现象模型准确率99%但换一批新数据就崩。原因有人把URL字符串用LabelEncoder转成整数后直接当特征模型学到了具体URL和标签的对应关系而不是泛化模式。解决URL字符串绝不能直接作为数值特征必须经过特征工程转成统计量。检查方法是看特征列表里有没有url_encoded这类字段。4.3 过采样后忘记重新划分验证集现象用了SMOTE过采样训练集和验证集准确率都高测试集一塌糊涂。原因SMOTE在划分训练集之前对整个数据集做了过采样导致合成样本同时出现在训练和验证集中。解决先train_test_split再只对训练集做SMOTE。顺序不能反。4.4 Dropout率设太高导致欠拟合现象训练loss降到0.5就降不动了训练集准确率只有0.80。原因Dropout设了0.6甚至0.7神经元被丢弃太多模型容量不足。解决Dropout从0.2到0.4之间试超过0.5通常弊大于利。如果特征维度本身就不高比如只有20维Dropout可以降到0.1甚至不用。4.5 学习率太大导致loss震荡不收敛现象训练loss在0.3到0.8之间来回跳始终不下降。原因Adam默认学习率0.001对某些数据偏大。解决降到0.0005或0.0001或者加ReduceLROnPlateau回调让学习率在验证loss停滞时自动减半。lr_scheduler tf.keras.callbacks.ReduceLROnPlateau( monitorval_loss, factor0.5, patience3, min_lr1e-6 )逻辑说明factor0.5表示每次触发时学习率乘以0.5patience3表示验证loss连续3轮不降就触发min_lr1e-6是下限防止学习率降到零。5. 模型保存、加载与批量预测的工程化收尾5.1 保存完整模型与只保存权重的区别Keras提供两种保存方式model.save(model.h5)保存结构和权重model.save_weights(weights.h5)只保存权重。部署时推荐保存完整模型加载后直接predict不用重新定义网络结构。如果特征提取管线有改动记得同步更新模型输入维度否则加载后predict会报shape不匹配。model.save(malicious_dnn.h5) loaded_model tf.keras.models.load_model(malicious_dnn.h5) print(loaded_model.input_shape) # 确认输入维度逻辑说明input_shape输出类似(None, 24)24是特征数。如果换了一批特征工程代码导致特征数变成26加载旧模型就会报错。所以模型文件和特征提取代码要版本对应。5.2 写一个批量预测脚本从CSV到带标签的输出实际使用时不会在notebook里一条条预测而是读CSV批量跑。下面这个脚本读入含url列的CSV输出原URL加预测标签和概率。import pandas as pd import tensorflow as tf from feature_extractor import extract_features # 复用训练时的函数 from sklearn.preprocessing import StandardScaler model tf.keras.models.load_model(malicious_dnn.h5) scaler StandardScaler() # 注意scaler的均值方差必须和训练时一致实际项目用joblib保存加载 # 这里假设重新fit会引入偏差正确做法是训练时保存scaler def predict_csv(input_path, output_path): df pd.read_csv(input_path) X df[url].apply(extract_features).apply(pd.Series) X_scaled scaler.fit_transform(X) # 仅演示生产环境应加载训练时的scaler probs model.predict(X_scaled).flatten() df[malicious_prob] probs df[pred_label] (probs 0.5).astype(int) df.to_csv(output_path, indexFalse) return df result predict_csv(new_urls.csv, predicted.csv) print(result[[url, malicious_prob, pred_label]].head(10))逻辑说明flatten()把shape从(N,1)压成(N,)方便赋值给DataFrame列。阈值0.5可以按业务需求调整比如安全场景调到0.3提高召回。关键提醒生产环境必须用joblib.dump(scaler, scaler.pkl)保存训练时的scaler预测时joblib.load加载绝不能重新fit否则归一化参数不一致预测结果全错。5.3 用joblib保存归一化器避免预测翻车接上一条正确做法是在训练阶段就保存scaler。import joblib # 训练阶段 joblib.dump(scaler, scaler.pkl) # 预测阶段 scaler joblib.load(scaler.pkl) X_scaled scaler.transform(X) # 注意是transform不是fit_transform逻辑说明transform用训练时学到的均值和方差做归一化fit_transform会重新计算两者结果不同。这个坑我见过太多次血泪经验就是任何在训练阶段fit过的对象预测阶段都必须加载而不是重建。5.4 阈值调优用PR曲线找最佳切分点默认0.5阈值不一定最优。用precision_recall_curve画出不同阈值下的precision和recall选一个业务上可接受的平衡点。from sklearn.metrics import precision_recall_curve probs model.predict(X_test_scaled).flatten() precision, recall, thresholds precision_recall_curve(y_test, probs) f1_scores 2 * precision * recall / (precision recall 1e-8) best_idx f1_scores.argmax() best_threshold thresholds[best_idx] print(f最佳阈值: {best_threshold:.4f}, F1: {f1_scores[best_idx]:.4f})逻辑说明1e-8防止除零。thresholds的长度比precision少1所以best_idx要对应好。找到最佳阈值后在预测脚本里把0.5替换成这个值。如果业务更看重召回可以手动选一个recall更高但precision略低的阈值。6. 进阶技巧用特征重要性反推模型学到了什么DNN常被叫黑匣子但用Permutation Importance可以大致看出哪些特征对预测贡献大。做法是在测试集上逐个把某一列特征打乱看AUC下降多少下降越多说明该特征越重要。这个技巧在答辩时特别有用老师问「你的模型为什么有效」你可以直接展示特征重要性排序。from sklearn.metrics import roc_auc_score import numpy as np baseline_auc roc_auc_score(y_test, model.predict(X_test_scaled).flatten()) importances {} for i, col in enumerate(X.columns): X_permuted X_test_scaled.copy() np.random.shuffle(X_permuted[:, i]) perm_auc roc_auc_score(y_test, model.predict(X_permuted).flatten()) importances[col] baseline_auc - perm_auc sorted_imp sorted(importances.items(), keylambda x: x[1], reverseTrue) for name, imp in sorted_imp[:10]: print(f{name}: {imp:.4f})逻辑说明np.random.shuffle原地打乱某一列其他列不变。baseline_auc - perm_auc越大说明该特征越关键。常见结果是url_len、entropy、sensitive_word排在前列这符合直觉——恶意URL往往更长、更随机、含诱导词。如果某个特征重要性是负数说明打乱后AUC反而升了通常是噪声特征可以考虑删掉。最后说个习惯我每次做完一个模型都会把特征提取、训练、评估、预测四个阶段拆成独立脚本用同一个feature_extractor.py贯穿始终。这样换数据集时只改数据加载部分特征和模型代码不动。期末大作业答辩时老师让你现场换一批URL预测你直接跑预测脚本就行不用临时改代码。这个习惯帮我省了无数次返工。希望帮到你。本文还有配套的精品资源点击获取
返回列表