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 异常维度检测:自动化筛查

手动看图当然好,但几百上千个维度,你不可能一个个看。我们需要一套自动化的检测流程。

我个人常用的方法是「多指标综合打分」:

  1. 统计指标异常:均值偏离0太远(比如 |mean| > 0.5)、方差过大或过小(var < 0.01 或 var > 2.0)
  2. 分布形态异常:|偏度| > 2、|超额峰度| > 5
  3. 离群点比例异常:超过3σ的点占比 > 1%
  4. 缺失值异常:该维度有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 知识体系总览

下面这张图总结了本章的核心逻辑,从原始向量到最终决策的完整链路:

向量维度与分布分析知识体系 向量维度与分布分析流程 原始向量集 (N×D) 维度统计 基础统计量 均值 / 方差 / 标准差 最小值 / 最大值 / 范围 分布形态 偏度(对称性) 峰度(尖峭程度) 可视化分析 直方图(整体形态) 箱线图(离群点) 异常维度检测引擎 多指标综合打分 · 自动标记 · 严重程度分级 决策输出:删除 / 变换 / 保留 被3+指标标记 → 严重异常 1-2个指标标记 → 轻度异常

这张图把整个流程串起来了。从原始向量出发,经过三个维度的分析——基础统计、分布形态、可视化——最后汇聚到异常检测引擎,输出决策。你在实际项目中,可以把这个流程做成一个自动化Pipeline,每天定时跑一遍。

最后说一句:维度分析不是一次性工作。向量数据会随着新数据的加入而漂移。我建议每周至少跑一次全量分析,把异常维度记录下来,形成趋势报表。这样你就能提前发现数据质量问题,而不是等到检索效果下降才去排查。