1. نظرة عامة
يوضّح هذا الدرس التطبيقي كيفية إنشاء مصنِّف مخصّص للنصوص باستخدام الضبط الفعال للمعلَمات (PET). بدلاً من تحسين النموذج بالكامل، تعمل طُرق PET على تعديل عدد صغير فقط من المَعلمات، ما يسهّل تدريب النموذج ويسرعه نسبيًا. ويسهّل ذلك أيضًا على النموذج تعلُّم سلوكيات جديدة باستخدام بيانات تدريب قليلة نسبيًا. تم وصف المنهجية بالتفصيل في مقالة Towards Agile Text Classifiers for Everyone التي توضّح كيفية تطبيق هذه الأساليب على مجموعة متنوعة من مهام السلامة وتحقيق أفضل أداء باستخدام بضع مئات من أمثلة التدريب فقط.
يستخدم هذا الدليل التعليمي طريقة الضبط الفعال للمعلَمات (PET) في LoRA ونموذج Gemma الأصغر حجمًا (gemma_instruct_2b_en
) لأنّه يمكن تشغيله بشكل أسرع وأكثر فعالية. يتناول هذا المشروع التعاوني خطوات نقل البيانات وتنسيقها للنموذج اللغوي الكبير وتدريب أوزان LoRA ثم تقييم النتائج. يتم تدريب هذا الإصدار التجريبي من "مختبر رموز Google" على مجموعة بيانات ETHOS، وهي مجموعة بيانات متاحة للجميع لرصد الكلام الذي يحض على الكراهية، وتم إنشاؤها من تعليقات على YouTube وReddit. عند تدريبه على 200 مثال فقط (1/4 من مجموعة البيانات)، يحقّق هذا النموذج قيمة F1: 0.80 وROC-AUC: 0.78، أي أعلى بقليل من أفضل الممارسات الحالية الواردة في قائمة الصدارة (في وقت كتابة هذه المقالة، 15 شباط (فبراير) 2024). عند تدريبه على كامل الأمثلة الـ 800، يحقّق مقياس دقة الاختبار F1 نتيجة 83.74 ونتيجة 88.17 لمنطقة تحت منحنى ROC. إنّ النماذج الأكبر حجمًا، مثل gemma_instruct_7b_en
، ستحقق أداءً أفضل بشكل عام، ولكن تكاليف التدريب والتنفيذ ستكون أكبر أيضًا.
تحذير: بما أنّ هذا الإصدار التجريبي من "مختبر رموز Google" يطوّر مصنّف أمان لرصد الكلام الذي يحضّ على الكراهية، فإنّ الأمثلة وتقييم النتائج يتضمّنان بعض اللغة المسيئة.
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()
تم اختبار هذا الدرس التطبيقي حول الترميز باستخدام Tensorflow كخلفية لـ Keras، ولكن يمكنك استخدام Tensorflow أو Pytorch أو JAX:
import os
os.environ["KERAS_BACKEND"] = "tensorflow"
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 |
|
|
1 |
|
|
2 |
|
|
3 |
|
|
4 |
|
|
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 دقائق تقريبًا لكل دورة تدريب على 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()
أخيرًا، يمكنك أيضًا الاطّلاع على منحنى ROC للتعرّف على الأخطاء المحتمَلة في التوقّعات عند استخدام عتبات مختلفة للنقاط.
from sklearn.metrics import RocCurveDisplay, roc_curve
fpr, tpr, _ = roc_curve(y_true, y_prob, pos_label=1)
RocCurveDisplay(fpr=fpr, tpr=tpr).plot()