”מכיוון שראית את…”: כך עובד אלגוריתם המלצות הצפייה של נטפליקס

כיצד יודעים שירותי הסטרימינג כמו נטפליקס אילו סדרות וסרטים אנחנו אוהבים? הנה הסבר מקצועי על האלגוריתם בצירוף קטעי קוד רלוונטיים

מקור: Pixabay

מאת: איתי כנרות ועופר הלמן

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

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

ההיסטוריה

בשנת 2007 חברה קטנה שניסתה להתחרות בבלוקבאסטר (כן, כן, ניחשתם נכון, נטפילקס) הכריזה על תחרות לחיזוי דירוג סרטים על סמך מידע שהיא אספה מהמשתמשים שלה (משנת 1998 עד 2005). לזוכה בתחרות הובטח פרס מרשים של מיליון דולר, סכום שהיה כנראה מספיק גבוה כדי להניע קבוצות מחקר ופיתוח לפתח אלגוריתם, שיוכל לחזות את דירוגי הסרטים בצורה המיטבית. הקבוצה שזכתה השתמשה בעיקרון ה-Collaborative filtering (CF), שמקבל את העדפות הצפייה של משתמשי השירות, ויודע להמליץ לכל משתמש מהם התכנים הנוספים שאותו צופה עשוי לאהוב.

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

מה חקרנו?

ישנם שני סוגים עיקריים של מנועי המלצות – מבוססי תוכן (Content Based) וסינון שיתופי (Collaborative Filtering), כאשר רוב מנועי ההמלצות המודרניים עושים שימוש בשני הסוגים ביחד. שיטות מבוססות תוכן לוקחות בחשבון דימיון בין מאפייני התכנים, ושיטות המבוססות סינון שיתופי מחשבות דמיון מתוך האינטראקציות של המשתמשים עם התוכן.

יוני
לא צפה
צפה
צפה
צפה
ליזה
לא צפתה
לא צפתה
צפתה
לא צפתה
תום
צפה
לא צפה
צפה
לא צפה

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

האלגוריתמים

ישנם מספר אלגוריתמים לסינון תוכן. הרעיון הוא למצוא תוכן שמתאים עבור משתמש מסויים, כשהמידע שיש לנו הוא מהם התכנים שבהם אותו משתמש צפה לאחרונה.

דמיון קוסיני הוא האלגוריתם הפשוט ביותר שניתן להשתמש בו. נחפש את n השכנים הדומים ביותר ונמליץ למשתמש תוכן שלא נצפה, בו צפו המשתמשים הדומים

 

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

יש הסבר אינטואיטיבי ל-Matrix Factorization, ולמה אנו צריכים מטריצות מסדר נמוך על מנת לייצג את המשתמשים והתכנים. נניח שמשתמש מסויים צפה או דירג  את הסרטים אווטאר, מטריקס ואקס-מן. אנו רואים שהמשתמש אוהב סרטי מדע בדיוני, וכנראה שקיימים עוד תכנים שהמשתמש עשוי לאהוב. לקטגוריית סרטי המדע הבדיוני יהיה ייצוג חזק באחד מ-k המאפיינים. Matrix Factorization נותן לנו מדד עבור כל משתמש במרחב המאפיינים וכמו כן מדד עבור כל תוכן במרחב המאפיינים.

בחירת k  נמצאת בשליטתינו, ככל שנרצה שאלגורתם “ימציא” יותר מאפייני משתמשים ותכנים כך נגדיל את k. כמובן שנשלם בזמן ריצה ארוך יותר ואם נגדיל מדי אז אולי גם בהתאמת יתר – זהו מצב בו המודל שיצרנו לא באמת למד אלא שינן את הנתונים. מצב של התאמת יתר גורם לכך שכאשר נבקש מהמודל לחזות התאמה של תוכן עבור משתמש שאין כדוגמתו בנתונים מהם הוא “למד”, המודל ייכשל.

פרדיקציה של התאמה (Rating) של תוכן j עבור משתמש i היא המכפלה של שורה i במטריצת מאפייני המשתמש, U, עם עמודה j במטריצת מאפייני התכנים, P.


אז איך מוצאים את מטריצות המאפיינים האופטימליות? כמו שעושים במשימות רבות בלמידת מכונה, מגדירים פונקציית הפסד (Loss Function). ב-Matrix Factorization אנו מעוניינים לשערך את מטריצת מאפייני המשתמשים (U) ואת מטריצת מאפייני התכנים (P) על ידי צימצום פונקציית ההפסד הבאה:

הדירוג האמיתי או האם משתמש צפה (או לא צפה) מיוצג על ידי rij. בתהליך האופטימיזציה אנו מחפשים את P האופטימלי שמיוצג על ידי הוקטור pj ואת U האופטימלי שמיוצג על ידי וקטור ui אשר מצמצם את את סכום השגיאה הריבועית בין הדירוג האמיתי rij והדירוג החזוי. חשוב להוסיף למשוואה רגולריזציה כדי להימנע מהתאמת יתר.

אופטימיזציה

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

דוגמת קוד

בדוגמאות השתמשנו בספריה שנקראת implicit, ספריה שמממשת אלגוריתמי Collaborative filtering בדגש על מידע מסוג דירוג משתמע (בשונה מדירוג מפורש) עם מספר אופציות לאופטימיזציה.

תחילה, הדאטה שלנו נראה כך. בכל שורה יש דגימת זמן, מזהה משתמש ומזהה תוכן

TIMESTAMP,USER_ID,ITEM_ID,PLAYS
2020-11-26 19:24:59.000000,User A,Item A,1
2021-01-02 05:05:46.000000,User A,Item B,1
2021-02-22 08:28:23.000000,User B,Item C,1

בשלב הראשון נייבא ספריות ונטען את הדאטה

import implicit
import pandas as pd
from scipy.sparse import coo_matrix


data = pd.read_csv("data.csv")
data['USER_ID'] = data['USER_ID'].astype("category")
data['ITEM_ID'] = data['ITEM_ID'].astype("category")
data['TIMESTAMP'] = pd.to_datetime(data.TIMESTAMP, format="%Y-%m-%d %H:%M:%S.000000")

אנקדוטה על הדאטה שלנו: שימו לב שכמות האחדות במטריצה היא פחות מחצי אחוז ויותר מ-99.5% הם אפסים.

n_users = data.USER_ID.unique().shape[0]
n_items = data.ITEM_ID.unique().shape[0]


print('Number of samples: {}'.format(data.shape[0]))
print('Number of users: {}'.format(n_users))
print('Number of assets: {}'.format(n_items))
print('Sparsity: {:4.3f}%'.format(float(data.shape[0]) / float(n_users*n_items) * 100))

 

>>> Number of samples: 2168235
>>> Number of users: 214091
>>> Number of assets: 2485
>>> Sparsity: 0.408%

בשלב הבא נאמן את המודל. בדוגמא שלנו בחרנו מודל Matrix Factorization  עם אופטימייזר ALS ובנוסף, החיפוש לוקטורים קרובים נעשה בעזרת Annoy, ספרייה לאחזור וקטורי. אנחנו הרצנו את האופטימייזר 10 איטרציות עם k=50

plays = coo_matrix((data['PLAYS'].astype(float),
                    (data['ITEM_ID'].cat.codes,
                     data['USER_ID'].cat.codes)))


user_items = plays.T.tocsr()


model = implicit.approximate_als.AnnoyAlternatingLeastSquares(factors=50, iterations=10, calculate_training_loss=True)
model.fit(plays)

לבסוף נדפיס את ההמלצות עבור המשתמש הנבחר


def user_id_to_index(usr_id):
    return list(data.USER_ID.cat.categories).index(usr_id)


def print_df(df):
    for index, row in df.iterrows():
        print(f"{row.ITEM_ID} | {row.DURATION/60:4.2f} | {row.TIMESTAMP}")


user_id = 3558640
user_index = user_id_to_index(user_id)


print_df(data[data.USER_ID == user_id])


recommendations = model.recommend(user_index, user_items, N=100)
print(f"{model.__class__.__name__} recommendations for user {user_id}")
for ix, res in enumerate(recommendations):
    rec = data.ITEM_ID.cat.categories[res[0]]
    print(f"{rec} | {ix} | {res[1]:.3f}")

דוגמת המלצה עבור המשתמש הנבחר
התכנים בהם צפה המשתמש

תוכן
דגימת זמן
הבית של יעל
2021-01-10 06:55:23
טרופותי
2020-12-03 12:57:15
יובל המבולבל טוב לצחוק
2021-02-03 06:55:31
כספיון
2020-12-19 05:27:26
מגלים עם דורה
2021-03-01 17:33:32
מעשה בחמישה בלונים
2021-01-15 06:47:31
עץ הכוכבים
2021-02-14 12:30:47

התכנים המומלצים עבור המשתמש הנבחר

תוכן
דירוג
ציון
הזחל הרעב
1
0.668
תירס חם
2
0.444
זוג
3
0.433
עכברשע
4
0.286
טוטו
5
0.263
דיג דיג דוג
6
0.260
סיור בספארי
7
0.231

מה עוד אנחנו רוצים לחקור?

האלגוריתם שהוצג כאן הוא אלגוריתם יחסית פשוט. הוא לא חף מבעיות, כגון cold start, הוא לא עובד כאשר אין דאטה, לדוגמא משתמש חדש או תוכן חדש. האלגוריתמים האופרטיביים שפיתחנו בחברה הם כמובן מורכבים יותר ועושים שימוש במגוון טכניקות שמתאימות יותר לעולם התוכן שלנו ולאתגרים שמאפיינים את התעשייה שלנו.

אתגר מרכזי שאנחנו צריכים לקחת בחשבון סובבת סביב נושא הפרטיות – בשנים האחרונות אנו עדים לשינויים רגולטוריים מרובים סביב פרטיות, והיום המשתמשים יכולים להחליט האם הם רוצים ששירותי הצפייה יעקבו אחריהם או יאספו מידע אישי במסד נתונים מרכזי או לא. כדי לממש את הפתרון שהצענו פה, כמובן שצריך מודל מרכזי שמכיר את כל המשתמשים, ואם המשתמש בחר שלא לשתף את המידע שלו – לא נוכל לספק המלצות.

מגמה נוספת שאנחנו רואים בעולם ה-ML היא ה-federated machine learning. בקצרה, מדובר על דרך שמאפשרת לעשות את רוב החישובים במכשיר הלקוח, ולעדכן את המודל המרכזי במידע אנונימי לגבי משקולות מכל לקוח. תניחו שפעם ביום כל לקוח מוריד את המודל המרכזי למכשיר שלו ומבצע את החישובים האישיים באופן לוקלי.

אז מה היה לנו?

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

 

איתי כנרות הוא סמנכ”ל מחקר ופיתוח ועופר הלמן הוא מפתח דאטה בכיר בחברת קלטורה​

 

כתב אורח

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

הגב

6 תגובות על "”מכיוון שראית את…”: כך עובד אלגוריתם המלצות הצפייה של נטפליקס"

avatar
Photo and Image Files
 
 
 
Audio and Video Files
 
 
 
Other File Types
 
 
 

* היי, אנחנו אוהבים תגובות!
תיקונים, תגובות קוטלות וכמובן תגובות מפרגנות - בכיף.
חופש הביטוי הוא ערך עליון, אבל לא נוכל להשלים עם תגובות שכוללות הסתה, הוצאת דיבה, תגובות שכוללות מידע המפר את תנאי השימוש של Geektime, תגובות שחורגות מהטעם הטוב ותגובות שהן בניגוד לדין. תגובות כאלו יימחקו מייד.

סידור לפי:   חדש | ישן | הכי מדורגים
עומרי
Guest

אחלה תוכן

רגב פורת
Guest

מעניין – תודה.

ג\'רום
Guest

נחמד מאוד.
זה מה שאני מצאתי על זה באתר שלהם
https://help.netflix.com/he/node/100639/il

ספיקו (אקס נטפליקסר)
Guest
ספיקו (אקס נטפליקסר)

שטויות, זה שיטות של לפני 10 שנים שמלמדים אותן במבואות ללימודה חישובית כדי לתת קצת מוטיבציה לסטודנטים שחרמנים על משין לרנינג. בפועל, מדובר באלגוריתמים אחרים לגמרי

KEY
Guest
עד עכשיו לא ברור מה זה משנה כל האלגוריתמים של נטפליקס כשהמאגר שלהם מבוסס על סרטי חסרי חשיבות, משעממים! יש מאות סרטים של שנות ה40-50-60-70-80 שלא נמצאים במאגר והם מעולים. ובמקום זה שמים סרט שועל מדבר של ואן דם(אפילו בסרטי ואן דם, ספורט הדמים פי אלף יותר טוב משועל מדבר). אז מי הסלקטור של הסרטים שמביא סרטים מאעפנים לריג’ן ישראל והורס לחלוטין את חווית הצפיה ואת איכות הסרטים? בעעעע it’s not the size of the boat in the ocean , it’s the motion in the ocean!! הנה עצה בשביל נטפליקס ישראל, קחו את רשימת ההמלצות של IMDB של הסרטים עם… Read more »
מישהו
Guest

זה טוב ויפה רק שכל התוכן שמשודר היום הוא זבל טהור מהול בפרופוגנדה

wpDiscuz

תגיות לכתבה: