טוקניזציה
טוקניזציה (tokenization) הוא התהליך בו מופרד הטקסט לסגמנטים קטנים הנקראים אסימונים או בלועזית טוקנים. אסימונים אלה מסמלים את היחידות האטומיות של הטקסט ולכן קל לחשוב עליהם כמילים אך הם יכולים להיות רק חלקי מילה, אותיות בודדות או סימני פיסוק. תהליך הטוקניזציה הכרחי לשם יצירת מילון והוא מתבצע בשלב עיבוד המידע לפני תהליך הלמידה.
טוקניזציה מבוססת חוקים
טוקניזציה מבוססת חוקים לרוב מתבססת על חוקים דיקדוקים התלויים בשפה שאנחנו עובדים איתה ולכן לספריות שונות מימוש שונה של שיטה זו. ספריה פופולרית בעולם עיבוד השפה שיכולה לשמש עבור טוקניזציה של משפט היא spacy.
האלגוריתם של spacy עבור טוקנזציה (מבוססת חוקים) של משפט בשפה האנגלית עובד באופן הבא:
- הפרד את המשפט בעזרת הרווחים במשפט.
- עבור על כל האסימונים שנצרו ובדוק האם יש לאחד מהם חוק ספציפי.
- עבור על כל האסימונים ונסה להחיל חוק prefix על כל אחד מהם. אם אחד האסימונים שונה בעקבות החלת החוק חזור ל2.
- עבור על כל האסימונים ונסה להחיל חוק suffix על כל אחד מהם. אם אחד האסימונים שונה בעקבות החלת החוק חזור ל2.
- עבור על האסימונים וחפש חוקים מיוחדים (למשל כמו don’t שצריך להתפרק ל [do, n’t]).
- עבור על כל הטוקנים ונסה להחיל חוק infix למשל כמו מקף באמצע המילה. אם אחד הטוקנים שונה בעקבות החלת החוק חזור ל2.
כמו שאנחנו רואים spacy יוצר אסימונים בצורה חמדנית עם כללים שמוגדרים לו מראש. בואו נבחן כמה דוגמאות של טוקניזציה בעזרת spacy.
# download the english version
!python -m spacy download en_core_web_sm
import spacy
from spacy.symbols import ORTH
# load the english version
nlp = spacy.load('en_core_web_sm')
# tokenize the text
doc = nlp("The quick brown fox jumps over the lazy dog.")
print([w.text for w in doc]) # ['The', 'quick', 'brown', 'fox', 'jumps', 'over', 'the', 'lazy', 'dog', '.']
אנחנו רואים ש spacy מחלק את הטקסט כמו שאנחנו מצפים, לפי הרווחים. כעת בואו נבדוק מה קורה כאשר יש לנו טקסט קצת יותר מאתגר.
doc = nlp("I don't want to go to the gym today. why not? cause i am too tired for this and i'll go tomorrow @10:00 anyway. ok i'll come with you tomorrow but i've to go today also cause Jina's parents will be there...")
print([w.text for w in doc]) #['I', 'do', "n't", 'want', 'to', 'go', 'to', 'the', 'gym', 'today', '.', 'why', 'not', '?', 'cause', 'i', 'am', 'too', 'tired', 'for', 'this', 'and', 'i', "'ll", 'go', 'tomorrow', '@10:00', 'anyway', '.', 'ok', 'i', "'ll", 'come', 'with', 'you', 'tomorrow', 'but', 'i', "'ve", 'to', 'go', 'today', 'also', 'cause', 'Jina', "'s", 'parents', 'will', 'be', 'there', '...']
כאן נוכל לראות ש spacy עשה עבודה די טובה עם סימני הפיסוק והקיצורים השונים אך למרות זאת לא פירק בצורה טובה את הטקסט @10:00. נוכל להגדיר חוק ספציפי שיטפל במקרה הזה בעזרת regex
nlp = spacy.load('en_core_web_sm')
suffixes = list(nlp.Defaults.suffixes)
suffixes += [r'(?<=@)[0-9]+:[0-9]+']
suffix_regex = spacy.util.compile_suffix_regex(suffixes)
nlp.tokenizer.suffix_search = suffix_regex.search
doc = nlp("I don't want to go to the gym today. why not? cause i am too tired for this and i'll go tomorrow @10:00 anyway. ok i'll come with you tomorrow but i've to go today also cause Jina's parents will be there...")
print([t.text for t in doc]) #['I', 'do', "n't", 'want', 'to', 'go', 'to', 'the', 'gym', 'today', '.', 'why', 'not', '?', 'cause', 'i', 'am', 'too', 'tired', 'for', 'this', 'and', 'i', "'ll", 'go', 'tomorrow', '@', '10:00', 'anyway', '.', 'ok', 'i', "'ll", 'come', 'with', 'you', 'tomorrow', 'but', 'i', "'ve", 'to', 'go', 'today', 'also', 'cause', 'Jina', "'s", 'parents', 'will', 'be', 'there', '...']
טוקניזציה מבוססת דאטה
בעיבוד שפה, הקלט של מודל הוא לרוב משפט מתוך טקסט והמשפט הזה עובר טוקניזציה כך שהאסימונים שנוצרים הם החלקים האטומים שהמודל עובד איתם. נהוג לחשוב על חלקים אלה כמילים מכיוון שהאינטואיציה שלנו אומרת שבשפה לרוב אין משמעות סמנטית לאותיות בודדות או לחלקי מילים. עם זאת, מסתבר שעבודה עם אותיות בודדות וחלקי מילים עובדות לא רע כאשר אנחנו מדברים על מודלי למידה עמוקה ומצמצמת לנו משמעותית את המילים החדשות שנפגוש בזמן ההסקה. דוגמא אפשרית לחסרון ביצוג של מילים כאסימונים היא אם המודל לומד את היחס בין צמד המילים old ו oldest זה לא מלמד אותו על היחס בין צמד המילים simple ו simplest
אך לעומת זאת אם נבחר ליצג את חלקיק המילה est כאסימון נוכל ללמוד משהו על שני הצמדים ביחד בעזרת האסימון הזה.
השאלה הנשאלת היא איך נקודד את האסימונים שלנו ככה שתהליך הקידוד לא יכלול אלפי חוקים ספציפים ולא יהיה תלוי בשפה? נלמד מהדאטה!
אך לפני כן בואו נתקין את חבילה של huggingface המכילה את הtokenizers
!pip install tokenizers
כמו כן עבור בחינה של האלגוריתמים השונים אנחנו נשתמש בטקסט בעברית שנוריד מווקיפדייה. גוף הטקסט שנעבוד איתו נבנה עלידי צמד חוקרים בעקבות המאמר הזה ונלקח מהגיט האב הזה
נגדיר פונקציות עזר שיעזרו לנו להוריד את הטקסט מהאינטרנט ונוריד את הטקסט מגיטהאב.
class DownloadProgressBar(tqdm):
def update_to(self, b=1, bsize=1, tsize=None):
if tsize is not None:
self.total = tsize
self.update(b * bsize - self.n)
def download_url(url, target_folder, filename):
# check if data exists
print("Check if data exists on disk")
if not os.path.isdir(target_folder):
print("Creating target folder")
os.mkdir(target_folder)
files = os.listdir(target_folder)
if not files:
print("Cannot find files on disk")
print("Downloading files")
with DownloadProgressBar(unit='B', unit_scale=True,
miniters=1, desc=url.split('/')[-1]) as t:
urllib.request.urlretrieve(url, filename=target_folder + filename, reporthook=t.update_to)
print("Download completed!")
download_url("https://raw.githubusercontent.com/NLPH/SVLM-Hebrew-Wikipedia-Corpus/master/SVLM_Hebrew_Wikipedia_Corpus.txt",
"./tokenization/data/",
"wiki-corpus-he.txt")
Byte Pair Encoding
BPE או בשמו הארוך יותר Byte Pair Encoding הוא אלגוריתם ששימש במקור לפתרון של דחיסת נתונים אך ניהיה פופולרי כאלגוריתם טוב ליצור אסימונים של חלקי מילים. האלגוריתם עצמו עובד בצורה הבאה:
- אתחל מילון שמכיל את כל האותיות בטקסט.
- אתחל מיפוי של כל רצפי המילים מחולקות לאותיות והתדירות שלהם.
- מצא את הזוג בעל התדירות הגבוה ביותר וצרף אותו למילון.
- עצור אם הגעת למילון בגודל הרצוי אחרת חזור ל 3.
לדוגמא אם יש לנו את המיפוי ההתחלתי הבא
רצף אותיות | תדירות |
---|---|
<l, o, w> | 5 |
<l, o, w, e, r> | 2 |
<n, e, w, e, s, t> | 6 |
<w, i, d, e, s, t> | 3 |
הנוצר מטקסט המכיל 5 פעמים את המילה low ופעמיים את המילה lower וכו.
האלגוריתם יצור את המילון הבא
| l, o, w, e, r, n, s, t, i, d |
זוג האסימונים במילון בעל התדירות המקסימלית הוא es בעל תדירות של 9 מכיוון שהוא מופיע 6 פעמים בכל המופעים של המילה newest ובמופעים של המילה widest
ולכן המילון שלנו יהפוך להיות
| l, o, w, e, r, n, s, t, i, d, es |
כעת זוג האסימונים במילון בעל התדירות המרבית במיפוי הוא est עם תדירות של 9 גם כן ולכן המילון יהפוך להיות
| l, o, w, e, r, n, s, t, i, d, es, est |
לאחר איטרציה זו הזוג בעל התדירות המרבית שעדין לא התווסף למילון הוא lo עם תדירות של 7 ולכן המילון יהפוך להיות
| l, o, w, e, r, n, s, t, i, d, es, est, lo |
וכך האלגוריתם ימשיך עד שיווצר המילון בגודל הרצוי.
כעת נבחן איך האלגוריתם פועל על משפט בעברית לאחר למידה על הקובץ הטקסט כאשר גודל המילון מוגבל ככה שהאלגוריתם יצטרך להחליט אילו אסימונים כדי לו להכניס למילון ואילו לא.
from tokenizers import CharBPETokenizer
tokenizer = CharBPETokenizer()
tokenizer.train(files="./tokenization/data/wiki-corpus-he.txt", vocab_size=9679)
tokens = tokenizer.encode("לפני כן שוחררו גורי דובים אמיתיים באצטדיון האוניברסיטה")
Output:
[‘לפני</w>’, ‘כן</w>’, ‘שוחררו</w>’, ‘גור’, ‘י</w>’, ‘דו’, ‘בים</w>’, ‘אמי’, ‘תיים</w>’, ‘בא’, ‘צ’, ‘טדיון</w>’, ‘האוניברסיטה</w>’]
ניתן ליראות ש BPE מעדיף להכניס למילון אסימונים בעלי תדירות גבוה גם אם הם לא בהכרח מיצגים מילים חוקיות כמו בים ותיים.
WordPiece
האלגוריתם WordPiece מאוד דומה לאלגוריתם BPE אך במקום שזוג האסימונים יבחר בגלל התדירות באלגוריתם זה ניצור מודל שפה עם המילון הנוכחי ונבחר את הזוג שיוצר לנו את המודל הכי טוב, כלומר זה שמשפר את סבירות של סט האימון.
מודל שפה הוא מודל שמנסה לחזות את המילה הבאה בהינתן הטקסט שראינו עד נקודה מסוימת.
- אתחל מילון שמכיל את כל האותיות בטקסט.
- חלק את המילים בטקסט לאותיות.
- מצא את חלק המילה שמעלה את ההסתברות לטקסט עבור מודל השפה.
- עצור אם הגעת למילון בגודל הרצוי אחרת חזור ל 3.
לדוגמא אם יש לנו את חלקי המילם הבאים:
רצף אותיות |
---|
<l, o, w> |
<l, o, w, e, r> |
<n, e, w, e, s, t> |
<w, i, d, e, s, t> |
ונניח ש est גורם להסתברות של הטקסט שלנו בהנתן מודל השפה שלנו להיות הגבוהה ביותר אז זה חלק המילה שיתווסף למילון.
כעת נבחן איך האלגוריתם הזה עובד על משפט בעברית לאחר תהליך למידה על טקסט
from tokenizers import BertWordPieceTokenizer
tokenizer = BertWordPieceTokenizer()
tokenizer.train(files="./tokenization/data/wiki-corpus-he.txt", vocab_size=9679)
tokens = tokenizer.encode("לפני כן שוחררו גורי דובים אמיתיים באצטדיון האוניברסיטה")
Output:
[‘לפני’, ‘כן’, ‘שוחר’, ‘##רו’, ‘גור’, ‘##י’, ‘דוב’, ‘##ים’, ‘אמיתי’, ‘##ים’, ‘בא’, ‘##צ’, ‘##טדיון’, ‘האוניברסיטה’]
לעומת WordPiece ,BPE מעדיף להכניס אסימונים המיצגיים מילים ולכן הפך את דוביים ל דוב וים. וזה מכיוון שהוא רוצה למקסם מודל שפה ולא תדירות של אותיות.
SentencePiece
בשני האלגוריתמיים הקודמים אנחנו נצטרך לחלק את הטקסט בעמצאות רווח למילים וזה יכול ליצור בעיה כאשר אנחנו נתעסק בשפות שהמילים בהן לא מפרדות ברווח, לדוגמא סינית. לכן באלגוריתם SentencePiece הוגדר אסימון מיוחד לרווח “_”. בתחילת ריצת האלגוריתם המילון מכיל אותיות ואת הטוקן המיוחד המסמל רווח וכך במקום לבחור במילה המשפרת את מודל השפה אנחנו נבחר בצמד האסימונים הבאים שמשפרים את מודל השפה ונוסיף אותם למילון.
כעת נבחן איך האלגוריתם עובד עבור המשפט בעברית לאחר תהליך הלמידה שלו.לאחר
from tokenizers import SentencePieceBPETokenizer
tokenizer = SentencePieceBPETokenizer()
tokenizer.train(files="./tokenization/data/wiki-corpus-he.txt", vocab_size=9679)
tokens = tokenizer.encode("לפני כן שוחררו גורי דובים אמיתיים באצטדיון האוניברסיטה").tokens
Output:
[‘▁לפני’, ‘▁כן’, ‘▁שוחר’, ‘רו’, ‘▁ג’, ‘ורי’, ‘▁דוב’, ‘ים’, ‘▁אמית’, ‘יים’, ‘▁בא’, ‘צ’, ‘טדיון’, ‘▁האוניברסיטה’]ֿ
ניתן לראות ש SentencePiece הוא בין WP ל BPE הוא אומנם מעדיף שהאסימונים יהיו מילים כמו בדוגמא של פירוק דוביים ל דוב ויים, אך בשונה מ WordPiece את המילה אמיתיים הוא פירק לאמית ויים בעוד ש WordPiece פרק אותה לאמיתי וים שתי מילים בעלות משמעות.
סיכום
הרבה מהפעמיים אנחנו יכולים להסתפק בטוקניזריים שפועלים לפי חוקי דיקדוק שהוגדרו לשפה מסויימת. היתרון בשימוש באלה הוא שאין תהליך של למידה ולכן לא צריכים לספק לאלגוריתם טקסט לאימון (מה שמונע מצב שהטוקנייזר יהיה מוטה לסוג מסויים של טקסט). למרות זאת, החסרון בטוקנייזר מבוסס חוקים מתבטא בכך שהוא צריך להיות ספציפי לשפה מסויימת ולרוב לא פועל טוב לשפות שלא מופרדות ברווח או לשפות עם מורפולוגיה מורכבת מדי. פתרון אחר הוא אלגוריתמים שלומדים את תהליך הטוקנזציה בעזרת דאטה לא מתוייג. האלגוריתמים שהוצגו פה די דומים במהות שלהם אך שונים בתהליך הבחירה של הטוקנים הנכנסים למילון. שלושת האלגוריתמים האלה נמצאים כיום בשימוש במודלים הנחשבים SotA כמו GPT2 וBert.