mirror of
https://github.com/pese-git/llm-arch-research.git
synced 2026-01-23 21:10:54 +00:00
feat: initial project setup with LLM architecture and HF integration
- Add LLM library with GPT model implementation - Add hf-proxy for HuggingFace integration - Add experiments for training and generation - Add comprehensive documentation and examples - Configure uv workspace with proper dependencies
This commit is contained in:
43
.gitignore
vendored
Normal file
43
.gitignore
vendored
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
*.so
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sitemedia/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
|
||||||
|
# Virtual environments
|
||||||
|
.venv
|
||||||
|
venv/
|
||||||
|
env/
|
||||||
|
ENV/
|
||||||
|
|
||||||
|
# Project specific
|
||||||
|
checkpoints/
|
||||||
|
logs/
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
__pycache__
|
||||||
317
README.md
Normal file
317
README.md
Normal file
@@ -0,0 +1,317 @@
|
|||||||
|
# LLM Architecture Research
|
||||||
|
|
||||||
|
Исследовательский проект для разработки и обучения архитектур больших языковых моделей (LLM).
|
||||||
|
|
||||||
|
## 🏗️ Архитектура проекта
|
||||||
|
|
||||||
|
Проект организован как монорепозиторий с использованием **uv** workspace:
|
||||||
|
|
||||||
|
- **`llm`** — основная библиотека с реализацией архитектур LLM
|
||||||
|
- **`hf-proxy`** — адаптер для интеграции с HuggingFace
|
||||||
|
- **`experiments`** — скрипты обучения и экспериментов
|
||||||
|
- **`notebooks`** — исследовательские ноутбуки
|
||||||
|
|
||||||
|
## 📁 Структура проекта
|
||||||
|
|
||||||
|
```
|
||||||
|
llm-arch-research/
|
||||||
|
│
|
||||||
|
├── pyproject.toml # корневой workspace конфиг
|
||||||
|
├── uv.lock
|
||||||
|
│
|
||||||
|
├── llm/ # основная библиотека архитектур
|
||||||
|
│ ├── pyproject.toml
|
||||||
|
│ └── src/llm/
|
||||||
|
│ ├── core/ # базовые компоненты
|
||||||
|
│ │ ├── base_model.py
|
||||||
|
│ │ ├── decoder.py
|
||||||
|
│ │ ├── multi_head_attention.py
|
||||||
|
│ │ ├── head_attention.py
|
||||||
|
│ │ ├── feed_forward.py
|
||||||
|
│ │ ├── token_embeddings.py
|
||||||
|
│ │ └── positional_embeddings.py
|
||||||
|
│ ├── models/gpt/ # GPT реализация
|
||||||
|
│ │ ├── gpt.py
|
||||||
|
│ │ └── __init__.py
|
||||||
|
│ ├── training/ # утилиты обучения
|
||||||
|
│ │ ├── dataset.py
|
||||||
|
│ │ ├── trainer.py
|
||||||
|
│ │ ├── optimizer.py
|
||||||
|
│ │ └── scheduler.py
|
||||||
|
│ ├── evaluation/ # оценка моделей
|
||||||
|
│ └── tokenizers/ # токенизаторы
|
||||||
|
│ ├── base_tokenizer.py
|
||||||
|
│ └── bpe_tokenizer.py
|
||||||
|
│
|
||||||
|
├── hf-proxy/ # адаптер HuggingFace
|
||||||
|
│ ├── pyproject.toml
|
||||||
|
│ └── src/hf_proxy/
|
||||||
|
│ ├── hf_config.py
|
||||||
|
│ ├── hf_adapter.py
|
||||||
|
│ ├── hf_tokenizer.py
|
||||||
|
│ └── hf_utils.py
|
||||||
|
│
|
||||||
|
├── experiments/ # скрипты обучения и экспериментов
|
||||||
|
│ ├── hf_integration/ # интеграция с HuggingFace
|
||||||
|
│ │ ├── train_with_hf_trainer.py
|
||||||
|
│ │ ├── generate_with_hf_tools.py
|
||||||
|
│ │ ├── simple_hf_training.py
|
||||||
|
│ │ └── test_hf_proxy.py
|
||||||
|
│ ├── llm_only/ # обучение без HF
|
||||||
|
│ │ ├── train_gpt_bpe.py
|
||||||
|
│ │ └── generate_gpt_bpe.py
|
||||||
|
│ └── shared/ # общие утилиты
|
||||||
|
│ ├── configs.py
|
||||||
|
│ └── data.py
|
||||||
|
│
|
||||||
|
├── checkpoints/ # сохраненные модели и токенизаторы
|
||||||
|
└── notebooks/ # исследовательские ноутбуки
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 Быстрый старт
|
||||||
|
|
||||||
|
### Установка зависимостей
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Установка всех зависимостей workspace
|
||||||
|
uv sync
|
||||||
|
|
||||||
|
# Установка с dev-зависимостями
|
||||||
|
uv sync --extra dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### Запуск обучения GPT
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Обучение базовой GPT модели
|
||||||
|
uv run python experiments/llm_only/train_gpt_bpe.py
|
||||||
|
|
||||||
|
# Обучение с интеграцией HuggingFace
|
||||||
|
uv run python experiments/hf_integration/simple_hf_training.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### Тестирование hf-proxy
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Базовое тестирование интеграции
|
||||||
|
uv run python experiments/hf_integration/test_hf_proxy.py
|
||||||
|
|
||||||
|
# Генерация через HF инструменты
|
||||||
|
uv run python experiments/hf_integration/generate_with_hf_tools.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### Использование в коде
|
||||||
|
|
||||||
|
```python
|
||||||
|
from llm.models.gpt import GPT
|
||||||
|
from llm.tokenizers import BPETokenizer
|
||||||
|
from hf_proxy import HFAdapter, HFTokenizerAdapter
|
||||||
|
|
||||||
|
# Создание модели
|
||||||
|
config = {
|
||||||
|
"vocab_size": 50257,
|
||||||
|
"embed_dim": 256,
|
||||||
|
"num_heads": 4,
|
||||||
|
"num_layers": 4,
|
||||||
|
"max_position_embeddings": 128,
|
||||||
|
"dropout": 0.1
|
||||||
|
}
|
||||||
|
model = GPT(config)
|
||||||
|
|
||||||
|
# Генерация текста
|
||||||
|
generated = model.generate(
|
||||||
|
input_ids,
|
||||||
|
max_new_tokens=50,
|
||||||
|
do_sample=True,
|
||||||
|
temperature=0.7
|
||||||
|
)
|
||||||
|
|
||||||
|
# Использование с HuggingFace через hf-proxy
|
||||||
|
hf_model = HFAdapter.from_llm_model(model)
|
||||||
|
hf_tokenizer = HFTokenizerAdapter(tokenizer)
|
||||||
|
|
||||||
|
# Генерация через HF интерфейс
|
||||||
|
generated = hf_model.generate(
|
||||||
|
input_ids=inputs['input_ids'],
|
||||||
|
max_new_tokens=50,
|
||||||
|
do_sample=True,
|
||||||
|
temperature=0.7
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🛠️ Технологический стек
|
||||||
|
|
||||||
|
- **Python 3.10+** — язык программирования
|
||||||
|
- **uv** — современный менеджер пакетов и workspace
|
||||||
|
- **PyTorch 2.8+** — фреймворк глубокого обучения
|
||||||
|
- **Transformers** — интеграция с HuggingFace
|
||||||
|
- **Datasets** — работа с данными
|
||||||
|
- **TOML** — конфигурационные файлы
|
||||||
|
|
||||||
|
## 📦 Зависимости
|
||||||
|
|
||||||
|
### Корневой workspace
|
||||||
|
```toml
|
||||||
|
[project]
|
||||||
|
dependencies = ["tqdm>=4,<5"]
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
dev = [
|
||||||
|
"pytest>=8.0.0",
|
||||||
|
"black>=24.0.0",
|
||||||
|
"ruff>=0.3.0",
|
||||||
|
"mypy>=1.8.0",
|
||||||
|
"jupyter>=1.0.0",
|
||||||
|
]
|
||||||
|
test = [
|
||||||
|
"pytest>=8.0.0",
|
||||||
|
"pytest-cov>=4.1.0",
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Пакет llm
|
||||||
|
```toml
|
||||||
|
[project]
|
||||||
|
dependencies = [
|
||||||
|
"torch>=2.3.0",
|
||||||
|
"numpy>=1.24.0",
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Пакет hf-proxy
|
||||||
|
```toml
|
||||||
|
[project]
|
||||||
|
dependencies = [
|
||||||
|
"torch>=2.3.0",
|
||||||
|
"transformers>=4.44.0",
|
||||||
|
"datasets>=2.20.0",
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎯 Реализованные возможности
|
||||||
|
|
||||||
|
### Архитектура GPT
|
||||||
|
- ✅ Токенные и позиционные эмбеддинги
|
||||||
|
- ✅ Многоголовое внимание с causal mask
|
||||||
|
- ✅ Декодерные блоки с residual connections
|
||||||
|
- ✅ Layer normalization
|
||||||
|
- ✅ Dropout регуляризация
|
||||||
|
|
||||||
|
### Генерация текста
|
||||||
|
- ✅ Жадный поиск (greedy decoding)
|
||||||
|
- ✅ Вероятностное сэмплирование
|
||||||
|
- ✅ Top-k сэмплирование
|
||||||
|
- ✅ Nucleus sampling (top-p)
|
||||||
|
- ✅ Контроль температуры
|
||||||
|
|
||||||
|
### Обучение
|
||||||
|
- ✅ Датасет для языкового моделирования
|
||||||
|
- ✅ Базовый тренировочный цикл
|
||||||
|
- ✅ Оптимизатор AdamW
|
||||||
|
- ✅ Сохранение чекпоинтов
|
||||||
|
|
||||||
|
### Интеграция с HuggingFace (hf-proxy)
|
||||||
|
- ✅ Адаптер моделей для совместимости с HF интерфейсами
|
||||||
|
- ✅ Адаптер токенизаторов с поддержкой всех методов HF
|
||||||
|
- ✅ Сохранение и загрузка в HF формате
|
||||||
|
- ✅ Совместимость с HF Trainer и pipelines
|
||||||
|
- ✅ Генерация через стандартные HF интерфейсы
|
||||||
|
|
||||||
|
## 🔬 Эксперименты с hf-proxy
|
||||||
|
|
||||||
|
### Успешно протестированные функции:
|
||||||
|
|
||||||
|
1. **Базовая интеграция** (`test_hf_proxy.py`)
|
||||||
|
- ✅ Создание HF адаптера для токенизаторов
|
||||||
|
- ✅ Создание HF адаптера для моделей
|
||||||
|
- ✅ Токенизация и декодирование
|
||||||
|
- ✅ Forward pass через адаптированную модель
|
||||||
|
- ✅ Сохранение и загрузка моделей
|
||||||
|
|
||||||
|
2. **Упрощенное обучение** (`simple_hf_training.py`)
|
||||||
|
- ✅ Обучение GPT модели с использованием hf-proxy
|
||||||
|
- ✅ Ручной цикл обучения без сложных зависимостей
|
||||||
|
- ✅ Сохранение результатов обучения
|
||||||
|
|
||||||
|
3. **Генерация через HF инструменты** (`generate_with_hf_tools.py`)
|
||||||
|
- ✅ Загрузка моделей в HF формате
|
||||||
|
- ✅ Генерация через стандартные HF интерфейсы
|
||||||
|
- ✅ Сравнение стратегий генерации
|
||||||
|
- ✅ Интерактивная генерация
|
||||||
|
|
||||||
|
### Решенные проблемы:
|
||||||
|
|
||||||
|
- ✅ Исправление метода `pad` в токенизаторе для обработки разных типов данных
|
||||||
|
- ✅ Корректная загрузка моделей с передачей конфигурации
|
||||||
|
- ✅ Совместимость с HF экосистемой
|
||||||
|
|
||||||
|
## 📊 Примеры работы
|
||||||
|
|
||||||
|
### Обучение модели
|
||||||
|
```bash
|
||||||
|
🚀 УПРОЩЕННОЕ ОБУЧЕНИЕ GPT С HF-PROXY
|
||||||
|
=========================================================
|
||||||
|
🔧 Подготовка данных...
|
||||||
|
📊 Данные: 10 train, 2 validation
|
||||||
|
🔧 Подготовка токенизатора...
|
||||||
|
✅ Токенизатор создан (vocab_size=473)
|
||||||
|
🔧 Подготовка модели...
|
||||||
|
✅ Модель создана
|
||||||
|
🎯 Обучение модели...
|
||||||
|
📊 Результаты обучения:
|
||||||
|
Final train loss: 4.6802
|
||||||
|
Final val loss: 5.1834
|
||||||
|
✅ Модель сохранена
|
||||||
|
```
|
||||||
|
|
||||||
|
### Генерация через HF интерфейсы
|
||||||
|
```bash
|
||||||
|
🧪 Тестирование HuggingFace pipeline...
|
||||||
|
🎯 Генерация текста через HF адаптер
|
||||||
|
🔤 Промпт: 'Искусственный'
|
||||||
|
🎯 Результат: 'Искусственный интеллект продолжает развиваться...'
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 Разработка
|
||||||
|
|
||||||
|
### Добавление зависимостей
|
||||||
|
```bash
|
||||||
|
# В корневой проект
|
||||||
|
uv add package-name
|
||||||
|
|
||||||
|
# В конкретный пакет
|
||||||
|
cd llm && uv add package-name
|
||||||
|
|
||||||
|
# Dev-зависимости
|
||||||
|
uv add --dev pytest black
|
||||||
|
```
|
||||||
|
|
||||||
|
### Запуск тестов
|
||||||
|
```bash
|
||||||
|
uv run pytest
|
||||||
|
```
|
||||||
|
|
||||||
|
### Форматирование кода
|
||||||
|
```bash
|
||||||
|
uv run black .
|
||||||
|
uv run ruff check .
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🤝 Вклад в проект
|
||||||
|
|
||||||
|
1. Форкните репозиторий
|
||||||
|
2. Создайте feature ветку
|
||||||
|
3. Внесите изменения
|
||||||
|
4. Запустите тесты: `uv run pytest`
|
||||||
|
5. Отформатируйте код: `uv run black . && uv run ruff check .`
|
||||||
|
6. Создайте pull request
|
||||||
|
|
||||||
|
## 📄 Лицензия
|
||||||
|
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Разработано с ❤️ для исследований в области LLM**
|
||||||
|
|
||||||
|
*Обновлено: Октябрь 2025*
|
||||||
131
experiments/README.md
Normal file
131
experiments/README.md
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
# Эксперименты с LLM архитектурами
|
||||||
|
|
||||||
|
Унифицированная структура экспериментов для обучения и генерации текста моделями LLM.
|
||||||
|
|
||||||
|
## 📁 Структура экспериментов
|
||||||
|
|
||||||
|
```
|
||||||
|
experiments/
|
||||||
|
├── llm_only/ # Эксперименты только с библиотекой llm
|
||||||
|
│ ├── train_gpt_bpe.py # Обучение GPT с BPE токенизатором
|
||||||
|
│ └── generate_gpt_bpe.py # Генерация с GPT + BPE
|
||||||
|
├── hf_integration/ # Эксперименты с hf-proxy
|
||||||
|
│ ├── train_with_hf_trainer.py # Обучение через HF Trainer
|
||||||
|
│ └── generate_with_hf_tools.py # Генерация через HF инструменты
|
||||||
|
├── shared/ # Общие утилиты
|
||||||
|
│ ├── data.py # Загрузка и подготовка данных
|
||||||
|
│ └── configs.py # Конфигурации моделей
|
||||||
|
└── README.md # Этот файл
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 Быстрый старт
|
||||||
|
|
||||||
|
### 1. Только библиотека llm (автономный режим)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Обучение GPT модели с собственным BPE токенизатором
|
||||||
|
uv run python experiments/llm_only/train_gpt_bpe.py
|
||||||
|
|
||||||
|
# Генерация текста обученной моделью
|
||||||
|
uv run python experiments/llm_only/generate_gpt_bpe.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Интеграция с HuggingFace через hf-proxy
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Обучение через HuggingFace Trainer
|
||||||
|
uv run python experiments/hf_integration/train_with_hf_trainer.py
|
||||||
|
|
||||||
|
# Генерация через HF инструменты
|
||||||
|
uv run python experiments/hf_integration/generate_with_hf_tools.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📊 Сравнение подходов
|
||||||
|
|
||||||
|
| Аспект | Только llm | С hf-proxy |
|
||||||
|
|--------|------------|------------|
|
||||||
|
| **Зависимости** | Только PyTorch | + HuggingFace Transformers |
|
||||||
|
| **Обучение** | Собственный Trainer | HF Trainer |
|
||||||
|
| **Генерация** | Прямой вызов модели | HF pipeline & интерфейсы |
|
||||||
|
| **Гибкость** | Полный контроль | Совместимость с HF экосистемой |
|
||||||
|
| **Сложность** | Проще | Более сложная настройка |
|
||||||
|
|
||||||
|
## 🔧 Конфигурация
|
||||||
|
|
||||||
|
Все эксперименты используют общие конфигурации из `shared/configs.py`:
|
||||||
|
|
||||||
|
- **Модели**: базовые, маленькие и большие конфигурации GPT
|
||||||
|
- **Токенизаторы**: параметры BPE обучения
|
||||||
|
- **Обучение**: гиперпараметры обучения
|
||||||
|
- **Генерация**: параметры генерации текста
|
||||||
|
|
||||||
|
## 📈 Результаты
|
||||||
|
|
||||||
|
Эксперименты сохраняют:
|
||||||
|
- Обученные модели в `checkpoints/`
|
||||||
|
- Токенизаторы в формате JSON
|
||||||
|
- Логи обучения и генерации
|
||||||
|
- Конфигурации моделей
|
||||||
|
|
||||||
|
## 🎯 Примеры использования
|
||||||
|
|
||||||
|
### Автономное использование (только llm)
|
||||||
|
|
||||||
|
```python
|
||||||
|
from llm.models.gpt import GPT
|
||||||
|
from llm.tokenizers import BPETokenizer
|
||||||
|
|
||||||
|
# Загрузка обученной модели
|
||||||
|
model = GPT(config)
|
||||||
|
model.load_state_dict(torch.load("checkpoints/gpt-bpe/model.pt"))
|
||||||
|
|
||||||
|
# Загрузка токенизатора
|
||||||
|
tokenizer = BPETokenizer.load("checkpoints/bpe_tokenizer.json")
|
||||||
|
|
||||||
|
# Генерация текста
|
||||||
|
input_ids = tokenizer.encode("промпт")
|
||||||
|
generated = model.generate(input_ids)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Интеграция с HF (через hf-proxy)
|
||||||
|
|
||||||
|
```python
|
||||||
|
from hf_proxy import HFAdapter, HFTokenizerAdapter
|
||||||
|
|
||||||
|
# Загрузка через адаптеры
|
||||||
|
hf_model = HFAdapter.from_pretrained("checkpoints/hf-trained/pytorch_model.bin")
|
||||||
|
hf_tokenizer = HFTokenizerAdapter.from_pretrained("checkpoints/hf-bpe-tokenizer")
|
||||||
|
|
||||||
|
# Использование с HF инструментами
|
||||||
|
from transformers import pipeline
|
||||||
|
pipe = pipeline("text-generation", model=hf_model, tokenizer=hf_tokenizer)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔍 Мониторинг
|
||||||
|
|
||||||
|
- **Логи обучения**: автоматически сохраняются в JSON
|
||||||
|
- **Метрики**: loss, длина генерации, эффективность токенизации
|
||||||
|
- **Визуализация**: можно интегрировать с TensorBoard через HF Trainer
|
||||||
|
|
||||||
|
## 🛠️ Разработка
|
||||||
|
|
||||||
|
### Добавление нового эксперимента
|
||||||
|
|
||||||
|
1. Создайте файл в соответствующей директории (`llm_only/` или `hf_integration/`)
|
||||||
|
2. Используйте общие утилиты из `shared/`
|
||||||
|
3. Сохраняйте результаты в стандартизированные пути
|
||||||
|
4. Документируйте конфигурации и результаты
|
||||||
|
|
||||||
|
### Модификация конфигураций
|
||||||
|
|
||||||
|
Измените соответствующие секции в `shared/configs.py`:
|
||||||
|
- `BASE_GPT_CONFIG` - параметры модели
|
||||||
|
- `BPE_CONFIG` - параметры токенизатора
|
||||||
|
- `TRAINING_CONFIG` - параметры обучения
|
||||||
|
- `GENERATION_CONFIG` - параметры генерации
|
||||||
|
|
||||||
|
## 📚 Дополнительные ресурсы
|
||||||
|
|
||||||
|
- [Документация llm библиотеки](../llm/README.md)
|
||||||
|
- [Документация hf-proxy](../hf-proxy/README.md)
|
||||||
|
- [Примеры использования](../notebooks/)
|
||||||
372
experiments/hf_integration/generate_with_hf_tools.py
Normal file
372
experiments/hf_integration/generate_with_hf_tools.py
Normal file
@@ -0,0 +1,372 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Experiment: generate_with_hf_tools.py
|
||||||
|
Description: Генерация текста обученной GPT моделью через HuggingFace инструменты.
|
||||||
|
Использует hf-proxy для интеграции кастомной модели с HF экосистемой.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import torch
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Добавляем путь к shared модулям
|
||||||
|
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
|
from hf_proxy import HFAdapter, HFTokenizerAdapter, create_hf_pipeline
|
||||||
|
|
||||||
|
from shared.configs import (
|
||||||
|
TEST_PROMPTS, GENERATION_CONFIG, PATHS
|
||||||
|
)
|
||||||
|
from shared.data import (
|
||||||
|
print_experiment_info, ensure_directories, ExperimentLogger
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def load_hf_model_and_tokenizer() -> tuple:
|
||||||
|
"""
|
||||||
|
Загружает модель и токенизатор в формате HuggingFace.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple: (hf_model, hf_tokenizer, model_config)
|
||||||
|
"""
|
||||||
|
# Используем упрощенную версию модели
|
||||||
|
model_path = "checkpoints/hf_simple_trained"
|
||||||
|
tokenizer_path = "checkpoints/hf_simple_tokenizer"
|
||||||
|
|
||||||
|
# Проверяем существование файлов
|
||||||
|
if not os.path.exists(model_path):
|
||||||
|
raise FileNotFoundError(
|
||||||
|
f"Модель не найдена: {model_path}\n"
|
||||||
|
f"Сначала обучите модель: uv run python experiments/hf_integration/simple_hf_training.py"
|
||||||
|
)
|
||||||
|
|
||||||
|
if not os.path.exists(tokenizer_path):
|
||||||
|
raise FileNotFoundError(
|
||||||
|
f"Токенизатор не найден: {tokenizer_path}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Загружаем адаптированный токенизатор
|
||||||
|
print("🔧 Загрузка адаптированного токенизатора...")
|
||||||
|
hf_tokenizer = HFTokenizerAdapter.from_pretrained(tokenizer_path)
|
||||||
|
print(f"✅ Токенизатор загружен (vocab_size={hf_tokenizer.vocab_size})")
|
||||||
|
|
||||||
|
# Загружаем конфигурацию модели
|
||||||
|
import json
|
||||||
|
config_path = os.path.join(model_path, "config.json")
|
||||||
|
with open(config_path, 'r', encoding='utf-8') as f:
|
||||||
|
model_config = json.load(f)
|
||||||
|
|
||||||
|
# Загружаем модель через HFAdapter с правильной конфигурацией
|
||||||
|
print("🔧 Загрузка адаптированной модели...")
|
||||||
|
model_bin_path = os.path.join(model_path, "pytorch_model.bin")
|
||||||
|
|
||||||
|
# Создаем конфигурацию из сохраненного config.json
|
||||||
|
from hf_proxy import HFAdapterConfig
|
||||||
|
hf_config = HFAdapterConfig(
|
||||||
|
vocab_size=model_config["vocab_size"],
|
||||||
|
hidden_size=model_config["hidden_size"],
|
||||||
|
num_hidden_layers=model_config["num_hidden_layers"],
|
||||||
|
num_attention_heads=model_config["num_attention_heads"],
|
||||||
|
max_position_embeddings=model_config["max_position_embeddings"],
|
||||||
|
hidden_dropout_prob=model_config.get("hidden_dropout_prob", 0.1),
|
||||||
|
attention_probs_dropout_prob=model_config.get("attention_probs_dropout_prob", 0.1),
|
||||||
|
)
|
||||||
|
|
||||||
|
hf_model = HFAdapter.from_pretrained(model_bin_path, hf_config=hf_config)
|
||||||
|
hf_model.eval()
|
||||||
|
print("✅ Модель загружена")
|
||||||
|
|
||||||
|
return hf_model, hf_tokenizer, model_config
|
||||||
|
|
||||||
|
|
||||||
|
def test_hf_pipeline(hf_model, hf_tokenizer):
|
||||||
|
"""
|
||||||
|
Тестирует создание HuggingFace pipeline.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
hf_model: Адаптированная модель
|
||||||
|
hf_tokenizer: Адаптированный токенизатор
|
||||||
|
"""
|
||||||
|
print("\n🧪 Тестирование HuggingFace pipeline...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Создаем pipeline
|
||||||
|
pipe = create_hf_pipeline(
|
||||||
|
hf_model,
|
||||||
|
tokenizer=hf_tokenizer,
|
||||||
|
device="cpu",
|
||||||
|
max_length=50,
|
||||||
|
do_sample=True,
|
||||||
|
temperature=0.7
|
||||||
|
)
|
||||||
|
|
||||||
|
print("✅ HuggingFace pipeline создан")
|
||||||
|
|
||||||
|
# Тестируем pipeline
|
||||||
|
test_prompts = TEST_PROMPTS[:3]
|
||||||
|
|
||||||
|
for prompt in test_prompts:
|
||||||
|
print(f"\n🔤 Промпт: '{prompt}'")
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = pipe(prompt, max_new_tokens=20)
|
||||||
|
print(f"🎯 Результат: {result[0]['generated_text']}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Ошибка в pipeline: {e}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Ошибка создания pipeline: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def generate_with_hf_model(hf_model, hf_tokenizer, prompt: str, config: dict) -> str:
|
||||||
|
"""
|
||||||
|
Генерирует текст через адаптированную модель HF.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
hf_model: Адаптированная модель
|
||||||
|
hf_tokenizer: Адаптированный токенизатор
|
||||||
|
prompt: Входной текст
|
||||||
|
config: Конфигурация генерации
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Сгенерированный текст
|
||||||
|
"""
|
||||||
|
print(f"🔤 Промпт: '{prompt}'")
|
||||||
|
print(f"📊 Параметры: max_tokens={config['max_new_tokens']}, "
|
||||||
|
f"temp={config['temperature']}, sample={config['do_sample']}")
|
||||||
|
|
||||||
|
# Кодируем через адаптированный токенизатор
|
||||||
|
inputs = hf_tokenizer(prompt, return_tensors="pt")
|
||||||
|
|
||||||
|
print(f"🎯 Токены промпта: {inputs['input_ids'].tolist()[0]}")
|
||||||
|
print("🔄 Генерация через HF адаптер...")
|
||||||
|
|
||||||
|
# Генерируем через адаптированную модель
|
||||||
|
with torch.no_grad():
|
||||||
|
generated_ids = hf_model.generate(
|
||||||
|
input_ids=inputs['input_ids'],
|
||||||
|
max_new_tokens=config["max_new_tokens"],
|
||||||
|
do_sample=config["do_sample"],
|
||||||
|
temperature=config["temperature"],
|
||||||
|
top_k=config["top_k"],
|
||||||
|
top_p=config["top_p"]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Декодируем через адаптированный токенизатор
|
||||||
|
generated_text = hf_tokenizer.decode(generated_ids[0], skip_special_tokens=True)
|
||||||
|
|
||||||
|
return generated_text
|
||||||
|
|
||||||
|
|
||||||
|
def test_different_hf_strategies(hf_model, hf_tokenizer, prompt: str):
|
||||||
|
"""
|
||||||
|
Тестирует разные стратегии генерации через HF интерфейс.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
hf_model: Адаптированная модель
|
||||||
|
hf_tokenizer: Адаптированный токенизатор
|
||||||
|
prompt: Тестовый промпт
|
||||||
|
"""
|
||||||
|
print(f"\n🎭 Сравнение стратегий генерации через HF для промпта: '{prompt}'")
|
||||||
|
print("=" * 70)
|
||||||
|
|
||||||
|
strategies = [
|
||||||
|
{"name": "🎯 Жадный поиск", "do_sample": False, "temperature": 1.0},
|
||||||
|
{"name": "🎲 Вероятностная (temp=0.7)", "do_sample": True, "temperature": 0.7},
|
||||||
|
{"name": "🔥 Случайная (temp=1.2)", "do_sample": True, "temperature": 1.2},
|
||||||
|
{"name": "❄️ Детерминированная (temp=0.3)", "do_sample": True, "temperature": 0.3},
|
||||||
|
]
|
||||||
|
|
||||||
|
for strategy in strategies:
|
||||||
|
print(f"\n{strategy['name']}:")
|
||||||
|
try:
|
||||||
|
config = GENERATION_CONFIG.copy()
|
||||||
|
config.update({
|
||||||
|
"do_sample": strategy["do_sample"],
|
||||||
|
"temperature": strategy["temperature"],
|
||||||
|
"max_new_tokens": 20
|
||||||
|
})
|
||||||
|
|
||||||
|
generated = generate_with_hf_model(hf_model, hf_tokenizer, prompt, config)
|
||||||
|
|
||||||
|
# Выделяем сгенерированную часть
|
||||||
|
generated_part = generated[len(prompt):]
|
||||||
|
print(f" 📤 Промпт: '{prompt}'")
|
||||||
|
print(f" 🎯 Сгенерировано: '{generated_part}'")
|
||||||
|
print(f" 📄 Полный текст: '{generated}'")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ❌ Ошибка: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def analyze_hf_tokenization(hf_tokenizer, texts: list):
|
||||||
|
"""
|
||||||
|
Анализирует токенизацию через адаптированный токенизатор.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
hf_tokenizer: Адаптированный токенизатор
|
||||||
|
texts: Список текстов для анализа
|
||||||
|
"""
|
||||||
|
print(f"\n🔍 Анализ токенизации через HF адаптер:")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
for i, text in enumerate(texts):
|
||||||
|
print(f"\nТекст {i+1}: '{text}'")
|
||||||
|
|
||||||
|
# Токенизация через адаптер
|
||||||
|
inputs = hf_tokenizer(text, return_tensors="pt")
|
||||||
|
tokens = inputs['input_ids'].tolist()[0]
|
||||||
|
token_strings = hf_tokenizer.tokenize(text)
|
||||||
|
|
||||||
|
print(f" Токены (ID): {tokens}")
|
||||||
|
print(f" Токены (текст): {token_strings}")
|
||||||
|
print(f" Количество токенов: {len(tokens)}")
|
||||||
|
|
||||||
|
# Декодирование обратно
|
||||||
|
decoded = hf_tokenizer.decode(tokens)
|
||||||
|
print(f" Декодированный: '{decoded}'")
|
||||||
|
|
||||||
|
if text == decoded:
|
||||||
|
print(f" ✅ Декодирование корректно")
|
||||||
|
else:
|
||||||
|
print(f" ⚠️ Расхождения")
|
||||||
|
|
||||||
|
|
||||||
|
def interactive_hf_generation(hf_model, hf_tokenizer):
|
||||||
|
"""
|
||||||
|
Режим интерактивной генерации через HF интерфейс.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
hf_model: Адаптированная модель
|
||||||
|
hf_tokenizer: Адаптированный токенизатор
|
||||||
|
"""
|
||||||
|
print(f"\n💬 Интерактивная генерация через HF (для выхода введите 'exit')")
|
||||||
|
print("-" * 60)
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
user_input = input("\n🔤 Введите промпт: ").strip()
|
||||||
|
|
||||||
|
if user_input.lower() in ['exit', 'quit', 'выход']:
|
||||||
|
break
|
||||||
|
|
||||||
|
if not user_input:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Запрашиваем параметры
|
||||||
|
try:
|
||||||
|
max_tokens = int(input("📏 Макс. токенов [50]: ") or "50")
|
||||||
|
temperature = float(input("🌡️ Температура [0.7]: ") or "0.7")
|
||||||
|
do_sample_input = input("🎲 Сэмплирование (y/n) [y]: ").lower()
|
||||||
|
do_sample = do_sample_input != 'n'
|
||||||
|
except:
|
||||||
|
max_tokens = 50
|
||||||
|
temperature = 0.7
|
||||||
|
do_sample = True
|
||||||
|
print("⚠️ Использую параметры по умолчанию")
|
||||||
|
|
||||||
|
config = GENERATION_CONFIG.copy()
|
||||||
|
config.update({
|
||||||
|
"max_new_tokens": max_tokens,
|
||||||
|
"temperature": temperature,
|
||||||
|
"do_sample": do_sample
|
||||||
|
})
|
||||||
|
|
||||||
|
generated = generate_with_hf_model(hf_model, hf_tokenizer, user_input, config)
|
||||||
|
|
||||||
|
generated_part = generated[len(user_input):]
|
||||||
|
print(f"\n🎯 Результат:")
|
||||||
|
print(f" 📤 Промпт: '{user_input}'")
|
||||||
|
print(f" 🎯 Сгенерировано: '{generated_part}'")
|
||||||
|
print(f" 📄 Полный текст: '{generated}'")
|
||||||
|
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\n👋 Завершение работы...")
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Ошибка: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Основная функция эксперимента."""
|
||||||
|
# === Настройка эксперимента ===
|
||||||
|
experiment_name = "Генерация текста через HF инструменты (с hf-proxy)"
|
||||||
|
experiment_config = {
|
||||||
|
"model": "GPT через HFAdapter",
|
||||||
|
"tokenizer": "BPE через HFTokenizerAdapter",
|
||||||
|
"инструменты": "HuggingFace pipeline & генерация",
|
||||||
|
"стратегия": "интеграция с HF экосистемой"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_experiment_info(experiment_name, experiment_config)
|
||||||
|
ensure_directories()
|
||||||
|
logger = ExperimentLogger(experiment_name)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Загружаем модель и токенизатор в HF формате
|
||||||
|
hf_model, hf_tokenizer, model_config = load_hf_model_and_tokenizer()
|
||||||
|
|
||||||
|
# === Анализ токенизации ===
|
||||||
|
analysis_texts = [
|
||||||
|
"Искусственный интеллект",
|
||||||
|
"Нейронные сети",
|
||||||
|
"Машинное обучение"
|
||||||
|
]
|
||||||
|
analyze_hf_tokenization(hf_tokenizer, analysis_texts)
|
||||||
|
|
||||||
|
# === Тестирование HF pipeline ===
|
||||||
|
test_hf_pipeline(hf_model, hf_tokenizer)
|
||||||
|
|
||||||
|
# === Генерация с разными промптами ===
|
||||||
|
print(f"\n🎯 Генерация текста через HF адаптер")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
for i, prompt in enumerate(TEST_PROMPTS):
|
||||||
|
print(f"\n📝 Пример {i+1}/{len(TEST_PROMPTS)}")
|
||||||
|
print("-" * 40)
|
||||||
|
|
||||||
|
try:
|
||||||
|
generated = generate_with_hf_model(hf_model, hf_tokenizer, prompt, GENERATION_CONFIG)
|
||||||
|
|
||||||
|
# Выделяем сгенерированную часть
|
||||||
|
generated_part = generated[len(prompt):]
|
||||||
|
|
||||||
|
print(f"📤 Промпт: '{prompt}'")
|
||||||
|
print(f"🎯 Сгенерировано: '{generated_part}'")
|
||||||
|
print(f"📄 Полный текст: '{generated}'")
|
||||||
|
print(f"📏 Длина: {len(generated)} символов")
|
||||||
|
|
||||||
|
# Логируем успешную генерацию
|
||||||
|
logger.log_metric(f"hf_generation_length_{i}", len(generated))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Ошибка при генерации: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# === Сравнение стратегий генерации ===
|
||||||
|
test_prompt = "Искусственный"
|
||||||
|
test_different_hf_strategies(hf_model, hf_tokenizer, test_prompt)
|
||||||
|
|
||||||
|
# === Интерактивная генерация ===
|
||||||
|
interactive_hf_generation(hf_model, hf_tokenizer)
|
||||||
|
|
||||||
|
# === Сохранение результатов ===
|
||||||
|
logger.save_logs("checkpoints/hf_integration_generation_logs.json")
|
||||||
|
|
||||||
|
print(f"\n🎉 Эксперимент с HF интеграцией завершен успешно!")
|
||||||
|
print(f"\n📚 Достигнутая интеграция:")
|
||||||
|
print(f" ✅ Загрузка модели и токенизатора в HF формате")
|
||||||
|
print(f" ✅ Использование HF pipeline")
|
||||||
|
print(f" ✅ Генерация через стандартные HF интерфейсы")
|
||||||
|
print(f" ✅ Совместимость с HF экосистемой")
|
||||||
|
|
||||||
|
except FileNotFoundError as e:
|
||||||
|
print(f"❌ {e}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Ошибка в эксперименте: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
285
experiments/hf_integration/simple_hf_training.py
Normal file
285
experiments/hf_integration/simple_hf_training.py
Normal file
@@ -0,0 +1,285 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Experiment: simple_hf_training.py
|
||||||
|
Description: Упрощенное обучение GPT модели с использованием hf-proxy.
|
||||||
|
Использует ручное обучение вместо сложного HuggingFace Trainer.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import torch
|
||||||
|
import torch.nn as nn
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
|
||||||
|
# Добавляем путь к shared модулям
|
||||||
|
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
|
from llm.models.gpt import GPT
|
||||||
|
from llm.tokenizers import BPETokenizer
|
||||||
|
from hf_proxy import HFAdapter, HFTokenizerAdapter
|
||||||
|
|
||||||
|
from shared.configs import (
|
||||||
|
TRAIN_TEXTS, BASE_GPT_CONFIG, BPE_CONFIG,
|
||||||
|
TRAINING_CONFIG, PATHS, TEST_PROMPTS
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def create_dataset(hf_tokenizer, texts, max_length=128):
|
||||||
|
"""
|
||||||
|
Создает простой датасет для обучения.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
hf_tokenizer: Адаптированный токенизатор
|
||||||
|
texts: Список текстов
|
||||||
|
max_length: Максимальная длина последовательности
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list: Список тензоров input_ids
|
||||||
|
"""
|
||||||
|
dataset = []
|
||||||
|
|
||||||
|
for text in texts:
|
||||||
|
# Токенизируем текст
|
||||||
|
inputs = hf_tokenizer(
|
||||||
|
text,
|
||||||
|
max_length=max_length,
|
||||||
|
truncation=True,
|
||||||
|
padding=False,
|
||||||
|
return_tensors="pt"
|
||||||
|
)
|
||||||
|
|
||||||
|
input_ids = inputs['input_ids'][0]
|
||||||
|
|
||||||
|
# Создаем метки для языкового моделирования
|
||||||
|
labels = input_ids.clone()
|
||||||
|
|
||||||
|
dataset.append({
|
||||||
|
'input_ids': input_ids,
|
||||||
|
'labels': labels
|
||||||
|
})
|
||||||
|
|
||||||
|
return dataset
|
||||||
|
|
||||||
|
|
||||||
|
def manual_training_loop(hf_model, hf_tokenizer, train_texts, val_texts, config):
|
||||||
|
"""
|
||||||
|
Ручной цикл обучения без использования Trainer.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
hf_model: Адаптированная модель
|
||||||
|
hf_tokenizer: Адаптированный токенизатор
|
||||||
|
train_texts: Тексты для обучения
|
||||||
|
val_texts: Тексты для валидации
|
||||||
|
config: Конфигурация обучения
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Результаты обучения
|
||||||
|
"""
|
||||||
|
print("🎯 Запуск ручного обучения...")
|
||||||
|
|
||||||
|
# Создаем датасеты
|
||||||
|
train_dataset = create_dataset(hf_tokenizer, train_texts)
|
||||||
|
val_dataset = create_dataset(hf_tokenizer, val_texts)
|
||||||
|
|
||||||
|
print(f"📊 Данные: {len(train_dataset)} train, {len(val_dataset)} validation")
|
||||||
|
|
||||||
|
# Оптимизатор
|
||||||
|
optimizer = torch.optim.AdamW(
|
||||||
|
hf_model.parameters(),
|
||||||
|
lr=config["learning_rate"]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Функция потерь
|
||||||
|
loss_fn = nn.CrossEntropyLoss()
|
||||||
|
|
||||||
|
# Обучение
|
||||||
|
hf_model.train()
|
||||||
|
train_losses = []
|
||||||
|
val_losses = []
|
||||||
|
|
||||||
|
for epoch in range(config["num_epochs"]):
|
||||||
|
print(f"\n📅 Эпоха {epoch + 1}/{config['num_epochs']}")
|
||||||
|
|
||||||
|
# Обучение
|
||||||
|
epoch_train_loss = 0
|
||||||
|
for i, batch in enumerate(train_dataset):
|
||||||
|
optimizer.zero_grad()
|
||||||
|
|
||||||
|
input_ids = batch['input_ids'].unsqueeze(0) # [1, seq_len]
|
||||||
|
labels = batch['labels'].unsqueeze(0) # [1, seq_len]
|
||||||
|
|
||||||
|
# Forward pass
|
||||||
|
outputs = hf_model(input_ids=input_ids, labels=labels)
|
||||||
|
loss = outputs.loss
|
||||||
|
|
||||||
|
# Backward pass
|
||||||
|
loss.backward()
|
||||||
|
optimizer.step()
|
||||||
|
|
||||||
|
epoch_train_loss += loss.item()
|
||||||
|
|
||||||
|
if i % 5 == 0:
|
||||||
|
print(f" Batch {i}/{len(train_dataset)}: loss = {loss.item():.4f}")
|
||||||
|
|
||||||
|
avg_train_loss = epoch_train_loss / len(train_dataset)
|
||||||
|
train_losses.append(avg_train_loss)
|
||||||
|
print(f" 📊 Средняя train loss: {avg_train_loss:.4f}")
|
||||||
|
|
||||||
|
# Валидация
|
||||||
|
hf_model.eval()
|
||||||
|
epoch_val_loss = 0
|
||||||
|
with torch.no_grad():
|
||||||
|
for batch in val_dataset:
|
||||||
|
input_ids = batch['input_ids'].unsqueeze(0)
|
||||||
|
labels = batch['labels'].unsqueeze(0)
|
||||||
|
|
||||||
|
outputs = hf_model(input_ids=input_ids, labels=labels)
|
||||||
|
epoch_val_loss += outputs.loss.item()
|
||||||
|
|
||||||
|
avg_val_loss = epoch_val_loss / len(val_dataset)
|
||||||
|
val_losses.append(avg_val_loss)
|
||||||
|
print(f" 📊 Средняя val loss: {avg_val_loss:.4f}")
|
||||||
|
|
||||||
|
hf_model.train()
|
||||||
|
|
||||||
|
return {
|
||||||
|
'train_losses': train_losses,
|
||||||
|
'val_losses': val_losses,
|
||||||
|
'final_train_loss': train_losses[-1],
|
||||||
|
'final_val_loss': val_losses[-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_generation_after_training(hf_model, hf_tokenizer, test_prompts):
|
||||||
|
"""
|
||||||
|
Тестирует генерацию после обучения.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
hf_model: Обученная модель
|
||||||
|
hf_tokenizer: Токенизатор
|
||||||
|
test_prompts: Тестовые промпты
|
||||||
|
"""
|
||||||
|
print("\n🧪 Тестирование генерации после обучения...")
|
||||||
|
hf_model.eval()
|
||||||
|
|
||||||
|
for prompt in test_prompts[:3]:
|
||||||
|
print(f"\n🔤 Промпт: '{prompt}'")
|
||||||
|
|
||||||
|
try:
|
||||||
|
inputs = hf_tokenizer(prompt, return_tensors="pt")
|
||||||
|
|
||||||
|
with torch.no_grad():
|
||||||
|
generated = hf_model.generate(
|
||||||
|
input_ids=inputs['input_ids'],
|
||||||
|
max_new_tokens=20,
|
||||||
|
do_sample=True,
|
||||||
|
temperature=0.8
|
||||||
|
)
|
||||||
|
|
||||||
|
generated_text = hf_tokenizer.decode(generated[0], skip_special_tokens=True)
|
||||||
|
print(f"🎯 Результат: '{generated_text}'")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Ошибка генерации: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Основная функция эксперимента."""
|
||||||
|
print("=" * 60)
|
||||||
|
print("🚀 УПРОЩЕННОЕ ОБУЧЕНИЕ GPT С HF-PROXY")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# === Подготовка данных ===
|
||||||
|
print("🔧 Подготовка данных...")
|
||||||
|
train_texts = TRAIN_TEXTS[:10] # Используем меньше данных для быстрого тестирования
|
||||||
|
val_texts = TRAIN_TEXTS[10:12]
|
||||||
|
|
||||||
|
print(f"📊 Данные: {len(train_texts)} train, {len(val_texts)} validation")
|
||||||
|
|
||||||
|
# === Подготовка токенизатора ===
|
||||||
|
print("🔧 Подготовка токенизатора...")
|
||||||
|
llm_tokenizer = BPETokenizer()
|
||||||
|
llm_tokenizer.train(
|
||||||
|
texts=train_texts,
|
||||||
|
vocab_size=BPE_CONFIG["vocab_size"],
|
||||||
|
special_tokens=BPE_CONFIG["special_tokens"]
|
||||||
|
)
|
||||||
|
|
||||||
|
hf_tokenizer = HFTokenizerAdapter(llm_tokenizer)
|
||||||
|
print(f"✅ Токенизатор создан (vocab_size={hf_tokenizer.vocab_size})")
|
||||||
|
|
||||||
|
# === Подготовка модели ===
|
||||||
|
print("🔧 Подготовка модели...")
|
||||||
|
model_config = BASE_GPT_CONFIG.copy()
|
||||||
|
model_config["vocab_size"] = hf_tokenizer.vocab_size
|
||||||
|
|
||||||
|
llm_model = GPT(model_config)
|
||||||
|
hf_model = HFAdapter.from_llm_model(llm_model)
|
||||||
|
print(f"✅ Модель создана")
|
||||||
|
|
||||||
|
# === Тестирование до обучения ===
|
||||||
|
print("\n🧪 Тестирование до обучения...")
|
||||||
|
test_generation_after_training(hf_model, hf_tokenizer, TEST_PROMPTS)
|
||||||
|
|
||||||
|
# === Обучение ===
|
||||||
|
print(f"\n🎯 Обучение модели...")
|
||||||
|
training_config = {
|
||||||
|
"learning_rate": TRAINING_CONFIG["learning_rate"],
|
||||||
|
"num_epochs": 2, # Меньше эпох для быстрого тестирования
|
||||||
|
"batch_size": TRAINING_CONFIG["batch_size"]
|
||||||
|
}
|
||||||
|
|
||||||
|
results = manual_training_loop(
|
||||||
|
hf_model, hf_tokenizer, train_texts, val_texts, training_config
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"\n📊 Результаты обучения:")
|
||||||
|
print(f" Final train loss: {results['final_train_loss']:.4f}")
|
||||||
|
print(f" Final val loss: {results['final_val_loss']:.4f}")
|
||||||
|
|
||||||
|
# === Тестирование после обучения ===
|
||||||
|
print("\n🧪 Тестирование после обучения...")
|
||||||
|
test_generation_after_training(hf_model, hf_tokenizer, TEST_PROMPTS)
|
||||||
|
|
||||||
|
# === Сохранение модели ===
|
||||||
|
print(f"\n💾 Сохранение модели...")
|
||||||
|
|
||||||
|
# Создаем директории
|
||||||
|
os.makedirs("checkpoints/hf_simple_trained", exist_ok=True)
|
||||||
|
os.makedirs("checkpoints/hf_simple_tokenizer", exist_ok=True)
|
||||||
|
|
||||||
|
# Сохраняем токенизатор
|
||||||
|
hf_tokenizer.save_pretrained("checkpoints/hf_simple_tokenizer")
|
||||||
|
print("✅ Токенизатор сохранен")
|
||||||
|
|
||||||
|
# Сохраняем модель
|
||||||
|
HFAdapter.save_pretrained(
|
||||||
|
hf_model,
|
||||||
|
"checkpoints/hf_simple_trained",
|
||||||
|
tokenizer=hf_tokenizer
|
||||||
|
)
|
||||||
|
print("✅ Модель сохранена")
|
||||||
|
|
||||||
|
# Сохраняем результаты
|
||||||
|
results_path = "checkpoints/simple_training_results.json"
|
||||||
|
with open(results_path, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump({
|
||||||
|
'training_config': training_config,
|
||||||
|
'model_config': model_config,
|
||||||
|
'results': results
|
||||||
|
}, f, indent=2, ensure_ascii=False)
|
||||||
|
print(f"✅ Результаты сохранены в {results_path}")
|
||||||
|
|
||||||
|
print(f"\n🎉 Упрощенное обучение завершено успешно!")
|
||||||
|
print(f"\n💡 Для использования обученной модели:")
|
||||||
|
print(f" uv run python experiments/hf_integration/generate_with_hf_tools.py")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Ошибка в эксперименте: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
206
experiments/hf_integration/test_hf_proxy.py
Normal file
206
experiments/hf_integration/test_hf_proxy.py
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Test: test_hf_proxy.py
|
||||||
|
Description: Тестирование базовой функциональности hf-proxy без сложных зависимостей.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import torch
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Добавляем путь к shared модулям
|
||||||
|
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
|
from llm.models.gpt import GPT
|
||||||
|
from llm.tokenizers import BPETokenizer
|
||||||
|
from hf_proxy import HFAdapter, HFTokenizerAdapter
|
||||||
|
|
||||||
|
from shared.configs import (
|
||||||
|
TRAIN_TEXTS, BASE_GPT_CONFIG, BPE_CONFIG,
|
||||||
|
TEST_PROMPTS, GENERATION_CONFIG
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_basic_hf_integration():
|
||||||
|
"""Тестирует базовую интеграцию hf-proxy."""
|
||||||
|
print("🧪 Тестирование базовой интеграции hf-proxy...")
|
||||||
|
|
||||||
|
# === Подготовка токенизатора ===
|
||||||
|
print("1. Подготовка токенизатора...")
|
||||||
|
llm_tokenizer = BPETokenizer()
|
||||||
|
llm_tokenizer.train(
|
||||||
|
texts=TRAIN_TEXTS,
|
||||||
|
vocab_size=BPE_CONFIG["vocab_size"],
|
||||||
|
special_tokens=BPE_CONFIG["special_tokens"]
|
||||||
|
)
|
||||||
|
|
||||||
|
hf_tokenizer = HFTokenizerAdapter(llm_tokenizer)
|
||||||
|
print(f" ✅ Токенизатор создан (vocab_size={hf_tokenizer.vocab_size})")
|
||||||
|
|
||||||
|
# === Подготовка модели ===
|
||||||
|
print("2. Подготовка модели...")
|
||||||
|
model_config = BASE_GPT_CONFIG.copy()
|
||||||
|
model_config["vocab_size"] = hf_tokenizer.vocab_size
|
||||||
|
|
||||||
|
llm_model = GPT(model_config)
|
||||||
|
hf_model = HFAdapter.from_llm_model(llm_model)
|
||||||
|
print(f" ✅ Модель создана")
|
||||||
|
|
||||||
|
# === Тестирование токенизации ===
|
||||||
|
print("3. Тестирование токенизации...")
|
||||||
|
test_texts = ["Искусственный интеллект", "Нейронные сети"]
|
||||||
|
|
||||||
|
for text in test_texts:
|
||||||
|
print(f" 📝 Текст: '{text}'")
|
||||||
|
|
||||||
|
# Оригинальный токенизатор
|
||||||
|
original_tokens = llm_tokenizer.encode(text)
|
||||||
|
print(f" Оригинальный: {len(original_tokens)} токенов")
|
||||||
|
|
||||||
|
# HF адаптер
|
||||||
|
hf_inputs = hf_tokenizer(text, return_tensors="pt")
|
||||||
|
print(f" HF адаптер: {hf_inputs['input_ids'].shape}")
|
||||||
|
|
||||||
|
# Декодирование
|
||||||
|
decoded = hf_tokenizer.decode(hf_inputs['input_ids'][0])
|
||||||
|
print(f" Декодированный: '{decoded}'")
|
||||||
|
|
||||||
|
# === Тестирование forward pass ===
|
||||||
|
print("4. Тестирование forward pass...")
|
||||||
|
for text in test_texts:
|
||||||
|
hf_inputs = hf_tokenizer(text, return_tensors="pt")
|
||||||
|
|
||||||
|
with torch.no_grad():
|
||||||
|
outputs = hf_model(**hf_inputs)
|
||||||
|
|
||||||
|
print(f" 📝 '{text}' -> logits: {outputs.logits.shape}")
|
||||||
|
|
||||||
|
# === Тестирование генерации ===
|
||||||
|
print("5. Тестирование генерации...")
|
||||||
|
hf_model.eval()
|
||||||
|
|
||||||
|
for prompt in TEST_PROMPTS[:3]:
|
||||||
|
print(f" 🔤 Промпт: '{prompt}'")
|
||||||
|
|
||||||
|
try:
|
||||||
|
inputs = hf_tokenizer(prompt, return_tensors="pt")
|
||||||
|
|
||||||
|
with torch.no_grad():
|
||||||
|
generated = hf_model.generate(
|
||||||
|
input_ids=inputs['input_ids'],
|
||||||
|
max_new_tokens=10,
|
||||||
|
do_sample=True,
|
||||||
|
temperature=0.8
|
||||||
|
)
|
||||||
|
|
||||||
|
generated_text = hf_tokenizer.decode(generated[0], skip_special_tokens=True)
|
||||||
|
print(f" 🎯 Результат: '{generated_text}'")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ❌ Ошибка: {e}")
|
||||||
|
|
||||||
|
# === Тестирование сохранения/загрузки ===
|
||||||
|
print("6. Тестирование сохранения/загрузки...")
|
||||||
|
try:
|
||||||
|
# Сохраняем токенизатор
|
||||||
|
hf_tokenizer.save_pretrained("test_save/tokenizer")
|
||||||
|
print(" ✅ Токенизатор сохранен")
|
||||||
|
|
||||||
|
# Сохраняем модель
|
||||||
|
HFAdapter.save_pretrained(hf_model, "test_save/model", tokenizer=hf_tokenizer)
|
||||||
|
print(" ✅ Модель сохранена")
|
||||||
|
|
||||||
|
# Загружаем токенизатор
|
||||||
|
loaded_tokenizer = HFTokenizerAdapter.from_pretrained("test_save/tokenizer")
|
||||||
|
print(f" ✅ Токенизатор загружен (vocab_size={loaded_tokenizer.vocab_size})")
|
||||||
|
|
||||||
|
# Загружаем модель
|
||||||
|
model_path = os.path.join("test_save/model", "pytorch_model.bin")
|
||||||
|
loaded_model = HFAdapter.from_pretrained(model_path)
|
||||||
|
print(" ✅ Модель загружена")
|
||||||
|
|
||||||
|
# Проверяем работоспособность загруженной модели
|
||||||
|
test_input = hf_tokenizer("Тест", return_tensors="pt")
|
||||||
|
with torch.no_grad():
|
||||||
|
loaded_outputs = loaded_model(**test_input)
|
||||||
|
print(f" ✅ Загруженная модель работает (logits: {loaded_outputs.logits.shape})")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ❌ Ошибка сохранения/загрузки: {e}")
|
||||||
|
|
||||||
|
print("\n🎉 Базовое тестирование hf-proxy завершено!")
|
||||||
|
|
||||||
|
|
||||||
|
def test_hf_tokenizer_methods():
|
||||||
|
"""Тестирует различные методы HF токенизатора."""
|
||||||
|
print("\n🧪 Тестирование методов HF токенизатора...")
|
||||||
|
|
||||||
|
# Создаем токенизатор
|
||||||
|
llm_tokenizer = BPETokenizer()
|
||||||
|
llm_tokenizer.train(
|
||||||
|
texts=TRAIN_TEXTS[:5],
|
||||||
|
vocab_size=500,
|
||||||
|
special_tokens=BPE_CONFIG["special_tokens"]
|
||||||
|
)
|
||||||
|
|
||||||
|
hf_tokenizer = HFTokenizerAdapter(llm_tokenizer)
|
||||||
|
|
||||||
|
test_text = "Искусственный интеллект и машинное обучение"
|
||||||
|
|
||||||
|
# Тестируем разные методы
|
||||||
|
print("1. Метод __call__:")
|
||||||
|
result = hf_tokenizer(test_text, return_tensors="pt")
|
||||||
|
print(f" Результат: {result}")
|
||||||
|
|
||||||
|
print("2. Метод encode:")
|
||||||
|
encoded = hf_tokenizer.encode(test_text)
|
||||||
|
print(f" Закодировано: {encoded}")
|
||||||
|
|
||||||
|
print("3. Метод decode:")
|
||||||
|
decoded = hf_tokenizer.decode(encoded)
|
||||||
|
print(f" Декодировано: '{decoded}'")
|
||||||
|
|
||||||
|
print("4. Метод tokenize:")
|
||||||
|
tokens = hf_tokenizer.tokenize(test_text)
|
||||||
|
print(f" Токены: {tokens}")
|
||||||
|
|
||||||
|
print("5. Метод get_vocab:")
|
||||||
|
vocab = hf_tokenizer.get_vocab()
|
||||||
|
print(f" Размер словаря: {len(vocab)}")
|
||||||
|
|
||||||
|
print("✅ Все методы токенизатора работают!")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Основная функция тестирования."""
|
||||||
|
print("=" * 60)
|
||||||
|
print("🧪 ТЕСТИРОВАНИЕ HF-PROXY")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Тестируем базовую интеграцию
|
||||||
|
test_basic_hf_integration()
|
||||||
|
|
||||||
|
# Тестируем методы токенизатора
|
||||||
|
test_hf_tokenizer_methods()
|
||||||
|
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print("🎉 ВСЕ ТЕСТЫ ПРОЙДЕНЫ УСПЕШНО!")
|
||||||
|
print("=" * 60)
|
||||||
|
print("\n📚 Проверенные функции:")
|
||||||
|
print(" ✅ Создание HF адаптера для токенизатора")
|
||||||
|
print(" ✅ Создание HF адаптера для модели")
|
||||||
|
print(" ✅ Токенизация и декодирование")
|
||||||
|
print(" ✅ Forward pass через адаптированную модель")
|
||||||
|
print(" ✅ Генерация текста")
|
||||||
|
print(" ✅ Сохранение и загрузка моделей")
|
||||||
|
print(" ✅ Все методы HF токенизатора")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\n❌ Ошибка в тестировании: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
298
experiments/hf_integration/train_with_hf_trainer.py
Normal file
298
experiments/hf_integration/train_with_hf_trainer.py
Normal file
@@ -0,0 +1,298 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Experiment: train_with_hf_trainer.py
|
||||||
|
Description: Обучение GPT модели через HuggingFace Trainer с использованием hf-proxy.
|
||||||
|
Интегрирует кастомную модель llm с инструментами HuggingFace.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import torch
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Добавляем путь к shared модулям
|
||||||
|
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
|
from llm.models.gpt import GPT
|
||||||
|
from llm.tokenizers import BPETokenizer
|
||||||
|
from hf_proxy import HFAdapter, HFTokenizerAdapter
|
||||||
|
|
||||||
|
from shared.configs import (
|
||||||
|
TRAIN_TEXTS, BASE_GPT_CONFIG, BPE_CONFIG,
|
||||||
|
TRAINING_CONFIG, PATHS, TEST_PROMPTS
|
||||||
|
)
|
||||||
|
from shared.data import (
|
||||||
|
load_training_data, ensure_directories,
|
||||||
|
print_experiment_info, ExperimentLogger
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def setup_hf_training():
|
||||||
|
"""
|
||||||
|
Настраивает окружение для обучения через HuggingFace Trainer.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple: (hf_model, hf_tokenizer, llm_tokenizer, model_config)
|
||||||
|
"""
|
||||||
|
print("🔧 Настройка HuggingFace обучения...")
|
||||||
|
|
||||||
|
# === Подготовка данных ===
|
||||||
|
train_texts, val_texts = load_training_data()
|
||||||
|
print(f"📊 Данные: {len(train_texts)} train, {len(val_texts)} validation")
|
||||||
|
|
||||||
|
# === Обучение/загрузка токенизатора ===
|
||||||
|
if os.path.exists(PATHS["bpe_tokenizer"]):
|
||||||
|
print("📝 Загрузка BPE токенизатора...")
|
||||||
|
llm_tokenizer = BPETokenizer.load(PATHS["bpe_tokenizer"])
|
||||||
|
print(f"✅ Токенизатор загружен (vocab_size={llm_tokenizer.get_vocab_size()})")
|
||||||
|
else:
|
||||||
|
print("📝 Обучение BPE токенизатора...")
|
||||||
|
llm_tokenizer = BPETokenizer()
|
||||||
|
llm_tokenizer.train(
|
||||||
|
texts=TRAIN_TEXTS,
|
||||||
|
vocab_size=BPE_CONFIG["vocab_size"],
|
||||||
|
special_tokens=BPE_CONFIG["special_tokens"]
|
||||||
|
)
|
||||||
|
llm_tokenizer.save(PATHS["bpe_tokenizer"])
|
||||||
|
print(f"✅ Токенизатор обучен и сохранен")
|
||||||
|
|
||||||
|
# === Создание адаптера токенизатора ===
|
||||||
|
print("🔧 Создание адаптера HuggingFace для токенизатора...")
|
||||||
|
hf_tokenizer = HFTokenizerAdapter(llm_tokenizer)
|
||||||
|
print(f"✅ Адаптер токенизатора создан")
|
||||||
|
|
||||||
|
# === Инициализация модели ===
|
||||||
|
model_config = BASE_GPT_CONFIG.copy()
|
||||||
|
model_config["vocab_size"] = llm_tokenizer.get_vocab_size()
|
||||||
|
|
||||||
|
print("🔧 Создание GPT модели...")
|
||||||
|
llm_model = GPT(model_config)
|
||||||
|
|
||||||
|
# === Создание адаптера модели ===
|
||||||
|
print("🔧 Создание адаптера HuggingFace для модели...")
|
||||||
|
hf_model = HFAdapter.from_llm_model(llm_model)
|
||||||
|
print(f"✅ Адаптер модели создан")
|
||||||
|
|
||||||
|
return hf_model, hf_tokenizer, llm_tokenizer, model_config, train_texts, val_texts
|
||||||
|
|
||||||
|
|
||||||
|
def test_hf_integration(hf_model, hf_tokenizer, llm_tokenizer):
|
||||||
|
"""
|
||||||
|
Тестирует интеграцию с HuggingFace инструментами.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
hf_model: Адаптированная модель
|
||||||
|
hf_tokenizer: Адаптированный токенизатор
|
||||||
|
llm_tokenizer: Оригинальный токенизатор
|
||||||
|
"""
|
||||||
|
print("\n🧪 Тестирование интеграции с HuggingFace...")
|
||||||
|
|
||||||
|
test_texts = ["Искусственный интеллект", "Нейронные сети"]
|
||||||
|
|
||||||
|
for text in test_texts:
|
||||||
|
print(f"\n🔤 Текст: '{text}'")
|
||||||
|
|
||||||
|
# Тестируем адаптированный токенизатор
|
||||||
|
hf_inputs = hf_tokenizer(text, return_tensors="pt")
|
||||||
|
print(f" HF токенизатор: {hf_inputs['input_ids'].shape}")
|
||||||
|
|
||||||
|
# Тестируем оригинальный токенизатор для сравнения
|
||||||
|
original_tokens = llm_tokenizer.encode(text)
|
||||||
|
print(f" Оригинальный токенизатор: {len(original_tokens)} токенов")
|
||||||
|
|
||||||
|
# Тестируем forward pass через адаптированную модель
|
||||||
|
try:
|
||||||
|
with torch.no_grad():
|
||||||
|
outputs = hf_model(**hf_inputs)
|
||||||
|
print(f" HF forward pass: успешно (logits: {outputs.logits.shape})")
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ❌ HF forward pass: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Основная функция эксперимента."""
|
||||||
|
# === Настройка эксперимента ===
|
||||||
|
experiment_name = "Обучение GPT через HF Trainer (с hf-proxy)"
|
||||||
|
experiment_config = {
|
||||||
|
"model": "GPT через HFAdapter",
|
||||||
|
"tokenizer": "BPE через HFTokenizerAdapter",
|
||||||
|
"trainer": "HuggingFace Trainer",
|
||||||
|
"vocab_size": BPE_CONFIG["vocab_size"],
|
||||||
|
"training_epochs": TRAINING_CONFIG["num_epochs"]
|
||||||
|
}
|
||||||
|
|
||||||
|
print_experiment_info(experiment_name, experiment_config)
|
||||||
|
ensure_directories()
|
||||||
|
logger = ExperimentLogger(experiment_name)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Настраиваем обучение
|
||||||
|
hf_model, hf_tokenizer, llm_tokenizer, model_config, train_texts, val_texts = setup_hf_training()
|
||||||
|
|
||||||
|
# Тестируем интеграцию
|
||||||
|
test_hf_integration(hf_model, hf_tokenizer, llm_tokenizer)
|
||||||
|
|
||||||
|
# === Подготовка датасетов HuggingFace ===
|
||||||
|
print(f"\n📊 Подготовка датасетов HuggingFace...")
|
||||||
|
|
||||||
|
from datasets import Dataset
|
||||||
|
|
||||||
|
def tokenize_function(examples):
|
||||||
|
"""Функция токенизации для HF datasets."""
|
||||||
|
# Используем адаптированный токенизатор
|
||||||
|
tokenized = hf_tokenizer(
|
||||||
|
examples["text"],
|
||||||
|
truncation=True,
|
||||||
|
padding=False,
|
||||||
|
max_length=model_config["max_position_embeddings"],
|
||||||
|
)
|
||||||
|
tokenized["labels"] = tokenized["input_ids"].copy()
|
||||||
|
return tokenized
|
||||||
|
|
||||||
|
# Создаем датасеты
|
||||||
|
train_dataset = Dataset.from_dict({"text": train_texts})
|
||||||
|
val_dataset = Dataset.from_dict({"text": val_texts})
|
||||||
|
|
||||||
|
# Токенизируем
|
||||||
|
train_dataset = train_dataset.map(
|
||||||
|
tokenize_function,
|
||||||
|
batched=True,
|
||||||
|
remove_columns=train_dataset.column_names,
|
||||||
|
)
|
||||||
|
val_dataset = val_dataset.map(
|
||||||
|
tokenize_function,
|
||||||
|
batched=True,
|
||||||
|
remove_columns=val_dataset.column_names,
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f" Train датасет: {len(train_dataset)} примеров")
|
||||||
|
print(f" Validation датасет: {len(val_dataset)} примеров")
|
||||||
|
|
||||||
|
# === Настройка HuggingFace Trainer ===
|
||||||
|
print(f"\n🔧 Настройка HuggingFace Trainer...")
|
||||||
|
|
||||||
|
from transformers import (
|
||||||
|
Trainer,
|
||||||
|
TrainingArguments,
|
||||||
|
DataCollatorForLanguageModeling
|
||||||
|
)
|
||||||
|
|
||||||
|
# Data collator для языкового моделирования
|
||||||
|
data_collator = DataCollatorForLanguageModeling(
|
||||||
|
tokenizer=hf_tokenizer,
|
||||||
|
mlm=False,
|
||||||
|
pad_to_multiple_of=8,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Аргументы обучения
|
||||||
|
training_args = TrainingArguments(
|
||||||
|
output_dir=PATHS["hf_model"],
|
||||||
|
overwrite_output_dir=True,
|
||||||
|
num_train_epochs=TRAINING_CONFIG["num_epochs"],
|
||||||
|
per_device_train_batch_size=TRAINING_CONFIG["batch_size"],
|
||||||
|
per_device_eval_batch_size=TRAINING_CONFIG["batch_size"],
|
||||||
|
learning_rate=TRAINING_CONFIG["learning_rate"],
|
||||||
|
warmup_steps=TRAINING_CONFIG["warmup_steps"],
|
||||||
|
logging_dir="./logs",
|
||||||
|
logging_steps=10,
|
||||||
|
eval_steps=50,
|
||||||
|
save_steps=100,
|
||||||
|
eval_strategy="steps",
|
||||||
|
save_strategy="steps",
|
||||||
|
load_best_model_at_end=True,
|
||||||
|
metric_for_best_model="loss",
|
||||||
|
greater_is_better=False,
|
||||||
|
dataloader_pin_memory=False,
|
||||||
|
report_to=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Создаем Trainer
|
||||||
|
trainer = Trainer(
|
||||||
|
model=hf_model,
|
||||||
|
args=training_args,
|
||||||
|
train_dataset=train_dataset,
|
||||||
|
eval_dataset=val_dataset,
|
||||||
|
data_collator=data_collator,
|
||||||
|
)
|
||||||
|
|
||||||
|
print("✅ HuggingFace Trainer настроен")
|
||||||
|
|
||||||
|
# === Запуск обучения ===
|
||||||
|
print(f"\n🎯 Запуск обучения через HuggingFace Trainer...")
|
||||||
|
|
||||||
|
train_result = trainer.train()
|
||||||
|
|
||||||
|
# Сохраняем лучшую модель
|
||||||
|
trainer.save_model()
|
||||||
|
hf_tokenizer.save_pretrained(PATHS["hf_model"])
|
||||||
|
|
||||||
|
print("✅ Обучение завершено успешно!")
|
||||||
|
print(f"📊 Final train loss: {train_result.metrics['train_loss']:.4f}")
|
||||||
|
|
||||||
|
if "eval_loss" in train_result.metrics:
|
||||||
|
print(f"📊 Final eval loss: {train_result.metrics['eval_loss']:.4f}")
|
||||||
|
|
||||||
|
# === Сохранение через hf-proxy ===
|
||||||
|
print(f"\n💾 Сохранение через hf-proxy...")
|
||||||
|
|
||||||
|
from hf_proxy import convert_to_hf_format
|
||||||
|
|
||||||
|
# Сохраняем токенизатор в HF формате
|
||||||
|
hf_tokenizer_dir = PATHS["hf_tokenizer"]
|
||||||
|
hf_tokenizer.save_pretrained(hf_tokenizer_dir)
|
||||||
|
|
||||||
|
# Сохраняем модель через hf-proxy
|
||||||
|
hf_proxy_dir = PATHS["hf_proxy_model"]
|
||||||
|
HFAdapter.save_pretrained(hf_model, hf_proxy_dir, tokenizer=hf_tokenizer)
|
||||||
|
|
||||||
|
print(f"✅ Модель сохранена в HF формате:")
|
||||||
|
print(f" - {PATHS['hf_model']}: стандартный HF формат")
|
||||||
|
print(f" - {hf_proxy_dir}: через hf-proxy")
|
||||||
|
print(f" - {hf_tokenizer_dir}: токенизатор в HF формате")
|
||||||
|
|
||||||
|
# === Тестирование генерации ===
|
||||||
|
print(f"\n🧪 Тестирование генерации после обучения...")
|
||||||
|
hf_model.eval()
|
||||||
|
|
||||||
|
for prompt in TEST_PROMPTS[:3]:
|
||||||
|
print(f"\n🔤 Промпт: '{prompt}'")
|
||||||
|
|
||||||
|
try:
|
||||||
|
inputs = hf_tokenizer(prompt, return_tensors="pt")
|
||||||
|
|
||||||
|
with torch.no_grad():
|
||||||
|
generated = hf_model.generate(
|
||||||
|
input_ids=inputs['input_ids'],
|
||||||
|
max_new_tokens=20,
|
||||||
|
do_sample=True,
|
||||||
|
temperature=0.8
|
||||||
|
)
|
||||||
|
|
||||||
|
generated_text = hf_tokenizer.decode(generated[0], skip_special_tokens=True)
|
||||||
|
print(f"🎯 Результат: '{generated_text}'")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Ошибка генерации: {e}")
|
||||||
|
|
||||||
|
# === Сохранение результатов ===
|
||||||
|
results = {
|
||||||
|
"experiment": experiment_name,
|
||||||
|
"model_config": model_config,
|
||||||
|
"training_config": TRAINING_CONFIG,
|
||||||
|
"final_loss": train_result.metrics.get('train_loss', 'N/A'),
|
||||||
|
"eval_loss": train_result.metrics.get('eval_loss', 'N/A')
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.save_logs("checkpoints/hf_integration_training_logs.json")
|
||||||
|
|
||||||
|
print(f"\n🎉 Эксперимент с HF интеграцией завершен успешно!")
|
||||||
|
print(f"\n💡 Для использования обученной модели:")
|
||||||
|
print(f" uv run python experiments/hf_integration/generate_with_hf_tools.py")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Ошибка в эксперименте: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
313
experiments/llm_only/generate_gpt_bpe.py
Normal file
313
experiments/llm_only/generate_gpt_bpe.py
Normal file
@@ -0,0 +1,313 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Experiment: generate_gpt_bpe.py
|
||||||
|
Description: Генерация текста обученной GPT моделью с BPE токенизатором.
|
||||||
|
Использует только библиотеку llm без зависимостей от HuggingFace.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import torch
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Добавляем путь к shared модулям
|
||||||
|
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
|
from llm.models.gpt import GPT
|
||||||
|
from llm.tokenizers import BPETokenizer
|
||||||
|
|
||||||
|
from shared.configs import (
|
||||||
|
BASE_GPT_CONFIG, TEST_PROMPTS, GENERATION_CONFIG, PATHS
|
||||||
|
)
|
||||||
|
from shared.data import (
|
||||||
|
print_experiment_info, ensure_directories, ExperimentLogger
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def load_model_and_tokenizer() -> tuple:
|
||||||
|
"""
|
||||||
|
Загружает обученную модель и токенизатор.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple: (модель, токенизатор, конфигурация)
|
||||||
|
"""
|
||||||
|
# Проверяем существование файлов
|
||||||
|
if not os.path.exists(PATHS["gpt_bpe_model"]):
|
||||||
|
raise FileNotFoundError(
|
||||||
|
f"Модель не найдена: {PATHS['gpt_bpe_model']}\n"
|
||||||
|
f"Сначала обучите модель: uv run python experiments/llm_only/train_gpt_bpe.py"
|
||||||
|
)
|
||||||
|
|
||||||
|
if not os.path.exists(PATHS["bpe_tokenizer"]):
|
||||||
|
raise FileNotFoundError(
|
||||||
|
f"Токенизатор не найден: {PATHS['bpe_tokenizer']}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Загружаем конфигурацию модели
|
||||||
|
import json
|
||||||
|
with open(PATHS["gpt_bpe_config"], 'r', encoding='utf-8') as f:
|
||||||
|
model_config = json.load(f)
|
||||||
|
|
||||||
|
# Загружаем токенизатор
|
||||||
|
print("🔧 Загрузка BPE токенизатора...")
|
||||||
|
tokenizer = BPETokenizer.load(PATHS["bpe_tokenizer"])
|
||||||
|
print(f"✅ Токенизатор загружен (vocab_size={tokenizer.get_vocab_size()})")
|
||||||
|
|
||||||
|
# Загружаем модель
|
||||||
|
print("🔧 Загрузка GPT модели...")
|
||||||
|
model = GPT(model_config)
|
||||||
|
model.load_state_dict(torch.load(PATHS["gpt_bpe_model"], map_location='cpu'))
|
||||||
|
model.eval()
|
||||||
|
print("✅ Модель загружена")
|
||||||
|
|
||||||
|
return model, tokenizer, model_config
|
||||||
|
|
||||||
|
|
||||||
|
def generate_text(
|
||||||
|
model: GPT,
|
||||||
|
tokenizer: BPETokenizer,
|
||||||
|
prompt: str,
|
||||||
|
config: dict
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
Генерирует текст на основе промпта.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
model: Обученная GPT модель
|
||||||
|
tokenizer: BPE токенизатор
|
||||||
|
prompt: Входной текст
|
||||||
|
config: Конфигурация генерации
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Сгенерированный текст
|
||||||
|
"""
|
||||||
|
print(f"🔤 Промпт: '{prompt}'")
|
||||||
|
print(f"📊 Параметры: max_tokens={config['max_new_tokens']}, "
|
||||||
|
f"temp={config['temperature']}, sample={config['do_sample']}")
|
||||||
|
|
||||||
|
# Кодируем промпт
|
||||||
|
input_ids = tokenizer.encode(prompt, add_special_tokens=False)
|
||||||
|
input_tensor = torch.tensor([input_ids], dtype=torch.long)
|
||||||
|
|
||||||
|
print(f"🎯 Токены промпта: {input_ids}")
|
||||||
|
print(f"🎯 Токены (текст): {tokenizer.tokenize(prompt)}")
|
||||||
|
print("🔄 Генерация...")
|
||||||
|
|
||||||
|
# Генерируем текст
|
||||||
|
with torch.no_grad():
|
||||||
|
generated_ids = model.generate(
|
||||||
|
x=input_tensor,
|
||||||
|
max_new_tokens=config["max_new_tokens"],
|
||||||
|
do_sample=config["do_sample"],
|
||||||
|
temperature=config["temperature"],
|
||||||
|
top_k=config["top_k"],
|
||||||
|
top_p=config["top_p"]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Декодируем результат
|
||||||
|
generated_text = tokenizer.decode(generated_ids[0].tolist())
|
||||||
|
|
||||||
|
return generated_text
|
||||||
|
|
||||||
|
|
||||||
|
def test_different_strategies(model: GPT, tokenizer: BPETokenizer, prompt: str):
|
||||||
|
"""
|
||||||
|
Тестирует разные стратегии генерации на одном промпте.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
model: Обученная модель
|
||||||
|
tokenizer: BPE токенизатор
|
||||||
|
prompt: Тестовый промпт
|
||||||
|
"""
|
||||||
|
print(f"\n🎭 Сравнение стратегий генерации для промпта: '{prompt}'")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
strategies = [
|
||||||
|
{"name": "🎯 Жадный поиск", "do_sample": False, "temperature": 1.0},
|
||||||
|
{"name": "🎲 Вероятностная (temp=0.7)", "do_sample": True, "temperature": 0.7},
|
||||||
|
{"name": "🔥 Случайная (temp=1.2)", "do_sample": True, "temperature": 1.2},
|
||||||
|
{"name": "❄️ Детерминированная (temp=0.3)", "do_sample": True, "temperature": 0.3},
|
||||||
|
]
|
||||||
|
|
||||||
|
for strategy in strategies:
|
||||||
|
print(f"\n{strategy['name']}:")
|
||||||
|
try:
|
||||||
|
config = GENERATION_CONFIG.copy()
|
||||||
|
config.update({
|
||||||
|
"do_sample": strategy["do_sample"],
|
||||||
|
"temperature": strategy["temperature"],
|
||||||
|
"max_new_tokens": 20
|
||||||
|
})
|
||||||
|
|
||||||
|
generated = generate_text(model, tokenizer, prompt, config)
|
||||||
|
|
||||||
|
# Выделяем сгенерированную часть
|
||||||
|
generated_part = generated[len(prompt):]
|
||||||
|
print(f" 📤 Промпт: '{prompt}'")
|
||||||
|
print(f" 🎯 Сгенерировано: '{generated_part}'")
|
||||||
|
print(f" 📄 Полный текст: '{generated}'")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ❌ Ошибка: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def analyze_tokenization(tokenizer: BPETokenizer, texts: list):
|
||||||
|
"""
|
||||||
|
Анализирует токенизацию различных текстов.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
tokenizer: BPE токенизатор
|
||||||
|
texts: Список текстов для анализа
|
||||||
|
"""
|
||||||
|
print(f"\n🔍 Анализ токенизации BPE:")
|
||||||
|
print("=" * 50)
|
||||||
|
|
||||||
|
for i, text in enumerate(texts):
|
||||||
|
print(f"\nТекст {i+1}: '{text}'")
|
||||||
|
|
||||||
|
# Токенизация
|
||||||
|
tokens = tokenizer.encode(text, add_special_tokens=False)
|
||||||
|
token_strings = tokenizer.tokenize(text)
|
||||||
|
|
||||||
|
print(f" Токены (ID): {tokens}")
|
||||||
|
print(f" Токены (текст): {token_strings}")
|
||||||
|
print(f" Количество токенов: {len(tokens)}")
|
||||||
|
print(f" Эффективность: {len(text)} символов → {len(tokens)} токенов")
|
||||||
|
|
||||||
|
# Декодирование обратно
|
||||||
|
decoded = tokenizer.decode(tokens)
|
||||||
|
if text == decoded:
|
||||||
|
print(f" ✅ Декодирование корректно")
|
||||||
|
else:
|
||||||
|
print(f" ⚠️ Расхождения: '{decoded}'")
|
||||||
|
|
||||||
|
|
||||||
|
def interactive_generation(model: GPT, tokenizer: BPETokenizer):
|
||||||
|
"""
|
||||||
|
Режим интерактивной генерации.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
model: Обученная модель
|
||||||
|
tokenizer: BPE токенизатор
|
||||||
|
"""
|
||||||
|
print(f"\n💬 Интерактивная генерация (для выхода введите 'exit')")
|
||||||
|
print("-" * 50)
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
user_input = input("\n🔤 Введите промпт: ").strip()
|
||||||
|
|
||||||
|
if user_input.lower() in ['exit', 'quit', 'выход']:
|
||||||
|
break
|
||||||
|
|
||||||
|
if not user_input:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Запрашиваем параметры
|
||||||
|
try:
|
||||||
|
max_tokens = int(input("📏 Макс. токенов [50]: ") or "50")
|
||||||
|
temperature = float(input("🌡️ Температура [0.7]: ") or "0.7")
|
||||||
|
do_sample_input = input("🎲 Сэмплирование (y/n) [y]: ").lower()
|
||||||
|
do_sample = do_sample_input != 'n'
|
||||||
|
except:
|
||||||
|
max_tokens = 50
|
||||||
|
temperature = 0.7
|
||||||
|
do_sample = True
|
||||||
|
print("⚠️ Использую параметры по умолчанию")
|
||||||
|
|
||||||
|
config = GENERATION_CONFIG.copy()
|
||||||
|
config.update({
|
||||||
|
"max_new_tokens": max_tokens,
|
||||||
|
"temperature": temperature,
|
||||||
|
"do_sample": do_sample
|
||||||
|
})
|
||||||
|
|
||||||
|
generated = generate_text(model, tokenizer, user_input, config)
|
||||||
|
|
||||||
|
generated_part = generated[len(user_input):]
|
||||||
|
print(f"\n🎯 Результат:")
|
||||||
|
print(f" 📤 Промпт: '{user_input}'")
|
||||||
|
print(f" 🎯 Сгенерировано: '{generated_part}'")
|
||||||
|
print(f" 📄 Полный текст: '{generated}'")
|
||||||
|
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\n👋 Завершение работы...")
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Ошибка: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Основная функция эксперимента."""
|
||||||
|
# === Настройка эксперимента ===
|
||||||
|
experiment_name = "Генерация текста GPT + BPE (только llm)"
|
||||||
|
experiment_config = {
|
||||||
|
"model": "GPT с BPE токенизатором",
|
||||||
|
"стратегия": "автономная генерация",
|
||||||
|
"вход": "промпты",
|
||||||
|
"выход": "сгенерированный текст"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_experiment_info(experiment_name, experiment_config)
|
||||||
|
ensure_directories()
|
||||||
|
logger = ExperimentLogger(experiment_name)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Загружаем модель и токенизатор
|
||||||
|
model, tokenizer, model_config = load_model_and_tokenizer()
|
||||||
|
|
||||||
|
# === Анализ токенизации ===
|
||||||
|
analysis_texts = [
|
||||||
|
"Искусственный интеллект",
|
||||||
|
"Нейронные сети",
|
||||||
|
"Машинное обучение",
|
||||||
|
]
|
||||||
|
analyze_tokenization(tokenizer, analysis_texts)
|
||||||
|
|
||||||
|
# === Генерация с разными промптами ===
|
||||||
|
print(f"\n🎯 Генерация текста с разными промптами")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
for i, prompt in enumerate(TEST_PROMPTS):
|
||||||
|
print(f"\n📝 Пример {i+1}/{len(TEST_PROMPTS)}")
|
||||||
|
print("-" * 40)
|
||||||
|
|
||||||
|
try:
|
||||||
|
generated = generate_text(model, tokenizer, prompt, GENERATION_CONFIG)
|
||||||
|
|
||||||
|
# Выделяем сгенерированную часть
|
||||||
|
generated_part = generated[len(prompt):]
|
||||||
|
|
||||||
|
print(f"📤 Промпт: '{prompt}'")
|
||||||
|
print(f"🎯 Сгенерировано: '{generated_part}'")
|
||||||
|
print(f"📄 Полный текст: '{generated}'")
|
||||||
|
print(f"📏 Длина: {len(generated)} символов")
|
||||||
|
|
||||||
|
# Логируем успешную генерацию
|
||||||
|
logger.log_metric(f"generation_length_{i}", len(generated))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Ошибка при генерации: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# === Сравнение стратегий генерации ===
|
||||||
|
test_prompt = "Искусственный"
|
||||||
|
test_different_strategies(model, tokenizer, test_prompt)
|
||||||
|
|
||||||
|
# === Интерактивная генерация ===
|
||||||
|
interactive_generation(model, tokenizer)
|
||||||
|
|
||||||
|
# === Сохранение результатов ===
|
||||||
|
logger.save_logs("checkpoints/llm_only_generation_logs.json")
|
||||||
|
|
||||||
|
print(f"\n🎉 Эксперимент генерации завершен успешно!")
|
||||||
|
|
||||||
|
except FileNotFoundError as e:
|
||||||
|
print(f"❌ {e}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Ошибка в эксперименте: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
231
experiments/llm_only/train_gpt_bpe.py
Normal file
231
experiments/llm_only/train_gpt_bpe.py
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Experiment: train_gpt_bpe.py
|
||||||
|
Description: Обучение GPT модели с собственным BPE токенизатором.
|
||||||
|
Использует только библиотеку llm без зависимостей от HuggingFace.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import torch
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Добавляем путь к shared модулям
|
||||||
|
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
|
from llm.models.gpt import GPT
|
||||||
|
from llm.tokenizers import BPETokenizer
|
||||||
|
from llm.training.dataset import TextDataset
|
||||||
|
from llm.training.trainer import Trainer
|
||||||
|
|
||||||
|
from shared.configs import (
|
||||||
|
TRAIN_TEXTS, BASE_GPT_CONFIG, BPE_CONFIG,
|
||||||
|
TRAINING_CONFIG, PATHS, TEST_PROMPTS
|
||||||
|
)
|
||||||
|
from shared.data import (
|
||||||
|
load_training_data, ensure_directories,
|
||||||
|
print_experiment_info, ExperimentLogger
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def train_bpe_tokenizer(texts: list, config: dict) -> BPETokenizer:
|
||||||
|
"""
|
||||||
|
Обучает BPE токенизатор на текстах.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
texts: Список текстов для обучения
|
||||||
|
config: Конфигурация токенизатора
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
BPETokenizer: Обученный токенизатор
|
||||||
|
"""
|
||||||
|
print("🔧 Обучение BPE токенизатора...")
|
||||||
|
|
||||||
|
tokenizer = BPETokenizer()
|
||||||
|
tokenizer.train(
|
||||||
|
texts=texts,
|
||||||
|
vocab_size=config["vocab_size"],
|
||||||
|
special_tokens=config["special_tokens"]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Сохраняем токенизатор
|
||||||
|
os.makedirs(os.path.dirname(PATHS["bpe_tokenizer"]), exist_ok=True)
|
||||||
|
tokenizer.save(PATHS["bpe_tokenizer"])
|
||||||
|
|
||||||
|
print(f"✅ BPE токенизатор обучен и сохранен: {PATHS['bpe_tokenizer']}")
|
||||||
|
print(f"📊 Размер словаря: {tokenizer.get_vocab_size()}")
|
||||||
|
|
||||||
|
return tokenizer
|
||||||
|
|
||||||
|
|
||||||
|
def test_tokenizer(tokenizer: BPETokenizer, texts: list):
|
||||||
|
"""
|
||||||
|
Тестирует токенизатор на примерах.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
tokenizer: Обученный токенизатор
|
||||||
|
texts: Список тестовых текстов
|
||||||
|
"""
|
||||||
|
print("\n🧪 Тестирование токенизатора:")
|
||||||
|
|
||||||
|
for i, text in enumerate(texts[:3]):
|
||||||
|
print(f"\nПример {i+1}:")
|
||||||
|
print(f" Исходный текст: '{text}'")
|
||||||
|
|
||||||
|
# Кодирование
|
||||||
|
tokens = tokenizer.encode(text)
|
||||||
|
token_strings = tokenizer.tokenize(text)
|
||||||
|
|
||||||
|
print(f" Токены (ID): {tokens}")
|
||||||
|
print(f" Токены (текст): {token_strings}")
|
||||||
|
print(f" Количество токенов: {len(tokens)}")
|
||||||
|
|
||||||
|
# Декодирование
|
||||||
|
decoded = tokenizer.decode(tokens)
|
||||||
|
print(f" Декодированный: '{decoded}'")
|
||||||
|
|
||||||
|
if text == decoded:
|
||||||
|
print(" ✅ Кодирование/декодирование корректно")
|
||||||
|
else:
|
||||||
|
print(" ⚠️ Небольшие расхождения")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Основная функция эксперимента."""
|
||||||
|
# === Настройка эксперимента ===
|
||||||
|
experiment_name = "Обучение GPT с BPE токенизатором (только llm)"
|
||||||
|
experiment_config = {
|
||||||
|
"model": "GPT",
|
||||||
|
"tokenizer": "BPE",
|
||||||
|
"vocab_size": BPE_CONFIG["vocab_size"],
|
||||||
|
"training_epochs": TRAINING_CONFIG["num_epochs"],
|
||||||
|
"batch_size": TRAINING_CONFIG["batch_size"],
|
||||||
|
"learning_rate": TRAINING_CONFIG["learning_rate"]
|
||||||
|
}
|
||||||
|
|
||||||
|
print_experiment_info(experiment_name, experiment_config)
|
||||||
|
ensure_directories()
|
||||||
|
logger = ExperimentLogger(experiment_name)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# === Подготовка данных ===
|
||||||
|
train_texts, val_texts = load_training_data()
|
||||||
|
print(f"📊 Данные: {len(train_texts)} train, {len(val_texts)} validation")
|
||||||
|
|
||||||
|
# === Обучение токенизатора ===
|
||||||
|
if os.path.exists(PATHS["bpe_tokenizer"]):
|
||||||
|
print("📝 Загрузка предварительно обученного токенизатора...")
|
||||||
|
tokenizer = BPETokenizer.load(PATHS["bpe_tokenizer"])
|
||||||
|
print(f"✅ Токенизатор загружен (vocab_size={tokenizer.get_vocab_size()})")
|
||||||
|
else:
|
||||||
|
tokenizer = train_bpe_tokenizer(TRAIN_TEXTS, BPE_CONFIG)
|
||||||
|
|
||||||
|
# Тестируем токенизатор
|
||||||
|
test_tokenizer(tokenizer, TEST_PROMPTS[:3])
|
||||||
|
|
||||||
|
# === Инициализация модели ===
|
||||||
|
model_config = BASE_GPT_CONFIG.copy()
|
||||||
|
model_config["vocab_size"] = tokenizer.get_vocab_size()
|
||||||
|
|
||||||
|
print(f"\n🔧 Инициализация GPT модели...")
|
||||||
|
print(f" Размер словаря: {model_config['vocab_size']}")
|
||||||
|
print(f" Размер эмбеддингов: {model_config['embed_dim']}")
|
||||||
|
print(f" Количество слоев: {model_config['num_layers']}")
|
||||||
|
print(f" Количество голов внимания: {model_config['num_heads']}")
|
||||||
|
|
||||||
|
model = GPT(model_config)
|
||||||
|
|
||||||
|
# === Подготовка датасета ===
|
||||||
|
print(f"\n📊 Подготовка датасета...")
|
||||||
|
train_dataset = TextDataset(
|
||||||
|
train_texts,
|
||||||
|
tokenizer,
|
||||||
|
block_size=model_config["max_position_embeddings"]
|
||||||
|
)
|
||||||
|
print(f" Размер train датасета: {len(train_dataset)} примеров")
|
||||||
|
|
||||||
|
# === Обучение модели ===
|
||||||
|
print(f"\n🎯 Начало обучения GPT модели...")
|
||||||
|
|
||||||
|
trainer = Trainer(
|
||||||
|
model=model,
|
||||||
|
train_dataset=train_dataset,
|
||||||
|
lr=TRAINING_CONFIG["learning_rate"],
|
||||||
|
batch_size=TRAINING_CONFIG["batch_size"],
|
||||||
|
num_epochs=TRAINING_CONFIG["num_epochs"],
|
||||||
|
warmup_steps=TRAINING_CONFIG["warmup_steps"]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Запускаем обучение
|
||||||
|
trainer.train()
|
||||||
|
|
||||||
|
# === Сохранение модели ===
|
||||||
|
print(f"\n💾 Сохранение модели...")
|
||||||
|
os.makedirs(os.path.dirname(PATHS["gpt_bpe_model"]), exist_ok=True)
|
||||||
|
|
||||||
|
# Сохраняем модель
|
||||||
|
torch.save(model.state_dict(), PATHS["gpt_bpe_model"])
|
||||||
|
|
||||||
|
# Сохраняем конфигурацию
|
||||||
|
import json
|
||||||
|
with open(PATHS["gpt_bpe_config"], 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(model_config, f, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
print(f"✅ Модель сохранена:")
|
||||||
|
print(f" - {PATHS['gpt_bpe_model']}: веса модели")
|
||||||
|
print(f" - {PATHS['gpt_bpe_config']}: конфигурация модели")
|
||||||
|
print(f" - {PATHS['bpe_tokenizer']}: токенизатор")
|
||||||
|
|
||||||
|
# === Тестирование генерации ===
|
||||||
|
print(f"\n🧪 Тестирование генерации текста...")
|
||||||
|
model.eval()
|
||||||
|
|
||||||
|
for prompt in TEST_PROMPTS[:3]:
|
||||||
|
print(f"\n🔤 Промпт: '{prompt}'")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Кодируем промпт
|
||||||
|
input_ids = tokenizer.encode(prompt, add_special_tokens=False)
|
||||||
|
input_tensor = torch.tensor([input_ids], dtype=torch.long)
|
||||||
|
|
||||||
|
# Генерируем текст
|
||||||
|
with torch.no_grad():
|
||||||
|
generated_ids = model.generate(
|
||||||
|
x=input_tensor,
|
||||||
|
max_new_tokens=20,
|
||||||
|
do_sample=True,
|
||||||
|
temperature=0.8
|
||||||
|
)
|
||||||
|
|
||||||
|
# Декодируем результат
|
||||||
|
generated_text = tokenizer.decode(generated_ids[0].tolist())
|
||||||
|
generated_part = generated_text[len(prompt):]
|
||||||
|
|
||||||
|
print(f"🎯 Сгенерировано: '{generated_part}'")
|
||||||
|
print(f"📄 Полный текст: '{generated_text}'")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Ошибка генерации: {e}")
|
||||||
|
|
||||||
|
# === Сохранение результатов ===
|
||||||
|
results = {
|
||||||
|
"experiment": experiment_name,
|
||||||
|
"model_config": model_config,
|
||||||
|
"training_config": TRAINING_CONFIG,
|
||||||
|
"tokenizer_vocab_size": tokenizer.get_vocab_size(),
|
||||||
|
"final_loss": "см. логи обучения" # В реальном эксперименте можно сохранить final loss
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.save_logs("checkpoints/llm_only_training_logs.json")
|
||||||
|
|
||||||
|
print(f"\n🎉 Эксперимент завершен успешно!")
|
||||||
|
print(f"\n💡 Для использования обученной модели:")
|
||||||
|
print(f" uv run python experiments/llm_only/generate_gpt_bpe.py")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Ошибка в эксперименте: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
100
experiments/shared/configs.py
Normal file
100
experiments/shared/configs.py
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
"""
|
||||||
|
Общие конфигурации для экспериментов.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# === Данные для обучения ===
|
||||||
|
TRAIN_TEXTS = [
|
||||||
|
"Мир программирования прекрасен и удивителен.",
|
||||||
|
"GPT модели учатся предсказывать следующий токен в последовательности.",
|
||||||
|
"Трансформеры революционно изменили обработку естественного языка.",
|
||||||
|
"Обучение больших языковых моделей требует значительных вычислительных ресурсов и больших объемов данных.",
|
||||||
|
"Искусственный интеллект продолжает развиваться стремительными темпами.",
|
||||||
|
"Глубокое обучение позволяет решать сложные задачи компьютерного зрения.",
|
||||||
|
"Нейронные сети имитируют работу человеческого мозга.",
|
||||||
|
"Машинное обучение находит применение в различных областях науки и техники.",
|
||||||
|
"Python является одним из самых популярных языков программирования для анализа данных.",
|
||||||
|
"Обработка естественного языка позволяет компьютерам понимать человеческую речь.",
|
||||||
|
"Рекуррентные нейронные сети хорошо подходят для работы с последовательностями.",
|
||||||
|
"Сверточные нейронные сети эффективны для обработки изображений.",
|
||||||
|
"Обучение с подкреплением используется для создания игровых ИИ.",
|
||||||
|
"Генеративные состязательные сети могут создавать реалистичные изображения.",
|
||||||
|
"Автоэнкодеры используются для сжатия данных и обучения представлений.",
|
||||||
|
]
|
||||||
|
|
||||||
|
# === Конфигурации моделей ===
|
||||||
|
|
||||||
|
# Базовая конфигурация GPT
|
||||||
|
BASE_GPT_CONFIG = {
|
||||||
|
"vocab_size": None, # Будет установлен динамически
|
||||||
|
"embed_dim": 256,
|
||||||
|
"num_heads": 4,
|
||||||
|
"num_layers": 4,
|
||||||
|
"max_position_embeddings": 128,
|
||||||
|
"dropout": 0.1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Конфигурация для маленькой модели (быстрое тестирование)
|
||||||
|
SMALL_GPT_CONFIG = {
|
||||||
|
"vocab_size": None,
|
||||||
|
"embed_dim": 128,
|
||||||
|
"num_heads": 2,
|
||||||
|
"num_layers": 2,
|
||||||
|
"max_position_embeddings": 64,
|
||||||
|
"dropout": 0.1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Конфигурация для большой модели (качественное обучение)
|
||||||
|
LARGE_GPT_CONFIG = {
|
||||||
|
"vocab_size": None,
|
||||||
|
"embed_dim": 512,
|
||||||
|
"num_heads": 8,
|
||||||
|
"num_layers": 6,
|
||||||
|
"max_position_embeddings": 256,
|
||||||
|
"dropout": 0.1
|
||||||
|
}
|
||||||
|
|
||||||
|
# === Конфигурации токенизатора ===
|
||||||
|
BPE_CONFIG = {
|
||||||
|
"vocab_size": 1000,
|
||||||
|
"special_tokens": ["<pad>", "<unk>", "<bos>", "<eos>"]
|
||||||
|
}
|
||||||
|
|
||||||
|
# === Конфигурации обучения ===
|
||||||
|
TRAINING_CONFIG = {
|
||||||
|
"learning_rate": 3e-4,
|
||||||
|
"batch_size": 2,
|
||||||
|
"num_epochs": 3,
|
||||||
|
"warmup_steps": 50,
|
||||||
|
"gradient_clip": 1.0
|
||||||
|
}
|
||||||
|
|
||||||
|
# === Конфигурации генерации ===
|
||||||
|
GENERATION_CONFIG = {
|
||||||
|
"max_new_tokens": 50,
|
||||||
|
"temperature": 0.7,
|
||||||
|
"do_sample": True,
|
||||||
|
"top_k": None,
|
||||||
|
"top_p": None
|
||||||
|
}
|
||||||
|
|
||||||
|
# === Пути для сохранения ===
|
||||||
|
PATHS = {
|
||||||
|
"bpe_tokenizer": "checkpoints/bpe_tokenizer.json",
|
||||||
|
"gpt_bpe_model": "checkpoints/gpt-bpe/model.pt",
|
||||||
|
"gpt_bpe_config": "checkpoints/gpt-bpe/config.json",
|
||||||
|
"hf_tokenizer": "checkpoints/hf-bpe-tokenizer",
|
||||||
|
"hf_model": "checkpoints/hf-trained",
|
||||||
|
"hf_proxy_model": "checkpoints/hf-trained-proxy"
|
||||||
|
}
|
||||||
|
|
||||||
|
# === Тестовые промпты ===
|
||||||
|
TEST_PROMPTS = [
|
||||||
|
"Искусственный",
|
||||||
|
"Нейронные",
|
||||||
|
"Машинное",
|
||||||
|
"Глубокое",
|
||||||
|
"Python",
|
||||||
|
"Трансформеры",
|
||||||
|
"Обучение",
|
||||||
|
"Программирование",
|
||||||
|
]
|
||||||
162
experiments/shared/data.py
Normal file
162
experiments/shared/data.py
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
"""
|
||||||
|
Общие утилиты для работы с данными в экспериментах.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
from typing import List, Tuple
|
||||||
|
from .configs import TRAIN_TEXTS, PATHS
|
||||||
|
|
||||||
|
|
||||||
|
def load_training_data(split_ratio: float = 0.8) -> Tuple[List[str], List[str]]:
|
||||||
|
"""
|
||||||
|
Загружает данные для обучения и разделяет на train/validation.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
split_ratio: Доля данных для обучения
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple: (train_texts, val_texts)
|
||||||
|
"""
|
||||||
|
train_size = int(len(TRAIN_TEXTS) * split_ratio)
|
||||||
|
train_data = TRAIN_TEXTS[:train_size]
|
||||||
|
val_data = TRAIN_TEXTS[train_size:]
|
||||||
|
|
||||||
|
return train_data, val_data
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_directories():
|
||||||
|
"""Создает необходимые директории если они не существуют."""
|
||||||
|
directories = [
|
||||||
|
"checkpoints",
|
||||||
|
"checkpoints/gpt-bpe",
|
||||||
|
"checkpoints/hf-bpe-tokenizer",
|
||||||
|
"checkpoints/hf-trained",
|
||||||
|
"checkpoints/hf-trained-proxy",
|
||||||
|
"logs"
|
||||||
|
]
|
||||||
|
|
||||||
|
for directory in directories:
|
||||||
|
os.makedirs(directory, exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
def get_model_paths(experiment_type: str = "llm_only") -> dict:
|
||||||
|
"""
|
||||||
|
Возвращает пути для конкретного типа эксперимента.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
experiment_type: Тип эксперимента ('llm_only' или 'hf_integration')
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Словарь с путями
|
||||||
|
"""
|
||||||
|
base_paths = PATHS.copy()
|
||||||
|
|
||||||
|
if experiment_type == "hf_integration":
|
||||||
|
base_paths.update({
|
||||||
|
"model": base_paths["hf_model"],
|
||||||
|
"tokenizer": base_paths["hf_tokenizer"]
|
||||||
|
})
|
||||||
|
else: # llm_only
|
||||||
|
base_paths.update({
|
||||||
|
"model": base_paths["gpt_bpe_model"],
|
||||||
|
"tokenizer": base_paths["bpe_tokenizer"]
|
||||||
|
})
|
||||||
|
|
||||||
|
return base_paths
|
||||||
|
|
||||||
|
|
||||||
|
def print_experiment_info(experiment_name: str, config: dict):
|
||||||
|
"""
|
||||||
|
Выводит информацию о запускаемом эксперименте.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
experiment_name: Название эксперимента
|
||||||
|
config: Конфигурация эксперимента
|
||||||
|
"""
|
||||||
|
print("=" * 70)
|
||||||
|
print(f"🚀 Эксперимент: {experiment_name}")
|
||||||
|
print("=" * 70)
|
||||||
|
print("📊 Конфигурация:")
|
||||||
|
for key, value in config.items():
|
||||||
|
print(f" {key}: {value}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
|
||||||
|
def save_experiment_results(results: dict, filepath: str):
|
||||||
|
"""
|
||||||
|
Сохраняет результаты эксперимента в файл.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
results: Словарь с результатами
|
||||||
|
filepath: Путь для сохранения
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
|
||||||
|
with open(filepath, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(results, f, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
|
print(f"✅ Результаты эксперимента сохранены: {filepath}")
|
||||||
|
|
||||||
|
|
||||||
|
def load_experiment_results(filepath: str) -> dict:
|
||||||
|
"""
|
||||||
|
Загружает результаты эксперимента из файла.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
filepath: Путь к файлу с результатами
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Загруженные результаты
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
|
||||||
|
if not os.path.exists(filepath):
|
||||||
|
return {}
|
||||||
|
|
||||||
|
with open(filepath, 'r', encoding='utf-8') as f:
|
||||||
|
return json.load(f)
|
||||||
|
|
||||||
|
|
||||||
|
class ExperimentLogger:
|
||||||
|
"""
|
||||||
|
Логгер для экспериментов.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, experiment_name: str):
|
||||||
|
self.experiment_name = experiment_name
|
||||||
|
self.metrics = {}
|
||||||
|
|
||||||
|
def log_metric(self, name: str, value: float):
|
||||||
|
"""Логирует метрику."""
|
||||||
|
if name not in self.metrics:
|
||||||
|
self.metrics[name] = []
|
||||||
|
self.metrics[name].append(value)
|
||||||
|
print(f"📈 {name}: {value:.4f}")
|
||||||
|
|
||||||
|
def log_step(self, step: int, loss: float, **kwargs):
|
||||||
|
"""Логирует шаг обучения."""
|
||||||
|
print(f"📊 Step {step}: loss={loss:.4f}", end="")
|
||||||
|
for key, value in kwargs.items():
|
||||||
|
print(f", {key}={value:.4f}", end="")
|
||||||
|
print()
|
||||||
|
|
||||||
|
def log_epoch(self, epoch: int, train_loss: float, val_loss: float = None):
|
||||||
|
"""Логирует завершение эпохи."""
|
||||||
|
print(f"🎯 Epoch {epoch}: train_loss={train_loss:.4f}", end="")
|
||||||
|
if val_loss is not None:
|
||||||
|
print(f", val_loss={val_loss:.4f}", end="")
|
||||||
|
print()
|
||||||
|
|
||||||
|
def save_logs(self, filepath: str):
|
||||||
|
"""Сохраняет логи эксперимента."""
|
||||||
|
import json
|
||||||
|
|
||||||
|
logs = {
|
||||||
|
"experiment_name": self.experiment_name,
|
||||||
|
"metrics": self.metrics
|
||||||
|
}
|
||||||
|
|
||||||
|
with open(filepath, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(logs, f, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
|
print(f"✅ Логи эксперимента сохранены: {filepath}")
|
||||||
10
hf-proxy/.gitignore
vendored
Normal file
10
hf-proxy/.gitignore
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# Python-generated files
|
||||||
|
__pycache__/
|
||||||
|
*.py[oc]
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
|
wheels/
|
||||||
|
*.egg-info
|
||||||
|
|
||||||
|
# Virtual environments
|
||||||
|
.venv
|
||||||
1
hf-proxy/.python-version
Normal file
1
hf-proxy/.python-version
Normal file
@@ -0,0 +1 @@
|
|||||||
|
3.10
|
||||||
0
hf-proxy/README.md
Normal file
0
hf-proxy/README.md
Normal file
18
hf-proxy/pyproject.toml
Normal file
18
hf-proxy/pyproject.toml
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
[project]
|
||||||
|
name = "hf-proxy"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "HuggingFace adapter for custom LLM models"
|
||||||
|
readme = "README.md"
|
||||||
|
authors = [
|
||||||
|
{ name = "Sergey Penkovsky", email = "sergey.penkovsky@gmail.com" }
|
||||||
|
]
|
||||||
|
requires-python = ">=3.10"
|
||||||
|
dependencies = [
|
||||||
|
"torch>=2.3.0",
|
||||||
|
"transformers>=4.44.0",
|
||||||
|
"datasets>=2.20.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["uv_build>=0.8.22,<0.9.0"]
|
||||||
|
build-backend = "uv_build"
|
||||||
44
hf-proxy/src/hf_proxy/__init__.py
Normal file
44
hf-proxy/src/hf_proxy/__init__.py
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
"""
|
||||||
|
HF-Proxy: Адаптер для интеграции моделей llm с HuggingFace Transformers.
|
||||||
|
|
||||||
|
Этот пакет предоставляет инструменты для:
|
||||||
|
- Конвертации кастомных LLM моделей в формат HuggingFace
|
||||||
|
- Использования моделей через стандартные интерфейсы Transformers
|
||||||
|
- Загрузки моделей в HuggingFace Hub
|
||||||
|
- Создания pipelines для генерации текста
|
||||||
|
|
||||||
|
Основные классы:
|
||||||
|
- HFAdapter: Главный адаптер для преобразования моделей
|
||||||
|
- HFGPTAdapter: Адаптер для GPT моделей
|
||||||
|
- HFUtils: Утилиты для работы с адаптером
|
||||||
|
- HFTokenizerAdapter: Адаптер для кастомных токенизаторов
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .hf_adapter import HFAdapter, HFGPTAdapter
|
||||||
|
from .hf_config import HFAdapterConfig, HFPretrainedConfig
|
||||||
|
from .hf_utils import HFUtils, TokenizerWrapper, create_hf_pipeline
|
||||||
|
from .hf_tokenizer import HFTokenizerAdapter, create_hf_tokenizer, convert_to_hf_format
|
||||||
|
|
||||||
|
__version__ = "0.2.0"
|
||||||
|
__author__ = "Sergey Penkovsky"
|
||||||
|
__email__ = "sergey.penkovsky@gmail.com"
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
# Основные классы адаптера
|
||||||
|
"HFAdapter",
|
||||||
|
"HFGPTAdapter",
|
||||||
|
|
||||||
|
# Конфигурации
|
||||||
|
"HFAdapterConfig",
|
||||||
|
"HFPretrainedConfig",
|
||||||
|
|
||||||
|
# Адаптеры токенизаторов
|
||||||
|
"HFTokenizerAdapter",
|
||||||
|
"create_hf_tokenizer",
|
||||||
|
"convert_to_hf_format",
|
||||||
|
|
||||||
|
# Утилиты
|
||||||
|
"HFUtils",
|
||||||
|
"TokenizerWrapper",
|
||||||
|
"create_hf_pipeline",
|
||||||
|
]
|
||||||
299
hf-proxy/src/hf_proxy/hf_adapter.py
Normal file
299
hf-proxy/src/hf_proxy/hf_adapter.py
Normal file
@@ -0,0 +1,299 @@
|
|||||||
|
"""
|
||||||
|
Адаптер для интеграции моделей llm с HuggingFace Transformers.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import torch
|
||||||
|
import torch.nn as nn
|
||||||
|
from typing import Optional, Tuple, Union, List
|
||||||
|
from transformers import (
|
||||||
|
PreTrainedModel,
|
||||||
|
GPT2LMHeadModel,
|
||||||
|
GPT2Config,
|
||||||
|
GenerationConfig,
|
||||||
|
LogitsProcessorList,
|
||||||
|
StoppingCriteriaList
|
||||||
|
)
|
||||||
|
from transformers.modeling_outputs import CausalLMOutputWithCrossAttentions
|
||||||
|
|
||||||
|
from .hf_config import HFAdapterConfig, HFPretrainedConfig
|
||||||
|
from llm.models.gpt import GPT
|
||||||
|
|
||||||
|
|
||||||
|
class HFGPTAdapter(PreTrainedModel):
|
||||||
|
"""
|
||||||
|
Адаптер для модели GPT из библиотеки llm.
|
||||||
|
Позволяет использовать кастомные GPT модели с HuggingFace Transformers.
|
||||||
|
"""
|
||||||
|
config_class = HFPretrainedConfig
|
||||||
|
|
||||||
|
def __init__(self, config: HFPretrainedConfig, llm_model: Optional[GPT] = None):
|
||||||
|
"""
|
||||||
|
Инициализация адаптера.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
config: Конфигурация HuggingFace
|
||||||
|
llm_model: Опционально, предварительно созданная модель llm
|
||||||
|
"""
|
||||||
|
super().__init__(config)
|
||||||
|
|
||||||
|
# Преобразуем HF конфигурацию в формат llm
|
||||||
|
llm_config = self._hf_to_llm_config(config)
|
||||||
|
|
||||||
|
# Создаем или используем переданную модель
|
||||||
|
if llm_model is None:
|
||||||
|
self.llm_model = GPT(llm_config)
|
||||||
|
else:
|
||||||
|
self.llm_model = llm_model
|
||||||
|
|
||||||
|
# Устанавливаем веса если они есть в конфигурации
|
||||||
|
if hasattr(config, 'state_dict') and config.state_dict is not None:
|
||||||
|
self.llm_model.load_state_dict(config.state_dict)
|
||||||
|
|
||||||
|
def _hf_to_llm_config(self, hf_config: HFPretrainedConfig) -> dict:
|
||||||
|
"""
|
||||||
|
Преобразует конфигурацию HF в формат llm.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
hf_config: Конфигурация HuggingFace
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Конфигурация для llm модели
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
"vocab_size": hf_config.vocab_size,
|
||||||
|
"embed_dim": hf_config.hidden_size,
|
||||||
|
"num_heads": hf_config.num_attention_heads,
|
||||||
|
"num_layers": hf_config.num_hidden_layers,
|
||||||
|
"max_position_embeddings": hf_config.max_position_embeddings,
|
||||||
|
"dropout": hf_config.hidden_dropout_prob,
|
||||||
|
}
|
||||||
|
|
||||||
|
def forward(
|
||||||
|
self,
|
||||||
|
input_ids: Optional[torch.Tensor] = None,
|
||||||
|
attention_mask: Optional[torch.Tensor] = None,
|
||||||
|
labels: Optional[torch.Tensor] = None,
|
||||||
|
past_key_values: Optional[Tuple[Tuple[torch.Tensor]]] = None,
|
||||||
|
use_cache: Optional[bool] = None,
|
||||||
|
output_attentions: Optional[bool] = None,
|
||||||
|
output_hidden_states: Optional[bool] = None,
|
||||||
|
return_dict: Optional[bool] = None,
|
||||||
|
**kwargs
|
||||||
|
) -> Union[Tuple, CausalLMOutputWithCrossAttentions]:
|
||||||
|
"""
|
||||||
|
Прямой проход модели.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
input_ids: Входные токены [batch_size, seq_len]
|
||||||
|
attention_mask: Маска внимания [batch_size, seq_len]
|
||||||
|
labels: Метки для вычисления loss [batch_size, seq_len]
|
||||||
|
past_key_values: Кешированные ключи и значения
|
||||||
|
use_cache: Использовать кеширование
|
||||||
|
output_attentions: Возвращать веса внимания
|
||||||
|
output_hidden_states: Возвращать скрытые состояния
|
||||||
|
return_dict: Возвращать словарь вместо кортежа
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
CausalLMOutputWithCrossAttentions или кортеж
|
||||||
|
"""
|
||||||
|
return_dict = return_dict if return_dict is not None else self.config.use_return_dict
|
||||||
|
|
||||||
|
# Основной forward pass
|
||||||
|
logits = self.llm_model(input_ids)
|
||||||
|
|
||||||
|
loss = None
|
||||||
|
if labels is not None:
|
||||||
|
# Сдвигаем логиты и метки для языкового моделирования
|
||||||
|
shift_logits = logits[..., :-1, :].contiguous()
|
||||||
|
shift_labels = labels[..., 1:].contiguous()
|
||||||
|
|
||||||
|
# Вычисляем cross-entropy loss
|
||||||
|
loss_fct = nn.CrossEntropyLoss()
|
||||||
|
loss = loss_fct(
|
||||||
|
shift_logits.view(-1, shift_logits.size(-1)),
|
||||||
|
shift_labels.view(-1)
|
||||||
|
)
|
||||||
|
|
||||||
|
if not return_dict:
|
||||||
|
output = (logits,)
|
||||||
|
if loss is not None:
|
||||||
|
output = (loss,) + output
|
||||||
|
return output
|
||||||
|
|
||||||
|
return CausalLMOutputWithCrossAttentions(
|
||||||
|
loss=loss,
|
||||||
|
logits=logits,
|
||||||
|
past_key_values=None, # Наша модель пока не поддерживает кеширование
|
||||||
|
hidden_states=None,
|
||||||
|
attentions=None,
|
||||||
|
cross_attentions=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
def prepare_inputs_for_generation(
|
||||||
|
self,
|
||||||
|
input_ids: torch.Tensor,
|
||||||
|
past_key_values: Optional[Tuple] = None,
|
||||||
|
**kwargs
|
||||||
|
) -> dict:
|
||||||
|
"""
|
||||||
|
Подготавливает входные данные для генерации.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
input_ids: Входные токены
|
||||||
|
past_key_values: Кешированные ключи и значения
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Подготовленные входные данные
|
||||||
|
"""
|
||||||
|
# Наша простая реализация пока не поддерживает past_key_values
|
||||||
|
return {"input_ids": input_ids}
|
||||||
|
|
||||||
|
def can_generate(self) -> bool:
|
||||||
|
"""Проверяет, может ли модель генерировать текст."""
|
||||||
|
return True
|
||||||
|
|
||||||
|
def generate(
|
||||||
|
self,
|
||||||
|
input_ids: Optional[torch.Tensor] = None,
|
||||||
|
attention_mask: Optional[torch.Tensor] = None,
|
||||||
|
generation_config: Optional[GenerationConfig] = None,
|
||||||
|
logits_processor: Optional[LogitsProcessorList] = None,
|
||||||
|
stopping_criteria: Optional[StoppingCriteriaList] = None,
|
||||||
|
**kwargs
|
||||||
|
) -> torch.Tensor:
|
||||||
|
"""
|
||||||
|
Генерация текста с поддержкой HuggingFace интерфейса.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
input_ids: Входные токены
|
||||||
|
attention_mask: Маска внимания
|
||||||
|
generation_config: Конфигурация генерации
|
||||||
|
logits_processor: Процессоры логитов
|
||||||
|
stopping_criteria: Критерии остановки
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
torch.Tensor: Сгенерированные токены
|
||||||
|
"""
|
||||||
|
# Извлекаем обязательные параметры из kwargs или используем значения по умолчанию
|
||||||
|
max_new_tokens = kwargs.pop('max_new_tokens', 50)
|
||||||
|
do_sample = kwargs.pop('do_sample', True)
|
||||||
|
|
||||||
|
# Используем встроенную генерацию llm модели
|
||||||
|
return self.llm_model.generate(
|
||||||
|
x=input_ids,
|
||||||
|
max_new_tokens=max_new_tokens,
|
||||||
|
do_sample=do_sample,
|
||||||
|
attention_mask=attention_mask,
|
||||||
|
**kwargs
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class HFAdapter:
|
||||||
|
"""
|
||||||
|
Основной класс адаптера для преобразования моделей llm в формат HuggingFace.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def from_llm_model(
|
||||||
|
llm_model: GPT,
|
||||||
|
hf_config: Optional[HFAdapterConfig] = None
|
||||||
|
) -> HFGPTAdapter:
|
||||||
|
"""
|
||||||
|
Создает адаптер из существующей llm модели.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
llm_model: Обученная модель из библиотеки llm
|
||||||
|
hf_config: Конфигурация для HuggingFace
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
HFGPTAdapter: Адаптированная модель
|
||||||
|
"""
|
||||||
|
if hf_config is None:
|
||||||
|
# Создаем конфигурацию из модели llm
|
||||||
|
hf_config = HFAdapterConfig.from_llm_config(llm_model.config)
|
||||||
|
|
||||||
|
# Преобразуем в PretrainedConfig
|
||||||
|
pretrained_config = HFPretrainedConfig(**hf_config.to_dict())
|
||||||
|
|
||||||
|
return HFGPTAdapter(pretrained_config, llm_model)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def from_pretrained(
|
||||||
|
model_path: str,
|
||||||
|
hf_config: Optional[HFAdapterConfig] = None
|
||||||
|
) -> HFGPTAdapter:
|
||||||
|
"""
|
||||||
|
Загружает модель из чекпоинта и создает адаптер.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
model_path: Путь к сохраненной модели
|
||||||
|
hf_config: Конфигурация для HuggingFace
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
HFGPTAdapter: Адаптированная модель
|
||||||
|
"""
|
||||||
|
# Загружаем состояние модели
|
||||||
|
state_dict = torch.load(model_path, map_location='cpu')
|
||||||
|
|
||||||
|
# Определяем конфигурацию из состояния модели или используем переданную
|
||||||
|
if hf_config is None:
|
||||||
|
# Пытаемся определить конфигурацию из состояния модели
|
||||||
|
# Это упрощенный подход - в реальности нужно сохранять конфигурацию отдельно
|
||||||
|
vocab_size = state_dict.get('_token_embeddings._embedding.weight', torch.zeros(50257, 768)).shape[0]
|
||||||
|
embed_dim = state_dict.get('_token_embeddings._embedding.weight', torch.zeros(50257, 768)).shape[1]
|
||||||
|
|
||||||
|
hf_config = HFAdapterConfig(
|
||||||
|
vocab_size=vocab_size,
|
||||||
|
hidden_size=embed_dim,
|
||||||
|
# Остальные параметры можно установить по умолчанию
|
||||||
|
)
|
||||||
|
|
||||||
|
pretrained_config = HFPretrainedConfig(**hf_config.to_dict())
|
||||||
|
|
||||||
|
# Создаем модель llm и загружаем веса
|
||||||
|
llm_config = {
|
||||||
|
"vocab_size": hf_config.vocab_size,
|
||||||
|
"embed_dim": hf_config.hidden_size,
|
||||||
|
"num_heads": hf_config.num_attention_heads,
|
||||||
|
"num_layers": hf_config.num_hidden_layers,
|
||||||
|
"max_position_embeddings": hf_config.max_position_embeddings,
|
||||||
|
"dropout": hf_config.hidden_dropout_prob,
|
||||||
|
}
|
||||||
|
|
||||||
|
llm_model = GPT(llm_config)
|
||||||
|
llm_model.load_state_dict(state_dict)
|
||||||
|
|
||||||
|
return HFGPTAdapter(pretrained_config, llm_model)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def save_pretrained(
|
||||||
|
model: HFGPTAdapter,
|
||||||
|
save_directory: str,
|
||||||
|
**kwargs
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Сохраняет адаптированную модель в формате HuggingFace.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
model: Адаптированная модель
|
||||||
|
save_directory: Директория для сохранения
|
||||||
|
**kwargs: Дополнительные параметры
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
|
||||||
|
# Создаем директорию если не существует
|
||||||
|
os.makedirs(save_directory, exist_ok=True)
|
||||||
|
|
||||||
|
# Сохраняем конфигурацию
|
||||||
|
config_path = os.path.join(save_directory, "config.json")
|
||||||
|
with open(config_path, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(model.config.to_dict(), f, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
# Сохраняем веса модели
|
||||||
|
model_path = os.path.join(save_directory, "pytorch_model.bin")
|
||||||
|
torch.save(model.llm_model.state_dict(), model_path)
|
||||||
|
|
||||||
|
# Сохраняем токенизатор если передан
|
||||||
|
if hasattr(kwargs, 'tokenizer') and kwargs['tokenizer'] is not None:
|
||||||
|
kwargs['tokenizer'].save_pretrained(save_directory)
|
||||||
134
hf-proxy/src/hf_proxy/hf_config.py
Normal file
134
hf-proxy/src/hf_proxy/hf_config.py
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
"""
|
||||||
|
Конфигурационные классы для адаптации моделей llm к HuggingFace.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import Dict, Any, Optional
|
||||||
|
from transformers import PretrainedConfig
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class HFAdapterConfig:
|
||||||
|
"""
|
||||||
|
Конфигурация для адаптера HuggingFace.
|
||||||
|
|
||||||
|
Параметры:
|
||||||
|
model_type: Тип модели (gpt, llama, etc.)
|
||||||
|
vocab_size: Размер словаря
|
||||||
|
hidden_size: Размер скрытого слоя
|
||||||
|
num_hidden_layers: Количество слоев
|
||||||
|
num_attention_heads: Количество голов внимания
|
||||||
|
max_position_embeddings: Максимальная длина последовательности
|
||||||
|
intermediate_size: Размер промежуточного слоя FFN
|
||||||
|
hidden_dropout_prob: Вероятность dropout
|
||||||
|
attention_probs_dropout_prob: Вероятность dropout в внимании
|
||||||
|
initializer_range: Диапазон инициализации весов
|
||||||
|
layer_norm_eps: Эпсилон для LayerNorm
|
||||||
|
use_cache: Использовать кеширование
|
||||||
|
pad_token_id: ID токена паддинга
|
||||||
|
eos_token_id: ID токена конца строки
|
||||||
|
bos_token_id: ID токена начала строки
|
||||||
|
"""
|
||||||
|
model_type: str = "gpt"
|
||||||
|
vocab_size: int = 50257
|
||||||
|
hidden_size: int = 768
|
||||||
|
num_hidden_layers: int = 12
|
||||||
|
num_attention_heads: int = 12
|
||||||
|
max_position_embeddings: int = 1024
|
||||||
|
intermediate_size: int = 3072
|
||||||
|
hidden_dropout_prob: float = 0.1
|
||||||
|
attention_probs_dropout_prob: float = 0.1
|
||||||
|
initializer_range: float = 0.02
|
||||||
|
layer_norm_eps: float = 1e-5
|
||||||
|
use_cache: bool = True
|
||||||
|
pad_token_id: int = 50256
|
||||||
|
eos_token_id: int = 50256
|
||||||
|
bos_token_id: int = 50256
|
||||||
|
|
||||||
|
# Дополнительные параметры для совместимости
|
||||||
|
architectures: list = field(default_factory=lambda: ["GPT2LMHeadModel"])
|
||||||
|
torch_dtype: str = "float32"
|
||||||
|
transformers_version: str = "4.44.0"
|
||||||
|
|
||||||
|
def to_dict(self) -> Dict[str, Any]:
|
||||||
|
"""Преобразует конфигурацию в словарь."""
|
||||||
|
return {
|
||||||
|
k: v for k, v in self.__dict__.items()
|
||||||
|
if not k.startswith('_') and not callable(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_llm_config(cls, llm_config: Dict[str, Any]) -> "HFAdapterConfig":
|
||||||
|
"""
|
||||||
|
Создает конфигурацию HF из конфигурации llm.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
llm_config: Конфигурация модели из библиотеки llm
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
HFAdapterConfig: Конфигурация для HuggingFace
|
||||||
|
"""
|
||||||
|
# Маппинг параметров из llm в HF формат
|
||||||
|
mapping = {
|
||||||
|
"embed_dim": "hidden_size",
|
||||||
|
"num_layers": "num_hidden_layers",
|
||||||
|
"num_heads": "num_attention_heads",
|
||||||
|
"max_position_embeddings": "max_position_embeddings",
|
||||||
|
"dropout": "hidden_dropout_prob",
|
||||||
|
"vocab_size": "vocab_size"
|
||||||
|
}
|
||||||
|
|
||||||
|
hf_config_dict = {}
|
||||||
|
for llm_key, hf_key in mapping.items():
|
||||||
|
if llm_key in llm_config:
|
||||||
|
hf_config_dict[hf_key] = llm_config[llm_key]
|
||||||
|
|
||||||
|
# Устанавливаем промежуточный размер (обычно 4x hidden_size)
|
||||||
|
if "hidden_size" in hf_config_dict:
|
||||||
|
hf_config_dict["intermediate_size"] = hf_config_dict["hidden_size"] * 4
|
||||||
|
|
||||||
|
return cls(**hf_config_dict)
|
||||||
|
|
||||||
|
|
||||||
|
class HFPretrainedConfig(PretrainedConfig):
|
||||||
|
"""
|
||||||
|
Конфигурация для предобученных моделей HuggingFace.
|
||||||
|
Наследуется от PretrainedConfig для полной совместимости.
|
||||||
|
"""
|
||||||
|
model_type = "gpt"
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
vocab_size=50257,
|
||||||
|
hidden_size=768,
|
||||||
|
num_hidden_layers=12,
|
||||||
|
num_attention_heads=12,
|
||||||
|
max_position_embeddings=1024,
|
||||||
|
intermediate_size=3072,
|
||||||
|
hidden_dropout_prob=0.1,
|
||||||
|
attention_probs_dropout_prob=0.1,
|
||||||
|
initializer_range=0.02,
|
||||||
|
layer_norm_eps=1e-5,
|
||||||
|
use_cache=True,
|
||||||
|
pad_token_id=50256,
|
||||||
|
eos_token_id=50256,
|
||||||
|
bos_token_id=50256,
|
||||||
|
**kwargs
|
||||||
|
):
|
||||||
|
super().__init__(
|
||||||
|
pad_token_id=pad_token_id,
|
||||||
|
eos_token_id=eos_token_id,
|
||||||
|
bos_token_id=bos_token_id,
|
||||||
|
**kwargs
|
||||||
|
)
|
||||||
|
|
||||||
|
self.vocab_size = vocab_size
|
||||||
|
self.hidden_size = hidden_size
|
||||||
|
self.num_hidden_layers = num_hidden_layers
|
||||||
|
self.num_attention_heads = num_attention_heads
|
||||||
|
self.max_position_embeddings = max_position_embeddings
|
||||||
|
self.intermediate_size = intermediate_size
|
||||||
|
self.hidden_dropout_prob = hidden_dropout_prob
|
||||||
|
self.attention_probs_dropout_prob = attention_probs_dropout_prob
|
||||||
|
self.initializer_range = initializer_range
|
||||||
|
self.layer_norm_eps = layer_norm_eps
|
||||||
|
self.use_cache = use_cache
|
||||||
418
hf-proxy/src/hf_proxy/hf_tokenizer.py
Normal file
418
hf-proxy/src/hf_proxy/hf_tokenizer.py
Normal file
@@ -0,0 +1,418 @@
|
|||||||
|
"""
|
||||||
|
Адаптер для интеграции кастомных токенизаторов llm с HuggingFace.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
from typing import Dict, List, Optional, Union
|
||||||
|
from llm.tokenizers import BPETokenizer, BaseTokenizer
|
||||||
|
|
||||||
|
|
||||||
|
class HFTokenizerAdapter:
|
||||||
|
"""
|
||||||
|
Упрощенный адаптер для кастомных токенизаторов llm.
|
||||||
|
Предоставляет совместимый с HuggingFace интерфейс.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, llm_tokenizer: BaseTokenizer):
|
||||||
|
"""
|
||||||
|
Инициализация адаптера.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
llm_tokenizer: Кастомный токенизатор из llm
|
||||||
|
"""
|
||||||
|
self.llm_tokenizer = llm_tokenizer
|
||||||
|
|
||||||
|
# Получаем словарь и размер
|
||||||
|
self._vocab = llm_tokenizer.get_vocab()
|
||||||
|
self.vocab_size = llm_tokenizer.get_vocab_size()
|
||||||
|
|
||||||
|
# Устанавливаем специальные токены
|
||||||
|
self.pad_token = getattr(llm_tokenizer, 'pad_token', '<pad>')
|
||||||
|
self.unk_token = getattr(llm_tokenizer, 'unk_token', '<unk>')
|
||||||
|
self.bos_token = getattr(llm_tokenizer, 'bos_token', '<bos>')
|
||||||
|
self.eos_token = getattr(llm_tokenizer, 'eos_token', '<eos>')
|
||||||
|
|
||||||
|
# Сохраняем ID специальных токенов
|
||||||
|
self.pad_token_id = getattr(llm_tokenizer, 'pad_token_id', 0)
|
||||||
|
self.unk_token_id = getattr(llm_tokenizer, 'unk_token_id', 1)
|
||||||
|
self.bos_token_id = getattr(llm_tokenizer, 'bos_token_id', 2)
|
||||||
|
self.eos_token_id = getattr(llm_tokenizer, 'eos_token_id', 3)
|
||||||
|
|
||||||
|
def __call__(self, text: str, **kwargs):
|
||||||
|
"""
|
||||||
|
Вызов токенизатора с параметрами как у HuggingFace.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text: Входной текст
|
||||||
|
**kwargs: Параметры токенизации
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Словарь с токенами
|
||||||
|
"""
|
||||||
|
return_tensors = kwargs.get('return_tensors', None)
|
||||||
|
padding = kwargs.get('padding', False)
|
||||||
|
truncation = kwargs.get('truncation', False)
|
||||||
|
max_length = kwargs.get('max_length', None)
|
||||||
|
add_special_tokens = kwargs.get('add_special_tokens', True)
|
||||||
|
|
||||||
|
# Кодируем текст
|
||||||
|
input_ids = self.llm_tokenizer.encode(
|
||||||
|
text,
|
||||||
|
add_special_tokens=add_special_tokens
|
||||||
|
)
|
||||||
|
|
||||||
|
# Применяем truncation
|
||||||
|
if truncation and max_length is not None and len(input_ids) > max_length:
|
||||||
|
input_ids = input_ids[:max_length]
|
||||||
|
|
||||||
|
# Применяем padding
|
||||||
|
if padding and max_length is not None and len(input_ids) < max_length:
|
||||||
|
input_ids = input_ids + [self.pad_token_id] * (max_length - len(input_ids))
|
||||||
|
|
||||||
|
# Конвертируем в тензоры если нужно
|
||||||
|
if return_tensors == "pt":
|
||||||
|
import torch
|
||||||
|
input_ids = torch.tensor([input_ids])
|
||||||
|
|
||||||
|
return {"input_ids": input_ids}
|
||||||
|
|
||||||
|
def encode(
|
||||||
|
self,
|
||||||
|
text: str,
|
||||||
|
text_pair: Optional[str] = None,
|
||||||
|
add_special_tokens: bool = True,
|
||||||
|
padding: bool = False,
|
||||||
|
truncation: bool = False,
|
||||||
|
max_length: Optional[int] = None,
|
||||||
|
return_tensors: Optional[str] = None,
|
||||||
|
**kwargs
|
||||||
|
) -> Union[List[int], List[List[int]]]:
|
||||||
|
"""
|
||||||
|
Кодирует текст в последовательность токенов.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text: Входной текст
|
||||||
|
text_pair: Второй текст (для парных задач)
|
||||||
|
add_special_tokens: Добавлять специальные токены
|
||||||
|
padding: Добавлять паддинг
|
||||||
|
truncation: Обрезать последовательность
|
||||||
|
max_length: Максимальная длина
|
||||||
|
return_tensors: Возвращать тензоры
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Список токенов или список списков токенов
|
||||||
|
"""
|
||||||
|
# Кодируем основной текст
|
||||||
|
token_ids = self.llm_tokenizer.encode(
|
||||||
|
text,
|
||||||
|
add_special_tokens=add_special_tokens
|
||||||
|
)
|
||||||
|
|
||||||
|
# Обрабатываем text_pair если есть
|
||||||
|
if text_pair is not None:
|
||||||
|
pair_ids = self.llm_tokenizer.encode(
|
||||||
|
text_pair,
|
||||||
|
add_special_tokens=False
|
||||||
|
)
|
||||||
|
token_ids.extend(pair_ids)
|
||||||
|
|
||||||
|
# Применяем truncation
|
||||||
|
if truncation and max_length is not None and len(token_ids) > max_length:
|
||||||
|
token_ids = token_ids[:max_length]
|
||||||
|
|
||||||
|
# Применяем padding
|
||||||
|
if padding and max_length is not None and len(token_ids) < max_length:
|
||||||
|
token_ids = token_ids + [self.pad_token_id] * (max_length - len(token_ids))
|
||||||
|
|
||||||
|
# Конвертируем в тензоры если нужно
|
||||||
|
if return_tensors == "pt":
|
||||||
|
import torch
|
||||||
|
return torch.tensor([token_ids])
|
||||||
|
elif return_tensors == "np":
|
||||||
|
import numpy as np
|
||||||
|
return np.array([token_ids])
|
||||||
|
|
||||||
|
return token_ids
|
||||||
|
|
||||||
|
def decode(
|
||||||
|
self,
|
||||||
|
token_ids: Union[int, List[int], List[List[int]]],
|
||||||
|
skip_special_tokens: bool = True,
|
||||||
|
**kwargs
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
Декодирует последовательность токенов в текст.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
token_ids: ID токенов
|
||||||
|
skip_special_tokens: Пропускать специальные токены
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Декодированный текст
|
||||||
|
"""
|
||||||
|
# Обрабатываем разные форматы входных данных
|
||||||
|
if isinstance(token_ids, int):
|
||||||
|
token_ids = [token_ids]
|
||||||
|
elif isinstance(token_ids, list) and len(token_ids) > 0 and isinstance(token_ids[0], list):
|
||||||
|
# Список списков - берем первый элемент
|
||||||
|
token_ids = token_ids[0]
|
||||||
|
|
||||||
|
# Фильтруем специальные токены если нужно
|
||||||
|
if skip_special_tokens:
|
||||||
|
special_ids = {self.pad_token_id, self.unk_token_id, self.bos_token_id, self.eos_token_id}
|
||||||
|
token_ids = [tid for tid in token_ids if tid not in special_ids]
|
||||||
|
|
||||||
|
return self.llm_tokenizer.decode(token_ids)
|
||||||
|
|
||||||
|
def tokenize(self, text: str, **kwargs) -> List[str]:
|
||||||
|
"""
|
||||||
|
Токенизирует текст в список строковых токенов.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text: Входной текст
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List[str]: Список токенов
|
||||||
|
"""
|
||||||
|
return self.llm_tokenizer.tokenize(text)
|
||||||
|
|
||||||
|
def pad(
|
||||||
|
self,
|
||||||
|
encoded_inputs,
|
||||||
|
padding=True,
|
||||||
|
max_length=None,
|
||||||
|
pad_to_multiple_of=None,
|
||||||
|
return_attention_mask=None,
|
||||||
|
return_tensors=None,
|
||||||
|
verbose=True,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Pad a list of encoded inputs.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
encoded_inputs: List of encoded inputs
|
||||||
|
padding: Padding strategy
|
||||||
|
max_length: Maximum length
|
||||||
|
pad_to_multiple_of: Pad to multiple of
|
||||||
|
return_attention_mask: Return attention mask
|
||||||
|
return_tensors: Return tensors
|
||||||
|
verbose: Verbose mode
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Padded inputs
|
||||||
|
"""
|
||||||
|
# Простая реализация padding для совместимости
|
||||||
|
if isinstance(encoded_inputs, (list, tuple)) and len(encoded_inputs) > 0:
|
||||||
|
# Находим максимальную длину
|
||||||
|
max_len = 0
|
||||||
|
for item in encoded_inputs:
|
||||||
|
input_ids = item["input_ids"]
|
||||||
|
# Обрабатываем разные типы данных
|
||||||
|
if isinstance(input_ids, int):
|
||||||
|
seq_len = 1
|
||||||
|
elif hasattr(input_ids, 'shape'):
|
||||||
|
seq_len = input_ids.shape[-1] if len(input_ids.shape) > 1 else len(input_ids)
|
||||||
|
else:
|
||||||
|
seq_len = len(input_ids)
|
||||||
|
max_len = max(max_len, seq_len)
|
||||||
|
|
||||||
|
if max_length is not None:
|
||||||
|
max_len = min(max_len, max_length)
|
||||||
|
|
||||||
|
# Применяем padding
|
||||||
|
for item in encoded_inputs:
|
||||||
|
input_ids = item["input_ids"]
|
||||||
|
|
||||||
|
# Получаем текущую длину
|
||||||
|
if isinstance(input_ids, int):
|
||||||
|
current_len = 1
|
||||||
|
elif hasattr(input_ids, 'shape'):
|
||||||
|
current_len = input_ids.shape[-1] if len(input_ids.shape) > 1 else len(input_ids)
|
||||||
|
else:
|
||||||
|
current_len = len(input_ids)
|
||||||
|
|
||||||
|
if current_len < max_len:
|
||||||
|
# Дополняем pad_token_id
|
||||||
|
padding_length = max_len - current_len
|
||||||
|
|
||||||
|
# Обрабатываем разные типы данных
|
||||||
|
if isinstance(input_ids, int):
|
||||||
|
item["input_ids"] = [input_ids] + [self.pad_token_id] * padding_length
|
||||||
|
elif hasattr(input_ids, 'shape'):
|
||||||
|
import torch
|
||||||
|
padding_tensor = torch.full((padding_length,), self.pad_token_id, dtype=input_ids.dtype)
|
||||||
|
item["input_ids"] = torch.cat([input_ids, padding_tensor])
|
||||||
|
else:
|
||||||
|
item["input_ids"] = input_ids + [self.pad_token_id] * padding_length
|
||||||
|
|
||||||
|
# Добавляем attention_mask если требуется
|
||||||
|
if "attention_mask" in item:
|
||||||
|
mask = item["attention_mask"]
|
||||||
|
if isinstance(mask, int):
|
||||||
|
item["attention_mask"] = [mask] + [0] * padding_length
|
||||||
|
elif hasattr(mask, 'shape'):
|
||||||
|
padding_mask = torch.zeros(padding_length, dtype=mask.dtype)
|
||||||
|
item["attention_mask"] = torch.cat([mask, padding_mask])
|
||||||
|
else:
|
||||||
|
item["attention_mask"] = mask + [0] * padding_length
|
||||||
|
elif return_attention_mask:
|
||||||
|
if isinstance(input_ids, int):
|
||||||
|
item["attention_mask"] = [1] + [0] * padding_length
|
||||||
|
elif hasattr(input_ids, 'shape'):
|
||||||
|
attention_mask = torch.ones(current_len, dtype=torch.long)
|
||||||
|
padding_mask = torch.zeros(padding_length, dtype=torch.long)
|
||||||
|
item["attention_mask"] = torch.cat([attention_mask, padding_mask])
|
||||||
|
else:
|
||||||
|
item["attention_mask"] = [1] * current_len + [0] * padding_length
|
||||||
|
|
||||||
|
# Конвертируем в тензоры если требуется
|
||||||
|
if return_tensors == "pt":
|
||||||
|
import torch
|
||||||
|
for key in list(encoded_inputs[0].keys()):
|
||||||
|
if isinstance(encoded_inputs[0][key], list):
|
||||||
|
for i in range(len(encoded_inputs)):
|
||||||
|
encoded_inputs[i][key] = torch.tensor(encoded_inputs[i][key])
|
||||||
|
|
||||||
|
return encoded_inputs
|
||||||
|
|
||||||
|
def get_vocab(self) -> Dict[str, int]:
|
||||||
|
"""Возвращает словарь токенизатора."""
|
||||||
|
return self._vocab
|
||||||
|
|
||||||
|
def __len__(self) -> int:
|
||||||
|
"""Возвращает размер словаря."""
|
||||||
|
return self.vocab_size
|
||||||
|
|
||||||
|
def save_pretrained(self, save_directory: str, **kwargs):
|
||||||
|
"""
|
||||||
|
Сохраняет токенизатор в формате HuggingFace.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
save_directory: Директория для сохранения
|
||||||
|
**kwargs: Дополнительные параметры
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Создаем директорию если не существует
|
||||||
|
os.makedirs(save_directory, exist_ok=True)
|
||||||
|
|
||||||
|
# Сохраняем конфигурацию токенизатора
|
||||||
|
tokenizer_config = {
|
||||||
|
"tokenizer_class": self.__class__.__name__,
|
||||||
|
"llm_tokenizer_type": self.llm_tokenizer.__class__.__name__,
|
||||||
|
"vocab_size": self.vocab_size,
|
||||||
|
"pad_token": self.pad_token,
|
||||||
|
"unk_token": self.unk_token,
|
||||||
|
"bos_token": self.bos_token,
|
||||||
|
"eos_token": self.eos_token,
|
||||||
|
"pad_token_id": self.pad_token_id,
|
||||||
|
"unk_token_id": self.unk_token_id,
|
||||||
|
"bos_token_id": self.bos_token_id,
|
||||||
|
"eos_token_id": self.eos_token_id,
|
||||||
|
}
|
||||||
|
|
||||||
|
config_path = os.path.join(save_directory, "tokenizer_config.json")
|
||||||
|
with open(config_path, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(tokenizer_config, f, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
|
# Сохраняем словарь
|
||||||
|
vocab_path = os.path.join(save_directory, "vocab.json")
|
||||||
|
with open(vocab_path, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(self._vocab, f, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
|
print(f"✅ Токенизатор сохранен в {save_directory}")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_pretrained(cls, pretrained_model_name_or_path: str, **kwargs):
|
||||||
|
"""
|
||||||
|
Загружает адаптированный токенизатор.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
pretrained_model_name_or_path: Путь к сохраненному токенизатору
|
||||||
|
**kwargs: Дополнительные параметры
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
HFTokenizerAdapter: Загруженный адаптер
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Проверяем, является ли путь директорией с файлами токенизатора
|
||||||
|
if os.path.isdir(pretrained_model_name_or_path):
|
||||||
|
# Загружаем из директории
|
||||||
|
config_path = os.path.join(pretrained_model_name_or_path, "tokenizer_config.json")
|
||||||
|
vocab_path = os.path.join(pretrained_model_name_or_path, "vocab.json")
|
||||||
|
|
||||||
|
if not os.path.exists(config_path) or not os.path.exists(vocab_path):
|
||||||
|
raise FileNotFoundError(
|
||||||
|
f"Файлы токенизатора не найдены в {pretrained_model_name_or_path}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Загружаем конфигурацию
|
||||||
|
with open(config_path, 'r', encoding='utf-8') as f:
|
||||||
|
config = json.load(f)
|
||||||
|
|
||||||
|
# Определяем тип токенизатора llm
|
||||||
|
llm_tokenizer_type = config.get("llm_tokenizer_type", "BPETokenizer")
|
||||||
|
|
||||||
|
if llm_tokenizer_type == "BPETokenizer":
|
||||||
|
# Создаем BPETokenizer и загружаем словарь
|
||||||
|
llm_tokenizer = BPETokenizer()
|
||||||
|
|
||||||
|
# Загружаем словарь
|
||||||
|
with open(vocab_path, 'r', encoding='utf-8') as f:
|
||||||
|
vocab = json.load(f)
|
||||||
|
|
||||||
|
llm_tokenizer.vocab = vocab
|
||||||
|
llm_tokenizer.inverse_vocab = {v: k for k, v in vocab.items()}
|
||||||
|
llm_tokenizer.vocab_size = len(vocab)
|
||||||
|
|
||||||
|
# Устанавливаем специальные токены
|
||||||
|
llm_tokenizer.pad_token = config.get("pad_token", "<pad>")
|
||||||
|
llm_tokenizer.unk_token = config.get("unk_token", "<unk>")
|
||||||
|
llm_tokenizer.bos_token = config.get("bos_token", "<bos>")
|
||||||
|
llm_tokenizer.eos_token = config.get("eos_token", "<eos>")
|
||||||
|
|
||||||
|
llm_tokenizer.pad_token_id = config.get("pad_token_id", 0)
|
||||||
|
llm_tokenizer.unk_token_id = config.get("unk_token_id", 1)
|
||||||
|
llm_tokenizer.bos_token_id = config.get("bos_token_id", 2)
|
||||||
|
llm_tokenizer.eos_token_id = config.get("eos_token_id", 3)
|
||||||
|
|
||||||
|
return cls(llm_tokenizer, **kwargs)
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Неподдерживаемый тип токенизатора: {llm_tokenizer_type}")
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Пытаемся загрузить как файл llm токенизатора
|
||||||
|
try:
|
||||||
|
llm_tokenizer = BPETokenizer.load(pretrained_model_name_or_path)
|
||||||
|
return cls(llm_tokenizer, **kwargs)
|
||||||
|
except:
|
||||||
|
raise ValueError(
|
||||||
|
f"Не удалось загрузить токенизатор из {pretrained_model_name_or_path}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def create_hf_tokenizer(llm_tokenizer: BaseTokenizer) -> HFTokenizerAdapter:
|
||||||
|
"""
|
||||||
|
Создает адаптер HuggingFace для кастомного токенизатора.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
llm_tokenizer: Токенизатор из библиотеки llm
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
HFTokenizerAdapter: Адаптированный токенизатор
|
||||||
|
"""
|
||||||
|
return HFTokenizerAdapter(llm_tokenizer)
|
||||||
|
|
||||||
|
|
||||||
|
def convert_to_hf_format(llm_tokenizer: BaseTokenizer, save_directory: str):
|
||||||
|
"""
|
||||||
|
Конвертирует кастомный токенизатор в формат HuggingFace.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
llm_tokenizer: Токенизатор из llm
|
||||||
|
save_directory: Директория для сохранения
|
||||||
|
"""
|
||||||
|
adapter = create_hf_tokenizer(llm_tokenizer)
|
||||||
|
adapter.save_pretrained(save_directory)
|
||||||
|
return adapter
|
||||||
325
hf-proxy/src/hf_proxy/hf_utils.py
Normal file
325
hf-proxy/src/hf_proxy/hf_utils.py
Normal file
@@ -0,0 +1,325 @@
|
|||||||
|
"""
|
||||||
|
Утилиты для работы с адаптером HuggingFace.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import torch
|
||||||
|
import json
|
||||||
|
from typing import Dict, Any, Optional, List
|
||||||
|
from transformers import AutoTokenizer, AutoConfig
|
||||||
|
from .hf_config import HFAdapterConfig, HFPretrainedConfig
|
||||||
|
from .hf_adapter import HFAdapter, HFGPTAdapter
|
||||||
|
|
||||||
|
|
||||||
|
class HFUtils:
|
||||||
|
"""
|
||||||
|
Утилиты для работы с HuggingFace адаптером.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def create_hf_config_from_llm(llm_config: Dict[str, Any]) -> HFPretrainedConfig:
|
||||||
|
"""
|
||||||
|
Создает конфигурацию HuggingFace из конфигурации llm.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
llm_config: Конфигурация модели из библиотеки llm
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
HFPretrainedConfig: Конфигурация для HuggingFace
|
||||||
|
"""
|
||||||
|
adapter_config = HFAdapterConfig.from_llm_config(llm_config)
|
||||||
|
return HFPretrainedConfig(**adapter_config.to_dict())
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def convert_to_hf_format(
|
||||||
|
llm_model,
|
||||||
|
tokenizer = None,
|
||||||
|
model_name: str = "custom-gpt"
|
||||||
|
) -> tuple:
|
||||||
|
"""
|
||||||
|
Конвертирует llm модель в формат HuggingFace.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
llm_model: Модель из библиотеки llm
|
||||||
|
tokenizer: Токенизатор (HF или кастомный)
|
||||||
|
model_name: Имя модели для сохранения
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple: (адаптированная модель, токенизатор)
|
||||||
|
"""
|
||||||
|
# Создаем адаптер
|
||||||
|
hf_model = HFAdapter.from_llm_model(llm_model)
|
||||||
|
|
||||||
|
# Если токенизатор не передан, создаем стандартный
|
||||||
|
if tokenizer is None:
|
||||||
|
from transformers import AutoTokenizer
|
||||||
|
tokenizer = AutoTokenizer.from_pretrained("gpt2")
|
||||||
|
# Устанавливаем специальные токены
|
||||||
|
if tokenizer.pad_token is None:
|
||||||
|
tokenizer.pad_token = tokenizer.eos_token
|
||||||
|
elif hasattr(tokenizer, '__class__') and 'BPETokenizer' in str(tokenizer.__class__):
|
||||||
|
# Если передан наш кастомный токенизатор, создаем адаптер
|
||||||
|
from .hf_tokenizer import create_hf_tokenizer
|
||||||
|
tokenizer = create_hf_tokenizer(tokenizer)
|
||||||
|
|
||||||
|
return hf_model, tokenizer
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def push_to_hub(
|
||||||
|
model: HFGPTAdapter,
|
||||||
|
tokenizer,
|
||||||
|
repo_name: str,
|
||||||
|
organization: Optional[str] = None,
|
||||||
|
private: bool = False,
|
||||||
|
**kwargs
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Загружает модель в HuggingFace Hub.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
model: Адаптированная модель
|
||||||
|
tokenizer: Токенизатор
|
||||||
|
repo_name: Имя репозитория
|
||||||
|
organization: Организация (опционально)
|
||||||
|
private: Приватный репозиторий
|
||||||
|
**kwargs: Дополнительные параметры
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
from huggingface_hub import HfApi, ModelCard, create_repo
|
||||||
|
|
||||||
|
# Создаем репозиторий
|
||||||
|
if organization:
|
||||||
|
repo_id = f"{organization}/{repo_name}"
|
||||||
|
else:
|
||||||
|
repo_id = repo_name
|
||||||
|
|
||||||
|
create_repo(repo_id, private=private, exist_ok=True)
|
||||||
|
|
||||||
|
# Сохраняем модель локально
|
||||||
|
import tempfile
|
||||||
|
import os
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||||
|
# Сохраняем модель
|
||||||
|
HFAdapter.save_pretrained(model, tmp_dir, tokenizer=tokenizer)
|
||||||
|
|
||||||
|
# Создаем Model Card
|
||||||
|
card = ModelCard.from_template(
|
||||||
|
model_name=repo_name,
|
||||||
|
language="ru",
|
||||||
|
license="apache-2.0",
|
||||||
|
tags=["llm", "gpt", "custom"],
|
||||||
|
)
|
||||||
|
card.save(os.path.join(tmp_dir, "README.md"))
|
||||||
|
|
||||||
|
# Загружаем в Hub
|
||||||
|
api = HfApi()
|
||||||
|
api.upload_folder(
|
||||||
|
folder_path=tmp_dir,
|
||||||
|
repo_id=repo_id,
|
||||||
|
commit_message="Initial commit with custom GPT model"
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"✅ Модель успешно загружена в HuggingFace Hub: {repo_id}")
|
||||||
|
|
||||||
|
except ImportError:
|
||||||
|
raise ImportError(
|
||||||
|
"Для загрузки в HuggingFace Hub установите huggingface_hub: "
|
||||||
|
"pip install huggingface_hub"
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def load_from_hub(
|
||||||
|
repo_id: str,
|
||||||
|
**kwargs
|
||||||
|
) -> tuple:
|
||||||
|
"""
|
||||||
|
Загружает модель из HuggingFace Hub.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
repo_id: ID репозитория
|
||||||
|
**kwargs: Дополнительные параметры
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple: (модель, токенизатор)
|
||||||
|
"""
|
||||||
|
from transformers import AutoTokenizer
|
||||||
|
|
||||||
|
# Загружаем токенизатор
|
||||||
|
tokenizer = AutoTokenizer.from_pretrained(repo_id, **kwargs)
|
||||||
|
|
||||||
|
# Загружаем конфигурацию
|
||||||
|
config = AutoConfig.from_pretrained(repo_id, **kwargs)
|
||||||
|
|
||||||
|
# Создаем модель llm на основе конфигурации
|
||||||
|
llm_config = {
|
||||||
|
"vocab_size": config.vocab_size,
|
||||||
|
"embed_dim": config.hidden_size,
|
||||||
|
"num_heads": config.num_attention_heads,
|
||||||
|
"num_layers": config.num_hidden_layers,
|
||||||
|
"max_position_embeddings": config.max_position_embeddings,
|
||||||
|
"dropout": config.hidden_dropout_prob,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Загружаем модель через адаптер
|
||||||
|
model = HFAdapter.from_pretrained(
|
||||||
|
f"{repo_id}/pytorch_model.bin",
|
||||||
|
HFAdapterConfig.from_llm_config(llm_config)
|
||||||
|
)
|
||||||
|
|
||||||
|
return model, tokenizer
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def compare_with_hf_model(
|
||||||
|
llm_model,
|
||||||
|
hf_model_name: str = "gpt2",
|
||||||
|
test_input: str = "Hello world"
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Сравнивает llm модель с эталонной моделью из HuggingFace.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
llm_model: Модель из библиотеки llm
|
||||||
|
hf_model_name: Имя модели HuggingFace для сравнения
|
||||||
|
test_input: Тестовый вход
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict: Результаты сравнения
|
||||||
|
"""
|
||||||
|
from transformers import AutoModelForCausalLM, AutoTokenizer
|
||||||
|
|
||||||
|
# Загружаем эталонную модель
|
||||||
|
hf_tokenizer = AutoTokenizer.from_pretrained(hf_model_name)
|
||||||
|
hf_model = AutoModelForCausalLM.from_pretrained(hf_model_name)
|
||||||
|
|
||||||
|
# Подготавливаем входные данные
|
||||||
|
inputs = hf_tokenizer(test_input, return_tensors="pt")
|
||||||
|
|
||||||
|
# Получаем логиты от обеих моделей
|
||||||
|
with torch.no_grad():
|
||||||
|
hf_logits = hf_model(**inputs).logits
|
||||||
|
llm_logits = llm_model(inputs['input_ids'])
|
||||||
|
|
||||||
|
# Сравниваем результаты
|
||||||
|
hf_probs = torch.softmax(hf_logits[0, -1], dim=-1)
|
||||||
|
llm_probs = torch.softmax(llm_logits[0, -1], dim=-1)
|
||||||
|
|
||||||
|
# Вычисляем метрики
|
||||||
|
kl_divergence = torch.nn.functional.kl_div(
|
||||||
|
torch.log(llm_probs + 1e-8),
|
||||||
|
hf_probs,
|
||||||
|
reduction='batchmean'
|
||||||
|
)
|
||||||
|
|
||||||
|
cosine_similarity = torch.nn.functional.cosine_similarity(
|
||||||
|
hf_logits.flatten(),
|
||||||
|
llm_logits.flatten(),
|
||||||
|
dim=0
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"kl_divergence": kl_divergence.item(),
|
||||||
|
"cosine_similarity": cosine_similarity.item(),
|
||||||
|
"hf_top_tokens": torch.topk(hf_probs, 5).indices.tolist(),
|
||||||
|
"llm_top_tokens": torch.topk(llm_probs, 5).indices.tolist(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class TokenizerWrapper:
|
||||||
|
"""
|
||||||
|
Обертка для токенизатора с дополнительными утилитами.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, tokenizer):
|
||||||
|
self.tokenizer = tokenizer
|
||||||
|
|
||||||
|
def encode_batch(self, texts: List[str], **kwargs) -> Dict[str, torch.Tensor]:
|
||||||
|
"""
|
||||||
|
Кодирует батч текстов.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
texts: Список текстов
|
||||||
|
**kwargs: Дополнительные параметры токенизации
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict: Токенизированные данные
|
||||||
|
"""
|
||||||
|
return self.tokenizer(
|
||||||
|
texts,
|
||||||
|
padding=True,
|
||||||
|
truncation=True,
|
||||||
|
return_tensors="pt",
|
||||||
|
**kwargs
|
||||||
|
)
|
||||||
|
|
||||||
|
def decode_batch(self, token_ids: torch.Tensor, **kwargs) -> List[str]:
|
||||||
|
"""
|
||||||
|
Декодирует батч токенов.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
token_ids: Тензор с токенами
|
||||||
|
**kwargs: Дополнительные параметры декодирования
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List[str]: Декодированные тексты
|
||||||
|
"""
|
||||||
|
if token_ids.dim() == 1:
|
||||||
|
token_ids = token_ids.unsqueeze(0)
|
||||||
|
|
||||||
|
texts = []
|
||||||
|
for i in range(token_ids.size(0)):
|
||||||
|
text = self.tokenizer.decode(
|
||||||
|
token_ids[i],
|
||||||
|
skip_special_tokens=True,
|
||||||
|
**kwargs
|
||||||
|
)
|
||||||
|
texts.append(text)
|
||||||
|
|
||||||
|
return texts
|
||||||
|
|
||||||
|
def get_vocab_size(self) -> int:
|
||||||
|
"""Возвращает размер словаря."""
|
||||||
|
return len(self.tokenizer)
|
||||||
|
|
||||||
|
def get_special_tokens(self) -> Dict[str, int]:
|
||||||
|
"""Возвращает специальные токены."""
|
||||||
|
return {
|
||||||
|
"pad_token": self.tokenizer.pad_token_id,
|
||||||
|
"eos_token": self.tokenizer.eos_token_id,
|
||||||
|
"bos_token": self.tokenizer.bos_token_id,
|
||||||
|
"unk_token": self.tokenizer.unk_token_id,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def create_hf_pipeline(
|
||||||
|
llm_model,
|
||||||
|
tokenizer=None,
|
||||||
|
device: str = "auto",
|
||||||
|
**kwargs
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Создает HuggingFace pipeline из llm модели.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
llm_model: Модель из библиотеки llm
|
||||||
|
tokenizer: Токенизатор
|
||||||
|
device: Устройство для вычислений
|
||||||
|
**kwargs: Дополнительные параметры pipeline
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
transformers.Pipeline: Готовый pipeline
|
||||||
|
"""
|
||||||
|
from transformers import pipeline
|
||||||
|
|
||||||
|
# Конвертируем модель в HF формат
|
||||||
|
hf_model, tokenizer = HFUtils.convert_to_hf_format(llm_model, tokenizer)
|
||||||
|
|
||||||
|
# Создаем pipeline
|
||||||
|
pipe = pipeline(
|
||||||
|
"text-generation",
|
||||||
|
model=hf_model,
|
||||||
|
tokenizer=tokenizer,
|
||||||
|
device=device,
|
||||||
|
**kwargs
|
||||||
|
)
|
||||||
|
|
||||||
|
return pipe
|
||||||
0
hf-proxy/src/hf_proxy/py.typed
Normal file
0
hf-proxy/src/hf_proxy/py.typed
Normal file
10
llm/.gitignore
vendored
Normal file
10
llm/.gitignore
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# Python-generated files
|
||||||
|
__pycache__/
|
||||||
|
*.py[oc]
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
|
wheels/
|
||||||
|
*.egg-info
|
||||||
|
|
||||||
|
# Virtual environments
|
||||||
|
.venv
|
||||||
1
llm/.python-version
Normal file
1
llm/.python-version
Normal file
@@ -0,0 +1 @@
|
|||||||
|
3.10
|
||||||
0
llm/README.md
Normal file
0
llm/README.md
Normal file
17
llm/pyproject.toml
Normal file
17
llm/pyproject.toml
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
[project]
|
||||||
|
name = "llm"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Research library for LLM architectures"
|
||||||
|
readme = "README.md"
|
||||||
|
authors = [
|
||||||
|
{ name = "Sergey Penkovsky", email = "sergey.penkovsky@gmail.com" }
|
||||||
|
]
|
||||||
|
requires-python = ">=3.10"
|
||||||
|
dependencies = [
|
||||||
|
"torch>=2.3.0",
|
||||||
|
"numpy>=1.24.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["uv_build>=0.8.22,<0.9.0"]
|
||||||
|
build-backend = "uv_build"
|
||||||
2
llm/src/llm/__init__.py
Normal file
2
llm/src/llm/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
def hello() -> str:
|
||||||
|
return "Hello from llm!"
|
||||||
0
llm/src/llm/configs/gpt.yaml
Normal file
0
llm/src/llm/configs/gpt.yaml
Normal file
2
llm/src/llm/core/__init__.py
Normal file
2
llm/src/llm/core/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
def hello() -> str:
|
||||||
|
return "Hello from llm!"
|
||||||
20
llm/src/llm/core/base_model.py
Normal file
20
llm/src/llm/core/base_model.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# llm/core/base_model.py
|
||||||
|
import torch.nn as nn
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
|
||||||
|
class BaseModel(nn.Module, ABC):
|
||||||
|
"""Базовый класс для всех LLM."""
|
||||||
|
|
||||||
|
def __init__(self, config):
|
||||||
|
super().__init__()
|
||||||
|
self.config = config
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def forward(self, input_ids, attention_mask=None):
|
||||||
|
"""Прямой проход модели."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def generate(self, input_ids, max_length=50):
|
||||||
|
"""Генерация текста (greedy или sampling)."""
|
||||||
|
pass
|
||||||
96
llm/src/llm/core/decoder.py
Normal file
96
llm/src/llm/core/decoder.py
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
from torch import nn
|
||||||
|
import torch
|
||||||
|
from .feed_forward import FeedForward
|
||||||
|
from .multi_head_attention import MultiHeadAttention
|
||||||
|
|
||||||
|
class Decoder(nn.Module):
|
||||||
|
"""
|
||||||
|
Декодер трансформера - ключевой компонент архитектуры Transformer.
|
||||||
|
|
||||||
|
Предназначен для:
|
||||||
|
- Обработки последовательностей с учетом контекста (самовнимание)
|
||||||
|
- Постепенного генерирования выходной последовательности
|
||||||
|
- Учета масок для предотвращения "заглядывания в будущее"
|
||||||
|
|
||||||
|
Алгоритм работы:
|
||||||
|
1. Входной тензор (batch_size, seq_len, emb_size)
|
||||||
|
2. Многоголовое внимание с residual connection и LayerNorm
|
||||||
|
3. FeedForward сеть с residual connection и LayerNorm
|
||||||
|
4. Выходной тензор (batch_size, seq_len, emb_size)
|
||||||
|
|
||||||
|
Основные характеристики:
|
||||||
|
- Поддержка масок внимания
|
||||||
|
- Residual connections для стабилизации градиентов
|
||||||
|
- Layer Normalization после каждого sub-layer
|
||||||
|
- Конфигурируемые параметры внимания
|
||||||
|
|
||||||
|
Примеры использования:
|
||||||
|
|
||||||
|
1. Базовый случай:
|
||||||
|
>>> decoder = Decoder(num_heads=8, emb_size=512, head_size=64, max_seq_len=1024)
|
||||||
|
>>> x = torch.randn(1, 10, 512) # [batch, seq_len, emb_size]
|
||||||
|
>>> output = decoder(x)
|
||||||
|
>>> print(output.shape)
|
||||||
|
torch.Size([1, 10, 512])
|
||||||
|
|
||||||
|
2. С маской внимания:
|
||||||
|
>>> mask = torch.tril(torch.ones(10, 10)) # Нижнетреугольная маска
|
||||||
|
>>> output = decoder(x, mask)
|
||||||
|
|
||||||
|
3. Инкрементальное декодирование:
|
||||||
|
>>> for i in range(10):
|
||||||
|
>>> output = decoder(x[:, :i+1, :], mask[:i+1, :i+1])
|
||||||
|
"""
|
||||||
|
def __init__(self,
|
||||||
|
num_heads: int,
|
||||||
|
emb_size: int,
|
||||||
|
head_size: int,
|
||||||
|
max_seq_len: int,
|
||||||
|
dropout: float = 0.1
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Инициализация декодера.
|
||||||
|
|
||||||
|
Параметры:
|
||||||
|
num_heads: int - количество голов внимания
|
||||||
|
emb_size: int - размерность эмбеддингов
|
||||||
|
head_size: int - размерность каждой головы внимания
|
||||||
|
max_seq_len: int - максимальная длина последовательности
|
||||||
|
dropout: float (default=0.1) - вероятность dropout
|
||||||
|
"""
|
||||||
|
super().__init__()
|
||||||
|
self._heads = MultiHeadAttention(
|
||||||
|
num_heads=num_heads,
|
||||||
|
emb_size=emb_size,
|
||||||
|
head_size=head_size,
|
||||||
|
max_seq_len=max_seq_len,
|
||||||
|
dropout=dropout
|
||||||
|
)
|
||||||
|
self._ff = FeedForward(emb_size=emb_size, dropout=dropout)
|
||||||
|
self._norm1 = nn.LayerNorm(emb_size)
|
||||||
|
self._norm2 = nn.LayerNorm(emb_size)
|
||||||
|
|
||||||
|
def forward(self, x: torch.Tensor, mask: torch.Tensor = None) -> torch.Tensor:
|
||||||
|
"""
|
||||||
|
Прямой проход через декодер.
|
||||||
|
|
||||||
|
Вход:
|
||||||
|
x: torch.Tensor - входной тензор [batch_size, seq_len, emb_size]
|
||||||
|
mask: torch.Tensor (optional) - маска внимания [seq_len, seq_len]
|
||||||
|
|
||||||
|
Возвращает:
|
||||||
|
torch.Tensor - выходной тензор [batch_size, seq_len, emb_size]
|
||||||
|
|
||||||
|
Алгоритм forward:
|
||||||
|
1. Применяем MultiHeadAttention к входу
|
||||||
|
2. Добавляем residual connection и LayerNorm
|
||||||
|
3. Применяем FeedForward сеть
|
||||||
|
4. Добавляем residual connection и LayerNorm
|
||||||
|
"""
|
||||||
|
# Self-Attention блок
|
||||||
|
attention = self._heads(x, mask)
|
||||||
|
out = self._norm1(attention + x)
|
||||||
|
|
||||||
|
# FeedForward блок
|
||||||
|
ffn_out = self._ff(out)
|
||||||
|
return self._norm2(ffn_out + out)
|
||||||
80
llm/src/llm/core/feed_forward.py
Normal file
80
llm/src/llm/core/feed_forward.py
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
from torch import nn
|
||||||
|
import torch
|
||||||
|
|
||||||
|
class FeedForward(nn.Module):
|
||||||
|
"""
|
||||||
|
Слой прямой связи (Feed Forward Network) для архитектуры трансформеров.
|
||||||
|
|
||||||
|
Этот слой состоит из двух линейных преобразований с расширением внутренней размерности
|
||||||
|
в 4 раза и механизмом dropout для регуляризации. Между линейными слоями применяется
|
||||||
|
активация ReLU.
|
||||||
|
|
||||||
|
Алгоритм работы:
|
||||||
|
1. Входной тензор x (размерность: [batch_size, seq_len, emb_size])
|
||||||
|
2. Линейное преобразование: emb_size -> 4*emb_size
|
||||||
|
3. Активация ReLU
|
||||||
|
4. Линейное преобразование: 4*emb_size -> emb_size
|
||||||
|
5. Применение dropout
|
||||||
|
6. Возврат результата (размерность: [batch_size, seq_len, emb_size])
|
||||||
|
|
||||||
|
Предназначение:
|
||||||
|
- Добавляет нелинейность в архитектуру трансформера
|
||||||
|
- Обеспечивает взаимодействие между различными размерностями эмбеддингов
|
||||||
|
- Работает независимо для каждого токена в последовательности
|
||||||
|
|
||||||
|
Примеры использования:
|
||||||
|
|
||||||
|
>>> # Инициализация слоя
|
||||||
|
>>> ff = FeedForward(emb_size=512, dropout=0.1)
|
||||||
|
>>>
|
||||||
|
>>> # Прямой проход
|
||||||
|
>>> x = torch.randn(32, 10, 512) # [batch_size, seq_len, emb_size]
|
||||||
|
>>> output = ff(x)
|
||||||
|
>>> print(output.shape) # torch.Size([32, 10, 512])
|
||||||
|
>>>
|
||||||
|
>>> # Работа с разными типами данных
|
||||||
|
>>> x_double = torch.randn(32, 10, 512, dtype=torch.float64)
|
||||||
|
>>> output_double = ff(x_double)
|
||||||
|
>>> print(output_double.dtype) # torch.float64
|
||||||
|
"""
|
||||||
|
def __init__(self, emb_size: int, dropout: float = 0.1):
|
||||||
|
"""
|
||||||
|
Инициализация слоя Feed Forward Network.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
emb_size: Размерность входных эмбеддингов
|
||||||
|
dropout: Вероятность dropout для регуляризации (по умолчанию: 0.1)
|
||||||
|
"""
|
||||||
|
super().__init__()
|
||||||
|
# Первый линейный слой (расширение размерности)
|
||||||
|
self._layer1 = nn.Linear(emb_size, emb_size * 4)
|
||||||
|
# ReLU активация
|
||||||
|
self._relu = nn.ReLU()
|
||||||
|
# Второй линейный слой (сжатие обратно)
|
||||||
|
self._layer2 = nn.Linear(emb_size * 4, emb_size)
|
||||||
|
# Dropout
|
||||||
|
self._dropout = nn.Dropout(dropout)
|
||||||
|
|
||||||
|
def forward(self, x: torch.Tensor):
|
||||||
|
"""
|
||||||
|
Прямой проход через слой Feed Forward Network.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
x: Входной тензор размерности [batch_size, seq_len, emb_size]
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Тензор той же размерности, что и входной
|
||||||
|
"""
|
||||||
|
# Сохраняем dtype входных данных
|
||||||
|
input_dtype = x.dtype
|
||||||
|
|
||||||
|
# Приводим веса к нужному типу если необходимо
|
||||||
|
if input_dtype != self._layer1.weight.dtype:
|
||||||
|
self._layer1 = self._layer1.to(dtype=input_dtype)
|
||||||
|
self._layer2 = self._layer2.to(dtype=input_dtype)
|
||||||
|
|
||||||
|
# Пропустим тензор x по очереди через все созданные слои
|
||||||
|
x = self._layer1(x)
|
||||||
|
x = self._relu(x)
|
||||||
|
x = self._layer2(x)
|
||||||
|
return self._dropout(x)
|
||||||
84
llm/src/llm/core/head_attention.py
Normal file
84
llm/src/llm/core/head_attention.py
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import torch
|
||||||
|
from torch import nn
|
||||||
|
import torch.nn.functional as F
|
||||||
|
from math import sqrt
|
||||||
|
|
||||||
|
class HeadAttention(nn.Module):
|
||||||
|
"""
|
||||||
|
Реализация одного головного механизма внимания из архитектуры Transformer.
|
||||||
|
Выполняет scaled dot-product attention с маскированием будущих позиций (causal attention).
|
||||||
|
|
||||||
|
Основной алгоритм:
|
||||||
|
1. Линейные преобразования входных данных в Q (query), K (key), V (value)
|
||||||
|
2. Вычисление scores = Q·K^T / sqrt(d_k)
|
||||||
|
3. Применение causal маски (заполнение -inf будущих позиций)
|
||||||
|
4. Softmax для получения весов внимания
|
||||||
|
5. Умножение весов на значения V
|
||||||
|
|
||||||
|
Пример использования:
|
||||||
|
>>> attention = HeadAttention(emb_size=64, head_size=32, max_seq_len=128)
|
||||||
|
>>> x = torch.randn(1, 10, 64) # [batch_size, seq_len, emb_size]
|
||||||
|
>>> output = attention(x) # [1, 10, 32]
|
||||||
|
|
||||||
|
Параметры:
|
||||||
|
emb_size (int): Размер входного эмбеддинга
|
||||||
|
head_size (int): Размерность выхода головы внимания
|
||||||
|
max_seq_len (int): Максимальная длина последовательности
|
||||||
|
|
||||||
|
Примечания:
|
||||||
|
- Использует нижнетреугольную маску для предотвращения "заглядывания в будущее"
|
||||||
|
- Автоматически адаптируется к разным версиям PyTorch
|
||||||
|
- Поддерживает batch-обработку входных данных
|
||||||
|
"""
|
||||||
|
def __init__(self, emb_size: int, head_size: int, max_seq_len: int):
|
||||||
|
super().__init__()
|
||||||
|
self._emb_size = emb_size
|
||||||
|
self._head_size = head_size
|
||||||
|
self._max_seq_len = max_seq_len
|
||||||
|
|
||||||
|
# Линейные преобразования для Q, K, V
|
||||||
|
self._k = nn.Linear(emb_size, head_size)
|
||||||
|
self._q = nn.Linear(emb_size, head_size)
|
||||||
|
self._v = nn.Linear(emb_size, head_size)
|
||||||
|
|
||||||
|
# Создание causal маски
|
||||||
|
mask = torch.tril(torch.ones(max_seq_len, max_seq_len))
|
||||||
|
self.register_buffer('_tril_mask', mask.bool() if hasattr(torch, 'bool') else mask.byte())
|
||||||
|
|
||||||
|
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
||||||
|
"""
|
||||||
|
Прямой проход через слой внимания.
|
||||||
|
|
||||||
|
Аргументы:
|
||||||
|
x (torch.Tensor): Входной тензор формы [batch_size, seq_len, emb_size]
|
||||||
|
|
||||||
|
Возвращает:
|
||||||
|
torch.Tensor: Выходной тензор формы [batch_size, seq_len, head_size]
|
||||||
|
|
||||||
|
Исключения:
|
||||||
|
ValueError: Если длина последовательности превышает max_seq_len
|
||||||
|
|
||||||
|
Пример внутренних преобразований:
|
||||||
|
Для входа x.shape = [2, 5, 64]:
|
||||||
|
1. Q/K/V преобразования -> [2, 5, 32]
|
||||||
|
2. Scores = Q·K^T -> [2, 5, 5]
|
||||||
|
3. После маски и softmax -> [2, 5, 5]
|
||||||
|
4. Умножение на V -> [2, 5, 32]
|
||||||
|
"""
|
||||||
|
seq_len = x.shape[1]
|
||||||
|
if seq_len > self._max_seq_len:
|
||||||
|
raise ValueError(f"Длина последовательности {seq_len} превышает максимум {self._max_seq_len}")
|
||||||
|
|
||||||
|
# 1. Линейные преобразования
|
||||||
|
k = self._k(x) # [B, T, hs]
|
||||||
|
q = self._q(x) # [B, T, hs]
|
||||||
|
|
||||||
|
# 2. Вычисление scores
|
||||||
|
scores = q @ k.transpose(-2, -1) / sqrt(self._head_size)
|
||||||
|
|
||||||
|
# 3. Применение causal маски
|
||||||
|
scores = scores.masked_fill(~self._tril_mask[:seq_len, :seq_len], float('-inf'))
|
||||||
|
|
||||||
|
# 4. Softmax и умножение на V
|
||||||
|
weights = F.softmax(scores, dim=-1)
|
||||||
|
return weights @ self._v(x)
|
||||||
104
llm/src/llm/core/multi_head_attention.py
Normal file
104
llm/src/llm/core/multi_head_attention.py
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
from torch import nn
|
||||||
|
import torch
|
||||||
|
from .head_attention import HeadAttention
|
||||||
|
|
||||||
|
class MultiHeadAttention(nn.Module):
|
||||||
|
"""
|
||||||
|
Реализация механизма многоголового внимания (Multi-Head Attention) из архитектуры Transformer.
|
||||||
|
|
||||||
|
Основные характеристики:
|
||||||
|
- Параллельная обработка входных данных несколькими головами внимания
|
||||||
|
- Поддержка маскирования (causal mask и пользовательские маски)
|
||||||
|
- Финальная проекция с dropout регуляризацией
|
||||||
|
|
||||||
|
Математическое описание:
|
||||||
|
MultiHead(Q, K, V) = Concat(head_1, ..., head_h)W^O
|
||||||
|
где head_i = Attention(QW_i^Q, KW_i^K, VW_i^V)
|
||||||
|
|
||||||
|
Примеры использования:
|
||||||
|
|
||||||
|
1. Базовый пример:
|
||||||
|
>>> mha = MultiHeadAttention(num_heads=8, emb_size=512, head_size=64, max_seq_len=1024)
|
||||||
|
>>> x = torch.randn(2, 50, 512) # [batch_size, seq_len, emb_size]
|
||||||
|
>>> output = mha(x) # [2, 50, 512]
|
||||||
|
|
||||||
|
2. С использованием маски:
|
||||||
|
>>> mask = torch.tril(torch.ones(50, 50)) # Causal mask
|
||||||
|
>>> output = mha(x, mask)
|
||||||
|
|
||||||
|
3. Интеграция в Transformer:
|
||||||
|
>>> # В составе Transformer слоя
|
||||||
|
>>> self.attention = MultiHeadAttention(...)
|
||||||
|
>>> x = self.attention(x, mask)
|
||||||
|
"""
|
||||||
|
def __init__(self, num_heads: int, emb_size: int, head_size: int, max_seq_len: int, dropout: float = 0.1):
|
||||||
|
"""
|
||||||
|
Инициализация многоголового внимания.
|
||||||
|
|
||||||
|
Параметры:
|
||||||
|
num_heads (int): Количество голов внимания. Типичные значения: 4-16
|
||||||
|
emb_size (int): Размерность входных и выходных эмбеддингов
|
||||||
|
head_size (int): Размерность каждой головы внимания (обычно emb_size // num_heads)
|
||||||
|
max_seq_len (int): Максимальная длина последовательности
|
||||||
|
dropout (float): Вероятность dropout (по умолчанию 0.1)
|
||||||
|
|
||||||
|
Контрольные значения:
|
||||||
|
- num_heads * head_size должно равняться emb_size
|
||||||
|
- head_size обычно выбирают 32-128
|
||||||
|
- max_seq_len зависит от задачи (512 для BERT, 2048 для GPT-3)
|
||||||
|
"""
|
||||||
|
super().__init__()
|
||||||
|
self._heads = nn.ModuleList([
|
||||||
|
HeadAttention(
|
||||||
|
emb_size=emb_size,
|
||||||
|
head_size=head_size,
|
||||||
|
max_seq_len=max_seq_len
|
||||||
|
) for _ in range(num_heads)
|
||||||
|
])
|
||||||
|
self._layer = nn.Linear(head_size * num_heads, emb_size)
|
||||||
|
self._dropout = nn.Dropout(dropout)
|
||||||
|
|
||||||
|
def forward(self, x: torch.Tensor, mask: torch.Tensor = None):
|
||||||
|
"""
|
||||||
|
Прямой проход через слой многоголового внимания.
|
||||||
|
|
||||||
|
Подробное описание преобразований тензоров:
|
||||||
|
1. Входной тензор [batch_size, seq_len, emb_size] разделяется на N голов:
|
||||||
|
- Каждая голова получает тензор [batch_size, seq_len, head_size]
|
||||||
|
2. Каждая голова вычисляет attention:
|
||||||
|
- Вход: [batch_size, seq_len, head_size]
|
||||||
|
- Выход: [batch_size, seq_len, head_size]
|
||||||
|
3. Конкатенация результатов:
|
||||||
|
- Объединенный выход: [batch_size, seq_len, num_heads * head_size]
|
||||||
|
4. Линейная проекция:
|
||||||
|
- Выход: [batch_size, seq_len, emb_size]
|
||||||
|
5. Применение dropout
|
||||||
|
|
||||||
|
Аргументы:
|
||||||
|
x (torch.Tensor): Входной тензор формы [batch_size, seq_len, emb_size]
|
||||||
|
mask (torch.Tensor, optional): Маска внимания формы [seq_len, seq_len]
|
||||||
|
|
||||||
|
Возвращает:
|
||||||
|
torch.Tensor: Выходной тензор формы [batch_size, seq_len, emb_size]
|
||||||
|
|
||||||
|
Пример преобразований для emb_size=512, num_heads=8:
|
||||||
|
Вход: [4, 100, 512]
|
||||||
|
-> Каждая голова: [4, 100, 64]
|
||||||
|
-> После внимания: 8 x [4, 100, 64]
|
||||||
|
-> Конкатенация: [4, 100, 512]
|
||||||
|
-> Проекция: [4, 100, 512]
|
||||||
|
-> Dropout: [4, 100, 512]
|
||||||
|
"""
|
||||||
|
# 1. Вычисляем attention для каждой головы
|
||||||
|
attention_outputs = [head(x) for head in self._heads]
|
||||||
|
|
||||||
|
# 2. Объединяем результаты всех голов
|
||||||
|
concatenated_attention = torch.cat(attention_outputs, dim=-1)
|
||||||
|
|
||||||
|
# 3. Проецируем в пространство эмбеддингов
|
||||||
|
projected_output = self._layer(concatenated_attention)
|
||||||
|
|
||||||
|
# 4. Применяем dropout для регуляризации
|
||||||
|
final_output = self._dropout(projected_output)
|
||||||
|
|
||||||
|
return final_output
|
||||||
90
llm/src/llm/core/positional_embeddings.py
Normal file
90
llm/src/llm/core/positional_embeddings.py
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
import torch
|
||||||
|
from torch import nn, Tensor
|
||||||
|
|
||||||
|
class PositionalEmbeddings(nn.Module):
|
||||||
|
"""
|
||||||
|
Класс для создания позиционных эмбеддингов через nn.Embedding.
|
||||||
|
|
||||||
|
Позиционные эмбеддинги используются в нейросетях для передачи информации
|
||||||
|
о позиции элементов в последовательности (например, в Transformer).
|
||||||
|
|
||||||
|
Особенности:
|
||||||
|
- Создаёт обучаемые позиционные эмбеддинги фиксированной длины
|
||||||
|
- Поддерживает обработку последовательностей переменной длины
|
||||||
|
- Автоматически размещает вычисления на том же устройстве, что и параметры
|
||||||
|
|
||||||
|
Args:
|
||||||
|
max_seq_len (int): Максимальная длина последовательности
|
||||||
|
emb_size (int): Размерность векторного представления позиций
|
||||||
|
|
||||||
|
Пример использования:
|
||||||
|
>>> pos_encoder = PositionalEmbeddings(max_seq_len=100, emb_size=256)
|
||||||
|
>>> # Получить эмбеддинги для последовательности из 10 элементов
|
||||||
|
>>> embeddings = pos_encoder(10) # Tensor shape: [10, 256]
|
||||||
|
>>> # Использование в модели
|
||||||
|
>>> class MyModel(nn.Module):
|
||||||
|
... def __init__(self):
|
||||||
|
... super().__init__()
|
||||||
|
... self.pos_emb = PositionalEmbeddings(100, 256)
|
||||||
|
... def forward(self, x):
|
||||||
|
... pos = self.pos_emb(x.size(1))
|
||||||
|
... return x + pos # Добавляем позиционную информацию
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, max_seq_len: int, emb_size: int):
|
||||||
|
super().__init__()
|
||||||
|
self.max_seq_len = max_seq_len
|
||||||
|
self.emb_size = emb_size
|
||||||
|
self.embedding = nn.Embedding(
|
||||||
|
num_embeddings=max_seq_len,
|
||||||
|
embedding_dim=emb_size
|
||||||
|
)
|
||||||
|
|
||||||
|
def forward(self, seq_len: int) -> Tensor:
|
||||||
|
"""
|
||||||
|
Возвращает позиционные эмбеддинги для заданной длины последовательности.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
seq_len (int): Длина последовательности (1 <= seq_len <= max_seq_len)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tensor: Тензор позиционных эмбеддингов формы [seq_len, emb_size]
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
IndexError: Если seq_len выходит за допустимые границы
|
||||||
|
|
||||||
|
Пример:
|
||||||
|
>>> pos_encoder = PositionalEmbeddings(100, 64)
|
||||||
|
>>> emb = pos_encoder(10) # Тензор 10x64
|
||||||
|
"""
|
||||||
|
if seq_len < 1 or seq_len > self.max_seq_len:
|
||||||
|
raise IndexError(f"Длина {seq_len} должна быть от 1 до {self.max_seq_len}")
|
||||||
|
positions = torch.arange(seq_len, device=self.embedding.weight.device)
|
||||||
|
return self.embedding(positions)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Демонстрация работы
|
||||||
|
print("Пример использования PositionalEmbeddings:")
|
||||||
|
pos_emb = PositionalEmbeddings(max_seq_len=50, emb_size=128)
|
||||||
|
|
||||||
|
# Пример 1: Базовое использование
|
||||||
|
print("\n1. Базовый пример:")
|
||||||
|
emb = pos_emb(10)
|
||||||
|
print(f"Форма выходного тензора: {emb.shape}")
|
||||||
|
print(f"Среднее значение: {emb.mean().item():.4f}")
|
||||||
|
|
||||||
|
# Пример 2: Интеграция с моделью
|
||||||
|
print("\n2. Пример интеграции с моделью:")
|
||||||
|
class DemoModel(nn.Module):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.pos_emb = PositionalEmbeddings(50, 128)
|
||||||
|
|
||||||
|
def forward(self, x):
|
||||||
|
pos = self.pos_emb(x.size(1))
|
||||||
|
return x + pos # Добавляем позиционную информацию
|
||||||
|
|
||||||
|
model = DemoModel()
|
||||||
|
input_tensor = torch.randn(2, 10, 128) # [batch, seq, features]
|
||||||
|
output = model(input_tensor)
|
||||||
|
print(f"Вход: {input_tensor.shape}, Выход: {output.shape}")
|
||||||
68
llm/src/llm/core/token_embeddings.py
Normal file
68
llm/src/llm/core/token_embeddings.py
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import torch
|
||||||
|
from torch import nn
|
||||||
|
from torch import Tensor
|
||||||
|
|
||||||
|
class TokenEmbeddings(nn.Module):
|
||||||
|
"""
|
||||||
|
Модуль PyTorch для преобразования индексов токенов в векторные представления (эмбеддинги).
|
||||||
|
|
||||||
|
Преобразует целочисленные индексы токенов в обучаемые векторные представления фиксированного размера.
|
||||||
|
Обычно используется как первый слой в нейронных сетях для задач NLP.
|
||||||
|
|
||||||
|
Аргументы:
|
||||||
|
vocab_size (int): Размер словаря (количество уникальных токенов)
|
||||||
|
emb_size (int): Размерность векторных представлений
|
||||||
|
|
||||||
|
Форматы данных:
|
||||||
|
- Вход: тензор (batch_size, seq_len) индексов токенов
|
||||||
|
- Выход: тензор (batch_size, seq_len, emb_size) векторных представлений
|
||||||
|
|
||||||
|
Примеры использования:
|
||||||
|
>>> embedding_layer = TokenEmbeddings(vocab_size=10000, emb_size=256)
|
||||||
|
>>> tokens = torch.tensor([[1, 2, 3], [4, 5, 6]]) # batch_size=2, seq_len=3
|
||||||
|
>>> embeddings = embedding_layer(tokens)
|
||||||
|
>>> embeddings.shape
|
||||||
|
torch.Size([2, 3, 256])
|
||||||
|
|
||||||
|
Примечание:
|
||||||
|
- Индексы должны быть в диапазоне [0, vocab_size-1]
|
||||||
|
- Эмбеддинги инициализируются случайно и обучаются в процессе тренировки модели
|
||||||
|
"""
|
||||||
|
def __init__(self, vocab_size: int, emb_size: int):
|
||||||
|
super().__init__()
|
||||||
|
self._embedding = nn.Embedding(
|
||||||
|
num_embeddings=vocab_size,
|
||||||
|
embedding_dim=emb_size
|
||||||
|
)
|
||||||
|
|
||||||
|
def forward(self, x: Tensor) -> Tensor:
|
||||||
|
return self._embedding(x)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def num_embeddings(self) -> int:
|
||||||
|
"""Возвращает размер словаря"""
|
||||||
|
return self._embedding.num_embeddings
|
||||||
|
|
||||||
|
@property
|
||||||
|
def embedding_dim(self) -> int:
|
||||||
|
"""Возвращает размерность эмбеддингов"""
|
||||||
|
return self._embedding.embedding_dim
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Пример использования
|
||||||
|
embedding = TokenEmbeddings(vocab_size=100, emb_size=128)
|
||||||
|
|
||||||
|
# Создаем тензор с индексами в пределах vocab_size (0-99)
|
||||||
|
tensor = torch.tensor([
|
||||||
|
[11, 45, 76, 34],
|
||||||
|
[34, 67, 45, 54]
|
||||||
|
])
|
||||||
|
|
||||||
|
# Проверяем индексы
|
||||||
|
if (tensor >= 100).any():
|
||||||
|
raise ValueError("Some indices are out of vocabulary range (vocab_size=100)")
|
||||||
|
|
||||||
|
output = embedding(tensor)
|
||||||
|
print("Embeddings shape:", output.shape)
|
||||||
|
print(f"{output.shape} | {output.mean().item():.11f}") # Формат как в ТЗ
|
||||||
2
llm/src/llm/evaluation/__init__.py
Normal file
2
llm/src/llm/evaluation/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
def hello() -> str:
|
||||||
|
return "Hello from llm!"
|
||||||
0
llm/src/llm/evaluation/benchmark.py
Normal file
0
llm/src/llm/evaluation/benchmark.py
Normal file
0
llm/src/llm/evaluation/perplexity.py
Normal file
0
llm/src/llm/evaluation/perplexity.py
Normal file
0
llm/src/llm/evaluation/utils.py
Normal file
0
llm/src/llm/evaluation/utils.py
Normal file
3
llm/src/llm/models/gpt/__init__.py
Normal file
3
llm/src/llm/models/gpt/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from .gpt import GPT
|
||||||
|
|
||||||
|
__all__ = ["GPT"]
|
||||||
264
llm/src/llm/models/gpt/gpt.py
Normal file
264
llm/src/llm/models/gpt/gpt.py
Normal file
@@ -0,0 +1,264 @@
|
|||||||
|
# llm/models/gpt/gpt2.py
|
||||||
|
import torch
|
||||||
|
import torch.nn as nn
|
||||||
|
import torch.nn.functional as F
|
||||||
|
from llm.core.base_model import BaseModel
|
||||||
|
from llm.core.decoder import Decoder
|
||||||
|
from llm.core.token_embeddings import TokenEmbeddings
|
||||||
|
from llm.core.positional_embeddings import PositionalEmbeddings
|
||||||
|
|
||||||
|
class GPT(BaseModel):
|
||||||
|
"""GPT-like трансформер для генерации текста
|
||||||
|
|
||||||
|
Args:
|
||||||
|
vocab_size: Размер словаря
|
||||||
|
max_seq_len: Макс. длина последовательности
|
||||||
|
emb_size: Размерность эмбеддингов
|
||||||
|
num_heads: Количество голов внимания
|
||||||
|
head_size: Размерность голов внимания
|
||||||
|
num_layers: Количество слоёв декодера
|
||||||
|
dropout: Вероятность dropout (default=0.1)
|
||||||
|
device: Устройство (default='cpu')
|
||||||
|
"""
|
||||||
|
def __init__(self, config):
|
||||||
|
super().__init__(config)
|
||||||
|
|
||||||
|
# Инициализация слоев
|
||||||
|
self._max_seq_len = config["max_position_embeddings"]
|
||||||
|
self._token_embeddings = TokenEmbeddings(
|
||||||
|
vocab_size=config["vocab_size"],
|
||||||
|
emb_size=config["embed_dim"]
|
||||||
|
)
|
||||||
|
self._position_embeddings = PositionalEmbeddings(
|
||||||
|
max_seq_len=config["max_position_embeddings"],
|
||||||
|
emb_size=config["embed_dim"]
|
||||||
|
)
|
||||||
|
self._dropout = nn.Dropout(config["dropout"])
|
||||||
|
# head_size = emb_size // num_heads
|
||||||
|
self._decoders = nn.ModuleList([Decoder(
|
||||||
|
num_heads=config["num_heads"],
|
||||||
|
emb_size=config["embed_dim"],
|
||||||
|
head_size=config["embed_dim"] // config["num_heads"],
|
||||||
|
max_seq_len=config["max_position_embeddings"],
|
||||||
|
dropout=config["dropout"]
|
||||||
|
) for _ in range(config["num_layers"])])
|
||||||
|
self._linear = nn.Linear(config["embed_dim"], config["vocab_size"])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def max_seq_len(self):
|
||||||
|
"""Возвращает максимальную длину последовательности."""
|
||||||
|
return self._max_seq_len
|
||||||
|
|
||||||
|
def forward(self, x: torch.Tensor, attention_mask=None) -> torch.Tensor:
|
||||||
|
"""Прямой проход через GPT
|
||||||
|
|
||||||
|
Args:
|
||||||
|
x: Входной тензор [batch_size, seq_len]
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Тензор логитов [batch_size, seq_len, vocab_size]
|
||||||
|
"""
|
||||||
|
# Проверка длины последовательности
|
||||||
|
if x.size(1) > self._max_seq_len:
|
||||||
|
raise ValueError(f"Длина последовательности {x.size(1)} превышает максимальную {self._max_seq_len}")
|
||||||
|
|
||||||
|
# Эмбеддинги токенов и позиций
|
||||||
|
tok_out = self._token_embeddings(x) # [batch, seq_len, emb_size]
|
||||||
|
pos_out = self._position_embeddings(x.size(1)) # [seq_len, emb_size]
|
||||||
|
|
||||||
|
# Комбинирование
|
||||||
|
out = self._dropout(tok_out + pos_out.unsqueeze(0)) # [batch, seq_len, emb_size]
|
||||||
|
|
||||||
|
# Стек декодеров
|
||||||
|
for decoder in self._decoders:
|
||||||
|
out = decoder(out)
|
||||||
|
|
||||||
|
return self._linear(out) # [batch, seq_len, vocab_size]
|
||||||
|
|
||||||
|
|
||||||
|
# def forward(self, input_ids, attention_mask=None):
|
||||||
|
# B, T = input_ids.size()
|
||||||
|
# pos = torch.arange(0, T, device=input_ids.device).unsqueeze(0)
|
||||||
|
#
|
||||||
|
# x = self.token_emb(input_ids) + self.pos_emb(pos)
|
||||||
|
#
|
||||||
|
# for block in self.blocks:
|
||||||
|
# x = block(x, attention_mask)
|
||||||
|
#
|
||||||
|
# x = self.ln_f(x)
|
||||||
|
# logits = self.head(x)
|
||||||
|
# return logits
|
||||||
|
|
||||||
|
|
||||||
|
def generate(self,
|
||||||
|
x: torch.Tensor,
|
||||||
|
max_new_tokens: int,
|
||||||
|
do_sample: bool,
|
||||||
|
temperature: float = 1.0,
|
||||||
|
top_k: int = None,
|
||||||
|
top_p: float = None,
|
||||||
|
attention_mask: torch.Tensor = None, # Добавляем для совместимости с HF
|
||||||
|
**kwargs # Игнорируем остальные параметры
|
||||||
|
) -> torch.Tensor:
|
||||||
|
"""Авторегрессивная генерация текста.
|
||||||
|
|
||||||
|
Параметры:
|
||||||
|
x: Входной тензор с индексами токенов формы [batch_size, seq_len],
|
||||||
|
где batch_size - размер батча, seq_len - длина последовательности.
|
||||||
|
max_new_tokens: Максимальное количество новых токенов для генерации.
|
||||||
|
do_sample: Флаг выбора режима генерации:
|
||||||
|
- True: вероятностное сэмплирование
|
||||||
|
- False: жадный поиск (argmax)
|
||||||
|
temperature: Параметр температуры для сэмплирования:
|
||||||
|
- >1.0 - более случайные результаты
|
||||||
|
- 1.0 - нейтральное значение
|
||||||
|
- <1.0 - более предсказуемые результаты
|
||||||
|
Должна быть > 0 (по умолчанию: 1.0)
|
||||||
|
top_k: Если задан (и do_sample=True), используется top-k сэмплирование:
|
||||||
|
- Выбираются только top_k самых вероятных токенов
|
||||||
|
- Остальным токенам устанавливается вероятность 0
|
||||||
|
- None: отключено (по умолчанию)
|
||||||
|
top_p: Если задан (и do_sample=True), используется nucleus (top-p) сэмплирование:
|
||||||
|
- Выбираются токены с кумулятивной вероятностью ≤ top_p
|
||||||
|
- Гарантируется, что хотя бы один токен остаётся (даже если его вероятность > top_p)
|
||||||
|
- None: отключено (по умолчанию)
|
||||||
|
- Должен быть в диапазоне (0, 1]
|
||||||
|
|
||||||
|
Возвращает:
|
||||||
|
torch.Tensor: Тензор с расширенной последовательностью токенов формы
|
||||||
|
[batch_size, seq_len + max_new_tokens]
|
||||||
|
|
||||||
|
Исключения:
|
||||||
|
ValueError: Если входная последовательность длиннее max_seq_len
|
||||||
|
ValueError: Если temperature <= 0
|
||||||
|
ValueError: Если одновременно заданы top_k и top_p
|
||||||
|
ValueError: Если top_k задан и ≤ 0
|
||||||
|
ValueError: Если top_p задан и не в диапазоне (0, 1]
|
||||||
|
|
||||||
|
Примеры:
|
||||||
|
>>> # Жадная генерация
|
||||||
|
>>> output = model.generate(input_ids, max_new_tokens=10, do_sample=False)
|
||||||
|
>>>
|
||||||
|
>>> # Вероятностная генерация с top-k
|
||||||
|
>>> output = model.generate(input_ids, max_new_tokens=10, do_sample=True, top_k=50)
|
||||||
|
>>>
|
||||||
|
>>> # Nucleus sampling (top-p)
|
||||||
|
>>> output = model.generate(input_ids, max_new_tokens=10, do_sample=True, top_p=0.9)
|
||||||
|
>>>
|
||||||
|
>>> # Комбинация температуры и top-k
|
||||||
|
>>> output = model.generate(input_ids, max_new_tokens=10, do_sample=True,
|
||||||
|
... temperature=0.7, top_k=50)
|
||||||
|
|
||||||
|
Примечания:
|
||||||
|
1. Для детерминированных результатов в режиме сэмплирования
|
||||||
|
зафиксируйте random seed (torch.manual_seed).
|
||||||
|
2. Температура влияет только на режим сэмплирования (do_sample=True).
|
||||||
|
3. Одновременное использование top_k и top_p запрещено.
|
||||||
|
4. При do_sample=False параметры top_k, top_p и temperature игнорируются.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
x (torch.Tensor): Входной тензор с индексами токенов формы [batch_size, seq_len],
|
||||||
|
где batch_size - размер батча, seq_len - длина последовательности.
|
||||||
|
max_new_tokens (int): Максимальное количество новых токенов для генерации.
|
||||||
|
do_sample (bool): Флаг выбора режима генерации:
|
||||||
|
- True: вероятностное сэмплирование
|
||||||
|
- False: жадный поиск (argmax)
|
||||||
|
temperature (float): Параметр температуры для сэмплирования:
|
||||||
|
- >1.0 - более случайные результаты
|
||||||
|
- 1.0 - нейтральное значение
|
||||||
|
- <1.0 - более предсказуемые результаты
|
||||||
|
Должна быть > 0 (по умолчанию: 1.0)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
torch.Tensor: Тензор с расширенной последовательностью токенов формы
|
||||||
|
[batch_size, seq_len + max_new_tokens]
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: Если входная последовательность длиннее max_seq_len
|
||||||
|
ValueError: Если temperature <= 0
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
>>> # Жадная генерация
|
||||||
|
>>> output = model.generate(input_ids, max_new_tokens=10, do_sample=False)
|
||||||
|
>>>
|
||||||
|
>>> # Вероятностная генерация с температурой
|
||||||
|
>>> output = model.generate(input_ids, max_new_tokens=10, do_sample=True, temperature=0.7)
|
||||||
|
>>>
|
||||||
|
>>> # Более случайная генерация
|
||||||
|
>>> output = model.generate(input_ids, max_new_tokens=10, do_sample=True, temperature=1.5)
|
||||||
|
|
||||||
|
Note:
|
||||||
|
Для детерминированных результатов в режиме сэмплирования
|
||||||
|
зафиксируйте random seed (torch.manual_seed).
|
||||||
|
Температура влияет только на режим сэмплирования (do_sample=True).
|
||||||
|
"""
|
||||||
|
for _ in range(max_new_tokens):
|
||||||
|
# 1. Обрезаем вход, если последовательность слишком длинная
|
||||||
|
x_cond = x[:, -self._max_seq_len:]
|
||||||
|
|
||||||
|
# 2. Передаем последовательность в метод forward класса GPT и полуаем логиты.
|
||||||
|
logits = self.forward(x_cond)
|
||||||
|
|
||||||
|
# 3. Берем логиты для последнего токена
|
||||||
|
last_logits = logits[:, -1, :] # [batch_size, vocab_size]
|
||||||
|
|
||||||
|
# Масштабируем логиты температурой
|
||||||
|
if temperature > 0:
|
||||||
|
logits_scaled = last_logits / temperature
|
||||||
|
else:
|
||||||
|
logits_scaled = last_logits
|
||||||
|
|
||||||
|
if do_sample == True and top_k != None:
|
||||||
|
_, topk_indices = torch.topk(logits_scaled, top_k, dim=-1)
|
||||||
|
|
||||||
|
# # Заменим все НЕ top-k логиты на -inf
|
||||||
|
masked_logits = logits_scaled.clone()
|
||||||
|
vocab_size = logits_scaled.size(-1)
|
||||||
|
|
||||||
|
# создаём маску: 1, если токен НЕ в topk_indices
|
||||||
|
mask = torch.ones_like(logits_scaled, dtype=torch.uint8)
|
||||||
|
mask.scatter_(1, topk_indices, 0) # 0 там, где top-k индексы
|
||||||
|
masked_logits[mask.byte()] = float('-inf')
|
||||||
|
|
||||||
|
logits_scaled = masked_logits
|
||||||
|
|
||||||
|
if do_sample == True and top_p != None:
|
||||||
|
# 1. Применим softmax, чтобы получить вероятности:
|
||||||
|
probs = F.softmax(logits_scaled, dim=-1) # [B, vocab_size]
|
||||||
|
# 2. Отсортируем токены по убыванию вероятностей:
|
||||||
|
sorted_probs, sorted_indices = torch.sort(probs, descending=True, dim=-1)
|
||||||
|
# 3. Посчитаем кумулятивную сумму вероятностей:
|
||||||
|
cum_probs = torch.cumsum(sorted_probs, dim=-1) # [B, vocab_size]
|
||||||
|
# 4. Определим маску: оставить токены, пока сумма < top_p
|
||||||
|
sorted_mask = (cum_probs <= top_p).byte() # [B, vocab_size]
|
||||||
|
# Гарантируем, что хотя бы первый токен останется
|
||||||
|
sorted_mask[:, 0] = 1
|
||||||
|
# 5. Преобразуем маску обратно в оригинальный порядок:
|
||||||
|
# Создаём полную маску из 0
|
||||||
|
mask = torch.zeros_like(probs, dtype=torch.uint8)
|
||||||
|
# Устанавливаем 1 в местах нужных токенов
|
||||||
|
mask.scatter_(dim=1, index=sorted_indices, src=sorted_mask)
|
||||||
|
# 6. Зануляем логиты токенов вне топ-p:
|
||||||
|
logits_scaled[~mask] = float('-inf')
|
||||||
|
|
||||||
|
# 4. Применяем Softmax
|
||||||
|
probs = F.softmax(logits_scaled, dim=-1) # [batch_size, vocab_size]
|
||||||
|
|
||||||
|
|
||||||
|
if do_sample == True:
|
||||||
|
# 5. Если do_sample равен True, то отбираем токен случайно с помощью torch.multinomial
|
||||||
|
next_token = torch.multinomial(probs, num_samples=1) # [batch_size, 1]
|
||||||
|
else:
|
||||||
|
# 5. Если do_sample равен False, то выбираем токен с максимальной вероятностью
|
||||||
|
next_token = torch.argmax(probs, dim=-1, keepdim=True) # [batch_size, 1]
|
||||||
|
|
||||||
|
# 6. Добавляем его к последовательности
|
||||||
|
x = torch.cat([x, next_token], dim=1) # [batch_size, seq_len+1]
|
||||||
|
return x
|
||||||
|
|
||||||
|
# def generate(self, input_ids, max_length=50):
|
||||||
|
# for _ in range(max_length):
|
||||||
|
# logits = self.forward(input_ids)
|
||||||
|
# next_token = torch.argmax(logits[:, -1, :], dim=-1, keepdim=True)
|
||||||
|
# input_ids = torch.cat([input_ids, next_token], dim=1)
|
||||||
|
# return input_ids
|
||||||
0
llm/src/llm/py.typed
Normal file
0
llm/src/llm/py.typed
Normal file
19
llm/src/llm/tokenizers/__init__.py
Normal file
19
llm/src/llm/tokenizers/__init__.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
"""
|
||||||
|
Модуль токенизаторов для библиотеки llm.
|
||||||
|
|
||||||
|
Предоставляет различные реализации токенизаторов:
|
||||||
|
- BPE (Byte Pair Encoding) токенизатор
|
||||||
|
- Базовый интерфейс для создания собственных токенизаторов
|
||||||
|
|
||||||
|
Примеры использования:
|
||||||
|
>>> from llm.tokenizers import BPETokenizer, SimpleBPETokenizer
|
||||||
|
>>> tokenizer = BPETokenizer()
|
||||||
|
>>> tokenizer.train(["текст для обучения", "еще текст"])
|
||||||
|
>>> tokens = tokenizer.encode("привет мир")
|
||||||
|
>>> text = tokenizer.decode(tokens)
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .base_tokenizer import BaseTokenizer
|
||||||
|
from .bpe_tokenizer import BPETokenizer, SimpleBPETokenizer
|
||||||
|
|
||||||
|
__all__ = ["BaseTokenizer", "BPETokenizer", "SimpleBPETokenizer"]
|
||||||
174
llm/src/llm/tokenizers/base_tokenizer.py
Normal file
174
llm/src/llm/tokenizers/base_tokenizer.py
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
"""
|
||||||
|
Базовый класс для токенизаторов.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from typing import List, Dict, Any, Optional
|
||||||
|
import json
|
||||||
|
|
||||||
|
|
||||||
|
class BaseTokenizer(ABC):
|
||||||
|
"""
|
||||||
|
Абстрактный базовый класс для всех токенизаторов.
|
||||||
|
|
||||||
|
Определяет общий интерфейс для токенизации текста.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.vocab: Dict[str, int] = {}
|
||||||
|
self.inverse_vocab: Dict[int, str] = {}
|
||||||
|
self.vocab_size: int = 0
|
||||||
|
|
||||||
|
# Специальные токены
|
||||||
|
self.pad_token = "<pad>"
|
||||||
|
self.unk_token = "<unk>"
|
||||||
|
self.bos_token = "<bos>"
|
||||||
|
self.eos_token = "<eos>"
|
||||||
|
|
||||||
|
self.pad_token_id: Optional[int] = None
|
||||||
|
self.unk_token_id: Optional[int] = None
|
||||||
|
self.bos_token_id: Optional[int] = None
|
||||||
|
self.eos_token_id: Optional[int] = None
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def train(self, texts: List[str], vocab_size: int = 1000, **kwargs):
|
||||||
|
"""
|
||||||
|
Обучение токенизатора на текстах.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
texts: Список текстов для обучения
|
||||||
|
vocab_size: Желаемый размер словаря
|
||||||
|
**kwargs: Дополнительные параметры обучения
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def encode(self, text: str, **kwargs) -> List[int]:
|
||||||
|
"""
|
||||||
|
Кодирование текста в последовательность токенов.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text: Входной текст
|
||||||
|
**kwargs: Дополнительные параметры кодирования
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List[int]: Список идентификаторов токенов
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def decode(self, tokens: List[int], **kwargs) -> str:
|
||||||
|
"""
|
||||||
|
Декодирование последовательности токенов в текст.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
tokens: Список идентификаторов токенов
|
||||||
|
**kwargs: Дополнительные параметры декодирования
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Декодированный текст
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def tokenize(self, text: str, **kwargs) -> List[str]:
|
||||||
|
"""
|
||||||
|
Токенизация текста в список строковых токенов.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text: Входной текст
|
||||||
|
**kwargs: Дополнительные параметры
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List[str]: Список токенов
|
||||||
|
"""
|
||||||
|
token_ids = self.encode(text, **kwargs)
|
||||||
|
return [self.inverse_vocab.get(token_id, self.unk_token) for token_id in token_ids]
|
||||||
|
|
||||||
|
def get_vocab(self) -> Dict[str, int]:
|
||||||
|
"""Возвращает словарь токенизатора."""
|
||||||
|
return self.vocab.copy()
|
||||||
|
|
||||||
|
def get_vocab_size(self) -> int:
|
||||||
|
"""Возвращает размер словаря."""
|
||||||
|
return self.vocab_size
|
||||||
|
|
||||||
|
def add_special_tokens(self, special_tokens: List[str]):
|
||||||
|
"""
|
||||||
|
Добавляет специальные токены в словарь.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
special_tokens: Список специальных токенов
|
||||||
|
"""
|
||||||
|
for token in special_tokens:
|
||||||
|
if token not in self.vocab:
|
||||||
|
token_id = len(self.vocab)
|
||||||
|
self.vocab[token] = token_id
|
||||||
|
self.inverse_vocab[token_id] = token
|
||||||
|
self.vocab_size += 1
|
||||||
|
|
||||||
|
# Обновляем ID специальных токенов
|
||||||
|
self.pad_token_id = self.vocab.get(self.pad_token)
|
||||||
|
self.unk_token_id = self.vocab.get(self.unk_token)
|
||||||
|
self.bos_token_id = self.vocab.get(self.bos_token)
|
||||||
|
self.eos_token_id = self.vocab.get(self.eos_token)
|
||||||
|
|
||||||
|
def save(self, filepath: str):
|
||||||
|
"""
|
||||||
|
Сохраняет токенизатор в файл.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
filepath: Путь для сохранения
|
||||||
|
"""
|
||||||
|
config = {
|
||||||
|
'vocab': self.vocab,
|
||||||
|
'vocab_size': self.vocab_size,
|
||||||
|
'pad_token': self.pad_token,
|
||||||
|
'unk_token': self.unk_token,
|
||||||
|
'bos_token': self.bos_token,
|
||||||
|
'eos_token': self.eos_token,
|
||||||
|
'tokenizer_type': self.__class__.__name__
|
||||||
|
}
|
||||||
|
|
||||||
|
with open(filepath, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(config, f, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def load(cls, filepath: str):
|
||||||
|
"""
|
||||||
|
Загружает токенизатор из файла.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
filepath: Путь к файлу
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
BaseTokenizer: Загруженный токенизатор
|
||||||
|
"""
|
||||||
|
with open(filepath, 'r', encoding='utf-8') as f:
|
||||||
|
config = json.load(f)
|
||||||
|
|
||||||
|
# Создаем экземпляр токенизатора
|
||||||
|
tokenizer = cls()
|
||||||
|
tokenizer.vocab = config['vocab']
|
||||||
|
tokenizer.vocab_size = config['vocab_size']
|
||||||
|
tokenizer.pad_token = config['pad_token']
|
||||||
|
tokenizer.unk_token = config['unk_token']
|
||||||
|
tokenizer.bos_token = config['bos_token']
|
||||||
|
tokenizer.eos_token = config['eos_token']
|
||||||
|
|
||||||
|
# Создаем обратный словарь
|
||||||
|
tokenizer.inverse_vocab = {v: k for k, v in tokenizer.vocab.items()}
|
||||||
|
|
||||||
|
# Обновляем ID специальных токенов
|
||||||
|
tokenizer.pad_token_id = tokenizer.vocab.get(tokenizer.pad_token)
|
||||||
|
tokenizer.unk_token_id = tokenizer.vocab.get(tokenizer.unk_token)
|
||||||
|
tokenizer.bos_token_id = tokenizer.vocab.get(tokenizer.bos_token)
|
||||||
|
tokenizer.eos_token_id = tokenizer.vocab.get(tokenizer.eos_token)
|
||||||
|
|
||||||
|
return tokenizer
|
||||||
|
|
||||||
|
def __len__(self) -> int:
|
||||||
|
"""Возвращает размер словаря."""
|
||||||
|
return self.vocab_size
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"{self.__class__.__name__}(vocab_size={self.vocab_size})"
|
||||||
428
llm/src/llm/tokenizers/bpe_tokenizer copy.py
Normal file
428
llm/src/llm/tokenizers/bpe_tokenizer copy.py
Normal file
@@ -0,0 +1,428 @@
|
|||||||
|
"""
|
||||||
|
BPE (Byte Pair Encoding) токенизатор.
|
||||||
|
|
||||||
|
Реализация алгоритма BPE для токенизации текста.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
from collections import defaultdict, Counter
|
||||||
|
from typing import List, Dict, Tuple, Optional
|
||||||
|
from .base_tokenizer import BaseTokenizer
|
||||||
|
|
||||||
|
|
||||||
|
class BPETokenizer(BaseTokenizer):
|
||||||
|
"""
|
||||||
|
BPE токенизатор для обработки текста.
|
||||||
|
|
||||||
|
Реализует алгоритм Byte Pair Encoding для создания субсловных токенов.
|
||||||
|
|
||||||
|
Примеры использования:
|
||||||
|
>>> tokenizer = BPETokenizer()
|
||||||
|
>>> tokenizer.train(["пример текста для обучения"], vocab_size=1000)
|
||||||
|
>>> tokens = tokenizer.encode("новый текст")
|
||||||
|
>>> text = tokenizer.decode(tokens)
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.merges: Dict[Tuple[str, str], int] = {}
|
||||||
|
self.pattern = r"""'s|'t|'re|'ve|'m|'ll|'d| ?\p{L}+| ?\p{N}+| ?[^\s\p{L}\p{N}]+|\s+(?!\S)|\s+"""
|
||||||
|
self.compiled_pattern = re.compile(self.pattern, re.UNICODE)
|
||||||
|
|
||||||
|
def train(self, texts: List[str], vocab_size: int = 1000, **kwargs):
|
||||||
|
"""
|
||||||
|
Обучение BPE токенизатора на текстах.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
texts: Список текстов для обучения
|
||||||
|
vocab_size: Желаемый размер словаря
|
||||||
|
**kwargs: Дополнительные параметры
|
||||||
|
- min_frequency: Минимальная частота для мерджа
|
||||||
|
- special_tokens: Список специальных токенов
|
||||||
|
"""
|
||||||
|
# Инициализация базового словаря
|
||||||
|
self._initialize_vocab()
|
||||||
|
|
||||||
|
# Добавляем специальные токены если указаны
|
||||||
|
special_tokens = kwargs.get('special_tokens', [self.pad_token, self.unk_token, self.bos_token, self.eos_token])
|
||||||
|
self.add_special_tokens(special_tokens)
|
||||||
|
|
||||||
|
# Предобработка текстов
|
||||||
|
words = self._preprocess_texts(texts)
|
||||||
|
|
||||||
|
# Получаем начальные токены
|
||||||
|
vocab = self._get_initial_vocab(words)
|
||||||
|
|
||||||
|
# Выполняем BPE мерджи
|
||||||
|
self._perform_merges(vocab, vocab_size, kwargs.get('min_frequency', 2))
|
||||||
|
|
||||||
|
# Строим финальный словарь
|
||||||
|
self._build_final_vocab()
|
||||||
|
|
||||||
|
def _initialize_vocab(self):
|
||||||
|
"""Инициализирует базовый словарь."""
|
||||||
|
self.vocab.clear()
|
||||||
|
self.inverse_vocab.clear()
|
||||||
|
self.merges.clear()
|
||||||
|
self.vocab_size = 0
|
||||||
|
|
||||||
|
def _preprocess_texts(self, texts: List[str]) -> List[List[str]]:
|
||||||
|
"""
|
||||||
|
Предобработка текстов для обучения.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
texts: Список текстов
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List[List[str]]: Предобработанные слова
|
||||||
|
"""
|
||||||
|
words = []
|
||||||
|
for text in texts:
|
||||||
|
# Базовая нормализация
|
||||||
|
text = text.lower().strip()
|
||||||
|
# Токенизация на слова
|
||||||
|
tokens = self.compiled_pattern.findall(text)
|
||||||
|
words.append(tokens)
|
||||||
|
return words
|
||||||
|
|
||||||
|
def _get_initial_vocab(self, words: List[List[str]]) -> Dict[str, int]:
|
||||||
|
"""
|
||||||
|
Создает начальный словарь из символов.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
words: Список токенизированных текстов
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict[str, int]: Начальный словарь частот
|
||||||
|
"""
|
||||||
|
vocab = Counter()
|
||||||
|
for word_list in words:
|
||||||
|
for word in word_list:
|
||||||
|
# Разбиваем слово на символы и добавляем специальный символ конца слова
|
||||||
|
chars = list(word) + ['</w>']
|
||||||
|
vocab.update([''.join(chars[i:i+1]) for i in range(len(chars))])
|
||||||
|
return vocab
|
||||||
|
|
||||||
|
def _perform_merges(self, vocab: Dict[str, int], target_vocab_size: int, min_frequency: int):
|
||||||
|
"""
|
||||||
|
Выполняет BPE мерджи до достижения целевого размера словаря.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
vocab: Начальный словарь
|
||||||
|
target_vocab_size: Целевой размер словаря
|
||||||
|
min_frequency: Минимальная частота для мерджа
|
||||||
|
"""
|
||||||
|
current_vocab_size = len(vocab) + len(self.vocab)
|
||||||
|
|
||||||
|
while current_vocab_size < target_vocab_size:
|
||||||
|
# Находим наиболее частую пару
|
||||||
|
pairs = self._get_stats(vocab)
|
||||||
|
if not pairs:
|
||||||
|
break
|
||||||
|
|
||||||
|
best_pair = max(pairs, key=pairs.get)
|
||||||
|
if pairs[best_pair] < min_frequency:
|
||||||
|
break
|
||||||
|
|
||||||
|
# Выполняем мердж
|
||||||
|
vocab = self._merge_vocab(vocab, best_pair)
|
||||||
|
self.merges[best_pair] = len(self.merges)
|
||||||
|
current_vocab_size += 1
|
||||||
|
|
||||||
|
def _get_stats(self, vocab: Dict[str, int]) -> Dict[Tuple[str, str], int]:
|
||||||
|
"""
|
||||||
|
Собирает статистику по парам символов.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
vocab: Словарь токенов
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict[Tuple[str, str], int]: Частоты пар
|
||||||
|
"""
|
||||||
|
pairs = defaultdict(int)
|
||||||
|
for word, freq in vocab.items():
|
||||||
|
symbols = word.split()
|
||||||
|
for i in range(len(symbols) - 1):
|
||||||
|
pairs[symbols[i], symbols[i + 1]] += freq
|
||||||
|
return pairs
|
||||||
|
|
||||||
|
def _merge_vocab(self, vocab: Dict[str, int], pair: Tuple[str, str]) -> Dict[str, int]:
|
||||||
|
"""
|
||||||
|
Объединяет пару символов в словаре.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
vocab: Исходный словарь
|
||||||
|
pair: Пара для объединения
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict[str, int]: Обновленный словарь
|
||||||
|
"""
|
||||||
|
new_vocab = {}
|
||||||
|
bigram = re.compile(r'(?<!\\S)' + re.escape(pair[0]) + r' ' + re.escape(pair[1]) + r'(?!\\S)')
|
||||||
|
replacement = pair[0] + pair[1]
|
||||||
|
|
||||||
|
for word in vocab:
|
||||||
|
new_word = bigram.sub(replacement, word)
|
||||||
|
new_vocab[new_word] = vocab[word]
|
||||||
|
|
||||||
|
return new_vocab
|
||||||
|
|
||||||
|
def _build_final_vocab(self):
|
||||||
|
"""Строит финальный словарь токенизатора."""
|
||||||
|
# Собираем все уникальные токены из мерджей
|
||||||
|
all_tokens = set()
|
||||||
|
|
||||||
|
# Добавляем специальные токены
|
||||||
|
all_tokens.update([self.pad_token, self.unk_token, self.bos_token, self.eos_token])
|
||||||
|
|
||||||
|
# Добавляем токены из мерджей
|
||||||
|
for pair in self.merges:
|
||||||
|
all_tokens.update(pair)
|
||||||
|
|
||||||
|
# Создаем словарь
|
||||||
|
for i, token in enumerate(sorted(all_tokens)):
|
||||||
|
self.vocab[token] = i
|
||||||
|
|
||||||
|
self.inverse_vocab = {v: k for k, v in self.vocab.items()}
|
||||||
|
self.vocab_size = len(self.vocab)
|
||||||
|
|
||||||
|
# Обновляем ID специальных токенов
|
||||||
|
self.pad_token_id = self.vocab.get(self.pad_token)
|
||||||
|
self.unk_token_id = self.vocab.get(self.unk_token)
|
||||||
|
self.bos_token_id = self.vocab.get(self.bos_token)
|
||||||
|
self.eos_token_id = self.vocab.get(self.eos_token)
|
||||||
|
|
||||||
|
def encode(self, text: str, **kwargs) -> List[int]:
|
||||||
|
"""
|
||||||
|
Кодирует текст в последовательность токенов.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text: Входной текст
|
||||||
|
**kwargs: Дополнительные параметры
|
||||||
|
- add_special_tokens: Добавлять специальные токены
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List[int]: Список идентификаторов токенов
|
||||||
|
"""
|
||||||
|
add_special_tokens = kwargs.get('add_special_tokens', False)
|
||||||
|
|
||||||
|
# Токенизация текста
|
||||||
|
tokens = self.compiled_pattern.findall(text)
|
||||||
|
|
||||||
|
# Применяем BPE к каждому токену
|
||||||
|
bpe_tokens = []
|
||||||
|
for token in tokens:
|
||||||
|
# Преобразуем токен в BPE представление
|
||||||
|
bpe_token = self._apply_bpe(token)
|
||||||
|
bpe_tokens.extend(bpe_token)
|
||||||
|
|
||||||
|
# Конвертируем в ID
|
||||||
|
token_ids = []
|
||||||
|
for token in bpe_tokens:
|
||||||
|
token_id = self.vocab.get(token, self.unk_token_id)
|
||||||
|
if token_id is not None:
|
||||||
|
token_ids.append(token_id)
|
||||||
|
|
||||||
|
# Добавляем специальные токены если нужно
|
||||||
|
if add_special_tokens:
|
||||||
|
if self.bos_token_id is not None:
|
||||||
|
token_ids.insert(0, self.bos_token_id)
|
||||||
|
if self.eos_token_id is not None:
|
||||||
|
token_ids.append(self.eos_token_id)
|
||||||
|
|
||||||
|
return token_ids
|
||||||
|
|
||||||
|
def _apply_bpe(self, token: str) -> List[str]:
|
||||||
|
"""
|
||||||
|
Применяет BPE к одному токену.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
token: Входной токен
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List[str]: Список BPE токенов
|
||||||
|
"""
|
||||||
|
# Простая реализация - в реальной реализации нужно применять обученные мерджи
|
||||||
|
word = token + '</w>'
|
||||||
|
tokens = [word[i:i+1] for i in range(len(word))]
|
||||||
|
|
||||||
|
# Применяем мерджи (упрощенная версия)
|
||||||
|
# В полной реализации нужно применять все обученные мерджи
|
||||||
|
for pair in self.merges:
|
||||||
|
i = 0
|
||||||
|
while i < len(tokens) - 1:
|
||||||
|
if tokens[i] == pair[0] and tokens[i + 1] == pair[1]:
|
||||||
|
tokens[i] = tokens[i] + tokens[i + 1]
|
||||||
|
del tokens[i + 1]
|
||||||
|
else:
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
return tokens
|
||||||
|
|
||||||
|
def decode(self, tokens: List[int], **kwargs) -> str:
|
||||||
|
"""
|
||||||
|
Декодирует последовательность токенов в текст.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
tokens: Список идентификаторов токенов
|
||||||
|
**kwargs: Дополнительные параметры
|
||||||
|
- skip_special_tokens: Пропускать специальные токены
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Декодированный текст
|
||||||
|
"""
|
||||||
|
skip_special_tokens = kwargs.get('skip_special_tokens', True)
|
||||||
|
|
||||||
|
# Конвертируем ID в токены
|
||||||
|
token_strings = []
|
||||||
|
for token_id in tokens:
|
||||||
|
token = self.inverse_vocab.get(token_id, self.unk_token)
|
||||||
|
|
||||||
|
# Пропускаем специальные токены если нужно
|
||||||
|
if skip_special_tokens and token in [self.pad_token, self.unk_token, self.bos_token, self.eos_token]:
|
||||||
|
continue
|
||||||
|
|
||||||
|
token_strings.append(token)
|
||||||
|
|
||||||
|
# Объединяем токены в текст
|
||||||
|
text = ''.join(token_strings)
|
||||||
|
|
||||||
|
# Убираем маркер конца слова
|
||||||
|
text = text.replace('</w>', ' ')
|
||||||
|
|
||||||
|
return text.strip()
|
||||||
|
|
||||||
|
def save(self, filepath: str):
|
||||||
|
"""
|
||||||
|
Сохраняет BPE токенизатор в файл.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
filepath: Путь для сохранения
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
|
||||||
|
config = {
|
||||||
|
'vocab': self.vocab,
|
||||||
|
'merges': {f"{k[0]} {k[1]}": v for k, v in self.merges.items()},
|
||||||
|
'vocab_size': self.vocab_size,
|
||||||
|
'pad_token': self.pad_token,
|
||||||
|
'unk_token': self.unk_token,
|
||||||
|
'bos_token': self.bos_token,
|
||||||
|
'eos_token': self.eos_token,
|
||||||
|
'pattern': self.pattern,
|
||||||
|
'tokenizer_type': self.__class__.__name__
|
||||||
|
}
|
||||||
|
|
||||||
|
with open(filepath, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(config, f, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def load(cls, filepath: str):
|
||||||
|
"""
|
||||||
|
Загружает BPE токенизатор из файла.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
filepath: Путь к файлу
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
BPETokenizer: Загруженный токенизатор
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
|
||||||
|
with open(filepath, 'r', encoding='utf-8') as f:
|
||||||
|
config = json.load(f)
|
||||||
|
|
||||||
|
tokenizer = cls()
|
||||||
|
tokenizer.vocab = config['vocab']
|
||||||
|
tokenizer.vocab_size = config['vocab_size']
|
||||||
|
tokenizer.pad_token = config['pad_token']
|
||||||
|
tokenizer.unk_token = config['unk_token']
|
||||||
|
tokenizer.bos_token = config['bos_token']
|
||||||
|
tokenizer.eos_token = config['eos_token']
|
||||||
|
tokenizer.pattern = config.get('pattern', tokenizer.pattern)
|
||||||
|
tokenizer.compiled_pattern = re.compile(tokenizer.pattern, re.UNICODE)
|
||||||
|
|
||||||
|
# Восстанавливаем мерджи
|
||||||
|
merges = config.get('merges', {})
|
||||||
|
tokenizer.merges = {}
|
||||||
|
for k, v in merges.items():
|
||||||
|
parts = k.split()
|
||||||
|
if len(parts) == 2:
|
||||||
|
tokenizer.merges[(parts[0], parts[1])] = v
|
||||||
|
|
||||||
|
# Создаем обратный словарь
|
||||||
|
tokenizer.inverse_vocab = {v: k for k, v in tokenizer.vocab.items()}
|
||||||
|
|
||||||
|
# Обновляем ID специальных токенов
|
||||||
|
tokenizer.pad_token_id = tokenizer.vocab.get(tokenizer.pad_token)
|
||||||
|
tokenizer.unk_token_id = tokenizer.vocab.get(tokenizer.unk_token)
|
||||||
|
tokenizer.bos_token_id = tokenizer.vocab.get(tokenizer.bos_token)
|
||||||
|
tokenizer.eos_token_id = tokenizer.vocab.get(tokenizer.eos_token)
|
||||||
|
|
||||||
|
return tokenizer
|
||||||
|
|
||||||
|
|
||||||
|
# Упрощенная версия для быстрого старта
|
||||||
|
class SimpleBPETokenizer(BPETokenizer):
|
||||||
|
"""
|
||||||
|
Упрощенная версия BPE токенизатора для демонстрации.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def train(self, texts: List[str], vocab_size: int = 1000, **kwargs):
|
||||||
|
"""Упрощенное обучение для демонстрации."""
|
||||||
|
# Инициализация базового словаря
|
||||||
|
self._initialize_vocab()
|
||||||
|
|
||||||
|
# Добавляем базовые токены
|
||||||
|
special_tokens = [self.pad_token, self.unk_token, self.bos_token, self.eos_token]
|
||||||
|
self.add_special_tokens(special_tokens)
|
||||||
|
|
||||||
|
# Простая реализация - собираем все символы
|
||||||
|
all_chars = set()
|
||||||
|
for text in texts:
|
||||||
|
all_chars.update(text)
|
||||||
|
|
||||||
|
# Добавляем символы в словарь
|
||||||
|
for char in sorted(all_chars):
|
||||||
|
if char not in self.vocab:
|
||||||
|
self.vocab[char] = len(self.vocab)
|
||||||
|
|
||||||
|
self.inverse_vocab = {v: k for k, v in self.vocab.items()}
|
||||||
|
self.vocab_size = len(self.vocab)
|
||||||
|
|
||||||
|
# Обновляем ID специальных токенов
|
||||||
|
self.pad_token_id = self.vocab.get(self.pad_token)
|
||||||
|
self.unk_token_id = self.vocab.get(self.unk_token)
|
||||||
|
self.bos_token_id = self.vocab.get(self.bos_token)
|
||||||
|
self.eos_token_id = self.vocab.get(self.eos_token)
|
||||||
|
|
||||||
|
def encode(self, text: str, **kwargs) -> List[int]:
|
||||||
|
"""Упрощенное кодирование - разбиваем на символы."""
|
||||||
|
add_special_tokens = kwargs.get('add_special_tokens', False)
|
||||||
|
|
||||||
|
token_ids = []
|
||||||
|
for char in text:
|
||||||
|
token_id = self.vocab.get(char, self.unk_token_id)
|
||||||
|
if token_id is not None:
|
||||||
|
token_ids.append(token_id)
|
||||||
|
|
||||||
|
if add_special_tokens:
|
||||||
|
if self.bos_token_id is not None:
|
||||||
|
token_ids.insert(0, self.bos_token_id)
|
||||||
|
if self.eos_token_id is not None:
|
||||||
|
token_ids.append(self.eos_token_id)
|
||||||
|
|
||||||
|
return token_ids
|
||||||
|
|
||||||
|
def decode(self, tokens: List[int], **kwargs) -> str:
|
||||||
|
"""Упрощенное декодирование."""
|
||||||
|
skip_special_tokens = kwargs.get('skip_special_tokens', True)
|
||||||
|
|
||||||
|
chars = []
|
||||||
|
for token_id in tokens:
|
||||||
|
char = self.inverse_vocab.get(token_id, self.unk_token)
|
||||||
|
if skip_special_tokens and char in [self.pad_token, self.unk_token, self.bos_token, self.eos_token]:
|
||||||
|
continue
|
||||||
|
chars.append(char)
|
||||||
|
|
||||||
|
return ''.join(chars)
|
||||||
207
llm/src/llm/tokenizers/bpe_tokenizer.py
Normal file
207
llm/src/llm/tokenizers/bpe_tokenizer.py
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
"""
|
||||||
|
BPE (Byte Pair Encoding) токенизатор.
|
||||||
|
|
||||||
|
Реализация алгоритма BPE для токенизации текста.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import List, Dict, Tuple, Optional
|
||||||
|
from .base_tokenizer import BaseTokenizer
|
||||||
|
|
||||||
|
|
||||||
|
class BPETokenizer(BaseTokenizer):
|
||||||
|
"""
|
||||||
|
BPE токенизатор для обработки текста.
|
||||||
|
|
||||||
|
Реализует алгоритм Byte Pair Encoding для создания субсловных токенов.
|
||||||
|
Использует вашу реализацию BPE.
|
||||||
|
|
||||||
|
Примеры использования:
|
||||||
|
>>> tokenizer = BPETokenizer()
|
||||||
|
>>> tokenizer.train(["пример текста для обучения"], vocab_size=1000)
|
||||||
|
>>> tokens = tokenizer.encode("новый текст")
|
||||||
|
>>> text = tokenizer.decode(tokens)
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.merges: Dict[Tuple[str, str], int] = {}
|
||||||
|
self.vocab_list: List[str] = []
|
||||||
|
|
||||||
|
def train(self, texts: List[str], vocab_size: int = 1000, **kwargs):
|
||||||
|
"""
|
||||||
|
Обучение BPE токенизатора на текстах.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
texts: Список текстов для обучения
|
||||||
|
vocab_size: Желаемый размер словаря
|
||||||
|
**kwargs: Дополнительные параметры
|
||||||
|
- special_tokens: Список специальных токенов
|
||||||
|
"""
|
||||||
|
# Объединяем все тексты в одну строку для обучения
|
||||||
|
combined_text = " ".join(texts)
|
||||||
|
|
||||||
|
# 1. Получаем уникальные токены (символы)
|
||||||
|
unique_tokens = sorted(set(combined_text))
|
||||||
|
tokens = unique_tokens.copy()
|
||||||
|
|
||||||
|
# 2. Разбиваем текст на токены-символы
|
||||||
|
sequence = list(combined_text)
|
||||||
|
|
||||||
|
# 3. Объединяем токены до достижения нужного размера словаря
|
||||||
|
while len(tokens) < vocab_size:
|
||||||
|
# Считаем частоты пар
|
||||||
|
pair_freq = {}
|
||||||
|
for i in range(len(sequence) - 1):
|
||||||
|
pair = (sequence[i], sequence[i + 1])
|
||||||
|
if pair not in pair_freq:
|
||||||
|
pair_freq[pair] = 0
|
||||||
|
pair_freq[pair] += 1
|
||||||
|
|
||||||
|
if not pair_freq:
|
||||||
|
break # нет пар — выходим
|
||||||
|
|
||||||
|
# Находим самую частую пару (в случае равенства — та, что встретилась первой)
|
||||||
|
most_frequent_pair = max(pair_freq.items(), key=lambda x: (x[1], -self._pair_first_index(sequence, x[0])))[0]
|
||||||
|
|
||||||
|
# Создаем новый токен
|
||||||
|
new_token = most_frequent_pair[0] + most_frequent_pair[1]
|
||||||
|
tokens.append(new_token)
|
||||||
|
|
||||||
|
i = 0
|
||||||
|
new_sequence = []
|
||||||
|
|
||||||
|
while i < len(sequence):
|
||||||
|
if i < len(sequence) - 1 and (sequence[i], sequence[i + 1]) == most_frequent_pair:
|
||||||
|
new_sequence.append(new_token)
|
||||||
|
i += 2 # пропускаем два символа — заменённую пару
|
||||||
|
else:
|
||||||
|
new_sequence.append(sequence[i])
|
||||||
|
i += 1
|
||||||
|
sequence = new_sequence
|
||||||
|
|
||||||
|
# 4. Создаем словари
|
||||||
|
self.vocab_list = tokens.copy()
|
||||||
|
self.vocab = dict(zip(tokens, range(vocab_size)))
|
||||||
|
self.inverse_vocab = dict(zip(range(vocab_size), tokens))
|
||||||
|
self.vocab_size = len(self.vocab)
|
||||||
|
|
||||||
|
# Добавляем специальные токены если указаны
|
||||||
|
special_tokens = kwargs.get('special_tokens', [self.pad_token, self.unk_token, self.bos_token, self.eos_token])
|
||||||
|
self.add_special_tokens(special_tokens)
|
||||||
|
|
||||||
|
def _pair_first_index(self, sequence, pair):
|
||||||
|
"""Находит первый индекс пары в последовательности."""
|
||||||
|
for i in range(len(sequence) - 1):
|
||||||
|
if (sequence[i], sequence[i + 1]) == pair:
|
||||||
|
return i
|
||||||
|
return float('inf') # если пара не найдена (в теории не должно случиться)
|
||||||
|
|
||||||
|
def encode(self, text: str, **kwargs) -> List[int]:
|
||||||
|
"""
|
||||||
|
Кодирует текст в последовательность токенов.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text: Входной текст
|
||||||
|
**kwargs: Дополнительные параметры
|
||||||
|
- add_special_tokens: Добавлять специальные токены
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List[int]: Список идентификаторов токенов
|
||||||
|
"""
|
||||||
|
add_special_tokens = kwargs.get('add_special_tokens', False)
|
||||||
|
|
||||||
|
# 1. Разбиваем текст на токены-символы
|
||||||
|
sequence = list(text)
|
||||||
|
# 2. Инициализация пустого списка токенов
|
||||||
|
tokens = []
|
||||||
|
# 3. Установить i = 0
|
||||||
|
i = 0
|
||||||
|
while i < len(text):
|
||||||
|
# 3.1 Найти все токены в словаре, начинающиеся с text[i]
|
||||||
|
start_char = text[i]
|
||||||
|
result = [token for token in self.vocab_list if token.startswith(start_char)]
|
||||||
|
# 3.2 Выбрать самый длинный подходящий токен
|
||||||
|
find_token = self._find_max_matching_token(text[i:], result)
|
||||||
|
if find_token is None:
|
||||||
|
# Обработка неизвестного символа
|
||||||
|
tokens.append(text[i]) # Добавляем сам символ как токен
|
||||||
|
i += 1
|
||||||
|
else:
|
||||||
|
# 3.3 Добавить токен в результат
|
||||||
|
tokens.append(find_token)
|
||||||
|
# 3.4 Увеличить i на длину токена
|
||||||
|
i += len(find_token)
|
||||||
|
|
||||||
|
# 4. Заменить токены на их ID
|
||||||
|
token_ids = self._tokens_to_ids(tokens)
|
||||||
|
|
||||||
|
# Заменяем -1 на unk_token_id
|
||||||
|
token_ids = [tid if tid != -1 else self.unk_token_id for tid in token_ids]
|
||||||
|
|
||||||
|
# Добавляем специальные токены если нужно
|
||||||
|
if add_special_tokens:
|
||||||
|
if self.bos_token_id is not None:
|
||||||
|
token_ids.insert(0, self.bos_token_id)
|
||||||
|
if self.eos_token_id is not None:
|
||||||
|
token_ids.append(self.eos_token_id)
|
||||||
|
|
||||||
|
return token_ids
|
||||||
|
|
||||||
|
def _find_max_matching_token(self, text: str, tokens: list) -> Optional[str]:
|
||||||
|
"""Находит самый длинный токен из списка, с которого начинается текст"""
|
||||||
|
matching = [token for token in tokens if text.startswith(token)]
|
||||||
|
return max(matching, key=len) if matching else None
|
||||||
|
|
||||||
|
def _tokens_to_ids(self, tokens: List[str]) -> List[int]:
|
||||||
|
"""Конвертирует список токенов в их ID с обработкой неизвестных токенов"""
|
||||||
|
ids = []
|
||||||
|
for token in tokens:
|
||||||
|
if token in self.vocab:
|
||||||
|
ids.append(self.vocab[token])
|
||||||
|
else:
|
||||||
|
ids.append(-1) # Специальное значение
|
||||||
|
return ids
|
||||||
|
|
||||||
|
def decode(self, tokens: List[int], **kwargs) -> str:
|
||||||
|
"""
|
||||||
|
Декодирует последовательность токенов в текст.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
tokens: Список идентификаторов токенов
|
||||||
|
**kwargs: Дополнительные параметры
|
||||||
|
- skip_special_tokens: Пропускать специальные токены
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Декодированный текст
|
||||||
|
"""
|
||||||
|
skip_special_tokens = kwargs.get('skip_special_tokens', True)
|
||||||
|
|
||||||
|
# Фильтруем специальные токены если нужно
|
||||||
|
if skip_special_tokens:
|
||||||
|
tokens = [tid for tid in tokens if tid not in [
|
||||||
|
self.pad_token_id, self.unk_token_id, self.bos_token_id, self.eos_token_id
|
||||||
|
]]
|
||||||
|
|
||||||
|
# Конвертируем ID в токены
|
||||||
|
token_strings = self._ids_to_tokens(tokens)
|
||||||
|
|
||||||
|
# Объединяем токены в текст
|
||||||
|
return ''.join(token_strings)
|
||||||
|
|
||||||
|
def _ids_to_tokens(self, ids: List[int]) -> List[str]:
|
||||||
|
"""Конвертирует список Ids в их tokens"""
|
||||||
|
tokens = []
|
||||||
|
for token_id in ids:
|
||||||
|
if token_id in self.inverse_vocab:
|
||||||
|
tokens.append(self.inverse_vocab[token_id])
|
||||||
|
else:
|
||||||
|
tokens.append(self.unk_token) # Специальное значение
|
||||||
|
return tokens
|
||||||
|
|
||||||
|
|
||||||
|
class SimpleBPETokenizer(BPETokenizer):
|
||||||
|
"""
|
||||||
|
Упрощенная версия BPE токенизатора для демонстрации.
|
||||||
|
Наследует вашу реализацию, но может быть упрощена при необходимости.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
2
llm/src/llm/training/__init__.py
Normal file
2
llm/src/llm/training/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
def hello() -> str:
|
||||||
|
return "Hello from llm!"
|
||||||
142
llm/src/llm/training/dataset.py
Normal file
142
llm/src/llm/training/dataset.py
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
import torch
|
||||||
|
from torch.utils.data import Dataset
|
||||||
|
from typing import List, Any
|
||||||
|
|
||||||
|
|
||||||
|
class TextDataset(Dataset):
|
||||||
|
"""
|
||||||
|
Простой датасет для языкового моделирования (LLM).
|
||||||
|
Работает с любым токенизатором, реализующим интерфейс BaseTokenizer.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, texts: List[str], tokenizer: Any, block_size: int = 128):
|
||||||
|
"""
|
||||||
|
Инициализация датасета.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
texts: Список текстов для обучения
|
||||||
|
tokenizer: Токенизатор с методами encode/decode
|
||||||
|
block_size: Максимальная длина последовательности
|
||||||
|
"""
|
||||||
|
self.examples = []
|
||||||
|
self.tokenizer = tokenizer
|
||||||
|
self.block_size = block_size
|
||||||
|
|
||||||
|
for text in texts:
|
||||||
|
# Кодируем текст в токены
|
||||||
|
input_ids = tokenizer.encode(text, add_special_tokens=False)
|
||||||
|
|
||||||
|
# Обрезаем или дополняем до нужной длины
|
||||||
|
if len(input_ids) > block_size:
|
||||||
|
input_ids = input_ids[:block_size]
|
||||||
|
else:
|
||||||
|
# Дополняем pad_token_id
|
||||||
|
pad_token_id = getattr(tokenizer, 'pad_token_id', 0)
|
||||||
|
input_ids = input_ids + [pad_token_id] * (block_size - len(input_ids))
|
||||||
|
|
||||||
|
self.examples.append(input_ids)
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
return len(self.examples)
|
||||||
|
|
||||||
|
def __getitem__(self, idx):
|
||||||
|
input_ids = torch.tensor(self.examples[idx], dtype=torch.long)
|
||||||
|
labels = input_ids.clone()
|
||||||
|
return {"input_ids": input_ids, "labels": labels}
|
||||||
|
|
||||||
|
|
||||||
|
class StreamingTextDataset(Dataset):
|
||||||
|
"""
|
||||||
|
Датасет для потоковой обработки больших текстов.
|
||||||
|
Токенизация происходит на лету, что экономит память.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, texts: List[str], tokenizer: Any, block_size: int = 128):
|
||||||
|
self.texts = texts
|
||||||
|
self.tokenizer = tokenizer
|
||||||
|
self.block_size = block_size
|
||||||
|
|
||||||
|
# Получаем pad_token_id из токенизатора
|
||||||
|
self.pad_token_id = getattr(tokenizer, 'pad_token_id', 0)
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
return len(self.texts)
|
||||||
|
|
||||||
|
def __getitem__(self, idx):
|
||||||
|
text = self.texts[idx]
|
||||||
|
|
||||||
|
# Токенизация на лету
|
||||||
|
input_ids = self.tokenizer.encode(text, add_special_tokens=False)
|
||||||
|
|
||||||
|
# Обрезаем или дополняем до нужной длины
|
||||||
|
if len(input_ids) > self.block_size:
|
||||||
|
input_ids = input_ids[:self.block_size]
|
||||||
|
else:
|
||||||
|
input_ids = input_ids + [self.pad_token_id] * (self.block_size - len(input_ids))
|
||||||
|
|
||||||
|
input_ids = torch.tensor(input_ids, dtype=torch.long)
|
||||||
|
labels = input_ids.clone()
|
||||||
|
|
||||||
|
return {"input_ids": input_ids, "labels": labels}
|
||||||
|
|
||||||
|
|
||||||
|
class TextDatasetWithSpecialTokens(TextDataset):
|
||||||
|
"""
|
||||||
|
Расширенная версия TextDataset с поддержкой специальных токенов.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, texts: List[str], tokenizer: Any, block_size: int = 128,
|
||||||
|
add_bos: bool = False, add_eos: bool = False):
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
texts: Список текстов
|
||||||
|
tokenizer: Токенизатор
|
||||||
|
block_size: Максимальная длина
|
||||||
|
add_bos: Добавлять токен начала последовательности
|
||||||
|
add_eos: Добавлять токен конца последовательности
|
||||||
|
"""
|
||||||
|
self.examples = []
|
||||||
|
self.tokenizer = tokenizer
|
||||||
|
self.block_size = block_size
|
||||||
|
self.add_bos = add_bos
|
||||||
|
self.add_eos = add_eos
|
||||||
|
|
||||||
|
for text in texts:
|
||||||
|
# Кодируем с специальными токенами
|
||||||
|
input_ids = tokenizer.encode(
|
||||||
|
text,
|
||||||
|
add_special_tokens=True,
|
||||||
|
add_bos_token=add_bos,
|
||||||
|
add_eos_token=eos
|
||||||
|
)
|
||||||
|
|
||||||
|
# Учитываем специальные токены при обрезке/дополнении
|
||||||
|
effective_block_size = block_size
|
||||||
|
if add_bos:
|
||||||
|
effective_block_size -= 1
|
||||||
|
if add_eos:
|
||||||
|
effective_block_size -= 1
|
||||||
|
|
||||||
|
if len(input_ids) > effective_block_size:
|
||||||
|
input_ids = input_ids[:effective_block_size]
|
||||||
|
|
||||||
|
# Добавляем специальные токены если нужно
|
||||||
|
if add_bos and hasattr(tokenizer, 'bos_token_id') and tokenizer.bos_token_id is not None:
|
||||||
|
input_ids = [tokenizer.bos_token_id] + input_ids
|
||||||
|
if add_eos and hasattr(tokenizer, 'eos_token_id') and tokenizer.eos_token_id is not None:
|
||||||
|
input_ids = input_ids + [tokenizer.eos_token_id]
|
||||||
|
|
||||||
|
# Дополняем до полной длины
|
||||||
|
pad_token_id = getattr(tokenizer, 'pad_token_id', 0)
|
||||||
|
if len(input_ids) < block_size:
|
||||||
|
input_ids = input_ids + [pad_token_id] * (block_size - len(input_ids))
|
||||||
|
|
||||||
|
self.examples.append(input_ids)
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
return len(self.examples)
|
||||||
|
|
||||||
|
def __getitem__(self, idx):
|
||||||
|
input_ids = torch.tensor(self.examples[idx], dtype=torch.long)
|
||||||
|
labels = input_ids.clone()
|
||||||
|
return {"input_ids": input_ids, "labels": labels}
|
||||||
14
llm/src/llm/training/optimizer.py
Normal file
14
llm/src/llm/training/optimizer.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import torch.optim as optim
|
||||||
|
|
||||||
|
def get_optimizer(model, lr=3e-4, weight_decay=0.01, optimizer_type="adamw"):
|
||||||
|
"""
|
||||||
|
Возвращает оптимизатор для обучения модели.
|
||||||
|
"""
|
||||||
|
if optimizer_type.lower() == "adamw":
|
||||||
|
return optim.AdamW(model.parameters(), lr=lr, weight_decay=weight_decay)
|
||||||
|
elif optimizer_type.lower() == "adam":
|
||||||
|
return optim.Adam(model.parameters(), lr=lr, weight_decay=weight_decay)
|
||||||
|
elif optimizer_type.lower() == "sgd":
|
||||||
|
return optim.SGD(model.parameters(), lr=lr, momentum=0.9)
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Неизвестный тип оптимизатора: {optimizer_type}")
|
||||||
13
llm/src/llm/training/scheduler.py
Normal file
13
llm/src/llm/training/scheduler.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
from torch.optim.lr_scheduler import LambdaLR
|
||||||
|
|
||||||
|
def get_linear_schedule_with_warmup(optimizer, num_warmup_steps, num_training_steps):
|
||||||
|
"""
|
||||||
|
Линейный планировщик обучения с warmup.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def lr_lambda(current_step):
|
||||||
|
if current_step < num_warmup_steps:
|
||||||
|
return float(current_step) / float(max(1, num_warmup_steps))
|
||||||
|
return max(0.0, float(num_training_steps - current_step) / float(max(1, num_training_steps - num_warmup_steps)))
|
||||||
|
|
||||||
|
return LambdaLR(optimizer, lr_lambda)
|
||||||
90
llm/src/llm/training/trainer.py
Normal file
90
llm/src/llm/training/trainer.py
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
import torch
|
||||||
|
import torch.nn.functional as F
|
||||||
|
from torch.utils.data import DataLoader
|
||||||
|
from tqdm import tqdm
|
||||||
|
from llm.training.optimizer import get_optimizer
|
||||||
|
from llm.training.scheduler import get_linear_schedule_with_warmup
|
||||||
|
|
||||||
|
class Trainer:
|
||||||
|
"""
|
||||||
|
Универсальный класс обучения LLM (GPT, LLaMA, Mistral и т.д.)
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, model, train_dataset, val_dataset=None, lr=3e-4, batch_size=8, num_epochs=3, warmup_steps=100):
|
||||||
|
self.model = model
|
||||||
|
self.train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
|
||||||
|
self.val_loader = DataLoader(val_dataset, batch_size=batch_size) if val_dataset else None
|
||||||
|
self.optimizer = get_optimizer(model, lr=lr)
|
||||||
|
self.scheduler = None
|
||||||
|
self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
||||||
|
self.model.to(self.device)
|
||||||
|
self.num_epochs = num_epochs
|
||||||
|
self.warmup_steps = warmup_steps
|
||||||
|
|
||||||
|
def compute_lm_loss(self, logits, labels):
|
||||||
|
"""
|
||||||
|
Вычисляет loss для языкового моделирования.
|
||||||
|
Сдвигает логиты и метки для предсказания следующего токена.
|
||||||
|
"""
|
||||||
|
# Сдвигаем логиты и метки для языкового моделирования
|
||||||
|
shift_logits = logits[..., :-1, :].contiguous()
|
||||||
|
shift_labels = labels[..., 1:].contiguous()
|
||||||
|
|
||||||
|
# Вычисляем cross-entropy loss
|
||||||
|
loss = F.cross_entropy(
|
||||||
|
shift_logits.view(-1, shift_logits.size(-1)),
|
||||||
|
shift_labels.view(-1),
|
||||||
|
ignore_index=-100 # Игнорируем padding tokens
|
||||||
|
)
|
||||||
|
return loss
|
||||||
|
|
||||||
|
def train(self):
|
||||||
|
total_steps = len(self.train_loader) * self.num_epochs
|
||||||
|
self.scheduler = get_linear_schedule_with_warmup(self.optimizer, self.warmup_steps, total_steps)
|
||||||
|
|
||||||
|
for epoch in range(self.num_epochs):
|
||||||
|
self.model.train()
|
||||||
|
total_loss = 0
|
||||||
|
|
||||||
|
progress_bar = tqdm(self.train_loader, desc=f"Epoch {epoch+1}/{self.num_epochs}")
|
||||||
|
for batch in progress_bar:
|
||||||
|
self.optimizer.zero_grad()
|
||||||
|
|
||||||
|
input_ids = batch["input_ids"].to(self.device)
|
||||||
|
labels = batch["labels"].to(self.device)
|
||||||
|
|
||||||
|
# Модель возвращает только логиты
|
||||||
|
logits = self.model(input_ids)
|
||||||
|
|
||||||
|
# Trainer вычисляет loss
|
||||||
|
loss = self.compute_lm_loss(logits, labels)
|
||||||
|
loss.backward()
|
||||||
|
|
||||||
|
torch.nn.utils.clip_grad_norm_(self.model.parameters(), 1.0)
|
||||||
|
self.optimizer.step()
|
||||||
|
self.scheduler.step()
|
||||||
|
|
||||||
|
total_loss += loss.item()
|
||||||
|
progress_bar.set_postfix(loss=loss.item())
|
||||||
|
|
||||||
|
avg_loss = total_loss / len(self.train_loader)
|
||||||
|
print(f"Epoch {epoch+1} finished — avg loss: {avg_loss:.4f}")
|
||||||
|
|
||||||
|
if self.val_loader:
|
||||||
|
self.evaluate()
|
||||||
|
|
||||||
|
def evaluate(self):
|
||||||
|
self.model.eval()
|
||||||
|
total_loss = 0
|
||||||
|
|
||||||
|
with torch.no_grad():
|
||||||
|
for batch in self.val_loader:
|
||||||
|
input_ids = batch["input_ids"].to(self.device)
|
||||||
|
labels = batch["labels"].to(self.device)
|
||||||
|
|
||||||
|
logits = self.model(input_ids)
|
||||||
|
loss = self.compute_lm_loss(logits, labels)
|
||||||
|
total_loss += loss.item()
|
||||||
|
|
||||||
|
avg_loss = total_loss / len(self.val_loader)
|
||||||
|
print(f"Validation loss: {avg_loss:.4f}")
|
||||||
0
notebooks/gpt_analysis.ipynb
Normal file
0
notebooks/gpt_analysis.ipynb
Normal file
35
pyproject.toml
Normal file
35
pyproject.toml
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
[project]
|
||||||
|
name = "llm-arch-research"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Research workspace for LLM architectures"
|
||||||
|
authors = [
|
||||||
|
{ name = "Sergey Penkovsky", email = "sergey.penkovsky@gmail.com" }
|
||||||
|
]
|
||||||
|
requires-python = ">=3.10"
|
||||||
|
dependencies = [
|
||||||
|
"accelerate>=0.26.0",
|
||||||
|
"hf-proxy",
|
||||||
|
"llm",
|
||||||
|
"tqdm>=4,<5",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
dev = [
|
||||||
|
"pytest>=8.0.0",
|
||||||
|
"black>=24.0.0",
|
||||||
|
"ruff>=0.3.0",
|
||||||
|
"mypy>=1.8.0",
|
||||||
|
"jupyter>=1.0.0",
|
||||||
|
]
|
||||||
|
test = [
|
||||||
|
"pytest>=8.0.0",
|
||||||
|
"pytest-cov>=4.1.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.uv.sources]
|
||||||
|
llm = { workspace = true, editable = true }
|
||||||
|
hf-proxy = { workspace = true, editable = true }
|
||||||
|
|
||||||
|
[tool.uv.workspace]
|
||||||
|
members = ["llm", "hf-proxy"]
|
||||||
|
exclude = []
|
||||||
Reference in New Issue
Block a user