Sistemas de recuperación de información (SRI), procesamiento del lenguaje natural (PLN), motores de búsqueda (MB), Modelo de recuperación información (MRI).
Como se ha ido comprobando a lo largo del curso, los sistemas de recuperación de información tienen que cumplir con el objetivo de otorgar documentos, responder preguntas, procesar paginas web y darles relevancia, entre otras muchas técnicas. Conocemos principalmente los motores de búsquedas dedicados a la tarea de recibir una consulta y entregar una serie de documentos y páginas web que considera relevantes. La explicación exacta de como funciona o qué fases realizan MBs como Chrome o mozilla va más allá del contenido que abarca este trabajo. Este trabajo está enfocado en el desarrollo de un modelo vectorial y otro booleano, que actúan sobre tres corpus distintos(Cran, Med y 20newsgroup).
El modelo vectorial se destaca por el empleo de vectores no binarios y por recuperar los documentos de acuerdo a su ranking de relevancia; esto le da un mayor índice de recuperación de documentos relevantes que el modelo booleano. Cuando hablamos de vectores no binarios, nos referimos a que los documentos pasan por un proceso donde se convierten a un vector, donde cada una de sus componentes son calculadas empleando la fórmula de
El modelo booleano se basa en la coincidencia exacta de términos entre el vector de la query con los documentos. Para ello emplea vectores booleanos y no hay existencia de un ranking por lo que todos los documentos son igual de relevantes. A pesar de ser más sencillo puede lograr buenos resultados con corpus que distingan bien sus temas y documentos.
Debemos destacar que solo existen tres modelos clásicos y sobre ellos se han ido construyendo otros más avanzados. Estos modelos "evolucionados" tienen diversas particularidades, como el uso de redes neuronales en el modelo vectorial o redes de inferencia en el probabilístico; esto los hace más certeros en cuanto a recuperar información. Incluso con el uso de estas evoluciones de los MRI, no debemos ser absolutos, pues no hay un modelo superior a otro, todo depende de sobre que corpus estemos trabajando, quiénes serán nuestros usuarios, para qué se desarrolla la el modelo, se quiere lograr eficiencia a coste de exactitud total de términos o mayor complejidad temporal en favor de obtener documentos relevantes sin total exactitud en los términos. Muchas son las preguntas que nos hacen decantarnos por un modelo y no por otro, y solo con el análisis de los posibles escenarios es posible una buena elección del Modelo de Recuperación de Información.
Nota
Debido al tamaño de las tablas de
Para la ejecución de la aplicación se hace necesario manejar ciertas depedencias que dejaremos descritas en esta sección del informe.
En el proyecto se encuetra actualmente configurado un entorno virtual para la ejecución del mismo por medio de pipenv; por tanto se hace necesario contar con ese paquete instalado para poder ejecutar la instalación de todas las dependencias de forma automática y no tener que preocuparse por esa questión; de cualquier manera se puede realizar de las dos formas.
Cómo instalar pipenv ?
pip3 install pipenv
Cómo ejecutar el proyecto una vez instalado pipenv.
cd `project root`
pipenv shell
pipenv install --ignore-pipfile
Los comandos anteriores garantizan la instalación adecuada de todas las dependencias del proyecto.
De cualquier modo es probable que haya que descargar los útiles para el funcionamiento de nltk de forma manual, para ello seguir los siguientes pasos.
cd `project root`
python3
import nltk
nltk.download(´paquete´)
Cada paquete debe cambiarse por el nombre real del paquete y es necesario contar con los siguientes instalados.
- stopwords
- averaged_perceptron_tagger
- wordnet
- omw-1.4
- punkt
Una vez que se encuentren correctamente instaladas todas las dependencias, ya sea por medio del entorno virtual o manualmente, ejecutamos el siguiente comando para correr la aplicación.
uvicorn main:app
Esta operación crea un servidor que se ejecuta por el puerto 8000 de 127.0.0.1; por tanto para solo resta abrir el navegador e introducir la siguiente dirección:
El modelo vectorial es de los más empleados hoy en día en el campo de los SRI. Sus diferentes variantes hacen de este un modelo versátil capaz de desenvolverse bien en muchos escenarios. Para la correcta elaboración de este modelo dividimos su implementación en cuatro fases principales descritas a continuación:
Procesamiento de documentos :
En esta fase nos encargamos de dado un documento reducir su tamaño los más posible, de forma tal que su representación vectorial sea menor, se mantenga su idea central y términos claves. Para esta tarea empleamos la biblioteca de python
def doc_to_tokens(self, plain_text, use_stemmer=False, use_lematizer=False):
# remove from:, mail addr and subject or Subject
plain_text = self.__remove_especial_exp(plain_text)
# tokenize and set to lower
tokens = word_tokenize(plain_text)
tokens = [t.lower() for t in tokens]
# remove regular expr.
tokens = self.__remove_punct_reg(tokens)
# filters all non words
tokens = [word for word in tokens if word.isalpha()]
# filters all stepwords
stop_words = set(stopwords.words('english'))
tokens = [t for t in tokens if not t in stop_words]
# word to root word
if use_stemmer:
stemmer = SnowballStemmer(language="english")
tokens = [stemmer.stem(word) for word in tokens]
if use_lematizer:
lemmatizer = WordNetLemmatizer()
tokens = [lemmatizer.lemmatize(word) for word in tokens]
return tokens
El flujo de trabajo con esta función es el siguiente
-
Eliminar las expresiones "From:", direcciones de correo y "Subject" o "subject".
-
Convertir las palabras en tokens y todas en minúscula.
-
Eliminar los signos de puntuación.
-
Eliminar todo lo que no represente palabras (Ej: números).
-
Eliminar las stopwords del inglés (Ej: and, for...).
-
Llevar las palabras a su raíz mediante el uso de lematizer o stemmer.
En las fases
En la fase
Vectorización de documentos y cálculo de
El módulo
Una proceso de suma importancia es el que se lleva a cabo en la siguiente función:
def start_search_engine_indexing(self):
try:
self.doc_tf = self.retrieve_from_disk(TF_DOCS_FILE)
self.idf = self.retrieve_from_disk(IDF_FILE)
self.doc_wights = self.retrieve_from_disk(DOCS_W)
self.doc_norm = self.retrieve_from_disk(NORM_DOCS)
self.query_exp.term_correlation = self.retrieve_from_disk(CORR)
except:
for dj, file in self.docs_id.items():
plain_text = self.cl.get_text(file)
tokens = self.cl.doc_to_tokens(plain_text, use_lematizer=True)
self.query_exp.get_corr_in_text(tokens)
self.__calc_tf(tokens, dj)
self.__calc_idf()
self.__calc_weights()
self.save_to_disk(CORR, self.query_exp.term_correlation)
self.save_to_disk(TF_DOCS_FILE, self.doc_tf)
self.save_to_disk(IDF_FILE, self.idf)
self.save_to_disk(DOCS_W, self.doc_wights)
self.save_to_disk(NORM_DOCS, self.doc_norm)
El flujo es el siguiente:
- Intentar extraer del disco duro las tablas donde se almacenaron los
$tf$ ,$idf$ , vectores de documentos, normas de los vectores de documentos y la tabla de correlación de términos. - Si existe algún problema en la fase anterior se procede a recorrer todos los ids y directorios de los documentos.
- Se busca el docuemento original y luego se tokeniza y procesa el texto mediante la fase anteriormente explicada.
- Se obtiene la matriz de correlación de términos.
- Se calcula la tabla
$tf$ de cada documento con sus términos. - Se calculan la tabla
$idf$ . - Cálculo de componentes del vector (
$calc_weights()$ , función que se emplea también para calcular las normas de los vectores de los documentos). - Finalmente se guardan en el disco duro las tablas obtenidas.
En los puntos
Para el punto
def __calc_tf(self, tokens, dj):
aux = {}
for t in tokens:
try:
aux[t, dj] += 1
except KeyError:
aux[t, dj] = 1
try:
self.invert_index[t] += 1
except KeyError:
self.invert_index[t] = 1
max_freq_tok = max(aux.values())
aux = {(key, dj): aux[key, dj] / max_freq_tok for key, _ in aux}
self.doc_tf.update(aux)
En el código se emplean diccionarios para representar tablas. Este método recibe una lista de tokens del documento y el id del mismo. Luego, empleando un diccionario auxiliar que representa la frecuencia de tokens en el documento que se analiza, se revisa cada token, si ya se encuentra la llave
La tabla
Para mantener en cuantos documentos se encuentra el token
Una vez revisada la lista de tokens del documento, se pasa a encontrar la frecuencia del token que más aparece, se divide cada valor del diccionario auxiliar por dicha frecuencia (normalización) y se actualiza el diccionario
En el punto 6 se desarrolla mediante la siguiente función:
def __calc_idf(self):
for t in self.invert_index:
self.idf[t] = math.log10(self.corpus_size / self.invert_index[t])
Para llevar a cabo el cálculo de la tabla
El punto
def __calc_weights(self):
for t in self.invert_index:
for d in self.docs_id:
try:
self.doc_wights[t, d] = self.doc_tf[t, d] * self.idf[t]
except KeyError:
pass
#calculate norms
for dj in self.docs_id:
self.doc_norm[dj] = np.linalg.norm([self.doc_wights[k] for k in self.doc_wights if k[1] == dj])
Finalmente para el cálculo de los pesos del vector documento la idea es ir por cada uno de los documentos y realizar la multiplicación
Aquí están plasmados algunos procesos análogos para cualquier corpus o modelo que se emplee.
Empleo de métricas
Para poder llevar a cabo una análisis de los modelos desarrollados se programaron las principales métricas estudiadas en conferencia: precisión, recobrado y
$P = \frac{rr}{rr+nr}$ $R = \frac{rr}{rr+rn}$ $f_1 = \frac{2PR}{P+R}$
Como la precisión y el recobrado se descompensan entre sí (si aumenta el recobrado disminuye la precisión y viceversa), es más fiable el análisis mediante
Las funciones empleadas para las métricas son las siguientes:
def recoverd_docs(self, retrived_docs: dict, relevant_docs: list):
rr = 0
nr = 0
for dicc in retrived_docs:
if dicc['id'] in relevant_docs:
rr += 1
else:
nr += 1
return rr, nr
def precision(self, retrived_docs: dict, relevant_docs: list):
rr, nr = self.recoverd_docs(retrived_docs, relevant_docs)
if rr and nr:
return (rr / (rr + nr)) * 100
else:
return 0
def recall(self, retrived_docs: dict, relevant_docs: list):
rr, _ = self.recoverd_docs(retrived_docs, relevant_docs)
rn = abs(len(relevant_docs) - rr)
if rr and rn:
return (rr / (rr + rn)) * 100
else:
return 0
def f1(self, retrived_docs: dict, relevant_docs: list):
p = self.precision(retrived_docs, relevant_docs)
r = self.recall(retrived_docs, relevant_docs)
if p != 0 or r != 0:
return (2 * p * r / (p + r))
else:
return 0
La primera función es la encargada de devolvernos los documentos relevantes recuperados y los no relevantes recuperados, lo que se obtiene de forma sencilla si tenemos el conjunto de recuperados y de los considerados relevantes por los expertos.
Nota
Como apunte que debemos tener en cuenta los documentos que se consideran relevantes es debido a que un grupo de expertos determina su relevancia. Consideran un documento relevante si responde a queries definidas para los corpus cran y med, para ello no necesariamente tienen que aparecer los términos de la query en el documento, pues puede ser que se responda con sinónimos o de otra forma.
Parseo de corpus
Los corpus cran y med tienen una forma particular en que están confeccionados. En el caso de cran, un documento tiene la siguiente estructura:
.I 1148
.T
knudsen flow through a circular capillary .
.A
w. c. demarcus and e. h. hopper
.B
carbide and carbon chemicals company, k-25 plant, post office box p,
oak ridge, tennessee
.W
knudsen flow through a circular capillary .
the problem of knudsen flow through a circular capillary has been
often discussed, usually by the momentum transfer method . however,
p. clausing gave a rigorous formulation for the problem and obtained an
integral equation for which he gave an approximate solution . from
time to time the accuracy of clausing's solution has been questioned
and since clausing did not give a rigorous estimate of his error we
have reinvestigated the problem .
Para el caso de med un ejemplo es el siguiente:
.I 1
.W
correlation between maternal and fetal plasma levels of glucose and free
fatty acids .
correlation coefficients have been determined between the levels of
glucose and ffa in maternal and fetal plasma collected at delivery .
significant correlations were obtained between the maternal and fetal
glucose levels and the maternal and fetal ffa levels . from the size of
the correlation coefficients and the slopes of regression lines it
appears that the fetal plasma glucose level at delivery is very strongly
dependent upon the maternal level whereas the fetal ffa level at
delivery is only slightly dependent upon the maternal level .
El módulo
Testeo de los modelos:
Para llevar a cabo el testeo de los diferentes modelos en cada corpus se creó el módulo
Escaneo del corpus:
Este es el mecanismo mediante el cual se obtiene todos los posibles documentos a partir de una dirección. Para ello recursivamente analiza los subdirectorios y va conformando una lista con todos las direcciones de los documentos que encuentra.
def __scan_corpus(self, path) -> List[str]:
if self.corpus_name == '20newsgroup':
directories = listdir(path)
else:
directories = sorted(listdir(path), key=lambda e: int(e))
files_found = []
for e in directories:
file_path = join(path, e)
if not isfile(file_path):
files_found += self.__scan_corpus(file_path)
else:
files_found.append(file_path)
return files_found
Como se puede observar al inicio existe un pequeño conflicto sobre como listar el directorio. Esto se debe a que para probar y utilizar el corpus cran y med es necesario que los documentos estén listados en orden. Esto se debe a que su nombre es un número y es eso lo que se emplea para determinar el id del documento y poder determinar, entre otras cosas, que fueron relevantes para una query.
Extracción de asuntos de documentos:
funcionalidad implementada de esta clase es
def get_subjects(self, file_paths):
subj = []
for _, fp in file_paths:
file = self.get_text(fp)
subject = re.findall('Subject:[^\n]*', file)
if subject:
subject = subject[0].replace('Subject:', '')
subject = subject.replace('Re:', '')
subject = subject.strip()
else:
subject = fp
subj.append(subject)
return subj
Para llevar a cabo la expansión de queries se aplican tres métodos fundamentales. La idea fundamental es sugerir al usuario nuevas queries relacionadas con la consulta que está realizando. De esta forma se puede lograr una recuperación de documentos basada en otros criterios como los sinónimos. Estos métodos de expansión por lo general tienden a tener un mejor índice de recuperación que con las consultas originales, aunque no se descartan resultados peores, pues se pueden sugerir nuevas queries con términos no presentes en los documentos.
Expansión de query mediante sinónimos:
La idea se basa en que muchas búsqueda pueden verse
El modelo booleano es uno de los modelos clásicos de recuperación de información. Este está basado en la lógica booleana y la teoría de conjuntos. Los documentos a buscar y la consulta del usuario, son concebidos como un conjunto de términos. La representación está basada en cuando los documentos contienen o no a los términos de la consulta.
El modelo booleano posee, notablemente, ciertas ventajas que hace que quizá llame un poco la atención de quienes quieren adentrarse en el tema de la recuperación de información, puesto que es un modelo simple basado en conjuntos que presenta grandes facilidades a la hora de implementarlo y comprenderlo; pero por le otro lado posee también varias limitaciones radicando estas principalmente en el hecho de que no posee ranking, solo recupera los documentos donde la coincidencia es exacta - no hay correspondencia parcial -, además de considerar a todos los términos igual de importantes; por solo mencionar algunas.
La implementación del modelo se realizó auxiliándose de la tabla de tf previamente computada. Esta contiene la frecuencia de cada término en los documentos y por tanto la información de si el término
A la query por su parte se le aplica el proceso tokenizacion donde se expanden las convierten las palabras a minúscula, se remueven los signos de puntuación, se remueven las palabras que contengan dígitos, asi como los stopwards y se aplica finalmente el proceso de lemantización; del mismo modo ocurre con el modelo vectorial.
Luego con la ayuda de los set() de python se va se va realizando el filtrado correspondiente para garantizar que los documentos que salgan al final de todo el proceso, sean precisamente los documentos que contienten todos los términos de las queries.
ans = []
for t in query_tokens:
temp = [id_doc for (ter, id_doc) in keys if ter == t]
if len(ans) == 0:
ans = temp.copy()
ans = list(set(temp) & set(ans))
if ans is None or len(ans) == 0:
break
return [(id_doc, self.docs_ids[id_doc]) for id_doc in ans]
En el frontend se brinda la posibilidad de abrir cada documento, de forma tal que pueda revisarse el contenido de cada uno de los retornados y además la posibilidad de decir si es relevante o no; para ello se presenta la clase Feedback.
Feedback contiene dos métodos: get_feedback
y set_feedback
, luego lo que se pretende básicamaente es: por cada documento para para el que el usuario provea un feedback, almacenar de alguna forma que se manejó que para la consulta
Para garantizar el adecuado almacenamiento de estos datos y el acceso a los mismo de manera constante se emplea un Trie, que no es más que una estructura arborea para guardar cadena, y que la inserción y la búsqueda en los mismo son lineal respecto al tamaño de la cadena
class Feedback:
def __init__(self):
self.first: Node = Node()
def set_feedback(self, _type: int, doc_id, query: List[str]):
node = self.first
for t in query:
if not node.terms.__contains__(t):
node.terms[t] = Node()
node = node.terms[t]
if _type == -1:
if node.relevant_docs.__contains__(doc_id):
node.relevant_docs.remove(doc_id)
node.no_relevant_docs.add(doc_id)
else:
if node.no_relevant_docs.__contains__(doc_id):
node.no_relevant_docs.remove(doc_id)
node.relevant_docs.add(doc_id)
def get_feedback(self, query: List[str]) -> Tuple[Set, Set]:
node = self.first
for t in query:
if node.terms.__contains__(t):
node = node.terms[t]
else:
return None
return node.relevant_docs, node.no_relevant_docs
De qué forma influye el feedback brindando por los usuarios en los resultado de las próximas respuestas ?
Por cada nueva query que se realize, se efectua un reajuste en los pesos de términos de esta, lo que asegura que se de importancia al feedback que otros usuarios han brindado para la misma. En principio si para otra query con esos mismos términos ya se realizó algún feedback, lo que se hace es construir una nueva que responde a la siguiente estructura:
$$
\begin{align*}
q_n = q + \frac{\beta}{|R|} * d_r - \frac{\delta}{|NR|} &* d_{nr} \
\beta = 0.5, \alpha = 1 \
\end{align*}
$$
En otras palabras, los pesos de los términos de la nueva query serán la suma a los pesos originales del producto de una constante
El algoritmo en resumen lo que hace es darle mayor importancia a los términos más usados en los documentos considerados relevantes para los usuarios, y por el contrario, los términos más usados en los documentos no relevantes se les da menor importancia.
El front se encuentra desarrollado en Vue, exponiendo una implementación en extremo sencilla, pero que es capaz que cubrir todas las necesidades del proyecto; se utilizó un servidor de vue durante el desarrollo del mismo, para posteriormente general los archivos estáticos necesarios para el correcto funcionamiento con la lógica escrita en python. Para realizar las peticiones al servidor de python se utliza fetch, interfaz que se encuentra expuesta y que es manejada por todos los navegadores actuales a la hora de consultar recursos.
De cualquier forma consultar el siguiente enlance para tener más detalles acerca de la implementación del mismo.
https://github.com/abelo98/Final-Project-MRI/tree/master/interface