Рубрики
Без рубрики

Практическое руководство по RNNS для исследования нейробиологии в Кере

Введение Рекуррентные модели нейронных сетей все больше становятся мощными моделями для … Теги с Python, Keras, MachineLearning, Neuroscience.

Рекуррентные модели нейронных сетей все чаще становятся мощными моделями для исследования нейробиологии, когда мы видели на встрече в этом году в этом году. Они могут быть использованы для E.g. Создание моделей данных для методов анализа тестирования ; или Выведите подключение между зонами мозга на основе реальных нейральных данных Отказ

Несмотря на их очевидное практическое использование, иногда может быть трудно сделать прыжок из письменной модели RNN в документе до практического применения в коде. Более того, хотя каркасы облегчили проще, чем когда-либо для прототипов моделей машинного обучения, в нейробиологических исследованиях мы часто заинтересованы в статистике сети, отличной от стандартных выходов, таких как точность и курс обучения – мы можем вместо этого хотеть извлечь активацию слоя и веса на разных этапах Во время модели обучения как прокси для реальной нейронной активности и подключения.

Это руководство является попыткой разработки и объяснения некоторых простых примеров RNN в рамках KERAS, которые вдохновлены и применимы к неврологии.

Colab Notebooks

Пример 1 – Мнист Пример 2 – Генерация данных Пример 3. – подключение

Чтобы показать общую структуру РНН в Кере, мы начнем с классического примера Mnist. Здесь мы получим меченную последовательность изображений рисованной цифры и обучить модель RNN, чтобы предсказать представленную цифру на изображении:

В Python давайте начнем с необходимого импорта и загрузки данных Mnist:

import numpy as np
from tensorflow import keras
from keras.callbacks import ModelCheckpoint
from matplotlib import pyplot as plt
from keras import backend as K

# load the dataset
mnist = keras.datasets.mnist
(x_train, y_train), (x_test, y_test) = mnist.load_data()
x_train, x_test = x_train / 255.0, x_test / 255.0
sample, sample_label = x_train[0], y_train[0]

Здесь мы просто загружаем стандартный набор данных Mnist из библиотеки KERAS и разделили его на поездов и наборов данных тестирования. X_TRAIN и X_TEST являются входными данными (последовательности изображений), Y_Train и Y_Test являются целевыми метками (цифры от 0 до 9).

Как чек здравомыслие, давайте построим некоторые из входных изображений вместе со своими этикетками. Закрепляя первые 10 примеров в тренировочном наборе данных и построения с этикетками, которые мы получаем:

# show examples
n_examples = 10
plt.figure()
for i in range(n_examples):
   plt.subplot(1, n_examples, i+1)
   plt.imshow(x_train[i])
   plt.title(y_train[i])
plt.show()

Все идет нормально. Теперь давайте определим некоторые параметры модели и построить фактическую модель:

# model parameters
input_dim = x_train[0].shape[0]
output_size = 10
epochs = 10
units = 64

# build model
model = keras.models.Sequential()
model.add(keras.layers.SimpleRNN(units, input_shape=(input_dim, input_dim)))
model.add(keras.layers.BatchNormalization())
model.add(keras.layers.Dense(output_size))

model.summary()

Сначала мы определили параметр входного измерения input_dim на основе входной формы изображения (28×28 пикселей). Затем мы устанавливаем определить вывод_size. Так как мы хотим 10 классов классификации (цифры 0-9). эпохи Определяет количество повторений обучения, а единицы Это количество нейронов, которое мы хотим в нашем RNN.

Затем мы используем последовательный интерфейс KERAS ‘Sequental Model, чтобы стекировать слои сети друг на друга. Сначала мы добавляем стандартный уровень RNN с Simplernn Затем слой нормализации для вывода и, наконец, полностью подключенный плотный выходной слой с 10 узлами. Вызов Model.Summary () должен распечатать:

Model: "sequential"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
simple_rnn (SimpleRNN)       (None, 64)                5952      
_________________________________________________________________
batch_normalization (BatchNo  (None, 64)                256       
_________________________________________________________________
dense (Dense)                (None, 10)                650       
=================================================================
Total params: 6,858
Trainable params: 6,730
Non-trainable params: 128

Давайте наступим через один слой, чтобы понять сеть, которую мы создали. У нас есть Simplernn слой с выходной формой (Нет, 64) и 5952 общих параметра. Откуда приходит информация о форме формы? Помните, что мы создали наш уровень RNN с 64 нейронами, поэтому вывод 64 в одном измерении формы имеет смысл. Измерение первой формы, показывающие Нет Соответствует пакетному или испытанному номеру, то есть количество примеров, которые мы будем кормить сетью по тренировку. KERAS позволяет нам расплывчатывать с этим номером и представлять его как Нет Так что мы можем легко кормить наборы данных различных размеров, не нарушая входные размеры слоя. Мы воспользовались этим фактом перед линейкой:

model.add(keras.layers.SimpleRNN(units, input_shape=(input_dim, input_dim)))

Входная форма RNN в KERAS должна иметь 3 измерения: партия, Timestep, функция, но мы только предоставили 2 DIMS из ввода формы. Это связано с тем, что измерение партии подразумевается KERAS, предполагая, что мы будем кормить в наборах данных различной длины.

Как насчет количества параметров для уровня RNN? У нас есть 64 единица в RNN, которые постоянно подключены (все связаны друг с другом), что дает нам важные параметры. Наш вход изображений составляет 28х28 пикселей, которые RNN представляет собой как 28 последовательностей из 28 функций, поэтому 28 функций каналов передаются на 64 RNN, дающие дополнительные параметры. Всего сейчас у нас есть параметры. Наконец, у нас есть 64 условия смещения для блоков RNN, давая конечное число параметра 5952.

Обучение модели в KERAS аналогично простым, нам просто нужно добавить:

# train
model.compile(
   loss=keras.losses.SparseCategoricalCrossentropy(from_logits=True),
   optimizer="sgd",
   metrics=["accuracy"]
)

history = model.fit(
   x_train, y_train, validation_data=(x_test, y_test), batch_size=1000, epochs=epochs
)

Мы используем Компилировать Функция для определения функции обучения потери, оптимизатор и наши выходные метрики. Поскольку мы предсказываем категорическую переменную (цифру), мы используем редкую категорическую кросс-энтропию здесь для функции потери. Наш оптимизатор является стандартным стохастическим градиентом спуском, и мы просим обучение отслеживать точность (% испытаний, модель прогнозирует цифру от входного изображения). Наконец мы называем model.fit () С помощью учебных и валидационных испытаний. Оставив это на тренировку, мы увидим, что некоторые вывода появляются:

Epoch 1/10
60/60 [==============================] - 3s 29ms/step - loss: 2.2067 - accuracy: 0.2987 - val_loss: 1.7675 - val_accuracy: 0.4677
Epoch 2/10
60/60 [==============================] - 1s 20ms/step - loss: 1.3351 - accuracy: 0.5689 - val_loss: 1.3675 - val_accuracy: 0.6531
Epoch 3/10
60/60 [==============================] - 1s 18ms/step - loss: 0.9991 - accuracy: 0.6946 - val_loss: 1.0370 - val_accuracy: 0.7391
Epoch 4/10
60/60 [==============================] - 1s 19ms/step - loss: 0.8169 - accuracy: 0.7563 - val_loss: 0.8105 - val_accuracy: 0.7897
Epoch 5/10
60/60 [==============================] - 1s 20ms/step - loss: 0.6963 - accuracy: 0.7922 - val_loss: 0.6728 - val_accuracy: 0.8142
Epoch 6/10
60/60 [==============================] - 1s 20ms/step - loss: 0.6205 - accuracy: 0.8149 - val_loss: 0.5768 - val_accuracy: 0.8389
Epoch 7/10
60/60 [==============================] - 1s 20ms/step - loss: 0.5545 - accuracy: 0.8340 - val_loss: 0.5140 - val_accuracy: 0.8552
Epoch 8/10
60/60 [==============================] - 1s 20ms/step - loss: 0.5008 - accuracy: 0.8500 - val_loss: 0.4637 - val_accuracy: 0.8644
Epoch 9/10
60/60 [==============================] - 1s 20ms/step - loss: 0.4618 - accuracy: 0.8630 - val_loss: 0.4319 - val_accuracy: 0.8741
Epoch 10/10
60/60 [==============================] - 1s 19ms/step - loss: 0.4256 - accuracy: 0.8751 - val_loss: 0.4006 - val_accuracy: 0.8813

После 10 эпох у нас есть модель, которая может предсказать цифру из изображения с> 80% точностью, довольно хорошим!

В качестве здравого чека давайте добавим некоторый код с помощью модели для прогнозирования цифры из одного из наших изображений набора данных:

# prediction example
test_image = x_train[0]
test_image = test_image[np.newaxis, :]
y_pred = model.predict(test_image)[0]
plt.figure()
plt.imshow(test_image[0])
plt.title(np.argmax(y_pred))
plt.show()

Какие выходы:

Мы создали переменную test_image С первым изображением от x_train Отказ Поскольку это 2D-изображение, и модель RNN ожидает дополнительного измерения партии, которое мы прокладываем изображение с пустым дополнительным измерением в следующей строке. Затем мы генерируем прогноз y_pred. Использование Model.Predict () С нашей test_image Отказ Далее мы загружаем изображение и название его прогнозированием вывода. Помните, что вывод модели представляет собой 10-элементное вектор, где каждый элемент представляет собой вероятность прогнозирования каждого из цифр 0-9. Чтобы получить фактическую прогнозируемую цифру, мы используем np.argmax () Чтобы найти элемент с наибольшей вероятностью. Как получается, это хорошо соответствует входным изображении!

Если вы невростаристы, и RNN – это «мозг», который вы изучаете, вам может не заботиться о точности модели модели. Ведь мы уже знаем, что наш мозг может распознать и классифицировать эти цифры изображения. Вместо этого вы можете больше заинтересованы в том, как – как сетевые нейроны ведут себя во время презентации каждого изображения. В искусственной нейронной сети активации слоя могут рассматриваться как аналогичные нейронные активности в мозге. Активации являются выходами нейронов в слое после получения их ввода от предыдущего слоя.

Если вы углубляетесь в свойства модели, которую вы только что обучали, вы заметите, что активации для каждого слоя не хранятся. Это имеет смысл, поскольку все, что нам нужно хранить для модельной подготовки, – это входные, выходные и обновленные веса между слоями. Хранение активаций для каждого этапа тренировок и ввода будет стоить много памяти. Чтобы получить активации слоя для определенного ввода изображения, нам нужно сделать дополнительную магию KERAS. Торчащий с test_image Мы уже определены, мы добавим некоторый код для изучения активации:

# activation example
layer_idx = 0
out_func = K.function([model.input], [model.layers[layer_idx].output])
out_vals = out_func([test_image])[0]
print(out_vals)

Давая нам выход:

[[ 0.56697565  0.09797323 -0.89703596  0.5188298  -0.12455866  0.047839
  -0.7718987  -0.8179464   0.51488703 -0.3178563  -0.13602903  0.7039628
  -0.22956386  0.23199454  0.49808362 -0.1646781   0.18231589 -0.52438575
  -0.7650064   0.26156113 -0.14623232  0.81333166  0.3180512  -0.4301887
  -0.8027709   0.07813827  0.41824704 -0.8176996   0.02754677  0.2746857
   0.64864177  0.59684217  0.51136965  0.6604145  -0.25604498  0.30178267
   0.31990722 -0.7244299   0.78560436 -0.42247573 -0.16835652 -0.197974
   0.1738112   0.61906195 -0.69502765  0.3859463  -0.09267779  0.27790046
   0.09295665 -0.07516889  0.83438504 -0.15787967 -0.553465   -0.67424375
   0.06541198 -0.1020548  -0.7939734  -0.09875299  0.20282765  0.63470924
  -0.33998007 -0.04162058 -0.33605504 -0.15319426]]

Здесь мы установили Sayer_idx 0 (соответствует первому слою в нашей сети, RNN). Затем мы использовали библиотеку BackeND KERAS, чтобы определить функцию Keras под названием out_func Это использует вход модели для получения вывода модели в индексе слоя, которую мы предоставляем. Затем мы используем эту функцию с нашими test_image Отказ Выходная мощность представляет собой 64-элементное вектор, соответствующее активациям наших 64 нейронов RNN после того, как его представлены с помощью тестового изображения.

В рамках бумаги 2018 года «неповторимое открытие демиксированной, низкоразмерной нейронной динамики на нескольких временах благодаря анализу тензорного компонента», Williams et al. Используйте статистику RNN для проверки утилиты их метода анализа компонентов тензора. Преимущество использования Ant здесь состоит в том, что у них есть доступ к земле истине активации слоя и веса подключения для сравнения от результатов метода TCA.

Стратегия генерации данных этой статьи показана ниже, воспроизведена из 3-го рисунка бумаги:

Вход в сеть составляет 40 сроков случайных шумов, с некоторым смещением так, что он имеет немного отрицательное или слегка положительное среднее значение во времени. Сеть обучается, чтобы сообщить о знаке ввода с большой величиной. Две правые панели показывают активацию во времени (+) и (-) адаптивной ячейки в RNN до и после тренировки. Со временем их ответы расходятся для получения положительного или отрицательного привода к выходной ячейке.

Давайте воссоздать эту модель в Кере.

Сначала нам нужно создать входные данные и ее целевые метки для обучения. Помните, что RNN в KERAS ожидает 3D-образных данных с размерами, соответствующими пакетной, Timestep, функции. Каждый вход имеет 40 Timesteps единой функции, и мы сделаем 100 000 учебных образцов, поэтому нам нужны 100 000x40x1 входных данных. Каждый вход нуждается в метке для тренировки модели (общий знак ввода), которые нуждается в одном положительном или отрицательном номере. Чтобы выполнить необходимый импорт и определить этот набор данных:

import numpy as np
from tensorflow import keras
from keras.callbacks import ModelCheckpoint
from matplotlib import pyplot as plt
from keras import backend as K

# create the dataset
data_n = 100000
timepoints = 40
sign_vec = (np.random.randint(2, size=(data_n, 1)) * 2) - 1

input = np.random.rand(data_n, timepoints) - 0.5  # random values centered around 0
input = input + (sign_vec * 0.2)
input = input[:, :, np.newaxis]  # reshape for RNN
output = sign_vec * 3

# plot the dataset
plt.figure()
for i in range(100):
   c = 'k' if sign_vec[i] == 1 else 'r'
   plt.plot(input[i, :], c)

plt.show()

Давайте выйдем через этот код. Сначала мы создаем переменную sign_vec генерировать случайную последовательность положительных или отрицательных чисел. Мы используем Numpy’s Рэннт Функция для создания 100 000 случайных целых чисел 0 и 1, затем масштабируется их так, чтобы мы получили либо -1 или 1. Далее мы определяем вход Как матрица случайных чисел размером 100000х40 со средним нулем. Затем мы добавляем sign_vec последовательность для перемещения каждой записи слегка положительный или отрицательный в соответствии с элементами в sign_vec Отказ Мы добавляем дополнительное измерение в вход Таким, что он удовлетворяет входным требованиям для нашего возможного RNN. Наконец, мы генерируем этикетки вывода для обучения, просто масштабируя sign_vec. Таким образом, мы получаем либо -3 или +3 метку. Конечный результат представляет собой набор данных из 40-элементных векторов, которые немного положительны или отрицательны, а соответствующая метка, описывающая знак. Последний код сегмента кода первые 100 примеров этого набора данных и цветов по знаку:

Далее нам нужно построить модель:

# build model
units = 50
model = keras.models.Sequential()
# have to return sequences to get the actual unrolled response across time
model.add(keras.layers.SimpleRNN(units, return_sequences=True, input_shape=(timepoints, 1)))
model.add(keras.layers.Dense(1))
model.summary()

Это очень похоже на нашу предыдущую модель с несколькими ключевыми корректировками. На этот раз мы будем использовать 50 единиц и определить Simplernn Слой с входной формой TimePointSX1 (40×1), чтобы соответствовать данным входящих тренировок. Обратите внимание, что мы добавили дополнительный аргумент для Simplernn Слой – return_sefence = Правда Отказ Когда мы предоставляем входные данные на RNN с несколькими временами TimePoints, внутренне RNN вычисляет вывод для первого Timestep и прохождение того, что вывод в качестве ввода для следующего расчета Timestep. Если return_sequences это Ложь Мы проходим только последнее состояние этих рецидивирующих расчетов времени. В нашем случае мы хотим увидеть активации RNN в каждом пункте времени, поэтому мы устанавливаем это на Правда Чтобы получить доступ к данным о RNN на каждом этапе последовательности. Наконец, мы добавляем единый блок густой слоя в качестве нашего сетевого вывода. Нам нужна только одна единица, поскольку наши тренировочные метки – это просто одно положительное или отрицательное число. Вызов Model.Summary () дает:

Model: "sequential"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
simple_rnn (SimpleRNN)       (None, 40, 50)            2600      
_________________________________________________________________
dense (Dense)                (None, 40, 1)             51        
=================================================================
Total params: 2,651
Trainable params: 2,651
Non-trainable params: 0

В нашем предыдущем примере Mnist мы проанализировали сеть только после того, как она закончила обучение. В этом случае мы хотим сравнить сеть до и после тренировки. Обучение модели в KERAS не сохраняет автоматическую структуру модель на каждом этапе обучения, поэтому нам нужно добавить дополнительный шаг здесь, чтобы периодически сохранить снимок нашей модели через обучение:

# set a history callback to save weights after each epoch
weights_file_prefix = "tmp/model_"
model.save(weights_file_prefix + "untrained.h5")
checkpoint = ModelCheckpoint(weights_file_prefix + "{epoch}.h5", save_freq='epoch')

Все, что мы здесь делаем, определяют путь Сохранить и использовать модель .save () Чтобы сохранить случайно инициализированные веса модели до начала тренировки. Тогда мы определяем ModelCheckPoint из модуля обратных вызовов KERAS. Все это делает, предоставляет функцию обратного вызова для процедуры обучения, которая сохраняет модельный снимок каждая эпоха.

Теперь мы можем перейти к обучению модели:

# train
epochs = 10
model.compile(
   loss="mse",
   optimizer="sgd",
   metrics=["mse"]
)

history = model.fit(
   input, output, batch_size=1000, epochs=epochs, callbacks=[checkpoint]
)

Это снова очень похоже на наш пример тренировки Mnist с небольшими корректировками. Во-первых, в этом случае мы действительно не заботимся о выходе, являемся ровно -3 или +3, мы просто хотим, чтобы это перескочить эти значения – поэтому мы просто используем среднюю ошибку в квадрате («MSE») как функцию потери, так и метрика в model.compile () Отказ В model.fit () Мы также добавляем наши ранее определенные ModelCheckPoint к тренировочным обратным вызовам.

После того, как модель закончила тренировку, мы хотели бы увидеть, воспроизводят ли мы невронное поведение от Williams et al. Для этого нам необходимо извлечь активацию нейрона во времени в ответ на положительный, либо отрицательный стимул:

# try a prediction on the trained network
n_examples = 100
neuron = 0

plt.figure()
for i in range(n_examples):
   input_test = input[i][np.newaxis, :, :]
   y_pred = model.predict(input_test)

   # get activations for rnn layer
   out_func = K.function([model.input], [model.layers[0].output])
   out_vals = out_func([input_test])
   out_activation = out_vals[0][0, :, :]

   c = 'k' if sign_vec[i] == 1 else 'r'
   plt.plot(out_activation[:, neuron], c)
plt.ylim(-1, 1)
plt.show()

Здесь мы зацикливаемся через 100 примеров учебных данных и используем хитрость извлечения активации, чтобы вовремя получить активацию для одного из нейронов в RNN. Затем мы зарабатываем время активации и цвет под знаком входного вектора:

Хорошо! Этот нейрон расходится в своей активности со временем в зависимости от знака ввода, как в статье.

Далее Давайте повторим этот процесс для неподготовленной сети. Для этого мы загрузим модельный снимок ранее в тренировке и применяем тот же процесс:

# repeat for the untrained network
model.load_weights(weights_file_prefix + "untrained.h5")
plt.figure()
for i in range(n_examples):
   input_test = input[i][np.newaxis, :, :]
   y_pred = model.predict(input_test)

   # get activations for rnn layer
   out_func = K.function([model.input], [model.layers[0].output])
   out_vals = out_func([input_test])
   out_activation = out_vals[0][0, :, :]

   c = 'k' if sign_vec[i] == 1 else 'r'
   plt.plot(out_activation[:, neuron], c)
plt.ylim(-1, 1)
plt.show()

Это точно так же, как на предыдущем шаге, но мы добавили model.load_wees () С первой моделью снимка мы сформировались до начала обучения. Запуск этого мы получаем:

Это реплицирует аналогичный результат в статье, в котором тот же нейрон, перед тренировкой, имеет перекрытие ответов на два типа ввода.

С определенными методами нейробиологии (например, визуализация 2p) мы можем генерировать богатые данные о деятельности групп нейронов со временем и в ответ на различные стимулы. Во многих случаях нам также хотелось бы сделать что-то вывод о подключении этих нейронов, чтобы понять архитектуру схемы, в которой они работают.

Без электронного микроскопа или парных записей, как мы могли подумать о том, чтобы сделать это с набором данных? Один из подходов может быть для моделирования наборов нейронов с группами RNNS. Использование реальных данных из нейрональных записей мы могли бы обучить РННН, чтобы соответствовать их ответам на реальные нейроны в ответ на некоторые стимулы. После того, как ответы достаточно подобны между настоящими нейронами и RNN, мы можем проанализировать весы соединения в RNN, чтобы вывести подключение между реальными нейронами.

В этом примере мы представим, что мы можем одновременно изображать два предполагаемого соединения нейронов. Мы также можем стимулировать определенные подгруппы одного из нейрональных популяций. Поэтому мы можем наблюдать эффект различных сорваний стимуляции на первом населении на выпуске второго населения. Для моделирования этой ситуации мы будем генерировать сеть «учителя» вместо настоящих нейрональных записей. Эта сеть будет 2-слойным RNN с известными весами соединения, которые мы будем кормить случайным входом в первом слое для получения серии выходов во втором слое. Нам не нужно тренировать эту сеть, поскольку она просто будет служить целью создания набора данных ввода из сети с известными весами. Мы будем использовать этот набор данных для обучения второй сети с той же архитектурой – «Студенческая сеть». Эта сеть научится воспроизводить входные разные отношения сеть учителей по процедуре обучения. Когда студенческая сеть производит сопоставимые ответы на сеть учителей, мы можем извлечь свои весы подключения и посмотреть, правильно ли он выводят соединение сети учителей.

Архитектура как учителей, так и студенческих сетей представлена ниже:

У нас есть вход RNN, в который мы кормите входные данные. Это полностью подключено (представлено черной стрелкой) до 2-го RNN, который производит наш выход.

На основании того, что мы сделали до сих пор, это кажется простой построить эту сеть; Мы могли бы написать что-то вроде этого:

neurons = 10

model = keras.models.Sequential()
model.add(layers.SimpleRNN(neurons, return_sequences=True, input_shape=(1, neurons)))
model.add(layers.SimpleRNN(neurons, input_shape=(1, neurons)))
model.summary()

Это действительно будет производить полностью подключенный 2-слойный структуру RNN. Мы также устанавливаем return_sefence = Правда На первом слое для обеспечения всех последовательностей входных данных передаются на второй слой. Однако, если мы вызовем Model.Summary () Мы столкнемся к проблеме:

Model: "sequential"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
simple_rnn (SimpleRNN)       (None, 1, 10)             210       
_________________________________________________________________
simple_rnn_1 (SimpleRNN)     (None, 10)                210       
=================================================================
Total params: 420
Trainable params: 420
Non-trainable params: 0

Для 10 нейронов в первом уровне RNN мы ожидаем иметь рецидивирующие соединения, которые дают 100 параметров. Оставшиеся 110 параметров поступают из соединений между RNN и входным слоем, + термины смещения для каждого соединения. Хотя мы не явно не определяли его, входной слой выводится KERAS в нашей последовательной модели. Так как мы устанавливаем наши input_shape = (1, нейроны) Мы сказали KERA, чтобы ожидать, что 10-элементный вход. Этот входной слой также полностью подключен к нашему первому уровню RNN по умолчанию в KERAS, поэтому мы заканчиваем дополнительные соединения 10×10 (+ 10 условий смещения).

Обычно это не будет проблемой, но в нашем случае мы хотим точно контролировать начальную активность в нашем первом уровне RNN, поскольку в нашей модели вход соответствует прямой стимуляции нейронов в этом слое. Если входные слои и уровни RNN полностью подключены, как в настоящее время является случай, будет перекрестный разговор между входными элементами и их целевыми нейронами, как схематичен ниже:

В полностью подключенном случае каждый нейрон в 2-м слое представляет собой продукт всех 4 входных элемента. Для нашей модели мы хотим входной слой и первый уровень RNN, чтобы использовать образец один на один, чтобы мы могли напрямую контролировать активацию RNN. К сожалению, нет никакого внебладового способа определить эти связи с KERA, поэтому мы должны будем думать о обходном пути. Давайте сначала реализовать это с более простым корпусом – подключение к нерегушеру.

Начнем с создания этой простой модели:

neurons = 4
model = keras.models.Sequential()
model.add(layers.Dense(neurons, input_shape=(1, neurons)))
model.summary()
Model: "sequential"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
dense (Dense)                (None, 1, 4)              20        
=================================================================
Total params: 20
Trainable params: 20
Non-trainable params: 0

Мы видим, что вход полностью подключен к выходу, поскольку у нас есть 20 параметров в плотном слое (4×4 входные соединения + 4 условия смещения). Мы также можем увидеть, что вход не совсем воспроизводится в выходном плотном слое из-за этого подключения с быстрым тестом:

print(model.predict(np.ones(1, 4)))
[[-0.79105026  0.09893554  1.4398389  -0.5700609 ]]

Вес между 4 входными элементами и 4 выходами в плотном выходном уровне представлены матрицей 4×4, описывающей каждый вес соединения. Что, если мы вручную заменили эту матрицу с помощью матрицы идентичности 4×4? (Матрица идентификации 1 на диагонали и 0 везде еще, матрица, умноженная на матрицу идентификатора одного и того же размера, равна исходной матрице):

# force weights, kernel weights are identity matrix
weights = model.layers[0].get_weights()
input_weights = np.eye(neurons, neurons)
bias_weights = weights[1]
model.layers[0].set_weights([input_weights, bias_weights])

Выше мы получаем вес от первого слоя матрицы (входные весы и условия смещения). Затем мы устанавливаем первые входные весы слоя в качестве матрицы идентичности и сохраняйте смещение веса одинаково. Если мы повторим тот же тест:

print(model.predict(np.ones((1, 4))))
[[1. 1. 1. 1.]]

У нас есть желаемый результат! Наш входной слой непосредственно представлен на нашем выходе.

Давайте теперь напишем какой-то код для моделирования процесса обучения в этой простой сети и дайте нам некоторые выходные метрики

# create a dummy dataset
data_n = 10000
train_x = np.random.rand(data_n, 1, 4)
train_y = train_x + 1

print('pretrain weights: ', model.weights[0], model.weights[1])

# train
model.compile(
   loss="mse",
   optimizer="sgd",
   metrics=["mse"]
)

history = model.fit(
   train_x, train_y, batch_size=64, epochs=10
)

print('trained weights: ', model.weights[0], model.weights[1])

Перед тренировкой мы получаем ожидаемый результат входных весов, соответствующих матрице идентичности:

print('pretrain weights: ', model.weights[0], model.weights[1])

pretrain weights:   

Но после тренировки:

print('trained weights: ', model.weights[0], model.weights[1])

trained weights:   

Они изменились! Хотя мы изначально устанавливаем эти веса к матрице идентичности, алгоритм обучения сдвинул их на каждую итерацию. KERAS позволяет нам скрывать определенные веса от процесса обучения с тренируемый = Ложь В определении слоя, но для случая RNN это будет скрывать бы повторяющиеся весы, а какие мы не хотим. Вместо этого мы воспользуемся пользовательскими ограничениями KERAS, которые мы можем направлять на определенные группы весов. Обычно эти ограничения будут использоваться для E.G. Регулярируйте или нормализуйте веса после обновлений веса, но здесь мы просто будем грубоваться весом нашим желаемым значениям:

# custom constraint to hold weights at user defined value
class HoldWeight(tf.keras.constraints.Constraint):
 """Constrains weight tensors to a set value/matrix"""
 def __init__(self, set):
     self.set = set

 def __call__(self, w):
   return self.set

Здесь мы создали новый класс, который наследует от Keras Ограничение сорт. Этот класс принимает аргумент Установить Какие будут наши желаемые значения веса. Мы переопределяем метод __call__ и вернуть наши желаемые веса вместо обновленных весов w .

Мы можем применить это к нашему плотному слою следующим образом:

model.add(layers.Dense(neurons, input_shape=(1, neurons), kernel_constraint=HoldWeight(np.eye(neurons, neurons))))

Применение того же теста на этой модели после тренировки:

print('trained weights: ', model.weights[0], model.weights[1])

trained weights:   

Мы видим, что наши входные веса остались в качестве матрицы идентичности, и только весы смещения были изменены.

Теперь мы можем поставить все это вместе и создать подход учителя-студент, который мы обсуждали ранее. Сначала создавая сеть учителей:

import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers
import numpy as np
import matplotlib.pyplot as plt
from keras.callbacks import ModelCheckpoint

# parameters
neurons = 10
n_generator = 1000000

# construct the teacher network
teacher_model = keras.models.Sequential()
teacher_model.add(layers.Input(shape=(1, neurons)))
teacher_model.add(layers.SimpleRNN(neurons, use_bias=False,
                                  return_sequences=True,
                                  kernel_constraint=HoldWeight(np.eye(neurons, neurons))))
teacher_model.layers[0].set_weights([np.eye(neurons, neurons),
                                    teacher_model.layers[0].get_weights()[1]])
teacher_model.add(layers.SimpleRNN(neurons, use_bias=False))
teacher_model.summary()

# generate teacher output for the training dataset
generator_input = np.random.rand(n_generator, 1, neurons)
generator_output = teacher_model.predict(generator_input)

# plot the first layer recurrent weights
plt.figure()
plt.imshow(teacher_model.weights[2], cmap='hot')
plt.show()

Мы определяем 2-слою сеть RNN и используем хитрость матрицы идентичности на первом слое, чтобы мы могли напрямую применить желаемый ввод на первый слой RNN. Далее мы создаем набор данных случайных 10-элемента (соответствующий 10 нейронам в входах RNN) в generator_input. . Мы кормите эти входы через сеть с Model.Predict () Чтобы дать сетевой вывод с 2-го RNN в Generator_output Отказ Наконец мы создаем тепловую карту, показывающую вес между рецидивирующими слоями с PESPERAL_MODEL. Weights [2] Отказ Мы используем индекс 2 здесь, так как веса перечислены в порядке внутри модели, входные веса являются Вес [0] , рецидивирующие весы в 1-м RNN являются Вес [1] Весы между RNNS являются Вес [2] Отказ

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

# set up the student
student_model = keras.models.Sequential()
student_model.add(layers.Input(shape=(1, neurons)))
student_model.add(layers.SimpleRNN(neurons,
                                  use_bias=False,
                                  return_sequences=True,
                                  kernel_constraint=HoldWeight(np.eye(neurons, neurons))))
student_model.layers[0].set_weights([np.eye(neurons, neurons),
                                    student_model.layers[0].get_weights()[1]])
student_model.add(layers.SimpleRNN(neurons, use_bias=False))
student_model.summary()

# set a history callback to save weights after each epoch
weights_file_prefix = "tmp/model_"
student_model.save(weights_file_prefix + "untrained.h5")
checkpoint = ModelCheckpoint(weights_file_prefix + "{epoch}.h5", save_freq='epoch')

# train
student_model.compile(
   loss="mse",
   optimizer="sgd",
   metrics=["mse", "accuracy"]
)

history = student_model.fit(
   generator_input, generator_output, batch_size=1000, epochs=100, callbacks=[checkpoint]
)

Это все должны выглядеть знакомыми сейчас. Архитектура сети точно такой же, как и сеть учителей. Мы определяем обратный вызов, чтобы сохранить модельные снимки в каждой эпоху, а затем составить модель и поезд на Generator_input , Generator_output набор данных.

Мы должны получить довольно хорошую производительность после наших 100 эпохов тренировок:

Epoch 98/100
1000/1000 [==============================] - 3s 3ms/step - loss: 1.3204e-05 - mse: 1.3204e-05 - accuracy: 0.9763
Epoch 99/100
1000/1000 [==============================] - 3s 3ms/step - loss: 1.2516e-05 - mse: 1.2516e-05 - accuracy: 0.9770
Epoch 100/100
1000/1000 [==============================] - 3s 3ms/step - loss: 1.1846e-05 - mse: 1.1846e-05 - accuracy: 0.9773

Наконец, давайте сравним вес RNN -> RNN весов в модели учителя, модель студента перед тренировкой, а после тренировки:

В каждом случае мы представляем прочность соединения между RNNS в качестве тепломапа, показывающей вес между каждой парой нейронов. Сеть учителей создала набор учебных наборов, поэтому эти веса – наша земля. Недоступная студенческая сеть имеет в значительной степени разнородную модель веса к сети учителей, в то время как обученная студенческая сеть имеет очень похожий шаблон для учителя. Следовательно, обучение студенческой сети на примерах ввода-выводах от учителя позволило нам выводить связь между слоями в нашей земле истине. Как упомянуто выше, мы можем представить эксперимент по нейробиологии, в котором мы заменяем набор данных сетевого учителя искусственного учителя с реальными записями из 2 популяций нейронов, стимулируя одно из популяций. Затем мы могли бы обучить студенческую сеть в этом наборе данных, чтобы вывести подключение между реальными нейронами.

Оригинал: “https://dev.to/robodoig/a-practical-guide-to-rnns-for-neuroscience-research-in-keras-1ad2”