مقدمه‌ای بر شبکه‌های عصبی بازگشتی - جلسهٔ چهارم

در جلسه‌ی چهارم از دوره‌ی «مقدمه‌ای بر شبکه‌های عصبی بازگشتی (RNN)» به بررسی روش‌های استفاده از یک شبکه‌ی عصبی بازگشتیِ آموزش داده‌شده برای پیش‌بینی داده‌هایی با طول دنباله‌های ورودی دیگری (گام‌های زمانی متفاوت) می‌پردازیم.کدهای مرتبط این جلسهروش اول:‎02_1_simple-RNN-diffrent-sequence-length.ipynbمشاهده:‌ باز کردن نوت‌بوک در NBViewerاجرا:‌ باز کردن نوت‌بوک در Google Colabروش دوم:‎02_2_simple-RNN-diffrent-sequence-length.ipynbمشاهده:‌ باز کردن نوت‌بوک در NBViewerاجرا:‌ باز کردن نوت‌بوک در Google Colabتطبیق با دوره ویدئوییقسمت سوم (۳۲ دقیقه) – پیاده‌سازی تخمین تابع با شبکه‌های بازگشتی ساده [دقایق پایانی]قسمت چهارم (۴ دقیقه) - طول دنباله متغیر در شبکه‌های بازگشتی (۱)قسمت پنجم (۱۱ دقیقه) - طول دنباله متغیر در شبکه‌های بازگشتی (۲)مروردر جلسه‌‌ی دوم دوره به سلول‌های بازگشتی پرداخته و همچنین معادلات مربوط به آن‌ها را بررسی کردیم. در جلسه‌ی سوم به پیاده‌سازی یک شبکه‌ی عصبی بازگشتی ساده با استفاده از کتاب‌خانه‌ی کراس (Keras) برای پیش‌بینی روند یک تابع سینوسی پرداختیم و ساختار شبکه و پارامترهای آن را بررسی کردیم.مقدمهدر این جلسه، به بررسی راهکارهای تعمیم مدل (Generalization) و اجرای آن به‌ازای طول دنباله‌های ورودی یا گام‌های زمانی (Timestep) مختلف می‌پردازیم و بررسی می‌کنیم چگونه می‌توان از یک مدل که با یک گام‌زمانی مشخص آموزش دیده، برای پیش‌بینی داده‌هایی با گام‌‌زمانی دیگر استفاده کرد.لازم به ذکر است همان‌طورکه در بخش پیش‌نیازهای دوره گفته شد، برای درک درست این دوره نیاز است با زبان پایتون (Python)، کتاب‌خانه‌ی کراس (Keras) و ساخت شبکه‌ها به دو حالت Sequential API و Functional API آشنایی داشته باشید.دسترسی به کدهااین جلسه شامل دو بخش است که هر بخش یک نوت‌بوک مجزا دارد، برای مشاهده و دریافت کدهای این جلسه می‌توانید از طریق محزن گیت‌هاب فایل نوت‌بوک‌ها را دریافت کرده و یا از طریق لینک‌های ابتدای پست به‌صورت آنلاین مشاهده کنید. برای اجرای نوت‌بوک‌ به‌صورت آنلاین و رایگان می‌توانید از طریق لینک زیر هر نوت‌بوک را در سرویس Google Colab اجرا کنید:اجرا نوت‌بوک روش اول:‌ باز کردن نوت‌بوک در Google Colabاجرا نوت‌بوک روش دوم:‌ باز کردن نوت‌بوک در Google Colabتعمیم مدل بازگشتیدر جلسه‌ی پیش دیدیم که برای آموزش یک مدل شبکه‌ی عصبی بازگشتی، یک سلول (یا واحد) بارگشتی (RNN Unit [Cell]) را براساس تعداد گام‌های زمانی تکرار می‌کنیم. در نتیجه، با‌توجه‌به اینکه یک سلول یکسان بازگشتی در هر بار تکرار استفاده و آموزش داده‌می‌شود، وزن‌های یک شبکه به گام‌زمانی (Timestep) مرتبط نیستند. بنابراین از نظر تئوری می‌توانیم توقع داشته باشیم تا مدل به طول گام‌های زمانی هنگام آموزش (در مثال جلسه پیش ۱۰) وابسته نبوده و بتوان آن را برای گام‌های زمانی مختلف مجدداً اجرا کرد؛ برای مثال با دیدن پنج ورودی تابع سینوسی، مدل باید بتواند خروجی ششم و با دیدن ۲۰ ورودی، خروجی ۲۱ام را پیش‌بینی کند.یادآوریدر جلسه‌ی قبل و در بررسی «تعداد پارامترهای آموزشی» در قسمت «بررسی ساختار مدل (Model summary) - قسمت دوم» دیدیم که تعداد پارامتر‌های مدل به تعداد دفعات تکرار (Unroll) واحد بازگشتی وابسته نیست.در این جلسه دو روش ساده را برای آن‌که مدل را در یک گام‌زمانی (مثلاً ۱۰) آموزش داده و در گام‌زمانی دیگری (مثلاً ۲۰) تست کنیم بررسی می‌کنیم.تست اولیهپیش از بررسی این روش‌ها و بدون تغییر در مدل جلسه‌ی پیش، بررسی می‌کنیم که آیا می‌توانیم آن را برای گام زمانی دیگری (در اینجا ۵۰) اجرا کنیم یا خیر. پیش از اجرای Predict داده‌های فاز تست را به‌صورت گام‌های زمانی ۵۰تایی دسته‌بندی کرده و سپس همانند جلسه قبل، به فرمت مورد انتظار کتابخانه کراس تبدیل می‌کنیم:# changing step to 50 (prev. 10) testX, testY = convertToDataset(test, 50) # Reshaping to Keras requested format testX = np.reshape(testX, (testX.shape[0], testX.shape[1], 1)) # -> testX.shape: (450, 50, 1)حال سعی می‌کنیم مدل را با گام زمانی جدید Predict کنیم:testPredict= model.predict(testX)اما متوجه می‌شویم که با اجرای دستور Predict روی داده‌های جدید، خطای زیر را دریافت می‌کنیم:ValueError: Error when checking input: expected sequential_1_input to have shape (10, 1) but got array with shape (50, 1)در این خطا بیان شده که مدل انتظار ورودی‌ای با سایز (۱۰ , ۱) داشته اما ورودی‌ای با سایز (۵۰ , ۱) دریافت کرده است.دلیل این خطا این است که در هنگام تعیین ساختار مدل، سایز ورودی را به‌صورت مشخص تعیین نکرده بودیم؛ از این رو با اولین خوراک داده‌های ورودی در هنگام fit کردن (اولین mini-batch داده‌ها) مدل سایز ورودی خود را مشخص کرده و بنابراین انتظار ورودی‌هایی با گام زمانی برابر ۱۰ را دارد. همانند قبل (در شبکه‌های کانولوشنی) شبکه‌ی ابعاد ورودی را چک کرده و در صورت مغایرت خطا می‌دهد.در واقع شبکه از نظر منطقی مشکلی نداشته و خطای ارسالی، تنها از لحاظ نحو (سینتکس [Syntax]) بوده و به تعریف ساختار مدل مربوط است. برای رفع این مشکل کارهای متنوعی می‌توان انجام داد که در این جلسه ساده‌ترین روش (روش اول) و یک روش دیگر که در جلسات آینده به آن احتیاج خواهیم داشت (روش دوم) را بررسی می‌کنیم.تغییر در طول بازه‌های زمانی ورودی شبکه - روش اولنوت‌بوک:‌ ‎02_1_simple-RNN-diffrent-sequence-length.ipynbطولانی بود نخواندم (TL;DR)با بیان None به‌عنوان ابعاد ورودی لایه‌ی بازگشتی، مشخص می‌کنیم این مقدار متغیر بوده و نامشخص است.در این روش برای آموزش مدل کاملاً مشابه قبل عمل می‌کنیم، با این تفاوت که برای داده‌های تست، گام‌های زمانی را متفاوت با داده‌های آموزش و برابر ۲۰ قرار داده و هنگام تعریف ساختار مدل و معرفی لایه‌ی بازگشتی، سایز ورودی‌های این لایه را به‌صورت زیر تعریف می‌کنیم:model.add(SimpleRNN(units=64, input_shape=(None, 1), activation=&quottanh&quot))در اینجا ورودی‌های مدل را به‌صورت مشخص بیان کردیم و با استفاده از None مشخص کرده‌ایم که تعداد گام‌های زمانی (Timesteps) میزانی نامشخص و متغیر بوده و سایز ویژگی‌های (فیچر [Feature]) هر ورودی برابر ۱ است.نکتهدر فریم‌ورک Tensorflow و کتاب‌خانه‌ی کراس (Keras)، معرفی input_shape برای یک لایه، شامل mini-batch نشده و تعداد نمونه‌های ورودی (Number of Sample) را بیان نمی‌کنیم.لازم به ذکر است که در این صورت و با مشخص‌کردن ضمنی ابعاد ورودی، می‌توانیم قبل از Fit کردن مدل، با استفاده از متد summary گزارشی از ساختار مدل دریافت کنیم. همچنین با اجرای دستور model.input می‌توانیم سایز ورودی را دریافت کنیم که به‌صورت زیر خواهد بود:model.input # <tf.Tensor 'simple_rnn_1_input:0' shape=(?, ?, 1) dtype=float32>همانطور که مشخص است، تعداد ورودی‌ها (Number of sequence - سایز mini-batch) و تعداد گام‌های زمانی (timestep) مقادیری نامشخص و متغیر تعریف شده‌اند.ادامه‌ی مراحل همانند جلسه‌ی پیش بوده. پس از آموزش مدل، می‌توانیم بر روی داده‌های تست که گام‌زمانی متفاوتی نسبت به داده‌های آموزش داشتند نیز پیش‌بینی گرفته و متد model.predict را روی داده‌های تست فراخوانی کنیم:trainPredict = model.predict(trainX) testPredict = model.predict(testX) predicted = np.concatenate((trainPredict, testPredict), axis=0) ... plt.show()نمودار پیش‌بینی مدل در روش اولتغییر در طول بازه‌های زمانی ورودی شبکه - روش دومنوت‌بوک:‌ ‎02_2_simple-RNN-diffrent-sequence-length.ipynbطولانی بود نخواندم (TL;DR) وزن‌های مدل را بعد از آموزش را ذخیره کرده و هنگام تست مدل را با گام‌زمانی متفاوت لود می‌کنیم.همانطور که در مقدمه‌ی این جلسه اشاره شد، وزن‌های مدل بازگشتی به تعداد گام‌زمانی‌های آن وابسته نیست. بنابراین می‌توانیم از وزن‌های آموزش داده شده یک مدل، در اجرای آن مدل در گام‌زمانی‌ دیگری نیز استفاده کنیم. برای این کار نیاز است تا وزن‌های مدل را پس از آموزش ذخیره کنیم و مدلی با ساختار مشابه اما گام‌زمانی متفاوت ایجاد کنیم.در این روش نیز برای آموزش مدل، کاملاً مشابه قبل عمل می‌کنیم؛ اما این‌بار برای ساخت مدل از یک تابع استفاده می‌کنیم تا با دریافت تعداد گام‌زمانی (Timestep) مورد نظر، مدلی با سایز ورودی (Input Shape) متناسب با آن گام‌زمانی ایجاد کند. بدین‌صورت می‌توانیم به‌راحتی مدل دیگری با گام‌‌زمانی متفاوت اما ساختار یکسان بسازیم. بنابراین تابع build_model را به‌صورت زیر خواهیم داشت:def build_model(sequence_length): model = Sequential() model.add(SimpleRNN(units=64, input_shape=(sequence_length, 1), activation=&quottanh&quot)) model.add(Dense(1)) return modelسپس، برای ساخت مدل، این تابع را با تعداد گام‌زمانی مورد نظر صدا می‌زنیم:model = build_model(10)این بار هم چون سایز ورودی‌های مدل به‌صورت ضمنی مشخص شده‌اند، می‌توانیم قبل از Fit کردن مدل، با استفاده از متد summary گزارشی از ساختار مدل دریافت کنیم که به‌صورت زیر خواهد بود:# model.summary() output: _________________________________________________________________ Layer (type) Output Shape Param # ================================================================= simple_rnn (SimpleRNN) (None, 64) 4224 _________________________________________________________________ dense (Dense) (None, 1) 65 ================================================================= Total params: 4,289 Trainable params: 4,289 Non-trainable params: 0 _________________________________________________________________همین‌طور، گزارش ورودی‌های مدل نیز به‌صورت زیر خواهد بود:# model.input output: <tf.Tensor 'simple_rnn_input:0' shape=(?, 10, 1) dtype=float32>مشخص است که تعداد ورودی‌ها (Number of sequence - سایز mini-batch) مقداری نامشخص (کاراکتر ?) بوده، اما تعداد گام‌های زمانی (timestep) دیگر نامشخص نیست و برابر ۱۰ تعریف شده است.در ادامه مدل را همانند جلسات پیش کامپایل می‌کنیم:model.compile(loss='mean_squared_error', optimizer='adam')حال می‌خواهیم پس از آموزش مدل و تعیین وزن‌های (Weights) آن، آن‌ها را ذخیره کنیم. برای این‌کار از بازبانگ‌های (‏‏Callback) تنسورفلو (Tensorflow) استفاده می‌کنیم.بازبانگ‌های (‏‏Callback) تنسورفلو (Tensorflow)بازبانگ‌ها (بازندا [Callback]) عموماً توابعی هستند که پس از رخداد یک رویداد (Event) خاص اجرا می‌شوند. با کمک بازبانگ‌ها می‌توانیم در زمان اجرا و بر اساس نوع رویداد فرآیندهای لازم را اجرا کنیم. در تنسورفلو رویدادهای (Event) مختلفی مثل شروع یک تکرار (iteration - آموزش روی یک mini-batch)، پایان یک تکرار، شروع یک ایپاک (Epoch) و پایان یک ایپاک تعریف شده‌اند که می‌توان در هنگام Fit کردن مدل بازبانگ‌های مربوط به آن‌ها را مشخص کرد.علاوه‌بر این، تعدادی از این بازبانگ‌ها به‌دلیل اینکه به‌صورت مکرر مورد استفاده هستند از پیش تعریف شده و قابل استفاده هستند. یکی از این بازبانگ‌ها Checkpoint است که به کمک آن می‌توان بعد از پایان هر ایپاک مدل را ذخیره کرد (مشابه اجرای model.save در انتهای هر ایپاک). مطالعه بیشتربرای آشنایی بیشتر با Callbackهای تنسورفلو (Tensorflow) می‌توانید وبینار زیر به تدریس علیرضا اخوان‌پور مدرس همین دوره را مشاهده کنید: https://class.vision/product/using-callback-in-kerasبرای استفاده از این بازبانگ می‌توان آن را به‌صورت زیر در برنامه وارد کرد:from tensorflow.keras.callbacks import ModelCheckpointسپس به‌صورت زیر یک بازبانگ جدید از کلاس ModelCheckpoint تعریف می‌کنیم:# Directory where the checkpoints will be saved checkpoint_dir = './notebook02_training_checkpoints' # Name of the checkpoint files checkpoint_prefix = os.path.join(checkpoint_dir, &quotckpt_{epoch}&quot) checkpoint_callback=ModelCheckpoint( filepath=checkpoint_prefix, save_weights_only=True)این کلاس یک آدرس برای ذخیره‌سازی فایل دریافت می‌کند. استفاده از فرمت از پیش مشخص تنسورفلو به‌صورت {epoch} باعث می‌شود که شماره‌ی هر ایپاک در نام فایل نوشته شده و ذخیره شود. همچنین با فعال‌کردن ویژگی save_weights_only مشخص کرده‌ایم که نیازی به ذخیره‌ی معماری مدل نیست و تنها ذخیره‌ی وزن‌های آن کافی است.نکتهبهتر است دایرکتوری حاوی فایل‌ها را از پیش ساخته باشید.نکتهاین بازبانگ یک کلاس تنسورفلویی بوده و برخلاف model.save کراس که با فرمت h5 مدل را ذخیره می‌کرد، با فرمت خاص خود مدل را ذخیره می‌کند.آموزش مدلپس از ساخت بازبانگ، در هنگام Fit کردن مدل به‌صورت زیر آن را به مدل معرفی می‌کنیم:history = model.fit(trainX,trainY, epochs=100, batch_size=16, verbose=2, callbacks=[checkpoint_callback])همانطور که مشخص است، متد Fit لیستی از بازبانگ‌ها دریافت می‌کند که در اینجا آرایه‌ای شامل باز‌بانگ فوق (checkpoint_callback) را برای آن مشخص کردیم.نکتهاگر خروجی اجرای سلول بالا را در نوت‌بوک مربوط به این جلسه مشاهده کنید، متوجه می‌شوید که پس از اجرای این سلول، تعدادی هشدار چاپ شده است. این هشدارها مربوط به این است که مدل کراس را با فرمت تنسورفلو ذخیره می‌کنیم و بنابراین پارامترهای مربوط به optimizer مدل که توسط کراس تعریف شده‌اند ذخیره نمی‌شوند. این مورد مشکلی برای ما به‌وجود نخواهد آورد.تست مدلحال پس از آموزش مدل، وزن‌های آموزش دیده‌ی مدل را در اختیار داریم. این وزن‌ها در ۱۰۰ جفت فایل (چرا که در ۱۰۰ ایپاک مدل را آموزش داده‌ایم - فایل شاخص [Index] و فایل داده‌ها) در پوشه‌ی notebook02_training_checkpoints با فرمت نام مشخص‌شده ایجاد شده‌اند.برای دریافت آدرس آخرین وزن‌های ذخیره شده، می‌توان از متد آماده‌ی زیر تنسورفلو کمک گرفت:tf.train.latest_checkpoint(checkpoint_dir) # output: './notebook02_training_checkpoints\\ckpt_100'در فاز تست این روش، می‌خواهیم مدل را با طول گام‌زمانی متفاوتی (در اینجا ۲۰) اجرا کنیم، برای این‌کار یک مدل جدید (با وزن‌های تصادفی) می‌سازیم:model = build_model(20)و وزن‌های آموزش دیده را در این مدل بارگذاری (Load) می‌کنیم. برای این‌کار از متد load_weights به‌صورت زیر استفاده می‌کنیم:model.load_weights(tf.train.latest_checkpoint(checkpoint_dir))پیش از پیش‌بینی توسط مدل نیاز است آن را Build یا کامپایل (Compile) کنیم:model.compile(loss='mean_squared_error', optimizer='adam')نکتهدر اینجا تابع هزینه (loss) و Optimizer تنها برای اجرای صحیح تابع کامپایل compile مشخص شده‌اند و کاربردی ندارند.سپس می‌توانیم همانند قبل، از مدل استفاده کنیم:model.evaluate(testX, testY, verbose=0) # output: 0.841938066482544در این جلسه در تکمیل مباحث جلسه‌‌ی قبلی، روش‌های اجرای مدل بازگشتی در طول‌های متفاوت گام‌زمانی را بررسی کردیم. همچنین با نحو کد‌های (Syntax) کراس (Keras) و بازبانگ‌های (Callback) تنسورفلو (Tensorflow) بیشتر آشنا شدیم. همچنین لازم به‌ذکر است که دو روشی که در این جلسه بررسی شدند، روش‌های ساده‌ای بودند که در جلسات آینده‌ی دوره به آن‌ها نیاز خواهیم داشت. رویکرد استفاده‌ی مجدد از وزن‌های آموزش دیده، رویکرد رایجی در آموزش شبکه‌های عصبی‌ست که در شبکه‌های غیربازگشتی -مانند شبکه‌های کانولوشنال (Convolutional) - نیز کاربرد دارد.در جلسه‌ی بعدی به بررسی دو مشکل رایج شبکه‌های عصبی بازگشتی یعنی محو شدگی و انفجار گرادیان‌ها (Exploding and Vanishing Gradients) در شبکه‌های بازگشتی می‌پردازیم.می‌توانید لیست جلسات دوره را در این پست مشاهده کنید.جلسه‌ٔ قبلیجلسه‌ٔ بعدی (در دست انتشار)این دوره به کوشش علیرضا اخوان‌پور به‌صورت ویدئویی تهیه شده و به قلم محمدحسن ستاریان با نظارت ایشان به‌صورت متنی نگارش و توسط بهار برادران افتخاری ویرایش شده‌ست.در صورت تمایل به خرید دوره به‌صورت ویدئویی می‌توانید با استفاده از کد تخفیف rnn4 از ۲۰ درصد تخفیف بهره‌مند شوید.