尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

大模型训练全流程实战指南实战篇(十六)——预训练数据集构建

发布时间:2026/9/24 17:56:58

资讯中心
01
ARTICLE

大模型训练全流程实战指南实战篇(十六)——预训练数据集构建

大模型训练全流程实战指南实战篇(十六)——预训练数据集构建
前言上篇文章介绍了数据治理的完整流水线先用 MinerU 把 PDF、docx 等异构文档统一解析成Markdown格式再调用多模态模型把图片替换为语义描述最后依次完成去重、质量过滤和隐私脱敏三道清洗工序。如果大家跟着笔者实操下来手里应该已经有了一批结构清晰的Markdown文档啦~但干净的文档还不等于可以直接训练的数据集。一篇博士论文动辄十几万字一篇漏洞分析可能只有三五百字篇幅长短不一。大家都知道大模型的上下文长度是有限的预训练阶段期望接收的是一条条规整的文本单元。本篇笔者要分享的就是如何将转化后的markdown文档切成合适的语义块并组装成LLamaFactory开箱即用的预训练数据集。一、认识预训练切片笔者知识库相关的文章中 一文带你了解RAG核心原理不再只是文档的搬运工 和 RAG系统效果不达预期一定要看看这篇详细高效的优化指南分享过固定长度、递归、文档结构等几种分块算法。不过那两篇的背景是RAG知识库现在面对预训练场景下的文本切片目标不同参数取法也略有不同。1.1 为什么不能把整篇文档喂给模型原因主要有以下三点长度差异过大书籍章节、CVE 描述、短篇资讯的篇幅相差数百倍直接一股脑全篇学习会导致大模型在批处理数据时样本长度严重不齐短文本被大量填充符浪费算力。模型存在上下文上限预训练配置里的cutoff_len大模型上下文长度笔者在 大模型训练全流程实战指南工具篇十—— 小白也能懂的大模型训练参数万字详解 中介绍过该参数决定了每次送入大模型的token长度超长文档必须断开。知识单元需要边界切片本质上是在告诉模型“这是一段相对完整的知识”边界切得合理模型学到的上下文依赖才完整。1.2 切片大小和上下文重合怎么定首先是切片长度会有些许不同RAG中切片通常只有三五百字因为RAG切片只承载“答案片段”靠检索命中而预训练要让模型读懂完整的论述过程块长建议要放宽到1000-2000字符。上下文重合长度也略有不同RAG中的overlap是为了方便检索时关键句被拦腰截断而预训练中重叠的内容会被模型重复学习重合过大既浪费token又容易造成局部过拟合所以一般不需要重合下面是笔者在处理本次网络安全预训练数据时使用的具体参数参数笔者取值作用与注意事项块长1500 字符目标块长。太小语义会被切碎太大长短样本失衡最小块长100 字符小于该长度的文本块并入上一块避免文本过短造成的残片单节最大切片数20超过则告警多半是塞进了整页表格或代码需要人工看一眼二、Markdown结构化切片实战2.1 切片器的设计思路当前大家手里已经有统一的markdown语料markdown格式的标题层级是天然的切片依据只需要在它的基础上完成如下操作首先定义后面反复用到的正则用来匹配标题和代码片段importos,re,glob,json,hashlib INPUT_DIR./cleaned_md# 第15篇产出的干净 Markdown 目录OUTPUT_DIR./llamafactory_dataCHUNK_PATHos.path.join(OUTPUT_DIR,chunks.jsonl)CHUNK_SIZE,MIN_SIZE1500,100MAX_SLICES20# 单节切片数告警阈值HEADING_REre.compile(r^(#{1,6})\s.$,re.MULTILINE)FENCE_REre.compile(r.*?,re.DOTALL)接下来按标题切分以#######标题为边界把文档拆开标题行保留在小节开头另外很多文档在第一个标题之前还有一段导言也要单独留出来defsplit_sections(md_text):按标题切成小节每节保留标题行标题前的导言单独成节matcheslist(HEADING_RE.finditer(md_text))ifnotmatches:return[(,md_text.strip())]sections[]ifmatches[0].start()0:headmd_text[:matches[0].start()].strip()ifhead:sections.append((,head))fori,minenumerate(matches):endmatches[i1].start()ifi1len(matches)elselen(md_text)sections.append((m.group(0).strip(),md_text[m.start():end].strip()))returnsections然后再把拆分的小节进一步拆分为段落一个小节可能由多段内容组成不同段落之间通过空行来区分。不过这里要注意的是一些代码块中存在#开头的注释行也存在空行因此应该先用正则将每段代码整块抠出来再对剩余的内容按空行切分成段落defatomic_paragraphs(block):拆出段落代码块整体保留其余按空行拆units,cursor[],0forminFENCE_RE.finditer(block):forpinre.split(r\n\s*\n,block[cursor:m.start()]):ifp.strip():units.append(p.strip())units.append(m.group(0).strip())# 围栏作为一个整体单元cursorm.end()forpinre.split(r\n\s*\n,block[cursor:]):ifp.strip():units.append(p.strip())returnunits经过上述拆分后的部分段落可能由于过分冗长导致超过了长度限制此时需要对长段落进一步处理先按句号拆分成句子再重新凑成 1500 字一组能不切开句子就不切万一某个句子单独就超过 1500 字才按字数硬切defsplit_oversized(unit,size,overlap):单个超长单元先按句子降级单句仍超长才硬滑窗代码块不切ifunit.lstrip().startswith():return[unit]sentsre.findall(r.?[。\n.!?;],unit,flagsre.DOTALL)or[unit]pieces,buf[],forsinsents:iflen(s)size:ifbuf:pieces.append(buf)bufforiinrange(0,len(s),size-overlap):pieces.append(s[i:isize])eliflen(buf)len(s)size:bufselse:pieces.append(buf)buf(buf[-overlap:]ifoverlapelse)sifbuf:pieces.append(buf)returnpieces除了过长的情况也可能出现某些段落的长度较短小于设定的chunk_size,这时候就需要使用贪心算法遍历段落如果下一段加入后还未超过chunk_size就与上一段放在一个切片中如果加入会超限制就新开一个切片实现代码如下defpack_units(units,size):把原子段落贪心装入块放不下就封箱开新箱超长单元先切后入库windows,cur[],[]defflush():ifcur:# 空箱子不封箱否则会混进空切片windows.append(\n\n.join(cur))cur[:][]forunitinunits:iflen(unit)size:flush()windows.extend(split_oversized(unit,size))continueifnotcurorlen(\n\n.join(cur))2len(unit)size:cur.append(unit)else:flush()cur.append(unit)flush()returnwindows以上针对不同情况的文档处理函数编写完成后就可以编写编排整篇文档的函数了。一篇文档里大节、小节往往混在一起比如一个 CVE 描述可能只有两三行单独成 chunk 太短。笔者给这些短小节准备了一个buf数组短小节先丢进buf几个凑够 1500 字后再一起成 chunk遇到长小节时先清空buf再让长小节按上面的规则自行切分。要是某个小节一口气切出 20 个以上 chunk多半是里面塞了整页表格或大段代码程序会打印一条告警提醒人工检查defchunk_markdown(md_text,source):chunks,buf[],[]fortitle,blockinsplit_sections(md_text):iflen(block)CHUNK_SIZE:ifbuf:# 先吐出积攒的短小节chunks.extend(pack_units(buf,CHUNK_SIZE))buf[]slicespack_units(atomic_paragraphs(block),CHUNK_SIZE)iflen(slices)MAX_SLICES:print(f[告警]{source}小节《{title[:20]}》切出{len(slices)}片请人工检查)chunks.extend(slices)else:buf.append(block)iflen(\n\n.join(buf))CHUNK_SIZE:chunks.append(\n\n.join(buf))buf[]ifbuf:chunks.extend(pack_units(buf,CHUNK_SIZE))merged[]# 过短块优先并入上一块forcinchunks:ifmergedandlen(c)MIN_SIZE:merged[-1]\n\ncelse:merged.append(c)iflen(merged)1andlen(merged[0])MIN_SIZE:merged[1]merged[0]\n\nmerged[1]# 首块没有上一块只能并入下一块mergedmerged[1:]records[]fori,textinenumerate(merged):cidhashlib.md5(f{source}-{i}-{text[:64]}.encode()).hexdigest()[:12]records.append({id:cid,source:source,index:i,text:text})returnrecords上面这段逻辑还有一个兜底处理完chunk后还会遇到一些零碎的不足最小字数的chunk,这时候直接丢弃还是会造成语义损失笔者一般会将这些小chunk并入前一个chunk的末尾如果这些小chunk的前一个chunk也比较短比如文档开头的导言只有几十个字那就只能将其并入下一个chunk的头部。为了信息的完整最后给每个chunk算一个id并记下它来自哪个文件、排第几后面判别环节淘汰任何一张卡片都能顺着这两个字段找回原文。最后编写入口函数遍历目录下的所有markdown文档逐篇切片每张卡片写成一行JSON存进chunks.jsonldefrun_chunk():filesglob.glob(os.path.join(INPUT_DIR,**,*.md),recursiveTrue)total0withopen(CHUNK_PATH,w,encodingutf-8)asf:forpathinfiles:withopen(path,encodingutf-8)asfp:recordschunk_markdown(fp.read(),os.path.basename(path))forrinrecords:f.write(json.dumps(r,ensure_asciiFalse)\n)totallen(records)print(f文档{len(files)}篇产出切片{total}个 -{CHUNK_PATH})最后测试一下脚本效果在指定目录下放入一份处理完成的 Markdown 文档执行脚本后可以看到指定文件夹内的 md 文档已经被切分成切片检查切片内容整体也比较完整。2.2 中间产物抽检运行完成如上代码之后会得到一个chunks.jsonl的文件每行表示一个切片。建议运行完成后任意抽几条人工看看确认一下标题有没有与正文正确衔接、代码块是否完整、参考文献区有没有混进来等不合理的地方这一步的中间产物抽检能够帮助检查一下切片策略的参数合不合理有不合理的地方再调整。三、大模型判别切片3.1 为什么切片还需要进一步大模型判别大家可能会问上篇文章已经做过大模型的文章质量判别为什么还需要对切片再过一次大模型主要是因为上篇文章还是文档级别的筛选即使是合格的文档切片后也会出现一些新问题比如论文正文质量很高但末尾的参考文献列表、致谢、作者简介会被切成独立的小块表格被分页截断后某个切片可能只剩半张表头OCR 残留的公式符号堆也可能集中在某一块里。这些碎片在整篇文档中占比很小文档级过滤感知不到切块后却成了独立样本必须再筛一遍。3.2 规则前置模型兜底鉴于工作实践中往往会考虑到节约成本不会将每个切片都用大模型提示词过滤一般会先用规则匹配快速筛除残片只有拿不准的切片才交给大模型判别。具体代码如下DROP_TITLES(参考文献,References,致谢,Acknowledgement,目录,作者简介,版权声明,版权所有)defrule_filter(rec):返回 (是否保留, 原因)textrec[text]plainre.sub(r[\s#*\-|\[\]()],,text)iflen(plain)MIN_SIZE:returnFalse,有效字数过少first_linenext((lnforlnintext.splitlines()ifln.strip()),)ifany(kinfirst_lineforkinDROP_TITLES):returnFalse,f无价值章节{first_line[:20]}lines[lnforlnintext.splitlines()ifln.strip()]cite_hitssum(bool(re.search(rhttps?://|\[\d\]|arxiv|doi,ln))forlninlines)iflinesandcite_hits/len(lines)0.5:returnFalse,疑似参考文献/链接列表symbol_ratiolen(re.findall(r[^\w\s\u4e00-\u9fa5],text))/max(len(text),1)ifsymbol_ratio0.35:returnFalse,特殊符号占比过高疑似乱码returnTrue,这四条规则分别对应字数残片、关键字匹配的无价值章节、参考文献列表和乱码情况。经过规则匹配后再将剩余段落交给大模型从多个维度进行评判。笔者一般会采用以下三个判别维度完整性标题后没有正文、半截表格、被截断的公式或代码均可判别为不合格。关联性广告导流、网站导航等与安全技术无关的内容。洁净度残留乱码、无意义符号堆砌和排版噪声等。针对以上三个维度编写的简单提示词如下。大模型服务沿用上一篇使用的阿里云百炼平台importtimefromconcurrent.futuresimportThreadPoolExecutor,as_completedfromopenaiimportOpenAI JUDGED_PATHos.path.join(OUTPUT_DIR,chunks_judged.jsonl)MAX_WORKERS8clientOpenAI(api_keyos.getenv(DASHSCOPE_API_KEY,),base_urlhttps://dashscope.aliyuncs.com/compatible-mode/v1,)JUDGE_PROMPT你是网络安全领域的语料质量审核员。请判断以下文本切片是否适合作为大模型预训练语料。 评估维度 1. 完整性不是标题残片、半截表格、被截断的公式或代码 2. 相关性包含网络安全或计算机技术的实质知识而非广告、导航、致谢、参考文献列表 3. 洁净度无 OCR 乱码、无意义符号堆砌、无导流信息 只输出 JSON{keep: 1或0, score: 0到10的整数, reason: 简短理由}defjudge_with_retry(rec,retries3):forattemptinrange(retries):try:respclient.chat.completions.create(modelqwen-plus,messages[{role:user,content:JUDGE_PROMPT\n---\nrec[text][:3000]}],response_format{type:json_object},temperature0,)datajson.loads(resp.choices[0].message.content)rec.update(keepbool(data.get(keep)),scoredata.get(score,0),judge_reasondata.get(reason,))returnrecexceptExceptionase:ifattemptretries-1:rec.update(keepTrue,score-1,judge_reasonf判别失败保留{e})returnrec# 失败默认保留避免接口抖动误杀数据time.sleep(2**attempt)defload_jsonl(path):ifnotos.path.exists(path):return[]withopen(path,encodingutf-8)asf:return[json.loads(line)forlineinfifline.strip()]defrun_judge():chunks,resultsload_jsonl(CHUNK_PATH),load_jsonl(JUDGED_PATH)done_ids{r[id]forrinresults}todo[]forrecinchunks:ifrec[id]indone_ids:continueok,whyrule_filter(rec)ifnotok:rec.update(keepFalse,score0,judge_reason规则过滤why)results.append(rec)else:todo.append(rec)withThreadPoolExecutor(MAX_WORKERS)aspool:futures{pool.submit(judge_with_retry,r):rforrintodo}fori,futinenumerate(as_completed(futures),1):results.append(fut.result())ifi%1000:# 每100条落盘一次中断后可续跑withopen(JUDGED_PATH,w,encodingutf-8)asf:forrinresults:f.write(json.dumps(r,ensure_asciiFalse)\n)print(f已判别{i}/{len(todo)})withopen(JUDGED_PATH,w,encodingutf-8)asf:forrinresults:f.write(json.dumps(r,ensure_asciiFalse)\n)droppedsum(1forrinresultsifnotr.get(keep))print(f判别完成共{len(results)}片剔除{dropped}片)执行结果如下对于刚才得到的 31 个切片剔除了其中 15 个。LLamaFactory 预训练数据集的格式非常简单JSONL 文件每行一个 JSON 对象对象中只需要一个text字段存放文本。如果大家有遗忘可以回顾文章 大模型训练全流程实战指南工具篇五——大模型训练全流程步骤详解与对应工具推荐 中的数据格式。只要把上面处理得到的chunks_judged.jsonl文件中每行的text字段提取出来就可以组织为数据。具体的预训练过程笔者会在下篇文章中详解同时说明预训练需要注意的相关事项。四、总结本篇围绕预训练数据集的构建先明确了预训练切片与 RAG 分块的目标差异给出了块长、最小块长等参数建议随后通过 Markdown 标题切分、段落原子化、超长单元降级、贪心装箱等步骤实现了结构化切片最后采用“规则前置、模型兜底”的策略对切片进行质量判别最终得到 LLamaFactory 可直接使用的chunks_judged.jsonl。下一篇笔者将正式进入预训练实战详细讲解LLamaFactory预训练过程及需要注意的关键事项大家敬请期待~
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。