Переработан первый блок презентации: задача, постановка, корпус, характеристики
This commit is contained in:
@@ -40,3 +40,12 @@ python presentation/build_deck.py
|
|||||||
|
|
||||||
Изображения игры используются в учебных целях; права принадлежат их
|
Изображения игры используются в учебных целях; права принадлежат их
|
||||||
правообладателям.
|
правообладателям.
|
||||||
|
|
||||||
|
## Текущая версия и автоматическая сборка
|
||||||
|
|
||||||
|
Актуальная версия — `stardew_stylometry.pptx`: она правится вручную и содержит
|
||||||
|
имена докладчиков и сокращённый набор слайдов. Скрипт `build_deck.py` — это
|
||||||
|
первоначальная автоматическая сборка; повторный запуск перезапишет файл и
|
||||||
|
потеряет ручные правки. Для точечной переработки первого блока использован
|
||||||
|
`rework_block1.py`; перед повторным применением скрипта сделайте резервную
|
||||||
|
копию PPTX.
|
||||||
|
|||||||
251
lab1/presentation/rework_block1.py
Normal file
251
lab1/presentation/rework_block1.py
Normal file
@@ -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: многоточия, кавычки, тире, неразрывные пробелы.",
|
||||||
|
"Плейсхолдеры <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())
|
||||||
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user