اذهب إلى المحتوى

طريقة الصقل Fine-Tune لنموذج ذكاء اصطناعي مُدَرَّبْ مُسبقًا


رشا سعد

يقلل استخدام نماذج الذكاء الاصطناعي المُدَرَّبة مُسبقًا pretrained model الوقت والجهد والتكاليف اللازمة لتدريب النماذج من الصفر، فضلًا عن إتاحة الفرصة أمامك لاستخدام أحدث النماذج المتوفرة على منصات متخصصة مثل تلك التي توفرها مكتبة المُحوّلات Transformers من منصة Hugging Face، لذا يلجأ مهندسو الذكاء الاصطناعي لاستخدام النماذج المُدَرَّبة مُسبقًا في كثير من الحالات ويعمدون إلى صقلها أو ضبطها بدقة وتدريبها على بيانات محددة تناسب أهدافهم، إذ يعني صقل النماذج fine-tuning أخذ نموذج تعلم آلي مدرب مسبقًا ومواصلة تدريبه على مجموعة بيانات أصغر وأكثر تخصصًا للحفاظ على قدرات النموذج المدرب، وتكييفه ليناسب استخدامات محددة ويعطي تنبؤاتٍ دقيقة، كما سنطرح بعض الأمثلة التوضيحية على تدريب النماذج باستخدام كل من التقنيات التالية:

  • مُدَرِّب مكتبة المحوّلات Transformers Trainer.

  • إطار العمل تنسرفلو TensorFlow مع كيراس Keras.

  • إطار العمل بايتورش PyTorch لوحده.

تحضير مجموعة بيانات التدريب

قبل أن نبدأ بصقل النموذج fine-tune سنُحَمِّل مجموعة بيانات Dataset ونُحَضِّر بياناتها كما تعلمنا في المقال السابق المعالجة المُسبقة للبيانات قبل تمريرها لنماذج الذكاء الاصطناعي.

اخترنا في هذا المقال مجموعة البيانات Yelp Reviews:

>>> from datasets import load_dataset

>>> dataset = load_dataset("yelp_review_full")
>>> dataset["train"][100]
{'label': 0,
 'text': 'My expectations for McDonalds are t rarely high. But for one to still fail so spectacularly...that takes something special!\\nThe cashier took my friends\'s order, then promptly ignored me. I had to force myself in front of a cashier who opened his register to wait on the person BEHIND me. I waited over five minutes for a gigantic order that included precisely one kid\'s meal. After watching two people who ordered after me be handed their food, I asked where mine was. The manager started yelling at the cashiers for \\"serving off their orders\\" when they didn\'t have their food. But neither cashier was anywhere near those controls, and the manager was the one serving food to customers and clearing the boards.\\nThe manager was rude when giving me my order. She didn\'t make sure that I had everything ON MY RECEIPT, and never even had the decency to apologize that I felt I was getting poor service.\\nI\'ve eaten at various McDonalds restaurants for over 30 years. I\'ve worked at more than one location. I expect bad days, bad moods, and the occasional mistake. But I have yet to have a decent experience at this store. It will remain a place I avoid unless someone in my party needs to avoid illness from low blood sugar.'}

وبما أن مجموعة بياناتنا نصية لذا سنحتاج مُرَمِّزًا tokenizer مناسبًا للنموذج لمعالجتها كما تعلمنا في مقالات السلسلة، تتضمن هذه المعالجة أساليب الحشو والاقتطاع لتوحيد أطوال السلاسل النصية، وسنستخدم دالةً تدعى map لتسريع المعالجة التحضيرية للبيانات وتطبيقها على كامل مجموعة البيانات dataset وفق التالي:

>>> from transformers import AutoTokenizer

>>> tokenizer = AutoTokenizer.from_pretrained("google-bert/bert-base-cased")


>>> def tokenize_function(examples):
    return tokenizer(examples["text"], padding="max_length", truncation=True)


>>> tokenized_datasets = dataset.map(tokenize_function, batched=True)

للسهولة وتسريع العمل يمكنك أخذ جزء من مجموعة البيانات فقط بدلًا من العمل معها كاملةً كما يلي:

>>> small_train_dataset = tokenized_datasets["train"].shuffle(seed=42).select(range(1000))
>>> small_eval_dataset = tokenized_datasets["test"].shuffle(seed=42).select(range(1000))

تدريب نموذج ذكاء اصطناعي باستخدام PyTorch Trainer

إن المُدَرِّبTrainer هو أحد أصناف مكتبة المحوّلات Transformers حيث يستخدم لتدريب نماذج المكتبة، ويوفر عليك أعباء إنشاء حلقة تدريب خاصة بمشروعك من الصفر، ويتمتع هذا الصنف بواجهة برمجية API متنوعة الخيارات وتؤمن مزايا تدريبية واسعة، مثل: تسجيل الأحداث logging، والتدرج التراكمي gradient accumulation، والدقة المختلطة mixed precision.

سنبدأ عملنا بتحميل النموذج وفق الأوامر التالية مع تحديد عدد التسميات التوضيحية labels المتوقعة من البيانات المُدخَلة، وإذا قرأت بطاقة وصف مجموعة البيانات التي حضرناها Yelp Review ستجد أن عدد التسميات labels فيها هو 5:

>>> from transformers import AutoModelForSequenceClassification

>>> model = AutoModelForSequenceClassification.from_pretrained("google-bert/bert-base-cased", num_labels=5)

ملاحة: عندما تُنَفِذ الأوامر السابقة ستواجه تحذيرًا مفاده أن بعض الأوزان المُدَرَّبة مسبقًا في النموذج لن تُسْتَخْدَمْ، وبعضها ستُعاد تهيئته عشوائيًا، يُعدّ هذا التحذير طبيعيًا ولا يستوجب القلق إذ سيُهمَل رأس النموذج BERT المُدَرَّب مسبقًا ويُستَبْدَل برأس تصنيف لا على التعيين، ثم يٌدَرَّب الرأس الجديد على تصنيف السلاسل وتنتقل إليه تلقائيًا كل المعرفة التي اكتسبها النموذج المدرب مسبقًا فيستفيد منها، علمًا أن رأس النموذج model head هو الجزء المسؤول عن معالجة مهمة معينة مثل التصنيف أو الترجمة ويستخدم لتحديد نتائجها.

معاملات التدريب الفائقة hyperparameters

سننشئ صنفًا لوسطاء التدريب TrainingArguments يتضمن كافة المعاملات الفائقة التي يمكننا ضبطها بالإضافة إلى الرايات flags الخاصة بتفعيل خيارات التدريب المختلفة، سنستعمل هنا المعاملات الافتراضية لكن يمكنك استعراض جميع المعاملات الفائقة وتجريبها لتصل إلى الإعدادات الملائمة لحالتك:

لا تنسَ أن تحدد مكان حفظ نقاط التحقق checkpoints الناتجة عن التدريب:

>>> from transformers import TrainingArguments

>>> training_args = TrainingArguments(output_dir="test_trainer")

التقييم

لا يعطي المُدَرِّب Trainer في الأحوال الطبيعية مؤشراتٍ عن أداء النماذج في أثناء التدريب، فإذا رغبت بالحصول على تقييمٍ لنموذجك، ينبغي أن تمرر دالة خاصة بهذا الأمر تحسب مؤشرات الأداء وترجع لك تقريرًا بتقييم النموذج، وفي هذا المجال توفر مكتبة التقييم Evaluate دالةً بسيطة تدعى accuracy يمكنك تحميلها باستخدام evaluate.load كما في المثال التالي (طالع هذه الجولة السريعة في مكتبة التقييم لمزيدٍ من المعلومات):

>>> import numpy as np
>>> import evaluate

>>> metric = evaluate.load("accuracy")

استدعِ الدالة compute مع metric وفق التالي لحساب دقة التنبؤات الناتجة عن نموذجك، ولأن نماذج مكتبة المحوّلات Transformers تضع مخرجاتها في السمة logits (كما تعلمنا سابقًا في مقال جولة سريعة للبدء مع مكتبة المحوّلات Transformers) فينبغي لنا في البداية تحويل logits الناتجة عن النموذج إلى تنبؤات predictions ثم تمريرها لدالة حساب الدقة:

>>> def compute_metrics(eval_pred):
        logits, labels = eval_pred
        predictions = np.argmax(logits, axis=-1)
        return metric.compute(predictions=predictions, references=labels)

والآن أعطِ القيمة "epoch" للمعامل evaluation_strategy من وسطاء التدريب TrainingArguments لمراقبة أداء نموذجك أثناء التدريب فهذا الخيار سيعطيك تقييمًا في نهاية كل دورة تدريبية epoch للنموذج:

>>> from transformers import TrainingArguments, Trainer

>>> training_args = TrainingArguments(output_dir="test_trainer", evaluation_strategy="epoch")

المُدَرِّب Trainer

لننشئ الآن كائن المُدَرِّب Trainer باستخدام جميع الإعدادات السابقة وهي: النموذج الذي اخترناه، ووسطاء التدريب، ومجموعة بيانات التدريب، ودالة التقييم وذلك وفق الأمر التالي:

>>> trainer = Trainer(
        model=model,
        args=training_args,
        train_dataset=small_train_dataset,
        eval_dataset=small_eval_dataset,
        compute_metrics=compute_metrics,
)

ثم دَرِّب نموذجك باستخدامه كما يلي:

>>> trainer.train()

تدريب نموذج TensorFlow باستخدام Keras

نناقش هنا تدريب نماذج من مكتبة Transformers باستخدام إطار العمل تنسرفلو TensorFlow وكيراس Keras API، حيث أن كيراس هو إطار عمل سهل ومفتوح المصدر يسمح بإنشاء شبكات عصبية معقدة بتعليمات قليلة، بدأ مشروعًا مستقلًا ثم اندمج مع TensorFlow (يمكنك معرفة المزيد بمطالعة قسم الذكاء الاصطناعي على أكاديمية حسوب وخاصةً المقال التعريفي مكتبات وأطر عمل الذكاء الاصطناعي).

تحويل البيانات إلى صيغة تناسب كيراس Keras

يتطلب تدريب نماذج Transformers باستخدام Keras API تحميل مجموعة بيانات بصيغة تتوافق مع كيراس Keras، وإحدى الطرق السهلة لذلك هي تحويل البيانات التدريبية إلى مصفوفات NumPy ثم تمريرها له، تناسب هذه الطريقة مجموعات البيانات صغيرة الحجم وهي ما سنجربه بدايةً قبل الانتقال إلى طرق أكثر تعقيدًا.

لنُحمِّل في البداية مجموعة بيانات، وقد اخترنا هنا المجموعة CoLA dataset من GLUE benchmark وهي مجموعة بيانات بسيطة تناسب تصنيف النصوص الثنائية binary text، وسنأخذ منها القسم المخصص للتدريب فقط:

from datasets import load_dataset

dataset = load_dataset("glue", "cola")
dataset = dataset["train"]  # أخذنا من مجموعة البيانات القسم الخاص بالتدريب فقط

سنُحَمِّل بعد ذلك مُرمِّزًا tokenizer يناسب النموذج ونستخدمه لترميز بيانات الندريب وتحويلها إلى مصفوفات NumPy، ولكن في مثالنا البيانات ثنائية بسيطة وتسمياتها التوضيحية labels هي مجموعة أصفار وواحدات فيمكننا تحويلها إلى مصفوفة NumPy مباشرةً دون ترميز:

from transformers import AutoTokenizer

tokenizer = AutoTokenizer.from_pretrained("google-bert/bert-base-cased")
tokenized_data = tokenizer(dataset["sentence"], return_tensors="np", padding=True)
# يرجع المُرَمِّز دفعات مُرَمَّزة من البيانات، حوّلناها هنا إلى قاموس يناسب Keras
tokenized_data = dict(tokenized_data)

labels = np.array(dataset["label"])  # هذه البيانات في الأساس مصفوفة أصفار وواحدات

وفي المرحة الأخيرة سنُحَمِّل النموذج ونخضعه لعملية تصريف compile ثم ملائمة fit، وننوه هنا أن كل نموذج في مكتبة المحوّلات يتضمن دالةً افتراضية لحساب الخسارة loss function تناسب المهمة التي يُستَخدم النموذج لأجلها، فلست بحاجة لضبط أي خيارات بهذا الخصوص:

from transformers import TFAutoModelForSequenceClassification
from tensorflow.keras.optimizers import Adam

# تحميل النموذج وتصريفه
model = TFAutoModelForSequenceClassification.from_pretrained("google-bert/bert-base-cased")
# غالبًا ما تكون معدلات التَعَلُّم المنخفضة هي الأنسب لصقل نماذج مكتبة المحوّلات المُدَرَّبة مُسبقًا 
model.compile(optimizer=Adam(3e-5))  # لاحظ عدم وجود أي وسيط يتعلق بدالة حساب الخسارة فهي افتراضية

model.fit(tokenized_data, labels)

ملاحظة: تختار نماذج Hugging Face تلقائيًا دوال الخسارة المناسبة لبُنيتها ومهامها، لذا لن تضطر لتمرير الوسيط الخاص بحساب الخسارة عند تصريف نموذجك باستخدام compile()‎ لكن الخيار يبقى لك ففي حال لم ترغب باستخدام دالة الخسارة الافتراضية يمكنك حسابها بنفسك.

يعمل أسلوب الترميز السابق جيدًا مع مجموعات البيانات صغيرة الحجم لكنه لا يُعدّ عمليًّا أبدًا مع مجموعات البيانات الكبيرة بل وسيؤدي إلى إبطاء عملية التدريب، يعود ذلك لسببين: الأول أن مصفوفتي الرموز والتسميات التوضيحية ستكونان كبيرتي الحجم وتحميلهما بالكامل إلى الذاكرة يُعدّ مشكلة، والسبب الثاني أن مكتبة Numpy لا تستطيع التعامل مع المصفوفات غير منظمة الأطوال المعروفة باسم jagged arrays يعني ذلك أنك ستضطر إلى حشو العناصر القصيرة في المصفوفة بالأصفار لتصبح جميعها بطول موحد يساوي أطول عنصر في المصفوفة، وهذا أيضًا سيُبَطئ التدريب.

تحميل مجموعة بيانات بصيغة tf.data.Dataset

يمكنك تحميل مجموعة بياناتك بصيغة tf.data.Dataset بدلًا من اتباع الطريقة السابقة والمخاطرة بإبطاء التدريب، سنقترح عليك طريقتين لإنجاز الأمر، وتستطيع إنشاء خط أنابيبك الخاص tf.data يدويًا إذا رغبت بذلك:

  • prepare_tf_dataset()‎: تعتمد هذه الطريقة على طبيعة نموذجك لذا تُعدّ الطريقة الأنسب والموصى بها في معظم الحالات، فهي تفحص مواصفات النموذج وتتعرف تلقائيًا على أعمدة مجموعة البيانات المتوافقة معه أي التي يمكن استخدامها كمدخلات للنموذج، وتتجاهل الأعمدة غير المتوافقة فتُنشئ بذلك مجموعة بيانات أبسط وأفضل أداءً.

  • to_tf_dataset: طريقة منخفضة المستوى low-level فهي تتحكم بالتفاصيل الدقيقة لطريقة إنشاء مجموعة بيانات التدريب، فتُمَكِّنك من تحديد الأعمدة columns وتسمياتها التوضيحية label_cols التي تود تضمينها في مجموعة البيانات.

لنبدأ بالطريقة الأولى prepare_tf_dataset()‎، ولكن قبل تطبيقها ينبغي لنا ترميز البيانات وإدخال مُخرجات المُرَمِّز بهيئة أعمدة إلى مجموعة البيانات dataset كما يلي:

def tokenize_dataset(data):
    # سنُدخِل مفاتيح القاموس الناتج هنا إلى مجموعة البيانات بصفتها أعمدة
    return tokenizer(data["text"])

dataset = dataset.map(tokenize_dataset)

تُخَزَّن مجموعات بيانات Hugging Face على القرص الصلب افتراضيًا، فلن تسبب ضغطًا على استخدام الذاكرة على حاسوبك، وبمجرد إضافة الأعمدة السابقة تستطيع الحصول على الدفعات batches من مجموعة البيانات، وإضافة رموز الحشو إلى كل دفعة وهو ما يقلل عدد رموز الحشو المطلوبة في كل مرة مقارنةً بحشو مجموعة البيانات كاملةً.

tf_dataset = model.prepare_tf_dataset(dataset["train"], batch_size=16, shuffle=True, tokenizer=tokenizer)

مررنا إلى Preparation_tf_dataset في التعليمات السابقة وسيطًا خاصًا هو tokenizer يحدد المُرَمِّز الذي سنستخدمه لحشو الدفعات المُحَمَّلة بطريقة صحيحة، لكن يمكنك الاستغناء عنه إذا كانت العينات في مجموعة بياناتك متساوية الطول ولا تحتاج لأي حشو، أو استبداله بوسيطٍ آخر نحو Collate_fn إذا كنت ترغب بتدريب النموذج على حالات أعقد، مثل نمذجة اللغة المقنعة masked language modelling أي إخفاء بعض الرموز ليتنبأ النموذج بالكلمات من السياق أو غيرها من الحالات، تستطيع تحدد نوعية المعالجة التحضيرية التي تريدها للدفعات قبل تمريرها للنموذج، يساعدك الاطلاع على بعض الأمثلة والملاحظات المتعلقة بالموضوع من منصة Hugging Face لتطبيق ذلك عمليًّا.

والآن بعد إنشاء مجموعة بيانات tf.data.Dataset يتبقى لنا الخطوة الأخيرة وهي تصريف النموذج compile وملائمته fit وفق التالي:

model.compile(optimizer=Adam(3e-5))  # No loss argument!

model.fit(tf_dataset)

تدريب نموذج ذكاء اصطناعي باستخدام Native PyTorch

يساعدك المُدَرِّب Trainer على تدريب نموذجك وضبطه بتعليمة واحدة فقط ويغنيك عن إنشاء حلقة التدريب من الصفر، لكن بعض المستخدمين يفضلون عدم الاعتماد على المُدَرِّب وإنشاء حلقات تدريبهم الخاصة بأنفسهم لتدريب نماذج مكتبة المحوّلات Transformers، فإذا كنت أحدهم يمكنك إجراء ذلك باستخدام إطار العمل PyTorch لوحده أي native PyTorch وفق الخطوات التالية، لكن في البداية ننصحك بتفريغ الذاكرة المؤقتة على جهازك أو دفتر ملاحظاتك notebook بإعادة تشغيله أو بتنفيذ هذه الأوامر:

del model
del trainer
torch.cuda.empty_cache()

تتضمن الخطوات التالية المعالجة التي سنجريها يدويًا على البيانات المُرَمَّزة tokenized_dataset لتحضيرها للتدريب.

1. تخَلَّص من عمود النص text لأن النموذج لا يقبل النصوص الخام مدخلاتٍ له:

>>> tokenized_datasets = tokenized_datasets.remove_columns(["text"])

2. عَدِّل اسم العمود label إلى labels ليتوافق مع اسم الوسيط الذي يقبله النموذج:

>>> tokenized_datasets = tokenized_datasets.rename_column("label", "labels")

3. اضبط تنسيق مجموعة البيانات لتُرجِع PyTorch tensors بدلًا من القوائم المعتادة:

>>> tokenized_datasets.set_format("torch")

4. ثم أنشئ مجموعة بيانات مُصَغَّرة من مجموعة البيانات الكاملة كما فعلنا في الفقرات السابقة لتسريع عملية صقل النموذج fine-tuning:

>>> small_train_dataset = tokenized_datasets["train"].shuffle(seed=42).select(range(1000))
>>> small_eval_dataset = tokenized_datasets["test"].shuffle(seed=42).select(range(1000))

مُحَمِّل البيانات DataLoader

أنشئ مُحَمِّل بيانات DataLoader لمجموعات بيانات التدريب والاختبار لتٌنَفِّذ العمليات التكرارية على دفعات البيانات:

>>> from torch.utils.data import DataLoader

>>> train_dataloader = DataLoader(small_train_dataset, shuffle=True, batch_size=8)
>>> eval_dataloader = DataLoader(small_eval_dataset, batch_size=8)

ثم حَمِّل النموذج وحَدِّد عدد التسميات labels المتوقعة له:

>>> from transformers import AutoModelForSequenceClassification

>>> model = AutoModelForSequenceClassification.from_pretrained("google-bert/bert-base-cased", num_labels=5)

المُحَسِّنْ Optimizer ومُجَدّوِل معدل التعلُّم learning rate scheduler

سننشئ مُحَسِّنْ Optimizer ومُجَدّوِل معدل التعلُّم learning rate scheduler لتدريب النموذج وضبطه، اخترنا هنا المُحَسِّنْ AdamW من PyTorch:

>>> from torch.optim import AdamW

>>> optimizer = AdamW(model.parameters(), lr=5e-5)

ثم مُجَدّوِل معدل التعلُّم باستخدام المُجَدّوِل الافتراضي للمُدَّرِب Trainer:

>>> from transformers import get_scheduler

>>> num_epochs = 3
>>> num_training_steps = num_epochs * len(train_dataloader)
>>> lr_scheduler = get_scheduler(
    name="linear", optimizer=optimizer, num_warmup_steps=0, num_training_steps=num_training_steps
)

وأخيرًا حَدِّدْ قيمة المعامل device لتكون "cpu" إذا كان لديك وحدة معالجة رسوميات (GPU) ورغبت باستخدامها للتدريب، فمن دونها سيستغرق التدريب مدةً طويلة في حال الاعتماد على وحدة المعالجة المركزية (CPU) لوحدها وقد تصل المدة لساعات بدلًا من بضع دقائق:

>>> import torch

>>> device = torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu")
>>> model.to(device)
اقتباس
اقتباس

ننوه هنا إلى توفر خدمات سحابية تتيح لك الوصول إلى وحدات GPU عبر السحابة من مزودين مثل: Colaboratory و SageMaker.

أصبحنا جاهزين الآن للتدريب.

حلقة التدريب Training loop

إليك نموذجًا للشيفرة البرمجية الخاصة بحلقة التدريب، ويمكنك استخدام المكتبة tqdm لإضافة شريط خاص bar يعرض لك تقدم مراحل التدريب لتتبع سير العملية:

>>> from tqdm.auto import tqdm

>>> progress_bar = tqdm(range(num_training_steps))

>>> model.train()
     for epoch in range(num_epochs):
        for batch in train_dataloader:
            batch = {k: v.to(device) for k, v in batch.items()}
            outputs = model(**batch)
            loss = outputs.loss
        loss.backward()

        optimizer.step()
        lr_scheduler.step()
        optimizer.zero_grad()
        progress_bar.update(1)

التقييم Evaluate

يختلف أسلوب تقييم أداء النموذج في حلقة التدريب المخصصة عنه في صنف المُدَرِّب Trainer، فبدلًا من حساب مؤشرات الأداء وتصدير التقارير عنها في نهاية كل دورة تدربية epoch سيجري التقييم هنا في نهاية التدريب إذ سنُجَمع كافة الدفعات باستخدم add_batch ونحسب مؤشرات الأداء.

>>> import evaluate

>>> metric = evaluate.load("accuracy")
>>> model.eval()
     for batch in eval_dataloader:
        batch = {k: v.to(device) for k, v in batch.items()}
        with torch.no_grad():
            outputs = model(**batch)

        logits = outputs.logits
        predictions = torch.argmax(logits, dim=-1)
        metric.add_batch(predictions=predictions, references=batch["labels"])

>>> metric.compute()

الخلاصة

تغرفنا في مقال اليوم على تقنية الصقل fine-tuning لتدريب نماذج الذكاء الاصطناعي المُدَرَّبة مسبقًا وتحسين أدائها على مهام معينة باستخدام بيانات محددة، ووضحنا الفوائد الرئيسية لاستخدام نماذج مُدَرَّبة مسبقًا وكيفية تحميل وتحضير البيانات لها وتدريبها وضبطها وتقييم أدائها من خلال استخدام مكتبات وأطر عمل متنوعة تساعدنا في تنفيذ مهام الصقل مثل Transformers و TensorFlow و Keras و PyTorch كي نحسن من كفاءة وأداء هذه النماذج في مشاريعنا الخاصة.

ترجمة -وبتصرف- لقسم Fine-tune a pretrained model من توثيقات Hugging Face.

اقرأ أيضًا

 


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...