diff --git a/.gitignore b/.gitignore index 7a3aabb..f6f8ec6 100644 --- a/.gitignore +++ b/.gitignore @@ -209,4 +209,8 @@ __marimo__/ .vscode example_output/ -trained_gpt_model.pt \ No newline at end of file +trained_gpt_model.pt + +data/corpus/pushkin_poetry + +.DS_Store \ No newline at end of file diff --git a/README.md b/README.md index 2e7081d..16deae8 100644 --- a/README.md +++ b/README.md @@ -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" # Устройство (опционально) -) -``` +## ⚠️ Устранение проблем +1. **Ошибка памяти**: + - Уменьшите `batch-size` и `seq-len` + ```bash + python bin/train_gpt_model.py --batch-size 2 --seq-len 64 + ``` -### Модель GPT -```python -from simple_llm.transformer.gpt import GPT +2. **Плохая генерация**: + - Увеличьте размер корпуса (>1MB текста) + - Добавьте больше эпох обучения (`--epochs 15`) -model = GPT( - vocab_size=10000, - max_seq_len=512, - emb_size=768, - num_heads=12, - num_layers=6 -) -``` +3. **Медленная работа**: + ```bash + # Для GPU добавьте перед запуском: + export CUDA_VISIBLE_DEVICES=0 + ``` -### Генерация текста -```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/) - -## 🛠 Тестирование -```bash -pytest tests/ -``` - -## 🤝 Как внести вклад +### 🛠 Как внести свой вклад: 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` diff --git a/bin/README.md b/bin/README.md new file mode 100644 index 0000000..fc2f12e --- /dev/null +++ b/bin/README.md @@ -0,0 +1,11 @@ +Параметры GPT-1: + +12 слоев. +12 голов Внимания в каждом слое. +768 – размерность эмбедингов. +40 000 – размер словаря. +0.1 – дропаут. +2.5e-4 – learning rate +100 эпох. +64 – размер батча +512 – длина одной последовательности. \ No newline at end of file diff --git a/bin/generate_text.py b/bin/generate_text.py new file mode 100755 index 0000000..0cca504 --- /dev/null +++ b/bin/generate_text.py @@ -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() diff --git a/bin/tokenize_corpus.py b/bin/tokenize_corpus.py new file mode 100755 index 0000000..d41824d --- /dev/null +++ b/bin/tokenize_corpus.py @@ -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() diff --git a/bin/train_gpt_model.py b/bin/train_gpt_model.py new file mode 100755 index 0000000..9e292a5 --- /dev/null +++ b/bin/train_gpt_model.py @@ -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() diff --git a/bin/train_tokenizer.py b/bin/train_tokenizer.py new file mode 100755 index 0000000..b552e96 --- /dev/null +++ b/bin/train_tokenizer.py @@ -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() diff --git a/data/corpus/corpus.txt b/data/corpus/sample/corpus.txt similarity index 100% rename from data/corpus/corpus.txt rename to data/corpus/sample/corpus.txt diff --git a/data/model/simple_llm_gpt.pth b/data/model/simple_llm_gpt.pth deleted file mode 100644 index 8e909a7..0000000 Binary files a/data/model/simple_llm_gpt.pth and /dev/null differ diff --git a/data/tokenizer/bpe_tokenizer.json b/data/tokenizer/bpe_tokenizer.json deleted file mode 100644 index e5884c8..0000000 Binary files a/data/tokenizer/bpe_tokenizer.json and /dev/null differ diff --git a/data/tokens/corpus_tokens.pkl b/data/tokens/corpus_tokens.pkl deleted file mode 100644 index 5eef224..0000000 Binary files a/data/tokens/corpus_tokens.pkl and /dev/null differ diff --git a/example/generate_text.py b/example/generate_text.py deleted file mode 100644 index 224edc3..0000000 --- a/example/generate_text.py +++ /dev/null @@ -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) diff --git a/example/tokenize_corpus.py b/example/tokenize_corpus.py deleted file mode 100644 index a248578..0000000 --- a/example/tokenize_corpus.py +++ /dev/null @@ -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") diff --git a/example/train_gpt_model.py b/example/train_gpt_model.py deleted file mode 100644 index 8a6fe67..0000000 --- a/example/train_gpt_model.py +++ /dev/null @@ -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") diff --git a/example/train_tokenizer.py b/example/train_tokenizer.py deleted file mode 100644 index 82fab83..0000000 --- a/example/train_tokenizer.py +++ /dev/null @@ -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") diff --git a/simple_llm/tokenizer/bpe.py b/simple_llm/tokenizer/bpe.py index 30b59ac..08b11fc 100644 --- a/simple_llm/tokenizer/bpe.py +++ b/simple_llm/tokenizer/bpe.py @@ -1,4 +1,5 @@ import dill +from tqdm import tqdm class BPE: """Реализация алгоритма Byte Pair Encoding (BPE) для токенизации текста. @@ -35,24 +36,30 @@ 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}') @@ -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): diff --git a/simple_llm/tokenizer/bpe_interface.py b/simple_llm/tokenizer/bpe_interface.py index 8998e66..32f5d6b 100644 --- a/simple_llm/tokenizer/bpe_interface.py +++ b/simple_llm/tokenizer/bpe_interface.py @@ -1,3 +1,4 @@ +import dill from abc import ABC, abstractmethod from typing import List, Dict @@ -84,4 +85,37 @@ class BPE(ABC): tokens.append(self.id2token[id]) else: tokens.append('') # Специальное значение - return tokens \ No newline at end of file + 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 \ No newline at end of file diff --git a/simple_llm/tokenizer/optimize_bpe.py b/simple_llm/tokenizer/optimize_bpe.py index 104eb56..2f4b15f 100644 --- a/simple_llm/tokenizer/optimize_bpe.py +++ b/simple_llm/tokenizer/optimize_bpe.py @@ -1,5 +1,5 @@ from .bpe_interface import BPE - +from tqdm import tqdm from collections import Counter from typing import List, Tuple, Dict @@ -18,19 +18,34 @@ class OptimizeBPE(BPE): self._init_vocab(sequence) pair_freq, pair_first_occurrence = self._get_pair_stats(sequence) - 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] + # Инициализация прогресс-бара + with tqdm(total=self.vocab_size, desc="Building vocabulary") as pbar: + pbar.update(len(self.vocab)) # Учитываем начальные токены - if new_token in self.vocab: - # Защита от зацикливания: пара уже была добавлена как новый токен. - del pair_freq[pair_to_merge] - continue + 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}'") - self.vocab.append(new_token) - sequence, pair_freq, pair_first_occurrence = self._merge_pair( - sequence, pair_to_merge, new_token, pair_freq - ) + if new_token in self.vocab: + # Защита от зацикливания: пара уже была добавлена как новый токен. + del pair_freq[pair_to_merge] + continue + + self.vocab.append(new_token) + sequence, pair_freq, pair_first_occurrence = self._merge_pair( + sequence, pair_to_merge, new_token, pair_freq + ) self._build_token_dicts() diff --git a/simple_llm/transformer/gpt.py b/simple_llm/transformer/gpt.py index 9115834..24b8c6d 100644 --- a/simple_llm/transformer/gpt.py +++ b/simple_llm/transformer/gpt.py @@ -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: @@ -344,13 +347,24 @@ class GPT(nn.Module): self.to(device) 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 - - #for inputs, targets in tqdm(train_loader, desc=f"Epoch {epoch+1}/{num_epoch}"): - for inputs, targets in train_loader: + start_time = time.time() + + # Прогресс-бар для батчей + 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) @@ -364,15 +378,33 @@ class GPT(nn.Module): optimizer.step() 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}") \ No newline at end of file + print(f"Средний Val Loss: {self.validation_loss:.4f}") \ No newline at end of file