647 lines
29 KiB
Plaintext
647 lines
29 KiB
Plaintext
{
|
||
"cells": [
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"# משימת סיווג טקסט\n",
|
||
"\n",
|
||
"במודול זה נתחיל עם משימת סיווג טקסט פשוטה המבוססת על מערך הנתונים **[AG_NEWS](http://www.di.unipi.it/~gulli/AG_corpus_of_news_articles.html)**: נסווג כותרות חדשות לאחת מתוך 4 קטגוריות: עולם, ספורט, עסקים ומדע/טכנולוגיה.\n",
|
||
"\n",
|
||
"## מערך הנתונים\n",
|
||
"\n",
|
||
"כדי לטעון את מערך הנתונים, נשתמש ב-API של **[TensorFlow Datasets](https://www.tensorflow.org/datasets)**.\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 1,
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"import tensorflow as tf\n",
|
||
"from tensorflow import keras\n",
|
||
"import tensorflow_datasets as tfds\n",
|
||
"\n",
|
||
"# In this tutorial, we will be training a lot of models. In order to use GPU memory cautiously,\n",
|
||
"# we will set tensorflow option to grow GPU memory allocation when required.\n",
|
||
"physical_devices = tf.config.list_physical_devices('GPU') \n",
|
||
"if len(physical_devices)>0:\n",
|
||
" tf.config.experimental.set_memory_growth(physical_devices[0], True)\n",
|
||
"\n",
|
||
"dataset = tfds.load('ag_news_subset')"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"כעת אנו יכולים לגשת לחלקי האימון והמבחן של מערך הנתונים באמצעות `dataset['train']` ו-`dataset['test']` בהתאמה:\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 3,
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"Length of train dataset = 120000\n",
|
||
"Length of test dataset = 7600\n"
|
||
]
|
||
}
|
||
],
|
||
"source": [
|
||
"ds_train = dataset['train']\n",
|
||
"ds_test = dataset['test']\n",
|
||
"\n",
|
||
"print(f\"Length of train dataset = {len(ds_train)}\")\n",
|
||
"print(f\"Length of test dataset = {len(ds_test)}\")"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"בואו נדפיס את 10 הכותרות החדשות הראשונות מהמאגר שלנו:\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 4,
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"3 (Sci/Tech) -> b'AMD Debuts Dual-Core Opteron Processor' b'AMD #39;s new dual-core Opteron chip is designed mainly for corporate computing applications, including databases, Web services, and financial transactions.'\n",
|
||
"1 (Sports) -> b\"Wood's Suspension Upheld (Reuters)\" b'Reuters - Major League Baseball\\\\Monday announced a decision on the appeal filed by Chicago Cubs\\\\pitcher Kerry Wood regarding a suspension stemming from an\\\\incident earlier this season.'\n",
|
||
"2 (Business) -> b'Bush reform may have blue states seeing red' b'President Bush #39;s quot;revenue-neutral quot; tax reform needs losers to balance its winners, and people claiming the federal deduction for state and local taxes may be in administration planners #39; sights, news reports say.'\n",
|
||
"3 (Sci/Tech) -> b\"'Halt science decline in schools'\" b'Britain will run out of leading scientists unless science education is improved, says Professor Colin Pillinger.'\n",
|
||
"1 (Sports) -> b'Gerrard leaves practice' b'London, England (Sports Network) - England midfielder Steven Gerrard injured his groin late in Thursday #39;s training session, but is hopeful he will be ready for Saturday #39;s World Cup qualifier against Austria.'\n"
|
||
]
|
||
}
|
||
],
|
||
"source": [
|
||
"classes = ['World', 'Sports', 'Business', 'Sci/Tech']\n",
|
||
"\n",
|
||
"for i,x in zip(range(5),ds_train):\n",
|
||
" print(f\"{x['label']} ({classes[x['label']]}) -> {x['title']} {x['description']}\")"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"## וקטוריזציה של טקסט\n",
|
||
"\n",
|
||
"עכשיו אנחנו צריכים להמיר טקסט ל**מספרים** שניתן לייצג כטנסורים. אם אנחנו רוצים ייצוג ברמת המילים, עלינו לבצע שני דברים:\n",
|
||
"\n",
|
||
"* להשתמש ב**טוקנייזר** כדי לפצל את הטקסט ל**טוקנים**.\n",
|
||
"* לבנות **אוצר מילים** של אותם טוקנים.\n",
|
||
"\n",
|
||
"### הגבלת גודל אוצר המילים\n",
|
||
"\n",
|
||
"בדוגמה של מערך הנתונים AG News, גודל אוצר המילים די גדול, יותר מ-100 אלף מילים. באופן כללי, אנחנו לא צריכים מילים שמופיעות לעיתים רחוקות בטקסט — רק כמה משפטים יכילו אותן, והמודל לא ילמד מהן. לכן, יש היגיון להגביל את גודל אוצר המילים למספר קטן יותר על ידי העברת פרמטר לבנאי של הוקטורייזר:\n",
|
||
"\n",
|
||
"שני השלבים הללו יכולים להתבצע באמצעות שכבת **TextVectorization**. בואו ניצור את אובייקט הוקטורייזר, ואז נקרא למתודה `adapt` כדי לעבור על כל הטקסט ולבנות אוצר מילים:\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 5,
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"vocab_size = 50000\n",
|
||
"vectorizer = keras.layers.experimental.preprocessing.TextVectorization(max_tokens=vocab_size)\n",
|
||
"vectorizer.adapt(ds_train.take(500).map(lambda x: x['title']+' '+x['description']))"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"> **שימו לב** שאנחנו משתמשים רק בתת-קבוצה של כל מערך הנתונים כדי לבנות אוצר מילים. אנחנו עושים זאת כדי להאיץ את זמן הביצוע ולא להשאיר אתכם ממתינים. עם זאת, אנחנו לוקחים את הסיכון שחלק מהמילים מכלל מערך הנתונים לא ייכללו באוצר המילים, ויתעלמו מהן במהלך האימון. לכן, שימוש בגודל אוצר המילים המלא והרצה על כל מערך הנתונים במהלך `adapt` עשויים להעלות את הדיוק הסופי, אך לא באופן משמעותי.\n",
|
||
"\n",
|
||
"כעת נוכל לגשת לאוצר המילים בפועל:\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 6,
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"['', '[UNK]', 'the', 'to', 'a', 'in', 'of', 'and', 'on', 'for']\n",
|
||
"Length of vocabulary: 5335\n"
|
||
]
|
||
}
|
||
],
|
||
"source": [
|
||
"vocab = vectorizer.get_vocabulary()\n",
|
||
"vocab_size = len(vocab)\n",
|
||
"print(vocab[:10])\n",
|
||
"print(f\"Length of vocabulary: {vocab_size}\")"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"באמצעות הוקטורייזר, אנו יכולים בקלות לקודד כל טקסט למערך של מספרים:\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 7,
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"data": {
|
||
"text/plain": [
|
||
"<tf.Tensor: shape=(7,), dtype=int64, numpy=array([ 112, 3695, 3, 304, 11, 1041, 1], dtype=int64)>"
|
||
]
|
||
},
|
||
"execution_count": 7,
|
||
"metadata": {},
|
||
"output_type": "execute_result"
|
||
}
|
||
],
|
||
"source": [
|
||
"vectorizer('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",
|
||
" \n",
|
||
"\n",
|
||
"> **Note**: ניתן גם לחשוב על BoW כסכום של כל הווקטורים המקודדים בשיטת one-hot עבור המילים הבודדות בטקסט.\n",
|
||
"\n",
|
||
"להלן דוגמה ליצירת ייצוג בשיטת Bag-of-words באמצעות ספריית הפייתון Scikit Learn:\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 8,
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"data": {
|
||
"text/plain": [
|
||
"array([[1, 1, 0, 2, 0, 0, 0, 0, 0]], dtype=int64)"
|
||
]
|
||
},
|
||
"execution_count": 8,
|
||
"metadata": {},
|
||
"output_type": "execute_result"
|
||
}
|
||
],
|
||
"source": [
|
||
"from sklearn.feature_extraction.text import CountVectorizer\n",
|
||
"sc_vectorizer = CountVectorizer()\n",
|
||
"corpus = [\n",
|
||
" 'I like hot dogs.',\n",
|
||
" 'The dog ran fast.',\n",
|
||
" 'Its hot outside.',\n",
|
||
" ]\n",
|
||
"sc_vectorizer.fit_transform(corpus)\n",
|
||
"sc_vectorizer.transform(['My dog likes hot dogs on a hot day.']).toarray()"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"אנחנו יכולים גם להשתמש בווקטורייזר של Keras שהגדרנו למעלה, להמיר כל מספר של מילה לקידוד one-hot ולהוסיף את כל הווקטורים הללו יחד:\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 9,
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"data": {
|
||
"text/plain": [
|
||
"array([0., 5., 0., ..., 0., 0., 0.], dtype=float32)"
|
||
]
|
||
},
|
||
"execution_count": 9,
|
||
"metadata": {},
|
||
"output_type": "execute_result"
|
||
}
|
||
],
|
||
"source": [
|
||
"def to_bow(text):\n",
|
||
" return tf.reduce_sum(tf.one_hot(vectorizer(text),vocab_size),axis=0)\n",
|
||
"\n",
|
||
"to_bow('My dog likes hot dogs on a hot day.').numpy()"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"> **הערה**: ייתכן שתופתעו שהתוצאה שונה מהדוגמה הקודמת. הסיבה לכך היא שבדוגמה של Keras, אורך הווקטור תואם לגודל אוצר המילים, שנבנה מכלל מאגר הנתונים של AG News, בעוד שבדוגמה של Scikit Learn בנינו את אוצר המילים מתוך טקסט הדוגמה באופן מיידי.\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"## אימון מסווג BoW\n",
|
||
"\n",
|
||
"עכשיו, לאחר שלמדנו כיצד לבנות את ייצוג תיקיית המילים (bag-of-words) של הטקסט שלנו, בואו נאמן מסווג שמשתמש בו. ראשית, עלינו להמיר את מערך הנתונים שלנו לייצוג תיקיית המילים. ניתן לעשות זאת באמצעות הפונקציה `map` באופן הבא:\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 11,
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"batch_size = 128\n",
|
||
"\n",
|
||
"ds_train_bow = ds_train.map(lambda x: (to_bow(x['title']+x['description']),x['label'])).batch(batch_size)\n",
|
||
"ds_test_bow = ds_test.map(lambda x: (to_bow(x['title']+x['description']),x['label'])).batch(batch_size)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"עכשיו נגדיר רשת עצבית מסווגת פשוטה שמכילה שכבה ליניארית אחת. גודל הקלט הוא `vocab_size`, וגודל הפלט מתאים למספר הקטגוריות (4). מכיוון שאנחנו פותרים משימת סיווג, פונקציית ההפעלה הסופית היא **softmax**:\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 12,
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"938/938 [==============================] - 66s 70ms/step - loss: 0.6144 - acc: 0.8427 - val_loss: 0.4416 - val_acc: 0.8697\n"
|
||
]
|
||
},
|
||
{
|
||
"data": {
|
||
"text/plain": [
|
||
"<keras.callbacks.History at 0x20c70a947f0>"
|
||
]
|
||
},
|
||
"execution_count": 12,
|
||
"metadata": {},
|
||
"output_type": "execute_result"
|
||
}
|
||
],
|
||
"source": [
|
||
"model = keras.models.Sequential([\n",
|
||
" keras.layers.Dense(4,activation='softmax',input_shape=(vocab_size,))\n",
|
||
"])\n",
|
||
"model.compile(loss='sparse_categorical_crossentropy',optimizer='adam',metrics=['acc'])\n",
|
||
"model.fit(ds_train_bow,validation_data=ds_test_bow)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"מכיוון שיש לנו 4 קטגוריות, דיוק של מעל 80% נחשב לתוצאה טובה.\n",
|
||
"\n",
|
||
"## אימון מסווג כרשת אחת\n",
|
||
"\n",
|
||
"מכיוון שהוקטורייזר הוא גם שכבת Keras, אנחנו יכולים להגדיר רשת שכוללת אותו ולאמן אותה מקצה לקצה. בצורה הזו, אין צורך לוקטור את מערך הנתונים באמצעות `map`, ואפשר פשוט להעביר את מערך הנתונים המקורי כקלט לרשת.\n",
|
||
"\n",
|
||
"> **הערה**: עדיין נצטרך להחיל פעולות `map` על מערך הנתונים כדי להמיר שדות ממילונים (כמו `title`, `description` ו-`label`) לטאפלים. עם זאת, כאשר טוענים נתונים מהדיסק, ניתן לבנות מראש מערך נתונים עם המבנה הנדרש.\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 13,
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"Model: \"model\"\n",
|
||
"_________________________________________________________________\n",
|
||
" Layer (type) Output Shape Param # \n",
|
||
"=================================================================\n",
|
||
" input_1 (InputLayer) [(None, 1)] 0 \n",
|
||
" \n",
|
||
" text_vectorization (TextVec (None, None) 0 \n",
|
||
" torization) \n",
|
||
" \n",
|
||
" tf.one_hot (TFOpLambda) (None, None, 5335) 0 \n",
|
||
" \n",
|
||
" tf.math.reduce_sum (TFOpLam (None, 5335) 0 \n",
|
||
" bda) \n",
|
||
" \n",
|
||
" dense_2 (Dense) (None, 4) 21344 \n",
|
||
" \n",
|
||
"=================================================================\n",
|
||
"Total params: 21,344\n",
|
||
"Trainable params: 21,344\n",
|
||
"Non-trainable params: 0\n",
|
||
"_________________________________________________________________\n",
|
||
"938/938 [==============================] - 73s 77ms/step - loss: 0.6057 - acc: 0.8414 - val_loss: 0.4202 - val_acc: 0.8736\n"
|
||
]
|
||
},
|
||
{
|
||
"data": {
|
||
"text/plain": [
|
||
"<keras.callbacks.History at 0x20c721521f0>"
|
||
]
|
||
},
|
||
"execution_count": 13,
|
||
"metadata": {},
|
||
"output_type": "execute_result"
|
||
}
|
||
],
|
||
"source": [
|
||
"def extract_text(x):\n",
|
||
" return x['title']+' '+x['description']\n",
|
||
"\n",
|
||
"def tupelize(x):\n",
|
||
" return (extract_text(x),x['label'])\n",
|
||
"\n",
|
||
"inp = keras.Input(shape=(1,),dtype=tf.string)\n",
|
||
"x = vectorizer(inp)\n",
|
||
"x = tf.reduce_sum(tf.one_hot(x,vocab_size),axis=1)\n",
|
||
"out = keras.layers.Dense(4,activation='softmax')(x)\n",
|
||
"model = keras.models.Model(inp,out)\n",
|
||
"model.summary()\n",
|
||
"\n",
|
||
"model.compile(loss='sparse_categorical_crossentropy',optimizer='adam',metrics=['acc'])\n",
|
||
"model.fit(ds_train.map(tupelize).batch(batch_size),validation_data=ds_test.map(tupelize).batch(batch_size))\n"
|
||
]
|
||
},
|
||
{
|
||
"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": 14,
|
||
"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": 14,
|
||
"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**, עלינו להעביר את הפרמטר `ngrams` לבנאי של `TextVectorization`. האורך של אוצר מילים של ביגרם הוא **גדול משמעותית**, ובמקרה שלנו הוא יותר מ-1.3 מיליון טוקנים! לכן, יש היגיון להגביל גם את טוקני הביגרם למספר סביר כלשהו.\n",
|
||
"\n",
|
||
"נוכל להשתמש באותו קוד כמו קודם כדי לאמן את הסיווג, אך זה יהיה מאוד לא יעיל מבחינת זיכרון. ביחידה הבאה, נאמן את סיווג הביגרם באמצעות embeddings. בינתיים, תוכלו להתנסות באימון סיווג הביגרם במחברת זו ולראות אם תוכלו להשיג דיוק גבוה יותר.\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"## חישוב אוטומטי של וקטורי BoW\n",
|
||
"\n",
|
||
"בדוגמה למעלה חישבנו וקטורי BoW באופן ידני על ידי סכימת הקידודים הבינאריים של מילים בודדות. עם זאת, הגרסה האחרונה של TensorFlow מאפשרת לנו לחשב וקטורי BoW באופן אוטומטי על ידי העברת הפרמטר `output_mode='count` לקונסטרקטור של הווקטורייזר. זה הופך את ההגדרה והאימון של המודל שלנו לקלים משמעותית:\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 15,
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"Training vectorizer\n",
|
||
"938/938 [==============================] - 7s 7ms/step - loss: 0.5929 - acc: 0.8486 - val_loss: 0.4168 - val_acc: 0.8772\n"
|
||
]
|
||
},
|
||
{
|
||
"data": {
|
||
"text/plain": [
|
||
"<keras.callbacks.History at 0x20c725217c0>"
|
||
]
|
||
},
|
||
"execution_count": 15,
|
||
"metadata": {},
|
||
"output_type": "execute_result"
|
||
}
|
||
],
|
||
"source": [
|
||
"model = keras.models.Sequential([\n",
|
||
" keras.layers.experimental.preprocessing.TextVectorization(max_tokens=vocab_size,output_mode='count'),\n",
|
||
" keras.layers.Dense(4,input_shape=(vocab_size,), activation='softmax')\n",
|
||
"])\n",
|
||
"print(\"Training vectorizer\")\n",
|
||
"model.layers[0].adapt(ds_train.take(500).map(extract_text))\n",
|
||
"model.compile(loss='sparse_categorical_crossentropy',optimizer='adam',metrics=['acc'])\n",
|
||
"model.fit(ds_train.map(tupelize).batch(batch_size),validation_data=ds_test.map(tupelize).batch(batch_size))"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"## תדירות מונחים - תדירות מסמכים הפוכה (TF-IDF)\n",
|
||
"\n",
|
||
"בייצוג BoW, משקל ההופעות של מילים מחושב באותה שיטה ללא קשר למילה עצמה. עם זאת, ברור שמילים נפוצות כמו *a* ו-*in* הן הרבה פחות חשובות לסיווג מאשר מונחים ייחודיים. ברוב משימות עיבוד שפה טבעית (NLP), יש מילים שהן רלוונטיות יותר מאחרות.\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": 16,
|
||
"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": 16,
|
||
"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": [
|
||
"בקראס, ניתן לחשב באופן אוטומטי את תדירויות TF-IDF באמצעות העברת הפרמטר `output_mode='tf-idf'` לשכבת `TextVectorization`. בואו נחזור על הקוד שהשתמשנו בו קודם כדי לראות אם שימוש ב-TF-IDF מעלה את הדיוק:\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 17,
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"Training vectorizer\n",
|
||
"938/938 [==============================] - 12s 12ms/step - loss: 0.4197 - acc: 0.8662 - val_loss: 0.3432 - val_acc: 0.8849\n"
|
||
]
|
||
},
|
||
{
|
||
"data": {
|
||
"text/plain": [
|
||
"<keras.callbacks.History at 0x20c729dfd30>"
|
||
]
|
||
},
|
||
"execution_count": 17,
|
||
"metadata": {},
|
||
"output_type": "execute_result"
|
||
}
|
||
],
|
||
"source": [
|
||
"model = keras.models.Sequential([\n",
|
||
" keras.layers.experimental.preprocessing.TextVectorization(max_tokens=vocab_size,output_mode='tf-idf'),\n",
|
||
" keras.layers.Dense(4,input_shape=(vocab_size,), activation='softmax')\n",
|
||
"])\n",
|
||
"print(\"Training vectorizer\")\n",
|
||
"model.layers[0].adapt(ds_train.take(500).map(extract_text))\n",
|
||
"model.compile(loss='sparse_categorical_crossentropy',optimizer='adam',metrics=['acc'])\n",
|
||
"model.fit(ds_train.map(tupelize).batch(batch_size),validation_data=ds_test.map(tupelize).batch(batch_size))"
|
||
]
|
||
},
|
||
{
|
||
"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": "0cb620c6d4b9f7a635928804c26cf22403d89d98d79684e4529119355ee6d5a5"
|
||
},
|
||
"kernel_info": {
|
||
"name": "conda-env-py37_tensorflow-py"
|
||
},
|
||
"kernelspec": {
|
||
"display_name": "py37_tensorflow",
|
||
"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"
|
||
},
|
||
"nteract": {
|
||
"version": "nteract-front-end@1.0.0"
|
||
},
|
||
"coopTranslator": {
|
||
"original_hash": "19b43951d55b377a76209c24c1f017e4",
|
||
"translation_date": "2025-08-28T22:02:46+00:00",
|
||
"source_file": "lessons/5-NLP/13-TextRep/TextRepresentationTF.ipynb",
|
||
"language_code": "he"
|
||
}
|
||
},
|
||
"nbformat": 4,
|
||
"nbformat_minor": 4
|
||
} |