Python Programming (6) - 선형대수
6. Linear algebra¶
Python에서의 선형 대수¶
- 이 단원에서는 선형 대수의 여러 개념을 Python 기본 함수와 리스트 등을 이용하여 구현하는 연습을 한다.
- 벡터를 생성하고, 벡터와 행렬 연산을 수행할 수 있는 함수를 작성한다.
- 이 후, Python 모듈인 numpy를 이용하여 직접 만들었던 선형 대수의 기능과 비교해 본다.
벡터 - Vectors¶
- 벡터는 벡터 공간의 원소를 벡터라 하며,
- 백터들은 서로 더하거나 스칼라에 의해 곱해질 수 있다.
- 벡터를 숫자들의 리스트라고 생각해 보자.
height_weigth_age = [70, # inches
170, # pounds
40 ] # years
grades = [95, # exam1
80, # exam2
75, # exam3
62 ] # exam4
- Python의 list는 벡터 연산을 제공하지 않기 때문에, 벡터 연산을 추가해 보자.
벡터 합과 차¶
- 원소별로 계산 : 다음을 구현하고 싶음
- [1, 2] 더하기 [3, 4] = [4, 6]
- [5, 3] 빼기 [1, 7] = [4, -4]
def vector_add(v, w):
"""adds two vectors componentwise"""
return [v_i + w_i for v_i, w_i in zip(v,w)]
vector_add([1,2], [3,4])
vector_add([1,2,3], [3,4,5])
v = [1, 2, 3]
w = [2, 3, 4]
z = vector_add(v, w)
y = vector_add(z, v)
print(y)
def vector_subtract(v, w):
"""subtracts two vectors componentwise"""
return [v_i - w_i for v_i, w_i in zip(v,w)]
vector_subtract([0, 0, 1], [1, 2, 3])
여러 개의 벡터들의 합¶
- 벡터들의 리스트가 있을 때, 리스트 내의 벡터들을 원소 별로 합하기
def vector_sum(vectors):
result = vectors[0]
for vector in vectors[1:]:
result = vector_add(result, vector)
return result
vectors = [[1, 2, 3],
[2, 3, 0],
[0, 1, -2]]
vector_sum(vectors)
from functools import reduce
# 앞의 vector_sum과 같은 기능을 가진다.
def vector_sum(vectors):
return reduce(vector_add, vectors)
vectors = [[1, 2, 3],
[2, 3, 0],
[0, 1, -2]]
vector_sum(vectors)
# 내부적으로는 다음과 동치
vector_add(vector_add([1, 2, 3], [2, 3, 0]), [0, 1, -2])
스칼라 곱¶
- 목표 : 3*[1, 2, 3] = [3, 6, 9]
def scalar_multiply(c, v):
return [c * vi for vi in v]
scalar_multiply(3, [1, 2, 3])
- 컴포넌트별 평균 : vector_mean([1, 2], [2, 4], [3, 6]) == [3, 6]
def vector_mean(vectors):
n = len(vectors)
return scalar_multiply(1/n, vector_sum(vectors))
vector_mean([[1, 2], [2, 4], [3, 6]])
- 단, Python 2.x의 경우 위의 나누기에 실수 나누기를 적용하기 위해서는 파일 위쪽에 다음을 표기
- Python 3.x는 실수 나누기가 적용되기 때문에 상관 없음
from __future__ import division
dot product¶
- 목표 :
- dot( [1, 2, 3], [0, 1, 2]) = sum([1 * 0, 2 * 1, 3 * 2]) = sum([0, 2, 6]) = 8
def dot(v, w):
"""v_1 * w_1 + ... + v_n * w_n"""
return sum(v_i * w_i for v_i, w_i in zip(v, w))
dot([1, 2, 3], [0, 1, 2])
제곱합¶
- 벡터 원소들의 제곱의 합
def sum_of_squares(v):
"""v_1 * v_1 + ... + v_n * v_n"""
return dot(v, v)
sum_of_squares([0, 1, 2])
- 벡터의 크기(magnitude) : 제곱합의 제곱근
import math
def magnitude(v):
return math.sqrt(sum_of_squares(v))
magnitude([3, 4])
magnitude([5, 12])
벡터 사이의 거리¶
- 한 벡터에서 다른 벡터를 뺀 후, 크기를 구하는 것과 동일
def distance(v, w):
return magnitude(vector_subtract(v, w))
distance([1, 2], [2, 3])
NumPy¶
- NumPy는 Python에서의 수학/과학 컴퓨팅의 기본 패키지로서
- numpy 모듈에는 지금까지 행한 벡터 연산들이 구현되어 있음.
- numpy 패키지 (모듈)는 Python을 사용하는 거의 모든 수치 계산에 사용된다.
- Python을 위한 벡터, 행렬 및 고차원 데이터 구조를 제공한다.
- http://www.numpy.org/
numpy
모듈을 이용하기 위해서는 다음과 같이 import
를 먼저 진행한다. 시작 시 한 번만 불러오면 된다.
import numpy as np
다음은 dot product 예제이다.
# dot product
np.dot([1,2], [3,4])
Numpy array¶
numpy의 다양한 기능은 array라는 데이터구조를 바탕으로 이루어진다.
- 효율적이고 계산이 편리함
numpy array는 다양한 방법을 통해 만들 수 있다.
- 파이썬 리스트 또는 튜플을 이용하는 방법
arange
,linspace
등과 같이 numpy 배열을 생성하는 데 사용되는 함수를 사용하는 방법- 파일에서 데이터를 읽어들이는 방법
- 다차원 배열을 구현
- 수학적 계산에 특화
Python list로부터 numpy array 만들기¶
import numpy as np # 이미 한 번 import하였으면 다시 하지 않아도 된다.
a = np.array([0, 1, 2, 3])
a
print(a)
a.ndim # 1 차원
a.shape # 형태 : (4,)
len(a) # 4
# matrix: Python list로 이루어진 list를 이용하여 matrix 만들기
b = np.array([[0,1,2], [3,4,5]])
b
b.ndim # 2차원
b.shape # 형태 (2,3)
len(b) # 2 : 첫번째 차원의 길이
array는 list와 비슷해 보이지만, numpy에서 array라는 별도의 데이터구조를 이용하는 몇 가지 이유가 있다.
- Python list는 동적으로 할당되며, list내의 원소들이 서로 다른 데이터형을 가질 수 있다.
- 이러한 점은 벡터나 행렬 계산을 느리게 혹은 불가능하게 한다.
- 반면 numpy array내의 원소들의 데이터 형은 일정하고(homogeneous), 변하지 않기 때문에, 메모리 효율적이고, 행렬이나 벡터 계산을 빠르게 할 수 있다.
- 이미 데이터 타입이 결정된 array의 원소를 다른 데이터 타입으로 변경하면 에러가 발생한다.
b.dtype
b[0,0] = "a"
array 생성¶
numpy에서는 다양한 방법을 통해 array를 생성할 수 있도록 도와준다.
a = np.arange(10) # range 함수와 비슷
a
b = np.arange(1, 9, 2)
b
c = np.linspace(0, 1, 6) #시작, 끝, 숫자 개수
c
d = np.ones((3, 3)) # 1로 이루어진 다차원 배열
d
e = np.zeros((2, 2)) # 0으로 이루어진 다차원 배열
e
f = np.eye(3) # identity 행렬
f
g = np.diag(np.array([1, 2, 3, 4])) # 대각 행렬
g
array 생성(2)¶
numpy.random 모듈을 이용한 랜덤 array 생성
np.random.rand(4) # uniform in [0, 1]
np.random.randn(4) # standard normal
2.5 * np.random.randn(4) + 3 # 평균 3, 표준편차 2.5인 정규분포
위 예제들에서 경우에 따라 마침표 (예 : 2. vs 2)가 표시된다는 것을 알 수 있다. 이는 데이터 유형의 차이 때문이다. 마침표가 없는 것은 정수형 데이터, 마침표가 있는 것은 실수형 데이터를 의미한다. numpy array는 한 종류의 데이터 유형만 가질 수 있다.
Basic Slicing and Indexing¶
- Python list와 비슷하게 [ ]를 이용하여 원소에 접근한다.
- slicing 또한 Python list와 마찬가지로
start:stop:step
를 이용한다.
v = np.array([1, 2, 3, 4, 5, 6])
v
v[0]
v[2], v[-1] # -1은 마지막 원소
v[1:5:2]
v[::-1] # 순서 뒤집기
2차원 array에서는 행(row)과 열(column)의 인덱스에 기반한다.
M = np.array([[1, 2], [3, 4]])
M
M[1, 1]
# 인덱스 하나만 사용하면 하나의 행을 슬라이싱
M[1] # row 1
M[1, :] # row 1 (여기서 :는 모든 열을 의미)
M[:, 1] # column 1 (여기서 :는 모든 행을 의미)
M[0, 0] = 10
M
대입 연산도 마찬가지로 이루어질 수 있다.
M[1, :] = -1
M
M[:, 1] = 777
M
a = np.array([[n + m * 10 for n in range(6)] for m in range(6)])
a
<img src = "figure/numpy_indexing.png", width = 500, height = 500> https://scipy-lectures.org/intro/numpy/array_object.html#what-are-numpy-and-numpy-arrays
a[0, 3:5]
a[4:, 4:]
a[:, 2]
a[2::2, ::2]
a[1:3, 1:3]
a[::2, ::2]
<img src = "figure/numpy_fancy_indexing.png", width = 600, height = 600>
Advanced Indexing¶
:
를 이용하는 slicing이 아닌 정수 혹은 Boolean의 list, array, tuple 등으로 이루어진 indexing을 말한다.
# 행과 열의 index들이 모두 slicing이 아닐 때 - 정수로 이루어진 리스트
a[[0,1,2,3,4], [1,2,3,4,5]]
a[(0,1,2,3,4), (1,2,3,4,5)]
# 아래와 같이 해석할 수 있다.
np.array([a[0,1], a[1,2], a[2,3], a[3,4], a[4,5]])
# slicing과는 다르다.
a[0:5, 1:6]
# 행과 열의 index중 하나라도 slicing일 때는 slicing의 규칙을 따른다.
a[3:, [0,2,5]]
# Boolean array를 이용할 수도 있다.
mask = np.array([1, 0, 1, 0, 0, 1], dtype=bool)
mask
a[mask, 2]
row_indices = [1, 2, 3]
a[row_indices]
col_indices = [1, 2, -1] # -1은 마지막 원소를 나타냄
a[row_indices, col_indices]
b = np.array([n for n in range(5)])
b
row_mask = np.array([True, False, True, False, False])
b[row_mask]
# same thing
row_mask = np.array([1,0,1,0,0], dtype=bool)
b[row_mask]
x = np.arange(0, 10, 0.5)
x
x[(5 < x) * (x < 7.5)]
- 행렬의 원소별 곱셈
M * N
Numpy array에서의 복사와 참조¶
- 성능 향상을 위해 파이썬에서의 많은 경우 복사를 하지 않고 참조를 하는 경우가 많다.
A = np.array([[1, 2], [3, 4]])
A
# B는 A를 참조만 한다.
B = A
# B의 변화는 A에 영향을 미침
B[0,0] = 10
B
- 만약 이런 현상을 원치 않는다면,
.copy()
를 이용하여 복사한다.
C = np.copy(A)
C[0, 0] = -1
C
array slicing 또한 기본적으로 참조를 바탕으로 하므로 주의해야 한다.
- view를 제공한다고도 표현한다.
# A는 바뀌지 않음
A
a = np.arange(10)
b = a[::2]
b
b[0] = 12
b
a # 주의
a = np.arange(10)
c = a[::2].copy() #복사하여 사용하면
c[0] = 12
c
a
# Numpy array의 경우
a = np.arange(10)
b = a[:] # 참조가 발생함
b[0] = 10
a
# Python list의 경우는 다르다.
x = list(range(10))
y = x[:] # list는 이 경우 복사함
y[0] = 10
x
반면 정수 리스트나 Boolean 리스트를 바탕으로 하는 advanced indexing은 복사를 기본으로 한다.
a = np.arange(10)
d = a[[0,1,2]]
d[0] = 10
d
# a는 바뀌지 않음
a
numpy와 numpy.linalg를 이용한 선형대수¶
- numpy는 벡터와 행렬 연산에 있어 강력한 기능을 가지고 있다.
- 벡터 합과 차
a = np.array([3,1,-1])
b = np.arange(3)
c = a + b
d = b - a
- 스칼라 곱
4 * a
- 제곱합
np.sum(a**2)
- dot product
np.dot(a, b)
- 벡터 사이의 거리
np.linalg.norm(a-b)
- 벡터 원소별 곱셈
a * b
M = np.array([[1,2], [3,4]])
M
- transpose
M.T
- 행렬식
np.linalg.det(M)
- 역행렬
np.linalg.inv(M)
- 해 찾기
c = np.array([2,1])
np.linalg.solve(M, c)
- 행렬곱
N = np.array([[-1,1],[2,1]])
N
np.matmul(M,N)
- 행렬의 원소별 곱셈
M * N