AI蒸馏攻防-大模型文本水印
大模型文本水印Text Watermarking的底层核心是由马里兰大学的 Tom Goldstein 和 John Kirchenbauer 等人在 2023 年提出的伪随机词表切分机制。它巧妙地利用了密码学哈希函数和统计学假设检验在不改变大模型原本语义的前提下秘密地给文本盖上“数字印章”。一、 文本水印的数学原理文本水印的核心思想是在模型生成每一个词时暗中用一把“私钥”将词表动态划分为红绿榜并人为拉偏它们的选字概率。整个数学流程可以分为生成添加水印和检测提取水印两个阶段1. 水印生成阶段动态 Logits 偏置假设大模型的词表大小为 V。在生成第 t 个词 x_t 时模型已经输出了前一个词 x_{t-1}实际工程中通常会取前 n个词构成的窗口。伪随机切分系统使用一个秘密的哈希密钥 K计算前一个词的哈希值作为随机种子S \text{Hash}(x_{t-1}, K)划分红绿榜用这个种子 S初始化一个伪随机数生成器PRNG将整个词表 V随机划分为两部分绿名单Greenlist, G允许高频使用的词占总词表的比例为 \gamma通常取 \gamma 0.5。红名单Redlist, R被限制使用的词占总词表的比例为 1 - \gamma。注入偏置Bias模型原本输出的原始未归一化概率Logits为 l_t。水印系统对属于绿名单的词强行加上一个扰动正值 \deltal_t[i] \begin{cases} l_t[i] \delta \text{if } i \in G \\ l_t[i] \text{if } i \in R \end{cases}采样输出最终通过 \text{Softmax}(l_t)得到新的概率分布并进行常规采样。因为 \delta 0$模型会以极高的概率倾向于选择绿名单中的词。2. 水印检测阶段单侧 Z 检验当拿到一段长度为 N的未知文本时检测方因为拥有相同的密钥 K可以完全复现大模型在生成每一步时的红绿榜划分。检测方在整段文本中统计实际上命中了多少个“绿名单词”记为 N_G。零假设Null Hypothesis, H_0该文本是人类写的或者是由未加水印的模型生成的。此时每个词命中绿名单的概率是完全随机的即独立同分布的伯努利试验概率为 \gamma。统计学指标在零假设下N_G 的期望值和方差分别为\mu \gamma N, \quad \sigma^2 \gamma(1 - \gamma)N计算 Z-scoreZ \frac{N_G - \gamma N}{\sqrt{\gamma(1 - \gamma)N}}如果计算出的 Z 值远大于常规阈值例如 Z 4.0则意味着“一段文本连续随机命中绿名单”的概率极低p \text{-value} 0.00003。此时我们可以以几乎 100% 的绝对把握拒绝零假设断定该文本包含 AI 水印。二、 Python 代码实现简易红绿单水印生成与检测器下面我们用一个极简的 Python 脚本完整模拟词表划分 \to 水印文本生成 \to 水印检测的端到端全流程。为了保证代码完全可运行我们用简单的随机数模型模拟大模型的 Logits。Pythonimport hashlib import numpy as np class GreenlistWatermarker: def __init__(self, vocab_size1000, gamma0.5, delta2.0, secret_keymy_secret_salt): :param vocab_size: 虚拟词表大小 :param gamma: 绿名单占比 (0-1) :param delta: 绿名单 Logits 增加的偏置大小 :param secret_key: 密码学密钥用于哈希混淆 self.vocab_size vocab_size self.gamma gamma self.delta delta self.secret_key secret_key self.vocab list(range(vocab_size)) def _get_greenlist(self, prev_token): 根据前一个 token 和密钥伪随机生成当前步的绿名单 # 将前一个 token 和密钥结合做 SHA256 哈希 hash_input f{self.secret_key}_{prev_token}.encode(utf-8) hash_seed int(hashlib.sha256(hash_input).hexdigest(), 16) % (2**32) # 种子锚定伪随机数发生器确保切分可复现 rng np.random.default_rng(hash_seed) shuffled_vocab rng.permutation(self.vocab) green_size int(self.vocab_size * self.gamma) greenlist set(shuffled_vocab[:green_size]) return greenlist def generate_token(self, prev_token, original_logits): 给 Logits 注入水印偏置并采样生成下一个 token greenlist self._get_greenlist(prev_token) # 复制一份原始 logits 并加上偏置 watermarked_logits original_logits.copy() for idx in range(self.vocab_size): if idx in greenlist: watermarked_logits[idx] self.delta # Softmax 并采样 exp_logits np.exp(watermarked_logits - np.max(watermarked_logits)) probs exp_logits / np.sum(exp_logits) next_token np.random.choice(self.vocab, pprobs) return next_token def detect(self, token_sequence): 检测输入序列返回绿色词总数和统计学 Z-score if len(token_sequence) 2: return 0, 0.0 green_count 0 N len(token_sequence) - 1 # 第一个词没有前序不计入统计 # 逐个位置复现绿名单并进行校验 for i in range(1, len(token_sequence)): prev_token token_sequence[i-1] curr_token token_sequence[i] greenlist self._get_greenlist(prev_token) if curr_token in greenlist: green_count 1 # 计算数学期望、方差与 Z-score expected_green self.gamma * N variance self.gamma * (1 - self.gamma) * N std_dev np.sqrt(variance) z_score (green_count - expected_green) / std_dev if std_dev 0 else 0.0 return green_count, z_score # 验证与测试 if __name__ __main__: # 初始化水印中心 watermarker GreenlistWatermarker(vocab_size1000, gamma0.5, delta4.0) print(--- 实验 1模拟【有水印】大模型文本生成与检测 ---) watermarked_text [42] # 初始种子 token for _ in range(100): # 模拟大模型输出的原始概率分布纯随机基础 Logits mock_original_logits np.random.normal(0, 1, size1000) next_t watermarker.generate_token(watermarked_text[-1], mock_original_logits) watermarked_text.append(next_t) g_count, z watermarker.detect(watermarked_text) print(f总检验 Token 数 (N): {len(watermarked_text)-1}) print(f命中绿色词数 (N_G): {g_count} (理论期望值约为: {(len(watermarked_text)-1)*0.5})) print(f计算所得 Z-score: {z:.4f} (当 Z 4.0 时即可 99.99% 确定被恶意蒸馏/白嫖)) print(\n--- 实验 2模拟【人类创作/无水印】普通文本检测 ---) # 模拟一段完全不带偏置随机产生的文本 normal_text list(np.random.randint(0, 1000, size101)) g_count_norm, z_norm watermarker.detect(normal_text) print(f总检验 Token 数 (N): {len(normal_text)-1}) print(f命中绿色词数 (N_G): {g_count_norm}) print(f计算所得 Z-score: {z_norm:.4f} (数值接近 0属于正常随机噪声))为什么这个机制无法被轻易破解无感对大模型来说\delta 的加入只是稍微改变了同义词之间的微小概率比如在生成“非常”和“十分”之间倾向于选择落入绿名单的那一个完全不破坏语义。安全红绿榜是由Hash(prev_token, Secret_key)决定的。只要不泄露Secret_key哪怕黑客把生成的文本拿去反复洗稿、替换同义词只要无法改变前后的词串组合他就无法抹去由于高频碰撞绿名单而导致的 Z值异常。这就是防御黑盒蒸馏的杀手锏。