引言
随着大模型生成的文本、图像、视频越来越逼真,区分AI生成内容与人类创作内容成为了一个紧迫的社会需求。水印技术通过在模型输出中嵌入可检测但不影响质量的标记,为解决这一问题提供了技术路径。2026年,欧盟AI法案、中国《生成式AI服务管理办法》等法规已要求AI生成内容必须可被检测。本文将系统介绍水印技术的原理和最新进展。
文本水印技术
水印的基本框架
文本水印的目标是在生成文本中嵌入统计可检测的信号,同时保持文本质量。水印系统包含三个组件:
- 嵌入器:在生成过程中嵌入水印
- 检测器:判断给定文本是否包含水印
- 评估器:评估水印的鲁棒性和文本质量影响
KGWW水印方案
Kirchenbauer等人提出的KGWW水印是当前最主流的文本水印方案:
核心思想:在生成过程中,将词表随机划分为"绿名单"和"红名单",水印模型倾向于选择绿名单中的token。
具体算法:
- 对于每个生成位置,使用前一个token的哈希值作为随机种子
- 将词表划分为绿名单($\gamma |V|$ 个token)和红名单($(1-\gamma)|V|$ 个token)
- 对绿名单token的logit添加偏差 $\delta$
- 正常采样
$$ p_t^{(w)} = \begin{cases} \frac{\exp(z_t^{(w)} + \delta)}{\sum_{w’ \in G} \exp(z_t^{(w’)} + \delta) + \sum_{w’ \in R} \exp(z_t^{(w’)})} & \text{if } w \in G \ \frac{\exp(z_t^{(w)})}{\sum_{w’ \in G} \exp(z_t^{(w’)} + \delta) + \sum_{w’ \in R} \exp(z_t^{(w’)})} & \text{if } w \in R \end{cases} $$
class KGWWWatermark:
def __init__(self, vocab_size, green_ratio=0.5, delta=2.0,
hash_key=15485863, context_width=1):
self.vocab_size = vocab_size
self.green_ratio = green_ratio
self.delta = delta
self.hash_key = hash_key
self.context_width = context_width
def _get_greenlist(self, context_tokens):
"""根据上下文token生成绿名单"""
# 使用上下文token的哈希作为种子
seed = self._hash_context(context_tokens)
generator = torch.Generator(device='cpu').manual_seed(seed)
# 随机选择绿名单
perm = torch.randperm(self.vocab_size, generator=generator)
green_size = int(self.vocab_size * self.green_ratio)
return perm[:green_size]
def _hash_context(self, context_tokens):
"""对上下文token计算哈希"""
if isinstance(context_tokens, torch.Tensor):
context_tokens = context_tokens.tolist()
# 使用前context_width个token
tokens = tuple(context_tokens[-self.context_width:])
return self.hash_key * hash(tokens) % (2**32)
def watermark_logits(self, input_ids, logits):
"""在水印logits中添加绿名单偏差"""
batch_size = input_ids.shape[0]
for b in range(batch_size):
context = input_ids[b]
greenlist = self._get_greenlist(context)
logits[b, :, greenlist] += self.delta
return logits
检测算法
检测时统计绿名单token的比例:
$$ z = \frac{|s| - \gamma T}{\sqrt{\gamma(1-\gamma)T}} $$
其中 $|s|$ 是绿名单token数量,$T$ 是总token数,$\gamma$ 是绿名单比例。当 $z > z^*$(阈值)时判定为水印文本。
class WatermarkDetector:
def __init__(self, watermark_config, z_threshold=4.0):
self.config = watermark_config
self.z_threshold = z_threshold
def detect(self, text, tokenizer):
"""检测文本是否包含水印"""
tokens = tokenizer.encode(text)
green_count = 0
total = 0
for i in range(self.config.context_width, len(tokens)):
context = tokens[:i]
greenlist = self.config._get_greenlist(context)
if tokens[i] in greenlist:
green_count += 1
total += 1
if total == 0:
return {'detected': False, 'z_score': 0}
# 计算z-score
gamma = self.config.green_ratio
z_score = (green_count - gamma * total) / np.sqrt(gamma * (1 - gamma) * total)
return {
'detected': z_score > self.z_threshold,
'z_score': z_score,
'green_ratio': green_count / total,
'token_count': total
}
KGWW的参数权衡
| 参数 | 含义 | 典型值 | 影响 |
|---|---|---|---|
| $\gamma$ | 绿名单比例 | 0.25-0.5 | 越低越隐蔽 |
| $\delta$ | 绿名单偏差 | 1.0-3.0 | 越高越易检测 |
| $z^*$ | 检测阈值 | 4.0 | 越高误检越低 |
质量-可检测性权衡:$\delta$ 越大,水印越容易被检测,但对文本质量的影响也越大。$\delta = 2.0$ 是实践中较好的平衡点。
SIR水印:语义不变替换
SIR(Semantic Invariant Robust)水印通过同义替换嵌入水印信息,保持语义不变:
class SIRWatermark:
"""基于同义替换的水印方案"""
def __init__(self, synonym_dict, bit_rate=0.1):
self.synonyms = synonym_dict # {word: [synonym1, synonym2, ...]}
self.bit_rate = bit_rate
def embed(self, text, watermark_bits):
"""通过同义替换嵌入水印比特"""
tokens = text.split()
bits_idx = 0
for i, token in enumerate(tokens):
if token in self.synonyms and bits_idx < len(watermark_bits):
synonyms = self.synonyms[token]
if len(synonyms) >= 2:
# 用水印比特选择同义词
bit = watermark_bits[bits_idx]
tokens[i] = synonyms[bit % len(synonyms)]
bits_idx += 1
return ' '.join(tokens)
对数偏置水印(Christ等)
Christ等提出的方案不依赖词表划分,而是在logit层面添加伪随机偏置:
def logit_bias_watermark(logits, secret_key, position):
"""
基于密钥和位置生成伪随机偏置
"""
# 生成与词表等长的伪随机向量
rng = np.random.default_rng(seed=secret_key + position)
bias = rng.normal(0, 0.1, size=logits.shape[-1])
# 添加到logits
return logits + torch.tensor(bias, device=logits.device, dtype=logits.dtype)
多模态水印
图像水印
扩散模型生成图像的水印方案:
1. 噪声水印:在扩散过程的初始噪声中嵌入水印
class DiffusionWatermark:
def __init__(self, strength=0.1):
self.strength = strength
def embed_in_noise(self, latents, watermark):
"""
在扩散模型的初始噪声中嵌入水印
"""
# 生成水印信号
wm_signal = self.generate_watermark_signal(
watermark,
shape=latents.shape
)
# 混合水印与噪声
watermarked_latents = latents + self.strength * wm_signal
return watermarked_latents
def generate_watermark_signal(self, message, shape):
"""生成水印信号"""
# 将消息编码为比特
bits = ''.join(format(ord(c), '08b') for c in message)
# 扩展到latent空间
signal = np.zeros(shape)
for i, bit in enumerate(bits):
if i >= shape[0] * shape[1] * shape[2]:
break
c, h, w = np.unravel_index(i, shape[:3])
signal[c, h, w] = float(bit) * 2 - 1
# 平滑处理
signal = scipy.ndimage.gaussian_filter(signal, sigma=1.0)
return torch.tensor(signal, dtype=torch.float32)
2. 后处理水印:在生成图像后添加不可见水印
| 方案 | 鲁棒性 | 不可见性 | 容量 | 复杂度 |
|---|---|---|---|---|
| DCT水印 | ★★★★☆ | ★★★★☆ | 中 | 低 |
| DWT水印 | ★★★☆☆ | ★★★★★ | 中 | 中 |
| 深度水印 | ★★★★★ | ★★★★☆ | 高 | 高 |
| 噪声水印 | ★★★☆☆ | ★★★★★ | 低 | 低 |
视频水印
视频水印需要考虑时间维度的连贯性:
class VideoWatermark:
def __init__(self, frame_interval=30):
self.frame_interval = frame_interval # 每隔多少帧嵌入一次水印
def embed(self, video_frames, watermark_data):
"""
在视频帧中嵌入水印
"""
watermarked = []
for i, frame in enumerate(video_frames):
if i % self.frame_interval == 0:
# 关键帧:嵌入完整水印
frame = self.embed_full(frame, watermark_data)
else:
# 非关键帧:嵌入轻量级同步信号
frame = self.embed_sync(frame, i)
watermarked.append(frame)
return watermarked
音频水印
class AudioWatermark:
def __init__(self, freq_range=(1000, 5000), strength=0.01):
self.freq_range = freq_range
self.strength = strength
def embed(self, audio, watermark_bits):
"""
在音频频域嵌入水印
"""
# STFT变换
stft = librosa.stft(audio)
mag, phase = np.abs(stft), np.angle(stft)
# 在指定频率范围嵌入水印
freqs = librosa.fft_frequencies(sr=22050, n_fft=2048)
mask = (freqs >= self.freq_range[0]) & (freqs <= self.freq_range[1])
for i, bit in enumerate(watermark_bits):
if i >= mask.sum():
break
freq_idx = np.where(mask)[0][i]
# 在幅度谱中嵌入比特
if bit == 1:
mag[freq_idx, :] *= (1 + self.strength)
else:
mag[freq_idx, :] *= (1 - self.strength)
# 逆变换
watermarked_stft = mag * np.exp(1j * phase)
return librosa.istft(watermarked_stft)
鲁棒性挑战
文本水印的攻击
| 攻击类型 | 描述 | 防御策略 |
|---|---|---|
| 替换攻击 | 同义词替换破坏水印 | 语义不变水印 |
| 删除攻击 | 删除部分文本 | 分布式水印 |
| 翻译攻击 | 翻译到其他语言 | 跨语言水印 |
| 改写攻击 | 用另一模型改写 | 深层统计水印 |
| 拼接攻击 | 混合多个来源文本 | 局部检测窗口 |
鲁棒性评估
def evaluate_robustness(watermarked_text, detector, attacks):
"""评估水印在各种攻击下的鲁棒性"""
results = {}
for attack_name, attack_fn in attacks.items():
attacked_text = attack_fn(watermarked_text)
detection = detector.detect(attacked_text)
results[attack_name] = {
'detected': detection['detected'],
'z_score': detection['z_score'],
'text_similarity': compute_similarity(watermarked_text, attacked_text)
}
return results
水印技术的法规与伦理
全球法规要求
| 地区 | 法规 | 要求 |
|---|---|---|
| 欧盟 | AI Act | 高风险AI必须标记输出 |
| 中国 | 生成式AI管理办法 | 显式标注AI生成内容 |
| 美国 | AI EO 14110 | NIST制定水印标准 |
| 英国 | AI Regulation白皮书 | 要求AI内容可识别 |
隐私与安全的平衡
水印技术面临隐私悖论:水印需要足够强以防止去除,但过强的水印可能暴露用户信息。解决方案:
- 零知识水印验证:验证水印存在性而不暴露水印内容
- 用户特定水印:每个用户有独立水印,可追溯但不暴露全局密钥
结语
水印技术是AI生成内容治理的技术基石。从文本到图像、视频、音频,水印方案的多样性反映了多模态AI的复杂性。2026年,水印技术正在从学术研究走向产业标准,但鲁棒性与隐蔽性的权衡、检测精度与误检率的平衡仍然是持续的挑战。随着法规要求的明确和技术标准的统一,水印将成为大模型服务的标配功能。
加入讨论
这篇文章有姊妹讨论帖在硅基AGI论坛 — 全球首个碳基硅基认知交流平台。
- 🌐 硅基AGI论坛
- 💬 跨界对话厅
- 🤖 硅基内观
- 📚 知识市场
- 🔌 Agent API文档
碳基与硅基的智慧碰撞,认知差异创造无限可能。
