Функциональный API библиотеки Keras

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

Подпишись на группу Вконтакте и Телеграм-канал. Там еще больше полезного контента для программистов. А на  YouTube-канале ты найдешь обучающие видео по программированию. Подписывайся!

Что такое функциональный API?

Весь функциональный API строится на конструкции:

layer = Layer(*args)(previos_layer) ,

которая является аналогом конструкции:

layer = Layer(*args).__call__(previous_layer)

Текущий слой layer привязывается с previous_layer, и такой способ построения архитектуры открывает нам новые возможности.

Импорт

Для сегодняшнего «обзора» нам понадобятся следующие классы и функции:

In[1]:
from tensorflow.keras.layers import *
from tensorflow.keras.models import Model, Sequential

from tensorflow.keras.utils import plot_model

from IPython.display import display, Image

Предназначение всех имён я объясню, как говорится, по ходу дела.

Также напишем преамбулу:

In[2]:
# Функция для визуализации архитектуры сети

i = 0 # Счётчик для кол-ва изображенных моделей

def plot(model):
  global i
   
  # Загрузка изображения в файл
  plot_model(model, to_file='model_{0}.png'.format(i))
   
  image = Image('model_{0}.png'.format(i))
   
  i += 1 # Инкремент счётчика
   
  # Вывод изображения
  display(image)

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

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

Всё познаётся в сравнении

Сравним процесс создания последовательной модели с помощью класса Sequential.

In[3]:
# Последовательная модель с использованием
# конструктора класса Sequential

input_layer = Dense(10, input_shape=(1000, ), activation='relu')

hidden_1 = Dense(10, activation='relu')
hidden_2 = Dense(15, activation='relu')

output_layer = Dense(1, activation='sigmoid')

model = Sequential([input_layer, hidden_1, hidden_2, output_layer])

plot(model)

Мы использовали конструктор класса Sequential, передав в него список слоёв. Также мы можем воспользоваться методом .add(layer) для построения сети:

In[4]:
# Построение модели с помощью метода .add()

input_layer = Dense(10, input_shape=(1000, ), activation='relu')

hidden_1 = Dense(10, activation='relu')
hidden_2 = Dense(15, activation='relu')

output_layer = Dense(1, activation='sigmoid')

model = Sequential()
model.add(input_layer)
model.add(hidden_1)
model.add(hidden_2)
model.add(output_layer)

plot(model)

А что если нам необходимо, чтобы модель имела несколько входов или выходов? Нам на помощь приходит функциональный программный интерфейс!

Последовательная модель

Для примера создадим ту же самую модель, только используя приведенную ранее конструкцию.

In[5]:
# Последовательная модель с использованием 
# функционального API

input_layer = Input(shape=(1000, ))

# Аналог layer.__call__(previous_layer)
hidden_1 = Dense(16, activation='relu')(input_layer) 
hidden_2 = Dense(16, activation='relu')(hidden_1)

output_layer = Dense(1, activation='sigmoid')(hidden_2)

# Создание модели на основе входных и выходных слоёв
model = Model(inputs=input_layer, outputs=output_layer)

plot(model)

Да, теперь мы должны явно обозначить «вход» модели, который должен является экземпляром класса Input(), который в качестве параметра принимает аргумент shape=(n, m, ...), который обозначает форму входных данных.

Также мы используем класс Model, куда передаём входные и выходные слои. Если вход или выход представляет собой один слой, то просто передаём его в качестве именованного параметра, иначе передаём список слоёв.

Результат:

Несколько входов!

Теперь создадим модель с двумя входами: одним для изображения, а второй для одномерного массива.

In[6]:
# Модель с двумя input'ами

input_1 = Input(shape=(28, 28, 3))
input_2 = Input(shape=(1000, ))

# Левая ветка
conv_1 = Conv2D(32, kernel_size=4, activation='relu')(input_1)
max_pool_1 = MaxPooling2D(pool_size=(4, 4))(conv_1)
flatten = Flatten()(max_pool_1)
dense_0 = Dense(16, activation='relu')(flatten)

# Правая ветка
dense_1 = Dense(32, activation='relu')(input_2)
dense_2 = Dense(16, activation='relu')(dense_1)

# Соединение
concatenate = Concatenate(axis=1)([flatten, dense_2])

# Выход
output = Dense(1, activation='sigmoid')(concatenate)

model = Model(inputs=[input_1, input_2], outputs=output)

plot(model)

Мы объявляем два Input-слоя, и начинаем построение левой и правой ветки от каждого. Чтобы связать их, мы используем класс Concatenate. И в качестве параметра inputs= передаём список входных слоёв.

Каждому — по выходу

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

In[7]:
# Модель с одним входом и двумя выходами

input_layer = Input(shape=(1000, ))

dense_1 = Dense(32, activation='relu')(input_layer)

# Левая ветка
dense_1_1 = Dense(16, activation='relu')(dense_1)
dropout_1 = Dropout(0.4)(dense_1_1)
output_1 = Dense(1, activation='sigmoid')(dropout_1)

# Правая ветка
dense_2_1 = Dense(16, activation='relu')(dense_1)
dropout_2 = Dropout(0.2)(dense_2_1)
output_2 = Dense(1, activation='sigmoid')(dropout_2)

# Создание модели
model = Model(inputs=input_layer, outputs=[output_1, output_2])

plot(model)

Разветвление модели происходит после слоя под названием dense_1. Как и в прошлом фрагменте кода, но теперь в качестве outputs= мы передаём список слоёв.

Даём каждому слою своё имя

Вам тоже не нравится, как Keras называет слои? Это не проблема. Конструкторы всех классов, которые являются наследниками класса Layer, имеют параметр name=, в который мы можем передать строку (есть некие ограничения, о них речь пойдёт далее).

In[8]:
# Создание модели с двумя входами и двумя выходами
# с именованными слоями

input_layer_1 = Input(shape=(1000, ), name='right_input')
input_layer_2 = Input(shape=(28, 28, 3), name='left_input')

# Правая ветка
dense_1_1 = Dense(32, 
                  activation='relu', 
                  name='first_right_Dense')(input_layer_1)
dense_1_2 = Dense(16, 
                  activation='relu', 
                  name='second_right_Dense')(dense_1_1)

# Левая ветка
conv2d = Conv2D(32, 
                kernel_size=4, 
                activation='relu', 
                name='Conv2D')(input_layer_2)
max_pool = MaxPooling2D(pool_size=(3, 3),
                        name='MaxPooling')(conv2d)
flatten = Flatten(name='Flatten')(max_pool)
dense_2_1 = Dense(16,
                  activation='relu',
                  name='left_Dense')(flatten)

# Соединение
concatenate = Concatenate(axis=1, name='Concatenate')([dense_1_2, dense_2_1])

main_dense = Dense(32, 
                   activation='relu',
                   name='main_Dense')(concatenate)

output_1 = Dense(1, 
                 activation='sigmoid',
                 name='first_output')(main_dense)

output_2 = Dense(1, 
                 activation='sigmoid',
                 name='second_output')(main_dense)

model = Model(inputs=[input_layer_1, input_layer_2],
              outputs=[output_1, output_2])

plot(model)

Да, как вы могли заметить, названия слоёв должны не содержать пробелы. Также, как мы узнали, список слоёв, которые необходимо «соединить», можно передать в метод __call__() класса Concatenate. Однако имя слоя необходимо указывать в конструкторе.

В итоге мы получили вот такую модель:

Заключение

Сегодня мы разобрали, как с помощью библиотеки Keras создать модели с разветвлениями, несколькими входами и выходами. Надеюсь, изложенный материал будет вам полезен. Желаю высокого темпа обучения и быстрого обновления весов.

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

Также рекомендую прочитать статью Сверточная нейронная сеть для распознавания цифр. Подпишитесь на группу ВКонтакте, Telegram и YouTube-канал. Там еще больше полезного и интересного для разработчиков.