В
наших занятиях по пакету Keras мы часто использовали метод compile(), через
который связывали модель с оптимизатором, функцией потерь и метриками:
model = keras.Sequential([
layers.Input(shape=(784,)),
layers.Dense(128, activation='relu'),
layers.Dense(64, activation='relu'),
layers.Dense(10, activation='softmax'),
])
model.compile(optimizer='adam',
loss='categorical_crossentropy',
metrics=['accuracy'])
Чаще
всего мы их определяли через строки, используя параметры по умолчанию. Если же
требуется более тонкая настройка, то следует использовать соответствующие
классы. В частности, для оптимизаторов частым выбором являются следующие
классы:
-
SGD – стохастический
градиентный спуск (с моментами, в том числе и Нестерова);
-
RMSprop – оптимизатор RMSprop;
-
Adam
– оптимизатор
Adam;
-
Adadelta
– оптимизатор
Adadelta;
-
Adagrad
– оптимизатор
Adagrad.
У
каждого из этих оптимизаторов есть свой набор параметров, подробнее о них можно
почитать в официальной документации:
https://keras.io/api/optimizers/
Например,
класс наиболее популярного оптимизатора Adam имеет следующий
синтаксис:
tf.keras.optimizers.Adam(learning_rate=0.001,
beta_1=0.9, beta_2=0.999, epsilon=1e-07, amsgrad=False, name="Adam",
**kwargs)
Именно
с такими параметрами он вызывается по умолчанию. Если требуются другие
значения, то мы должны создать экземпляр этого класса и указать нужные
параметры, например, так:
model.compile(optimizer=keras.optimizers.Adam(learning_rate=0.01),
loss='categorical_crossentropy',
metrics=['accuracy'])
То
же самое и с функциями потерь. Существует их стандартный набор в пакете Keras, например:
-
BinaryCrossentropy
– бинарная кросс-энтропия;
-
CategoricalCrossentropy
– категориальная кросс-энтропия;
-
KLDivergence
– дивергенция Кульбака-Лейблера;
-
MeanSquaredError
– средний квадрат ошибки;
-
MeanAbsoluteError
– средняя абсолютная ошибка.
Полный
перечень этих классов можно посмотреть на странице:
https://keras.io/api/losses/
Также
на занятиях по курсу «Нейронные сети» мы подробно говорили когда (для каких
задач) какую функцию лучше применять. Если вы этого не знаете, то рекомендую
этот курс к просмотру.
Соответственно,
если нам нужно обратиться непосредственно к классу, то это можно реализовать,
следующим образом:
model.compile(optimizer=keras.optimizers.Adam(learning_rate=0.01),
loss=keras.losses.CategoricalCrossentropy(),
metrics=['accuracy'])
Наконец,
метрики также можно задавать с помощью различных предопределенных классов:
-
Accuracy
-
BinaryAccuracy
-
CategoricalAccuracy
Полный
перечень этих классов можно посмотреть на странице:
https://keras.io/api/metrics/
Применяются
они очевидным образом:
model.compile(optimizer=keras.optimizers.Adam(learning_rate=0.01),
loss=keras.losses.CategoricalCrossentropy(),
metrics=[keras.metrics.CategoricalAccuracy()])
Собственные функции потерь
Конечно,
при необходимости, оптимизаторы, функции потерь и метрики мы можем
конструировать свои собственные. Чаще всего это относится к функциям потерь и
реже к метрикам. Создавать свои «хорошие» оптимизаторы – это уже удел избранных
и мы оставим это за рамками текущего курса (в принципе, встроенных вполне
достаточно для большинства реальных проектов).
Итак,
собственные потери в Kerasможно определить двумя способами:
-
непосредственно
через функцию;
-
с
помощью класса, унаследованного от keras.losses.Loss.
Начнем
с наиболее простого варианта – определение своей функции потерь. Для этого
достаточно объявить функцию с двумя аргументами y_true, y_pred и
математическими операциями над этими тензорами, например, так:
def myloss(y_true, y_pred):
return tf.reduce_mean(tf.square(y_true - y_pred))
Здесь
мы вычисляем средний квадрат рассогласования значений между требуемым выходом y_true и
спрогнозированным сетью y_pred. Все вычисления
лучше всего делать через функции Tensorflow, так как они, затем, будут
участвовать в автоматическом дифференцировании для вычисления градиентов. А эта
операция гарантированно корректно работает именно с функциями пакета Tensorflow (например,
функции NumPy здесь использовать
нельзя).
После
того, как функция объявлена, мы должны передать ссылку на нее в методе compile, следующим
образом:
model.compile(optimizer=keras.optimizers.Adam(learning_rate=0.001),
loss=myloss,
metrics=[keras.metrics.CategoricalAccuracy()])
Все,
теперь параметры модели будут подстраиваться для минимизации указанной функции
потерь.
Второй
способ определения потерь – это описание дочернего класса от базового keras.losses.Loss.
Зачем это нужно?Например, если мы хотим передавать дополнительные параметры для
вычисления потерь. Давайте предположим, что функция потерь имеет вид:
то
есть, здесь два дополнительных параметра α и β. Как раз их можно
определить, используя класс в качестве функции потерь, следующим образом:
class MyLoss(keras.losses.Loss):
def __init__(self, alpha=1.0, beta=1.0):
super().__init__()
self.alpha = alpha
self.beta = beta
def call(self, y_true, y_pred):
return tf.reduce_mean(tf.square(self.alpha * y_true - self.beta * y_pred))
Здесь
в конструкторе создаются два локальных свойства alpha и beta, а затем, в
методе call() производится
вычисление среднего квадрата ошибок с учетом этих параметров.
В
методе compile() мы можем
воспользоваться этим классом, например, так:
model.compile(optimizer=keras.optimizers.Adam(learning_rate=0.001),
loss=MyLoss(0.5, 2.0),
metrics=[keras.metrics.CategoricalAccuracy()])
Опять
же, это лишь пример того, как можно задавать пользовательские потери в пакете Keras. Необходимость
в этом возникает редко – в нестандартных задачах. Как правило, мы пользуемся
уже встроенными функциями или классами.
Пользовательские метрики
Наряду
с потерями можно создавать и собственные метрики. Для этого описывается класс
на основе базового tf.keras.metrics.Metric со следующим набором методов:
-
__init__(self)
– конструктор для инициализации метрики;
-
update_state(self, y_true, y_pred, sample_weight=None) – обновление переменных состояния,
которые, затем, используются в методе result() для
окончательного вычисления метрики;
-
result(self)
– метод для вычисления метрики на основе переменных состояния;
-
reset_states(self)
– сброс переменных состояния (например, при переходе к новой эпохе при обучении
нейронной сети).
Давайте
на конкретном примере посмотрим, как это все работает. Я взял за основу
довольно удачный пример из документации:
https://www.tensorflow.org/guide/keras/train_and_evaluate
и
видоизменил его для подсчета доли правильно классифицированных данных, то есть,
фактически, повторил метрику CategoricalAccuracy. Сам класс имеет
следующий вид:
class CategoricalTruePositives(keras.metrics.Metric):
def __init__(self, name="my_metric"):
super().__init__(name=name)
self.true_positives = self.add_weight(name="acc", initializer="zeros")
self.count = tf.Variable(0.0)
def update_state(self, y_true, y_pred, sample_weight=None):
y_pred = tf.reshape(tf.argmax(y_pred, axis=1), shape=(-1, 1))
y_true = tf.reshape(tf.argmax(y_true, axis=1), shape=(-1, 1))
values = tf.cast(y_true, "int32") == tf.cast(y_pred, "int32")
if sample_weight is not None:
sample_weight = tf.cast(sample_weight, "float32")
values = tf.multiply(values, sample_weight)
values = tf.cast(values, "float32")
self.true_positives.assign_add(tf.reduce_mean(values))
self.count.assign_add(1.0)
def result(self):
return self.true_positives / self.count
def reset_states(self):
self.true_positives.assign(0.0)
self.count.assign(0.0)
Смотрите,
вначале в конструкторе мы формируем две переменные состояния: true_positives – сумма долей
верной классификации; count – общее число долей. Затем, в методе update_state() мы для
каждого мини-батча вычисляем вектор верной классификации (values), определяем
долю правильной классификации и увеличиваем счетчик count на единицу. В методе result() делаем
окончательные вычисления для метрики (вычисляем среднюю долю верной
классификации), а в методе reset_states() сбрасываем
переменные true_positives и count в ноль.
После
этого, мы можем добавить наш класс метрики к списку метрик:
model.compile(optimizer=keras.optimizers.Adam(learning_rate=0.001),
loss=keras.losses.CategoricalCrossentropy(),
metrics=[keras.metrics.CategoricalAccuracy(), CategoricalTruePositives()])
И
при обучении сети обе метрики должны показывать одинаковые результаты. По
аналогии можно создавать самые разные метрики для контроля за процессом подбора
параметров моделей.
Настройка сети с несколькими выходами
Давайте
теперь представим, что мы проектируем сеть с несколькими выходами. Например,
это может быть модель автоэнкодера с дополнительным выходом для классификации
изображений:
Описать
такую архитектуру достаточно просто, используя функциональный подход:
enc_input = layers.Input(shape=(28, 28, 1))
x = layers.Conv2D(32, 3, activation='relu')(enc_input)
x = layers.MaxPooling2D(2, padding='same')(x)
x = layers.Conv2D(64, 3, activation='relu')(x)
x = layers.MaxPooling2D(2, padding='same')(x)
x = layers.Flatten()(x)
hidden_output = layers.Dense(8, activation='linear')(x)
x = layers.Dense(7 * 7 * 8, activation='relu')(hidden_output)
x = layers.Reshape((7, 7, 8))(x)
x = layers.Conv2DTranspose(64, 5, strides=(2, 2), activation="relu", padding='same')(x)
x = layers.BatchNormalization()(x)
x = layers.Conv2DTranspose(32, 5, strides=(2, 2), activation="linear", padding='same')(x)
x = layers.BatchNormalization()(x)
dec_output = layers.Conv2DTranspose(1, 3, activation="sigmoid", padding='same', name="dec_output")(x)
x2 = layers.Dense(128, activation='relu')(hidden_output)
class_output = layers.Dense(10, activation='softmax', name="class_output")(x2)
model = keras.Model(enc_input, [dec_output, class_output])
Смотрите,
здесь классификатор представляет собой обычную полносвязную сеть с десятью
выходами. Затем, с помощью класса Model мы создаем
модель сети с одним входом и двумя выходами.
Далее,
нам бы хотелось обучить эту сеть, но функции потерь для выходов следует брать
разными:
-
для
первого выхода – средний квадрат ошибок рассогласований между входом и выходом;
-
для
второго выхода – категориальную кросс-энтропию.
Сделать
это можно, по крайней мере, двумя способами. В первом случае в методе compile() указать
список соответствующих функций для параметра loss:
model.compile(optimizer='adam',
loss=['mean_squared_error', 'categorical_crossentropy']
)
Тогда
средний квадрат ошибок будет связан с первым выходом модели, а категориальная
кросс-энтропия – со вторым. Кроме того, при обучении в методе fit() также нужно
будет указать список требуемых выходных данных для обоих выходов:
model.fit(x_train, [x_train, y_train], epochs=1)
Во
втором случае мы можем в методах compile() и fit() словари,
вместо списков, что может быть несколько удобнее. Например, функции потерь
можно связать с выходами по их именам, следующим образом:
model.compile(optimizer='adam',
loss={
'dec_output': 'mean_squared_error',
'class_output': 'categorical_crossentropy'
}
)
И с метриками:
model.compile(optimizer='adam',
loss={
'dec_output': 'mean_squared_error',
'class_output': 'categorical_crossentropy'
},
metrics={
'dec_output': None,
'class_output': 'acc'
}
)
Обратите
внимание, мы для первого выхода указали метрику None, то есть, она
отсутствует. По идее, можно было бы вообще не указывать первую строчку в
словаре для metrics, а оставить
только вторую.
И
то же самое в методе fit() – для указания выходных значений
воспользуемся словарем:
model.fit(x_train, {'dec_output': x_train, 'class_output': y_train}, epochs=1)
Как
видите, все достаточно удобно. При этом, общая функция потерь будет
складываться из значений функция для обоих выходов. Если нам нужно
дополнительно указать веса loss_weightsпри их сложении:
то
это можно сделать через параметр loss_weights, следующим образом:
model.compile(optimizer='adam',
loss={
'dec_output': 'mean_squared_error',
'class_output': 'categorical_crossentropy'
},
loss_weights = [1.0, 0.5],
metrics={
'dec_output': None,
'class_output': 'acc'
}
)
Здесь
использован список, но также можно указать словарь.
Давайте
в конце этой программы пропустим через обученный автоэнкодер первое тестовое
изображение и оценим полученные выходы:
p = model.predict(tf.expand_dims(x_test[0], axis=0))
print( tf.argmax(p[1], axis=1).numpy() )
plt.subplot(121)
plt.imshow(x_test[0], cmap='gray')
plt.subplot(122)
plt.imshow(p[0].squeeze(), cmap='gray')
plt.show()
После
запуска (с весами loss_weights = [1.0, 1.0]), увидим максимальное значение на 7-м
выходе классификатора и следующее восстановленное изображение на декодере:
Конечно,
одной эпохи обучения для такой задачи мало, это лишь пример того, как можно
независимо настраивать параметры для модели с множеством разнотипных выходов.
И, я надеюсь, вы теперь хорошо себе это представляете.