Coding

경제학코딩2 9주차 - What is torch.nn really?

김복꾼 2024. 1. 19. 14:24
728x90

9주차에는 PyTorch 라이브러리를 사용한 Neural Network를 구축 및 훈련하는 방법에 대해 배웠다.

강의에서 배운 사고의 흐름을 복습 및 정리하고자 한다.

 

1. Import

가장 먼저 시작해야 되는 것은, 필요한 라이브러리를 import하는 것이다.

import numpy as np

import matplotlib.pyplot as plt

import torch

import torch.nn as nn

import torch.nn.functional as F

 

2. Dataset

다음으로 할 것은 데이터셋을 준비하는 것이다.

import tensorflow as tf

MNIST 데이터셋을 불러오기 위해, TensorFlow 라이브러리를 import하였다.

 

(x_train, y_train), (x_test, y_test) = tf.keras.datasets.mnist.load_data()

이후 TensorFlow의 Keras API를 사용하여, MNIST 데이터셋을 불러왔다. MNIST 데이터셋은 손으로 쓴 숫자 이미지 및 label를 포함하는데,

이때 x_train과 y_train은 훈련 데이터와 레이블을, x_test와 y_test는 테스트 데이터와 레이블을 나타낸다.

 

plt.imshow(x_train[1])

이러한 코드를 사용하면, 아래와 같은 이미지를 얻을 수 있다.

MNIST 데이터셋이 어떤 데이터셋인지 이해하는데 도움이 된다.

 

x_train = torch.tensor(x_train.reshape(60000, 784)/255, dtype=torch.float32)

이 부분은 훈련 데이터 x_train을 60000개의 이미지와 각 이미지가 28x28의 784 픽셀 값을 가지는 형태로 재구성해준다.

RGB 픽셀값이 0~255인 것을 고려하여 '/255'를 취한다면, 이미지 데이터를 0과 1사이의 값으로 정규화할 수 있다.

torch.tensor로 작성함으로써 PyTorch Tensor로 변환하였다.

 

x_test = torch.tensor(x_test.reshape(10000, 784)/255, dtype=torch.float32)

y_train = torch.tensor(y_train, dtype=torch.long)

y_test = torch.tensor(y_test, dtype=torch.int64)

위의 과정을 x_test, y_train, y_test에도 적용하였다.

이번에도 역시 데이터가 PyTorch Tensor로 변환되었다.

 

즉, 이러한 단계들을 거침으로써 MNIST 데이터셋을 PyTorch에서 사용할 수 있도록 한 것이다.

 

3. Model Building

그 다음 단계는 PyTorch 연산을 사용하여 model_1을 정의하는 것이다.

 

def log_softmax(x):

return x - x.exp().sum(-1, keepdim=True).log()

log_softmax 함수는 입력 x가 들어오면, 각 class에 대한 log 확률을 계산해준다.

 

def model_1(inputs):

outputs = inputs @ weights + bias

return log_softmax(outputs)

log_softmax 함수의 정의가 완료되면, 비로소 model_1을 정의해줄 수 있다.

입력 input에 가중치 weights를 곱하고 편향 bias를 더한 뒤, 나온 output을 log_softmax 함수에 적용하도록 구성하였다.

 

def nll(prob, target):

return -prob[range(target.shape[0]), target].mean()

loss_func = nll

nll은 negative log likelihood의 손실함수가 되도록 하였다.

예측된 확률 prob와 실제 타깃 target을 기반으로 손실을 계산하게 하였다.

 

def accuracy(prob, target):

pred = torch.argmax(prob, dim=-1)

return (pred == target).float().mean()

accuracy 함수는 모델의 정확도를 계산해준다.

예측된 확률에서 가장 높은 값을 가진 index를 선택하고, 이를 실제 타깃과 비교하여 정확도를 계산하게 하였다.

 

weights = torch.randn((784,10))

weights.requires_grad_()

bias = torch.zeros((10,), requires_grad=True)

마지막으로 weights와 bias를 정의하여, 모델의 가중치와 편향을 초기화하였다.

가중치는 무작위로 초기화하도록 하였고, 편향은 0으로 초기화하도록 하였다.

requires_grad_()를 호출하여 이들이 훈련 과정에서 업데이트될 수 있도록 설정하였다.

 

이러한 과정을 거침으로써 model_1의 정의가 완료되었다.

 

4. Model Training

model_1의 정의가 완료되었다면, 그 다음 단계는 model_1을 훈련시키는 것이다.

 

model_1(x_train)

torch.randint(10, (1,))

epochs = 1000

batch_size = 128

lr = 0.01

x_train 데이터를 model_1에 적용하고, 훈련을 위한 hyperparameter를 설정하였다.

epoch의 수를 1000으로 하여 전체 데이터셋을 1000번 반복하게 하였다.

또한 batch_size와 lr를 정의하여, 한 번에 처리할 데이터의 양 및 학습률도 정해두었다.

 

for epoch in range(epochs):

idx = torch.randint(len(x_train), (batch_size,))

xs = x_train[idx]

ys = y_train[idx]

 

prob = model_1(xs)

loss = loss_func(prob, ys)

if epoch % 100 == 0:

print(loss)

 

loss.backward()

 

with torch.no_grad():

weights -= weights.grad * lr

bias -= bias.grad * lr

 

weights.grad.zero_()

bias.grad.zero_()

다음으로, model의 훈련 과정을 정해주었다. 각 epoch마다 수행될 과제를 정한 것이다.

우선 torch.randint를 사용해 무작위 인덱스를 선택하고, 이를 사용해 훈련 데이터와 레이블의 배치를 생성하게 하였다.

다음으로 model_1으로 예측을 수행하고, loss_func으로 손실을 계산하게 하였다.

loss.backward를 호출하여 weights와 bias에 대한 손실의 기울기를 계산하게 하였고,

torch.no_grad로 weights와 bias를 업데이트하게 하였다.

기울기를 이용해 weights를 조정하고, 이후 기울기를 0으로 재설정하게 하였다.

 

print(loss_func(model_1(x_test), y_test))

print(accuracy(model_1(x_test), y_test))

마지막으로, test 데이터셋에 대한 model의 손실과 정확도를 출력하게 하였다.

 

손실은 3.5820이, 정확도는 0.4636이 나왔다.

model_1은 기초적인 방식으로 훈련시킨 것이니만큼, 정확도가 높지 않음을 알 수 있다.

 

4번 단계를 거침으로써 model_1의 훈련이 완료되었다.

model_1은 PyTorch를 사용해 weights와 bias가 정의되었고, 손실함수와 정확도 계산이 구현되었다.

 

5. Refactoring 1: torch.nn.functional

model을 refactoring하여, torch.nn.functional 모듈의 함수가 손실함수가 되도록 할 수 있다.

즉, 4번 단계에서 손실함수만 자동화시킨 것이다.

 

loss_func = F.cross_entropy

def model_2(inputs):

return inputs @ weights + bias

torch.nn.functional 모듈의 cross_entropy 함수를 손실함수로 설정한 것이다.

cross_entropy 함수는 log_softmax 함수와 nll을 결합한 것으로, 보다 효율적으로 손실을 계산하게 해준다.

기존의 손실함수 대신 cross_entropy를 사용하기 위해, model_2를 새롭게 만들었다.

 

weights = torch.randn((784,10))

weights.requires_grad_()

bias = torch.zeros((10,), requires_grad=True)

batch_size = 128

lr = 0.01

epochs = 1000

for epoch in range(epochs):

 

idx = torch.randint(len(x_train), (batch_size,))

xs = x_train[idx]

ys = y_train[idx]

 

prob = model_2(xs)

loss = loss_func(prob, ys)

if epoch % 100 == 0:

print(loss)

loss.backward()

 

with torch.no_grad():

weights -= weights.grad * lr

bias -= bias.grad * lr

 

weights.grad.zero_()

bias.grad.zero_()

이 부분은 4단계와 동일하다.

loss_func이 가리키는 함수가 cross_entropy로 바뀌었을 뿐, 외형적인 표현은 동일하다.

 

print(loss_func(model_1(x_test), y_test))

print(accuracy(model_1(x_test), y_test))

마지막으로, test 데이터셋에 대한 model의 손실과 정확도를 출력하게 하였다.

 

 

손실은 3.3366이, 정확도는 0.4750이 나왔다.

정확도가 model_1의 0.4636에 비해서 소폭 상승하기는 했으나, 여전히 높지 않음을 알 수 있다.

 

5번 단계를 거침으로써, 손실함수를 자동화시킬 수 있었다.

즉, torch.nn.functional 모듈의 함수를 손실함수로 사용한 model_2를 만들 수 있었다.

그 외의 과정은 4번 단계와 동일하다.

즉, 지금까지의 과정을 정리하면 다음과 같다.

첫 model -> refactoring 1(손실함수 자동화)

 

6. Refactoring 2: nn.Module

model을 다시 refactoring 하여, nn.Module 클래스를 사용하도록 할 수 있다.

weights와 bias의 계산을 조금 더 구조화되고 객체 지향적인 방식으로 바꾸는 것이다.

 

class MyModel(nn.Module):

def __init__(self):

super().__init__()

self.weights = nn.Parameter(torch.randn(784, 10))

self.bias = nn.Parameter(torch.zeros(10))

여기서 정의된 MyModel은, nn.module을 사용하는 새로운 class이다.

__init__ method는 model의 초기화를 수행하는데,

우선 super().__init()로 nn.Module을 호출한 후, self.weights와 self.bias로 weights와 bias를 초기화하게 된다.

 

def forward(self, x):

return x @ self.weights + self.bias

forward 함수는 입력 데이터에 weights를 곱하고 bias를 더하는 연산을 수행한다.

 

model = MyModel()

위에서 정의했던 모든 것들이 model 안에 들어오게 된다.

기존에는 아래와 같이 weights와 bias를 따로 한 번 더 정의해주는 과정이 필요했다면,

[ weights = torch.randn((784,10))

weights.requires_grad_()

bias = torch.zeros((10,), requires_grad=True) ]

이제는 MyModel class의 정의로 필요없어지게 되었다.

 

batch_size = 128

lr = 0.01

epochs = 1000

for epoch in range(epochs):

 

idx = torch.randint(len(x_train), (batch_size,))

xs = x_train[idx]

ys = y_train[idx]

 

prob = model_2(xs)

loss = loss_func(prob, ys)

if epoch % 100 == 0:

print(loss)

loss.backward()

 

with torch.no_grad():

for p in model.parameters():

p -= p.grad * lr

model.zero_grad()

4번, 5번에 있는 기존 모델과 동일하다.

다만, 마지막 with torch.no_grad()부터는 달라진다.

torch.no_grad는 model.parameters를 통해 모든 parameter를 업데이트시키고,

model.zero_grad()를 호출해 기울기를 0으로 재설정하는 역할을 한다.

 

즉, MyModel과 forward에서 weights와 bias를 다르게 정의한 만큼,

loop에서 해당 부분도 달라진 것이다.

 

아래는 4, 5번에서 사용되었던 기존의 코드이다.

이와 비교해보면 어떤 부분에서 차이가 나는지 알 수 있다.

[ with torch.no_grad():

weights -= weights.grad * lr

bias -= bias.grad * lr

 

weights.grad.zero_()

bias.grad.zero_() ]

 

 

print(loss_func(model(x_test), y_test))

print(accuracy(model(x_test), y_test))

이렇게 nn.Module을 사용했을 때 손실과 정확도를 계산해볼 수 있다.

 
 

여전히 정확도가 0.4391로 초기의 방식과 크게 차이나지 않는 모습이다.

 

6번 단계를 거침으로써,

nn.Module의 class를 사용한 새 model을 만들 수 있었다.

지금까지의 과정을 정리하면 다음과 같다.

첫 model -> refactoring 1(손실함수 자동화) -> refactoring 2(nn.Module로 weights와 bias 업데이트 방식 변화)

아직까지는 부족한 모습이다.

 

7. Refactoring 3: nn.Linear

model을 또다시 refactoring 하여 nn.Linear class를 사용하도록 할 수 있다.

nn.Linear로 weights와 bias 계산을 자동화시킨 것이다.

 

class MyModel(nn.Module):

def __init__(self):

super().__init__()

self.linear = nn.Linear(784, 10)

기존의 코드에서 weights와 bias를 정의하는 방식이 바뀌었다.

원래는 아래와 같았다면,

[ self.weights = nn.Parameter(torch.randn(784, 10))

self.bias = nn.Parameter(torch.zeros(10)) ]

이제는 nn.Linear에 있는 self.linear를 사용하여 784개의 입력을 받고 10개의 출력을 생성하는

선형 레이어를 사용하게 되었다. weights와 bias를 자동으로 관리해 준다.

 

def forward(self, x):

return self.linear(x)

forward 함수도 바뀌었다.

기존의 방식은 아래와 같이 수동으로 weights를 곱하고 bias를 더하게 하였다면,

[ def forward(self, x):

return x @ self.weights + self.bias ]

이제는 self.linear가 자동으로 해 준다. 입력 데이터에 선형 변환을 적용하는 것이다.

 

model = MyModel()

batch_size = 128

lr = 0.01

epochs = 1000

for epoch in range(epochs):

 

idx = torch.randint(len(x_train), (batch_size,))

xs = x_train[idx]

ys = y_train[idx]

 

prob = model_2(xs)

loss = loss_func(prob, ys)

if epoch % 100 == 0:

print(loss)

loss.backward()

 

with torch.no_grad():

for p in model.parameters():

p -= p.grad * lr

model.zero_grad()

6단계와 비교했을 때,

weights와 bias의 정의만 nn.Linear을 사용한 것이고,

기본적으로 nn.Module을 사용한다는 점은 동일하므로,

이 부분의 코드는 딱히 수정할 필요가 없다.

 

print(loss_func(model(x_test), y_test))

print(accuracy(model(x_test), y_test))

결과는 어떻게 되었을까?

 

정확도가 0.8705로 대폭 상승하였음을 알 수 있다.

 

7번 단계를 거침으로써,

nn.Linear를 사용해 weights와 bias 업데이트를 자동화할 수 있었다.

지금까지의 과정을 정리하면 다음과 같다.

첫 model -> refactoring 1(손실함수 자동화) -> refactoring 2(nn.Module로 weights와 bias 업데이트 방식 변화) -> refactoring 3(nn.Linear로 weights와 bias 업데이트를 완전 자동화)

 

8. Refactoring 4: torch.optim

model을 또다시 refactoring 하여 torch.optim을 사용하도록 할 수 있다.

기존의 모델과 동일하지만 최적화 알고리즘만 더한 것이다.

 

model = MyModel()

7번 단계에서 사용한 model을 그대로 사용하였다.

이전과 달라지는 것은 아래 하나이다.

 

optimizer = optim.Adam(params=model.parameters())

최적화 알고리즘으로 optim의 Adam을 선택한 것이다.

Adam은 효율적인 확률적 최적화 알고리즘으로, 딥러닝 모델에서 자주 사용된다고 한다.

이를 사용하면 parameter 업데이트 과정을 보다 효율적이고 직관적으로 관리할 수 있게 된다.

 

batch_size = 128

lr = 0.01

epochs = 1000

for epoch in range(epochs):

 

idx = torch.randint(len(x_train), (batch_size,))

xs = x_train[idx]

ys = y_train[idx]

 

prob = model_2(xs)

loss = loss_func(prob, ys)

if epoch % 100 == 0:

print(loss)

loss.backward()

 

with torch.no_grad():

for p in model.parameters():

p -= p.grad * lr

model.zero_grad()

7단계와 비교했을 때 달라진 것은 없다.

 

print(loss_func(model(x_test), y_test))

print(accuracy(model(x_test), y_test))

7단계에서 달라진 것은 단 한 가지이다.

optimizer = optim.Adam(params=model.parameters()) 한 줄이 추가된 것이다.

최적화 과정이 하나 더해졌을 뿐인데 성능은 어떨까?

 
 

0.8705였던 것이 0.9125로 상당히 상승한 것을 확인할 수 있다.

정확도가 90%를 넘은만큼 이제는 상당히 신뢰할 수 있는 모델이 되었다.

 

8번 단계는 7번에서 최적화 과정 하나만을 더한 것이지만, 상당한 성능 증가가 있었음을 확인할 수 있다.

지금까지의 과정을 정리하면 다음과 같다.

첫 model -> refactoring 1(손실함수 자동화) -> refactoring 2(nn.Module로 weights와 bias 업데이트 방식 변화) -> refactoring 3(nn.Linear로 weights와 bias 업데이트를 완전 자동화) -> refactoring 4(torch.optim을 사용하여 최적화 과정 추가)

 

9. Refactoring 5: Dataset and DataLoader

model을 또다시 refactoring 하여 Dataset과 DataLoader을 사용하도록 한 것이다.

 

train_ds = TensorDataset(x_train, y_train)

model = MyModel()

optimizer = optim.Adam(params=model.parameters(), lr=lr)

batch_size = 128

lr = 0.01

epochs = 1000

for epoch in range(epochs):

idx = torch.randint(len(x_train), (batch_size,))

xs, ys = train_ds[idx]

 

pred = model(xs)

loss = loss_func(pred, ys)

 

loss.backward()

optimizer.step()

optimizer.zero_grad()

model의 최종 version이다.

pred를 수행하고, loss_func으로 손실을 계산하게 하였다.

loss.backward()로 손실의 기울기를 계산하게 하였고,

optiizer.step()로 parameter를 업데이트, optimizer.zero.grad로 parameter의 기울기를 0으로 재설정하게 하였다.

 

(x_train, y_train), (x_test, y_test) = tf.keras.datasets.mnist.load_data()

x_train = torch.tensor(x_train.reshape(60000, 784)/255, dtype=torch.float32)

x_test = torch.tensor(x_test.reshape(10000, 784)/255, dtype=torch.float32)

y_train = torch.tensor(y_train, dtype=torch.long)

y_test = torch.tensor(y_test, dtype=torch.int64)

Keras API를 사용해 MNIST 데이터셋을 불러왔다.

2번에서 한 데이터셋 불러오기 과정을 반복한 것이다.

 

class MyDataset(torch.utils.data.Dataset):

def __init__(self, data):

self.data = data

 

def __getitem__(self, idx):

x = self.data[0][idx]

y = self.data[1][idx]

 

return x, y

 

def __len__(self):

return len(self.data[0])

dataset의 class를 MyDataset으로 정의해주었다.

데이터 관리를 더 효율적으로 하기 위한 것이다.

 

ds = MyDataset((x_train, y_train))

ds[0:5]

이후 x_train과 y_train을 MyDataset을 사용하여 ds에 넣어주었다.

또한, 데이터가 잘 불러와졌는지 확인하기 위해 ds의 첫 5개 샘플을 조회하였다.

 
 
 

28 곱하기 28로 제대로 불러와진 것을 알 수 있다.

 

batch_size = 3

train_ds = MyDataset((x_train, y_train))

train_dl = DataLoader(train_ds, batch_size=10)

for xb, yb in train_dl:

print(xb.shape)

print(yb)

break

model = MyModel()

optimizer = optim.Adam(params=model.parameters(), lr=lr)

for epoch in range(1):

for xb, yb in train_dl:

pred = model(xb)

loss = loss_func(pred, yb)

 

loss.backward()

optimizer.step()

optimizer.zero_grad()

train_d1은 DataLoader를 사용하여 train_ds를 불러오도록 한 것이다.

train_d1를 이용한 xb, yb의 loop는, train_d1로부터 xb와 yb를 가져와 출력한 다음 루프를 종료하게 한다.

나머지 부분은 앞에서 했던 것을 총 정리한 것이다.

 

print(loss_func(model(xb), yb))

이 code를 실행시켜보면 손실의 크기를 볼 수 있다.

 
 
.

0.0127로 매우 작음을 알 수 있다.

 

10. Using GPU for Training

같은 방식에서, 런타임만 GPU를 사용하도록 할 수 있다.

다만 colab에서 제대로 작동하지 않는 관계로 생략하도록 하겠다.

 

 

 

11. 직접 TEST 수행

뭔가 실감이 잘 나지 않아, 아래와 같이 코드를 짜보았다.

 

Refactoring 5까지 완료된 최종 모델에 수행시킨 것으로,

10개 sample에 대해 어떠한 성능을 보이는지 확인해보았다.

 

correct = 0

total = 0

with torch.no_grad():

model.eval()

for sample_index in range(1, 11):

sample_data = x_test[sample_index]

sample_label = y_test[sample_index]

prediction = model(sample_data)

predicted_label = prediction.argmax()

 

print(f"Sample {sample_index}: Predicted Label: {predicted_label.item()}, Actual Label: {sample_label.item()}")

 

if predicted_label == sample_label:

correct += 1

total += 1

 

accuracy = correct / total

print(f"Accuracy for samples 1 to 100: {accuracy:.2f}")

 

결과는 아래의 사진과 같다.

 

위에 보이는 것과 같이, sample 8을 제외하고는 다 정답을 맞히는 것을 볼 수 있다.

sample 8은 왜 틀린 것인지, 직접 8번의 사진을 불러와보았다.

code는 아래의 code를 사용하였다.

 

import matplotlib.pyplot as plt

 

# 5번 샘플의 데이터 로드 및 이미지 형태로 변환

sample_data = x_test[8].reshape(28, 28)

 

# 이미지 시각화

plt.imshow(sample_data, cmap='gray')

plt.title(f"Sample 8 - Actual Label: {y_test[8].item()}")

plt.show()

 

code를 실행시킨 결과, 다음과 같은 사진이 나왔다.

 

Model 예측한 숫자는 6, 실제 값은 5였는데,

이건 사람인 내가 봐도 5보다는 6에 가까운 것 같다는 생각이 들었다.

 

이런 애매한 데이터를 제외하면 거의 100%에 가까운 정확도를 보이는 것이니,

성공적인 neural network이라고 할 수 있을 것이다.

 

 

 

 

 

 

728x90