DO YOU KNOW TENSOR & NUMPY?

본 문서는 [케라스 창시자에게 배우는 딥러닝] 책을 기반으로 하고 있으며, subinium(본인)이 정리하고 추가한 내용입니다. 생략된 부분과 추가된 부분이 있으니 추가/수정하면 좋을 것 같은 부분은 댓글로 이야기해주시면 감사하겠습니다.

이번 파트에는 코드와 사전에 알아야하는 내용이 많아 가볍게 정리했습니다. 머신 러닝에 관련된 부분은 다른 문서를 참조해주세요.

2.1 신경망과의 첫 만남

머신 러닝의 hello world!라고 불리는 MNIST 는 많은 사람들이 알 것이라고 생각합니다. 아래가 MNIST 예제입니다.

MNIST Example - wikipedia

28 * 28 픽셀의 손글씨 숫자 이미지를 0~9(총 10개)로 분류하는 문제입니다. 1980년대 미국 국립표준기술연구소(NIST)에서 수집한 6만 개의 훈련 이미지와 1만 개의 테스트 이미지로 구성되어 있습니다.

지난 게시물에서 한번 mnist 예제를 실행해보았는데, 이제 코드와 원리를 하나하나 이해해보도록 합시다.

우선 MNIST 문제는 분류 문제입니다. 분류 문제에서 각 범주(Category)클래스(class) 라고 부르고, 데이터 포인트는 샘플(sample) 이라고 불리며, 샘플의 클래스를 레이블(label) 이라고 합니다.

즉 MNIST는 총 10개의 범주가 있고, 10개의 클래스로 나눌 수 있고, 위의 예시들은 샘플들의 집합이며, 첫 줄의 레이블은 0이라고 할 수 있습니다.

keras에는 이미 mnist 데이터가 포함되어 있어, 다음과 같이 데이터를 가져올 수 있습니다.

# 코드 2-1 케라스에서 MNIST 데이터셋 적재하기
from keras.datasets import mnist
(train_images, train_labels), (test_images, test_labels) = mnist.load_data()

각 데이터는 numpy 배열 형태로 데이터를 지니고 있습니다. 이번에는 신경망을 만드는 코드는 다음과 같습니다.

# 코드 2-2 신경망 구조
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'))

위 문제에서 각 층은. relu 층과 softmax 층입니다. 이렇게 신경망에 층을 추가하여, 의미있는 표현을 점진적으로 추출하는 것입니다. 마지막의 소프트맥스 층의 경우는 10개의 확률 점수가 들어 있는 배열을 반환합니다.

신경망이 훈련 준비를 마치기 위해서는 컴파일 단계에 포함될 3가지가 더 필요합니다.

  • 손실함수
  • 옵티마이저
  • 훈련과 테스트 과정을 모니터링할 지표
# 코드 2-3 컴파일 단계
network.compile(optimizer='rmsprop', loss='categorical_crossentropy', metrics=['accuracy'])

또한 현재는 [0,255] 사이의 값인 uint8타입으로 데이터들이 있으므로 변환해줍니다.

# 코드 2-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

또한 레이블을 범주형으로 인코딩해야 합니다. 아직 갈길이 멀군요. :-)

# 코드 2-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)
test_loss, test_acc = network.evaluate(test_images, test_labels)
print('test_acc:', test_acc)

다음과 같은 코드를 실행하면 다음과 같은 결과를 얻을 수 있습니다.

Epoch 1/5
60000/60000 [==============================] - 1s 22us/step - loss: 0.2571 - acc: 0.9257
Epoch 2/5
60000/60000 [==============================] - 1s 12us/step - loss: 0.1027 - acc: 0.9695
Epoch 3/5
60000/60000 [==============================] - 1s 12us/step - loss: 0.0686 - acc: 0.9797
Epoch 4/5
60000/60000 [==============================] - 1s 12us/step - loss: 0.0494 - acc: 0.9856
Epoch 5/5
60000/60000 [==============================] - 1s 12us/step - loss: 0.0368 - acc: 0.9894

10000/10000 [==============================] - 0s 16us/step

test_acc: 0.9789

훈련하는 동안 2개의 정보가 출력됩니다. 훈련 데이터에 대한 네트워크의 손실과 정확도입니다. 훈련시에 정확도는 98.94%까지 증가함을 알 수 있습니다. 그리고 테스트의 정확도는 97.89%가 나오는 것을 확인할 수 있습니다. 이런 정확도의 차이는 과대적합(overfitting) 때문입니다.

생각보다 머신 러닝 기초를 조금 보고와서 그런지 코드가 대부분 이해가 되긴합니다. 하지만 혼자서 다시 짜라하면 못짤 것 같네요.

2.2 신경망을 위한 데이터 표현

신경망에서 가장 중요한 것은 데이터에 대한 이해입니다. 신경망에서 가장 기초적인 데이터는 텐서(tensor) 라고 부르는 다차원 배열 데이터에서 시작합니다.

텐서는 데이터를 위한 컨테이너입니다. 텐서는 즉 임의의 차원 개수를 가지는 행렬, 벡터에 대한 표현이라고 생각하면 될 것 같습니다. 차원(dimension) 대신 축(axis)이라는 표현을 쓰기도 하니, 이 부분에 대해서 생각하면 데이터를 머릿속으로 생각하기에는 좋을 것 같습니다.

2.2.1 스칼라(0D 텐서)

하나의 숫자만 담고 있는 텐서를 스칼라(scala) 라고 부릅니다. numpy에서는 float32나 float64 데이터 하나가 스칼라 텐서입니다.

ndim속성으로 축 개수를 확인할 수 있고, 텐서의 축 개수를 랭크(rank) 라고 부릅니다.

2.2.2 벡터(1D 텐서)

숫자의 배열을 벡터(vector) 또는 1D 텐서라고 부릅니다.

2.2.3 행렬(2D 텐서)

벡터의 배열을 행렬(matrix) 또는 2D 텐서라고 부릅니다.

2.2.4 3D 텐서와 고차원 텐서

행렬의 배열을 3D 텐서라고 부릅니다. 이렇게 계속 배열을 만들어가며 텐서를 만듭니다. 보통은 0D에서 4D를 다루며, 동영상의 경우에는 5D 텐서까지 가기도 합니다.

2.2.5 핵심 속성

  • 축의 개수(랭크)
  • 크기(shape) : 텐서의 각 축을 따라 얼마나 많은 차원이 있는 지를 나타낸 파이썬의 튜플입니다. 2D는 (3,5)와 같이 나타낼 수 있습니다.
  • 데이터 타입 : 텐서에 포함된 데이터 타입입니다. 사전에 할당되어 연속된 메모리에 저장되어야 하여 가변 길이 문자열을 지원하지 않습니다.

속성의 확인은 ndim, shape, dtype 속성으로 확인할 수 있습니다.

2.2.6 넘파이로 텐서 조작하기

파이썬에서 배열에서 특정 원소를 선택하는 것을 슬라이싱 이라고 합니다. 넘파이에서는 좀 특별한 슬라이싱이 가능합니다. train_data는 (60000,28,28)의 shape을 가지고 있습니다. 여기서 10001번째부터 20001번까지 중간 14*14의 배열만 슬라이싱하고 싶다면 다음과 같은 코드로 가능합니다.

# 코드 : numpy slicing
my_slice = train_images[10000:20000, 7:-7, 7:-7]

3차원 직육면체로 생각하면 이해가 쉬울 것 같습니다.

2.2.7 배치 데이터

모든 데이터 텐서의 첫번째 축(인덱스 0)은 샘플 축(sample axis) 입니다. 딥러닝 모델은 한 번에 전체 데이터 셋을 처리하는 것이 아닌 단위로 나누어 순차적으로 처리합니다. 이때 나누는 단위를 배치(batch) 라고 합니다.

예시로 train_images에서 크기 128의 첫번째 배치는 다음과 같습니다.

batch = train_images[:128]

이렇게 배치 데이터를 다룰 때는 첫 번쨰 축을 배치 축, 배치 차원 이라고 부릅니다.

2.2.8 텐서의 실제 사례

  • 벡터테이터 : (samples, features) 크기의 2D 텐서
  • 시계열 데이터 또는 시퀀스 데이터 : (sample, timesteps, features) 크기의 3D 텐서
  • 이미지 : (samples, frames, width, channels) 또는 (samples, channels, height, width)의 크기의 4D 텐서
  • 동영상 : (samples, frames, height, width, channels) 또는 (samples, frames, channels, height, width) 크기의 5D 텐서

2.2.9 벡터 데이터

대부분의 경우에 해당됩니다. 하나의 데이터 포인트가 벡터로 인코딩 될 수 있으므로 배치 데이터는 2D 텐서로 인코딩될 것입니다. 첫 번째 축은 샘플 축, 두 번째 축은 특성 축 입니다.

2.2.10 시계열 데이터 또는 시퀀스 데이터

위의 벡터 데이터에서 시간 축이 추가된 데이터셋에서 사용됩니다.

다음과 같은 예시에서 사용될 수 있습니다.

  • 주식 가격 데이터셋
  • 트윗 데이터셋

2.2.11 이미지 데이터

높이, 너비, 컬러채널으로 한 이미지를 포매팅할 수 있고, 샘플의 개수가 있어 4D 텐서로 가능합니다. 흑백의 경우에는 컬러채널이 1개이기에 3D 텐서로 데이터셋을 표현할 수도 있습니다.

이미지의 경우에는 두 자기 방식으로 텐서의 크기를 지정합니다.

  • 채널 마지막 방식 : 텐서플로
  • 채널 우선 방식 : 씨아노

케라스에서는 두 형식을 모두 지원합니다. 옵션 설정으로 가능합니다.

2.2.12 비디오 데이터

비디오는 이미지를 프레임 단위로 읽는 것이므로 1D 텐서만 추가하면 됩니다.

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

텐서가 할 수 있는 연산을 알아봅시다.

2.3.1 원소별 연산

원소별 연산이라 함은 연산의 결과가 텐서에 있는 원소를 독립적으로 연산함을 의미합니다. 넘파이 배열에서는 시스템에 설치된 BLAS 구현에 복잡한 일들을 위임하며, 이는 병렬화되고 효율적인 텐서 조작 루틴으로 빠른 속도의 연산을 지원합니다.

relu연산, 덧셈 연산 등이 가능합니다.

2.3.2 브로드캐스팅

크기가 다른 텐서를 연산하면 오류가 나야하지만, numpy에서는 브로드캐스팅을 통해 연산을 진행합니다. 브로드캐스팅은 두 단계로 이루어집니다.

  1. 큰 텐서의 ndim에 맞도록 작은 텐서 축이 추가됩니다.
  2. 작은 텐서가 새축을 따라서 큰 텐서의 크기에 맞도록 반복됩니다.

글만 읽었을때는 메모리의 낭비가 심합니다. 그렇기에 실제적으로는 연산의 반복, 즉 알고리즘 수준에서만 연산이 진행됩니다.

2.3.3 텐서 점곱

텐서 곱샘이라고 부르는 점곱 연산은 가장 널리 사용되고 유용한 텐서 연산입니다. 행렬 곱으로 생각하면 편합니다.

텐서플로에서는 matmul함수를 사용하고(@ 연산 가능), 넘파이와 케라스에서는 dot 연산자를 사용합니다. (dot함수로도 가능)

고차원 텐서에서는 다음과 같이 연산이 가능합니다.

(a,b,c,d) (d, e) -> (a, b, c, e)

2.3.4 텐서 크기 변환

텐서 크기의 변환은 특정 크기에 맞게 열과 행을 재배열하는 것입니다. 전치(transposition) 연산도 많이 사용합니다.

# 코드 : 크기 변환
train_images = train_images.reshape((60000,28*28)) # 기존 60000, 28, 28
x = np.transpose(x) # 전치 연산 : 행렬에서는 행과 열을 바꾸는 연산

2.3.5 텐서 연산의 기하학적 해석

텐서는 N차원의 좌표 포인트로 해석될 수 있습니다. 그렇기에 벡터 연산도 가능합니다.

일반적으로 아핀 변환, 회전, 스케일링 등 기본적인 기하학적 연산은 텐서 연산으로 가능합니다.

2.3.6 딥러닝의 기하학적 해석

생략

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

훈련 반복 루프는 다음과 같은 역전파 알고리즘을 통해 반복되고 훈련됩니다.

  1. 훈련 샘플 x와 상응하는 타깃 y의 배치를 추출합니다.
  2. x를 사용하여 네트워크를 실행하고, 예측값을 구합니다.
  3. 실제값과 예측값의 차이로 현재 배치의 네트워크에 대한 손실을 계산합니다.
  4. 배치에 대한 손실이 감소하도록 네트워크의 가중치를 업데이트합니다.

이 과정에서 가중치를 업데이트하기 위한 효율적인 방법으로 미분 가능한 점을 활용하여, 그래디언트르 계산하여 업데이트 하는 것이 더 좋은 방법입니다.

이 부분은 가볍게만 정리했고, 머신 러닝에서 더 자세하게 다루겠습니다.

2.4.1 변화율이란?

가장 중요한 것은 결과값에 대한 함수 값의 기울기입니다. 엡실론 만큼 변화에 대한 결과 값의 변화를 찾기 위한 과정입니다.

즉, 데이터를 아주 작게 변화시켰을때 예상되는 데이터 변화값을 도함수로 구하는 것입니다. 그렇다면 원하는 방향으로 수치를 변경할 수 있습니다.

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

위의 변화율을 텐서 연산에 적용한 것이 그래디언트입니다. 다차원 입력인 텐서를 입력 받는 함수에 변화율 개념을 확장시킨 것 입니다. 값에 따라 너무 크게 변화하지 않게 하기위하여 스케일링 비율인 step값을 잘 설정해야합니다.

2.4.3 확률적 경사 하강법

미분 가능한 함수가 주어지면 이론적으로 함수의 최솟값을 해석적으로 구할 수 있습니다. 변화율이 0이 되는 지점을 향해 계속 조금씩 나아가면 됩니다. 모멘텀을 이용하여 더 효율적으로 계산할 수 있습니다.

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

연쇄 법칙(chain rule)이라 불리는 $f(g(x))’ = f’g(x) * g’(x)$ 를 이용하여 많은 층의 값들을 경사 하강법으로 연산할 수 있습니다.

현재는 텐서플로처럼 기호 미분이 가능한 최신 프레임워크를 활용하여 미분값을 구하니, 역전파 알고리즘을 직접 구현하고 역전파 공식을 유도하는 등의 시간 및 노력의 소모가 필요 없습니다. 이런게 머신 러닝의 대중화를 만든 것이겠죠?

2.5 첫 번째 예제 다시 살펴보기

훈련 데이터에서 수행되는 각 반복을 에포크(epoch) 라고 합니다.

생략

2.6 요약

생략

Leave a Comment