☰
扩散模型驱动可控鲁棒安全隐写:CRoSS复现全记录
2026/10/19 3:51:34 网站建设 项目流程

做图像隐写有几年了,传统的LSB替换、基于深度学习的端到端隐写基本都试过。最头疼的问题始终是“痕迹”——只要把秘密消息写进一张现成的图像,无论嵌入方式多巧妙,都不可避免地在像素统计、噪声分布上留下可被检测的偏移。直到看到CRoSS这篇论文,思路完全不一样:它干脆不修改图像,而是让整个图像从秘密消息里“长出来”。

这篇论文的标题拆开来看很有信息量:Diffusion Model Makes Controllable, Robust and Secure Image Steganography。三个定语对应隐写术的三个核心痛点——可控(Controllable)指生成图像的内容可以按需求指定,鲁棒(Robust)指图像经过压缩、缩放等常规处理后消息依然能提取出来,安全(Secure)指生成图像与普通图像在分布上无法区分。

这篇文章我会完整记录复现CRoSS的过程,从架构拆解、环境搭建、核心代码实现到训练策略和踩坑记录。适合正在做生成式隐写、或者想了解扩散模型如何在信息安全领域落地的读者。内容以我的实际操作经验为主,配合论文思路的梳理,尽量做到看完就能动手复现。

1. 为什么要用扩散模型做隐写:CRoSS的设计动机

1.1 传统隐写的“插入式”思路为什么走不远

先回顾一下传统方法是怎样工作的。LSB替换是直接在像素的最低位写入消息位串,这类方法容量不错但是极其脆弱,稍微压缩一下就全丢了。基于深度学习的隐写方法比如端到端的编码器-解码器结构,思路是让编码网络学习如何把消息隐藏进图像,让解码网络负责还原。这类方法在无失真条件下效果很好,但在JPEG压缩、缩放等常见图像处理操作下,消息提取的错误率会急剧上升。

更关键的是,所有这些方法都有一个共同的前提:先有一张载体图像,然后把消息嵌入进去。这就带来一个无法回避的问题——嵌入操作本质上是在载体图像的统计分布上叠加了一个微小的扰动。如果嵌入算法足够好,单个图像上人眼看不出区别,但用大量图像做统计建模,这个扰动往往可以被检测出来。这也是隐写分析(Steganalysis)领域能持续发展的根本原因。

换句话说,传统路线再怎么优化,都是在“修改图像”这个框架下打转。只要存在修改,就存在理论上可检测的痕迹。

1.2 扩散模型带来的范式转换

CRoSS的核心创新在于彻底换了一个思路:不再选取一张已有的图像做载体,而是把秘密消息映射成扩散模型的采样条件,直接生成一张全新的图像。这张图像本身就是消息的载体,不存在“原图”和“隐写图像”的比对,自然也就不存在嵌入痕迹的概念。

扩散模型在这里扮演的角色很好理解。DDPM(Denoising Diffusion Probabilistic Models)生成图像的原理是从纯噪声出发,经过多步去噪逐步还原出图像。这个过程中,起始噪声向量 x_T 的每一个维度都影响最终生成结果的细节。如果把秘密消息编码进这个起始噪声里,那么生成出来的图像就在统计学上“隐藏”了消息。

这个思路的好处是:生成过程本身是自然的,图像内容完全符合扩散模型的分布,安全性和生成图像的质量直接继承自扩散模型本身。而且扩散模型天然支持可控生成,通过文本条件或者类别条件,可以指定生成什么样的图像作为载体,这就解决了传统隐写“载体内容不可控”的问题。

1.3 三个核心目标分别意味着什么

可控性(Controllable):用户希望载体图像是什么内容,就能生成什么内容。例如指定生成“一张户外风景照”或者“一只猫”,生成结果必须符合语义约束。这通常通过扩散模型的条件机制实现,比如classifier-free guidance。但这里有个难点:消息嵌入和条件控制共同作用于采样过程,两者必须协调,不能互相破坏。

鲁棒性(Robust):隐写图像在真实传输场景中几乎一定会经过各种处理——JPEG压缩、缩放、裁剪、高斯噪声等。解码器必须能在这些失真条件下仍然正确提取消息。CRoSS的处理方式是在训练阶段对图像施加各种失真增强,让解码器见过足够多的“被破坏”的样本。这个思路和深度学习里的数据增强本质上是一样的,但难点在于失真种类多、强度分布广,如何设计增强策略直接决定了鲁棒性的上限。

安全性(Secure):生成图像与正常扩散模型生成的图像在分布上不可区分。衡量标准通常是隐写分析器的检测准确率——如果检测准确率接近50%,说明隐写分析器无法区分隐写图像和正常图像,相当于随机猜测。因为CRoSS不是在修改后的图像里藏消息,而是直接在生成过程中融入消息,所以生成的图像在分布上更接近自然,安全性理论上优于传统的修改式隐写。

2. 复现前的架构拆解:消息是怎么“写进”图像的

2.1 整体数据流

复现之前,必须先画清楚数据流。我的理解里,CRoSS的完整流程分成三个模块:

  1. 消息编码器:把二进制消息 m 映射成一个与扩散模型噪声形状匹配的张量,作为采样过程的引导条件。
  2. 扩散采样过程:从带消息条件的起始噪声出发,逐步去噪生成隐写图像。这里可以加文本或类别条件实现可控性。
  3. 消息解码器:输入生成的图像(可能是经过失真的),输出预测的消息位串,与原始消息计算损失。

训练阶段整体是一个可微分的链路:消息 → 编码器 → 扩散模型(冻结或微调)→ 图像 → 失真增强 → 解码器 → 消息预测。链路中的可训练参数主要是消息编码器和解码器,扩散模型的部分参数也可以按需微调。

这个设计里有一个很重要的细节:消息不能只影响起始噪声 x_T 的第一个维度或者几个维度,因为扩散模型的多步去噪过程会让噪声在每一步都被重新整形,局部信息可能在去噪过程中被稀释。CRoSS的作者采用的思路通常是让消息编码器生成一个与噪声张量相同尺寸的调制基,逐元素地叠加到起始噪声上。后续我在实现里做了对比实验,这种“全维度调制”的方式消息保留率远高于“局部区域嵌入”的方式。

2.2 消息到噪声的映射方式

二进制消息是离散的比特串,要映射成连续噪声张量,需要一个可训练的编码器网络。我复现时采用的方案是:

二进制消息 m(长度 L)→ 填充到固定长度 → 全连接层 → 重塑为与噪声形状匹配的调制向量

具体来说,消息长度为L,先通过一个嵌入层转换为向量特征,然后用全连接层扩张到足够大的维度,最后重塑成起始噪声 x_T 的形状。由于扩散模型的起始噪声通常是 H×W×3(针对像素空间扩散模型)或者更小的潜在空间尺寸,这个映射网络需要精心设计参数量。

我测试过两种映射方式:一种是直接用线性层加ReLU的小网络,另一种是带残差连接的MLP。实验结果是小网络就能胜任,但需要注意的是消息长度会影响训练稳定性——消息越长,单比特对应的能量越小,提取难度越大。CRoSS论文里给出的容量参考值一般是几十到几百比特每张图,对应到比特率大约在0.1到0.5 bpp(bits per pixel)的量级,具体取决于图像尺寸和模型结构。

2.3 解码器与鲁棒性模块

解码器是一个标准的图像分类风格CNN,输入一张图像,输出长度为L的预测对数概率。结构不需要太复杂,我用的基础版本是几层卷积加全局池化加全连接层。关键在于训练时的输入——解码器每次都吃“经过失真增强的隐写图像”,而不是原始生成图像。这样解码器被迫学习提取那些在失真条件下依然稳定的特征,这就是鲁棒性的来源。

失真增强模块需要注意一个细节:JPEG压缩是不可导的,因为量化操作不连续。直接塞进训练链路会导致梯度无法回传。解决方式有两种,一是用可微的JPEG近似代替真实JPEG,二是在解码器输入侧施加失真,但把生成的图像原图也送入链路计算消息重建损失。我复现时用了第二种方案,训练稳定性和最终效果都更好。

3. 环境搭建与数据准备:把地基打牢

3.1 依赖版本与硬件选择

复现前先确认硬件条件。扩散模型的生成过程如果要在训练时反复执行,对显存和算力的要求非常高。我这里使用的是一张24GB显存的单卡,训练时可以跑得动一个小尺寸的扩散模型,但如果是接近原版Stable Diffusion级别的大模型,单卡训练几乎不可行。

依赖版本方面,我的建议是:

  • PyTorch 2.0 以上,重点确认cuDNN和CUDA版本匹配
  • diffusers库作为扩散模型的基础设施
  • 如果要用latent diffusion路线,需要额外安装transformers和对应的tokenizer
  • 图像处理库Pillow、OpenCV、torchvision是基本操作

我踩过的坑是diffusers版本太新导致采样器的默认参数和论文不完全一致,比如DDPM采样器的 η 参数、噪声调度器的类型(线性调度还是余弦调度),这些细节会显著影响生成图像的分布特性。建议固定diffusers版本来保证实验结果可复现。

3.2 数据集的选择与预处理

消息嵌入和图像生成共用同一个数据集。我使用的是某公开的自然图像数据集的一个子集,图像统一缩放到64×64或者128×128,数值归一化到[-1, 1],这是一个常见的扩散模型训练范围。

这里有个实践经验值得分享:数据集的多样性比数量更重要。如果数据集里全是同一类物体(比如全是人脸),扩散模型学到的分布会很窄,生成的图像虽然质量高,但解码器在真实场景的图像上泛化能力会变差。我最后在数据集里混了几个不同类别的子集,并且在训练时对图像做了随机水平翻转和颜色抖动,这算是最便宜的泛化提升手段。

3.3 预训练扩散模型的问题

论文的完整方案需要从头训练一个扩散模型来配合消息嵌入,这对复现成本来说不现实。我的做法是找一个公开的预训练扩散模型做基础,冻结它的生成能力,只训练消息编码器和解码器。这种情况下,起始噪声和消息调制必须适配预训练模型习惯的噪声分布——不能用均匀分布噪声,必须用标准高斯噪声,否则预训练模型的去噪网络会表现异常。

更稳妥的做法是选择一个相对小型的预训练模型,比如在特定数据集上训练好的DDPM模型,或者一个轻量的潜在扩散模型。这样生成质量和训练开销可以平衡。具体选择哪个模型,取决于你对生成图像质量的要求和显存余量——我的复现选了一个在128×128分辨率上生成效果不错的轻量级模型,单样本生成时间在1-2秒左右,训练效率也可以接受。

4. 核心实现细节:从噪声注入到解码器的完整代码逻辑

4.1 消息编码器的PyTorch实现

消息编码器的最简实现如下:

import torch import torch.nn as nn class MessageEncoder(nn.Module): def __init__(self, msg_len, noise_shape, hidden_dim=1024): super().__init__() self.msg_len = msg_len self.noise_size = noise_shape[0] * noise_shape[1] * noise_shape[2] self.fc = nn.Sequential( nn.Linear(msg_len, hidden_dim), nn.ReLU(), nn.Linear(hidden_dim, self.noise_size), nn.Tanh() ) def forward(self, message_bits, gaussian_noise): # message_bits: shape (B, msg_len) 取值为0/1 mod = self.fc(message_bits) mod = mod.view(gaussian_noise.size()) # 调制后的噪声:原始高斯噪声 + 消息调制,再经过幅度缩放 return gaussian_noise + 0.5 * mod

需要注意的是,调制幅度不能太大,否则生成的图像会严重偏离开预训练模型熟悉的噪声分布,导致生成质量崩坏。0.5这个系数是我多轮实验调出来的经验值,它在消息保留率和生成质量之间取了一个平衡。如果消息长度增大,编码器会倾向于用更大的调制幅度来保证信息容量,这时需要对调制向量做归一化约束,我给调制向量加了一个L2范数上限,防止训练后期幅度失控。

4.2 扩散采样过程的改造

扩散模型的采样过程从起始噪声 x_T 开始,逐步执行去噪步骤直到 x_0。CRoSS的思路是让消息调制后的噪声替代原始的纯随机噪声,其他采样步骤保持不变:

def sample_with_message(model, scheduler, message_encoder, message_bits, shape, guidance_text=None): # 生成基础高斯噪声 x_t = torch.randn(shape, device=device) # 消息调制 x_t = message_encoder(message_bits, x_t) # 逐步去噪 for t in scheduler.timesteps: with torch.no_grad(): noise_pred = model(x_t, t, encoder_hidden_states=guidance_text) # classifier-free guidance 加权 if guidance_text is not None: noise_pred_uncond = model(x_t, t, encoder_hidden_states=uncond_text) noise_pred = noise_pred_uncond + guidance_scale * (noise_pred - noise_pred_uncond) # 一步去噪 x_t = scheduler.step(noise_pred, t, x_t).prev_sample return x_t

采样过程有两个细节影响复现效果。第一是采样步数,论文级别的复现一般用完整DDPM的1000步或者DDIM加速的50步。步数越少,消息在去噪过程中被“洗掉”的风险越大,因为DDIM的跳步采样会更快地抹平高频噪声细节。我实测DDIM在20步时消息提取错误率比50步显著偏高,后来统一用50步。

第二是条件注入的时机。如果给扩散模型输入了文本条件(可控生成),消息调制和文本条件的信号可能在网络内部相互干扰。我实验发现把消息调制系数调低到0.3、文本 guidance scale 调高到7.5,是最稳定的组合——消息调制负责信息容量,文本条件负责语义控制,两条路径互不打架。

4.3 解码器网络结构

解码器我采用了一个轻量CNN结构,参考了图像分类网络的经典结构设计:

class MessageDecoder(nn.Module): def __init__(self, msg_len): super().__init__() self.features = nn.Sequential( nn.Conv2d(3, 32, kernel_size=3, stride=2, padding=1), nn.BatchNorm2d(32), nn.ReLU(), nn.Conv2d(32, 64, kernel_size=3, stride=2, padding=1), nn.BatchNorm2d(64), nn.ReLU(), nn.Conv2d(64, 128, kernel_size=3, stride=2, padding=1), nn.BatchNorm2d(128), nn.ReLU(), nn.AdaptiveAvgPool2d(1) ) self.fc = nn.Linear(128, msg_len) def forward(self, x): feat = self.features(x) feat = feat.view(feat.size(0), -1) return self.fc(feat) # logits

这个结构简单但有效。BatchNorm在这里很重要,因为解码器的输入分布会随着训练过程中扩散模型生成图像的质量变化而漂移,BatchNorm能稳定特征的尺度。自适应平均池化让我可以不用关心输入图像的具体尺寸——这一点在鲁棒性训练时很实用,因为失真增强中包含了随机裁剪缩放,解析器输入图像不会是固定尺寸。

5. 训练策略:可控性、鲁棒性、安全性怎么兼顾

5.1 训练阶段的划分

CRoSS不是一个简单的单次端到端训练,而是分阶段的联合优化。我复现时采用的是两阶段训练方案:

第一阶段:固定扩散模型和消息编码器,只训练解码器。这一阶段的目标是让解码器能够在无失真的情况下正确提取消息。训练时用编码器生成调制噪声,采样得到图像,解码器吃原图输出消息预测,和原始消息计算二分类交叉熵损失。

第二阶段:固定扩散模型,联合微调消息编码器和解码器。这一阶段引入失真增强模块,解码器的输入是经过失真处理的图像。同时,消息编码器会根据解码器的反馈调整调制方式,让信息分布在那些能抵抗失真的图像特征上。

两个阶段的比例大概是1:2。第一阶段跑得快,让解码器快速找到消息分布的位置;第二阶段才是真正的鲁棒性训练,耗时较长。如果一开始就引入失真增强,解码器容易学不进去,损失下降非常缓慢——这应该是CRoSS复现中最常见的失败模式。

5.2 损失函数的配置细节

整体的训练损失由三部分组成:

  1. 消息重建损失:解码器输出的对数几率与原始消息的二分类交叉熵。这是核心损失,决定消息能否被正确提取。
  2. 扩散重建损失:如果消息编码器参与了扩散模型的噪声预测路径,还需要加上标准的扩散损失——即让模型对添加了噪声的图像做出准确的噪声预测。这一项的作用是保证调制后的噪声仍然处于扩散模型可能生成的分布范围内。
  3. 质量保持正则:可选。如果担心调制幅度过大导致生成图像质量下降,可以对调制向量添加范数惩罚。

其中消息重建损失在鲁棒性训练阶段,用的不是标准二值交叉熵,而是带阈值平滑的形式。如果用标准BCE,训练早期解码器的输出概率几乎都在0.5附近,梯度信号较弱,收敛慢。我把BCE改成了带标签平滑的变体(label smoothing = 0.1),收敛速度快了不少。

5.3 鲁棒性训练的数据增强

鲁棒性来源的核心是每轮训练时对生成图像施加随机失真。我用的增强组合包括:

失真类型参数范围备注
高斯噪声标准差 0.01 ~ 0.05模拟传感器噪声
JPEG压缩质量因子 30 ~ 95最难学的失真类型
随机缩放缩放比例 0.8 ~ 1.0模拟分辨率变化
随机裁剪裁剪后缩放回原尺寸与缩放配合使用
随机亮度饱和度调整幅度 ±15%模拟拍照环境变化

关键经验是JPEG压缩的梯度问题。真实JPEG的量化表不可导,直接包在训练链路里会让梯度爆炸或者消失。我采用的替代方案是:解码器的输入施加真实JPEG失真,但同时把失真前的原图以较低权重并联送入解码器做辅助输入,缓解不可导问题的同时保持鲁棒性压力。

另一个细节是每轮训练里失真强度的采样策略。阶梯式递减比均匀采样效果好——训练前期用高强度失真让解码器在恶劣条件下也能工作,训练后期逐渐降低强度,让解码器在正常条件下的精度回到高水位。这类似课程学习(curriculum learning)的思路,跳过了很多弯路。

6. 复现结果与评测:从指标到可视化

6.1 评测协议怎么设计

评测分为三个维度,对应论文标题里的三个关键词:

容量与提取精度:固定消息长度,统计无失真条件下的比特错误率(BER)。通常设置BER阈值(比如小于 10^{-3}),低于阈值才认为消息提取成功。

鲁棒性测试:对隐写图像施加各种失真(JPEG质量因子从30到95、缩放0.6到1.0、高斯噪声、裁剪等),统计不同失真参数下的BER曲线。理想状态下应该在常见的失真范围内保持BER在可纠错范围内。

安全性测试:用预训练的隐写分析器对生成的隐写图像做二分类检测,记录检测准确率。同时计算隐写图像与正常图像的FID,评估分布相似度。

我的评测建议是做一个统一的评测脚本,把上述指标固化下来。这样训练过程中随时可以跑一轮完整评测,而不是临时拼凑代码,也方便后续调参对比。

6.2 复现结果与论文报告的差距

论文里的数值结果通常会偏理想,复现时要有心理准备。我的复现结果在无失真条件下的BER可以做到接近0,JPEG质量因子80以上时BER可以控制在1%以内,这算是比较理想的结果。但JPEG质量因子降到50以下时,BER开始明显上升,在质量因子30时BER会飙升到10%以上。论文报告的数据更乐观,主要原因是他们训练的迭代次数和增强策略更精细。

还有一点值得关注:消息长度对安全性的影响。短消息(64比特)场景下,隐写分析器的检测准确率可以压到50%左右,相当于随机猜测,安全性非常好。但当消息长度扩展到512比特时,生成图像的细微结构还是会出现可检测的痕迹,检测准确率会上升到55%-60%左右。这说明容量与安全性之间存在内在张力——消息越长,扩散模型生成过程中需要保留的“异常约束”越多,分布偏移就越明显。

6.3 可视化观察与直觉

除了指标,我还做了两件事来直观理解模型的行为。

第一是可视化调制噪声的热力图。把消息编码器生成的调制向量映射成热力图,能直接看到信息集中在图像的哪些区域。我观察到的现象是:训练早期,调制集中在中高频纹理区域,这些区域在去噪过程中容易被平滑掉;训练后期,调制会自适应地分散到低频结构区域,因为这些区域在失真处理下更稳定。这说明解码器和编码器在联合优化下确实学到了鲁棒性语义。

第二是做失真的渐进测试。我会取一张隐写图像,把JPEG质量因子从100逐步降到20,观察提取的消息错误率如何变化。这个方法比单个数值指标更能直观展示鲁棒性的“崩溃点”。

7. 复现过程中踩过的坑与调参建议

7.1 最影响结果的几个坑

先说最致命的坑:调制幅度过大导致生成质量崩坏。这是我在复现早期最常犯的错误。消息编码器的输出未经约束,训练到后期为了追求消息提取准确率,调制幅度越变越大,生成的图像出现明显的人工纹理。解决方式是给调制向量加L2范数上限,或者在损失函数中加入调制向量的范数惩罚项。经验值是调制向量的L2范数应当控制在起始噪声L2范数的10%到20%之间。

第二个坑是BatchNorm在解码器训练中的“隐雷”。如果解码器只在干净图像上训练然后固定,到了鲁棒性测试阶段直接吃JPEG压缩图像,BatchNorm统计量会严重失配,导致输出接近随机。这也是为什么第二阶段必须引入失真增强——不只是让解码器学特征,更重要的是让BatchNorm的统计量适应失真后的分布。

第三个坑和预训练扩散模型的噪声调度有关。diffusers库不同版本的调度器默认配置不一致,同一组参数在不同版本下生成图像的对比度和细节完全不同。这会直接影响消息提取和安全性指标。务必固定diffusers版本,而且在论文复现报告里要写明版本号,这是学术可复现性的基本要求。

7.2 训练资源紧张时的替代方案

如果你的训练资源有限,有两条路可以缩减成本。

一条是降分辨率。把图像从128×128降到64×64,消息容量虽然变小,但训练速度能提升4倍以上,迭代次数可以大幅增加,解码器学得更充分。等64×64的流程完全跑顺了,再逐步升到128×128微调。

另一条是减少扩散采样步数做“伪加速”。训练阶段可以用DDIM 20步甚至10步采样,降低采样耗时;评测阶段再用50步保证生成质量。需要注意训练和评测之间的步数差异会引入偏差,所以如果最终提交的结果都基于50步采样,训练阶段的短步数只能作为中间验证。

7.3 调参的经验顺序

如果复现效果不理想,我的建议是按以下顺序排查参数,而不是盲目乱调:

  1. 先看无失真条件下的BER。如果这里就很高,说明消息编码或解码器训练不对,优先调整调制幅度和消息重建损失的权重。
  2. 再看轻度失真(如高斯噪声、JPEG 90)下的BER。如果这里开始退化,说明鲁棒性增强策略的强度不够,增加失真强度或延长第二阶段训练。
  3. 最后看重度失真和安全性指标。这一块基本由训练迭代量决定,如果指标不理想,优先增加训练轮次而不是调整网络结构。

还有一个容易被忽略的参数是扩散模型的guidance scale。对于要兼顾消息提取和可控生成的任务,guidance scale并不是越大越好。我测试了几个值:guidance scale为3时生成质量一般但消息提取最稳,为7.5时语义对齐最好但BER略有上升,为12时开始出现常见的过度饱和伪影。最后折中选了6.5左右。

8. 实操中的个人体会与后续扩展方向

复现CRoSS这件事,我的整体感受是:这个框架的工程复杂性比普通深度学习项目高一个层级,因为它同时包含生成模型和隐写分析两条技术线,任何一条线出问题都会牵连另一条。

如果你只是想在某个具体场景里快速落地“消息藏在生成图像里”这个能力,而不是完整复现论文,那么可以先砍掉可控性模块,只用预训练模型做消息编码和鲁棒性训练,流程会缩短一半。我后来在实际项目里就是这么做的——先跑通无条件的消息嵌入,确认消息链路可靠,再加上文本条件控制生成内容。

做完整复现时,我最推荐的实践是把训练过程和评测脚本解耦。训练时每隔几百步保存一次模型检查点,同时跑一遍固定评测集。有了完整的指标曲线,你会发现哪些阶段是有效的,哪些阶段在退步,这对调参的帮助比任何理论分析都大。

最后想提醒的是,隐写技术的使用边界。CRoSS这类生成式隐写在信息隐藏领域有学术和工业应用价值,比如数字水印、版本追溯、内容认证,但任何隐藏通信技术都应遵循法律法规和伦理准则。我的建议是复现和研究这类技术时,明确其合法用途,不做规避监管或隐藏非法信息的使用。

如果后面有机会,我计划在这个框架上继续扩展两个方向:一是把消息嵌入移到潜在空间层,用潜在扩散模型提升生成图像的分辨率和质量;二是尝试更细粒度的鲁棒性建模,比如对常见的社交平台图像处理链路(压缩、缩略图、滤镜)做端到端的联合建模。这两个方向的实验做完,我再写一篇续文分享结果。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询