2-1. NumPy 소개: 배열과 기본 연산

1. NumPy 소개: 배열의 강력한 도구

NumPy(Numerical Python)는 파이썬에서 과학적 계산을 위한 핵심 라이브러리입니다. 특히, 대규모 다차원 배열 및 행렬 연산을 효율적으로 수행하도록 설계되었습니다. 파이썬의 list 자료형으로도 배열과 유사한 작업을 할 수 있지만, NumPy는 속도, 메모리 사용량, 그리고 다양한 수학적 기능 측면에서 훨씬 뛰어납니다. NumPy는 데이터 과학, 머신러닝, 딥러닝 등 다양한 분야에서 없어서는 안 될 필수 도구입니다.

1) NumPy의 필요성: 왜 NumPy를 써야 할까?

파이썬의 기본 자료형인 list는 유연성이 뛰어나지만, 대규모 데이터 처리에 있어서는 여러 단점을 가집니다.

  • 속도: list는 각 요소가 객체로 저장되어 있어, 연산 시 오버헤드가 발생합니다. NumPy는 C로 구현된 배열을 사용하므로, 훨씬 빠른 연산이 가능합니다.
  • 메모리 효율성: list는 각 요소의 자료형을 저장하기 위한 추가적인 메모리를 사용합니다. NumPy 배열은 동일한 자료형의 데이터를 연속된 메모리 공간에 저장하므로, 메모리 사용량을 줄일 수 있습니다.
  • 편의성: NumPy는 배열 연산을 위한 다양한 함수와 기능을 제공합니다. 예를 들어, list에서 각 요소에 특정 값을 더하려면 반복문을 사용해야 하지만, NumPy에서는 간단한 연산으로 처리할 수 있습니다.

이러한 장점들 덕분에 NumPy는 데이터 과학 분야에서 널리 사용되고 있으며, Pandas, scikit-learn, TensorFlow, PyTorch 등 다른 라이브러리들의 기반이 됩니다.

2. ndarray: NumPy의 핵심 객체

NumPy의 핵심은 ndarray (n-dimensional array) 객체입니다. ndarray는 동일한 자료형을 가진 n차원 배열을 나타냅니다. ndarray는 NumPy 연산의 기본 단위이며, NumPy의 모든 기능은 ndarray를 중심으로 이루어집니다.

1) ndarray 생성

ndarray는 다양한 방법으로 생성할 수 있습니다. 가장 기본적인 방법은 numpy.array() 함수를 사용하는 것입니다. 이 함수는 파이썬 list 또는 tuplendarray로 변환합니다.

import numpy as np

# 1차원 배열 생성
arr1 = np.array([1, 2, 3])
print(arr1)  # [1 2 3]
print(arr1.shape) # (3,)

# 2차원 배열 생성
arr2 = np.array([[1, 2, 3], [4, 5, 6]])
print(arr2)
print(arr2.shape)  # (2, 3)

arr1은 1차원 배열(벡터)이고, arr2는 2차원 배열(행렬)입니다. shape 속성은 배열의 차원과 각 차원의 크기를 나타냅니다. arr1.shape는 (3,)으로, 1차원 배열이고 3개의 요소를 가지고 있다는 것을 의미합니다. arr2.shape는 (2, 3)으로, 2x3 행렬임을 의미합니다.

2) 배열 생성 함수

NumPy는 특정 값을 가진 배열을 생성하는 다양한 함수를 제공합니다.

  • np.zeros(shape): 주어진 shape의 모든 요소가 0으로 초기화된 배열을 생성합니다.
  • np.ones(shape): 주어진 shape의 모든 요소가 1로 초기화된 배열을 생성합니다.
  • np.full(shape, fill_value): 주어진 shape의 모든 요소가 fill_value로 초기화된 배열을 생성합니다.
  • np.eye(N): N x N 단위 행렬을 생성합니다.
  • np.arange([start,] stop[, step,], dtype=None): start부터 stop까지(stop은 포함하지 않음) step 간격으로 증가하는 값을 가진 배열을 생성합니다.
  • np.linspace(start, stop, num): start부터 stop까지(stop을 포함) num개의 균등한 간격으로 분할된 값을 가진 배열을 생성합니다.
  • np.random.rand(shape): 0과 1 사이의 난수로 채워진 배열을 생성합니다.
  • np.random.randn(shape): 평균 0, 표준편차 1의 가우시안 분포(정규 분포)를 따르는 난수로 채워진 배열을 생성합니다.
# 0으로 채워진 배열
zeros_array = np.zeros((2, 3))
print(zeros_array)

# 1로 채워진 배열
ones_array = np.ones((3, 2))
print(ones_array)

# 특정 값으로 채워진 배열
full_array = np.full((2, 2), 7)
print(full_array)

# 단위 행렬
eye_matrix = np.eye(3)
print(eye_matrix)

# 0부터 9까지의 배열
arange_array = np.arange(10)
print(arange_array)

# 0부터 1까지 5개의 균등한 간격의 값
linspace_array = np.linspace(0, 1, 5)
print(linspace_array)

# 0과 1 사이의 난수
rand_array = np.random.rand(2, 2)
print(rand_array)

# 가우시안 분포 난수
randn_array = np.random.randn(2, 2)
print(randn_array)

3) 자료형 (dtype)

ndarray는 각 요소의 자료형(dtype)을 가집니다. NumPy는 다양한 자료형을 지원하며, dtype을 명시적으로 지정하여 배열을 생성할 수도 있습니다. 자료형을 지정하면 메모리 사용량을 최적화하고, 연산의 정확성을 보장할 수 있습니다.

# 정수형 배열 (int64)
int_array = np.array([1, 2, 3], dtype=np.int64)
print(int_array.dtype)

# 실수형 배열 (float32)
float_array = np.array([1.0, 2.0, 3.0], dtype=np.float32)
print(float_array.dtype)

# 문자열 배열
string_array = np.array(['apple', 'banana', 'cherry'])
print(string_array.dtype)

자주 사용되는 dtype은 다음과 같습니다.

  • np.int8, np.int16, np.int32, np.int64: 정수형 (8, 16, 32, 64비트)
  • np.float16, np.float32, np.float64: 실수형 (16, 32, 64비트)
  • np.bool_: 불리언형
  • np.str_: 문자열형

3. 배열 연산

NumPy는 배열 간의 다양한 연산을 지원합니다. 이러한 연산은 element-wise 방식으로 수행되며, 반복문을 사용하지 않고도 간결하고 효율적인 코드를 작성할 수 있게 해줍니다.

1) 기본 연산

NumPy는 덧셈(+), 뺄셈(-), 곱셈(), 나눗셈(/), 거듭제곱(*)과 같은 기본적인 산술 연산을 지원합니다.

arr_a = np.array([1, 2, 3])
arr_b = np.array([4, 5, 6])

# 덧셈
add_result = arr_a + arr_b
print(add_result)  # [5 7 9]

# 뺄셈
subtract_result = arr_a - arr_b
print(subtract_result)  # [-3 -3 -3]

# 곱셈
multiply_result = arr_a * arr_b
print(multiply_result)  # [ 4 10 18]

# 나눗셈
divide_result = arr_a / arr_b
print(divide_result)  # [0.25 0.4  0.5 ]

# 거듭제곱
power_result = arr_a ** 2
print(power_result)  # [1 4 9]

2) 브로드캐스팅 (Broadcasting)

브로드캐스팅은 NumPy에서 서로 다른 shape의 배열 간에 연산을 수행하는 기능입니다. shape가 호환되지 않는 경우, NumPy는 자동으로 배열의 shape를 맞춰 연산을 수행합니다. 이는 코드의 간결성을 높이고, 개발자가 불필요한 shape 변환을 신경 쓰지 않도록 해줍니다.

브로드캐스팅은 특정 조건이 충족될 때만 가능하며, 규칙을 이해하는 것이 중요합니다.

브로드캐스팅 규칙 설명 뒤

브로드캐스팅 규칙:

  1. 두 배열의 차원 수가 다르면, 차원 수가 적은 배열의 shape 앞에 1을 추가합니다.
  2. 두 배열의 각 차원의 크기가 같거나, 둘 중 하나의 크기가 1이면 브로드캐스팅이 가능합니다.
  3. 브로드캐스팅이 가능한 경우, 크기가 1인 차원은 다른 배열의 크기에 맞춰 늘어납니다.
arr_c = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
arr_d = np.array([1, 0, 1])

# arr_d는 (3,) shape이고, arr_c는 (3, 3) shape입니다.
# arr_d의 shape 앞에 1이 추가되어 (1, 3)이 됩니다.
# arr_c와 arr_d의 각 차원 크기를 비교합니다.
# (3, 3) vs (1, 3) -> 두 번째 차원 크기가 같고, 첫 번째 차원은 arr_d가 1이므로 브로드캐스팅 가능
# arr_d가 (3, 3)으로 브로드캐스팅 되어 연산이 수행됩니다.

broadcast_result = arr_c + arr_d
print(broadcast_result)
# [[ 2  2  4]
#  [ 5  5  7]
#  [ 8  8 10]]

3) 유니버셜 함수 (ufunc)

NumPy는 배열의 각 요소에 대해 연산을 수행하는 ufunc (universal function)를 제공합니다. ufunc는 C로 구현되어 있어, 반복문을 사용하는 것보다 훨씬 빠릅니다.

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

# 제곱근
sqrt_result = np.sqrt(arr_e)
print(sqrt_result)  # [1.         1.41421356 1.73205081]

# 지수 함수
exp_result = np.exp(arr_e)
print(exp_result)  # [ 2.71828183  7.3890561  20.08553692]

# 로그 함수
log_result = np.log(arr_e)
print(log_result)  # [0.        0.69314718 1.09861229]

NumPy는 np.add, np.subtract, np.multiply, np.divide와 같은 기본 연산에 해당하는 ufunc를 제공하며, 삼각 함수, 지수 함수, 로그 함수 등 다양한 수학 함수를 ufunc 형태로 제공합니다.

4. 배열 슬라이싱 (Slicing)

배열 슬라이싱은 배열의 일부분을 선택하는 강력한 기능입니다. 슬라이싱을 통해 데이터를 조작하고, 특정 부분에 접근하여 연산을 수행할 수 있습니다.

1) 1차원 배열 슬라이싱

1차원 배열 슬라이싱은 파이썬 list 슬라이싱과 유사합니다.

arr_f = np.array([0, 1, 2, 3, 4, 5])

# 처음 3개 요소
slice1 = arr_f[:3]
print(slice1)  # [0 1 2]

# 인덱스 2부터 4까지의 요소 (4는 포함되지 않음)
slice2 = arr_f[2:5]
print(slice2)  # [2 3 4]

# 인덱스 3부터 끝까지의 요소
slice3 = arr_f[3:]
print(slice3)  # [3 4 5]

# 처음부터 끝까지, 2칸 간격으로
slice4 = arr_f[::2]
print(slice4)  # [0 2 4]

2) 다차원 배열 슬라이싱

다차원 배열 슬라이싱은 각 차원별로 슬라이스 범위를 지정합니다.

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

# 첫 번째 행
row1 = arr_g[0, :]
print(row1)  # [1 2 3]

# 첫 번째 열
col1 = arr_g[:, 0]
print(col1)  # [1 4 7]

# 첫 번째 행과 두 번째 행
rows12 = arr_g[0:2, :]
print(rows12)
# [[1 2 3]
#  [4 5 6]]

# 첫 번째 행과 두 번째 열
element = arr_g[0, 1]
print(element) # 2

# 1,2 행과 1,2 열
sub_matrix = arr_g[0:2, 0:2]
print(sub_matrix)
# [[1 2]
#  [4 5]]

3) 슬라이싱과 뷰 (View)

슬라이싱은 원본 배열의 뷰(view)를 반환합니다. 뷰는 원본 배열의 데이터를 공유하므로, 뷰를 수정하면 원본 배열도 변경됩니다.

arr_h = np.array([1, 2, 3, 4, 5])
slice_h = arr_h[:3]
slice_h[0] = 100

print(arr_h)  # [100   2   3   4   5]

위 예제에서 slice_harr_h의 뷰입니다. slice_h의 첫 번째 요소를 변경하면 arr_h의 첫 번째 요소도 변경됩니다. 만약 원본 배열의 복사본을 얻고 싶다면, copy() 메서드를 사용해야 합니다.

arr_i = np.array([1, 2, 3, 4, 5])
copy_i = arr_i[:3].copy()
copy_i[0] = 100

print(arr_i)  # [1 2 3 4 5]
print(copy_i) # [100   2   3]

5. 마치며

NumPy는 파이썬에서 과학적 계산을 위한 핵심 라이브러리이며, ndarray를 기반으로 강력한 기능들을 제공합니다. 본 포스트에서는 NumPy의 기본 개념, ndarray 생성, 배열 연산, 슬라이싱에 대해 알아보았습니다. NumPy는 데이터 과학, 머신러닝, 딥러닝 등 다양한 분야에서 필수적인 도구이므로, NumPy를 능숙하게 사용하는 것은 매우 중요합니다. 다음 포스트에서는 NumPy의 배열 인덱싱과 브로드캐스팅에 대해 자세히 살펴보겠습니다.

비슷한 글 추천

Comments (0)

No comments yet. Be the first to comment!