Basic Deep Learning/Dive into Deep Learning 리뷰

[D2L] 3-2. Linear Neural Networks (Softmax Regression)

needmorecaffeine 2022. 7. 9. 20:15

Linear Regression의 다음 장으로 Softmax Regression에 대해 다룬다.

Linear regression은 그 값을 예측하는 문제에 사용되는 반면 softmax regression은 분류문제에 사용된다.

우선 classification에 대해 짚고 넘어가자

 

1. Classification

아래 사진의 예시로 이해하면 쉽다.

4가지 feature를 가지고 있는 이미지 사진을 3가지 카테고리로 분류하는 문제이다.

network 구조로 표현하기 위한 방식이다.

도식화하면 다음과 같다. linear regression과 마찬가지로 softmax regression도 한 개의 layer를 가진다.

그럼 다시 본래의 목적으로 돌아와서 위 O 값인 logits 값을 각각의 클래스에 해당할 확률이라고 바로 해석할 수 있을까?

정답은 아니다. 위 logit 값들은 음의 값을 가질 수 있다는 점, 모든 logit의 값을 더해도 1이 된다는 보장이 없다는 점 때문이다.

위 logit 값을 클래스에 해당할 확률로 계산하기 위해서는 softmax operation이 필요하다.

 

2. Softmax Operation

logit값을 모델의 확률값으로 해석하기 위해서는 다음의 과정이 필요하다.

이렇게 softmax 연산을 거치게 되면 logit값을 확률값으로 해석할 수 없는 문제를 해결하며 연산값을 확률로 해석할 수 있다.

 

minibatch의 vectorization도 살펴보면 다음과 같다.

3. Loss function

위처럼 연산하게 되면 loss function은 어떻게 되어야 할지 살펴보자.

기본적인 논리는 linear regression과 다를 것이 없다.

위 loss 값을 더 자세히 살펴보면 직관적으로 이해할 수 있다.

이를 해석해보자면, 미분 값은 (sofmax연산으로 계산된 모델의 확률값)과 (실제 값)의 차이를 의미한다. loss function의 목적에 부합한다고 할 수 있다.

 

그렇다면 cross entropy loss는 위의 내용과 어떻게 관련지을 수 있을까?

이전까지는 하나의 결과값에 대해 다루었고 label에 대해 (0,0,1)과 같이 binary entry로 표현했다. 

하지만 전체 결과값에 대해서는 같은 원리가 적용되지만 위와 같이 표현할 수는 없다. 그래서 나타내는 표현이 cross entropy loss이다.

binary entry가 아닌 확률 벡터 (0.1, 0.2, 0.7)과 같은 표현으로 결과값을 다룬다. 이렇게 표현하는 방식은 이후의 과정에서 매우 자주 사용된다.


[실습 - 막코드]

실제 Fashion-MNIST데이터셋을 통해 image classification task를 수행해보고자 한다. 

 

1. Reading Dataset

필요한 모듈을 import하고 데이터를 다운받는다.

%matplotlib inline
import torch
import torchvision
from torch.utils import data
from torchvision import transforms
from d2l import torch as d2l

d2l.use_svg_display()

## ToTensor : converts the image data from PIL type to 32-bit floating point
trans = transforms.ToTensor() 
mnist_train = torchvision.datasets.FashionMNIST(
    root="../data", train=True, transform=trans, download=True)
mnist_test = torchvision.datasets.FashionMNIST(
    root="../data", train=False, transform=trans, download=True)

이 데이터셋은 10개의 카테고리, training set에는 6000장, test set에는 1000장을 포함하고 있다.

각각의 이미지는 28*28 = 784개의 픽셀로 구성되어 있고 gray scale image이기에 channel은 하나이다. 

데이터를 시각화해보면 다음과 같다.

2. Minibatch

Dataloader를 통해 구현한다.

batch_size = 256

def get_dataloader_workers():
	return 4 ## use 4 processes to read the data
    
train_iter = data.DataLoader(mnist_train, batch_zie, shuffle = True, num_workers = get_dataloader_workers())

이렇게 설정한 dataloader를 기반으로 데이터를 원하는 크기로 불러오고 minibatch까지 한번에 구현하는 함수를 만든다.

## Download Fashion-MNIST data and load into memory

def load_data_fashion_mnist(batch_size, resize = None):
	trans = [transforms.ToTensor()]
    if resize :
    	trans.insert(0, transforms.Resize(resize))
    trans = transforms.Compose(trans)
    mnist_train = torchvision.datasets.FashionMNIST(
        root="../data", train=True, transform=trans, download=True)
    mnist_test = torchvision.datasets.FashionMNIST(
        root="../data", train=False, transform=trans, download=True)
    return (data.DataLoader(mnist_train, batch_size, shuffle=True,
                            num_workers=get_dataloader_workers()),
            data.DataLoader(mnist_test, batch_size, shuffle=False,
                            num_workers=get_dataloader_workers()))

위 함수를 이용해 이제 학습을 위한 최종 데이터셋 작업을 한다.

train_iter, test_iter = load_data_fashion_mnist(32, resize = 64)
for X, y in train_iter:
	print(X.shape, X.dtype, y.shape, y.dtype)
    break
## 얻게 되는 데이터셋
torch.Size([32, 1, 64, 64]) torch.float32 torch.Size([32]) torch.int64

3. Setting

학습과 모델을 위한 setting을 한다.

## Initializing Model Parameters

num_inputs = 784 ## 28*28
num_outputs = 10

W = torch.normal(0, 0.01, size = (num_inputs, num_ouputs), requires_grad = True)
b = torch.zeros(num_outputs, requires_grad = True)

## Define Softmax operation

def softmax(X):
	X_exp = torch.exp(X)
    partition = X_exp.sum(1, keepdim = True)
    return X_exp / partition

위 sum 연산을 살펴보면 아래와 같고 아래 성질을 이용해 softmax 연산을 구현하다.

# Sum operation
X = torch.tensor([[1, 2, 3], [4, 5, 6]])

X.sum(0, keepdim = True)

## 결과 
tensor([[5, 7, 9]])

X.sum(1, keepdim = True)

## 결과
tensor([[6],
	[15]])

모델과 loss function을 정의한다.

## Define model

def net(X):
	return softmax(torch.matmul(X.reshape((-1, W.shape[0])), W) + b) 
    
## Define Loss function = Cross entropy loss

def cross_entropy(y_hat, y) :
	return - torch.log(y_hat[range(len(y_hat)), y])
    
corss_entropy(y_aht, y)

## 결과
tensor([2.3026. 0.6931])

classification accuracy를 계산하기 위한 함수는 다음과 같다.

## Classification Accuracy

## compute the # of correct predictions

def accuracy(y_hat, y):
	if len(y_hat.shape) > 1 and y_hat.shape[1] > 1:
    	# y_hat's second dimension = prediction score
        # armax to get the predicted class by the index for the largest entry
    	y_hat = y_hat.argmax(aixs = 1)
    # compare with real y (matchinf dtype to compare)
    cmp = y_hat.type(y.dtype) == y
    
    return float(cmp.type(y.dtype).sum())
    
## compute accuracy for a model

def evaluate_accuracy(net, data_iter):
	if isinstance(net, torch.nn.Module):
    	net.eval() ## evaluation mode
    metric = Accumulator(2) ## #of correct prediction, # of predictions
    
    with torch.no_grad():
    	for X, y in data_iter:
        	metric.add(accuracy(net(X), y), y.numel()) ## numel = # of elements
    return metric[0] / metric[1]
    
    
## ACcumilator : utility class to accumulate sums over multiple variables
## store the # of correct prediction and # of predictions as iterate

class Accumulator: 
    def __init__(self, n):
        self.data = [0.0] * n

    def add(self, *args):
        self.data = [a + float(b) for a, b in zip(self.data, args)]

    def reset(self):
        self.data = [0.0] * len(self.data)

    def __getitem__(self, idx):
        return self.data[idx]

4. Training

실제 학습을 진행한다.

def train_epoch_ch3(net, train_iter, loss, updater):
	if ininstance(net, torch.nn.Module):
    	net.train() # train mode
    metric = Accumulator(3) # sum of loss, sum of training accuracy, # of examples
    
    for X, y in train_iter:
    	y_hat = net(X)
        l = loss(y_hat, y)
        if isinstance(updater, torch.optim.Optimizer):
        	updater.zero_grad()
            l.mean.backward()
            updater.step()
        else:
        	l.sum().backward()
            updater(X.shape[0])
        metric.add(float(l.sum()), accuracy(y_hat, y), y.numel())
    return metric[0] / metric[2], metric[1] / metric[2] ## loss and accuracy

학습 진행 내용 및 결과를 확인하기 위해 시각화하는 class도 설정한다.

class Animator: 
    """For plotting data in animation."""
    def __init__(self, xlabel=None, ylabel=None, legend=None, xlim=None,
                 ylim=None, xscale='linear', yscale='linear',
                 fmts=('-', 'm--', 'g-.', 'r:'), nrows=1, ncols=1,
                 figsize=(3.5, 2.5)):
        # Incrementally plot multiple lines
        if legend is None:
            legend = []
        d2l.use_svg_display()
        self.fig, self.axes = d2l.plt.subplots(nrows, ncols, figsize=figsize)
        if nrows * ncols == 1:
            self.axes = [self.axes, ]
        # Use a lambda function to capture arguments
        self.config_axes = lambda: d2l.set_axes(
            self.axes[0], xlabel, ylabel, xlim, ylim, xscale, yscale, legend)
        self.X, self.Y, self.fmts = None, None, fmts

    def add(self, x, y):
        # Add multiple data points into the figure
        if not hasattr(y, "__len__"):
            y = [y]
        n = len(y)
        if not hasattr(x, "__len__"):
            x = [x] * n
        if not self.X:
            self.X = [[] for _ in range(n)]
        if not self.Y:
            self.Y = [[] for _ in range(n)]
        for i, (a, b) in enumerate(zip(x, y)):
            if a is not None and b is not None:
                self.X[i].append(a)
                self.Y[i].append(b)
        self.axes[0].cla()
        for x, y, fmt in zip(self.X, self.Y, self.fmts):
            self.axes[0].plot(x, y, fmt)
        self.config_axes()
        display.display(self.fig)
        display.clear_output(wait=True)
def train_ch3(net, train_iter, test_iter, loss, num_epochs, updater):  #@save
    animator = Animator(xlabel='epoch', xlim=[1, num_epochs], ylim=[0.3, 0.9],
                        legend=['train loss', 'train acc', 'test acc'])
    for epoch in range(num_epochs):
        train_metrics = train_epoch_ch3(net, train_iter, loss, updater)
        test_acc = evaluate_accuracy(net, test_iter)
        animator.add(epoch + 1, train_metrics + (test_acc,))
    train_loss, train_acc = train_metrics
    assert train_loss < 0.5, train_loss
    assert train_acc <= 1 and train_acc > 0.7, train_acc
    assert test_acc <= 1 and test_acc > 0.7, test_acc
lr = 0.1

def updater(batch_size):
    return d2l.sgd([W, b], lr, batch_size)
    
num_epochs = 10
train_ch3(net, train_iter, test_iter, cross_entropy, num_epochs, updater)

학습 결과이다. test accuracy가 0.8 이상의 값을 가짐을 알 수 있다.

5. Prediction

훈련된 결과를 바탕으로 예측을 해본다.

def predict_ch3(net, test_iter, n=6):  #@save
    """Predict labels (defined in Chapter 3)."""
    for X, y in test_iter:
        break
    trues = d2l.get_fashion_mnist_labels(y)
    preds = d2l.get_fashion_mnist_labels(net(X).argmax(axis=1))
    titles = [true +'\n' + pred for true, pred in zip(trues, preds)]
    d2l.show_images(
        X[0:n].reshape((n, 28, 28)), 1, n, titles=titles[0:n])

predict_ch3(net, test_iter)

예측이 잘 되는 것을 확인할 수 있다.


[ 실습 - API ]

파이토치를 이용해 위 과정을 간단화 해보자.

한번에 다 기술한다.

import torch
from torch import nn
from d2l import torch as d2l

# Setting

batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)

# Initializing model parameters

net = nn.Sequential(nn.Flatten(), nn.Linear(784, 10)) # flatten : reshape the input

def init_weights(m):
    if type(m) == nn.Linear:
        nn.init.normal_(m.weight, std=0.01)

net.apply(init_weights)


# Loss function
loss = nn.CrossEntropyLoss(reduction='none')

# Optimization algorithm
trainer = torch.optim.SGD(net.parameters(), lr = 0.1)

# Training
num_epochs = 10
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, trainer)