DL

[DL] TensorFlow/Keras 모델 재현성 확보 가이드. (feat. 시드 고정)

moonzoo 2025. 5. 9. 10:46

 
안녕하세요! 딥러닝 모델을 학습하다 보면 분명 어제와 똑같은 코드를 돌렸는데 결과가 다르게 나와서 당황스러웠던 경험, 다들 한 번쯤 있으시죠? 특히 중요한 연구를 진행하거나 서비스를 개발할 때, 모델의 학습 결과가 매번 달라진다면 실험 결과를 신뢰하기 어렵고, 문제 발생 시 원인을 찾기도 매우 힘들어집니다.
 
오늘은 이러한 학습 결과의 비일관성 문제를 해결하고, 언제나 동일한 결과를 얻을 수 있도록 TensorFlow/Keras 환경에서 학습 재현성을 확보하는 방법을 알아보겠습니다. 저도 처음에는 단순히 글로벌로 numpy, radom, tf의 seed를 고정해서 사용했는데요. 이것만으로는 학습 결과가 일관되지 않더라구요...ㅠ
 
그럼 왜 학습 결과가 계속 바뀔까요?
딥러닝 모델 학습 과정에는 생각보다 많은 무작위성이 숨어있습니다. 대표적인 원인들은 다음과 같습니다.

  • 가중치 초기화: 신경망의 가중치는 학습 시작 전 무작위 값으로 초기화됩니다.
  • 드롭아웃 (Dropout): 학습 과정에서 과적합을 방지하기 위해 무작위로 뉴런을 비활성화합니다.
  • 데이터 순서: 학습 데이터를 미니배치로 만들 때, 데이터를 섞는(shuffle) 과정에서 순서가 무작위로 결정됩니다.
  • 알고리즘 내부의 무작위성: 특정 연산(특히 GPU 병렬 처리 시)의 내부 알고리즘이 미세한 결과 차이를 유발할 수 있습니다.
  • 외부 라이브러리: Python의 random이나 NumPy 라이브러리에서 생성하는 난수도 영향을 줄 수 있습니다.

이러한 무작위성 요소들을 제어하지 않으면, 코드는 같아도 실행할 때마다 다른 경로로 학습이 진행되어 결국 다른 결과를 내놓게 됩니다.
 
그럼 이제 이러한 무작위성 요소들을 제어할 수 있는 SEED에 대해 단계적으로 알아보겠습니다.
결과를 고정하는 핵심은 바로 이 '무작위성'을 통제하는 것입니다. SEED를 통해 난수 생성 알고리즘에 동일한 시작점을 알려주면 언제나 동일한 순서의 난수를 생성하게 됩니다.
 

1단계: 모든 난수 생성기에 동일한 SEED 심기 (한번만 설정하시면 됩니다.)

가장 먼저, 우리 코드 환경 전반에 걸쳐 사용될 마스터 시드(SEED) 값을 정하고, 이 값을 Python 내장 random, NumPy, 그리고 TensorFlow의 전역 난수 생성기에 알려주어야 합니다.
 

import random
import numpy as np
import tensorflow as tf

# --- 마스터 SEED 값 정의 ---
# 어떤 숫자든 괜찮지만, 실험 내내 동일한 값을 사용해야 합니다.
SEED = 42

# 1. Python 내장 random 모듈 시드 설정
random.seed(SEED)

# 2. NumPy 시드 설정
np.random.seed(SEED)

# 3. TensorFlow 전역 시드 설정
# 이 설정은 TensorFlow 연산 전반에 걸쳐 사용되는 난수 생성에 영향을 줍니다.
# 한 번만 호출해주면 됩니다!
tf.random.set_seed(SEED)

 
SEED 값은 스크립트 최상단에 변수로 정의해두고, 코드 전체에서 이 변수를 일관되게 사용하면 관리하기 편리합니다.
 

2단계: 모델 레이어 구석구석 SEED 심기

전역 시드만으로는 부족할 때가 많습니다. 특히 신경망의 각 레이어는 자체적으로 가중치를 초기화하거나 드롭아웃 같은 무작위 연산을 수행하기 때문에, 이 부분에도 명시적으로 시드를 설정해주는 것이 중요합니다.
핵심은 각 레이어의 initializer 관련 인자와 Dropout 레이어의 seed 인자입니다.
다음은 시드를 적용한 예시입니다.
 

from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Embedding, SimpleRNN, Dropout, Dense
from tensorflow.keras.initializers import GlorotUniform, Orthogonal

model_focal = Sequential([
    # Embedding 레이어: embeddings_initializer에 시드 설정
    Embedding(input_dim=actual_char_vocab_size,
              output_dim=embedding_dim,
              input_length=char_max_len,
              embeddings_initializer=GlorotUniform(seed=SEED)), # 시드 적용.

    # SimpleRNN 레이어: kernel_initializer, recurrent_initializer에 시드 설정
    # SimpleRNN 자체의 seed 인자는 내부 dropout 등에 사용될 수 있습니다.
    SimpleRNN(hidden_units,
              return_sequences=False,
              kernel_initializer=GlorotUniform(seed=SEED),        # 시드 적용
              recurrent_initializer=Orthogonal(seed=SEED)),      # 시드 적용
              # 참고: SimpleRNN의 'seed' 인자는 레이어 내 드롭아웃 같은 일부 연산에 대한 시드입니다.
              # 명시적으로 recurrent_dropout을 사용하지 않아도, 안전하게 설정해두면 좋습니다.
              # seed=SEED), # 만약 SimpleRNN의 seed 인자를 사용한다면 이렇게
              # 하지만 아래 Dropout 레이어에서 명시적으로 관리하므로 없어도 괜찮을 수 있습니다.

    # Dropout 레이어: 생성자 인자로 seed 설정
    Dropout(0.5, seed=SEED),                                      # 시드 적용

    # Dense 레이어: kernel_initializer에 시드 설정
    Dense(hidden_units // 2,
          activation='relu',
          kernel_initializer=GlorotUniform(seed=SEED)),       # 시드 적용

    Dropout(0.3, seed=SEED),                                      # 시드 적용

    Dense(num_classes,
          activation='softmax',
          kernel_initializer=GlorotUniform(seed=SEED))        # 시드 적용
])

model_focal.summary()

주요 레이어별 시드 설정 포인트:

  • Embedding 레이어: embeddings_initializer 인자에 tf.keras.initializers.GlorotUniform(seed=SEED)와 같이 시드가 포함된 초기화 객체를 전달합니다.
  • Dense (완전 연결) 레이어: kernel_initializer 인자에 동일한 방식으로 시드가 포함된 초기화 객체를 전달합니다.
  • RNN 계열 레이어 (SimpleRNN, LSTM, GRU 등):
    • kernel_initializer: 입력에 대한 가중치 초기화 시드
    • recurrent_initializer: 순환 상태에 대한 가중치 초기화 시드 (이 부분을 놓치기 쉬운데, 매우 중요합니다!)
      • RNN은 과거의 기억을 다음 시점으로 넘겨줄 때, 특별한 가중치 행렬을 사용합니다. 이 가중치가 바로 recurrent weight이고, 이 순환 가중치를 처음에 어떻게 설정할지를 결정하는 것이 recurrent_initializer 입니다.
      • 이 과거의 기억을 처리하는 규칙이 매번 달라지면 학습의 시작점이 달라지기 때문에 고정 해주셔야 합니다.
  • Dropout 레이어: Dropout(rate, seed=SEED)와 같이 생성자의 seed 인자를 사용합니다.

3단계: 데이터 파이프라인 SEED 고정

모델과 환경에 시드를 잘 심었더라도, 학습에 사용되는 데이터의 순서나 구성이 매번 달라진다면 결과는 여전히 흔들릴 수 있습니다.

  • 데이터 로드 순서: 파일 시스템에서 데이터를 읽어올 때, 파일 목록의 순서가 고정적인지 확인하세요. 필요하다면 파일명을 기준으로 정렬하는 등의 처리가 필요할 수 있습니다.
  • 데이터 분할 (train_test_split 등): sklearn.model_selection.train_test_split 같은 함수를 사용한다면, random_state=SEED 인자를 반드시 설정해주세요.
  • tf.data.Dataset 사용 시:
    • dataset.shuffle(buffer_size, seed=SEED, reshuffle_each_iteration=False): shuffle 연산에 seed를 설정하고, 에포크마다 다시 섞지 않도록 reshuffle_each_iteration=False로 지정하는 것이 재현성에 유리합니다. (True로 하면 에포크마다 다른 순서로 섞이지만, 시드 기반으로 섞이긴 합니다. 완전한 고정을 원한다면 False로!)
    • dataset.map(...): map 함수 내에서 데이터 증강(augmentation)과 같이 무작위 연산을 수행한다면, 해당 연산에도 시드를 적용하거나 tf.random.stateless_uniform과 같이 상태 없는(stateless) 난수 생성 함수를 사용하는 것을 고려해야 합니다.

Tensorflow/Keras 모델 재현성 확보 완료!

위의 단계들을 꼼꼼히 적용하고 코드를 실행해보시면, 이전과는 달리 매번 동일한 학습 손실(loss)과 정확도(accuracy)를 확인하실 수 있을 겁니다. 
 
딥러닝 모델 학습에서 재현성을 확보하는 것은 안정적이고 신뢰할 수 있는 연구와 개발의 첫걸음입니다. 오늘 함께 알아본 시드 고정 방법들이 연구에 조금이라도 도움이 되셨으면 합니다. 물론, 아주 복잡한 모델이나 분산 학습 환경 등에서는 여전히 미세한 차이가 발생할 수도 있습니다. 하지만 대부분의 일반적인 상황에서는 만족스러운 수준의 재현성을 얻으실 수 있을 것 같습니다!