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
의
자식 클래스들을 보여준다.
여기서는 배치 크기가 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