大模型训练全流程实战指南实战篇(十五)——预训练数据治理

前言

上篇文章梳理了网络安全大模型的七大数据来源------涵盖学术论文与顶会、NIST/MITRE 标准框架、CVE 漏洞库、电子书、开源数据集、大模型蒸馏等类别,并为每一类都提供了具体的获取渠道和采集示例。

然而,数据采集完成只是第一步。此时大家手中的仍是形态各异的"原料":论文有双栏排布也有单栏排布,电子书图文混排,漏洞库以 JSON 格式居多,大量文章中还夹杂着截图......这些异构数据显然无法直接用于模型预训练。

如何将这些纷繁复杂的原始素材,转化为标准、干净的预训练数据集呢?答案在于搭建一条完整的数据加工流水线。这套流程大致分为三步:首先,利用文档解析工具将各类异构文档统一转换为 Markdown 格式;接着,依次进行去重、质量过滤、隐私脱敏等清洗操作;最后,按需分块导出,形成可直接用于预训练的数据集。

本期内容,笔者就带大家把这条流水线完整跑通,逐环节拆解其中的关键技术与实操细节,一起来动手吧~

一、文档解析

加工流水线的第一步,是将异构文档统一转换为 Markdown 格式。Markdown 的标题层级天然保留了文档结构信息,后续无论是分块还是清洗,都可以依据章节语义进行切分,是目前最理想的中间格式。

1.1 文档解析工具安装与使用

大模型训练全流程实战指南工具篇(六)------OCR工具实战指南中,笔者介绍了 DeepSeek-OCR 工具,用于将文档转化为 Markdown 格式。除了 DeepSeek-OCR,上海人工智能实验室开源的 MinerU 也是笔者日常工作中最常用的 OCR 工具之一。它对学术论文、固定版式 PDF、docx 以及网页数据的还原效果出色,公式和表格细节保留完整,且支持多种运行模式,部分模式在仅有 CPU 的环境下也能顺利执行。本篇将围绕 MinerU 展开讲解。

先来看 MinerU 的安装,过程非常简单。笔者建议为其创建独立的 Conda 环境:

bash 复制代码
conda create -n mineru python=3.13 #创建名为mineru的conda虚拟环境
conda activate mineru #激活mineru环境
pip install -U "mineru[all]" #安装mineru依赖

安装完成后,使用 MinerU 将文档转化为 Markdown 格式的操作也非常简单。假设原始文档存放在 raw_docs 目录下(这里以一篇网络安全论文为例),执行如下命令即可:

bash 复制代码
mineru -p ./raw_docs -o ./raw_output 

其中 -p 指定输入文件或目录,-o 指定输出目录。命令执行后,MinerU 会调用模型对每个文档进行版面分析、文本提取(或 OCR)和公式识别,最终输出结构化的 Markdown 结果。

如果 GPU 计算资源有限,可以使用 pipeline 模式,该模式会调用更小的模型来完成版式识别和文本提取:

bash 复制代码
mineru -p ./raw_docs -o ./raw_output -b pipeline

识别完成后的解析产物中,每个PDF都会生成一个同名的目录,该目录下的结构如下:

其中有两个路径需要重点关注:.md 文件即转换后的 Markdown 文档。打开后可以看到,双栏论文的阅读顺序、章节标题和参考文献均被正确还原,公式也转为了 LaTeX 格式:

不过纯文本模型无法直接"阅读"图片,因此 Markdown 中的 ![](images/xxx.jpg) 引用依然保留。这些图片往往承载着关键信息------比如攻击链示意图、网络拓扑图、流量截图等,若不处理就会丢失。这些图片都被抽取到了同目录下的 images 文件夹中:

针对这些图片数据,就需要用到下一小节的处理脚本了~

1.2 图片内容处理

上一节提到,Markdown 中的 ![](images/xxx.jpg) 仅是图片引用。笔者要训练的是大语言模型,需要纯文本数据,那么图片信息该如何保留呢?

其实思路在大模型训练全流程实战指南工具篇(六)------OCR工具实战指南中就已提及:可以调用多模态大模型为每张图片生成语义描述,将图片"翻译"成文字。生成的描述文本会在后续清洗流程中插入 Markdown 的对应位置,这样图片承载的知识就以文本形式留存下来。

1.2.1 单张图片转化为描述

本次笔者选用阿里云百炼平台的 qwen3.8-flash 系列模型。使用前,需要先登录阿里云百炼平台,在 API KEY 管理页面创建一个 api_key

获取 api_key 后,即可通过 OpenAI 兼容接口访问 qwen3.8-flash 模型。注意,请求前需要将图片转换为 base64 编码:

python 复制代码
import base64
from openai import OpenAI

client = OpenAI(
    api_key='',  # 百炼平台申请的 API Key
    base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
)

def describe_image(image_path: str) -> str:
    with open(image_path, "rb") as f:
        b64 = base64.b64encode(f.read()).decode()
    completion = client.chat.completions.create(
        model="qwen3.8-flash",
        messages=[{"role": "user", "content": [
            {"type": "image_url",
             "image_url": {"url": f"data:image/png;base64,{b64}"}},
            {"type": "text",
             "text": "你是一名网络安全专家。请用一段话详细描述这张图片的内容,"
                     "若图中包含文字标注,请一并准确转录。"},
        ]}],
    )
    return completion.choices[0].message.content

if __name__ == "__main__":
    print(describe_image("./images/f9534223ea3d6c1cd740555708cab1a64f3286c4005d78fae12b11709187d4ac.jpg"))

执行结果如下:

1.2.2 图片批量处理生成描述

实际语料动辄成百上千张图片,逐张手动调用显然不现实。常规做法是:先通过正则提取 Markdown 中所有图片引用,再批量调用模型生成描述,最后用描述文本替换原始图片引用。实现代码如下:

python 复制代码
# ---------- 批量处理逻辑 ----------
MD_PATH = "./面向大模型分布式微调的选择性聚合轻量化后门防御方法.md"
IMAGE_DIR = "./images"
OUTPUT_PATH = "./面向大模型分布式微调的选择性聚合轻量化后门防御方法_new.md"  # 直接覆盖原文件

# 第一步:用正则提取 Markdown 中实际引用的图片
IMG_PATTERN = re.compile(r"![([^]]*)](images/([^)]+))")
with open(MD_PATH, encoding="utf-8") as f:
    md_text = f.read()
referenced = [(m.group(0), m.group(2)) for m in IMG_PATTERN.finditer(md_text)]
print(f"Markdown 中引用的图片数量: {len(referenced)}")

# 第二步:逐张生成描述并直接替换
for i, (full_match, fname) in enumerate(referenced, 1):
    path = os.path.join(IMAGE_DIR, fname)
    if not os.path.exists(path):
        print(f"[{i}/{len(referenced)}] {fname} 文件不存在,跳过")
        continue
    try:
        desc = describe_image(path)
        # 用【图片内容:...】包裹描述,替换原图片引用
        replacement = f"【图片内容:{desc}】"
        md_text = md_text.replace(full_match, replacement, 1)
        print(f"[{i}/{len(referenced)}] {fname} 完成")
    except Exception as e:
        print(f"[{i}/{len(referenced)}] {fname} 失败: {e}")
    time.sleep(0.5)  # 简单限速,避免触发 API 限流

# 第三步:写回文件
with open(OUTPUT_PATH, "w", encoding="utf-8") as f:
    f.write(md_text)
print("全部完成,结果已写回", OUTPUT_PATH)

执行结果如下:

查看转换后的文档,原先图片引用的位置已全部替换为相应的语言描述:

1.2.3 别忘了图文一致性检验

这里有一个容易踩的坑:多模态模型偶尔会"看图说话说过头" ,编造图中并不存在的细节。

在实践中,建议批量生成描述后,按 5%~10% 的比例进行抽检,重点核对描述内容与图片前后文的逻辑是否吻合;对于关键报告(如威胁分析报告中的攻击链图),则全量人工过一遍。宁可少几张图的描述,也不要让错误信息混入训练集。

二、数据清洗流水线

经过上一节的处理,图片描述已经替换到了 Markdown 文档中。那么,这份文档能直接拿去训练了吗?显然还不行。

刚才处理的是论文数据,本身质量较高。但如果换成爬取的社区数据呢?随手翻开一篇,转载重复、广告导流、真实 IP 和邮箱等"杂质"比比皆是。从这类"半成品"到真正可用的预训练数据,中间还隔着三道关键工序:去重、质量过滤、隐私脱敏

下面,笔者就逐道工序带大家实现。

2.1 去重:安全资讯的转载重灾区

同一个漏洞通告,往往被十几个网站转载,措辞略有不同------这是社区数据的常态。如果不去重,模型会把这类内容反复学习多遍,既挤占了其他知识的"学习配额",又容易过拟合这些高频重复文本。

在实践工作中,去重环节通常采用两级策略:先精确去重,再近似去重

对于完全相同的文档(转载时原样复制的情况很常见),直接用哈希判定,速度极快:

python 复制代码
import hashlib

def exact_dedup(texts: dict) -> dict:
    """texts: {文件名: 文本},返回去重后的字典"""
    seen, result = set(), {}
    for name, text in texts.items():
        h = hashlib.md5(text.encode("utf-8")).hexdigest()
        if h not in seen:
            seen.add(h)
            result[name] = text
    return result

不过更多情况下,转载文章并非原样复制------改个标题、换两段导语,MD5 就失效了。这时就需要近似去重登场:将文本切分为 n-gram 词集合,用 MinHash 算法生成签名,再通过 LSH(局部敏感哈希)快速找出签名高度相似的文档对。

近似去重不需要重复造轮子。笔者在大模型训练全流程实战指南工具篇(五)------大模型训练全流程步骤详解与对应工具推荐中已推荐过不少工具,这里选用 datasketch 库进行演示。

首先执行 pip install jieba datasketch 安装依赖:

datasketch 提供的 MinHash + LSH 组合方案,能以线性时间复杂度高效完成近似去重。其核心原理是:MinHash 将每篇文章的分词集合压缩为固定长度的整数签名,压缩后的签名仍能近似估计两篇文章的相似度;LSH 则通过将签名切分为多个块并分别哈希,确保相似文章至少在一个块上发生碰撞,从而被快速召回为候选对,避免全局两两比较。完整实现代码如下:

python 复制代码
import jieba
from datasketch import MinHash, MinHashLSH

# 1. 模拟文章数据(ID -> 正文)
articles = {
    1: "人工智能是计算机科学的一个分支,致力于开发能够执行人类智能任务的系统。",
    2: "人工智能是计算机科学的分支,专注于创建能完成人类智能活动的系统。",  # 近似重复
    3: "今天天气很好,适合出门散步和运动。"
}

# 2. 分词与预处理
def tokenize(text):
    return [w for w in jieba.lcut(text.lower()) if len(w) > 1]  # 过滤单字

# 3. 生成 MinHash 签名(perm=128)
def get_minhash(text, num_perm=128):
    mh = MinHash(num_perm=num_perm)
    for w in tokenize(text):
        mh.update(w.encode('utf-8'))
    return mh

# 4. 构建 LSH 索引(相似度阈值 0.8)
lsh = MinHashLSH(threshold=0.8, num_perm=128)
for aid, txt in articles.items():
    lsh.insert(aid, get_minhash(txt))

# 5. 查询相似并标记重复(使用并查集处理传递性)
parent = {aid: aid for aid in articles}

def find(x):
    while parent[x] != x:
        parent[x] = parent[parent[x]]
        x = parent[x]
    return x

def union(x, y):
    rx, ry = find(x), find(y)
    if rx != ry:
        parent[ry] = rx

for aid, txt in articles.items():
    mh = get_minhash(txt)
    candidates = list(lsh.query(mh))          # 0.4.6+ 返回生成器
    for cid in candidates:
        if cid != aid:
            # 可选:二次精确验证(提高准确率)
            # if mh.jaccard(get_minhash(articles[cid])) >= 0.8:
            union(aid, cid)

# 6. 提取去重后的文章(每个并查集簇保留一个代表)
unique_ids = []
seen_roots = set()
for aid in articles:
    root = find(aid)
    if root not in seen_roots:
        seen_roots.add(root)
        unique_ids.append(aid)

print("保留的文章ID:", unique_ids)   # 输出 [1, 3](2 被去重)

关键参数调优num_perm(签名长度,默认128)越大精度越高但内存增加;threshold 阈值控制去重灵敏度,建议先在小样本上测试,0.7~0.9 为常见区间。对于百万级数据,可改用 MinHashLSHForest 提升检索速度;若需处理传递性重复,务必使用并查集合并所有关联项。

注意事项 :LSH 本质是近似算法,召回候选后建议用 mh.jaccard() 精确计算相似度作为二次过滤;中文分词质量直接影响去重效果,可结合停用词表和自定义词典优化。整套方案在保持高速的同时,能有效识别"洗稿"或局部改写,是工业级文本去重的实用利器。

2.2 质量过滤:把噪声挡在训练集外

社区爬取的数据里,混着广告、导航栏残留和标题党内容。这些低质数据是大模型训练无法容忍的。质量过滤这道工序的目标很明确:宁可错杀少量低质文本,也不让噪声进入训练集

笔者先实现规则过滤,覆盖绝大多数明显噪声:

python 复制代码
AD_KEYWORDS = ["扫码关注", "点击阅读全文", "加微信", "领取资料"]

def quality_filter(text: str) -> bool:
    """返回 True 表示文本通过过滤(保留)"""
    # 1. 有效字数:去掉 Markdown 符号和空白后统计
    plain = re.sub(r"[#>\-|`!()\[\]]", "", text)
    plain = re.sub(r"\s", "", plain)
    if len(plain) < 100:
        return False
    # 2. 特殊字符占比:乱码、OCR 残留的典型特征
    special_ratio = len(re.findall(r"[^\w\s\u4e00-\u9fa5]", text)) / max(len(text), 1)
    if special_ratio > 0.3:
        return False
    # 3. 广告关键词命中
    if any(kw in text for kw in AD_KEYWORDS):
        return False
    return True

三条规则各管一类噪声:

规则 针对的噪声 示例
有效字数下限 导航页、目录页、摘要碎片 "首页 / 安全资讯 / 正文"
特殊字符占比 OCR 乱码、编码损坏文本 大段的 锟斤拷 或符号串
广告关键词 导流话术 "扫码关注公众号领取资料"

当然,实际工程中规则往往不只这三条,笔者所在公司的过滤规则多达上百种,大家可根据自身需求灵活扩展。

那如果规则过滤拿不准该怎么办呢?这时候就轮到"万事不绝问大模型"了。如果公司已有本地部署的大模型,一个简单有效的做法是编写自动化脚本调用模型 API,通过提示词将有问题的文本剔除:

python 复制代码
from openai import OpenAI

client = OpenAI(
    api_key=os.environ["DASHSCOPE_API_KEY"],
    base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
)

def model_quality_filter(text: str) -> bool:
    """调用大模型判断文本是否包含有效安全知识"""
    prompt = f"""你是一名网络安全数据质量评估专家。请判断以下文本是否包含有效的网络安全知识。

判断标准:
1. 文本是否包含实质性的安全技术内容(如漏洞分析、攻防技术、安全规范等)
2. 文本是否有明确的技术价值,而非广告、导航、无意义内容
3. 文本是否适合作为大模型预训练语料

请仅返回一个数字:1表示通过(有有效知识),0表示不通过(无有效知识)。

待评估文本:
---
{text}
---

请判断(仅返回0或1):"""

    completion = client.chat.completions.create(
        model="qwen-plus",
        messages=[{"role": "user", "content": prompt}],
    )
    result = completion.choices[0].message.content.strip()
    return result.startswith("1")

除了上述提示词,大家还可以设计更系统化的评估方案------比如为每个规则设定评分项,最后综合打分并剔除低于阈值的文本。不过需要注意,模型过滤速度较慢且成本较高,实践中通常与规则过滤结合使用,只对规则过滤后存疑的数据进行模型二次判断,兼顾效率与质量。

2.3 隐私脱敏:安全领域的必修课

隐私脱敏并非网络安全语料的专属需求,而是所有领域数据都需重视的一环。安全文章中最不缺的就是真实 IP、域名、邮箱、内网主机名------尤其是事件复盘和应急响应报告,经常包含受害单位的真实资产信息。如果把这些原样喂给模型,模型可能在未来的回答中"背"出这些敏感内容。安全模型先自己泄了密,这无论如何说不过去。

在日常工作中,隐私脱敏同样采用两段式处理

首先用正则实现基础脱敏,下面代码覆盖了三类最常见的敏感信息:

python 复制代码
def desensitize(text: str) -> str:
    # IP 地址 → 占位符
    text = re.sub(r"\b(?:\d{1,3}\.){3}\d{1,3}\b", "[IP已脱敏]", text)
    # 邮箱地址 → 占位符
    text = re.sub(r"[\w.-]+@[\w.-]+\.\w+", "[邮箱已脱敏]", text)
    # 口令类字段(password=xxx 等)
    text = re.sub(r"(password|passwd|pwd)\s*[=:]\s*\S+",
                  r"\1=[已脱敏]", text, flags=re.I)
    return text

不过正则覆盖不了所有情况------比如叙述句中的"运维人员账号 admin_zhang"、内部系统名称等。对高敏感的报告类语料,笔者会再交给大模型过一遍,由模型识别并替换人名、账号等实体:

python 复制代码
from openai import OpenAI

client = OpenAI(
    api_key=os.environ["DASHSCOPE_API_KEY"],
    base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
)

def model_desensitize(text: str) -> str:
    """调用大模型识别并替换敏感实体"""
    prompt = f"""你是一名数据安全脱敏专家。请识别并替换以下文本中的敏感信息。

需要脱敏的敏感信息类型:
1. 人名(如:张三、admin_zhang)→ 替换为 [人名已脱敏]
2. 账号/用户名(如:admin、root、user123)→ 替换为 [账号已脱敏]
3. 内部系统名/主机名(如:server-01、db-master)→ 替换为 [主机名已脱敏]
4. 手机号(如:13800138000)→ 替换为 [手机号已脱敏]
5. 身份证号 → 替换为 [身份证号已脱敏]
6. 银行卡号 → 替换为 [银行卡号已脱敏]

注意:
- 保留 CVE 编号、版本号、技术术语等非敏感技术信息
- 仅替换明确的敏感实体,不要过度脱敏
- 直接返回脱敏后的文本,不要添加额外说明

待脱敏文本:
---
{text}
---

请返回脱敏后的文本:"""

    completion = client.chat.completions.create(
        model="qwen3.7-plus",
        messages=[{"role": "user", "content": prompt}],
    )
    return completion.choices[0].message.content.strip()

同样,脱敏后的数据需要按 5%~10% 的比例进行抽检,确认没有"漏网之鱼",同时确保没有误伤正常的技术内容。经过以上三道工序------去重、质量过滤、隐私脱敏,Markdown 文档就完成了从"原料"到"有效数据集"的蜕变。

以上就是本篇文章的全部内容。本文完整代码可关注笔者同名公众号:大模型真好玩 ,并私信 大模型训练 免费获取。

三、总结

本期内容,我们走完了预训练数据治理的完整流水线。从异构文档到可用语料,全过程可以概括为两大阶段、五道工序

阶段一:结构化解析------先用 MinerU 将 PDF、docx 等异构文档统一解析为 Markdown 格式,再调用 Qwen-VL 多模态模型为图片批量生成语义描述并替换原始引用,完成从"异构原料"到"结构化半成品"的转换。

阶段二:清洗与脱敏------按照"去重 → 质量过滤 → 隐私脱敏"的顺序执行三道清洗工序,去除重复内容、过滤低质噪声、替换敏感实体,确保输出数据的多样性与合规性。

至此大家手里已经有了一份干净、结构化的 Markdown 语料库。在下一篇文章中,笔者将为大家演示如何将这些有效数据进一步转化为预训练所需的格式,并使用 LLaMAFactory 进行大模型的增量预训练。敬请期待!

除大模型训练外,笔者也在同步更新《深入浅出LangChain&LangGraph AI Agent 智能体开发》免费专栏,要说明该专栏适合所有对 LangChain 感兴趣的学习者,无论之前是否接触过 LangChain。该专栏基于笔者在实际项目中的深度使用经验,系统讲解了使用LangChain/LangGraph如何开发智能体,目前已更新 50 讲,并持续补充实战与拓展内容。欢迎感兴趣的同学关注笔者的掘金账号与专栏,也可关注笔者的同名微信公众号大模型真好玩 ,每期分享涉及的代码均可在公众号私信: LangChain智能体开发免费获取。

相关推荐
RSABLOCKCHAIN1 小时前
打造工业级多智能体量化交易闭环:基于 Alpaca、LangGraph 与双轨执行的智能投资与持仓治理系统
人工智能·python·ai·aigc
光锥智能1 小时前
HUAWEI WATCH 6系列正式发布:鸿蒙AI手表,智慧健康旗舰
人工智能·华为·harmonyos
晓窗科技1 小时前
AI基座哪家服务好
大数据·人工智能·python
高洁011 小时前
AI智能体落地价值:从工具赋能到产业重构,解锁企业降本增效新逻辑
人工智能·深度学习·机器学习·transformer·知识图谱
Fnetlink11 小时前
Fnet 云网安 260907
网络·人工智能·安全·web安全·网络安全·智能路由器
专业科讯1 小时前
品牌企业想做GEO优化,如何选择适合自己的服务公司?
人工智能