
Привет, Хаброжители! Мы хотим поделиться с вами главой из книги «Алгоритмы машинного обучения» , которую уже можно предзаказать на нашем сайте.
В этой главе
-
Вариационные автоэнкодеры для обнаружения аномалий временных рядов
-
Сети смешанной плотности, использующие амортизированный вариационный вывод
-
Механизм внимания и трансформеры
-
Графовые нейронные сети
-
Исследования в области ML: глубокое обучение
В этой главе мы поговорим о передовых алгоритмах глубокого обучения. Они были подобраны с учетом соответствия их архитектуры современным стандартам качества и широты спектра применения. В этой главе мы изучим генеративные модели, основанные на вариационных автоэнкодерах (variational autoencoders, VAE), и рассмотрим полноценную реализацию детектора аномалий для данных временнˆых рядов. Мы продолжим наше путешествие знакомством с интригующей комбинацией нейронных сетей и классических моделей гауссовой смеси с использованием амортизированного вариационного вывода и взглянем на реализацию сети смешанной плотности. Затем мы сосредоточимся на концепции внимания и изучим реализацию с чистого листа архитектуры трансформера для задачи классификации. Наконец, мы рассмотрим графовые нейронные сети и используем одну из них для классификации вершин в графе цитирования. На протяжении всей этой главы мы будем пользоваться библиотекой глубокого обучения Keras/TensorFlow.
11.1. Автоэнкодеры
Автоэнкодер — это обучаемая в режиме без учителя нейронная сеть, которая используется для уменьшения размерности или выделения признаков. Она состоит из энкодера, скрытого слоя — так называемого узкого места (bottleneck) — и декодера, где энкодер и декодер являются обучаемыми нейронными сетями, как показано на рис. 11.1.
Автоэнкодеры обучаются восстанавливать свои входные данные. Другими словами, выходные данные автоэнкодера должны максимально точно соответствовать его входным данным. Чтобы обучение нейросети не свелось к выучиванию ею тривиальной функции тождества, скрытый слой в середине должен быть действительно узким местом. При обучении автоэнкодера минимизируется ошибка восстановления (reconstruction error), гарантируя, что скрытые модули отражают наиболее существенную информацию во входных данных.
На практике автоэнкодеры используются для извлечения признаков и не способны к созданию хорошо структурированных скрытых пространств. Воттут-товделоивступают VAE — см. диссертациюД. П. Кингмы (D. P. Kingma) «Variational Inference and Deep Learning: A New Synthesis» (University of Amsterdam, 2017). Архитектура VAE также содержит энкодер и декодер. Однако вместо сжатия входного изображения до размеров узкого скрытого слоя VAE преобразует изображение в параметры статистического распределения (например, среднее значение и дисперсию гауссовой случайной величины). Затем VAE из этого распределения генерирует выборочное значение, которое используется для декодирования обратно в исходное представление. Рисунок 11.2 демонстрирует архитектуру VAE.
Обучение VAE структурирует скрытое пространство таким образом, что каждая точка может быть декодирована в корректный выходной формат. Целевой функцией вариационного автоэнкодера является нижняя вариационная граница (ELBO). Пусть x представляет собой входное пространство, а z — скрытое пространство. Пусть p(x | z) — распределение декодера с параметрами θ, которое, при условии, что выборочное значение из скрытого пространства равно z, восстанавливает исходное значение входных данных x. Аналогично, пусть q(z | x) — это распределение энкодера с параметрами ϕ, которое принимает входные данные x и кодирует их в скрытую переменную z. Обратите внимание, что параметры θ и ϕ получаются при обучении. Наконец, пусть p(z) — априорное распределение скрытого пространства. Поскольку мы стремимся к тому, чтобы вариационное апостериорное распределение было как можно ближе к истинному апостериорному, то для обучения VAE используется следующая функция потерь:

Первое слагаемое определяет, насколько хорошо VAE восстанавливает точку данных x из выборочного значения z вариационного апостериорного распределения, а второе — насколько вариационное апостериорное распределение q(z | x) близко к априорному распределению p(z). Если предположить, что априорное распределение p(z) является гауссовым, то член KL-дивергенции можно переписать следующим образом (см. формулу 11.2). В следующем разделе мы увидим, как VAE можно использовать для обнаружения аномалий во временных рядах.

11.1.1. VAE: обнаружение аномалий во временных рядах
Давайте рассмотрим модель VAE, которая оперирует данными временных рядов с целью обнаружения аномалий. Эта архитектура показана на рис. 11.3:
Входные данные состоят из n сигналов x1,…, xn, а выходные представляют собой логарифмическую вероятность наблюдения входных данных xi при нормальных (неаномальных) параметрах обучения μi, σi. Это означает, что модель обучается на неаномальных данных в режиме без учителя, и когда в заданных входных данных xi возникает аномалия, соответствующий логарифм правдоподобия p(xi | {μi, σi}) падает, и по пересечению порогового значения можно судить о наличии аномалии.
Исходя из предположения гауссова правдоподобия, каждый датчик для представления аномалии имеет две степени свободы: (μ, σ). В результате для n входных датчиков мы обучаем 2n выходных параметров (среднее значение и дисперсию), которые позволяют отличить аномальное поведение от нормального.
Хотя входные сигналы независимы, в VAE они вкладываются (embedded) в совместное скрытое пространство семплирующего слоя. Структура скрытого пространства приближается к стандартному нормальному распределению N(0,1) путем минимизации KL-дивергенции. Модель обучается в режиме без учителя с помощью целевой функции, которая помогает достичь следующих двух целей: (1) максимизации логарифмического правдоподобия модели, усредненного по датчикам, и (2) структурирования скрытого пространства для приближения к N(0, 1):

Теперь мы можем приступать к знакомству с полноценной реализацией детектора аномалий LSTM-VAE, построенной на базе библиотеки Keras/TensorFlow. В приведенном ниже коде мы загружаем набор данных NAB (который можно найти в папке данных репозитория кода) и подготавливаем их для обучения. Numenta Anomaly Benchmark (NAB) — это новый тест для оценки алгоритмов обнаружения аномалий в потоковых и онлайн-приложениях. Далее мы определяем архитектуру детектора аномалий вместе с пользовательской функцией потерь и обучаем модель. Вам стоит попробовать запустить этот код в режиме блокнота на платформе Google Colab (доступна по ссылке https://colab.research.google.com/), чтобы посмотреть на результаты пошагового исполнения, а также ускорить процесс обучения модели при помощи графического процессора.
Листинг 11.1. Детектор аномалий LSTM-VAE
import numpy as np
import pandas as pd
import tensorflow as tf
from tensorflow import keras
import tensorflow_probability as tfp
from keras.layers import Input, Dense, Lambda, Layer
from keras.layers import LSTM, RepeatVector
from keras.models import Model
from keras import backend as K
from keras import metrics
from keras import optimizers
import math
import json
from scipy.stats import norm
from sklearn.model_selection import train_test_split
from sklearn import preprocessing
from sklearn.metrics import confusion_matrix
from sklearn.preprocessing import StandardScaler
from keras.callbacks import ModelCheckpoint
from keras.callbacks import TensorBoard
from keras.callbacks import LearningRateScheduler
from keras.callbacks import EarlyStopping
import matplotlib.pyplot as plt
tf.keras.utils.set_random_seed(42)
SAVE_PATH = "/content/drive/MyDrive/Colab Notebooks/data/"
DATA_PATH = "/content/drive/MyDrive/data/"
def scheduler(epoch, lr):
if epoch < 4:
return lr
else:
return lr * tf.math.exp(-0.1)
nab_path = DATA_PATH + 'NAB/
nab_data_path = nab_path
labels_filename = '/labels/combined_labels.json'
train_file_name = 'artificialNoAnomaly/art_daily_no_noise.csv'
test_file_name = 'artificialWithAnomaly/art_daily_jumpsup.csv'
#train_file_name = 'realAWSCloudwatch/rds_cpu_utilization_cc0c53.csv'
#test_file_name = 'realAWSCloudwatch/rds_cpu_utilization_e47b3b.csv'
labels_file = open(nab_path + labels_filename, 'r')
labels = json.loads(labels_file.read())
labels_file.close()
def load_data_frame_with_labels(file_name):
data_frame = pd.read_csv(nab_data_path + file_name)
data_frame['anomaly_label'] = data_frame['timestamp'].isin(
labels[file_name]).astype(int)
return data_frame
train_data_frame = load_data_frame_with_labels(train_file_name) test_data_frame = load_data_frame_with_labels(test_file_name)
plt.plot(train_data_frame.loc[0:3000,'value'])
plt.plot(test_data_frame['value'])
train_data_frame_final = train_data_frame.loc[0:3000,:]
test_data_frame_final = test_data_frame
data_scaler = StandardScaler()
data_scaler.fit(train_data_frame_final[['value']].values)
train_data = data_scaler.transform(train_data_frame_final[['value']].values)
test_data = data_scaler.transform(test_data_frame_final[['value']].values)
def create_dataset(dataset, look_back=64):
dataX, dataY = [], []
for i in range(len(dataset)-look_back-1):
dataX.append(dataset[i:(i+look_back),:])
dataY.append(dataset[i+look_back,:])
return np.array(dataX), np.array(dataY)
X_data, y_data = create_dataset(train_data, look_back=64) #look_back =
➥ window_size
X_train, X_val, y_train, y_val = train_test_split(X_data, y_data,
➥ test_size=0.1, random_state=42)
X_test, y_test = create_dataset(test_data, look_back=64) #look_back =
➥ window_size
batch_size = 256Параметры обучения
num_epochs = 32
timesteps = X_train.shape[1]Параметры модели
input_dim = X_train.shape[-1]
intermediate_dim = 16
latent_dim = 2
epsilon_std = 1.0
class Sampling(Layer): Семплирующий слой
def call(self, inputs):
z_mean, z_log_var = inputs
batch = tf.shape(z_mean)[0]
dim = tf.shape(z_mean)[1]
epsilon = tf.keras.backend.random_normal(shape=(batch, dim))
return z_mean + tf.exp(0.5 * z_log_var) * epsilon
class Likelihood(Layer): Слой правдоподобия
def call(self, inputs):
x, x_decoded_mean, x_decoded_scale = inputs
dist = tfp.distributions.MultivariateNormalDiag(x_decoded_mean,
➥ x_decoded_scale)
likelihood = dist.log_prob(x)
return likelihood
# Архитектура VAE
# кодер
x = Input(shape=(timesteps, input_dim,))
h = LSTM(intermediate_dim)(x)
z_mean = Dense(latent_dim)(h)
z_log_sigma = Dense(latent_dim, activation='softplus')(h)
# семплирование
z = Sampling()((z_mean, z_log_sigma))
# декодер
decoder_h = LSTM(intermediate_dim, return_sequences=True)
decoder_loc = LSTM(input_dim, return_sequences=True)
decoder_scale = LSTM(input_dim, activation='softplus', return_sequences=True)
h_decoded = RepeatVector(timesteps)(z)
h_decoded = decoder_h(h_decoded)
x_decoded_mean = decoder_loc(h_decoded)
x_decoded_scale = decoder_scale(h_decoded)
# логарифмическое правдоподобие
llh = Likelihood()([x, x_decoded_mean, x_decoded_scale])
vae = Model(inputs=x, outputs=llh) Определение VAE-модели
# Потери: KL-дивергенция плюс логарифмическое правдоподобие
kl_loss = – 0.5 * K.mean(1 + z_log_sigma - K.square(z_mean) –
➥ K.exp(z_log_sigma))
tot_loss = –K.mean(llh - kl_loss)
vae.add_loss(tot_loss)
# Функция потерь и оптимизатор
loss_fn = tf.keras.losses.MeanSquaredError()
optimizer = tf.keras.optimizers.Adam()
@tf.function
def training_step(x):
with tf.GradientTape() as tape:
reconstructed = vae(x) # Восстановление входных данных
# Вычисление потерь
loss = 0 #loss_fn(x, reconstructed)
loss += sum(vae.losses)
# Обновление весов VAE
grads = tape.gradient(loss, vae.trainable_weights)
optimizer.apply_gradients(zip(grads, vae.trainable_weights))
return loss
losses = [] # Регистрация потерь во времени
dataset = tf.data.Dataset.from_tensor_slices(X_train).batch(batch_size)
for epoch in range(num_epochs):
for step, x in enumerate(dataset):
loss = training_step(x)
losses.append(float(loss))
print("Epoch:", epoch, "Loss:", sum(losses) / len(losses))
plt.figure()
plt.plot(losses, c='b', lw=2.0, label='Обучение')
plt.title('Модель LSTM-VAE')
plt.xlabel('Эпоха')
plt.ylabel('Общие потери')
plt.legend(loc='upper right')
plt.show()
pred_test = vae.predict(X_test)
plt.plot(pred_test[:,0])
is_anomaly = pred_test[:,0] < -1e1
plt.figure()
plt.plot(test_data, color='b')
plt.figure()
plt.plot(is_anomaly, color='r')
Рисунок 11.4 демонстрирует, что в простом прямоугольном входном сигнале удается обнаружить падение логарифмической вероятности и после пересечения порогового значения выдать сигнал о наличии аномалии. Уменьшение общих потерь в процессе обучения показано на рис. 11.5.
В следующем разделе мы рассмотрим амортизированный вариационный вывод применительно к сетям смешанной плотности.
11.2. Амортизированный вариационный вывод
Амортизированный вариационный вывод (VI) является воплощением идеи, что вместо оптимизации набора свободных параметров мы можем ввести параметризованную функцию, которая сопоставляет пространство наблюдений с параметрами приближенного апостериорного распределения. В практическом плане наблюдения могут подаваться на вход нейросети, которая выводит параметры среднего значения и дисперсии для скрытой переменной, связанной с этими наблюдениями (с чем мы столкнулись в архитектуре VAE). Затем можно оптимизировать параметры этой нейросети вместо отдельных параметров каждого наблюдения.
Одним из преимуществ амортизированного VI является повторное использование предыдущих выводов по аналогии с динамическим программированием, в котором мы храним решения ранее вычисленных подзадач. В качестве примера рассмотрим два запроса, приведенные на рис. 11.6, — см. книгу Сэмюэл Дж. Гершмана (Samuel J. Gershman), Ноа Д. Гудмана (Noah D. Goodman) «Amortized Inference in Probabilistic Reasoning» (Cognitive Science, 2014):
Давайте рассмотрим один из примеров амортизированного VI, а именно сеть смешанной плотности, в которой мы будем использовать многослойный перцептрон (МСП) для параметризации модели гауссовой смеси.
Видно, что запрос 1 является подзапросом запроса 2. Тем самым условное распределение, вычисленное для запроса 1, может быть повторно использовано для ответа на запрос 2. Еще одним преимуществом амортизированного VI является то, что в нем отсутствует необходимость аналитического вывода ELBO, поскольку оптимизация происходит с помощью стохастического градиентного спуска (SGD). Ограничение амортизированного VI состоит в том, что пробел в обобщающей способности (generalization gap) модели зависит от емкости выбранной нейронной сети как стохастической функции.
Давайте рассмотрим один из примеров амортизированного VI, а именно сеть смешанной плотности, в которой мы будем использовать многослойный перцептрон (МСП) для параметризации модели гауссовой смеси.
11.2.1. Сети смешанной плотности
Сети смешанной плотности (mixture density networks, MDN) — это модели смеси, в которых параметры, такие как среднее значение, ковариация и пропорция смеси, получаются в результате обучения нейронной сети. Сети MDN объединяют в себе структурированное представление данных (смесь плотностей) с неструктурированным выводом параметров (нейронная сеть МСП). При обучении MDN параметры смеси получаются за счет максимизации логарифмического правдоподобия или, что эквивалентно, минимизации потерь отрицательного логарифмического правдоподобия.
Предполагая модель гауссовой смеси с K компонентами, мы можем записать вероятность тестовой точки данных yi при условии обучающих данных x следующим образом:

Здесь параметры μk, σk, πk выучиваются нейронной сетью (например, МСП) с параметрами θ:

Архитектура MDN представлена на рис. 11.7.
Как результат, нейронная сеть (НС) представляет собой модель с несколькими выходами, на которые накладываются следующие ограничения:

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

В нашем примере используется предположение об изотропности ковариационной матрицы: Σk = σk2 I. Таким образом, мы можем записать d-мерный гауссиан в виде произведения. С учетом формулы для многомерного гауссиана мы получаем следующее:

Поскольку ковариационная матрица изотропна, мы можем переписать формулу 11.8 следующим образом:

Давайте теперь познакомимся с гауссовой MDN, реализованной с использованием Keras/TensorFlow. В нашем примере мы используем синтетические данные с эталонными значениями среднего и дисперсии. Данные генерируются при помощи семплирования из многомерного распределения. Затем мы определяем архитектуру MDN с несколькими выходами с ограничениями на пропорции смеси и дисперсию. Наконец, мы вычисляем потерю отрицательного логарифмического правдоподобия, обучаем модель и отображаем результаты предсказания на тестовых данных. Вам стоит попробовать запустить этот код в режиме блокнота на платформе Google Colab (доступна по ссылке https://colab.research.google.com/), чтобы посмотреть на результаты пошагового исполнения, а также ускорить процесс обучения модели при помощи графического процессора.
Листинг 11.2. Сеть плотности гауссовой смеси
import numpy as np
import pandas as pd
import tensorflow as tf
from tensorflow import keras
from keras.models import Model
from keras.layers import concatenate, Input
from keras.layers import Dense, Activation, Dropout, Flatten
from keras.layers import BatchNormalization
from keras import regularizers
from keras import backend as K
from keras.utils import np_utils
from keras.callbacks import ModelCheckpoint
from keras.callbacks import TensorBoard
from keras.callbacks import LearningRateScheduler
from keras.callbacks import EarlyStopping
from sklearn.datasets import make_blobs
from sklearn.metrics import adjusted_rand_score
from sklearn.metrics import normalized_mutual_info_score
from sklearn.model_selection import train_test_split
import math
import matplotlib.pyplot as plt
import matplotlib.cm as cm
tf.keras.utils.set_random_seed(42)
SAVE_PATH = "/content/drive/MyDrive/Colab Notebooks/data/"
def scheduler(epoch, lr): Изменение скорости обучения
if epoch < 4:
return lr
else:
return lr * tf.math.exp(-0.1)
def generate_data(N): Синтетические эталонные данные
pi = np.array([0.2, 0.4, 0.3, 0.1])
mu = [[2,2], [-2,2], [-2,-2], [2,-2]]
std = [[0.5,0.5], [1.0,1.0], [0.5,0.5], [1.0,1.0]]
x = np.zeros((N,2), dtype=np.float32)
y = np.zeros((N,2), dtype=np.float32)
z = np.zeros((N,1), dtype=np.int32)
for n in range(N):
k = np.argmax(np.random.multinomial(1, pi))
x[n,:] = np.random.multivariate_normal(mu[k], np.diag(std[k]))
y[n,:] = mu[k]
z[n,:] = k
# конец for
z = z.flatten()
return x, y, z, pi, mu, std
def tf_normal(y, mu, sigma): Изотропный многомерный гауссиан
y_tile = K.stack([y]*num_clusters, axis=1) #[batch_size, K, D]
result = y_tile - mu
sigma_tile = K.stack([sigma]*data_dim, axis=-1) #[batch_size, K, D]
result = result * 1.0/(sigma_tile+1e-8)
result = -K.square(result)/2.0
oneDivSqrtTwoPI = 1.0/math.sqrt(2*math.pi)
result = K.exp(result) * (1.0/(sigma_tile + 1e-8))*oneDivSqrtTwoPI
result = K.prod(result, axis=-1) #[batch_size, K] независимые
➥ и одинаково распределенные гауссианы
return result
def NLLLoss(y_true, y_pred): Потеря отрицательного логарифмического правдоподобия
out_mu = y_pred[:,:num_clusters*data_dim]
out_sigma = y_pred[:,num_clusters*data_dim : num_clusters*(data_dim+1)]
out_pi = y_pred[:,num_clusters*(data_dim+1):]
out_mu = K.reshape(out_mu, [-1, num_clusters, data_dim])
result = tf_normal(y_true, out_mu, out_sigma)
result = result * out_pi
result = K.sum(result, axis=1, keepdims=True)
result = –K.log(result + 1e-8)
result = K.mean(result)
return tf.maximum(result, 0)
# генерация данных
X_data, y_data, z_data, pi_true, mu_true, sigma_true = generate_data(4096)
data_dim = X_data.shape[1]
num_clusters = len(mu_true)
num_train = 3500
X_train, X_test, y_train, y_test = X_data[:num_train,:],
➥ X_data[num_train:,:], y_data[:num_train,:], y_data[num_train:,:]
z_train, z_test = z_data[:num_train], z_data[num_train:]
# визуализация данных
plt.figure()
plt.scatter(X_train[:,0], X_train[:,1], c=z_train, cmap=cm.bwr)
plt.title('training data')
plt.show()
#plt.savefig(SAVE_PATH + '/mdn_training_data.png')
batch_size = 128Параметры обучения
num_epochs = 128
hidden_size = 32Параметры модели
weight_decay = 1e-4
# Архитектура MDN
input_data = Input(shape=(data_dim,))
x = Dense(32, activation='relu')(input_data)
x = Dropout(0.2)(x)
x = BatchNormalization()(x)
x = Dense(32, activation='relu')(x)
x = Dropout(0.2)(x)
x = BatchNormalization()(x)
mu = Dense(num_clusters * data_dim, activation
➥ ='linear')(x) Кластерные средние
sigma = Dense(num_clusters, activation=K.exp)(x) Диагональная ковариационная матрица
pi = Dense(num_clusters, activation='softmax')(x) Пропорции смеси
out = concatenate([mu, sigma, pi], axis=-1)
model = Model(input_data, out)
model.compile(
loss=NLLLoss,
optimizer=tf.keras.optimizers.Adam(),
metrics=["accuracy"]
)
model.summary()
# Определение обратных вызовов
file_name = SAVE_PATH + 'mdn-weights-checkpoint.h5'
checkpoint = ModelCheckpoint(file_name, monitor='val_loss', verbose=1,
➥ save_best_only=True, mode='min')
reduce_lr = LearningRateScheduler(scheduler, verbose=1)
early_stopping = EarlyStopping(monitor='val_loss', min_delta=0.01,
➥ patience=16, verbose=1)
#tensor_board = TensorBoard(log_dir='./logs', write_graph=True)
callbacks_list = [checkpoint, reduce_lr, early_stopping]
hist = model.fit(X_train, y_train, batch_size
➥ =batch_size, epochs=num_epochs, callbacks
➥ =callbacks_list, validation_split=0.2,
➥ shuffle=True, verbose=2) Обучение модели
y_pred = model.predict(X_test) Оценка модели
mu_pred = y_pred[:,:num_clusters*data_dim]
mu_pred = np.reshape(mu_pred, [-1, num_clusters, data_dim])
sigma_pred = y_pred[:,num_clusters*data_dim : num_clusters*(data_dim+1)]
pi_pred = y_pred[:,num_clusters*(data_dim+1):]
z_pred = np.argmax(pi_pred, axis=-1)
rand_score = adjusted_rand_score(z_test, z_pred)
print("adjusted rand score: ", rand_score)
nmi_score = normalized_mutual_info_score(z_test, z_pred)
print("normalized MI score: ", nmi_score)
mu_pred_list = []
sigma_pred_list = []
for label in np.unique(z_pred):
z_idx = np.where(z_pred == label)[0]
mu_pred_lbl = np.mean(mu_pred[z_idx,label,:], axis=0)
mu_pred_list.append(mu_pred_lbl)
sigma_pred_lbl = np.mean(sigma_pred[z_idx,label], axis=0)
sigma_pred_list.append(sigma_pred_lbl)
# конец for
print("true means:")
print(np.array(mu_true))
print("predicted means:")
print(np.array(mu_pred_list))
print("true sigmas:")
print(np.array(sigma_true))
print("predicted sigmas:")
print(np.array(sigma_pred_list))
# создание графиков
plt.figure()
plt.scatter(X_test[:,0], X_test[:,1], c=z_pred, cmap=cm.bwr)
plt.scatter(np.array(mu_pred_list)[:,0], np.array(mu_pred_list)[:,1],
➥ s=100, marker='x', lw=4.0, color='k')
plt.title('Тестовые данные')
plt.figure()
plt.plot(hist.history['loss'], 'b', lw=2.0, label='Обучение')
plt.plot(hist.history['val_loss'], '--r', lw=2.0, label='Валидация')
plt.title('Сеть смешанной плотности')
plt.xlabel('Эпоха')
plt.ylabel('Потеря отрицательного логарифмического правдоподобия')
plt.legend(loc='upper left')
На рис. 11.8 показаны кластерные центроиды, наложенные поверх тестовых данных, а также потери при обучении и валидации.
Можно видеть, что прогнозируемые средние значения близки к центрам кластеров. Интересно заметить, что для приведенного примера два центра кластера совпадают. Читатель может поэкспериментировать с различными значениями начального числа (seed) и количеством обучающих точек, чтобы понять поведение модели. Кроме того, из графиков видно, что потери как при обучении, так и при валидации уменьшаются с увеличением количества эпох. В следующем разделе мы рассмотрим мощную архитектуру трансформера, построенную по принципу обучения с самоконтролем (self-supervised learning).
11.3. Внимание и трансформеры
Механизм внимания позволяет модели адаптивно, при помощи весовых коэффициентов внимания, регулировать степень внимания к различным частям входных данных. Внимание может применяться во многих видах нейронных сетей, но впервые оно было использовано в контексте рекуррентных нейронных сетей (РНС). В РНС-моделях Seq2Seq, подобных используемым для машинного перевода, выходной контекстный вектор, который суммирует входное предложение, не имеет доступа к входным словам по отдельности. Это приводит к снижению показателей производительности, измеряемых метрикой BLEU. Мы можем избавиться от этого недочета, позволив выходным данным напрямую взвешенным образом уделять внимание словам на входе. Другими словами, мы можем представить вектор контекста как взвешенную сумму векторов входных слов hsenc:

Здесь ats — это обучаемые значения весов внимания, определяемые следующим образом:

Существует несколько способов обучения функции оценки (score), например мультипликативный стиль (multiplicative style):

Здесь W — промежуточная обучаемая матрица. Внимание можно обобщенно представить себе как мягкий поиск по словарю (soft dictionary lookup). Мягкий поиск по словарю относится к типу поиска, при котором точное соответствие не найдено. Это полезно при поиске слов, которые могли быть написаны с ошибками или связаны с искомым термином. Мы можем рассматривать механизм внимания как сравнение набора целевых векторов, или запросов, qi с набором векторов-кандидатов, или ключей, kj. Для каждого запроса мы оцениваем, насколько он связан с каждым ключом, а затем используем эти оценки для взвешивания и суммирования значений vj, связанных с каждым ключом. Таким образом, матрицу внимания A можно определить следующим образом:

Учитывая весовые коэффициенты внимания Aij, вычисляем взвешенную комбинацию значений vj, связанных с каждым ключом. В результате для i-го запроса имеем следующее:

Здесь можно выбрать нормированную мультипликативную оценку с W = 1:

Если имеется N запросов (Q размера N × D) и N пар ключ — значение (K размером N × D), то можем записать результат (взвешенный набор значений V, ключи которого K больше всего напоминают запрос Q) в матричной форме:

Множитель softmax в произведении гарантирует, что распределение дает в сумме 1, и его можно рассматривать как указатель на место, куда нужно смотреть в матрице значений V.
На рис. 11.9 показана тепловая карта матрицы внимания для нейронного машинного перевода (neural machine translation, NMT). В нем запрос — это целевая последовательность, в то время как ключи и значения — исходная последовательность. Диаграмма показывает, на какие исходные английские слова модель обращает внимание при создании целевой переводной последовательности на испанском языке.
Самовнимание — ключевой компонент архитектуры трансформера. Трансформер — это модель Seq2Seq, которая использует внимание как в энкодере, так и в декодере, что устраняет необходимость в РНС. На рис. 11.10 показана архитектура трансформера — см. статью Ашиша Васвани (Ashish Vaswani) и др. «Attention Is All You Need» (NeurIPS, 2017). Давайте более детально рассмотрим строительные блоки трансформера.

На верхнем уровне мы видим архитектуру «энкодер — декодер» с входами в левой ветви и выходами в правой ветви. Также можно выделить такие элементы, как многоголовое внимание (multihead attention), позиционное кодирование, плотные слои и слои нормализации (normalization layer), а также остаточные связи (residual connection) для облегчения обратного распространения ошибки.
Идею самовнимания можно распространить на многоголовое внимание. По сути, создавая несколько «голов», мы распараллеливаем механизм внимания, как показано на рис. 11.11. Выходы нескольких голов затем объединяются (concatenate) и преобразуются при помощи плотного слоя. Благодаря многоголовому вниманию модель имеет несколько независимых способов понимания входных данных.

Чтобы модель могла использовать порядок следования, нужно ввести некоторую информацию о расположении токенов в последовательности. Именно для этого требуется позиционное кодирование, информация о котором добавляется к входным эмбеддингам в нижней части ветвей энкодера и декодера. Позиционные кодировки имеют те же размеры, что и эмбеддинги, так что их можно складывать. Таким образом, если одно и то же слово появляется в нескольких позициях, фактическое представление слова будет отличаться в зависимости от того, где оно стоит в предложении.
Наконец, нормализация входных данных производится путем вычисления среднего значения и дисперсии по каналам и пространственным измерениям, а для завершения работы энкодера добавляется линейный (плотный) уровень. Линейный слой появляется после многоголового самовнимания, чтобы спроецировать представление в пространство более высокой размерности, а затем вернуться к исходному пространству. Это помогает решать проблемы со стабильностью и предотвращает неправильную инициализацию.
Если мы внимательно посмотрим на декодер, то заметим, что он содержит все те же компоненты и, в дополнение к маскированному многоголовому слою самовнимания, новый многоголовый слой самовнимания, известный как внимание «энкодер — декодер». Конечный результат работы декодера преобразуется с помощью заключительного линейного слоя, а выходные вероятности, которые предсказывают следующий токен в выходной последовательности, вычисляются с помощью стандартной функции softmax.
Цель маскированного внимания (masked attention) — соблюдать принцип причинности при генерации выходного предложения. Поскольку полное предложение еще недоступно и генерируется по одному токену за раз, мы маскируем выходные данные, вводя матрицу масок M, которая содержит только два типа значений: ноль и отрицательную бесконечность:

Внимание «энкодер — декодер» — это всего лишь известное нам многоголовое самовнимание, за исключением того, что запрос Q поступает из другого источника, чем ключи K и значения V. Такое внимание также известно в литературе как перекрестное внимание (cross-attention). Стоит запомнить, что в примере с машинным переводом наша целевая последовательность или запрос Q поступает из декодера, в то время как энкодер действует как база данных и предоставляет ключи K и значения V. Интуитивный подход, лежащий в основе слоя внимания «энкодер — декодер», заключается в соединении входных и выходных предложений. Таким образом, внимание «энкодер — декодер» обучается связывать входное предложение с соответствующим выходным словом, определяя, насколько каждое целевое слово связано с входными словами.
В конечном счете нули будут преобразованы в единицы с помощью softmax, тогда как отрицательные значения бесконечности станут нулевыми, тем самым удаляя соответствующее соединение из выхода.
И это все! Теперь мы готовы к знакомству с классификатором на основе трансформера, реализованным с нуля. Вам стоит попробовать запустить этот код в режиме блокнота на платформе Google Colab (доступна по ссылке https://colab.research.google.com/), чтобы посмотреть на результаты пошагового исполнения, а также ускорить процесс обучения модели при помощи графического процессора.
Листинг 11.3. Трансформер для текстовой классификации
import numpy as np
import pandas as pd
import tensorflow as tf
from tensorflow import keras
from keras.models import Model, Sequential
from keras.layers import Layer, Dense, Dropout, Activation
from keras.layers import LayerNormalization, MultiHeadAttention
from keras.layers import Input, Embedding, GlobalAveragePooling1D
from keras import regularizers
from keras.preprocessing import sequence
from keras.utils import np_utils
from keras.callbacks import ModelCheckpoint
from keras.callbacks import TensorBoard
from keras.callbacks import LearningRateScheduler
from keras.callbacks import EarlyStopping
import matplotlib.pyplot as plt
tf.keras.utils.set_random_seed(42)
SAVE_PATH = "/content/drive/MyDrive/Colab Notebooks/data/"
def scheduler(epoch, lr): Изменение скорости обучения
if epoch < 4:
return lr
else:
return lr * tf.math.exp(-0.1)
# Загрузка набора данных
max_words = 20000 Топ-20 тысяч самых частотных слов
seq_len = 200 Первые 200 слов каждой рецензии на фильм
(x_train, y_train), (x_val, y_val) =
➥ keras.datasets.imdb.load_data(num_words=max_words)
x_train = keras.utils.pad_sequences(x_train, maxlen=seq_len)
x_val = keras.utils.pad_sequences(x_val, maxlen=seq_len)
class TransformerBlock(Layer):
def __init__(self, embed_dim, num_heads, ff_dim, rate=0.1):
super(TransformerBlock, self).__init__()
self.att = MultiHeadAttention(num_heads=num_heads, key_dim=embed_dim)
self.ffn = Sequential(
[Dense(ff_dim, activation="relu"), Dense(embed_dim)]
)
self.layernorm1 = LayerNormalization(epsilon=1e-6)
self.layernorm2 = LayerNormalization(epsilon=1e-6)
self.dropout1 = Dropout(rate)
self.dropout2 = Dropout(rate)
def call(self, inputs, training):
attn_output = self.att(inputs, inputs)
attn_output = self.dropout1(attn_output, training=training)
out1 = self.layernorm1(inputs + attn_output)
ffn_output = self.ffn(out1)
ffn_output = self.dropout2(ffn_output, training=training)
return self.layernorm2(out1 + ffn_output)
class TokenAndPositionEmbedding(Layer):
def __init__(self, maxlen, vocab_size, embed_dim):
super(TokenAndPositionEmbedding, self).__init__()
self.token_emb = Embedding(input_dim=vocab_size,
➥ output_dim=embed_dim)
self.pos_emb = Embedding(input_dim=maxlen, output_dim=embed_dim)
def call(self, x):
maxlen = tf.shape(x)[-1]
positions = tf.range(start=0, limit=maxlen, delta=1)
positions = self.pos_emb(positions)
x = self.token_emb(x)
return x + positions
batch_size = 32Параметры обучения
num_epochs = 8
embed_dim = 32Параметры модели
num_heads = 2
ffdim = 32
# архитектура трансформера
inputs = Input(shape=(seq_len,))
embedding_layer = TokenAndPositionEmbedding(seq_len, max_words, embed_dim)
x = embedding_layer(inputs)
transformer_block = TransformerBlock(embed_dim, num_heads, ff_dim)
x = transformer_block(x)
x = GlobalAveragePooling1D()(x)
x = Dropout(0.1)(x)
x = Dense(20, activation="relu")(x)
x = Dropout(0.1)(x)
outputs = Dense(2, activation="softmax")(x)
model = Model(inputs=inputs, outputs=outputs)
model.compile(
loss=keras.losses.SparseCategoricalCrossentropy(),
optimizer=tf.keras.optimizers.Adam(),
metrics=["accuracy"]
)
# Определение обратных вызовов
file_name = SAVE_PATH + 'transformer-weights-checkpoint.h5'
#checkpoint = ModelCheckpoint(file_name, monitor='val_loss', verbose=1,
➥ save_best_only=True, mode='min')
reduce_lr = LearningRateScheduler(scheduler, verbose=1)
early_stopping = EarlyStopping(monitor='val_loss', min_delta=0.01,
➥ patience=16, verbose=1)
#tensor_board = TensorBoard(log_dir='./logs', write_graph=True)
callbacks_list = [reduce_lr, early_stopping]
hist = model.fit(x_train, y_train, batch_size=batch_size,
➥ epochs=num_epochs, callbacks=callbacks_list, validation_data=(x_val,
➥ y_val)) Оценка модели
test_scores = model.evaluate(x_val, y_val, verbose=2) Обучение модели
print("Test loss:", test_scores[0])
print("Test accuracy:", test_scores[1])
plt.figure()
plt.plot(hist.history['loss'], 'b', lw=2.0, label='Обучение')
plt.plot(hist.history['val_loss'], '--r', lw=2.0, label='Валидация')
plt.title('Трансформерная модель')
plt.xlabel('Эпоха')
plt.ylabel('Потери кросс-энтропии')
plt.legend(loc='upper right')
plt.show()
plt.figure()
plt.plot(hist.history['accuracy'], 'b', lw=2.0, label='Обучение')
plt.plot(hist.history['val_accuracy'], '--r', lw=2.0, label='Валидация')
plt.title('Трансформерная модель')
plt.xlabel('Эпоха')
plt.ylabel('Доля верных результатов')
plt.legend(loc='upper left')
plt.show()
plt.figure()
plt.plot(hist.history['lr'], lw=2.0, label='learning rate')
plt.title('Transformer model')
plt.xlabel('Epochs')
plt.ylabel('Learning Rate')
plt.legend()
plt.show()
В этом разделе мы познакомились с тем, как самовнимание может быть использовано в архитектуре трансформера для классификации тональности текстов. В следующем разделе мы рассмотрим нейронные сети в применении к графовым данным.
11.4. Графовые нейронные сети
Самые разнообразные виды информации могут быть представлены в виде графов. Среди примеров можно назвать графы знаний, социальные сети, молекулярные структуры и сети цитирования документов. Графовые нейронные сети (ГНС) работают с графами и реляционными данными. В этом разделе мы изучим графовые сверточные сети (ГСС) для классификации вершин в наборе данных сети цитирования CORA. Но сначала давайте рассмотрим основы ГНС.
Пусть G = (V, E) — граф с вершинами V и ребрами E. Чтобы отразить реберную связь, можно построить матрицу смежности A, причем Aij = 1, если ребро существует между вершинами i и j, и 0 в противном случае. Для неориентированных графов матрица смежности будет симметричной: A = AT. А еще при обучении ГНС будет полезна матрица признаков вершин X. Если у нас есть N вершин и F признаков на вершину, то размер X равен N Ч F.
Например, в наборе данных CORA каждая вершина представляет собой документ, а каждое ребро — цитату, которая образует направленное ребро между двумя вершинами. Мы можем зафиксировать отношения цитирования с помощью матрицы смежности A. Поскольку каждый документ представляет собой набор слов, то можно ввести признаки — индикаторы, которые сигнализируют нам, присутствует ли в документе то или иное словарное слово. В этом случае N будет количеством документов, а F — размером словаря. Таким образом, мы можем представить текстовую информацию в каждом документе с помощью двоичной матрицы X размером N Ч F.
Важно заметить, что ребра также могут иметь свой собственный набор признаков. В этом случае, если размер признаков ребер равен S, а количество ребер равно M, мы можем построить матрицу признаков ребер E размером M Ч S. Также важно провести различие между классификацией всего графа как целого (например, при классификации молекул) и классификацией вершин внутри графа (например, как в сети цитирования CORA). В первом случае мы имеем дело с классификацией в пакетном режиме (batch mode classification), а во втором — с последовательной классификацией (single mode classification).
Интересно провести параллель между ГСС и СНС. Изображения также можно рассматривать в виде графов, хотя и с регулярной структурой. Например, вершины могут обозначать пиксели, признаки вершин могут представлять значения пикселей, а признаки ребер — евклидово расстояние между всеми пикселями графа. В этом свете ГСС можно рассматривать как обобщение СНС, поскольку они работают на произвольно связных графах.
Мы можем рассматривать распространение информации в спектральной ГСС как распространение сигнала вдоль вершин. Спектральная ГСС использует разложение по собственным векторам матрицы Лапласа графа для распространения сигнала с помощью следующего ключевого уравнения прямого распространения:

Давайте подробно разберем это уравнение. Если мы возьмем матрицу смежности A и умножим ее на матрицу признаков X, то произведение AX будет представлять собой сумму признаков соседних вершин. Однако суммирование производится по всем соседям, за исключением самой вершины. Чтобы исправить это, мы добавляем петлю в матрицу смежности, складывая ее с единичной матрицей. Мы получаем (A + I) X, но нам не хватает нормировки. Чтобы ввести ее, мы добавляем деление на степени всех вершин. Таким образом, мы формируем диагональную степенную матрицу D и умножаем наше выражение на D в степени –1/2 с левой и правой стороны. Затем мы производим знакомые нам операции: добавляем умножение на обучаемую матрицу весов W и слагаемое смещения b. Мы добавляем поверх этого нелинейность — и вуаля! Уравнение прямого распространения ГСС готово.
Теперь у нас есть все составляющие для реализации графовой нейронной сети с использованием библиотеки Spektral Keras/Tensorflow. В листинге 11.4 мы начинаем с импорта набора данных, который можно найти в папке данных репозитория кода на GitHub. Мы подготавливаем данные к обработке и определяем архитектуру графовой нейронной сети. Далее мы обучаем модель и отображаем результаты. Вам стоит попробовать запустить этот код в режиме блокнота на платформе Google Colab (доступна по ссылке https://colab.research.google.com/), чтобы посмотреть на результаты пошагового исполнения, а также ускорить процесс обучения модели при помощи графического процессора.
Графовая сверточная нейронная сеть для классификации графов цитирования
import numpy as np
import pandas as pd
import tensorflow as tf
from tensorflow import keras
import networkx as nx
from tensorflow.keras.utils import to_categorical
from sklearn.preprocessing import LabelEncoder
from sklearn.utils import shuffle
from sklearn.metrics import classification_report
from sklearn.model_selection import train_test_split
from spektral.layers import GCNConv
from tensorflow.keras.models import Model
from tensorflow.keras.layers import Input, Dropout, Dense
from tensorflow.keras import Sequential
from tensorflow.keras.optimizers import Adam
from tensorflow.keras.callbacks import TensorBoard, EarlyStopping
from tensorflow.keras.regularizers import l2
import os
from collections import Counter
from sklearn.manifold import TSNE
import matplotlib.pyplot as plt
tf.keras.utils.set_random_seed(42)
SAVE_PATH = "/content/drive/MyDrive/Colab Notebooks/data/"
DATA_PATH = "/content/drive/MyDrive/data/cora/"
column_names = ["paper_id"] + [f"term_{idx}" for idx in range(1433)] +
➥ ["subject"]
node_df = pd.read_csv(DATA_PATH + "cora.content", sep="t", header=None,
➥ names=column_names)
print("Node df shape:", node_df.shape)
edge_df = pd.read_csv(DATA_PATH + "cora.cites", sep="t", header=None,
➥ names=["target", "source"])
print("Edge df shape:", edge_df.shape)
nodes = nodedf.iloc[:,0].tolist()Парсинг данных вершин
labels = node_df.iloc[:,-1].tolist()
X = node_df.iloc[:,1:-1].values
X = np.array(X,dtype=int)
N = X.shape[0] # количество вершин
F = X.shape[1] # размер признаков вершин
edge_list = [(x, y) for x, y in zip(edge_df['target'],
➥ edge_df['source'])] Парсинг данных ребер
num_classes = len(set(labels))
print('Number of nodes:', N)
print('Number of features of each node:', F)
print('Labels:', set(labels))
print('Number of classes:', num_classes)
def sample_data(labels, limit=20, val_num=500, test_num=1000):
label_counter = dict((l, 0) for l in labels)
train_idx = []
for i in range(len(labels)):
label = labels[i]
if label_counter[label]<limit:
# добавление примера к обучающим данным
train_idx.append(i)
label_counter[label]+=1
# выход из цикла при нахождении 20 примеров каждого класса
if all(count == limit for count in label_counter.values()):
break
# получение индексов, не попадающих в обучающие данные
rest_idx = [x for x in range(len(labels)) if x not in train_idx]
# получение val_num первых индексов
val_idx = rest_idx[:val_num]
test_idx = rest_idx[val_num:(val_num+test_num)]
return train_idx, val_idx,test_idx
train_idx,val_idx,test_idx = sample_data(labels)
# назначение маски
train_mask = np.zeros((N,),dtype=bool)
train_mask[train_idx] = True
val_mask = np.zeros((N,),dtype=bool)
val_mask[val_idx] = True
test_mask = np.zeros((N,),dtype=bool)
test_mask[test_idx] = True
print("Training data distribution:n{}".format(Counter([labels[i] for i in
➥ train_idx])))
print("Validation data distribution:n{}".format(Counter([labels[i] for i
➥ in val_idx])))
def encode_label(labels):
label_encoder = LabelEncoder()
labels = label_encoder.fit_transform(labels)
labels = to_categorical(labels)
return labels, label_encoder.classes_
labels_encoded, classes = encode_label(labels)
G = nx.Graph()Построение графа
G.add_nodes_from(nodes)
G.add_edges_from(edge_list)
A = nx.adjacency_matrix(G) Получение матрицы смежности
print('Graph info: ', nx.info(G))
# Параметры
channels = 16 Количество каналов в первом слое
dropout = 0.5 Коэффициент прореживания признаков
l2_reg = 5e-4 Коэффициент регуляризации L2
learning_rate = 1e-2 Скорость обучения
epochs = 200 Количество эпох обучения
es_patience = 10 Параметр терпения для раннего останова
# Операции предварительной обработки
A = GCNConv.preprocess(A).astype('f4')
# Определение модели
X_in = Input(shape=(F, ))
fltr_in = Input((N, ), sparse=True)
dropout_1 = Dropout(dropout)(X_in)
graph_conv_1 = GCNConv(channels,
activation='relu',
kernel_regularizer=l2(l2_reg),
use_bias=False)([dropout_1, fltr_in])
dropout_2 = Dropout(dropout)(graph_conv_1)
graph_conv_2 = GCNConv(num_classes,
activation='softmax',
use_bias=False)([dropout_2, fltr_in])
# Построение модели
model = Model(inputs=[X_in, fltr_in], outputs=graph_conv_2)
model.compile(optimizer=Adam(learning_rate=learning_rate),
loss='categorical_crossentropy',
weighted_metrics=['accuracy'])
model.summary()
# Обучение модели
validation_data = ([X, A], labels_encoded, val_mask)
hist = model.fit([X, A],
labels_encoded,
sample_weight=train_mask,
epochs=epochs,
batch_size=N,
validation_data=validation_data,
shuffle=False,
callbacks=[
EarlyStopping(patience=es_patience, restore_best_weights=True)
]) Обучение модели
# Оценка модели
X_test = X
A_test = A
y_test = labels_encoded
y_pred = model.predict([X_test, A_test], batch_size=N) Оценка модели
report = classification_report(np.argmax(y_test,axis=1),
➥ np.argmax(y_pred,axis=1), target_names=classes)
print('GCN Classification Report: n {}'.format(report))
layer_outputs = [layer.output for layer in model.layers]
activation_model = Model(inputs=model.input, outputs=layer_outputs)
activations = activation_model.predict([X,A],batch_size=N)
# Получение результатов t-SNE
# получение представления скрытого слоя после первого слоя ГСС
x_tsne = TSNE(n_components=2).fit_transform(activations[3])
def plot_tSNE(labels_encoded,x_tsne):
color_map = np.argmax(labels_encoded, axis=1)
plt.figure(figsize=(10,10))
for cl in range(num_classes):
indices = np.where(color_map==cl)
indices = indices[0]
plt.scatter(x_tsne[indices,0], x_tsne[indices, 1], label=cl)
plt.legend()
plt.show()
plot_tSNE(labels_encoded,x_tsne)
plt.figure()
plt.plot(hist.history['loss'], 'b', lw=2.0, label='Обучение')
plt.plot(hist.history['val_loss'], '--r', lw=2.0, label='Валидация')
plt.title('ГНС-модель')
plt.xlabel('Эпоха')
plt.ylabel('Потери кросс-энтропии')
plt.legend(loc='upper right')
plt.show()
plt.figure()
plt.plot(hist.history['accuracy'], 'b', lw=2.0, label='Обучение')
plt.plot(hist.history['val_accuracy'], '--r', lw=2.0, label='Валидация')
plt.title('ГНС-модель')
plt.xlabel('Эпоха')
plt.ylabel('Доля верных результатов')
plt.legend(loc='upper left')
plt.show()
На рис. 11.13 показаны потери классификации и верность результатов для обучающего и валидационного наборов данных:
На рис. 11.14 показано представление скрытого слоя ГСС, визуализированное с помощью метода t-SNE, для набора данных CORA:
Оформить предзаказ на книгу «Алгоритмы машинного обучения» со скидкой 30%
можно на нашем сайте по промокоду – Manning
Автор: ph_piter


