目录
前言
一、项目背景与问题定义
1.1 为什么需要基于振动的路面识别?
1.2 核心挑战
二、数据理解与预处理
2.1 数据结构
2.2 标签解析
2.3 全局标准化
2.4 RMS包络特征
2.5 滑窗采样
2.6 防止数据泄露(关键设计)
三、物理合法的数据增强
四、CNN-LSTM模型架构
4.1 整体结构
4.2 Channel Attention机制
4.3 模型参数量
五、训练策略与超参数
5.1 类别权重放大(解决不平衡)
5.2 学习率调度器
5.3 早停机制(监控验证损失)
5.4 完整超参数
六、实验结果与分析
6.1 混淆矩阵
6.2 分类报告
6.3 综合指标
6.4 训练曲线
七、阈值调优
八、继续改进建议
8.1 频域特征增强
8.2 损失函数升级
8.3 数据质量控制
8.4 多轮交叉验证
8.5 多传感器融合
8.6 模型轻量化
九、部署建议
9.1 ONNX导出
9.2 车载芯片适配
十、总结
前言
在智能驾驶系统中,路面类型识别(沥青 vs 湿滑)是车辆稳定性控制的前置关键环节。传统方法依赖视觉传感器,但在雨雾、夜间等恶劣条件下性能大打折扣。本文介绍的基于车轮三轴加速度传感器的路面识别系统,通过分析轮胎与路面摩擦产生的振动信号,实现全天候、高精度的路面分类。
核心成果:在真实采集数据上达到97.64%准确率、0.9929 AUC,湿滑路面召回率89.80%,满足工业级部署要求。
一、项目背景与问题定义
1.1 为什么需要基于振动的路面识别?
| 传感器 | 优势 | 劣势 |
|---|---|---|
| 摄像头(视觉) | 直观、分辨率高 | 受光照、雨雾影响大 |
| 激光雷达 | 精度高 | 成本高、雨雾衰减严重 |
| 车轮加速度(IMU) | 全天候、不受光照影响 | 信号复杂,需要算法提取特征 |
1.2 核心挑战
数据严重不平衡:湿滑路面数据量远少于沥青路面
多源噪声干扰:车身振动、发动机噪声、路面起伏叠加
数据泄露风险:同一采集片段相邻窗口高度相关
工业级约束:模型需满足高召回率(安全优先)、低延迟、小体积
二、数据理解与预处理
2.1 数据结构
原始数据存储在Excel文件中,每个Sheet命名格式为{路面类型}-{车轮编号}{采集次数}。
| Sheet名称 | 含义 |
|---|---|
沥青-11 | 沥青路面、左前轮、第1次采集 |
湿滑-32 | 湿滑路面、右后轮、第2次采集 |
车轮编号:1=左前,2=右前,3=左后,4=右后
每个Sheet包含三列加速度数据x,y,z,采样频率1800Hz。
2.2 标签解析
def parse_sheet_info(sheet_name: str): parts = sheet_name.strip().split('-') surface = parts[0] # '沥青' 或 '湿滑' code = parts[1] # 如 '32' wheel = int(code[0]) # 车轮编号 trial = int(code[1]) # 采集次数 return surface, wheel, trial2.3 全局标准化
采用两阶段加载策略:先收集所有Sheet的数据计算全局均值和标准差,再统一标准化。这确保了不同Sheet、不同车轮间的数据可比性。
# 第一遍:收集全局统计量 all_concat = np.concatenate(all_raw_data, axis=0) global_mean = all_concat.mean(axis=0) global_std = all_concat.std(axis=0) # 第二遍:应用标准化 norm_xyz = (raw_xyz - global_mean) / (global_std + 1e-8)2.4 RMS包络特征
RMS(均方根)包络反映振动信号的能量变化趋势,能有效表征路面粗糙度差异。湿滑路面通常表现为能量衰减更快、波动更平滑。
def compute_rms_envelope(data, window=50): """滑动RMS计算,反映振动能量包络""" rms = np.zeros_like(data) half = window // 2 for i in range(len(data)): start = max(0, i - half) end = min(len(data), i + half) rms[i] = np.sqrt(np.mean(data[start:end]**2)) return rms通过添加RMS包络,输入特征从3通道 → 6通道(x,y,z + RMS_x, RMS_y, RMS_z)。
2.5 滑窗采样
for i in range(0, len(feature_matrix) - WINDOW_SIZE, STEP_SIZE): window_data = feature_matrix[i:i + WINDOW_SIZE] all_windows.append(window_data) all_labels.append(surface) all_groups.append(sheet)| 参数 | 值 | 说明 |
|---|---|---|
| WINDOW_SIZE | 600 | ~0.33秒(1800Hz下) |
| STEP_SIZE | 250 | 50%重叠率 |
2.6 防止数据泄露(关键设计)
同一个Sheet(同一车轮/同一采集批次)的所有窗口,必须全部落在训练集或全部落在测试集。
from sklearn.model_selection import GroupShuffleSplit gss = GroupShuffleSplit(n_splits=1, test_size=0.2, random_state=42) train_idx, test_idx = next(gss.split(X, y_encoded, groups=groups)) # 额外保障:确保测试集包含两类样本三、物理合法的数据增强
在振动信号中,并非所有增强方式都物理可解释:
| 增强方式 | 物理含义 | 是否采用 |
|---|---|---|
| 幅值缩放 | 模拟传感器灵敏度差异 | ✅ |
| 高斯噪声 | 模拟电气噪声 | ✅ |
| 循环移位 | 破坏时序因果性 | ❌ |
| 时间反转 | 改变振动传播方向 | ❌ |
def augment_vibration_window(window_xyz, noise_std=0.03, scale_range=(0.9, 1.1)): aug = window_xyz.copy() scale = np.random.uniform(*scale_range) aug *= scale noise = np.random.normal(0, noise_std, size=window_xyz.shape) aug += noise return aug关键设置:SAMPLE_THRESHOLD = 0强制所有类别参与增强。
四、CNN-LSTM模型架构
4.1 整体结构
输入: 600 × 6 (窗口长度 × 特征通道) ↓ Conv1D(6→96, k=15) + BN + ReLU + MaxPool(3) + Dropout(0.2) ↓ Conv1D(96→160, k=7) + BN + ReLU + MaxPool(2) + Dropout(0.25) ↓ Conv1D(160→160, k=3) + BN + ReLU + MaxPool(2) + Dropout(0.3) ↓ Channel Attention (160维) ← 自动学习通道重要性 ↓ Bi-LSTM (hidden=128, bidirectional) ↓ LSTM (hidden=96, unidirectional) ↓ 分类器: Linear(96→128) + BN + ReLU + Dropout(0.4) + Linear(128→2)4.2 Channel Attention机制
class ChannelAttention(nn.Module): def __init__(self, channels, reduction=8): super().__init__() self.fc = nn.Sequential( nn.AdaptiveAvgPool1d(1), nn.Flatten(), nn.Linear(channels, max(channels // reduction, 4)), nn.ReLU(), nn.Linear(max(channels // reduction, 4), channels), nn.Sigmoid() ) def forward(self, x): weight = self.fc(x).unsqueeze(-1) return x * weight # 通道级注意力加权4.3 模型参数量
| 模块 | 参数量 |
|---|---|
| Conv Blocks | ≈ 200K |
| Channel Attention | ≈ 1.5K |
| LSTM Layers | ≈ 500K |
| Classifier | ≈ 75K |
| 总计 | ≈ 776K |
五、训练策略与超参数
5.1 类别权重放大(解决不平衡)
class_weights_raw = compute_class_weight("balanced", classes=np.unique(y_train_raw), y=y_train_raw) class_weights_raw[1] = class_weights_raw[1] * 2.5 # 湿滑权重放大2.5倍5.2 学习率调度器
scheduler = ReduceLROnPlateau(optimizer, mode='min', factor=0.5, patience=10, min_lr=1e-6) scheduler.step(avg_val_loss)5.3 早停机制(监控验证损失)
监控验证损失而非验证准确率——后者容易被多数类(沥青)虚高。
early_stopper = EarlyStopping(patience=25, mode='min') should_stop = early_stopper(avg_val_loss, model)5.4 完整超参数
| 参数 | 值 |
|---|---|
| 优化器 | AdamW |
| 学习率 | 1e-3 |
| 权重衰减 | 1e-4 |
| 批大小 | 96 |
| 最大Epoch | 150 |
| 早停耐心值 | 25 |
六、实验结果与分析
6.1 混淆矩阵
| 真实\预测 | 预测: 沥青 | 预测: 湿滑 |
|---|---|---|
| 真实: 沥青 | 866 | 17 |
| 真实: 湿滑 | 5 | 44 |
解读:
湿滑漏报仅5个(召回率 89.8%)
沥青误报仅17个(误报率 1.9%)
整体准确率97.64%
6.2 分类报告
| 指标 | 沥青 | 湿滑 |
|---|---|---|
| Precision | 0.9943 | 0.7213 |
| Recall | 0.9807 | 0.8980 |
| F1-Score | 0.9875 | 0.8000 |
6.3 综合指标
| 指标 | 数值 |
|---|---|
| 整体准确率 | 97.64% |
| ROC-AUC | 0.9929 |
| 湿滑召回率 | 89.80% |
| 湿滑精确率 | 72.13% |
| 湿滑F1 | 0.8000 |
6.4 训练曲线
从图中可以看出:
训练损失从0.77快速收敛至0.10以下
验证损失稳定在0.10~0.15区间(验证集规模小,波动正常)
训练准确率与验证准确率均稳定在98%左右
训练与验证曲线高度吻合,没有过拟合迹象
七、阈值调优
threshold = 0.6 preds = (probs[:, 1] > threshold).long()| 阈值 | 召回率 | 精确率 | F1 |
|---|---|---|---|
| 0.5 | 100% | 50.0% | 0.67 |
| 0.6 | 89.8% | 72.1% | 0.80 |
| 0.7 | 80.0% | 80.0% | 0.80 |
推荐阈值 0.6:在安全(召回率)与用户体验(精确率)间取得最佳平衡。
八、继续改进建议
虽然当前模型表现优秀,但以下方向可进一步提升:
8.1 频域特征增强
当前模型仅在时域(RMS包络)做特征增强。建议在数据加载阶段添加频域特征:
from scipy.fft import rfft def add_spectral_features(norm_xyz, n_fft=128): """添加FFT幅度谱作为额外通道""" fft_features = [] for axis in range(3): spec = np.abs(rfft(norm_xyz[:, axis], n=n_fft)) spec = np.log1p(spec) # 对数压缩 fft_features.append(spec) return np.concatenate(fft_features, axis=1)预期效果:沥青和湿滑在频域的能量分布差异更显著,可进一步提升精确率。
8.2 损失函数升级
尝试Focal Loss替代加权交叉熵,强制模型关注被错误分类的湿滑样本:
class FocalLoss(nn.Module): def __init__(self, alpha=0.25, gamma=2.0): super().__init__() self.alpha = alpha self.gamma = gamma def forward(self, inputs, targets): ce_loss = nn.CrossEntropyLoss(reduction='none')(inputs, targets) pt = torch.exp(-ce_loss) return (self.alpha * (1 - pt) ** self.gamma * ce_loss).mean()8.3 数据质量控制
问题:您手动跳过了湿滑-41(疑似标签错误),但可能还有其他样本存在标注噪声。
解决方案:对每个Sheet做频谱可视化,剔除明显异常波形的样本,或使用主动学习策略进行人工复核。
8.4 多轮交叉验证
当前使用单次随机划分(random_state=42),建议使用5折交叉验证评估模型稳定性:
from sklearn.model_selection import StratifiedKFold skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)8.5 多传感器融合
将车轮加速度信号与车辆CAN总线信号(车速、方向盘转角、制动压力)融合:
# 扩展输入特征 can_features = pd.DataFrame([speed, steering_angle, brake_pressure]).T feature_matrix = np.concatenate([norm_xyz, rms_features, can_features], axis=1)8.6 模型轻量化
当前模型参数量约77万,如需部署到低成本MCU,可尝试:
通道剪枝:移除Conv层中不重要的通道
知识蒸馏:用当前模型作为教师网络,训练一个更小的学生模型(如纯CNN)
量化训练:训练时模拟INT8量化,部署后加速3~4倍
九、部署建议
9.1 ONNX导出
dummy_input = torch.randn(1, 600, 6).to(DEVICE) torch.onnx.export( model, dummy_input, "road_classifier.onnx", opset_version=13, input_names=['input'], output_names=['output'], dynamic_axes={'input': {0: 'batch_size'}, 'output': {0: 'batch_size'}} )9.2 车载芯片适配
| 芯片平台 | 部署方式 |
|---|---|
| 地平线J3/J5 | ONNX → 地平线模型转换工具 |
| TI TDA4 | ONNX → ONNXRuntime |
| 高通SA8155 | ONNX → TensorFlow Lite |
十、总结
本文详细介绍了一个可直接部署的工业级路面识别系统:
| 维度 | 成果 |
|---|---|
| 准确率 | 97.64% |
| AUC | 0.9929 |
| 湿滑召回率 | 89.80% |
| 湿滑F1 | 0.8000 |
| 模型大小 | ≈ 776K 参数 |
核心技术要点:
全局标准化 + RMS包络特征
物理合法的数据增强
CNN-LSTM + Channel Attention架构
类别权重放大 + 早停监控验证损失
阈值调优(0.6)
关于"欠拟合"的问题:根据训练曲线(损失稳定在0.1以下、准确率98%),模型不存在欠拟合。验证损失波动源于验证集规模较小(仅49个湿滑样本),属正常现象,不影响最终分类性能。如需进一步提升,建议按照第八节的改进建议操作。
📌声明:本文数据来源于江淮试验场实际采集,已做脱敏处理。代码可依需开源,欢迎交流探讨。