Files
Sergey Penkovsky cc4138aba8 Рефакторинг и улучшение компонентов
Основные изменения в коде:

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 (токены)
   - Подготовка к генерации новых данных
2025-07-24 16:45:31 +03:00

233 lines
10 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import dill
from tqdm import tqdm
class BPE:
"""Реализация алгоритма Byte Pair Encoding (BPE) для токенизации текста.
BPE - это алгоритм сжатия данных, адаптированный для токенизации текста в NLP.
Работает путем итеративного объединения наиболее частых пар символов/токенов.
Пример использования:
>>> tokenizer = BPE(vocab_size=100)
>>> tokenizer.fit("текст для обучения")
>>> encoded = tokenizer.encode("пример текста")
>>> decoded = tokenizer.decode(encoded)
Args:
vocab_size (int): Максимальный размер словаря токенов
"""
def __init__(self, vocab_size: int):
self.vocab_size = vocab_size
self.id2token = {}
self.token2id = {}
def fit(self, text: str):
"""Обучает токенизатор на заданном тексте.
Процесс обучения:
1. Начинает с базовых символов текста
2. Итеративно находит и объединяет самые частые пары символов
3. Продолжает пока не достигнет заданного размера словаря
Args:
text (str): Текст для обучения токенизатора
Пример:
>>> 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])
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:
break # нет пар — выходим
#for x in pair_freq.items():
# self.debug(x, sequence)
# Находим самую частую пару (в случае равенства — та, что встретилась первой)
most_frequent_pair = max(pair_freq.items(), key=lambda x: (x[1], -self._pair_first_index(sequence, x[0])))[0]
print(f"Самая частая пара: {most_frequent_pair} (встречается {pair_freq[most_frequent_pair]} раз)")
# Создаем новый токен
new_token = most_frequent_pair[0] + most_frequent_pair[1]
print(f"Добавлен новый токен: '{new_token}'")
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
#break
# 4. Создаем словари
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):
if (sequence[i], sequence[i + 1]) == pair:
return i
return float('inf') # если пара не найдена (в теории не должно случиться)
def encode(self, text: str):
"""Кодирует текст в последовательность ID токенов.
Использует жадный алгоритм для поиска наиболее длинных совпадений:
1. Начинает с первого символа
2. Ищет самый длинный токен из словаря, совпадающий с началом текста
3. Добавляет ID найденного токена в результат
4. Сдвигается на длину найденного токена и повторяет
Args:
text (str): Текст для кодирования
Returns:
list: Список ID токенов (неизвестные символы кодируются как -1)
Пример:
>>> encoded = tokenizer.encode("Пример текста")
>>> print(encoded)
[12, 34, 56, 78]
"""
# 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 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
return self._tokens_to_ids(tokens)
def _find_max_matching_token(self, text: str, tokens: list):
"""Находит самый длинный токен из списка, с которого начинается текст"""
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):
"""Конвертирует список токенов в их ID с обработкой неизвестных токенов"""
ids = []
for token in tokens:
if token in self.token2id:
ids.append(self.token2id[token])
else:
ids.append(-1) # Специальное значение
return ids
def decode(self, ids: list) -> str:
"""Декодирует последовательность ID обратно в текст.
Args:
ids (list): Список ID токенов
Returns:
str: Декодированный текст
Пример:
>>> decoded = tokenizer.decode([12, 34, 56, 78])
>>> print(decoded)
"Пример текста"
"""
return ''.join(self._ids_to_tokens(ids))
def _ids_to_tokens(self, ids: list) -> list:
"""Внутренний метод преобразования ID в токены.
Args:
ids (list): Список ID токенов
Returns:
list: Список соответствующих токенов (неизвестные ID = '')
"""
"""Конвертирует список Ids в их tokens"""
tokens = []
for id in ids:
if id in self.id2token:
tokens.append(self.id2token[id])
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