[1/18] 행렬과 벡터의 곱

해당 시리즈는 프로그래밍 언어 중 하나인 줄리아(Julia)로 딥러닝(Deep learning)을 구현하면서 원리를 설명합니다.


벡터란

벡터(Vector)는 '크기'와 '방향'을 모두 나타낸다. 예를 들어, 자동차가 시속 5마일로 달린다고 해보자. 여기서 5마일은 단순히 크기일뿐 방향을 나타내고 있지 않으므로 벡터라고 할 수 없다. 벡터로 나타내기 위해서는 5마일에 동쪽으로 향하는 '크기'와 '방향'까지 포함된 개념이어야 한다.

벡터의 기호는 \(\overrightarrow{a}\)이며, 아래와 같은 '열벡터'가 기본 형태이다.

\[\overrightarrow{a}= \begin{bmatrix} X \\ Y \\ \end{bmatrix}\]

벡터의 성분 개수와 차원은 동일하기 때문에 위의 열벡터는 성분 xy 를 가진 2차원 벡터이다.

행벡터는 기본형인 열벡터를 '전치'한 형태이다. 여기서 전치란 주대각선(Main diagonal)을 대칭한 것이며, 보통 행렬에서 많이 사용된다.

\[\overrightarrow{a}^T= \begin{bmatrix} X&Y \end{bmatrix}\]

행렬의 경우 아래의 행렬과 벡터의 곱에서 더 자세하게 설명할 것이다.

벡터의 내적

벡터의 곱셈은 크게 '외적(Cross product)'과 '내적(Dot product)' 2가지의 방법이 사용되며, 우리는 그 중 딥러닝에서 사용하는 내적에 대해 살펴볼 것이다.

내적을 수학식으로 표현하면 아래와 같다.

\[\overrightarrow{a}\cdot\overrightarrow{b}=\sum_{i=1}^n a_ib_i=a_1b_1+a_2b_2+a_3b_3+\cdots+a_nb_n\]

열벡터 a와 b를 내적하면 같은 위치에 있는 성분끼리 곱한 수들을 더한다. 즉, 열벡터 당 하나의 스칼라가 값으로 나오는 것이다.

\[\overrightarrow{a}=\begin{bmatrix}a_1\\a_2\\a_3\\\vdots\\a_n\end{bmatrix}\quad \overrightarrow{b}=\begin{bmatrix}b_1\\b_2\\b_3\\\vdots\\b_n\end{bmatrix}\quad \overrightarrow{a}\cdot\overrightarrow{b}=a_1b_1+a_2b_2+a_3b_3\cdots a_nb_n\]

이와 같은 과정을 벡터의 내적이라고 한다.

행렬과 벡터의 곱

행렬이란 \(m \times n\)의 2차원 배열이다. 여기서 \(m\)은 행의 개수이고 \(n\)은 열의 개수이다. 아래는 \(m \times n\)의 행렬을 시각화한 것이다.

\[A= \begin{bmatrix} a_{11} & \cdots & a_{1n} \\ \vdots & \ddots & \vdots \\ a_{m1} & \cdots & a_{mn} \end{bmatrix}\]

그렇다면 위와 같은 행렬을 벡터와 곱하는 것이 가능할까?

NOTE 앞의 질문에 대답하기 전에 행렬과 벡터의 관계를 명확히 하려고 한다. 앞서 백터를 설명하는 섹션에서 봤던 열벡터 \(\overrightarrow{a}\)\(2 \times 1\) 행렬이라고 할 수 있으며, \(\overrightarrow{a}^T\)\(1 \times 2\) 행렬이라고 할 수 있다. 즉, 열벡터와 행벡터 모두 행렬로서 바라볼 수 있으며 그 반대도 가능하다.

행렬과 벡터를 곱하는 방법은 2가지가 있다.

행 단위를 행벡터로 가정하여 내적한다

\[A\overrightarrow{x}= \begin{bmatrix} a_{11} & a_{12} & \cdots & a_{1n} \\ a_{21} & a_{22} & \cdots & \vdots \\ \vdots & \cdots & \ddots & \vdots \\ a_{m1} & \cdots & \cdots & a_{mn} \end{bmatrix} \cdot \begin{bmatrix} x_1\\ x_2\\ \vdots\\ x_n\\ \end{bmatrix} = \begin{bmatrix} a_{11}x_1 + a_{12}x_2 + \cdots + a_{1n}x_n \\ a_{12}x_1 + a_{22}x_2 + \cdots\cdots\cdot\ \vdots\ \\ \ \vdots\ \\ a_{m1}x_1 + \cdots\cdots\cdots\cdot + a_{mn}x_n \end{bmatrix} =\overrightarrow{b}\]

위의 공식은 행렬 A의 각 행을 행벡터로 인식하여 \(A\)\(\overrightarrow{x}\)가 내적한다. 즉, 전치행렬 \(A^T\)의 열들을 벡터로 내적하는 것과 같다. 그렇기에 \(\overrightarrow{x}\)의 길이는 행렬 A의 열 개수인 \(n\)이어야 하며, 결과값인 \(\overrightarrow{b}\)는 길이가 \(m\)이다.

\[A\overrightarrow{x}= A(m \times n) \cdot \overrightarrow{x}(n \times 1) = \overrightarrow{b}(m \times 1)\]

직접 숫자를 넣어 예시를 풀어보자.

\[A\overrightarrow{x}= \begin{bmatrix} 3 & 1 & 0 & 3 \\ 2 & 4 & 7 & 0 \\ -1 & 2 & 3 & 4 \\ \end{bmatrix} \cdot \begin{bmatrix} 1\\ 2\\ 3\\ 4\\ \end{bmatrix} = \begin{bmatrix} 3\times1+1\times2+0\times3+3\times4 \\ 2\times1+4\times2+7\times3+0\times4 \\ -1\times1+2\times2+3\times3+4\times4\quad \end{bmatrix} = \begin{bmatrix} 17\\ 31\\ 28\\ \end{bmatrix}\]

위의 예시를 살펴보면 행렬 A의 각각의 행벡터와 \(\overrightarrow{x}\)를 내적하여 나온 스칼라 값이 결과값인 것을 확인할 수 있다. 결국 위의 식을 정리한다면 다음와 같다.

\[A\overrightarrow{x}= \begin{bmatrix} \ \overrightarrow{a_1^T}\ \\ \ \overrightarrow{a_2^T}\ \\ \ \vdots\ \\ \ \overrightarrow{a_n^T}\ \\ \end{bmatrix} \cdot \overrightarrow{x} \ = \begin{bmatrix} \ \overrightarrow{a_1} \cdot\overrightarrow{x}\\ \ \overrightarrow{a_2} \cdot\overrightarrow{x}\\ \ \vdots\\ \ \overrightarrow{a_n} \cdot\overrightarrow{x}\\ \end{bmatrix}\]

행렬을 열벡터의 모음으로 가정하여 내적한 후, 각 행들을 더해준다

다음으로는 '열벡터' 개념만으로 행렬 벡터 곱을 이해하는 방법이다. 어떤 방법을 사용하든 결과는 동일하지만 진행되는 논리구조는 다르다. 둘 중 어느 방법을 사용해도 상관없지만 개인적으로는 이 방법이 더 편하다고 생각한다. 먼저 아래의 행렬을 각각의 열벡터의 모음이라고 생각해보자.

\[\begin{matrix} \overrightarrow{v_1}\ & \overrightarrow{v_2}\ & \cdots & \overrightarrow{v_n} \end{matrix}\] \[\begin{bmatrix} a_{11} & a_{12} & \cdots & a_{1n} \\ a_{21} & a_{22} & \cdots & \vdots \\ \vdots & \cdots & \ddots & \vdots \\ a_{m1} & \cdots & \cdots & a_{mn} \end{bmatrix}\]

위의 도식처럼 행렬 A의 각 열들은 \(v_1\), \(v_2\), \(\cdots\) \(v_n\)인 열벡터로 분리되었다. \(v\)의 개수는 행렬 A의 열 개수와 동일하며, 여기서는 n을 4라고 가정하자. 그 다음 \(\overrightarrow{v}\) 백터들을 각각 \(\overrightarrow{x}\)와 내적한 후, 결과로 나온 값들을 행 단위로 더해주면 된다.

\[A\overrightarrow{x}=\begin{bmatrix} \overrightarrow{v_1}\\ \overrightarrow{v_2}\\ \overrightarrow{v_3}\\ \overrightarrow{v_4}\\ \end{bmatrix} \begin{bmatrix} x_1\\ x_2\\ x_3\\ x_4\\ \end{bmatrix} = \begin{matrix}[x_1\overrightarrow{v_1}+x_2\overrightarrow{v_2}+x_3\overrightarrow{v_3}+x_4\overrightarrow{v_4}]\end{matrix}\]

따라서 \(A\overrightarrow{x}\)\(\overrightarrow{v}\) 벡터들에 \(\overrightarrow{x}\)의 스칼라(상수)를 곱해준 것이다. 이는 \(\overrightarrow{v}\) 벡터들의 값들을 가중해준다는 의미와 같다. 마지막으로 \(\overrightarrow{x}\)의 스칼라(상수)를 곱해진 \(\overrightarrow{v}\) 백터들을 행 단위로 더해주면 된다.

이제는 원리는 파악했으니 아래의 문제를 풀어보도록 하자.

\[A\overrightarrow{x}= \begin{bmatrix} 3 & 1 & 0 & 3 \\ 2 & 4 & 7 & 0 \\ -1 & 2 & 3 & 4 \\ \end{bmatrix} \cdot \begin{bmatrix} 1\\ 2\\ 3\\ 4\\ \end{bmatrix}\]

보다시피 위의 문제와 동일하다. 다만 답이 어떻게 도출되는지 과정의 차이를 집중적으로 살펴볼 것이다.

\[\begin{matrix} \overrightarrow{v_1}\ &\overrightarrow{v_2}\ & \overrightarrow{v_3}\ & \overrightarrow{v_4} \end{matrix}\] \[ \begin{bmatrix} 3\\ 2\\ -1\\ \end{bmatrix} \begin{bmatrix} 1\\ 4\\ 2\\ \end{bmatrix} \begin{bmatrix} 0\\ 7\\ 3\\ \end{bmatrix} \begin{bmatrix} 3\\ 0\\ 4\\ \end{bmatrix}\]

\[\begin{matrix}[x_1\overrightarrow{v_1}+x_2\overrightarrow{v_2}+x_3\overrightarrow{v_3}+x_4\overrightarrow{v_4}]\end{matrix}\]

위의 도식처럼 열벡터 단위로 분리한 후, 아래의 공식에 대입한다.

\[\begin{bmatrix} 3\times1+1\times2+0\times3+3\times4 \\ 2\times1+4\times2+7\times3+0\times4 \\ -1\times1+2\times2+3\times3+4\times4\quad \end{bmatrix} = \begin{bmatrix} 3 & 2 & 0 & 12 \\ 2 & 8 & 21 & 0 \\ -1 & 4 & 9 & 16 \\ \end{bmatrix}\]

위의 결과를 행 단위로 더해준다.

\[\begin{bmatrix} 3 + 2 + 0 + 12 \\ 2 + 8 + 21 + 0 \\ -1 + 4 + 9 + 16 \\ \end{bmatrix} = \begin{bmatrix} 17 \\ 31 \\ 28 \\ \end{bmatrix}\]

보다시피 첫 번째 방법과 결과는 같다. 위의 예시들은 모두 행렬과 열벡터 한 개를 내적하였다. 만약 행렬과 열벡터 여러 개를 한 번에 내적하고 싶다면 어떻게 해야 하는가? 벡터를 하나씩 행렬과 내적한 결과를 순서대로 나열하면 될 것이다. 이를 한번에 진행해주는 연산방법을 '행렬곱'이라고 한다.

행렬곱

행렬곱은 여러 개의 벡터를 한번에 행렬과 곱하는 것과 동일한 결과를 제공한다. 즉, 위에서 봤던 행렬과 벡터의 곱을 여러 번 진행한 것이라고 볼 수 있다. 만약 행렬 A와 행렬 B를 곱하여 행렬 C를 도출한다고 가정해보자.

\[ C = A*B \rightarrow c_{mn}=a_m^T \cdot b_n \]

행렬 C의 요소 \(c_{mn}\)는 행렬 A의 \(m\)번째 행과 행렬 B의 \(n\)번째 열을 내적한 값이다. 이를 도식으로 나타내면 다음과 같다.

\[\begin{bmatrix} a_{11} & a_{12} & \cdots & a_{1n} \\ a_{21} & a_{22} & \cdots & \vdots \\ \vdots & \cdots & \ddots & \vdots \\ a_{m1} & \cdots & \cdots & a_{mn} \end{bmatrix} \begin{bmatrix} b_{11} & b_{12} & \cdots & b_{1n} \\ b_{21} & b_{22} & \cdots & \vdots \\ \vdots & \cdots & \ddots & \vdots \\ b_{m1} & \cdots & \cdots & b_{mn} \end{bmatrix}\] \[= \begin{bmatrix} (a_{11}b_{11}+a_{12}b_{21}\cdots+a_{1n}b_{m1}) & \cdots & (a_{11}b_{m1}+a_{12}b_{m2}\cdots+a_{1n}b_{mn}) \\ (a_{21}b_{11}+a_{22}b_{21}\cdots+a_{2n}b_{m1}) & \cdots & (a_{21}b_{m1}+a_{22}b_{m2}\cdots+a_{2n}b_{mn}) \\ \vdots & & \vdots \\ (a_{m1}b_{11}+a_{m2}b_{21}\cdots+a_{mn}b_{m1}) & \cdots & (a_{m1}b_{m1}+a_{m2}b_{m2}\cdots+a_{mn}b_{mn}) \end{bmatrix}\]

행렬곱에서는 행렬 두 개가 계산되는 것이므로 결과 또한 행렬의 형태를 가진다.

\[A*B= A(m \times n)*(n \times o) = C(m \times o)\]

이제 예시를 풀어보자.

\[\begin{bmatrix} 1 & 4 \\ 2 & 5 \\ 3 & 6 \\ \end{bmatrix} * \begin{bmatrix} 1 & 2 & 3\\ 4 & 5 & 6\\ \end{bmatrix}\]

위의 행렬들은 \(3\times2\)\(2\times3\)이므로 결과값 행렬은 \(3\times3\)행렬이어야 한다.

\[\begin{bmatrix} (1\times1+4\times4) & (1\times2+4\times5) & (1\times3+4\times6) \\ (2\times1+5\times4) & (2\times2+5\times5) & (2\times3+5\times6) \\ (3\times1+6\times4) & (3\times2+6\times5) & (3\times3+6\times6) \\ \end{bmatrix}\] \[=\] \[\begin{bmatrix} 17 & 22 & 27 \\ 22 & 29 & 36 \\ 27 & 36 & 45 \\ \end{bmatrix}\]

문제의 답으로 \(3\times3\) 행렬이 나온 것을 확인할 수 있다. 이런 행렬곱은 신경망 계산에서 벡터의 단위로 내적 계산을 한번에 하기 위해 사용된다.

줄리아에서의 곱셈

줄리아에서 내적을 사용하기 위해서는 아래의 코드를 입력하여 해당 패키지를 다운로드해야 한다. LinearAlgebra 사용법에 대해 자세히 알고 싶다면 이 링크에서 확인할 수 있다.

1
2
3
Julia> import Pkg
Julia> Pkg.add("LinearAlgebra")
Julia> using LinearAlgebra

준비가 끝났다면 줄리아에서 내적을 어떻게 계산하는지 살펴보자. 줄리아에서 함수 dot()은 두 개의 백터를 내적한다. 아래의 예시를 통해 확인해보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Julia> a = ones(1,3)
Julia> a

1×3 Array{Float64,2}:
1.0 1.0 1.0

Julia> b = a'
Julia> b

3×1 Adjoint{Float64,Array{Float64,2}}:
1.0
1.0
1.0

Julia> dot(a,b)
3.0

위의 코드는 \(1\times3\) 행렬인 a와, a의 전치행렬인 b를 생성하고 두 벡터를 내적한다. 그 결과로는 3이 도출되었다. 그렇다면 모든 신경망 계산에 dot()만을 사용하면 되는 것일까? 그렇지 않다. dot()은 모든 배열들을 하나의 벡터로 벡터화하는 문제가 있다. 아래의 예시를 통해서 알아보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Julia> a = ones(2,3)
Julia> a
2×3 Array{Float64,2}:
1.0 1.0 1.0
1.0 1.0 1.0

Julia> b = a'
Julia> b
3×2 Adjoint{Float64,Array{Float64,2}}:
1.0 1.0
1.0 1.0
1.0 1.0

Julia> dot(a,b)
6.0

위의 예시는 \(2\times3\) 행렬인 a와 a의 전치행렬을 생성한다는 점에서 같지만, 각 변수들이 2차원 배열인 매트릭스(Matrix)라는 점에서 다르다. 즉, 첫 번째 예시는 1차 배열을 다루기 때문에 벡터화되어도 상관없는 반면 두 번째 예시는 2차원 배열이기 때문에 행과 열을 보존하여 내적을 해야 하는 것이다. 따라서 이는 행렬곱을 사용하여 처리해주어야 한다. 행렬곱 연산자는 *(a,b)이다.

1
2
3
4
5
6
Julia> a = ones(2,3)
Julia> b = a'
Julia> *(a,b)
2×2 Array{Float64,2}:
3.0 3.0
3.0 3.0

신경망 계산

신경망은 입력층, 은닉층, 출력층으로 구분되며 가중치와 편향을 사용하여 계산한다. 이제 신경망에서 사용되는 계산법을 모두 배웠으므로 위의 지식을 바탕으로 신경망이 어떻게 작동하는지 알아보자. 아래의 그림은 신경망 구조의 예시이다. 참고로 아래의 그림은 하나의 예시일 뿐이며, 각 층 변수의 개수는 가변적으로 설정할 수 있다.

신경망 구조

위의 그림은 2층 신경망 구조이다. 위 그림에서 동그라미는 각 층의 성분으로 노드라고 하며, 노드끼리 연결된 화살표는 가중치인 엣지라고 한다. 지금부터는 위 신경망 구조를 코드로 구현하여 '행렬과 벡터 곱', '행렬곱'을 사용해볼 것이다.

먼저 위 구조를 분해해보도록 하자.

입력층(\(1\times4\) 행렬), 은닉층(\(1\times2\) 행렬), 출력층(\(1\times3\) 행렬)은 다음과 같다.

입력층: \(X=[x_1,x_2,x_3,x_4]\quad\) 은닉층: \(H=[h_1,h_2]\quad\) 출력층: \(Y=[y_1,y_2,y_3]\quad\)

신경망을 계산하기 위해서는 가중치와 편향이 필요하다. 가중치는 각 층 사이에서 곱해지는 행렬이며, 편향은 각 층의 계산마다 더해지는 상수이다. 가중치와 편향에 대한 정의는 4장인 경사하강법에서 자세히 다룰 것이다.

각 층은 가중치인 \(W\)를 곱하고 편향 \(B\)를 더하여 다음 층으로 넘어간다. 이를 수학식으로 표현하면 아래와 같다.

\(H=(X*W)+B\)

위의 신경망 구조에서 가중치와 편향은 총 2개가 필요하다. 먼저 가중치부터 살펴본다면, 첫 번째 가중치는 입력층과 은닉층 사이에 있으므로 \(4\times2\) 행렬이어야 한다. 또한 두 번째는 은닉층과 출력층 사이에 있으므로 \(2\times3\) 행렬이다.

\[가중치 행렬 1: \begin{bmatrix} w_{11} & w_{12} \\ w_{21} & w_{22} \\ w_{31} & w_{32} \\ w_{41} & w_{42} \end{bmatrix}\quad 가중치 행렬 2: \begin{bmatrix} w_{51} & w_{52} & w_{53}\\ w_{61} & w_{62} & w_{63}\\ \end{bmatrix}\]

다음은 편향이다. 편향은 각 층과 가중치가 곱해진 결과에 더하므로 입력층에서 은닉층 사이의 첫 번째 편향은 \(1\times2\) 행렬이며, 두 번째 편향은 \(1\times3\) 행렬이다.

\[ 편향1: \begin{bmatrix} b_1 & b_2 \end{bmatrix}\quad 편향 2: \begin{bmatrix} b_3 & b_4 & b_5 \end{bmatrix}\]

입력층에서 은닉층까지의 계산을 순서대로 나타내면 아래의 수학식과 같다.

\[h_1=w_{11}x_1+w_{21}x_2+w_{31}x_3+w_{41}x_4+b_1\\ h_2=w_{12}x_1+w_{22}x_2+w_{32}x_3+w_{42}x_4+b_2\]

다음은 은닉층에서 출력층의 계산을 수학식으로 작성한 것이다.

\[y_1=w_{51}h_1+w_{61}h_2+b_3\\ y_2=w_{52}h_1+w_{62}h_2+b_4\\ y_3=w_{53}h_1+w_{63}h_2+b_5 \]

이제 준비가 완료되었다. 위의 수학식들을 코드로 구현해보자.

1
2
3
4
5
Julia> X = [1 2 3 4]
Julia> W1 = [1 2; 3 4;5 6;7 8]
Julia> W2 = [1 2 3; 4 5 6]
Julia> B1 = [1 2]
Julia> B2 = [1 2 3]

먼저 입력층 행렬과 가중치 행렬, 편향을 할당해준다. 그 후 위의 수학공식을 구현하여 은닉층을 구현해보자.

1
2
Julia> H = *(X,W1)+B1
51 62

은닉층은 입력층의 값에 가중치 W1을 곱한 후, 편향 B1을 더하였다. 마지막으로 출력층을 구현하면 아래와 같다.

1
2
Julia> Y = *(H,W2)+B2
300 414 528

사실 신경망을 구현하는 것은 위의 설명보다 더 복잡하다. 하지만 위의 원리를 이해하지 못한다면 신경망 자체를 파악할 수 없다. 다음 글에서는 신경망에 쓰이는 활성화 함수들을 살펴보고 줄리아로 구현할 것이다.


[1/18] 행렬과 벡터의 곱
https://dev-bearabbit.github.io/ko/DeeplearningJulia/Deeplearning-1/
Author
Jess
Posted on
2020년 3월 26일
Licensed under