오늘은 파이썬으로 기초 Convolution Neural Network(CNN) 구현에 대한 포스팅이다. 이전 글을 읽고 오면 이해하기 편할 것이다. 이번에 구현할 내용은 CNN을 구성하는 convolution layer와, pooling레이어 중 자주 쓰이는 maxpooling 레이어이다. 모든 구현은 python으로 이루어지며, numpy 라이브러리를 사용한다.
- 구현용 설명
- kernel
- stride
- padding
- conv img 사이즈
- pooling layer
- 코드
- Conv2D
- MaxPool2D
- 코드 확인
참고 : 이전 글
2022.03.09 - [머신러닝 with 파이썬] - 파이썬으로 기초 MLP 구현하기
파이썬으로 기초 MLP 구현하기
이번에는 기억을 되살려 tensorflow, pytorch를 사용하지 않고 파이썬만을 사용하여 Multi Layer Perceptron(MLP)를 구현해보도록 하겠다. 구현할 함수는 딱 4개밖에 없다. 구현할 것들 backpropagation 역전파 Me..
hi-lu.tistory.com
2022.03.27 - [머신러닝 with 파이썬] - 파이썬으로 기초 RNN 구현하기
파이썬으로 기초 RNN 구현하기
지난 포스트에서는 파이썬으로 인공신경망과 역전파 등을 포함해 MLP를 구현해 보았다. 이번에는 자연어 처리에서 많이 쓰였던 RNN신경망을 구현해보고자 한다. (요즘에는 트랜스포머가 모든 걸
hi-lu.tistory.com
2022.04.03 - [머신러닝 with 파이썬] - 파이썬과 기초 딥러닝 개념- 이론편 1
파이썬과 기초 딥러닝 개념- 이론편 1
이번 주제부터는 이론 편과 실전 편으로 나눠서 글을 작성하겠다. 파이썬과 numpy 라이브러리를 사용한다 가정하고 딥러닝에 대한 개념을 조금 잡고 지나가 보자. 인공신경망. neural network 활성화
hi-lu.tistory.com
1. 구현용 설명
1. kernel
컨벌루션 층은 합성곱을 의미한다. MLP, RNN구현에서 neural network(NN)의 형태는 입력값 x와 가중치를 곱하는 형태였다. CNN은 커널이란 이름의 가중치를 입력값의 일부와 곱한다. 예를 들어, 아래와 같이 사람 3명이 있는 1개 이미지가 있다. 이때 커널이 (3,3) 크기의 벡터라면 다음과 같이 이미지와 포갤 수 있다.
커널의 크기와 동일한 크기의 이미지 조각과 커널 벡터가 곱해진다. 커널과 곱하지 않은 이미지는? 이 또한 똑같이 새로운 커널과 곱해준다. 지금 이미지의 크기가 (32,32)이고 커널은 (3,3)으로 정했으니 총 몇 개의 커널이 있으면 연산을 완성할 수 있을까?
2. Stride
커널을 얼마나 듬성듬성 놓을 것인지에 대한 값이다. 아래 그림의 오른쪽 위(1)는 stride가 1일 때, 아래(2)는 stride가 3일 때의 그림이다.
아하, 그럼 stride를 고려하면 총 몇 개의 커널이 필요할지 알겠구나! 아니다. 한 가지 개념이 더 필요하다.
3. Padding
커널 사이즈는 (3,3)이고 stride는 3이라고 하자. 지금 이 이미지의 크기가 (32,32)이니 아래 그림처럼 커널을 할당하지 못하는 부분이 생긴다. 남은 부분의 길이는 2인데 커널의 길이는 3이니 말이다.
이럴 때 저 보라색 색칠된 곳에 값을 만들어주면 되지 않을까? 아래 그림의 분홍색 테두리처럼 값을 덧대어준다면 커널을 끝에도 적용할 수 있을 것이다. 이때 아무 값이나 테두리에 채워 넣어줄 수는 없다. 보통 딥러닝에서는 zero-padding, 0으로 이 부분을 채워준다.
지금 예시는 padding size가 1이었기 때문에 테두리를 각 변에 한 줄씩 추가해주었다.(W가 32에서 34로 변했다.) padding size가 3이라면? padding이 적용된 이미지의 크기는 (32 + 3*2 , 32 + 3*2 )이 되겠다.
4. Conv img 사이즈
그럼 이제 첫 번째 질문이었던 '총 몇 개의 커널이 있으면 연산을 완성할 수 있을까?'에 대답해 보자.
- 이미지의 길이는 32이다.
- kernel의 사이즈는 3이다.
- padding의 사이즈는 1이다. (-> 32 + 1 * 2)
- stride는 1이다.
(32 + 1*2 - 3) / 1 + 1 = 32
오, 한 줄 완성하는 데에 32개가 필요하겠다. 그러면 convolution layer를 통과한 출력 값은 (32,32)의 사이즈를 갖게 되겠구나! 이를 수식으로 풀어쓰면 다음과 같다.
(image_size + padding_size * 2 - kernel_size) / stride + 1
5. Pooling
하지만 기억하라. 우리는 합성곱을 적용해서 사이즈 (3,3)인 kernel에 각각 (3,3) 이미지 조각을 곱해주었다. 곱했기 때문에 해당 연산에서 나온 결괏값의 사이즈는 (3,3)이다. 때문에 컨볼루션 연산을 마친 이미지 사이즈는 (32,32)가 아닌 (32,32,3,3)이다. 이제 이런 의문이 들어야 한다. 합성곱을 해서 뭘 어쩔 건데?
Pooling층은 합성곱을 거친 결과 행렬에 대해 하나의 값을 도출해준다. pooling의 종류는 아래와 같은데, 이름에 맞는 연산을 한다.
- MinPooling : 행렬의 최솟값을 반환한다.
- AveragePooling : 행렬의 평균값을 반환한다.
- MaxPooling : 행렬의 최댓값을 반환한다.
아래는 pooling층의 kernel 사이즈가 (3,3) 일 때의 작동 예시다.
여기까지 했다면 우리는 CNN에서 진행하는 한 이미지의 특징을 추출하기 위한 과정을 모두 습득했다.
2. 구현 코드
구현에 앞서 activation 함수(ex. sigmoid, prelu)는 이전 포스트에 구현한 함수를 갖다 쓸 예정이다. 자 이제 Convolution layer와 MaxPooling layer를 구현해보자.
1. Conv2D 구현
먼저 컨벌루션을 거치면 h, w가 어떻게 변할지 정의해주자. 위의 수식에 맞게 코드를 짜주면 된다.
다시 보는 output 이미지 사이즈 크기
(image_size + padding_size * 2 - kernel_size) / stride + 1
class Conv2D:
def __init__(self, input_shape, kernel_size, padding = 0, stride = 1, act_fn = 'relu'):
self.batch_size, self.h, self.w = input_shape
self.kernel_size, self.padding, self.stride = kernel_size, padding, stride
#kernel size는 정사이즈로 가정. (kernel_size, kernel_size)
self.out_size_h = int(( self.h - self.kernel_size + 2 * padding ) / self.stride + 1)
self.out_size_w = int(( self.w - self.kernel_size + 2 * padding ) / self.stride + 1)
print(self.out_size_h, self.out_size_w)
다음으로 커널을 정의한다. random값으로 초기화해주자.
self.weight = np.random.rand(self.out_size_h, self.out_size_w,self.batch_size, self.kernel_size, self.kernel_size)
#구현상 편의를 위해 bias는 생략하도록 한다.
이제 Padding을 적용할 것이다. padding을 적용하면 이미지의 H, W는 아래와 같이 바뀌게 된다.
padding_h = h + padding_size * 2
padding_w = w + padding_size * 2
def _make_padding(self, x):
#padding은 zero padding으로 통일
new_x = np.zeros((self.batch_size, self.padding*2 + self.h , self.padding*2 + self.w ))
new_x[:, self.padding:self.h+self.padding, self.padding : self.w + self.padding] = x
return new_x
패딩을 적용한 만큼 새로운 이미지 크기를 정의해주었고, 원래 이미지의 값을 넣어주었다.
이제 stride를 고려하며 conv 연산을 수행하는 __call__ 함수를 추가하면 Conv2D 클래스 코드가 완성되었다.
class Conv2D:
def __init__(self, input_shape, kernel_size, padding = 0, stride = 1, act_fn = 'relu'):
self.batch_size, self.h, self.w = input_shape
self.kernel_size, self.padding, self.stride = kernel_size, padding, stride
#kernel size는 정사이즈로 가정. (kernel_size, kernel_size)
self.out_size_h = int(( self.h - self.kernel_size + 2 * padding ) / self.stride + 1)
self.out_size_w = int(( self.w - self.kernel_size + 2 * padding ) / self.stride + 1)
print(self.out_size_h, self.out_size_w)
if act_fn == 'sigmoid':
self.act_fn = Sigmoid()
else:
self.act_fn = PReLU()
self.weight = np.random.rand(self.out_size_h, self.out_size_w,self.batch_size, self.kernel_size, self.kernel_size)
#구현상 편의를 위해 bias는 생략하도록 한다.
def _make_padding(self, x):
#padding은 zero padding으로 통일
new_x = np.zeros((self.batch_size, self.padding*2 + self.h , self.padding*2 + self.w ))
new_x[:, self.padding:self.h+self.padding, self.padding : self.w + self.padding] = x
return new_x
def __call__(self, x):
# x는 이미지데이터.
conv_x = []
x = self._make_padding(x)
for h in range(0, self.out_size_h):
tmp = []
hs = h * self.stride
for w in range(0, self.out_size_w):
ws = w * self.stride
#print(hs+self.kernel_size, ws + self.kernel_size)
tmp.append(x[:,hs:hs+self.kernel_size, ws : ws + self.kernel_size] * self.weight[h][w])
conv_x.append(tmp)
conv_x = np.array(conv_x) # 현재 사이즈는 self.out_size_h, self.out_size_w, batch_size, kernel_size, kernel_size
#print(conv_x.shape)
#batch_size가 앞에 오도록 바꿔주자.
conv_x = np.transpose(conv_x, (2,0,1,3,4))
return np.array(conv_x)
2. MaxPool2D 구현
pooling 층 중에서 MaxPooling 층을 구현해보도록 하자. 좋은 생각이 안 나서 극악의 시간 복잡도를 가지는 for문이 탄생하게 되었다..
코드를 보면 pooling층의 kernel사이즈는 self.pooling_size로 받고 있다. MaxPool이기 때문에 np.max를 사용했다. 마찬가지로 min pool, average pooling 층을 구현하고 싶다면 np.min, np.mean을 사용하면 되겠다.
class MaxPool2D:
def __init__(self, pooling_size):
self.pooling_size = pooling_size
def __call__(self, conv_x):
batch_size, out_size_h, out_size_w, kernel_size, _ = conv_x.shape
new_kernel_size = kernel_size // self.pooling_size
pool_x = np.zeros((batch_size,out_size_h, out_size_w, new_kernel_size, new_kernel_size))
for b in range(batch_size): #극악의 시간복잡도!
for h in range(out_size_h):
for w in range(out_size_w):
for hk in range(new_kernel_size):
for wk in range(new_kernel_size):
pool_x[b,h,w,hk,wk] = np.max(conv_x[b,h,w,hk:hk+self.pooling_size, wk:wk+self.pooling_size].reshape(-1, self.pooling_size * self.pooling_size), axis=1)
pool_x = np.array(pool_x) #batch_size, out_size_h, out_size_w,kernel, kernel
pool_x = np.transpose(pool_x, (0,1,3,2,4))
pool_x = pool_x.reshape(batch_size, out_size_h * new_kernel_size, out_size_w * new_kernel_size)
return pool_x
3. 코드 확인
이제 코드가 잘 돌아가는지 확인해보자. 임의로 x라는 벡터를 만들어 conv2D와 MaxPool2D를 통과했을 때 사이즈를 출력해보겠다.
x = np.random.rand(10,32,32) # batch_size 10, H 32, W 32인 임의의 데이터
### conv2d와 maxpooling 층을 선언한다.
conv2d = Conv2D(x.shape, kernel_size= 3, padding = 1, stride = 1)
maxpool = MaxPool2D(pooling_size = 3)
conv_x = conv2d(x)
print(f'conv_x의 size는 {conv_x.shape}')
pool_x = maxpool(conv_x)
print(f'pooling을 거친 후 size는 {pool_x.shape}')
#### 출력값
#conv_x의 size는 (10, 32, 32, 3, 3)
#pooling을 거친 후 size는 (10, 32, 32)
잘 작동한다.
오늘은 Convolution layer와 MaxPooling layer를 구현해 보았다. 다음 포스트는 오늘 구현한 클래스에 역전파를 붙여서 실제로 cnn을 학습해보고자 한다.
'머신러닝 > 파이썬 구현 머신러닝' 카테고리의 다른 글
pytorch 공식 구현체로 보는 transformer MultiheadAttention과 numpy로 구현하기 (0) | 2023.07.01 |
---|---|
딥러닝 이론 optimizer 정리 - GD , SGD, Momentum, Adam (0) | 2022.05.14 |
파이썬과 기초 딥러닝 개념- 이론편 1 (0) | 2022.04.03 |
파이썬으로 기초 RNN 구현하기 (1) | 2022.03.27 |
파이썬으로 기초 MLP 구현하기 (2) | 2022.03.09 |