Optimizing Token Choice for Code Watermarking: An RL Approach¶
会议: ICML 2026
arXiv: 2508.11925
代码: https://github.com/TimeLovercc/CodeTracer (有)
领域: LLM安全 / 代码水印 / 强化学习
关键词: 代码水印, GRPO, Gumbel-Top-k, 直通估计, z-score
一句话总结¶
CodeTracer 在冻结的 code LLM 旁边挂一个小的 watermark policy 网络,用 GRPO + 双奖励(执行通过 + z-score)+ Gumbel-Top-k 直通估计联合学习"在哪个 token 位置加水印、选哪一组 green token",在几乎不掉 Pass@1 的前提下把代码水印的检测 AUROC 从 ~70% 抬到 ~78%。
研究背景与动机¶
领域现状:主流 LLM 水印(Kirchenbauer 2023 的 green-red 方案)在生成时把词表随机切成 green/red 两半,对 green token 加固定 logit bias \(\delta\),检测时统计绿 token 频次做 z-test。在自然语言上效果不错,因为大多数位置允许多个语义等价的 token。
现有痛点:代码生成场景里 (1) 大量位置是语法强制的(def、括号、关键字),改动直接编译失败;(2) 不同位置对扰动的容忍度异质(变量名能改、API 名不能改);(3) 低熵分布让无差别 bias 容易把代码改坏。早期 SWEET、CodeIP 等方法要么需要在检测时拿到原 LLM 的 logits 或 prompt 计算熵,要么需要手写每种语言的语法变换规则,实际部署门槛高。
核心矛盾:水印的"统计可检测性"与代码的"功能正确性"在低熵、强语法约束下是直接对抗的——加得弱检测不出来,加得强代码跑不通。
本文目标:(i) 自动判断哪些位置可以安全加水印,(ii) 在可加位置选一组保功能的 green token 集合 \(G\),(iii) 检测端不依赖原 LLM。
切入角度:把"是否加水印 \(w\)"和"green 集合 \(G\)"建模成一个上下文相关的 policy \(\pi_\phi(a\mid\mathbf{c})\),与冻结 LLM \(\pi_\theta\) 组合成 \(\pi_{\theta\oplus\phi}\),让强化学习自己去学语法/语义约束——因为代码同时具备两类天然 verifiable reward:单元测试通过与否、z-score 高低。
核心 idea:把代码水印改写成"训练一个小的 policy 网络去 bias LLM 的下一 token 分布"的 RL 问题,用 GRPO + STE + Gumbel-Top-k 把离散决策塞进端到端梯度里。
方法详解¶
整体框架¶
CodeTracer 想解决的是:在低熵、强语法约束的代码上,怎么既把水印加得统计可检测、又不把代码改坏。做法是在冻结的 code LLM 旁边挂一个小的 watermark policy 网络,由它逐位置决定"这个 token 位置加不加水印、加的话用哪一组 green token",再把决策叠回 LLM 的下一 token 分布上。具体地,prompt \(\mathbf{x}\) 进来后冻结 LLM \(\pi_\theta\) 算出 logits \(\mathbf{l}\in\mathbb{R}^{|\mathcal{V}|}\),可训练 policy \(\pi_\phi\) 看一个固定窗口的上下文 \(\mathbf{c}\) 输出 \((w, G)\)——\(w\in\{0,1\}\) 是否加水印、\(G\subset\mathcal{V}\) 是大小 \(k=\lfloor\gamma|\mathcal{V}|\rfloor\) 的 green 集合;组合出的 watermarked logits \(\tilde{l}_j = l_j + w\cdot\delta\cdot\mathbb{1}_{v_j\in G}\) 经 softmax 采样得到 \(\tilde y_t\)。检测时只用 \(\pi_\phi\) 重放每个位置的 \((w, G)\),在 \(w=1\) 的子集上统计落进 \(G\) 的频次做单比例 z-test \(z = (N_G - T\gamma)/\sqrt{T\gamma(1-\gamma)}\),全程不碰原 LLM。
%%{init: {'flowchart': {'rankSpacing': 24, 'nodeSpacing': 28, 'padding': 6, 'wrappingWidth': 400}}}%%
flowchart TD
X["prompt x"] --> POL["策略化水印 π_φ(~118M 旁路)<br/>看上下文窗口 → 动作 (w, G)"]
X --> LLM["冻结 code LLM π_θ → logits l"]
POL --> STE["STE + Gumbel-Top-k<br/>把 w 硬开关、G 选 k 个变可微"]
STE --> CMB["叠加偏置 l_j + w·δ·1[v∈G]<br/>→ softmax 采样"]
LLM --> CMB
CMB --> CODE["watermarked code"]
CODE -->|"检测:仅用 π_φ"| DET["重放每位置 (w, G)<br/>对 w=1 子集做 z-test"]
CODE -->|"训练 rollout"| RW["GRPO + 三路奖励<br/>R1 执行 + R2 z-score + R3 token级 → 优势"]
RW -.->|"更新 π_φ(θ 冻结)"| POL
关键设计¶
1. 策略化水印:把固定的 green-red 划分换成上下文相关、可学习的旁路策略
Kirchenbauer 那套水印对每个位置都用同一个随机切分和固定 \(\delta\),在代码里既区分不出"变量名能改、API 名不能改",也没法躲开语法强制的位置。CodeTracer 的做法是训练时把 LLM 参数 \(\theta\) 全冻结,只学一个约 118M 的 watermark 模型 \(\phi\)(相对 1.5B base LLM 不到 10% 参数)。\(\pi_\phi\) 是个小 Transformer,输出 \((|\mathcal{V}|+1)\) 维向量 \((w_\phi, \mathbf{l}_\phi)\),\(w_\phi\) 决定 \(w\)、\(\mathbf{l}_\phi\) 决定 \(G\) 的排序,于是"在哪加、选哪组"都成了随上下文变化的策略。冻结 LLM 是关键取舍:它绕开了 fine-tune LLM(如 Xu et al. 2024)那种对代码能力的不可预期破坏,也让 \(\pi_\phi\) 变成一个纯旁路模块——训练时只挂 1.5B,推理时可以直接 plug-in 到没见过的 8B 上;检测端同样只持有 \(\pi_\phi\) 就能复现 \((w, G)\),不依赖任何 base LLM。
2. STE + Gumbel-Top-k:把 \((w, G)\) 的离散决策塞进端到端梯度
\(w\in\{0,1\}\) 的硬开关和"从 \(|\mathcal{V}|\) 里 top-\(k\) 选出 \(G\)"都是离散操作,梯度过不去,policy 就没法和 GRPO 的策略梯度联合训练。对 \(w\),用 Straight-Through Estimator \(w = \mathbb{1}_{w_\phi>0} + \sigma(w_\phi) - \text{sg}(\sigma(w_\phi))\),前向走硬阈值、反向沿 \(\sigma\) 的梯度。对 \(G\),用 Gumbel-Top-\(k\):先给 logits 加 Gumbel 噪声 \(\mathbf{g} = \mathbf{l}_\phi + (-\log(-\log \mathbf{u}))\)(\(\mathbf{u}\sim\text{Uniform}(0,1)^{|\mathcal{V}|}\))取 top-\(k\) 得到 \(G\),再用 \(\mathbf{l}_G = \mathbb{1}_{v\in G} + \mathcal{S}(\mathbf{g}) - \text{sg}(\mathcal{S}(\mathbf{g}))\) 形式的 indicator 前向硬选、反向沿 Gumbel-Softmax 松弛。选 Gumbel-Top-\(k\)(Xie & Ermon 2019)而不是普通 categorical reparam,是因为后者只能近似单个采样,处理不了"固定选 \(k\) 个";而 green set 本质就是固定基数的子集采样,Gumbel-Top-\(k\) 正好对得上——离散侧保住了水印的统计可验证性,连续侧又能把梯度喂回 \(\pi_\phi\)。
3. GRPO + 三路奖励:在零标注下,让策略自己学会"该加哪、该选哪组"
没有现成的"水印代码"训练数据,但代码天然带两类 verifiable signal——能不能跑通、z-score 高不高——所以这里直接套 DeepSeek-R1 的 GRPO,用三路奖励驱动。\(R_1\) 是执行 reward(全部 test case 通过给 1、否则 0),作为保功能的硬约束;\(R_2\) 是饱和 z-score reward(\(z\geq 3\) 给 1、\(0<z<3\) 线性、\(z\leq 0\) 给 0),逼检测显著性往上走;\(R_3\) 是 token 级 process reward(\(w_t=1\) 且 \(s_t\in G_t\) 给 \(+1\)、落 red 给 \(-1\)、不加水印给 0)。三者经优势函数 \(\hat A(s_t, a_t) = (A_1 + A_2)\cdot\mathbb{1}_{\text{is\_code}}(s_t)\) 合流:outcome 级 \(A_1\) 与 token 级 \(A_2\) 相加后再用 \(\mathbb{1}_{\text{is\_code}}\) 屏蔽非代码 token,避免在自然语言 chain-of-thought 段落上白白消耗水印预算。引入 \(R_3\) 是这里最要紧的一步——纯 outcome 奖励对序列里每个 token 都给同样的信号,对"哪些位置该加"指导太粗;补上 token 级即时反馈后训练收敛和最终性能都明显上去(消融里去掉 \(R_3\),AUROC 掉 7.84pp、TPR 掉 16.05pp)。
损失函数 / 训练策略¶
最终目标是带 KL 正则的 GRPO clipped objective:
\(\max_\phi \mathbb{E}_{s\sim\mathcal{D}}\left[\frac{1}{|s|}\sum_t \min\left(r_t(\phi)\hat A_t, \text{clip}(r_t(\phi), 1-\varepsilon, 1+\varepsilon)\hat A_t\right)\right] - \beta D_{\text{KL}}(\pi_{\theta\oplus\phi}\|\pi_{\text{ref}})\)
其中 \(r_t(\phi) = \pi_{\theta\oplus\phi}(s_t|s_{<t})/\pi_{\text{ref}}(s_t|s_{<t})\)。参考策略 \(\pi_{\text{ref}}\) 用 \(\pi_{\theta\oplus\phi}\) 的旧拷贝(self-referential)。训练流程是先用 SFT 让 \(\pi_\phi\) 学到代码 token 分布,再上 GRPO;整个训练在单卡 A100 上约 1 天完成。Base LLM 用 OpenCoder-1.5B-Instruct,\(\gamma=0.5\)、\(\delta\) 走标准设置。
实验关键数据¶
主实验¶
HumanEval / MBPP 上对比 post-hoc 检测(logp、LogRank、DetectGPT、GPTZero)和 active 水印(WLLM、EXP-edit、SWEET):
| 数据集 | 方法 | Pass@1 (%) | AUROC (%) | TPR@5%FPR (%) |
|---|---|---|---|---|
| HumanEval | Base (无水印) | 65.42 | – | – |
| HumanEval | WLLM | 58.05 | 70.17 | 20.73 |
| HumanEval | EXP-edit | 59.29 | 66.50 | 25.61 |
| HumanEval | SWEET† | 60.46 | 76.24 | 27.44 |
| HumanEval | CodeTracer | 62.65 | 77.71 | 32.32 |
| MBPP | Base | 43.35 | – | – |
| MBPP | WLLM | 39.66 | 76.44 | 27.80 |
| MBPP | SWEET† | 39.64 | 77.24 | 24.80 |
| MBPP | CodeTracer | 42.10 | 78.42 | 31.60 |
Post-hoc 方法 AUROC 普遍 47–52%(基本随机),代码场景已经不能依赖被动检测。CodeTracer 在 Pass@1 上相对其它水印掉点最少(HumanEval -2.77pp vs WLLM -7.37pp),同时 TPR 比次优高 ~5pp。1.5B 上训好的 \(\pi_\phi\) 直接套到 OpenCoder-8B 上 Pass@1 71.77%(vs Base 72.04%),AUROC 78.69%(vs WLLM 65.90%、EXP-edit 54.21%),plug-in 迁移成立。
消融实验¶
| 配置 | Pass@1 (%) | AUROC (%) | TPR (%) | 说明 |
|---|---|---|---|---|
| CodeTracer (full) | 60.82 | 82.95 | 46.34 | 完整三奖励 |
| w/o \(A_2\) (无 token 级 \(R_3\)) | 61.15 | 75.11 | 30.29 | 检测能力崩 |
| w/o \(A_1\) (无 outcome 奖励) | 60.34 | 79.52 | 34.91 | 两端都掉 |
| CodeTracer-1 (纯 RL, 无 SFT 初始化) | 62.65 | 77.71 | 32.32 | 保功能档 |
| CodeTracer-2 (SFT + RL) | 60.82 | 82.95 | 46.34 | 重检测档 |
关键发现¶
- 三个奖励里 process-level \(R_3\) 最关键:去掉它 AUROC 直接掉 7.84pp、TPR 掉 16pp,说明 token 级即时反馈对策略收敛的贡献远大于序列级的 z-score 反馈。
- SFT 初始化在 detectability 与 functionality 间提供一个明确旋钮(CodeTracer-1 vs -2),实际部署可按场景选档。
- 攻击鲁棒性:DIPPER 改写下 AUROC 58.42 vs WLLM 55.92;变量重命名下 AUROC 73.36 vs WLLM 70.91;攻击后仍保持领先但所有方法都明显退化。
- 推理 overhead 几乎可忽略:\(\pi_\phi\) 与 LLM 并行跑,附加延迟 < 100μs(LLM 本身 500–800ms),显存增加 < 0.5GB。
- 跨语言:Java / C++(HumanEvalPack)上 CodeTracer 表现一致,说明 RL 学到的是较通用的"哪些位置可改"先验,不是 Python 专属。
亮点与洞察¶
- 把"找可水印位置"自动化:之前的代码水印要么手写 AST 规则、要么依赖 LLM 自带 entropy。CodeTracer 直接用 RL 让 policy 自己学,省掉了一套和语言强耦合的工程。
- Gumbel-Top-k 用在 watermark 上很贴切:green set 本质就是"固定大小的子集采样",比传统 categorical reparam 更天然,思路可以迁移到任何需要"固定基数离散选择 + 端到端梯度"的场景(如稀疏注意力、可学习 prompt-token mask)。
- 三奖励里 process reward 占主导,违反"end-to-end + outcome only 更高级"的常见直觉:在 token 级标签便宜可得时,pure outcome RL 反而更慢、更差,提醒做 RLVR 时应该主动找 cheap dense signal。
- 检测端零 LLM 依赖:把 watermarking 做成"只携带 \(\pi_\phi\) 即可验证"的服务化形态,对 API 供应商更友好——可以把检测器交给第三方而不暴露 base LLM。
局限与展望¶
- 作者承认在 DIPPER 改写攻击下 AUROC 跌到 58.42%、TPR 仅 14.31%,对强语义改写仍脆弱;变量重命名鲁棒性也只是相对最好、绝对值掉得不少。
- 自己看到的局限:(1)训练成本仍需 RL rollout + 真实代码执行 sandbox,rollout 时的 test 执行延迟会显著拖慢训练,可扩展到的语言/库被沙箱覆盖度限制;(2)\(\gamma\)、\(\delta\) 等水印超参仍是固定全局值,文中没探索按位置自适应;(3)只在 1.5B/8B OpenCoder 上验证,更大尺寸(70B+)的 plug-in 是否仍稳定未知;(4)安全场景下还需考虑 adversary 拿到 \(\pi_\phi\) 后能否"反水印"——本文未做白盒攻击。
- 改进思路:把 \(\delta\) 也做成 policy 输出(按位置自适应强度),用 sandbox-less 的执行近似(learned reward model)替代真实 test 以提速;或者把 \(\pi_\phi\) 进一步蒸馏成 logit bias lookup table,让 detection 端连 transformer 都不用跑。
相关工作与启发¶
- vs WLLM (Kirchenbauer 2023a):WLLM 用 PRF 固定切 green/red、固定 \(\delta\),本文把这两个量都让 policy 学;在代码上 WLLM 掉 7+pp Pass@1,CodeTracer 只掉 ~3pp,说明可学习的位置/集合是低熵场景的关键。
- vs SWEET (Lee 2023):SWEET 用 entropy 阈值选哪些位置加水印,检测时要原 LLM 重新算 entropy;本文把"该加哪"内化进 \(\pi_\phi\),检测端零 LLM 依赖,部署更轻。
- vs CodeIP (Guan 2024):CodeIP 靠 token type 预测器 + 语法规则注入,强工程;CodeTracer 用 RL 自动学语法约束,跨语言泛化更好。
- vs Xu 2024 (RL for LM watermarking):Xu 2024 直接 fine-tune LLM 来学水印,可能破坏原能力;本文冻结 LLM 只学旁路 \(\pi_\phi\),更可控、可迁移。
评分¶
- 新颖性: ⭐⭐⭐⭐ 把代码水印重新表述成 policy 学习问题、用 Gumbel-Top-k + STE 解决离散梯度的 RL 公式很清晰;不过组件本身(GRPO、STE、Gumbel-Top-k)都是现成的。
- 实验充分度: ⭐⭐⭐⭐ HumanEval / MBPP / HumanEvalPack 三个 benchmark + 鲁棒性 + 迁移 + 消融齐全;缺更大模型(70B+)和针对 \(\pi_\phi\) 的白盒攻击实验。
- 写作质量: ⭐⭐⭐⭐ 问题动机层层推进,公式与算法表述准确;图表略多但可读。
- 价值: ⭐⭐⭐⭐ 代码水印是 LLM 安全里被严重低估的方向,给出了一个"训练一次、可插拔到更大 LLM、检测端零依赖"的范式,工业可用性高。