Добавлена презентация и материалы для трёх блоков доклада
42
lab1/presentation/README.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# Презентация
|
||||
|
||||
Готовая презентация: `stardew_stylometry.pptx` (16:9, 32 слайда).
|
||||
PDF-версия для просмотра: `stardew_stylometry.pdf`.
|
||||
|
||||
## Структура
|
||||
|
||||
- **Блок 1. Корпус** — 2005 реплик, 34 персонажа, сбор, очистка, разметка,
|
||||
распределение реплик и длин, ограничения.
|
||||
- **Блок 2. Методология** — признаки, LSA, логистическая регрессия, SVM,
|
||||
деревья, ансамбли, перекрёстная проверка и метрики.
|
||||
- **Блок 3. Результаты** — сравнение моделей, вклад признаков, контроль
|
||||
содержания, матрицы ошибок, классы-аттракторы, выводы.
|
||||
|
||||
На каждом слайде есть фон `images/background.jpg` и маленькая курица
|
||||
`images/chicken.png` как пасхалка. На титульном слайде — `images/title.jpg`;
|
||||
на слайде о персонажах — `images/characters.webp`; на последнем слайде — мем
|
||||
`images/meme.webp`. Круговые диаграммы — `images/replics-count.png` и
|
||||
`images/median.png`.
|
||||
|
||||
## Сборка
|
||||
|
||||
```bash
|
||||
cd lab1
|
||||
python -m pip install -r presentation/requirements.txt
|
||||
python presentation/make_slide_figures.py
|
||||
python presentation/build_deck.py
|
||||
```
|
||||
|
||||
`make_slide_figures.py` строит матрицы ошибок и сравнительные диаграммы из
|
||||
`results/final/finalists/voting_et_char_lean/oof_predictions.csv` и
|
||||
`FINAL_RESULTS.csv`. `build_deck.py` собирает PPTX, добавляет заметки
|
||||
докладчика, фон, курицу и изображения.
|
||||
|
||||
## Редактирование
|
||||
|
||||
Текст и порядок слайдов задаются в `build_deck.py`. После изменения текста
|
||||
достаточно повторно запустить сборку. Готовый PPTX можно править и напрямую
|
||||
в PowerPoint/LibreOffice; заметки докладчика находятся в заметках слайдов.
|
||||
|
||||
Изображения игры используются в учебных целях; права принадлежат их
|
||||
правообладателям.
|
||||
809
lab1/presentation/build_deck.py
Normal file
@@ -0,0 +1,809 @@
|
||||
"""Сборка презентации по лабораторной работе.
|
||||
|
||||
Требования:
|
||||
- 16:9, фон на каждом слайде;
|
||||
- три содержательных блока на трёх рассказчиков;
|
||||
- курица на каждом слайде;
|
||||
- титульная картинка, персонажи, две круговые диаграммы, матрицы ошибок,
|
||||
мем на последнем слайде;
|
||||
- тёмный текст на светлых панелях.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from PIL import Image
|
||||
from pptx import Presentation
|
||||
from pptx.dml.color import RGBColor
|
||||
from pptx.enum.shapes import MSO_SHAPE
|
||||
from pptx.enum.text import MSO_ANCHOR, PP_ALIGN
|
||||
from pptx.util import Inches, Pt
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
IMAGES = ROOT / "presentation" / "images"
|
||||
GEN = ROOT / "presentation" / "generated"
|
||||
OUT = ROOT / "presentation" / "stardew_stylometry.pptx"
|
||||
|
||||
SW, SH = 13.333, 7.5
|
||||
FONT = "Arial"
|
||||
|
||||
CREAM = RGBColor(248, 241, 220)
|
||||
CREAM_LIGHT = RGBColor(255, 252, 240)
|
||||
DARK = RGBColor(30, 45, 33)
|
||||
GREEN = RGBColor(49, 93, 58)
|
||||
GREEN_DARK = RGBColor(28, 63, 47)
|
||||
GOLD = RGBColor(204, 153, 51)
|
||||
BROWN = RGBColor(126, 80, 42)
|
||||
RED = RGBColor(166, 55, 43)
|
||||
WHITE = RGBColor(255, 255, 255)
|
||||
BORDER = RGBColor(181, 158, 111)
|
||||
|
||||
|
||||
def add_background(slide) -> None:
|
||||
slide.shapes.add_picture(
|
||||
str(IMAGES / "background.jpg"), 0, 0, width=Inches(SW), height=Inches(SH)
|
||||
)
|
||||
|
||||
|
||||
def add_panel(slide, x, y, w, h, fill=CREAM, line=BORDER, radius: float = 0.06):
|
||||
shape = slide.shapes.add_shape(
|
||||
MSO_SHAPE.ROUNDED_RECTANGLE, Inches(x), Inches(y), Inches(w), Inches(h)
|
||||
)
|
||||
shape.fill.solid()
|
||||
shape.fill.fore_color.rgb = fill
|
||||
shape.line.color.rgb = line
|
||||
shape.line.width = Pt(1.1)
|
||||
shape.shadow.inherit = False
|
||||
try:
|
||||
shape.adjustments[0] = radius
|
||||
except Exception:
|
||||
pass
|
||||
return shape
|
||||
|
||||
|
||||
def add_text(
|
||||
slide,
|
||||
x,
|
||||
y,
|
||||
w,
|
||||
h,
|
||||
text: str,
|
||||
size: int = 18,
|
||||
color=DARK,
|
||||
bold: bool = False,
|
||||
align=PP_ALIGN.LEFT,
|
||||
anchor=MSO_ANCHOR.TOP,
|
||||
margin: float = 0.08,
|
||||
):
|
||||
box = slide.shapes.add_textbox(Inches(x), Inches(y), Inches(w), Inches(h))
|
||||
frame = box.text_frame
|
||||
frame.word_wrap = True
|
||||
frame.vertical_anchor = anchor
|
||||
frame.margin_left = Inches(margin)
|
||||
frame.margin_right = Inches(margin)
|
||||
frame.margin_top = Inches(0.04)
|
||||
frame.margin_bottom = Inches(0.04)
|
||||
paragraph = frame.paragraphs[0]
|
||||
paragraph.alignment = align
|
||||
run = paragraph.add_run()
|
||||
run.text = text
|
||||
run.font.name = FONT
|
||||
run.font.size = Pt(size)
|
||||
run.font.bold = bold
|
||||
run.font.color.rgb = color
|
||||
return box
|
||||
|
||||
|
||||
def add_bullets(slide, x, y, w, h, items, size: int = 16, color=DARK, title: str | None = None):
|
||||
box = slide.shapes.add_textbox(Inches(x), Inches(y), Inches(w), Inches(h))
|
||||
frame = box.text_frame
|
||||
frame.word_wrap = True
|
||||
frame.margin_left = Inches(0.08)
|
||||
frame.margin_right = Inches(0.08)
|
||||
frame.margin_top = Inches(0.04)
|
||||
first = True
|
||||
if title:
|
||||
paragraph = frame.paragraphs[0]
|
||||
paragraph.space_after = Pt(7)
|
||||
run = paragraph.add_run()
|
||||
run.text = title
|
||||
run.font.name = FONT
|
||||
run.font.size = Pt(size + 3)
|
||||
run.font.bold = True
|
||||
run.font.color.rgb = GREEN_DARK
|
||||
first = False
|
||||
for item in items:
|
||||
if isinstance(item, tuple):
|
||||
text, level = item
|
||||
else:
|
||||
text, level = item, 0
|
||||
paragraph = frame.paragraphs[0] if first else frame.add_paragraph()
|
||||
first = False
|
||||
prefix = "• " if level == 0 else "– "
|
||||
paragraph.text = prefix + text
|
||||
paragraph.level = level
|
||||
paragraph.space_after = Pt(5)
|
||||
paragraph.line_spacing = 1.0
|
||||
for run in paragraph.runs:
|
||||
run.font.name = FONT
|
||||
run.font.size = Pt(size if level == 0 else size - 1)
|
||||
run.font.color.rgb = color
|
||||
return box
|
||||
|
||||
|
||||
def add_table(slide, x, y, w, h, rows, col_widths=None, font_size: int = 11, header: bool = True):
|
||||
shape = slide.shapes.add_table(len(rows), len(rows[0]), Inches(x), Inches(y), Inches(w), Inches(h))
|
||||
table = shape.table
|
||||
if col_widths:
|
||||
total = sum(col_widths)
|
||||
for index, value in enumerate(col_widths):
|
||||
table.columns[index].width = Inches(w * value / total)
|
||||
for row_index, row in enumerate(rows):
|
||||
for col_index, value in enumerate(row):
|
||||
cell = table.cell(row_index, col_index)
|
||||
cell.margin_left = Inches(0.05)
|
||||
cell.margin_right = Inches(0.05)
|
||||
cell.margin_top = Inches(0.02)
|
||||
cell.margin_bottom = Inches(0.02)
|
||||
cell.fill.solid()
|
||||
if header and row_index == 0:
|
||||
cell.fill.fore_color.rgb = GREEN
|
||||
else:
|
||||
cell.fill.fore_color.rgb = CREAM_LIGHT if row_index % 2 else CREAM
|
||||
paragraph = cell.text_frame.paragraphs[0]
|
||||
paragraph.alignment = PP_ALIGN.LEFT if col_index == 0 else PP_ALIGN.CENTER
|
||||
run = paragraph.add_run()
|
||||
run.text = str(value)
|
||||
run.font.name = FONT
|
||||
run.font.size = Pt(font_size)
|
||||
run.font.bold = header and row_index == 0
|
||||
run.font.color.rgb = WHITE if header and row_index == 0 else DARK
|
||||
return table
|
||||
|
||||
|
||||
def add_image_fit(slide, path: Path, x, y, max_w, max_h, panel: bool = False):
|
||||
with Image.open(path) as image:
|
||||
width, height = image.size
|
||||
aspect = width / height
|
||||
if max_w / max_h > aspect:
|
||||
draw_h = max_h
|
||||
draw_w = draw_h * aspect
|
||||
else:
|
||||
draw_w = max_w
|
||||
draw_h = draw_w / aspect
|
||||
draw_x = x + (max_w - draw_w) / 2
|
||||
draw_y = y + (max_h - draw_h) / 2
|
||||
if panel:
|
||||
add_panel(slide, draw_x - 0.07, draw_y - 0.07, draw_w + 0.14, draw_h + 0.14, fill=CREAM_LIGHT)
|
||||
slide.shapes.add_picture(str(path), Inches(draw_x), Inches(draw_y), width=Inches(draw_w), height=Inches(draw_h))
|
||||
|
||||
|
||||
def add_chicken(slide, x: float = 12.96, y: float = 7.12, size: float = 0.24):
|
||||
slide.shapes.add_picture(
|
||||
str(IMAGES / "chicken.png"), Inches(x), Inches(y), width=Inches(size), height=Inches(size)
|
||||
)
|
||||
|
||||
|
||||
def add_notes(slide, text: str) -> None:
|
||||
slide.notes_slide.notes_text_frame.text = text
|
||||
|
||||
|
||||
def new_slide():
|
||||
slide = Presentation().slides.add_slide # placeholder, replaced below
|
||||
return slide
|
||||
|
||||
|
||||
def add_title_bar(slide, title: str, block: str = "", subtitle: str | None = None):
|
||||
add_panel(slide, 0.42, 0.28, 12.49, 0.86, fill=CREAM)
|
||||
add_text(slide, 0.58, 0.34, 10.2, 0.45, title, size=25, bold=True, color=GREEN_DARK)
|
||||
if block:
|
||||
add_text(slide, 10.0, 0.42, 2.65, 0.35, block, size=13, color=BROWN, align=PP_ALIGN.RIGHT)
|
||||
if subtitle:
|
||||
add_text(slide, 0.58, 0.76, 11.8, 0.3, subtitle, size=12, color=BROWN)
|
||||
|
||||
|
||||
def add_footer(slide, block: str, number: int):
|
||||
add_text(slide, 0.45, 7.12, 7.8, 0.24, block, size=10, color=DARK)
|
||||
add_text(slide, 12.15, 7.12, 0.55, 0.24, str(number), size=10, color=DARK, align=PP_ALIGN.RIGHT)
|
||||
|
||||
|
||||
def content_slide(prs, title: str, block: str, number: int, subtitle: str | None = None):
|
||||
slide = prs.slides.add_slide(prs.slide_layouts[6])
|
||||
add_background(slide)
|
||||
add_title_bar(slide, title, block, subtitle)
|
||||
add_footer(slide, block, number)
|
||||
add_chicken(slide)
|
||||
return slide
|
||||
|
||||
|
||||
def section_slide(prs, number: int, block: str, title: str, subtitle: str, questions: list[str], notes: str, slide_number: int):
|
||||
slide = prs.slides.add_slide(prs.slide_layouts[6])
|
||||
add_background(slide)
|
||||
add_panel(slide, 0.65, 0.9, 12.0, 5.6, fill=CREAM)
|
||||
add_text(slide, 0.95, 1.15, 2.6, 1.0, block, size=22, bold=True, color=GOLD)
|
||||
add_text(slide, 0.95, 2.05, 11.2, 0.9, title, size=38, bold=True, color=GREEN_DARK)
|
||||
add_text(slide, 0.95, 3.05, 11.0, 0.6, subtitle, size=20, color=BROWN)
|
||||
add_bullets(slide, 0.95, 3.9, 11.0, 1.9, questions, size=17, color=DARK)
|
||||
add_text(slide, 0.95, 5.9, 7.0, 0.35, f"Рассказчик {number}", size=15, bold=True, color=RED)
|
||||
add_footer(slide, block, slide_number)
|
||||
add_chicken(slide)
|
||||
add_notes(slide, notes)
|
||||
return slide
|
||||
|
||||
|
||||
def main() -> int:
|
||||
prs = Presentation()
|
||||
prs.slide_width = Inches(SW)
|
||||
prs.slide_height = Inches(SH)
|
||||
slide_no = 0
|
||||
|
||||
# 1. Титульный слайд
|
||||
slide_no += 1
|
||||
slide = prs.slides.add_slide(prs.slide_layouts[6])
|
||||
add_background(slide)
|
||||
add_panel(slide, 0.55, 1.15, 6.15, 4.25, fill=CREAM)
|
||||
add_image_fit(slide, IMAGES / "title.jpg", 0.7, 1.3, 5.85, 3.9, panel=False)
|
||||
add_panel(slide, 6.95, 1.15, 5.85, 5.45, fill=CREAM)
|
||||
add_text(slide, 7.2, 1.45, 5.3, 0.35, "Лабораторная работа по анализу текстов", size=13, color=BROWN)
|
||||
add_text(slide, 7.2, 1.9, 5.3, 1.55, "Можно ли различить голоса персонажей?", size=32, bold=True, color=GREEN_DARK)
|
||||
add_text(slide, 7.2, 3.45, 5.25, 1.4, "Стилометрическая классификация 34 персонажей Stardew Valley по 2005 репликам", size=18, color=DARK)
|
||||
add_text(slide, 7.2, 4.9, 5.3, 0.6, "Три блока · три рассказчика", size=16, bold=True, color=RED)
|
||||
add_text(slide, 7.2, 5.28, 5.3, 0.45, "Корпус → методология → результаты", size=15, color=BROWN)
|
||||
add_panel(slide, 0.55, 5.75, 12.25, 0.8, fill=CREAM_LIGHT)
|
||||
add_text(slide, 0.75, 5.9, 11.9, 0.45, "2005 реплик · 34 персонажа · один автор · 5-блочная перекрёстная проверка", size=14, color=DARK, align=PP_ALIGN.CENTER)
|
||||
add_chicken(slide, 12.95, 7.12, 0.26)
|
||||
add_notes(slide, "Приветствие. Тема — стилометрия реплик Stardew Valley. Сразу уточнить: автор один, задача — различить речевые маски персонажей, а не установить авторство. Три блока: корпус, методология, результаты.")
|
||||
|
||||
# 2. Структура
|
||||
slide_no += 1
|
||||
slide = content_slide(prs, "Структура доклада", "Введение", slide_no)
|
||||
blocks = [
|
||||
("01", "Корпус", "Рассказчик 1", ["2005 реплик", "34 персонажа", "сбор, очистка, разметка", "ограничения данных"]),
|
||||
("02", "Методология", "Рассказчик 2", ["признаки и LSA", "классификаторы", "перекрёстная проверка", "метрики"]),
|
||||
("03", "Результаты", "Рассказчик 3", ["лучшие модели", "матрицы ошибок", "кого путает модель", "выводы"]),
|
||||
]
|
||||
for index, (num, title, narrator, lines) in enumerate(blocks):
|
||||
x = 0.45 + index * 4.27
|
||||
add_panel(slide, x, 1.5, 4.0, 5.15, fill=CREAM)
|
||||
add_text(slide, x + 0.25, 1.7, 0.9, 0.5, num, size=30, bold=True, color=GOLD)
|
||||
add_text(slide, x + 0.25, 2.3, 3.5, 0.5, title, size=24, bold=True, color=GREEN_DARK)
|
||||
add_text(slide, x + 0.25, 2.9, 3.5, 0.35, narrator, size=13, color=RED)
|
||||
add_bullets(slide, x + 0.25, 3.45, 3.5, 2.7, lines, size=14, color=DARK)
|
||||
add_notes(slide, "Показать три блока и передать слово первому рассказчику. Подчеркнуть: сначала данные, затем методы, затем интерпретация.")
|
||||
|
||||
# 3. Блок 1
|
||||
slide_no += 1
|
||||
section_slide(
|
||||
prs, 1, "Блок 1", "Корпус: как собрали и разметили",
|
||||
"Что именно мы классифицируем и почему это сложная задача",
|
||||
["Откуда данные и какова единица анализа", "Кто в корпусе и как распределены реплики", "Длина реплик и очистка", "Паспорт корпуса и ограничения"],
|
||||
"Передать слово рассказчику 1. Блок отвечает на вопрос: что представляет собой выборка и можно ли ей доверять.",
|
||||
slide_no,
|
||||
)
|
||||
|
||||
# 4. Что анализируем
|
||||
slide_no += 1
|
||||
slide = content_slide(prs, "Что анализируем", "Блок 1 · Корпус", slide_no)
|
||||
add_panel(slide, 0.45, 1.4, 5.95, 5.4, fill=CREAM)
|
||||
add_bullets(slide, 0.7, 1.6, 5.45, 4.9, [
|
||||
"Единица анализа — одна реплика персонажа.",
|
||||
"Всего 2005 реплик; 34 персонажа; один автор.",
|
||||
"Класс — имя персонажа; один объект относится ровно к одному классу.",
|
||||
"Реплики короткие: в среднем 19 слов, медиана 17.",
|
||||
"Корпус англоязычный; разметка — только текст и персонаж.",
|
||||
], size=17, title="Постановка задачи")
|
||||
add_panel(slide, 6.65, 1.4, 6.2, 5.4, fill=CREAM_LIGHT)
|
||||
add_text(slide, 6.85, 1.55, 5.8, 0.35, "Примеры разных голосов", size=16, bold=True, color=GREEN_DARK)
|
||||
quotes = [
|
||||
("Abigail (139 реплик)", "Oh, that's right... I heard someone new was moving onto that old farm."),
|
||||
("Jas (34 реплики)", "Wow, a birthday present! I love it!"),
|
||||
("Krobus (28 реплик)", "I wanted to give you this as a 'thank you' for letting me live here with you."),
|
||||
("Wizard (24 реплики)", "Ah, yes. I have predicted your arrival a long time ago, young PLAYER."),
|
||||
]
|
||||
y = 1.98
|
||||
for name, quote in quotes:
|
||||
add_text(slide, 6.85, y, 5.8, 0.3, name, size=13, bold=True, color=BROWN)
|
||||
add_text(slide, 6.85, y + 0.28, 5.75, 0.55, f"«{quote}»", size=13, color=DARK)
|
||||
y += 1.08
|
||||
add_notes(slide, "Объяснить: одна реплика — один объект. Показать четыре голоса: длинная речь Abigail, короткая детская реплика Jas, нечеловеческая вежливость Krobus, архаичный Wizard. Это иллюстрирует, что сигнал может быть в лексике, длине, пунктуации и синтаксисе.")
|
||||
|
||||
# 5. Кто в корпусе
|
||||
slide_no += 1
|
||||
slide = content_slide(prs, "Кто в корпусе", "Блок 1 · Корпус", slide_no)
|
||||
add_panel(slide, 0.4, 1.35, 8.8, 5.55, fill=CREAM_LIGHT)
|
||||
add_image_fit(slide, GEN / "characters.png", 0.5, 1.42, 8.6, 5.4)
|
||||
add_panel(slide, 9.3, 1.35, 3.6, 5.55, fill=CREAM)
|
||||
add_bullets(slide, 9.48, 1.55, 3.25, 5.1, [
|
||||
"34 персонажа: подростки, взрослые, пожилые, дети и нелюди.",
|
||||
"Все реплики написаны одним разработчиком; многие — в одних и тех же обновлениях и сюжетных контекстах.",
|
||||
"Классы сильно неравномерны: от 15 до 139 реплик.",
|
||||
"Метаданные о поле, возрасте и виде используются только для анализа ошибок, не как признаки.",
|
||||
], size=14, title="Состав")
|
||||
add_notes(slide, "Показать портреты. Подчеркнуть: один автор и общие сюжетные контексты — поэтому сходство голосов ожидаемо. Метаданные не подаются классификатору; они нужны только для интерпретации ошибок.")
|
||||
|
||||
# 6. Количество реплик
|
||||
slide_no += 1
|
||||
slide = content_slide(prs, "Сколько реплик у каждого", "Блок 1 · Корпус", slide_no, "Радиальная диаграмма: число реплик по персонажам")
|
||||
add_panel(slide, 0.4, 1.35, 7.2, 5.55, fill=CREAM_LIGHT)
|
||||
add_image_fit(slide, IMAGES / "replics-count.png", 0.5, 1.42, 7.0, 5.4)
|
||||
add_panel(slide, 7.85, 1.35, 5.05, 5.55, fill=CREAM)
|
||||
add_bullets(slide, 8.02, 1.55, 4.7, 5.0, [
|
||||
"Максимум — Abigail: 139 реплик; минимум — Mister Qi: 15.",
|
||||
"Разница между классами почти десятикратная.",
|
||||
"При 34 классах случайное угадывание даёт около 2,9%.",
|
||||
"Поэтому доля правильных ответов обманчива: модель может выбирать частые классы.",
|
||||
"Основная метрика — макро-усреднённая F-мера: каждый персонаж весит одинаково.",
|
||||
], size=16, title="Дисбаланс классов")
|
||||
add_notes(slide, "Диаграмма показывает дисбаланс. Сразу объяснить, почему accuracy не подходит: модель, всегда выбирающая Abigail, получит около 7% правильных ответов, но почти нулевую macro-F1 по остальным 33 классам.")
|
||||
|
||||
# 7. Длина реплик
|
||||
slide_no += 1
|
||||
slide = content_slide(prs, "Длина реплик", "Блок 1 · Корпус", slide_no, "Радиальная диаграмма: медианная длина предложения в символах")
|
||||
add_panel(slide, 0.4, 1.35, 7.2, 5.55, fill=CREAM_LIGHT)
|
||||
add_image_fit(slide, IMAGES / "median.png", 0.5, 1.42, 7.0, 5.4)
|
||||
add_panel(slide, 7.85, 1.35, 5.05, 5.55, fill=CREAM)
|
||||
add_bullets(slide, 8.02, 1.55, 4.7, 5.0, [
|
||||
"Средняя длина — 19,1 слова; медиана — 17 слов.",
|
||||
"Диапазон — от 3 до 78 слов.",
|
||||
"Медианная длина предложения в символах: от 52 у Vincent до 114 у Sandy.",
|
||||
"Короткие реплики дают мало свидетельств; стилометрические оценки на них нестабильны.",
|
||||
"Длина сама по себе — сильный, но нежелательный признак: она может стать прокси персонажа.",
|
||||
], size=15, title="Короткие тексты")
|
||||
add_notes(slide, "Числа у подписей — медианная длина предложения в символах. Показать контраст Vincent/Jas и Sandy/Wizard. Сказать, что длина — законный признак, но её вклад нужно проверять отдельно, иначе модель будет различать не стиль, а «кто говорит длиннее».")
|
||||
|
||||
# 8. Очистка и разметка
|
||||
slide_no += 1
|
||||
slide = content_slide(prs, "Очистка и разметка", "Блок 1 · Корпус", slide_no)
|
||||
add_panel(slide, 0.45, 1.35, 6.2, 5.55, fill=CREAM)
|
||||
add_bullets(slide, 0.65, 1.55, 5.85, 5.1, [
|
||||
"Источник — игровые реплики; CSV в cp1251, XLSX как резерв.",
|
||||
"Нормализация Unicode: многоточия, кавычки, тире, неразрывные пробелы.",
|
||||
"Плейсхолдеры <PLAYER>, <FARM> и другие заменены нейтральными словами; их количество сохранено отдельным признаком.",
|
||||
"Токенизация и разбиение на предложения — NLTK; частеречная разметка — Penn Treebank → Universal POS.",
|
||||
"Стоп-слова, пунктуация и регистр не удалялись: это стилометрический сигнал.",
|
||||
], size=15, title="Что сделали")
|
||||
add_panel(slide, 6.9, 1.35, 5.95, 5.55, fill=CREAM_LIGHT)
|
||||
add_bullets(slide, 7.1, 1.55, 5.55, 5.1, [
|
||||
"Шум: дубликаты, технические артефакты, общие короткие фразы.",
|
||||
"Ручная фильтрация уже удалила короткие и общие реплики; правила не сохранились.",
|
||||
"В текущем CSV 0 точных дублей; 13 пар с косинусной близостью ≥ 0,8 — все внутри одного персонажа.",
|
||||
"Нет идентификаторов сцен и диалогов; это ограничение анализа.",
|
||||
"Разметка — только имя персонажа; никакой ручной стилистической разметки нет.",
|
||||
], size=15, title="Что считаем шумом")
|
||||
add_notes(slide, "Объяснить различие между шумом и стилем. Удалять пунктуацию или стоп-слова нельзя: это носители стиля. Ручная фильтрация — ограничение: она могла удалить как раз короткие характерные реплики. Отметить, что почти-дубликаты не пересекают классы, поэтому утечки через них нет.")
|
||||
|
||||
# 9. Паспорт корпуса
|
||||
slide_no += 1
|
||||
slide = content_slide(prs, "Паспорт корпуса", "Блок 1 · Корпус", slide_no)
|
||||
add_panel(slide, 0.45, 1.35, 7.1, 5.55, fill=CREAM_LIGHT)
|
||||
rows = [
|
||||
["Показатель", "Значение"],
|
||||
["Реплик", "2005"],
|
||||
["Персонажей", "34"],
|
||||
["Словоупотреблений", "38 207"],
|
||||
["Средняя длина", "19,1 слова"],
|
||||
["Медиана длины", "17 слов"],
|
||||
["Минимум / максимум", "3 / 78 слов"],
|
||||
["Классы", "15–139 реплик"],
|
||||
["Точные дубли", "0"],
|
||||
["Почти-дубли (cos ≥ 0,8)", "13 пар, все внутри класса"],
|
||||
]
|
||||
add_table(slide, 0.7, 1.6, 6.6, 4.9, rows, col_widths=[3.4, 3.2], font_size=13)
|
||||
add_panel(slide, 7.85, 1.35, 5.05, 5.55, fill=CREAM)
|
||||
add_bullets(slide, 8.02, 1.6, 4.7, 5.0, [
|
||||
"Корпус пригоден для классификации, но не для сильных выводов о «чистом» стиле.",
|
||||
"Ограничения: короткие тексты, дисбаланс, один автор, отсутствие сцен.",
|
||||
"Все дальнейшие результаты нужно сравнивать с случайным уровнем 2,9%, а не с нулём.",
|
||||
], size=16, title="Что важно")
|
||||
add_notes(slide, "Этот слайд — «паспорт» данных. Подчеркнуть: 2005 объектов и 34 класса — небольшая выборка; 15 реплик у Mister Qi дают очень широкий интервал. Поэтому дальше используем повторную перекрёстную проверку и доверительные интервалы.")
|
||||
|
||||
# 10. Блок 2
|
||||
slide_no += 1
|
||||
section_slide(
|
||||
prs, 2, "Блок 2", "Методология: как измеряем стиль",
|
||||
"Признаки, модели и проверка — на понятном языке",
|
||||
["Какие свойства текста считаем", "Как работает LSA", "Как устроены классификаторы", "Как не обмануть себя при оценке"],
|
||||
"Передать слово рассказчику 2. Блок объясняет, что именно подаётся модели и почему выбранным методам можно доверять.",
|
||||
slide_no,
|
||||
)
|
||||
|
||||
# 11. Общая схема
|
||||
slide_no += 1
|
||||
slide = content_slide(prs, "Общая схема", "Блок 2 · Методология", slide_no)
|
||||
steps = [
|
||||
("Реплика", "текст персонажа"),
|
||||
("Очистка", "Unicode, плейсхолдеры, пробелы"),
|
||||
("Токены и POS", "NLTK, Universal POS"),
|
||||
("Признаки", "ручные, n-граммы, LSA"),
|
||||
("Модель", "вероятности по 34 классам"),
|
||||
("Проверка", "5 блоков, повторы, метрики"),
|
||||
]
|
||||
x = 0.55
|
||||
for index, (title, subtitle) in enumerate(steps):
|
||||
add_panel(slide, x, 2.0, 1.85, 2.35, fill=CREAM)
|
||||
add_text(slide, x + 0.12, 2.18, 1.6, 0.45, title, size=15, bold=True, color=GREEN_DARK, align=PP_ALIGN.CENTER)
|
||||
add_text(slide, x + 0.1, 2.7, 1.65, 1.1, subtitle, size=12, color=DARK, align=PP_ALIGN.CENTER)
|
||||
if index < len(steps) - 1:
|
||||
add_text(slide, x + 1.82, 2.7, 0.35, 0.4, "→", size=26, bold=True, color=RED, align=PP_ALIGN.CENTER)
|
||||
x += 2.12
|
||||
add_panel(slide, 0.55, 4.75, 12.15, 1.55, fill=CREAM_LIGHT)
|
||||
add_bullets(slide, 0.75, 4.9, 11.75, 1.2, [
|
||||
"Обучение и преобразования выполняются только внутри обучающих блоков; тест не участвует в настройке.",
|
||||
"Вектор признаков — это не «понимание» текста, а числовое описание его статистических свойств.",
|
||||
], size=15)
|
||||
add_notes(slide, "Провести по схеме слева направо. Главная мысль: модель не «читает» реплику как человек, а считает частоты и комбинации признаков. Отдельно подчеркнуть, что все преобразования обучаются внутри блоков, иначе возникает утечка.")
|
||||
|
||||
# 12. Признаки
|
||||
slide_no += 1
|
||||
slide = content_slide(prs, "Признаки: что именно считаем", "Блок 2 · Методология", slide_no)
|
||||
add_panel(slide, 0.45, 1.35, 6.1, 5.55, fill=CREAM)
|
||||
add_bullets(slide, 0.65, 1.55, 5.7, 5.1, [
|
||||
"454 ручных признака в 11 группах.",
|
||||
"Длина: слова, символы, предложения, длина слова.",
|
||||
"Лексика и орфография: распределение длин, апострофы, заглавные, цифры.",
|
||||
"Словарное богатство: TTR, Guiraud, Yule's K, hapax; на коротких текстах нестабильно.",
|
||||
"Пунктуация: запятые, многоточия, восклицания, вопросы, повторы.",
|
||||
"Разговорные маркеры: междометия, смех, хеджи, вежливость, сокращения.",
|
||||
], size=14, title="Ручные признаки")
|
||||
add_panel(slide, 6.75, 1.35, 6.1, 5.55, fill=CREAM_LIGHT)
|
||||
add_bullets(slide, 6.95, 1.55, 5.7, 5.1, [
|
||||
"Функциональные слова: около 150 закрытых слов и 12 категорий.",
|
||||
"Местоимения: 1-е, 2-е, 3-е лицо; отношение «я/ты».",
|
||||
"Части речи: Universal POS и Penn Treebank, время, модальность, герундий.",
|
||||
"Синтаксис-прокси: клаузы, подчинительные союзы, пассив, вопросы.",
|
||||
"Символьные n-граммы 2–5 и POS n-граммы 1–3.",
|
||||
"LSA: сжатые векторы по словам, содержанию, POS и символам.",
|
||||
], size=14, title="Синтаксис и векторные блоки")
|
||||
add_notes(slide, "Не перечислять все 454 признака. Объяснить логику: закрытые классы слов и пунктуация — классическая стилометрия; n-граммы — морфология и синтаксис; LSA — сжатое представление. Отметить, что словарное богатство и читаемость помечены как предварительные из-за коротких текстов.")
|
||||
|
||||
# 13. LSA
|
||||
slide_no += 1
|
||||
slide = content_slide(prs, "LSA простыми словами", "Блок 2 · Методология", slide_no)
|
||||
add_panel(slide, 0.45, 1.35, 6.0, 5.55, fill=CREAM)
|
||||
add_bullets(slide, 0.65, 1.55, 5.6, 5.1, [
|
||||
"Строим таблицу «документ × термин» и взвешиваем частоты через TF-IDF.",
|
||||
"Сжимаем таблицу сингулярным разложением до k скрытых осей.",
|
||||
"Каждая реплика превращается в вектор из k чисел.",
|
||||
"Оси не обязаны быть темами; их знак и порядок произвольны.",
|
||||
"k выбирается по объяснённой дисперсии и внешней метрике.",
|
||||
], size=16, title="Идея")
|
||||
add_panel(slide, 6.65, 1.35, 6.2, 5.55, fill=CREAM_LIGHT)
|
||||
add_bullets(slide, 6.85, 1.55, 5.8, 5.1, [
|
||||
"word — лексика и тема.",
|
||||
"content — содержание после удаления стоп-слов.",
|
||||
"fw — функциональные слова и синтаксис.",
|
||||
"char — морфология и пунктуация.",
|
||||
"pos — частеречные последовательности.",
|
||||
"В работе k = 50–400; для content-LSA качество росло до 400.",
|
||||
], size=15, title="Варианты")
|
||||
add_panel(slide, 0.65, 5.65, 11.9, 0.8, fill=CREAM_LIGHT)
|
||||
add_text(slide, 0.8, 5.78, 11.6, 0.5, "Ключевой вопрос: LSA по содержательным словам ловит тему, а не только стиль.", size=15, bold=True, color=RED, align=PP_ALIGN.CENTER)
|
||||
add_notes(slide, "Объяснить SVD без формул: было много столбцов, стало несколько скрытых осей; это сжатие, а не «понимание». Вариант content может кодировать тему, поэтому его нельзя автоматически называть стилем. В конце блока результатов покажем контроль содержания.")
|
||||
|
||||
# 14. Логистическая регрессия
|
||||
slide_no += 1
|
||||
slide = content_slide(prs, "Логистическая регрессия", "Блок 2 · Методология", slide_no)
|
||||
add_panel(slide, 0.45, 1.35, 6.0, 5.55, fill=CREAM)
|
||||
add_text(slide, 0.7, 1.55, 5.5, 0.35, "Как работает", size=18, bold=True, color=GREEN_DARK)
|
||||
add_bullets(slide, 0.7, 1.95, 5.5, 4.7, [
|
||||
"Для каждого класса считает линейную оценку: сумма «признак × вес» плюс сдвиг.",
|
||||
"Преобразует оценки в вероятности через softmax: они неотрицательны и суммируются в 1.",
|
||||
"Обучение минимизирует перекрёстную энтропию — штраф за низкую вероятность истинного класса.",
|
||||
"L2-регуляризация удерживает веса малыми; параметр C задаёт силу регуляризации.",
|
||||
"class_weight='balanced' повышает вес редких классов.",
|
||||
], size=15)
|
||||
add_panel(slide, 6.65, 1.35, 6.2, 5.55, fill=CREAM_LIGHT)
|
||||
add_bullets(slide, 6.85, 1.55, 5.8, 5.1, [
|
||||
"Плюсы: устойчива при большом числе признаков, даёт вероятности, коэффициенты интерпретируемы.",
|
||||
"Минусы: только линейные границы; при сильной корреляции признаков веса нестабильны.",
|
||||
"В работе — основная линейная базовая модель.",
|
||||
"Сравнение C: 0,03–10; балансировка классов критична.",
|
||||
], size=15, title="Что важно")
|
||||
add_notes(slide, "Объяснить на пальцах: модель складывает голоса признаков с весами; softmax превращает их в вероятности. C — обратная сила штрафа: меньше C — проще модель. Отметить, что без балансировки редкие персонажи почти не находятся; это было видно в эксперименте.")
|
||||
|
||||
# 15. SVM и деревья
|
||||
slide_no += 1
|
||||
slide = content_slide(prs, "SVM, деревья и ансамбли", "Блок 2 · Методология", slide_no)
|
||||
add_panel(slide, 0.45, 1.35, 6.05, 5.55, fill=CREAM)
|
||||
add_bullets(slide, 0.65, 1.55, 5.65, 5.1, [
|
||||
"Линейный SVM ищет границу с максимальным зазором; C штрафует ошибки.",
|
||||
"SVM не даёт вероятностей: только решающую функцию; тройка лучших считается по её значениям.",
|
||||
"Случайный лес: много деревьев, каждое на своей подвыборке; голосование.",
|
||||
"Сверхслучайные деревья (ExtraTrees): пороги в узлах выбираются случайно; меньше дисперсии, быстрее.",
|
||||
"Мягкое голосование усредняет вероятности нескольких моделей.",
|
||||
"Стековое объединение обучает мета-классификатор на предсказаниях базовых моделей.",
|
||||
], size=14, title="Идеи моделей")
|
||||
add_panel(slide, 6.7, 1.35, 6.15, 5.55, fill=CREAM_LIGHT)
|
||||
add_bullets(slide, 6.9, 1.55, 5.75, 5.1, [
|
||||
"Деревья ловят нелинейные взаимодействия: «если многоточий и короткая реплика — это Jas».",
|
||||
"Легко переобучаются на 2005 объектах; нужны ограничения глубины и листьев.",
|
||||
"Дельта Барроуза: расстояние до центроида по частотным словам — классическая проверка.",
|
||||
"Ансамбль полезен, если ошибки моделей не полностью совпадают.",
|
||||
"Стековое объединение в наших данных переобучилось и оказалось хуже голосования.",
|
||||
], size=14, title="Сильные и слабые стороны")
|
||||
add_notes(slide, "Не углубляться в оптимизацию. Главное: SVM — линейная граница с зазором; деревья — нелинейные правила; ExtraTrees — случайные пороги; голосование усредняет вероятности. Дельта Барроуза — независимая классическая проверка, а не ещё одна нейросеть.")
|
||||
|
||||
# 16. Проверка
|
||||
slide_no += 1
|
||||
slide = content_slide(prs, "Как проверяем", "Блок 2 · Методология", slide_no)
|
||||
add_panel(slide, 0.45, 1.35, 6.25, 5.55, fill=CREAM)
|
||||
add_bullets(slide, 0.65, 1.55, 5.85, 5.1, [
|
||||
"Стратифицированная перекрёстная проверка: 5 блоков, доли классов сохраняются.",
|
||||
"Повторяем 3–5 раз с разными разбиениями — оцениваем разброс.",
|
||||
"Вложенная проверка: внешние блоки — оценка, внутренние — настройка.",
|
||||
"Отложенная выборка 80/20 — независимая проверка финальных кандидатов.",
|
||||
], size=16, title="Схема")
|
||||
add_panel(slide, 6.9, 1.35, 5.95, 5.55, fill=CREAM_LIGHT)
|
||||
add_bullets(slide, 7.1, 1.55, 5.55, 5.1, [
|
||||
"TF-IDF, SVD, масштабирование и отбор признаков обучаются только на обучающей выборке.",
|
||||
"Тестовая выборка не участвует в выборе гиперпараметров или признаков.",
|
||||
"Проверка перестановкой меток дала 0,027–0,028 — случайный уровень.",
|
||||
"Почти-дубликаты внутри одного класса не пересекают обучение и тест.",
|
||||
"Метрики: макро-F1, сбалансированная доля, тройка лучших, MCC, каппа, матрица ошибок.",
|
||||
], size=15, title="Контроль утечек")
|
||||
add_notes(slide, "Объяснить, почему нельзя настраивать на тесте. Привести аналогию: если подсматривать ответы, оценка будет завышена. Перестановка меток — контроль: если модель на случайных метках даёт 0,027, пайплайн не течёт.")
|
||||
|
||||
# 17. Метрики
|
||||
slide_no += 1
|
||||
slide = content_slide(prs, "Метрики: что и зачем", "Блок 2 · Методология", slide_no)
|
||||
add_panel(slide, 0.45, 1.35, 6.15, 5.55, fill=CREAM_LIGHT)
|
||||
rows = [
|
||||
["Метрика", "Что показывает"],
|
||||
["Макро-F1", "Средняя F-мера по 34 классам; все персонажи равны"],
|
||||
["Доля правильных", "Доля совпавших ответов; обманчива при дисбалансе"],
|
||||
["Сбалансированная доля", "Средняя полнота по классам"],
|
||||
["Тройка лучших", "Истинный класс в тройке лучших"],
|
||||
["MCC / каппа", "Согласие с поправкой на случай"],
|
||||
["Матрица ошибок", "Кто с кем путается"],
|
||||
]
|
||||
add_table(slide, 0.65, 1.55, 5.75, 4.9, rows, col_widths=[2.2, 3.6], font_size=12)
|
||||
add_panel(slide, 6.8, 1.35, 6.05, 5.55, fill=CREAM)
|
||||
add_bullets(slide, 7.0, 1.55, 5.65, 5.1, [
|
||||
"Случайный уровень при 34 классах: macro-F1 ≈ 0,029, top-3 ≈ 0,088.",
|
||||
"Модель, всегда выбирающая Abigail: доля правильных ответов ≈ 6,9%, macro-F1 ≈ 0,004.",
|
||||
"Поэтому основной вывод делаем по macro-F1 и доверительным интервалам.",
|
||||
"Дополнительно смотрим F-меру по каждому персонажу и матрицу ошибок.",
|
||||
], size=16, title="Почему не доля правильных ответов")
|
||||
add_notes(slide, "Главный методический слайд: объяснить, что accuracy при 34 классах может вводить в заблуждение. Пример Dummy: 6,9% accuracy, но почти нулевая macro-F1. Поэтому все выводы — по macro-F1, а per-class F1 показывает, кого именно модель не находит.")
|
||||
|
||||
# 18. Блок 3
|
||||
slide_no += 1
|
||||
section_slide(
|
||||
prs, 3, "Блок 3", "Результаты и интерпретация",
|
||||
"Что получилось, кого модель путает и что это значит",
|
||||
["Лучшие модели и масштаб качества", "Стиль или содержание", "Матрицы ошибок и пары персонажей", "Ограничения и выводы"],
|
||||
"Передать слово рассказчику 3. Здесь важно не только назвать число, но и объяснить, что оно означает и чего оно не означает.",
|
||||
slide_no,
|
||||
)
|
||||
|
||||
# 19. Главный результат
|
||||
slide_no += 1
|
||||
slide = content_slide(prs, "Главный результат", "Блок 3 · Результаты", slide_no)
|
||||
add_panel(slide, 0.4, 1.35, 9.0, 5.55, fill=CREAM_LIGHT)
|
||||
add_image_fit(slide, GEN / "model_comparison.png", 0.5, 1.45, 8.8, 5.35)
|
||||
add_panel(slide, 9.55, 1.35, 3.35, 5.55, fill=CREAM)
|
||||
add_bullets(slide, 9.72, 1.55, 3.05, 5.1, [
|
||||
"Мягкое голосование: 0,304.",
|
||||
"ExtraTrees, char=10000: 0,276.",
|
||||
"Вложенный выбор: 0,275.",
|
||||
"Случайный уровень: 0,029.",
|
||||
"На отложенной выборке: 0,333.",
|
||||
], size=15, title="Макро-F1")
|
||||
add_notes(slide, "Назвать числа и сразу масштаб: 0,30 при 34 классах — это примерно в 10 раз выше случайного. Не говорить «низкая точность» без сравнения с 0,029. Оговорить, что 0,333 на отложенной выборке — одно разбиение и широкий интервал.")
|
||||
|
||||
# 20. Что добавляет качество
|
||||
slide_no += 1
|
||||
slide = content_slide(prs, "Что добавляет качество", "Блок 3 · Результаты", slide_no)
|
||||
add_panel(slide, 0.4, 1.35, 8.95, 5.55, fill=CREAM_LIGHT)
|
||||
add_image_fit(slide, GEN / "feature_progression.png", 0.5, 1.45, 8.75, 5.35)
|
||||
add_panel(slide, 9.5, 1.35, 3.4, 5.55, fill=CREAM)
|
||||
add_bullets(slide, 9.67, 1.55, 3.1, 5.1, [
|
||||
"Ручные признаки: 0,105.",
|
||||
"+ char/POS n-граммы: 0,182.",
|
||||
"+ content-LSA: 0,249.",
|
||||
"ExtraTrees + char=10000: 0,276.",
|
||||
"Мягкое голосование: 0,304.",
|
||||
"Основной прирост — лексика и n-граммы, не «чистый синтаксис».",
|
||||
], size=14, title="По шагам")
|
||||
add_notes(slide, "Показать, что качество растёт не от «магии модели», а от добавления лексических и символьных признаков. Отметить: функциональные слова и POS- LSA сами по себе дают только 0,06–0,07.")
|
||||
|
||||
# 21. Стиль или содержание
|
||||
slide_no += 1
|
||||
slide = content_slide(prs, "Стиль или содержание?", "Блок 3 · Результаты", slide_no)
|
||||
add_panel(slide, 0.4, 1.35, 8.4, 5.55, fill=CREAM_LIGHT)
|
||||
add_image_fit(slide, GEN / "topic_control.png", 0.5, 1.45, 8.2, 5.35)
|
||||
add_panel(slide, 8.95, 1.35, 3.95, 5.55, fill=CREAM)
|
||||
add_bullets(slide, 9.12, 1.55, 3.65, 5.1, [
|
||||
"Исходный текст: 0,248.",
|
||||
"После замены содержательных слов: 0,113.",
|
||||
"Падение более чем вдвое.",
|
||||
"Остаток 0,113 всё ещё в 3,9 раза выше случайного.",
|
||||
"Значит, стиль есть, но содержание доминирует.",
|
||||
], size=15, title="Контроль")
|
||||
add_notes(slide, "Это ключевой слайд для честной интерпретации. Содержательные слова заменили на CONTENT, оставив служебные слова, POS и пунктуацию. Качество упало с 0,248 до 0,113. Значит, нельзя утверждать, что модель выучила «чистый идиолект»: она во многом различает, о чём персонаж говорит.")
|
||||
|
||||
# 22. Как читать матрицу
|
||||
slide_no += 1
|
||||
slide = content_slide(prs, "Как читать матрицу ошибок", "Блок 3 · Результаты", slide_no)
|
||||
add_panel(slide, 0.45, 1.35, 6.2, 5.55, fill=CREAM_LIGHT)
|
||||
add_text(slide, 0.7, 1.55, 5.7, 0.4, "Маленький пример", size=17, bold=True, color=GREEN_DARK)
|
||||
mini = [
|
||||
["истина \\ прогноз", "A", "B", "C"],
|
||||
["A", "70", "20", "10"],
|
||||
["B", "15", "75", "10"],
|
||||
["C", "5", "25", "70"],
|
||||
]
|
||||
add_table(slide, 0.7, 2.05, 5.6, 2.5, mini, col_widths=[2.2, 1.1, 1.1, 1.1], font_size=14)
|
||||
add_text(slide, 0.7, 4.7, 5.7, 1.8, "Строка — истинный персонаж. Столбец — предсказание модели. Диагональ — правильные ответы. Вне диагонали — ошибки. Числа — проценты от строки.", size=14, color=DARK)
|
||||
add_panel(slide, 6.85, 1.35, 6.0, 5.55, fill=CREAM)
|
||||
add_bullets(slide, 7.05, 1.55, 5.6, 5.1, [
|
||||
"Полная матрица 34×34 на доске нечитаема.",
|
||||
"Поэтому на слайде — 12 самых частых персонажей и столбец «прочие».",
|
||||
"Отдельно показываем 18 самых частых пар ошибок.",
|
||||
"Полная матрица 34×34 — в раздатке/приложении.",
|
||||
"На доске смотрим диагональ, крупные внедиагональные клетки и «прочие».",
|
||||
], size=15, title="Как показываем")
|
||||
add_notes(slide, "Объяснить матрицу на маленьком примере. Сказать: полная матрица 34×34 на доске превращается в кашу; поэтому используем три уровня: top-12 матрица, top-18 пар, полная матрица в раздатке. Это методически честно и читаемо.")
|
||||
|
||||
# 23. Матрица top12
|
||||
slide_no += 1
|
||||
slide = content_slide(prs, "Матрица ошибок: 12 персонажей", "Блок 3 · Результаты", slide_no)
|
||||
add_panel(slide, 0.35, 1.35, 9.05, 5.55, fill=CREAM_LIGHT)
|
||||
add_image_fit(slide, GEN / "confusion_top12.png", 0.45, 1.45, 8.85, 5.35)
|
||||
add_panel(slide, 9.6, 1.35, 3.3, 5.55, fill=CREAM)
|
||||
add_bullets(slide, 9.77, 1.55, 3.0, 5.1, [
|
||||
"Диагональ: доля правильных ответов внутри класса.",
|
||||
"Abigail: 21% верно, 18% уходит в «прочие».",
|
||||
"Alex: 37% верно, 11% → Abigail.",
|
||||
"Sebastian: 19% верно, 20% → Abigail.",
|
||||
"У многих строк крупная доля «прочих»: модель не находит редкие классы.",
|
||||
], size=13, title="Что видно")
|
||||
add_notes(slide, "Провести по строкам: Abigail, Alex, Sebastian. Обратить внимание на столбец «прочие»: он большой, потому что 22 класса не поместились. Это не «мусор», а важная часть ошибок. Сказать, что диагональ далеко не 100%, но и не 0.")
|
||||
|
||||
# 24. Кого путает
|
||||
slide_no += 1
|
||||
slide = content_slide(prs, "Кого модель путает", "Блок 3 · Результаты", slide_no)
|
||||
add_panel(slide, 0.35, 1.35, 9.0, 5.55, fill=CREAM_LIGHT)
|
||||
add_image_fit(slide, GEN / "confusion_pairs.png", 0.45, 1.45, 8.8, 5.35)
|
||||
add_panel(slide, 9.55, 1.35, 3.35, 5.55, fill=CREAM)
|
||||
add_bullets(slide, 9.72, 1.55, 3.05, 5.1, [
|
||||
"Sebastian → Abigail: 65.",
|
||||
"Sebastian → Alex: 61.",
|
||||
"Abigail → Sebastian: 59.",
|
||||
"Abigail → Alex: 56.",
|
||||
"Основной клубок — подростки/молодые взрослые: Abigail, Alex, Sebastian, Haley, Sam.",
|
||||
], size=13, title="Частые пары")
|
||||
add_notes(slide, "Показать, что ошибки не случайны: они концентрируются в одном круге персонажей. Это ожидаемо: один автор, общие темы, сходная эмоциональная окраска, короткие реплики. Это не доказательство «автор не старался», а наоборот — свидетельство близких речевых масок внутри группы.")
|
||||
|
||||
# 25. Классы-аттракторы
|
||||
slide_no += 1
|
||||
slide = content_slide(prs, "Классы-аттракторы", "Блок 3 · Результаты", slide_no)
|
||||
add_panel(slide, 0.4, 1.35, 9.0, 5.55, fill=CREAM_LIGHT)
|
||||
add_image_fit(slide, GEN / "hub_shares.png", 0.5, 1.45, 8.8, 5.35)
|
||||
add_panel(slide, 9.55, 1.35, 3.35, 5.55, fill=CREAM)
|
||||
add_bullets(slide, 9.72, 1.55, 3.05, 5.1, [
|
||||
"Alex: 11,4% предсказаний при истинной доле 6,0%.",
|
||||
"Abigail: 10,9% при 6,9%.",
|
||||
"Редкие классы недопредставлены.",
|
||||
"Это не «демографическая» путаница, а стягивание к популярным голосам.",
|
||||
"Коррекция приоров не улучшила macro-F1.",
|
||||
], size=13, title="Кто перетягивает")
|
||||
add_notes(slide, "Объяснить «класс-аттрактор»: модель чаще выбирает Alex и Abigail, потому что их признаки часто встречаются и они похожи на многих. Редкие персонажи проигрывают. Простая коррекция приоров не помогла; нужны более тонкие правила или больше данных.")
|
||||
|
||||
# 26. Per-class F1
|
||||
slide_no += 1
|
||||
slide = content_slide(prs, "Персонажи: кто различается", "Блок 3 · Результаты", slide_no)
|
||||
add_panel(slide, 0.3, 1.35, 9.25, 5.55, fill=CREAM_LIGHT)
|
||||
add_image_fit(slide, GEN / "per_class_f1.png", 0.4, 1.45, 9.05, 5.35)
|
||||
add_panel(slide, 9.75, 1.35, 3.15, 5.55, fill=CREAM)
|
||||
add_bullets(slide, 9.9, 1.55, 2.85, 5.1, [
|
||||
"Лучшие: Willy, Evelyn, Pierre, Leo, Pam.",
|
||||
"Худшие: Kent, Mister Qi, Penny, Abigail, Leah.",
|
||||
"Редкие классы — широкие интервалы.",
|
||||
"Мягкое голосование улучшает Wizard, Dwarf, Vincent.",
|
||||
], size=13, title="Факты")
|
||||
add_notes(slide, "Показать, что качество по персонажам неоднородно. Evelyn/Willy/Pierre различаются лучше; Kent/Mister Qi/Penny — хуже. Это связано и с числом реплик, и с их речевой функцией. Не делать вывод, что персонаж «плохой»: у редких классов просто мало данных.")
|
||||
|
||||
# 27. Почему путаются
|
||||
slide_no += 1
|
||||
slide = content_slide(prs, "Почему персонажи путаются", "Блок 3 · Результаты", slide_no)
|
||||
add_panel(slide, 0.45, 1.35, 6.15, 5.55, fill=CREAM)
|
||||
add_bullets(slide, 0.65, 1.55, 5.75, 5.1, [
|
||||
"Один автор и общие сюжетные контексты: персонажи говорят об одних и тех же событиях.",
|
||||
"Сходные возраст и роль: подростки/молодые взрослые используют похожие темы и интонации.",
|
||||
"Короткие реплики: мало слов, чтобы проявить индивидуальный синтаксис.",
|
||||
"Дисбаланс: редкие классы реже выбираются моделью.",
|
||||
"Общие формулы вежливости, шутки, междометия.",
|
||||
], size=15, title="Причины")
|
||||
add_panel(slide, 6.8, 1.35, 6.05, 5.55, fill=CREAM_LIGHT)
|
||||
add_bullets(slide, 7.0, 1.55, 5.65, 5.1, [
|
||||
"Клубок: Abigail, Alex, Sebastian, Haley, Sam.",
|
||||
"Нелюди (Krobus, Dwarf, Wizard) различаются лучше, но Wizard и Dwarf редки.",
|
||||
"Дети (Jas, Vincent) имеют короткие реплики; их легче узнать по длине.",
|
||||
"Это не доказательство сходства «характеров», а свойство текстовых признаков.",
|
||||
"Нужны сцены и больше данных, чтобы отделить стиль от контекста.",
|
||||
], size=15, title="Что это значит")
|
||||
add_notes(slide, "Ответить на вопрос «почему одни и те же персонажи путаются»: один автор, общие темы, сходный возраст/роль, короткие реплики, дисбаланс. Подчеркнуть: мы не делаем психологических выводов о персонажах; мы объясняем статистику ошибок.")
|
||||
|
||||
# 28. Что означают оценки
|
||||
slide_no += 1
|
||||
slide = content_slide(prs, "Что означают оценки", "Блок 3 · Результаты", slide_no)
|
||||
add_panel(slide, 0.45, 1.35, 12.4, 5.55, fill=CREAM)
|
||||
add_bullets(slide, 0.7, 1.55, 11.9, 5.1, [
|
||||
"0,30 макро-F1 при 34 классах — это примерно в 10 раз выше случайного уровня 0,029.",
|
||||
"Абсолютное значение умеренное: по одной короткой реплике персонажа действительно трудно определить.",
|
||||
"Это не означает, что автор «не старался» или что стиль неразличим. Наоборот, устойчивое превышение случайного уровня говорит о намеренно различных голосах.",
|
||||
"Умеренность объясняется короткими текстами, дисбалансом классов, общими темами и формулами, а не отсутствием стиля.",
|
||||
"Мы не решаем задачу «он или не он писал»: все реплики одного автора. Мы решаем, какой персонаж говорит.",
|
||||
], size=17, title="Уточнение интерпретации")
|
||||
add_notes(slide, "Здесь важно поправить исходную формулировку. Нельзя говорить «низкие оценки хорошие, потому что автор не старался». Корректно: оценки выше случайного уровня, но умеренные; это свидетельствует о частичной различимости голосов. Один автор — значит, различия созданы намеренно, а не разными авторами. Низкие абсолютные значения — следствие трудности задачи, а не приговор стилю.")
|
||||
|
||||
# 29. Ограничения
|
||||
slide_no += 1
|
||||
slide = content_slide(prs, "Ограничения", "Блок 3 · Результаты", slide_no)
|
||||
add_panel(slide, 0.45, 1.35, 6.15, 5.55, fill=CREAM)
|
||||
add_bullets(slide, 0.65, 1.55, 5.75, 5.1, [
|
||||
"Один корпус, одна игра, один автор.",
|
||||
"Нет внешней валидации на другом корпусе.",
|
||||
"Короткие реплики и дисбаланс классов.",
|
||||
"Ручная фильтрация корпуса не воспроизводится.",
|
||||
"Нет идентификаторов сцен и диалогов.",
|
||||
], size=16, title="Данные")
|
||||
add_panel(slide, 6.8, 1.35, 6.05, 5.55, fill=CREAM_LIGHT)
|
||||
add_bullets(slide, 7.0, 1.55, 5.65, 5.1, [
|
||||
"Выбор признаков и ансамбля опирался на предыдущие результаты.",
|
||||
"Вложенная проверка снижает, но не устраняет оптимизм.",
|
||||
"Содержательные слова и тема вносят большой вклад.",
|
||||
"Простая калибровка приоров не помогла.",
|
||||
"Стековое объединение переобучается на малом числе объектов.",
|
||||
], size=16, title="Методология")
|
||||
add_notes(slide, "Перечислить ограничения честно. Главное: нельзя переносить выводы на другие игры/авторов без внешней проверки. И нельзя называть результат «чистым стилем», пока не отделена тема.")
|
||||
|
||||
# 30. Выводы
|
||||
slide_no += 1
|
||||
slide = content_slide(prs, "Выводы", "Блок 3 · Результаты", slide_no)
|
||||
add_panel(slide, 0.45, 1.35, 12.4, 5.55, fill=CREAM)
|
||||
add_bullets(slide, 0.75, 1.55, 11.8, 5.1, [
|
||||
"Персонажи статистически различимы: лучшая модель — 0,304 макро-F1, консервативная оценка — 0,275 при случайном уровне 0,029.",
|
||||
"Основной вклад дают лексика, символьные n-граммы и LSA по содержательным словам; функциональные слова и синтаксис сами по себе слабы.",
|
||||
"После замены содержательных слов качество падает с 0,248 до 0,113: содержание доминирует, но стилевой сигнал сохраняется.",
|
||||
"Главная проблема — редкие классы и классы-аттракторы; мягкое голосование помогает, но не решает её полностью.",
|
||||
"Корректный вывод: различимы речевые маски по лексико-тематическому профилю; для утверждения о «чистом стиле» нужны внешняя валидация и тематический контроль.",
|
||||
], size=17, title="Пять тезисов")
|
||||
add_notes(slide, "Сформулировать выводы без переубеждения аудитории. Главное: не «мы всё доказали», а «мы получили устойчивый выше случайного уровня сигнал; основной вклад — лексика/тема; для стиля нужны дальнейшие проверки».")
|
||||
|
||||
# 31. Приложение: полная матрица
|
||||
slide_no += 1
|
||||
slide = content_slide(prs, "Приложение: полная матрица 34×34", "Приложение", slide_no)
|
||||
add_panel(slide, 0.35, 1.35, 7.4, 5.55, fill=CREAM_LIGHT)
|
||||
add_image_fit(slide, GEN / "confusion_full34.png", 0.45, 1.45, 7.2, 5.35)
|
||||
add_panel(slide, 7.95, 1.35, 4.95, 5.55, fill=CREAM)
|
||||
add_bullets(slide, 8.12, 1.55, 4.6, 5.1, [
|
||||
"Полная матрица 34×34 — для раздаточного материала.",
|
||||
"На проекторе она слишком плотная; в докладе используем top-12 и top-18 пар.",
|
||||
"Строки — истинные персонажи; столбцы — предсказания; цвет — доля строки.",
|
||||
"Диагональ — правильные ответы; тёмные внедиагональные клетки — систематические ошибки.",
|
||||
], size=15, title="Как использовать")
|
||||
add_notes(slide, "Этот слайд можно не показывать, а раздать. Если показывать, не читать все клетки: показать диагональ и несколько тёмных пятен.")
|
||||
|
||||
# 32. Финал: мем
|
||||
slide_no += 1
|
||||
slide = prs.slides.add_slide(prs.slide_layouts[6])
|
||||
add_background(slide)
|
||||
add_panel(slide, 0.55, 1.2, 6.2, 5.45, fill=CREAM)
|
||||
add_text(slide, 0.85, 1.5, 5.6, 1.0, "Спасибо за внимание!", size=34, bold=True, color=GREEN_DARK)
|
||||
add_text(slide, 0.85, 2.55, 5.5, 1.5, "Вопросы, обсуждение и предложения по экспериментам", size=18, color=DARK)
|
||||
add_text(slide, 0.85, 4.2, 5.5, 1.2, "«Один автор — разные голоса»", size=20, bold=True, color=RED)
|
||||
add_panel(slide, 7.0, 0.9, 5.8, 6.0, fill=CREAM_LIGHT)
|
||||
add_image_fit(slide, GEN / "meme.png", 7.15, 1.05, 5.5, 5.7)
|
||||
add_chicken(slide, 0.25, 7.05, 0.28)
|
||||
add_notes(slide, "Финальный слайд с мемом. Поблагодарить, перейти к вопросам. Курица спрятана слева внизу — пасхалка.")
|
||||
|
||||
prs.save(OUT)
|
||||
print(f"Готово: {OUT}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
BIN
lab1/presentation/generated/characters.png
Normal file
|
After Width: | Height: | Size: 2.4 MiB |
BIN
lab1/presentation/generated/confusion_full34.png
Normal file
|
After Width: | Height: | Size: 178 KiB |
BIN
lab1/presentation/generated/confusion_pairs.png
Normal file
|
After Width: | Height: | Size: 170 KiB |
BIN
lab1/presentation/generated/confusion_top12.png
Normal file
|
After Width: | Height: | Size: 238 KiB |
BIN
lab1/presentation/generated/feature_progression.png
Normal file
|
After Width: | Height: | Size: 141 KiB |
BIN
lab1/presentation/generated/hub_shares.png
Normal file
|
After Width: | Height: | Size: 96 KiB |
BIN
lab1/presentation/generated/meme.png
Normal file
|
After Width: | Height: | Size: 678 KiB |
BIN
lab1/presentation/generated/model_comparison.png
Normal file
|
After Width: | Height: | Size: 132 KiB |
BIN
lab1/presentation/generated/per_class_f1.png
Normal file
|
After Width: | Height: | Size: 240 KiB |
BIN
lab1/presentation/generated/topic_control.png
Normal file
|
After Width: | Height: | Size: 104 KiB |
BIN
lab1/presentation/images/background.jpg
Normal file
|
After Width: | Height: | Size: 238 KiB |
BIN
lab1/presentation/images/characters.webp
Normal file
|
After Width: | Height: | Size: 314 KiB |
BIN
lab1/presentation/images/chicken.png
Normal file
|
After Width: | Height: | Size: 411 B |
BIN
lab1/presentation/images/median.png
Normal file
|
After Width: | Height: | Size: 680 KiB |
BIN
lab1/presentation/images/meme.webp
Normal file
|
After Width: | Height: | Size: 66 KiB |
BIN
lab1/presentation/images/replics-count.png
Normal file
|
After Width: | Height: | Size: 795 KiB |
BIN
lab1/presentation/images/title.jpg
Normal file
|
After Width: | Height: | Size: 66 KiB |
263
lab1/presentation/make_slide_figures.py
Normal file
@@ -0,0 +1,263 @@
|
||||
"""Графики для презентации: матрицы ошибок, сравнение моделей, ошибки по классам."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import matplotlib
|
||||
matplotlib.use("Agg")
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from matplotlib.colors import LinearSegmentedColormap
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
OUT = ROOT / "presentation" / "generated"
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
OOF = ROOT / "results" / "final" / "finalists" / "voting_et_char_lean" / "oof_predictions.csv"
|
||||
METRICS = ROOT / "FINAL_RESULTS.csv"
|
||||
|
||||
plt.rcParams.update({
|
||||
"font.family": "DejaVu Sans",
|
||||
"font.size": 13,
|
||||
"axes.titlesize": 18,
|
||||
"axes.labelsize": 13,
|
||||
"savefig.facecolor": "#F8F1DC",
|
||||
"figure.facecolor": "#F8F1DC",
|
||||
})
|
||||
|
||||
CREAM = "#F8F1DC"
|
||||
DARK = "#203A2B"
|
||||
GREEN = "#4D7C4A"
|
||||
BROWN = "#8B5A2B"
|
||||
RED = "#B23A2E"
|
||||
GOLD = "#D8A23A"
|
||||
|
||||
CMAP = LinearSegmentedColormap.from_list("stardew_blue", ["#FFFDF4", "#BFD9C0", "#2E6B57", "#173A34"])
|
||||
|
||||
|
||||
def load_oof() -> pd.DataFrame:
|
||||
frame = pd.read_csv(OOF)
|
||||
frame["y_true"] = frame["y_true"].astype(str)
|
||||
frame["y_pred"] = frame["y_pred"].astype(str)
|
||||
return frame
|
||||
|
||||
|
||||
def top_confusions(frame: pd.DataFrame, n: int = 18) -> pd.Series:
|
||||
errors = frame[frame["y_true"] != frame["y_pred"]]
|
||||
pairs = errors.groupby(["y_true", "y_pred"]).size().sort_values(ascending=False).head(n)
|
||||
pairs.index = [f"{a} → {b}" for a, b in pairs.index]
|
||||
return pairs
|
||||
|
||||
|
||||
def confusion_top12(frame: pd.DataFrame) -> None:
|
||||
labels = frame["y_true"].value_counts().head(12).index.tolist()
|
||||
matrix = pd.crosstab(frame["y_true"], frame["y_pred"]).reindex(index=labels, fill_value=0)
|
||||
shown = matrix[[c for c in labels if c in matrix.columns]].copy()
|
||||
shown["прочие"] = matrix.drop(columns=[c for c in labels if c in matrix.columns]).sum(axis=1)
|
||||
shown = shown.div(shown.sum(axis=1), axis=0) * 100
|
||||
|
||||
fig, ax = plt.subplots(figsize=(12.5, 7.4))
|
||||
image = ax.imshow(shown.to_numpy(), cmap=CMAP, vmin=0, vmax=60, aspect="auto")
|
||||
ax.set_xticks(np.arange(shown.shape[1]))
|
||||
ax.set_yticks(np.arange(shown.shape[0]))
|
||||
ax.set_xticklabels(shown.columns, rotation=42, ha="right", fontsize=10)
|
||||
ax.set_yticklabels(shown.index, fontsize=11)
|
||||
ax.set_xlabel("Предсказанный персонаж", fontsize=14, color=DARK)
|
||||
ax.set_ylabel("Истинный персонаж", fontsize=14, color=DARK)
|
||||
ax.set_title("Матрица ошибок: 12 самых частых персонажей, % строки", fontsize=18, color=DARK, pad=12)
|
||||
for row in range(shown.shape[0]):
|
||||
for col in range(shown.shape[1]):
|
||||
value = shown.iloc[row, col]
|
||||
if value >= 0.5:
|
||||
color = "white" if value > 32 else DARK
|
||||
ax.text(col, row, f"{value:.0f}", ha="center", va="center", fontsize=9, color=color)
|
||||
cbar = fig.colorbar(image, ax=ax, fraction=0.035, pad=0.02)
|
||||
cbar.set_label("% строки", color=DARK)
|
||||
fig.tight_layout()
|
||||
fig.savefig(OUT / "confusion_top12.png", dpi=220, bbox_inches="tight")
|
||||
plt.close(fig)
|
||||
|
||||
|
||||
def confusion_full(frame: pd.DataFrame) -> None:
|
||||
labels = sorted(frame["y_true"].unique())
|
||||
matrix = pd.crosstab(frame["y_true"], frame["y_pred"]).reindex(index=labels, columns=labels, fill_value=0)
|
||||
matrix = matrix.div(matrix.sum(axis=1), axis=0) * 100
|
||||
fig, ax = plt.subplots(figsize=(13.5, 11.5))
|
||||
image = ax.imshow(matrix.to_numpy(), cmap=CMAP, vmin=0, vmax=35, aspect="auto")
|
||||
ax.set_xticks(np.arange(len(labels)))
|
||||
ax.set_yticks(np.arange(len(labels)))
|
||||
ax.set_xticklabels(labels, rotation=90, fontsize=5.5)
|
||||
ax.set_yticklabels(labels, fontsize=5.5)
|
||||
ax.set_xlabel("Предсказанный персонаж", fontsize=12, color=DARK)
|
||||
ax.set_ylabel("Истинный персонаж", fontsize=12, color=DARK)
|
||||
ax.set_title("Полная матрица ошибок, % строки (раздаточный материал)", fontsize=16, color=DARK)
|
||||
fig.colorbar(image, ax=ax, fraction=0.03, pad=0.015).set_label("% строки")
|
||||
fig.tight_layout()
|
||||
fig.savefig(OUT / "confusion_full34.png", dpi=220, bbox_inches="tight")
|
||||
plt.close(fig)
|
||||
|
||||
|
||||
def pairs_chart(frame: pd.DataFrame) -> None:
|
||||
pairs = top_confusions(frame, 18).sort_values()
|
||||
fig, ax = plt.subplots(figsize=(12.6, 7.2))
|
||||
colors = [RED if i >= len(pairs) - 5 else BROWN for i in range(len(pairs))]
|
||||
ax.barh(pairs.index, pairs.values, color=colors, edgecolor="white", linewidth=0.6)
|
||||
for index, value in enumerate(pairs.values):
|
||||
ax.text(value + 0.6, index, str(value), va="center", fontsize=10, color=DARK)
|
||||
ax.set_xlabel("Число ошибок за 3 повтора", fontsize=13, color=DARK)
|
||||
ax.set_title("Самые частые пары «истинный → предсказанный»", fontsize=18, color=DARK)
|
||||
ax.tick_params(axis="y", labelsize=10)
|
||||
ax.grid(axis="x", alpha=0.2, color=DARK)
|
||||
ax.set_xlim(0, pairs.max() * 1.16)
|
||||
fig.tight_layout()
|
||||
fig.savefig(OUT / "confusion_pairs.png", dpi=220, bbox_inches="tight")
|
||||
plt.close(fig)
|
||||
|
||||
|
||||
def per_class_chart(frame: pd.DataFrame) -> None:
|
||||
first = frame[frame["repeat"] == frame["repeat"].min()]
|
||||
labels = sorted(first["y_true"].unique())
|
||||
rows = []
|
||||
for label in labels:
|
||||
mask = first["y_true"] == label
|
||||
tp = int(((first["y_true"] == label) & (first["y_pred"] == label)).sum())
|
||||
fp = int(((first["y_true"] != label) & (first["y_pred"] == label)).sum())
|
||||
fn = int(((first["y_true"] == label) & (first["y_pred"] != label)).sum())
|
||||
precision = tp / (tp + fp) if tp + fp else 0.0
|
||||
recall = tp / (tp + fn) if tp + fn else 0.0
|
||||
f1 = 2 * precision * recall / (precision + recall) if precision + recall else 0.0
|
||||
rows.append((label, f1, int(mask.sum())))
|
||||
table = pd.DataFrame(rows, columns=["class", "f1", "support"]).sort_values("f1")
|
||||
worst = table.head(12).sort_values("f1", ascending=False)
|
||||
best = table.tail(12).sort_values("f1")
|
||||
|
||||
fig, axes = plt.subplots(1, 2, figsize=(13.2, 7.0), gridspec_kw={"width_ratios": [1, 1]})
|
||||
axes[0].barh(worst["class"], worst["f1"], color=RED, edgecolor="white")
|
||||
axes[0].set_title("Худшие 12 персонажей", fontsize=15, color=DARK)
|
||||
axes[0].set_xlim(0, 0.72)
|
||||
axes[0].grid(axis="x", alpha=0.2)
|
||||
for index, (value, support) in enumerate(zip(worst["f1"], worst["support"])):
|
||||
axes[0].text(value + 0.008, index, f"{value:.2f} (n={support})", va="center", fontsize=9, color=DARK)
|
||||
axes[1].barh(best["class"], best["f1"], color=GREEN, edgecolor="white")
|
||||
axes[1].set_title("Лучшие 12 персонажей", fontsize=15, color=DARK)
|
||||
axes[1].set_xlim(0, 0.72)
|
||||
axes[1].grid(axis="x", alpha=0.2)
|
||||
for index, (value, support) in enumerate(zip(best["f1"], best["support"])):
|
||||
axes[1].text(value + 0.008, index, f"{value:.2f} (n={support})", va="center", fontsize=9, color=DARK)
|
||||
fig.suptitle("F-мера по персонажам: мягкое голосование, первый повтор", fontsize=17, color=DARK, y=0.99)
|
||||
fig.tight_layout()
|
||||
fig.savefig(OUT / "per_class_f1.png", dpi=220, bbox_inches="tight")
|
||||
plt.close(fig)
|
||||
|
||||
|
||||
def model_comparison() -> None:
|
||||
table = pd.read_csv(METRICS)
|
||||
wanted = [
|
||||
("voting_et_char_lean", "Мягкое голосование"),
|
||||
("et_char10000", "ExtraTrees, char=10000"),
|
||||
("nested_feature_selection", "Вложенный выбор"),
|
||||
("et_full", "ExtraTrees, полный"),
|
||||
("logreg_lean_content300", "Logreg, lean + content"),
|
||||
("stacking", "Стековое объединение"),
|
||||
]
|
||||
rows = []
|
||||
for key, label in wanted:
|
||||
row = table[table["label"] == key]
|
||||
if not row.empty:
|
||||
rows.append((label, float(row.iloc[0]["macro_f1_mean"]), float(row.iloc[0]["macro_f1_std"])))
|
||||
rows.append(("Случайный уровень", 0.029, 0.0))
|
||||
frame = pd.DataFrame(rows, columns=["label", "mean", "std"]).sort_values("mean")
|
||||
fig, ax = plt.subplots(figsize=(12.4, 6.8))
|
||||
colors = [GOLD if label == "Мягкое голосование" else (RED if label == "Случайный уровень" else GREEN) for label in frame["label"]]
|
||||
ax.barh(frame["label"], frame["mean"], xerr=frame["std"], color=colors, edgecolor="white", capsize=4)
|
||||
for index, (value, std) in enumerate(zip(frame["mean"], frame["std"])):
|
||||
offset = value + max(std, 0.0) + 0.008
|
||||
ax.text(offset, index, f"{value:.3f}", va="center", fontsize=11, color=DARK)
|
||||
ax.set_xlim(0, 0.38)
|
||||
ax.set_xlabel("Макро-усреднённая F-мера", fontsize=13, color=DARK)
|
||||
ax.set_title("Сравнение моделей: среднее и стандартное отклонение", fontsize=18, color=DARK)
|
||||
ax.grid(axis="x", alpha=0.2)
|
||||
fig.tight_layout()
|
||||
fig.savefig(OUT / "model_comparison.png", dpi=220, bbox_inches="tight")
|
||||
plt.close(fig)
|
||||
|
||||
|
||||
def feature_progression() -> None:
|
||||
labels = [
|
||||
"Только длина",
|
||||
"Ручные признаки",
|
||||
"Ручные + char/POS",
|
||||
"+ content-LSA",
|
||||
"ExtraTrees, char=10000",
|
||||
"Мягкое голосование",
|
||||
]
|
||||
values = [0.035, 0.105, 0.182, 0.249, 0.276, 0.304]
|
||||
fig, ax = plt.subplots(figsize=(12.4, 6.6))
|
||||
colors = [BROWN, BROWN, GREEN, GREEN, GOLD, RED]
|
||||
ax.bar(labels, values, color=colors, edgecolor="white")
|
||||
for index, value in enumerate(values):
|
||||
ax.text(index, value + 0.008, f"{value:.3f}", ha="center", fontsize=12, color=DARK)
|
||||
ax.set_ylim(0, 0.35)
|
||||
ax.set_ylabel("Макро-усреднённая F-мера", fontsize=13, color=DARK)
|
||||
ax.set_title("Что добавляет качество", fontsize=18, color=DARK)
|
||||
ax.tick_params(axis="x", rotation=18, labelsize=11)
|
||||
ax.grid(axis="y", alpha=0.2)
|
||||
fig.tight_layout()
|
||||
fig.savefig(OUT / "feature_progression.png", dpi=220, bbox_inches="tight")
|
||||
plt.close(fig)
|
||||
|
||||
|
||||
def topic_control() -> None:
|
||||
labels = ["Исходный\nExtraTrees", "Маскированный\nExtraTrees", "Исходный\nLogreg", "Маскированный\nLogreg"]
|
||||
values = [0.2483, 0.1129, 0.1467, 0.1067]
|
||||
fig, ax = plt.subplots(figsize=(10.8, 6.2))
|
||||
colors = [GREEN, RED, GREEN, RED]
|
||||
ax.bar(labels, values, color=colors, edgecolor="white")
|
||||
for index, value in enumerate(values):
|
||||
ax.text(index, value + 0.007, f"{value:.3f}", ha="center", fontsize=12, color=DARK)
|
||||
ax.set_ylim(0, 0.30)
|
||||
ax.set_ylabel("Макро-усреднённая F-мера", fontsize=13, color=DARK)
|
||||
ax.set_title("Контроль содержания: замена содержательных слов на CONTENT", fontsize=17, color=DARK)
|
||||
ax.tick_params(axis="x", labelsize=11)
|
||||
ax.grid(axis="y", alpha=0.2)
|
||||
fig.tight_layout()
|
||||
fig.savefig(OUT / "topic_control.png", dpi=220, bbox_inches="tight")
|
||||
plt.close(fig)
|
||||
|
||||
|
||||
def hub_shares(frame: pd.DataFrame) -> None:
|
||||
true = frame["y_true"].value_counts(normalize=True)
|
||||
pred = frame["y_pred"].value_counts(normalize=True)
|
||||
labels = pred.head(10).index.tolist()
|
||||
x = np.arange(len(labels))
|
||||
fig, ax = plt.subplots(figsize=(12.4, 6.2))
|
||||
ax.bar(x - 0.2, [true.get(label, 0) * 100 for label in labels], width=0.4, label="Истинная доля", color=GREEN)
|
||||
ax.bar(x + 0.2, [pred.get(label, 0) * 100 for label in labels], width=0.4, label="Доля предсказаний", color=RED)
|
||||
ax.set_xticks(x)
|
||||
ax.set_xticklabels(labels, rotation=25, ha="right", fontsize=11)
|
||||
ax.set_ylabel("% реплик", fontsize=13, color=DARK)
|
||||
ax.set_title("Классы-аттракторы: кого модель выбирает слишком часто", fontsize=17, color=DARK)
|
||||
ax.legend(frameon=False)
|
||||
ax.grid(axis="y", alpha=0.2)
|
||||
fig.tight_layout()
|
||||
fig.savefig(OUT / "hub_shares.png", dpi=220, bbox_inches="tight")
|
||||
plt.close(fig)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
frame = load_oof()
|
||||
confusion_top12(frame)
|
||||
confusion_full(frame)
|
||||
pairs_chart(frame)
|
||||
per_class_chart(frame)
|
||||
model_comparison()
|
||||
feature_progression()
|
||||
topic_control()
|
||||
hub_shares(frame)
|
||||
print("Готово:", OUT)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
3
lab1/presentation/requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
python-pptx>=1.0,<2
|
||||
Pillow>=10
|
||||
matplotlib>=3.8
|
||||