DrowSFormer-XAI:ConvNet-Transformer交叉注意力融合的驾驶员疲劳检测,99.58%准确率+可解释AI

论文深度解读与代码复现

Rajalingam et al., “DrowSFormer-XAI: A Cross-Attentive Neural Network-Transformer Fusion Framework for Drowsiness Detection with Explainable AI”, Neural Processing Letters, 58(2), 2026.

DOI: 10.1007/s11063-026-11841-6

论文信息

项目 内容
标题 DrowSFormer-XAI: A Cross-Attentive Neural Network-Transformer Fusion Framework for Drowsiness Detection with Explainable AI
作者 Kathirvel Rajalingam, Saravanan Srinivasan, Sakthi Govindaraju, Sandeep Kumar Mathivanan, Sangeetha Ramaswamy, Usha Moorthy
期刊 Neural Processing Letters, Vol. 58, No. 2, Article 30
年份 2026
DOI 10.1007/s11063-026-11841-6

核心创新

DrowSFormer-XAI首次将ConvNeXt-Base(空间局部特征)与CaiT-S36(全局语义上下文)通过交叉注意力模块双向融合,并集成Grad-CAM可解释AI,在驾驶员疲劳检测中达到99.58%准确率的同时提供决策可视化。

核心贡献(1-3句总结):

  1. 提出ConvNet-Transformer双分支交叉注意力融合架构,解决纯ConvNet缺乏全局上下文和纯Transformer忽略局部细节的问题
  2. 集成Grad-CAM可解释AI,在安全关键场景中提供决策透明度
  3. 在DD-I数据集(49,793张图像)上达到99.58%准确率,超越所有基线模型

方法详解

1. 问题定义

现有疲劳检测方法的局限性:

方法类型 优势 局限性
生理信号 (EEG/ECG/EMG/EOG) 高准确率 侵入性强,需接触皮肤
行为视觉 (摄像头) 非侵入,可嵌入式 光照/姿态/遮挡敏感
车辆行为 (车道/方向盘) 数据易获取 反应滞后,可能为时已晚
纯ConvNet 精细空间特征 缺乏全局上下文
纯Transformer 全局依赖建模 忽略局部细节(如微小闭眼)
黑盒模型 性能高 不可解释,安全关键场景不可信

2. 架构详解

3. 交叉注意力机制

核心创新——双向交叉注意力让两个分支互相增强:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
"""
DrowSFormer-XAI 核心实现
论文: Rajalingam et al., Neural Processing Letters 2026
DOI: 10.1007/s11063-026-11841-6

依赖: torch>=2.0, timm>=0.9, torchvision>=0.15
"""

import torch
import torch.nn as nn
import torch.nn.functional as F
from typing import Tuple

class CrossAttentionFusion(nn.Module):
"""
交叉注意力融合模块

ConvNeXt特征(空间精细) ↔ CaiT tokens(全局语义)
双向信息交换:全局上下文增强局部特征,局部细节引导全局注意力

数学公式:
Q_conv = W_q · F_conv (ConvNet特征作为Query)
K_trans = W_k · F_trans (Transformer tokens作为Key)
V_trans = W_v · F_trans (Transformer tokens作为Value)

F_cross = Softmax(Q_conv · K_trans^T / √d_k) · V_trans

反向同理:Transformer用ConvNet特征做Query
"""

def __init__(
self,
conv_dim: int = 1024, # ConvNeXt-Base 特征维度
trans_dim: int = 384, # CaiT-S36 特征维度
num_heads: int = 8,
dropout: float = 0.1
):
super().__init__()
self.num_heads = num_heads
self.scale = (trans_dim // num_heads) ** -0.5

# 投影到统一维度
self.conv_proj = nn.Linear(conv_dim, trans_dim)
self.trans_proj = nn.Linear(trans_dim, trans_dim)

# 交叉注意力 QKV
self.q_conv = nn.Linear(trans_dim, trans_dim)
self.k_trans = nn.Linear(trans_dim, trans_dim)
self.v_trans = nn.Linear(trans_dim, trans_dim)

self.q_trans = nn.Linear(trans_dim, trans_dim)
self.k_conv = nn.Linear(trans_dim, trans_dim)
self.v_conv = nn.Linear(trans_dim, trans_dim)

# 输出投影
self.out_conv = nn.Linear(trans_dim, conv_dim)
self.out_trans = nn.Linear(trans_dim, trans_dim)

self.dropout = nn.Dropout(dropout)
self.norm_conv = nn.LayerNorm(conv_dim)
self.norm_trans = nn.LayerNorm(trans_dim)

def forward(
self, conv_feat: torch.Tensor, trans_tokens: torch.Tensor
) -> Tuple[torch.Tensor, torch.Tensor]:
"""
Args:
conv_feat: ConvNeXt特征 [B, N_conv, conv_dim]
trans_tokens: CaiT tokens [B, N_trans, trans_dim]

Returns:
conv_enhanced: 增强后的Conv特征 [B, N_conv, conv_dim]
trans_enhanced: 增强后的Trans tokens [B, N_trans, trans_dim]
"""
B = conv_feat.shape[0]

# 投影到统一维度
conv_proj = self.conv_proj(conv_feat) # [B, N_conv, trans_dim]
trans_proj = self.trans_proj(trans_tokens) # [B, N_trans, trans_dim]

# --- 方向1: Conv特征用Trans做Key/Value ---
q_c = self.q_conv(conv_proj) # [B, N_conv, trans_dim]
k_t = self.k_trans(trans_proj) # [B, N_trans, trans_dim]
v_t = self.v_trans(trans_proj) # [B, N_trans, trans_dim]

# 多头注意力
q_c = q_c.view(B, -1, self.num_heads, trans_proj.size(-1) // self.num_heads).transpose(1, 2)
k_t = k_t.view(B, -1, self.num_heads, trans_proj.size(-1) // self.num_heads).transpose(1, 2)
v_t = v_t.view(B, -1, self.num_heads, trans_proj.size(-1) // self.num_heads).transpose(1, 2)

attn_ct = (q_c @ k_t.transpose(-2, -1)) * self.scale
attn_ct = attn_ct.softmax(dim=-1)
out_ct = (attn_ct @ v_t).transpose(1, 2).reshape(B, -1, trans_proj.size(-1))

# --- 方向2: Trans tokens用Conv做Key/Value ---
q_t = self.q_trans(trans_proj)
k_c = self.k_conv(conv_proj)
v_c = self.v_conv(conv_proj)

q_t = q_t.view(B, -1, self.num_heads, trans_proj.size(-1) // self.num_heads).transpose(1, 2)
k_c = k_c.view(B, -1, self.num_heads, trans_proj.size(-1) // self.num_heads).transpose(1, 2)
v_c = v_c.view(B, -1, self.num_heads, trans_proj.size(-1) // self.num_heads).transpose(1, 2)

attn_tc = (q_t @ k_c.transpose(-2, -1)) * self.scale
attn_tc = attn_tc.softmax(dim=-1)
out_tc = (attn_tc @ v_c).transpose(1, 2).reshape(B, -1, trans_proj.size(-1))

# 投影回各自维度 + 残差连接 + LayerNorm
conv_enhanced = self.norm_conv(conv_feat + self.out_conv(self.dropout(out_ct)))
trans_enhanced = self.norm_trans(trans_tokens + self.out_trans(self.dropout(out_tc)))

return conv_enhanced, trans_enhanced


class SEBlock(nn.Module):
"""Squeeze-and-Excitation 通道重标定"""
def __init__(self, channels: int, reduction: int = 16):
super().__init__()
self.squeeze = nn.AdaptiveAvgPool2d(1)
self.excite = nn.Sequential(
nn.Linear(channels, channels // reduction),
nn.ReLU(inplace=True),
nn.Linear(channels // reduction, channels),
nn.Sigmoid()
)

def forward(self, x: torch.Tensor) -> torch.Tensor:
b, c, _, _ = x.shape
s = self.squeeze(x).view(b, c)
s = self.excite(s).view(b, c, 1, 1)
return x * s


class CBAM(nn.Module):
"""Convolutional Block Attention Module (空间+通道注意力)"""
def __init__(self, channels: int, reduction: int = 16, kernel_size: int = 7):
super().__init__()
self.channel_att = SEBlock(channels, reduction)
self.spatial_att = nn.Sequential(
nn.Conv2d(2, 1, kernel_size=kernel_size, padding=kernel_size // 2),
nn.Sigmoid()
)

def forward(self, x: torch.Tensor) -> torch.Tensor:
x = self.channel_att(x)
avg_out = torch.mean(x, dim=1, keepdim=True)
max_out, _ = torch.max(x, dim=1, keepdim=True)
s = torch.cat([avg_out, max_out], dim=1)
s = self.spatial_att(s)
return x * s


class FocalLoss(nn.Module):
"""Focal Loss - 解决疲劳数据类别不平衡"""
def __init__(self, alpha: float = 0.25, gamma: float = 2.0):
super().__init__()
self.alpha = alpha
self.gamma = gamma

def forward(self, preds: torch.Tensor, targets: torch.Tensor) -> torch.Tensor:
ce = F.cross_entropy(preds, targets, reduction='none')
pt = torch.exp(-ce)
loss = self.alpha * (1 - pt) ** self.gamma * ce
return loss.mean()


class DrowSFormerXAI(nn.Module):
"""
DrowSFormer-XAI 完整模型

ConvNeXt-Base + CaiT-S36 + Cross-Attention + SE + CBAM + Mish
参数量: ~159M, FLOPs: ~63.4G

分类输出: 4类
- 0: drowsy (疲劳)
- 1: non-drowsy (清醒)
- 2: closed eyes (闭眼)
- 3: open eyes (睁眼)
"""

def __init__(self, num_classes: int = 4):
super().__init__()
import timm

# ConvNeXt-Base 分支 (局部空间特征)
self.conv_branch = timm.create_model(
'convnext_base', pretrained=True, num_classes=0
)
conv_dim = 1024 # ConvNeXt-Base 输出维度

# CaiT-S36 分支 (全局语义特征)
self.trans_branch = timm.create_model(
'cait_s36', pretrained=True, num_classes=0
)
trans_dim = 384 # CaiT-S36 输出维度

# 交叉注意力融合
self.cross_attn = CrossAttentionFusion(
conv_dim=conv_dim, trans_dim=trans_dim, num_heads=8
)

# 后处理模块
self.se = SEBlock(conv_dim, reduction=16)
self.cbam = CBAM(conv_dim, reduction=16)
self.mish = nn.Mish()

# 分类头
self.classifier = nn.Sequential(
nn.Linear(conv_dim + trans_dim, 512),
nn.Mish(),
nn.Dropout(0.3),
nn.Linear(512, num_classes)
)

# 标签平滑
self.label_smoothing = 0.1

def forward(
self, x: torch.Tensor, return_attention: bool = False
) -> torch.Tensor:
"""
Args:
x: 输入图像 [B, 3, 224, 224]
return_attention: 是否返回Grad-CAM热力图

Returns:
logits: 分类logits [B, num_classes]
(可选) heatmap: Grad-CAM热力图
"""
# 双分支特征提取
conv_feat = self.conv_branch(x) # [B, 1024]
trans_feat = self.trans_branch(x) # [B, 384]

# Reshape为序列形式
conv_seq = conv_feat.unsqueeze(1) # [B, 1, 1024]
trans_seq = trans_feat.unsqueeze(1) # [B, 1, 384]

# 交叉注意力融合
conv_enhanced, trans_enhanced = self.cross_attn(conv_seq, trans_seq)

# Conv分支后处理
conv_out = conv_enhanced.squeeze(1) # [B, 1024]
conv_2d = conv_out.view(-1, 1024, 1, 1)
conv_2d = self.se(conv_2d)
conv_2d = self.cbam(conv_2d)
conv_2d = self.mish(conv_2d)
conv_out = conv_2d.view(-1, 1024)

trans_out = trans_enhanced.squeeze(1) # [B, 384]

# 拼接分类
combined = torch.cat([conv_out, trans_out], dim=-1) # [B, 1408]
logits = self.classifier(combined)

if return_attention:
# Grad-CAM: 使用最后一个卷积层的梯度
heatmap = self._compute_gradcam(conv_2d, logits)
return logits, heatmap

return logits

def _compute_gradcam(
self, feature_map: torch.Tensor, logits: torch.Tensor
) -> torch.Tensor:
"""Grad-CAM 可解释性热力图"""
# 获取预测类别的梯度
pred_class = logits.argmax(dim=1)
one_hot = F.one_hot(pred_class, num_classes=logits.size(1)).float()

grad = torch.autograd.grad(
logits[:, pred_class].sum(),
feature_map,
retain_graph=True,
create_graph=False
)[0]

# 全局平均池化梯度 → 通道权重
weights = grad.mean(dim=(2, 3), keepdim=True) # [B, C, 1, 1]

# 加权求和
heatmap = (weights * feature_map).sum(dim=1, keepdim=True)
heatmap = F.relu(heatmap)
heatmap = F.interpolate(
heatmap, size=(224, 224), mode='bilinear', align_corners=False
)
heatmap = heatmap / (heatmap.max() + 1e-8)

return heatmap


# ==================== 训练与评估 ====================
if __name__ == "__main__":
# 模型配置
model = DrowSFormerXAI(num_classes=4)

# 统计参数量
total_params = sum(p.numel() for p in model.parameters())
trainable_params = sum(p.numel() for p in model.parameters() if p.requires_grad)
print(f"总参数量: {total_params / 1e6:.1f}M")
print(f"可训练参数: {trainable_params / 1e6:.1f}M")

# 模拟输入
x = torch.randn(4, 3, 224, 224)

# 前向传播
logits = model(x)
print(f"输入: {x.shape}")
print(f"输出: {logits.shape}")
print(f"Logits: {logits[0]}")

# 训练配置(按论文设置)
optimizer = torch.optim.Adam(
model.parameters(),
lr=1e-4,
weight_decay=1e-4
)
scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(
optimizer, T_max=50, eta_min=1e-6
)
criterion = FocalLoss(alpha=0.25, gamma=2.0)

print(f"\n--- 训练配置 ---")
print(f"优化器: Adam (lr=1e-4, weight_decay=1e-4)")
print(f"调度器: CosineAnnealing (T_max=50)")
print(f"损失函数: Focal Loss (alpha=0.25, gamma=2.0)")
print(f"标签平滑: 0.1")
print(f"Batch size: 64")
print(f"最大epoch: 50 (early stop patience=10)")

# 数据增强配置
print(f"\n--- 数据增强 ---")
print(f"旋转: ±30°")
print(f"水平翻转: 50%")
print(f"亮度: 50-150%")
print(f"缩放: ±20%")
print(f"平移: ±20%")

4. 损失函数设计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 完整损失函数 = Focal Loss + Label Smoothing

class DrowSFormerLoss(nn.Module):
"""组合损失:Focal Loss + Label Smoothing"""

def __init__(self, alpha=0.25, gamma=2.0, smoothing=0.1):
super().__init__()
self.focal = FocalLoss(alpha, gamma)
self.smoothing = smoothing

def forward(self, preds, targets):
# Focal Loss
focal_loss = self.focal(preds, targets)

# Label Smoothing
n_classes = preds.size(-1)
log_probs = F.log_softmax(preds, dim=-1)
smooth_loss = -log_probs.mean()

return focal_loss + self.smoothing * smooth_loss

5. 训练数据构建

数据集 来源 图像数 标注
DDD 真实驾驶员 ~35,000 drowsy/non-drowsy
MRL Eye 实验室眼部 ~15,000 closed/open
DD-I(合并) DDD + MRL 49,793 4类标注
增强后 平衡采样 ~65,000 4类均衡

数据划分: 70%训练 / 15%验证 / 15%测试,验证集10折交叉验证

实验结果

性能对比

模型 准确率 精确率 召回率 F1 特异度
DrowSFormer-XAI 99.58% 99.13% 99.08% 99.10% 99.27%
CaiT-S36 98.42% 97.80% 97.65% 97.72% 97.90%
ConvNeXt-Base 97.15% 96.30% 96.10% 96.20% 96.50%
ResNeSt101 96.80% 95.90% 95.70% 95.80% 96.10%
Twins-SVT-Base 95.20% 94.10% 93.80% 93.95% 94.20%

交叉验证稳定性

K折 准确率范围 标准差
K=3 99.3-99.7% ±0.15
K=5 99.4-99.8% ±0.13
K=10 99.3-99.8% ±0.13

独立测试集验证

指标 值
测试图像数 7,469
准确率 99.58%
特异度 99.70%
F1分数 99.29%

可解释AI:Grad-CAM

DrowSFormer-XAI的关键差异化在于决策可解释性:

检测场景 Grad-CAM热力图关注区域 判定
疲劳驾驶 眼睑区域(下垂的眼皮) ✅ 正确关注
正常驾驶 眼睛+虹膜区域 ✅ 正确关注
闭眼 眼睑闭合区域 ✅ 正确关注
睁眼 虹膜+瞳孔区域 ✅ 正确关注

安全关键意义: 当系统判定”疲劳”时,Grad-CAM热力图可显示模型是否真正在看眼部区域,避免误判(如因为光照变化误判)。

IMS应用启示

1. 架构选型建议

场景 推荐方案 理由
量产DMS ConvNeXt单分支 延迟要求高,159M参数过重
高性能DMS DrowSFormer完整架构 精度最优,可解释
边缘部署 DistillGaze蒸馏版 <5MB,<2W
安全审计 DrowSFormer + Grad-CAM 法规要求可解释

2. 部署优化路径

1
2
3
4
5
6
7
8
9
10
11
12
# 量化部署建议
# 原始模型: 159M参数, 63.4 GFLOPS
# 量化后: ~40M参数 (INT8), ~16 GFLOPS

# 步骤1: 知识蒸馏(CaiT-S36 → 轻量Transformer)
# 步骤2: INT8量化(FP32 → INT8)
# 步骤3: 剪枝(移除注意力权重小的head)
# 步骤4: ONNX导出 + TensorRT部署

# 目标芯片: QCS8255 (Hexagon NPU, 26 TOPS)
# 预期推理延迟: <15ms (量化后)
# 预期帧率: >60fps

3. 可解释AI的法规意义

Euro NCAP 2026要求DMS系统具备可审计性——系统判定需要可追溯。DrowSFormer的Grad-CAM热力图直接满足此要求:

Euro NCAP要求 DrowSFormer对应能力
检测可追溯 Grad-CAM热力图
误判可分析 注意力区域可视化
系统可信度 热力图+置信度输出
人工验证支持 热力图导出为图像

4. 与现有IMS模块集成

局限性与改进方向

  1. 参数量大:159M参数对边缘芯片偏重,需蒸馏
  2. 数据局限:DD-I数据集主要为正面面部,侧脸/大角度姿态不足
  3. 实时性:63.4 GFLOPS在QCS8255上需量化+剪枝才能满足30fps
  4. 跨域泛化:训练数据不含夜间红外场景,需补充

结论

DrowSFormer-XAI证明了ConvNet-Transformer融合在驾驶员疲劳检测中的优越性,99.58%准确率和Grad-CAM可解释性使其成为安全关键DMS的理想架构。但159M参数量需要通过蒸馏和量化才能在量产芯片部署。

对IMS开发的核心启示:

  1. 交叉注意力融合是处理多尺度特征的有效范式,可推广到OOP姿态估计
  2. 可解释AI不再可选——Euro NCAP要求审计能力
  3. 数据增强策略(旋转+亮度+翻转)对疲劳数据不平衡有效

参考来源:

  • Rajalingam et al., Neural Processing Letters 58(2), 2026, DOI: 10.1007/s11063-026-11841-6
  • ConvNeXt: Liu et al., CVPR 2022
  • CaiT: Touvron et al., ICCV 2021
  • Grad-CAM: Selvaraju et al., ICCV 2017