TensorFlow 1.x در مقابل TensorFlow 2 - رفتارها و API ها

مشاهده در TensorFlow.org در Google Colab اجرا شود در GitHub مشاهده کنید دانلود دفترچه یادداشت

در زیر هود، TensorFlow 2 از الگوی برنامه نویسی اساسا متفاوت از TF1.x پیروی می کند.

این راهنما تفاوت‌های اساسی بین TF1.x و TF2 را از نظر رفتارها و APIها و نحوه ارتباط همه اینها با سفر مهاجرت شما شرح می‌دهد.

خلاصه سطح بالا از تغییرات عمده

اساساً، TF1.x و TF2 از مجموعه متفاوتی از رفتارهای زمان اجرا در مورد اجرا (اشتیاق در TF2)، متغیرها، جریان کنترل، اشکال تانسور و مقایسه برابری تانسور استفاده می‌کنند. برای سازگاری با TF2، کد شما باید با مجموعه کامل رفتارهای TF2 سازگار باشد. در طول مهاجرت، می‌توانید اکثر این رفتارها را به‌صورت جداگانه از طریق tf.compat.v1.enable_* یا tf.compat.v1.disable_* فعال یا غیرفعال کنید. تنها استثنا حذف مجموعه‌ها است که یکی از عوارض جانبی فعال کردن/غیرفعال کردن اجرای مشتاقانه است.

در سطح بالا، TensorFlow 2:

  • API های اضافی را حذف می کند.
  • API ها را سازگارتر می کند - به عنوان مثال، RNN های یکپارچه و بهینه سازهای یکپارچه.
  • توابع را به جلسات ترجیح می دهد و با اجرای Eager که به طور پیش فرض فعال شده است همراه با tf.function که وابستگی های کنترل خودکار برای نمودارها و کامپایل را فراهم می کند، بهتر با زمان اجرا پایتون ادغام می شود.
  • مجموعه‌های نمودار جهانی را منسوخ می‌کند.
  • با استفاده از ResourceVariables بر ReferenceVariables معنای همزمانی متغیر را تغییر می‌دهد.
  • پشتیبانی از جریان کنترل مبتنی بر عملکرد و متمایز (Control Flow v2).
  • TensorShape API را برای نگه داشتن int s به جای اشیاء tf.compat.v1.Dimension می کند.
  • مکانیک برابری تانسور را به روز می کند. در TF1.x عملگر == روی تانسورها و متغیرها برابری مرجع شی را بررسی می کند. در TF2 برابری ارزش را بررسی می کند. علاوه بر این، تانسورها/متغیرها دیگر قابل هش نیستند، اما اگر نیاز به استفاده از آنها در مجموعه ها یا به عنوان کلیدهای dict دارید، می توانید از طریق var.ref() ارجاعات اشیاء قابل هش را به آنها دریافت کنید.

بخش های زیر زمینه بیشتری را در مورد تفاوت های بین TF1.x و TF2 ارائه می دهد. برای کسب اطلاعات بیشتر در مورد فرآیند طراحی پشت TF2، RFC ها و اسناد طراحی را بخوانید.

پاکسازی 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) ناسازگار هستند 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 سازگار با TF2 پیروی نمی کند. کدهایی که چنین نمودارهایی را وارد می کنند، معمولاً آنها را از طریق Sessions اجرا می کنند و نباید با TF2 سازگار در نظر گرفته شوند.
  • tf.feature_column.* APIهای ستون ویژگی عموماً به ایجاد متغیر tf.compat.v1.get_variable به سبک TF1 متکی هستند و فرض می‌کنند که متغیرهای ایجاد شده از طریق مجموعه‌های جهانی قابل دسترسی هستند. از آنجایی که TF2 از مجموعه ها پشتیبانی نمی کند، API ها ممکن است هنگام اجرای آنها با فعال بودن رفتارهای TF2 به درستی کار نکنند.

سایر تغییرات API

  • TF2 دارای پیشرفت های قابل توجهی در الگوریتم های قرار دادن دستگاه است که استفاده از tf.colocate_with غیر ضروری می کند. اگر حذف آن باعث کاهش عملکرد شود، لطفاً یک اشکال را ثبت کنید.

  • همه موارد استفاده tf.v1.ConfigProto با توابع معادل از tf.config کنید.

اعدام مشتاقانه

TF1.x از شما می‌خواهد که به‌صورت دستی یک درخت نحو انتزاعی (گراف) را با ایجاد tf.* فراخوانی‌های API و سپس با ارسال مجموعه‌ای از تانسورهای خروجی و تانسورهای ورودی به یک تماس session.run ، درخت نحو انتزاعی را به صورت دستی کامپایل کنید. TF2 با اشتیاق اجرا می شود (مانند پایتون که معمولاً انجام می دهد) و باعث می شود نمودارها و جلسات شبیه جزئیات پیاده سازی شوند.

یکی از محصولات جانبی قابل توجه اجرای مشتاق این است که tf.control_dependencies دیگر مورد نیاز نیست، زیرا همه خطوط کد به ترتیب اجرا می شوند (در یک tf.function ، کد با عوارض جانبی به ترتیب نوشته شده اجرا می شود).

دیگر جهانی نیست

TF1.x به شدت به فضاهای نام جهانی و مجموعه های ضمنی متکی بود. وقتی tf.Variable را فراخوانی می‌کردید، در یک مجموعه در نمودار پیش‌فرض قرار می‌گرفت و در آنجا باقی می‌ماند، حتی اگر مسیر متغیر پایتون را که به آن اشاره می‌کرد از دست بدهید. سپس می‌توانید آن tf.Variable را بازیابی کنید، اما فقط در صورتی که نامی را که با آن ایجاد شده است بدانید. اگر کنترل ایجاد متغیر را نداشتید انجام این کار دشوار بود. در نتیجه، انواع مکانیسم‌ها برای کمک به شما برای یافتن متغیرهای خود و چارچوب‌ها برای یافتن متغیرهای ایجاد شده توسط کاربر گسترش یافتند. برخی از این موارد عبارتند از: دامنه های متغیر، مجموعه های سراسری، روش های کمکی مانند tf.get_global_step و tf.global_variables_initializer ، بهینه سازهایی که به طور ضمنی گرادیان ها را بر روی همه متغیرهای آموزش پذیر محاسبه می کنند و غیره. TF2 همه این مکانیسم ها ( متغیرهای 2.0 RFC ) را به نفع مکانیزم پیش فرض حذف می کند - شما متغیرهای خود را پیگیری می کنید. اگر مسیر یک tf.Variable را گم کنید، زباله جمع‌آوری می‌شود.

نیاز به ردیابی متغیرها کار اضافی ایجاد می‌کند، اما با ابزارهایی مانند شیم‌های مدل‌سازی و رفتارهایی مانند مجموعه‌های متغیر شی‌گرا ضمنی در tf.Module s و tf.keras.layers.Layer ، بار به حداقل می‌رسد.

توابع، نه جلسات

تماس session.run تقریباً شبیه یک فراخوانی تابع است: ورودی ها و تابعی را که باید فراخوانی شود را مشخص می کنید و مجموعه ای از خروجی ها را برمی گردانید. در TF2، می‌توانید یک تابع پایتون را با استفاده از tf.function تا آن را برای کامپایل JIT علامت‌گذاری کنید تا TensorFlow آن را به‌عنوان یک نمودار واحد اجرا کند ( توابع 2.0 RFC ). این مکانیسم به TF2 اجازه می دهد تا تمام مزایای حالت نمودار را به دست آورد:

  • عملکرد: عملکرد را می توان بهینه کرد (هرس گره، ادغام هسته و غیره)
  • قابلیت حمل: عملکرد را می توان صادر/وارد کرد ( SavedModel 2.0 RFC )، به شما این امکان را می دهد که از توابع TensorFlow مدولار استفاده مجدد و به اشتراک بگذارید.
# TF1.x
outputs = session.run(f(placeholder), feed_dict={placeholder: input})
# TF2
outputs = f(input)

با قدرت تلاقی آزادانه کد پایتون و تنسورفلو، می‌توانید از بیانی بودن پایتون استفاده کنید. با این حال، TensorFlow قابل حمل در زمینه‌هایی بدون مفسر پایتون، مانند موبایل، C++ و جاوا اسکریپت اجرا می‌شود. برای کمک به جلوگیری از بازنویسی کد خود هنگام اضافه کردن 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 می شود. برای اطلاعات بیشتر در مورد انتقال از APIهایی که با اجرای مشتاقانه و tf.function ناسازگار هستند به APIهایی که با آنها سازگار هستند، به راهنمای انتقال TF2 مراجعه کنید.

در زیر برخی از الگوهای برنامه معمولی وجود دارد که به یک API مرتبط نیستند که ممکن است هنگام تغییر از tf.Graph s و tf.compat.v1.Session به اجرای مشتاق با tf.function s مشکل ایجاد کند.

الگوی 1: دستکاری شیء پایتون و ایجاد متغیر تنها یک بار انجام می شود و چندین بار اجرا می شود

در برنامه های TF1.x که به نمودارها و جلسات متکی هستند، معمولاً انتظار این است که تمام منطق پایتون در برنامه شما فقط یک بار اجرا شود. با این حال، با اجرای مشتاقانه و tf.function ، منصفانه است که انتظار داشته باشید که منطق پایتون شما حداقل یک بار، اما احتمالاً چندین بار اجرا شود (چه چندین بار مشتاقانه، یا چندین بار در مسیرهای مختلف 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: تانسورهای خارج از محدوده به دلیل ردیابی tf.function

همانطور که در مثال 1 نشان داده شده است، tf.function زمانی که ایجاد متغیر را در اولین فراخوانی تشخیص دهد، دوباره دنبال می شود. این می تواند باعث سردرگمی اضافی شود، زیرا دو ردیابی دو نمودار ایجاد می کنند. هنگامی که نمودار دوم از ردیابی مجدد تلاش می کند به یک Tensor از نمودار تولید شده در طول اولین ردیابی دسترسی پیدا کند، Tensorflow خطایی ایجاد می کند که شکایت می کند که Tensor خارج از محدوده است. برای نشان دادن سناریو، کد زیر مجموعه داده ای را در اولین 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 غیرمنتظره به دلیل استفاده از دیکته

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 ساده روی کد پایتون، به دقت در مورد کد فکر کنند.

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

قاعده کلی این است که از تکیه بر عوارض جانبی پایتون در منطق خود اجتناب کنید و فقط از آنها برای رفع اشکال ردیابی خود استفاده کنید.

مثال 4: دستکاری لیست جهانی پایتون

کد TF1.x زیر از یک لیست کلی از ضررها استفاده می کند که فقط برای حفظ لیست ضررهای ایجاد شده توسط مرحله آموزشی فعلی استفاده می کند. توجه داشته باشید که منطق پایتون که ضررها را به لیست اضافه می کند، صرف نظر از اینکه جلسه برای چند مرحله آموزشی اجرا شده است، فقط یک بار فراخوانی می شود.

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(...)  

با این حال، اگر این منطق پایتون ساده‌لوحانه با اجرای مشتاقانه به 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 که می‌تواند خطا ایجاد کند، اتفاق بیفتد. بنابراین، اگر می دانید که برنامه شما ممکن است مستعد این الگو باشد، بیشتر مراقب باشید.

راه حل کلی برای این الگو این است که کد را بازسازی کنید یا در صورت لزوم از فراخوانی های پایتون استفاده کنید تا مطمئن شوید که مقدار هر بار به جای اینکه به طور تصادفی در حافظه پنهان ذخیره شود دوباره محاسبه می شود.

مثال 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: مقدار دهی اولیه اعداد تصادفی نمادین که به عنوان ویژگی های شی اختصاص داده شده و سپس از طریق اشاره گر مجددا استفاده می شوند، به طور تصادفی در حافظه پنهان هنگام تغییر به Eager ذخیره می شوند.

ماژول 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 حاوی بخش‌های مختلف برنامه استفاده کنید، یا از یک آرگومان شرطی برای tf.function که بر روی آن منشعب می‌کنید، استفاده کنید تا فقط مواردی را که واقعاً می‌خواهید اجرا کنید، اجرا کنید.

حذف مجموعه ها

هنگامی که اجرای مشتاق فعال است، APIهای compat.v1 مربوط به مجموعه گراف (از جمله آنهایی که در مجموعه‌های زیر سرپوشی مانند tf.compat.v1.trainable_variables ) دیگر در دسترس نیستند. برخی ممکن است ValueError s را افزایش دهند، در حالی که برخی دیگر ممکن است بی سر و صدا لیست های خالی را برگردانند.

استانداردترین استفاده از مجموعه ها در TF1.x برای حفظ مقداردهی اولیه، گام جهانی، وزن ها، تلفات منظم سازی، تلفات خروجی مدل و به روز رسانی های متغیری است که باید اجرا شوند مانند لایه های BatchNormalization .

برای رسیدگی به هر یک از این کاربردهای استاندارد:

  1. Initializers - نادیده گرفته شود. با فعال بودن اجرای مشتاق، مقداردهی اولیه متغیر دستی مورد نیاز نیست.
  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 عملیات های سطح پایین درون خطی مانند 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 شد. بنابراین برای دریافت int نیازی به فراخوانی .value نیست.

اشیاء 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 بعید است که کد شما را در سکوت بشکند. با این حال، ممکن است مشاهده کنید که کدهای مربوط به شکل شروع به بالا بردن AttributeError s به عنوان int s و tf.compat.v1.Dimension None ندارند. در زیر چند نمونه از این 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 تغییر کردند. علاوه بر این، تانسورها و متغیرها دیگر مستقیماً قابل هش کردن یا استفاده در مجموعه‌ها یا کلیدهای دیکت نیستند، زیرا ممکن است هش کردن آنها بر اساس مقدار ممکن نباشد. در عوض، آنها یک .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 و 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>

منابع و مطالعه بیشتر

  • برای مطالعه بیشتر در مورد مهاجرت به TF2 از TF1.x از بخش مهاجرت به TF2 دیدن کنید.
  • راهنمای نگاشت مدل را بخوانید تا نقشه‌برداری مدل‌های TF1.x خود را برای کار مستقیم در TF2 بیشتر بدانید.