Deep Learning with Python - Ch.02

케라스 창시자에게 배우는 딥러닝을 실습하면서 정리한 포스트입니다. 코드 예제와 코드 설명은 역자 깃허브에서 받아볼 수 있습니다. 출판물이고 개인적으로만 참고하기 위한 요약노트이다 보니 설명이 불친절한 점은 양해 바랍니다. 보다 자세한 내용을 원하시는 분은 위 링크의 책을 참고하시기 바랍니다.


2장. 시작 전에: 신경망의 수학적 구성 요소

신경망과의 첫 만남

  • MNIST 예제
# 케라스에서 MNIST 데이터셋 불러오기
from keras.datasets import mnist
(train_images, train_labels), (test_images, test_labels) = mnist.load_data()
# 신경망 구조
from keras import models
from keras import layers
network = models.Sequential()
network.add(layers.Dense(512, activation='relu', input_shape=(28 * 28,)))
network.add(layers.Dense(10, activation='softmax'))
  • 신경망의 핵심 구성 요소인 층(layer)은 일종의 데이터 처리 필터
  • 어떤 데이터가 들어가면 더 유용한 형태로 출력됨(층은 주어진 문제에 더 의미 있는 표현을 입력된 데이터로부터 추출함)
  • 딥러닝 모델은 데이터 정제 필터(층)가 연속되어 있는 데이터 프로세싱을 위한 여과기와 같음
  • 위 예시는 조밀하게 연결된 신경망 층인 Dense 층 2개가 연속된 구조의 신경망 구조
  • 두 번째 층은 10개의 확률 점수가 들어 있는 배열(모두 더하면 1)을 반환하는 소프트맥스 층
  • 각 점수는 현재 숫자 이미지가 10개의 숫자 클래스 중 하나에 속할 확률임
  • 신경망이 훈련 준비를 마치기 위해서 컴파일 단계에 포함될 세 가지가 더 필요
    • 손실 함수 : 훈련 데이터에서 신경망의 성능을 측정하는 방법. 네트워크가 옳은 방향으로 학습될 수 있도록 도움
    • 옵티마이저: 입력된 데이터와 손실 함수를 기반으로 네트워크를 업데이트하는 메커니즘입니다.
    • 훈련과 테스트 과정을 모니터링할 지표 : 여기에서는 정확도(정확히 분류된 이미지의 비율)만 고려하겠습니다.
# 컴파일 단계
network.compile(optimizer='rmsprop',
loss='categorical_crossentropy',
metrics=['accuracy'])
  • 트레이닝 시작 전에 데이터를 네트워크에 맞는 크기로 바꾸고 모든 값을 0과 1 사이로 스케일을 조정
  • MNIST 이미지는 [0, 255] 사이의 값인 unit8 타입의 (60000, 28, 28) 크기의 배열로 저장돼 있음. 이를 0과 1 사이 값을 가지는 float32 타입의 (60000, 28*28) 크기인 배열로 변경
  • categorical_crossentropy: 손실 함수. 가중치 텐서를 학습하기 위한 피드백 신호로 사용. 훈련하는 동안 최소화됨
  • rmsprop : 경사 하강법 적용 방식은 이 옵티마이저에 의해 결정
# 이미지 데이터 준비하기
train_images = train_images.reshape((60000, 28 * 28))
train_images = train_images.astype('float32') / 255

test_images = test_images.reshape((10000, 28 * 28))
test_images = test_images.astype('float32') / 255
# 레이블 준비_범주형으로 인코딩
from keras.utils import to_categorical
train_labels = to_categorical(train_labels)
test_labels = to_categorical(test_labels)
# fit 메서드로 트레이닝 데이터에 모델을 학습시킴
network.fit(train_images, train_labels, epochs=5, batch_size=128)
  • 네트워크가 128개 샘플씩 미니 배치로 훈련 데이터를 5번 반복
  • 5번의 에포크 동안 네트워크는 2345번의 그래디언트 업데이트를 수행(에포크당 469번)
  • 훈련 데이터에 대한 네트워크의 손실과 정확도 정보가 출력됨
Epoch 1/5
60000/60000 [==============================] - 3s 54us/step - loss: 0.2556 - acc: 0.9261
Epoch 2/5
60000/60000 [==============================] - 3s 51us/step - loss: 0.1042 - acc: 0.9688
Epoch 3/5
60000/60000 [==============================] - 3s 51us/step - loss: 0.0685 - acc: 0.9796
Epoch 4/5
60000/60000 [==============================] - 3s 52us/step - loss: 0.0499 - acc: 0.9845
Epoch 5/5
60000/60000 [==============================] - 3s 52us/step - loss: 0.0369 - acc: 0.9888
<keras.callbacks.History at 0x137a2e860>
# 테스트셋에서 모델이 잘 작동하는지 확인하기
test_loss, test_acc = network.evaluate(test_images, test_labels)
print('test_acc:', test_acc)
10000/10000 [==============================] - 0s 38us/step
test_acc: 0.9798

잘 작동한다!

  • 테스트셋의 정확도는 97.8%. 트레이닝셋 정확도와 차이가 나는 이유는 오버피팅 때문임

신경망을 위한 데이터 표현

  • 텐서 : 데이터(숫자)를 위한 컨테이너
  • 최근의 모든 머신러닝 시스템은 텐서를 기본 데이터 구조로 사용

스칼라(0D 텐서)

  • 하나의 숫자만 담고 있는 텐서
  • 넘파이에서는 float32나 float64 타입의 숫자가 스칼라 텐서
  • ndim 속성을 사용하면 넘파이 배열의 축 개수를 확인 가능
  • 스칼라 텐서의 축 개수는 0(ndim == 0)
  • 텐서의 축 개수를 랭크(rank)라고도 부름
# 스칼라 텐서(0D 텐서))
import numpy as np
x = np.array(12)
x, x.ndim
(array(12), 0)

벡터(1D 텐서)

  • 숫자의 배열
  • 하나의 축을 가짐
# 벡터(1D 텐서)
x = np.array([12, 3, 6, 14, 7])
x, x.ndim
(array([12,  3,  6, 14,  7]), 1)
  • 위 예에서 x는 5개의 원소를 가지고 있으므로 5차원 벡터
    • 5D 벡터: 하나의 축을 따라 5개의 차원을 가진 것
    • 5D 텐서: 5개의 축을 가진 것
  • 차원수(dimensionality)는 특정 축을 따라 놓인 원소의 개수(5D 벡터와 같은 경우)이거나 텐서의 축 개수(5D 텐서와 같은 경우)이거나 텐ㅅ의 축 개수(5D 텐서와 같은 경우)를 의미하므로 가끔 혼동하기 쉽다
  • 후자의 경우 랭크 5인 텐서라고 말하는게 보다 정확(텐서의 랭크가 축의 개수)

행렬(2D 텐서)

  • 벡터의 배열이 행렬 또는 2D 텐서
  • 행렬에는 행과 열 2개의 축이 있음
x = np.array([[5, 78, 2, 34, 0],
[6, 79, 3, 35, 1],
[7, 80, 4, 36, 2]])
x.ndim
2

3D 텐서와 고차원 텐서

  • 행렬들을 하나의 새로운 배열로 합치면 숫자로 채워진 직육면체 형태인 3D 텐서
# 3D 텐서
x = np.array([[[2, 45, 2, 34, 0],
[5, 34, 5, 36, 1],
[7, 80, 4, 36, 2]],
[[5, 78, 2, 34, 0],
[6, 79, 3, 35, 1],
[7, 80, 4, 37, 2]],
[[5, 89, 3, 35, 1],
[7, 80, 4, 36, 2],
[8, 72, 2, 40, 5]]])
x.ndim
3
  • 3D 텐서들을 하나의 배열로 합치면 4D 텐서
  • 딥러닝에선 보통 0D에서 4D까지의 텐서를 다룬다
  • 동영상 데이터를 다룰 때는 5D 텐서까지 가기도

핵심 속성

  • 텐서의 핵심 속성 3가지
    • 축의 개수(rank) : 3D 텐서에는 3개의 축, 행렬에는 2개의 축
    • 크기(shape) : 텐서의 각 축을 따라 얼마나 많은 차원이 있는지 나타낸 파이썬의 튜플.
    • 데이터 타입 : float32, float64. uint8 등

배치 데이터

  • 일반적으로 데이터 텐서의 첫 번째 축(0번 축)은 샘플 축(MNIST 예제에서는 숫자 이미지가 샘플)
  • 딥러닝 모델은 데이터를 작은 batch로 나눠서 처리
  • 이런 batch 데이터를 다룰 땐 0번 축을 배치 축 또는 배치 차원이라 부른다

텐서의 실제 사례

  • 벡터 데이터
    • (샘플들, 피처들) 크기의 2D 텐서
    • ex) 사람의 나이, 우편번호, 소득으로 구성된 인구 통계 데이터. 각 사람은 3개의 값을 가진 벡터로 구성. 10만명이 포함된 전체 데이터셋은 (100000, 3) 크기의 텐서에 저장
  • 시계열 데이터
    • (샘플들, timsteps, 피처들) 크기의 3D 텐서
    • 시간이 중요할 때는 시간 축을 포함해 3D 텐서로 저장
    • 관례적으로 시간 축은 항상 1번(두 번째) 축
    • ex) 주식 가격 데이터셋. 1분마다 현재 주식 가격, 지난 1분 동안 최고가와 최소가를 저장. 1분마다 데이터는 3D 벡터로 인코딩. 하루(390분) 거래는 (390, 3) 크기의 2D 텐서로 인코딩. 250일치 데이터는 (250, 390, 3) 크기의 3D 텐서로 저장될 수 있음.
  • 이미지
    • (샘플들, 높이, 너비, 컬러 채널) 크기의 4D 텐서
    • ex) 256 x 256 크기의 흑백 이미지에 대한 128개의 배치는 (128, 256, 256, 1) 크기의 텐서에 저장 가능. 컬러라면 (128, 256, 256, 3)
  • 동영상
    • (samples, frames, height, width, channels) 크기의 5D 텐서
    • ex) 60초짜리 144 x 256 유튜브 영상을 초당 4프레임으로 샘플링하면 240프레임. 이 영상을 4개 가진 배치는 (4, 240, 144, 256, 3) 크기의 텐서에 저장.

신경망의 톱니바퀴 : 텐서 연산

  • 케라스 층 생성하기
    • keras.layers.Dense(512, activation='relu')
    • 2D 텐서를 입력받고 입력 텐서의 새로운 표현인 또 다른 2D 텐서를 반환하는 함수로 볼 수 있음
    • output = relu(dot(W, input) + b) 함수와 같음
    • 이 함수에는 3개의 텐서 연산이 있음. 입력 텐서와 W의 dot, dot의 결과인 2D 텐서와 벡터 b 사이의 덧셈, relu 연산

원소별 연산

  • relu 함수와 덧셈은 원소별 연산.
  • 각 원소에 독립적으로 적용됨
# 파이썬으로 구현한 원소별 연산_relu
def naive_relu(x):
assert len((x.shape) == 2)

x = x.copy()
for i in range(x.shape[0]):
for j in range(x.shape[1]):
x[i, j] = max(x[i, j], 0)
return x

# 파이썬으로 구현한 원소별 연산_덧셈
def naive_add(x, y):
assert len(x.shape) == 2
assert x.shape == y.shape

x = x.copy()
for i in range(x.shape[0]):
for j in range(x.shape[1]):
x[i, j] += y[i, j]
return x

브로드캐스팅

  • 크기가 다른 두 텐서가 더해진다면? 실행 가능하다면 작은 텐서가 큰 텐서 크기에 맞춰 브로드캐스팅됨
    1. 큰 텐서의 ndim에 맞게 작은 텐서에 브로드캐스팅 축이 추가됨
    2. 작은 텐서가 새 축을 따라서 큰 텐서의 크기에 맞도록 반복됨
  • ex) X.shape = (32, 10), y.shape = (10,)
    1. y에 비어 있는 축을 추가해 (1, 10)으로
    2. y를 이 축에 32번 반복하면 텐서 Y.shape는 (32, 10)
    3. 크기가 같아져서 더할 수 있음
# 단순 구현
def naive_add_matrix_and_vector(x, y):
assert len(x.shape) == 2
assert len(y.shape) == 1
assert x.shape[1] == y.shape[0]

x = x.copy()
for i in range(x.shape[0]):
for j in range(x.shape[1]):
x[i, j] += y[j]
return x
# 크기가 다른 두 텐서에 브로드캐스팅으로 원소별 maximum 연산 적용
import numpy as np
x = np.random.random((64, 3, 32, 10))
y = np.random.random((32, 10))

z = np.maximum(x, y)

tensor product

  • 원소별 연산과 반대로 입력 텐서의 원소들을 결합시킴
# 점곱 연산
def naive_vector_dot(x, y):
assert len(x.shape) == 1
assert len(y.shape) == 1
assert x.shape[0] == y.shape[0]

z = 0.
for i in range(x.shape[0]):
z += x[i] * y[i]
return z
# 행렬 x와 벡터 y의 점곱
def naive_matrix_vector_dot(x, y):
assert len(x.shape) == 2
assert len(y.shape) == 1
assert x.shape[1] == y.shape[0]

z = np.zeros(x.shape[0])

for i in range(x.shape[0]):
for j in range(x.shape[1]):
z[i] += x[i, j] * y[j]
return z

텐서 크기 변환(tensor reshaping)

  • 특정 크기에 맞게 열과 행을 재배열
  • 행과 열을 바꾸는 전치도 자주 사용

텐서 연산의 기하학적 해석

  • 아핀 변환, 회전, 스케일링 등 기본적인 기하학적 연산은 텐선으로 표현 가능

딥러닝의 기하학적 해석

  • 빨간색 파란색 2장의 색종이를 겹친 다음 뭉쳐서 작은 공을 만든다고 가정
  • 종이공: 입력 데이터, 색종이: 분류 문제의 데이터 클래스
  • 신경망의 역할: 종이 공을 펼쳐서 두 클래스가 분리되는 변환을 찾는 것

신경망의 엔진: 그래디언트 기반 최적화

output = relu(dot(W, input) + b)

  • 텐서 W와 b는 층의 속성. 가중치 또는 훈련되는 파라미터.
  • 이런 가중치에는 훈련 데이터를 신경망에 노출시켜 학습된 정보가 담겨 있음
  • 초기에는 가중치 행렬이 작은 난수로 채워짐(무작위 초기화 단계)
  • 피드백 신호에 따라 가중치가 점진적으로 조정됨(훈련 단계)
  • 훈련 반복 루프
    1. 트레이닝 샘플 x와 타깃 y의 배치를 추출
    2. x를 사용해 네트워크를 실행(forward pass 단계)하고 y_pred 구하기
    3. y와 y_pred 차이를 측정해 이 배치에 대한 네트워크 손실 계산
    4. 배치에 대한 손실이 감소되도록 네트워크 가중치 업데이트
  • 이 접근법은 모든 가중치 행렬의 원소바다 두 번의 forward pass를 계산해야 하므로 비효율적
  • 신경망에 사용된 연산이 미분 가능하다는 점을 이용해 네트워크 가중치에 대한 손실의 그래디언트를 계산하는 게 더 좋은 방법

변화율

  • derivative!

텐서 연산의 변화율: 그래디언트

  • 다차원 입력, 즉 텐서를 입력으로 받는 함수에 변화율 개념을 확장시킨 것
  • y_pred = dot(W, x)
  • loss_value = loss(y_pred, y)
  • 입력 데이터 x와 y가 고정돼 있다면 이 함수는 W를 손실 값에 매핑하는 함수로 볼 수 있음
  • loss_value = f(W)
  • W0(W의 현재값)에서 f의 변화율: W와 같은 크기의 텐서인 gradient(f)(W0)
  • 이 텐서의 각 원소 gradient(f)(W0)[i, j]: W0[i, j]를 변경했을 때 loss_value가 바뀌는 방향과 크기를 나타냄
  • 즉 gradient(f)(W0)가 W0에서 함수 f(W) = loss_value의 그래디언트
  • gradient(f)(W0)는 W0에서 f(W)의 기울기를 나타내는 텐서로 해석 가능
  • f(W)에서 그래디언트의 반대 방향으로 W를 움직이면 f(W) 값을 줄일 수 있음

확률적 경사 하강법

  • 절충안: 미니 배치 확률적 경사 하강법(미니 배치 SGD)
    1. 훈련샘플 배치 x와 타깃 y를 추출
    2. x로 네트워크를 실행하고 y_pred 구하기
    3. y와 y_pred 사이의 오차를 측정해 네트워크 손실 계산
    4. 네트워크의 파라미터에 대한 손실 함수의 그래디언트 계산(backward pass)
    5. 그래디언트 반대 방향으로 파라미터 이동
  • 트루 SGD
    • 반복마다 하나의 샘플과 하나의 타깃을 뽑음
  • 배치 SGD
    • 가용한 모든 데이터로 반복 실행
    • 업데이트가 정확하지만 많은 비용
  • SGD 변종들
    • 업데이트할 다음 가중치를 계산할 때 현재 그래디언트 값만 보지 않고 이전에 업데이트된 가중치를 여러 가지 다른 방식으로 고려
    • ex) 모멘텀을 사용한 SGD, Adagrad, RMSProp 등
    • 이런 변종들을 최적화 방법 or 옵티마이저라 부름
    • 모멘텀은 SGD에 있는 2개의 문제점인 수렴 속도와 지역 최솟값을 해결
# 모멘텀 단순 구현
past_velocity = 0.
momentum = 0.1
while loss > 0.1:
w, loss, gradient = get_current_parameters()
velocity = momentum * past_velocity - learning_rate * gradient
w = w + momentum * velocity - learning_rate * gradient
past_velocity = velocity
update_parameter(w)

변화율 연결: 역전파 알고리즘

  • 3개의 텐서 연산 a, b, c와 가중치 행렬 W1, W2, W3로 구성된 네트워크 f의 예
  • f(W1, W2, W3) = a(W1, b(W2, c(W3)))
  • 연쇄법칙을 신경망 그래디언트 계산에 적용한 역전파 알고리즘
© 2019 THE DATASCIENTIST All Rights Reserved.
Theme by hiero