본문 바로가기

딥러닝 학습

8장 이미지 분류 - 합성곱 신경망 (4)

반응형

8-4 합성곱 신경망을 만들고 훈련

- 합성곱 신경망의 전체 구조

- 합성곱 신경망의 정방향 계산 구현

1. 합성곱 적용

1) self.conv_w : 합성곱에 사용할 가중치

2) stride, padding : 특성 맵의 가로와 세로 크기를 일정하게 만들기 위해 각각 1, 'SAME'으로 지정

2. 렐루 함수 적용

3. 풀링 적용하고 완전 연결층 수정

def forpass(self, x):
  # 3x3 합성곱 연산 수행
  c_out = tf.nn.conv2d(x, self.conv_w, strides=1, padding='SAME') + self.conv_b
  # 렐루 활성화 함수 적용
  r_out = tf.nn.relu(c_out)
  # 2x2 최대 풀링 적용
  p_out = tf.nn.max_pool2d(r_out, ksize=2, strides=2, padding='VALID')
  # 첫 번째 배치 차원을 제외하고 출력을 일렬로 펼침
  f_out = tf.reshape(p_out, [x.shape[0], -1])
  z1 = tf.matmul(f_out, self.w1) + self.b1     # 첫 번째 층의 선형 식을 계산
  a1 = tf.nn.relu(z1)                          # 활성화 함수를 적용
  z2 = tf.matmul(a1, self.w2) + self.b2        # 두 번째 층의 선형 식을 계산
  return z2

- 합성곱 신경망의 역방향 계산 구현

자동 미분(automatic differentiation) 기능 사용

x = tf.Variable(np.array([1.0, 2.0, 3.0]))
with tf.GradientTape() as tape:
  y = x**3 + 2 * x +5

print(tape.gradient(y, x))


##출력: tf.Tensor([ 5. 14. 29.], shape=(3,), dtype=float64)

1. 역방향 계산 구현하기

2. 그레이디언트 계산하기

x = tf.Variable(np.array([1.0, 2.0, 3.0]))
with tf.GradientTape() as tape:
    y = tf.nn.softmax(x)

# 그래디언트를 계산합니다.
print(tape.gradient(y, x))


##출력: tf.Tensor([1.99908031e-17 5.43406367e-17 1.47713165e-16], shape=(3,), dtype=float64)
def training(self, x, y):
  m = len(x)                    # 샘플 개수를 저장합니다.
  with tf.GradientTape() as tape:
      z = self.forpass(x)       # 정방향 계산을 수행합니다.
      # 손실을 계산합니다.
      loss = tf.nn.softmax_cross_entropy_with_logits(y, z)
      loss = tf.reduce_mean(loss)

  weights_list = [self.conv_w, self.conv_b,
                  self.w1, self.b1, self.w2, self.b2]
  # 가중치에 대한 그래디언트를 계산합니다.
  grads = tape.gradient(loss, weights_list)
  # 가중치를 업데이트합니다.
  self.optimizer.apply_gradients(zip(grads, weights_list))
반응형

- 옵티마이저 객체를 만들어서 가중치 초기화

1. fit() 메서드 수정

def fit(self, x, y, epochs=100, x_val=None, y_val=None):
  self.init_weights(x.shape, y.shape[1])    # 은닉층과 출력층의 가중치를 초기화합니다.
  self.optimizer = tf.optimizers.SGD(learning_rate=self.lr)
  # epochs만큼 반복합니다.
  for i in range(epochs):
      print('에포크', i, end=' ')
      # 제너레이터 함수에서 반환한 미니배치를 순환합니다.
      batch_losses = []
      for x_batch, y_batch in self.gen_batch(x, y):
          print('.', end='')
          self.training(x_batch, y_batch)
          # 배치 손실을 기록합니다.
          batch_losses.append(self.get_loss(x_batch, y_batch))
      print()
      # 배치 손실 평균내어 훈련 손실 값으로 저장합니다.
      self.losses.append(np.mean(batch_losses))
      # 검증 세트에 대한 손실을 계산합니다.
      self.val_losses.append(self.get_loss(x_val, y_val))

2. init_weights()메서드 수정

def init_weights(self, input_shape, n_classes):
  g = tf.initializers.glorot_uniform()
  self.conv_w = tf.Variable(g((3, 3, 1, self.n_kernels)))
  self.conv_b = tf.Variable(np.zeros(self.n_kernels), dtype=float)
  n_features = 14 * 14 * self.n_kernels
  self.w1 = tf.Variable(g((n_features, self.units)))          # (특성 개수, 은닉층의 크기)
  self.b1 = tf.Variable(np.zeros(self.units), dtype=float)    # 은닉층의 크기
  self.w2 = tf.Variable(g((self.units, n_classes)))           # (은닉층의 크기, 클래스 개수)
  self.b2 = tf.Variable(np.zeros(n_classes), dtype=float)     # 클래스 개수

- glorot_uniform()

init_weights() 메서드의 glorot_uniform() 함수는 가중치를 촉화 할 때 그로럿(Glorot) 초기화를 사용할 수 있게 함

※ glorot_uniform()은 GlorotUniform 클래스의 별명으로 클래스를 함수처럼 호출한 것

-> 신경망 모델이 너무 커지면 손실 함수도 복잡해져서 출발점에 따라 결과가 달라질 수 있음

1차원 손실함수에서 경사 하강법은 출발점으로부터 기울기가 0이 되는 최저점 찾음

-> 가중치를 적절히 초기화하지 않으면 왼쪽 그래프처럼 지역 최적점 찾음

-> 가중치를 적절히 초기화하면 오른쪽 그래프처럼 전역 최적점 찾음

 

글로럿 초기화 방식으로 가중치 초기화

텐서플로의 glorot_uniform()은 위의 두 수 사이에서 균등하게 난수를 발생시켜 가중치 초기화

 

※ 전체 코드

class ConvolutionNetwork:
    
    def __init__(self, n_kernels=10, units=10, batch_size=32, learning_rate=0.1):
        self.n_kernels = n_kernels  # 합성곱의 커널 개수
        self.kernel_size = 3        # 커널 크기
        self.optimizer = None       # 옵티마이저
        self.conv_w = None          # 합성곱 층의 가중치
        self.conv_b = None          # 합성곱 층의 절편
        self.units = units          # 은닉층의 뉴런 개수
        self.batch_size = batch_size  # 배치 크기
        self.w1 = None              # 은닉층의 가중치
        self.b1 = None              # 은닉층의 절편
        self.w2 = None              # 출력층의 가중치
        self.b2 = None              # 출력층의 절편
        self.a1 = None              # 은닉층의 활성화 출력
        self.losses = []            # 훈련 손실
        self.val_losses = []        # 검증 손실
        self.lr = learning_rate     # 학습률

    def forpass(self, x):
        # 3x3 합성곱 연산을 수행합니다.
        c_out = tf.nn.conv2d(x, self.conv_w, strides=1, padding='SAME') + self.conv_b
        # 렐루 활성화 함수를 적용합니다.
        r_out = tf.nn.relu(c_out)
        # 2x2 최대 풀링을 적용합니다.
        p_out = tf.nn.max_pool2d(r_out, ksize=2, strides=2, padding='VALID')
        # 첫 번째 배치 차원을 제외하고 출력을 일렬로 펼칩니다.
        f_out = tf.reshape(p_out, [x.shape[0], -1])
        z1 = tf.matmul(f_out, self.w1) + self.b1     # 첫 번째 층의 선형 식을 계산합니다
        a1 = tf.nn.relu(z1)                          # 활성화 함수를 적용합니다
        z2 = tf.matmul(a1, self.w2) + self.b2        # 두 번째 층의 선형 식을 계산합니다.
        return z2
    
    def init_weights(self, input_shape, n_classes):
        g = tf.initializers.glorot_uniform()
        self.conv_w = tf.Variable(g((3, 3, 1, self.n_kernels)))
        self.conv_b = tf.Variable(np.zeros(self.n_kernels), dtype=float)
        n_features = 14 * 14 * self.n_kernels
        self.w1 = tf.Variable(g((n_features, self.units)))          # (특성 개수, 은닉층의 크기)
        self.b1 = tf.Variable(np.zeros(self.units), dtype=float)    # 은닉층의 크기
        self.w2 = tf.Variable(g((self.units, n_classes)))           # (은닉층의 크기, 클래스 개수)
        self.b2 = tf.Variable(np.zeros(n_classes), dtype=float)     # 클래스 개수
        
    def fit(self, x, y, epochs=100, x_val=None, y_val=None):
        self.init_weights(x.shape, y.shape[1])    # 은닉층과 출력층의 가중치를 초기화합니다.
        self.optimizer = tf.optimizers.SGD(learning_rate=self.lr)
        # epochs만큼 반복합니다.
        for i in range(epochs):
            print('에포크', i, end=' ')
            # 제너레이터 함수에서 반환한 미니배치를 순환합니다.
            batch_losses = []
            for x_batch, y_batch in self.gen_batch(x, y):
                print('.', end='')
                self.training(x_batch, y_batch)
                # 배치 손실을 기록합니다.
                batch_losses.append(self.get_loss(x_batch, y_batch))
            print()
            # 배치 손실 평균내어 훈련 손실 값으로 저장합니다.
            self.losses.append(np.mean(batch_losses))
            # 검증 세트에 대한 손실을 계산합니다.
            self.val_losses.append(self.get_loss(x_val, y_val))

    # 미니배치 제너레이터 함수
    def gen_batch(self, x, y):
        bins = len(x) // self.batch_size                   # 미니배치 횟수
        indexes = np.random.permutation(np.arange(len(x))) # 인덱스를 섞습니다.
        x = x[indexes]
        y = y[indexes]
        for i in range(bins):
            start = self.batch_size * i
            end = self.batch_size * (i + 1)
            yield x[start:end], y[start:end]   # batch_size만큼 슬라이싱하여 반환합니다.
            
    def training(self, x, y):
        m = len(x)                    # 샘플 개수를 저장합니다.
        with tf.GradientTape() as tape:
            z = self.forpass(x)       # 정방향 계산을 수행합니다.
            # 손실을 계산합니다.
            loss = tf.nn.softmax_cross_entropy_with_logits(y, z)
            loss = tf.reduce_mean(loss)

        weights_list = [self.conv_w, self.conv_b,
                        self.w1, self.b1, self.w2, self.b2]
        # 가중치에 대한 그래디언트를 계산합니다.
        grads = tape.gradient(loss, weights_list)
        # 가중치를 업데이트합니다.
        self.optimizer.apply_gradients(zip(grads, weights_list))
   
    def predict(self, x):
        z = self.forpass(x)                 # 정방향 계산을 수행합니다.
        return np.argmax(z.numpy(), axis=1) # 가장 큰 값의 인덱스를 반환합니다.
    
    def score(self, x, y):
        # 예측과 타깃 열 벡터를 비교하여 True의 비율을 반환합니다.
        return np.mean(self.predict(x) == np.argmax(y, axis=1))

    def get_loss(self, x, y):
        z = self.forpass(x)                 # 정방향 계산을 수행합니다.
        # 손실을 계산하여 저장합니다.
        loss = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(y, z))
        return loss.numpy()

 

 

 

 

 

※ 해당 내용은 <Do it! 딥러닝 입문>의 내용을 토대로 학습하며 정리한 내용입니다.

반응형