Tinh chỉnh mô hình ngôn ngữ lớn với thư viện Hugging Face

1. Môi trường huấn luyện được đề xuất

Để tối ưu chi phí và tận dụng tài nguyên miễn phí, Google ColabKaggle là hai lựa chọn hàng đầu. Cả hai nền tảng này đều cung cấp GPU miễn phí hoặc với chi phí thấp.

  • Google Colab: Với bản Pro+ (khoảng 300 VNĐ/tháng), người dùng có thể truy cập GPU A100 40GB, mặc dù có giới hạn thời gian sử dụng. Colab cũng cho phép dễ dàng kết nối với Google Drive.

    https://colab.research.google.com/

  • Kaggle: Nền tảng này cung cấp miễn phí hai GPU T4, với giới hạn 30 giờ mỗi tuần. Đây là lựa chọn tốt cho những ai muốn tiết kiệm chi phí hoàn toàn.

    Kaggle: Your Machine Learning and Data Science Community

2. Lựa chọn mô hình nền tảng

Trong hướng dẫn này, chúng ta sẽ tập trung vào việc tinh chỉnh một mô hình sinh văn bản dạng hỏi đáp, tương tự như cách DeepSeek hoặc GPT hoạt động. Đối với các tác vụ hội thoại, việc sử dụng các mô hình đã được tinh chỉnh cho instruction-following (tuân thủ chỉ thị) là rất quan trọng.

Một ví dụ điển hình là meta-llama/Llama-3.2-3B-Instruct. Bạn có thể tự do lựa chọn mô hình khác phù hợp với tài nguyên phần cứng và yêu cầu cụ thể của mình bằng cách điều chỉnh biến TEN_MAU_HINH.

# TEN_MAU_HINH = "meta-llama/Llama-3.2-1B"
TEN_MAU_HINH = "meta-llama/Llama-3.2-3B-Instruct"
# TEN_MAU_HINH = "Qwen/Qwen2.5-1.5B"

Mã nguồn được cung cấp hỗ trợ tinh chỉnh các mô hình tự hồi quy (autoregressive models) như GPT, Llama, Qwen. Nếu bạn quan tâm đến việc tinh chỉnh các kiến trúc khác như T5 hoặc BERT, bạn sẽ cần tùy chỉnh hàm xử lý token (tokenize_function) cho phù hợp.

3. Tiền xử lý dữ liệu

Một thách thức phổ biến khi làm việc với các bộ dữ liệu trên Hugging Face là sự đa dạng về định dạng. Các trường khóa có thể là input, question, dialog, v.v., gây khó khăn trong việc chuẩn hóa dữ liệu đầu vào cho mô hình.

Đối với các mô hình tự hồi quy, dữ liệu đầu vào cần được nối thành một chuỗi văn bản hoàn chỉnh. Để mô hình có thể phân biệt giữa phần "input" (câu hỏi) và phần "output" (câu trả lời), các mô hình thường sử dụng các token đặc biệt để đánh dấu sự bắt đầu và kết thúc của các đoạn hội thoại hoặc vai trò của người nói (ví dụ: người dùng, trợ lý).

Chúng ta sẽ sử dụng AutoTokenizer từ thư viện transformers để tải trình token hóa:

from transformers import AutoTokenizer

trinh_token_hoa = AutoTokenizer.from_pretrained(
    pretrained_model_name_or_path=TEN_MAU_HINH, 
    cache_dir=THU_MUC_CACHE_MAU_HINH
)

Nếu kiểm tra biến added_tokens_encoder của trình token hóa, bạn sẽ thấy các token đặc biệt được định nghĩa cho mô hình đó.

Ví dụ, một đoạn hội thoại đơn giản như:

  • Người dùng: Xin chào
  • Trợ lý: Xin chào, tôi có thể giúp gì cho bạn?

Trước khi được token hóa, chuỗi đầu vào thực tế cho mô hình sẽ được định dạng theo một template chat cụ thể, thường là ChatML, trông như sau:

<|eot_id|><|start_header_id|>user<|end_header_id|>
Xin chào<|eot_id|><|start_header_id|>assistant<|end_header_id|>
Xin chào, tôi có thể giúp gì cho bạn?<|eot_id|>

Khi suy luận, bạn chỉ cần cung cấp phần câu hỏi và token bắt đầu của câu trả lời:

<|eot_id|><|start_header_id|>user<|end_header_id|>
Xin chào<|eot_id|><|start_header_id|>assistant<|end_header_id|>

Lúc này, mô hình sẽ biết rằng nó cần tạo ra phần phản hồi tiếp theo. Thay vì tự định nghĩa hàm chuyển đổi cho từng bộ dữ liệu, chúng ta chỉ cần chuyển đổi dữ liệu thành định dạng ChatML và sử dụng hàm tokenizer.apply_chat_template() tiện lợi.

Để tìm hiểu thêm về ChatML, bạn có thể tham khảo tài liệu Chat Templating của Hugging Face.

Bộ dữ liệu được sử dụng trong ví dụ này là tập con Meidcal_Encyclopedia_cn từ bộ FreedomIntelligence/HuatuoGPT2-Pretraining-Instruction, với hơn 400.000 dòng dữ liệu.

Liên kết dataset

4. Cấu hình tinh chỉnh (LoRA)

Do giới hạn về phần cứng, chúng ta sẽ áp dụng phương pháp tinh chỉnh LoRA (Low-Rank Adaptation), giúp giảm đáng kể số lượng tham số cần huấn luyện trong khi vẫn duy trì hiệu quả tốt. Đồng thời, việc kích hoạt chế độ độ chính xác hỗn hợp bf16 sẽ giúp giảm mức tiêu thụ bộ nhớ và tăng tốc độ tính toán.

from peft import LoraConfig, get_peft_model
from transformers import TrainingArguments

cau_hinh_lora = LoraConfig(
    r=8,  # Hạng của LoRA
    lora_alpha=32,  # Hệ số tỷ lệ LoRA
    lora_dropout=0.05,  # Tỷ lệ dropout
    bias="none",  # Không tinh chỉnh bias
    task_type="CAUSAL_LM",  # Loại tác vụ: sinh văn bản tự hồi quy
    target_modules=["q_proj", "k_proj", "v_proj", "o_proj"], # Các module để áp dụng LoRA
)

doi_so_dao_tao = TrainingArguments(
    output_dir=THU_MUC_CHECKPOINT,
    overwrite_output_dir=True,
    per_device_train_batch_size=8,  # Điều chỉnh linh hoạt theo VRAM GPU
    per_device_eval_batch_size=8,   # Điều chỉnh linh hoạt theo VRAM GPU
    gradient_accumulation_steps=8,  # Số bước tích lũy gradient
    num_train_epochs=3,  # Số epoch huấn luyện
    learning_rate=2e-4,  # Tốc độ học
    bf16=True,  # Kích hoạt chế độ bf16
    logging_steps=50,  # Ghi log mỗi 50 bước
    evaluation_strategy="steps", # Đánh giá theo số bước
    eval_steps=1000,  # Đánh giá mỗi 1000 bước (điều chỉnh theo kích thước dữ liệu)
    save_strategy="steps", # Lưu checkpoint theo số bước
    save_steps=500, # Lưu checkpoint mỗi 500 bước
    report_to="none", # Không báo cáo lên nền tảng nào
)

Bạn nên điều chỉnh các tham số khác như per_device_train_batch_size, eval_steps, save_steps dựa trên cấu hình phần cứng và kích thước bộ dữ liệu của mình.

5. Chi tiết mã nguồn

5.1. Các thao tác khởi tạo

Các bước này thường được thực hiện ở đầu notebook, đặc biệt khi dùng Colab:

from google.colab import drive
from huggingface_hub import login
import os

# Gắn Google Drive. Bỏ qua nếu không sử dụng Colab hoặc không cần lưu vào Drive.
drive.mount('/content/drive')

# Đăng nhập vào Hugging Face Hub bằng token của bạn
# (lưu trữ token an toàn bằng userdata.get trong Colab)
login(token=userdata.get('hugging_face_token')) # Cần nếu sử dụng mô hình Llama

Lưu ý: Chức năng drive.mount() chỉ hoạt động trên Google Colab. Hàm login() là cần thiết nếu bạn làm việc với các mô hình yêu cầu xác thực, ví dụ như một số phiên bản của Llama.

5.2. Giao diện dữ liệu và hàm tiền xử lý token

Do mỗi bộ dữ liệu có cấu trúc khác nhau, chúng ta định nghĩa một giao diện chung để các lớp con có thể triển khai logic trích xuất đầu vào và đầu ra riêng. Hàm chuyen_doi_token_cho_lm sẽ chuẩn hóa dữ liệu thành định dạng mà mô hình yêu cầu.

from abc import ABC, abstractmethod
from datasets import load_dataset

# Định nghĩa các hằng số (ví dụ)
THU_MUC_CACHE_DU_LIEU = "./data_cache"
THU_MUC_CACHE_MAU_HINH = "./model_cache"
THU_MUC_CHECKPOINT = "./checkpoints"
THU_MUC_DAU_RA_MAU_HINH = "./fine_tuned_model"
# TEN_MAU_HINH đã được định nghĩa ở mục 2.

# Hàm trợ giúp để định dạng ChatML
def dinh_dang_chatml_nhieu_mau(cac_cau_hoi: list[str], cac_cau_tra_loi: list[str]) -> list[list[dict]]:
    """Chuyển đổi các cặp câu hỏi-trả lời thành định dạng ChatML cho nhiều mẫu."""
    danh_sach_chatml = []
    for cau_hoi, cau_tra_loi in zip(cac_cau_hoi, cac_cau_tra_loi):
        chatml_mau = []
        if cau_hoi:
            chatml_mau.append({"role": "user", "content": cau_hoi})
        if cau_tra_loi:
            chatml_mau.append({"role": "assistant", "content": cau_tra_loi})
        danh_sach_chatml.append(chatml_mau)
    return danh_sach_chatml

class GiaoDienDuLieu(ABC):
    """Giao diện trừu tượng cho việc tải và tiền xử lý bộ dữ liệu."""
    DUONG_DAN = ''
    TEN_CON = None

    def __init__(self, trinh_token_hoa_tham_so):
        self.du_lieu_nguyen = load_dataset(self.DUONG_DAN, self.TEN_CON, cache_dir=THU_MUC_CACHE_DU_LIEU)
        self.trinh_token_hoa = trinh_token_hoa_tham_so

    def lay_du_lieu(self):
        """Trả về đối tượng dataset đã tải."""
        return self.du_lieu_nguyen

    @abstractmethod
    def _trich_xuat_dau_vao_dau_ra_nhieu_mau(self, mau_du_lieu):
        """
        Trích xuất các câu hỏi và câu trả lời từ một lô mẫu dữ liệu.
        Phương thức này cần được triển khai bởi các lớp con.
        """
        pass

    def chuyen_doi_token_cho_lm(self, mau_du_lieu):
        """
        Tiền xử lý và token hóa một lô mẫu dữ liệu cho mô hình ngôn ngữ tự hồi quy (Causal LM).
        """
        cac_cau_hoi, cac_cau_tra_loi = self._trich_xuat_dau_vao_dau_ra_nhieu_mau(mau_du_lieu)
        
        # Định dạng thành ChatML
        dinh_dang_chatml = dinh_dang_chatml_nhieu_mau(cac_cau_hoi, cac_cau_tra_loi)
        
        # Áp dụng template chat của tokenizer
        dau_vao_da_dinh_dang = [
            self.trinh_token_hoa.apply_chat_template(chatml_item, tokenize=False)
            for chatml_item in dinh_dang_chatml
        ]

        # Token hóa và thêm padding/truncation
        dau_vao_mo_hinh = self.trinh_token_hoa(
            dau_vao_da_dinh_dang, 
            padding="max_length", 
            truncation=True, 
            max_length=128
        )
        # Đối với Causal LM, nhãn thường là bản sao của input_ids
        dau_vao_mo_hinh["labels"] = dau_vao_mo_hinh["input_ids"].copy()
        return dau_vao_mo_hinh

class BoDuLieuHuatuo(GiaoDienDuLieu):
    """Lớp triển khai cụ thể cho bộ dữ liệu HuatuoGPT2."""
    DUONG_DAN = "FreedomIntelligence/HuatuoGPT2-Pretraining-Instruction"
    TEN_CON = "Meidcal_Encyclopedia_cn"

    def _trich_xuat_dau_vao_dau_ra_nhieu_mau(self, cac_vi_du):
        cac_cau_hoi = []
        cac_cau_tra_loi = []
        for hoi_thoai in cac_vi_du['conversations']:
            cau_hoi_hien_tai = ""
            cau_tra_loi_hien_tai = ""
            for luot_noi in hoi_thoai:
                if luot_noi["from"] == "human":
                    cau_hoi_hien_tai = luot_noi["value"]
                elif luot_noi["from"] == "gpt":
                    cau_tra_loi_hien_tai = luot_noi["value"]
            cac_cau_hoi.append(cau_hoi_hien_tai.strip())
            cac_cau_tra_loi.append(cau_tra_loi_hien_tai.strip())
        return cac_cau_hoi, cac_cau_tra_loi

# Khởi tạo và xử lý bộ dữ liệu (trinh_token_hoa đã được định nghĩa ở mục 3)
the_hien_du_lieu = BoDuLieuHuatuo(trinh_token_hoa)
du_lieu_goc = the_hien_du_lieu.lay_du_lieu()

# Chuyển đổi bộ dữ liệu thành định dạng mô hình yêu cầu và chia tập huấn luyện/kiểm tra
du_lieu_da_token_hoa = du_lieu_goc.map(
    the_hien_du_lieu.chuyen_doi_token_cho_lm, 
    num_proc=4, 
    batched=True
)
bo_du_lieu_chia = du_lieu_da_token_hoa['train'].train_test_split(test_size=0.2)
tap_huan_luyen = bo_du_lieu_chia['train']
tap_kiem_tra = bo_du_lieu_chia['test']

Khi sử dụng transformers.Trainer, bộ dữ liệu huấn luyện cần phải có trường 'labels'. Đối với các mô hình tự hồi quy (Causal Language Models), nội dung của trường 'labels' thường là một bản sao của 'input_ids'.

5.3. Tải mô hình và bắt đầu huấn luyện

Chúng ta sẽ tải mô hình nền tảng, sau đó chuyển đổi nó thành một mô hình LoRA sử dụng get_peft_model(). Mã này cũng hỗ trợ tiếp tục huấn luyện từ checkpoint cuối cùng nếu quá trình bị gián đoạn.

import torch
from transformers import AutoModelForCausalLM, Trainer
from peft import get_peft_model # Đảm bảo đã import

# Tải mô hình nền tảng
mo_hinh_goc = AutoModelForCausalLM.from_pretrained(
    TEN_MAU_HINH,
    torch_dtype=torch.bfloat16,  # Sử dụng hỗn hợp độ chính xác bf16
    device_map="auto",  # Tự động phân bổ GPU
    cache_dir=THU_MUC_CACHE_MAU_HINH
).to("cuda")

# Áp dụng LoRA vào mô hình (cau_hinh_lora đã được định nghĩa ở mục 4)
mo_hinh_peft = get_peft_model(mo_hinh_goc, cau_hinh_lora).to("cuda")
mo_hinh_peft.print_trainable_parameters()  # In số lượng tham số có thể huấn luyện

# Kiểm tra sự tồn tại của checkpoint để tiếp tục huấn luyện
def kiem_tra_checkpoint_ton_tai(thu_muc_checkpoint: str) -> bool:
    """Kiểm tra xem có checkpoint nào trong thư mục được chỉ định không."""
    if not os.path.exists(thu_muc_checkpoint):
        return False
    return any(d.startswith("checkpoint") for d in os.listdir(thu_muc_checkpoint))

# Định nghĩa TrainingArguments (đã cấu hình ở mục 4)
# doi_so_dao_tao = TrainingArguments(...) 

# Khởi tạo Trainer
huong_dan_vien = Trainer(
    model=mo_hinh_peft,
    args=doi_so_dao_tao,
    train_dataset=tap_huan_luyen,
    eval_dataset=tap_kiem_tra,
    tokenizer=trinh_token_hoa,
)

# Bắt đầu huấn luyện, tiếp tục từ checkpoint nếu có
huong_dan_vien.train(resume_from_checkpoint=kiem_tra_checkpoint_ton_tai(THU_MUC_CHECKPOINT))
huong_dan_vien.save_model(THU_MUC_DAU_RA_MAU_HINH)

Để sử dụng chế độ độ chính xác hỗn hợp bf16, bạn cần thiết lập cả tham số torch_dtype khi tải mô hình và bf16=True trong TrainingArguments.

5.4. Xác thực kết quả

Sau khi huấn luyện, chúng ta sẽ kiểm tra hoạt động của mô hình bằng cách hợp nhất các trọng số LoRA vào mô hình gốc và sử dụng pipeline để suy luận.

from transformers import pipeline
from peft import PeftModel

# Hằng số (đã định nghĩa trước đó)
# THU_MUC_DAU_RA_MAU_HINH = "./fine_tuned_model" 
# TEN_MAU_HINH đã được định nghĩa ở mục 2.

# Định dạng đầu vào chat cho suy luận
def dinh_dang_dau_vao_chat(input_text: str, tokenizer_obj: AutoTokenizer) -> str:
    """Định dạng văn bản người dùng thành chuỗi đầu vào ChatML cho suy luận."""
    chatml_input = dinh_dang_chatml_nhieu_mau([input_text], [None])[0] # Chỉ lấy một mẫu
    formatted_input = tokenizer_obj.apply_chat_template(
        chatml_input, 
        tokenize=False, 
        add_generation_prompt=True # Thêm prompt để mô hình bắt đầu tạo sinh
    )
    return formatted_input

# Tải lại tokenizer (đảm bảo pad_token được thiết lập)
trinh_token_hoa_suy_luan = AutoTokenizer.from_pretrained(TEN_MAU_HINH)
if trinh_token_hoa_suy_luan.pad_token is None:
    trinh_token_hoa_suy_luan.pad_token = trinh_token_hoa_suy_luan.eos_token

# Tải mô hình gốc
mo_hinh_nen_tang = AutoModelForCausalLM.from_pretrained(
    TEN_MAU_HINH,
    torch_dtype=torch.float16, # Sử dụng float16 cho suy luận nếu cần
    device_map="auto",
    cache_dir=THU_MUC_CACHE_MAU_HINH
)

# Tải adapter LoRA và hợp nhất vào mô hình gốc
mo_hinh_adapter_lora = PeftModel.from_pretrained(mo_hinh_nen_tang, THU_MUC_DAU_RA_MAU_HINH)
mo_hinh_da_hop_nhat = mo_hinh_adapter_lora.merge_and_unload()
mo_hinh_da_hop_nhat.eval() # Chuyển mô hình sang chế độ đánh giá

# Khởi tạo pipeline sinh văn bản
ong_dan_sinh_van_ban = pipeline(
    "text-generation",
    model=mo_hinh_da_hop_nhat, # Sử dụng mô hình đã hợp nhất
    tokenizer=trinh_token_hoa_suy_luan,
    return_full_text=False, # Chỉ trả về phần sinh ra, không bao gồm prompt
    max_new_tokens=100 # Giới hạn số token mới
)

print("Bắt đầu trò chuyện! Nhập 'exit' để kết thúc.")
while True:
    dau_vao_nguoi_dung = input("Bạn: ")
    if dau_vao_nguoi_dung.lower() == "exit":
        print("Kết thúc trò chuyện.")
        break
    
    dau_vao_da_dinh_dang = dinh_dang_dau_vao_chat(dau_vao_nguoi_dung, trinh_token_hoa_suy_luan)
    phan_hoi = ong_dan_sinh_van_ban(dau_vao_da_dinh_dang)
    print(f"Mô hình: {phan_hoi[0]['generated_text']}")

Khi suy luận, chuỗi đầu vào của người dùng cũng cần được định dạng theo template chat tương ứng để mô hình có thể hiểu đúng ngữ cảnh và tạo ra phản hồi phù hợp. Bạn có thể in biến dau_vao_da_dinh_dang để kiểm tra định dạng.

6. Thí nghiệm Overfitting với dữ liệu nhỏ

Để xác minh rằng logic xử lý mã nguồn hoạt động chính xác, chúng ta tiến hành một thí nghiệm nhỏ: huấn luyện mô hình với một lượng dữ liệu rất ít để xem liệu nó có thể đạt được hiệu ứng overfitting (quá khớp) hay không.

Chúng ta sử dụng một bộ dữ liệu thử nghiệm mô phỏng cấu trúc của FreedomIntelligence/HuatuoGPT2-Pretraining-Instruction, chỉ chứa 5 dòng dữ liệu và tất cả đều có cùng câu trả lời: "Tôi không biết".

Bộ dữ liệu thử nghiệm

Để tăng khả năng overfitting, chúng ta điều chỉnh số epoch huấn luyện lên cao hơn:

doi_so_dao_tao = TrainingArguments(
    output_dir=THU_MUC_CHECKPOINT,
    overwrite_output_dir=True,
    per_device_train_batch_size=8,
    per_device_eval_batch_size=8,
    gradient_accumulation_steps=8,
    num_train_epochs=100, # Tăng số epoch đáng kể
    learning_rate=2e-4,
    bf16=True,
    logging_steps=10,
    evaluation_strategy="steps",
    eval_steps=10,
    save_strategy="steps",
    save_steps=10,
    report_to="none",
)

Sau khi huấn luyện với các tham số trên và tải mô hình, khi chúng ta đưa ra bất kỳ câu hỏi nào, mô hình đều nhất quán trả lời "Tôi không biết", cho thấy quá trình tinh chỉnh đã thành công trong việc làm mô hình quá khớp với bộ dữ liệu nhỏ này.

Điều này chứng minh rằng logic tiền xử lý và huấn luyện đã được thiết lập chính xác.

7. Kết quả huấn luyện và đánh giá

Sau hơn 7 giờ huấn luyện trên bộ dữ liệu đầy đủ, quá trình tinh chỉnh đã hoàn tất. Bây giờ, hãy so sánh phản hồi của mô hình trước và sau khi tinh chỉnh với một số câu hỏi thử nghiệm như "Đau đầu thì làm sao?", "Bị nhiệt miệng phải làm gì?", "Tại sao tôi uống rượu lại đỏ mặt?".

7.1. Phản hồi của mô hình trước tinh chỉnh

Mô hình gốc thường đưa ra các câu trả lời chung chung, hoặc đôi khi là những phản hồi không liên quan lắm đến chủ đề y tế, thiếu đi sự chuyên sâu và cấu trúc của một cuộc hội thoại trợ lý y tế.

7.2. Phản hồi của mô hình sau tinh chỉnh

Có thể thấy rõ ràng rằng phong cách trả lời của mô hình đã thay đổi đáng kể. Các phản hồi có cấu trúc tốt hơn, thường bắt đầu bằng các tiêu đề rõ ràng và bao gồm nhiều thuật ngữ y học hơn, mặc dù không phải lúc nào cũng đảm bảo độ chính xác tuyệt đối về mặt y khoa. Từ phản hồi cho câu hỏi thứ ba, có vẻ như mô hình vẫn còn một chút dấu hiệu của overfitting, điều này có thể được cải thiện bằng cách sử dụng một bộ dữ liệu lớn hơn và đa dạng hơn.

Thẻ: HuggingFace LLM Fine-tuning lora PEFT Transformers

Đăng vào ngày 18 tháng 6 lúc 01:07