news 2026/10/14 17:03:28

端到端图像水印嵌入与提取:深度学习实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
端到端图像水印嵌入与提取:深度学习实战指南

简介:这是一套基于PyTorch构建的端到端图像水印嵌入与提取系统,利用深度卷积网络实现鲁棒水印的联合优化,覆盖COCO、ImageNet、Cifar10与QRCode四类主流数据集,可满足深度学习课程设计、毕业设计、科研复现及工程原型验证等场景需求。压缩包共58个文件,其中7个Python模块按职责清晰拆分:model.py负责网络结构、dataset.py封装数据加载、loss.py实现不可见性与鲁棒性联合损失、callbacks.py集成TensorBoard日志监控,另有train_config.py统一管理超参;此外还包含2篇PDF原理文档(论文与残差块设计说明)、40张示例图片及完整README,整体仅3.88MB,结构精简易上手。目前已有27人学习下载。代码实测可在Python 3.8+与PyTorch 1.9+环境稳定运行,支持普通GPU或CPU训练,运行main.py即可启动完整流程,权重与日志自动保存;配套说明与源码便于深入理解端到端水印的损失设计和模块组织,适合希望快速跑通基线并在此基础上改进算法的开发者。

1. 端到端图像水印嵌入与提取系统,到底解决什么问题

传统水印方案要么是明晃晃贴在角落的可见水印,截掉就没了;要么是 DCT/DWT 域里的手工特征调制,碰上裁剪、缩放、JPEG 重压缩,提取率立刻跳水。这几年深度学习把水印做成一个端到端的嵌入-提取闭环:输入一张原图和一段二进制信息,网络把信息调制进像素里,人眼看着没变化,但不管图片被怎么折腾,解码器都能把二进制信息再抠出来。这套用 Python 实现、覆盖 COO、ImageNet、Cifar10、QRCode 多数据集并带完整训练流程的方案,解决的就是「水印扛不扛攻击、能不能规模化训练」这两个最实际的问题。适合谁?做版权保护的、做图片溯源追踪的、以及想把二维码信息直接藏进图片里的开发者。这篇文章不讲玄学,只讲网络怎么搭、数据怎么喂、训练怎么不翻车。

2. 端到端水印框架拆解:编码器、解码器与信息瓶颈

2.1 水印嵌入的本质:不是加个 Logo,是调制像素统计特征

端到端水印的操作对象不是图片的空间域像素值本身,而是特征分布。编码器拿到的输入是两样东西:一张 H×W×3 的载体图 I,和一段长度为 L 的二进制序列 M(称为 Payload)。输出是一张和水印图嵌入后的图 I',人眼看去 I 和 I' 几乎没有差异,但 I' 的局部统计特征里已经编码进了 M 的全部信息。

我在实际项目里有个很核心的经验:嵌入强度不要用全局标量去控制,最好让网络自己学会逐像素调制。常见做法是在编码器末尾接一个 tanh 激活把残差限制在 [-ε, ε] 区间,ε 是手动设置的强度上限。这样既避免了生成图颜色整体偏移,又给了解码器明确的信号边界。

import torch import torch.nn as nn class Encoder(nn.Module): def __init__(self, payload_len=64, eps=0.15): super().__init__() # 下采样路径,提取多尺度特征 self.conv1 = nn.Conv2d(3, 64, 3, padding=1) self.conv2 = nn.Conv2d(64, 128, 3, stride=2, padding=1) self.conv3 = nn.Conv2d(128, 256, 3, stride=2, padding=1) # Payload 先扩展成与特征图同尺寸的 mask self.fc = nn.Linear(payload_len, 256 * 8 * 8) # 上采样恢复原图尺寸 self.deconv1 = nn.ConvTranspose2d(256 + 256, 128, 4, stride=2, padding=1) self.deconv2 = nn.ConvTranspose2d(128, 64, 4, stride=2, padding=1) self.out = nn.Sequential( nn.Conv2d(64, 3, 3, padding=1), nn.Tanh() ) self.eps = eps def forward(self, image, payload): # payload 广播成空间特征,拼进编码器 p = self.fc(payload).view(payload.size(0), 256, 8, 8) x = torch.relu(self.conv1(image)) x = torch.relu(self.conv2(x)) x = torch.relu(self.conv3(x)) x = torch.cat([x, p], dim=1) x = torch.relu(self.deconv1(x)) x = torch.relu(self.deconv2(x)) residual = self.out(x) # 残差限制在 eps 范围内,保证视觉质量 return image + residual * self.eps

这段代码的核心是 Payload 先通过全连接扩展成 8×8 的空间分布,再和图像特征做拼接。好处是信息不是藏在某个固定区域,而是散布到全图所有位置,后面即使被随机裁剪掉一半像素,另一半像素里依然保有完整信息的冗余。eps 参数就是「水印可见度」的旋钮:设 0.05 基本看不出差别,设 0.3 在纯色区域能察觉轻微纹理噪声。我一般从 0.1 起步,之后按提取率逐步微调。

2.2 解码器的设计:从被污染的图像里把比特位抠出来

解码器比编码器更敏感,它决定了系统的上限。它的输入 I'' 可能是 I' 经过 JPEG 压缩、缩放、加噪甚至裁剪后的版本,输出是一个 L 维向量,每一位代表该比特为 1 的概率。这里最容易犯的错是把解码器做成一个全连接网络直接拍平像素,真实场景里一旦出现裁剪,像素的位置全错位,全连接层基本报废。

正确思路是解码器也用卷积网络,保持空间平移不变性,最后用全局平均池化把特征压成固定长度的向量。这样不管输入图被裁剪成什么尺寸,只要卷积核能滑动,特征就能被提取。

class Decoder(nn.Module): def __init__(self, payload_len=64): super().__init__() # 输入可能是任意尺寸,用纯卷积保持平移不变 self.features = nn.Sequential( nn.Conv2d(3, 64, 3, padding=1), nn.ReLU(), nn.Conv2d(64, 128, 3, stride=2, padding=1), nn.ReLU(), nn.Conv2d(128, 256, 3, stride=2, padding=1), nn.ReLU(), nn.Conv2d(256, 512, 3, stride=2, padding=1), nn.ReLU(), ) self.head = nn.Sequential( nn.AdaptiveAvgPool2d(1), nn.Flatten(), nn.Linear(512, payload_len) ) def forward(self, stego_image): # 输出 logits,配合 BCEWithLogitsLoss 使用 x = self.features(stego_image) return self.head(x)

这里有个细节值得注意:AdaptiveAvgPool2d(1) 让解码器不关心输入分辨率,训练时用 128×128,测试时来了张 512×512 的图也能直接跑。解码器的最后一层不加 Sigmoid,因为训练时用的 BCEWithLogitsLoss 内部自带 Sigmoid,提前加上反而容易造成梯度消失。

2.3 信息瓶颈:Payload 长度选多少直接决定系统成败

Payload 长度 L 是整套系统最关键的约束条件。L 太小,比如 8 位,能表达的信息太少,只够存个编号;L 太大,比如 512 位,解码器很难把这么多独立的比特从有限的像素冗余里准确还原,提取率会断崖式下跌。我做过的实验里,64 位是视觉质量和提取率的甜点区间,128 位也能跑但需要把嵌入强度 eps 往上调 0.05 左右。

另一个问题是 Payload 的比特分布。如果直接随机生成 0/1 序列,网络很容易记住某些固定模式。我习惯对 Payload 做一次随机置乱(用固定随机种子),相当于给信息加了密,也让网络训练时面对的分布更均匀。这个置乱表要保存在系统外部,提取时先还原再解析,不然后面算法版本升级,旧图的水印全解不出来。

3. 多数据集支持:COO、ImageNet、Cifar10、QRCode 的加载与适配

3.1 四个数据集在系统里分别扮演什么角色

标题里列的四个数据集不是随便凑数的,它们的差异恰好覆盖了水印系统落地时遇到的各类场景。Cifar10 是 32×32 的小图,训练速度快,适合开发阶段频繁调试网络结构;ImageNet 子集图片分辨率高、语义丰富,用来验证水印在真实摄影图片上的不可见性;COO 这个自建数据集(某公司内部收集的物体居中样本集合)和 ImageNet 最大的区别在于背景干净、主体突出,水印在这种图上的表现会和自然图像完全不同——背景平坦区域多,残差稍微大一点就穿帮;QRCode 作为数据集的特殊性在于,它的 Payload 本身是结构化的二维码内容,不是随机比特,解码时除了要还原比特位,还要过一遍二维码容错校验,相当于给水印系统套了一层纠错保险。

import torch from torch.utils.data import Dataset from torchvision import datasets, transforms import cv2 import numpy as np class WatermarkDataset(Dataset): def __init__(self, name, root="./data", payload_len=64, split="train"): self.payload_len = payload_len self.split = split if name == "cifar10": self.data = datasets.CIFAR10( root=root, train=(split == "train"), download=True, transform=transforms.ToTensor() ) elif name == "imagenet_sub": # 只取前 N 个类做轻量验证,完整版直接指向 ImageFolder self.data = datasets.ImageFolder( root=f"{root}/imagenet/{split}", transform=transforms.Compose([ transforms.Resize((128, 128)), transforms.ToTensor() ]) ) elif name == "coo": # 自建数据集,目录结构同 ImageFolder self.data = datasets.ImageFolder( root=f"{root}/coo/{split}", transform=transforms.Compose([ transforms.Resize((128, 128)), transforms.ToTensor() ]) ) else: raise ValueError(f"Unknown dataset: {name}") def __len__(self): return len(self.data) def __getitem__(self, idx): img, _ = self.data[idx] # 随机生成 Payload,每一位独立均匀分布 payload = torch.randint(0, 2, (self.payload_len,)).float() return img, payload

这个统一接口的关键在于 Payload 是随数据即时生成的,而不是预先存在硬盘上。好处是每一轮 epoch 网络见到的都是全新的水印内容,天然避免了对训练集的过拟合。坏处是如果后续要做「固定水印 + 固定图片」的测试集对比,需要额外保存一批 seed,让每次生成的 Payload 可复现。

3.2 分辨率适配:Cifar10 的 32×32 怎么和 ImageNet 的 224×224 共存

直接把不同分辨率的数据一起喂给网络是行不通的。常见的做法是全部 Resize 到同一个尺寸,但这个尺寸的选择有讲究。我一般分为两档:快速实验用 64×64,正式训练用 128×128。64 能在一个消费级显卡上把 batch 开到 64,训练一轮只要几分钟,适合验证 loss 能不能降;128 是视觉质量和训练速度的折中,再往上到 224,显存占用翻倍,收益却只是解码器对缩放攻击的鲁棒性略好一点。

Resize 的方式也要注意,我踩过坑的是简单粗暴用cv2.resize(img, (w, h), interpolation=cv2.INTER_LINEAR)来处理全部数据集,结果发现 Cifar10 本身像素颗粒大,线性插值会把边缘模糊掉,编码器学出来的残差全是高频噪声。后来对 Cifar10 改用INTER_CUBIC,保留边缘锐度,水印提取率和视觉质量都有明显提升。这属于数据预处理层面的细节,但直接影响后面网络的收敛速度。

3.3 QRCode 的接入方式:把二进制 Payload 换成二维码内容

接入 QRCode 的思路有两种,很多文章混为一谈。第一种是把二维码图片当作载体图的局部贴上去,这是可见水印,不在本文讨论范围;第二种是把二维码的内容(比如一串 URL 或者文本)先编码成二进制字符串,再切成长度为 payload_len 的块,作为 Payload 送入编码器。第二种才叫端到端隐藏式嵌入。实际操作里把文本转比特很容易:先 UTF-8 编码,再按位展开,不够长就填充,太长就做截断加哈希校验。

import qrcode import io from PIL import Image def qr_content_to_payload(text, payload_len=64): # 先按 UTF-8 转成字节流 byte_data = text.encode("utf-8") # 按位展开成 0/1 列表 bit_list = [] for b in byte_data: for i in range(7, -1, -1): bit_list.append((b >> i) & 1) # 超出 payload_len 的部分截断,不足则循环填充 if len(bit_list) > payload_len: bit_list = bit_list[:payload_len] else: idx = 0 while len(bit_list) < payload_len: bit_list.append(bit_list[idx % len(bit_list)]) idx += 1 return torch.tensor(bit_list, dtype=torch.float32) def generate_qr_reference(text): # 生成二维码图像作为解码质量的参照物 qr = qrcode.QRCode(version=5, error_correction=qrcode.constants.ERROR_CORRECT_H) qr.add_data(text) qr.make(fit=True) img = qr.make_image(fill_color="black", back_color="white") return img.resize((128, 128))

提取端要做的事情是反过程:从解码器拿到 64 位比特后,先拼回字节,再做 UTF-8 解码。这里最折磨人的问题是比特错位——如果训练时用的是「按位展开再循环填充」的逻辑,提取时必须严格按照同样顺序拼装,差一位整个文本就乱码。我的建议是把比特处理逻辑封装成一个单独的PayloadCodec类,训练和推理都走同一个类,避免两套实现在某次重构后悄悄分叉。

4. 完整训练流程:损失配比、训练循环与收敛判断

4.1 损失函数怎么配:四个损失项的权重关系

端到端水印训练本质是一个多目标优化问题。我常用的损失函数拆成四块:L_rec 是载体图与嵌入图的像素级重建误差,用 L1 或 MSE,保证视觉一致性;L_ext 是解码器输出与原始 Payload 的交叉熵,保证信息能提取出来;L_adv 是判别器损失,让嵌入图更接近自然图像分布,这属于锦上添花,前期可以不加;L_perc 是感知损失,用预训练 VGG 的特征距离替代逐像素距离。

def watermarked_loss(orig_img, stego_img, payload, decoded, lambda_rec=1.0, lambda_ext=2.0): # 像素级重建:L1 比 MSE 对极端残差更宽容 l_rec = torch.mean(torch.abs(orig_img - stego_img)) # 提取损失:BCEWithLogits 自带 sigmoid 稳定性更好 l_ext = nn.functional.binary_cross_entropy_with_logits( decoded, payload, reduction="mean" ) return lambda_rec * l_rec + lambda_ext * l_ext, l_rec, l_ext

权重配比是最吃经验的部分。lambda_ext 设太小,网络会偷懒,直接让嵌入图等于原图,解码器随便猜,loss 也能降得很低;lambda_rec 设太小,网络会疯狂往图里塞噪声,提取率上去了,图却没法看了。我调过很多组配比,比较稳的起始点是 lambda_rec=1.0, lambda_ext=2.0。这里有个判断技巧:观察训练早期 l_rec 和 l_ext 的绝对值量级,如果 l_rec 是 0.01 而 l_ext 是 0.5,那梯度基本被提取损失主导,需要把 lambda_rec 往上提或者用梯度归一化处理。

4.2 训练循环与关键超参数:BATCH_SIZE、学习率、EMA

训练环节有个经常被忽略的点:水印系统要同时对「嵌入质量」和「提取准确率」负责,很容易因为 batch 内样本的难度差异导致震荡。我一般会给解码器单独配一个低一点的学习率,比如编码器 1e-4、解码器 5e-5,因为解码器如果学得太快,会让编码器得到一个「过于容易」的解码任务,残差信号反而没有充分散布。另外强烈建议开 EMA(指数移动平均)保存一份模型参数的滑动平均,推理时用 EMA 版本,提取率通常能稳定提升 2~3 个百分点,这个收益几乎零成本。

optimizer = torch.optim.AdamW( [ {"params": encoder.parameters(), "lr": 1e-4}, {"params": decoder.parameters(), "lr": 5e-5}, ], weight_decay=1e-5 ) scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=200) ema_params = {} @torch.no_grad() def update_ema(decay=0.999): for name, param in encoder.named_parameters(): if name not in ema_params: ema_params[name] = param.data.clone() else: ema_params[name].mul_(decay).add_(param.data, alpha=1 - decay) for epoch in range(200): for img, payload in train_loader: img, payload = img.cuda(), payload.cuda() stego = encoder(img, payload) decoded = decoder(stego) loss, l_rec, l_ext = watermarked_loss(img, stego, payload, decoded) optimizer.zero_grad() loss.backward() torch.nn.utils.clip_grad_norm_(encoder.parameters(), 1.0) torch.nn.utils.clip_grad_norm_(decoder.parameters(), 1.0) optimizer.step() update_ema() scheduler.step() print(f"epoch {epoch}: loss={loss.item():.4f} rec={l_rec.item():.4f} ext={l_ext.item():.4f}")

这里的clip_grad_norm_很重要。编码器和解码器联合训练时,梯度经过编码器回传的路径长,偶尔会出现个别 batch 的梯度爆炸,如果不做裁剪,参数会直接飞到 NaN。我之前没有做裁剪,损失曲线在 30 轮左右突然断崖式变成 nan,白白浪费了十几个小时的训练时间。梯度裁剪的参数 1.0 是经验值,如果你的输入做了标准化,可以尝试调成 0.5。

4.3 训练中止与恢复:Checkpoint 策略与收敛判断

判断水印模型「有没有练好」,不看 loss 曲线,看两个硬指标:验证集上的比特提取准确率(Accuracy)和嵌入图 PSNR。我见过不少人在 loss 降不动了就开始折腾网络结构,其实系统性的做法是每 5 个 epoch 跑一次验证,把提取准确率和 PSNR 记录到 CSV,训练结束后回头看一眼两个指标有没有达到预设目标,例如准确率 ≥ 98%,PSNR ≥ 36dB。如果准确率卡在 90% 上不去,优先调 lambda_ext 和 eps;如果 PSNR 低于 32dB,图已经能看到明显纹理,优先降 eps。

Checkpoint 一定要存两份:一份是当前步数的可断点续训版本,包含 optimizer 和 scheduler 的状态;另一份是最佳验证指标版本的含 EMA 参数快照。我习惯把 EMA 参数单独存成.pt文件,因为续训时如果用 EMA 参数继续训练,模型会不稳定。这个习惯帮我避免过好几次「验证效果好、一续训就崩」的尴尬场景。

5. 避坑与排查:端到端水印训练最常见的五个翻车点

5.1 提取准确率卡在 50% 附近,和随机猜一样

现象:训练了 50 个 epoch,loss 在降,但验证集上的比特准确率始终在 50%~55% 之间徘徊。原因很可能是解码器输出端的归一化和损失函数不匹配——如果解码器最后一层用了 Sigmoid,又叠加了 BCEWithLogitsLoss(内部自带 Sigmoid),数值上会出现双重压缩,梯度大幅衰减。解决:只用 BCEWithLogitsLoss,解码器末尾不接任何激活;或者改用F.binary_cross_entropy(decoded.sigmoid(), payload),两种写法选一种,不要混用。

5.2 嵌入图质量很好,但一碰 JPEG 压缩就全丢

现象:无攻击时提取率 99%,加上 JPEG 质量因子 80 的压缩后掉到 40%。原因在于训练时没有做过 JPEG 近似模拟,解码器只见过纯净的嵌入图,对频域变化毫无泛化能力。解决:在训练循环里对 stego 图做随机可微的 JPEG 模拟——常见做法是用torchvision.transforms里的高斯模糊加随机量化噪声近似,或者直接用jpeg4py做不可微扰动,反向传播跳过这一步。我给训练管线里加的扰动顺序是「随机 JPEG → 随机高斯噪声 → 随机裁剪一小块」,每种攻击的发生概率设成 0.3,训练出来的模型抗攻击能力非常可观。

5.3 训练初期 PSNR 异常高,但后来突然暴跌

现象:前 20 个 epoch PSNR 稳定在 40dB,突然在 25 轮降到 28dB,肉眼可见水印纹理。原因多半是 eps 没有做退火——网络发现把残差撑满上限可以轻松完成解码任务,梯度引导它拼命往残差上限逼近。解决:把 eps 设置为前 30 轮从 0.05 线性升到 0.15。这个方案是我在一个模拟项目里试出来的,效果立竿见影,前期低 eps 让编码器先学会和原图对齐,后期再放宽边界学习如何塞信息。

5.4 QRCode 内容解码出来全是乱码,但比特准确率很高

现象:单独看比特准确率 97%,但把比特拼回文本后全是乱码。原因在于 PayloadCodec 的比特顺序不一致——训练时随机生成的是「高位在前」序列,推理时拼接却用了「低位在前」的顺序,或者填充逻辑在某个版本里被改动过一处。解决:给 PayloadCodec 写一个自校验函数,每次训练前随机生成 10 条文本,编码进图再提取出来对比原始文本,确保整个编解码链路端到端一致。这个自校验函数建议常驻测试集里,防止一次无意的重构就静默破坏整个系统。

5.5 多个数据集混训时,模型偏向 Cifar10,ImageNet 上提取率偏低

现象:混合数据集训练,最终在 Cifar10 上提取率 99%,在 ImageNet 子集上只有 88%。原因有两种可能:Cifar10 尺寸小、类别简单,每轮 epoch 能跑更多 batch,模型被简单样本绑架;或者两种数据集的图像分布差异太大,模型学了一个偏向某一侧的中间表示。解决:一是对数据集做容量均衡采样,让每个 epoch 里四个数据集的样本数大致一致;二是给不同数据集分配不同的嵌入强度 eps——Cifar10 用 0.1,ImageNet 用 0.15,自建 COO 用 0.12,QRCode 用 0.18。原因也好理解,复杂图需要更强的嵌入信号才能保证提取率,但这个经验值不是万能的,需要按自己的数据分布调整。

6. 鲁棒性验证与进阶技巧:从「能跑」到「敢上线」

模型训练收敛之后,最重要的一步是用一套固定的攻击基准去验收。我在模拟项目里做过一份攻击矩阵:JPEG 压缩分别测质量因子 90、70、50;几何攻击测随机裁剪到 70%、缩放回原尺寸;噪声攻击测高斯噪声标准差 0.01、0.03。每个攻击场景跑 100 张图,统计比特错误率。这个表看起来死板,但它是判断「这个水印系统能不能顶到线上」的唯一根据。如果某个场景的错误率超了 2%,QRCode 解码还能靠容错撑住,但纯文本 Payload 就可能出错,这时就要针对该场景增加对应的攻击模拟训练或者提高 eps。

6.1 攻击模拟的注入方式和嵌入强度的曲线调度

给训练管线加入攻击模拟时,有个进阶技巧是「课程式鲁棒训练」:前 50 轮不注入任何攻击,让网络先把基本的嵌入-提取闭环跑通;50 轮之后按 epoch 逐步提高攻击强度,比如每 10 轮把 JPEG 质量从 95 降到 80,噪声标准差从 0 提到 0.03。这种方式比一开始就上最强攻击稳定得多——模型还没学会走路就被推去爬山,loss 很容易发散;课程式训练则让网络逐步适应更难的任务,最终收敛出的解也更均衡。

6.2 业务验证:把密钥、版本号和水印载荷管理起来

真正放进业务环境时,水印系统不只是两个神经网络,还要想清楚三件事:密钥管理、版本兼容和负载格式。密钥管理指 Payload 置乱的随机种子以及编码表,不能硬编码在模型文件里,要单独配置化;版本兼容指旧的模型提取新的图,或者新版模型提取旧图,都要有明确的算法版本协商机制;负载格式建议第一版就预留 4 字节的元信息位,存放水印算法版本号和校验码,后面升级算法时能低代价兼容老图。

6.3 一个实用的验证脚本:批量跑提取率和 PSNR

最后给出一个能直接用的小工具脚本,用来批量验证某张图在指定攻击下的表现。我通常会在训练完成后把最佳 epoch 的模型跑一遍全流程验收,输出一张「攻击类型 × 指标」的小表格,这比盯着一堆 loss 曲线直观得多。

def evaluate_model(encoder, decoder, img, payload, attack_fn=None): encoder.eval(); decoder.eval() with torch.no_grad(): stego = encoder(img.unsqueeze(0), payload.unsqueeze(0)) if attack_fn is not None: stego = attack_fn(stego) decoded = decoder(stego) # 比特准确率按 0.5 阈值二值化 pred_bits = (decoded > 0).float() bit_acc = (pred_bits == payload.unsqueeze(0)).float().mean().item() # PSNR 用原图和嵌入图计算 mse = torch.mean((img - stego[0]) ** 2).item() psnr = 10 * np.log10(1.0 / mse) return bit_acc, psnr, stego[0] # 用法 # bit_acc, psnr, _ = evaluate_model(enc, dec, img, payload, # attack_fn=lambda x: x * 0.9) # 模拟轻微亮度攻击

这个函数里的 attack_fn 是个灵活插槽,你可以把前面说的 JPEG 模拟、高斯噪声、裁剪统统包装成可调用对象塞进去,不用为每种攻击单独写一套验证逻辑。参数方面,evaluate_model明确要求输入 img 和 payload 都带 batch 维度,函数里处理的是单位 batch 的场景,如果你的验证数据是批量多张图,把unsqueeze去掉即可。

我现在养成的习惯是:每次调整任何环节——数据增强、网络宽度、嵌入强度,都必跑一遍这个评估函数,把结果和上一个版本打印在同一屏对比。水印系统这个东西,单独看 loss 根本看不出差别,一旦对比同一张原图在不同模型下嵌入图的 PSNR 和解码器的提取结果,哪个改了之后是变好还是变坏就一目了然。这套流程帮我在某公司的上线项目里避开了「看着指标很好、实际攻击全崩」的大坑,也让我越来越相信:端到端水印不是黑匣子,每一步实验做得干净,系统的可靠性就是能一点点堆出来的。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/14 17:03:26

AI前沿速递|AI学会“装傻“了!

2026年10月14日 三篇最新arXiv论文深度解读 导读&#xff1a; 2026年10月中旬&#xff0c;AI领域接连出现三个令人深思的突破。一篇被NeurIPS 2026接收的论文证明&#xff0c;AI模型可以利用密码学方法可证明地隐藏任意危险能力&#xff0c;让所有安全审计手段失效&#xff1b…

作者头像 李华
网站建设 2026/10/14 17:03:18

NPU数据流设计:从HBM到Shared Memory的带宽优化实战

做NPU开发这几年&#xff0c;我越来越觉得一个朴素的道理&#xff1a;计算引擎永远在等数据。主频、MAC阵列规模决定了理论算力&#xff0c;但实际跑一个卷积层&#xff0c;吞吐多少往往由数据能不能按时喂到位决定。这个“喂数据”的链路&#xff0c;就是本系列第三章要聊的核…

作者头像 李华
网站建设 2026/10/14 17:03:13

Java泛型原理与实战:从类型擦除到通配符避坑指南

1. 这份笔记不是“抄书”&#xff0c;而是泛型在真实开发中落地前的必经门槛你翻过《Java编程思想》第15章&#xff0c;也跟着视频敲过List<String>的demo&#xff0c;但当第一次在公司项目里写DAO层接口时&#xff0c;面对BaseMapper<T>和Page<T>的嵌套泛型…

作者头像 李华
网站建设 2026/10/14 17:03:10

UCIe 2.0解析:Chiplet互连从物理层到管理框架的演进

最近在啃UCIe 2.0的规范文档&#xff0c;起因是团队在规划下一代Chiplet互连方案&#xff0c;发现不少同事还停留在“UCIe就是改版PCIe”的粗浅印象里。实际上&#xff0c;UCIe 2.0对整个Die-to-Die互连体系做了相当大的扩圈&#xff0c;从单纯的物理层标准升级为包含管理、可观…

作者头像 李华
网站建设 2026/10/14 17:02:27

温冲后磁环电感阻抗衰减引发EMC超标:机理与可靠性验证方案

-40℃到150℃的温冲之后&#xff0c;你的磁环电感还有几分功力&#xff1f;这个问题我一开始真没当回事&#xff0c;直到某车载网关项目的EMC复测在温度条件下翻车&#xff0c;才明白磁性元件在宽温下的变化能有多隐蔽。整块板卡常温摸底明明余量充足&#xff0c;放进温箱跑了三…

作者头像 李华