5. 케라스와 텐서플로우#

감사의 글

아래 내용은 프랑소와 숄레의 Deep Learning with Python(2판)의 소스코드 내용을 참고해서 작성되었습니다. 자료를 공개한 저자에게 진심어린 감사를 전합니다.

소스코드

여기서 언급되는 코드를 (구글 코랩) 케라스와 텐서플로우에서 직접 실행할 수 있다.

슬라이드

본문 내용을 요약한 슬라이드를 다운로드할 수 있다.

주요 내용

딥러닝 주요 라이브러리와 케라스의 핵심 API를 소개한다.

5.1. 딥러닝 주요 라이브러리#

5.1.1. 텐서플로우#

텐서플로우는 파이썬에 기반한 머신러닝 플랫폼이며, 머신러닝 모델의 훈련에 특화된 텐서 연산을 지원한다.

  • 그레이디언트 자동 계산

  • GPU, TPU 등 고성능 병렬 하드웨어 가속기 활용 가능

  • 여러 대의 컴퓨터 또는 클라우드 컴퓨팅 서비스 활용 가능

  • C++(게임), 자바스크립트(웹브라우저), TFLite(모바일 장치) 등과 호환 가능

5.1.2. 케라스#

딥러닝 모델 구성 및 훈련에 효율적으로 사용될 수 있는 다양한 API를 제공하며, 텐서플로우의 프론트엔드front end 인터페이스 기능을 수행한다. 원래 텐서플로우와 독립적으로 개발되었지만 텐서플로우 2.0부터 텐서플로우 라이브러리의 최상위 프레임워크framework로 포함됐다.

모듈, 패키지, 라이브러리, 프레임워크

한 번 구현한 파이썬 코드를 다른 파이썬 파일의 코드에서 공유해서 사용할 수 있도록 하기 위해 모듈module을 활용한다. 파이썬 모듈은 간단하게 말하면 하나의 파이썬 소스코드 파일이며, 확장자로 .py 가 사용된다. 모듈에는 보통 서로 연관된 함수와 클래스 등이 저장된다.

하나의 모듈이 독립적으로 제공되기도 하지만 다른 모듈과 함께 하나의 모음집으로 제공되기도 하며, 모음집의 크기와 용도에 따라 패키지, 라이브러리, 프레임워크 등 다양한 이름으로 불린다.

종류

설명

모듈module

파이썬 소스코드 파일

패키지package

모듈을 모아놓은 디렉토리(폴더)

라이브러리library

모듈, 패키지 등 재사용이 가능한 코드의 모음집을 통칭헤서 부르는 이름

프레임워크framework

라이브러리 보다 포괄적인 개념. 라이브러리가 도구 모음집만 제공하는 반면에 프레임워크는 라이브러리와 함께 라이브러리를 쉽게 적용할 수 있는 틀frame과 아키텍처architecture를 함께 제공

5.1.3. 케라스 3.0, 텐서플로우, 파이토치, 잭스#

파이토치PyTorch 또한 텐서 연산을 지원하는 딥러닝 라이브러리이다. 텐서플로우와 케라스의 조합이 강력하지만 신경망의 보다 섬세한 조정은 약하다는 지적을 많이 받는 반면에 파이토치는 상대적으로 보다 자유롭게 신경망을 구성할 수 있다고 평가된다.

케라스 3.0은 텐서플로우 뿐만 아니라 파이토치, 그리고 고성능 딥러닝 프레임워크인 잭스Jax의 프론트엔드 기능도 함께 지원한다. 잭스는 LLM 처럼 거대 모델 훈련을 위해 구글에서 만든 라이브러리이며 매우 빠른 연선을 지원한다.

텐서플로우와 케라스의 조합이 여전히 보다 많이 사용되지만 딥러닝 연구에서 파이토치와 잭스의 활용 또한 점점 늘고 있다.

5.1.4. 딥러닝 개발환경#

딥러닝 신경망 모델의 훈련을 위해서 GPU 또는 TPU 등을 활용할 것을 강력히 추천한다. 그렇지 않으면 딥러닝 신경망 모델의 훈련이 제대로 실행되지 않을 가능성이 높다. 구글 코랩을 이용하면 특별한 준비 없이 바로 신경망 모델을 GPU, TPU 등과 함께 훈련시킬 수 있다.

또한 엔비디아 그래픽카드가 장착된 PC에서 딥러닝 모델을 훈련시킬 수 있다. 윈도우 11 운영체제에서 GPU를 지원하는 개발환경을 준비하는 요령은 WSL2에 GPU 지원 Tensorflow, PyTorch 설치 요령를 참고한다.

보다 전문적인 딥러닝 연구를 위해 대용량의 메모리와 고성능의 CPU, GPU, TPU 가 필요한 경우 직접 모든 장비를 마련하기는 어려울 수 있다. 대신 구글 클라우드 플랫폼 또는 아마존 웹서비스(AWS EC2) 등에서 유료로 지원되는 고성능 클라우드 컴퓨팅 서비스를 활용할 것을 추천한다.

5.2. 케라스 핵심 API#

딥러닝 신경망 모델은 층layer으로 구성된다. 모델에 사용되는 층의 종류와 층을 쌓는 방식에 따라 모델이 처리할 수 있는 데이터와 훈련 방식이 달라진다. 케라스 라이브러리가 신경망의 효율적인 층의 구성과 모델 훈련에 적합한 다양한 API를 제공한다.

5.2.1. 층 API#

층은 입력 데이터를 지정된 방식에 따라 다른 모양의 데이터로 변환하여 전달하는 순전파forward pass를 담당한다. 또한 데이터 변환에 필요한 가중치weight와 편향bias을 저장한다.

데이터를 변환하는 방식에 따라 층의 기능이 달라진다. 케라스는 딥러닝 신경망 모델 구성에 가장 많이 사용되는 층을 다양한 클래스로 제공한다. 케라스를 활용하여 딥러닝 모델을 구성하는 일은 호환 가능한 층들을 적절하게 연결하여 층을 쌓는 것을 의미한다.

예제: Dense 층 상세

2.1절에서 MNIST 데이터셋을 이용한 분류 모델에 사용된 신경망 모델은 연속으로 쌓은 두 개의 Dense 층으로 구성된다.

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

Dense 층은 Dense 클래스의 인스턴스로 생성되는데 아래에 정의된 SimpleDense 클래스의 인스턴스를 Dense 층 대신 사용할 수 있다. 클래스 정의에서 다음 두 가지 사항에 주목한다.

  • tensorflow.keras.layers 모듈의 Layer 클래스 상속

    • 케라스는 모든 종류의 층을 tf.keras.layers 모듈의 Layer 클래스의 자식 클래스로 제공하기 때문.

  • build() 메서드와 call() 메서드 구현

    • build() 메서드: 가중치와 편향 초기화

    • call() 메서드: 데이터 변환 계산

from tensorflow import keras

class SimpleDense(keras.layers.Layer):
    def __init__(self, units, activation=None):
        super().__init__()
        self.units = units           # 유닛 개수 지정
        self.activation = activation # 활성화 함수 지정

    # 가중치와 편향 초기화 메서드
    def build(self, input_shape):
        input_dim = input_shape[-1]   # 입력 샘플 백테의 차원
        self.W = self.add_weight(shape=(input_dim, self.units),
                                 initializer="random_normal")
        self.b = self.add_weight(shape=(self.units,),
                                 initializer="zeros")

    # 데이터 변환 메서드
    def call(self, inputs):
        y = inputs @ self.W + self.b
        if self.activation is not None:
            y = self.activation(y)
        return y

클래스에서 선언된 SimpleDense 세 개의 메서드의 정의에 사용된 변수와 메서드는 다음과 같다.

  • 생성자 __init__() 메서드:

    • units: 출력 샘플 벡터의 차원 지정

    • activation: 활성화 함수 지정

  • build() 메서드:

    • input_shape: 입력값(inputs)으로 들어온 배치 2D 텐서의 모양. 1번 인덱스의 항목이 입력 샘플 벡터의 차원.

    • input_dim: 입력 샘플 벡터의 차원

    • Wb: 가중치 텐서와 편향 벡터

    • add_weight(모양, 초기화방법): 지정된 모양의 텐서 생성 및 초기화. Layer 클래스에서 상속됨.

  • call() 메서드:

    • inputs: 입력 배치. 2D 텐서 데이터셋

SimpleDense 층의 데이터 변환

모델 훈련 루프에 포함된 순전파는 층에서 층으로 이어지는 연속된 데이터 변환으로 이뤄진다. SimpleDense 층을 이용하여 입력 배치 데이터셋이 변환되어 다음 층으로 전달되는 과정을 살펴본다.

아래 코드에서 my_dense 변수는 하나의 SimpleDense 인스턴스로 선언된 층을 가리킨다.

  • 유닛 수: 512개

  • 활성화 함수: relu

my_dense = SimpleDense(units=512, activation=tf.nn.relu)

아래 코드는 입력 배치 예제 데이터셋으로 사용될 (128, 784) 모양의 텐서를 하나 임의로 생성한다.

  • 128: 배치 크기

  • 784: MNIST 데이터셋의 손글씨 이미지 한 장의 픽셀 개수(28 * 28 = 128)

input_tensor = tf.ones(shape=(128, 784))

이제 my_dense를 함수 호출하듯이 사용하면 출력값이 계산된다.

output_tensor = my_dense(input_tensor)

층의 출력값은 (128, 512) 모양의 텐서다. 이유는 차원이 784인 각 데이터 샘플 벡터가 차원이 512인 벡터로 변환되기 때문이다. 512는 유닛 수임에 주목한다.

print(output_tensor.shape)
(128, 512)

Layer 클래스의 __call__() 매직 메서드

이전 코드에서 output_tensor에 할당되는 값, 즉, my_dense 층을 통과하면서 변환된 값은 my_dense를 마치 input_tensor를 인자로 하는 함수 호출의 결과다. 하지만 my_denseSimpleDense 클래스의 인스턴스이지 함수가 아니다.

그럼에도 불구하고 층 객체를 마치 함수처럼 사용할 수 있는 이유는 Layer 클래스로부터 상속하는 __call__() 메서드가 작동하기 때문이다. 즉, my_dense(input_tensor)가 실행되면 파이썬 실행기는 내부적으로 다음과 같이 __call___() 메서드를 호출한다.

my_dense.__call__(input_tensor)

__call__() 매직 메서드가 하는 일의 핵심을 코드로 나타내면 다음과 같다.

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

즉, __call__() 메서드는 다음 두 가지를 책임진다.

  • 모델 훈련을 시작할 때 층에 필요한 가중치 텐서와 편형 텐서가 없는 경우 적절한 모양의 가중치 텐서와 편향 벡터를 초기화한다. 또한 역전파 과정에서 옵티마이저에 의해 업데이트된 가중치 텐서와 편향 벡터를 저장한다.

    • self.built: 모델 훈련에 사용될 가중치와 편향이 준비되어 있는지 여부 기억

    • self.build(inputs.shape): 입력 데이터셋의 모양 정보를 이용하여 적절한 모양의 가중치 텐서는 정규 분포를 이용하여 무작위로, 적절한 모양의 편향 벡터는 0 벡터로 초기화.

  • 저장된 가중치 텐서와 편향 벡터를 이용하여 벡터를 변환하여 출력값으로 반환한다.

    • self.call(inputs): 입력 텐서를 출력 텐서로 변환하는 계산 실행

my_dense.__call__(input_tensor)와 연관지어 설명하면 다음과 같다.

  • build() 메서드 호출. 가중치 텐서와 편향 텐서가 존재하지 않은 경우 아래 내용 실행.

    • (784, 512) 모양의 가중치 텐서 W 생성 및 무작위 초기화.

    • (512, ) 모양의 편향 벡터 b 생성 및 0으로 초기화.

  • call() 메서드 호출. 저장되어 있는 가중치 텐서와 편향 벡터를 이용하여 입력 데이터셋을 아래 방식으로 변환.

    y = input_tensor @ W + b   # 아핀 변환
    y = tf.nn.relu(y)          # 활성화 함수 적용
    

5.2.2. 모델 API#

여러 개의 층으로 구성된 리스트를 이용하여 순차 모델을 구성하는 Sequential 클래스는 층을 일렬로 쌓아 올리는 모델이다. 마지막 층을 제외한 모든 층은 이전 층에서 전달된 입력 텐서를 변환한 다음에 다음 층으로 전달한다. 반면에 마지막 층에서 변환된 값은 모델의 최종 예측값으로 사용된다.

tf.keras.Model 클래스

Sequential 클래스를 포함하여 케라스가 지원하는 모델 클래스는 tensorflow.keras.Model 클래스를 상속한다. 예를 들어 아래 코드는 Sequential 클래스와 유사하게 작동하는 MySequential 클래스를 tensorflow.keras.Model 클래스를 상속한다.

from tensorflow import keras

class MySequential(keras.Model):
    def __init__(self, list_layers): # 층들의 리스트 지정
        super().__init__()
        self.list_layers = list_layers

    # 순전파: 층과 층을 선형적으로 연결
    def call(self, inputs):
        outputs = inputs
        for layer in self.list_layers: # 이전 층에서 변환된 텐서를 다음 층으로 바로 전달
            outputs = layer(outputs)
        return outputs

앞서 두 개의 Dense 층으로 구성된 순차 모델을 MySequential 클래스와 두 개의 SimpleDense 층을 이용하여 다음과 같이 선언할 수 있다.

layer_1 = SimpleDense(units=512, activation=tf.nn.relu)   # 첫째 밀집층
layer_2 = SimpleDense(units=10, activation=tf.nn.softmax) # 둘째 밀집층

model = MySequential([layer_1, layer_2])

SimpleDense층 대신 Dense 층을 이용한다면 다음과 같이 활성화 함수를 문자열로 지정할 수 있음에 주의한다.

layer_1 = Dense(units=512, activation='relu')   # 첫째 밀집층
layer_2 = Dense(units=10, activation='softmax') # 둘째 밀집층

model = MySequential([layer_1, layer_2])

모델의 학습과정과 층의 구성

모델의 학습과정은 전적으로 층의 구성방식에 의존한다. 그리고 층의 구성 방식은 주어진 데이터셋과 모델이 해결해야 하는 문제에 따라 달라진다. 층을 구성할 때 특별히 정해진 규칙은 없지만 문제 유형에 따른 권장 모델이 다양하게 개발되었다.

앞으로 보다 복잡하고 다양한 방식으로 층을 구성하는 방식들을 살펴볼 것이다. 예를 들어, 아래 그림은 12장 자연어 처리에서 소개하는 트랜스포머Transformer 모델의 복잡한 층 연결 구조를 보여준다.

5.2.3. 모델 컴파일 API#

모델 컴파일은 선언된 모델을 진행하기 위해 필요한 다음 세 가지 설정을 추가로 지정하는 과정을 가리킨다.

  • 손실 함수

    • 훈련 중 모델의 성능이 얼마나 나쁜지 측정.

    • 가중치와 편향 의존.

    • 가중치와 편향에 대해 미분 가능해야 함.

    • 옵티마이저가 경사하강법을 적용할 때 사용되는 함수.

  • 옵티마이저

    • 가중치와 편향을 업데이트하는 역전파 반복 실행

  • 평가지표

    • 훈련과 테스트 과정을 모니터링 할 때 사용되는 모델 평가 지표.

    • 손실 함수와는 달리 훈련에 직접 사용되지는 않음.

케라스는 모델 컴파일에 사용되는 다양한 API를 제공하며 아래 코드에서처럼 문자열로 지정될 수 있다.

model.compile(optimizer="rmsprop",
              loss="mean_squared_error",
              metrics=["accuracy"])

위 코드에서 사용된 각각의 문자열이 가리키는 API는 다음과 같다.

문자열

파이썬 객체

"rmsprop"

keras.optimizers.RMSprop()

"mean_squared_error"

keras.losses.MeanSquaredError()

"accuracy"

keras.metrics.BinaryAccuracy()]

앞서 SimpleDense를 통해 본 것처럼 문자열 대신 API를 직접 지정해도 된다.

model.compile(optimizer=keras.optimizers.RMSprop(),
              loss=keras.losses.MeanSquaredError(),
              metrics=[keras.metrics.BinaryAccuracy()])

하지만 다음 두 가지의 경우엔 문자열 대신 해당 API를 직접 지정해야 한다.

  • 사용자가 직접 학습률(learning_rate)을 지정하는 옵티마이저를 사용하고자 하는 경우처럼 모델의 하이퍼파라미터를 기본값이 아닌 다른 값으로 지정하고자 하는 경우

  • 사용자가 직접 정의한 API를 사용하고자 하는 경우

다음은 직접 객체를 지정하는 방식으로 모델을 컴파일하는 형식을 보여준다.

model.compile(optimizer=keras.optimizers.RMSprop(learning_rate=1e-4),
              loss=사용자정의손실함수객체,
              metrics=[사용자정의평가지표_1, 사용자정의평가지표_2])

일반적으로 가장 많이 사용되는 옵티마이저, 손실함수, 평가지표는 다음과 같으며 앞으로 다양한 예제를 통해 적절한 옵티마이저, 손실함수, 평가지표를 선택하는 방법을 살펴볼 것이다.

옵티마이저 API

다양한 옵티마이저의 장단점에 대해서는 Hands-on Machine Learning 3판의 11장에 정리되어 있다.

옵티마이저

문자열

keras.optimizers.SGD

“SGD”

keras.optimizers.RMSprop

“rmsprop”

keras.optimizers.Adam

“adam”

keras.optimizers.AdamW

“adamw”

keras.optimizers.Adagrad

“adagrad”

keras.optimizers.Nadam

“nadam”

손실 함수 API

일반적으로 모델의 종류에 따라 손실 함수를 선택한다.

손실 함수

문자열

용도

keras.losses.CategoricalCrossentropy

“categorical_crossentropy”

다중 클래스 분류. 원-핫 형식 타깃

keras.losses.SparseCategoricalCrossentropy

“sparse_categorical_crossentropy”

다중 클래스 분류. 정수 타깃

keras.losses.BinaryCrossentropy

“binary_crossentropy”

이진 분류

keras.losses.MeanSquaredError

“mean_squared_error”

회귀

keras.losses.MeanAbsoluteError

“mean_absolute_error”

회귀

keras.losses.CosineSimilarity

“cosine_similarity”

문장 번역, 물건 추천, 이미지 분류 등

평가지표 API

일반적으로 모델 종류와 목적에 따라 평가 지표를 선택한다.

평가지표

문자열

용도

keras.metrics.CategoricalAccuracy

“categorical_accuracy”

다중클래스 분류 정확도 측정. 원-핫 형식 타깃

keras.metrics.SparseCategoricalAccuracy

“sparse_categorical_accuracy”

다중클래스 분류 정확도 측정. 정수 타깃

keras.metrics.BinaryAccuracy

“binary_accuracy”

이진 분류 정확도 측정

keras.metrics.AUC

없음

다중 클래스 분류 AUC 측정

keras.metrics.Precision

없음

다중 클래스 분류 정밀도 측정

keras.metrics.Recall

없음

다중 클래스 분류 재현율 측정

5.2.4. 모델 훈련 API#

컴파일된 모델의 fit() 메서드를 적절한 인자와 함께 호출하면 스텝 단위로 반복되는 훈련 루프training loop가 실행된다. 훈련 루푸는 지정된 에포크 만큼 또는 학습이 충분히 이루어졌다는 평가가 내려질 때까지 반복된다.

지도학습 모델 훈련

지도 학습 모델의 훈련은 아래 코드에서처럼 적절한 인자들과 함께 fit() 메서드를 호출할 때 진행된다.

  • inputs: 훈련셋

  • targets: 타깃셋

  • epochs: 에포크 수

  • batch_size: 배치 크기

training_history = model.fit(
    inputs,
    targets,
    epochs=5,
    batch_size=128
)

History 객체: 훈련 결과

훈련이 종료되면 fit() 메서드는 모델의 훈련 결과를 담고 있는 History 객체가 반환된다. 예를 들어 History 객체의 history 속성은 에포크별로 계산된 손실값과 평가지표값을 사전 자료형으로 가리킨다.

training_history.history
{'loss': [0.2729695439338684,
  0.11179507523775101,
  0.07302209734916687,
  0.0526457279920578,
  0.04022042825818062],
 'accuracy': [0.9212833046913147,
  0.9672333598136902,
  0.9783666729927063,
  0.9844833612442017,
  0.988099992275238]}

검증셋 활용

머신러닝 모델 훈련의 목표는 훈련셋에 대한 높은 성능이 아니라 훈련에서 보지 못한 새로운 데이터에 대한 높은 성능이다.

새로운 데이터에 대한 모델의 성능을 예측하기 위해 검증셋, 즉 검증용 데이터셋을 훈련중에 활용할 수 있다. 주어진 훈련셋의 20~30 % 정도를 검증셋으로 지정한다. 훈련셋의 크기에 따라 검증셋의 비율을 적절하게 조정한다. 또한 훈련셋 자체가 매우 작은 경우엔 검증셋을 따로 분리하기 보다는 K-겹 교차 검증 등을 사용해야 한다.

훈련셋과 검증셋이 서로 겹치지 않도록 주의해야 한다. 그렇지 않으면 훈련 중에 모델이 검증셋에 포함된 데이터를 학습하기에 정확환 모델 평가를 할 수 없게 된다.

아래 코드는 미리 지정된 검증셋 val_inputs와 검증 타깃셋 val_targetsvalidation_data의 키워드 인자로 지정해서 모델 훈련이 진행되는 동안 에포크가 끝날 때마다 모델의 검증셋에 대한 성능을 측정하도록 한다.

model.fit(
    training_inputs,
    training_targets,
    epochs=5,
    batch_size=16,
    validation_data=(val_inputs, val_targets)
)

그러면 훈련이 종료되어 생성된 History 객체에 검증셋에 대한 손실값과 정확도도 함께 저장된다.

training_history.history
{'loss': [0.02950882911682129,
  0.021471761167049408,
  0.015012570656836033,
  0.011033009737730026,
  0.0080801947042346],
 'accuracy': [0.991428554058075,
  0.9937618970870972,
  0.9962857365608215,
  0.9974523782730103,
  0.9982380867004395],
 'val_loss': [0.029974577948451042,
  0.03373847156763077,
  0.03262251615524292,
  0.03768538683652878,
  0.03493628650903702],
 'val_accuracy': [0.9906111359596252,
  0.9896666407585144,
  0.9901666641235352,
  0.9882222414016724,
  0.9900555610656738]}

5.2.5. 모델 평가 API#

훈련이 종료된 모델을 실전 성능 평가를 evaluate() 메서드를 테스트셋과 함께 호출한다. 모델의 실전 성능 평가 또한 지정된 배치 단위로 실행된다.

loss_and_metrics = model.evaluate(test_images, test_labels, batch_size=128)

evaluate() 메서드는 테스트셋에 대한 손실값과 평가지표값을 담은 리스트를 반환한다.

print(loss_and_metrics)
[0.06799975782632828, 0.98089998960495]

5.2.6. 모델 활용 API#

실전에 배치된 모델은 새로이 입력된 데이터에 대한 예측을 실행한다. 학습된 모델의 예측값은 predict() 메서드를 활용하여 계산한다. 예측 또한 지정 크기의 배치 단위로 실행된다.

predictions = model.predict(new_inputs, batch_size=128)

5.3. 연습 문제#

  1. (실습) 케라스와 텐서플로우