DPO
This content is not available in your language yet.
Mục tiêu
Phần tiêu đề “Mục tiêu”Sau bài này, bạn sẽ:
- Hiểu vì sao tối ưu ưa thích trực tiếp (Direct Preference Optimization, DPO) tối ưu đúng mục tiêu của bước RL trong RLHF mà không cần mô hình phần thưởng (reward model, RM) riêng, không cần vòng học tăng cường.
- Tự dẫn được loss DPO qua bốn bước: nghiệm của mục tiêu có phạt KL, đảo ra phần thưởng, thay vào Bradley–Terry để hằng số chuẩn hóa triệt tiêu, rồi cực đại likelihood.
- Đọc được gradient của DPO: vì sao cặp đang xếp sai được đẩy mạnh nhất, và làm gì.
- Nhận ra các hiểu nhầm hay gặp: DPO vẫn có phần thưởng (ẩn), vẫn cần mô hình tham chiếu (reference model), và loss thấp chưa chắc là mô hình tốt.
Cần biết trước
Phần tiêu đề “Cần biết trước”- RLHF, in Large Language Models(Not translated yet) (Learned)
Trực giác
Phần tiêu đề “Trực giác”Một đường vòng khá xa
Phần tiêu đề “Một đường vòng khá xa”Bạn có một bộ dữ liệu ưa thích: vài chục nghìn bộ ba gồm prompt, câu trả lời được chọn và câu trả lời bị loại. Theo RLHF, bạn dùng nó qua hai chặng. Chặng một: huấn luyện một RM bắt chước lựa chọn của người. Chặng hai: chạy PPO, tức chính sách sinh câu trả lời, RM chấm, cập nhật, rồi lại sinh. Trong lúc chạy có bốn mô hình cùng nằm trong bộ nhớ: chính sách, mô hình tham chiếu, RM và hàm giá trị (value function). Kèm theo là nhiều siêu tham số nhạy cảm.
Để ý rằng RM chỉ là trạm trung chuyển. Thông tin thật nằm trong các cặp so sánh. RM nén nó thành một hàm điểm, rồi PPO lại bung hàm điểm đó ra thành chính sách. Mỗi chặng thêm một chỗ có thể sai: RM chấm sai ở vùng nó chưa thấy, và PPO tìm ra đúng chỗ sai đó, như phần reward hacking ở bài trước.
Đáp án nằm sẵn trong một công thức
Phần tiêu đề “Đáp án nằm sẵn trong một công thức”Bài trước có một công thức đáng chú ý: nếu biết hàm điểm , chính sách tốt nhất là . Công thức đi từ điểm ra chính sách. Rafailov và cộng sự (2023) hỏi: đi ngược lại thì sao? Nếu mỗi chính sách ứng với một hàm điểm, thì thay vì học điểm rồi đi tìm chính sách, ta học thẳng chính sách, và đọc điểm ra từ chính nó.
Quay lại chuyện học nấu ăn ở bài trước. RLHF thuê một giám khảo ảo, rồi nấu đi nấu lại cho giám khảo ảo chấm. DPO bỏ giám khảo. Bạn cầm từng cặp “món A ngon hơn món B” và sửa thẳng công thức nấu: so với cách nấu cũ, nghiêng thêm về A, rời xa B. “Điểm” của một món chính là bạn đã đổi cách nấu món đó bao nhiêu so với cách cũ.
Bài toán của bài này: từ mục tiêu “điểm cao nhưng đừng đi xa mô hình SFT”, làm sao đổi biến để huấn luyện chính sách trực tiếp trên các cặp ưa thích, bằng một loss đơn giản như bài phân loại, không cần RM riêng và không cần vòng RL?
Lý thuyết
Phần tiêu đề “Lý thuyết”Ký hiệu
Phần tiêu đề “Ký hiệu”Giữ như bài RLHF: prompt , câu trả lời , câu được chọn , câu bị loại , chính sách , mô hình tham chiếu , hệ số phạt KL . Paper DPO viết chính sách là . Bài này giữ cho khớp bài trước, nơi là tham số của RM. Paper viết , bài viết , cùng một nghĩa.
là xác suất của cả câu trả lời. Như bài trước, là tổng log-prob của từng token, nên với một câu vài chục token, nó là một số âm cỡ vài chục.
Bốn bước dưới đây theo mục 4 và phụ lục A của paper.
Bước 1: nghiệm của mục tiêu có phạt KL
Phần tiêu đề “Bước 1: nghiệm của mục tiêu có phạt KL”Cố định một prompt và một hàm điểm bất kỳ. Để gọn, tạm bỏ khỏi ký hiệu. Giả sử được chọn tự do trong mọi phân phối trên tập câu trả lời, không bị giới hạn bởi kiến trúc mạng. Viết kỳ vọng thành tổng, mục tiêu là
Tìm làm lớn nhất cũng là tìm làm nhỏ nhất: chia cho số dương không đổi điểm tối ưu, còn đổi dấu thì lớn nhất thành nhỏ nhất.
Gộp vào log. Vì , và hiệu hai log bằng log của thương:
Mẫu số trông như một phân phối, nhưng tổng của nó trên mọi nói chung không bằng 1. Ta chuẩn hóa nó. Đặt
là hằng số chuẩn hóa (partition function, còn gọi là hàm phân hoạch). dương và có tổng bằng 1, nên là một phân phối hợp lệ. Mẫu số ở trên đúng bằng , và log của một thương tách được thành hiệu:
Thay lại vào tổng. không phụ thuộc và , nên số hạng ra ngoài nguyên vẹn. Phần còn lại đúng là định nghĩa của KL:
không phụ thuộc . KL luôn , và bằng 0 khi và chỉ khi hai phân phối trùng nhau (bất đẳng thức Gibbs, Gibbs’ inequality). Vậy nhỏ nhất, tức lớn nhất, đúng khi . Viết lại :
Đây là công thức bài trước đã dùng. Tiện thể, tại thì KL bằng 0, nên giá trị lớn nhất của mục tiêu là .
Vì sao không dùng thẳng công thức này cho LLM? Vì là tổng trên mọi câu trả lời có thể. Từ vựng token và câu dài token thì có câu. Không ai tính nổi.
Bước 2: đảo ra phần thưởng
Phần tiêu đề “Bước 2: đảo ra phần thưởng”Lấy hai vế của công thức . Log của một tích là tổng các log:
Chuyển vế để đứng một mình, gộp hai log thành log của thương, rồi nhân hai vế với :
Đọc: điểm của một câu trả lời bằng nhân “chính sách tối ưu thích câu này hơn mô hình tham chiếu bao nhiêu”, cộng một số chỉ phụ thuộc prompt. Biết chính sách là biết điểm, sai khác một hằng số theo . Có điều hằng số đó vẫn chứa .
Bước 3: thay vào Bradley–Terry, Z triệt tiêu
Phần tiêu đề “Bước 3: thay vào Bradley–Terry, Z triệt tiêu”Bradley–Terry chỉ dùng hiệu điểm của hai câu trả lời cho cùng một prompt. Thay biểu thức ở bước 2 vào . Hai câu chung , nên chung số hạng , và hai số hạng đó trừ nhau bằng 0:
Không còn , không còn . Xác suất người chọn chỉ còn phụ thuộc và . Cũng vì thế mà dữ liệu DPO phải là cặp trả lời cho cùng một prompt: ghép hai câu của hai prompt khác nhau thì không triệt tiêu.
Kiểm bằng số trên ví dụ ba đáp án của bài trước: mô hình SFT viết A, B, C với xác suất , RM chấm , và . Tính theo bước 1, rồi so:
| Đại lượng | A | B | C |
|---|---|---|---|
| 0.175 | 0.238 | 0.587 | |
| 1.2308 | 1.2308 | 1.2308 |
Hàng cuối giống nhau ở cả ba đáp án. Đó chính là , với . Nên hiệu của hàng thứ hai giữa hai đáp án bất kỳ đúng bằng hiệu điểm RM: B so với A là .
Bước 4: loss DPO
Phần tiêu đề “Bước 4: loss DPO”Ta không biết . Nhưng ta có chính sách với tham số học được. Ý của DPO: coi là ứng viên cho , đặt nó vào công thức xác suất ở bước 3, rồi chỉnh để công thức đó giải thích tốt nhất lựa chọn thật của người. Làm như với RM ở bài trước: cực đại likelihood, lấy , trung bình trên các cặp:
So với loss của RM, : cùng một dạng. Chỉ khác là điểm , vốn do một mạng riêng cho ra, được thay bằng
Paper gọi đây là phần thưởng ẩn (implicit reward). Mô hình ngôn ngữ vừa là chính sách, vừa là RM. Đó là ý của tên paper: “Your Language Model is Secretly a Reward Model”.
Viết gọn cho một cặp. Vì , mỗi log tỉ số là hiệu hai log-prob. Bỏ cho gọn, đặt
cho biết câu thắng được nâng lên so với tham chiếu nhiều hơn câu thua bao nhiêu. Đại lượng trong loss là lề (margin), như bài trước: hiệu hai phần thưởng ẩn, . Loss của một cặp là . Khi chính sách còn trùng tham chiếu, và loss bằng , đúng như RM chưa học gì.
Mọi thứ trong loss là log-prob: chạy và trên và , cộng log-prob các token của phần trả lời. Không cần sinh câu mới. không đổi suốt quá trình, nên log-prob của nó tính trước một lần là đủ.
Đổi biến, không xấp xỉ thêm. Bước 1 đến 3 là đẳng thức, nên loss DPO là một phép đổi biến: tìm thẳng chính sách thay vì tìm hàm điểm rồi mới tìm chính sách. Với vài giả thiết nhẹ, paper chứng minh (mục 5.1, định lý 1) rằng mỗi lớp hàm điểm hợp với Bradley–Terry (gom các hàm điểm chỉ khác nhau một hàm của ) đều có một đại diện dạng , nên cách đặt tham số này không bỏ sót lớp nào.
Gradient: DPO học như thế nào
Phần tiêu đề “Gradient: DPO học như thế nào”Bài trước đã tính: đạo hàm của theo lề là , mà . Ở đây . Trong , hai số hạng của là hằng số, nên theo quy tắc chuỗi, gradient của theo là . Ghép lại:
Đọc từng phần, nhớ rằng ta đi ngược gradient:
- Ngoặc cuối: tăng log-prob của câu thắng, giảm log-prob của câu thua.
- Trọng số , bằng : xác suất mà phần thưởng ẩn cho rằng người đã chọn câu kia. Cặp đang xếp sai () có trọng số trên 0.5, được đẩy mạnh. Cặp đã xếp đúng với lề lớn gần như không được đẩy thêm.
- ở đầu: hệ số chung, làm bước cập nhật lớn hay nhỏ.
Trọng số này quan trọng. Paper có so với một cách làm không có trọng số, gọi là unlikelihood: chỉ tăng log-prob câu được chọn, giảm log-prob câu bị loại, cặp nào cũng cùng một lực. Trên bài tóm tắt TL;DR, cách đó sinh ra những chuỗi vô nghĩa kiểu “when when when when…” (bảng 3 ở phụ lục của paper).
Vai trò của β
Phần tiêu đề “Vai trò của β”có mặt ở hai chỗ, và cả hai nói cùng một điều.
- Trong mục tiêu, là hệ số phạt KL. Nhìn nghiệm : lớn thì thừa số gần 1, chính sách gần như đứng yên. nhỏ thì chính sách được đi xa.
- Trong loss, nhân thành lề. Muốn một cặp đạt : với cần , với cần . nhỏ đòi chính sách dịch log-prob xa tham chiếu hơn nhiều. Đó là cách nói khác của “dây lỏng”.
khuếch đại cả hai chiều: cặp xếp đúng () có loss giảm khi tăng, cặp xếp sai () có loss tăng. Vì thế loss trung bình không nhất thiết đơn điệu theo , như ô code sẽ cho thấy. Đừng so loss giữa hai lần chạy có khác nhau.
Paper dùng làm mặc định, riêng bài tóm tắt TL;DR dùng (phụ lục B). Đừng nhầm với tốc độ học : paper dùng với bộ tối ưu RMSprop, một con số hoàn toàn khác.
Sợi dây cũng không chặn được mọi thứ. Azar và cộng sự (2023) chỉ ra: nếu dữ liệu cho thấy một đáp án luôn thắng, loss chỉ về 0 khi , và DPO đẩy xác suất của câu thua về 0 bất kể . Đây giống trường hợp dữ liệu tách được với RM ở bài trước.
DPO so với RLHF
Phần tiêu đề “DPO so với RLHF”Quy trình DPO theo paper có hai bước. Một: với mỗi prompt, lấy hai câu trả lời từ và nhờ người chọn, hoặc dùng lại một bộ dữ liệu ưa thích công khai. Hai: tối ưu trên bộ dữ liệu cố định đó. Không sinh câu nào trong lúc huấn luyện, nên gọi là học ngoại tuyến (offline). là mô hình SFT. Khi không có mô hình SFT, họ fine-tune trên các câu được chọn trước, để gần với phân phối đã sinh ra dữ liệu.
Kết quả của họ nên đọc kèm phạm vi: mô hình tối đa 6 tỉ tham số, năm 2023. Ở bài điều khiển cảm xúc trên IMDb, DPO cho cân bằng giữa phần thưởng và KL tốt hơn PPO, kể cả khi PPO được dùng phần thưởng thật. Ở bài tóm tắt TL;DR, so với bản tóm tắt tham chiếu trong tập test và do GPT-4 chấm, DPO có tỉ lệ thắng (win rate) khoảng 61%, PPO khoảng 57% ở nhiệt độ tốt nhất của nó. Chính tác giả ghi rằng tỉ lệ thắng do GPT-4 tính bị ảnh hưởng bởi prompt chấm, và việc mở rộng lên mô hình lớn hơn nhiều vẫn còn để ngỏ. Câu “DPO ổn định hơn PPO” là lập luận của họ (mục 5.2, về nguồn gốc bất ổn của các thuật toán actor-critic như PPO), không phải một phép đo trực tiếp.
Họ hàng của DPO
Phần tiêu đề “Họ hàng của DPO”Chỉ nêu ý, để bạn biết tên khi gặp:
- IPO (Identity Preference Optimization, Azar và cộng sự, 2023). Nhắm vào chỗ overfit vừa nói. Trong khung lý thuyết của họ, DPO ứng với một hàm biến đổi xác suất ưa thích là hàm logit. IPO thay nó bằng hàm đồng nhất, nên loss thành bình phương, kéo về một giá trị hữu hạn thay vì vô cùng. Trong TRL là
loss_type="ipo", và khi đó tham sốbetađóng vai hệ số điều chuẩn của paper IPO. - KTO (Kahneman–Tversky Optimization, Ethayarajh và cộng sự, 2024). Không cần cặp: mỗi câu trả lời chỉ cần nhãn “tốt” hay “xấu”. Lấy cảm hứng từ lý thuyết triển vọng (prospect theory) của Kahneman và Tversky.
- ORPO (Odds Ratio Preference Optimization, Hong và cộng sự, 2024). Không cần mô hình tham chiếu, gộp SFT và bước ưa thích thành một giai đoạn: loss SFT cộng một số hạng dựa trên tỉ số odds (odds ratio) giữa câu thắng và câu thua.
Giới hạn
Phần tiêu đề “Giới hạn”- Dữ liệu cố định. Câu trong dữ liệu không do sinh ra thì có lệch phân phối. Mô hình cũng không tự thử câu mới như PPO.
- Overfit khi ưa thích gần như tất định, như kết quả của Azar và cộng sự ở trên.
- Tối ưu quá mức chưa biến mất. Tác giả DPO để ngỏ câu hỏi reward over-optimization biểu hiện thế nào ở DPO (mục 7). Không có RM riêng không có nghĩa là hết vấn đề.
- Thiên lệch độ dài. Log-prob của cả câu là tổng theo token, nên phụ thuộc độ dài câu. TRL có biến thể
loss_type="sigmoid_norm"(theo paper SimPO của Meng và cộng sự, 2024) chuẩn hóa theo số token.
Minh họa
Phần tiêu đề “Minh họa”RLHF có ba bước: SFT, học RM, tối ưu chính sách bằng RL. Đặt DPO cạnh nó: DPO giữ bước 1 (mô hình SFT làm ) và giữ dữ liệu ưa thích, chỉ gộp bước 2 và bước 3 thành một bước tối ưu loss.
| Tiêu chí | RLHF với PPO | DPO |
|---|---|---|
| Bước SFT | Có, làm và điểm khởi tạo | Có, làm và điểm khởi tạo |
| Dữ liệu ưa thích dùng để | Huấn luyện RM | Tính loss trực tiếp |
| Hàm điểm | Mạng riêng , đóng băng khi chạy RL | Phần thưởng ẩn |
| Sinh câu trả lời khi huấn luyện | Có, mỗi vòng | Không (offline) |
| Mô hình phải chạy | Chính sách, tham chiếu, RM, hàm giá trị | Chính sách, tham chiếu (tính trước được) |
| Mục tiêu | Cùng mục tiêu, qua nghiệm dạng đóng | |
| Cách tối ưu | PPO, có kẹp | , như bài phân loại |
Ví dụ code
Phần tiêu đề “Ví dụ code”Code trong bài chạy thử lần cuối tháng 10/2026, với Python 3.14 và NumPy 2.4 (bản có trong Pyodide). Bài có một ô, chạy ngay trên trang. Không có ô nào cần chạy trên máy bạn.
Loss DPO trên bốn cặp
Phần tiêu đề “Loss DPO trên bốn cặp”Ô dùng log-prob của cả câu, giả lập cho bốn cặp. Thay vì nhìn số tuyệt đối, hãy nhìn mỗi câu đã dịch chuyển bao nhiêu so với tham chiếu:
| Cặp | Câu được chọn | Câu bị loại | |
|---|---|---|---|
| 1 | 5 | ||
| 2 | |||
| 3 | 3 | ||
| 4 | 2 |
Cặp 1 đã dịch đúng hướng rõ rệt. Cặp 2 dịch sai hướng. Cặp 3 hạ cả hai câu, nhưng câu bị loại hạ nhiều hơn. Ô in phần thưởng ẩn, loss và trọng số gradient ở , rồi loss trung bình ở bốn giá trị , cuối cùng là trường hợp chính sách trùng tham chiếu.
Loss DPO theo β
# Hàm mất mát DPO trên bốn cặp ưa thích giả lậpimport numpy as np
# ln pi(y | x) của cả câu = tổng log-prob các token (số giả lập).# w: câu được chọn, l: câu bị loại. Mỗi vị trí là một cặp.# Chính sách đang học:logp_w = np.array([-38.0, -55.0, -47.0, -60.0])logp_l = np.array([-44.0, -50.0, -48.0, -58.0])# Mô hình tham chiếu (SFT):ref_logp_w = np.array([-40.0, -52.0, -46.0, -61.0])ref_logp_l = np.array([-41.0, -49.0, -44.0, -57.0])
def sigmoid(z): return 1 / (1 + np.exp(-z))
def dpo(beta, logp_w, logp_l, ref_logp_w, ref_logp_l): r_w = beta * (logp_w - ref_logp_w) # phần thưởng ẩn của câu được chọn r_l = beta * (logp_l - ref_logp_l) # phần thưởng ẩn của câu bị loại margin = r_w - r_l # lề, bằng beta * d loss = np.logaddexp(0, -margin) # = -ln sigmoid(lề), tính ổn định weight = sigmoid(-margin) # trọng số của cặp trong gradient return r_w, r_l, loss, weight
d = (logp_w - ref_logp_w) - (logp_l - ref_logp_l)print("d =", d)pairs = (logp_w, logp_l, ref_logp_w, ref_logp_l)r_w, r_l, loss, weight = dpo(0.1, *pairs) # beta = 0.1print("reward chosen ", np.round(r_w, 2))print("reward rejected", np.round(r_l, 2))print("loss ", np.round(loss, 4))print("weight ", np.round(weight, 4))print("accuracy", np.mean(r_w > r_l))
for beta in (0.01, 0.1, 0.5, 1.0): loss = dpo(beta, *pairs)[2] print(f"beta {beta:<4}: mean loss {loss.mean():.4f}", np.round(loss, 3))
# Chính sách trùng tham chiếu: d = 0, loss = ln 2 với mọi betaloss = dpo(0.5, ref_logp_w, ref_logp_l, ref_logp_w, ref_logp_l)[2]print("policy = ref:", np.round(loss, 4), f"ln 2 = {np.log(2):.4f}")The first run downloads Python, about 10 MB.
Đọc kết quả:
d = [ 5. -2. 3. 2.]: khớp bảng trên.reward chosenvàreward rejected: phần thưởng ẩn ở , bằng nhân độ dịch chuyển.accuracy 0.75: 3 trên 4 cặp có lề dương. TRL ghi đúng chỉ số này với tênrewards/accuracies. Để ý cặp 3: phần thưởng của câu được chọn là , tức chính sách đã hạ xác suất câu đó, mà cặp vẫn được tính là đúng.loss: . Cặp 2 xếp sai nên loss lớn nhất, trên .weight, tức : . Cặp 2 có trọng số lớn nhất, cặp 1 nhỏ nhất, đúng như mục Gradient. Đạo hàm đầy đủ của loss theo còn nhân thêm : với cặp 2 là .- Bảng theo : loss trung bình , , rồi tăng lại . Nhìn từng cột: khi đi từ tới , loss của cặp 1 giảm từ xuống , còn của cặp 2 tăng từ lên . Ở , riêng cặp 2 đã chiếm khoảng 92% tổng loss. khuếch đại cả cặp đúng lẫn cặp sai.
policy = ref: cả bốn cặp có loss , vì .
Bạn có thể thử: sửa logp_w của cặp 2 để , rồi chạy lại. Khi mọi cặp đều có , loss trung bình giảm đều khi tăng.
Trên mô hình thật, bạn không tự viết vòng lặp này. Thư viện TRL của Hugging Face có DPOTrainer (bản 1.16.0). Dữ liệu có ba cột prompt, chosen, rejected. loss_type mặc định là "sigmoid", chính là loss ở trên, và beta mặc định là 0.1. Bỏ trống ref_model thì nó dùng mô hình ở trạng thái trước khi huấn luyện làm tham chiếu. Bài không có ô chạy cho phần này vì cần mô hình thật và GPU.
Lỗi hay gặp
Phần tiêu đề “Lỗi hay gặp”Kiểm tra nhanh
Phần tiêu đề “Kiểm tra nhanh”Tóm tắt và đọc thêm
Phần tiêu đề “Tóm tắt và đọc thêm”Ý chính:
- Mục tiêu có phạt KL của RLHF có nghiệm dạng đóng . Đảo lại, mỗi chính sách cho một hàm điểm .
- Bradley–Terry chỉ dùng hiệu điểm của hai câu cùng prompt, nên triệt tiêu. Thay bằng được loss DPO : hồi quy logistic trên lề của phần thưởng ẩn .
- Gradient tăng câu thắng, giảm câu thua, với trọng số lớn nhất ở cặp đang xếp sai. lớn giữ chính sách gần , và khuếch đại cả cặp đúng lẫn cặp sai trong loss.
- So với RLHF: không RM riêng, không vòng RL, không sinh câu khi huấn luyện. Vẫn cần mô hình tham chiếu, vẫn có thể overfit, và loss thấp chưa phải chất lượng thật.
Đọc thêm:
- Rafailov, R., Sharma, A., Mitchell, E., Ermon, S., Manning, C. D., Finn, C. (2023). Direct Preference Optimization: Your Language Model is Secretly a Reward Model, arXiv:2305.18290, mục 4 và phụ lục A.1, A.2. Paper gốc, phần dẫn xuất bài này đi theo. Paper dùng cho chính sách và cho log tự nhiên.
- Azar, M. G., Rowland, M., Piot, B., Guo, D., Calandriello, D., Valko, M., Munos, R. (2023). A General Theoretical Paradigm to Understand Learning from Human Preferences, arXiv:2310.12036, mục 4 và 5. Vì sao DPO có thể overfit, và IPO.
- Ethayarajh, K., Xu, W., Muennighoff, N., Jurafsky, D., Kiela, D. (2024). KTO: Model Alignment as Prospect Theoretic Optimization, arXiv:2402.01306, ICML 2024. Học từ nhãn tốt/xấu, không cần cặp.
- Hong, J., Lee, N., Thorne, J. (2024). ORPO: Monolithic Preference Optimization without Reference Model, arXiv:2403.07691. Gộp SFT và bước ưa thích, không cần mô hình tham chiếu.
- Meng, Y., Xia, M., Chen, D. (2024). SimPO: Simple Preference Optimization with a Reference-Free Reward, arXiv:2405.14734. Chuẩn hóa log-prob theo độ dài câu, cũng không cần mô hình tham chiếu.
- Hugging Face. DPO Trainer (TRL 1.16.0), tài liệu chính thức, đọc tháng 10/2026.
DPOTrainer, danh sáchloss_type, và các chỉ sốrewards/*để theo dõi khi huấn luyện.
Your browser is blocking storage. Your progress will be lost when you reload the page.
Content licensed under CC BY-NC-SA 4.0. Code under the MIT license.About