Post

인공신경망 ( ANN ) 3 MNIST 이미지 인식 ( 분류/추론/순전파 )

ANN에서 문제를 해결하는 방식은 두 단계로 이루어져 있다.

  1. 학습 : 학습 데이터를 이용해 가중치 매개변수를 학습한다. (지도 학습, SL(Supervised Learning))
  2. 추론 : 학습한 가중치 매개변수를 이용해 입력 데이터에 대한 결과를 추측한다. 학습은 지도 학습(Supervised Learning)과 비지도 학습(Unsupervised Learning)으로 나뉘며 지도 학습의 추론은 다시 분류(Classification)와 회귀(Regression)로 나뉜다. 지금부터는 신경망의 분류(Classification)를 다룬다.

MNIST 데이터셋 인식

MNIST는 손글씨 숫자 이미지 집합이다. ML에서 다양한 곳에서 사용된다. 0부터 9까지의 숫자 이미지로 구성되며 훈련 이미지는 60,000장, 시험 이미지는 10,000장 준비되어 있다. 이미지 데이터 크기는 28x28(784) 크기다.

img -> numpy array 변환

밑바닥부터 시작하는 딥러닝에서는 MINT 데이터셋을 받아 이미지를 넘파이 배열로 변환해주는 파이썬 스크립트 mnist.py를 제공한다. 저장된 파일이 있는지 검사하고 없으면 다운로드

  • 다운로드한 이미지 파일을 넘파이 배열로 변환(_load_img())
  • 넘파이 배열을 pickle 모듈을 이용해 객체 그대로 파일로 저장하고 추후 아래 함수로 load
1
2
3
4
5
def load_mnist(normalize=True, flatten=True, one_hot_label=False):
    with gzip.open(file_path, 'rb') as f:
            data = np.frombuffer(f.read(), np.uint8, offset=16)
    data = data.reshape(-1, img_size)
return (dataset['train_img'], dataset['train_label']), (dataset['test_img'], dataset['test_label'])

* one-hot은 딱 한 요소만 1이고 나머지는 0인 bit group을 의미한다. 넘파이 배열로 저장해 놓았던 이미지 파일을 불러와 이미지로 출력할 때는 다음과 같이 사용한다.

1
2
3
4
5
6
7
8
(x_train, t_train), (x_test, t_test) = load_mnist(flatten=True, normalize=False)

img = x_train[0]
label = t_train[0]

img = img.reshape(28, 28)
pil_img = Image.fromarray(np.uint8(img))
pil_img.show()

MNIST는 60000장의 훈련 이미지를 제공한다고 했다. x_train.shape는 flatten=True인 경우 60000x784이고 flatten=False인 경우 60000x1x28x28이다. 즉 x_train의 각 row가 하나의 이미지 데이터를 보관하고 있으며 각 요소는 이미지의 픽셀 값이다. 그래서 x_train[0]은 60000장의 이미지 중 첫번째 이미지다. label은 그 이미지에 대응되는 숫자 값이다. ( 필기체 5의 라벨은 5다. ) 784개의 원소로 이루어진 1차원 배열이든, 1x28x28이든 이미지로 출력하려면 28x28로 reshape해야한다.

전처리(pre-processing), 정규화(normalization)

normalize=True이면 0~255 범위인 각 픽셀의 값을 0.0~1.0 범위를 갖도록 255로 나눈다. 입력 데이터에 특정 변환을 가하는 것을 전처리(pre-processing)라고 하고, 위와 같이 데이터를 특정 범위로 변환하는 전처리를 정규화(normalization)이라 한다. 즉 위에서는 전처리 작업으로 정규화를 수행했다.

신경망 추론 ( 분류 / 순전파 forward propagation)

MNIST 데이터셋을 이용해 추론을 수행하는 신경망을 입력층 뉴런은 784개, 출력층 뉴런은 10개로 구성한다. 은닉층은 임의로 구성해도 좋다. 입력층 뉴런이 784개인 이유는 이미지 데이터 픽셀 수가 784이기 때문이다. 왜 이미지 픽셀 수만큼 입력층 뉴런이 존재해야 하냐면, 이미지 하나가 입력되면, 각각의 픽셀 데이터에 가중치를 곱한 결과를 종합해 출력층에서 정답을 찾아내야 하기 때문. 이미지 하나를 분석해서 분류해야 하는 작업이기 때문에, 입력층 뉴런의 수는 이미지 하나의 속성을 입력 받을수 있을 만큼 존재해야 한다.

분류 작업에서 입력층 뉴런의 수는 분류해야 하는 단일 입력이 가지고 있는 원소의 수 만큼 존재해야 하며 출력층 뉴런의 수는 분류해야 하는 가짓수 만큼 존재해야 한다.

추론 처리에서는 시험 이미지, 시험 레이블인 x_test, t_test(10,000개)를 사용한다.

1
2
3
4
5
6
7
8
9
    x, t = get_data()
    network = init_network()
    
    accuracy_cnt = 0
    for i in range(len(x)):
        y = predict(network, x[i])
        p = np.argmax(y)
        if p == t[i]:
            accuracy_cnt += 1
배치(batch) 처리

위 코드에서는 for문을 이용해 10000개의 이미지를 한장 씩 predict()로 넘기게 된다. 이미지 여러장을 한꺼번에 입력해 일괄적으로 묶어 한꺼번에 처리(batch) 하면 더 효율적으로 처리할 수 있다.

1x784 -> 1x10이었던 기존 코드를 100x784 -> 100x10으로 묶어서 처리하는 코드는 다음과 같다.

1
2
3
4
5
6
7
8
9
10
    x, t = get_data()
    network = init_network()

    batch_size = 100    
    accuracy_cnt = 0
    for i in range(0, len(x), batch_size):
        x_batch = x[i:i+batch_size]
        y_batch = predict(network, x_batch)
        p = np.argmax(y_batch, axis=1)
        accuracy_cnt += np.sum(p == t[i:i+batch_size])

맨 아래 두줄에서 브로드캐스트가 동작했다. range(start, end, step )과 slice로 간단히 처리한 부분이 눈에 띈다. axis=1을 꼭 지정해 주어야 한다. 이는 출력인 100x10 배열 y_batch에서 1번째 차원을 구성하는 각 원소에서 최댓값의 인덱스를 찾도록 한 것이다.

다음과 같은 행렬이 있을 때, 배열의 차수는 배열안에 배열이 있으니 이차원이다. 즉, 행렬은 이차원 배열이다.

내부에 있는 [0.1 0.3 0.5]같은 배열이 1번째 차원을 구성하는 각 원소이므로, 이 원소(일차원 배열)에서 최댓값의 인덱스를 찾는 것이다.

아무튼 이래저래 이해가 안되면, 각 row에서 최댓값의 인덱스를 구한다고 하면 이해가 쉽다. 각 row에서 최댓값의 인덱스를 뽑아 일차원 배열로 만들고, 이를 레이블 배열 t와 비교한다.

사실 그냥 x를 넘겨도 된다. 데이터의 개수가 적으면 이게 더 빠를 수도 있다.

1
2
3
y = predict(network, x)
p = np.argmax(y, axis=1)
accuracy\_cnt += np.sum(p == t)

근데 x가 1만개라서 그런 것 뿐이지, x가 임계치를 넘어가면 수용이 안되니까 배치 처리해야한다. 하드웨어에 따라 다르겠지만 가상머신에서 돌리는 것도 천 단위까지는 배치로 묶어서 전달할 수록 더 빨라진다. 어차피 배치 처리의 이점을 못볼 만큼 작은 데이터면 배치 처리 하나 안하나 비슷한 시간이 나오므로 그냥 배치 처리 하는게 이식성이 좋다.

This post is licensed under CC BY 4.0 by the author.