En un anterior post sobre tokens, ya vimos la representación mínima de cada palabra. Que corresponde a darle un número a la mínima división de cada palabra.
Sin embargo los transformers y por tanto los LLMs, no representan así la información de las palabras, sino que lo hacen mediante embeddings
.
Vamos a ver primero dos formas de representar las palabras, el ordinal encoding
y el one hot encoding
. Y viendo los problemas de estos dos tipos de representaciones podremos llegar hasta los word embeddings
y los sentence embeddings
.
Además vamos a ver un ejemplo de cómo entrenar un modelo de word embeddings
con la librería gensim
.
Y por último veremos cómo usar modelos preentrenados de embeddings
con la librería transformers
de HuggingFace.
Esta es la manera más básica de representar las palabras dentro de los transformers. Consiste en darle un número a cada palabra, o quedarnos con los números que ya tienen asignados los tokens.
Sin embargo este tipo de representación tiene dos problemas
- Imaginemos que mesa corresponde al token 3, gato al token 1 y perro al token 2. Se podría llegar a suponer que
mesa = gato + perro
, pero no es así. No existe esa relación entre esas palabras. Incluso podríamos pensar que adjudicando los tokens correctos sí podría llegar a darse este tipo de relaciones. Sin embargo este pensamiento se viene abajo con las palabras que tienen más de un significado, como por ejemplo la palabrabanco
- El segundo problema es que las redes neuronales internamente hacen muchos cálculos numéricos, por lo que podría darse el caso en el que si mesa tiene el token 3, tenga internamente más importancia que la palabra gato que tiene el token 1.
De modo que este tipo de representación de las palabras se puede descartar muy rápidamente
Aquí lo que se hace es usar vectores de N
dimensiones. Por ejemplo vimos que OpenAI tiene un vocabulario de 100277
tokens distintos. Por lo que si usamos one hot encoding
, cada palabra se representaría con un vector de 100277
dimensiones.
Sin embargo el one hot encodding tiene otro dos grandes problemas
- No tiene en cuenta la relación entre las palabras. Por lo que si tenemos dos palabras que son sinónimos, como por ejemplo
gato
yfelino
, tendríamos dos vectores distintos para representarlas.
En el lenguaje la relación entre las palabras es muy importante, y no tener en cuenta esta relación es un gran problema.
- El segundo problema es que los vectores son muy grandes. Si tenemos un vocabulario de
100277
tokens, cada palabra se representaría con un vector de100277
dimensiones. Esto hace que los vectores sean muy grandes y que los cálculos sean muy costosos. Además estos vectores van a ser todo ceros, excepto en la posición que corresponda al token de la palabra. Por lo que la mayoría de los cálculos van a ser multiplicaciones por cero, que son cálculos que no aportan nada. Así que vamos a tener un montón de memoria asignada a vectores en los que solo se tiene un1
en una posición determinada.
Con los word embeddings se intenta solucionar los problemas de los dos tipos de representaciones anteriores. Para ello se usan vectores de N
dimensiones, pero en este caso no se usan vectores de 100277 dimensiones, sino que se usan vectores de muchas menos dimensiones. Por ejemplo veremos que OpenAI usa 1536
dimensiones.
Cada una de las dimensiones de estos vectores representan una característica de la palabra. Por ejemplo una de las dimensiones podría representar si la palabra es un verbo o un sustantivo. Otra dimensión podría representar si la palabra es un animal o no. Otra dimensión podría representar si la palabra es un nombre propio o no. Y así sucesivamente.
Sin embargo estas características no se definen a mano, sino que se aprenden de forma automática. Durante el entrenamiento de los transformers, se van ajustando los valores de cada una de las dimensiones de los vectores, de modo que se aprenden las características de cada una de las palabras.
Al hacer que cada una de las dimensiones de las palabras represente una característica de la palabra, se consigue que las palabras que tengan características similares, tengan vectores similares. Por ejemplo las palabras gato
y felino
tendrán vectores muy similares, ya que ambas son animales. Y las palabras mesa
y silla
tendrán vectores similares, ya que ambas son muebles.
En la siguiente imagen podemos ver una representación de 3 dimensiones depalabras, y podemos ver que todas las palabras relacionadas con school
están cerca, todas las palabras relacionadas con food
están cerca y todas las palabras relacionadas con ball
están cerca.
Tener que cada una de las dimensiones de los vectores represente una característica de la palabra, consigue que podamos hacer operaciones con palabras. Por ejemplo si a la palabra rey
se le resta la palabra hombre
y se le suma la palabra mujer
, obtenemos una palabra muy parecida a la palabra reina
. Más adelante lo comprobaremos con un ejemplo
Como cada una de las palabras se representa mediante un vector de N
dimensiones, podemos calcular la similitud entre dos palabras. Para ello se usa la función de similitud del coseno o cosine similarity
.
Si dos palabras están cercanas en el espacio vectorial, quiere decir que el álgulo que hay entre sus vectores es pequeño, por lo que su coseno es cercano a 1. Si hay un ángulo de 90 grados entre los vectores, el coseno es 0, es decir que no hay similitud entre las palabras. Y si hay un ángulo de 180 grados entre los vectores, el coseno es -1, es decir que las palabras son opuestas.
Ahora que sabemos lo que son los embeddings
, veamos unos ejemplos con los embeddings
que nos proporciona la API
de OpenAI
.
Para ello primero tenemos que tener instalado el paquete de OpenAI
pip install openai
Importamos las librerías necesarias
from openai import OpenAI
import torch
from torch.nn.functional import cosine_similarity
api_key = "Pon aquí tu API key"
Seleccionamos que modelo de embeddings queremos usar. En este caso vamos a usar text-embedding-ada-002
que es el que recomienda OpenAI
en su documentación de embeddings.
model_openai = "text-embedding-ada-002"
Creamos un cliente de la API
client_openai = OpenAI(api_key=api_key, organization=None)
Vamos a ver cómo son los embeddings
de la palabra Rey
word = "Rey"
embedding_openai = torch.Tensor(client_openai.embeddings.create(input=word, model=model_openai).data[0].embedding)
embedding_openai.shape, embedding_openai
Como vemos obtenemos un vector de 1536
dimensiones
Vamos a obtener los embeddings de las palabras rey
, hombre
, mujer
y reina
embedding_openai_rey = torch.Tensor(client_openai.embeddings.create(input="rey", model=model_openai).data[0].embedding)
embedding_openai_hombre = torch.Tensor(client_openai.embeddings.create(input="hombre", model=model_openai).data[0].embedding)
embedding_openai_mujer = torch.Tensor(client_openai.embeddings.create(input="mujer", model=model_openai).data[0].embedding)
embedding_openai_reina = torch.Tensor(client_openai.embeddings.create(input="reina", model=model_openai).data[0].embedding)
embedding_openai_reina.shape, embedding_openai_reina
Vamos a obtener el embedding resultante de restarle a rey
el embedding de hombre
y sumarle el embedding de mujer
embedding_openai = embedding_openai_rey - embedding_openai_hombre + embedding_openai_mujer
embedding_openai.shape, embedding_openai
Por último comparamos el resultado obtenido con el embedding de reina
. Para ello usamos la función de cosine_similarity
que nos proporciona la librería pytorch
similarity_openai = cosine_similarity(embedding_openai.unsqueeze(0), embedding_openai_reina.unsqueeze(0)).item()
print(f"similarity_openai: {similarity_openai}")
Como vemos es un valor muy cercano a 1, por lo que podemos decir que el resultado obtenido es muy parecido al embedding de reina
Si usamos palabras en inglés, obtenemos un resultado más cercano a 1
embedding_openai_rey = torch.Tensor(client_openai.embeddings.create(input="king", model=model_openai).data[0].embedding)
embedding_openai_hombre = torch.Tensor(client_openai.embeddings.create(input="man", model=model_openai).data[0].embedding)
embedding_openai_mujer = torch.Tensor(client_openai.embeddings.create(input="woman", model=model_openai).data[0].embedding)
embedding_openai_reina = torch.Tensor(client_openai.embeddings.create(input="queen", model=model_openai).data[0].embedding)
embedding_openai = embedding_openai_rey - embedding_openai_hombre + embedding_openai_mujer
similarity_openai = cosine_similarity(embedding_openai.unsqueeze(0), embedding_openai_reina.unsqueeze(0))
print(f"similarity_openai: {similarity_openai}")
Esto es normal, ya que el modelo de OpenAi ha sido entrenado con más textos en inglés que en español
Existen varios tipos de word embeddings, y cada uno de ellos tiene sus ventajas e inconvenientes. Vamos a ver los más importantes
- Word2Vec
- GloVe
- FastText
- BERT
- GPT-2
Word2Vec es un algoritmo que se usa para crear word embeddings. Este algoritmo fue creado por Google en 2013, y es uno de los algoritmos más usados para crear word embeddings.
Tiene dos variantes, CBOW
y Skip-gram
. CBOW
es más rápido de entrenar, mientras que Skip-gram
es más preciso. Vamos a ver cómo funciona cada uno de ellos
CBOW
o Continuous Bag of Words
es un algoritmo que se usa para predecir una palabra a partir de las palabras que la rodean. Por ejemplo si tenemos la frase El gato es un animal
, el algoritmo intentará predecir la palabra gato
a partir de las palabras que la rodean, en este caso El
, es
, un
y animal
.
En esta arquitectura, el modelo predice cuál es la palabra más probable en el contexto dado. Por lo tanto, las palabras que tienen la misma probabilidad de aparecer se consideran similares y, por lo tanto, se acercan más en el espacio dimensional.
Supongamos que en una oración reemplazamos barco
con bote
, entonces el modelo predice la probabilidad para ambos y si resulta ser similar entonces podemos considerar que las palabras son similares.
Skip-gram
o Skip-gram with Negative Sampling
es un algoritmo que se usa para predecir las palabras que rodean a una palabra. Por ejemplo si tenemos la frase El gato es un animal
, el algoritmo intentará predecir las palabras El
, es
, un
y animal
a partir de la palabra gato
.
Esta arquitectura es similar a la de CBOW, pero en cambio el modelo funciona al revés. El modelo predice el contexto usando la palabra dada. Por lo tanto, las palabras que tienen el mismo contexto se consideran similares y, por lo tanto, se acercan más en el espacio dimensional.
GloVe
o Global Vectors for Word Representation
es un algoritmo que se usa para crear word embeddings. Este algoritmo fue creado por la Universidad de Stanford en 2014.
Word2Vec ignora el hecho de que algunas palabras de contexto se producen con más frecuencia que otras y también solo tienen en cuenta el contexto local y por lo tanto, no capturar el contexto global.
Este algoritmo usa una matriz de co-ocurrencia para crear los word embeddings. Esta matriz de co-ocurrencia es una matriz que contiene el número de veces que aparece cada palabra junto a cada una de las otras palabras del vocabulario.
FastText
es un algoritmo que se usa para crear word embeddings. Este algoritmo fue creado por Facebook en 2016.
Una de las principales desventajas de Word2Vec
y GloVe
es que no pueden codificar palabras desconocidas o fuera del vocabulario.
Entonces, para lidiar con este problema, Facebook propuso un modelo FastText
. Es una extensión de Word2Vec
y sigue el mismo modelo Skip-gram
y CBOW
. pero a diferencia de Word2Vec
que alimenta palabras enteras en la red neuronal, FastText
primero divide las palabras en varias subpalabras (o n-grams
) y luego las alimenta a la red neuronal.
Por ejemplo, si el valor de n
es 3 y la palabra es manzana
entonces su tri-gram será [
, man
, anz
, nza
, zan
, ana
, na>
] y su embedding de palabras será la suma de la representación vectorial de estos tri-grams. Aquí, los hiperparámetros min_n
y max_n
se consideran como 3 y los caracteres <
y >
representan el comienzo y el final de la palabra.
Por lo tanto, utilizando esta metodología, las palabras desconocidas se pueden representar en forma vectorial, ya que tiene una alta probabilidad de que sus n-grams
también estén presentes en otras palabras.
Este algoritmo es una mejora de Word2Vec
, ya que además de tener en cuenta las palabras que rodean a una palabra, también tiene en cuenta los n-grams
de la palabra. Por ejemplo si tenemos la palabra gato
, también tiene en cuenta los n-gramas
de la palabra, en este caso ga
, at
y to
, para n = 2
.
Las técnicas de word embedding han dado un resultado decente, pero el problema es que el enfoque no es lo suficientemente preciso. No tienen en cuenta el orden de las palabras en las que aparecen, lo que conduce a la pérdida de la comprensión sintáctica y semántica de la oración.
Por ejemplo, Vas allí para enseñar, no a jugar
Y Vas allí a jugar, no a enseñar
Ambas oraciones tendrán la misma representación en el espacio vectorial, pero no significan lo mismo.
Además, el modelo de word embedding no puede dar resultados satisfactorios en una gran cantidad de datos de texto, ya que la misma palabra puede tener un significado diferente en una oración diferente según el contexto de la oración.
Por ejemplo, Voy a sentarme en el banco
Y Voy a hacer gestiones en el banco
En ambas oraciones, la palabra banco
tiene diferentes significados.
Por lo tanto, requerimos un tipo de representación que pueda retener el significado contextual de la palabra presente en una oración.
El sentence embedding es similar al word embedding, pero en lugar de palabras, codifican toda la oración en la representación vectorial.
Una forma simple de obtener sentence embedding es promediando los word embedding de todas las palabras presentes en la oración. Pero no son lo suficientemente precisos.
Algunos de los modelos más avanzados para sentence embedding son ELMo
, InferSent
y Sentence-BERT
ELMo
o Embeddings from Language Models
es un modelo de sentence embedding que fue creado por la Universidad de Allen en 2018. Utiliza una red LSTM profunda bidireccional para producir representación vectorial. ELMo
puede representar las palabras desconocidas o fuera del vocabulario en forma vectorial ya que está basado en caracteres.
InferSent
es un modelo de sentence embedding que fue creado por Facebook en 2017. Utiliza una red LSTM profunda bidireccional para producir representación vectorial. InferSent
puede representar las palabras desconocidas o fuera del vocabulario en forma vectorial ya que está basado en caracteres. Las oraciones están codificadas en una representación vectorial de 4096 dimensiones.
La capacitación del modelo se realiza en el conjunto de datos de Stanford Natural Language Inference (SNLI
). Este conjunto de datos está etiquetado y escrito por humanos para alrededor de 500K pares de oraciones.
Sentence-BERT
es un modelo de sentence embedding que fue creado por la Universidad de Londres en 2019. Utiliza una red LSTM profunda bidireccional para producir representación vectorial. Sentence-BERT
puede representar las palabras desconocidas o fuera del vocabulario en forma vectorial ya que está basado en caracteres. Las oraciones están codificadas en una representación vectorial de 768 dimensiones.
El modelo de NLP de última generación BERT
es excelente en las tareas de Similitud Textual Semántica, pero el problema es que tomaría mucho tiempo para un corpus enorme (65 horas para 10,000 oraciones), ya que requiere que ambas oraciones se introduzcan en la red y esto aumenta el cálculo por un factor enorme.
Por lo tanto, Sentence-BERT
es una modificación del modelo BERT
.
Para descargar el dataset que vamos a usar hay que instalar la librería dataset
de huggingface:
pip install datasets
Para entrenar el modelo de embeddings vamos a usar la librería gensim
. Para instalarla con conda usamos
conda install -c conda-forge gensim
Y para instalarla con pip usamos
pip install gensim
Para limpiar el dataset que nos hemos descargado vamos a usar expresiones regulares, que normalmente ya está instalada en python, y nltk
que es una librería de procesamiento de lenguaje natural. Para instalarla con conda usamos
conda install -c anaconda nltk
Y para instalarla con pip usamos
pip install nltk
Ahora que tenemos todo instalado podemos importar las librerías que vamos a usar:
from gensim.models import Word2Vec
from gensim.parsing.preprocessing import strip_punctuation, strip_numeric, strip_short
import re
from nltk.corpus import stopwords
from nltk.tokenize import word_tokenize
Vamos a descargar un dataset de textos procedentes de la wikipedia en español, para ello ejecutamos lo siguiente:
from datasets import load_dataset
dataset_corpus = load_dataset('large_spanish_corpus', name='all_wikis')
Vamos a ver cómo es
dataset_corpus
Como podemos ver, el dataset tiene más de 28 millones de textos. Vamos a ver alguno de ellos:
dataset_corpus['train']['text'][0:10]
Como hay muchos ejemplos vamos a crear un subset de 10 millónes de ejemplos para poder trabajar más rápido:
subset = dataset_corpus['train'].select(range(10000000))
Ahora nos descargamos las stopwords
de nltk
, que son palabras que no aportan información y que vamos a eliminar de los textos
import nltk
nltk.download('stopwords')
Ahora vamos a descargar los punkt
de nltk
, que es un tokenizer
que nos va a permitir separar los textos en frases
nltk.download('punkt')
Creamos una función para limpiar los datos, esta función va a:
- Pasar el texto a minúsculas
- Eliminar las urls
- Eliminar las menciones a redes sociales como
@twitter
p#hashtag
- Eliminar los signos de puntuación
- Eliminar los números
- Eliminar las palabras cortas
- Eliminar las stopwords
Como estamos usando un dataset de huggeface, los textos están en formato dict
, así que devolvemos un diccionario.
def clean_text(sentence_batch):
# extrae el texto de la entrada
text_list = sentence_batch['text']
cleaned_text_list = []
for text in text_list:
# Convierte el texto a minúsculas
text = text.lower()
# Elimina URLs
text = re.sub(r'http\S+|www\S+|https\S+', '', text, flags=re.MULTILINE)
# Elimina las menciones @ y '#' de las redes sociales
text = re.sub(r'\@\w+|\#\w+', '', text)
# Elimina los caracteres de puntuación
text = strip_punctuation(text)
# Elimina los números
text = strip_numeric(text)
# Elimina las palabras cortas
text = strip_short(text,minsize=2)
# Elimina las palabras comunes (stop words)
stop_words = set(stopwords.words('spanish'))
word_tokens = word_tokenize(text)
filtered_text = [word for word in word_tokens if word not in stop_words]
cleaned_text_list.append(filtered_text)
# Devuelve el texto limpio
return {'text': cleaned_text_list}
Aplicamos la función a los datos
sentences_corpus = subset.map(clean_text, batched=True)
Vamos a guardar el dataset filtrado en un fichero para no tener que volver a ejecutar el proceso de limpieza
sentences_corpus.save_to_disk("sentences_corpus")
Para cargarlo podemos hacer
from datasets import load_from_disk
sentences_corpus = load_from_disk('sentences_corpus')
Ahora lo que vamos a tener es una lista de listas, donde cada lista es una frase tokenizada y sin stopwords. Es decir, tenemos una lista de frases, y cada frase es una lista de palabras. Vamos a ver cómo es:
for i in range(10):
print(f'La frase "{subset["text"][i]}" se convierte en la lista de palabras "{sentences_corpus["text"][i]}"')
Vamos a entrenar un modelo de embeddings que convertirá palabras en vectores. Para ello vamos a usar la librería gensim
y su modelo Word2Vec
.
dataset = sentences_corpus['text']
dim_embedding = 100
window_size = 5 # 5 palabras a la izquierda y 5 palabras a la derecha
min_count = 5 # Ignora las palabras con frecuencia menor a 5
workers = 4 # Número de hilos de ejecución
sg = 1 # 0 para CBOW, 1 para Skip-gram
model = Word2Vec(dataset, vector_size=dim_embedding, window=window_size, min_count=min_count, workers=workers, sg=sg)
Este modelo se ha entrenado en la CPU, ya qye gensim
no tiene opción de realizar el entrenamiento en la GPU y aun así en mi ordenador ha tardado X minutos en entrenar el modelo. Aunque la dimensión del embedding que hemos elegido es de solo 100 (a diferencia del tamaño de los embeddings de openai que es de 1536), no es un tiempo demasiado grande, ya que el dataset tiene 10 millones de frases.
Los grandes modelos de lenguaje son entrenados con datasets de miles de millones de frases, por lo que es normal que el entrenamiento de un modelo de embeddings con un dataset de 10 millones de frases tarde unos minutos.
Una vez entrenado el modelo lo guardamos en un archivo para poder usarlo en el futuro
model.save('word2vec.model')
Si lo quisiéramos cargar en el futuro, podemos hacerlo con
model = Word2Vec.load('word2vec.model')
Vamos a ver las palabras más similares de algunas palabras
model.wv.most_similar('perro', topn=10)
model.wv.most_similar('gato', topn=10)
Ahora vamos a ver el ejemplo en el que comprobamos la similitud de la palabra reina
con el resultado de a la palabra rey
le restamos la palabra hombre
y le sumamos la palabra mujer
embedding_hombre = model.wv['hombre']
embedding_mujer = model.wv['mujer']
embedding_rey = model.wv['rey']
embedding_reina = model.wv['reina']
embedding = embedding_rey - embedding_hombre + embedding_mujer
from torch.nn.functional import cosine_similarity
embedding = torch.tensor(embedding).unsqueeze(0)
embedding_reina = torch.tensor(embedding_reina).unsqueeze(0)
similarity = cosine_similarity(embedding, embedding_reina, dim=1)
similarity
Como vemos hay bastante similitud
Vamos a visualizar los embeddings, para ello primero obtenemos los vectores y las palabras del modelo
embeddings = model.wv.vectors
words = list(model.wv.index_to_key)
Como la dimensión de los embeddings es 100, para poder visualizarlos en 2 o 3 dimensiones tenemos que reducir la dimensión. Para ello vamos a usar PCA
(más rápido) o TSNE
(más preciso) de sklearn
from sklearn.decomposition import PCA
dimmesions = 2
pca = PCA(n_components=dimmesions)
reduced_embeddings_PCA = pca.fit_transform(embeddings)
from sklearn.manifold import TSNE
dimmesions = 2
tsne = TSNE(n_components=dimmesions, verbose=1, perplexity=40, n_iter=300)
reduced_embeddings_tsne = tsne.fit_transform(embeddings)
Ahora los visualizamos en 2 dimensiones con matplotlib
. Vamos a visualizar la reducción de dimensionalidad que hemos hecho con PCA
y con TSNE
import matplotlib.pyplot as plt
plt.figure(figsize=(10, 10))
for i, word in enumerate(words[:200]): # Limitar a las primeras 200 palabras
plt.scatter(reduced_embeddings_PCA[i, 0], reduced_embeddings_PCA[i, 1])
plt.annotate(word, xy=(reduced_embeddings_PCA[i, 0], reduced_embeddings_PCA[i, 1]), xytext=(5, 2), textcoords='offset points', ha='right', va='bottom')
plt.title('Embeddings (PCA)')
plt.show()
plt.figure(figsize=(10, 10))
for i, word in enumerate(words[:200]): # Limitar a las primeras 200 palabras
plt.scatter(reduced_embeddings_tsne[i, 0], reduced_embeddings_tsne[i, 1])
plt.annotate(word, xy=(reduced_embeddings_tsne[i, 0], reduced_embeddings_tsne[i, 1]), xytext=(5, 2),
textcoords='offset points', ha='right', va='bottom')
plt.show()
Para usar modelos preentrenados de embeddings
vamos a usar la librería transformers
de huggingface
. Para instalarla con conda usamos
conda install -c conda-forge transformers
Y para instalarla con pip usamos
pip install transformers
Con la tarea feature-extraction
de huggingface
podemos usar modelos preentrenados para obtener los embeddings de las palabras. Para ello primero impoertamos la librería necesaria
from transformers import pipeline
Vamos a obtener los embeddings
de BERT
checkpoint = "bert-base-uncased"
feature_extractor = pipeline("feature-extraction",framework="pt",model=checkpoint)
Vamos a ver los embeddings
de la palabra rey
embedding = feature_extractor("rey", return_tensors="pt").squeeze(0)
embedding.shape
Como vemos obtenemos un vector de 768
dimensiones, es decir, los embeddings
de BERT
tienen 768
dimensiones. Por otro lado vemos que tiene 3 vectores de embeddings
, esto es porque BERT
añade un token al principio y otro al final de la frase, por lo que a nosotros solo nos interesa el vector del medio
Vamos a volver a hacer el ejemplo en el que comprobamos la similitud de la palabra reina
con el resultado de a la palabra rey
le restamos la palabra hombre
y le sumamos la palabra mujer
embedding_hombre = feature_extractor("man", return_tensors="pt").squeeze(0)[1]
embedding_mujer = feature_extractor("woman", return_tensors="pt").squeeze(0)[1]
embedding_rey = feature_extractor("king", return_tensors="pt").squeeze(0)[1]
embedding_reina = feature_extractor("queen", return_tensors="pt").squeeze(0)[1]
embedding = embedding_rey - embedding_hombre + embedding_mujer
Vamos a ver la similitud
import torch
from torch.nn.functional import cosine_similarity
embedding = torch.tensor(embedding).unsqueeze(0)
embedding_reina = torch.tensor(embedding_reina).unsqueeze(0)
similarity = cosine_similarity(embedding, embedding_reina, dim=1)
similarity.item()
Usando los embeddings
de BERT
también obtenemos un resultado muy cercano a 1