Реализация обучаемого обобщенного функционального слоя Bump в Keras / Tensorflow - PullRequest
8 голосов
/ 27 марта 2020

Я пытаюсь закодировать следующий вариант функции Bump , примененной по компонентам:

generalized bump function equation,

где σ обучаемо; но это не работает (об ошибках сообщается ниже).


Моя попытка:

Вот то, что я до сих пор кодировал (если это помогает). Предположим, у меня есть две функции (например):

  def f_True(x):
    # Compute Bump Function
    bump_value = 1-tf.math.pow(x,2)
    bump_value = -tf.math.pow(bump_value,-1)
    bump_value = tf.math.exp(bump_value)
    return(bump_value)

  def f_False(x):
    # Compute Bump Function
    x_out = 0*x
    return(x_out)

class trainable_bump_layer(tf.keras.layers.Layer):

    def __init__(self, *args, **kwargs):
        super(trainable_bump_layer, self).__init__(*args, **kwargs)

    def build(self, input_shape):
        self.threshold_level = self.add_weight(name='threshlevel',
                                    shape=[1],
                                    initializer='GlorotUniform',
                                    trainable=True)

    def call(self, input):
        # Determine Thresholding Logic
        The_Logic = tf.math.less(input,self.threshold_level)
        # Apply Logic
        output_step_3 = tf.cond(The_Logic, 
                                lambda: f_True(input),
                                lambda: f_False(input))
        return output_step_3

Отчет об ошибке:

    Train on 100 samples
Epoch 1/10
WARNING:tensorflow:Gradients do not exist for variables ['reconfiguration_unit_steps_3_3/threshlevel:0'] when minimizing the loss.
WARNING:tensorflow:Gradients do not exist for variables ['reconfiguration_unit_steps_3_3/threshlevel:0'] when minimizing the loss.
 32/100 [========>.....................] - ETA: 3s

...

tensorflow:Gradients do not exist for variables 

Более того, он, кажется, не применяется по компонентам (помимо не обучаемой проблемы). В чем может быть проблема?

Ответы [ 3 ]

4 голосов
/ 30 марта 2020

К сожалению, никакая операция по проверке того, находится ли x в пределах (-σ, σ), не будет дифференцируемой и, следовательно, σ не может быть изучена с помощью любого метода градиентного спуска. В частности, невозможно вычислить градиенты относительно self.threshold_level, поскольку tf.math.less не дифференцируемо по отношению к условию.

Относительно поэлементного условия вы можете вместо этого использовать tf .where для выбора элементов из f_True(input) или f_False(input) в соответствии с компонентными логическими значениями условия. Например:

output_step_3 = tf.where(The_Logic, f_True(input), f_False(input))

ПРИМЕЧАНИЕ: Я ответил на основании предоставленного кода, где self.threshold_level не используется в f_True и f_False. Если self.threshold_level используется в этих функциях, как в представленной формуле, функция, конечно, будет дифференцируемой по self.threshold_level.

Обновлено 19/04/2020: Спасибо @ сегодня для уточнения .

3 голосов
/ 03 апреля 2020

Я предлагаю вам попробовать нормальный дистрибутив вместо удара. В моих тестах здесь эта функция bump не работает хорошо (я не могу найти ошибку, но не сбрасываю ее, но мой график показывает два очень острых удара, что не хорошо для сетей)

С при нормальном распределении вы получите обычный и дифференцируемый выступ, высотой, шириной и центром которого вы можете управлять.

Итак, вы можете попробовать эту функцию:

y = a * exp ( - b * (x - c)²)

Попробуйте в некотором графике и посмотрите, как он себя ведет.

Для этого:

class trainable_bump_layer(tf.keras.layers.Layer):

    def __init__(self, *args, **kwargs):
        super(trainable_bump_layer, self).__init__(*args, **kwargs)

    def build(self, input_shape):

        #suggested shape (has a different kernel for each input feature/channel)
        shape = tuple(1 for _ in input_shape[:-1]) + input_shape[-1:]

        #for your desired shape of only 1:
        shape = tuple(1 for _ in input_shape) #all ones

        #height
        self.kernel_a = self.add_weight(name='kernel_a ',
                                    shape=shape
                                    initializer='ones',
                                    trainable=True)

        #inverse width
        self.kernel_b = self.add_weight(name='kernel_b',
                                    shape=shape
                                    initializer='ones',
                                    trainable=True)

        #center
        self.kernel_c = self.add_weight(name='kernel_c',
                                    shape=shape
                                    initializer='zeros',
                                    trainable=True)

    def call(self, input):
        exp_arg = - self.kernel_b * K.square(input - self.kernel_c)
        return self.kernel_a * K.exp(exp_arg)

2 голосов
/ 19 апреля 2020

Я немного удивлен, что никто не упомянул основную (и единственную) причину данного предупреждения! Как представляется, этот код должен реализовывать обобщенный вариант функции Bump; однако, просто взгляните на функции, реализованные снова:

def f_True(x):
    # Compute Bump Function
    bump_value = 1-tf.math.pow(x,2)
    bump_value = -tf.math.pow(bump_value,-1)
    bump_value = tf.math.exp(bump_value)
    return(bump_value)

def f_False(x):
    # Compute Bump Function
    x_out = 0*x
    return(x_out)

Ошибка очевидна: в этих функциях не используется обучаемый вес слоя! Так что есть не удивительно, что вы получаете сообщение о том, что для этого не существует градиента: вы вообще его не используете, поэтому нет градиента для его обновления! Скорее, это именно оригинальная функция Bump (то есть без тренируемого веса).

Но вы можете сказать, что: «по крайней мере, я использовал тренируемый вес при условии tf.cond, поэтому необходимо быть какие-то градиенты ?! "; однако, это не так, и позвольте мне прояснить путаницу:

  • Прежде всего, как вы заметили, нас интересует поэлементное обусловливание. Таким образом, вместо tf.cond вам нужно использовать tf.where.

  • Другое неправильное представление состоит в том, чтобы утверждать, что, поскольку tf.less используется в качестве условия и поскольку оно не дифференцируемо, т.е. он не имеет градиента по отношению к своим входам (что верно: для функции с логическим выводом по сравнению с ее действительными значениями нет определенного градиента), то это приводит к выдаче предупреждения!

    • Это просто неправильно! Производная здесь будет взята из вывода слоя по отношению к обучаемому весу, а условие выбора НЕ присутствует в выходных данных. Скорее, это просто логический тензор, который определяет выходную ветвь, которая будет выбрана. Это оно! Производная условия не берется и никогда не понадобится. Так что это не причина для данного предупреждения; причина только в том, что я упомянул выше: никакой вклад обучаемого веса в вывод слоя. (Примечание: если пункт об условии вас немного удивляет, подумайте о простом примере: функция ReLU, которая определяется как relu(x) = 0 if x < 0 else x. Если производная условия, т.е. x < 0, считается / необходима, который не существует, то мы не сможем использовать ReLU в наших моделях и обучать их, используя методы градиентной оптимизации вообще!)

(Примечание: начиная с этого момента, я бы обозначил и обозначил пороговое значение как сигма , как в уравнении).

Хорошо! Мы нашли причину ошибки в реализации. Можем ли мы это исправить? Конечно! Вот обновленная рабочая реализация:

import tensorflow as tf
from tensorflow.keras.initializers import RandomUniform
from tensorflow.keras.constraints import NonNeg

class BumpLayer(tf.keras.layers.Layer):
    def __init__(self, *args, **kwargs):
        super(BumpLayer, self).__init__(*args, **kwargs)

    def build(self, input_shape):
        self.sigma = self.add_weight(
            name='sigma',
            shape=[1],
            initializer=RandomUniform(minval=0.0, maxval=0.1),
            trainable=True,
            constraint=tf.keras.constraints.NonNeg()
        )
        super().build(input_shape)

    def bump_function(self, x):
        return tf.math.exp(-self.sigma / (self.sigma - tf.math.pow(x, 2)))

    def call(self, inputs):
        greater = tf.math.greater(inputs, -self.sigma)
        less = tf.math.less(inputs, self.sigma)
        condition = tf.logical_and(greater, less)

        output = tf.where(
            condition, 
            self.bump_function(inputs),
            0.0
        )
        return output

Несколько замечаний относительно этой реализации:

  • Мы заменили tf.cond на tf.where, чтобы сделать элемент условное преобразование.

  • Далее, как вы можете видеть, в отличие от вашей реализации, которая проверяла только одну сторону неравенства, мы используем tf.math.less, tf.math.greater, а также tf.logical_and чтобы выяснить, имеют ли входные значения величины меньше sigma (альтернативно, мы могли бы сделать это, используя только tf.math.abs и tf.math.less; без разницы!). И давайте повторим это: использование функций логического вывода таким способом не вызывает никаких проблем и не имеет ничего общего с производными / градиентами.

  • Мы также используем ограничение неотрицательности на значении сигмы, изученной слоем. Почему? Поскольку значения сигмы меньше нуля не имеют смысла (т. Е. Диапазон (-sigma, sigma) плохо определен, когда сигма отрицательна).

  • И, учитывая предыдущий пункт, мы позаботимся об инициализации правильное значение сигмы (то есть небольшое неотрицательное значение).

  • А также, пожалуйста, не делайте такие вещи, как 0.0 * inputs! Это избыточно (и немного странно) и эквивалентно 0.0; и оба имеют градиент 0.0 (относительно inputs). Умножение нуля на тензор не добавляет ничего и не решает существующую проблему, по крайней мере, в этом случае!

Теперь давайте проверим, как это работает. Мы пишем несколько вспомогательных функций для генерации обучающих данных на основе фиксированного значения сигмы, а также для создания модели, содержащей один BumpLayer с формой ввода (1,). Давайте посмотрим, сможет ли он узнать значение сигмы, которое используется для генерации обучающих данных:

import numpy as np

def generate_data(sigma, min_x=-1, max_x=1, shape=(100000,1)):
    assert sigma >= 0, 'Sigma should be non-negative!'
    x = np.random.uniform(min_x, max_x, size=shape)
    xp2 = np.power(x, 2)
    condition = np.logical_and(x < sigma, x > -sigma)
    y = np.where(condition, np.exp(-sigma / (sigma - xp2)), 0.0)
    dy = np.where(condition, xp2 * y / np.power((sigma - xp2), 2), 0)
    return x, y, dy

def make_model(input_shape=(1,)):
    model = tf.keras.Sequential()
    model.add(BumpLayer(input_shape=input_shape))

    model.compile(loss='mse', optimizer='adam')
    return model

# Generate training data using a fixed sigma value.
sigma = 0.5
x, y, _ = generate_data(sigma=sigma, min_x=-0.1, max_x=0.1)

model = make_model()

# Store initial value of sigma, so that it could be compared after training.
sigma_before = model.layers[0].get_weights()[0][0]

model.fit(x, y, epochs=5)

print('Sigma before training:', sigma_before)
print('Sigma after training:', model.layers[0].get_weights()[0][0])
print('Sigma used for generating data:', sigma)

# Sigma before training: 0.08271004
# Sigma after training: 0.5000002
# Sigma used for generating data: 0.5

Да, он может узнать значение сигмы, используемое для генерации данных! Но гарантируется ли, что он действительно работает для всех различных значений обучающих данных и инициализации сигмы? Ответ - нет! На самом деле, возможно, что вы запустили приведенный выше код и получили nan в качестве значения сигмы после тренировки или inf в качестве значения потери! Так в чем проблема? Почему могут быть получены значения nan или inf? Давайте обсудим это ниже ...


Работа с числовой стабильностью

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

Итак, давайте посмотрим на эту обобщенную функцию bump (и пока отбросим пороговое значение). Очевидно, что эта функция имеет особенности (т. Е. Точки, в которых функция или ее градиент не определены) в x^2 = sigma (т.е. когда x = sqrt(sigma) или x=-sqrt(sigma)). На приведенной ниже анимированной диаграмме показана функция выпуклости (solid красная линия), ее производные по сигме (пунктирная зеленая линия) и x=sigma и x=-sigma (две вертикальные пунктирные синие линии), когда сигма начинается с нуля и увеличивается до 5:

Generalized bump function when sigma starts from zero and is increased to five.

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

Еще больше, существует проблема c поведение tf.where, которое вызывает проблему значений nan для сигма-переменной в слое: удивительно, если полученное значение в неактивной ветви tf.where очень велико или inf, что с функцией bump приводит к Если значение градиента очень велико или inf, тогда градиент tf.where будет nan, несмотря на то, что inf находится в неактивной ветви и даже не выбран (см. * 1122). * Проблема Github , которая обсуждает именно это) !!

Так есть ли обходной путь для этого поведения tf.where? Да, на самом деле есть способ как-то решить эту проблему, что объясняется в этом ответе : в основном мы можем использовать дополнительный tf.where для предотвращения применения функции в этих регионах. Другими словами, вместо применения self.bump_function к любому входному значению, мы фильтруем те значения, которые НЕ находятся в диапазоне (-self.sigma, self.sigma) (т. Е. Фактический диапазон, к которому должна применяться функция), и вместо этого снабжаем функцию нулем (которое является всегда выдает безопасные значения, т. е. равно exp(-1)):

     output = tf.where(
            condition, 
            self.bump_function(tf.where(condition, inputs, 0.0)),
            0.0
     )

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

true_learned_sigma = []
for s in np.arange(0.1, 10.0, 0.1):
    model = make_model()
    x, y, dy = generate_data(sigma=s, shape=(100000,1))
    model.fit(x, y, epochs=3 if s < 1 else (5 if s < 5 else 10), verbose=False)
    sigma = model.layers[0].get_weights()[0][0]
    true_learned_sigma.append([s, sigma])
    print(s, sigma)

# Check if the learned values of sigma
# are actually close to true values of sigma, for all the experiments.
res = np.array(true_learned_sigma)
print(np.allclose(res[:,0], res[:,1], atol=1e-2))
# True

Он может правильно выучить все значения сигмы! Это мило. Этот обходной путь работал! Хотя есть одно предостережение: это гарантированно будет работать должным образом и изучать любое сигма-значение, если входные значения для этого слоя больше -1 и меньше 1 (т.е. это случай по умолчанию нашей функции generate_data); в противном случае все еще существует проблема потери inf, которая может произойти, если входные значения имеют величину больше 1 (см. точки № 1 и № 2 ниже).


Вот некоторые продукты для размышления для кур ios и заинтересованного ума:

  1. Только что было упомянуто, что если входные значения для этого слоя больше 1 или меньше, чем -1, то это может вызвать проблемы. Можете ли вы спорить, почему это так? (Подсказка: используйте приведенную выше анимированную диаграмму и рассмотрите случаи, когда sigma > 1 и входное значение находится между sqrt(sigma) и sigma (или между -sigma и -sqrt(sigma).)

  2. Можете ли вы предоставить исправление для проблемы в точке # 1, т.е. чтобы слой мог работать для всех входных значений? (Подсказка: как обходной путь для tf.where, подумайте о том, как Вы можете дополнительно отфильтровать небезопасные значения , к которым может быть применена функция bump, и произвести взрывной вывод / градиент.)

  3. Однако, если вы не заинтересованы в устранении этой проблемы и хотели бы использовать этот слой в модели в том виде, в каком она есть сейчас, тогда как бы вы гарантировали, что входные значения для этого слоя всегда будут между -1 и 1? (Подсказка: как В одном из решений существует широко используемая функция активации, которая выдает значения именно в этом диапазоне и может потенциально использоваться в качестве функции активации слоя, находящегося перед этим слоем.)

  4. Если вы посмотрите на последний фрагмент кода, вы увидите, что мы использовали epochs=3 if s < 1 else (5 if s < 5 else 10). Это почему? Почему большие значения сигмы нуждаются в изучении большего количества эпох? (Подсказка: снова используйте анимированную диаграмму и рассматривайте производную функции для входных значений от -1 до 1 при увеличении значения сигмы. Какова их величина?)

  5. Нужно ли проверять сгенерированные обучающие данные для любых nan, inf или очень больших значений y и отфильтровывать их? (Подсказка: да, если sigma > 1 и диапазон значений, то есть min_x и max_x, выходят за пределы (-1, 1); в противном случае нет, это не нужно! Почему это так? Оставлено как упражнение!)

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...