본문 바로가기
Book Review/[케라스 창시자에게 배우는 딥러닝] 리뷰

텐서플로우를 통한 딥러닝 모델 직접 구현

by 3n952 2022. 12. 7.

앞선 포스트에서는 keras를 이용하여 딥러닝의 간단한 구조를 이해하고 모델을 만들어서

학습해보았습니다.

 

이번 포스트에서는 그 과정을 직접 코드로 구현하여 보다 완전하고 명확하게 딥러닝의 구조를 이해해보고자 합니다.

기본적인 텐서연산과 역전파는 직접 구현하진 않지만 케라스의 기능을 사용하지 않는 저수준의 구현을 해보겠습니다.

이번에 나오는 내용이 이해가 안되더라도 계속해서 포스트에서 설명할 것이기 때문에 일일히 모든 것을 이해할 필요는 없습니다.

다만, 각각의 구체적인 구현에서 어떠한 부분이 핵심 내용인지 짚고 넘어가는 것은 필수적입니다.


 

우선 단순한 dense클래스를 구현해보겠습니다.

output = activation(dot(W, input) + b)의 식을 기억하실겁니다.

W는 가중치, b는 편향으로 모델 파라미터입니다.

dense층에서는 이 내용이 이뤄져야합니다.

 

간단한 파이썬 클래스를 구현하여 해당 부분을 구현해보겠습니다.

코드는 다음과 같습니다.

import tensorflow as tf

class NaiveDense:
  def __init__(self, input_size, output_size, activation):
    self.activation = activation

    w_shape = (input_size, output_size)
    w_initial_value = tf.random.uniform(w_shape, minval = 0, maxval = 1e-1)
    self.W = tf.Variable(w_initial_value)

    b_shape = (output_size,)
    b_initial_value = tf.zeros(b_shape)
    self.b = tf.Variable(b_initial_value)
  
  def __call__(self, inputs):
    return self.activation(tf.matmul(inputs,self.W)+self.b)
  
  @property
  def weights(self):
    return [self.W, self.b]

naivedense 클래스를 구현하여 2개의 텐서플로 변수 w,b를 만들고 __call__()메서드에서 앞서 언급한 변환(위에 밑줄친 부분)을 적용해보았습니다.

 

0~0.1사이의 값을 랜덤으로 초기화하여 (input_size, output_size)크기의 행렬W를 만들었습니다.

0의 값으로 초기화하여 (output_size,)의 크기의 행렬b를 만들었습니다.

이후 __call__()메서드에서 정향향 패스를 수행하여 앞선 변환 식이 계산됩니다.

마지막으로는 층의 가중치를 추출하기위해 weight()메소드를 추가해주었습니다.

 

 

이제는 keras.Sequential()메서드 부분을 구현해보겠습니다.

층의 리스트를 받고 층을 순서대로 호출하는 부분입니다. 층의 파라미터를 쉽게 구할 수 있도록 weights속성을 제공하는 부분도 구현해보겠습니다.

 

코드는 다음과 같습니다.

class NaiveSequential:
  def __init__(self, layers):
    self.layers = layers
  
  def __call__(self, inputs):
    x = inputs
    for layer in self.layers:
      x = layer(x)
    
  @property
  def weights(self):
    weights = []
    for layer in self.layers:
      weights += layer.weights
    return weights

 

데이터를 미니 배치로 순회할 방법을 만들어줘야합니다.

이를 배치 제너레이터라고 합니다.

코드는 다음과 같습니다.

import math

class BatchGenerator:
  def __init__(self, images, labels, batch_size = 128):
    assert len(images) == len(labels)
    self.index = 0
    self.images = images
    self.labels = labels
    self.batch_size = batch_size
    self.num_batches = math.ceil(len(images) / batch_size) #실수를 반올림하여 정수로 만들었다.
  
  def next(self):
    images = self.images[self.index:self.index + self.batch_size]
    labels = self.labels[self.index:self.index + self.batch_size]
    self.index += self.batch_size
    return images, labels

 

미니배치로 나눠준 것은 훈련을 나눠서 하기 위함입니다. 이는 한 배치 데이터에서 모델을 실행하고 가중치를 업데이트 할 수 있게 합니다.

즉 배치별로 계속해서 훈련이 이뤄지고 가중치가 업데이트되는 것입니다. 

정리하면, 

1. 배치에 있는 이미지에 대해 모델의 예측을 계산한다.

2. 실제 레이블을 사용하여 이 예측의 손실 값을 계산한다.

3. 모델 가중치에 대한 손실의 그레이디언트를 계산한다.

4. 이 그레이디언트의 반대 방향으로 가중치를 조금 이동한다.

 

해당 기능을 함수로 구현해보겠습니다.

코드는 다음과 같습니다.

def one_training_step(model, images_batch, labels_batch):
  with tf.GradientTape() as tape: #텐서플로우의 gradienttape객체사용: 그레이디언트 계산을 위해서
    #정방향 패스를 실행. gradienttape블록 안에서 모델의 예측을 계산한다.
    predictions = model(image_batch)
    per_sameple_losses = tf.keras.losses.sparse_categorical_crossentropy(labels_batch, predictions)
    average_loss = tf.reduce_mean(per_sample_losses)
  #가중치에 대한 손실의 그레이디언트를 계산  
  gradients = tape.gradient(average_loss, model.weights)
  update_weights(gradients, model.weights) #update_weights함수사용. 구현코드는 이후에 나옴.
  return average_loss

 

update_weights함수는 배치의 손실을 감소시키기위해 반대 방향으로 가중치를 '조금' 이동하는 것입니다.

학습률에 의해 '조금'의 정도가 결정되는 것입니다.

update_weight함수 구현하는 간단한 방법은 각 가중치에서 gradient * learning_rate(학습률)을 빼는 것입니다.

구현 코드는 다음과 같습니다.

learning_rate = 1e-3 # (= 0.001)

def update_weights(gradients, weights):
  for g, w in zip(gradients, weights):
    w.assign_sub(g*learning_rate) #텐서플로 변수의 assign_sub는 -=연산과 같음.

 

훈련 에포크는 훈련 데이터의 각 배치에 대한 훈련 스텝을 반복하는 것입니다. 에포크 1은 모든 데이터를 한번 훑었다는 것을 의미합니다.

이번에는 에포크를 사용하여 전체 훈련 루프를 구현해보겠습니다.

코드는 다음과 같습니다.

def fit(model, images, labels, epochs, batch_size=128):
  for epoch_counter in range(epochs):
    print(f'에포크 {epoch_countet}')
    batch_generator = BatchGenerator(images, labels)
    for batch_counter in range(batch_generator.num_batchs):
      images_batch, labels_batch = batch_generator.next()
      loss = one_training_step(model, images_batch, labels_batch)
      if batch_counter % 100 == 0:
        print(f"{batch_counter}번째 배치 손실: {loss:.2f}")

 

 

이번 포스팅에서는 수동으로 keras API를 직접 구현해봤습니다.

케라스에서는 단지 몇 단어, 몇 줄의 코드로 할 수 있는 작업이 이렇게 길게 표현됩니다.

심지어 아주 간단하게 표현한 것이므로 케라스가 얼마나 편하고 성능이 좋은 도구인지 다시 한 번 알게해줍니다.

이렇게 직접구현하는 것은 신경망 안에서 어떠한 코드가, 일이 일어나는지 이해하는데 큰 도움이 될 것입니다.