ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Sigmoid Activation Saturation 알아보기
    AI-ML 2024. 1. 10. 22:19
    728x90
    반응형

    - 목차

     

    키워드.

    • - Saturation
    • - Sigmoid

     

    들어가며.

    이번 글에서는 Deep Learning 에서 사용되는 대표적인 Activation Function 인 Sigmoid 의 Saturation 에 대해서 알아보려고 합니다.

    Activation Function 은 크게 Sigmoid 계열과 ReLU 계열로 나뉩니다.

    아래 왼쪽 그림이 Sigmoid 함수이고, 우측의 그림이 ReLU 함수입니다.

    ReLU 함수는 Sigmoid 함수의 일부 단점들을 극복하는 특징을 보이며, 근래의 Deep Learning 분야에서 주로 사용되는 Activation Function 입니다.

     

     

    그럼 위 Sigmoid 함수가 가지는 Saturation 은 과연 무엇일까요 ?

     

    Sigmoid Saturation.

    아래 2개의 이미지는 Sigmoid 함수와 Sigmoid 의 도함수를 표현한 그래프입니다.

    좌측의 이미지가 Sigmoid 함수이고, 우측의 이미지가 Sigmoid 의 기울기를 표현한 도함수입니다.

    각 그래프는 빨간색으로 그어진 2개의 라인이 존재하는데요.

    이 빨간색 라인이 바로 Saturation 이 발생하는 영역입니다.

     

     

    Saturation 은 우리말로 포화라는 의미를 가집니다.

    즉, 더 이상 변화 또는 진전이 없는 정체 상태를 의미합니다.

    Deep Learning 의 Forward Propagation 과정에서 이전 레이어의 출력이 Sigmoid 함수의 입력이 됩니다.

    하지만 Sigmoid 의 함수는 0 ~ 1 사이의 값을 가지기 때문에 아무리 입력값이 크더라고 1 로 수렴됩니다.

    반대로 아무리 작은 크기의 입력값이 Sigmoid 로 입력되더라도 그 값은 0 으로 수렴합니다.

    아래 예시처럼 1개의 Input 과 1개의 Weight 를 가지는 간단한 Model 이 존재할 때에,

    여러 Input 과 Weight 의 경우에 대해서 아래와 같은 결과를 보입니다.

    Sigmoid 의 입력값이 아무리 작아도 또는 아무리 커도 0 과 1 로 수렴하는 문제가 존재합니다.

    이는 Forward Propagation 과정에서 발생하는 단점으로 동작합니다.

     

     

    Backpropagation 에서의 Saturation.

     

    그리고 Backpropagation 과정에서도 Saturation 이슈가 발생합니다.

    수학적으로 Sigmoid 함수와 도함수는 아래의 수식과 같습니다.

     

    $$ \sigma(x) = \frac{1}{1 + e^{-x}} $$

    $$ \frac{d \sigma(x)}{d x} = \sigma(x) (1 - \sigma(x)) $$

    ( 참고로 Sigmoid 함수는 수학적으로 $\sigma(x)$ 와 같이 표현됩니다. )

    Sigmoid 함수의 미분함수는 Sigmoid 함수를 응용한다는 점에 주목해주시고, 이왕이면 외워주시면 좋습니다.

     

    아래와 같이 간단한 Neural Net 이 존재한다고 가정합니다.

    하나의 Input 과 하나의 Weight 가 곱해지고, 곱해진 출력값이 Sigmoid Layer 의 입력이 되는 구조입니다.

     

     

    이러한 Neural Net 의 Backpropagation 과정은 아래와 같습니다.

    과정이 복잡할 수 있는데요. 하나씩 설명해보도록 하겠습니다.

     

     

    Sigmoid 함수의 결과값은 $ Y_{pred} $ 라고 이름지었습니다.

    즉, 최종적인 예측값을 의미하죠.

    어떠한 Loss Function 을 사용했는지는 명시하지 않았지만, MSE 나 Cross Entropy 를 사용하였다는 가정하겠습니다.

    이 경우에 Sigmoid Layer 가 전달받는 Gradient 는 $ \frac{d(Loss) }{d(Y_{pred})} $ 로 표현됩니다.

    그리고 X 로 표현한 Percentron 은 뒤 레이어인 Sigmoid 에 의해서 $ \sigma(Output) (1 - \sigma(Output)) $ Gradient 을 전달받게 됩니다.

    그리고 가장 최초의 파라미터인 Weight 의 Local Gradient 는 Input 값이 되며,

    최종적으로 모든 Gradient 들을 Chain Rule 에 의해서 곱한 결과가 Weight 에게 전달되는 Gradient 입니다.

    정리하면, Weight 는 아래와 같은 구조로 Parameter 가 Update 되게 됩니다.

     

    $$ Weight_{next} = Weight -1 \times LearningRate \times \frac{d(Loss) }{d (Y_{pred})} \times \sigma(Output) (1 - \sigma(Output)) \times Input $$

     

    위 설명에서 제가 하고 싶은 이야기는 첫번째 Perceptron 의 출력값인 Output 이 너무 크거나 작게되면,

    Backpropagation 과정에서 Sigmoid Layer 가 전달하는 Gradient 가 0 으로 수렴한다는 것입니다.

    이렇게 되면, Weight 의 업데이트는 이루어지지 않습니다.

    여기까지 설명드린 내용은 이어지는 내용에서 코드적으로 상세히 설명해보겠습니다.

     

    Saturation 를 코드로 구현해보기.

    간단한 Linear Classification 모델을 통해서 Saturation 이슈를 파악합니다.

    -500부터 100 까지의 숫자 중에서 500 이하의 숫자는 0을 출력하고, 500 을 초과하는 숫자는 1를 출력하는 모델을 만들어보겠습니다.

     

    위 데이터셋은 $ y = 1 \times x $ 의 함수 형태를 취하는 Model 이라면 정상적으로 분류가 가능합니다.

    왜냐하면 $ y = 1 \times x $ 은 -500 ~ -1 까지의 음수에 대하여 음수의 결과를 출력하고,

    1 ~ 500 인 양수 입력에 대하여 양수의 결과를 출력하기 때문입니다.

     

    그래서 아래와 같이 pytorch 를 활용한 간단한 Model 클래스를 생성합니다.

    하나의 Linear Layer 와 Sigmoid 로 구성됩니다.

    import torch
    import torch.nn as nn
    class SimpleModel(nn.Module):
        def __init__(self):
            super(SimpleModel, self).__init__()
            self.linear = nn.Linear(1, 1, bias=False)
            self.sigmoid = nn.Sigmoid()
    
        def forward(self, input):
            return self.sigmoid(self.linear(input))

     

     

    그리고 전체적인 코드는 아래와 같습니다.

    아래의 예시를 간단히 설명하면,

    -500 ~ 500 까지의 정수를 학습 데이터셋으로 사용합니다.

    그리고 0 또는 1 를 출력하는 이진 분류를 구현하기 위해서 Sigmoid Activation 과 BCELoss 를 사용합니다.

     

    import torch
    import torch.nn as nn
    from torch.utils.data import DataLoader, Dataset
    
    input = torch.arange(-500, 501, 1, dtype=torch.float32).view(-1, 1)
    label = torch.cat([torch.zeros(500), torch.ones(501)]).view(-1, 1)
    
    class SimpleDataset(Dataset):
        def __init__(self, input, label):
            super(SimpleDataset, self).__init__()
            self.data = torch.cat([input, label], dim=1)
    
        def __getitem__(self, index):
            return self.data[index]
    
        def __len__(self):
            return self.data.shape[0]
    
    class SimpleModel(nn.Module):
        def __init__(self):
            super(SimpleModel, self).__init__()
            self.linear = nn.Linear(1, 1, bias=False)
            self.sigmoid = nn.Sigmoid()
    
        def forward(self, input):
            return self.sigmoid(self.linear(input))
    
    
    model = SimpleModel()
    optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
    loss_function = nn.BCELoss()
    dataset = SimpleDataset(input, label)
    loader = DataLoader(dataset, batch_size=64, shuffle=True)
    
    losses = []
    for epoch in range(2000):
        local_losses = []
        for i, batch in enumerate(loader):
            x_train = batch[:, [0]]
            y_train = batch[:, [1]]
    
            output = model(x_train)
            loss = loss_function(output, y_train)
    
            optimizer.zero_grad()
            loss.backward()
            optimizer.step()
            local_losses.append(loss.item())
            if i % 100 == 0:
                print(epoch, i, loss.item(), model.linear.weight.item())
    
        import statistics
        losses.append(statistics.mean(local_losses))

     

    위 Binary Classifier 의 Epoch - Loss 그래프는 아래와 같습니다.

    Epoch 을 거듭하면서 Loss 를 0 에 수렴해갑니다.

    그리고 이번 글의 주제인 Sigmoid Saturation 은 발생하지 않았습니다.

     

     

     

    위의 코드를 약간 수정하여 Saturation 현상을 만들어보겠습니다.

    Model 의 Weight 를 살짝 변형하면 됩니다.

    아래와 같이 model 의 Linear Transformation Layer 의 Weight 를 -1000000 으로 설정합니다.

    model.linear.weight = \
    nn.Parameter(torch.tensor([[-1000000]], dtype=torch.float32, requires_grad=True))

     

    model 의 Weight 를 수정한 전체 코드와 정확도 실행의 결과는 아래와 같습니다.

    import torch
    import torch.nn as nn
    from torch.utils.data import DataLoader, Dataset
    
    input = torch.arange(-500, 501, 1, dtype=torch.float32).view(-1, 1)
    label = torch.cat([torch.zeros(500), torch.ones(501)]).view(-1, 1)
    
    class SimpleDataset(Dataset):
        def __init__(self, input, label):
            super(SimpleDataset, self).__init__()
            self.data = torch.cat([input, label], dim=1)
    
        def __getitem__(self, index):
            return self.data[index]
    
        def __len__(self):
            return self.data.shape[0]
    
    class SimpleModel(nn.Module):
        def __init__(self):
            super(SimpleModel, self).__init__()
            self.linear = nn.Linear(1, 1, bias=False)
            self.sigmoid = nn.Sigmoid()
    
        def forward(self, input):
            return self.sigmoid(self.linear(input))
    
    
    model = SimpleModel()
    model.linear.weight = nn.Parameter(torch.tensor([[-1000000]], dtype=torch.float32, requires_grad=True))
    optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
    loss_function = nn.BCELoss()
    dataset = SimpleDataset(input, label)
    loader = DataLoader(dataset, batch_size=64, shuffle=True)
    
    losses = []
    for epoch in range(2000):
        local_losses = []
        for i, batch in enumerate(loader):
            x_train = batch[:, [0]]
            y_train = batch[:, [1]]
    
            output = model(x_train)
            loss = loss_function(output, y_train)
    
            optimizer.zero_grad()
            loss.backward()
            optimizer.step()
            local_losses.append(loss.item())
            if i % 100 == 0:
                print(epoch, i, loss.item(), model.linear.weight.item())
    
        import statistics
        losses.append(statistics.mean(local_losses))

     

    Weight 가 음의 방향으로 큰 값을 가지게 되면 더이상 Loss 는 줄어들지 않습니다.

    이러한 현상을 Saturation 이라고 합니다.

    그럼 Weight 가 큰 값을 가지게 되었을 때에 Saturation 이 발생하는 원인을 무엇일까요 ?

     

    $$ weight_{next} = weight -1 \times lr \times x_{input} \times \frac{\delta Loss}{\delta weight} $$

     

    위의 식은 Weight 가 학습이 진행되는 동안 각 Epoch 마다 값이 업데이트되는 수학적 공식입니다.

    여기서 중요한 것은 $ \frac{\partial Loss}{\partial weight} $ 인데요.

    Sigmoid Saturation 에 의해서 $ \frac{\partial Loss}{\delta weight} $ 이 값이 0 으로 수렴하는 것이 원인입니다.

    그래서 아무리 학습이 진행되어도 가중치는 튜닝되지 않게 됩니다.


    $$ \frac{d (Loss)}{d (weight)} = \frac{d (Loss)}{d (sigmoid(wx))} \times \frac{d (sigmoid(wx))} {d wx} \times \frac{d (wx)}{d (w)} $$

    $$ \frac{d (Loss)}{d (sigmoid(wx))} \times sigmoid(wx) \times (1 - sigmoid(wx)) \times x$$

     

    여기서 W 인 값이 -1000000 이고, x 값이 -500 ~ 500 인 상황입니다.

    그럼 w 와 x 가 곱해진 모든 경우의 수는 sigmoid(wx) 에 의해서 0 또는 1 로 Saturation 될 수 밖에 없으며,

    이 상황에서 Backpropagation 에 의한 가중치의 최적화는 발생하지 않게 됩니다.

     

     

     

     

    반응형
Designed by Tistory.