이미지 로딩 중...

NumPy 배열 생성 및 인덱싱 완벽 가이드 - 슬라이드 1/11
A

AI Generated

2025. 11. 21. · 11 Views

NumPy 배열 생성 및 인덱싱 완벽 가이드

NumPy 배열을 처음부터 만들고, 원하는 데이터를 정확히 꺼내는 방법을 배웁니다. 실무에서 자주 쓰이는 배열 생성 방법과 인덱싱 기법을 초급자도 이해할 수 있게 쉽게 설명합니다.


목차

  1. 기본_배열_생성하기
  2. 인덱싱으로_원소_접근하기
  3. 슬라이싱으로_범위_선택하기
  4. 불린_인덱싱으로_조건_필터링
  5. 팬시_인덱싱으로_임의_위치_선택
  6. arange와_linspace로_규칙적인_배열_만들기
  7. zeros_ones_empty로_초기화_배열_만들기
  8. eye와_identity로_단위행렬_만들기
  9. reshape로_배열_모양_바꾸기
  10. 다차원_배열의_축_이해하기

1. 기본_배열_생성하기

시작하며

여러분이 데이터 분석 프로젝트를 시작할 때 이런 상황을 겪어본 적 있나요? 100개, 1000개가 넘는 숫자 데이터를 다뤄야 하는데, 파이썬의 기본 리스트로는 계산 속도가 너무 느려서 답답했던 경험 말이에요.

이런 문제는 실제 개발 현장에서 자주 발생합니다. 특히 대량의 숫자 데이터를 처리하거나, 행렬 연산을 해야 할 때 파이썬 기본 리스트는 속도와 편의성 면에서 한계가 있습니다.

게다가 복잡한 수학 연산을 일일이 반복문으로 작성하다 보면 코드도 길어지고 실수하기도 쉽죠. 바로 이럴 때 필요한 것이 NumPy 배열입니다.

NumPy 배열은 파이썬 리스트보다 훨씬 빠르고, 복잡한 수학 연산도 한 줄로 간단히 처리할 수 있어서 데이터 과학의 필수 도구로 자리 잡았습니다.

개요

간단히 말해서, NumPy 배열은 같은 종류의 데이터를 빠르게 저장하고 계산할 수 있는 특별한 상자입니다. NumPy 배열이 필요한 이유는 속도와 편의성 때문입니다.

파이썬 리스트에 백만 개의 숫자를 더하려면 반복문을 돌려야 하지만, NumPy는 한 줄로 끝납니다. 예를 들어, 주식 가격 데이터 1년치(약 250개)의 평균과 표준편차를 계산해야 한다면, NumPy를 사용하면 단 몇 밀리초 만에 결과를 얻을 수 있습니다.

기존에는 for 반복문을 돌면서 하나하나 계산했다면, 이제는 배열 전체에 한 번에 연산을 적용할 수 있습니다. 이것을 '벡터화 연산'이라고 부르는데, 코드가 짧아지고 속도도 10배에서 100배까지 빨라집니다.

NumPy 배열의 핵심 특징은 세 가지입니다. 첫째, 같은 타입의 데이터만 저장해서 메모리를 효율적으로 사용하고, 둘째, C언어로 만들어져서 계산이 매우 빠르며, 셋째, 다차원 배열을 쉽게 다룰 수 있어서 행렬이나 이미지 데이터 처리에 최적입니다.

이러한 특징들이 머신러닝과 데이터 분석의 기초가 되는 이유입니다.

코드 예제

import numpy as np

# 리스트로부터 1차원 배열 만들기
arr1 = np.array([1, 2, 3, 4, 5])
print("1차원 배열:", arr1)

# 2차원 배열 만들기 (행렬처럼 생겼어요)
arr2 = np.array([[1, 2, 3], [4, 5, 6]])
print("2차원 배열:\n", arr2)

# 0부터 9까지 자동으로 생성
arr3 = np.arange(10)
print("0~9까지:", arr3)

# 모든 값이 0인 배열 만들기
zeros = np.zeros((3, 4))
print("0으로 채운 3x4 배열:\n", zeros)

설명

이것이 하는 일: NumPy 배열을 여러 가지 방법으로 만드는 기본 방법들을 보여줍니다. 파이썬 리스트를 배열로 변환하는 것부터, 규칙적인 숫자를 자동으로 생성하는 것까지 실무에서 가장 많이 쓰는 방법들이에요.

첫 번째로, np.array()를 사용하면 기존 리스트를 NumPy 배열로 변환할 수 있습니다. 왜 이렇게 하냐고요?

리스트 상태에서는 +를 하면 리스트가 합쳐지지만, 배열로 바꾸면 +가 각 원소끼리 더해지는 수학 연산이 되기 때문입니다. 실무에서는 CSV 파일을 읽어온 리스트 데이터를 배열로 변환하는 경우가 많습니다.

그 다음으로, np.arange(10)이 실행되면서 0부터 9까지의 숫자를 자동으로 생성합니다. 내부에서는 range()와 비슷하게 작동하지만, 결과가 리스트가 아닌 배열이라는 차이가 있어요.

이 방법은 x축 좌표를 만들거나, 반복 실험의 시행 번호를 매길 때 매우 유용합니다. np.zeros()는 특정 크기의 배열을 만들고 모든 값을 0으로 채웁니다.

(3, 4)라는 튜플을 전달하면 3행 4열의 2차원 배열이 만들어지죠. 마지막으로, 이렇게 만들어진 배열은 이후 실제 데이터로 채워지거나 계산의 기본 틀로 사용됩니다.

예를 들어, 이미지 처리를 할 때 결과를 저장할 빈 배열을 먼저 만들고, 픽셀 값을 하나씩 계산해서 채워 넣는 식이죠. 여러분이 이 코드를 사용하면 다양한 상황에 맞는 배열을 빠르게 만들 수 있습니다.

실무에서의 이점은 첫째, 코드가 간결해지고(반복문 불필요), 둘째, 메모리를 효율적으로 사용하며, 셋째, 배열 생성 후 바로 수학 연산을 적용할 수 있다는 것입니다.

실전 팁

💡 배열을 만들 때 dtype 매개변수로 데이터 타입을 지정하세요. np.array([1, 2, 3], dtype=float)처럼 하면 정수가 실수로 변환되어 나중에 나눗셈할 때 소수점 결과를 얻을 수 있어요.

💡 큰 배열을 만들 때는 np.empty()를 사용하세요. np.zeros()보다 빠른데, 초기화를 하지 않기 때문입니다. 어차피 나중에 값을 다 채울 거라면 empty가 효율적이에요.

💡 np.linspace(0, 10, 50)는 0부터 10까지 50개의 균등한 간격으로 숫자를 만듭니다. 그래프의 x축을 만들거나 균일한 샘플링이 필요할 때 arange보다 정확해요.

💡 2차원 배열을 만들 때 [[1,2],[3,4]] 대신 실수로 [1,2,3,4]를 넣으면 1차원 배열이 됩니다. 꼭 리스트 안에 리스트를 넣어서 차원을 명확히 하세요.

💡 np.ones((3,3)) * 5 처럼 하면 5로 채워진 배열을 간단히 만들 수 있습니다. np.full((3,3), 5)와 같은 결과지만, ones를 쓰는 게 더 직관적일 때가 많아요.


2. 인덱싱으로_원소_접근하기

시작하며

여러분이 학생 100명의 시험 점수가 담긴 배열에서 첫 번째 학생의 점수만 확인하고 싶을 때 어떻게 할까요? 또는 특정 위치의 데이터만 골라내서 분석해야 하는 상황이 생기곤 합니다.

이런 문제는 데이터 분석에서 기본 중의 기본입니다. 전체 데이터 중에서 필요한 부분만 선택하지 못하면, 불필요한 계산을 하게 되고 결과도 원하는 대로 나오지 않습니다.

파이썬 리스트의 인덱싱을 알고 있다면 90% 비슷하지만, 2차원 이상에서는 조금 다른 방식이 필요해요. 바로 이럴 때 필요한 것이 NumPy의 인덱싱입니다.

대괄호 안에 숫자나 범위를 넣어서 정확히 원하는 원소나 영역을 가져올 수 있죠. 1차원은 물론이고, 2차원 배열에서 특정 행이나 열, 심지어 특정 위치의 값만 딱 집어낼 수 있습니다.

개요

간단히 말해서, 인덱싱은 배열에서 원하는 위치의 데이터를 꺼내는 방법입니다. 왜 이 개념이 필요한지 실무 관점에서 보면, 데이터의 일부만 확인하거나 수정할 때 매우 자주 사용됩니다.

예를 들어, 이미지 데이터에서 특정 픽셀의 RGB 값을 확인하거나, 센서 데이터에서 특정 시간대의 측정값만 추출하는 경우에 필수적입니다. 전체 데이터를 복사하지 않고도 원하는 부분만 빠르게 접근할 수 있어서 메모리와 시간을 절약합니다.

기존에는 반복문으로 배열을 순회하면서 조건에 맞는 값을 찾았다면, 이제는 인덱스 번호만으로 직접 접근할 수 있습니다. NumPy 인덱싱의 핵심 특징은 첫째, 0부터 시작한다는 것(첫 번째 원소는 [0]), 둘째, 음수 인덱스로 뒤에서부터 접근 가능([-1]은 마지막 원소), 셋째, 2차원 이상에서는 [행, 열] 형식으로 쉼표로 구분한다는 것입니다.

이러한 특징들이 복잡한 다차원 데이터를 직관적으로 다룰 수 있게 해줍니다.

코드 예제

import numpy as np

# 1차원 배열 인덱싱
arr = np.array([10, 20, 30, 40, 50])
print("첫 번째 원소:", arr[0])  # 10
print("마지막 원소:", arr[-1])  # 50

# 2차원 배열 인덱싱
matrix = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
print("1행 2열 값:", matrix[0, 1])  # 2
print("3행 3열 값:", matrix[2, 2])  # 9

# 특정 행 전체 가져오기
print("2행 전체:", matrix[1])  # [4, 5, 6]

# 값 수정하기
arr[0] = 100
print("수정 후:", arr)

설명

이것이 하는 일: 배열의 특정 위치에 있는 데이터를 가져오거나 수정하는 방법을 보여줍니다. 파이썬 리스트와 비슷하지만, 다차원 배열에서 더 강력한 기능을 제공해요.

첫 번째로, arr[0]은 배열의 첫 번째 원소를 가져옵니다. 왜 0부터 시작하냐면, 컴퓨터 과학에서는 전통적으로 시작 위치를 0으로 세기 때문이에요.

arr[-1]처럼 음수를 쓰면 뒤에서부터 세는데, 마지막 원소의 인덱스를 계산하지 않아도 되어 편리합니다. 실무에서는 최신 데이터나 마지막 결과를 빠르게 확인할 때 자주 씁니다.

그 다음으로, matrix[0, 1]이 실행되면서 0행 1열의 값인 2를 가져옵니다. 내부에서는 행 번호와 열 번호를 쉼표로 구분해서 2차원 위치를 정확히 지정하는 거예요.

주의할 점은 matrix[0][1]처럼 대괄호를 두 번 쓸 수도 있지만, [0, 1] 방식이 더 빠르고 NumPy가 권장하는 방법입니다. matrix[1]처럼 행 번호만 쓰면 그 행 전체가 1차원 배열로 반환됩니다.

이건 2행의 모든 열 데이터를 한 번에 가져오는 거죠. 마지막으로, arr[0] = 100처럼 인덱싱으로 접근한 후 값을 할당하면 원본 배열의 해당 위치가 바로 수정됩니다.

이게 중요한 이유는 원본을 직접 수정하기 때문에 메모리를 절약하지만, 의도치 않게 원본이 바뀔 수 있어 주의가 필요합니다. 여러분이 이 코드를 사용하면 대량의 데이터에서 필요한 부분만 빠르게 찾아 확인하거나 수정할 수 있습니다.

실무에서의 이점은 첫째, 반복문 없이 직접 접근으로 속도가 빠르고, 둘째, 코드가 직관적이어서 가독성이 좋으며, 셋째, 복잡한 다차원 데이터도 [행,열,깊이] 식으로 명확하게 다룰 수 있다는 것입니다.

실전 팁

💡 2차원 배열에서 특정 열 전체를 가져오려면 matrix[:, 1]처럼 콜론을 사용하세요. 모든 행의 1열만 선택하는 강력한 방법입니다.

💡 인덱싱으로 접근한 값을 수정하면 원본이 바뀝니다. 원본을 보존하려면 arr.copy()로 복사본을 만든 후 작업하세요.

💡 배열의 크기를 모를 때는 arr[len(arr)-1] 대신 arr[-1]을 쓰세요. 더 짧고 실수할 일도 없습니다.

💡 존재하지 않는 인덱스에 접근하면 IndexError가 발생합니다. 특히 반복문에서 range(len(arr)) 대신 for item in arr을 사용하면 이런 오류를 방지할 수 있어요.


3. 슬라이싱으로_범위_선택하기

시작하며

여러분이 1000개의 데이터 중에서 처음 100개만 미리보기로 확인하고 싶거나, 중간의 특정 구간만 분석하고 싶을 때가 있죠? 하나씩 인덱싱하면 너무 번거롭고, 반복문을 쓰자니 코드가 길어집니다.

이런 문제는 데이터 전처리와 분석에서 정말 자주 발생합니다. 시계열 데이터에서 특정 기간만 추출하거나, 이미지의 특정 영역만 자르거나, 학습 데이터를 훈련/검증 세트로 나눌 때 범위 선택이 필수적입니다.

일일이 반복문으로 복사하면 시간도 오래 걸리고 코드도 복잡해져요. 바로 이럴 때 필요한 것이 슬라이싱입니다.

시작:끝:간격 형식으로 원하는 범위를 한 줄로 깔끔하게 선택할 수 있어서, 데이터 처리의 생산성이 극적으로 올라갑니다.

개요

간단히 말해서, 슬라이싱은 배열의 일부 구간을 통째로 잘라내는 방법입니다. 슬라이싱이 필요한 이유는 데이터의 부분 집합을 빠르게 추출하기 위해서입니다.

하나의 값이 아니라 연속된 여러 값을 한 번에 가져올 수 있어요. 예를 들어, 주식 데이터 1년치 중에서 최근 1개월(약 20거래일)의 데이터만 분석하고 싶다면, 슬라이싱으로 [-20:]만 써주면 끝납니다.

복잡한 반복문이나 조건문이 전혀 필요 없죠. 기존에는 for 문으로 원하는 범위를 순회하며 새 리스트에 append 했다면, 이제는 [시작:끝] 한 줄로 같은 결과를 얻을 수 있습니다.

슬라이싱의 핵심 특징은 첫째, [start:stop:step] 형식을 사용하고(stop은 포함 안 됨), 둘째, 생략 가능([:5]는 처음부터 5개, [5:]는 5번째부터 끝까지), 셋째, 음수 인덱스와 음수 스텝 사용 가능([::-1]로 역순), 넷째, 원본의 뷰(view)를 반환해서 메모리 효율적이라는 것입니다. 이러한 특징들이 대용량 데이터를 다룰 때 필수적인 이유입니다.

코드 예제

import numpy as np

arr = np.array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

# 기본 슬라이싱: 인덱스 2부터 5 전까지 (2, 3, 4)
print("arr[2:5]:", arr[2:5])

# 처음부터 5개
print("arr[:5]:", arr[:5])

# 5번째부터 끝까지
print("arr[5:]:", arr[5:])

# 2칸씩 건너뛰며 선택
print("arr[::2]:", arr[::2])  # 0, 2, 4, 6, 8

# 역순으로 뒤집기
print("arr[::-1]:", arr[::-1])

# 2차원 슬라이싱: 처음 2행, 처음 2열
matrix = np.array([[1,2,3], [4,5,6], [7,8,9]])
print("matrix[:2, :2]:\n", matrix[:2, :2])

설명

이것이 하는 일: 배열의 연속된 구간을 효율적으로 추출합니다. 하나의 값이 아니라 여러 값을 포함하는 부분 배열을 만드는 거예요.

첫 번째로, arr[2:5]는 인덱스 2부터 5 '전까지'의 원소를 가져옵니다. 왜 5가 포함 안 되냐면, 프로그래밍에서는 끝 지점을 '개수'로 생각하기 때문이에요.

결과는 [2, 3, 4]로 총 3개의 원소가 나옵니다. 실무에서는 "n번째부터 m개를 가져와"라고 생각하면 편해요.

arr[2:5]는 2번 인덱스부터 3개(5-2=3)를 가져오는 거죠. 그 다음으로, arr[:5]처럼 시작을 생략하면 자동으로 처음부터 시작합니다.

내부에서는 0:5로 해석되어 처음 5개 원소를 반환해요. 반대로 arr[5:]는 5번 인덱스부터 끝까지 모두 가져옵니다.

데이터 미리보기나, 훈련/테스트 세트 분리할 때 이런 방식을 자주 씁니다. arr[::2]는 시작과 끝을 생략하고 스텝만 2로 지정한 겁니다.

처음부터 끝까지 2칸씩 건너뛰면서 선택하는 거죠. 결과는 [0, 2, 4, 6, 8]처럼 짝수 인덱스의 원소들만 나옵니다.

arr[::-1]은 스텝을 -1로 주어 역순으로 배열을 뒤집는 트릭인데, 시계열 데이터를 과거부터 최신순으로 바꿀 때 유용해요. 마지막으로, 2차원 배열에서 matrix[:2, :2]는 "처음 2행, 처음 2열"의 교집합 영역을 선택합니다.

쉼표 앞이 행 슬라이싱, 뒤가 열 슬라이싱이에요. 결과는 2x2 크기의 부분 배열 [[1,2], [4,5]]가 됩니다.

이미지를 자르거나 데이터의 일부 특성만 선택할 때 이런 2차원 슬라이싱이 핵심입니다. 여러분이 이 코드를 사용하면 원하는 구간의 데이터를 순식간에 추출할 수 있습니다.

실무에서의 이점은 첫째, 반복문 없이 한 줄로 처리되어 코드가 간결하고, 둘째, NumPy가 내부적으로 최적화해서 속도가 빠르며, 셋째, 원본 데이터를 복사하지 않고 뷰를 만들어 메모리를 절약한다는 것입니다.

실전 팁

💡 슬라이싱 결과는 원본의 뷰(view)입니다. 슬라이스한 배열을 수정하면 원본도 바뀌므로, 독립된 복사본이 필요하면 .copy()를 꼭 사용하세요.

💡 arr[:-1]은 마지막 원소만 제외한 전체를 가져옵니다. 데이터에서 이상치인 마지막 값을 빼고 분석할 때 유용해요.

💡 간격(step)을 음수로 주면 역방향으로 슬라이싱됩니다. arr[5:2:-1]은 5번부터 3번까지 역순으로 가져와요.

💡 2차원에서 matrix[:, 1:3]은 "모든 행의 1~2열"을 선택합니다. 콜론(:)만 쓰면 "전체"를 의미해서, 특정 차원 전체를 가져올 때 편리해요.

💡 빈 슬라이스 arr[5:2]처럼 시작이 끝보다 크면 빈 배열이 반환됩니다. 오류가 아니니 조건문으로 체크할 필요 없어요.


4. 불린_인덱싱으로_조건_필터링

시작하며

여러분이 학생 100명의 점수 배열에서 80점 이상인 학생만 찾고 싶거나, 센서 데이터에서 이상치(너무 크거나 작은 값)를 제거해야 할 때가 있죠? for 문으로 하나씩 확인하면 되지만, 배열이 커질수록 코드가 복잡해지고 느려집니다.

이런 문제는 실전 데이터 처리에서 거의 매번 발생합니다. 조건에 맞는 데이터만 선별하는 필터링 작업은 데이터 정제의 핵심이에요.

예를 들어, 주가 데이터에서 급등락한 날만 찾거나, 고객 데이터에서 특정 연령대만 추출하는 작업 말이죠. 전통적인 방법으로는 if 문과 반복문을 중첩해서 써야 하는데, 코드도 길고 실수하기 쉽습니다.

바로 이럴 때 필요한 것이 불린 인덱싱입니다. 조건식을 배열에 직접 적용하면 True/False 배열이 나오고, 이걸 인덱스로 쓰면 True인 위치의 값만 자동으로 필터링됩니다.

단 한 줄로 복잡한 조건 필터링이 끝나는 마법 같은 기능이에요.

개요

간단히 말해서, 불린 인덱싱은 True/False 배열을 인덱스로 사용해서 조건에 맞는 원소만 선택하는 방법입니다. 이 개념이 필요한 이유는 조건부 데이터 선택을 간결하고 빠르게 하기 위해서입니다.

SQL의 WHERE 절처럼 "이 조건을 만족하는 데이터만 줘"를 배열에서 구현하는 거예요. 예를 들어, 기온 데이터 1년치에서 영하인 날만 세거나, 판매 데이터에서 목표치를 달성한 날만 분석하는 경우에 매우 자주 사용됩니다.

파이썬 리스트 컴프리헨션보다 빠르고, 코드도 더 직관적이죠. 기존에는 result = [x for x in arr if x > 80] 같은 리스트 컴프리헨션을 썼다면, 이제는 result = arr[arr > 80] 한 줄로 같은 결과를 얻을 수 있습니다.

불린 인덱싱의 핵심 특징은 첫째, 비교 연산자(>, <, ==, !=)를 배열에 직접 사용하면 같은 크기의 불린 배열 반환, 둘째, 그 불린 배열을 인덱스로 쓰면 True 위치만 선택, 셋째, & (and), | (or), ~ (not) 연산자로 복잡한 조건 조합 가능, 넷째, where(), any(), all() 같은 함수와 함께 더 강력한 필터링 가능하다는 것입니다. 이러한 특징들이 복잡한 데이터 정제를 쉽게 만들어줍니다.

코드 예제

import numpy as np

# 점수 배열
scores = np.array([45, 78, 92, 67, 88, 55, 95, 72])

# 조건: 80점 이상
condition = scores >= 80
print("80점 이상 여부:", condition)  # [False False True False True False True False]

# 불린 인덱싱: 80점 이상인 점수만 선택
high_scores = scores[condition]
print("80점 이상 점수:", high_scores)  # [92 88 95]

# 한 줄로 간결하게
print("80점 이상:", scores[scores >= 80])

# 복합 조건: 70점 이상 90점 미만
medium = scores[(scores >= 70) & (scores < 90)]
print("70-90점:", medium)  # [78 88 72]

# 조건에 맞는 값 수정
scores[scores < 60] = 60  # 60점 미만을 60점으로 상향
print("수정 후:", scores)

설명

이것이 하는 일: 배열 전체에 조건을 적용해서, 조건을 만족하는 원소만 자동으로 골라냅니다. 반복문 없이도 복잡한 필터링을 순식간에 처리할 수 있어요.

첫 번째로, scores >= 80을 실행하면 배열의 각 원소를 80과 비교해서 불린(True/False) 배열을 만듭니다. 왜 이렇게 하냐면, NumPy는 벡터화 연산을 지원해서 하나의 비교 연산이 전체 배열에 자동으로 적용되기 때문이에요.

결과는 [False, False, True, False, True, False, True, False]처럼 각 위치의 조건 만족 여부를 담은 배열입니다. 이게 핵심인데, 배열의 크기와 불린 배열의 크기가 정확히 일치해야 해요.

그 다음으로, scores[condition]이 실행되면서 condition이 True인 위치의 값만 추출합니다. 내부에서는 NumPy가 불린 배열을 순회하며 True인 인덱스의 원소만 모아서 새 배열을 만드는 거죠.

결과는 [92, 88, 95]로, 원래 배열에서 80 이상이었던 값들만 남습니다. 이 방식이 강력한 이유는 어떤 복잡한 조건이든 한 줄로 처리할 수 있다는 거예요.

물론 condition 변수 없이 scores[scores >= 80]처럼 한 줄로 쓸 수도 있습니다. 코드가 더 간결해지죠.

(scores >= 70) & (scores < 90)처럼 괄호로 묶어서 & (and) 연산자를 쓰면 복잡한 조건도 표현 가능합니다. 주의할 점은 파이썬의 and가 아니라 &를 써야 하고, 각 조건을 꼭 괄호로 감싸야 한다는 거예요.

마지막으로, scores[scores < 60] = 60처럼 불린 인덱싱으로 선택한 부분에 값을 할당하면 조건부 수정이 가능합니다. 60점 미만인 모든 점수를 찾아서 60으로 바꾸는 거죠.

이건 이상치 처리나 데이터 정규화에서 엄청나게 유용한 패턴입니다. for 문으로 하려면 인덱스를 추적하면서 복잡하게 써야 하는데, 불린 인덱싱은 단 한 줄로 끝나요.

여러분이 이 코드를 사용하면 조건에 맞는 데이터를 찾거나 수정하는 작업을 극도로 단순화할 수 있습니다. 실무에서의 이점은 첫째, 반복문과 if 문 없이 선언적으로 작성되어 코드가 읽기 쉽고, 둘째, NumPy가 C 레벨에서 최적화해서 대용량 데이터도 빠르게 처리하며, 셋째, 실수로 인덱스를 잘못 계산할 일이 없어 버그가 줄어든다는 것입니다.

실전 팁

💡 복합 조건에서는 and/or 대신 &/|를 쓰고 각 조건을 괄호로 감싸세요. (arr > 10) & (arr < 20)처럼요. 괄호를 안 쓰면 연산자 우선순위 때문에 오류가 납니다.

💡 np.where(condition, x, y)를 쓰면 조건이 True면 x, False면 y를 선택합니다. 삼항 연산자의 배열 버전이라고 생각하면 돼요.

💡 arr[~(arr > 80)]처럼 ~(틸드)는 not을 의미합니다. 80 이하를 선택하려면 arr <= 80 대신 ~(arr > 80)로도 쓸 수 있어요.

💡 불린 인덱싱 결과는 원본의 복사본입니다. 수정해도 원본에 영향 없지만, arr[condition] = value 형태로 직접 할당하면 원본이 바뀌니 주의하세요.

💡 arr.any()는 하나라도 True면 True, arr.all()은 모두 True여야 True를 반환합니다. 전체 데이터가 조건을 만족하는지 빠르게 확인할 때 유용해요.


5. 팬시_인덱싱으로_임의_위치_선택

시작하며

여러분이 학생 50명 중에서 1등, 10등, 25등, 50등의 점수만 골라서 비교하고 싶다면 어떻게 할까요? 각각 인덱싱하면 arr[0], arr[9], arr[24], arr[49]처럼 네 번 반복해야 하고, 코드도 지저분해집니다.

이런 문제는 불규칙한 패턴의 데이터를 선택할 때 항상 발생합니다. 슬라이싱은 연속된 구간만 가능하고, 불린 인덱싱은 조건 기반이라 특정 위치들을 직접 지정하기 어려워요.

예를 들어, 무작위로 샘플링한 인덱스 리스트가 있을 때, 그 위치들의 데이터만 한 번에 가져올 방법이 필요합니다. 바로 이럴 때 필요한 것이 팬시 인덱싱입니다.

인덱스 번호들을 리스트나 배열로 만들어서 한 번에 넘겨주면, 그 위치들의 값을 모아서 배열로 반환해줍니다. 여러 번 접근할 필요 없이 단 한 번의 인덱싱으로 끝나는 편리한 방법이죠.

개요

간단히 말해서, 팬시 인덱싱은 인덱스 번호들의 리스트나 배열을 사용해서 원하는 여러 위치의 값을 한 번에 선택하는 방법입니다. 팬시 인덱싱이 필요한 이유는 불규칙한 패턴의 데이터를 효율적으로 추출하기 위해서입니다.

머신러닝에서 무작위로 뽑은 샘플의 인덱스가 [3, 7, 15, 28, 42]처럼 주어졌을 때, 이 위치들의 데이터를 한 번에 가져와야 하는 경우가 많아요. 예를 들어, K-fold 교차 검증에서 특정 폴드의 인덱스로 데이터를 나누거나, 랜덤 샘플링 결과를 배열로 만들 때 필수적입니다.

for 문으로 하나씩 모으는 것보다 훨씬 빠르고 간결하죠. 기존에는 result = [arr[i] for i in indices]처럼 리스트 컴프리헨션을 썼다면, 이제는 result = arr[indices] 한 줄로 같은 결과를 얻을 수 있습니다.

팬시 인덱싱의 핵심 특징은 첫째, 리스트나 배열을 인덱스로 사용, 둘째, 인덱스의 순서대로 결과 배열이 구성됨, 셋째, 같은 인덱스를 여러 번 넣으면 중복해서 가져올 수 있음, 넷째, 2차원 이상에서는 행과 열 인덱스를 각각 배열로 지정 가능하다는 것입니다. 이러한 특징들이 복잡한 데이터 재배치나 샘플링 작업을 단순화합니다.

코드 예제

import numpy as np

arr = np.array([10, 20, 30, 40, 50, 60, 70, 80, 90, 100])

# 인덱스 리스트로 여러 위치 선택
indices = [0, 3, 5, 9]
selected = arr[indices]
print("선택된 값:", selected)  # [10, 40, 60, 100]

# 순서를 바꿔서 선택
reversed_indices = [9, 5, 3, 0]
print("역순 선택:", arr[reversed_indices])  # [100, 60, 40, 10]

# 중복 선택 가능
duplicates = [0, 0, 1, 1]
print("중복 선택:", arr[duplicates])  # [10, 10, 20, 20]

# 2차원 팬시 인덱싱
matrix = np.array([[1,2,3], [4,5,6], [7,8,9]])
rows = [0, 2, 2]  # 0행, 2행, 2행
cols = [0, 1, 2]  # 0열, 1열, 2열
print("대각선+α:", matrix[rows, cols])  # [1, 8, 9]

설명

이것이 하는 일: 원하는 위치들의 인덱스를 배열이나 리스트로 만들어서 한 번에 넘기면, 그 위치들의 값을 모아서 새 배열로 만들어줍니다. 순서를 바꾸거나 중복해서 선택하는 것도 자유롭죠.

첫 번째로, arr[indices]에서 indices가 [0, 3, 5, 9]라면 각 인덱스 위치의 값을 순서대로 가져옵니다. 왜 이게 편리하냐면, 반복문 없이 한 번에 여러 위치를 접근할 수 있기 때문이에요.

내부적으로는 NumPy가 indices를 순회하며 각 위치의 값을 모으지만, 사용자 입장에서는 단 한 줄로 끝납니다. 결과는 [10, 40, 60, 100]처럼 지정한 인덱스 순서대로 값이 나열된 배열이에요.

그 다음으로, 인덱스 순서를 [9, 5, 3, 0]처럼 바꾸면 결과도 [100, 60, 40, 10]으로 순서가 바뀝니다. 이게 중요한 이유는 데이터를 원하는 순서로 재배치할 수 있다는 거예요.

예를 들어, 정렬된 인덱스를 이용해 원본 데이터를 정렬하거나, 무작위 인덱스로 데이터를 셔플할 때 이 방식을 씁니다. duplicates = [0, 0, 1, 1]처럼 같은 인덱스를 여러 번 넣으면 해당 값이 중복해서 나옵니다.

슬라이싱이나 일반 인덱싱으로는 불가능한 기능이에요. 데이터 증강(augmentation)이나 부트스트랩 샘플링처럼 같은 데이터를 중복해서 뽑아야 할 때 유용합니다.

마지막으로, 2차원 배열에서 matrix[rows, cols]는 (rows[i], cols[i]) 위치의 값들을 선택합니다. rows=[0,2,2], cols=[0,1,2]면 (0,0), (2,1), (2,2) 위치, 즉 [1, 8, 9]를 가져와요.

이건 대각선이나 특정 패턴의 원소들을 골라낼 때 매우 강력합니다. 주의할 점은 rows와 cols의 길이가 같아야 한다는 거예요.

여러분이 이 코드를 사용하면 불규칙한 패턴의 데이터를 자유자재로 선택하고 재배치할 수 있습니다. 실무에서의 이점은 첫째, 머신러닝 샘플링이나 데이터 셔플에 바로 적용 가능하고, 둘째, 반복문 없이 선언적으로 작성되어 의도가 명확하며, 셋째, NumPy의 내부 최적화 덕분에 대량 데이터도 빠르게 처리된다는 것입니다.

실전 팁

💡 팬시 인덱싱 결과는 원본의 복사본입니다. 슬라이싱과 달리 뷰가 아니므로, 결과를 수정해도 원본은 안전해요.

💡 np.random.choice()나 np.random.permutation()으로 랜덤 인덱스를 만들고 팬시 인덱싱하면 무작위 샘플링이 한 줄로 끝납니다.

💡 2차원에서 matrix[[0,2]]처럼 행 인덱스만 주면 해당 행들 전체가 선택됩니다. matrix[[0,2], :]와 같은 의미예요.

💡 argsort()와 함께 쓰면 강력합니다. sorted_indices = arr.argsort() 후 arr[sorted_indices]하면 정렬된 배열을 얻어요.

💡 불린 인덱싱과 팬시 인덱싱을 혼합할 수도 있습니다. arr[condition][indices]처럼 연쇄해서 쓸 수 있지만, 읽기 어려우니 나눠서 쓰는 게 좋아요.


6. arange와_linspace로_규칙적인_배열_만들기

시작하며

여러분이 그래프를 그리기 위해 x축 좌표를 0부터 100까지 균등하게 나눈 점들이 필요하다면 어떻게 할까요? 리스트 컴프리헨션으로 만들 수도 있지만, 간격 계산이 복잡하고 실수하기 쉽습니다.

이런 문제는 데이터 시각화나 시뮬레이션에서 자주 발생합니다. 규칙적인 숫자 시퀀스가 필요한데, 직접 타이핑하기엔 너무 길고 range()는 정수만 되고 간격 조절이 불편해요.

특히 과학 계산에서 "0부터 10까지 100개의 점"처럼 개수 기반으로 균등 분할이 필요할 때가 많은데, 이걸 직접 계산하면 오차가 생기기 쉽습니다. 바로 이럴 때 필요한 것이 arange와 linspace입니다.

arange는 범위와 간격으로, linspace는 범위와 개수로 배열을 만들어서, 상황에 맞게 선택해서 쓸 수 있습니다. 둘 다 한 줄로 깔끔한 숫자 배열을 생성해줘요.

개요

간단히 말해서, arange는 시작부터 끝까지 일정 간격으로 숫자를 만들고, linspace는 시작부터 끝까지 지정한 개수만큼 균등하게 나눈 숫자를 만듭니다. 이 두 함수가 필요한 이유는 규칙적인 숫자 시퀀스 생성이 데이터 과학의 기본이기 때문입니다.

arange는 파이썬 range()의 강화판으로 실수 간격도 지원해서, 0.1씩 증가하는 배열도 쉽게 만들어요. linspace는 과학 계산에서 필수인데, "정확히 N개의 점으로 구간을 나눠줘"라고 할 때 쓰입니다.

예를 들어, 함수 그래프를 그릴 때 x축을 100개 점으로 나누거나, 애니메이션의 프레임 시간을 균등하게 나눌 때 linspace가 완벽합니다. 기존에는 [i * 0.1 for i in range(100)]처럼 리스트 컴프리헨션으로 만들었다면, 이제는 np.arange(0, 10, 0.1)로 더 명확하고 빠르게 만들 수 있습니다.

arange와 linspace의 핵심 특징은 첫째, arange(start, stop, step)는 stop을 포함하지 않고 step 간격으로 증가, 둘째, linspace(start, stop, num)는 stop을 포함하고 num개의 점으로 균등 분할, 셋째, 둘 다 dtype을 지정해서 정수/실수 선택 가능, 넷째, linspace는 간격을 자동 계산해서 부동소수점 오차가 적다는 것입니다. 이러한 특징들이 정확한 숫자 배열 생성을 보장합니다.

코드 예제

import numpy as np

# arange: 간격 기반 (끝 값 미포함)
arr1 = np.arange(0, 10, 2)  # 0부터 2씩 증가, 10 미포함
print("arange(0, 10, 2):", arr1)  # [0 2 4 6 8]

# arange with float step
arr2 = np.arange(0, 1, 0.2)  # 실수 간격도 가능
print("arange(0, 1, 0.2):", arr2)  # [0.  0.2 0.4 0.6 0.8]

# linspace: 개수 기반 (끝 값 포함)
arr3 = np.linspace(0, 10, 5)  # 0부터 10까지 5개
print("linspace(0, 10, 5):", arr3)  # [ 0.   2.5  5.   7.5 10. ]

# linspace로 균등한 100개 점 만들기
x = np.linspace(0, 2*np.pi, 100)  # 그래프용 x축
print("x축 처음 5개:", x[:5])

# endpoint=False 옵션 (끝 값 제외)
arr4 = np.linspace(0, 10, 5, endpoint=False)
print("endpoint=False:", arr4)  # [0. 2. 4. 6. 8.]

설명

이것이 하는 일: 규칙적으로 증가하거나 균등하게 분포된 숫자 배열을 자동으로 생성합니다. 사용자는 범위와 간격(또는 개수)만 지정하면 돼요.

첫 번째로, np.arange(0, 10, 2)는 0부터 시작해서 2씩 더하면서 10 미만까지 숫자를 만듭니다. 왜 10이 포함 안 되냐면, 파이썬 range()처럼 끝 값은 제외하는 게 관례이기 때문이에요.

결과는 [0, 2, 4, 6, 8]로 총 5개가 나오죠. 실무에서는 인덱스 생성이나 정수 시퀀스가 필요할 때 자주 씁니다.

장점은 파이썬 range()와 사용법이 비슷해서 익숙하고, 실수 간격도 지원한다는 거예요. 그 다음으로, np.arange(0, 1, 0.2)처럼 step을 0.2로 주면 실수 간격으로 배열이 생성됩니다.

내부에서는 0에 0.2를 계속 더하면서 1 미만까지 만드는데, 부동소수점 오차 때문에 가끔 예상과 다른 개수가 나올 수 있어요. 예를 들어, 0.1씩 10번 더하면 정확히 1.0이 안 될 수도 있죠.

그래서 정확한 개수가 중요하면 linspace를 쓰는 게 안전합니다. np.linspace(0, 10, 5)는 0과 10 사이를 정확히 5개 점으로 균등하게 나눕니다.

간격을 자동으로 계산해서 [0, 2.5, 5, 7.5, 10]을 만들어줘요. arange와 달리 끝 값 10이 포함됩니다.

이게 중요한 이유는 과학 계산에서 "시작과 끝 모두 포함한 N개의 점"이 필요한 경우가 많기 때문이에요. 그래프를 그릴 때 x축이 정확히 시작점과 끝점을 포함해야 하는 경우가 대표적입니다.

마지막으로, np.linspace(0, 2*np.pi, 100)처럼 하면 삼각함수 그래프를 그릴 완벽한 x축 좌표가 만들어집니다. 0부터 2π까지 100개로 나누면 각 점 사이 간격이 자동으로 2π/99로 계산되어 균등하게 분포해요.

endpoint=False 옵션을 주면 끝 값을 제외해서 arange처럼 동작하는데, 주기 함수에서 끝점이 시작점과 겹치는 걸 방지할 때 유용합니다. 여러분이 이 코드를 사용하면 복잡한 반복문 없이 깔끔한 숫자 배열을 순식간에 만들 수 있습니다.

실무에서의 이점은 첫째, arange는 간격 중심이라 반복 횟수가 명확할 때 좋고, 둘째, linspace는 개수 중심이라 정확한 샘플링에 적합하며, 셋째, 둘 다 부동소수점 연산을 내부에서 처리해서 사용자가 오차 걱정 없이 쓸 수 있다는 것입니다.

실전 팁

💡 실수 간격으로 arange를 쓸 때는 개수가 예상과 다를 수 있어요. 정확한 개수가 필요하면 무조건 linspace를 쓰세요.

💡 linspace에서 retstep=True 옵션을 주면 (배열, 간격)을 튜플로 반환합니다. arr, step = np.linspace(0, 10, 5, retstep=True)처럼 간격을 알고 싶을 때 편해요.

💡 arange(10)처럼 하나만 쓰면 0부터 10 미만까지(즉, 0~9)를 의미합니다. range()와 완전히 똑같아요.

💡 그래프 x축은 보통 linspace를 씁니다. 점 개수가 명확하고 시작/끝을 모두 포함해야 하기 때문이죠.

💡 큰 배열을 만들 때 step이 너무 작으면 메모리를 많이 먹습니다. np.arange(0, 1e9, 0.001)처럼 쓰면 수십억 개 원소가 생겨서 메모리가 부족할 수 있어요.


7. zeros_ones_empty로_초기화_배열_만들기

시작하며

여러분이 데이터를 저장할 빈 공간이 필요한데, 크기는 정해졌지만 아직 값을 모르는 상황이 있죠? 예를 들어, 반복문으로 계산한 결과를 차곡차곡 쌓아야 하는데, 파이썬 리스트에 append하면 속도가 느려서 답답한 경험 말이에요.

이런 문제는 시뮬레이션이나 반복 계산에서 자주 발생합니다. 미리 크기를 알 때는 빈 배열을 만들어두고 인덱스로 직접 할당하는 게 append보다 10배 이상 빠릅니다.

하지만 어떤 값으로 초기화해야 할지 고민되죠. 0이 필요할 때도 있고, 1이 필요할 때도 있고, 아예 초기화가 필요 없을 때도 있으니까요.

바로 이럴 때 필요한 것이 zeros, ones, empty 함수들입니다. 원하는 크기와 초기값을 지정해서 배열을 빠르게 만들고, 나중에 실제 계산 결과로 채워 넣을 수 있습니다.

상황에 맞는 초기화 방법을 선택하면 코드도 명확하고 성능도 좋아져요.

개요

간단히 말해서, zeros는 0으로 채워진 배열, ones는 1로 채워진 배열, empty는 초기화하지 않은(쓰레기 값) 배열을 만듭니다. 이 함수들이 필요한 이유는 미리 크기를 정해서 배열을 만들면 성능이 좋기 때문입니다.

파이썬 리스트는 append할 때마다 메모리를 재할당하는데, NumPy 배열은 처음부터 필요한 공간을 확보해서 인덱스로 직접 쓸 수 있어요. 예를 들어, 10000번 반복하는 시뮬레이션 결과를 저장할 때, 빈 리스트에 append하는 것보다 미리 10000 크기의 배열을 만들고 인덱스로 채우는 게 훨씬 빠릅니다.

zeros는 카운터나 누적 합 초기화에, ones는 곱셈 누적이나 마스크 초기화에, empty는 어차피 전부 덮어쓸 거라 초기값이 필요 없을 때 씁니다. 기존에는 result = []로 시작해서 result.append(value)를 반복했다면, 이제는 result = np.zeros(size)로 만들고 result[i] = value로 직접 할당할 수 있습니다.

초기화 함수들의 핵심 특징은 첫째, 함수 이름이 초기값을 직관적으로 표현(zeros=0, ones=1, empty=초기화 안 함), 둘째, 크기를 튜플로 전달해서 다차원 배열 쉽게 생성, 셋째, dtype 매개변수로 데이터 타입 지정 가능(정수/실수/불린 등), 넷째, empty가 가장 빠르지만 예측 불가능한 값이라 주의 필요하다는 것입니다. 이러한 특징들이 효율적인 메모리 사용과 빠른 배열 초기화를 가능하게 합니다.

코드 예제

import numpy as np

# 0으로 채운 1차원 배열
zeros_1d = np.zeros(5)
print("zeros(5):", zeros_1d)  # [0. 0. 0. 0. 0.]

# 0으로 채운 2차원 배열
zeros_2d = np.zeros((3, 4))  # 3행 4열
print("zeros((3,4)):\n", zeros_2d)

# 1로 채운 배열
ones_arr = np.ones((2, 3))
print("ones((2,3)):\n", ones_arr)

# 초기화하지 않은 배열 (가장 빠름, 쓰레기 값)
empty_arr = np.empty((2, 2))
print("empty((2,2)):\n", empty_arr)  # 랜덤한 값

# dtype 지정: 정수형 배열
zeros_int = np.zeros(5, dtype=int)
print("정수형 zeros:", zeros_int)  # [0 0 0 0 0]

# full: 특정 값으로 채우기
fives = np.full((2, 3), 5)
print("full((2,3), 5):\n", fives)

설명

이것이 하는 일: 지정한 크기와 초기값으로 새 배열을 만듭니다. 반복 계산 전에 결과를 저장할 공간을 미리 확보하는 용도로 자주 쓰여요.

첫 번째로, np.zeros(5)는 크기 5의 1차원 배열을 만들고 모든 값을 0.0으로 초기화합니다. 왜 0.0이냐면, 기본 dtype이 float64(실수)이기 때문이에요.

0으로 초기화하는 이유는 덧셈 누적(sum)이나 카운터처럼 0부터 시작해야 하는 계산이 많기 때문입니다. 실무에서는 히스토그램 빈(bin) 초기화나, 반복 계산 결과를 누적할 때 씁니다.

그 다음으로, np.zeros((3, 4))처럼 튜플을 전달하면 3행 4열의 2차원 배열이 만들어집니다. 내부에서는 3*4=12개의 메모리 공간을 연속으로 할당하고 모두 0으로 채우는 거죠.

이미지 처리에서 결과 이미지를 저장할 빈 캔버스를 만들거나, 행렬 연산의 결과를 담을 공간을 준비할 때 유용합니다. np.ones((2, 3))은 모든 값이 1.0인 배열을 만듭니다.

곱셈 누적(product)처럼 1부터 시작해야 하는 계산이나, 초기 가중치를 1로 설정할 때 씁니다. 또는 ones에 곱셈을 해서 np.ones((2,3)) * 5처럼 특정 값으로 채운 배열을 만들 수도 있어요.

np.empty((2, 2))는 초기화를 하지 않고 메모리만 할당합니다. 가장 빠른 방법이지만, 배열에는 이전에 그 메모리 위치에 있던 쓰레기 값이 남아있어 예측할 수 없어요.

그래서 어차피 전체를 다른 값으로 채울 거라면 empty를 써서 초기화 시간을 아낄 수 있습니다. 예를 들어, 반복문에서 result[i] = calculation()처럼 모든 인덱스를 확실히 덮어쓸 거라면 empty가 효율적이죠.

마지막으로, dtype=int를 지정하면 정수형 배열이 됩니다. np.zeros(5, dtype=int)는 [0, 0, 0, 0, 0]을 만들어요(소수점 없음).

np.full((2,3), 5)는 0이나 1이 아닌 특정 값으로 채우고 싶을 때 편리한 함수입니다. 초기 가중치를 특정 값으로 설정하거나, 기본값이 있는 설정 배열을 만들 때 유용해요.

여러분이 이 코드를 사용하면 반복 계산 전에 효율적인 저장 공간을 준비할 수 있습니다. 실무에서의 이점은 첫째, 미리 크기를 정하면 append보다 10배 이상 빠르고, 둘째, 초기값이 명확해서 코드 의도가 분명하며, 셋째, empty를 적절히 쓰면 대용량 배열도 빠르게 만들 수 있다는 것입니다.

실전 팁

💡 반복문에서 결과를 저장할 때는 빈 리스트 대신 미리 zeros나 empty로 배열을 만들고 인덱스로 채우세요. 속도 차이가 엄청납니다.

💡 empty는 초기화 시간을 절약하지만, 혹시라도 일부 인덱스를 빠뜨리면 쓰레기 값이 남아 버그가 생겨요. 확실할 때만 사용하세요.

💡 zeros_like(arr), ones_like(arr)를 쓰면 기존 배열과 같은 shape과 dtype으로 0 또는 1 배열을 만듭니다. 크기를 다시 계산할 필요가 없어요.

💡 dtype을 bool로 지정하면 True/False 배열이 됩니다. np.zeros(5, dtype=bool)은 모두 False인 마스크를 만들어요.

💡 큰 배열(예: 10000x10000)을 만들 때 zeros나 ones는 초기화 시간이 꽤 걸립니다. empty를 쓰면 거의 즉시 만들어지지만, 반드시 전체를 덮어써야 해요.


8. eye와_identity로_단위행렬_만들기

시작하며

여러분이 선형대수 문제를 풀거나 행렬 연산을 할 때, 대각선만 1이고 나머지는 0인 특별한 행렬이 필요한 경우가 있어요. 이걸 단위행렬(Identity Matrix)이라고 부르는데, 직접 만들려면 2차원 배열에 반복문을 돌려야 해서 번거롭습니다.

이런 문제는 머신러닝의 가중치 초기화나, 선형 방정식 풀이, 행렬의 역행렬 계산 같은 수학적 계산에서 자주 발생합니다. 단위행렬은 숫자 1처럼 곱해도 원래 행렬이 그대로 나오는 특성이 있어서, 기본값이나 초기값으로 많이 써요.

하지만 대각선 위치를 계산해서 1을 넣는 코드를 매번 쓰기엔 너무 복잡하죠. 바로 이럴 때 필요한 것이 eye와 identity 함수입니다.

단 한 줄로 완벽한 단위행렬을 만들 수 있어서, 선형대수 계산이 필요한 모든 상황에서 필수적인 도구예요.

개요

간단히 말해서, eye와 identity는 대각선이 1이고 나머지는 0인 정사각 행렬을 만드는 함수들입니다. 단위행렬이 필요한 이유는 행렬 곱셈의 항등원 역할을 하기 때문입니다.

어떤 행렬 A에 단위행렬 I를 곱하면 A 그대로 나오죠(A × I = A). 이 특성이 중요한 이유는 선형 방정식을 풀 때 양변에 단위행렬을 곱해서 정리하거나, 신경망의 초기 가중치를 단위행렬로 설정해서 정보 손실을 막는 등 다양한 용도로 쓰이기 때문이에요.

예를 들어, ResNet 같은 딥러닝 모델에서는 스킵 연결을 단위행렬로 초기화해서 그래디언트 소실을 방지합니다. 기존에는 이중 for 문으로 [i][i] 위치만 1로 설정했다면, 이제는 np.eye(n) 한 줄로 n×n 단위행렬이 바로 만들어집니다.

eye와 identity의 핵심 특징은 첫째, 정사각 행렬(행과 열의 크기가 같음) 생성, 둘째, 대각선(i==j 위치)은 1, 나머지는 0, 셋째, eye는 대각선 위치를 조절할 수 있고 직사각 행렬도 가능, 넷째, identity는 항상 정사각 단위행렬만 생성해서 더 단순하다는 것입니다. 이러한 특징들이 선형대수 계산을 단순화하고 코드 가독성을 높여줍니다.

코드 예제

import numpy as np

# 3x3 단위행렬
identity_3 = np.identity(3)
print("identity(3):\n", identity_3)
# [[1. 0. 0.]
#  [0. 1. 0.]
#  [0. 0. 1.]]

# eye는 identity와 같지만 더 유연
eye_3 = np.eye(3)
print("eye(3):\n", eye_3)

# eye로 직사각 행렬 (4x5)
rect = np.eye(4, 5)
print("eye(4, 5):\n", rect)  # 4행 5열, 대각선만 1

# k 매개변수: 대각선 위치 이동
upper_diag = np.eye(3, k=1)  # 대각선을 한 칸 위로
print("eye(3, k=1):\n", upper_diag)
# [[0. 1. 0.]
#  [0. 0. 1.]
#  [0. 0. 0.]]

# 정수형 단위행렬
identity_int = np.eye(3, dtype=int)
print("정수형 eye(3):\n", identity_int)

설명

이것이 하는 일: 선형대수에서 항등원 역할을 하는 단위행렬을 자동으로 생성합니다. 행렬 연산의 기본이 되는 특수 행렬이에요.

첫 번째로, np.identity(3)은 3×3 크기의 단위행렬을 만듭니다. 왜 대각선만 1이냐면, 이 행렬은 곱셈에서 숫자 1과 같은 역할을 하도록 설계되었기 때문이에요.

수학적으로 A × I = A가 성립하려면 대각선만 1이어야 합니다. 결과는 [[1,0,0], [0,1,0], [0,0,1]]처럼 0행0열, 1행1열, 2행2열만 1이고 나머지는 0인 정사각 행렬이 나옵니다.

그 다음으로, np.eye(3)도 identity(3)과 완전히 같은 결과를 만들지만, eye가 더 강력한 기능을 제공합니다. 내부 동작은 비슷하지만, eye는 추가 매개변수를 받을 수 있어요.

만약 단순히 정사각 단위행렬만 필요하다면 identity를 쓰는 게 의도가 명확하고, 더 복잡한 행렬이 필요하면 eye를 쓰는 게 좋습니다. np.eye(4, 5)처럼 두 번째 매개변수를 주면 4행 5열의 직사각 행렬이 만들어집니다.

대각선(0,0), (1,1), (2,2), (3,3)만 1이고 나머지는 0이에요. 정사각이 아니어도 대각선 개념은 유지되는 거죠.

이건 특정 차원의 데이터를 선택하는 선택 행렬을 만들 때 유용합니다. k 매개변수를 쓰면 대각선 위치를 이동할 수 있습니다.

np.eye(3, k=1)은 대각선을 한 칸 위로 올려서 (0,1), (1,2)만 1이 됩니다. k=-1이면 한 칸 아래로 내려가고요.

이건 상삼각행렬이나 하삼각행렬의 기초를 만들거나, 시계열 데이터에서 한 타임스텝 앞/뒤를 참조하는 행렬을 만들 때 씁니다. 마지막으로, dtype=int를 지정하면 정수형 단위행렬이 됩니다.

기본은 float64인데, 정수 연산만 할 거라면 int로 지정하면 메모리를 절약할 수 있어요. 딥러닝의 마스크 행렬처럼 0과 1만 쓰는 경우 정수형이면 충분하죠.

여러분이 이 코드를 사용하면 복잡한 반복문 없이 선형대수의 기본 도구를 즉시 얻을 수 있습니다. 실무에서의 이점은 첫째, 행렬 연산의 항등원이라는 수학적 의미가 코드에 명확히 드러나고, 둘째, 신경망 가중치 초기화 같은 특수한 용도로 바로 쓸 수 있으며, 셋째, eye의 k 매개변수로 다양한 대각 행렬 패턴을 쉽게 만들 수 있다는 것입니다.

실전 팁

💡 선형 방정식 Ax = b를 풀 때, 단위행렬을 활용하면 역행렬 계산이 쉬워집니다. np.linalg.inv()와 함께 자주 써요.

💡 딥러닝에서 가중치를 초기화할 때 np.eye()로 단위행렬을 쓰면 그래디언트 소실을 줄일 수 있습니다. 특히 Recurrent 네트워크에서 유용해요.

💡 eye(n, k=1)과 eye(n, k=-1)을 더하면 삼중대각행렬을 쉽게 만들 수 있습니다. 수치 미분이나 차분 방정식에서 쓰여요.

💡 identity는 이름이 명확해서 코드 가독성이 좋지만, eye가 더 유연합니다. 보통은 eye를 쓰는 게 범용적이에요.

💡 큰 단위행렬(예: 10000x10000)은 메모리를 많이 먹습니다. sparse matrix(희소 행렬)가 필요하면 scipy.sparse.eye()를 사용하세요.


9. reshape로_배열_모양_바꾸기

시작하며

여러분이 1차원 배열로 데이터를 받았는데, 이걸 2차원 테이블 형태로 바꿔야 한다면 어떻게 할까요? 예를 들어, 12개의 숫자를 3행 4열 행렬로 재배치하거나, 이미지 데이터를 1차원으로 펼쳐서 머신러닝 모델에 넣어야 하는 경우 말이죠.

이런 문제는 데이터 전처리와 머신러닝에서 정말 자주 발생합니다. 데이터의 논리적 구조는 바꿔야 하지만 값 자체는 그대로여야 할 때, 반복문으로 새 배열을 만들면 메모리도 낭비되고 코드도 복잡해집니다.

특히 이미지 데이터는 (높이, 너비, 채널) 형태인데, CNN에 넣으려면 (배치, 채널, 높이, 너비)로 바꿔야 해서 차원 조작이 필수적이에요. 바로 이럴 때 필요한 것이 reshape입니다.

배열의 값은 그대로 두고 차원과 크기만 재배치해서, 다양한 형태로 데이터를 변환할 수 있습니다. 메모리 복사 없이 뷰만 바꾸기 때문에 빠르고 효율적이죠.

개요

간단히 말해서, reshape는 배열의 값 순서는 유지하면서 차원과 크기를 바꾸는 함수입니다. reshape가 필요한 이유는 데이터의 물리적 저장은 그대로지만 논리적 구조를 바꿔야 할 때가 많기 때문입니다.

1차원 배열 [1,2,3,4,5,6]을 2×3 행렬로 바꾸거나, 반대로 2차원 이미지를 1차원으로 펼치는 작업이 대표적이에요. 예를 들어, 28×28 손글씨 이미지를 전통적인 머신러닝 모델에 넣으려면 784(28*28) 길이의 1차원 벡터로 펼쳐야 하는데, reshape(-1)을 쓰면 한 줄로 끝납니다.

딥러닝에서 배치 차원을 추가하거나, 시계열 데이터의 형태를 (샘플, 타임스텝, 특성)으로 재구성할 때도 필수적이죠. 기존에는 이중 반복문으로 새 배열을 만들고 값을 하나씩 복사했다면, 이제는 arr.reshape(new_shape) 한 줄로 원하는 형태로 변환할 수 있습니다.

reshape의 핵심 특징은 첫째, 전체 원소 개수는 변하지 않음(3×4를 2×6으로는 되지만 2×5는 불가), 둘째, -1을 쓰면 나머지 차원에서 자동 계산(reshape(-1)은 1차원으로 펼침), 셋째, 원본을 수정하지 않고 뷰를 반환(메모리 절약), 넷째, 연속된 메모리가 아니면 복사본 생성한다는 것입니다. 이러한 특징들이 유연하고 효율적인 차원 변환을 가능하게 합니다.

코드 예제

import numpy as np

# 1차원 배열
arr = np.arange(12)  # [0, 1, 2, ..., 11]
print("원본:", arr)

# 2차원으로 reshape (3행 4열)
matrix = arr.reshape(3, 4)
print("reshape(3, 4):\n", matrix)
# [[ 0  1  2  3]
#  [ 4  5  6  7]
#  [ 8  9 10 11]]

# 3차원으로 reshape (2, 2, 3)
tensor = arr.reshape(2, 2, 3)
print("reshape(2, 2, 3):\n", tensor)

# -1 사용: 자동 계산 (나머지 차원에서 추론)
auto = arr.reshape(3, -1)  # 3행, 열은 자동(4가 됨)
print("reshape(3, -1):\n", auto)

# 1차원으로 펼치기 (flatten과 비슷)
flat = matrix.reshape(-1)
print("reshape(-1):", flat)  # [0 1 2 3 4 5 6 7 8 9 10 11]

# 원본은 그대로
print("원본 변화 없음:", arr)

설명

이것이 하는 일: 배열의 원소 순서는 그대로 두고, 다차원 구조만 재배치합니다. 마치 같은 블록들을 다른 모양의 상자에 다시 담는 것과 같아요.

첫 번째로, arr.reshape(3, 4)는 12개 원소를 3행 4열로 재배치합니다. 왜 이게 가능하냐면, 3 × 4 = 12로 전체 원소 개수가 동일하기 때문이에요.

원소들은 행 우선 순서(row-major)로 채워져서, 첫 행에 [0,1,2,3], 둘째 행에 [4,5,6,7] 이런 식으로 배치됩니다. 실무에서는 데이터베이스에서 가져온 1차원 데이터를 표 형태로 시각화할 때 자주 써요.

그 다음으로, arr.reshape(2, 2, 3)처럼 3차원으로도 바꿀 수 있습니다. 내부에서는 12개를 2×2×3=12 크기의 3차원 텐서로 재구성하는 거죠.

첫 번째 차원이 2라서 2개의 2×3 행렬이 쌓인 형태가 됩니다. 이미지 데이터를 배치로 묶거나, 시계열을 (배치, 시간, 특성) 형태로 바꿀 때 이런 다차원 reshape를 많이 씁니다.

arr.reshape(3, -1)처럼 -1을 쓰면 "3행이고, 열 개수는 알아서 계산해줘"라는 의미입니다. NumPy가 12 ÷ 3 = 4를 자동으로 계산해서 3×4 행렬을 만들어줘요.

이게 편리한 이유는 전체 크기가 바뀔 때마다 수동으로 계산하지 않아도 되기 때문입니다. 특히 배치 크기를 유연하게 다룰 때 reshape(-1, feature_size)처럼 쓰면 샘플 개수가 몇 개든 자동으로 처리돼요.

matrix.reshape(-1)처럼 -1만 쓰면 전체를 1차원으로 펼칩니다. 다차원 배열을 평평하게 만드는 거죠.

flatten() 메서드와 비슷하지만, reshape(-1)은 가능하면 뷰를 반환해서 메모리를 절약하고, flatten()은 항상 복사본을 만든다는 차이가 있어요. 이미지를 벡터로 만들거나, 다차원 텐서를 회귀 모델의 입력으로 바꿀 때 reshape(-1)을 많이 씁니다.

마지막으로, reshape는 원본을 수정하지 않습니다. 새로운 뷰 또는 복사본을 반환하기 때문에, arr는 여전히 1차원 [0,1,2,...,11] 그대로예요.

만약 원본을 바꾸고 싶다면 arr = arr.reshape(3, 4)처럼 재할당하면 됩니다. 여러분이 이 코드를 사용하면 데이터의 논리적 구조를 자유롭게 바꿀 수 있습니다.

실무에서의 이점은 첫째, 머신러닝 모델의 입력 형태에 맞춰 데이터를 쉽게 변환하고, 둘째, 메모리 복사 없이 뷰만 바꿔서 효율적이며, 셋째, -1 자동 계산으로 유연한 코드 작성이 가능하다는 것입니다.

실전 팁

💡 reshape할 때 원소 개수가 안 맞으면 에러가 납니다. 12개를 3×5로 바꾸려고 하면 ValueError: cannot reshape. 항상 곱이 같은지 확인하세요.

💡 arr.reshape(-1, 1)은 1차원을 2차원 열벡터로 만듭니다. sklearn 같은 라이브러리에서 1차원 배열을 거부할 때 이렇게 변환하세요.

💡 arr.flatten()은 항상 복사본을 만들지만, arr.ravel()이나 arr.reshape(-1)은 가능하면 뷰를 반환합니다. 성능이 중요하면 reshape를 쓰세요.

💡 reshape는 C-order(행 우선)로 동작합니다. order='F' 옵션을 주면 Fortran-order(열 우선)로 바뀌는데, 거의 안 써요.

💡 딥러닝에서 배치 차원 추가는 arr.reshape(1, *arr.shape) 또는 arr[np.newaxis, ...]로 더 명확하게 할 수 있습니다.


10. 다차원_배열의_축_이해하기

시작하며

여러분이 2차원 배열에서 "각 행의 합"을 구하고 싶은데, sum() 함수를 쓰니까 전체 합만 나와서 당황한 적 있나요? 또는 axis=0, axis=1이 뭔지 헷갈려서 결과가 반대로 나온 경험 말이에요.

이런 문제는 다차원 데이터를 다룰 때 정말 자주 발생합니다. NumPy의 많은 함수들이 axis 매개변수를 받는데, 이게 정확히 어느 방향인지 이해하지 못하면 원하는 결과를 얻기 어렵습니다.

예를 들어, 학생별 과목 점수 행렬에서 학생별 평균(행 방향)을 구할지, 과목별 평균(열 방향)을 구할지 axis로 지정해야 하는데, 직관과 반대로 동작하는 것처럼 느껴질 수 있어요. 바로 이럴 때 필요한 것이 축(axis) 개념의 정확한 이해입니다.

axis는 "어느 방향으로 연산을 적용할지"를 지정하는 게 아니라, "어느 차원을 없앰으로써 연산할지"를 뜻합니다. 이 개념을 확실히 잡으면 sum, mean, max 같은 모든 집계 함수를 자유자재로 쓸 수 있어요.

개요

간단히 말해서, 축(axis)은 다차원 배열에서 각 차원을 가리키는 번호로, axis=0은 첫 번째 차원(보통 행), axis=1은 두 번째 차원(보통 열)을 의미합니다. 축 개념이 필요한 이유는 다차원 데이터에서 특정 방향으로만 연산을 적용하기 위해서입니다.

2차원 배열 (3, 4) 모양에서 sum()을 하면 12개 원소가 모두 더해지는데, sum(axis=0)을 하면 행 방향으로 합쳐져서 4개의 값(각 열의 합)이 나오고, sum(axis=1)을 하면 열 방향으로 합쳐져서 3개의 값(각 행의 합)이 나옵니다. 예를 들어, 판매 데이터 (상품, 지역) 행렬에서 상품별 총매출(axis=1)이나 지역별 총매출(axis=0)을 구할 때 axis를 정확히 써야 해요.

기존에는 이중 반복문으로 각 행/열을 순회하며 합을 계산했다면, 이제는 arr.sum(axis=...) 한 줄로 원하는 방향의 집계를 얻을 수 있습니다. 축 개념의 핵심 특징은 첫째, axis=k는 k번째 차원을 따라 연산을 적용(그 차원이 사라짐), 둘째, 2차원에서 axis=0은 "행들을 합쳐서" 열 개수만큼 결과, axis=1은 "열들을 합쳐서" 행 개수만큼 결과, 셋째, axis=None(기본값)은 전체 배열에 연산, 넷째, 3차원 이상에서는 axis=(0,1)처럼 여러 축을 동시에 지정 가능하다는 것입니다.

이러한 특징들이 복잡한 집계를 직관적으로 만들어줍니다.

코드 예제

import numpy as np

# 2차원 배열 (3행 4열): 학생별 과목 점수
scores = np.array([
    [80, 90, 85, 95],  # 학생1
    [70, 85, 80, 90],  # 학생2
    [90, 95, 88, 92]   # 학생3
])
print("점수 행렬 (3학생 x 4과목):\n", scores)

# axis 없음: 전체 합
total = scores.sum()
print("전체 합:", total)  # 1040

# axis=0: 행 방향으로 합침 -> 각 열(과목)의 합
subject_sum = scores.sum(axis=0)
print("과목별 합 (axis=0):", subject_sum)  # [240 270 253 277]

# axis=1: 열 방향으로 합침 -> 각 행(학생)의 합
student_sum = scores.sum(axis=1)
print("학생별 합 (axis=1):", student_sum)  # [350 325 365]

# 평균도 동일하게
student_avg = scores.mean(axis=1)
print("학생별 평균:", student_avg)  # [87.5  81.25 91.25]

# 3차원 예제
tensor = np.arange(24).reshape(2, 3, 4)
print("3차원 (2, 3, 4):\n", tensor)
print("axis=0 합 (2, 3, 4) -> (3, 4):\n", tensor.sum(axis=0))

설명

이것이 하는 일: 다차원 배열에서 특정 차원을 따라 집계 연산을 수행합니다. 지정한 axis 차원이 사라지고, 나머지 차원만 남은 결과가 나와요.

첫 번째로, scores.sum()처럼 axis를 지정하지 않으면 전체 배열의 모든 원소를 더합니다. 왜 이게 기본이냐면, 대부분의 경우 전체 합이나 전체 평균을 먼저 보기 때문이에요.

3×4=12개의 점수를 모두 더해서 1040이 나옵니다. 이건 전체 학생의 전체 과목 점수를 다 합친 거죠.

그 다음으로, scores.sum(axis=0)은 axis=0 방향, 즉 행 방향으로 연산을 적용합니다. "행들을 합친다"는 의미인데, 결과적으로는 각 열의 합이 나와요.

왜 헷갈리냐면, "axis=0은 행을 없앤다"고 생각해야 이해가 쉽기 때문입니다. 3개 행이 사라지고 4개 열만 남아서, 결과는 길이 4인 1차원 배열 [240, 270, 253, 277]이 됩니다.

이건 각 과목(열)의 전체 학생 합계를 의미해요. 첫 번째 과목의 총점은 80+70+90=240이죠.

scores.sum(axis=1)은 axis=1 방향, 즉 열 방향으로 연산을 적용합니다. "열들을 합친다"는 의미로, 4개 열이 사라지고 3개 행만 남아서, 결과는 길이 3인 1차원 배열 [350, 325, 365]가 됩니다.

이건 각 학생(행)의 전체 과목 합계예요. 첫 번째 학생의 총점은 80+90+85+95=350입니다.

scores.mean(axis=1)처럼 mean, max, min, std 등 모든 집계 함수가 axis를 동일하게 사용합니다. mean(axis=1)은 각 행의 평균이니까 학생별 평균 점수 [87.5, 81.25, 91.25]가 나와요.

마지막으로, 3차원 배열 (2, 3, 4)에서 tensor.sum(axis=0)을 하면 첫 번째 차원(크기 2)이 사라져서 결과는 (3, 4) 모양이 됩니다. 2개의 (3,4) 행렬을 원소별로 더한 거죠.

axis=(0, 1)처럼 여러 축을 동시에 지정하면 두 차원이 모두 사라져서 더 낮은 차원의 결과가 나옵니다. 여러분이 이 개념을 이해하면 다차원 데이터의 집계를 자유자재로 할 수 있습니다.

실무에서의 이점은 첫째, 반복문 없이 한 줄로 원하는 방향의 통계를 구하고, 둘째, 판다스 데이터프레임이나 텐서플로도 같은 axis 개념을 쓰기 때문에 일관성 있게 적용할 수 있으며, 셋째, 복잡한 다차원 데이터도 축 개념으로 명확히 이해할 수 있다는 것입니다.

실전 팁

💡 헷갈릴 때는 "axis=k는 k번째 차원을 없앤다"고 기억하세요. axis=0은 첫 번째 차원(행)이 사라져 열 개수만큼 결과가 나와요.

💡 (3, 4) 배열에서 axis=0 결과는 (4,), axis=1 결과는 (3,)입니다. 결과 shape를 미리 예상하면 실수를 줄일 수 있어요.

💡 keepdims=True 옵션을 주면 축이 사라지지 않고 크기 1로 유지됩니다. sum(axis=1, keepdims=True)는 (3, 4) -> (3, 1)로, 브로드캐스팅할 때 유용해요.

💡 axis=-1은 마지막 차원을 의미합니다. 2차원에서는 axis=1과 같고, 차원 수가 바뀌어도 "마지막"을 가리켜 유연해요.

💡 argmax(axis=0)처럼 인덱스 반환 함수도 axis를 같은 방식으로 씁니다. 각 열에서 최댓값의 행 인덱스를 찾아줘요.


#NumPy#Array#Indexing#Slicing#DataManipulation#Data Science

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.

함께 보면 좋은 카드 뉴스

데이터 증강과 정규화 완벽 가이드

머신러닝 모델의 성능을 극대화하는 핵심 기법인 데이터 증강과 정규화에 대해 알아봅니다. 실무에서 바로 활용할 수 있는 다양한 기법과 실전 예제를 통해 과적합을 방지하고 모델 성능을 향상시키는 방법을 배웁니다.

ResNet과 Skip Connection 완벽 가이드

딥러닝 모델이 깊어질수록 성능이 떨어지는 문제를 해결한 혁신적인 기법, ResNet과 Skip Connection을 초급자도 이해할 수 있도록 쉽게 설명합니다. 실제 구현 코드와 함께 배워보세요.

CNN 아키텍처 완벽 가이드 LeNet AlexNet VGGNet

컴퓨터 비전의 기초가 되는 세 가지 핵심 CNN 아키텍처를 배웁니다. 손글씨 인식부터 이미지 분류까지, 딥러닝의 발전 과정을 따라가며 각 모델의 구조와 특징을 실습 코드와 함께 이해합니다.

CNN 기초 Convolution과 Pooling 완벽 가이드

CNN의 핵심인 Convolution과 Pooling을 초급자도 쉽게 이해할 수 있도록 설명합니다. 이미지 인식의 원리부터 실제 코드 구현까지, 실무에서 바로 활용 가능한 내용을 담았습니다.

TensorFlow와 Keras 완벽 입문 가이드

머신러닝과 딥러닝의 세계로 들어가는 첫걸음! TensorFlow와 Keras 프레임워크를 처음 접하는 분들을 위한 친절한 가이드입니다. 실무에서 바로 활용할 수 있는 핵심 개념과 예제를 통해 AI 모델 개발의 기초를 탄탄히 다져보세요.