[전편]
37. 텐서를 다루다
38. 형상 변환 함수
39. 합계 함수
40. 브로드캐스트 함수
41. 행렬의 곱
42. 선형 회귀
43. 신경망
-------------------------------------------------------
[후편]
44. 매개변수를 모아두는 계층
45. 계층을 모아두는 계층
46. Optimizer로 수행하는 매개변수 갱신
47. 소프트맥스 함수와 교차 엔트로피 오차
48. 다중 클래스 분류
49. Dataset 클래스와 전처리
50. 미니배치를 뽑아주는 DataLoader
51. MNIST 학습
37. 텐서를 다루다
머신러닝 데이터로 벡터나 행렬 등의 '텐서'가 주로 쓰임
37.1 원소별 계산
지금까지 함수 구현은 입출력 '스칼라'로 가정 (ex. sin 함수)
입력 x가 텐서, 가령 행렬일 경우는?
- sin 함수가 x 원소 각각에 적용
* CAUTION
앞의 코드는 x와 c의 형상이 같아야 함 => 그래야 텐서의 원소 사이에 일대일 대응 관계 만들어짐
넘파이 브로드캐스트 : x와 c의 형상이 다르면 자동으로 데이터 복사하여 같은 형상의 첸서로 변환
37.2 텐서 사용 시의 역전파
지금까지 구현한 함수들이 '텐서'를 이용해 계산해도 역전파 코드가 문제 없이 작동하는 이유
- 우리는 그동안 '스칼라'를 대상으로 역전파 구현
- 지금까지 구현한 DeZero 함수에 '텐서'를 건네면 텐서의 원소마다 '스칼라'로 계산
- 텐서의 원소별 '스칼라' 계산이 이루어지면 '스칼라'를 가정해 구현한 역전파는 '텐서'의 원소별 계산에서도 성립
=> DeZero 함수들은 '텐서'를 사용한 계산에도 역전파를 올바르게 해냄
* NOTE
- 머신러닝 문제에서는 텐서를 입력받아 스칼라를 출력하는 함수(손실 함수)를 설정하는 것이 일반적
'마지막 출력이 스칼라인 계산 그래프'에 대한 역전파
x = Variable(np.array([[1, 2, 3], [4, 5, 6]]))
c = Variable(np.array([[10, 20, 30], [40, 50, 60]]))
t = x + c
y = F.sum(t)
y.backward(retain_grad=True)
print(y.grad)
print(t.grad)
print(x.grad)
print(c.grad)
- y.backward(retain_grad=True) : 각 변수의 미분값 구해짐, True로 설정하여 미분값 유지됨
중요한 특징
- 기울기의 형상과 데이터(순전파 때의 데이터)의 형상이 일치
x.shape == x.grad.shape
c.shape == c.grad.shape
t.shape == t.grad.shape - 이 성질을 이용하면 원소별 계산이 아닌 함수(sum, reshape 등)를 구현하기 쉬움
37.3 [보충] 텐서 사용 시의 역전파
텐서를 사용했을 때의 역전파를 수식으로 설명
- y = F(x)라는 함수
- x와 y는 벡터 (두 벡터 모두 원소 수가 n개)
- 최종 결과는 원소별 미분을 계산한 다음 그 결괏값을 원소별로 곱하면 얻을 수 있음
- = 원소별 연산에서는 역전파도 미분을 원소별로 곱하여 구함
* NOTE
- 명시적으로 야코비 행렬을 구하여 행렬의 곱을 계산할 필요 없이 단순히 결과만 구하면 됨
38. 형상 변환 함수
텐서를 사용한 계산에서의 역전파
- 원소별 연산을 수행하는 함수(add, sin 등)는 입출력 데이터가 스칼라라고 가정하고 순전파와 역전파를 구현할 수 있음
- 이 경우 텐서를 입력해도 역전파가 올바르게 성립함
텐서의 형상을 변환하는 reshape 함수, 행렬을 전치하는 transpose 함수
=> 텐서의 형상을 바꾸는 함수
38.1 reshape 함수 구현
사전 준비 : reshape 함수의 사용법
- np.reshape(x, shape) 형태 : x를 shape 인수로 지정한 형상으로 변환
shape : (가로, 세로) : (2, 3) → (6, ) - 텐서의 원소 수는 같고 형상만 바뀜
reshape를 DeZero로 변환하자!
역전파 구현
* NOTE
- 계산을 원소별로 하지 않는 함수는 텐서의 형상을 고려
- 변수의 데이터와 기울기의 형상이 일치하는지 확인
- ex) x가 Variable 인스턴스일 때 x.data.shape == x.grad.shape를 만족할 수 있도록 역전파 구현
reshape 함수
- 단순히 형상만 변환 = 구체적인 계산 하지 않음
→ 역전파는 출력 쪽에서 전해지는 기울기에 아무런 손도 대지 않고 입력 쪽으로 흘려보냄
reshape 함수의 역전파
- 이 기울기를 x.data.shape와 x.grad.shape가 일치하도록 변환
= 입력 변수 쪽의 형상에 맞게 변환
(예시에서는 (6,) 형상을 (2, 3)으로 변환)
코드
class Reshape(Function):
def __init__(self, shape):
self.shape = shape
def forward(self, x):
self.x_shape = x.shape
y = x.reshape(self.shape)
return y
def backward(self, gy):
return reshape(gy, self.x_shape)
def reshape(x, shape):
if x.shape == shape:
return as_variable(x)
return Reshape(shape)(x)
- Reshape 클래스 초기화 시, 변형 목표가 되는 형상을 shape 인수로 받음
- forward 메서드(순전파)는 넘파이의 reshape 함수를 사용하여 형상 변환
self.x_shape = x.shape 코드에서 입력 x의 형상 기억 - backward 메서드(역전파)에서 입력 형상(self.x_shape)으로 변환
- 마무리로 reshape 함수 구현
- reshape 함수가 Variable 인스턴스를 반환함을 보장하기 위해 as_variable 함수 사용하여 Variable 인스턴스로 변환
reshape 함수 사용 코드
x = Variable(np.array([[0, 1, 2], [3, 4, 5]]))
y = F.reshape(x, (6,)) # y = x.reshape(6)
y.backward(retain_grad=True)
print(x.grad)
x = Variable(np.array([[1, 2, 3], [4, 5, 6]]))
y = F.transpose(x) # y = x.T
y.backward()
print(x.grad)
- reshape 함수 사용하여 형상 변환
- y.backward(retrain_grad=True)를 수행하여 x의 기울기 구함
- 이 과정에서 y의 기울기도 자동으로 채워짐
- 채워진 기울기의 형상은 y와 같음(y.grad.shape == y.shape), 원소는 모두 1로 이루어진 텐서
- 텐서의 형상이 (2, 3)에서 (6,)으로 변환
- 역전파에서는 기울기의 형상이 (6,)에서 (2, 3)으로 변환 (순전파 때와 반대)
=> 각 변수의 data와 grad의 형상이 일치함을 확인
38.2 Variable에서 reshape 사용하기
DeZero의 reshape의 함수를 넘파이의 reshape와 비슷하게 만듦
- 튜플, 리스트, 인수 그대로 받기...
class Variable:
...
def reshape(self, *shape):
if len(shape) == 1 and isinstance(shape[0], (tuple, list)):
shape = shape[0]
return dezero.functions.reshape(self, shape)
- Variable 클래스에 '가변 인수'를 받는 reshape 메서드 추가
- 주어진 인수를 적절히 조정하여 앞서 구현한 DeZero의 reshape 함수 호출
38.3 행렬의 전치
행렬을 전치하면 그림과 같이 변함
- 전치를 transpose라는 이름의 DeZero 함수로 구현
- 넘파이에서도 transpose 함수를 사용하여 전치를 할 수 있음
- 텐서의 원소 자체는 그대로, 형상만 바뀜
- 역전파에서는 출력 쪽에서 전해지는 기울기의 형상만 변경
= 순전파 때와 정확히 '반대' 형태
class Transpose(Function):
def __init__(self, axes=None):
self.axes = axes
def forward(self, x):
y = x.transpose(self.axes)
return y
def backward(self, gy):
if self.axes is None:
return transpose(gy)
axes_len = len(self.axes)
inv_axes = tuple(np.argsort([ax % axes_len for ax in self.axes]))
return transpose(gy, inv_axes)
def transpose(x, axes=None):
return Transpose(axes)(x)
- 역전파에서는 순전파와는 '반대'의 변환
Variable 인스턴스에서도 transpose 함수를 사용할 수 있도록 코드 추가
# dezero/core.py
class Variable:
...
def transpose(self):
return dezero.functions.transpose(self)
@property
def T(self):
return dezero.functions.transpose(self)
첫 번째인 transpose는 '인스턴스 메서드'로 이용하기 위한 코드
두 번째인 T에는 @property 데코레이터 (* @property : '인스턴스 변수'로 사용할 수 있게 해주는 데코레이터)
38.4 [보충] 실제 transpose 함수
넘파이의 np.transpose 함수는 더 범용적인 사용 가능
- 축의 순서를 지정하면 그에 맞게 데이터의 축이 달라짐
- 인수를 None으로 주면 축이 역순으로 정렬
- x가 행렬일 때 x.transpose()를 실행하면 0번째와 1번째 축의 데이터가 1번째와 0번째 순서로 바뀜 = '전치'
39. 합계 함수
DeZero에 합계를 구하는 함수 sum을 추가
39.1 sum 함수의 역전파
덧셈의 미분은 y = x0 + x1 일 때 f''(x0) = 1, f''(x1) = 1
덧셈의 역전파
- 덧셈을 수행한 후 변수 y로부터 역전파
- 변수 x0와 x1에는 출력 쪽에서 전해준 1이라는 기울기를 두 개로 '복사'하여 전달
- 원소가 2개인 벡터를 사용해도 똑같이 이루어짐
- 변수 x는 2개의 원소로 구성된 벡터
- sum 함수 적용하면 스칼라 출력
- 역전파는 1을 [1, 1] 벡터로 확장해 전파
- 원소가 2개 이상인 벡터의 합에 대한 역전파도 이끌어낼 수 있음
- 기울기 벡터의 원소 수만큼 '복사 => sum 함수의 역전파
39.2 sum 함수 구현
- DeZero의 sum 함수 역전파에서는 입력 변수의 형상과 같아지도록 기울기의 원소를 복사
- 역전파에서는 Variable 인스턴스를 사용하므로 복사 작업도 DeZero 함수로
dezero/functions.py
class Sum(Function):
def forward(self, x):
self.x_shape = x.shape
y = x.sum()
return y
def backward(self, gy):
gx = broadcast_to(gy, self.x_shape)
return gx
def sum(x):
return Sum()(x)
- broadcast_to 함수를 사용하여 입력 변수와 형상이 같아지도록 기울기 gy의 원소를 복사
-> sum 함수 구현 끝!
import numpy as np
from dezero import Variable
import dezero.functions as F
x = Variable(np.array([1, 2, 3, 4, 5, 6]))
y = F.sum(x)
y.backward()
print(y)
print(x.grad)
# 결과
variable(21)
variable([1 1 1 1 1 1])
x = Variable(np.array([[1, 2, 3], [4, 5, 6]]))
y = F.sum(x)
y.backward()
print(y)
print(x.grad)
# 결과
variable(21)
variable([[1 1 1]
[1 1 1]])
- 입력 변수가 벡터가 아닌 경우에도 제대로 작동
현재의 sum 함수를 확장하여 '진짜' sum 함수 완성!
39.3 axis와 keepdims
* CAUTION
- 인수 axis는 int 외에도 None과 튜플도 받음
- None이면 모든 원ㄴ소를 다 더한 값 하나(스칼라)
- (0, 2)처럼 튜플로 지정하면 해당 튜플에서 지정한 축 모두에 대해 계산 : 0번과 2번 축 모두에 대해 합계
넘파이의 np.sum 함수는 더욱 정교
- axis를 통해 축을 지정하여 합계를 구할 방향 정할 수 있음
- keepdims를 통해 입력과 출력의 차원 수(축 수)를 똑같게 유지할지 정할 수 있음
x = Variable(np.array([[1, 2, 3], [4, 5, 6]]))
y = F.sum(x, axis=0)
y.backward()
print(y)
print(x.grad)
# 결과
[5 7 9]
(2, 3) -> (3,)
x = Variable(np.random.randn(2, 3, 4, 5))
y = x.sum(keepdims=True)
print(y.shape)
# 결과
[[21]]
(1, 1) # False 였다면 y의 형상은 () 스칼라였을 것
두 인수 axis와 keepdims는 실전에서 자주 사용 → sum 함수도 두 인수 지원하도록 수정
코드
dezero/functions.py
class Sum(Function):
def __init__(self, axis, keepdims):
self.axis = axis
self.keepdims = keepdims
def forward(self, x):
self.x_shape = x.shape
y = x.sum(axis=self.axis, keepdims=self.keepdims)
return y
def backward(self, gy):
gy = utils.reshape_sum_backward(gy, self.x_shape, self.axis,
self.keepdims)
gx = broadcast_to(gy, self.x_shape)
return gx
def sum(x, axis=None, keepdims=False):
return Sum(axis, keepdims)(x)
- Sum 클래스를 초기화할 때 axis와 keepdims를 입력받아 속성으로 설정
- 순전파에서는 이 속성들을 사용해 합계
- 역전파에서는 기존처럼 broadcast_to 함수를 사용하여 입력 변수의 형상과 같아지도록 기울기의 원소 복사
sum 함수를 Variable의 메서드로 사용할 수 있게 함
dezero/core.py
class Variable:
...
def sum(self, axis=None, keepdims=False):
return dezero.functions.sum(self, axis, keepdims)
이제 다음 예와 같이 사용할 수 있음
x = Variable(np.array([[1, 2, 3], [4, 5, 6]]))
y = F.sum(x, axis=0)
y.backward()
print(y)
print(x.grad)
x = Variable(np.random.randn(2, 3, 4, 5))
y = x.sum(keepdims=True)
print(y.shape)
40. 브로드캐스트 함수
DeZero에 broadcast_to 함수 구현
40.1 broadcast_to 함수와 sum_to 함수 (넘파이 버전)
np.broadcast_to(x, shape)
- ndarray 인스턴스인 x의 원소를 복제하여 shape 인수로 지정한 형상이 되도록 해줌
* NOTE : 원소 복사
- DeZero는 같은 변수(Variable 인스턴스)를 여러 번 사용하여 계산할 수 있음
- y = x + x 계산에서 x를 '복사'하여 이용한다고 가정
- 역전파에서는 x에 기울기를 두 번 흘려보내게 되어 기울기가 '더해지게' 됨
.→ '원소 복사'가 일어날 경우 기울기를 '합'하면 됨!
braodcast 함수의 역전파
- '원소 복사'가 일어날 경우 역전파 때는 기울기의 '합'을 구함
- broadcast_to 함수의 역전파는 입력 x의 형상과 같아지도록 기울기의 합을 구함
- sum_to(x, shape)함수가 있으면 간단하게 해결되는 문제
(* sum_to(x, shape) : x의 원소의 합을 구해 shape 형상으로 만들어주는 함수)
sum_to(x, shape)
- 원소의 합을 구해 shape 형상으로 만들어주는 함수
- [그림 40-1]과 같은 순전파와 역전파의 관계 만들어짐
- dezoro/util.py에 넘파이 버전 sum_to 함수 준비
sum_to 함수의 역전파
- sum_to 함수의 역전파는 broadcast_to 함수를 사용하여 입력 x의 형상과 같아지도록 기울기의 원소를 복제
40.2 broadcast_to 함수와 sum_to 함수 (DeZero 버전)
# dezero/functions.py
class BroadcastTo(Function):
def __init__(self, shape):
self.shape = shape
def forward(self, x):
self.x_shape = x.shape
xp = dezero.cuda.get_array_module(x)
y = xp.broadcast_to(x, self.shape)
return y
def backward(self, gy):
gx = sum_to(gy, self.x_shape)
return gx
def broadcast_to(x, shape):
if x.shape == shape:
return as_variable(x)
return BroadcastTo(shape)(x)
# dezero/functions.py
class SumTo(Function):
def __init__(self, shape):
self.shape = shape
def forward(self, x):
self.x_shape = x.shape
y = utils.sum_to(x, self.shape)
return y
def backward(self, gy):
gx = broadcast_to(gy, self.x_shape)
return gx
def sum_to(x, shape):
if x.shape == shape:
return as_variable(x)
return SumTo(shape)(x)
- broadcast_to 함수와 sum_to 함수는 상호 의존적
- 넘파이 브로드캐스트에 대응하기 위해서 sum_to 함수 구현
40.3 브로드캐스트 대응
# dezero/core.py
class Add(Function):
def forward(self, x0, x1):
self.x0_shape, self.x1_shape = x0.shape, x1.shape
y = x0 + x1
return y
def backward(self, gy):
gx0, gx1 = gy, gy
if self.x0_shape != self.x1_shape: # for broadcaset
gx0 = dezero.functions.sum_to(gx0, self.x0_shape)
gx1 = dezero.functions.sum_to(gx1, self.x1_shape)
return gx0, gx1
- 순전파 때 브로드캐스트가 일어난다면 입력되는 x0와 x1의 형상이 다를 것
- 이 점을 이용해 두 형상이 다를 때 브로드캐스트용 역전파를 계산
- 기울기 gx0는 x0의 형상이 되도록 합을 구하고, 기울기 gx1은 x1의 형상이 되도록 합을 구함
마찬가지로 Mul, Sub, Div 클래스 등 사칙연산 클래스에는 모두 같은 수정 반영
41. 행렬의 곱
두 가지를 DeZero 함수로 구현
- 벡터의 내적
- 행렬의 곱
41.1 벡터의 내적과 행렬의 곱
벡터의 내적
- 두 벡터 사이의 대응 원소의 곱을 모두 합한 값
* CAUTION
수식으로 표기할 때 스칼라는 보통 글꼴, 벡터나 행렬은 볼드체로
행렬의 곱
벡터의 내적과 행렬의 곱 계산은 모두 np.dot 함수로 처리
- np.dot(x, y)의 두 인수가 모두 1차원 배열이면 벡터의 내적을 계산
- 인수가 2차원 배열이면 행렬의 곱을 계산
41.2 행렬의 형상 체크
행렬과 벡터를 사용한 계산에서는 '형상'에 주의
41.3 행렬의 곱의 역전파
y = xW 계산을 예시로 역전파 살펴보기
이 식의 형상을 체크하면
이 때 역전파의 계산 그래프는 다음과 같아짐
(L/x)', (L/W)'을 도출하기 위해, 행렬의 형상에 주목하여 일관성이 유지되도록 행렬 곱 수행
각 행렬의 원소를 계산하여 양변을 비교하면 유도할 수 있음
행렬 곱의 형상 체크도 충족하는지 확인
→ 이 수식이 있으면 DeZero 함수로 '행렬의 곱'을 손쉽게 구현할 수 있음
역전파는 [그림 41-6]의 수식을 DeZero 함수를 사용해 옮기면 됨
코드
# dezero/functions.py
class MatMul(Function):
def forward(self, x, W):
y = x.dot(W)
return y
def backward(self, gy):
x, W = self.inputs
gx = matmul(gy, W.T)
gW = matmul(x.T, gy)
return gx, gW
def matmul(x, W):
return MatMul()(x, W)
계산 코드
import numpy as np
from dezero import Variable
import dezero.functions as F
x = Variable(np.random.randn(2, 3))
w = Variable(np.random.randn(3, 4))
y = F.matmul(x, w)
y.backward()
print(x.grad.shape)
print(w.grad.shape)
# 계산 결과
(2, 3)
(3, 4)
- x.grad.shape와 x.shape가 동일
- w.grad.shape와 W.shape가 동일
DeZero 버전의 행렬 곱 완성!
42. 선형 회귀
머신러닝 문제에 부딪혀 보자!
'선형 회귀(linear regression)' 구현
42.1 토이 데이터셋
- 작은 데이터셋
- 시드값 고정
x와 y라는 두 개의 변수로 구성된 데이터셋 생성
- x와 y는 선형관계
- y에 추가된 노이즈로 점들이 퍼져 있음
- 목표 : x값이 주어지면 y값을 예측하는 모델(수식0을 만드는 것
* NOTE
- 회귀 : x로부터 실숫값 y를 예측하는 것
- 선형 회귀 : 회귀 모델 중 예측값이 선형(직선)을 이루는 것
42.2 선형 회귀 이론
y와 x가 선형 관계
→ y = Wx + b 라는 직선 식으로 표현
- 목표 : 데이터에 맞는 직선을 찾는 것!
- 데이터와 예측치의 차이, '잔차'를 최소화해야 함
- 예측치(모델)와 데이터의 오차를 나타내는 지표를 다음 식으로 정의
평균 제곱 오차(mean squared error)
- 총 N개의 점에 대해 (x_i, y_i)의 각 점에서 제곱오차를 구한 다음 모두 더함
목표 : 손실 함수의 출력을 최소화하는 W와 b를 찾는 것
함수 최적화 문제 → 경사하강법을 사용
42.3 선형 회귀 구현
전반 코드
import numpy as np
import matplotlib.pyplot as plt
from dezero import Variable
import dezero.functions as F
# Generate toy dataset
np.random.seed(0)
x = np.random.rand(100, 1)
y = 5 + 2 * x + np.random.rand(100, 1)
x, y = Variable(x), Variable(y)
W = Variable(np.zeros((1, 1)))
b = Variable(np.zeros(1))
def predict(x):
y = F.matmul(x, W) + b
return y
- 매개변수 W와 b를 Variable 인스턴스로 생성
- W의 형상은 (1, 1), b의 형상은 (1,)
- 대응하는 차원의 원소 수가 일치
- 결과인 y의 형상은 (100, 1) : 총 100개의 데이터로 이루어진 x에 대해 모든 데이터 각각에 W를 곱한 것
- 단 한 번의 계산으로 모든 데이터의 예측치가 구해짐
- x.shape[1]과 W.shaple[0]을 일치시켜야 행렬 곱이 제대로 계산
- 100개의 데이터 각각에 대해 W에 의한 '벡터의 내적'을 계산
후반 코드
def mean_squared_error(x0, x1):
diff = x0 - x1
return F.sum(diff ** 2) / len(diff)
lr = 0.1
iters = 100
for i in range(iters):
y_pred = predict(x)
loss = mean_squared_error(y, y_pred)
W.cleargrad()
b.cleargrad()
loss.backward()
# Update .data attribute (No need grads when updating params)
W.data -= lr * W.grad.data
b.data -= lr * b.grad.data
print(W, b, loss)
- 평균 제곱 오차를 구하는 함수 구현 : [식 42.1]을 DeZero 함수를 사용하여 구현
- 경사하강법으로 매개변수 갱신
(* 주의 : W.data -= lr * W.grad.data 처럼 인스턴스 변수의 data에 대해 계산) - 매개변수 갱신은 단순히 데이터를 갱신할 뿐이므로 계산 그래프를 만들 필욧 없음
손실 함수의 출력값이 줄어드는 것 확인
42.2 [보충] DeZero의 mean_squared_error 함수
평균 제곱 오차를 구하는 함수 구현
개선 사항
def mean_squared_error(x0, x1):
diff = x0 - x1
return F.sum(diff ** 2) / len(diff)
- 중간에 등장하는 이름 없는 변수 세 개
- 변수들은 계산그래프에 기록되어 있기 때문에 계산 그래프가 존재하는 동안은 메모리에 계속 살아 있음
- 변수들의 데이터(ndarray 인스턴스)도 마찬가지로 계속 살아 있음
→ 메모리 부족 문제
더 나은 방식
class MeanSquaredError(Function):
def forward(self, x0, x1):
diff = x0 - x1
y = (diff ** 2).sum() / len(diff)
return y
def backward(self, gy):
x0, x1 = self.inputs
diff = x0 - x1
gx0 = gy * diff * (2. / len(diff))
gx1 = -gx0
return gx0, gx1
def mean_squared_error(x0, x1):
return MeanSquaredError()(x0, x1)
- Function 클래스를 상속하여 구현
- 순전파는 ndarray 인스턴스로 구현
- 이 처리 단위를 하나로 묶어서 역전파 코드를 backward에 구현
- 수식으로 미분을 계산, 해당 수식을 코드로 옮김 - 메모리를 덜 사용하는 계산 그래프가 만들어짐
- 이전에 중간에 등장하던 변수들은 MeanSquareError 클래스의 forward 메서드에서만 사용됨
- 이 변수들은 ndarray 인스턴스로 사용되어, forward 메서드의 범위를 벗어나는 순간 메모리에서 삭제됨
43. 신경망
신경망으로 '진화'
43.1 DeZero의 linear 함수
선형 변환(linear transfomation) or 아핀 변환(affine transfomation)
y = F.matmul(x, W) + b
* NOTE
- 선형 변환은 엄밀히 말하면 b가 포함되진 않음
- 그러나 b를 더하는 계산을 포함하는 것이 일반적
- 선형 변환은 신경망에서 완전연결계층(fully correctied layer), 매개변서 W는 가중치, 매개변수 b는 편향
- 왼 : 지금까지 구현한 형태
- DeZero의 matmul 함수의 출력은 Variable 인스턴스이므로 계산 그래프에 기록됨
- 즉, Variable 인스턴스와 담긴 데이터(ndarray 인스턴스)는 계산 그래프가 존재하는 동안은 계속 메모리에 머뭄 - 오 : Function 클래스를 상속하여 새롭게 Linear라는 함수 클래스 구현 (메모리 더 효율적)
- 중간 결과가 Variable 인스턴스로 보존되지 않기 때문에 순전파 시 사용하던 중간 데이터는 순전파 끝나는 즉시 삭제
그러나, 전자의 방식을 채용하며 메모리 효율도 개선할 수 있는 '묘수'
- 왼쪽 그림에서 t의 데이터는 +역전파에 필요하지 않음 → +의 역전파는 출력 쪽의 기울기를 단순히 흘려보낼 뿐
- matmul 역전파는 입력 x, W, b만 사용
- => 기울기를 흘려보내야 하므로 계산 그래프에서는 변수 t가 필요하지만, 그 안의 데이터는 즉시 지워도 됨
def linear_simple(x, W, b=None):
t = matmul(x, W)
if b is None:
return t
y = t + b
t.data = None # t의 데이터 삭제
return y
- x, W는 Variable 인스턴스 혹은 ndarray 인스턴스라고 가정
- 편향 b 생략할 수 있음
- t의 데이터는 계산이 끝난 후 삭제할 수 있음(참조 카운트 0이 되어 파이썬 인터프리터에 의해 삭제)
* NOTE
- 신경망에서 큰 텐서를 취급하는 경우 불필요한 ndarray는 즉시 삭제하는 것이 바람직
43.2 비선형 데이터셋
import numpy as np
# Generate toy dataset
np.random.seed(0)
x = np.random.rand(100, 1)
y = 5 + 2 * x + np.random.rand(100, 1)
- sin 함수를 사용하여 데이터 생성
- 선형 관계가 아님
- 신경망이 해결사로 필요
43.3 활성화 함수와 신경망
활성화 함수(activation function)
- 신경망은 선형 변환의 출력에 비선형 변환을 수행, 이 비선형 변환
- 대표적으로 ReLU, 시그모이드 함수
시그모이드 함수
- 비선형
- 이 비선형 변환이 텐서의 각 원소에 적용
def sigmoid_simple(x):
x = as_variable(x)
y = 1 / (1 + exp(-x))
return y
* NOTE
- 더 나은 방식은 Function 클래스를 상속한 Sigmoid 클래스를 구현하는 것
43.4 신경망 구현
일반적인 신경망
- 선형 변환 - 활성화 함수 - 선형 변환 - 활성화 함수 - 선형 변환 - ...
- 신경망 추론 : 선형 변환, 활성화 함수 순서대로 적용
- 추론을 제대로 하려면 '학습' 필요
- 신경망의 학습 : 추론을 처리한 후 손실 함수를 추가, 손실 함수의 출력을 최소화하는 매개변수 찾음
* NOTE
- 신경망에서는 선형 변환이나 활성화 함수 등에 의한 변환을 층(layer)라고 함
- N번의 변환을 수행하는 구조 'N층 신경망'
if '__file__' in globals():
import os, sys
sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
import numpy as np
import matplotlib.pyplot as plt
from dezero import Variable
import dezero.functions as F
# 데이터셋
np.random.seed(0)
x = np.random.rand(100, 1)
y = np.sin(2 * np.pi * x) + np.random.rand(100, 1)
# 1. 가중치 초기화
I, H, O = 1, 10, 1
W1 = Variable(0.01 * np.random.randn(I, H))
b1 = Variable(np.zeros(H))
W2 = Variable(0.01 * np.random.randn(H, O))
b2 = Variable(np.zeros(O))
# 2. 신경망 추론
def predict(x):
y = F.linear(x, W1, b1)
y = F.sigmoid(y)
y = F.linear(y, W2, b2)
return y
lr = 0.2
iters = 10000
# 신경망 학습
for i in range(iters):
y_pred = predict(x)
loss = F.mean_squared_error(y, y_pred)
W1.cleargrad()
b1.cleargrad()
W2.cleargrad()
b2.cleargrad()
loss.backward()
W1.data -= lr * W1.grad.data
b1.data -= lr * b1.grad.data
W2.data -= lr * W2.grad.data
b2.data -= lr * b2.grad.data
if i % 1000 == 0:
print(loss)
# Plot
plt.scatter(x, y, s=10)
plt.xlabel('x')
plt.ylabel('y')
t = np.arange(0, 1, .01)[:, np.newaxis]
y_pred = predict(t)
plt.plot(t, y_pred.data, color='r')
plt.show()
- 1. 매개변수 초기화
- I : 입력층의 차원 수
- H : 은닉층의 차원수 (하이퍼파라미터)
- O : 출력층의 차원수 - 2. 신경망 추론
- 3. 매개변수 갱신
- 활성화 함수와 선형 변환을 거듭 적용하여 비선형 관계도 제대로 학습할 수 있음
- 더 깊은 신경망도 구현할 수 있음
- 하지만 층이 깊어질수록 매개변수 관리 필요
=> 다음 단계에서 매개변수 관리 간소화 구조를 만들어 보자
'AI > 딥러닝 프레임워크 기초' 카테고리의 다른 글
[밑시딥③] 제5고지. DeZero의 도전(전편) (1) | 2024.03.06 |
---|---|
[밑시딥③] 제4고지. 신경망 만들기(후편) (1) | 2024.03.05 |
[밑시딥③] 제3고지. 고차 미분 계산(후편) (1) | 2024.02.27 |
[밑시딥③] 제3고지. 고차 미분 계산(전편) (1) | 2024.02.26 |
[프레임워크 기초] 제2고지. 자연스러운 코드(후편) (2) | 2024.02.08 |