이 튜토리얼에서는 [MNIST 데이터셋](http://yann.lecun.com/exdb/mnist/)과 PyTorch를 이용하여 손글씨 숫자를 예측하는 방법에 대해 설명합니다.
필요한 Python 패키지와 이 튜토리얼의 실행에 사용된 Python과 주요 패키지의 버전 정보는 다음과 같습니다.
```python
import sys
import sklearn.datasets
import torch
from torch import nn, optim
import torch.nn.functional as F
from torch.utils.data import DataLoader, TensorDataset
import matplotlib.pyplot as plt
print(f"Python: {sys.version}")
print(f"pytorch: {torch.__version__}")
```
```plain
Python: 3.6.9 (default, Nov 7 2019, 10:44:02)
[GCC 8.3.0]
pytorch: 1.4.0
```
## 데이터 준비
[OpenML](https://openml.org/)에서 제공하는 MNIST 데이터셋을 [Scikit-learn](https://scikit-learn.org/stable/modules/generated/sklearn.datasets.fetch_openml.html)을 통해 불러와서 사용하겠습니다.
다운받은 데이터셋을 훈련 데이터와 시험 데이터로 나눕니다.
```python
mnist = sklearn.datasets.fetch_openml('mnist_784', data_home="mnist_784")
```
MNIST 데이터셋의 데이터는 0부터 255까지의 정수인 회색조 값으로 저장되어 있습니다.
이를 0부터 1 사이의 실수로 바꾸고, PyTorch를 이용하기 위해 `torch.Tensor`로 바꿉니다.
```python
x_train = torch.tensor(mnist.data[:60000], dtype=torch.float) / 255
y_train = torch.tensor([int(x) for x in mnist.target[:60000]])
x_test = torch.tensor(mnist.data[60000:], dtype=torch.float) / 255
y_test = torch.tensor([int(x) for x in mnist.target[60000:]])
```
몇 개의 샘플을 그려 보면 다음과 같습니다.
```python
fig, axes = plt.subplots(2, 4, constrained_layout=True)
for i, ax in enumerate(axes.flat):
ax.imshow(1 - x_train[i].reshape((28, 28)), cmap="gray", vmin=0, vmax=1)
ax.set(title=f"{y_train[i]}")
ax.set_axis_off()
```
## 예측 모델
단순한 선형 형태의 모델을 만들어 보겠습니다.
784개의 피처(각 픽셀)를 갖는 데이터 $\mathbf{x}$(즉, 크기가 784인 벡터)와 정답 $t$가 주어졌을 때 크기가 10 x 784인 weight 행렬 $W$와 크기가 10인 bias 벡터 $\mathbf{b}$가 있다면
$$
W\mathbf{x} + \mathbf{b}
$$
을 계산하면 나오는 값 10개 중에 $\mathbf{x}$의 실제 정답에 해당하는 인덱스의 값 $\mathbf{x}_{t}$가 가장 높게 나오도록 하는 $W$와 $\mathbf{b}$를 찾으려고 합니다.
여기서 위 결괏값을 확률분포처럼 일종의 “정규화”를 할 수 있도록 [softmax 함수](https://en.wikipedia.org/wiki/Softmax_function)를 사용하면 각 원소는 0부터 1 사이의 값을 갖고, 각 원소의 합이 1이 되도록 하는 모델 $f(\mathbf{x}) = \sigma(W\mathbf{x} + \mathbf{b})$를 만들 수 있습니다.
$$
\mathbf{y} = f(\mathbf{x}) = \sigma(W\mathbf{x} + \mathbf{b}) \\\\
\text{where } \sigma(\mathbf{x})\_i = \frac{e^{x\_i}}{\sum\_{j = 0}^9 e^{x\_j}}
$$
## 비용 함수
Negative log-likelihood를 이용하여, 예측이 올바르면 값이 작고, 예측이 틀린 경우 값이 커지는 비용함수를 만듭니다.
$$
L(\mathbf{y}) = \mathrm{NLL}(\mathbf{y}) = -\log(\mathbf{y}_{t})
$$
예를 들어 정답이 5인 데이터의 예측값이 5일 확률을 0.95라고 예측했다면 비용은 $-\log(0.95) = 0.051$이고, 0.05라고 (아주 잘못) 예측했다면 $-\log(0.95) = 2.996$이 됩니다.
## 학습
위와 같은 모델에서 학습이란 비용함수를 줄이도록 $W$와 $\mathbf{b}$의 값을 찾는 것을 말합니다.
학습 데이터 $\mathbf{x}$와 그 답 $t$가 주어지면 비용 $L$을 계산할 수 있고, $\frac{\partial L}{\partial W}$와 $\frac{\partial L}{\partial \mathbf{b}}$를 계산할 수 있습니다.
그러므로 경사 하강법 등을 이용하여 $W$와 $\mathbf{b}$ 바꿔 가면서 $L$을 최소화시키도록 하는 것이 "학습" 과정입니다. 즉, 다음 과정을 반복합니다.
$$
\\begin{aligned}
W &\\leftarrow W - \\eta \\frac{\\partial L}{\\partial W} \\\\
\\mathbf{b} &\\leftarrow \\mathbf{b} - \\eta \\frac{\\partial L}{\\partial \\mathbf{b}}
\\end{aligned}
$$
PyTorch를 이용하여 모델을 구현하고, `autograd`를 이용하여 경사를 구해 $W$와 $\mathbf{b}$를 업데이트하는 방법은 다음과 같습니다.
(아래 코드에서는 데이터를 여러 개 한번에 받아(batch) 계산하기 위해 연산을 $\mathbf{x}^T W^T + \mathbf{b}^T$로 바꾸고, 계산의 안정성과 효율성 등 몇가지 이유로 log softmax
$$
\\begin{aligned}
\\log \\sigma(\\mathbf{x})\_i
&= \\log e^{x\_i} - \\log \\sum\_{j = 0}^9 e^{x\_j} \\\\
&= x\_i - \\log \\sum\_{j = 0}^9 e^{x\_j}
\\end{aligned}
$$
를 이용하였습니다.)
```python
def log_softmax(x):
return x - x.exp().sum(dim=-1).log().unsqueeze(-1)
def model(x, weights, bias):
return log_softmax(x @ weights + bias)
def neg_likelihood(log_pred, y_true):
return -log_pred[torch.arange(y_true.size()[0]), y_true].mean()
def accuracy(log_pred, y_true):
y_pred = torch.argmax(log_pred, dim=1)
return (y_pred == y_true).to(torch.float).mean()
def print_loss_accuracy(log_pred, y_true, loss_function):
with torch.no_grad():
print(f"Loss: {neg_likelihood(log_pred, y_true):.6f}")
print(f"Accuracy: {100 * accuracy(log_pred, y_true).item():.2f} %")
loss_function = neg_likelihood
```
```python
batch_size = 100
learning_rate = 0.5
n_epochs = 5
```
```python
weights = torch.randn(784, 10, requires_grad=True)
bias = torch.randn(10, requires_grad=True)
for epoch in range(n_epochs):
# Batch 반복
for i in range(x_train.size()[0] // batch_size):
start_index = i * batch_size
end_index = start_index + batch_size
x_batch = x_train[start_index:end_index]
y_batch_true = y_train[start_index:end_index]
# Forward
y_batch_log_pred = model(x_batch, weights, bias)
loss = loss_function(y_batch_log_pred, y_batch_true)
# Backward
loss.backward()
# Update
with torch.no_grad():
weights.sub_(learning_rate * weights.grad)
bias.sub_(learning_rate * bias.grad)
# Zero the parameter gradients
weights.grad.zero_()
bias.grad.zero_()
with torch.no_grad():
y_test_log_pred = model(x_test, weights, bias)
print(f"End of epoch {epoch + 1}")
print_loss_accuracy(y_test_log_pred, y_test, loss_function)
print("---")
```
```plain
End of epoch 1
Loss: 0.662250
Accuracy: 86.78 %
---
End of epoch 2
Loss: 0.544936
Accuracy: 88.43 %
---
End of epoch 3
Loss: 0.494356
Accuracy: 88.92 %
---
End of epoch 4
Loss: 0.462041
Accuracy: 89.33 %
---
End of epoch 5
Loss: 0.438665
Accuracy: 89.55 %
---
```
정확도가 89 % 정도가 나오는 것을 확인할 수 있습니다.
## PyTorch 활용
위에서는 경사 계산을 위해 PyTorch `tensor`의 `autograd` 기능만 사용했으나, PyTorch에서 제공하는 `tensor`, `Module`, `Functional`등을 유용하게 이용할 수 있습니다.
```python
class Model(nn.Module):
def __init__(self):
super().__init__()
self.linear = nn.Linear(784, 10)
def forward(self, x):
return self.linear(x)
loss_function = F.cross_entropy
```
```python
batch_size = 100
learning_rate = 0.5
n_epochs = 5
```
```python
train_dataset = TensorDataset(x_train, y_train)
train_loader = DataLoader(train_dataset, batch_size=batch_size)
model = Model()
optimizer = optim.SGD(model.parameters(), lr=learning_rate)
for epoch in range(n_epochs):
for x_batch, y_batch_true in train_loader:
# Zero the parameter gradients
optimizer.zero_grad()
# Forward
y_batch_log_pred = model(x_batch)
loss = loss_function(y_batch_log_pred, y_batch_true)
# Backword
loss.backward()
# Update
optimizer.step()
with torch.no_grad():
y_test_log_pred = model(x_test)
print(f"End of epoch {epoch + 1}")
print_loss_accuracy(y_test_log_pred, y_test, loss_function)
print("---")
```
```plain
End of epoch 1
Loss: -7.931778
Accuracy: 90.52 %
---
End of epoch 2
Loss: -8.638107
Accuracy: 90.98 %
---
End of epoch 3
Loss: -9.024913
Accuracy: 91.11 %
---
End of epoch 4
Loss: -9.284806
Accuracy: 91.23 %
---
End of epoch 5
Loss: -9.477826
Accuracy: 91.38 %
---
```
제출기간이 종료되었습니다.
종료된 문제는 답안을 제출할 수 없습니다. 제출한 답안은 답안 제출 목록에서 확인하실 수 있습니다.