diff --git a/lab1/presentation/README.md b/lab1/presentation/README.md index d1bd5a0..8f949a9 100644 --- a/lab1/presentation/README.md +++ b/lab1/presentation/README.md @@ -40,3 +40,12 @@ python presentation/build_deck.py Изображения игры используются в учебных целях; права принадлежат их правообладателям. + +## Текущая версия и автоматическая сборка + +Актуальная версия — `stardew_stylometry.pptx`: она правится вручную и содержит +имена докладчиков и сокращённый набор слайдов. Скрипт `build_deck.py` — это +первоначальная автоматическая сборка; повторный запуск перезапишет файл и +потеряет ручные правки. Для точечной переработки первого блока использован +`rework_block1.py`; перед повторным применением скрипта сделайте резервную +копию PPTX. diff --git a/lab1/presentation/rework_block1.py b/lab1/presentation/rework_block1.py new file mode 100644 index 0000000..e0506a5 --- /dev/null +++ b/lab1/presentation/rework_block1.py @@ -0,0 +1,251 @@ +"""Переработка первого блока текущей презентации. + +Скрипт не пересобирает весь файл: он читает уже отредактированный PPTX, +удаляет старые слайды первого блока и вставляет новую академическую +последовательность: задача -> строгая постановка -> корпус -> очистка -> +характеристики. +""" + +from __future__ import annotations + +import sys +from pathlib import Path + +from pptx import Presentation +from pptx.enum.text import PP_ALIGN +from pptx.util import Inches + +HERE = Path(__file__).resolve().parent +if str(HERE) not in sys.path: + sys.path.insert(0, str(HERE)) + +from build_deck import ( # noqa: E402 + DARK, + GEN, + GREEN_DARK, + RED, + add_background, + add_bullets, + add_chicken, + add_footer, + add_image_fit, + add_notes, + add_panel, + add_table, + add_text, + add_title_bar, +) + + +PPTX = HERE / "stardew_stylometry.pptx" +BLOCK = "Блок 1 · Корпус" +NARRATOR = "А. А. Аладьина" + + +def delete_slide(prs, index: int) -> None: + slide_id = prs.slides._sldIdLst[index] + rel_id = slide_id.rId + prs.part.drop_rel(rel_id) + del prs.slides._sldIdLst[index] + + +def move_slide(prs, old_index: int, new_index: int) -> None: + slide_id = prs.slides._sldIdLst[old_index] + prs.slides._sldIdLst.remove(slide_id) + prs.slides._sldIdLst.insert(new_index, slide_id) + + +def base_slide(prs, title: str, subtitle: str | None = None): + slide = prs.slides.add_slide(prs.slide_layouts[6]) + add_background(slide) + add_title_bar(slide, title, NARRATOR, subtitle) + add_footer(slide, BLOCK, 0) + add_chicken(slide) + return slide + + +def slide_task(prs): + slide = base_slide(prs, "Решаемая задача") + add_panel(slide, 0.45, 1.35, 6.15, 5.55) + add_bullets(slide, 0.65, 1.55, 5.75, 5.1, [ + "Дано: 2005 коротких реплик, 34 персонажа, один автор.", + "Требуется: по одной реплике определить персонажа.", + "Класс — имя персонажа; один объект относится ровно к одному классу.", + "Реплики короткие: в среднем 19 слов, медиана 17.", + "Классы неравномерны: от 15 до 139 реплик.", + ], size=16, title="Постановка") + add_panel(slide, 6.8, 1.35, 6.05, 5.55) + add_bullets(slide, 7.0, 1.55, 5.65, 5.1, [ + "Проверяется, различимы ли речевые маски персонажей статистически.", + "Это не авторская атрибуция: все реплики написаны одним автором.", + "Ошибки между персонажами ожидаемы: общий автор, общие темы и контексты.", + "Практическая ценность — методика стилометрии для коротких текстов.", + "Ограничение: один корпус, одна игра, один автор.", + ], size=16, title="Что именно проверяется") + add_notes(slide, "Рассказчик 1: Аладьина Анна Андреевна. Сформулировать задачу без рекламных формулировок: есть корпус коротких реплик, нужно предсказать персонажа. Сразу отделить это от авторской атрибуции: автор один, задача — различимость речевых масок.") + + +def slide_formal(prs): + slide = base_slide(prs, "Строгая постановка задачи", "Формальная запись и протокол оценки") + add_panel(slide, 0.45, 1.35, 7.55, 5.55) + add_bullets(slide, 0.65, 1.55, 7.15, 5.1, [ + "D = {(x_i, y_i)}, i = 1..n; n = 2005.", + "x_i — одна реплика (короткий текст).", + "y_i ∈ Y, |Y| = 34 — имя персонажа.", + "φ(x_i) ∈ R^d — вектор признаков реплики.", + "f_θ: R^d → R^34 — классификатор с параметрами θ.", + "ŷ_i = argmax_{c ∈ Y} f_θ(φ(x_i))_c — предсказание.", + ], size=16, title="Дано") + add_panel(slide, 8.2, 1.35, 4.65, 5.55) + add_bullets(slide, 8.38, 1.55, 4.3, 5.1, [ + "Обучение: минимизация перекрёстной энтропии + L2-регуляризация.", + "Веса классов: balanced, чтобы редкие персонажи не игнорировались.", + "Оценка: macro-F1 = (1/|Y|) Σ_c 2P_cR_c / (P_c + R_c).", + "Схема: стратифицированная 5-блочная CV, повторы, вложенная настройка.", + "Случайный уровень: 0,029; top-3: 0,088.", + "Один объект — одна реплика; обучение и оценка разделены.", + ], size=14, title="Критерий и проверка") + add_notes(slide, "Рассказчик 1. Это ключевой слайд строгой постановки. Проговорить обозначения: объект — реплика, метка — персонаж, признаковое описание — φ(x), классификатор — f. Отдельно сказать, что оцениваем не accuracy, а macro-F1, потому что классов 34 и они неравномерны.") + + +def slide_corpus(prs): + slide = base_slide(prs, "Корпус: сбор и разметка") + add_panel(slide, 0.45, 1.35, 7.05, 5.55) + add_text(slide, 0.68, 1.55, 6.6, 0.3, "1. Сбор", size=17, bold=True, color=GREEN_DARK) + add_bullets(slide, 0.68, 1.9, 6.55, 1.35, [ + "Реплики извлечены из игровых диалогов Stardew Valley.", + "Единица анализа — одна реплика; всего 2005 объектов.", + "Корпус охватывает 34 персонажа; все тексты на английском языке.", + ], size=14) + add_text(slide, 0.68, 3.2, 6.6, 0.3, "2. Разметка", size=17, bold=True, color=GREEN_DARK) + add_bullets(slide, 0.68, 3.55, 6.55, 1.35, [ + "Метка — имя говорящего персонажа; один объект — один класс.", + "Ручная проверка соответствия реплики и персонажа.", + "Метаданные (пол, возраст, вид) хранятся отдельно и не подаются модели.", + ], size=14) + add_text(slide, 0.68, 4.85, 6.6, 0.3, "3. Ограничения", size=17, bold=True, color=GREEN_DARK) + add_bullets(slide, 0.68, 5.18, 6.55, 1.4, [ + "Нет идентификаторов сцен и диалогов.", + "Классы неравномерны: 15–139 реплик.", + "Часть коротких и общих фраз удалена вручную; правила не сохранились.", + ], size=14) + add_panel(slide, 7.7, 1.35, 5.15, 5.55) + add_image_fit(slide, GEN / "characters.png", 7.85, 1.5, 4.85, 3.9, panel=False) + add_text(slide, 7.9, 5.55, 4.75, 1.1, "34 персонажа: подростки, взрослые, пожилые, дети и нелюди. Все реплики написаны одним разработчиком.", size=13, color=DARK, align=PP_ALIGN.CENTER) + add_notes(slide, "Рассказчик 1. Описать три шага: сбор, разметка, ограничения. Не утверждать, что корпус идеален: ручная фильтрация и отсутствие сцен — существенные ограничения. Метаданные не являются признаками.") + + +def slide_cleaning(prs): + slide = base_slide(prs, "Очистка и шум") + add_panel(slide, 0.45, 1.35, 6.1, 5.55) + add_bullets(slide, 0.65, 1.55, 5.7, 5.1, [ + "Нормализация Unicode: многоточия, кавычки, тире, неразрывные пробелы.", + "Плейсхолдеры , и другие заменены нейтральными словами.", + "Количество плейсхолдеров сохранено отдельным признаком.", + "Токенизация, предложения и POS-разметка — NLTK; Universal POS.", + "Пунктуация, регистр и стоп-слова сохранены: это стилометрический сигнал.", + ], size=15, title="Что сделали") + add_panel(slide, 6.75, 1.35, 6.1, 5.55) + add_bullets(slide, 6.95, 1.55, 5.7, 5.1, [ + "Шум: дубликаты, технические артефакты, общие короткие формулы.", + "Ручная фильтрация удалила короткие и общие фразы; правила не сохранены.", + "В текущем CSV 0 точных дублей; 13 пар с косинусной близостью ≥ 0,8 — внутри класса.", + "Нет сцен и диалогов: нельзя учесть корреляцию реплик внутри сцены.", + "Разметка только по имени; ручной стилистической разметки нет.", + ], size=15, title="Что считаем шумом") + add_notes(slide, "Рассказчик 1. Объяснить, что очистка не должна удалять стиль: пунктуация, регистр и служебные слова сохранены. Ручная фильтрация — ограничение; почти-дубликаты не пересекают классы, поэтому утечки через них нет. Отметить отсутствие сцен.") + + +def slide_class_stats(prs): + slide = base_slide(prs, "Характеристики корпуса: классы", "Распределение реплик по персонажам") + add_panel(slide, 0.4, 1.35, 7.2, 5.55) + add_image_fit(slide, GEN.parent / "images" / "replics-count.png", 0.5, 1.45, 7.0, 5.35) + add_panel(slide, 7.8, 1.35, 5.1, 5.55) + rows = [ + ["Показатель", "Значение"], + ["Реплик", "2005"], + ["Персонажей", "34"], + ["Максимум", "Abigail, 139"], + ["Минимум", "Mister Qi, 15"], + ["Разброс классов", "15–139"], + ["Случайный уровень", "2,9%"], + ] + add_table(slide, 8.0, 1.55, 4.7, 3.2, rows, col_widths=[2.4, 2.3], font_size=12) + add_bullets(slide, 8.0, 4.9, 4.7, 1.8, [ + "Дисбаланс почти десятикратный.", + "Поэтому основная метрика — macro-F1, а не доля правильных ответов.", + ], size=13) + add_notes(slide, "Рассказчик 1. Показать дисбаланс: максимум Abigail 139, минимум Mister Qi 15. При 34 классах случайное угадывание — 2,9%. Отсюда macro-F1 и веса классов; accuracy может обманывать.") + + +def slide_length_stats(prs): + slide = base_slide(prs, "Характеристики корпуса: длина реплик", "Медианная длина предложения в символах") + add_panel(slide, 0.4, 1.35, 7.2, 5.55) + add_image_fit(slide, GEN.parent / "images" / "median.png", 0.5, 1.45, 7.0, 5.35) + add_panel(slide, 7.8, 1.35, 5.1, 5.55) + rows = [ + ["Показатель", "Значение"], + ["Средняя длина", "19,1 слова"], + ["Медиана", "17 слов"], + ["Диапазон", "3–78 слов"], + ["Медиана в символах", "52–114"], + ] + add_table(slide, 8.0, 1.55, 4.7, 2.5, rows, col_widths=[2.4, 2.3], font_size=12) + add_bullets(slide, 8.0, 4.25, 4.7, 2.35, [ + "Короткие реплики дают мало стилометрических свидетельств.", + "Длина — сильный признак, но может стать прокси персонажа.", + "Её вклад проверяется отдельно и по группам длины.", + ], size=13) + add_notes(slide, "Рассказчик 1. Числа у подписей — медианная длина предложения в символах. Контраст Vincent/Jas и Sandy/Wizard. Длина — законный, но опасный признак: она может отражать не стиль, а привычную длину реплик персонажа.") + + +def renumber(prs) -> None: + for index, slide in enumerate(prs.slides, start=1): + for shape in slide.shapes: + if not shape.has_text_frame: + continue + text = shape.text_frame.text.strip() + if text.isdigit() and shape.top > Inches(6.9) and shape.width < Inches(0.8): + shape.text_frame.text = str(index) + + +def replace_narrators(prs) -> None: + mapping = { + "Рассказчик 2": "В. Н. Копнин", + "Рассказчик 3": "М. В. Савостиков", + } + for slide in prs.slides: + for shape in slide.shapes: + if shape.has_text_frame and shape.text_frame.text.strip() in mapping: + shape.text_frame.text = mapping[shape.text_frame.text.strip()] + + +def main() -> int: + prs = Presentation(PPTX) + # Сначала создаём новые слайды в конце, чтобы их части получили + # уникальные имена и не конфликтовали с удаляемыми слайдами. + base = len(prs.slides) + slide_task(prs) + slide_formal(prs) + slide_corpus(prs) + slide_cleaning(prs) + slide_class_stats(prs) + slide_length_stats(prs) + + # Переносим новые слайды сразу после титульного. + for offset in range(6): + move_slide(prs, base + offset, 1 + offset) + + # Теперь старые слайды первого блока находятся на позициях 7–12. + for _ in range(6): + delete_slide(prs, 7) + + replace_narrators(prs) + renumber(prs) + prs.save(PPTX) + print(f"Готово: {PPTX}; слайдов: {len(prs.slides)}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/lab1/presentation/stardew_stylometry.pdf b/lab1/presentation/stardew_stylometry.pdf index 2d4b79a..8534dd0 100644 Binary files a/lab1/presentation/stardew_stylometry.pdf and b/lab1/presentation/stardew_stylometry.pdf differ diff --git a/lab1/presentation/stardew_stylometry.pptx b/lab1/presentation/stardew_stylometry.pptx index 87df534..0734d2f 100644 Binary files a/lab1/presentation/stardew_stylometry.pptx and b/lab1/presentation/stardew_stylometry.pptx differ