기본 레이어 이해하기 시리즈
간단한 데이터(e.g. | [(2, 5), (3, 7), (5, 4), ... ])들의 경우 앞서 설명한 선형변환만으로도 해결할 수 있다. 그런데 데이터의 단위가 커지고 연산해야하는 데이터의 양이 늘어나면 단순 선형변환으로는 데이터를 처리하는 데 한계가 있다.
예를 들어 보자. 만약 위 사진을 선형 레이어의 input으로 받는다면 처리 과정은 어떻게 될까? 이미지 파일은 1픽셀을 1개의 데이터로 취급하므로, (사진의 가로 길이
, 사진의 세로 길이
, 채널 수
)만큼의 shape를 가진 행렬로 표현된다.
위 이미지의 사이즈가 1920*1080이라면,
1920*1080*3 = 6,220,800 이므로 이미지 하나를 변환하는 연산에만 622만 개의 파라미터가 필요하다. 한눈에 매우 비효율적이라는 것을 알 수 있다. 이처럼 픽셀 하나 하나를 살펴보는 것은 비효율적일 뿐 아니라, 이미지 처리 관점에서는 별로 의미가 없는 행위이기도 하다.
그래서 만들어진 게 Convolution(합성곱) 레이어이다.
그런데 딥러닝에서 사용하는 합성곱과 신호 처리/제어 쪽에서 사용하는 합성곱은 조금 다르다. 연산 과정이 비슷하기 때문에 Convolution
이라는 이름이 붙었을 뿐이다. 이게 무슨 말인지는 잠시 후에 설명하겠다.
합성곱 레이어의 핵심 아이디어는 이웃한 데이터를 통해 패턴을 학습하는 것이다.
이게 무슨 의미인지 알기 위해서는 이미지 데이터를 왜 학습시키는지를 이해해야 한다. 우리는 이미지를 가지고 무슨 작업을 할까? 편집, 왜곡, 생성, 분류 등의 tasks를 생각해볼 수 있다.
그런데 이런 작업들은 이미지 상의 특정한 객체를 구분할 수 있어야 가능한 작업이다. '눈썹을 지워라' 라던가, '파란색을 빨간색으로 바꿔라' 같은 작업을 한다고 치자. '눈썹'이 뭔지, '파란색'과 '빨간색'이 뭔지 기계가 알아들어야 작업을 시킬 수 있다.
따라서, 합성곱 레이어의 목적은 주어진 이미지 상의 객체를 구분하는 것이 된다.
<Image>
[0][0][1][0][0][0]
[0][0][1][1][0][0]
[0][0][0][1][1][0]
어떻게 하면 이미지 상의 객체를 구분할 수 있을까? 일단 픽셀을 하나씩 보는 것으로는 이미지 상의 객체를 구분할 수 없다. 위 그림을 자세히 살펴보자. (1, 3)에 위치한 과 (2, 3)에 위치한 을 구분할 수 있을까?
그런데 이웃한 여러 개의 픽셀을 하나로 묶어서 보면 이야기가 달라진다.
<A> <B>
[0, 1, 1] [0, 0, 0]
[0, 0, 1] [1, 0, 0]
[0, 0, 0] [1, 1, 0]
와 에는 둘 다 1의 값을 가진 픽셀이 3개씩 존재하지만, 두 데이터는 서로 다른 패턴의 구조를 가지고 있다. 이런 식으로 일정한 크기의 데이터 위에 형성된 패턴을 학습할 수 있다면 같은 크기의 다른 데이터들과 구분할 수 있게 된다.
그렇다면 일정한 크기의 데이터
를 어떤 기준으로 나누어야 할까?
위 그림에서 주황색 행렬을 필터
라고 부른다. 필터의 개별 값이 원본 이미지의 입력값에 곱해질 때는 커널
이라 지칭한다. 필터를 통해서 보고 있는 원본 이미지의 지역 데이터는 윈도우
라고 부른다.
위 그림을 다시 살펴보자. 필터의 값에 따라 결과 이미지가 달라지는 것을 확인할 수 있다.
여러 가지 필터의 종류와 시각적 예시 | 링크
개인적으로는 커널보다 필터라는 이름이 더 직관적이라고 생각한다. '사용자의 목적에 맞는 데이터를 찾아낸다'는 목적성을 더 확실하게 표현하기 때문이다.
필터가 목적성을 갖는다는 것은 목적에 맞는 핵심 데이터를 추출함으로써 불필요한 연산을 피할 수 있다는 의미와 같다. 앞에서 살펴본 선형변환의 차원축소 아이디어와 비슷하다. 예시를 살펴보자.
위 그림을 보면 이미지 데이터 가 필터 를 통해 어떻게 변환되는지 직관적으로 알 수 있다. 윈도우(붉은색 사각형)의 데이터에 커널의 각 값을 곱하고, 전부 더하면 4가 나온다. (3*3) 사이즈의 데이터를 4라는 숫자 하나로 변환한 것이다. 이처럼 각 값을 곱하고 더하는 과정을 단일 곱셈 누산
이라고 한다. 앞서 언급했듯이 신호/처리 분야의 합성곱 연산과는 계산 과정이 다르다.
이렇게 얻은 출력 데이터 를 특성 맵
(feature map)이라고 하며, 각 필터가 한번에 이동하는 거리를 Stride
라고 한다. 앞에서 이야기한 패턴을 구분할 수 있는 일정한 크기의 데이터가 바로 특성 맵이다.
위 그림을 다시 보자. (7*7) 크기의 이미지를 집어넣었는데 (5*5) 크기의 특성 맵을 얻었다. 연산량은 줄었겠지만 혹시 데이터가 손실되진 않았을까? 손실되었다면 어떤 데이터가 어떻게 손실되었을까?
합성곱 레이어는 필터가 전체 이미지를 스트라이드만큼 움직이면서 데이터를 추출하는 식으로 작동한다. 그런데 만약 필터 크기가 (3*3)인데 스트라이드가 2라면? 데이터 추출 과정에서 중복으로 참고하는 데이터가 생기게 된다.
문제는 모서리에 위치한 데이터를 한 번 밖에 참고하지 않는다는 것이다. 결과적으로 모서리에 위치한 중요 데이터의 영향력이 줄어들게 된다. 또한 계속해서 합성곱 연산을 하면 데이터의 크기가 점점 줄어들기 때문에 최종적으로는 단 1칸짜리 피처 맵만 남는다. 데이터가 소실된 것이다.
이를 해결하기 위해 패딩
이라는 방법을 사용한다. 패딩 하면 중고등학생 시절 유행했던 모 브랜드의 아우터가 생각나는데, 여기서 사용하는 패딩도 정확히 같은 목적을 위해 사용된다. 바로 알맹이를 보존하는 것이다.
패딩을 통해 데이터의 입력과 출력 크기를 유지할 수 있다. 패딩 값은 주변에 영향을 끼치지 않기 위해 0을 사용한다. 패딩 크기가 반드시 1일 필요는 없지만, 너무 큰 패딩은 오히려 데이터의 밀도를 떨어뜨리게 되므로 적당선을 지키는 게 중요하다. 입력과 출력의 크기를 맞추는 것을 same padding
, 입력 데이터만으로 합성곱 연산을 처리하는 것을 valid padding
이라 한다.
지금까지의 내용을 잘 살펴보면 패딩, 필터의 크기, 입력의 크기, 스트라이드 값에 따라 출력 데이터의 shape가 바뀐다는 것을 알 수 있다.
입력 크기 =
필터 크기 =
출력 크기 =
패딩 =
스트라이드 = 라고 하면,
이고, 이다.
단, 이나 의 결과값은 정수여야 한다. 만약 정수가 아니라면 오류를 출력하거나 가까운 정수로 반올림하는 등의 처리가 필요하다.
+1을 해주는 이유는 날짜를 세는 것과 동일한 원리이다.
앞서 이미지 데이터의 shape에 채널 수
라는 변수가 있었다. 일반적으로 컬러 이미지는 RGB값을 각각의 채널로 받기 때문에 채널 수는 3이 된다. 흑백 이미지일때는 흑백만 구분하면 되기 때문에 채널 수가 1이 된다.
필터는 여러 장 겹쳐서 사용할 수 있다. 필터의 채널 수와 입력 이미지의 채널 수만 맞춰주면 된다. 안경점에서 시력검사를 할 때, 적절한 도수를 찾기 위해 여러 장의 렌즈를 끼워보는 것과 같은 원리이다.
위 그림은 (1920, 1080, 3) 크기의 데이터에 (5, 5, 3) 크기의 필터 16장을 겹친 합성곱 레이어를 적용한 것이다. 앞에서 출력 데이터의 크기를 구하는 공식을 사용해보자. 패딩은 1이라고 가정한다.
= = (384.4, 216.4)
(1920, 1080, 3) * (3, 16, 5, 5) 5 = (384, 216, 16)
필터를 16장 사용했으므로 해당 합성곱 레이어의 최종 출력값, 다시 말해 의 shape는 (384, 216, 16) 이다. 이제 선형변환을 해 주면...
- (384, 216, 16) (1, 384*216*16) = (1, 1327104)
- (1, 1327104) * (1327104, 1) = (1, 1) (1, )
앞서 데이터를 그냥 집어넣었을 때는 622만 개의 파라미터가 생성됐지만, 이번에는 약 132만 개의 파라미터만 생성되는 걸 확인할 수 있다. 연산해야 할 내용이 80%나 줄어든 것이다.
이상의 과정을 코드로 구현하면 다음과 같다.
import tensorflow as tf
batch_size = 64
pic = tf.zeros((batch_size, 1920, 1080, 3))
print("입력 이미지 데이터:", pic.shape)
conv_layer = tf.keras.layers.Conv2D(filters=16,
kernel_size=(5, 5),
strides=5,
use_bias=False)
conv_out = conv_layer(pic)
print("\nConvolution 결과:", conv_out.shape)
print("Convolution Layer의 Parameter 수:", conv_layer.count_params())
flatten_out = tf.keras.layers.Flatten()(conv_out)
print("\n1차원으로 펼친 데이터:", flatten_out.shape)
linear_layer = tf.keras.layers.Dense(units=1, use_bias=False)
linear_out = linear_layer(flatten_out)
print("\nLinear 결과:", linear_out.shape)
print("Linear Layer의 Parameter 수:", linear_layer.count_params())
출력 결과는 아래와 같다.
입력 이미지 데이터: (64, 1920, 1080, 3)
Convolution 결과: (64, 384, 216, 16)
Convolution Layer의 Parameter 수: 1200
1차원으로 펼친 데이터: (64, 1327104)
Linear 결과: (64, 1)
Linear Layer의 Parameter 수: 1327104
Convolution Layer의 파라미터 개수는 필터들을 다 합친 shape의 파라미터 크기와 같다.
따라서 3*5*5*16 = 1200개이다.
당연하지만 합성곱 레이어에도 편향이 존재한다.
선형변환에서는 편향의 shape가 (선형변환 결과 차원의 수
, ) 였다. 그리고 위 코드에서 합성곱 레이어의 출력값 shape는 (batch size
, width
, height
, 필터의 수
) 이다. 그렇다면 합성곱 레이어의 편향의 shape는 어떻게 될까?
선형변환에서 편향이 변환 결과 차원의 수만큼 필요한 이유는 각 변수에 대한 함수마다 더해주어야 하기 때문이라고 했다. 합성곱 레이어는 주어진 이미지 상의 패턴을 구분하는 것이 목적이므로, 패턴을 구분하는 단위
개수만큼의 편향이 필요할 것이다.
합성곱 레이어에서 패턴을 구분하는 단위는 특성 맵이다. 따라서 특성 맵의 개수만큼의 편향이 필요하다. 특성 맵의 개수는 필터의 수와 같으므로, 위 코드에서 편향의 shape는 (1, 1, 16)이 된다.
편향 계산 방식은 선형 레이어와 같다. 필터 연산 후 그냥 더해주면 된다.
합성곱 레이어가 꼭 이미지 데이터의 처리에만 쓰이는 것은 아니다.
전체 데이터 처리 과정을 찬찬히 되짚어보자. 합성곱 레이어에서는 전체 input 데이터를 하나 하나 뜯어보지 않고 필터 크기만큼씩 데이터를 묶어서 처리한다. 즉, 데이터를 집약시키는 일을 한다.
결과적으로 굳이 이미지 데이터가 아니더라도, 데이터를 집약시켜서 처리할 필요가 있다면 합성곱 레이어를 사용할 수 있다는 뜻이다.
실제로 트랜스포머 기반 모델이 나오기 전에는 CNN, RNN 기반 모델이 대부분이었다.
- 합성곱 레이어의 목적은 이미지 상의 패턴을 구분하는 것이다.
- 패턴을 구분하기 위해서 데이터를 필터 크기로 묶어 처리한다.
- 결과적으로 Convolution Layer에서는 데이터를 집약시키는 효과가 발생한다.