AI-For-Beginners/translations/he/lessons/5-NLP/17-GenerativeNetworks/GenerativePyTorch.ipynb

414 lines
23 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# רשתות גנרטיביות\n",
"\n",
"רשתות עצביות חוזרות (RNNs) והגרסאות שלהן עם תאים מבוקרים, כמו תאי זיכרון לטווח ארוך וקצר (LSTMs) ויחידות חוזרות מבוקרות (GRUs), מספקות מנגנון למידול שפה, כלומר, הן יכולות ללמוד את סדר המילים ולספק תחזיות למילה הבאה ברצף. הדבר מאפשר לנו להשתמש ב-RNNs למשימות **גנרטיביות**, כמו יצירת טקסט רגיל, תרגום מכונה ואפילו יצירת כיתוב לתמונות.\n",
"\n",
"בארכיטקטורת RNN שדנו בה ביחידה הקודמת, כל יחידת RNN הפיקה את מצב החבוי הבא כתוצאה. עם זאת, ניתן גם להוסיף פלט נוסף לכל יחידה חוזרת, מה שיאפשר לנו להפיק **רצף** (ששווה באורכו לרצף המקורי). יתרה מכך, ניתן להשתמש ביחידות 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",
"import numpy as np\n",
"from torchnlp import *\n",
"train_dataset,test_dataset,classes,vocab = load_dataset()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## בניית אוצר מילים של תווים\n",
"\n",
"כדי לבנות רשת גנרטיבית ברמת תווים, עלינו לפצל את הטקסט לתווים בודדים במקום למילים. ניתן לעשות זאת על ידי הגדרת טוקנייזר שונה:\n"
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Vocabulary size = 82\n",
"Encoding of 'a' is 1\n",
"Character with code 13 is c\n"
]
}
],
"source": [
"def char_tokenizer(words):\n",
" return list(words) #[word for word in words]\n",
"\n",
"counter = collections.Counter()\n",
"for (label, line) in train_dataset:\n",
" counter.update(char_tokenizer(line))\n",
"vocab = torchtext.vocab.vocab(counter)\n",
"\n",
"vocab_size = len(vocab)\n",
"print(f\"Vocabulary size = {vocab_size}\")\n",
"print(f\"Encoding of 'a' is {vocab.get_stoi()['a']}\")\n",
"print(f\"Character with code 13 is {vocab.get_itos()[13]}\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"בואו נראה את הדוגמה כיצד ניתן לקודד את הטקסט מתוך מערך הנתונים שלנו:\n"
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"tensor([ 0, 1, 2, 2, 3, 4, 5, 6, 3, 7, 8, 1, 9, 10, 3, 11, 2, 1,\n",
" 12, 3, 7, 1, 13, 14, 3, 15, 16, 5, 17, 3, 5, 18, 8, 3, 7, 2,\n",
" 1, 13, 14, 3, 19, 20, 8, 21, 5, 8, 9, 10, 22, 3, 20, 8, 21, 5,\n",
" 8, 9, 10, 3, 23, 3, 4, 18, 17, 9, 5, 23, 10, 8, 2, 2, 8, 9,\n",
" 10, 24, 3, 0, 1, 2, 2, 3, 4, 5, 9, 8, 8, 5, 25, 10, 3, 26,\n",
" 12, 27, 16, 26, 2, 27, 16, 28, 29, 30, 1, 16, 26, 3, 17, 31, 3, 21,\n",
" 2, 5, 9, 1, 23, 13, 32, 16, 27, 13, 10, 24, 3, 1, 9, 8, 3, 10,\n",
" 8, 8, 27, 16, 28, 3, 28, 9, 8, 8, 16, 3, 1, 28, 1, 27, 16, 6])"
]
},
"execution_count": 3,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"def enc(x):\n",
" return torch.LongTensor(encode(x,voc=vocab,tokenizer=char_tokenizer))\n",
"\n",
"enc(train_dataset[0][1])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## אימון RNN גנרטיבי\n",
"\n",
"הדרך שבה נאמן RNN לייצר טקסט היא כדלקמן. בכל שלב, ניקח רצף של תווים באורך `nchars`, ונבקש מהרשת לייצר את התו הבא עבור כל תו קלט:\n",
"\n",
"![תמונה המציגה דוגמה ליצירת המילה 'HELLO' באמצעות RNN.](../../../../../translated_images/he/rnn-generate.56c54afb52f9781d.webp)\n",
"\n",
"בהתאם לתרחיש בפועל, ייתכן שנרצה לכלול גם תווים מיוחדים, כמו *סוף רצף* `<eos>`. במקרה שלנו, אנחנו רק רוצים לאמן את הרשת ליצירת טקסט אינסופי, ולכן נקבע את גודל כל רצף להיות שווה ל-`nchars` טוקנים. כתוצאה מכך, כל דוגמת אימון תכלול `nchars` קלטים ו-`nchars` פלטים (שהם רצף הקלט מוזז סמל אחד שמאלה). מיניבאץ' יכלול כמה רצפים כאלה.\n",
"\n",
"הדרך שבה נייצר מיניבאצ'ים היא לקחת כל טקסט חדשות באורך `l`, ולייצר ממנו את כל שילובי הקלט-פלט האפשריים (יהיו `l-nchars` שילובים כאלה). הם יהוו מיניבאץ' אחד, וגודל המיניבאצ'ים יהיה שונה בכל שלב אימון.\n"
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"(tensor([[ 0, 1, 2, ..., 28, 29, 30],\n",
" [ 1, 2, 2, ..., 29, 30, 1],\n",
" [ 2, 2, 3, ..., 30, 1, 16],\n",
" ...,\n",
" [20, 8, 21, ..., 1, 28, 1],\n",
" [ 8, 21, 5, ..., 28, 1, 27],\n",
" [21, 5, 8, ..., 1, 27, 16]]),\n",
" tensor([[ 1, 2, 2, ..., 29, 30, 1],\n",
" [ 2, 2, 3, ..., 30, 1, 16],\n",
" [ 2, 3, 4, ..., 1, 16, 26],\n",
" ...,\n",
" [ 8, 21, 5, ..., 28, 1, 27],\n",
" [21, 5, 8, ..., 1, 27, 16],\n",
" [ 5, 8, 9, ..., 27, 16, 6]]))"
]
},
"execution_count": 4,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"nchars = 100\n",
"\n",
"def get_batch(s,nchars=nchars):\n",
" ins = torch.zeros(len(s)-nchars,nchars,dtype=torch.long,device=device)\n",
" outs = torch.zeros(len(s)-nchars,nchars,dtype=torch.long,device=device)\n",
" for i in range(len(s)-nchars):\n",
" ins[i] = enc(s[i:i+nchars])\n",
" outs[i] = enc(s[i+1:i+nchars+1])\n",
" return ins,outs\n",
"\n",
"get_batch(train_dataset[0][1])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"עכשיו נגדיר את רשת הגנרטור. היא יכולה להתבסס על כל תא חוזר שדיברנו עליו ביחידה הקודמת (פשוט, LSTM או GRU). בדוגמה שלנו נשתמש ב-LSTM.\n",
"\n",
"מכיוון שהרשת מקבלת תווים כקלט וגודל אוצר המילים די קטן, אין צורך בשכבת הטמעה; קלט מקודד ב-one-hot יכול לעבור ישירות לתא LSTM. עם זאת, מכיוון שאנחנו מעבירים מספרי תווים כקלט, עלינו לקודד אותם ב-one-hot לפני העברתם ל-LSTM. זה נעשה על ידי קריאה לפונקציה `one_hot` במהלך מעבר `forward`. מקודד הפלט יהיה שכבה ליניארית שתמיר את המצב הנסתר לפלט מקודד ב-one-hot.\n"
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {},
"outputs": [],
"source": [
"class LSTMGenerator(torch.nn.Module):\n",
" def __init__(self, vocab_size, hidden_dim):\n",
" super().__init__()\n",
" self.rnn = torch.nn.LSTM(vocab_size,hidden_dim,batch_first=True)\n",
" self.fc = torch.nn.Linear(hidden_dim, vocab_size)\n",
"\n",
" def forward(self, x, s=None):\n",
" x = torch.nn.functional.one_hot(x,vocab_size).to(torch.float32)\n",
" x,s = self.rnn(x,s)\n",
" return self.fc(x),s"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"במהלך האימון, אנחנו רוצים להיות מסוגלים לדגום טקסט שנוצר. כדי לעשות זאת, נגדיר פונקציה בשם `generate` שתפיק מחרוזת פלט באורך `size`, שמתחילה מהמחרוזת ההתחלתית `start`.\n",
"\n",
"כך זה עובד: ראשית, נעביר את כל המחרוזת ההתחלתית דרך הרשת, ונקבל את מצב הפלט `s` ואת התו הבא החזוי `out`. מכיוון ש-`out` מקודד בשיטת one-hot, נשתמש ב-`argmax` כדי לקבל את האינדקס של התו `nc` במילון, ונשתמש ב-`itos` כדי לזהות את התו בפועל ולהוסיף אותו לרשימת התווים `chars`. התהליך הזה של יצירת תו אחד חוזר על עצמו `size` פעמים כדי לייצר את מספר התווים הנדרש.\n"
]
},
{
"cell_type": "code",
"execution_count": 8,
"metadata": {},
"outputs": [],
"source": [
"def generate(net,size=100,start='today '):\n",
" chars = list(start)\n",
" out, s = net(enc(chars).view(1,-1).to(device))\n",
" for i in range(size):\n",
" nc = torch.argmax(out[0][-1])\n",
" chars.append(vocab.get_itos()[nc])\n",
" out, s = net(nc.view(1,-1),s)\n",
" return ''.join(chars)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"עכשיו בואו נתחיל את האימון! לולאת האימון כמעט זהה לכל הדוגמאות הקודמות שלנו, אבל במקום דיוק אנחנו מדפיסים טקסט שנוצר באופן מדגמי כל 1000 אפוקים.\n",
"\n",
"יש לשים דגש מיוחד על הדרך שבה אנחנו מחשבים את ההפסד. אנחנו צריכים לחשב את ההפסד בהתחשב בפלט המקודד ב-one-hot `out`, ובטקסט הצפוי `text_out`, שהוא רשימת אינדקסי התווים. למרבה המזל, הפונקציה `cross_entropy` מצפה לפלט לא מנורמל של הרשת כארגומנט הראשון, ולמספר המחלקה כארגומנט השני, שזה בדיוק מה שיש לנו. היא גם מבצעת ממוצע אוטומטי על גודל המיני-באטץ'.\n",
"\n",
"אנחנו גם מגבילים את האימון לפי מספר הדגימות `samples_to_train`, כדי שלא נצטרך להמתין יותר מדי זמן. אנחנו מעודדים אתכם להתנסות ולנסות אימון ארוך יותר, אולי לכמה אפוקים (במקרה כזה תצטרכו ליצור לולאה נוספת סביב הקוד הזה).\n"
]
},
{
"cell_type": "code",
"execution_count": 9,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Current loss = 4.398899078369141\n",
"today sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr s\n",
"Current loss = 2.161320447921753\n",
"today and to the tor to to the tor to to the tor to to the tor to to the tor to to the tor to to the tor t\n",
"Current loss = 1.6722588539123535\n",
"today and the court to the could to the could to the could to the could to the could to the could to the c\n",
"Current loss = 2.423795223236084\n",
"today and a second to the conternation of the conternation of the conternation of the conternation of the \n",
"Current loss = 1.702607274055481\n",
"today and the company to the company to the company to the company to the company to the company to the co\n",
"Current loss = 1.692358136177063\n",
"today and the company to the company to the company to the company to the company to the company to the co\n",
"Current loss = 1.9722288846969604\n",
"today and the control the control the control the control the control the control the control the control \n",
"Current loss = 1.8705692291259766\n",
"today and the second to the second to the second to the second to the second to the second to the second t\n",
"Current loss = 1.7626899480819702\n",
"today and a security and a security and a security and a security and a security and a security and a secu\n",
"Current loss = 1.5574463605880737\n",
"today and the company and the company and the company and the company and the company and the company and \n",
"Current loss = 1.5620026588439941\n",
"today and the be that the be the be that the be the be that the be the be that the be the be that the be t\n"
]
}
],
"source": [
"net = LSTMGenerator(vocab_size,64).to(device)\n",
"\n",
"samples_to_train = 10000\n",
"optimizer = torch.optim.Adam(net.parameters(),0.01)\n",
"loss_fn = torch.nn.CrossEntropyLoss()\n",
"net.train()\n",
"for i,x in enumerate(train_dataset):\n",
" # x[0] is class label, x[1] is text\n",
" if len(x[1])-nchars<10:\n",
" continue\n",
" samples_to_train-=1\n",
" if not samples_to_train: break\n",
" text_in, text_out = get_batch(x[1])\n",
" optimizer.zero_grad()\n",
" out,s = net(text_in)\n",
" loss = torch.nn.functional.cross_entropy(out.view(-1,vocab_size),text_out.flatten()) #cross_entropy(out,labels)\n",
" loss.backward()\n",
" optimizer.step()\n",
" if i%1000==0:\n",
" print(f\"Current loss = {loss.item()}\")\n",
" print(generate(net))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"דוגמה זו כבר מייצרת טקסט די טוב, אך ניתן לשפר אותה בכמה דרכים:\n",
"\n",
"* **שיפור יצירת מיניבאצ'ים**. הדרך שבה הכנו את הנתונים לאימון הייתה יצירת מיניבאץ' אחד מתוך דגימה אחת. זה לא אידיאלי, מכיוון שמיניבאצ'ים הם בגדלים שונים, וחלקם אפילו לא יכולים להיווצר, כי הטקסט קטן מ-`nchars`. בנוסף, מיניבאצ'ים קטנים לא מנצלים את ה-GPU בצורה מספקת. יהיה חכם יותר לקחת מקטע טקסט גדול מכל הדגימות, ואז ליצור את כל זוגות הקלט-פלט, לערבב אותם, וליצור מיניבאצ'ים בגודל שווה.\n",
"\n",
"* **LSTM רב-שכבתי**. יש היגיון לנסות 2 או 3 שכבות של תאי LSTM. כפי שציינו ביחידה הקודמת, כל שכבה של LSTM מוציאה דפוסים מסוימים מהטקסט, ובמקרה של מחולל ברמת תווים, ניתן לצפות שהשכבה הנמוכה של LSTM תהיה אחראית על חילוץ הברות, והשכבות הגבוהות - על מילים ושילובי מילים. ניתן ליישם זאת בפשטות על ידי העברת פרמטר מספר-השכבות לבנאי של LSTM.\n",
"\n",
"* ייתכן שתרצה גם להתנסות עם **יחידות GRU** ולבדוק אילו מהן מבצעות טוב יותר, וכן עם **גדלים שונים של שכבות נסתרות**. שכבה נסתרת גדולה מדי עשויה להוביל לבעיה של התאמת יתר (לדוגמה, הרשת תלמד את הטקסט המדויק), וגודל קטן יותר עשוי שלא להפיק תוצאה טובה.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## יצירת טקסט רך וטמפרטורה\n",
"\n",
"בהגדרה הקודמת של `generate`, תמיד בחרנו את התו עם ההסתברות הגבוהה ביותר כתו הבא בטקסט שנוצר. הדבר הוביל לכך שהטקסט לעיתים קרובות \"חזר על עצמו\" בין רצפי תווים זהים שוב ושוב, כמו בדוגמה הזו:\n",
"```\n",
"today of the second the company and a second the company ...\n",
"```\n",
"\n",
"עם זאת, אם נבחן את התפלגות ההסתברויות עבור התו הבא, ייתכן שההבדל בין כמה מההסתברויות הגבוהות ביותר אינו גדול, לדוגמה, תו אחד יכול להיות בעל הסתברות של 0.2, ותו אחר - 0.19, וכו'. לדוגמה, כאשר מחפשים את התו הבא ברצף '*play*', התו הבא יכול להיות באותה מידה רווח או **e** (כמו במילה *player*).\n",
"\n",
"מסקנה זו מובילה אותנו להבנה שלא תמיד \"הוגן\" לבחור את התו עם ההסתברות הגבוהה ביותר, משום שבחירה בתו עם ההסתברות השנייה הגבוהה עדיין יכולה להוביל לטקסט משמעותי. חכם יותר **לדגום** תווים מתוך התפלגות ההסתברויות שמתקבלת מפלט הרשת.\n",
"\n",
"דגימה זו יכולה להתבצע באמצעות הפונקציה `multinomial`, שמיישמת את מה שנקרא **התפלגות מולטינומית**. פונקציה שמיישמת את יצירת הטקסט ה**רך** הזו מוגדרת להלן:\n"
]
},
{
"cell_type": "code",
"execution_count": 10,
"metadata": {
"scrolled": true
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"--- Temperature = 0.3\n",
"Today and a company and complete an all the land the restrational the as a security and has provers the pay to and a report and the computer in the stand has filities and working the law the stations for a company and with the company and the final the first company and refight of the state and and workin\n",
"\n",
"--- Temperature = 0.8\n",
"Today he oniis its first to Aus bomblaties the marmation a to manan boogot that pirate assaid a relaid their that goverfin the the Cappets Ecrotional Assonia Cition targets it annight the w scyments Blamity #39;s TVeer Diercheg Reserals fran envyuil that of ster said access what succers of Dour-provelith\n",
"\n",
"--- Temperature = 1.0\n",
"Today holy they a 11 will meda a toket subsuaties, engins for Chanos, they's has stainger past to opening orital his thempting new Nattona was al innerforder advan-than #36;s night year his religuled talitatian what the but with Wednesday to Justment will wemen of Mark CCC Camp as Timed Nae wome a leaders\n",
"\n",
"--- Temperature = 1.3\n",
"Today gpone 2.5 fech atcusion poor cocles toparsdorM.cht Line Pamage put 43 his calt lowed to the book, that has authh-the silia rruch ailing to'ory andhes beutirsimi- Aefffive heading offil an auf eacklets is charged evis, Gunymy oy) Mony has it after-sloythyor loveId out filme, the Natabl -Najuntaxiggs \n",
"\n",
"--- Temperature = 1.8\n",
"Today plary, P.slan chly\\401 mardregationly #39;t 8.1Mide) closes ,filtcon alfly playin roven!\\grea.-QFBEP: Iss onfarchQ/itilia CCf Zivesigntwasta orce.-Peul-aw.uicrin of fuglinfsut aftaningwo, MIEX awayew Aice Woiduar Corvagiugge oppo esig ThusBratourid canthly-RyI.co lagitems\\eexciaishes.conBabntusmor I\n",
"\n"
]
}
],
"source": [
"def generate_soft(net,size=100,start='today ',temperature=1.0):\n",
" chars = list(start)\n",
" out, s = net(enc(chars).view(1,-1).to(device))\n",
" for i in range(size):\n",
" #nc = torch.argmax(out[0][-1])\n",
" out_dist = out[0][-1].div(temperature).exp()\n",
" nc = torch.multinomial(out_dist,1)[0]\n",
" chars.append(vocab.get_itos()[nc])\n",
" out, s = net(nc.view(1,-1),s)\n",
" return ''.join(chars)\n",
" \n",
"for i in [0.3,0.8,1.0,1.3,1.8]:\n",
" print(f\"--- Temperature = {i}\\n{generate_soft(net,size=300,start='Today ',temperature=i)}\\n\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"הוספנו פרמטר נוסף שנקרא **טמפרטורה**, שמשמש לציון עד כמה עלינו להיצמד להסתברות הגבוהה ביותר. אם הטמפרטורה היא 1.0, אנו מבצעים דגימה מולטינומית הוגנת, וכאשר הטמפרטורה עולה לאינסוף - כל ההסתברויות הופכות לשוות, ואנו בוחרים באופן אקראי את התו הבא. בדוגמה למטה ניתן לראות שהטקסט הופך לחסר משמעות כאשר אנו מעלים את הטמפרטורה יותר מדי, והוא מזכיר טקסט \"מחזורי\" שנוצר באופן קשיח כאשר הטמפרטורה מתקרבת ל-0.\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": "7673cd150d96c74c6d6011460094efb4",
"translation_date": "2025-08-28T21:34:47+00:00",
"source_file": "lessons/5-NLP/17-GenerativeNetworks/GenerativePyTorch.ipynb",
"language_code": "he"
}
},
"nbformat": 4,
"nbformat_minor": 4
}