Author InformationName: Shutter Zor(左祥太)Email: Shutter_Z@outlook.com
我将在本推文中再次介绍如何使用 Python 计算文本相似度指标,除简单计算外,还涉及传统方法与大语言模型的比较。
我个人认为文本相似度还有非常大的写作空间,同时也亟待发掘新的、富含经济意义的指标。文本相似度指标在近期的顶刊也非常活跃,比如有两个典型的例子:
罗进辉等使用的是传统的相似度计算方法,蔡跃洲等后者使用的是 BERT 的 Embedding 功能。
下列文本为小米公司的几份专利摘要,具体如下:
# 专利摘要文本1,申请号:CN202480029985.2patent1 = "本公开涉及通信方法、通信设备、通信系统及存储介质。通信方法包括:AF向第一网元发送第一信息,所述第一信息用于指示取消A‑IoT设备的周期性操作。本公开通过AF向第一网元发送第一信息,以指示取消A‑IoT设备的周期性操作,从而有效减少A‑IoT设备的能量消耗。"# 专利摘要文本2,申请号:CN202480000774.6patent2 = "本公开涉及通信方法、通信设备、通信系统及存储介质。该方法包括:第一实体接收网络功能服务消费方发送的第一请求,所述第一请求包括网络功能服务提供方相关的第一信息,所述第一信息是基于所述网络功能服务提供方的第二信息进行处理得到的信息,所述网络功能服务消费方位于第一网络,所述网络功能服务提供方位于第二网络;所述第一实体发送第二请求。通过本公开实施例,可以提高通信安全性。"# 专利摘要文本3,申请号:CN202410796819.7patent3 = "本公开涉及一种屏幕的显示方法、装置、设备、存储介质和程序产品,涉及屏幕显示技术领域,方法包括:获取终端生成的待显示数据包,待显示数据包包括目标帧对应的帧同步信号、目标帧中每个像素行对应的行同步信号以及目标显示数据;确定终端的屏幕对应的面板时序;根据面板时序,对行同步信号进行补偿,得到目标行同步信号;根据帧同步信号、目标行同步信号和目标显示数据,生成目标显示信号,并在终端的屏幕上进行显示。这样,根据终端屏幕对应的面板时序对待显示数据包中的行同步信号进行补偿,从而避免了屏幕出现花屏、横屏、亮带等等异常情况,保证了屏幕的显示效果和稳定性,提升了用户体验。"在后续的代码中,我将分别使用传统方法与大语言模型方法介绍文本相似度的计算。
传统方法的具体步骤如下:
具体而言,我这里直接引用 The Journal of Finance 的《Lazy Prices》一文中的例子做演示。

以此类推就能算出“一对文本”之间的相似度。但是需要注意,上面这篇文章取的是布尔向量,即只有 0 和 1 组成的向量。在实践过程中也可以使用简单词频或者 TF-IDF 等方法替换向量中的具体元素,可以参考:【NLP】从向量空间模型到词嵌入模型。
具体代码如下:
import reimport jiebaimport numpy as npimport tqdm as notebook_tqdmfrom sklearn.feature_extraction.text import TfidfVectorizerfrom sklearn.metrics.pairwise import cosine_similarityfrom sentence_transformers import SentenceTransformer# 1. 加载停用词defload_stopwords(path="stopwords_full.txt") -> set: stopwords = set()with open(path, "r", encoding="utf-8") as f:for line in f: w = line.strip()if w: stopwords.add(w)return stopwordsSTOPWORDS = load_stopwords()# 2. jieba 分词defsegment_sentence(sentence: str) -> list: words = jieba.cut(sentence, cut_all=False)return [w.strip() for w in words if w.strip()]# 3. 去除停用词defremove_stopwords(words: list) -> list:return [w for w in words if w notin STOPWORDS]# 4. 去除标点符号defremove_punctuation(words: list) -> str: joined = " ".join(words) cleaned = re.sub(r"[^0-9A-Za-z\u4e00-\u9fa5\s]", " ", joined) cleaned = re.sub(r"\s+", " ", cleaned).strip()return cleaned# 5. 总流程:文档预处理defpreprocess_document(text: str) -> list:# Step1 分词 words = segment_sentence(text)# Step2 去停用词 words = remove_stopwords(words)# Step3 去标点符号 cleaned_list = remove_punctuation(words)return cleaned_list# 计算代码defcalculate_tfidf_cosine_similarity(text1_words, text2_words):# 确保输入是字符串形式(如果输入是列表,需要转换为字符串)if isinstance(text1_words, list): text1_words = ' '.join(text1_words)if isinstance(text2_words, list): text2_words = ' '.join(text2_words)# 创建TF-IDF向量化器 vectorizer = TfidfVectorizer()# 构建TF-IDF矩阵 tfidf_matrix = vectorizer.fit_transform([text1_words, text2_words])# 计算余弦相似度 similarity = cosine_similarity(tfidf_matrix[0:1], tfidf_matrix[1:2])[0][0]return similarity# 开始计算text1 = preprocess_document(patent1)text2 = preprocess_document(patent2)text3 = preprocess_document(patent3)similarity12 = calculate_tfidf_cosine_similarity(text1, text2)similarity13 = calculate_tfidf_cosine_similarity(text1, text3)print(f"专利1与专利2的相似度: {similarity12:.4f}\n专利1与专利3的相似度: {similarity13:.4f}")最后的结果:
专利1与专利2的相似度: 0.3227专利1与专利3的相似度: 0.0422使用大语言模型计算相似度的方法要简单很多,具体步骤为:
具体而言,这里的原理就是,通过预训练的语言模型将句子转换成统一维度的向量,再直接计算向量的余弦相似度。
具体代码如下:
# 加载模型embedding_model = SentenceTransformer("./bge-base-zh-v1.5")# 向量化embedding = embedding_model.encode([patent1, patent2, patent3])# 计算并打印结果cos12 = embedding_model.similarity(embedding[0], embedding[1])cos13 = embedding_model.similarity(embedding[0], embedding[2])print(f"专利1与专利2的相似度: {cos12.item():.4f}\n专利1与专利3的相似度: {cos13.item():.4f}")得到结果如下:
专利1与专利2的相似度: 0.7394专利1与专利3的相似度: 0.5220在这里我加载了一个预训练的大语言模型,叫做“bge-bash-zh-v1.5”,它来自北京智源人工智能研究院,模型效果比“paraphrase-multilingual-MiniLM-L12-v2”强,是一个专注于中文的通用嵌入模型。除此之外,在这里你也可以加载其他的 Embedding 模型。
其实不难发现,两种方法算出来的相似度虽然存在较大的差异,但在一定程度上也能反映出文本间相似度的不同,整体趋势是对的。
但是传统的方法有一个问题,即当一方文本过短,一方文本过长时,相似度会被长文本稀释。相比之下使用预训练的语言模型就没有这种烦恼,因为都是生成同一维度的向量。