PCA를 이용한 도형에 대한 주축 구하기

PCA는 주성분분석(Principal Component Analysis)로 차원감소에 활용됩니다. 예를들어 2차원에 분포된 데이터를 1차원의 어떤 축에 대해 투영했을때, 투영되어진 데이터의 분포(데이터간의 거리)가 가장 큰 축, 바로 이 축이 주성분이며, 이를 찾는 방법입니다. GIS에서도 이 PCA를 이용하는 경우가 있는데, 이해를 위해 Python 언어로 설명해 봅니다.

Python 언어로 정의된 다음과 같은 좌표가 있다.

X = np.array([
    [150,  60, -30, -20, -120, -90],
    [ 70, 180,  90,  50, -30,   70]
], dtype=np.float64)

X = X.transpose()

X축에 대한 좌표값과 Y축에 대한 좌표값 각각으로 구성되다가 전치되어 X, Y 좌표가 쌍을 맺는 형태이다. 위의 좌표를 화면에 표시해 보기 위해 이미지 버퍼를 생성하고 표시한다. 아래처럼..

dst = np.full((512,512,3), (255, 255, 255), dtype= np.uint8)

rows, cols, _ = dst.shape
centerX = cols // 2
centerY = rows // 2

cv2.line(dst, (0,256), (cols-1, 256), (0, 0, 0))
cv2.line(dst, (256,0), (256,rows), (0, 0, 0))

FLIP_Y = lambda y: rows - 1 - y

for k in range(X.shape[0]):
    x, y = X[k,:]
    cx = int(x+centerX)
    cy = int(y+centerY)
    cy = FLIP_Y(cy)
    cv2.circle(dst, (cx,cy), radius=5, color=(0,0,255), thickness=-1)
    
cv2.imshow('dst', dst)               
cv2.waitKey()    
cv2.destroyAllWindows()

결과는 아래와 같다.

1번 코드에서 좌표값을 고려해서 충분한 크기(512×512)의 버퍼를 준비한다. 7, 8번 코드는 화면의 정중앙을 원점으로 XY축을 그려주는 것이다. Y축에 대해서는 수학적인 축방향과 컴퓨터 그래픽 화면의 축방향이 반대이므로 10번 람다 함수가 필요하다. 12번 코드에 표시된 반복문을 통해 각 좌표에 대해 빨간색 원으로 표시한다. (꼭 필요한 부분은 아니지만) 입력 데이터를 시각화했으므로 이제 주축을 계산해보자.

cov, mean = cv2.calcCovarMatrix(X, mean=None, flags=cv2.COVAR_NORMAL+cv2.COVAR_ROWS)
ret, eVals, eVects = cv2.eigen(cov)

def ptsEigenVector(eVal, eVect):
    scale = np.sqrt(eVal)
    
    x1 = scale*eVect[0]
    y1 = scale*eVect[1]

    x2, y2 = -x1, -y1

    x1 += mean[0,0] + centerX
    y1 += mean[0,1] + centerY
    x2 += mean[0,0] + centerX
    y2 += mean[0,1] + centerY
    
    y1 = FLIP_Y(y1)
    y2 = FLIP_Y(y2)

    return x1, y1, x2, y2

1번 코드의 cov, mean은 각각 X축과 Y축을 구성하는 각각의 공분산 그리고 좌표값의 평균이다. 2번 코드의 eVals와 eVects는 공분산 cov에 대한 고유값과 고유벡터이다. 함수 ptsEigenVector는 주축 계산을 위한 함수이다. 이 함수의 쓰임은 아래와 같다.

x1x, y1x, x2x, y2x = ptsEigenVector(eVals[0], eVects[0])
x1y, y1y, x2y, y2y = ptsEigenVector(eVals[1], eVects[1])

x1x, y1x, x2x, y2x는 X축에 대한 주축 선분의 시작점과 끝점이고, x1y, y1y, x2y, y2y는 Y축에 대한 주축의 시작점과 끝점이다. 이제 결과를 시각화 해보자.

cv2.line(dst, (x1x, y1x), (x2x, y2x), (255, 0, 0), 1)
cv2.line(dst, (x1y, y1y), (x2y, y2y), (255, 0, 0), 1)

cv2.imshow('dst', dst)               
cv2.waitKey()    
cv2.destroyAllWindows()

결과는 아래와 같다.

scikit-learn의 SVM을 통한 분류(Classification)

SVM(Support Vector Machine)은 데이터 분석 중 분류에 이용되며 지도학습 방식의 모델입니다. SVM에 대한 좋은 구현체는 사이킷-런(scikit-learn)인데, 이를 이용해 SVM에 대한 내용을 정리해 봅니다.

먼저 학습을 위한 입력 데이터가 필요한데, scikit-learn은 데이터 분류를 목적으로 데이터를 생성해 주는 make_blobs라는 함수를 제공합니다. 이를 이용해 아래처럼 2종류의 총 40개의 샘플 데이터를 생성합니다.

import numpy as np
import matplotlib.pyplot as plt
from sklearn import svm
from sklearn.datasets.samples_generator import make_blobs

X, y = make_blobs(n_samples=40, centers=2, random_state=20)

위에서 생성한 데이터 샘플을 SVM으로 학습시키는 코드는 다음과 같습니다.

clf = svm.SVC(kernel='linear')
clf.fit(X, y)

SVM은 선형 분류와 비선형 분류를 지원하는데, 그 중 선형 모델을 위해 kernel을 linear로 지정하였습니다. 비선형에 대한 kernel로는 rbf와 poly 등이 있습니다.

학습된 SVM 모델을 통해 데이터 (3,4)를 분류하는 코드는 다음과 같습니다.

newData = [[3,4]]
print(clf.predict(newData))

다음은 시각화입니다. 샘플 데이터와 초평면(Hyper-Plane), 지지벡터(Support Vector)를 그래프에 표시하는 코드는 다음과 같습니다.

# 샘플 데이터 표현
plt.scatter(X[:,0], X[:,1], c=y, s=30, cmap=plt.cm.Paired)

# 초평면(Hyper-Plane) 표현
ax = plt.gca()

xlim = ax.get_xlim()
ylim = ax.get_ylim()

xx = np.linspace(xlim[0], xlim[1], 30)
yy = np.linspace(ylim[0], ylim[1], 30)
YY, XX = np.meshgrid(yy, xx)
xy = np.vstack([XX.ravel(), YY.ravel()]).T
Z = clf.decision_function(xy).reshape(XX.shape)

ax.contour(XX, YY, Z, colors='k', levels=[-1,0,1], alpha=0.5, linestyles=['--', '-', '--'])

# 지지벡터(Support Vector) 표현
ax.scatter(clf.support_vectors_[:,0], clf.support_vectors_[:,1], s=60, facecolors='r')

plt.show()

결과는 다음과 같습니다. 빨간색 포인트가 지지벡터이고, 진한 회색선이 초명편입니다.

다음은 비선형 SVM로써 kernel이 rbf인 결과 그래프입니다.

[Python] 알파벳을 인덱스로 구성하기

먼저 다음과 같은 문장이 있다고 하자.

sample = 'I will go.'

위의 문장을 구성하는 알파벳 중에서 중복되지 않는 고유한 알파벳만을 추출하면..

uniq_chars = set(sample) # {'w', ' ', 'o', 'i', 'l', '.', 'g', 'I'}

위의 추출된 결과는 set이므로 이를 list로 만들면..

idx2char = list(uniq_chars) # ['w', ' ', 'o', 'i', 'l', '.', 'g', 'I']

인덱스 값을 Value로, 해당 인덱스의 알파벳을 Key로 구성된 데이터는 다음처럼 얻을 수 있다.

char2idx = {c: i for i, c in enumerate(idx2char)} # {'w': 0, ' ': 1, 'o': 2, 'i': 3, 'l': 4, '.': 5, 'g': 6, 'I': 7}

이제 처음 문장(sample 변수)을 인덱스 값으로 구성된 list는 다음과 같다.

sample_idx = [char2idx[c] for c in sample] # [7, 1, 0, 3, 4, 4, 1, 6, 2, 5]

Numpy의 axis에 따른 연산

넘파이의 sum 함수를 예로 axis의 값에 따라 어떻게 연산이 처리되는지를 시각화해 본다.

먼저 x는 다음과 같다.

x = np.array([
    [ 1,  2,  3,  4],
    [ 5,  6,  7,  8],
    [ 9, 10, 11, 12],
])

위의 x를 행렬로 시각화 하면 다음과 같다.

이 x에 대한 axis=0으로 한 sum 함수에 대한 코드는 다음과 같으며 그 결과는 바로 다음의 그림과 같다.

np.sum(x, axis=0)

이 x에 대한 axis=1으로 한 sum 함수에 대한 코드는 다음과 같으며 그 결과는 바로 다음의 그림과 같다.

np.sum(x, axis=1)

수치미분(접선)의 결과를 그래프로 표현하기

다음과 같은 함수가 있을 때.. 이 함수를 미분한 결과는 이 함수의 그래프에 대한 접선의 방정식이 됩니다.

위 함수에 대한 코드 정의는 다음과 같습니다.

def fx(x):
    return x**3 + x

미분은, 중앙차분 방식으로 정의하면 다음과 같구요.

def numerical_diff(f, x):
    h = 1e-4
    return (f(x+h) - f(x-h)) / (2*h)

미분 결과는 접선인데, 이 접선을 표현하는 함수를 반환하는 함수는 다음과 같습니다.

def tangent_line(f, x):
    d = numerical_diff(f, x)
    y = f(x) - d*x
    return lambda t: d*t + y

이제 x 절편의 범위를 0~20까지 잡고 함수의 그래프와 이 함수의 x = 11에서의 접선을 그리는 코드는 다음과 같습니다.

import numpy as np
import matplotlib.pylab as plt

# def numerical_diff(f, x):
# def tangent_line(f, x):
# def fx(x):

x = np.arange(0.0, 20.0, 0.1)
y = fx(x)
plt.plot(x,y)

tf = tangent_line(fx, 11)
y2 = tf(x)
plt.plot(x, y2)

plt.show()

결과 그래프는 다음과 같습니다.

이와 같은 미분에 대한 파이선 코드는 기계 학습이나 신경망 학습에서 가중치와 편향에 대한 최적의 값을 얻기 위해 활용되는 경사하강법(Gradient Descent Method)에서 사용됩니다.