본문 바로가기
TYLER SONGBlog
블로그 목록
LLM Research

NumPy 기초: shape과 axis만 알면 배열 연산이 보인다

오늘은 NumPy의 핵심 자료구조인 ndarray를 만들고, shape(모양)과 axis(축) 개념으로 다차원 배열을 다루는 법을 배운다. 행렬을 만들어 행/열 합을 구하고, reshape과 슬라이싱으로 배열 모양을 자유자재로 바꿔본다. 마지막엔 파이썬 순수 for문과 NumPy 연산의 속도 차이를 직접 눈으로 확인한다.

송민성7분 읽기

오늘의 목표

파이썬 리스트가 아니라 NumPy의 배열(ndarray)로 숫자 데이터를 다루는 법을 익힌다. 특히 배열의 "모양(shape)"과 "축(axis)"이라는 개념을 이해해서, 나중에 행렬·텐서 연산을 볼 때 헷갈리지 않는 게 오늘의 목표다.

개념과 직관

ndarray가 뭔가

지금까지 파이썬 리스트 [1, 2, 3]으로 숫자를 모았다면, NumPy는 numpy.ndarray(n-dimensional array, n차원 배열)라는 전용 자료구조를 쓴다. 겉보기엔 리스트랑 비슷해 보이지만 결정적 차이가 있다: ndarray는 안에 든 숫자들이 메모리에 촘촘하게 붙어 저장되고, 타입이 통일되어 있어서(dtype) 계산이 훨씬 빠르다.

비유하자면 파이썬 리스트는 "책 여러 권을 아무 책장에나 흩어놓은 것"이고, ndarray는 "같은 크기의 책을 한 줄로 딱 맞춰 꽂아놓은 것"이다. 크기가 통일돼 있으니 몇 번째 칸에 뭐가 있는지 계산으로 바로 찾아갈 수 있어서 빠르다.

python
import numpy as np a = np.array([1, 2, 3]) print(a, type(a))

shape (모양)

배열이 몇 차원이고 각 차원에 몇 개씩 들어있는지를 나타내는 튜플이다. 3행 4열짜리 표(행렬)라면 shape은 (3, 4)다.

  • 1차원 배열: [1, 2, 3] → shape (3,) — 그냥 숫자 3개가 일렬로.
  • 2차원 배열: 행과 열이 있는 표 → shape (3, 4) — 3행 4열.
  • 3차원 배열: 표가 여러 장 쌓인 것 → shape (2, 3, 4) — 표 2장, 각 표는 3행 4열.

그림으로 상상하면: 1차원은 "줄", 2차원은 "표(엑셀 시트)", 3차원은 "표가 여러 장 겹친 책"이다.

python
m = np.array([[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]]) print(m.shape) # (3, 4) print(m.dtype) # int64 (환경에 따라 다를 수 있음)

dtype은 배열 안 숫자들의 타입(정수인지 실수인지 등)이다. 파이썬 리스트는 원소마다 다른 타입을 섞을 수 있지만, ndarray는 원소 전부가 같은 dtype이어야 한다. 이게 속도의 비밀 중 하나다.

axis (축)

axis는 "어느 방향으로 계산할지"를 지정하는 번호다. 2차원 배열에서:

  • axis=0은 "행 방향으로 내려가며", 즉 각 열(column)끼리 계산
  • axis=1은 "열 방향으로 가로질러", 즉 각 행(row)끼리 계산

헷갈리기 쉬운데, 이렇게 외우면 편하다: axis 번호는 "그 축의 크기가 사라지는(합쳐지는) 방향"이다. axis=0으로 sum하면 shape (3,4)에서 첫 번째 숫자 3이 없어지고 (4,)가 남는다. axis=1로 sum하면 두 번째 숫자 4가 없어지고 (3,)이 남는다.

python
print(m.sum(axis=0)) # 각 열의 합 -> shape (4,) print(m.sum(axis=1)) # 각 행의 합 -> shape (3,)

배열 만드는 여러 방법

python
z = np.zeros((2, 3)) # 0으로 채운 2x3 배열 o = np.ones((2, 3)) # 1로 채운 2x3 배열 r = np.arange(0, 10, 2) # 0부터 10 전까지 2씩 증가 -> [0,2,4,6,8]

reshape과 transpose

reshape은 원소 개수는 그대로 두고 모양만 바꾸는 것이다. 12개 숫자가 있으면 (3,4)로도, (2,6)으로도, (12,)로도 접을 수 있다. 단, 전체 원소 개수는 항상 같아야 한다(3×4=12, 2×6=12).

transpose는 행과 열을 뒤바꾸는 것, 즉 표를 90도 돌려서 행이었던 게 열이 되는 것이다.

python
v = np.arange(6) # [0,1,2,3,4,5], shape (6,) v2 = v.reshape(2, 3) # shape (2,3) v3 = v2.transpose() # shape (3,2), 행<->열 뒤바뀜

인덱싱·슬라이싱·불리언 마스크

파이썬 리스트처럼 a[0], a[1:3]으로 잘라올 수 있는데, 2차원부터는 a[행, 열] 식으로 콤마로 구분해서 접근한다.

python
print(m[0, 1]) # 0번째 행, 1번째 열의 값 -> 2 print(m[:, 1]) # 모든 행의 1번째 열 -> [2, 6, 10] print(m[1, :]) # 1번째 행 전체 -> [5, 6, 7, 8]

불리언 마스크(boolean mask)는 "조건을 만족하는 자리만 True로 표시한 배열"을 인덱스처럼 써서 원소를 골라내는 방법이다.

python
mask = m > 6 print(mask) # True/False로 이루어진 같은 모양의 배열 print(m[mask]) # 6보다 큰 값들만 1차원으로 뽑아냄

코드로 직접 해보기

1) 3x4 행렬 만들어 행·열 합 구하기

python
import numpy as np mat = np.arange(1, 13).reshape(3, 4) print(mat) # [[ 1 2 3 4] # [ 5 6 7 8] # [ 9 10 11 12]] col_sum = mat.sum(axis=0) # 각 열의 합 row_sum = mat.sum(axis=1) # 각 행의 합 print("열 합:", col_sum) # [15 18 21 24] print("행 합:", row_sum) # [10 26 42]

2) 1차원을 (2,3)으로 reshape

python
flat = np.array([10, 20, 30, 40, 50, 60]) reshaped = flat.reshape(2, 3) print(reshaped) # [[10 20 30] # [40 50 60]] # reshape(-1, 3)처럼 -1을 넣으면 "이 자리는 알아서 계산해줘"라는 뜻 auto = flat.reshape(-1, 3) print(auto.shape) # (2, 3)

3) 조건에 맞는 원소만 골라 바꾸기

python
data = np.array([3, -1, 7, -5, 2, -9]) data[data < 0] = 0 # 음수인 자리만 0으로 교체 print(data) # [3 0 7 0 2 0]

이건 리스트로 하려면 for문 돌면서 if 검사하는 걸, NumPy는 조건식 하나로 끝낸다는 점을 기억해두자.

오늘의 실험

목표: "파이썬 이중 for문 합계"와 "np.sum" 중 뭐가 빠른지 직접 재본다.

python
import numpy as np import time size = 1000 arr = np.random.rand(size, size) # 방법 1: 순수 파이썬 이중 for문 start = time.time() total = 0 for i in range(size): for j in range(size): total += arr[i, j] end = time.time() print("for문 걸린 시간:", end - start) # 방법 2: np.sum start = time.time() total_np = arr.sum() end = time.time() print("np.sum 걸린 시간:", end - start)

기록할 것:

  • size를 100, 500, 1000, 2000으로 바꿔가며 두 방법의 시간을 표로 적어본다.
  • 결과값(totaltotal_np)이 소수점 오차 범위 내에서 같은지도 확인한다(부동소수점이라 완전히 똑같지 않을 수 있다).
  • 배열 크기가 커질수록 두 방법의 시간 차이가 어떻게 벌어지는지 관찰하고, 왜 그런지 한두 문장으로 스스로 설명해본다. (힌트: NumPy는 내부적으로 C로 구현되어 있고, 반복문을 파이썬 레벨이 아니라 더 낮은 레벨에서 돌린다.)

왜 이걸 배우나 (LLM / Post-training 연결)

LLM 내부의 모든 데이터(입력 토큰, 임베딩, 어텐션 가중치 등)는 결국 shape을 가진 다차원 배열(텐서)로 표현된다. PyTorch 텐서는 오늘 배운 ndarray와 사실상 같은 사고방식으로 동작하고, "shape이 안 맞아서 에러가 난다"는 상황은 모델을 학습시키거나 디버깅할 때 매일 만나는 문제다. axis 개념도 그대로 이어져서, 나중에 배치(batch) 차원, 시퀀스 길이 차원, 임베딩 차원 중 어느 축으로 합/평균을 낼지 지정하는 게 손실 함수 계산이나 어텐션 구현의 핵심이 된다. 오늘 헷갈리지 않고 넘어가면 이후 몇 주가 훨씬 편해진다.

초보자가 막히는 지점

  • axis=0과 axis=1을 반대로 생각하는 실수: "axis=0이니까 첫 번째 방향(가로)이겠지"라고 직관적으로 착각하기 쉽다. 오늘 알려준 방법대로 "그 축의 숫자가 사라지는 방향"으로 외우는 게 훨씬 안 헷갈린다.
  • reshape 시 원소 개수 불일치 에러: ValueError: cannot reshape array of size N into shape (a,b) 에러가 뜨면 무조건 a×b가 N과 같은지부터 확인한다.
  • shape (3,)과 (3,1)을 같다고 착각: (3,)은 그냥 벡터고 (3,1)은 3행 1열짜리 2차원 행렬이다. 둘은 다르게 취급되어서 나중에 브로드캐스팅(broadcasting)에서 예상치 못한 결과가 나올 수 있다. 지금은 "다르다"는 것만 기억해두면 된다.
  • 불리언 마스크가 원본을 바꾼다는 것을 모르고 당황함: m[mask] = 0처럼 쓰면 원본 배열 자체가 바뀐다. 원본을 보존하고 싶으면 m.copy()를 먼저 떠서 작업한다.
  • 정수 dtype 배열에 실수를 대입하면 잘림: int 타입 배열에 arr[0] = 3.7을 넣으면 3으로 잘려서 저장된다. dtype을 항상 신경 쓰는 습관을 들이자 (arr.dtype으로 확인).

알고리즘 (병행 1시간)

  • Easy 1문제 + Medium 1문제 (구현 위주): LeetCode에서 배열/문자열 조작 기반의 Easy 1개, Medium 1개를 골라 푼다. 오늘 배운 NumPy와 직접 관련은 없지만, 코딩 인터뷰 감각과 "배열을 인덱스로 다루는 감각"을 같이 키우는 용도다.
  • 기록 형식: 문제마다 아래 3가지를 반드시 메모해둔다.

- 접근: 어떤 아이디어로 풀었는지 (예: 투 포인터, 해시맵 등) - 시간복잡도: Big-O로 표현 (예: O(n), O(n log n)) - 틀린 이유: 처음에 틀렸다면 왜 틀렸는지 (엣지 케이스 누락, 인덱스 범위 실수 등) — 이 기록이 나중에 비슷한 실수를 줄여준다.

© 2026 Tyler Song