3장 케라스와 텐서플로우

감사말: 프랑소와 숄레의 Deep Learning with Python, Second Edition 3장에 사용된 코드에 대한 설명을 담고 있으며 텐서플로우 2.6 버전에서 작성되었습니다. 소스코드를 공개한 저자에게 감사드립니다.

구글 코랩 설정: '런타임 -> 런타임 유형 변경' 메뉴에서 GPU를 지정한다. 이후 아래 명령어를 실행했을 때 오류가 발생하지 않으면 필요할 때 GPU가 자동 사용된다.

!nvidia-smi

구글 코랩에서 사용되는 tensorflow 버전을 확인하려면 아래 명령문을 실행한다.

import tensorflow as tf
tf.__version__

tensorflow가 GPU를 사용하는지 여부를 알고 싶으면 주피터 노트북 등 사용하는 편집기 및 파이썬 터미널에서 아래 명령문을 실행한다.

import tensorflow as tf
tf.config.list_physical_devices('GPU')

주요 내용

3.5 텐서플로우 기본 사용법

신경망 모델 훈련 핵심 1

  1. 상수 텐서와 변수 텐서
    • 상수 텐서(constant tensor): 입출력 데이터 등 변하지 않는 텐서
    • 변수 텐서(variable): 모델 가중치, 편향 등 업데이트 되는 텐서
  2. 텐서 연산: 덧셈, relu, 점곱 등
  3. 역전파(backpropagation):
    • 손실함수의 그레이디언트 계산 후 모델 가중치 업데이트
    • 그레이디언트 테이프(GradientTape) 이용

텐서플로우 기본 API 활용법

상수 텐서

상수 텐서는 한 번 생성되면 값을 수정할 수 없다. 딥러닝 연산에 많이 사용되는 상수 텐서는 다음과 같다.

한 번 생성된 상수 텐서는 수정이 불가능하다.

>>> x[0, 0] = 1.0

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-7-242a5d4d3c4a> in <module>
----> 1 x[0, 0] = 1.0

TypeError: 'tensorflow.python.framework.ops.EagerTensor' object does not support item assignment

넘파이 어레이는 반면에 수정 가능하다.

변수 텐서

신경망 모델 훈련 도중에 가중치 텐서는 업데이트될 수 있어야 한다. 이런 텐서는 변수 텐서로 선언해야 하며, Variaible 클래스로 감싼다.

Variable 클래스의 assign() 메서드를 활용하여 텐서 항목 전체 또는 일부를 수정할 수 있다.

주의사항: 모양(shape)이 동일한 텐서를 사용해야 한다.

>>> v.assign(tf.ones((3, 2)))

---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-13-e381ab0c94e6> in <module>
----> 1 v.assign(tf.ones((3, 2)))

~\anaconda3\lib\site-packages\tensorflow\python\ops\resource_variable_ops.py in assign(self, value, use_locking, name, read_value)
    886         else:
    887           tensor_name = " " + str(self.name)
--> 888         raise ValueError(
    889             ("Cannot assign to variable%s due to variable shape %s and value "
    890              "shape %s are incompatible") %

ValueError: Cannot assign to variable Variable:0 due to variable shape (3, 1) and value shape (3, 2) are incompatible

특정 항목을 수정하려면 인덱싱과 함께 사용한다.

상수 텐서의 경우처럼 각 항목은 EagerTensor 객체이지만 이번엔 항목 변환이 가능하다. 텐서플로우 내부에서 변수 텐서인 경우와 아닌 경우를 구분해서 assign() 메서드의 지원여부를 판단하는 것으로 보인다.

assign_add() 메서드는 변수 텐서에 대한 덧셈 연산을 수행한다. 단, 해당 객체의 항목이 업데이트된다.

텐서 연산

텐서 연산은 넘파이 어레이에 대한 연산과 거의 같다. 다음은 몇 가지 예제를 보여준다.

참고: 보다 다양한 텐서와 텐서 연산에 대한 자세한 설명은 텐서플로우 가이드(TensorFlow Guide) 영어판을 참고하라.

matmul() 곱셈은 넘파이의 점곱(dot) 연산자와 유사하게 작동하며 2차원 행렬모양의 텐서의 경우 행렬 곱으로 실행된다.

곱셈 연산자(*)는 항목별 곱셈으로 처리된다.

GradientTape API (다시 살펴 보기)

그레이디언트 테이프는 텐서 변수에 의존하는 미분함수의 그레이디언트를 자동 계산해준다. 아래 코드는 제곱 함수의 미분을 계산한다.

$$ f(x) = x^2 \quad \Longrightarrow \quad \nabla f(x) = \frac{df(x)}{dx} = 2x $$

그레이디언트 테이프 기능을 이용하여 신경망 모델 훈련 중에 손실 함수의 그레이디언트를 계산한다.

gradient = tape.gradient(loss, weights)

상수 텐서에 대해 그레이디언트 테이프를 이용하려면 tape.watch() 메서드로 감싸야 한다.

참고: 2차 미분도 가능하지만 여기서는 관심 대상이 아니다.

time = tf.Variable(0.)

with tf.GradientTape() as outer_tape:
    with tf.GradientTape() as inner_tape:
        position =  4.9 * time ** 2
    speed = inner_tape.gradient(position, time)

acceleration = outer_tape.gradient(speed, time)

저수준 선형 분류 신경망 구현

순수 텐서플로우 API만을 이용하여 선형 분류 신경망을 구현한다.

데이터셋 생성

두 개의 (1000, 2) 모양의 양성, 음성 데이터셋을 하나의 (2000, 2) 모양의 데이터셋으로 합치면서 동시에 자료형을 np.float32로 지정한다. 자료형을 지정하지 않으면 np.float64로 지정되어 보다 많은 메모리와 실행시간을 요구한다.

음성 샘플의 타깃은 0, 양성 샘플의 타깃은 1로 지정한다.

양성, 음성 샘플을 색깔로 구분하면 다음과 같다.

가중치 변수 텐서 생성

예측 모델(함수) 선언

아래 함수는 하나의 층만 사용하는 모델이 출력값을 계산하는 과정이다.

손실 함수: 평균 제곱 오차(MSE)

훈련 단계

하나의 배치에 대해 예측값을 계산한 후에 손실 함수의 그레이디언트를 이용하여 가중치와 편향을 업데이트한다.

배치 훈련

배치 훈련을 총 40번 반복한다.

훈련상태를 보면 여전히 개선의 여지가 보인다. 따라서 학습을 좀 더 시켜본다.

예측

예측 결과를 확인하면 다음과 같다. 예측값이 0.5보다 클 때 양성으로 판정하는 것이 좋은데 이유는 샘플들의 레이블이 0 또는 1이기 때문이다. 모델은 훈련과정 중에 음성 샘플은 최대한 0에, 양성 샘플은 최대한 1에 가까운 값으로 예측하여 손실값이 줄도록 노력하며, 옵티마이저가 그렇게 유도한다. 따라서 0과 1의 중간값인 0.5가 판단 기준으로 적절하다.

결정 경계를 직선으로 그리려면 아래 식을 이용한다.

y = - W[0] /  W[1] * x + (0.5 - b) / W[1]

이유는 위 모델의 예측값이 다음과 같이 계산되며,

W[0]*x + W[1]*y + b

위 예측값이 0.5보다 큰지 여부에 따라 음성, 양성이 판단되기 때문이다.

3.6 케라스의 핵심 API 이해

신경망 모델 훈련 핵심 2

  1. 층(layer)과 모델: 층을 적절하게 쌓아 모델 구성
  2. 손실 함수(loss function): 학습 방향을 유도하는 피드백 역할 수행
  3. 옵티마이저(optimizer): 학습 방향을 정하는 기능 수행
  4. 메트릭(metric): 정확도 등 모델 성능 평가 용도
  5. 훈련 반복(training loop): 미니 배치 경사하강법 실행

층(layer)의 역할은 다음과 같다.

층은 사용되는 클래스에 따라 다양한 형식의 텐서를 취급한다.

케라스를 활용하여 딥러닝 모델을 구성하는 것은 호환 가능한 층들을 적절하게 연결하여 층을 쌓는 것을 의미한다.

Layer 클래스

케라스에서 사용되는 모든 층 클래스는 Layer 클래스를 상속하며, 이를 통해 상속받는 __call__() 메서드가 가중치와 편향 벡터를 생성 및 초기화하고 입력 데이터를 출력 데이터로 변환하는 일을 수행한다.

Layer 클래스에서 선언된 __call__() 메서드가 하는 일을 간략하게 나타내면 다음과 같다.


def __call__(self, inputs):
    if not self.built:
        self.build(inputs.shape)
        self.built = True
return self.call(inputs)

위 코드에 사용된 인스턴스 변수와 메서드는 다음과 같다.

즉, 층은 입력값이 들어오면 가장 먼저 입력값의 모양을 확인한 다음에 가중치 텐서와 편향 텐서를 생성 및 초기화하며, 그 다음에 아핀 변환과 활성화 함수를 이용하여 출력값을 계산한다.

층은 입렵값을 확인하면서 동시에 입력값의 모양을 확인하기 때문에 2장에서 살펴본 MNIST 모델 사용된 Dense 클래스처럼 입력 데이터에 정보를 미리 요구하지 않는다.


from tensorflow import keras
from tensorflow.keras import layers

model = keras.Sequential([
    layers.Dense(512, activation="relu"),
    layers.Dense(10, activation="softmax")
])

Dense 클래스 직접 구현하기

위 설명을 바탕으로 해서 Dense 클래스와 유사하게 작동하는 SimpleDense 클래스를 직접 정의하려면 build() 메서드와 call() 메서드를 아래와 같이 구현하면 된다. 두 메서드의 정의에 사용된 매개변수와 메서드는 다음과 같다.

SimpleDense 층 하나로 이루어진 모델이 작동하는 방식은 다음과 같다.

출력값 생성: __call__() 메서드를 실행하면 다음 사항들이 연속적으로 처리된다.

층에서 모델로

딥러닝 모델은 층으로 구성된 그래프라고 할 수 있다. 앞서 살펴 본 Sequential 모델은 층을 일렬로 쌓아 신경망을 구성하며, 층층이 쌓인 층은 아래 층에서 전달한 값을 받아 변환한 후 위 층으로 전달하는 방식으로 작동한다. 예를 들어, 아래 모델은 4개의 SimpleDense 층으로 구성되었다. 각 층의 역할은 앞서 설명한 my_dense 모델의 그것과 동일하다. 즉, 32, 64, 32, 10 개의 특성을 갖는 출력값을 계산한 후 다음 층으로 전달한다.

model = keras.Sequential([
    SimpleDense(32, activation="relu"),
    SimpleDense(64, activation="relu"),
    SimpleDense(32, activation="relu"),
    SimpleDense(10, activation="softmax")
])

앞으로 층을 구성하는 다양한 방식을 살펴볼 것이다. 예를 들어, 아래 그림은 나중에 살펴 볼 트랜스포머(Transformer) 모델에 사용된 층들의 복잡한 관계를 보여준다.

그림 출처: Deep Learning with Python(Manning MEAP)

가설 공간

모델의 학습과정은 층을 어떻게 구성하였는가에 전적으로 의존한다. 앞서 살펴 보았듯이 각각의 층에서 이루어지는 일은 기본적으로 아핀 변환과 활성화 함수 적용이다. 여러 개의 Dense 층을 Sequential 모델을 이용하여 층을 구성하면 아핀 변환,relu() 등의 활성화 함수를 연속적으로 적용하여 입력 텐서를 특정 모양의 텐서로 변환한다. 반면에 다른 방식으로 구성된 모델은 다른 방식으로 텐서를 하나의 표현에서 다른 표현으로 변환시킨다.

이렇듯 층을 구성하는 방식에 따라 텐서들이 가질 수 있는 표현들의 공간이 정해진다. 이런 의미에서 '망 구성방식(network topology)에 따른 표현 가설 공간(hypothesis space)'이란 표현을 사용한다.

망 구성방식

신경망의 구성은 주어진 데이터셋과 모델의 목적에 따라 결정되며 특별한 규칙이 따로 있지는 않다. 주어진 문제를 해결하는 모델의 구성은 이론 보다는 많은 실습을 통한 경험에 의존한다. 앞으로 다양한 예제를 통해 다양한 모델을 구성하는 방식을 다룬다.

모델 컴파일

모델의 구조를 정의한 후에 아래 세 가지 설정을 추가로 지정해야 한다.

세 가지 설정에 사용된 문자열은 지정된 함수를 가리키도록 준비되어 있다. 예를 들어 아래 코드는 앞서의 컴파일과 동일한 결과가 나오도록 옵티마이저, 손실함수, 평가지표에 필요한 객체들을 직접 지정하였다.

앞으로 다양한 예제를 통해 옵티마이저, 손실함수, 평가지표를 적절하게 선택하는 방법을 살펴볼 것이다.

fit() 메서드 작동법

모델을 훈련시키려면 fit() 메서드를 적절한 인자들과 함께 호출해야 한다.

아래 코드는 앞서 넘파이 어레이로 생성한 (2000, 2) 모양의 양성, 음성 데이터셋을 대상으로 훈련한다.

훈련이 종료되면 fit() 메서드는 History 객체를 반환하며, history 속성에 훈련 과정 중에 측정된 손실값, 평가지표를 에포크 단위로 기억한다.

검증 세트 활용

훈련된 모델이 완전히 새로운 데이터에 대해 예측을 잘하는지 여부를 판단하려면 전체 데이터셋을 훈련 세트와 검증 세트로 구분해야 한다.

아래 코드는 훈련 세트와 검증 세트를 수동으로 구분하는 방법을 보여준다.

History 객체는 훈련 세트 뿐만 아니라 검증 세트를 대상으로도 손실값과 평가지표를 기억한다.

훈련 후에 검증 세트를 이용하여 평가하려면 evaluate() 메서드를 이용한다.

모델 활용

훈련된 모델을 활용하는 두 가지 방법이 있다.

먼저, __call__() 메서드를 활용한다. 즉, 데이터셋을 모델과 함께 직접 호출한다.

하지만 이 방식은 데이터셋이 매우 크면 적절하지 않을 수 있다. 따라서 predict() 메서드를 이용하여 배치를 활용하는 것을 추천한다.