# ---
# jupyter:
#   jupytext:
#     text_representation:
#       extension: .py
#       format_name: light
#       format_version: '1.5'
#       jupytext_version: 1.14.4
#   kernelspec:
#     display_name: Python 3
#     language: python
#     name: python3
# ---

# ## Word Embeddings

# * NLP trabaja con diferentes formas de data discreta.
#     * Palabras
# * Las palabras vienen de un conjunto finito (aka vocabulary)
# * Otros tipos de data discreta:
#     * caracteres
#     * POS tags
#     * Named entities
#     * items en un catalogo de productos

# * Representar tipos discretos como vectores ha sido parte del exito de NLP.
# * Cuando los tipos discretos son palabras la representacion vectorial se conoce como **word embedding**
#     * Count based -> TF-IDF
# * A nosotros nos interesan metodos basados en aprendizaje o prediccion.
#     * learning-based embeddings
#     * prediction-based embeddings
#     * Las representaciones se aprenden maximizando un objetivo para una tarea de aprendizaje especifica

# ## Porque aprender embeddings?

# 1. Reducir la dimensionalidad hace que sean computacionalmente mas eficientes.
# 2. Las representaciones basadas en conteos resultand en vectores con alta dimensionalidad.
#     * codifica informacion similar de forma redundante
#     * No comparte informacion estadistica
# 3. Usar inputs con alta dimensionalidad crea problemas para la optimizaion.
#     * Los modelos de deep learning tienen millones de parametros
# 4. Representaciones aprendidas (o ajustadas) de data especifica para una tarea son optimas para esa tarea.
#     * Con enfoques que utilizan heuristicas, no esta claro si son relevantes para la tarea.

# ## Eficiencia de los Embeddings
#
# * Vector one-hot multiplicando un layer linear.

# ![Eficiencia de los embeddings](../assets/one_hot_linearl.png)

# * Como el vector on-hot esta lleno de ceros y un 1, el lugar del 1 va a actuar como un seleccionador en la multiplicacion de matrices.
# * Funciona pero es computacionalmente costoso e ineficiente.
# * El vector one-hot esta multiplicando cada numer en la matriz de pesos de la capa linear y calculando la suma para cada fila.
# * Basado en esto, podriamos ignorar la multiplicacion y en vez usar un entero como indice para jalar la fila seleccionada.

# ## Enfoques para aprender word embeddings

# * El objetivo es:
#     * entender que son los word embeddings
#     * como y donde son aplicables
#     * como usarlos de forma de forma segura en un model
#     * sus limitaciones
#     
# * En realidad, rara vez se encuentra uno en una situacion donde se necesite desarrollar un algoritmo para entrenar word embeddings.
# * Todos los metodos de word embeddings se entrenan solo con palabras (unlabeled data) de forma supervisada.
#     * Esto es posible creando tareas supervisadas donde la data esta implicitamente etiquetada.
#     * La representacion esta optimizada a resolver la tarea que creamos, caputrando propiedades estadisticas y linguisticas del corpus.

# ### Tareas auxiliares

# * Dada una secuencia de palabras, predecir la siguiente palabra. _language modeling_
# * Dada una secuencia de palabras antes y despues, predecir la palabra restante.
# * Data una palabra, predecir palabras que ocurren dentro de una ventana, independiente de la posicion.

# **Ejemplos:**
# * GloVe
# * Continuous Bag-of-Words (CBOW)
# * Skipgrams

# ## Usos practicos de Word Embeddings pre entrenadas

# * Corpus grandes - como todo Google News, Wikipedia, o [Common Crawl](commoncrawl.org)
# * Disponibles de forma gratuita para descargar y usar.

# ### Cargando embeddings
#
# * Normalmente vienen en el siguiente formato:
#     * cada linea empieza con la palabra que esta siendo _embedded_
#     * seguida por la secuencia de numeros (representacion vectorial)
#     * el largo de esta secuencia es la dimension de su representacion (aka _embedding dimension_)
#     * el _embedding dimension_ normalmente esta en los cientos
#     * el numero de tokens es usualmente el tamanio del vocabulario y en los millones.

import torch
import torch.nn as nn
from tqdm import tqdm
from annoy import AnnoyIndex
import numpy as np


class PreTrainedEmbeddings(object):
    """ A wrapper around pre-trained word vectors and their use """
    def __init__(self, word_to_index, word_vectors):
        """
        Args:
            word_to_index (dict): mapping from word to integers
            word_vectors (list of numpy arrays)
        """
        self.word_to_index = word_to_index
        self.word_vectors = word_vectors
        self.index_to_word = {v: k for k, v in self.word_to_index.items()}

        self.index = AnnoyIndex(len(word_vectors[0]), metric='euclidean')
        print("Building Index")
        for _, i in self.word_to_index.items():
            self.index.add_item(i, self.word_vectors[i])
        self.index.build(50)
        print("Finished")
        
    @classmethod
    def from_embeddings_file(cls, embedding_file):
        """Instantiate from pre-trained vector file.
        
        Vector file should be of the format:
            word0 x0_0 x0_1 x0_2 x0_3 ... x0_N
            word1 x1_0 x1_1 x1_2 x1_3 ... x1_N
        
        Args:
            embedding_file (str): location of the file
        Returns: 
            instance of PretrainedEmbeddigns
        """
        word_to_index = {}
        word_vectors = []

        with open(embedding_file) as fp:
            for line in fp.readlines():
                line = line.split(" ")
                word = line[0]
                vec = np.array([float(x) for x in line[1:]])
                
                word_to_index[word] = len(word_to_index)
                word_vectors.append(vec)
                
        return cls(word_to_index, word_vectors)
    
    def get_embedding(self, word):
        """
        Args:
            word (str)
        Returns
            an embedding (numpy.ndarray)
        """
        return self.word_vectors[self.word_to_index[word]]

    def get_closest_to_vector(self, vector, n=1):
        """Given a vector, return its n nearest neighbors
        
        Args:
            vector (np.ndarray): should match the size of the vectors 
                in the Annoy index
            n (int): the number of neighbors to return
        Returns:
            [str, str, ...]: words that are nearest to the given vector. 
                The words are not ordered by distance 
        """
        nn_indices = self.index.get_nns_by_vector(vector, n)
        return [self.index_to_word[neighbor] for neighbor in nn_indices]
    
    def compute_and_print_analogy(self, word1, word2, word3):
        """Prints the solutions to analogies using word embeddings

        Analogies are word1 is to word2 as word3 is to __
        This method will print: word1 : word2 :: word3 : word4
        
        Args:
            word1 (str)
            word2 (str)
            word3 (str)
        """
        vec1 = self.get_embedding(word1)
        vec2 = self.get_embedding(word2)
        vec3 = self.get_embedding(word3)

        # compute the fourth word's embedding
        spatial_relationship = vec2 - vec1
        vec4 = vec3 + spatial_relationship

        closest_words = self.get_closest_to_vector(vec4, n=4)
        existing_words = set([word1, word2, word3])
        closest_words = [word for word in closest_words 
                             if word not in existing_words] 

        if len(closest_words) == 0:
            print("Could not find nearest neighbors for the computed vector!")
            return
        
        for word4 in closest_words:
            print("{} : {} :: {} : {}".format(word1, word2, word3, word4))


embeddings = PreTrainedEmbeddings.from_embeddings_file('data/glove/glove.6B.100d.txt')

# ## Relaciones entre word embeddings

# * El feature principal es que codifican relaciones semanticas y sintacticas que se manifiestan como regularidades en el uso de palabras.
# * Por ejemplo, hablamos de perros y gatos en formas similares.
#     * Como consecuencia sus embeddings estan mucho mas cercanos entre ellos que entre los embeddings de otros animales como elefantes.
# * Podemos explorar las relaciones semanticas codificadas en los embeddings de varias formas.
#     
# **Tarea de analogia**
# * Palabra1 : Palabra2 :: Palabra3 : ______
# 1. Palabra2 - Palabra1
#     * Esta vector de diferencias codifica la relacion entre la Palabra1 y Palabra2
# 2. Sumarle esa diferencia a la Palabra3
#     * Esto produce un vector cercano a la Palabra4 donde esta el espacio en blanco
# 3. Buscar el nearest-neighbor en el indice con el vector resultante resuelve el problema de analogia.

embeddings.compute_and_print_analogy('man', 'he', 'woman')

embeddings.compute_and_print_analogy('fly', 'plane', 'sail')

embeddings.compute_and_print_analogy('cat', 'kitten', 'dog')

embeddings.compute_and_print_analogy('blue', 'color', 'dog')

embeddings.compute_and_print_analogy('leg', 'legs', 'hand')

embeddings.compute_and_print_analogy('toe', 'foot', 'finger')

embeddings.compute_and_print_analogy('talk', 'communicate', 'read')

embeddings.compute_and_print_analogy('blue', 'democrat', 'red')

# ## Una de las analogias mas comunes codifica roles de genero
#
#
# **Diferenciar entre regularidades en el lenguaje y sesgos culturales es dificil.**
#
# Ver:
# [Ethics in NLP](ethicsinnlp.org)

embeddings.compute_and_print_analogy('man', 'king', 'woman')

embeddings.compute_and_print_analogy('man', 'doctor', 'woman')

# ## No siempre son correctas

embeddings.compute_and_print_analogy('fast', 'fastest', 'small')



StarcoderdataJupyterScriptsDedupFiltered