
简介面向网络安全与深度学习方向的毕业设计及课程作业以域名生成算法DGA检测为实战主题解决恶意域名绕过传统签名检测的实际问题完整覆盖从数据分析、模型训练到系统集成的流程。资源共19个文件包含12个Python脚本分工清晰数据处理模块完成域名向量化与特征提取模型模块实现BiLSTM及注意力机制训练评估模块封装准确率、召回率等指标3个Jupyter Notebook演示数据探索与实验过程2篇PDF论文为算法设计提供理论支撑。压缩包仅1.43MB结构紧凑。已有226人学习下载。借助该项目可快速掌握深度学习在安全领域的落地方法包括NLP文本表示、序列模型构建、过拟合抑制、类别不平衡评估AUC并了解Python与C混合编程的系统集成思路代码模块化便于复用数据预处理和实验设计均有清晰注释可直接扩展用于其他序列分类任务适合作为计算机相关专业毕业设计或课程作业的完整参考。1. 基于深度学习的DGA检测毕设选题为什么扎堆在这“基于深度学习的DGA检测”描述的是一类二分类任务输入一个域名判断它是正常域名还是恶意程序用域名生成算法批量产出的C2域名。DGA是僵尸网络和勒索软件对抗黑名单封禁的常规手段恶意家族每天能变出百万级候选域名安全情报库的收录速度永远追不上。深度学习模型则能从字符分布、长度、随机性等角度自动学习两类域名的差异在域名进入情报库之前就给出判断。对毕设和课程作业而言这个选题的优势很明显数据有公开来源、模型结构成熟、效果可量化没有GPU也能在CPU上跑完整轮实验。正在选毕设题目的网安方向学生以及想快速上手一个完整深度学习项目的学习者都适合从这个方向切入。2. 数据与特征工程决定模型上限的是这一步模型结构决定下限数据集决定上限。我在帮学生调DGA检测项目时见过太多案例模型换了好几个结构准确率都上不去最后定位到是标签错了几百条、训练集和测试集混了样本、域名清洗少做了两步。所以先用一整章把数据讲清楚后面写代码才不至于反复返工。2.1 恶意样本与正常样本数据来源、清洗与配比恶意域名样本主要来自两类渠道。一类是学术公开数据集比较常用的是DGArchive按恶意软件家族分类存放了多年的DGA域名包含每个域名的首次出现时间适合做时间线切分实验另一类是安全厂商每天发布的威胁情报文件比如Netlab 360和Bambenek维护的DGA实时列表域名按天滚动更新适合拿来验证模型在“新产生域名”上的表现。正常域名样本的常见做法是取Alexa Top 1M或Cisco Umbrella Top 1M的当日榜单截取前20万到50万条当作正常类。拿到原始数据后清洗这步不能跳。第一域名统一小写去掉协议头和路径只保留“子域名.二级域名.顶级域名”的主体结构结尾的单点要去掉否则同一个域名会被拆成两条记录。第二去重要按完整域名而不是主域一个正常主域下面可能挂几万个不同子域按主域去重会把正常子域的多样性砍掉一半以上。第三恶意样本不够时宁可合并更多公开数据集也不要自己拼随机字符串硬凑那样模型学出来的“DGA特征”是假的。配比策略分两个阶段考虑。训练阶段恶意与正常按1比1或1比2即可让两类的梯度贡献均衡评估阶段另外构造一个恶意样本只占5%左右的测试集模拟真实DNS流量重点观察低误报率条件下的召回率。只有这样做出来的指标答辩时才经得起追问。提示公开数据集的格式差异很大有的给了域名和家族名有的只有一列域名。合并前先统一列名别在数据处理脚本里写死文件名和编码格式用utf-8和csv.DictReader这类通用方式读入。2.2 字符级编码与序列长度把域名变成模型能读的数值张量域名是变长字符序列深度学习模型只接受固定维度的数值张量所以第一步是建字符表、写编码器。字符表建议包含小写字母a到z、数字0到9、连字符和点号共38个有效字符单独留索引0给padding留一个未知字符索引给表外字符。点号和连字符必须保留因为正常域名与DGA域名在连字符出现位置、数字密度上的差异很明显去掉等于扔特征。基于字符表的映射代码不复杂但有一个点值得强调字符表生成后要存成json文件训练和推理必须共用同一张表。我见过最典型的返工场景是训练脚本里字符表用字符串拼接生成推理时换台机器重新拼顺序变了所有索引全部错位模型加载后预测结果等于随机猜查半天也找不到原因。# vocab.py — 字符表构建与保存 import json CHARS abcdefghijklmnopqrstuvwxyz0123456789.- # 38个有效字符 char2idx {ch: i 1 for i, ch in enumerate(CHARS)} # 0留给padding char2idx[UNK] len(char2idx) 1 # 未知字符兜底 with open(char2idx.json, w) as f: json.dump({char2idx: char2idx, chars: CHARS}, f) def encode_domain(domain: str, max_len: int 75) - list: domain domain.strip().lower().rstrip(.) seq [char2idx.get(ch, char2idx[UNK]) for ch in domain[:max_len]] seq [0] * (max_len - len(seq)) # padding到定长 return seqencode_domain做了三件事小写归一化、按字符映射到整数、用0补齐到75。为什么是75域名理论最长253字符但实际完整域名超过75的很少而LSTM按时间步展开序列越长训练越慢截断到75能覆盖绝大多数样本同时把计算开销压在合理范围内。CNN类模型可以放宽到100因为卷积对长度不敏感。实际编码时注意把域名里的“www.”保留下来——DGA域名几乎不会以www开头这个信息对分类器是有效的判别特征。2.3 训练/验证/测试划分家族隔离与时间线切分这是整个数据准备里最容易被忽视、又最影响论文可信度的环节。很多初学者直接对所有样本做一次随机切分结果同一家族的多个变种域名同时出现在训练集和测试集里。模型其实记住的是某个家族的“指纹”换一个从未见过的家族就失效这在答辩时是致命的。正确做法是按域名所属家族做分组隔离同一个家族的样本只能进入训练、验证、测试三者之一。代码上可以用sklearn的GroupShuffleSplit。另一个加分项是时间线切分如果数据带时间戳训练集用前80%时间段测试集用后20%模拟“模型上线后去识别新域名”的真实场景。# split.py — 家族隔离与时间线切分 import pandas as pd from sklearn.model_selection import GroupShuffleSplit # df需要包含三列: domain(清洗后), family(家族名), timestamp(首现时间) df df.sort_values(timestamp).reset_index(dropTrue) # 第一阶段按时间切分训练80% / 测试20% split_idx int(len(df) * 0.8) train_df df.iloc[:split_idx].copy() test_df df.iloc[split_idx:].copy() # 第二阶段训练集内按家族分组切出验证集 gss GroupShuffleSplit(n_splits1, test_size0.15, random_state42) train_idx, val_idx next(gss.split(train_df, groupstrain_df[family])) val_df train_df.iloc[val_idx].copy() train_df train_df.iloc[train_idx].copy()这段代码先按时间排序切出测试集再在训练数据内部按family分组切验证集。random_state设固定值保证论文结果可以被复现。这里有个坑要留意GroupShuffleSplit切出的验证集与训练集家族不重叠但时间仍在同一区间如果某类家族演进很快验证集指标还是会略好于真实部署。想更严格就让验证集和测试集在家族、时间两个维度都做到不重叠这属于加分项论文里写清楚划分方式即可。3. 模型选型与对比LSTM、CNN与对比实验的设计到模型环节先明确一件事DGA检测的输入是字符序列输出是二分类本质上是文本分类任务的一个特殊子集。文本分类里成熟的结构都可以借鉴但DGA域名有自己的特点——序列短、模式稀疏、语义信息弱模型结构过深反而容易过拟合。3.1 LSTM为何是DGA检测的默认基线LSTM天然适合变长序列建模这是它成为该任务默认基线的第一理由。域名内部的字符存在前后依赖关系比如DGA域名经常出现连续三个辅音字母、数字穿插在中间、或者缺少元音的高熵片段LSTM的遗忘门和输入门可以选择性保留这类局部模式。配合双向结构模型能同时看到字符左侧和右侧的上下文对“这个字符在这类域名中是否常见”的判断比单向更稳。参数配置上有一个常见默认组合embedding维度64或128LSTM隐藏层维度128或256层数2层dropout取0.3到0.5。不要把层数加到4层以上——域名序列最长不过75个字符堆到3层以上训练时间成倍增加在小数据集上很容易过拟合验证集指标反而掉下来。提示LSTM每加一层反向传播路径就加深一条梯度消失和训练时长同时恶化。毕设项目里2层是性价比最高的选择双向比单向在DGA任务上通常能带来2到4个百分点的F1提升代价是参数量和推理时间翻倍值不值要看你的算力约束。3.2 CNN替代方案把域名当作n-gram特征视图CNN的切入点是把域名视作一维信号用不同宽度的卷积核提取局部n-gram特征。宽度为3的卷积核等价于看连续3个字符的组合模式宽度为5的核相当于看连续5个字符。DGA域名和正常域名在局部字符组合上的分布差异正是CNN能抓住的判别信息。相比LSTMCNN的优势是训练速度快、显存占用小CPU上也能跑劣势是不擅长捕捉长距离依赖但域名本身长度有限长距离依赖并不丰富所以CNN在DGA检测上的精度通常和LSTM在同一水平训练时间却能少一半以上。毕设如果对运行时间有硬约束或者想展示不同结构的对比实验CNN是很好的备选方案。实战配置建议embedding维度64卷积核宽度取[2,3,4]各配64个通道接全局最大池化把三个尺度的特征拼接后过全连接层。这个结构在PyTorch里实现不超过20行还能和LSTM组成一组干净的对比实验。模型参数量量级训练耗时适合场景双向LSTM(2层)数十万中序列特征明显、论文基线多尺度CNN数十万快资源受限、追求线上推理速度LSTMAttention百万以内慢需要展示创新点的改进模型3.3 深度学习对比试验怎么做基线、变体与消融的论文结构毕设论文的对比实验是否可信取决于设计方式。常见做法是拉四个模型一个传统机器学习基线随机森林加手工统计特征、一个LSTM基线、一个CNN方案、一个主推的改进模型比如带注意力机制的双向LSTM。对比维度固定在准确率、召回率、F1、PR-AUC和单条推理耗时五项上控制变量是训练数据、超参数和随机种子一致。消融实验要提前设计目的是证明主推模型里每个模块都有效。假设你的模型是“双向LSTM加注意力”至少要做三组消融去掉注意力、去掉双向、两者都去掉逐项对比指标变化。这组实验工作量不大但能直接回答“你的改进点到底有没有用”这个问题。训练时记得固定随机种子否则每次结果波动超过0.5个百分点对比表就没法解释了。4. 用PyTorch跑通DGA检测预处理、训练与评估的最小实现前两章把数据和模型结构定了这一章给一个能直接跑通的PyTorch最小闭环依赖只有torch、pandas、scikit-learn三件套。深度学习环境配置的细节CUDA、驱动、cudnn这里不展开只提醒一句CPU训练完全够用PyTorch的CPU版本安装最省心不必为了这个项目一开始就折腾GPU驱动。4.1 数据预处理代码Dataset类与DataLoader封装数据从csv读进来之后先做清洗和编码再交给Dataset和DataLoader。Dataset类负责按索引取样本DataLoader负责组batch和打乱顺序。标签用float类型是为了直接套用BCEWithLogitsLoss。# dataset.py — 域名Dataset封装 import torch from torch.utils.data import Dataset class DomainDataset(Dataset): def __init__(self, domains, labels, max_len75): self.domains domains # 已编码的二维list: [样本数, 序列长度] self.labels labels # 1恶意, 0正常 self.max_len max_len def __len__(self): return len(self.domains) def __getitem__(self, idx): seq self.domains[idx][:self.max_len] seq seq [0] * (self.max_len - len(seq)) # 长度不足补0 return (torch.tensor(seq, dtypetorch.long), torch.tensor(self.labels[idx], dtypetorch.float))这里假设传入的domains已经是encode_domain处理过的整数列表Dataset只做截断和补齐。训练时batch由DataLoader自动叠成[batch, seq_len]的张量batch_firstTrue在模型里已经约定好维度顺序二者保持一致才不会传错。4.2 模型定义代码Embedding 双向LSTM 分类头模型结构分三段embedding层把字符索引映射为稠密向量双向LSTM提取序列特征全连接分类头输出logits。注意分类头最后不接sigmoid训练时用BCEWithLogitsLoss自带sigmoid计算数值更稳定。# model.py — 双向LSTM分类器 import torch.nn as nn class DGALSTM(nn.Module): def __init__(self, vocab_size, embed_dim64, hidden_dim128, num_layers2, dropout0.3, padding_idx0): super().__init__() self.embedding nn.Embedding(vocab_size, embed_dim, padding_idxpadding_idx) self.lstm nn.LSTM( embed_dim, hidden_dim, num_layers, batch_firstTrue, bidirectionalTrue, dropoutdropout ) self.head nn.Sequential( nn.Linear(hidden_dim * 2, 64), nn.ReLU(), nn.Dropout(dropout), nn.Linear(64, 1) # 输出裸logits ) def forward(self, x): emb self.embedding(x) # (batch, seq_len, embed_dim) out, _ self.lstm(emb) # (batch, seq_len, hidden_dim*2) last out[:, -1, :] # 取最后一个时间步 return self.head(last).squeeze(1) # (batch,)参数说明padding_idx0让embedding层对padding位输出全零向量不参与梯度更新bidirectionalTrue让输出维度变成hidden_dim的两倍所以分类头第一个Linear的输入是hidden_dim*2。取out[:, -1, :]作为序列表示等价于取最后一个字符位置的状态但双向LSTM的最后一个时间步已经聚合了整条序列的信息实际效果与取最后隐状态拼接差别不大代码上更简洁。4.3 训练循环与调参细节学习率、Batch Size、Loss与早停训练循环的核心是三个固定动作前向算loss、反向传播、优化器更新。DGA检测是二分类loss用BCEWithLogitsLoss。早停策略务必加上否则跑到最后一个epoch大概率过拟合。# train.py — 训练主循环 import torch import torch.nn as nn from torch.utils.data import DataLoader from sklearn.metrics import f1_score train_loader DataLoader(train_dataset, batch_size64, shuffleTrue) val_loader DataLoader(val_dataset, batch_size64, shuffleFalse) model DGALSTM(vocab_sizelen(char2idx) 1) optimizer torch.optim.Adam(model.parameters(), lr1e-3) criterion nn.BCEWithLogitsLoss() best_f1 0.0 patience 0 for epoch in range(30): model.train() total_loss 0.0 for x, y in train_loader: optimizer.zero_grad() logits model(x) loss criterion(logits, y) loss.backward() optimizer.step() total_loss loss.item() # 验证 model.eval() preds, labels [], [] with torch.no_grad(): for x, y in val_loader: logits model(x) prob torch.sigmoid(logits) preds.extend((prob 0.5).int().tolist()) labels.extend(y.tolist()) f1 f1_score(labels, preds) print(fEpoch {epoch 1:02d} | loss {total_loss / len(train_loader):.4f} | F1 {f1:.4f}) # 早停: 连续5轮F1没有提升就停 if f1 best_f1: best_f1 f1 torch.save(model.state_dict(), best_model.pt) patience 0 else: patience 1 if patience 5: print(early stopping) break一点调参经验lr1e-3是Adam的常规起点loss震荡不降时先降到3e-4不要一次性降到1e-5。batch size在CPU上建议32GPU可以用64或128但DGA样本量通常不大batch过大会让收敛变慢。epoch上限30配合早停一般10到15轮就能达到稳定要不要调大epoch取决于验证集的F1曲线是否还有上升趋势。超参数建议值调参方向embed_dim64数据量大可升到128hidden_dim128显存充足可升到256num_layers21层欠拟合3层以上过拟合learning_rate1e-3不收敛则降为3e-4batch_size32~64CPU用32GPU可64max_len75覆盖绝大多数域名即可4.4 评估指标与阈值选择F1、PR曲线与混淆矩阵准确率在类别不平衡的测试集上会骗人。恶意样本只占5%时模型全部预测正常也能拿到95%的准确率但没有任何检测价值。更合适的指标是F1、召回率和PR-AUC。PR曲线专门用于正样本稀少的二分类场景比ROC曲线更能反映真实性能。# evaluate.py — 阈值扫描与PR-AUC计算 import numpy as np from sklearn.metrics import precision_recall_curve, auc with torch.no_grad(): test_logits model(test_x).squeeze() test_probs torch.sigmoid(test_logits).numpy() precision, recall, thresholds precision_recall_curve(test_y, test_probs) pr_auc auc(recall, precision) # 在验证集上找F1最高的阈值而不是默认0.5 f1_scores 2 * precision * recall / (precision recall 1e-9) best_idx np.argmax(f1_scores[:-1]) # thresholds长度比分隔点少1 best_threshold thresholds[best_idx] print(fbest threshold {best_threshold:.3f}, F1 {f1_scores[best_idx]:.4f}, PR-AUC {pr_auc:.4f})阈值默认0.5不一定是F1最优位置。实际部署中误报率和漏报率的代价不同比如误报一个正常域名最多是DNS日志里多一条告警漏报一个DGA域名可能意味着僵尸网络已经在内网活动。最终阈值取决于项目定义的代价函数把验证集上扫出来的最佳threshold保存下来推理时直接使用。5. 毕设避坑DGA检测项目常见的5个翻车现场这一章的目的是把反复出现的坑整理出来。每个坑按“现象、原因、解决”的方式写遇到类似问题时可以直接对号入座。5.1 现象验证集准确率99%换一批数据直接打回原形原因训练集和测试集划分时没有按家族隔离同一家族的变体同时出现在两边模型学到的是家族指纹而不是通用的DGA特征。另一个常见原因是数据清洗不彻底测试集里残留了大量与训练集完全相同的域名。解决回到2.3节做分组隔离和时间线切分。换数据后重点观察模型在“未知家族”上的召回率而不是总准确率。如果未知家族召回率掉到60%以下说明泛化能力没有建立起来优先增加恶意样本家族多样性而不是扩大模型。5.2 现象训练loss不降准确率卡在某个值原因常见三种。学习率过大导致loss在最优值附近震荡字符表编码错误多个字符映射到同一索引embedding输出区分不开数据标签错位恶意样本的label传成了0。解决把lr从1e-3降到3e-4重跑5个epoch观察。检查char2idx映射是否一一对应。然后随机抽20条训练数据人工核对domain和label是否对齐。这类问题半小时内就能定位基本不用怀疑模型结构——LSTM做这个任务不会结构性不收敛。5.3 现象正常域名误报率奇高模型变成“宁杀错不放过”原因训练数据配比失衡恶意样本远多于正常样本模型收敛到“全部预测为恶意”的局部最优。或者正常样本来源单一比如只用Alexa榜单前1000的域名缺少长尾正常域名导致模型没有见过足够多样的正常模式。解决训练配比调回1比1或1比2同时扩充正常域名样本的多样性加入排名靠后的长尾域名。评测时把决策阈值往高调比如0.7到0.9误报率会明显下降代价是漏报率上升具体选哪个阈值取决于你项目对两类错误的代价定义。5.4 现象显存不够或训练缓慢代码跑不起来原因序列长度设置过长、batch size过大、LSTM的num_layers设置过高三者叠加显存成倍上涨。很多同学在同一台机器上先跑CNN再直接跑LSTM内存直接溢出。解决max_len限制在75以内batch size从32开始hidden_dim设为64或128LSTM层数不超过2。如果仍OOM用torch.utils.checkpoint对LSTM层做梯度检查点用少量额外计算换显存。实际上这个项目的数据规模8G显存的GPU上训练两万条样本完全没有压力重点是别一开始就把超参开太大。5.5 现象测试集指标虚高论文里的数字导师不认原因测试集和训练集样本存在时间重叠或者两个公开数据集合并后出现重复域名去重只在单一数据集内部做了。另一种常见情况是反复用测试集调参相当于把测试集信息间接泄漏给模型。解决数据集合并后以完整域名为key做全局去重。测试集只在最终评估时用一次调试过程用验证集。论文里写明两个关键数字训练集和测试集的时间窗口以及家族重叠情况。做到这三点评审通常不会再质疑你的实验设计。6. 进阶把训练好的模型封装成命令行检测工具模型训练完毕设如果只交付一组曲线和数据文件总感觉差一口气。花一个小时写一个CLI检测工具演示时输入一条命令就能对任意域名给出判断比PPT里的图表直观得多。PyTorch模型保存成state_dict之后推理逻辑可以封装成一个独立脚本。# 训练完成后用最佳阈值对单条域名做预测 python detect.py --checkpoint best_model.pt --domain asdkjhqwoeiruqpwoei.example.comdetect.py的内部流程加载保存的char2idx.json重建编码器实例化DGALSTM模型并load_state_dict然后对域名执行encode_domain、前向传播、sigmoid转概率最后按验证集扫描出的最优阈值输出“恶意”或“正常”。注意推理阶段的阈值要使用4.4节在验证集上扫出来的阈值而不是默认的0.5。更进一步可以把检测逻辑包成HTTP接口内部一次请求处理一个域名返回概率值和分类结果。这样演示时可以直接配一个简单的前端页面输入域名实时出结果老师的观感会比命令行好很多。接口封装不复杂用FastAPI写几十行就能跑起来关键是把模型加载和推理放在进程启动时一次性完成不要在每次请求时重新载入权重否则延迟高得没法看。回看整个项目数据隔离、字符表管理、阈值选择这三件事是DGA检测最容易出问题也最值得提前花时间的环节。训练一个模型本身不难难的是让每个实验环节可复现、让指标经得起追问。我的习惯是每完成一环实验就把数据和脚本归档一次附上命令行参数和运行日期答辩前能省下很多反复对数的痛苦希望帮到你。本文还有配套的精品资源点击获取