余弦相似度手动实现为 cos(θ) = (A·B) / (||A|| × ||B||),需用 tf.reduce_sum(A*B, axis=-1) 计点积、tf.linalg.norm(..., axis=-1) 算模长,分母加 1e-8 防除零。

用 tf.linalg.norm 和 tf.reduce_sum 手动实现余弦相似度
余弦相似度本质是向量夹角的余弦值,公式为 cos(θ) = (A·B) / (||A|| × ||B||)。TensorFlow 没有直接叫 cosine_similarity 的单函数(注意:tf.keras.losses.cosine_similarity 是 loss,返回的是 1 - cosine 且默认沿 axis=-1 reduce,不直接等价于相似度值),所以推荐手动组合算子更可控。
常见错误是忘记归一化或维度错配导致 InvalidArgumentError: Incompatible shapes。务必确保两个输入张量 shape 一致,且参与点积的轴(通常是特征维)要对齐。
- 若输入是 batched 向量(shape
[batch_size, features]),点积用tf.reduce_sum(A * B, axis=1) - 模长用
tf.linalg.norm(A, axis=1),不能漏掉axis参数,否则会压缩整个 batch - 为避免除零,分母加极小值
1e-8(tf.keras.utils.normalize内部也这么干)
def cosine_similarity_tf(a, b):
dot = tf.reduce_sum(a * b, axis=-1)
norm_a = tf.linalg.norm(a, axis=-1)
norm_b = tf.linalg.norm(b, axis=-1)
return dot / (norm_a * norm_b + 1e-8)
用 tf.keras.losses.cosine_similarity 反推相似度值
这个 API 名字有误导性——它实际计算的是 1 - cosine_similarity,且默认返回 shape [batch_size] 的张量(即沿最后一维 reduce)。如果你硬要用它,得手动翻转:
- 调用时传
axis=-1(默认值,可省略),但结果要1 - result - 注意它在内部做了 L2 归一化,所以输入无需预归一化;但若输入含全零向量,仍可能触发 NaN(因归一化时除零)
- 它不支持
keepdims=True,如需保留维度得自己tf.expand_dims
# 等价于手动实现,但更黑盒 sim = 1 - tf.keras.losses.cosine_similarity(a, b, axis=-1)
处理不同 shape 输入:比如 query 是单向量,doc 是 batch
实际检索场景中,常是一个 query 向量 vs 一堆 doc 向量(shape [1, d] vs [N, d])。直接相乘会 broadcast 失败或结果错位。
立即学习“Python免费学习笔记(深入)”;
正确做法是用 tf.einsum 显式指定求和轴,或 reshape 后点积:
- 推荐
tf.einsum('id,jd->ij', a, b):自动处理广播,返回[1, N]相似度矩阵 - 或把 a 扩展为
[1, d],b 保持[N, d],再用tf.linalg.matmul(a, b, transpose_b=True) - 别用
a * b—— 它会尝试 element-wise,shape 不匹配就报错
# query: [1, d], docs: [N, d]
sim_matrix = tf.einsum('id,jd->ij', query, docs) # shape [1, N]
norm_query = tf.linalg.norm(query, axis=-1, keepdims=True)
norm_docs = tf.linalg.norm(docs, axis=-1, keepdims=True)
cos_sim = sim_matrix / (norm_query * norm_docs + 1e-8)
导出为 SavedModel 时要注意梯度与 dtype
如果该函数要进训练流程或部署为模型,必须确认它支持自动微分且 dtype 一致。常见坑:
- 输入 tensor 若是
int32,tf.linalg.norm会静默转成float32,但后续运算可能混 dtype 导致InvalidArgumentError: cannot compute Mul on different dtypes - 所有中间变量建议显式
tf.cast(..., tf.float32) - 若函数被
@tf.function装饰,确保输入 shape 有定义(避免None维度引发 trace 失败)
复杂点往往不在公式本身,而在 shape 对齐、dtype 一致性、以及是否真正需要梯度——如果只是离线打分,用 numpy 更轻量;进了图就得盯住每个算子的输入输出约束。


















