Как создать пользовательскую функцию активации только с Python в Tensorflow?

Предположим, вам нужно сделать функцию активации, которая невозможна, используя только предварительно определенные блок-блоки тензорного потока, что вы можете сделать?

Итак, в Tensorflow можно сделать свою собственную функцию активации. Но это довольно сложно, вам нужно написать его на С++ и перекомпилировать весь файл tensorflow [1] [2].

Есть ли более простой способ?

Ответ 1

Да, есть!

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

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

Запись функции активации:

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

def spiky(x):
    r = x % 1
    if r <= 0.5:
        return r
    else:
        return 0

Что выглядит следующим образом: Spiky Activation

Первый шаг - превратить его в функцию numpy, это легко:

import numpy as np
np_spiky = np.vectorize(spiky)

Теперь мы должны написать его производную.

Градиент активации: в нашем случае это легко, это 1, если x mod 1 <0,5 и 0 в противном случае. Так:

def d_spiky(x):
    r = x % 1
    if r <= 0.5:
        return 1
    else:
        return 0
np_d_spiky = np.vectorize(d_spiky)

Теперь за трудную часть создания функции TensorFlow.

Выполнение numpy fct с помощью метода tensorflow fct: Начнем с того, что np_d_spiky превратится в функцию тензорного потока. Существует функция в tensorflow tf.py_func(func, inp, Tout, stateful=stateful, name=name) [doc], которая преобразует любую функцию numpy в функцию tensorflow, поэтому мы можем ее использовать:

import tensorflow as tf
from tensorflow.python.framework import ops

np_d_spiky_32 = lambda x: np_d_spiky(x).astype(np.float32)


def tf_d_spiky(x,name=None):
    with tf.name_scope(name, "d_spiky", [x]) as name:
        y = tf.py_func(np_d_spiky_32,
                        [x],
                        [tf.float32],
                        name=name,
                        stateful=False)
        return y[0]

tf.py_func действует на списки тензоров (и возвращает список тензоров), поэтому мы имеем [x] (и возвращаем y[0]). Опция stateful заключается в том, чтобы указать tensorflow, всегда ли функция дает одинаковый вывод для одного и того же входа (stateful = False), и в этом случае тензорный поток может просто графом тензорного потока, это наш случай и, вероятно, будет иметь место в большинстве ситуаций. На данный момент нужно быть осторожным, так как numpy использует float64 но tenorflow использует float32 поэтому вам нужно преобразовать вашу функцию, чтобы использовать float32 прежде чем вы сможете преобразовать ее в функцию тензорного потока, иначе тензор будет жаловаться. Вот почему нам нужно сначала сделать np_d_spiky_32.

Как насчет градиентов? Проблема только в том, что мы делаем только то, что, несмотря на то, что теперь мы имеем tf_d_spiky которая является версией np_d_spiky с тензорным np_d_spiky, мы не могли использовать ее как функцию активации, если бы хотели, потому что тензорный поток не знает, как вычислить градиенты этого функция.

Взлом для получения градиентов. Как объясняется в упомянутых выше источниках, существует хак для определения градиентов функции с использованием tf.RegisterGradient [doc] и tf.Graph.gradient_override_map [doc]. Копируя код из гарпуны, мы можем изменить функцию tf.py_func чтобы одновременно определить градиент:

def py_func(func, inp, Tout, stateful=True, name=None, grad=None):

    # Need to generate a unique name to avoid duplicates:
    rnd_name = 'PyFuncGrad' + str(np.random.randint(0, 1E+8))

    tf.RegisterGradient(rnd_name)(grad)  # see _MySquareGrad for grad example
    g = tf.get_default_graph()
    with g.gradient_override_map({"PyFunc": rnd_name}):
        return tf.py_func(func, inp, Tout, stateful=stateful, name=name)

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

Функция градиента: так что для нашей косой активации функция, как мы это сделаем:

def spikygrad(op, grad):
    x = op.inputs[0]

    n_gr = tf_d_spiky(x)
    return grad * n_gr  

Функция активации имеет только один вход, поэтому x = op.inputs[0]. Если операция имела много входов, нам нужно было бы возвращать кортеж, один градиент для каждого входа. Например, если операция была ab то градиент по отношению к a является +1 а по отношению к b - -1 поэтому мы бы return +1*grad,-1*grad. Обратите внимание, что нам нужно возвращать функции тензорного потока ввода, поэтому tf_d_spiky, np_d_spiky не работал бы, потому что он не может действовать на тензоры тензорного потока. В качестве альтернативы мы могли бы написать производную с использованием функций тензорного потока:

def spikygrad2(op, grad):
    x = op.inputs[0]
    r = tf.mod(x,1)
    n_gr = tf.to_float(tf.less_equal(r, 0.5))
    return grad * n_gr  

Объединив все это: теперь, когда у нас есть все части, мы можем объединить их все вместе:

np_spiky_32 = lambda x: np_spiky(x).astype(np.float32)

def tf_spiky(x, name=None):

    with tf.name_scope(name, "spiky", [x]) as name:
        y = py_func(np_spiky_32,
                        [x],
                        [tf.float32],
                        name=name,
                        grad=spikygrad)  # <-- here the call to the gradient
        return y[0]

И теперь мы закончили. И мы можем проверить это.

Контрольная работа:

with tf.Session() as sess:

    x = tf.constant([0.2,0.7,1.2,1.7])
    y = tf_spiky(x)
    tf.initialize_all_variables().run()

    print(x.eval(), y.eval(), tf.gradients(y, [x])[0].eval())

[0,2 0,69999999 1.20000005 1,70000005] [0,2 0,20000005 0] [1. 0. 1. 0.]

Успех!

Ответ 2

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

Для функции spiky в вашем ответе это может выглядеть следующим образом

def spiky(x):
    r = tf.floormod(x, tf.constant(1))
    cond = tf.less_equal(r, tf.constant(0.5))
    return tf.where(cond, r, tf.constant(0))

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