이번 포스트에서는 Deep Learning에서 가장 많이 사용되는 activation function들인 sigmoid, tanh, ReLU에 대해서 알아본다.

실제로는 상당히 다양한 종류의 activation function들이 존재하고 2017년 Google Brain에서 개발한 Swish activation function 등, 지금도 효유적인 backpropagation을 위한 activation function들이 개발되고 있다.

이때 이 모든 activation function들을 한 번에 다루는 것은 비효율적이기도 하고, 지겨우니까 대표적인 activation function들에 대해 배우고, learning process에 접목시켜본 뒤에 다른 activation function들의 특징을 파악하는 편이 더 좋을 것 같다.

 

따라서 몇 차례의 포스트를 걸쳐 기본적인 sigmoid, tanh, ReLU에 대해서 배워보도록 하자.

 

Sigmoid Activation Function


Deep learning을 배우는 사람들은 아마 logistic regression을 배우면서 처음 sigmoid라는 non-linear activation function을 접할 것이다. 그리고 이 sigmoid function은 logistic function의 subset인데 output이 0과 1사이이므로 확률로의 해석이 가능하며 x축으로의 bias가 없어서 learning에 효과적이므로 logistic funciton들 중 가장 많이 사용된다.

 

먼저 logistic function의 식은 다음과 같다.

이 logistic function에서 input의 boundary를 (0,1)로 맞추고 x축에 대한 bias도 없애면

우리가 알고있는 sigmoid function의 형태가 된다. 이 sigmoid function는 함수의 모양과 도함수를 그려보면 다음과 같다.

x_range = np.linspace(-5, 5, 300)
sigmoid = 1 / (1 + np.exp(-1*x_range))
sigmoid_d = sigmoid*(1 - sigmoid)

fig, ax = plt.subplots(2, 1, figsize = (15,10))
ax[0].set_title("Sigmoid Function", fontsize = 20)
ax[0].plot(x_range, sigmoid)
ax[0].grid()

ax[1].set_title("Derivative of Sigmoid", fontsize = 20)
ax[1].plot(x_range, sigmoid_d)
ax[1].grid()

 

극한을 이용하여 sigmoid의 수렴값을 구해보면

이므로 sigmoid의 output은 확률로 해석이 가능하며

전체 0부터 1사이의 output 중 90% 정도인 0.1과 0.9를 input이 실수 전체 중 -2부터 2일때가 차지한다. 따라서 [-2, 2] 이외의 값들은 outlier로 취급되어 sigmoid의 output에서는 0.1, 0.9와 큰 차이를 보이지 않는다.

 

그리고 이 sigmoid의 derivative는 다음과 같이 구할 수 있다.

 

따라서 sigmoid function에 대한 node는 다음과 같이 구현할 수 있다.

class sigmoid_node():
    def __init__(self):
        self.x, self.z = None, None
        
    def forward(self, x):
        self.x = x
        self.z = 1 / (1 + np.exp(-1*self.x))
        return self.z
    
    def backward(self, dJ):
        return self.z * (1 - self.z) * dJ

 

Tanh Activation Fucntion


위의 sigmoid보다 대부분의 경우에서 좋은 성능을 보여주는 activation function은 tanh이다. 따라서 hidden layer에서 sigmoid보다 tanh를 사용하는 경우가 많다. 하지만 마지막 layer에서 classification을 할 때는 tanh보다 sigmoid를 많이 사용한다.

이 tanh activation function은 다음과 같다.

 

이 tanh의 모양과 도함수의 모양은 다음과 같다.

x_range = np.linspace(-5, 5, 300)
tanh = (np.exp(x_range) - np.exp(-1*x_range))/(np.exp(x_range) + np.exp(-1*x_range))
tanh_d = (1 + tanh)*(1 - tanh)

fig, ax = plt.subplots(2, 1, figsize = (15,10))
ax[0].set_title("Tanh Function", fontsize = 20)
ax[0].plot(x_range, tanh)
ax[0].grid()

ax[1].set_title("Derivative of Tanh", fontsize = 20)
ax[1].plot(x_range, tanh_d)
ax[1].grid()

위의 식에서 극한을 이용하면 다음과 같은 결과를 얻을 수 있다.

 

이때 알 수 있는 것은, sigmoid와 달리 activation function의 output이 zero-centered라는 것이다. 즉, deep learning의 hidden layer에 이 tanh를 사용할 경우, 다음 layer의 input이 자동으로 zero-centered input이 되는 것을 알 수 있다. 이것이 tanh가 sigmoid보다 좋은 성능을 보여주는 첫 번째 이유가 된다.

 

sigmoid와 마찬가지로 tanh의 도함수를 구해보면 다음과 같다.

 

따라서 tanh는 다음과 같이 구현할 수 있다.

class tanh_node():
    def __init__(self):
        self.x, self.z = None, None
    
    def forward(self, x):
        self.x = x
        self.z = (np.exp(self.x) - np.exp(-1*self.x))/(np.exp(self.x) + np.exp(-1*self.x))
        return self.z
        
    def backward(self, dJ):
        return (1 + self.z) * (1 - self.z) * dJ

 

ReLU Activation Function


요즘 deep learning에서 가장 첫 번째로 시도되는 activation function은 ReLU일 것이다. 이 ReLU가 다른 activation function보다 좋은 성능을 내는지에 대한 논문도 다양하며 실제로 다양한 neural network에서 좋은 성능을 보여준다. 먼저 ReLU의 식은 다음과 같다.

 

즉, ReLU의 input이 양수일 경우엔 그 양수값을 내보내고, 음수일 경우 0을 내보낸다. ReLU가 좋은 성능을 내는 첫 번째 이유가 이 식에 있다. Sigmoid, Tanh는 여러 연산들을 하여 activation value를 만드는데 비해, 이 ReLU는 한 번의 비교연산만을 통하여 output을 내기 때문에 연산속도가 빠르다. 보통 tanh보다 5배정도 빠르다고 알려져있다.

 

이 ReLU의 그래프와 도함수를 보면 다음과 같다.

x_range = np.linspace(-5, 5, 300)
zero_arr = np.zeros_like(x_range)
relu = np.maximum(x_range, zero_arr)
relu_d = relu / (0.0001 + relu)
fig, ax = plt.subplots(2, 1, figsize = (15,10))
ax[0].set_title("Tanh Function", fontsize = 20)
ax[0].plot(x_range, relu)
ax[0].grid()

ax[1].set_title("Derivative of Tanh", fontsize = 20)
ax[1].plot(x_range, relu_d)
ax[1].grid()

이때, 코드를 보면 relu_d를 구현하는데 조금의 트릭이 들어간 것을 알 수 있다. 음수일 때 0, 양수일 때 1을 만들기 위하여 relu에 relu를 나눠주는데 0으로 나눠줄 수 없으므로 gradient에 영향을 주지 않는 0.0001 정도를 더해서 나눠준 것을 알 수 있다.

 

도함수는 어렵게 구할 것 없이, 양수일 때는 1이고 음수일 때는 0이 된다.

 

따라서 ReLU는 다음과 같이 구현할 수 있다.

class relu_node():
    def __init__(self):
        self.x, self.z = None, None
        
    def forward(self, x):
        self.x = x
        self.z = np.maximum(0, self.x)
        return self.z
    
    def backward(self, dJ):
        return self.z / (0.0001 + self.z)

Activation Functions Comparison


위의 3가지 activation function을 같이 그려보면 다음과 같다.

먼저 위의 그래프를 통해 알 수 있는 것은, sigmoid와 tanh는 output의 범위가 정해져있지만 ReLU는 output의 범위가 정해져있지 않다는 것이다.

그리고 도함수에서 sigmoid와 tanh는 1보다 작기 때문에 layer가 많아질수록 propagate되는 값들이 0에 가까워질 것을 알 수 있다. 즉, sigmoid와 tanh는 태생적으로 vanishing gradient problem을 야기할 수 있는 activation function들이다.

+ Recent posts