Files
NLP/lab1/presentation/rework_block1.py

252 lines
15 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Переработка первого блока текущей презентации.
Скрипт не пересобирает весь файл: он читает уже отредактированный 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: многоточия, кавычки, тире, неразрывные пробелы.",
"Плейсхолдеры <PLAYER>, <FARM> и другие заменены нейтральными словами.",
"Количество плейсхолдеров сохранено отдельным признаком.",
"Токенизация, предложения и 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())