코딩 더 매트릭스 - 9장 내적

norm, 내적, 직교

2019년 02월 09일

필립 클라인의 저서 코딩 더 매트릭스 9장 내적


  1. norm과 내적의 정의와 성질을 이해합니다.
  2. 직교의 의미를 이해합니다.
  3. 투영을 이용해 벡터를 분해하고 분해한 성분이 어떤 의미를 가지는지 알아봅니다.
  4. 내적을 이용해 간단한 기계 학습을 배워봅니다.





norm과 내적

두 벡터 uuvv의 거리는 차분 vuv - u의 길이로 정의할 수 있습니다. 벡터에 대해서는 "길이"라는 용어 대신 보통 norm을 사용합니다. 벡터 vv의 norm은 v\parallel v \parallel로 씁니다. norm은 다음 성질들을 만족합니다.

  • 임의의 벡터 vv에 대해 v\parallel v\parallel은 영이 아닌 실수이다.
  • 임의의 벡터 vv에 대해 v\parallel v\parallel이 영이 될 필요충분조건은 vv가 영벡터인 것이다.
  • 임의의 벡터 vv와 스칼라 α\alpha에 대해 αv=αv\parallel{\alpha v}\parallel = |\alpha| \parallel v\parallel 이다.
  • 임의의 벡터 uuvv에 대해 u+vu+v\parallel{u + v}\parallel \le \parallel u\parallel + \parallel v\parallel이다.

마지막 성질은 아래 삼각형을 보면 쉽게 이해할 수 있습니다. 삼각형의 한변의 길이는 절대 다른 두변의 길이의 합보다 크지 않습니다.





실수 벡터들에 대한 내적

R\Bbb{R}상의 벡터들에 대한 내적은 도트곱으로 정의됩니다.

u,v=uv\langle u,v \rangle = u \cdot v

실수 벡터들에 대한 내적은 도트곱의 성질들을 따릅니다.

  • u+v,w=u,w+v,w\langle u + v, w \rangle = \langle u, w \rangle + \langle v, w \rangle
  • u,v=v,u\langle u, v\rangle = \langle v, u\rangle
  • αu,v=αu,v\langle \alpha u, v \rangle = \alpha \langle u, v \rangle

벡터 vv의 norm은 벡터들의 내적으로 다음과 같이 쓸 수 있습니다.

v=v,v\parallel v \parallel = \sqrt{\langle v, v \rangle}

v=[v1,v2,...,vn]v = [v_1, v_2, ..., v_n]이라 하면

v2=v,v=v12+v22+...+vn2=iDvi2\begin{aligned} \parallel v \parallel^2 & = \langle v, v \rangle \\ & = {v_1}^2 + {v_2}^2 + ... + {v_n}^2 \\ & = \sum_{i \in D} {v_i}^2 \end{aligned}

따라서 v=iDvi2\parallel v \parallel = \sqrt{\sum_{i \in D} {v_i}^2} 입니다.





직교성 Orthogonality

직교(Orthogonal)는 수직(perpendicular)에 대한 수학적인 용어입니다. 직교의 정의에 대해 알아보기 전에 피타고라스의 정리를 이용해 그 동기를 살펴보겠습니다.

위 삼각형에서 빗변 u+vu + v의 제곱의 길이는 다음과 같습니다.

u+v2=u+v,u+v=u+v,u+u+v,v=u,u+v,u+u,v+v,v=u2+2u,v+v2\begin{aligned} {\parallel u + v \parallel}^2 & = \langle u + v, u + v \rangle \\ & = \langle u + v, u \rangle + \langle u + v, v \rangle \\ & = \langle u, u \rangle + \langle v, u \rangle + \langle u, v \rangle + \langle v, v \rangle \\ & = {\parallel u \parallel}^2 + 2 \langle u, v \rangle + {\parallel v \parallel}^2 \end{aligned}

마지막 표현이 u2+v2{\parallel u \parallel}^2 + {\parallel v \parallel}^2가 되기 위한 필요충분조건은 u,v=0\langle u, v \rangle = 0이 될 때입니다. 이때 uuvv는 직교합니다.

피타고라스 정리의 a2=b2+c2a^2 = b^2 + c^2은 직각삼각형에 대해 성립하므로 만약 벡터 uu, vv가 직교하면 다음이 성립합니다.

u+v2=u2+v2{\parallel u + v \parallel}^2 = {\parallel u \parallel}^2 + {\parallel v \parallel}^2

직교의 몇가지 성질을 알아보겠습니다.

  • uuvv가 직교할 때 uu는 모든 스칼라 α\alpha 에 대해 αv\alpha v 와 직교합니다.
u,αv=αu,v=α0=0\because \langle u, \alpha v \rangle = \alpha \langle u, v \rangle = \alpha 0 = 0
  • uuvv 둘 다 ww와 직교할때 u+vu + vww와 직교합니다.
u+v,w=u,w+v,w=0+0=0\because \langle u + v, w \rangle = \langle u, w \rangle + \langle v, w \rangle = 0 + 0 = 0





평행 및 수직 성분으로의 벡터 분해

임의의 벡터 bbvv에 대해 bvb^{\parallel v}bbvv를 따른 투영(projection), bvb^{\perp v}bbvv에 직교하는 투영이라고 정의합니다.

b=bv+bvb = b^{\parallel v} + b^{\perp v}

bvb^{\parallel v}vv와 방향이 같거나 반대이고 크기가 다른 벡터이므로 다음과 같이 쓸 수 있습니다. 어떤 스칼라 σR\sigma \in R에 대해,

bv=σvb^{\parallel v} = \sigma v

그리고 bvb^{\perp v}vv에 직교합니다.

벡터 bbvv에 대해 bb에 가장 가까운 Span{v}Span\{v\}내의 점은 bvb^{\parallel v}이고 그 거리는 bvb^{\perp v}입니다. 이를 증명하기 위해 Span{v}Span\{v\}내의 벡터 pp를 두고 다음과 같이 그려보겠습니다.

세 점 bb, bvb^{\parallel v}, pp는 삼각형을 형성하고 있습니다. 여기서 피타고라스의 정리에 의해 세 점을 다음과 같이 쓸 수 있습니다.

bp2=bvp2+bbv2{\parallel b - p \parallel}^2 = {\parallel b^{\parallel v} - p \parallel}^2 + {\parallel b - b^{\parallel v} \parallel}^2

만약 bvpb^{\parallel v} \neq p이면 bvp2>0{\parallel b^{\parallel v} - p \parallel}^2 > 0입니다. 따라서 bbv2<bvp2+bbv2{\parallel b - b^{\parallel v} \parallel}^2 < {\parallel b^{\parallel v} - p \parallel}^2 + {\parallel b - b^{\parallel v} \parallel}^2 이므로 bbv<bp\parallel b - b^{\parallel v}\parallel < \parallel b - p \parallel 입니다. 이는 bb에 가장 가까운 Span{v}Span\{v\}내의 벡터는 bvb^{\parallel v}임을 의미하고 이 때 거리 bbv\parallel b - b^{\parallel v} \parallelbv\parallel b^{\perp v} \parallel입니다.





투영 및 가장 가까운 점 찾기

bv=σvb^{\parallel v} = \sigma v에서 σ\sigma는 어떻게 구할 수 있을까요? 직교하는 두 벡터 bvb^{\perp v}vv를 내적하면 0이므로 다음과 같이 쓸 수 있습니다.

bv,v=0\langle b^{\perp v}, v \rangle = 0

bv=bbvb^{\perp v} = b - b^{\parallel v}이므로 다음과 같이 바꿔 쓸 수 있습니다.

bbv,v=b,vbv,v=b,vσv,v=b,vσv,v\begin{aligned} \langle b - b^{\parallel v}, v \rangle & = \langle b, v \rangle - \langle b^{\parallel v}, v \rangle \\ & = \langle b, v \rangle - \langle \sigma v, v \rangle \\ & = \langle b, v \rangle - \sigma \langle v, v \rangle \end{aligned}

σ\sigma에 대해 풀면 다음을 얻습니다.

σ=b,vv,v\sigma = \frac{\langle b, v \rangle}{\langle v, v \rangle}

위의 정리를 기반으로 bvb^{\parallel v}bvb^{\perp v}를 구하는 프로시저 project_along, project_orthogonal_1를 작성해 보겠습니다.

def project_along(b, v):
    sigma = (b * v) / (v * v) if v * v != 0 else 0
    return sigma * v

def project_orthogonal_1(b, v):
    return b - project_along(b, v)





외적과 투영

벡터 uuvv의 외적은 행렬-행렬 곱 uvTuv^T으로 정의됩니다.

[ u ][ vT ]\begin{bmatrix} \text{ } \\ u \\ \text{ } \end{bmatrix} \begin{bmatrix} \text{ } & v^T & \text{ } \end{bmatrix}

외적을 이용해 vv를 따른 bb의 투영 bvb^{\parallel v}를 표현할 수 있습니다. bv=σvb^{\parallel v} = \sigma v이고 σ=v,vb,v\sigma = \frac{\langle v, v \rangle}{\langle b, v \rangle}이므로 다음과 같이 쓸 수 있습니다.

bv=bvv,vvb^{\parallel v} = \frac{b \cdot v}{\langle v, v \rangle}v

위 식을 행렬-행렬 곱셈으로 표현하면 다음과 같습니다.

1v,v[ v ]([ vT ][ b ])=1v,v([ v ][ vT ])행렬[ b ]\begin{aligned} \frac{1}{\langle v, v \rangle} \begin{bmatrix} \text{ }\\ v \\ \text{ } \end{bmatrix} \begin{pmatrix} \begin{bmatrix} \text{ } & v^T & \text{ } \end{bmatrix} \begin{bmatrix} \text{ }\\ b \\ \text{ } \end{bmatrix} \end{pmatrix} = \frac{1}{\langle v, v \rangle} \underbrace{ \begin{pmatrix} \begin{bmatrix} \text{ }\\ v \\ \text{ } \end{bmatrix} \begin{bmatrix} \text{ } & v^T & \text{ } \end{bmatrix} \end{pmatrix} }_{행렬} \begin{bmatrix} \text{ }\\ b \\ \text{ } \end{bmatrix} \end{aligned}

다음은 위 식의 행렬을 찾는 프로시저 projection_matrix입니다.

from matutil import zero_mat
from vecutil import list2vec

def projection_matrix(v):
    M = zero_mat((v.D, v.D))
    norm = v * v
    for r in v.D:
        for c in v.D:
            M[r, c] = v[r] * v[c]
    return M * (1 / norm)





기계학습

직교와 투영에 기반에 간단한 기계학습을 살펴보겠습니다. 데이터는 유방암 진단에 대한 각각의 피쳐벡터를 행으로 가지는 행렬과 결과벡터입니다. 행렬 방정식 Mx=bMx = b의 행렬 MM과 벡터 bbcancer_data.py의 프로시저 read_training_data로 가져올 수 있습니다.

from cancer_data import read_training_data

M, b = read_training_data('../assets/train.data')

기계학습을 통해 Mx=bMx = b를 최대한 만족하는 xˉ\bar{x}를 찾게 될 것입니다. 이 때 Mxˉ=bˉM\bar{x} = \bar{b}라 하면 학습한 데이터를 바탕으로 도출한 결과와 실제 데이터와의 차이, 즉 에러를 (bˉb)2(\bar{b} - b)^2으로 나타냅니다. 기계학습을 이미 배웠다면 익숙할 것입니다. 가설 벡터 ww와 피쳐벡터 aia_i에 대해 함수 hh를 다음과 같이 쓰겠습니다.

h(ai)=waih(a_i) = w \cdot a_i

앞서 Mxˉ=bˉM\bar{x} = \bar{b}bˉ\bar{b}의 한 엔트리는 h(ai)h(a_i)입니다. 따라서 전체 에러를 다음과 같이 쓸 수 있습니다.

(h(a1)b1)2+(h(a2)b2)2+...+(h(am)bm)2(h(a_1) - b_1)^2 + (h(a_2) - b_2)^2 + ... + (h(a_m) - b_m)^2

행렬A와 결과벡터 b, 가설 벡터 w를 인수로 받아 전체 에러를 구하는 프로시저 loss를 작성해 보겠습니다.

def loss(A, b, w):
    diff = A * w - b
    return diff * diff

제곱의 합은 내적임을 이용해 반복문 없이 간단하게 프로시저를 작성할 수 있었습니다.

전체 에러의 최소화를 위해 경사하강법(그래디언트 디센트)를 알아볼 필요가 있습니다. 만약 솔루션 공간을 평면으로 그릴 수 있다면 다음과 같은 입체 평면이 그려집니다.

솔루션의 초기값이 위 평면의 임의의 지점에 있다고 가정하겠습니다. 최솟값에 도달하기 위해 지금의 점에서 가장 경사가 급한 지점으로 하강하고 다시 이동한 지점에서 경사가 가장 급한 지점으로 하강하면 최솟값에 도달하리라 예상할 수 있습니다. (실제로 전체 평면의 최소지점이 아닌 국지적인 최소지점에 도달할 수도 있습니다.) 이를 위해 그래디언트 f\nabla f를 이용합니다. 그래디언트는 미적분학에서 쓰는 용어로 함수 f([x1,...,xn])f([x_1, ..., x_n])의 그래디언트는 다음과 같이 정의됩니다.

[fx1,...,fxn][\frac{\partial f}{\partial x_1}, ..., \frac{\partial f}{\partial x_n}]

전체 에러를 구하는 손실함수를 L(x)L(x)라 하면 L(x)L(x)를 다음과 같이 쓸 수 있습니다.

L(x)=i=1m(aixbi)2L(x) = \sum_{i=1}^{m}(a_i \cdot x - b_i)^2

L(x)L(x)xjx_j에 대해 편미분하면

Lxj=i=1mxj(aixbi)2=i=1m2(aixbi)aij\begin{aligned} \frac{\partial L}{\partial x_j} & = \sum_{i=1}^{m}\frac{\partial}{\partial x_j}(a_i \cdot x - b_i)^2 \\ & = \sum_{i=1}^{m}2 (a_i \cdot x - b_i) a_{ij} \end{aligned}

여기서 aija_{ij}xaix \cdot a_i에서 xjx_j와 곱해지는 aia_i의 엔트리입니다. 각각의 편미분을 구하면 그래디언트 값인 벡터가 구해집니다.

L(x)=[i=1m2(aixbi)ai1,i=1m2(aixbi)ai2,...,i=1m2(aixbi)aij]\nabla L(x) = [\sum_{i=1}^{m}2 (a_i \cdot x - b_i) a_{i1}, \sum_{i=1}^{m}2 (a_i \cdot x - b_i) a_{i2}, ..., \sum_{i=1}^{m}2 (a_i \cdot x - b_i) a_{ij}]

그래디언트를 찾는 프로시저 find_grad를 작성해보겠습니다. 연산 과정을 떠올려 보면 벡터-행렬 곱셈을 이용해 간단하게 구현할 수 있습니다.

def find_grad(A, b, w):
    return 2 * (A * w - b) * A

그래디언트 디센트는 find_grad를 통해 찾은 그래디언트에 작은 스칼라를 곱한 값을 원래의 ww에서 뺌으로서 ww를 업데이트 합니다. 이 것은 솔루션 평면에서 한 스텝 하강하는 과정과 같습니다. 이 때 곱하는 작은 스칼라는 스텝 크기라 하고 σ\sigma로 나타냅니다.

def gradient_descent_step(A, b, w, sigma):
    return w - sigma * find_grad(A, b, w)

프로시저 gradient_descent_step를 사용해 주어진 이터레이션 수만큼 하강을 반복하는 프로시저 gradient_descent를 작성해 보겠습니다.

def gradient_descent(A, b, w, sigma, T):
    for i in range(T):
        w = gradient_descent_step(A, b, w, sigma)
        if i % 30 == 0:
            print(loss(A, b, w))
    return w

위 프로시저에서는 이터레이션의 매 30번마다 현재 전체 에러를 출력합니다.

이제 유방암 진단 데이터를 이용해 가설 벡터를 구할 수 있도록 학습하고 validate.data에 적용해 보겠습니다.

from vecutil import list2vec
from vec import Vec

A, b = read_training_data('../assets/train.data')
w = Vec(A.D[1], {})
sigma = 10e-10	# 스텝 크기
T = 300			# 반복횟수
w = gradient_descent(A, b, w, sigma, T)
A_val, b_val = read_training_data('../assets/validate.data')
print(loss(A_val, b_val, w))

>>>
204.03071938964135

전체 에러만으로는 실제 결과와 얼마나 다른지 직관적으로 보기가 어렵습니다. 아래는 실제 데이터와 예측한 데이터가 얼마나 다른지 보여주고자 만든 프로시저입니다.

from vecutil import zero_vec

def signum(u):
    v = zero_vec(u.D)
    for d in u.D:
        if u[d] >= 0:
            v[d] = 1
        else:
            v[d] = -1
    return v

def fraction_wrong(A, b, w):
    r = signum(A * w)
    diff = (r - b) / 2
    return (diff * diff) / len(b.D)

프로시저 signum은 양수는 모두 1에 음수는 모두 -1에 매핑합니다. fraction_wrong는 벡터의 엔트리 값이 항상 1 또는 -1임을 이용해 실제와 예측이 얼마나 다른지 보여줍니다.

print(fraction_wrong(A_val, b_val, w))

>>>
0.1