keras-team에서 tf.keras로 통일하는 방향을 사실상 확정한 분위기. 2.2.x 이후 multi-backend keras는 maintenance 모드로 들어가고, 앞으로는 TensorFlow 2.0에 포함되는 tf.keras에 힘을 싣겠다는 게 Chollet 본인 발표. 지난주 실무 마이그하면서 느낀 점 정리.
개인적으로 multi-backend 시절의 자유도가 아쉽긴 하다. 근데 실질적으로 Theano는 2017년에 개발 종료, CNTK도 최근 커밋이 끊겼고 PlaidML 정도가 남았는데 그것도 작은 커뮤니티. 결국 TF와 PyTorch 양강 구도에서 keras는 TF 위로 올라가는 게 자연스러운 흐름.
마이그레이션 — 실제로 달라지는 것
# before — multi-backend keras 2.2.4
from keras.models import Sequential
from keras.layers import Dense, Conv2D
from keras.callbacks import EarlyStopping
from keras import backend as K
# after — tf.keras (TF 1.13)
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense, Conv2D
from tensorflow.keras.callbacks import EarlyStopping
import tensorflow as tf
# backend 호출은 tf.keras.backend 또는 직접 tf 사용
대부분의 고수준 API(Model, Layer, callbacks, losses, metrics)는 시그니처가 거의 동일해서 import 경로만 바꾸면 돌아간다. 걸리는 건 다음 몇 가지.
- 옵티마이저 객체. TF 1.x는
tf.train.AdamOptimizer와 keras.optimizers.Adam이 따로 존재. tf.keras 안에서는 원래 multi-backend keras의 옵티마이저와 유사하지만 내부 wrapper가 다름. 저장된 모델의 옵티마이저 상태를 다른 구현으로 로드하면 학습 이어붙일 때 loss가 튄다. 한 프로젝트 안에서 반드시 하나로 통일
- 커스텀 레이어 내부의 backend.
K.function, K.gradients, K.learning_phase 같은 함수 호출은 tf.keras에서 대응 API가 미묘하게 다르거나, TF 2.0에서는 eager/graph 모드 구분 때문에 재설계 필요
- Sequence, DataGenerator. 대체로 호환인데 multiprocessing 워커 수, worker_init_fn 처리가 TF 기반으로 통일되며 살짝 바뀜.
use_multiprocessing=True는 OS 플랫폼에 따라 여전히 까다로움
- Learning phase 자동 전환. multi-backend keras는 BatchNorm/Dropout이 학습/추론 모드를 전역 플래그로 관리. tf.keras + eager에서는 모델 호출 시
training=True/False 인자로 명시적. 커스텀 레이어 call(self, x, training=None)에서 training을 받아서 내부 BN에 넘기는 패턴을 꼭 써야 함
TF 2.0 alpha
며칠 전에 2.0 alpha 공개됨. 가장 큰 변화는 eager execution이 기본이라는 점. 1.x 시대의 "그래프 먼저 그리고 session 돌리기"가 사라지고, 파이썬 코드 그대로 실행되는 정의-실행 동시 모델. PyTorch에서 건너오는 사람들에게 자연스러운 방식.
그리고 tf.function 데코레이터. 성능이 필요한 구간만 그래프 컴파일. 내부적으로 AutoGraph가 파이썬 제어문(for, while, if)을 그래프 오퍼레이션으로 변환. 이 조합으로 "디버깅은 PyTorch처럼, 성능은 TF 1.x처럼"을 노린다.
import tensorflow as tf
@tf.function
def train_step(x, y, model, loss_fn, optimizer):
with tf.GradientTape() as tape:
logits = model(x, training=True)
loss = loss_fn(y, logits)
grads = tape.gradient(loss, model.trainable_variables)
optimizer.apply_gradients(zip(grads, model.trainable_variables))
return loss
loop을 이렇게 직접 쓰는 게 어색할 수도 있는데, 실전에서 loss에 auxiliary term 붙이거나 gradient clipping/accumulation을 커스텀하기 훨씬 쉽다. model.fit의 편의성이 필요한 건 그대로 쓰고, 실험적인 학습 로직은 custom loop. 하이브리드 구성.
할 일
- 현재 서비스 3개 모델 import 경로 → tf.keras
- 옵티마이저와 체크포인트 포맷 통일(SavedModel로)
- 이번 분기 중 TF 2.0 beta 나오면 소규모 모델부터 2.0 마이그
- 커스텀 레이어의 backend 호출 제거,
tf.function 적용 구간 선별
아쉬운 점: Theano 시절 theano.gradient.jacobian 같은 걸 잘 썼는데 tf.keras + TF는 JAX 나오기 전까지 고차 미분 쪽 편의성이 떨어짐. JAX가 안정화되면 그쪽으로 연구용은 옮길지도. 운영용은 당분간 tf.keras로 정착.