Sign In
Free Sign Up
  • English
  • Español
  • 简体中文
  • Deutsch
  • 日本語
Sign In
Free Sign Up
  • English
  • Español
  • 简体中文
  • Deutsch
  • 日本語

Cómo resumir documentos extensos con LangChain y OpenAI

Los modelos de lenguaje grandes han facilitado muchas tareas como la creación de chatbots, la traducción de idiomas, la síntesis de texto, etc. Solíamos escribir modelos para la síntesis de texto, y siempre había problemas de rendimiento. Ahora, podemos hacer esto fácilmente con el uso de modelos de lenguaje grandes (LLMs, por sus siglas en inglés). Por ejemplo, los LLMs de última generación ya pueden manejar un libro completo en su ventana de contexto. Sin embargo, todavía existen algunas limitaciones al resumir documentos muy extensos.

# Limitaciones de la síntesis de texto de documentos extensos mediante LLMs

El límite contextual o longitud del contexto en un LLM se refiere al número de tokens que un modelo puede procesar. Cada modelo tiene su propia longitud de contexto, también conocida como límite máximo de tokens. Por ejemplo, un modelo GPT-4 estándar tiene una longitud de contexto de 128,000 tokens. Perderá información para los tokens que excedan ese límite. Algunos LLMs de última generación tienen un límite contextual de hasta 1 millón de tokens. Sin embargo, a medida que aumenta el límite contextual, los LLMs sufren limitaciones como el efecto de primacía y el efecto de recencia. También podemos explorar formas de mitigar estos efectos.

  • El efecto de primacía en los LLMs se refiere a que el modelo otorga más importancia a la información presentada al principio de una secuencia.
  • El efecto de recencia se refiere a que el modelo enfatiza la información más reciente que procesa.

Ambos efectos sesgan el modelo hacia partes específicas de los datos de entrada. El modelo puede omitir información importante en medio de la secuencia.

El segundo problema es el costo. Podemos resolver el primer problema del límite contextual dividiendo el texto, pero simplemente no podemos pasar el libro completo directamente al modelo. Eso costaría mucho. Por ejemplo, si tenemos 1 millón de tokens de un libro y lo pasamos directamente al modelo GPT-4, el costo total sería de alrededor de $90 (tokens de inicio y finalización). Tenemos que encontrar un punto intermedio para resumir nuestro texto considerando el precio, el límite contextual y el contexto completo del libro.

En este tutorial, aprenderás a resumir un libro completo teniendo en cuenta el precio y el límite contextual del modelo. Empecemos.

# Resumir documentos extensos con LangChain y OpenAI

# Configuración del entorno

Para seguir el tutorial, necesitas tener:

  • Python instalado
  • Un IDE (VS Code funcionará)

Para instalar las dependencias, abre tu terminal y ejecuta el siguiente comando:

pip install langchain openai tiktoken fpdf2 pandas

Este comando instalará todas las dependencias necesarias.

# Cargar el libro

Utilizarás el libro "David Copperfield" de Charles Dickens, que está disponible públicamente para este proyecto. Carguemos el libro utilizando la utilidad PyPDFLoader proporcionada por LangChain.

from langchain.document_loaders import PyPDFLoader

# Cargar el libro
loader = PyPDFLoader("David-Copperfield.pdf")
pages = loader.load_and_split()

Cargará el libro completo, pero solo nos interesa la parte del contenido. Podemos omitir las páginas como el Prefacio y la Introducción.

# Recortar las partes de apertura y cierre
pages = pages[6:1308]
# Combinar las páginas y reemplazar las tabulaciones por espacios
text = ' '.join([page.page_content.replace('\t', ' ') for page in pages])

Ahora tenemos el contenido. Imprimamos los primeros 200 caracteres.

text[0:200]

# Preprocesamiento

Eliminemos el contenido innecesario del texto, como caracteres no imprimibles, espacios adicionales, etc.

import re
def clean_text(text):
   # Eliminar la frase específica 'Free eBooks at Planet eBook.com' y los espacios en blanco circundantes
   cleaned_text = re.sub(r'\s*Free eBooks at Planet eBook\.com\s*', '', text, flags=re.DOTALL)
   # Eliminar espacios adicionales
   cleaned_text = re.sub(r' +', ' ', cleaned_text)
   # Eliminar caracteres no imprimibles, opcionalmente precedidos por 'David Copperfield'
   cleaned_text = re.sub(r'(David Copperfield )?[\x00-\x1F]', '', cleaned_text)
   # Reemplazar caracteres de nueva línea por espacios
   cleaned_text = cleaned_text.replace('\n', ' ')
   # Eliminar espacios alrededor de guiones
   cleaned_text = re.sub(r'\s*-\s*', '', cleaned_text)
   return cleaned_text
clean_text=clean_text(text)

Después de limpiar los datos, estamos listos para abordar el problema de resumir.

# Cargar la API de OpenAI

Antes de usar la API de OpenAI, necesitamos configurarla y proporcionar las credenciales aquí.

import os
os.environ["OPENAI_API_KEY"] = "tu-clave-de-openai-aquí"

Ingresa tu clave de API allí y configurará la variable de entorno.

Veamos cuántos tokens tenemos en el libro:

from langchain import OpenAI
llm = OpenAI()
Tokens = llm.get_num_tokens(clean_text)
print (f"Tenemos {Tokens} tokens en el libro")

Tenemos más de 466,000 tokens en este libro, y si los pasamos todos directamente al LLM, nos cobraría mucho. Entonces, para reducir el costo, implementaremos el agrupamiento K-means para extraer las partes importantes del libro.

Nota: La decisión de utilizar el agrupamiento K-means fue inspirada por el tutorial del gurú de datos Greg Kamradt (opens new window).

Para obtener las partes importantes del libro, primero dividamos el libro en diferentes fragmentos.

# Dividir el contenido en documentos

Dividiremos el contenido del libro en documentos utilizando la utilidad SemanticChunker de LangChain.

from langchain_experimental.text_splitter import SemanticChunker
from langchain_openai.embeddings import OpenAIEmbeddings
text_splitter = SemanticChunker(
   OpenAIEmbeddings(), breakpoint_threshold_type="interquartile"
)
docs = text_splitter.create_documents([clean_text])

El SemanticChunker recibe dos argumentos, el primero es el modelo de embeddings. Los embeddings generados por este modelo se utilizan para dividir el texto en función de la semántica. El segundo es el breakpoint_threshold_type, que determina los puntos en los que se debe dividir el texto en diferentes fragmentos en función de la similitud semántica.

Nota: Al procesar estos fragmentos más pequeños y semánticamente similares, nuestro objetivo es minimizar los efectos de recencia y primacía en nuestro LLM. Esta estrategia permite que nuestro modelo maneje cada contexto pequeño de manera más efectiva, asegurando una interpretación y generación de respuestas más equilibradas.

# Encontrar los embeddings de cada documento

Ahora, obtengamos los embeddings de cada documento generado. Obtendrás los embeddings utilizando el método predeterminado de OpenAI.

import numpy as np
import openai
def get_embeddings(text):
   response = openai.embeddings.create(
       model="text-embedding-3-small",
       input=text
   )
   return response.data
embeddings=get_embeddings([doc.page_content for doc in docs]
)

El método get_embeddings nos da los embeddings de todos los documentos.

Nota: El método text-embedding-3-small fue lanzado especialmente por OpenAI, y se considera más económico y rápido.

# Reorganizar los datos

A continuación, convertiremos las listas de contenidos de los documentos y sus embeddings en un DataFrame de pandas para facilitar el manejo y análisis de datos.

import pandas as pd
content_list = [doc.page_content for doc in docs]
df = pd.DataFrame(content_list, columns=['page_content'])
vectors = [embedding.embedding for embedding in embeddings]
array = np.array(vectors)
embeddings_series = pd.Series(list(array))
df['embeddings'] = embeddings_series
Boost Your AI App Efficiency now
Sign up for free to benefit from 150+ QPS with 5,000,000 vectors
Free Trial
Explore our product

# Aplicar Faiss para un agrupamiento eficiente

Ahora, transformaremos los vectores de documentos en un formato compatible con Faiss (opens new window), los agruparemos en 50 grupos utilizando K-means y luego crearemos un índice de Faiss para búsquedas de similitud eficientes entre documentos.

**<code>import numpy as np \
import faiss \
<em># Convertir a float32 si aún no lo es</em> \
array = array.astype('float32')  \
num_clusters = 50 \
<em># Dimensionalidad de los vectores</em> \
dimension = array.shape[1]  \
<em># Entrenar KMeans con Faiss</em> \
kmeans = faiss.Kmeans(dimension, num_clusters, niter=20, verbose=True) \
kmeans.train(array) \
<em># Acceder directamente a los centroides</em> \
centroids = kmeans.centroids  \
<em># Crear un nuevo índice para el conjunto de datos original</em> \
index = faiss.IndexFlatL2(dimension) \
<em># Agregar el conjunto de datos original al índice</em> \
index.add(array)</code></strong> 

Este agrupamiento K-means agrupará los documentos en 50 grupos.

Nota: La razón de elegir el agrupamiento K-means es que cada grupo tendrá un contenido similar o un contexto similar porque todos los documentos dentro de ese grupo tienen embeddings relacionados, y seleccionaremos el que esté más cerca del núcleo.

# Seleccionar los documentos importantes

Ahora, solo seleccionaremos el documento más importante de cada grupo. Para esto, solo seleccionaremos el vector más cercano al centroide.

D, I = index.search(centroids, 1)

Este código utiliza el método de búsqueda en el índice para encontrar el documento más cercano a cada centroide en la lista de centroides. Devuelve dos matrices: D, que contiene las distancias de los documentos más cercanos a sus respectivos centroides, e I, que contiene los índices de estos documentos más cercanos. El segundo parámetro 1 en el método de búsqueda especifica que solo se debe encontrar el documento más cercano para cada centroide.

Ahora necesitamos ordenar los índices de documentos seleccionados porque los documentos están en secuencia con respecto a la secuencia del libro.

sorted_array = np.sort(I, axis=0)
sorted_array=sorted_array.flatten()
extracted_docs = [docs[i] for i in sorted_array]

# Obtener el resumen de cada documento

El siguiente paso es obtener el resumen de cada documento utilizando el modelo GPT-4 para ahorrar dinero. Para usar GPT-4, definamos el modelo.

model = ChatOpenAI(temperature=0,model="gpt-4")

Definamos el texto de inicio y creemos una plantilla de texto de inicio utilizando LangChain para pasarlo al modelo.

from langchain_core.output_parsers import StrOutputParser
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
prompt = ChatPromptTemplate.from_template("""
Se te darán diferentes pasajes de un libro uno por uno. Proporciona un resumen del siguiente texto. Tu resultado debe ser detallado y tener al menos 2 párrafos. Al resumir, sumérgete directamente en la narrativa o descripciones del texto sin usar frases introductorias como 'En este pasaje'. Aborda directamente los eventos principales, los personajes y los temas, encapsulando la esencia y los detalles significativos del texto en una narrativa fluida. El objetivo es presentar una visión unificada del contenido, continuando la historia de manera fluida como si el pasaje progresara naturalmente hacia el resumen.

Pasaje:

```{text}```
RESUMEN:
"""
)

Esta plantilla de texto de inicio ayudará al modelo a resumir los documentos de manera más efectiva y eficiente.

El siguiente paso es definir una cadena de LangChain utilizando el Lenguaje de Expresión de LangChain (LCEL).

chain= (
    prompt
   | model
   |StrOutputParser() )

La cadena de resumen utiliza el StrOutputParser (opens new window) para analizar la salida. También hay otros analizadores de salida (opens new window) para explorar.

Finalmente, aplica la cadena definida a cada documento para obtener un resumen.

from tqdm import tqdm
final_summary = ""

for doc in tqdm(extracted_docs, desc="Procesando documentos"):
   # Obtén el nuevo resumen.
   new_summary = chain2.invoke({"text": doc.page_content})
   # Actualiza la lista de los dos últimos resúmenes: elimina el primero y agrega el nuevo al final.
   final_summary+=new_summary

El código anterior aplica la cadena a cada documento uno por uno y concatena cada resumen en final_summary.

# Guardar el resumen como PDF

El siguiente paso es dar formato al resumen y guardarlo en formato PDF.

from fpdf import FPDF

class PDF(FPDF):
   def header(self):
       # Seleccionar Arial en negrita 15
       self.set_font('Arial', 'B', 15)
       # Mover a la derecha
       self.cell(80)
       # Título enmarcado
       self.cell(30, 10, 'Resumen', 1, 0, 'C')
       # Salto de línea
       self.ln(20)

   def footer(self):
       # Ir a 1,5 cm desde abajo
       self.set_y(-15)
       # Seleccionar Arial en cursiva 8
       self.set_font('Arial', 'I', 8)
       # Número de página
       self.cell(0, 10, 'Página %s' % self.page_no(), 0, 0, 'C')

# Instanciar objeto PDF y agregar una página
pdf = PDF()
pdf.add_page()
pdf.set_font("Arial", size=12)

# Asegurarse de que el texto de 'last_summary' se trate como UTF-8
# Reemplaza 'last_summary' con tu variable de texto real si es diferente
# Asegúrate de que tu texto sea una cadena codificada en UTF-8
last_summary_utf8 = last_summary.encode('latin-1', 'replace').decode('latin-1')
pdf.multi_cell(0, 10, last_summary_utf8)

# Guardar el PDF en un archivo
pdf_output_path = "s_output1.pdf"
pdf.output(pdf_output_path)

¡Ahora tenemos el resumen completo del libro en formato PDF!

Join Our Newsletter

# Conclusión

En este tutorial, hemos navegado por las complejidades de resumir textos extensos como libros enteros utilizando LLMs, al tiempo que abordamos desafíos relacionados con los límites contextuales y el costo. Hemos aprendido los pasos para preprocesar el texto e implementar una estrategia que combina la segmentación semántica y el agrupamiento K-means para gestionar las limitaciones contextuales del modelo de manera efectiva. Mediante el uso de un agrupamiento eficiente, hemos extraído de manera eficiente los pasajes clave, reduciendo la sobrecarga de procesar textos masivos directamente. Este enfoque no solo reduce significativamente los costos al minimizar el número de tokens procesados, sino que también mitiga los efectos de recencia y primacía inherentes en los LLMs, asegurando una consideración equilibrada de todos los segmentos de texto.

Ha habido una gran emoción por desarrollar aplicaciones de IA a través de las API de LLMs, donde las bases de datos vectoriales desempeñan un papel importante al ofrecer un almacenamiento y recuperación eficientes de embeddings contextuales. MyScaleDB (opens new window) es una base de datos vectorial diseñada específicamente para aplicaciones de IA, teniendo en cuenta todos los factores como el costo, la precisión y la velocidad. Su interfaz compatible con SQL permite a los desarrolladores comenzar a desarrollar sus aplicaciones de IA sin tener que aprender algo nuevo.

Si deseas discutir más con nosotros, te invitamos a unirte a MyScale Discord (opens new window) para compartir tus pensamientos y comentarios.

Este artículo fue publicado originalmente en The New Stack. (opens new window)

Keep Reading

Start building your Al projects with MyScale today

Free Trial
Contact Us