عرض مُصنِّفات السلامة المنهجية الرشيقة مع جيما

1- نظرة عامة

يشرح هذا الدرس التطبيقي حول الترميز كيفية إنشاء مصنِّف نص مخصّص باستخدام ميزة "ضبط كفاءة المعلَمات" (PET). فبدلاً من ضبط النموذج بالكامل، تعمل طرق PET على تحديث كمية صغيرة فقط من المعاملات، ما يجعل عملية التدريب سهلة وسريعة نسبيًا. كما أنّها تسهِّل على النموذج تعلُّم سلوكيات جديدة باستخدام بيانات تدريب قليلة نسبيًا. تم وصف المنهجية بالتفصيل في مقالة نحو مصنِّفات النصوص الرشيقة للجميع التي توضّح كيفية تطبيق هذه الأساليب على مجموعة متنوعة من مهام الأمان وتحقيق الأداء المتطوّر من خلال بضع مئات من أمثلة التدريب فقط.

يستخدم هذا الدرس التطبيقي حول الترميز طريقة LoRA PET ونموذج Gemma الأصغر (gemma_instruct_2b_en) لأنّه يمكن تشغيله بشكل أسرع وأكثر كفاءة. يتناول هذا التعاون خطوات نقل البيانات وتنسيقها للنموذج اللغوي الكبير (LLM) والتدريب على مقاييس LoRA، ثم تقييم النتائج. يتدرّب هذا الدرس التطبيقي حول الترميز على مجموعة بيانات 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، أداءً أفضل، إلا أنّ تكاليف التدريب والتنفيذ تكون أكبر أيضًا.

تحذير محفّز: لأنّ هذا الدرس التطبيقي عن الترميز يطوِّر أداة تصنيف أمان لرصد الكلام الذي يحض على الكراهية، وقد تحتوي الأمثلة على هذه الأمثلة وتقييم النتائج على لغة بذيئة.

‫2. التثبيت والإعداد

بالنسبة لهذا الدرس التطبيقي، ستحتاج إلى إصدار حديث من 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']]

سيظهر لك شيء مشابه لما يلي:

label

تعليق

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. إنهاء كل شيء كمصنِّف

لتسهيل الاستخدام، يمكنك تجميع جميع الدوال التي أنشأتها للتو في مصنِّف واحد يشبه واجهة برمجة التطبيقات مع استخدام دوال سهلة الاستخدام ومألوفة مثل 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 التكيّف منخفض الترتيب. وهو أحد الأساليب الدقيق التي يمكن استخدامها لتحسين النماذج اللغوية الكبيرة بكفاءة. يمكنك الاطّلاع على مزيد من المعلومات في المقالة LoRA: التوافق مع النماذج اللغوية الكبيرة.

يوفر تنفيذ Keras لـ Gemma طريقة enable_lora() التي يمكنك استخدامها للضبط:

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

بعد تمكين LoRA، يمكنك البدء في عملية الضبط. يستغرق ذلك حوالي 5 دقائق في كل حقبة على 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)

سيؤدي التدريب على فترات أخرى إلى زيادة الدقة، إلى أن يحدث فرط التخصيص.

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. تقييم النموذج

وأخيرًا، ستقيّم أداء نموذجنا باستخدام مقياسَين شائعَين، هما نتيجة F1 وAUC-ROC. تسجّل درجة دقة الاختبار أخطاء موجبة خاطئة وأخرى موجبة خاطئة من خلال تقييم المتوسط التوافقي للدقة والتذكر عند حد تصنيف معين. من ناحية أخرى، يسجل 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()

مصفوفة التشويش

وأخيرًا، يمكنك أيضًا إلقاء نظرة على منحنى خاصية تشغيل جهاز الاستقبال للتعرف على أخطاء التنبؤ المحتملة عند استخدام حدود مختلفة للدرجات.

from sklearn.metrics import RocCurveDisplay, roc_curve

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

منحنى خاصية تشغيل جهاز الاستقبال