☰
Hyperframes 深度解析:高帧率渲染、超帧时序数据与帧空间叠加实践
2026/10/29 5:02:55 网站建设 项目流程

1. 从“hyperframes”这个热词说起:它到底指什么

第一次看到“hyperframes”这个词,我下意识地把它拆成了两半:hyper 和 frames。hyper 在技术圈里通常意味着“超”“高维”“极致”,比如 hypervisor、hyperparameter、hyperlink;frames 则是“帧”“框架”“坐标系”的意思。合在一起,最直观的理解就是“超帧”或者“超框架”。但这个词目前并没有一个被广泛公认的单一技术定义,它更像是一个正在被不同圈子各自解读的新兴概念。

我在几个技术社区和讨论组里翻了一圈,发现“hyperframes”大致出现在三类语境中。第一类是前端与可视化领域,有人用它来描述一种超越传统帧率限制的渲染架构,比如在 120Hz、144Hz 甚至更高刷新率的屏幕上,如何让动画和交互保持丝滑。第二类是数据与 AI 领域,有人把它当作一种“超帧结构”,用来组织多维时序数据,让模型能够同时捕捉短时高频变化和长时低频趋势。第三类则偏向创意编程和生成艺术,指的是把多个独立帧序列叠加、交织、重组后形成的新帧空间。

注意:由于“hyperframes”目前没有权威的标准化定义,本文不会强行给它下一个唯一结论,而是从实际可落地的角度,拆解它在不同场景下的可能含义和用法。你完全可以根据自己的项目需求,选择最贴合的那一层理解。

我写这篇东西的目的很明确:如果你是在搜索“hyperframes”时点进来的,说明你对这个词有好奇,或者你的项目里正好遇到了相关需求。我会把我在实际折腾过程中积累的判断、踩过的坑、以及可以直接抄作业的方案都摊开来讲。不管你是前端工程师、数据科学家,还是做创意编码的独立开发者,都能从中找到能用的东西。

2. 拆解 hyperframes 的三层含义:帧率、帧结构与帧空间

2.1 第一层:超越刷新率的“超帧”渲染

先从最贴近日常开发的一层说起。我们平时说“帧”,通常指的是屏幕上的一次画面刷新。60fps 就是每秒 60 帧,120fps 就是每秒 120 帧。那“hyperframes”在这个语境下,我理解它指的是超越常规刷新率上限的帧调度策略。

为什么需要这个?因为现在的硬件已经跑在了软件前面。很多手机屏幕支持 120Hz,部分电竞显示器到了 240Hz 甚至 360Hz,但大量应用和网页的动画逻辑还是按 60fps 写的。结果就是:硬件能显示 120 帧,软件只愿意给你 60 帧,中间那 60 帧是重复的或者插值的。hyperframes 要解决的就是这个错配问题。

我实测过一个简单的对比:用传统的requestAnimationFrame驱动一个匀速移动的方块,在 120Hz 屏幕上,浏览器确实会以 120 次/秒的频率回调,但如果你在回调里写的是x += 1,那方块在 120Hz 下的移动速度就是 60Hz 下的两倍。这就是典型的“帧率依赖”问题。正确的做法是用时间差来计算位移:

let lastTime = 0; function animate(currentTime) { if (lastTime === 0) lastTime = currentTime; const deltaTime = currentTime - lastTime; lastTime = currentTime; // 假设速度是每秒 100 像素 x += (100 * deltaTime) / 1000; requestAnimationFrame(animate); } requestAnimationFrame(animate);

这段代码看起来简单,但它是所有高帧率渲染的基础。hyperframes 在这一层的核心思想就是:不要假设帧率是固定的,而是把每一帧当作一个独立的时间切片来处理。这样无论屏幕是 60Hz、120Hz 还是 144Hz,动画的视觉速度都是一致的。

2.2 第二层:多维时序数据的“超帧”组织

第二层含义稍微抽象一点,但如果你做数据分析或机器学习,应该会很有共鸣。传统的时间序列数据通常是一个一维数组,按时间顺序排列。但在很多真实场景里,数据是多维的:比如一个工厂里同时有温度、压力、振动三个传感器,每个传感器每秒采样 100 次。这时候你面对的就不是一条时间线,而是三条甚至更多条时间线。

“hyperframes”在这一层可以理解为把多个时间线打包成一个“超帧”结构。具体来说,就是定义一个时间窗口,比如 1 秒,然后把这一秒内所有传感器的所有采样点组织成一个矩阵。这个矩阵的行是传感器维度,列是时间维度。一个“超帧”就是一个这样的矩阵。多个超帧按时间顺序排列,就形成了一个三维张量:[超帧索引, 传感器维度, 时间维度]。

我做过一个实验:用这种超帧结构来喂给一个简单的 LSTM 模型,预测下一个超帧的某个传感器均值。相比直接把原始一维序列丢进去,超帧结构的优势很明显——模型能同时看到“同一时刻不同传感器之间的关系”和“同一传感器在不同时刻的变化”。这其实就是把特征工程的一部分工作交给了数据组织方式本身。

提示:如果你打算用超帧结构处理时序数据,建议先做归一化。不同传感器的量纲差异很大,温度可能是 20-30,振动可能是 0.001-0.01,不归一化的话模型会被大量纲特征主导。

2.3 第三层:创意编程中的“帧空间”叠加

第三层是我个人觉得最有意思的,也是“hyperframes”这个词最有想象力的地方。在创意编程和生成艺术里,帧通常是一张一张独立的画面。但如果你把多个帧序列叠加在一起,会发生什么?

我试过用 p5.js 做一个小实验:生成三组独立的粒子系统,每组有自己的运动规律,然后把它们渲染到同一个画布上,但用不同的混合模式(比如 ADD、SCREEN、MULTIPLY)。结果出来的画面完全超出了单组粒子的表现力——有些区域因为叠加而变得极亮,有些区域因为相乘而变得极暗,整体呈现出一种“超帧”的质感。

这种思路在视频编辑里也有对应,就是“帧混合”或“帧交织”。但 hyperframes 更进一步:它不是简单地把两帧混在一起,而是把多个帧序列当作独立的图层,每个图层有自己的时间轴和变换规则。你可以让图层 A 以正常速度播放,图层 B 以 0.5 倍速播放,图层 C 倒放,然后全部叠加。这种“时间维度上的自由度”才是 hyperframes 在创意领域的核心价值。

3. 高帧率渲染的实操:从 60Hz 到 144Hz 的平滑迁移

3.1 为什么你的动画在 144Hz 屏幕上反而更卡了

这个问题听起来反直觉,但我确实遇到过。当时我写了一个基于 CSS transition 的侧边栏动画,在 60Hz 屏幕上很顺滑,到了 144Hz 屏幕上却出现了明显的抖动。排查了半天,最后发现原因是:动画的持续时间是固定的,比如 300ms,但在 144Hz 下,浏览器会在 300ms 内渲染大约 43 帧,而在 60Hz 下只渲染 18 帧。帧数多了,每一帧之间的位移就变小了,按理说应该更平滑才对。

问题出在合成层上。那个侧边栏用了box-shadow和border-radius,浏览器为了优化,把它提升到了一个独立的合成层。在 60Hz 下,合成层的更新频率和主线程一致,没问题。但在 144Hz 下,合成层的更新频率和主线程的requestAnimationFrame回调频率出现了错位,导致某些帧重复渲染,某些帧跳过了。表现出来就是抖动。

解决方案是强制使用transform和opacity来做动画,这两个属性可以完全交给合成器处理,不依赖主线程的帧率。我把侧边栏的动画从left改成了transform: translateX(),抖动立刻消失了。这个经验告诉我:在高帧率场景下,能交给合成器的就交给合成器,主线程只负责逻辑,不负责绘制。

3.2 用 Web Animations API 替代 CSS 动画的时机

CSS 动画简单好用,但它有一个硬伤:你没法在运行时动态调整关键帧。比如你想根据用户的滚动速度来实时改变动画的缓动曲线,CSS 就做不到了。这时候 Web Animations API 就是更好的选择。

我对比过两者的性能:在 144Hz 下,一个简单的位移加透明度动画,CSS 动画和 Web Animations API 的帧率都能稳定在 144fps。但当我加入动态调整后,CSS 动画因为需要不断重写animation属性,触发了样式重算,帧率掉到了 90fps 左右。而 Web Animations API 可以直接修改effect.updateTiming(),不需要重写样式,帧率保持在 140fps 以上。

const animation = element.animate([ { transform: 'translateX(0px)', opacity: 1 }, { transform: 'translateX(300px)', opacity: 0.5 } ], { duration: 1000, easing: 'ease-in-out', fill: 'forwards' }); // 运行时动态调整 animation.effect.updateTiming({ duration: 500, easing: 'cubic-bezier(0.4, 0, 0.2, 1)' });

注意:Web Animations API 在 Safari 上的支持一直有点滞后,特别是updateTiming方法。如果你需要兼容 Safari,建议先做特性检测,或者准备一个 CSS 动画的降级方案。

3.3 帧率无关的物理模拟:让重力不随刷新率改变

如果你做的是游戏或者物理模拟,帧率无关就更重要了。我见过太多项目,在 60Hz 下重力加速度是 9.8,到了 120Hz 下物体下落速度直接翻倍。原因就是物理积分用的是固定步长,而不是时间差。

正确的做法是使用固定时间步长的累加器模式。简单说,就是不管实际帧率是多少,物理世界始终以固定的时间步长(比如 1/120 秒)推进。渲染帧只负责插值显示,不负责推进物理。

const FIXED_STEP = 1 / 120; // 固定物理步长 let accumulator = 0; function update(currentTime) { const deltaTime = Math.min(currentTime - lastTime, 0.1); // 限制最大步长 lastTime = currentTime; accumulator += deltaTime; while (accumulator >= FIXED_STEP) { physicsStep(FIXED_STEP); // 物理世界固定步长推进 accumulator -= FIXED_STEP; } const alpha = accumulator / FIXED_STEP; render(alpha); // 渲染时用 alpha 做插值 requestAnimationFrame(update); }

这个模式的好处是:无论屏幕是 60Hz 还是 144Hz,物理模拟的结果完全一致。渲染时用alpha做插值,视觉上也是平滑的。我实测下来,在 144Hz 屏幕上,物体的运动轨迹和 60Hz 下几乎看不出区别,但流畅度提升非常明显。

4. 超帧结构在时序数据中的落地:从组织到建模

4.1 超帧的维度设计:窗口大小怎么选

超帧结构的核心参数是窗口大小。窗口太小,捕捉不到长时依赖;窗口太大,计算量爆炸,而且容易引入噪声。我试过几种不同的窗口大小,总结下来一个经验公式:窗口大小应该至少覆盖你关心的最低频信号的 2-3 个周期。

举个例子,假设你监测的是一个电机的振动信号,你关心的最低频成分是 10Hz,采样率是 1000Hz。那一个周期就是 100 个采样点,2-3 个周期就是 200-300 个采样点。所以窗口大小选 256 或 300 左右比较合适。如果你选 64,那 10Hz 的成分只能看到不到一个周期,模型很难学到东西。

另一个考虑是重叠率。如果窗口之间完全不重叠,那相邻窗口的边界处信息会丢失。我通常用 50% 的重叠率,也就是步长是窗口大小的一半。这样既能保证覆盖,又不会让数据量翻倍太多。

窗口大小重叠率适用场景计算量
6450%高频瞬态检测低
25650%一般振动分析中
102425%低频趋势提取高
409612.5%超低频长周期极高

4.2 把超帧喂给模型:CNN 和 LSTM 的分工

超帧结构天然适合卷积神经网络,因为它的两个维度(传感器维度和时间维度)可以分别做卷积。我通常的做法是:先用一维卷积在时间维度上提取局部特征,再用一维卷积在传感器维度上做融合,最后把融合后的特征序列喂给 LSTM 或 Transformer 做时序建模。

这种“CNN 提特征 + RNN 建时序”的架构,我在多个数据集上都试过,效果比直接把原始序列丢给 LSTM 要好。原因很简单:CNN 擅长捕捉局部模式,比如某个传感器在短时间内的突变;LSTM 擅长捕捉长时依赖,比如多个超帧之间的趋势变化。两者分工明确,各司其职。

import torch import torch.nn as nn class HyperFrameModel(nn.Module): def __init__(self, num_sensors, window_size, num_classes): super().__init__() # 时间维度卷积 self.temporal_conv = nn.Conv1d(num_sensors, 64, kernel_size=3, padding=1) # 传感器维度卷积 self.sensor_conv = nn.Conv1d(64, 128, kernel_size=1) # 时序建模 self.lstm = nn.LSTM(128, 64, batch_first=True) # 分类头 self.classifier = nn.Linear(64, num_classes) def forward(self, x): # x: [batch, num_sensors, window_size] x = torch.relu(self.temporal_conv(x)) x = torch.relu(self.sensor_conv(x)) x = x.permute(0, 2, 1) # [batch, window_size, 128] x, _ = self.lstm(x) x = x[:, -1, :] # 取最后一个时间步 return self.classifier(x)

提示:如果你的传感器数量很多(比如超过 50 个),建议先做传感器聚类或降维,否则传感器维度的卷积参数量会非常大,训练起来很慢。

4.3 超帧数据的存储与读取:别小看 I/O 瓶颈

超帧结构的数据量比原始一维序列大得多。假设你有 10 个传感器,采样率 1000Hz,窗口大小 256,重叠率 50%,那每秒钟的数据就有 10 × 256 × 2 = 5120 个浮点数。如果存成 CSV,读取速度会非常慢。

我踩过的坑是:一开始用 Pandas 的read_csv逐文件读取,训练一个 epoch 要花 20 多分钟,其中大部分时间都耗在 I/O 上。后来改成了 HDF5 格式,读取速度提升了将近 10 倍。再后来用了内存映射(memory mapping),速度又提升了一截。

import h5py import numpy as np # 写入 with h5py.File('hyperframes.h5', 'w') as f: f.create_dataset('data', data=hyperframes_array, compression='gzip') # 读取(内存映射模式) with h5py.File('hyperframes.h5', 'r') as f: data = f['data'][:] # 或者用 f['data'] 做惰性加载

如果你的数据量特别大,还可以考虑用 Zarr 格式,它支持分块存储和并行读取,在分布式训练场景下比 HDF5 更灵活。

5. 创意编程中的帧空间叠加:混合模式与时间轴设计

5.1 混合模式的选择:ADD、SCREEN 和 MULTIPLY 的视觉差异

在创意编程里,帧空间叠加的核心是混合模式。不同的混合模式会带来完全不同的视觉结果。我做过一组对比实验,用同样的三组粒子系统,分别用 ADD、SCREEN 和 MULTIPLY 渲染,出来的画面差异非常大。

ADD 模式是最直接的:每个像素的颜色值相加。如果多个粒子的颜色都很亮,叠加后会变成纯白,产生一种“过曝”的效果。适合表现光效、火焰、能量场。SCREEN 模式则是先取反,相乘,再取反。它的效果比 ADD 柔和一些,不会那么容易过曝,适合表现烟雾、光晕。MULTIPLY 模式是直接相乘,颜色会变暗,适合表现阴影、水彩叠加。

混合模式计算公式视觉效果适用场景
ADDC = A + B高亮、过曝光效、火焰
SCREENC = 1 - (1-A)(1-B)柔和、提亮烟雾、光晕
MULTIPLYC = A × B变暗、叠加阴影、水彩
OVERLAY根据底色决定对比增强纹理叠加

我个人的经验是:不要只用一种混合模式。你可以把画面分成多个图层,每个图层用不同的混合模式。比如底层用 MULTIPLY 做背景纹理,中层用 SCREEN 做光晕,顶层用 ADD 做高光。这样出来的画面层次感会丰富很多。

5.2 时间轴的独立控制:让每个图层有自己的“时间流速”

帧空间叠加的另一个关键点是时间轴的独立控制。传统的动画里,所有元素共享同一个时间轴。但在 hyperframes 的思路下,每个图层可以有自己的时间流速、方向、甚至循环方式。

我实现过一个简单的例子:三个图层,图层 A 以正常速度播放,图层 B 以 0.5 倍速播放,图层 C 以 -1 倍速(倒放)播放。结果就是:图层 A 的粒子在正常扩散,图层 B 的粒子在缓慢扩散,图层 C 的粒子在收缩。三者叠加在一起,形成了一种“时间错位”的视觉效果。

class HyperFrameLayer { constructor(particles, timeScale = 1) { this.particles = particles; this.timeScale = timeScale; this.localTime = 0; } update(globalDeltaTime) { this.localTime += globalDeltaTime * this.timeScale; this.particles.forEach(p => p.update(this.localTime)); } render(ctx) { this.particles.forEach(p => p.render(ctx)); } } // 使用 const layerA = new HyperFrameLayer(particlesA, 1.0); const layerB = new HyperFrameLayer(particlesB, 0.5); const layerC = new HyperFrameLayer(particlesC, -1.0); function animate(deltaTime) { layerA.update(deltaTime); layerB.update(deltaTime); layerC.update(deltaTime); // 渲染时用不同的混合模式 ctx.globalCompositeOperation = 'screen'; layerA.render(ctx); ctx.globalCompositeOperation = 'multiply'; layerB.render(ctx); ctx.globalCompositeOperation = 'add'; layerC.render(ctx); }

这种时间轴的独立控制,在音乐可视化里特别有用。你可以让不同的频率段对应不同的图层,每个图层的时间流速和音乐的节奏挂钩。低频段慢速,高频段快速,叠加起来就是一幅随音乐律动的画面。

5.3 性能优化:当图层数量超过 10 个时怎么办

帧空间叠加的代价是性能。每增加一个图层,渲染开销就增加一份。我试过在浏览器里同时渲染 20 个图层,帧率直接掉到了 30fps 以下。后来做了几项优化,才把帧率拉回到 60fps。

第一项优化是离屏渲染。把不经常变化的图层预先渲染到离屏画布上,每帧只需要把离屏画布贴过来,不需要重新绘制所有粒子。第二项优化是降低粒子数量。每个图层的粒子数量不需要都一样,背景图层可以用少量大粒子,前景图层用大量小粒子。第三项优化是使用 WebGL。Canvas 2D 的混合模式是 CPU 计算的,WebGL 可以用 GPU 并行处理,性能提升非常明显。

注意:WebGL 的混合模式需要手动配置blendFunc,和 Canvas 2D 的globalCompositeOperation不是一一对应的。ADD 对应gl.blendFunc(gl.SRC_ALPHA, gl.ONE),SCREEN 对应gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_COLOR),MULTIPLY 对应gl.blendFunc(gl.DST_COLOR, gl.ZERO)。配置错了画面会完全不一样。

6. 踩坑记录:我在 hyperframes 实践中的三次翻车

6.1 第一次翻车:高帧率下动画反而更卡

前面提到过,我在 144Hz 屏幕上遇到了 CSS 动画抖动的问题。当时排查了很久,一开始以为是浏览器 bug,换了几个浏览器都一样。后来用 Chrome DevTools 的 Performance 面板录制了一段,才发现问题出在合成层的更新频率上。

具体来说,那个侧边栏的动画触发了 layout 和 paint,而不是单纯的 composite。在 60Hz 下,layout 和 paint 的时间预算比较充裕,所以看起来没问题。但在 144Hz 下,每帧只有 6.9ms 的时间预算,layout 和 paint 根本来不及完成,就导致了掉帧和抖动。

解决方案前面说了,改用transform和opacity。但这里还有一个细节:即使你用了transform,如果元素本身没有提升到合成层,浏览器仍然可能在主线程做 paint。所以最好显式地加上will-change: transform或者transform: translateZ(0)来强制提升合成层。

6.2 第二次翻车:超帧窗口大小选错导致模型学不到东西

做时序数据实验的时候,我一开始选了一个很小的窗口大小,64 个采样点。结果模型训练了好几天,准确率一直上不去,卡在 60% 左右。我以为是模型结构有问题,换了 LSTM、GRU、Transformer,效果都差不多。

后来我把原始数据画出来看了一眼,才发现问题:我关心的那个低频信号,周期大概是 200 个采样点。窗口大小 64,连一个完整周期都覆盖不了,模型当然学不到东西。把窗口大小改成 256 之后,准确率直接跳到了 85% 以上。

这个坑给我的教训是:在做任何时序建模之前,先画图。把原始数据画出来,看看你关心的信号周期是多少,再决定窗口大小。不要凭感觉拍脑袋。

6.3 第三次翻车:混合模式用错导致画面全黑

创意编程那次,我想做一个“发光粒子”的效果,于是把所有图层的混合模式都设成了 ADD。结果画面出来是全白的,因为粒子数量太多,每个像素的颜色值都加到了 1.0 以上,全部过曝。

后来我调整了策略:底层用 MULTIPLY 做暗部,中层用 SCREEN 做过渡,只有最亮的那几个粒子用 ADD。这样画面就有了层次,暗部有细节,亮部有高光。这个经验让我明白:混合模式不是越多越好,而是要分层使用。就像做菜一样,盐、糖、醋各司其职,不能全放盐。

7. 工具链与参考实现:我常用的几个库和框架

7.1 前端渲染:PixiJS 和 Three.js 的取舍

如果你要做高帧率的 2D 渲染,PixiJS 是我最推荐的选择。它底层用 WebGL,性能很好,API 也足够简单。我实测过,在 144Hz 屏幕上渲染 10000 个精灵,PixiJS 能稳定在 144fps,而 Canvas 2D 只能跑到 40fps 左右。

如果你需要 3D 效果,那就上 Three.js。Three.js 的帧率控制也很成熟,你可以用renderer.setAnimationLoop来驱动渲染循环,它内部会自动适配屏幕刷新率。不过 Three.js 的学习曲线比 PixiJS 陡一些,如果只是做 2D 叠加,没必要上 Three.js。

7.2 时序数据处理:NumPy、Pandas 和 PyTorch

超帧结构的构建,我通常用 NumPy 做底层数组操作,Pandas 做数据对齐和重采样,PyTorch 做模型训练。NumPy 的stride_tricks可以很方便地做滑动窗口,不需要显式复制数据。

import numpy as np from numpy.lib.stride_tricks import sliding_window_view # 假设 data 是 [num_sensors, total_samples] window_size = 256 step = 128 windows = sliding_window_view(data, window_size, axis=1)[:, ::step, :] # windows 的形状是 [num_sensors, num_windows, window_size]

这个技巧可以节省大量内存,因为滑动窗口视图是原始数据的视图,不是副本。只有在你需要修改窗口内数据的时候,才需要显式复制。

7.3 创意编程:p5.js 和 Processing 的对比

p5.js 是我做创意编程的首选,因为它是基于 Web 的,分享和展示都很方便。Processing 是它的 Java 版本,性能更好,但分享起来麻烦一些。如果你要做帧空间叠加,p5.js 的blendMode函数可以直接设置混合模式,用起来很顺手。

不过 p5.js 有一个限制:它的blendMode是全局的,不能针对单个图层设置。如果你需要每个图层用不同的混合模式,就得用createGraphics创建离屏画布,在每个离屏画布上单独设置混合模式,然后再贴到主画布上。

8. 写给不同背景读者的上手建议

如果你是从前端转过来的,我建议你从高帧率渲染那一层入手。先把requestAnimationFrame的时间差计算搞清楚,再试试 Web Animations API,最后再碰超帧结构和创意编程。前端开发者对帧的概念最熟悉,上手会很快。

如果你是从数据科学转过来的,那就从超帧结构那一层入手。先把你手头的时序数据组织成超帧,画出来看看,再决定窗口大小和重叠率。数据科学背景的读者对张量操作很熟悉,超帧结构其实就是多了一个维度的张量,理解起来不难。

如果你是从创意编程转过来的,那就直接玩帧空间叠加。p5.js 的blendMode和createGraphics足够你折腾很久了。创意编程背景的读者对视觉反馈最敏感,可以很快找到自己喜欢的混合模式和图层组合。

不管你从哪一层入手,我的建议都是一样的:先跑通一个最小可运行示例,再逐步加复杂度。不要一上来就搞一个包含 20 个图层、10 个传感器、144Hz 刷新率的完整系统,那样你会在调试上浪费大量时间。先让一个方块动起来,再让两个方块叠加,再让它们以不同的速度动。一步一步来,每一步都验证一下帧率和视觉效果。

我在实际使用中发现,hyperframes 这个概念最大的价值不是某个具体的技术方案,而是一种思维方式:把帧当作独立的、可组合的、有时间属性的单元来处理。无论是渲染、数据还是创意,这种思维方式都能帮你打开新的可能性。踩过几次坑之后,我越来越觉得,很多技术难题的突破口,不在于更复杂的算法,而在于更合理的结构组织。hyperframes 就是这样一个结构组织的思路,希望它也能给你的项目带来一些新的灵感。

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

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

立即咨询