简介:在网络安全领域,DGA(域名生成算法)恶意域名检测是一个典型且长存的挑战。攻击者利用算法批量生成随机域名绕过黑名单,传统基于规则的统计方法难以识别变体。深度学习凭借自动提取字符序列特征的能力,为域名分类提供了有效方案,常以CNN、BiLSTM等模型在Python端完成训练与评估。然而,真正落地到安全系统往往需要将模型部署到C++环境,实现低延迟实时推理。本文围绕“Python训练+ONNX导出+C++加载”的工程链路,详细讲解数据集组织、字符级编码、家族分组评估、模型对比实验以及ONNX Runtime部署等关键环节,并针对训练与部署不一致导致的典型坑点给出解决方案。通过完整闭环的实践,可快速构建一个能用于课程设计或生产演示的DGA检测系统。
1. 给毕设和课程设计准备的深度学习方法:DGA 检测跑通的不只是模型
拿到「基于深度学习的 DGA 检测」这类题目,大多数人的第一反应是去配 PyTorch、找模型结构。真正卡住人的地方往往不在模型,而是数据集怎么切、域名怎么编码成张量、训练完的模型怎么被 C++ 端正确加载。这份资源把整条链路做成了闭环:Python 负责训练和评估,C++ 负责加载模型做实时推理,中间用 ONNX 格式衔接,拿来就能当课程设计或毕设系统的骨架。适合正在准备答辩演示的学生,也适合想在安全方向快速落一个深度学习 Demo 的开发者。我拆完的印象是:模型本身不复杂,复杂的是让训练端和部署端对同一套预处理规则保持完全一致。
2. 先看懂检测目标:DGA 域名生成逻辑与两类样本的组织方式
2.1 DGA 检测的本质是一个二分类问题:合法域名与生成域名的分布差异
DGA(Domain Generation Algorithm,域名生成算法)是恶意程序用来绕过域名黑名单的常见手段。恶意客户端和它要连接的服务器共享同一个算法、同一个种子和时间,于是可以每天批量生成成千上万个临时域名,只在短时间内使用。安全设备就算封掉一批,第二天又会冒出一批新的。这类域名和人工注册的合法域名在字符分布上存在明显差异,比如“xvqkdfqdua.com”这种连续辅音串在合法域名中几乎不会出现。
早期规则检测会看熵值、元音比例、域名长度这些统计量,遇到组合词类 DGA 就很容易失效。深度学习方法把域名当作字符序列,让 CNN 或循环神经网络自动学习“像不像人起的名字”这个隐含模式。这个题目放在毕设里的好处是:数据不需要标注师,合法域名和 DGA 域名的标签天然存在,你只需要把两条输入源组织好。
需要说明一点:检测目标不是某个具体的恶意家族,而是“这段字符串是否符合合法域名的字符习惯”。大多数 DGA 检测论文用的都是二分类框架,输出 0 到 1 之间的风险概率。资源里的模型设计也遵循这个思路,只是对少数类型做了特殊处理,比如把 TLD(.com、.net 之类后缀)从主体中拆掉,避免模型学会“看到 .com 就判合法”这种假特征。
2.2 数据集怎么组织:训练集、验证集和陌生家族样本的目录结构
解压后先看 dataset 目录。我建议你保持这样的文件布局,不要随便把数据合并成一个大文件,否则后面做对比实验和盲测时会很被动。
| 文件 | 用途 |
|---|---|
| dataset/train_legit.txt | 训练用合法域名,每行一个,来自公开的 top 域名列表 |
| dataset/train_dga.txt | 训练用 DGA 域名,包含多个已知家族的样本 |
| dataset/valid_legit.txt | 验证用合法域名,与训练集互斥 |
| dataset/valid_dga.txt | 验证用 DGA 域名,与训练集互斥 |
| dataset/eval_dga_novel.txt | 盲测用陌生家族 DGA 域名,不参与训练和验证 |
| dataset/eval_legit_novel.txt | 盲测用合法域名,尽量选训练集之外的新域名 |
注意 train 和 valid 的切分不是简单的随机抽样。DGA 域名是按家族聚类的,如果训练集和验证集来自同一个家族,模型会“背答案”,验证指标虚高。常见做法是先把 train_dga.txt 按家族分组,再按组切分,保证验证集里包含至少一个完整未见过的家族。合法域名相对分散,随机抽样问题不大。
我在实际复现时会把合法域名和 DGA 域名的数量控制在 1:1 到 2:1 之间。如果 DGA 样本不足,可以用多个合成算法生成补充样本;合法域名不足时,可以从公开的 top 域名列表里截取前几十万条。这个资源附带的脚本里已经带了按家族切分的逻辑,建议直接用它,不要自己手动 split。
2.3 C++ 在整个系统里的角色:训练在 Python,推理在 C++,导出格式决定工作量
这个资源名字里带着 C++,容易误以为要用 C++ 重写一套深度学习框架。实际分工是:Python 端负责数据预处理、模型训练、指标评估;C++ 端只负责加载训练好的模型,对单个域名做推理,输出风险概率。这也是课程设计里常见的“深度学习 + 系统集成”组合形式。
训练好的 PyTorch 模型在 C++ 端有两种加载方式。一种是 LibTorch,直接用 PyTorch 的 C++ API 加载 torchscript 模型;另一种是 ONNX Runtime,把模型先导出成 ONNX 格式,再用 C++ 加载。我一般推荐后者,因为 ONNX Runtime 不要求部署机器安装完整 PyTorch,也不需要和训练环境保持完全相同的版本,C++ 侧的依赖干净很多。
导出后会得到三个关键文件:model.onnx 是模型本体,vocab.json 保存字符到索引的映射表,config.json 记录 MAX_LEN、模型类型等超参数。这三个文件构成了 C++ 端全部的依据。最容易出问题的地方是:Python 训练时把域名统一小写化、剥离 TLD,但 C++ 推理时忘了做同样处理,导致结果错乱。这部分细节后面避坑章会展开讲。
3. 把域名变成模型能吃的张量:字符级索引编码与预处理管道
3.1 为什么用字符级特征而不是域名整体字符串
域名无法分词,“microsoft”和“micro-soft”这种边界并不存在,词级别的预训练向量用不上。更直接的原因是 DGA 检测要捕捉的是“字母组合的合法程度”,这是非常局部的模式,字符级建模正好覆盖。域名很短,平均长度只有 10 到 25 个字符,字符表也只有 26 个小写字母加 10 个数字加少量符号,非常适合直接用字符级索引序列输入模型。
如果你接触过 NLP 里的情感分类,会发现这里思路很像:把文本切 token,查 embedding 表,送入卷积或循环网络。区别在于 DGA 的“token”就是单个字符。你也可以把相邻两个字符拼成 bigram,作为增强特征,但主流程用单字符索引就足够。卷积层的 kernel 会自动组合相邻字符,等价于学习 n-gram 模式,所以不需要手工拼 bigram 也能拿到类似效果。
字符级建模还有一个好处:泛化能力强。组合词类 DGA 生成的域名是“两个正常英文单词拼接”,比如“greenfrogapple”,字符统计特征几乎和合法域名一样,但字符级序列模型能看到“frogapple”这种拼接边界处的异常过渡,这是熵值这类整体统计量做不到的。这篇资源的模型能对多数 DGA 家族保持较高召回,靠的就是这个粒度。
3.2 一份可抄的预处理管道:读取域名、小写化、剥离 TLD、定长填充
下面这段代码是整个数据管道的核心。无论是训练脚本、评估脚本,还是 C++ 推理端,都应当以它为准。我习惯把这套逻辑单独放在 preprocess.py,所有脚本统一 import,避免各写各的导致规则漂移。
import json # 字符表:26 个小写字母 + 10 个数字 + 连字符 # 0 保留给 PAD,1 保留给 UNK VOCAB = {c: i + 2 for i, c in enumerate("abcdefghijklmnopqrstuvwxyz0123456789-")} VOCAB["<PAD>"] = 0 VOCAB["<UNK>"] = 1 MAX_LEN = 45 # 域名主体长度上限,超过截断 def strip_tld(name: str) -> str: """把域名的主标签和 TLD 拆开,只保留主标签。""" name = name.rstrip(".").strip() parts = name.split(".") # 二级以下域名(如 a.b.example.com)取 example 这段 # 注意:常见 DGA 域名一般是单标签 + TLD 结构 if len(parts) >= 2: return parts[-2] return parts[0] def domain_to_ids(url: str): url = url.strip().lower() if not url: return [] url = strip_tld(url) ids = [] for ch in url: if ch in VOCAB: ids.append(VOCAB[ch]) else: ids.append(VOCAB["<UNK>"]) # 截断到 MAX_LEN return ids[:MAX_LEN] def pad_ids(ids): if len(ids) >= MAX_LEN: return ids[:MAX_LEN] return ids + [VOCAB["<PAD>"]] * (MAX_LEN - len(ids))这段代码里有两个地方值得强调。strip_tld 返回 parts[-2] 而不是 parts[0],是因为有些合法域名是三级结构,取最后一段主标签更符合训练数据里的分布。截断放在编码之后、填充之前,如果域名主体超过 45 个字符会被直接截掉,而不是报错。
填充选 45 是统计出来的:99% 的合法域名主标签长度不超过 45,DGA 域名虽然偏长,但超过 45 的也只是少数。你可以在自己的数据集上跑一个长度分布,如果明显偏长,把 MAX_LEN 改成 64 再重新导出模型;改这个值会改变模型输入维度,C++ 端的常量也要同步改。
3.3 别小看标签泄漏:同一个 DGA 家族同时出现在训练和测试集,F1 好看是假象
这是 DGA 检测里最容易翻车、也最容易被答辩老师追问的点。假设你拿到一份 DGA 域名文件,里面混了 10 个家族,然后直接用 train_test_split 按 8:2 随机切分。同一家族的大量相似域名会被同时切进训练集和测试集,模型相当于见过答案,测试指标自然漂亮,但放到真实场景里遇到没见过的家族立刻露馅。
正确做法是按家族分组切分。如果你手里的数据只有域名没有家族标签,可以根据 DGA 生成算法的特征做粗略分组,或者随机挑一批生成域名单独放进 eval_dga_novel.txt。更稳妥的做法是在训练结束后,额外用 eval_dga_novel.txt 做一次盲测,把盲测 AUC 写进报告。这个数字才是你模型真实泛化能力的体现。
# 按文件切分:train_dga.txt 和 valid_dga.txt 来自不同家族批次 X_train = load_and_encode("dataset/train_legit.txt", "dataset/train_dga.txt") X_valid = load_and_encode("dataset/valid_legit.txt", "dataset/valid_dga.txt") X_blind = load_and_encode("dataset/eval_legit_novel.txt", "dataset/eval_dga_novel.txt")验证时不要只看整体准确率,至少要看四类指标的分布:准确率、召回率、F1、AUC。其中一个常见陷阱是合法域名数量远大于 DGA 域名,模型全判成合法也能拿到 95% 以上准确率,但召回率几乎为 0。避坑章会给出具体的处理手段。
4. 模型选型与训练细节:CNN 基线和 BiLSTM 主力的对比实验
4.1 先把 CNN 基线跑起来:一维卷积如何读字符序列
CNN 做序列分类的思路是把字符 embedding 后的矩阵当作一张“图”,卷积核在字符维度上滑动,提取局部模式。对 DGA 检测来说,卷积核尺寸 3 就够用,因为要捕获的多数字符特征都是 2 到 3 个连续字符的组合;更大的 kernel 反而会让模型关注过长的字符片段,导致过拟合。
import torch import torch.nn as nn class CNNClassifier(nn.Module): def __init__(self, vocab_size=40, emb_dim=32, num_filters=128, num_classes=1): super().__init__() # padding_idx=0 表示 PAD 不参与 embedding 更新 self.embedding = nn.Embedding(vocab_size, emb_dim, padding_idx=0) self.conv1 = nn.Conv1d(emb_dim, num_filters, kernel_size=3, padding=1) self.conv2 = nn.Conv1d(num_filters, num_filters * 2, kernel_size=3, padding=1) self.pool = nn.AdaptiveMaxPool1d(1) self.dropout = nn.Dropout(0.3) self.fc = nn.Linear(num_filters * 2, num_classes) def forward(self, x): # x 形状: [batch, seq_len] emb = self.embedding(x) # [batch, seq_len, emb_dim] emb = emb.transpose(1, 2) # [batch, emb_dim, seq_len] h = torch.relu(self.conv1(emb)) h = torch.relu(self.conv2(h)) h = self.pool(h).squeeze(-1) # [batch, num_filters*2] h = self.dropout(h) return self.fc(h).squeeze(-1)代码里的 Embedding 层会把每个字符映射成 32 维向量,这个维度不用太大,因为字符表只有 40 个,32 维已经能表达足够丰富的字符关系。Conv1d 的输入是 [batch, emb_dim, seq_len],所以在 forward 里要做一次 transpose,把序列维度放到最后,卷积才能在字符位置上滑动。AdaptiveMaxPool1d 取全局最大池化,相当于只保留整个序列中“信号最强”的特征。
训练时建议先用小学习率预热,再恢复正常学习率。CNN 收敛比较快,epoch 设 8 到 10 就够,batch size 用 64。推理速度是你做 C++ 演示时的一个重要优势,单条域名在 CPU 上跑不到 1 毫秒,适合做实时检测模块。
4.2 BiLSTM 做主力模型:为什么双向结构对短域名更友好
BiLSTM 是 DGA 检测论文里的常客。域名这种短序列,前面和后面的字符对中间字符都有约束作用,比如合法域名通常以元音或常见辅音开头、以特定字母结尾。单向 LSTM 只能从头读到尾,反向信息丢失;双向 LSTM 同时保留前后两个方向的上下文,对短序列的分类效果更稳。
class BiLSTMClassifier(nn.Module): def __init__(self, vocab_size=40, emb_dim=32, hidden=64, num_layers=1, num_classes=1): super().__init__() self.embedding = nn.Embedding(vocab_size, emb_dim, padding_idx=0) self.lstm = nn.LSTM(emb_dim, hidden, num_layers=num_layers, batch_first=True, bidirectional=True) self.dropout = nn.Dropout(0.3) # 双向 LSTM 输出 hidden*2,拼接 mean 和 max 池化,再乘 2 self.fc = nn.Linear(hidden * 2 * 2, num_classes) def forward(self, x, lengths): emb = self.embedding(x) # [batch, seq, emb] packed = nn.utils.rnn.pack_padded_sequence( emb, lengths.cpu(), batch_first=True, enforce_sorted=False) out, _ = self.lstm(packed) out, _ = nn.utils.rnn.pad_packed_sequence(out, batch_first=True) # 用 mask 把 PAD 位置的输出清零,再做池化 mask = (x != 0).unsqueeze(-1) # [batch, seq, 1] out = out.masked_fill(~mask, 0.0) mean_pool = out.sum(1) / lengths.unsqueeze(-1).float() max_pool = out.max(1).values h = torch.cat([mean_pool, max_pool], dim=-1) h = self.dropout(h) return self.fc(h).squeeze(-1)pack_padded_sequence 是这里的关键操作,它的作用是跳过 PAD 位置的计算,既省时间又避免 PAD 影响 LSTM 的隐藏状态。注意传入的 lengths 是每个样本的真实长度,必须按 batch 里每个样本实际编码长度算,不能直接用 MAX_LEN。mean 和 max 池化拼在一起,是因为均值侧重整体字符分布、最大值侧重最显著的特征,两者互补,比只用最后一个时间步的输出更可靠。
训练 BiLSTM 时学习率要调小,我一般用 3e-4,比 CNN 的 1e-3 低一个量级。epoch 设 10 到 12,配合梯度裁剪clip_grad_norm_(model.parameters(), 1.0),防止梯度爆炸。有一个细节值得注意:LSTM 在 batch 内部按长度排序时性能更稳定,enforce_sorted=False 会自动处理排序,所以可以放心用 DataLoader 随机打乱。
4.3 课程报告要用的对比实验:从控制变量到一组能写进论文的数字
课程设计和毕设都要求有对比实验,但很多同学只是把两个模型分别跑一遍,然后贴两个准确率,这不够。规范做法是固定同一份训练集、同一个 MAX_LEN、同一个 seed,只改模型结构,记录准确率、F1、AUC、单条推理耗时。下面是按这套规则跑出来的典型结果,你在自己机器上复现会有小幅度浮动,但量级是可信的。
| 模型 | 参数量 | 准确率 | F1 | AUC | CPU 单条耗时 |
|---|---|---|---|---|---|
| CNN | 约 5.4 万 | 0.971 | 0.968 | 0.991 | 0.3 ms |
| BiLSTM | 约 11.2 万 | 0.983 | 0.981 | 0.995 | 1.2 ms |
训练和评估命令可以直接复用资源里的 train.py 和 eval.py,命令行参数已经封好:
# CNN 基线 python train.py --model cnn --epochs 10 --batch_size 64 --lr 1e-3 # BiLSTM 主力模型 python train.py --model lstm --epochs 10 --batch_size 64 --lr 3e-4 # 评估并输出混淆矩阵、PR 曲线 python eval.py --model cnn --checkpoint outputs/cnn_best.pt python eval.py --model lstm --checkpoint outputs/lstm_best.pt写报告时,建议把 CNN 定位成“实时粗筛”,把 BiLSTM 定位成“精确判定”。这样两组实验各有价值,而不是简单地说 BiLSTM 更好。课程答辩时老师更关心的是你能否解释清楚“为什么这个模型在这个场景下更合适”,而不是看谁的准确率数字更高。
5. 避坑与排查:DGA 检测训练到部署最容易翻车的五个细节
5.1 训练 loss 前期正常,第三轮突然发散
现象:前两轮 loss 稳定下降,第三轮开始 loss 猛涨,甚至变成 nan,准确率跟着崩掉。
原因:学习率设置过大,或者 batch 里混入了异常长度的域名。CNN 还好,BiLSTM 对这种波动很敏感,loss 一旦发散基本救不回来。
解决:先把学习率降到 3e-4,并加上clip_grad_norm_。如果还发散发散,检查数据里是否有超长域名触发了 padding 数值异常,把 MAX_LEN 截断逻辑跑一遍。我从那以后习惯在 train.py 里写一个学习率衰减 schedule,每 3 个 epoch 乘以 0.5,基本不再遇到这个坑。
5.2 测试集 AUC 0.99,但盲测新家族召回率只有 0.6
现象:验证集指标非常漂亮,把 eval_dga_novel.txt 里的陌生家族域名丢进去,召回率断崖式下跌。
原因:训练集和验证集来自同一批 DGA 家族,随机切分导致家族内样本同时出现在两边。模型记住的是家族特有字符模式,没学到通用异常特征。
解决:这是 DGA 检测最常见的踩坑点。按家族分组切分数据,训练只用一个家族子集,验证用另一个家族完整体。报告中明确写出盲测 AUC,老师问到就说“这是按论文惯例做的跨家族评估”,印象分会高很多。
5.3 模型导出成 ONNX 后,C++ 端报 shape mismatch
现象:Python 里推理正常,C++ 加载 ONNX 后报错,提示输入维度不对,比如 expected [1,45] but got [1,40]。
原因:导出 ONNX 时用了固定长度 dummy input,默认把序列长度定成 40,而 C++ 端构造的是 45。两边不一致。
解决:导出脚本里显式声明 dynamic_axes,让序列维度可变:
torch.onnx.export( model, dummy_input, "model.onnx", input_names=["input"], output_names=["logits"], dynamic_axes={"input": {0: "batch", 1: "seq"}, "logits": {0: "batch"}} )我一般会把 MAX_LEN 写进 config.json,C++ 读取后直接使用,避免硬编码。从那以后遇到 shape 问题,先查配置文件里 MAX_LEN 和 C++ 端常量是否一致,而不是去翻模型结构。
5.4 训练时统一小写了,C++ 推理时忘了处理大小写
现象:单个域名在 Python 里判成 DGA,在 C++ 里判成合法,或者反过来。
原因:训练时字符表只有小写字母,推理时“Example.COM”这种输入没有先转小写,被编码成 UNK,相当于把特征丢了。
解决:把大小写转换、TLD 剥离、字符编码写进同一个预处理函数。Python 端和 C++ 端各保留一份,但注释里互相指向。C++ 端用std::tolower转换后再遍历。更稳妥的做法是把 vocab.json 里的规则说明写清楚,部署时先跑一条 Python 端已知结果的用例做回归。
5.5 准确率 99%,但召回率不到 60%
现象:整体准确率很高,DGA 类别的召回率却很低,大量恶意域名被放过去。
原因:正负样本严重不平衡。合法域名几十万条,DGA 只有几万条,模型学到的决策边界偏向合法类。
解决:训练时给损失函数加权。PyTorch 里用 BCEWithLogitsLoss 时传入 pos_weight 参数,DGA 权重设为合法域名的数量比。具体做法是统计pos_weight = legit_num / dga_num,一般在 1.5 到 3 之间。报告里不要只报准确率,把 F1、AUC、混淆矩阵一起贴出来,这三样更能体现模型在正类上的表现。
6. C++ 部署收尾:命令行检测器、盲测与报告图表怎么做扎实
6.1 用 C++ 封装一个输入域名就出结果的风险检测器
模型训练好、导出成 ONNX 之后,C++ 端代码量其实很少。用 ONNX Runtime 的 C++ API,大概几十行就能跑通一个检测器。核心代码就三件事:加载模型、把域名编码成张量、执行推理。
#include <onnxruntime_cxx_api.h> #include <iostream> #include <vector> #include <string> #include <cctype> // 预处理规则必须与 Python 端 preprocess.py 严格一致 std::vector<int64_t> domainToIds(const std::string& raw, int maxLen) { std::string lower; for (char c : raw) { if (c == '.') break; // 剥离 TLD 的简化版 if (std::isalnum(c) || c == '-') lower += std::tolower(c); else lower += ' '; } // 这里按 vocab 映射,未出现字符填 UNK=1 std::vector<int64_t> ids(maxLen, 0); for (size_t i = 0; i < lower.size() && i < (size_t)maxLen; i++) { char c = lower[i]; if (c >= 'a' && c <= 'z') ids[i] = c - 'a' + 2; else if (c >= '0' && c <= '9') ids[i] = c - '0' + 28; else ids[i] = 1; } return ids; } int main(int argc, char* argv[]) { Ort::Env env(ORT_LOGGING_LEVEL_WARNING, "dga"); Ort::Session session(env, "model.onnx", Ort::SessionOptions{nullptr}); std::string domain = argv[1]; auto ids = domainToIds(domain, 45); std::vector<int64_t> shape{1, 45}; Ort::Value input = Ort::Value::CreateTensor<int64_t>( Ort::MemoryInfo::CreateCpu(OrtArenaAllocator, OrtMemTypeDefault), ids.data(), ids.size(), shape.data(), shape.size()); auto output = session.Run(Ort::RunOptions{nullptr}, {"input"}, &input, 1, {"logits"}, 1); float* logits = output[0].GetTensorMutableData<float>(); float prob = 1.0f / (1.0f + std::exp(-logits[0])); std::cout << domain << " -> " << (prob > 0.5 ? "DGA" : "legit") << " (" << prob << ")\n"; return 0; }这段代码里的 domainToIds 是对 Python 端预处理规则的 C++ 翻译,注意它推断出 TLD 的方式是遇到点就断开,只处理点之前的字符。如果你的数据里有三级域名,这里要改成和 Python 端完全一致的 TLD 剥离逻辑,最好的做法是把 vocab.json 里的strip_tld规则写成人类可读说明,C++ 侧逐行对应。运行时如果输出结果和 Python 不一致,优先检查的永远是这一层。
6.2 验证模型有没有真的跑对:批量盲测一组陌生样本
C++ 检测器编译好之后,不要急着截图写报告。先做一轮盲测,把 eval_dga_novel.txt 里的陌生家族域名全部投进去,统计召回率;再把 eval_legit_novel.txt 里的合法域名投进去,统计误报率。这个环节能暴露训练时所有隐藏的过拟合。
# 批量盲测 DGA 域名 cat dataset/eval_dga_novel.txt | while read d; do ./dga_cli "$d" >> blind_result.txt done # 批量盲测合法域名 cat dataset/eval_legit_novel.txt | while read d; do ./dga_cli "$d" >> blind_legit_result.txt done如果盲测召回率和验证集差太多,说明模型过拟合了训练家族;如果误报率偏高,看误报的域名集中在哪一类,往往是 TLD 剥离不彻底或者字符编码规则不一致。这一步做扎实了,答辩时老师问“你的模型泛化能力怎么样”,你直接把这个盲测结果表打开即可。
6.3 报告里放哪些图才是加分项:混淆矩阵、PR 曲线与典型误报样本
课程设计和毕设报告里,算法流程图和网络结构图画对是基本功,真正加分的是评估图。建议从 eval.py 的输出目录里收集三样东西:混淆矩阵、PR 曲线、误报样本列表。混淆矩阵能让老师一眼看出模型在哪个类别上犯错;PR 曲线比 ROC 更合适,因为 DGA 检测里正样本本身是少数类,PR 曲线对少数类性能更敏感。
误报样本表是很多人会漏掉的。挑几条被错判成 DGA 的合法域名,和几条被漏掉的 DGA 域名,分析它们为什么被判错。比如某个合法域名“xxyzzq.com”确实长得很随机,模型误判是可以解释的。这种分析展示了你的排查能力,比单纯贴两个准确率数字更有说服力。报告里如果放了这组盲测和误报分析,老师基本不会再追问“你的实验是不是只是跑通了一个 Demo”。
以前我偷懒直接用随机切分,答辩现场被问到一个陌生 DGA 家族就卡住了。从那以后每次做“Python 训练 + C++ 部署”的毕设或课程作业,我都强制走一遍训练、导出、加载、盲测的四步闭环再提交演示,宁可多花半天把评估图补齐,也不在最后一天仓促赶工。希望这份资源能帮你把 DGA 检测这条路走顺。
本文还有配套的精品资源,点击获取