news 2026/10/20 5:54:32

NGSIM轨迹数据驱动的驾驶风格识别与GMM聚类实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
NGSIM轨迹数据驱动的驾驶风格识别与GMM聚类实战

老实讲,驾驶行为分析这个方向我关注了很久,市面上能拿到的公开轨迹数据集本身就不多,而像NGSIM这样带完整车辆轨迹、时间戳和局部坐标的开源数据,几乎算是做驾驶风格研究的“标准练习册”了。最近我正好把一个基于NGSIM的驾驶风格识别小项目跑通了,核心链路就是从原始轨迹里提取纵向和横向的行为特征,再用高斯混合模型自动聚类出不同的驾驶风格群体。这活儿做完以后,我很确信这套流程不只是实验室作业,它对辅助驾驶参数标定、车队安全管理和车险定价都有直接的参考意义。这篇就把整个项目的过程、每个环节的选择逻辑和踩过的坑原原本本写出来,给后续要做类似工作的朋友一条可以照抄的路线。

1. 数据准备与预处理

1.1 NGSIM数据集长什么样

NGSIM(Next Generation Simulation)是由国外某交通研究机构发布的一套自然驾驶轨迹数据,它的最大特点是用路边高架摄像机连续拍摄真实车流,再通过图像识别技术把每一辆车的运动轨迹提取出来。拿到手的数据是典型的CSV表格,每个文件里包含多条车辆轨迹,采样频率是10Hz,也就是说每隔0.1秒记录一次车辆的位置、速度、加速度,以及所在车道、前后车关系等关键信息。这类数据对研究驾驶行为特别友好,因为它是真实道路上的自然驾驶,不是实验室里被试者故意表演出来的,因此有很高的生态效度。

我用的数据是其中的高速公路路段数据,具体是某个工作日下午大约45分钟的路段记录,覆盖一条主线和一个交织区。原始CSV里的核心字段包括车辆ID、帧ID、时间戳、局部坐标X/Y、车速、加速度、所在车道编号、前车ID、后车ID,以及跟车距离。读取之后你会发现数据没有想象中的干净,树荫遮挡、车辆换道时ID切换、偶尔的图像识别抖动都会造成轨迹毛刺,所以在提取特征之前,必须先做清洗和重构。

1.2 轨迹清洗与平滑

拿到原始数据的第一个操作是按车辆ID分组,把每一辆车的行驶过程拼成一条连续轨迹。这个看起来简单,实际坑不少。NGSIM里不是每辆车都会从头到尾出现,很多车在某个帧突然出现又突然消失,那是因为它开进了摄像机视野边缘或者被大车遮挡了。我设了一个清洗规则:单条有效轨迹至少连续出现25秒,否则丢弃。这个阈值不是拍脑袋定的,因为后面要算跟车时距、加速度分布这类统计特征,样本太短计算出来的统计量方差巨大,聚类结果会跟着失真。

轨迹平滑也是必须做的一步。原始数据虽然提供了加速度字段,但那是从位置差分得到的,噪声相当大。我试过直接拿原始加速度算急加速频率,结果出现一秒内的加速度突变值达到每秒几米级别,这明显不符合真实车辆物理特性。后来我用Savitzky-Golay滤波器对速度序列做平滑,窗口取11帧,多项式阶数取3,再用平滑后的速度差分出加速度。这个组合我实际跑下来噪声抑制效果好,对峰值的保留也比简单滑动平均强不少,急加速事件不会被糊掉。

import pandas as pd import numpy as np from scipy.signal import savgol_filter # 按车辆ID和时间戳排序 df = df.sort_values(['Vehicle_ID', 'Frame_ID']).reset_index(drop=True) # 只保留连续轨迹足够长的车辆 valid_ids = df.groupby('Vehicle_ID')['Frame_ID'].apply( lambda x: x.max() - x.min() >= 250 # 0.1s一帧,250帧=25秒 ) df = df[df['Vehicle_ID'].isin(valid_ids[valid_ids].index)] # 对每辆车分别做速度平滑,再重新计算加速度 smoothed_speeds = [] for vid, group in df.groupby('Vehicle_ID'): spd = group['Velocity'].values if len(spd) >= 11: spd_s = savgol_filter(spd, window_length=11, polyorder=3) else: spd_s = spd smoothed_speeds.append(spd_s) df['Velocity_Smooth'] = np.concatenate(smoothed_speeds) df['Acc_Calc'] = np.gradient(df['Velocity_Smooth'], 0.1)

平滑完加速度后,还有一个容易忽略的点:车辆长时间停车状态会带来大量零速度、零加速度的帧。比如车辆在匝道出口排队,这一分钟它压根没有驾驶行为可言,但统计特征会被这些静置帧稀释。我保留这些帧单独算“停车占比”这个指标,而计算加加速度和急加减速频率时则先把速度低于0.5米/秒的帧剔除,保证动态特征集中反映真正的“驾驶动作”。

2. 驾驶风格特征工程

2.1 特征选择思路

驾驶风格这个词听起来抽象,落到数据上其实可以拆成几个维度:纵向激进程度、横向稳定性、跟车风险习惯、对加减速的控制能力。每个维度都要选出有代表性的统计指标,而且指标之间不能高度共线,否则聚类结果会被某一类特征主导。我最初一口气算了二十多个特征,跑完相关性矩阵后发现很多指标冗余,比如平均加速度和加速度标准差相关性超过0.8,保留一个就够了。这一步“做减法”很重要,后续GMM在高维小样本下容易过拟合,特征精简后模型稳定性和可解释性都明显提升。

2.2 纵向驾驶特征

纵向维度是驾驶风格最直观的体现,我最终留下了四个特征:

第一个是正加速度的95分位数。相比平均加速度,分位数能更好地刻画“急”的程度,因为日常巡航状态下平均值几乎都是零点几,根本区分不出风格差异,但95分位数会显著拉开激进驾驶者和保守驾驶者的差距。

第二个是急减速事件频率,定义是加速度小于-3米每秒平方且持续超过0.3秒的次数,归一化到每千秒频次。高速公路上急减速往往发生在车流密集或者前车突然刹车时,习惯贴着前车跑的驾驶员这类事件频率通常更高。

第三个是加加速度的均方根值,也就是jerk的RMS。这个指标衡量的是加速度变化的剧烈程度,数值越大说明驾驶员操作越“突兀”,松踩踏板不够平顺。很多人可能忽略jerk,但我实测下来它对风格聚类的区分度比单纯加速度标准差还要好。

第四个是巡航速度的85分位数。虽然跟风格没有绝对关系,但综合同路段的流量状态后,它仍然能反映驾驶员对速度的偏好倾向。

2.3 横向与跟车特征

横向稳定性选了两个特征:横向速度标准差和每千秒换道次数。NGSIM里有一个字段是车辆中心的横向坐标,我对它做差分得到横向速度,然后算标准差。这个值大说明车辆在车道内频繁轻微摆动,反映的是驾驶员的注意力分配和操控精细度。换道次数可以直接从车道编号的变化统计出来,喜欢频繁变道超车的驾驶员显然和常年守着一条道的不是同一类人。

跟车行为特征中,我觉得最实用的是跟车时距THW(Time Headway)的中位数,THW等于车头间距除以当前车速,物理含义是“如果前车突然刹停,我多久会撞上去”。这个指标能体现驾驶员对安全距离的耐受程度,THW常年小于1秒的驾驶员风格激进程度基本不会低。顺便提一句,THW只在前方有车时才有定义,没有前车自由行驶的片段要单独剔除,否则会把无穷大值混进来。

到这里,我整理了一张表记录每个特征的计算方式和对应场景,方便后面聚类结果不理想时回溯问题:

特征名计算方式代表的风格维度
正加速度95分位数加速度序列>0部分的分位数急加速倾向
急减速事件频率加速度<-3m/s²且持续0.3s以上事件的千秒频次紧急制动习惯
加加速度RMS加速度差分的均方根操作平顺性
巡航速度85分位数速度序列的85分位数速度偏好
横向速度标准差横向坐标差分后的标准差车道内稳定性
千秒换道次数车道编号变化次数/时长*1000变道激进程度
THW中位数车头间距/车速,取中位数跟车安全距离习惯

不同特征之间的量纲差异很大,有的零点几,有的几百,直接丢进聚类模型会让数值大的特征主导距离计算,所以聚类前必须做标准化。这一步千万别省,很多初学者拿到数据直接GMM,结果聚类结果是按速度尺度切开的,而不是按行为风格切开的,问题就出在没有标准化。

3. 高斯混合聚类实现

3.1 为什么选高斯混合模型

驾驶风格聚类这件事,K-Means也能做,但有一个本质区别:K-Means是硬划分,每个样本要么属于A类要么属于B类,且它假设簇是球形且大小相近。而现实中的驾驶风格存在大量“中间派”,比如一个驾驶员可能在高速上开得很激进,但到了交织区又变得极其保守,你很难非黑即白地把他划进某一类。高斯混合模型(GMM)是软聚类,它假设所有样本来自若干个高斯分布的叠加,每个样本属于每个类是一个概率值,而不是0或1。

具体原理上,GMM用多个二维或高维高斯分布来拟合数据分布,每个高斯分布代表一个潜在风格簇。模型训练通过EM算法迭代:E步根据当前参数计算每个样本属于每个高斯的概率(责任度),M步用这些责任度加权更新每个高斯的均值、协方差和权重。迭代到对数似然收敛,就得到了一个概率化的聚类结果。这种软划分对驾驶风格这个场景特别合适,因为驾驶员风格本来就是一个连续谱,硬贴标签反而失真。

3.2 特征标准化与分量数选择

训练GMM之前,我先把特征做了标准化,用的StandardScaler,保证每个特征方差为1均值0。这一步让我在实验对比中明显看到聚类轮廓系数上升,因为消除了量纲干扰。

接下来是选K,也就是高斯分量个数。GMM不会自己决定分成几类,需要我们先给一个K。业界最常见的做法是画BIC曲线,BIC(贝叶斯信息准则)在似然值的基础上加了一个对参数数量的惩罚项,避免模型为了拟合数据无限增加分量。公式是BIC等于负二倍对数似然加上参数数量乘以样本数量的自然对数。我遍历K从1到6,跑每个模型的BIC,选BIC值最小的K。

有一点要单独提醒:BIC最小值有时候不是那么尖锐,K=3和K=4的BIC可能差很小。这时候可以看一下每个簇的样本占比,如果某个簇占比不足5%,说明K选大了,那个簇大概率是从一个连续分布里硬切出来的细碎部分,业务上没有独立存在的意义。我最后选了K=3,三类风格分别对应保守型、普通型和激进型,每一类占比都超过了20%,解释起来也很顺。

3.3 聚类代码与训练过程

代码用scikit-learn的GaussianMixture就能完成,不需要自己实现EM。但有几个参数值得手动调一下:n_init建议设10以上,因为EM算法对初始值敏感,多跑几次取最优的才稳定;covariance_type我试过full和diag两种,full对特征间相关性建模更完整,但参数数量随特征数平方增长,在样本量只有几万的时候容易过拟合,diag模式参数量更少,跑出来的结果在轮廓系数上差不太多。我最后用了full,因为能捕捉到“激进驾驶往往伴随频繁换道”这种特征相关性。

from sklearn.preprocessing import StandardScaler from sklearn.mixture import GaussianMixture from sklearn.metrics import silhouette_score import numpy as np # feature_matrix是车辆级别的特征矩阵 scaler = StandardScaler() X_scaled = scaler.fit_transform(feature_matrix) # 遍历K值,记录BIC和轮廓系数 bic_scores = [] sil_scores = [] K_range = range(1, 7) for k in K_range: gmm = GaussianMixture(n_components=k, covariance_type='full', n_init=10, random_state=42) gmm.fit(X_scaled) bic_scores.append(gmm.bic(X_scaled)) if k >= 2: labels = gmm.predict(X_scaled) sil_scores.append(silhouette_score(X_scaled, labels)) # 选K=3重新训练,保存预测概率 best_k = 3 gmm_final = GaussianMixture(n_components=best_k, covariance_type='full', n_init=10, random_state=42) gmm_final.fit(X_scaled) prob = gmm_final.predict_proba(X_scaled) labels = gmm_final.predict(X_scaled)

跑完K=3的模型后,我顺手看了每个样本的最大责任度概率。大部分样本的归属概率都在0.8以上,说明三类之间边界还算清晰。但也有大概15%的样本最大归属概率只有0.5左右,这些就是前面说的中间派驾驶员,他们的特征在两个簇之间摇摆。这恰好是GMM相比K-Means的优势,我可以用这个概率值做“风格置信度”的输出,而不是直接硬着嘴说他就属于某一类。

4. 聚类结果分析与场景落地

4.1 三类风格画像

聚类完成后,我把三个簇的特征均值做了对比,结果非常符合直觉。保守型驾驶员的正加速度95分位数最低,千秒换道次数很少,THW中位数明显偏大,说明他们跟前车的距离余量给得很足,基本不怎么急加速也不乱变道。激进型驾驶员几乎每项指标都相反,急减速事件频繁,加加速度RMS高出保守型一截,THW经常压到1秒以内。普通型居中,但有趣的是普通型在横向速度标准差上和保守型比较接近,差异主要集中在纵向维度的加速度分布上,这说明大多数驾驶员在方向盘操作上差别没那么大,真正区分风格的主要在油门和刹车脚法。

我还画了每个特征在三个簇之间的箱线图,发现不同簇的重叠区主要集中在中间带。也就是说,单纯一个特征很难把三类完全分开,必须用多维特征组合才能稳定区分。这解释了为什么这个项目值得用聚类来做,而不是靠单一指标设阈值一刀切。

4.2 聚类稳定性验证

聚类模型最怕的是结果只是对当前这批数据的偶然拟合。我做了两轮稳定性验证。第一轮是把样本随机打乱后按7比3切训练集和测试集,单独对训练集重新拟合GMM,再把测试集映射到训练好的模型上,对比两种方式下测试集样本的标签一致性。我算出的调整兰德指数(ARI)大约是0.83,说明标签跨批次稳定性可以接受。

第二轮是减少特征数量,看看聚类结果是否依赖于特定特征。我发现去掉横向速度标准差以后,大多数样本的标签保持不变,但去掉THW中位数以后,保守型和普通型之间的边界明显漂移。这提示THW是区分这两类的关键特征,后续如果采集数据时跟车距离字段缺失,需要谨慎对待模型输出。

4.3 结果能用在哪些场景

这个聚类模型做完以后可不是躺在笔记本里的。我至少想到了三个落地方向。第一个是车队管理,通过车载终端上传轨迹数据,每天自动更新每个司机的风格标签,把连续N天被标为激进型的司机拉出来做专项培训,比单纯靠事故记录去做事后管理要前置得多。

第二个是辅助驾驶参数标定,量产车上自适应巡航的跟车距离和舒适加减速阈值,完全可以依据驾驶风格动态调整,激进型用户给一个较短的默认跟车时距,保守型用户给一个更保守的设定,这样能提升系统对不同用户的适配度。

第三个是保险定价,尤其是按里程付费的险种,驾驶风格特征可以直接作为风险分层的输入。同样的行驶里程,急加速频繁和THW过短的用户未来出险概率理论上更高,保费里应该体现这个差异。当然这个方向涉及数据合规和个人隐私保护,能不能商用要打一个问号,但技术链条是通的。

5. 常见问题与排查经验

这部分是我实际跑数据过程中遇到的典型问题,每一条都是拿时间换来的教训。

5.1 拿不到理想的聚类效果

如果你跑出来的GMM聚类轮廓系数很低,比如连0.3都不到,先别急着换算法。我遇到过的第一个原因是特征里混入了异常值,尤其是急减速频率这个特征,某辆车因为数据抖动被算出了几十次根本不存在的急减速,直接拉坏了整个特征分布。解决办法是先对每个特征做箱线图检查,把超过四分位距三倍以上的样本单独审查,确认不是真实行为再剔除。

第二个原因是时间窗口问题。NGSIM数据只有几十分钟,不代表每辆车都有机会展现完整的风格。有的车在数据里只有5分钟,还全堵在路上,平均速度低得可怜。这类样本的瞬时特征根本不能代表驾驶员的真实习惯。后来我加了一条硬性规则:参与聚类分析的车辆轨迹时长必须在5分钟以上,否则只保留给后续出行特征分析,不进聚类模型。

5.2 分量数选择纠结

刚才提到BIC有时候对K不敏感,这时候别只盯着数值。一个实用的做法是看每个簇的责任度概率分布:把每个样本在那个簇里的最大概率画出来,如果所有簇的最大概率都集中在0.4到0.6,说明这个K值下的簇划分很模糊,样本在两个簇之间来回漂,这时候要么降K,要么看一下是不是特征空间本身没有明显的多峰结构。我刚开始一度想强行分4类,结果第四个簇的责任度概率最高也就0.5,最后还是回归到3类,更干净也更好解释。

5.3 特征分布偏斜严重

特征工程里有一类特征,比如急加速事件频率,它的分布是高度右偏的,大部分司机事件数很少,极少数司机事件数很多。如果不做任何处理直接标准化,大多数样本会被压向负值区间,离群点却甩得很远,模型会把离群点单独切出一类来。后来我做了Box-Cox变换把分布往正态方向拉了一下,再去标准化,聚类结果才回归正常。碰到类似计数类特征,建议都先做分布变换,不要直接放进高斯模型,毕竟GMM的核心假设就是每个簇服从高斯分布,输入特征也得大致满足这个前提。

5.4 计算性能问题

NGSIM单段数据只有几十万行,跑起来其实很快,但要处理多段甚至跨多个文件的数据时,逐帧Python循环会成为性能瓶颈。我一开始用for循环逐行算跟车时距,跑一个下午才处理完一段路,后来改成向量化的pandas操作,同样的任务十几秒就完成了。如果是更大规模的数据集,建议用polars或者干脆把特征聚合写成SQL查询算子,效率差距是数量级的。

还有一点,GaussianMixture在训练时会把样本数乘以分量数再乘以迭代次数,如果样本量上了百万,内存占用会变得可观。这种场景可以先用MiniBatchKMeans做一次粗聚类得到中心点,再用这些中心点初始化GMM的均值参数,收敛速度和稳定性都会好不少,我后面的多路段实验中就是这么干的。

整个项目跑下来,我最深的感受是:驾驶风格分析最核心的功夫不在模型本身,而在特征定义和数据清洗。你把每一个物理量定义清楚,把每一条轨迹的异常清洗干净,GMM只是一个水到渠成的收尾工具。那个把NGSIM轨迹换算成THW和jerk分布的过程,才是整个项目里最花时间、也最值得反复打磨的地方。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/20 5:54:17

P2P_SDK集成指南:NAT穿透与音视频传输避坑实践

简介&#xff1a;这份TUTK IoTC官方SDK压缩包为开发者在Android、iOS、Windows及macOS平台快速构建摄像头P2P监控系统提供完整工具链&#xff0c;面向物联网或音视频领域的工程人员。压缩包内共1187个文件&#xff0c;整体约56.15MB&#xff0c;其中既包含Java、C源码及头文件&…

作者头像 李华
网站建设 2026/10/20 5:54:17

从零搭建可复用的演示文稿自动化生成方案:内容与样式分离实战

1. 从零搭建一套可复用的演示文稿自动化生成方案做过技术分享、项目汇报或者课程培训的人都有一个共同的痛点&#xff1a;每次做演示文稿&#xff0c;内容其实早就想清楚了&#xff0c;但真正花时间的反而是排版、配色、对齐、字体统一这些重复劳动。一套演示文稿从构思到最终交…

作者头像 李华
网站建设 2026/10/20 5:54:16

diagram-design:从信息结构到可视化表达的方法论

1. 从一个空输入说起&#xff1a;为什么"diagram-design"值得单独拎出来聊拿到"diagram-design"这个标题的时候&#xff0c;项目正文、关键词、摘要描述全是空的。这种情况其实挺常见——很多时候一个项目或者一个想法&#xff0c;最初就只有一个名字&…

作者头像 李华
网站建设 2026/10/20 5:52:42

conda环境复制与迁移实战:从clone到conda-pack的完整指南

先说一个我几乎每周都会看到的场景&#xff1a;项目做了一半&#xff0c;conda create出一个新环境装了十几二十个包&#xff0c;跑通之后想复制一份给另一个项目用&#xff0c;于是直接conda create --clone&#xff0c;或者索性把整个conda envs/xxx目录拷到服务器上。结果往…

作者头像 李华
网站建设 2026/10/20 5:52:21

OpenCode装oh-my-opencode:AI一键辅助插件配置管理实战

如果你最近也入了 OpenCode 的坑&#xff0c;那你多半会跟我一样&#xff0c;在某天深夜开始搜另一件事&#xff1a;给 OpenCode 装 oh-my-opencode。这插件说白了就是给 OpenCode 配一套统一的插件、主题、别名和命令模板管理框架&#xff0c;和很多工具圈里常见的 “oh-my-”…

作者头像 李华
网站建设 2026/10/20 5:52:20

用Docker部署自动化测试环境:告别环境漂移,一键可复现

“我本地跑得好好的&#xff0c;线上怎么崩了&#xff1f;”这句话我在自动化测试这行听了快十年&#xff0c;自己也说过不少次。多数情况下&#xff0c;问题根本不在代码逻辑&#xff0c;而在运行环境——本地装了某个版本的浏览器驱动&#xff0c;本地数据库里有几条测试数据…

作者头像 李华