Pytorch
TensorFlow

#
Đằng sau pipeline

Đây là phần đầu tiên có nội dung hơi khác một chút tùy thuộc vào việc bạn sử dụng PyTorch hay TensorFlow. Chuyển đổi công tắc trên đầu tiêu đề để chọn nền tảng bạn thích!

Hãy bắt đầu với một ví dụ hoàn chỉnh, cùng xem những gì xảy ra phía sau khi chúng tôi thực thi đoạn mã sau trong Chương 1:

    from transformers import pipeline

    classifier = pipeline("sentiment-analysis")
    classifier(
        [
            "I've been waiting for a HuggingFace course my whole life.",
            "I hate this so much!",
        ]
    )

và thu được:

    [{'label': 'POSITIVE', 'score': 0.9598047137260437},
     {'label': 'NEGATIVE', 'score': 0.9994558095932007}]

Như chúng ta đã thấy trong Chương 1, pipeline này nhóm ba bước lại với nhau: tiền xử lý, đưa các đầu vào qua mô hình và hậu xử lý:

Hãy cùng đi qua từng phần này.

##
Tiền xử lý với một tokenizer

Giống như các mạng nơ-ron khác, các mô hình Transformers không thể xử lý trực tiếp văn bản thô, vì vậy bước đầu tiên trong quy trình của chúng ta là chuyển các đầu vào văn bản thành dạng số mà mô hình có thể hiểu được. Để làm điều này, chúng ta sử dụng tokenizer, hàm sẽ chịu trách nhiệm về:

- Tách đầu vào thành các từ, từ phụ, hoặc ký hiệu (như dấu chấm câu) được gọi là tokens
- Ánh xạ mỗi token thành một số nguyên
- Thêm đầu vào bổ sung có thể hữu ích cho mô hình

Tất cả quá trình tiền xử lý này cần được thực hiện giống hệt như khi mô hình được huấn luyện trước, vì vậy trước tiên chúng ta cần tải xuống thông tin đó từ Model Hub. Để làm điều này, chúng tôi sử dụng lớp `AutoTokenizer` và phương thức `from_pretrained()` của nó. Sử dụng tên checkpoint mô hình của chúng ta, nó sẽ tự động tìm nạp dữ liệu được liên kết với tokenizer của mô hình và lưu vào bộ nhớ cache (vì vậy nó chỉ được tải xuống lần đầu tiên bạn chạy mã bên dưới).

Vì checkpoint mặc định của `sentiment-analysis` là `distilbert-base-unsased-finetuned-sst-2-english` (bạn có thể xem thẻ mô hình của nó tại đây), chúng ta chạy như sau:

    from transformers import AutoTokenizer

    checkpoint = "distilbert-base-uncased-finetuned-sst-2-english"
    tokenizer = AutoTokenizer.from_pretrained(checkpoint)

Khi có tokenizer rồi, chúng ta có thể truyền trực tiếp các câu của mình vào bên trong và nhận lại một từ điển đã sẵn sàng để cung cấp cho mô hình! Việc duy nhất cần làm là chuyển đổi danh sách các ID đầu vào thành các tensor.

Bạn có thể sử dụng 🤗 Transformers mà không phải lo lắng về khung ML nào được sử dụng phía dưới; nó có thể là PyTorch hoặc TensorFlow hoặc Flax đối với một số mô hình. Tuy nhiên, các mô hình Transformer chỉ chấp nhận tensor làm đầu vào. Nếu đây là lần đầu tiên bạn nghe về tensor, bạn có thể nghĩ chúng như là mảng NumPy. Mảng NumPy có thể là giá trị vô hướng (0D), vectơ (1D), ma trận (2D) hoặc có nhiều kích thước hơn. Nó thực sự là một tensor; Các tensor của các khung ML khác hoạt động tương tự và thường khởi tạo đơn giản như các mảng NumPy.

Để chỉ định loại tensors mà chúng ta muốn trả về (PyTorch, TensorFlow hoặc thuần NumPy), ta sử dụng tham số `return_tensors`:

    raw_inputs = [
        "I've been waiting for a HuggingFace course my whole life.",
        "I hate this so much!",
    ]
    inputs = tokenizer(raw_inputs, padding=True, truncation=True, return_tensors="pt")
    print(inputs)

Đừng lo lắng về padding (đệm) và truncation (cắt bớt) vội; chúng tôi sẽ giải thích những điều đó sau. Những điều chính cần nhớ ở đây là bạn có thể chuyển một câu hoặc một danh sách các câu, cũng như chỉ định loại tensors bạn muốn lấy lại (nếu không có loại nào được truyền vào, mặc định bạn sẽ nhận được kết quả trả về là một danh sách).

Đây là kết quả tương ứng tensor PyTorch:

    {
        'input_ids': tensor([
            [  101,  1045,  1005,  2310,  2042,  3403,  2005,  1037, 17662, 12172, 2607,  2026,  2878,  2166,  1012,   102],
            [  101,  1045,  5223,  2023,  2061,  2172,   999,   102,     0,     0,     0,     0,     0,     0,     0,     0]
        ]),
        'attention_mask': tensor([
            [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
            [1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0]
        ])
    }

Bản thân kết quả đầu ra là một từ điển có chứa hai khóa, `input_ids` và `attention_mask`. `input_ids` chứa hai hàng số nguyên (một cho mỗi câu) là số nhận dạng duy nhất của token trong mỗi câu. Chúng tôi sẽ giải thích `attention_mask` là gì ở phần sau của chương này.

##
Đi qua mô hình

Chúng ta có thể tải xuống mô hình được huấn luyện trước của mình giống như cách đã làm với tokenizer. 🤗 Transformers cung cấp một lớp `AutoModel` cũng có phương thức `from_pretrained()`:

    from transformers import AutoModel

    checkpoint = "distilbert-base-uncased-finetuned-sst-2-english"
    model = AutoModel.from_pretrained(checkpoint)

Trong đoạn mã này, chúng ta đã tải xuống cùng một checkpoint đã sử dụng trong pipeline của mình trước đây (nó được lưu vào bộ nhớ đệm rồi) và khởi tạo một mô hình với nó.

Kiến trúc này chỉ chứa mô-đun Transformer cơ sở: với một số đầu vào, nó xuất ra cái mà chúng ta sẽ gọi là hidden states (trạng thái ẩn), còn được gọi là đặc trưng. Đối với mỗi đầu vào mô hình, chúng ta sẽ truy xuất một vectơ đa chiều đại diện cho sự hiểu theo ngữ cảnh của đầu vào đó bằng mô hình Transformer.

Nếu điều này không hợp lý, đừng lo lắng về nó. Chúng tôi sẽ giải thích tất cả sau.

Mặc dù những trạng thái ẩn này có thể tự hữu ích, nhưng chúng thường là đầu vào cho một phần khác của mô hình, được gọi là head (đầu). Trong Chapter 1, các tác vụ khác nhau có thể được thực hiện với cùng một kiến trúc, nhưng mỗi tác vụ này sẽ có một phần đầu khác nhau được liên kết với nó.

###
Một vectơ đa chiều

Đầu ra vectơ của mô-đun Transformer thường lớn với ba chiều:

- Kích thước batch (lô): Số chuỗi được xử lý tại một thời điểm (trong ví dụ của chúng tôi là 2).
- Độ dài chuỗi: Độ dài biểu diễn số của chuỗi (trong ví dụ của chúng tôi là 16).
- Kích thước ẩn: Kích thước vectơ của mỗi đầu vào mô hình.

Nó được cho là “có số chiều cao” vì giá trị cuối cùng. Kích thước ẩn có thể rất lớn (768 là giá trị phổ biến cho các mô hình nhỏ hơn và trong các mô hình lớn hơn, con số này có thể đạt tới 3072 hoặc hơn).

Có thể thấy điều này nếu chúng ta cung cấp các đầu vào đã xử lý trước cho mô hình của mình:

    outputs = model(**inputs)
    print(outputs.last_hidden_state.shape)

    torch.Size([2, 16, 768])

Lưu ý rằng đầu ra của các mô hình 🤗 Transformers hoạt động giống như các `namedtuple` hoặc từ điển. Bạn có thể truy cập các phần tử theo thuộc tính (như chúng ta đã làm) hoặc theo khóa (`outputs["last_hidden_state"]`), hoặc thậm chí theo chỉ mục nếu bạn biết chính xác nơi bạn đang tìm kiếm (`outputs[0]`).

###
Đầu mô hình: Hợp lý tời từng con số

Các đầu mô hình lấy vector đa chiều của các trạng thái ẩn làm đầu vào và chiếu chúng lên một chiều khác. Chúng thường bao gồm một hoặc một vài lớp tuyến tính:

Đầu ra của mô hình Transformer được gửi trực tiếp đến đầu mô hình để được xử lý.

Trong biểu đồ này, mô hình được biểu diễn bằng lớp nhúng của nó và các lớp tiếp theo. Lớp nhúng chuyển đổi mỗi ID trong đầu vào được mã hóa thành một vectơ đại diện cho token được liên kết. Các lớp tiếp theo thao tác các vectơ đó bằng cách sử dụng cơ chế chú ý để tạo ra biểu diễn cuối cùng của các câu.

Có nhiều kiến trúc khác nhau có sẵn trong 🤗 Transformers, với mỗi kiến trúc được thiết kế xoay quanh một tác vụ cụ thể. Đây là danh sách không đầy đủ:

- `*Model` (truy xuất các trạng thái ẩn)
- `*ForCausalLM`
- `*ForMaskedLM`
- `*ForMultipleChoice`
- `*ForQuestionAnswering`
- `*ForSequenceClassification`
- `*ForTokenClassification`
- and others 🤗

Với ví dụ của mình, chúng ta sẽ cần một mô hình có đầu phân loại tuần tự (để có thể phân loại các câu là khẳng định hoặc phủ định). Vì vậy, ta sẽ không sử dụng lớp `AutoModel` mà là `AutoModelForSequenceClassification`:

    from transformers import AutoModelForSequenceClassification

    checkpoint = "distilbert-base-uncased-finetuned-sst-2-english"
    model = AutoModelForSequenceClassification.from_pretrained(checkpoint)
    outputs = model(**inputs)

Giờ thì nếu chúng ta nhìn vào hình dạng các đầu vào của mình, kích thước sẽ thấp hơn nhiều: đầu mô hình lấy các vectơ đa chiều mà chúng ta đã thấy trước đây và xuất ra các vectơ có chứa hai giá trị (mỗi giá trị tương ứng một nhãn):

    print(outputs.logits.shape)

    torch.Size([2, 2])

Vì chúng ta chỉ có hai câu và hai nhãn, kết quả nhận được từ mô hình của chúng ta là dạng 2 x 2.

##
Hậu xử lý đầu ra

Các giá trị chúng ta nhận được dưới dạng đầu ra từ mô hình không nhất thiết phải tự có nghĩa. Hãy cùng xem:

    print(outputs.logits)

    tensor([[-1.5607,  1.6123],
            [ 4.1692, -3.3464]], grad_fn=<AddmmBackward>)

Mô hình đã dự đoán `[-1.5607, 1.6123]` cho câu đầu tiên và `[4.1692, -3.3464]` cho câu thứ hai. Đó không phải là xác suất mà là logits, điểm số thô, chưa chuẩn hóa được xuất ra bởi lớp cuối cùng của mô hình. Để được chuyển đổi thành xác suất, chúng cần phải trải qua lớp SoftMax (tất cả các mô hình 🤗 Transformers đều xuất ra logits, vì hàm mất mát cho việc huấn luyện thường sẽ kết hợp hàm kích hoạt cuối cùng, chẳng hạn như SoftMax, với hàm mất mát thực tế, chẳng hạn như entropy chéo):

    import torch

    predictions = torch.nn.functional.softmax(outputs.logits, dim=-1)
    print(predictions)

    tensor([[4.0195e-02, 9.5980e-01],
            [9.9946e-01, 5.4418e-04]], grad_fn=<SoftmaxBackward>)

Bây giờ chúng ta có thể thấy rằng mô hình đã dự đoán `[0.0402, 0.9598]` cho câu đầu tiên và `[0.9995, 0.0005]` cho câu thứ hai. Đây là những điểm xác suất dễ nhận biết.

Để lấy các nhãn tương ứng với từng vị trí, chúng ta có thể kiểm tra thuộc tính `id2label` của cấu hình mô hình (tìm hiểu thêm về điều này trong phần tiếp theo):

    model.config.id2label

    {0: 'NEGATIVE', 1: 'POSITIVE'}

Bây giờ chúng ta có thể kết luận rằng mô hình đã dự đoán như sau:

- Câu đầu tiên: TIÊU CỰC: 0,0402, TÍCH CỰC: 0,9598
- Câu thứ hai: TIÊU CỰC: 0,9995, TÍCH CỰC: 0,0005

Chúng tôi đã tái tạo thành công ba bước của quy trình: tiền xử lý bằng tokenizers, đưa đầu vào qua mô hình và hậu xử lý! Giờ thì chúng ta hãy dành một chút thời gian để đi sâu hơn vào từng bước đó.

✏️ Thử nghiệm thôi! Chọn hai (hoặc nhiều) văn bản của riêng bạn và chạy chúng thông qua `sentiment-analysis`. Sau đó, tự mình lặp lại các bước bạn đã thấy ở đây và kiểm tra xem bạn có thu được kết quả tương tự không!