![]() | ![]() | ![]() | ![]() |
W TensorFlow 2 przyspieszone wykonywanie jest domyślnie włączone. Interfejs użytkownika jest intuicyjny i elastyczny (wykonywanie jednorazowych operacji jest znacznie łatwiejsze i szybsze), ale może to odbywać się kosztem wydajności i możliwości wdrażania.
Możesz użyć tf.function
do tworzenia wykresów ze swoich programów. Jest to narzędzie do transformacji, które tworzy niezależne od Pythona wykresy przepływu danych z kodu Pythona. Pomoże to w tworzeniu wydajnych i przenośnych modeli i jest wymagane do korzystania z SavedModel
.
Ten przewodnik pomoże Ci tf.function
jak tf.function
działa pod maską, abyś mógł z niej efektywnie korzystać.
Główne wnioski i zalecenia to:
- Debuguj w trybie
@tf.function
, a następnie udekoruj za pomocą@tf.function
. - Nie polegaj na efektach ubocznych Pythona, takich jak mutacja obiektu lub dołączanie listy.
-
tf.function
działa najlepiej z operacjami TensorFlow; Wywołania NumPy i Python są konwertowane na stałe.
Ustawiać
import tensorflow as tf
Zdefiniuj funkcję pomocniczą, aby zademonstrować rodzaje błędów, które możesz napotkać:
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))
Podstawy
Stosowanie
Zdefiniowana Function
(na przykład przez zastosowanie dekoratora @tf.function
) jest jak podstawowa operacja TensorFlow: można ją wykonywać z niecierpliwością; możesz obliczyć gradienty; i tak dalej.
@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>
Można użyć Function
s wewnątrz innych Function
y.
@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
mogą być szybsze niż chętny kod, szczególnie w przypadku wykresów z wieloma małymi operacjami. Ale w przypadku wykresów z kilkoma drogimi operacjami (takimi jak zwoje), możesz nie zauważyć dużego przyspieszenia.
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.003833446999806256 Function conv: 0.004606625999940661 Note how there's not much difference in performance for convolutions
Rysunek kalkowy
Ta sekcja pokazuje, jak Function
działa pod maską, w tym szczegóły implementacji, które mogą ulec zmianie w przyszłości . Jednak gdy już zrozumiesz, dlaczego i kiedy odbywa się śledzenie, znacznie łatwiej tf.function
efektywnie korzystać z tf.function
!
Co to jest „śledzenie”?
Function
uruchamia program na wykresie TensorFlow . Jednak tf.Graph
nie może reprezentować wszystkich rzeczy, które można napisać w gorącym programie TensorFlow. Na przykład Python obsługuje polimorfizm, ale tf.Graph
wymaga, aby jego dane wejściowe miały określony typ i wymiar danych. Możesz też wykonać zadania poboczne, takie jak odczytywanie argumentów wiersza poleceń, zgłaszanie błędu lub praca z bardziej złożonym obiektem Pythona; żadna z tych rzeczy nie może działać w tf.Graph
.
Function
wypełnia tę lukę, rozdzielając kod na dwa etapy:
1) W pierwszym etapie, zwanym „ śledzeniem ”, Function
tworzy nowy tf.Graph
. Kod Pythona działa normalnie, ale wszystkie operacje TensorFlow (takie jak dodanie dwóch Tensorów) są odroczone : są przechwytywane przez tf.Graph
i nie są uruchamiane.
2) W drugim etapie tf.Graph
jest tf.Graph
zawierający wszystko, co zostało odroczone w pierwszym etapie. Ten etap jest znacznie szybszy niż etap śledzenia.
W zależności od swoich danych wejściowych Function
nie zawsze będzie uruchamiać pierwszy etap, gdy zostanie wywołana. Zobacz „Zasady śledzenia” poniżej, aby lepiej zrozumieć, w jaki sposób decyduje o tym. Pominięcie pierwszego etapu i wykonanie tylko drugiego etapu zapewnia wysoką wydajność TensorFlow.
Gdy Function
zdecyduje się na śledzenie, po etapie śledzenia następuje natychmiast drugi etap, więc wywołanie Function
jednocześnie tworzy i uruchamia tf.Graph
. Później zobaczysz, jak możesz uruchomić tylko etap śledzenia za pomocą funkcji get_concrete_function
.
Kiedy przekazujemy argumenty różnych typów do Function
, uruchamiane są oba etapy:
@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)
Zwróć uwagę, że jeśli wielokrotnie wywołujesz Function
z tym samym typem argumentu, TensorFlow pominie etap śledzenia i ponownie użyje wcześniej prześledzonego wykresu, ponieważ wygenerowany wykres byłby identyczny.
# This doesn't print 'Tracing with ...'
print(double(tf.constant("b")))
tf.Tensor(b'bb', shape=(), dtype=string)
Możesz użyć pretty_printed_concrete_signatures()
aby zobaczyć wszystkie dostępne ślady:
print(double.pretty_printed_concrete_signatures())
double(a) Args: a: float32 Tensor, shape=() Returns: float32 Tensor, shape=() double(a) Args: a: string Tensor, shape=() Returns: string Tensor, shape=() double(a) Args: a: int32 Tensor, shape=() Returns: int32 Tensor, shape=()
Do tej pory widzieliście, że tf.function
tworzy buforowaną, dynamiczną warstwę wysyłania na podstawie logiki śledzenia wykresów TensorFlow. Aby być bardziej szczegółowym na temat terminologii:
-
tf.Graph
jest surową,tf.Graph
od języka, przenośną reprezentacją obliczeń TensorFlow. -
ConcreteFunction
otacza pliktf.Graph
. -
Function
zarządza pamięcią podręcznąConcreteFunction
si wybiera właściwą dla danych wejściowych. -
tf.function
funkcję Pythona, zwracając obiektFunction
. - Tracing tworzy
tf.Graph
i zawija go wConcreteFunction
, znanej również jako ślad.
Zasady śledzenia
Function
określa, czy ponownie użyć śledzonej funkcji ConcreteFunction
, obliczając klucz pamięci podręcznej z argumentów wejściowych i kwargs. Klucz pamięci podręcznej to klucz, który identyfikuje ConcreteFunction
na podstawie argumentów wejściowych i kwargs wywołania Function
, zgodnie z następującymi regułami (które mogą ulec zmianie):
- Klucz wygenerowany dla argumentu
tf.Tensor
to jego kształt i typ. - Począwszy od TensorFlow 2.3, kluczem wygenerowanym dla argumentu
tf.Variable
jest jegoid()
. - Kluczem wygenerowanym dla prymitywu Pythona jest jego wartość. Klucz wygenerowany dla zagnieżdżonych
dict
s,list
s,tuple
s,namedtuple
s iattr
s to spłaszczona krotka. (W wyniku tego spłaszczenia wywołanie konkretnej funkcji z inną strukturą zagnieżdżenia niż ta używana podczas śledzenia spowoduje błąd TypeError). - W przypadku wszystkich innych typów Pythona klucze są oparte na obiekcie
id()
dzięki czemu metody są śledzone niezależnie dla każdej instancji klasy.
Kontrolowanie odtwarzania
Retracing, czyli sytuacja, w której Function
tworzy więcej niż jeden ślad, pomaga zapewnić, że TensorFlow generuje prawidłowe wykresy dla każdego zestawu danych wejściowych. Jednak śledzenie jest kosztowną operacją! Jeśli twoja Function
odtworzy nowy wykres dla każdego wywołania, przekonasz się, że twój kod wykonuje się wolniej niż gdybyś nie używał tf.function
.
Aby kontrolować zachowanie śledzenia, można użyć następujących technik:
- Określ
input_signature
wtf.function
aby ograniczyć śledzenie.
@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])))
# We 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]]))
# We 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 "<ipython-input-1-73d0ca52e838>", line 8, in assert_raises yield File "<ipython-input-1-20f544b8adbf>", 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 "<ipython-input-1-73d0ca52e838>", line 8, in assert_raises yield File "<ipython-input-1-20f544b8adbf>", 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))
Określ wymiar [Brak] w
tf.TensorSpec
aby zapewnić elastyczność ponownego wykorzystania śledzenia.Ponieważ TensorFlow dopasowuje tensory na podstawie ich kształtu, użycie wymiaru
None
jako symbolu wieloznacznego umożliwiFunction
s ponowne wykorzystanie śladów dla danych wejściowych o zmiennej wielkości. Dane wejściowe o zmiennej wielkości mogą wystąpić, jeśli masz sekwencje o różnej długości lub obrazy o różnych rozmiarach dla każdej partii (zobacz na przykład samouczki Transformer i 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)
Rzutuj argumenty Pythona na Tensory, aby zredukować odtwarzanie.
Często argumenty Pythona są używane do kontrolowania hiperparametrów i konstrukcji wykresów - na przykład
num_layers=10
lubtraining=True
lubnonlinearity='relu'
. Więc jeśli argument Pythona się zmieni, ma sens, że musiałbyś odtworzyć wykres.Jednak możliwe jest, że argument Pythona nie jest używany do sterowania konstrukcją wykresu. W takich przypadkach zmiana wartości Pythona może wywołać niepotrzebne odtworzenie. Weźmy na przykład tę pętlę szkoleniową, którą AutoGraph będzie dynamicznie rozwijać. Pomimo wielu śladów, wygenerowany wykres jest w rzeczywistości identyczny, więc odtworzenie nie jest konieczne.
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
Jeśli chcesz wymusić odtworzenie, utwórz nową Function
. Gwarantuje się, że oddzielne obiekty Function
nie będą współużytkować śladów.
def f():
print('Tracing!')
tf.print('Executing')
tf.function(f)()
tf.function(f)()
Tracing! Executing Tracing! Executing
Uzyskanie konkretnych funkcji
Za każdym razem, gdy śledzona jest funkcja, tworzona jest nowa konkretna funkcja. Możesz bezpośrednio uzyskać konkretną funkcję, używając 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")))
Tracing with Tensor("a:0", shape=(), dtype=string) tf.Tensor(b'cc', shape=(), dtype=string)
Drukowanie ConcreteFunction
wyświetla podsumowanie jej argumentów wejściowych (z typami) i typ wyjściowy.
print(double_strings)
ConcreteFunction double(a) Args: a: string Tensor, shape=() Returns: string Tensor, shape=()
Możesz także bezpośrednio pobrać podpis konkretnej funkcji.
print(double_strings.structured_input_signature)
print(double_strings.structured_outputs)
((TensorSpec(shape=(), dtype=tf.string, name='a'),), {}) Tensor("Identity:0", shape=(), dtype=string)
Użycie konkretnego śledzenia z niekompatybilnymi typami spowoduje zgłoszenie błędu
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 "<ipython-input-1-73d0ca52e838>", line 8, in assert_raises yield File "<ipython-input-1-e4e2860a4364>", 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]
Możesz zauważyć, że argumenty Pythona są traktowane w specjalny sposób w sygnaturze wejściowej konkretnej funkcji. Przed TensorFlow 2.3 argumenty Pythona były po prostu usuwane z sygnatury konkretnej funkcji. Począwszy od TensorFlow 2.3, argumenty Pythona pozostają w sygnaturze, ale są ograniczone do przyjęcia wartości ustawionej podczas śledzenia.
@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.6/site-packages/tensorflow/python/eager/function.py", line 1683, in _call_impl cancellation_manager) File "/tmpfs/src/tf_docs_env/lib/python3.6/site-packages/tensorflow/python/eager/function.py", line 1728, in _call_with_flat_signature self._flat_signature_summary(), ", ".join(sorted(kwargs)))) TypeError: pow(a) got unexpected keyword arguments: b. During handling of the above exception, another exception occurred: Traceback (most recent call last): File "<ipython-input-1-73d0ca52e838>", line 8, in assert_raises yield File "<ipython-input-1-d163f3d206cb>", 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
Uzyskanie wykresów
Każda konkretna funkcja jest wywoływalnym opakowaniem wokół tf.Graph
. Chociaż pobieranie rzeczywistego obiektu tf.Graph
nie jest czymś, co normalnie musisz robić, możesz go łatwo uzyskać z dowolnej konkretnej funkcji.
graph = double_strings.graph
for node in graph.as_graph_def().node:
print(f'{node.input} -> {node.name}')
[] -> a ['a', 'a'] -> add ['add'] -> Identity
Debugowanie
Ogólnie rzecz biorąc, debugowanie kodu jest łatwiejsze w tf.function
trybie niż wewnątrz tf.function
. Powinieneś upewnić się, że twój kod wykonuje się bezbłędnie w trybie tf.function
przed dekorowaniem za pomocą tf.function
. Aby pomóc w procesie debugowania, możesz wywołać tf.config.run_functions_eagerly(True)
aby globalnie wyłączyć i ponownie tf.function
.
Podczas wyszukiwania problemów, które pojawiają się tylko w tf.function
, oto kilka wskazówek:
- Zwykłe, stare wywołania
print
Pythonie są wykonywane tylko podczas śledzenia, pomagając w wyśledzeniu, kiedy funkcja zostaje (ponownie) śledzona. -
tf.print
będą wykonywane za każdym razem i mogą pomóc w śledzeniu wartości pośrednich podczas wykonywania. -
tf.debugging.enable_check_numerics
to łatwy sposób na śledzenie, gdzie są tworzone NaN i Inf. -
pdb
może pomóc ci zrozumieć, co się dzieje podczas śledzenia. (Uwaga: PDB przeniesie Cię do kodu źródłowego przekształconego przez AutoGraph).
Transformacje AutoGraph
AutoGraph to biblioteka, która jest domyślnie włączona w tf.function
i przekształca podzbiór chętnego kodu Pythona na zgodne z grafami operacje TensorFlow. Obejmuje to przepływ sterowania, taki jak if
, for
, while
.
Operacje TensorFlow, takie jak tf.cond
i tf.while_loop
nadal działają, ale przepływ sterowania jest często łatwiejszy do napisania i zrozumienia, gdy jest napisany w języku Python.
# Simple loop
@tf.function
def f(x):
while tf.reduce_sum(x) > 1:
tf.print(x)
x = tf.tanh(x)
return x
f(tf.random.uniform([5]))
[0.418865323 0.911027312 0.382812262 0.722406387 0.160078526] [0.39597407 0.721625 0.365147263 0.618397713 0.158725053] [0.376498967 0.617914855 0.34973979 0.550011516 0.157405376] [0.359663159 0.549674571 0.336144745 0.500528812 0.156118125] [0.344917297 0.500276268 0.324031234 0.462533 0.154862] [0.331860244 0.462334394 0.313147396 0.43214643 0.15363577] [0.320191294 0.431984901 0.303297669 0.407113552 0.152438238] [0.309679836 0.406978786 0.294327527 0.386019081 0.151268333] [0.300145775 0.385904342 0.286112964 0.36792323 0.150125] [0.29144603 0.367824018 0.278553277 0.352173865 0.149007246] [0.283465207 0.352086931 0.271565557 0.338302 0.147914127] [0.276109159 0.338225 0.265080959 0.325960636 0.146844745] [0.269300193 0.325891823 0.259041727 0.31488657 0.145798281] [0.262973547 0.314824551 0.253399 0.304876 0.144773886] [0.257074684 0.304819793 0.248111084 0.29576847 0.143770814] [0.25155738 0.29571715 0.243142202 0.287435412 0.142788336] [0.246382073 0.287388295 0.238461465 0.279772669 0.14182575] [0.241514727 0.279729217 0.234042 0.272694677 0.140882403] [0.23692593 0.272654444 0.229860321 0.266130418 0.139957651] [0.232590035 0.266093045 0.225895762 0.260020494 0.139050901] [0.228484616 0.259985656 0.222130179 0.254314691 0.13816157] [0.224589869 0.254282087 0.218547404 0.248970196 0.137289107] [0.220888361 0.248939633 0.215133116 0.243950352 0.13643299] [0.217364609 0.243921608 0.211874455 0.23922351 0.135592714] [0.214004785 0.239196405 0.208759964 0.234762147 0.134767786] [0.210796505 0.234736577 0.20577924 0.230542287 0.133957744] [0.207728744 0.230518073 0.202923 0.226542845 0.133162171] <tf.Tensor: shape=(5,), dtype=float32, numpy= array([0.20479149, 0.22651988, 0.20018281, 0.22274524, 0.13238062], dtype=float32)>
Jeśli jesteś ciekawy, możesz sprawdzić kod generowany przez autograf.
print(tf.autograph.to_code(f.python_function))
def tf__f(x): with ag__.FunctionScope('f', 'fscope', ag__.ConversionOptions(recursive=True, user_requested=True, optional_features=(), internal_convert_user_code=True)) as fscope: do_return = False retval_ = ag__.UndefinedReturnValue() def get_state(): return (x,) def set_state(vars_): nonlocal x (x,) = vars_ def loop_body(): nonlocal x ag__.converted_call(ag__.ld(tf).print, (ag__.ld(x),), None, fscope) x = ag__.converted_call(ag__.ld(tf).tanh, (ag__.ld(x),), None, fscope) def loop_test(): return (ag__.converted_call(ag__.ld(tf).reduce_sum, (ag__.ld(x),), None, fscope) > 1) ag__.while_stmt(loop_test, loop_body, get_state, set_state, ('x',), {}) try: do_return = True retval_ = ag__.ld(x) except: do_return = False raise return fscope.ret(retval_, do_return)
Warunki
AutoGraph skonwertuje niektóre instrukcje if <condition>
na odpowiadające tf.cond
wywołania tf.cond
. To podstawienie jest wykonywane, jeśli <condition>
jest tensorem. W przeciwnym if
instrukcja if
jest wykonywana jako warunkowa w Pythonie.
Warunek Pythona jest wykonywany podczas śledzenia, więc dokładnie jedna gałąź warunku zostanie dodana do wykresu. Bez AutoGraph ten śledzony wykres nie byłby w stanie przyjąć alternatywnej gałęzi, jeśli istnieje przepływ sterowania zależny od danych.
tf.cond
śledzi i dodaje obie gałęzie warunku do wykresu, dynamicznie wybierając gałąź w czasie wykonywania. Śledzenie może mieć niezamierzone skutki uboczne; więcej informacji można znaleźć w sekcji Efekty śledzenia AutoGraph .
@tf.function
def fizzbuzz(n):
for i in tf.range(1, n + 1):
print('Tracing for loop')
if i % 15 == 0:
print('Tracing fizzbuzz branch')
tf.print('fizzbuzz')
elif i % 3 == 0:
print('Tracing fizz branch')
tf.print('fizz')
elif i % 5 == 0:
print('Tracing buzz branch')
tf.print('buzz')
else:
print('Tracing default branch')
tf.print(i)
fizzbuzz(tf.constant(5))
fizzbuzz(tf.constant(20))
Tracing for loop Tracing fizzbuzz branch Tracing fizz branch Tracing buzz branch Tracing default branch 1 2 fizz 4 buzz 1 2 fizz 4 buzz fizz 7 8 fizz buzz 11 fizz 13 14 fizzbuzz 16 17 fizz 19 buzz
Zapoznaj się z dokumentacją referencyjną, aby uzyskać dodatkowe ograniczenia dotyczące instrukcji if konwertowanych metodą AutoGraph.
Pętle
AutoGraph skonwertuje niektóre instrukcje for
i while
na równoważne operacje pętli TensorFlow, takie jak tf.while_loop
. Jeśli nie przeprowadzeniu modyfikacji for
lub while
pętla jest wykonany w postaci pętli Pythona.
Ta zamiana ma miejsce w następujących sytuacjach:
-
for x in y
: jeśliy
jesttf.while_loop
, przekonwertuj natf.while_loop
. W szczególnym przypadku, gdyy
jesttf.data.Dataset
,tf.data.Dataset
jest kombinacja operacjitf.data.Dataset
. -
while <condition>
: jeśli<condition>
jesttf.while_loop
, przekonwertuj natf.while_loop
.
Pętla Pythona jest wykonywana podczas śledzenia, dodając dodatkowe tf.Graph
do tf.Graph
dla każdej iteracji pętli.
Pętla TensorFlow śledzi treść pętli i dynamicznie wybiera liczbę iteracji do uruchomienia w czasie wykonywania. Treść pętli pojawia się tylko raz w wygenerowanym tf.Graph
.
Zobacz dokumentację referencyjną, aby uzyskać dodatkowe ograniczenia dotyczące instrukcji AutoGraph przekonwertowanych for
i while
.
Pętla przez dane Pythona
Częstą tf.function
jest tf.function
danych Python / Numpy w tf.function
. Ta pętla będzie wykonywana podczas procesu śledzenia, dodając kopię twojego modelu do tf.Graph
dla każdej iteracji pętli.
Jeśli chcesz zawinąć całą pętlę szkoleniową w tf.function
, najbezpieczniejszym sposobem na to jest zawinięcie danych jakotf.data.Dataset
tak aby AutoGraph dynamicznie rozwijał pętlę szkoleniową.
def measure_graph_size(f, *args):
g = f.get_concrete_function(*args).graph
print("{}({}) contains {} nodes in its graph".format(
f.__name__, ', '.join(map(str, args)), len(g.as_graph_def().node)))
@tf.function
def train(dataset):
loss = tf.constant(0)
for x, y in dataset:
loss += tf.abs(y - x) # Some dummy computation.
return loss
small_data = [(1, 1)] * 3
big_data = [(1, 1)] * 10
measure_graph_size(train, small_data)
measure_graph_size(train, big_data)
measure_graph_size(train, tf.data.Dataset.from_generator(
lambda: small_data, (tf.int32, tf.int32)))
measure_graph_size(train, tf.data.Dataset.from_generator(
lambda: big_data, (tf.int32, tf.int32)))
train([(1, 1), (1, 1), (1, 1)]) contains 11 nodes in its graph train([(1, 1), (1, 1), (1, 1), (1, 1), (1, 1), (1, 1), (1, 1), (1, 1), (1, 1), (1, 1)]) contains 32 nodes in its graph train(<FlatMapDataset shapes: (<unknown>, <unknown>), types: (tf.int32, tf.int32)>) contains 10 nodes in its graph train(<FlatMapDataset shapes: (<unknown>, <unknown>), types: (tf.int32, tf.int32)>) contains 10 nodes in its graph
Podczas pakowania danych Python / Numpy w tf.data.Dataset.from_generator
danych należy pamiętać o tf.data.Dataset.from_generator
i tf.data.Dataset.from_tensors
. Pierwsza z nich zachowa dane w Pythonie i tf.py_function
je przez tf.py_function
co może mieć wpływ na wydajność, podczas gdy druga będzie tf.constant()
kopię danych jako jeden duży węzeł tf.constant()
na wykresie, co może mieć wpływ na pamięć.
Odczyt danych z plików przez TFRecordDataset / CsvDataset / etc. jest najbardziej efektywnym sposobem wykorzystania danych, ponieważ wtedy sam TensorFlow może zarządzać asynchronicznym ładowaniem i wstępnym pobieraniem danych, bez konieczności angażowania języka Python. Aby dowiedzieć się więcej, zobacz przewodnik tf.data .
Gromadzenie wartości w pętli
Powszechnym wzorcem jest gromadzenie wartości pośrednich z pętli. Zwykle dokonuje się tego poprzez dołączenie do listy Pythona lub dodanie wpisów do słownika Pythona. Jednak ponieważ są to efekty uboczne Pythona, nie będą one działać zgodnie z oczekiwaniami w dynamicznie rozwijanej pętli. Użyj tf.TensorArray
do gromadzenia wyników z dynamicznie rozwijanej pętli.
batch_size = 2
seq_len = 3
feature_size = 4
def rnn_step(inp, state):
return inp + state
@tf.function
def dynamic_rnn(rnn_step, input_data, initial_state):
# [batch, time, features] -> [time, batch, features]
input_data = tf.transpose(input_data, [1, 0, 2])
max_seq_len = input_data.shape[0]
states = tf.TensorArray(tf.float32, size=max_seq_len)
state = initial_state
for i in tf.range(max_seq_len):
state = rnn_step(input_data[i], state)
states = states.write(i, state)
return tf.transpose(states.stack(), [1, 0, 2])
dynamic_rnn(rnn_step,
tf.random.uniform([batch_size, seq_len, feature_size]),
tf.zeros([batch_size, feature_size]))
<tf.Tensor: shape=(2, 3, 4), dtype=float32, numpy= array([[[0.04024231, 0.8750231 , 0.7828673 , 0.95796347], [0.6933379 , 1.4302992 , 1.0286844 , 1.2873636 ], [1.0946463 , 2.3919935 , 1.383383 , 1.6184174 ]], [[0.35952663, 0.8509462 , 0.04545867, 0.40333533], [0.43437874, 1.1436893 , 0.31203532, 0.4216336 ], [0.46808207, 1.8400187 , 0.3120687 , 1.1190954 ]]], dtype=float32)>
Ograniczenia
Function
TensorFlow ma kilka ograniczeń projektowych, o których należy pamiętać podczas konwertowania funkcji Pythona na Function
.
Wykonywanie efektów ubocznych Pythona
Efekty uboczne, takie jak drukowanie, dołączanie do list i mutowanie zmiennych globalnych, mogą zachowywać się nieoczekiwanie wewnątrz Function
, czasami wykonując dwukrotnie lub nie wszystkie. Występują tylko przy pierwszym wywołaniu Function
z zestawem danych wejściowych. Następnie wyśledzony tf.Graph
jest ponownie wykonywany, bez wykonywania kodu Pythona.
Ogólna zasada jest taka, aby unikać polegania na efektach ubocznych Pythona w swojej logice i używać ich tylko do debugowania śladów. W przeciwnym razie interfejsy API tf.data
, takie jak tf.data
, tf.print
, tf.summary
, tf.Variable.assign
i tf.TensorArray
są najlepszym sposobem na zapewnienie wykonania kodu przez środowisko wykonawcze TensorFlow przy każdym wywołaniu.
@tf.function
def f(x):
print("Traced with", x)
tf.print("Executed with", x)
f(1)
f(1)
f(2)
Traced with 1 Executed with 1 Executed with 1 Traced with 2 Executed with 2
Jeśli chcesz wykonać kod Pythona podczas każdego wywołania Function
, tf.py_function
jest tf.py_function
wyjściową. Wadą tf.py_function
jest to, że nie jest przenośny ani szczególnie wydajny, nie można go zapisać za pomocą SavedModel i nie działa dobrze w konfiguracjach rozproszonych (multi-GPU, TPU). Ponadto, ponieważ tf.py_function
musi być podłączony do wykresu, tf.py_function
wszystkie wejścia / wyjścia na tensory.
Zmiana zmiennych globalnych i bezpłatnych w Pythonie
Zmiana globalnych i wolnych zmiennych w Pythonie liczy się jako efekt uboczny Pythona, więc dzieje się to tylko podczas śledzenia.
external_list = []
@tf.function
def side_effect(x):
print('Python side effect')
external_list.append(x)
side_effect(1)
side_effect(1)
side_effect(1)
# The list append only happened once!
assert len(external_list) == 1
Python side effect
Należy unikać mutowania kontenerów, takich jak listy, dykty i inne obiekty, które znajdują się poza Function
. Zamiast tego użyj argumentów i obiektów TF. Na przykład sekcja „Akumulowanie wartości w pętli” zawiera przykład implementacji operacji podobnych do list.
W niektórych przypadkach możesz przechwytywać stan i manipulować nim, jeśli jest to tf.Variable
. W ten sposób wagi modeli Keras są aktualizowane za pomocą powtarzających się wywołań tej samej funkcji ConcreteFunction
.
Korzystanie z iteratorów i generatorów Pythona
Wiele funkcji Pythona, takich jak generatory i iteratory, polega na środowisku wykonawczym Pythona do śledzenia stanu. Ogólnie rzecz biorąc, chociaż konstrukcje te działają zgodnie z oczekiwaniami w trybie przyspieszonym, są przykładami efektów ubocznych Pythona i dlatego występują tylko podczas śledzenia.
@tf.function
def buggy_consume_next(iterator):
tf.print("Value:", next(iterator))
iterator = iter([1, 2, 3])
buggy_consume_next(iterator)
# This reuses the first value from the iterator, rather than consuming the next value.
buggy_consume_next(iterator)
buggy_consume_next(iterator)
Value: 1 Value: 1 Value: 1
Podobnie jak TensorFlow ma wyspecjalizowany tf.TensorArray
dla konstrukcji list, ma wyspecjalizowany tf.data.Iterator
dla konstrukcji iteracyjnych. Aby zapoznać się z omówieniem, zobacz sekcję dotyczącą Transformacji AutoGraph . Ponadto interfejs API tf.data
może pomóc we wdrożeniu wzorców generatorów:
@tf.function
def good_consume_next(iterator):
# This is ok, iterator is a tf.data.Iterator
tf.print("Value:", next(iterator))
ds = tf.data.Dataset.from_tensor_slices([1, 2, 3])
iterator = iter(ds)
good_consume_next(iterator)
good_consume_next(iterator)
good_consume_next(iterator)
Value: 1 Value: 2 Value: 3
Usuwanie zmiennych tf. Między wywołaniami Function
Innym błędem, który możesz napotkać, jest zmienna zbierana bezużytecznie. Funkcja ConcreteFunction
zachowuje WeakRefs tylko do zmiennych, które zamykają, więc musisz zachować odniesienie do wszystkich zmiennych.
external_var = tf.Variable(3)
@tf.function
def f(x):
return x * external_var
traced_f = f.get_concrete_function(4)
print("Calling concrete function...")
print(traced_f(4))
# The original variable object gets garbage collected, since there are no more
# references to it.
external_var = tf.Variable(4)
print()
print("Calling concrete function after garbage collecting its closed Variable...")
with assert_raises(tf.errors.FailedPreconditionError):
traced_f(4)
Calling concrete function... tf.Tensor(12, shape=(), dtype=int32) Calling concrete function after garbage collecting its closed Variable... Caught expected exception <class 'tensorflow.python.framework.errors_impl.FailedPreconditionError'>: Traceback (most recent call last): File "<ipython-input-1-73d0ca52e838>", line 8, in assert_raises yield File "<ipython-input-1-9a93d2e07632>", line 16, in <module> traced_f(4) tensorflow.python.framework.errors_impl.FailedPreconditionError: 2 root error(s) found. (0) Failed precondition: Error while reading resource variable _AnonymousVar3 from Container: localhost. This could mean that the variable was uninitialized. Not found: Resource localhost/_AnonymousVar3/N10tensorflow3VarE does not exist. [[node ReadVariableOp (defined at <ipython-input-1-9a93d2e07632>:4) ]] (1) Failed precondition: Error while reading resource variable _AnonymousVar3 from Container: localhost. This could mean that the variable was uninitialized. Not found: Resource localhost/_AnonymousVar3/N10tensorflow3VarE does not exist. [[node ReadVariableOp (defined at <ipython-input-1-9a93d2e07632>:4) ]] [[ReadVariableOp/_2]] 0 successful operations. 0 derived errors ignored. [Op:__inference_f_782] Function call stack: f -> f
Znane problemy
Jeśli Function
nie jest oceniana prawidłowo, błąd może wynikać z tych znanych problemów, które mają zostać naprawione w przyszłości.
W zależności od zmiennych globalnych i bezpłatnych Pythona
Function
tworzy nową ConcreteFunction
gdy jest wywoływana z nową wartością argumentu Pythona. Jednak nie robi tego dla zamknięcia Pythona, globalnych lub nielokalnych tej Function
. Jeśli ich wartość zmieni się między wywołaniami Function
, Function
nadal będzie używać wartości, które miały, gdy była śledzona. Różni się to od działania zwykłych funkcji Pythona.
Z tego powodu zalecamy funkcjonalny styl programowania, który używa argumentów zamiast zamykania nazw zewnętrznych.
@tf.function
def buggy_add():
return 1 + foo
@tf.function
def recommended_add(foo):
return 1 + foo
foo = 1
print("Buggy:", buggy_add())
print("Correct:", recommended_add(foo))
Buggy: tf.Tensor(2, shape=(), dtype=int32) Correct: tf.Tensor(2, shape=(), dtype=int32)
print("Updating the value of `foo` to 100!")
foo = 100
print("Buggy:", buggy_add()) # Did not change!
print("Correct:", recommended_add(foo))
Updating the value of `foo` to 100! Buggy: tf.Tensor(2, shape=(), dtype=int32) Correct: tf.Tensor(101, shape=(), dtype=int32)
Możesz zamknąć nazwy zewnętrzne, o ile nie zaktualizujesz ich wartości.
W zależności od obiektów Pythona
Zalecenie przekazywania obiektów Pythona jako argumentów do tf.function
zawiera wiele znanych problemów, które powinny zostać naprawione w przyszłości. Ogólnie rzecz biorąc, możesz polegać na spójnym śledzeniu, jeśli używasz prymitywu Pythona lub struktury zgodnej z tf.nest
jako argumentu lub przekazujesz inne wystąpienie obiektu do Function
. Jednak Function
nie utworzy nowego śladu, gdy przekażesz ten sam obiekt i zmieni tylko jego atrybuty .
class SimpleModel(tf.Module):
def __init__(self):
# These values are *not* tf.Variables.
self.bias = 0.
self.weight = 2.
@tf.function
def evaluate(model, x):
return model.weight * x + model.bias
simple_model = SimpleModel()
x = tf.constant(10.)
print(evaluate(simple_model, x))
tf.Tensor(20.0, shape=(), dtype=float32)
print("Adding bias!")
simple_model.bias += 5.0
print(evaluate(simple_model, x)) # Didn't change :(
Adding bias! tf.Tensor(20.0, shape=(), dtype=float32)
Użycie tej samej Function
do oceny zaktualizowanej instancji modelu będzie błędne, ponieważ zaktualizowany model ma ten sam klucz pamięci podręcznej, co oryginalny model.
Z tego powodu zalecamy napisanie Function
aby uniknąć uzależnienia od zmiennych atrybutów obiektu lub tworzyć nowe obiekty.
Jeśli nie jest to możliwe, jednym obejściem jest tworzenie nowych Function
każdym razem, gdy modyfikujesz obiekt, aby wymusić odtworzenie:
def evaluate(model, x):
return model.weight * x + model.bias
new_model = SimpleModel()
evaluate_no_bias = tf.function(evaluate).get_concrete_function(new_model, x)
# Don't pass in `new_model`, `Function` already captured its state during tracing.
print(evaluate_no_bias(x))
tf.Tensor(20.0, shape=(), dtype=float32)
print("Adding bias!")
new_model.bias += 5.0
# Create new Function and ConcreteFunction since you modified new_model.
evaluate_with_bias = tf.function(evaluate).get_concrete_function(new_model, x)
print(evaluate_with_bias(x)) # Don't pass in `new_model`.
Adding bias! tf.Tensor(25.0, shape=(), dtype=float32)
Ponieważ odtwarzanie może być kosztowne , możesz użyć tf.Variable
s jako atrybutów obiektu, które można zmutować (ale nie zmienić, uwaga!) W celu uzyskania podobnego efektu bez konieczności tf.Variable
.
class BetterModel:
def __init__(self):
self.bias = tf.Variable(0.)
self.weight = tf.Variable(2.)
@tf.function
def evaluate(model, x):
return model.weight * x + model.bias
better_model = BetterModel()
print(evaluate(better_model, x))
tf.Tensor(20.0, shape=(), dtype=float32)
print("Adding bias!")
better_model.bias.assign_add(5.0) # Note: instead of better_model.bias += 5
print(evaluate(better_model, x)) # This works!
Adding bias! tf.Tensor(25.0, shape=(), dtype=float32)
Tworzenie zmiennych tf.Variables
Function
obsługuje tworzenie zmiennych tylko raz, przy pierwszym wywołaniu, a następnie ponowne ich użycie. Nie można tworzyć tf.Variables
w nowych śladach. Tworzenie nowych zmiennych w kolejnych wywołaniach jest obecnie niedozwolone, ale będzie w przyszłości.
Przykład:
@tf.function
def f(x):
v = tf.Variable(1.0)
return v
with assert_raises(ValueError):
f(1.0)
Caught expected exception <class 'ValueError'>: Traceback (most recent call last): File "<ipython-input-1-73d0ca52e838>", line 8, in assert_raises yield File "<ipython-input-1-8a0913e250e0>", line 7, in <module> f(1.0) ValueError: in user code: <ipython-input-1-8a0913e250e0>:3 f * v = tf.Variable(1.0) /tmpfs/src/tf_docs_env/lib/python3.6/site-packages/tensorflow/python/ops/variables.py:262 __call__ ** return cls._variable_v2_call(*args, **kwargs) /tmpfs/src/tf_docs_env/lib/python3.6/site-packages/tensorflow/python/ops/variables.py:256 _variable_v2_call shape=shape) /tmpfs/src/tf_docs_env/lib/python3.6/site-packages/tensorflow/python/ops/variables.py:67 getter return captured_getter(captured_previous, **kwargs) /tmpfs/src/tf_docs_env/lib/python3.6/site-packages/tensorflow/python/eager/def_function.py:731 invalid_creator_scope "tf.function-decorated function tried to create " ValueError: tf.function-decorated function tried to create variables on non-first call.
Możesz tworzyć zmienne wewnątrz Function
o ile te zmienne są tworzone tylko przy pierwszym wykonaniu funkcji.
class Count(tf.Module):
def __init__(self):
self.count = None
@tf.function
def __call__(self):
if self.count is None:
self.count = tf.Variable(0)
return self.count.assign_add(1)
c = Count()
print(c())
print(c())
tf.Tensor(1, shape=(), dtype=int32) tf.Tensor(2, shape=(), dtype=int32)
Używanie z wieloma optymalizatorami Keras
Możesz napotkać ValueError: tf.function-decorated function tried to create variables on non-first call.
podczas używania więcej niż jednego optymalizatora Keras z tf.function
. Ten błąd występuje, ponieważ optymalizatory wewnętrznie tworzą tf.Variables
gdy stosują gradienty po raz pierwszy.
opt1 = tf.keras.optimizers.Adam(learning_rate = 1e-2)
opt2 = tf.keras.optimizers.Adam(learning_rate = 1e-3)
@tf.function
def train_step(w, x, y, optimizer):
with tf.GradientTape() as tape:
L = tf.reduce_sum(tf.square(w*x - y))
gradients = tape.gradient(L, [w])
optimizer.apply_gradients(zip(gradients, [w]))
w = tf.Variable(2.)
x = tf.constant([-1.])
y = tf.constant([2.])
train_step(w, x, y, opt1)
print("Calling `train_step` with different optimizer...")
with assert_raises(ValueError):
train_step(w, x, y, opt2)
Calling `train_step` with different optimizer... Caught expected exception <class 'ValueError'>: Traceback (most recent call last): File "<ipython-input-1-73d0ca52e838>", line 8, in assert_raises yield File "<ipython-input-1-d3d3937dbf1a>", line 18, in <module> train_step(w, x, y, opt2) ValueError: in user code: <ipython-input-1-d3d3937dbf1a>:9 train_step * optimizer.apply_gradients(zip(gradients, [w])) /tmpfs/src/tf_docs_env/lib/python3.6/site-packages/tensorflow/python/keras/optimizer_v2/optimizer_v2.py:604 apply_gradients ** self._create_all_weights(var_list) /tmpfs/src/tf_docs_env/lib/python3.6/site-packages/tensorflow/python/keras/optimizer_v2/optimizer_v2.py:781 _create_all_weights _ = self.iterations /tmpfs/src/tf_docs_env/lib/python3.6/site-packages/tensorflow/python/keras/optimizer_v2/optimizer_v2.py:788 __getattribute__ return super(OptimizerV2, self).__getattribute__(name) /tmpfs/src/tf_docs_env/lib/python3.6/site-packages/tensorflow/python/keras/optimizer_v2/optimizer_v2.py:926 iterations aggregation=tf_variables.VariableAggregation.ONLY_FIRST_REPLICA) /tmpfs/src/tf_docs_env/lib/python3.6/site-packages/tensorflow/python/keras/optimizer_v2/optimizer_v2.py:1132 add_weight aggregation=aggregation) /tmpfs/src/tf_docs_env/lib/python3.6/site-packages/tensorflow/python/training/tracking/base.py:810 _add_variable_with_custom_getter **kwargs_for_getter) /tmpfs/src/tf_docs_env/lib/python3.6/site-packages/tensorflow/python/keras/engine/base_layer_utils.py:142 make_variable shape=variable_shape if variable_shape else None) /tmpfs/src/tf_docs_env/lib/python3.6/site-packages/tensorflow/python/ops/variables.py:260 __call__ return cls._variable_v1_call(*args, **kwargs) /tmpfs/src/tf_docs_env/lib/python3.6/site-packages/tensorflow/python/ops/variables.py:221 _variable_v1_call shape=shape) /tmpfs/src/tf_docs_env/lib/python3.6/site-packages/tensorflow/python/ops/variables.py:67 getter return captured_getter(captured_previous, **kwargs) /tmpfs/src/tf_docs_env/lib/python3.6/site-packages/tensorflow/python/eager/def_function.py:731 invalid_creator_scope "tf.function-decorated function tried to create " ValueError: tf.function-decorated function tried to create variables on non-first call.
Jeśli musisz zmienić optymalizator podczas uczenia, obejściem jest utworzenie nowej Function
dla każdego optymalizatora, wywołując bezpośrednio ConcreteFunction
.
opt1 = tf.keras.optimizers.Adam(learning_rate = 1e-2)
opt2 = tf.keras.optimizers.Adam(learning_rate = 1e-3)
# Not a tf.function.
def train_step(w, x, y, optimizer):
with tf.GradientTape() as tape:
L = tf.reduce_sum(tf.square(w*x - y))
gradients = tape.gradient(L, [w])
optimizer.apply_gradients(zip(gradients, [w]))
w = tf.Variable(2.)
x = tf.constant([-1.])
y = tf.constant([2.])
# Make a new Function and ConcreteFunction for each optimizer.
train_step_1 = tf.function(train_step).get_concrete_function(w, x, y, opt1)
train_step_2 = tf.function(train_step).get_concrete_function(w, x, y, opt2)
for i in range(10):
if i % 2 == 0:
train_step_1(w, x, y) # `opt1` is not used as a parameter.
else:
train_step_2(w, x, y) # `opt2` is not used as a parameter.
Używanie z wieloma modelami Keras
Możesz również napotkać ValueError: tf.function-decorated function tried to create variables on non-first call.
podczas przekazywania różnych instancji modelu do tej samej Function
.
Ten błąd występuje, ponieważ modele Keras (które nie mają zdefiniowanego kształtu wejściowego ) i warstwy Keras tworzą tf.Variables
przy pierwszym wywołaniu. Możesz próbować zainicjować te zmienne wewnątrz Function
, która została już wywołana. Aby uniknąć tego błędu, spróbuj wywołać model.build(input_shape)
celu zainicjowania wszystkich wag przed model.build(input_shape)
modelu.
Dalsza lektura
Aby dowiedzieć się, jak eksportować i ładować Function
, zobacz przewodnik SavedModel . Aby dowiedzieć się więcej na temat optymalizacji wykresów, które są wykonywane po śledzeniu, zobacz przewodnik dotyczący grapplera . Aby dowiedzieć się, jak zoptymalizować potok danych i profilować model, zobacz przewodnik Profiler .