You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
ML-For-Beginners/translations/he/8-Reinforcement/2-Gym/README.md

22 KiB

דרישות מקדימות

בשיעור הזה נשתמש בספרייה בשם OpenAI Gym כדי לדמות סביבות שונות. ניתן להריץ את הקוד של השיעור הזה באופן מקומי (למשל, מ-Visual Studio Code), ובמקרה כזה הסימולציה תיפתח בחלון חדש. כאשר מריצים את הקוד אונליין, ייתכן שתצטרכו לבצע כמה התאמות בקוד, כפי שמתואר כאן.

OpenAI Gym

בשיעור הקודם, חוקי המשחק והמצב הוגדרו על ידי מחלקת Board שהגדרנו בעצמנו. כאן נשתמש בסביבת סימולציה מיוחדת, שתדמה את הפיזיקה שמאחורי מוט האיזון. אחת מסביבות הסימולציה הפופולריות ביותר לאימון אלגוריתמים של למידת חיזוק נקראת Gym, שמנוהלת על ידי OpenAI. באמצעות ה-Gym נוכל ליצור סביבות שונות, החל מסימולציית CartPole ועד משחקי Atari.

הערה: ניתן לראות סביבות נוספות הזמינות ב-OpenAI Gym כאן.

ראשית, נתקין את ה-Gym ונייבא את הספריות הנדרשות (קוד בלוק 1):

import sys
!{sys.executable} -m pip install gym 

import gym
import matplotlib.pyplot as plt
import numpy as np
import random

תרגיל - אתחול סביבת CartPole

כדי לעבוד עם בעיית איזון מוט ה-CartPole, עלינו לאתחל את הסביבה המתאימה. כל סביבה מקושרת ל:

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

  • מרחב פעולות שמגדיר פעולות אפשריות. במקרה שלנו מרחב הפעולות הוא דיסקרטי, ומורכב משתי פעולות - שמאלה ו-ימינה. (קוד בלוק 2)

  1. כדי לאתחל, הקלידו את הקוד הבא:

    env = gym.make("CartPole-v1")
    print(env.action_space)
    print(env.observation_space)
    print(env.action_space.sample())
    

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

  1. הריצו את הקוד הבא וראו מה מתקבל.

    זכרו שמומלץ להריץ את הקוד הזה בהתקנה מקומית של Python! (קוד בלוק 3)

    env.reset()
    
    for i in range(100):
       env.render()
       env.step(env.action_space.sample())
    env.close()
    

    אתם אמורים לראות משהו דומה לתמונה הזו:

    CartPole ללא איזון

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

    env.reset()
    
    done = False
    while not done:
       env.render()
       obs, rew, done, info = env.step(env.action_space.sample())
       print(f"{obs} -> {rew}")
    env.close()
    

    בסופו של דבר תראו משהו כזה בתוצאות המחברת:

    [ 0.03403272 -0.24301182  0.02669811  0.2895829 ] -> 1.0
    [ 0.02917248 -0.04828055  0.03248977  0.00543839] -> 1.0
    [ 0.02820687  0.14636075  0.03259854 -0.27681916] -> 1.0
    [ 0.03113408  0.34100283  0.02706215 -0.55904489] -> 1.0
    [ 0.03795414  0.53573468  0.01588125 -0.84308041] -> 1.0
    ...
    [ 0.17299878  0.15868546 -0.20754175 -0.55975453] -> 1.0
    [ 0.17617249  0.35602306 -0.21873684 -0.90998894] -> 1.0
    

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

    • מיקום העגלה
    • מהירות העגלה
    • זווית המוט
    • קצב הסיבוב של המוט
  3. קבלו את הערכים המינימליים והמקסימליים של המספרים הללו: (קוד בלוק 5)

    print(env.observation_space.low)
    print(env.observation_space.high)
    

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

    למעשה, סימולציית CartPole נחשבת לפתרון אם נצליח להגיע לתגמול ממוצע של 195 לאורך 100 ניסיונות רצופים.

דיסקרטיזציה של מצב

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

יש כמה דרכים לעשות זאת:

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

ניתן להשתמש באינטרפולציה ליניארית כדי להביא ערכים לטווח סופי (למשל, מ-20- עד 20), ואז להמיר מספרים לשלמים על ידי עיגול. זה נותן לנו פחות שליטה על גודל המצב, במיוחד אם איננו יודעים את הטווחים המדויקים של ערכי הקלט. לדוגמה, במקרה שלנו 2 מתוך 4 הערכים אינם מוגבלים בטווח העליון/תחתון שלהם, מה שעשוי להוביל למספר אינסופי של מצבים.

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

  1. הנה הפונקציה שתיקח את התצפית מהמודל שלנו ותפיק טופס של 4 ערכים שלמים: (קוד בלוק 6)

    def discretize(x):
        return tuple((x/np.array([0.25, 0.25, 0.01, 0.1])).astype(np.int))
    
  2. בואו נחקור גם שיטת דיסקרטיזציה נוספת באמצעות בינים: (קוד בלוק 7)

    def create_bins(i,num):
        return np.arange(num+1)*(i[1]-i[0])/num+i[0]
    
    print("Sample bins for interval (-5,5) with 10 bins\n",create_bins((-5,5),10))
    
    ints = [(-5,5),(-2,2),(-0.5,0.5),(-2,2)] # intervals of values for each parameter
    nbins = [20,20,10,10] # number of bins for each parameter
    bins = [create_bins(ints[i],nbins[i]) for i in range(4)]
    
    def discretize_bins(x):
        return tuple(np.digitize(x[i],bins[i]) for i in range(4))
    
  3. עכשיו נריץ סימולציה קצרה ונצפה בערכי הסביבה הדיסקרטיים. אתם מוזמנים לנסות גם את discretize וגם את discretize_bins ולראות אם יש הבדל.

    discretize_bins מחזירה את מספר הבין, שהוא מבוסס על 0. לכן עבור ערכי משתנה קלט סביב 0 היא מחזירה את המספר מאמצע הטווח (10). ב-discretize, לא דאגנו לטווח ערכי הפלט, מה שמאפשר להם להיות שליליים, ולכן ערכי המצב אינם מוזזים, ו-0 מתאים ל-0. (קוד בלוק 8)

    env.reset()
    
    done = False
    while not done:
       #env.render()
       obs, rew, done, info = env.step(env.action_space.sample())
       #print(discretize_bins(obs))
       print(discretize(obs))
    env.close()
    

    בטלו את ההערה בשורה שמתחילה ב-env.render אם אתם רוצים לראות איך הסביבה מתבצעת. אחרת, תוכלו להריץ אותה ברקע, מה שמהיר יותר. נשתמש בהרצה "בלתי נראית" זו במהלך תהליך למידת Q.

מבנה טבלת Q

בשיעור הקודם, המצב היה זוג פשוט של מספרים מ-0 עד 8, ולכן היה נוח לייצג את טבלת Q באמצעות טנזור numpy עם צורה של 8x8x2. אם נשתמש בדיסקרטיזציה באמצעות בינים, גודל וקטור המצב שלנו גם ידוע, ולכן נוכל להשתמש באותה גישה ולייצג מצב באמצעות מערך בצורת 20x20x10x10x2 (כאן 2 הוא הממד של מרחב הפעולות, והמידות הראשונות מתאימות למספר הבינים שבחרנו להשתמש עבור כל אחד מהפרמטרים במרחב התצפיות).

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

  1. השתמשו בזוג (state,action) כמפתח המילון, והערך יתאים לערך הכניסה בטבלת Q. (קוד בלוק 9)

    Q = {}
    actions = (0,1)
    
    def qvalues(state):
        return [Q.get((state,a),0) for a in actions]
    

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

בואו נתחיל בלמידת Q

עכשיו אנחנו מוכנים ללמד את פיטר לשמור על איזון!

  1. ראשית, נגדיר כמה היפרפרמטרים: (קוד בלוק 10)

    # hyperparameters
    alpha = 0.3
    gamma = 0.9
    epsilon = 0.90
    

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

    gamma הוא גורם ההנחה שמראה באיזו מידה עלינו להעדיף תגמול עתידי על פני תגמול נוכחי.

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

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

שיפור האלגוריתם

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

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

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

  1. אספו את כל התגמולים המצטברים בכל סימולציה בוקטור rewards לצורך גרפיקה מאוחרת יותר. (קוד בלוק 11)

    def probs(v,eps=1e-4):
        v = v-v.min()+eps
        v = v/v.sum()
        return v
    
    Qmax = 0
    cum_rewards = []
    rewards = []
    for epoch in range(100000):
        obs = env.reset()
        done = False
        cum_reward=0
        # == do the simulation ==
        while not done:
            s = discretize(obs)
            if random.random()<epsilon:
                # exploitation - chose the action according to Q-Table probabilities
                v = probs(np.array(qvalues(s)))
                a = random.choices(actions,weights=v)[0]
            else:
                # exploration - randomly chose the action
                a = np.random.randint(env.action_space.n)
    
            obs, rew, done, info = env.step(a)
            cum_reward+=rew
            ns = discretize(obs)
            Q[(s,a)] = (1 - alpha) * Q.get((s,a),0) + alpha * (rew + gamma * max(qvalues(ns)))
        cum_rewards.append(cum_reward)
        rewards.append(cum_reward)
        # == Periodically print results and calculate average reward ==
        if epoch%5000==0:
            print(f"{epoch}: {np.average(cum_rewards)}, alpha={alpha}, epsilon={epsilon}")
            if np.average(cum_rewards) > Qmax:
                Qmax = np.average(cum_rewards)
                Qbest = Q
            cum_rewards=[]
    

מה שתשימו לב מהתוצאות הללו:

  • קרוב למטרה שלנו. אנו קרובים מאוד להשגת המטרה של קבלת 195 תגמולים מצטברים לאורך 100+ ריצות רצופות של הסימולציה, או שאולי כבר השגנו אותה! גם אם נקבל מספרים קטנים יותר, עדיין איננו יודעים, מכיוון שאנו מחשבים ממוצע לאורך 5000 ריצות, ורק 100 ריצות נדרשות בקריטריון הרשמי.

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

תצפית זו ברורה יותר אם נגרף את התקדמות האימון.

גרפיקה של התקדמות האימון

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

plt.plot(rewards)

התקדמות גולמית

מהגרף הזה, לא ניתן להסיק דבר, מכיוון שבשל טבעו של תהליך האימון הסטוכסטי, אורך הסשנים משתנה מאוד. כדי להבין יותר את הגרף הזה, נוכל לחשב את הממוצע הרץ לאורך סדרת ניסויים, נאמר 100. ניתן לעשות זאת בנוחות באמצעות np.convolve: (קוד בלוק 12)

def running_average(x,window):
    return np.convolve(x,np.ones(window)/window,mode='valid')

plt.plot(running_average(rewards,100))

התקדמות האימון

שינוי היפרפרמטרים

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

  • עבור קצב הלמידה, alpha, נוכל להתחיל עם ערכים קרובים ל-1, ואז להמשיך להקטין את הפרמטר. עם הזמן, נקבל ערכי הסתברות טובים בטבלת Q, ולכן עלינו להתאים אותם בעדינות, ולא להחליף לחלוטין בערכים חדשים.

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

משימה 1: נסו לשחק עם ערכי ההיפרפרמטרים ולבדוק אם אתם מצליחים להשיג תגמול מצטבר גבוה יותר. האם אתם מגיעים מעל 195? משימה 2: כדי לפתור את הבעיה באופן פורמלי, עליכם להגיע ל-195 תגמול ממוצע לאורך 100 ריצות רצופות. מדדו זאת במהלך האימון וודאו שהבעיה נפתרה באופן פורמלי!

לראות את התוצאה בפעולה

יהיה מעניין לראות כיצד המודל המאומן מתנהג בפועל. בואו נריץ את הסימולציה ונשתמש באותה אסטרטגיית בחירת פעולות כמו במהלך האימון, על ידי דגימה בהתאם להתפלגות ההסתברות ב-Q-Table: (בלוק קוד 13)

obs = env.reset()
done = False
while not done:
   s = discretize(obs)
   env.render()
   v = probs(np.array(qvalues(s)))
   a = random.choices(actions,weights=v)[0]
   obs,_,done,_ = env.step(a)
env.close()

אתם אמורים לראות משהו כזה:

a balancing cartpole


🚀אתגר

משימה 3: כאן השתמשנו בעותק הסופי של Q-Table, שייתכן שאינו הטוב ביותר. זכרו ששמרנו את ה-Q-Table עם הביצועים הטובים ביותר במשתנה Qbest! נסו את אותו הדוגמה עם ה-Q-Table הטוב ביותר על ידי העתקת Qbest ל-Q ובדקו אם אתם מבחינים בהבדל.

משימה 4: כאן לא בחרנו את הפעולה הטובה ביותר בכל שלב, אלא דגמנו לפי התפלגות ההסתברות המתאימה. האם יהיה הגיוני יותר תמיד לבחור את הפעולה הטובה ביותר, עם הערך הגבוה ביותר ב-Q-Table? ניתן לעשות זאת באמצעות פונקציית np.argmax כדי למצוא את מספר הפעולה המתאים לערך הגבוה ביותר ב-Q-Table. יישמו את האסטרטגיה הזו ובדקו אם היא משפרת את האיזון.

שאלון לאחר ההרצאה

משימה

אמן מכונית הרים

סיכום

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

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


כתב ויתור:
מסמך זה תורגם באמצעות שירות תרגום מבוסס בינה מלאכותית Co-op Translator. למרות שאנו שואפים לדיוק, יש להיות מודעים לכך שתרגומים אוטומטיים עשויים להכיל שגיאות או אי דיוקים. המסמך המקורי בשפתו המקורית צריך להיחשב כמקור סמכותי. עבור מידע קריטי, מומלץ להשתמש בתרגום מקצועי על ידי אדם. איננו נושאים באחריות לאי הבנות או לפרשנויות שגויות הנובעות משימוש בתרגום זה.