/
exender
/
coursework-data-analysis
Обзор
Документация
Войти
/
exender
/
coursework-data-analysis
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
task49_nlp.py
289 строк
15 KB
exender
Create: task49_nlp.py, task48_images.py, task64_timeseries.py, task58_insurance.py
08 июн 2026, 14:44
Верифицирован
08 июн 2026, 14:44
0500eb6
Код
Авторство
О чём код?
""" Задача 49: Краткое изложение документов и генерация тестов (NLP) Датасет: CNN/DailyMail News Summarization Dataset Источник: https://huggingface.co/datasets/abisee/cnn_dailymail Установка зависимостей: pip install pandas numpy matplotlib seaborn nltk datasets Запуск: python task49_nlp.py """ import numpy as np import pandas as pd import matplotlib matplotlib.use('Agg') import matplotlib.pyplot as plt import seaborn as sns import re from collections import Counter import warnings warnings.filterwarnings('ignore') # ─── 0. Загрузка данных ──────────────────────────────────────────────────── def load_cnn_dailymail(): """Загружает CNN/DailyMail через HuggingFace datasets.""" try: from datasets import load_dataset print("Загружаем CNN/DailyMail 3.0.0 через HuggingFace...") dataset = load_dataset('abisee/cnn_dailymail', '3.0.0') # Берём выборку из train для анализа train_sample = dataset['train'].select(range(5000)) df = pd.DataFrame({ 'article': train_sample['article'], 'summary': train_sample['highlights'] }) print(f"Загружено {len(df)} пар статья–резюме (выборка из train)") return df except Exception as e: print(f"HuggingFace недоступен: {e}") return None def generate_synthetic_nlp_data(): """Генерирует синтетические текстовые данные, имитирующие CNN/DailyMail.""" print("Генерируем синтетические текстовые данные...") np.random.seed(42) n = 2000 # Распределения длин (в словах) article_lengths = np.round(np.random.lognormal(6.6, 0.5, n)).astype(int).clip(50, 2100) summary_lengths = np.round(np.random.lognormal(3.9, 0.35, n)).astype(int).clip(12, 250) # Генерация псевдо-текстов нужной длины vocab = ['the', 'a', 'an', 'in', 'of', 'to', 'and', 'is', 'was', 'are', 'president', 'government', 'said', 'year', 'people', 'police', 'country', 'city', 'world', 'new', 'officials', 'state', 'report', 'company', 'market', 'data', 'model', 'system', 'network', 'analysis'] articles, summaries = [], [] for al, sl in zip(article_lengths, summary_lengths): art = ' '.join(np.random.choice(vocab, al)) summ = ' '.join(np.random.choice(vocab, sl)) articles.append(art) summaries.append(summ) return pd.DataFrame({'article': articles, 'summary': summaries}) # Загрузка df = load_cnn_dailymail() if df is None: df = generate_synthetic_nlp_data() # ─── 1. Базовая статистика ────────────────────────────────────────────────── def tokenize(text): """Простая токенизация (слова).""" return re.findall(r'\b\w+\b', text.lower()) print("\nВычисляем статистику длин...") df['article_words'] = df['article'].apply(lambda x: len(tokenize(x))) df['summary_words'] = df['summary'].apply(lambda x: len(tokenize(x))) df['compression_ratio'] = df['article_words'] / df['summary_words'].clip(lower=1) print("\n" + "="*60) print("СТАТИСТИКА ДЛИН ТЕКСТОВ") print("="*60) for col, label in [('article_words', 'Статьи'), ('summary_words', 'Резюме'), ('compression_ratio', 'Коэфф. компрессии')]: s = df[col].describe() print(f"\n{label}:") print(f" Среднее: {s['mean']:.1f} | Медиана: {df[col].median():.1f} | " f"СКО: {s['std']:.1f} | Мин: {s['min']:.0f} | Макс: {s['max']:.0f}") # ─── 2. Гистограммы длин ──────────────────────────────────────────────────── def plot_length_distributions(): fig, axes = plt.subplots(1, 3, figsize=(16, 5)) fig.suptitle('Распределение длин текстов датасета CNN/DailyMail', fontsize=14, fontweight='bold') # Длина статей axes[0].hist(df['article_words'], bins=50, color='steelblue', edgecolor='white', alpha=0.85) axes[0].axvline(df['article_words'].mean(), color='red', linestyle='--', label=f'Среднее: {df["article_words"].mean():.0f}') axes[0].axvline(df['article_words'].median(), color='orange', linestyle=':', label=f'Медиана: {df["article_words"].median():.0f}') axes[0].set_title('Длина статей (слова)', fontweight='bold') axes[0].set_xlabel('Количество слов') axes[0].set_ylabel('Частота') axes[0].legend(fontsize=9) axes[0].grid(alpha=0.3) # Длина резюме axes[1].hist(df['summary_words'], bins=40, color='coral', edgecolor='white', alpha=0.85) axes[1].axvline(df['summary_words'].mean(), color='navy', linestyle='--', label=f'Среднее: {df["summary_words"].mean():.0f}') axes[1].axvline(df['summary_words'].median(), color='green', linestyle=':', label=f'Медиана: {df["summary_words"].median():.0f}') axes[1].set_title('Длина резюме (слова)', fontweight='bold') axes[1].set_xlabel('Количество слов') axes[1].set_ylabel('Частота') axes[1].legend(fontsize=9) axes[1].grid(alpha=0.3) # Коэффициент компрессии ratio_clipped = df['compression_ratio'].clip(upper=50) axes[2].hist(ratio_clipped, bins=40, color='mediumseagreen', edgecolor='white', alpha=0.85) axes[2].axvline(df['compression_ratio'].median(), color='red', linestyle='--', label=f'Медиана: {df["compression_ratio"].median():.1f}x') axes[2].set_title('Коэффициент компрессии\n(статья / резюме)', fontweight='bold') axes[2].set_xlabel('Коэффициент компрессии') axes[2].set_ylabel('Частота') axes[2].legend(fontsize=9) axes[2].grid(alpha=0.3) plt.tight_layout() plt.savefig('fig10_nlp_lengths.png', dpi=150, bbox_inches='tight') plt.close() print("Рисунок 10 сохранён: fig10_nlp_lengths.png") plot_length_distributions() # ─── 3. Частотный анализ токенов ──────────────────────────────────────────── STOP_WORDS = { 'the', 'a', 'an', 'in', 'of', 'to', 'and', 'is', 'was', 'are', 'that', 'for', 'on', 'with', 'at', 'from', 'by', 'this', 'be', 'as', 'it', 'or', 'not', 'he', 'she', 'they', 'we', 'i', 'his', 'her', 'their', 'have', 'has', 'had', 'will', 'would', 'could', 'about', 'its', 'but', 'were', 'been', 'after', 'also', 'said', 'who', 'which', 'what', 'into', 'up', 'when', 'there', 'more', 'than', 'two', 'one', 'all', 'out' } def plot_word_frequency(): print("\nАнализируем частотность токенов (выборка 500 статей)...") sample_texts = ' '.join(df['article'].sample(min(500, len(df)), random_state=42).tolist()) tokens = tokenize(sample_texts) tokens_no_stop = [t for t in tokens if t not in STOP_WORDS and len(t) > 2] counter = Counter(tokens_no_stop) top_n = 20 top_words = counter.most_common(top_n) words, freqs = zip(*top_words) fig, axes = plt.subplots(1, 2, figsize=(16, 6)) fig.suptitle('Частотный анализ токенов CNN/DailyMail', fontsize=13, fontweight='bold') # Топ слов (горизонтальный bar) colors = plt.cm.viridis(np.linspace(0.2, 0.9, top_n)) axes[0].barh(words[::-1], freqs[::-1], color=colors) axes[0].set_title(f'Топ-{top_n} слов (без стоп-слов)', fontweight='bold') axes[0].set_xlabel('Частота') axes[0].grid(alpha=0.3, axis='x') # Закон Ципфа all_freqs = sorted(counter.values(), reverse=True)[:200] ranks = np.arange(1, len(all_freqs) + 1) axes[1].loglog(ranks, all_freqs, 'o-', markersize=3, color='steelblue', alpha=0.7, label='Данные') # Теоретическая линия Ципфа C = all_freqs[0] zipf_theory = C / ranks axes[1].loglog(ranks, zipf_theory, 'r--', linewidth=2, label='Закон Ципфа (1/r)') axes[1].set_title('Закон Ципфа (log-log)', fontweight='bold') axes[1].set_xlabel('Ранг') axes[1].set_ylabel('Частота') axes[1].legend() axes[1].grid(alpha=0.3, which='both') plt.tight_layout() plt.savefig('fig11_nlp_frequency.png', dpi=150, bbox_inches='tight') plt.close() print("Рисунок 11 сохранён: fig11_nlp_frequency.png") plot_word_frequency() # ─── 4. Анализ пересечения токенов статья–резюме (ROUGE-подобная метрика) ──── def compute_overlap(article, summary): """Доля токенов резюме, присутствующих в статье.""" art_tokens = set(tokenize(article)) sum_tokens = set(tokenize(summary)) if not sum_tokens: return 0.0 return len(sum_tokens & art_tokens) / len(sum_tokens) print("\nВычисляем ROUGE-1 recall (доля токенов резюме в статье)...") sample = df.sample(min(500, len(df)), random_state=42) sample = sample.copy() sample['overlap'] = sample.apply( lambda row: compute_overlap(row['article'], row['summary']), axis=1) print(f"ROUGE-1 overlap: среднее = {sample['overlap'].mean():.3f}, " f"медиана = {sample['overlap'].median():.3f}") print(f"→ Высокое значение ({sample['overlap'].median():.2f}) указывает на " f"преимущественно экстрактивный характер резюме") # ─── 5. Анализ дубликатов ─────────────────────────────────────────────────── print("\n" + "="*60) print("АНАЛИЗ ДУБЛИКАТОВ") print("="*60) dup_summaries = df['summary'].duplicated().sum() print(f"Дублирующиеся резюме: {dup_summaries} ({dup_summaries/len(df)*100:.2f}%)") df_clean = df.drop_duplicates(subset=['summary']).reset_index(drop=True) print(f"После удаления дубликатов: {len(df_clean)} записей") # ─── 6. Визуализация итоговая ──────────────────────────────────────────────── def plot_summary_stats(): fig, axes = plt.subplots(2, 2, figsize=(14, 10)) fig.suptitle('Итоговый анализ датасета CNN/DailyMail', fontsize=14, fontweight='bold') # Scatter: длина статьи vs длина резюме subset = df.sample(min(1000, len(df)), random_state=1) axes[0, 0].scatter(subset['article_words'], subset['summary_words'], alpha=0.3, s=15, color='steelblue') axes[0, 0].set_title('Длина статьи vs. длина резюме', fontweight='bold') axes[0, 0].set_xlabel('Слов в статье') axes[0, 0].set_ylabel('Слов в резюме') axes[0, 0].grid(alpha=0.3) # Коэффициент компрессии violin axes[0, 1].violinplot([df['compression_ratio'].clip(upper=40)], positions=[1], showmedians=True) axes[0, 1].set_title('Распределение коэф. компрессии', fontweight='bold') axes[0, 1].set_ylabel('Коэффициент компрессии') axes[0, 1].set_xticks([1]) axes[0, 1].set_xticklabels(['Все документы']) axes[0, 1].grid(alpha=0.3) # Overlap histogram axes[1, 0].hist(sample['overlap'], bins=30, color='coral', edgecolor='white', alpha=0.85) axes[1, 0].axvline(sample['overlap'].median(), color='navy', linestyle='--', label=f'Медиана: {sample["overlap"].median():.2f}') axes[1, 0].set_title('Распределение ROUGE-1 overlap', fontweight='bold') axes[1, 0].set_xlabel('Доля токенов резюме в статье') axes[1, 0].set_ylabel('Частота') axes[1, 0].legend() axes[1, 0].grid(alpha=0.3) # Длины резюме boxplot axes[1, 1].boxplot(df['summary_words'], vert=True, patch_artist=True, boxprops=dict(facecolor='lightyellow'), medianprops=dict(color='red', linewidth=2)) axes[1, 1].set_title('Boxplot длин резюме', fontweight='bold') axes[1, 1].set_ylabel('Количество слов') axes[1, 1].grid(alpha=0.3) plt.tight_layout() plt.savefig('fig12_nlp_summary.png', dpi=150, bbox_inches='tight') plt.close() print("Рисунок 12 сохранён: fig12_nlp_summary.png") plot_summary_stats() # ─── 7. Итоговые выводы ───────────────────────────────────────────────────── print("\n" + "="*60) print("ИТОГОВЫЕ ВЫВОДЫ") print("="*60) print(f"• Всего проанализировано: {len(df):,} пар статья–резюме") print(f"• Средняя длина статьи: {df['article_words'].mean():.0f} слов") print(f"• Средняя длина резюме: {df['summary_words'].mean():.0f} слов") print(f"• Средний коэффициент компрессии: {df['compression_ratio'].mean():.1f}x") print(f"• ROUGE-1 overlap (медиана): {sample['overlap'].median():.2f} → экстрактивный характер") print(f"• Дубликаты резюме: {dup_summaries} ({dup_summaries/len(df)*100:.2f}%)") print(f"\nРекомендации для задачи 49:") print(" 1. Использовать mT5-small или ruT5-base для русскоязычных документов") print(" 2. Fine-tuning на 5 000–10 000 парах из CNN/DailyMail даёт базовую модель") print(" 3. Для специализации на техдокументации ЧКПЗ собрать 500–1 000 пар на рус. языке") print(" 4. Оценивать качество по ROUGE-1/2/L и BERTScore") print("\n✓ Анализ NLP датасета завершён.") print(" Файлы: fig10_nlp_lengths.png, fig11_nlp_frequency.png, fig12_nlp_summary.png")