Búsqueda por similitud de texto con campos de vectores

Desde los comienzos como un motor de búsqueda de recetas, Elasticsearch se diseñó para brindar una búsqueda de texto completo poderosa y rápida. Dado su origen, mejorar la búsqueda de texto ha sido una motivación importante para nuestro trabajo continuo con vectores. En Elasticsearch 7.0, presentamos tipos de campos experimentales para vectores de alta dimensión, y ahora en la versión 7.3 se incluye soporte para usar estos vectores en la puntuación de documentos.

Este blog se enfoca en una técnica en particular denominada búsqueda por similitud de texto. En este tipo de búsqueda, un usuario ingresa una búsqueda de texto libre breve, y los documentos se clasifican según la similitud con la búsqueda. La similitud de texto puede ser útil en diversos casos de uso:

  • Responder preguntas: dada una colección de preguntas frecuentes, encuentra preguntas similares a la que ingresó el usuario.
  • Búsqueda de artículos: en una colección de artículos de investigación, devuelve artículos con un título estrechamente relacionado con la búsqueda del usuario.
  • Búsqueda de imágenes: en un conjunto de datos de imágenes con pie de foto, encuentra imágenes cuyo pie de foto es similar a la descripción del usuario.

Un enfoque simple respecto de la búsqueda por similitud sería clasificar los documentos según la cantidad de palabras que comparten con la búsqueda. Pero un documento puede ser similar a la búsqueda incluso si tienen muy pocas palabras en común; una noción más robusta de similitud también tomaría en cuenta el contenido sintáctico y semántico.

La comunidad de procesamiento del lenguaje natural (NLP) ha desarrollado una técnica llamada incrustación de texto que codifica palabras y oraciones como vectores numéricos. Estas representaciones vectoriales están diseñadas para capturar el contenido lingüístico del texto y se pueden usar para evaluar la similitud entre una búsqueda y un documento.

En este blog se explora cómo las incrustaciones de texto y el tipo dense_vector de Elasticsearch podrían usarse para dar soporte a la búsqueda por similitud. Primero brindaremos una visión general de técnicas de incrustación, luego pasaremos por un prototipo simple de búsqueda por similitud con Elasticsearch.

Nota: Usar incrustaciones de texto en la búsqueda es complejo y cambiante. Esperamos que este blog te brinde un punto de partida para la exploración, pero no es una recomendación para una implementación o arquitectura de búsqueda en particular.

¿Qué son las incrustaciones de texto?

Veamos en más detalle los diferentes tipos de incrustaciones de texto y cómo se comparan con los enfoques de búsqueda tradicionales.

Incrustaciones de palabras

Un modelo de incrustación de palabras representa a una palabra como un vector numérico denso. La finalidad de estos vectores es capturar las propiedades semánticas de la palabra; las palabras cuyos vectores son cercanos deberían ser similares en términos de significado semántico. En una incrustación bien hecha, las direcciones en el espacio vectorial están vinculadas a diferentes aspectos del significado de la palabra. Por ejemplo, el vector de "Canada" puede estar cerca de "France" en una dirección y de "Toronto" en otra.

Las comunidades de NLP y búsqueda se han interesado desde hace tiempo en las representaciones vectoriales de palabras. En los últimos años, durante la revisión de muchas tareas tradicionales a través de redes neuronales, hubo un resurgimiento del interés en las incrustaciones de palabras. Se desarrollaron algunos algoritmos de incrustación de palabras, incluido word2vec y GloVe. Estos enfoques usan grandes colecciones de texto y examinan el contexto en el que aparece cada palabra para determinar su representación vectorial:

  • El modelo word2vec Skip-gram entrena una red neuronal para predecir las palabras del contexto alrededor de una palabra en una oración. Las ponderaciones internas de la red proporcionan las incrustaciones de palabras.
  • En GloVe, la similitud de las palabras depende de la frecuencia con la que aparecen con otras palabras del contexto. El algoritmo entrena un modelo lineal simple sobre recuentos de coocurrencia de palabras.

Muchos grupos de investigación distribuyen modelos previamente entrenados en grandes corpus de textos como Wikipedia o Common Crawl, por lo que resulta conveniente descargarlos e incorporarlos a tareas posteriores. Si bien las versiones previamente entrenadas suelen usarse de forma directa, puede resultar útil ajustar el modelo para que se adapte a la tarea y al conjunto de datos objetivo específicos. Por lo general, esto se logra ejecutando un paso de perfeccionamiento ligero en el modelo previamente entrenado.

Las incrustaciones de palabras han demostrado ser bastante robustas y eficaces, y ahora es habitual usar incrustaciones en lugar de tokens individuales en tareas de NLP como traducción automática y clasificación de sentimientos.

Incrustaciones de oraciones

Más recientemente, los investigadores han comenzado a enfocarse en técnicas de incrustación que representan no solo palabras, sino secciones de texto más extensas. Los enfoques más recientes se basan en arquitecturas de redes neuronales complejas y, en ocasiones, incorporan datos etiquetados durante el entrenamiento para ayudar a captar la información semántica.

Una vez entrenados, los modelos pueden tomar una oración y producir un vector para cada palabra en contexto, al igual que un vector para la oración completa. De manera similar a la incrustación de palabras, hay disponibles versiones previamente entrenadas de muchos modelos, lo que permite a los usuarios omitir el costoso proceso de entrenamiento. Mientras que el proceso de entrenamiento puede consumir muchos recursos, invocar el modelo es mucho más ligero; los modelos de incrustación de oraciones suelen ser lo suficientemente rápidos como para usarse como parte de aplicaciones en tiempo real.

Algunas técnicas de incrustación de oraciones comunes son InferSent, Universal Sentence Encoder, ELMo y BERT. La mejora de las incrustaciones de palabras y oraciones es un área activa de investigación, y es probable que se introduzcan modelos sólidos adicionales.

Comparación con enfoques de búsqueda tradicionales

En la recuperación tradicional de información, una manera común de representar texto como un vector numérico es asignar una dimensión para cada palabra en el vocabulario. Luego, el vector de un fragmento de texto se basa en la cantidad de veces que aparece cada término del vocabulario. Esta forma de representar texto suele denominarse "bolsa de palabras" porque simplemente hacemos un recuento de las repeticiones de palabras sin considerar la estructura de la oración.

Las incrustaciones de texto se diferencian de las representaciones vectoriales tradicionales en algunos aspectos importantes:

  • Los vectores codificados son densos y de relativamente baja dimensión, por lo general en un rango de 100 a 1000 dimensiones. Por el contrario, los vectores de bolsa de palabras son dispersos y pueden incluir más de 50 000 dimensiones. Los algoritmos de incrustación codifican texto en un espacio dimensional más bajo como parte del modelado de su significado semántico. Idealmente, las palabras y frases sinónimas acaban con una representación similar en el nuevo espacio vectorial.
  • Las incrustaciones de oraciones pueden tomar en cuenta el orden de las palabras para determinar la representación vectorial. Por ejemplo, la frase "tune in" puede mapearse como un vector muy diferente a "in tune".
  • En la práctica, las incrustaciones de oraciones no suelen generalizarse correctamente en grandes secciones de texto. Por lo común, no se usan para representar fragmentos más extensos que un párrafo breve.

Uso de incrustaciones para la búsqueda por similitud

Supongamos que tenemos una gran colección de preguntas y respuestas. Un usuario puede hacer una pregunta, y nosotros queremos recuperar la pregunta más similar de nuestra colección para ayudarlo a encontrar una respuesta.

Podríamos usar las incrustaciones de texto para permitir recuperar preguntas similares:

  • Durante la indexación, cada pregunta se pasa por un modelo de incrustación de oraciones para producir un vector numérico.
  • Cuando un usuario ingresa una búsqueda, se pasa por el mismo modelo de incrustación de oraciones para producir un vector. Para clasificar las respuestas, calculamos la similitud de los vectores entre cada pregunta y el vector de búsqueda. Al comparar vectores de incrustación, es habitual usar la similitud coseno.

En este repositorio se proporciona un ejemplo simple de cómo se podría lograr esto en Elasticsearch. El script principal indexa ~20 000 preguntas del conjunto de datos de StackOverflow, y luego el usuario puede ingresar búsquedas de texto libre en el conjunto de datos.

Pronto revisaremos cada parte del script en detalle, pero primero veamos algunos ejemplos de resultados. En muchos casos, el método puede capturar la similitud incluso cuando no hubo una superposición marcada entre la búsqueda y la pregunta indexada:

  • "zipping up files" devuelve "Compressing / Decompressing Folders & Files"
  • "determine if something is an IP" devuelve "How do you tell whether a string is an IP or a hostname"
  • "translate bytes to doubles" devuelve "Convert Bytes to Floating Point Numbers in Python"

Detalles de implementación

El script comienza por descargar y crear el modelo de incrustación en TensorFlow. Elegimos Universal Sentence Encoder de Google, pero es posible usar muchos otros métodos de incrustación. El script usa el modelo de incrustación como está, sin entrenamiento ni perfeccionamiento adicionales.

A continuación, creamos el índice de Elasticsearch, que incluye mapeos para el título de la pregunta, etiquetas y también el título de la pregunta codificado como vector:

"mappings": {
  "properties": {
    "title": {
      "type": "text"
    },
    "title_vector": {
      "type": "dense_vector",
      "dims": 512
    }
    "tags": {
      "type": "keyword"
    },
    ...
  }
}

En el mapeo de dense_vector, debemos especificar la cantidad de dimensiones que contendrán los vectores. Al indexar un campo title_vector, Elasticsearch comprobará que tenga la misma cantidad de dimensiones que la especificada en el mapeo.

Para indexar documentos, pasamos el título de la pregunta por el modelo de incrustación para obtener una matriz numérica. Esta matriz se agrega al documento en el campo title_vector.

Cuando un usuario ingresa una búsqueda, el texto se pasa primero por el mismo modelo de incrustación y se almacena en el parámetro query_vector. A partir de la versión 7.3, Elasticsearch proporciona una función cosineSimilarity en su lenguaje de scripting nativo. Entonces, para clasificar preguntas según su similitud con la búsqueda del usuario, usamos una búsqueda script_score:

{
  "script_score": {
    "query": {"match_all": {}},
    "script": {
      "source": "cosineSimilarity(params.query_vector, doc['title_vector']) + 1.0",
      "params": {"query_vector": query_vector}
    }
  }
}

Nos aseguramos de pasar el vector de búsqueda como parámetro de script para evitar volver a compilar el script en cada búsqueda nueva. Como Elasticsearch no admite puntuaciones negativas, es necesario agregar una a la similitud coseno.

Nota: En este blog se usó originalmente una sintaxis diferente para las funciones de vectores disponible en Elasticsearch 7.3, pero se dejó de usar en la versión 7.6.

Limitaciones importantes

La búsqueda script_score está diseñada para incluir una búsqueda restrictiva y modificar las puntuaciones de los documentos que devuelve. Sin embargo, proporcionamos una búsqueda match_all, lo que significa que el script se ejecutará en todos los documentos del índice. Esta es una limitación actual de la similitud de vectores en Elasticsearch: los vectores pueden usarse para puntuar documentos, pero no en el paso de recuperación inicial. El soporte de la recuperación basada en la similitud de vectores es un área importante de trabajo continuo.

Para evitar analizar todos los documentos y mantener un rendimiento rápido, la búsqueda match_all puede reemplazarse con una más selectiva. Probablemente, la búsqueda adecuada para usar en la recuperación dependerá del caso de uso específico.

Si bien antes vimos algunos ejemplos prometedores, es importante observar que los resultados también pueden ser desmedidos y poco intuitivos. Por ejemplo, "zipping up files" también asigna puntuaciones altas a "Partial .csproj Files" y "How to avoid .pyc files?". Y cuando el método devuelve resultados sorprendentes, no siempre está claro cómo depurar el problema; el significado de cada componente del vector suele ser confuso y no corresponder a un concepto interpretable. Con las técnicas de puntuación tradicionales basadas en la superposición de palabras, suele ser más fácil responder la pregunta "¿por qué este documento se clasifica entre los primeros?".

Como mencionamos anteriormente, el objetivo de este prototipo es servir como ejemplo de cómo podrían usarse los modelos de incrustación con campos de vectores, y no como una solución lista para la producción. Al desarrollar una estrategia de búsqueda nueva, es fundamental probar el rendimiento del enfoque en tus propios datos y asegurarte de compararlo con una referencia sólida como una búsqueda match. Puede ser necesario realizar cambios importantes en la estrategia para lograr resultados sólidos, incluido el ajuste del modelo de incrustación para el set de datos objetivo o la prueba de diferentes formas de incorporar incrustaciones como la expansión de búsqueda a nivel de la palabra.

Conclusiones

Las técnicas de incrustación proporcionan una forma poderosa de capturar el contenido lingüístico de un fragmento de texto. Al indexar las incrustaciones y asignar la puntuación según la distancia de los vectores, podemos clasificar los documentos por una noción de similitud que supera la superposición a nivel de palabras.

Esperamos presentar más funcionalidades en torno al tipo de campo de vectores. Usar vectores para búsqueda es un área importante y con matices; como siempre, nos encantaría conocer tus casos de uso y experiencias en Github y en los foros de debate.