다중분류를 위한 대표적인 손실함수, torch.nn.CrossEntropyLoss

딥러닝의 많은 이론 중 가장 중요한 부분이 손실함수와 역전파입니다. PyTorch에서는 다양한 손실함수를 제공하는데, 그 중 torch.nn.CrossEntropyLoss는 다중 분류에 사용됩니다. torch.nn.CrossEntropyLoss는 nn.LogSoftmax와 nn.NLLLoss의 연산의 조합입니다. nn.LogSoftmax는 신경망 말단의 결과 값들을 확률개념으로 해석하기 위한 Softmax 함수의 결과에 log 값을 취한 연산이고, nn.NLLLoss는 nn.LogSoftmax의 log 결과값에 대한 교차 엔트로피 손실 연산(Cross Entropy Loss|Error)입니다.

Softmax와 교차 엔트로피 손실 연산에 대한 각각의 설명은 아래와 같습니다.

활성화 함수(Activation Function)

손실함수(Loss Function)

참고로 nn.NLLLoss의 구현 코드는 아래와 같습니다.

import torch

def NLLLoss(logs, targets):
    out = torch.zeros_like(targets, dtype=torch.float)
    for i in range(len(targets)):
        out[i] = logs[i][targets[i]]
    return -out.sum()/len(out)

물론 PyTorch에서도 torch.nn.NLLLoss를 통해 위와 동일한 기능을 제공합니다. 결과적으로 Softmax의 Log 결과를 Cross Entropy Loss 값의 결과를 얻기 위해 3가지 방식이 존재하는데, 아래와 같습니다.

x = torch.Tensor([[0.8982, 0.805, 0.6393, 0.9983, 0.5731, 0.0469, 0.556, 0.1476, 0.8404, 0.5544]])
y = torch.LongTensor([1])

# Case 1
cross_entropy_loss = torch.nn.CrossEntropyLoss()
print(cross_entropy_loss(x, y)) # tensor(2.1438)

# Case 2
log_softmax = torch.nn.LogSoftmax(dim=1)
x_log = log_softmax(x)
print(NLLLoss(x_log, y)) # tensor(2.1438)

# Case 3
nll_loss = torch.nn.NLLLoss()
print(nll_loss(x_log, y)) # tensor(2.1438)

위의 세가지 방식 중 torch.nn.CrossEntropyLoss처럼 연산을 한번에 처리하는 것이 수식이 간소화되어 역전파가 더 안정적으로 이루지므로 실제 사용에 권장됩니다.

torch.nn.CrossEntropyLoss를 이용하여 손실값을 구하는 것에 초점을 맞춰보면.. 먼저 torch.nn.CrossEntropyLoss의 수식은 다음과 같습니다.

    $$loss(x,class)=-\log\biggl(\frac{\exp(x[class])}{\sum_{j}{\exp(x[j])}}\biggr)=-x[class]+\log\biggl(\sum_{j}{\exp(x[j])}}\biggr)$$

위의 수식을 살펴보면 앞서 언급한 것처럼 Softmax와 Log처리 및 Cross Entropy Loss 연산의 조합이라는 것을 알수 있습니다.

torch.nn.CrossEntropyLoss를 코드를 통해 설명하면… 예를들어 신경망 말단에서 총 10개의 값(앞서 언급한 x값)이 출력되었고, 실제 레이블 값(앞서 언급한 y 또는 class)은 1일때에 손실값을 구하는 코드는 아래와 같습니다.

import torch
import torch.nn as nn
import numpy as np

output = torch.Tensor([[0.8982, 0.805, 0.6393, 0.9983, 0.5731, 0.0469, 0.556, 0.1476, 0.8404, 0.5544]])
target = torch.LongTensor([1])

criterion = nn.CrossEntropyLoss()
loss = criterion(output, target)
print(loss) # tensor(2.1438)

참고로 위의 코드에서 사용된 nn.CrossEntropyLoss의 수식을 알고 있으므로 nn.CrossEntropyLoss을 사용하지 않고 직접 손실값을 계산한다면 다음과 같습니다.

import torch
import torch.nn as nn
import numpy as np

output = [0.8982, 0.805, 0.6393, 0.9983, 0.5731, 0.0469, 0.556, 0.1476, 0.8404, 0.5544]
target = [1]
loss = np.log(sum(np.exp(output))) - output[target[0]]
print(loss) # 2.143818427948945

손실값이 필요할 때는 신경망의 학습인데, 학습에서 데이터는 GPU 자원을 최대한 활용하기 위해 배치 단위로 처리됩니다. 즉, 앞서 언급한 것처럼 1개 단위가 아닌 2개 이상의 데이터가 한꺼번에 들어온다는 것입니다. 이에 대한 처리에 대한 예는 다음 코드와 같습니다.

import torch
import torch.nn as nn
import numpy as np

output = torch.Tensor(
    [
        [0.8982, 0.805, 0.6393, 0.9983, 0.5731, 0.0469, 0.556, 0.1476, 0.8404, 0.5544],
        [0.9457, 0.0195, 0.9846, 0.3231, 0.1605, 0.3143, 0.9508, 0.2762, 0.7276, 0.4332]
    ]
)

target = torch.LongTensor([1, 5])

criterion = nn.CrossEntropyLoss()
loss = criterion(output, target)
print(loss) # tensor(2.3519)

위의 코드를 nn.CrossEntropyLoss()를 사용하지 않고 계산한다면 다음 코드와 같구요.

import torch
import torch.nn as nn
import numpy as np

output = [0.8982, 0.805, 0.6393, 0.9983, 0.5731, 0.0469, 0.556, 0.1476, 0.8404, 0.5544]
target = [1]
loss1 = np.log(sum(np.exp(output))) - output[target[0]]

output = [0.9457, 0.0195, 0.9846, 0.3231, 0.1605, 0.3143, 0.9508, 0.2762, 0.7276, 0.4332]
target = [5]
loss2 = np.log(sum(np.exp(output))) - output[target[0]]

print((loss1 + loss2)/2) # 2.351937720511233

즉, 배치 처리에 대한 손실값은 배치를 구성하는 각 데이터의 손실값들의 평균이라는 점을 확인할 수 있습니다.

파이썬의 matplotlib 노트

파이썬의 matplotlib는 수치 데이터를 그래프로 효과적으로 표시해주는 API입니다. 이에 대해 간단한 활용 예시에 대한 코드를 기록해 둡니다.

import matplotlib.pyplot as plt

x = [1, 2, 3, 4, 5]
y = [1, 2, 3, 4, 5]

plt.scatter(x, y)

plt.show()

X축과 Y축에 대한 포인트 데이터를 표시하는 코드입니다. 결과는 다음과 같습니다.

그래프에서 포인트의 크기와 색상, 투명도를 지정하는 예제는 다음과 같습니다.

import matplotlib.pyplot as plt

x = [1, 2, 3, 4, 5]
y = [1, 2, 3, 4, 5]
s = [10, 20, 30, 40, 50]

plt.scatter(x = x, y = y, s = s, c = 'red', alpha=0.5)

plt.show()

결과는 다음과 같습니다.

다음은 꺽은선 그래프입니다.

import numpy as np
import matplotlib.pyplot as plt

x = np.arange(0, 10, 0.5)
y = np.sin(x)

plt.plot(x, y)
plt.show()

x축과 y축의 데이터는 4번과 5번 코드에서 정의합니다. 결과는 다음과 같습니다.

하나의 차트에 여러개의 그래프를 동시에 표시하고, 추가적으로 제목, 축이름 등을 표시하는 코드입니다.

import numpy as np
import matplotlib.pyplot as plt

x = np.arange(0, 10, 0.5)
y1 = np.sin(x)
y2 = np.cos(x)

plt.plot(x, y1, label="sin(x)")
plt.plot(x, y2, label="cos(x)", linestyle="--")

plt.xlabel("x")
plt.xlabel("y")

plt.title("sin & cos")
plt.legend()

plt.show()

3차원 차트의 경우 먼저 X, Y축에 대한 데이터와 이 X, Y를 변수로 하여 계산된 Z 값의 함수가 정의해야 합니다. 이렇게 정의된 X, Y, Z에 대한 3차원 그래프는 아래의 예제 코드를 통해 3차원 차트로 시각화할 수 있습니다.

import numpy as np
from mpl_toolkits.mplot3d import Axes3D
import matplotlib.pyplot as plt

X = np.arange(-5, 5, 0.25)
Y = np.arange(-5, 5, 0.25)
X, Y = np.meshgrid(X, Y)
Z = X**2 + Y**2

fig = plt.figure()
ax = fig.gca(projection='3d')
surf = ax.plot_wireframe(X, Y, Z, color='black')

plt.show()

여러개의 차트를 동시에 표시하는 경우입니다.

import matplotlib.pyplot as plt
import numpy as np

x = np.linspace(0, np.pi * 3, 100)
fig, axes = plt.subplots(2,2)

axes[0][0].plot(x, np.sin(x))
axes[0][1].plot(x, np.arccos(x))
axes[1][0].plot(x, np.cos(x))
axes[1][1].plot(x, np.arcsin(x))

plt.show()

아래는 차트를 그리는 스타일을 지정하고 범례를 표현하는 코드입니다.

import matplotlib.pyplot as plt
import numpy as np

x = np.arange(-10,10)
y = x**2

plt.plot(x, y, 
    linewidth=2, color='green', linestyle=':', 
    marker='*', markersize=10, markerfacecolor='yellow', markeredgecolor='red', 
    label='y=x^2')
   
plt.legend()

plt.show()

결과는 다음과 같습니다.