余弦相似度计算过慢,如何优化语义搜索中的相似度函数?
优化固定向量集的语义搜索余弦相似度计算速度
嘿,我看你遇到的问题是固定300条句子的语义搜索太慢,瓶颈在余弦相似度计算,而且用numba的版本结果不准对吧?刚好你的场景是向量固定,这有几个针对性的优化方案,肯定能把10-12秒的耗时压下来:
方案1:预计算固定向量的模长(最直接的减重复计算)
你现在的代码每次循环都要计算句子向量的模长np.linalg.norm(v2),但300条句子的向量是固定的啊!这完全是重复劳动,预计算一次就能省掉每次查询的300次模长计算。
操作步骤:
- 提前运行一次模长计算,把结果存起来(只需要跑一次,因为向量不变):
import numpy as np # vectors是你的(300,500)向量矩阵 vec_norms = np.linalg.norm(vectors, axis=1)
- 修改你的余弦相似度函数和搜索函数,直接用预计算好的模长:
def cosine_similarity(v1, v2, v2_norm): mag1 = np.linalg.norm(v1) if (not mag1) or (not v2_norm): return 0 return np.dot(v1, v2) / (mag1 * v2_norm) def semantic_search(cleaned_query, data, vectors, vec_norms): query_vec = get_features(cleaned_query)[0].ravel() query_norm = np.linalg.norm(query_vec) res = [] for i, d in enumerate(data): qvec = vectors[i].ravel() sim = cosine_similarity(query_vec, qvec, vec_norms[i]) if sim > 0.5: # 注意:原来的排序用字符串会有问题,转成float再排序更准确 res.append((format(sim * 100, '.2f'), data[i])) return sorted(res, key=lambda x: float(x[0]), reverse=True)[:15]
方案2:用numpy向量化计算,彻底干掉Python循环(最推荐)
Python的for循环本身就慢,numpy的向量化运算底层是C实现的,批量处理300个向量的相似度会快到离谱,直接把耗时从秒级降到毫秒级。
优化后的代码:
# 预计算模长(仅一次) vec_norms = np.linalg.norm(vectors, axis=1) def semantic_search(cleaned_query, data, vectors, vec_norms): query_vec = get_features(cleaned_query)[0].ravel() query_norm = np.linalg.norm(query_vec) # 处理查询向量为0的特殊情况 if query_norm == 0: return [] # 批量计算所有点积和相似度,不用循环! dot_products = np.dot(vectors, query_vec) similarities = dot_products / (query_norm * vec_norms) # 筛选相似度>0.5的结果 valid_mask = similarities > 0.5 valid_indices = np.where(valid_mask)[0] valid_similarities = similarities[valid_mask] valid_sentences = [data[i] for i in valid_indices] # 按相似度降序排序,取前15个 sorted_results = sorted(zip(valid_similarities, valid_sentences), key=lambda x: x[0], reverse=True)[:15] # 转成你需要的百分比格式 return [(format(sim * 100, '.2f'), sent) for sim, sent in sorted_results]
这个方案的结果和你原来的cosine_similarity完全一致,但速度提升是数量级的,因为完全避开了Python循环的开销。
方案3:修复numba版本的余弦相似度函数(如果想保留numba)
你之前的nb_cosine结果不准是因为它返回的是余弦距离(1-相似度),而不是你需要的余弦相似度!改一下返回值就好了:
import numba as nb import numpy as np @nb.jit(nopython=True, fastmath=True) def nb_cosine(x, y): xx, yy, xy = 0.0, 0.0, 0.0 for i in range(len(x)): xx += x[i] * x[i] yy += y[i] * y[i] xy += x[i] * y[i] norm_product = np.sqrt(xx * yy) if norm_product == 0: return 0.0 # 这里返回的是相似度,不是距离 return xy / norm_product
修复后再结合方案1的预计算模长,速度也会比原来的Python循环快很多,而且结果准确。
额外小优化:缓存查询向量的编码结果
你的get_features每次都要创建TensorFlow Session并初始化变量,这也有一定开销。如果同一个查询可能被重复搜索,可以用lru_cache缓存结果:
from functools import lru_cache # 确保graph是全局变量,否则缓存可能失效 @lru_cache(maxsize=100) def get_features(texts): if type(texts) is str: texts = [texts] with tf.Session(graph=graph) as sess: sess.run([tf.global_variables_initializer(), tf.tables_initializer()]) return sess.run(embed(texts))
如果查询都是唯一的,这个优化作用不大,但聊胜于无。
内容的提问来源于stack exchange,提问作者Jamik




