인공신경망이 인풋 데이터와 정답(label) 데이터를 가지고 학습을 한다고 했을 때, 신경망의 내부에서 무슨 일이 일어나고 있는지, 기계가 데이터를 가지고 학습을 하는 원리가 무엇인지 궁금하실거예요. 이번 포스팅은 바로 이 마법에 대한 소개 글입니다. 

 

"오차역전파(Backpropagation)"은 "오차를 역방향으로 전파하는 방법 (backward propagation of errors)" 의 줄임말로서, 인공 신경망을 훈련시키기 위한 지도 학습 알고리즘으로 자주 사용됩니다. 이는 신경망이 오류에서 학습하고 시간이 지남에 따라 성능을 향상시킬 수 있도록 하는 훈련 과정에서 중요한 구성 요소입니다. 


다음은 오차역전파 알고리즘이 학습하는 단계입니다. 

 

1. 초기화 (Initialization)

2. 순방향 전파 (Forward Pass)

3. 오류 계산 (Calculate Error) 

4. 역방향 전파 (Backward Pass, Backpropagation) 

5. 경사 하강법 (Gradient Descent)

 

 

[ 손실함수와 경사하강법에 의한 최적화를 통한 모델 훈련 ]

model training using loss function and optimization by gradient descent

 

 

 

1. 초기화 (Initialization)

 

신경망에서 초기화(Initialization)는 훈련이 시작되기 전에 네트워크의 가중치(Weights)와 편향(Biases)을 설정하는 과정을 나타냅니다. 올바른 초기화는 신경망의 성공적인 훈련에 중요하며 모델의 수렴 및 성능에 큰 영향을 미칠 수 있습니다.

신경망을 만들 때 그 가중치 및 편향을 일정한 값으로 초기화합니다. 이러한 초기 값의 선택은 네트워크가 얼마나 빨리 학습하며 올바른 해법으로 수렴하는지에 영향을 줄 수 있습니다. 부적절한 초기화는 수렴이 느리게 되거나 그라디언트가 소멸하거나 폭발하는 등의 문제를 초래할 수 있습니다.

가중치 초기화에는 여러 기법이 있으며, 일반적으로 사용되는 몇 가지 방법에는 다음이 포함됩니다.


(1) 랜덤 초기화(Random Initialization): 가중치에 무작위 값 할당하기. 이는 흔한 접근법으로, 가우시안(정규) 분포 또는 균일 분포를 사용하여 무작위 값 생성하는 방법을 사용할 수 있습니다. 


(2) 자비에/글로럿 초기화(Xavier/Glorot Initialization): 이 방법은 시그모이드 및 쌍곡선 탄젠트(tanh) 활성화 함수와 잘 작동하도록 설계되었습니다. 가중치는 평균이 0이고 분산이 (1/ 입력 뉴런 수) 인 가우시안 분포에서 값들을 추출하여 초기화됩니다. 

 

(3) He 초기화(He Initialization): "ReLU 초기화"로도 알려져 있으며, ReLU 활성화 함수를 사용하는 네트워크에 적합합니다. 가중치는 평균이 0이고 분산이 (2/ 입력 뉴런 수) 인 가우시안 분포에서 값들을 추출하여 초기화됩니다. 


적절한 초기화 방법을 선택하는 것은 사용된 활성화 함수에 따라 다르며, 신경망을 효과적으로 훈련시키기 위한 중요한 측면입니다. 적절한 초기화 기술을 사용하면 훈련 중 발생하는 일반적인 문제를 완화하고 더 안정적이고 빠른 수렴에 기여할 수 있습니다. 

 

 

 

2. 순방향 전파 (Forward Pass)

 

신경망에서의 순전파(forward pass)는 입력 데이터를 네트워크를 통과시켜 출력을 생성하는 과정을 나타냅니다. 순전파 중에는 입력 데이터가 신경망의 각 층을 통과하고, 각각의 층에서 변환을 거쳐 최종 출력을 생성합니다. 


순전파의 단계를 설명하면 다음과 같습니다. 


(1) 입력 층(Input Layer): 데이터 입력, 즉 데이터 세트의 특징들이 신경망의 입력 층에 공급됩니다. 


(2) 가중치와 편향(Weights and Biases): 인접한 층 사이의 각 뉴런 간에는 가중치가 할당됩니다. 이러한 가중치는 연결의 강도를 나타냅니다. 또한 각 뉴런은 편향(bias) 항을 갖습니다. 입력 데이터는 가중치로 곱해지고 편향이 결과에 더해집니다. 


(3) 활성화 함수(Activation Function): 입력과 편향의 가중 합은 활성화 함수를 통과합니다. 이는 신경망에 비선형성을 도입하여 데이터의 복잡한 관계를 학습할 수 있도록 합니다. 일반적인 활성화 함수로는 ReLU(렐루), 시그모이드, tanh 등이 있습니다. 


(4) 출력(Output): 활성화 함수의 결과는 해당 층의 출력이 되고, 이는 네트워크의 다음 층에 대한 입력으로 전달됩니다. 


(5) 과정 반복(Repeating the process): 2~4단계는 각각의 이전 층에 대해 반복되어 네트워크의 모든 층을 통과합니다. 최종 출력은 일반적으로 출력 층(Output Layer)에서 생성됩니다. 


순전파는 예측 단계에서 정보가 네트워크를 통과하는 방식입니다. 네트워크는 학습된 가중치와 편향을 사용하여 입력 데이터를 의미 있는 예측으로 변환합니다. 순전파의 출력은 실제 대상 값과 비교되며, 이 오차는 역전파(backpropagation) 단계에서 가중치와 편향을 업데이트하는 데 사용되어 네트워크가 시간이 지남에 따라 학습하고 성능을 향상시킬 수 있도록 합니다. 

 

Feedforward Neural Network

 

 

3. 오류 계산 (Calculate Error) 

 

신경망의 출력은 손실 함수(loss function)를 사용하여 실제 목표 값(실제 값)과 비교됩니다. 이는 예측 값과 실제 출력 간의 차이를 측정합니다. 이 손실 함수를 최소화하는 것이 목표로, 이는 네트워크의 예측이 가능한 한 실제 값에 가까워지도록 합니다. 

 

다양한 유형의 손실 함수가 있으며, 사용할 적절한 손실 함수의 선택은 해결하려는 문제의 성격에 따라 다릅니다. 일반적인 손실 함수에는 다음이 포함됩니다. 


(1) 평균 제곱 오차 (Mean Squared Error, MSE): 회귀 문제(Regression problem)에 적합하며, 예측된 값과 실제 값 간의 평균 제곱 차이를 계산합니다. 


(2) 이진 교차 엔트로피 손실 (Binary Cross-Entropy Loss): 이진 분류 문제(Binary classification problem)에 사용되며, 예측된 확률 분포와 실제 분포 간의 불일치를 측정합니다. 


(3) 다중 클래스 교차 엔트로피 손실 (Categorical Cross-Entropy Loss): 다중 클래스 분류 문제(Multi-class classification problem)에 적합하며, 각 클래스에 대한 예측된 확률 분포와 실제 분포 간의 불일치를 측정합니다. 


(4) 힌지 손실 (Hinge Loss): 서포트 벡터 머신(Support Vector Machine, SVM)에서 흔히 사용되며, 일부 분류 작업에서도 사용됩니다.

 

손실 함수의 선택은 학습 과정에 영향을 미치며, 주어진 작업의 특수한 요구사항과 특성과 일치하는 손실 함수를 선택하는 것이 중요합니다. 최적화 알고리즘은 훈련 중에 계산된 손실을 기반으로 모델 매개변수를 조정하여 시간이 지남에 따라 모델의 성능을 향상시키려고 노력합니다. 

 

 

4. 역방향 전파 (Backward Pass, Backpropagation)

 

역전파(Backward Pass, Backpropagation)는 신경망을 훈련시키는 중요한 단계입니다. 입력 데이터가 네트워크를 통과하여 예측을 생성하는 순전파 이후에는 역전파를 사용하여 계산된 손실에 기초해 모델의 매개변수(가중치와 편향)를 업데이트합니다. 이 과정의 목표는 예측된 출력과 실제 목표 값 사이의 차이를 최소화하는 것입니다.


아래는 역전파 오류 전파 과정의 주요 단계입니다.  


(1) 출력에 대한 손실의 그래디언트 계산 (Compute Gradient of the Loss with Respect to the Output) : 손실 함수의 그래디언트를 모델의 출력에 대해 계산합니다. 이 단계에서는 출력이 조정될 경우 손실이 얼마나 변하는지를 결정합니다. 

 

(2) 네트워크를 통한 그래디언트 역전파 (Compute Gradient of the Loss with Respect to the Output) : 그래디언트를 네트워크의 층을 통해 역방향으로 전파합니다. 이는 미적분의 연쇄 법칙을 적용하여 각 층의 가중치와 편향에 대한 손실의 그래디언트를 계산하는 것을 포함합니다. 

 

(3) 가중치와 편향 업데이트 (Update Weights and Biases) : 계산된 그래디언트를 사용하여 네트워크의 가중치와 편향을 업데이트합니다. 최적화 알고리즘인 확률적 경사 하강법(SGD) 또는 그 변형 중 하나는 손실을 줄이는 방향으로 매개변수를 조정합니다. 

 

(4) 다중 반복에 대해 반복 (Repeat for Multiple Iterations (Epochs)) : 단계 1-3은 여러 번의 반복 또는 에폭에 대해 반복되며 각 반복에서는 훈련 샘플의 배치가 처리됩니다. 이 반복적인 프로세스를 통해 네트워크는 매개변수를 조정하여 시간이 지남에 따라 성능을 향상시킬 수 있습니다. 

 

 

역전파의 수학적 세부 사항은 편미분(partial derivatives)과 연쇄 법칙(the chain rule)을 포함합니다. 구체적으로 그래디언트는 오류를 층을 통해 역방향으로 전파함으로써 계산되며, 가중치와 편향은 그래디언트의 반대 방향으로 업데이트됩니다.

 

 

[ The Chain Rule]

The Chain Rule

 

 

[ example: Partial Derivative of w1 with respect to E1 ]

Partial Derivative of w1 with respect to Error

 


역전파는 신경망을 훈련시키는 데 기본 개념으로, 네트워크가 실수에서 학습하고 매개변수를 계속해서 조정할 수 있게 합니다. 이 반복적인 과정을 통해 모델은 새로운, 보지 않은 데이터에 대한 예측을 더 잘 수행하게 됩니다.

 

 

5. 경사 하강법 (Gradient Descent)

 

계산된 도함수는 손실을 줄이는 방향으로 가중치와 편향을 업데이트하는 데 사용됩니다.
이를 위해 일반적으로 경사 하강 최적화 알고리즘(Optimization by Gradient-Descent)이 사용되며, 학습률에 의해 스케일링된 기울기의 일부를 빼서 가중치와 편향을 조절합니다. 

경사하강법 ( Gradient Descent )



- Eta (η)는 학습율(the learning rate)로서, 학습율은 기계 학습 및 신경망 훈련에서 사용되는 하이퍼파라미터 중 하나로, 각 훈련 단계에서 모델의 가중치를 조정하는 정도를 나타냅니다. 즉, 학습율은 가중치 업데이트의 크기(step size)를 결정하는 매개변수입니다.


높은 학습율은 각 단계에서 더 큰 가중치 업데이트를 의미하며, 이는 모델이 빠르게 학습할 수 있게 할 수 있습니다. 그러나 너무 높은 학습율은 수렴을 방해하고 발산(explosion)할 수 있기 때문에 조심스럽게 선택되어야 합니다.


낮은 학습율은 더 안정적인 학습을 제공하지만, 수렴하는 데 더 많은 시간이 걸릴 수 있습니다. 또한, 지역 최솟값(local optima)에서 빠져나오기 어려울 수 있습니다.


적절한 학습율을 선택하는 것은 모델의 성능과 수렴 속도에 큰 영향을 미칩니다. 학습율은 하이퍼파라미터 튜닝 과정에서 조정되며, 실험을 통해 최적의 값을 찾는 것이 일반적입니다. 일반적으로는 0.1, 0.01, 0.001과 같은 값을 시도하며 Cross-validation을 통해 모델의 성능을 평가하여 최적의 학습율을 결정합니다.

 

 

다음은 오차역전파법을 설명하기 위해 가상으로 만든, 각 레이어 별로 input size = 3, hidden size 1 = 3, hidden size 2 = 3, output size = 1 의 모델 아키텍처를 가지는 아주 간단한 PyTorch 예시가 되겠습니다.  MSE 를 손실함수로 하고, Adam optimizer를 사용하였습니다. 

 

import torch
import torch.nn as nn
import torch.nn.functional as F

# Define the neural network architecture with hyperparameters
class SimpleNet(nn.Module):
    def __init__(self, input_size, hidden_size_1, hidden_size_2, output_size):
        super(SimpleNet, self).__init__()
        self.fc1 = nn.Linear(input_size, hidden_size_1)
        self.bn1 = nn.BatchNorm1d(hidden_size_1)
        self.fc2 = nn.Linear(hidden_size_1, hidden_size_2)
        self.bn2 = nn.BatchNorm1d(hidden_size_2)
        self.fc3 = nn.Linear(hidden_size_2, output_size)

    def forward(self, x):
        x = F.relu(self.bn1(self.fc1(x)))
        x = F.relu(self.bn2(self.fc2(x)))
        x = self.fc3(x)

        return x

# Set Hyperparameters
input_size = 3
hidden_size_1 = 6
hidden_size_2 = 3
output_size = 1
epochs = 100
learning_rate = 0.001

# Initiate the network
model = SimpleNet(input_size, hidden_size_1, hidden_size_2, output_size)

print(model)
# SimpleNet(
#   (fc1): Linear(in_features=3, out_features=6, bias=True)
#   (bn1): BatchNorm1d(6, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
#   (fc2): Linear(in_features=6, out_features=3, bias=True)
#   (bn2): BatchNorm1d(3, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
#   (fc3): Linear(in_features=3, out_features=1, bias=True)
# )

 

 

GPU 를 사용할 수 있으면 Device를 "cuda"로, CPU를 사용할 수 있으면 Device를 "cpu"로 설정해줍니다. 아래 예에서는 Tesla V100 16G GPU를 사용하므로 "cuda"로 Device를 설정해주었습니다. 

 

# Check for CUDA availability
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

print(device)
# cuda


#### when you run at Jupyter Notebook, add '!' at the beginning.
! nvidia-smi -L
# GPU 0: Tesla V100-SXM2-16GB (UUID: GPU-d445249d-b63e-5e62-abd1-7646bb409870)

 

 

 

Loss 함수는 MSE (Mean Squared Error) 로 설정하였고, Optimizer는 Adam 으로 설정해주었습니다. 

모델 훈련에 사용할 데이터셋을 난수를 발생시켜서 생성하고, 위에서 지정한 Device 에 모델의 파라미터와 버퍼, 그리고 데이터를 이동시켰습니다 (modeo.to(device), torch.randn((100, input_size)).to(device)). 

 

# Move the model's parameters and buffers to the specified device.
model.to(device)

# Set Loss and Optimizer
criterion = nn.MSELoss()
optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)


# Generate dummy input and target data, move the data to the specified device
inputs = torch.randn((100, input_size)).to(device)
targets = torch.randn((100, output_size)).to(device)

 

 

 

Epochs = 100 만큼 for loop 순환문을 사용해서 반복하면서 모델을 훈련 시킵니다. Forward pass, Calculate loss, Backward pass, Update weights (Optimize) 의 과정을 반복합니다. 

 

# Training phase
model.train() # training mode
for epoch in range(epochs):
    # Forward pass
    output = model(inputs)
    loss = criterion(output, targets) # calculate loss

    # Backward pass and optimize
    optimizer.zero_grad() # clear previous gradients
    loss.backward() # compute gradients, backward pass
    optimizer.step() # update weights

    # Print training statistics
    if (epoch + 1) % 10 == 0:
        print(f'Epoch [{epoch+1}/{epochs}], Loss: {loss.item():.4f}')

# Epoch [10/100], Loss: 0.9778
# Epoch [20/100], Loss: 0.9476
# Epoch [30/100], Loss: 0.9217
# Epoch [40/100], Loss: 0.9000
# Epoch [50/100], Loss: 0.8826
# Epoch [60/100], Loss: 0.8674
# Epoch [70/100], Loss: 0.8534
# Epoch [80/100], Loss: 0.8423
# Epoch [90/100], Loss: 0.8329
# Epoch [100/100], Loss: 0.8229

 

 

위에서 훈련한 모델에 가상으로 만든 1개의 샘플 데이터를 인풋으로 넣어서 예측을 해보겠습니다. 배치가 아니가 단 1개의 샘플 데이터만 사용하기 때문에 model.eval() 을 해주었습니다. (만약 model.eval() 을 추가해주지 않으면, ValueError: Expected more than 1 value per channel when training, got input size torch.Size([1, 2]) 가 발생합니다.)

 

# Example of using the trained model
test_input = torch.randn((1, input_size)).to(device)

model.eval() # evaluation mode
with torch.no_grad():
    predicted_output = model(test_input)

print("Test Input:", test_input)
print("Predicted Output:", predicted_output.item())

# Test Input: tensor([[ 0.3862,  0.1250, -0.3322]], device='cuda:0')
# Predicted Output: -0.00717635452747345

 

 

위에서 학습이 완료된 인공신경망 모델의 레이어 이름과 파라미터별 가중치, 편향에 접근해서 확인해보도록 하겠습니다. 

 

## (1) Access to the names and parameters in the trained model
for name, param in model.named_parameters():
    print(name)
    print(param)
    print("-----" * 15)

# fc1.weight
# Parameter containing:
# tensor([[-0.0542, -0.5121,  0.3793],
#         [-0.5354, -0.1862,  0.4207],
#         [ 0.3339, -0.0813, -0.2598],
#         [ 0.5527, -0.3505, -0.4357],
#         [-0.0351, -0.0992,  0.1252],
#         [-0.2110,  0.0713, -0.2545]], device='cuda:0', requires_grad=True)
# ---------------------------------------------------------------------------
# fc1.bias
# Parameter containing:
# tensor([ 0.5726,  0.1125,  0.4138, -0.1896,  0.1353, -0.0372], device='cuda:0',
#        requires_grad=True)
# ---------------------------------------------------------------------------
# bn1.weight
# Parameter containing:
# tensor([0.9044, 0.9735, 1.0557, 0.9440, 1.0983, 1.0414], device='cuda:0',
#        requires_grad=True)
# ---------------------------------------------------------------------------
# bn1.bias
# Parameter containing:
# tensor([-0.1062,  0.0177,  0.0283, -0.0527,  0.0059, -0.0117], device='cuda:0',
#        requires_grad=True)
# ---------------------------------------------------------------------------
# fc2.weight
# Parameter containing:
# tensor([[-0.3057,  0.0730,  0.2711,  0.0770,  0.0497,  0.4484],
#         [-0.3078,  0.0153,  0.1769,  0.2855, -0.4853,  0.2110],
#         [-0.3342, -0.2749,  0.3440, -0.2844,  0.0763, -0.1959]],
#        device='cuda:0', requires_grad=True)
# ---------------------------------------------------------------------------
# fc2.bias
# Parameter containing:
# tensor([-0.0536, -0.3275,  0.0371], device='cuda:0', requires_grad=True)
# ---------------------------------------------------------------------------
# bn2.weight
# Parameter containing:
# tensor([1.0843, 0.9524, 0.9426], device='cuda:0', requires_grad=True)
# ---------------------------------------------------------------------------
# bn2.bias
# Parameter containing:
# tensor([-0.0564,  0.0576, -0.0848], device='cuda:0', requires_grad=True)
# ---------------------------------------------------------------------------
# fc3.weight
# Parameter containing:
# tensor([[-0.4607,  0.4394, -0.1971]], device='cuda:0', requires_grad=True)
# ---------------------------------------------------------------------------
# fc3.bias
# Parameter containing:
# tensor([-0.0125], device='cuda:0', requires_grad=True)
# ---------------------------------------------------------------------------

 

 

 

## (2) Access to the weights and biases in the trained model
print("[ Model FC1 Weights ]")
print(model.fc1.weight.cpu().detach().numpy())
print("-----" * 10)
print("[ Model FC2 Weights ]")
print(model.fc2.weight.cpu().detach().numpy())

# [ Model FC1 Weights ]
# [[-0.05423066 -0.51214963  0.37927148]
#  [-0.53538287 -0.18618222  0.42071185]
#  [ 0.33391565 -0.08127454 -0.2597795 ]
#  [ 0.55268896 -0.3505293  -0.4356556 ]
#  [-0.03507916 -0.09922788  0.1251672 ]
#  [-0.21096292  0.07126608 -0.25446963]]
# --------------------------------------------------
# [ Model FC2 Weights ]
# [[-0.30573702  0.07304495  0.27107015  0.07700069  0.04968859  0.44840544]
#  [-0.3078182   0.01533379  0.17685111  0.28549835 -0.485255    0.21100621]
#  [-0.33416617 -0.27492833  0.3440289  -0.28436708  0.07630474 -0.19592032]]

 

 

이번 포스팅이 많은 도움이 되었기를 바랍니다. 

행복한 데이터 과학자 되세요.  :-)

 

728x90
반응형
Posted by Rfriend
,

자동미분(Automatic differentiation, Algorithmic differentiation, computational differentiation, auto-differentiation, 또는 줄여서 간단히 auto-diff) 은 컴퓨터 프로그램에 의해서 구체화된 함수의 미분을 산술적으로 계산할 때 사용하는 기술의 집합을 말합니다. 컴퓨터 프로그램에서 구체화한 함수는 아무리 복잡해보이더라도 기본적인 산술 연산 (덧셈, 뺄셈, 곱셉, 나눗셈 등)과 기본적인 함수 (지수, 로그, 싸인, 코싸인 등) 의 연속적인 실행으로 이루어진다는 아이디어를 기반으로 합니다. 복잡한 함수도 연쇄 법칙(Chain Rule)을 이용함으로써 합성함수를 구성하는 각 기본함의 미분의 곱으로 나타내고 반복적으로 계산함으로써 자동으로 복잡한 함수를 정확하고 효율적으로 미분할 수 있습니다.   


자동미분(Automatic Differentiation)은 딥러닝에서 오차 역전파 알고리즘을 사용해서 모델을 학습할 때 유용하게 사용합니다. TensorFlow는 Gradient Tapes 를 이용하면 즉시 실행 모드(eager execution mode)에서 쉽게 오차 역전파를 수행할 수 있습니다. 


이번 포스팅에서는 


1. 즉시실행모드에서 자동미분을 하기 위해 Gradient tapes 이 필요한 이유

2. TensorFlow에서 Gradient tapes를 이용해 자동미분하는 방법

3. 시그모이드 함수 자동미분 시각화

4. 테이프가 볼 것을 조정하는 방법 (Controlling what the tape watches)

5. Python 조건절 분기문을 이용한 테이프 기록 흐름 조정 (Control flow)


에 대해서 소개하겠습니다. 


이번 포스팅은 TensorFlow 튜토리얼의 내용을 번역하고 코드를 대부분 사용하였습니다. 


먼저 numpy, tensorflow, matplotlib 모듈을 importing 하겠습니다. 



import numpy as np

import matplotlib.pyplot as plt

import tensorflow as tf


print(tf.__version__)

2.4.0-dev20200913




  1. 즉시 실행 모드에서 자동미분을 하기 위해 Gradient tapes 이 필요한 이유


예를 들어서  y = a * x 라는 방정식에서 a 와 y 가 상수(constant)이고 x 가 변수(Varialbe) 일 때, 이 방정식을 오차역전파법으로 풀어서 변수 x 를 구하고자 한다고 합시다. 그러면 간단한 손실함수인 loss = abs(a * x - y) 를 최소로 하는 x 를 구하면 됩니다. 


아래 예의 경우 8.0 = 2.0 * x 방정식 함수로 부터 변수 x 의 값을 구하고자 합니다. x 를 10.0 에서 시작해서 abs(a * x - y) 손실함수 값을 최소로 하는 x 를 구하려면 '손실함수에 대한 x의 미분 (the gradient of the loss with respect to x)를 구해서 x 값을 미분값만큼 빼서 갱신해주어야 합니다. 


그런데 아래의 TensorFlow 2.x 버전에서의 Python 즉시실행모드(eager mode)에서 손실(Loss) 을 "바로 즉시 계산"(eager execution)해보려서 출력 결과를 보면 numpy=12.0 인 Tensor 상수입니다. 여기서 자동미분을 하려고 하면 문제가 생기는데요, 왜냐하면 자동미분을 하기 위해 필요한 함수와 계산 식의 연산 과정과 입력 값에 대한 정보가 즉시실행모드에서는 없기 때문입니다. (입력 값과 연산 과정은 저장 안하고 즉시실행해서 결과만 출력했음).  



a, y = tf.constant(2.0), tf.constant(8.0)

x = tf.Variable(10.0) # In practice, we start with a random value

loss = tf.math.abs(a * x - y)


loss

[Out] <tf.Tensor: shape=(), dtype=float32, numpy=12.0>

 




  2. TensorFlow에서 Gradient tapes를 이용해 자동미분하는 방법


이 문제를 해결하기 위해 TensorFlow 는 중간 연산 과정(함수, 연산)을 테이프(tape)에 차곡차곡 기록해주는 Gradient tapes 를 제공합니다. 


with tf.GradientTape() as tape: 로 저장할 tape을 지정해주면, 이후의 GradientTape() 문맥 아래에서의 TensorFlow의 연관 연산 코드는 tape에 저장이 됩니다. 이렇게 tape에 저장된 연산 과정 (함수, 연산식) 을 가져다가 TensorFlow는 dx = tape.gradient(loss, x) 로 후진 모드 자동 미분 (Reverse mode automatic differentiation) 방법으로 손실에 대한 x의 미분을 계산합니다. 이렇게 계산한 손실에 대한 x의 미분을 역전파(backpropagation)하여 x의 값을 갱신(update)하는 작업을 반복하므로써 변수 x의 답을 찾아가는 학습을 진행합니다. 


위의 (1)번에서 소개한 8.0 = 2.0 * x 의 방정식에서는 변수 x = 4.0 이 답인데요, TensorFlow 의 GradientTape() 과 tape.gradient() 를 사용해서 오차 역전파 방법으로 갱신하여 문제를 풀어보겠습니다. 처음에 x = 10.0에서 시작형 4회 반복하니 x = 4.0 으로 답을 잘 찾아갔군요.  



# UDF for training

def train_func():

    with tf.GradientTape() as tape:

        loss = tf.math.abs(a * x - y)

    

    # calculate gradient

    dx =  tape.gradient(loss, x)

    print('x = {}, dx = {:.2f}'.format(x.numpy(), dx))

    

    # update x <- x - dx

    x.assign(x - dx)



# Run train_func() UDF repeately

for i in range(4):

    train_func()


[Out]
x = 10.0, dx = 2.00
x = 8.0, dx = 2.00
x = 6.0, dx = 2.00
x = 4.0, dx = 0.00

 




라는 함수에서 (Target) y에 대한 (Source) x의 미분 (derivative of target y with respect to source x) 을 TensorFlow의 GradientTape.gradient() 메소드를 사용해서 계산해보겠습니다. (우리는 이미 고등학교 때 배웠던 미분에 따라 라는 것을 알고 있습니다. x = 4.0 이므로 dy/dx 를 풀면 2 * 4.0 = 8.0 이겠군요.)


GradientTape.gradient(target, sources)



x = tf.Variable(4.0)


with tf.GradientTape() as tape:

    y = x ** 2

    

    

# dy = 2x * dx

dy_dx = tape.gradient(y, x)

dy_dx.numpy()

[Out] 8.0

 




위의 간단한 예에서는 스칼라(Scalar) 를 사용하였다면, tf.GradientTape() 은 어떤 형태의 텐서에 대해서도 사용할 수 있습니다. GradientTape.gradient(target, sources) 에서 sources 에는 여러개 변수를 리스트나 사전형(Dictionary) 형태로 입력해줄 수 있습니다.  



# tf.GradientTape works as easily on any tensor. 

w = tf.Variable(tf.random.normal((4, 2)), name='w')

b = tf.Variable(tf.zeros(2, dtype=tf.float32), name='b')

x = [[1.0, 2.0, 3.0, 4.0]]


with tf.GradientTape(persistent=True) as tape:

    y = x @ w + b

    loss = tf.reduce_mean(y ** 2)

    

# To get the gradienet of y w.r.t both variables, w and b

[dl_dw, dl_db] = tape.gradient(loss, [w, b])



print('loss:', loss)

print('w:', w)

print('b:', b)

[Out]
loss: tf.Tensor(74.95752, shape=(), dtype=float32)
w: <tf.Variable 'w:0' shape=(4, 2) dtype=float32, numpy=
array([[-0.05742593, -0.06279723],
       [-0.7892129 ,  1.8175325 ],
       [ 3.1122675 ,  0.12920259],
       [ 0.8164586 ,  0.3712036 ]], dtype=float32)>
b: <tf.Variable 'b:0' shape=(2,) dtype=float32, numpy=array([0., 0.], dtype=float32)>

 



Target에 대한 Source의 미분 결과는 Source의 형상(shape)을 따릅니다. 가령, 위의 예에서 손실함수(Loss)에 대한 가중치(w)의 미분 (derivative of loss with respect to weight) 의 결과는 Source에 해당하는 가중치(w)의 형상을 따라서 (4, 2) 가 됩니다. 



print('shape of w:', w.shape)

print('shape of dl_dw:', dl_dw.shape)

[Out]
shape of w: (4, 2)
shape of dl_dw: (4, 2)

 




  3. 시그모이드 함수의 자동미분 시각화 (Auto-diff plot of Sigmoid function)


딥러닝에서 활성화 함수 중의 하나로 사용되는 S 곡선 모양의 시그모이드 함수 (Sigmoid function) 은 아래와 같습니다. 


이 시그모이드 함수를 x 에 대해서 미분하면 아래와 같습니다. 


시그모이드 함수와 시그모이드 함수의 미분 값을 x 가 -10.0에서 10.0 사이의 200개의 관측치에 대해 TensorFlow의 tf.GradientTape() 와 GradientTae=pe.gradient(Sigmoid_func, x) 로 계산해서 하나의 그래프에 겹쳐서 시각화를 해보겠습니다. 



x = tf.linspace(-10.0, 10.0, 200+1)


with tf.GradientTape() as tape:

    tape.watch(x)

    y = tf.nn.sigmoid(x)


# For an element-wise calculation, 

# the gradient of the sum gives the derivative of each element

# with respect to its input-element, since each element is independent.

dy_dx = tape.gradient(y, x)



# plot of y, dy/dx

plt.plot(x, y, 'r-', label='y')

plt.plot(x, dy_dx, 'b--', label='dy/dx')

plt.legend()

_ = plt.xlabel('x')


plt.show()






  4. 테이프가 "볼(저장)" 것을 조정하는 방법 (Colltrolling whtat the tape "watches")


TensorFlow 가 오차역전파 알고리즘으로 학습(training by backpropagation algorithm)할 때 사용하는 자동미분은 '학습 가능한 변수 (trainable tf.Variable)'를 대상으로 합니다. 


따라서 만약에 (1) 변수가 아닌 상수 ('tf.Tensor') 라면 TensorFlow가 안 보는 (not watched), 즉 기록하지 않는 (not recorded) 것이 기본 설정이므로 자동 미분을 할 수 없으며, (2) 변수(tf.Variable) 더라도 '학습 가능하지 않으면 (not trainable)' 자동 미분을 할 수 없습니다



# A trainable variable

x0 = tf.Variable(3.0, name='x0')


# Not trainable

x1 = tf.Variable(3.0, name='x1', trainable=False)


# Not a Variable: A variable + tensor returns a tensor.

x2 = tf.Variable(2.0, name='x2') + 1.0


# Not a variable

x3 = tf.constant(3.0, name='x3')


with tf.GradientTape() as tape:

    y = (x0**2) + (x1**2) + (x2**2)


# Only x0 is a trainable tf.Variable, --> can calculate gradient

grad = tape.gradient(y, [x0, x1, x2, x3]) 



for g in grad:

    print(g)


[Out] 

tf.Tensor(6.0, shape=(), dtype=float32) # <-- dy_dx0, trainable variable None # <-- dy_dx1, not trainable None # <-- dy_dx2, not a variable None # <-- dy_dx3, not a variable

 



y 에 대한 tf.Tensor 인 x의 미분을 구하기 위해서는 GradientTape.watch(x) 를 호출해서 tf.Tensor 인 x를 테이프에 기록해주면 됩니다. 



x = tf.constant(3.0)

with tf.GradientTape() as tape:

    tape.watch(x) # watch 'x' and then record it onto tape

    y = x**2


# dy = 2x * dx

dy_dx = tape.gradient(y, x)

print(dy_dx.numpy())

[Out] 6.0

 



TensorFlow의 테이프에 기록되는 '학습 가능한 변수'는 GradientTape.watched_variables() 메소드로 확인할 수 있습니다. 



[var.name for var in tape.watched_variables()]

[Out] ['Variable:0']

 



반대로, 모든 변수(tf.Variable)를 테이프에 기록하는 TensorFlow의 기본설정을 비활성화(disable)하려면 watch_accessed_variables=False 으로 매개변수를 설정해주면 됩니다. 

아래의 예에서는 x0, x1 의 두 개의 tf.Variable 모두를 watch_accessed_variables=False 로 비활성화해놓고, GradientTape.watch(x1) 으로 x1 변수만 테이프에 기록해서 자동 미분을 해보겠습니다. 



x0 = tf.Variable(0.0)

x1 = tf.Variable(10.0)


with tf.GradientTape(watch_accessed_variables=False) as tape:

    tape.watch(x1)

    y0 = tf.math.sin(x0)

    y1 = tf.nn.softplus(x1)

    y = y0 + y1

    ys = tf.reduce_sum(y)



# dy = 2x * dx

grad = tape.gradient(ys, {'x0': x0, 'x1': x1})


print('dy/dx0:', grad['x0'])

print('dy/dx1:', grad['x1'].numpy())

[Out]

dy/dx0: None dy/dx1: 0.9999546

 




  5. Python 조건절을 사용한 테이프 기록 흐름을 조정

     (Control flow using Python condition statements)


Python 의 조건절을 이용한 분기문을 이용하면 TensorFlow 의 tf.GradientTape() 맥락 아래에서 테이프에 기록하는 연산 과정의 흐름을 조건절에 해당하는 것만 취사선택해서 기록할 수 있습니다. 


아래 예에서는 if x > 0.0 일 때는 result = v0, 그렇지 않을 때는(else) result = v1 ** 2 로 result 의 연산을 다르게 해서 테이프에 기록하고, GradientTape.gradient() 로 자동미분 할 때는 앞서 if else 조건절에서 True에 해당되어서 실제 연산이 일어났던 연산에 미분을 연결해서 계산하라는 코드입니다. 


아래 예에서는 x = tf.constant(1.0) 으로서 if x > 0.0 조건절을 만족(True)하므로 result = v0 연산이 실행되고 뒤에 tape.gradient()에서 자동미분이 되었으며, else 조건절 아래의 연산은 실행되지 않았으므로 뒤에 tape.gradient()의 자동미분은 해당되지 않았습니다 (dv1: None)



x = tf.constant(1.0)


v0 = tf.Variable(2.0)

v1 = tf.Variable(2.0)


with tf.GradientTape(persistent=True) as tape:

    tape.watch(x)


    # Python flow control

    if x > 0.0:

        result = v0

    else:

        result = v1**2 


dv0, dv1 = tape.gradient(result, [v0, v1])


print('dv0:', dv0)

print('dv1:', dv1)

[Out] 
dv0: tf.Tensor(1.0, shape=(), dtype=float32)
dv1: None

 



[ Reference ]

* Automatic Differentiation: https://en.wikipedia.org/wiki/Automatic_differentiation

* TensorFlow Gradient tapes and Automatic differentiation
   : https://www.tensorflow.org/guide/autodiff?hl=en


많은 도움이 되었기를 바랍니다. 

이번 포스팅이 도움이 되었다면 아래의 '공감~'를 꾹 눌러주세요. 



728x90
반응형
Posted by Rfriend
,