تولید متن توسط مدلهای یادگیری ماشین یکی از روندهای مهم فعلی صنعت پردازش زبان طبیعی است. تلاش برای تولید متن توسط ماشین، سابقهی طولانی دارد. کارپاتی (مدیر هوشمصنوعی تسلا) بود که در سال ۲۰۱۵ و در یک پست وبلاگی معروف، نشان داد میتوان با استفاده از LSTM و مدلهای نسبتا! ساده و در سطح کاراکتر، نتایج قابل قبولی در تولید متن کسب کرد. در این روش، مدل؛ صرفا به ارتباط بین کاراکترها نگاه میکند و احتمال وقوع کاراکتری بعد از کاراکتر دیگر را محاسبه میکند. در زبان فارسی نیز آقای افشین خاشعی همین شیوه را روی اشعار شاهنامه پیادهسازی کرده و نتایج خوبی گرفته است.
با این حال در سالهای اخیر و با توسعه مدلهای غولپیکری مانند GPT-3 شاهد تولید نوشتههایی هستیم که به لحاظ معنایی قادر هستند با متون تولید شده توسط انسان رقابت کنند. این مدلها بسیار پیچیدهتر هستند، بر روی حجم عظیمی از متن تعلیم داده شدهاند و در سطح واژهها (توکنها!) عمل میکنند و نه کاراکترها.
در این آموزش مرحله به مرحله، یک مدل یادگیری ماشین بسیار ساده میسازیم که میتواند شعر رپ فارسی تولید کند. برای ساخت این مدل از فریمورک شناخته شدهی PyTorch و معماری LSTM بهره میبریم. اساس کدهایی که در این آموزش استفاده شده، برگرفته از این مخزن است ولی تغییراتی نیز در آن داده شده است. برای پیادهسازی این مدل صرفا به کتابخانه torch نیاز داریم و چیز دیگری لازم نداریم.
توجه: برای درک کامل این آموزش شما میبایست شناخت اولیه نسبت به یادگیری ماشین داشته باشید و تئوریهای مربوط به شبکههای بازگشتی و مشخصا LSTM را مطالعه کنید. با این حال حتی اگر اولین بار شما است که با این مفاهیم آشنا میشوید، توصیه میکنم کد را روی گوگل کولب اجرا کنید تا درک بهتری از مراحل کار بدست آورید.
اشعار رپ فارسی، برخلاف شاهنامه از انسجام خاصی برخوردار نیستند. هر رپری شیوه خاص خود را دارد و هیچ دیتاست مشخصی هم از این اشعار موجود نیست. مجبور بودم، خودم دست به کار شده و چیزی حدود ۹۰۰۰ مصرع! فارسی از اشعار رپ را کنار هم جمع کنم. هر مصرع طول متفاوتی دارد و کلمات بسیاری دیده میشود که کلمات متداول فارسی نیستند و گاهی ابداع خود رپر هستند و قبلا استفاده نشدهاند. نمونهای از بخشهای دیتاست:
یه روز خوب میاد که ما هم رو نکشیم، به هم نگاه بد نکنیم
با هم دوست باشیم و دست بندازیم رو شونه های هم، آها… مثل بچگیا تو دبستان
هیجکدوممون هم نیستیم بیکار، در حال ساخت و ساز ایران
....
چشا رو به ابراس توی عمقم خود غواص
نوک پا سر بالا دست باز سلامتی هر کسی تنهاست
دنبال اسم در کردن لا دخیاس یارو
کار خفنش اینه دیشب توی پاسگاه بود
...
مطمئنا مدل، برای یادگیری کار سختی در پیش دارد ولی ما سعیمان را میکنیم!
قصد ندارم تمیزکاری عجیب و غریبی روی این دیتاست اعمال کنم. امیدوارم که مدل نهایی قادر باشد از تولید کاراکترهایی که متداول نیستند خودداری کند. دیتاست خود را خط به خط میخوانم، مصرعهایی که بیشتر از پنج کاراکتر دارند را نگه میدارم (چرا پنج؟) و صرفا با هضم آنها را نورمالایز میکنم.
دیتالودر بخش مهمی از کار ما را تشکیل میدهد. وظیفهی این کلاس آماده کردن دیتا برای ورود به مدل است. اینکه ما چگونه دیتا را آماده میکنیم نقش کلیدی در خروجی کار ما خواهد داشت.
هر مصرع را به کاراکترهای جدا از هم تبدیل میکنم.
در واقع مدل من باید بیاموزد که بعد از هر کاراکتری کدام کاراکتر را پیشبینی کند.
طبق تصویر بالا یک آرایهی دو بعدی میسازیم. تعداد ستونهای این آرایه پارامتری است که خودمان از پیش آن را انتخاب کردیم. برای مثال اگر جملهی یه روز خوب میاد را در نظر بگیرید. این جمله به آرایهی زیر تبدیل میشود:
سپس تمام کاراکترها را به عدد تبدیل میکنم. از صفر شروع میکنم و به تعداد کاراکترهای منحصر بفردی که داریم عدد اختصاص میدهیم. مثلا 4 را برای و اختصاص میدهم و هرجا که و ببینم 4 جایگزین میکنم. این آرایه اصلی من است که برای مدل ارسال میکنم و مدل از آن ترتیب رخ دادن کاراکترها را یاد میگیرد.
ولی X و Y مدل من دقیقا کدام آرایهها هستند؟ آرایهی ورودی و آرایهی مورد هدف مدل چیست؟ همانطور که بالاتر نوشتم انتظار دارم که مدل با دیدن هر کاراکتر، کاراکتر بعدی را حدس بزند. برای مثال ردیف اول را در نظر بگیرید. ورودی و خروجی مدل من باید یک ایندکس تفاوت داشته باشند. آرایهی INPUT همیشه یک ایندکس عقبتر از آرایه TARGET است. یعنی مدل با دیدن کاراکترهای قبلی یاد میگیرد که کاراکتر بعدی چیست.
این قطعه کد همه این کارها را برای ما انجام میدهد. توجه کنید که من CharDataset را از کلاس Dataset به ارث میبرم. در واقع همه متغیرها و توابع کلاس Dataset را دارم ولی سه تابع __init__ و __len__ و __getitem__ را بازنویسی (overwrite) میکنم. برای آشنایی با شیوهی کار این کلاس، مستندات پایتورچ را مطالعه کنید.
تنظیم ابعاد آرایهها، شاید سختترین بخش درک نحوهی کار این آموزش باشد. دوباره پیشنهاد میکنم برای درک بهتر، کد را خودتان اجرا کنید و ابعاد تک تک آرایهها را به دقت بررسی کنید. جزییات زیادی در این بخش وجود دارد که من از ورود به تک تک آنها خودداری میکنم.
از مدل سادهای استفاده میکنم که سه لایه اصلی دارد. لایهی اول یک One-Hot انکودینگ است. لایهی دوم LSTM ما است و لایهی سوم یک لایهی خطی ساده است.
مدل CharRNN تمام متغیرها و توابع nn.Module از پایتورچ را به ارث میبرد. با این تفاوت که توابع __init__ و forward در آن بازنویسی میشوند.
حال که هم معماری مدل را ساختیم و هم دادههایمان را سروسامان دادیم، باید تابعی داشته باشیم که وظیفهی یاد دادن به مدل را بر عهده دارد. در هر epoch و برای هر batch این تابع را فراخوانی خواهیم کرد تا مدل را آموزش دهد.
تابع تولید کننده نقشی در یادگیری مدل ندارد. صرفا به ارزیابی عملکرد مدل کمک میکند. این تابع با گرفتن یک رشتهی متنی، ادامهی آن را تولید میکند. این رشتهی متنی میتواند یک حرف، کلمه و یا صرفا یک کاراکتر باشد. ابتدا رشتهی متنی اولیه را به مدل میدهیم و سپس به تعداد دلخواه (predict_len) کاراکتر پیشبینی میکنیم. متغیر output_dist در این تابع، یک آرایه، باندازهی تمام حروف استفاده شده در دیتاست است. در این آرایه احتمالا وقوع هر کاراکتر بعد از دیدن کاراکترهای قبلی را میبینیم و با استفاده از torch.multinomial ایندکس کاراکتری را انتخاب میکنیم که احتمال وقوع آن از بقیه بیشتر است. دیکشنری itos هم با دریافت ایندکس هر کاراکتر آن را تبدیل به کاراکتر واقعی کرده و به ما برمیگرداند.
تقریبا همه ابزارهای لازم برای شروع یادگیری را در اختیار داریم. پارامترهای اولیه مدل را تعریف میکنیم. مقدار سِلهای داخلی LSTM (و یا همان hidden_size) و تعداد لایههای LSTM که روی هم سوار میکنیم (n_layers) در خروجی کار اهمیت بیشتری دارند. با این حال تنظیم بقیه هایپرپارامترها هم میتواند به یک خروجی خوب (بالاخص پرهیز ار overfitting) کمک کند.
در این مرحله با فراخوانی کلاس CharRNN، مدل را با پارامترهای مدنظر میسازیم. n_characters تعداد منحصربفرد کاراکترهای موجود در دیتاست ما است. خروجی مدل برای هر کاراکتر یک آرایه به اندازه تمام کاراکترها است.
برای بهینه کردن گرادینتها از Adam استفاده میکنیم که اغلب میتوان از آن نتیجهی بهتری گرفت. CrossEntropy را هم بعنوان نوع Loss انتخاب میکنیم.
در نهایت یک حلقه از تعداد epochها میسازیم و دادهمان را از طریق دیتالودر برای مدل ارسال میکنیم. بعد از هر ۲۰۰ batch که برای مدل فرستادیم، مقدار Loss را پرینت میکنیم. همچنین تابع generate را با جمله آغازین "یه روز خوب" فراخوانی میکنیم تا یک ایدهی کلی نسبت به روند یادگیری مدلمان داشته باشیم و در نهایت بعد از هر ۵ دور کامل یادگیری، وزنهای مدل را ذخیره میکنیم.
روند یادگیری ممکن است طولانی باشد. بهترین راهحل برای سریعتر کردن این روند استفاده از GPU روی Google Colab است. همانطور که در بین خطهای کد هم دیدید، در بخشهای از cuda استفاده کردیم. با فراخوانی کودا، آرایهها را به GPU منتقل میکنیم و سرعت روند یادگیری افزایش مییابد.
خوب! نتیجه خیلی فوقالعاده نیست ولی از نظر من بد هم نیست. اگر سطح انتظار را کمی پایین بیاوریم میتوان گفت که مدل درست کار میکند و میتواند ساختار کلمات را بفهمد و آنها را کنار هم قرار دهد. ولی اگر بخواهیم سختگیری کنیم باید بپرسیم آیا انسجام کل متن حفظ شده؟ و یا اصلا خروجی قابل فهم است؟ چند نمونه از خروجیها را باهم ببینم:
یه روز خوب میاد، این درد شیر
تو فرض کن
واسه اینکه خسته مثل سخته
اگه با هم دیگه زندگی به مادری به من باش از خونه باشیم
من خنده میخوابم مثل اسبی که با ما چندان از آسمون مثل سربازن همه چی هست در میان، هیچ مغزی
نمیخواد که من مثل تو میدونم
شمارم باشی که تو از نبود و من باشیم
که میگه با هم تو این سرابه میخوابم
یه روز خوب میاد، یه روز خوب میاد، این دلو ببینم
فکر کن بالاترین شاده بی تو میدونم
وقتی که زیرزنی هم نیستیم دیدم یه روز خوب میاد، این آخرین بالاسره
همه چی فکر میشه بالا بهتره، میخندم
در نهایت به نظر من جالب است که LSTM میتواند صرفا با دیدن کاراکترها، آن هم روی یک دیتاست کوچک و محدود، ارتباط بین کلمهها و حتی در برخی مواقع Context را متوجه شود. (این را هم در نظر بگیرید که بسیاری از شعرهای رپ فارسی که ما آدمها نوشتهایم هم، قابل درک نیستند!)
ولی برای بهتر کردن این نتایج چه کارهای دیگری میتوانیم انجام دهیم؟
هرچند LSTMها از قدرتمندترین مدلهای موجود هستند ولی پس از سال ۲۰۱۸ و معرفی ترنسفورمرها، از محبوبیت کمتری در تسکهای تولید متن برخوردارند. یکی از قدرتمندترین مدلها بر اساس معماری ترنسفورمر، GPT است. معماری GPT با پارامترهای مختلف و توسط شرکتهای مختلف در دیتاستهایی به ابعاد متنوع آموزش دیده است. برخی از این مدلها بصورت متن باز و برخی در انحصار شرکتهای مختلف در دسترس قرار دارند. دیتاست اغلب این مدلها، دیتاستهای چندزبانه است. با این حال بدیهی است که بیشتر دیتای موجود در آنها به زبان انگلیسی باشد.
میتوانیم مدل GPT-3 با ۱۷۰ میلیارد پارامتر که توانایی تشخیص زبانهای مختلف دارد را روی پلتفرم OpenAI برای تولید شعر رپ فارسی آزمایش کنیم. نتیجه کار فوقالعاده است! با وجود اینکه دیتاست GPT-3 شناخت چندانی از محتوای فارسی ندارد ولی میتواند یک شعر رپ خوب درباره پیتزا و اسپاگتی بسراید که با یه روز خوب میاد شروع میشود!
همچنین مدل GPT-3 با تعداد ۲۰ میلیارد پارامتر که توسط eleuther.ai توسعه داده شده است را میتوانیم روی goose.ai آزمایش کنیم. انتظار نداریم این مدل به خوبی مدل openAI باشد با این حال به خوبی میتواند ساختار شعر را متوجه شود. هرچند احتمالا به دلیل دیتاست کوچکتری که روی آن آموزش دیده، خلاقیت کمتری دارد!