본문 바로가기

Machine Learning_모델설계_Python

Numpy_1 개념 및 배열 객체 다루기

안녕하세요 배도리입니다. 앞으로 총 6개에 걸쳐 넘파이랑 판다스에 대해서 설명드리겠습니다. 저는 처음 넘파이와 판다스를 접했을 때 설렜습니다. 제가 적은 코드로 결과물이 나온다는것에 재밌었고 까만 화면에 컴퓨터로 뚝딱뚝딱 하니까 뭔가 있어보였습니다. 그래서 회사를 퇴사하고 주변 친구들이 "요즘 무슨 공부해?" 라고 물어볼때 저는 "아 나 요즘 넘파이랑 판다스 공부하고있어 ㅎㅎ" 라고 답하면서 어깨가 들썩들썩 거렸던 기억이 납니다. 지금에서 보면 정말 기본중에 기본인데 뭘 그리 우쭐됐을까 싶습니다. 하지만 기본중에 기본이니 만큼 넘파이와 판다스가 익숙하고 능숙해야 앞으로 배울 방법론과 데이터 전처리에 큰 도움이 된다고 생각합니다. 능숙하고 익숙해지기 위해선 저와 같은 범인들은 그저 꾸준히 해야겟죠? 그럼 이제 부터 넘파이에 대해 배워보겠습니다. 

 

넘파이의 개념 및 특징

▪ 넘파이의 개념

-Python의 고성능 과학 계산용 라이브러리입니다.

-벡터나 행렬 같은 선형대수의 표현법을 코드로 처리합니다. (선형대수의 지식은 필수 입니다! 이것과 관련해서 다음에 포스팅 하도록 해보겠습니다.)

-사실상의 표준 라이브러리입니다.

-다차원 리스트나 크기가 큰 데이터 처리에 유리합니다.

 

▪ 넘파이의 특징

-속도가 빠르고 메모리 사용이 효율적 데이터를 메모리에 할당하는 방식이 기존과 다릅니다.

-반복문을 사용하지 않습니다. 연산할 때 병렬로 처리하고  함수를 한 번에 많은 요소에 적용합니다.

다양한 선형대수 관련 함수 제공합니다.

C, C++, 포트란 등 다른 언어와 통합 사용 가능합니다.

 

넘파이의 배열

1. 넘파이 배열(ndarray)과 텐서(tensor)

▪ ndarray: 텐서 데이터를 다루는 객체입니다. 객체란 컴퓨터 프로그래밍에서 데이터와 해당 데이터를 조작하는 메서드(함수)들을 묶어놓은 개념입니다

▪ tensor: 선형대수의 데이터 배열(array) • 랭크(rank)에 따라 이름이 다릅니다.

아래의 표와 그림을 보시면 좀더 이해가기 쉽습니다.(스칼라 같은 경우 tensor of rank 0으로 이해하시면 됩니다.)

랭크(rank) 이름 예시
0 스칼라(scalar) 6
1 벡터(vector) [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
2 행렬(matrix) [[0, 1, 2, 3, 4, 5], [6, 7, 8, 9, 10, 11]
3 3차원 텐서(3-order tensor) [[[0, 1, 2], [3, 4, 5]], [[6, 7, 8], [9, 10, 11]]]
n N차원 텐서(n-order tensor)  

텐써

 

 

2. 배열(ndarray)의 메모리 구조

▪ 배열 생성 - np.array 함수를 사용하여 배열 생성합니다. 코드와 함께 하나씩 설명하겠습니다.

(앞으로 보통의 설명은 코드와 함께합니다. 최대한 상세하게 설명하겠습니다.)

우선 import numpy as np는 넘파이 라이브러리를 불러오고, np를 넘파이 객체로 사용할 수 있도록 설정하는 구문입니다.

import 키워드를 사용하여 넘파이 라이브러리를 가져옵니다. 그리고 as 키워드를 사용하여 가져온 넘파이 라이브러리를 np라는 이름으로 사용하겠다고 지정합니다.(np가 무슨 의미가 있는건 아니지만 보통 모두 이렇게 np라는 이름을씁니다.)

import numpy as np

• 매개변수 1: 배열 정보

• 매개변수 2: 넘파이 배열로 표현하려는 데이터 타입

▪ 리스트(list)와의 차이점은 넘파이 배열은 텐서의 구조에 맞춰 배열을 생성해야 함

– m x n 배열을 생성한다면, 이 행렬의 모든 엔트리(entry)는 데이터로 채워져야 함

– 리스트의 경우는 데이터를 일부 채우지 않아도 무리없이 작동함

import numpy as np 
test_array = np.array([1, 4, 5, 8], float)  #매개변수 1: [1,4,5,8], 매개변수 2:  float 
test_array
array([1., 4., 5., 8.]) 
import numpy as np 
test_list= [[1, 4, 5, 8],[1,2,3]] #배열이 맞지않음
test_array = np.array(test_list, float)
 
ValueError 

 

▪ 동적 타이핑(dynamic typing)을 지원하지 않음- 하나의 데이터 타입만 사용합니다. 쉽게 말하면개변수 2에  float 이라고 쓰는것처럼 변수의 타입을 명시적으로 선언한다고 생각하시면 됩니다

test_array = np.array([1, 4, 5, "8"], float) #문자로 줘도 실수형으로 반환
test_array
array([1., 4., 5., 8.])

 

▪ 데이터를 메모리에 연속적으로 나열 - 각 데이터 값에 대한 메모리 크기가 동일하고 검색이나 연산 속도가 리스트에 비해 훨씬 빠릅니다. 즉 동적 타이핑을 포기하는 대신, 값이 연속적으로 배열되어 있어 검색·연산이 리스트에 비해 훨씬 빠릅니다.

 

 

3. 넘파이 배열의 생성

▪ 배열 객체의 데이터 특징을 출력하는 요소(property)는 dtype과 shape

• dtype: 넘파이 배열의 데이터 타입을 반환합니다.

test_array.dtype
dtype('float64')

• shape: 넘파이 배열에서 객체(object)의 차원(dimension)에 대한 구성 정보를 반환합니다.

▪ 배열의 구조(shape)은 넘파이 배열 객체의 차원 구성이 어떠한지 나타내는 함수입니다.

• 배열의 구조는 튜플(tuple) 형태로 출력됨 • 랭크(rank)의 증가에 따라 조금씩 다르게 표현됨

랭크가 1일 때

test_array.shape
(4,)

 

랭크가 2일 때

test_array2 = np.array([[1, 4, 5, 8],[1, 4, 5, 8]], float) #문자로 줘도 실수형으로 반환
test_array2.shape
(2, 4)

 

매트릭스(랭크2)일 때 앞의 값은 행(row) 뒤에 값은열(column)

matrix = [[1,2,5,8], [1,2,5,8], [1,2,5,8]]
np.array(matrix, int).shape
(3, 4)

 

랭크가 3일경우

tensor_rank3 = [ 
    [[1, 2, 5, 8], [1, 2, 5, 8], [1, 2, 5, 8]],
    [[1, 2, 5, 8], [1, 2, 5, 8], [1, 2, 5, 8]],
    [[1, 2, 5, 8], [1, 2, 5, 8], [1, 2, 5, 8]],
    [[1, 2, 5, 8], [1, 2, 5, 8], [1, 2, 5, 8]]
]
np.array(tensor_rank3, int).shape
(4, 3, 4)

 

자 이제 랭크가 4입니다.

tensor_rank4 = [ #4차원 가보자
    [[[1, 2, 5, 8], [1, 2, 5, 8], [1, 2, 5, 8]],
    [[1, 2, 5, 8], [1, 2, 5, 8], [1, 2, 5, 8]],
    [[1, 2, 5, 8], [1, 2, 5, 8], [1, 2, 5, 8]],
    [[1, 2, 5, 8], [1, 2, 5, 8], [1, 2, 5, 8]]],
    [[[1, 2, 5, 8], [1, 2, 5, 8], [1, 2, 5, 8]],
    [[1, 2, 5, 8], [1, 2, 5, 8], [1, 2, 5, 8]],
    [[1, 2, 5, 8], [1, 2, 5, 8], [1, 2, 5, 8]],
    [[1, 2, 5, 8], [1, 2, 5, 8], [1, 2, 5, 8]]]
]
np.array(tensor_rank4, int).shape
(2, 4, 3, 4)

이걸 이해하시려면 앞에서 넣어놨던 그림을 공간적으로도 이해해도됩니다. 그런데 좀더 쉽게 이해하시려면 대괄호구분으로 이해하셔도 편합니다. 첫 대괄호 안에 숫자 4개가 있으니 (4,) 그 다음 괄호한에 같은것이 3개 있으니까 (3,4) 그다음 같은것이 4개 있으니까 (4,3,4) 마지막으로 2개가 있어서 (2,4,3,4) 저는 이렇게 이해하고 넘어갔습니다.

 

 

차원의 수를 바로 알고싶다면 ndim(the number of dimensions): 차원의 수를 np.array에 붙여줍시다.

np.array(tensor_rank3, int).ndim
3

 

▪ dtype - 매개변수 dtype으로 넘파이 배열의 데이터 타입 지정 -변수가 사용하는 메모리 크기가 정해집니다.

np.array( [ [1, 2, 3.5], [4, 5, 6.5]], dtype=int)
array([[1, 2, 3], [4, 5, 6]])
 
 
np.array( [ [1, 2, 3.5], [4, 5, 6.5]], dtype=float)
array([[1. , 2. , 3.5], [4. , 5. , 6.5]])

 

▪ itemsize 요소(property)로 넘파이 배열에서 각 요소가 차지하는 바이트(byte) 확인합니다.

- np.float64로 dtype을 선언하면 64비트, 즉 8바이트 차지 

- np.float32로 dtype을 선언하면 32비트, 즉 4바이트 차지

np.array( [[1, 2, 3.5,3,444], [4, 5, 6.5,4,4]], dtype=np.float64).itemsize
8
 
np.array( [[1, 2, 3.5], [4, 5, 6.5]], dtype=np.float32).itemsize
4

 

 

4. 배열의 구조 다루기

▪ rreshape 함수로 배열의 구조를 변경하고 랭크를 조절 - 반드시 전체 요소의 개수는 통일해야됩니다.

x = np.array([[1, 2, 5, 8], [1, 2, 5, 8]])
x.shape
(2, 4)
 
 
x.reshape(2,2) #엘리먼트수가 같은 상황에서만 가능
ValueError
 
 
x.reshape(4,2)
array([[1, 2], [5, 8], [1, 2], [5, 8]])

 

▪ reshape 함수로 배열의 구조를 변경하고 랭크를 조절

- -1의 사용법: 전체 요소의 개수는 고정시키고 1개를 제외한 나머지 차원의 크기를 지 정했을 때 전체 요소의 개수를 고려하여 마지막 차원이 자동으로 지정되는 기법입니다. 이거 아주 유용합니다.

x = np.array(range(8)).reshape(4,2) #range를 통해 배열을 만들수있습니다.
x
array([[0, 1], [2, 3], [4, 5], [6, 7]])
x.reshape(2,-1)
array([[0, 1, 2, 3], [4, 5, 6, 7]])
x.reshape(2,2,-1)
array([[[0, 1], [2, 3]], [[4, 5], [6, 7]]])
x.reshape(-1,)
array([0, 1, 2, 3, 4, 5, 6, 7])

 

▪ flatten 함수는 데이터 그대로 1차원으로 변 - 데이터의 개수는 그대로 존재하고 배열의 구조만 변경됩니다. 이것도 나중에 유용합니다.

x = np.array(range(8)).reshape(2,2,-1)
x
array([[[0, 1], [2, 3]], [[4, 5], [6, 7]]])
x.flatten()
array([0, 1, 2, 3, 4, 5, 6, 7])

 

 

5. 인덱싱과 슬라이싱

▪ 인덱싱(indexing): 리스트에 있는 값의 상대적인 주소(offset)로 값에 접근합니다. 넘파이 배열의 인덱스 표현에는 ‘,’을 지원합니다.( ‘[행][열]’ 또는 ‘[행,열]’ 형태)

- 3차원 텐서 이상은 shape에서 출력되는 랭크 순서대로 인덱싱에 접근합니다.

x = np.array([[1, 2, 3], [4.5, 5, 6]],int)
x
array([[1, 2, 3], [4, 5, 6]])
x[0,0] 1
x[0,2] 3
x[0, 1] = 100 #바꾸는것도 가능합니다.
x
array([[ 1, 100, 3], [ 4, 5, 6]])

 

그럼 3차원에서는 어떻게 할까요

tensor_rank3 = [ 
    [[0, 1], [2, 3]],
    [[4, 5], [6, 7]],
    [[9, 9], [10, 11]],
]
t_r3 = np.array(tensor_rank3, int)
t_r3.shape
 
(3, 2, 2)

 

11이라는 숫자를 뽑으려면 세번째의 두번째의 두번째니까 2,1,1 이렇게 합니다. 물론 파이썬은 0부터 시작입니다.

t_r3[2,1,1] 11

 

▪ 슬라이싱(slicing): 인덱스를 사용하여 리스트 일부를 잘라내어 반환 - 넘파이 배열은 행과 열을 나눠 각각 슬라이싱할 수 있습니다. 그림으로 한번 보겠습니다.

슬라이싱

전체 행의 2열이상 뽑아보겠습니다. (2: 의 의미는 3열부터 시작하겠다는 뜻입니다)

x = np.array([[1, 2, 3, 4, 5], [6, 7, 8, 9, 10]], int)
x[:,2:] 
 
array([[ 3, 4, 5], [ 8, 9, 10]])

 

1행의 1~2열을 뽑겠습니다.(1:3의 의미는 2열부터 3열까지 뽑는 의미입니다.)

x[1,1:3
 
array([7, 8])

 

▪ 증가값(step): 리스트에서 데이터의 요소를 호출할 때 데이터를 건너뛰면서 반환

- ‘[시작 인덱스:마지막 인덱스:증가값]’ 형태입니다. 이건 각 랭크에 있는 요소별로 모두 적용할 수 있습니다. 홀수반환 짝수반환 이럴때 자주 쓰입니다.

x = np.array(range(15), int).reshape(3, -1) #우선 새로 만들겠습니다.
x
 
array(
[[ 0, 1, 2, 3, 4],
[ 5, 6, 7, 8, 9],
[10, 11, 12, 13, 14]])

 

1::2의 의미는 2열부터 시작하고  2만큼 증가 시키겠다는 뜻입니다. 그러면 짝열만 나오겠죠?

x[:,1::2]
array(
[[ 1, 3],
[ 6, 8],
[11, 13]])

 

이제 행은 2만큼 증가시키고 열은 3만큼 증가시켜 뽑겠습니다. 둘다 시작은 첫행 첫열은 포함합니다.

x[::2,::3]
array(
[[ 0, 3],
[10, 13]])

 

이번에는 3차원으로 가보겠습니다.

tensor_rank3 = [
    [[0, 1], [2, 3]],
    [[4, 5], [6, 7]],
    [[8, 9], [10, 11]],
]
t_r3 = np.array(tensor_rank3, int)

 

여기서 [[4, 5], [6, 7]], 이걸 빼고 뽑고싶다면

t_r3[::2]
 
array([[[ 0, 1], [ 2, 3]], [[ 8, 9], [10, 11]]]

 

 

6. 배열 생성 함수

- arange

▪ range 함수와 같이 차례대로 값을 생성합니다.

▪ ‘(시작 인덱스, 마지막 인덱스, 증가값)’으로 구성합니다.

▪ range 함수와 달리 증가값에 실수형이 입력되어도 값을 생성할 수 있습니다.

▪ 소수점 값을 주기적으로 생성할 때 유용합니다.

▪ range()는 이터레이터를 반환하고, arange()는 배열을 반환합니다.(range()는 실제로 시퀀스를 생성하지않음)

np.arange(10) #시작인덱스, 마지막인덱스, 증가값으로 구성
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
np.arange(-5, 5)
array([-5, -4, -3, -2, -1, 0, 1, 2, 3, 4])
np.arange(0, 5,0.5)
array([0. , 0.5, 1. , 1.5, 2. , 2.5, 3. , 3.5, 4. , 4.5])

 

- ones, zeros

▪ ones 함수: 1로만 구성된 넘파이 배열을 생성합니다.

▪ zeros 함수: 0으로만 구성된 넘파이 배열을 생성합니다.

▪ 사전에 shape 값을 넣어서 원하는 크기의 넘파이 배열 생성합니다.

▪ 생성 시점에서 dtype을 지정해주면 해당 데이터 타입으로 배열 생성합니다.

np.ones(shape=(5,2), dtype=np.int8)
array([[1, 1], [1, 1], [1, 1], [1, 1], [1, 1]], dtype=int8)
np.zeros(shape=(2,2), dtype=np.float32)
array([[0., 0.], [0., 0.]], dtype=float32)

 

- empty

▪ empty 함수 : 활용 가능한 메모리 공간 확보하여 반환합니다.

• ones와 zeros는 먼저 shape의 크기만큼 메모리를 할당하고 그곳에 값을 채웁니다.

• 해당 메모리 공간에 값이 남았을 경우 그 값을 함께 반환합니다.

• empty는 메모리를 초기화하지 않아 생성될 때마다 다른 값 반환합니다.

▪ 생성 시점에서 dtype을 지정해주면 해당 데이터 타입으로 배열 생성합니다.

np.empty(shape=(2,4), dtype=np.float32) #
#배열의 요소는 초기화되지 않으므로 임의의 값이 들어갑니다.
array([[0. , 1.875 , 0. , 2.25 ], [0. , 2.3125, 0. , 2.5 ]], dtype=float32)

 

- ones_like, zeros_like, empty_like

▪ ones_like 함수: 기존 넘파이 배열과 같은 크기로 만들어 내용을 1로 채웁니다.

▪ zeros_like 함수: 기존 넘파이 배열과 같은 크기로 만들어 내용을 0으로 채웁니다.

▪ empty_like 함수: 기존 넘파이 배열과 같은 크기로 만들어 빈 상태로 만듭니다..

x = np.arange(12).reshape(3,4)
x
array([[ 0, 1, 2, 3], [ 4, 5, 6, 7], [ 8, 9, 10, 11]])
 
np.ones_like(x)
array([[1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1]])
 
np.zeros_like(x)
array([[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]])
 
np.empty_like(x)
array([[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]) #이거 빈상태입니다. zero랑 달라요

 

- identity, eye, diag

▪ identity 함수: 단위행렬(i행렬)을 생성합니다.

• 매개변수 n으로 n×n 단위행렬을 생성합니다.

np.identity(n=3, dtype=int) #단위 행령을 생성 i 행렬
 
array(
[[1, 0, 0],
[0, 1, 0],
[0, 0, 1]])

 

▪ eye 함수 : 시작점과 행렬 크기를 지정, 단위행렬 생성합니다.

• N은 행의 개수, M은 열의 개수를 지정 • k는 열의 값을 기준으로 시작 인덱스 입니다.

np.eye(N=3, M=5)
array(
[[1., 0., 0., 0., 0.],
[0., 1., 0., 0., 0.],
[0., 0., 1., 0., 0.]])
 
np.eye(N=3, M=5, k=3) #K는 1의 시작위치
array(
[[0., 0., 0., 1., 0.],
[0., 0., 0., 0., 1.],
[0., 0., 0., 0., 0.]])

 

▪ diag 함수 : 행렬의 대각성분 값을 추출합니다.

• k는 열의 값을 기준으로 시작 인덱스입니다.

matrix = np.arange(9).reshape(3,3)
matrix
array(
[[0, 1, 2],
[3, 4, 5],
[6, 7, 8]])
 
np.diag(matrix)
array([0, 4, 8])
 
np.diag(matrix, k=1)
array([1, 5])

 

7. 통계 분석 함수

▪ uniform 함수: 균등분포 함수

• 형식: np.random.uniform(시작값, 끝값, 데이터개수)

▪ normal 함수: 정규분포 함수

• np.random.normal(평균값, 분산, 데이터개수)

np.random.uniform(0, 5, 10) #(시작값, 끝값, 데이터개수) 균등분포함수
array([2.22367616, 2.8286466 , 4.35935841, 0.21897672, 4.61329179, 3.81520245, 1.34739281, 0.65992621, 3.03299715, 4.97364063])
 
np.random.normal(0, 2, 10) #(평균값, 분산, 데이터개수) 정규분포함수
array([ 0.05124111, -1.08882446, -1.07086545, -4.45826838, 3.78724076, -0.95802436, -1.15427134, -0.92596507, 0.12089848, -1.84587299])
 

 

여기까지입니다. 배열을 배웠습니다. 다음에는 넘파이성능의 장단점과 연산을 배워보겠습니다.