TensorFlow 2.1: Практическое руководство

Режим кераса, режим нетерпения и режим графика

Если вы, как и я, похожи на нормального человека, вы, вероятно, испытали, как иногда вы так увлекаетесь разработкой своего приложения, что трудно найти момент, чтобы остановиться и подумать, делаем ли мы что-то максимально возможное эффективный способ: используем ли мы правильные инструменты? какая структура лучше всего подходит для моего случая использования? расширяем ли этот подход? мы имеем в виду масштабируемость?

Это особенно актуально в области искусственного интеллекта. Все мы знаем, что ИИ - это быстро меняющаяся область. Новые исследования публикуются каждый день. Между основными фреймворками искусственного интеллекта, которые развиваются высокими темпами, идет огромная борьба. Выпущены новые аппаратные архитектуры, микросхемы и оптимизации для поддержки развертывания растущего внедрения ИИ… Однако, несмотря на все навороты, иногда вам нужно остановиться и пересмотреть свое мнение.

Когда лучше остановиться и подумать? Это будете знать только вы. Для меня этот момент наступил совсем недавно. Я использую Keras и Tensorflow 1.x (TF1) как на работе, так и в своих личных проектах с тех пор, как начал работать в этой области. Я полностью влюблен в высокоуровневый подход библиотеки Keras и низкоуровневый подход Tensorlfow, который позволяет вам изменять вещи под капотом, когда вам нужно больше настройки.

Хотя я был большим поклонником этого брака между Keras и Tensorflow, всегда был очень специфический недостаток, делавший эту пару далеко не идиллической: функции отладки. Как вы уже знаете, в Tensorflow есть парадигма, согласно которой сначала определяется вычислительный граф, затем его компилируют (или переносят на графический процессор), а затем запускают очень эффективно. Эта парадигма очень хороша и имеет смысл с технической точки зрения, но когда у вас есть модель в графическом процессоре, ее практически невозможно отладить.

Вот почему через некоторое время и по совпадению, что прошел примерно год с тех пор, как TensorFlow 2.0 был опубликован в его альфа-версии, я решил сделать снимок на TensorFlow 2.1 (я мог бы начать с TF2.0, но мы все знаем, что люблю новое программное обеспечение) и расскажу, как все прошло.

TensorFLow 2.1

Печальная правда заключается в том, что мне было трудно понять, как я должен был использовать эту новую версию TensorFlow, знаменитую стабильную версию 2.1. Я знаю, что существует множество руководств, блокнотов и текстов кода… Однако я обнаружил, что трудности были не в программировании, поскольку, в конце концов, это просто Python, а смена парадигмы. Проще говоря: программирование на TensorFlow 2 отличается от TensorFlow 1 точно так же, как объектно-ориентированное программирование отличается от функционального программирования.

Проведя несколько экспериментов, я обнаружил, что в TensorFlow 2.1 есть 3 подхода к построению моделей:

  • Режим Keras (tf.keras): основан на определении графика и запускает график позже.
  • Активный режим: основан на определении выполнения всех операций, которые определяют граф итеративно.
  • Режим графика (tf.function): смесь двух предыдущих подходов.

Так что скучно! Покажи мне код!

Режим Кераса

Это стандартное использование, к которому мы все привыкли. Используйте просто Keras с настраиваемой функцией потерь с потерями в квадрате ошибок. Сеть представляет собой трехуровневую глубокую сеть.

# The network
x = Input(shape=[20])
h = Dense(units=20, activation='relu')(x)
h = Dense(units=10, activation='relu')(h)
y = Dense(units=1)(h)

Цель здесь - научить сеть научиться суммировать вектор из 20 элементов. Итак, мы снабжаем сеть набором данных из [10000 x 20], то есть 10000 выборок по 20 характеристик в каждой (элементы для суммирования). Это в:

# Training samples
train_samples = tf.random.normal(shape=(10000, 20))
train_targets = tf.reduce_sum(train_samples, axis=-1)
test_samples = tf.random.normal(shape=(100, 20))
test_targets = tf.reduce_sum(test_samples, axis=-1)

Мы можем запустить этот пример и получить обычный красивый вывод Keras:

Epoch 1/10
10000/10000 [==============================] - 10s 1ms/sample - loss: 1.6754 - val_loss: 0.0481
Epoch 2/10
10000/10000 [==============================] - 10s 981us/sample - loss: 0.0227 - val_loss: 0.0116
Epoch 3/10
10000/10000 [==============================] - 10s 971us/sample - loss: 0.0101 - val_loss: 0.0070

Так что здесь происходит? Ну, ничего, просто пример игрушки Keras, тренирующийся по 10 секунд в эпоху (в NVIDIA GTX 1080 Ti). А как насчет парадигмы программирования? Как и раньше, как и в TF1.x, вы определяете граф, а затем запускаете его, вызывая keras.models.Model.fit. А особенности отладки? Как и раньше ... Нет. Вы не можете даже установить простую точку останова в функции потерь.

После запуска у вас может возникнуть очень очевидный вопрос: где же, черт возьми, все приятные функции, обещанные выпуском TensorFlow 2? И ты был бы прав. Если интеграция с пакетом Keras означает отсутствие необходимости устанавливать дополнительный пакет… в чем преимущество?

Вдобавок возникает еще один важный вопрос: где же самые известные, как и следовало ожидать, функции отладки? К счастью, именно здесь на помощь приходит режим нетерпения.

Режим нетерпения

Что, если бы я сказал вам, что есть способ создавать ваши модели в интерактивном режиме и иметь доступ ко всем операциям во время выполнения? - Если вас трясет от волнения, это означает, что вы испытали глубокую боль из-за ошибки времени выполнения в случайном пакете после 10 эпох ... Да, я знаю, я тоже был там, мы можем начать называть себя братьями по оружие после тех сражений.

Ну да, это именно тот режим работы, который вы искали. В этом режиме все тензорные операции интерактивны, вы можете установить точку останова и получить доступ к любой из промежуточных тензорных переменных. Однако за эту гибкость приходится платить: более явный код. Давайте взглянем:

Первое, что может прийти вам в голову после прочтения кода, может быть: много кода только для выполнения model.compile и model.fit. Да, верно. Но с другой стороны, вы контролируете все, что происходило под капотом раньше. А что происходило под капотом? Цикл обучения.

Итак, теперь все меняется. При таком подходе вы можете спроектировать, как все будет работать с нуля. Вот что вы можете указать сейчас:

  • Метрики: когда-нибудь хотели измерить результаты по образцам, партиям или по любой другой настраиваемой статистике? Нет проблем, мы вас прикрыли. Теперь вы можете использовать старую добрую скользящую среднюю или любую другую настраиваемую метрику в зависимости от того, что хотите.
  • Функция потерь: когда-нибудь хотелось сделать сумасшедшую функцию потерь, зависящую от нескольких параметров? Что ж, это тоже решено, вы можете получить все, что захотите, в определении функции потерь без того, чтобы Керас жаловался на это своим _7 _ (ссылка)
  • Градиенты: вы можете получить доступ к градиентам и определить особенности прямого и обратного прохода. Да, наконец, пожалуйста, присоединяйтесь ко мне в большом: Ура!

Метрики указаны с новым tf.keras.metrics API. Вы просто берете нужную метрику, определяете ее и используете следующим образом:

# Getting metric instanced
metric = tf.keras.metrics.Mean() 
# Run your model to get the loss and update the metric
loss = [...]
metric(loss)
# Print the metric 
print('Training Loss: %.3f' % metric.result().numpy())

Функция потерь и градиенты вычисляются при прямом и обратном проходе соответственно. В этом подходе прямой проход должен быть записан tf.GradientTape. tf.GradientTape будет отслеживать (или записывать) все тензорные операции, выполняемые в прямом проходе, чтобы он мог вычислять градиенты в обратном проходе. Другими словами: чтобы бежать назад, вы должны помнить путь, который вы выбрали вперед.

# Forward pass: needs to be recorded by gradient tape
with tf.GradientTape() as tape:
    y_pred = model(x)
    loss = loss_compute(y_true, y_pred)
# Backward pass:
gradients = tape.gradient(loss, model.trainable_weights)
optimizer.apply_gradients(zip(gradients, model.trainable_weights))

Это довольно просто: на прямом проходе вы запускаете свой прогноз и смотрите, насколько хорошо вы его сделали, вычисляя убыток. На обратном проходе вы проверяете, как ваши веса повлияли на эту потерю, вычисляя градиенты, а затем пытаетесь минимизировать потери, обновляя веса (с помощью оптимизатора).

Вы также можете заметить в коде, что в конце каждой эпохи вычисляется потеря проверки (путем выполнения только прямого прохода без обновления весов).

Что ж, давайте посмотрим, как это соотносится с предыдущим подходом (я немного уменьшил вывод, чтобы он мог уместиться здесь):

Epoch 1:
Loss: 1.310: 100%|███████████| 10000/10000 [00:41<00:00, 239.70it/s]
Epoch 2:
Loss: 0.018: 100%|███████████| 10000/10000 [00:41<00:00, 240.21it/s]
Epoch 3:
Loss: 0.010: 100%|███████████| 10000/10000 [00:41<00:00, 239.28it/s]

Что случилось? Вы заметили? На одну и ту же машину уходила 41 секунда на эпоху, то есть приращение времени в 4 раза… И это всего лишь фиктивная модель. Можете ли вы представить, как это можно масштабировать для реальной модели использования, такой как RetinaNet, YOLO или MaskRCNN?

К счастью, хорошие ребята из TensorFlow знали об этом и реализовали режим графа.

Графический режим

Режим графика (из AutoGraph или tf.function) представляет собой своего рода смешанный режим между двумя предыдущими. Вы можете понять, что это такое, здесь и здесь. Но я обнаружил, что эти руководства немного сбивают с толку, поэтому я объясняю это своими словами.

Если режим Keras был связан с определением графика и последующим запуском его в графическом процессоре, а активный режим был для интерактивного выполнения каждого шага, режим графика позволяет вам кодировать, как если бы вы были в активном режиме, но запускать обучение почти так же быстро, как если бы вы были в режиме Keras (так что да, в графическом процессоре).

Единственное изменение в отношении активного режима состоит в том, что в графическом режиме вы разбиваете код на небольшие функции и аннотируете эти функции с помощью @tf.function. Давайте посмотрим, как все изменилось:

Теперь вы видите, как вычисления прямого и обратного прохода были преобразованы в 2 функции, которые были аннотированы декоратором @tf.function.

Так что же здесь происходит на самом деле? Легкий. Каждый раз, когда вы аннотируете функцию с помощью декоратора @tf.function, вы «компилируете» эти операции в GPU так же, как это делает Keras. Итак, аннотируя свои функции, вы указываете TensorFlow запускать эти операции на оптимизированном графике в графическом процессоре.

На самом деле происходит то, что функция анализируется AutoGraph, tf.autograph. AutoGraph возьмет входные и выходные данные функции и сгенерирует из них график TensorFlow, что означает, что он будет анализировать операции, чтобы получить выходные данные из входов в график TensorFlow. Этот сгенерированный график будет очень эффективно загружен в графический процессор.

Вот почему это своего рода смешанный режим, потому что все операции выполняются интерактивно, за исключением операций, аннотированных декоратором @tf.function.

Это также означает, что у вас будет доступ ко всем переменным и тензорам, кроме тех, которые входят в функции, отмеченные знаком @tf.function, из которых вы будете иметь доступ только к его входам и выходам. Этот подход устанавливает очень четкий способ отладки, при котором вы можете начать разработку в интерактивном режиме в активном режиме, а затем, когда ваша модель будет готова, подтолкнуть ее к производственной производительности с помощью @tf.function. Звучит хорошо, правда? Посмотрим, как это пойдет:

Epoch 1:
Loss: 1.438: 100%|████████████| 10000/10000 [00:16<00:00, 612.3it/s]
Epoch 2:
Loss: 0.015: 100%|████████████| 10000/10000 [00:16<00:00, 615.0it/s]
Epoch 3:
Loss: 0.009:  72%|████████████| 7219/10000  [00:11<00:04, 635.1it/s]

Что ж, изумительные 16 с / эпоху. Вы можете подумать, что он не такой быстрый, как режим Keras, но, с другой стороны, вы получаете все функции отладки и очень близкую производительность.

Выводы

Если вы читали статью, вас не удивит, что в конце концов все это сводится к очень старой проблеме программного обеспечения: гибкость или эффективность? Режим нетерпения или режим Keras? Ну зачем соглашаться? Используйте режим графика!

На мой взгляд, ребята из TensorFlow проделали отличную работу по обеспечению большей гибкости для нас, разработчиков, без слишком большого ущерба для эффективности. Итак, с моей точки зрения, я могу сказать им только браво.