1학년 / 2학기 / AI-R-PY / 5강

5주차: NumPy 배열과 벡터 연산

이번 강의에서 배울 것

  • 파이썬 리스트와 NumPy 배열(ndarray)의 차이를 설명할 수 있다.
  • shape, ndim, dtype, size로 배열의 구조를 읽을 수 있다.
  • 원소별 곱(*)과 행렬곱(np.dot, @)을 구분해 계산할 수 있다.
  • axis를 지정해 합계·평균·최솟값과 argmin·argsort를 구할 수 있다.
  • 2차원 배열을 인덱싱·슬라이싱하고 reshape·전치로 모양을 바꿀 수 있다.

학습 경로

이전: 보충 · Pandas 기본기능 총정리 · 다음: 6주차 · 정규표현식, XML, JSON

범위: 5주차 슬라이드 3–34 · Drill 501–507 · 실습 노트북 RPY_5주차_2.ipynb

단원 도입

핵심 포인트중요도이렇게 확인한다
*(원소별 곱)와 np.dot·@(행렬곱)의 차이★★★같은 두 배열의 두 결과를 손으로 계산하기
shape·ndim·dtype·size 읽기★★★1·2·3차원 배열의 속성 쓰기 (Drill 501)
axis=0(열 방향) vs axis=1(행 방향)★★np.mean(A, axis=0) 결과 예측하기
2차원 슬라이싱 A[행, 열]★★표에서 잘라낸 부분 배열 쓰기
분산과 표준편차의 관계★np.sqrt(np.var(x)) == np.std(x) 설명하기

형광펜 읽는 법: 노랑 핵심 정의·결론 · 빨강 헷갈리기 쉬운 구분·함정 · 파랑 외워야 할 순서·목록

코드 읽는 법: 코드 옆 주석의 # → 뒤가 실제 실행 결과다. 슬라이드의 결과와 다르면 슬라이드 정정 박스에서 이유를 설명한다.

PART 1 · 왜 NumPy인가

NumPy란

3p

  • NumPy는 scientific computing(과학 계산)을 위한 라이브러리
  • 속도와 메모리 면에서 이점이 큼
  • Pandas도 NumPy를 기반으로 만들어졌다 → DataFrame의 각 열 속에는 NumPy 배열이 들어 있다
import numpy as np   # 관례적인 별칭은 np

리스트와 ndarray

4p~5p

list1 = ["dance", 9, 5.3, "disco", 8]   # 서로 다른 자료형도 OK
list1[2]   # → 5.3
list1[3]   # → 'disco'
 
a = np.array([9, 5, 7, 6, 8])
a[1]       # → 5
구분파이썬 리스트 listNumPy 배열 ndarray
요소의 자료형섞여도 됨모두 같은 자료형
크기자유롭게 늘고 줄어듦일반적으로 고정
인덱싱·슬라이싱가능가능 (다차원은 A[행, 열])
* 2의 의미리스트를 두 번 이어 붙임각 원소에 2를 곱함
수학 연산반복문 필요배열 전체에 바로 적용
[9, 5, 7, 6, 8] * 2            # → [9, 5, 7, 6, 8, 9, 5, 7, 6, 8]
np.array([9, 5, 7, 6, 8]) * 2  # → array([18, 10, 14, 12, 16])

배열의 속성

6p~8p

a = np.array([9, 5, 7, 6, 8])
type(a)    # → numpy.ndarray
a.dtype    # → dtype('int64')   ← 정수 배열
a.size     # → 5                ← 원소 개수
a.ndim     # → 1                ← 차원 수
a.shape    # → (5,)             ← 각 차원의 길이, 튜플
 
b = np.array([9.1, 5.2, 7.2, 6.3])
b.dtype    # → dtype('float64') ← 실수 배열
속성뜻np.array([9, 5, 7, 6, 8])
shape각 차원의 길이를 담은 튜플(5,)
ndim차원 수 = len(shape)1
size전체 원소 수 = shape 값들의 곱5
dtype원소의 자료형int64

원소 수정과 1차원 슬라이싱

9p~10p

a = np.array([9, 5, 7, 6, 8])
a[0] = 100
a          # → array([100, 5, 7, 6, 8])
 
a = np.array([9, 5, 7, 6, 8])
c = a[1:4]
c          # → array([5, 7, 6])     ← 끝 인덱스 4는 포함하지 않음
a[3:5] = 1, 2
a          # → array([9, 5, 7, 1, 2])

Drill 501 · 1·2·3차원 배열의 속성

11p

a1 = np.array([1, 2, 3])                    # 1차원: 벡터
a2 = np.array([[1, 2.0, 3.3],
               [4, 5, 6.5]])                # 2차원: 행렬
a3 = np.array([[[1, 2, 3], [4, 5, 6], [7, 8, 9]],
               [[10, 11, 12], [13, 14, 15], [16, 17, 18]]])   # 3차원
 
a1.shape, a1.ndim, a1.dtype, a1.size   # → ((3,), 1, dtype('int64'), 3)
a2.shape, a2.ndim, a2.dtype, a2.size   # → ((2, 3), 2, dtype('float64'), 6)
a3.shape, a3.ndim, a3.dtype, a3.size   # → ((2, 3, 3), 3, dtype('int64'), 18)

PART 2 · 배열 만들기

Drill 502 · ones, zeros와 astype

12p

ones = np.ones((10, 2))      # 10행 2열, 모두 1.0
ones.dtype                   # → dtype('float64')
ones.astype(int)             # → 정수 1로 바뀐 새 배열
 
zeros = np.zeros((5, 3, 3))  # 3×3 행렬 5개, 모두 0.0
zeros.dtype                  # → dtype('float64')
함수만드는 배열기본 dtype
np.ones(shape)모든 원소가 1float64
np.zeros(shape)모든 원소가 0float64
np.random.random(shape)0 이상 1 미만의 난수float64
np.random.randint(high, size=shape)0 이상 high 미만의 정수 난수int64
np.linspace(start, stop, num)구간을 num개로 균등 분할float64

Drill 503 · 난수와 seed

13p

np.random.random((5, 3))           # 실행할 때마다 다른 0~1 실수
 
np.random.seed(0)                  # 난수 시작점 고정
np.random.randint(10, size=(5, 3))
# → array([[5, 0, 3],
#          [3, 7, 9],
#          [3, 5, 2],
#          [4, 7, 6],
#          [8, 8, 1]])
 
import pandas as pd
np.random.seed(0)
df = pd.DataFrame(np.random.randint(10, size=(5, 3)))   # 배열 → DataFrame
df.columns = ["영수", "철수", "영희"]

linspace: 구간을 균등하게 나누기

21p

np.linspace(-2, 2, num=5)   # → array([-2., -1.,  0.,  1.,  2.])
np.linspace(-2, 2, 11)      # → array([-2. , -1.6, -1.2, -0.8, -0.4,  0. ,  0.4,  0.8,  1.2,  1.6,  2. ])

PART 3 · 기본 연산

벡터 합·차·상수배·상수 더하기

14p~15p, 18p

a = np.array([9, 5, 7, 6, 8])
b = np.array([1, 1, 1, 1, 2])
 
a + b    # → array([10,  6,  8,  7, 10])   벡터 합
a - b    # → array([ 8,  4,  6,  5,  6])   벡터 차
2 * a    # → array([18, 10, 14, 12, 16])   상수배
a + 1    # → array([10,  6,  8,  7,  9])   모든 원소에 1을 더함

같은 위치의 원소끼리 계산한다. 반복문이 필요 없다.

원소별 곱 vs 내적

16p~17p

a * b          # → array([ 9,  5,  7,  6, 16])   element-wise 곱
np.dot(a, b)   # → 43                            dot product(내적)

집계 메서드와 수학 함수

19p~20p, 22p

a = np.array([9, 5, 7, 6, 8])
a.mean()   # → 7.0   (9+5+7+6+8)/5
a.max()    # → 9
 
x = np.array([0, np.pi / 2])   # np.pi = π
np.sin(x)                      # → array([0., 1.])   sin(0), sin(π/2)

Drill 504

a1 = np.array([1, 2, 3])
a2 = np.array([[1, 2.0, 3.3], [4, 5, 6.5]])
 
np.sum(a1)      # → 6
np.mean(a2)     # → 3.6333...   축을 안 주면 전체 6개 원소의 평균
np.max(a2)      # → 6.5
np.min(a2)      # → 1.0
np.log(a1)      # → array([0.        , 0.69314718, 1.09861229])   자연로그
np.exp(a1)      # → array([ 2.71828183,  7.3890561 , 20.08553692])
np.square(a1)   # → array([1, 4, 9])
종류함수결과 모양
집계(aggregate)sum, mean, max, min, var, std값 하나 (axis를 주면 한 차원이 줄어듦)
원소별(universal function)sin, log, exp, square, sqrt입력과 같은 모양
위치(argument)argmin, argmax, argsort값이 아니라 인덱스

axis: 어느 방향으로 줄일까

실습 노트북

A = np.array([[3, 3],
              [4, 4]])
 
np.mean(A)           # → 3.5          전체 평균
np.mean(A, axis=0)   # → array([3.5, 3.5])   열마다(위→아래로 내려가며) 평균
np.mean(A, axis=1)   # → array([3., 4.])     행마다(왼→오른쪽으로) 평균
np.min(A, axis=0)    # → array([3, 3])

Drill 505 · 분산과 표준편차

23p

high_var_array = np.array([1, 100, 200, 300, 4000, 5000])
low_var_array  = np.array([2, 4, 6, 8, 10])
 
np.var(high_var_array), np.var(low_var_array)   # → (4296133.47..., 8.0)
np.std(high_var_array), np.std(low_var_array)   # → (2072.71..., 2.828...)
 
np.sqrt(np.var(high_var_array))                 # → 2072.711623024829

Drill 506 · 비교 연산과 arg 함수

24p

a1 = np.array([1, 2, 3])
a2 = np.array([[1, 2.0, 3.3], [4, 5, 6.5]])
 
a1 > 5      # → array([False, False, False])
a1 == a1    # → array([ True,  True,  True])
a1 >= a2
# → array([[ True,  True, False],
#          [False, False, False]])
 
np.argsort(a1)          # → array([0, 1, 2])   정렬했을 때의 인덱스 순서
np.argmin(a1)           # → 0                  최솟값의 위치
np.argmin(a2, axis=0)   # → array([0, 0, 0])   열마다 최솟값이 있는 행 번호

PART 4 · 다차원 배열

2차원 배열 만들기와 속성

25p~26p

a = [[1, 1, 1, 1], [2, 2, 2, 2], [3, 3, 3, 3]]
A = np.array(a)
 
A.ndim    # → 2
A.shape   # → (3, 4)   (행 수, 열 수)
A.size    # → 12       3 × 4
열 0열 1열 2열 3
행 01111
행 12222
행 23333

2차원 인덱싱과 슬라이싱

27p~29p

A[0][1]        # → 1   행 0, 열 1
A[2][3]        # → 3   행 2, 열 3
A[2, 3]        # → 3   같은 뜻, NumPy 권장 표기
 
A[0:2, 2]      # → array([1, 2])        행 0~1, 열 2
A[0:2, 0:2]    # → array([[1, 1],
               #          [2, 2]])      행 0~1, 열 0~1

행렬 연산

30p~33p

A = np.array([[1, 1], [2, 2]])
B = np.array([[3, 3], [4, 4]])
 
A + B          # → [[4, 4], [6, 6]]
A - B          # → [[-2, -2], [-2, -2]]
2 * A          # → [[2, 2], [4, 4]]
A * B          # → [[3, 3], [8, 8]]      원소별 곱
np.dot(A, B)   # → [[7, 7], [14, 14]]    행렬곱
A @ B          # → [[7, 7], [14, 14]]    행렬곱의 다른 표기

Drill 507 · reshape와 전치

34p

a2 = np.array([[1, 2.0, 3.3], [4, 5, 6.5]])   # shape (2, 3)
 
a2.reshape(2, 3, 1).shape   # → (2, 3, 1)   원소 6개는 그대로, 모양만 변경
a2.T                        # → 행과 열을 바꾼 전치 행렬
a2.T.shape                  # → (3, 2)
 
matrix = np.random.random(size=(5, 3, 3))
matrix.shape                # → (5, 3, 3)
matrix.T.shape              # → (3, 3, 5)   축 순서가 거꾸로 뒤집힘

정답과 해설

1. a * b → array([6, 20]), np.dot(a, b) → 2×3 + 4×5 = 26. 원소별 곱은 배열, 내적은 숫자 하나다.

2. ndim = 3, size = 4×2×3 = 24.

3. (3,). axis=1(열 방향 축)이 사라지고 행마다 하나씩 남는다.

4. array([[2, 2], [3, 3]]). 행 12, 열 12를 자른다.

5. [[1×1+2×3, 1×2+2×4], [3×1+4×3, 3×2+4×4]] = [[7, 10], [15, 22]].

6. 다르다. [[1, 2, 3], [4, 5, 6]]을 reshape(3, 2)하면 원소를 순서대로 다시 채워 [[1, 2], [3, 4], [5, 6]]이 되고, .T는 행과 열을 바꿔 [[1, 4], [2, 5], [3, 6]]이 된다.

플래시카드

5주차 — 파이썬 리스트와 NumPy 배열의 가장 큰 차이 두 가지는?

배열은 모든 원소의 자료형이 같고(섞이면 하나로 변환), 반복문 없이 배열 전체에 연산을 적용한다(벡터화). 리스트에 * 2를 하면 이어 붙이고, 배열에 * 2를 하면 각 원소에 2를 곱한다.

5주차 — shape, ndim, size, dtype은 각각 무엇인가?

shape은 각 차원의 길이 튜플, ndim은 차원 수, size는 전체 원소 수, dtype은 원소 자료형. 모두 괄호 없이 쓰는 속성이다.

5주차 — a * b와 np.dot(a, b)의 차이는?

*는 같은 자리끼리 곱한 배열(element-wise), np.dot·@는 행렬곱(1차원이면 곱해서 더한 내적 숫자 하나).

5주차 — axis=0과 axis=1로 평균을 내면 각각 무엇이 남는가?

axis=0은 위에서 아래로 접어 열마다 하나, axis=1은 왼쪽에서 오른쪽으로 접어 행마다 하나가 남는다. 지정한 축이 사라진다.

5주차 — np.random.seed(0)을 쓰는 이유는?

난수 생성의 시작점을 고정해 같은 난수열을 다시 얻기 위해서(재현성). 단, seed 이후 난수 함수를 부르는 순서까지 같아야 같은 값이 나온다.

5주차 — np.linspace(-2, 2, 5)의 결과와 range와의 차이는?

[-2., -1., 0., 1., 2.]. 끝값을 포함하고 세 번째 인자는 점의 개수다. range는 끝값을 제외하고 세 번째 인자가 간격이다.

5주차 — 표준편차와 분산의 관계는?

표준편차 = 분산의 제곱근. np.sqrt(np.var(x))는 np.std(x)와 같다.

5주차 — 브로드캐스팅이란?

모양이 다른 배열끼리 연산할 때 길이가 1이거나 빠진 차원을 자동으로 늘려 맞추는 규칙. 뒤쪽 차원부터 길이가 같거나 1이어야 한다.

핵심 점검

  • NumPy 배열의 모든 원소는 이다.
  • np.array([9, 5, 7, 6, 8]).shape의 결과는 이다.
  • 원소별 곱은 *, 행렬곱은 np.dot() 또는 로 쓴다.
  • np.mean(A, axis=0)은 마다 하나의 평균을 돌려준다.
  • np.argmin()은 최솟값이 아니라 최솟값의 를 돌려준다.
  • A[0:2, 2]에서 쉼표 앞은 , 쉼표 뒤는 이다.
  • 표준편차는 분산의 이다.
오늘도 한 걸음 앞으로.

내용을 이해했다면 학습 완료를 체크해 주세요.

학습 기록은 현재 브라우저에 저장됩니다.
HJS / STUDY한지석