AI-For-Beginners/translations/he/lessons/5-NLP/13-TextRep/TextRepresentationPyTorch.i...

577 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",
"כפי שציינו, נתמקד במשימת סיווג טקסט פשוטה המבוססת על **AG_NEWS** dataset, שמטרתה לסווג כותרות חדשות לאחת מתוך 4 קטגוריות: עולם, ספורט, עסקים ומדע/טכנולוגיה.\n",
"\n",
"## מערך הנתונים\n",
"\n",
"מערך נתונים זה מובנה בתוך המודול [`torchtext`](https://github.com/pytorch/text), כך שניתן לגשת אליו בקלות.\n"
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [],
"source": [
"import torch\n",
"import torchtext\n",
"import os\n",
"import collections\n",
"os.makedirs('./data',exist_ok=True)\n",
"train_dataset, test_dataset = torchtext.datasets.AG_NEWS(root='./data')\n",
"classes = ['World', 'Sports', 'Business', 'Sci/Tech']"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"כאן, `train_dataset` ו-`test_dataset` מכילים אוספים שמחזירים זוגות של תווית (מספר של מחלקה) וטקסט בהתאמה, לדוגמה:\n"
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"(3,\n",
" \"Wall St. Bears Claw Back Into the Black (Reuters) Reuters - Short-sellers, Wall Street's dwindling\\\\band of ultra-cynics, are seeing green again.\")"
]
},
"execution_count": 2,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"list(train_dataset)[0]"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"אז, בואו נדפיס את עשרת הכותרות החדשות הראשונות מהמאגר שלנו:\n"
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"**Sci/Tech** -> Wall St. Bears Claw Back Into the Black (Reuters) Reuters - Short-sellers, Wall Street's dwindling\\band of ultra-cynics, are seeing green again.\n",
"**Sci/Tech** -> Carlyle Looks Toward Commercial Aerospace (Reuters) Reuters - Private investment firm Carlyle Group,\\which has a reputation for making well-timed and occasionally\\controversial plays in the defense industry, has quietly placed\\its bets on another part of the market.\n",
"**Sci/Tech** -> Oil and Economy Cloud Stocks' Outlook (Reuters) Reuters - Soaring crude prices plus worries\\about the economy and the outlook for earnings are expected to\\hang over the stock market next week during the depth of the\\summer doldrums.\n",
"**Sci/Tech** -> Iraq Halts Oil Exports from Main Southern Pipeline (Reuters) Reuters - Authorities have halted oil export\\flows from the main pipeline in southern Iraq after\\intelligence showed a rebel militia could strike\\infrastructure, an oil official said on Saturday.\n",
"**Sci/Tech** -> Oil prices soar to all-time record, posing new menace to US economy (AFP) AFP - Tearaway world oil prices, toppling records and straining wallets, present a new economic menace barely three months before the US presidential elections.\n"
]
}
],
"source": [
"for i,x in zip(range(5),train_dataset):\n",
" print(f\"**{classes[x[0]]}** -> {x[1]}\")\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"מכיוון שמערכי נתונים הם איטרטורים, אם אנחנו רוצים להשתמש בנתונים מספר פעמים אנחנו צריכים להמיר אותם לרשימה:\n"
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {},
"outputs": [],
"source": [
"train_dataset, test_dataset = torchtext.datasets.AG_NEWS(root='./data')\n",
"train_dataset = list(train_dataset)\n",
"test_dataset = list(test_dataset)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## טוקניזציה\n",
"\n",
"עכשיו אנחנו צריכים להמיר טקסט ל**מספרים** שניתן לייצג כטנסורים. אם אנחנו רוצים ייצוג ברמת מילים, יש לבצע שני דברים:\n",
"* להשתמש ב**טוקנייזר** כדי לחלק את הטקסט ל**טוקנים**\n",
"* לבנות **אוצר מילים** של אותם טוקנים.\n"
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"['he', 'said', 'hello']"
]
},
"execution_count": 4,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"tokenizer = torchtext.data.utils.get_tokenizer('basic_english')\n",
"tokenizer('He said: hello')"
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {},
"outputs": [],
"source": [
"counter = collections.Counter()\n",
"for (label, line) in train_dataset:\n",
" counter.update(tokenizer(line))\n",
"vocab = torchtext.vocab.vocab(counter, min_freq=1)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"באמצעות אוצר מילים, אנו יכולים בקלות לקודד את המחרוזת המפוצלת שלנו למערך של מספרים:\n"
]
},
{
"cell_type": "code",
"execution_count": 19,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Vocab size if 95810\n"
]
},
{
"data": {
"text/plain": [
"[599, 3279, 97, 1220, 329, 225, 7368]"
]
},
"execution_count": 19,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"vocab_size = len(vocab)\n",
"print(f\"Vocab size if {vocab_size}\")\n",
"\n",
"stoi = vocab.get_stoi() # dict to convert tokens to indices\n",
"\n",
"def encode(x):\n",
" return [stoi[s] for s in tokenizer(x)]\n",
"\n",
"encode('I love to play with my words')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## ייצוג טקסט בשיטת Bag of Words\n",
"\n",
"מכיוון שמילים מייצגות משמעות, לפעמים אפשר להבין את המשמעות של טקסט רק על ידי התבוננות במילים הבודדות, בלי להתחשב בסדר שלהן במשפט. לדוגמה, כאשר מסווגים חדשות, מילים כמו *מזג אוויר*, *שלג* עשויות להצביע על *תחזית מזג אוויר*, בעוד שמילים כמו *מניות*, *דולר* ייחשבו כחלק מ*חדשות כלכליות*.\n",
"\n",
"ייצוג וקטורי **Bag of Words** (BoW) הוא הייצוג הווקטורי המסורתי הנפוץ ביותר. כל מילה מקושרת לאינדקס בווקטור, והאלמנט בווקטור מכיל את מספר ההופעות של מילה מסוימת במסמך נתון.\n",
"\n",
"![תמונה שמראה כיצד ייצוג וקטורי בשיטת Bag of Words מיוצג בזיכרון.](../../../../../translated_images/bag-of-words-example.606fc1738f1d7ba98a9d693e3bcd706c6e83fa7bf8221e6e90d1a206d82f2ea4.he.png)\n",
"\n",
"> **Note**: ניתן גם לחשוב על BoW כסכום של כל הווקטורים המקודדים בשיטת one-hot עבור המילים הבודדות בטקסט.\n",
"\n",
"להלן דוגמה ליצירת ייצוג בשיטת Bag of Words באמצעות ספריית Scikit Learn בשפת Python:\n"
]
},
{
"cell_type": "code",
"execution_count": 7,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"array([[1, 1, 0, 2, 0, 0, 0, 0, 0]], dtype=int64)"
]
},
"execution_count": 7,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"from sklearn.feature_extraction.text import CountVectorizer\n",
"vectorizer = CountVectorizer()\n",
"corpus = [\n",
" 'I like hot dogs.',\n",
" 'The dog ran fast.',\n",
" 'Its hot outside.',\n",
" ]\n",
"vectorizer.fit_transform(corpus)\n",
"vectorizer.transform(['My dog likes hot dogs on a hot day.']).toarray()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"כדי לחשב וקטור של שקית מילים מתוך הייצוג הווקטורי של מערך הנתונים AG_NEWS שלנו, נוכל להשתמש בפונקציה הבאה:\n"
]
},
{
"cell_type": "code",
"execution_count": 20,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"tensor([2., 1., 2., ..., 0., 0., 0.])\n"
]
}
],
"source": [
"vocab_size = len(vocab)\n",
"\n",
"def to_bow(text,bow_vocab_size=vocab_size):\n",
" res = torch.zeros(bow_vocab_size,dtype=torch.float32)\n",
" for i in encode(text):\n",
" if i<bow_vocab_size:\n",
" res[i] += 1\n",
" return res\n",
"\n",
"print(to_bow(train_dataset[0][1]))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"> **הערה:** כאן אנו משתמשים במשתנה הגלובלי `vocab_size` כדי לציין את גודל ברירת המחדל של אוצר המילים. מכיוון שלעתים קרובות גודל אוצר המילים די גדול, ניתן להגביל את גודל אוצר המילים למילים הנפוצות ביותר. נסו להקטין את הערך של `vocab_size` ולהריץ את הקוד למטה, ותראו כיצד זה משפיע על הדיוק. אתם אמורים לצפות לירידה מסוימת בדיוק, אך לא דרמטית, בתמורה לביצועים גבוהים יותר.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## אימון מסווג BoW\n",
"\n",
"עכשיו, כשכבר למדנו איך לבנות ייצוג Bag-of-Words לטקסט שלנו, בואו נאמן מסווג על בסיסו. קודם כל, עלינו להמיר את מערך הנתונים שלנו לאימון כך שכל הייצוגים הווקטוריים הפוזיציוניים יומרו לייצוג Bag-of-Words. ניתן לעשות זאת על ידי העברת הפונקציה `bowify` כפרמטר `collate_fn` ל-`DataLoader` הסטנדרטי של torch:\n"
]
},
{
"cell_type": "code",
"execution_count": 21,
"metadata": {},
"outputs": [],
"source": [
"from torch.utils.data import DataLoader\n",
"import numpy as np \n",
"\n",
"# this collate function gets list of batch_size tuples, and needs to \n",
"# return a pair of label-feature tensors for the whole minibatch\n",
"def bowify(b):\n",
" return (\n",
" torch.LongTensor([t[0]-1 for t in b]),\n",
" torch.stack([to_bow(t[1]) for t in b])\n",
" )\n",
"\n",
"train_loader = DataLoader(train_dataset, batch_size=16, collate_fn=bowify, shuffle=True)\n",
"test_loader = DataLoader(test_dataset, batch_size=16, collate_fn=bowify, shuffle=True)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"עכשיו נגדיר רשת עצבית מסווגת פשוטה שמכילה שכבה ליניארית אחת. גודל וקטור הקלט שווה ל-`vocab_size`, וגודל הפלט מתאים למספר הקטגוריות (4). מכיוון שאנחנו פותרים משימת סיווג, פונקציית ההפעלה הסופית היא `LogSoftmax()`.\n"
]
},
{
"cell_type": "code",
"execution_count": 22,
"metadata": {},
"outputs": [],
"source": [
"net = torch.nn.Sequential(torch.nn.Linear(vocab_size,4),torch.nn.LogSoftmax(dim=1))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"עכשיו נגדיר לולאת אימון סטנדרטית של PyTorch. מכיוון שהמערך שלנו די גדול, למטרת ההוראה שלנו נאמן רק למשך אפוקה אחת, ולפעמים אפילו פחות מאפוקה (הגדרת הפרמטר `epoch_size` מאפשרת לנו להגביל את האימון). כמו כן, נדווח על דיוק האימון המצטבר במהלך האימון; תדירות הדיווח מוגדרת באמצעות הפרמטר `report_freq`.\n"
]
},
{
"cell_type": "code",
"execution_count": 24,
"metadata": {},
"outputs": [],
"source": [
"def train_epoch(net,dataloader,lr=0.01,optimizer=None,loss_fn = torch.nn.NLLLoss(),epoch_size=None, report_freq=200):\n",
" optimizer = optimizer or torch.optim.Adam(net.parameters(),lr=lr)\n",
" net.train()\n",
" total_loss,acc,count,i = 0,0,0,0\n",
" for labels,features in dataloader:\n",
" optimizer.zero_grad()\n",
" out = net(features)\n",
" loss = loss_fn(out,labels) #cross_entropy(out,labels)\n",
" loss.backward()\n",
" optimizer.step()\n",
" total_loss+=loss\n",
" _,predicted = torch.max(out,1)\n",
" acc+=(predicted==labels).sum()\n",
" count+=len(labels)\n",
" i+=1\n",
" if i%report_freq==0:\n",
" print(f\"{count}: acc={acc.item()/count}\")\n",
" if epoch_size and count>epoch_size:\n",
" break\n",
" return total_loss.item()/count, acc.item()/count"
]
},
{
"cell_type": "code",
"execution_count": 25,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"3200: acc=0.8028125\n",
"6400: acc=0.8371875\n",
"9600: acc=0.8534375\n",
"12800: acc=0.85765625\n"
]
},
{
"data": {
"text/plain": [
"(0.026090790722161722, 0.8620069296375267)"
]
},
"execution_count": 25,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"train_epoch(net,train_loader,epoch_size=15000)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## בי-גרמים, טרי-גרמים ו-N-גרמים\n",
"\n",
"אחת המגבלות של גישת שקית המילים היא שחלק מהמילים הן חלק מביטויים רב-מילתיים. לדוגמה, המילה 'נקניקייה' ('hot dog') בעלת משמעות שונה לחלוטין מהמילים 'חם' ('hot') ו'כלב' ('dog') בהקשרים אחרים. אם נייצג תמיד את המילים 'חם' ו'כלב' באמצעות אותם וקטורים, זה עלול לבלבל את המודל שלנו.\n",
"\n",
"כדי להתמודד עם זה, **ייצוגי N-גרמים** משמשים לעיתים קרובות בשיטות לסיווג מסמכים, שבהן התדירות של כל מילה, זוג מילים או שלישיית מילים מהווה תכונה שימושית לאימון מסווגים. בייצוג בי-גרמים, לדוגמה, נוסיף את כל זוגות המילים לאוצר המילים, בנוסף למילים המקוריות.\n",
"\n",
"להלן דוגמה כיצד ליצור ייצוג שקית מילים של בי-גרמים באמצעות Scikit Learn:\n"
]
},
{
"cell_type": "code",
"execution_count": 26,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Vocabulary:\n",
" {'i': 7, 'like': 11, 'hot': 4, 'dogs': 2, 'i like': 8, 'like hot': 12, 'hot dogs': 5, 'the': 16, 'dog': 0, 'ran': 14, 'fast': 3, 'the dog': 17, 'dog ran': 1, 'ran fast': 15, 'its': 9, 'outside': 13, 'its hot': 10, 'hot outside': 6}\n"
]
},
{
"data": {
"text/plain": [
"array([[1, 0, 1, 0, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]],\n",
" dtype=int64)"
]
},
"execution_count": 26,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"bigram_vectorizer = CountVectorizer(ngram_range=(1, 2), token_pattern=r'\\b\\w+\\b', min_df=1)\n",
"corpus = [\n",
" 'I like hot dogs.',\n",
" 'The dog ran fast.',\n",
" 'Its hot outside.',\n",
" ]\n",
"bigram_vectorizer.fit_transform(corpus)\n",
"print(\"Vocabulary:\\n\",bigram_vectorizer.vocabulary_)\n",
"bigram_vectorizer.transform(['My dog likes hot dogs on a hot day.']).toarray()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"החיסרון העיקרי בגישת N-gram הוא שהגודל של אוצר המילים מתחיל לגדול בצורה מהירה מאוד. בפועל, אנחנו צריכים לשלב את ייצוג ה-N-gram עם טכניקות לצמצום ממדים, כמו *embeddings*, עליהן נדון ביחידה הבאה.\n",
"\n",
"כדי להשתמש בייצוג N-gram במאגר הנתונים **AG News**, אנחנו צריכים לבנות אוצר מילים מיוחד של ngram:\n"
]
},
{
"cell_type": "code",
"execution_count": 27,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Bigram vocabulary length = 1308842\n"
]
}
],
"source": [
"counter = collections.Counter()\n",
"for (label, line) in train_dataset:\n",
" l = tokenizer(line)\n",
" counter.update(torchtext.data.utils.ngrams_iterator(l,ngrams=2))\n",
" \n",
"bi_vocab = torchtext.vocab.vocab(counter, min_freq=1)\n",
"\n",
"print(\"Bigram vocabulary length = \",len(bi_vocab))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"נוכל להשתמש באותו קוד כמו למעלה כדי לאמן את הסיווג, אך זה יהיה מאוד לא יעיל מבחינת זיכרון. ביחידה הבאה, נאמן סיווג ביגרם באמצעות embeddings.\n",
"\n",
"> **הערה:** ניתן להשאיר רק את ה-ngrams שמופיעים בטקסט יותר ממספר הפעמים שצוין. זה יבטיח שביגרמים נדירים יושמטו, ויקטין משמעותית את הממדיות. כדי לעשות זאת, יש להגדיר את הפרמטר `min_freq` לערך גבוה יותר, ולצפות בשינוי באורך אוצר המילים.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## תדירות מונח-היפוך תדירות מסמך TF-IDF\n",
"\n",
"בייצוג BoW, הופעות של מילים מקבלות משקל שווה, ללא קשר למילה עצמה. עם זאת, ברור שמילים נפוצות כמו *a*, *in* וכו' הן הרבה פחות חשובות לסיווג מאשר מונחים מיוחדים. למעשה, ברוב משימות עיבוד שפה טבעית, יש מילים שהן רלוונטיות יותר מאחרות.\n",
"\n",
"**TF-IDF** מייצג **תדירות מונח–היפוך תדירות מסמך**. זהו וריאציה של תיקיית מילים (bag of words), שבה במקום ערך בינארי 0/1 שמציין את הופעתה של מילה במסמך, נעשה שימוש בערך עשרוני, הקשור לתדירות הופעת המילה בקורפוס.\n",
"\n",
"באופן פורמלי יותר, המשקל $w_{ij}$ של מילה $i$ במסמך $j$ מוגדר כ:\n",
"$$\n",
"w_{ij} = tf_{ij}\\times\\log({N\\over df_i})\n",
"$$\n",
"כאשר\n",
"* $tf_{ij}$ הוא מספר ההופעות של $i$ ב-$j$, כלומר ערך BoW שראינו קודם\n",
"* $N$ הוא מספר המסמכים באוסף\n",
"* $df_i$ הוא מספר המסמכים המכילים את המילה $i$ בכל האוסף\n",
"\n",
"ערך TF-IDF $w_{ij}$ עולה באופן יחסי למספר הפעמים שמילה מופיעה במסמך ומותאם לפי מספר המסמכים בקורפוס שמכילים את המילה, מה שעוזר להתחשב בעובדה שיש מילים שמופיעות בתדירות גבוהה יותר מאחרות. לדוגמה, אם מילה מופיעה *בכל* המסמכים באוסף, $df_i=N$, ו-$w_{ij}=0$, והמונחים הללו יוזנחו לחלוטין.\n",
"\n",
"ניתן ליצור בקלות וקטוריזציה של טקסט באמצעות TF-IDF בעזרת Scikit Learn:\n"
]
},
{
"cell_type": "code",
"execution_count": 28,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"array([[0.43381609, 0. , 0.43381609, 0. , 0.65985664,\n",
" 0.43381609, 0. , 0. , 0. , 0. ,\n",
" 0. , 0. , 0. , 0. , 0. ,\n",
" 0. ]])"
]
},
"execution_count": 28,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"from sklearn.feature_extraction.text import TfidfVectorizer\n",
"vectorizer = TfidfVectorizer(ngram_range=(1,2))\n",
"vectorizer.fit_transform(corpus)\n",
"vectorizer.transform(['My dog likes hot dogs on a hot day.']).toarray()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## מסקנה\n",
"\n",
"עם זאת, למרות שייצוגי TF-IDF מספקים משקל תדירות למילים שונות, הם אינם מסוגלים לייצג משמעות או סדר. כפי שאמר הבלשן המפורסם ג'יי. אר. פירת' בשנת 1935, \"המשמעות המלאה של מילה תמיד תלויה בהקשר, ואין לקחת ברצינות שום מחקר על משמעות מחוץ להקשר.\" בהמשך הקורס נלמד כיצד ללכוד מידע הקשרי מטקסט באמצעות מודלים לשוניים.\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": "7b9040985e748e4e2d4c689892456ad7",
"translation_date": "2025-08-28T21:59:57+00:00",
"source_file": "lessons/5-NLP/13-TextRep/TextRepresentationPyTorch.ipynb",
"language_code": "he"
}
},
"nbformat": 4,
"nbformat_minor": 2
}