{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# רשתות עצביות חוזרות\n", "\n", "במודול הקודם השתמשנו בייצוגים סמנטיים עשירים של טקסט ובמסווג ליניארי פשוט מעל ההטמעות. מה שהארכיטקטורה הזו עושה הוא לתפוס את המשמעות המצטברת של מילים במשפט, אך היא אינה מתחשבת ב**סדר** המילים, מכיוון שפעולת האגרגציה מעל ההטמעות הסירה את המידע הזה מהטקסט המקורי. מכיוון שמודלים אלו אינם מסוגלים לדגם את סדר המילים, הם אינם יכולים לפתור משימות מורכבות או עמומות יותר כמו יצירת טקסט או מענה על שאלות.\n", "\n", "כדי לתפוס את המשמעות של רצף טקסט, עלינו להשתמש בארכיטקטורה אחרת של רשת עצבית, הנקראת **רשת עצבית חוזרת**, או RNN. ב-RNN, אנו מעבירים את המשפט דרך הרשת סמל אחד בכל פעם, והרשת מייצרת **מצב** מסוים, אותו אנו מעבירים שוב לרשת יחד עם הסמל הבא.\n", "\n", "בהינתן רצף קלט של טוקנים $X_0,\\dots,X_n$, RNN יוצרת רצף של בלוקים של רשת עצבית, ומאמנת את הרצף הזה מקצה לקצה באמצעות התפשטות לאחור. כל בלוק רשת מקבל זוג $(X_i,S_i)$ כקלט, ומייצר $S_{i+1}$ כתוצאה. המצב הסופי $S_n$ או הפלט $X_n$ מועבר למסווג ליניארי כדי לייצר את התוצאה. כל בלוקי הרשת חולקים את אותם משקלים, ומאומנים מקצה לקצה באמצעות מעבר אחד של התפשטות לאחור.\n", "\n", "מכיוון שוקטורי המצב $S_0,\\dots,S_n$ מועברים דרך הרשת, היא מסוגלת ללמוד את התלויות הרציפות בין מילים. לדוגמה, כאשר המילה *לא* מופיעה במקום כלשהו ברצף, היא יכולה ללמוד לשלול אלמנטים מסוימים בתוך וקטור המצב, מה שמוביל לשלילה.\n", "\n", "> מכיוון שמשקלי כל בלוקי ה-RNN בתמונה משותפים, ניתן לייצג את אותה תמונה כבלוק אחד (מימין) עם לולאת משוב חוזרת, שמעבירה את מצב הפלט של הרשת חזרה לקלט.\n", "\n", "בואו נראה כיצד רשתות עצביות חוזרות יכולות לעזור לנו לסווג את מערך הנתונים של החדשות שלנו.\n" ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Loading dataset...\n", "Building vocab...\n" ] } ], "source": [ "import torch\n", "import torchtext\n", "from torchnlp import *\n", "train_dataset, test_dataset, classes, vocab = load_dataset()\n", "vocab_size = len(vocab)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## מסווג RNN פשוט\n", "\n", "במקרה של RNN פשוט, כל יחידה חוזרת היא רשת ליניארית פשוטה, אשר מקבלת וקטור קלט משולב ווקטור מצב, ומייצרת וקטור מצב חדש. PyTorch מייצגת יחידה זו באמצעות מחלקת `RNNCell`, ורשתות של יחידות כאלה - כשכבת `RNN`.\n", "\n", "כדי להגדיר מסווג RNN, תחילה ניישם שכבת הטמעה להורדת הממדיות של אוצר המילים בקלט, ולאחר מכן נוסיף שכבת RNN מעליה:\n" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], "source": [ "class RNNClassifier(torch.nn.Module):\n", " def __init__(self, vocab_size, embed_dim, hidden_dim, num_class):\n", " super().__init__()\n", " self.hidden_dim = hidden_dim\n", " self.embedding = torch.nn.Embedding(vocab_size, embed_dim)\n", " self.rnn = torch.nn.RNN(embed_dim,hidden_dim,batch_first=True)\n", " self.fc = torch.nn.Linear(hidden_dim, num_class)\n", "\n", " def forward(self, x):\n", " batch_size = x.size(0)\n", " x = self.embedding(x)\n", " x,h = self.rnn(x)\n", " return self.fc(x.mean(dim=1))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "> **Note:** אנו משתמשים בשכבת הטמעה לא מאומנת כאן לצורך פשטות, אך לתוצאות טובות יותר ניתן להשתמש בשכבת הטמעה מאומנת מראש עם הטמעות Word2Vec או GloVe, כפי שתואר ביחידה הקודמת. כדי להבין טוב יותר, ייתכן שתרצו להתאים את הקוד כך שיעבוד עם הטמעות מאומנות מראש.\n", "\n", "במקרה שלנו, נשתמש במעמיס נתונים מרופד, כך שכל אצווה תכיל מספר רצפים מרופדים באורך זהה. שכבת ה-RNN תקבל את רצף טנסורי ההטמעות ותפיק שני פלטים:\n", "* $x$ הוא רצף של פלטי תאי RNN בכל שלב\n", "* $h$ הוא מצב מוסתר סופי עבור האלמנט האחרון ברצף\n", "\n", "לאחר מכן ניישם מסווג ליניארי מחובר-מלא כדי לקבל את מספר המחלקות.\n", "\n", "> **Note:** RNNs הם די קשים לאימון, מכיוון שברגע שתאי ה-RNN נפרסים לאורך אורך הרצף, מספר השכבות המעורבות באחזור לאחור הופך להיות די גדול. לכן, יש לבחור קצב למידה קטן ולאמן את הרשת על מערך נתונים גדול יותר כדי להפיק תוצאות טובות. זה יכול לקחת זמן רב, ולכן מומלץ להשתמש ב-GPU.\n" ] }, { "cell_type": "code", "execution_count": 3, "metadata": { "scrolled": true }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "3200: acc=0.3090625\n", "6400: acc=0.38921875\n", "9600: acc=0.4590625\n", "12800: acc=0.511953125\n", "16000: acc=0.5506875\n", "19200: acc=0.57921875\n", "22400: acc=0.6070089285714285\n", "25600: acc=0.6304296875\n", "28800: acc=0.6484027777777778\n", "32000: acc=0.66509375\n", "35200: acc=0.6790056818181818\n", "38400: acc=0.6929166666666666\n", "41600: acc=0.7035817307692308\n", "44800: acc=0.7137276785714286\n", "48000: acc=0.72225\n", "51200: acc=0.73001953125\n", "54400: acc=0.7372794117647059\n", "57600: acc=0.7436631944444444\n", "60800: acc=0.7503947368421052\n", "64000: acc=0.75634375\n", "67200: acc=0.7615773809523809\n", "70400: acc=0.7662642045454545\n", "73600: acc=0.7708423913043478\n", "76800: acc=0.7751822916666666\n", "80000: acc=0.7790625\n", "83200: acc=0.7825\n", "86400: acc=0.7858564814814815\n", "89600: acc=0.7890513392857142\n", "92800: acc=0.7920474137931034\n", "96000: acc=0.7952708333333334\n", "99200: acc=0.7982258064516129\n", "102400: acc=0.80099609375\n", "105600: acc=0.8037594696969697\n", "108800: acc=0.8060569852941176\n" ] } ], "source": [ "train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=16, collate_fn=padify, shuffle=True)\n", "net = RNNClassifier(vocab_size,64,32,len(classes)).to(device)\n", "train_epoch(net,train_loader, lr=0.001)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## זיכרון לטווח ארוך וקצר (LSTM)\n", "\n", "אחת הבעיות המרכזיות של RNNs קלאסיים היא בעיית **הגרדיאנטים הנעלמים**. מכיוון ש-RNNs מאומנים מקצה לקצה במעבר אחד של back-propagation, קשה להם להעביר את השגיאה לשכבות הראשונות של הרשת, ולכן הרשת אינה יכולה ללמוד קשרים בין טוקנים רחוקים. אחת הדרכים להימנע מבעיה זו היא להכניס **ניהול מצב מפורש** באמצעות שימוש במה שנקרא **שערים**. ישנן שתי ארכיטקטורות ידועות מסוג זה: **זיכרון לטווח ארוך וקצר** (LSTM) ו-**יחידת ממסר עם שערים** (GRU).\n", "\n", "![תמונה המציגה דוגמה של תא זיכרון לטווח ארוך וקצר](../../../../../lessons/5-NLP/16-RNN/images/long-short-term-memory-cell.svg)\n", "\n", "רשת LSTM מאורגנת בצורה דומה ל-RNN, אך ישנם שני מצבים שעוברים משכבה לשכבה: המצב האמיתי $c$, והווקטור הנסתר $h$. בכל יחידה, וקטור נסתר $h_i$ משולב עם הקלט $x_i$, והם שולטים במה שקורה למצב $c$ באמצעות **שערים**. כל שער הוא רשת נוירונים עם פונקציית הפעלה מסוג סיגמואיד (פלט בטווח $[0,1]$), שניתן לחשוב עליה כמסכה ביטית כאשר מכפילים אותה בווקטור המצב. השערים הם (משמאל לימין בתמונה למעלה):\n", "* **שער השכחה** לוקח את הווקטור הנסתר וקובע אילו רכיבים של הווקטור $c$ עלינו לשכוח ואילו להעביר הלאה.\n", "* **שער הקלט** לוקח מידע מסוים מהקלט ומהווקטור הנסתר, ומכניס אותו למצב.\n", "* **שער הפלט** משנה את המצב דרך שכבה ליניארית עם הפעלה מסוג $\\tanh$, ואז בוחר חלק מהרכיבים שלה באמצעות הווקטור הנסתר $h_i$ כדי לייצר מצב חדש $c_{i+1}$.\n", "\n", "ניתן לחשוב על רכיבי המצב $c$ כדגלים שניתן להדליק ולכבות. לדוגמה, כאשר אנו נתקלים בשם *אליס* ברצף, ייתכן שנרצה להניח שהוא מתייחס לדמות נשית, ולהרים דגל במצב שמציין שיש לנו שם עצם נשי במשפט. כאשר ניתקל בהמשך בביטוי *וגם טום*, נרים דגל שמציין שיש לנו שם עצם ברבים. כך, על ידי מניפולציה של המצב, נוכל לכאורה לעקוב אחר תכונות דקדוקיות של חלקי המשפט.\n", "\n", "> **Note**: משאב מצוין להבנת המבנה הפנימי של LSTM הוא המאמר הנהדר הזה [Understanding LSTM Networks](https://colah.github.io/posts/2015-08-Understanding-LSTMs/) מאת כריסטופר אולה.\n", "\n", "למרות שהמבנה הפנימי של תא LSTM עשוי להיראות מורכב, PyTorch מסתירה את המימוש הזה בתוך המחלקה `LSTMCell`, ומספקת את האובייקט `LSTM` לייצוג שכבת LSTM שלמה. לכן, המימוש של מסווג LSTM יהיה דומה מאוד ל-RNN הפשוט שראינו קודם:\n" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [], "source": [ "class LSTMClassifier(torch.nn.Module):\n", " def __init__(self, vocab_size, embed_dim, hidden_dim, num_class):\n", " super().__init__()\n", " self.hidden_dim = hidden_dim\n", " self.embedding = torch.nn.Embedding(vocab_size, embed_dim)\n", " self.embedding.weight.data = torch.randn_like(self.embedding.weight.data)-0.5\n", " self.rnn = torch.nn.LSTM(embed_dim,hidden_dim,batch_first=True)\n", " self.fc = torch.nn.Linear(hidden_dim, num_class)\n", "\n", " def forward(self, x):\n", " batch_size = x.size(0)\n", " x = self.embedding(x)\n", " x,(h,c) = self.rnn(x)\n", " return self.fc(h[-1])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "3200: acc=0.259375\n", "6400: acc=0.25859375\n", "9600: acc=0.26177083333333334\n", "12800: acc=0.2784375\n", "16000: acc=0.313\n", "19200: acc=0.3528645833333333\n", "22400: acc=0.3965625\n", "25600: acc=0.4385546875\n", "28800: acc=0.4752777777777778\n", "32000: acc=0.505375\n", "35200: acc=0.5326704545454546\n", "38400: acc=0.5557552083333334\n", "41600: acc=0.5760817307692307\n", "44800: acc=0.5954910714285714\n", "48000: acc=0.6118333333333333\n", "51200: acc=0.62681640625\n", "54400: acc=0.6404779411764706\n", "57600: acc=0.6520138888888889\n", "60800: acc=0.662828947368421\n", "64000: acc=0.673546875\n", "67200: acc=0.6831547619047619\n", "70400: acc=0.6917897727272727\n", "73600: acc=0.6997146739130434\n", "76800: acc=0.707109375\n", "80000: acc=0.714075\n", "83200: acc=0.7209134615384616\n", "86400: acc=0.727037037037037\n", "89600: acc=0.7326674107142858\n", "92800: acc=0.7379633620689655\n", "96000: acc=0.7433645833333333\n", "99200: acc=0.7479032258064516\n", "102400: acc=0.752119140625\n", "105600: acc=0.7562405303030303\n", "108800: acc=0.76015625\n", "112000: acc=0.7641339285714286\n", "115200: acc=0.7677777777777778\n", "118400: acc=0.7711233108108108\n" ] }, { "data": { "text/plain": [ "(0.03487814127604167, 0.7728)" ] }, "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], "source": [ "net = LSTMClassifier(vocab_size,64,32,len(classes)).to(device)\n", "train_epoch(net,train_loader, lr=0.001)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## רצפים ארוזים\n", "\n", "בדוגמה שלנו, היינו צריכים למלא את כל הרצפים במיני-אצווה עם וקטורי אפס. למרות שזה גורם לבזבוז זיכרון מסוים, עם RNNs זה קריטי יותר מכיוון שנוצרים תאי RNN נוספים עבור פריטי הקלט המלאים, שמשתתפים באימון אך אינם נושאים מידע קלט חשוב. יהיה הרבה יותר טוב לאמן את ה-RNN רק לגודל הרצף האמיתי.\n", "\n", "כדי לעשות זאת, פורמט מיוחד לאחסון רצפים מלאים הוצג ב-PyTorch. נניח שיש לנו מיני-אצווה מלאה שנראית כך:\n", "```\n", "[[1,2,3,4,5],\n", " [6,7,8,0,0],\n", " [9,0,0,0,0]]\n", "```\n", "כאן 0 מייצג ערכים מלאים, והווקטור של אורכי הרצפים האמיתיים הוא `[5,3,1]`.\n", "\n", "כדי לאמן RNN בצורה יעילה עם רצפים מלאים, אנחנו רוצים להתחיל את האימון של קבוצת תאי RNN הראשונה עם מיני-אצווה גדולה (`[1,6,9]`), אבל אז לסיים את העיבוד של הרצף השלישי ולהמשיך את האימון עם מיני-אצוות קטנות יותר (`[2,7]`, `[3,8]`), וכן הלאה. כך, רצף ארוז מיוצג כווקטור אחד - במקרה שלנו `[1,6,9,2,7,3,8,4,5]`, וווקטור אורכים (`[5,3,1]`), שממנו ניתן לשחזר בקלות את המיני-אצווה המלאה המקורית.\n", "\n", "כדי ליצור רצף ארוז, ניתן להשתמש בפונקציה `torch.nn.utils.rnn.pack_padded_sequence`. כל השכבות החוזרות, כולל RNN, LSTM ו-GRU, תומכות ברצפים ארוזים כקלט ומייצרות פלט ארוז, שניתן לפענח באמצעות `torch.nn.utils.rnn.pad_packed_sequence`.\n", "\n", "כדי להיות מסוגלים ליצור רצף ארוז, אנחנו צריכים להעביר את וקטור האורכים לרשת, ולכן אנחנו צריכים פונקציה שונה להכנת מיני-אצוות:\n" ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [], "source": [ "def pad_length(b):\n", " # build vectorized sequence\n", " v = [encode(x[1]) for x in b]\n", " # compute max length of a sequence in this minibatch and length sequence itself\n", " len_seq = list(map(len,v))\n", " l = max(len_seq)\n", " return ( # tuple of three tensors - labels, padded features, length sequence\n", " torch.LongTensor([t[0]-1 for t in b]),\n", " torch.stack([torch.nn.functional.pad(torch.tensor(t),(0,l-len(t)),mode='constant',value=0) for t in v]),\n", " torch.tensor(len_seq)\n", " )\n", "\n", "train_loader_len = torch.utils.data.DataLoader(train_dataset, batch_size=16, collate_fn=pad_length, shuffle=True)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "הרשת בפועל תהיה דומה מאוד ל-`LSTMClassifier` שהוזכר למעלה, אך מעבר ה-`forward` יקבל גם מיני-אצווה מרופדת וגם וקטור של אורכי הרצפים. לאחר חישוב ההטמעה, אנו מחשבים רצף ארוז, מעבירים אותו לשכבת ה-LSTM, ואז פותחים את התוצאה בחזרה.\n", "\n", "> **הערה**: למעשה, אנחנו לא משתמשים בתוצאה הבלתי-ארוזה `x`, מכיוון שאנחנו משתמשים בפלט מהשכבות הנסתרות בחישובים הבאים. לכן, ניתן להסיר את שלב פתיחת האריזה לחלוטין מהקוד הזה. הסיבה שאנחנו משאירים אותו כאן היא כדי לאפשר לך לשנות את הקוד בקלות, במקרה שתצטרך להשתמש בפלט הרשת בחישובים נוספים.\n" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [], "source": [ "class LSTMPackClassifier(torch.nn.Module):\n", " def __init__(self, vocab_size, embed_dim, hidden_dim, num_class):\n", " super().__init__()\n", " self.hidden_dim = hidden_dim\n", " self.embedding = torch.nn.Embedding(vocab_size, embed_dim)\n", " self.embedding.weight.data = torch.randn_like(self.embedding.weight.data)-0.5\n", " self.rnn = torch.nn.LSTM(embed_dim,hidden_dim,batch_first=True)\n", " self.fc = torch.nn.Linear(hidden_dim, num_class)\n", "\n", " def forward(self, x, lengths):\n", " batch_size = x.size(0)\n", " x = self.embedding(x)\n", " pad_x = torch.nn.utils.rnn.pack_padded_sequence(x,lengths,batch_first=True,enforce_sorted=False)\n", " pad_x,(h,c) = self.rnn(pad_x)\n", " x, _ = torch.nn.utils.rnn.pad_packed_sequence(pad_x,batch_first=True)\n", " return self.fc(h[-1])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [] }, { "cell_type": "code", "execution_count": 8, "metadata": { "scrolled": true }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "3200: acc=0.285625\n", "6400: acc=0.33359375\n", "9600: acc=0.3876041666666667\n", "12800: acc=0.44078125\n", "16000: acc=0.4825\n", "19200: acc=0.5235416666666667\n", "22400: acc=0.5559821428571429\n", "25600: acc=0.58609375\n", "28800: acc=0.6116666666666667\n", "32000: acc=0.63340625\n", "35200: acc=0.6525284090909091\n", "38400: acc=0.668515625\n", "41600: acc=0.6822596153846154\n", "44800: acc=0.6948214285714286\n", "48000: acc=0.7052708333333333\n", "51200: acc=0.71521484375\n", "54400: acc=0.7239889705882353\n", "57600: acc=0.7315277777777778\n", "60800: acc=0.7388486842105263\n", "64000: acc=0.74571875\n", "67200: acc=0.7518303571428572\n", "70400: acc=0.7576988636363636\n", "73600: acc=0.7628940217391305\n", "76800: acc=0.7681510416666667\n", "80000: acc=0.7728125\n", "83200: acc=0.7772235576923077\n", "86400: acc=0.7815393518518519\n", "89600: acc=0.7857700892857142\n", "92800: acc=0.7895043103448276\n", "96000: acc=0.7930520833333333\n", "99200: acc=0.7959072580645161\n", "102400: acc=0.798994140625\n", "105600: acc=0.802064393939394\n", "108800: acc=0.8051378676470589\n", "112000: acc=0.8077857142857143\n", "115200: acc=0.8104600694444445\n", "118400: acc=0.8128293918918919\n" ] }, { "data": { "text/plain": [ "(0.029785829671223958, 0.8138166666666666)" ] }, "execution_count": 8, "metadata": {}, "output_type": "execute_result" } ], "source": [ "net = LSTMPackClassifier(vocab_size,64,32,len(classes)).to(device)\n", "train_epoch_emb(net,train_loader_len, lr=0.001,use_pack_sequence=True)\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "> **הערה:** ייתכן ששמת לב לפרמטר `use_pack_sequence` שאנו מעבירים לפונקציית האימון. נכון לעכשיו, פונקציית `pack_padded_sequence` דורשת שמערך אורך הרצף יהיה על התקן CPU, ולכן פונקציית האימון צריכה להימנע מהעברת נתוני אורך הרצף ל-GPU במהלך האימון. ניתן לעיין במימוש של פונקציית `train_emb` בקובץ [`torchnlp.py`](../../../../../lessons/5-NLP/16-RNN/torchnlp.py).\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## RNN דו-כיווני ורב-שכבתי\n", "\n", "בדוגמאות שלנו, כל הרשתות החוזרות פעלו בכיוון אחד, מתחילת הרצף ועד סופו. זה נראה טבעי, כי זה דומה לאופן שבו אנו קוראים ומאזינים לדיבור. עם זאת, במקרים מעשיים רבים יש לנו גישה אקראית לרצף הקלט, ולכן ייתכן שיהיה הגיוני להריץ חישוב חוזר בשני הכיוונים. רשתות כאלה נקראות **RNN דו-כיווני**, וניתן ליצור אותן על ידי העברת הפרמטר `bidirectional=True` לבנאי של RNN/LSTM/GRU.\n", "\n", "כאשר עובדים עם רשת דו-כיוונית, נצטרך שני וקטורי מצב חבוי, אחד לכל כיוון. PyTorch מקודד את הווקטורים הללו כווקטור אחד בגודל כפול, מה שמאוד נוח, כי בדרך כלל תעבירו את מצב החבוי המתקבל לשכבה לינארית מחוברת-מלאה, ותצטרכו רק לקחת את הגידול בגודל בחשבון בעת יצירת השכבה.\n", "\n", "רשת חוזרת, בין אם היא חד-כיוונית או דו-כיוונית, לוכדת דפוסים מסוימים בתוך רצף, ויכולה לאחסן אותם בווקטור מצב או להעביר אותם לפלט. כמו ברשתות קונבולוציה, ניתן לבנות שכבה חוזרת נוספת מעל הראשונה כדי ללכוד דפוסים ברמה גבוהה יותר, שנבנים מדפוסים ברמה נמוכה שהופקו על ידי השכבה הראשונה. זה מוביל אותנו למושג של **RNN רב-שכבתי**, שמורכב משתי רשתות חוזרות או יותר, כאשר הפלט של השכבה הקודמת מועבר לשכבה הבאה כקלט.\n", "\n", "![תמונה המציגה RNN רב-שכבתי מסוג LSTM](../../../../../translated_images/he/multi-layer-lstm.dd975e29bb2a59fe.webp)\n", "\n", "*תמונה מתוך [הפוסט הנהדר הזה](https://towardsdatascience.com/from-a-lstm-cell-to-a-multilayer-lstm-network-with-pytorch-2899eb5696f3) מאת Fernando López*\n", "\n", "PyTorch הופך את בניית הרשתות הללו למשימה פשוטה, כי כל מה שצריך לעשות הוא להעביר את הפרמטר `num_layers` לבנאי של RNN/LSTM/GRU כדי לבנות באופן אוטומטי מספר שכבות של חזרתיות. זה גם אומר שגודל וקטור החבוי/מצב יגדל באופן יחסי, ותצטרכו לקחת זאת בחשבון בעת טיפול בפלט של השכבות החוזרות.\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## רשתות עצביות חוזרות (RNNs) למשימות אחרות\n", "\n", "ביחידה זו ראינו ש-RNNs יכולים לשמש לסיווג רצפים, אך למעשה, הם יכולים להתמודד עם משימות רבות נוספות, כמו יצירת טקסט, תרגום מכונה ועוד. נבחן את המשימות הללו ביחידה הבאה.\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n---\n\n**כתב ויתור**: \nמסמך זה תורגם באמצעות שירות תרגום מבוסס בינה מלאכותית [Co-op Translator](https://github.com/Azure/co-op-translator). למרות שאנו שואפים לדיוק, יש לקחת בחשבון שתרגומים אוטומטיים עשויים להכיל שגיאות או אי דיוקים. המסמך המקורי בשפתו המקורית צריך להיחשב כמקור סמכותי. עבור מידע קריטי, מומלץ להשתמש בתרגום מקצועי על ידי אדם. איננו נושאים באחריות לאי הבנות או לפרשנויות שגויות הנובעות משימוש בתרגום זה.\n" ] } ], "metadata": { "interpreter": { "hash": "16af2a8bbb083ea23e5e41c7f5787656b2ce26968575d8763f2c4b17f9cd711f" }, "kernelspec": { "display_name": "Python 3.8.12 ('py38')", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.8.12" }, "coopTranslator": { "original_hash": "522ee52ae3d5ae933e283286254e9a55", "translation_date": "2025-08-28T21:50:54+00:00", "source_file": "lessons/5-NLP/16-RNN/RNNPyTorch.ipynb", "language_code": "he" } }, "nbformat": 4, "nbformat_minor": 2 }