عملکرد بهتر با عملکرد tf

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

در TensorFlow 2، اجرای مشتاق به طور پیش فرض روشن است. رابط کاربری بصری و منعطف است (اجرای عملیات یکباره بسیار آسان‌تر و سریع‌تر است)، اما این می‌تواند به هزینه عملکرد و قابلیت استقرار تمام شود.

می توانید از tf.function برای ایجاد نمودار از برنامه های خود استفاده کنید. این یک ابزار تبدیل است که نمودارهای جریان داده مستقل از پایتون را از کد پایتون شما ایجاد می کند. این به شما کمک می‌کند تا مدل‌های قابل حمل و عملکردی ایجاد کنید، و لازم است از SavedModel استفاده کنید.

این راهنما به شما کمک می کند تا نحوه عملکرد tf.function در زیر کاپوت را تصور کنید، بنابراین می توانید از آن به طور موثر استفاده کنید.

نکات و توصیه های اصلی عبارتند از:

  • در حالت مشتاق اشکال زدایی کنید، سپس با @tf.function تزئین کنید.
  • به عوارض جانبی پایتون مانند جهش شی یا ضمیمه لیست اعتماد نکنید.
  • tf.function بهترین عملکرد را با تنظیمات TensorFlow دارد. فراخوانی های NumPy و Python به ثابت تبدیل می شوند.

برپایی

import tensorflow as tf

برای نشان دادن انواع خطاهایی که ممکن است با آن مواجه شوید، یک تابع کمکی تعریف کنید:

import traceback
import contextlib

# Some helper code to demonstrate the kinds of errors you might encounter.
@contextlib.contextmanager
def assert_raises(error_class):
  try:
    yield
  except error_class as e:
    print('Caught expected exception \n  {}:'.format(error_class))
    traceback.print_exc(limit=2)
  except Exception as e:
    raise e
  else:
    raise Exception('Expected {} to be raised but no error was raised!'.format(
        error_class))

مبانی

استفاده

Function که تعریف می‌کنید (مثلاً با اعمال دکوراتور @tf.function ) درست مانند یک عملیات هسته‌ای TensorFlow است: می‌توانید آن را با اشتیاق اجرا کنید. شما می توانید گرادیان ها را محاسبه کنید. و غیره

@tf.function  # The decorator converts `add` into a `Function`.
def add(a, b):
  return a + b

add(tf.ones([2, 2]), tf.ones([2, 2]))  #  [[2., 2.], [2., 2.]]
<tf.Tensor: shape=(2, 2), dtype=float32, numpy=
array([[2., 2.],
       [2., 2.]], dtype=float32)>
v = tf.Variable(1.0)
with tf.GradientTape() as tape:
  result = add(v, 1.0)
tape.gradient(result, v)
<tf.Tensor: shape=(), dtype=float32, numpy=1.0>

می توانید از Function s در داخل Function های دیگر استفاده کنید.

@tf.function
def dense_layer(x, w, b):
  return add(tf.matmul(x, w), b)

dense_layer(tf.ones([3, 2]), tf.ones([2, 2]), tf.ones([2]))
<tf.Tensor: shape=(3, 2), dtype=float32, numpy=
array([[3., 3.],
       [3., 3.],
       [3., 3.]], dtype=float32)>

Function s می تواند سریعتر از کد مشتاق باشد، به خصوص برای نمودارهایی با تعداد زیادی عملیات کوچک. اما برای نمودارهایی با چند عملیات گران قیمت (مانند کانولوشن)، ممکن است سرعت زیادی مشاهده نکنید.

import timeit
conv_layer = tf.keras.layers.Conv2D(100, 3)

@tf.function
def conv_fn(image):
  return conv_layer(image)

image = tf.zeros([1, 200, 200, 100])
# Warm up
conv_layer(image); conv_fn(image)
print("Eager conv:", timeit.timeit(lambda: conv_layer(image), number=10))
print("Function conv:", timeit.timeit(lambda: conv_fn(image), number=10))
print("Note how there's not much difference in performance for convolutions")
Eager conv: 0.006058974999177735
Function conv: 0.005791576000774512
Note how there's not much difference in performance for convolutions

ردیابی

این بخش نحوه Function در زیر هود را نشان می دهد، از جمله جزئیات پیاده سازی که ممکن است در آینده تغییر کند . با این حال، هنگامی که متوجه شدید که چرا و چه زمانی ردیابی اتفاق می افتد، استفاده موثر از tf.function بسیار آسان تر است!

"ردیابی" چیست؟

یک Function برنامه شما را در یک نمودار TensorFlow اجرا می کند. با این حال، یک tf.Graph نمی تواند همه چیزهایی را که در یک برنامه مشتاق TensorFlow بنویسید، نشان دهد. به عنوان مثال، پایتون از چندشکلی پشتیبانی می کند، اما tf.Graph نیاز دارد که ورودی های آن دارای یک نوع داده و بعد مشخص باشد. یا ممکن است کارهای جانبی مانند خواندن آرگومان های خط فرمان، ایجاد خطا، یا کار با یک شیء پیچیده تر پایتون را انجام دهید. هیچ یک از این چیزها نمی توانند در یک tf.Graph اجرا شوند.

Function این شکاف را با جداسازی کد شما در دو مرحله پر می کند:

1) در مرحله اول که به آن " ردیابی " گفته می شود، Function یک tf.Graph جدید ایجاد می کند. کد پایتون به طور معمول اجرا می شود، اما تمام عملیات TensorFlow (مانند افزودن دو تنسور) به تعویق افتاده است: آنها توسط tf.Graph ضبط می شوند و اجرا نمی شوند.

2) در مرحله دوم یک tf.Graph که شامل هر آنچه در مرحله اول به تعویق افتاده بود اجرا می شود. این مرحله بسیار سریعتر از مرحله ردیابی است.

بسته به ورودی هایش، Function همیشه اولین مرحله را هنگام فراخوانی اجرا نمی کند. "قوانین ردیابی" را در زیر ببینید تا درک بهتری از نحوه تعیین این امر داشته باشید. رد شدن از مرحله اول و تنها اجرای مرحله دوم چیزی است که عملکرد بالای TensorFlow را به شما می دهد.

وقتی Function تصمیم به ردیابی می‌گیرد، مرحله ردیابی بلافاصله با مرحله دوم دنبال می‌شود، بنابراین فراخوانی Function هم tf.Graph را ایجاد و اجرا می‌کند. بعداً خواهید دید که چگونه می توانید فقط مرحله ردیابی را با get_concrete_function کنید.

وقتی آرگومان های انواع مختلف را به یک Function ارسال می کنید، هر دو مرحله اجرا می شوند:

@tf.function
def double(a):
  print("Tracing with", a)
  return a + a

print(double(tf.constant(1)))
print()
print(double(tf.constant(1.1)))
print()
print(double(tf.constant("a")))
print()
Tracing with Tensor("a:0", shape=(), dtype=int32)
tf.Tensor(2, shape=(), dtype=int32)

Tracing with Tensor("a:0", shape=(), dtype=float32)
tf.Tensor(2.2, shape=(), dtype=float32)

Tracing with Tensor("a:0", shape=(), dtype=string)
tf.Tensor(b'aa', shape=(), dtype=string)

توجه داشته باشید که اگر به طور مکرر Function را با همان نوع آرگومان فراخوانی کنید، TensorFlow مرحله ردیابی را رد می کند و از گراف ردیابی شده قبلی مجددا استفاده می کند، زیرا نمودار تولید شده یکسان است.

# This doesn't print 'Tracing with ...'
print(double(tf.constant("b")))
tf.Tensor(b'bb', shape=(), dtype=string)

می‌توانید از pretty_printed_concrete_signatures() برای دیدن همه ردیابی‌های موجود استفاده کنید:

print(double.pretty_printed_concrete_signatures())
double(a)
  Args:
    a: int32 Tensor, shape=()
  Returns:
    int32 Tensor, shape=()

double(a)
  Args:
    a: float32 Tensor, shape=()
  Returns:
    float32 Tensor, shape=()

double(a)
  Args:
    a: string Tensor, shape=()
  Returns:
    string Tensor, shape=()

تاکنون مشاهده کرده‌اید که tf.function یک لایه انتقال پویا و ذخیره‌سازی شده روی منطق ردیابی نمودار TensorFlow ایجاد می‌کند. برای توضیح بیشتر در مورد اصطلاحات:

  • یک tf.Graph یک نمایش خام، زبان شناس و قابل حمل از یک محاسبه TensorFlow است.
  • یک ConcreteFunction یک tf.Graph را می پیچد.
  • یک Function یک حافظه پنهان از ConcreteFunction ها را مدیریت می کند و گزینه مناسب را برای ورودی های شما انتخاب می کند.
  • tf.function یک تابع پایتون را می پیچد و یک شی Function را برمی گرداند.
  • Tracing یک tf.Graph ایجاد می‌کند و آن را در یک ConcreteFunction می‌پیچد که به عنوان ردیابی نیز شناخته می‌شود.

قوانین ردیابی

یک Function تعیین می کند که آیا از یک ConcreteFunction ردیابی شده مجدداً استفاده شود یا خیر. یک کلید حافظه پنهان کلیدی است که یک ConcreteFunction را بر اساس آرگ های ورودی و کوارگ های فراخوانی Function ، طبق قوانین زیر (که ممکن است تغییر کند) شناسایی می کند:

  • کلید تولید شده برای tf.Tensor شکل و نوع d آن است.
  • کلید تولید شده برای tf.Variable یک شناسه متغیر منحصر به فرد است.
  • کلید تولید شده برای یک پایتون اولیه (مانند int ، float ، str ) مقدار آن است.
  • کلید تولید شده برای namedtuple تودرتو، listtuple s، s dict و attr s مجموعه مسطح کلیدهای برگ است (نگاه کنید به nest.flatten ). (در نتیجه این مسطح کردن، فراخوانی یک تابع بتن با ساختار تودرتو متفاوت از آنچه در طول ردیابی استفاده می شود منجر به TypeError می شود).
  • برای همه انواع دیگر پایتون، کلید منحصر به شی است. به این ترتیب یک تابع یا متد برای هر نمونه ای که با آن فراخوانی می شود به طور مستقل ردیابی می شود.

کنترل ردیابی مجدد

Retracing، زمانی است که Function شما بیش از یک ردیابی ایجاد می‌کند، به شما کمک می‌کند تا اطمینان حاصل شود که TensorFlow نمودارهای درستی برای هر مجموعه ورودی ایجاد می‌کند. با این حال، ردیابی یک عملیات گران است! اگر Function شما نمودار جدیدی را برای هر تماس دوباره دنبال کند، متوجه خواهید شد که کد شما کندتر از زمانی که از tf.function استفاده نکرده باشید، اجرا می شود.

برای کنترل رفتار ردیابی، می توانید از تکنیک های زیر استفاده کنید:

  • برای محدود کردن ردیابی، tf.function را در input_signature مشخص کنید.
@tf.function(input_signature=(tf.TensorSpec(shape=[None], dtype=tf.int32),))
def next_collatz(x):
  print("Tracing with", x)
  return tf.where(x % 2 == 0, x // 2, 3 * x + 1)

print(next_collatz(tf.constant([1, 2])))
# You specified a 1-D tensor in the input signature, so this should fail.
with assert_raises(ValueError):
  next_collatz(tf.constant([[1, 2], [3, 4]]))

# You specified an int32 dtype in the input signature, so this should fail.
with assert_raises(ValueError):
  next_collatz(tf.constant([1.0, 2.0]))
Tracing with Tensor("x:0", shape=(None,), dtype=int32)
tf.Tensor([4 1], shape=(2,), dtype=int32)
Caught expected exception 
  <class 'ValueError'>:
Caught expected exception 
  <class 'ValueError'>:
Traceback (most recent call last):
  File "/tmp/ipykernel_26244/3551158538.py", line 8, in assert_raises
    yield
  File "/tmp/ipykernel_26244/1851403433.py", line 9, in <module>
    next_collatz(tf.constant([[1, 2], [3, 4]]))
ValueError: Python inputs incompatible with input_signature:
  inputs: (
    tf.Tensor(
[[1 2]
 [3 4]], shape=(2, 2), dtype=int32))
  input_signature: (
    TensorSpec(shape=(None,), dtype=tf.int32, name=None)).
Traceback (most recent call last):
  File "/tmp/ipykernel_26244/3551158538.py", line 8, in assert_raises
    yield
  File "/tmp/ipykernel_26244/1851403433.py", line 13, in <module>
    next_collatz(tf.constant([1.0, 2.0]))
ValueError: Python inputs incompatible with input_signature:
  inputs: (
    tf.Tensor([1. 2.], shape=(2,), dtype=float32))
  input_signature: (
    TensorSpec(shape=(None,), dtype=tf.int32, name=None)).
  • یک بعد [None] را در tf.TensorSpec کنید تا در استفاده مجدد از ردیابی انعطاف پذیر باشد.

    از آنجایی که TensorFlow تانسورها را بر اساس شکل آنها منطبق می‌کند، استفاده از یک بعد None به عنوان علامت عام به Function s اجازه می‌دهد تا از ردیابی‌ها برای ورودی با اندازه متغیر استفاده کند. اگر دنباله هایی با طول های مختلف یا تصاویری با اندازه های مختلف برای هر دسته داشته باشید، ورودی با اندازه متغیر می تواند رخ دهد (برای مثال به آموزش های ترانسفورماتور و Deep Dream مراجعه کنید).

@tf.function(input_signature=(tf.TensorSpec(shape=[None], dtype=tf.int32),))
def g(x):
  print('Tracing with', x)
  return x

# No retrace!
print(g(tf.constant([1, 2, 3])))
print(g(tf.constant([1, 2, 3, 4, 5])))
Tracing with Tensor("x:0", shape=(None,), dtype=int32)
tf.Tensor([1 2 3], shape=(3,), dtype=int32)
tf.Tensor([1 2 3 4 5], shape=(5,), dtype=int32)
  • برای کاهش ردیابی مجدد، آرگومان های پایتون را به تنسورها ارسال کنید.

    اغلب، آرگومان‌های پایتون برای کنترل هایپرپارامترها و ساختارهای گراف استفاده می‌شوند - برای مثال num_layers=10 یا training=True یا nonlinearity='relu' . بنابراین، اگر آرگومان پایتون تغییر کند، منطقی است که باید نمودار را دوباره دنبال کنید.

    با این حال، ممکن است از آرگومان پایتون برای کنترل ساخت گراف استفاده نشود. در این موارد، تغییر در مقدار پایتون می‌تواند باعث ردیابی مجدد بی‌ضروری شود. به عنوان مثال، این حلقه آموزشی را در نظر بگیرید، که AutoGraph به صورت پویا باز می شود. با وجود ردیابی های متعدد، نمودار تولید شده در واقع یکسان است، بنابراین ردیابی مجدد غیر ضروری است.

def train_one_step():
  pass

@tf.function
def train(num_steps):
  print("Tracing with num_steps = ", num_steps)
  tf.print("Executing with num_steps = ", num_steps)
  for _ in tf.range(num_steps):
    train_one_step()

print("Retracing occurs for different Python arguments.")
train(num_steps=10)
train(num_steps=20)

print()
print("Traces are reused for Tensor arguments.")
train(num_steps=tf.constant(10))
train(num_steps=tf.constant(20))
Retracing occurs for different Python arguments.
Tracing with num_steps =  10
Executing with num_steps =  10
Tracing with num_steps =  20
Executing with num_steps =  20

Traces are reused for Tensor arguments.
Tracing with num_steps =  Tensor("num_steps:0", shape=(), dtype=int32)
Executing with num_steps =  10
Executing with num_steps =  20

اگر نیاز به ردیابی مجدد دارید، یک Function جدید ایجاد کنید. اشیاء Function جداگانه تضمین می شود که ردیابی را به اشتراک نگذارند.

def f():
  print('Tracing!')
  tf.print('Executing')

tf.function(f)()
tf.function(f)()
Tracing!
Executing
Tracing!
Executing

به دست آوردن توابع بتن

هر بار که یک تابع ردیابی می شود، یک تابع بتن جدید ایجاد می شود. با استفاده از get_concrete_function می توانید مستقیماً یک تابع بتن به دست آورید.

print("Obtaining concrete trace")
double_strings = double.get_concrete_function(tf.constant("a"))
print("Executing traced function")
print(double_strings(tf.constant("a")))
print(double_strings(a=tf.constant("b")))
Obtaining concrete trace
Executing traced function
tf.Tensor(b'aa', shape=(), dtype=string)
tf.Tensor(b'bb', shape=(), dtype=string)
# You can also call get_concrete_function on an InputSpec
double_strings_from_inputspec = double.get_concrete_function(tf.TensorSpec(shape=[], dtype=tf.string))
print(double_strings_from_inputspec(tf.constant("c")))
tf.Tensor(b'cc', shape=(), dtype=string)

چاپ یک ConcreteFunction خلاصه ای از آرگومان های ورودی (با انواع) و نوع خروجی آن را نمایش می دهد.

print(double_strings)
ConcreteFunction double(a)
  Args:
    a: string Tensor, shape=()
  Returns:
    string Tensor, shape=()

شما همچنین می توانید به طور مستقیم امضای یک تابع بتن را بازیابی کنید.

print(double_strings.structured_input_signature)
print(double_strings.structured_outputs)
((TensorSpec(shape=(), dtype=tf.string, name='a'),), {})
Tensor("Identity:0", shape=(), dtype=string)

استفاده از ردیابی بتن با انواع ناسازگار باعث ایجاد خطا می شود

with assert_raises(tf.errors.InvalidArgumentError):
  double_strings(tf.constant(1))
Caught expected exception 
  <class 'tensorflow.python.framework.errors_impl.InvalidArgumentError'>:
Traceback (most recent call last):
  File "/tmp/ipykernel_26244/3551158538.py", line 8, in assert_raises
    yield
  File "/tmp/ipykernel_26244/3196284684.py", line 2, in <module>
    double_strings(tf.constant(1))
tensorflow.python.framework.errors_impl.InvalidArgumentError: cannot compute __inference_double_162 as input #0(zero-based) was expected to be a string tensor but is a int32 tensor [Op:__inference_double_162]

ممکن است متوجه شوید که آرگومان‌های پایتون در امضای ورودی یک تابع مشخص رفتار خاصی دارند. قبل از TensorFlow 2.3، آرگومان های پایتون به سادگی از امضای تابع بتن حذف شدند. با شروع TensorFlow 2.3، آرگومان‌های پایتون در امضا باقی می‌مانند، اما برای گرفتن مقدار تنظیم شده در طول ردیابی محدود می‌شوند.

@tf.function
def pow(a, b):
  return a ** b

square = pow.get_concrete_function(a=tf.TensorSpec(None, tf.float32), b=2)
print(square)
ConcreteFunction pow(a, b=2)
  Args:
    a: float32 Tensor, shape=<unknown>
  Returns:
    float32 Tensor, shape=<unknown>
assert square(tf.constant(10.0)) == 100

with assert_raises(TypeError):
  square(tf.constant(10.0), b=3)
Caught expected exception 
  <class 'TypeError'>:
Traceback (most recent call last):
  File "/tmpfs/src/tf_docs_env/lib/python3.7/site-packages/tensorflow/python/eager/function.py", line 1721, in _call_impl
    cancellation_manager)
  File "/tmpfs/src/tf_docs_env/lib/python3.7/site-packages/tensorflow/python/eager/function.py", line 1765, in _call_with_flat_signature
    raise TypeError(f"{self._flat_signature_summary()} got unexpected "
TypeError: pow(a) got unexpected keyword arguments: b.

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/tmp/ipykernel_26244/3551158538.py", line 8, in assert_raises
    yield
  File "/tmp/ipykernel_26244/2310937119.py", line 4, in <module>
    square(tf.constant(10.0), b=3)
TypeError: ConcreteFunction pow(a, b) was constructed with int value 2 in b, but was called with int value 3.

به دست آوردن نمودارها

هر تابع بتن یک لفاف قابل فراخوانی در اطراف یک tf.Graph است. اگرچه بازیابی شی tf.Graph واقعی چیزی نیست که معمولاً باید انجام دهید، می توانید آن را به راحتی از هر تابع مشخصی بدست آورید.

graph = double_strings.graph
for node in graph.as_graph_def().node:
  print(f'{node.input} -> {node.name}')
[] -> a
['a', 'a'] -> add
['add'] -> Identity

اشکال زدایی

به طور کلی، اشکال زدایی کد در حالت مشتاق آسان تر از داخل tf.function است. قبل از تزئین با tf.function باید مطمئن شوید که کد شما بدون خطا در حالت مشتاق اجرا می شود. برای کمک به فرآیند اشکال‌زدایی، می‌توانید با tf.config.run_functions_eagerly(True) تماس بگیرید تا tf.function را به صورت سراسری غیرفعال و دوباره فعال کنید.

هنگام ردیابی مشکلاتی که فقط در tf.function ظاهر می شوند، در اینجا چند نکته وجود دارد:

  • تماس‌های print ساده پایتون فقط در حین ردیابی اجرا می‌شوند و به شما کمک می‌کنند تا زمانی که عملکرد شما (دوباره) ردیابی می‌شود، ردیابی کنید.
  • تماس‌های tf.print هر بار اجرا می‌شوند و می‌توانند به شما در ردیابی مقادیر میانی در طول اجرا کمک کنند.
  • tf.debugging.enable_check_numerics یک راه آسان برای ردیابی محل ایجاد NaNs و Inf است.
  • pdb ( اشکال‌زدای پایتون ) می‌تواند به شما در درک آنچه در طول ردیابی می‌گذرد کمک کند. (اخطار: pdf شما را وارد کد منبع تبدیل شده توسط pdb می کند.)

تبدیلات خودکار

AutoGraph یک کتابخانه است که به طور پیش‌فرض در tf.function است و زیرمجموعه‌ای از کد مشتاق پایتون را به عملیات TensorFlow سازگار با گراف تبدیل می‌کند. این شامل جریان کنترل می شود مانند if , for , while .

عملیات های TensorFlow مانند tf.cond و