4. 훈련 루프 상세#

참고

Writing a training loop from scratch를 참고하였습니다.

소스코드

여기서 언급되는 코드를 (구글 코랩) 훈련 루프에서 직접 실행할 수 있다.

슬라이드

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

주요 내용

신경망 모델의 fit() 메서드를 실행할 때 텐서플로우 내부에서 훈련 루프가 진행되는 과정을 상세히 살펴본다.

4.1. 모델 지정#

설명을 위해 세 개의 Dense 층으로 구성된 순차 모델을 MNIST 데이터셋을 이용하여 훈련시킨다.

import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers
import numpy as np

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

4.2. 옵티마이저, 손실 함수, 평가지표 지정#

모델 훈련에 필요한 요소인 옵티마이저, 손실 함수, 평가지표를 지정하기 위해 일반적으로 모델의 compile() 메서드를 다음과 같이 실행한다.

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

하지만 여기서는 모델의 fit() 메서드의 본체를 직접 구현하려 하기에 compile() 메서드를 실행하는 대신 컴파일 과정에 요구되는 API를 직접 선언한다.

옵티마이저 API 선언

아래코드는 모델 컴파일에 사용된 문자열 'rmsprop'에 해당한다.

optimizer = keras.optimizers.RMSprop(learning_rate=1e-3)

손실 함수 API 선언

0, 1, 2, 3 등 정수 형식의 타깃(레이블)을 예측하는 다중 클래스 분류 모델을 훈련시키는 경우 보통 SparseCategoricalCrossentropy 클래스를 손실함수 API로 지정한다. 아래코드는 모델 컴파일에 사용된 문자열 'sparse_categorical_crossentropy'에 해당한다.

loss_fn = keras.losses.SparseCategoricalCrossentropy(from_logits=True)

평가지표 API 선언

0, 1, 2, 3 등 정수 형식의 타깃(레이블)을 예측하는 다중 클래스 분류 모델을 훈련시키는 경우 평가지표는 SparseCategoricalAccuracy 클래스를 이용한다. 아래코드는 모델 컴파일에 사용된 문자열 'accuracy'에 해당한다.

train_acc_metric = keras.metrics.SparseCategoricalAccuracy() # 훈련셋 대상 평가

4.3. 데이터셋 준비#

훈련에 사용될 데이터를 준비한다.

훈련셋과 테스트셋 지정

from tensorflow.keras.datasets import mnist

# 훈련셋과 테스트셋 가져오기
(x_train, y_train), (x_test, y_test) = mnist.load_data()
x_train = np.reshape(x_train, (-1, 784))
x_test = np.reshape(x_test, (-1, 784))

배치 묶음 Dataset 객체 지정

훈련셋을 대상으로 배치 묶음으로 구성된 이터러블 객체인 텐서플로우의 tf.data.Dataset 자료형 값을 선언한다. tf.data.Dataset는 머신러닝 모델 훈련에 사용되는 대용량 데이터의 효율적인 처리를 지원하는 모음 자료형이다.

이터러블 객체

이터러블iterable 객체는 for 반복문에 활용될 수 있는 값이다. 보다 자세한 설명은 이터러블, 이터레이터, 제너레이터를 참고한다.

아래 이미지는 넘파이 어레이 훈련셋과 타깃셋을 조합하여 하나의 tf.data.Dataset 객체를 생성하는 과정을 보여준다.

tf.data.Dataset의 다양한 API(메서드)를 이용하면 적정한 배치를 모델 훈련에 제공하는 이터러블 객체를 생성할 수 있다. 아래 이미지는 대용량 데이터를 다룰 때 유용한 tf.data.Dataset의 자식 클래스들을 보여준다.

<그림 출처: TensorFlow>

여기서는 배치 크기가 128인 묶음으로 구성된 Dataset객체를 생성하여 훈련 루프에 이용한다. 아래 코드는 훈련용 Dataset 객체를 지정한다.

  • 훈련셋 어레이와 훈련셋 타깃 어레이로부터 Dataset 생성 후 배치로 묶은 Dataset으로 변환.

  • shuffle() 메서드를 이용하여 데이터 무작위 섞기 실행 후 배치 묶음 생성

batch_size = 128

# 훈련용 Dataset 객체
train_dataset = tf.data.Dataset.from_tensor_slices((x_train, y_train))
train_dataset = train_dataset.shuffle(buffer_size=1024).batch(batch_size)

4.4. 훈련 루프#

모델 훈련을 담당하는 훈련 루프는 다음과 같다.

  • 지정된 에포크 수만큼 에포크를 반복하는 for 반복문 실행

  • 각 에포크에 대해 배치 단위로 스텝을 진행하는 for 반복문 실행

    • 각 배치에 대해 GradientTape() 영역 내에서 순전파 실행 후 손실값 계산

    • 계산된 손실값을 대상으로 모델 가중치의 그래디언트 계산

    • 옵티마이저를 사용하여 모델의 가중치 업데이트

    • 해당 스텝의 평가지표 계산, 즉 훈련셋 평가지표 객체의 update_state() 메서드 호출

  • 평가지표를 확인하면서 에포크 마무리

    • 매 스텝을 통해 업데이트된 평가지표의 최종 결과 확인, 즉, result() 메서드 호출

    • 평가지표 초기화, 즉 reset_state() 메서드 호출

import time

epochs = 10
for epoch in range(epochs):
    print(f"\n{epoch} 번째 에포크 시작")
    # 에포크 시작시간
    start_time = time.time()

    # 훈련 스텝: 배치 단위로 진행되는 훈련 루프
    for step, (x_batch_train, y_batch_train) in enumerate(train_dataset):
        # 그레이디언트테이프: 손실함수 대상 그레이디언트 계산 준비
        with tf.GradientTape() as tape:
            logits = model(x_batch_train, training=True)
            loss_value = loss_fn(y_batch_train, logits)
        
        # 그레이디언트 계산 후 가중치 업데이트
        grads = tape.gradient(loss_value, model.trainable_weights)
        optimizer.apply_gradients(zip(grads, model.trainable_weights))

        # 훈련세 대상 평가지표(정확도) 업데이트
        train_acc_metric.update_state(y_batch_train, logits)

        # 100번째 스텝마타 손실값 출력
        if step % 100 == 0 and step > 0:
            print(f"  - {step}번째 스텝 손실값: {loss_value:.4f}")

    ## 에포크 마무리 단계
    
    # 에포크의 정확도 출력
    train_acc = train_acc_metric.result()
    print(f"  - 에포크 훈련 후 모델 정확도: {train_acc:.4f}")

    # 평가지표 초기화: 에포크 단위로 정확도 계산을 새롭게 진행하기 위해.
    train_acc_metric.reset_state()

    # 에포크 진행에 걸린시간 출력
    print(f"  - 에포크 훈련에 걸린 시간: {time.time() - start_time:.4f}")
0 번째 에포크 시작
  - 100번째 스텝 손실값: 2.1188
  - 200번째 스텝 손실값: 1.9567
  - 300번째 스텝 손실값: 0.9321
  - 400번째 스텝 손실값: 0.6668
  - 에포크 훈련 후 모델 정확도: 0.8797
  - 에포크 훈련에 걸린 시간: 7.6723

1 번째 에포크 시작
  - 100번째 스텝 손실값: 0.4685
  - 200번째 스텝 손실값: 0.3514
  - 300번째 스텝 손실값: 0.0946
  - 400번째 스텝 손실값: 0.2356
  - 에포크 훈련 후 모델 정확도: 0.9433
  - 에포크 훈련에 걸린 시간: 7.8152

2 번째 에포크 시작
  - 100번째 스텝 손실값: 0.1057
  - 200번째 스텝 손실값: 0.4352
  - 300번째 스텝 손실값: 0.3588
  - 400번째 스텝 손실값: 0.1679
  - 에포크 훈련 후 모델 정확도: 0.9583
  - 에포크 훈련에 걸린 시간: 7.6305

3 번째 에포크 시작
  - 100번째 스텝 손실값: 0.2093
  - 200번째 스텝 손실값: 0.1426
  - 300번째 스텝 손실값: 0.1657
  - 400번째 스텝 손실값: 0.0974
  - 에포크 훈련 후 모델 정확도: 0.9656
  - 에포크 훈련에 걸린 시간: 7.4949

4 번째 에포크 시작
  - 100번째 스텝 손실값: 0.1324
  - 200번째 스텝 손실값: 0.1083
  - 300번째 스텝 손실값: 0.1788
  - 400번째 스텝 손실값: 0.1302
  - 에포크 훈련 후 모델 정확도: 0.9717
  - 에포크 훈련에 걸린 시간: 7.7237

5 번째 에포크 시작
  - 100번째 스텝 손실값: 0.4711
  - 200번째 스텝 손실값: 0.3974
  - 300번째 스텝 손실값: 0.1506
  - 400번째 스텝 손실값: 0.3549
  - 에포크 훈련 후 모델 정확도: 0.9743
  - 에포크 훈련에 걸린 시간: 7.7204

6 번째 에포크 시작
  - 100번째 스텝 손실값: 0.1762
  - 200번째 스텝 손실값: 0.0775
  - 300번째 스텝 손실값: 0.1324
  - 400번째 스텝 손실값: 0.0979
  - 에포크 훈련 후 모델 정확도: 0.9772
  - 에포크 훈련에 걸린 시간: 7.6753

7 번째 에포크 시작
  - 100번째 스텝 손실값: 0.1643
  - 200번째 스텝 손실값: 0.2421
  - 300번째 스텝 손실값: 0.1368
  - 400번째 스텝 손실값: 0.3365
  - 에포크 훈련 후 모델 정확도: 0.9789
  - 에포크 훈련에 걸린 시간: 7.7692

8 번째 에포크 시작
  - 100번째 스텝 손실값: 0.1190
  - 200번째 스텝 손실값: 0.0344
  - 300번째 스텝 손실값: 0.0426
  - 400번째 스텝 손실값: 0.1442
  - 에포크 훈련 후 모델 정확도: 0.9804
  - 에포크 훈련에 걸린 시간: 7.6562

9 번째 에포크 시작
  - 100번째 스텝 손실값: 0.3695
  - 200번째 스텝 손실값: 0.3615
  - 300번째 스텝 손실값: 0.0614
  - 400번째 스텝 손실값: 0.0680
  - 에포크 훈련 후 모델 정확도: 0.9831
  - 에포크 훈련에 걸린 시간: 7.7118

4.5. @tf.function 데코레이터#

텐서플로우의 텐서를 다루는 함수에 @tf.function 데코레이터를 추가하면 모델 훈련 속도가 빨라질 수 있다. 훈련 속도의 변화가 발생하는 이유는 여기서는 다루지 않는다. 다만 전적으로 텐서플로우의 텐서 연산만 사용하는 함수에만 적용될 수 있음에 주의한다.

아래 코드는 훈련 스텝을 담당하는 함수를 선언한 다음에 @tf.function 데코레이터를 추가한다.

@tf.function
def train_step(x, y):
    with tf.GradientTape() as tape:
        logits = model(x, training=True)
        loss_value = loss_fn(y, logits)
    grads = tape.gradient(loss_value, model.trainable_weights)
    optimizer.apply_gradients(zip(grads, model.trainable_weights))
    train_acc_metric.update_state(y, logits)
    return loss_value

아래 코드는 위 두 개의 함수를 이용하여 모델 훈련을 훨씬 빠르게 진행한다.

import time

epochs = 10
for epoch in range(epochs):
    print(f"\n{epoch} 번째 에포크 시작")
    # 에포크 시작시간
    start_time = time.time()

    # 훈련 스텝: 배치 단위로 진행되는 훈련 루프
    for step, (x_batch_train, y_batch_train) in enumerate(train_dataset):
        loss_value = train_step(x_batch_train, y_batch_train)

        # 100번째 스텝마타 손실값 출력
        if step % 100 == 0 and step > 0:
            print(f"  - {step}번째 스텝 손실값: {loss_value:.4f}")

    ## 에포크 마무리 단계
    
    # 에포크의 정확도 출력
    train_acc = train_acc_metric.result()
    print(f"  - 에포크 훈련 후 모델 정확도: {train_acc:.4f}")

    # 평가지표 초기화: 에포크 단위로 정확도 계산을 새롭게 진행하기 위해.
    train_acc_metric.reset_state()

    # 에포크 진행에 걸린시간 출력
    print(f"  - 에포크 훈련에 걸린 시간: {time.time() - start_time:.4f}")
0 번째 에포크 시작
  - 100번째 스텝 손실값: 0.0693
  - 200번째 스텝 손실값: 0.2337
  - 300번째 스텝 손실값: 0.0785
  - 400번째 스텝 손실값: 0.0332
  - 에포크 훈련 후 모델 정확도: 0.9845
  - 에포크 훈련에 걸린 시간: 1.4537

1 번째 에포크 시작
  - 100번째 스텝 손실값: 0.0816
  - 200번째 스텝 손실값: 0.1980
  - 300번째 스텝 손실값: 0.0417
  - 400번째 스텝 손실값: 0.3837
  - 에포크 훈련 후 모델 정확도: 0.9844
  - 에포크 훈련에 걸린 시간: 0.9751

2 번째 에포크 시작
  - 100번째 스텝 손실값: 0.2355
  - 200번째 스텝 손실값: 0.0504
  - 300번째 스텝 손실값: 0.0007
  - 400번째 스텝 손실값: 0.4392
  - 에포크 훈련 후 모델 정확도: 0.9865
  - 에포크 훈련에 걸린 시간: 0.9317

3 번째 에포크 시작
  - 100번째 스텝 손실값: 0.2081
  - 200번째 스텝 손실값: 0.0340
  - 300번째 스텝 손실값: 0.2235
  - 400번째 스텝 손실값: 0.4308
  - 에포크 훈련 후 모델 정확도: 0.9873
  - 에포크 훈련에 걸린 시간: 0.9103

4 번째 에포크 시작
  - 100번째 스텝 손실값: 0.3127
  - 200번째 스텝 손실값: 0.0000
  - 300번째 스텝 손실값: 0.0349
  - 400번째 스텝 손실값: 0.1000
  - 에포크 훈련 후 모델 정확도: 0.9873
  - 에포크 훈련에 걸린 시간: 0.9877

5 번째 에포크 시작
  - 100번째 스텝 손실값: 0.0252
  - 200번째 스텝 손실값: 0.3112
  - 300번째 스텝 손실값: 0.0605
  - 400번째 스텝 손실값: 0.0480
  - 에포크 훈련 후 모델 정확도: 0.9888
  - 에포크 훈련에 걸린 시간: 0.9603

6 번째 에포크 시작
  - 100번째 스텝 손실값: 0.2532
  - 200번째 스텝 손실값: 0.2600
  - 300번째 스텝 손실값: 0.2482
  - 400번째 스텝 손실값: 0.1113
  - 에포크 훈련 후 모델 정확도: 0.9900
  - 에포크 훈련에 걸린 시간: 0.9107

7 번째 에포크 시작
  - 100번째 스텝 손실값: 0.0262
  - 200번째 스텝 손실값: 0.1634
  - 300번째 스텝 손실값: 0.2633
  - 400번째 스텝 손실값: 0.0062
  - 에포크 훈련 후 모델 정확도: 0.9900
  - 에포크 훈련에 걸린 시간: 0.9000

8 번째 에포크 시작
  - 100번째 스텝 손실값: 0.1859
  - 200번째 스텝 손실값: 0.1314
  - 300번째 스텝 손실값: 0.0584
  - 400번째 스텝 손실값: 0.1928
  - 에포크 훈련 후 모델 정확도: 0.9907
  - 에포크 훈련에 걸린 시간: 0.9393

9 번째 에포크 시작
  - 100번째 스텝 손실값: 0.0504
  - 200번째 스텝 손실값: 0.4150
  - 300번째 스텝 손실값: 0.1889
  - 400번째 스텝 손실값: 0.0000
  - 에포크 훈련 후 모델 정확도: 0.9905
  - 에포크 훈련에 걸린 시간: 0.9409

주의사항

@tf.function 데코레이터를 추가한다 해서 모델 훈련 속도가 항상 빨라지는 것은 아님에 주의한다. 어느 경우에 빠르고, 언제 그렇지 않은지에 대한 설명은 Better performance with tf.function을 참고한다.

4.6. fit() 메서드 호출과 비교#

텐서플로우 모델의 fit() 메서드는 @tf.function 데코레이터를 적절하게 활용하기에 보다 빠르게 훈련을 진행한다.

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

model.compile(optimizer="rmsprop",
              loss="sparse_categorical_crossentropy",
              metrics=["accuracy"])
start_time = time.time()

model.fit(x_train, y_train, epochs=10, batch_size=128)

# 모델 훈련에 걸린시간 출력
print(f"\n모델 훈련에 걸린 시간: {time.time() - start_time:.4f}")
Epoch 1/10
469/469 ━━━━━━━━━━━━━━━━━━━━ 1s 1ms/step - accuracy: 0.9846 - loss: 0.1155
Epoch 2/10
469/469 ━━━━━━━━━━━━━━━━━━━━ 0s 1ms/step - accuracy: 0.9855 - loss: 0.0980
Epoch 3/10
469/469 ━━━━━━━━━━━━━━━━━━━━ 0s 993us/step - accuracy: 0.9873 - loss: 0.0931
Epoch 4/10
469/469 ━━━━━━━━━━━━━━━━━━━━ 0s 1ms/step - accuracy: 0.9883 - loss: 0.0839
Epoch 5/10
469/469 ━━━━━━━━━━━━━━━━━━━━ 1s 1ms/step - accuracy: 0.9889 - loss: 0.0881
Epoch 6/10
469/469 ━━━━━━━━━━━━━━━━━━━━ 1s 1ms/step - accuracy: 0.9888 - loss: 0.0944
Epoch 7/10
469/469 ━━━━━━━━━━━━━━━━━━━━ 1s 1ms/step - accuracy: 0.9903 - loss: 0.0775
Epoch 8/10
469/469 ━━━━━━━━━━━━━━━━━━━━ 1s 1ms/step - accuracy: 0.9898 - loss: 0.0982
Epoch 9/10
469/469 ━━━━━━━━━━━━━━━━━━━━ 1s 1ms/step - accuracy: 0.9904 - loss: 0.0950
Epoch 10/10
469/469 ━━━━━━━━━━━━━━━━━━━━ 1s 1ms/step - accuracy: 0.9916 - loss: 0.0816

모델 훈련에 걸린 시간: 5.3690

4.7. 연습문제#

  1. (실습) 훈련 루프 상세