رفتن به نوشته‌ها

از گنجور تا لینوکس

یکی از کارهای جالبی که با شبکه‌های بازگشتی و مدل‌های زبانی انجام شده است، تولید متن یا اصلاحا Text Generation است. ایده‌ای که برای تولید متن به کار می‌رود در بسیاری از اپلیکشن‌ها از جمله تولید نت‌های موسیقی،‌ شبیه‌سازی و تقلید متن‌ و صداهای تولید شده توسط اشخاص مشهور و… می‌تواند مورد استفاده قرار بگیرد. در این پست میخواهیم همین موضوع را در تنسورفلو و کراس با کمک دادگان گنجور (اشعار فارسی) پیاده‌سازی کنیم.

توضیحات تئوری رو در پست آقای کاراپتی (karpathy) میتونید مطالعه بفرمایید و در اینجا صرفا به پیاده‌سازی مدل خواهیم پرداخت. چیزی که قصد داریم پیاده‌سازی کنیم، این است که یک فایل متنی به یک مدل شبکه عصبی عمیق بدهیم و  مدل زبانی آن را در سطح کاراکتر ایجاد کند و سپس به تولید متن بپردازیم. برای پیاده‌سازی از شبکه‌های عصبی بازگشتی (با سلول GRU) استفاده خواهیم کرد.

برای شروع ابتدا تنسورفلو رو نصب کنید. برای آموزش نصب تنسورفلو میتونید به اینجا مراجعه کنید (ترجیحا برای پایتون ۳ رو نصب کنید). پس از اینکه نصب کردید دستور زیر رو در ترمینال بزنید تا از نصب آن اطمینان حاصل کنید:

خب سعی میکنم مدل رو مطابق این پست پیاده‌سازی کنم (به صورت کلاس‌بندی شده و متشکل از ۵ ۶ متد اصلی). همانطور که در پیاده‌سازی شی‌گرا در تنسورفلو به آن اشاره شد معمولا کلاس ما از توابع زیر تشکیل شده است:

که هرکدام:

  • __init__: این تابع وظیفه گرفتن هایپرپارامترهای شبکه مثل تعداد کاراکترهای منحصر به فرد، سایز بردارها، تعداد لایه‌های شبکه و .. به عهده خواهد داشت.
  • _create_input: وظیفه ساختن Iterator مناسب رو برعهده دارد (پیاده‌سازی با استفاده از tf.data انجام خواهد شد).
  • _create_model: این تابع وظیفه ساختن مدل اصلی را برعهده دارد. در این تابع ابتدا بردارهای نهفته (embedding)‌ در سطح کاراکتر را میسازیم و سپس آنها را به یک شبکه بازگشتی با سلول GRU است پاس میدهیم.
  • _create_loss تابع خطا که همانطور در ادامه خواهید دید به یک مساله دسته‌بندی (classification) تبدیل میشه و معمولا از cross entropy استفاده میشود.
  • _create_optimizer: برای بهینه کننده از Adam استفاده میکنیم که نسبت به مابقی معمولا سریع‌تر و نتایج بهتری میدهد.
  • _create_summary برای لاگ کردن و مشاهده نتایج در تنسوربورد.

خوب قسمت اول گرفتن هایپرپارامترهای شبکه است. هایپرپارامترهای که من در نظر گرفتم شامل: کاراکترهای منحصر به فرد، سایز بردارهای نهفته، سایز لایه پنهان GRU، تعداد لایه‌های GRU (در صورتی که بخواهیم شبکه‌های بازگشتی را استک کنیم)، اندازه دسته‌ها (batch size) و حداکثر طول رشته یا اصلاحا sequence length (یعنی یک جمله ما حداکثر چقدر طول دارد از اون بیشتر رو نادیده میگیریم و از اون کمتر رو با صفر جایگزین میکنیم) و در نهایت نرخ یادگیری:

قدم بعدی ساختن ورودی‌های شبکه است یا به عبارت دیگر اینکه چطوری ورودی‌هامون رو به شبکه بفرستیم. همانطور که میدونید ما نمیتونیم مستقیما (به صورت string) کلمات رو شبکه عصبی بدیم و معمولا باید به صورت توالی از اعداد (sequence of integers) یا به صورت تک روشن (one hot) به شبکه بدهیم.

همانطور که میدونید در مدل زبانی (در سطح کاراکتر) قرار است کاراکتر به کاراکتر یکی یکی کلمات را حدس بزنیم. در هنگام آموزش مدل ما باید با توجه کاراکتر قبلی کاراکتر بعدی را حدس بزند و سپس آن را با برچسب درست مقایسه کند. همانطور که میدونید در بحث مدل زبانی عملا ما برچسبی نداریم و تسک به صورت بدون ناظر (unsupervised) انجام می‌شود. در نتیجه به ازای عبارت زیر:

پردازش زبان طبیعی

ابتدا جلمه را کاراکتر به کاراکتر میکنیم و سپس هر کاراکتر را به یک عدد نگاشت میدهیم. یعنی: کاراکتر «پ» معادل عدد ۱۲ میشه، کاراکتر «ر» معادل عدد ۴ میشه و  به همین ترتیب رشته ورودی ما (x) به صورت زیر خواهد شد:

۱۲ ۴ ۳ ۱ ۹ ۷ ۹ ۱۱ ۱ ۱۸ ۲۳ ۱۱ ۳۲ ۲۳ (۳۲)

 شبکه ما باید اینطوری باشه که در هر لحظه (timestamp) با توجه به کاراکترهای قبلی کاراکترهای بعدی را حدس بزند. مثلا توالی ۱۲ ۴ ۳ ۱ ۹ ۷ ۹ ۱۱ ۱ ۱۸ ۲۳ ۱۱ ۳۲ ۲۳ رو دید باید بتواند ۳۲ را حدس بزند. پس برچسب ما (y) به صورت زیر خواهد شد:

۴ ۳ ۱ ۹ ۷ ۹ ۱۱ ۱ ۱۸ ۲۳ ۱۱ ۳۲ ۲۳ ۳۲

همانطور که مشاهده می‌کنید. برچسب‌های ما (y) یک کاراکتر جلوتر از رشته ورودی (x) خواهد بود. همانطور که مشاهده میکنید، عدد ۱۲ تو رشته ورودی وجود ندارد ولی به جای آن ۳۲ قرار دارد. برای اینکه بتونیم از پیکره‌امون همچین توالی رو درست کنیم میتونیم خیلی راحت از یک تابع کمکی استفاده کنیم که به صورت زیر تعریف میشه:

که همانطور که ملاحظه میکنید یک فایل رو توسط کلاس Reader میخونیم. هر کاراکتر رو به یک آیدی منحصر به فرد نگاشت می‌کنیم و سپس تک تک جملات فایل رو به طول‌های ثابت مثلا ۴۰ (sequence length) تقسیم میکنه و در نهایت x و y رو به صورت زیر برمیگرداند:

x: ۱۲ ۴ ۳ ۱ ۹ ۷ ۹ ۱۱ ۱ ۱۸ ۲۳ ۱۱ ۳۲ ۲۳

y: ۴ ۳ ۱ ۹ ۷ ۹ ۱۱ ۱ ۱۸ ۲۳ ۱۱ ۳۲ ۲۳ ۳۲

 

همچنین بد نیست بدونید که کلاس Reader هم وظیفه اش این است که یک فایل ورودی میگره و در نهایت دوتا دیکشنری بهمون برمی گرداند یکی word_to_int_table و یکی هم int_to_word_table. که کلید یکی از دیکشنری‌ها کاراکترهاست و کلید دیگری ایدی متناظر با اون کاراکتر است.

خب برای اینکه این اعداد رو به شبکه عصبی بدیم باید دوتا place_holder تعریف کنیم. یکی برای x و دیگری برای y. در نتیجه در تابع create_input چیزی شبیه به زیر خواهیم داشت:

خب حالا نوبت به ساخت مدل میرسه. در اینجا برای راحتی کار من از embedding استفاده نکردم ولی به راحتی میتونید embedding هم با کمک embedding_lookup پیاده سازی کنید. در اینجا یک شبکه بازگشتی با سلول GRU داریم. همچنین در صورتی که num_layers بیشتر از ۱ باشد چندین سلول  GRU را روی هم استک میکنیم. مثلا اگر تعداد لایه‌ها (num_layers) رو ۲ در نظر بگیریم چیزی شبیه عکس زیر خواهیم داشت (فقط اینجا سلولش LSTM هست):

تعریف شبکه بازگشتی در تنسورفلو به صورت زیر انجام می‌شود:

در ابتدا ورودی‌هامون رو که به صورت یک توالی از اعداد طبیعی بود رو به بردارهای one hot تبدیل میکنیم. بعدش نوع سلول و تعداد لایه‌ها رو مشخص میکنیم. مقدار اولیه بردار وضعیت (state vector) رو با صفر مقداردهی میکنیم. و سپس ورودی‌های one hot رو به dynamic_rnn ارسال میکنیم. که نتیجه این تابع دوتا خروجی به ما برمیگرداند. state و output. بردار وضعیت (state) اون چیزی است که به سلول بعدی میره و output خروجی شبکه بازگشتی است.و اون چیزی که برای ما مهم است خروجی هر مرحله است:

 

در واقع اگر هر خروجی (output[i])  رو از یک دسته‌بندی‌کننده پیشرو  (MLP classifier ) عبور بدهیم میتونیم حدس بزنیم که در اون لحظه (timestep) کاراکتر خروجی چی بوده است. یعنی در شکل بالا باید بتونیم حدس بزنیم که موقعی که x_0 به عنوان ورودی آمد خروجی output_0 بعد از عبور از یک classifier کاراکتر بعدیش چی میتونه باشه. برای اینکه بتونیم هر خروجی رو از MLP classifier عبور بدهیم از تابع tf.layers.dense استفاده میکنیم. این تابع توالی ouput رو میگیره و روی هرکدام MLP classifer رو اجرا میکنه.

قسمت های بعدی تعریف loss و تعریف optimizer است که به راحتی انجام میشود:

تنها موردی که وجود دارد این است که در اینجا من از تابع sparse_softmax_loss استفاده کردم که دیگه بردارهامو تغییر ندم (نخوام اونها رو به one hot تبدیل کنم).

مرحله بعدی نوشتن خلاصه‌ها است. مثلا برای مشاهده loss میتونید summary آن را ذخیره کنید و بدین ترتیب تابع create_summary ما چیزی شبیه زیر خواهد شد:

از merge_all استفاده میکنیم تا تنها با یک summary سروکار داشته باشیم.

و قسمت نهایی آموزش مدل است که احتمالا مشابه‌اشو رو انجام دادید. ورودی‌های تون رو از طریق place_holder ها به شبکه میدید و sess.run رو فراخوانی میکنید. خروجی گراف رو هم ذخیره میکنید تا بعدا اگر خواستید مدل رو لوود کنید نیاز به آموزش دوباره شبکه نداشته باشید.

و قسمت نهایی این پست هم میرسه به بخش inference یا تست مدل که ببینیم شبکه چه چیزی تولید می‌کند:

برای تست با یک جمله تصادفی میتونیم شروع میکنیم که من کاراکتر خالی ” فرستادم. بعد دسته‌ای (batch) به طول یک درست کردم و کاراکتر خالی به رو شبکه فرستادم. شبکه به ازای تمام کاراکترها یک احتمال به ما میدهد (مثلا به ازای کاراکتر «الف» احتمال ۰.۱ میدهد به ازای کاراکتر «ب» احتمال ۰.۲ و… ). با یک توزیع احتمالاتی bionomial محتمل‌ترین کاراکتر را انتخاب میکنم و این کاراکتر رو به انتهای لیست قبلی اضافه میکنم. برای اینکه شبکه وضعیت مرحله قبل رو یادش بمونه از state_vector هم به عنوان ورودی feed_dict میدهیم. این کارو همینجوری تکرار میکنیم مثلا تا ۴۰۰ تا کاراکتر تا جملات مختلفی تولید کنیم.

مدل بالا را روی دادگان گنجور تست کردم و تقریبا نتایج جالبی گرفتم که در پایین برخی از آنها را میتونید ملاحظه بفرمایید:

در iteration ۱ روی بخشی از گنجور‌ (تنها روی اشعار فردوسی) چیزی شبیه زیر تولید شد که نشون میده شبکه چیز خاصی یاد نگرفته است:

 

در iteration ۱۰ که همانطور ملاحظه میکنید توانسته است فاصله را تشخیص دهد و بعضی از کلمات به درستی ایجاد کرده است:

 

پیاده‌سازی کامل این مدل رو میتونید در کراس و تنسورفلو مشاهده کنید.

منتشر شده در پردازش زبان طبیعیتنسورفلویادگیری عمیق

اولین باشید که نظر می دهید

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *