Search
Duplicate
🥑

CNN을 활용한 숫자 이미지 분류

소속팀
위키 팀
팀명
아보카도
데모

CNN 알아보기

1. CNN 정의

CNN은 Convolutional Neural Networks의 줄임말로 Convolution 전처리 작업이 들어가는 Neural Network 모델이다. CNN은 하나 또는 여러개의 Convolutional Layer와 그 위에 올려진 일반적인 인공 신경망 계층들로 이루어져 있으며, 가중치와 Pooling Layer들을 추가로 활용한다.
CNN은 Deep Neural Network의 문제점을 해결하고자 나타난 모델이다. DNN은 기본적으로 1차원 형태의 데이터를 사용하기 때문에 3차원 데이터인 한장의 사진을 1차원으로 평면화시켜 신경망을 학습하였다. 이 과정에서 공간적 지역적 정보가 손실될 수밖에 없었고 이미지에 대한 정보 부족으로 특징을 추출하거나 학습에 있어서 비효율적이고 모델의 정확도를 높이는데 한계가 나타나게 되었다.
이에 따라 이미지의 공간 정보를 유지한 상태로 학습이 가능한 모델인 CNN이 등장하게 되고 2차원 구조의 입력 데이터를 충분히 활용하여 computer vision 분야에서 우수한 성능을 보여준다.
CNN은 Convolution Layer와 Pooling Layer를 여러 겹 쌓아 이미지의 특징을 추출하고 Fully Connected Layer에서 이미지를 분류하게 된다. 이미지의 공간 정보를 유지하면서 추가적으로 한 픽셀과 주변 픽셀들의 연관성을 파악하여 특징을 효과적으로 인식하기 때문에 이미지를 인식하기 위해 패턴을 찾는데 강점을 갖고 있다.

2. CNN 구조

CNN은 하나의 필터만 사용하지 않고, 다양한 필터를 사용한다. 다양한 필터를 사용하여 필터마다 다른 특징을 나타내게 만든다. 필터별로 활성화 맵을 만들어내고, 활성화맵 개수의 볼륨을 가지는 출력이 만들어지게 된다. 이렇게 만들어진 Convolution layer와 활성화함수를 같이 쌓아올리고, Pooling Layer를 곳곳에 넣어 CNN모델을 구성한다.

2-1. Convolutional Layer

Convolutional Layer는 입력에 Convolution작업을 적용하고, 다음 계층에 그 결과를 전달한다. 예를 들어 어느 이미지에 Convolution을 적용하면 이미지 크기도 줄어들고, 동시에 그 필드에 포함된 모든 정보를 하나로 모아 한 개의 픽셀로 합치게 된다. 해결해야 하는 문제의 유형과 알아내고자 하는 특징의 종류에 따라, 여러 가지 종류의 Convolution을 사용할 수 있다.
입력 이미지의 모든 영역에 필터를 적용해 특징을 찾기 위해서 필터를 이용해 연산처리를 하게 된다. 이 과정에서 가중치에 해당하는 필터w는 가로와 세로의 크기는 input의 크기보다 크지만 않으면 되지만, 깊이는 무조건 input값과 같아야 한다. filter를, 왼쪽 위부터 모든 부분마다 내적을 해주는게 포인트다 (WTx+bW^Tx+b). 이를 Convolution 연산이라고 하고 모든 부분에 대해 필터의 특정 위치에서 나온 값들을 출력하여 활성화 맵을 형성하게 되는 것이다. 예를 들어, 32*32*3 이미지에 5*5*3 필터의 내적을 실시하면 28*28*1의 활성화 맵이 나온다.
내적을 실시하는 원리는 보폭에 따라 필터를 밀어주는 것이다. 위는 7*7 인풋에 보폭을 1로 설정한 후, 3*3의 내적을 실시하는 모습이다. 아웃풋은 (N(크기)-F(필터)) / stride(보폭) + 1의 사이즈를 가진다. 보폭은 반드시 아웃풋을 자연수로 만드는 값이어야한다.
만약 가장자리에 zero padding을 적용한다면, 사이즈는 (N+2)*(N+2)로 늘어나기 때문에 이를 유념해 공식을 적용해 아웃풋을 구해야한다. 이처럼 zero padding을 적용하는 이유는 전체크기를 유지하기 위해서다. 또한 필터가 가지고 있는 파라미터의 수는 F*F*깊이 + b 이다.
정리 (W=가로, H=세로, D=깊이, 필터개수 = K, 필터크기 =F, 보폭 = S, 패딩수 = P) input size = W1H1D1W_1*H_1*D_1 일때, output size = W2H2D2W_2*H_2*D_2 W2=(W1F+2P)/S+1W_2 = (W_1-F+2P)/S + 1 H2=(H1F+2P)/S+1H_2 = (H_1-F+2P)/S + 1 D2=KD_2 = K
코드
keras.layers.Conv2D(32,kernel_size=(3,3),input_shape=(28,28,1),padding='SAME',activation=tf.nn.relu)
Python
복사
코드의 마지막 인자인 activation이 궁금하다면 Activation Functions 참고

2-2. Pooling Layer

Convolutional Layer을 거쳐 나온 activation map, 그리고 활성화 함수를 거친 출력의 크기와 깊이를 유지하며 downsampling 하는 과정을 의미한다. 이미지의 크기를 계속 유지한 채 Fully Connected Layer에 전달된다면 연산량이 늘어나므로 결과값의 크기도 줄이고 특정 feature를 강조하는 과정을 Pooling Layer에서 하는 것이다. 참고로 이미지의 사이즈를 줄이는 것이 목적이기 때문에 패딩을 고려하지 않는다.
가장 일반적으로 쓰이는 방법은 Max Pooling으로, 필터가 슬라이딩하며 필터 안에 가장 큰 값을 고르는 방법을 의미한다. 이는 뉴런이 가장 큰 신호에 반응하는 것과 유사한 방법을 이용한 것인데, 인접 픽셀들 간의 유사도가 매우 높다는 이미지 데이터의 특징을 이용하여 선택 영역에서 가장 큰 값을 해당 영역의 대표값으로 설정하게 된다. 다양한 pooling 사이즈, 필터 크기, 레이어 수 등을 시도해 보는 등의 Cross-validation이 필요하다.
대표적인 pooling의 종류 1. Max Pooling 정해진 filter의 크기 안에서 가장 큰 값을 뽑아내는 Pooling 2. Average Pooling 정해진 filter의 크기 안에 있는 값들의 평균을 뽑아내는 Pooling
정리 (W=가로, H=세로, D=깊이, Pooling Layer 필터크기 =F, 보폭 = S) input size = W1H1D1W_1*H_1*D_1 일때, output size = W2H2D2W_2*H_2*D_2 W2=(W1F)/S+1W_2 = (W_1-F)/S + 1 H2=(H1F)/S+1H_2 = (H_1-F)/S + 1 D2=KD_2 = K
코드
keras.layers.MaxPool2D(pool_size=(2,2),padding='SAME')
Python
복사

2-3. FC Layer

2-2의 출력을 1차원 벡터로 만들어 모든 출력을 연결한다. 이때는 공간적 구조를 신경쓰지 않고 하나로 통합한 다음, 클래스 스코어를 계산한다. 이렇게 출력된 값은 계층구조의 최상위 값으로, 각 값들이 의미하는 것은 필터의 Template이 얼마나 활성화되었는가이다.
이 Layer에서는 앞에서 추출한 이미지의 특징을 통해 무엇을 의미하는 데이터 인지를 분류한다. 데이터 타입을 Fully Connected 네트워크 형태로 변경한 후 활성화 함수인 Softmax activation function을 적용하여 마무리한다.
활성화 함수에 대해 더 알아보고자 한다면 Activation Functions 문서 참조
코드
keras.layers.Flatten(), keras.layers.Dense(10,activation="softmax")
Python
복사

CNN 구현하기

3-1. 데이터 소개

train data: 60,000개의 데이터가 있고, 각 행은 index, label(숫자의 범주 0~9), 이미지의 각 픽셀값(784개)으로 구성되어있다.
test data: 10,000개의 데이터가 있고, 각 행은 784개의 픽셀값으로 구성되어있다.
csv 데이터를 확인해보면 60000 x 786의 데이터셋으로 되어있지만, 784개의 픽셀값을 시각화하면 알 수 있듯이 이는 사실 28*28 크기의 숫자 이미지다. 각 픽셀은 밝기 정도에 따라 0 ~ 255로 나누어 색깔을 나타내는 수치로 표현한다. 숫자는 보통 0~255의 8비트 정수로 저장하는데 8비트로 표현해도 사람의 눈으로 인식하는데는 문제가 없다. 8비트로 표현하게 되면 2진수 8자리로 나타내는 것이기 때문에 범위가 255까지로 되는 것이다.

3-2. 데이터 전처리

모델을 train하고 test할 때에 'index', 'label'는 필요가 없어서 삭제한다.
label 열은 따로 떼어내서 target으로 만들어두었다.
target = train["label"] x_train = train.drop(['index','label'],axis=1) x_test = test.drop("index",axis=1)
Python
복사
모델은 정수를 처리하는것보다 0~1 사이의 실수를 처리할 때 더 좋은 성능을 보이기 때문에0~255의 픽셀값으로 이루어진 이미지에 255를 나눠줘 전처리를 해준다.
이러한 과정을 Normalization이라고 한다. Normalization을 통해서 data의 scale을 동일하게 만들어 주는데 이는 gradient가 원활하게 업데이트되도록 해준다. 왼쪽 처럼 Unnormalized한 데이터는 최적값 도달이 복잡하지만, Normalized의 경우에는 쉽게 최적값에 도달할 수 있으며 빠르게 훈련시킬 수 있다.
x_train = x_train/255 x_test = x_test/255 #표준화
Python
복사
CNN 입력 모델 형태로 차원 변환한다. 784개의 픽셀값을 28 x 28 형태의 정방 행렬로 만든다.
reshape(데이터수, 이미지가로, 이미지세로, 흑백이미지면1) 을 통해 데이터를 4D 텐서로 재정의한다. 이 과정을 처리해야지 각각의 이미지에 접근할 수 있다.
reshape의 인자 -1은 일단 해당 자리 부분의 shape는 비워두고 다른 차원의 shape가 다 결정됐을경우 원래 배열의 길이와 남은 차원으로부터 추정하여 shape를 결정하는것을 의미한다.
x_train = np.array(x_train).reshape((-1,28,28,1)) #reshape((60000,28,28,1))이랑 같음 x_train.shape #(60000, 28, 28, 1) x_test= np.array(x_test).reshape((-1,28,28,1)) x_test.shape #(10000, 28, 28, 1)
Python
복사
출력 데이터는 위에서 만든 target을 처리해준다. 숫자가 0~9까지의 값으로 이루어져 있기 때문에 총 10개로 이루어진 One-Hot Encoding을 생성하여 출력 레이블을 만든다.
One-Hot Encoding이란 하나의 값만 1을 가지고 나머지 값은 모두 0을 가지도록 표현하여 데이터를 구별하는 인코딩이다. 데이터에서 1 값의 위치를 통해서 이 데이터가 어떤 범주에 속하는지 보여주어 컴퓨터가 쉽게 이해할 수 있도록 해주는 것이다.
from tensorflow.keras.utils import to_categorical target = to_categorical(target,num_classes=10) target.shape
Python
복사

3-3. 모델 구성

Conv2D
keras.layers.Conv2D(32,kernel_size=(3,3),input_shape=(28,28,1),padding='SAME',activation=tf.nn.relu)
Python
복사
첫번째 인자 : filter의 수. filter는 이미지에서 feature을 분리해내는 기능을 하며 filter의 값은 convolution에 사용되는 filter의 개수이며, 출력 공간의 깊이를 결정한다. filter의 개수를 증가시키면 상세 정보는 손실되지만 일반적인 특징을 찾을 수 있다.
두번째 인자 : kernel_size 값(행, 열). convolution에 사용되는 filter의 크기를 의미한다.
input_shape : 샘플 수를 제외한 입력 형태를 정의. 모델에서 첫 layer일 때만 필요하다. (행, 열, 채널 수)로 나타내는데 흑백 이미지인 경우에는 채널이 1이고, 컬러(RGB) 이미지인 경우에는 채널을 3으로 설정한다.
padding : 경계 처리 방법.
‘valid’ : 유효한 영역만 출력하기 때문에 출력 이미지 사이즈는 입력 이미지 사이즈보다 작다.
‘same’ : 입력 이미지 사이즈와 출력 이미지 사이즈를 동일하게 출력한다.
activation : 활성화 함수.
‘linear’ : 디폴트 값으로 입력 뉴런과 가중치로 계산된 결과값이 그대로 출력.
‘relu’ : 은익층에 주로 사용. 원리는 활성화함수 참고.
‘sigmoid’ : 이진 분류 문제에서 출력층에 주로 사용. 원리는 활성화함수 참고.
‘softmax’ : 다중 클래스 분류 문제에서 출력층에 주로 사용. 원리는 Loss Funtion 참고.
MaxPool2D
keras.layers.MaxPool2D(pool_size=(2,2),padding='SAME')
Python
복사
Convolutional Layer에서 나온 Feature map의 값을 샘플링해서 정보를 압축하는 과정이다.
pool_size : 수직, 수평 축소 비율. (2, 2)이면 출력 이미지의 크기는 입력 이미지 크기의 반이 된다.
strides : pooling filter를 이동시키는 간격. strides를 지정해주지 않으면, pooling filter의 크기와 같아서 영역의 오버랩 없이 pooling이 이루어진다.
padding : "valid" or "same" 값을 입력받는다. Conv2D에서의 "valid"와 "same"과 의미가 같다. stride 값이 2이상 일 때 이미지의 모서리에서 데이터가 끝나는 경우, "valid"에서는 해당 데이터가 버려지지만, "same"의 경우 데이터가 없는 부분에 zero-padding을 통해 데이터의 손실을 막는다.
data_format : "channels_last" or "channels_first" 값을 입력받는다.
Dense 층 (Fully-connected layer)
keras.layers.Flatten(), keras.layers.Dense(10,activation="softmax")
Python
복사
Convolution Layer와 Pooling Layer를 통과하며 출력된 값에는 주요 특징만 추출되는데 이 특징은 2차원 데이터로 이루어져 있다. 하지만 Dense Layer에서는 1차원 데이터로 바꾸어서 학습이 되어야 하기 때문에 Flatten Layer를 거쳐 2차원 데이터를 1차원 데이터로 바꾼다.
네트워크가 과적합되는 경우를 방지하기 위해서 Dropout Layer를 이용하기도 한다. Dropout Layer에서는 무작위로 뉴런의 집합을 제거하여 각각의 Layer에서 뉴런의 영향력을 줄인다. 자세한 원리는 정규화 문서 참고.
Dense Layer에서는 입력 뉴런과 출력 뉴런을 연결선으로 연결시켜주는 Layer이다. 각 연결선은 Weight를 포함하며, 가중치가 높을수록 해당 입력 뉴런이 출력 뉴런에 미치는 영향이 크고, 낮을수록 미치는 영향이 작다.
첫 번째 인자 : 출력 뉴런의 수
input_dim : 입력 뉴런의 수
activation : 활성화 함수
최종 모델(Conv2D와 MaxPooling2D를 반복해서 구성)
import tensorflow as tf from tensorflow import keras model = keras.Sequential([ keras.layers.Conv2D(32,kernel_size=(3,3),input_shape=(28,28,1),padding='SAME',activation=tf.nn.relu), #First Convolutional Layer 정의 keras.layers.Conv2D(32, kernel_size = (3,3),padding = 'Same',activation=tf.nn.relu), keras.layers.MaxPool2D(pool_size=(2,2),padding='SAME'), #MaxPooling 계층 keras.layers.Dropout(0.25), keras.layers.Conv2D(64,kernel_size=(3,3),padding='SAME',activation=tf.nn.relu), keras.layers.Conv2D(64,kernel_size=(3,3),padding='SAME',activation=tf.nn.relu), keras.layers.MaxPool2D(pool_size=(2,2),padding='SAME'), keras.layers.Flatten(), keras.layers.Dense(256,activation="relu"), #완전연결계층 거친후 나오는 출력데이터수 keras.layers.Dense(256,activation="relu"), keras.layers.Dropout(0.5), keras.layers.Dense(10,activation="softmax")]) #분류클래스 수
Python
복사
model.compile(optimizer='Adam',loss='categorical_crossentropy',metrics=['accuracy'])
Python
복사
모델을 모두 작성하면 이를 compile 한다. 모델의 loss function을 정의하고, 정한 loss function로 얻은 값을 통해 모델의 파라미터를 조정하는 optimizer 그리고 사용할 지표를 설정한다.
optimizer : 훈련 과정을 설정하는 옵티마이저. 종류는 최적화 문서 참고.
loss : 훈련 과정에서 사용할 손실 함수.
mean_squared_error : 회귀 문제에서 사용.
categorical_crossentropy : 다중 클래스 분류에서 사용.
sparse_categorical_crossentropy : 다중 클래스 분류에서 사용. categorical_crossentropy와 다르게 레이블을 One-Hot Encoding하지 않고 정수 인코딩 된 상태에서 사용 가능하다.
binary_crossentropy : 이진 분류에서 사용.
metrics : 훈련을 모니터링하기 위한 지표.
최종 결과
Accuracy: 0.9927
순위: #15

3-4. 시행착오를 통해 배운 점

(1) 얼마나 층을 촘촘히 쌓는지 중요하다.
첫 모델에서 conv2d+ max pooling 조합으로 2번만 쌓았더니 정확도가 0.7로 굉장히 낮아 Layer를 더 깊게 쌓아 모델을 생성하였다.
CNN의 성능을 향상시키기 위해서 직접적으로 망의 크기를 늘리면 된다는 것을 알 수 있었다. 망의 크기를 늘린다는 것은 Layer의 depth를 늘리는 것뿐만 아니라, Layer 안에서 unit의 수(width)를 늘리는 것도 하나의 방법이다.
하지만 성능이 계속해서 향상되는 것도 아니고 Layer만 계속 쌓는다면 연산량이 늘어 학습시간이 오래 걸리기도 하고 학습에 사용할 데이터 양이 제한적일 경우 overfitting 문제가 발생한다.
(2) 어떤 옵티마이저를 쓰는지도 중요하다.
처음에는 컴파일링할 때, sgd를 썼었는데 adam을 사용하였더니 epoch 횟수가 30에서 10으로 줄었는데도 충분한 결과를 뽑아낼 수 있었다.
SGD : loss의 값이 최소가 되는 방향으로 가중치를 업데이트하는 방법으로 계속 반복해서 optimization을 진행한다. 기울기 변화가 작은 부분에서는 매우 느리게 이동하는 문제가 발생한다. 또한 Mini-Batch Gradient Descent의 경우 미니배치마다 loss를 계산하여 최적화를 진행하는데 이는 매우 비효율적이다. noise가 발생하면 optimization의 성능이 저하된다.
# Vanilla update x += - learning_rate * dx #dx: x에서의 loss에 대한 기울기
Python
복사
ADAM : First moment와 second moment를 이용해 이전 정보를 유지하고 gradients의 제곱을 이용하는 방법이다. 이때 First moment는 velocity를 담당한다. 그리고 second moment는 AdaGrad이나 RMSProp처럼 gradient 제곱 활용한다. 간단하게 구현할 수 있으면 효율적으로 계산하기 때문에 메모리를 많이 사용하지 않는다. 최적의 gradient를 찾을 때 적절한 속도와 방향으로 움직이므로 다른 optimizer들보다 성능이 좋다.
# almost first_moment = 0 second_moment = 0 while True: dx = compute_gradient(x) first_moment = beta1 * first_moment + (1-beta1)*dx second_moment = beta2 * second_moment + (1-beta2) * dx * dx x -= learning_rate * first_moment / (np.sqrt(second_moment) + 1e-7))
Python
복사
그 외의 옵티마이저는 최적화 문서 참고.
(3) epoch 설정
epoch는 전체 트레이닝 데이터가 신경망을 통과한 횟수 즉 총 훈련 횟수라고 볼 수 있다.
너무 많이 학습하면 overfitting 문제가 발생한다. 따라서 좋은 결과값이 나오면 epochs수가 남아있더라도 종료시키는 early stopping 방법을 이용해야 한다.
epoch를 줄 때 batch size 또한 고려해야 한다. batch size는 전체 트레이닝 데이터 셋을 여러 작은 그룹을 나누었을 때 하나의 batch에 속하는 데이터 수를 의미한다. 모든 데이터를 한꺼번에 집어넣으면 메모리가 부족해지고 훈련 속도 또한 느려지므로 대부분의 경우 데이터를 나누어 사용한다.
만약 batch size가 너무 크면 처리해야할 양이 많다는 것을 의미하므로 학습 속도가 느려지고, 어떤 경우에는 메모리 부족 문제가 나타날 수 있다. 반면 batch size가 너무 작으면 가중치가 자주 업데이트되기 때문에 비교적 불안정하게 훈련이 진행된다. 따라서 배치 사이즈를 바꿔가면서 언제 모델이 가장 효율적으로 훈련되는지 확인해야 한다.
padding 이란? Convolution layer를 통과했을 때 activation map의 크기가 줄어들게 되는데, 이미지의 Size를 유지하여 가장자리의 정보를 사라지지 않게 하기 위해 이미지 주변을 특정값으로 채워 늘리는 것이다.