이번 포스팅에서는 인덱스 반복자(iterator) enumerate() 와 numpy 의 다차원 배열에 대한 인덱스 반복자 (multidimensional index iterator) np.ndenumerate(arr) 메소드에 대해서 소개하겠습니다. 

 

(1) 인덱스 반복자 (index iterator): enumerate()

(2) 다차원 배열 (multidimensional index iterator): numpy.ndenumerate(arr)

 

 

 

반복(iteration) 이란 일련의 작업 명령문을 반복해서 실행(repeated execution)하는 것을 말합니다. Python은 이러한 반복 작업을 더 쉽게 수행할 수 있도록 하는 여러 언어 기능이 있습니다.

 

반복자(iterator)는 반복할 수 있는 값의 수를 계산하는 객체입니다. Python의 데이터 유형 중에서 리스트(Lists), 튜플(Tuples), 사전형(Dictionaries), 문자열(Strings), 집합(Sets) 이 모두 반복 가능한 객체들로서, 반복자(iterator)를 가져올 수 있습니다. 

 

 

 

(1) 인덱스 반복자 (index iterator): enumerate()

 

Python에서는 for loop 순환문이 종종 반복가능한 객체에 순환 반복문으로 사용이 됩니다. for loop 문을 사용하면 인덱스 또는 순환하는 횟수는 반환하지 않고, 오로지 반복가능한 객체의 원소만을 반환하게 됩니다.

 

아래 예에서는 리스트(list)에 for loop 문을 사용해서 리스트 안의 원소를 하나씩 순서대로 반복해서 프린트하는 코드입니다. (이때 인덱스 정보는 없음)  

 

## (1) without index iterator
x_list = ['a', 'b', 'c', 'd']

for x in x_list:
    print('item:', x)
    
# item: a
# item: b
# item: c
# item: d

 

 

 

만약 for loop 순환문을 반복가능한 객체에 사용하면서 enumerate() 메소드를 사용하면 쉽고 편리하게 인덱스 정보 (index information)을 같이 반환할 수 있습니다.

 

아래 예에서는 enumerate(x_list) 를 for loop 순환문과 같이 사용해서 '인덱스 정보(index information) + 리스트 내 원소 정보'를 순환하면서 반복적으로 프린트해보았습니다. 

 

## (2) enumerate()
## : iteration with access to the index information
x_list = ['a', 'b', 'c', 'd']
print(x_list)

print('----' * 5)

for i, x in enumerate(x_list):
    print('index:', i, '  item:', x)
    

# ['a', 'b', 'c', 'd']
# --------------------
# index: 0   item: a
# index: 1   item: b
# index: 2   item: c
# index: 3   item: d

 

 

 

(2) 다차원 배열 (multidimensional index iterator): numpy.ndenumerate(arr)

 

numpy 의 다차원 배열 (multidimensional array)에 대해서 위의 (1) enumerate() 메소드가 했던 것처럼 반복 가능한 객체에 대해 인덱스 정보를 가져오고 싶다면 numpy.ndenumerate(arr) 메소드를 사용하면 됩니다. 

 

아래 예에서는 3차원 배열(3 dimensional array) 에 대해서 for loop 순환문과 np.ndenumerate(arr) 메소드를 같이 사용해서 3차원 배열의 인덱스 정보 (index information of 3 dimensional array) 와 원소를 순환하면서 반복적으로 프린트해보았습니다. 

 

## numpy.ndenumerate(arr)
## : multidimensional index iterator

import numpy as np

y_3d_arr = np.array([[[1, 2], [3, 4]], [[5, 6], [7, 8]]])
print(y_3d_arr)

print('------' * 5)

for i, y in np.ndenumerate(y_3d_arr):
    print('index:', i, '  item:', y)
    
# [[[1 2]
#   [3 4]]

#  [[5 6]
#   [7 8]]]
# ------------------------------
# index: (0, 0, 0)   item: 1
# index: (0, 0, 1)   item: 2
# index: (0, 1, 0)   item: 3
# index: (0, 1, 1)   item: 4
# index: (1, 0, 0)   item: 5
# index: (1, 0, 1)   item: 6
# index: (1, 1, 0)   item: 7
# index: (1, 1, 1)   item: 8

 

 

반복 가능한 객체로 리스트(Lists), 튜플(Tuples), 사전형(Dictionaries), 문자열(Strings), 집합(Sets) 등이 있다고 했는데요, 아래 예에서는 다차원 리스트에 대해서 for loop 순환문과 np.ndenumerate() 메소드를 같이 사용해서 인덱스 정보와 리스트 내 원소를 순환하면서 반복적으로 인쇄를 해보았습니다. 

 

## iterable objects: Lists, Tuples, Dictionaries, Strings, Sets

x_2d_list = [['a', 'b'], ['c', 'd']]
print(x_2d_list)

print('-----' * 5)

for i, x in np.ndenumerate(x_2d_list):
    print('index:', i, '  item:', x)
    
# [['a', 'b'], ['c', 'd']]
# -------------------------
# index: (0, 0)   item: a
# index: (0, 1)   item: b
# index: (1, 0)   item: c
# index: (1, 1)   item: d

 

 

이번 포스팅이 많은 도움이 되었기를 바랍니다 .

행복한 데이터 과학자 되세요. :-)

 

728x90
반응형
Posted by Rfriend
,

이전 포스팅에서는 numpy 배열의 원소 값을 사전(dictionary)의 (Key, Value)를 매핑해서 변환하는 방법을 소개하였습니다. (rfriend.tistory.com/620)

 

이번 포스팅에서는 Python numpy 의 array 배열의 순서대로 정수를 사전의 키(Key)로 하고, 배열 값을 사전의 값(Value)으로 하는 Python 사전(dictionary) 으로 변환하는 몇 가지 방법을 소개하겠습니다.

 

(1) dict() 와 enumerate() 함수를 이용해 배열로 부터 사전 만들기

(2) for loop 과 enumerate() 함수를 이용해 배열로 부터 사전 만들기

 

 

 

(1) dict() 와 enumerate() 함수를 이용해 배열로 부터 사전 만들기

 

먼저, numpy 라이브러리를 불러오고, 예제로 사용할 (5, 0) shape 의 numpy array 배열을 하나 만들어보겠습니다.

 

import numpy as np

cls_weight = np.array([0.30, 0.50, 0.10, 0.03, 0.07])
cls_weight
[Out]
array([0.3 , 0.5 , 0.1 , 0.03, 0.07])

cls_weight.shape
[Out] 
(5,)

 

 

위의 'cls_weight' 배열을 사전(dictionary)으로 변환해보겠습니다. 사전(dict) 키(Key)가 '0' 부터 시작하고, 배열의 순서대로 사전의 키가 하나씩 증가하며, 배열의 순서대로 사전에 값을 할당하여 보겠습니다.  dict() 함수는 객체를 '키(Key) : 값(Value)' 의 쌍을 가지는 사전형 자료구조를 만들어줍니다.

 

## converting numpy array to dictionary, 
## dict key is starting from 0
cls_weight_dict_from_0 = dict(enumerate(cls_weight))

cls_weight_dict_from_0
[Out]
{0: 0.3, 1: 0.5, 2: 0.1, 3: 0.03, 4: 0.07}

 

 

이때 dict() 안의 enumerate() 메소드는 객체를 순환할 때 회수를 세어주는 counter 를 같이 생성해서 enumerate 객체를 반환합니다. for loop 으로 enumerate 객체를 순환하면서 counter 와 배열 내 값을 차례대로 출력을 해보면 아래와 같습니다.

## enumerate() method adds a counter to an iterable 
## and returns it in a form of enumerate object
for i, j in enumerate(cls_weight):
    print(i, ':', j)
    
[Out]
0 : 0.3
1 : 0.5
2 : 0.1
3 : 0.03
4 : 0.07

 

 

경우에 따라서는 배열의 값으로 사전을 만들었을 때, 사전의 키 값이 '0'이 아니라 '1'이나 혹은 다른 숫자로 부터 시작하는 것을 원할 수도 있습니다. 이럴 경우 enumerate(iterable_object, 1) 처럼 원하는 숫자(아래 예에서는 '1')를 추가해주면 그 값이 더해져서 counter 가 생성이 됩니다.

 

## converting numpy array to dictionary, 
## dict key is starting from 1

cls_weight_dict_from_1 = dict(enumerate(cls_weight, 1))

cls_weight_dict_from_1
[Out]
{1: 0.3, 2: 0.5, 3: 0.1, 4: 0.03, 5: 0.07}

 

 

만약 사전(dictionary)으로 변환하려고 하는 numpy array의 axis 1의 축이 있다면 flatten() 메소드를 사용해서 axis 0 만 있는 배열로 먼저 평평하게 펴준 (axis 1 축을 없앰) 후에 위의 dict(enumerate()) 를 똑같이 사용해주면 됩니다.  아래 예는 shape (5, 1) 의 배열을 flatten() 메소드를 써서 shape (5, 0) 으로 바꿔준 후에 dict(enumerate()) 로 배열을 사전으로 변환해주었습니다.

 

## array with axis1
cls_weight_2 = np.array([[0.30], [0.50], [0.10], [0.03], [0.07]])
cls_weight_2
[Out]
array([[0.3 ],
       [0.5 ],
       [0.1 ],
       [0.03],
       [0.07]])


cls_weight_2.shape
[Out]
(5, 1)


## use flatten() method to convert shape (5, 1) to (5, 0)
cls_weight_dict_2 = dict(enumerate(cls_weight_2.flatten()))
print(cls_weight_dict_2)
[Out]
{0: 0.3, 1: 0.5, 2: 0.1, 3: 0.03, 4: 0.07}

 

 

 

(2) for loop 과 enumerate() 함수를 이용해 배열로 부터 사전 만들기

 

이번에는 for loop 과 enumerate() 메소드를 같이 이용하는 방법입니다. 위의 (1) 번 대비 좀 복잡한 느낌이 있기는 하지만, (1) 번 대비 (2) 방법은 for loop 안의 코드 블럭에 좀더 자유롭게 원하는 복잡한 로직을 녹여서 사전(dictionary)을 구성할 수 있다는 장점이 있습니다.

 

아래 예에서는 (a) 먼저 cls_weight_dict_3 = {} 로 비어있는 사전을 만들어 놓고, (b) for loop 으로 순환 반복을 하면서 enumerate(cls_weight) 가 반환해주는 (counter, 배열값) 로 부터 counter 정수 숫자를 받아서 cls_weight_dict_3 의 키(Key) 로 할당해주고, 배열의 값을 사전의 해당 키에 할당해주는 방식입니다.  사전의 키에 값 할당(assinging Value to dict by mapping Key)은 Dict[Key] = Value 구문으로 해줍니다.

 

cls_weight = np.array([0.30, 0.50, 0.10, 0.03, 0.07])
cls_weight
[Out]
array([0.3 , 0.5 , 0.1 , 0.03, 0.07])

## Converting a numpy array to a dictionary
## Dict key is starting from 0
cls_weight_dict_3 = {}

for i, c_w in enumerate(cls_weight):
    cls_weight_dict_3[i] = c_w
    

print(cls_weight_dict_3)
[Out]
{0: 0.3, 1: 0.5, 2: 0.1, 3: 0.03, 4: 0.07}

 

 

사전의 키를 '0' 이 아니라 '1'부터 시작하게 하려면 enumerate()의 counter가 0부터 시작하므로, counter를 사전의 키에 할당할 때 'counter+1' 을 해주면 됩니다.

 

## converting a numpy array to a dictionary using for loop
## dict key is strating from 1

## null dict
cls_weight_dict_3_from_1 = {}

## assigning values by keys + 1
for i, c_w in enumerate(cls_weight):
    cls_weight_dict_3_from_1[i+1] = c_w
    
    
print(cls_weight_dict_3_from_1)
[Out]
{1: 0.3, 2: 0.5, 3: 0.1, 4: 0.03, 5: 0.07}

 

 

이번 포스팅이 많은 도움이 되었기를 바랍니다.

행복한 데이터 과학자 되세요! :-)

 

728x90
반응형
Posted by Rfriend
,

이번 포스팅에서는 1차원 배열 내 고유한 원소 집합 (a set with unique elements) 을 찾고, 더 나아가서 고유한 원소별 개수(counts per unique elements)도 세어보고, 원소 개수를 기준으로 정렬(sorting)도 해보는 여러가지 방법을 소개하겠습니다.

 

 

(1) numpy 1D 배열 안에서 고유한 원소 집합 찾기
    (finding a set with unique elements in 1D numpy array)

(2) numpy 1D 배열 안에서 고유한 원소 별로 개수 구하기
    (counts per unique elements in 1D numpy array)

(3) numpy 1D 배열 안에서 고유한 원소(key) 별 개수(value)를 사전형으로 만들기

    (making a dictionary with unique sets and counts of 1D numpy array)

(4) numpy 1D 배열의 고유한 원소(key) 별 개수(value)의 사전을 정렬하기

    (sorting a dictionary with unique sets and counts of 1D numpy array)

(5) numpy 1D 배열을 pandas Series 로 변환해서 고유한 원소 별 개수 구하고 정렬하기

    (converting 1D array to pandas Series, and value_counts(), sort_values())

(6) numpy 1D 배열을 pandas DataFrame으로 변환해 고유 원소별 개수 구하고 정렬하기

    (converting 1D array to pandas DataFrame, and value_counts(), sort_values())

 

 

 

 

먼저, 예제로 사용할 간단한 numpy 1D 배열을 만들어보겠습니다.

 

## simple 1D numpy array

import numpy as np

arr = np.array(['a', 'c', 'c', 'b', 'a', 
                'b', 'b', 'c', 'a', 'c', 
                'b', 'a', 'a', 'a', 'c'])
                
                
arr
[Out] array(['a', 'c', 'c', 'b', 'a', 'b', 'b', 'c', 'a', 'c', 
             'b', 'a', 'a', 'a', 'c'], dtype='<U1')
             

 

 

(1) numpy 1D 배열 안에서 고유한 원소 집합 찾기
    (finding a set with unique elements in 1D numpy array)

 

np.unique() 메소드를 사용하면 numpy 배열 내 고유한 원소(unique elements)의 집합을 찾을 수 있습니다.

 

## np.unique(): Find the unique elements of an array
np.unique(arr)
[Out] 
array(['a', 'b', 'c'], dtype='<U1')

 

 

더 나아가서, return_inverse=True 매개변수를 설정해주면, 아래의 예처럼 numpy 배열 내 고유한 원소의 집합 배열과 함께 '고유한 원소 집합 배열의 indices 의 배열' 을 추가로 반환해줍니다.

따라서 이 기능을 이용하면 array(['a', 'c', 'c', 'b', 'a', 'b', 'b', 'c', 'a', 'c', 'b', 'a', 'a', 'a', 'c']) 를 ==> array([0, 2, 2, 1, 0, 1, 1, 2, 0, 2, 1, 0, 0, 0, 2]) 로 쉽게 변환할 수 있습니다.

 

## return_inverse=True: If True, also return the indices of the unique array
np.unique(arr, 
          return_inverse=True)
[Out]
(array(['a', 'b', 'c'], dtype='<U1'),
 array([0, 2, 2, 1, 0, 1, 1, 2, 0, 2, 1, 0, 0, 0, 2]))
 

 

 

 

(2) numpy 1D 배열 안에서 고유한 원소 별로 개수 구하기
    (counts per unique elements in 1D numpy array)

 

위의 (1)번에서 np.unique() 로 numpy 배열 내 고유한 원소의 집합을 찾았다면, return_counts = True 매개변수를 설정해주면 각 고유한 원소별로 개수를 구해서 배열로 반환할 수 있습니다.

 

## return_counts: If True, also return the number of times each unique item appears in ar.
np.unique(arr, 
          return_counts = True)     

[Out]
(array(['a', 'b', 'c'], dtype='<U1'), array([6, 4, 5]))

 

 

 

(3) numpy 1D 배열 안에서 고유한 원소(key) 별 개수(value)를 사전형으로 만들기

    (making a dictionary with unique sets and counts of 1D numpy array)

 

위의 (2)번에서 각 고유한 원소별 개수를 구해봤는데요, 이를 파이썬의 키:값 쌍 (key: value pair) 형태의 사전(dictionary) 객체로 만들어보겠습니다.

 

먼저 np.unique(arr, return_counts = True) 의 결과를 unique, counts 라는 이름의 array로 할당을 받고, 이를 zip(unique, counts) 으로 쌍(pair)을 만들어준 다음에, dict() 를 사용해서 사전형으로 변환해주었습니다.

 

## making a dictionary with unique elements and counts of 1D array
unique, counts = np.unique(arr, return_counts = True)
uniq_cnt_dict = dict(zip(unique, counts))

uniq_cnt_dict
[Out]
{'a': 6, 'b': 4, 'c': 5}

 

 

 

(4) numpy 1D 배열의 고유한 원소(key) 별 개수(value)의 사전을 정렬하기

    (sorting a dictionary with unique sets and counts of 1D numpy array)

 

위의 (3)번까지 잘 진행을 하셨다면 이제 (unique : counts) 쌍의 사전을 'counts' 의 값을 기준으로 오름차순 정렬(sorting a dict by value in ascending order) 또는 내림차순 정렬 (sorting a dict by value in descending order) 하고 싶은 마음이 생길 수 있는데요, 이럴 경우 sorted() 메소드를 사용하면 되겠습니다. (pytho dictionary 정렬 참조: rfriend.tistory.com/473)

 

## sorting a dictionary by value in ascending order
## -- reference: https://rfriend.tistory.com/473
sorted(uniq_cnt_dict.items(), 
       key = lambda x: x[1])
       
[Out]
[('b', 4), ('c', 5), ('a', 6)]


## sorting a dictionary by value in descending order
sorted(uniq_cnt_dict.items(), 
       reverse = True, 
       key = lambda x: x[1])
       
[Out]
[('a', 6), ('c', 5), ('b', 4)]

 

 

 

(5) numpy 1D 배열을 pandas Series 로 변환해 고유한 원소별 개수 구하고 정렬하기

    (converting 1D array to pandas Series, and value_counts(), sort_values())

 

pandas 의 Series 나 DataFrame으로 변환해서 데이터 분석 하는 것이 더 익숙하거나 편리한 상황에서는 pandas.Series(array) 나 pandas.DataFrame(array) 로 변환을 해서, value_count() 메소드로 원소의 개수를 세거나, sort_values() 메소드로 값을 기준으로 정렬을 할 수 있습니다.

 

import pandas as pd

## converting an array to pandas Series
arr_s = pd.Series(arr)
arr_s
[Out]
0     a
1     c
2     c
3     b
4     a
5     b
6     b
7     c
8     a
9     c
10    b
11    a
12    a
13    a
14    c
dtype: object


## counting values by unique elements of pandas Series
arr_s.value_counts()
[Out]
a    6
c    5
b    4
dtype: int64


## sorting by values in ascending order of pandas Series
arr_s.value_counts().sort_values(ascending=True)
[Out]
b    4
c    5
a    6
dtype: int64

 

 

(6) numpy 1D 배열을 pandas DataFrame으로 변환해 고유한 원소별 개수 구하고 정렬하기

    (converting 1D array to pandas DataFrame, and value_counts(), sort_values())

 

만약 pandas Series 내 고유한 원소별 개수를 구한 결과를 개수의 오름차순으로 정렬을 하고 싶다면 sort_values(ascending = True) 를 설정해주면 됩니다. (내림차순이 기본 설정, default to descending order)

 

import pandas as pd

## converting an array to pandas DataFrame
arr_df = pd.DataFrame(arr, columns=['x1'])
arr_df

[Out]
x1
0	a
1	c
2	c
3	b
4	a
5	b
6	b
7	c
8	a
9	c
10	b
11	a
12	a
13	a
14	c


## counting the number of unique elements in Series
arr_df['x1'].value_counts()
[Out]
a    6
c    5
b    4
Name: x1, dtype: int64


## # sorting by the counts of unique elements in ascending order
arr_df['x1'].value_counts().sort_values(ascending=True)
[Out]
b    4
c    5
a    6
Name: x1, dtype: int64

 

 

이번 포스팅이 많은 도움이 되었기를 바랍니다.

행복한 데이터 과학자 되세요!  :-)

 

728x90
반응형
Posted by Rfriend
,

이번 포스팅에서는 Python numpy 의 배열의 원소 값을 사전(dictionary)의 {키: 값} 쌍 ({key: value} pair) 을 이용해서, 배열의 원소 값과 사전의 키를 매핑하여 사전의 값으로 배열의 원소값을 변환하는 방법을 소개하겠습니다.

 

아래의 예에서는 다중분류 (multi-class classification) 기계학습 모델로 부터 각 관측치가 5개 classes 별 속할 확률을 배열로 반환받은 상황을 가정하여 만들어보았습니다.

 

(1) 다중분류 확률 배열로 부터 최대값의 위치 인덱스 가져오기

(2) np.vectorize() 와 dict.get() 을 사용해서 최대값 위치 인덱스와 분류 레이블을 매핑하기

(3) for loop 과 dict.get() 을 사용해서 최대값 위치 인덱스와 분류 레이블을 매핑하기

 

 

 

(1) 다중분류 확률 배열로 부터 최대값의 위치 인덱스 가져오기

 

먼저, 5개 class를 가지는 다중분류 문제에서 5개 class 별 속할 확률을 기계학습 분류 모델로 부터 아래의 'pred_proba' 라는 이름의 배열로 얻었다고 가정해보겠습니다.

 

import numpy as np

## probability for each classes
pred_proba = np.array([[0., 0., 0.2, 0.8, 0.], 
                       [0.9, 0., 0., 0., 0.1], 
                       [0., 0., 0.6, 0.2, 0.2], 
                       [0., 0., 0.5, 0.3, 0.2], 
                       [0., 0.1, 0.3, 0., 0.6], 
                       [0., 0.4, 0., 0.3, 0.3]])

pred_proba
[Out]
array([[0. , 0. , 0.2, 0.8, 0. ],
       [0.9, 0. , 0. , 0. , 0.1],
       [0. , 0. , 0.6, 0.2, 0.2],
       [0. , 0. , 0.5, 0.3, 0.2],
       [0. , 0.1, 0.3, 0. , 0.6],
       [0. , 0.4, 0. , 0.3, 0.3]])

 

 

이들 확률값 배열로 부터 하나의 예측값을 구하기 위해 이들 5개 각 class별 확률 중에서 가장 큰 값을 가지는 위치 (indices of maximum value) 의 class 를 모델이 예측한 class 라고 정의해보겠습니다.  

np.argmax(pred_proba, axis=1) 은 배열 내의 각 관측치 별 (axis = 1) 로 가장 큰 확률값의 위치의 인덱스를 반환합니다.  가령, 위의 pred_proba 의 첫번째 관측치의 5개 class 별 속할 확률은 [0., 0., 0.2, 0.8, 0.] 의 배열로서, 확률 0.8 이 가장 큰 값이므로 위치 인덱스 '3'을 반환하였습니다.

 

## positional index for maximum probability
pred_idx = np.argmax(pred_proba, axis=1)
pred_idx
[Out]
array([3, 0, 2, 2, 4, 1])

 

 

(2) np.vectorize() 와 dict.get() 을 사용해서 최대값 위치 인덱스와 분류 레이블을 매핑하기

 

위의 (1)번에서 구한 확률 최대값의 위치 인덱스 가지고, 이번에는 아래의 'class_map_dict'와 같이 {키: 값} 쌍 사전의 '키(key)'를 기준으로 매핑을 해서, 다중분류 모델의 예측값을 'class 이름'으로 변환을 해보겠습니다.

 

## dictionary with pairs of {index_max_proba: class_name}
class_map_dict = {
    0: 'noraml', 
    1: 'class01', 
    2: 'class02', 
    3: 'class03',
    4: 'class04'
}

class_map_dict
[Out]
{0: 'noraml', 1: 'class01', 2: 'class02', 3: 'class03', 4: 'class04'}

 

 

 

이때 dict.get(key) 를 유용하게 사용할 수 있습니다. dict.get(key) 메소드는 사전(dict)의 키에 쌍으로 대응하는 값을 반환해줍니다. 따라서 바로 위에서 정의해준 'class_map_dict'의 키 값을 넣어주면, 각 키에 해당하는 'normal'~'class04' 의 사전 값을 반환해줍니다.

 

## get() returns the value for the specified key if key is in dict.
class_map_dict.get(pred_idx[0])
[Out]
'class03'


class_map_dict.get(0)
[Out]
'noraml'

 

 

사전의 (키: 값)을 매핑하려는 배열 내 원소가 많을 경우, np.vectorize() 메소드를 이용하면 매우 편리하고 또 빠르게 사전의 (키: 값)을 매핑을 해서 배열의 값을 변환할 수 있습니다. 아래 예에서는 'class_map_dict' 의 (키: 값) 사전을 사용해서 'pred_idx'의 확률 최대값 위치 인덱스 배열을 'pred_cls' 의 예측한 클래스(레이블) 이름('normal'~'class04')으로 변환해주었습니다.

 

np.vectorize() 는 numpy의 broadcasting 규칙을 사용해서 매핑을 하므로 코드가 깔끔하고, for loop을 사용하지 않으므로 원소가 많은 배열을 처리해야 할 경우 빠릅니다.

 

## vectorization of dict.get(array_idx) for all elements of array
pred_cls = np.vectorize(class_map_dict.get)(pred_idx)

pred_cls
[Out]
array(['class03', 'noraml', 'class02', 'class02', 'class04', 'class01'],
      dtype='<U7')
      

* np.vectorize() reference: numpy.org/doc/stable/reference/generated/numpy.vectorize.html

 

 

 

(3) for loop 과 dict.get() 을 사용해서 최대값 위치 인덱스와 분류 레이블을 매핑하기

 

만약 위의 (2)번 처럼 np.vectorize() 메소드를 사용하지 않는다면, 아래처럼 for loop 사용해서 확률 최대값 위치 인덱스의 개수 만큼 순환 반복을 하면서 dict.get() 함수를 적용해주어야 합니다. 위의 (2)번 대비 코드도 길고, 또 대상 배열이 클 경우 시간도 더 오래 걸리므로 np.vectorize() 사용을 권합니다.

 

## manually using for loop
pred_cls_mat = np.empty(pred_idx.shape, dtype='object')

for i in range(len(pred_idx)):
    pred_cls_mat[i] = class_map_dict.get(pred_idx[i])
    
pred_cls_mat
[Out]
array(['class03', 'noraml', 'class02', 'class02', 'class04', 'class01'],
      dtype=object)

 

 

이번 포스팅이 많은 도움이 되었기를 바랍니다.

행복한 데이터 과학자 되세요!  :-)

 

728x90
반응형
Posted by Rfriend
,
이번 포스팅에서는 Python Numpy 의 배열(array)을 특정 형상(shape)으로 변형할 때 빈 자리를 '0'이나 다른 값으로 채우는 2가지 방법을 소개하겠습니다.

1. numpy.pad() 함수를 사용하여 배열(array)을 특정 형상의 배열로 변형할 때 빈자리를 '0'으로 채우기
2. tensorflow.keras.preprocessing.sequence.pad_sequence() 함수를 사용하여 배열의 원소 개수가 다른 Ragged array 를 특정 형상의 배열로 바꾸면서 빈자리를 '0'으로 채우기


1. numpy 배열을 특정 형상의 배열로 변형할 때 빈자리를 '0'으로 채우기 (padding)

    : numpy.pad() 함수



먼저, numpy 라이브러리를 importing 하고, 예제로 사용할 2 by 3 의 간단한 2차원 배열(array)을 만들어보겠습니다.




import numpy as np


x = np.array([[1, 2, 3],
                 [7, 2, 5]])
print(x)

array([[1, 2, 3],
          [7, 2, 5]])




위의 2 by 3 의 2차원 배열 x 의 위, 아래에 1개씩의 행을 추가하고, 왼쪽, 오른쪽에 1개씩의 열을 추가하여 4 by 5 의 2차원 배열을 만들되, 새로 추가되는 행과 열의 자리는 '0'으로 채워넣기(padding)를 numpy.pad() 함수를 사용하여 해보겠습니다.

numpy.pad(array, pad_width, mode='constant', **kwargs)


# np.pad(x, (1, 1))

np.pad(x, (1, 1),
       mode='constant',
       constant_values=0)


array([[0, 0, 0, 0, 0]

    [0, 1, 2, 3, 0],     [0, 7, 2, 5, 0],     [0, 0, 0, 0, 0]])




만약 위의 행 1개 추가, 왼쪽 열 1개 추가, 아래쪽 행 2개 추가, 오른쪽 열 2개를 추가하고 싶다면 pad_width 매개변수에 (1, 2) 를 설정해주면 됩니다.


np.pad(x, (1, 2),
       mode='constant',
       constant_values=0)


array([[0, 0, 0, 0, 0, 0],
       [0, 1, 2, 3, 0, 0],
       [0, 7, 2, 5, 0, 0],
       [0, 0, 0, 0, 0, 0],
       [0, 0, 0, 0, 0, 0]])

 



np.pad() 메소드를 사용하지 않고, 아래처럼 numpy.zeros() 로 원하는 형상(shape)대로 모두 '0'으로 채워진 배열을 먼저 만들어놓고, indexing을 사용하여 왼쪽, 위쪽, 오른쪽, 아래쪽에 행과 열을 1개씩 비워놓을 수 있는 위치에 기존 배열을 삽입하여 np.pad() 메소드를 사용했을 때와 동일한 결과를 얻을 수도 있습니다. 이때는 새로 만들어지는 배열 z의 형상(shape)과 기존 배열 x를 채워넣을 위치의 indexing에 신경을 써주어야 하므로 조금 신경이 쓰이는 편이기는 합니다. (위의 np.pad() 와 일처리 순서가 정 반대라고 생각하면 됩니다.)


z = np.zeros((4, 5))
print(z)

[[0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0.]]


z[1:-1, 1:-1] = x
print(z)
[[0. 0. 0. 0. 0.] [0. 1. 2. 3. 0.] [0. 7. 2. 5. 0.] [0. 0. 0. 0. 0.]]

 



np.pad() 함수에 몇가지 재미있는 옵션을 마저 소개하겠습니다.
constant_values 를 설정해주면 '0' 대신에 원하는 다른 특정 상수 값으로 빈 자리를 채워넣을 수 있습니다. '0' 대신에 '-1'을 한번 채워볼까요?


np.pad(x, (1, 1), constant_values=-1)


array([[-1, -1, -1, -1, -1],
       [-1,  1,  2,  3, -1],
       [-1,  7,  2,  5, -1],
       [-1, -1, -1, -1, -1]])




빈 자리 채워넣기하는 방법(mode)에는 항상 똑같은 상수('constant', default) 값을 채워넣는 방법 외에도 'edge', 'linear_ramp', 'maximum', 'mean', 'median', reflect', 'symmetric', 'wrap', 'empty' 등의 다양한 mode 옵션을 제공합니다. 이들 중에서 위의 예시에서 사용한 'constant' 이외에 'edge', 'maximum', 'wrap' 의 mode 옵션을 사용하여 채워넣기 padding을 해보겠습니다. (아래 결과에서 빨간색으로 표시한 부분이 padding된 부분입니다.)


  • mode = 'edge' : 가장 변두리의 원소 값으로 빈 곳 채우기


np.pad(x, (1, 1), mode='edge')


array([[1, 1, 2, 3, 3], [1, 1, 2, 3, 3], [7, 7, 2, 5, 5], [7, 7, 2, 5, 5]])

 


  • mode = 'maximum' : 행과 열의 가장 큰 값으로 빈 곳 채우기


np.pad(x, (1, 1), mode='maximum')


array([[7, 7, 2, 5, 7],
       [3, 1, 2, 3, 3],
       [7, 7, 2, 5, 7],
       [7, 7, 2, 5, 7]])



  • mode = 'wrap' : 행과 열의 반대편 끝에 있는 원소 값으로 빈 곳 채우기


np.pad(x, (1, 1), mode='wrap')


array([[5, 7, 2, 5, 7],
       [3, 1, 2, 3, 1],
       [5, 7, 2, 5, 7],
       [3, 1, 2, 3, 1]])


* Reference: https://numpy.org/doc/stable/reference/generated/numpy.pad.html



  2. 원소 개소가 다른 Ragged array를 특정 형상의 배열로 바꿀 때 빈자리를 '0'으로 채우기

     : tensorflow.keras.preprocessing.sequence.pad_sequence() 함수


위의 np.pad() 함수의 경우 변경하기 전의 원래 배열이 (m by n) 형상인 고정된 차원의 배열을 대상으로 채워넣기를 하였습니다. 두번째로 소개하려는 keras의 sequence.pad_sequence() 함수는 각 행의 원소 개수가 다른 Ragged array(?) 를 대상으로 특정 (j by k) 형상의 고정된 배열로 바꾸려고 할 때 빈 자리를 '0'으로 채워넣는데 사용할 수 있는 차이가 있습니다.


아래의 예를 보면 원소 개수가 1개, 2개, 3개, 4개로서 들쭉날쭉함을 알 수 있습니다. (list를 원소로 가지고 있고, data type 이 object 이네요.)



x2 = np.array([[1], [2, 3], [4, 5, 6], [7, 8, 9, 10]])
display(x2)


array([list([1]), 
      list([2, 3]),
      list([4, 5, 6]),
      list([7, 8, 9, 10])], dtype=object)

 



TensorFlow와 Keras의 tf.keras.preprocessing.sequence() 메소드를 importing 해보겠습니다.



import tensorflow as tf
from tensorflow.keras.preprocessing import sequence

print(tf.__version__)

2.3.0

 



이제 Keras의 pad_sequences() 함수를 사용하여 가장 많은 원소를 가진 행에 맞추어서 (4 by 4) 형상의 배열로 바꾸고, 왼쪽의 빈자리는 '0'으로 채워넣기(padding)를 해보겠습니다.

tf.keras.preprocessing.sequence.pad_sequences(
    sequences, maxlen=None, dtype='int32',
    padding='pre', truncating='pre',
    value=0.0
)

이렇게 (4 by 4) 형상으로 해서 빈자리는 '0'으로 채워주고 나니 각 행의 원소 개수가 모두 4개로서 배열다운 배열이 되었습니다. padding을 해주는 위치의 기본 설정값은 padding='pre' 로서 앞쪽(왼쪽)에 '0'을 채워줍니다. (value=0 이 기본 설정값으로서 '0' 값으로 채워줌)


sequence.pad_sequences(x2) # default: padding='pre', value=0


array([[ 0,  0,  0,  1],
       [ 0,  0,  2,  3],
       [ 0,  4,  5,  6],
       [ 7,  8,  9, 10]], dtype=int32)

 



만약 padding을 해주는 위치를 뒤쪽(오른쪽)으로 하고 싶다면 padding='post' 로 매개변수 설정을 바꿔주면 됩니다.


sequence.pad_sequences(x2, padding='post')


array([[ 1,  0,  0,  0],
       [ 2,  3,  0,  0],
       [ 4,  5,  6,  0],
       [ 7,  8,  9, 10]], dtype=int32)

 



물론 빈 곳 채워넣기(padding)하는 값을 '0'이 아니라 다른 값으로 할 수도 있습니다. '-1'을 사용(value=-1)해서 앞쪽에 빈 곳을 채워넣기해보겠습니다.



sequence.pad_sequences(x2, padding='pre', value=-1)


array([[-1, -1, -1,  1],
       [-1, -1,  2,  3],
       [-1,  4,  5,  6],
       [ 7,  8,  9, 10]], dtype=int32)

 



maxlen 매개변수값을 별도로 설정해주지 않으면 배열 내 행 중에서 가장 많은 원소를 가진 행을 기준으로 maxlen 이 자동으로 정해지는데요, 이를 사용자가 직접 설정해줄 수도 있습니다. 아래의 예에서 maxlen=5 로 설정해주면 (4 by 5) 의 padding 된 배열이 생성됩니다.


sequence.pad_sequences(x2, padding='pre', value=0, maxlen=5)


array([[ 0,  0,  0,  0,  1],
       [ 0,  0,  0,  2,  3],
       [ 0,  0,  4,  5,  6],
       [ 0,  7,  8,  9, 10]], dtype=int32)




아래 예에서처럼 원래의 배열 x2 의 최대길이는 4인데 maxlen=3 으로 값을 설정하게 되면 4-3=1 개의 길이만큼의 원소 값들을 잘라내기(truncating) 해주어야 합니다. 이때 truncating='post' 라고 설정해주면 뒤쪽(오른쪽)을 기준으로 '1'개의 값들을 잘라내주고, 앞쪽을 기준으로 비어있는 곳에는 '0'의 값을 채워주게 됩니다.


sequence.pad_sequences(x2, padding='pre', value=0, maxlen=3, truncating='post')


array([[0, 0, 1],
       [0, 2, 3],
       [4, 5, 6],
       [7, 8, 9]], dtype=int32)



* Reference: https://www.tensorflow.org/api_docs/python/tf/keras/preprocessing/sequence/pad_sequences

많은 도움이 되었기를 바랍니다.
이번 포스팅이 도움이 되었다면 아래의 '공감~'를 꾹 눌러주세요. :-)

728x90
반응형
Posted by Rfriend
,

행렬의 값이 대부분 '0'인 행렬을 희소행렬(Sparse matrix) 이라고 하며, 반대로 행렬의 값이 대부분 '0이 아닌 값'을 가지는 경우 밀집행렬(Dense matrix) 혹은 조밀행렬이라고 합니다. 


가령, 자연어처리(NLP)에서 텍스트를 파싱해서 TF-IDF 행렬을 만들다보면 대부분의 값은 '0'으로 채워져 있고 '0'이 아닌 값은 듬성듬성 들어있는 희소행렬을 주로 얻게 됩니다. 


희소행렬(Sparse matrix)의 경우 대부분의 값이 '0'이므로 이를 그대로 사용할 경우 메모리 낭비가 심하고 또 연산시간도 오래 걸리는 단점이 있습니다. 이런 단점을 피하기 위해 희소행렬을 다른 형태의 자료구조로 변환해서 저장하고 사용합니다. 


희소행렬을 저장하는 자료구조 4가지에는 


(a) Dictionary of keys(DOK): key (행번호, 열번호) 대 value (데이터) 매핑

(b) List of lists (LIL): 링크드 리스트 알고리즘을 이용한 저장 기법, 내용의 추가와 삭제가 용이하지만 CSR 대비 메모리 낭비가 큼

(c) Coordinate list (COO): (행, 열, 값) 튜플 목록 저장

(d) Compressed sparse row (CSR): 가로의 순서대로 재정렬하는 방법으로 행에 관여하여 정리 압축


가 있습니다. 

* reference: https://en.wikipedia.org/wiki/Sparse_matrix



이중에서 Compressed Sparse Row(CSR) 형태(format) 자료구조의 장점과 단점을 표로 정리해보면 아래와 같습니다. 


 CSR 자료구조의 장점

(Advantages of the CSR format)

CSR 자료구조의 단점

(Disadvantages of the CSR format)

  •  효율적인 산술 연산
     (예: CSR + CSR, CSR * CSR 등)
  • 효율적인 행 슬라이싱
    (efficient row slicing)
  • 빠른 행렬 벡터 곱
    (fast matrix vector products)
  • 느린 열 슬라이싱
    (slow column slicing) 
    --> Compressed Sparse Column format 고려
  • 희소성 구조의 변화 시 연산비용 큼
    --> LIL, DOK 고려



이번 포스팅에서는 희소행렬에 대해 이들 중에서도 SciPy 모듈의 csr_matrix() 메소드를 사용하여 


(1) NumPy 희소행렬을 SciPy 압축 희소 행(CSR) 행렬로 변환하기

   (Converting a NumPy sparse matrix to a SciPy compressed sparse row matrix)


(2) SciPy 압축 희소 행(CSR) 행렬을 NumPy 희소행렬로 변환하기 

   (Converting a SciPy compressed sparse row matrix to a NumPy sparse matrix)


하는 각 2가지 방법을 소개하겠습니다. 






  (1) NumPy array 행렬을 SciPy 압축 희소 행(CSR) 행렬로 변환하기

      (Transforming a NumPy matrix to a SciPy compressed sparse row matrix)


NumPy array 데이터형의 희소행렬을 SciPy 압축 희소 행(CSR) 행렬 (compressed sparse row matrix)로 만드는 3가지 방법을 아래의 arr 넘파이 배열을 예로 들어서 설명해보겠습니다. 


class scipy.sparse.csr_matrix(arg1, shape=None, dtype=None, copy=False)


(1-1) arr 넘파이 배열에 대해 => scipy.sparse.csr_matrix(arr)

(1-2) 값 data, '0'이 아닌 원소의 열 위치 indices, 행 위치 시작 indptr 

        => csr_matrix((data, indices, indptr), shape=(5, 4))

(1-3) 값 data, '0'이 아닌 원소의 (행, 열) 위치 => csr_matrix((data, (row, col)), shape=(5, 4))




Compressed Sparse Row matrix로 변환할 대상이 되는 NumPy array 예제 행렬인 'arr' 을 먼저 만들어보겠습니다. 



import numpy as np

from scipy.sparse import csr_matrix


arr = np.array([[0, 1, 0, 2], 

                [0, 3, 4, 5], 

                [0, 0, 0, 0], 

                [6, 0, 0, 7], 

                [0, 8, 0, 0]])


arr

[Out]
array([[0, 1, 0, 2],
       [0, 3, 4, 5],
       [0, 0, 0, 0],
       [6, 0, 0, 7],
       [0, 8, 0, 0]])



(1-1) arr 넘파이 배열에 대해 => scipy.sparse.csr_matrix(arr)


NumPy 배열 (rank-2 ndarray), 희소행렬, 밀집행렬을 scipy.sparse.csr)matrix() 메소드 안에 넣어주면 되니 제일 쉬운 방법입니다. 



# converting NumPy array into SciPy Compressed Sparse Row matrix

csr_mat = csr_matrix(arr)


csr_mat

[Out] <5x4 sparse matrix of type '<class 'numpy.longlong'>'
	with 8 stored elements in Compressed Sparse Row format>

 



위에서 만든 'csr_mat' 이름의 5x4 sparse matrix (CSR format) 에서 특성값(attributes)으로서 

  - (a) csr_mat.indptr : 행렬의 '0'이 아닌 원소의 행의 시작 위치

  - (b) csr_mat.indices : 행렬의 '0'이 아닌 원소의 열의 위치

  - (c) csr_mat.data : 행렬의 '0'이 아닌 원소 값



print('-- Compressed Sparse Row matrix --')

print('indptr:', csr_mat.indptr)

print('indices:', csr_mat.indices)

print('data:', csr_mat.data)


-- Compressed Sparse Row matrix --
indptr: [0 2 5 5 7 8]
indices: [1 3 1 2 3 0 3 1]
data: [1 2 3 4 5 6 7 8]

 



이를 그림으로 좀더 알기 쉽게 표현을 해보면 아래와 같습니다. 헷갈리지 않고 좀더 알아보기에 편리하도록 NumPy array 행렬의 값(data)을 숫자가 아니라 영어 알파벳으로 바꾸어서 표시하였습니다. 



SciPy Compressed Sparse Row matrix 에서 

  - data 는 행렬의 '0'이 아닌 원소 값이므로 이해하기 어려운게 없습니다. 

  - indices 도 행렬의 '0'이 아닌 원소의 위치 (row, column) 에서 열(column) 위치(index) 배열 [1, 3, 1, 2, 3, 0, 3, 1 ] 이므로 어려울게 없습니다. 

  - indptr 은 저는 처음에 봤을 때는 이게 뭔가하고 유심히 보면서 좀 고민을 했습니다. ^^;  indptr은 행을 기준으로 했을 때 행별로 '0'이 아닌 원소가 처음 시작하는 위치의 배열입니다. 말로 설명하기 좀 어려운데요, 가령 위의 NumPy 배열 'arr'의 '0'이 아닌 원소의 위치 (행 row, 열 col) 배열(위 그림의 중간에 표시되어 있음)을 보면, 

'arr' 배열의 첫번째 행 [0, a, 0, b] 는 '0'이 아닌 원소의 (row, col) 배열0 위치에서 시작, 

               두번째 행 [0, c, d, e] 는 '0'이 아닌 원소의 (row, col) 배열의 2 위치에서 시작, 

               세번째 행 [0, 0, 0, 0] 는 '0'이 아닌 원소의 (row, col) 배열의 5 위치에서 시작, (비어있음) 

               네번째 행 [f, 0, 0, g] 는 '0'이 아닌 원소의 (row, col) 배열의 5 위치에서 시작, 

                        (--> 왜냐하면, 세번째 행의 모든 값이 '0' 이므로 같은 위치인 5에서 시작함)

               다섯번째 행 [0, h, 0, 0] 는 '0'이 아닌 원소의 (row, col) 배열의 7 위치에서 시작, 

               마지막으로, 'arr' 의 원소의 개수 8 에서 끝남.  


이렇게 indptr을 이용하는 이유는 행 기준의 '0'이 아닌 원소의 (row, col) 을 사용하는 것보다 데이터를 좀더 압축할 수 (즉, 줄일 수) 있기 때문입니다. 위의 예의 경우 row 기준으로 '0'이 아닌 원소의 (row, col)에서 row만 보면 [0, 0, 1, 1, 1, 3, 3, 4] 로서 [0, 0], [1, 1, 1], [3, 3] 처럼 같은 행에 두 개 이상의 '0'이 아닌 원소가 있으면 같은 행 숫자가 반복됩니다. 이럴 때 indptr 을 사용하면 [0, 2, 5, 5, 7, 8] 처럼 행 기준으로 '0'이 아닌 원소가 시작되는 row 위치만 가져오면 되므로 저장해야하는 정보량을 줄일 수 (압축) 있게 됩니다.   



(1-2) 값 data, '0'이 아닌 원소의 열 위치 indices, 행 위치 시작 indptr 

        => csr_matrix((data, indices, indptr), shape=(5, 4))


NumPy array 행렬이 없더라도, data, indices, indptr 입력값과 output 행렬의 형상(shape) 을 알고 있다면 SciPy Compressed Sparse Row matrix를 아래처럼 만들 수 있습니다.

(다만, indptr, indices 를 사람이 직접 입력하기에는 좀 어려운 면이 있어서 위의 (1-1) 방법보다는 좀 어려워보이네요.)



# converting NumPy array into SciPy Compressed Sparse Row matrix

indptr = np.array([0, 2, 5, 5, 7, 8]) # the location of the first element of the row.

indices = np.array([1, 3, 1, 2, 3, 0, 3, 1]) # column indices

data = np.array([1, 2, 3, 4, 5, 6, 7, 8])    # corresponding value


csr_mat2 = csr_matrix((data, indices, indptr), shape=(5, 4))

csr_mat2

[Out] <5x4 sparse matrix of type '<class 'numpy.int64'>'
	with 8 stored elements in Compressed Sparse Row format>



print('-- Compressed Sparse Row matrix 2 --')

print('indptr:', csr_mat2.indptr)

print('indices:', csr_mat2.indices)

print('data:', csr_mat2.data)


-- Compressed Sparse Row matrix 2 --
indptr: [0 2 5 5 7 8]
indices: [1 3 1 2 3 0 3 1]
data: [1 2 3 4 5 6 7 8]

 




(1-3) 값 data, '0'이 아닌 원소의 (행, 열) => csr_matrix((data, (row, col)), shape=(5, 4))


세번째는 행렬에서 '0' 이 아닌 원소의 값(data)과 (행, 열) 위치 (row_ind, col_ind), 그리고 행렬의 형상(shape) 을 입력해주는 방식입니다. (사람 입장에서는 이 (1-3) 방식이 위의 (1-2) 방식보다는 직관적으로 이해하기가 더 쉽기는 합니다.)



# converting NumPy array into SciPy Compressed Sparse Row matrix

row = np.array([0, 0, 1, 1, 1, 3, 3, 4])

col = np.array([1, 3, 1, 2, 3, 0, 3, 1])

data = np.array([1, 2, 3, 4, 5, 6, 7, 8])


csr_mat3 = csr_matrix((data, (row, col)), shape=(5, 4))

csr_mat3

[Out] <5x4 sparse matrix of type '<class 'numpy.longlong'>'
	with 8 stored elements in Compressed Sparse Row format>

 

print('-- Compressed Sparse Row matrix 3 --')

print('indptr:', csr_mat3.indptr)

print('indices:', csr_mat3.indices)

print('data:', csr_mat3.data)


-- Compressed Sparse Row matrix 2 --
indptr: [0 2 4 4 6 7]
indices: [1 3 1 2 0 3 1]
data: [1 2 3 4 5 6 7]


-- Compressed Sparse Row matrix 3 --
indptr: [0 2 5 5 7 8]
indices: [1 3 1 2 3 0 3 1]
data: [1 2 3 4 5 6 7 8]





  (2) SciPy 압축 희소 행(CSR) 행렬을 NumPy 행렬로 변환하기

     (Transforming a SciPy compressed sparse row matrix into a NumPy matrix) 


SciPy 압축 희소 행 행렬을 NumPy 행렬로 변환하기는 아래 2가지 메소드를 이용하면 매우 쉽습니다. 


(2-1) scipy.sparse.csr_matrix.toarray() 메소드

(2-2) scipy.sparse.csr_matrix.todense() 메소드



위에서 만든 'csr_mat', 'csr_mat2', 'csr_mat3' 세 개의 압축 희소 행(CSR) 행렬을 아래에서 원래의 NumPy array 배열로 변환해보니 모두 동일하게 제대로 변환이 되었네요. 


(2-1) scipy.sparse.csr_matrix.toarray() 메소드



# converting sparse matrix to NumPy array

csr_mat.toarray()

[Out]
array([[0, 1, 0, 2],
       [0, 3, 4, 5],
       [0, 0, 0, 0],
       [6, 0, 0, 7],
       [0, 8, 0, 0]], dtype=int64)


csr_mat2.toarray()

[Out]
array([[0, 1, 0, 2],
       [0, 3, 4, 5],
       [0, 0, 0, 0],
       [6, 0, 0, 7],
       [0, 8, 0, 0]], dtype=int64)


csr_mat3.toarray()

[Out]
array([[0, 1, 0, 2],
       [0, 3, 4, 5],
       [0, 0, 0, 0],
       [6, 0, 0, 7],
       [0, 8, 0, 0]], dtype=int64)





(2-2) scipy.sparse.csr_matrix.todense() 메소드


SciPy Compressed Sparse Row matrix를 원래의 행렬로 변환할 때 그게 희소행렬(Sparse matrix) 일 수도 있고 아니면 밀집행렬(Dense matrix) 일 수도 있기 때문에 메소드 이름을 csr_matrix.todense() 라고 하면 좀 오해의 소지도 있어서 썩 잘 지은 메소드 이름은 아니라고 생각하는데요, 어쨌든 반환된 후의 결과는 위의 csr_matrix.toarray() 와 동일합니다. 



csr_mat.todense()

[Out]
array([[0, 1, 0, 2],
       [0, 3, 4, 5],
       [0, 0, 0, 0],
       [6, 0, 0, 7],
       [0, 8, 0, 0]], dtype=int64)

 




  (3) 동일 위치에 중복된 원소값은 합산 (Duplicate entries are summed together.)


아래의 행렬처럼 (row, column) 이 (0, 0)인 위치에 5, 3 의 값이 중복되어 있고, (1, 1)인 위치에 2, 4 의 값이 중복되어 있는 Compressed Sparse Row matrix 데이터는 중복된 위치의 값을 더해주게 됩니다. 


  5 + 3

 0

 0

 0

 2 + 4

 0

 0

 0

 0



# Duplicate entries are summed together. 

row = np.array([0, 1, 1, 0])

col = np.array([0, 1, 1, 0])

data = np.array([5, 2, 4, 3])

csr_matrix((data, (row, col)), shape=(3, 3)).toarray()


[Out]
array([[8, 0, 0],
       [0, 6, 0],
       [0, 0, 0]], dtype=int64)

 



[ Reference ]

* SciPy 모듈 sparse.csr_matrix() 메소드

  : https://docs.scipy.org/doc/scipy/reference/generated/scipy.sparse.csr_matrix.html

* Sparse matrix: https://en.wikipedia.org/wiki/Sparse_matrix



많은 도움이 되었기를 바랍니다. 

이번 포스팅이 도움이 되었다면 아래의 '공감~'를 꾹 눌러주세요. 



728x90
반응형
Posted by Rfriend
,

이번 포스팅에서는 값 배열에 가중치 배열을 곱해서 합한 가중합(weighted sum)을 구하는 3가지 방법을 소개하겠습니다. 


a 를 가중치, b 를 값 배열이라고 했을 때, 


(1) 내적을 이용한 가중합 계산: np.dot(a, b) or np.matmul(a, b)

(2) 브로드캐스팅(broadcasting)을 이용하여 가중치와 값을 원소끼리 곱한 후 합하는

     np.sum(a.reshape(5, 1) * b, axis=0)

(3) repeat()로 가중치를 값 배열 1축만큼 반복 생성한 후, 가중치와 값의 원소끼리 곱한 후 합하는, 

     np.sum(a.reshape(5, 1).repeat(3, axis=1) * b, axis=0)




먼저, numpy를 import하고, 예제로 사용할 가중치 배열 a와, 값의 행렬 b를 만들어보겠습니다. 



import numpy as np


# weights

a = np.array([0.5, 0.3, 0.1, 0.08, 0.02])


print('a shape:', a.shape)

a shape: (5,)


print(a)

[0.5  0.3  0.1  0.08 0.02]



# values

b = np.arange(15).reshape(5, 3)


print('b shape:', b.shape)

b shape: (5, 3)


print(b)

[[ 0  1  2]

 [ 3  4  5]

 [ 6  7  8]

 [ 9 10 11]

 [12 13 14]]

 




  (1) 내적을 이용한 가중합 계산: np.dot(a, b) 또는 np.matmul(a, b)


가장 편리한 방법은 np.dot() 또는 np.matmul() 메소드를 사용하여 내적(inner prodct, dot product)을 계산하는 것입니다. 이때 가중치 벡터 a 에 대해서는 형태 변환(reshape)을 할 필요가 없이 그대로 사용할 수 있습니다.  



np.dot(a, b)

Out[2]: array([2.46, 3.46, 4.46])


np.matmul(a, b)

Out[3]: array([2.46, 3.46, 4.46])

 




  (2) Broadcasting을 이용하여 가중치와 값을 원소끼리 곱한 후, axis=0으로 합하기


이번에는 위의 (1) 내적을 계산의 각 단계별로 분리해서 순서대로 해보겠습니다. 가중치 a와 값 b의 원소끼리 곱한 후에, axis=0을 기준으로 합할 것입니다. 


먼저, 가중치 a와 값 b를 원소끼리 곱하기 위해 가중치 a의 형태(shape)를 기존의 (5,)에서 a.reshape(5, 1) 을 적용하여 (5, 1) 의 형태로 변환을 해줍니다. 값이 들어있는 배열 b의 형태는 (5, 3) 이므로 가중치 배열 a의 (5, 1) 형태를 값 배열 b에 곱해주면 ==> 서로 형태가 같지 않으므로 numpy 는 가중치 a 배열 (5, 1) 을 (5, 3)으로 자동으로 형태 변환을 시켜서 값 배열 b 의 (5, 3) 형태와 동일하게 맞추어 주어 원소간 곱을 해줍니다. 이러한 기능을 브로드캐스팅(boradcasting) 이라고 합니다. 



# shape of a_rs and b are different

a_rs = a.reshape(5, 1)

print(a_rs.shape)

print(a_rs)

(5, 1)


print(b.shape)

(5, 3)


# multiply using boradcasting of a_rs

a_rs_b_mult = a_rs * b


print(a_rs_b_mult.shape)

(5, 3)


print(a_rs_b_mult)

[[0.   0.5  1.  ]

 [0.9  1.2  1.5 ]

 [0.6  0.7  0.8 ]

 [0.72 0.8  0.88]

 [0.24 0.26 0.28]]



# weighted sum

np.sum(a_rs_b_mult, axis=0)

Out[9]: array([2.46, 3.46, 4.46])



* numpy 배열들의 다른 차원의 배열 간 산술연산 시 Broadcasting 은 아래 포스팅을 참고하세요. 

https://rfriend.tistory.com/287




  (3) repeat()로 가중치를 반복 생성한 후, 가중치와 값을 원소끼리 곱한 후 합하기


위의 (2)번에서는 가중치 배열 a의 형태를 바꾼 후의 a_rs 배열과 값 b 배열을 곱할 때, 사람 눈에는 보이지않게 numpy가 알아서 자동으로 가중치 a_rs 배열 (5, 1) 형태를 브로드캐스팅(broadcasting)을 해주어서 (5, 3) 형태로 만들어서 원소끼리 곱해주었습니다. 




반면에, 이번 (3)번에서는 사람이 repeat(n, axis) 메소드를 사용해서 명시적으로 배열을 n번 만큼 axis 축을 기준으로 반복해주어서 (2)번의 브로드캐스팅의 역할을 수행해주는 것입니다. 


구현 관점에서 보면 브로드케스팅이 편리한 장점이 있고, 반면에 repeat() 메소드로 명시적으로 기입을 해주면 코딩하는 사람이 이해하기 쉬운 장점이 있습니다. 



# match the shape of a and b by repeatition 

a_rs_rp = a.reshape(5, 1).repeat(3, axis=1)


print(a_rs_rp.shape)

(5, 3)


print(a_rs_rp)

[[0.5  0.5  0.5 ]

 [0.3  0.3  0.3 ]

 [0.1  0.1  0.1 ]

 [0.08 0.08 0.08]

 [0.02 0.02 0.02]]



# multiplication of a_rs_rp and b per each elements

a_rs_rp_b_mult = a_rs_rp * b


print(a_rs_rp_b_mult.shape)

(5, 3)


print(a_rs_rp_b_mult)

[[0.   0.5  1.  ]

 [0.9  1.2  1.5 ]

 [0.6  0.7  0.8 ]

 [0.72 0.8  0.88]

 [0.24 0.26 0.28]]



# weighted sum

np.sum(a_rs_rp_b_mult, axis=0)

Out[17]: array([2.46, 3.46, 4.46])

 



많은 도움이 되었기를 바랍니다. 

이번 포스팅이 도움이 되었다면 아래의 '공감~'를 꾹 눌러주세요. 



728x90
반응형
Posted by Rfriend
,

이번 포스팅에서는 Python numpy 모듈의 random.choice() 메소드를 사용하여 임의(무작위, 확률) 추출 (random sampling)하는 방법을 소개하겠습니다. 

 

numpy.random.choice() 메소드의 기본 Syntax는 아래와 같습니다. 각 parameter별로 예를 들어서 설명을 해보겠습니다. 

 

numpy.random.choice(a, size=None, replace=True, p=None)

 

이때 표본을 추출할 모집단에 해당하는 a 는 1-D array-like 또는 np.arange(n) 의 정수가 됩니다. 

 

 

 

 

  (1) 1부터 5까지의 정수 모집단에서 3개의 균등확률표본을 복원추출 하기
  (Generate a uniform random sample from np.arange(5) of size 3)

 

표본이 뽑힐 확률 p를 명시적으로 지정해주지 않으면 모든 원소가 뽑힐 확률이 동일한 (즉, p=1/N) 균등확률분포를 가정하고 표본이 추출됩니다. 

그리고, 복원추출(replacement)이 기본 설정값이므로 똑같은 값이 2번 이상 표본으로 뽑힐 수도 있습니다. 

 



import numpy as np
 
np.random.choice(5, 3) # with replacement
Out[3]: array([4, 0, 0])
 

 

참고로, 위의 np.random.choice(5, 3) 코드는 np.random.randint(0,5,3) 과 동일합니다. 

 

 

균등확률분포로 부터 임의 추출이므로 매번 표본으로 뽑히는 값이 바뀌게 됩니다. 위와 코드는 같지만 추출된 표본은 다르지요?

 



np.random.choice(5, 3) # sampled with different values
Out[4]: array([2, 0, 0])
 

 

 

복원추출을 하게 되면 1~5 의 정수를 가지는 모집단에서 5개를 초과하는 표본을 뽑는데 문제가 없습니다. 

 



np.random.choice(5, 10) # with replacement
Out[5]: array([2, 2, 4, 2, 4, 0, 0, 4, 3, 2])
 

 

 

 

 (2) 1~5의 정수 모집단에서 비균등 확률 p 로 3개 원소 임의 표본 복원추출 하기
    (Generate a non-uniform random sample from np.arange(5) of size 3:)

 

만약 모집단 내 각 원소별로 표본으로 뽑힐 확률 p를 알고 있다면, 혹은 명시적으로 지정을 해주고 싶다면 모수 p에 표본이 추출될 확률을 입력해주면 됩니다. 

 



p = [0.1, 0, 0.3, 0.6, 0]
np.random.choice(5, 3, p=p)
Out[7]: array([2, 3, 3])
 

 

 

만약 표본을 추출할 모집단(a)의 원소 개수(n)과 각 원소별 표본이 뽑힐 확률(p)의 원소 개수가 서로 다를 경우 ValueError 가 발생합니다. (아래 예에서는 확률 p에는 3개가, 모집단 a는 5개 원소로서 서로 다름)

 



# ValueError: 'a' and 'p' must have same size
p = [0.1, 0, 0.3]           # size 3
np.random.choice(5, 3, p=p) # size 5


ValueError: 'a' and 'p' must have same size
 

 

 

만약 모집단 원소별 표본으로 뽑힐 확률 p의 전체 합이 1 이 아니거나 0~1사이 값이 아닌 경우도 ValueError가 발생합니다. 왜냐하면, 확률의 정의 상 (1) 각 사건별 확률의 전체 합은 1이고, (2) 각 사건별 확률은 0~1 사이의 실수를 가져야 하기 때문입니다. 

 



# ValueError: probabilities do not sum to 1
p = [0.4, 0, 0.3, 0.6, 0]   # sum to 1.3 (not 1)
np.random.choice(5, 3, p=p)


ValueError: probabilities do not sum to 1




# ValueError: probabilities are not non-negative
p = [-0.4, 0, 0.3, 1.6, 0]   
np.random.choice(5, 3, p=p)


ValueError: probabilities are not non-negative

 

 

 

  (3) 1~5 정수 모집단에서 3개의 균등확률표본을 비복원추출(non-replacement) 하기
  (Generate a uniform random sample from np.arange(5) of size 3 
   without replacement)

 

옵션 중에서 replace=False 로 설정을 해주면 비복원추출(without replacement)을 합니다. 즉, 모집단에서 표본을 추출할 때 각 원소를 딱 한번만 추출하기 때문에 동일한 원소가 2번 이상 뽑히는 일은 없습니다. (default 옵션은 replace=True 임)

 



# Generate a uniform random sample from np.arange(5) of size 3 without replacement:
np.random.choice(5, 3, replace=False)
Out[11]: array([3, 2, 1])
 

 

참고로 위의 np.random.choice(5, 3, replace=False) 코드는 np.random.permutation(np.arange(5))[:3] 과 동일합니다. 

 

 

비복원추출을 할 때는 한가지 조심해야 할 것이 있는데요, 모집단의 원소 개수보다 많은 수의 샘플을 비복원추출(replace=False)하려고 하면 ValueError가 발생합니다. (아래 예처럼, 5개의 원소를 가진 모집단에서 10개 표본을 비복원(즉, 중복 없이) 추출할 수는 없겠지요!)

 



# ValueError: Cannot take a larger sample than population when 'replace=False'
np.random.choice(5, 10, replace=False)


ValueError: Cannot take a larger sample than population when 'replace=False'
 

 

 

위의 (2)번에서 표본추출확률 p 를 설정하는 것과, 이번 (3)번의 비복원추출(replace=False)을 함께 설정해줌으로써 비균등확률(non-uniform random sample)로 비복원추출(without replacement) 샘플링도 물론 가능합니다. 

 



# Generate a non-uniform random sample from np.arange(5) of size 3 without replacement:
p = [0.1, 0, 0.3, 0.6, 0]
np.random.choice(5, 3, replace=False, p=p)
Out[13]: array([3, 0, 2])
 

 

 

 

  (4) 정수 대신에 임의의 배열처럼 생긴 객체의 원소를 확률추출하기
  (Any of the above can be repeated with an arbitrary array-like instead of just integers.)

 

np.random.choice(a, size=None, replace=True, p=None) 의 syntax에서 a 부분에 정수 배열 (np.arange(n)) 말고도 1-D array-like 객체를 대상으로 할 수도 있습니다. 아래 예에서는 과일 이름을 원소로 가지는 리스트로부터 비복원 비균등 확률 표본추출을 해보았습니다. 

 



# Any of the above can be repeated with an arbitrary array-like instead of just integers.
fruits = ['apple', 'banana', 'cherries', 'durian', 'grapes', 'lemon', 'mango']
p = [0.1, 0, 0.2, 0.5, 0.1, 0.05, 0.05]
np.random.choice(fruits, 3, p=p, replace=False)
Out[14]: array(['cherries', 'lemon', 'durian'], dtype='<U8')
 

 

 

 

  (5) 초기값 설정을 통한 재현가능성 확보
  (setting seed number for reproducibility)

 

np.random.seed() 로 초기값(seed value)을 설정해주면 매번 똑같은 확률표본을 추출할 수 있습니다. 만약 재현가능성(reproducibility)이 필요한 경우라면 초기값을 설정해주세요. 

 



# set seed number for reproducibility
np.random.seed(1004)
np.random.choice(5, 3)
Out[15]: array([2, 3, 3])


np.random.seed(1004)
np.random.choice(5, 3)
Out[16]: array([2, 3, 3])

 

 

참고로, 기계학습을 할 때 train set, test set 을 무작위로 분할하는 여러가지 방법은 https://rfriend.tistory.com/519 를 참고하시기 바랍니다. 

 

많은 도움이 되었기를 바랍니다. 

이번 포스팅이 도움이 되었다면 아래의 '공감~

'를 꾹 눌러주세요. :-)

 

* Reference: https://docs.scipy.org/doc//numpy-1.10.4/reference/generated/numpy.random.choice.html

 

 

728x90
반응형
Posted by Rfriend
,

이번 포스팅에서는 A, B 두개의 값이 들어있는 배열과 위치를 지정해둔 idx 배열이 있다고 했을 때, A배열의 idx 배열 내 index 위치에다가 B배열의 원소를 순서대로 더하기하는 두가지 방법을 소개하겠습니다. 


(방법 1) for loop 으로 A배열의 idx 위치에 B배열의 원소 순서대로 더하기

(방법 2) np.add.at(A, idx, B) 메소드로 A배열의 idx 위치에 B배열의 원소 순서대로 더하기


for loop을 이용한 (방법 1)은 numpy의 add.at() 함수를 몰라도 어렵지 않게 구현할 수 있고 또 코드를 해석하기도 편한 장점이 있습니다만, 만약 더해야 하는 원소의 개수가 많아지면 for loop 연산을 하는데 시간이 많이 소요되는 단점이 있습니다. 


numpy의 add.at() 메소드를 사용하는 (방법 2)는 Vectorization 연산을 하여 한꺼번에 두 배열 간 idx 위치에 맞게 더하기 연산을 수행하므로 위의 for loop 방법 1 대비 빠르다는 장점이 있습니다. (더해야 하는 원소 개수가 작으면 방법1과 방법2의 속도 차이를 느끼기 힘든데요, 만약 몇 몇 백만개라면 for loop으로 수 분~ 수 시간 걸릴 것이 numpy.add.at() 메소드로는 수 초안에 끝낼 수도 있을만큼 성능 차이가 많이 날 것입니다. (대신 numpy.add.at() 함수를 기억하고 사용법도 알고 있어야 하겠지만요.)





  (방법 1) for loop 으로 A배열의 idx 위치에 B배열의 원소 순서대로 더하기


먼저, 예제로 사용할 A배열, B배열, idx 배열을 만들어보겠습니다. 



import numpy as np


A = np.arange(24).reshape(8, 3)

print(A)

[[ 0  1  2]
 [ 3  4  5]
 [ 6  7  8]
 [ 9 10 11]
 [12 13 14]
 [15 16 17]
 [18 19 20]
 [21 22 23]]


B = np.arange(12).reshape(4, 3)

print(B)

[[ 0  1  2]
 [ 3  4  5]
 [ 6  7  8]
 [ 9 10 11]]


idx = np.array([0, 0, 3, 5])




자, 이제 준비가 되었으니 for loop 문과 enumerate() 함수를 사용하여 A배열의 idx 배열 내 숫자의 위치에다가 B배열의 원소를 순서대로 더해보겠습니다. 

idx 가 array([0, 0, 2, 5]) 이므로 A배열의 1차원의 idx([0, 0, 3, 5]) 번째 위치(0, 0, 3, 5)의 원소에 B배열의 0, 1, 2, 3 번째 위치의 원소를 더하는 연산을 수행하게 됩니다. 


말로만 설명을 들으면 좀 헷갈리고 이해가 잘 안갈수도 있는데요, 아래에 배열 덧셈 연산식과 연산이 끝난 A배열에서 결과값이 바뀐 0, 2, 5 번째 원소를 빨간색으로 표시해두었으니 참고하기 바랍니다. (* idx([0, 0, 3, 5]) 에서 A[0]에 B[0]과 B[1]이 각 각 더해지는 것에 주의)


array A at idx([0])  + array B[0]          = array A[0]

array([0, 1, 2])        + array([0, 1, 2])    = array([0, 2, 4])


array A at idx([0])  + array B[1         = array A[0]

array([0, 2, 4])      + array([3, 4, 5])     = array([3, 6, 9])


array A at idx([3])  + array B[2]          = array A[3]

array([9, 10, 11])     + array([6, 7, 8])    = array([15, 17, 19])


array A at idx([5])  + array B[3]          = array A[5]

array([15, 16, 17])   + array([9, 10, 11])  = array([24, 25, 26])



for i, id in enumerate(idx):

    A[id] += B[i]


print(A)

[[ 3  6  9]
 [ 3  4  5]
 [ 6  7  8]
 [15 17 19]
 [12 13 14]
 [24 26 28]
 [18 19 20]
 [21 22 23]]

 




  (방법 2) np.add.at(A, idx, B) 함수로 A배열의 idx 위치에 B배열의 원소 순서대로 더하기


위의 for loop을 이용한 (방법 1)과 똑같은 연산을 numpy의 add.at(A, idx, B) 메소드를 이용해서 수행하면 아래와 같습니다. 코드도 깔끔하고 연산속도도 더 빠르므로 특히 대용량 배열을 다루어야 하는 경우라면 알아두면 좋겠습니다. 



import numpy as np


A = np.arange(24).reshape(8, 3)

B = np.arange(12).reshape(4, 3)

idx = np.array([0, 0, 3, 5])


np.add.at(A, idx, B)


print(A)

[[ 3  6  9]
 [ 3  4  5]
 [ 6  7  8]
 [15 17 19]
 [12 13 14]
 [24 26 28]
 [18 19 20]
 [21 22 23]]

 



많은 도움이 되었기를 바랍니다. 

이번 포스팅이 도움이 되었다면 아래의 '공감~'를 꾹 눌러주세요. ^_^


728x90
반응형
Posted by Rfriend
,

이번 포스팅에서는 Python Numpy 배열 (array)에 차원을 추가하는 3가지 방법을 소개하겠습니다. 딥러닝 공부하다 보면 computer vision의 CNN에서 이미지 파일을 불러와서 다차원 배열로 변환할 때 사용하곤 합니다. 

1. numpy.reshape() 을 이용한 차원 추가

2. numpy.expand_dims() 을 이용한 차원 추가

3. numpy.newaxis 을 이용한 차원 추가


예제로 사용할 간단한 (4, 3, 2) 3차원의 다차원 배열을 만들어보겠습니다. 


import numpy as np

a = np.arange(24).reshape(4, 3, 2)

a

array([[[ 0,  1],
        [ 2,  3],
        [ 4,  5]],

       [[ 6,  7],
        [ 8,  9],
        [10, 11]],

       [[12, 13],
        [14, 15],
        [16, 17]],

       [[18, 19],
        [20, 21],
        [22, 23]]])

a.shape

(4, 3, 2)




(4, 3, 2) 차원의 배열 a에 차원을 추가하여 (1, 4, 3, 2)의 4차원 배열로 만들어보겠습니다. 


  1. numpy.reshape() 를 이용한 차원 추가


 

np.reshape(a, (1, 4, 3, 2))

array([[[[ 0,  1],
         [ 2,  3],
         [ 4,  5]],

        [[ 6,  7],
         [ 8,  9],
         [10, 11]],

        [[12, 13],
         [14, 15],
         [16, 17]],

        [[18, 19],
         [20, 21],
         [22, 23]]]])


 np.reshape(a, ((1,) + a.shape))

array([[[[ 0,  1],
         [ 2,  3],
         [ 4,  5]],

        [[ 6,  7],
         [ 8,  9],
         [10, 11]],

        [[12, 13],
         [14, 15],
         [16, 17]],

        [[18, 19],
         [20, 21],
         [22, 23]]]])

 a.reshape((1,) + a.shape)

array([[[[ 0,  1],
         [ 2,  3],
         [ 4,  5]],

        [[ 6,  7],
         [ 8,  9],
         [10, 11]],

        [[12, 13],
         [14, 15],
         [16, 17]],

        [[18, 19],
         [20, 21],
         [22, 23]]]])



  2. numpy.expand_dims() 를 이용한 차원 추가



np.expand_dims(a, axis=0)

array([[[[ 0,  1],
         [ 2,  3],
         [ 4,  5]],

        [[ 6,  7],
         [ 8,  9],
         [10, 11]],

        [[12, 13],
         [14, 15],
         [16, 17]],

        [[18, 19],
         [20, 21],
         [22, 23]]]])

 



  3. numpy.newaxis 를 이용한 차원 추가



a[:, np.newaxis]

array([[[[ 0,  1],
         [ 2,  3],
         [ 4,  5]]],


       [[[ 6,  7],
         [ 8,  9],
         [10, 11]]],


       [[[12, 13],
         [14, 15],
         [16, 17]]],


       [[[18, 19],
         [20, 21],
         [22, 23]]]])

 


많은 도움이 되었기를 바랍니다. 

이번 포스팅이 도움이 되었다면 아래의 '공감~'를 꾹 눌러주세요. :-)

728x90
반응형
Posted by Rfriend
,