Dive into Deep Learning
Dive into Deep Learning

2.4. 선형 대수

자, 이제 데이터를 저장하고 조작하는 방법을 배웠으니, 모델에 대한 대부분을 이해하는데 필요한 기초적인 선형 대수 일부를 간단하게 살펴보겠습니다. 기초적인 개념과 관련된 수학 표기법, 그리고 코드로의 구현까지 모두 소개할 것입니다. 기본 선형 대수에 익숙하다면, 이 절은 빨리 읽거나 다음 절로 넘어가도 됩니다.

[1]:
from mxnet import nd

2.4.1. 스칼라(scalar)

선형대수나 머신 러닝을 배워본 적이 없다면, 아마도 한번에 하나의 숫자를 다루는데 익숙할 것입니다. 예를 들어, 팔로 알토의 기온이 화씨 52도입니다. 공식 용어를 사용하면 이 값은 스칼라(scalar) 입니다. 이 값을 섭씨로 바꾸기를 원한다면, ​\(c = (f - 32) * 5/9\) 공식에 ​\(f\) 값으로 ​\(52\) 대입하면 됩니다. 이 공식에서 각 항들 ​\(32\), ​\(5\), ​\(9\) 은 스칼라 값입니다. 플래이스 홀더 ​\(c\) 와 ​\(f\) 를 변수라고 부르고, 아직 정해지지 않은 스칼라 값들을 위해 있습니다.

수학적인 표기법으로는 우리는 스칼라를 소문자(\(x​\), \(y​\), \(z​\))로 표기 합니다. 또한 모든 스칼라에 대한 공간은 \(\mathcal{R}​\)로 적습니다. 이후에 공간이 정확히 무엇인지를 알아보겠지만, 편의상 지금은 \(x​\) 가 스칼라라고 이야기하는 것은 \(x \in \mathcal{R}​\) 로 표현하기로 하겠습니다.

MXNet에서 스칼라는 하나의 원소를 갖는 NDArray로 표현됩니다. 아래 코드에서는 두 개의 스칼라를 생성하고, 친숙한 수치 연산 - 더하기, 빼기, 나누기, 그리고 제곱을 수행합니다.

[2]:
x = nd.array([3.0])
y = nd.array([2.0])

print('x + y = ', x + y)
print('x * y = ', x * y)
print('x / y = ', x / y)
print('x ** y = ', nd.power(x,y))
x + y =
[5.]
<NDArray 1 @cpu(0)>
x * y =
[6.]
<NDArray 1 @cpu(0)>
x / y =
[1.5]
<NDArray 1 @cpu(0)>
x ** y =
[9.]
<NDArray 1 @cpu(0)>

asscalar 메소드를 이용하면 NDArray를 Python의 float 형으로 변환할 수 있습니다. 하지만 이렇게 하는 것은 좋은 아이디어가 아님을 알아두세요. 그 이유는 이를 수행하는 동안, 프로세스 제어를 Python에게 줘야하기 때문에 NDArray는 결과를 내기 위한 다른 것들을 모두 멈춰야 합니다. 아쉽게도, Python은 병렬로 일을 처리하는데 좋지 못합니다. 이런 연산을 코드나 네트워크에서 수행한다면 학습하는데 오랜 시간이 걸릴 것입니다.

[3]:
x.asscalar()
[3]:
3.0

2.4.2. 벡터(vector)

벡터를 [1.0,3.0,4.0,2.0] 처럼 숫자들의 리스트로 생각할 수 있습니다. 벡터의 각 숫자는 하나의 스칼라 변수로 이루어져 있습니다. 이 숫자들을 우리는 벡터의 원소 또는 구성 요소 라고 부릅니다. 종종 우리는 실제 세상에서 중요한 값을 담은 벡터들에 관심을 갖습니다. 예를 들어 채무 불이행 위험을 연구하고 있다면, 모든 지원자를 원소가 수입, 재직 기간, 이전의 불이행 횟수 등을 포함한 벡터와 연관을 지을지도 모릅니다. 병원 내 환자들의 심장 마비 위험을 연구하는 사람은, 환자들을 최근 바이탈 사인, 콜레스테롤 지수, 하루 운동 시간 등을 원소로 갖는 벡터로 표현 할 것입니다. 수학적인 표기 법을 이용할 때 벡터는 굵은 글씨체로 소문자 (\(\mathbf{u}\), \(\mathbf{v}\), \(\mathbf{w})\) 를 사용해서 표현합니다. MXNet에서는 임의의 숫자를 원소로 갖는 1D NDArray를 이용해서 벡터를 다루게 됩니다.

[4]:
x = nd.arange(4)
print('x = ', x)
x =
[0. 1. 2. 3.]
<NDArray 4 @cpu(0)>

첨자를 사용해서 벡터의 요소를 가리킬 수 있습니다. 즉, \(\mathbf{u}​\) 의 4번째 요소는 \(u_4​\) 로 표현합니다. \(u_4​\) 는 스칼라이기 때문에 굵은 글씨가 아닌 폰트로 표기하는 것을 유의하세요. 코드에는 NDArray\(i​\) 번째 인덱스로 이를 지정합니다.

[5]:
x[3]
[5]:

[3.]
<NDArray 1 @cpu(0)>

2.4.3. 길이, 차원(dimensionality), 모양(shape)

앞 절에서 소개한 개념 몇 개를 다시 살펴보겠습니다. 벡터는 숫자들의 배열입니다. 모든 배열은 길이를 갖듯이, 벡터도 길이를 갖습니다. 벡터 \(\mathbf{x}\)\(n\) 개의 실수 값을 갖는 스칼라들로 구성되어 있다면, 이는 수학적인 표현으로 \(\mathbf{x} \in \mathcal{R}^n\) 로 적습니다. 벡터의 길이는 일반적으로 차원(\(dimension\)) 이라고 합니다. Python 배열처럼, NDArray의 길이도 Python의 내장 함수 len() 를 통해서 얻을 수 있습니다.

벡터의 길이는 .shape 속성으로도 얻을 수 있습니다. shape은 NDArray 객체의 각 축에 대한 차원의 목록으로 표현됩니다. 벡터는 축이 하나이기 때문에, 벡터의 모양(shape)은 하나의 숫자로 표현됩니다.

[6]:
x.shape
[6]:
(4,)

차원(dimension)이라는 단어가 여러가지 의미로 사용되기 때문에, 헷갈릴 수도 있습니다. 어떤 경우에는 벡터의 차원(dimensionality) 을 벡터의 길이 (원소들의 수)로 사용하기도 하고, 어떤 경우에는 배열의 축의 개수로 사용되기도 합니다. 후자의 경우에는 스칼라는 0 차원을 갖고, 벡터는 1차원을 갖습니다.

혼동을 줄이기 위해서, 우리는 2D 배열 또는 3D 배열이라고 말할 때, 축이 각각 2개 3개인 배열을 의미하도록 합니다. 하지만 만약 n-차원 벡터라고 하는 경우에는, 길이가 n 인 벡터를 의미합니다.

[7]:
a = 2
x = nd.array([1,2,3])
y = nd.array([10,20,30])
print(a * x)
print(a * x + y)

[2. 4. 6.]
<NDArray 3 @cpu(0)>

[12. 24. 36.]
<NDArray 3 @cpu(0)>

2.4.4. 행렬(matrix)

벡터가 오더 0인 스칼라를 오더 1로 일반화하는 것처럼, 행렬은 1\(D\)에서 2\(D\) 로 벡터를 일반화합니다. 일반적으로 대문자 (\(A\), \(B\), \(C\))로 표현하는 행렬은 코드에서는 축이 2개인 배열로 표현합니다. 시각화한다면, 행렬은 원소 \(a_{ij}\)\(i\)-열, \(j\)-행에 속하는 표로 그릴 수 있습니다.

\[\begin{split}A=\begin{pmatrix} a_{11} & a_{12} & \cdots & a_{1m} \\ a_{21} & a_{22} & \cdots & a_{2m} \\ \vdots & \vdots & \ddots & \vdots \\ a_{n1} & a_{n2} & \cdots & a_{nm} \\ \end{pmatrix}​\end{split}\]

MXNet에서는 \(n\) 행, \(m\) 열을 갖는 행렬을 만드는 방법은 두 요소를 갖는 (n,m) 모양(shape)을 이용해서 ones 또는 zeros 함수를 호출을 통해 ndarray 를 얻는 것입니다.

[8]:
A = nd.arange(20).reshape((5,4))
print(A)

[[ 0.  1.  2.  3.]
 [ 4.  5.  6.  7.]
 [ 8.  9. 10. 11.]
 [12. 13. 14. 15.]
 [16. 17. 18. 19.]]
<NDArray 5x4 @cpu(0)>

행렬은 유용한 자료 구조입니다. 행렬을 이용해서 서로 다른 양식의 변형을 갖는 데이터를 구성할 수 있습니다. 예를 들어보면, 행렬의 행들은 서로 다른 환자에 대한 정보를, 열은 서로 다른 속성에 대한 정보를 의미할 수 있습니다.

행렬 \(A\) 의 스칼라 원소 \(a_{ij}\) 을 지정하는 방법은 행(\(i\))과 열(\(j\))에 대한 인덱스를 지정하면 됩니다. : 를 사용해서 공백으로 두면, 해당 차원의 모든 원소를 의미합니다. (앞 절에서 봤던 방법입니다.)

행렬을 전치하는 방법은 T 를 이용합니다. 전치 행렬은 만약 \(B = A^T​\) 이면, 모든 \(i​\)\(j​\) 에 대해서 \(b_{ij} = a_{ji}​\) 인 행렬을 의미합니다.

[9]:
print(A.T)

[[ 0.  4.  8. 12. 16.]
 [ 1.  5.  9. 13. 17.]
 [ 2.  6. 10. 14. 18.]
 [ 3.  7. 11. 15. 19.]]
<NDArray 4x5 @cpu(0)>

2.4.5. 텐서(tensor)

벡터가 스칼라를 일반화하고, 행렬이 벡터를 일반화하는 것처럼 더 많은 축을 갖는 자료 구조를 만들 수 있습니다. 텐서(tensor)는 임의의 개수의 축을 갖는 행렬을 표현하는 일반적인 방법을 제공합니다. 예를 들어 벡터는 1차 오더(order) 텐서이고, 행렬은 2차 오더(order) 텐서입니다.

3D 자료 구조를 갖는 이미지를 다룰 때 텐서를 사용하는 것은 아주 중요하게 됩니다. 즉, 각 축이 높이, 넓이, 그리고 세가지 색(RGB) 채널을 의미합니다. 지금은 이것에 대한 내용은 생략하고, 기본적인 것을 확실하게 아는 것을 목표로 하겠습니다.

[10]:
X = nd.arange(24).reshape((2, 3, 4))
print('X.shape =', X.shape)
print('X =', X)
X.shape = (2, 3, 4)
X =
[[[ 0.  1.  2.  3.]
  [ 4.  5.  6.  7.]
  [ 8.  9. 10. 11.]]

 [[12. 13. 14. 15.]
  [16. 17. 18. 19.]
  [20. 21. 22. 23.]]]
<NDArray 2x3x4 @cpu(0)>

2.4.6. 텐서 연산의 기본 성질

스칼라, 벡터, 행렬, 그리고 어떤 오더를 갖는 텐서들은 우리가 자주 사용할 유용한 특성들을 가지고 있습니다. 요소별 연산(element-wise operation)의 정의에서 알 수 있듯이, 같은 모양(shape)들에 대해서 연산을 수행하면, 요소별 연산의 결과는 같은 모양(shape)을 갖는 텐서입니다. 또다른 유용한 특성은 모든 텐서에 대해서 스칼라를 곱하면 결과는 같은 모양(shape)의 텐서입니다. 수학적으로 표현하면, 같은 모양(shape)의 두 텐서 \(X\)\(Y\)가 있다면 \(\alpha X + Y\) 는 같은 모양(shape)을 갖습니다.

[11]:
a = 2
x = nd.ones(3)
y = nd.zeros(3)
print(x.shape)
print(y.shape)
print((a * x).shape)
print((a * x + y).shape)
(3,)
(3,)
(3,)
(3,)

더하기와 스칼라 곱으로 보존되는 특성이 모양(shape) 뿐만은 아닙니다. 이 연산들은 벡터 공간의 맴버쉽을 보존해줍니다. 하지만, 여러분의 첫번째 모델을 만들어서 수행하는데 중요하지 않기 때문에, 이 장의 뒤에서 설명하겠습니다.

2.4.7. 합과 평균

임의의 텐서들로 수행할 수 있는 조금 더 복잡한 것은 각 요소의 합을 구하는 것입니다. 수학기호로는 합을 \(\sum​\) 로 표시합니다. 길이가 \(d​\) 인 벡터 \(\mathbf{u}​\) 의 요소들의 합은 \(\sum_{i=1}^d u_i​\) 로 표현하고, 코드에서는 nd.sum() 만 호출하면 됩니다.

[12]:
print(x)
print(nd.sum(x))

[1. 1. 1.]
<NDArray 3 @cpu(0)>

[3.]
<NDArray 1 @cpu(0)>

임의의 모양(shape)을 갖는 텐서의 원소들의 합을 비슷하게 표현할 수 있습니다. 예를 들어 \(m \times n\) 행렬 \(A\) 의 원소들의 합은 \(\sum_{i=1}^{m} \sum_{j=1}^{n} a_{ij}\) 이고, 코드로는 다음과 같습니다.

[13]:
print(A)
print(nd.sum(A))

[[ 0.  1.  2.  3.]
 [ 4.  5.  6.  7.]
 [ 8.  9. 10. 11.]
 [12. 13. 14. 15.]
 [16. 17. 18. 19.]]
<NDArray 5x4 @cpu(0)>

[190.]
<NDArray 1 @cpu(0)>

합과 관련된 것으로 평균(mean) 이 있습니다. (average라고도 합니다.) 평균은 원소들의 합을 원소들의 개수로 나눠서 구합니다. 어떤 벡터 \(\mathbf{u}\) 의 평균을 수학 기호로 표현하면 \(\frac{1}{d} \sum_{i=1}^{d} u_i\) 이고, 행렬 \(A\) 에 대한 평균은 s \(\frac{1}{n \cdot m} \sum_{i=1}^{m} \sum_{j=1}^{n} a_{ij}\) 이 됩니다. 코드로 구현하면, 임의의 shape을 갖는 텐서의 평균은 nd.mean() 을 호출해서 구합니다.

[14]:
print(nd.mean(A))
print(nd.sum(A) / A.size)

[9.5]
<NDArray 1 @cpu(0)>

[9.5]
<NDArray 1 @cpu(0)>

2.4.8. 점곱(dot product)

지금까지는 원소들 사이에 연산인 더하기와 평균에 대해서 살펴봤습니다. 이 연산들이 우리가 할 수 있는 전부라면, 선형대수를 별도의 절로 만들어서 설명할 필요가 없을 것입니다. 즉, 가장 기본적인 연산들 중에 하나로 점곱(dot product)이 있습니다. 두 벡터, \(\mathbf{u}​\)\(\mathbf{v}​\), 가 주어졌을 때, 점곱, \(\mathbf{u}^T \mathbf{v}​\) ,은 요소들끼리 곱을 한 결과에 대한 합이 됩니다. 즉, \(\mathbf{u}^T \mathbf{v} = \sum_{i=1}^{d} u_i \cdot v_i​\).

[15]:
x = nd.arange(4)
y = nd.ones(4)
print(x, y, nd.dot(x, y))

[0. 1. 2. 3.]
<NDArray 4 @cpu(0)>
[1. 1. 1. 1.]
<NDArray 4 @cpu(0)>
[6.]
<NDArray 1 @cpu(0)>

두 벡터의 점곱 nd.dot(x, y) , 은 원소들끼리의 곱을 수행한 후, 합을 구하는 것과 동일합니다.

[16]:
nd.sum(x * y)
[16]:

[6.]
<NDArray 1 @cpu(0)>

점곱은 다양한 경우에 유용하게 사용됩니다. 예를 들어, 가중치들의 집합 \(\mathbf{w}\) 에 대해서, 어떤 값 \(u\) 의 가중치를 적용한 합은 점곱인 \(\mathbf{u}^T \mathbf{w}\)으로 계산될 수 있습니다. 가중치들이 0 또는 양수이고, 합이 1 \(\left(\sum_{i=1}^{d} {w_i} = 1\right)\) 인 경우, 행렬의 곱은 가중치 평균(weighted average) 을 나타냅니다. 길이가 1인 두 벡터 (길이가 무엇인지는 아래에서 norm을 설명할 때 다룹니다)가 있을 때, 점곱을 통해서 두 벡터 사이의 코사인 각을 구할 수 있습니다.

2.4.9. 행렬-벡터 곱

점곱을 어떻게 계산하는지 알아봤으니, 행렬-벡터 곱을 알아볼 준비가 되었습니다. 우선 행렬 \(A\) 와 열벡터 \(\mathbf{x}\) 를 시각적으로 표현하는 것으로 시작합니다.

\[\begin{split}A=\begin{pmatrix} a_{11} & a_{12} & \cdots & a_{1m} \\ a_{21} & a_{22} & \cdots & a_{2m} \\ \vdots & \vdots & \ddots & \vdots \\ a_{n1} & a_{n2} & \cdots & a_{nm} \\ \end{pmatrix},\quad\mathbf{x}=\begin{pmatrix} x_{1} \\ x_{2} \\ \vdots\\ x_{m}\\ \end{pmatrix} ​\end{split}\]

행렬을 다시 행벡터 형태로 표현이 가능합니다.

\[\begin{split}A= \begin{pmatrix} \mathbf{a}^T_{1} \\ \mathbf{a}^T_{2} \\ \vdots \\ \mathbf{a}^T_n \\ \end{pmatrix},​\end{split}\]

여기서 각 \(\mathbf{a}^T_{i} \in \mathbb{R}^{m}\) 는 행렬의 \(i\) 번째 행을 표시하는 행벡터입니다.

그러면 행렬-벡터 곱 \(\mathbf{y} = A\mathbf{x}​\) 은 컬럼 벡터 \(\mathbf{y} \in \mathbb{R}^n​\) 이며, 각 원소 \(y_i​\) 는 점곱 \(\mathbf{a}^T_i \mathbf{x}​\) 입니다.

\[\begin{split}A\mathbf{x}= \begin{pmatrix} \mathbf{a}^T_{1} \\ \mathbf{a}^T_{2} \\ \vdots \\ \mathbf{a}^T_n \\ \end{pmatrix} \begin{pmatrix} x_{1} \\ x_{2} \\ \vdots\\ x_{m}\\ \end{pmatrix} = \begin{pmatrix} \mathbf{a}^T_{1} \mathbf{x} \\ \mathbf{a}^T_{2} \mathbf{x} \\ \vdots\\ \mathbf{a}^T_{n} \mathbf{x}\\ \end{pmatrix}\end{split}\]

즉, 행렬 \(A\in \mathbb{R}^{n \times m}​\) 로 곱하는 것을 벡터를 \(\mathbb{R}^{m}​\) 에서 \(\mathbb{R}^{n}​\)로 사영시키는 변환으로도 생각할 수 있습니다.

이런 변환은 아주 유용하게 쓰입니다. 예를 들면, 회전을 정사각 행렬의 곱으로 표현할 수 있습니다. 다음 절에서 보겠지만, 행렬-벡터 곱을 뉴럴 네트워크의 각 층의 연산을 표현하는데도 사용합니다.

ndarray' 를 이용해서 행렬-벡터의 곱을 계산할 때는 점곱에서 사용했던 nd.dot() 함수를 동일하게 사용합니다. 행렬 A 와 벡터 x 를 이용해서 nd.dot(A,x) 를 호출하면, MXNet은 행렬-벡터 곱을 수행해야한다는 것을 압니다. A 의 열의 개수와 x 의 차원이 같아야 한다는 점을 유의하세요.

[17]:
nd.dot(A, x)
[17]:

[ 14.  38.  62.  86. 110.]
<NDArray 5 @cpu(0)>

2.4.10. 행렬-행렬 곱

점곱과 행렬-벡터 곱을 잘 이해했다면, 행렬-행렬 곱은 아주 간단할 것입니다.

행렬 \(A \in \mathbb{R}^{n \times k}​\), \(B \in \mathbb{R}^{k \times m}​\) 가 있다고 하겠습니다.

\[\begin{split}A=\begin{pmatrix} a_{11} & a_{12} & \cdots & a_{1k} \\ a_{21} & a_{22} & \cdots & a_{2k} \\ \vdots & \vdots & \ddots & \vdots \\ a_{n1} & a_{n2} & \cdots & a_{nk} \\ \end{pmatrix},\quad B=\begin{pmatrix} b_{11} & b_{12} & \cdots & b_{1m} \\ b_{21} & b_{22} & \cdots & b_{2m} \\ \vdots & \vdots & \ddots & \vdots \\ b_{k1} & b_{k2} & \cdots & b_{km} \\ \end{pmatrix}​\end{split}\]

행렬의 곱 \(C = AB\) 를 계산하기 위해서, \(A\) 를 행벡터들로, \(B\) 를 열벡터들로 생각하면 쉽습니다.

\[\begin{split}A= \begin{pmatrix} \mathbf{a}^T_{1} \\ \mathbf{a}^T_{2} \\ \vdots \\ \mathbf{a}^T_n \\ \end{pmatrix}, \quad B=\begin{pmatrix} \mathbf{b}_{1} & \mathbf{b}_{2} & \cdots & \mathbf{b}_{m} \\ \end{pmatrix}.\end{split}\]

각 행벡터 \(\mathbf{a}^T_{i}\)\(\mathbb{R}^k\) 에 속하고, 각 열벡터 \(\mathbf{b}_j\)\(\mathbb{R}^k\) 에 속한다는 것을 주의하세요.

그러면, 행렬 \(C \in \mathbb{R}^{n \times m}\) 의 각 원소 \(c_{ij}\)\(\mathbf{a}^T_i \mathbf{b}_j\) 로 구해집니다.

\[\begin{split}C = AB = \begin{pmatrix} \mathbf{a}^T_{1} \\ \mathbf{a}^T_{2} \\ \vdots \\ \mathbf{a}^T_n \\ \end{pmatrix} \begin{pmatrix} \mathbf{b}_{1} & \mathbf{b}_{2} & \cdots & \mathbf{b}_{m} \\ \end{pmatrix} = \begin{pmatrix} \mathbf{a}^T_{1} \mathbf{b}_1 & \mathbf{a}^T_{1}\mathbf{b}_2& \cdots & \mathbf{a}^T_{1} \mathbf{b}_m \\ \mathbf{a}^T_{2}\mathbf{b}_1 & \mathbf{a}^T_{2} \mathbf{b}_2 & \cdots & \mathbf{a}^T_{2} \mathbf{b}_m \\ \vdots & \vdots & \ddots &\vdots\\ \mathbf{a}^T_{n} \mathbf{b}_1 & \mathbf{a}^T_{n}\mathbf{b}_2& \cdots& \mathbf{a}^T_{n} \mathbf{b}_m \end{pmatrix}\end{split}\]

행렬-행렬 곱 \(AB\) 을 단순히 \(m\) 개의 행렬-벡터의 곱을 수행한 후, 결과를 붙여서 \(n \times m\) 행렬로 만드는 것으로 생각할 수도 있습니다. 일반적인 점곱과 행렬-벡터 곱을 계산하는 것처럼 MXNet에서 행렬-행렬의 곱은 nd.dot() 으로 계산됩니다.

[18]:
B = nd.ones(shape=(4, 3))
nd.dot(A, B)
[18]:

[[ 6.  6.  6.]
 [22. 22. 22.]
 [38. 38. 38.]
 [54. 54. 54.]
 [70. 70. 70.]]
<NDArray 5x3 @cpu(0)>

2.4.11. 놈(norm)

모델을 구현하기 전에 배워야할 개념이 하나 더 있습니다. 선형대수에서 가장 유용한 연산 중에 놈(norm) 이 있습니다. 엄밀하지 않게 설명하면, 놈은 벡터나 행렬이 얼마나 큰지를 알려주는 개념입니다. \(\|\cdot\|\) 으로 놈을 표현하는데, \(\cdot\) 은 행렬이나 벡터가 들어갈 자리입니다. 예를 들면, 벡터 \(\mathbf{x}\) 나 행렬 \(A\) 를 각각 \(\|\mathbf{x}\|\) or \(\|A\|\) 로 적습니다.

모든 놈은 다음 특성을 만족시켜야 합니다.

  1. \(\|\alpha A\| = |\alpha| \|A\|\)
  2. \(\|A + B\| \leq \|A\| + \|B\|\)
  3. \(\|A\| \geq 0\)
  4. If \(\forall {i,j}, a_{ij} = 0\), then \(\|A\|=0\)

위 규칙을 말로 설명하면, 첫번째 규칙은 행렬이나 벡터의 모든 원소에 상수 \(\alpha\) 만큼 스캐일을 바꾸면, 놈도 그 상수의 절대값 만큼 스캐일이 바뀐다는 것입니다. 두번째 규칙은 친숙한 삼각부등식입니다. 세번째는 놈은 음수가 될 수 없다는 것입니다. 거의 모든 경우에 가장 작은 크기가 0이기에 이 규칙은 당연합니다. 마지막 규칙은 가장 작은 놈은 행렬 또는 벡터가 0으로 구성되었을 경우라는 기본적인 것에 대한 것입니다. 0이 아닌 행렬에 놈이 0이 되도록 놈을 정의하는 것이 가능합니다. 하지만, 0인 행렬에 0이 아닌 놈이 되게 하는 놈을 정의하는 것은 불가능합니다. 길게 설명했지만, 이것을 이해했다면 중요한 개념을 얻었을 것입니다.

수학시간에 배운 유클리디안 거리(Euclidean distance)를 기억한다면, 0이 아닌 것과 삼각부등식이 떠오를 것입니다. 놈이 거리를 측정하는 것과 비슷하다는 것을 인지했을 것입니다.

사실 유클리디안 거리 \(\sqrt{x_1^2 + \cdots + x_n^2}\) 는 놈입니다. 특히, 이를 \(\ell_2\)-놈이라고 합니다. 행렬의 각 원소에 대해서 유사하게 계산한 것 \(\sqrt{\sum_{i,j} a_{ij}^2}\) 을 푸로베니우스 놈(Frobenius norm)이라고 합니다. 머신 러닝에서는 자주 제곱 \(\ell_2\) 놈을 사용합니다. (\(\ell_2^2\) 로 표현합니다.) \(\ell_1\) 놈도 흔히 사용합니다. \(\ell_1\) 놈은 절대값들의 합으로, 이상치(outlier)에 덜 중점을 주는 편리한 특성이 있습니다.

\(\ell_2​\) 놈의 계산은 nd.norm() 으로 합니다.

[19]:
nd.norm(x)
[19]:

[3.7416573]
<NDArray 1 @cpu(0)>

\(\ell_1​\) 놈을 계산하는 방법은 각 원소의 절대값을 구한 후, 모두 합하는 것입니다.

[20]:
nd.sum(nd.abs(x))
[20]:

[6.]
<NDArray 1 @cpu(0)>

2.4.12. 놈(norm)와 목적(objective)

더 깊이 나가지는 않겠지만, 이 개념들이 왜 중요한지 궁금할 것입니다. 머신 러닝에서 우리는 종종 최적화 문제를 풀기를 시도합니다 - 즉, 관찰된 데이터에 할당된 확률을 최대화하기, 예측된 값과 실제 값의 차이를 최소화하기, 단어, 제품, 새로운 기사와 같은 아이템들에 가까운 아이템들의 거리가 최소화되는 벡터를 할당하기 등을 시도합니다. 아마도 머신 러닝 알고리즘의 (데이터를 제외한) 가장 중요한 요소인 이 목적(objective)들은 자주 놈(norm)으로 표현됩니다.

2.4.13. 중급 선형 대수

여러분이 여기까지 잘 따라오면서 모든 내용을 이해했다면, 솔직하게 여러분은 모델을 시작할 준비가 되었습니다. 먄약 조급함을 느낀다면, 이 절의 나머지는 넘어가도 됩니다. 실제로 적용할 수 있는 유용한 모델들을 구현하는데 필요한 모든 선형대수에 대해서 알아봤고, 더 알고 싶으면 다시 돌아올 수 있습니다.

하지만, 머신 러닝만 고려해봐도 선형대수에 대한 더 많은 내용이 있습니다. 이후 어느 시점에 여러분이 머신 러닝 경력을 만들기를 원한다면, 여기서 다룬 것보다 더 많은 것을 알아야할 것입니다. 유용하고 더 어려운 개념을 소개하면서 이 절을 마치겠습니다.

2.4.14. 벡터의 기본 성질들

벡터는 숫자를 담는 자료 구조보다 더 유용합니다. 벡터의 원소에 숫자를 읽고 적는 것, 유용한 수학 연산을 수행하는 것과 더불어, 벡터를 재미있는 방법으로 분석할 수 있습니다.

벡터 공간의 개념은 중요한 개념입니다. 벡터 공간이 되기에 필요한 조건은 다음과 같습니다.

  • 더하기 공리(Additive axioms) (x,y,z가 모두 벡터라고 가정합니다.): \(x+y = y+x​\) , \((x+y)+z = x+(y+z)​\) , \(0+x = x+0 = x​\) 그리고 \((-x) + x = x + (-x) = 0​\).
  • 곱하기 공리(Multiplicative axioms) (x는 벡터이고 a, b는 스칼라입니다.): \(0 \cdot x = 0​\) , \(1 \cdot x = x​\) , \((a b) x = a (b x)​\).
  • 분배 공리(Distributive axioms) (x와 y는 벡터, a, b는 스칼라로 가정합니다.): \(a(x+y) = ax + ay​\) and \((a+b)x = ax +bx​\).

2.4.15. 특별한 행렬들

이 책에서 사용할 특별한 행렬들이 있습니다. 그 행렬들에 대해서 조금 자세히 보겠습니다.

  • 대칭 행렬(Symmetric Matrix) 이 행렬들은 대각선 아래, 위의 원소들이 같은 값을 갖습니다. 즉, \(M^\top = M\) 입니다. 이런 예로는 짝들의(pairwise) 거리를 표현하는 행렬 \(M_{ij} = \|x_i - x_j\|\)이 있습니다. 페이스북 친구 관계를 대칭 행렬로 표현할 수 있습니다. \(i\)\(j\) 가 친구라면 \(M_{ij} = 1\) 이 되고, 친구가 아니라면 \(M_{ij} = 0\) 로 표현하면 됩니다. 하지만, 트위터 그래프는 대칭이 아님을 주목해세요. \(M_{ij} = 1\), 즉 \(i\)\(j\) 를 팔로우하는 것이 꼭 \(j\)\(i\) 를 팔로우하는 것, \(M_{ji} = 1\), 은 아니기 때문입니다.
  • 비대칭 행렬(Antisymmetric Matrix) \(M^\top = -M​\) 를 만족하는 행렬입니다. 임의의 행렬은 대칭 행렬과 비대칭 행렬로 분해될 수 있습니다. 즉, \(M = \frac{1}{2}(M + M^\top) + \frac{1}{2}(M - M^\top)​\) 로 표현될 수 있습니다.
  • 대각 지배 행렬(Diagonally Dominant Matrix) 대각 원소들 보다 대각이 아닌 원소들이 작은 행렬입니다. 즉, \(M_{ii} \geq \sum_{j \neq i} M_{ij}\) 이고 \(M_{ii} \geq \sum_{j \neq i} M_{ji}\) 입니다. 어떤 행렬이 이 특성을 갖는다면, 대각원소를 사용해서 \(M\) 을 추정할 수 있고, 이를 \(\mathrm{diag}(M)\) 로 표기합니다.
  • 양의 정부호 행렬(Positive Definite Matrix) 이 행렬은 \(x \neq 0\) 이면, \(x^\top M x > 0\) 인 좋은 특성을 갖습니다. 직관적으로 설명하면, 벡터의 제곱 놈, \(\|x\|^2 = x^\top x\), 의 일반화입니다. \(M = A^\top A\) 이면 이 조건이 만족시킨다는 것을 쉽게 확인할 수 있습니다. 이유는 \(x^\top M x = x^\top A^\top A x = \|A x\|^2\) 이기 때문입니다. 모든 양의 정부호 행렬은 이런 형태로 표현될 수 있다는 더 심오한 이론이 있습니다.

2.4.16. 요약

몇 페이지들(또는 Jupyter 노트북 한개)을 통해서 뉴럴 네트워크의 중요한 부분들을 이해하는데 필요한 모든 선형대수에 대해서 알아봤습니다. 물론 선형대수에는 더 많은 내용이 있고, 이것들은 머신 러닝에 유용하게 쓰입니다. 예를 들어, 행렬을 분해할 수 있는데, 이 분해는 실세계의 데이터셋의 아래 차원의 구조를 알려주기도 합니다. 행렬 분해를 이용하는데 집중하는 머신 러닝의 별도의 분야가 있습니다. 이를 이용해서 데이터의 구조를 밝히고 예측 문제를 풀기 위해서 고차원의 텐서를 일반화하기도 합니다. 하지만 이 책에서는 딥러닝에 집중합니다. 여러분이 실제 데이터를 사용해서 유용한 머신 러닝 모델을 만들기 시작한다면, 수학에 대해서 더 관심을 갖게될 것이라고 믿습니다. 하지만 수학적인 내용은 나중에 더 설명하기로 하고, 이 절은 여기서 마무리하겠습니다.

선형 대수에 대해서 더 배우기를 원한다면, 유용한 교재들이 있습니다.

2.4.17. Scan the QR Code to Discuss

image0