Text classification, recognition of positive and negative Persian comments

عنوان پروژه: پردازش متون فارسی، تشخیص کامنت های مثبت و منفی مقدمه و اهداف: در این پروژه، هدف، بررسی و تحلیل نظرات کاربران فارسی‌زبان در فیلم، سریال، انیمه و انیمیشن با استفاده از روش‌های یادگیری عمیق است. این پروژه شامل دو مدل اصلی می‌شود: یک مدل با استفاده از شبکه‌های عصبی بازگشتی (RNN) (با استفاده از یک لایه LSTM) و یک مدل با استفاده از شبکه‌های عصبی کانولوشنی (CNN) (از طریق کانولوشن یک بعدی) که در نهایت این دو مدل با هم مقایسه شده اند. توضیح کلی از فرایند پروژه: دیتاست شامل دو پوشه Negative و  Positive است که کامنت ها در آنها در فایل های txt جدا از هم به صورت label.index ذخیره شده است. پس از بارگذاری دیتاست، ابتدا فایل های خالی درون آن حذف، سپس نظرات در review و لیبل ها در labels ذخیره می شوند (فایل های موجود در پوشه Negative با لیبل 0 و فایل های موجود در پوشه Positive با لیبل 1). سپس فایل های تکراری را حذف میکنیم و فایل های باقی مانده را براساس طول کامنت ها sort می کنیم. داده ها را به دو دسته آزمون و آزمایش تقسیم بندی کرده و بعد از پیش پردازش داده ها و تشکلیل train_ds و val_ds، مدل ها را تشکیل داده و کامپایل و fit می کنیم. در این پروژه تعداد داده های جمع آوری شده 2500 داده بوده که تعداد کمی محسوب می شود پس داده ها جوری پیش پردازش شده اند که هنگام فیت شدن مدل overfit کاهش پیدا کند و به درصد دقت نسبی بالای 80 درصد برسد.

train_ds = tf.data.Dataset.from_tensor_slices((X_train, y_train))

train_ds = train_ds.map(vectorization,

                        num_parallel_calls=AUTOTUNE)

train_ds = train_ds.cache()

train_ds = train_ds.shuffle(BUFFER_SIZE)

train_ds = train_ds.padded_batch(BATCH_SIZE,

                                 drop_remainder=True,

                                 padding_values=(fa_tokenizer.pad_token_id, 0))




train_ds = train_ds.shuffle(STEPS_PER_EPOCH)

train_ds = train_ds.prefetch(AUTOTUNE)




val_ds = tf.data.Dataset.from_tensor_slices((X_test, y_test))

val_ds = val_ds.map(vectorization, num_parallel_calls=AUTOTUNE)

val_ds = val_ds.cache()

val_ds = val_ds.padded_batch(BATCH_SIZE,

                               drop_remainder=True,

                               padding_values=(fa_tokenizer.pad_token_id, 0))

val_ds = val_ds.prefetch(AUTOTUNE)
ورودی های پروژه:
  1. مجموعه داده‌ها
  • فایل فشرده نظرات کاربران: این فایل شامل نظرات فارسی کاربران در دو دسته مثبت و منفی است (پوشه Positive و Negative). داده ها به صورت index در فایل های متنی جدا ذخیره شده اند.
  1. کتابخانه‌ها و ابزارهای مورد استفاده
  2. Tokenizer
  • توکنایزر مدل AutoTokenizer فارسی برای تبدیل متن به توکن‌های عددی، مدل bolbolzaban/gpt2-persian
(این توکنایزر برای تبدیل متن فارسی به توکن‌های عددی استفاده می‌شود.)
  1.  تنظیمات مدل و پارامترها
  • تنظیمات عمومی:
AUTOTUNE = tf.data.experimental.AUTOTUNE BATCH_SIZE = 32 STEPS_PER_EPOCH = len(X_train) // BATCH_SIZE BUFFER_SIZE = 256 py_fa_vectorization : تبدیل متن ورودی به توکن‌های عددی از طریق tokenizer. vectorization: وکتورایز کردن ورودی ها.
  • پارامترهای مدل‌ها:
مدل اول RNN (LSTM): لایه تعبیه‌سازی (Embedding) با ورودی دیکشنری توکنایزر و ابعاد 128 لایه LSTM دوطرفه با 64 واحد لایه‌ های Dense و Dropout مدل دوم CNN (Conv1D): لایه Embedding  با ورودی دیکشنری توکنایزر و ابعاد 128 لایه کانولوشن یک بعدی با 128 فیلتر و کرنل سایز 5 لایه‌های Dense  و Dropout
  1. داده‌های ورودی جدید برای پیش‌بینی
  2. توابع و کدهای مورد استفاده
بررسی مدل RNN(LSTM):
  • Embedding Layer
Input بردارهای عددی (input_ids) با طول متغیر (و به اندازه‌ی بزرگترین جمله در داده‌های train) که توسط tokenizer تولید شده‌اند. Embedding : Output  هر token به یک بردار ابعادی کمتر (در اینجا 128 ابعاد).
  • Bidirectional LSTM Layer
Input: Embedding هر token با ابعاد 128. Output: بردارهای ویژگی که توسط لایه‌های LSTM در هر دو جهت (جلو و عقب) استخراج شده‌اند. اندازه خروجی برابر با 64 است (تعداد واحدهای LSTM).
  • Dense Layer (Fully Connected)
Input: بردارهای ویژگی خروجی از LSTM با اندازه 64. Output: بردارهای با ابعاد 128 که از تابع فعالسازی ReLU عبور کرده‌اند.
  • Dropout Layer
Input : بردارهای ویژگی خروجی از LSTM با اندازه 64. Output : بردارهای با ابعاد 128 که از تابع فعالسازی ReLU عبور کرده‌اند.
  • Dropout Layer
Input : بردارهای با ابعاد 128. Output: بردارهایی با ابعاد 128 که در هر مرحله با احتمال 0.3 تصادفاً صفر می‌شوند (برای جلوگیری از بیش‌برازش).
  • Output Layer (Dense Layer)
Input : بردارهای با ابعاد 128. Output: دو خروجی (برای برچسب‌های مثبت و منفی) که با استفاده از تابع softmax به احتمالات تبدیل می‌شوند.
model = tf.keras.Sequential([

    tf.keras.layers.Embedding(

        input_dim=len(fa_tokenizer.get_vocab()),

        output_dim=128,

        mask_zero=True),

    tf.keras.layers.Bidirectional(tf.keras.layers.LSTM(64)),

    tf.keras.layers.Dense(128, activation='relu'),

    tf.keras.layers.Dropout(0.3),

    tf.keras.layers.Dense(2)

])
Loss Function (masked_loss_function)
  • y_true : برچسب‌های واقعی که به عنوان ورودی به مدل داده می‌شوند.
  • pred : پیش‌بینی‌های مدل برای برچسب‌ها.
  • loss_object : تابع خطای اصلی که در اینجا SparseCategoricalCrossentropy است.
  • pad_token_id : توکن پدینگ مورد استفاده برای توکن‌های پدینگ در داده‌ها.
  • mask: یک ماسک برای نادیده گرفتن توکن‌های پدینگ ایجاد می‌شود.
  • masked_loss: خطای محاسبه شده با در نظر گرفتن ماسک.
تابع masked_loss_function از اهمیت ویژه‌ای برای مدل‌هایی که با داده‌های دنباله‌ای کار می‌کنند برخوردار است، زیرا اجازه می‌دهد که خطا فقط برای داده‌های واقعی (بدون در نظر گرفتن توکن‌های پدینگ) محاسبه شود، که این امر می‌تواند به بهبود عملکرد و دقت مدل کمک کند.
loss_object = tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True)

acc_object = tf.keras.metrics.Accuracy()


def masked_loss_function(y_true, pred):

    loss = loss_object(y_true, pred)

    mask = tf.cast(tf.not_equal(y_true, fa_tokenizer.pad_token_id), dtype=loss.dtype)

    masked_loss = tf.reduce_sum(loss * mask) / tf.reduce_sum(mask)


    return masked_loss
Accuracy Metric (MaskedAccuracy)
  • برای اندازه‌گیری دقت مدل با استفاده از ماسک (نادیده گرفتن توکن‌های پدینگ)، کلاس MaskedAccuracy تعریف شده است.
  • update_state: در این متد، پیش‌بینی‌های مدل به صورت label ids تبدیل می‌شوند و یک ماسک برای نادیده گرفتن توکن‌های پدینگ اعمال می‌شود. سپس دقت به وسیله‌ی متریک دقت (Accuracy) که در constructor کلاس تعریف شده است، به روزرسانی می‌شود.
  • result: این متد نتیجه‌ی دقت محاسبه شده را برمی‌گرداند.
  • reset_state: با استفاده از این متد، وضعیت متریک دقت ریست می‌شود.
class MaskedAccuracy(tf.keras.metrics.Metric):

    def __init__(self, name='masked_accuracy', **kwargs):

        super(MaskedAccuracy, self).__init__(name=name, **kwargs)

        self.acc_object = tf.keras.metrics.Accuracy()




    def update_state(self, y_true, y_pred, sample_weight=None):

        # Convert predictions to label ids

        y_pred = tf.argmax(y_pred, axis=-1)


        # Create a mask to ignore padding tokens

        mask = tf.logical_not(tf.equal(y_true, fa_tokenizer.pad_token_id))


        # Cast the mask to int32 and squeeze to ensure correct shape

        mask = tf.cast(mask, tf.int32)

        mask = tf.squeeze(mask, axis=-1)


        # Apply the mask to y_true and y_pred

        y_true = tf.boolean_mask(y_true, mask)

        y_pred = tf.boolean_mask(y_pred, mask)


        # Update the accuracy metric

        self.acc_object.update_state(y_true, y_pred)


    def result(self):

        return self.acc_object.result()


    def reset_state(self):

        self.acc_object.reset_states()



# Instantiate the custom metric

masked_accuracy_metric = MaskedAccuracy()


# Compile the model with the custom masked accuracy metric

model.compile(

    loss=masked_loss_function,

    optimizer=tf.keras.optimizers.Adam(1e-2),

    metrics=[masked_accuracy_metric]

)
بررسی مدل CNN (Conv1D)
  • Embedding Layer
Input: بردارهای عددی (input_ids) با طول متغیر (و به اندازه‌ی بزرگترین جمله در داده‌های آموزشی) که توسط tokenizer تولید شده‌اند. Output: Embedding  هر token به یک بردار ابعادی کمتر (در اینجا 128 ابعاد).
  • Conv1D Layer
Input: Embedding  هر token با ابعاد 128. Output : ویژگی‌های محلی استخراج شده از جملات با استفاده از فیلترهای Conv1D با طول 5 و تابع فعالسازی ReLU
  • GlobalMaxPooling1D Laye
Input: ویژگی‌های محلی استخراج شده از Conv1D Output: بردارهای ویژگی که حداکثر مقدار هر کانال را انتخاب می‌کنند و برای ورود به لایه‌های بعدی آماده می‌کنند.
  • Dense Layer (Fully Connected)
Input: بردارهای ویژگی با اندازه متغیر (بسته به تعداد فیلترها و پارامترهای Conv1D) Output : بردارهای با ابعاد 128 که از تابع فعالسازی ReLU عبور کرده‌اند.
  • Dropout Layer
Input : بردارهای با ابعاد 128. Output : بردارهایی با ابعاد 128 که در هر مرحله با احتمال 0.3 تصادفاً صفر می‌شوند (برای جلوگیری از بیش‌برازش).
  • Output Layer (Dense Layer)
Input : بردارهای با ابعاد 128. Output: دو خروجی (برای برچسب‌های مثبت و منفی) که با استفاده از تابع softmax به احتمالات تبدیل می‌شوند.
model_2 = tf.keras.Sequential([

    tf.keras.layers.Embedding(

        input_dim=len(fa_tokenizer.get_vocab()),

        output_dim=128,

        mask_zero=True),

    tf.keras.layers.Conv1D(128, 5, activation='relu'),

    tf.keras.layers.GlobalMaxPooling1D(),

    tf.keras.layers.Dense(128, activation='relu'),

    tf.keras.layers.Dropout(0.3),

    tf.keras.layers.Dense(2)

])
تنظیمات اولیه‌ی آموزش مدل ها:
  • استفاده از متد fit برا آموزش
epochs: تعداد دوره‌های آموزشی که مدل در طول آن‌ها آموزش می‌بیند. early_stopping_patience: تعداد دوره‌هایی که باید بدون بهبود در معیار انتخابی که اینجا val_loss است، بگذرد تا آموزش متوقف شود. reduce_lr_patience: تعداد دوره‌هایی که باید بدون بهبود در معیار انتخابی بگذرد تا نرخ یادگیری کاهش یابد. early_stopping: یک callback برای متوقف کردن آموزش زودهنگام در صورت عدم بهبود در val_loss. reduce_lr: یک callback برای کاهش نرخ یادگیری در صورت عدم بهبود در val_loss.
epochs = 30

early_stopping_patience = 10

reduce_lr_patience = 3


early_stopping = keras.callbacks.EarlyStopping(

    monitor="val_loss", patience=early_stopping_patience, restore_best_weights=True

)

reduce_lr = keras.callbacks.ReduceLROnPlateau(monitor='val_loss', factor=0.6, patience=reduce_lr_patience)

history = model.fit(

    train_ds,

    validation_data=val_ds,

    epochs=epochs,

    callbacks=[early_stopping, reduce_lr],

)
(در مدل CNN، نام مدل به model_2 و نام history به history_2 تغییر پیدا می کند) خروجی های پروژه:
  • خروجی‌های پیش‌پردازش داده‌ها
پس از حذف فایل های خالی: reviews , label پس از خواندن از فایل‌ها، label ها شامل 0 برای نظرات منفی و 1 برای نظرات مثبت پس از حذف موارد تکراری: unique_reviews, unique_labels پس از مرتب‌سازی بر اساس طول: reviews , label که هم یکتا اند هم مرتب شده بر اساس طول از کوچک به بزرگ تقسیم داده‌ها به مجموعه‌های train و  test: X_train، X_test، y_train، y_test
  • خروجی‌های مدل ها
خروجی مدل RNN (LSTM): summary: شامل معماری و تعداد پارامترهای مدل history: شامل مقدار دقت و خطا در هر ایپاک برای مجموعه‌های test و validation نمودار دقت و خطا در هر ایپاک
  • خروجی مدل CNN (Conv1D):
summary: شامل معماری و تعداد پارامترهای مدل History_2: شامل مقدار دقت و خطا در هر ایپاک برای مجموعه‌های test و validation نمودار دقت و خطا در هر ایپاک   همانطور که مشاهده می شود، دقت در مدل CNN 4 درصد نسبت به RNN بهتر است که نشان از عملکرد بهتر ان روی داده های کمتر این پروژه دارد؛ به دلیل کم بودن داده ها، overfit شدن بدیهی است، اما تلاش شده تا با استفاده از preprocessing و مدل مناسب، مقدار آن کاهش یابد و دقت نسبی قابل قبولی که بتواند بالای 80 درصد باشد، حاصل شود Confusion Matrix برای هر مدل: همانطور که مشاهده می شود به طور کلی در این پروژه که داده های کم داریم، مدل CNN وضعیت را بهتر می کند. (درست است که کلاس 1 را مدل LSTM بهتر تشخیص داده  اما نسبت به کلاس دیگر فاصله چندانی با مدل CNN ندارد و در مدل CNN وضعیت کلاس هم به طور چشمگیری بهتر می شود). در این پروژه دقت مدل RNN به 81 درصد رسیده اما در مدل CNN دقت تا 4 درصد افزایش  یافته و به 85 درصد رسیده است. پس می توان نتیجه گرفت که با توجه به تنظیمات انجام شده در اینجا و تعداد داده های خیلی کم، با مدل CNN می توان به نتیجه نسبتا بهتری رسید. البته انتظار می رود با افزایش تعداد داده ها به طور چشمگیر، دقت در مدل LSTM با توجه به حافظه ترتیبی کوتاه مدت آن، بهتر شود، اما در اینجا با توجه به نتیجه گرفته شده در سطح تعداد داده های کم، CNN (Conv1D) انتخاب بهتری است.