머신러닝

넘파이(NumPy) 1

haniru 2026. 1. 6. 23:55

넘파이에 대한 설명

1. 파이썬의 특징과 한계

파이썬은 귀도 반 로섬(Guido van Rossum)이 만든 언어로, 그 명칭은 그가 좋아하던 코미디 팀 '몬티 파이썬'에서 따왔다. 로고가 뱀인 이유는 'Python'이 비단구렁이를 뜻하기 때문이며, 실제 파이썬은 뱀처럼 매우 유연(Flexible)하다.

  • 장점: 문법이 쉬워 배우기 좋고, 오픈소스 라이브러리가 방대하여 연구 및 개발 단계에서 접근성이 매우 높다. 다른 언어(C, C++ 등)와 결합이 쉬운 '글루(Glue) 언어'이기도 하다.
  • 단점: 파이썬은 인터프리터 방식의 스크립트 언어라 순수 파이썬만으로 대량의 수치 연산을 처리하기에는 속도가 너무 느리다.

2. 왜 넘파이(NumPy)인가?

파이썬의 '느린 속도'라는 치명적인 단점을 해결하기 위해 등장한 것이 넘파이다.

  • C 언어 기반: 넘파이의 핵심 연산 부분은 C 언어로 작성되어 있어 실행 속도가 매우 빠르다.
  • 벡터화 연산: 반복문(for문) 없이도 배열 간 연산을 한 번에 처리할 수 있어 효율적이다.
  • 표준: 데이터 과학의 사실상 표준 라이브러리이며, 판다스(Pandas), 맷플롯립(Matplotlib), 사이킷런(Scikit-learn) 등이 모두 넘파이를 기반으로 작동한다.

3. 텐서(Tensor)와 다차원 배열

데이터를 표현할 때 차원이 높아질수록 설명해야 할 정보가 많아진다.

  • 스칼라(0차원): 점 (하나의 숫자)
  • 벡터(1차원): 선 (숫자들의 나열)
  • 행렬(2차원): 면 (표 형태)
  • 텐서(3차원 이상): 공간 (행렬을 여러 겹 쌓은 형태). 고차원 공간인 하이퍼플레인(Hyperplane)을 수학적으로 다루기 위해 필수적인 개념이다.

4. 딥러닝 프레임워크: 텐서플로우 vs 파이토치

넘파이의 다차원 배열 개념을 확장하여 GPU 연산과 자동 미분 기능을 추가한 것이 현대의 딥러닝 프레임워크다.

구분 텐서플로우(TensorFlow) 파이토치(PyTorch)
개발 주체 구글 (Google) 메타 (Meta, 구 페이스북)
주요 특징 정적 그래프, 산업 현장 및 상용화에 유리 동적 그래프, 파이썬답고 유연한 코드
주 사용처 기업, 대규모 서비스 운영, 모바일 기기 연구소, 대학, 최신 논문 구현
비고 알파고(AlphaGo) 개발에 사용됨 현재 연구용으로 가장 점유율이 높음

 

참고: 알파고는 구글 딥마인드에서 개발했으므로 텐서플로우(정확히는 그 이전 버전이나 관련 라이브러리) 계열을 사용했다. 파이토치는 연구 효율성이 좋아 현재 인공지능 학계에서 대세로 자리 잡고 있다.

 

 

5. 넘파이가 빠른 이유 (정리)

  1. 연속된 메모리 할당: CPU 캐시 적중률(Cache Hit Rate)이 높아짐.
  2. 벡터화(Vectorization): C로 구현되어 있어 반복문 없이 CPU의 SIMD(Single Instruction Multiple Data) 기능을 활용해 데이터를 한꺼번에 처리함.
  3. 타입 고정: 파이썬 리스트와 달리 모든 요소의 데이터 타입이 동일하므로, 매번 타입을 확인할 필요가 없음.

 

파이참에서 넘파이 설치

상단 파일 > 설정 > 인터프리터로 가서 pandas 설치

 

파이썬 프로젝트에서 폴더 만들기

 

 

함수와 메소드의 차이

 

  • 함수 (Function): 특정 객체에 속하지 않고 독립적으로 존재하며, 데이터를 인자로 받아 처리한다.
    • 예: np.array([2, 3, 4]) → np라는 모듈 레벨에서 호출하는 함수다.
  • 메소드 (Method): 클래스 내부에 정의되어 특정 객체(인스턴스)를 통해 호출된다. 해당 객체의 상태를 이용하거나 변경한다.
    • 예: a.all() → a라는 ndarray 객체가 가진 기능을 실행하는 메소드다.

 

 

코드 (주피터 노트북)

주피터 노트북은 웹 기반의 인터랙티브 쉘(IPython)을 사용하기 때문에 일반적인 파이썬 스크립트 실행과는 다른 특징이 있다.

  • 자동 출력: 셀의 마지막 줄에 변수명(a)만 작성하면 print(a)를 생략해도 값이 출력된다. 이는 커널이 마지막 표현식의 결과를 가로채서 웹 브라우저(UI)로 던져주기 때문이다.
  • 차이점: * print(a): 표준 출력(stdout)으로 문자열을 내보낸다.
    • a: 객체 자체를 반환하며, 주피터는 이를 Out[ ] 영역에 더 보기 좋은 형태(Rich Representation)로 표시한다.

암시적 형변환 (Implicit Upcasting)

넘파이 배열은 모든 요소가 동일한 데이터 타입(dtype)을 가져야 한다.

  • 현상: np.array([2, 3.0, 4])와 같이 정수와 실수가 섞여 있으면, 넘파이는 데이터를 안전하게 보존하기 위해 더 넓은 범위인 **실수형(float64)**으로 모든 요소를 통일한다.
  • 결과: print(a)를 하면 [2. 3. 4.]와 같이 점(.)이 붙어서 출력된다.
  • 확인: a.dtype을 입력하면 해당 배열의 타입을 정확히 알 수 있다.
import numpy as np
from numpy.ma.extras import average

a = np.array([2,3.0,4])
print(a)
a

 

파이썬 기본 리스트에서 + 연산자는 두 리스트의 요소를 산술적으로 더하는 것이 아니라, 뒤에 이어 붙이는 역할을 함.

(C언어 데이터 구조에서의 Concatenate와 같은 동작)

store_a = [20, 10, 30]
store_b = [70, 90, 70]
list_sum = store_a + store_b
list_sum

 

np.array()를 호출하면 파이썬 리스트의 데이터를 바탕으로 연속된 C 메모리 블록을 할당한다.

  • 주소 전달: np_store_a 변수는 데이터 자체가 아니라, 해당 데이터가 시작되는 메모리 주소를 가리키는 객체(포인터 역할)다.
  • 메타데이터: 이 객체는 메모리 주소뿐만 아니라 데이터의 타입(dtype), 차원(ndim), 각 요소 간의 거리(strides) 정보를 함께 저장하여 데이터에 즉시 접근하게 한다.
  • 효율성: 파이썬 리스트처럼 객체 참조를 따라다니는 것이 아니라, 하드웨어 수준에서 연속된 주소를 계산해 직접 읽기 때문에 속도가 압도적으로 빠르다.
np_store_a = np.array(store_a)
np_store_b = np.array(store_b)
array_sum = np_store_a + np_store_b
array_sum

 

a = np.array([2, 3, 4])를 실행하면 컴퓨터 내부에서는 다음과 같은 일이 일어난다.

  • 메모리 할당: 숫자 2, 3, 4가 메모리의 연속된 공간에 배치된다.
  • 포인터 역할: 변수 a는 메모리에 올라간 첫 번째 요소(숫자 2)의 주소값을 가리키는 포인터 역할을 수행한다.
  • 객체화: a는 단순히 주소만 가진 게 아니라, 넘파이의 ndarray라는 클래스로부터 만들어진 **인스턴스(객체)**다.

속성

 

  • a.shape: 배열의 형태를 나타낸다. (3,)은 요소가 3개인 1차원 벡터를 의미하며, e_1 방향으로 데이터가 나열되어 있다.
  • a.ndim: 차원의 수다. 여기서는 1차원이다.
  • a.dtype: 내부 데이터의 타입(예: int64, float64)이다.
  • a.itemsize: 각 요소 하나가 차지하는 메모리 크기(바이트)다.
  • a.size: 전체 요소의 개수(3개)다.
  • a.strides: 다음 요소로 넘어가기 위해 메모리상에서 몇 바이트를 건너뛰어야 하는지 나타낸다.

 

a = np.array([2, 3, 4])
a.shape, a.ndim, a.dtype, a.itemsize, a.size, a.strides, a.all()

 

 

b.shape 결과인 (2, 3)은 배열의 차원 정보를 담고 있으며, 넘파이에서 축 번호는 0번부터 시작한다.

b = np.array([[1,2,3],[4,5,6]])
b.shape

 

넘파이(NumPy) 배열 간의 산술 연산

a = np.array([10, 20, 30])
b = np.array([40, 50, 60])
print(a+b)
print(a*b)
print(a/b)

 

넘파이에서 별도로 타입을 지정하지 않으면 기본적으로 64비트 사용 -> 데이터 과학에서는 32비트만으로도 충분한 경우가 많다. 64비트를 쓰면 메모리 사용량이 2배로 늘어나 연산 속도가 느려진다.

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

 

브로드캐스팅: a + 10이나 a * 10이 가능한 이유는 넘파이의 브로드캐스팅 덕분이다.

넘파이는 스칼라 값 10을 배열 a의 형태인 (3,)에 맞춰 [10, 10, 10]으로 확장한 뒤 연산을 수행한다.

a = np.array([10, 20, 30])
print(a * 10)
print(a + 10)

 

배열 b: shape (2, 3). 2행 3열의 행렬

배열 c: shape (3,). 요소가 3개인 1차원 벡터

브로드캐스팅 발생: 두 배열의 마지막 차원(열) 크기가 3으로 일치하므로, 넘파이는 c를 (2, 3) 형태로 확장하여 연산을 수행한다.

b = np.array([[10, 20, 30], [40, 50, 60]], dtype=np.int32)
c = np.array([2, 3, 4], dtype=np.int32)
print(b+c) # 실제 수학에서는 안됨.
print(b*c)
print(b-c)
더보기
더보기

# b (2x3) [[10, 20, 30], [40, 50, 60]]

# c (브로드캐스팅 후 2x3) [[2, 3, 4], [2, 3, 4]]

파이썬 리스트의 연산

a:list = [1,2,3,4,5,6]
print(max(a))
print(min(a))

sum = 0
for item in a:
    sum = sum + item

print(sum/len(a))

 

파이썬 넘파이 배열의 연산

a = np.array([10, 20, 30], dtype=np.int32)
print(a.max(), a.min())

sum = 0
for item in a:
    sum = sum + item
print(sum/a.size)

print(a.mean()) # 이 방식이 더 빠르다
a.astype(np.float32) # int 32, float 32 를 머신러닝에서 많이 쓴다

 

평탄화

flatten()은 원본 배열의 주소를 참조하는 것이 아니라, 데이터를 복사하여 새로운 1차원 배열 객체를 생성한다. 따라서 f_b = b.flatten() 실행 후 원본 b의 구조(3행 2열)는 그대로 유지된다.

b = np.array([[1,1],[2,2,],[3,3]])
print(b.shape) # 3 바이 2
f_b = b.flatten()
print(f'b: {b}')
print(f_b)

 

전치행렬

b = np.array([[1,1],[2,2,],[3,3]])
print(b.T)
print(b.transpose()) # .T보다 더 정밀한 제어가 가능

 

정렬

c2.sort()는 인플레이스(In-place) 연산이다. 즉, 메소드를 호출한 원본 객체의 메모리 내부 데이터를 직접 수정하며 별도의 반환값이 없다.

c1 = np.array([35, 25, 55, 69, 19, 99]) # 기본은 오름차순, 기본이 퀵
c1.sort(kind='quicksort')
print(c1)
print(c1[::-1]) # 순서가 역으로 바뀐다. 내림차순이다

c2 = np.array([[35, 24, 55], [69,19,9], [4,1,11]])
c2.sort(axis=0)
print(c2)

 

a = np.arange(1, 10, 1): [1, 2, 3, 4, 5, 6, 7, 8, 9] 형태의 1차원 벡터가 생성

a = a.reshape(3, 3): 1차원 배열을 2차원 행렬로 변환

b = np.full((3, 3), 3): 지정한 형태(Shape)를 특정 상수로 가득 채움

a = np.arange(1,10,1)
a = a.reshape(3,3)
print(a)
b = np.full((3, 3), 3)
print(b)
a = np.arange(1, 10, 1)
result =a.reshape(3, 3)
print(result)

b = np.full(9, 3)
print(b)
result = b.reshape(3, 3)
print(result)

 

sort로 정렬 후 내림차순(flip) 정렬

d = np.array([[35, 24, 55], [69, 19, 9], [4, 1, 11]])
# 1. 가로 방향(axis=1) 내림차순 정렬
# 먼저 오름차순 정렬 후 뒤집기
sort_axis1 = np.sort(d, axis=1)
desc_axis1 = np.flip(sort_axis1, axis=1)
print(desc_axis1)

# 2. 세로 방향(axis=0) 내림차순 정렬
sort_axis0 = np.sort(d, axis=0)
desc_axis0 = np.flip(sort_axis0, axis=0)
print(desc_axis0)

 

np.append의 기본 동작: 평탄화(Flattening) - [1, 2, 3, 4, 5, 6, 7, 8, 9]

리스트로 한 번 감싸는 [a] 를 사용하면 a의 형태가 (3,)에서 (1, 3)인 2차원 행렬로 확장

# append
a = np.array([1,2,3])
b = np.array([[4,5,6],[7,8,9]])
result = np.append(a, b)
print(result)
result = np.append([a], b, axis=0) # [] 를 덧씌움
print(result)

 

arange와 reshape

y=np.arange(12)
print(y)
result = y.reshape(3,4)
print(result)
a1 = np.array([[1,2],[3,4],[5,6],[7,8]])
a1 = np.reshape(a1, (-1,4)) # 알아서 갯수를 확인함
print(a1)

a = np.arange(1, 9) # 1차원 배열 a
b = a.reshape(2, 2, 2) # 3차원 배열 b, 2바이 2가 두개 있다

print(a)
print(b)

 

random 모듈을 활용한 다양한 난수 생성 방법과 정규분포의 특성

 

result = np.random.randint(150, 191, size=10) # 191은 안 들어감
print(result)

# 가우시안 분포 - 표준 정규분포 - normal을 쓴다
rnd = np.random.randn(5) # 평균이 0이고 표준편차가 1인 표준정규분포. 그래프가 모여져있음
print(rnd)
rnd = np.random.randn(5) * 10 + 165 # 평균이 165, 표준편차가 10 -> 가우시안 분포를 따른다, 평균이 커졌으므로 그래프가 퍼져있음
print(rnd)

np.random.seed(42) # 랜덤함수지만 결과가 같음. seed를 넣으면 같은 숫자가 나온다

# 평균값이 165, 표준편차가 10인 정규 분포 함수는 다음과 같이 생성 가능하다.
nums = np.random.normal(loc=165, scale=10, size=(3,4)).round(2)
print(nums)

퍼질수록 표준편차 커짐

shuffle vs permutation

 

  • np.random.shuffle(x) (In-place 연산):
    • 원본 배열 x의 메모리에 직접 접근하여 순서를 바꾼다.
    • 반환값(Return value)이 없으며(None), 메소드를 실행한 후 원본 객체의 상태가 변경된다.
  • np.random.permutation(x) (Copy 연산):
    • 원본 배열은 그대로 두고, 순서가 섞인 새로운 배열 복사본을 생성하여 반환한다.
    • 데이터를 안전하게 보존해야 할 때 사용한다.

 

a = np.arange(10)
np.random.shuffle(a)
print(a) # 할때마다 다르다

np.random.permutation([2,4,6,8,10])

 

 

train_data, test_data 뽑기

data = np.arange(1, 51, 1)
np.random.shuffle(data)
print(data)

train_data = np.array([])
test_data = np.array([])

for i in data:
    if (i < data.size * 0.8):
        train_data = np.append(i, train_data)
    else:
        test_data = np.append(i, test_data)

print(f"train_data: {train_data}")
print(f"test_data: {test_data}")
def train_test_split(balls):
    shuffled = np.random.permutation(balls)

    # train_size = int(shuffled.size * 0.8)
    # train_data = shuffled[:train_size]
    # test_data = shuffled[train_size:]
    # return train_data, test_data

    split_idx = int(len(shuffled) * 0.8)
    return shuffled[:split_idx], shuffled[split_idx:]


balls = np.arange(1, 51)
(x_train, x_test) = train_test_split(balls)
print(x_train, x_test)

 

평균 구하기

a = np.array([10, 20, 30, 40, 50])
print("a의 평균값 : ", a.mean())

 

합계 구하기

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

print(np.sum(arr,axis=0))
print(np.sum(arr,axis=1))
print(np.sum(arr,axis=(0,1)))
코드 연산 방향 결과 구조 설명
np.sum(arr, axis=0) 세로 (↓) [5, 10, 15, 20, 25] 각 열의 합계를 구함. 2차원이 1차원 벡터로 축소됨.
np.sum(arr, axis=1) 가로 (→) [15, 15, 15, 15, 15] 각 행의 합계를 구함. 요소 5개인 벡터가 됨.
np.sum(arr, axis=(0,1)) 전체 75 모든 요소를 더해 하나의 스칼라 값으로 만듦.

 

선형 방정식

a = np.array([[1,2],[1,-3]])
b = np.array([6,1])
s = np.linalg.solve(a, b)
print(s)

A = np.array([[1, 1, -1], [2,-1,3], [1,2,1]], dtype='int32')
B = np.array([0, 9, 8], dtype='int32')
result = np.linalg.solve(A, B)
print(result)

 

2차원 행렬식 계산

a = np.array([[2,1],[4,5]])
det_a = np.linalg.det(a)
b = np.array([[1,2],[3,-6]])
det_b = np.linalg.det(b)
print(det_a, det_b)

'머신러닝' 카테고리의 다른 글

PANDAS  (0) 2026.01.08
SEABORN  (0) 2026.01.07
MATPLOTLIB  (0) 2026.01.07
넘파이(NumPy) 2  (0) 2026.01.07
파이참 설치  (0) 2026.01.05