Keras functional API в TensorFlow

Смотрите на TensorFlow.org Запустите в Google Colab Изучайте код на GitHub Скачайте ноутбук

Setup

from __future__ import absolute_import, division, print_function, unicode_literals

try:
  # %tensorflow_version существует только Colab.
  %tensorflow_version 2.x
except Exception:
  pass
import tensorflow as tf

tf.keras.backend.clear_session()  # Для простого сброса состояния ноутбука.

Введение

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

Он основан на том, что модель глубоко обучения обычно представляет собой ориентированный ациклический граф (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')

png

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

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

png

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

"Граф слоев" это очень интуитивный ментальный образ для модели глубокого обучения, а 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])
Epoch 1/5
750/750 [==============================] - 2s 2ms/step - loss: 0.3530 - accuracy: 0.9002 - val_loss: 0.1853 - val_accuracy: 0.9462
Epoch 2/5
750/750 [==============================] - 2s 2ms/step - loss: 0.1631 - accuracy: 0.9508 - val_loss: 0.1408 - val_accuracy: 0.9592
Epoch 3/5
750/750 [==============================] - 2s 2ms/step - loss: 0.1188 - accuracy: 0.9655 - val_loss: 0.1149 - val_accuracy: 0.9661
Epoch 4/5
750/750 [==============================] - 2s 2ms/step - loss: 0.0935 - accuracy: 0.9721 - val_loss: 0.1059 - val_accuracy: 0.9693
Epoch 5/5
750/750 [==============================] - 2s 2ms/step - loss: 0.0785 - accuracy: 0.9766 - val_loss: 0.0999 - val_accuracy: 0.9708
313/313 - 0s - loss: 0.0920 - accuracy: 0.9729
Test loss: 0.09198278188705444
Test accuracy: 0.9728999733924866

Полное руководство посвященное обучению и оценки моделей, см. по ссылке руководство обучения и оценки.

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

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

Стандартным способом сохранения Functional модели является вызов model.save() позволяющий сохранить всю модель в один файл. Позже вы можете восстановить ту же модель из этого файла, даже если у вас больше нет доступа к коду создавшему модель.

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

  • Архитектуру модели
  • Значения весов модели (которые были получены во время обучения)
  • Конфигурация обучения модели (то что вы передавали в compile)
  • Оптимизатор и его состояние, если оно было (это позволяет возобновить обучение с того места, где вы остановились)
model.save('path_to_my_model.h5')
del model
# Восстановить в точности ту же модель исключительно из файла:
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()
Model: "encoder"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
img (InputLayer)             [(None, 28, 28, 1)]       0         
_________________________________________________________________
conv2d (Conv2D)              (None, 26, 26, 16)        160       
_________________________________________________________________
conv2d_1 (Conv2D)            (None, 24, 24, 32)        4640      
_________________________________________________________________
max_pooling2d (MaxPooling2D) (None, 8, 8, 32)          0         
_________________________________________________________________
conv2d_2 (Conv2D)            (None, 6, 6, 32)          9248      
_________________________________________________________________
conv2d_3 (Conv2D)            (None, 4, 4, 16)          4624      
_________________________________________________________________
global_max_pooling2d (Global (None, 16)                0         
=================================================================
Total params: 18,672
Trainable params: 18,672
Non-trainable params: 0
_________________________________________________________________
Model: "autoencoder"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
img (InputLayer)             [(None, 28, 28, 1)]       0         
_________________________________________________________________
conv2d (Conv2D)              (None, 26, 26, 16)        160       
_________________________________________________________________
conv2d_1 (Conv2D)            (None, 24, 24, 32)        4640      
_________________________________________________________________
max_pooling2d (MaxPooling2D) (None, 8, 8, 32)          0         
_________________________________________________________________
conv2d_2 (Conv2D)            (None, 6, 6, 32)          9248      
_________________________________________________________________
conv2d_3 (Conv2D)            (None, 4, 4, 16)          4624      
_________________________________________________________________
global_max_pooling2d (Global (None, 16)                0         
_________________________________________________________________
reshape (Reshape)            (None, 4, 4, 1)           0         
_________________________________________________________________
conv2d_transpose (Conv2DTran (None, 6, 6, 16)          160       
_________________________________________________________________
conv2d_transpose_1 (Conv2DTr (None, 8, 8, 32)          4640      
_________________________________________________________________
up_sampling2d (UpSampling2D) (None, 24, 24, 32)        0         
_________________________________________________________________
conv2d_transpose_2 (Conv2DTr (None, 26, 26, 16)        4624      
_________________________________________________________________
conv2d_transpose_3 (Conv2DTr (None, 28, 28, 1)         145       
=================================================================
Total params: 28,241
Trainable params: 28,241
Non-trainable params: 0
_________________________________________________________________

Обратите внимание, что мы делаем архитектуру декодирования строго симметричной архитектуре кодирования, так что мы получим размерность выходных данных такую же как и входных данных (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()
Model: "encoder"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
original_img (InputLayer)    [(None, 28, 28, 1)]       0         
_________________________________________________________________
conv2d_4 (Conv2D)            (None, 26, 26, 16)        160       
_________________________________________________________________
conv2d_5 (Conv2D)            (None, 24, 24, 32)        4640      
_________________________________________________________________
max_pooling2d_1 (MaxPooling2 (None, 8, 8, 32)          0         
_________________________________________________________________
conv2d_6 (Conv2D)            (None, 6, 6, 32)          9248      
_________________________________________________________________
conv2d_7 (Conv2D)            (None, 4, 4, 16)          4624      
_________________________________________________________________
global_max_pooling2d_1 (Glob (None, 16)                0         
=================================================================
Total params: 18,672
Trainable params: 18,672
Non-trainable params: 0
_________________________________________________________________
Model: "decoder"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
encoded_img (InputLayer)     [(None, 16)]              0         
_________________________________________________________________
reshape_1 (Reshape)          (None, 4, 4, 1)           0         
_________________________________________________________________
conv2d_transpose_4 (Conv2DTr (None, 6, 6, 16)          160       
_________________________________________________________________
conv2d_transpose_5 (Conv2DTr (None, 8, 8, 32)          4640      
_________________________________________________________________
up_sampling2d_1 (UpSampling2 (None, 24, 24, 32)        0         
_________________________________________________________________
conv2d_transpose_6 (Conv2DTr (None, 26, 26, 16)        4624      
_________________________________________________________________
conv2d_transpose_7 (Conv2DTr (None, 28, 28, 1)         145       
=================================================================
Total params: 9,569
Trainable params: 9,569
Non-trainable params: 0
_________________________________________________________________
Model: "autoencoder"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
img (InputLayer)             [(None, 28, 28, 1)]       0         
_________________________________________________________________
encoder (Functional)         (None, 16)                18672     
_________________________________________________________________
decoder (Functional)         (None, 28, 28, 1)         9569      
=================================================================
Total params: 28,241
Trainable params: 28,241
Non-trainable params: 0
_________________________________________________________________

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

Распространенным вариантом использования вложения моделей является 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)

png

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

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

# Учебные входные данные
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')
# Учебные целевые данные
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)
Epoch 1/2
40/40 [==============================] - 0s 11ms/step - loss: 1.2818 - priority_loss: 0.7073 - department_loss: 2.8726
Epoch 2/2
40/40 [==============================] - 0s 10ms/step - loss: 1.2716 - priority_loss: 0.6995 - department_loss: 2.8603

<tensorflow.python.keras.callbacks.History at 0x7f1df31b0630>

При вызове 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()
Model: "toy_resnet"
__________________________________________________________________________________________________
Layer (type)                    Output Shape         Param #     Connected to                     
==================================================================================================
img (InputLayer)                [(None, 32, 32, 3)]  0                                            
__________________________________________________________________________________________________
conv2d_8 (Conv2D)               (None, 30, 30, 32)   896         img[0][0]                        
__________________________________________________________________________________________________
conv2d_9 (Conv2D)               (None, 28, 28, 64)   18496       conv2d_8[0][0]                   
__________________________________________________________________________________________________
max_pooling2d_2 (MaxPooling2D)  (None, 9, 9, 64)     0           conv2d_9[0][0]                   
__________________________________________________________________________________________________
conv2d_10 (Conv2D)              (None, 9, 9, 64)     36928       max_pooling2d_2[0][0]            
__________________________________________________________________________________________________
conv2d_11 (Conv2D)              (None, 9, 9, 64)     36928       conv2d_10[0][0]                  
__________________________________________________________________________________________________
add (Add)                       (None, 9, 9, 64)     0           conv2d_11[0][0]                  
                                                                 max_pooling2d_2[0][0]            
__________________________________________________________________________________________________
conv2d_12 (Conv2D)              (None, 9, 9, 64)     36928       add[0][0]                        
__________________________________________________________________________________________________
conv2d_13 (Conv2D)              (None, 9, 9, 64)     36928       conv2d_12[0][0]                  
__________________________________________________________________________________________________
add_1 (Add)                     (None, 9, 9, 64)     0           conv2d_13[0][0]                  
                                                                 add[0][0]                        
__________________________________________________________________________________________________
conv2d_14 (Conv2D)              (None, 7, 7, 64)     36928       add_1[0][0]                      
__________________________________________________________________________________________________
global_average_pooling2d (Globa (None, 64)           0           conv2d_14[0][0]                  
__________________________________________________________________________________________________
dense_9 (Dense)                 (None, 256)          16640       global_average_pooling2d[0][0]   
__________________________________________________________________________________________________
dropout (Dropout)               (None, 256)          0           dense_9[0][0]                    
__________________________________________________________________________________________________
dense_10 (Dense)                (None, 10)           2570        dropout[0][0]                    
==================================================================================================
Total params: 223,242
Trainable params: 223,242
Non-trainable params: 0
__________________________________________________________________________________________________

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

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

png

Давайте обучим ее:

(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)
Downloading data from https://www.cs.toronto.edu/~kriz/cifar-10-python.tar.gz
170500096/170498071 [==============================] - 11s 0us/step
625/625 [==============================] - 3s 6ms/step - loss: 1.9341 - acc: 0.2653 - val_loss: 1.6056 - val_acc: 0.3951

<tensorflow.python.keras.callbacks.History at 0x7f1df29e25f8>

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

Другим хорошим использованием 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()
Downloading data from https://storage.googleapis.com/tensorflow/keras-applications/vgg19/vgg19_weights_tf_dim_ordering_tf_kernels.h5
574717952/574710816 [==============================] - 6s 0us/step

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

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.

Однако, сабклассирование 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 модель это скорее структура данных чем кусок кода, она безопасно сериализуется и может быть сохранена в виде одного файла который позволяет вам воссоздать в точности ту же модель без доступа к исходному коду. Смотрите наше руководство по сохранению и сериализации для деталей.

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

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

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

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

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


Чтобы глубже погрузиться в разницу между Functional API и сабклассированием Model, вы можете прочитать Что такое Symbolic и Imperative API в TensorFlow 2.0?.

Сочетание различных стилей 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)))
(1, 10, 32)

Обратно, вы можете использовать любой сабклассированный 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!

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