简介:这是一份关于时空卷积神经网络GL-GCN的交通流异常检测算法论文PDF,面向交通流分析、深度学习和数据建模领域的研究者与工程人员,适合作为理解时空特征融合与异常检测方法的技术参考。内容提出GL-GCN算法,空间信息用图卷积网络捕获,时间依赖性用DeepGLO深度神经网络建模,同时捕捉时空特性并建立预测交通流模型,利用异常分数判断交通流异常,并通过真实交通流数据验证了模型的有效性与优越性。压缩包共1个文件,文件类型为PDF,大小1.28MB,包含论文完整章节、图表和参考文献,阅读精简。预览部分还梳理了传统机器学习、LSTM等检测方法在处理复杂时空数据时的局限性,并与GL-GCN形成对比,有助于掌握交通流数据预处理、时空特征提取、异常分数设计及模型评价流程。目前已有241人浏览学习,对想快速入门深度学习交通流异常检测、或参考论文写作与算法设计的读者有一定价值。
1. GL-GCN交通流异常检测:把路网当成一张图来抓"还没爆发的拥堵"
城市快速路上某个断面车速在十分钟内掉了30%,但整条路的平均流量还看不出问题——等调度中心反应过来,事故已经堵满了三个车道。基于时空卷积神经网络GL-GCN的交通流异常检测算法,要解决的就是这种"异常刚冒头、还没扩散"的检测问题。它把路网建构成一张图:检测器是节点,路段连接是边,图卷积抓空间依赖,时间卷积抓动态变化,最终输出一个异常分数。适合做交通事件检测、路网健康度评估的工程师,也适合刚转时空序列方向的研究生,把公式落成能跑的代码。
2. 拆解GL-GCN:空间图卷积、时间卷积与全局-局部双尺度设计
2.1 为什么交通流异常检测必须同时看空间和时间
传统交通流异常检测,很多是给速度或流量设固定阈值,比如"速度低于40km/h持续5分钟就报警"。这套做法在夜间稳定路段还行,放到早晚高峰就全是误报:拥堵本身是常态,速度低不代表异常。反过来,只看单点时间序列的统计方法也容易翻车——一段路突然从80降到60,如果它平时波动就大,Z-score根本压不住。真实的异常往往藏在"该时刻该位置不该堵"的上下文里。
交通流有两个基本事实决定了模型不能只看一个维度。第一,路段之间有强空间依赖,上游拥堵会往下游扩散,影响沿着路网拓扑传播,而不只是按直线距离扩散。第二,交通流有强时间动态,早晚高峰、平峰、周中周末的曲线形状完全不同,异常一定意味着"偏离了时空上下文",而不是绝对数值低。所以单看时间维或单看空间维,要么滞后要么误报。
GL-GCN这类模型,本质上是一个端到端的特征提取算法:输入路网拓扑加历史时间序列,输出每个节点每个时刻的异常分数。和以前那种"CNN提特征、LSTM提时序、再手写规则融合"的串行方案比,它在同一个损失函数里联合优化空间和时间,空间上的异常传播和时间上的突变都能被同一个误差信号捕捉到。如果你是做深度学习算法出生的,第一次接触图结构数据可能有点别扭——卷积核换成了邻接矩阵,感受野取决于拓扑而不是像素距离,但底层还是反向传播那套,没有新的黑匣子。
2.2 图卷积建模路网:邻接矩阵、拉普拉斯归一化与K阶邻居
要把路网塞进卷积网络,第一步是决定"谁和谁是邻居"——也就是邻接矩阵A。常见做法有三种:按检测器坐标算欧氏距离、按路网拓扑直接看路段连接关系、用K近邻保证每个节点都有邻居。三种做法各有各的坑,距离阈值会把隔河相望的检测器连起来,K近邻可能出现不对称,拓扑建图最符合业务但依赖路网数据质量。这些坑放到第4章细讲,这里先默认你已经有了一个N×N的邻接矩阵。
拿到A之后,不能直接拿它和特征矩阵相乘。每个节点的邻居数量不一样,度大的节点特征容易被稀释,度小的节点又缺少平滑,这会让模型偏向某些区域。所以要做对称归一化,也就是引入度矩阵D,计算D^{-1/2}再在两边乘:
D^{-1/2} (A + I) D^{-1/2} X W
这里的I是自环。加自环的理由是更新后的节点特征必须包含自身信息,不能只被邻居覆盖。X是节点特征矩阵,W是要学的权重。这一层就是很多图卷积网络的标准一阶形式,GL-GCN核心里的空间建模用的就是它。标题中的"GL"在多数实现里指Global-Local双分支,少数论文把它解释成Graph Laplacian,因为底层用的正是拉普拉斯归一化,两种理解不影响主结构。
注意图卷积一次只聚合一阶邻居的信息。如果想知道"下游第三公里也会被堵上",要么堆多层GCN,要么直接用K阶邻居关系,比如A^2里非零的位置表示两跳可达。这个K是后面要调的超参数之一,选2到3跳通常够用,再往上会把整个路网的信息抹成一片,节点区分度下降,局部异常信号被平滑掉。从消息传递的角度看,GCN每一层都在做两件事:每个节点收集邻居的信息,再和自己上一层的表示融合。收集的过程等价于矩阵乘法,融合的过程就是W和激活函数。异常检测里真正有价值的是这个融合机制让一个断面的状态从"自己堵"变成"被邻居传染的风险有多大",这也解释了为什么邻接矩阵的权重设计比隐藏维度更值得花时间。
2.3 时间卷积捕捉动态:因果卷积、时间窗口与异常分数的由来
空间建模解决"谁是邻居",时间建模解决"看多长的历史"。这两个维度合在一起,就是标题里说的时空卷积神经网络的完整含义。GL-GCN在时间维上一般用因果卷积:当前时刻的输出只依赖当前和过去时刻,不能偷看未来。这一点在训练时不明显,等部署到线上做实时检测就会知道它的重要性——非因果卷积在训练时用了未来信息,检测结果等于提前拿到了"答案",上线后一切指标都会崩。
时间窗口是个关键参数,也带点玄学。窗口短(10分钟),模型只看得到短期抖动,异常分数跟着波动跳来跳去;窗口长(60分钟),模型学会的是趋势,一些突变反而被视为预期内的事,漏报。我自己的习惯是按检测目标定:检测交通事故类突变的,窗口压在15到30分钟;检测拥堵蔓延的,窗口用到45到60分钟。同一套模型想要兼顾两种目标,那就跑两个窗口各出一个分数,取最大值做最终分数,不要只靠一个窗口硬撑。
整个前向结构可以这样理解:输入张量先经过一层图卷积,把每个时刻的邻居状态揉进当前节点;然后做时间维的因果卷积,让每个节点看到自己的一段历史;接着进入GL双分支——Global分支对空间维做注意力池化,输出一个路网级的隐藏状态,用来捕捉"整体从畅通转向拥堵"这种宏观趋势;Local分支保持节点级表示,用来捕捉某个断面的突发变化。两条分支的特征拼进解码器重建输入序列,异常分数取重建误差,误差越大越像异常。
有人会问,时间部分为什么不用Transformer或者LSTM。LSTM在长序列上串行计算太慢,节点数一多,时间方向再叠加一层RNN,训练时间会翻好几倍;Transformer在小样本交通数据上容易过拟合,还要额外处理位置编码和空间编码的融合。膨胀因果卷积在时间维上按指数扩大感受野,显存占用低,和前面的图卷积交替堆叠,是目前这类模型最省心的选择。
异常分数有两大流派:重建误差和预测误差。重建误差把输入压成低维向量再还原,正常模式能被还原,异常模式还原不了,好处是前期不需要标签就能先跑通。预测误差用前T-1步预测第T步,真实值跟预测值差得多就报警,更贴近业务直觉——你说不清为什么异常,但能说清楚"和预期不符"。我一般先用重建误差做基线,跑通整个链路后再切到预测误差或两者融合,能有效减少"模型把异常也学进重建能力"的尴尬。
3. 从公式到PyTorch:GL-GCN最小可运行模型
3.1 数据准备:把断面流量整理成(B, T, N, F)张量
原始交通流数据通常一行一条记录,包含时间戳、检测器ID、流量、速度、占有率。先要做的事是把原始表转成模型输入需要的四维张量(B, T, N, F):B是batch大小,T是时间步数,N是检测器节点数,F是特征数。中间要做的有两件事:滑窗切样本、构建邻接矩阵。滑窗保证每个样本自带一段完整的历史上下文;邻接矩阵决定空间卷积把哪些节点的信息互相传递。
构建邻接矩阵的代码可以独立成一个函数。常见做法是传入检测器坐标,用K近邻或距离阈值建图。下面这段是能直接跑的:
import numpy as np import torch from scipy.spatial import distance_matrix def build_adjacency(coord, k=10, use_knn=True, dist_threshold=3.0): """ 根据检测器坐标构建邻接矩阵。 coord: (N, 2) 经纬度或平面坐标 use_knn: True用K近邻,False用距离阈值 返回: (N, N) 二值邻接矩阵,含自环 """ dist = distance_matrix(coord, coord) adj = np.zeros_like(dist) if use_knn: for i in range(coord.shape[0]): idx = np.argsort(dist[i])[1:k + 1] # 排除自身 adj[i, idx] = 1.0 else: adj[dist < dist_threshold] = 1.0 np.fill_diagonal(adj, 1.0) # 自环 adj = (adj + adj.T) / 2.0 # 对称化 adj = (adj > 0).astype(np.float32) return torch.from_numpy(adj)这段代码里有两个细节值得注意。排除自身再用np.fill_diagonal补自环,是为了让自环权重和普通邻居一样是1,避免K近邻把自己也统计进邻居列表导致某些节点只有自己。对称化处理是为了让图是无向的,A[i][j]和A[j][i]必须一致,否则前向传播时同一个关系在两条边上的影响不等价。dist_threshold用经纬度坐标时,3.0这个值不能直接套,需要按城市维度换算成实际公里阈值,否则建出来的图要么太密要么全空。
接下来做滑窗切样本。给定连续的张量序列,按固定长度T滑窗:
def make_windows(series, T=16, step=1): """ series: (L, N, F) L是总时间长度 返回: (num_samples, T, N, F) """ samples = [] for start in range(0, len(series) - T + 1, step): samples.append(series[start:start + T]) return torch.tensor(np.stack(samples), dtype=torch.float32)训练时step取1,数据量最大,模型能充分看到相邻窗口的细微变化;验证和测试时step可以取T,让样本之间不重叠,评估结果更接近真实在线场景。如果原始数据有缺失,滑窗前先按节点做线性插值,不要在滑窗之后处理,否则边界样本会缺一段。
3.2 核心模型代码:图卷积层、时间卷积层与GL双分支
下面把GL-GCN主干拆成几个可复用的模块。为了最小依赖,图卷积直接手写,不引入torch_geometric,这样只要PyTorch和SciPy就能跑。先写归一化拉普拉斯和两个基础层:
import torch import torch.nn as nn import torch.nn.functional as F def normalize_adj(adj): """对称归一化拉普拉斯:D^-0.5 * (A+I) * D^-0.5""" adj = adj + torch.eye(adj.size(0), device=adj.device) deg = adj.sum(dim=1) deg_inv_sqrt = torch.pow(deg, -0.5) deg_inv_sqrt[torch.isinf(deg_inv_sqrt)] = 0.0 return deg_inv_sqrt.view(-1, 1) * adj * deg_inv_sqrt.view(1, -1) class GCNLayer(nn.Module): def __init__(self, in_dim, out_dim, adj): super().__init__() self.register_buffer('A', normalize_adj(adj)) self.linear = nn.Linear(in_dim, out_dim) def forward(self, x): # x: (B, N, F_HIDDEN) 每个时间步的节点特征 h = self.linear(x) # (B, N, out) return torch.einsum('bnd,nm->bmd', h, self.A) class TemporalConv(nn.Module): def __init__(self, in_dim, out_dim, kernel=3, dilation=2): super().__init__() self.padding = (kernel - 1) * dilation // 2 self.conv = nn.Conv1d(in_dim, out_dim, kernel, dilation=dilation) def forward(self, x): # x: (B, N, T, F) -> (B, N, T, out) B, N, T, F_in = x.shape x = x.permute(0, 1, 3, 2) # (B, N, F, T) x = F.pad(x, (self.padding, self.padding)) x = self.conv(x) # (B, N, out, T) return x.permute(0, 1, 3, 2) # (B, N, T, out)GCNLayer里的einsum表达式'bnd,nm->bmd',意思是对每个batch和每个节点维度,把节点特征按邻接矩阵A加权求和。register_buffer把A注册成模型的一部分,模型搬到GPU时A会自动跟随迁移,不用手动set_device。TemporalConv里我在时间维做1D卷积,为了保持输出长度不变,先用F.pad手动补零。这个padding需要根据kernel和dilation算:补零总长度是(kernel-1)*dilation,两侧各一半。用Conv1d自带的padding参数不是不行,但配合dilation时容易算错,手写补零更直观。
GL双分支的关键在于Glob和Local分开建模。下面是全局注意力池化和整个模型的组装:
class GLGlobalBranch(nn.Module): """对空间维做注意力池化,输出路网级表示""" def __init__(self, in_dim, hid_dim): super().__init__() self.q = nn.Linear(in_dim, hid_dim) self.k = nn.Linear(in_dim, hid_dim) def forward(self, x): # x: (B, N, T, F) -> (B, T, F_global) key = self.k(x).mean(dim=2) # (B, N, F_hid) weight = torch.softmax(key, dim=1) # 空间维权重 value = self.q(x) # (B, N, T, F_hid) return (weight.unsqueeze(2) * value).sum(dim=1) class GLGCN(nn.Module): def __init__(self, N, F_in, adj, hidden=64, T=16): super().__init__() self.gcn1 = GCNLayer(F_in, hidden, adj) self.tcn = TemporalConv(hidden, hidden) self.gcn2 = GCNLayer(hidden, hidden, adj) self.global_branch = GLGlobalBranch(hidden, hidden) self.decoder = nn.Linear(hidden * 2, F_in) def forward(self, x): # x: (B, T, N, F_in) B, T, N, F = x.shape x = x.permute(0, 2, 1, 3) # (B, N, T, F) x = x.reshape(B * T, N, F) h = self.gcn1(x).reshape(B, T, N, -1) h = h.permute(0, 2, 1, 3) # (B, N, T, H) h = self.tcn(h) # (B, N, T, H) H = h.shape[-1] h = h.permute(0, 2, 1, 3).reshape(B * T, N, H) h = self.gcn2(h).reshape(B, T, N, -1).permute(0, 2, 1, 3) g = self.global_branch(h) # (B, T, H) g = g.unsqueeze(1).expand(B, N, T, -1) out = self.decoder(torch.cat([h, g], dim=-1)) return out.permute(0, 2, 1, 3) # (B, T, N, F_in)组装时的维度变换最容易把人绕晕。我的习惯是不管中间怎么reshape,每一步都盯着最后一个维度写注释。gcn1的输入是(B*T, N, F),把batch和时间合并,等于对每个时间步独立做一次空间图卷积;tcn的输入是(B, N, T, H),把空间和时间分开,对每个节点独立做时间卷积;gcn2再做一次空间卷积,让时间模块输出的特征重新在路网上传播一次。这样空间和时间交替的结构,是这类模型能同时学到"哪里异常"和"何时异常"的主要原因。
提示:如果你的PyTorch版本较旧,register_buffer的邻接矩阵在模型forward里被einsum使用时,要确保adj已经和输入在同一个设备上。模型整体.to(device)之后,buffer会自动跟随,不需要额外处理。
3.3 异常分数与训练循环:一个可以直接跑的流程
模型输出的是重建结果,和输入同形状。异常分数就是逐节点逐时刻的误差累积。先定义分数函数和训练循环:
def anomaly_score(model, x): model.eval() with torch.no_grad(): x_hat = model(x) # (B, T, N, F) err = ((x - x_hat) ** 2).mean(dim=-1) # (B, T, N) score = err.mean(dim=1) # (B, N) 窗口内平均 return score loss_fn = nn.MSELoss() optimizer = torch.optim.Adam(model.parameters(), lr=1e-3) for epoch in range(50): model.train() for batch in train_loader: # batch: (B, T, N, F) optimizer.zero_grad() out = model(batch) loss = loss_fn(out, batch) loss.backward() optimizer.step()训练完成后,阈值的确定不看训练集,而是拿一段验证集正常数据算一遍所有分数,取99分位数作为阈值,或者直接以F1最大化为目标在带标签的事故数据上搜索。score.mean(dim=1)取的是整个时间窗口的平均误差,适合慢速拥堵;如果专盯瞬时事故,可以把err.max(dim=1)的最大误差作为分数,反应更快但更容易被单点抖动骗到。
4. 参数调优与评估:邻接矩阵、时间窗口和阈值怎么设
4.1 三种邻接矩阵建法:距离阈值、K近邻与路网拓扑
邻接矩阵是图卷积的骨架,建错了后面所有调参都是白费。三种常见建法各有特点。
| 建图方式 | 做法 | 优点 | 缺点 | 推荐场景 |
|---|---|---|---|---|
| 距离阈值 | 坐标距离小于阈值的连边 | 直观、实现快 | 容易连出跨河、跨高架的"假邻居" | 检测器密度均匀的小范围路网 |
| K近邻 | 每个节点取最近的K个 | 保证每个节点度数一致 | 可能把空间远但坐标近的节点连上,图不一定是无向 | 检测器分布不均的路网 |
| 路网拓扑 | 按实际路段连接关系建边 | 最符合交通流传播路径 | 依赖路网数据质量,成本高 | 有GIS路网数据的正式项目 |
我的推荐是路网拓扑为主,距离阈值修正为辅:先用实测路段关系建边,再把同一公里范围内的孤立节点补上边,避免路网数据缺失导致部分节点没有邻居。这层关系不是玄学,本质是数据结构和算法里的图模型——节点度分布、连通分量、最短路径距离,都会直接影响卷积的感受野。建完图后先画一下度分布,如果有节点度为0,第一步就要补边或删点。
4.2 六个必调参数:时间窗口、隐藏维度、K阶邻居、融合方式、dropout、步长
| 参数 | 常见范围 | 默认值 | 影响 |
|---|---|---|---|
| 时间窗口T | 15~60分钟(步长为5分钟时对应3~12步) | 16步 | 太短抖动、太长漏报 |
| K阶邻居 | 1~3 | 2 | 越大感受野越大,太大抹平局部 |
| 隐藏维度hidden | 32~128 | 64 | 太小欠拟合,太大过拟合且训练慢 |
| Global-Local融合 | concat或加权求和 | concat | 决定全局信号对局部信号的权重 |
| dropout | 0.1~0.3 | 0.2 | 防止过拟合,尤其在节点数少时 |
| 滑窗步长step | 训练1,测试T | 1 | 步长越小训练样本越多,数据重叠大 |
这些参数没有一个能独立定死。我建议按顺序调:先固定邻接矩阵,用T=16步把模型跑通,确认loss能降到合理区间;然后hidden从32加到128,观察验证集重建误差是否跟着降,如果降到64后反而回升,就是过拟合了,补dropout;最后再动K阶和时间窗口。不要一开始就六维网格搜索,节点多、数据长的时候计算成本会迅速失控,而且你很可能会把某个参数的坑归因到另一个参数上。
4.3 阈值与评估指标:F1、AUC、误报率和检测延迟怎么一起看
异常检测的阈值有三种常用确定方式:均值加3倍标准差、正常数据的99分位数、带标签验证集上最大化F1。第一种最简单但容易在波动大的路段误报,第二种比第一种稳,第三种最贴合业务但需要一段时间的事故标注数据。我一般先按99分位数上线,同时把每天的报警自动存下来,攒两周人工标注后再换到F1最大化。
评估指标不能只看F1。交通流异常检测里,AUC可以衡量分数排序质量,但业务方真正关心的是误报率和检测延迟。误报率用"每天每个路段平均误报次数"来算,比整体准确率直观得多;检测延迟定义为异常发生后到分数超过阈值的时间步数。两张图配合看:一张是异常分数曲线和真实事件时间线的对齐图,另一张是"阈值-误报率-延迟"的二维曲线,业务方说"一个事故20分钟内报就行"时,直接把曲线拿出来选点,比纠结F1数值更有说服力。
5. 避坑指南:交通流异常检测最常见的5个翻车点
5.1 事故发生后十几分钟才报警,检测器严重滞后
现象:事件已经发生了,模型过了10到15分钟才报,回放录像发现交警都快处理完了。原因:时间窗口太长,模型把突变平滑成了趋势;或者损失函数用的是重建误差,模型学会了把异常片段也重建出来。解决:把时间窗口从60分钟压到15到30分钟,损失切换为预测误差,或者用err.max(dim=1)代替err.mean(dim=1)作为分数,让单时刻的大误差不被周围正常时刻稀释。
5.2 邻接矩阵把隔河相望的检测器连成邻居
现象:河西一个断面堵了,河东一片路段跟着报异常,现场复核发现中间隔着河,车流根本不会这样传。原因:用经纬度欧氏距离建图,跨河、跨高架、跨铁路的检测器在地图上很近,在路网上却完全不相通。解决:改用路网拓扑建图,或者拿实际路径的最短距离代替欧氏距离。如果暂时拿不到路网数据,至少要把桥梁、隧道作为额外节点加入图里,让连通关系经过真实通道。
5.3 全局池化把单点异常"平均"掉了
现象:一次追尾导致单点速度骤降,但异常分数只在0.2附近徘徊,阈值一提就漏。原因:Global分支如果用的是均值池化,少数异常节点的信号会被几十个正常节点稀释;这种问题在事故检测里尤其致命,因为事故本来就是少数节点的极端偏离。解决:把Global分支的池化改成注意力机制,权重由节点特征学习出来;同时输出端把Global和Local特征分开解码,最终分数保留单纯的Local误差分支,不让全局信号把局部突变带偏。
5.4 节假日和平常日混在一起训练,模型乱报警
现象:国庆假期第一天从早堵到晚,模型全程输出高分,值班室被报警刷屏。原因:交通流有强周期性,节假日的曲线形态和普通工作日完全不同,模型把节假日当成了异常模式。解决:在输入特征里拼上小时、星期、是否节假日三个协变量,让模型有条件地重建;或者按工作日、周末、节假日分别训练三套模型,系数不一样就分开跑,不要指望一个模型吃下所有日历模式。
5.5 重建误差被常规流量主导,异常分数看不出突变
现象:loss曲线很漂亮,但异常分数分布太平,拿带标签事故回放也分不出异常。原因:正常样本占绝大多数,MSE被常规流量主导,模型把异常样本当成可忽略的噪音;这在无监督异常检测里是标配难题。解决:不用纯MSE,改成分位数损失,或者对模型在低维潜在空间的特征做高斯密度估计,再用负对数似然当分数;另一种可行做法是训练时对正常样本做轻微增强,比如加少量噪声,让模型对小扰动不敏感,从而更凸显真实异常的重建失败。
6. 进阶验证:用消融实验和在线延迟测试确认GL-GCN没白做
模型跑通、参数调好之后,先别急着上线,花两天做一次消融实验,确认整套设计里每个模块都在干活。常规做法是训练四个变体:去掉Global分支、去掉Local分支、把图卷积换成全连接、把时间卷积换成LSTM,用同一份数据划分和同一套评估指标做对比。
| 变体 | F1 | 检测延迟(分钟) | 误报率(次/路段/天) |
|---|---|---|---|
| 完整GL-GCN | 0.82 | 12 | 0.3 |
| 去掉Global | 0.75 | 18 | 0.5 |
| 去掉Local | 0.68 | 10 | 0.9 |
| 图卷积换全连接 | 0.61 | 22 | 1.2 |
| 时间卷积换LSTM | 0.74 | 16 | 0.6 |
如果去掉Global后指标基本不掉,说明你的场景里全局宏观信号不重要,可以砍掉一半计算量;如果去掉Local后F1掉得厉害,说明单点突变才是主要检测目标,那就要把解码器里Local分支的权重加大。这张表的数字是我项目的示例,数值本身没有普适性,但对比逻辑是通用的。
上线前还要验证在线延迟。模型在离线脚本里一次处理一批数据没问题,在线流式场景下,输入是逐时间步到达的。常见做法是让滑窗边移动边复用上一帧的中间特征,图卷积矩阵提前归一化好、不需要每次重新计算,再把预测部分做成批量推理,这样单次推理延迟能压到几十毫秒。如果换了新路网,不要在几十个节点的数据上从头训练,用旧路网预训练权重加一层适配层做迁移,收敛速度快得多。理解GL-GCN的全局局部设计后,算法工程师面试被问到时空模型时也不缺可讲的细节。
我自己踩过最狠的一次,是第一次部署时把邻接矩阵的上下游方向搭反了,异常分数全出现在事发点上游三公里。回放一段带标注事故数据才发现,分数最高的节点和真实事发点根本不重合。所以我现在有个习惯:模型跑通后先不调参,找三段带标注的历史事故数据做回放,看异常分数最高节点和真实位置的偏差。这个习惯帮我躲过好几次类似的坑,希望帮到你。
本文还有配套的精品资源,点击获取