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。水印系统对属于绿名单的词强行加上一个扰动正值 \delta:
l'_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-score:
Z = \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。
Python
import hashlib
import numpy as np
class GreenlistWatermarker:
def __init__(self, vocab_size=1000, gamma=0.5, delta=2.0, secret_key="my_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, p=probs)
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_size=1000, gamma=0.5, delta=4.0)
print("--- 实验 1:模拟【有水印】大模型文本生成与检测 ---")
watermarked_text = [42] # 初始种子 token
for _ in range(100):
# 模拟大模型输出的原始概率分布(纯随机基础 Logits)
mock_original_logits = np.random.normal(0, 1, size=1000)
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, size=101))
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值异常。这就是防御黑盒蒸馏的杀手锏。


更多推荐
所有评论(0)