[도서완독]Hands On Machine Learning

[HandsOn]12. 텐서플로를 사용한 사용자 정의 모델과 훈련 - 연습문제

입짧은달님 2022. 2. 15. 17:14

https://github.com/rickiepark/handson-ml2/blob/master/12_custom_models_and_training_with_tensorflow.ipynb

 

GitHub - rickiepark/handson-ml2: 핸즈온 머신러닝 2/E의 주피터 노트북

핸즈온 머신러닝 2/E의 주피터 노트북. Contribute to rickiepark/handson-ml2 development by creating an account on GitHub.

github.com

답안은 여기를 참고하였다!

정말 어렵지만 내맘대로 커스텀한 모델을 짜기 위해선 필수적이니 꼭 알아두도록 하자. 

절대 첫 번째에 익숙해지지 않으니 보고 또 볼 것!

 

12. 사용자 정의 모델과 훈련 알고리즘

1. 사용자 정의 손실 함수

후버 손실(keras.losses.Huber)을 없다고 생각해보고 짜도록 하자.

이 때 모델을 저장하고 로드하는 것까지 생각해야 한다.

 

후버 손실에서 threshold를 매개변수로 받아야 한다면? ( 1.0 이하면 이 함수를 쓰고, 이상이면 이 함수를 써라 이런식으로 커스텀을 하고 싶다면) 

1. keras.losses.Loss 클래스를 상속

2. get_config() 메서드를 구현

하여 해결할 수 있다. ( 이해가 안되므로 외우자 그냥 )

 

import tensorflow as tf
import keras

class HuberLoss(keras.losses.Loss):
    def __init__(self,threshold=1.0,**kwargs):
        self.threshold=threshold
        super().__init__(**kwargs)
    def call(self, y_true, y_pred):
        error=y_true-y_pred
        is_small_error=tf.abs(error) < self.threshold
        squared_loss=tf.square(error)/2
        linear_loss=self.threshold*tf.abs(error)-self.threshold**2/2
        return tf.where(is_small_error,squared_loss,linear_loss)
        def get_config(self):
            base_config=super().get_config()
            return {**base_config,"threshold":self.threshold}

생성자에서 기본적인 하이퍼 파라미터를 상속을 받고, threshold도 지정을 해 줌.

call 메서드에서 손실 계산하여 반환.

get_config 메서드는 모르겠당.. 그냥 매개변수 받을 때 갖다 붙이는듯... 

 

이렇게 해 놓으면 모델을 컴파일할때 이 클래스의 인스턴스를 사용할 수 있다. 

모델을 저장할 때 threshold도 같이 저장되고, 로드할 때 클래스 이름과 클래스를 매핑해 주면 된다. 

 

2. 사용자 정의 손실 지표

비슷함.. (지금까지 해 왔던 것처럼)지표를 정의만 하면 케라스가 자동으로 함수 호출하고 에포크동안 평균 기록해줌. 

클래스를 일일히 다 정의해주는 거는

1. 매개변수를 저장할 때!

2. 배치가 계속될 때마다 전체 배치를 포함하는 '정밀도' 같은 '스트리밍 지표'를 구현할 때!

 

3. 사용자 정의 층

텐서플로에 없는 특이한 층을 가진 네트워크를 만들어야 할 때가 있음!

가중치를 가진 층을 만들려면 keras.layers.Layer를 상속해야 한다. 

class MyDense(keras.layers.Layer):
    def __init__(self, units, activation=None, **kwargs):
        super().__init__(**kwargs)
        self.units = units
        self.activation = keras.activations.get(activation)

    def build(self, batch_input_shape):
        self.kernel = self.add_weight(
            name="kernel", shape=[batch_input_shape[-1], self.units],
            initializer="glorot_normal")
        self.bias = self.add_weight(
            name="bias", shape=[self.units], initializer="zeros")
        super().build(batch_input_shape) # must be at the end

    def call(self, X):
        return self.activation(X @ self.kernel + self.bias)

    def compute_output_shape(self, batch_input_shape):
        return tf.TensorShape(batch_input_shape.as_list()[:-1] + [self.units])

    def get_config(self):
        base_config = super().get_config()
        return {**base_config, "units": self.units,
                "activation": keras.activations.serialize(self.activation)}

1. 생성자는 모든 하이퍼 파라미터를 매개변수로 받음. 부모 생성자 호출하고, 하이퍼파라미터를 속성으로 저장.

2. build() 메서드의 역할은 가중치를 만드는 것.

   "kernel": 연결 가중치(입력의 마지막 차원 크기를 받음) ,

   "bias" : 편향, 0으로 초기화

3. compute_output_shape(): 이 층의 출력 크기를 반환 

4. get_config(): 앞서 보았던 것과 같음. keras.activations.serialize()를 사용하여 활성화 함수의 전체 설정 저장

model = keras.models.Sequential([
    MyDense(30, activation="relu", input_shape=input_shape),
    MyDense(1)
])

 

4. 사용자 정의 모델

 

한 마디로, keras.Model 클래스를 상속하여 생성자에서 층과 변수를 만들고 모델이 해야 할 작업을 call()메서드에 구현한다. 

이런 모델이 있다고 가정: 스킵 연결이 있는 ResidualBlock 층을 가진 예제 모델

먼저 Residual Block 층을 만들자.

 

class ResidualBlock(keras.layers.Layer):
    def __init__(self, n_layers, n_neurons, **kwargs):
        super().__init__(**kwargs)
        self.hidden = [keras.layers.Dense(n_neurons, activation="elu",
                                          kernel_initializer="he_normal")
                       for _ in range(n_layers)]

    def call(self, inputs):
        Z = inputs
        for layer in self.hidden:
            Z = layer(Z)
        return inputs + Z

층 안에 층이 포함되어 있음 !

hidden 에 층을 레이어 갯수만큼 지정해줌.

그 다음에 call()메서드에서 Z를 hidden 에 통과시키고, input 이랑 더해준다. 

 

ResidualBlock 층 완성!

 

해당 층을 이용해서 모델을 만든다. 

class ResidualRegressor(keras.models.Model):
    def __init__(self, output_dim, **kwargs):
        super().__init__(**kwargs)
        self.hidden1 = keras.layers.Dense(30, activation="elu",
                                          kernel_initializer="he_normal")
        self.block1 = ResidualBlock(2, 30)
        self.block2 = ResidualBlock(2, 30)
        self.out = keras.layers.Dense(output_dim)

    def call(self, inputs):
        Z = self.hidden1(inputs)
        for _ in range(1 + 3):
            Z = self.block1(Z)
        Z = self.block2(Z)
        return self.out(Z)

모델을 상속받고, 생성자에 필요한 층을 정의하고( 내맘대로 적고있음 )

call() 메서드에 모델의 형태를 정의한다. 

모델 완성!

 

5.  자동 미분을 사용하여 그레이디언트 계산하기

간단한 함수로 예를 살펴보자.

def f(w1,w2):
    return 3*w1**2 + 2*w1*w2

자동 미분을 사용해 텐서플로에서 쉽게 그레이디언트를 계산해보자. 

w1,w2=tf.Variable(5.),tf.variable(3.)
with tf.GradientTape() as tape:
    z=f(w1,w2)

gradients=tape.gradient(z,[w1,w2])
gradients
Out[172]:
[<tf.Tensor: shape=(), dtype=float32, numpy=36.0>,
 <tf.Tensor: shape=(), dtype=float32, numpy=10.0>]

포인트 (5,3)에서의 그레이디언트 벡터가 (36,10) 으로 계산되었다!

 

6.  사용자 정의 훈련 반복

만약에 모델을 피팅할때, fit()의 유연성이 충분하지 않다면? (예를 들어 fit()메서드에서는 compile할 때 하나의 옵티마이저만 지원하는데, 두 개의 다른 옵티마이저를 레이어마다 쓰고 싶다면)

 

훈련 반복을 직접 구현해야 함! 중요한데 넘나어려운것...

 

간단한 모델을 만들어보자. 훈련 반복을 직접 다루기 때문에 컴파일할 필요는 없음.

그냥 간단한 시퀀셜 모델을 만들었다.

l2_reg = keras.regularizers.l2(0.05)
model = keras.models.Sequential([
    keras.layers.Dense(30, activation="elu", kernel_initializer="he_normal",
                       kernel_regularizer=l2_reg),
    keras.layers.Dense(1, kernel_regularizer=l2_reg)
])

훈련 세트에서 샘플 배치를 랜덤하게 추출하는 함수를 만들자.

def random_batch(X, y, batch_size=32):
    idx = np.random.randint(len(X), size=batch_size)
    return X[idx], y[idx]

훈련 상태를 출력하는 함수도 만든다. (사실 내가만든건 아니고 책에 적혀있는거임)

간편하게 tqdm 라이브러리를 쓸 수도 있음.

def print_status_bar(iteration, total, loss, metrics=None):
    metrics = " - ".join(["{}: {:.4f}".format(m.name, m.result())
                         for m in [loss] + (metrics or [])])
    end = "" if iteration < total else "\n"
    print("\r{}/{} - ".format(iteration, total) + metrics,
          end=end)

 

이제 몇 개의 하이퍼파라미터를 정의하고 옵티마이저, 손실 함수, 지표를 선택해야 함.

n_epochs = 5
batch_size = 32
n_steps = len(X_train) // batch_size
optimizer = keras.optimizers.Nadam(learning_rate=0.01)
loss_fn = keras.losses.mean_squared_error
mean_loss = keras.metrics.Mean()
metrics = [keras.metrics.MeanAbsoluteError()]

 

사용자 정의 훈련 반복문: print status bar를 사용하지 않고 tqdm을 사용하였다.

try:
    from tqdm.notebook import trange
    from collections import OrderedDict
    with trange(1, n_epochs + 1, desc="All epochs") as epochs:
        for epoch in epochs:
            with trange(1, n_steps + 1, desc="Epoch {}/{}".format(epoch, n_epochs)) as steps:
                for step in steps:
                    X_batch, y_batch = random_batch(X_train_scaled, y_train)
                    with tf.GradientTape() as tape:
                        y_pred = model(X_batch)
                        main_loss = tf.reduce_mean(loss_fn(y_batch, y_pred))
                        loss = tf.add_n([main_loss] + model.losses)
                    gradients = tape.gradient(loss, model.trainable_variables)
                    optimizer.apply_gradients(zip(gradients, model.trainable_variables))
#                    for variable in model.variables:
#                        if variable.constraint is not None:
#                            variable.assign(variable.constraint(variable))   
모델에 가중치 제한을 추가한 항.근데 잘 모르겠어서 주석처리함
                    status = OrderedDict()
                    mean_loss(loss)
                    status["loss"] = mean_loss.result().numpy()
                    for metric in metrics:
                        metric(y_batch, y_pred)
                        status[metric.name] = metric.result().numpy()
                    steps.set_postfix(status)
            for metric in [mean_loss] + metrics:
                metric.reset_states()
except ImportError as ex:
    print("To run this cell, please install tqdm, ipywidgets and restart Jupyter")

넘나 어려워보인다. 천천히 이해해보자. 

한 에포크 안에서 배치가 돌고 있다.

랜덤 배치 함수를 이용해서 X 배치와 y 배치를 지정하고, 

 그레이디언트 테이프를 이용해서 배치마다 나온 loss를 "훈련 가능한 변수" 로 미분한다. 

 이 값들을 optimizer에 보냄. 

 

status라는 dictionary를 만들고, 상태들을 여기에 저장.

한 에포크가 다 돌면 metric 을 리셋.

 

너무 어렵다...