15. 代码训练数据怎么处理?和自然语言数据有什么本质不同?
整理代码数据的清洗、去重、质量过滤与训练取舍。
简单回答
代码训练数据的处理和自然语言数据有几个本质差异:代码有明确的语法可以验证(不能解析的代码可以直接过滤)、有可执行性(能跑通的代码 vs 跑不通的代码质量差距巨大)、有强重复性(GitHub 上大量代码是 fork、copy 或自动生成的)、且许可证敏感(GPL 代码进商业模型有法律风险)。处理流程上要做语法过滤(解析失败的丢弃)、近重复去重(MinHash 不够还要 SimHash 或更精细的 AST 级别)、license 过滤(保留宽松许可证)、质量评分(行长度、注释比例、是否有测试)。Code LLaMA、StarCoder、DeepSeek-Coder 等模型的成功很大程度上来自数据处理质量。
详细解释
代码数据的特殊性
自然语言数据(网页、书籍、论文)的处理思路是"过滤明显垃圾、去重、按质量打分"。代码数据有几个根本不同:
有明确语法。一段 Python 代码要么能被 ast.parse 解析通过、要么解析失败。失败的代码极大概率是垃圾——可能是粘贴错误、压缩失败、文件损坏。这是自然语言不具备的强信号,过滤效率高得多。
有可执行性。能跑通且通过测试的代码质量远高于"看起来对但跑不通"的。理论上可以用执行结果作为质量信号,但实际成本高(要沙箱执行、要构造输入),只在小规模高质量数据集(HumanEval style)才用。
强重复性。GitHub 上大量代码是同一份代码的 fork、不同 license 重新发布、教程书籍代码的多次复用、auto-generated boilerplate。一段 Linux kernel 代码可能在几千个 repo 里以略微不同的形式出现。MinHash 这种 Jaccard 相似度去重在自然语言上够用,在代码上经常漏——因为代码的近重复是"逻辑相同但格式略变",要更精细的方式才能识别。
许可证敏感。代码有明确的 license——MIT/Apache/BSD 是宽松的,GPL/AGPL 是 copyleft 要求衍生品也开源,proprietary 是不允许使用的。模型训练时如果用了 GPL 代码,理论上模型本身或生成的代码可能受 GPL 约束。这是法律风险,多数商业模型选择只用宽松许可证的代码。
多语言、多框架的覆盖度。代码涉及 100+ 编程语言、每个语言又有不同框架版本。数据处理要考虑覆盖度——只用 Python 训出来的模型在 Rust 上能力很差。
处理流程
一个典型的代码数据处理 pipeline:
Step 1:原始数据采集
主要来源是 GitHub(官方 BigQuery 公开数据、GH Archive 流量、直接 git clone)、GitLab、Bitbucket、Stack Overflow、各种代码竞赛网站。
爬下来的原始数据通常是几百 TB 的 git 仓库,包含代码、commit history、issue、PR 等。处理时通常先剥出"当前 main/master 的代码文件"作为主要训练素材。
Step 2:语言识别和文件过滤
按文件扩展名 + 启发式(shebang line、特征关键字)识别文件语言。过滤掉:
- 二进制文件(.png、.jpg、.pdf 误进了 repo 的)
- 极大文件(>1MB 的源码文件,通常是生成的或脏数据)
- 极小文件(<10 行,信号不够)
- 自动生成的文件(vendored dependencies、压缩的 JS 库、proto 生成文件)
- 配置文件(除非专门训练这类)
Step 3:语法过滤
对每种语言用对应解析器尝试解析,解析失败的直接丢弃。Python 用 ast、JavaScript 用 acorn、C/C++ 用 tree-sitter(或 clang)。
这一步过滤率惊人——原始 GitHub 代码里大约 10-30% 的代码语法解析失败(不完整文件、依赖外部宏、模板语言混合在源码里、纯垃圾粘贴)。一刀切掉显著提升数据质量。
Step 4:许可证过滤
每个 repo 检查 LICENSE 文件,仅保留宽松许可证的代码。常见白名单:MIT、Apache 2.0、BSD(2/3-Clause)、ISC、Unlicense、CC0。GPL/AGPL/LGPL 通常排除(也有团队保留但单独标记,训练时按需控制)。没有明确 license 的 repo 通常视作 "all rights reserved" 排除。
工程上的难点是 license 检测的准确性——README 里写 "MIT" 不一定 LICENSE 文件就是 MIT;同一个 repo 里不同子目录可能有不同 license(dual-licensing)。BigCode 项目(StarCoder 背后)发布了一套 license 检测工具叫 ScanCode,准确率比简单文件名匹配高。
Step 5:去重
代码去重比文本更复杂。常见做法分几层:
- 完全重复(exact match):文件内容完全一致,按 hash 去重。这一步能砍掉 30-50% 的代码。
- 近重复(near-duplicate):内容只有少量不同(变量名、空白、注释)。MinHash + Jaccard 是基础方法,但对代码效果有限——因为变量重命名后文本相似度可能降到很低但实际是同一段代码。
- 语义重复:逻辑相同但实现细节不同。用 AST 级别去重(哈希语法树而不是字符串)能捕获更多重复。一些工作(如 SemDedup)用代码 embedding 的相似度做去重。
The Stack 数据集(StarCoder 用的)的研究表明,代码近重复去重做到位能让训练效率提升 30%(同样训练 step 数下 benchmark 表现更好)。
Step 6:质量评分
用启发式或学到的模型给每个文件打质量分。常见特征:
- 注释比例(太低可能没文档,太高可能是教学代码)
- 平均行长度(极长行往往是 minified 或自动生成)
- 标识符质量(变量名都是单字母 vs 有意义命名)
- 复杂度分布(圈复杂度过高可能是堆砌代码)
- 是否有测试(带 test 的 repo 平均质量高)
- Star 数和 fork 数(社区认可度的代理指标)
低质量代码不一定全删,但训练时可以给低权重。也有工作用一个小模型对代码做"quality classifier"打分,效果比纯启发式好。
Step 7:去敏感信息
代码里经常意外含有 secrets——API key、密码、私钥、个人信息。专门的扫描工具(detect-secrets、TruffleHog)必须跑过。检测到的 secret 要么删除整个文件、要么 mask 替换。
不做这一步训出来的模型可能在生成时直接吐出真实 API key——这是真实发生过的事故。
Step 8:数据增强(可选)
有些团队会对代码数据做增强——添加自然语言注释(用 LLM 生成)、合成代码-说明对("这段代码做什么")、合成单元测试。这些合成数据对代码理解和生成能力都有帮助。
训练阶段的特殊取舍
代码数据进入训练时还有一些特殊考虑:
Repo-level vs File-level:单文件训练丢失了跨文件的上下文(import 关系、API 使用习惯)。Repo-level 把同一个 repo 的文件按拓扑序拼成长上下文训练,能让模型学到跨文件依赖。但 Repo-level 数据需要更长的 context window,且 repo 内部还要小心去重。
自然语言 + 代码混合:commit message、README、issue 讨论里的自然语言和代码是天然配对的。这些数据让模型学到"用自然语言描述代码意图"——code-to-NL 和 NL-to-code 的双向能力。
代码与多语言能力的取舍:代码数据训多了模型在自然语言任务上能力会下降,反过来也成立。混合比例是核心 trade-off,主流模型一般 5-20% 代码数据。Code-specialized 模型(StarCoder、DeepSeek-Coder)的代码占比可以高到 50-90%。
Fill-in-the-Middle (FIM):代码场景常见的训练目标。把代码切成 prefix + middle + suffix,让模型学习根据 prefix + suffix 填出中间。这对代码补全能力提升明显,是 GitHub Copilot 类工具的核心训练技巧。
评测和过拟合
代码数据处理最容易踩的坑是 训练集泄露——HumanEval、MBPP、BigCodeBench 等评测题目直接出现在训练数据里,导致评测分数虚高但实际能力不如数字。
防范措施:
- 已知评测集的代码片段(题目描述、参考解、典型解)做 substring match 从训练数据中剔除
- 评测集的题目会以变体出现(StackOverflow 上有人贴过类似问题),近重复检测要严格
- 用 unseen evals(每月更新的新题目)作为补充评测,验证模型不是只会背诵
DeepSeek-Coder、Code-LLaMA 等团队都报告过早期版本因为训练集泄露导致 benchmark 分数虚高的故事,最终都做了系统性的去污染。
面试时可以这样答
代码数据和自然语言数据有几个本质差异。代码有明确语法可以解析验证,自然语言没有。代码有可执行性,能跑通的远比跑不通的有价值。代码重复性极强,GitHub 上大量代码是 fork 或 copy。代码许可证敏感,GPL 代码进商业模型有法律风险。
处理流程典型分八步。原始采集(GitHub、GitLab)→ 语言识别和明显垃圾过滤(二进制、极大极小、自动生成)→ 语法过滤(用每种语言的 parser 验证,10-30% 代码会被砍掉)→ license 过滤(白名单 MIT/Apache/BSD,排除 GPL)→ 多层去重(exact、MinHash 近重复、AST 级语义去重)→ 质量评分(注释比例、行长度、命名质量、有无测试)→ 去敏感信息(detect-secrets/TruffleHog 扫 API key 密码)→ 可选的合成数据增强。
训练阶段的特殊点。Repo-level 训练能学跨文件依赖但需要长 context。NL+ 代码混合让模型学双向能力。Fill-in-the-Middle 是代码场景的关键训练目标,对补全能力提升大。混合比例是核心 trade-off,通用模型 5-20% 代码,code-specialized 模型 50-90%。
最容易踩的坑是训练集泄露——HumanEval、MBPP 这些评测题被训练数据包含导致分数虚高。要做评测集 substring 剔除、近重复检测、用 unseen evals 验证。DeepSeek-Coder 这些团队都报告过早期版本污染问题,最终都做了系统性去污染。
工程价值上,The Stack 数据集证明了代码近重复去重做到位能让训练效率提升 30%。StarCoder、DeepSeek-Coder 的成功本质都是数据处理做得好。
常见追问
- AST 级去重具体怎么实现?变量重命名后 AST 还是会有差异,怎么处理?
- License 检测的 false positive 怎么办?误判 GPL 代码可能丢掉大量有用数据。
- Repo-level 训练时 repo 内部的文件顺序怎么定?随机还是按 import 拓扑?