Python과 OpenCV – 48 : kNN을 이용한 글자 인식(OCR)

이 글의 원문은 https://opencv-python-tutroals.readthedocs.io/en/latest/py_tutorials/py_ml/py_knn/py_knn_opencv/py_knn_opencv.html 입니다.

이 글은 손으로 그린 글자를 판독하는 기능에 대한 것입니다. 이를 위해 몇가지 훈련 데이터(train_data)와 시험 데이터(test_data)가 필요합니다. 아래와 같은 2000×1000 픽셀 크기의 digits.png 파일을 사용합니다.

이 이미지에는 손으로 작성한 5000개의 0~9까지의 문자가 담겨 있습니다. 문자 하나당 500개씩 기록되어 있으며, 가로와 세로로 각각 100개씩, 5개씩 표기되어 있습니다. 이미지에서 문자 하나가 차지하는 크기는 20×20 픽셀입니다. 가장 먼저 이 이미지에서 5000개의 문자 단위로 잘라내야 합니다. 그리고 이 20×20 픽셀 크기 문자 이미지를 400 크기의 단일 행으로 만듭니다. 이 데이터가 모든 픽셀에 대한 화소값을 가지는 피쳐셋(Feature Set)입니다. 우리가 생성할 수 있는 가장 단순한 피쳐셋입니다. 이 데이터에서 각 문자의 250개에 해당하는 부분은 train_data로 사용하고 나머지 250개는 test_data로 사용합니다.

이제 코드를 작성해 보면..

import numpy as np
import cv2

img = cv2.imread('./data/digits.png')
gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)

# Now we split the image to 5000 cells, each 20x20 size
cells = [np.hsplit(row,100) for row in np.vsplit(gray,50)]

# Make it into a Numpy array. It size will be (50,100,20,20)
x = np.array(cells)

# Now we prepare train_data and test_data.
train = x[:,:50].reshape(-1,400).astype(np.float32) # Size = (2500,400)
test = x[:,50:100].reshape(-1,400).astype(np.float32) # Size = (2500,400)

# Create labels for train and test data
k = np.arange(10)
train_labels = np.repeat(k,250)[:,np.newaxis]
test_labels = train_labels.copy()

# Initiate kNN, train the data, then test it with test data for k=1
knn = cv2.ml.KNearest_create()
knn.train(train, cv2.ml.ROW_SAMPLE, train_labels)
ret, result, neighbors, dist = knn.findNearest(test, k=5)

# Now we check the accuracy of classification
# For that, compare the result with test_labels and check which are wrong
matches = result==test_labels
correct = np.count_nonzero(matches)
accuracy = correct*100.0/result.size
print(accuracy)

코드를 설명하면, 8번은 digits.png 이미지를 가로로 100개, 세로로 50로 잘라 조각내어 cells 변수에 저장하는데, 각각의 조각 이미지에는 문자 하나가 담겨 있습니다. 11번 코드는 다시 이 cells를 NumPy의 배열로 만들어 x 변수에 저장합니다. 14번 코드는 배열 x 중 절반을 학습 데이터로 사용하고 나머지 절반을 테스트 데이터로 사용하고자 각각 train과 test 변수에 담습니다. train 변수에 저장된 문자에 대해 0~9까지의 값으로 라벨링해줘야 하는데, 18-19번 코드가 그에 해당합니다. 바로 이 train 데이터와 train_labels 데이터가 학습 데이터라고 할 수 있습니다. 이렇게 학습된 데이터를 토대로 test 변수에 저장된 문자들이 0~9까지 중 무엇에 해당하는지 kNN 알고리즘으로 파악하는 것이 23~25번 코드입니다. 최종적으로 테스트 데이터가 정확히 인식되었는지 확인하는 코드가 29~32번 코드입니다. 출력값은 91.76인데, 즉 성공률이 91.76%라는 의미입니다.

인식 정확도를 개선하기 위해서는 인식이 실패한 데이터를 학습시켜 train 변수에 추가하고 다음에 이 변수를 재활용하는 것입니다. 이를 위해 파일에 저장하고 다음에 저장된 파일로부터 불러오는 함수가 필요합니다. 학습 데이터를 저장하는 예는 다음과 같습니다.

np.savez('knn_data.npz',train=train, train_labels=train_labels)

데이터 파일을 불러오는 예는 다음과 같습니다.

with np.load('knn_data.npz') as data:
    print data.files
    train = data['train']
    train_labels = data['train_labels']

Python과 OpenCV – 47 : kNN(k-Nearest Neighbour)의 이해

이 글의 원문은 https://opencv-python-tutroals.readthedocs.io/en/latest/py_tutorials/py_ml/py_knn/py_knn_understanding/py_knn_understanding.html#knn-understanding 입니다.

kNN은 감독학습(Supervised Learning)을 통한 가장 단순한 분류 알고리즘 중에 하나입니다. 알고리즘에 대한 아이디어의 시작점은 공간 상의 시험 데이터와 가장 가까운 것들을 묶는다는 것입니다. 아래의 이미지를 통해 볼 수 있죠.

위의 이미지에는 파란색 사각형과 빨간색 삼각형으로 구성되어 있습니다. 즉 2개의 그룹이 존재합니다. 각 그룹을 클래스(Class)라고 부릅니다. 이제 새로운 요소(초록색 원)이 나타났습니다. 이 새로운 요소는 파란색 사각형으로 분류될까요, 아니면 빨간색 삼각형으로 분류 될까요? 이를 kNN 알고리즘을 활용해 보자는 것입니다.

한가지 방법은 이 초록색 원과 가장 가까운 이웃을 찾는 것입니다. 위의 그림에서는 빨간색 삼각형과 가장 가깝다는 것을 확인할 수 있습니다. 그래서 이 초록색 원은 빨간색 삼각형으로 분류됩니다. 이 방법은 오직 가장 가까운 이웃에 의한 분류이므로 단순히 ‘가장 가까운 이웃(Nearest Neighbour)’ 이라고 부릅니다.

그러나 여기에는 문제가 있습니다. 빨간색 삼각형이 가장 가까울 수 있습니다. 그러나 이 초록색 원 주위에는 아주 많은 파란색 사각형이 존재한다면 어떨까요? 그러면 파란색 사각형은 빨간색 삼각형보다 더 많은 영향력을 초록색 원에 주고 있다고 할 수 있습니다. 그래서 가장 가까운 것만으로는 충분하지 않습니다. 대신 어떤 k-Nearest 군이라는 개념을 검사해야 합니다. 그림에서, 3 Nearest 군, 즉 k=3이라고 합시다. 초록색 원과 가장 가까운 3(k값)개를 취합니다.그러면 2개의 빨간색과 1개이 파란색이 있고 초록색은 하나 더 많은 빨간색으로 분류됩니다. k=7이라면? 5개이 파란색과 2개의 빨간색이 존재하고 파란색으로 분류됩니다. k값을 변경하는 것이 전부입니다. 재미있는 것은 만약 k=4일때입니다. 이 경우 2개의 빨간색과 2개의 파란색이 존재합니다. 애매해지죠. 그래서 k값은 홀수로 잡는 것이 좋습니다. 이러한 분류 방법을 k-Nearest Neighbor이라고 합니다.

지금까지 언급한 kNN에서는 k개의 이웃 모두를 동일하게 다루고 있습니다. 이게 맞는 것일까요? 예를 들어, k=4인 경우에 분류가 애매해 진다고 했습니다. 그러나 좀더 살펴보면, 2개의 빨간색은 다른 2개의 파란색보다 좀더 가깝습니다. 그렇다면 빨간색에 더 많은 가중치를 줘야 합니다. 이를 수학적으로 어떻게 설명할까요? 가까운 요소들에 대해 그 거리에 따라 계산된 가중치를 줘야 합니다. 가까운 요소에는 더 높은 가중치를, 상대적으로 멀리 있는 요소에는 낮은 가중치를 말이죠. 결국 가장 높은 가중치의 합을 가지는 쪽으로 분류될 수 있는데, 이를 Modified kNN이라고 합니다.

여기에 몇가지 중요한 것을 발견할 수 있습니다.

  • 공간 상에 분포되는 빨간색과 파란색 요소 전체에 대한 정보가 필요합니다. 왜냐하면 새로운 요소와 이미 존재하는 각 요소 사이의 거리를 구해야 하기 때문입니다. 그래서 매우 많은 요소가 존재할 경우 더 많은 메모리와 더 많은 계산 시간이 필요할 것입니다.
  • kNN에는 어떤 형태의 훈련과 준비가 필요치 않습니다.

OpneCV에서 kNN을 살펴봅시다.

이 글에서는 먼저 앞서 살펴본 그림처럼 2개의 군으로 구성된 단순한 예를 활용하겠습니다. 다음 글에서 좀더 복잡한 예를 처리할 것이구요.

빨간색 군은 Class-0(0이라 표기)이라고 하고, 파란색 군은 Class-1(1이라고 표기)이라고 합시다. 25개의 요소를 새성하고 각 요소에 대해 Class-0과 Class-1 중 하나로 정합니다. 이를 위해 Numpy에서 난수 생성자가 유용합니다.

시각화는 Matplotlib가 유용합니다. 빨간색 군은 빨간색 삼각형으로, 파란색 군은 파란색 사각형으로 표시합니다.

import cv2
import numpy as np
import matplotlib.pyplot as plt

# Feature set containing (x,y) values of 25 known/training data
trainData = np.random.randint(0,100,(25,2)).astype(np.float32)

# Labels each one either Red or Blue with numbers 0 and 1
responses = np.random.randint(0,2,(25,1)).astype(np.float32)

# Take Red families and plot them
red = trainData[responses.ravel()==0]
plt.scatter(red[:,0],red[:,1],80,'r','^')

# Take Blue families and plot them
blue = trainData[responses.ravel()==1]
plt.scatter(blue[:,0],blue[:,1],80,'b','s')

plt.show()

실행 결과는 다음과 같습니다. 난수를 사용했으므로 실행 결과는 매번 다릅니다.

다음으로 trainData와 responses 변수를 전달하여 kNN 알고리즘을 초기화 합니다(검색 트리가 구성됩니다).

이제 새로운 요소를 가져와 이 새로운 요소가 빨간색 또는 파란색 중 어디로 분류될지 OpenCV의 kNN을 이용합니다. kNN으로 가기 전에 먼저 새로운 요소, 즉 테스트 데이터에 대한 정보를 알아야 합니다. 이 테스트 데이터는 실수형 타입의 배열로써 크기는 입니다. 이제 새로운 요소와 가장 가까운 요소를 찾습니다. 가장 가까운 요소를 몇개까지 찾을 것인지 지정할 수 있습니다. 이러한 검색의 결과는 다음과 같습니다.

  • kNN 이론의 기반하여 새로운 요소에 Class-0과 Class-1과 같은 이름이 반환됩니다. 만약 매우 단순한 Nearest Neighbour 알고리즘을 원한다면 k=1로 지정합니다.
  • 새로운 요소와 각각의 가까운 요소 사이의 거리값이 반환됩니다.

지금까지의 설명에 대한 코드는 다음과 같습니다. 이 추가 코드는 앞의 코드 중 18번째 줄에 추가됩니다.

newcomer = np.random.randint(0,100,(1,2)).astype(np.float32)
plt.scatter(newcomer[:,0],newcomer[:,1],80,'g','o')

knn = cv2.ml.KNearest_create()
knn.train(trainData, cv2.ml.ROW_SAMPLE, responses)
ret, results, neighbours ,dist = knn.findNearest(newcomer, 3)

print("result: ", results)
print("neighbours: ", neighbours)
print("distance: ", dist)

실행 결과는 다음과 같습니다.

콘솔에 출력된 결과는 다음과 같구요.

result: [[0.]]
neighbours: [[0. 1. 0.]]
distance: [[ 32. 202. 261.]]

결과는 빨간색(Class-0)으로 분류되었다는 것과 새로운 요소(초록색 원)에서 가장 가까운 3개의 요소가 있으며, 검색된 3개의 요소는 빨간색 요소(Class-0)이 2개이고 파란색 요소(Class-1)이 1개이며 각각의 거리 값입니다.

만약 새로운 요소의 개수가 많다면 배열로 전달할 수 있으며, 각각의 분류 결과는 배열로써 얻어집니다. 아래는 10개의 새로운 요소에 대한 분류에 대해 앞의 코드를 대체하는 코드입니다.

newcomers = np.random.randint(0,100,(10,2)).astype(np.float32)
plt.scatter(newcomers[:,0],newcomers[:,1],80,'g','o')

knn = cv2.ml.KNearest_create()
knn.train(trainData, cv2.ml.ROW_SAMPLE, responses)
ret, results, neighbours ,dist = knn.findNearest(newcomers, 3)

print("result: ", results)
print("neighbours: ", neighbours)
print("distance: ", dist)