آموزش شبکه عصبی بازگشتی بخش پنجم : معرفی LSTM

4 536

بسم الله الرحمن الرحیم

در بخش قبل با شبکه عصبی بازگشتی GRU آشنا شدیم و در این بخش به معرفی شبکه عصبی بازگشتی LSTM میپردازیم.

پیشتر(۱و۲و۳) دیدیم که یک شبکه عصبی بازگشتی سنتی (اگر به اندازه کافی بزرگ باشد) از نظر تئوری باید قادر به تولید دنباله هایی با هر پیچیدگی ای باشد اما در عمل مشاهده میکنیم که این شبکه در ذخیره سازی اطلاعات مرتبط با ورودی های گذشته به مدت طولانی ناتوان است.(منبع) علاوه بر اینکه این خصصیه توانایی این شبکه در مدل سازی ساختارهای بلند مدت را تضعیف میکند، این “فراموشی” باعث میشود تا این نوع از شبکه ها در زمان تولید دنباله در معرض ناپایداری قرار گیرند. مشکلی که وجود دارد(که البته در تمامی مدلهای تولیدی شرطی نیز متداول است) این است که اگر پیش بینی های شبکه تنها وابسته به چند ورودی اخیر باشد و این ورودی ها خود نیز توسط شبکه تولید شده باشند، شانس بسیار کمی برای تصحیح و جبران اشتباهات گذشته توسط شبکه وجود دارد.

داشتن یک حافظه بلند مدت تر دارای اثر تثبیت کننده است چرا که حتی اگر شبکه نتواند از تاریخچه اخیر خود درک صحیحی پیدا کند، باز با این وجود قادر است با نگاه در گذشته پیش بینی خود را کامل کند. مشکل ناپایداری بطور ویژه در زمان مواجه با داده اعشاری وخیم میشود چرا که پیش بینی ها میتوانند از مانیفولدی که داده های آموزشی بر روی آن قرار گرفته اند فاصله بگیرند. یک راه حل که برای مدلهای شرطی مطرح شده است تزریق نویز به پیش بینی های صورت گرفته توسط شبکه قبل از تغذیه آنها به گام زمانی بعدی است.(منبع) این کار باعث تقویت شبکه در قبال ورودی های غیرمنتظره میشود. با این وجود اما یک حافظه بهتر، راه حل به مراتب بهتر و تاثیرگذار تری است. حافظه طولانی کوتاه مدت یا به اختصار LSTM یک معماری شبکه عصبی بازگشتی است که برای ذخیره سازی و دسترسی بهتر به اطلاعات نسبت به نسخه سنتی آن طراحی شده است.

برخلاف شبکه عصبی بازگشتی سنتی که در آن محتوا در هر گام زمانی از نو بازنویسی میشود در یک شبکه عصبی بازگشتی LSTM شبکه قادر است نسبت به حفظ حافظه فعلی از طریق دروازه های معرفی شده تصمیم گیری کند. بطور شهودی اگر واحد LSTM ویژگی مهمی در دنباله ورودی در گام های ابتدایی را تشخیص دهد بسادگی میتواند این اطلاعات را طی مسیر طولانی منتقل کند بنابر این اینگونه وابستگی های بلندمدت احتمالی را دریافت و حفظ دارد.

همانطور که قبلا بطور مختصر اشاره کرده بودیم واحد حافظه طولانی کوتاه مدت (Long Short-Term Memory) ابتدا توسط هوخرایتر و اشمیت هوبر در سال ۱۹۹۷ معرفی شد. از آن زمان به بعد تغییرات جزئی در LSTM ایجاد شده است. مبانی که از آن در رابطه با آموزش و پیاده سازی این نوع شبکه ها مطرح میشود برگرفته از مقاله ای تحت عنوان Generating Sequences WithRecurrent Neural Networks  در سال ۲۰۱۳ است.

شبکه LSTM در مقاله اصلی بصورت زیر معرفی شده است :
برخلاف شبکه عصبی بازگشتی سنتی که صرفا جمع متوازن سیگنالهای ورودی را محاسبه کرده و سپس از یک تابع فعالسازی عبور میدهد هر واحد LSTM از یک حافظه C_{t} در زمان t بهره میبرد. خروجی h_t و یا فعالسازی واحد LSTM بصورت h_{t} = \Gamma_o . tanh(C_t) است که در آن \Gamma_o دروازه خروجی است که کنترل کننده میزان محتوایی است که از طریق حافظه ارائه میشود. دروازه خروجی از طریق عبارت \Gamma_o = \sigma(W_o . [ h_{t-1}, X_{t} ] + b_o) محاسبه میشود که در آن \sigma تابع فعالسازی سیگموید است. W_o نیز یک ماتریس اوریب است. سلول حافظه C_t نیز با فراموشی نسبی حافظه فعلی و اضافه کردن محتوای حافظه جدید بصورت \hat{C}_{t} بصورت C_{t} = \Gamma_{f} . C_{t-1} + \Gamma_u . \hat{C}_{t} بروز رسانی میشود که در آن محتوای حافظه جدید از طریق عبارت \hat{C}_{t} = tanh(W_C . [ h_{t-1}, X_{t} ] + b_c) بدست می آید. آن میزان از حافظه فعلی که باید فراموش شود توسط دروازه فراموشی \Gamma_f کنترل میشود و آن میزانی از محتوای حافظه جدید که باید به سلول حافظه اضافه شود توسط دروازه بروزرسانی (یا بعضا به دروازه ورودی معروف است) انجام میگیرد. این عمل با محاسبات زیر صورت میگیرد :
\Gamma_f = \sigma(W_f . [ h_{t-1}, X_{t} ] + b_f)
\Gamma_u = \sigma(W_u . [ h_{t-1}, X_{t} ] + b_u)

اما همه این مفاهیم به چه معناست؟ اجازه دهید بصورت دقیق همه موارد را با هم مرور کنیم تا به درک بهتری برسیم.

توضیح مفاهیم

برای اینکار قبل از اینکه به توضیح بیشتر شبکه LSTM بپردازیم اجازه دهید یکبار مشکل اصلی را با هم مرور کنیم. اولین تصویری که در زیر مشاهده میکنید تصویر یک شبکه عصبی بازگشتی استاندراد است. همانطور که در تصویر مشخص است در هر گام زمانی ما دو ورودی دریافت میکنیم و دو خروجی نیز میتوانیم تولید کنیم. در اینجا مشاهده میکنیم که هیچ مکانیزمی جهت انتقال یک ویژگی از گام های ابتدایی به گام های انتهایی وجود ندارد. مکانیزمی هم جهت مقابله با مبحث محوشدگی گرادیان وجود ندارد!

چرا این حرف را میزنیم؟ از کجا متوجه شدیم ؟

پیشتر در آموزشهای قبلی (بطور خاص در مبحث GRU )دیدیم که اگر شبکه در ابتدای دنباله ویژگی مهمی را بدست آورد در یک شبکه عصبی سنتی قادر به انتقال آن به گام های بعدی نیست. اما چرا نیست؟ عملا مشاهده میکنیم که در هر گام زمانی محتوای هر سلول (یا همان گام زمانی) با مقادیر جدید از گام زمانی قبل و ورودی جدید جایگزین میشوند و بردار حافظه نهان ما اینطور تنها برای چندگام زمانی بسیار محدود اخیر قابلیت تاثیرگذاری دارد(یعنی تاثیرگذاری صرفا محلی و محدود به چند گام زمانی اخیر است). این مهم را در تصویر فوق مشاهده میکنیم. همچنین ما قبلا گفته بودیم که از نظر تئوری میتوان گفت این شبکه باید قادر به این کار باشد یعنی باید بتواند با هر دنباله ای با هر طولی کار کند! اما عملا چنین چیزی اتفاق نمی افتد. چرا؟ دلیل این امر این است که چیزی برای تحمیل این مساله وجود ندارد. چیزی در مکانیزمهای تعبیه شده در شبکه وجود ندارد که شبکه را به سمت حفظ یک ویژگی از گام های زمانی قبلی و ندید گرفتن ورودی های جدید وادار کند! (هرچند از نظر تئوری نیز این کار بصورت بی نقض قابل انجام نیست مگر اینکه تغییراتی در تابع فعالسازی نیز رخ دهد).
در نسخه های بهبود یافته همانند GRU و (بزودی LSTM ) میبینیم که ما با اعمال محدودیت در آزادی پارامترها (با قرار دادن دروازهای جدید) در فرایند بهینه سازی این قابلیت را به شبکه ارائه میکنیم.
اجازه دهید یک شبکه عصبی LSTM را با هم ببینیم و این بحث را بیشتر باز کنیم.
در زیر ما شاهد یک شبکه عصبی بازگشتی LSTM هستیم. :
روابط مطرح شده همانطور که میبینید بصورت زیر هستند :

\hat{C}_{t} = tanh(W_C . [ h_{t-1}, X_{t} ] + b_c)

C_{t} = \Gamma_f . C_{t-1} + \Gamma_u . \hat{C}_{t}

\Gamma_f = \sigma(W_f . [ h_{t-1}, X_{t} ] + b_f)

\Gamma_u = \sigma(W_u . [ h_{t-1}, X_{t} ] + b_u)

\Gamma_o = \sigma(W_o . [ h_{t-1}, X_{t} ] + b_o)

h_{t} = \Gamma_o . tanh(C_t)

اما این روابط به چه معنا هستند و چگونه به رفع محدودیتی که از آن صحبت کردیم کمک میکنند؟

در شبکه عصبی LSTM ما با مفاهیم جدیدی مواجه میشویم که در شبکه عصبی بازگشتی سنتی وجود نداشتند. در این شبکه اصطلاحا سه دروازه یا gate وجود دارد که از طریق آن شبکه نسبت به کنترل جریان داده درون خود اقدام میکند.
این سه دروازه عبارتند از :
  • دروازه نسیان یا فراموشی (Forget gate)
  • دروازه بروزرسانی (Update gate) (به دروازه ورودی یا Input gate هم معروف است)
  • و دروازه خروجی (Output gate)
علاوه بر این سه دروازه یک سلول حافظه نیز وجود دارد که از آن اصطلاحا به Memory Cell یا به اختصار C یاد میشود. این ها مفاهیم تازه در این شبکه هستند و شبکه علاوه بر این ۴ مفهوم جدید دارای یک ورودی از حافظه پنهان یا همان h و ورودی یا همان X نیز بهره برده و دو خروجی تولید میکند(یک خروجی C_tو خروجی دیگر h_t است که خود به دو بخش تقسیم میشود بخشی به گام زمانی بعد منتقل شده و بخشی نیز در صورت نیاز به تولید خروجی در گام زمانی فعلی مورد استفاده قرار میگیرد.)
نکته:
لازم به ذکر است تمامی دروازه های جدید دارای اندازه یکسان با بردار حافظه مخفی (h) هستند. همانطور که در روابط مشخص است در اینجا بر خلاف GRU بردار حالت پنهان h با بردار حافظه C یکسان نیست! و این دو کارکردهای متفاوتی دارند.

خب حالا چطور این دروازه ها محدودیتهایی که صحبتش را کردیم مرتفع میسازند؟

دروازه فراموشی یا همان Forget gate که در عبارات بالا بصورت \Gamma_f نمایش داده شده است، وظیفه کنترل جریان اطلاعات از گام زمانی قبلی را دارد. این دروازه مشخص میکندآیا اطلاعات حافظه از گام زمانی قبل مورد استفاده قرار گیرد یا خیر و اگر بایداز گام زمانی قبل چیزی وارد شود به چه میزان باشد.
دروازه بروزرسانی یا همان Update gate که در عبارات بالا بصورت \Gamma_u نمایش داده شده است، وظیفه کنترل جریان اطلاعات جدید را بر عهده دارد. این دروازه مشخص میکند آیا در گام زمانی فعلی باید از اطلاعات جدید مورد استفاده قرار گیرد یا خیر و اگر بلی به چه میزان. از این دروازه عموما به دروازه ورودی نیز یاد میشود.
دروازه خروجی یا همان Output gate که در عبارات بالا بصورت \Gamma_o نمایش داده شده است، نیز مشخص میکند چه میزان از اطلاعات گام زمانی قبل با اطلاعات گام زمانی فعلی به گام زمانی بعد منتقل شود.
وجود این دروازه ها به این شکل است که مکانیزم کنترلی بسیار دقیقی را ایجاد میکند. حالا اجازه دهید با یک مثال کمی این مفاهیم را واضح تر بیان کنیم :

دروازه فراموشی :

فرض کنید ما چند کلمه از یک متن را از ورودی میخوانیم و میخواهیم از یک LSTM برای چک و کنترل ساختار گرامر استفاده کنیم (مثلا ببینیم آیا فاعل مفرد است یا جمع ). اگر فاعل از مفرد به جمع تغییر پیدا کرد(یا بلعکس) ما باید راهی پیدا کنیم تا مقدار ذخیره شده قبلی در حافظه را با حالت جدید تعویض کنیم. در LSTM این کار از طریق دروازه forget بصورت زیر انجام میشود:
\Gamma_f = \sigma(W_f . [ h_{t-1}, X_{t} ] + b_f)
در اینجا W_f ماتریس وزنی است که رفتار دروازه فراموشی را کنترل میکند. در بخش قبل دیدیم که برای سادگی کار چطور بردارهای  X_{t} و h_{t-1} را با هم ترکیب میکنیم و در یک عملیات آنها را شرکت میدهیم. (برای مرور اینجا را ببینید). اگر ما عملیات فوق را انجام دهیم چون از تابع فعالسازی سیگموید استفاده میکنیم نتیجه برداری بنام \Gamma_f خواهد بود که مقادیری بین ۰ و ۱ خواهد داشت. این بردار سپس در عبارت بعدی در C_{t-1} ضرب خواهد شد. بنابر این اگر مقادیر بردار دروازه فراموشی \Gamma_f صفر باشد(یا به سمت صفر میل کند) عملا به معنای در نظر نگرفتن محتوای C_{t-1} است. به عبارت ساده تر یعنی شبکه اطلاعات ارائه شده توسط C_{t-1} را دور انداخته و هیچ توجهی به آن نمیکند. به همین صورت اگر مقادیر بردار \Gamma_f ۱ باشد این اطلاعات توسط شبکه حفظ میشود.مقادیر مابینی نیز موجب میشود شبکه به همان میزان از محتوای ارائه شده از گام زمانی قبل استفاده کند(یعنی بخشی را دور ریخته و از بخش دیگر استفاده کند).

دروازه بروزرسانی:

حالا بعد از اینکه با موفقیت فراموش کردیم که فاعل ما مفرد است(یعنی مقادیر قبلی حافظه که اشاره به مفرد بودن فاعل داشت را پاک کردیم)، نیاز داریم تا راهی پیدا کنیم تا نشان دهیم که الان فاعل جمع است (و دیگر مفرد نیست)(یعنی در ورودی ما با فاعل جمع سرو کار داریم (داده الان ما فاعلش جمع است!). اینجا از دروازه بروزرسانی استفاده میکنیم که بصورت زیر محاسبه میشود :
\Gamma_u = \sigma(W_u . [ h_{t-1}, X_{t} ] + b_u)
حالا برای بروزرسانی فاعل جدید، ما نیاز به یک بردار جدید داریم که بتوانیم آنرا با حالت قبلی حافظه جمع کنیم پس برای اینکار بصورت زیر عمل میکنیم. ابتدا بردار جدیدی که صحبتش را کردیم بصورت زیر ایجاد میکنیم :

\hat{C}_{t} = tanh(W_C . [ h_{t-1}, X_{t} ] + b_c)

و در آخر هم حافظه را بروز رسانی میکنیم :

C_{t} = \Gamma_{f} . C_{t-1} + \Gamma_u . \hat{C}_{t}

در عبارت فوق بخش ابتدایی مشخص کننده این است که چه میزان اطلاعات از بخش قبل(حافظه از گام زمانی قبل) استفاده شود و بخش دوم حاوی اطلاعات جدید است که مورد استفاده قرار میگیرد.

دروازه خروجی :

در انتها نیز برای اینکه مشخص کنیم در خروجی از چه محتوایی باید استفاده کنیم از دروازه خروجی بهره میبریم. شیوه کار بصورت زیر است :

\Gamma_o = \sigma(W_o . [ h_{t-1}, X_{t} ] + b_o)

h_{t} = \Gamma_o . tanh(C_t)

 

نکته

تصویر و عملیات اصلی LSTM برابر آنچه در مقاله اصلی آمده است بصورت زیر است.

همانطور که مشاهده میکنید تصویر ارائه شده ما پیچیدگی کمتری دارد و اکنون شما باید براحتی بتوانید نمادها و عملیاتهای رخ داده در زیر را درک کنید.

i_t = \sigma (W_1.X_t + W_2.h_{t-1} + W_3.c_{t-1} + b_i)

f_t = \sigma (W_4.X_t + W_5.h_{t-1} + W_6.c_{t-1} + b_f)

c_t = f_t.c_{t-1} + i_t.tanh(W_7.X_t + W_8.h_{t-1} + b_c)

o_t = \sigma (W_9.X_t + W_10.h_{t-1} + W_11.c_t + b_o)

روابط فوق بصورت زیر نیز ارائه میشوند :

i_t = \sigma (W_{xi}.X_t + W_{hi}.h_{t-1} + W_{ci}.c_{t-1} + b_i)

f_t = \sigma (W_{xf}.X_t + W_{hf}.h_{t-1} + W_{cf}.c_{t-1} + b_f)

c_t = f_t.c_{t-1} + i_t.tanh(W_{cx}.X_t + W_{ch}.h_{t-1} + b_c)

o_t = \sigma (W_{xo}.X_t + W_{ho}.h_{t-1} + W_{co}.c_t + b_o)

همانطور که از شیوه نامگذاری در عبارات فوق بر می آید \sigma تابع سیگموید بوده و i f o و c به ترتیب بردارهای مربوط به دروازه ورودی(یا همان دروازه بروزرسانی)، دروازه فراموشی، دروازه خروجی و سلول حافظه میباشند که همگی آنها دارای اندازه یکسان و برابر با اندازه بردار حالت مخفی h هستند. اندیس ماتریسهای وزن نیز به آنچه که از نام آنها بر می آید اشاره میکنند بعنوان مثال W_{hi} به معنای ماتریس وزن مربوط به ضرب حالت پنهان با بردار دروازه ورودی است، به همین شکل W_{xo} نیز به معنای ماتریس وزن مربوط به ضرب  بردار دروازه ورودی با بردار دروازه خروجی است و همینطور الی آخر. ماتریس های وزن مربوط به سلول حافظه و بردار دروازه (مثل W_{ci} بصورت مورب (diagonal) هستند بنابر این درایه m ام در هر بردار دروازه فقط ورودی از درایه m ام بردار سلول حافظه را دریافت میکند. عبارات بایاس (که با i,f,c و o به ترتیب مشخص شده اند) برای وضوح بیشتر حذف شده اند. الگوریتم LSTM اصلی از یک روش خاص برای محاسبه گرادیان استفاده میکرد که به آن اجازه میداد وزنها بعد از هر گام زمانی بروزرسانی شوند. اما در اینجا گرادیان ها از طریق “الگوریتم پس انتشار خطا در زمان” محاسبه میشوند. یک مشکل که در زمان آموزش LSTM خود را نمایش میدهد این است که مقادیر گرادیان ها بشدت بزرگ میشوند که در نتیجه این مساله باعث رخداد خطا میشود(سر ریز و..) برای جلوگیری از این مشکل از روش gradient clipping بهره برده میشود تا اینطور مقادیر آن در گستره از پیش مشخص شده قرار گیرند.

گفتیم شبکه LSTM محبوب ترین راه حل برای رفع مشکل محو شدگی گرادیان است. اکنون بعد از مشاهده و درک مکانیزم های بالا باید به دلیل این مهم پی برده باشید. مکانیزم های طراحی شده دو نقش اساسی در اینجا ایفا میکنند :
  • اول اینکه هر واحد براحتی قادر است یک ویژگی خاص در جریان ورودی را در صورت مهم تلقی شدن برای گام های زمانی طولانی بعدی بیاد داشته باشد. هر ویژگی ای که توسط دروازه بروزرسانی GRU یا دروازه فراموشی LSTM مهم تشخیص داده شود بدون اینکه رونویسی شود به همان حالت باقی مانده و منتقل میشود.
  • نکته دوم و شاید مهمتر اینکه این مکانیزم های جدید در عمل مسیرهای میانبری ایجاد میکنند که چندین گام زمانی را ندید گرفته و پشت سر میگذارند (از روی چندین گام زمانی میپرند) این میانبرها به همین صورت به خطای تولیدی اجازه میدهد تا بدون آنکه خیلی سریع محو شود براحتی در فاز پس انتشار منتقل گردد و اینطور معضلات مرتبط با گرادیان های محو شونده کاهش میابد.
نکته :
شبکه های بازگشتی LSTM رخداد محوشدگی گرادیان در آنها کمتر است همچنین در نظر داشته باشید که امکان وقوع مشکل انفجار گرادیان کماکان در این نوع شبکه ها وجود دارد و برای برطرف سازی آن از روشهایی که پیشتر مطرح کردیم(اینجا کلیک کنید) استفاده میشود.
تاریخچه تغییرات در معماری LSTM

شبکه عصبی بازگشتی LSTM از بدو شروع تا به امروز دستخوش تغییرات مختلفی شده است. نسخه اولیه LSTM که در سال ۱۹۹۷ توسط سپ هوخرایتر و استاد او یورگن اشمیت هوبر ارائه شد با نسخه ای که امروزه ازآن استفاده میکنیم بسیار متفاوت است. نسخه اولیه LSTM تنها شامل واحد حافظه، دروازه بروزرسانی (یا دروازه ورودی) و دروازه خروجی بود. در این نسخه از چرخه خطای ثابت (یا اطلاحا CEC که مخفف Constant Error Carousel ) برای جلوگیری از محوشدگی و انفجار گرادیان بهره برده شد . در این نسخه از نسخه سفارشی شده الگوریتم پس انتشار کوتاه شده در زمان استفاده شد.

دو سال بعد در سال ۱۹۹۹ فلیکس پرز و استاد او یورگن اشمیت هوبر و فرد کامینس در مقاله ای تحت عنوان Learning to forget: continual prediction with LSTM دروازه نسیان(یا فراموشی) را که به (دروازه نگهداری هم معروف بود) به معماری LSTM اضافه کردند. علاوه بر این تغییر، حذف تابع فعالسازی خروجی نیز از دیگر تغییرات صورت گرفته توسط ایشان بود. در این نسخه از ترکیبی از الگوریتم پس انتشار کوتاه شده در زمان و نسخه سفارشی شده الگوریتم Real Time Recurrent Learning (به اختصار RTRL) استفاده شد.علت افزودن دورازه نسیان در مقاله ایشان اینطور عنوان شده است :”یکی از نقاط ضعف شبکه های LSTM پردازش جریان های ورودی مستمر بدون انتهای مشخص است. بدون بازنشانی، مقادیر حالت درونی ممکن است بشدت بزرگ شده و نهایتا باعث از کار افتادگی شبکه شوند. راه حل ما استفاده از دروازه فراموشی تطبیق پذیری است که به سلول LSTM اجازه فراگیری بازنشانی خود در زمان های مناسب را میدهد تا اینطور منابع داخلی را آزاد کند.”

در سال ۲۰۱۴ هم نسخه ساده تری از LSTM بنام واحد بازگشتی دروازه ای یا همان Gated Recurrent Unit یا به اختصار GRU توسط چو و همکاران معرفی شد که سربار را کاهش و کارایی نزدیک به LSTM (و در بعضی موارد بهتر از آن) ارائه داد.

 

پیاده سازی :
در بخش بعد :
تا به اینجا کلیات شبکه های عصبی بازگشتی را یاد گرفتیم . در ادامه انشاءالله وارد جزییات بیشتر این شبکه ها میشویم. این اطلاعات بخشی در زمان ارائه مثالها و بخشی تحت مطالب جداگانه ارائه خواهند شد.
4 نظرات
  1. صابر می گوید

    با سلام و تشکر از زحماتی با ارزشی که می کشید
    پیشنهادی داشتم اینکه فونت مطالب رو تغییر دهید مثل Samim که توی سایت dotnettips.ir هستش یا فونت ایران سنس
    اینجوری خوندنش آسونتر میشه
    بازم ممنونم از تمامی زحماتی که بابت آموزش این مطالب کمیاب توی زبان فارسی

    1. سید حسین حسن پور متی کلایی می گوید

      سلام
      خیلی ممنونم هم از نظر لطفتون و هم از پیشنهادت خوبتون. حتما در نظر میگیرم.
      قالب سایت قراره تغییر بکنه و حتما فونت پیشنهادی شما رو هم تست میکنم ببینم اگر در قالب جدید خوب نمایش داده میشه انشاءالله استفاده میکنیم.

      1. صابر فرهادی می گوید

        سلام
        خیلی خوب شد خدا قوت

        1. سید حسین حسن پور متی کلایی می گوید

          سلام.
          خیلی ممنون از پیشنهاد خوب شما که من رو با این فونت آشنا کردید!

ارسال یک پاسخ

آدرس ایمیل شما منتشر نخواهد شد.

این سایت از اکیسمت برای کاهش هرزنامه استفاده می کند. بیاموزید که چگونه اطلاعات دیدگاه های شما پردازش می‌شوند.