Функции активации являются важной частью любой нейронной сети. Но почему они важны? Что они делают? Давайте сначала посмотрим, как работают функции активации и почему они так распространены в глубоком обучении. После этого мы рассмотрим несколько наиболее распространенных функций активации, различные проблемы, для которых вы можете их использовать, и способы их создания на Python.

Что такое перцептрон?

Чтобы понять функции активации, вам сначала нужно узнать, что такое персептроны. Персептроны являются строительными блоками нейронных сетей. По своей сути персептрон принимает входной вектор, суммирует значения и выдает результат. Однако перцептроны обычно сложнее — если бы все, что делал персептрон, — это складывать числа, он не был бы очень полезен. Как мы можем увеличить мощность наших персептронов? Мы можем преобразовывать входные данные, используя веса, умножая каждое входное значение на его собственное уникальное скалярное значение. Эти веса позволяют нам придавать большее значение конкретным входным значениям. Кроме того, к выходным данным можно добавить скалярную смещение, предоставляя дополнительную переменную для настройки точности.

Зачем персептронам нужна функция активации?

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

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

Двоичный шаг

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

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

Итак, когда вы должны использовать функцию активации бинарного шага? Здесь мы будем использовать его, чтобы превратить персептроны в цифровые логические вентили. Давайте создадим вентиль И, используя нашу функцию активации бинарного шага в Python. Для некоторых математических операций мы будем использовать библиотеку NumPy, которую можно импортировать с помощью import numpy as np.

Вот базовый класс Perceptron, который мы будем использовать для наших демонстраций.

class Perceptron():
    def __init__(self, inputs, weights, bias):
        
        # The inputs to the perceptron
        self.inputs = inputs
        
        # The weights of the perceptron
        self.weights = weights
        
        # The perceptron's bias
        self.bias = bias
    def get_output(self):
        
        # Multiply each input by its weight and sum the result
        output = np.dot(self.inputs, self.weights)
        
        # Add a bias to the previous result
        output = output + self.bias
        
        return output

А вот код для нашей функции активации бинарного шага.

class BinaryStep():
    def transform(self, inputs):
        if inputs.get_output() < 0:
            return 0
        return 1

Довольно просто, правда? Давайте инициализируем новый персептрон и пропустим его через нашу функцию активации бинарного шага. Чтобы сделать логический элемент И, я установлю все значения веса равными 1, а смещение — -2. (Чтобы понять причину этих значений, ознакомьтесь с этой статьей)

# Sets the inputs to a numpy array
inputs = np.array([1, 0])
# Sets the weights to a numpy array of all 1s for our AND gate
weights = np.array([1, 1])
# Sets the bias to -2 for our AND gate
bias = -2

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

p = Perceptron(inputs, weights, bias)
b = BinaryStep()
b.transform(p)

Мы получаем вывод 0, что указывает на то, что наш код работает правильно. Из наших знаний о цифровых логических вентилях И мы знаем, что наш код должен возвращать 1, если наши входы равны 1.

inputs = np.array([1, 1])
weights = np.array([1, 1])
bias = -2
p = Perceptron(inputs, weights, bias)
b = BinaryStep()
b.transform(p)

Мы получаем результат 1, выглядит хорошо.

Сигмоид

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

На приведенной ниже диаграмме мы видим, что сигмовидная функция находится в диапазоне от 0 до 1, что означает, что сильно отрицательное значение на выходе будет близко к 1, а сильно положительное значение на выходе будет близко к 1.

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

Ниже приведен код сигмовидной функции активации.

class Sigmoid():
    def transform(self, inputs):
        output = 1 / (1 + np.exp(-1 * inputs.get_output()))
        return output

Давайте представим, что у нас есть нейронная сеть, которая может классифицировать изображения в зависимости от того, есть ли на них кролик. Допустим, для первого изображения наша сеть дает нам вектор [0,67, 0,93, 0,74, 0,84]. Мы сохраним наши веса равными 1, но установим смещение на 0 для этого примера.

Мы передаем это в наш персептрон и получаем результат 3,18. Пропустив это через нашу сигмовидную функцию активации, мы получим значение 0,96, что означает, что наша модель вполне уверена, что на этом изображении есть кролик!

inputs = np.array([0.67, 0.93, 0.74, 0.84])
weights = np.array([1, 1, 1, 1])
bias = 0
p = Perceptron(inputs, weights, bias)
p.get_output() # 3.18
s = Sigmoid()
s.transform(p) # 0.96

Что произойдет, если на нашем изображении нет кролика? Наша вымышленная нейронная сеть дает нам результат [-0,66, 0,12, -0,98, -0,95]. Пропуская это через наш персептрон, мы получаем значение -2,47. Мы пропускаем это значение через нашу сигмовидную функцию, которая выводит значение 0,078, что указывает на то, что на этом изображении, скорее всего, нет кролика.

inputs = np.array([-0.66, 0.12, -0.98, -0.95])
weights = np.array([1, 1, 1, 1])
bias = 0
p = Perceptron(inputs, weights, bias)
p.get_output()
s = Sigmoid()
s.transform(p)

Тан

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

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

Как Тан пытается решить эту проблему? Функция гиперболического тангенса сосредоточена вокруг 0, как показано на диаграмме ниже. Таким образом, если вы подадите очень отрицательное значение, гиперболический тангенс вернет что-то близкое к -1 вместо 0 с сигмоидой.

Давайте рассмотрим пример функции активации tanh в коде.

class Tanh():
    def transform(self, inputs):
        x = inputs.get_output()
        output = (np.exp(x) - np.exp(-x)) / (np.exp(x) + np.exp(-x))
        return output

А вот пример tanh с использованием нашего базового класса Perceptron из приведенного выше.

# Sets the inputs to an array of length 4 with range [-1, 1]
inputs = np.random.rand(4) * 2 - 1
# Sets the weights to an array of length 4 with range [-1, 1]
weights = np.random.rand(4) * 2 - 1
bias = 0
p = Perceptron(inputs, weights, bias)
p.get_output() # 1.39
t = Tanh()
t.transform(p) # 0.88

РеЛУ

Однако и sigmoid, и tanh страдают от одной и той же проблемы — проблемы исчезающих градиентов. Если посмотреть на графики сигмовидной и тангенсной функций, то можно увидеть, что чем дальше вправо/влево вы уходите по графикам, тем больше градиент приближается к нулю. При обучении нейронной сети с использованием такого метода, как стохастический градиентный спуск или SGD, если градиент крошечный, нейронная сеть не будет обучаться, поскольку веса будут обновляться практически в незначительной степени с каждой итерацией.

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

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

Благодаря своей простоте ReLU можно быстро и эффективно рассчитать в больших масштабах. Из-за этого ReLU очень распространен в глубоком обучении, особенно когда вы хотите, чтобы ваша модель обучалась быстро.

Ниже представлена ​​реализация класса ReLU в Python.

class ReLU():
    def transform(self, inputs):
        if inputs.get_output() > 0:
            return inputs.get_output()
        return 0

Давайте кратко рассмотрим пример с использованием этого класса ReLU:

inputs = np.array([0.65, 0.32, -0.51, 0.93])
weights = np.ones(4)
bias = 0
p = Perceptron(inputs, weights, bias)
p.get_output() # 1.39
r = ReLU()
r.transform(p) # 1.39

Поскольку выход больше 0, функция ReLU оставляет вывод без изменений. Что, если бы мы немного подкорректировали входной массив, чтобы персептрон выдавал на выходе значение меньше 0?

inputs = np.array([-0.37, 0.41, -0.59, -0.03])
weights = np.ones(4)
bias = 0
p = Perceptron(inputs, weights, bias)
p.get_output() # -0.58
r = ReLU()
r.transform(p) # 0

Выход установлен на 0, именно то, что мы ожидали.

Дырявый ReLU

Leaky ReLU основан на традиционном ReLU с небольшим изменением, заключающимся в том, что отрицательные значения отображаются на линию с меньшим наклоном вместо нуля.

В Leaky ReLU α — это значение наклона в отрицательной области графика (обычно около 0,1).

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

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

Давайте посмотрим на реализацию Leaky ReLU в коде.

class LeakyReLU():
    def transform(self, inputs):
        if inputs.get_output() > 0:
            return inputs.get_output()
        return inputs.get_output() * 0.1 # 0.1 is our value for α

И быстрый пример:

inputs = np.array([0.99, 0.14, 0.87, 0.03])
weights = np.ones(4)
bias = 0
p = Perceptron(inputs, weights, bias)
p.get_output() # 2.03
l = LeakyReLU()
l.transform(p) # 2.03

Как и в традиционном ReLU, если вход Leaky ReLU больше 0, выход равен входу. Давайте посмотрим, что произойдет, если вход меньше 0.

inputs = np.array([-0.70, 0.44, -0.92, -0.88])
weights = np.ones(4)
bias = 0
p = Perceptron(inputs, weights, bias)
p.get_output() # -2.06
l = LeakyReLU()
l.transform(p) # -0.206

Вместо того, чтобы возвращать 0, теперь мы возвращаем небольшое отрицательное значение.

Софтмакс

Softmax используется в задачах, отличных от других функций активации, которые мы обсуждали до сих пор. Вместо вывода одного числа softmax выводит вектор. Когда это может быть выгодно? Одним из распространенных применений функции softmax является выходной слой задачи мультиклассовой классификации. Допустим, вы хотите классифицировать различные изображения в зависимости от птицы, которую они содержат. Softmax берет выходные данные вашего последнего слоя и преобразует значения в вероятности, суммируя их до 1. Это позволяет нам узнать, насколько наша модель уверена в том, что изображение содержит конкретную птицу.

Давайте посмотрим, как softmax работает в Python.

class SoftMax():
    def transform(self, inputs):
        outputs = np.empty(inputs.shape[0])
        total = sum([np.exp(p.get_output()) for p in inputs])
        for x in range(len(inputs)):
            outputs[x] = np.exp(inputs[x].get_output()) / total
        return outputs

Не плохо. Как мы можем использовать его для вычисления вероятностей различных классов? Поскольку softmax отображает несколько значений, нам нужно передать выходные данные от нескольких персептронов, инициализируя их таким образом.

p1 = Perceptron(np.array([0.56, -0.72, -0.63, 0.18]), np.ones(4), 0)
p2 = Perceptron(np.array([0.61, -0.28, 0.37, 0.89]), np.ones(4), 0)
p3 = Perceptron(np.array([-0.34, 0.16, 0.42, 0.67]), np.ones(4), 0)

Затем мы группируем три персептрона в массив NumPy.

p_ary = np.array([p1, p2, p3])
[p.get_output() for p in p_ary] # [-0.61, 1.59, 0.91]

Пропустив эти значения через нашу функцию softmax, мы получим наши выходные вероятности. Обратите внимание, что сумма вероятностей равна 1, что имеет смысл для мультиклассовых вероятностей.

s = SoftMax()
s.transform(p_ary) # array([0.06850611, 0.61826854, 0.31322535])

Я надеюсь, что теперь вы лучше понимаете, что могут делать функции активации и когда вы можете использовать их в своих нейронных сетях. Если вы нашли этот контент полезным, подписывайтесь на меня здесь, на Medium. Я бы хотел связаться с вами в LinkedIn!

Прочитайте эти статьи, которые я считаю полезными, если хотите узнать больше о функциях активации!

https://towardsdatascience.com/everything-you-need-to-know-about-activation-functions-in-deep-learning-models-84ba9f82c253

https://analyticsindiamag.com/how-do-activation-functions-introduce-non-linearity-in-neural-networks/#:~:text=Функция%20активации%20%20есть%20присутствуют,проблемы%20являются%20высоко %20не%2Dлинейный.

https://medium.com/@danqing/a-practical-guide-to-relu-b83ca804f1f7#:~:text=Leaky%20ReLU%20имеет%20два%20преимуществ, на%200%20ускоряет%20обучение%20.

https://medium.com/data-science-bootcamp/understand-the-softmax-function-in-minutes-f3a59641e86d

https://medium.com/@stanleydukor/neural-representation-of-and-or-not-xor-and-xnor-logic-gates-perceptron-algorithm-b0275375fea1