На
предыдущем занятии мы с вами увидели, как можно с нуля создавать слои и модели,
используя базовые классы:
tf.keras.layers.Layerиtf.keras.Model
пакета
Keras – официального
бэкэндаTensorflow. Однако, для
описания стандартных архитектур нейронных сетей в Keras уже существуют
предопределенные классы для слоев:
-
Dense()
– полносвязный
слой;
-
Conv1D,
Conv2D, Conv3D – сверточные слои;
-
Conv2DTranspose,
Conv3DTranspose – транспонированные (обратные) светочные слои;
-
SimpleRNN,
LSTM, GRU – рекуррентные слои;
-
MaxPooling2D,
Dropout, BatchNormalization – вспомогательныеслои
и
многие другие (подробнее см. в официальной документации https://keras.io/api/layers/). А также предопределенные классы
моделей:
-
Model
– общий класс модели;
-
Sequential
– последовательная модель.
(Подробнее
о них также см. официальную документацию https://keras.io/api/models/).
Некоторые
из этих классов мы с вами уже использовали в курсе «Нейронные сети». Если вы
его не смотрели, то рекомендую ознакомиться:
Курс по нейронным сетям
Я
не буду здесь повторяться и рассказывать о классах слоев в Keras. Применение их
вполне очевидно и даже если вы впервые о них слышите, то из примеров станет
ясно, как они используются при конструировании нейронных сетей.
Класс Sequential
Часто
архитектуры нейронных сетей строят в виде последовательности слоев, начиная с
входного и заканчивая выходным:
Теоретически,
число скрытых слоев может быть сколь угодно большим. Для описания такой модели,
как раз и применяется класс Sequential.Например, для описания изображенной
сети, модель можно сформировать, следующим образом. Вначале импортируем
необходимые зависимости:
import os
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'
import tensorflow as tf
from tensorflow.keras.layers import Dense
from tensorflow import keras
from tensorflow.keras.datasets import mnist
from tensorflow.keras.utils import to_categorical
А,
затем, создаем экземпляр класса Sequential, то есть, последовательную
архитектуру нейронной сети:
model = keras.Sequential([
Dense(128, activation='relu'),
Dense(10, activation='softmax')
])
В
действительности, это эквивалентно последовательному вызову слоев для некоторого
входного тензора:
layer1 = Dense(128, activation='relu')
layer2 = Dense(10, activation='softmax')
x = tf.random.uniform((1, 20), 0, 1)
y = layer2(layer1(x))
Класс
Sequential предоставляет
лишь удобство и некоторый дополнительный функционал при работе с моделью. Например,
все слои доступны через список model.layers:
Можно
удалить последний слой методом pop():
А,
затем, добавить методом add():
model.add(Dense(5, activation='linear'))
Соответственно,
можно вначале определить пустую модель (без слоев):
model = keras.Sequential()
а
после этого добавить необходимые элементы:
model.add(Dense(128, activation='relu', name="layer1"))
model.add(Dense(10, activation='softmax', name="layer2"))
При
этом каждому слою можно задать свое имя, которые будут использоваться при
отображении служебной информации о модели.
У
каждого слоя и у модели в целом имеется свойство weights, содержащее список
настраиваемых параметров (весовых коэффициентов). Если обратиться к первому
слою и свойству weights:
print( model.layers[0].weights )
то
увидим пустой список. А если сделать то же самое для всей модели:
то
получим исключение (ошибку). Это связано с тем, что до момента подачи входного
сигнала на вход сети весовые коэффициенты еще не были сформированы. Здесь все
работает по аналогии с нашим классом DenseLayer, который мы создавали на
предыдущем занятии. Пока не будет вызван метод build слоя, весовые коэффициенты
отсутствуют.
Давайте
пропустим через модель входной сигнал, состоящий из одного наблюдения длиной 20
чисел:
x = tf.random.uniform((1, 20), 0, 1)
y = model(x)
Теперь при обращении
к свойству weights получим список
всех весовых коэффициентов модели:
и
мы также можем вывести структуру этой модели с помощью метода summary():
Слой Input
Помимо
функциональных слоев в Kerasсуществуют вспомогательные слои и один
из них определяется классом Input. Как вы уже догадались, этот слой
служит для описания формы входных данных. То есть, если модель не имеет слоя Input, то размерность
входного вектора устанавливается по входному тензору при первом вызове, как мы
это только что с вами видели на примере. Но, если явно указать размерность
через класс Input, то модель сети
строится сразу с начальным набором весов. Например, опишем последовательную
модель, следующим образом:
model = keras.Sequential([
Input(shape=(20, )),
Dense(128, activation='relu'),
Dense(10, activation='softmax')
])
Здесь
первый слой Input() устанавливает
размерность входного тензора, равным 20 элементов. Это означает, что на вход
следует подавать данные в формате:
[batch_size, 20]
Обратите
внимание, в Keras первая
размерность – это размер мини-батча, по которому производится расчет градиентов
и оптимизация весовых коэффициентов. Поэтому, указывая размер 20, получаем
матрицу:
batch_size x 20
Именно
так мы задавали входной тензор для нашей модели (размерностью 1 x 20):
x = tf.random.uniform((1, 20), 0, 1)
y = model(x)
Также
следует иметь в виду, что в коллекции model.layers:
будет
всего два слоя, а не три, так как входной слой Input не является
функциональным и нужен лишь для определения свойств входного сигнала.Того же
эффекта можно добиться, просто указывая размерность через параметр input_shape:
model = keras.Sequential([
Dense(128, activation='relu', input_shape=(784,), name="hidden_1"),
Dense(10, activation='softmax', name="output")
])
Разумеется,
такой параметр можно использовать только у первого слоя в последовательной
модели. На практике рекомендуется всегда заранее прописывать размерности
входного тензора, чтобы избежать возможных ошибок при подаче на вход другого
сигнала (другой размерности).
Обучение модели
Последовательная
модель обучается абсолютно также, как и любая другая модель в Keras. Если у нас
есть обучающая выборка:
(x_train, y_train), (x_test, y_test) = mnist.load_data()
x_train = x_train / 255
x_test = x_test / 255
x_train = tf.reshape(tf.cast(x_train, tf.float32), [-1, 28*28])
x_test = tf.reshape(tf.cast(x_test, tf.float32), [-1, 28*28])
y_train = to_categorical(y_train, 10)
y_test_cat = to_categorical(y_test, 10)
то
достаточно задать функцию потерь, оптимизатор:
model.compile(optimizer='adam',
loss='categorical_crossentropy',
metrics=['accuracy'])
и
запустить обучением через метод fit():
model.fit(x_train, y_train, batch_size=32, epochs=5)
Мы
все это с вами уже делали несколько раз, поэтому просто привожу фрагмент
программы, как пример обучения последовательной нейронной сети.
Извлечение признаков из последовательной модели
Давайте
теперь на основе последовательной модели сформируем немного более сложную
модель, которая будет иметь тот же один вход, но два выхода (по одному от каждого
слоя):
Такое
преобразование легко выполнить с помощью другого, общего класса моделей – keras.Model. При создании
его экземпляра достаточно в сформированной нейронной сети указать список входов
и список выходов. В нашем случае, это можно сделать так:
model_ex = keras.Model(inputs=model.inputs,
outputs=[layer.output for layer in model.layers])
Обратите
внимание, мы это делаем после обучения сети. В результате, модель model_ex будет содержать
те же самые слои и весовые коэффициенты, что и модель model, так как лишь
меняет конфигурацию входов и выходов, но не создает сеть заново. То есть, обе
модель ссылаются на одну и ту же архитектуру НС.Благодаря этому, мы можем
обучать любую из них, а затем, использовать обе, как обученный вариант НС.
В
качестве маленького эксперимента давайте создадим модель model_ex непосредственно
после модели model, обучим первую
модель (model) и сравним
выходные результаты последнего слоя:
x = tf.expand_dims(x_test[0], axis=0)
y = model_ex(x)
y2 = model(x)
print(y, y2, sep="\n\n")
Как
видим, выходные значения идентичны. Это показывает, что обе модели представляют
одну и ту же нейронную сеть. Но, model_ex дополнительно
еще дает доступ к промежуточным слоям.
Однако,
если сформировать модель, указав только один выход с промежуточного слоя:
model_ex = keras.Model(inputs=model.inputs,
outputs=model.layers[0].output)
то
получим урезанный вариант исходной модели. Здесь выходной слой будет
отсутствовать, так как он идет после первого. А если указать один выходной
слой:
model_ex = keras.Model(inputs=model.inputs,
outputs=model.get_layer(name="output").output)
то
получим эквивалент исходной модели, так как все промежуточные слои между входом
и указанными выходами автоматически повторяются. Мы можем в этом убедиться,
вызвав метод summary() для второй
модели:
Обратите
внимание, как мы обратились к выходному слою – по его имени, которое было
задано через параметр name этого слоя. Это пример того, как можно
извлекать отдельные слои из моделей нейронных сетей.
Расширение
существующей модели
Пакет
Keras позволяет
весьма гибко создавать и обучать НС. Например, мы можем расширить первую
модель, добавив в нее еще один полносвязный слой:
model_ex = keras.Sequential([
model,
Dense(10, activation="tanh")
])
Смотрите,
здесь первая часть модель – это первая модель, а далее описан еще один слой Dense. Мало того, мы
можем обучить этот последний слой, не трогая веса первой модели, то есть,
исключая ее из обучения. Для этого достаточно определить свойство:
И
после компилирования второй модели:
model_ex.compile(optimizer='adam',
loss='categorical_crossentropy',
metrics=['accuracy'])
дообучим
ее последний слой:
model_ex.fit(x_train, y_train, batch_size=32, epochs=3)
При
необходимости можно «замораживать» отдельные слои, исключая их из обучения. Для
этого следует обратиться к слою и установить у него тот же параметр trainable в
False:
model.layers[0].trainable = False
Надеюсь,
из этого занятия вы стали лучше понимать, как конструируются последовательные
модели в Keras, как они
модифицируются и обучаются.