Trời tính không bằng máy tính

blog, khám phá

Dễ hiểu hơn: Transformer là gì? GPT hoạt động thế nào?

Ở số trước của loạt bài “Dễ hiểu hơn”, chúng ta đã tìm hiểu về những kỹ thuật được sử dụng để xử lý ngôn ngữ tự nhiên trước khi thế giới phát minh ra Transformer. Trong bài viết này, chúng ta sẽ tiếp tục “theo dòng thời gian” tìm hiểu tới sự phát triển của ngành xử lý ngôn ngữ tự nhiên này, cũng như cách mà large language models như GPT hay LLaMA hoạt động.

Loạt bài “Dễ hiểu hơn”:

Transformer là gì?

Transformer là một cấu trúc language model (mô hình xử lý ngôn ngữ tự nhiên) được đưa ra từ báo cáo nghiên cứu mang tên “Attention is all you need”. Cấu trúc này xử lý nhiều vấn đề mà RNN gặp phải (như đã nói ở số trước).

Có nhiều model dựa trên cấu trúc transformer, nổi tiếng nhất thì có ELMo, BERT và gần đây nhất là họ hàng nhà GPT và LLaMA. Ở trong bài này, mình sẽ lấy GPT và LLaMA làm ví dụ, nên cấu trúc transformer được giới thiệu trong bài này sẽ là dạng decoder-only.

GPT là viết tắt của Generative Pre-trained Transformer, một khái niệm khó mà có thể dịch thẳng sang tiếng Việt được, vậy nên mình sẽ giải thích từng từ một:

  • Generative: tức model này có thể tạo ra nội dung mới, ví dụ nhập vào một câu hỏi thì nó có thể tạo ra (generate) câu trả lời
  • Pre-trained: model đã được training từ dữ liệu chưa có chọn lọc, hầu hết là để model hiểu được ngôn ngữ trước đã, chứ chưa quan tâm đến ngữ cảnh.
  • Transformer: (như đã giải thích phía trên)

Lưu ý: do bài viết hướng đến đa số người mới tìm hiểu, nên mình sẽ lược bỏ một vài chi tiết quá phức tạp để giải thích, ví dụ như encoder-decoder, softmax, masked attention,…

Next token prediction

Transformer có nhiều ứng dụng khác nhau, nhưng trong bài viết này ta sẽ tập trung tới next token prediction (dự đoán token tiếp theo). Để hiểu token là gì, bạn có thể đọc số trước của loạt bài này.

ChatGPT thực chất hoạt động theo nguyên lý next token prediction. Tức nếu bạn đưa ra một đoạn văn bản chưa hoàn chỉnh, ví dụ: “Hôm nay trời mưa, tôi không…”, thì GPT sẽ điền tiếp, ví dụ như: “…muốn đi học” chẳng hạn. Tất nhiên có nhiều cách điền khác như “không muốn đi làm”, “không muốn ra ngoài”,… nhưng quan trọng là phần điền tiếp phải ăn khớp với phần phía trước.

Để điền tiếp một cách chính xác, model cần phải hiểu được sự liên kết giữa các từ trong câu, cũng như quan trọng hơn là ngữ cảnh chung của văn bản.

Đối với ChatGPT, có thể hiểu nôm na rằng model được train để hiểu “sau câu hỏi là câu trả lời”. Vì vậy, khi bạn nhập vào một câu hỏi “bạn khỏe không?”, nó sẽ đưa ra câu trả lời “tôi khỏe, cảm ơn”. Nếu như bạn có đọc blog của OpenAI lúc mới có phiên bản InstructGPT (tiền thân của ChatGPT), bạn sẽ thấy là nếu không train điều này, model GPT-3 sẽ trả lời những thứ không có liên quan gì cả. Quá trình “dạy thêm” này được gọi là fine-tune, tức chỉ dạy thêm những thứ cụ thể (VD dạy thêm việc trả lời câu hỏi, chứ không dạy thêm kiếm thức).

Cách mà chúng ta đọc

“Một điều rất hay đối với ngôn ngữ tự nhiên, đó chính là không phải từ nào trong câu cũng quan trọng như nhau.”

Câu trên có thể rút ngắn lại thành:

“điều hay với ngôn ngữ tự nhiên, là không phải từ nào cũng quan trọng.”

Như vậy, vấn đề của chúng ta là làm thế nào để thiết kế ra một model có thể “đo đạc” được sự cần thiết của các từ trong một câu nói. Lý do cần đo đạc sự quan trọng của câu từ, không phải là để rút ngắn câu như trên, mà là để biết rằng model cần chú ý tới thông tin gì để tạo ra từ tiếp theo trong câu (nói cách khác, nếu bạn đặt một câu hỏi, thì model cần biết nên chú ý vào từ nào trong câu hỏi để tạo ra câu trả lời).

Để đạt được điều này, transformer sử dụng một kỹ thuật gọi là “self-attention”, tức là tìm sự quan trọng của một từ đối với các từ khác trong cùng một đoạn văn bản (vì là cùng một văn bản, nên có từ “self” trong “self-attention”)

Trong ví dụ trên, từ “đó” để chỉ “một điều” mà mình đã nói ở đầu câu, vì vậy khi đọc tới từ “đó”, thì hệ số của từ “điều” phải nhiều hơn so với các từ khác.

Việc đo đạc “mức độ quan trọng” này là phần cốt lõi, cũng là phần phức tạp nhất trong Transformer.

Query-Key-Value

Để tính toán ra “mức độ quan trọng” của một từ bất kỳ trong câu so với các từ còn lại, transformer sử dụng các ma trận để tính toán. Mình sẽ không đi vào chi tiết về toán (vì mình cũng không giỏi toán đến vậy hehe), nhưng tóm gọn lại thì chúng ta phải tính ra 3 ma trận phục vụ cho 3 mục đích khác nhau:

  • Ma trận Query có thể hiểu giống như việc bạn nhập một câu hỏi lên google
  • Ma trận Key có thể hiểu như tiêu đề trang web để google so sánh giữa câu hỏi của bạn và tiêu đề đó
  • Ma trận Value là nội dung thông tin thực sự để trả lời cho câu hỏi của bạn

Trong ví dụ dưới đây, mình sẽ giả sử là mình đã nhập vào 3 token “con”, “chào”, “bố” và muốn model tính ra token thứ 4 “mẹ”. Câu hoàn chỉnh ta mong muốn “con chào bố mẹ”

Khi bạn nhập một token vào model, điều xảy ra là:

  • Các ma trận Query-Key-Value (Q-K-V) sẽ được tính toán cho token đó
  • Chúng ta lấy Query của token hiện tại, nhân nó với Key của tất cả các token trước đó. Phép tính nhân ma trận này giúp ta tính toán xem token hiện tại có “dính dáng” tới token trước đó như thế nào.
  • Kết quả nhận được chính là hệ số “mức độ quan trọng”

Dưới đây, giả dụ ta đang ở token thứ 3 “bố”

  1. Ta tính Q-K-V của token thứ 3
  2. Sau đó nhân Q của token thứ 3 với K của token 1, 2 và 3

Ở ví dụ kể trên, sau khi nhân Q với K, ta tính được: rằng từ “bố” là quan trọng nhất (có hệ số 0.9), vì ta thường nói cụm từ “bố mẹ”.

Việc tiếp theo ta cần làm là nhân ma trận Value của từng token với hệ số “mức độ quan trọng” của nó, sau đó cộng tổng lại. Công đoạn này gọi là “weighted sum”. Kết quả cuối cùng ta nhận được gồm một ma trận duy nhất, chứa thông tin về nội dung về tất cả những thứ đã có trước đó:

Cuối cùng, ta chiếu (project) ma trận kết quả thành một vector, và sử dụng deep neural network (cụ thể hơn là feed-forward network) để biến ma trận kết quả thành token:

Chúng ta có thể lặp lại mọi công đoạn kể trên để “điền tiếp” từ tiếp theo sau từ “mẹ”, ví dụ “con chào bố mẹ ạ!”. Tất nhiên việc điền cái gì hoàn toàn phụ thuộc vào tệp dữ liệu mà người ta đưa vào lúc train cho model.

Câu hỏi còn lại là: làm thế nào để tính được các ma trận Query-Key-Value?

Thực tế, trong quá trình training, model sẽ học được cách tính toán các ma trận này. Có thể hiểu nôm na, việc tính ra ma trận này trực tiếp ảnh hưởng tới “sự thông minh” của model. Học càng nhiều thì IQ càng cao – giống như con người vậy.

Positional encoding

Tới đây, ta lại có thêm một vấn đề: do việc tính toán nhân và cộng ở trên mang tính giao hoán (a + b = b + a) nên từ góc độ của máy, việc nói “chó ăn cơm” và “ăn cơm chó” không khác gì nhau cả.

Vậy nên trước khi đưa token vào để tính Q-K-V, chúng ta cần “gộp” thông tin về vị trí vào chung với token. Có thể hiểu nôm na rằng sau khi token đã được biến thành vector (như mô tả ở số trước), thì vector này sẽ được cộng với vector chuyên để mô tả vị trí của token đó:

Trong ví dụ phía trên, ở tầng dưới cùng (output sau khi cộng), mình viết cả từ gốc ra để dễ hiểu. Thực tế, output này là một vector, sẵn sàng để được nạp vào phép tính Q-K-V.

Có nhiều phương pháp để tạo ra vector vị trí. Vào thời điểm viết bài này, các model mã nguồn mở như LLaMA, Mistral, Phi-2,… đều dùng Rotary Positional Embedding (RoPE). Do GPT-3 là mã nguồn đóng nên mình không có thông tin chính thức về phương pháp mà OpenAI sử dụng.

Áp dụng trong thực tế

Những thứ mình vừa giải thích ở trên, khi được áp dụng vào model thực tế thì sẽ được nhân lên về số lượng, ví dụ:

Model không chỉ tính một bộ Q-K-V, mà có nhiều bộ Q-K-V sẽ dùng được tính toán cùng một lúc. Lý do là để các “bộ” khác nhau có thể “sàng lọc” được các thông tin khác nhau từ một đoạn văn bản. Kỹ thuật này được gọi là multi-head attention. Ví dụ với LLaMA 2, có 32 bộ được sử dụng.

Không chỉ vậy, thực tế, người ta nối tiếp các bộ Q-K-V với nhau (đầu ra của bộ này là đầu vào của bộ tiếp theo). Mỗi lần “nối” như vậy, có thể coi như model có thêm một lớp (layer). Ví dụ dưới đây mình hỏi “The captital of Vietnamese is…” Các lớp đầu tiên thường sẽ đảm nhận nhiệm vụ “tách” thông tin về ngữ pháp, còn càng các lớp phía sau sẽ càng “tách” các thông tin trừu tượng hơn (như city, Vietnam,…). Ảnh sau có được là do mình sửa lại mã nguồn của llama.cpp:

Ngoài ra, các bạn có thể xem biểu diễn 3D ở đây để hiểu rõ hơn về cấu trúc của một model transformer thông dụng: https://bbycroft.net/llm

So với Recurrent neural network

Ở bài viết trước, mình có nói tới cấu trúc model hệ Recurrent neural network (RNN). Transformer tuy khó hiểu hơn, nhưng lại khắc phục các vấn đề lớn như:

  • Nhiều token có thể được nhập vào cùng một lúc, tăng hiệu quả của việc xử lý song song (VD bạn có nhiều GPU cùng chạy song song). Điều này có được là do việc tính các ma trận Q-K-V có thể được thực hiện song song với nhau, cũng như positional embedding của mỗi token có thể được tính một cách độc lập.
  • Tuy việc nhân ma trận rất tốn thời gian, nhưng kết quả nhân các ma trận K-V có thể được tái sử dụng, giảm thời gian tính toán. Nhờ có kỹ thuật K-V cache, thời gian tính toán ra token mới gần như không thay đổi, cho dù đó là token thứ 100, 1000 hay 10000 đi chăng nữa.
  • Trong khi RNN “thu gọn” ý nghĩa của câu nói thành một vector duy nhất, thì Transformer giữ tất cả nội dung đã có trong bộ nhớ. Bạn tốn thêm RAM, nhưng chắc chắn là không bị đánh mất thông tin nào cả.

Nguồn tham khảo

MỚI! Đăng ký nhận bài viết mới nhất qua chatbot: