这两天在网上看到一个新闻,说 Anthropic 计划推出一种隐形水印技术。
核心目的就是为了识别由AI生成的文本内容。
难道那边也有要求?公众号发AI生成的内容得标注一下子?哈哈!
重点是哈:Anthropic 表示,自8月2日及之后发布的模型起,该公司会在 Claude AI 生成的文本中直接嵌入人类无法察觉、仅机器可识别的水印。
人类肉眼看不到该水印,不会降低文本质量与可读性。
即便复制粘贴文本,水印也会随之保留,部分轻度编辑操作也无法清除水印。
为啥我隐隐感觉这个表述这么的熟悉?
我之前是不是写过这个技术?

我终于想起来了,我在今年3月写过一篇文章,主要说的是前端零宽隐形水印。
详见:# 别再用显性水印!前端零宽隐形水印,实现内容溯源级版权保护,已封装复制即用
当时我写这篇文章的时候其实用的表述和这个差不多,前端零宽字符隐形水印的核心优势就是:
肉眼完全不可见、复制粘贴不丢失。
当然,咱们这里不 YY 哈!
人家 Anthropic 用的不一定就是这个技术,只是说从"效果描述"上和零宽字符的效果描述差不多。
对于"肉眼不可见,但机器可识别"这种技术方案,其实除了零宽字符还有一种更高明的方案 ------ LLM 统计语义水印。
这种技术详细方案可以见《Kirchenbauer 2023 KGW 算法》这篇论文。
简单理解这种技术:不插入任何看不见特殊字符,不修改字面文字。
在模型生成文字的选词概率 里面埋统计学指纹,人读完全看不出差别。
但是用专用算法做统计检验,可以判断该文本是否由该大模型生成。
他的核心原理是通过 KGW 红绿列表算法,选词的时候生成的水印。
大模型是自回归生成,根据上文,输出下一个 token(词片段)的概率分布,从里面挑词输出。
而统计水印就是在每一步选词的时候,进行轻微的干预,让选词的概率偏向某一种,人为制造出统计学偏差。
咱们打个比方,类似于,我们人眼现在看豆子都一样,我们分不清楚绿豆和红豆的颜色,我们认为他们都是豆子。
本来大模型挑豆子就是随机的,可能挑红的,也可能挑绿的,没啥规律。
现在通过统计水印干预后,大模型挑的大多数都是红的,尽量避免,或者完全不挑绿的。
但是人眼看不出区别,机器可以分辨。
生成阶段(埋水印)
- 拿到已经生成的上文 token 序列,用密钥 + 哈希函数算出一个随机种子 K。
- 利用这个种子 K,把模型全部词汇表随机切分成两类:绿名单 (Green)、红名单 (Red)。
- 不禁止红名单,只给绿名单所有 token 的 logit(分数)统一加一个小偏置,从而提高选中绿名单词语的概率。
- 模型继续正常采样输出下一个词,循环往复生成完整文本。

这种轻微偏差的运算下,模型输出出来的内容,人是看不出区别的。
比方说,原本大模型的输出为:今天天气晴朗。
在统计水印的干预下,变成了:今天天气明媚。
在人看来这两个词语没什么太大的区别,语序/语义几乎完全一致。
但是机器可以识别出来,这两个词一个在红名单,一个在绿名单。
统计水印就是靠这种选词偏好埋的标记,不是调换词语先后顺序。
注意:这里不会强制必须选绿词,只是提高概率。
强制选绿可能会导致输出的内容存在问题,所以这里只是概率。
这样输出的内容自然绿词偏多,红词偏少,机器就可以识别了。
检测阶段(提取水印)
拿到一段文本,不知道其生成过程,只知道公开检测密钥。
- 逐 token,复现当初的哈希 + 种子逻辑,重新还原每一步的绿 / 红名单;
- 统计整篇文本里面,实际出现多少绿名单 token;
- 用统计学 Z‑检验,计算 p 值:判断 "绿词占比偏高" 是随机巧合,还是水印干预造成的。
判断如果 p 值足够小,极大概率是大模型生成的。
判断如果 p 值不够小,则有可能是人写的,或者人改编的。

明白了 LLM 统计水印的原理你就明白了为什么这个水印是有局限性的:
已经写好的文本,统计水印无能为力。
因为统计水印本身通过选词概率进行识别,而已经写好的文本不存在任何选词概率了,固定死了。
同时如果你对AI生成的文本进行大篇幅的修改,同义词替换等操作,大量的 Token 被替换,这也无法正确识别。
还有就是非常短的一句话,或者是一个段落,这样也很难正确识别,容易出现误判。
另外也正是因为 LLM 统计水印这种偏差影响,会导致大模型输出的内容可能存在负面影响。
毕竟不是所有的词语、释意都有对应的词来替换,替换后可能会让人微微的感觉语序、语义存在问题。
英文或许还好,但是中文这个问题尤其明显。
中文的语义实在是太复杂了,不通过一长段的上下文你很难读懂这部分到底在表达什么意思。(梦回文言文阅读理解)
这里简单给大家上个Demo,可以尝尝LLM统计水印的效果:
py
import hashlib
import random
from scipy.stats import norm
# ====================== KGW 水印模拟 Demo ======================
# 1. 模拟词汇表,模拟大模型的token词表
VOCAB = [
"晴天", "明媚", "下雨", "潮湿", "刮风", "寒冷", "温暖", "凉爽",
"大海", "高山", "森林", "河流", "城市", "夜晚", "清晨", "黄昏"
]
WATERMARK_SEED = 12345 # 水印密钥,生成与检测必须相同
GAMMA = 0.5 # 红绿划分比例,绿词占词表50%
DELTA = 2.0 # 给绿词加分的偏置,越大水印信号越强
def get_green_red_list(context_tokens: list, seed: int):
"""
根据上文context + seed哈希,生成本步的绿名单、红名单(KGW核心)
"""
ctx_str = "|".join(context_tokens)
h = hashlib.sha256(f"{ctx_str}_{seed}".encode()).hexdigest()
rand = random.Random(int(h, 16) % (2**32))
shuffled_vocab = VOCAB.copy()
rand.shuffle(shuffled_vocab)
split_idx = int(len(shuffled_vocab) * GAMMA)
green = set(shuffled_vocab[:split_idx])
red = set(shuffled_vocab[split_idx:])
return green, red
def watermarked_generate(max_len=12):
"""模拟带水印的文本生成:每一步优先抬高绿词概率"""
output = []
for _ in range(max_len):
green_set, red_set = get_green_red_list(output, WATERMARK_SEED)
logits = {}
for word in VOCAB:
base_score = random.uniform(0, 1)
if word in green_set:
base_score += DELTA # 绿词加分,提高被选中概率
logits[word] = base_score
# 选分数最高的词输出
next_word = max(logits.items(), key=lambda x: x[1])[0]
output.append(next_word)
return " ".join(output), output
def detect_watermark(text_tokens: list, seed: int):
"""
KGW水印检测:统计绿词占比,计算Z‑score,返回z值,p值
z越高代表水印信号越强,一般 z>3 认为大概率存在水印
"""
num_green = 0
total = len(text_tokens)
for idx in range(total):
context = text_tokens[:idx]
green, _ = get_green_red_list(context, seed)
token = text_tokens[idx]
if token in green:
num_green += 1
expected = total * GAMMA
variance = total * GAMMA * (1 - GAMMA)
if variance <= 0:
return {"z_score": 0, "num_green": num_green, "total": total}
z_score = (num_green - expected) / (variance ** 0.5)
return {
"z_score": z_score,
"num_green": num_green,
"total_tokens": total
}
if __name__ == "__main__":
# 生成一段带水印文本
gen_text, tokens = watermarked_generate(max_len=14)
print(f"【生成带水印文本】:{gen_text}")
res = detect_watermark(tokens, WATERMARK_SEED)
print(f"【水印检测结果】{res}")
print("\n说明:z_score >3,代表水印信号显著;z接近0无水印")
# 测试:打乱改写一部分token,模拟人工改写破坏水印
import copy
tokens_broken = copy.deepcopy(tokens)
for i in range(4):
tokens_broken[i] = random.choice(VOCAB)
res_broken = detect_watermark(tokens_broken, WATERMARK_SEED)
print(f"\n【改写破坏后检测结果】{res_broken}")
# 运行输出示例
# 【生成带水印文本】:明媚 温暖 大海 晴天 黄昏 凉爽 清晨 高山 明媚 温暖 大海 晴天 黄昏 凉爽
# 【水印检测结果】{'z_score': 4.123..., 'num_green':11, 'total_tokens':14}
# 说明:z_score >3,代表水印信号显著;z 接近 0 无水印
# 【改写破坏后检测结果】{'z_score': 0.412..., 'num_green':8, 'total_tokens':14}
或许我猜 Anthropic 是用的这两种方案的结合?

回头大家可以拿 Anthropic 生成出来的文本检测试试,据说老美那边已经有人提前布局"去除 Anthropic 水印"产业了。
合着世界上最锋利的矛和世界上最坚固的盾?