최적 가중치값을 얻기 위한 경사하강법을 Python 코드로 이해하기

이 글은 텐서플로나 파이토치와 같은 같은 딥러닝 라이브러리의 기반을 이해하거나, 직접 개발하고자 할때 참고할 만한 글로 작성한 코드는 이해를 위해 나이브하게 작성했다 . 아래는 이 글에서 사용할 모델로 단순화를 위해 은닉층은 없고 입력층과 출력층만이 존재한다.

입력값 x와 해당 입력값에 대한 라벨값 t는 다음과 같다.

x = np.array([20.1, 32.2])
t = np.array([0, 0, 1])

데이터가 단일 항목인데, 실제는 그 개수가 상당이 많을 것이다. 가중치 W는 입력값의 특성개수가 2개이고 츨력값의 분류수가 3개이므로 2×3 행렬이며, 초기값은 아래처럼 난수로 잡는다.

W = np.random.randn(2, 3)

모델에 대한 입력 데이터가 정해졌으므로, 초기에 난수로 정한 가중치값을 보정하기 위한 방법인 경사하강법에 대한 함수인 gradient를 다음처럼 사용할 수 있다.

dW = gradient(x, t, W)
print(dW)

gradient 함수는 인자로 입력값과 라벨값 그리고 보정할 가중치값이 저장된 행렬을 받으며, 가중치값들을 보정할 경사도(미분값)를 가중치 행렬과 동일한 크기로 반환한다. 먼저 손실함수 loss는 다음과 같다.

def predict(input, weight):
    return np.dot(input, weight)

def softmax(input):
    input = input - np.max(input)
    return np.exp(input) / np.sum(np.exp(input))

def cee(activated, label):
    label = label.argmax(axis=0)
    result = -np.sum(np.log(activated[label] + 1e-7))

    return result    

def loss(input, label, weight):
    output = predict(input, weight)
    activated = softmax(output)
    loss = cee(activated, label)

    return loss

손실함수는 교차 엔트로피 오차(Cross Entropy Error, CEE) 함수를 사용했으며, 출력층에 대한 활성화 함수는 Softmax를 사용한 것을 알 수 있다. 이제 gradient 함수는 아래와 같다.

def gradient(input, label, weight):
    h = 1e-4
    grad = np.zeros_like(weight)
    
    it = np.nditer(weight, flags=['multi_index'])
    while not it.finished:
        idx = it.multi_index
        v = weight[idx]

        weight[idx] = v + h
        fxh1 = loss(input, label, weight)
        
        weight[idx] = v - h 
        fxh2 = loss(input, label, weight)
        
        grad[idx] = (fxh1 - fxh2) / (2*h)
        
        weight[idx] = v

        it.iternext()   
        
    return grad

가중치에 대한 W의 각 요소별로 편미분을 기울기 방향을 구하고 있다. 이 기울기 방향으로 일정한 길이만큼 가중치값을 이동해 주는 것을 반복하면 최소의 손실값을 갖는 가중치들의 모음을 얻을 수 있게 된다. 기울기 방향을 구하기 위한 방법으로 편미분에 대한 수치해석 기법을 활용했으나, 실제 텐서플로우나 파이토치 등과 같은 머신러닝 라이브러리에서는 역전파기법을 활용한다.

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()

결과는 아래와 같다.