AI异常检测与欺诈识别完全教程

教程简介

本教程全面讲解AI异常检测与欺诈识别的核心技术,涵盖统计方法、深度学习异常检测(AutoEncoder/VAE/GAN)、时序异常检测、图异常检测(GNN)、金融欺诈检测、实时流式检测、无监督/半监督方法、可解释性与误报管理等核心内容,通过交易欺诈检测系统案例帮助开发者掌握异常检测技术。

AI异常检测与欺诈识别完全教程

1. 异常检测概述与分类

异常检测(Anomaly Detection)是识别数据中偏离正常模式的观测值的过程。这些偏离点可能代表设备故障、网络入侵、金融欺诈等重要事件。根据数据形态和检测方式,异常可分为以下几类:

点异常(Point Anomaly):单个数据点显著偏离整体分布。例如一笔远超正常金额的交易。

上下文异常(Contextual Anomaly):在特定上下文中异常,但在全局范围内正常。例如夏天出现30°C是正常的,但冬天出现30°C就是异常。

集体异常(Collective Anomaly):单个数据点可能正常,但一组数据点的组合模式异常。例如网络流量中短时间内大量相同请求可能代表DDoS攻击。

import numpy as np
import matplotlib.pyplot as plt

def generate_anomaly_data(n_normal=1000, n_anomaly=50, seed=42):
    """生成包含异常的模拟数据"""
    np.random.seed(seed)
    
    # 正常数据:两个高斯分布的混合
    normal1 = np.random.normal(loc=[5, 5], scale=[1, 1], size=(n_normal // 2, 2))
    normal2 = np.random.normal(loc=[-5, -5], scale=[1, 1], size=(n_normal // 2, 2))
    normal = np.vstack([normal1, normal2])
    
    # 异常数据:均匀分布的离群点
    anomaly = np.random.uniform(low=-15, high=15, size=(n_anomaly, 2))
    
    X = np.vstack([normal, anomaly])
    y = np.array([0] * n_normal + [1] * n_anomaly)
    
    return X, y

X, y = generate_anomaly_data()
print(f"正常样本: {sum(y == 0)}, 异常样本: {sum(y == 1)}")
print(f"异常比例: {sum(y == 1) / len(y):.2%}")

2. 统计方法与传统机器学习

基于统计的方法

最基础的异常检测方法利用统计学原理,假设正常数据服从某种已知分布:

from scipy import stats

class StatisticalAnomalyDetector:
    """基于统计方法的异常检测器"""
    
    def __init__(self, method="zscore", threshold=3.0):
        self.method = method
        self.threshold = threshold
        self.mean = None
        self.std = None
    
    def fit(self, X: np.ndarray):
        self.mean = np.mean(X, axis=0)
        self.std = np.std(X, axis=0)
        return self
    
    def predict(self, X: np.ndarray) -> np.ndarray:
        if self.method == "zscore":
            z_scores = np.abs((X - self.mean) / (self.std + 1e-8))
            return (z_scores > self.threshold).any(axis=1).astype(int)
        
        elif self.method == "iqr":
            Q1 = np.percentile(X, 25, axis=0)
            Q3 = np.percentile(X, 75, axis=0)
            IQR = Q3 - Q1
            lower = Q1 - self.threshold * IQR
            upper = Q3 + self.threshold * IQR
            return ((X < lower) | (X > upper)).any(axis=1).astype(int)
        
        elif self.method == "mad":
            # Median Absolute Deviation
            median = np.median(X, axis=0)
            mad = np.median(np.abs(X - median), axis=0)
            modified_z = 0.6745 * (X - median) / (mad + 1e-8)
            return (np.abs(modified_z) > self.threshold).any(axis=1).astype(int)

# 使用示例
for method in ["zscore", "iqr", "mad"]:
    detector = StatisticalAnomalyDetector(method=method, threshold=3.0)
    detector.fit(X[y == 0])  # 只用正常数据训练
    preds = detector.predict(X)
    tp = sum((preds == 1) & (y == 1))
    fp = sum((preds == 1) & (y == 0))
    print(f"{method:6s} | 检出: {tp}/{sum(y==1)}, 误报: {fp}")

Isolation Forest

孤立森林是目前最流行的无监督异常检测算法之一,其核心思想是异常点由于稀疏性更容易被"孤立":

from sklearn.ensemble import IsolationForest
from sklearn.metrics import precision_recall_fscore_support

class IsolationForestDetector:
    def __init__(self, n_estimators=100, contamination=0.05):
        self.model = IsolationForest(
            n_estimators=n_estimators,
            contamination=contamination,
            random_state=42
        )
    
    def fit(self, X: np.ndarray):
        self.model.fit(X)
        return self
    
    def predict(self, X: np.ndarray) -> np.ndarray:
        # IsolationForest返回-1表示异常,1表示正常
        preds = self.model.predict(X)
        return (preds == -1).astype(int)
    
    def score_samples(self, X: np.ndarray) -> np.ndarray:
        # 返回异常分数(越小越异常)
        return -self.model.score_samples(X)

# 训练与评估
iso_detector = IsolationForestDetector(contamination=0.05)
iso_detector.fit(X)
preds = iso_detector.predict(X)

precision, recall, f1, _ = precision_recall_fscore_support(y, preds, average="binary")
print(f"Precision: {precision:.3f}, Recall: {recall:.3f}, F1: {f1:.3f}")

Local Outlier Factor (LOF)

LOF 通过比较数据点与其邻居的局部密度来判断异常程度:

from sklearn.neighbors import LocalOutlierFactor

class LOFDetector:
    def __init__(self, n_neighbors=20, contamination=0.05):
        self.model = LocalOutlierFactor(
            n_neighbors=n_neighbors,
            contamination=contamination,
            novelty=True  # 支持对新数据预测
        )
    
    def fit(self, X: np.ndarray):
        self.model.fit(X)
        return self
    
    def predict(self, X: np.ndarray) -> np.ndarray:
        preds = self.model.predict(X)
        return (preds == -1).astype(int)

lof_detector = LOFDetector(n_neighbors=30, contamination=0.05)
lof_detector.fit(X[y == 0])
preds = lof_detector.predict(X)
precision, recall, f1, _ = precision_recall_fscore_support(y, preds, average="binary")
print(f"LOF - Precision: {precision:.3f}, Recall: {recall:.3f}, F1: {f1:.3f}")

3. 深度学习异常检测

AutoEncoder 方法

AutoEncoder 通过学习数据的压缩表示来检测异常——正常数据的重建误差较小,异常数据的重建误差较大:

import torch
import torch.nn as nn
from torch.utils.data import DataLoader, TensorDataset

class AnomalyAutoEncoder(nn.Module):
    """用于异常检测的自编码器"""
    
    def __init__(self, input_dim, encoding_dim=8):
        super().__init__()
        self.encoder = nn.Sequential(
            nn.Linear(input_dim, 64),
            nn.ReLU(),
            nn.BatchNorm1d(64),
            nn.Dropout(0.2),
            nn.Linear(64, 32),
            nn.ReLU(),
            nn.Linear(32, encoding_dim),
        )
        self.decoder = nn.Sequential(
            nn.Linear(encoding_dim, 32),
            nn.ReLU(),
            nn.Linear(32, 64),
            nn.ReLU(),
            nn.Linear(64, input_dim),
        )
    
    def forward(self, x):
        encoded = self.encoder(x)
        decoded = self.decoder(encoded)
        return decoded
    
    def get_anomaly_score(self, x: torch.Tensor) -> torch.Tensor:
        """计算重建误差作为异常分数"""
        with torch.no_grad():
            reconstructed = self.forward(x)
            mse = torch.mean((x - reconstructed) ** 2, dim=1)
        return mse

def train_autoencoder(model, data, epochs=50, lr=1e-3, batch_size=64):
    """训练自编码器"""
    dataset = TensorDataset(torch.FloatTensor(data))
    loader = DataLoader(dataset, batch_size=batch_size, shuffle=True)
    optimizer = torch.optim.Adam(model.parameters(), lr=lr)
    criterion = nn.MSELoss()
    
    model.train()
    for epoch in range(epochs):
        total_loss = 0
        for batch in loader:
            x = batch[0]
            reconstructed = model(x)
            loss = criterion(reconstructed, x)
            
            optimizer.zero_grad()
            loss.backward()
            optimizer.step()
            total_loss += loss.item()
        
        if (epoch + 1) % 10 == 0:
            print(f"Epoch {epoch+1}/{epochs}, Loss: {total_loss/len(loader):.6f}")

# 训练模型(只用正常数据)
normal_data = X[y == 0]
ae_model = AnomalyAutoEncoder(input_dim=2, encoding_dim=4)
train_autoencoder(ae_model, normal_data, epochs=50)

# 检测异常
scores = ae_model.get_anomaly_score(torch.FloatTensor(X))
threshold = scores[y == 0].mean() + 3 * scores[y == 0].std()
preds = (scores > threshold).int().numpy()
precision, recall, f1, _ = precision_recall_fscore_support(y, preds, average="binary")
print(f"AutoEncoder - Precision: {precision:.3f}, Recall: {recall:.3f}, F1: {f1:.3f}")

VAE(变分自编码器)

VAE 在 AutoEncoder 基础上引入了概率建模,可以更好地捕捉数据分布:

class VariationalAutoEncoder(nn.Module):
    """变分自编码器用于异常检测"""
    
    def __init__(self, input_dim, latent_dim=8):
        super().__init__()
        # 编码器
        self.encoder = nn.Sequential(
            nn.Linear(input_dim, 64),
            nn.ReLU(),
            nn.Linear(64, 32),
            nn.ReLU(),
        )
        self.fc_mu = nn.Linear(32, latent_dim)
        self.fc_logvar = nn.Linear(32, latent_dim)
        
        # 解码器
        self.decoder = nn.Sequential(
            nn.Linear(latent_dim, 32),
            nn.ReLU(),
            nn.Linear(32, 64),
            nn.ReLU(),
            nn.Linear(64, input_dim),
        )
    
    def encode(self, x):
        h = self.encoder(x)
        return self.fc_mu(h), self.fc_logvar(h)
    
    def reparameterize(self, mu, logvar):
        std = torch.exp(0.5 * logvar)
        eps = torch.randn_like(std)
        return mu + eps * std
    
    def forward(self, x):
        mu, logvar = self.encode(x)
        z = self.reparameterize(mu, logvar)
        reconstructed = self.decoder(z)
        return reconstructed, mu, logvar
    
    def vae_loss(self, x, reconstructed, mu, logvar):
        """VAE损失 = 重建损失 + KL散度"""
        recon_loss = nn.functional.mse_loss(reconstructed, x, reduction='sum')
        kl_loss = -0.5 * torch.sum(1 + logvar - mu.pow(2) - logvar.exp())
        return recon_loss + kl_loss
    
    def get_anomaly_score(self, x: torch.Tensor) -> torch.Tensor:
        """综合重建误差和KL散度作为异常分数"""
        with torch.no_grad():
            reconstructed, mu, logvar = self.forward(x)
            recon_err = torch.mean((x - reconstructed) ** 2, dim=1)
            kl = -0.5 * torch.sum(1 + logvar - mu.pow(2) - logvar.exp(), dim=1)
            return recon_err + 0.1 * kl

GAN-based 异常检测

AnoGAN 利用 GAN 生成正常数据的能力来检测异常:

class AnomalyGAN(nn.Module):
    """基于GAN的异常检测(简化版AnoGAN)"""
    
    def __init__(self, latent_dim=32, output_dim=2):
        super().__init__()
        self.generator = nn.Sequential(
            nn.Linear(latent_dim, 64),
            nn.LeakyReLU(0.2),
            nn.Linear(64, 32),
            nn.LeakyReLU(0.2),
            nn.Linear(32, output_dim),
        )
        self.discriminator = nn.Sequential(
            nn.Linear(output_dim, 32),
            nn.LeakyReLU(0.2),
            nn.Linear(32, 64),
            nn.LeakyReLU(0.2),
            nn.Linear(64, 1),
            nn.Sigmoid(),
        )
    
    def find_closest_z(self, x: torch.Tensor, n_steps=1000, lr=0.01):
        """AnoGAN核心:找到最能重建输入x的潜在向量z"""
        z = torch.randn(x.shape[0], 32, requires_grad=True)
        optimizer = torch.optim.Adam([z], lr=lr)
        
        for _ in range(n_steps):
            generated = self.generator(z)
            recon_loss = torch.mean((x - generated) ** 2)
            optimizer.zero_grad()
            recon_loss.backward()
            optimizer.step()
        
        final_generated = self.generator(z)
        anomaly_score = torch.mean((x - final_generated) ** 2, dim=1)
        return anomaly_score.detach()

4. 时序异常检测

时间序列数据的异常检测需要考虑时间维度上的依赖关系。

基于 STL 分解

STL(Seasonal and Trend decomposition using Loess)将时序分解为趋势、季节性和残差,残差中的异常值即为时序异常:

import pandas as pd

class STLAnomalyDetector:
    """基于STL分解的时序异常检测"""
    
    def __init__(self, period=7, threshold=3.0):
        self.period = period
        self.threshold = threshold
        self.residual_stats = None
    
    def fit(self, ts: pd.Series):
        """拟合正常数据的残差分布"""
        # 简化版STL:移动平均去趋势和季节性
        trend = ts.rolling(window=self.period, center=True).mean()
        detrended = ts - trend
        seasonal = detrended.rolling(window=self.period).mean()
        residual = detrended - seasonal
        residual = residual.dropna()
        
        self.residual_stats = {
            "mean": residual.mean(),
            "std": residual.std(),
        }
        return self
    
    def predict(self, ts: pd.Series) -> pd.Series:
        trend = ts.rolling(window=self.period, center=True).mean()
        detrended = ts - trend
        seasonal = detrended.rolling(window=self.period).mean()
        residual = detrended - seasonal
        
        z_scores = np.abs((residual - self.residual_stats["mean"]) / 
                          (self.residual_stats["std"] + 1e-8))
        return (z_scores > self.threshold).fillna(False)

# 生成模拟时序数据
np.random.seed(42)
dates = pd.date_range("2024-01-01", periods=365, freq="D")
trend = np.linspace(10, 30, 365)
seasonal = 5 * np.sin(2 * np.pi * np.arange(365) / 7)
noise = np.random.normal(0, 1, 365)
values = trend + seasonal + noise

# 注入异常
values[100] += 20   # 突增异常
values[200] -= 15   # 突降异常

ts = pd.Series(values, index=dates)
detector = STLAnomalyDetector(period=7, threshold=3.0)
detector.fit(ts)
anomalies = detector.predict(ts)
print(f"检测到 {anomalies.sum()} 个异常点")
print(f"异常位置: {ts[anomalies].index.tolist()}")

LSTM 时序异常检测

使用 LSTM 学习正常时序模式,当实际值与预测值偏差过大时判定为异常:

class LSTMAnomalyDetector(nn.Module):
    """基于LSTM的时序异常检测"""
    
    def __init__(self, input_dim=1, hidden_dim=64, num_layers=2):
        super().__init__()
        self.lstm = nn.LSTM(input_dim, hidden_dim, num_layers, 
                           batch_first=True, dropout=0.2)
        self.fc = nn.Linear(hidden_dim, input_dim)
    
    def forward(self, x):
        # x: [batch, seq_len, input_dim]
        lstm_out, _ = self.lstm(x)
        predictions = self.fc(lstm_out)
        return predictions

def create_sequences(data: np.ndarray, seq_length: int = 30):
    """创建滑动窗口序列"""
    sequences = []
    targets = []
    for i in range(len(data) - seq_length):
        sequences.append(data[i:i + seq_length])
        targets.append(data[i + seq_length])
    return np.array(sequences), np.array(targets)

# 准备数据
seq_length = 30
X_seq, y_seq = create_sequences(values.reshape(-1, 1), seq_length)

# 训练(使用前80%数据)
split = int(0.8 * len(X_seq))
X_train = torch.FloatTensor(X_seq[:split])
y_train = torch.FloatTensor(y_seq[:split])

model = LSTMAnomalyDetector(input_dim=1, hidden_dim=64)
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
criterion = nn.MSELoss()

model.train()
for epoch in range(50):
    predictions = model(X_train).squeeze(-1)
    # 取序列最后一个时间步的预测
    loss = criterion(predictions[:, -1, :], y_train)
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()

# 异常检测
model.eval()
with torch.no_grad():
    X_all = torch.FloatTensor(X_seq)
    preds = model(X_all).squeeze(-1)[:, -1, :].numpy()
    errors = np.abs(preds.flatten() - y_seq.flatten())
    threshold = np.mean(errors[:split]) + 3 * np.std(errors[:split])
    anomalies = errors > threshold
    print(f"LSTM检测到 {anomalies.sum()} 个异常点")

5. 图异常检测

图结构数据中的异常检测需要考虑节点之间的关系:

import numpy as np
from collections import defaultdict

class SimpleGraphAnomalyDetector:
    """简化的图异常检测:基于节点度和社区结构"""
    
    def __init__(self):
        self.graph = defaultdict(set)
        self.node_features = {}
    
    def add_edge(self, u: str, v: str):
        self.graph[u].add(v)
        self.graph[v].add(u)
    
    def set_node_features(self, node: str, features: np.ndarray):
        self.node_features[node] = features
    
    def detect_degree_anomaly(self, threshold_factor: float = 3.0) -> list:
        """基于节点度的异常检测"""
        degrees = {node: len(neighbors) for node, neighbors in self.graph.items()}
        mean_deg = np.mean(list(degrees.values()))
        std_deg = np.std(list(degrees.values()))
        
        anomalies = []
        for node, deg in degrees.items():
            if abs(deg - mean_deg) > threshold_factor * std_deg:
                anomalies.append((node, deg, "度异常"))
        return anomalies
    
    def detect_structural_anomaly(self) -> list:
        """基于结构特征的异常检测(如聚类系数)"""
        anomalies = []
        for node in self.graph:
            neighbors = self.graph[node]
            if len(neighbors) < 2:
                continue
            
            # 计算聚类系数
            possible_triangles = len(neighbors) * (len(neighbors) - 1) / 2
            actual_triangles = 0
            for n1 in neighbors:
                for n2 in neighbors:
                    if n1 != n2 and n2 in self.graph[n1]:
                        actual_triangles += 1
            actual_triangles //= 2  # 去重
            
            clustering = actual_triangles / possible_triangles if possible_triangles > 0 else 0
            
            # 聚类系数极低可能是异常(如桥接节点)
            if clustering < 0.01 and len(neighbors) > 5:
                anomalies.append((node, clustering, "低聚类系数"))
        
        return anomalies

# 构建示例图
detector = SimpleGraphAnomalyDetector()
normal_nodes = [f"n{i}" for i in range(50)]
for i in range(len(normal_nodes) - 1):
    detector.add_edge(normal_nodes[i], normal_nodes[i + 1])
detector.add_edge(normal_nodes[0], normal_nodes[-1])  # 形成环

# 添加异常节点:度极高
anomaly_node = "anomaly_hub"
for i in range(20):
    detector.add_edge(anomaly_node, normal_nodes[i])

degree_anomalies = detector.detect_degree_anomaly()
for node, deg, reason in degree_anomalies:
    print(f"节点 {node}: 度={deg}, 原因={reason}")

6. 金融欺诈检测系统

金融欺诈检测是异常检测最重要的应用场景之一,面临极端不平衡、实时性要求高、概念漂移等挑战:

import numpy as np
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import train_test_split

class FraudDetectionPipeline:
    """金融欺诈检测流水线"""
    
    def __init__(self):
        self.scaler = StandardScaler()
        self.models = {}
        self.feature_names = []
    
    def engineer_features(self, transactions: list) -> np.ndarray:
        """特征工程:从原始交易数据提取欺诈相关特征"""
        features = []
        for tx in transactions:
            feat = [
                tx["amount"],
                tx["amount"] / (tx.get("avg_amount", 1) + 1e-8),  # 金额偏离度
                tx.get("hour_of_day", 12),
                tx.get("day_of_week", 3),
                tx.get("distance_from_home", 0),       # 交易地点与常驻地距离
                tx.get("time_since_last_tx", 0),        # 距上次交易时间
                tx.get("tx_count_1h", 0),               # 1小时内交易次数
                tx.get("tx_count_24h", 0),              # 24小时内交易次数
                tx.get("unique_merchants_24h", 0),      # 24小时商户数
                tx.get("is_foreign", 0),                # 是否境外交易
                tx.get("is_online", 0),                 # 是否线上交易
                tx.get("merchant_risk_score", 0),       # 商户风险评分
            ]
            features.append(feat)
        return np.array(features)
    
    def handle_imbalance(self, X, y, strategy="smote"):
        """处理类别不平衡"""
        if strategy == "smote":
            # 简化版SMOTE:对少数类进行过采样
            minority_idx = np.where(y == 1)[0]
            majority_idx = np.where(y == 0)[0]
            
            n_synthetic = len(majority_idx) - len(minority_idx)
            synthetic_samples = []
            
            for _ in range(n_synthetic):
                idx = np.random.choice(minority_idx)
                neighbor_idx = np.random.choice(minority_idx)
                alpha = np.random.random()
                sample = alpha * X[idx] + (1 - alpha) * X[neighbor_idx]
                synthetic_samples.append(sample)
            
            X_resampled = np.vstack([X, np.array(synthetic_samples)])
            y_resampled = np.hstack([y, np.ones(n_synthetic)])
            
            # 打乱顺序
            shuffle_idx = np.random.permutation(len(y_resampled))
            return X_resampled[shuffle_idx], y_resampled[shuffle_idx].astype(int)
        
        elif strategy == "undersample":
            minority_idx = np.where(y == 1)[0]
            majority_idx = np.where(y == 0)[0]
            selected_majority = np.random.choice(majority_idx, 
                                                  size=len(minority_idx) * 5, 
                                                  replace=False)
            selected_idx = np.concatenate([minority_idx, selected_majority])
            return X[selected_idx], y[selected_idx]
    
    def train_ensemble(self, X_train, y_train):
        """训练集成模型"""
        from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
        from sklearn.linear_model import LogisticRegression
        
        self.models = {
            "rf": RandomForestClassifier(n_estimators=100, random_state=42, 
                                         class_weight="balanced"),
            "gb": GradientBoostingClassifier(n_estimators=100, random_state=42),
            "lr": LogisticRegression(class_weight="balanced", max_iter=1000),
        }
        
        for name, model in self.models.items():
            model.fit(X_train, y_train)
            print(f"模型 {name} 训练完成")
    
    def predict(self, X: np.ndarray, method="voting") -> np.ndarray:
        """集成预测"""
        predictions = {}
        probabilities = {}
        
        for name, model in self.models.items():
            predictions[name] = model.predict(X)
            if hasattr(model, "predict_proba"):
                probabilities[name] = model.predict_proba(X)[:, 1]
        
        if method == "voting":
            # 加权投票
            weights = {"rf": 0.4, "gb": 0.4, "lr": 0.2}
            weighted_proba = sum(
                probabilities[name] * weights[name] 
                for name in self.models
            )
            return (weighted_proba > 0.5).astype(int)
        
        return predictions["rf"]  # 默认使用随机森林

# 模拟欺诈检测场景
np.random.seed(42)
n_normal = 10000
n_fraud = 100  # 极端不平衡:1%欺诈率

# 生成模拟数据
X_normal = np.random.randn(n_normal, 12) * 0.5
X_fraud = np.random.randn(n_fraud, 12) * 2 + 1  # 欺诈数据分布不同

X = np.vstack([X_normal, X_fraud])
y = np.array([0] * n_normal + [1] * n_fraud)

# 训练流水线
pipeline = FraudDetectionPipeline()
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, 
                                                      random_state=42, stratify=y)
X_train_balanced, y_train_balanced = pipeline.handle_imbalance(X_train, y_train)
pipeline.train_ensemble(X_train_balanced, y_train_balanced)

# 评估
from sklearn.metrics import classification_report
preds = pipeline.predict(X_test)
print("\n分类报告:")
print(classification_report(y_test, preds, target_names=["正常", "欺诈"]))

7. 实时流式异常检测

生产环境中的异常检测通常需要处理实时数据流:

import time
from collections import deque
from dataclasses import dataclass, field
from typing import Callable

@dataclass
class StreamingAnomalyDetector:
    """流式异常检测器:基于滑动窗口"""
    
    window_size: int = 1000
    alert_threshold: float = 3.0
    window: deque = field(default_factory=lambda: deque(maxlen=1000))
    alert_callbacks: list = field(default_factory=list)
    
    def register_alert(self, callback: Callable):
        """注册告警回调"""
        self.alert_callbacks.append(callback)
    
    def process_event(self, event: dict) -> dict:
        """处理单个事件"""
        value = event.get("value", 0)
        timestamp = event.get("timestamp", time.time())
        
        # 添加到窗口
        self.window.append(value)
        
        # 至少需要一定数据量才能判断
        if len(self.window) < 30:
            return {"is_anomaly": False, "score": 0, "reason": "数据不足"}
        
        # 计算异常分数
        window_array = np.array(self.window)
        mean = np.mean(window_array[:-1])  # 排除当前值
        std = np.std(window_array[:-1]) + 1e-8
        z_score = abs(value - mean) / std
        
        is_anomaly = z_score > self.alert_threshold
        
        result = {
            "is_anomaly": is_anomaly,
            "score": z_score,
            "value": value,
            "mean": mean,
            "std": std,
            "timestamp": timestamp,
        }
        
        if is_anomaly:
            result["reason"] = f"Z-score {z_score:.2f} 超过阈值 {self.alert_threshold}"
            for callback in self.alert_callbacks:
                callback(result)
        
        return result

# 使用示例
detector = StreamingAnomalyDetector(window_size=500, alert_threshold=3.0)

def alert_handler(result: dict):
    print(f"⚠️ 异常告警! 值={result['value']:.2f}, "
          f"Z-score={result['score']:.2f}")

detector.register_alert(alert_handler)

# 模拟实时数据流
np.random.seed(42)
for i in range(200):
    if i in [50, 100, 150]:  # 注入异常
        value = np.random.normal(100, 5)
    else:
        value = np.random.normal(0, 1)
    
    result = detector.process_event({"value": value, "timestamp": time.time()})
    if result["is_anomaly"]:
        print(f"  位置 {i}: {result}")

8. 无监督 vs 半监督方法

无监督方法

无监督方法不需要标签,直接从数据结构中发现异常模式。常用方法包括聚类、密度估计和降维:

from sklearn.cluster import DBSCAN
from sklearn.decomposition import PCA

class UnsupervisedAnomalyDetector:
    """基于多种无监督方法的异常检测"""
    
    def __init__(self):
        self.models = {}
    
    def dbscan_detect(self, X: np.ndarray, eps=0.5, min_samples=5) -> np.ndarray:
        """DBSCAN聚类:噪声点即为异常"""
        clustering = DBSCAN(eps=eps, min_samples=min_samples)
        labels = clustering.fit_predict(X)
        # label为-1的点是噪声(异常)
        return (labels == -1).astype(int)
    
    def pca_reconstruct_detect(self, X: np.ndarray, n_components=2, 
                                threshold_percentile=95) -> np.ndarray:
        """PCA重建误差法"""
        pca = PCA(n_components=n_components)
        X_reduced = pca.fit_transform(X)
        X_reconstructed = pca.inverse_transform(X_reduced)
        
        recon_error = np.sum((X - X_reconstructed) ** 2, axis=1)
        threshold = np.percentile(recon_error, threshold_percentile)
        return (recon_error > threshold).astype(int)

# 对比两种方法
unsup = UnsupervisedAnomalyDetector()
dbscan_preds = unsup.dbscan_detect(X, eps=2.0, min_samples=10)
pca_preds = unsup.pca_reconstruct_detect(X, n_components=6, threshold_percentile=95)

print(f"DBSCAN检出: {dbscan_preds.sum()}, PCA检出: {pca_preds.sum()}")

半监督方法

半监督方法利用少量标签来提升检测效果,在实际业务中非常实用:

class SemiSupervisedAnomalyDetector:
    """半监督异常检测:自训练框架"""
    
    def __init__(self, base_model, threshold=0.5):
        self.base_model = base_model
        self.threshold = threshold
    
    def fit_iterative(self, X_labeled, y_labeled, X_unlabeled, 
                      n_iterations=5, confidence_threshold=0.9):
        """自训练迭代:逐步将高置信度预测加入训练集"""
        X_train = X_labeled.copy()
        y_train = y_labeled.copy()
        
        for iteration in range(n_iterations):
            self.base_model.fit(X_train, y_train)
            
            if len(X_unlabeled) == 0:
                break
            
            # 对无标签数据预测
            proba = self.base_model.predict_proba(X_unlabeled)
            max_confidence = np.max(proba, axis=1)
            predictions = np.argmax(proba, axis=1)
            
            # 选择高置信度样本
            high_conf_mask = max_confidence > confidence_threshold
            n_new = high_conf_mask.sum()
            
            if n_new == 0:
                print(f"迭代 {iteration}: 无高置信度样本,停止")
                break
            
            # 加入训练集
            X_train = np.vstack([X_train, X_unlabeled[high_conf_mask]])
            y_train = np.hstack([y_train, predictions[high_conf_mask]])
            
            # 移除已标注的无标签数据
            X_unlabeled = X_unlabeled[~high_conf_mask]
            
            print(f"迭代 {iteration}: 新增 {n_new} 个伪标签样本, "
                  f"剩余无标签 {len(X_unlabeled)}")
        
        return self

9. 可解释性与误报管理

在金融等高风险领域,异常检测结果的可解释性至关重要:

class AnomalyExplainer:
    """异常检测结果解释器"""
    
    def __init__(self, feature_names: list):
        self.feature_names = feature_names
    
    def explain_point(self, x: np.ndarray, x_normal_mean: np.ndarray, 
                      x_normal_std: np.ndarray) -> dict:
        """解释单个样本为什么被判定为异常"""
        z_scores = np.abs((x - x_normal_mean) / (x_normal_std + 1e-8))
        
        # 按异常程度排序
        sorted_indices = np.argsort(z_scores)[::-1]
        
        explanations = []
        for idx in sorted_indices[:5]:  # 取前5个最异常的特征
            if z_scores[idx] > 1.5:  # 只报告显著异常的特征
                direction = "高于" if x[idx] > x_normal_mean[idx] else "低于"
                explanations.append({
                    "feature": self.feature_names[idx],
                    "value": float(x[idx]),
                    "normal_mean": float(x_normal_mean[idx]),
                    "z_score": float(z_scores[idx]),
                    "description": (
                        f"{self.feature_names[idx]} = {x[idx]:.2f}, "
                        f"比正常均值{direction} {z_scores[idx]:.1f}个标准差"
                    ),
                })
        
        return {
            "is_anomaly": any(e["z_score"] > 3 for e in explanations),
            "overall_score": float(np.max(z_scores)),
            "top_explanations": explanations,
        }
    
    def generate_report(self, X_anomalies: np.ndarray, 
                        X_normal_mean: np.ndarray,
                        X_normal_std: np.ndarray) -> str:
        """生成异常检测报告"""
        report_lines = ["=== 异常检测报告 ===\n"]
        
        for i, x in enumerate(X_anomalies):
            result = self.explain_point(x, X_normal_mean, X_normal_std)
            report_lines.append(f"样本 #{i+1}:")
            report_lines.append(f"  异常分数: {result['overall_score']:.2f}")
            report_lines.append(f"  主要原因:")
            for exp in result["top_explanations"]:
                report_lines.append(f"    - {exp['description']}")
            report_lines.append("")
        
        return "\n".join(report_lines)

# 使用示例
feature_names = [
    "金额", "金额偏离度", "小时", "星期几", "距离常驻地",
    "距上次交易间隔", "1h交易次数", "24h交易次数",
    "24h商户数", "是否境外", "是否线上", "商户风险分"
]

explainer = AnomalyExplainer(feature_names)
normal_mean = np.mean(X[y == 0], axis=0)
normal_std = np.std(X[y == 0], axis=0)

# 解释一个异常样本
anomaly_sample = X[y == 1][0]
explanation = explainer.explain_point(anomaly_sample, normal_mean, normal_std)
for exp in explanation["top_explanations"]:
    print(f"  {exp['description']}")

误报管理策略

降低误报率是欺诈检测系统的核心挑战:

class FalsePositiveManager:
    """误报管理器"""
    
    def __init__(self):
        self.feedback_log = []
        self.whitelist = set()
        self.threshold_adjustments = {}
    
    def add_feedback(self, transaction_id: str, predicted: int, 
                     actual: int, reason: str = ""):
        """记录人工审核反馈"""
        self.feedback_log.append({
            "tx_id": transaction_id,
            "predicted": predicted,
            "actual": actual,
            "correct": predicted == actual,
            "reason": reason,
        })
    
    def analyze_fp_patterns(self) -> dict:
        """分析误报模式"""
        fps = [f for f in self.feedback_log 
               if f["predicted"] == 1 and f["actual"] == 0]
        
        if not fps:
            return {"total_fp": 0, "patterns": []}
        
        # 分析误报原因分布
        reason_counts = {}
        for fp in fps:
            reason = fp.get("reason", "unknown")
            reason_counts[reason] = reason_counts.get(reason, 0) + 1
        
        return {
            "total_fp": len(fps),
            "total_feedback": len(self.feedback_log),
            "fp_rate": len(fps) / max(len(self.feedback_log), 1),
            "top_reasons": sorted(reason_counts.items(), 
                                  key=lambda x: x[1], reverse=True)[:5],
        }
    
    def adaptive_threshold(self, target_precision: float = 0.95) -> float:
        """根据反馈自适应调整阈值"""
        if len(self.feedback_log) < 100:
            return 0.5  # 数据不足时使用默认阈值
        
        scores = []
        labels = []
        for fb in self.feedback_log:
            scores.append(fb.get("score", 0.5))
            labels.append(fb["actual"])
        
        scores = np.array(scores)
        labels = np.array(labels)
        
        # 搜索满足目标精确率的最优阈值
        best_threshold = 0.5
        for threshold in np.arange(0.1, 1.0, 0.01):
            preds = (scores > threshold).astype(int)
            if preds.sum() == 0:
                continue
            precision = sum((preds == 1) & (labels == 1)) / max(preds.sum(), 1)
            if precision >= target_precision:
                best_threshold = threshold
                break
        
        return best_threshold

10. 实战案例:交易欺诈检测系统

下面整合前面所有模块,构建一个完整的交易欺诈检测系统:

import json
import time
from datetime import datetime

class TransactionFraudSystem:
    """交易欺诈检测完整系统"""
    
    def __init__(self):
        self.rule_engine = RuleEngine()
        self.ml_model = None  # 训练好的ML模型
        self.feature_store = {}  # 用户特征存储
        self.alert_queue = []
        self.stats = {"total": 0, "flagged": 0, "confirmed_fraud": 0}
    
    def process_transaction(self, tx: dict) -> dict:
        """处理单笔交易的完整流程"""
        self.stats["total"] += 1
        user_id = tx["user_id"]
        
        # 1. 更新用户特征
        self._update_features(user_id, tx)
        features = self._get_features(user_id, tx)
        
        # 2. 规则引擎(快速过滤)
        rule_result = self.rule_engine.evaluate(tx, features)
        if rule_result["blocked"]:
            return self._create_alert(tx, "rule", rule_result, score=1.0)
        
        # 3. ML模型评分
        ml_score = self._ml_score(features)
        
        # 4. 综合决策
        final_score = 0.6 * ml_score + 0.4 * rule_result.get("risk_score", 0)
        
        if final_score > 0.8:
            return self._create_alert(tx, "ml+rule", {
                "ml_score": ml_score, 
                "rule_score": rule_result.get("risk_score", 0),
                "final_score": final_score,
            }, score=final_score)
        elif final_score > 0.5:
            return {"action": "review", "score": final_score, "tx_id": tx["id"]}
        else:
            return {"action": "approve", "score": final_score, "tx_id": tx["id"]}
    
    def _update_features(self, user_id: str, tx: dict):
        """实时更新用户特征"""
        if user_id not in self.feature_store:
            self.feature_store[user_id] = {
                "tx_history": [],
                "total_amount_24h": 0,
                "tx_count_1h": 0,
                "tx_count_24h": 0,
                "unique_merchants": set(),
                "last_tx_time": 0,
            }
        
        store = self.feature_store[user_id]
        now = time.time()
        
        store["tx_history"].append(tx)
        store["total_amount_24h"] += tx["amount"]
        store["tx_count_24h"] += 1
        store["tx_count_1h"] += 1
        store["unique_merchants"].add(tx.get("merchant_id", ""))
        store["last_tx_time"] = now
    
    def _get_features(self, user_id: str, tx: dict) -> np.ndarray:
        """提取特征向量"""
        store = self.feature_store[user_id]
        features = [
            tx["amount"],
            store["total_amount_24h"],
            store["tx_count_1h"],
            store["tx_count_24h"],
            len(store["unique_merchants"]),
            time.time() - store["last_tx_time"],
            int(tx.get("is_foreign", 0)),
            int(tx.get("is_online", 0)),
        ]
        return np.array(features).reshape(1, -1)
    
    def _ml_score(self, features: np.ndarray) -> float:
        """ML模型评分"""
        if self.ml_model is None:
            return 0.0
        proba = self.ml_model.predict_proba(features)[0][1]
        return float(proba)
    
    def _create_alert(self, tx: dict, source: str, details: dict, 
                      score: float) -> dict:
        """创建告警"""
        self.stats["flagged"] += 1
        alert = {
            "alert_id": f"ALT-{self.stats['flagged']:06d}",
            "tx_id": tx["id"],
            "user_id": tx["user_id"],
            "amount": tx["amount"],
            "score": score,
            "source": source,
            "details": details,
            "timestamp": datetime.now().isoformat(),
            "status": "pending",
        }
        self.alert_queue.append(alert)
        return {"action": "block", "alert": alert}


class RuleEngine:
    """规则引擎"""
    
    def __init__(self):
        self.rules = [
            self._check_amount_limit,
            self._check_frequency,
            self._check_time_anomaly,
        ]
    
    def evaluate(self, tx: dict, features: np.ndarray) -> dict:
        risk_score = 0
        reasons = []
        
        for rule in self.rules:
            result = rule(tx, features)
            if result["triggered"]:
                risk_score += result["weight"]
                reasons.append(result["reason"])
        
        return {
            "blocked": risk_score > 0.9,
            "risk_score": min(risk_score, 1.0),
            "reasons": reasons,
        }
    
    def _check_amount_limit(self, tx: dict, features: np.ndarray) -> dict:
        if tx["amount"] > 50000:
            return {"triggered": True, "weight": 0.5, 
                    "reason": f"单笔金额 {tx['amount']} 超过5万"}
        return {"triggered": False, "weight": 0, "reason": ""}
    
    def _check_frequency(self, tx: dict, features: np.ndarray) -> dict:
        if features[0][2] > 10:  # 1小时内超过10笔
            return {"triggered": True, "weight": 0.4, 
                    "reason": "1小时内交易次数过多"}
        return {"triggered": False, "weight": 0, "reason": ""}
    
    def _check_time_anomaly(self, tx: dict, features: np.ndarray) -> dict:
        hour = datetime.now().hour
        if hour >= 1 and hour <= 5:
            return {"triggered": True, "weight": 0.3, 
                    "reason": "凌晨时段交易"}
        return {"triggered": False, "weight": 0, "reason": ""}


# 运行示例
system = TransactionFraudSystem()
test_transactions = [
    {"id": "TX001", "user_id": "U100", "amount": 200, "is_online": 1},
    {"id": "TX002", "user_id": "U100", "amount": 80000, "is_foreign": 1},
    {"id": "TX003", "user_id": "U101", "amount": 50, "is_online": 0},
]

for tx in test_transactions:
    result = system.process_transaction(tx)
    print(f"交易 {tx['id']}: {result['action']}, "
          f"分数: {result.get('score', 'N/A'):.2f}")

11. 模型监控与更新策略

部署后的异常检测模型需要持续监控和更新,以应对概念漂移和新型攻击:

class ModelMonitor:
    """异常检测模型监控器"""
    
    def __init__(self, window_size=10000):
        self.window_size = window_size
        self.predictions = []
        self.ground_truth = []
        self.feature_distributions = {}
        self.alert_history = []
    
    def log_prediction(self, features: np.ndarray, prediction: int, 
                       score: float, actual: int = None):
        """记录预测结果"""
        self.predictions.append({
            "prediction": prediction,
            "score": score,
            "actual": actual,
            "timestamp": time.time(),
            "features": features.tolist(),
        })
        
        if actual is not None:
            self.ground_truth.append({
                "predicted": prediction,
                "actual": actual,
                "timestamp": time.time(),
            })
    
    def check_data_drift(self, reference_data: np.ndarray, 
                         current_data: np.ndarray, 
                         threshold=0.1) -> dict:
        """检测数据漂移(使用PSI - Population Stability Index)"""
        drift_results = {}
        
        for col in range(reference_data.shape[1]):
            # 计算PSI
            ref_hist, bins = np.histogram(reference_data[:, col], bins=20)
            cur_hist, _ = np.histogram(current_data[:, col], bins=bins)
            
            # 归一化
            ref_pct = ref_hist / ref_hist.sum() + 1e-8
            cur_pct = cur_hist / cur_hist.sum() + 1e-8
            
            psi = np.sum((cur_pct - ref_pct) * np.log(cur_pct / ref_pct))
            drift_results[f"feature_{col}"] = {
                "psi": float(psi),
                "drifted": psi > threshold,
            }
        
        total_drifted = sum(1 for v in drift_results.values() if v["drifted"])
        return {
            "features": drift_results,
            "total_drifted": total_drifted,
            "needs_retrain": total_drifted > len(drift_results) * 0.3,
        }
    
    def performance_report(self) -> dict:
        """生成性能报告"""
        labeled = [p for p in self.predictions if p["actual"] is not None]
        
        if not labeled:
            return {"status": "no_labeled_data"}
        
        y_true = [p["actual"] for p in labeled]
        y_pred = [p["prediction"] for p in labeled]
        
        tp = sum(1 for t, p in zip(y_true, y_pred) if t == 1 and p == 1)
        fp = sum(1 for t, p in zip(y_true, y_pred) if t == 0 and p == 1)
        fn = sum(1 for t, p in zip(y_true, y_pred) if t == 1 and p == 0)
        tn = sum(1 for t, p in zip(y_true, y_pred) if t == 0 and p == 0)
        
        precision = tp / max(tp + fp, 1)
        recall = tp / max(tp + fn, 1)
        f1 = 2 * precision * recall / max(precision + recall, 1e-8)
        
        return {
            "total_labeled": len(labeled),
            "precision": precision,
            "recall": recall,
            "f1": f1,
            "confusion_matrix": {"tp": tp, "fp": fp, "fn": fn, "tn": tn},
            "alert_rate": (tp + fp) / max(len(labeled), 1),
        }
    
    def should_retrain(self) -> dict:
        """判断是否需要重新训练"""
        reasons = []
        
        # 检查性能下降
        report = self.performance_report()
        if report.get("f1", 1.0) < 0.7:
            reasons.append(f"F1分数下降至 {report['f1']:.3f}")
        
        if report.get("precision", 1.0) < 0.8:
            reasons.append(f"精确率下降至 {report['precision']:.3f}")
        
        # 检查误报率上升
        if report.get("alert_rate", 0) > 0.1:
            reasons.append(f"告警率过高: {report['alert_rate']:.2%}")
        
        return {
            "should_retrain": len(reasons) > 0,
            "reasons": reasons,
            "current_performance": report,
        }


class AutoRetrainer:
    """自动重训练管理器"""
    
    def __init__(self, model_class, monitor: ModelMonitor):
        self.model_class = model_class
        self.monitor = monitor
        self.model_versions = []
        self.current_version = 0
    
    def retrain(self, X_new: np.ndarray, y_new: np.ndarray, 
                X_old: np.ndarray = None, y_old: np.ndarray = None):
        """重训练模型"""
        self.current_version += 1
        
        # 合并新旧数据(可配置比例)
        if X_old is not None:
            # 旧数据采样,避免数据量过大
            old_sample_idx = np.random.choice(
                len(X_old), size=min(len(X_old), len(X_new) * 3), replace=False
            )
            X_combined = np.vstack([X_old[old_sample_idx], X_new])
            y_combined = np.hstack([y_old[old_sample_idx], y_new])
        else:
            X_combined = X_new
            y_combined = y_new
        
        # 训练新模型
        new_model = self.model_class()
        new_model.fit(X_combined, y_combined)
        
        # 评估新模型
        from sklearn.metrics import f1_score
        new_preds = new_model.predict(X_combined)
        new_f1 = f1_score(y_combined, new_preds)
        
        version_info = {
            "version": self.current_version,
            "model": new_model,
            "f1": new_f1,
            "train_size": len(X_combined),
            "timestamp": datetime.now().isoformat(),
        }
        self.model_versions.append(version_info)
        
        print(f"模型 v{self.current_version} 训练完成, F1={new_f1:.3f}")
        return new_model

# 监控与重训练流程示例
from sklearn.ensemble import RandomForestClassifier

monitor = ModelMonitor()
retrainer = AutoRetrainer(RandomForestClassifier, monitor)

# 模拟生产环境监控
for i in range(100):
    features = np.random.randn(1, 12)
    prediction = np.random.choice([0, 1], p=[0.95, 0.05])
    score = np.random.random()
    actual = np.random.choice([0, 1], p=[0.95, 0.05])
    monitor.log_prediction(features, prediction, score, actual)

# 检查是否需要重训练
decision = monitor.should_retrain()
print(f"是否需要重训练: {decision['should_retrain']}")
if decision["should_retrain"]:
    print(f"原因: {decision['reasons']}")

异常检测与欺诈识别是一个持续演进的领域。随着深度学习和图计算技术的发展,检测精度和实时性不断提升。在实际业务中,最关键的不是选择最复杂的模型,而是建立完善的数据流水线、监控体系和反馈机制,让系统能够在对抗环境中持续进化。

内容声明

本文内容为AI技术学习教程,仅供学习参考。如涉及技术问题,欢迎通过 xurj005@163.com 与我们交流。

目录