Code › codeit-ai-sprint
NumPy 배열의 차원과 축 이해하기
NumPy 배열이 리스트와 어떻게 다른지, shape와 axis를 처음 배우며 정리한 수업 기록
이번 수업은 벡터와 행렬에서 시작해서 NumPy 배열로 넘어갔다. 처음에는 수학 개념처럼 보였는데, 뒤로 갈수록 shape와 axis를 이해하기 위한 준비였다는 쪽으로 정리됐다.
벡터와 행렬에서 시작했다
벡터는 숫자를 순서대로 묶은 것이고, 선형대수에서는 크기와 방향을 가진 대상으로 다룬다. 숫자 하나로 표현하면 1차원 벡터이고, x와 y 두 값으로 표현하면 2차원 벡터가 된다. 3차원 벡터는 x, y, z 세 값으로 표현할 수 있지만, 차원이 더 커지면 그림으로 직접 그리기 어렵다.
행렬은 숫자를 행과 열로 배치한 직사각형 배열이다. 데이터 표에서는 보통 행이 샘플이고 열이 변수라서, 여러 샘플의 여러 특성을 한 번에 담을 수 있다.
행렬 연산에서는 덧셈과 뺄셈은 같은 크기의 행렬끼리만 가능하고, 스칼라곱은 모든 원소에 같은 수를 곱한다. 행렬곱은 같은 위치끼리 곱하는 연산이 아니라, 앞 행렬의 열 수와 뒤 행렬의 행 수가 맞아야 하는 연산이다. m x n 행렬과 n x p 행렬을 곱하면 결과는 m x p 행렬이 된다.
행렬과 벡터를 곱하면 행렬이 벡터에 선형 변환을 적용해서 새 벡터를 만든다. 2차원에서는 회전, 확대와 축소, 반사 같은 변환을 행렬로 나타낼 수 있다. 여기서 행렬은 벡터 자체가 아니라, 벡터를 입력으로 받아 새 벡터를 계산하는 연산자로 쓰인다.
NumPy 배열과 파이썬 리스트
파이썬 리스트는 파이썬 기본 자료구조이고, NumPy 배열은 NumPy가 제공하는 ndarray다. 둘 다 여러 값을 순서대로 다룰 수 있지만 내부 구조와 계산 방식은 다르다.
파이썬 리스트는 여러 값의 참조를 순서대로 담는다. 값마다 타입이 달라도 담을 수 있고, 중간에 다른 종류의 값을 넣을 수도 있다.
반면 dtype은 NumPy 배열에 들어 있는 값의 자료형을 뜻하고, NumPy 배열은 보통 하나의 dtype으로 통일된 값을 연속된 메모리에 놓고 계산한다. NumPy가 빠른 이유 중 하나는 핵심 연산이 C로 구현되어 있기 때문이다. 파이썬 코드에서 반복문을 직접 도는 대신, NumPy 내부의 C 레벨 루프가 같은 dtype의 연속된 데이터를 처리하고, 빌드 환경과 CPU가 지원하면 SIMD 같은 CPU 최적화도 사용할 수 있다.
문자열을 다룰 때도 이 차이가 드러난다. astype(str)로 바꾸면 NumPy가 문자열 길이를 고정된 폭으로 잡을 수 있고, 더 긴 문자열을 넣을 때 뒤가 잘리는 문제가 생길 수 있다. 문자열 길이나 타입이 섞인 값을 안전하게 다뤄야 하면 object dtype을 쓰는 식으로 배열의 규칙을 바꿔야 한다.
shape와 reshape
shape는 배열의 각 차원 크기를 튜플로 보여 준다. 1차원 배열의 shape가 (6,)처럼 쉼표를 포함하는 이유는, 원소가 하나인 튜플도 튜플임을 표시해야 하기 때문이다.
import numpy as np
data = np.array([1, 2, 3, 4, 5, 6])
print(data.shape)
# (6,)
matrix = data.reshape(2, 3)
print(matrix)
# [[1 2 3]
# [4 5 6]]
reshape(2, 3)은 숫자 6개를 2행 3열 배열로 다시 배치한다. reshape(2, 5, 10)이라면 10개짜리 줄이 5개 있고, 그런 묶음이 2개 있는 3차원 배열로 읽을 수 있다.
이미지 데이터도 같은 방식으로 읽을 수 있다. 이미지 배열의 shape가 (334, 500, 3)이면 세로 334칸, 가로 500칸, 채널 3개짜리 데이터라는 뜻이고, 마지막 차원의 3은 보통 RGB 채널을 나타낸다.
image = np.zeros((334, 500, 3))
print(image.shape)
# (334, 500, 3)
print(image[0, 0, 0])
# 0.0
image[0, 0, 0]은 왼쪽 위 픽셀에서 첫 번째 채널 값을 꺼내는 표현이고, image[0, 0]은 그 픽셀의 RGB 값 세 개를 한 번에 가리킨다.
axis, 줄이는 연산과 붙이는 연산
axis는 연산을 적용할 축이다. 합계나 최댓값처럼 차원을 줄이는 연산에서는 지정한 축의 진행 방향으로 값을 모으고, 결과에서는 그 축이 사라진다.
2행 3열 배열에서 axis=0은 행 축이고, 행이 진행되는 방향은 세로 방향이다. 그래서 sum(axis=0)은 세로 방향으로 값을 더해 행 축을 압축하고, 결과로는 열별 합계가 남는다. max(axis=0)을 쓰면 같은 방향으로 비교하되, 더하는 대신 열별 최댓값을 남긴다.
반대로 axis=1은 열 축이고, 열이 진행되는 방향은 가로 방향이다. sum(axis=1)은 각 행 안에서 가로로 값을 모아 행별 합계를 만들고, max(axis=1)은 각 행 안에서 가장 큰 값을 남긴다.
matrix = np.array([[1, 2, 3], [4, 5, 6]])
print(matrix.sum(axis=0))
# [5 7 9]
print(matrix.max(axis=0))
# [4 5 6]
print(matrix.sum(axis=1))
# [ 6 15]
print(matrix.max(axis=1))
# [3 6]
같은 axis라도 concatenate와 stack에서는 의미가 달라진다. sum이나 max는 지정한 축을 따라 값을 모으면서 차원을 줄이지만, concatenate는 새 축을 만들지 않고 기존 축의 길이를 늘리며 stack은 새 축을 하나 만들어 배열을 포갠다.
a = np.array([[1, 2], [3, 4]])
b = np.array([[5, 6], [7, 8]])
print(np.concatenate([a, b], axis=0).shape)
# (4, 2)
print(np.concatenate([a, b], axis=1).shape)
# (2, 4)
print(np.stack([a, b], axis=0).shape)
# (2, 2, 2)
정리하면 sum과 max는 축을 압축하고, concatenate는 기존 축을 연장하며, stack은 새 축을 만든다. axis는 같은 숫자로 쓰이지만 함수가 무엇을 하느냐에 따라 결과 shape가 달라진다.
keepdims=True를 쓰면 sum이나 max처럼 축을 줄이는 연산에서도 줄어든 축을 크기 1로 남긴다. 값은 줄어들지만 배열의 차원 수는 유지되기 때문에, 이후 broadcasting이나 다른 배열 연산을 이어 붙일 때 형태를 맞추기 쉽다.
copy와 view
슬라이싱은 새 배열을 복사하는 작업이 아니라, 원본 배열의 일부를 바라보는 view를 만들 수 있다. 그래서 잘라낸 조각을 수정하면 원본도 같이 바뀔 수 있고, 원본과 분리된 배열이 필요할 때는 copy를 명시적으로 호출해야 한다.
patch = image[157:177, 240:260]
safe_patch = patch.copy()
이미지 일부를 잘라 실험할 때 이 차이를 모르면 원본 이미지까지 바뀔 수 있다. view는 원본과 데이터를 공유하고, copy는 새 배열을 만든다는 점을 구분해야 한다.
broadcasting과 마스킹
브로드캐스팅은 shape가 다른 배열끼리도 규칙이 맞으면 자동으로 형태를 맞춰 계산하는 기능이다. 예를 들어 행렬 전체에 같은 값을 더하거나, 각 행에 같은 벡터를 더하는 작업을 반복문 없이 쓸 수 있다.
마스킹은 조건을 만족하는 원소만 골라내거나 바꾸는 방식이다. 조건식의 결과가 boolean 배열로 만들어지고, 그 배열을 인덱스로 사용해서 필요한 위치만 선택한다.
array1 = np.arange(16).reshape(4, 4)
mask = array1 < 10
array1[~mask] = 100
print(array1)
반복문으로 하나씩 검사해도 같은 결과를 만들 수는 있지만, NumPy에서는 이런 조건 선택과 대입을 배열 연산으로 표현할 수 있다. 코드가 짧아지는 것뿐 아니라, 내부 연산도 배열 단위로 처리된다.
정리
이번 수업은 벡터와 행렬에서 시작해서 NumPy 배열의 shape와 axis로 이어졌다. 벡터와 행렬은 데이터를 숫자 배열로 다루기 위한 기본 언어이고, NumPy 배열은 같은 dtype의 값을 연속 메모리에 놓고 계산하는 구조다.
axis는 단순히 외울 값이 아니라, 함수가 어느 축을 기준으로 값을 모으거나 붙일지 정하는 기준이라 앞으로도 중요한 부분이다. 실제로 3차원에서는 입체 도형을 그려가면서 시각화해서 이해하는것이 좋을 것 같다. 연습해야지.