
简介一套面向计算机类毕业设计或课程作业的域名生成算法检测项目利用深度学习识别恶意软件生成的域名帮助抵御基于该算法的僵尸网络通信。资源围绕循环神经网络、长短时记忆网络及注意力机制展开覆盖数据预处理、特征工程、模型构建、训练评估等完整环节适合正在研究网络安全与深度学习交叉课题的学生参考。压缩包共含十九个文件其中十二个脚本负责模型定义、数据切分、训练评估与工具封装三个交互式笔记演示数据准备与探索性分析两篇论文提供算法背景与实验对比另有说明文档整体体积约一点四三兆字节目录按功能模块划分便于查找代码。目前已有二百二十六人学习。使用这份资源可以快速搭建双向长短时记忆网络检测基线复现论文中的实验流程同时借鉴其工程化系统集成方式、C性能优化与调参思路为后续改进、扩展或答辩展示打下扎实基础。1. DGA检测在做什么用一个二分类题目撑起毕设和课程作业某天翻防火墙日志看到内网主机每隔几分钟就在查询一串毫无语义的域名像qkzwxrhyoj.com、jfqynstbafd.top。这通常不是故障而是主机被植入恶意软件后正在按域名生成算法Domain Generation AlgorithmDGA批量生成新域名用来与远端的C2服务器保持联系。此时靠黑名单封禁已经失效因为域名每天都在换。基于深度学习的DGA检测正是把这个问题收敛成一个二分类任务给定一个域名判断它属于人工注册的正常域名还是算法随机生成的恶意域名。对毕设和课程作业来说这个题目数据可获得、实验可视化强、工作量适中不需要昂贵设备适合完整走一遍“数据处理—模型设计—训练调参—评估说明”的深度学习流程。2. 先备好数据和标签良性域名与恶意DGA家族的获取与切分做DGA检测的第一步不是建模型而是把训练数据准备到“能快速复现、能写进论文的数据清洗说明”的程度。这里面的坑比模型本身多得多。2.1 良性域名从哪来公开Top列表与流量日志的取舍常见做法是用公开的热门域名列表当良性样本。早期很多人用Alexa Top 1M但这份数据已经停更现在更常见的是Cisco Umbrella的Top Million列表或者Common Crawl里抽样出的域名。这类数据的优点是量大、稳定、标签干净——它们是被真实用户访问过的域名基本可以认定是良性的。另一种来源是机构自己的DNS访问日志能反映真实网络环境但数据往往涉及隐私课设阶段不容易拿到即使拿到也需要做匿名化。我一般建议起步阶段用公开Top列表的注册域名部分随机抽2万到5万条作为良性样本就足够跑通流程。注意要保留“注册域名”而不要太依赖子域比如a.b.example.com和c.example.com本质上属于同一个域名所有者训练时会让模型记住“example.com 长得像良性”这种偏置。简单做法是用tldextract库提取注册域再按注册域去重。import tldextract def normalize_domain(url: str) - str: ext tldextract.extract(url) registered f{ext.domain}.{ext.suffix} # 例如 example.com return registered.lower()这段代码解决的是“把完整URL收敛成注册域”。tldextract会处理www.example.co.uk这类多级公共后缀拿到example.co.uk而不是误取co.uk。参数上不需要额外配置只是要注意ext.suffix可能为空比如内网主机名nginx-server这种情况直接丢弃该样本。2.2 DGA样本从哪来公开家族样本 vs 自写生成器恶意DGA域名最可靠的来源是安全厂商和学术机构公开的DGA家族样本集比较常见的有360开源的DGA家族样本以及DGArchive这类学术项目整理的家族数据。每个样本通常带域名、DGA家族名、生成时间等字段。这类数据的价值在于一个数据集里覆盖了多个家族的生成策略有的家族生成纯随机字符串有的生成两个单词的拼接有的混合数字与字母模型在这种多样性下学到的才是“不像正常注册域名”的通用分布而不是背下某个特定家族的模式。另一种做法是自写生成器。部分已知DGA家族的生成算法在公开文献中有描述为了做对照实验可以复现一套。注意自写生成器的代价是很容易生成一批高度相似的样本导致训练集内部冗余严重。如果你选择这条路建议每个家族至少保留1万个样本并且通过随机种子控制保证生成的样本有足够差异。两类方式对比下来公开家族样本更适合做主实验自写生成器更适合做论文里的扩展实验比如验证“模型在未见过的生成器上是否失效”。起步阶段先用公开样本集各家族混合取样1万到3万条和良性样本数量大致持平训练一个初始模型。2.3 训练集、验证集、测试集怎么切才不“数据泄漏”这是DGA检测里最容易被忽略的一个环节也是后来泛化性崩盘的根源。很多课程作业直接把所有数据随机打乱按比例7:2:1切分结果训练集和测试集里出现了同一批时间窗口内生成的DGA域名。因为DGA生成通常依赖时间或者连续种子同一窗口的样本高度相似模型相当于把题目答案背下来了测试F1可能达到0.99换一批新域名立刻掉到及格线以下。正确做法是按时间或生成序号排序后再切分。DGA样本集通常自带日期字段先按日期排序取前70%做训练后30%做测试。这样测试集里的域名在时间上晚于训练集更接近真实部署场景——模型要面对的是“未来”的域名。良性域名没有时间概念就按域名长度排序后切分避免超长域名都堆在某一侧。以时间排序的语义很明确恶意软件更换生成窗口后那些在训练窗口之后出现的DGA域名就是模型未来要面对的样本。如果数据集没有显式时间字段但有生成索引或者家族内编号按编号排序也有近似效果。这一点建议在论文里单独写一段它通常是答辩时区分“背答案”和“会泛化”的关键证据。3. 把域名喂给模型字符序列化与模型结构选型DGA检测的输入是字符串不是图像也不是数值表格。怎么把域名变成模型能吃的张量以及选什么模型结构吸收这个序列决定了后续训练是顺风顺水还是反复折腾。3.1 域名转字符序列长度截断、填充与样本顺序域名先整体小写然后用tldextract取出注册域部分再做两件事去掉TLD段反转主域名。之所以反转是因为LSTM这类循环模型对序列末尾的信息更敏感。如果我们保留example.com正向顺序并在右侧补零模型在最后一个时间步读到的是大量padding有效信息被拉得很远。把主域名反转成elpmaxe再让序列在右侧对齐有效字符集中在序列前端最后一个时间步的隐状态能聚合到更多真实信号。通俗讲模型读到的顺序是“主域名的字符”在前面而不是一堆空位在前面。字符映射表用固定字母表包含小写字母、数字、连字符、点再加一个UNK占位。长度方面注册域部分超过75个字符的样本极少截断到75即可。如果数据集里长域名比例偏高先打印长度分布再定截断值不要直接拍脑袋。ALPHABET abcdefghijklmnopqrstuvwxyz0123456789-. UNK_IDX len(ALPHABET) 1 # 未知字符单独一个索引不能和padding共用 ch2idx {ch: i 1 for i, ch in enumerate(ALPHABET)} ch2idx[UNK] UNK_IDX MAX_LEN 75 def domain_to_ids(domain: str, max_len: int MAX_LEN) - list[int]: ext tldextract.extract(domain) main_part ext.domain # 去掉suffix避免TLD分布干扰 seq [ch2idx.get(ch, UNK_IDX) for ch in reversed(main_part)] if len(seq) max_len: seq seq[:max_len] else: seq [0] * (max_len - len(seq)) # 0 保留给 padding return seq这里的关键参数是UNK_IDX和0的分工。0是padding专用不在字母表内任何不在字母表里的字符比如中文域名转punycode前的Unicode字符都映射到UNK_IDX而不是映射成0。踩过坑的人都知道把未知字符映射到padding索引会让模型分不清“这个位置没有字符”和“这个位置有一个我认不出的字符”推理时一旦遇到生僻字符整个batch的概率分布都会乱掉。main_part取ext.domain而不是完整注册域是为了把.com、.top这类后缀忽略掉——恶意软件换TLD成本极低保留TLD只会让模型学到“.com更可能是良性”这种随时间漂移的假规律。3.2 LSTM、CNN、Transformer怎么选毕设场景下的对比表同样一份字符序列可以接不同模型主体。我这里给一个适合毕设的选型对比避免一上来就上复杂结构导致调不动。模型训练速度CPU推理速度对序列模式的捕捉毕设里的定位TextCNN一维卷积快快能捕捉局部n-gram模式对长度依赖弱轻量基线用来证明“加循环结构是否有收益”BiLSTM中中双向建模能捕捉长短程依赖推荐主模型效果和可解释性均衡Transformer慢中全局注意力但小数据下容易过拟合对比实验体现工作量上限选主模型我一般推荐BiLSTM原因很实际DGA域名本质上是字符序列恶意家族生成时的随机字符之间有长程统计关系LSTM天然适配这种序列建模同时LSTM的参数量适中一张普通显卡甚至纯CPU都能在几小时内跑完。TextCNN可以作为基线论文里写一句“本文采用TextCNN作为轻量对比模型验证序列建模模块的有效性”就构成一组消融。Transformer可以加进去对比但要有心理准备几万条样本下Transformer收敛慢而且很容易过拟合需要更强的正则。3.3 为什么不直接用手工特征这个题本来就是为了端到端有人会问传统方法用人工特征域名长度、数字占比、元音比例、字符熵、可发音性评分加随机森林也能到不错的AUC为什么非要深度学习这个问题答辩时大概率会被问到。答案是手工特征依赖人对“什么是异常域名”的归纳恶意软件作者完全可以针对这些统计特征构造对抗样本深度学习模型直接从字符分布里学决策边界不需要预设规则换一个未知家族时通常比手工特征泛化得更好。所以做深度学习版本的DGA检测核心卖点不是“我堆了一个神经网络”而是“整个系统从原始字符串到判定结果只经过一次端到端的特征学习”。这一点要在论文和答辩里反复强调它决定了你的题目是深度学习课设而不是传统机器学习课设。4. 用PyTorch跑通最小训练管线模型定义、损失与调参前面数据准备好了现在落到代码。下面这个管线是我在课设场景里常用的最小实现单机CPU能跑环境用conda隔离避免搞坏系统Python。4.1 先配环境别在环境上花掉一整天conda create -n dga_det python3.10 -y conda activate dga_det pip install torch scikit-learn pandas tldextract --index-url https://download.pytorch.org/whl/cpu说明--index-url指向PyTorch官方CPU wheel源适合没有NVIDIA GPU的笔记本。如果机器有独显去掉这行直接pip install torch会默认拉取CUDA版本。常见做法是先用CPU版把代码跑通再决定要不要用云GPU实例做大规模实验。scikit-learn用于指标计算tldextract用于域名标准化这两个库是整个流程里除了PyTorch之外仅有的第三方依赖把依赖控制在最小范围能让复现容易很多。4.2 模型定义Embedding BiLSTM 全连接模型部分不需要很花哨一个双向LSTM加一层全连接足够在公开数据上拿到可观的指标。import torch import torch.nn as nn class DGALSTM(nn.Module): def __init__(self, vocab_size41, embed_dim64, hidden_size64, num_layers2, num_classes1, dropout0.3): super().__init__() self.embedding nn.Embedding(vocab_size, embed_dim, padding_idx0) self.lstm nn.LSTM(input_sizeembed_dim, hidden_sizehidden_size, num_layersnum_layers, batch_firstTrue, bidirectionalTrue) self.dropout nn.Dropout(dropout) self.fc nn.Linear(hidden_size * 2, num_classes) def forward(self, x): emb self.embedding(x) # (batch, seq_len, embed_dim) out, _ self.lstm(emb) # (batch, seq_len, hidden*2) out out[:, -1, :] # 取最后一步聚合整个序列信息 out self.dropout(out) return self.fc(out).squeeze(-1) # 输出logits形状(batch,)关键点有三个。padding_idx0告诉Embedding层索引0对应的向量永远不参与梯度更新这样padding不会给模型引入噪声bidirectionalTrue让模型同时从左到右和从右到左读取字符双向隐状态拼接后维度是hidden_size * 2所以全连接层输入维度要翻倍输出层不加sigmoid直接返回logits配合后面的BCEWithLogitsLoss在数值上比“先sigmoid再算BCE”更可靠不会出现概率取对数时溢出为NaN的问题。参数解读vocab_size41对应26个字母10个数字3个符号-、.、UNK1个padding索引embed_dim64和hidden_size64是经验值提升到128会带来小幅指标上涨但训练时长接近翻倍课设阶段64起步够用num_layers2给模型一点深度再往上叠对几万条样本收益不明显。4.3 训练循环损失函数与类别不平衡处理真实场景里良性域名数量远大于DGA域名比例可能达到几十比一。如果直接拿原始比例训练模型会把所有样本都判成良性因为全判良性loss也很低。课程作业通常不会把比例拉那么极端但训练前仍然建议按负样本:正样本≈5:1到10:1粗处理再用pos_weight给正样本的loss加权。from torch.utils.data import DataLoader, TensorDataset import torch.optim as optim # 假设 train_ids, train_labels 已经构造好 dataset TensorDataset(torch.tensor(train_ids), torch.tensor(train_labels)) loader DataLoader(dataset, batch_size64, shuffleTrue, num_workers0) model DGALSTM() pos_weight torch.tensor([3.0]) # 先粗设按负样本/正样本比例校准 criterion nn.BCEWithLogitsLoss(pos_weightpos_weight) optimizer optim.Adam(model.parameters(), lr1e-3) model.train() for epoch in range(8): for x, y in loader: optimizer.zero_grad() logits model(x) loss criterion(logits, y.float()) loss.backward() optimizer.step()说明pos_weight3.0的含义是正样本的loss权重是负样本的3倍它补偿“正样本数量少”的劣势。如果你把训练集切成了良性3万、DGA 1万那么负样本:正样本3:1pos_weight就设3.0如果数据集比例不同按负样本数除以正样本数估算。这个参数不是调出来的是算出来的。batch_size64是CPU训练时比较稳的选择lr1e-3配Adam是默认稳妥组合如果loss震荡不降把lr降到3e-4再试。训练结束后评估注意不能用准确率当唯一指标。类别不平衡下准确率会虚高要同时看AUC、F1和PR曲线from sklearn.metrics import roc_auc_score, f1_score, classification_report # y_true为真实标签y_prob为sigmoid后的概率y_pred为二值化结果 auc roc_auc_score(y_true, y_prob) f1 f1_score(y_true, y_pred) print(classification_report(y_true, y_pred, target_names[normal, dga])) print(fAUC{auc:.4f} F1{f1:.4f})这段代码的意义在于建立一套完整的评估口径。AUC反映排序能力F1反映实际判定效果classification_report能直接看到DGA类的精确率和召回率。论文里把这几个数一并报告比单写一个“准确率99%”有说服力得多。4.4 超参经验表先记住这几个值参数推荐范围说明MAX_LEN64~80看主域名长度分布覆盖90%以上样本即可embed_dim64~12864起步128用于最终实验hidden_size64~128BiLSTM的隐层维度64够用num_layers1~2层数超过2对小数据集收益很小dropout0.2~0.50.3是常见默认值batch_size64~128CPU训练用64有GPU可提到128lr1e-3~3e-4Adam配1e-3loss震荡就降pos_weight按样本比例算负样本数/正样本数不要拍脑袋这些参数不是越极端越好课设阶段追求的是“每个参数都有明确依据”。答辩被问到“为什么hidden_size取64”你可以答“先用小模型跑通流程再在64/128之间对比128在验证集上的提升小于训练成本所以选择64。”这个回答比“因为大家都这么设”可靠得多。5. DGA检测避坑指南5个让训练和推理翻车的真实场景这一节集中写我用这个方案复现和调试时遇到过的典型问题每条按现象、原因、解决三步给出方便直接对照排查。5.1 训练和推理用的序列化不一致现象模型在验证集上AUC高达0.99部署推理时对短域名却全部输出接近0.5或对某些域名概率恒为0。原因训练时调用了domain_to_ids其内部做了反转和右侧对齐推理代码里图省事直接seq [ch2idx[c] for c in domain]没有反转也没有补齐序列分布完全不同模型看到的是“另一种格式”的输入。解决把domain_to_ids作为唯一入口训练和推理都走同一个函数不要在推理脚本里重新实现一遍字符映射。代码评审时确认推理路径里的reversed和seq [0] * (max_len - len(seq))两行还在。这个坑最容易出在“demo写得太快、直接复制了一段简化代码”的场景。5.2 未知字符被映射到padding索引现象训练loss偶尔出现NaN或者模型对个别包含特殊字符的域名输出概率恒等于0。原因字符映射用了ch2idx.get(c, 0)把字母表外的字符映射到了0。而0是padding的索引模型无法区分“这里没字符”和“这里有个不认识的字符”Embedding层输出混乱梯度反向传播时数值失控。解决按第4章的写法单独设UNK_IDX让所有未知字符落在一个独立索引上。同时输入侧先做domain.lower()再用tldextract归一化。国际化域名如果包含中文或特殊符号用Python的idna编码先转成ASCII兼容形式再进字符映射。5.3 用随机切分导致的时间窗口泄漏现象测试F1是0.99换一个时间段的DGA样本实测立刻掉到0.7以下模型像“背答案”。原因训练测试直接随机打乱切分同一时间窗口或同一随机种子生成的DGA域名同时出现在两端模型记忆的是该窗口的字符分布而不是泛化的DGA模式。解决数据切分前先按时间或生成序号排序取前70%做训练、后30%做测试如果数据集没有时间字段但有多家族字段可以按家族holdout——把某个家族完整地留在测试集里训练时完全不见该家族这是检验泛化能力的最严格做法。5.4 类别不平衡下的准确率虚高现象训练输出acc0.998看起来非常漂亮但细看DGA类的召回率只有0.1几乎全是负样本。原因良性域名远多于DGA域名模型只要全会判成良性准确率就极高但检测任务真正的目标是抓恶意样本。解决评估报告用PR-AUC、F1和DGA类召回率不要只展示准确率。训练时用pos_weight校正类别权重或者直接采样让正负样本比例落到1:5以内保证每个batch里都有足够多的正样本参与梯度更新。5.5 空序列和全padding样本现象某个batch的loss突然变成NaN或者训练进程在某个样本上卡死。原因部分域名经过tldextract提取后主域名为空比如输入是单段内网主机名、或者后缀处理异常生成的全是padding序列模型对全零输入计算梯度时数值不稳。解决数据清洗阶段过滤掉主域名长度小于2的样本训练前检查是否存在全零序列有则直接丢弃。给Dataset类加一个保护逻辑如果sum(seq)0跳过该样本。这样既不影响数据分布也不用担心训练崩溃。6. 验证与进阶从AUC到未知DGA家族的泛化测试6.1 对比实验怎么做才不被问倒课程作业和毕设的答辩环节老师最常问的一句话是“你这个模型比传统方法好在哪”。所以实验部分至少要覆盖三组手工特征加随机森林传统基线、TextCNN、BiLSTM。三组在完全相同的训练/测试切分下报告AUC、PR-AUC、F1和单条推理耗时。数据切分方案是否一致决定了对比实验是否公平。建议把训练、验证、测试的种子固定比如统一用random_state42这样论文里写的每个数字都可复现。进阶做法是加一组按DGA家族holdout的实验训练时去掉一个家族的全部样本测试时只评估这个家族。如果模型在这个指标上也能维持不错的AUC说明它不是记住了特定家族的字面模式而是学到了“正常注册域名”的分布边界。这个实验在论文里非常加分因为它直接回应了DGA检测最核心的痛点——未知家族。6.2 从离线指标到真实流量判断单域名判定只是第一步。真实场景里恶意主机会在短时间内发起大量DGA域名查询所以更可靠的做法是加一个时间窗口聚合规则统计同一内网IP在5分钟内请求的新域名数量、这些域名各自的DGA得分均值、以及最高得分。如果单域名得分超过0.8或者窗口内超过3个域名得分在0.5以上触发告警。这个规则不复杂但在答辩时体现的是系统思维。模型导出为ONNX方便脱离PyTorch环境做推理import torch.onnx model.eval() dummy torch.randint(0, 40, (1, 75)) torch.onnx.export(model, dummy, dga_lstm.onnx, input_names[ids], output_names[logits], dynamic_axes{ids: {0: batch, 1: seq}}, opset_version12)用dynamic_axes允许batch维度变化推理时配合onnxruntime一次跑一个域名或一批域名。实际部署时把模型接到DNS查询链路的旁路日志流上比串行拦截更容易落地。我自己第一次跑这个题的时候手里没有真实内网日志就在公开数据上把F1调到了0.99一度觉得这个题太简单。后来换了一个时间段的新DGA样本一测指标掉得厉害才意识到这个题的难点从来不在模型结构而在数据切分和评估口径是否贴近真实场景。从那以后每次做这类检测任务都先把数据划分方案写在代码注释里再开始调模型。希望帮到你。本文还有配套的精品资源点击获取