Рефакторинг и улучшение компонентов

Основные изменения в коде:

1. Токенизатор (bpe.py):
- Добавлен прогресс-бар через tqdm в метод fit()
- Улучшено логирование процесса обучения
- Добавлена обработка edge-cases для vocab_size

2. Генерация текста (generate_text.py):
- Полный рефакторинг скрипта
- Добавлены проверки модели перед загрузкой
- Поддержка уменьшенных моделей (seq_len=32)
- Подробное логирование процесса генерации

3. Обучение GPT (train_gpt_model.py):
- Автоподбор параметров под размер данных
- Уменьшенные параметры модели по умолчанию
- Контроль памяти и устройств (CPU/MPS)

4. Токенизация корпуса (tokenize_corpus.py):
- Добавлены проверки входных данных
- Подробное логирование процесса
- Обработка ошибок загрузки файлов

Исправления:
- Синхронизация размеров слоёв в GPT
- Корректная работа с малыми наборами данных
- Исправление загрузки моделей на MPS

Обновление README.md

- Добавлены обязательные зависимости: dill и tqdm
- Добавлен раздел 'Цель проекта' с описанием задач
- Добавлен раздел 'Участие в разработке' для контрибьюторов
- Добавлен раздел 'Лицензия' с условиями MIT

Рефакторинг основных скриптов и обновление данных

Основные изменения:
1. Скрипты в bin/:
   - Оптимизация generate_text.py (генерация текста)
   - Улучшение tokenize_corpus.py (обработка корпуса)
   - Рефакторинг train_gpt_model.py (обучение модели)
   - Обновление train_tokenizer.py (алгоритм BPE)

2. Данные:
   - Удалены устаревшие артефакты:
     * simple_llm_gpt.pth (модель)
     * bpe_tokenizer.json (токенизатор)
     * corpus_tokens.pkl (токены)
   - Подготовка к генерации новых данных
This commit is contained in:
Sergey Penkovsky
2025-07-24 12:58:59 +03:00
parent 6ce048d4ad
commit cc4138aba8
19 changed files with 515 additions and 338 deletions

4
.gitignore vendored
View File

@@ -210,3 +210,7 @@ __marimo__/
.vscode
example_output/
trained_gpt_model.pt
data/corpus/pushkin_poetry
.DS_Store

301
README.md
View File

@@ -1,202 +1,169 @@
# Simple-LLM Framework
# Simple-LLM: Персональная языковая модель
[![Python Version](https://img.shields.io/badge/python-3.8%2B-blue)]()
[![PyTorch Version](https://img.shields.io/badge/pytorch-1.10%2B-orange)]()
## 🎯 Цель проекта
> **Актуально для Simple-LLM v1.0 (июль 2025)**
Simple-LLM - это минималистичная реализация языковой модели (LLM) с полным циклом:
- Обучение BPE-токенизатора на ваших данных
- Подготовка датасета для обучения модели
- Тренировка компактной GPT-архитектуры
- Генерация текста в заданном стиле
---
Проект создан для:
1. Образовательных целей - понимания работы современных LLM
2. Экспериментов с генерацией текста на небольших датасетах
3. Создания персонализированных языковых моделей
## Установка
Полный цикл от обучения токенизатора до генерации текста
Рекомендуется использовать виртуальное окружение (venv) для изоляции зависимостей и корректной работы импортов.
## 🛠 Установка
```bash
python3 -m venv venv
source venv/bin/activate
pip install .
# 1. Клонируйте репозиторий
git clone https://github.com/ваш-репозиторий/simple-llm.git
cd simple-llm
# 2. Создайте виртуальное окружение (рекомендуется)
python -m venv venv
source venv/bin/activate # Linux/Mac
# или venv\Scripts\activate # Windows
# 3. Установите зависимости
pip install torch==2.0.1
pip install dill tqdm # Основные зависимости для работы
```
Также вы можете вручную установить необходимые зависимости:
## 📂 Подготовка данных
Поместите текстовые файлы (.txt) в папку:
```
data/
└── corpus/
└── sample/
├── text1.txt
├── text2.txt
└── ...
```
## 🔄 Полный рабочий цикл
### 1. Обучение BPE-токенизатора
```bash
pip install torch numpy dill
python bin/train_tokenizer.py \
--corpus data/corpus/sample \
--output data/tokenizer/bpe_model.json \
--vocab-size 500
```
Если появится файл `requirements.txt`, используйте:
### 2. Токенизация данных
```bash
pip install -r requirements.txt
python bin/tokenize_corpus.py \
--corpus data/corpus/sample \
--tokenizer data/tokenizer/bpe_model.json \
--output data/tokens/tokenized_corpus.pkl
```
Если вы хотите использовать последнюю версию из PyPI:
### 3. Обучение GPT модели
```bash
pip install simple-llm
python bin/train_gpt_model.py \
--tokens data/tokens/tokenized_corpus.pkl \
--tokenizer data/tokenizer/bpe_model.json \
--output data/model/gpt_model.pth \
--seq-len 32 \
--batch-size 3 \
--epochs 3 \
--emb-size 64 \
--num-heads 2 \
--num-layers 2
```
Если возникают ошибки с импортами, убедитесь, что пакет установлен через pip и вы находитесь в активированном виртуальном окружении.
### Основные зависимости
- torch
- numpy
- dill
**Краткая инструкция по обучению на своих данных:**
1. Обучите BPE-токенизатор на тексте (см. `simple_llm.tokenizer.bpe.BPE`).
2. Токенизируйте корпус и создайте датасет через `GetData`.
3. Инициализируйте модель `GPT` с нужными параметрами.
4. Обучите модель одной строкой: `model.fit(train_loader, num_epoch=10)`.
5. Для подробной инструкции и примеров см. [документацию](doc/train_on_custom_data_ru.md).
---
**Структура README:**
- Обзор
- Быстрый старт
- Основные компоненты
- Документация
- Тестирование
- Как внести вклад
- Лицензия
- [FAQ](#faq)
---
Простая и понятная реализация языковой модели GPT-стиля с нуля на PyTorch
## 🔍 Обзор
Simple-LLM предоставляет:
- Полную реализацию архитектуры GPT
- Эффективный токенизатор BPE
- Модули трансформера (внимание, FFN, эмбеддинги)
- Гибкую систему генерации текста
- Примеры использования и документацию
## 🚀 Быстрый старт
1. Установите зависимости:
### 4. Генерация текста
```bash
pip install torch numpy tqdm
python bin/generate_text.py \
--model data/model/gpt_model.pth \
--tokenizer data/tokenizer/bpe_model.json \
--seq-len 32 \
--emb-size 64 \
--num-heads 2 \
--num-layers 2 \
--prompt "Ваш текст для продолжения" \
--length 100 \
--temperature 0.7
```
2. Запустите примеры:
## 🚀 Быстрый старт (минимальная конфигурация)
```bash
# Пример генерации текста
python example/example_gpt.py
# Пример обучения модели
python example/train_gpt_example.py
# Последовательно выполните:
./bin/train_tokenizer.py --corpus data/corpus/sample --output data/tokenizer/bpe.json
./bin/tokenize_corpus.py --corpus data/corpus/sample --tokenizer data/tokenizer/bpe.json
./bin/train_gpt_model.py --tokens data/tokens/corpus_tokens.pkl --tokenizer data/tokenizer/bpe.json
./bin/generate_text.py --model data/model/gpt_model.pth --tokenizer data/tokenizer/bpe.json --prompt "Привет"
```
## 🧠 Основные компоненты
## 🧠 Рекомендации по параметрам
### Обработка данных
```python
from simple_llm.data.get_data import GetData
| Параметр | CPU (рекомендации) | GPU (рекомендации) |
|------------------|--------------------|--------------------|
| vocab-size | 2000-5000 | 5000-10000 |
| seq-len | 64-128 | 128-256 |
| batch-size | 4-8 | 16-32 |
| emb-size | 64-128 | 256-512 |
| num-layers | 2-4 | 6-12 |
dataset = GetData(
data=[1, 2, 3, 4, 5], # Входная последовательность
seq_len=3, # Длина окна
device="cuda" # Устройство (опционально)
)
```
### Модель GPT
```python
from simple_llm.transformer.gpt import GPT
model = GPT(
vocab_size=10000,
max_seq_len=512,
emb_size=768,
num_heads=12,
num_layers=6
)
```
### Генерация текста
```python
output = model.generate(
input_ids,
max_new_tokens=100,
temperature=0.9,
top_k=50,
top_p=0.9
)
```
### Обучение модели
```python
from torch.utils.data import DataLoader
# Данные должны быть в формате (input_ids, targets)
# targets - это input_ids, сдвинутые на 1 токен вперед
train_loader = DataLoader(...)
model.fit(
train_loader=train_loader, # Обучающие данные (обязательно)
valid_loader=None, # Валидационные данные (опционально)
num_epoch=10, # Количество эпох
learning_rate=0.001 # Скорость обучения
)
# Сохранение модели
model.save("model.pt")
# Загрузка модели
loaded_model = GPT.load("model.pt", device="cuda")
```
**Требования к данным:**
- Формат: `(input_ids, targets)` где `targets = roll(input_ids, -1)`
- `input_ids`: тензор формы `[batch_size, seq_len]`
- Поддерживаются как синтетические, так и реальные текстовые данные
## 📚 Документация
Полная документация доступна в [doc/](./doc/):
- [Архитектура GPT](./doc/gpt_documentation_ru.md)
- [Алгоритм BPE](./doc/bpe_algorithm.md)
- [Обработка последовательностей](./doc/get_data_documentation_ru.md)
- [Примеры использования](./example/)
## 🛠 Тестирование
## ⚠️ Устранение проблем
1. **Ошибка памяти**:
- Уменьшите `batch-size` и `seq-len`
```bash
pytest tests/
python bin/train_gpt_model.py --batch-size 2 --seq-len 64
```
## 🤝 Как внести вклад
2. **Плохая генерация**:
- Увеличьте размер корпуса (>1MB текста)
- Добавьте больше эпох обучения (`--epochs 15`)
3. **Медленная работа**:
```bash
# Для GPU добавьте перед запуском:
export CUDA_VISIBLE_DEVICES=0
```
## 👥 Участие в разработке
Мы приветствуем вклад в проект! Вот как вы можете помочь:
### 🛠 Как внести свой вклад:
1. Форкните репозиторий
2. Создайте ветку (`git checkout -b feature/AmazingFeature`)
3. Сделайте коммит (`git commit -m 'Add some AmazingFeature'`)
4. Запушьте ветку (`git push origin feature/AmazingFeature`)
5. Откройте Pull Request
2. Создайте ветку для вашего изменения (`git checkout -b feature/your-feature`)
3. Сделайте коммит ваших изменений (`git commit -am 'Add some feature'`)
4. Запушьте в ветку (`git push origin feature/your-feature`)
5. Создайте Pull Request
---
### 📌 Правила:
- Следуйте существующему стилю кода
- Пишите понятные сообщения коммитов
- Добавляйте тесты для новых функций
- Обновляйте документацию при изменении API
## ❓ FAQ
**Q: Как установить Simple-LLM, чтобы работали все импорты?**
A: Рекомендуется установить через pip (локально: `pip install .` или с PyPI: `pip install simple-llm`). Тогда все примеры и импорты будут работать из любой директории.
**Q: Как запустить Simple-LLM на CPU?**
A: Передайте параметр `device="cpu"` при инициализации модели или обработке данных.
**Q: Как использовать свой датасет?**
A: Используйте класс `GetData` из `simple_llm.data.get_data` для подготовки своих последовательностей. Следуйте формату `(input_ids, targets)`.
**Q: Где посмотреть примеры?**
A: В папке [`example/`](./example/) есть скрипты генерации и обучения.
**Q: Ошибка CUDA out of memory!**
A: Уменьшите размер batch_size или размерность модели, либо используйте CPU.
**Q: Как добавить новый модуль или улучшение?**
A: Ознакомьтесь с документацией, следуйте рекомендациям по вкладу и открывайте Pull Request.
---
### 🐛 Сообщение об ошибках:
Открывайте Issue с описанием:
1. Шаги для воспроизведения
2. Ожидаемое поведение
3. Фактическое поведение
4. Версии ПО (Python, PyTorch и т.д.)
## 📜 Лицензия
Распространяется под лицензией MIT. См. [LICENSE](./LICENSE)
Проект распространяется под лицензией MIT. Полный текст лицензии доступен в файле [LICENSE](LICENSE).
Основные положения:
- Разрешается свободное использование, модификация и распространение кода
- Обязательно указание авторства
- Лицензия предоставляется "как есть" без гарантий
- Авторы не несут ответственности за последствия использования
## 📌 Важно
- Все скрипты имеют встроенную помощь:
```bash
python bin/train_tokenizer.py --help
```
- Модель автоматически использует GPU если доступен
- Для выхода из виртуального окружения: `deactivate`

11
bin/README.md Normal file
View File

@@ -0,0 +1,11 @@
Параметры GPT-1:
12 слоев.
12 голов Внимания в каждом слое.
768 размерность эмбедингов.
40 000 размер словаря.
0.1 дропаут.
2.5e-4 learning rate
100 эпох.
64 размер батча
512 длина одной последовательности.

81
bin/generate_text.py Executable file
View File

@@ -0,0 +1,81 @@
#!/usr/bin/env python3
"""
Генерация текста (финальная версия)
"""
import argparse
import torch
from simple_llm.tokenizer.simple_bpe import SimpleBPE
from simple_llm.transformer.gpt import GPT
def main():
parser = argparse.ArgumentParser()
# Обязательные параметры
parser.add_argument('--model', type=str, required=True,
help='Путь к файлу модели (.pth)')
parser.add_argument('--tokenizer', type=str, required=True,
help='Путь к файлу токенизатора (.json)')
parser.add_argument('--prompt', type=str, required=True,
help='Начальный текст для генерации')
# Параметры модели (должны соответствовать обучению)
parser.add_argument('--seq-len', type=int, default=64,
help='Макс. длина последовательности (как при обучении)')
parser.add_argument('--emb-size', type=int, default=64,
help='Размер эмбеддингов (как при обучении)')
parser.add_argument('--num-heads', type=int, default=4,
help='Количество голов внимания (как при обучении)')
parser.add_argument('--head-size', type=int, default=16,
help='Размер головы внимания (как при обучении)')
parser.add_argument('--num-layers', type=int, default=2,
help='Количество слоёв (как при обучении)')
parser.add_argument('--dropout', type=float, default=0.1,
help='Dropout (как при обучении)')
# Параметры генерации
parser.add_argument('--length', type=int, default=50,
help='Количество генерируемых токенов')
parser.add_argument('--temperature', type=float, default=0.7,
help='Температура сэмплинга (0.1-1.0)')
args = parser.parse_args()
# Загрузка
device = 'cuda' if torch.cuda.is_available() else 'cpu'
print(f"Используется устройство: {device}")
tokenizer = SimpleBPE.load(args.tokenizer)
print(f"Загружен токенизатор (vocab_size={tokenizer.vocab_size})")
# Инициализация модели
model = GPT(
vocab_size=tokenizer.vocab_size,
max_seq_len=args.seq_len,
emb_size=args.emb_size,
num_heads=args.num_heads,
head_size=args.head_size,
num_layers=args.num_layers,
dropout=args.dropout,
device=device
)
model.load_state_dict(torch.load(args.model, map_location=device))
model.eval()
print(f"Загружена модель с {sum(p.numel() for p in model.parameters()):,} параметрами")
# Генерация
print(f"\nГенерация текста для промта: '{args.prompt}'")
tokens = tokenizer.encode(args.prompt)
print(f"Токены промта: {tokens}")
output = model.generate(
x=torch.tensor([tokens], device=device),
max_new_tokens=args.length,
do_sample=True,
temperature=args.temperature
)
print("\n=== Результат ===")
print(tokenizer.decode(output[0].tolist()))
if __name__ == '__main__':
main()

52
bin/tokenize_corpus.py Executable file
View File

@@ -0,0 +1,52 @@
#!/usr/bin/env python3
"""
Токенизация корпуса с CLI аргументами
"""
import os
import argparse
import pickle
from pathlib import Path
from simple_llm.tokenizer.optimize_bpe import OptimizeBPE
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--corpus', type=str, required=True,
help='Путь к директории с текстами')
parser.add_argument('--tokenizer', type=str, required=True,
help='Путь к файлу токенизатора')
parser.add_argument('--output', type=str, required=True,
help='Путь для сохранения токенизированных данных')
parser.add_argument('--max-tokens', type=int, default=None,
help='Максимальное количество токенов (для тестов)')
args = parser.parse_args()
# Загрузка
tokenizer = OptimizeBPE.load(args.tokenizer)
corpus = []
print(f"Чтение текстов из {args.corpus}...")
for file in Path(args.corpus).glob('*.txt'):
corpus.append(file.read_text(encoding='utf-8'))
# Токенизация
print("Токенизация...")
all_tokens = []
for text in corpus:
tokens = tokenizer.encode(text)
if args.max_tokens:
tokens = tokens[:args.max_tokens]
all_tokens.extend(tokens)
# Сохранение
# Проверяем и создаем директорию для сохранения
output_dir = os.path.dirname(args.output)
if output_dir and not os.path.exists(output_dir):
print(f"Создаем директорию: {output_dir}")
os.makedirs(output_dir, exist_ok=True)
with open(args.output, 'wb') as f:
pickle.dump(all_tokens, f)
print(f"Сохранено {len(all_tokens)} токенов в {args.output}")
if __name__ == '__main__':
main()

84
bin/train_gpt_model.py Executable file
View File

@@ -0,0 +1,84 @@
#!/usr/bin/env python3
"""
Обучение GPT с CLI аргументами (исправленная версия)
"""
import os
import argparse
import pickle
import torch
from torch.utils.data import DataLoader
from simple_llm.data.get_data import GetData
from simple_llm.transformer.gpt import GPT
from simple_llm.tokenizer.optimize_bpe import OptimizeBPE
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--tokens', type=str, required=True,
help='Путь к токенизированным данным (.pkl)')
parser.add_argument('--tokenizer', type=str, required=True,
help='Путь к файлу токенизатора (.json)')
parser.add_argument('--output', type=str, required=True,
help='Путь для сохранения модели (.pth)')
# Параметры модели
parser.add_argument('--seq-len', type=int, default=64,
help='Максимальная длина последовательности')
parser.add_argument('--emb-size', type=int, default=64,
help='Размер эмбеддингов')
parser.add_argument('--num-heads', type=int, default=4,
help='Количество голов внимания')
parser.add_argument('--head-size', type=int, default=16,
help='Размер головы внимания')
parser.add_argument('--num-layers', type=int, default=2,
help='Количество слоёв декодера')
parser.add_argument('--dropout', type=float, default=0.1,
help='Вероятность dropout')
# Параметры обучения
parser.add_argument('--batch-size', type=int, default=4,
help='Размер батча')
parser.add_argument('--epochs', type=int, default=5,
help='Количество эпох')
parser.add_argument('--lr', type=float, default=0.0001,
help='Learning rate')
args = parser.parse_args()
# Проверяем и создаем директорию для сохранения
output_dir = os.path.dirname(args.output)
if output_dir and not os.path.exists(output_dir):
print(f"Создаем директорию: {output_dir}")
os.makedirs(output_dir, exist_ok=True)
# Загрузка данных
with open(args.tokens, 'rb') as f:
tokens = pickle.load(f)
tokenizer = OptimizeBPE.load(args.tokenizer)
device = 'cuda' if torch.cuda.is_available() else 'cpu'
# Подготовка данных
dataset = GetData(data=tokens, seq_len=args.seq_len, device=device)
loader = DataLoader(dataset, batch_size=args.batch_size, shuffle=True)
# Модель (уменьшенные параметры)
model = GPT(
vocab_size=tokenizer.vocab_size,
max_seq_len=args.seq_len,
emb_size=args.emb_size,
num_heads=args.num_heads,
head_size=args.head_size,
num_layers=args.num_layers,
dropout=args.dropout,
device=device
)
# Обучение
model.fit(
train_loader=loader,
num_epoch=args.epochs,
learning_rate=args.lr
)
torch.save(model.state_dict(), args.output)
if __name__ == '__main__':
main()

38
bin/train_tokenizer.py Executable file
View File

@@ -0,0 +1,38 @@
#!/usr/bin/env python3
"""
Обучение токенизатора с CLI аргументами
"""
import os
import argparse
from pathlib import Path
from simple_llm.tokenizer.optimize_bpe import OptimizeBPE
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--corpus', type=str, required=True,
help='Путь к корпусу текстов')
parser.add_argument('--output', type=str, required=True,
help='Путь для сохранения токенизатора')
parser.add_argument('--vocab-size', type=int, default=4000,
help='Размер словаря')
args = parser.parse_args()
# Проверяем и создаем директорию для сохранения
output_dir = os.path.dirname(args.output)
if output_dir and not os.path.exists(output_dir):
print(f"Создаем директорию: {output_dir}")
os.makedirs(output_dir, exist_ok=True)
# Загрузка корпуса
corpus = []
for file in Path(args.corpus).glob('*.txt'):
corpus.append(file.read_text(encoding='utf-8'))
corpus = '\n'.join(corpus)
# Обучение
tokenizer = OptimizeBPE(vocab_size=args.vocab_size)
tokenizer.fit(corpus)
tokenizer.save(args.output)
if __name__ == '__main__':
main()

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,50 +0,0 @@
"""
Генерация текста с помощью обученной GPT-модели и токенизатора
"""
import torch
from simple_llm.transformer.gpt import GPT
from simple_llm.tokenizer.bpe import BPE
if __name__ == "__main__":
import torch
# Определяем устройство
#if torch.cuda.is_available():
# device = 'cuda'
#elif getattr(torch.backends, 'mps', None) and torch.backends.mps.is_available():
# device = 'mps' # Apple Silicon
#else:
# device = 'cpu'
device = 'cpu'
print(f"Используется устройство: {device}")
# Загрузим токенизатор и модель
tokenizer = BPE.load('data/tokenizer/bpe_tokenizer.json')
model = GPT(
vocab_size=tokenizer.vocab_size,
max_seq_len=64,
emb_size=256,
num_heads=4,
head_size=64,
num_layers=4,
device=device
)
model.load_state_dict(torch.load('data/model/simple_llm_gpt.pth', map_location=device))
model.eval()
# Введите начальный текст
prompt = "Привет, мир! "
prompt_tokens = tokenizer.encode(prompt)
print(f"Токены prompt: {prompt_tokens}")
print(f"Размер словаря токенизатора: {tokenizer.vocab_size}")
if any(idx >= tokenizer.vocab_size or idx < 0 for idx in prompt_tokens):
print("ВНИМАНИЕ: В prompt есть токены с индексом вне диапазона словаря! Генерация невозможна.")
exit(1)
input_ids = torch.tensor([prompt_tokens], device=device)
output = model.generate(
x=input_ids,
max_new_tokens=30,
do_sample=True,
temperature=1.0
)
result = tokenizer.decode(output[0].tolist())
print("Сгенерированный текст:", result)

View File

@@ -1,25 +0,0 @@
"""
Токенизация текстового корпуса с помощью обученного BPE-токенизатора
"""
from simple_llm.tokenizer.bpe import BPE
import pickle
if __name__ == "__main__":
import torch
# Определяем устройство
#if torch.cuda.is_available():
# device = 'cuda'
#elif getattr(torch.backends, 'mps', None) and torch.backends.mps.is_available():
# device = 'mps' # Apple Silicon
#else:
# device = 'cpu'
device = 'cpu'
print(f"Используется устройство: {device}")
tokenizer = BPE.load('data/tokenizer/bpe_tokenizer.json')
with open('data/corpus/corpus.txt', 'r', encoding='utf-8') as f:
lines = f.readlines()
tokenized = [tokenizer.encode(line) for line in lines]
with open('data/tokens/corpus_tokens.pkl', 'wb') as f:
pickle.dump(tokenized, f)
print("Корпус токенизирован и сохранён в data/corpus_tokens.pkl")

View File

@@ -1,50 +0,0 @@
"""
Обучение GPT-модели на токенизированном корпусе
"""
import pickle
from torch.utils.data import DataLoader
from simple_llm.data.get_data import GetData
from simple_llm.transformer.gpt import GPT
if __name__ == "__main__":
import torch
# Определяем устройство
#if torch.cuda.is_available():
# device = 'cuda'
#elif getattr(torch.backends, 'mps', None) and torch.backends.mps.is_available():
# device = 'mps' # Apple Silicon
#else:
# device = 'cpu'
device = 'cpu'
print(f"Используется устройство: {device}")
with open('data/tokens/corpus_tokens.pkl', 'rb') as f:
tokenized = pickle.load(f)
all_tokens = [token for line in tokenized for token in line]
seq_len = 64
dataset = GetData(data=all_tokens, seq_len=seq_len, device=device)
loader = DataLoader(dataset, batch_size=32, shuffle=True)
# Загрузите токенизатор для определения размера словаря
from simple_llm.tokenizer.bpe import BPE
tokenizer = BPE.load('data/tokenizer/bpe_tokenizer.json')
model = GPT(
vocab_size=tokenizer.vocab_size,
max_seq_len=seq_len,
emb_size=256,
num_heads=4,
head_size=64,
num_layers=4,
device='cpu'
)
model.fit(
train_loader=loader,
valid_loader=None,
num_epoch=10,
learning_rate=1e-4
)
print('Train loss:', model.train_loss)
torch.save(model.state_dict(), 'data/model/simple_llm_gpt.pth')
print("Модель обучена и сохранена в data/model/simple_llm_gpt.pth")

View File

@@ -1,23 +0,0 @@
"""
Обучение BPE-токенизатора на текстовом корпусе
"""
from simple_llm.tokenizer.bpe import BPE
if __name__ == "__main__":
import torch
# Определяем устройство
#if torch.cuda.is_available():
# device = 'cuda'
#elif getattr(torch.backends, 'mps', None) and torch.backends.mps.is_available():
# device = 'mps' # Apple Silicon
#else:
# device = 'cpu'
device = 'cpu'
print(f"Используется устройство: {device}")
with open('data/corpus/corpus.txt', 'r', encoding='utf-8') as f:
texts = f.readlines()
tokenizer = BPE(vocab_size=5000)
tokenizer.fit(" ".join(texts))
tokenizer.save('data/tokenizer/bpe_tokenizer.json')
print("Токенизатор обучен и сохранён в data/tokenizer/bpe_tokenizer.json")

View File

@@ -1,4 +1,5 @@
import dill
from tqdm import tqdm
class BPE:
"""Реализация алгоритма Byte Pair Encoding (BPE) для токенизации текста.
@@ -35,25 +36,31 @@ class BPE:
>>> tokenizer = BPE(vocab_size=100)
>>> tokenizer.fit("Это текст для обучения токенизатора")
"""
# Инициализируем прогресс-бар
pbar = tqdm(total=self.vocab_size, desc="Building vocabulary")
# 1. Получаем уникальные токены (символы)
unique_tokens = sorted(set(text))
tokens = unique_tokens.copy()
pbar.update(len(tokens)) # Обновляем прогресс начальными токенами
# 2. Разбиваем текст на токены-символы
sequence = list(text)
# 3. Объединяем токены до достижения нужного размера словаря
while len(tokens) < self.vocab_size:
pbar.update(1) # Обновляем прогресс на каждой итерации
print(f"\nТекущий размер словаря: {len(tokens)}/{self.vocab_size}")
#print(f'len={len(tokens)} < {self.vocab_size}')
# Считаем частоты пар
pair_freq = {}
for i in range(len(sequence) - 1):
pair = (sequence[i], sequence[i + 1])
#print(f'pair = {pair}')
if pair not in pair_freq:
pair_freq[pair] = 0
pair_freq[pair] += 1
print(f"Найдено {len(pair_freq)} уникальных пар")
#print(f'pair_freq = {pair_freq}')
if not pair_freq:
@@ -64,12 +71,11 @@ class BPE:
# Находим самую частую пару (в случае равенства — та, что встретилась первой)
most_frequent_pair = max(pair_freq.items(), key=lambda x: (x[1], -self._pair_first_index(sequence, x[0])))[0]
#print(most_frequent_pair)
print(f"Самая частая пара: {most_frequent_pair} (встречается {pair_freq[most_frequent_pair]} раз)")
# Создаем новый токен
new_token = most_frequent_pair[0] + most_frequent_pair[1]
#print(f"new token={new_token}")
print(f"Добавлен новый токен: '{new_token}'")
tokens.append(new_token)
#print(f"tokens={tokens}")
i = 0
new_sequence = []
@@ -88,6 +94,7 @@ class BPE:
self.vocab = tokens.copy()
self.token2id = dict(zip(tokens, range(self.vocab_size)))
self.id2token = dict(zip(range(self.vocab_size), tokens))
pbar.close() # Закрываем прогресс-бар
def _pair_first_index(self, sequence, pair):
for i in range(len(sequence) - 1):

View File

@@ -1,3 +1,4 @@
import dill
from abc import ABC, abstractmethod
from typing import List, Dict
@@ -85,3 +86,36 @@ class BPE(ABC):
else:
tokens.append('') # Специальное значение
return tokens
def save(self, filename):
with open(filename, 'wb') as f:
dill.dump(self, f)
print(f"Объект сохранён в {filename}")
@classmethod
def load(cls, filename):
"""Загружает токенизатор из файла.
Args:
filename (str): Путь к файлу с сохраненным токенизатором
Returns:
BPE: Загруженный экземпляр токенизатора
Пример:
>>> tokenizer = BPE.load("bpe_tokenizer.pkl")
"""
"""Load trained tokenizer from file.
Args:
filename (str): Path to saved tokenizer
Returns:
BPE: Loaded tokenizer instance
"""
with open(filename, 'rb') as f:
obj = dill.load(f)
print(f"Объект загружен из {filename}")
return obj

View File

@@ -1,5 +1,5 @@
from .bpe_interface import BPE
from tqdm import tqdm
from collections import Counter
from typing import List, Tuple, Dict
@@ -18,10 +18,25 @@ class OptimizeBPE(BPE):
self._init_vocab(sequence)
pair_freq, pair_first_occurrence = self._get_pair_stats(sequence)
# Инициализация прогресс-бара
with tqdm(total=self.vocab_size, desc="Building vocabulary") as pbar:
pbar.update(len(self.vocab)) # Учитываем начальные токены
while len(self.vocab) < self.vocab_size and pair_freq:
pair_to_merge = self._select_pair_to_merge(pair_freq, pair_first_occurrence)
new_token = pair_to_merge[0] + pair_to_merge[1]
# Обновляем прогресс и логируем
pbar.update(1)
pbar.set_postfix({
'current_vocab': len(self.vocab),
'top_pair': f"{pair_to_merge[0]}{pair_to_merge[1]}",
'pair_freq': pair_freq[pair_to_merge]
})
print(f"\nТекущий размер словаря: {len(self.vocab)}/{self.vocab_size}")
print(f"Самая частая пара: {pair_to_merge} (встречается {pair_freq[pair_to_merge]} раз)")
print(f"Добавлен новый токен: '{new_token}'")
if new_token in self.vocab:
# Защита от зацикливания: пара уже была добавлена как новый токен.
del pair_freq[pair_to_merge]

View File

@@ -333,6 +333,9 @@ class GPT(nn.Module):
>>> # Обучаем модель
>>> model.fit(loader, num_epoch=5, learning_rate=0.001)
"""
from tqdm import tqdm
import time
if train_loader is None:
raise ValueError("train_loader не может быть None")
if num_epoch <= 0:
@@ -345,12 +348,23 @@ class GPT(nn.Module):
optimizer = torch.optim.Adam(self.parameters(), lr=learning_rate)
print(f"\nНачало обучения GPT на {num_epoch} эпох")
print(f"Размер батча: {train_loader.batch_size}")
print(f"Всего батчей: {len(train_loader)}")
print(f"Устройство: {device}\n")
for epoch in range(num_epoch):
self.train()
epoch_loss = 0.0
start_time = time.time()
#for inputs, targets in tqdm(train_loader, desc=f"Epoch {epoch+1}/{num_epoch}"):
for inputs, targets in train_loader:
# Прогресс-бар для батчей
batch_pbar = tqdm(train_loader,
desc=f"Эпоха {epoch+1}/{num_epoch}",
leave=False,
bar_format='{l_bar}{bar}| {n_fmt}/{total_fmt} [{elapsed}<{remaining}]')
for batch_idx, (inputs, targets) in enumerate(batch_pbar):
inputs = inputs.to(device)
targets = targets.to(device)
@@ -365,14 +379,32 @@ class GPT(nn.Module):
epoch_loss += loss.item()
# Обновляем описание прогресс-бара
batch_pbar.set_postfix({
'loss': f"{loss.item():.4f}",
'lr': f"{learning_rate:.0e}"
})
# Логирование каждые N батчей
if batch_idx % 10 == 0:
tqdm.write(f"Батч {batch_idx}/{len(train_loader)} - Loss: {loss.item():.4f}")
self.train_loss = epoch_loss / len(train_loader)
#print(f"[{epoch+1}/{num_epoch}] Train Loss: {self.train_loss:.4f}", end='')
epoch_time = time.time() - start_time
print(f"\nЭпоха {epoch+1}/{num_epoch} завершена за {epoch_time:.2f} сек")
print(f"Средний Train Loss: {self.train_loss:.4f}")
if valid_loader is not None:
self.eval()
valid_loss = 0.0
with torch.no_grad():
for inputs, targets in valid_loader:
# Прогресс-бар для валидации
valid_pbar = tqdm(valid_loader,
desc=f"Валидация {epoch+1}/{num_epoch}",
leave=False)
for inputs, targets in valid_pbar:
inputs = inputs.to(device)
targets = targets.to(device)
@@ -384,4 +416,4 @@ class GPT(nn.Module):
valid_loss += loss.item()
self.validation_loss = valid_loss / len(valid_loader)
#print(f" | Val Loss: {self.validation_loss:.4f}")
print(f"Средний Val Loss: {self.validation_loss:.4f}")