การแสดงตัวแยกประเภทด้านความปลอดภัยแบบคล่องตัวด้วย Gemma

1. ภาพรวม

Codelab นี้จะแสดงวิธีสร้างตัวแยกประเภทข้อความที่กำหนดเองโดยใช้การปรับแต่งพารามิเตอร์ (PET) อย่างมีประสิทธิภาพ แทนที่จะปรับแต่งทั้งโมเดล เมธอด PET จะอัปเดตพารามิเตอร์เพียงไม่กี่รายการ ทำให้ฝึกได้ง่ายและรวดเร็ว นอกจากนี้ยังช่วยให้โมเดลเรียนรู้พฤติกรรมใหม่ๆ ได้ง่ายขึ้นด้วยข้อมูลการฝึกเพียงเล็กน้อย วิธีการนี้อธิบายไว้โดยละเอียดในต่อตัวแยกประเภทข้อความแบบ Agile สำหรับทุกคน ซึ่งจะแสดงวิธีนำเทคนิคเหล่านี้ไปใช้กับงานด้านความปลอดภัยต่างๆ และบรรลุผลงานศิลปะที่ล้ำสมัย โดยมีตัวอย่างการฝึกอบรมเพียง 2-300 ตัวอย่าง

Codelab นี้ใช้เมธอด PET LoRA และโมเดล Gemma ขนาดเล็ก (gemma_instruct_2b_en) เนื่องจากเรียกใช้ได้อย่างรวดเร็วและมีประสิทธิภาพมากกว่า Colab ประกอบด้วยขั้นตอนการส่งผ่านข้อมูล จัดรูปแบบข้อมูลสำหรับ LLM, การฝึกยกน้ำหนัก และประเมินผลลัพธ์ Codelab นี้จะฝึกชุดข้อมูล ETHOS ซึ่งเป็นชุดข้อมูลที่พร้อมใช้งานแบบสาธารณะสำหรับการตรวจหาวาจาสร้างความเกลียดชัง ซึ่งสร้างขึ้นจากความคิดเห็นของ YouTube และ Reddit เมื่อฝึกกับตัวอย่างเพียง 200 ตัวอย่าง (1/4 ของชุดข้อมูล) จะได้ค่า F1: 0.80 และ ROC-AUC: 0.78 ซึ่งสูงกว่า SOTA ที่รายงานอยู่ในปัจจุบันในลีดเดอร์บอร์ด (ณ เวลาที่เขียน วันที่ 15 ก.พ. 2024) เมื่อฝึกกับตัวอย่างทั้ง 800 คนแล้ว เช่น ได้คะแนน F1 ที่ 83.74 และได้คะแนน ROC-AUC อยู่ที่ 88.17 คะแนน โดยปกติโมเดลขนาดใหญ่ เช่น gemma_instruct_7b_en จะทำงานได้ดีกว่า แต่ต้นทุนการฝึกและการดำเนินการก็สูงกว่าด้วย

ทริกเกอร์คำเตือน: เนื่องจาก Codelab นี้พัฒนาตัวแยกประเภทเพื่อความปลอดภัยสำหรับตรวจจับวาจาสร้างความเกลียดชัง ตัวอย่าง และการประเมินผลลัพธ์มีภาษาที่ไม่เหมาะสม

2. การติดตั้งและการตั้งค่า

สำหรับ Codelab นี้ คุณจะต้องมีเวอร์ชันล่าสุด keras (3), keras-nlp (0.8.0) และบัญชี Kaggle เพื่อดาวน์โหลดโมเดลฐาน

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

หากต้องการเข้าสู่ระบบ Kaggle คุณจะจัดเก็บไฟล์ข้อมูลเข้าสู่ระบบ kaggle.json ไว้ที่ ~/.kaggle/kaggle.json หรือเรียกใช้สิ่งต่อไปนี้ในสภาพแวดล้อม Colab ก็ได้

import kagglehub

kagglehub.login()

3. โหลดชุดข้อมูล ETHOS

ในส่วนนี้ คุณจะได้โหลดชุดข้อมูลสำหรับฝึกตัวแยกประเภทและประมวลผลล่วงหน้าเป็นชุดการฝึกและการทดสอบ คุณจะใช้ชุดข้อมูลการวิจัยยอดนิยม ETHOS ซึ่งรวบรวมไว้เพื่อตรวจหาวาจาสร้างความเกลียดชังในโซเชียลมีเดีย คุณดูข้อมูลเพิ่มเติมเกี่ยวกับวิธีรวบรวมชุดข้อมูลได้ในบทความ ETHOS: ชุดข้อมูลการตรวจจับความเกลียดชังทางออนไลน์

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']]

คุณจะเห็นข้อมูลที่คล้ายกับ

ป้ายกำกับ

ความคิดเห็น

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. ดาวน์โหลดและสร้างอินสแตนซ์ของโมเดล

คุณใช้โมเดล Gemma ได้หลายวิธีตามที่อธิบายไว้ในเอกสารประกอบ สิ่งที่คุณต้องทำเมื่อใช้ Keras

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

คุณสามารถทดสอบว่าโมเดลนี้กำลังทำงานโดยสร้างข้อความบางส่วนดังนี้

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

5. โทเค็นการประมวลผลข้อความและตัวคั่นล่วงหน้า

คุณสามารถประมวลผลข้อความล่วงหน้าและใช้โทเค็นตัวคั่นเพื่อช่วยให้โมเดลเข้าใจความตั้งใจของเราได้ดีขึ้น ซึ่งจะทำให้โมเดลมีแนวโน้มที่จะสร้างข้อความที่ไม่เหมาะกับรูปแบบที่คาดไว้น้อยลง ตัวอย่างเช่น คุณอาจพยายามขอการจัดประเภทความรู้สึกจากโมเดลโดยเขียนพรอมต์ดังนี้

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

Text: you look very nice today
Classification:

ในกรณีนี้ โมเดลอาจแสดงผลหรือไม่แสดงสิ่งที่คุณกำลังมองหา ตัวอย่างเช่น หากข้อความมีอักขระขึ้นบรรทัดใหม่ ก็มีแนวโน้มที่จะส่งผลเสียต่อประสิทธิภาพของโมเดล วิธีที่มีประสิทธิภาพมากกว่าคือการใช้โทเค็นคั่น ข้อความแจ้งจะกลายเป็นดังนี้

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

ซึ่งสามารถใช้ฟังก์ชันที่ประมวลผลข้อความล่วงหน้าได้ ดังนี้

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:'])

ตอนนี้หากคุณเรียกใช้ฟังก์ชันโดยใช้พรอมต์และข้อความเดิม คุณควรจะได้รับเอาต์พุตเหมือนเดิม

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)

ซึ่งควรแสดงผลดังนี้

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. ระบบประมวลผลเอาต์พุต

เอาต์พุตของโมเดลคือโทเค็นที่มีความน่าจะเป็นต่างๆ โดยปกติแล้ว ในการสร้างข้อความ คุณจะต้องเลือกโทเค็นที่เป็นไปได้มากที่สุดเพียงไม่กี่รายการ และสร้างประโยค ย่อหน้า หรือแม้แต่เอกสารที่สมบูรณ์ แต่เพื่อจุดประสงค์ของการแยกประเภท สิ่งที่สำคัญจริงๆ ก็คือการที่โมเดลเชื่อว่า Positive น่าจะเป็นไปได้มากกว่า Negative หรือในทางกลับกัน

จากโมเดลที่คุณสร้างอินสแตนซ์ก่อนหน้านี้ นี่คือวิธีประมวลผลเอาต์พุตของโมเดลลงในความน่าจะเป็นแบบอิสระว่าโทเค็นถัดไปจะเป็น Positive หรือ 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()))

คุณทดสอบฟังก์ชันนั้นได้ด้วยการเรียกใช้ด้วยพรอมต์ที่สร้างไว้ก่อนหน้านี้ ดังนี้

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

ซึ่งจะแสดงผลลัพธ์ที่คล้ายกับข้อความต่อไปนี้

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

7. รวมทั้งหมดไว้เป็นตัวแยกประเภท

เพื่อให้การใช้งานง่ายขึ้น คุณสามารถรวมฟังก์ชันทั้งหมดที่คุณเพิ่งสร้างขึ้นไว้ในตัวแยกประเภทแบบ sklearn รายการเดียว พร้อมฟังก์ชันที่ใช้งานง่ายและคุ้นเคยอย่างเช่น 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. การปรับแต่งโมเดล

LoRA ย่อมาจาก Low-Rank Adaptation เป็นเทคนิคการปรับแต่งที่สามารถนำมาใช้ปรับแต่งโมเดลภาษาขนาดใหญ่ได้อย่างมีประสิทธิภาพ อ่านข้อมูลเพิ่มเติมได้ในบทความLoRA: Low-Rank Adaptation of Large Language Models

การใช้งาน Keras ของ Gemma จะมีเมธอด enable_lora() ที่คุณสามารถใช้ในการปรับแต่ง

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

หลังจากเปิดใช้ LoRA แล้ว คุณก็เริ่มกระบวนการปรับแต่งได้ การดำเนินการนี้จะใช้เวลาประมาณ 5 นาทีต่อ Epoch ใน 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)

การฝึกให้มี Epoch มากขึ้นจะทำให้มีความแม่นยำสูงขึ้น จนกว่าจะปรับให้เร็วเกินไป

9. ตรวจสอบผลลัพธ์

ตอนนี้คุณตรวจสอบผลลัพธ์ของตัวแยกประเภทแบบคล่องตัวที่เพิ่งฝึกได้แล้ว โค้ดนี้จะแสดงคะแนนคลาสที่คาดการณ์ไว้สำหรับข้อความบางส่วน

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. การประเมินโมเดล

สุดท้าย คุณจะประเมินประสิทธิภาพของโมเดลโดยใช้เมตริกทั่วไป 2 รายการ ได้แก่ คะแนน F1 และ AUC-ROC คะแนน F1 จะจับข้อผิดพลาดผลลบลวงและผลบวกลวงด้วยการประเมินค่าเฉลี่ยฮาร์มอนิกของความแม่นยำและความอ่อนไหวที่เกณฑ์การจัดประเภทบางเกณฑ์ ในทางกลับกัน AUC-ROC จะหาสมดุลระหว่างอัตราผลบวกจริงและอัตราผลบวกลวงตามเกณฑ์ต่างๆ และคำนวณพื้นที่ใต้กราฟนี้

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

อีกวิธีที่น่าสนใจในการประเมินการคาดการณ์โมเดลคือเมทริกซ์ความสับสน เมทริกซ์ความสับสนจะแสดงข้อผิดพลาดในการคาดการณ์ประเภทต่างๆ เป็นภาพ

from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay

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

เมทริกซ์ความสับสน

สุดท้าย คุณยังสามารถดูเส้นโค้ง ROC เพื่อทำความเข้าใจข้อผิดพลาดในการคาดการณ์ที่อาจเกิดขึ้นเมื่อใช้เกณฑ์การให้คะแนนที่แตกต่างกัน

from sklearn.metrics import RocCurveDisplay, roc_curve

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

เส้นโค้ง ROC