pejrich
Can anyone help me implement similarity/cosine similarity in Nx?
I’m trying to port some code from python, to elixir. The python code generates tensors from embedding in BERT, then does some form of similarity comparison between them. From what i’ve found online, it looks like cosine similarity is the calculation I’m looking for, but I can’t quite understand it enough to implement it in Nx.
The formula is listed as A ⋅ B / ||A|| ||B||. I have two tensors with with the shape #Nx.Tensor<f32[1][6][119547]...>. So far this is all i’ve come up with:
for i <- 0..5, j <- 0..5 do
t1 = tensor1[0][i]
t2 = tensor2[0][j]
Nx.dot(t1, t2) / ???
end
I found another formula on the wikipedia page that’s numpy code, which says:
np.sum(a*b)/(np.sqrt(np.sum(a**2)) * np.sqrt(np.sum(b**2)))
I think converted to Nx that’s:
defmodule CosSim do
import Nx.Defn
defn cosine_similarity(a, b) do
left = Nx.sqrt(Nx.sum(a**2))
right = Nx.sqrt(Nx.sum(b**2))
Nx.sum(a * b) / (left * right)
end
end
If I do a quick test:
a = Nx.tensor([1,2,3])
b = Nx.tensor([4,5,6])
CosSim.cosine_similarity(a, b)
#Nx.Tensor<
f32
EXLA.Backend<host:0, 0.528063503.4042653716.111775>
0.9746317863464355
>
If I try to validate it in python:
>>> a = np.matrix([1,2,3])
>>> b = np.matrix([4,5,6])
>>> np.sum(a*b)/(np.sqrt(np.sum(a**2)) * np.sqrt(np.sum(b**2)))
ValueError: shapes (1,3) and (1,3) not aligned: 3 (dim 1) != 1 (dim 0)
So something is off. Admittdly i’ve very new to any of this ML/Nx stuff, so maybe I’m way off, or maybe i’m close. Any tips?
Most Liked
lucaong
You could implement the same behavior as scikit (pairwise cosine similarities between vectors in the given matrices) without iteration, by normalizing along the last axis, transposing the second matrix, and performing a matrix multiplication:
defmodule Pairwise do
import Nx.Defn
defnp l2_norm(x, opts \\ []) do
(x * x)
|> Nx.sum(opts)
|> Nx.sqrt()
end
defnp normalize(x, opts \\ []) do
x / l2_norm(x, axes: opts[:axes], keep_axes: true)
end
defn cosine_similarity(a, b) do
normalized_a = normalize(a, axes: [-1])
normalized_b = normalize(b, axes: [-1])
Nx.dot(normalized_a, Nx.transpose(normalized_b))
end
end
You can now use it passing two vectors:
a = Nx.tensor([1, 2, 3])
b = Nx.tensor([9, 8, 7])
Pairwise.cosine_similarity(a, b)
# =>
# #Nx.Tensor<
# f32
# 0.8826588988304138
# >
Or passing two matrices, and getting pairwise similarities, like with scikit:
a = Nx.tensor([[1, 2, 3], [4, 5, 6]])
b = Nx.tensor([[9, 8, 7], [6, 5, 4]])
Pairwise.cosine_similarity(a, b)
# =>
# #Nx.Tensor<
# f32[2][2]
# [
# [0.8826588988304138, 0.8528028130531311],
# [0.9654632806777954, 0.948051929473877]
# ]
>
lucaong
Pitching in just to say that your function is formally correct, although you could simplify it a bit by using existing Nx functions for the dot product and the norm (possibly benefiting from some optimizations):
defmodule CosSim do
import Nx.Defn
defn cosine_similarity(a, b) do
Nx.dot(a, b) / (Nx.LinAlg.norm(a) * Nx.LinAlg.norm(b))
end
end
You can verify that it’s correct with some sanity checks:
# Similarity for vectors pointing in the same direction is 1
a = Nx.tensor([1, 2])
b = Nx.tensor([2, 4])
CosSim.cosine_similarity(a, b)
# =>
# #Nx.Tensor<
# f32
# 1.0
# >
# Similarity for orthogonal vectors is 0
a = Nx.tensor([3, 0])
b = Nx.tensor([0, 3])
CosSim.cosine_similarity(a, b)
# =>
# #Nx.Tensor<
# f32
# 0.0
# >
# Similarity for vectors pointing in opposite directions is -1:
a = Nx.tensor([5, 0, 2])
b = Nx.tensor([-10, 0, -4])
CosSim.cosine_similarity(a, b)
# =>
# #Nx.Tensor<
# f32
# -1.0
# >
# Vectors at 45deg have cosine similarity = cos(Pi/4) ~ 0.7071
a = Nx.tensor([1, 0])
b = Nx.tensor([1, 1])
CosSim.cosine_similarity(a, b)
# =>
# #Nx.Tensor<
# f32
# 0.7071067690849304
# >
Note though that the definition of the cosine distance in scholar also has additional special cases for when the L2 norm is so small that it would cause numerical stability issues, or when one or both the operands have norm equal to zero. Additionally, it allows to calculate the cosine distance between multiple vectors in batch, by passing rank-2 tensors as arguments.
polvalente
We have Cosine Distance available in Scholar: scholar/lib/scholar/metrics/distance.ex at main · elixir-nx/scholar · GitHub
Cosine Similarity would be 1 - CosDistance ![]()
Last Post!
lud
Hello and thank you.
In the meantime I found some documentation that led me to implement this:
defp cosine_similarity(a, b) do
dot(a, b) / (euclidean_norm(a) * euclidean_norm(b))
end
defp dot(a, b, acc \\ 0)
defp dot([ha | ta], [hb | tb], acc), do: dot(ta, tb, acc + ha * hb)
defp dot([], [], acc), do: acc
defp euclidean_norm(values) do
# ||A|| = √(A₁² + A₂² + … + Aₙ²)
values |> Enum.reduce(0, fn v, sum -> sum + :math.pow(v, 2) end) |> :math.sqrt()
end
Which seems similar but does not seem to be equal to what you wrote.
It works anyway. I’m going to try your code.
Actually it is way faster than with NX because I do not have everything in “the Nx way”. I have an ETS table that contains 150 000 tensors of 200 f32 values.
I should convert that to a single tensor “keyed” with binary names if possible and try with that. But for now with my ad hoc implementation and my raw data (coming from parsing a word2vec file) it’s faster to just have lists of floats.
I also tried to use exla but it was slower (I do not have a graphic card on this computer).
Edit well your code is faster (less traversals I guess) and seems to yield the same results, though I am not sure why because I suck at maths but I guess divide by norm for each coordinate or divide at the end is the same).
I am building a solver for the french version of cemantle.
Thanks!
Popular in Questions
Other popular topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #channels
- #elixirconf
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #phoenix_html
- #iex
- #blog-post
- #graphql
- #genstage
- #ai
- #websockets
- #supervisor
- #elixirconf-us
- #advent-of-code
- #distillery
- #processes
- #forms
- #api
- #metaprogramming
- #security
- #hex










