Создание вашей первой модели индийского языка: пошаговое руководство

Вас заинтриговала идея создания собственной модели индийского языка, но она кажется вам сложной? Я тоже, пока не углубился в технологию и не осознал ее потенциал и возможности. Давайте вместе разберемся в этой сложности в этом пошаговом руководстве. простой блокнот со всеми ингредиентами и рецептами, необходимыми для создания собственной Большой языковой модели (LLM) с нуля. Нет, я не говорю об использовании предварительно обученной модели или тонкой настройке модели, построении чего-то с нуля. Вы правильно поняли.

Давайте построим модель с субмиллионом параметров, которая сможет изучать и генерировать тамильские имена. Это простейшая форма языковой модели, в которой мы собираемся построить модель для изучения лингвистических правил формирования уникальных тамильских имен. Сделав это, я познакомлю вас с основами создания LLM с нуля. Рецепт остается тем же самым для модели с миллионом параметров или модели с миллиардом параметров. Итак, как только вы закончите эту статью, у вас будет формула для создания собственного LLM, и эту формулу можно воспроизвести для любого языка.

Шаг 1. Создание токенизатора

LLM — это нейронная сеть, которая принимает в качестве входных данных только числа (с плавающей запятой). поэтому нам нужен механизм для преобразования отрывка текста на любом языке в числовое представление. Этот процесс называется токенизацией. При токенизации слова в предложениях разбиваются на подслова и им присваивается уникальное целое число. Это может показаться трудным для понимания, просто подождите, мы добьемся ясности, работая на практике.

Давайте начнем разработку простого символьного токенизатора для тамильского языка. Массив токенов будет выглядеть так

USER_DEFINED_SYMBOLS = ["<pad>", "<s>", "</s>", "<mask>", "."]
symbols = USER_DEFINED_SYMBOLS

vowels = [
    "அ", "ஆ", "இ", "ஈ", "உ", "ஊ", "எ", "ஏ", "ஐ", "ஒ", "ஓ", "ஔ", "ஃ"
]
consonants = [
    "க", "ங", "ச", "ஞ", "ட", "ண", "த", "ந", "ப", "ம", "ய", "ர", "ற",
    "ன", "ல", "ள", "ழ", "வ", "ஷ", "ஸ", "ஹ", "க்ஷ", "ஜ", "ஶ", "ஸ்ரீ"
]
dependent_vowels = [
    "ா", "ி", "ீ", "ு", "ூ", "ெ", "ே", "ை", "ொ", "ோ", "ௌ", "்"
]
# merge all letters into a single array
symbols.extend(vowels + consonants + dependent_vowels)

Пока игнорируйте специальные токены, такие как ‹Pad› ‹s› ‹/s› и другие. Сосредоточьтесь на массиве «символы», который содержит гласные, согласные и зависимые гласные. С помощью этого набора токенов из одного символа можно охватить весь спектр контента на тамильском языке.

Затем мы используем «SentencePieceTrainer» с нашим массивом символов и файлом полного набора данных для построения модели токенизатора. SentencePieceTrainer прочитает полное содержимое набора данных и добавит недостающие токены в токенизатор.

def train_tokenizer(language, input_path, model_prefix, vocab_size):
    spm.SentencePieceTrainer.train(
        input=input_path,
        model_prefix=model_prefix,
        vocab_size=vocab_size,
        user_defined_symbols=language,
        model_type="BPE"
    )

train_tokenizer(symbols, './data/baby_names.txt', 'tokenizer', 58)

Как только SentencePieceTrainer сгенерирует tokenizer.model, мы готовы преобразовать языковые элементы в токены. Вот как мы это делаем

# construct the tokenizer from tokenizer.model
tokenizer = LlamaTokenizer.from_pretrained(out_folder_path)
tokenizer.pad_token = tokenizer.eos_token

sample_sentence = "தமிழ் வாழ்க"
# directly invoke tokenizer, alternatively you can call tokenizer.encode
tokens = tokenizer(sample_sentence, truncation=True,
                padding='max_length', max_length=16)
print(f"Original Sentence: {sample_sentence}\nTokenized Sentence: {tokens}")

Оператор печати напечатает что-то вроде ниже

Original Sentence: தமிழ் வாழ்க
Tokenized Sentence: {
'input_ids': [1, 56, 25, 28, 45, 35, 55, 56, 36, 44, 35, 55, 19, 2, 2, 2], 
'attention_mask': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0]}

Корреляция: த=25, ம=28, ி=45 и так далее и так далее. токен 56 — это пробел. Обратный процесс называется декодированием токенов i-e в настоящие буквы. В строке ниже выводится «‹s›‹/s›‹pad›‹mask›.அஆஇஈஉ»

tokenizer.decode([1,2,3,4,5,6,7,8,9,10])

Массив «attention_mask» — это вспомогательная структура, сообщающая модели, какие части токенов следует учитывать для обучения и потери. 0 означает игнорировать. 1 означает учитывать.

Выходные данные LLM будут иметь идентификаторы токенов, и нам понадобится tokenizer.decode, чтобы восстановить их обратно в элементы языка.

Шаг 2. Создайте модель LLM

Теперь, когда мы разобрались с токенизатором, нам нужна модель для обучения. Модель построена с использованием простого файла конфигурации, подобного приведенному ниже, и класса Huggingface Transformer с именем LlamaForCausalLM. Да, я просто забыл сказать вам, что на самом деле мы используем модель архитектуры Llama для нашего практического примера.

config_content = {
    "_name_or_path": "./names_1m",
    "architectures": [
        "LlamaForCausalLM"
    ],
    "bos_token_id": 2,
    "eos_token_id": 3,
    "hidden_act": "silu",
    "hidden_size": 64,
    "initializer_range": 0.02,
    "intermediate_size": 180,
    "max_position_embeddings": 32,
    "model_type": "llama",
    "num_attention_heads": 16,
    "num_hidden_layers": 8,
    "num_key_value_heads": 16,
    "pad_token_id": 1,
    "pretraining_tp": 1,
    "rms_norm_eps": 1e-06,
    "rope_scaling": None,
    "tie_word_embeddings": False,
    "torch_dtype": "float32",
    "transformers_version": "4.28.1",
    "use_cache": False,
    "vocab_size": 58
}

Слишком много магических чисел и параметров, верно? Не бойтесь, а сейчас давайте просто сосредоточимся на приведенных ниже. Остальные параметры и магические числа мы можем оставить как есть. Никакого вреда не возникнет.

bos_token_id = идентификатор токена ‹s›

eos_token_id = идентификатор токена ‹/s›

скрытый_размер = количество нейронов в скрытом слое

промежуточный_размер = размер блоков трансформаторов в скрытом слое

max_position_embeddings = размер контекстного окна модели

model_type = архитектура модели

num_attention_heads = сколько токенов можно просматривать одновременно

num_key_value_heads = работает в тандеме с num_attention_heads для параллелизма

num_hidden_layers = Сколько скрытых слоев у нас в этой нейронной сети

vocab_size = количество токенов в модели токенизатора

Размер модели определяется скрытыми_размерами, vocab_size, промежуточными_размерами и числом_скрытых_слоев. Изменение этих параметров приведет к увеличению или уменьшению размера модели. Значения этих параметров обычно выбираются кратными 8.

Вы также можете представить, что vocab_size — это количество нейронов во входном и выходном слоях.

В данном примере конфигурации будет создана модель параметров 400 КБ (0,4 М). мы должны сохранить конфигурацию модели как config.json по тому же пути, что и tokenizer.model, а затем мы делаем

config = LlamaConfig.from_pretrained(path)
model = LlamaForCausalLM(config)
if torch.backends.mps.is_available():
    device = torch.device("mps")
else:
    device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model.to(device)

Вышеупомянутое будет читать конфигурацию и строить модель ламы, используя конфигурацию, и отправлять ее в графический процессор, если он доступен в противном случае для процессора. Помните, что обучение модели с помощью процессора просто неэффективно и займет очень много времени.

Шаг 3. Подготовьте набор данных для обучения.

Когда у нас есть токенизатор и модель, пришло время загрузить набор данных и разделить его на обучение и тестирование (80:20). Для этого примера мы взяли примерно 10 тысяч строк детских имен на тамильском языке с префиксом пола.

dataset = load_dataset('text', data_files=path)
shuffled_dataset = dataset['train'].shuffle(seed=42)
split_datasets = shuffled_dataset.train_test_split(test_size=0.2)

train_dataset = split_datasets['train'].map(
    lambda examples: tokenizer(
        examples['text'], truncation=True,
        padding='max_length', max_length=block_size
    ),
    batched=True
)

test_dataset = split_datasets['test'].map(
    lambda examples: tokenizer(
        examples['text'], truncation=True,
        padding='max_length', max_length=block_size
    ),
    batched=True
)

Несколько примеров строк из набора данных показаны ниже.

பெண்,அகநகை.
பெண்,அகல்.
பெண்,அகல்நிலா.
பெண்,அகல்விழி.
பெண்,அகவழகி.
பெண்,அங்கவை.

Шаг 4: Причинно-языковое моделирование, или предварительное обучение

Для обучения нашей модели мы используем технику Causal Language Modeling (CLM) из библиотеки трансформаторов Huggingface.

Причинное языковое моделирование – это метод прогнозирования распределения вероятностей слов, которые, скорее всего, будут следующими, учитывая предыдущую последовательность слов.

Запомните и поймите приведенное выше утверждение, даже если это займет время. Знания и интеллект языковых моделей проистекают из их способности эффективно выполнять вышеуказанное моделирование.

В основе CLM лежит DataCollator, Huggingface Trainer. Тренер управляет машинным обучением. Для более глубокого изучения параметров обучения потребуется отдельная статья. На данный момент это ключевые параметры

num_train_epochs = Сколько раз мы повторяем передачу одного и того же набора данных тренеру

per_device_train_batch_size = Сколько строк данных будет отправлено в пакете. Для эффективного обучения оно должно быть ≥2. Рекомендуется кратность 8.

eval_steps = Как часто мы оцениваем обучение модели с помощью тестового разделения

data_collator = DataCollatorForLanguageModeling(tokenizer=tokenizer, mlm=False)

training_args = TrainingArguments(
    output_dir=out_folder_path,
    overwrite_output_dir=True,
    num_train_epochs=100,
    per_device_train_batch_size=8,
    save_steps=10000,
    logging_steps=10,
    eval_steps=1000,
    logging_dir=f'{out_folder_path}/logs',
    evaluation_strategy="steps",
    load_best_model_at_end=True,
    metric_for_best_model="loss",
    greater_is_better=False,
)

trainer = Trainer(
    model=model,
    args=training_args,
    data_collator=data_collator,
    train_dataset=train_dataset,
    eval_dataset=test_dataset,
    callbacks=[EarlyStoppingCallback(early_stopping_patience=2, early_stopping_threshold=0.001)]
)

trainer.train()
model.save_pretrained(out_folder_path)

После завершения обучения лучшая модель сохраняется в выходной папке. Здесь мы также использовали метод EarlyStoppingCallback, который автоматически прерывает обучение, если потери при проверке перестают улучшаться. Для тех, кто плохо знаком с машинным обучением, «Потеря проверки» — это способность модели обрабатывать невидимые данные. Почему это так важно, когда кажется, что потери в тренировках улучшаются? Цель тренинга – научить модель формированию имен на тамильском языке. Когда потери при проверке не уменьшаются, это означает, что модель больше не изучает шаблон или концепцию, а скорее начала «переобучать» данные обучения. Когда происходит переобучение, модель имеет тенденцию запоминать данные, а не изучать на их основе закономерности и принципы. Запоминание — это плохо, потому что оно отнимает у модели творческие способности. Переоснащенная модель подобна дрессированному попугаю: она просто повторяет увиденные данные и не производит ничего нового и не думает самостоятельно.

Вот статистика обучения для нашего примера: вы можете видеть, что обучение прекращается, когда потери при проверке перестают улучшаться.

Шаг 5: Вывод

После завершения обучения модели мы подсказываем модель. model.generate — это ключевая функция, которая принимает закодированное приглашение в качестве входных данных.

Параметры вывода также достойны отдельной статьи. Они являются ключевыми

max_length = Сколько новых токенов должно быть сгенерировано

температура = насколько креативной или логичной должна быть модель (макс. 1,0).

top_p = в выходных данных будут учитываться только те токены, которые превышают эту вероятность

top_k = Если большинство токенов преодолеют порог вероятности, каким будет ограничение на количество токенов

повторение_пеналти = Наказать модель, если она неоднократно повторяет один и тот же набор токенов

def generate_names(model, tokenizer, prompt):
    input_ids = tokenizer.encode(prompt, return_tensors="pt").to(model.device)
    attention_mask = torch.ones_like(input_ids).to(model.device)
    generated_names = set()

    with torch.no_grad():
        while len(generated_names) < 20:
            output = model.generate(
                input_ids=input_ids,
                attention_mask=attention_mask,
                max_length=32,
                early_stopping=True,
                temperature=0.9,
                top_p=0.9,
                top_k=50,
                do_sample=True,
                output_scores=True,
                pad_token_id=tokenizer.eos_token_id,
                repetition_penalty=1.4,
                eos_token_id=tokenizer.eos_token_id
            )
            output_str = tokenizer.decode(output[0], skip_special_tokens=True).split(".")[0]
            if output_str not in generated_names and output_str not in unique_names:
                print(output_str)
                generated_names.add(output_str)

male_names_prompt = "ஆண்,"
female_names_prompt = "பெண்,"

model.eval()
generate_names(model, tokenizer, male_names_prompt)
generate_names(model, tokenizer, female_names_prompt)

Приведенный выше блок сценария написан для вывода 20 уникальных имен, которых нет в исходном наборе обучающих данных.

Вот некоторые интересные имена, которые я видел в модели. Мы можем получить гораздо лучшие имена, если расширим набор обучающих данных дальше.

ஆண்,திருநாவன்
ஆண்,காரிக்குன்னன்
ஆண்,அகலாரழகன்
ஆண்,காரிக்குமரன்
ஆண்,குமரிமாறன்
ஆண்,கார்மேயன்
பெண்,தேனிசைமாமகள்
பெண்,காரியம்மை
பெண்,திருவாய்மதி

С целью познакомить вас с концепциями. Я останавливаюсь здесь. То, что я здесь показал, — это лишь верхушка айсберга. Если мы углубимся в это, можно увидеть гораздо больше.

Полная реализация и демо-версия доступны как Блокнот Google Colab здесь. Пожалуйста, запустите блокнот, чтобы увидеть результаты самостоятельно. Если вы измените токенизатор и набор данных, концепция должна работать для любого языка.

В зависимости от вашего ответа и участия в этой статье, я смогу расширить свое написание этой статьи. Поверьте, чтобы написать такую ​​статью, нужны целые выходные. Поэтому хотелось убедиться, что оно того стоит.