Text Summarization: Сжатие текста с LLM

nlpsummarizationllmopensourcetext-generation
← Back to Blog

Введение

Text Summarization — это задача автоматического создания краткого представления длинного текста. LLM произвели революцию в этой области, сделав суммаризацию значительно качественнее классических методов.


Типы суммаризации

Extractive Summarization

Извлечение ключевых предложений из оригинального текста:

Оригинал:
  "Искусственный интеллект (ИИ) — область информатики.
   ИИ включает создание умных агентов.
   Машинное обучение — часть ИИ.
   Глубокое обучение — часть машинного обучения.
   Нейронные сети — основа глубокого обучения."

Извлечённые предложения:
  "Искусственный интеллект (ИИ) — область информатики."
  "Машинное обучение — часть ИИ."
  "Нейронные сети — основа глубокого обучения."

Методы:

  • TextRank — графовый ранжирование предложений
  • LexRank — улучшенный TextRank с TF-IDF
  • BERTScore — ранжирование по семантической важности

Abstractive Summarization

Генерация нового текста, который передаёт суть:

Оригинал:
  "В 2023 году было выпущено более 100 новых LLM.
   OpenAI выпустила GPT-4, Meta — LLaMA 2, Google — Gemini.
   Эти модели показывают впечатляющие результаты в различных
   задачах: от генерации текста до программирования."

Абстрактная суммаризация:
  "В 2023 году появилось более 100 новых больших языковых моделей,
   включая GPT-4, LLaMA 2 и Gemini, показавших впечатляющие результаты."

Extractive Summarization

TextRank

import networkx as nx
from sklearn.metrics.pairwise import cosine_similarity
import numpy as np

class TextRankSummarizer:
    def __init__(self, similarity_threshold=0.8):
        self.threshold = similarity_threshold
    
    def sentence_similarity(self, sent1, sent2):
        # Простое TF-IDF сходство
        vec1 = self.tfidf.transform([sent1])
        vec2 = self.tfidf.transform([sent2])
        return cosine_similarity(vec1, vec2)[0][0]
    
    def rank_sentences(self, sentences):
        # Строим граф сходства
        graph = nx.Graph()
        
        for i, sent1 in enumerate(sentences):
            for j, sent2 in enumerate(sentences):
                if i != j:
                    sim = self.sentence_similarity(sent1, sent2)
                    if sim > self.threshold:
                        graph.add_edge(i, j, weight=sim)
        
        # PageRank
        pr = nx.pagerank(graph, weight='weight')
        return sorted(pr.items(), key=lambda x: x[1], reverse=True)
    
    def summarize(self, text, num_sentences=3):
        sentences = self.tokenize(text)
        ranked = self.rank_sentences(sentences)
        
        # Сохраняем оригинальный порядок
        selected = sorted(ranked[:num_sentences], key=lambda x: sentences.index(x[0]))
        return ' '.join([s[0] for s in selected])

BERT-based Extractive

from transformers import AutoTokenizer, AutoModel
import torch
import numpy as np

class BERTSummarizer:
    def __init__(self, model_name="bert-base-uncased"):
        self.tokenizer = AutoTokenizer.from_pretrained(model_name)
        self.model = AutoModel.from_pretrained(model_name)
    
    def get_sentence_embedding(self, sentence):
        inputs = self.tokenizer(sentence, return_tensors="pt", 
                               truncation=True, max_length=512)
        with torch.no_grad():
            outputs = self.model(**inputs)
        # Используем [CLS] токен
        return outputs.last_hidden_state[0, 0].numpy()
    
    def summarize(self, text, num_sentences=3, max_length=500):
        sentences = self.split_sentences(text)
        
        # Получаем эмбеддинги
        embeddings = np.array([
            self.get_sentence_embedding(s) for s in sentences
        ])
        
        # Ранжируем по важности
        scores = np.linalg.norm(embeddings, axis=1)
        top_indices = np.argsort(scores)[-num_sentences:]
        
        # Сохраняем порядок
        selected = sorted(top_indices)
        return ' '.join([sentences[i] for i in selected])

Abstractive Summarization с LLM

Zero-shot Summarization

from openai import OpenAI

client = OpenAI()

response = client.chat.completions.create(
    model="gpt-4o",
    messages=[
        {
            "role": "system",
            "content": """Ты — эксперт по суммаризации текстов.
Создай краткое резюме текста в 3-5 предложениях.
Перед резюме укажи ключевые темы."""
        },
        {"role": "user", "content": long_article_text}
    ],
    temperature=0.3
)

# Результат:
# Ключевые темы: LLM, AI, OpenAI, Meta
# Резюме: В 2023 году рынок больших языковых моделей ...

Controlled summarization

# Суммаризация с контролем длины
response = client.chat.completions.create(
    model="gpt-4o",
    messages=[
        {
            "role": "system",
            "content": """Суммируй текст.
- Краткое резюме: 1 предложение
- Основное резюме: 3 предложения
- Детальное резюме: 7 предложений"""
        },
        {"role": "user", "content": article}
    ]
)

# Или через structured output:
response = client.chat.completions.create(
    model="gpt-4o",
    messages=[{"role": "user", "content": f"Суммируй: {article}"}],
    response_format={
        "type": "json_object",
        "schema": {
            "one_sentence_summary": "string",
            "three_sentence_summary": "string",
            "detailed_summary": "string",
            "key_points": ["string"]
        }
    }
)

Few-shot Summarization

messages = [
    {"role": "system", "content": "Суммируй тексты."},
    
    {"role": "user", "content": "Длинный текст о погоде..."},
    {"role": "assistant", "content": "Ожидаются осадки..."},
    
    {"role": "user", "content": "Длинный текст о спорте..."},
    {"role": "assistant", "content": "Команда победила..."},
    
    {"role": "user", "content": long_article}
]

response = client.chat.completions.create(
    model="gpt-4o-mini",
    messages=messages
)

Summarization с локальными моделями

llama.cpp для суммаризации

import requests

def summarize_with_llama(text, max_tokens=300):
    prompt = f"""Суммируй следующий текст в 3-5 предложениях.

Текст:
{text}

Резюме:"""

    response = requests.post(
        "http://localhost:8080/completion",
        json={
            "prompt": prompt,
            "max_tokens": max_tokens,
            "temperature": 0.3,
            "stop": ["Текст:", "\n\n"]
        }
    )
    
    return response.json()["response"]

Fine-tuned модели для суммаризации

from transformers import AutoModelForSeq2SeqLM, AutoTokenizer

# BART для суммаризации
model_name = "facebook/bart-large-cnn"
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForSeq2SeqLM.from_pretrained(model_name)

def summarize_bart(text, max_length=150, min_length=50):
    inputs = tokenizer(
        "summarize: " + text,
        return_tensors="pt",
        max_length=1024,
        truncation=True
    )
    
    summaries = model.generate(
        inputs.input_ids,
        max_length=max_length,
        min_length=min_length,
        length_penalty=1.0,
        num_beams=4
    )
    
    return tokenizer.decode(summaries[0], skip_special_tokens=True)

T5 для суммаризации

from transformers import AutoModelForSeq2SeqLM, AutoTokenizer

model_name = "google/t5-v1_1-base"
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForSeq2SeqLM.from_pretrained(model_name)

# T5 использует префиксы для задач
inputs = tokenizer(
    "summarize: " + text,
    return_tensors="pt",
    max_length=512,
    truncation=True
)

output = model.generate(
    inputs.input_ids,
    max_length=150,
    do_sample=True,
    temperature=0.7,
    top_p=0.9
)

summary = tokenizer.decode(output[0], skip_special_tokens=True)

Long document summarization

Map-Reduce

Step 1 (Map): Разделить документ на части
  [Часть 1] [Часть 2] [Часть 3] [Часть 4]

Step 2 (Map): Суммаризировать каждую часть
  [Резюме 1] [Резюме 2] [Резюме 3] [Резюме 4]

Step 3 (Reduce): Объединить и суммаризировать
  [Итоговое резюме]
def map_reduce_summarization(text, chunk_size=2000):
    # Map: разбиваем на чанки
    chunks = split_text(text, chunk_size)
    
    # Map: суммаризируем каждый чанк
    chunk_summaries = []
    for chunk in chunks:
        summary = llm_summarize(chunk)
        chunk_summaries.append(summary)
    
    # Reduce: объединяем и суммаризируем
    combined = '\n'.join(chunk_summaries)
    final_summary = llm_summarize(combined)
    
    return final_summary

Recursive summarization

def recursive_summarization(text, max_tokens=4000):
    """Рекурсивная суммаризация для очень длинных документов"""
    chunks = split_text(text, max_tokens // 2)
    
    if len(chunks) == 1:
        return llm_summarize(text)
    
    # Суммаризируем каждую половину
    mid = len(chunks) // 2
    summary1 = recursive_summarization(''.join(chunks[:mid]))
    summary2 = recursive_summarization(''.join(chunks[mid:]))
    
    # Объединяем
    combined = f"{summary1}\n{summary2}"
    return recursive_summarization(combined)

Summarization с контекстным окном

def sliding_window_summarization(text, window_size=3000, overlap=500):
    """Суммаризация с скользящим окном"""
    summaries = []
    start = 0
    
    while start < len(text):
        end = start + window_size
        chunk = text[start:end]
        
        summary = llm_summarize(chunk)
        summaries.append(summary)
        
        start = end - overlap
    
    # Финальная суммаризация
    return llm_summarize('\n'.join(summaries))

Оценка суммаризации

Метрики

ROUGE (Recall-Oriented Understudy for Gisting Evaluation):
  ROUGE-1: сходство по униграммам
  ROUGE-2: сходство по биграммам
  ROUGE-L: сходство по longest common subsequence

BERTScore:
  Семантическое сходство с помощью BERT
  
BLEU:
  Precision n-gram matching
  
METEOR:
  Учитывает синонимы и стемминг

Код оценки

import rouge
from transformers import pipeline

def evaluate_summarization(true_summary, pred_summary):
    # ROUGE
    rouge_evaluator = rouge.Rouge()
    rouge_scores = rouge_evaluator.get_scores(
        pred_summary, true_summary
    )
    
    # BERTScore
    bertscore = pipeline("bertscore")
    scores = bertscore([pred_summary], [true_summary])
    
    return {
        "rouge1": rouge_scores["rouge-1"]["f"],
        "rouge2": rouge_scores["rouge-2"]["f"],
        "rougeL": rouge_scores["rouge-l"]["f"],
        "bertscore_f1": np.mean(scores["f1"])
    }

Практические применения

1. Новостные агрегаторы

# Автоматическая суммаризация новостей
articles = load_news(category="technology")

for article in articles:
    summary = summarize(article["content"], sentences=3)
    print(f"{article['title']}: {summary}")

2. Анализ документов

# Суммаризация юридических документов
legal_doc = load_legal_document("contract.pdf")

summary = llm_summarize(legal_doc, {
    "system": "Суммируй ключевые условия договора."
})

# Извлечение ключевых пунктов
key_points = llm_extract(summary, {
    "system": "Извлеки ключевые пункты: сроки, стороны, обязательства."
})

3. Meeting notes

# Суммаризация записей встреч
transcript = speech_to_text(meeting_recording)

summary = llm_summarize(transcript, {
    "system": """Создай структурированное резюме встречи:
- Принятые решения
- Задачи на следующую неделю
- Ответственные лица"""
})

4. Academic papers

# Суммаризация научных статей
paper = load_arxiv_paper("2301.12345")

summary = llm_summarize(paper["full_text"], {
    "system": """Суммируй научную статью:
- Проблема
- Метод
- Результаты
- Выводы"""
})

Open Source модели

Seq2Seq модели:
  ✅ BART (Facebook)
  ✅ T5 (Google)
  ✅ PEGASUS (Google)
  ✅ mBART (multilingual)

Генеративные модели:
  ✅ Llama 3
  ✅ Mistral
  ✅ Falcon
  ✅ Qwen

Специализированные:
  ✅ SummEval (benchmark)
  ✅ XLSum (multilingual)
  ✅ CNN/DailyMail (dataset)

Итоги

Summarization с LLM стала значительно доступнее. Zero-shot подход работает для большинства задач, а fine-tuned модели дают хороший баланс качества и стоимости.

Ключевые выводы:

  • Extractive — быстро, но ограниченно
  • Abstractive — качественнее, но дороже
  • Map-Reduce для длинных документов
  • ROUGE — основной метрик, но не идеальный
  • Локальные модели (BART, T5) для приватных данных