资讯动态

CBLOF异常检测算法:基于聚类的局部离群点识别原理与实战

发布时间:2026/10/24 6:13:54 来源:尧图企业网站定制
很多数据分析师在聊“异常检测”时第一反应都是孤立森林、LOF、One-Class SVM这些经典模型但真正在处理带簇结构的数据时我反而更常用CBLOF——基于聚类的局部异常因子检测算法。它思路非常朴素先用K-means把数据切成几堆再根据“这堆有多大”和“这个点离自己那堆的中心有多远”联合打一个异常分。相比LOF需要两两算距离、跑一次就等到天荒地老CBLOF计算量小、解释性强和业务方解释“为什么它是异常”时特别方便。这篇就把CBLOF的完整原理、手写实现、PyOD落地、调参避坑一次讲透适合做数据质量、风控、运维监控的同学直接抄作业。1. 为什么异常检测会想到聚类1.1 异常的本质是“和大多数不一样”异常检测的核心假设其实很朴素正常数据总是成群出现异常数据总是特立独行。信用卡盗刷和普通消费的支付行为不一样设备故障前传感器读数和正常工况不一样这反映在数据上就是正常点密集分布在某些区域异常点要么落在稀疏区要么自己聚成一小撮。聚类算法本身就是干这个事的——把密度高的区域识别出来把“少数派”暴露出来。但直接拿K-means做异常检测会有一个大坑K-means对离群点非常敏感异常点会把聚类中心往自己那边拽最后中心偏移正常簇被拉变形。所以CBLOF没有粗暴地拿“离中心远不远”当唯一标准而是引入了簇大小作为权重这就是它和原生K-means的关键区别。1.2 CBLOF和其他异常检测算法的定位差异很多人一上来就问CBLOF和LOF局部异常因子到底什么关系名字都带Local Outlier Factor容易混。LOF的定义是“这个点周围邻居的密度比邻居们的邻居密度低多少”它刻画的是局部异常。代价是复杂度基本是O(n²)级别样本到几万条就明显吃力。CBLOF虽然名字里也有Local但它的“局部”指的是“先聚成局部簇再看点到簇中心距离”复杂度主要是K-means的O(n·k·iter)级别在十万条数据下跑得飞快。两者适用面也不同。LOF擅长密度不均的数据比如一个区域正常点间距是1另一个区域正常点间距是0.1用全局距离阈值就废了CBLOF则优先考虑有明显团状结构的数据比如用户分群、设备工况分段这类场景。说直白点LOF是微观视角看邻居CBLOF是宏观视角看群体归属。1.3 适合CBLOF的业务场景我实际用下来CBLOF在三个场景里特别出彩数据质量检查某个字段值有问题、某些样本明显偏离主体分布它能快速把脏数据踢出来。风控规则前置先跑CBLOF把强异常样本筛掉再进精细模型省算力。传感器/时序指标异常同一设备经常有几个稳定运行状态正常状态天然成簇CBLOF正好对口。如果数据本身分布均匀、没有天然簇结构CBLOF就不合适这时候孤立森林或者自编码器往往更稳。场景选对CBLOF才能发挥作用。2. CBLOF算法的三个关键步骤2.1 第一步用K-means先把数据分群CBLOF的第一步就是对全部训练数据做聚类默认用K-means。设数据集有n个样本我们指定聚成k个簇聚类之后每个样本都会分到一个簇标签记这些簇为C1C2……Ck每个簇的样本数量记作|Ci|簇中心记作center_i。要注意的是CBLOF并不是把K-means的结果直接当最终判定聚类只是为了构建一个“群体结构”。既然只是打底聚成几簇就很重要。k太小正常点和异常点混在一个大簇里后面乘上簇大小反而把异常分压低了k太大正常簇被切碎可能某些正常簇因为体积小而整体被判成异常簇。后面调参部分我会专门讲k怎么选。实操时为了保证K-means收敛稳定建议把n_init设到10以上同时固定random_state否则每次跑出来的簇划分不一样异常分数抖动会很大。2.2 第二步把簇分成大簇和小簇聚完类之后把所有的簇按照样本数量分成两类大簇large clusters和小簇small clusters。判断依据就是两个参数alpha和beta。这里我直接给PyOD库的实现逻辑它也是最常用的版本。对于某个簇Ci如果满足下面两个条件中的任意一个就把它划入大簇|Ci| ≥ alpha × n即这个簇的样本数超过全量样本的alpha倍|Ci| ≥ max_j|Cj| / beta即这个簇的样本数不少于当前最大簇的1/beta。默认情况alpha0.9beta5。通俗解释就是一个簇要么本身大到包含全量数据的90%以上要么至少是最大簇的五分之一才算“有分量的正常群体”。不满足条件的的簇统称为小簇。为什么要设置beta这个条件因为单看alpha容易翻车。如果单簇样本量都达不到总样本90%那么所有簇都会变成小簇CBLOF就失效了。beta的存在保证最大簇无论如何都能进入大簇集合算法不会出现“大簇集合为空”的尴尬局面。这是很典型的工程兜底设计看源码时会发现这个小细节自己手写时特别容易漏。2.3 第三步按簇归属为每个样本打分打完标签之后对每个样本算CBLOF异常分数。计算公式分两种情况如果样本x属于大簇Ci它的异常分数是CBLOF(x) |Ci| × dist(x, center_i)也就是“样本到自己所属簇中心的距离”乘以“该簇的样本量”。大簇里的点离中心越远分数越高同时簇越大同样的偏移会被放大得越明显。如果样本x属于小簇Cj它的异常分数是CBLOF(x) |Cj| × min_{大簇k} dist(x, center_k)也就是“样本到最近大簇中心的距离”再乘上自己所属小簇的样本量。注意这里是到最近大簇中心的距离而不是到自身簇中心。小簇的点本身就少见再乘以小簇大小异常分数会被抬得非常高。这个设计的直觉很到位小簇要么是真正的异常簇要么是噪声聚集区无论哪种里面每个点的可信度都极低大簇里跑偏的点虽然可疑但大簇量大说明大方向可信只有真正偏离得够远才值得高分。所以CBLOF最后把分数从高到低排分数最高的那批就是异常候选。到这一步CBLOF就完成了从无监督数据到“每个样本一个异常分数”的转换。后面定阈值就是用contamination参数比如你预估异常率是2%就取分数排名前2%的位置作为阈值分数超过阈值的打成异常。PyOD里这个过程封装成了fit之后自动完成。3. 手写CBLOF核心代码跑一个完整实例3.1 构造一份“一眼能看出异常”的演示数据为了把原理讲清楚我不用真实业务数据直接用代码生成一份有清晰簇结构的二维数据。三个正常簇各250个点再加三撮远离正常区域的异常点每撮5个。这样训练完效果好坏一眼就能看出来。import numpy as np import matplotlib.pyplot as plt from sklearn.cluster import KMeans from sklearn.datasets import make_blobs # 生成3个正常簇每个簇250个点 centers_normal np.array([[0, 0], [9, 0], [4, 9]]) X_normal, _ make_blobs(n_samples750, centerscenters_normal, cluster_std0.6, random_state42) # 手动构造三撮远离正常区域的异常点 X_outliers np.array([ [12, 12], [12.5, 12.5], [11.8, 12.2], [12.3, 11.9], [12.1, 13.0], [-4, 12], [-4.5, 11.5], [-3.8, 12.2], [-4.2, 11.8], [-3.5, 12.5], [13, -2], [12.5, -2.5], [13.5, -2.2], [12.2, -1.8], [13.8, -2.8] ]) X np.vstack([X_normal, X_outliers]) y_true np.array([0] * 750 [1] * 15)这份数据总共765个样本正常点比例约98%异常点约2%。先看一眼分布正常簇非常紧凑异常点则完全游离在外任何聚类的异常检测算法都应该能轻松识别。3.2 手写CBLOF实现下面这部分代码是CBLOF的完整手写版本。我不依赖任何专用库只用了NumPy和scikit-learn里边的KMeans方便你看清每一步到底干了什么。def compute_cblof(X, n_clusters6, alpha0.9, beta5, random_state42): 手写CBLOF异常分数计算。 返回scores异常分数、labels簇标签、 centers簇中心、large_clusters大簇id列表 n_samples X.shape[0] # 1. K-means聚类 kmeans KMeans(n_clustersn_clusters, n_init10, random_staterandom_state) labels kmeans.fit_predict(X) centers kmeans.cluster_centers_ # 2. 统计每个簇的样本数量 cluster_sizes np.bincount(labels, minlengthn_clusters) max_size cluster_sizes.max() # 3. 划分大簇与小簇 large_clusters [] small_clusters [] for cid in range(n_clusters): if cluster_sizes[cid] alpha * n_samples or cluster_sizes[cid] max_size / beta: large_clusters.append(cid) else: small_clusters.append(cid) # 4. 计算每个样本的异常分数 scores np.zeros(n_samples) for i in range(n_samples): cid labels[i] if cid in large_clusters: # 大簇中的点到自身簇中心的距离 * 簇大小 dist np.linalg.norm(X[i] - centers[cid]) scores[i] dist * cluster_sizes[cid] else: # 小簇中的点到最近大簇中心的距离 * 自身簇大小 min_dist np.inf for lc in large_clusters: dist np.linalg.norm(X[i] - centers[lc]) if dist min_dist: min_dist dist scores[i] min_dist * cluster_sizes[cid] return scores, labels, centers, large_clusters, small_clusters, cluster_sizes这段代码学习性价比很高核心逻辑就40行左右。我故意不用向量化而是用for循环逐样本计算就是为了让“大簇打分、小簇找最近大簇打分”的逻辑一目了然。实际生产你可以用往向量化改写但对理解算法没帮助。3.3 运行结果与可视化跑一遍看看每个簇被划分成了什么。scores, labels, centers, large_clusters, small_clusters, cluster_sizes \ compute_cblof(X, n_clusters6, alpha0.9, beta5, random_state42) print(簇大小分布:, cluster_sizes) print(大簇id:, large_clusters) print(小簇id:, small_clusters)输出大概长这样簇大小分布: [250 5 5 250 250 5] 大簇id: [0, 3, 4] 小簇id: [1, 2, 5]这个结果非常理想三个正常簇被识别为大簇三撮异常点被切成了三个小簇。正常簇都有自己的250个点最大簇250除以5等于50远小于250所以它们全部满足大簇条件异常簇每簇只有5个点自然成为小簇。接下来可视化异常分数。我把分数做一次min-max归一化方便用颜色深浅展示。from sklearn.preprocessing import MinMaxScaler scores_norm MinMaxScaler().fit_transform(scores.reshape(-1, 1)).ravel() plt.figure(figsize(13, 5)) # 左图K-means聚类结果 plt.subplot(121) plt.scatter(X[:, 0], X[:, 1], clabels, cmaprainbow, s20, alpha0.7) plt.scatter(centers[:, 0], centers[:, 1], cblack, markerx, s150, labelcluster center) plt.title(K-means Clusters) plt.legend() # 右图CBLOF异常检测结果分数最高的前2%标红 plt.subplot(122) threshold_score np.percentile(scores_norm, 98) outlier_flags scores_norm threshold_score plt.scatter(X[~outlier_flags, 0], X[~outlier_flags, 1], clightgray, s20, alpha0.5, labelnormal) plt.scatter(X[outlier_flags, 0], X[outlier_flags, 1], cred, s45, edgecolorblack, labeloutlier) plt.title(CBLOF Outlier Detection (top 2%)) plt.legend() plt.tight_layout() plt.show()预期效果左图能清楚看到K-means把数据切成6个簇右图红色标出的点正好就是右上角、左上角、右下角的三撮异常点。15个异常点基本都在红色集合里回归正常样本被判错的数量极少。用准确率衡量一下pred_outlier scores_norm threshold_score y_pred pred_outlier.astype(int) from sklearn.metrics import precision_score, recall_score print(precision:, precision_score(y_true, y_pred)) print(recall:, recall_score(y_true, y_pred))在这份数据上precision和recall都会在0.9以上。这并不稀奇因为数据就是特意做成“理想考试卷”的三撮异常点离正常簇足够远把CBLOF原理展示得干干净净。需要注意的是手写版在计算分数时没有做任何平滑或归一化不同特征量纲下结果区别很大。真实数据一定要先做标准化后面的避坑章节我会细说。3.4 手写结果和PyOD结果对比手写版OK之后再跑一遍PyOD库的CBLOF验证结果是否一致。这里不需要额外安装PyOD在环境里准备好之后直接import。from pyod.models.cblof import CBLOF clf CBLOF(n_clusters6, alpha0.9, beta5, contamination0.02, random_state42) clf.fit(X) pyod_scores clf.decision_scores_ pyod_pred clf.labels_ print(large cluster centers:, clf.large_cluster_centers_) print(small cluster centers:, clf.small_cluster_centers_) print(decision scores sample:, pyod_scores[:10])PyOD的CBLOF在单次运行中识别出的大簇中心和我们手写版基本一致簇id可能因KMeans初值不同有差异decision_scores与手写分数呈强正相关。唯一区别是PyOD内部做了一些排序和索引处理核心公式没有变。这一步验证下来手写版可以放心用于教学理解实际项目直接调用PyOD。4. 用PyOD库快速落地CBLOF4.1 训练与预测一行代码解决PyOD把这个算法封装得极其简单我日常在项目里接入CBLOF只需要三步。from pyod.models.cblof import CBLOF from pyod.utils.data import evaluate_print # 1. 初始化模型 clf CBLOF(n_clusters6, alpha0.9, beta5, contamination0.02, random_state42) # 2. 训练 clf.fit(X) # 3. 输出标签和打分 y_pred clf.labels_ y_scores clf.decision_scores_ evaluate_print(CBLOF, y_true, y_scores)predict接口会自动把decision_scores和训练时由contamination确定的阈值比较大于阈值标为1异常否则为0正常。evaluate_print是PyOD自带的评估工具打印出ROC、Precisionn等一系列指标对快速验证模型效果非常方便。这里需要理解PyOD的设计哲学fit阶段就根据contamination参数确定了threshold_属性predict时直接拿decision_scores和threshold_比大小。所以在真实业务中如果你对历史数据的异常比例心里有数比如0.5%直接把contamination设为0.005即可如果没数就先看scores分布找拐点位置。4.2 核心参数对照与调参要点参数默认值作用调参建议n_clusters8聚类个数数据有明显K类结构时设为该值否则先用轮廓系数尝试几个值clustering_estimatorKMeans聚类器需要密度簇时可换成DBSCAN但要注意score计算依赖中心alpha0.9大簇覆盖样本比例异常率预估为1%时alpha可以放宽到0.95~0.99beta5大簇相对最大簇比例想让更多簇变成“大簇”时调大beta反之调小use_weightsFalse是否按簇大小加权距离大簇与小簇规模悬殊时打开降低大簇内部误报contamination0.1异常比例务心按业务实际异常率设置否则阈值会错random_state42随机种子固定后结果可复现调参时一定固定4.3 模型效果怎么评估无监督异常检测没有y_true标签怎么评价效果是个经典问题。我的经验是分三种情况第一种有部分历史标注样本。直接用ROC-AUC、Precision、Recall这些指标PyOD的evaluate_print可以帮到忙。第二种完全无标注。观察decision_scores的分布正常情况会看到大部分样本分数很低小部分分数明显翘尾翘尾部分就是异常候选。可以计算分数的分位数比如选择P95或P98作为阈值。如果分数分布非常平滑、没有明显拐点算法很可能没学到有效结构优先检查数据和参数。第三种事后核验。把模型筛选出的异常样本交给业务同学做人工验证。为了让业务方更容易理解我会把decision_scores再做一次min-max归一化乘上100得到“异常指数”数值越高越可疑这样汇报时更直观。5. 实践避坑指南5.1 先标准化再做聚类K-means本质是距离模型特征量纲不一致会直接毁掉聚类结果。比如一个特征取值0~1另一个特征取值0~10000第二个特征会彻底主导距离计算第一个特征名存实亡。CBLOF分数又依赖距离和簇大小所以特征尺度不统一整个算法就是空中楼阁。标准化建议用StandardScaler或者RobustScaler。后者对异常点更鲁棒因为它是基于中位数和四分位距的。我在有脏数据的场景下默认用RobustScaler。from sklearn.preprocessing import RobustScaler scaler RobustScaler() X_scaled scaler.fit_transform(X) clf.fit(X_scaled)注意一句话先标准化再聚类再算分数顺序不能错。5.2 K值、alpha、beta分别怎么调这三个参数是CBLOF的命门。K值方面如果数据本身是多个业务状态聚成的K直接取业务状态数如果不清楚就用轮廓系数选一个相对合理的值然后固定下来。不要一味追求大KK过大正常簇会被切碎切出来的小簇会被整体误判为异常。alpha控制大簇覆盖样本比例。正常群体的占比越高alpha可以越大。比如后台数据99%都是正常的alpha直接设0.98或0.99都没问题。反过来异常率偏高alpha就得往下压但又不能低于最大簇占比否则大簇集合可能失衡。beta控制大簇相对最大簇的规模门槛。beta越大更多簇会被当作小簇异常候选增多beta越小更多簇变为大簇异常候选减少。我建议从默认值5开始以recall和precision的平衡为目标调节不要一上来就改动alpha。这三个参数本质上是联动的我给一个偷懒经验固定K先调alpha再调beta。alpha影响的是整体灵敏度beta影响小簇的“存亡”调参时观察decision_scores分布是否有明显分段比单看指标更有用。5.3 数据没有簇结构怎么办曾有团队拿CBLOF去检测某段时间内均匀分散的指标异常结果效果差强人意。原因很简单数据本身是均匀分布K-means强行切出来的“簇”只是几何划分不是语义分组CBLOF的“簇大小”就失去了解释力。这种情况我的判断建议是先用一个快速的可视化或者轮廓系数检查数据是否有簇结构。如果轮廓系数低于0.2说明聚类本身就勉强CBLOF大概率不可用切换到孤立森林或者局部异常因子更合适。CBLOF不是一个万能算法它的强绑定条件是“正常数据有聚集性”。5.4 高维数据的注意点高维场景下欧氏距离区分度会迅速退化因为各个维度距离都差不多聚类中心的偏移计算很容易被淹没。再加上K-means在高维下收敛慢且容易陷入局部最优CBLOF的效果会明显下滑。我的做法是特征维度超过几十维时先做PCA或UMAP把维度压到二维到二十维之间再喂给CBLOF。如果业务上必须保留原始特征语义那就换用基于树模型的孤立森林或者基于重构误差的自编码器。降维后再做CBLOF还有一个额外好处可视化异常点非常方便方便向业务方解释。5.5 常见问题速查表现象可能原因解决办法所有样本异常分数都很高特征未标准化或数据无明显簇结构先RobustScaler再检查轮廓系数正常点被大量误报为异常contamination设置太高或k太大调低contamination减小k真正的异常点分数不高异常点被塞进了大簇增大k或提高beta多次运行结果不稳定K-means初始中心随机固定random_state或把n_init调大大簇集合为空或只有一个簇alpha设置过高挤压了大簇范围检查beta兜底逻辑调低alpha这里额外分享一个经验不要迷信“无监督模型不需要调参”。CBLOF的聚类环节本身就非常依赖参数环境我至少会固定random_state然后在几个不同种子下跑一遍看异常集合的稳定性。如果异常集合变化极大说明数据本身结构不够干净先做数据清洗比调参更有价值。5.6 到底选CBLOF还是其他算法算法复杂度核心思路推荐场景CBLOFO(n·k·iter)聚类 簇大小 距离数据有明显簇结构样本量较大LOFO(n²)局部近邻密度密度不均匀、局部异常明显孤立森林O(n·log n)随机切割隔离高维、无簇结构、异常占比低自编码器训练时长重构误差特征复杂、有专家算力支持不用每次都纠结选谁。我的习惯是先做数据探索如果散点图或降维图上肉眼能看到团状结构直接上CBLOF如果数据乱成一团直接孤立森林兜底。CBLOF的优势是在可解释性和训练成本之间取得了很好的平衡这也是我坚持用它做数据质量筛查的原因。最后分享一个我个人的心得。CBLOF看起来很简单但它在实际项目里帮我省下大量排查时间。有一次做某系统日志数据质量筛查几百万条记录里混着一批字段异常的样本CBLOF配合降维可视化一眼就看出问题数据自成一个小簇当场定位到上游解析逻辑有漏洞。这种“异常成簇”的现象并不罕见而CBLOF正好是识别这类问题最直接的手段。如果你手头数据恰好有天然群体结构认真把这篇里的参数过一遍它大概率能成为你的数据体检常备工具。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑