Study/MachineLearning

[Deep Learning with Python] 1. 신경망의 수학적 구성 요소

soohwan_justin 2021. 9. 12. 10:44

이번 포스트는 케라스 창시자에게 배우는 딥러닝(Deep Learning with Python)의 ch.2를 참고하였습니다.

 

이번 포스트를 읽기 전에, [Do it!] 시리즈를 먼저 읽는것을 권장합니다. 이 포스트에서는 [Do it!] 시리즈에 중복해서 나온 내용들을 많이 생략하였습니다.

 

이 책은 딥 러닝의 입문서로서는 적절하지 않다고 생각됩니다. 딥 러닝에 대한 어느정도 지식이 있고, 케라스의 사용방법에 대해 좀 더 자세히 알고 싶은 분께 권장합니다.

 

 

 

 

 

 

 

먼저, 복습해볼 겸 간단하게 MNIST 데이터 세트를 사용하여 케라스를 사용하여 완전 연결 신경망을 구성해보겠습니다.

 

 

1. 데이터를 불러오고, 훈련 데이터의 형태를 확인해봅니다.

from keras.datasets import mnist
(train_images, train_labels), (test_images, test_labels) = mnist.load_data()
 
print(train_images.shape)
print(len(train_labels))
print(train_labels)

 

 

2. 테스트 데이터의 형태를 확인해봅니다.

print(test_images.shape)
print(len(test_labels))
print(test_labels)

 

 

3. 신경망을 만들어봅니다. 은닉 층에는 512개의 뉴런을 가지고, 활성화 함수로는 렐루 함수를 사용하는 층을 만듭니다. 출력 층은 10개의 뉴런을 갖고있고, 활성화 함수로는 소프트맥스 함수를 사용합니다.

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'))
network.compile(optimizer='rmsprop', loss='categorical_crossentropy', metrics=['accuracy'])

 

층(layer)은 일종의 데이터 처리 필터라고 할 수 있으며, 신경망의 핵심 구성 요소입니다. 이 층에 어떤 데이터가 들어가면, 더 유용한 형태로 바뀌어 출력으로 나옵니다. 입력에 어떤 특징이 있으면 그 특징을 더 키우고, 의미없는 특징이 있다면 그 특징을 줄이는 방식으로 훈련하게 됩니다. 즉, 층은 입력된 데이터로부터 주어진 문제에 더 의미 있는 표현(representation)을 추출합니다. 대부분의 딥러닝은 간단한 층을 연결하여 구성하고, 점진적으로 데이터를 정제하는 형태를 갖고있습니다.

 

신경망을 컴파일 할때 사용한 옵션은 다음과 같습니다.

 

- 손실 함수 : 네트워크가 옳은 방향으로 학습될 수 있도록 도와주는 함수입니다. 이 값을 줄이는 방향으로 가중치가 업데이트 되어야 합니다.

- 옵티마이저 : 입력된 데이터와 손실 함수를 기반으로 네트워크를 업데이트 하는 방식을 정합니다. 이전에는 주로 경사 하강법을 사용했습니다.

- 훈련과 테스트 과정을 모니터링할 지표 : 이 예시에서는 정확도(제대로 분류된 이미지의 비율)을 사용합니다.

 

 

4. 훈련 데이터를 전처리 합니다.

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

이미지 데이터를 1차원 배열로 바꾸고, 각 값을 정규화 합니다.

 

 

5. 데이터를 원-핫 인코딩 하고, 훈련을 해봅니다.

from keras.utils import to_categorical
 
train_labels = to_categorical(train_labels)
test_labels = to_categorical(test_labels)
 
network.fit(train_images, train_labels, epochs=5, batch_size=128)

 

6. 훈련한 신경망을 평가해봅니다.

test_loss, test_acc = network.evaluate(test_images, test_labels)

 

 

신경망을 위한 데이터 표현

 

텐서란, 데이터를 위한 컨테이너(container)입니다. 텐서는 임의의 차원 개수를 가지는 행렬의 일반화된 모습이며, 텐서에서는 차원(demension)을 축(axis)라고도 합니다.

 

 

스칼라(0D 텐서)

 

하나의 숫자만 담고 있는 텐서를 스칼라(scalar) 또는 0D 텐서라고 합니다. ndim을 사용하면 넘파이 배열의 축 개수를 확인할 수 있습니다. 스칼라 텐서의 축 개수는 0이며, 텐서의 축 개수를 rank 라고도 합니다.

import numpy as np
x = np.array(12)
print(x.ndim)

 

벡터(1D 텐서)

 

숫자의 배열을 벡터(vector)라고 합니다. 1D 텐서는 하나의 축을 갖습니다.

x = np.array([4, 9, 10, 5, 7])
print(x.ndim)

 

이 벡터는 원소가 5개이므로 1차원 텐서인 5차원 벡터입니다. 5차원 벡터와 5차원 텐서를 혼동하지 않길 바랍니다. 벡터는 1차원 텐서입니다. 텐서의 차원은 축의 수를 의미하고, 벡터의 차원은 그 축에서의 원소의 개수를 의미합니다.

 

 

행렬(2D 텐서)

 

벡터의 배열이 행렬(matrix) 또는 2D 텐서입니다.

x = np.array([[1 , 5 , 9 , 10, 3 ],
              [10, 29, 4 , 7 , 9 ],
              [94, 3 , 53, 15, 14]])
print(x.ndim)

 

3D텐서와 고차원 텐서

 

행렬들을 하나의 배열로 합치면 직육면체 형태의 3D 텐서가 만들어집니다.

x = np.array([[[1 , 5 , 9 , 10, 3 ],
               [10, 29, 4 , 7 , 9 ],
               [94, 3 , 53, 15, 14]],
              [[1 , 5 , 9 , 10, 3 ],
               [10, 29, 4 , 7 , 9 ],
               [94, 3 , 53, 15, 14]],
              [[1 , 5 , 9 , 10, 3 ],
               [10, 29, 4 , 7 , 9 ],
               [94, 3 , 53, 15, 14]]])
 
print(x.ndim)

그리고 이 3D 텐서를 또 하나로 합치면 4D 텐서가 됩니다. 딥러닝에서는 보통 0D~4D의 텐서를 사용하며, 동영상 데이터를 다룰 경우에는 5D 텐서까지 사용하기도 합니다.

 

 

텐서의 속성

 

텐서의 핵심 속성에는 3가지가 있습니다.

- 축의 개수(랭크) : 텐서의 차원과 같습니다. 넘파이 라이브러리의 ndim 속성에 저장되어 있습니다.

- 크기(shape) : 텐서의 축에 따라 어느 정도 크기의 차원이 있는지를 튜플(tuple)로 나타냅니다. 앞에서 예시로 들었던 벡터의 크기는 (5,) 행렬의 크기는 (3, 5) 3D 텐서의 크기는 (3, 3, 5)입니다.  스칼라는 ()으로, 크기가 없습니다.

- 데이터 타입 : 텐서에 포함된 데이터의 타입입니다. 보통 float32, unit8, float64등을 사용합니다. 텐서는 사전에 할당되어 연속적인 메모리에 저장되어야 하므로 가변 길이의 문자열은 지원하지 않습니다.

 

 

넘파이로 텐서 조작하기

 

배열에 있는 특정 원소들을 선택하는 것을 슬라이싱(slicing)이라고 합니다.

 

11번째 원소부터 100번째 원소를 선택하는 예시는 다음과 같습니다.

(train_images, train_labels), (test_images, test_labels) = mnist.load_data()
 
my_slice = train_images[10:100]
print(my_slice.shape)

 

이렇게 슬라이싱 된 데이터 중 첫 번째 데이터를 확인해봅니다.

import matplotlib.pyplot as plt
plt.imshow(my_slice[0], cmap='gray')
plt.show()

 

위 이미지의 오른쪽 아래 1/4 부분만 슬라이싱 하는 코드는 다음과 같습니다.

my_slice = train_images[10:100, 14:, 14:]
print(my_slice.shape)

 

 

plt.imshow(my_slice[0], cmap='gray')
plt.show()

 

 

텐서의 실제 사례

 

우리가 사용할 데이터는 대부분 다음 중 하나에 속합니다.
 

- 벡터 데이터 : (samples, features) 크기의 2D 텐서

- 시계열 데이터 또는 시퀀스(sequence) 데이터 : (samples, timesteps, features) 크기의 3D 텐서

- 이미지 : (samples, height, width, channels) 또는 (samples, channels, height, width) 크기의 4D 텐서

- 동영상 : (samples, frames, height, width, channels) 또는 (samples, frames, challens, height, width) 크기의 5D 텐서

 

 

 

벡터 데이터

 

대부분의 데이터의 경우에 해당됩니다. 예를 들면, 다음과 같습니다.

 

- 키, 몸무게, 최근 3일 이내 운동 여부로 구성된 1만명의 데이터의 경우에는 각 사람들이 각 샘플에 해당되며, 각 샘플당 3개의 특성을 갖고있으므로 (10000, 3) 크기의 텐서에 저장될 수 있습니다.

 

 

시계열 또는 시퀀스 데이터

 

데이터에서 시간 또는 순서가 중요할 때는 시간 축을 포함하여 3D 텐서로 저장합니다. 각각의 샘플은 2D텐서로 인코딩 되며, 이 샘플을 시간 또는 순서대로 나열하므로 배치 데이터는 3D 텐서로 인코딩 됩니다. 관례적으로, 시간 축은 항상 두 번째 축(인덱스가 1인 축)입니다.

 

- 주식 가격 데이터셋: 1분마다 현재가, 최고가, 최소가를 저장합니다. 주식은 390분동안 거래됩니다. 따라서 1일만 생각해보면, 1분마다 데이터는 2D 텐서인 3D 벡터로 인코딩 됩니다. 만약 250일의 데이터를 저장한다면 (250, 390, 3)크기의 3D 텐서가 됩니다. 여기서는 1일치의 데이터가 하나의 샘플이 됩니다.

 

 

- 영화 리뷰 데이터셋: 링크를 참조하세요

 

 

이미지 데이터

 

이미지는 전형적으로 (높이, 너비, 컬러 채널)의 3차원으로 이루어집니다. 흑백의 경우 컬러 채널은 1이고, 컬러 이미지의 경우는 3입니다. 만약 (256, 256)크기의 이미지 128개가 있다면, 컬러의 경우 (128, 256, 256, 3)이고 흑백의 경우에는 (128, 256, 256, 1) 입니다. (아래 예시에서 3채널 컬러를 나타낸 RGB는 이해를 돕기 위한 예시 입니다.)

 

이미지 텐서의 크기를 지정하는 방식은 두 가지 입니다. 텐서플로는 채널을 마지막에 두는 channel-last 방식을 사용하고, 씨아노는 채널을 처음에 부는 channel-first 방식을 사용합니다. 즉, 텐서플로는 (samples, height, width, color_depth)이고, 씨아노는 (samples, color_depth, height, width)를 사용합니다.

 

 

비디오 데이터

 

하나의 비디오는 프레임의 연속이고 각 프레임은 컬러 이미지입니다. 이전에 이미지에서 나타냈듯이, 각 프레임은 (frames, height, width, color_depth)의 4D 텐서입니다. 그러므로 여러 비디오의 배치는 (samples, frames, height, width, color_depth)의 5D 텐서로 저장될 수 있습니다.

 

예를 들어, 60초의 144x256해상도의 비디오 클립 4개를 초당 4프레임으로 샘플링하면, 비디오 당 240프레임이 되고, 이런 비디오가 4개가 있으므로 (4, 240, 144, 256, 3)입니다.

 

 

 

텐서 연산

 

브로드캐스팅

 

브로드캐스팅은 크기가 다른 텐서를 더했을 때, 작은 텐서가 큰 텐서의 크기에 맞추어지는 것입니다. 이는 다음과 같이 두 단계로 이루어집니다.

 

1. 큰 텐서의 ndim에 맞도록 작은 텐서에 축이 추가됩니다.

2. 작은 텐서가 새 축을 따라서 큰 텐서의 크기에 맞게 반복됩니다.

 

예를 들어, (32, 10) 크기인 X에 (10, ) 크기인 y를 더한다고 가정하면, 큰 텐서의 크기에 맞게 새로운 축이 하나 추가되어 y는 (1, 10)이 됩니다. 그리고, 이렇게 새로운 축을 따라 32번 반복되어 y는 최종적으로 (32, 10)이 됩니다.

 

더 간단한 예시를 들자면, (5,) 크기인 X=[1, 2, 3, 4, 5]가 있습니다. 여기에 ()크기인 스칼라 y=3을 더하면, y는 새로운 축이 하나 추가되어 (1,)이 되고, 이 축을 따라 5번 반복되어 모든 원소의 값이 3인 (5,) 크기의 벡터가 되며, 이 값을 더하면 X=[4, 5, 6, 7, 8]이 됩니다.

 

 

고차원 텐서간의 점곱

 

행렬 연산에서, (a, b)크기의 행렬과 (b, c) 크기의 행렬을 점곱하면 (a, c)크기의 행렬이 나오는 것 처럼, 고차원 텐서간의 점곱의 결과는 다음과 같습니다.

 

(a, b, c, d) · (d,) = (a, b, c)

(a, b, c, d) · (d, e) = (a, b, c, e)

 

 

딥러닝의 기하학적 해석

 

모든 텐서 연산은 입력 데이터의 기하학적 변환으로 나타낼 수 있습니다. 즉, 아핀 변환, 회전, 스케일링 등 기본적인 기하학적 연산으로 나타낼 수 있다는 것입니다. 따라서 이런 단순한 단계가 길게 이어지면 고차원 공간에서 매우 복잡한 기하학적 변환을 할 수 있다는 것으로 해석할 수 있습니다.

 

이미지를 분류하는 예시를 들겠습니다. 만약 색깔이 다른 두 개의 색종이를 구겨서 뭉친다고 생각해보세요. 이렇게 뭉쳐진 공이 입력 데이터고, 색종이의 색은 분류 문제의 데이터 클래스 입니다. 이 색종이를 분류하기 위해서는 조금씩 종이를 펴야 분류가 가능할 것입니다. 이렇게 조금씩 종이를 펴는 과정이 딥러닝에서의 간단한 계산들에 대응됩니다. 실제로 딥 러닝이 진행되는 과정을 보면, 입력으로 사용했던 이미지가 점점 단순해지다가, 사람이 보기에는 무슨 의미인지 모를 정도로 단순해집니다. 결국 복잡했던 이미지들도 단순한 몇개의 이미지로 귀결되고, 이 단순한 이미지에서의 특징을 통해 데이터를 분류합니다.

 

예를 들어, 32X32의 입력 이미지를 분류한다고 할 때, 기존의 이미지를 갖고 분류 하려면 32x32 = 1024개의 포인트를 고려해야하지만 2X2까지 줄이면 그냥 4개의 포인트만 보고 어떤 포인트에 어떤 데이터가 들어갔는지에 따라 분류하면 굉장히 간단하게 분류할 수 있을것입니다.

 

 

그래이디언트 기반 최적화

 

그래이디언트를 기반으로 손실 함수의 최솟값을 찾을 때, 학습률(learning rate)가 중요하다고 했었습니다.

 

다음 그림을 보겠습니다.

 

 

가중치를 초기화한 후, 손실함수의 최솟값을 찾기 위해 경사 하강법으로 진행하려고 하는데, 초기화했을 때의 위치가 A가 될 수도 있고, B가 될 수도 있습니다. 만약 초깃값의 위치가 B라면 손실함수 값이 가장 작은 전역 최솟값에 도달할 수 있겠지만, A에서 시작한다면, 학습률이 낮은 경우에는 지역 최솟값에 갇혀서 가중치가 더이상 업데이트 되지 못할 것입니다.

 

이를 해결하기 위해, 물리학에서 사용하는 모멘텀(momentum)을 사용합니다. 모멘텀은 현재 기울기 값 뿐 아니라 과거의 가속도로 인한 현재 속도까지 고려합니다.

 

즉, 각 A지점에서 공을 굴린다고 생각해보세요. 공의 속도가 충분하다면, 지역 최솟값을 넘어 전역 최솟값으로 진행할 수 있을 것입니다.