Добавлена презентация и материалы для трёх блоков доклада
This commit is contained in:
809
lab1/presentation/build_deck.py
Normal file
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())
|
||||
Reference in New Issue
Block a user