[케라스 창시자에게 배우는 딥러닝 2판] 4, 5일차(7장)
이번에 역대급으로 내용이 많다. 스압주의
7장: 케라스 완전 정복
- 7장은 아예 코드만 보는거라고 봐도 무방하다
7.1 다양한 워크플로
- 케라스 API 설계는 복잡성의 단계적 공개원칙을 따른다.
7.2 케라스 모델을 만드는 여러 방법
- 난이도에 따라 Sequential 모델, 함수형 API, Model 서브클래싱 세 가지로 구분할 수 있다. (점점 어려워짐)
- 시퀀셜 모델은 쉽지만 제약이 많다. 함수형 API는 적당히 쉽고 적당히 조절가능하다. Model 서브클래싱은 어렵지만 아주 자유롭게 전부 다 세밀하게 조정 가능하다.
- 시퀀셜 활용 방법
- 시퀀셜 안에 리스트 형식으로 다 넣기
- add()함수로 추가하기
? 질문: add()말고도 케라스 시퀀셜 모델 클래스에서 사용할 수 있는 다른 함수는 뭐가 있을까? pop, compile, summary, fit, evaluate, predict, save, load_weights 등이 있다
- 세팅 활용 방법
- 입력값도 한번에 넣기
- 입력값을 나중에 넣어서 build 에 입력값 넣기 (여기서 입력값 = 입력 데이터 shape)
- 함수형 API 사용방법
- 레고 쌓기와 비슷하다
- featrue의 shape은 (None, 20100)임
? 질문 Input 객체엔 어떤 메서드와 상태변수가 있을까? shape, dtype, get_shape, get_dtype
- 이러한 객체를 심볼릭 텐서라고 한다. 실제 데이터를 가지고 있지 않지만 텐서의 사양이 미리 인코딩되어 있음
- randint(a, b): a 이상 b 이하
- 랜덤에 대한 정보: https://www.daleseo.com/python-random/
- 다중입력, 다중출력이 가능함. 아래처럼 표현 가능
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})
- 갑(자기)분(위기) 퀴즈!!! 위 코드에서 각 디셔너리의 key는 각 layer __과 __의 변수명이다.
- 정답
- input, output
# 모델의 연결 구조(토폴로지) 시각화
# 파일 자동 저장(현재 연결된 디렉토리로)
keras.utils.plot_model(model, "ticket_classifier.png")
- 모델 Subclassing
- 모델 로직을 많이 책임져야하며 잠재적 오류 가능성이 높다.
- call, summary 등의 다른 함수를 쓸 수 없다.
- 서브클래싱한 모델을 포함하는 함수형 모델 만들기와
- 함수형 모델을 포함하는 서브클래싱 모델 만들기도 가능
7.3 내장된 훈련 루프와 평가 루프 사용하기
- 표준 워크플로 compile, fit, evaluate, predict 사용하는 것 다시 상기
- 워크플로 커스터마이징할 수 있는 몇 가지 방법
- 사용자 정의 클래스 만들기
- 그냥 코드상으로 추가하기
- 이번 7.3장은 metric과 콜백에 대해서 커스터마이징 하는 방법 소개함 이 외에도 loss, optimizer 등등등 커스터마이징 가능한데 여기서 소개 안함
- 사용자 정의 지표 만들기
- 지표(metric)은 모델의 성능을 측정하는 열쇠다
- 지표가 층과 마찬가지로 텐서플로 변수에 내부 상태를 저장하는데 다른 점은 역전파로 업데이트 되지 않는다는 점
- 예시로 RMSE계산하는 간단한 사용자 정의 지표 구현
#**kwargs는 "keyword arguments"의 약어로, 함수에 임의의 개수의 키워드 인자를 전달
# super()은 상위의 클래스를 상속받아서 초기화를 한다는 뜻
# assign_add: 상태 변수(tf.constant가 아니라 tf.Variable)에 대한 모든 업데이트
# <https://www.tensorflow.org/api_docs/python/tf/keras/metrics/Metric#update_state>
import tensorflow as tf
class RootMeanSquaredError(keras.metrics.Metric):
def __init__(self, name="rmse", **kwargs):
super().__init__(name=name, **kwargs)
self.mse_sum = self.add_weight(name="mse_sum", initializer="zeros")
self.total_samples = self.add_weight(
name="total_samples", initializer="zeros", dtype="int32")
def update_state(self, y_true, y_pred, sample_weight=None): #sample_weight 사용X
y_true = tf.one_hot(y_true, depth=tf.shape(y_pred)[1])
mse = tf.reduce_sum(tf.square(y_true - y_pred))
self.mse_sum.assign_add(mse)
num_samples = tf.shape(y_pred)[0]
self.total_samples.assign_add(num_samples)
def result(self):
return tf.sqrt(self.mse_sum / tf.cast(self.total_samples, tf.float32))
# tf.cast: tf.cast 함수는 텐서의 데이터 타입을 변환하기 위해 사용
# 예시) 정수형 x에 대해 실수형으로 바꿀 때: x_float = tf.cast(x, dtype=tf.float32)
def reset_state(self): # 객체를 다시 생성하지 않고 상태를 초기화 하는 방법
self.mse_sum.assign(0.)
self.total_samples.assign(0)
model = get_mnist_model()
model.compile(optimizer="rmsprop",
loss="sparse_categorical_crossentropy",
metrics=["accuracy", **RootMeanSquaredError()**])
model.fit(train_images, train_labels,
epochs=3,
validation_data=(val_images, val_labels))
test_metrics = model.evaluate(test_images, test_labels)
→ 기존 워크플로에서 컴파일 안에 메트릭안에 **RootMeanSquaredError()**가 있는것
- 콜백 사용하기
- 콜백은 손에서 떠나가면 제어가 불가능한 종이비행기 대신 제어 가능한 드론을 사용하는 것과 같다. 훈련 중지, 모델 저장, 가중치 적재, 모델 상태 변경 등에 사용된다.
- 여기서 예시로 보여주는 것은 ModelCheckpoint, EarlyStopping 콜백을 보여줌
callbacks_list = [
keras.callbacks.EarlyStopping(
monitor="val_accuracy",
patience=2, #val_accuracy가 2번정도 더 나은 결과가 안 나오면 멈춤(숫자변경가능)
),
keras.callbacks.ModelCheckpoint(
filepath="checkpoint_path.keras", #모델이 저장됨
monitor="val_loss", # 좋음의 기준은 val_loss
save_best_only=True, #가장 좋았던 모델만 저장함
)
]
model = get_mnist_model()
model.compile(optimizer="rmsprop",
loss="sparse_categorical_crossentropy",
metrics=["accuracy"])
model.fit(train_images, train_labels,
epochs=10,
callbacks=callbacks_list,
validation_data=(val_images, val_labels))
- 사용자 정의 콜백 만들기
from matplotlib import pyplot as plt
class LossHistory(keras.callbacks.Callback):
def on_train_begin(self, logs): #훈련이 시작될 때 호출
self.per_batch_losses = []
def on_batch_end(self, batch, logs): #훈련이 끝날 때 호출
self.per_batch_losses.append(logs.get("loss"))
# logs 딕셔너리에 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 = []
model = get_mnist_model()
model.compile(optimizer="rmsprop",
loss="sparse_categorical_crossentropy",
metrics=["accuracy"])
model.fit(train_images, train_labels,
epochs=10,
callbacks=[**LossHistory()**],
validation_data=(val_images, val_labels))
- 텐서보드를 활용한 모니터링과 시각화
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])
%load_ext tensorboard
%tensorboard --logdir ./tb_logs
- 클라우드 환경에서 안보임 로컬에서 보임→ 환경에 따라 다름 텐서보드 사용하고 싶으면 링크 참고: https://blog.naver.com/PostView.nhn?blogId=rhkdgud61&logNo=222272750848
- 결론: 이런식으로 우리가 기존에 없던 것에 RootMeanSquaredError(), LossHistory(), tensorboard 같은 것을 작성하여 필요로 하는 옵션들을 추가할 수 있다.
7.4 사용자 정의 훈련 루프, 평가 루프 만들기
- fit() 워크플로는 쉬운 사용성과 유연성 사이에서 알맞은 균형을 유지한다
- 하지만, 지도학습에만 초점이 맞춰져있어서 자기지도학습, 생성 학습, 강화 학습 등은 저수준의 유연성이 필요한 새로운 기능을 추가하여 해야한다.
질문?? 그럼 이번 절에서 위의 안되는 것도 가능하도록 되어있는건가????? → 이번 7.4에서는 지도학습에 대해서 fit, evaluate을 자세히 뜯어보지만 이런식으로 자세히 뜯어보면 강화학습, 생성학습 등등도 응용하면 할 수 있다!! 하지만 이에 대해서는 더 공부해야함~
- 전형적인 훈련 루프 다시 복습
- 현재 배치 데이터에 대한 손실 값을 얻기 위해 그레디언트 테이프 안에서 정방향 패스
- 모델 가중치에 대한 손실의 그레디언트 계산
- 현재 배치 데이터에 대한 손실 값을 낮추는 방향으로 모델 가중치 업데이트
- dropout은 훈련단계와 추론단계에서의 동작이 다름: 훈련때 일부 랜덤 노드를 하이퍼파라미터인 수보다 작은 값을 제외하여 가중치 업데이트하고 추론때는 모든 노드를 쓴다
- 정방향 패스에서 케라스 모델 호출할 때 dropout(inputs, training = True)지정 기억하기
- 또한, 모델 가중치 그레디언트 추출할 때 model.weights가 아니라 trainable_weights임
- 모델과 층의 종류 2가지
- 훈련 가능한 가중치: 역전파로 업데이트
- 훈련되지 않는 가중치: 정방향 패스 동안 업데이트 ex) 유일하게 BatchNormalization
→ summary에서 훈련 가능한 가중치와 훈련되지 않은 가중치 나눠지고 total은 그 둘 합침. 참고
- 측정 지표의 저수준 사용법
- 원하는 측정 지표 API를 불러온다
- 불러온 측정 지표 객체에 전에 이미 만든 update_state, result함수를 불러서 사용한다
- **훈련 루프와 평가 루프의 저수준 사용법(**도 마찬가지긴 한데 암튼 코드)
- 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):
with tf.GradientTape() as tape: #훈련과정 기록
predictions = model(inputs, training=True)
loss = loss_fn(targets, predictions)
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()
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}")
- evaluate()메서드대신 쓰는 저수준의 평가 루프
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}")
- tf.function으로 성능 높히기
- 즉시 실행은 코드 디버깅을 쉽게 하지만 성능 측면에서는 최적이 아님(속도가 느림)
- 계산 그래프로 컴파일 하면 코드 디버깅은 어렵지만 전역적인 최적화 가능
- 계산 그래프로 컴파일 하는 방식: @tf.function을 맨 앞에 추가하면 됨(위코드랑 같으니 생략)
- fit()이 사용할 사용자 정의 스텝구현 (위의 저수준 훈련 루프랑 뭐가 다르지)→ 클래스로 만듬
loss_fn = keras.losses.SparseCategoricalCrossentropy()
loss_tracker = keras.metrics.Mean(name="loss")
class CustomModel(keras.Model):
def train_step(self, data):
inputs, targets = data
with tf.GradientTape() as tape:
predictions = 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)
return {"loss": loss_tracker.result()}
@property # 속성 값을 더 편리하게 설정하기 위해 사용함
def metrics(self):
return [loss_tracker]
# @propperty 예시
class Student:
def __init__(self, name):
self._name = name
@property
def name(self):
return self._name
@name.setter
def name(self, value):
self._name = value
student = Student("John")
student.name = "Alice"
print(student.name) # "Alice" 출력
- complie() 메서드 사용하여 지표와 손실 재설정
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)
gradients = tape.gradient(loss, self.trainable_weights)
self.optimizer.apply_gradients(zip(gradients, self.trainable_weights))
self.compiled_metrics.update_state(targets, predictions)
return {m.name: m.result() for m in self.metrics}
<문제>
문제1: 함수형API와 서브클래싱의 장점?
문제2: 시퀀셜과 비교했을 때 함수형API만 가능한 것은?
문제3: 함수형API와 비교했을 때 서브클래싱만 가능한 것은?
문제4: 만일 reset_state 함수가 없을 때 발생하는 일은?
문제5: 아래 링크에서 사용자 정의 콜백이 어떻게 사용되는지 코드 봐보기
문제6: train_step 함수가 어떻게 동작하는가?
<문제링크>
- 사용자 정의 콜백 만들기
https://www.tensorflow.org/api_docs/python/tf/keras/callbacks
- 추가문제 1-1번
https://www.kaggle.com/datasets/arnabchaki/indian-restaurants-2023
- 추가문제 1-2번
https://www.kaggle.com/datasets/mirichoi0218/insurance
- 문제1: 함수형API는 제약이 많은 Sequensial보다 더 많은 기능을 추가하여 쉽게 사용할 수 있으며 서브클래싱은 어렵지만 함수형 API보다 더 세밀하고 자유롭게 사용할 수 있다. 추가로 함수형API는 summary(), plot_model()함수를 사용가능하지만 서브클래싱은 안됨
- 문제2: 다중입력, 다중출력, 층 연결 구조 활용이 가능하다, 특성 추출을 이용해서 층을 재사용 할 수 있다.
- 문제3: 함수형에서 사용하지 못하는 콜백, 분산훈련지원 같은 쉽게 사용할 수 있는 fit()의 메서드를 서브클래싱에서는 오버라이딩하여 사용할 수 있다.
- 문제4: reset_state 메서드는 에폭마다 성능이 얼마나 변하였는지 계산해야한다. 만약 없어지면 지표의 상태가 계속 축적되어서 올바른 지표변화를 볼 수 없다.
- 문제5: 콜백 함수 알아보기
tf.keras.callbacks.EarlyStopping(
monitor='val_loss',
min_delta=0,
patience=0,
verbose=0,
mode='auto',
baseline=None,
restore_best_weights=False,
start_from_epoch=0
)
tf.keras.callbacks.ModelCheckpoint(
filepath,
monitor: str = 'val_loss',
verbose: int = 0,
save_best_only: bool = False,
save_weights_only: bool = False,
mode: str = 'auto',
save_freq='epoch',
options=None,
initial_value_threshold=None,
**kwargs
)
→ 가장 많이 쓰는 두개의 함수?객체??의 인자값 확인~~
- 문제6: 정방향 패스 실행, 역방향 패스 실행, 반환할 log딕셔너리 선언, 모니터링할 지표 리스트의 크기(훈련데이터 크기)만큼 반복하여 인덱스 마다 지표 종류에 따라 실제값과 예측값의 차이를 업데이트 하고 값을 log딕셔너리에 저장, 전체 데이터에 대한 loss값 계산, log반환~
- 추가문제: 인도 레스토랑 2023 데이터셋 이용할 것임: 캐글링크
<질문>
- 질문1: 7.3부터 class 있는 코드 블럭이 서브클래싱이고 class없는 코드 블럭이 함수형인가? 아니다!!!!!!! 완전 다르다!!! 서브캘리싱, 함수형 API는 모델을 지칭하는 것이고 7.3과7.4는 저수준의 코딩 방법(저수준 = 더 세세히 코드를 살펴보는 방법)을 알려주는 것이다
- 질문2: p.270 지도학습에만 초점이 맞춰져있어서 자기지도학습, 생성 학습, 강화 학습 등은 저수준의 유연성이 필요한 새로운 기능을 추가하여 해야한다. 그럼 이번 절에서 위의 안되는 것도 가능하도록 되어있는건가????? 이번 절에서 소개는 안하지만 배우고 수정하면 됨
- 질문3: 8.1의 그림8-4의 세번째 그림이 이해안감 -> 잘못된게 맞다 오케이
<7장 후기>
저수준 사용법이라고 해서 코드 Mean 함수를 만들면 sum(for i in a)/n 이런것부터 완전 바닥에서 코드쓰는 줄 알았는데 그런 간단한 계산은 케라스에서 불러서 쓰고 거기서 추가로 필요한 기능들을 위해서 update_state나 result함수를 추가하여 사용하는 것 같다. 컴파일 할때 단순히 loss = Rmsporp 등등 이런식으로 사용하기보다 더 추가적인 결과를 얻는 다던가 등등의 추가 기증을 사용하고 싶을 때 이렇게 세세하게 살펴보고 새로 정의하여 쓰는 것 같다. 아직 제대로 사용해보지 못해서 제대로 감이 오지는 않은데 이 책을 다시 보고 나서 7장은 반드시 다시 봐야겠다. 너무 어렵다. 마치 코끼리를 이해하기 위해서 전체를 보는 게 아니라 발톱, 콧구멍만 보는 기분이다. 그래도 여러번 가까이서 보고 멀리서 보고 반복하다 보니 대충 감은 잡히는 것 같다 감나무.