如何加速spaCy多标签文本分类器的评估计算?
我基于spaCy构建了一个多标签文本分类器,现在评估过程耗时极长。排查后发现,耗时的环节不是模型预测,而是log loss、precision、recall及fscore的计算。
以下是我的评估代码:
from sklearn.metrics import precision_recall_fscore_support from sklearn.metrics import log_loss import time import numpy as np def evaluate( nlp, texts, cats, labels, threshold=0.3, beta=0.5, batch_size=8 ): t0 = time.time() docs = nlp.pipe(texts, batch_size=batch_size) t1 = time.time() pred_probs = np.array([list(doc.cats.values()) for doc in docs]) avg_log_loss = log_loss(cats, pred_probs) results = {'log_loss': avg_log_loss} y_pred = pred_probs > threshold prc, rec, fscore, _ = precision_recall_fscore_support( y_true=cats, y_pred=y_pred, beta=beta, average='micro', warn_for=set() ) results[f'f{beta}_{threshold}'] = fscore results[f'prc_{threshold}'] = prc results[f'rec_{threshold}'] = rec t2 = time.time() print(f"Used {t1-t0} on predicting; {t2-t1} on scoring") return results
运行后输出:
Used 4.76837158203125e-06 on predicting; 377.1225287914276 on scoring
请问有什么方法可以加速这些评分指标的计算?
你的模型预测速度快得离谱,但评分计算直接拖垮了整个流程——这很常见,毕竟scikit-learn的内置指标函数是通用型设计,要兼容各种场景,难免在特定需求下有冗余开销。这里有几个针对性的优化方案,我自己在处理大规模多标签数据时亲测有效:
1. 手动实现micro平均的precision/recall/fscore
precision_recall_fscore_support要处理多类、多标签、不同平均方式等一堆情况,内部分支和检查逻辑很多。而你用的是average='micro',完全可以绕开通用函数,直接用NumPy向量化计算:
# 替换原来的precision_recall_fscore_support调用 y_true = cats y_pred = pred_probs > threshold # 用向量化操作快速统计TP、FP、FN tp = np.sum((y_true == 1) & (y_pred == 1)) fp = np.sum((y_true == 0) & (y_pred == 1)) fn = np.sum((y_true == 1) & (y_pred == 0)) # 计算指标(处理分母为0的边界情况) prc = tp / (tp + fp) if (tp + fp) > 0 else 0.0 rec = tp / (tp + fn) if (tp + fn) > 0 else 0.0 beta_sq = beta ** 2 fscore = (1 + beta_sq) * prc * rec / (beta_sq * prc + rec) if (beta_sq * prc + rec) > 0 else 0.0 # 赋值到结果字典 results[f'f{beta}_{threshold}'] = fscore results[f'prc_{threshold}'] = prc results[f'rec_{threshold}'] = rec
这种方式没有多余的逻辑,完全依赖NumPy的底层优化,数据量越大,速度提升越明显。
2. 手动优化log_loss的计算
sklearn的log_loss同样有很多通用处理(比如类别权重、样本权重),如果你的场景是标准的多标签二分类(每个标签是0/1),可以手动实现更轻量的版本:
epsilon = 1e-15 # 避免log(0)的数值溢出问题 pred_probs_clipped = np.clip(pred_probs, epsilon, 1 - epsilon) # 向量化计算所有样本的log loss,再取平均 avg_log_loss = -(cats * np.log(pred_probs_clipped) + (1 - cats) * np.log(1 - pred_probs_clipped)).mean()
这个实现跳过了sklearn内部的参数检查、权重处理等步骤,纯NumPy运算的速度会快不少。
3. 用稀疏矩阵处理稀疏多标签数据
如果你的多标签数据大部分标签都是0(稀疏场景),把cats和y_pred转换成稀疏矩阵(比如scipy.sparse.csr_matrix),计算时只会处理非零元素,能大幅减少计算量和内存占用:
from scipy.sparse import csr_matrix # 转换为稀疏矩阵 y_true_sparse = csr_matrix(cats) y_pred_sparse = csr_matrix(pred_probs > threshold) # 稀疏矩阵下的TP/FP/FN计算 tp = (y_true_sparse.multiply(y_pred_sparse)).sum() fp = y_pred_sparse.sum() - tp fn = y_true_sparse.sum() - tp
稀疏矩阵的运算在处理稀疏数据时,比密集数组快几个数量级,尤其适合标签数量多的场景。
4. 确保数组是连续存储的
NumPy对连续存储的数组(C-contiguous)运算效率更高,如果你的cats或pred_probs是从列表转换来的非连续数组,先转成连续数组:
pred_probs = np.ascontiguousarray(pred_probs) cats = np.ascontiguousarray(cats)
这个小操作能让后续的向量化运算跑得更顺畅。
5. 分块处理超大规模数据
如果数据大到内存装不下,可以把数据分成小块,分别计算每个块的TP/FP/FN和log loss,最后汇总结果:
比如计算log loss:
chunk_size = 10000 # 按需调整 chunk 大小 total_loss = 0.0 n_samples = len(cats) epsilon = 1e-15 for i in range(0, n_samples, chunk_size): chunk_cats = cats[i:i+chunk_size] chunk_probs = pred_probs[i:i+chunk_size] chunk_probs_clipped = np.clip(chunk_probs, epsilon, 1 - epsilon) chunk_loss = -(chunk_cats * np.log(chunk_probs_clipped) + (1 - chunk_cats) * np.log(1 - chunk_probs_clipped)).sum() total_loss += chunk_loss avg_log_loss = total_loss / n_samples
TP/FP/FN也可以分块累加,最后再计算指标,既省内存又能避免一次性处理大数组的性能瓶颈。
内容的提问来源于stack exchange,提问作者tsorn




