8 분 소요

딥러닝 이론 공부 후에 프로젝트를 진행하며 실습의 부족함을 느껴, 파이토치 실습 위주의 학습을 진행하려고 한다.

교재는 “텐초의 파이토치 딥러닝 특강”을 참고하였다. 더 자세히 학습하고 싶은 사람은, 이론 부분을 따로 참고해서 읽길 권한다.

주석을 상세히 적으며 설명하였지만, 초반부분에 이미 설명한 부분은 설명을 생략하였다.

1단계: 딥러닝 입문하기

  • 핵심 용어
    • 인공 뉴런(퍼셉트론): 입력값과 가중치, 편향을 이용해 출력값을 내는 수학적 모델이다.

    • 단층 인공 신경망: 퍼셉트론을 하나만 사용하는 인공 신경망이다.

    • 다층 인공 신경망: 퍼셉트론을 여러 개 사용하는 인공 신경망이다.

    • 가중치: 입력의 중요도를 나타낸다.

    • 편향: 활성화의 경계가 원점으로부터 얼마나 이동할지를 결정한다.

    • 활성화 함수: 해당 뉴런의 출력을 다음 뉴런으로 넘길지를 결정한다.

    • 손실 함수: 정답과 신경망의 예측의 차이를 나타내는 함수이다.

    • 경사 하강법: 손실을 가중치에 대해 미분한 다음, 기울기의 반대 방향으로 학습률만큼 이동시키는 알고리즘이다.

    • 오차 역전파: 올바른 가중치를 찾기 위해 오차를 출력층으로부터 입력층까지 전파하는 방법이다.

    • 오버 피팅: 학습에 사용한 데이터에 최적화되게 학습되어서 다른 데이터에 대한 예측 성능이 떨어지는 경우를 의미한다.

    • 기울기 소실: 출력층으로부터 멀어질 수록 역전파되는 오차가 0에 가까워지는 현상이다.

1. 파이토치 권고 코딩 스타일

# 두 텐서의 합 구하기
import torch

# 텐서 만들기
a = torch.tensor([1, 2, 3])
b = torch.tensor([4, 5, 6])

# 두 텐서의 합 구하기
c = a + b

# 텐서 출력하기
print(c)
tensor([5, 7, 9])
  • 파이토치는 클래스를 사용하도록 권고한다.

    1. 모듈 클래스: 신경망의 동작 정의

    2. 데이터셋 클래스: 데이터를 다룸


1.1 모듈 클래스

# 모듈 클래스 (기본적인 뼈대)
class Net(nn.Module):
  def __init__(self):
    '''

    # 신경망 구성 요소 정의

    '''
  def forward(self, input):
    '''

    # 신경망의 동작 원리

    '''
    return output
  1. __init__(): 신경망의 구성 요소를 정의하는 함수

  2. forward(): 신경망의 동작을 정의하는 함수


1.2 데이터셋 클래스

# 데이터셋 클래스 (기본적인 뼈대)
class Dataset():
  def __init__(self):
    '''
    필요한 데이터 불러오기
    '''
  def __len__(self):
    '''
    데이터의 개수 반환
    '''
    return len(data)
  def __getitem__(self, i):
    '''
    i번째 입력 데이터와
    i번째 정답을 반환
    '''
    return data[i], label[i]
  1. __init__(): 학습에 필요한 데이터 불러오기

  2. __len__(): 데이터 개수 반환하기

  3. __getitem__(): i번째 입력 데이터와 정답을 반환하기


1.3 모듈 클래스와 데이터 클래스를 이용하여 학습하기

# 데이터로더로부터 데이터와 정답을 받아오기
for data, label in DataLoader():
  # 모델의 예측값 계산
  prediction = model(data)

  # 손실 함수를 이용해 오차 계산하기
  loss = LossFunction(prediction, label)

  # 오차 역전파
  loss.backward()

  # 신경망 가중치 수정
  optimizer.step()
  • DataLoader: 데이터셋 클래스를 입력으로 받아 학습에 필요한 양 만큼의 데이터를 불러오는 역할을 수행한다.
  1. 데이터로더로부터 데이터와 정답을 불러와 신경망의 예측값을 계산한다.

  2. 손실 함수를 이용해 신경망의 오차를 계산한다.

  3. backward() 메서드를 이용해 오차를 역전파한다.

  4. step() 메서드를 이용해 신경망의 가중치를 수정한다.


2. 딥러닝 문제 해결 프로세스

  1. 해결할 문제 정의

  2. 데이터 수집

  3. 데이터 가공

  4. 딥러닝 모델 설계

  5. 딥러닝 모델 학습

  6. 성능 평가


3. 딥러닝 문제 해결 체크리스트

  • 풀어야할 문제 이해하기

    [ ] 정확한 값을 예측하는 회귀 문제인가?

    [ ] 입력이 속한 범주를 예측하는 분류 문제인가?

  • 데이터 파악하기

    [ ] 입력 자료형과 정답 확인하기

    [ ] 클래스 간의 불균형은 없는지 확인하기

    [ ] 누락된 데이터 혹은 자료형에 맞지 않는 데이터가 포함되어 있는지 확인하기

  • 데이터 전처리

    [ ] 학습에 필요한 데이터가 부족하다면 데이터 증강하기

    [ ] 데이터를 정규화해서 값의 범위 맞추기

  • 신경망 설계

    [ ] 데이터의 공간 정보가 중요하면 합성곱 적용하기

    [ ] 데이터의 순서 정보가 중요하면 RNN, Transformer 계열 적용하기

  • 신경망 학습

    [ ] 적합한 손실 함수 찾기

    [ ] 가중치 수정을 위한 최적화 정하기

    [ ] 신경망의 성능을 평가하기 위한 평가 지표 정하기

  • 손실이 무한대로 발산하면

    [ ] 손실 함수 바꿔보기

    [ ] 데이터에 이상한 값이 섞여 있는지 확인하기

    [ ] 학습률 줄이기

  • 손실이 0으로 수렴한다면

    [ ] 데이터가 부족하지 않은지 확인하기

    [ ] 신경망 크기 줄여보기


4. 직관적 분석에 유용한 시각화

import numpy as np
import matplotlib.pyplot as plt

# 데이터 생성
# 0부터 5까지 0.2씩 증가시킨 값을 저장
t = np.arange(0, 5, 0.2)
t2 = t ** 2
t3 = t ** 3

# 평균은 mu, 표준편차를 sigma로 갖는 정규분포로부터 랜덤하기 10000개 추출
mu, sigma = 100, 15
x = mu + sigma * np.random.randn(10000)


4.1 선형 그래프

# 두 개의 그래프 한 figure에 그리기
plt.plot(t, t2, label='t2')
plt.plot(t, t3, label='t3')
plt.title('show 2 plos in 1 figure')
plt.legend()
plt.show()

image


4.2 서브 플롯

# 서브 플롯
plt.subplot(1, 2, 1) # (행, 열, 순서)
plt.plot(t, t)
plt.subplot(1, 2, 2)
plt.plot(t, t3)
plt.show()

image


4.3 히스토그램

# 히스토그램 
plt.xlabel('x')
plt.ylabel('y')
plt.title('Histogram of Normal distribution')
plt.hist(x, 50, density=1, facecolor='g', alpha=0.75) # (데이터, 구간개수, 1이면 y축 값이 1이 되도록 정규화(면적1인 밀도 함수)), 색상, 불투명도)
plt.show()

image


5. 사인 함수 예측하기

  • 핵심 용어
    • 모듈: 신경망을 구성하는 기본 객체이다. 모듈에는 구성요소를 정의하는 __init__() 함수와 순전파의 동작을 정의하는 forward() 함수가 있다.

    • MSE(평균 제곱 오차): 값의 차이의 제곱의 평균이다. 회귀에서 MSE 손실을 사용한다.

    • CE(크로스 엔트로피): 두 확률 분포의 차이이다. 분류에서 CE 손실을 사용한다.

    • 다중분류: 신경망의 입력을 여러 범주로 분류하는 알고리즘이다.

    • 피처: 신경망의 입력으로 들어오는 값으로 데이터가 갖고 있는 특징이다.

    • 배치: 데이터셋의 일부로 신경망의 입력으로 들어가는 단위이다.

    • 에포크: 전체 데이터를 모두 한 번씩 사용했을 때의 단위이다.

    • 이터레이션: 하나의 에포크에 들어있는 배치 수이다.

    • Adam: 모멘텀과 RMSprop을 섞어놓은 가장 흔하게 사용되는 최적화 알고리즘이다.

    • 모멘텀: 기울기를 계산할 때 관성을 고려하는 최적화 알고리즘이다.

    • RMSprop: 이동평균을 이용해 이전 기울기보다 현재의 기울기에 더 가중치를 두는 최적화 알고리즘이다.

  • 신경망 학습 순서

    1. 모델 정의

    2. 모델 순전파

    3. 오차 계산

    4. 오차 역전파 (가중치 업데이트)

    5. 2~4 원하는 만큼 반복

    6. 학습 종료


5.1 랜덤하게 가중치를 적용하여 사인 곡선 그리기

import math
import torch
import matplotlib.pyplot as plt

x = torch.linspace(-math.pi, math.pi, 1000) # -pi부터 pi 사이에서 점을 1000개 추출
y = torch.sin(x)

# 임의의 가중치를 뽑아 y 만들기
a = torch.randn(())
b = torch.randn(())
c = torch.randn(())
d = torch.randn(())

# 사인 함수를 근사할 3차 다항식 정의
y_random = a * x**3 + b * x**2 + c * x + d

# 실제 사인 곡선을 실제 y값으로 만들기
plt.subplot(2, 1, 1)
plt.title('y true')
plt.plot(x, y)

# 예측 사인 곡선을 임의의 가중치로 만든 y값으로 만들기
plt.subplot(2, 1, 2)
plt.title('y pred')
plt.plot(x, y_random)

# 실제와 예측된 사인 곡선 출력하기
plt.show()

image

  • linspace(a, b, c): 시작점a부터 종료점b까지 데이터 c개를 반환하기

  • sin(a): 입력a에 대한 사인 함수 값을 반환하기

  • randn(): 정규분포를 따르는 랜덤한 값을 반환하기

  • subplot(pos): 그림의 위치 pos에 그래프를 지정하기


5.2 가중치를 학습시켜 사인 곡선 그리기

learning_rate = 1e-6 # 학습률 정의

# 학습 2000번 진행
for epoch in range(2000):
  y_pred = a*x**3 + b*x**2 + c*x + d

  loss = (y_pred - y).pow(2).sum().item() # 손실 정의
  if epoch % 100 == 0:
    print(f'epoch{epoch+1} loss:{loss}')

  grad_y_pred = 2.0 * (y_pred - y) # 기울기의 미분값
  grad_a = (grad_y_pred * x**3).sum()
  grad_b = (grad_y_pred * x**2).sum()
  grad_c = (grad_y_pred * x).sum()
  grad_d = grad_y_pred.sum()

  a -= learning_rate * grad_a # 가중치 업데이트
  b -= learning_rate * grad_b
  c -= learning_rate * grad_c
  d -= learning_rate * grad_d

# 실제 사인 곡선 그리기
plt.subplot(3, 1, 1)
plt.title('y true')
plt.plot(x, y)

# 예측한 사인 곡선 그리기
plt.subplot(3, 1, 2)
plt.title('y pred')
plt.plot(x, y_pred)

# 랜덤한 가중치의 사인 곡선 그리기
plt.subplot(3, 1, 3)
plt.plot(x, y_random)
plt.title('y_random')

plt.show()

image


6. 캘리포니아 집값 예측하기: 회귀 분석

6.1 데이터 불러오기

import pandas as pd
from sklearn.datasets import fetch_california_housing

dataset = fetch_california_housing()
df = pd.DataFrame(dataset['data']) # 데이터 불러오기
df.columns = dataset['feature_names'] # 특징 이름 불러오기
df['target'] = dataset['target'] # 데이터프레임에 정답 추가

print(df.head())
   MedInc  HouseAge  AveRooms  AveBedrms  Population  AveOccup  Latitude  Longitude  target
0  8.3252      41.0  6.984127   1.023810       322.0  2.555556     37.88    -122.23   4.526  
1  8.3014      21.0  6.238137   0.971880      2401.0  2.109842     37.86    -122.22   3.585  
2  7.2574      52.0  8.288136   1.073446       496.0  2.802260     37.85    -122.24   3.521  
3  5.6431      52.0  5.817352   1.073059       558.0  2.547945     37.85    -122.25   3.413  
4  3.8462      52.0  6.281853   1.081081       565.0  2.181467     37.85    -122.25   3.422  


6.2 모델 정의 및 학습하기

# 선형회귀 MLP 모델 설계
import torch
import torch.nn as nn
from torch.optim.adam import Adam

# 모델 정의
model = nn.Sequential(
    nn.Linear(8, 100),
    nn.ReLU(),
    nn.Linear(100, 1)
)

X = df.iloc[:, :-1].values
y = df['target'].values

batch_size = 100
learning_rate = 0.001

# 가중치를 수정하는 최적화 함수 정의
optim = Adam(model.parameters(), lr=learning_rate)

# 에포크 반복
for epoch in range(200):

  # 배치 반복
  for i in range(len(x)//batch_size):
    start = i*batch_size
    end = start + batch_size

    # 파이토치 실수형 텐서로 변환
    x = torch.FloatTensor(X[start:end])
    y = torch.FloatTensor(y[start:end])

    # 가중치의 기울기를 0으로 초기화
    optim.zero_grad()
    # 모델의 예측값 계산
    preds = model(x)
    # MSE 손실 계산
    loss = nn.MSELoss()(preds, y)
    # 오차 역전파
    loss.backward()
    # 최적화 진행
    optim.step()
  
  if epoch % 20 == 0:
    print(f'epoch{epoch} loss:{loss.item()}')
epoch0 loss:28.34756851196289
epoch20 loss:2.6354379653930664
...
epoch160 loss:0.6834787130355835
epoch180 loss:0.6697102189064026


6.3 모델 성능 평가하기

prediction = model(torch.FloatTensor(X[0, :13]))
real = y[0]
print(f'prediction:{prediction.item()} real:{real}')
prediction:2.0168309211730957 real:4.526000022888184


7. 손글씨 분류하기: 다중 분류

7.1 데이터 로드하기

import matplotlib.pyplot as plt

from torchvision.datasets.mnist import MNIST
from torchvision.transforms import ToTensor

# 학습용 데이터와 평가용 데이터 분리
training_data = MNIST(root='./', train=True, download=True, transform=ToTensor())
test_data = MNIST(root='./', train=False, download=True, transform=ToTensor())

print(len(training_data))
print(len(test_data))

# 샘플 이미지 9개 출력
for i in range(9):
  plt.subplot(3, 3, i+1)
  plt.imshow(training_data.data[i])
plt.show()
60000
10000

image

  • 모든 데이터가 파이썬 이미지 파일로 저장되어 있기에 ToTensor() 함수를 이용해 파이토치 텐서로 바꿔준다.
from torch.utils.data.dataloader import DataLoader

train_loader = DataLoader(training_data, batch_size=32, shuffle=True)
test_loader = DataLoader(test_data, batch_size=32, shuffle=False)
  • 학습용 데이터는 데이터를 섞어 모든 범주의 데이터가 골고루 나오도록한다.

  • 평가용 데이터는 섞을 필요가 없다.

  • DataLoader(A): 데이터셋 A를 원하는 배치 크기로 나누어 반환한다.

    • batch_size: 배치 크기 결정

    • shuffle: 데이터 섞을지에 대한 여부 결정


7.2 모델 정의 및 학습하기

import torch
import torch.nn as nn

from torch.optim.adam import Adam

# 학습에 사용할 프로세서 지정
device = 'cuda' if torch.cuda.is_available() else 'cpu'

model = nn.Sequential(
    nn.Linear(784, 64),
    nn.ReLU(),
    nn.Linear(64, 64),
    nn.ReLU(),
    nn.Linear(64, 10)
)
model.to(device) # 모델의 파라미터를 GPU로 보냄

lr = 1e-3
optim = Adam(model.parameters(), lr=lr)
for epoch in range(20):
  for data, label in train_loader:
    optim.zero_grad()
    # 입력 데이터의 모양을 모델의 입력에 맞게 변환
    data = torch.reshape(data, (-1, 784)).to(device)
    preds = model(data)

    loss = nn.CrossEntropyLoss()(preds, label.to(device)) # 손실 계산
    loss.backward() # 오차 역전파
    optim.step() # 최적화 진행

  print(f'epoch{epoch+1} loss:{loss.item()}')

# 모델을 MNIST.pth라는 이름으로 저장
torch.save(model.state_dict(), "MNIST.pth")
epoch1 loss:0.0890120267868042
epoch2 loss:0.07549810409545898
...
epoch19 loss:0.0012714057229459286
epoch20 loss:0.0012847029138356447
  • is_available(): GPU 사용 가능 여부를 반환한다.

  • to(device): 텐서를 device로 보낸다. (모델 파라미터, 데이터)

  • reshape(A, shape): 텐서A를 shape 모양으로 변형시킨다.

  • save(A): 객체를 A에 저장한다.

  • A.state_dict(pth): A 모델의 가중치를 딕셔너리 형태로 반환한 뒤, pth에 저장한다.


7.3 모델 성능 평가하기

# 모델 가중치 불러오기
model.load_state_dict(torch.load("MNIST.pth", map_location=device))

num_corr = 0 # 분류에 성공한 전체 개수

with torch.no_grad(): # 기울기를 계산하기 않음
  for data, label in test_loader:
    data = torch.reshape(data, (-1, 784)).to(device)

    output = model(data.to(device))
    preds = output.data.max(1)[1] # 모델의 예측값 계산

    # 올바르게 분류한 개수
    corr = preds.eq(label.to(device).data).sum().item()
    num_corr += corr

  print(f"Accuracy:{num_corr/len(test_data)}") # 분류 정확도 출력
Accuracy:0.9769
  • map_location: 모델 파일을 불러올 위치

  • no_grad(): 기울기를 계산하지 않는다.

  • max() 최대 예측값과 인덱스를 리스트로 묶어서 반환한다.

    • max(1)[1]: 가장 높은 값을 갖는 위치를 반환한다.

    • max(0): 배치에서 가장 높은 값

    • max(1): 클래스 차원에서 가장 높은 값

  • eq(): 값이 같으면 1, 다르면 0 반환한다.

  • A.item(): 텐서A의 값만을 반환한다.

댓글남기기