본문 바로가기
파이썬 프로그래밍/Numpy 딥러닝

28. [CNN기초] 1차원 배열 CNN 훈련하기-2(배치구현)

by Majestyblue 2023. 1. 4.

27번 예제에서 1개의 입력에 대해 1개의 필터로 훈련하였다면

3개의 입력에 대해서 1개의 필터로 훈련한다면 어떨까?

추가로 hyper parameter를 엄밀하게 정의해 보겠다.

 

 

 

1. 변수 정의하기

먼저 입력과 출력을 정의해 보자. 입력 X는 (3, 5) 배열이고 목표 Y는 (3, 3) 배열이다.

입력값 X와 목표값 Y

 

 

 

 

 

 

 

 입력값을 matplotlib로 이미지화 한다면 아래와 같다.

1에 가까울 수록 밝고, 0에 가까울 수록 어둡다.

 

 

 

 

 

 

가중치 W과 편향 B 설정이다.

정해진 값이 아닌 numpy 랜덤 값을 이용하여 생성해 주자. 

np.random.seed를 설정하여 매번 실행할 때 마다 같은 결과를 얻을 수 있도록 하자.

 

 

 

 

 

 

 

 

2. 합성곱 결과 확인하기(특성맵)

 특성맵(feature map)이란, 어떤 이미지를 합성곱 한 결과로써 이미지의 '특성'을 담고 있다.

 아래 공식을 이용하면 합성곱 결과 어떤 크기의 특성맵이 출력되는지 알 수 있다.

 

 

일반적으로 높이와 너비는 같은 size로 두고

높이와 너비에 대한 패딩P와 스트라이드S도 같게 둔다.

 

이를 이용하여, 한 개의 입력 (5, ) 필터 (3, )에 대한 특성맵 출력 크기를 구해보자.

Out = ((5 - 3 + 2×0)/1 + 1 = 3

27번 예제의 결과와 동일하다.

따라서 3개의 배치로 이루어진 입력 X에 대한 합성곱 결과는 아래와 같다.

 

 

 

위 식을 코드로 표현하면 아래와 같다.

특성맵 출력 크기를 fmap_width로 지정하였다.

왜냐면 1차원 연산이라 height은 필요 없기 때문이다.

 

 

 

 

 

 

3. 배치 입력 처리를 위한 함수 재정의하기

합성곱 연산 과정을 다시 살펴보고 hyper parameter로 설정하자.

주목해야 할 것은 i, pred 배열의 요소 개수, j이다.

 

 

1) 합성곱 함수 재정의하기

 

i는 합성곱 연산 횟수를 의미하고

pred는 합성곱 연산 결과 배열을 의미하므로

i값과 pred 요소 개수는 fmap_width이 되고

 

j는 필터 요소 개수를 의미하므로 weight.shape[0]이 된다.

 

이를 이용하여 배치단위의 합성곱을 수행하는 함수 Conv1D를 정의하여 보자.

 

def Conv1D(input, weight, bias):
  conv = np.zeros(fmap_width,)
  for i in range(fmap_width):
    a = 0
    for j in range(weight.shape[0]):
      a += input[j+i]*weight[j]
    conv[i] = a

  return conv + bias

 

 

아래 이전에 사용했던 함수와 hyper parameter를 비교하여 보자, 이렇게 정리하는 편이 일반화에 좋다.

-> 그러니까, 함부로 숫자를 넣지 않고 hyper parameter로 정의하자 이 뜻이죠.

def forward(input, weight, bias):
  conv = np.zeros(3,)
  for i in range(3):
    a = 0
    for j in range(3):
      a += input[j+i]*W[j]
    conv[i] = a

  return conv + bias

 

 

 

 

 

2) 순전파 함수 재정의하기

def forward(inputs):
  count = 0
  for input in inputs:
    out = Conv1D(input, W, B)
    out = out[np.newaxis, :]
    if count == 0:
      outs = out.copy()
    else:
      outs = np.concatenate((outs, out), axis=0)
    count += 1
  return outs

print(forward(X))

 계산 과정을 그림과 글로 한단계씩 살펴보자.

 

 

 

 

 

 

2) 합성곱 역전파 함수 재정의하기

 아래 그림에서와 같이 기울기를 담을 grad_conv함수는 W의 크기와 같아야 하므로 weight.shape[0]이 되고

반복하는 i는 필터 개수에 대응하므로 마찬가지로 weight.shape[0]이 된다.

j는 특성맵 배열 요소 개수에 대응하므로 fmap_width가 된다. 

def gradient_conv (input, weight):
  grad_conv = np.zeros(weight.shape[0],)
  for i in range(weight.shape[0]):
    a = 0
    for j in range(fmap_width):
      a += input[j+i]
    grad_conv[i] = a

  return grad_conv

 

 

 

 

 

배치단위로 합성곱 역전파를 수행하는 함수의 재구성이다.

순전파를 재구성한 함수에서 

out = Conv1D(input, W, B) 가
out = gradient_conv(input, wieght) 으로 대체된 것 빼고는
코드와 원리는 거의 그대로이다. 
def backward_conv(inputs, weight):
  count = 0
  for input in inputs:
    out = gradient_conv(input, weight)
    out = out[np.newaxis, :]
    if count == 0:
      outs = out.copy()
    else:
      outs = np.concatenate((outs, out))
    count += 1
  return outs

print(backward_conv(X, W))

 

 

 

 

그 외에 오차함수, 전체 역전파 함수, 학습 과정은 크게 달라지는 것은 없다.

아래는 전체 실행 코드이다.

import numpy as np
import matplotlib.pyplot as plt

X = np.array([[1., 0.7, 0.5, 0.3, 0.1],
              [0.5, 0.7, 1, 0.7, 0.5],
              [0.1, 0.3, 0.5, 0.7, 1.]])
Y = np.array([[1., 0., 0.],
              [0., 1., 0.],
              [0., 0., 1.]])

fmap_width = 3 # ((5-3+2*0)/1 + 1)

np.random.seed(221222)
W = np.random.randn(3, )
B = np.random.randn(1, )

#W = np.array([1., 1., 1.])
#B = np.array([0.5])
#기회가 되면 주석으로 처리한 가중치와 편향으로 실행해 보길 바란다.

def Conv1D(input, weight, bias):
  conv = np.zeros(fmap_width,)
  for i in range(fmap_width):
    a = 0
    for j in range(weight.shape[0]):
      a += input[j+i]*weight[j]
    conv[i] = a

  return conv + bias

def forward(inputs):
  count = 0
  for input in inputs:
    out = Conv1D(input, W, B)
    out = out[np.newaxis, :]
    if count == 0:
      outs = out.copy()
    else:
      outs = np.concatenate((outs, out), axis=0)
    count += 1
  return outs

def loss(pred, target):
  losses = np.mean(np.power((pred - target), 2))
  return losses

def gradient_conv (input, weight):
  grad_conv = np.zeros(weight.shape[0],)
  for i in range(weight.shape[0]):
    a = 0
    for j in range(fmap_width):
      a += input[j+i]
    grad_conv[i] = a

  return grad_conv

def backward_conv(inputs, weight):
  count = 0
  for input in inputs:
    out = gradient_conv(input, weight)
    out = out[np.newaxis, :]
    if count == 0:
      outs = out.copy()
    else:
      outs = np.concatenate((outs, out))
    count += 1
  return outs

def loss_gradient(input, target, pred):
  dL_dY = 2*(pred-target) # (batch, 3) - (batch, 3)
  #print('dL_dY :', dL_dY)

  dY_dW = backward_conv(input, W)
  #print('dY_dW :', dY_dW) # (batch, 3)

  dL_dW = np.sum(dL_dY * dY_dW, axis=0) # (batch, 3) * (batch, 3) 열을 기준으로 1, 2, 3행을 내용을 더함
  #print('dL_dW :', dL_dW)

  dL_dB = np.sum(dL_dY)
  #print('dL_dB', dL_dB)

  return dL_dW, dL_dB

learning_rate = 0.01
epochs = 200

for epoch in range(epochs+1):
  predict = forward(X)
  
  dL_dW, dL_dB = loss_gradient(X, Y, predict)

  W = W + -1*learning_rate*dL_dW
  B = B + -1*learning_rate*dL_dB

  if epoch % 20 == 0:
    print('epoch :', epoch, '\n', 'forward :' , '\n', forward(X))

 

 

epoch = 20마다 실행하였다. 결과는 어떠하였을까?

 

 

target인 Y와 비교하였을 때 잘 맞지 않는 것을 알 수 있다. 

이를 어떻게 해결할 수 있을까?

 

26번 첫 CNN 포스트에서 이미지 인식이 잘 안될 땐 필터 등 가중치를 늘리는 방법이 있다고 하였다.

다음시간에는 이를 구현해 보도록 하자.