Добавлены итоговый отчёт и архив лабораторной работы
3
lab1/.gitignore
vendored
@@ -18,3 +18,6 @@ __pycache__/
|
|||||||
/presentation/latex/*.toc
|
/presentation/latex/*.toc
|
||||||
/presentation/latex/*.synctex.gz
|
/presentation/latex/*.synctex.gz
|
||||||
/presentation/latex/preview/
|
/presentation/latex/preview/
|
||||||
|
|
||||||
|
/report/build/
|
||||||
|
/dist/stage/
|
||||||
|
|||||||
529
lab1/ARCHIVE_README.md
Normal file
@@ -0,0 +1,529 @@
|
|||||||
|
# Архив лабораторной работы №1
|
||||||
|
|
||||||
|
**Тема:** классификация текстов на основе стилометрических характеристик.
|
||||||
|
**Материал:** реплики персонажей игры Stardew Valley.
|
||||||
|
**Задача:** по одной реплике определить, какому из 34 персонажей она
|
||||||
|
принадлежит, и проверить, какие стилометрические признаки несут этот сигнал.
|
||||||
|
|
||||||
|
Это не авторская атрибуция: все реплики написаны одним автором. Мы различаем
|
||||||
|
**речевые маски персонажей** — намеренно созданные автором различия в лексике,
|
||||||
|
синтаксисе, пунктуации и разговорных маркерах.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Что в архиве
|
||||||
|
|
||||||
|
| Путь | Что это |
|
||||||
|
|---|---|
|
||||||
|
| `lab1/report/REPORT.pdf` | Итоговый отчёт, 21 страница. Главный документ. |
|
||||||
|
| `lab1/report/REPORT.docx` | Редактируемая версия отчёта. |
|
||||||
|
| `lab1/report/REPORT.md` | Исходник отчёта в Markdown. |
|
||||||
|
| `lab1/report/figures/` | Рисунки отчёта. |
|
||||||
|
| `lab1/presentation/stardew_stylometry.pdf` | Презентация для защиты. |
|
||||||
|
| `lab1/presentation/stardew_stylometry.pptx` | Редактируемая презентация. |
|
||||||
|
| `lab1/presentation/latex/` | LaTeX-исходник и PDF презентации. |
|
||||||
|
| `lab1/presentation/images/`, `generated/` | Исходные изображения и диаграммы. |
|
||||||
|
| `lab1/stylometry/` | Основной код проекта. |
|
||||||
|
| `lab1/scripts/` | Проверка окружения, запуск серий, сборка отчёта, упаковка. |
|
||||||
|
| `lab1/tests/` | 33 теста (`pytest`). |
|
||||||
|
| `lab1/configs/default.toml` | Конфигурация по умолчанию. |
|
||||||
|
| `lab1/data/dialogues.csv` | Корпус: 2005 реплик, cp1251, разделитель `;`. |
|
||||||
|
| `lab1/data/dialogues.xlsx` | Тот же корпус в Excel. |
|
||||||
|
| `lab1/data/stats_per_character.xlsx` | Статистика по персонажам. |
|
||||||
|
| `lab1/data/metadata/` | Метаданные персонажей для анализа ошибок. |
|
||||||
|
| `lab1/data/corpus_report_*.txt` | Отчёты о состоянии корпуса до/после очистки. |
|
||||||
|
| `lab1/FINAL_RESULTS.csv` | Сводная таблица финальных результатов. |
|
||||||
|
| `lab1/FINAL_REPORT.md` | Расширенная версия отчёта с деталями экспериментов. |
|
||||||
|
| `lab1/EXPERIMENTS.md` | Протокол экспериментов и команд. |
|
||||||
|
| `lab1/FEATURES.md` | Признаки, формулы, обоснование. |
|
||||||
|
| `lab1/METRICS_AND_CLASSIFIERS.md` | Метрики и классификаторы. |
|
||||||
|
| `lab1/TUNING.md` | Настройка гиперпараметров. |
|
||||||
|
| `lab1/RUNBOOK.md` | Подробная инструкция по запуску полной серии. |
|
||||||
|
| `lab1/RESULTS_ANALYSIS.md` | Анализ результатов и ошибок. |
|
||||||
|
| `lab1/FOLLOWUP_EXPERIMENTS.md` | Дополнительные эксперименты. |
|
||||||
|
| `lab1/PRESENTATION_OUTLINE.md` | Структура презентации. |
|
||||||
|
| `lab1/exports/nlp_lab1_final_results_20261026_120000.tar.gz` | Финальные результаты в компактном виде. |
|
||||||
|
| `lab1/requirements.txt` | Основные зависимости. |
|
||||||
|
| `lab1/requirements-report.txt` | Зависимости для пересборки отчёта. |
|
||||||
|
|
||||||
|
Полный список файлов с размерами и SHA-256 — в `MANIFEST.txt`.
|
||||||
|
Контрольные суммы ключевых файлов — в `CHECKSUMS.sha256`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Требования
|
||||||
|
|
||||||
|
### 2.1. Обязательные
|
||||||
|
|
||||||
|
- Python **3.11+** (проверено на 3.14);
|
||||||
|
- Linux, macOS или Windows с WSL;
|
||||||
|
- около 1 ГБ свободного места с виртуальным окружением и NLTK;
|
||||||
|
- интернет — только для установки пакетов и скачивания NLTK.
|
||||||
|
|
||||||
|
### 2.2. Для пересборки отчёта
|
||||||
|
|
||||||
|
- `pandoc` 3.x;
|
||||||
|
- LibreOffice (`libreoffice --headless`);
|
||||||
|
- Python-пакеты `python-docx`, `PyMuPDF` (см. `requirements-report.txt`);
|
||||||
|
- шрифты с кириллицей: Times New Roman или Liberation Serif, Liberation Mono.
|
||||||
|
|
||||||
|
### 2.3. Для пересборки LaTeX-презентации
|
||||||
|
|
||||||
|
- XeLaTeX;
|
||||||
|
- пакеты Beamer, tcolorbox, TikZ, fontspec, babel-russian;
|
||||||
|
- шрифт Liberation Sans.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Установка
|
||||||
|
|
||||||
|
Все команды выполняются из каталога `lab1`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd lab1
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.1. Виртуальное окружение
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m venv .venv
|
||||||
|
source .venv/bin/activate
|
||||||
|
```
|
||||||
|
|
||||||
|
Windows PowerShell:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python -m venv .venv
|
||||||
|
.venv\Scripts\activate
|
||||||
|
```
|
||||||
|
|
||||||
|
Если окружение уже есть в корне проекта, можно использовать его:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
source ../.venv/bin/activate
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2. Зависимости
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
python -m pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
Для пересборки отчёта дополнительно:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m pip install -r requirements-report.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3. Ресурсы NLTK
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python scripts/download_nltk_data.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Скрипт скачивает `punkt`, `punkt_tab`, `averaged_perceptron_tagger`,
|
||||||
|
`averaged_perceptron_tagger_eng`, `stopwords` в `lab1/nltk_data`.
|
||||||
|
|
||||||
|
Без этих ресурсов POS-признаки отключатся, а результаты будут неполными.
|
||||||
|
Если машина без интернета, скопируйте готовый каталог `nltk_data` в `lab1/`.
|
||||||
|
|
||||||
|
### 3.4. Проверка
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python scripts/check_environment.py
|
||||||
|
python -m pytest -q
|
||||||
|
```
|
||||||
|
|
||||||
|
Ожидается:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Окружение готово.
|
||||||
|
33 passed
|
||||||
|
```
|
||||||
|
|
||||||
|
`check_environment.py` печатает версии библиотек, SHA-256 корпуса, число
|
||||||
|
реплик, персонажей и словоупотреблений, проверяет токенизатор и POS-теггер.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Быстрый запуск
|
||||||
|
|
||||||
|
### 4.1. Аудит корпуса
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m stylometry.cli audit
|
||||||
|
```
|
||||||
|
|
||||||
|
Создаёт:
|
||||||
|
|
||||||
|
- `data/processed/audit.md` — отчёт по корпусу;
|
||||||
|
- `data/processed/lines.csv` — нормализованные реплики;
|
||||||
|
- `data/processed/figures/class_distribution.png`.
|
||||||
|
|
||||||
|
Проверьте: 2005 реплик, 34 персонажа, 38 207 словоупотреблений, точных
|
||||||
|
дублей нет.
|
||||||
|
|
||||||
|
### 4.2. Ручные признаки
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m stylometry.cli features
|
||||||
|
```
|
||||||
|
|
||||||
|
Создаёт:
|
||||||
|
|
||||||
|
- `features/stylometric.csv` — матрица 2005 × 454;
|
||||||
|
- `features/feature_dictionary.csv` — описание признаков.
|
||||||
|
|
||||||
|
### 4.3. Одна модель
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m stylometry.cli run \
|
||||||
|
--preset stylometry_pos_lsa \
|
||||||
|
--model logreg \
|
||||||
|
--folds 5 \
|
||||||
|
--repeats 5
|
||||||
|
```
|
||||||
|
|
||||||
|
Результаты появятся в `results/<preset>_<model>_<timestamp>/`:
|
||||||
|
`summary.md`, `summary.json`, `fold_metrics.csv`, `pooled_metrics.csv`,
|
||||||
|
`per_class_report.csv`, `confusion_matrix.csv`, `oof_predictions.csv`.
|
||||||
|
|
||||||
|
### 4.4. Полная серия
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash scripts/run_all_experiments.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Скрипт последовательно запускает все стадии и пишет лог в
|
||||||
|
`results/logs/run_all_<timestamp>.log`. Ориентировочное время — 1,5–3 часа
|
||||||
|
на обычном ноутбуке. Быстрый режим без финальных чисел:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
QUICK=1 bash scripts/run_all_experiments.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.5. Финальные серии
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python scripts/run_final_experiments.py \
|
||||||
|
--stages nested topic calibration ensemble extra \
|
||||||
|
--out results/final
|
||||||
|
```
|
||||||
|
|
||||||
|
Эта команда воспроизводит вложенный выбор конфигурации, контроль содержания,
|
||||||
|
коррекцию приоров, ансамбли и дополнительные конфигурации. Именно эти
|
||||||
|
результаты сведены в `FINAL_RESULTS.csv` и в отчёте.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Что делает каждый модуль
|
||||||
|
|
||||||
|
| Модуль | Назначение |
|
||||||
|
|---|---|
|
||||||
|
| `stylometry/io.py` | Чтение CSV/XLSX, проверка колонок, SHA-256 корпуса. |
|
||||||
|
| `stylometry/cleaning.py` | Unicode-нормализация, плейсхолдеры, пробелы. |
|
||||||
|
| `stylometry/tokenization.py` | NLTK: токенизация, предложения, POS; резервный режим. |
|
||||||
|
| `stylometry/features/` | 454 ручных признака в 11 группах. |
|
||||||
|
| `stylometry/lsa.py` | TF-IDF + TruncatedSVD для word/content/fw/char/pos. |
|
||||||
|
| `stylometry/pipelines.py` | Объединение блоков признаков, модели, сетки параметров. |
|
||||||
|
| `stylometry/cv.py` | Повторная стратифицированная и вложенная кросс-валидация. |
|
||||||
|
| `stylometry/evaluation.py` | Macro-F1, accuracy, balanced accuracy, MCC, каппа, top-k, log loss, Brier, бутстрэп. |
|
||||||
|
| `stylometry/stats.py` | Мак-Немар, Уилкоксон, Фридман, поправка Холма. |
|
||||||
|
| `stylometry/delta.py` | Дельта Барроуза: MFW, z-нормирование, манхэттенское и косинусное расстояние. |
|
||||||
|
| `stylometry/verification.py` | Верификация «свой/чужой»: ROC-AUC, EER, TAR@FAR. |
|
||||||
|
| `stylometry/clustering.py` | Иерархическая и k-means кластеризация, ARI, NMI, силуэт. |
|
||||||
|
| `stylometry/plotting.py` | Диаграммы классов, scree, матрицы ошибок, PCA, t-SNE. |
|
||||||
|
| `stylometry/metadata.py` | Метаданные персонажей, анализ подгрупп. |
|
||||||
|
| `stylometry/cli.py` | Единая точка входа для всех команд. |
|
||||||
|
| `main.py` | Короткая обёртка над `stylometry.cli`. |
|
||||||
|
|
||||||
|
### Группы ручных признаков
|
||||||
|
|
||||||
|
1. Длина.
|
||||||
|
2. Лексика.
|
||||||
|
3. Словарное богатство.
|
||||||
|
4. Символы.
|
||||||
|
5. Пунктуация.
|
||||||
|
6. Функциональные слова.
|
||||||
|
7. Местоимения.
|
||||||
|
8. Части речи.
|
||||||
|
9. Синтаксис.
|
||||||
|
10. Читаемость.
|
||||||
|
11. Разговорные маркеры.
|
||||||
|
|
||||||
|
Формулы и обоснование — в `FEATURES.md`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Данные и разметка
|
||||||
|
|
||||||
|
### 6.1. Формат
|
||||||
|
|
||||||
|
`data/dialogues.csv`:
|
||||||
|
|
||||||
|
- кодировка cp1251;
|
||||||
|
- разделитель `;`;
|
||||||
|
- колонки `Реплика` и `Персонаж`;
|
||||||
|
- 2005 строк, 34 класса.
|
||||||
|
|
||||||
|
`data/dialogues.xlsx` — тот же корпус для Excel.
|
||||||
|
`data/stats_per_character.xlsx` — число реплик и длины по персонажам.
|
||||||
|
`data/metadata/` — пол, возраст, вид и роль для анализа ошибок; в признаки
|
||||||
|
классификатора эти поля не подаются.
|
||||||
|
|
||||||
|
### 6.2. Правила разметки
|
||||||
|
|
||||||
|
1. Одна реплика — один объект.
|
||||||
|
2. Метка — имя говорящего.
|
||||||
|
3. Один объект относится ровно к одному классу.
|
||||||
|
4. Соответствие реплики и метки проверено вручную.
|
||||||
|
5. Служебные метки `<PLAYER>`, `<FARM>`, `<PET>`, `<SPOUSE>`, `<TIME>`,
|
||||||
|
`<NOUN>`, `<NAME>` заменены нейтральными словами; их число сохранено
|
||||||
|
отдельным признаком.
|
||||||
|
|
||||||
|
### 6.3. Очистка и шум
|
||||||
|
|
||||||
|
Нормализуется только Unicode: варианты кавычек, тире, многоточий и пробелов.
|
||||||
|
Пунктуация, регистр и стоп-слова **сохраняются** — это стилометрический
|
||||||
|
сигнал.
|
||||||
|
|
||||||
|
Шумом считались точные дубликаты, технические артефакты, общие короткие
|
||||||
|
формулы и плейсхолдеры. Корпус прошёл ручную фильтрацию; точные правила
|
||||||
|
фильтрации не сохранились, поэтому исходное распределение восстановить
|
||||||
|
нельзя. Это ограничение зафиксировано в отчёте.
|
||||||
|
|
||||||
|
### 6.4. Ограничение по объёму
|
||||||
|
|
||||||
|
Формальные требования: не менее 90 текстов по 30 предложений и примерно
|
||||||
|
равные классы. В этом корпусе единица анализа — реплика (в среднем 19 слов),
|
||||||
|
классы неравномерны (15–139 реплик). По согласованию с преподавателем
|
||||||
|
эксперимент выполнен на уровне реплик; в отчёте раздел 2.5 честно описывает
|
||||||
|
это отклонение. Для компенсации используются веса классов и macro-F1.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Метрики и результаты
|
||||||
|
|
||||||
|
Основная метрика — **macro-F1**: средняя F-мера по 34 классам, каждый
|
||||||
|
персонаж весит одинаково. Дополнительно считаются accuracy, balanced
|
||||||
|
accuracy, top-3/top-5, MCC, каппа Коэна, log loss, Brier, матрица ошибок,
|
||||||
|
бутстрэп-интервалы.
|
||||||
|
|
||||||
|
| Конфигурация | Macro-F1 |
|
||||||
|
|---|---:|
|
||||||
|
| Мягкое голосование трёх моделей | **0,3036 ± 0,0189** |
|
||||||
|
| ExtraTrees, `char_max_features=10000` | 0,2757 ± 0,0219 |
|
||||||
|
| Вложенный выбор конфигурации | 0,2746 ± 0,0233 |
|
||||||
|
| ExtraTrees, полный набор | 0,2499 ± 0,0209 |
|
||||||
|
| Логистическая регрессия, lean + content-LSA | 0,2466 ± 0,0191 |
|
||||||
|
| Случайный лес, `char_max_features=10000` | 0,2583 ± 0,0066 |
|
||||||
|
| Только ручные признаки | 0,1051 ± 0,0038 |
|
||||||
|
| Случайный уровень | 0,029 |
|
||||||
|
|
||||||
|
На отложенной выборке 80/20 мягкое голосование дало 0,3333.
|
||||||
|
|
||||||
|
Контроль содержания: после замены существительных, глаголов, прилагательных,
|
||||||
|
наречий и числительных маркером `CONTENT` macro-F1 падает с 0,2483 до 0,1129.
|
||||||
|
Остаток всё ещё выше случайного уровня, но основной сигнал —
|
||||||
|
лексико-тематический.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Как пересобрать отчёт
|
||||||
|
|
||||||
|
Отчёт собирается из `report/REPORT.md`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd lab1
|
||||||
|
python scripts/build_report.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Скрипт:
|
||||||
|
|
||||||
|
1. вызывает `pandoc` и делает `report/build/REPORT_raw.docx`;
|
||||||
|
2. оформляет стили, поля, колонтитулы, таблицы и рисунки;
|
||||||
|
3. сохраняет `report/REPORT.docx`;
|
||||||
|
4. конвертирует DOCX в `report/REPORT.pdf` через LibreOffice;
|
||||||
|
5. проверяет, что в PDF есть все основные разделы.
|
||||||
|
|
||||||
|
Только DOCX:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python scripts/build_report.py --skip-pdf
|
||||||
|
```
|
||||||
|
|
||||||
|
Требования: `pandoc`, `libreoffice`, `python-docx`, `PyMuPDF`.
|
||||||
|
Рисунки лежат в `report/figures/`; Markdown ссылается на них относительно
|
||||||
|
`report/`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Как пересобрать презентацию
|
||||||
|
|
||||||
|
### 9.1. PPTX
|
||||||
|
|
||||||
|
В архиве лежит **финальная** версия `stardew_stylometry.pptx` (30 слайдов)
|
||||||
|
после ручных правок. Скрипт `build_deck.py` — генератор исходной версии
|
||||||
|
презентации; он создаёт 32 слайда и **перезапишет** финальный файл, если его
|
||||||
|
запустить. Не запускайте скрипт, если хотите сохранить ручные правки: для
|
||||||
|
изменений открывайте PPTX в PowerPoint или LibreOffice.
|
||||||
|
|
||||||
|
Если нужно пересобрать исходную версию с нуля:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd lab1/presentation
|
||||||
|
python build_deck.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Скрипт использует `python-pptx`, `Pillow` и `matplotlib`. Диаграммы уже лежат
|
||||||
|
в `presentation/generated/`; для их перегенерации нужны результаты серий
|
||||||
|
(`results/final/...`), которых нет в архиве — сначала запустите эксперименты
|
||||||
|
из раздела 4.
|
||||||
|
|
||||||
|
### 9.2. LaTeX/PDF
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd lab1/presentation/latex
|
||||||
|
xelatex stardew_stylometry.tex
|
||||||
|
xelatex stardew_stylometry.tex
|
||||||
|
```
|
||||||
|
|
||||||
|
Нужны XeLaTeX, Beamer, tcolorbox, TikZ и шрифт Liberation Sans. Готовый PDF —
|
||||||
|
`stardew_stylometry.pdf`. Изображения подтягиваются из `../images/` и
|
||||||
|
`../generated/`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Где смотреть результаты
|
||||||
|
|
||||||
|
- `lab1/report/REPORT.pdf` — итоговый отчёт со всеми числами и выводами.
|
||||||
|
- `lab1/FINAL_RESULTS.csv` — сводная таблица всех финальных конфигураций.
|
||||||
|
- `lab1/FINAL_REPORT.md` — расширенное описание экспериментов.
|
||||||
|
- `lab1/RESULTS_ANALYSIS.md` — анализ ошибок, классов-аттракторов и длины.
|
||||||
|
- `lab1/exports/nlp_lab1_final_results_20261026_120000.tar.gz` — исходные
|
||||||
|
`summary.json`, `fold_metrics.csv`, `confusion_matrix.csv` и предсказания
|
||||||
|
финальных серий.
|
||||||
|
- `lab1/presentation/generated/` — диаграммы для отчёта и презентации.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Частые проблемы
|
||||||
|
|
||||||
|
### `check_environment.py` не находит POS-теггер
|
||||||
|
|
||||||
|
Повторите:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python scripts/download_nltk_data.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Проверьте, что каталог `lab1/nltk_data` существует и не пуст. Если сеть
|
||||||
|
недоступна, скопируйте `nltk_data` с другой машины.
|
||||||
|
|
||||||
|
### `pytest` не устанавливается
|
||||||
|
|
||||||
|
Убедитесь, что активировано виртуальное окружение и версия Python не ниже
|
||||||
|
3.11:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python --version
|
||||||
|
python -m pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
### `python -m stylometry.cli` не находит модуль
|
||||||
|
|
||||||
|
Команда должна выполняться из каталога `lab1`, а не из корня репозитория:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd lab1
|
||||||
|
python -m stylometry.cli audit
|
||||||
|
```
|
||||||
|
|
||||||
|
### LibreOffice не собирает PDF
|
||||||
|
|
||||||
|
Проверьте:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
libreoffice --version
|
||||||
|
```
|
||||||
|
|
||||||
|
На сервере без графической сессии используйте именно `--headless`
|
||||||
|
(это уже делает `build_report.py`). Если LibreOffice нет, отчёт можно
|
||||||
|
открыть в Word/LibreOffice вручную и экспортировать в PDF.
|
||||||
|
|
||||||
|
### Кириллица в PDF выглядит квадратами
|
||||||
|
|
||||||
|
Установите шрифты Liberation Serif/Sans и Liberation Mono. В отчёте
|
||||||
|
используются Times New Roman (с запасным Liberation Serif) и Liberation Mono.
|
||||||
|
|
||||||
|
### Полная серия идёт слишком долго
|
||||||
|
|
||||||
|
Запускайте в `tmux` или `nohup`, см. `RUNBOOK.md`, раздел 5.4. Для проверки
|
||||||
|
работоспособности используйте `QUICK=1`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. Что исключено из архива
|
||||||
|
|
||||||
|
Чтобы архив оставался компактным и воспроизводимым, в него не включены:
|
||||||
|
|
||||||
|
- `.venv/` — виртуальное окружение;
|
||||||
|
- `.git/` — история репозитория;
|
||||||
|
- `nltk_data/` — скачивается скриптом;
|
||||||
|
- `data/processed/` — аудит и промежуточные таблицы;
|
||||||
|
- `features/` — матрица признаков (создаётся `features`);
|
||||||
|
- `results/` — промежуточные результаты серий;
|
||||||
|
- большие `exports/*.tar.gz` — кроме компактного финального архива;
|
||||||
|
- `presentation/preview/` — PNG-превью слайдов;
|
||||||
|
- `__pycache__/`, `.pytest_cache/` — кэши;
|
||||||
|
- `presentation/stardew_stylometry.bak` — резервная копия;
|
||||||
|
- служебные файлы LaTeX (`.aux`, `.log`, `.nav`, `.out`, `.snm`, `.toc`).
|
||||||
|
|
||||||
|
Всё это восстанавливается командами из разделов 3–4.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 13. Проверка целостности архива
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sha256sum -c CHECKSUMS.sha256
|
||||||
|
```
|
||||||
|
|
||||||
|
`MANIFEST.txt` содержит список всех файлов архива с размерами и SHA-256.
|
||||||
|
Если контрольные суммы совпали, архив распакован полностью.
|
||||||
|
|
||||||
|
Минимальная проверка после распаковки:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd lab1
|
||||||
|
python scripts/check_environment.py
|
||||||
|
python -m pytest -q
|
||||||
|
python -m stylometry.cli audit
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 14. Атрибуция
|
||||||
|
|
||||||
|
Диалоги Stardew Valley принадлежат ConcernedApe. Корпус используется
|
||||||
|
исключительно в учебных и исследовательских целях. Код проекта можно
|
||||||
|
использовать и изменять для учебных задач.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 15. Шпаргалка команд
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# из lab1
|
||||||
|
python scripts/check_environment.py # проверить окружение
|
||||||
|
python -m pytest -q # 33 теста
|
||||||
|
python -m stylometry.cli audit # аудит корпуса
|
||||||
|
python -m stylometry.cli features # 454 ручных признака
|
||||||
|
python -m stylometry.cli run \
|
||||||
|
--preset stylometry_pos_lsa --model logreg --repeats 5
|
||||||
|
bash scripts/run_all_experiments.sh # полная серия
|
||||||
|
python scripts/run_final_experiments.py \
|
||||||
|
--stages nested topic calibration ensemble extra --out results/final
|
||||||
|
python scripts/build_report.py # отчёт DOCX + PDF
|
||||||
|
```
|
||||||
@@ -136,6 +136,25 @@ python -m stylometry.cli subgroup-report \
|
|||||||
| `lsa_word`, `lsa_pos` | только соответствующий вариант LSA |
|
| `lsa_word`, `lsa_pos` | только соответствующий вариант LSA |
|
||||||
| `length` | только признаки длины (нижняя граница) |
|
| `length` | только признаки длины (нижняя граница) |
|
||||||
|
|
||||||
|
## Отчёт
|
||||||
|
|
||||||
|
Итоговый отчёт:
|
||||||
|
|
||||||
|
- **`report/REPORT.pdf`** — основной документ, 21 страница;
|
||||||
|
- **`report/REPORT.docx`** — редактируемая версия;
|
||||||
|
- **`report/REPORT.md`** — исходник Markdown;
|
||||||
|
- **`report/figures/`** — рисунки.
|
||||||
|
|
||||||
|
Пересборка:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python scripts/build_report.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Для сборки нужны `pandoc`, LibreOffice и пакеты из
|
||||||
|
`requirements-report.txt`. Скрипт сам оформляет титул, колонтитулы, таблицы,
|
||||||
|
нумерацию рисунков и проверяет, что в PDF есть все разделы.
|
||||||
|
|
||||||
## Презентация
|
## Презентация
|
||||||
|
|
||||||
Есть две версии:
|
Есть две версии:
|
||||||
@@ -148,7 +167,7 @@ python -m stylometry.cli subgroup-report \
|
|||||||
- **`presentation/latex/stardew_stylometry.pdf`** — собранный PDF LaTeX-версии
|
- **`presentation/latex/stardew_stylometry.pdf`** — собранный PDF LaTeX-версии
|
||||||
(30 страниц).
|
(30 страниц).
|
||||||
|
|
||||||
LaTeX-версия содержит три блока на трёх докладчиков, фон и курицу на каждом
|
LaTeX-версия содержит три части на трёх докладчиков, фон и курицу на каждом
|
||||||
слайде, титульную заставку, изображение персонажей, две круговые диаграммы,
|
слайде, титульную заставку, изображение персонажей, две круговые диаграммы,
|
||||||
матрицы ошибок, F-меры по персонажам, приложение с полной матрицей и финальный
|
матрицы ошибок, F-меры по персонажам, приложение с полной матрицей и финальный
|
||||||
мем. Формул почти нет: одна компактная запись обозначений на слайде постановки
|
мем. Формул почти нет: одна компактная запись обозначений на слайде постановки
|
||||||
@@ -180,6 +199,10 @@ python -m stylometry.cli run --config configs/default.toml \
|
|||||||
|
|
||||||
## Основные документы
|
## Основные документы
|
||||||
|
|
||||||
|
- **`START_HERE.md`** — краткая инструкция: что открыть и как проверить
|
||||||
|
окружение за три минуты.
|
||||||
|
- **`ARCHIVE_README.md`** — подробное описание архива: состав, установка,
|
||||||
|
запуск, структура модулей, пересборка отчёта и презентации.
|
||||||
- **`RUNBOOK.md`** — пошаговая инструкция для запуска полной серии
|
- **`RUNBOOK.md`** — пошаговая инструкция для запуска полной серии
|
||||||
экспериментов: установка, проверка окружения, все команды, упаковка
|
экспериментов: установка, проверка окружения, все команды, упаковка
|
||||||
результатов и типичные ошибки.
|
результатов и типичные ошибки.
|
||||||
|
|||||||
81
lab1/START_HERE.md
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
# С чего начать
|
||||||
|
|
||||||
|
Это архив лабораторной работы №1 по курсу «Автоматический анализ текстов»:
|
||||||
|
стилометрическая классификация реплик персонажей Stardew Valley.
|
||||||
|
|
||||||
|
## Что внутри
|
||||||
|
|
||||||
|
- `lab1/report/REPORT.pdf` — **главный отчёт** (21 страница).
|
||||||
|
- `lab1/report/REPORT.docx` — редактируемая версия отчёта.
|
||||||
|
- `lab1/report/REPORT.md` — исходник отчёта.
|
||||||
|
- `lab1/presentation/stardew_stylometry.pdf` — презентация для защиты.
|
||||||
|
- `lab1/presentation/stardew_stylometry.pptx` — редактируемая презентация.
|
||||||
|
- `lab1/presentation/latex/` — LaTeX-исходник презентации.
|
||||||
|
- `lab1/stylometry/` — код: чтение корпуса, признаки, LSA, модели,
|
||||||
|
кросс-валидация, метрики, верификация.
|
||||||
|
- `lab1/data/` — корпус и метаданные.
|
||||||
|
- `lab1/tests/` — 33 теста.
|
||||||
|
- `lab1/FINAL_RESULTS.csv` — сводная таблица результатов.
|
||||||
|
- `lab1/exports/nlp_lab1_final_results_20261026_120000.tar.gz` — компактный
|
||||||
|
архив финальных результатов (без промежуточных матриц).
|
||||||
|
|
||||||
|
## Проверка за три минуты
|
||||||
|
|
||||||
|
Все команды выполняются из каталога `lab1`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd lab1
|
||||||
|
|
||||||
|
python -m venv .venv
|
||||||
|
source .venv/bin/activate # Windows: .venv\Scripts\activate
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
python -m pip install -r requirements.txt
|
||||||
|
|
||||||
|
python scripts/download_nltk_data.py
|
||||||
|
python scripts/check_environment.py
|
||||||
|
python -m pytest -q
|
||||||
|
```
|
||||||
|
|
||||||
|
Ожидаемый результат:
|
||||||
|
|
||||||
|
- `check_environment.py` заканчивается строкой `Окружение готово.`;
|
||||||
|
- `pytest` печатает `33 passed`;
|
||||||
|
- в корпусе 2005 реплик и 34 персонажа.
|
||||||
|
|
||||||
|
Если всё совпало, отчёт и презентацию можно открывать сразу. Для повторного
|
||||||
|
запуска экспериментов смотрите `ARCHIVE_README.md`.
|
||||||
|
|
||||||
|
## Главный результат
|
||||||
|
|
||||||
|
| Что | Значение |
|
||||||
|
|---|---:|
|
||||||
|
| Корпус | 2005 реплик, 34 персонажа |
|
||||||
|
| Основная метрика | macro-F1 |
|
||||||
|
| Мягкое голосование трёх моделей | **0,3036 ± 0,0189** |
|
||||||
|
| Консервативная оценка (вложенный выбор) | **0,2746 ± 0,0233** |
|
||||||
|
| Случайный уровень | **0,029** |
|
||||||
|
|
||||||
|
Основной сигнал — лексико-тематический. После замены содержательных слов
|
||||||
|
нейтральным маркером macro-F1 падает с 0,248 до 0,113. Поэтому в отчёте
|
||||||
|
результат сформулирован как различимость **речевых масок по
|
||||||
|
лексико-тематическому профилю**, а не как доказательство «чистого» стиля.
|
||||||
|
|
||||||
|
## Что читать дальше
|
||||||
|
|
||||||
|
1. `ARCHIVE_README.md` — подробная инструкция: установка, запуск, структура
|
||||||
|
проекта, что делает каждый модуль, как пересобрать отчёт и презентацию.
|
||||||
|
2. `lab1/RUNBOOK.md` — пошаговый запуск полной серии экспериментов.
|
||||||
|
3. `lab1/README.md` — краткое описание проекта и команд.
|
||||||
|
4. `lab1/FINAL_REPORT.md` — расширенная версия отчёта с деталями.
|
||||||
|
5. `lab1/EXPERIMENTS.md` — протокол экспериментов.
|
||||||
|
6. `lab1/FEATURES.md` — формулы и обоснование признаков.
|
||||||
|
7. `lab1/METRICS_AND_CLASSIFIERS.md` — метрики и классификаторы.
|
||||||
|
|
||||||
|
## Важно
|
||||||
|
|
||||||
|
- Ресурсы NLTK (`nltk_data`, около 80 МБ) в архив не включены: они
|
||||||
|
скачиваются командой `python scripts/download_nltk_data.py`.
|
||||||
|
- Виртуальное окружение, промежуточные матрицы признаков и сырые результаты
|
||||||
|
серий в архив не включены: они восстанавливаются скриптами.
|
||||||
|
- Диалоги Stardew Valley принадлежат ConcernedApe; корпус используется
|
||||||
|
исключительно в учебных и исследовательских целях.
|
||||||
BIN
lab1/dist/nlp_lab1_submission_20261101.zip
vendored
Normal file
1
lab1/dist/nlp_lab1_submission_20261101.zip.sha256
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
635ed9d56653259be30733fe8b80316e8ce1f3b32ac178170f5deef0f3e39d79 nlp_lab1_submission_20261101.zip
|
||||||
@@ -1,15 +1,15 @@
|
|||||||
# Презентация
|
# Презентация
|
||||||
|
|
||||||
Готовая презентация: `stardew_stylometry.pptx` (16:9, 32 слайда).
|
Готовая презентация: `stardew_stylometry.pptx` (16:9, 30 слайдов в финальной версии; исходная автоматическая сборка — 32 слайда).
|
||||||
PDF-версия для просмотра: `stardew_stylometry.pdf`.
|
PDF-версия для просмотра: `stardew_stylometry.pdf`.
|
||||||
|
|
||||||
## Структура
|
## Структура
|
||||||
|
|
||||||
- **Блок 1. Корпус** — 2005 реплик, 34 персонажа, сбор, очистка, разметка,
|
- **Часть 1. Корпус** — 2005 реплик, 34 персонажа, сбор, очистка, разметка,
|
||||||
распределение реплик и длин, ограничения.
|
распределение реплик и длин, ограничения.
|
||||||
- **Блок 2. Методология** — признаки, LSA, логистическая регрессия, SVM,
|
- **Часть 2. Методология** — признаки, LSA, логистическая регрессия, SVM,
|
||||||
деревья, ансамбли, перекрёстная проверка и метрики.
|
деревья, ансамбли, перекрёстная проверка и метрики.
|
||||||
- **Блок 3. Результаты** — сравнение моделей, вклад признаков, контроль
|
- **Часть 3. Результаты** — сравнение моделей, вклад признаков, контроль
|
||||||
содержания, матрицы ошибок, классы-аттракторы, выводы.
|
содержания, матрицы ошибок, классы-аттракторы, выводы.
|
||||||
|
|
||||||
На каждом слайде есть фон `images/background.jpg` и маленькая курица
|
На каждом слайде есть фон `images/background.jpg` и маленькая курица
|
||||||
@@ -58,7 +58,7 @@ python presentation/build_deck.py
|
|||||||
- `latex/stardew_stylometry.pdf` — собранный PDF, 30 страниц;
|
- `latex/stardew_stylometry.pdf` — собранный PDF, 30 страниц;
|
||||||
- сборка: `cd latex && xelatex stardew_stylometry.tex` (два прохода).
|
- сборка: `cd latex && xelatex stardew_stylometry.tex` (два прохода).
|
||||||
|
|
||||||
В LaTeX-версии три блока на трёх докладчиков, фон и курица на каждом слайде,
|
В LaTeX-версии три части на трёх докладчиков, фон и курица на каждом слайде,
|
||||||
титульная заставка, изображение персонажей, две круговые диаграммы, матрицы
|
титульная заставка, изображение персонажей, две круговые диаграммы, матрицы
|
||||||
ошибок, F-меры по персонажам, приложение с полной матрицей и финальный мем.
|
ошибок, F-меры по персонажам, приложение с полной матрицей и финальный мем.
|
||||||
Формул почти нет: одна компактная запись обозначений на слайде постановки
|
Формул почти нет: одна компактная запись обозначений на слайде постановки
|
||||||
|
|||||||
@@ -40,11 +40,11 @@
|
|||||||
% Рисуется в конце каждой рамки поверх содержимого и не меняет вёрстку.
|
% Рисуется в конце каждой рамки поверх содержимого и не меняет вёрстку.
|
||||||
\newcommand{\slidefooter}{%
|
\newcommand{\slidefooter}{%
|
||||||
\begin{tikzpicture}[remember picture, overlay]
|
\begin{tikzpicture}[remember picture, overlay]
|
||||||
\node[anchor=north east, xshift=-0.55cm, yshift=-0.22cm]
|
\node[anchor=north east, xshift=-0.1cm, yshift=-0.1cm]
|
||||||
at (current page.north east)
|
at (current page.north east)
|
||||||
{\colorbox{cream}{\scriptsize\color{inkbrown}%
|
{\colorbox{cream}{\scriptsize\color{inkbrown}%
|
||||||
Слайд \insertframenumber\ из \inserttotalframenumber
|
\insertframenumber\slash\inserttotalframenumber
|
||||||
\quad·\quad осталось: \the\numexpr\inserttotalframenumber-\insertframenumber\relax}};
|
\quad}};
|
||||||
\end{tikzpicture}%
|
\end{tikzpicture}%
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
BIN
lab1/report/REPORT.docx
Normal file
652
lab1/report/REPORT.md
Normal file
@@ -0,0 +1,652 @@
|
|||||||
|
---
|
||||||
|
title: "Отчёт по лабораторной работе №1"
|
||||||
|
subtitle: "Классификация текстов на основе стилометрических характеристик"
|
||||||
|
author: "А. А. Аладьина, В. Н. Копнин, М. В. Савостиков"
|
||||||
|
date: "01.11.2026"
|
||||||
|
lang: ru-RU
|
||||||
|
---
|
||||||
|
|
||||||
|
# Аннотация
|
||||||
|
|
||||||
|
Работа посвящена стилометрической классификации реплик персонажей игры
|
||||||
|
Stardew Valley. Корпус содержит 2005 реплик, принадлежащих 34 персонажам;
|
||||||
|
все реплики написаны одним автором. Проверяется, различимы ли речевые маски
|
||||||
|
персонажей по отдельной реплике, если исключить авторскую атрибуцию как
|
||||||
|
таковую.
|
||||||
|
|
||||||
|
Построены 454 ручных стилометрических признака, символьные и частеречные
|
||||||
|
n-граммы, а также несколько вариантов латентно-семантического анализа.
|
||||||
|
Сравнивались логистическая регрессия, линейный метод опорных векторов,
|
||||||
|
наивный байесовский классификатор, случайный лес, сверхслучайные деревья,
|
||||||
|
метод ближайших соседей и дельта Барроуза. Использована повторная
|
||||||
|
стратифицированная перекрёстная проверка, вложенная настройка параметров и
|
||||||
|
отдельная отложенная выборка.
|
||||||
|
|
||||||
|
Лучший наблюдаемый результат — мягкое голосование трёх моделей: macro-F1
|
||||||
|
около **0,304** при перекрёстной проверке и **0,333** на отложенной выборке.
|
||||||
|
Консервативная оценка при вложенном выборе конфигурации — **0,275**.
|
||||||
|
Случайный уровень для 34 классов — **0,029**. Основной вклад дают
|
||||||
|
лексико-тематические признаки и символьные n-граммы; после замены
|
||||||
|
содержательных слов нейтральным маркером качество падает с 0,248 до 0,113.
|
||||||
|
Следовательно, результат корректно интерпретировать как различимость речевых
|
||||||
|
масок по лексико-тематическому профилю, а не как доказательство «чистого»
|
||||||
|
идиолекта.
|
||||||
|
|
||||||
|
# 1. Постановка задачи
|
||||||
|
|
||||||
|
## 1.1. Направление исследования
|
||||||
|
|
||||||
|
Направление работы — классификация текстов по стилометрическим
|
||||||
|
характеристикам. В отличие от классической авторской атрибуции, все тексты
|
||||||
|
корпуса принадлежат одному автору. Исследуется другой вопрос:
|
||||||
|
|
||||||
|
> различаются ли речевые маски 34 персонажей по стилометрическим признакам
|
||||||
|
> отдельных реплик, если автор у них общий?
|
||||||
|
|
||||||
|
Такая постановка позволяет проверить, насколько автор намеренно создаёт
|
||||||
|
разные голоса персонажей, и какие признаки несут этот сигнал.
|
||||||
|
|
||||||
|
## 1.2. Гипотезы
|
||||||
|
|
||||||
|
| Обозначение | Гипотеза |
|
||||||
|
|---|---|
|
||||||
|
| H1 | Функциональные слова, части речи и символьные n-граммы различают персонажей выше случайного уровня |
|
||||||
|
| H2 | На коротких репликах частеречные и символьные признаки устойчивее словарного богатства и читаемости |
|
||||||
|
| H3 | LSA по всем словам отражает тему, а LSA по частям речи и функциональным словам — стиль |
|
||||||
|
| H4 | Качество зависит от длины реплики |
|
||||||
|
| H5 | Ошибки группируются вокруг персонажей со сходным возрастом, полом, ролью или видом |
|
||||||
|
|
||||||
|
## 1.3. Формальная постановка
|
||||||
|
|
||||||
|
Пусть \(D=\{(x_i,y_i)\}_{i=1}^{n}\) — корпус, где \(n=2005\),
|
||||||
|
\(x_i\) — одна реплика, \(y_i\in Y\) — имя персонажа,
|
||||||
|
\(|Y|=34\). Каждый объект относится ровно к одному классу.
|
||||||
|
|
||||||
|
Требуется построить классификатор
|
||||||
|
|
||||||
|
\[
|
||||||
|
f\colon \Phi(x_i)\to Y,
|
||||||
|
\]
|
||||||
|
|
||||||
|
где \(\Phi(x_i)\) — вектор признаков реплики. Качество оценивается
|
||||||
|
макро-усреднённой F-мерой:
|
||||||
|
|
||||||
|
\[
|
||||||
|
\operatorname{macroF1}=\frac{1}{|Y|}\sum_{c\in Y}
|
||||||
|
\frac{2P_cR_c}{P_c+R_c}.
|
||||||
|
\]
|
||||||
|
|
||||||
|
Макро-усреднение выбрано потому, что все 34 персонажа важны одинаково,
|
||||||
|
а классы неравномерны.
|
||||||
|
|
||||||
|
# 2. Корпус
|
||||||
|
|
||||||
|
## 2.1. Источник и единица анализа
|
||||||
|
|
||||||
|
Корпус собран из диалогов игры Stardew Valley. Единица анализа — одна
|
||||||
|
реплика персонажа; тексты на английском языке. Метка — имя говорящего,
|
||||||
|
определённое по игровым диалогам и проверенное вручную.
|
||||||
|
|
||||||
|
Метаданные о поле, возрасте, виде и роли персонажа хранятся отдельно и
|
||||||
|
используются только для анализа ошибок. В признаки классификатора они не
|
||||||
|
подаются, чтобы не подменять стилометрическую задачу демографической.
|
||||||
|
|
||||||
|
## 2.2. Сбор и разметка
|
||||||
|
|
||||||
|
Правила формирования корпуса:
|
||||||
|
|
||||||
|
1. Из игровых диалогов извлекаются реплики персонажей.
|
||||||
|
2. Каждая реплика становится отдельным объектом.
|
||||||
|
3. Метка — имя говорящего; один объект относится ровно к одному классу.
|
||||||
|
4. Проводится ручная проверка соответствия реплики и метки.
|
||||||
|
5. Метаданные персонажей ведутся отдельной таблицей.
|
||||||
|
6. Служебные метки игрового движка (`<PLAYER>`, `<FARM>`, `<PET>`,
|
||||||
|
`<SPOUSE>`, `<TIME>`, `<NOUN>`, `<NAME>`) заменяются нейтральными словами;
|
||||||
|
их количество сохраняется отдельным признаком.
|
||||||
|
|
||||||
|
Корпус хранится в CSV с разделителем `;` в кодировке cp1251; обязательные
|
||||||
|
колонки — «Реплика» и «Персонаж». Метаданные персонажей ведутся отдельным
|
||||||
|
XLSX-файлом. Такое разделение позволяет менять признаки, не затрагивая
|
||||||
|
разметку, и проверять корпус независимо от моделей.
|
||||||
|
|
||||||
|
## 2.3. Очистка и шум
|
||||||
|
|
||||||
|
Выполнялась минимальная нормализация:
|
||||||
|
|
||||||
|
- Unicode-очистка: разные варианты кавычек, тире, многоточий и пробелов
|
||||||
|
приводятся к единому виду;
|
||||||
|
- замена плейсхолдеров нейтральными словами;
|
||||||
|
- токенизация и разбиение на предложения средствами NLTK;
|
||||||
|
- частеречная разметка Penn Treebank с переводом в Universal POS.
|
||||||
|
|
||||||
|
Не удалялись:
|
||||||
|
|
||||||
|
- пунктуация;
|
||||||
|
- регистр;
|
||||||
|
- стоп-слова;
|
||||||
|
- короткие реплики, оставшиеся после ручной фильтрации.
|
||||||
|
|
||||||
|
Пунктуация, регистр и служебные слова рассматриваются как стилометрический
|
||||||
|
сигнал, а не как шум.
|
||||||
|
|
||||||
|
Шумом считались:
|
||||||
|
|
||||||
|
- точные дубликаты;
|
||||||
|
- технические артефакты;
|
||||||
|
- общие короткие формулы;
|
||||||
|
- плейсхолдеры игрового движка.
|
||||||
|
|
||||||
|
Корпус прошёл ручную фильтрацию: короткие и общие фразы удалены. Точные
|
||||||
|
правила этой фильтрации не сохранились, поэтому исходное распределение
|
||||||
|
восстановить нельзя. Это ограничение зафиксировано в выводах.
|
||||||
|
|
||||||
|
## 2.4. Характеристики корпуса
|
||||||
|
|
||||||
|
| Показатель | Значение |
|
||||||
|
|---|---:|
|
||||||
|
| Реплик | 2005 |
|
||||||
|
| Персонажей | 34 |
|
||||||
|
| Словоупотреблений | 38 207 |
|
||||||
|
| Средняя длина реплики | 19,1 слова |
|
||||||
|
| Медиана длины | 17 слов |
|
||||||
|
| Минимум / максимум | 3 / 78 слов |
|
||||||
|
| Классы | 15–139 реплик |
|
||||||
|
| Точные дубли реплик | 0 |
|
||||||
|
| Почти-дубли (косинусная близость ≥ 0,8) | 13 пар, все внутри одного класса |
|
||||||
|
| Реплик с плейсхолдерами | 172 |
|
||||||
|
|
||||||
|
{width=70%}
|
||||||
|
|
||||||
|
{width=82%}
|
||||||
|
|
||||||
|
{width=92%}
|
||||||
|
|
||||||
|
Классы неравномерны: максимум — Abigail (139 реплик), минимум — Mister Qi
|
||||||
|
(15 реплик). Разница почти десятикратная. Поэтому доля правильных ответов
|
||||||
|
может вводить в заблуждение: модель, всегда выбирающая частый класс, получает
|
||||||
|
заметную accuracy, но низкую macro-F1.
|
||||||
|
|
||||||
|
## 2.5. Соответствие формальным требованиям
|
||||||
|
|
||||||
|
Задание предусматривает не менее 90 текстов объёмом не менее 30 предложений
|
||||||
|
и примерно равные классы. В данном корпусе единица анализа — реплика; она
|
||||||
|
короче формального «текста» и содержит в среднем 19 слов. По согласованию с
|
||||||
|
преподавателем эксперимент выполнен на уровне реплик: это позволяет
|
||||||
|
использовать все 2005 объектов и корректно поставить задачу для коротких
|
||||||
|
игровых реплик. Классы не равны по объёму; для компенсации использованы
|
||||||
|
веса классов и macro-F1.
|
||||||
|
|
||||||
|
Это ограничение важно учитывать при сравнении с работами, где документы
|
||||||
|
содержат десятки предложений.
|
||||||
|
|
||||||
|
# 3. Стилометрические признаки
|
||||||
|
|
||||||
|
## 3.1. Ручные признаки: 454 признака в 11 группах
|
||||||
|
|
||||||
|
| Группа | Примеры |
|
||||||
|
|---|---|
|
||||||
|
| Длина | число слов, символов, предложений; длина предложения |
|
||||||
|
| Лексика | длины слов, апострофы, заглавные буквы, цифры |
|
||||||
|
| Словарное богатство | TTR (доля уникальных слов), Guiraud, Herdan, Yule's K, Simpson's D, доля гапаксов и дислегомен |
|
||||||
|
| Символы | частоты букв, регистр, цифры, пунктуация |
|
||||||
|
| Пунктуация | запятые, точки, многоточия, вопросы, восклицания, повторы |
|
||||||
|
| Функциональные слова | артикли, предлоги, союзы, местоимения, вспомогательные глаголы |
|
||||||
|
| Местоимения | 1-е, 2-е, 3-е лицо; отношение «я/ты» |
|
||||||
|
| Части речи | Universal POS, Penn Treebank, время, модальность, герундий |
|
||||||
|
| Синтаксис | клаузы (элементарные предложения), подчинительные союзы, пассивные конструкции, вопросы |
|
||||||
|
| Читаемость | Flesch, Flesch—Kincaid, Gunning Fog, SMOG, Coleman—Liau, ARI |
|
||||||
|
| Разговорные маркеры | междометия, смех, осторожные формулировки (хеджи), интенсификаторы, вежливость, сокращения |
|
||||||
|
|
||||||
|
Словарное богатство и читаемость помечены как предварительные: на репликах
|
||||||
|
длиной 10–20 слов они нестабильны. Основные группы для коротких текстов —
|
||||||
|
функциональные слова, части речи, пунктуация, символьные n-граммы и
|
||||||
|
разговорные маркеры.
|
||||||
|
|
||||||
|
### Обоснование выбора
|
||||||
|
|
||||||
|
- Функциональные слова — классическое ядро стилометрии: закрытый класс мало
|
||||||
|
зависит от темы.
|
||||||
|
- Части речи и синтаксические прокси отражают структуру фразы.
|
||||||
|
- Символьные n-граммы устойчивы к теме и фиксируют морфологию, пунктуацию и
|
||||||
|
орфографию.
|
||||||
|
- Разговорные маркеры важны для игровых реплик: междометия, многоточия,
|
||||||
|
смех и вежливость различают персонажей.
|
||||||
|
- LSA сжимает разреженное пространство и позволяет сравнить тематический и
|
||||||
|
стилистический вклады.
|
||||||
|
|
||||||
|
## 3.2. Символьные и частеречные n-граммы
|
||||||
|
|
||||||
|
- Символьные n-граммы: TF-IDF по символьным n-граммам в границах слов
|
||||||
|
(`char_wb`), диапазон 2–5, `min_df=3`, до 5000–10 000 признаков,
|
||||||
|
L2-нормализация.
|
||||||
|
- Частеречные n-граммы: TF-IDF по последовательностям Universal POS,
|
||||||
|
диапазон 1–3, до 500 признаков.
|
||||||
|
|
||||||
|
TF-IDF — вес признака, который растёт с частотой внутри реплики и падает с
|
||||||
|
частотой в корпусе: частые общие n-граммы получают меньший вес, редкие
|
||||||
|
характерные — больший.
|
||||||
|
|
||||||
|
Символьные n-граммы фиксируют морфологию и оформление; n-граммы частей речи —
|
||||||
|
синтаксические шаблоны. Оба блока устойчивее к теме, чем словарные n-граммы.
|
||||||
|
|
||||||
|
## 3.3. Латентно-семантический анализ
|
||||||
|
|
||||||
|
LSA реализован как:
|
||||||
|
|
||||||
|
```text
|
||||||
|
TF-IDF -> усечённое сингулярное разложение (TruncatedSVD) -> L2-нормализация
|
||||||
|
```
|
||||||
|
|
||||||
|
Варианты:
|
||||||
|
|
||||||
|
| Вариант | Что отражает |
|
||||||
|
|---|---|
|
||||||
|
| `word` | лексику и тему |
|
||||||
|
| `word2` | лексику и устойчивые сочетания |
|
||||||
|
| `content` | содержание после удаления стоп-слов |
|
||||||
|
| `fw` | функциональные слова и синтаксис |
|
||||||
|
| `char` | морфологию и пунктуацию |
|
||||||
|
| `pos` | частеречные последовательности |
|
||||||
|
|
||||||
|
Число компонент подбиралось в диапазоне 50–400 по объяснённой дисперсии и
|
||||||
|
внешней метрике. Для `content`-LSA качество росло до 400 компонент.
|
||||||
|
Ключевое ограничение: LSA по содержательным словам может отражать тему, а не
|
||||||
|
только стиль. Поэтому варианты сравнивались между собой, а не выбирались
|
||||||
|
заранее.
|
||||||
|
|
||||||
|
# 4. Классификаторы
|
||||||
|
|
||||||
|
В работе использовано шесть семейств ML-классификаторов и классическая дельта
|
||||||
|
Барроуза — всего семь подходов; требование «не менее трёх» выполнено с
|
||||||
|
запасом.
|
||||||
|
|
||||||
|
## 4.1. Логистическая регрессия
|
||||||
|
|
||||||
|
Линейная модель: для каждого класса вычисляется оценка
|
||||||
|
\(s_c(x)=w_c^\top x+b_c\), затем вероятности через softmax. Обучение
|
||||||
|
минимизирует перекрёстную энтропию с L2-регуляризацией. Параметр \(C\)
|
||||||
|
управляет силой регуляризации; `class_weight="balanced"` повышает вес редких
|
||||||
|
классов.
|
||||||
|
|
||||||
|
Сильные стороны: устойчивость при большом числе признаков, вероятности,
|
||||||
|
интерпретируемые коэффициенты. Ограничение: только линейные границы.
|
||||||
|
|
||||||
|
## 4.2. Линейный метод опорных векторов
|
||||||
|
|
||||||
|
Ищет линейную границу с максимальным зазором; параметр \(C\) штрафует
|
||||||
|
нарушения зазора. Не выдаёт вероятностей: для тройки лучших используется
|
||||||
|
решающая функция. Силён на текстовых признаках, но требует настройки \(C\).
|
||||||
|
|
||||||
|
## 4.3. Случайный лес
|
||||||
|
|
||||||
|
Ансамбль решающих деревьев: каждое дерево обучается на подвыборке,
|
||||||
|
разбиения используют случайное подмножество признаков. Класс определяется
|
||||||
|
голосованием. Лес ловит нелинейные взаимодействия, но склонен к
|
||||||
|
переобучению на 2005 объектах; ограничивались глубина и размер листьев.
|
||||||
|
|
||||||
|
## 4.4. Сверхслучайные деревья (ExtraTrees)
|
||||||
|
|
||||||
|
Похожи на случайный лес, но пороги разбиения выбираются случайно. Это
|
||||||
|
уменьшает дисперсию и ускоряет обучение. В работе ExtraTrees с расширенным
|
||||||
|
символьным словарём показали лучший результат среди одиночных моделей.
|
||||||
|
|
||||||
|
## 4.5. Наивный байесовский классификатор
|
||||||
|
|
||||||
|
MultinomialNB использует теорему Байеса и предположение об условной
|
||||||
|
независимости признаков; сглаживание Лапласа задаётся параметром \(\alpha\).
|
||||||
|
Быстрый baseline для частотных признаков; требует неотрицательных значений,
|
||||||
|
поэтому LSA-компоненты для него исключались.
|
||||||
|
|
||||||
|
## 4.6. Метод ближайших соседей
|
||||||
|
|
||||||
|
Класс определяется по \(k\) ближайшим объектам; использовалось косинусное
|
||||||
|
расстояние и веса, обратные расстоянию. Простая локальная модель; чувствительна
|
||||||
|
к размерности и масштабу признаков.
|
||||||
|
|
||||||
|
## 4.7. Дельта Барроуза
|
||||||
|
|
||||||
|
Классический стилометрический метод: выбираются наиболее частотные слова,
|
||||||
|
частоты z-нормируются по обучающей выборке, документ относится к классу с
|
||||||
|
минимальным расстоянием до центроида. Использовались манхэттенское и
|
||||||
|
косинусное расстояния. Дельта — независимая проверка, не заменяющая
|
||||||
|
ML-классификаторы.
|
||||||
|
|
||||||
|
## 4.8. Ансамбли
|
||||||
|
|
||||||
|
- **Мягкое голосование** усредняет вероятности нескольких моделей.
|
||||||
|
- **Объединение через мета-классификатор (стекинг)** обучает второй уровень
|
||||||
|
на предсказаниях базовых моделей. В наших данных оно переобучилось и
|
||||||
|
оказалось хуже мягкого голосования.
|
||||||
|
|
||||||
|
# 5. Схема верификации
|
||||||
|
|
||||||
|
## 5.1. Перекрёстная проверка
|
||||||
|
|
||||||
|
Основная схема:
|
||||||
|
|
||||||
|
```text
|
||||||
|
RepeatedStratifiedKFold(n_splits=5, n_repeats=3–5)
|
||||||
|
```
|
||||||
|
|
||||||
|
При 5 фолдах модель обучается на 4 фолдах и проверяется на 1; доли классов
|
||||||
|
сохраняются внутри фолдов. Повторность позволяет оценить разброс.
|
||||||
|
|
||||||
|
## 5.2. Вложенная проверка
|
||||||
|
|
||||||
|
Внешние фолды используются для оценки качества, внутренние — для настройки
|
||||||
|
гиперпараметров. Дополнительно выполнен вложенный выбор конфигурации
|
||||||
|
признаков и модели: в каждом внешнем фолде конфигурация выбиралась только по
|
||||||
|
внутренним фолдам.
|
||||||
|
|
||||||
|
## 5.3. Контроль утечек
|
||||||
|
|
||||||
|
- TF-IDF, SVD, масштабирование и отбор признаков обучаются только на
|
||||||
|
обучающих фолдах.
|
||||||
|
- Тестовая выборка не участвует в выборе гиперпараметров или признаков.
|
||||||
|
- Контроль перестановкой меток дал macro-F1 0,0270–0,0282 — случайный
|
||||||
|
уровень.
|
||||||
|
- Все 13 почти-дубликатов находятся внутри одного класса; межклассовых
|
||||||
|
почти-дублей не обнаружено, поэтому прямой утечки метки нет. При более
|
||||||
|
строгой оценке такие пары всё равно следует держать в одном фолде.
|
||||||
|
|
||||||
|
## 5.4. Метрики
|
||||||
|
|
||||||
|
| Метрика | Что показывает |
|
||||||
|
|---|---|
|
||||||
|
| Macro-F1 | Основная метрика: средняя F-мера по 34 классам |
|
||||||
|
| Доля правильных ответов | Доля совпавших ответов; обманчива при дисбалансе |
|
||||||
|
| Сбалансированная доля | Средняя полнота по классам |
|
||||||
|
| Top-3 | Истинный класс попал в тройку лучших |
|
||||||
|
| MCC (коэффициент корреляции Мэтьюза) | Корреляция истинных и предсказанных меток |
|
||||||
|
| Каппа Коэна | Согласие с поправкой на случай |
|
||||||
|
| Log loss / Brier | Качество вероятностей |
|
||||||
|
| Матрица ошибок | Какие персонажи перепутываются |
|
||||||
|
|
||||||
|
Дополнительно использованы бутстрэп-интервалы, критерий Мак-Немара для
|
||||||
|
парных сравнений, критерий Уилкоксона и поправка Холма—Бонферрони.
|
||||||
|
|
||||||
|
# 6. Результаты
|
||||||
|
|
||||||
|
## 6.1. Сравнение моделей
|
||||||
|
|
||||||
|
| Конфигурация | Macro-F1 | Доля правильных ответов | Top-3 |
|
||||||
|
|---|---:|---:|---:|
|
||||||
|
| Мягкое голосование трёх моделей | **0,3036 ± 0,0189** | 0,2808 | 0,4823 |
|
||||||
|
| ExtraTrees, `char_max_features=10000` | 0,2757 ± 0,0219 | 0,2633 | 0,4554 |
|
||||||
|
| Вложенный выбор конфигурации | 0,2746 ± 0,0233 | 0,2603 | — |
|
||||||
|
| Случайный лес, `char_max_features=10000` | 0,2583 ± 0,0066 | 0,2391 | 0,4197 |
|
||||||
|
| ExtraTrees, полный набор | 0,2499 ± 0,0209 | 0,2374 | 0,4234 |
|
||||||
|
| Логистическая регрессия, lean + content-LSA | 0,2466 ± 0,0191 | 0,2349 | 0,4030 |
|
||||||
|
| Логистическая регрессия, `stylometry_pos_lsa` | 0,1785 ± 0,0060 | 0,1679 | 0,3322 |
|
||||||
|
| Только ручные признаки | 0,1051 ± 0,0038 | 0,1012 | 0,2338 |
|
||||||
|
| Случайный уровень | 0,029 | 0,029 | 0,088 |
|
||||||
|
|
||||||
|
На отложенной выборке 80/20 мягкое голосование дало **0,3333**; ExtraTrees
|
||||||
|
на полном наборе — 0,2793; случайный лес с `char_max_features=10000` —
|
||||||
|
0,2554; логистическая регрессия с content-LSA — 0,2459.
|
||||||
|
|
||||||
|
{width=82%}
|
||||||
|
|
||||||
|
## 6.2. Вклад признаков
|
||||||
|
|
||||||
|
| Условие | Macro-F1 |
|
||||||
|
|---|---:|
|
||||||
|
| Только длина | 0,035 |
|
||||||
|
| Только ручные стилометрические признаки | 0,105 |
|
||||||
|
| Ручные + символьные и частеречные n-граммы | 0,182 |
|
||||||
|
| То же + `content`-LSA | 0,249 |
|
||||||
|
| ExtraTrees с расширенным символьным словарём | 0,276 |
|
||||||
|
| Мягкое голосование | 0,304 |
|
||||||
|
|
||||||
|
{width=72%}
|
||||||
|
|
||||||
|
Основной прирост дают символьные n-граммы, лексика и `content`-LSA.
|
||||||
|
Функциональные слова и POS-LSA сами по себе слабы (0,06–0,07): это важный
|
||||||
|
отрицательный результат.
|
||||||
|
|
||||||
|
## 6.3. Стиль или содержание
|
||||||
|
|
||||||
|
Содержательные слова (существительные, глаголы, прилагательные, наречия,
|
||||||
|
числительные) заменялись нейтральным маркером `CONTENT`; служебные слова,
|
||||||
|
пунктуация и частеречные теги сохранялись.
|
||||||
|
|
||||||
|
| Условие | Macro-F1 |
|
||||||
|
|---|---:|
|
||||||
|
| Исходный текст, стилевой набор, ExtraTrees | 0,2483 |
|
||||||
|
| Маскированный текст, стилевой набор, ExtraTrees | 0,1129 |
|
||||||
|
| Исходный текст, стилевой набор, LogReg | 0,1467 |
|
||||||
|
| Маскированный текст, стилевой набор, LogReg | 0,1067 |
|
||||||
|
|
||||||
|
{width=68%}
|
||||||
|
|
||||||
|
Падение более чем вдвое показывает, что значительная часть качества связана
|
||||||
|
с лексическим содержанием. Остаток 0,113 всё ещё в 3,9 раза выше случайного
|
||||||
|
уровня: некоторый стилевой сигнал сохраняется, но он слабее тематического.
|
||||||
|
|
||||||
|
## 6.4. Матрицы ошибок
|
||||||
|
|
||||||
|
Строки матрицы — истинный персонаж, столбцы — предсказание модели, диагональ
|
||||||
|
— правильные ответы, вне диагонали — ошибки. Полная матрица 34×34 на доске
|
||||||
|
нечитаема; поэтому в основном тексте показаны 12 самых частых персонажей и
|
||||||
|
столбец «прочие», а также 18 самых частых пар ошибок.
|
||||||
|
|
||||||
|
{width=92%}
|
||||||
|
|
||||||
|
{width=92%}
|
||||||
|
|
||||||
|
Основные пары ошибок (за три повтора): Sebastian → Abigail (65),
|
||||||
|
Sebastian → Alex (61), Abigail → Sebastian (59), Abigail → Alex (56).
|
||||||
|
Крупнейший клубок — молодые персонажи Abigail, Alex, Sebastian, Haley, Sam.
|
||||||
|
Многие из них писались в одних и тех же обновлениях и сюжетных контекстах,
|
||||||
|
поэтому сходство голосов ожидаемо; это не разные авторы.
|
||||||
|
|
||||||
|
## 6.5. Ошибки по персонажам
|
||||||
|
|
||||||
|
{width=92%}
|
||||||
|
|
||||||
|
Лучшие результаты по F-мере: Willy, Evelyn, Pierre, Pam, Leo, Vincent.
|
||||||
|
Худшие: Kent, Mister Qi, Penny, Abigail, Leah, Jodi. Редкие классы имеют
|
||||||
|
широкие интервалы; у Mister Qi всего 15 реплик.
|
||||||
|
|
||||||
|
Abigail и Alex являются классами-аттракторами: модель ошибочно относит к ним
|
||||||
|
чужие реплики. Abigail получает 10–18% всех предсказаний при истинной доле
|
||||||
|
около 6,9%. Простая коррекция априорных вероятностей классов не улучшила
|
||||||
|
macro-F1.
|
||||||
|
|
||||||
|
{width=88%}
|
||||||
|
|
||||||
|
## 6.6. Длина реплики
|
||||||
|
|
||||||
|
| Длина | Macro-F1 |
|
||||||
|
|---|---:|
|
||||||
|
| 0–5 слов | 0,097 |
|
||||||
|
| 6–10 | 0,244 |
|
||||||
|
| 11–15 | 0,233 |
|
||||||
|
| 16–20 | 0,239 |
|
||||||
|
| 21–30 | 0,295 |
|
||||||
|
| 31+ | 0,271 |
|
||||||
|
|
||||||
|
Зависимость немонотонная: максимум приходится на 21–30 слов, после 30 слов
|
||||||
|
качество снижается. Гипотеза «чем длиннее, тем лучше» не подтверждается.
|
||||||
|
|
||||||
|
## 6.7. Верификация и кластеризация
|
||||||
|
|
||||||
|
- Верификация «свой/чужой»: ROC-AUC 0,630–0,633; EER (равная ошибка
|
||||||
|
ложного принятия и ложного отказа) 0,412–0,415; top-1 0,133–0,141.
|
||||||
|
Выше случайного, но практически слабо.
|
||||||
|
- Кластеризация: ARI (скорректированный индекс Рэнда) = 0 для k = 3/5/8;
|
||||||
|
NMI (нормированная взаимная информация) 0,14–0,53. Устойчивых кластеров,
|
||||||
|
соответствующих персонажам, нет.
|
||||||
|
|
||||||
|
## 6.8. Калибровка и ансамбли
|
||||||
|
|
||||||
|
Коррекция априорных вероятностей вида
|
||||||
|
\(p'_{ic}\propto p_{ic}\pi_c^{-\alpha}\) проверялась по схеме
|
||||||
|
«один повтор — отложенный, остальные — обучающие». Среднее изменение
|
||||||
|
macro-F1 составило **−0,0022**: простой пересчёт приоров не помог.
|
||||||
|
|
||||||
|
Мягкое голосование трёх моделей (ExtraTrees на полном наборе, ExtraTrees с
|
||||||
|
`char_max_features=10000`, логистическая регрессия с lean + content-LSA)
|
||||||
|
дало **0,3036 ± 0,0189**. Объединение через мета-классификатор оказалось хуже
|
||||||
|
(0,2259 ± 0,0163) из-за переобучения на малом числе объектов.
|
||||||
|
|
||||||
|
# 7. Обсуждение
|
||||||
|
|
||||||
|
1. **Персонажи статистически различимы, но сигнал умеренный.** 0,304
|
||||||
|
macro-F1 при 34 классах — примерно в 10 раз выше случайного уровня 0,029.
|
||||||
|
Абсолютное значение ограничено короткими репликами, дисбалансом и общими
|
||||||
|
темами.
|
||||||
|
|
||||||
|
2. **Основной вклад — лексико-тематический.** Символьные n-граммы и
|
||||||
|
`content`-LSA дают основной прирост; функциональные слова и POS-LSA сами
|
||||||
|
по себе слабы. После маскирования содержательных слов качество падает
|
||||||
|
более чем вдвое.
|
||||||
|
|
||||||
|
3. **Наиболее устойчивая одиночная модель — ExtraTrees с
|
||||||
|
`char_max_features=10000`.** Она выбрана в 14 из 15 внешних фолдов
|
||||||
|
вложенной процедуры и даёт 0,2757 ± 0,0219.
|
||||||
|
|
||||||
|
4. **Мягкое голосование — лучший наблюдаемый результат**, но состав
|
||||||
|
ансамбля выбирался по предыдущим сериям. Для строгого вывода нужна
|
||||||
|
внешняя валидация.
|
||||||
|
|
||||||
|
5. **Ошибки — это коллапс редких классов в классы-аттракторы**, а не
|
||||||
|
доказательство сходства характеров. Abigail, Alex и ещё несколько
|
||||||
|
популярных классов забирают чужие реплики.
|
||||||
|
|
||||||
|
6. **Один автор и общие контексты** делают путаницу между персонажами
|
||||||
|
ожидаемой. Это не означает, что автор «не старался»: превышение
|
||||||
|
случайного уровня как раз говорит о намеренно созданных речевых масках.
|
||||||
|
|
||||||
|
# 8. Ограничения
|
||||||
|
|
||||||
|
- Один корпус, одна игра, один автор; внешняя валидация не проводилась.
|
||||||
|
- Единица анализа — реплика, а не текст из 30 предложений; это согласовано,
|
||||||
|
но ограничивает сравнение с документными корпусами.
|
||||||
|
- Классы неравномерны; редкие классы имеют широкие интервалы.
|
||||||
|
- Ручная фильтрация корпуса невоспроизводима.
|
||||||
|
- Нет идентификаторов сцен и диалогов.
|
||||||
|
- Выбор признаков и ансамбля частично опирался на предыдущие серии;
|
||||||
|
вложенная проверка снижает, но не устраняет оптимизм.
|
||||||
|
- Содержательные слова и тема вносят большой вклад; «чистый стиль» не
|
||||||
|
доказан.
|
||||||
|
- Простая коррекция приоров не помогла; калибровка редких классов требует
|
||||||
|
дальнейшей работы.
|
||||||
|
|
||||||
|
# 9. Выводы
|
||||||
|
|
||||||
|
1. Речевые маски 34 персонажей статистически различимы по отдельным
|
||||||
|
репликам: лучшая модель — 0,304 macro-F1, консервативная оценка — 0,275
|
||||||
|
при случайном уровне 0,029.
|
||||||
|
2. Основной сигнал — лексико-тематический: символьные n-граммы и
|
||||||
|
`content`-LSA дают основной прирост; функциональные слова и частеречная
|
||||||
|
LSA сами по себе слабы.
|
||||||
|
3. После замены содержательных слов качество падает с 0,248 до 0,113:
|
||||||
|
содержание доминирует, но стилевой сигнал сохраняется.
|
||||||
|
4. Главная проблема — редкие классы и классы-аттракторы; мягкое голосование
|
||||||
|
помогает, но не решает её полностью.
|
||||||
|
5. Корректная формулировка результата: различимы речевые маски по
|
||||||
|
лексико-тематическому профилю; утверждение о «чистом стиле» требует
|
||||||
|
внешней валидации и тематического контроля.
|
||||||
|
|
||||||
|
# 10. Воспроизводимость
|
||||||
|
|
||||||
|
## 10.1. Структура проекта
|
||||||
|
|
||||||
|
```text
|
||||||
|
lab1/
|
||||||
|
├── stylometry/ # чтение, признаки, LSA, модели, метрики
|
||||||
|
├── scripts/ # проверка окружения, запуск серий, упаковка
|
||||||
|
├── tests/ # pytest
|
||||||
|
├── configs/ # конфигурация
|
||||||
|
├── data/ # корпус и метаданные
|
||||||
|
├── report/ # этот отчёт
|
||||||
|
├── presentation/ # PPTX/PDF и LaTeX-версия
|
||||||
|
├── README.md
|
||||||
|
├── RUNBOOK.md
|
||||||
|
├── EXPERIMENTS.md
|
||||||
|
├── FEATURES.md
|
||||||
|
├── METRICS_AND_CLASSIFIERS.md
|
||||||
|
├── TUNING.md
|
||||||
|
├── FINAL_REPORT.md
|
||||||
|
├── FINAL_RESULTS.csv
|
||||||
|
├── requirements.txt
|
||||||
|
└── requirements-report.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
Краткая и подробная инструкции к архиву (`START_HERE.md`,
|
||||||
|
`ARCHIVE_README.md`) лежат в его корне, рядом с каталогом `lab1`.
|
||||||
|
|
||||||
|
## 10.2. Установка и запуск
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd lab1
|
||||||
|
python -m venv .venv
|
||||||
|
source .venv/bin/activate
|
||||||
|
python -m pip install -r requirements.txt
|
||||||
|
python scripts/download_nltk_data.py
|
||||||
|
|
||||||
|
python scripts/check_environment.py
|
||||||
|
python -m pytest -q
|
||||||
|
|
||||||
|
# аудит и признаки
|
||||||
|
python -m stylometry.cli audit
|
||||||
|
python -m stylometry.cli features
|
||||||
|
|
||||||
|
# основной эксперимент
|
||||||
|
python -m stylometry.cli run \
|
||||||
|
--preset stylometry_pos_lsa --model logreg --repeats 5
|
||||||
|
|
||||||
|
# полная серия
|
||||||
|
bash scripts/run_all_experiments.sh
|
||||||
|
|
||||||
|
# дополнительные эксперименты
|
||||||
|
python scripts/run_final_experiments.py \
|
||||||
|
--stages nested topic calibration ensemble extra \
|
||||||
|
--out results/final
|
||||||
|
|
||||||
|
# пересборка отчёта (pandoc + LibreOffice)
|
||||||
|
python scripts/build_report.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## 10.3. Программное обеспечение
|
||||||
|
|
||||||
|
- Python 3.11+;
|
||||||
|
- NumPy, pandas, SciPy, scikit-learn, matplotlib;
|
||||||
|
- NLTK (`punkt`, `punkt_tab`, `averaged_perceptron_tagger_eng`, `stopwords`);
|
||||||
|
- python-pptx и Pillow — для презентации;
|
||||||
|
- pandoc, LibreOffice, python-docx и PyMuPDF — для сборки отчёта;
|
||||||
|
- XeLaTeX и Beamer — для LaTeX-версии презентации.
|
||||||
|
|
||||||
|
# Приложение A. Полная матрица ошибок
|
||||||
|
|
||||||
|
{width=92%}
|
||||||
|
|
||||||
|
# Приложение B. Сводная таблица результатов
|
||||||
|
|
||||||
|
См. `FINAL_RESULTS.csv` в корне проекта.
|
||||||
|
|
||||||
|
# Приложение C. Состав архива
|
||||||
|
|
||||||
|
В архив включены код, тесты, конфигурации, корпус, метаданные, документация,
|
||||||
|
отчёт, презентация (PPTX и LaTeX), сводные результаты и инструкции запуска.
|
||||||
|
Виртуальное окружение, кэш NLTK и промежуточные матрицы признаков в архив не
|
||||||
|
включены: они восстанавливаются скриптами.
|
||||||
|
|
||||||
|
# Приложение D. Источники и программные средства
|
||||||
|
|
||||||
|
1. Burrows, J. F. «Delta»: a Measure of Stylistic Difference and a Guide to
|
||||||
|
Likely Authorship // Literary and Linguistic Computing. 2002. Vol. 17,
|
||||||
|
no. 3. P. 267–287.
|
||||||
|
2. Eder, M., Rybicki, J., Kestemont, M. Stylometry with R: A Package for
|
||||||
|
Computational Text Analysis // The R Journal. 2016. Vol. 8, no. 1.
|
||||||
|
P. 107–121.
|
||||||
|
3. Mosteller, F., Wallace, D. L. Inference and Disputed Authorship:
|
||||||
|
The Federalist. Reading, MA: Addison-Wesley, 1964.
|
||||||
|
4. Stamatatos, E. A Survey of Modern Authorship Attribution Methods // Journal of the American Society for Information Science and Technology. 2009. Vol. 60, no. 3. P. 538–556.
|
||||||
|
5. Pedregosa, F. et al. Scikit-learn: Machine Learning in Python //
|
||||||
|
Journal of Machine Learning Research. 2011. Vol. 12. P. 2825–2830.
|
||||||
|
6. Bird, S., Klein, E., Loper, E. Natural Language Processing with Python.
|
||||||
|
Sebastopol, CA: O'Reilly Media, 2009.
|
||||||
|
7. ConcernedApe. Stardew Valley. Video game. 2016.
|
||||||
|
|
||||||
BIN
lab1/report/REPORT.pdf
Normal file
BIN
lab1/report/figures/characters.png
Normal file
|
After Width: | Height: | Size: 2.4 MiB |
BIN
lab1/report/figures/confusion_full34.png
Normal file
|
After Width: | Height: | Size: 178 KiB |
BIN
lab1/report/figures/confusion_pairs.png
Normal file
|
After Width: | Height: | Size: 170 KiB |
BIN
lab1/report/figures/confusion_top12.png
Normal file
|
After Width: | Height: | Size: 238 KiB |
BIN
lab1/report/figures/feature_progression.png
Normal file
|
After Width: | Height: | Size: 141 KiB |
BIN
lab1/report/figures/hub_shares.png
Normal file
|
After Width: | Height: | Size: 96 KiB |
BIN
lab1/report/figures/median.png
Normal file
|
After Width: | Height: | Size: 680 KiB |
BIN
lab1/report/figures/model_comparison.png
Normal file
|
After Width: | Height: | Size: 132 KiB |
BIN
lab1/report/figures/per_class_f1.png
Normal file
|
After Width: | Height: | Size: 240 KiB |
BIN
lab1/report/figures/replics-count.png
Normal file
|
After Width: | Height: | Size: 795 KiB |
BIN
lab1/report/figures/topic_control.png
Normal file
|
After Width: | Height: | Size: 104 KiB |
5
lab1/requirements-report.txt
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# Дополнительные зависимости для пересборки отчёта.
|
||||||
|
# Основные зависимости проекта — в requirements.txt.
|
||||||
|
python-docx>=1.1,<2
|
||||||
|
PyMuPDF>=1.24,<2
|
||||||
|
Pillow>=10,<13
|
||||||
570
lab1/scripts/build_report.py
Normal file
@@ -0,0 +1,570 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Сборка отчёта: Markdown -> DOCX -> PDF.
|
||||||
|
|
||||||
|
Скрипт рассчитан на запуск из каталога lab1:
|
||||||
|
|
||||||
|
python scripts/build_report.py
|
||||||
|
|
||||||
|
Требуются pandoc, python-docx и libreoffice. PDF собирается через
|
||||||
|
LibreOffice, чтобы не зависеть от LaTeX-шаблона и корректно передать
|
||||||
|
кириллицу, таблицы и рисунки.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import docx
|
||||||
|
from docx.enum.section import WD_SECTION
|
||||||
|
from docx.enum.table import WD_TABLE_ALIGNMENT
|
||||||
|
from docx.enum.text import WD_ALIGN_PARAGRAPH, WD_BREAK, WD_LINE_SPACING
|
||||||
|
from docx.oxml import OxmlElement
|
||||||
|
from docx.oxml.ns import qn
|
||||||
|
from docx.shared import Cm, Pt, RGBColor
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
REPORT_MD = ROOT / "report" / "REPORT.md"
|
||||||
|
BUILD = ROOT / "report" / "build"
|
||||||
|
DOCX_OUT = ROOT / "report" / "REPORT.docx"
|
||||||
|
PDF_OUT = ROOT / "report" / "REPORT.pdf"
|
||||||
|
|
||||||
|
BODY_FONT = "Times New Roman"
|
||||||
|
HEAD_FONT = "Times New Roman"
|
||||||
|
MONO_FONT = "Liberation Mono"
|
||||||
|
INK = RGBColor(0x1A, 0x1A, 0x1A)
|
||||||
|
MUTED = RGBColor(0x55, 0x55, 0x55)
|
||||||
|
ACCENT = RGBColor(0x5B, 0x3A, 0x1E)
|
||||||
|
|
||||||
|
TABLE_WIDTHS = [
|
||||||
|
(2.8, 13.8),
|
||||||
|
(11.6, 5.0),
|
||||||
|
(4.6, 12.0),
|
||||||
|
(3.4, 13.2),
|
||||||
|
(5.6, 11.0),
|
||||||
|
(7.6, 3.4, 3.4, 2.2),
|
||||||
|
(9.6, 7.0),
|
||||||
|
(10.0, 6.6),
|
||||||
|
(11.6, 5.0),
|
||||||
|
]
|
||||||
|
|
||||||
|
TABLE_CAPTIONS = [
|
||||||
|
"Гипотезы исследования и способ их проверки",
|
||||||
|
"Характеристики корпуса",
|
||||||
|
"Группы ручных стилометрических признаков",
|
||||||
|
"Варианты латентно-семантического анализа",
|
||||||
|
"Метрики качества и их назначение",
|
||||||
|
"Сравнение конфигураций классификаторов",
|
||||||
|
"Вклад групп признаков",
|
||||||
|
"Контроль содержания: исходный и маскированный текст",
|
||||||
|
"Качество классификации по длине реплики",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def run(cmd: list[str], cwd: Path | None = None) -> None:
|
||||||
|
print("+", " ".join(cmd))
|
||||||
|
subprocess.run(cmd, cwd=cwd, check=True)
|
||||||
|
|
||||||
|
|
||||||
|
def set_run_font(run, name: str, size: float | None = None, bold: bool | None = None,
|
||||||
|
italic: bool | None = None, color: RGBColor | None = None) -> None:
|
||||||
|
run.font.name = name
|
||||||
|
run._element.rPr.rFonts.set(qn("w:eastAsia"), name)
|
||||||
|
if size is not None:
|
||||||
|
run.font.size = Pt(size)
|
||||||
|
if bold is not None:
|
||||||
|
run.bold = bold
|
||||||
|
if italic is not None:
|
||||||
|
run.italic = italic
|
||||||
|
if color is not None:
|
||||||
|
run.font.color.rgb = color
|
||||||
|
|
||||||
|
|
||||||
|
def set_style_font(style, name: str, size: float, bold: bool = False,
|
||||||
|
color: RGBColor = INK, italic: bool | None = None) -> None:
|
||||||
|
style.font.name = name
|
||||||
|
style.font.size = Pt(size)
|
||||||
|
style.font.bold = bold
|
||||||
|
style.font.color.rgb = color
|
||||||
|
if italic is not None:
|
||||||
|
style.font.italic = italic
|
||||||
|
rpr = style.element.get_or_add_rPr()
|
||||||
|
rfonts = rpr.find(qn("w:rFonts"))
|
||||||
|
if rfonts is None:
|
||||||
|
rfonts = OxmlElement("w:rFonts")
|
||||||
|
rpr.append(rfonts)
|
||||||
|
for attr in ("w:ascii", "w:hAnsi", "w:eastAsia", "w:cs"):
|
||||||
|
rfonts.set(qn(attr), name)
|
||||||
|
|
||||||
|
|
||||||
|
def set_paragraph_spacing(style, before: float = 0, after: float = 0,
|
||||||
|
line: float = 1.5) -> None:
|
||||||
|
pf = style.paragraph_format
|
||||||
|
pf.space_before = Pt(before)
|
||||||
|
pf.space_after = Pt(after)
|
||||||
|
pf.line_spacing = line
|
||||||
|
|
||||||
|
|
||||||
|
def shade(cell, fill: str) -> None:
|
||||||
|
tc_pr = cell._tc.get_or_add_tcPr()
|
||||||
|
shd = OxmlElement("w:shd")
|
||||||
|
shd.set(qn("w:val"), "clear")
|
||||||
|
shd.set(qn("w:color"), "auto")
|
||||||
|
shd.set(qn("w:fill"), fill)
|
||||||
|
tc_pr.append(shd)
|
||||||
|
|
||||||
|
|
||||||
|
def set_cell_margins(table, top=60, bottom=60, left=90, right=90) -> None:
|
||||||
|
tbl_pr = table._tbl.tblPr
|
||||||
|
mar = OxmlElement("w:tblCellMar")
|
||||||
|
for edge, value in (("top", top), ("left", left), ("bottom", bottom), ("right", right)):
|
||||||
|
el = OxmlElement(f"w:{edge}")
|
||||||
|
el.set(qn("w:w"), str(value))
|
||||||
|
el.set(qn("w:type"), "dxa")
|
||||||
|
mar.append(el)
|
||||||
|
tbl_pr.append(mar)
|
||||||
|
|
||||||
|
|
||||||
|
def set_repeat_header(row) -> None:
|
||||||
|
tr_pr = row._tr.get_or_add_trPr()
|
||||||
|
el = OxmlElement("w:tblHeader")
|
||||||
|
el.set(qn("w:val"), "true")
|
||||||
|
tr_pr.append(el)
|
||||||
|
cant_split = OxmlElement("w:cantSplit")
|
||||||
|
cant_split.set(qn("w:val"), "true")
|
||||||
|
tr_pr.append(cant_split)
|
||||||
|
|
||||||
|
|
||||||
|
def set_table_borders(table, color: str = "B8A88F", size: int = 4) -> None:
|
||||||
|
tbl_pr = table._tbl.tblPr
|
||||||
|
borders = OxmlElement("w:tblBorders")
|
||||||
|
for edge in ("top", "left", "bottom", "right", "insideH", "insideV"):
|
||||||
|
el = OxmlElement(f"w:{edge}")
|
||||||
|
el.set(qn("w:val"), "single")
|
||||||
|
el.set(qn("w:sz"), str(size))
|
||||||
|
el.set(qn("w:space"), "0")
|
||||||
|
el.set(qn("w:color"), color)
|
||||||
|
borders.append(el)
|
||||||
|
tbl_pr.append(borders)
|
||||||
|
|
||||||
|
|
||||||
|
def add_page_field(paragraph, total: bool = False) -> None:
|
||||||
|
run = paragraph.add_run()
|
||||||
|
fld_char_begin = OxmlElement("w:fldChar")
|
||||||
|
fld_char_begin.set(qn("w:fldCharType"), "begin")
|
||||||
|
instr_text = OxmlElement("w:instrText")
|
||||||
|
instr_text.set(qn("xml:space"), "preserve")
|
||||||
|
instr_text.text = "NUMPAGES" if total else "PAGE"
|
||||||
|
fld_char_end = OxmlElement("w:fldChar")
|
||||||
|
fld_char_end.set(qn("w:fldCharType"), "end")
|
||||||
|
run._r.append(fld_char_begin)
|
||||||
|
run._r.append(instr_text)
|
||||||
|
run._r.append(fld_char_end)
|
||||||
|
|
||||||
|
|
||||||
|
def add_bottom_border(paragraph, color: str = "5B3A1E", size: int = 6) -> None:
|
||||||
|
p_pr = paragraph._p.get_or_add_pPr()
|
||||||
|
borders = OxmlElement("w:pBdr")
|
||||||
|
bottom = OxmlElement("w:bottom")
|
||||||
|
bottom.set(qn("w:val"), "single")
|
||||||
|
bottom.set(qn("w:sz"), str(size))
|
||||||
|
bottom.set(qn("w:space"), "4")
|
||||||
|
bottom.set(qn("w:color"), color)
|
||||||
|
borders.append(bottom)
|
||||||
|
p_pr.append(borders)
|
||||||
|
|
||||||
|
|
||||||
|
def add_left_border(paragraph, color: str = "B8A88F", size: int = 18) -> None:
|
||||||
|
p_pr = paragraph._p.get_or_add_pPr()
|
||||||
|
borders = OxmlElement("w:pBdr")
|
||||||
|
left = OxmlElement("w:left")
|
||||||
|
left.set(qn("w:val"), "single")
|
||||||
|
left.set(qn("w:sz"), str(size))
|
||||||
|
left.set(qn("w:space"), "8")
|
||||||
|
left.set(qn("w:color"), color)
|
||||||
|
borders.append(left)
|
||||||
|
p_pr.append(borders)
|
||||||
|
|
||||||
|
|
||||||
|
def add_page_break(paragraph) -> None:
|
||||||
|
run = paragraph.add_run()
|
||||||
|
run.add_break(WD_BREAK.PAGE)
|
||||||
|
|
||||||
|
|
||||||
|
def style_document(doc) -> None:
|
||||||
|
section = doc.sections[0]
|
||||||
|
section.page_width = Cm(21.0)
|
||||||
|
section.page_height = Cm(29.7)
|
||||||
|
section.left_margin = Cm(2.6)
|
||||||
|
section.right_margin = Cm(1.8)
|
||||||
|
section.top_margin = Cm(2.0)
|
||||||
|
section.bottom_margin = Cm(2.0)
|
||||||
|
section.header_distance = Cm(1.0)
|
||||||
|
section.footer_distance = Cm(1.0)
|
||||||
|
|
||||||
|
styles = doc.styles
|
||||||
|
|
||||||
|
normal = styles["Normal"]
|
||||||
|
set_style_font(normal, BODY_FONT, 12)
|
||||||
|
normal.paragraph_format.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
|
||||||
|
normal.paragraph_format.first_line_indent = Cm(1.25)
|
||||||
|
normal.paragraph_format.line_spacing = 1.5
|
||||||
|
normal.paragraph_format.space_before = Pt(0)
|
||||||
|
normal.paragraph_format.space_after = Pt(0)
|
||||||
|
|
||||||
|
for name in ("Body Text", "First Paragraph"):
|
||||||
|
st = styles[name]
|
||||||
|
set_style_font(st, BODY_FONT, 12)
|
||||||
|
st.paragraph_format.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
|
||||||
|
st.paragraph_format.first_line_indent = Cm(1.25)
|
||||||
|
st.paragraph_format.line_spacing = 1.5
|
||||||
|
st.paragraph_format.space_before = Pt(0)
|
||||||
|
st.paragraph_format.space_after = Pt(6)
|
||||||
|
|
||||||
|
styles["First Paragraph"].paragraph_format.space_before = Pt(0)
|
||||||
|
|
||||||
|
title = styles["Title"]
|
||||||
|
set_style_font(title, HEAD_FONT, 24, bold=True, color=ACCENT)
|
||||||
|
title.paragraph_format.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||||
|
title.paragraph_format.space_before = Pt(150)
|
||||||
|
title.paragraph_format.space_after = Pt(12)
|
||||||
|
title.paragraph_format.line_spacing = 1.1
|
||||||
|
|
||||||
|
subtitle = styles["Subtitle"]
|
||||||
|
set_style_font(subtitle, HEAD_FONT, 15, bold=False, color=INK)
|
||||||
|
subtitle.paragraph_format.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||||
|
subtitle.paragraph_format.space_before = Pt(0)
|
||||||
|
subtitle.paragraph_format.space_after = Pt(28)
|
||||||
|
subtitle.paragraph_format.line_spacing = 1.2
|
||||||
|
|
||||||
|
author = styles["Author"]
|
||||||
|
set_style_font(author, BODY_FONT, 13, color=INK)
|
||||||
|
author.paragraph_format.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||||
|
author.paragraph_format.space_before = Pt(0)
|
||||||
|
author.paragraph_format.space_after = Pt(6)
|
||||||
|
|
||||||
|
date = styles["Date"]
|
||||||
|
set_style_font(date, BODY_FONT, 13, color=MUTED)
|
||||||
|
date.paragraph_format.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||||
|
date.paragraph_format.space_before = Pt(0)
|
||||||
|
date.paragraph_format.space_after = Pt(0)
|
||||||
|
|
||||||
|
for name, size, before, after in (
|
||||||
|
("Heading 1", 17, 0, 10),
|
||||||
|
("Heading 2", 14, 14, 6),
|
||||||
|
("Heading 3", 12.5, 12, 4),
|
||||||
|
):
|
||||||
|
st = styles[name]
|
||||||
|
set_style_font(st, HEAD_FONT, size, bold=True, color=INK)
|
||||||
|
st.paragraph_format.alignment = WD_ALIGN_PARAGRAPH.LEFT
|
||||||
|
st.paragraph_format.first_line_indent = Cm(0)
|
||||||
|
st.paragraph_format.space_before = Pt(before)
|
||||||
|
st.paragraph_format.space_after = Pt(after)
|
||||||
|
st.paragraph_format.line_spacing = 1.15
|
||||||
|
st.paragraph_format.keep_with_next = True
|
||||||
|
|
||||||
|
styles["Heading 1"].paragraph_format.page_break_before = False
|
||||||
|
styles["Heading 1"].paragraph_format.space_before = Pt(20)
|
||||||
|
|
||||||
|
block = styles["Block Text"]
|
||||||
|
set_style_font(block, BODY_FONT, 11.5, italic=False)
|
||||||
|
block.paragraph_format.left_indent = Cm(1.0)
|
||||||
|
block.paragraph_format.right_indent = Cm(0.5)
|
||||||
|
block.paragraph_format.first_line_indent = Cm(0)
|
||||||
|
block.paragraph_format.line_spacing = 1.3
|
||||||
|
block.paragraph_format.space_before = Pt(6)
|
||||||
|
block.paragraph_format.space_after = Pt(8)
|
||||||
|
block.paragraph_format.keep_together = True
|
||||||
|
|
||||||
|
source = styles["Source Code"]
|
||||||
|
set_style_font(source, MONO_FONT, 9)
|
||||||
|
source.paragraph_format.left_indent = Cm(0.6)
|
||||||
|
source.paragraph_format.first_line_indent = Cm(0)
|
||||||
|
source.paragraph_format.line_spacing = 1.0
|
||||||
|
source.paragraph_format.space_before = Pt(4)
|
||||||
|
source.paragraph_format.space_after = Pt(4)
|
||||||
|
source.paragraph_format.alignment = WD_ALIGN_PARAGRAPH.LEFT
|
||||||
|
source.paragraph_format.keep_together = True
|
||||||
|
|
||||||
|
try:
|
||||||
|
verbatim = styles["Verbatim Char"]
|
||||||
|
set_style_font(verbatim, MONO_FONT, 9.5)
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
for name in ("Image Caption", "Caption"):
|
||||||
|
st = styles[name]
|
||||||
|
set_style_font(st, BODY_FONT, 10, italic=True, color=MUTED)
|
||||||
|
st.paragraph_format.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||||
|
st.paragraph_format.first_line_indent = Cm(0)
|
||||||
|
st.paragraph_format.line_spacing = 1.15
|
||||||
|
st.paragraph_format.space_before = Pt(4)
|
||||||
|
st.paragraph_format.space_after = Pt(12)
|
||||||
|
|
||||||
|
for name in ("Table Caption",):
|
||||||
|
st = styles[name]
|
||||||
|
set_style_font(st, BODY_FONT, 10, italic=False, color=INK)
|
||||||
|
st.paragraph_format.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||||
|
st.paragraph_format.first_line_indent = Cm(0)
|
||||||
|
st.paragraph_format.line_spacing = 1.15
|
||||||
|
st.paragraph_format.space_before = Pt(4)
|
||||||
|
st.paragraph_format.space_after = Pt(12)
|
||||||
|
|
||||||
|
st.paragraph_format.alignment = WD_ALIGN_PARAGRAPH.LEFT
|
||||||
|
st.paragraph_format.first_line_indent = Cm(0)
|
||||||
|
st.paragraph_format.line_spacing = 1.15
|
||||||
|
st.paragraph_format.space_before = Pt(10)
|
||||||
|
st.paragraph_format.space_after = Pt(4)
|
||||||
|
st.paragraph_format.keep_with_next = True
|
||||||
|
|
||||||
|
|
||||||
|
def add_header_footer(doc) -> None:
|
||||||
|
section = doc.sections[0]
|
||||||
|
|
||||||
|
header = section.header
|
||||||
|
header.is_linked_to_previous = False
|
||||||
|
hp = header.paragraphs[0]
|
||||||
|
hp.alignment = WD_ALIGN_PARAGRAPH.RIGHT
|
||||||
|
hp.paragraph_format.space_after = Pt(0)
|
||||||
|
run = hp.add_run("Стилометрия реплик Stardew Valley · отчёт по лабораторной работе №1")
|
||||||
|
set_run_font(run, BODY_FONT, 8.5, color=MUTED)
|
||||||
|
add_bottom_border(hp, color="D8CBB8", size=4)
|
||||||
|
|
||||||
|
footer = section.footer
|
||||||
|
footer.is_linked_to_previous = False
|
||||||
|
fp = footer.paragraphs[0]
|
||||||
|
fp.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||||
|
fp.paragraph_format.space_before = Pt(0)
|
||||||
|
fp.paragraph_format.space_after = Pt(0)
|
||||||
|
run = fp.add_run("Стр. ")
|
||||||
|
set_run_font(run, BODY_FONT, 9, color=MUTED)
|
||||||
|
add_page_field(fp)
|
||||||
|
for r in fp.runs:
|
||||||
|
set_run_font(r, BODY_FONT, 9, color=MUTED)
|
||||||
|
run = fp.add_run(" из ")
|
||||||
|
set_run_font(run, BODY_FONT, 9, color=MUTED)
|
||||||
|
add_page_field(fp, total=True)
|
||||||
|
for r in fp.runs:
|
||||||
|
set_run_font(r, BODY_FONT, 9, color=MUTED)
|
||||||
|
|
||||||
|
|
||||||
|
def add_title_page_notes(doc) -> None:
|
||||||
|
"""Добавляет подпись направления под титульным блоком и разрыв страницы."""
|
||||||
|
date_para = None
|
||||||
|
for p in doc.paragraphs:
|
||||||
|
if p.style.name == "Date":
|
||||||
|
date_para = p
|
||||||
|
break
|
||||||
|
if date_para is None:
|
||||||
|
return
|
||||||
|
anchor = date_para
|
||||||
|
note = anchor.insert_paragraph_before()
|
||||||
|
note.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||||
|
note.paragraph_format.first_line_indent = Cm(0)
|
||||||
|
note.paragraph_format.space_before = Pt(18)
|
||||||
|
run = note.add_run("Направление: автоматический анализ текстов\n")
|
||||||
|
set_run_font(run, BODY_FONT, 11, color=MUTED)
|
||||||
|
run = note.add_run("Корпус: Stardew Valley · 2005 реплик · 34 персонажа")
|
||||||
|
set_run_font(run, BODY_FONT, 11, color=MUTED)
|
||||||
|
|
||||||
|
spacer = doc.add_paragraph()
|
||||||
|
spacer.paragraph_format.space_after = Pt(0)
|
||||||
|
add_page_break(spacer)
|
||||||
|
date_para._p.addnext(spacer._p)
|
||||||
|
|
||||||
|
|
||||||
|
def number_figures(doc) -> None:
|
||||||
|
n = 0
|
||||||
|
for p in doc.paragraphs:
|
||||||
|
if p.style.name == "Image Caption" and p.text.strip():
|
||||||
|
n += 1
|
||||||
|
p.text = f"Рисунок {n}. {p.text.strip()}"
|
||||||
|
for run in p.runs:
|
||||||
|
set_run_font(run, BODY_FONT, 10, italic=True, color=MUTED)
|
||||||
|
|
||||||
|
|
||||||
|
def add_table_captions(doc) -> None:
|
||||||
|
for idx, table in enumerate(doc.tables):
|
||||||
|
if idx >= len(TABLE_CAPTIONS):
|
||||||
|
break
|
||||||
|
para = doc.add_paragraph(style="Table Caption")
|
||||||
|
table._tbl.addprevious(para._p)
|
||||||
|
run = para.add_run(f"Таблица {idx + 1}. {TABLE_CAPTIONS[idx]}")
|
||||||
|
set_run_font(run, BODY_FONT, 10, bold=True, color=INK)
|
||||||
|
para.paragraph_format.first_line_indent = Cm(0)
|
||||||
|
para.paragraph_format.space_before = Pt(10)
|
||||||
|
para.paragraph_format.space_after = Pt(4)
|
||||||
|
para.paragraph_format.keep_with_next = True
|
||||||
|
|
||||||
|
|
||||||
|
def set_column_widths(table, widths_cm: tuple[float, ...]) -> None:
|
||||||
|
table.autofit = False
|
||||||
|
tbl_pr = table._tbl.tblPr
|
||||||
|
layout = OxmlElement("w:tblLayout")
|
||||||
|
layout.set(qn("w:type"), "fixed")
|
||||||
|
tbl_pr.append(layout)
|
||||||
|
grid = table._tbl.tblGrid
|
||||||
|
for idx, width in enumerate(widths_cm):
|
||||||
|
if idx < len(grid.gridCol_lst):
|
||||||
|
grid.gridCol_lst[idx].set(qn("w:w"), str(int(Cm(width).twips)))
|
||||||
|
for row in table.rows:
|
||||||
|
for idx, width in enumerate(widths_cm):
|
||||||
|
if idx < len(row.cells):
|
||||||
|
row.cells[idx].width = Cm(width)
|
||||||
|
|
||||||
|
|
||||||
|
def style_tables(doc) -> None:
|
||||||
|
for t_idx, table in enumerate(doc.tables):
|
||||||
|
table.alignment = WD_TABLE_ALIGNMENT.CENTER
|
||||||
|
table.autofit = True
|
||||||
|
set_table_borders(table)
|
||||||
|
set_cell_margins(table)
|
||||||
|
for r_idx, row in enumerate(table.rows):
|
||||||
|
if r_idx == 0:
|
||||||
|
set_repeat_header(row)
|
||||||
|
else:
|
||||||
|
tr_pr = row._tr.get_or_add_trPr()
|
||||||
|
cant_split = OxmlElement("w:cantSplit")
|
||||||
|
cant_split.set(qn("w:val"), "true")
|
||||||
|
tr_pr.append(cant_split)
|
||||||
|
for cell in row.cells:
|
||||||
|
for p in cell.paragraphs:
|
||||||
|
p.paragraph_format.first_line_indent = Cm(0)
|
||||||
|
p.paragraph_format.space_before = Pt(1)
|
||||||
|
p.paragraph_format.space_after = Pt(1)
|
||||||
|
p.paragraph_format.line_spacing = 1.0
|
||||||
|
if r_idx < len(table.rows) - 1:
|
||||||
|
p.paragraph_format.keep_with_next = True
|
||||||
|
for run in p.runs:
|
||||||
|
set_run_font(
|
||||||
|
run, BODY_FONT, 9.5 if len(table.columns) >= 5 else 10,
|
||||||
|
bold=(r_idx == 0),
|
||||||
|
)
|
||||||
|
if r_idx == 0:
|
||||||
|
shade(cell, "F1E8D8")
|
||||||
|
# Ширина таблицы по ширине полей.
|
||||||
|
tbl_pr = table._tbl.tblPr
|
||||||
|
tbl_w = OxmlElement("w:tblW")
|
||||||
|
tbl_w.set(qn("w:type"), "pct")
|
||||||
|
tbl_w.set(qn("w:w"), "100%")
|
||||||
|
tbl_pr.append(tbl_w)
|
||||||
|
if t_idx < len(TABLE_WIDTHS) and len(TABLE_WIDTHS[t_idx]) == len(table.columns):
|
||||||
|
set_column_widths(table, TABLE_WIDTHS[t_idx])
|
||||||
|
# Небольшой отступ между таблицей и следующим абзацем.
|
||||||
|
spacer = doc.add_paragraph()
|
||||||
|
spacer.paragraph_format.space_before = Pt(0)
|
||||||
|
spacer.paragraph_format.space_after = Pt(0)
|
||||||
|
spacer.paragraph_format.line_spacing = 1.0
|
||||||
|
sr = spacer.add_run("")
|
||||||
|
sr.font.size = Pt(4)
|
||||||
|
table._tbl.addnext(spacer._p)
|
||||||
|
|
||||||
|
|
||||||
|
def style_blockquotes(doc) -> None:
|
||||||
|
for p in doc.paragraphs:
|
||||||
|
if p.style.name == "Block Text":
|
||||||
|
add_left_border(p)
|
||||||
|
|
||||||
|
|
||||||
|
def add_math_font_note(doc) -> None:
|
||||||
|
"""OMML-формулы наследуют Cambria Math; явно задаём её для совместимости."""
|
||||||
|
for r in doc.element.body.iter(qn("m:r")):
|
||||||
|
rpr = r.find(qn("w:rPr"))
|
||||||
|
if rpr is None:
|
||||||
|
rpr = OxmlElement("w:rPr")
|
||||||
|
r.insert(0, rpr)
|
||||||
|
rfonts = rpr.find(qn("w:rFonts"))
|
||||||
|
if rfonts is None:
|
||||||
|
rfonts = OxmlElement("w:rFonts")
|
||||||
|
rpr.append(rfonts)
|
||||||
|
rfonts.set(qn("w:ascii"), "Cambria Math")
|
||||||
|
rfonts.set(qn("w:hAnsi"), "Cambria Math")
|
||||||
|
|
||||||
|
|
||||||
|
def build_docx() -> None:
|
||||||
|
BUILD.mkdir(parents=True, exist_ok=True)
|
||||||
|
raw = BUILD / "REPORT_raw.docx"
|
||||||
|
run([
|
||||||
|
"pandoc", str(REPORT_MD),
|
||||||
|
"-f", "markdown+tex_math_dollars+tex_math_single_backslash+raw_tex",
|
||||||
|
"-t", "docx",
|
||||||
|
"-o", str(raw),
|
||||||
|
f"--resource-path={REPORT_MD.parent}",
|
||||||
|
])
|
||||||
|
doc = docx.Document(raw)
|
||||||
|
style_document(doc)
|
||||||
|
section = doc.sections[0]
|
||||||
|
section.different_first_page_header_footer = True
|
||||||
|
add_header_footer(doc)
|
||||||
|
# Титульный лист без колонтитулов.
|
||||||
|
for part in (section.first_page_header, section.first_page_footer):
|
||||||
|
part.is_linked_to_previous = False
|
||||||
|
for paragraph in part.paragraphs:
|
||||||
|
paragraph.text = ""
|
||||||
|
add_title_page_notes(doc)
|
||||||
|
number_figures(doc)
|
||||||
|
add_table_captions(doc)
|
||||||
|
style_tables(doc)
|
||||||
|
style_blockquotes(doc)
|
||||||
|
add_math_font_note(doc)
|
||||||
|
doc.core_properties.title = "Отчёт по лабораторной работе №1"
|
||||||
|
doc.core_properties.subject = "Классификация текстов на основе стилометрических характеристик"
|
||||||
|
doc.core_properties.author = "А. А. Аладьина, В. Н. Копнин, М. В. Савостиков"
|
||||||
|
doc.core_properties.language = "ru-RU"
|
||||||
|
doc.save(DOCX_OUT)
|
||||||
|
print(f"DOCX: {DOCX_OUT}")
|
||||||
|
|
||||||
|
|
||||||
|
def build_pdf() -> None:
|
||||||
|
if shutil.which("libreoffice") is None:
|
||||||
|
raise SystemExit("libreoffice не найден: PDF не собран")
|
||||||
|
run([
|
||||||
|
"libreoffice", "--headless", "--convert-to", "pdf",
|
||||||
|
"--outdir", str(PDF_OUT.parent), str(DOCX_OUT),
|
||||||
|
])
|
||||||
|
produced = PDF_OUT.parent / (DOCX_OUT.stem + ".pdf")
|
||||||
|
if produced != PDF_OUT and produced.exists():
|
||||||
|
produced.replace(PDF_OUT)
|
||||||
|
print(f"PDF: {PDF_OUT}")
|
||||||
|
|
||||||
|
|
||||||
|
def check_pdf() -> None:
|
||||||
|
try:
|
||||||
|
import fitz # PyMuPDF
|
||||||
|
except ImportError:
|
||||||
|
print("PyMuPDF не установлен: пропускаю проверку PDF")
|
||||||
|
return
|
||||||
|
doc = fitz.open(PDF_OUT)
|
||||||
|
print(f"PDF страниц: {doc.page_count}")
|
||||||
|
text = "\n".join(page.get_text() for page in doc)
|
||||||
|
required = [
|
||||||
|
"Аннотация", "Постановка задачи", "Корпус", "Стилометрические признаки",
|
||||||
|
"Классификаторы", "Схема верификации", "Результаты", "Обсуждение",
|
||||||
|
"Ограничения", "Выводы", "Воспроизводимость",
|
||||||
|
]
|
||||||
|
missing = [item for item in required if item.lower() not in text.lower()]
|
||||||
|
if missing:
|
||||||
|
print("Не найдены разделы:", ", ".join(missing))
|
||||||
|
raise SystemExit(1)
|
||||||
|
print("Все основные разделы найдены в PDF")
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument("--skip-pdf", action="store_true", help="только DOCX")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if not REPORT_MD.exists():
|
||||||
|
raise SystemExit(f"нет файла {REPORT_MD}")
|
||||||
|
build_docx()
|
||||||
|
if not args.skip_pdf:
|
||||||
|
build_pdf()
|
||||||
|
check_pdf()
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
208
lab1/scripts/package_submission.py
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Собирает итоговый ZIP-архив лабораторной работы.
|
||||||
|
|
||||||
|
Запуск из каталога lab1:
|
||||||
|
|
||||||
|
python scripts/package_submission.py
|
||||||
|
|
||||||
|
Скрипт копирует только нужные файлы во временный каталог, формирует
|
||||||
|
MANIFEST.txt и CHECKSUMS.sha256, затем упаковывает всё в
|
||||||
|
dist/nlp_lab1_submission_YYYYMMDD.zip. Рядом кладётся .sha256 самого архива.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import hashlib
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import zipfile
|
||||||
|
from datetime import date
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
DIST = ROOT / "dist"
|
||||||
|
DEFAULT_NAME = f"nlp_lab1_submission_{date.today():%Y%m%d}"
|
||||||
|
|
||||||
|
EXCLUDE_PREFIXES = (
|
||||||
|
"data/processed/",
|
||||||
|
"features/",
|
||||||
|
"results/",
|
||||||
|
"nltk_data/",
|
||||||
|
".venv/",
|
||||||
|
".git/",
|
||||||
|
"presentation/preview/",
|
||||||
|
"report/build/",
|
||||||
|
".pytest_cache/",
|
||||||
|
"dist/",
|
||||||
|
"exports/nlp_lab1_results_",
|
||||||
|
"exports/nlp_lab1_final_results_20261026_120000.tar.gz.sha256",
|
||||||
|
)
|
||||||
|
|
||||||
|
EXCLUDE_NAMES = {
|
||||||
|
"stardew_stylometry.bak",
|
||||||
|
"START_HERE.md",
|
||||||
|
"ARCHIVE_README.md",
|
||||||
|
}
|
||||||
|
|
||||||
|
LATEX_AUX_SUFFIXES = {".aux", ".log", ".nav", ".out", ".snm", ".toc"}
|
||||||
|
|
||||||
|
KEY_FILES = [
|
||||||
|
"lab1/report/REPORT.pdf",
|
||||||
|
"lab1/report/REPORT.docx",
|
||||||
|
"lab1/report/REPORT.md",
|
||||||
|
"lab1/presentation/stardew_stylometry.pdf",
|
||||||
|
"lab1/presentation/stardew_stylometry.pptx",
|
||||||
|
"lab1/presentation/latex/stardew_stylometry.pdf",
|
||||||
|
"lab1/presentation/latex/stardew_stylometry.tex",
|
||||||
|
"lab1/FINAL_RESULTS.csv",
|
||||||
|
"lab1/data/dialogues.csv",
|
||||||
|
"lab1/exports/nlp_lab1_final_results_20261026_120000.tar.gz",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def sha256(path: Path) -> str:
|
||||||
|
h = hashlib.sha256()
|
||||||
|
with path.open("rb") as fh:
|
||||||
|
for chunk in iter(lambda: fh.read(1024 * 1024), b""):
|
||||||
|
h.update(chunk)
|
||||||
|
return h.hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def should_copy(rel: Path) -> bool:
|
||||||
|
posix = rel.as_posix()
|
||||||
|
if rel.name in EXCLUDE_NAMES:
|
||||||
|
return False
|
||||||
|
if "__pycache__" in rel.parts or rel.suffix == ".pyc":
|
||||||
|
return False
|
||||||
|
if rel.suffix in LATEX_AUX_SUFFIXES:
|
||||||
|
return False
|
||||||
|
if any(posix == prefix.rstrip("/") or posix.startswith(prefix)
|
||||||
|
for prefix in EXCLUDE_PREFIXES):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def copy_tree(stage: Path) -> int:
|
||||||
|
target_root = stage / "lab1"
|
||||||
|
count = 0
|
||||||
|
for path in sorted(ROOT.rglob("*")):
|
||||||
|
if not path.is_file():
|
||||||
|
continue
|
||||||
|
rel = path.relative_to(ROOT)
|
||||||
|
if not should_copy(rel):
|
||||||
|
continue
|
||||||
|
dst = target_root / rel
|
||||||
|
dst.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
shutil.copy2(path, dst)
|
||||||
|
count += 1
|
||||||
|
return count
|
||||||
|
|
||||||
|
|
||||||
|
def write_manifest(stage: Path) -> int:
|
||||||
|
files = sorted(p for p in stage.rglob("*") if p.is_file())
|
||||||
|
lines = [
|
||||||
|
"Архив: " + stage.name,
|
||||||
|
f"Файлов в манифесте: {len(files)} (сам MANIFEST.txt не включён)",
|
||||||
|
"",
|
||||||
|
f"{'SHA-256':64} {'Размер':>12} Путь",
|
||||||
|
"-" * 100,
|
||||||
|
]
|
||||||
|
total = 0
|
||||||
|
for path in files:
|
||||||
|
size = path.stat().st_size
|
||||||
|
total += size
|
||||||
|
rel = path.relative_to(stage).as_posix()
|
||||||
|
lines.append(f"{sha256(path)} {size:>12} {rel}")
|
||||||
|
lines.append("-" * 100)
|
||||||
|
lines.append(f"Итого байт: {total}")
|
||||||
|
(stage / "MANIFEST.txt").write_text("\n".join(lines) + "\n", encoding="utf-8")
|
||||||
|
return len(files)
|
||||||
|
|
||||||
|
|
||||||
|
def write_checksums(stage: Path) -> None:
|
||||||
|
lines = []
|
||||||
|
for rel_name in KEY_FILES:
|
||||||
|
path = stage / rel_name
|
||||||
|
if path.exists():
|
||||||
|
lines.append(f"{sha256(path)} {rel_name}")
|
||||||
|
else:
|
||||||
|
print(f"ВНИМАНИЕ: нет ключевого файла {rel_name}")
|
||||||
|
(stage / "CHECKSUMS.sha256").write_text("\n".join(lines) + "\n", encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def zip_stage(stage: Path, archive: Path) -> None:
|
||||||
|
archive.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
with zipfile.ZipFile(archive, "w", compression=zipfile.ZIP_DEFLATED,
|
||||||
|
compresslevel=6) as zf:
|
||||||
|
for path in sorted(stage.rglob("*")):
|
||||||
|
if not path.is_file():
|
||||||
|
continue
|
||||||
|
rel = path.relative_to(stage).as_posix()
|
||||||
|
arcname = f"{stage.name}/{rel}"
|
||||||
|
info = zipfile.ZipInfo.from_file(path, arcname)
|
||||||
|
info.compress_type = zipfile.ZIP_DEFLATED
|
||||||
|
with path.open("rb") as src, zf.open(info, "w") as dst:
|
||||||
|
shutil.copyfileobj(src, dst)
|
||||||
|
|
||||||
|
|
||||||
|
def human(n: int) -> str:
|
||||||
|
for unit in ("Б", "КБ", "МБ", "ГБ"):
|
||||||
|
if n < 1024 or unit == "ГБ":
|
||||||
|
return f"{n:.1f} {unit}" if unit != "Б" else f"{n} {unit}"
|
||||||
|
n /= 1024
|
||||||
|
return f"{n} Б"
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument("--name", default=DEFAULT_NAME, help="имя каталога и ZIP")
|
||||||
|
parser.add_argument("--keep-stage", action="store_true",
|
||||||
|
help="не удалять промежуточный каталог dist/stage")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
name = args.name
|
||||||
|
stage = DIST / "stage" / name
|
||||||
|
if stage.exists():
|
||||||
|
shutil.rmtree(stage)
|
||||||
|
stage.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
print(f"1/4. Копирование файлов в {stage} ...")
|
||||||
|
copied = copy_tree(stage)
|
||||||
|
shutil.copy2(ROOT / "START_HERE.md", stage / "START_HERE.md")
|
||||||
|
shutil.copy2(ROOT / "ARCHIVE_README.md", stage / "ARCHIVE_README.md")
|
||||||
|
|
||||||
|
print("2/4. Формирование CHECKSUMS.sha256 ...")
|
||||||
|
write_checksums(stage)
|
||||||
|
|
||||||
|
print("3/4. Формирование MANIFEST.txt ...")
|
||||||
|
total = write_manifest(stage)
|
||||||
|
|
||||||
|
archive = DIST / f"{name}.zip"
|
||||||
|
print(f"4/4. Упаковка {archive} ...")
|
||||||
|
zip_stage(stage, archive)
|
||||||
|
|
||||||
|
digest = sha256(archive)
|
||||||
|
(DIST / f"{name}.zip.sha256").write_text(
|
||||||
|
f"{digest} {archive.name}\n", encoding="utf-8"
|
||||||
|
)
|
||||||
|
|
||||||
|
print()
|
||||||
|
print(f"Файлов в lab1: {copied}")
|
||||||
|
print(f"Всего файлов в архиве: {total + 1}")
|
||||||
|
print(f"Размер ZIP: {human(archive.stat().st_size)}")
|
||||||
|
print(f"SHA-256 ZIP: {digest}")
|
||||||
|
print(f"Архив: {archive}")
|
||||||
|
print(f"Контрольная сумма: {archive}.sha256")
|
||||||
|
|
||||||
|
if not args.keep_stage:
|
||||||
|
shutil.rmtree(stage, ignore_errors=True)
|
||||||
|
try:
|
||||||
|
(DIST / "stage").rmdir()
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||