ЦІЙ ВІДПОВІДЬ : спрямований на те, щоб надати детальний опис випуску на рівні графіка / обладнання - включаючи шлейфи поїздів TF2 проти TF1, процесори введення даних та виконання режиму Eager vs. Graph. Для підсумків та рекомендацій щодо вирішення проблеми дивіться мою іншу відповідь.
ВЕРДИКА ДІЯЛЬНОСТІ : іноді одна швидша, інша - залежно від конфігурації. Що стосується TF2 проти TF1, то вони в середньому приблизно на рівні, але існують значні відмінності на основі конфігурацій, і TF1 козирує TF2 частіше, ніж навпаки. Див. "БЕНХМАРКІНГ" нижче.
EAGER VS. GRAPH : м'ясо цієї всієї відповіді для деяких: прагнення TF2 повільніше, ніж TF1, згідно з моїм тестуванням. Детальніше далі.
Принципова відмінність між ними полягає в тому, що Graph налаштовує обчислювальну мережу проактивно і виконує, коли "їм сказано" - тоді як Eager виконує все при створенні. Але історія тільки починається тут:
Нетерплячий НЕ позбавлений Графіка , і насправді може бути переважно Графіком, всупереч очікуванням. Що це в значній мірі - це виконаний графік - це включає ваги моделі та оптимізатора, що складають велику частину графіка.
Eager відновлює частину власного графіка при виконанні ; Прямий наслідок не повністю побудованого графіка - див. результати профілера. Це обчислювальні витрати.
Захист - це повільніші введення / вхідні вводи ; відповідно до цього коментаря та коду Git , Numpy вхідні дані в Eager включають накладні витрати на копіювання тензорів з процесора в GPU. Переглядаючи вихідний код, відмінності в обробці даних очевидні; Eager безпосередньо передає Numpy, тоді як Graph передає тензори, які потім оцінюють Numpy; невідомий точний процес, але останні повинні включати оптимізацію рівня GPU
TF2 Eager повільніше, ніж TF1 Eager - це ... несподівано. Дивіться результати бенчмаркінгу нижче. Відмінності охоплюють від незначних до значних, але є послідовними. Не впевнений, чому це так - якщо роз'яснений розробник TF, відповідь буде оновлено.
TF2 проти TF1 : цитування відповідних частин розробника TF, Q. Скотта Чжу, відповідь - з / б мій акцент та переформулювання:
У нетерпінні час виконання повинен виконувати ops та повертати числове значення для кожного рядка коду python. Характер виконання одного кроку призводить до того, що воно відбувається повільно .
У TF2 Керас використовує tf.function для складання свого графіка для тренувань, оцінювання та прогнозування. Ми називаємо їх «функцією виконання» для моделі. У TF1 "функцією виконання" була FuncGraph, яка поділяла деякий загальний компонент як функцію TF, але має іншу реалізацію.
Під час процесу ми якось залишили неправильну реалізацію для train_on_batch (), test_on_batch () та predict_on_batch () . Вони все ще є чисельно правильними , але функція виконання для x_on_batch - це чиста функція python, а не функція, що обгорнула функцію python. Це спричинить повільність
У TF2 ми перетворюємо всі вхідні дані в tf.data.Dataset, за допомогою якого ми можемо уніфікувати нашу функцію виконання для обробки одного типу входів. Можливо, у конверсії набору даних буде деяка накладна оплата , і я думаю, що це разова накладні витрати, а не витрати на одну партію
З останнім реченням останнього абзацу вище та останнім пунктом нижче абзацу:
Щоб подолати повільність у режимі нетерпіння, у нас є @ tf.function, яка перетворить функцію пітона в графік. Коли числове значення подачі, як np масив, тіло функції tf.function перетворюється в статичний графік, оптимізуючи його, і повертає кінцеве значення, яке є швидким і повинно мати аналогічні показники, як графічний режим TF1.
Я не згоден - за моїми результатами профілювання, які показують, що обробка вхідних даних Eager є значно повільнішою, ніж у Graph. Крім того, не впевнені, tf.data.Dataset
зокрема, але Eager неодноразово викликає кілька одних і тих же методів перетворення даних - див. Профілер.
Нарешті, пов’язана програма dev: Значна кількість змін для підтримки циклів Keras v2 .
Шлейфи поїздів : залежно від (1) Eager vs. Graph; (2) формат вхідних даних, навчання буде проходити в с виразним контуром поїзда - в TF2, _select_training_loop()
, training.py , один з:
training_v2.Loop()
training_distributed.DistributionMultiWorkerTrainingLoop(
training_v2.Loop()) # multi-worker mode
# Case 1: distribution strategy
training_distributed.DistributionMultiWorkerTrainingLoop(
training_distributed.DistributionSingleWorkerTrainingLoop())
# Case 2: generator-like. Input is Python generator, or Sequence object,
# or a non-distributed Dataset or iterator in eager execution.
training_generator.GeneratorOrSequenceTrainingLoop()
training_generator.EagerDatasetOrIteratorTrainingLoop()
# Case 3: Symbolic tensors or Numpy array-like. This includes Datasets and iterators
# in graph mode (since they generate symbolic tensors).
training_generator.GeneratorLikeTrainingLoop() # Eager
training_arrays.ArrayLikeTrainingLoop() # Graph
Кожен по-різному обробляє розподіл ресурсів і несе наслідки для продуктивності та можливостей.
Поїзд Loops: fit
проти train_on_batch
, keras
протиtf.keras
: кожен з чотирьох використань різних контурів поїзда, хоча , можливо , не у всіх можливих комбінаціях. keras
' fit
, наприклад, використовує форму fit_loop
, наприклад training_arrays.fit_loop()
, і його train_on_batch
може використовувати K.function()
. tf.keras
має більш складну ієрархію, описану в частині попереднього розділу.
Поїзд циклів: документація - відповідний джерело docstring про деякі з різних методів виконання:
На відміну від інших операцій TensorFlow, ми не перетворюємо числові входи python в тензори. Крім того, для кожного окремого числового значення пітона формується новий графік
function
створює окремий графік для кожного унікального набору вхідних форм і типів даних .
Одному об'єкту tf.function може знадобитися зіставити декілька обчислювальних графіків під кришкою. Це повинно бути видно лише як продуктивність (графіки відстеження мають ненульову обчислювальну та пам’ятну вартість )
Процесори вхідних даних : аналогічно вище, процесор вибирається в кожному конкретному випадку, залежно від внутрішніх прапорів, встановлених відповідно до конфігурацій виконання (режим виконання, формат даних, стратегія розподілу). Найпростіший випадок з Eager, який працює безпосередньо з масивами Numpy. Деякі конкретні приклади дивіться у цій відповіді .
РОЗМІР МОДЕЛІ, РОЗМІР ДАНИХ:
- Є вирішальним; жодна конфігурація не увінчалася всіма розмірами моделі та даних.
- Розмір дані щодо розміру моделі має важливе значення; для невеликих даних та моделі передача даних (наприклад, процесор до GPU) може домінувати накладні витрати. Крім того, невеликі накладні процесори можуть працювати повільніше на великих даних за домінуючий час перетворення даних (див.
convert_to_tensor
У розділі "ПРОФІЛЕР")
- Швидкість різниться за різними засобами обробки ресурсів для процесорів вхідних та вхідних даних.
БЕНЧМАРКИ : м'ясо, що перемелюється. - Word Document - таблиця Excel
Термінологія :
- % без чисел - всі секунди
- % обчислено як
(1 - longer_time / shorter_time)*100
; обґрунтування: нас цікавить , який фактор швидший за інший; shorter / longer
насправді є нелінійним відношенням, не корисним для прямого порівняння
- Визначення знаку%:
- TF2 проти TF1:
+
якщо TF2 швидше
- GvE (Graph vs. Eager):
+
якщо графік швидший
- TF2 = TensorFlow 2.0.0 + Keras 2.3.1; TF1 = TensorFlow 1.14.0 + Keras 2.2.5
ПРОФІЛЕР :
ПРОФІЛЕР - Пояснення : Профілер IDE Spyder 3.3.6.
Деякі функції повторюються в гніздах інших; отже, важко відстежити точний поділ між функціями "обробка даних" та "навчанням", тому буде певне збіг - як це виражено в останньому результаті.
% цифр обчислюється wrt час виконання мінус час складання
- Час побудови обчислюється шляхом підсумовування всіх (унікальних) час виконання, які були названі 1 або 2 рази
- Навчайте час, що обчислюється, підсумовуючи всі (унікальні) тривалість виконання, які називались у той самий # раз, як ітерації, і деякі з них.
- На жаль, функції функціонують відповідно до їхніх оригінальних назв (тобто вони
_func = func
матимуть профіль як func
), що змішується під час збирання - звідси необхідність виключити його
ТЕСТОВА ОКОЛНІСТЬ :
- Виконаний код внизу w / мінімальні фонові завдання, що виконуються
- Графічний процесор був "розігрітий" без декількох ітерацій перед тимчасовими ітераціями, як пропонується в цьому дописі
- CUDA 10.0.130, cuDNN 7.6.0, TensorFlow 1.14.0 та TensorFlow 2.0.0, створені з джерела, плюс Анаконда
- Python 3.7.4, Spyder 3.3.6 IDE
- GTX 1070, Windows 10, 24 Гб оперативної пам'яті DDR4 2,4 МГц, процесор i7-7700HQ 2,8 ГГц
МЕТОДОЛОГІЯ :
- Орієнтовний показник "малий", "середній" та "великий" модель та розміри даних
- Виправте # параметрів для кожного розміру моделі, незалежно від розміру вхідних даних
- "Більша" модель має більше параметрів і шарів
- "Більші" дані мають більш довгу послідовність, але однакові
batch_size
іnum_channels
- Моделі використовують тільки
Conv1D
, Dense
«Ті, що навчаються» шари; Уникнути RNN на реалізацію TF-версії. відмінності
- Завжди проїжджав один поїзд, розташований поза шлейфом тестування, щоб опустити побудову моделі та оптимізатора
- Не використовуються розріджені дані (наприклад
layers.Embedding()
) або розріджені цілі (наприклад,SparseCategoricalCrossEntropy()
ОБМЕЖЕННЯ : "повна" відповідь пояснювала б кожен можливий цикл поїзда та ітератор, але це, безумовно, поза моїми можливостями в часі, неіснуюча зарплата або загальна необхідність. Результати настільки ж хороші, як і методологія - інтерпретувати відкрито.
КОД :
import numpy as np
import tensorflow as tf
import random
from termcolor import cprint
from time import time
from tensorflow.keras.layers import Input, Dense, Conv1D
from tensorflow.keras.layers import Dropout, GlobalAveragePooling1D
from tensorflow.keras.models import Model
from tensorflow.keras.optimizers import Adam
import tensorflow.keras.backend as K
#from keras.layers import Input, Dense, Conv1D
#from keras.layers import Dropout, GlobalAveragePooling1D
#from keras.models import Model
#from keras.optimizers import Adam
#import keras.backend as K
#tf.compat.v1.disable_eager_execution()
#tf.enable_eager_execution()
def reset_seeds(reset_graph_with_backend=None, verbose=1):
if reset_graph_with_backend is not None:
K = reset_graph_with_backend
K.clear_session()
tf.compat.v1.reset_default_graph()
if verbose:
print("KERAS AND TENSORFLOW GRAPHS RESET")
np.random.seed(1)
random.seed(2)
if tf.__version__[0] == '2':
tf.random.set_seed(3)
else:
tf.set_random_seed(3)
if verbose:
print("RANDOM SEEDS RESET")
print("TF version: {}".format(tf.__version__))
reset_seeds()
def timeit(func, iterations, *args, _verbose=0, **kwargs):
t0 = time()
for _ in range(iterations):
func(*args, **kwargs)
print(end='.'*int(_verbose))
print("Time/iter: %.4f sec" % ((time() - t0) / iterations))
def make_model_small(batch_shape):
ipt = Input(batch_shape=batch_shape)
x = Conv1D(128, 40, strides=4, padding='same')(ipt)
x = GlobalAveragePooling1D()(x)
x = Dropout(0.5)(x)
x = Dense(64, activation='relu')(x)
out = Dense(1, activation='sigmoid')(x)
model = Model(ipt, out)
model.compile(Adam(lr=1e-4), 'binary_crossentropy')
return model
def make_model_medium(batch_shape):
ipt = Input(batch_shape=batch_shape)
x = ipt
for filters in [64, 128, 256, 256, 128, 64]:
x = Conv1D(filters, 20, strides=1, padding='valid')(x)
x = GlobalAveragePooling1D()(x)
x = Dense(256, activation='relu')(x)
x = Dropout(0.5)(x)
x = Dense(128, activation='relu')(x)
x = Dense(64, activation='relu')(x)
out = Dense(1, activation='sigmoid')(x)
model = Model(ipt, out)
model.compile(Adam(lr=1e-4), 'binary_crossentropy')
return model
def make_model_large(batch_shape):
ipt = Input(batch_shape=batch_shape)
x = Conv1D(64, 400, strides=4, padding='valid')(ipt)
x = Conv1D(128, 200, strides=1, padding='valid')(x)
for _ in range(40):
x = Conv1D(256, 12, strides=1, padding='same')(x)
x = Conv1D(512, 20, strides=2, padding='valid')(x)
x = Conv1D(1028, 10, strides=2, padding='valid')(x)
x = Conv1D(256, 1, strides=1, padding='valid')(x)
x = GlobalAveragePooling1D()(x)
x = Dense(256, activation='relu')(x)
x = Dropout(0.5)(x)
x = Dense(128, activation='relu')(x)
x = Dense(64, activation='relu')(x)
out = Dense(1, activation='sigmoid')(x)
model = Model(ipt, out)
model.compile(Adam(lr=1e-4), 'binary_crossentropy')
return model
def make_data(batch_shape):
return np.random.randn(*batch_shape), \
np.random.randint(0, 2, (batch_shape[0], 1))
def make_data_tf(batch_shape, n_batches, iters):
data = np.random.randn(n_batches, *batch_shape),
trgt = np.random.randint(0, 2, (n_batches, batch_shape[0], 1))
return tf.data.Dataset.from_tensor_slices((data, trgt))#.repeat(iters)
batch_shape_small = (32, 140, 30)
batch_shape_medium = (32, 1400, 30)
batch_shape_large = (32, 14000, 30)
batch_shapes = batch_shape_small, batch_shape_medium, batch_shape_large
make_model_fns = make_model_small, make_model_medium, make_model_large
iterations = [200, 100, 50]
shape_names = ["Small data", "Medium data", "Large data"]
model_names = ["Small model", "Medium model", "Large model"]
def test_all(fit=False, tf_dataset=False):
for model_fn, model_name, iters in zip(make_model_fns, model_names, iterations):
for batch_shape, shape_name in zip(batch_shapes, shape_names):
if (model_fn is make_model_large) and (batch_shape is batch_shape_small):
continue
reset_seeds(reset_graph_with_backend=K)
if tf_dataset:
data = make_data_tf(batch_shape, iters, iters)
else:
data = make_data(batch_shape)
model = model_fn(batch_shape)
if fit:
if tf_dataset:
model.train_on_batch(data.take(1))
t0 = time()
model.fit(data, steps_per_epoch=iters)
print("Time/iter: %.4f sec" % ((time() - t0) / iters))
else:
model.train_on_batch(*data)
timeit(model.fit, iters, *data, _verbose=1, verbose=0)
else:
model.train_on_batch(*data)
timeit(model.train_on_batch, iters, *data, _verbose=1)
cprint(">> {}, {} done <<\n".format(model_name, shape_name), 'blue')
del model
test_all(fit=True, tf_dataset=False)