关于AI模型评估的几点记录
如果只能用一句话说AI模型评估:先把失败复现出来。
刚做AI项目的时候,我也犯过同样的毛病:盯着准确率不放,好像这个数字能说明一切。
为什么准确率骗了我好几次
第一次踩坑是在一个文本分类项目上。训练集准确率98%、验证集97%、测试集96%,感觉稳了,直接上线。结果第一天就收到一堆用户反馈:好几个明显的垃圾评论没被过滤掉,反而有些正常评论被误删了。
查了一下数据才发现,数据集里正负样本比例是1:9,模型只要全猜负,准确率就能到90%。它学会了"偷懒",把很少出现的正样本全当噪声处理了。
# 当时用的评估代码,看起来很完美
from sklearn.metrics import accuracy_score, classification_report
y_pred = model.predict(X_test)
print(f"Accuracy: {accuracy_score(y_test, y_pred):.4f}")
print(classification_report(y_test, y_pred))
这串代码能跑出漂亮的数字,但它隐瞒了两件事:样本分布、误报和漏报的代价。邮件过滤系统里,漏掉一封垃圾邮件顶多是用户多删一次;但把重要邮件当成垃圾邮件删了,用户可能直接走人。这两个错误的代价完全不对等。
准确率只在样本均衡、各类别同等重要时才有参考价值。实际项目里,这两种情况很少同时出现。
换一套能看清局面的指标
后来项目里基本都看这组指标:精确率、召回率、F1-score、PR曲线和ROC-AUC。
from sklearn.metrics import precision_score, recall_score, f1_score
from sklearn.metrics import precision_recall_curve, roc_auc_score
import matplotlib.pyplot as plt
y_pred_proba = model.predict_proba(X_test)[:, 1]
precision, recall, thresholds = precision_recall_curve(y_test, y_pred_proba)
# 找一个业务能接受的平衡点
# 比如精确率不低于0.85的情况下,召回率能达到多少
threshold = thresholds[np.argmax(precision >= 0.85)]
y_pred_adj = (y_pred_proba >= threshold).astype(int)
print(f"Precision@threshold={threshold:.3f}: {precision_score(y_test, y_pred_adj):.4f}")
print(f"Recall@threshold={threshold:.3f}: {recall_score(y_test, y_pred_adj):.4f}")
print(f"F1: {f1_score(y_test, y_pred_adj):.4f}")
print(f"ROC-AUC: {roc_auc_score(y_test, y_pred_proba):.4f}")
这组指标比单纯看准确率更诚实。精确率告诉我"模型说它是正的,有多大把握真对",召回率告诉我"真正正的样本里,模型找出来了多少"。业务不同,这两个指标的优先级也不同。
像风控系统,宁可误报也不能漏过真正的风险,召回率优先;像推荐系统,宁可少推几条也不能推用户讨厌的,精确率优先。
PR曲线比单点的数字更有用。曲线下的面积、曲线形状、不同阈值下的精确率-召回率权衡,这些能让我更清楚模型在哪些场景下能用、哪些场景下会出问题。
ROC-AUC有时候会骗人,尤其是数据不平衡的时候。那段时间我也搞混过几次,后来干脆优先看PR曲线,只在确实需要时才参考AUC。
评估集怎么建才不骗人
吃过几次数据泄露的亏之后,我现在建评估集特别小心。
最惨的一次是把时间序列数据随机分成训练集和测试集,结果模型"学会"了用未来预测过去,线下指标特别好,上线直接崩。后来改成按时间切分,把最后一段时间的数据全留作测试,效果才真实。
# 时间序列数据的正确切分方式
import pandas as pd
df['timestamp'] = pd.to_datetime(df['timestamp'])
df = df.sort_values('timestamp')
split_point = int(len(df) * 0.8)
train_df = df.iloc[:split_point]
test_df = df.iloc[split_point:]
# 这样切,模型才不会偷看未来
另一个常见坑是用户级泄露。比如推荐系统里,同一个用户的行为数据如果在训练集和测试集都出现,模型很容易"记住"用户的特征,而不是真正学会推荐策略。
# 用户级切分,防止泄露
from sklearn.model_selection import GroupShuffleSplit
gss = GroupShuffleSplit(n_splits=1, test_size=0.2, random_state=42)
train_idx, test_idx = next(gss.split(df, groups=df['user_id']))
train_df = df.iloc[train_idx]
test_df = df.iloc[test_idx]
现在建评估集时,我通常会检查这几件事:是否按时间切分、是否存在用户级泄露、测试集分布是否和实际一致、有没有包含不应该包含的特征。
A/B测试和线上监控,比离线指标更诚实
离线评估再好,也不等于线上效果。后来项目里都加了A/B测试流程和线上监控。
A/B测试的基本框架大概是这样:
# 简化的A/B测试分流逻辑
import hashlib
def get_group(user_id):
"""根据user_id返回对照组('control')或实验组('treatment')"""
hash_val = int(hashlib.md5(user_id.encode()).hexdigest(), 16)
return 'treatment' if hash_val % 2 == 0 else 'control'
def evaluate_metrics(group):
"""计算各组的业务指标"""
if group == 'control':
return {
'CTR': 0.045,
'conversion_rate': 0.012,
'avg_order_value': 89.5
}
else:
return {
'CTR': 0.052,
'conversion_rate': 0.014,
'avg_order_value': 87.2
}
真实项目里会用更复杂的流量控制和统计检验,但这能说明A/B测试的核心:在同样的用户群体上,对比新旧方案的实际效果。
线上监控也很重要,它能告诉你模型是否在"缓慢退化":
# 简化的线上监控逻辑
def monitor_model(predictions, ground_truth, window=1000):
"""滑动窗口监控模型指标"""
if len(predictions) < window:
return None
recent_preds = predictions[-window:]
recent_truth = ground_truth[-window:]
metrics = {
'accuracy': accuracy_score(recent_truth, recent_preds),
'precision': precision_score(recent_truth, recent_preds),
'recall': recall_score(recent_truth, recent_preds)
}
# 如果指标明显下降,触发告警
if metrics['accuracy'] < 0.85:
send_alert(f"Model performance dropped: {metrics}")
return metrics
我见过模型上线三个月后,效果悄悄下降了15%,但没人发现,直到某天业务方说"最近怎么感觉不太对"。加上监控之后,这种问题能更早发现。
踩过的一些坑和解决方案
这几年踩的坑不少,这里挑几个印象比较深的:
坑1:数据漂移
特征分布变了,模型不适应。比如疫情期间,电商数据里的购买模式突然变了,之前的模型预测全部失效。
解决方法是定期做分布检验,发现漂移就重新训练:
from scipy.stats import ks_2samp
def detect_drift(reference_data, current_data, alpha=0.05):
"""检测特征分布是否发生漂移"""
drift_detected = {}
for feature in reference_data.columns:
statistic, p_value = ks_2samp(
reference_data[feature],
current_data[feature]
)
drift_detected[feature] = p_value < alpha
return drift_detected
坑2:概念漂移
特征分布没变,但特征和目标的关系变了。比如推荐系统里,用户偏好随季节变化,同样的行为在不同时候代表不同的意图。
这个难办一些,通常需要更频繁的模型更新和更短的训练窗口。
坑3:评估集太干净
线上数据总比评估集脏。有些项目里,评估集是经过清洗的"好数据",但线上总会有异常值、缺失值、格式问题。
后来我会在评估集里人工加入一些噪声,让它更接近真实环境:
# 在评估集里加入模拟噪声,测试模型鲁棒性
def add_noise_to_eval(X_eval, noise_level=0.01):
"""添加高斯噪声"""
noise = np.random.normal(0, noise_level, X_eval.shape)
return X_eval + noise
# 模拟缺失值
def add_missing_values(X_eval, missing_rate=0.01):
"""随机设置一些特征为缺失值"""
mask = np.random.random(X_eval.shape) < missing_rate
X_noisy = X_eval.copy()
X_noisy[mask] = np.nan
return X_noisy
坑4:A/B测试流量太小
有一次A/B测试跑了两周,说实验组"显著提升",后来仔细看统计检验,样本量根本不够,那个"显著"是假的。
现在做A/B测试会先算最小样本量,再决定跑多久:
from statsmodels.stats.power import TTestIndPower
def calculate_min_sample_size(effect_size=0.01, alpha=0.05, power=0.8):
"""计算最小样本量"""
analysis = TTestIndPower()
sample_size = analysis.solve_power(
effect_size=effect_size,
alpha=alpha,
power=power,
ratio=1.0
)
return int(sample_size * 2) # 两组都需要这个样本量
# 效果很小的改进需要更多样本才能检测出来
print(f"Minimum sample needed: {calculate_min_sample_size(0.01)}")
一些工具和框架
实际项目里,用这些工具比手写代码省事:
- scikit-learn:基础的指标计算、交叉验证、数据切分
- MLflow:跟踪模型实验、记录指标和参数
- Prometheus + Grafana:线上监控
- Great Expectations:数据质量检查
- Fairlearn:模型公平性评估
MLflow 的记录大概这样用:
import mlflow
with mlflow.start_run():
model.fit(X_train, y_train)
y_pred = model.predict(X_test)
mlflow.log_metric("accuracy", accuracy_score(y_test, y_pred))
mlflow.log_metric("precision", precision_score(y_test, y_pred))
mlflow.log_metric("recall", recall_score(y_test, y_pred))
mlflow.log_param("model_type", type(model).__name__)
mlflow.log_param("n_estimators", model.n_estimators)
这样可以追踪不同实验的指标,比较不同配置的效果,不至于改了参数就忘了之前的结果。
写在后面
模型评估不是一次性的工作,它是一个持续的过程。从数据准备、离线评估、A/B测试到线上监控,每一步都可能藏坑。
准确率只是一个数字,但这个数字背后,有数据分布、业务目标、错误代价、用户场景。真正懂一个模型,要看这些数字背后的故事,而不是只看数字本身。
这几年做下来,一个体会是:没有"万能指标",只有"适合当前场景的指标"。离线指标好不代表线上一定好,线上指标好也不代表模型真的学会了什么。评估的目的不是得到一个漂亮的分数,而是理解模型在什么情况下能用、在什么情况下会翻车。
模型评估说到底,是在帮我们做判断:这个模型能不能上、什么时候需要重新训练、哪里还需要改进。它不是科学,是一门需要经验和判断的手艺。
版权声明: 本文首发于 指尖魔法屋-关于AI模型评估的几点记录(https://blog.thinkmoon.cn/post/999-model-evaluation-practice-metrics-benchmark/) 转载或引用必须申明原指尖魔法屋来源及源地址!
评论
使用 GitHub 账号登录后即可留言,支持 Markdown。