Tìm hiểu quy trình huấn luyện GPT từ đầu với dự án nanoGPT

NanoGPT là một thư viện mã nguồn mở do Andrej Karpathy phát triển nhằm mục đích giáo dục, hiện thực hóa kiến trúc GPT-2 một cách đơn giản và dễ hiểu nhất. Toàn bộ mã nguồn được cô đọng trong vài tệp tin chính, giúp người học dễ dàng nắm bắt luồng xử lý từ khâu tiền xử lý dữ liệu đến quá trình huấn luyện. Để phù hợp với tài nguyên phần cứng thông thường, dự án cung cấp một phiên bản demo sử dụng kỹ thuật token hóa cấp ký tự (character-level) trên tập dữ liệu văn bản nhỏ.

1. Chuẩn bị và xử lý dữ liệu

Bước đầu tiên là thu thập và xử lý dữ liệu đầu vào. Trong ví dụ này, chúng ta sử dụng tập dữ liệu các tác phẩm của Shakespeare.

1.1. Tải dữ liệu

Chúng ta sẽ tải tệp dữ liệu về từ nguồn trực tuyến nếu chưa tồn tại trong thư mục làm việc.

import os
import requests

def load_shakespeare_data(destination):
    if not os.path.exists(destination):
        url = 'https://raw.githubusercontent.com/karpathy/char-rnn/master/data/tinyshakespeare/input.txt'
        response = requests.get(url)
        with open(destination, 'w', encoding='utf-8') as f:
            f.write(response.text)

dataset_path = 'input.txt'
load_shakespeare_data(dataset_path)

1.2. Xây dựng bộ từ vựng

Tập dữ liệu sẽ được phân tích để trích xuất các ký tự duy nhất nhằm xây dựng bảng từ vựng (vocabulary).

with open(dataset_path, 'r', encoding='utf-8') as f:
    text_content = f.read()

unique_symbols = sorted(list(set(text_content)))
vocab_size = len(unique_symbols)

print(f'Kích thước bộ từ vựng: {vocab_size}')
print('Các ký tự:', ''.join(unique_symbols))

Kết quả trả về bộ từ vựng có 65 ký tự. Đây là một con số rất nhỏ so với các mô hình hiện đại như Qwen2.5 (hơn 150k token), giúp giảm đáng kể độ phức tạp tính toán.

1.3. Mã hóa và giải mã (Encoding/Decoding)

Chúng ta thiết lập các ánh xạ hai chiều giữa ký tự và số nguyên để mô hình có thể xử lý.

char_to_int = {ch: i for i, ch in enumerate(unique_symbols)}
int_to_char = {i: ch for i, ch in enumerate(unique_symbols)}

def encode_string(s):
    return [char_to_int[c] for c in s]

def decode_indices(lst):
    return ''.join([int_to_char[i] for i in lst])

1.4. Chia tập dữ liệu

Dữ liệu được chia thành hai phần: 90% cho huấn luyện và 10% cho kiểm tra.

data_length = len(text_content)
split_index = int(data_length * 0.9)

train_dataset = text_content[:split_index]
test_dataset = text_content[split_index:]

train_tokens = encode_string(train_dataset)
test_tokens = encode_string(test_dataset)

2. Cấu hình và quá trình huấn luyện

Vì đây là mô hình thử nghiệm trên tập dữ liệu nhỏ, các siêu tham số (hyperparameters) được tinh chỉnh để phù hợp với bối cảnh này.

2.1. Thiết lập tham số

config = {
    'eval_interval': 250,        # Tần suất đánh giá
    'eval_iters': 200,           # Số bước đánh giá mỗi lần
    'log_interval': 10,          # Tần suất in log
    'gradient_accumulation': 1,  # Tích lũy gradient
    'context_window': 256,       # Độ dài chuỗi đầu vào (block_size)
    
    # Kiến trúc Transformer
    'num_layers': 6,
    'num_heads': 6,
    'embedding_dim': 384,
    
    # Huấn luyện
    'dropout': 0.2,
    'learning_rate': 1e-3,
    'max_iters': 5000,
    'warmup_iters': 100,
    'min_lr': 1e-4
}

2.2. Theo dõi huấn luyện

Quá trình huấn luyện diễn ra trong vài nghìn bước lặp. Hàm mất mát (loss) trên tập huấn luyện sẽ giảm từ mức khoảng 4.2 xuống còn khoảng 0.62, trong khi mất mát trên tập kiểm tra giảm xuống khoảng 1.7.

step 0: train loss 4.2874, val loss 4.2823
iter 10: loss 3.2457
...
iter 2500: loss 1.5012
...
step 5000: train loss 0.6208, val loss 1.7051

Sau khi hoàn thành, trọng số mô hình được lưu trữ trong một tệp tin có kích thước khoảng 129MB.

3. Thử nghiệm sinh văn bản (Inference)

Để kiểm tra khả năng của mô hình, chúng ta sử dụng một chuỗi ký tự ban đầu (prompt) và yêu cầu mô hình tiếp tục sinh ra các ký tự tiếp theo.

import torch

def sample_generation(prompt_text, length=500):
    device = 'cuda' if torch.cuda.is_available() else 'cpu'
    model.to(device)
    
    # Chuyển đổi prompt sang tensor
    input_ids = encode_string(prompt_text)
    input_tensor = torch.tensor(input_ids, dtype=torch.long, device=device).unsqueeze(0)
    
    model.eval()
    with torch.no_grad():
        # Sinh tiếp các token mới
        generated_ids = model.generate(input_tensor, max_new_tokens=length)
        
    return decode_indices(generated_ids[0].tolist())

# Thực hiện sinh văn bản với prompt "hi"
print(sample_generation("hi"))

Kết quả đầu ra

Khi nhập vào chuỗi "hi", mô hình có xu hướng tiếp tục bằng từ "his". Điều này là do trong tập dữ liệu Shakespeare, từ "his" xuất hiện với tần suất rất cao (hơn 1400 lần). Mô hình đã học được xác suất thống kê của các chuỗi ký tự.

Ví dụ đoạn văn bản được sinh ra:

his friends, you will keep you the crown,
For we are not so dead, nor art thou usurp,
And from her afearful way. But, a wize of was
As man with come as well-a-ways it enter'd with it.

BENVOLIO:
Be it poor of traitor like down upon the correct:
...

Mặc dù văn bản được tạo ra mang phong cách cổ điển và ngữ pháp chưa hoàn toàn chính xác, nhưng nó cho thấy mô hình đã học được cấu trúc cơ bản của ngôn ngữ. Vì đây là mô hình ngôn ngữ (Language Model) thuần túy chưa qua tinh chỉnh chỉ thị (instruction tuning), nó không hỗ trợ đối thoại hay thực hiện lệnh phức tạp mà chủ yếu hoạt động theo cơ chế hoàn thành văn bản.

Thẻ: GPT nanoGPT PyTorch LLM Transformer

Đăng vào ngày 19 tháng 7 lúc 12:01