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

Введение


Functional API позволяет создавать модели более гибко чем Sequential API, он может обрабатывать модели с нелинейной топологией, модели с общими слоями, и модели с несколькими входами или выходами.

Он основан на том, что модель глубоко обучения обычно представляет собой ориентированный ациклический граф (DAG) слоев

Functional API — это набор инструментов для построения графа слоев.

Рассмотрим следующую модель:

(вход: 784-мерный вектор)
?
[Плотный слой (64 элемента, активация relu)]
?
[Плотный слой (64 элемента, активация relu)]
?
[Плотный слой (10 элементов, активация softmax)]
?
(выход: вероятностное распределение на 10 классов)
Это простой граф из 3 слоев.

Для построения этой модели с помощью Functional API, вам надо начать с создания входного узла:

from tensorflow import keras

inputs = keras.Input(shape=(784,))

Здесь мы просто указываем размерность наших данных: 784-мерных векторов. Обратите внимание, что количество данных всегда опускается, мы указываем только размерность каждого элемента. Для ввода предназначенного для изображений размеров `(32, 32, 3)`, мы бы использовали:

img_inputs = keras.Input(shape=(32, 32, 3))

То, что возвращает inputs, содержит информацию о размерах и типе данных которые вы планируете передать в вашу модель:

inputs.shape

TensorShape([None, 784])

inputs.dtype

tf.float32

Вы создаете новый узел в графе слоев, вызывая слой на этом объекте inputs:

from tensorflow.keras import layers

dense = layers.Dense(64, activation='relu')
x = dense(inputs)

«Вызов слоя» аналогичен рисованию стрелки из «входных данных» в созданный нами слой. Мы «передаем» входные данные в dense слой, и мы получаем x.

Давайте добавим еще несколько слоев в наш граф слоев:

x = layers.Dense(64, activation='relu')(x)
outputs = layers.Dense(10, activation='softmax')(x)

Сейчас мы можем создать Model указав его входы и выходы в графе слоев:

model = keras.Model(inputs=inputs, outputs=outputs)

Посмотрим еще раз полный процесс определения модели:

inputs = keras.Input(shape=(784,), name='img')
x = layers.Dense(64, activation='relu')(inputs)
x = layers.Dense(64, activation='relu')(x)
outputs = layers.Dense(10, activation='softmax')(x)

model = keras.Model(inputs=inputs, outputs=outputs, name='mnist_model')

Давайте посмотрим как выглядит сводка модели:

model.summary()

Model: "mnist_model"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
img (InputLayer)             [(None, 784)]             0         
_________________________________________________________________
dense_3 (Dense)              (None, 64)                50240     
_________________________________________________________________
dense_4 (Dense)              (None, 64)                4160      
_________________________________________________________________
dense_5 (Dense)              (None, 10)                650       
=================================================================
Total params: 55,050
Trainable params: 55,050
Non-trainable params: 0
_________________________________________________________________

Мы также можем начертить модель в виде графа:

keras.utils.plot_model(model, 'my_first_model.png')

image

И опционально выведем размерности входа и выхода каждого слоя на построенном графе:

keras.utils.plot_model(model, 'my_first_model_with_shape_info.png', show_shapes=True)

image

Это изображение и код который мы написали идентичны. В версии кода, связывающие стрелки просто заменены операциями вызова.

«Граф слоев» это очень интуитивный ментальный образ для модели глубокого обучения, а Functional API это способ создания моделей которые близко отражают этот ментальный образ.

Обучение, оценка и вывод


Обучение, оценка и вывод работают для моделей построенных с использованием Functional API точно так же как и в Sequential моделях.

Рассмотрим быструю демонстрацию.

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

(x_train, y_train), (x_test, y_test) = keras.datasets.mnist.load_data()
x_train = x_train.reshape(60000, 784).astype('float32') / 255
x_test = x_test.reshape(10000, 784).astype('float32') / 255

model.compile(loss='sparse_categorical_crossentropy',
              optimizer=keras.optimizers.RMSprop(),
              metrics=['accuracy'])
history = model.fit(x_train, y_train,
                    batch_size=64,
                    epochs=5,
                    validation_split=0.2)
test_scores = model.evaluate(x_test, y_test, verbose=2)
print('Test loss:', test_scores[0])
print('Test accuracy:', test_scores[1]) 

Сохранение и сериализация


Сохранение и сериализация для моделей построенных с использованием Functional API работает точно так же как и для Sequential моделей.

Стандартным способом сохранения Functional модели является вызов model.save() позволяющий сохранить всю модель в один файл.

Позже вы можете восстановить ту же модель из этого файла, даже если у вас больше нет доступа к коду создавшему модель.

Этот файл включает:

  • Архитектуру модели
  • Значения весов модели (которые были получены во время обучения)
  • Конфигурация обучения модели (то что вы передавали в compile)
  • Оптимизатор и его состояние, если оно было (это позволяет возобновить обучение с того места, где вы остановились)


model.save('path_to_my_model.h5')
del model
# Recreate the exact same model purely from the file:
model = keras.models.load_model('path_to_my_model.h5')

Использование одного и того же графа слоев для определения нескольких моделей


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

В приведенном ниже примере мы используем один и тот же стек слоев для создания двух моделей:
модель кодировщика (encoder) которая преобразует входные изображения в 16-мерные вектора, и сквозную модель автокодировщика (autoencoder) для обучения.

encoder_input = keras.Input(shape=(28, 28, 1), name='img')
x = layers.Conv2D(16, 3, activation='relu')(encoder_input)
x = layers.Conv2D(32, 3, activation='relu')(x)
x = layers.MaxPooling2D(3)(x)
x = layers.Conv2D(32, 3, activation='relu')(x)
x = layers.Conv2D(16, 3, activation='relu')(x)
encoder_output = layers.GlobalMaxPooling2D()(x)

encoder = keras.Model(encoder_input, encoder_output, name='encoder')
encoder.summary()

x = layers.Reshape((4, 4, 1))(encoder_output)
x = layers.Conv2DTranspose(16, 3, activation='relu')(x)
x = layers.Conv2DTranspose(32, 3, activation='relu')(x)
x = layers.UpSampling2D(3)(x)
x = layers.Conv2DTranspose(16, 3, activation='relu')(x)
decoder_output = layers.Conv2DTranspose(1, 3, activation='relu')(x)

autoencoder = keras.Model(encoder_input, decoder_output, name='autoencoder')
autoencoder.summary()

Обратите внимание, что мы делаем архитектуру декодирования строго симметричной архитектуре кодирования, так что мы получим размерность выходных данных такую же как и входных данных (28, 28, 1). Обратным к слою Conv2D является слой Conv2DTranspose, а обратным к слою MaxPooling2D будет слой UpSampling2D.

Модели можно вызывать как слои


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

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

encoder_input = keras.Input(shape=(28, 28, 1), name='original_img')
x = layers.Conv2D(16, 3, activation='relu')(encoder_input)
x = layers.Conv2D(32, 3, activation='relu')(x)
x = layers.MaxPooling2D(3)(x)
x = layers.Conv2D(32, 3, activation='relu')(x)
x = layers.Conv2D(16, 3, activation='relu')(x)
encoder_output = layers.GlobalMaxPooling2D()(x)

encoder = keras.Model(encoder_input, encoder_output, name='encoder')
encoder.summary()

decoder_input = keras.Input(shape=(16,), name='encoded_img')
x = layers.Reshape((4, 4, 1))(decoder_input)
x = layers.Conv2DTranspose(16, 3, activation='relu')(x)
x = layers.Conv2DTranspose(32, 3, activation='relu')(x)
x = layers.UpSampling2D(3)(x)
x = layers.Conv2DTranspose(16, 3, activation='relu')(x)
decoder_output = layers.Conv2DTranspose(1, 3, activation='relu')(x)

decoder = keras.Model(decoder_input, decoder_output, name='decoder')
decoder.summary()

autoencoder_input = keras.Input(shape=(28, 28, 1), name='img')
encoded_img = encoder(autoencoder_input)
decoded_img = decoder(encoded_img)
autoencoder = keras.Model(autoencoder_input, decoded_img, name='autoencoder')
autoencoder.summary()

Как вы видите, модель может быть вложена: модель может содержать подмодель (поскольку модель можно рассматривать как слой).

Распространенным вариантом использования вложения моделей является ensembling.

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

def get_model():
  inputs = keras.Input(shape=(128,))
  outputs = layers.Dense(1, activation='sigmoid')(inputs)
  return keras.Model(inputs, outputs)

model1 = get_model()
model2 = get_model()
model3 = get_model()

inputs = keras.Input(shape=(128,))
y1 = model1(inputs)
y2 = model2(inputs)
y3 = model3(inputs)
outputs = layers.average([y1, y2, y3])
ensemble_model = keras.Model(inputs=inputs, outputs=outputs)


Манипулирование сложными топологиями графов


Модели с несколькими входами и выходами


Functional API упрощает манипуляции с несколькими входами и выходами. Это не может быть сделано с Sequential API.

Вот простой пример.

Допустим, вы создаете систему для ранжирования клиентских заявок по приоритетам и направления их в нужный отдел.

У вашей модели будет 3 входа:

  • Заголовок заявки (текстовые входные данные)
  • Текстовое содержание заявки (текстовые входные данные)
  • Любые теги добавленные пользователем (категорийные входные данные)

У модели будет 2 выхода:

  • Оценка приоритета между 0 и 1 (скалярный сигмоидный выход)
  • Отдел который должен обработать заявку (softmax выход относительно множества отделов)

Давайте построим модель в несколько строк с помощью Functional API.

num_tags = 12  # Количество различных тегов проблем
num_words = 10000  # Размер словаря полученный в результате предобработки текстовых данных
num_departments = 4  # Количество отделов для предсказаний

title_input = keras.Input(shape=(None,), name='title')  # Последовательность целых чисел переменной длины
body_input = keras.Input(shape=(None,), name='body')  # Последовательность целых чисел переменной длины
tags_input = keras.Input(shape=(num_tags,), name='tags')  # Бинарный вектор размера `num_tags`

# Вложим каждое слово заголовка в 64-мерный вектор
title_features = layers.Embedding(num_words, 64)(title_input)
# Вложим каждое слово текста в 64-мерный вектор
body_features = layers.Embedding(num_words, 64)(body_input)

# Сокращаем последовательность вложенных слов заголовка до одного 128-мерного вектора
title_features = layers.LSTM(128)(title_features)
# Сокращаем последовательность вложенных слов заголовка до одного 32-мерного вектора
body_features = layers.LSTM(32)(body_features)

# Объединим все признаки в один вектор с помощью конкатенации
x = layers.concatenate([title_features, body_features, tags_input])

# Добавим логистическую регрессию для прогнозирования приоритета по признакам
priority_pred = layers.Dense(1, activation='sigmoid', name='priority')(x)
# Добавим классификатор отделов прогнозирующий на признаках
department_pred = layers.Dense(num_departments, activation='softmax', name='department')(x)

# Создание сквозной модели, предсказывающей приоритет и отдел
model = keras.Model(inputs=[title_input, body_input, tags_input],
                    outputs=[priority_pred, department_pred])

Давайте начертим граф модели:

keras.utils.plot_model(model, 'multi_input_and_output_model.png', show_shapes=True)



При компиляции этой модели, мы можем присвоить различные функции потерь каждому выходу.

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

model.compile(optimizer=keras.optimizers.RMSprop(1e-3),
              loss=['binary_crossentropy', 'categorical_crossentropy'],
              loss_weights=[1., 0.2])

Так как мы дали имена нашим выходным слоям, мы можем также указать функции потерь:

model.compile(optimizer=keras.optimizers.RMSprop(1e-3),
              loss={'priority': 'binary_crossentropy',
                    'department': 'categorical_crossentropy'},
              loss_weights=[1., 0.2])

Мы можем обучить модель передавая списки массивов Numpy входных данных и меток:

import numpy as np

# Dummy input data
title_data = np.random.randint(num_words, size=(1280, 10))
body_data = np.random.randint(num_words, size=(1280, 100))
tags_data = np.random.randint(2, size=(1280, num_tags)).astype('float32')
# Dummy target data
priority_targets = np.random.random(size=(1280, 1))
dept_targets = np.random.randint(2, size=(1280, num_departments))

model.fit({'title': title_data, 'body': body_data, 'tags': tags_data},
          {'priority': priority_targets, 'department': dept_targets},
          epochs=2,
          batch_size=32)

При вызове fit с объектом Dataset, должны возвращаться либо кортеж списков, таких как ([title_data, body_data, tags_data], [priority_targets, dept_targets]), либо кортеж словарей ({'title': title_data, 'body': body_data, 'tags': tags_data}, {'priority': priority_targets, 'department': dept_targets}).

Учебная resnet модель


В дополнение к моделям с несколькими входами и выходами, Functional API упрощает манипулирование топологиями с нелинейной связностью, то есть моделями, в которых слои не связаны последовательно. Такие модели также не может быть реализованы с помощью Sequential API (это видно из названия).

Распространенный пример использования этого — residual connections.

Давайте построим учебную ResNet модель для CIFAR10 чтобы продемонстрировать это.

inputs = keras.Input(shape=(32, 32, 3), name='img')
x = layers.Conv2D(32, 3, activation='relu')(inputs)
x = layers.Conv2D(64, 3, activation='relu')(x)
block_1_output = layers.MaxPooling2D(3)(x)

x = layers.Conv2D(64, 3, activation='relu', padding='same')(block_1_output)
x = layers.Conv2D(64, 3, activation='relu', padding='same')(x)
block_2_output = layers.add([x, block_1_output])

x = layers.Conv2D(64, 3, activation='relu', padding='same')(block_2_output)
x = layers.Conv2D(64, 3, activation='relu', padding='same')(x)
block_3_output = layers.add([x, block_2_output])

x = layers.Conv2D(64, 3, activation='relu')(block_3_output)
x = layers.GlobalAveragePooling2D()(x)
x = layers.Dense(256, activation='relu')(x)
x = layers.Dropout(0.5)(x)
outputs = layers.Dense(10, activation='softmax')(x)

model = keras.Model(inputs, outputs, name='toy_resnet')
model.summary()

Давайте начертим граф модели:

keras.utils.plot_model(model, 'mini_resnet.png', show_shapes=True)



И обучим ее:

(x_train, y_train), (x_test, y_test) = keras.datasets.cifar10.load_data()
x_train = x_train.astype('float32') / 255.
x_test = x_test.astype('float32') / 255.
y_train = keras.utils.to_categorical(y_train, 10)
y_test = keras.utils.to_categorical(y_test, 10)

model.compile(optimizer=keras.optimizers.RMSprop(1e-3),
              loss='categorical_crossentropy',
              metrics=['acc'])
model.fit(x_train, y_train,
          batch_size=64,
          epochs=1,
          validation_split=0.2)

Совместное использование слоев


Другим хорошим использованием Functional API являются модели, использующие общие слои. Общие слои — это экземпляры слоев, которые переиспользуются в одной и той же модели: они изучают признаки, которые относятся к нескольким путям в графе слоев.

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

Чтобы совместно использовать слой в Functional API, просто вызовите тот же экземпляр слоя несколько раз. Например, здесь слой Embedding используется совместно на двух текстовых входах:

# Вложения для 1000 различных слов в 128-мерные вектора
shared_embedding = layers.Embedding(1000, 128)

# Целочисленные последовательности переменной длины
text_input_a = keras.Input(shape=(None,), dtype='int32')

# Целочисленные последовательности переменной длины
text_input_b = keras.Input(shape=(None,), dtype='int32')

# Мы переиспользуем тот же слой для кодирования на обоих входах
encoded_input_a = shared_embedding(text_input_a)
encoded_input_b = shared_embedding(text_input_b)

Извлечение и повторное использование узлов в графе слоев


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

Это также означает, что мы можем получить доступ к активациям промежуточных слоев («узлов» в графе) и использовать их в других местах. Это чрезвычайно полезно для извлечения признаков, например!

Давайте посмотрим пример. Это модель VGG19 с весами предобученными на ImageNet:

from tensorflow.keras.applications import VGG19

vgg19 = VGG19()

И это промежуточные активации модели, полученные путем запроса к структуре данных графа:

features_list = [layer.output for layer in vgg19.layers]

Мы можем использовать эти признаки для создания новой модели извлечения признаков, которая возвращает значения активаций промежуточного уровня — и мы можем сделать все это в 3 строчки

feat_extraction_model = keras.Model(inputs=vgg19.input, outputs=features_list)

img = np.random.random((1, 224, 224, 3)).astype('float32')
extracted_features = feat_extraction_model(img)

Это удобно при реализации neural style transfer, как и в других случаях.

Расширение API при помощи написания кастомных слоев


tf.keras обладает широким набором встроенных слоев. Вот несколько примеров:

Сверточные слои: Conv1D, Conv2D, Conv3D, Conv2DTranspose, и т.д.
Слои пулинга: MaxPooling1D, MaxPooling2D, MaxPooling3D, AveragePooling1D, и т.д.
Слои RNN: GRU, LSTM, ConvLSTM2D, и т.д.
BatchNormalization, Dropout, Embedding, и т.д.

Если вы не нашли то, что вам нужно, легко расширить API создав собственный слой.

Все слои сабклассируют класс Layer и реализуют:

Метод call, определяющий вычисления выполняемые слоем.
Метод build, создающий веса слоя (заметим что это всего лишь стилевое соглашение; вы можете также создать веса в __init__).

Вот простая реализация Dense слоя:

class CustomDense(layers.Layer):

  def __init__(self, units=32):
    super(CustomDense, self).__init__()
    self.units = units

  def build(self, input_shape):
    self.w = self.add_weight(shape=(input_shape[-1], self.units),
                             initializer='random_normal',
                             trainable=True)
    self.b = self.add_weight(shape=(self.units,),
                             initializer='random_normal',
                             trainable=True)

  def call(self, inputs):
    return tf.matmul(inputs, self.w) + self.b

inputs = keras.Input((4,))
outputs = CustomDense(10)(inputs)

model = keras.Model(inputs, outputs)

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

class CustomDense(layers.Layer):

  def __init__(self, units=32):
    super(CustomDense, self).__init__()
    self.units = units

  def build(self, input_shape):
    self.w = self.add_weight(shape=(input_shape[-1], self.units),
                             initializer='random_normal',
                             trainable=True)
    self.b = self.add_weight(shape=(self.units,),
                             initializer='random_normal',
                             trainable=True)

  def call(self, inputs):
    return tf.matmul(inputs, self.w) + self.b

  def get_config(self):
    return {'units': self.units}


inputs = keras.Input((4,))
outputs = CustomDense(10)(inputs)

model = keras.Model(inputs, outputs)
config = model.get_config()

new_model = keras.Model.from_config(
    config, custom_objects={'CustomDense': CustomDense})

Опционально, вы также можете реализовать метод класса from_config (cls, config), который отвечает за пересоздание экземпляра слоя, учитывая его словарь конфигурации. Реализация по умолчанию from_config выглядит так:

def from_config(cls, config):
  return cls(**config)

Когда использовать Functional API


Как определить когда лучше использовать Functional API для создания новой модели, или просто сабклассировать Model напрямую?

В целом, Functional API более высокоуровневый и простой в использовании, он имеет ряд функций, которые не поддерживаются сабклассированными моделями.

Однако, сабклассирование Model дает вам большую гибкость при создании моделей, которые не описываются легко в виде направленного ациклического графа слоев (например, вы не сможете реализовать Tree-RNN с Functional API, вам нужно сабклассировать напрямую Model).

Cильные стороны Functional API:


Свойства перечисленные ниже являются все верными и для Sequential моделей (которые также являются структурами данных), но они верны для сабклассированных моделей (которые представляют собой код Python, а не структуры данных).

С Functional API получается более короткий код.


Нет super(MyClass, self).__init__(...), нет def call(self, ...):, и т.д.

Сравните:

inputs = keras.Input(shape=(32,))
x = layers.Dense(64, activation='relu')(inputs)
outputs = layers.Dense(10)(x)
mlp = keras.Model(inputs, outputs)

С сабклассированной версией:

class MLP(keras.Model):

  def __init__(self, **kwargs):
    super(MLP, self).__init__(**kwargs)
    self.dense_1 = layers.Dense(64, activation='relu')
    self.dense_2 = layers.Dense(10)

  def call(self, inputs):
    x = self.dense_1(inputs)
    return self.dense_2(x)

# Создадим экземпляр модели.
mlp = MLP()
# Необходимо создать состояние модели.
# У модели нет состояния пока она не была вызвана хотя бы раз.
_ = mlp(tf.zeros((1, 32))) 

Ваша модель валидируется по ходу ее написания


В Functional API входные спецификации (shape и dtype) создаются заранее (через `Input`), и каждый раз, когда вы вызываете слой, слой проверяет, что спецификации переданные ему соответствует его предположениям, если это не так то вы получите полезное сообщение об ошибке.

Это гарантирует, что любая модель которую вы построите с Functional API запустится. Вся отладка (не относящаяся к отладке сходимости) будет происходить статично во время конструирования модели, а не во время выполнения. Это аналогично проверке типа в компиляторе.

Вашу Functional модель можно представить графически, а также она проверяема.


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

features_list = [layer.output for layer in vgg19.layers]
feat_extraction_model = keras.Model(inputs=vgg19.input, outputs=features_list)

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

Слабые стороны Functional API


Он не поддерживает динамичные архитектуры.


Functional API обрабатывает модели как DAG слоев. Это справедливо для большинства архитектур глубокого обучения, но не для всех: например, рекурсивные сети или Tree RNN не соответствуют этому предположению и не могут быть реализованы в Functional API.

Иногда вам просто нужно написать все с нуля.


При написании продвинутых архитектур вы можете захотеть сделать то, что выходит за рамки «определения DAG слоев»: например, вы можете использовать несколько пользовательских методов обучения и вывода на экземпляре вашей модели. Это требует сабклассирования.

Сочетание и комбинирование различных стилей API


Важно отметить, что выбор между Functional API или сабклассированием Model не является бинарным решением, которое ограничивает вас одной категорией моделей. Все модели в API tf.keras могут взаимодействовать друг с другом, будь то Sequential модели, Functional модели или сабклассированные Models/Layers, написанные с нуля.

Вы всегда можете использовать Functional модель или Sequential модель как часть сабклассированного Model/Layer:

units = 32
timesteps = 10
input_dim = 5

# Define a Functional model
inputs = keras.Input((None, units))
x = layers.GlobalAveragePooling1D()(inputs)
outputs = layers.Dense(1, activation='sigmoid')(x)
model = keras.Model(inputs, outputs)


class CustomRNN(layers.Layer):

  def __init__(self):
    super(CustomRNN, self).__init__()
    self.units = units
    self.projection_1 = layers.Dense(units=units, activation='tanh')
    self.projection_2 = layers.Dense(units=units, activation='tanh')
    # Our previously-defined Functional model
    self.classifier = model

  def call(self, inputs):
    outputs = []
    state = tf.zeros(shape=(inputs.shape[0], self.units))
    for t in range(inputs.shape[1]):
      x = inputs[:, t, :]
      h = self.projection_1(x)
      y = h + self.projection_2(state)
      state = y
      outputs.append(y)
    features = tf.stack(outputs, axis=1)
    print(features.shape)
    return self.classifier(features)

rnn_model = CustomRNN()
_ = rnn_model(tf.zeros((1, timesteps, input_dim)))
 

Обратно, вы можете использовать любой сабклассированный Layer или Model в Functional API в том случае если реализован метод call который соответствует одному из следующих паттернов:

call(self, inputs, **kwargs) где inputs это тензор или вложенная струтура тензоров (напр. список тензоров), и где **kwargs это нетензорные аргументы (не входные данные).
call(self, inputs, training=None, **kwargs) где training это булево значение показывающее в каком режиме должен вести себя слой, обучения или вывода.
call(self, inputs, mask=None, **kwargs) где mask это тензор булевой маски (полезно для RNN, например).
call(self, inputs, training=None, mask=None, **kwargs) — конечно вы можете иметь одновременно оба параметра определяющих поведение слоя.

В дополнение, если вы реализуете метод `get_config` на вашем пользовательском Layer или Model, Functional модели которые вы создадите с ним будут сериализуемы и клонируемы.

Далее приведем небольшой пример где мы используем кастомный RNN написанный с нуля Functional модели:

units = 32
timesteps = 10
input_dim = 5
batch_size = 16


class CustomRNN(layers.Layer):

  def __init__(self):
    super(CustomRNN, self).__init__()
    self.units = units
    self.projection_1 = layers.Dense(units=units, activation='tanh')
    self.projection_2 = layers.Dense(units=units, activation='tanh')
    self.classifier = layers.Dense(1, activation='sigmoid')

  def call(self, inputs):
    outputs = []
    state = tf.zeros(shape=(inputs.shape[0], self.units))
    for t in range(inputs.shape[1]):
      x = inputs[:, t, :]
      h = self.projection_1(x)
      y = h + self.projection_2(state)
      state = y
      outputs.append(y)
    features = tf.stack(outputs, axis=1)
    return self.classifier(features)

# Заметьте что мы задаем статичный размер пакета для входных данных
# аргументом `batch_shape`, потому что внутренние вычисления `CustomRNN` требуют 
# фиксированного размера пакета (когда мы создает нулевые тензоры `state`).
inputs = keras.Input(batch_shape=(batch_size, timesteps, input_dim))
x = layers.Conv1D(32, 3)(inputs)
outputs = CustomRNN()(x)

model = keras.Model(inputs, outputs)

rnn_model = CustomRNN()
_ = rnn_model(tf.zeros((1, 10, 5)))

Это завершает наше руководство по Functional API!

Теперь у вас под рукой мощный набор инструментов для построения моделей глубокого обучения.

После проверки перевод появится также на сайте Tensorflow.org. Если вы хотите поучаствовать в переводе документации сайта Tensorflow.org на русский, обращайтесь в личку или комментарии. Любые исправления и замечания приветствуются. В качестве иллюстрации использовалось изображение модели GoogLeNet, которая тоже является направленным ациклическим графом.