<jupyter_start><jupyter_text>Práctica de Sistemas Recomendadores 3: Content based En este práctico, volveremos a utilizar la biblioteca de Python [sklearn](https://scikit-learn.org/stable/), para aprender sobre 2 algoritmos para recomendación basado en contenidos y de unas herramientas para preprocesar los textos. En particula, este practico verá:* TF-IDF* Word Embeddings* Uso de Stop Words**Ayudantes**: <NAME>, <NAME> y <NAME>  Actividad 1Antes de empezar con la actividad, responder la siguiente pregunta con lo visto en clases**Pregunta:** Explique con sus palabras a qué se refiere con recomendación basada en contenido. En particular responda- ¿Qué datos se utilizan para recomendación basada en contenidos?- Mencione un ejemplo donde sea factible utilizar este tipo de recomendación y justifique. **Respuesta:** COMPLETAR  Descargando la informaciónVaya ejecutando cada celda presionando el botón de **Play** o presionando Ctrl+Enter (Linux y Windows) o Command+Enter (Macosx) para descargar las bases de datos.*   Recursos:  * `dictionary.p`  * `dictionary-stemm.p`  * `tfidf_model.p`  * `tfidf_model-stemm.p`*   Dataset:  *  `corpus1.csv`<jupyter_code># Descarga de recursos
!curl -L -o 'resources.tar.gz' 'https://github.com/PUC-RecSys-Class/Syllabus/blob/master/Practico%204/files/resources.tar.gz?raw=true'

# Descompresión del archivo
!tar -xvf resources.tar.gz
# Descarga del dataset
!curl -L -o 'dataset.tar.gz' 'https://github.com/PUC-RecSys-Class/Syllabus/blob/master/Practico%204/files/dataset.tar.gz?raw=true'

# Descompresión del archivo
!tar -xvf dataset.tar.gz<jupyter_output><empty_output><jupyter_text>Revisar archivos descargadosRevisemos el _dataset_ descargado:<jupyter_code>import pandas as pd

corpus_df = pd.read_csv('./corpus1.csv', sep='\t',
                        header=None, encoding='latin',
                        names=['id', 'title', 'abstract'])
corpus_df.head(5)<jupyter_output><empty_output><jupyter_text>Podemos ver que este _dataet: contiene 3 columnas:* **_id_**: identificador de cada texto* **_title_**: título del documento, en este caso, de un _paper_* **_abstract_**: primer párafo del _paper_ que es una representación abreviada, objetiva y precisa del contenido de un documento o recurso, sin interpretación crítica y sin mención expresa del autor del resumen.  Preparar entornoPrimero es necesario instalar algunas librerías previas<jupyter_code>!pip install nltk
!pip install sklearn
!pip install gensim
!pip install pandas
!pip install numpy<jupyter_output><empty_output><jupyter_text>Luego necesitamos importar las librerías a utilizar en este práctico. No se asusten por todas las librerías, iremos explicando lo más importante a medida que se avanza en el práctico.<jupyter_code>import string

import gensim
import nltk
import numpy as np
import pandas as pd
import sklearn

from collections import Counter
from os.path import isfile
from textwrap import wrap

from gensim import corpora, models, similarities
from nltk.corpus import stopwords
from nltk.stem.porter import PorterStemmer
from scipy.sparse import csr_matrix
from sklearn.neighbors import NearestNeighbors
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.decomposition import NMF

import json
import warnings
import gensim 
warnings.filterwarnings("ignore")
pd.options.display.max_columns = None<jupyter_output><empty_output><jupyter_text>Preprocesamiento de datos Volvemos a cargar el _dataset_ a utilizar<jupyter_code>corpus_df = pd.read_csv('./corpus1.csv', sep='\t',
                        header=None, encoding='latin',
                        names=['id', 'title', 'abstract'])
corpus_df.head(5)<jupyter_output><empty_output><jupyter_text>Luego descargamos las librerías de NLTK necesarias:<jupyter_code>nltk.download('punkt')<jupyter_output><empty_output><jupyter_text>En este momento estamos bajando un _tokenizador_ específico llamado [Punkt Sentence Tokenizer](https://kite.com/python/docs/nltk.tokenize.punkt). Este será usado a continuación para realizar una cierta tarea con los textos (no vamos a decir cual es porque una actividad es que comenten que hace dado unos ejemplos que mostramos c: ). Lo siguiente es implementar una función que transforme texto no estructurado a una lista de *tokens* procesados.<jupyter_code>def get_tokens(text):
    # Pasar todo a minuscula
    lowers = text.lower()
    
    # Quitar puntuación
    no_punctuation = lowers.translate({ord(c): None for c in string.punctuation})
    
    # Tokenizar 
    tokens = nltk.word_tokenize(no_punctuation)
    
    # Retornar resultado
    return tokens


print(get_tokens("I'm a super student for recommender systems!"))
print(get_tokens("First sentence. Seconde sentence."))<jupyter_output><empty_output><jupyter_text>En el código anterior, para ejecutar `nltk.word_tokenize()` era necesario tener descargado _punkt_.  Actividad 2En función a las frases ingresadas y al resultado impreso, ¿Qué significa _Tokenizar_? **Respuesta:** COMPLETAR<jupyter_code># A cada abstract le aplicamos la función de get_tokens
corpus_df['tokenized_abstract'] = corpus_df.abstract.map(get_tokens)
corpus_df.head(5)<jupyter_output><empty_output><jupyter_text>Ahora se tiene que generar un diccionario con todas las palabras del *corpus*. Se recomienda revisar la documentación de gensim y leer cómo usar los diccionarios: [corpora.dictionary](https://radimrehurek.com/gensim/corpora/dictionary.html)<jupyter_code>dict_file = './resources/dictionary.p'

if isfile(dict_file): # Verificar si existe el archivo
    dictionary = corpora.dictionary.Dictionary().load(dict_file)
    
else: # En otro caso, crear el archivo y guardarlo
    dictionary = corpora.dictionary.Dictionary(documents=corpus_df.tokenised_abstract.tolist())
    dictionary.save(dict_file)
# Texto original
print("Texto 1")
wrap(str(corpus_df.loc[0]["tokenized_abstract"]))

# Texto pasado por el diccionario
print("Texto 1")
wrap(str(dictionary.doc2bow(corpus_df.loc[0]["tokenized_abstract"])))<jupyter_output><empty_output><jupyter_text>Cuando se hizo `dictionary.doc2bow` se transformó una lista de palabas a un contador de ellas. En donde cada tupla representa `(ID, cantidad de veces)` de modo que se reduce la cantidad de palabras del texto a información numerica. Por ejemplo, la tupla `(30, 5)` indica que la palabra con ID 30 está 5 veces en el texto. Revisando el texto podemos ver que la palabra **"a"** es la que está repetida 5 veces. Esto implica que **"a"** está asignada al ID 30.Ahora aplicaremos esta función a cada texto del _dataset_.<jupyter_code>corpus_df['bow'] = corpus_df.tokenized_abstract.map(dictionary.doc2bow)

corpus = corpus_df['bow'].tolist()

corpus_df.head(5)<jupyter_output><empty_output><jupyter_text>Tf-idf Recordemos que Tf-idf es una medida numérica que expresa cuán relevante es una palabra para un documento en una colección. Ahora, dada la frecuencia de cada palabra en cada texto, se v a utilizar esta ténica para obtener tuplas de la forma `(ID, Tf-idf)` en donde ID será el ID de la palabra igual como estaba antes (por ejemplo **"a"** tiene ID 30) y Tf-Idf será el valor dado por este algoritmo a la palabra en cuestión.<jupyter_code>tfidf_model_file = 'resources/tfidf_model.p'

if isfile(tfidf_model_file):
    tfidf_model = models.tfidfmodel.TfidfModel().load(tfidf_model_file)

else:
    tfidf_model = models.tfidfmodel.TfidfModel(corpus, dictionary=dictionary)
    tfidf_model.save(tfidf_model_file)

corpus_df['tf_idf'] = tfidf_model[corpus_df.bow.tolist()]
corpus_df.head(5)<jupyter_output><empty_output><jupyter_text>Generar recomendaciones: En esta sección se implementan las funciones necesarias para poder generar recomendaciones dado lo que un usuario ha consumido. De manera artificial, se "samplearán" 3 documentos aleatorios que representarán al usuario objetivo (`sample`). Luego tendrás que generar diferentes recomendaciones y evaluar los resultados.<jupyter_code># Random users
samples = corpus_df.sample(3)
samples_ids = []

for n, (ix, paper) in enumerate(samples.iterrows()):
    samples_ids.append(ix)
    idx, title, abstract, bow, tf_idf = paper[[
        'id', 'title', 'abstract', 'bow', 'tf_idf']]
    print('%d) %s' % (n+1, title))
    print('')
    print("\n".join(wrap(abstract)))
    print('\n')<jupyter_output><empty_output><jupyter_text>Lo anterior son 3 textos tomados al azar. Asumiremso que una persona vió estos 3 textos y ahora vamos a recomendarle 5 nuevos por cada documento.<jupyter_code># Recommendation functions

N = len(dictionary)


def to_sparse(matrix):
    return csr_matrix([
        gensim.matutils.sparse2full(row, length=N)
        for row in matrix
    ])


def make_recommendations(model, metric, neighbors):
    M = len(corpus)

    X = to_sparse(corpus_df[model].tolist())
    document_index = NearestNeighbors(
        n_neighbors=(neighbors + 1),
        algorithm='brute',
        metric=metric).fit(X)
    return document_index


def print_recommendations(indexes, model):
    for n, (ix, paper) in enumerate(samples.iterrows()):
        dists, neighbors = indexes.kneighbors([gensim.matutils.sparse2full(paper[model], length=N)])
        print(paper['title'])
        print('')
        print('Documentos cercanos: ')
        i = 1
        for neighbour in neighbors[0]:
            if ix != neighbour:
                line = str(i) + ". " + corpus_df.iloc[neighbour]['title']
                print(line)
                i += 1
        print('\n')<jupyter_output><empty_output><jupyter_text>A continuación deberá utilizar las funciones implementadas anteriormente para generar nuevas recomendaciones variando los parámetros del modelo. **Agregue nuevas celdas para cada implementación y/o pregunta.**<jupyter_code># Recommendation example: TF-IDF
doc_idx = make_recommendations('tf_idf', 'euclidean', 5)
print_recommendations(doc_idx, 'tf_idf')<jupyter_output><empty_output><jupyter_text>Stop words A continuación, intentaremos mejorar los resultados obtenidos con TF-IDF eliminando las *stopwords*. ¿Qué son las *stopwords*? Son palabras vacías, sin significado, que no aportan (de manera significativa) al sentido de una frase, como los artículos, pronombres, etc.<jupyter_code>nltk.download('stopwords')
from nltk.corpus import stopwords

def remove_stopwords(text):
    filtered_words = [
        word for word in text if word not in stopwords.words('english')
    ]
    return filtered_words<jupyter_output><empty_output><jupyter_text>Ahora eliminamos los stopwords de los textos y volvemos a hacer todo el proceso pero con textos diferentes. Este proceso dura aproximadamente **5 minutos**<jupyter_code>%%time
# Puede que se demore un poco esta celda
corpus_df['tokenized_abstract_without_stopwords'] = corpus_df.tokenized_abstract.map(remove_stopwords)
corpus_df.head(5)
corpus_df['bow_without_stopwords'] = corpus_df.tokenized_abstract_without_stopwords.map(dictionary.doc2bow)
corpus_df.head(5)
corpus = corpus_df['bow_without_stopwords'].tolist()

tfidf_model_file_without_stopwords = 'resources/tfidf_model.p'

if isfile(tfidf_model_file):
    tfidf_model_without_stopwords = models.tfidfmodel.TfidfModel().load(tfidf_model_file)

else:
    tfidf_model_without_stopwords = models.tfidfmodel.TfidfModel(corpus, dictionary=dictionary)
    tfidf_model_without_stopwords.save(tfidf_model_file_without_stopwords)

corpus_df['tf_idf_without_stopwords'] = tfidf_model_without_stopwords[corpus_df.bow_without_stopwords.tolist()]
corpus_df.head(5)<jupyter_output><empty_output><jupyter_text>**Actividad:** Genere recomendaciones para un nuevo usuario utilizando los nuevos vectores generados sin stop-words.<jupyter_code><jupyter_output><empty_output><jupyter_text>Word EmbeddingsEn esta sección haremos recomendacion de textos médicos de [PubMed](https://www.ncbi.nlm.nih.gov/pubmed/) que han sido revisados por expertos. RESPONDER LAS SIGUIENTES PREGUNTAS: - ¿Que son word embeddings? ¿Cuál es la intuición?- ¿Por qué son útiles para representar documentos?<jupyter_code># Descarga de recursos
!wget https://www.dropbox.com/s/gc3x9rp4gu2tmch/documents_w2vec.json.zip
!unzip documents_w2vec.json.zip
# Descarga del dataset
!wget https://www.dropbox.com/s/1bxuw3uf3xwyrr7/pubmed_data.csv<jupyter_output><empty_output><jupyter_text>Podemos ver que este _dataet: contiene 4 columnas:* **user_id**: identificador de cada usuario * **pid**: identificador de cada texto con su correlativo de PubMed. * **_title_**: título del documento, en este caso, de un _paper_* **_abstract_**: primer párafo del _paper_ que es una representación abreviada, objetiva y precisa del contenido de un documento o recurso.<jupyter_code>df = pd.read_csv('pubmed_data.csv')
df.head()
# creamos diccionario de titulos y abstracts que utilizaremos despues
dict_title_abstract = {}

for pid, title, abstract in zip(df.pid, df.title, df.abstract):
  dict_title_abstract[pid] = {'title': title, 'abstract': abstract}
# cargamos diccionario de embeddings por cada documento (pre-procesado)
w2vec_vectors = json.load(open('documents_w2vec.json'))<jupyter_output><empty_output><jupyter_text>creamos un objeto *gensim.keyedvectors* para hacer más eficiente la búsqueda de documentos similares<jupyter_code>embedding_size = 300

doc2vec = gensim.models.keyedvectors.Word2VecKeyedVectors(embedding_size)
keys = list(w2vec_vectors.keys())
values = [
    w2vec_vectors[key]
    for key in keys
]
doc2vec.add(keys, values)<jupyter_output><empty_output><jupyter_text>Generar recomendaciones función **find_similar** para encontrar documentos similares a un pid en particular que recibe id del documento y los topn documentos mas similares y retorna topn documentos más similares<jupyter_code>def find_similar(pid, topn):
  results = []

  for id_, score in doc2vec.similar_by_vector(doc2vec[pid], topn=topn):
      results.append([id_, score, dict_title_abstract[int(id_)]['title'], dict_title_abstract[int(id_)]['abstract']])

  return pd.DataFrame(results[1:], columns = ['pid', 'score', 'title', 'abstract'])
find_similar('22508578', 10)<jupyter_output><empty_output><jupyter_text>función **recommend** para recomendar a un usuario de acuerdo una muestra de documentos que ha leído.<jupyter_code>def recommend(user_id, topn, sample_user):
  user_docs =  df[df.user_id==user_id]['pid'].sample(sample_user)

  results = []

  for pid in user_docs:

    for id_, score in doc2vec.similar_by_vector(doc2vec[str(pid)], topn=topn):

      if int(id_) in dict_title_abstract:
        results.append([id_, score, dict_title_abstract[int(id_)]['title'], dict_title_abstract[int(id_)]['abstract']])
    

  results = sorted(results, key = lambda x: int(x[1]))

  return pd.DataFrame(results[topn:], columns = ['pid', 'score', 'title', 'abstract']).head(10)
# documentos leidos por el usuario 
df[df.user_id==348892].sample(10)
recommend(user_id= 348892, topn= 10, sample_user = 5)<jupyter_output><empty_output><jupyter_text>RESPONDER:- ¿Qué problemas puede tener la recomendación basada en contenido?<jupyter_code><jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.09754633951193492, 'fraction_numerical': 0.007400986798239765, 'mean_word_length': 5.622075055187638, 'pattern_counts': {'":': 0, '<': 96, '<?xml version=': 0, '>': 96, 'https://': 8, 'lorem ipsum': 0, 'www.': 3, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 2, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '23', 'dir': 'jupyter-structured-clean-dedup', 'id': '665661', 'license': 'MIT', 'n_tokens_mistral': 5270, 'n_tokens_neox': 5103, 'n_words': 1358, 'path': 'ayudantias/tutorial-content-based.ipynb', 'repo_name': 'PUC-RecSys-Class/RecSysPUC-2019', 'size': '15025'}