Análisis de similitud semántica de textos con TensorFlow Hub y Dataflow

Este es el segundo artículo de una serie en la que se describe cómo realizar un análisis de similitud semántica de documentos con incorporaciones de texto. Las incorporaciones se extraen con el módulo de Universal Sentence Encoder tf.Hub, en una canalización de procesamiento escalable con Dataflow y tf.Transform. Luego, las incorporaciones extraídas se almacenan en BigQuery, donde se calcula la similitud coseno entre estas incorporaciones a fin de recuperar los documentos que presentan una mayor similitud en términos semánticos. El código de implementación está en el repositorio de GitHub asociado.

Para obtener más información sobre los conceptos de incorporaciones y los casos prácticos, consulta la descripción general: extracción y entrega de las incorporaciones de características para el aprendizaje automático.

Introducción

Para encontrar documentos relacionados en una colección, puedes utilizar distintas técnicas de recuperación de información. Un enfoque consiste en extraer las palabras clave y hacer coincidir los documentos en función del número de términos que estos tienen en común. Sin embargo, con este enfoque se omiten documentos en los que se utilizan términos similares, pero no idénticos.

Otro enfoque es el análisis de similitud semántica, que se examina en este artículo. Con el análisis de similitud de textos, puedes obtener documentos relevantes, aunque no tengas palabras clave de la Búsqueda adecuadas para encontrarlos. En su lugar, puedes encontrar artículos, libros, documentos y comentarios de los clientes mediante la búsqueda con documentos representativos.

Este artículo se centra en el análisis de similitud de textos basado en incorporaciones. Sin embargo, también puedes utilizar un enfoque similar para otros tipos de contenido, como imágenes, audios y videos, siempre que puedas convertir los contenidos objetivo en incorporaciones.

En este artículo, se explica lo siguiente:

  • El uso de Apache Beam y tf.Transform para procesar archivos de texto
  • El uso del módulo de Universal Sentence Encoder (tf.Hub) de TensorFlow Hub para extraer incorporaciones de texto de títulos y contenidos de artículos
  • La ejecución de la canalización de procesamiento de textos a gran escala con Dataflow
  • El almacenamiento de los artículos procesados y sus incorporaciones en BigQuery
  • La búsqueda de artículos similares en BigQuery a través de una secuencia de comandos SQL de similitud coseno

Arquitectura de soluciones

En la figura 1 se muestra la arquitectura general de la solución de análisis de similitud de textos. En el caso de los datos textuales, la solución utiliza Reuters-21578, que es una colección de artículos disponibles para el público. El conjunto de datos se describe en el conjunto de datos de Reuters más adelante en este artículo. Los documentos de ejemplo se cargan en Cloud Storage. La canalización de procesamiento se implementa mediante Apache Beam y tf.Transform, y se ejecuta a gran escala en Dataflow.

Arquitectura de una solución de alto nivel que representa el análisis de similitud de textos
Figura 1. Arquitectura de una solución de alto nivel que representa el análisis de similitud de textos

En la canalización, los documentos se procesan para extraer el título, los temas y el contenido de cada artículo. La canalización de procesamiento usa el módulo de Universal Sentence Encoder en tf.Hub a fin de extraer incorporaciones de texto para el título y el contenido de cada artículo. Estos valores, así como las incorporaciones extraídas, se almacenan en BigQuery. Poder almacenar los artículos y las incorporaciones en BigQuery te permite explorar artículos similares con la métrica de similitud coseno entre las incorporaciones de títulos y contenidos.

Conceptos clave

En la siguiente lista, se explican los conceptos ilustrados en la figura 1.

Cloud Storage
Cloud Storage permite almacenar y recuperar cualquier cantidad de datos en todo el mundo y en cualquier momento. Puedes usar Cloud Storage en distintas situaciones, incluida la distribución de archivos de datos grandes destinados al procesamiento y a las estadísticas. En esta solución, los documentos del artículo fuente se almacenan en un bucket de Cloud Storage, que se considera un data lake para los documentos sin procesar. Además, se almacenará una versión de los datos procesados (es decir, la colección de incorporaciones extraídas) en forma de conjunto de archivos TFRecord en Cloud Storage, el cual se utilizará para entrenar modelos del AA más adelante.
Apache Beam
Apache Beam es un modelo de programación unificada de código abierto que ejecuta trabajos de procesamiento de datos por lotes y transmisión. Esta solución utiliza Apache Beam para implementar la canalización Extraer, Transformar, Cargar (ETL): 1) leer datos sin procesar de Cloud Storage; 2) procesar artículos y extraer incorporaciones, y 3) almacenar incorporaciones y artículos en BigQuery.
tf.Transform
TensorFlow Transform es una biblioteca para preprocesar datos con TensorFlow. En esta solución, se usa tf.Transform como contexto para llamar al módulo tf.Hub a fin de extraer la incorporación de texto.
Dataflow
Dataflow es un servicio completamente administrado, sin servidores y confiable para ejecutar canalizaciones de Apache Beam a gran escala en Google Cloud. Dataflow se usa para escalar los datos de procesamiento del texto de entrada y las extracciones de las incorporaciones a fin de almacenarlos en BigQuery.
Incorporaciones de texto
En el aprendizaje automático (AA), una incorporación de texto es un vector de atributos de valor real que representa la semántica de una palabra (por ejemplo, mediante el uso de Word2vec) o una oración (por ejemplo, mediante Universal Sentence Encoder). Las incorporaciones pueden entrenarse previamente en contextos genéricos o entrenarse para tareas específicas. Las incorporaciones de texto se usan para representar atributos de entrada textuales en modelos del AA, como clasificación, regresión y agrupamiento en clústeres.
tf.Hub
TensorFlow Hub es una biblioteca de módulos del AA reutilizables. Estos módulos pueden ser modelos entrenados previamente o incorporaciones extraídas de textos, imágenes, entre otros. Esta solución utiliza el módulo de incorporación de texto entrenado previamente del Codificador de Oraciones Universal para convertir el título y el contenido de cada artículo en un vector de características numéricas (incorporación). Este vector de características puede utilizarse para calcular la similitud entre diferentes artículos.
BigQuery
BigQuery es el almacén de datos de estadísticas de bajo costo, a escala de petabytes y completamente administrado de Google. Esta solución almacena los artículos y las incorporaciones extraídas en BigQuery para que puedan consultarse más adelante.
Similitud coseno
La similitud coseno es una medida de similitud entre dos vectores distintos de cero de un espacio de producto interno, que se basa en el coseno del ángulo entre ellos. En esta solución, se usa con el fin de calcular la similitud entre dos artículos o de hacer coincidir un artículo basado en una consulta de búsqueda, en función de las incorporaciones extraídas. Si dos vectores de incorporación de texto son similares, la similitud coseno entre ellos produce un valor cercano a 1.

El conjunto de datos de Reuters

La solución descrita en este artículo utiliza Reuters-21587, distribución 1.0, que es una colección de artículos de noticias disponibles para el público. Los artículos del conjunto de datos aparecieron en el servicio de noticias por satélite de Reuters en 1987. Los miembros del personal de Reuters Ltd. y de Carnegie Group, Inc. se encargaron de indexarlos y ensamblarlos con categorías en 1987. En 1990, Reuters y CGI pusieron los documentos a disposición del Lab de Recuperación de Información del Departamento de Informática y Ciencias de la Información de la Universidad de Massachusetts (en Amherst) para fines de investigación.

La descripción completa del conjunto de datos se puede encontrar en el archivo readme.txt de la recopilación. Los atributos clave del conjunto de datos son los siguientes:

  • Cantidad total de artículos: 21,578
  • Cantidad de archivos: 22
  • Formato de archivos: lenguaje de marcación generalizado estándar (SGML) en archivos .sgm
  • Cantidad de artículos por archivo: 1,000, excepto el último archivo, que contiene 578 artículos

A partir de las distintas etiquetas de cada artículo, la solución extrae lo siguiente:

  • Título: el título del artículo
  • Cuerpo: el contenido completo del texto del artículo
  • Temas: una o más categorías a las que pertenece el artículo

Crear la canalización ETL con Apache Beam

El código para la canalización está en el módulo de Python de pipeline.py, que se encuentra en el repositorio de GitHub de esta solución. La canalización ETL consta de los siguientes pasos de alto nivel, que se detallan en las siguientes secciones:

  1. Lee los archivos fuente de Cloud Storage.
  2. Extrae los objetos del artículo de los archivos.
  3. Analiza cada objeto del artículo para generar el título, los temas y el contenido.
  4. Genera un vector de incorporación para el título del artículo.
  5. Genera un vector de incorporación para el contenido del artículo.
  6. Escribe el resultado de cada artículo en BigQuery.

Leer y analizar los archivos del artículo

Como se señaló antes, los datos fuente constan de varios archivos .sgm, cada uno de los cuales incluye varios artículos. La primera tarea es leer estos archivos, analizar el contenido SGML y extraer los objetos del artículo. El siguiente código muestra los primeros tres pasos de la canalización de Beam:

pipeline = beam.Pipeline(options=pipeline_options)

with impl.Context(known_args.transform_temp_dir):
    articles = (
        pipeline
        | 'Get Paths' >> beam.Create(get_paths(known_args.file_pattern))
        | 'Get Articles' >> beam.Map(get_articles)
        | 'Get Article' >> beam.FlatMap(lambda x: x)
    )

El método get_articles acepta una ruta de acceso de archivos y muestra una PCollection de artículos. El método FlatMap posterior es responsable de acoplar la colección de artículos que se muestra. Como se explicó antes, el objetivo del método get_articles es analizar un archivo .sgm y mostrar un objeto del artículo (es decir, un diccionario) que incluya el título, los temas (separados por comas) y el contenido. Este proceso se muestra en el siguiente código:

def get_articles(file_path):
  import bs4
  import tensorflow as tf

  data = tf.gfile.GFile(file_path).read()
  soup = bs4.BeautifulSoup(data, "html.parser")
  articles = []
  for raw_article in soup.find_all('reuters'):
    article = {
        'title': get_title(raw_article),
        'content': get_content(raw_article),
        'topics': get_topics(raw_article),
    }
    if None not in article.values():
      if [] not in article.values():
        articles.append(article)
  return articles

En esta solución, se usa la biblioteca Beautiful Soup (bs4) de Python para analizar los archivos .sgm.

Implementar el método preprocess_fn

Una vez que se hayan leído, analizado y extraído los artículos, el siguiente paso en la canalización ETL de Beam será generar incorporaciones de texto para el título y el contenido de cada artículo. En esta solución, la lógica de transformación se implementa en el método preprocess_fn. Para llamar a este método, se usan las API tf.Transform en la canalización de Beam, con el método AnalyzeAndTransformDataset, como se muestra en el siguiente código:

dataset = (articles, get_metadata())

transformed_dataset, transform_fn = (
    dataset
    | 'Analyse & Transform dataset' >> impl.AnalyzeAndTransformDataset(preprocess_fn)
)

El método preprocess_fn usa un diccionario de atributos de entrada, que consta de tensores de TensorFlow generados a partir de los campos de conjunto de datos creados en el paso anterior. En el ejemplo, los atributos de entrada incluyen title, topics y content. En este método, las incorporaciones se generan para el título y el contenido mediante llamadas a los respectivos métodos get_embed_title y get_embed_content, como se muestra en el siguiente código:

def preprocess_fn(input_features):
  import tensorflow_transform as tft

  title_embed = tft.apply_function(get_embed_title, input_features['title'])
  content_embed = tft.apply_function(get_embed_content, input_features['content'])
  output_features = {
      'topics': input_features['topics'],
      'title': input_features['title'],
      'content': input_features['content'],
      'title_embed': title_embed,
      'content_embed': content_embed,
  }
  return output_features

Los métodos get_embed_* usan tf.Hub para generar incorporaciones. En la siguiente sección, se proporcionan más detalles sobre este paso. El método preprocess_fn genera los atributos de entrada, junto con title_embed y content_embed, que son dos vectores de atributos de valor real.

Los beneficios de usar tf.Transform a fin de implementar la transformación en esta canalización de Apache Beam para el procesamiento de texto son los siguientes:

  • Simplicidad. Llamar a un módulo de incorporación de texto tf.Hub (descrito con más detalle en la siguiente sección) necesita el contexto de TensorFlow. Es decir, tendrías que crear un objeto tf.Graph, agregar tensores tf.placeholder, crear objetos tf.session, etc. para llamar al módulo tf.Hub. Sin embargo, en tf.Transform, la función preprocess_fn trabaja dentro de un contexto implícito de TensorFlow, en el que puedes llamar a cualquier operación de TensorFlow (incluida la llamada a un módulo tf.Hub) sin la sobrecarga de realizar todos los pasos detallados.

  • Extensibilidad. La solución utiliza incorporaciones de texto como representaciones de características del texto para realizar un análisis de similitud entre artículos. Sin embargo, a fin de preparar los datos textuales para otras tareas, incluidas la clasificación de textos y la extracción de temas, puede haber otras representaciones de características útiles, como los n-gramas para la bolsa de palabras (BOW), la frecuencia de términos (TF) y la frecuencia de términos-frecuencia de documentos inversa (TF-IDF). Para obtener más detalles, consulta cómo se presentan los datos de texto a un algoritmo que espera una entrada numérica en las guías de aprendizaje automático de Google. Estos tipos de representaciones requieren transformaciones de conjunto de datos de paso completo, para las cuales está diseñado tf.Transform; la biblioteca de tf.Transform incluye una implementación de estas transformaciones y de otras. Por lo tanto, si deseas extender esta canalización de ETL con otras transformaciones de paso completo, puedes agregarlas con facilidad en la función preprocess_fn.

Para obtener más detalles sobre el preprocesamiento de datos y las transformaciones de TensorFlow, consulta el preprocesamiento de datos para aprendizaje automático: opciones y recomendaciones y el preprocesamiento de datos para aprendizaje automático mediante transformaciones de TensorFlow en la documentación de Google Cloud.

Generar incorporaciones con TensorFlow Hub

Como se analizó antes en los conceptos clave, tf.Hub incluye un conjunto de modelos de TensorFlow entrenados previamente que te permiten generar vectores de atributos de valor real (incorporaciones) para imágenes y textos. En esta solución, se usa el módulo de incorporación de texto de Universal Sentence Encoder. El módulo acepta una oración y muestra un vector numérico de 512 dimensiones que representa la incorporación de una oración determinada.

En el siguiente código correspondiente al método get_embed_title, se muestra cómo generar y, luego, incorporar un vector para el título de un artículo determinado.

def get_embed_title(title,
    module_url='https://tfhub.dev/google/universal-sentence-encoder/2'):

  import tensorflow as tf
  import tensorflow_hub as hub

  module = hub.Module(module_url)
  embed = module(title)
  return embed

Con el objetivo de generar un vector de incorporación para el contenido de un artículo determinado, el código hace lo siguiente:

  1. Divide el artículo en oraciones
  2. Genera la incorporación de cada oración con el módulo codificador de oraciones
  3. Calcula el promedio de las incorporaciones generadas de todas las oraciones

El código produce un vector de atributos único para representar la incorporación de contenido específico, sin importar cuántas oraciones haya en el contenido de un artículo. Esto se muestra en el siguiente código para la función get_embed_content.

def get_embed_content(content, delimiter='\n',
    module_url='https://tfhub.dev/google/universal-sentence-encoder/2'):

  import tensorflow as tf
  import tensorflow_hub as hub

  module = hub.Module(module_url)

  def _map_fn(t):
    t = tf.cast(t, tf.string)
    t = tf.string_split([t], delimiter).values
    e = module(t)
    e = tf.reduce_mean(e, axis=0)
    return tf.squeeze(e)

  embed = tf.map_fn(_map_fn, content, dtype=tf.float32)
  return embed

Escribir el resultado en BigQuery

El último paso de la canalización ETL de Beam consiste en escribir el resultado del paso de procesamiento anterior en una tabla de BigQuery. Este proceso se muestra en el siguiente código:

transformed_data, transformed_metadata = transformed_dataset
(
        transformed_data
        | 'Convert to Insertable data' >> beam.Map(to_bq_row)
        | 'Write to BigQuery table' >> beam.io.WriteToBigQuery(
            project=known_args.bq_project,
            dataset=known_args.bq_dataset,
            table=known_args.bq_table,
            schema=get_bigquery_schema(),
            create_disposition=beam.io.BigQueryDisposition.CREATE_IF_NEEDED,
            write_disposition=beam.io.BigQueryDisposition.WRITE_TRUNCATE)
)

Si la tabla no existe, la canalización la creará; pero, si la tabla incluye los datos anteriores, la canalización la truncará. Puedes cambiar este comportamiento si estableces los parámetros beam.io.BigQueryDisposition.

Para crear la tabla de BigQuery, la solución necesita un objeto TableSchema. En este ejemplo, el esquema de los datos de salida es el siguiente:

  • title: string, valor nulo
  • content: string, valor nulo
  • topics: string, valor nulo
  • title_embed: flotante, repetido (porque es un arreglo de 512 elementos)
  • content_embed: flotante, repetido (porque es un arreglo de 512 elementos)

En el siguiente código, se muestra cómo crear el objeto TableSchema para BigQuery.

def get_bigquery_schema():

  from apache_beam.io.gcp.internal.clients import bigquery

  table_schema = bigquery.TableSchema()
  columns = (('topics', 'string', 'nullable'),
             ('title', 'string', 'nullable'),
             ('content', 'string', 'nullable'),
             ('title_embed', 'float', 'repeated'),
             ('content_embed', 'float', 'repeated'))

  for column in columns:
    column_schema = bigquery.TableFieldSchema()
    column_schema.name = column[0]
    column_schema.type = column[1]
    column_schema.mode = column[2]
    table_schema.fields.append(column_schema)

  return table_schema

Ejecuta la canalización en Dataflow

Para ejecutar la canalización ETL de Beam, solo debes ejecutar el módulo main.py con los argumentos requeridos y establecer el argumento --runner en DataflowRunner. El programa principal ejecuta el método run_pipeline en el módulo pipeline.py. Con una secuencia de comandos en el repositorio de GitHub, se muestra cómo establecer la configuración (por ejemplo, mediante el ajuste de las variables de entorno) y cómo ejecutar la canalización. En la siguiente lista, se muestra un comando típico para ejecutar la canalización.

python main.py \
  --file_pattern=$FILE_PATTERN \
  --bq_project=$PROJECT \
  --bq_dataset=$DATASET \
  --bq_table=$TABLE \
  --transform_temp_dir=$TRANSFORM_TEMP_DIR \
  --transform_export_dir=$TRANSFORM_EXPORT_DIR \
  --enable_tfrecord \
  --tfrecord_export_dir $TFRECORD_EXPORT_DIR \
  --enable_debug \
  --debug_output_prefix=$DEBUG_OUTPUT_PREFIX \
  --project=$PROJECT \
  --runner=$RUNNER \
  --region=$REGION \
  --staging_location=$STAGING_LOCATION \
  --temp_location=$TEMP_LOCATION \
  --setup_file=$(pwd)/setup.py \
  --job_name=$JOB_NAME \
  --worker_machine_type=n1-highmem-2

En la figura 2, se muestra la ejecución de la canalización de Dataflow en Cloud Console.

Grafo de ejecución de Dataflow de la canalización “tf.Transform”
Figura 2. Grafo de ejecución de Dataflow de la canalización tf.Transform

Explora artículos similares en BigQuery

Después de ejecutar la canalización y cargar los resultados del procesamiento de artículos de Reuters, encontrarás un conjunto de datos llamado reuters en BigQuery, con una tabla llamada embeddings. Puedes ver que las incorporaciones de título y de contenido se almacenan en la tabla como arreglos flotantes (campos repetidos). Para ver las incorporaciones, ejecuta la siguiente consulta:

#standardSQL

SELECT
  title,
  content,
  title_embed,
  content_embed
FROM
  reuters.embeddings
LIMIT 1

En la figura 3, se presenta una muestra del resultado que se obtiene cuando se ejecuta esta instrucción de SQL.

Resultado de muestra de la tabla de BigQuery `reuters.embeddings`
Figura 3. Resultado de muestra de la tabla de BigQuery reuters.embeddings

Dados dos vectores de incorporación A y B (dos arreglos title_embed o content_embed), la similitud coseno entre A y B se calcula de la siguiente manera:

$$ cosine(A,B) = \frac{\sum_{i=1}^{n}A_i \cdot B_i}{\sqrt{\sum_{i=1}^{n}{A_i^2}}\cdot\sqrt{\sum_{i=1}^{n}{B_i^2}}} $$

Aquí, n es el número de elementos presentes en el vector. En el ejemplo, el vector de incorporación tiene 512 dimensiones. Esta fórmula de similitud coseno puede implementarse en una secuencia de comandos SQL de BigQuery para encontrar el artículo que sea más similar al artículo determinado. Por ejemplo, quizás desees encontrar los artículos de Reuters más similares al que se titula "High Winds Keep Vessels Trapped in Baltic Ice" (Fuertes vientos mantienen buques atrapados en el hielo báltico). Puedes encontrar los 10 artículos más similares según el campo de incorporaciones de título (title_embed) mediante la siguiente consulta:

#standardSQL

SELECT
  c.k1 as input_article_title,
  c.k2 as similar_article_title,
  SUM(vv1*vv2) / (SQRT(SUM(POW(vv1,2))) * SQRT(SUM(POW(vv2,2)))) AS similarity
FROM
(
  SELECT
    a.key k1, a.val v1, b.key k2, b.val v2
  FROM
  (
    SELECT title key, title_embed val
    FROM reuters.embeddings
    WHERE title LIKE "HIGH WINDS KEEP VESSELS TRAPPED IN BALTIC ICE"
    LIMIT 1

   ) a
  CROSS JOIN
  (
    SELECT title key, title_embed val
    FROM reuters.embeddings
  ) b
) c
, UNNEST(c.v1) vv1 with offset ind1 JOIN UNNEST(c.v2) vv2 with offset ind2 ON (ind1=ind2)
GROUP BY c.k1, c.k2
ORDER BY similarity DESC
LIMIT 10

Verás resultados como los que se enumeran en la figura 4.

Resultados de la consulta cuando se usan incorporaciones de título en la comparación de similitud
Figura 4. Resultados de la consulta cuando se usan incorporaciones de título en la comparación de similitud

Si usas content_embed en lugar de title_embed, encontrarás los 10 artículos más similares en función de las incorporaciones de contenido, en lugar de los títulos. Los resultados se muestran en la figura 5.

Resultados de la consulta cuando se usan incorporaciones de contenido en la comparación de similitud
Figura 5. Resultados de la consulta cuando se usan incorporaciones de contenido en la comparación de similitud

Como se muestra en los resultados, aunque el título de entrada no haya incluido la palabra “barco” o “tormenta”, la consulta recuperó artículos que hablan de barcos y accidentes, porque son relevantes para los términos "viento fuerte", "buques" y "atrapado" en el título de entrada.

Pasos siguientes