넘파이에 대한 설명
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. 넘파이가 빠른 이유 (정리)
- 연속된 메모리 할당: CPU 캐시 적중률(Cache Hit Rate)이 높아짐.
- 벡터화(Vectorization): C로 구현되어 있어 반복문 없이 CPU의 SIMD(Single Instruction Multiple Data) 기능을 활용해 데이터를 한꺼번에 처리함.
- 타입 고정: 파이썬 리스트와 달리 모든 요소의 데이터 타입이 동일하므로, 매번 타입을 확인할 필요가 없음.
파이참에서 넘파이 설치
상단 파일 > 설정 > 인터프리터로 가서 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)
파이썬 리스트의 연산
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 |