해당 포스팅은 '밑바닥부터 시작하는 딥러닝2'를 공부하고 정리, 요약한 글입니다. 모든 내용은 해당 도서를 기준으로 합니다.
◼️ 1.1 수학과 파이썬 복습
◾ 1.1.1 벡터와 행렬
벡터는 키기와 방향을 가진 양이며 파이썬에서는 1차원 배열로 취급할 수 있다.
행렬은 숫자가 2차원 형태(사각형 형상)로 늘어선 것이다.
그림 1-1처럼 벡터는 2차원 배열로, 행렬은 2차원 배열로 표현할 수 있다. 행렬에서 가로줄을 행(row)라고 하고 세로줄을 열(column)이라고 한다.
벡터는 표현하는 방법이 두가지 인데, 하나는 숫자들을 세로로 나열하는 방법(열벡터)이고 또 하나는 가로로 나열하는 방법(행벡터)이다.
◾ 1.1.2 행렬의 원소별 연산
import numpy as np
W = np.array([[1, 2, 3],
[4, 5, 6]])
X = np.array([[0, 1, 2],
[3, 4, 5]])
W + X
>> array([[ 1, 3, 5],
[ 7, 9, 11]])
W * X
>> array([[ 0, 2, 6],
[12, 20, 30]])
피연산자인 다차원 배열들에서 서로 대응하는 원소끼리(각 원소가 독립적으로) 연산이 이뤄진다.
◾ 1.1.3 브로드캐스트
넘파이의 다차원 배열에서는 형상이 다른 배열끼리도 연산할 수 있다.
A = np.array([[1, 2],
[3, 4]])
A * 10
>> array([[10, 20],
[30, 40]])
그림 1-3처럼 스칼라 값이 10이 2x2 행렬로 확장된 후에 원소별 연산을 수행한다. 이를 브로드캐스트라고 한다.
A = np.array([[1, 2],
[3, 4]])
b = np.array([10, 20])
A * b
>> array([[10, 40],
[30, 80]])
그림 1-4처럼 배열인 b가 2차원 배열 A와 형상이 같아지도록 확장이 된다.
◾ 1.1.3 벡터의 내적과 행렬의 곱
그림 1-5처럼 행렬의 곱은 왼쪽 행렬의 행벡터(가로 방향)와 오른쪽 행렬의 열벡터(세로 방향)의 내적(원소별 곱의 합)으로 계산한다.
# 벡터의 내적
a = np.array([1, 2, 3])
b = np.array([4, 5, 6])
np.dot(a, b)
>> 32
# 행렬의 곱
A = np.array([[1, 2],
[3, 4]])
B = np.array([[5, 6],
[7, 8]])
np.matmul(A, B)
>> array([[19, 22],
[43, 50]])
np.dot(A, B)
>> array([[19, 22],
[43, 50]])
◾ 1.1.5 행렬 형상 확인
행렬이나 벡터를 사용해서 계산할 때는 형상에 주의해야 한다. 행렬 A와 B가 대응하는 차원의 원소 수가 같아야 하며 결과로 만들어진 행렬 C의 형상은 A의 행 수와 B의 열 수가 된다.
◼️ 1.2 신경망의 추론
◾ 1.2.1 신경망 추론 전체 그림
그림 1-7은 입력층에는 뉴런 2개, 출력층에는 3개가 있으며 은닉층에도 적당한 수의 뉴런을 배치한 2차원 데이터를 입력으로 하고 3차원 데이터를 출력으로 하는 함수를 나타낸 그림이다.
뉴런을 O로, 그 사이의 연결을 화살표로 나타냈으며 화살표에는 가중치가 존재하고 그 가중치와 뉴런의 값을 각각 곱해서 그 합이 다음 뉴런의 입력으로 쓰인다.
이때 각 층에서는 이전 뉴런의 값에 영향받지 않는 정수도 더해진다. 이 정수는 편향(bias)라고 한다.
그림 1-7을 수식으로 나타내면 다음과 같다.
완전연결 계층이 수행하는 변환은 행렬의 곱을 이용해 위 식을 다음과 같이 정리해서 쓸 수 있다.
위 식을 간소화 하면 식 1.4처럼 쓸 수 있다.
신경망의 추론이나 학습에서는 다수의 샘플 데이터(미니 배치)를 한꺼번에 처리한다. 이때 N개의 샘플 데이터가 한꺼번에 완전연결계층에 의해 변환되고 은닉층에는 N개 분의 뉴런이 함께 계산 된다.
import numpy as np
W1 = np.random.randn(2, 4) # 가중치
b1 = np.random.randn(4) # 편향
x = np.random.randn(10, 2) # 입력
h = np.matmul(x, W1) + b1
완전연결계층에 의한 변환은 선형 변환인데 여기에 비선형 효과를 부여하는 것이 활성화 함수이다.
시그모이드 함수는 알파벳 s자 모양의 곡선 함수로 임의의 실수를 입력받아 0에서 1 사이의 실수를 출력한다.
◾ 1.2.2 계층으로 클래스화 및 순전파 구현
forward()와 backward() 매서드는 각각 순전파와 역전파를 수행하게 된다.params는 가중치와 편향 같은 매개변수를 담는 리스트이다.
import numpy as np
# 시그모이드(Sigmoid) 레이어 구현
class Sigmoid:
'''Sigmoid Layer class
Sigmoid layer에는 학습하는 params가 따로 없으므로
인스턴스 변수인 params는 빈 리스트로 초기화
'''
def __init__(self):
self.params = []
def forward(self, x):
"""순전파(forward propagation) 메서드
Args:
x(ndarray): 입력으로 들어오는 값
Returns:
Sigmoid 활성화 값
"""
return 1 / (1 + np.exp(-x))
# 완전연결계층(Affine) 구현
class Affine:
'''FC layer'''
def __init__(self, W, b):
"""
Args:
W(ndarray): 가중치(weight)
b(ndarray): 편향(bias)
"""
self.params = [W, b]
def forward(self, x):
"""순전파(forward propagation) 메서드
Args:
x(ndarray): 입력으로 들어오는 값
Returns:
out(ndarray): Wx + b
"""
W, b = self.params
out = np.matmul(x, W) + b
return out
class TwoLayerNet:
def __init__(self, input_size, hidden_size, output_size):
I, H, O = input_size, hidden_size, output_size
# 가중치와 편향 초기화
# input -> hidden
W1 = np.random.randn(I, H)
b1 = np.random.randn(H)
# hidden -> output
W2 = np.random.randn(H, O)
b2 = np.random.randn(O)
# 레이어 생성
self.layers = [
Affine(W1, b1),
Sigmoid(),
Affine(W2, b2)
]
# 모든 가중치를 리스트에 모은다.
self.parmas = [layer.params for layer in self.layers]
# self.params = []
# for layer in self.layers:
# self.params += layer.params
def predict(self, x):
for layer in self.layers:
x = layer.forward(x)
return x
이 클래스의 초기화 메서드는 가중치를 초기화하고 3개의 계층을 생성하여 학습해야 할 가중치 매개변수들을 params 리스트에 저장한다.
◼️ 1.3 신경망의 학습
신경망의 학습은 최적의 매개변수 값을 찾는 작업이다.
◾ 1.3.1 손실함수
학습 단계의 특정 시점에서 신경망의 성능을 나타내는 척도로 손실(loss)를 사용한다. 학습 데이터와 신경망이 예측한 결과를 비교하여 예측이 얼마나 나쁜가를 산출한 단일 값(스칼라)이다.
다중 클래스 분류 신경망에서는 손실 함수로 흔히 cross-entropy error를 이용한다. 이는 신경망이 출력하는 각 클래스의 확률과 정답 레이블을 이용해서 구할 수 있다.
소프트 맥스 함수의 출력의 각 원소는 0.0 이상 1.0 이하의 실수이다. 그 원소들을 모두 더하면 1.0이 된다. 이것이 소프트맥스의 출력을 확률로 해석할 수 있는 이유이다. 소프트맥스의 출력인 이 확률이 다음 차례인 cross entropy error에 입력이 된다.
미니 배치를 교려하면 다음과 같이 식을 쓸 수 있다.
N으로 나눠 1개 당의 평균 손실 함수를 구하게 됨으로써 미니배치의 크기에 관계없이 항상 일관된 척도를 얻을 수 있다.
◾ 1.3.2 미분과 기울기
신경망 학습의 목표는 손실을 최소화하는 매개변수를 찾는 것이다. 이때 중요한 것이 미분과 기울기이다.
여러 개의 변수라도 미분을 할 수 있는데 이를 식으로 나타내면 다음과 같다.
벡터의 각 원소에 대한 미분을 정리한 것이 gradient이다.
행렬에서의 기울기는 다음과 같이 나타낸다.
◾ 1.3.3 연쇄 법칙
학습 시 신경망은 학습 데이터를 주면 손실을 출력한다. 우리가 얻고자 하는 것은 각 매개변수에 대한 손실의 기울기이다.
연쇄 법칙을 사용하여 오차역전파를 사용하여 신경망의 기울기를 구할 수 있다.
◾ 1.3.4 계산 그래프
계산 그래프는 계산 과정을 시각적으로 보여준다.
계산 그래프를 이용하면 순전파와 역전파에 대한 이해가 쉬워진다.
◾ 1.3.5 기울기 도출과 역전파 구현
# Sigmoid 계층 구현
class Sigmoid:
def __init__(self):
self.params, self.grads = [], []
self.out = None
def forward(self, x):
out = 1 / (1 + np.exp(-x))
self.out = out
return out
def backward(self, dout):
dx = dout * (1.0 - self.out) * self.out
return dx
# affinde 계층 구현
class Affine:
def __init__(self, W, b):
self.params = [W, b]
self.grads = [np.zeros_like(W), np.zeros_like(b)]
self.x = None
def forward(self, x):
W, b = self.params
out = np.matmul(x, W) + b
self.x = x
return out
def backward(self, dout):
W, b = self.params
dx = np.matmul(dout, W.T)
dW = np.matmul(self.x.T, dout)
db = np.sum(dout, axis=0)
self.grads[0][...] = dW
self.grads[1][...] = db
return dx
import sys
sys.path.append('..') # 부모 디렉터리의 파일을 가져올 수 있도록 설정
import numpy as np
from common.functions import softmax, cross_entropy_error
class SoftmaxWithLoss:
def __init__(self):
self.params, self.grads = [], []
self.y = None # softmax의 출력
self.t = None # 정답 레이블
def forward(self, x, t):
self.t = t
self.y = softmax(x)
# 정답 레이블이 원핫일 경우 정답 인덱스로 변환
if self.t.size == self.y.size:
self.t = self.t. argmax(axis=1)
loss = cross_entropy_error(self.y, self.t)
return loss
def backward(self, dout=1):
batch_size = self.t.shape[0]
dx = self.y.copy()
dx[np.arange(batch_size), self.t] -= 1
dx *= dout
dx /= batch_size
return dx
◾ 1.3.6 가중치 갱신
신경망의 학습은 다음 순서로 수행한다.
- 1단계 : 훈련 데이터 중에서 무작위로 다수의 데이터를 골라낸다.
- 2단계 : 오차역전파로 각 가중치 매개변수에 대한 솔실 함수의 기울기를 구한다.
- 3단계 : 기울기를 사용하여 가중치 매개변수를 갱신한다. <- 매개변수를 그 기울기와 반대방향으로 갱신하면서 손실을 줄일 수 있는데 이를 경사하강법이라고 한다.
- 4단계 : 1~3단계를 필요만큼 반복한다.
확률적경사하강법(Stochastic Gradient Descent, SGD)는 가중치를 기울기 방향으로 일정한 거리만큼 갱신한다.
class SGD:
def __init__(self, lr=0.01):
self.lr = lr # learning rate
def update(self, params, grads):
for i in range(len(params)):
params[i] -= self.lr * grads[i]
◼️ 1.4 신경망으로 문제를 풀다
◾ 1.4.1 스파이럴 데이터셋
◾ 1.4.2 신경망 구현
# ch01/two_layer_net.py
import sys
sys.path.append('..')
from common.np import *
from common.layers import Affine, Sigmoid, SoftmaxWithLoss
class TwoLayerNet:
def __init__(self, input_size, hidden_size, output_size):
I, H, O = input_size, hidden_size, output_size
# 가중치와 편향 초기화
W1 = 0.01 * np.random.randn(I, H)
b1 = np.zeros(H)
W2 = 0.01 * np.random.randn(H, O)
b2 = np.zeros(O)
# 레이어 생성
self.layers = [
Affine(W1, b1),
Sigmoid(),
Affine(W2, b2)
]
self.loss_layer = SoftmaxWithLoss()
# 모든 가중치와 기울기를 리스트에 모음.
self.params, self.grads = [], []
for layer in self.layers:
self.params += layer.params
self.grads += layer.grads
def predict(self, x):
for layer in self.layers:
x = layer.forward(x)
return x
def forward(self, x, t):
score = self.predict(x)
loss = self.loss_layer.forward(score, t)
return loss
def backward(self, dout=1):
dout = self.loss_layer.backward(dout)
for layer in reversed(self.layers):
dout = layer.backward(dout)
return dout
◾ 1.4.3 학습용 코드
# ch01/train_custom_loop.py
import sys
sys.path.append('..')
import numpy as np
import matplotlib
import matplotlib.pyplot as plt
from tqdm import tqdm # pip install tqdm
from common.optimizer import SGD
from dataset import spiral
from two_layer_net import TwoLayerNet
matplotlib.rc('font', family='Malgun Gothic') # linux
# matplotlib.rc('font', family='AppleGothic') # Mac
# 1. 하이퍼파라미터 설정
max_epoch = 300
batch_size = 30
hidden_size = 10
learning_rate = 1.0
# 2. 데이터 읽기, 모델과 옵티마이저 생성
x, t = spiral.load_data()
model = TwoLayerNet(input_size=2,
hidden_size=hidden_size,
output_size=3)
optimizer = SGD(lr=learning_rate)
# 학습에 사용하는 변수
data_size = len(x)
max_iters = data_size // batch_size
total_loss = 0
loss_count = 0
loss_list = []
for epoch in tqdm(range(max_epoch)):
# 3. 데이터 셔플링
idx = np.random.permutation(data_size)
x = x[idx]
t = t[idx]
for iters in range(max_iters):
batch_x = x[iters*batch_size:(iters+1)*batch_size]
batch_t = t[iters*batch_size:(iters+1)*batch_size]
# 기울기를 구해 매개변수 갱신
loss = model.forward(batch_x, batch_t)
model.backward()
optimizer.update(model.params, model.grads)
total_loss += loss
loss_count += 1
# 정기적으로 학습 경과 출력
if (iters+1) % 10 == 0:
avg_loss = total_loss / loss_count
print(f'| 에폭 {epoch+1} | 반복{iters+1}/{max_iters} | 손실 {avg_loss:.2f}')
loss_list.append(avg_loss)
total_loss, loss_count = 0, 0
- 1. 하이퍼파라미터를 설정
- 2. 데이터를 읽고 신경망과 옵티마이저를 생성
- 3. 에폭 단위로 데이터를 뒤섞고 뒤섞은 데이터 중 앞에서부터 순서대로 뽑아내는 방식 사용
- 4. 계속해서 기울기를 구해 매개변수를 갱신
- 5. 정기적으로 학습 결과를 출력
위 코드를 실행하면 아래와 같이 print문이 출력된다.
학습 후 신경망이 어떻게 분리했는지 시각화 하면 다음과 같다.
학습된 신경망이 나선형 패턴을 제대로 파악함을 알 수 있다.
◾ 1.4.4 Trainer 클래스
# ch01/train.py
%matplotlib inline
import sys
sys.path.append('..')
from common.optimizer import SGD
from common.trainer import Trainer
from dataset import spiral
from two_layer_net import TwoLayerNet
# 하이퍼파라미터 설정
max_epoch = 300
batch_size = 30
hidden_size = 10
learning_rate = 1.0
x, t = spiral.load_data()
model = TwoLayerNet(input_size=2, hidden_size=hidden_size, output_size=3)
optimizer = SGD(lr=learning_rate)
trainer = Trainer(model, optimizer)
trainer.fit(x, t, max_epoch, batch_size, eval_interval=10)
trainer.plot()
위 코드를 실행하면 이전과 같은 신경망 학습이 진행되며 아래와 같이 출력 된다.
◼️ 1.5 계산 고속화
◾ 1.5.1 비트 정밀도
넘파이의 부동소수점 수는 기본적으로 64비트 데이터 타입을 사용한다.
import numpy as np
a = np.random.randn(3)
a.dtype
>> dtype('float64')
신경망 추론과 학습은 32비트 부동소수점 수로도 문제없이 수행할 수 있으며 메모리 관점에서는 항상 32비트가 더 좋다고 말할 수 있다.
b = np.random.randn(3).astype(np.float32)
b.dtype
>> dtype('float32')
c = np.random.randn(3).astype('f')
c.dtype
>> dtype('float32')
1.5.2 GPU(쿠파이)
쿠파이는 GPU를 이용해 병렬 계산을 수행해 주는 라이브러리이다.
import cupy as cp
x = cp.arange(6).reshape(2, 3).astype('f')
x
>> array([[0., 1., 2.],
[3., 4., 5.]], dtype=float32)
x.sum(axis=1)
>> array([ 3., 12.], dtype=float32)
◼️ 1.6 정리
- 신경망은 입력층, 은닉층, 출력층을 지닌다.
- 완전연결계층에 의해 선형 변환이 이뤄지고 활성화 함수에 의해 비선형 변환이 이뤄진다.
- 완전연결계층이나 미니배치 처리는 행렬로 모아 한꺼번에 계산할 수 있다.
- 오차역전파법을 사용해 신경망의 손실에 관한 기울기를 효율적으로 구할 수 있다.
- 신경망의 구성요소들을 계층으로 모듈화 해 두면 이를 조립 해 신경망을 쉽게 구성할 수 있다.
- 신경망 고속화에는 GPU를 이용한 병렬 계산과 데이터의 비트 정밀도가 중요하다.