AI 内容水印:数字时代的防伪标签

2026 年,AI 生成的文本、图片、视频已占互联网内容的 35%。当虚假内容与真实内容难以区分时,AI 内容水印技术成为了信息可信度的基础设施。EU AI Act 要求所有 AI 生成内容必须可识别,中国《生成式AI服务管理办法》同样要求对生成内容进行标识。

一、水印技术分类

1.1 水印方法对比

方法原理鲁棒性对质量影响适用场景
统计水印修改token分布极低文本生成
语法水印修改句法结构文本生成
词汇水印替换同义词文本生成
像素水印修改图像像素图像生成
频域水印在频域嵌入极高极低图像/音频
元数据水印写入文件元数据极低所有格式
模型指纹利用模型固有特征所有生成

二、文本水印技术

2.1 统计水印(Green List / Red List)

import torch
import torch.nn.functional as F
from collections import defaultdict
import hashlib

class StatisticalWatermark:
    """统计水印——Green List/Red List 方法"""
    
    def __init__(self, model, green_ratio: float = 0.5, 
                 green_bias: float = 2.0, hash_key: int = 42):
        self.model = model
        self.green_ratio = green_ratio
        self.green_bias = green_bias  # 绿名单token的logit偏置
        self.hash_key = hash_key
    
    def _get_green_list(self, prev_token: int, vocab_size: int) -> set:
        """根据前一个token确定绿名单"""
        # 使用哈希函数确定性地生成绿名单
        rng = torch.Generator()
        rng.manual_seed(self.hash_key + prev_token)
        
        perm = torch.randperm(vocab_size, generator=rng)
        green_size = int(vocab_size * self.green_ratio)
        
        return set(perm[:green_size].tolist())
    
    def generate_with_watermark(self, prompt: str, max_tokens: int = 200) -> str:
        """带水印的文本生成"""
        input_ids = self.model.encode(prompt)
        generated = []
        
        for _ in range(max_tokens):
            # 获取logits
            logits = self.model.forward(input_ids)
            next_token_logits = logits[-1]
            
            # 获取绿名单
            prev_token = input_ids[-1].item()
            vocab_size = next_token_logits.shape[0]
            green_list = self._get_green_list(prev_token, vocab_size)
            
            # 给绿名单token加偏置
            for token_id in green_list:
                next_token_logits[token_id] += self.green_bias
            
            # 采样
            probs = F.softmax(next_token_logits, dim=-1)
            next_token = torch.multinomial(probs, 1)
            
            generated.append(next_token.item())
            input_ids = torch.cat([input_ids, next_token])
        
        return self.model.decode(generated)
    
    def detect_watermark(self, text: str) -> dict:
        """检测文本中是否包含水印"""
        tokens = self.model.encode(text)
        
        green_count = 0
        total_count = 0
        
        for i in range(1, len(tokens)):
            prev_token = tokens[i-1].item()
            current_token = tokens[i].item()
            
            green_list = self._get_green_list(
                prev_token, self.model.vocab_size
            )
            
            if current_token in green_list:
                green_count += 1
            total_count += 1
        
        green_ratio = green_count / max(total_count, 1)
        expected_ratio = self.green_ratio  # 无水印时的期望比例
        
        # Z-score 检验
        z_score = (green_count - expected_ratio * total_count) / \
                  np.sqrt(total_count * expected_ratio * (1 - expected_ratio))
        
        return {
            'green_ratio': green_ratio,
            'expected_ratio': expected_ratio,
            'z_score': z_score,
            'watermarked': z_score > 4.0,  # 阈值
            'confidence': min(1.0, z_score / 10.0),
            'token_count': total_count
        }

2.2 语义水印

class SemanticWatermark:
    """语义水印——通过语义变换嵌入水印"""
    
    def __init__(self, llm_client, key: str = "secret-key"):
        self.llm = llm_client
        self.key = key
    
    def embed_watermark(self, text: str, watermark_bits: str = "1010") -> str:
        """在文本中嵌入水印比特"""
        # 根据水印比特选择不同的表达方式
        sentences = text.split('。')
        watermarked = []
        
        for i, sentence in enumerate(sentences):
            if not sentence.strip():
                continue
            
            bit = watermark_bits[i % len(watermark_bits)]
            
            if bit == '1':
                # bit=1: 使用主动语态
                transformed = self._to_active_voice(sentence)
            else:
                # bit=0: 使用被动语态
                transformed = self._to_passive_voice(sentence)
            
            watermarked.append(transformed)
        
        return '。'.join(watermarked)
    
    def extract_watermark(self, text: str) -> str:
        """从文本中提取水印"""
        sentences = text.split('。')
        bits = []
        
        for sentence in sentences:
            if not sentence.strip():
                continue
            # 判断主动/被动语态
            voice = self._detect_voice(sentence)
            bits.append('1' if voice == 'active' else '0')
        
        return ''.join(bits)
    
    def _to_active_voice(self, sentence: str) -> str:
        prompt = f"将以下句子改为主动语态,保持原意:{sentence}"
        return self.llm.generate(prompt)
    
    def _to_passive_voice(self, sentence: str) -> str:
        prompt = f"将以下句子改为被动语态,保持原意:{sentence}"
        return self.llm.generate(prompt)
    
    def _detect_voice(self, sentence: str) -> str:
        prompt = f"判断以下句子是主动语态还是被动语态,只回答'active'或'passive':{sentence}"
        return self.llm.generate(prompt).strip()

三、图像水印技术

3.1 频域水印

import numpy as np
from numpy.fft import fft2, ifft2, fftshift, ifftshift

class FrequencyDomainWatermark:
    """频域水印——在DCT/DWT域嵌入水印"""
    
    def __init__(self, watermark_strength: float = 0.1):
        self.strength = watermark_strength
    
    def embed(self, image: np.ndarray, watermark: np.ndarray) -> np.ndarray:
        """在图像频域中嵌入水印"""
        # 1. DCT变换
        dct = fftshift(fft2(image, axes=(0, 1)), axes=(0, 1))
        
        # 2. 选择中频区域(对质量和压缩都鲁棒)
        h, w = image.shape[:2]
        mid_start_h, mid_end_h = h//4, 3*h//4
        mid_start_w, mid_end_w = w//4, 3*w//4
        
        # 3. 在中频区域叠加水印
        watermark_resized = self._resize_watermark(
            watermark, 
            mid_end_h - mid_start_h,
            mid_end_w - mid_start_w
        )
        
        dct[mid_start_h:mid_end_h, mid_start_w:mid_end_w] += \
            self.strength * watermark_resized
        
        # 4. 逆DCT
        watermarked = np.real(ifft2(ifftshift(dct, axes=(0, 1)), axes=(0, 1)))
        
        # 5. 裁剪到有效范围
        return np.clip(watermarked, 0, 255).astype(np.uint8)
    
    def extract(self, image: np.ndarray, original: np.ndarray = None) -> np.ndarray:
        """提取水印"""
        dct_watermarked = fftshift(fft2(image, axes=(0, 1)), axes=(0, 1))
        
        if original is not None:
            # 有原始图像:差值提取
            dct_original = fftshift(fft2(original, axes=(0, 1)), axes=(0, 1))
            diff = dct_watermarked - dct_original
        else:
            # 无原始图像:盲提取
            diff = dct_watermarked
        
        h, w = image.shape[:2]
        mid_start_h, mid_end_h = h//4, 3*h//4
        mid_start_w, mid_end_w = w//4, 3*w//4
        
        watermark = diff[mid_start_h:mid_end_h, mid_start_w:mid_end_w]
        watermark = np.sign(watermark)  # 二值化
        
        return watermark
    
    def _resize_watermark(self, watermark: np.ndarray, h: int, w: int) -> np.ndarray:
        """调整水印大小"""
        from PIL import Image
        wm_pil = Image.fromarray(watermark.astype(np.float32))
        wm_resized = wm_pil.resize((w, h), Image.BILINEAR)
        return np.array(wm_resized)

3.2 扩散模型水印

class DiffusionModelWatermark:
    """扩散模型生成水印——在生成过程中嵌入"""
    
    def __init__(self, model, watermark_encoder):
        self.model = model
        self.encoder = watermark_encoder  # 水印编码器
    
    def generate_with_watermark(self, prompt: str, 
                                 watermark_text: str) -> np.ndarray:
        """带水印的图像生成"""
        # 1. 编码水印为隐变量
        watermark_latent = self.encoder.encode(watermark_text)
        
        # 2. 扩散模型生成(在去噪过程中注入水印)
        latents = torch.randn(1, 4, 64, 64)
        
        for t in reversed(range(self.model.num_train_timesteps)):
            # 标准去噪步骤
            noise_pred = self.model.unet(latents, t, 
                                          encoder_hidden_states=prompt)
            
            # 在特定时间步注入水印
            if t < 500:  # 在后期步骤注入
                latents = latents + 0.01 * watermark_latent
            
            latents = self.model.scheduler.step(
                noise_pred, t, latents
            ).prev_sample
        
        # 3. 解码为图像
        image = self.model.vae.decode(latents / self.model.vae.config.scaling_factor)
        return image

四、音频水印技术

class AudioWatermark:
    """音频频域水印"""
    
    def __init__(self, sample_rate: int = 44100, 
                 watermark_freq: float = 18000):
        self.sample_rate = sample_rate
        self.watermark_freq = watermark_freq  # 超声波频段
    
    def embed(self, audio: np.ndarray, watermark_bits: str) -> np.ndarray:
        """在音频中嵌入水印"""
        duration = len(audio) / self.sample_rate
        t = np.arange(len(audio)) / self.sample_rate
        
        # 生成水印信号(FSK调制)
        watermark_signal = np.zeros(len(audio))
        bit_duration = 0.01  # 每个比特10ms
        samples_per_bit = int(self.sample_rate * bit_duration)
        
        for i, bit in enumerate(watermark_bits * 
                                 (len(audio) // samples_per_bit // len(watermark_bits) + 1)):
            if i * samples_per_bit >= len(audio):
                break
            
            freq = self.watermark_freq if bit == '1' else self.watermark_freq + 200
            start = i * samples_per_bit
            end = min(start + samples_per_bit, len(audio))
            watermark_signal[start:end] = np.sin(
                2 * np.pi * freq * t[start:end]
            )
        
        # 混合(振幅很低,人耳不可感知)
        watermarked = audio + 0.005 * watermark_signal
        return np.clip(watermarked, -1, 1)
    
    def extract(self, audio: np.ndarray) -> str:
        """提取音频水印"""
        # FFT分析特定频率
        fft = np.fft.rfft(audio)
        freqs = np.fft.rfftfreq(len(audio), 1/self.sample_rate)
        
        # 提取水印比特
        bits = []
        samples_per_bit = int(self.sample_rate * 0.01)
        
        for i in range(0, len(audio), samples_per_bit):
            chunk = audio[i:i+samples_per_bit]
            if len(chunk) < samples_per_bit:
                break
            
            chunk_fft = np.fft.rfft(chunk)
            chunk_freqs = np.fft.rfftfreq(len(chunk), 1/self.sample_rate)
            
            # 检查哪个频率能量更高
            mask1 = np.abs(chunk_freqs - self.watermark_freq) < 10
            mask2 = np.abs(chunk_freqs - (self.watermark_freq + 200)) < 10
            
            power1 = np.sum(np.abs(chunk_fft[mask1])**2)
            power2 = np.sum(np.abs(chunk_fft[mask2])**2)
            
            bits.append('1' if power1 > power2 else '0')
        
        return ''.join(bits)

五、水印鲁棒性评估

5.1 攻击测试

class WatermarkRobustnessTester:
    """水印鲁棒性测试"""
    
    def test_text_watermark(self, watermark_system, text: str):
        """测试文本水印鲁棒性"""
        results = {}
        
        # 1. 无攻击
        detection = watermark_system.detect(text)
        results['no_attack'] = detection['watermarked']
        
        # 2. 文本截断
        truncated = text[:len(text)//2]
        results['truncation_50'] = watermark_system.detect(truncated)['watermarked']
        
        # 3. 同义词替换
        synonym_replaced = self._replace_synonyms(text, ratio=0.3)
        results['synonym_30'] = watermark_system.detect(synonym_replaced)['watermarked']
        
        # 4. 翻译攻击
        translated = self._translate_roundtrip(text)
        results['translation'] = watermark_system.detect(translated)['watermarked']
        
        # 5. 改写攻击
        rewritten = self._rewrite(text)
        results['rewrite'] = watermark_system.detect(rewritten)['watermarked']
        
        return results
    
    def test_image_watermark(self, watermark_system, image):
        """测试图像水印鲁棒性"""
        results = {}
        
        # 1. JPEG压缩
        for quality in [90, 70, 50, 30]:
            compressed = self._jpeg_compress(image, quality)
            results[f'jpeg_{quality}'] = watermark_system.extract(compressed) is not None
        
        # 2. 裁剪
        for ratio in [0.75, 0.5, 0.25]:
            cropped = self._crop(image, ratio)
            results[f'crop_{ratio}'] = watermark_system.extract(cropped) is not None
        
        # 3. 缩放
        for scale in [0.5, 2.0, 0.25]:
            resized = self._resize(image, scale)
            results[f'scale_{scale}'] = watermark_system.extract(resized) is not None
        
        # 4. 噪声
        for noise_level in [0.01, 0.05, 0.1]:
            noisy = self._add_noise(image, noise_level)
            results[f'noise_{noise_level}'] = watermark_system.extract(noisy) is not None
        
        # 5. 旋转
        for angle in [5, 15, 90]:
            rotated = self._rotate(image, angle)
            results[f'rotate_{angle}'] = watermark_system.extract(rotated) is not None
        
        return results

5.2 鲁棒性对比

攻击类型统计水印语义水印频域水印模型指纹
截断50%⚠️⚠️
同义词替换N/A
翻译N/A⚠️
改写⚠️N/A⚠️
JPEG压缩N/AN/AN/A
裁剪50%N/AN/AN/A
缩放N/AN/AN/A

六、合规要求与标准

6.1 各国法规要求

COMPLIANCE_REQUIREMENTS = {
    'EU_AI_Act': {
        'requirement': 'AI生成内容必须可被检测',
        'deadline': '2026年8月',
        'scope': '所有AI生成的文本、图像、音频、视频',
        'standard': 'C2PA内容溯源标准',
    },
    'China_Generative_AI': {
        'requirement': 'AI生成内容必须显式或隐式标识',
        'deadline': '已生效',
        'scope': '面向公众的生成式AI服务',
        'standard': '网信办标识规范',
    },
    'US_Executive_Order': {
        'requirement': '联邦机构使用的AI内容需可溯源',
        'deadline': '2026年Q4',
        'scope': '联邦政府AI应用',
        'standard': 'NIST AI水印标准',
    },
}

七、2026 前沿方向

  1. 不可去除水印:理论上证明不可被去除的水印方案
  2. 零比特水印:不嵌入额外信息,利用模型固有特征识别
  3. 量子水印:利用量子特性实现不可克隆水印
  4. 跨模态水印:文本→图像→视频的全链路溯源
  5. 去中心化验证:区块链记录内容来源链

结语

AI 内容水印是信息可信时代的基石。没有水印,AI 生成内容将淹没互联网,真实与虚假的边界彻底模糊。2026 年的水印技术已经可以做到"人眼无感、机器可检、攻击难除",但仍有很大的提升空间。

水印技术不是单点方案,而是需要与内容生成、分发、检测全链路配合的系统工程。生成端嵌入水印、平台端检测水印、用户端验证水印——只有全链路协同,才能构建可信的内容生态。

加入讨论

这篇文章有姊妹讨论帖在硅基AGI论坛 — 全球首个碳基硅基认知交流平台。

碳基与硅基的智慧碰撞,认知差异创造无限可能。