3、向量维度与分布分析:维度统计、均值/方差/偏度/峰度计算、分布可视化(直方图、箱线图)、异常维度检测
向量数据进来之后,第一件事是什么?
我个人习惯,不是急着建索引,也不是调参。而是先看看这批向量的「长相」——也就是它们的维度和分布。你想想看,一个768维的向量,如果其中几十个维度全是0,或者某个维度的值跑到了几百开外,那后续的检索和聚类基本就废了。
这一章,我们就来聊聊怎么给向量做「体检」。
3.1 维度统计:先摸清底细
拿到一批向量,我通常会先问几个问题:
- 这批向量的维度数是多少?是不是所有向量维度一致?
- 每个维度的取值范围是什么?有没有空值或NaN?
- 每个维度的均值、方差大概在什么量级?
嗯,这里要注意。很多人在生产环境里遇到过「维度塌陷」——就是某个维度在所有向量上的值都一模一样。这种维度对检索毫无贡献,反而增加计算量。
核心指标速查表
| 统计量 | 含义 | 正常范围(经验值) |
|---|---|---|
| 均值 | 该维度所有值的平均 | 接近0(经过归一化后) |
| 方差 | 该维度值的离散程度 | 0.1 ~ 1.0 之间 |
| 偏度 | 分布的不对称性 | -1 ~ 1 之间 |
| 峰度 | 分布的尖峭程度 | 接近3(正态分布基准) |
代码实现其实不复杂。我习惯用NumPy一把梭:
import numpy as np
# 假设 vectors 是 (N, D) 的数组
vectors = np.random.randn(10000, 768)
# 维度统计
dim_means = np.mean(vectors, axis=0)
dim_vars = np.var(vectors, axis=0)
dim_stds = np.std(vectors, axis=0)
# 检查是否有全零维度
zero_dims = np.where(np.all(vectors == 0, axis=0))[0]
if len(zero_dims) > 0:
print(f"警告:发现 {len(zero_dims)} 个全零维度")
# 检查是否有NaN
nan_dims = np.where(np.any(np.isnan(vectors), axis=0))[0]
if len(nan_dims) > 0:
print(f"危险:发现 {len(nan_dims)} 个维度包含NaN")
我的经验:有一次处理用户画像向量,发现第127维的均值是0.98,其他维度都在0附近。查了半天,原来是特征工程阶段把「是否VIP用户」这个二值特征直接塞进了向量,没做归一化。这种「离群维度」会严重干扰距离计算。
3.2 偏度与峰度:分布的「形状」说了算
光看均值和方差够吗?不够。我举个例子:两个维度可能有完全相同的均值和方差,但一个分布是正态的,另一个是双峰的。这时候偏度和峰度就能帮你区分。
偏度(Skewness):衡量分布是否对称。
- 偏度 ≈ 0:对称分布(如正态分布)
- 偏度 > 0:右偏,长尾在右边
- 偏度 < 0:左偏,长尾在左边
峰度(Kurtosis):衡量分布的「尖」或「平」。
- 峰度 ≈ 3:正态分布(有些库用超额峰度,此时 ≈ 0)
- 峰度 > 3:尖峰,数据集中在均值附近,但有厚尾
- 峰度 < 3:平峰,分布比较均匀
计算也很直接:
from scipy.stats import skew, kurtosis
# 计算每个维度的偏度和峰度
dim_skewness = skew(vectors, axis=0)
dim_kurtosis = kurtosis(vectors, axis=0) # 默认是超额峰度
# 找出异常维度
high_skew_dims = np.where(np.abs(dim_skewness) > 2)[0]
high_kurt_dims = np.where(np.abs(dim_kurtosis) > 5)[0]
print(f"偏度异常的维度数:{len(high_skew_dims)}")
print(f"峰度异常的维度数:{len(high_kurt_dims)}")
避坑指南:我曾经遇到过一个案例,某个维度的峰度高达87。一开始以为是数据问题,后来发现是那个维度对应的是「用户点击次数」,大部分用户是0,少数大V有几万次。这种分布用标准化根本拉不回来,最后做了对数变换才解决。
3.3 分布可视化:一图胜千言
统计数字再漂亮,也不如一张图来得直观。我一般会做两种图:直方图和箱线图。
3.3.1 直方图:看整体形态
直方图能告诉你这个维度的值是怎么分布的——是集中在某个区间,还是均匀散开,还是有两个峰值。
import matplotlib.pyplot as plt
# 随机选几个维度画直方图
sample_dims = [0, 100, 200, 500]
fig, axes = plt.subplots(2, 2, figsize=(12, 8))
for idx, dim in enumerate(sample_dims):
ax = axes[idx // 2][idx % 2]
ax.hist(vectors[:, dim], bins=50, alpha=0.7, color='#3498db')
ax.set_title(f'Dimension {dim}')
ax.set_xlabel('Value')
ax.set_ylabel('Frequency')
plt.tight_layout()
plt.show()
看直方图的时候,我主要关注三点:
- 是否有明显的双峰或多峰——可能意味着这个维度编码了两种不同的模式
- 是否有极端值——尾巴拖得太长,说明有离群点
- 是否集中在某个小范围——比如所有值都在[-0.1, 0.1]之间,那这个维度的区分度就很差
3.3.2 箱线图:抓异常值
箱线图是检测异常维度的利器。它能直观展示中位数、四分位数和离群点。
# 箱线图:看多个维度的分布对比
sample_data = [vectors[:, i] for i in range(0, 768, 64)] # 每隔64维取一个
plt.figure(figsize=(14, 6))
plt.boxplot(sample_data, labels=[f'Dim {i}' for i in range(0, 768, 64)])
plt.xticks(rotation=45)
plt.ylabel('Value')
plt.title('Boxplot of Sampled Dimensions')
plt.grid(True, alpha=0.3)
plt.show()
箱线图里,如果某个维度的「箱子」特别扁,说明这个维度的值高度集中;如果「须」特别长,说明离群点多。我一般会设定一个规则:超过箱线图上下边缘3倍IQR的点,标记为异常。
3.4 异常维度检测:自动化筛查
手动看图当然好,但几百上千个维度,你不可能一个个看。我们需要一套自动化的检测流程。
我个人常用的方法是「多指标综合打分」:
- 统计指标异常:均值偏离0太远(比如 |mean| > 0.5)、方差过大或过小(var < 0.01 或 var > 2.0)
- 分布形态异常:|偏度| > 2、|超额峰度| > 5
- 离群点比例异常:超过3σ的点占比 > 1%
- 缺失值异常:该维度有NaN或inf
def detect_anomalous_dimensions(vectors,
mean_thresh=0.5,
var_low=0.01,
var_high=2.0,
skew_thresh=2.0,
kurt_thresh=5.0):
"""
自动检测异常维度
"""
N, D = vectors.shape
anomalies = {}
# 1. 均值异常
means = np.mean(vectors, axis=0)
anomalies['mean'] = np.where(np.abs(means) > mean_thresh)[0].tolist()
# 2. 方差异常
vars = np.var(vectors, axis=0)
anomalies['variance'] = np.where((vars < var_low) | (vars > var_high))[0].tolist()
# 3. 偏度异常
skews = skew(vectors, axis=0)
anomalies['skewness'] = np.where(np.abs(skews) > skew_thresh)[0].tolist()
# 4. 峰度异常
kurts = kurtosis(vectors, axis=0)
anomalies['kurtosis'] = np.where(np.abs(kurts) > kurt_thresh)[0].tolist()
# 5. 离群点比例
z_scores = np.abs((vectors - means) / np.sqrt(vars + 1e-8))
outlier_ratio = np.mean(z_scores > 3, axis=0)
anomalies['outlier'] = np.where(outlier_ratio > 0.01)[0].tolist()
# 汇总:被标记3次以上的维度
all_flagged = []
for dims in anomalies.values():
all_flagged.extend(dims)
from collections import Counter
flag_counts = Counter(all_flagged)
severe_anomalies = [dim for dim, cnt in flag_counts.items() if cnt >= 3]
return anomalies, severe_anomalies
实战建议:异常维度不一定要直接删除。我通常的做法是:
- 严重异常(被3个以上指标标记):先检查特征来源,确认是错误则删除或重做
- 轻度异常(1-2个指标标记):做对数变换或分箱处理
- 只有离群点多的维度:考虑用截尾均值或中位数替代
3.5 知识体系总览
下面这张图总结了本章的核心逻辑,从原始向量到最终决策的完整链路:
这张图把整个流程串起来了。从原始向量出发,经过三个维度的分析——基础统计、分布形态、可视化——最后汇聚到异常检测引擎,输出决策。你在实际项目中,可以把这个流程做成一个自动化Pipeline,每天定时跑一遍。
最后说一句:维度分析不是一次性工作。向量数据会随着新数据的加入而漂移。我建议每周至少跑一次全量分析,把异常维度记录下来,形成趋势报表。这样你就能提前发现数据质量问题,而不是等到检索效果下降才去排查。