딥러닝
Ch7
나몬
2024. 5. 12. 23:08
7.1 다양한 워크플로
- 케라스 API 설계는 복잡성의 단계적 공개(progressive disclosure) 원칙을 따름
- 시작은 쉽게 하고, 필요할 때 단계마다 점진적으로 학습하여 아주 복잡한 경우를 처리할 수 있음
- 간단한 문제는 누구나 쉽게 처리할 수 있어야 하지만 어떤 고급 워크플로도 가능해야 함
- 원하는 작업이 얼마나 복잡하고 드문 경우인지에 상관없이 이를 달성하기 위한 명백한 방법이 있어야 함
- 이런 방법은 간단한 워크플로에서 배운 것을 기반으로 함
- 이는 초보자에서 전문가로 성장하면서 동일한 도구를 그대로 사용할 수 있다는 의미
- 다만 사용 방법만 달라질 뿐
- 케라스를 사용하는 '올바른' 한 가지 방법이 있는 것이 아님
- 케라스는 매우 간단한 것부터 매우 유연한 것까지 다양한 워크플로를 제공
- 케라스 모델을 만드는 방법은 여러 가지고, 모델을 훈련하는 방법도 여러 가지
- 이를 통해 다양한 요구 사항을 충족시킬 수 있음
- 이런 모든 워크플로는 동일하게 Layer와 Model 같은 API를 기반으로 하기 때문에 한 워크플로의 구성 요소를 다른 워크플로에서 사용할 수 있음
- 즉, 워크플로 간에 서로 호출될 수 있음
7.2 케라스 모델을 만드는 여러 방법
- 케라스에서 모델을 만드는 API는 세 가지
- Sequential 모델이 가장 시작하기 쉬운 API
- 기본적으로 하나의 파이썬 리스트
- 단순히 층을 쌓을 수만 있음
- 함수형 API(Functional API)는 그래프 같은 모델 구조를 주로 다룸
- 이 API는 사용성과 유연성 사이의 적절한 중간 지점에 해당
- 가장 널리 사용되는 모델 구축 API
- Model 서브클래싱(subclassing)은 모든 것을 밑바닥부터 직접 만들 수 있는 저수준 방법
- 모든 상세한 내용을 완전히 제어하고 싶은 경우에 적합
- 여러 가지 케라스 내장 기능을 사용하지 못하기 때문에 실수가 발생할 위험이 많음
- Sequential 모델이 가장 시작하기 쉬운 API
모델 구출 복잡성의 단계적 공개
Sequential 모델
- Sequential 클래스
from tensorflow import keras
from tensorflow.keras import layers
model = keras.Sequential([
layers.Dense(64, activation="relu"),
layers.Dense(10, activation="softmax")
])
- 동일한 모델을 add() 메서드를 통해 점진적으로 만들 수 있음
- 이 메서드는 파이썬 리스트의 append() 메서드와 비슷함
- 점진적으로 Sequential 모델 만들기
model = keras.Sequential()
model.add(layers.Dense(64, activation="relu"))
model.add(layers.Dense(10, activation="softmax"))
- 층은 처음 호출될 때 만들어진다는 것을 보았음(즉, 가중치를 만듦)
- 층의 가중치 크기가 입력 크기에 따라 달라지기 때문임
- 즉, 입력 크기를 알기 전까지 가중치를 만들 수 없음
- 앞의 Sequential 모델은 어떤 가중치도 가지고 있지 않음
- 가중치를 생성하려면 어떤 데이터로 호출하거나 입력 크기를 지정하여 build() 메서드를 호출
- build() 메서드가 호출 전의 모델은 가중치가 없다
model.weights # 이때는 아직 모델의 build()메서드가 호출되지 않았다.
- 가중치를 만들기 위해 모델을 호출
model.build(input_shape=(None, 3)) # 모델의 build() 메서드 호출: 이제 모델의 크기가 (3,)인 샘플을 기대한다. 입력 크기의 None은 어떤 배치 크기도 가능하다는 의미
model.weights
- build() 메서드가 호출된 후 디버깅에 유용한 summary() 메서드를 사용하여 모델 구조를 출력할 수 있음
- summary() 메소드
model.summary()
summary() 메서드
- 여기에서 보듯이 이 모델의 이름은 'sequential_1'
- 케라스에서는 모델과 층을 포함해서 모든 것에 이름을 지정할 수 있음
- name 매개변수로 모델과 층에 이름 지정하기
model = keras.Sequential(name="my_example_model")
model.add(layers.Dense(64, activation="relu", name="my_first_layer"))
model.add(layers.Dense(10, activation="softmax", name="my_last_layer"))
model.build((None, 3))
model.summary()
name 매개변수로 모델과 층에 이름 지정하기
- Sequential 모델을 점진적으로 만들 때 층을 추가하고 난 후 summary()메서드를 호출하여 현재 모델을 확인할 수 있으면 유용
- 모델의 build() 메서드를 호출하기 전까지는 summary() 메서드를 호출할 수 없음!
- Sequential 모델의 가중치를 바로 생성하는 방법이 있음
- 모델의 입력 크기를 미리 지정하면 됨
- 이를 위해 Input 클래스를 사용
- 모델의 입력 크기를 미리 지정하기
model = keras.Sequential()
model.add(keras.Input(shape=(3,)))
model.add(layers.Dense(64, activation="relu"))
- 이제 summary() 메서드를 사용하여 층을 추가함에 따라 모델의 출력 크기 변화를 확인할 수 있음
함수형 API
- Sequential 모델은 사용하기 쉽지만 적용할 수 있는 곳이 극히 제한적
- 하나의 입력과 하나의 출력을 가지며 순서대로 층을 쌓은 모델만 표현할 수 있음
- 실제로 다중 입력(예를 들어 이미지와 이미지의 메타데이터), 다중 출력(예를 들어 데이터에 대해 여러 가지 항목을 예측하는 모델) 또는 비선형적인 구조를 가진 모델을 자주 만날 수 있음
- 이런 경우에는 함수형 API를 사용한 모델을 만듦
- 실전에서 이런 케라스 모델을 가장 흔하게 만날 수 있음
- 이 방식은 레고 블록을 가지고 노는 것처럼 재미있고 강력함
간단한 예제
- 이전 절에서 했던 것처럼 2개의 층을 쌓아 보자
- 이 모델을 함수형 API 버전으로 만들면 다음과 같음
- 2개의 Dense 층을 가진 간단한 함수형 모델
inputs = keras.Input(shape=(3,), name="my_input")
features = layers.Dense(64, activation="relu")(inputs)
outputs = layers.Dense(10, activation="softmax")(features)
model = keras.Model(inputs=inputs, outputs=outputs)
- 단계별로 살펴보면 다음과 같음
- Input 클래스 객체를 정의하는 것으로 시작(다른 것과 마찬가지로 입력에도 이름을 지정할 수 있음)
inputs = keras.Input(shape=(3,), name="my_input")
- inputs 객체는 모델이 처리할 데이터의 크기와 dtype에 대한 정보를 가지고 있음
- 이런 객체를 심볼릭 텐서(symbolic tensor)라고 부름
- 실제 데이터를 가지고 있지 않지만 사용할 때 모델이 보게 될 데이터 텐서의 사양이 인코딩되어 있음
- 즉, 미래의 데이터 텐서를 나타냄
- 그 다음 층을 만들고 전의 입력으로 호출
features = layers.Dense(64, activation="relu")(inputs)
- 모든 케라스 층은 실제 데이터 텐서나 심볼릭 텐서로 호출할 수 있음
- 후자의 경우 크기와 dtype 정보가 업데이트된 새로운 심볼릭 텐서를 반환
- 최종 출력을 얻은 후 입력과 출력을 Model 클래스에 전달하여 모델 객체를 생성
outputs = layers.Dense(10, activation="softmax")(features)
model = keras.Model(inputs=inputs, outputs=outputs)
- 다음은 이 모델의 summary() 메서드 호출 결과
다중 입력, 다중 출력 모델
- 간단한 모델과 달리 배부분 딥러닝 모델은 리스트와 같은 형태가 아니라 그래프를 닮았음
- 예를 들어 입력이 여러 개이거나 출력이 여러 개
- 이런 종류의 모델에서 함수형 API가 진짜 빛을 발함
- 고객 이슈 티켓에 우선순위를 지정하고 적절한 부서로 전달하는 시스템을 만든다고 해 보자
- 이 모델은 3개의 입력을 사용
- 이슈 티켓의 제목(텍스트 입력)
- 이슈 티켓의 텍스트 본문(텍스트 입력)
- 사용자가 추가한 태그(범주형 입력으로 여기에서는 원-핫 인코딩되었다고 가정)
- 텍스트 입력을 크기가 vocabulary_size인 0과 1로 이루어진 배열로 인코딩할 수 있음
- 이 모델은 출력도 2개
- 이슈 티켓의 우선순위 점수로 0과 1 사이의 스칼라(시그모이드 출력)
- 이슈 티켓을 처리해야 할 부서(전체 부서 집합에 대한 소프트맥스 출력)
- 다중 입력, 다중 출력 함수형 모델
vocabulary_size = 10000
num_tags = 100
num_departments = 4
# 모델의 입력을 정의한다.
title = keras.Input(shape=(vocabulary_size,), name="title")
text_body = keras.Input(shape=(vocabulary_size,), name="text_body")
tags = keras.Input(shape=(num_tags,), name="tags")
# 입력 특성을 하나의 텐서 features로 연결한다.
features = layers.Concatenate()([title, text_body, tags])
# 중간층을 적용하여 입력 특성을 더 풍부한 표현을 재결합시킨다.
features = layers.Dense(64, activation="relu")(features)
# 모델의 출력을 정의한다.
priority = layers.Dense(1, activation="sigmoid", name="priority")(features)
department = layers.Dense(
num_departments, activation="softmax", name="department")(features)
# 입력과 출력을 지정하여 모델을 만든다.
model = keras.Model(inputs=[title, text_body, tags], outputs=[priority, department])
- 함수형 API는 간단하고 레고 블록 같지만 층으로 구성된 어떤 그래프도 정의할 수 있는 매우 유연한 방법
다중 입력, 다중 출력 모델 훈련하기
- Sequential 모델을 훈련하는 것과 거의 같은 방법으로 이 모델을 훈련할 수 있음
- 입력과 출력 데이터의 리스트로 fit() 메서드를 호출하면 됨
- 데이터의 리스트는 Model 클래스에 전달한 순서와 같아야 함
- 입력과 타깃 배열 리스트를 전달하여 모델 훈련하기
import numpy as np
num_samples = 1280
# 더미(dummy) 입력 데이터
title_data = np.random.randint(0, 2, size=(num_samples, vocabulary_size))
text_body_data = np.random.randint(0, 2, size=(num_samples, vocabulary_size))
tags_data = np.random.randint(0, 2, size=(num_samples, num_tags))
# 더미 타깃 데이터
priority_data = np.random.random(size=(num_samples, 1))
department_data = np.random.randint(0, 2, size=(num_samples, num_departments))
model.compile(optimizer="rmsprop",
loss=["mean_squared_error", "categorical_crossentropy"],
metrics=[["mean_absolute_error"], ["accuracy"]])
model.fit([title_data, text_body_data, tags_data],
[priority_data, department_data],
epochs=1)
model.evaluate([title_data, text_body_data, tags_data],
[priority_data, department_data])
priority_preds, department_preds = model.predict([title_data, text_body_data, tags_data])
- 입력과 타깃 배열을 딕셔너리로 전달하여 모델 훈련하기
model.compile(optimizer="rmsprop",
loss={"priority": "mean_squared_error", "department": "categorical_crossentropy"},
metrics={"priority": ["mean_absolute_error"], "department": ["accuracy"]})
model.fit({"title": title_data, "text_body": text_body_data, "tags": tags_data},
{"priority": priority_data, "department": department_data},
epochs=1)
model.evaluate({"title": title_data, "text_body": text_body_data, "tags": tags_data},
{"priority": priority_data, "department": department_data})
priority_preds, department_preds = model.predict(
{"title": title_data, "text_body": text_body_data, "tags": tags_data})
함수형 API의 장점: 층 연결 구조 활용하기
- 함수형 모델은 명시적인 그래프 데이터 구조
- 층이 어떻게 연결되어 있는지 조사하고 이전 그래프 노드(node)(층의 출력)를 새 모델의 일부로 재사용할 수 있음
- 대부분의 연구자들이 심층 신경망에 대해 생각할 때 사용하는 '멘탈 모델(mental model)'인 층 그래프(graph of layers)에도 잘 맞음
- 이를 통해 모델 시각화와 특성 추출이라는 두 가지 중요한 기능이 가능
- 방금 정의한 모델의 연결 구조(모델의 토폴로지(topology))를 시각화해 보자
- plot_model() 함수를 사용하여 함수형 모델을 그래프로 그릴 수 있음
keras.utils.olot_model(model, "ticket_classifier.png")
plot_model()로 생성한 이슈 티켓 분류 모델 그림
- 모델에 있는 각 층의 입출력 크기를 추가하면 디버깅에 도움이 될 수 있다.
keras.utils.plot_model(
model, "ticket_classifier_with_shape_info.png", show_shapes=True)
크기 정보가 추가된 모델 그림
- 텐서 크기에 None은 배치 크기를 나타냄
- 즉, 이 모델은 어떤 크기의 배치에서도 사용 가능
- 층 연결 구조를 참조하여 그래프에 있는 개별 노드를 조사하고 재사용(층 호출)할 수 있음
- model.layers 속성은 모델에 있는 모든 층의 리스트를 가지고 있음
- 각 층에 대해 layer.input과 layer.output을 출력해 볼 수 있음
- 이를 통해 특성 추출(feature extraction)을 수행하여 다른 모델에서 중간 특성을 재사용하는 모델을 만들 수 있음
- 이전 모델에 또 다른 출력을 추가한다고 가정해 보자
- 이슈 티켓이 해결되는 데 걸리는 시간, 즉 일종의 난이도를 추정하려고 함
- 이를 위해 'quick', 'medium', 'difficult' 3개의 범주에 대한 분류 층을 추가
- 모델을 처음부터 다시 만들고 재훈련할 필요가 없음
- 다음 코드와 같이 중간층을 참조할 수 있기 때문에 이전 모델의 중간 특성에서 수 있음
features = model.layers[4].output
difficulty = layers.Dense(3, activation="softmax", name="difficulty")(features)
new_model = keras.Model(
inputs=[title, text_body, tags],
outputs=[priority, department, difficulty])
새로운 모델의 그래프
Model 서브클래싱
- 마지막으로 알아야 할 모델 구축 패턴은 가장 고급 방법인 Model 서브클래싱
- Model 클래스를 상속하는 것도 매우 비슷함
- __init__() 메서드에서 모델이 사용할 층을 정의
- call() 메서드에서 앞서 만든 층을 사용하여 모델의 정방향 패스를 정의
- 서브클래스의 객체를 만들고 데이터와 함께 호출하여 가중치를 만듦
이전 예제를 서브클래싱 모델로 다시 만들기
- Model 클래스를 상속하여 고객 이슈 티켓 관리 모델을 다시 구현해 보자
class CustomerTicketModel(keras.Model):
def __init__(self, num_departments):
super().__init__() # 꼭 부모 클래스의 생성자를 호출해야 한다.
# 생성자에서 층을 정의
self.concat_layer = layers.Concatenate()
self.mixing_layer = layers.Dense(64, activation="relu")
self.priority_scorer = layers.Dense(1, activation="sigmoid")
self.department_classifier = layers.Dense(
num_departments, activation="softmax")
def call(self, inputs): # call() 메서드에서 정방향 패스를 정의
title = inputs["title"]
text_body = inputs["text_body"]
tags = inputs["tags"]
features = self.concat_layer([title, text_body, tags])
features = self.mixing_layer(features)
priority = self.priority_scorer(features)
department = self.department_classifier(features)
return priority, department
- 모델을 정의하고 나면 이 클래스의 객체를 만들 수 있음
- Layer 클래스와 마찬가지로 어떤 데이터로 처음 호출할 때 가중치를 만듦
model = CustomerTicketModel(num_departments=4)
priority, department = model(
{"title": title_data, "text_body": text_body_data, "tags": tags_data})
- 지금까지는 모든 것이 이미 보았던 Layer 클래스 상속과 매우 비슷함
- Layer클래스 상속과 Model 클래스 상속의 차이점은 다음과 같다.
- '층'은 모델을 만드는 데 사용하는 구성 요소고 '모델'은 실제로 훈련하고 추론에 사용하는 최상위 객체
- 간단히 말해서 Model클래스는 fit(), evaluate(), predict() 메서드를 가지고 있음
- Layer 클래스에는 이런 메서드가 없음
- 그 외에는 두 클래스가 거의 동일
- Sequential이나 함수형 모델과 마찬가지로 Model을 상속하여 만든 모델을 컴파일하고 훈련할 수 있음
model.compile(optimizer="rmsprop",
# 손실과 측정 지표로 전달하는 값은 call() 메서드가 반환하는 것과 정확히 일치해야 한다.(여기서는 2개의 원소를 가진 리스트)
loss=["mean_squared_error", "categorical_crossentropy"],
metrics=[["mean_absolute_error"], ["accuracy"]])
# 입력 데이터의 구조는 call() 메서드가 기대하는 것과 정확히 일치해야 한다.(여기에서는 title, text_body, tags 키를 가진 딕셔너리)
model.fit({"title": title_data,
"text_body": text_body_data,
"tags": tags_data},
# 타깃 데이터의 구조는 call() 메서드가 반환하는 것과 정확히 일치해야 한다. (여기에서는 2개의 원소를 가진 리스트)
[priority_data, department_data],
epochs=1)
model.evaluate({"title": title_data,
"text_body": text_body_data,
"tags": tags_data},
[priority_data, department_data])
priority_preds, department_preds = model.predict({"title": title_data,
"text_body": text_body_data,
"tags": tags_data})
- Model 서브클래싱 워크플로는 모델을 만드는 가장 유연한 방법 (사용자가 원하는대로 정의 가능)
- 층의 유향 비순환 그래프(directed ascyclic graph)(순환이 없는)로 표현할 수 없는 모델을 만들 수 있음
- 예를 들어 call() 메서드가 for루프 안에서 층을 사용하거나 재귀적으로 호출하는 모델
- 모든 것이 가능
- 다만 책임은 작성자에게 있음
주의: 서브클래싱된 모델이 지원하지 않는 것
- 이런 자유에는 대가가 따름
- 서브클래싱 모델에서는 모델 로직을 많이 책임져야 하며 잠재적인 오류 가능성이 훨씬 큼
- 결과적으로 더 많은 디버깅 작업을 해야 함
- 레고 블록을 맞추는 것이 아니라 새로운 파이썬 객체를 개발하고 있기 때문임
- 함수형과 서브클래싱 모델은 태생적으로 크게 다름
- 함수형 모델은 명시적인 데이터 구조인 층의 그래프이므로 출력하고 조사하고 수정할 수 있음
- 서브클래싱 모델은 한 덩어리의 바이트코드(bytecode)
- 원시 코드가 담긴 call() 메서드를 가진 파이썬 클래스
- 이것이 (원하는 어떤 기능도 코드로 작성할 수 있어) 서브클래싱 워크플로의 유연성의 원천이지만 새로운 제약 사항이 발생
- 예를 들어 층이 서로 연결되는 방식이 call() 메서드 안에 감추어지기 때문에 이 정보를 활용할 수 없음
- summary() 메서드가 층의 연결 구조를 출력할 수 없고 plot_model()함수로 모델의 구조를 그래프로 그릴 수 없음
- 비슷하게 서브클래싱 모델은 그래프가 없기 때문에 특성 추출을 위해 층 그래프의 노드를 참조할 수 없음
- 이 모델의 객체를 생성하고 나면 정방향 패스는 완전히 블랙박스(진행상황을 볼 수 없는)가 됨
여러 방식을 혼합하여 사용하기
- 중요한 것은 Sequential 모델, 함수형 API, Model 서브클래싱 패턴 중 하나를 선택한다고 다른 패턴의 사용을 제한하지 않는다는 점
- 케라스 API로 만든 모델을 Sequential 모델, 함수형 모델 또는 밑바닥부터 만든 서브클래싱 모델인지에 상관없이 부드럽게 서로 상호 운영할 수 있음
- 각각의 방법은 모두 동일한 워크플로 스펙트럼(범주)의 일부분
- 서브클래싱한 모델을 포함하는 함수형 모델 만들기
class Classifier(keras.Model):
def __init__(self, num_classes=2):
super().__init__()
if num_classes == 2:
num_units = 1
activation = "sigmoid"
else:
num_units = num_classes
activation = "softmax"
self.dense = layers.Dense(num_units, activation=activation)
def call(self, inputs):
return self.dense(inputs)
inputs = keras.Input(shape=(3,))
features = layers.Dense(64, activation="relu")(inputs)
outputs = Classifier(num_classes=10)(features)
model = keras.Model(inputs=inputs, outputs=outputs)
- 함수형 모델을 포함하는 서브클래싱 모델 만들기
inputs = keras.Input(shape=(64,))
outputs = layers.Dense(1, activation="sigmoid")(inputs)
binary_classifier = keras.Model(inputs=inputs, outputs=outputs)
class MyModel(keras.Model):
def __init__(self, num_classes=2):
super().__init__()
self.dense = layers.Dense(64, activation="relu")
self.classifier = binary_classifier
def call(self, inputs):
features = self.dense(inputs)
return self.classifier(features)
model = MyModel()
작업에 적합한 도구 사용하기
- 캐라스 모델 구축을 위해 가장 단순한 워크플로인 Sequential 모델부터 가장 고급인 Model 서브클래싱까지 다양한 워크플로를 학습함
- 각 방법은 장단점이 있으므로 현재 작업에 가장 잘 맞는 것을 선택
- 일반적으로 함수형 API가 쉬운 사용성과 유연성 사이에 적절한 절충점
- 층 연결 구조를 활용하여 모델 출력이나 특성 추출과 같은 용도에 잘 맞음
- 함수형 API를 사용할 수 있다면, 즉 모델을 층의 유향 비순환 그래프로 표현할 수 있다면 Model 서브클래싱보다 이 방식을 사용할 것을 권장
- 일반적으로 서브클래싱 층을 포함한 함수형 모델을 사용하면 함수현 API의 장점을 유지하면서 높은 개발 유연성을 제공할 수 있음
7.3 내장된 훈련 루프와 평가 루프 사용하기
- 아주 쉬운 것부터 매우 유연한 것까지 한 번에 한 단계씩 전체 워크플로에 접근할 수 있는 복잡성의 단계적 공개 원칙은 모델 훈련에도 적용
- 케라스는 모델 훈련을 위해 다양한 워크플로를 제공
- 간단하게 데이터로 fit() 메서드를 호출하거나 밑바닥부터 새로운 훈련 알고리즘을 작성할 수 있는 고급 방법도 제공
- 이미 compile(), fit(), evaluate(), predict() 워크플로를 잘 알고 있음
- 표준 워크플로: compile(), fit(), evaluate(), predict()
from tensorflow.keras.datasets import mnist
def get_mnist_model(): # 모델은 만든다. (나중에 재사용할 수 있도록 별도의 함수로 만든다.)
inputs = keras.Input(shape=(28 * 28,))
features = layers.Dense(512, activation="relu")(inputs)
features = layers.Dropout(0.5)(features)
outputs = layers.Dense(10, activation="softmax")(features)
model = keras.Model(inputs, outputs)
return model
(images, labels), (test_images, test_labels) = mnist.load_data() # 데이터를 로드하고 검증을 위해 일부를 떼어 놓는다.
images = images.reshape((60000, 28 * 28)).astype("float32") / 255
test_images = test_images.reshape((10000, 28 * 28)).astype("float32") / 255
train_images, val_images = images[10000:], images[:10000]
train_labels, val_labels = labels[10000:], labels[:10000]
model = get_mnist_model()
# 옵티마이저, 최소화할 손실 함수, 모니터링할 지표를 지정하여 모델을 컴파일
model.compile(optimizer="rmsprop",
loss="sparse_categorical_crossentropy",
metrics=["accuracy"])
# fit() 메서드를 사용해서 모델을 훈련. 본 적 없는 데이터에 대한 성능을 모니터링하기 위해 검증 데이터를 함께 제공
model.fit(train_images, train_labels,
epochs=3,
validation_data=(val_images, val_labels))
# evaluate() 메서드를 사용해서 새로운 데이터에 대한 손실과 측정 지표를 계산
test_metrics = model.evaluate(test_images, test_labels)
# predict() 메서드를 사용하여 새로운 데이터에 대한 분류 확률을 계산
predictions = model.predict(test_images)
- 이 간단한 워크플로를 커스터마이징할 수 있는 몇 가지 방법이 있음
- 사용자 정의 측정 지표를 전달
- fit() 메서드에 콜백(callback)을 전달하여 훈련하는 동안 특정 시점에 수행될 행동을 예약
사용자 정의 지표 만들기
- 지표(metric)는 모델의 성능을 측정하는 열쇠
- 특히 훈련 데이터 성능과 테스트 데이터 성능 차이를 측정하는 것이 중요(과대적합과 과소적합의 절충점인 최적접합을 찾기 위해)
- 분류와 회귀에 일반적으로 사용되는 지표는 keras.metrics 모듈에 이미 포함되어 있음
- 대부분의 경우 여기에 포함된 지표를 사용할 것
- 일반적이지 않은 작업을 한다면 사용자 정의 지표를 만들 수 있어야 함
- 케라스 지표는 keras.metrics.Metric 클래스에 상속한 클래스
- 층과 마찬가지로 지표는 텐서플로 변수에 내부 상태를 저장
- 층과 다른 점은 이 변수가 역전파로 업데이트되지 않는다는 것
- 상태 업데이트 로직은 update_state() 메서드 안에 직접 작성
- Metric 클래스를 상속하여 사용자 정의 지표 구현하기
# tensorflow 라이버르리 import
import tensorflow as tf
# Metric 클래스를 상속한다. RootMeanSquareError라는 클래스를 정의한다. 모델의 예측으로부터 rmse지표를 계산하는 사용자 정의 지표 클래스이다.
class RootMeanSquaredError(keras.metrics.Metric):
# 생성자에서 상태 변수를 정의한다. 층과 마찬가지로 add_weight() 메서드를 사용한다.
def __init__(self, name="rmse", **kwargs):
# 부모 생성자를 호출해 기본 생성자를 초기화한다.
super().__init__(name=name, **kwargs)
# 이 줄은 add_weight() 메서드를 사용하여 상태 변수 mse_sum을 생성합니다. 모델 평가 중에 평균 제곱 오차의 누적 합계를 저장합니다.
self.mse_sum = self.add_weight(name="mse_sum", initializer="zeros")
# 이 줄은 add_weight()를 사용하여 또 다른 상태 변수 total_samples를 생성합니다. 모델 평가 중에 처리된 총 샘플 수를 저장합니다.
self.total_samples = self.add_weight(
name="total_samples", initializer="zeros", dtype="int32")
# 이 메서드는 각 데이터 배치에 대한 실제 레이블(y_true) 및 모델 예측(y_pred)을 기반으로 상태 변수(mse_sum 및 total_samples) 업데이트를 담당합니다.
def update_state(self, y_true, y_pred, sample_weight=None):
# 이 라인은 평균 제곱 오차를 계산하기 위해 y_true를 원-핫 인코딩 표현으로 변환합니다.
y_true = tf.one_hot(y_true, depth=tf.shape(y_pred)[1])
# 이 라인은 예측 값에서 실제 레이블을 빼고 결과를 제곱하여 배치의 평균 제곱 오차(MSE)를 계산합니다. 그런 다음 제곱 차이를 합산합니다.
mse = tf.reduce_sum(tf.square(y_true - y_pred))
# 이 줄은 현재 배치의 MSE를 추가하여 mse_sum 상태 변수를 업데이트합니다.
self.mse_sum.assign_add(mse)
# 이 줄은 현재 배치(num_samples)의 샘플 수를 계산하고 num_samples를 추가하여 total_samples 상태 변수를 업데이트합니다.
num_samples = tf.shape(y_pred)[0]
self.total_samples.assign_add(num_samples)
# 이 메서드는 누적된 mse_sum 및 total_samples를 기반으로 최종 RMSE 메트릭 값을 계산합니다.
def result(self):
return tf.sqrt(self.mse_sum / tf.cast(self.total_samples, tf.float32))
# 이 메서드는 상태 변수(mse_sum 및 total_samples)를 초기 값으로 재설정하여 이전 평가에서 누적된 데이터를 지웁니다.
def reset_state(self):
self.mse_sum.assign(0.)
self.total_samples.assign(0)
사용자 정의 지표 테스트
콜백 사용하기
- 대규모 데이터셋에서 model.fit() 메서드를 사용하여 수십 번의 에포크를 실행하는 것은 종이 비행기를 날리는 것과 조금 비슷함
- 일단 손을 떠나면 종이 비행기 경로와 착륙 지점을 제어할 방법이 없음
- 나쁜 결과를 피하려면 (그래서 종이 비행기를 낭비하지 않으려면) 종이 비행기 대신 다른 것을 사용하는 것이 좋음
- 드론은 주변 환경을 감지한 데이터를 조작부에 전달하여 현재 상태를 바탕으로 자동으로 운전
- 케라스 콜백(callback)은 model.fit() 호출을 종이 비행기에서 스스로 판단하고 동적으로 결정하는 똑똑한 자동 드론으로 바꾸어 줄 것
- 콜백은 fit() 메서드 호출 시 모델에 전달되는 객체(특정 메서드를 구현한 클래스 객체)
- 훈련하는 동안 모델은 여러 지점에서 콜백을 호출
- 콜백은 모델의 상태와 성능에 대한 모든 정보에 접근하고 훈련 중지, 모델 저장, 가중치 적재 또는 모델 상태 변경 등을 처리할 수 있음
- 다음은 콜백을 사용하는 몇 가지 사례
- 모델 체크포인트(checkpoint) 저장: 훈련하는 동안 어떤 지점에서 모델의 현재 가중치를 저장
- 조기 종료(early stopping): 검증 손실이 더 이상 향상되지 않을 때 훈련을 중지(물론 훈련하는 동안 얻은 가장 좋은 모델을 저장)
- 훈련하는 동안 하이퍼파라미터 값을 동적으로 조정: 옵티마이저의 학습률 같은 경우
- 훈련과 검증 지표를 로그에 기록하거나 모델이 학습한 표현이 업데이트될 때마다 시각화: 앞서 보았던 fit() 메서드의 진행 표시줄이 하나의 콜백
- keras.callbacks 모듈에는 여러 가지 내장 콜백이 포함되어 있음(다음은 전체 리스트가 아님)
ModelCheckpoint와 EarlyStopping 콜백
- 모델을 훈련할 때는 미리 예상할 수 없는 것이 많음
- 특히 최적의 검증 손실을 얻기 위해 얼마나 많은 에포크가 필요한지 알지 못함
- 지금까지 예제는 적절한 훈련 에포크를 알아내기 위해 첫 번째 실행에서 과대적합이 시작될 때까지 충분한 에포크로 훈련했음
- 그런 다음 최적의 에포크 횟수로 처음부터 다시 훈련을 시작
- 당연히 이런 방식은 낭비가 많음
- 더 좋은 처리 방법은 검증 손실이 더 이상 향상되지 않을 때 훈련을 멈추는 것
- EarlyStopping 콜백을 사용하여 이를 구현할 수 있음
- EarlyStopiing 콜백은 정해진 에포크 동안 모니터링 지표가 향상되지 않을 때 훈련을 중지
- 예를 들어 과대적합이 시작되자마자 훈련을 중지할 수 있음
- 에포크 횟수를 줄여 다시 모델을 훈련할 필요가 없음
- 일반적으로 이 콜백은 훈련하는 동안 모델을 계속 저장해 주는 ModelCheckpoint 콜백과 함께 사용
- (선택적으로 지금까지 가장 좋은 모델만 저장할 수 있고 즉, 에포크 끝에서 최고의 성능을 낸 모델)
- fit() 메서드에서 callbacks 매개변수 사용하기
# fit() 메서드의 callbacks 매개변수를 사용하여 콜백의 리스트를 모델로 전달한다. 몇 개의 콜백이라도 전달할 수 있다.
callbacks_list = [
# 성능 향상이 멈추면 훈련을 중지시킨다.
keras.callbacks.EarlyStopping(
# 모델의 검증 정확도를 모니터링한다.
monitor="val_accuracy",
# 두 번의 에포크 동안 정확도가 향상되지 않으면 훈련을 중지
patience=2,
),
# 매 에포크 끝에서 현재 가중치를 저장
keras.callbacks.ModelCheckpoint(
# 모델 파일의 저장 경로
filepath="checkpoint_path.keras",
# 이 두 매개변수는 val_loss가 좋아지지 않으면 모델 파일을 덮어쓰지 않는다는 뜻이다. 훈련하는 동안 가장 좋은 모델이 저장된다.
monitor="val_loss",
save_best_only=True,
)
]
model = get_mnist_model()
model.compile(optimizer="rmsprop",
loss="sparse_categorical_crossentropy",
metrics=["accuracy"]) # 정확도를 모니터링하므로 모델 지표에 포함되어야 한다.
# 콜백이 검증 손실과 검증 정확도를 모니터링하기 때문에 fit() 메서드를 호출할 때 validation_data 매개변수로 검증 데이터를 전달해야 한다. 학습 후 항상 모델을 수동으로 저장할 수 있다.
model.fit(train_images, train_labels,
epochs=10,
callbacks=callbacks_list,
validation_data=(val_images, val_labels))
- 저장된 모델은 다음과 같이 로드할 수 있음
model = keras.models.load_model("checkpoint_path.keras")
사용자 정의 콜백 만들기
- 내장 콜백에서 제공하지 않는 특정 행동이 훈련 도중 필요하면 자신만의 콜백을 만들 수 있음
- 콜백은 keras.callbacks.Callback 클래스를 상속받아 구현
- 그 다음 이름에서 알 수 있듯이 훈련하는 동안 여러 지점에서 호출될 다음과 같은 메서드를 구현
- 이 메서드들은 모두 logs 매개변수와 함께 호출
- 이 매개변수 값은 이전 배치, 에포크 또는 훈련 실행에 대한 정보(훈련과 검증 지표 등)가 담긴 딕셔너리
- 또한, on_epoch_*와 on_batch_* 메서드는 에포크 인덱스나 배치 인덱스를 첫 번째 매개변수(정수)로 받음
- Callback 클래스를 상속하여 사용자 정의 콜백 만들기
from matplotlib import pyplot as plt
# keras.callbacks.Callback을 상속받는 사용자 정의 클래스 LossHistory를 정의합니다. 이 클래스는 모델 훈련 중 손실을 추적하고 시각화하는 데 사용
class LossHistory(keras.callbacks.Callback):
# 이 메서드는 훈련 과정이 시작될 때 호출되는 콜백입니다. 이는 per_batch_losses라는 인스턴스 변수를 빈 리스트로 초기화합니다. 이 리스트는 훈련 중 각 배치의 손실을 저장
def on_train_begin(self, logs):
self.per_batch_losses = []
# 이 메서드는 각 훈련 배치가 끝날 때 호출되는 콜백입니다. 현재 배치의 손실을 per_batch_losses 리스트에 추가
def on_batch_end(self, batch, logs):
self.per_batch_losses.append(logs.get("loss"))
# 이 메서드는 각 훈련 에포크가 끝날 때 호출되는 콜백입니다. 에포크 동안 각 배치의 훈련 손실을 그래프로 만들어 이미지로 저장
def on_epoch_end(self, epoch, logs):
plt.clf()
plt.plot(range(len(self.per_batch_losses)), self.per_batch_losses,
label="Training loss for each batch")
plt.xlabel(f"Batch (epoch {epoch})")
plt.ylabel("Loss")
plt.legend()
plt.savefig(f"plot_at_epoch_{epoch}")
self.per_batch_losses = []
손실 그래프를 저장하는 사용자 정의 콜백의 출력 결과
텐서보드를 사용한 모니터링 시각화
- 좋은 연구를 하거나 좋은 모델을 개발하려면 실험하는 모델 내부에서 어떤 일이 일어나는지 자주 그리고 많은 피드백을 받아야 함
- 모델이 얼마나 잘 작동하는지 가능한 많은 정보를 얻는 것이 실험을 하는 목적
- 발전은 루프처럼 반복되는 과정을 통해 일어남
- 한 아이디어가 떠오르면 이 아이디어를 검증할 실험을 계획
- 실험을 수행하고 생성된 정보를 가공
- 이 정보는 다음 아이디어에 영감을 줌
- 이 루프를 더 많이 실행할수록 아이디어는 더 정제되고 강력해질 것
- 이때 케라스는 가능한 최단 시간에 아이디어를 실험으로 구현하도록 도와줌
- 고속 GPU를 통해 가능한 빠르게 실험의 결과를 얻도록 도와줄 것
- 실험 결과를 처리하기 위해 텐서보드(TensorBoard)가 필요함
발전의 반복 루프
- 텐서보드는 로컬에서 실행할 수 있는 브라우저 기반 애플리케이션
- 훈련하는 동안 모델 안에서 일어나는 모든 것을 모니터링하기 위한 가장 좋은 방법
- 텐서보드를 사용하여 다음과 같은 일을 수행할 수 있음
- 훈련하는 동안 측정 지표를 시각적으로 모니터링
- 모델 구조를 시각화
- 활성화 출력과 그레이디언트의 히스토그램을 그림 임베딩을 3D로 표현
- 모델의 최종 손실 외에 더 많은 정보를 모니터링하면 모델 작동에 대한 명확한 그림을 그릴 수 있음
- 결국 모델을 더 빠르게 개선할 수 있음
- 케라스 모델의 fit() 메서드와 함께 텐서보드를 사용하는 가장 쉬운 방법은 keras.callbacks.TensorBoard 콜백
model = get_mnist_model()
model.compile(optimizer="rmsprop",
loss="sparse_categorical_crossentropy",
metrics=["accuracy"])
tensorboard = keras.callbacks.TensorBoard(
log_dir="./tb_logs",
)
model.fit(train_images, train_labels,
epochs=10,
validation_data=(val_images, val_labels),
callbacks=[tensorboard])
- 모델이 실행되고 나면 지정한 위치에 로그를 기록할 것
- 로컬 컴퓨터에서 파이썬 스크립트를 실행한다면 다음 명령으로 로컬에서 텐서보드를 실행할 수 있음
- (pip 명령으로 텐서플로를 설치했다면 tensorboard 명령을 사용할 수 있고 만약 이 실행 파일이 없다면 pip install tensorboard와 같이 수동으로 텐서보드를 설치할 수 있음)
tensorboard --logdir /full_path_to_your_log_dir
- 이 명령을 실행하고 화면에 출력된 URL에 접속하면 텐서보드 인터페이스를 볼 수 있음
- 코랩 노트북을 사용한다면 다음 명령을 사용하여 텐서보드 인스턴스를 노트북 일부로 실행할 수 있음
%load_ext tensorboard
%tensorboard --logdir ./tb_logs
7.4 사용자 정의 훈련, 평가 루프 만들기
- fit() 워크플로는 쉬운 사용성과 유연성 사이에서 알맞은 군형을 유지
- 대부분의 경우 이를 사용할 것
- 사용자 정의 지표, 사용자 정의 손실, 사용자 정의 콜백이 있음에도 딥러닝 연구자들이 원하는 모든 것을 지원하지는 못함
- 무엇보다도 내장 fit() 워크플로는 지도 학습(supervised learning)에만 초점이 맞추어져 있음
- 지도 학습은 입력 데이터와 이에 연관된 타깃(레이블 또는 어노테이션이라고도 부름)을 가지며 모델의 예측과 이 타깃의 함수로 손실을 계산
- 모든 형태의 머신 러닝이 카테고리에 속하는 것은 아님
- 생성 학습(generative learning), 자기지도 학습(selfsupervised learning)(타깃을 입력에서 얻는다.), 강화 학습(reinforcement learning)(강아지를 훈련하는 것처럼 간혈적인 '보상(reward)'으로 학습)등 명시적인 타깃이 없는 경우가 있음
- 일반적인 지도 학습을 하는 경우에도 연구자로서 저수준의 유연성이 필요한 새로운 기능을 추가하고 싶을 수 있음
- 내장 fit() 메서드로 충분하지 않은 상황이면 자신만의 훈련 로직을 직접 작성
- 다시 정리하면 전형적인 훈련 루프틑 다음과 같은 내용이 포함
- 현재 배치 데이터에 대한 손실 값을 얻기 위해 그레이디언트 테이프 안에서 정방향 패스를 실행(모델의 출력을 계산)
- 모델 가중치에 대한 손실의 그레이디언트를 계산
- 현재 배치 데이터에 대한 손실 값을 낮추는 방향으로 모델 가중치를 업데이트
- 이런 단계가 필요한 만큼 많은 배치에 대해 반복
- 이것이 fit() 메서드가 처리하는 일
훈련 vs 추론
- 지금까지 본 저수준 훈련 루프 예제에서는 predictions = model(inputs)를 통해 단계 1(정방향 계산)을 수행해고 gradients = tape.gradient(loss, model.weights)를 통해 단계 2(그레이디언트 테이프로 계산한 그레이디언트를 추출)를 수행
- 일반적으로 두 가지 중요한 세부 사항을 고려해야 함
- Dropout 층과 같은 일부 케라스 층은 훈련(training)과 (예측을 만들기 위해 모델을 사용하는) 추론(inference)에서 동작이 다름
- 이런 층은 call() 메서드에 training 불리언(boolean) 매개변수를 제공
- dropout(inputs, training=True)와 같이 호출하면 이전 층의 활성화 출력 값을 일부 랜덤하게 제외
- dropout(inputs, training=False)와 같이 호출하면 아무런 일을 수행하지 않음
- 함수형 모델과 Sequential 모델도 call() 메서드에서 training 매개변수를 제공
- 정방향 패스에서 캐라스 모델을 호출할 때는 training=True로 지정해야 하는 것을 기억하자!
- 정방향 패스는 predictions = model(inputs, training=True)가 됨
- 모델 가중치 그레이디언트를 추론할 때 tape.gradients(loss, model.weights)가 아니라 tape.gradients(loss, model.trainable_weights)를 사용해야 함
- 사실 층과 모델에는 두 종류의 가중치가 있음
- 훈련 가능한 가중치: Dense 층의 커널과 편향처럼 모델의 손실을 최소화하기 위해 역전파로 업데이트
- 훈련되지 않는 가중치: 해당 층의 정방향 패스 동안 업데이트. 예를 들어 얼마나 많은 배치를 처리했는지 카운트하는 사용자 정의 층이 필요하다면 이 정보를 훈련되지 않는 가중치에 저장하고 배치마다 값을 1씩 증가시킴
- 케라스에 내장된 층 중에는 훈련되지 않는 가중치를 가진 층은 9장에서 소개할 Batch Normalization뿐
- BatchNormalization 층은 처리하는 데이터의 평균과 표준 편차에 대한 정보를 추적하여 (6장에서 배운 개념인) 특성 정규화(feature normalization)를 실시간으로 근사하기 위해 훈련되지 않은 가중치가 필요함
- 이 두 가지를 고려하면 지도 학습을 위한 훈련 스탭은 다음과 같이 작성할 수 있음
def train_step(inputs, targets):
with tf.GradientTape() as tape:
predictions = model(inputs, training=True)
loss = loss_fn(targets, predictions)
gradients = tape.gradients(loss, model.trainable_weights)
optimizer.apply_gradients(zip(model.trainable_weights, gradients))
측정 지표의 저수준 사용법
- 저수준 훈련 루프에서 (사용자 정의 지표든 내장 지표든) 케라스 지표를 사용하게 될 것
- 앞에서 측정 지표 API에 대해 배웠음
- 단순히 각 배치의 타깃과 예측에 대해 update_state(y_true, y_pred)를 호출하면 됨
- result() 메서드를 사용하여 현재 지표 같은 값을 얻음
metric = keras.metrics.SparseCategoricalAccuracy()
targets = [0, 1, 2]
predictions = [[1, 0, 0], [0, 1, 0], [0, 0, 1]]
metric.update_state(targets, predictions)
current_result = metric.result()
print(f"결과: {current_result:.2f}")
- 모델의 손실처럼 스칼라 값의 평균을 추적해야 할 수도 있음
- keras.metrics.Mean을 사용해서 이를 처리할 수 있음
values = [0, 1, 2, 3, 4]
mean_tracker = keras.metrics.Mean()
for value in values:
mean_tracker.update_state(value)
print(f"평균 지표: {mean_tracker.result():.2f}")
- (훈련 에포크나 평가를 시작할 때처럼) 현재 결과를 재설정하고 싶을 때 metric.reset_state()를 사용하는 것을 잊지 말자
완전한 훈련과 평가 루프
- 정방향 패스, 역방향 패스, 지표 추적을 fit()과 유사한 훈련 스탭 함수로 연결해 보겠음
- 단계별 훈련 루프 작성하기: 훈련 스탭 함수
model = get_mnist_model()
# 손실 함수를 준비
loss_fn = keras.losses.SparseCategoricalCrossentropy()
# 옵티마이저를 준비
optimizer = keras.optimizers.RMSprop()
# 모니터링할 지표 리스트를 준비
metrics = [keras.metrics.SparseCategoricalAccuracy()]
# 손실 평균을 추적할 평균 지표를 준비
loss_tracking_metric = keras.metrics.Mean()
def train_step(inputs, targets):
# 정방향 패스를 실행. training = True를 전달
with tf.GradientTape() as tape:
predictions = model(inputs, training=True)
loss = loss_fn(targets, predictions)
# 역방향 패스를 실행한다. model.trainable_weights를 사용
gradients = tape.gradient(loss, model.trainable_weights)
optimizer.apply_gradients(zip(gradients, model.trainable_weights))
# 측정 지표를 계산한다
logs = {}
for metric in metrics:
metric.update_state(targets, predictions)
logs[metric.name] = metric.result()
# 손실 평균을 계산한다.
loss_tracking_metric.update_state(loss)
logs["loss"] = loss_tracking_metric.result()
return logs
- 매 에포크 시작과 평가 전에 지표의 상태를 재설정해야 함
- 다음은 이를 위한 유틸리티 함수
- 단계별 훈련 루프 작성하기: 지표 재설정
def reset_metrics():
for metric in metrics:
metric.reset_state()
loss_tracking_metric.reset_state()
- 이제 완전한 훈련 루프를 구성할 수 있음
- tf.data.Dataset객체를 사용하여 넘파이 데이터를 크기가 32인 배치로 데이터를 순회하는 반복자로 바꿈
- 단계별 훈련 루프 작성하기: 훈련 루프 자체
training_dataset = tf.data.Dataset.from_tensor_slices((train_images, train_labels))
training_dataset = training_dataset.batch(32)
epochs = 3
for epoch in range(epochs):
reset_metrics()
for inputs_batch, targets_batch in training_dataset:
logs = train_step(inputs_batch, targets_batch)
print(f"{epoch}번째 에포크 결과")
for key, value in logs.items():
print(f"...{key}: {value:.4f}")
- 다음은 평가 루프
- 간단한 for 루프로 하나의 배치 데이터를 처리하는 test_step() 함수를 반복하여 호출
- test_step() 함수는 train_step() 함수 로직의 일부분을 사용
- 모델의 가중치를 업데이트하는 코드가 빠져 있음
- 단계별 평가 루프 작성하기
def test_step(inputs, targets):
predictions = model(inputs, training=False) # training=False를 전달한다
loss = loss_fn(targets, predictions)
logs = {}
for metric in metrics:
metric.update_state(targets, predictions)
logs["val_" + metric.name] = metric.result()
loss_tracking_metric.update_state(loss)
logs["val_loss"] = loss_tracking_metric.result()
return logs
val_dataset = tf.data.Dataset.from_tensor_slices((val_images, val_labels))
val_dataset = val_dataset.batch(32)
reset_metrics()
for inputs_batch, targets_batch in val_dataset:
logs = test_step(inputs_batch, targets_batch)
print("평가 결과:")
for key, value in logs.items():
print(f"...{key}: {value:.4f}")
- 방금 fit() 메서드와 evaluate() 메서드를 구현!
- 사실 완전히 똑같지는 않음
- fit()과 evaluate() 메서드는 대규모 분산 계산과 같은 더 많은 기능을 제공
- 이를 위해서는 조금 더 많은 작업이 필요함
- 또한, 몇 가지 핵심적인 성능 최적화가 포함되어 있음
tf.function으로 성능 높이기
- 기본적으로 동일한 로직을 구현했지만 아마 직접 작성한 루프가 케라스에 내장된 fit()과 evaluate() 메서드보다 훨씬 느리게 실행됨
- 기본적으로 텐서플로 코드는 넘파이나 일반적인 파이썬 코드와 비슷하게 즉시(eagerly) 라인 단위로 실행되기 때문임
- 즉시 실행(eager execution)은 코드 디버깅을 쉽게 만들어 줌
- 성능 측면에서는 최적이 아님
- 텐서플로 코드는 계산 그래프(computation graph)로 컴파일하는 것이 더 성능이 좋음
- 여기에서는 라인 단위로 해석되는 코드에서는 할 수 없는 전역적인 최적화가 가능
- 이렇게 만드는 문법은 매우 간단함
- 다음과 같이 실행하기 전에 컴파일하고 싶은 함수에 @tf.function
- 평가 스텝 함수에 @tf.function 데코레이터 추가하기
@tf.function # 이 라인만 추가되었다.
def test_step(inputs, targets):
predictions = model(inputs, training=False)
loss = loss_fn(targets, predictions)
logs = {}
for metric in metrics:
metric.update_state(targets, predictions)
logs["val_" + metric.name] = metric.result()
loss_tracking_metric.update_state(loss)
logs["val_loss"] = loss_tracking_metric.result()
return logs
val_dataset = tf.data.Dataset.from_tensor_slices((val_images, val_labels))
val_dataset = val_dataset.batch(32)
reset_metrics()
for inputs_batch, targets_batch in val_dataset:
logs = test_step(inputs_batch, targets_batch)
print("평가 결과:")
for key, value in logs.items():
print(f"...{key}: {value:.4f}")
- 코랩 CPU에서 이 평가 루프를 실행하는데 2.8초에서 0.6초로 줄었음
- 훨씬 빨라졌음!
- 코드를 디버깅할 때 @tf.function 데코레이터를 쓰지 말고 즉시 실행 모드를 사용하는 것이 좋음
- 이 방식이 버그를 추적하기 더 쉬움
- 코드가 제대로 작동하고 성능을 높이고 싶을 때 훈련 스탬과 평가 스탭에 @tf.function 데코레이터를 추가
fit() 메서드를 사용자 정의 루프로 활용하기
- 이전 절에서 밑바닥부터 완전한 사용자 정의 훈련 루프를 만들었음
- 이런 방식이 가장 높은 유연성을 제공하지만 많은 코드를 작성해야 하고 콜백이나 분산 훈련 지원 같은 fit() 메서드가 제공하는 많은 편리한 기능을 사용할 수 없음
- 사용자 정의 훈련 알고리즘이 필요하지만 케라스에 내장된 훈련 로직의 기능을 활용하고 싶다면 어떨까?
- 사실 fit() 메서드와 밑바닥부터 작성한 훈련 루프 사이의 중간 지점이 있음
- 사용자 정의 훈련 스탭 함수를 제공하고 나머지 처리는 프레임워크에 위임할 수 있음
- 이렇게 하려면 Model 클래스의 train_step() 메서드를 오버라이딩(overriding)
- 이 함수는 fit() 메서드가 배치 데이터마다 호출하는 메서드
- 그 다음 이전처럼 fit() 메서드를 호출하면 자신만의 학습 알고리즘을 실행시킬 수 있음
- 간단한 예는 다음과 같음
- keras.Model을 상속한 새로운 클래스를 만듦
- train_step(self, data)메서드를 오버라이드. 이 메서드의 내용은 이전 절에서 만든 것과 동일. (손실을 포함하여) 측정 지표 이름과 현재 값이 매핑된 딕셔너리를 반환
- 모델의 Metric 객체들을 반환하는 metrics 속성을 구현. 이를 활용하여 매 에포크 시작이나 evaluate()를 호출할 때 모델이 지표 객체들의 reset_state() 메서드를 자동으로 호출할 수 있음. 수동으로 지표를 재설정할 필요가 없음
- fit()이 사용할 사용자 정의 훈련 스탭 구현하기
loss_fn = keras.losses.SparseCategoricalCrossentropy()
loss_tracker = keras.metrics.Mean(name="loss") # 이 객체는 훈련과 평균 과정에서 배치 손실의 평균을 추적한다.
class CustomModel(keras.Model):
def train_step(self, data): # train_step 메서드를 오버라이딩한다.
inputs, targets = data
with tf.GradientTape() as tape:
predictions = self(inputs, training=True) # 모델이 클래스 자체이므로 model(inputs, training=True)대신에 self(inputs, training=True)를 사용
loss = loss_fn(targets, predictions)
gradients = tape.gradient(loss, self.trainable_weights)
self.optimizer.apply_gradients(zip(gradients, self.trainable_weights))
loss_tracker.update_state(loss) # 손실의 평균을 추적하는 loss_tracker를 업데이트한다.
return {"loss": loss_tracker.result()} # 평균 손실을 구한다.
@property
# 에포크마다 재설정할 지표는 여기에 나열해야 한다.
def metrics(self):
return [loss_tracker]
- 이제 사용자 정의 모델의 객체를 만들고 컴파일하고 (손실은 모델 밖에서 이미 정의했기 때문에 옵티마이저만 전달), 보통 때처럼 fit() 메서드로 훈련할 수 있음
inputs = keras.Input(shape=(28 * 28,))
features = layers.Dense(512, activation="relu")(inputs)
features = layers.Dropout(0.5)(features)
outputs = layers.Dense(10, activation="softmax")(features)
model = CustomModel(inputs, outputs)
model.compile(optimizer=keras.optimizers.RMSprop())
model.fit(train_images, train_labels, epochs=3)
- 몇 가지 주의할 점이 있음
- 이 패턴 때문에 함수형 API로 모델을 만드는 데 문제가 되지 않음. Sequential 모델, 함수형 모델, 서브클래싱 모델을 만드는지에 상관없이 이 방식을 하용할 수 있음
- 프레임워크가 알아서 처리하기 때문에 train_step 메서드를 오버라이딩할 때 @tf.function 데코레이터를 사용할 필요가 없음
- 이제 compile() 메서드를 통해 지표와 손실을 설정하면 어떨까?
- compile() 메서드를 호출한 후 다음을 참조할 수 있음
- self.compiled_loss: compile() 메서드에 전달할 손실 함수
- self.compiled_metrics: compile() 메서드에 전달된 지표 목록이 포함되어 있는 객체. self.compiled_metrics.update_state()를 호출하여 모든 지표를 동시에 업데이트 할 수 있음
- self.metrics: compile() 메서드에 전달한 실제 지표의 목록. 앞서 loss_tracking_metric으로 수동으로 했던 것과 비슷하게 손실을 추적하는 지표도 포함
- 다음과 같이 사용 가능
class CustomModel(keras.Model):
def train_step(self, data):
inputs, targets = data
with tf.GradientTape() as tape:
predictions = self(inputs, training=True)
loss = self.compiled_loss(targets, predictions) # self.compiled_loss를 사용해서 손실을 계산
gradients = tape.gradient(loss, self.trainable_weights)
self.optimizer.apply_gradients(zip(gradients, self.trainable_weights))
self.compiled_metrics.update_state(targets, predictions) # self.compiled_metrics로 모델의 지표를 업데이트
return {m.name: m.result() for m in self.metrics} # 측정 지표 이름과 현재 값을 매핑한 딕셔너리를 반환한다.
- 테스트
inputs = keras.Input(shape=(28 * 28,))
features = layers.Dense(512, activation="relu")(inputs)
features = layers.Dropout(0.5)(features)
outputs = layers.Dense(10, activation="softmax")(features)
model = CustomModel(inputs, outputs)
model.compile(optimizer=keras.optimizers.RMSprop(),
loss=keras.losses.SparseCategoricalCrossentropy(),
metrics=[keras.metrics.SparseCategoricalAccuracy()])
model.fit(train_images, train_labels, epochs=3)