异常检测算法¶
一句话说明¶
异常检测找出数据中"不正常"的点,在生信中用于识别污染细胞(QC异常)、基因组结构变异、测序质量异常批次,以及临床数据中的异常样本。
核心知识点¶
异常类型¶
| 类型 | 例子 |
|---|---|
| 点异常 | 单个异常值(一个污染细胞) |
| 上下文异常 | 在该背景下不正常(某通路在对照组中过度激活) |
| 集体异常 | 一组数据整体异常(批次效应) |
主流算法对比¶
| 算法 | 原理 | 优点 | 生信场景 |
|---|---|---|---|
| Z-score | 偏离均值几个标准差 | 简单快速 | QC 指标过滤 |
| IQR | 四分位范围外的点 | 鲁棒,不受极值影响 | 离群样本检测 |
| Isolation Forest | 随机切割易孤立 = 异常 | 无需假设分布 | 高维异常检测 |
| Local Outlier Factor | 局部密度低于邻居 | 检测局部异常 | 细胞群体分析 |
| One-Class SVM | 学习正常边界 | 非线性边界 | 序列质量检测 |
| Autoencoder | 重建误差大 = 异常 | 无监督深度学习 | 组学数据异常 |
| DBSCAN | 噪声点即为异常 | 形状无关 | 细胞聚类中的噪声 |
生信常见应用¶
- scRNA-seq QC:异常细胞(线粒体比例>20%、基因数<200)
- 基因组 CNV 检测:拷贝数异常区域
- 批次异常:整个批次测序质量低
- 蛋白质结构异常:残基构象异常
- 临床试验:异常的生化指标值
实战代码¶
import numpy as np
import pandas as pd
from sklearn.ensemble import IsolationForest
from sklearn.neighbors import LocalOutlierFactor
from sklearn.preprocessing import StandardScaler
# ===== 1. 简单 Z-score 异常检测(单细胞 QC 例子)=====
def z_score_filter(values, threshold=3.0):
"""
基于 Z-score 检测异常值
threshold: 超过几个标准差算异常(通常3)
返回: 是否为正常样本的布尔数组
"""
mean = np.mean(values) # 均值
std = np.std(values) # 标准差
z_scores = np.abs((values - mean) / std) # 每个值的 Z-score
return z_scores <= threshold # True=正常,False=异常
# 模拟 scRNA-seq QC 指标
np.random.seed(42)
n_cells = 500
n_genes_per_cell = np.random.normal(2000, 500, n_cells) # 每个细胞检测到的基因数
n_genes_per_cell = np.clip(n_genes_per_cell, 0, None) # 非负
# 插入几个异常细胞(空液滴或双联体)
n_genes_per_cell[490:495] = [20, 15, 10, 8000, 9000] # 低基因=空液滴,高基因=双联体
is_normal = z_score_filter(n_genes_per_cell, threshold=3)
print(f'正常细胞: {is_normal.sum()} / {len(is_normal)}')
print(f'异常细胞索引: {np.where(~is_normal)[0]}')
# ===== 2. Isolation Forest 高维异常检测 =====
# 构造多维 QC 矩阵(实际用 scanpy 的 obs 数据)
qc_features = np.column_stack([
n_genes_per_cell, # 检测到的基因数
np.random.normal(0.1, 0.05, n_cells).clip(0, 1), # 线粒体基因比例
np.random.normal(5000, 1000, n_cells).clip(0, None) # 总 UMI 计数
])
# 手动设置异常值
qc_features[490:495, 1] = [0.9, 0.85, 0.8, 0.02, 0.01] # 高线粒体=死细胞
# 归一化
scaler = StandardScaler()
qc_scaled = scaler.fit_transform(qc_features)
# 训练 Isolation Forest
iso_forest = IsolationForest(
n_estimators=100, # 树的数量
contamination=0.05, # 预期异常比例(5%)
random_state=42
)
iso_pred = iso_forest.fit_predict(qc_scaled) # -1=异常,1=正常
anomaly_idx = np.where(iso_pred == -1)[0]
print(f'\nIsolation Forest 检测到 {len(anomaly_idx)} 个异常细胞')
print(f'异常细胞索引(前10个): {anomaly_idx[:10]}')
# ===== 3. 自编码器异常检测 =====
import torch
import torch.nn as nn
class AnomalyAutoencoder(nn.Module):
"""
自编码器:正常数据重建误差小,异常数据重建误差大
用于高维基因表达矩阵的异常样本检测
"""
def __init__(self, input_dim, hidden_dim=32, bottleneck_dim=8):
super().__init__()
# 编码器:压缩到低维
self.encoder = nn.Sequential(
nn.Linear(input_dim, hidden_dim), # 输入 -> 隐藏
nn.ReLU(),
nn.Linear(hidden_dim, bottleneck_dim) # 隐藏 -> 瓶颈(最低维)
)
# 解码器:从低维重建
self.decoder = nn.Sequential(
nn.Linear(bottleneck_dim, hidden_dim), # 瓶颈 -> 隐藏
nn.ReLU(),
nn.Linear(hidden_dim, input_dim) # 隐藏 -> 重建输出
)
def forward(self, x):
z = self.encoder(x) # 压缩
x_recon = self.decoder(z) # 重建
return x_recon
# 模拟基因表达数据(50个基因)
gene_dim = 50
normal_data = torch.randn(400, gene_dim) # 正常样本
anomaly_data = torch.randn(10, gene_dim) * 5 # 异常样本(尺度不同)
all_data = torch.cat([normal_data, anomaly_data], dim=0)
# 只用正常数据训练(无监督)
ae_model = AnomalyAutoencoder(input_dim=gene_dim)
optimizer = torch.optim.Adam(ae_model.parameters(), lr=1e-3)
loss_fn = nn.MSELoss()
for epoch in range(100):
ae_model.train()
recon = ae_model(normal_data) # 重建正常数据
loss = loss_fn(recon, normal_data) # 重建误差
optimizer.zero_grad()
loss.backward()
optimizer.step()
# 计算所有样本的重建误差(异常样本重建误差应更大)
ae_model.eval()
with torch.no_grad():
recon_all = ae_model(all_data)
recon_errors = ((recon_all - all_data) ** 2).mean(dim=1) # 每个样本的重建误差
# 设定阈值(训练集的 95 百分位)
with torch.no_grad():
normal_errors = ae_model(normal_data)
normal_errors = ((normal_errors - normal_data) ** 2).mean(dim=1)
threshold = torch.quantile(normal_errors, 0.95) # 第 95 百分位数为阈值
predicted_anomalies = (recon_errors > threshold).sum().item()
print(f'\n自编码器检测到 {predicted_anomalies} 个异常样本(真实异常: 10)')
面试常问点¶
Q: 单细胞分析中用什么指标检测异常细胞? A: 三大 QC 指标:①n_genes(基因数,低=空液滴,高=双联体)②n_counts(UMI总数)③pct_mito(线粒体比例,高=死细胞/应激)。通常用 MAD(中位绝对偏差)而非 Z-score,因为更鲁棒。
Q: Isolation Forest 的原理? A: 随机选特征和分割点,异常点离群需要更少的分割就能被"孤立",所以路径长度短 = 异常分数高。
Q: 无监督异常检测怎么评估效果? A: 有标签时用 AUC-ROC、PR曲线;无标签时靠业务验证(检测到的异常是否真有生物问题)。
Q: 重建误差异常检测的局限? A: 如果异常数据"长得像"正常数据(内分布异常),自编码器可能重建误差也小,漏检。
速查表¶
| 术语 | 解释 |
|---|---|
| 点异常 | 单个样本异常 |
| Z-score | (x-μ)/σ,偏离均值程度 |
| IQR | Q3-Q1,四分位距,鲁棒离群检测 |
| 污染率 contamination | 预期异常比例,需先验知识 |
| 重建误差 | AE 重建数据与原始的差异 |
| MAD | 中位绝对偏差,比 std 更鲁棒 |
| 双联体 Doublet | 两个细胞误判为一个细胞 |
| 空液滴 Empty drop | 没有细胞的液滴误判为细胞 |