AI(ML & DL)

[기계학습] Python NumPy란?? ( 2 )

ch010104 2025. 3. 24. 21:05

1️⃣ dtype과 itemsize


NumPy의 배열(ndarray)은 **모든 원소가 같은 타입(dtype)**을 가져야 효율적으로 동작!!

c = np.arange(1, 5)
print(c.dtype, c)
# int64 [1 2 3 4]

c = np.arange(1.0, 5.0)
print(c.dtype, c)
# float64 [1. 2. 3. 4.]

d = np.arange(1, 5, dtype=np.complex64)
#complex64는 복소수를 의미
print(d.dtype, d)
# complex64 [1.+0.j 2.+0.j 3.+0.j 4.+0.j]

e = np.arange(1, 5, dtype=np.complex64)
# complex64비트 = 8바이트
print(e.itemsize)
# 8 (bytes)​

 

  • dtype은 해당 배열의 원소의 데이터 타입을 반환
  • itemsize는 해당 배열의 각 원소의 데이터 타입의 크기를 반환(1바이트 = 8비트)
    - 각 원소의 데이터 타입의 크기를 반환하기 때문에 배열의 원소의 개수에 영향을 받지 x

2️⃣ 배열의 메모리 구조 .data

f = np.array([[1,2],[1000, 2000]], dtype=np.int32)
print(f.data)
# <memory at 0x7da5477dcad0>

# 메모리 내용 확인
print(f.data.tobytes())
# b'\x01\x00\x00\x00\x02\x00\x00\x00\xe8\x03\x00\x00\xd0\x07\x00\x00'
  • f는 2차원 배열이지만 실제로는 1차원으로 평탄화된 형태의 바이트 버퍼에 저장됨
  • 이 바이트 버퍼에 접근하는 방법은 .data 속성을 통해 가능

3️⃣ .shape vs .reshape()

g = np.arange(24)
print(g.shape, g.ndim)
# (24,) 1

g.shape = (6, 4)
print(g.shape, g.ndim)
# (6, 4) 2

g.shape = (2, 3, 4)
print(g.shape, g.ndim)
# (2, 3, 4) 3

#-------------------------------------------
g2 = g.reshape(4, 6)
print(g2)
# [[ 0  1  2  3  4  5]
#  [ 6  7  8  9 10 11]
#  [12 13 14 15 16 17]
#  [18 19 20 21 22 23]]

g2[1, 2] = 999
print(g2)
# [[ 0  1  2  3  4  5]
#  [ 6  7  999  9 10 11]
#  [12 13 14 15 16 17]
#  [18 19 20 21 22 23]]

print(g)
# [[[ 0  1  2  3]
#   [ 4  5  6  7]
#   [ 999  9 10 11]]
#
#  [[12 13 14 15]
#   [16 17 18 19]
#   [20 21 22 23]]
#-------------------------------------------
g2 = g.reshape(4, 6).copy()
print(g2)
# [[ 0  1  2  3  4  5]
#  [ 6  7  8  9 10 11]
#  [12 13 14 15 16 17]
#  [18 19 20 21 22 23]]

g2[1, 2] = 999
print(g2)
# [[ 0  1  2  3  4  5]
#  [ 6  7  999  9 10 11]
#  [12 13 14 15 16 17]
#  [18 19 20 21 22 23]]

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

 

  • g.shape으로 해당 배열의 어떠한 모양의 배열인지 알 수 있음.

  • 또한, g.shape = (6, 4) 와 같은 형식으로 명시적으로 배열의 모양을 변경 가능!!
    - 이 경우, 기존 배열 g의 형태를 변경하는 것!!

  • g.reshape()의 경우 g2 = g.reshape(4, 6)과 같은 형태로 사용
    - 이 경우, 기존 배열 g의 형태는 변경하지 않고, 새로운 배열 g2를 생성하여 (4, 6)의 형태로 만듬
    - reshape()을 할 경우에도, 배열 g와 g2는 하나의 메모리를 공유하기 때문에, 하나가 바뀌면 다른 하나도 같이 변경됨!

  • g.reshape().copy()의 경우 g2 = g.reshape(4, 6).copy()와 같은 형태로 사용
    - 이 경우, 기존 배열 g의 형태는 변경하지 않고, 새로운 배열 g2를 생성하여 (4, 6)의 형태로 만듬
    - reshape().copy()을 할 경우에도, 배열 g와 g2는 메모리를 공유하지 않기 때문에, 하나가 바뀌어도 다른 하나도 같이 변경 x!

**.shape, .reshape() 으로 배열을 변경하더라도 , 기존 데이터의 size 원소의 총 개수는 일정하게 유지해야함!!()**

24 = 6 * 4 = 2 * 3 * 4


4️⃣ ravel()

print(g.ravel())
# [ 0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23]
  • 어떠한 형태의 배열도 1차원 배열로 반환!!

5️⃣ 산술 연산

a = np.array([14, 23, 32, 41])
b = np.array([5, 4, 3, 2])

print("a + b =", a + b)
# [19 27 35 43]

print("a - b =", a - b)
# [ 9 19 29 39]

print("a * b =", a * b)
# [70 92 96 82]

print("a / b =", a / b)
# [ 2.8         5.75       10.66666667 20.5       ]

print("a ** b =", a ** b)
# [537824 279841  32768   1681]

 

 

  • 산술 연산은 두 배열 간의 모양이 같을 경우에만 가능함!!
  • 만약 두 배열 간의 모양이 다르더라도, Broadcasting의 3가지 규칙에 의해 모양이 통일될 수 있을 경우 산술 연산 가능

6️⃣ Broadcasting – 3가지 규칙

# 1번째 규칙
h = np.arange(5).reshape(1, 1, 5)
print(h + [10, 20, 30, 40, 50])
# [[[10 21 32 43 54]]]

# 2번째 규칙
k = np.arange(6).reshape(2, 3)
print(k + [[100], [200]])
# [[100 101 102]
#  [203 204 205]]

# 2번째 규칙
print(k + [100, 200, 300])
# [[100 201 302]
#  [103 204 305]]

# 3번째 규칙(에러 발생 예)
try:
    k + [33, 44]
except ValueError as e:
    print(e)
# operands could not be broadcast together with shapes (2,3) (2,)

 

1번째 규칙: 차원이 다르면 앞에 1을 추가

  • 배열 h는 (1, 1, 5)의 모양을 가지고 있고, [10, 20, 30, 40, 50] 은 (5,) 의 모양을 가지고 있음.
  • 두 배열의 차원이 3, 1로 서로 다르기 때문에 차원이 1인 (5,) 앞에 1을 추가하여 (1, 1, 5) 과 같이 3차원으로 변경
    - 앞에 1을 추가하는 건 가능하지만, 뒤에 1을 추가하는 건 불가!!
    - [10, 20, 30, 40, 50] -> [[[10, 20, 30, 40, 50]]]
  • 두 배열의 모양이 같기 때문에 산술 연산 가능!!

2번째 규칙: 차원이 같으면, 가장 큰 배열 크기만큼 반복

  • 배열 k는 (2, 3)의 모양을 가지고 있고, [[100], [200]] 은 (2, 1)의 모양을 가지고 있음.
  • 두 배열의 차원이 2로 같지만, 모양이 다름. (2, 1) 배열을 y축으로 3번 반복 복사해서 (2, 3)으로 만듬
    - [[100], [200]] -> [[100 100 100], [200 200 200]]
  • 두 배열의 모양이 같기 때문에 산술 연산 가능!!

** k + [100, 200, 300] **

  • (3,) 의 [100, 200, 300] 을 (2, 3)인 k와 같은 모양으로 맞추어야함. 
  • (3,) -> (1, 3) -> (2, 3)
    - [100, 200, 300] -> [[100, 200, 300]] -> [[100, 200, 300], [100, 200, 300]]
  • 두 배열의 모양이 같기 때문에 산술 연산 가능!!

3번째 규칙: 1, 2 번째 규칙을 적용해서 모양을 통일시킬 수 없으면, 에러 발생(산술 연산 불가능!!)

  • 배열 k는 (2, 3)의 모양을 가지고 있고, [33, 44] 는 (2,)의 모양을 가지고 있음
  • 1번째 규칙을 적용해 (2,) -> (1, 2)로 차원을 일치
  • 2번째 규칙을 이용해 (1, 2)에서 (2, 3)으로 통일 시킬 수 없기 때문에 에러 발생!!
 

7️⃣ Upcasting + 데이터 타입 범위

k1 = np.arange(5, dtype=np.uint8)
k2 = k1 + np.array([5,6,7,8,9], dtype=np.int8)
print(k2.dtype, k2)
# int16 [ 5  7  9 11 13]

k3 = k1 + 1.5
print(k3.dtype, k3)
# float64 [1.5 2.5 3.5 4.5 5.5]
  • 데이터 타입이 서로 다른 두 배열간의 산술 연산에서는 산술 연산 결과 배열이 연산된 두 배열의 데이터 타입 범위를 모두 포함할 수 있는 타입으로 반환!!
  • ex) unit8(0 ~ 255) + int 8(-128 ~ 127) = int 16(-32,768 ~ 32,767) 으로 반환
dtype 비트 범위
int8 8 -128 ~ 127
uint8 8 0 ~ 255
int16 16 -32,768 ~ 32,767
float64 64 ±1.8e308

8️⃣ 조건 연산 & boolean indexing

m = np.array([20, -5, 30, 40])
print(m < 25)
# [True True False False]

print(m[m < 25])
# [20 -5]

 

  • m < 25 와 같이 사용하면 [True True False False] 와 같은 boolean indexing으로 반환
  • m[m < 25] = m[True True False False] = [20, -5]

9️⃣ ndarray 메서드

a = np.array([[-2.5, 3.1, 7], [10, 11, 12]])
print(a.mean())
# 6.766666666666667

for func in (a.min, a.max, a.sum, a.prod, a.std, a.var):
    print(func.__name__, '=', func())

# min = -2.5
# max = 12.0
# sum = 40.6
# prod(배열 전체의 곱) = -71610.0 
# std(표준편차) = 5.0848...
# var(분산) = 25.8555...

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

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

c.sum(axis=0)
# [12, 14, 16, 18],
# [20, 22, 24, 26],
# [28, 30, 32, 34]

c.sum(axis=1)
# [12, 15, 18, 21],
# [48, 51, 54, 57]

c.sum(axis=(0,2))
# [60, 92, 124]

# [실습] ndarray c에 대해서 x-axis 방향으로 max 함수가 적용되도록 코드 작성
# result = c.max(axis=2)
# [[ 3 7 11]
# [15 19 23]]

 

  • 배열의 전체 원소의 값들에 대해서 min, max, sum 등의 ndarray 메서드를 적용할 수 있음.
  • c.shape == (2, 3, 4)
    • axis=0: 크기 2 → 총 2개의 "매트릭스(z축)" -> (3, 4) 모양으로 반환
    • axis=1: 크기 3 → 각 매트릭스에 3개의 "행(y축)" -> (2, 4) 모양으로 반환
    • axis=2: 크기 4 → 각 행에 4개의 "열(x축)" -> (2, 3) 모양으로 반환
  • c.sum(axis=0)
    - c[0][0][0] + c[1][0][0] = 0 + 12 = 12
    - c[0][2][3] + c[1][2][3] = 11 + 23 = 34

  • c.sum(axis=1)
    - 0+4+8 = 12, 1+5+9 = 15, ...
  • c.sum(axis=(0,2))
    • y = 0: [0,1,2,3] + [12,13,14,15] → 합 = 60
    • y = 1: [4~7] + [16~19] → 합 = 92
    • y = 2: [8~11] + [20~23] → 합 = 124
    • (3, ) 모양으로 반환

🔟 Universal Functions (ufuncs) - 범용 함수

# 원본 배열
a = np.array([[-2.5, 3.1, 7], [10, 11, 12]])

# 제곱
print(np.square(a))
# [[  6.25   9.61  49.  ]
#  [100.   121.   144.  ]]

# 절댓값
print(np.abs(a))  
# [[2.5 3.1 7.], 
# [10. 11. 12.]]

# 제곱근 (음수는 nan 발생)
print(np.sqrt(a))
# [[       nan 1.76068169 2.64575131]
#  [3.16227766 3.31662479 3.46410162]]

# 지수 함수 (e^x)
print(np.exp(a))  
# [[0.082 22.19 1096.63], 
# [22026.46 59874.14 162754.79]]

# 자연로그 (음수는 nan)
print(np.log(a)) 
# [[nan 1.13 1.94], 
# [2.30 2.39 2.48]]

# 부호 반환: 음수→-1, 양수→1, 0→0
print(np.sign(a))  
# [[-1. 1. 1.], 
# [1. 1. 1.]]

# 올림 (ceil)
print(np.ceil(a))  
# [[-2. 4. 7.], 
# [10. 11. 12.]]

# 정수부, 소수부 분리
frac, integer = np.modf(a)
print("소수부:\n", frac)
print("정수부:\n", integer)

# NaN 여부 확인
print(np.isnan(a))  
# 모두 False

# 코사인 (라디안 기준)
print(np.cos(a))  
# [[-0.80 -0.999 0.75], [-0.839 0.004 0.843]]

1️⃣1️⃣ Binary ufunc

a = np.array([1, -2, 3, 4])
b = np.array([2, 8, -1, 7])

print(np.add(a, b))
# [ 3  6  2 11]

print(np.greater(a, b))
# [False False True False]

print(np.maximum(a, b))
# [2 8 3 7]

print(np.copysign(a, b))
# [ 1.  2. -3.  4.]
# a의 절대값을 유지하면서, b의 부호를 따라감

 


1️⃣2️⃣ 배열 인덱싱

a = np.array([1, 5, 3, 19, 13, 7, 3])

print(a[3])
# [19]

print(a[2:5])
# [ 3 19 13]

print(a[2:-1])
# [ 3, 19, 13,  7]
 
print(a[:2])
# [1, 5]
 
print(a[2::2]) # 2번째 인덱스부터 마지막까지 2간격으로 
# [ 3, 13, 3]
 
print(a[::-1]) # reverse order, a는 그대로 남아 있음
# [ 3,  7, 13, 19,  3,  5,  1]

a[2:5] = -1
print(a)
# [ 1  5 -1 -1 -1  7  3]

a[2:5] = [997, 998, 999]
print(a)
# [  1   5 997 998 999   7   3]
  • a[2:5] 의 경우 끝 인덱스인 5는 포함 x

1️⃣3️⃣ Python 배열과의 차이

a[2:5] = -1
# 슬라이스 전체에 -1 적용됨 (broadcast)

# 에러 예시
# a[2:5] = [1,2,3,4,5,6] → ValueError
# del a[2:5] → 삭제 불가

#----------------------------------------------------------------------------------------------
a = np.array([1, 5, -1, 4, -1, 7, 3])

# 슬라이싱으로 뷰(view) 생성
a_slice = a[2:6]   # [-1, 4, -1, 7]
a_slice[1] = 1000  # a[3]이 1000으로 바뀜!

print(a)  # → 원본 배열도 바뀜!
# [ 1, 5, -1, 1000, -1, 7, 3]

# 반대로 원본을 수정하면 slice도 영향을 받음
a[3] = 2000
print(a_slice)  # → 슬라이스도 같이 바뀜!
# [-1, 2000, -1, 7]
#----------------------------------------------------------------------------------------------
another_slice = a[2:6].copy()
another_slice[1] = 3000  # 복사본만 바뀜!

print(a)  # 원본 배열 영향 없음
# [1, 5, -1, 2000, -1, 7, 3]

a[3] = 4000
print(another_slice)  # 복사본 유지됨
# [-1, 3000, -1, 7]

1️⃣4️⃣ Multidimensional Indexing

b = np.arange(48).reshape(4, 12)
print(b)
# [[ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11],
#  [12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23],
#  [24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35],
#  [36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47]]

print(b[1, 2])
# 14

b[1, :]
# array([12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23])
# shape (12,)

b[:, 1]
# array([ 1, 13, 25, 37])
# shape (4,)

print(b[1, :])
# [12 13 14 15 16 17 18 19 20 21 22 23]
# 벡터 (1차원)

print(b[1:2, :])
# [[12 13 14 15 16 17 18 19 20 21 22 23]]
# 행이 1개인 2차원 배열

1️⃣5️⃣ Fancy Indexing

b = np.arange(48).reshape(4, 12)
print(b)
# [[ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11],
#  [12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23],
#  [24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35],
#  [36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47]]

print(b[(0,2), 2:5]) # 행 0과 2, 열 2부터 4까지 (2:5)
# [[ 2  3  4]
#  [26 27 28]]

print(b[:, (-1, 2, -1)]) # 모든 행, 열 [-1, 2, -1] 순서대로 선택
# [[11  2 11]
#  [23 14 23]
#  [35 26 35]
#  [47 38 47]]

print(b[(-1, 2, -1, 2), (5, 9, 1, 9)]) # (행, 열)을 각각 짝지어서 개별 좌표 선택
# [41, 33, 37, 33]

 


1️⃣6️⃣ 고차원 인덱싱

c = b.reshape(4,2,6)
print(c)
# [[[ 0,  1,  2,  3,  4,  5],
#   [ 6,  7,  8,  9, 10, 11]],
#  
#  [[12, 13, 14, 15, 16, 17],
#   [18, 19, 20, 21, 22, 23]],
# 
#  [[24, 25, 26, 27, 28, 29],
#   [30, 31, 32, 33, 34, 35]],
# 
#  [[36, 37, 38, 39, 40, 41],
#   [42, 43, 44, 45, 46, 47]]]

c[2, 1, 4]  # matrix 2, row 1, col 4
# 34

c[2, :, 3]  # matrix 2, all rows, col 3
# [27, 33]

c[2, 1]  # Return matrix 2, row 1, all columns.  This is equivalent to c[2, 1, :]
# [30, 31, 32, 33, 34, 35]

 


1️⃣7️⃣ Ellipsis (...) 와 Boolean Indexing

c = b.reshape(4,2,6)
print(c)
# [[[ 0,  1,  2,  3,  4,  5],
#   [ 6,  7,  8,  9, 10, 11]],
#  
#  [[12, 13, 14, 15, 16, 17],
#   [18, 19, 20, 21, 22, 23]],
# 
#  [[24, 25, 26, 27, 28, 29],
#   [30, 31, 32, 33, 34, 35]],
# 
#  [[36, 37, 38, 39, 40, 41],
#   [42, 43, 44, 45, 46, 47]]]

print(c[2, ...]) # matrix 2, all rows, all columns.  This is equivalent to c[2, :, :]
# [[24 25 26 27 28 29]
#  [30 31 32 33 34 35]]

print(c[2, 1, ...])  # matrix 2, row 1, all columns.  This is equivalent to c[2, 1, :]
# [30, 31, 32, 33, 34, 35]
 
print(c[2, ..., 3])  # matrix 2, all rows, column 3.  This is equivalent to c[2, :, 3]
# [27, 33]

print(c[..., 3]) # matrix 2, row 1, all columns.  This is equivalent to c[2, 1, :]
# [[ 3  9]
#  [15 21]
#  [27 33]
#  [39 45]]
#----------------------------------------------------------------------------------------------
b = np.arange(48).reshape(4, 12)
print(b)

# [[ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11],
#  [12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23],
#  [24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35],
#  [36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47]]

rows_on = np.array([True, False, True, False])
cols_on = np.array([False, True, False] * 4)

print(b[rows_on, :]) # Rows 0, 2 출력
# [[ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11],
#  [24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35]]

print(b[:, cols_on]) # Columns 1, 4, 7, 10 출력
# [[ 1,  4,  7, 10],
#  [13, 16, 19, 22],
#  [25, 28, 31, 34],
#  [37, 40, 43, 46]]

 


1️⃣8️⃣np.ix_() 

b = np.arange(48).reshape(4, 12)
print(b)

# [[ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11],
#  [12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23],
#  [24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35],
#  [36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47]]

rows_on = np.array([True, False, True, False])
cols_on = np.array([False, True, False] * 4)

print(b[np.ix_(rows_on, cols_on)])
# [[ 1  4  7 10]
#  [25 28 31 34]]

print(b % 3 == 1) # 배열 b의 각 원소에 대해서 b % 3 == 1 을 수행해서 참 거짓 반환
# [[False, True, False, False, True, False, False, True, False, False, True, False],
#  [False, True, False, False, True, False, False, True, False, False, True, False],
#  [False, True, False, False, True, False, False, True, False, False, True, False],
#  [False, True, False, False, True, False, False, True, False, False, True, False]]

print(b[b % 3 == 1]) # True 인것들만 반환
# [1, 4, 7, 10, 13, 16, 19, 22, 25, 28, 31, 34, 37, 40, 43, 46]

1️⃣9️⃣실습 예제

c = b.reshape(4,2,6)
print(c)
# [[[ 0,  1,  2,  3,  4,  5],
#   [ 6,  7,  8,  9, 10, 11]],
#  
#  [[12, 13, 14, 15, 16, 17],
#   [18, 19, 20, 21, 22, 23]],
# 
#  [[24, 25, 26, 27, 28, 29],
#   [30, 31, 32, 33, 34, 35]],
# 
#  [[36, 37, 38, 39, 40, 41],
#   [42, 43, 44, 45, 46, 47]]]

# 1. matrix 1, 3의 row 1 역순
print(c[(1,3), 1, ::-1])
# [[23 22 21 20 19 18]
#  [47 46 45 44 43 42]]

# 2. 모든 matrix의 row 0의 col 1,2,5
print(c[:, 0, [1,2,5]])
# [[ 1  2  5]
#  [13 14 17]
#  [25 26 29]
#  [37 38 41]]

# 3. 짝수이면서 30 이상
print(c[(c % 2 == 0) & (c >= 30)])
# [30 32 34 36 38 40 42 44 46]