CodeBERT实战:从代码搜索到代码补全
【免费下载链接】CodeBERTCodeBERT项目地址: https://gitcode.com/gh_mirrors/co/CodeBERT
凌晨接手一个三年没人动的遗留库,第二天评审要你用自然语言回答:“哪些函数会改全局配置?”grep 只认字符串,而 CodeBERT——微软开源的代码预训练模型系列,主打代码搜索、代码补全与代码摘要——正是为这类 NL-PL(自然语言-编程语言)任务设计的。它适合做代码检索系统、NL2Code 原型、遗留代码问答的开发者。
30秒速览:六个模型各管一摊
这个仓库是微软 6 个代码预训练模型的官方实验代码:CodeBERT(EMNLP 2020)、GraphCodeBERT(ICLR 2021)、UniXcoder(ACL 2022)、CodeReviewer(FSE 2022)、CodeExecutor(ACL 2023)、LongCoder(ICML 2023)。它们解决同一个问题:代码和自然语言活在两个世界——所有模型都在 CodeSearchNet 的 NL-PL 对上预训练,覆盖 Python、Java、JavaScript、PHP、Ruby、Go 六种语言,无需你自训模型就能直接拿嵌入和生成能力。
| 模型 | 架构形态 | 能干什么 | 上下文长度 |
|---|---|---|---|
| CodeBERT | 双向 Encoder | NL-PL 嵌入、代码搜索、克隆检测 | 512 |
| GraphCodeBERT | Encoder + 数据流图 | 代码搜索/翻译/重构/克隆检测 | 512 |
| UniXcoder | Enc / Dec / Enc-Dec 三模式 | 搜索、补全、摘要、函数名与 API 推荐 | 512 |
| LongCoder | 稀疏长程 Transformer | 长上下文代码补全(LCC) | 3968 |
| CodeExecutor | Encoder-Decoder | 预测代码执行轨迹,需多卡 | 1024 |
它是怎么跑起来的:先当翻译,再当电路工
可以把 RoBERTa 想象成只读过中文小说的翻译,而 CodeBERT 是拿「文档 + 对应代码」的双语语料把他练出来的——所以输入序列长这样:自然语言,[SEP],代码。预训练用两个任务:MLM(随机掩码猜 token)和 RTD(Replaced Token Detection,随机替换若干 token,让模型判断哪些被换过)。RTD 逼着模型盯住<和>、==和=这种细粒度差异,这正是嵌入能力强的原因。
💡反直觉的点:codebert-base是 RTD 路线,官方明确说它不适合 mask 预测,补全/填空必须换codebert-base-mlm,两个 checkpoint 不通用。
GraphCodeBERT 在此基础上多走一步:用 tree-sitter 把代码解析成数据流图(DFG),图中节点(如变量定义)的向量由它覆盖的所有 token 嵌入归一化平均得到——GraphCodeBERT/codesearch/model.py里核心就几行 einsum。类比一下:普通模型读代码像读逐字稿,GraphCodeBERT 像看电路图,知道数据从哪流到哪。效果在官方过滤版数据集中:代码搜索整体 MRR 0.713,CodeBERT 是 0.693,纯 RoBERTa 只有 0.617。
跟着做:从跑通到玩起来
先拿到仓库和依赖:
git clone https://gitcode.com/gh_mirrors/co/CodeBERT pip install torch transformers最小可运行示例:给一段自然语言和一段代码各算一个向量,看余弦相似度。取[CLS]位表示作为整段语义向量,并做归一化:
import torch from transformers import AutoTokenizer, AutoModel device = torch.device("cuda" if torch.cuda.is_available() else "cpu") tokenizer = AutoTokenizer.from_pretrained("microsoft/codebert-base") model = AutoModel.from_pretrained("microsoft/codebert-base").to(device) def encode(text): ids = tokenizer(text, return_tensors="pt", max_length=512).to(device) with torch.no_grad(): # 推理期省显存,关掉反向图 emb = model(**ids).last_hidden_state[:, 0] return torch.nn.functional.normalize(emb, dim=1) nl = encode("return the maximum value") code = encode("def max(a,b): if a>b: return a else return b") print(torch.cosine_similarity(nl, code, dim=1))预期输出是一个标量:这对语义匹配的 NL-PL 得分会明显高于「return the maximum value」配上一个求最小值的函数。仓库 README 里还有一个 token 级嵌入的完整示例,可对照验证。
拿到向量后,最顺手的组合玩法是代码检索:把代码库按函数切块、批量 encode、灌进 FAISS 或 pgvector,查询走自然语言。想省内存就按批 encode 再释放。
进阶:填空式代码修复。换 MLM 版 checkpoint,用 pipeline 三行搞定:
from transformers import RobertaForMaskedLM, RobertaTokenizer, pipeline model = RobertaForMaskedLM.from_pretrained("microsoft/codebert-base-mlm") tokenizer = RobertaTokenizer.from_pretrained("microsoft/codebert-base-mlm") fill_mask = pipeline('fill-mask', model=model, tokenizer=tokenizer) print(fill_mask("if (x is not None) <mask> (x>1)"))预期输出(官方示例的真实分数):候选依次为and(0.6049)、or(0.3068)、if(0.0213)……and高出一大截,说明模型真的理解了条件句逻辑,而不是在做词频猜测。
补全和摘要这类生成任务,CodeBERT 系列交给 UniXcoder,它的 decoder-only 模式开箱即用:
import torch from unixcoder import UniXcoder # 仓库 UniXcoder/ 目录下的封装类 device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model = UniXcoder("microsoft/unixcoder-base").to(device) context = """ def f(data,file_path): # write json data into file_path in python language """ tokens_ids = model.tokenize([context], max_length=512, mode="<decoder-only>") source_ids = torch.tensor(tokens_ids).to(device) predictions = model.decode(model.generate( source_ids, decoder_only=True, beam_size=3, max_length=128)) print(context + predictions[0][0])预期输出(官方示例):
def f(data,file_path): # write json data into file_path in python language data = json.dumps(data) with open(file_path, 'w') as f: f.write(data)encoder-decoder 模式还能预测函数名(write_json/write_file)和推荐 API(json.dumps排第一)。想在自己的语料上复现代码搜索微调,CodeBERT/codesearch/目录给了完整流程:先process_data.py处理数据,再用run_classifier.py微调,关键参数照抄官方:学习率 1e-5、8 个 epoch、序列长 200,官方在 2 卡 P100 上完成训练,最后用mrr.py评测。
和谁比:系列内部先选型
这六个模型同仓同源(均为 MIT 协议、HF 上可一键加载),差异在形态而非生态,真正的选型是「任务匹配」:
| 维度 | codebert-base | graphcodebert-base | unixcoder-base |
|---|---|---|---|
| 上手难度 | 低,一行 HF 加载 | 中,需 tree-sitter 依赖 | 低,需仓库内封装类 |
| 适用场景 | NL-PL 嵌入、检索、克隆检测 | 结构化代码理解,检索 MRR 更高 | 理解 + 生成一体(补全/摘要/命名) |
| 上下文 | 512 | 512 | 512 |
| 许可证 | MIT | MIT | MIT |
明确建议:只要嵌入做代码搜索,选 codebert-base,最轻;要生成代码、写摘要,选 UniXcoder,CodeBERT 本体是纯 encoder,没有解码头;搜索 MRR 想抠出那 2 个点,选 GraphCodeBERT;函数超过 512 token,选 LongCoder(源长 3968);想学执行轨迹预训练,看 CodeExecutor,但它默认 8 卡分布式,单机基本跑不动。
踩坑实录:推理与微调常见问题
- 症状:
fill_mask输出全是垃圾词 →根因:base 版是 RTD 预训练,不是 MLM →解法:换microsoft/codebert-base-mlm。 - 症状:对 base 版调
model.generate()得到乱码 →根因:纯 encoder 没有语言模型头 →解法:生成任务改用 UniXcoder 或 -mlm 版。 - 症状:
my-languages.so加载崩溃或 tree-sitter 报错 →根因:预编译产物与本地 tree-sitter 版本不匹配 →解法:进parser/目录执行bash build.sh重编译。 - 症状:长函数嵌入相似度不稳定 →根因:超过 512 token 被截断,语义被砍半 →解法:按 AST 切块嵌入,或换 LongCoder。
- 症状:代码搜索微调 MRR 远低于论文值 →根因:学习率、epoch、序列长偏离官方配置 →解法:照抄 lr 1e-5 / 8 epochs / max_seq_length 200 再谈超参。
- 症状:CodeExecutor 预训练 OOM →根因:官方配置是 8 卡 + block_size 1024 →解法:调小
per_gpu_train_batch_size,用gradient_accumulation_steps补有效批量。 - 症状:同一函数的 NL-PL 相似度意外偏低 →根因:代码里注释、链接、特殊字符没清洗 →解法:先
remove_comments并保证代码可被 AST 解析,这是 GraphCodeBERT 数据清洗的四条规则之一。 - 部署建议:CPU 上批量算嵌入,务必
torch.no_grad()+ 分批推理,能上 CUDA 就上 CUDA,fp16 可再省一半显存;嵌入结果建议 L2 归一化后直接存,查询侧省一次 normalize。
收个尾
CodeBERT 系列给你一双代码的「双语眼睛」:嵌入用 CodeBERT,生成用 UniXcoder,长上下文用 LongCoder。下一步:把上面第一段嵌入代码跑起来,亲眼看看一个函数和它的文档向量靠得多近。
【免费下载链接】CodeBERTCodeBERT项目地址: https://gitcode.com/gh_mirrors/co/CodeBERT
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考