Python Programming

Python Programming (6) - 선형대수

oziguyo_ 2020. 3. 28. 06:53
728x90
6.Linear_algebra

6. Linear algebra

Python에서의 선형 대수

  • 이 단원에서는 선형 대수의 여러 개념을 Python 기본 함수와 리스트 등을 이용하여 구현하는 연습을 한다.
  • 벡터를 생성하고, 벡터와 행렬 연산을 수행할 수 있는 함수를 작성한다.
  • 이 후, Python 모듈인 numpy를 이용하여 직접 만들었던 선형 대수의 기능과 비교해 본다.

벡터 - Vectors

  • 벡터는 벡터 공간의 원소를 벡터라 하며,
  • 백터들은 서로 더하거나 스칼라에 의해 곱해질 수 있다.
  • 벡터를 숫자들의 리스트라고 생각해 보자.
In [1]:
height_weigth_age = [70,    # inches
                    170,    # pounds
                    40 ]    # years
In [2]:
grades = [95,    # exam1
          80,    # exam2
          75,    # exam3
          62 ]   # exam4
  • Python의 list는 벡터 연산을 제공하지 않기 때문에, 벡터 연산을 추가해 보자.

벡터 합과 차

  • 원소별로 계산 : 다음을 구현하고 싶음
    • [1, 2] 더하기 [3, 4] = [4, 6]
    • [5, 3] 빼기 [1, 7] = [4, -4]
In [3]:
def vector_add(v, w):
    """adds two vectors componentwise"""
    return [v_i + w_i for v_i, w_i in zip(v,w)]
In [4]:
vector_add([1,2], [3,4])
Out[4]:
[4, 6]
In [5]:
vector_add([1,2,3], [3,4,5])
Out[5]:
[4, 6, 8]
In [6]:
v = [1, 2, 3]
w = [2, 3, 4]
z = vector_add(v, w)
y = vector_add(z, v)
print(y)
[4, 7, 10]
In [7]:
def vector_subtract(v, w):
    """subtracts two vectors componentwise"""
    return [v_i - w_i for v_i, w_i in zip(v,w)]
In [8]:
vector_subtract([0, 0, 1], [1, 2, 3])
Out[8]:
[-1, -2, -2]

여러 개의 벡터들의 합

  • 벡터들의 리스트가 있을 때, 리스트 내의 벡터들을 원소 별로 합하기
In [9]:
def vector_sum(vectors):
    result = vectors[0]
    for vector in vectors[1:]:
        result = vector_add(result, vector)
    return result
In [10]:
vectors = [[1, 2, 3],
           [2, 3, 0],
           [0, 1, -2]]

vector_sum(vectors)
Out[10]:
[3, 6, 1]
In [11]:
from functools import reduce
# 앞의 vector_sum과 같은 기능을 가진다.
def vector_sum(vectors):
    return reduce(vector_add, vectors)
In [12]:
vectors = [[1, 2, 3],
           [2, 3, 0],
           [0, 1, -2]]

vector_sum(vectors)
Out[12]:
[3, 6, 1]
In [13]:
# 내부적으로는 다음과 동치
vector_add(vector_add([1, 2, 3], [2, 3, 0]), [0, 1, -2])
Out[13]:
[3, 6, 1]

스칼라 곱

  • 목표 : 3*[1, 2, 3] = [3, 6, 9]
In [14]:
def scalar_multiply(c, v):
    return [c * vi for vi in v]
In [15]:
scalar_multiply(3, [1, 2, 3])
Out[15]:
[3, 6, 9]
  • 컴포넌트별 평균 : vector_mean([1, 2], [2, 4], [3, 6]) == [3, 6]
In [16]:
def vector_mean(vectors):
    n = len(vectors)
    return scalar_multiply(1/n, vector_sum(vectors))
In [17]:
vector_mean([[1, 2], [2, 4], [3, 6]])
Out[17]:
[2.0, 4.0]
  • 단, Python 2.x의 경우 위의 나누기에 실수 나누기를 적용하기 위해서는 파일 위쪽에 다음을 표기
    • Python 3.x는 실수 나누기가 적용되기 때문에 상관 없음
In [18]:
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
In [19]:
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))
In [20]:
dot([1, 2, 3], [0, 1, 2])
Out[20]:
8

제곱합

  • 벡터 원소들의 제곱의 합
In [21]:
def sum_of_squares(v):
    """v_1 * v_1 + ... + v_n * v_n"""
    return dot(v, v)
In [22]:
sum_of_squares([0, 1, 2])
Out[22]:
5
  • 벡터의 크기(magnitude) : 제곱합의 제곱근
In [23]:
import math
def magnitude(v):
    return math.sqrt(sum_of_squares(v))
In [24]:
magnitude([3, 4])
Out[24]:
5.0
In [25]:
magnitude([5, 12])
Out[25]:
13.0

벡터 사이의 거리

  • 한 벡터에서 다른 벡터를 뺀 후, 크기를 구하는 것과 동일
In [26]:
def distance(v, w):
    return magnitude(vector_subtract(v, w))
In [27]:
distance([1, 2], [2, 3])
Out[27]:
1.4142135623730951

NumPy

  • NumPy는 Python에서의 수학/과학 컴퓨팅의 기본 패키지로서
  • numpy 모듈에는 지금까지 행한 벡터 연산들이 구현되어 있음.
  • numpy 패키지 (모듈)는 Python을 사용하는 거의 모든 수치 계산에 사용된다.
  • Python을 위한 벡터, 행렬 및 고차원 데이터 구조를 제공한다.
  • http://www.numpy.org/

numpy 모듈을 이용하기 위해서는 다음과 같이 import를 먼저 진행한다. 시작 시 한 번만 불러오면 된다.

In [28]:
import numpy as np  

다음은 dot product 예제이다.

In [29]:
# dot product
np.dot([1,2], [3,4])
Out[29]:
11

Numpy array

  • numpy의 다양한 기능은 array라는 데이터구조를 바탕으로 이루어진다.

    • 효율적이고 계산이 편리함
  • numpy array는 다양한 방법을 통해 만들 수 있다.

    • 파이썬 리스트 또는 튜플을 이용하는 방법
    • arange, linspace 등과 같이 numpy 배열을 생성하는 데 사용되는 함수를 사용하는 방법
    • 파일에서 데이터를 읽어들이는 방법
  • 다차원 배열을 구현
  • 수학적 계산에 특화

Python list로부터 numpy array 만들기

In [30]:
import numpy as np  # 이미 한 번 import하였으면 다시 하지 않아도 된다.
a = np.array([0, 1, 2, 3])

a
Out[30]:
array([0, 1, 2, 3])
In [31]:
print(a)
[0 1 2 3]
In [32]:
a.ndim  # 1 차원
Out[32]:
1
In [33]:
a.shape  # 형태 : (4,)
Out[33]:
(4,)
In [34]:
len(a)  # 4
Out[34]:
4
In [35]:
# matrix: Python list로 이루어진 list를 이용하여 matrix 만들기
b = np.array([[0,1,2], [3,4,5]])

b
Out[35]:
array([[0, 1, 2],
       [3, 4, 5]])
In [36]:
b.ndim  # 2차원
Out[36]:
2
In [37]:
b.shape # 형태 (2,3)
Out[37]:
(2, 3)
In [38]:
len(b)  # 2 : 첫번째 차원의 길이
Out[38]:
2

array는 list와 비슷해 보이지만, numpy에서 array라는 별도의 데이터구조를 이용하는 몇 가지 이유가 있다.

  • Python list는 동적으로 할당되며, list내의 원소들이 서로 다른 데이터형을 가질 수 있다.
  • 이러한 점은 벡터나 행렬 계산을 느리게 혹은 불가능하게 한다.
  • 반면 numpy array내의 원소들의 데이터 형은 일정하고(homogeneous), 변하지 않기 때문에, 메모리 효율적이고, 행렬이나 벡터 계산을 빠르게 할 수 있다.
  • 이미 데이터 타입이 결정된 array의 원소를 다른 데이터 타입으로 변경하면 에러가 발생한다.
In [39]:
b.dtype
Out[39]:
dtype('int32')
In [40]:
b[0,0] = "a"
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-40-9c6a29519c7a> in <module>()
----> 1 b[0,0] = "a"

ValueError: invalid literal for int() with base 10: 'a'

array 생성

numpy에서는 다양한 방법을 통해 array를 생성할 수 있도록 도와준다.

In [43]:
a = np.arange(10)  # range 함수와 비슷

a
Out[43]:
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
In [44]:
b = np.arange(1, 9, 2) 

b
Out[44]:
array([1, 3, 5, 7])
In [45]:
c = np.linspace(0, 1, 6)  #시작, 끝, 숫자 개수

c
Out[45]:
array([0. , 0.2, 0.4, 0.6, 0.8, 1. ])
In [46]:
d = np.ones((3, 3))  # 1로 이루어진 다차원 배열

d
Out[46]:
array([[1., 1., 1.],
       [1., 1., 1.],
       [1., 1., 1.]])
In [47]:
e = np.zeros((2, 2))  # 0으로 이루어진 다차원 배열

e
Out[47]:
array([[0., 0.],
       [0., 0.]])
In [48]:
f = np.eye(3)  # identity 행렬

f
Out[48]:
array([[1., 0., 0.],
       [0., 1., 0.],
       [0., 0., 1.]])
In [49]:
g = np.diag(np.array([1, 2, 3, 4]))  # 대각 행렬

g
Out[49]:
array([[1, 0, 0, 0],
       [0, 2, 0, 0],
       [0, 0, 3, 0],
       [0, 0, 0, 4]])

array 생성(2)

numpy.random 모듈을 이용한 랜덤 array 생성

In [50]:
np.random.rand(4)       # uniform in [0, 1]
Out[50]:
array([0.76767629, 0.33294295, 0.33645484, 0.9047073 ])
In [51]:
np.random.randn(4)      # standard normal
Out[51]:
array([-0.24796071,  0.56965874,  1.65606846,  0.51729609])
In [52]:
2.5 * np.random.randn(4) + 3   # 평균 3, 표준편차 2.5인 정규분포
Out[52]:
array([3.81524923, 2.76766133, 3.14255768, 0.1636803 ])

위 예제들에서 경우에 따라 마침표 (예 : 2. vs 2)가 표시된다는 것을 알 수 있다. 이는 데이터 유형의 차이 때문이다. 마침표가 없는 것은 정수형 데이터, 마침표가 있는 것은 실수형 데이터를 의미한다. numpy array는 한 종류의 데이터 유형만 가질 수 있다.

Basic Slicing and Indexing

  • Python list와 비슷하게 [ ]를 이용하여 원소에 접근한다.
  • slicing 또한 Python list와 마찬가지로 start:stop:step를 이용한다.
In [53]:
v = np.array([1, 2, 3, 4, 5, 6])
v
Out[53]:
array([1, 2, 3, 4, 5, 6])
In [54]:
v[0]
Out[54]:
1
In [55]:
v[2], v[-1]  # -1은 마지막 원소
Out[55]:
(3, 6)
In [56]:
v[1:5:2]
Out[56]:
array([2, 4])
In [57]:
v[::-1]  # 순서 뒤집기
Out[57]:
array([6, 5, 4, 3, 2, 1])

2차원 array에서는 행(row)과 열(column)의 인덱스에 기반한다.

In [58]:
M = np.array([[1, 2], [3, 4]])
M
Out[58]:
array([[1, 2],
       [3, 4]])
In [59]:
M[1, 1]
Out[59]:
4
In [60]:
# 인덱스 하나만 사용하면 하나의 행을 슬라이싱
M[1]  # row 1 
Out[60]:
array([3, 4])
In [61]:
M[1, :] # row 1 (여기서 :는 모든 열을 의미)
Out[61]:
array([3, 4])
In [62]:
M[:, 1] # column 1 (여기서 :는 모든 행을 의미)
Out[62]:
array([2, 4])
In [63]:
M[0, 0] = 10

M
Out[63]:
array([[10,  2],
       [ 3,  4]])

대입 연산도 마찬가지로 이루어질 수 있다.

In [64]:
M[1, :] = -1

M
Out[64]:
array([[10,  2],
       [-1, -1]])
In [65]:
M[:, 1] = 777

M
Out[65]:
array([[ 10, 777],
       [ -1, 777]])
In [66]:
a = np.array([[n + m * 10 for n in range(6)] for m in range(6)])

a
Out[66]:
array([[ 0,  1,  2,  3,  4,  5],
       [10, 11, 12, 13, 14, 15],
       [20, 21, 22, 23, 24, 25],
       [30, 31, 32, 33, 34, 35],
       [40, 41, 42, 43, 44, 45],
       [50, 51, 52, 53, 54, 55]])

<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

In [67]:
a[0, 3:5]
Out[67]:
array([3, 4])
In [68]:
a[4:, 4:]
Out[68]:
array([[44, 45],
       [54, 55]])
In [69]:
a[:, 2]
Out[69]:
array([ 2, 12, 22, 32, 42, 52])
In [70]:
a[2::2, ::2]
Out[70]:
array([[20, 22, 24],
       [40, 42, 44]])
In [71]:
a[1:3, 1:3]
Out[71]:
array([[11, 12],
       [21, 22]])
In [72]:
a[::2, ::2]
Out[72]:
array([[ 0,  2,  4],
       [20, 22, 24],
       [40, 42, 44]])

<img src = "figure/numpy_fancy_indexing.png", width = 600, height = 600>

Advanced Indexing

:를 이용하는 slicing이 아닌 정수 혹은 Boolean의 list, array, tuple 등으로 이루어진 indexing을 말한다.

In [73]:
# 행과 열의 index들이 모두 slicing이 아닐 때 - 정수로 이루어진 리스트
a[[0,1,2,3,4], [1,2,3,4,5]]
Out[73]:
array([ 1, 12, 23, 34, 45])
In [74]:
a[(0,1,2,3,4), (1,2,3,4,5)]
Out[74]:
array([ 1, 12, 23, 34, 45])
In [75]:
# 아래와 같이 해석할 수 있다.
np.array([a[0,1], a[1,2], a[2,3], a[3,4], a[4,5]])
Out[75]:
array([ 1, 12, 23, 34, 45])
In [76]:
# slicing과는 다르다.
a[0:5, 1:6]
Out[76]:
array([[ 1,  2,  3,  4,  5],
       [11, 12, 13, 14, 15],
       [21, 22, 23, 24, 25],
       [31, 32, 33, 34, 35],
       [41, 42, 43, 44, 45]])
In [77]:
# 행과 열의 index중 하나라도 slicing일 때는 slicing의 규칙을 따른다.
a[3:, [0,2,5]]
Out[77]:
array([[30, 32, 35],
       [40, 42, 45],
       [50, 52, 55]])
In [78]:
# Boolean array를 이용할 수도 있다.
mask = np.array([1, 0, 1, 0, 0, 1], dtype=bool)
mask
Out[78]:
array([ True, False,  True, False, False,  True])
In [79]:
a[mask, 2]
Out[79]:
array([ 2, 22, 52])
In [80]:
row_indices = [1, 2, 3]
a[row_indices]
Out[80]:
array([[10, 11, 12, 13, 14, 15],
       [20, 21, 22, 23, 24, 25],
       [30, 31, 32, 33, 34, 35]])
In [81]:
col_indices = [1, 2, -1]             # -1은 마지막 원소를 나타냄
a[row_indices, col_indices]
Out[81]:
array([11, 22, 35])
In [82]:
b = np.array([n for n in range(5)])
b
Out[82]:
array([0, 1, 2, 3, 4])
In [83]:
row_mask = np.array([True, False, True, False, False])
b[row_mask]
Out[83]:
array([0, 2])
In [84]:
# same thing
row_mask = np.array([1,0,1,0,0], dtype=bool)
b[row_mask]
Out[84]:
array([0, 2])
In [85]:
x = np.arange(0, 10, 0.5)
x
Out[85]:
array([0. , 0.5, 1. , 1.5, 2. , 2.5, 3. , 3.5, 4. , 4.5, 5. , 5.5, 6. ,
       6.5, 7. , 7.5, 8. , 8.5, 9. , 9.5])
In [86]:
x[(5 < x) * (x < 7.5)]
Out[86]:
array([5.5, 6. , 6.5, 7. ])
  • 행렬의 원소별 곱셈
In [87]:
M * N
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-87-8da4014d6915> in <module>()
----> 1 M * N

NameError: name 'N' is not defined

Numpy array에서의 복사와 참조

  • 성능 향상을 위해 파이썬에서의 많은 경우 복사를 하지 않고 참조를 하는 경우가 많다.
In [88]:
A = np.array([[1, 2], [3, 4]])

A
Out[88]:
array([[1, 2],
       [3, 4]])
In [89]:
# B는 A를 참조만 한다.
B = A
In [90]:
# B의 변화는 A에 영향을 미침
B[0,0] = 10

B
Out[90]:
array([[10,  2],
       [ 3,  4]])
  • 만약 이런 현상을 원치 않는다면, .copy()를 이용하여 복사한다.
In [91]:
C = np.copy(A)
C[0, 0] = -1

C
Out[91]:
array([[-1,  2],
       [ 3,  4]])

array slicing 또한 기본적으로 참조를 바탕으로 하므로 주의해야 한다.

  • view를 제공한다고도 표현한다.
In [92]:
# A는 바뀌지 않음
A
Out[92]:
array([[10,  2],
       [ 3,  4]])
In [93]:
a = np.arange(10)
b = a[::2]
b
Out[93]:
array([0, 2, 4, 6, 8])
In [94]:
b[0] = 12
b
Out[94]:
array([12,  2,  4,  6,  8])
In [95]:
a   # 주의
Out[95]:
array([12,  1,  2,  3,  4,  5,  6,  7,  8,  9])
In [96]:
a = np.arange(10)
c = a[::2].copy()  #복사하여 사용하면
c[0] = 12
c
Out[96]:
array([12,  2,  4,  6,  8])
In [97]:
a
Out[97]:
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
In [98]:
# Numpy array의 경우
a = np.arange(10)
b = a[:]   # 참조가 발생함
b[0] = 10
a
Out[98]:
array([10,  1,  2,  3,  4,  5,  6,  7,  8,  9])
In [99]:
# Python list의 경우는 다르다.
x = list(range(10))
y = x[:]     # list는 이 경우 복사함
y[0] = 10
x
Out[99]:
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

반면 정수 리스트나 Boolean 리스트를 바탕으로 하는 advanced indexing은 복사를 기본으로 한다.

In [100]:
a = np.arange(10)
d = a[[0,1,2]]
In [101]:
d[0] = 10
d
Out[101]:
array([10,  1,  2])
In [102]:
# a는 바뀌지 않음
a
Out[102]:
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

numpy와 numpy.linalg를 이용한 선형대수

  • numpy는 벡터와 행렬 연산에 있어 강력한 기능을 가지고 있다.
  • 벡터 합과 차
In [103]:
a = np.array([3,1,-1])
b = np.arange(3)
c = a + b
d = b - a
  • 스칼라 곱
In [104]:
4 * a
Out[104]:
array([12,  4, -4])
  • 제곱합
In [105]:
np.sum(a**2)
Out[105]:
11
  • dot product
In [106]:
np.dot(a, b)
Out[106]:
-1
  • 벡터 사이의 거리
In [107]:
np.linalg.norm(a-b)
Out[107]:
4.242640687119285
  • 벡터 원소별 곱셈
In [108]:
a * b
Out[108]:
array([ 0,  1, -2])
In [109]:
M = np.array([[1,2], [3,4]])

M
Out[109]:
array([[1, 2],
       [3, 4]])
  • transpose
In [110]:
M.T
Out[110]:
array([[1, 3],
       [2, 4]])
  • 행렬식
In [111]:
np.linalg.det(M)
Out[111]:
-2.0000000000000004
  • 역행렬
In [112]:
np.linalg.inv(M)
Out[112]:
array([[-2. ,  1. ],
       [ 1.5, -0.5]])
  • 해 찾기
In [113]:
c = np.array([2,1])
np.linalg.solve(M, c)
Out[113]:
array([-3. ,  2.5])
  • 행렬곱
In [114]:
N = np.array([[-1,1],[2,1]])

N
Out[114]:
array([[-1,  1],
       [ 2,  1]])
In [115]:
np.matmul(M,N)
Out[115]:
array([[3, 3],
       [5, 7]])
  • 행렬의 원소별 곱셈
In [116]:
M * N
Out[116]:
array([[-1,  2],
       [ 6,  4]])
728x90