TensorFlow 1.x לעומת TensorFlow 2 - התנהגויות וממשקי API

הצג באתר TensorFlow.org הפעל בגוגל קולאב הצג ב-GitHub הורד מחברת

מתחת למכסה המנוע, TensorFlow 2 עוקב אחר פרדיגמת תכנות שונה מהותית מ-TF1.x.

מדריך זה מתאר את ההבדלים הבסיסיים בין TF1.x ו-TF2 מבחינת התנהגויות וממשקי ה-API, וכיצד כל אלה קשורים למסע ההגירה שלך.

סיכום ברמה גבוהה של שינויים גדולים

ביסודו של דבר, TF1.x ו-TF2 משתמשים בקבוצה שונה של התנהגויות זמן ריצה סביב ביצוע (להוט ב-TF2), משתנים, זרימת בקרה, צורות טנסור והשוואות שוויון טנזור. כדי להיות תואם TF2, הקוד שלך חייב להיות תואם לסט המלא של התנהגויות TF2. במהלך ההעברה, אתה יכול להפעיל או להשבית את רוב ההתנהגויות הללו בנפרד באמצעות ממשקי ה-API tf.compat.v1.enable_* או tf.compat.v1.disable_* . החריג היחיד הוא הסרת אוספים, שהיא תופעת לוואי של הפעלת/השבתה של ביצוע להוט.

ברמה גבוהה, TensorFlow 2:

  • מסיר ממשקי API מיותרים .
  • הופך את ממשקי ה-API לעקביים יותר - לדוגמה, Unified RNNs ו- Unified Optimizers .
  • מעדיף פונקציות על פני הפעלות ומשתלב טוב יותר עם זמן הריצה של Python עם הפעלת Eager כברירת מחדל יחד עם tf.function המספק תלות אוטומטית בקרה עבור גרפים והידור.
  • מוציא משימוש אוספי גרפים גלובליים.
  • משנה את סמנטיקה של מקביליות משתנים על-ידי שימוש ב- ResourceVariables על פני ReferenceVariables .
  • תומך בזרימת בקרה מבוססת פונקציות וניתנת להפרדה (Control Flow v2).
  • מפשט את ה-API של TensorShape כדי להחזיק int s במקום אובייקטי tf.compat.v1.Dimension .
  • עדכון מכניקת שוויון טנזור. ב-TF1.x האופרטור == על טנזורים ומשתנים בודק שוויון התייחסות לאובייקט. ב-TF2 הוא בודק שוויון ערכי. בנוסף, טנסורים/משתנים כבר אינם ניתנים לגיבוש, אבל אתה יכול לקבל הפניות לאובייקט הניתנות לגיבוש אליהם באמצעות var.ref() אם אתה צריך להשתמש בהם בסטים או כמפתחות dict .

הסעיפים שלהלן מספקים קצת יותר הקשר על ההבדלים בין TF1.x ו-TF2. למידע נוסף על תהליך העיצוב מאחורי TF2, קרא את ה- RFCs ואת מסמכי העיצוב .

ניקוי API

ממשקי API רבים נעלמו או הועברו ב-TF2. חלק מהשינויים העיקריים כוללים הסרה tf.app , tf.flags ו- tf.logging לטובת הקוד הפתוח absl-py כעת, החזרת פרויקטים שחיו ב- tf.contrib וניקוי מרחב השמות הראשי של tf.* על ידי העברת פונקציות פחות משומשות לתוך חבילות משנה כמו tf.math . כמה ממשקי API הוחלפו במקבילות ה-TF2 שלהם - tf.summary , tf.keras.metrics ו- tf.keras.optimizers .

tf.compat.v1 : נקודות קצה מדור קודם של ממשק API ותאימות

סמלים מתחת למרחבי השמות tf.compat ו- tf.compat.v1 אינם נחשבים לממשקי API של TF2. מרחבי השמות האלה חושפים שילוב של סמלי תאימות, כמו גם נקודות קצה מדור קודם של API מ-TF 1.x. אלה נועדו לסייע במעבר מ-TF1.x ל-TF2. עם זאת, מכיוון שאף אחד מממשקי API אלה של compat.v1 אינו ממשקי API של TF2 אידיומטיים, אל תשתמש בהם לכתיבת קוד TF2 חדש לגמרי.

סמלי tf.compat.v1 בודדים עשויים להיות תואמים ל-TF2 מכיוון שהם ממשיכים לעבוד גם עם התנהגויות TF2 מופעלות (כגון tf.compat.v1.losses.mean_squared_error ), בעוד שאחרים אינם תואמים ל-TF2 (כגון tf.compat.v1.metrics.accuracy ). סמלים רבים compat.v1 (אם כי לא כולם) מכילים מידע הגירה ייעודי בתיעוד שלהם, המסביר את מידת התאימות שלהם להתנהגויות TF2, כמו גם כיצד להעביר אותם לממשקי API של TF2.

סקריפט השדרוג של TF2 יכול למפות סמלי API רבים compat.v1 API מקבילים של TF2 במקרה שבו הם כינויים או בעלי אותם ארגומנטים אך עם סדר שונה. אתה יכול גם להשתמש בסקריפט השדרוג כדי לשנות את השם של ממשקי API של TF1.x באופן אוטומטי.

ממשקי API של חברים כוזבים

יש קבוצה של סמלי "חבר כוזב" שנמצאו במרחב השמות TF2 tf (לא תחת compat.v1 ) שלמעשה מתעלמים מהתנהגויות TF2 מתחת למכסה המנוע, ו/או אינם תואמים לחלוטין לסט המלא של התנהגויות TF2. ככאלה, סביר להניח שממשקי API אלה יתנהגו בצורה לא נכונה עם קוד TF2, אולי בדרכים שקטות.

  • tf.estimator.* : אומדנים יוצרים ומשתמשים בגרפים והפעלות מתחת למכסה המנוע. ככאלה, אין לראות בהם תואמי TF2. אם הקוד שלך מפעיל מעריכים, הוא אינו משתמש בהתנהגויות TF2.
  • keras.Model.model_to_estimator(...) : זה יוצר הערכה מתחת למכסה המנוע, שכאמור לעיל אינו תואם TF2.
  • tf.Graph().as_default() : זה נכנס להתנהגויות של גרף TF1.x ואינו עוקב אחר התנהגויות tf.function תואמות TF2. קוד שנכנס לגרפים כמו זה יריץ אותם בדרך כלל באמצעות Sessions, ואין לראות בו תואם TF2.
  • tf.feature_column.* ממשקי API של עמודת התכונות מסתמכים בדרך כלל על יצירת משתנים בסגנון TF1 tf.compat.v1.get_variable ומניחים שלמשתנים שנוצרו יהיה גישה דרך אוספים גלובליים. מכיוון ש-TF2 אינו תומך באוספים, ייתכן שממשקי API לא יפעלו כראוי בעת הפעלתם כאשר התנהגויות TF2 מופעלות.

שינויים אחרים ב-API

  • TF2 מציג שיפורים משמעותיים באלגוריתמים של מיקום המכשירים אשר הופך את השימוש ב- tf.colocate_with למיותר. אם הסרתו גורמת לירידה בביצועים אנא שלח באג .

  • החלף את כל השימוש ב- tf.v1.ConfigProto בפונקציות מקבילות מ- tf.config .

ביצוע להוט

TF1.x דרש ממך לתפור ידנית עץ תחביר מופשט (הגרף) על ידי ביצוע קריאות tf.* API ולאחר מכן קומפילציה ידנית של עץ התחביר המופשט על ידי העברת קבוצה של טנסורי פלט וטנסורי קלט לקריאה session.run . TF2 פועל בשקיקה (כמו ש-Python עושה בדרך כלל) וגורם לגרפים ולהפעלות להרגיש כמו פרטי יישום.

תוצר לוואי בולט אחד של ביצוע נלהב הוא ש- tf.control_dependencies כבר לא נדרש, מכיוון שכל שורות הקוד מבוצעות לפי הסדר (בתוך tf.function , קוד עם תופעות לוואי מופעל לפי הסדר שנכתב).

אין יותר גלובלים

TF1.x הסתמך במידה רבה על מרחבי שמות גלובליים ואוספים גלובליים. tf.Variable , הוא יוכנס לאוסף בגרף ברירת המחדל, והוא יישאר שם, גם אם איבדת את המעקב אחר המשתנה Python שמצביע עליו. לאחר מכן תוכל לשחזר את tf.Variable הזה, אבל רק אם אתה יודע את השם איתו הוא נוצר. זה היה קשה לעשות אם לא היית בשליטה על יצירת המשתנה. כתוצאה מכך, כל מיני מנגנונים התרבו כדי לנסות לעזור לך למצוא שוב את המשתנים שלך, ולמסגרות למצוא משתנים שנוצרו על ידי המשתמש. חלקם כוללים: היקפים משתנים, אוספים גלובליים, שיטות מסייעות כמו tf.get_global_step ו- tf.global_variables_initializer , אופטימיזרים המחשבים באופן מרומז גרדיאנטים על כל המשתנים הניתנים לאימון, וכן הלאה. TF2 מבטל את כל המנגנונים הללו ( Variables 2.0 RFC ) לטובת מנגנון ברירת המחדל - אתה עוקב אחר המשתנים שלך. אם אתה מאבד מעקב אחר tf.Variable , הוא נאסף אשפה.

הדרישה לעקוב אחר משתנים יוצרת קצת עבודה נוספת, אבל עם כלים כמו דוגמנות shims והתנהגויות כמו אוספי משתנים מונחה עצמים מרומזים ב- tf.Module s ו- tf.keras.layers.Layer , העומס ממוזער.

פונקציות, לא הפעלות

קריאת session.run היא כמעט כמו קריאת פונקציה: אתה מציין את התשומות ואת הפונקציה שיש לקרוא, ואתה מקבל בחזרה סט של פלטים. ב-TF2, אתה יכול לקשט פונקציית Python באמצעות tf.function כדי לסמן אותה עבור קומפילציה של JIT כך ש-TensorFlow יריץ אותה כגרף בודד ( Functions 2.0 RFC ). מנגנון זה מאפשר ל-TF2 לזכות בכל היתרונות של מצב גרף:

  • ביצועים: ניתן לבצע אופטימיזציה של הפונקציה (גיזום צמתים, היתוך ליבה וכו')
  • ניידות: ניתן לייצא/לייבא את הפונקציה מחדש ( SavedModel 2.0 RFC ), מה שמאפשר לך לעשות שימוש חוזר ולשתף פונקציות TensorFlow מודולריות.
# TF1.x
outputs = session.run(f(placeholder), feed_dict={placeholder: input})
# TF2
outputs = f(input)

עם הכוח לשלב בחופשיות Python וקוד TensorFlow, אתה יכול לנצל את כושר הביטוי של Python. עם זאת, TensorFlow הנייד פועל בהקשרים ללא מתורגמן Python, כגון נייד, C++ ו-JavaScript. כדי לעזור להימנע משכתוב הקוד שלך בעת הוספת tf.function , השתמש ב- AutoGraph כדי להמיר תת-קבוצה של מבני Python למקבילות ה-TensorFlow שלהם:

  • for / while -> tf.while_loop ( break continue נתמכים)
  • if -> tf.cond
  • for _ in dataset -> dataset.reduce

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

התאמה לשינויי התנהגות TF 2.x

ההגירה שלך ל-TF2 הושלמה רק לאחר שעברת לסט המלא של התנהגויות TF2. ניתן להפעיל או להשבית את קבוצת ההתנהגויות המלאה באמצעות tf.compat.v1.enable_v2_behaviors ו- tf.compat.v1.disable_v2_behaviors . הסעיפים שלהלן דנים בפירוט בכל שינוי התנהגות מרכזי.

שימוש ב- tf.function s

השינויים הגדולים ביותר בתוכניות שלך במהלך ההגירה עשויים לבוא משינוי פרדיגמת מודל התכנות הבסיסי מגרפים והפעלות לביצוע נלהב ו- tf.function . עיין במדריכי ההעברה של TF2 כדי ללמוד עוד על מעבר ממשקי API שאינם תואמים לביצוע נלהב ו- tf.function לממשקי API התואמים להם.

להלן כמה דפוסי תוכניות נפוצים שאינם קשורים לאף API אחד שעלולים לגרום לבעיות בעת מעבר מ- tf.Graph s ו- tf.compat.v1.Session s לביצוע נלהב עם tf.function s.

דפוס 1: מניפולציה של אובייקט Python ויצירת משתנים שנועדו להיעשות רק פעם אחת, מופעל מספר פעמים

בתוכנות TF1.x שמסתמכות על גרפים והפעלות, הציפייה היא בדרך כלל שכל הלוגיקה של Python בתוכנית שלך תפעל פעם אחת בלבד. עם זאת, עם ביצוע נלהב ו- tf.function , זה הוגן לצפות שהלוגיקה של Python שלך תופעל לפחות פעם אחת, אבל אולי יותר פעמים (או מספר פעמים בהתלהבות, או מספר פעמים על פני tf.function שונים). לפעמים, tf.function אפילו יתחקה פעמיים על אותו קלט, ויגרום להתנהגויות בלתי צפויות (ראה דוגמה 1 ו-2). עיין במדריך tf.function לפרטים נוספים.

דוגמה 1: יצירת משתנים

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

def f():
  v = tf.Variable(1.0)
  return v

with tf.Graph().as_default():
  with tf.compat.v1.Session() as sess:
    res = f()
    sess.run(tf.compat.v1.global_variables_initializer())
    sess.run(res)

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

@tf.function
def f():
  print("trace") # This will print twice because the python body is run twice
  v = tf.Variable(1.0)
  return v

try:
  f()
except ValueError as e:
  print(e)

פתרון עוקף הוא שמירה במטמון ושימוש חוזר במשתנה לאחר יצירתו בקריאה הראשונה.

class Model(tf.Module):
  def __init__(self):
    self.v = None

  @tf.function
  def __call__(self):
    print("trace") # This will print twice because the python body is run twice
    if self.v is None:
      self.v = tf.Variable(0)
    return self.v

m = Model()
m()

דוגמה 2: Tensors מחוץ לטווח עקב tf.function

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

class Model(tf.Module):
  def __init__(self):
    self.dataset = None

  @tf.function
  def __call__(self):
    print("trace") # This will print once: only traced once
    if self.dataset is None:
      self.dataset = tf.data.Dataset.from_tensors([1, 2, 3])
    it = iter(self.dataset)
    return next(it)

m = Model()
m()

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

class Model(tf.Module):
  def __init__(self):
    self.v = None
    self.dataset = None

  @tf.function
  def __call__(self):
    print("trace") # This will print twice because the python body is run twice
    if self.v is None:
      self.v = tf.Variable(0)
    if self.dataset is None:
      self.dataset = tf.data.Dataset.from_tensors([1, 2, 3])
    it = iter(self.dataset)
    return [self.v, next(it)]

m = Model()
try:
  m()
except TypeError as e:
  print(e) # <tf.Tensor ...> is out of scope and cannot be used here.

הפתרון הפשוט ביותר הוא להבטיח שיצירת המשתנים ויצירת מערך הנתונים נמצאים שניהם מחוץ לקריאת tf.funciton . לדוגמה:

class Model(tf.Module):
  def __init__(self):
    self.v = None
    self.dataset = None

  def initialize(self):
    if self.dataset is None:
      self.dataset = tf.data.Dataset.from_tensors([1, 2, 3])
    if self.v is None:
      self.v = tf.Variable(0)

  @tf.function
  def __call__(self):
    it = iter(self.dataset)
    return [self.v, next(it)]

m = Model()
m.initialize()
m()

עם זאת, לפעמים לא ניתן להימנע מיצירת משתנים ב- tf.function (כגון משתני חריץ בחלק מממטבי TF keras ). ובכל זאת, אנחנו יכולים פשוט להעביר את יצירת הנתונים מחוץ לקריאת tf.function . הסיבה שאנו יכולים להסתמך על זה היא כי tf.function יקבל את מערך הנתונים כקלט מרומז ושני הגרפים יכולים לגשת אליו כראוי.

class Model(tf.Module):
  def __init__(self):
    self.v = None
    self.dataset = None

  def initialize(self):
    if self.dataset is None:
      self.dataset = tf.data.Dataset.from_tensors([1, 2, 3])

  @tf.function
  def __call__(self):
    if self.v is None:
      self.v = tf.Variable(0)
    it = iter(self.dataset)
    return [self.v, next(it)]

m = Model()
m.initialize()
m()

דוגמה 3: יצירה מחדש של אובייקט Tensorflow בלתי צפויה עקב שימוש ב-dict

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

class Model(tf.Module):
  def __init__(self):
    self.datasets = {}
    self.iterators = {}

  def __call__(self, key):
    if key not in self.datasets:
      self.datasets[key] = tf.compat.v1.data.Dataset.from_tensor_slices([1, 2, 3])
      self.iterators[key] = self.datasets[key].make_initializable_iterator()
    return self.iterators[key]

with tf.Graph().as_default():
  with tf.compat.v1.Session() as sess:
    m = Model()
    it = m('a')
    sess.run(it.initializer)
    for _ in range(3):
      print(sess.run(it.get_next())) # prints 1, 2, 3

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

class Model(tf.Module):
  def __init__(self):
    self.datasets = {}
    self.iterators = {}

  @tf.function
  def __call__(self, key):
    if key not in self.datasets:
      self.datasets[key] = tf.data.Dataset.from_tensor_slices([1, 2, 3])
      self.iterators[key] = iter(self.datasets[key])
    return self.iterators[key]

m = Model()
for _ in range(3):
  print(next(m('a'))) # prints 1, 1, 1

אנו יכולים להשתמש ב- tf.init_scope כדי להרים את מערך הנתונים ואת יצירת האיטרטור מחוץ לגרף, כדי להשיג את ההתנהגות הצפויה:

class Model(tf.Module):
  def __init__(self):
    self.datasets = {}
    self.iterators = {}

  @tf.function
  def __call__(self, key):
    if key not in self.datasets:
      # Lifts ops out of function-building graphs
      with tf.init_scope():
        self.datasets[key] = tf.data.Dataset.from_tensor_slices([1, 2, 3])
        self.iterators[key] = iter(self.datasets[key])
    return self.iterators[key]

m = Model()
for _ in range(3):
  print(next(m('a'))) # prints 1, 2, 3

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

דוגמה 4: מניפולציה של רשימת Python גלובלית

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

all_losses = []

class Model():
  def __call__(...):
    ...
    all_losses.append(regularization_loss)
    all_losses.append(label_loss_a)
    all_losses.append(label_loss_b)
    ...

g = tf.Graph()
with g.as_default():
  ...
  # initialize all objects
  model = Model()
  optimizer = ...
  ...
  # train step
  model(...)
  total_loss = tf.reduce_sum(all_losses)
  optimizer.minimize(total_loss)
  ...
...
sess = tf.compat.v1.Session(graph=g)
sess.run(...)  

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

all_losses = []

class Model():
  def __call__(...):
    ...
    all_losses.append(regularization_loss)
    all_losses.append(label_loss_a)
    all_losses.append(label_loss_b)
    ...

# initialize all objects
model = Model()
optimizer = ...

def train_step(...)
  ...
  model(...)
  total_loss = tf.reduce_sum(all_losses) # global list is never cleared,
  # Accidentally accumulates sum loss across all training steps
  optimizer.minimize(total_loss)
  ...

תבנית 2: טנזור סמלי שנועד לחשב מחדש כל שלב ב-TF1.x נשמר בטעות עם הערך ההתחלתי בעת המעבר ל-eager.

דפוס זה גורם בדרך כלל לקוד שלך להתנהג בצורה לא נכונה בעת ביצוע נלהב מחוץ ל-tf.functions, אך מעלה InaccessibleTensorError אם מטמון הערך הראשוני מתרחש בתוך tf.function . עם זאת, שים לב שכדי להימנע מתבנית 1 לעיל, לעתים קרובות, בטעות, תבנה את הקוד שלך בצורה כזו שאחסון הערך הראשוני הזה יתרחש מחוץ לכל tf.function . שיכולה להעלות שגיאה. לכן, שים לב במיוחד אם אתה יודע שהתוכנית שלך עשויה להיות רגישה לדפוס זה.

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

דוגמה 1: קצב למידה/פרמטר יתר/וכו'. לוחות זמנים התלויים בצעד הגלובלי

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

g = tf.Graph()
with g.as_default():
  ...
  global_step = tf.Variable(0)
  learning_rate = 1.0 / global_step
  opt = tf.compat.v1.train.GradientDescentOptimizer(learning_rate)
  ...
  global_step.assign_add(1)
...
sess = tf.compat.v1.Session(graph=g)
sess.run(...)

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

global_step = tf.Variable(0)
learning_rate = 1.0 / global_step # Wrong! Only computed once!
opt = tf.keras.optimizers.SGD(learning_rate)

def train_step(...):
  ...
  opt.apply_gradients(...)
  global_step.assign_add(1)
  ...

מכיוון שהדוגמה הספציפית הזו היא דפוס נפוץ ויש לאתחל אתחולי האופטימיזציה רק ​​פעם אחת ולא בכל שלב אימון, כלי האופטימיזציה של TF2 תומכים בלוחות זמנים tf.keras.optimizers.schedules.LearningRateSchedule או קריאות ל-Python כארגומנטים עבור קצב הלמידה והיפרפרמטרים אחרים.

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

שקול את מודול NoiseAdder הבא:

class NoiseAdder(tf.Module):
  def __init__(shape, mean):
    self.noise_distribution = tf.random.normal(shape=shape, mean=mean)
    self.trainable_scale = tf.Variable(1.0, trainable=True)

  def add_noise(input):
    return (self.noise_distribution + input) * self.trainable_scale

השימוש בו באופן הבא ב-TF1.x יחשב טנזור רעש אקראי חדש בכל פעם שההפעלה מופעלת:

g = tf.Graph()
with g.as_default():
  ...
  # initialize all variable-containing objects
  noise_adder = NoiseAdder(shape, mean)
  ...
  # computation pass
  x_with_noise = noise_adder.add_noise(x)
  ...
...
sess = tf.compat.v1.Session(graph=g)
sess.run(...)

עם זאת, ב-TF2 אתחול ה- noise_adder בהתחלה יגרום ל- noise_distribution להיות מחושב פעם אחת בלבד ולהקפיא עבור כל שלבי האימון:

...
# initialize all variable-containing objects
noise_adder = NoiseAdder(shape, mean) # Freezes `self.noise_distribution`!
...
# computation pass
x_with_noise = noise_adder.add_noise(x)
...

כדי לתקן את זה, החלף את NoiseAdder כדי לקרוא ל- tf.random.normal בכל פעם שיש צורך בטנזור אקראי חדש, במקום להתייחס לאותו אובייקט טנסור בכל פעם.

class NoiseAdder(tf.Module):
  def __init__(shape, mean):
    self.noise_distribution = lambda: tf.random.normal(shape=shape, mean=mean)
    self.trainable_scale = tf.Variable(1.0, trainable=True)

  def add_noise(input):
    return (self.noise_distribution() + input) * self.trainable_scale

דפוס 3: קוד TF1.x מסתמך ישירות על טנסורים ומחפש לפי שם

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

שמות טנסור אינם נוצרים בעת ביצוע נלהב מחוץ ל- tf.function כלל, כך שכל השימושים ב- tf.Tensor.name חייבים להתרחש בתוך tf.function . זכור שסביר מאוד שהשמות שנוצרו בפועל יהיו שונים בין TF1.x ו-TF2 אפילו בתוך אותה tf.function , וערבויות API אינן מבטיחות יציבות של השמות שנוצרו על פני גרסאות TF.

דפוס 4: הפעלת TF1.x מריץ באופן סלקטיבי רק חלק מהגרף שנוצר

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

לדוגמה, ייתכן שיהיה לך גם מחולל וגם מבחנה בתוך גרף בודד, ותשתמש בקריאות נפרדות של tf.compat.v1.Session.run כדי לסירוגין בין אימון בלבד של המפלה או רק אימון המחולל.

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

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

הסרת אוספים

כאשר הפעלה נלהבת מופעלת, ממשקי API של compat.v1 הקשורים לאיסוף גרפים (כולל אלה שקוראים או כותבים לאוספים מתחת למכסה המנוע כגון tf.compat.v1.trainable_variables ) אינם זמינים עוד. חלקם עשויים להעלות את ValueError s, בעוד שאחרים עשויים להחזיר רשימות ריקות בשקט.

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

כדי לטפל בכל אחד מהשימושים הסטנדרטיים האלה:

  1. אתחולים - התעלם. אתחול משתנה ידני אינו נדרש כאשר הפעלה להוט מופעלת.
  2. צעד גלובלי - ראה את התיעוד של tf.compat.v1.train.get_or_create_global_step להוראות העברה.
  3. משקולות - מפה את הדגמים שלך ל- tf.Module s/ tf.keras.layers.Layer s/ tf.keras.Model s על ידי ביצוע ההנחיות במדריך מיפוי המודלים ולאחר מכן השתמש במנגנוני מעקב המשקל המתאימים שלהם, כגון tf.module.trainable_variables .
  4. הפסדי רגוליזציה - מפה את המודלים שלך ל- tf.Module s/ tf.keras.layers.Layer s/ tf.keras.Model s על ידי ביצוע ההנחיות במדריך מיפוי המודלים ולאחר מכן השתמש ב- tf.keras.losses . לחלופין, אתה יכול גם לעקוב באופן ידני אחר הפסדי ההסדרה שלך.
  5. הפסדי פלט של מודל - השתמש במנגנוני ניהול אובדן tf.keras.Model או עקוב בנפרד אחר ההפסדים שלך מבלי להשתמש באוספים.
  6. עדכוני משקל - התעלם מהאוסף הזה. ביצוע להוט ו- tf.function (עם תלות חתימה ושליטה אוטומטית) פירושו שכל העדכונים המשתנים יופעלו אוטומטית. אז, לא תצטרך להריץ במפורש את כל עדכוני המשקל בסוף, אבל שים לב שזה אומר שעדכוני המשקל עשויים להתרחש בזמן שונה ממה שהם עשו בקוד TF1.x שלך, בהתאם לאופן שבו השתמשת בתלות בקרה.
  7. סיכומים - עיין במדריך ה-API של סיכום המעבר .

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

ResourceVariables במקום ReferenceVariables

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

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

כדי לבודד את ההשפעה של שינוי התנהגות זה על הקוד שלך, אם ביצוע להוט מושבת, תוכל להשתמש ב- tf.compat.v1.disable_resource_variables() ו- tf.compat.v1.enable_resource_variables() כדי להשבית או לאפשר שינוי התנהגות זה באופן גלובלי. תמיד נעשה שימוש במשתני ResourceVariables אם הפעלה נלהבת מופעלת.

בקרת זרימת v2

ב-TF1.x, פעולות זרימת בקרה כגון tf.cond ו- tf.while_loop inline פעולות ברמה נמוכה כגון Switch , Merge וכו'. TF2 מספק פעולות זרימת בקרה פונקציונליות משופרות המיושמות עם עקבות tf.function נפרדות עבור כל ענף ותמיכה בידול מסדר גבוה יותר.

כדי לבודד את ההשפעה של שינוי התנהגות זה על הקוד שלך, אם ביצוע להוט מושבת, תוכל להשתמש ב- tf.compat.v1.disable_control_flow_v2() ו- tf.compat.v1.enable_control_flow_v2() כדי להשבית או לאפשר שינוי התנהגות זה באופן גלובלי. עם זאת, אתה יכול להשבית את זרימת הבקרה v2 רק אם גם ביצוע להוט מושבת. אם הוא מופעל, זרימת שליטה v2 תמיד תהיה בשימוש.

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

  • קוד המסתמך על שמות אופרטורים וטנזור
  • קוד המתייחס לטנזורים שנוצרו בתוך ענף זרימת בקרה של TensorFlow מחוץ לאותו ענף. זה עשוי לייצר InaccessibleTensorError

שינוי התנהגות זה נועד להיות ניטרלי לביצועים חיובי, אך אם אתה נתקל בבעיה שבה זרימת שליטה v2 מתפקדת גרועה יותר עבורך מאשר זרימת שליטה ב-TF1.x, אנא שלח בעיה עם שלבי השעתוק.

שינויים בהתנהגות של TensorShape API

המחלקה TensorShape הייתה פשוטה כדי להחזיק int s, במקום אובייקטי tf.compat.v1.Dimension . אז אין צורך לקרוא ל-. .value כדי לקבל int .

אובייקטי tf.compat.v1.Dimension בודדים עדיין נגישים מ- tf.TensorShape.dims .

כדי לבודד את ההשפעה של שינוי התנהגות זה על הקוד שלך, אתה יכול להשתמש ב- tf.compat.v1.disable_v2_tensorshape() וב- tf.compat.v1.enable_v2_tensorshape() כדי להשבית או לאפשר שינוי התנהגות זה באופן גלובלי.

להלן מדגים את ההבדלים בין TF1.x ל-TF2.

import tensorflow as tf
# Create a shape and choose an index
i = 0
shape = tf.TensorShape([16, None, 256])
shape
TensorShape([16, None, 256])

אם היה לך את זה ב-TF1.x:

value = shape[i].value

אז עשה זאת ב-TF2:

value = shape[i]
value
16

אם היה לך את זה ב-TF1.x:

for dim in shape:
    value = dim.value
    print(value)

לאחר מכן, עשה זאת ב-TF2:

for value in shape:
  print(value)
16
None
256

אם היה לך את זה ב-TF1.x (או השתמשת בכל שיטת מימד אחרת):

dim = shape[i]
dim.assert_is_compatible_with(other_dim)

אז עשה זאת ב-TF2:

other_dim = 16
Dimension = tf.compat.v1.Dimension

if shape.rank is None:
  dim = Dimension(None)
else:
  dim = shape.dims[i]
dim.is_compatible_with(other_dim) # or any other dimension method
True
shape = tf.TensorShape(None)

if shape:
  dim = shape.dims[i]
  dim.is_compatible_with(other_dim) # or any other dimension method

הערך הבוליאני של tf.TensorShape הוא True אם הדרגה ידועה, אחרת לא False .

print(bool(tf.TensorShape([])))      # Scalar
print(bool(tf.TensorShape([0])))     # 0-length vector
print(bool(tf.TensorShape([1])))     # 1-length vector
print(bool(tf.TensorShape([None])))  # Unknown-length vector
print(bool(tf.TensorShape([1, 10, 100])))       # 3D tensor
print(bool(tf.TensorShape([None, None, None]))) # 3D tensor with no known dimensions
print()
print(bool(tf.TensorShape(None)))  # A tensor with unknown rank.
True
True
True
True
True
True

False

שגיאות פוטנציאליות עקב שינויים ב- TensorShape

לא סביר ששינויי ההתנהגות של TensorShape ישברו את הקוד שלך בשקט. עם זאת, ייתכן שתראה שקוד הקשור לצורה מתחיל להעלות את s AttributeError שכן ל- int s ול- None s אין את אותן תכונות שיש tf.compat.v1.Dimension s. להלן כמה דוגמאות למאפיינים של AttributeError :

try:
  # Create a shape and choose an index
  shape = tf.TensorShape([16, None, 256])
  value = shape[0].value
except AttributeError as e:
  # 'int' object has no attribute 'value'
  print(e)
'int' object has no attribute 'value'
try:
  # Create a shape and choose an index
  shape = tf.TensorShape([16, None, 256])
  dim = shape[1]
  other_dim = shape[2]
  dim.assert_is_compatible_with(other_dim)
except AttributeError as e:
  # 'NoneType' object has no attribute 'assert_is_compatible_with'
  print(e)
'NoneType' object has no attribute 'assert_is_compatible_with'

שוויון טנזור לפי ערך

האופרטורים הבינאריים == ו- != על משתנים וטנסורים שונו להשוואה לפי ערך ב-TF2 במקום להשוואה לפי הפניה לאובייקט כמו ב-TF1.x. בנוסף, טנסורים ומשתנים כבר לא ניתנים לגיבוש ישיר או שמיש אותם בסטים או במפתחות dict, מכיוון שייתכן שלא ניתן יהיה לגיבב אותם לפי ערך. במקום זאת, הם חושפים שיטה .ref() שבה אתה יכול להשתמש כדי לקבל הפניה ניתנת לגיבוש לטנזור או למשתנה.

כדי לבודד את ההשפעה של שינוי התנהגות זה, אתה יכול להשתמש ב- tf.compat.v1.disable_tensor_equality() וב- tf.compat.v1.enable_tensor_equality() כדי להשבית או לאפשר שינוי התנהגות זה באופן גלובלי.

לדוגמה, ב-TF1.x, שני משתנים בעלי אותו ערך יחזירו false כאשר אתה משתמש באופרטור == :

tf.compat.v1.disable_tensor_equality()
x = tf.Variable(0.0)
y = tf.Variable(0.0)

x == y
False

בעוד ב-TF2 עם בדיקת שוויון טנזור מופעלת, x == y יחזיר True .

tf.compat.v1.enable_tensor_equality()
x = tf.Variable(0.0)
y = tf.Variable(0.0)

x == y
<tf.Tensor: shape=(), dtype=bool, numpy=True>

לכן, ב-TF2, אם אתה צריך להשוות לפי הפניה לאובייקט, הקפד להשתמש ב- is and is not

tf.compat.v1.enable_tensor_equality()
x = tf.Variable(0.0)
y = tf.Variable(0.0)

x is y
False

גיבוב טנסורים ומשתנים

עם התנהגויות TF1.x בעבר יכולת להוסיף משתנים וטנסורים ישירות למבני נתונים הדורשים גיבוב, כגון מפתחות set ו- dict .

tf.compat.v1.disable_tensor_equality()
x = tf.Variable(0.0)
set([x, tf.constant(2.0)])
{<tf.Variable 'Variable:0' shape=() dtype=float32, numpy=0.0>,
 <tf.Tensor: shape=(), dtype=float32, numpy=2.0>}

עם זאת, ב-TF2 עם שוויון טנסור מופעל, טנזורים ומשתנים נעשים בלתי ניתנים לגיבוש עקב שינוי הסמנטיקה של האופרטור == ו- != לבדיקת שוויון ערך.

tf.compat.v1.enable_tensor_equality()
x = tf.Variable(0.0)

try:
  set([x, tf.constant(2.0)])
except TypeError as e:
  # TypeError: Variable is unhashable. Instead, use tensor.ref() as the key.
  print(e)
Variable is unhashable. Instead, use tensor.ref() as the key.

לכן, ב-TF2 אם אתה צריך להשתמש באובייקטים טנסוריים או משתנים כמפתחות או תוכן set , אתה יכול להשתמש ב- tensor.ref() כדי לקבל הפניה ניתנת לגיבוש שיכולה לשמש כמפתח:

tf.compat.v1.enable_tensor_equality()
x = tf.Variable(0.0)

tensor_set = set([x.ref(), tf.constant(2.0).ref()])
assert x.ref() in tensor_set

tensor_set
{<Reference wrapping <tf.Variable 'Variable:0' shape=() dtype=float32, numpy=0.0>>,
 <Reference wrapping <tf.Tensor: shape=(), dtype=float32, numpy=2.0>>}

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

referenced_var = x.ref().deref()
assert referenced_var is x
referenced_var
<tf.Variable 'Variable:0' shape=() dtype=float32, numpy=0.0>

משאבים וקריאה נוספת