
标签相似度计算的实战与应用(含矩阵优化与代码)
摘要
在实际的文本分类与标签体系维护中,常常需要对不同模型输出的标签进行比较:这些标签是否一致?不一致时语义上是否接近?某些标签样本数量过少会影响质量……本文基于一个真实工作流示例,介绍如何使用 sentence-transformers(SBERT)、pandas 和少量实用工具来计算标签语义相似度、判断一致性、统计分布,并提供完整的可运行代码(含 GPU 自动检测、进度条、以及高效的矩阵优化实现),最后把文章与代码打包为可发布的 HTML 格式,便于复制到博客或知识库。
目录
- 为什么要做标签相似度计算
- 标签表示方法回顾
- 相似度计算方法
- 实战场景与数据说明
- 完整实现(含进度条与 GPU 支持)
- 矩阵优化版(高性能对角线提取)—— 附录 A
- 运行结果与分析
- 实践经验与优化建议
- 结语
一、为什么要做标签相似度计算
当你在工程中使用多个分类器(如传统的 TF-IDF + LinearSVC 与现代的 SBERT + LinearSVC)时,常会遇到标签不一致的情况。标签相似度计算可以帮助我们:
- 判断不同模型预测的不一致是否具有语义上的接近性;
- 识别低质量、样本稀少的标签,以便合并或重新标注;
- 在自动化系统中做软合并(例如当相似度 > 阈值时把两个标签视为可合并);
- 为后续错误分析与人工审核提供可视化线索。
二、标签表示方法回顾
把标签文字转换为向量是关键。常见方案:
- TF-IDF:传统,稀疏向量,解释性强,但难以捕捉同义与近义;
- 词向量(Word2Vec/GloVe):能捕捉到一些词义,但对短文本(如标签)需要合适聚合;
- SBERT(Sentence-BERT):对短文本、句子、标签的向量化效果好,直接用于余弦相似度计算非常合适。
三、相似度计算方法
本文使用的核心指标是 余弦相似度(cosine similarity),因为在高维语义向量中,余弦相似度能更稳定地反映角度相似性:
cosine_sim(a, b) = (a · b) / (||a|| * ||b||)
取值范围通常为 [-1, 1],在现代的 SBERT 向量上,多数语义相关的文本会得到 [0.5, 1.0] 的相似度,近义词/同义词接近 0.8 – 1.0。
四、实战场景与数据说明
示例数据为一个 Excel 文件(路径示例):
D:\dev\Python\lib_sbert_analysis.xlsx
关键列:
Book_Name:标题或书名;TFIDF_LinearSVC:基于 TF-IDF 的模型预测标签;SBERT_LinearSVC384:基于 SBERT-384 特征的模型预测标签。
目标:对每一行计算这两个标签的语义相似度(SBERT),判断是否相等,并将 TFIDF 标签的全局出现次数写回到该行,最终把结果保存到含两个 sheet 的 Excel。
五、完整实现(含进度条与 GPU 支持)
下面是一个可直接运行的完整版脚本。它具有:
- GPU 自动检测并加速编码(若安装了 CUDA 版 PyTorch);
- embedding 编码进度条;
- 逐行相似度计算进度条;
- TFIDF 标签计数映射进度条;
- 输出 Excel(两个 sheet:数据和标签分布)。
代码:完整版(含进度条)
import pandas as pd
from sentence_transformers import SentenceTransformer, util
from collections import Counter
import torch
from tqdm import tqdm
# 参数
excel_path = r"D:\dev\Python\CsvLib\lib_t_sbert_analysis1.xlsx"
output_path = excel_path.replace(".xlsx", "_with_analysis.xlsx")
# GPU 优先
device = "cuda" if torch.cuda.is_available() else "cpu"
print(f"正在使用设备: {device}")
model = SentenceTransformer("all-MiniLM-L6-v2", device=device)
# 读取 Excel
df = pd.read_excel(excel_path)
# 检查列
required_cols = ["TFIDF_LinearSVC", "SBERT_LinearSVC384"]
for col in required_cols:
if col not in df.columns:
raise ValueError(f"缺少必要的列: {col}")
# 准备标签列表
tfidf_labels = df["TFIDF_LinearSVC"].astype(str).tolist()
sbert_labels = df["SBERT_LinearSVC384"].astype(str).tolist()
# 编码(带进度)
print("\n编码 TFIDF 标签中...")
emb_tfidf = model.encode(tfidf_labels, convert_to_tensor=True, device=device, show_progress_bar=True)
print("\n编码 SBERT 标签中...")
emb_sbert = model.encode(sbert_labels, convert_to_tensor=True, device=device, show_progress_bar=True)
# 逐行计算相似度(显示进度)
print("\n计算相似度中...")
similarities = []
for i in tqdm(range(len(tfidf_labels)), desc="逐行相似度计算"):
sim = util.cos_sim(emb_tfidf[i], emb_sbert[i]).item()
similarities.append(sim)
# 标签是否一致
same_label = [t == s for t, s in zip(tfidf_labels, sbert_labels)]
# 统计 TFIDF 标签分布并映射(带进度)
print("\n统计 TFIDF 标签分布...")
from collections import Counter
tfidf_label_counts = Counter(tfidf_labels)
from tqdm import tqdm
tqdm.pandas(desc="映射标签计数")
df["TFIDF_Label_Count"] = df["TFIDF_LinearSVC"].progress_map(tfidf_label_counts.get)
# 添加列并保存
df["Label_Similarity"] = similarities
df["Label_Equal"] = same_label
print("\n写入 Excel 文件...")
with pd.ExcelWriter(output_path, engine="openpyxl") as writer:
df.to_excel(writer, sheet_name="Data_With_Analysis", index=False)
pd.DataFrame.from_dict(tfidf_label_counts, orient="index", columns=["Count"]) \
.sort_values("Count", ascending=False) \
.to_excel(writer, sheet_name="TFIDF_Label_Distribution")
print(f"\n✅ 结果已保存到: {output_path}")
util.cos_sim 会有开销,推荐直接使用矩阵操作取对角线(见附录 A 的矩阵优化代码),这是最快的方式。六、矩阵优化版(高性能对角线提取) — 附录 A
当数据量很大时,逐条调用 util.cos_sim(emb_tfidf[i], emb_sbert[i]) 会产生大量小矩阵乘法的开销。更高效的做法是:
- 把所有 TFIDF 标签向量批量编码成一个矩阵(N x D);
- 把所有 SBERT 标签向量批量编码成一个矩阵(N x D);
- 一次性计算相似度矩阵
S = cos_sim(emb_tfidf, emb_sbert)(返回 N x N 矩阵); - 取对角线
diag(S),每个元素就是对应行的相似度。
这种方法把大量重复的内存/赋值/函数调用合并为一次矩阵乘法,能够充分发挥 GPU 的并行能力。
矩阵优化代码(附录)
import pandas as pd
from sentence_transformers import SentenceTransformer, util
import torch
excel_path = r"D:\dev\Python\CsvLib\lib31w7_t_sbert_analysis1.xlsx"
output_path = excel_path.replace('.xlsx', '_with_analysis_matrix_opt.xlsx')
device = 'cuda' if torch.cuda.is_available() else 'cpu'
print('device =', device)
model = SentenceTransformer('all-MiniLM-L6-v2', device=device)
df = pd.read_excel(excel_path)
if 'TFIDF_LinearSVC' not in df.columns or 'SBERT_LinearSVC384' not in df.columns:
raise ValueError('缺少必要列')
# 准备文本
A = df['TFIDF_LinearSVC'].astype(str).tolist()
B = df['SBERT_LinearSVC384'].astype(str).tolist()
# 批量编码为张量(N x D)
emb_A = model.encode(A, convert_to_tensor=True, device=device, show_progress_bar=True)
emb_B = model.encode(B, convert_to_tensor=True, device=device, show_progress_bar=True)
# 计算完整相似度矩阵(N x N),然后取对角线
# 注意:如果 N 非常大(例如 > 200k),这个矩阵会占用大量 GPU/CPU 内存。
S = util.cos_sim(emb_A, emb_B) # 返回 torch 稀疏/密集张量
similarities = S.diagonal().cpu().tolist()
# 写回 DataFrame
df['Label_Similarity'] = similarities
# 其余部分(标签一致判断、计数、保存)同之前示例
from collections import Counter
df['Label_Equal'] = (df['TFIDF_LinearSVC'].astype(str) == df['SBERT_LinearSVC384'].astype(str))
counts = Counter(df['TFIDF_LinearSVC'].astype(str))
df['TFIDF_Label_Count'] = df['TFIDF_LinearSVC'].astype(str).map(counts.get)
# 保存
with pd.ExcelWriter(output_path, engine='openpyxl') as writer:
df.to_excel(writer, sheet_name='Data_With_Analysis', index=False)
pd.DataFrame.from_dict(counts, orient='index', columns=['Count']).sort_values('Count', ascending=False).to_excel(writer, sheet_name='TFIDF_Label_Distribution')
print('saved ->', output_path)
内存提醒:矩阵法是计算速度最快的方式,但同时需要较多的内存(尤其是当 N 很大时,N x N 的相似度矩阵会非常大)。在内存受限的情况下,可以把数据分成若干批次(batch)来计算对角线。
七、运行结果与分析(示例)
在一个中等规模的数据集(例如 300k 行)的试验中:
- 使用逐行相似度的方式在 CPU 上耗时较长(分钟级到十几分钟);
- 若在 GPU 上使用矩阵优化,一次性编码 + 取对角线可将相似度计算时间降低 5-10x(视显存与 batch 策略而定);
- 通过查看
TFIDF_Label_Count列,可以快速识别出样本数极少的标签(例如 < 20),这些标签通常需要合并或重标注; - 平均相似度与一致率是衡量两套模型预测一致性的简单指标:当平均相似度高且一致率低时,说明模型给出了语义相近但不同的标签,适合考虑标签合并或标签重命名。
八、实践经验与优化建议
- 优先使用 GPU 批量编码:对短文本(标签)做批量 encode 可以显著节省时间;
- 矩阵法提速,但要注意内存:当显存/内存足够时优先使用矩阵对角线提取;否则采用分 batch 的方式;
- 阈值策略:在工程中设定相似度阈值(例如 0.8)来判定“可合并”标签;
- 标签清理规则:建议对出现次数 < 10 或 < 20 的标签集中检查并合并;
- 日志与进度:大数据量任务务必有进度条和日志记录,便于排查与中断恢复;
- 批量化写入:避免频繁小文件写入,使用一次性写入 Excel 或 CSV 更高效;
九、结语
本文提供了一套从思路到实践的完整流程:如何把不同模型的标签进行语义比对、如何把统计信息写回到原始数据、以及在大规模场景下如何使用矩阵优化来提速。文章附带了两版可运行代码(逐行版与矩阵优化版)。