На этом шаге мы рассмотрим способ реализации таких слоев.
Подобно кубикам лего, состыковать можно только совместимые слои. Понятие совместимости слоев в нашем случае отражает лишь тот факт, что каждый слой принимает и возвращает тензоры определенной формы. Взгляните на следующий пример:
from tensorflow.keras import layers # Полносвязный слой с 32 выходами layer = layers.Dense(32, activation="relu")
Слой возвращает тензор, первое измерение которого равно 32. Данный слой можно связать со слоем ниже, только если тот принимает 32-мерные векторы.
В большинстве случаев библиотека Keras избавляет от необходимости беспокоиться о совместимости, поскольку слои, добавляемые в модели, автоматически конструируются так, чтобы соответствовать форме входного слоя. Представьте, что вы написали следующий код:
from tensorflow.keras import models from tensorflow.keras import layers model = models.Sequential([ layers.Dense(32, activation="relu"), layers.Dense(32) ])
Слои не получают никакой информации о форме входных данных - они автоматически определяют ее по форме первого измерения своих входных данных.
В упрощенной версии слоя Dense, реализованной нами на 57 шаге (и названной NaiveDense), требовалось явно передать размер входных данных конструктору, чтобы получить возможность создать веса. Это не очень удобно, поскольку тогда при конструировании моделей мы вынуждены будем явно указывать в каждом новом слое форму выходных данных предыдущего слоя:
model = NaiveSequential([ NaiveDense(input_size=784, output_size=32, activation="relu"), NaiveDense(input_size=32, output_size=64, activation="relu"), NaiveDense(input_size=64, output_size=32, activation="relu"), NaiveDense(input_size=32, output_size=10, activation="softmax") ])
Было бы еще хуже, если бы при выборе формы своих выходных данных слой руководствовался сложными правилами. Допустим, наш слой возвращает выходные данные с формой
(batch, input_size * 2 if input_size % 2 == 0 else input_size * 3).
Если бы мы повторно реализовали слой NaiveDense как слой Keras, поддерживающий автоматическое определение формы входных данных, то он выглядел бы как предыдущий слой SimpleDense (см. пример 3.22) с его методами build() и call().
В SimpleDense мы не создаем веса в конструкторе, как это делали в примере NaiveDense; теперь они создаются в специальном методе конструирования состояния build(), который принимает в аргументе форму первого измерения входных данных. Метод build() вызывается автоматически при первом вызове слоя (через метод __call__()). Именно поэтому мы определили вычисления в отдельном методе call(), а не в методе __call__() непосредственно. В общих чертах метод __call__() базового слоя выглядит примерно так:
def __call__(self, inputs): if not self.built: self.build(inputs.shape) self.built = True return self.call(inputs)
Благодаря автоматическому определению формы наш предыдущий пример становится простым и понятным:
model = keras.Sequential([ SimpleDense(32, activation="relu"), SimpleDense(64, activation="relu"), SimpleDense(32, activation="relu"), SimpleDense(10, activation="softmax") ])
Обратите внимание: автоматическое определение формы не единственное, что может метод __call__() класса Layer. Он также решает множество других задач, в частности делает выбор между жадным (немедленным) и графовым (с этим понятием вы познакомитесь в следующих шагах) выполнением, а также накладывает маску на входные данные. Пока просто запомните: приступая к реализации собственных слоев, описывайте прямой проход в методе call().
На следующем шаге мы рассмотрим переход от слоев к моделям.