简介一套面向机器学习与智能交通方向学习者的异常驾驶检测项目聚焦驾驶行为中的异常模式识别提供可运行的源码与说明书便于按需二次修改。压缩包内共有六个文件以三个交互式编程笔记为主配合两个网页可视化文件与一份说明文档整体大小约九点五兆结构紧凑可直接对照代码、可视化页面与说明进行学习。目前已有一百人学习下载适合具备一定编程与机器学习基础、希望结合真实场景完成异常检测实验的读者。内容涵盖异常检测算法实现、速度进度分析等模块可帮助读者理解从数据导入、特征构造到模型训练与结果展示的完整流程说明书给出环境配置与运行指引降低复现门槛项目允许自行调整模型与参数适用于课程设计、毕业设计或企业预研中的驾驶数据安全分析场景。1. 基于机器学习的异常驾驶检测源码、说明书与可修改的完整流程如果你手里有一段车载 OBD 采集的速度和加速度日志想从里面把急刹车、急转弯、频繁变道这些异常驾驶片段自动捞出来这份基于机器学习的异常驾驶检测源码包就能直接派上用场。它不是一个只贴了模型的空壳 demo而是一套能跑、能改、有说明书README的完整机器学习项目压缩包里有异常检测主流程、速度曲线分析、AutoPlait 时间序列模式发现三个 notebook外加 HTML 预览和说明文档。对正在做课程设计的本科生、刚入门时序异常检测的工程师以及想给车队做行为评分的从业者都适用。可自己修改意味着窗口大小、特征、模型参数都留了改口值得下载后照着复现一遍。2. 拆包对齐README、三个 notebook 与数据字段约定2.1 压缩包里到底能跑什么打开压缩包后先别急着点 Run All把文件角色分清楚再动手。AnomalyDetection.ipynb 是异常检测主流程从读数据、切窗口、构造特征到训练模型和标记异常时间点都在里面闭环SpeedProgress.ipynb 负责速度-时间曲线的绘制与预处理通常用来确认数据质量比如看整段速度曲线上有没有明显尖刺或长时间丢点AutoPlait.ipynb 是另一个完整算法做时间序列自动分段与模式发现适合进阶时拿来和主流程做交叉验证。两个 html 文件是 notebook 的静态导出预览不需要装 Python 就能直接看代码和图表适合在下载后快速判断这套源码的工作方式再决定要不要配环境。README.md 就是标题里说的说明书。环境要求、目录结构、参数入口一般都会写明我拿到这类包的习惯是先打开 README再看 html 预览最后才动 notebook这样能少走很多弯路。可自己修改的入口也通常写在 README 里数据路径、字段名、模型参数改的时候顺着这些标记往下找即可。下表把这六个文件的角色列清楚。文件作用建议动作AnomalyDetection.ipynb主流程特征构建、模型训练、异常标记先完整跑一遍再动参数AnomalyDetection.html主流程的静态预览不装环境也能看结论SpeedProgress.ipynb速度曲线分析与数据清洗核对数据质量时运行SpeedProgress.html速度分析的静态预览先看曲线长什么样AutoPlait.ipynb时间序列自动分段、模式发现进阶做交叉验证README.md说明书环境、目录、参数约定最先读这个2.2 数据形态与字段映射这类项目需要的核心字段是时间戳、速度、加速度有转向角或横摆角速度更好。异常驾驶检测本质上是跑「时序特征 无监督模型」输入只要满足一行一个时间点的长表格式就能用。实际数据来源可能是 OBD 盒子、手机 GPS 加加速度计也可能是 CAN 总线导出的 CSV字段命名往往各不相同车速可能叫 speed、VehicleSpeed、gps_speed加速度可能叫 acc、long_acc、X_acceleration。常见做法是先做一层字段映射把这些名字统一成内部列名后续代码只认内部列名换数据源时不用改模型代码。拿到一个不熟悉的 CSV 时我一般会先用下面这段脚本扫一眼 notebook 结构和数据列名确认字段对得上再往下走import json from pathlib import Path def scan_ipynb(path): 打印 notebook 每个 cell 的类型与代码首行快速定位主流程 with Path(path).open(r, encodingutf-8) as f: nb json.load(f) print(f{path}: {len(nb[cells])} cells) for i, cell in enumerate(nb[cells]): src .join(cell[source]) if cell[cell_type] code and src.strip(): print(f[{i:02d}] code | {src.splitlines()[0]}) scan_ipynb(AnomalyDetection.ipynb)这段代码的作用是解析 ipynb 的 JSON 结构把每个单元格的类型和首行代码打印出来。参数 path 是 notebook 文件路径返回结果里 code 单元格的首行通常就是 import 或函数定义能帮你快速判断这个 notebook 先做了什么、后做了什么不用一屏一屏往下翻代码。数据字段确认没问题之后如果手上暂时没有真实驾驶数据也可以用合成数据先把流程跑通。下面这段代码生成 1200 行、1Hz 采样的样例日志并在第 200 到 205 行、第 700 到 703 行人为注入加速和减速突变模拟急加速与急减速片段import pandas as pd import numpy as np np.random.seed(42) n 1200 ts pd.date_range(2024-01-01, periodsn, freq1s) speed np.clip(np.random.normal(60, 8, n), 0, 110) speed[200:205] 45 speed[700:703] - 55 acc np.gradient(speed) sig pd.DataFrame({ ts: ts, speed_kmh: speed, acc_ms2: acc }) sig.to_csv(demo_driving.csv, indexFalse)参数说明n 是采样点数freq1s 表示每行相隔 1 秒实际项目里 OBD 数据常见是 1Hz 到 10Hz频率越高窗口能切得越短。speed 加了 clip 保证速度非负且不超过 110np.gradient 用来计算相邻点的变化率作为加速度。这份合成数据能验证全流程但它的异常模式比较刻意真实数据里的噪声和渐变会比它复杂得多。2.3 运行环境与依赖版本环境配置是下载这种源码包后第一个容易卡住的地方。项目主体是 Jupyter notebook推荐直接用 Anaconda 建一个 Python 3 环境依赖集中在 pandas、numpy、matplotlib、scikit-learn 这几个包。运行前先检查一遍版本避免新老版本 API 差异带来的怪问题import sys import sklearn, pandas, numpy print(sys.version) print(sklearn, sklearn.__version__) print(pandas, pandas.__version__) print(numpy, numpy.__version__)这段代码打印当前解释器和三个核心库的版本号。参数上没有需要调整的东西主要是确认环境存在。注意如果用的是 sklearn 1.xIsolationForest 的参数语义和 predict 返回的 1/-1 含义都比较稳定如果你还在老环境里先确认版本再谈调参不然有些报错会让排查绕一大圈。3. 从速度序列到异常分数特征构建与模型选型怎么做3.1 滑动窗口与几个接地气的特征先想清楚一个问题为什么不能拿每个采样点直接进模型因为单个时间点的瞬时速度噪声太大一个 GPS 跳变就能让数值瞬间偏离正常范围模型会把它当异常可它实际上只是传感器毛刺。急刹车、急变道这类行为持续至少 1 到 3 秒所以正确做法是切滑动窗口把一小段时间内的统计量当作一个样本。窗口太小压不住噪声窗口太大又会把一次完整的急刹动作磨平常见做法是取 3 到 5 秒再让窗口之间有一半重叠。窗口内我一般构造四个特征均值反映该时段的平均车速标准差反映速度波动最大差分反映瞬时突变强度急动度jerk反映加速度的变化率。急动度是急刹车场景里比较好用的特征因为急刹时加速度变化往往非常突兀这在差分二次项上会留下明显的峰值def build_window_features(df, value_col, window5, step1): import numpy as np records [] vals df[value_col].to_numpy() ts df[ts].to_numpy() for start in range(0, len(vals) - window 1, step): seg vals[start: start window] d1 np.abs(np.diff(seg)) d2 np.abs(np.diff(seg, 2)) if len(seg) 3 else np.array([0.0]) records.append({ ts_start: ts[start], ts_end: ts[start window - 1], mean: float(np.mean(seg)), std: float(np.std(seg)), max_delta: float(np.max(d1)) if len(d1) else 0.0, jerk_mean: float(np.mean(d2)) if len(d2) else 0.0, }) return pd.DataFrame(records)参数说明value_col 指定要切窗口的列比如 speed_kmhwindow 是窗口长度按采样点数算1Hz 数据里 window5 就是 5 秒step 是滑动步长step 越小重叠越多异常边界越平滑但计算量也越大。这段函数的要点是同时回传 ts_start 和 ts_end因为后续把异常点还原成异常时间段时需要知道每个窗口对应的真实时间范围。3.2 模型选型为什么先上隔离森林而不是深度学习异常驾驶检测里异常的定义本身是模糊的你通常只知道正常驾驶大概长什么样不知道异常具体有多少种所以首选无监督方案。常见的无监督模型有几个备选孤立森林、局部异常因子LOF、一类支持向量机One-Class SVM以及基于重构误差的 AutoEncoder。这四类我都试过简单场景下首先推荐孤立森林。模型适合场景短板备注Isolation Forest中低维表格特征、数据量大对高维相关性不敏感快、参数少、默认值可用LOF局部密度异常对 k 邻域参数敏感需要调邻域大小One-Class SVM正常数据很干净数据量大时训练慢核函数选型有玄学AutoEncoder高维特征、时序依赖明显样本少容易过拟合属于深度学习路线选型理由其实很直接孤立森林通过随机切分把异常点更快地孤立出来计算复杂度接近线性几万行特征数据几秒就能跑完LOF 需要定义邻域参数不好拍One-Class SVM 在数据量上来之后训练耗时明显增加AutoEncoder 是深度学习模型效果上限高但调网络结构、调 epochs 的成本也高课程设计和入门场景里性价比不如树模型。所以 AnomalyDetection.ipynb 这种以机器学习为主的项目主模型放在隔离森林上是合理的。3.3 训练主模型与首跑输出特征表构造好之后先把特征做标准化再进模型。隔离森林依赖随机切分路径长度理论上不强制要求标准化但标准化后分数分布更均匀后面的分位数阈值也更好解释。训练时固定 random_state 是必须养成的习惯否则每次跑出来的异常点集合都会变后续评估就无从谈起from sklearn.ensemble import IsolationForest from sklearn.preprocessing import StandardScaler feature_cols [mean, std, max_delta, jerk_mean] X df_feat[feature_cols].values X_scaled StandardScaler().fit_transform(X) model IsolationForest( n_estimators200, max_samples256, contamination0.03, random_state42 ) model.fit(X_scaled)参数说明n_estimators 是树的数量默认 100200 能让结果更稳定max_samples 是每棵树实际采样的样本数256 在几万行的特征表上足够用太小会丢失正常模式太大容易让单棵树记住噪声contamination 是异常比例的估计值代表你预期这趟数据里有百分之几是异常先给 0.03 作为起点后续要按分数分布校准。训练完成后对全量特征打分并标记异常df_feat[score] model.decision_function(X_scaled) df_feat[anomaly] model.predict(X_scaled) -1 print(df_feat[anomaly].mean())这里的关键点是 predict 返回 1 表示正常、-1 表示异常score 则是连续分数分数越大代表越正常。直接用 predict 能快速看到结果但它把阈值藏在内部了要调阈值、要分析误报必须回到 score 上做文章。print 输出的均值就是实际检出的异常比例如果它和 contamination 差得很远说明特征或数据分布有情况值得停下来检查。4. 把异常阈值调准分数分布、评估指标与参数调整顺序4.1 先看分数分布再决定阈值predict 的输出只有两种状态看着简单但它隐藏了一个问题阈值是谁定的隔离森林内部按 contamination 推了一个阈值这个值在你对数据没有先验认识时很可能不符合你要的业务口径。比如车队管理里你更在意漏报希望把可疑片段都捞出来人工复核这时内部阈值往往偏严反过来如果用于自动罚款误报太多会被投诉阈值就要适当收紧。所以我的习惯是训练后先画 score 的直方图再去看 1%、3%、5% 的分位点落在哪里用分位数来决定阈值而不是直接信任 predictimport matplotlib.pyplot as plt plt.hist(df_feat[score], bins50, alpha0.7) plt.axvline(np.quantile(df_feat[score], 0.03), colorred, linestyle--) plt.xlabel(decision_function score) plt.ylabel(count) plt.show()这段代码把分数分布画成直方图并在 3% 分位点画一条红色虚线。0.03 是异常比例预期如果红线往左说明这 3% 的样本分数明显低于主体异常和正常能分开模型效果好如果红线贴着主体分数说明大部分样本挤在一起靠阈值硬切效果会很差。参数上0.03 可以换成 0.01、0.05 甚至 0.1换一次重画一次图肉眼看分布比单纯看 F1 更直观。提示contamination 是异常比例估计值不是最终阈值。最终阈值应该来自分数分布的分位数两者在模型内部有关联但业务口径不同。4.2 有标签数据时用 precision、recall 和 F1 算清账如果这份数据是课程设计里带标注的训练后有真实标签可以算账。计算时要把 anomaly 这个布尔列转成 1/0不然 sklearn 的评估接口会报警。需要说明的是这里评估的是窗口级结果和最终要还原的时间段级评价还不一样窗口级指标能快速反馈调参方向from sklearn.metrics import precision_score, recall_score, f1_score y_true df_label[is_anomaly].astype(int).to_numpy() y_pred df_feat[anomaly].astype(int).to_numpy() print(precision:, precision_score(y_true, y_pred)) print(recall:, recall_score(y_true, y_pred)) print(f1:, f1_score(y_true, y_pred))参数说明y_true 是人工标注的标签1 表示异常、0 表示正常y_pred 是模型预测也是 1 异常、0 正常。precision 高说明检出来的基本都是真异常误报少recall 高说明该抓的都抓到了漏报少。调参时如果只盯着一个指标容易跑偏比如把阈值调到很松可以刷高 recall但工作日高峰路段的正常缓行也会被标成异常误报率会很难看。我一般先固定 baseline 看 F1再根据业务是偏漏报还是偏误报去动阈值。没有真实标签时也有替代评价办法统计异常点合并后形成的异常段数量、每段持续时长、异常段占全天时间的比例再拿这些数和实际情况对一下。比如一段 2 小时的市区通勤数据检出了 40 个异常段、平均每段 3 秒这个频次基本符合常理如果检出一段持续 20 分钟的异常那大概率是数据整体漂移不是驾驶行为异常。4.3 参数表与调参顺序这套流程里值得调的参数其实不多核心就是五个窗口长度、滑动步长、contamination、树数量、每棵树采样量。我把推荐范围和调参方向整理成一张表调参时照着顺序动参数推荐范围调参方向window3 到 5 秒太小噪声大太大磨平急刹step1 秒或 window/2越小越平滑越大计算越快contamination0.01 到 0.1偏漏报调大偏误报调小n_estimators100 到 300树多更稳但训练更慢max_samples128 到 512太小丢模式太大记噪声调参顺序比参数本身更重要。先固定 random_state保证每次改动都可复现第一步固定 window 和 step扫描 contamination 从 0.01 到 0.1第二步固定 contamination扫描 window 在 3 到 5 之间变化第三步才去动 n_estimators 和 max_samples。原因是前两个参数直接决定特征分布对结果影响最大树参数在几百棵之后边际收益很小不值得一开始就花时间去试。每调一轮记录一组 F1最后对比曲线选点。5. 避坑与常见问题这几个翻车点先解决这个项目本身不复杂真正的坑几乎都在数据侧和参数口径上。以下五个问题我都实际踩过每个按「现象 → 原因 → 解决」的顺序说清楚能帮你少走弯路。5.1 跑完一遍预测结果全是正常点现象是 predict 之后一个异常点都没有df_feat[anomaly].mean() 输出 0.0。第一次遇到时我以为是模型失效后来发现多半是两个原因之一一是 contamination 被设成了 0模型认为数据里没有异常二是特征列里出现全零或常数比如只保留了一个速度列切窗之后 std 全为 0这类常量特征让树完全切不出差异。解决办法是先看特征统计量再给 contamination 一个有意义的起点。检查特征的标准差是否都大于 0如果某一列全是 0 或接近 0把它从 feature_cols 里删掉如果特征没问题把 contamination 设成 0.01 到 0.05 之间的数值不要用 auto因为 auto 在某些版本下会按不确定性较大的方式推断比例。5.2 GPS 跳变被当成急刹车整段标红现象是异常点集中在某一个时间段回看速度曲线那里有一个尖峰像急刹但实际上是传感器丢星或隧道里 GPS 信号跳变。原因很明显原始数据有毛刺而 max_delta 和 jerk_mean 对瞬时突变非常敏感一个采样点的跳变就会让窗口内特征飙升。解决思路是先滤波再做特征我给这种场景用中值滤波而不是均值滤波因为中值能保留真实突变、同时吃掉孤立跳点。一个简单的做法是对速度列做滚动中值df[speed_kmh] ( df[speed_kmh] .rolling(5, centerTrue, min_periods1) .median() )注意 rolling 窗口为 5 时边界点会取不到完整的 5 个值min_periods1 让边界用已有数据计算不会产生 NaN。滤波之后重新生成窗口特征再去跑模型这类误报通常会大幅减少。滤波强度不要太高7 点以上的中值窗口会把真实的急加速也平滑掉反而影响召回。5.3 构造窗口特征时内存暴涨Kernel 直接重启现象是前一步还正常执行到滑窗循环时 notebook 卡死然后 Kernel died。原因是 window 设得过大、step 设为 1同时把所有原始数据一次性读进来窗口数量接近采样点数特征矩阵被撑到几十万行甚至上百万行内存直接爆掉。解决办法是先压缩数据量再验证流程。把 window 控制在 5 以内、step 设为 2能减少一半窗口数如果原始数据是几百万行先抽样 10 万行跑通流程确认效果后再考虑全量。另外读 CSV 时给相关列指定 float32 类型内存占用直接减半对树模型精度几乎没有影响。5.4 FileNotFoundError 或 KeyError路径和列名的坑现象是 notebook 里写的是相对路径比如 data/demo_driving.csv但运行时提示找不到文件或者明明 CSV 里有速度数据却报 KeyError说找不到 speed 这一列。原因是 Jupyter 的工作目录不一定等于 notebook 所在目录尤其是从终端直接 jupyter notebook 启动时工作目录是启动命令的位置列名则可能是中文、带空格或者大小写不一致。解决方式是先打印当前工作目录确认位置再用 Path 拼接路径。列名问题用一层统一映射解决把原始列名规范成小写、去空格、去单位后再进流程from pathlib import Path import os print(cwd:, os.getcwd()) BASE_DIR Path.cwd() DATA_PATH BASE_DIR / demo_driving.csv print(exists:, DATA_PATH.exists()) df_raw pd.read_csv(DATA_PATH) df_raw.columns [c.strip().lower().replace( , _) for c in df_raw.columns]这段代码先打印工作目录确认 notebook 实际在哪运行再通过 Path.cwd() 拼出数据文件绝对路径避免相对路径失效最后对列名做统一清洗把带空格和大小写不一致的列名收敛成干净格式。后面所有代码都使用清洗后的列名换数据时只需改映射关系不用改模型代码。5.5 自己没有真实驾驶数据流程跑不起来现象是等真实数据到位才能开始调代码结果拖了一两周项目还没起步。很多人会卡在这一步实际上这个项目对数据的要求很低只要有时刻和速度两列就能把整条链路走通。第 2.2 节给的合成数据就是标准替代方案先拿合成数据验证代码能跑再去找真实数据替换。真实数据可以从 OBD 设备导出 CSV也可以把手机 GPS 记录仪导出的轨迹整理成时刻、速度两列。注意两点一是采样率要统一混着 1Hz 和 5Hz 的数据做窗口特征会失真二是时间戳列要解析成 datetime 类型后续做异常段合并时才能用时间差计算持续时间。先解决数据有无的问题再解决数据质量的问题顺序不要搞反。6. 进阶一条路异常段合并与 AutoPlait 对照验证6.1 把逐点异常合并成时间段模型输出的异常是逐窗口的但业务上需要的是「几点几分出现了一段持续几秒的危险驾驶」而不是一串时间戳。逐点看容易漏掉规律合并成片段后再看才有意义。合并的逻辑很简单异常窗口按时间排序相邻间隔小于某个阈值就看成同一个异常段比如间隔不超过 10 秒就合并成一段def merge_anomaly_runs(df_feat, gap_s10): df df_feat[df_feat[anomaly]].copy().sort_values(ts_start) if df.empty: return pd.DataFrame(columns[start, end, duration_s]) df[grp] (df[ts_start].diff().dt.total_seconds() gap_s).cumsum() out df.groupby(grp).agg( start(ts_start, min), end(ts_end, max) ) out[duration_s] (out[end] - out[start]).dt.total_seconds() return out.reset_index(dropTrue)这段代码通过 ts_start 的差分判断异常窗口是否连续gap_s 是合并阈值超过 10 秒就认为是两次独立事件。输出结果是每个异常段的起止时间和持续时间日常排查时拿它去对应视频或地图轨迹比盯着几百个异常点高效得多。6.2 用 AutoPlait 做一次无监督交叉验证AutoPlait.ipynb 在项目里不是摆设它做的是把整段长时序自动切成若干片段并归纳出重复出现的模式全程不需要标签。它和隔离森林的视角不同隔离森林看的是单窗口偏离程度AutoPlait 看的是整段序列的结构变化。把两边结果做交集能有效压低误报。具体做法是跑完 AutoPlait 后看它给出的 segment 列表找到那些只在某个低频模式里出现的片段如果隔离森林标出的异常段恰好落在这个低频模式里这个异常就比较可信如果某个异常段落在最常见的正常模式中大概率是误报。这个交叉验证习惯我用过很多次尤其在没有标签的真实采集数据上比单纯调 contamination 更能发现问题。从那以后我每次拿到新的驾驶采集数据都会强制先跑一遍扫描脚本检查采样率是否统一、列名是否需要清洗、速度曲线有没有毛刺三项都过关才把数据喂给模型。这套流程不复杂却能省掉大多数看起来像模型问题的数据事故。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?