Giới thiệu các giải pháp phân loại an toàn linh hoạt với Gemma

1. Tổng quan

Lớp học lập trình này minh hoạ cách tạo một thuật toán phân loại văn bản tuỳ chỉnh bằng tính năng tinh chỉnh hiệu quả tham số (PET). Thay vì tinh chỉnh toàn bộ mô hình, các phương thức PET chỉ cập nhật một số lượng nhỏ các tham số, giúp việc huấn luyện tương đối dễ dàng và nhanh chóng. Điều này cũng giúp một mô hình dễ dàng tìm hiểu các hành vi mới mà không cần tương đối ít dữ liệu huấn luyện. Phương pháp này được mô tả chi tiết trong bài viết Hướng đến các công cụ phân loại văn bản linh hoạt cho mọi người, trong đó cho thấy cách những kỹ thuật này có thể được áp dụng cho nhiều tác vụ an toàn và đạt được hiệu suất tối ưu chỉ với vài trăm ví dụ huấn luyện.

Lớp học lập trình này sử dụng phương thức LoRA PET và mô hình Gemma nhỏ hơn (gemma_instruct_2b_en) vì phương thức này có thể chạy nhanh hơn và hiệu quả hơn. Colab này trình bày các bước nhập dữ liệu, định dạng dữ liệu cho LLM, huấn luyện các trọng số LoRA, sau đó đánh giá kết quả. Lớp học lập trình này huấn luyện về tập dữ liệu ETHOS, một tập dữ liệu được cung cấp công khai để phát hiện lời nói hận thù. Tập dữ liệu này được tạo từ các bình luận trên YouTube và Reddit. Khi chỉ được huấn luyện dựa trên 200 ví dụ (1/4 tập dữ liệu), tập dữ liệu này đạt được F1: 0,80 và ROC-AUC: 0,78, cao hơn một chút so với SOTA hiện được báo cáo trên bảng xếp hạng (tại thời điểm viết bài là ngày 15 tháng 2 năm 2024). Khi được huấn luyện trên đầy đủ 800 ví dụ, giống như nó đạt được điểm F1 là 83,74 và điểm ROC-AUC là 88,17. Các mô hình lớn hơn như gemma_instruct_7b_en thường sẽ hoạt động hiệu quả hơn, nhưng chi phí huấn luyện và thực thi cũng lớn hơn.

Cảnh báo kích hoạt: vì lớp học lập trình này phát triển một thuật toán phân loại sự an toàn để phát hiện lời nói hận thù, nên các ví dụ và quy trình đánh giá kết quả có chứa một số ngôn từ thô tục.

2. Cài đặt và thiết lập

Đối với lớp học lập trình này, bạn cần có phiên bản keras (3), keras-nlp (0.8.0) gần đây và tài khoản Kaggle để tải mô hình cơ sở xuống.

!pip install -q -U keras-nlp
!pip install -q -U keras

Để đăng nhập vào Kaggle, bạn có thể lưu trữ tệp thông tin xác thực kaggle.json của mình tại ~/.kaggle/kaggle.json hoặc chạy lệnh sau trong môi trường Colab:

import kagglehub

kagglehub.login()

3. Tải tập dữ liệu ETHOS

Trong phần này, bạn sẽ tải tập dữ liệu để huấn luyện thuật toán phân loại của chúng tôi và xử lý trước tập dữ liệu đó thành một chuyến tàu và tập hợp kiểm thử. Bạn sẽ sử dụng ETHOS của bộ dữ liệu nghiên cứu phổ biến đã được thu thập để phát hiện lời nói hận thù trên mạng xã hội. Bạn có thể xem thêm thông tin về cách thu thập tập dữ liệu trong bài viết ETHOS: Tập dữ liệu phát hiện lời nói hận thù trên mạng.

import pandas as pd

gh_root = 'https://raw.githubusercontent.com'
gh_repo = 'intelligence-csd-auth-gr/Ethos-Hate-Speech-Dataset'
gh_path = 'master/ethos/ethos_data/Ethos_Dataset_Binary.csv'
data_url = f'{gh_root}/{gh_repo}/{gh_path}'

df = pd.read_csv(data_url, delimiter=';')
df['hateful'] = (df['isHate'] >= df['isHate'].median()).astype(int)

# Shuffle the dataset.
df = df.sample(frac=1, random_state=32)

# Split into train and test.
df_train, df_test = df[:800],  df[800:]

# Display a sample of the data.
df.head(5)[['hateful', 'comment']]

Bạn sẽ thấy nội dung tương tự như sau:

nhãn

bình luận

0

0

You said he but still not convinced this is a ...

1

0

well, looks like its time to have another child.

2

0

What if we send every men to mars to start a n...

3

1

It doesn't matter if you're black or white, ...

4

0

Who ever disliked this video should be ashamed...

4. Tải xuống và tạo thực thể cho Mô hình

Như đã mô tả trong tài liệu, bạn có thể dễ dàng sử dụng mô hình Gemma theo nhiều cách. Với Keras, đây là những việc bạn cần làm:

import keras
import keras_nlp

# For reproducibility purposes.
keras.utils.set_random_seed(1234)

# Download the model from Kaggle using Keras.
model = keras_nlp.models.GemmaCausalLM.from_preset('gemma_instruct_2b_en')

# Set the sequence length to a small enough value to fit in memory in Colab.
model.preprocessor.sequence_length = 128

Bạn có thể kiểm tra xem mô hình có đang hoạt động không bằng cách tạo một số văn bản:

model.generate('Question: what is the capital of France? ', max_length=32)

5. Xử lý trước văn bản và mã thông báo dòng phân cách

Để giúp mô hình hiểu rõ hơn ý định của chúng ta, bạn có thể xử lý trước văn bản và sử dụng mã thông báo của dòng phân cách. Điều này giúp mô hình ít có khả năng tạo văn bản không phù hợp với định dạng dự kiến. Ví dụ: bạn có thể thử yêu cầu phân loại ý kiến từ mô hình bằng cách viết một câu lệnh như sau:

Classify the following text into one of the following classes:[Positive,Negative]

Text: you look very nice today
Classification:

Trong trường hợp này, mô hình có thể đưa ra hoặc không đưa ra nội dung bạn đang tìm kiếm. Ví dụ: nếu văn bản chứa các ký tự dòng mới, có khả năng ảnh hưởng tiêu cực đến hiệu suất của mô hình. Một phương pháp hiệu quả hơn là sử dụng mã thông báo dòng phân cách. Sau đó, lời nhắc sẽ trở thành:

Classify the following text into one of the following classes:[Positive,Negative]
<separator>
Text: you look very nice today
<separator>
Prediction:

Bạn có thể tóm tắt lớp này bằng cách sử dụng một hàm xử lý trước văn bản:

def preprocess_text(
    text: str,
    labels: list[str],
    instructions: str,
    separator: str,
) -> str:
  prompt = f'{instructions}:[{",".join(labels)}]'
  return separator.join([prompt, f'Text:{text}', 'Prediction:'])

Bây giờ, nếu chạy hàm này bằng cùng một câu lệnh và văn bản như trước đây, bạn sẽ nhận được kết quả tương tự:

text = 'you look very nice today'

prompt = preprocess_text(
    text=text,
    labels=['Positive', 'Negative'],
    instructions='Classify the following text into one of the following classes',
    separator='\n<separator>\n',
)

print(prompt)

Kết quả nào sẽ xuất ra:

Classify the following text into one of the following classes:[Positive,Negative]
<separator>
Text:well, looks like its time to have another child
<separator>
Prediction:

6. Xử lý hậu kỳ đầu ra

Kết quả của mô hình là các mã thông báo với các xác suất khác nhau. Thông thường, để tạo văn bản, bạn sẽ chọn trong số ít mã thông báo có khả năng xuất hiện nhất và xây dựng câu, đoạn văn hoặc thậm chí tài liệu đầy đủ. Tuy nhiên, đối với mục đích phân loại, điều thực sự quan trọng là mô hình có cho rằng Positive có nhiều khả năng hơn Negative hoặc ngược lại hay không.

Với mô hình mà bạn đã tạo thực thể trước đó, dưới đây là cách bạn có thể xử lý kết quả của mô hình đó thành các xác suất độc lập về việc mã thông báo tiếp theo là Positive hay Negative:

import numpy as np


def compute_output_probability(
    model: keras_nlp.models.GemmaCausalLM,
    prompt: str,
    target_classes: list[str],
) -> dict[str, float]:
  # Shorthands.
  preprocessor = model.preprocessor
  tokenizer = preprocessor.tokenizer

  # NOTE: If a token is not found, it will be considered same as "<unk>".
  token_unk = tokenizer.token_to_id('<unk>')

  # Identify the token indices, which is the same as the ID for this tokenizer.
  token_ids = [tokenizer.token_to_id(word) for word in target_classes]

  # Throw an error if one of the classes maps to a token outside the vocabulary.
  if any(token_id == token_unk for token_id in token_ids):
    raise ValueError('One of the target classes is not in the vocabulary.')

  # Preprocess the prompt in a single batch. This is done one sample at a time
  # for illustration purposes, but it would be more efficient to batch prompts.
  preprocessed = model.preprocessor.generate_preprocess([prompt])

  # Identify output token offset.
  padding_mask = preprocessed["padding_mask"]
  token_offset = keras.ops.sum(padding_mask) - 1

  # Score outputs, extract only the next token's logits.
  vocab_logits = model.score(
      token_ids=preprocessed["token_ids"],
      padding_mask=padding_mask,
  )[0][token_offset]

  # Compute the relative probability of each of the requested tokens.
  token_logits = [vocab_logits[ix] for ix in token_ids]
  logits_tensor = keras.ops.convert_to_tensor(token_logits)
  probabilities = keras.activations.softmax(logits_tensor)

  return dict(zip(target_classes, probabilities.numpy()))

Bạn có thể kiểm tra hàm đó bằng cách chạy hàm với lời nhắc mà bạn tạo trước đó:

compute_output_probability(
    model=model,
    prompt=prompt,
    target_classes=['Positive', 'Negative'],
)

Kết quả sẽ tương tự như sau:

{'Positive': 0.99994016, 'Negative': 5.984089e-05}

7. Gói tất cả dưới dạng Trình phân loại

Để dễ sử dụng, bạn có thể gói tất cả các hàm vừa tạo vào một thuật toán phân loại giống như sklearn với các hàm dễ sử dụng và quen thuộc như predict()predict_score().

import dataclasses


@dataclasses.dataclass(frozen=True)
class AgileClassifier:
  """Agile classifier to be wrapped around a LLM."""

  # The classes whose probability will be predicted.
  labels: tuple[str, ...]

  # Provide default instructions and control tokens, can be overridden by user.
  instructions: str = 'Classify the following text into one of the following classes'
  separator_token: str = '<separator>'
  end_of_text_token: str = '<eos>'

  def encode_for_prediction(self, x_text: str) -> str:
    return preprocess_text(
        text=x_text,
        labels=self.labels,
        instructions=self.instructions,
        separator=self.separator_token,
    )

  def encode_for_training(self, x_text: str, y: int) -> str:
    return ''.join([
        self.encode_for_prediction(x_text),
        self.labels[y],
        self.end_of_text_token,
    ])

  def predict_score(
      self,
      model: keras_nlp.models.GemmaCausalLM,
      x_text: str,
  ) -> list[float]:
    prompt = self.encode_for_prediction(x_text)
    token_probabilities = compute_output_probability(
        model=model,
        prompt=prompt,
        target_classes=self.labels,
    )
    return [token_probabilities[token] for token in self.labels]

  def predict(
      self,
      model: keras_nlp.models.GemmaCausalLM,
      x_eval: str,
  ) -> int:
    return np.argmax(self.predict_score(model, x_eval))


agile_classifier = AgileClassifier(labels=('Positive', 'Negative'))

8. Tinh chỉnh mô hình

LoRA là viết tắt của Low- Ratings thưa (Thay đổi xếp hạng thấp). Đây là một kỹ thuật tinh chỉnh có thể được dùng để tinh chỉnh một cách hiệu quả các mô hình ngôn ngữ lớn. Bạn có thể đọc thêm về nội dung này trong bài viết LoRA: Điều chỉnh theo thứ hạng thấp của Mô hình ngôn ngữ lớn.

Việc triển khai Gemma Keras cung cấp phương thức enable_lora() mà bạn có thể dùng để tinh chỉnh:

# Enable LoRA for the model and set the LoRA rank to 4.
model.backbone.enable_lora(rank=4)

Sau khi bật LoRA, bạn có thể bắt đầu quá trình tinh chỉnh. Quá trình này mất khoảng 5 phút mỗi khoảng thời gian bắt đầu của hệ thống trên Colab:

import tensorflow as tf

# Create dataset with preprocessed text + labels.
map_fn = lambda xy: agile_classifier.encode_for_training(*xy)
x_train = list(map(map_fn, df_train[['comment', 'hateful']].values))
ds_train = tf.data.Dataset.from_tensor_slices(x_train).batch(2)

# Compile the model using the Adam optimizer and appropriate loss function.
model.compile(
    loss=keras.losses.SparseCategoricalCrossentropy(from_logits=True),
    optimizer=keras.optimizers.Adam(learning_rate=0.0005),
    weighted_metrics=[keras.metrics.SparseCategoricalAccuracy()],
)

# Begin training.
model.fit(ds_train, epochs=4)

Việc huấn luyện nhiều khoảng thời gian bắt đầu của hệ thống sẽ mang lại độ chính xác cao hơn cho đến khi xảy ra hiện tượng quá tải.

9. Kiểm tra kết quả

Giờ đây, bạn có thể kiểm tra kết quả của thuật toán phân loại linh hoạt mà mình vừa huấn luyện. Mã này sẽ cho ra điểm số dự đoán của lớp dựa trên một đoạn văn bản:

text = 'you look really nice today'
scores = agile_classifier.predict_score(model, text)
dict(zip(agile_classifier.labels, scores))
{'Positive': 0.99899644, 'Negative': 0.0010035498}

10. Đánh giá mô hình

Cuối cùng, bạn sẽ đánh giá hiệu suất của mô hình bằng hai chỉ số phổ biến là điểm F1AUC-ROC. Điểm F1 xác định được các lỗi âm tính giả và dương tính giả bằng cách đánh giá trung bình điều hoà của độ chính xác và thu hồi ở một ngưỡng phân loại nhất định. Mặt khác, AUC-ROC ghi lại sự đánh đổi giữa tỷ lệ dương tính thật và tỷ lệ dương tính giả trên một loạt các ngưỡng rồi tính phần diện tích dưới đường cong này.

from sklearn.metrics import f1_score, roc_auc_score

y_true = df_test['hateful'].values
# Compute the scores (aka probabilities) for each of the labels.
y_score = [agile_classifier.predict_score(model, x) for x in df_test['comment']]
# The label with highest score is considered the predicted class.
y_pred = np.argmax(y_score, axis=1)
# Extract the probability of a comment being considered hateful.
y_prob = [x[agile_classifier.labels.index('Negative')] for x in y_score]

# Compute F1 and AUC-ROC scores.
print(f'F1: {f1_score(y_true, y_pred):.2f}')
print(f'AUC-ROC: {roc_auc_score(y_true, y_prob):.2f}')
F1: 0.84
AUC-ROC: = 0.88

Một cách thú vị khác để đánh giá dự đoán của mô hình là ma trận nhầm lẫn. Ma trận nhầm lẫn sẽ mô tả trực quan các loại lỗi dự đoán khác nhau.

from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay

cm = confusion_matrix(y_true, y_pred)
ConfusionMatrixDisplay(
  confusion_matrix=cm,
  display_labels=agile_classifier.labels,
).plot()

ma trận nhầm lẫn

Cuối cùng, bạn cũng có thể nhìn vào đường cong ROC để biết các lỗi dự đoán có thể xảy ra khi sử dụng các ngưỡng tính điểm khác nhau.

from sklearn.metrics import RocCurveDisplay, roc_curve

fpr, tpr, _ = roc_curve(y_true, y_score, pos_label=1)
RocCurveDisplay(fpr=fpr, tpr=tpr).plot()

Đường cong ROC