זיהוי SMS ספאמי באמצעות מודל RNN

מחבר:
בתאריך:

במדריך הקודם ראינו כיצד לאתר הודעות SMS ספאמיות בשיטת bag of words בה אין חשיבות לסדר המילים. במדריך זה נראה כיצד להשתמש במודלים מבוססי סדר באמצעות רשתות מסוג RNN להשגת אותה מטרה.

במדריך זה נציג יישום של RNN בשביל למידת מכונה במסגרת עיבוד שפה אנושית NLP הכולל 3 שלבים:

  1. הפיכת מילים לטוקנים כאשר לכל טוקן ייחודי מוקצה ספרת אינדקס.
  2. מיפוי כל ספרת אינדקס לוקטור הנושא משמעות סמנטית במסגרת שכבת embedding.
  3. שימוש ברשת נוירונית מסוג RNN ללימוד הקשרים בין הוקטורים ברצף.

להורדת הקוד שנפתח במדריך

מודל מבוסס רצף מסוג RNN לסיווג הודעות SMS

 

נייבא את הספריות הדרושות ללמידת מכונה:

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

בשביל לעבוד עם מערכת הקבצים:

import os

את למידת המכונה נעשה באמצעות TensorFlow וממשק Keras:

import tensorflow as tf
from tensorflow import keras

 

מסד הנתונים

את מסד הנתונים הורדתי כקובץ CSV מאתר התחרויות Kaggle https://www.kaggle.com/datasets/uciml/sms-spam-collection-dataset

# import dataset
df = pd.read_csv('spam.csv', encoding = "ISO-8859-1", usecols=["v1", "v2"])
df.columns = ["Category", "Message"]
df

ham or spam SMS dataset from Kaggle

df.shape
(5572, 2)
df.isnull().sum().sum()
0
df.Category.unique()
array(['ham', 'spam'], dtype=object)
df.groupby(['Category']).count()

Category

Message

ham

4825

spam

747

categories = df.Category.unique() # ['ham','spam']
NUM_CLASSES = len(categories)

מסד הנתונים כולל 5,547 הודעות SMS באנגלית המסווגות ל-2 קטגוריות: ham (לגיטימיות) או spam. הסט אינו מאוזן עם 13.5% בלבד מההודעות המסווגות ספאם.

 

הכנת הנתונים ללמידת מכונה

כדי לעבוד עם ה-pipeline של Keras נסדר את הודעות הטקסט בתיקיות ע"פ המבנה:

data/
  exp_0/
    test/
      ham/
      spam/
    train/
      ham/
      spam/
    val/
      ham/
      spam/

בתוך התיקייה data תהיה תיקיית הניסוי exp_0, ובתוכה 3 תיקיות ע"פ החלוקה המקובלת לקבוצת אימון מבחן ובקרה (test, train, val). בתוך כל אחת משלוש התיקיות יהיו תיקיות הקטגוריה: ham או spam. כל אחת מתיקיות הקטגוריה יחזיקו קבצי טקסט (סיומת txt) שבכל אחד מהקבצים תהיה כתובה הודעת SMS אחת.

נסדר את הקבצים בתיקיות:

BASE_DIR = './'
DATA_DIR = os.path.join(BASE_DIR, 'data/')
# make data directory
!mkdir -p ./data/
!mkdir -p ./data/ham_or_spam
SRC_DIR = os.path.join(DATA_DIR, 'ham_or_spam')
!mkdir -p ./data/ham_or_spam/ham
!mkdir -p ./data/ham_or_spam/spam
len_df = len(df)
for idx, row in df.iterrows():
   if idx > len_df:
       break
   else:
       new_path = os.path.join(DATA_DIR, 'ham_or_spam', row[0], str(idx)+'.txt')
       f = open(new_path, 'w')
       f.write(row[1])
       f.close()
       idx+=1
dir_list = os.listdir(SRC_DIR)
for name in sorted(dir_list):
   path = os.path.join(name)
   print(path)
ham
spam
# make data directory for the experiment
EXP_DIR = "exp_0"
!mkdir -p ./data/exp_0/
# make train, val, test directories
!mkdir -p ./data/exp_0/train ./data/exp_0/val ./data/exp_0/test
 
TRAIN_DIR = os.path.join(DATA_DIR, EXP_DIR, "train")
VAL_DIR = os.path.join(DATA_DIR, EXP_DIR, "val")
TEST_DIR = os.path.join(DATA_DIR, EXP_DIR, "test")
import os, pathlib, shutil, random
for category in categories:
   if not os.path.exists(os.path.join(TRAIN_DIR, category)):
       os.makedirs(os.path.join(TRAIN_DIR, category))
   if not os.path.exists(os.path.join(VAL_DIR, category)):
       os.makedirs(os.path.join(VAL_DIR, category))
  
   files = os.listdir(os.path.join(SRC_DIR, category))
   random.Random(42).shuffle(files)
   num_val_samples = int(0.3 * len(files))
   val_files = files[-num_val_samples:]
   for fname in val_files:
       new_fname = fname
       shutil.copy2(os.path.join(SRC_DIR, category, fname),
                   os.path.join(VAL_DIR, category, new_fname))
   train_files = files[:-num_val_samples]
   for fname in train_files:
       new_fname = fname
       shutil.copy2(os.path.join(SRC_DIR, category, fname),
                   os.path.join(TRAIN_DIR, category, new_fname))
for category in categories:
   if not os.path.exists(os.path.join(TEST_DIR, category)):
       os.makedirs(os.path.join(TEST_DIR, category))
 
   files = os.listdir(os.path.join(VAL_DIR, category))
   random.Random(42).shuffle(files)
   num_val_samples = int(0.33 * len(files))
   val_files = files[:num_val_samples]
   for fname in val_files:
       new_fname = fname
       shutil.move(os.path.join(VAL_DIR, category, fname),
                   os.path.join(TEST_DIR, category, new_fname))

נוודא את מה שעשינו:

for category in categories:
   file_count = len(os.listdir(os.path.join(TRAIN_DIR, category)))
   print(f"TRAIN {category} has {file_count} files")
 
   file_count = len(os.listdir(os.path.join(VAL_DIR, category)))
   print(f"VAL {category} has {file_count} files")
 
   file_count = len(os.listdir(os.path.join(TEST_DIR, category)))
   print(f"TEST {category} has {file_count} files")
TRAIN ham has 3378 files
VAL ham has 970 files
TEST ham has 477 files
TRAIN spam has 523 files
VAL spam has 151 files
TEST spam has 73 files
  • סה"כ 6 תיקיות.

נייבא את התיקיות ל- Keras במבנה של אצוות batches תוך התחשבות בקטגוריות:

batch_size = 32
 
train_ds = keras.utils.text_dataset_from_directory(
   os.path.join(DATA_DIR, EXP_DIR, "train"),
   label_mode="categorical",
   batch_size=batch_size
)
 
val_ds = keras.utils.text_dataset_from_directory(
   os.path.join(DATA_DIR, EXP_DIR, "val"),
   label_mode="categorical",
   batch_size=batch_size
)
 
test_ds = keras.utils.text_dataset_from_directory(
   os.path.join(DATA_DIR, EXP_DIR, "test"),
   label_mode="categorical",
   shuffle=False,
   batch_size=batch_size
)
Found 3901 files belonging to 2 classes.
Found 1121 files belonging to 2 classes.
Found 550 files belonging to 2 classes.

התוצאה הינה generator. נציץ בו:

for inputs, targets in train_ds:
   print("inputs.shape:", inputs.shape)
   print("inputs.dtype:", inputs.dtype)
   print("targets.shape:", targets.shape)
   print("targets.dtype:", targets.dtype)
   print("inputs[0]:", inputs[0])
   print("targets[0]:", targets[0])
   break
inputs.shape: (32,)
inputs.dtype: 
targets.shape: (32, 2)
targets.dtype: dtype: 'float32'
inputs[0]: tf.Tensor(b'Hi Petey!noixc3xa5xc3x95m ok just wanted 2 chat coz avent spoken 2 u 4 a long time-hope ur doin alrite.have good nit at js love ya am.x', shape=(), dtype=string)
targets[0]: tf.Tensor([1. 0.], shape=(2,), dtype=float32)

כיוון שמחשבים לא יודעים לעבוד עם טקסט צריך להפוך את התכנים לוקטורים המייצגים כל אחד טוקן אחר (המונח token מקביל פחות או יותר למילה). תהליך ההפיכה לוקטורים מכונה וקטוריזציה. נעזר בשכבת TextVectorization כדי לעשות וקטוריזציה לטקסט.

קודם שנערוך וקטוריזציה עלינו למצוא את האורך המקסימלי של הוקטורים המקודדים מפני שזה יהיה האורך של הוקטורים אותם נזין לרשת הנוירונית בגלל ש-RNN מסוגל לעבוד רק עם רצפים בעלי אורך זהה. כדי לקבל רצפים באורך זהה ראשית נקבע את האורך הרצוי, ואחר כך נעבור על כל אחד מהרצפים ונקטע רצפים ארוכים מדי או נרפד באפס רצפים קצרים מדי. בחירה מושכלת של האורך חשובה מפני שרצפים ארוכים מדי גורמים להאטה משמעותית של התהליך ולמהילת הסיגנל, בעוד רצפים קצרים מדי עלולים לפספס את החלקים האינפורמטיביים ביותר אם הם נמצאים במורד הרצף באותו חלק שנקטע. כדי למצוא את האורך המקסימלי של הוקטורים המקודדים נחשב את מספר המילים הממוצע ברצפים, ולכך נוסיף 2 סטיות תקן כדי להצליח לקודד את מלוא האורך של כ- 97% מהרצפים (תחת הנחה של התפלגות נורמלית):

import math
 
messages = df.Message.values
lengths = [len(msg.split()) for msg in messages]
arr = np.array(lengths)
mean = np.mean(arr, axis=0)
stdev = np.std(arr, axis=0)
max_len = math.floor(mean + 2 * stdev)
print("mean: ", mean)
print("stdev: ", stdev)
print("max_len: ", max_len)

התוצאה:

mean:  15.494436468054559
stdev:  11.328410463078066
max_len:  38
  • אורכה הממוצע של הודעת טקסט הוא 15.5 מילים וסטיית התקן היא 11.3 מילים. נוסיף 2 סטיות תקן לממוצע ונקבל שאורך הוקטורים המקודדים צריך להיות 38 מילים כדי ללכוד את האורך המלא של כ-97% מההודעות.

נגדיר, בהתאם, את האורך המקסימלי של הוקטורים המקודדים על 38 טוקנים:

MAX_SEQUENCE_LEN = 38

במסגרת תהליך הוקטוריזציה רצפים ארוכים מ-38 טוקנים יקוצצו ורצפים קצרים יותר "ירופדו" באפסים לצורך השלמת אורכם. בסיומו יקבלו כל הוקטורים המקודדים אורך אחיד של 38 טוקנים.

נגדיר את מספרם המקסימלי של הטוקנים באוצר המילים הכללי על 20,000 לכל היותר כי זה המספר המקובל ביותר לאוצר מילים בתחום עיבוד שפת אנוש NLP:

MAX_TOKENS = 20000

נאתחל את המתודה של Keras שתעשה וקטוריזציה:

from keras import layers
 
# vectorization layer
text_vectorization = layers.TextVectorization(
   max_tokens=MAX_TOKENS,
   output_mode="int",
   output_sequence_length=MAX_SEQUENCE_LEN,
)

תהליך וקטוריזציה כולל:

  • סטנדרטיזציה standardization של טקסטים במסגרתה הופכות כל האותיות לקטנות, מוסרים סימני הפיסוק, ומוסרים תווים לא סטנדרטיים.
  • הפיכה לטוקנים tokenization על ידי חיתוך הרצף ברווחים שבין המילים.
  • אינדוקס indexing המקנה לכל טוקן מספר אינדקס ייחודי (עד למספר מקסימום של 20,000 טוקנים)

נוסף לכך, העברנו למתודה שתפקידה להפוך רצפים לוקטורים TextVectorization() את הפרמטר:

  • output_mode="int" - אשר מאנדקס את הטוקנים על ידי כך שהוא יוצר מילון אשר ממפה כל טוקן ייחודי על סיפרת אינדקס ייחודית. שני האינדקסים הראשונים במילון שמורים לטוקנים מיוחדים: הטוקן שמספר האינדקס שלו 0 מוקצה לריפוד padding, הטוקן שמספר האינדקס שלו 1 מוקצה למילה לא קיימת במסד הנתונים (מסומן [UNK]), וכל יתר הטוקנים מוקצים על פי מידת השכיחות. קודם השכיחים ביותר.

נאמן את Keras על סט נתוני האימון בלבד באמצעות המתודה adapt():

# prepare a text only dataset
text_only_train_ds = train_ds.map(lambda x, y: x)
 
# the adapt method needs to learn
# to vectorize only on the train dataset
text_vectorization.adapt(text_only_train_ds)

נבחן את הפונקציה שתפקידה לעשות וקטוריזציה על טקסט לדוגמה:

# vectorize a test sentence
output = text_vectorization([["i love the amiga"]])
print(output)
tf.Tensor(
[[ 2 66  6  1  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0
   0  0  0  0  0  0  0  0  0  0  0  0  0  0]], shape=(1, 38), dtype=int64)

המידע נמצא בתוך טנסור של TensorFlow. נחלץ אותו לפורמט שיהיה לנו קל יותר לעבוד איתו:

print(output.numpy()[0])
[ 2 66  6  1  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0
  0  0  0  0  0  0  0  0  0  0  0  0  0  0]
  • אורך הוקטור המקודד הוא 38 פיקסלים
  • הפונקציה שיצרה את הוקטור שיבצה בכל עמדה את ספרת האינדקס של הטוקן המתאים מתוך אוצר המילים

מה במילון אוצר המילים?

# a dict mapping words to their indices
vocab = text_vectorization.get_vocabulary()
word_index = dict(zip(vocab, range(len(vocab))))
word_index
{'': 0,
 '[UNK]': 1,
 'i': 2,
 'to': 3,
 'you': 4,
 'a': 5,
 'the': 6,
…
'mobiles': 992,
 'mistake': 993,
 'men': 994,
 'meh': 995,
 'march': 996,
 'loves': 997,
 'login': 998,
 'loan': 999,
 ...}
  • הטוקן שמספר האינדקס שלו 0 מוקצה לריפוד padding, ומשמש להשלמת הרצף במקרה של רצפים קצרים מדי.
  • הטוקן שמספר האינדקס שלו 1 מוקצה לכל המילים שלא קיימת במסד הנתונים (מסומן [UNK])
  • כל יתר הטוקנים מוקצים על פי מידת השכיחות. קודם השכיחים ביותר. לכן הראשונים להופיע הם: 'i', 'to', 'you', 'a', 'the'
  • הטוקנים מכילים אותיות קטנות לטיניות סטנדרטיות ומספרים בלבד. ללא סימני פיסוק ורווחים. בהתאם, נמצא במילון את הטוקן 'i' ולא 'I'.

נהפוך את ה-keys וה-values של מילון אוצר המילים:

inv_word_index = {v: k for k, v in word_index.items()}
inv_word_index
{0: '',
 1: '[UNK]',
 2: 'i',
 3: 'to',
 4: 'you',
 5: 'a',
 6: 'the',
…

מה הפריט האחרון במילון אוצר המילים ההפוך?

print("last item key: ", list(inv_word_index)[-1])
print("last item value: ", inv_word_index[list(inv_word_index)[-1]])
last item key:  7783
last item value:  0089my
  • פריט שמספר האינדקס שלו הוא 7783 וערכו '0089my'

מהיכן צץ הטוקן המוזר הזה?

df.query('Message.str.contains("0089")', engine='python')
Category Message
1686 spam todays vodafone numbers ending with 0089(my la...

נבחן את המתודה המקודדת שלנו על ידי הזנת הטקסט הקצר הבא:

# vectorize a test sentence
output = text_vectorization([["i love the amiga"]])
print(output)
 
# extract the array from the tensor
print(output.numpy()[0])
tf.Tensor(
[[ 2 66  6  1  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0
   0  0  0  0  0  0  0  0  0  0  0  0  0  0]], shape=(1, 38), dtype=int64)
 
[ 2 66  6  1  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0
  0  0  0  0  0  0  0  0  0  0  0  0  0  0]
  • מערך של 38 טוקנים: 4 הראשונים מקודדים כי זה מספר המילים במשפט וכל היתר הם padding.

נמצה חזרה את המשפט מתוך הוקטור המקודד:

np_outputs = output.numpy()[0]
 
words = []
for token in np_outputs: 
   if inv_word_index[token]:
       words.append(inv_word_index[token])
 
print(' '.join(words))
i love the [UNK]
  • המילה האחרונה מקבלת [UNK] היות והמילה "amiga" אינה קיימת באוצר המילים.

ניישם את המתודה שעושה וקטוריזציה על מסדי הנתונים:

# vectorize the 3 datasets
vectorized_train_ds = train_ds.map(
   lambda x, y: (text_vectorization(x), y))
 
vectorized_val_ds = val_ds.map(
   lambda x, y: (text_vectorization(x), y))
 
vectorized_test_ds = test_ds.map(
   lambda x, y: (text_vectorization(x), y))

מה קיבלנו?

for inputs, targets in vectorized_train_ds:
   print("inputs.shape:", inputs.shape)
   print("inputs.dtype:", inputs.dtype)
   print("targets.shape:", targets.shape)
   print("targets.dtype:", targets.dtype)
   print("inputs[0]:", inputs[0])
   print("targets[0]:", targets[0])
   break
inputs.shape: (32, 38)
inputs.dtype: dtype: 'int64'
targets.shape: (32, 2)
targets.dtype: dtype: 'float32'
inputs[0]: tf.Tensor(
[  43   10  937    4    5  151   73 2718  244   11   85   14  388    8
   71  218    2   61 4286    4  388   25 3068    3   49    3  151 5516
   19   12 7035    8   42 4172   12 1358   10  215], shape=(38,), dtype=int64)
targets[0]: tf.Tensor([1. 0.], shape=(2,), dtype=float32)
  • בכל אצווה 32 וקטורים שאורך כל אחד מהם 38 טוקנים.

 

מודל RNN לסיווג טקסטים

המודל שלנו יכיל שכבתRNN דו-צדדית Bidirectional RNN כיוון שזו ארכיטקטורת מודל המיועדת ללמוד מרצפים:

# RNN model with an embedding layer
# trained from scratch
def get_model(max_tokens, hidden_dim):
   inputs = keras.Input(shape=(None,), dtype="int64")                             
   embedded = layers.Embedding(input_dim=max_tokens,
                               output_dim=128,
                               mask_zero=True)(inputs)
   x = layers.Bidirectional(layers.LSTM(hidden_dim))(embedded)
   x = layers.Dropout(0.25)(x)
   outputs = layers.Dense(NUM_CLASSES, activation="softmax")(x)
   model = keras.Model(inputs, outputs)
 
   return model
  • את שכבת ה- RNN מזינה שכבת Embedding אשר מקודדת את המשמעות הסמנטית של כל אחד מהטוקנים במרחב הטמעה embedding בעל N מימדים. במקרה שלנו N=128. המשמעות הסמנטית מבוטאת במידת הקרבה והיחסים בין הוקטורים במרחב גיאומטרי. לדוגמה, במרחב הטמעה סביר, הוקטורים המייצגים את צמד המילים "מלך" ו-"מלכה" יתקבצו יחדיו הרחק מצמד המילים "גמל" ו-"נאקה". בנוסף, כיוון הקשר בתוך צמדי המילים יהיה דומה כיוון שהוא מבטא יחס של מין (זכר ונקבה).
  • במקרה שלנו, שכבת ה- embedding לומדת את ערכי הוקטורים במהלך האימון באמצעות back propagation.
  • ניתן להיעזר בשכבתembedding שאומנה מראש pre-trained word embedding בפרט אם כמות הטקסט העומדת לרשותינו לצורך אימון הרשת הנוירונית היא קטנה מדי. במקרה שלנו, נאמן את שכבת ה-embedding מאפס. מה שאפשרי היות שיש לנו מספיק טקסט להתאמן עליו. יתרון נוסף של אימון מאפס עשוי להיות במקרה שאנו מעוניינים ליצור מודל בתחום המאופיין בז'רגון ייחודי. לדוגמה, מודל NLP העוסק בחוזים משפטיים או מאמרים ביוכימיים.
  • כפי שראינו, בתהליך הוקטוריזציה הרצפים הקצרים מרופדים באפסים עד לאורך הרצוי. הבעיה עם הפרקטיקה הזו שהיא גורמת למהילת הסיגנל ברצפים קצרים כי אותו RNN שמגיע לקצה של רצף מלא אפסים "שוכח" את ראשית הרצף. כדי לפתור את הבעיה הוספנו לשכבת ה- Embedding את הפרמטר mask_zero=True המורה ל- RNN להתעלם מחלקי הוקטור המרופדים באפסים. את הפרמטר אנחנו מוסיפים לשכבת ה- embedding וההוראה הזו עוברת ליתר השכבות, ובפרט שכבות ה- RNN באמצעות Keras.

מה במודל?

model = get_model(MAX_TOKENS, 64)
model.summary()
_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 input_1 (InputLayer)        [(None, None)]            0         
                                                                 
 embedding (Embedding)       (None, None, 128)         2560000   
                                                                 
 bidirectional (Bidirectiona  (None, 128)              98816     
 l)                                                              
                                                                 
 dropout (Dropout)           (None, 128)               0         
                                                                 
 dense (Dense)               (None, 2)                 258       
                                                                 
=================================================================
Total params: 2,659,074
Trainable params: 2,659,074
Non-trainable params: 0

נקמפל:

model.compile(optimizer=tf.keras.optimizers.Adam(),
             loss="categorical_crossentropy",
             metrics=["accuracy"])

את המודל הרצתי במשך 10 סיבובים על GPU:

callbacks = [
   keras.callbacks.ModelCheckpoint("nlp_rnn.keras",
                                   save_best_only=True)
]
model.fit(vectorized_train_ds.cache(),
         validation_data=vectorized_val_ds.cache(),
         batch_size=BATCH_SIZE,
         epochs=10,
         callbacks=callbacks)
  • את המודל הטוב ביותר שמצא Keras בתהליך האימון אחסנתי בתוך קובץ "nlp_rnn.keras" בעזרת callback.
  • השתמשתי ב-cache של Keras כדי לחסוך את הצורך לטעון מחדש את נתוני המידע המאוחסנים במסדי הנתונים בכל epoch.

 

הערכת התוצאות

נטען את המודל הטוב ביותר אותו מצאנו בתהליך האימון:

model = keras.models.load_model("nlp_rnn.keras")

נעריך את מידת הדיוק של המודל על קבוצת האימון ששמרנו בצד:

loss, acc = model.evaluate(vectorized_test_ds)
18/18 [==============================] - 1s 29ms/step - loss: 0.0582 - accuracy: 0.9855
  • כמעט 99%

היכן שגה המודל?

from sklearn.metrics import confusion_matrix, accuracy_score, classification_report, f1_score

y_pred = model.predict(vectorized_test_ds)
y_pred = np.argmax(y_pred,axis=1)
 
y_actual = []
for bx in test_ds:
   mx = np.argmax(bx[1].numpy(), axis=1)
   y_actual.extend(mx)

print(confusion_matrix(y_actual, y_pred))
[[474   3]
 [  5  68]]
print(classification_report(y_actual, y_pred))
precision    recall  f1-score   support

           0       0.99      0.99      0.99       477
           1       0.96      0.93      0.94        73

    accuracy                           0.99       550
   macro avg       0.97      0.96      0.97       550
weighted avg       0.99      0.99      0.99       550
  • נראה שהמודל שוגה בשני אופנים: מסווג הודעות ספאם כאילו היו לגיטימיות (false negative) ומה שיותר גרוע מסווג הודעות לגיטימיות כאילו היו ספאם (false positive) מה שעלול לגרום להגעת הודעות שמצפים להם לפח האשפה.

נציץ בהודעות שהמודל שגה בסיווג שלהם:

test_ds_texts = []
for batch in test_ds:
   for txt in batch[0]:
       test_ds_texts.append(txt.numpy())
 
print(len(test_ds_texts))
print(test_ds_texts[0])
550
b'Don know..wait i will check it.'
wrong_ids = []
for idx, x in enumerate(y_actual):
   if y_actual[idx] != y_pred[idx]:
       print(y_actual[idx], y_pred[idx], test_ds_texts[idx])
0 1 b'Total video converter free download type this in google search:)'
0 1 b'Cheers for the message Zogtorius. Ixc3xa5xc3x95ve been staring at my phone for an age deciding whether to text or not.'
0 1 b'The house is on the water with a dock, a boat rolled up with a newscaster who dabbles in jazz flute behind the wheel'
1 0 b'For sale - arsenal dartboard. Good condition but no doubles or trebles!'
1 0 b'0A$NETWORKS allow companies to bill for SMS, so they are responsible for their suppliers"'
1 0 b'Missed call alert. These numbers called but left no message. 07008009200'
1 0 b'Cashbin.co.uk (Get lots of cash this weekend!) www.cashbin.co.uk Dear Welcome to the weekend We have got our biggest and best EVER cash give away!! These..'
1 0 b'Email AlertFrom: Jeri StewartSize: 2KBSubject: Low-cost prescripiton drvgsTo listen to email call 123'

המודל סיווג את ההודעה כספאם:

Total video converter free download type this in google search:)

וזו אכן נראית הודעת ספאם למרות שמסד הנתונים מציין אחרת:

df.query('Message.str.contains("Total video converter free download")', engine='python')
Category Message
1505 ham Total video converter free download type this ...
  • נראה שמי שהכין את מסד הנתונים שגה בסיווג ההודעה.

אומנם שיעור הדיוק נראה גבוה כמעט 99% אבל הבעיה העיקרית היא שהמודל עלול לסווג הודעות לגיטימיות כאילו היו ספאם, וכך לשלוח הודעות שמחכים להם אל פח האשפה.

לסיכום, ראינו כיצד להשתמש במודל רצף מבוסס RNN ושכבת Embedding לסיווג טקסטים. במדריך קודם, אף שהשתמשנו במודל bag of words פשוט הגענו לתוצאות טובות יותר. במדריך הנוכחי אימנו את שכבת ה-Embedding מאפס תחת ההנחה שאוצר המילים העומד לרשותנו גדול ומגוון מספיק כדי לאמן את המודל. אימון מאפס עשוי להועיל כאשר מטפלים בתחום הדורש עגה או שפה מיוחדת. דוגמת, אימון NLP לעבוד על מסמכים משפטיים או ספרות ביו-רפואית. במדריך הבא, במקום לאמן את שכבת ה-Embedding בעצמנו נשתמש במודל מאומן מראש pre-trained model.

להורדת הקוד אותו פיתחנו במדריך

 

אולי גם זה יעניין אותך

פיתוח מודל לאנליזת סנטימנט באמצעות למידת מכונה ו-TensorFlow

שימוש ב-word embedding שאומן מראש במודל Keras לסיווג טקסטים

הטרנספורמרים משנים את עולם הבינה המלאכותית

לכל המדריכים בנושא של למידת מכונה

 

אהבתם? לא אהבתם? דרגו!

0 הצבעות, ממוצע 0 מתוך 5 כוכבים

 

 

הוסף תגובה חדשה

 

= 7 + 4