Python 선형대수 입문


  • 본문의 내용은 Data Science from scratch(밑바닥부터 시작하는 데이터 과학)을 읽고 작성했습니다.
  • 본문의 소스코드의 일부는 Joel Grus의 Github에서 Unlicensed 라이선스로 배포되고 있습니다.

Welcome to linear algebra

Linear algebra is the study of vectors and linear functions
선형대수는 벡터와 선형함수에 대한 학문입니다.


벡터는 벡터끼리 더하거나 상수와 곱해지면 새로운 벡터를 생성하는 도구. 더 자세히 설명하면, 벡터는 어떤 유한한 차원의 공간에 존재하는 점들이다.

대부분의 데이터, 숫자로 표현된 데이터는 벡터로 표현할 수 있다.

예를 들어 수많은 사람들의 키, 몸무게, 나이에 대한 데이터가 주어졌다고 해보자. 그렇다면 주어진 데이터를 키, 몸무게, 나이로 구성된 3차원 벡터로 표현할 수 있을 것이다.

또 다른 예로, 시험을 네 번 보는 수업을 가르친다면 각 학생의 성적을 ‘시험1 점수 시험2 점수 시험3 점수 시험4 점수’ 로 구성된 4차원 벡터로 표현할 수 있을 것이다.

벡터를 python에서 가장 간단하게 표현하는 방법은 숫자로 구성된 list로 표현하는 것이다.

예를 들어, 3차원 벡터는 세 개의 숫자로 구성된 list로 표현할 수 있다.

키_몸무게_나이 = [174, 60, 17]
성적 = [95, 85, 74, 92]

list로 벡터를 표현하는 방법의 문제점은 list를 통해 벡터 연산을 할 수 없다는 점, 성능이 나쁘다는 점 등이 있다. (하지만 이번엔 그 점은 무시한다.)

python의 list를 이용한 벡터 연산을 위한 도구를 직접 만들어보자.

벡터 연산

두 개의 벡터를 더한다는 것은, 각 벡터 상에서 같은 위치에 있는 성분끼리 더한다는 것이다. 가령 길이가 같은 v와 w라는 두 벡터를 더하면 계산된 새로운 벡터의 첫 번째 성분은 v[0] + w[0] 두 번째 성분은 v[1] + w[1] 등등으로 구성된다. (만약 두 벡터의 길이가 다르면 두 벡터를 더할 수 없다. - 버려진다.)

벡터 덧셈은 zip을 사용해서 두 벡터를 묶은 뒤, 두 배열의 각 성분끼리 더하는 list comprehension을 적용하면 된다.

from functools import reduce
from operator import mul

"""각 성분끼리 더한다."""
def vector_add(v, w):
    return [v_i + w_i for v_i, w_i in zip(v, w)]

"""각 성분끼리 뺀다."""
def vector_substract(v, w):
    return [v_i - w_i for v_i, w_i in zip(v, w)]

"""각 성분들끼리 더한다"""
def vector_sum(vectors):
    return reduce(vector_add, vectors)

print(키_몸무게_나이, 성적)
print(list(zip(키_몸무게_나이, 성적)))
print(vector_add(키_몸무게_나이, 성적))
print(vector_substract(키_몸무게_나이, 성적))
>>
[174, 60, 17] [95, 85, 74, 92]
[(174, 95), (60, 85), (17, 74)]
[269, 145, 91]
[79, -25, -57]

What is scalar?

집합 = 같은 성질을 가진 대상의 모임

grade = [50,12,78,20,85,94]

체(Field) = 사칙연산을 자유롭게 할 수 있는 수들의 집합

체 F와 집합 V에 대하여 V의 두 원소를 더하는 덧셈, V의 원소에 F를 곱하는 연산이 잘될 때 V를 F위에서 정의된 벡터 공간이라고 한다. V의 원소는 벡터이며 F의 원소는 스칼라이다.

혹은..

  • 크기를 나타내는 값
  • 우리가 계산할 때 사용하는 숫자들이 스칼라라고 할 수 있다.

벡터의 내적은 Scalar product (스칼라곱) 또는 Dot Product 라고도 하며, 기호로 · (Dot)을 쓴다.

벡터의 내적을 구하는 방법은 두가지가 있는데,
첫번째가 좌표값의 각 성분을 곱해 더하는 것이다.

A=[4,3]
B=[6,0]
A·B (A와 B의 내적)
= (AxBx)+(AyBy)
= (4x6)+(3*0) = 24

# 스칼라곱
def scalar_multiply(c, v):
    """c는 숫자(스칼라), v는 벡터"""
    return [c * v_i for v_i in v]

# 같은 길이의 벡터list가 주어졌을 때 각 성분별 평균 구하기

def vector_average(vectors):
    """성분의 갯수/1 * 성분의 합 = 성분별 평균"""
    n = len(vectors)
    return scalar_multiply(1/n, vector_sum(vectors))

# 벡터의 내적은 벡터의 각 성분별 곱한 값을 더해준 값이다.

def dot(v, w):  # Dot product
    """v_i * w_i + ... + v_n * w_n"""
    return sum(v_i * w_i for v_i, w_i in zip(v,w))

내적은 벡터 V가 벡터 W 방향으로 얼마나 멀리 뻗어 나가는지를 나타낸다. 예를 들어 w = [1, 0]이면 dot(v, w)는 v의 첫 번째 성분이다.

dot([4,3],[6,0])
24

def sum_of_square(v):
    """v_1 * v_1 + ... v_n * v_n"""
    return dot(v, v)

sum_of_square([4,3])
25

import math

# 벡터의 크기를 계산해보자.
# 벡터의 크기 = root벡터의 제곱의 합
def magnitude(v):
    return math.sqrt(sum_of_square(v)) # math.sqrt는 제곱근을 계산해준다.

magnitude([4,3])
5.0

이제 두 벡터간의 거리를 계산하기 위해 필요한 준비가 됐다. 두 벡터간의 거리는 다음과 같다.

\[\sqrt{(v_1 - w_1)^2+...+(v_2 - w_2)^2}\]
def squared_distance(v, w):
    """(v_1)"""
    return sum_of_square(vector_substract(v,w))

print(squared_distance([1,3],[4,6]))

def distance1(v, w):
    return math.sqrt(squared_distance(v, w))

def distance2(v, w):
    return magnitude(vector_substract(v, w))

print(distance1([1,3],[4,2]))
print(distance2([1,3],[4,2]))

18
3.1622776601683795
3.1622776601683795

벡터를 list로 표현하는 것은 벡터의 원리를 설명하는데 편리하지만 성능상에 문제가 있다. 실제로 코딩을 할 때는 성능도 좋고 다양한 연산이 이미 구현된 NumPy 라이브러리를 사용하자.

행렬(Matrix)

행렬은 2차원으로 구성된 숫자의 집합이며, list의 list로 표현할 수 있다. list 안의 list들은 행렬의 행을 나타내며 모두 같은 길이를 가지게 된다.

예를 들어 A라는 행렬에서 A[i][j]는 1번째 행과 j번째 열에 속한 숫자를 의미한다.

# A는 2개의 행과 3개의 열로 구성되어 있다.
A = [[1,2,3],[4,5,6]]

# B는 3개의 행과 2개의 열로 구성되어 있다.
B = [[1,2],[3,4],[5,6]]

행렬을 list들의 list로 나타내는 경우, 행렬 A는 len(A)개의 행과 len(A[0])개의 열로 구성되어 있다. 이와 같은 행렬의 형태는 다음과 같이 계산할 수 있다.

def shape(A):
    num_rows = len(A)
    num_cols = len(A[0]) if A else 0
    return num_rows, num_cols

shape(A)
(2, 3)

def make_matrix(num_rows, num_cols, entry_fn):
    return [[entry_fn(i, j)
            for j in range(num_cols)
            for i in range(num_rows)]]

def is_diagnoal(i, j):
    """대각선의 원소는 1, 나머지는 0"""
    return 1 if i is j else 0

identify_matrix = make_matrix(5,5,is_diagnoal)
identify_matrix

[[1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1]]

행렬은 몇 가지 이유로 매우 중요하다. 먼저 각 벡터를 행렬의 행으로 나타내면 여러 벡터로 구성된 데이터셋을 행렬로 표현할 수 있다. 예컨데 1000명에 대한 키, 몸무게, 나이가 주어졌다면 1000 x 3 행렬로 표현할 수 있다.

두 번째로, k차원의 벡터를 n차원 벡터로 변환해주는 선형함수를 n x k 행렬로 표현할 수 있다.

세 번째로, 행렬로 이진 관계(binary relationship)을 나타낼 수 있다.

friendships = [[0, 1, 1, 0, 0, 0, 0, 0, 0, 0], # 사용자 0
               [1, 0, 1, 1, 0, 0, 0, 0, 0, 0]] # 사용자 1

만약 네트워크에 연결된 사용자 수가 적다면 행렬은 수 많은 0값을 저장해야 해 비효율적이다.

하지만 행렬에서는 두 사용자가 연결되어 있는지 훨씬 빠르게 확인할 수 있다. 직접 행렬의 값만 보면 되기 때문이다.

friendships[0][2]
1
friendships[1][4]
1

사용자가 누구와 연결되어 있는지 확인하기 위해서는 해당 사용자를 나타내는 열(또는 행)만 보면 된다.

friendships = [[0, 1, 1, 0, 0, 0, 0, 0, 0, 0], # 사용자 0
               [1, 0, 1, 1, 0, 0, 0, 0, 0, 0]] # 사용자 1
friends_of_zero = [i for i, is_friend in enumerate(friendships[0]) if is_friend]
friends_of_zero

[1, 2]

더 공부하기!

https://www.math.ucdavis.edu/~linear
http://joshua.smcvt.edu/linearalgebra
http://www.math.brown.edu/~treil/papers/LADW/LADW.html
http://www.numpy.org