主动说话人检测(Active Speaker Detection, ASD)是一个音视频多模态任务:给定一段包含多人的视频,模型需要逐帧判断每个可见人脸是否正在说话。
这项技术在视频会议、影视后期、社交机器人、助听设备等场景中有着广泛应用。其核心挑战在于,仅靠音频无法区分"谁"在说话,仅靠视频又容易被点头、咀嚼等非语言动作干扰,因此必须将音频和视觉信息有效融合。
LR-ASD(Lightweight and Robust Network for Active Speaker Detection)正是为此设计的一个轻量级解决方案。它最初以 Light-ASD 的名字发表于 CVPR 2023,后在 IJCV 2025 发表了扩展版本,在保持极小模型体积(约 0.84M 参数,权重文件仅 3.4MB)的同时,在 AVA-ActiveSpeaker 数据集上达到了 94.45% 的 mAP。
LR-ASD 采用经典的双流编码 + 融合检测架构,由四个核心模块组成:

┌─────────────────┐
MFCC (T×4, 13) ──▶│ Audio Encoder │──▶ 音频嵌入 (T, 128)─┐
└─────────────────┘ │
▼
┌─────────────────┐ ┌────────────┐
│ Fusion │────▶│ Detector │──▶ (T, 128) ──▶ FC ──▶ 说话/非说话
└─────────────────┘ └────────────┘
┌─────────────────┐ ▲
灰度帧 (T, 112, 112)──▶│ Visual Encoder │──▶ 视觉嵌入 (T, 128)─┘
└─────────────────┘四个模块分别是:
模块 | 输入 | 输出 | 作用 |
|---|---|---|---|
Audio Encoder | MFCC 特征 (B, 1, 13, T×4) | (B, T, 128) | 从音频频谱中提取时序特征 |
Visual Encoder | 灰度人脸帧 (B, 1, T, 112, 112) | (B, T, 128) | 从人脸视频中提取时序特征 |
Fusion | 音频嵌入 + 视觉嵌入 | (B, T, 256) | 注意力机制融合两路特征 |
Detector | 融合特征 (B, T, 256) | (B, T, 128) | 双向 GRU 建模时序依赖 |
最终通过一个全连接层(FC: 128→2)和 softmax 输出每帧属于"说话"类的概率。
音频编码器接收 MFCC(Mel 频率倒谱系数)特征作为输入。原始音频以 16kHz 采样,通过 python_speech_features.mfcc() 提取 13 维 MFCC 特征,每秒产生约 100 帧(即每个视频帧对应 4 个 MFCC 帧)。
编码器由 3 个 Audio Block + 2 个 MaxPool 层堆叠而成:
输入 (B, 1, 13, T×4)
│
├── Audio_Block_1 (1→32) # MFCC 维度方向卷积 + 时间方向卷积
├── MaxPool (时间维度减半)
├── Audio_Block_2 (32→64)
├── MaxPool (时间维度减半)
├── Audio_Block_3 (64→128)
│
├── Mean (在 MFCC 维度取平均) # 将 13 维压缩为 1
└── Transpose
│
输出 (B, T, 128)每个 Audio Block 的设计体现了空间-时间分离卷积的思想:先用 (k, 1) 大小的卷积核沿 MFCC 频率维度卷积(两层,kernel 大小分别为 5 和 3),再用 (1, k) 大小的卷积核沿时间维度卷积。这种分离设计相比直接使用 2D 卷积,能显著减少参数量,同时分别捕获频率模式和时间动态。
视觉编码器接收灰度人脸帧序列。输入的像素值(0-255)会先进行归一化:
x = (x / 255 - 0.4161) / 0.1688其中 mean=0.4161、std=0.1688 是在训练集上统计的人脸图像均值和标准差。
编码器结构与音频编码器对称,由 3 个 Visual Block + 2 个 MaxPool3d 层 + AdaptiveMaxPool2d 组成:
输入 (B, 1, T, 112, 112)
│
├── Visual_Block_1 (1→32, stride=2) # 空间分辨率减半 → 56×56
├── MaxPool3d (空间减半) # → 28×28
├── Visual_Block_2 (32→64)
├── MaxPool3d (空间减半) # → 14×14
├── Visual_Block_3 (64→128) # 14×14
│
├── Transpose → (B, T, 128, 14, 14)
├── Reshape → (B×T, 128, 14, 14)
├── AdaptiveMaxPool2d → (B×T, 128, 1, 1) # 空间维度压缩
└── View → (B, T, 128)Visual Block 同样采用空间-时间分离策略:先用 3D 卷积核 (1, k, k) 处理空间信息,再用 (k, 1, 1) 处理时间信息。第一个 Block 在空间维度使用 stride=2 做下采样。
Fusion 模块负责将音频嵌入 (B, T, 128) 和视觉嵌入 (B, T, 128) 融合为统一表示 (B, T, 256)。
x = concat(audio_embed, visual_embed, dim=2) # (B, T, 256)
identity = x.transpose(1, 2) # (B, 256, T)
w = sigmoid(BN(Conv1d(identity))) # 通道注意力权重
x = (identity * w).transpose(1, 2) # 加权后的融合特征核心思想是通过一个 1×1 卷积 + BatchNorm + Sigmoid 生成通道注意力权重,让模型自适应地调整音频和视觉通道的贡献程度。例如在嘈杂环境下,模型可以学会降低音频通道的权重,更多依赖视觉信息。
Detector 模块在融合特征上建模时序依赖关系。它使用手动实现的双向 GRU(而非 PyTorch 内置的 bidirectional GRU),分别用一个正向 GRU 和一个反向 GRU 处理序列:
x1, _ = gru_forward(dropout(x)) # 正向 GRU: (B, T, 64)
x2, _ = gru_backward(dropout(flip(x)))# 反向 GRU: (B, T, 64)
x2 = flip(x2) # 翻转回正序
x = Fusion(x1, x2) # 注意力融合: (B, T, 128)GRU 的隐藏维度为 256 // 4 = 64,正向和反向的输出再通过同一个 Fusion 模块融合,最终得到 (B, T, 128) 的时序感知特征。
检测器输出经过 reshape 变为 (B×T, 128),然后通过一个线性层 FC(128→2) 映射到二分类 logits,最后 softmax 取第二个通道("说话"类)的概率作为最终分数。
训练采用多任务学习策略,同时优化两个损失:
总损失为 loss = lossAV + 0.5 × lossV。纯视觉辅助损失迫使视觉编码器单独就能提取出有判别力的特征,从而提升整体的鲁棒性。
原始音频 (任意采样率)
│
├── 重采样至 16kHz 单声道
│
├── MFCC 提取 (numcep=13, winlen=0.025s, winstep=0.010s)
│ → 每秒约 100 帧,每帧 13 维
│
├── 对齐到视频帧数: target_length = num_video_frames × 4
│ → 不足则 wrap padding,超出则截断
│
└── 输出: float32 数组,shape (T×4, 13)视频预处理是一个多步骤的 pipeline,分为两个阶段:
阶段一:人脸裁剪(从原始视频到人脸片段)
原始视频
│
├── 1. 场景检测 (SceneDetect)
│ → 将视频按镜头切换分割为多个场景
│
├── 2. 人脸检测 (S3FD)
│ → 逐帧检测所有人脸的 bounding box
│ → 检测时将帧缩放至原始尺寸的 0.25 倍以加速
│
├── 3. 人脸跟踪
│ → 在每个场景内,用 IOU > 0.5 将逐帧检测结果关联为连续轨迹
│ → 过滤掉长度不足 10 帧的短轨迹
│ → 使用中值滤波平滑 bounding box 轨迹
│
└── 4. 人脸视频裁剪
→ 以人脸框中心为基准,向外扩展 40% (cropScale=0.40)
→ 裁剪并 resize 至 224×224
→ 同步裁剪对应时间段的音频
→ 输出: 每个人脸轨迹一对 .avi + .wav 文件阶段二:模型输入预处理(从人脸片段到模型输入)
人脸视频片段 (224×224, 25fps)
│
├── 转为灰度图
├── resize 至 224×224 (如果不是的话)
├── 中心裁剪 112×112: face[56:168, 56:168]
│ → 效果上聚焦于人脸中下部(鼻子、嘴巴、下巴区域)
│ → 额头和头发被裁掉,嘴部运动占比更大
│
└── 输出: float32 数组,shape (T, 112, 112),像素值 0-255
(归一化在模型 forward 内部完成)关于中心裁剪这一步值得展开说明:人脸检测时用了 cropScale=0.40 的外扩,使得 224×224 的裁剪画面中人脸居中,但上下留有较多的头发/下巴空间。推理时再取中心 112×112(即原图的 1/4 面积),就自然地把关注区域收窄到了鼻子到下巴的范围。这个设计是合理的——对于说话人检测任务,嘴唇运动是最强的视觉信号。
训练阶段对音频和视觉各有专门的增强策略:
音频增强:以 50% 的概率随机叠加同 batch 内其他样本的音频作为噪声,SNR 在 -5dB 到 +5dB 之间随机采样,模拟真实环境中的背景干扰。
视觉增强:随机选择以下四种方式之一:
orig:保持原样flip:水平翻转crop:随机裁剪 70%~100% 区域并 resize 回原尺寸rotate:随机旋转 -15° 到 +15°对于单个人脸视频片段,推理流程是三步 pipeline:
# 1. 音频前端:MFCC → 音频嵌入
audio_embed = model.forward_audio_frontend(audio_feature) # (1, T, 128)
# 2. 视觉前端:灰度帧 → 视觉嵌入
visual_embed = model.forward_visual_frontend(visual_feature) # (1, T, 128)
# 3. 音视频后端:融合 → 时序检测 → 分类
av_output = model.forward_audio_visual_backend(audio_embed, visual_embed) # (T, 128)
score = lossAV.forward(av_output, labels=None) # (T,) 每帧分数分数 > 0 表示该帧被判定为"正在说话",分数越大越确信。
为了获得更稳定的结果,原始代码采用了多时长滑窗推理 + 结果平均的策略:
duration_set = {1, 1, 1, 2, 2, 2, 3, 3, 4, 5, 6} # 秒对同一段视频分别以 1 秒、2 秒、3 秒……6 秒为窗口大小切片推理,每种时长重复多次(1 秒 ×3、2 秒 ×3 等),最后将所有结果在帧维度上取平均。这样做的好处是:
对于一段包含多人的原始视频,完整的推理 pipeline 如下:
原始视频.mp4
│
├── Step 1: 场景检测 ──── CPU
├── Step 2: 人脸检测 ──── GPU (S3FD)
├── Step 3: 人脸跟踪 ──── CPU
├── Step 4: 视频裁剪 ──── CPU + ffmpeg
├── Step 5: 说话人检测 ── GPU (LR-ASD)
│ → 对每个人脸轨迹,多时长推理得到逐帧分数
└── Step 6: 可视化 ────── CPU
→ 绿色框标记说话人,红色框标记非说话人
→ 输出: video_out.avi参数 | 值 | 说明 |
|---|---|---|
优化器 | AdamW | weight_decay=0.01 |
初始学习率 | 0.001 | |
学习率调度 | StepLR | 每 epoch 衰减为 0.95 倍 |
最大 epoch | 40 | |
Batch size | 2000 帧 | 动态 batch:按总帧数而非样本数 |
Dropout | 0.5 | 仅在 Detector 的 GRU 输入处 |
"动态 batch"是一个有意思的设计:由于不同视频片段的长度差异很大(几帧到几百帧),固定样本数的 batch 会导致显存占用波动剧烈。LR-ASD 的做法是将训练集按视频长度排序,然后以"总帧数不超过 2000"为限组织 mini-batch。这样既保证了显存使用的稳定性,又让同一 batch 内的视频长度相近,减少了 padding 的浪费。
指标 | 值 |
|---|---|
mAP | 94.45% |
参数量 | 0.84M |
权重大小 | 3.4MB |
使用 AVA 预训练权重直接测试(zero-shot 迁移):
Name | Bell | Boll | Lieb | Long | Sick | Avg |
|---|---|---|---|---|---|---|
F1 | 88.8% | 77.9% | 90.3% | 85.4% | 88.3% | 86.1% |
使用 TalkSet 微调后的权重:
Name | Bell | Boll | Lieb | Long | Sick | Avg |
|---|---|---|---|---|---|---|
F1 | 96.9% | 89.4% | 97.6% | 99.0% | 99.2% | 96.4% |
TalkSet 微调带来了超过 10 个百分点的平均 F1 提升,尤其在 Long 和 Sick 两个场景上接近满分。
LR-ASD 的"轻量"名副其实。以下是各模块的参数分布:
模块 | 参数数量 | 占比 |
|---|---|---|
Visual Encoder | 约 72 个参数张量 | ~50% |
Audio Encoder | 约 72 个参数张量 | ~45% |
Fusion | 6 个参数张量 | ~2% |
Detector (GRU + Fusion) | 14 个参数张量 | ~3% |
分类头 (FC) | 2 个参数张量 | <1% |
整个模型总计约 0.84M 参数(84 万),这得益于:
bidirectional=True 的 GRU 更灵活,融合方式也更丰富。torch, torchvision
numpy, opencv-python
scipy, python_speech_features
pandas, tqdm
scenedetect, sklearn# 下载并预处理 AVA 数据集
python train.py --dataPathAVA /path/to/AVA --download
# 训练
python train.py --dataPathAVA /path/to/AVA
# 使用预训练权重评估
python train.py --dataPathAVA /path/to/AVA --evaluation# 使用 AVA 预训练权重
python Columbia_test.py --evalCol --colSavePath /path/to/columbia
# 使用 TalkSet 微调权重
python Columbia_test.py --evalCol --pretrainModel weight/finetuning_TalkSet.model --colSavePath /path/to/columbia# 将视频放入 demo 目录
python Columbia_test.py --videoName my_video --videoFolder demo
# 输出: demo/my_video/pyavi/video_out.avi
# 绿色框 = 说话人, 红色框 = 非说话人原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。