개발·프레임워크

BUILD / 14번째 글

AI 개발을 위한 Python 핵심 라이브러리

NumPy 브로드캐스팅과 축, Pandas 성능과 dtype, Scikit-learn 파이프라인의 데이터 누수 차단, 손실 곡선 읽기, 환경 재현까지 — AI 개발의 Python 기본기를 실전 기준으로 정리합니다.

PALDYN Team29 MIN READ

지난 글에서 AI 회의 요약 시스템까지 실전 애플리케이션 패턴을 살펴봤다. 이번 글에서는 그 아래에 깔린 도구와 라이브러리를 다룬다.

라이브러리 목록을 나열하는 일은 검색으로도 되므로, 여기서는 처음 쓰는 사람이 실제로 막히는 자리에 자리를 더 준다. 형상이 안 맞는다는 오류 메시지를 읽는 법, 느린 Pandas 코드가 느린 이유, 교차 검증 점수가 실제보다 좋게 나오는 구조적 원인, 그리고 손실 곡선의 모양으로 무엇을 아는가 같은 것들이다.

Python AI 생태계 핵심 라이브러리

NumPy

NumPy는 파이썬 AI 생태계의 공통 바닥이다. 거의 모든 프레임워크가 내부적으로 ndarray를 주고받는 형식으로 쓴다. ndarray는 같은 타입의 숫자를 연속된 메모리에 늘어놓은 덩어리이고, 파이썬 리스트와 달리 원소마다 객체를 두지 않기 때문에 연산이 빠르다.

브로드캐스팅

브로드캐스팅은 모양이 다른 두 배열을 연산할 때 작은 쪽을 늘려 맞추는 규칙이다. 규칙은 뒤쪽 축부터 짝을 맞춰 보며, 길이가 같거나 한쪽이 1이면 통과하고 둘 다 아니면 오류다.

import numpy as np

a = np.zeros((32, 10))     # 배치 32, 특성 10
b = np.ones(10)            # 특성별 값 하나씩
(a + b).shape              # (32, 10)  — b가 32번 반복된 것처럼 동작

c = np.ones((32, 1))       # 샘플별 값 하나씩
(a + c).shape              # (32, 10)  — c가 열 방향으로 늘어남

여기서 헷갈리는 자리는 (10,)과 (10, 1)의 차이다. 앞은 열 방향으로 늘어나고 뒤는 행 방향으로 늘어나므로, 평균을 빼는 코드에서 이 둘을 헷갈리면 오류 없이 엉뚱한 값이 계산된다. 정규화 결과가 이상한데 오류는 안 난다면 십중팔구 이 자리다.

브로드캐스팅이 편한 만큼 위험한 이유가 여기 있다. 규칙이 통과하는 조합은 오류를 내지 않으므로, 의도와 다른 방향으로 늘어난 계산이 조용히 진행된다. 모양이 큰 쪽으로 커지는 연산을 쓸 때는 결과의 shape을 한 번 확인하는 편이 안전하고, 특히 손실을 계산하는 자리에서는 이 확인이 필수다. 예측과 정답의 모양이 (32,)와 (32, 1)로 어긋나면 브로드캐스팅이 32×32 행렬을 만들어 내고, 그 평균이 손실로 쓰여도 학습은 그럭저럭 돌아가는 것처럼 보인다.

축

axis는 「어느 방향으로 접을 것인가」를 뜻한다. 2차원 배열에서 axis=0은 행을 따라 내려가며 접으므로 결과가 열마다 하나씩 남고, axis=1은 열을 따라 가로로 접으므로 행마다 하나씩 남는다. 헷갈릴 때는 「사라지는 축」을 적는 값이라고 외우는 편이 빠르다. 모양이 (32, 10)인 배열에 axis=0을 주면 32가 사라져 (10,)이 된다.

x = np.random.randn(32, 10)

x.mean(axis=0).shape       # (10,)   — 특성별 평균
x.mean(axis=1).shape       # (32,)   — 샘플별 평균
x.mean(axis=0, keepdims=True).shape   # (1, 10) — 축을 남긴다

keepdims를 쓰는 이유가 앞의 브로드캐스팅과 이어진다. 평균을 빼는 연산에서 축을 남겨 두면 모양이 저절로 맞아떨어지므로, reshape을 손으로 넣어 맞추는 코드보다 틀릴 자리가 적다.

3차원 이상으로 가면 축에 이름을 붙여 두는 습관이 도움이 된다. 신경망에서 흔히 쓰는 (배치, 길이, 차원) 같은 모양은 주석 한 줄로 적어 두면 몇 주 뒤에 그 코드를 다시 볼 때 축 번호를 세어 보지 않아도 된다. 축을 바꾸는 연산 앞뒤로 모양을 주석에 적어 두는 것도 같은 이유에서 값한다.

형상 오류 메시지

오류 메시지를 끝까지 읽는 습관이 디버깅 시간을 가장 많이 줄인다. 자주 보는 둘은 뜻이 다르다.

  • operands could not be broadcast together with shapes (32,10) (10,32) — 원소별 연산에서 브로드캐스팅 규칙이 깨진 것이다. 뒤쪽 축부터 짝을 지어 보면 10과 32가 만나 둘 다 1이 아니라서 막혔다. 어느 한쪽을 전치해야 한다.
  • matmul: Input operand 1 has a mismatch in its core dimension — 행렬 곱에서 앞 배열의 열 수와 뒤 배열의 행 수가 다르다는 뜻이다.

두 경우 모두 고치기 전에 할 일은 같다. 연산 직전에 두 배열의 shape을 찍어 보는 것이다. 머릿속으로 추적하는 것보다 빠르고, 대개 데이터를 불러오는 단계에서 이미 모양이 틀어져 있었다는 사실을 발견하게 된다.

모양을 찍는 일을 아예 함수로 만들어 두는 팀도 있다. 배열 이름과 모양을 한 줄로 찍는 작은 도우미를 두고 의심스러운 자리마다 부르면, 나중에 지울 때도 검색 한 번으로 끝난다.

NumPy · Pandas 핵심 패턴

Pandas

실제 데이터는 CSV와 데이터베이스에서 오고, 그것을 표로 다루는 자리가 Pandas의 DataFrame이다. 기본 사용법은 어렵지 않지만, 데이터가 수십만 행으로 늘어나면 같은 결과를 내는 코드 사이에 수십 배의 속도 차이가 생긴다.

벡터화와 apply

import pandas as pd

df = pd.read_csv("dataset.csv")

# 느린 쪽 — 행마다 파이썬 함수를 부른다
df["len_slow"] = df["text"].apply(lambda t: len(t))

# 빠른 쪽 — 내부 루프가 C에서 돈다
df["len_fast"] = df["text"].str.len()

차이의 원인은 단순하다. apply는 행마다 파이썬 함수 호출이 일어나고, 벡터화된 연산은 루프가 라이브러리 안쪽에서 돈다. 여기서 벡터화란 원소마다 명령을 내리는 대신 배열 전체에 한 번 명령을 내리는 방식을 말한다. iterrows로 직접 도는 코드는 그중에서도 가장 느리다. 행마다 파이썬 객체를 새로 만들기 때문인데, 이 사실을 알면 왜 수십 배 차이가 나는지가 설명된다.

모든 apply를 없앨 수는 없다. 행 사이에 순서 의존이 있거나 외부 API를 부르는 일은 벡터화되지 않는다. 판단 기준은 「이 연산이 열 전체에 한 번에 적용될 수 있는가」다. 문자열 처리, 사칙연산, 비교, 매핑은 대개 가능하다.

속도를 고치기 전에 재 보는 순서도 중요하다. 느린 줄이 어디인지 짐작으로 고치다 보면 이미 빠른 코드를 더 복잡하게 만들고 끝난다. 주피터에서는 셀 단위로 시간을 재는 매직 커맨드가 있고, 스크립트에서는 구간마다 시각을 찍어 두는 것으로 충분하다. 대개 전체 시간의 대부분이 한두 줄에 몰려 있다.

dtype과 메모리

메모리가 부족해 노트북이 죽는 일의 절반은 타입 문제다. 기본으로 읽으면 정수는 64비트, 실수도 64비트로 잡히는데 대부분의 값은 그만큼 필요하지 않다. 특히 값의 종류가 적은 문자열 열을 category로 바꾸면 줄어드는 폭이 크다.

df.info(memory_usage="deep")          # 열별 메모리 확인

df["user_id"] = df["user_id"].astype("int32")
df["score"]   = df["score"].astype("float32")
df["country"] = df["country"].astype("category")

줄이기 전에 info로 재고, 줄인 뒤에 다시 재는 순서를 지킨다. 어림으로 바꾸다 보면 정밀도가 필요한 열까지 32비트로 내려 학습 결과가 달라지는 일이 생긴다.

애초에 다 읽지 않는 방법도 있다. 필요한 열만 지정해 읽거나 타입을 읽을 때부터 지정하면 최고점 메모리가 아예 낮아진다. 파일이 기가바이트 단위라면 읽는 단계에서 조각으로 나눠 처리하거나, 열 단위로 저장되는 형식으로 한 번 바꿔 두는 편이 낫다.

결측과 병합

결측값은 지우는 것이 기본이 아니다. 어떤 열에 얼마나 비어 있는지를 먼저 보고, 행을 지울지 값을 채울지 그 열을 통째로 뺄지를 정한다. 20%가 비어 있는 열을 평균으로 채우면 그 열의 분포가 평균 근처로 몰려 모델이 잘못된 신호를 배운다.

병합에서 가장 자주 나는 사고는 행 수가 늘어나는 것이다. 키가 중복된 테이블을 붙이면 조합이 곱해지므로, 병합 직후에 행 수를 확인하는 한 줄을 습관으로 둔다. 전과 후의 행 수가 다르면 키가 유일하지 않다는 뜻이다.

병합 방식도 뜻을 알고 고른다. 왼쪽 테이블을 기준으로 붙이면 짝이 없는 행은 결측으로 남고, 양쪽에 다 있는 행만 남기면 조용히 데이터가 줄어든다. 「어느 쪽 행을 남길 것인가」를 먼저 말로 정하고 그에 맞는 방식을 고르면, 결과를 보고 놀라는 일이 없다.

손실 곡선

학습이 잘 되고 있는지는 숫자 하나가 아니라 곡선의 모양이 말해 준다. 훈련 손실과 검증 손실을 한 그림에 그리는 것이 진단의 출발점이고, 이것을 그리지 않고 최종 정확도만 보는 것이 가장 흔한 손해다.

여기서 손실은 모델의 예측이 정답에서 얼마나 떨어져 있는지를 하나의 수로 만든 값이고, 학습은 그 수를 줄이는 방향으로 모델 안의 숫자를 옮기는 절차다. 훈련 손실은 모델이 보면서 배운 데이터에서 잰 값이고 검증 손실은 학습에 쓰지 않은 데이터에서 잰 값이라, 둘의 관계가 학습 상태를 말해 준다.

손실 곡선의 네 가지 모양

과적합의 모양

훈련 손실은 계속 내려가는데 검증 손실이 어느 지점부터 올라간다면 과적합이다. 모델이 훈련 데이터의 답을 외우기 시작한 것이고, 올라가기 시작한 그 지점이 멈췄어야 할 자리다. 대응은 셋 중 하나다. 그 지점에서 학습을 멈추거나, 데이터를 늘리거나, 모델이 외울 여지를 줄인다.

반대로 훈련 손실과 검증 손실이 나란히 내려가다 함께 평평해지면 그건 정상 종료에 가깝다. 더 낮추고 싶다면 이때는 규제를 푸는 것이 아니라 모델을 키우거나 데이터를 늘려야 한다. 두 곡선 사이의 간격이 과적합의 크기이고, 곡선의 기울기가 아직 배울 것이 남았는지를 말해 준다고 보면 읽기가 쉬워진다.

학습률의 모양

곡선이 위아래로 크게 튀거나 아예 발산한다면 학습률이 크다는 신호다. 한 걸음의 폭이 너무 커서 골짜기를 건너뛰는 상태다. 반대로 곡선이 거의 평평한 채로 아주 천천히 내려가면 학습률이 작거나 모델이 문제를 담기에 작은 것이다. 둘을 가르려면 학습률을 열 배 올려 몇 에폭만 돌려 본다. 곡선이 움직이기 시작하면 학습률 문제였다.

네 번째 모양도 알아 두면 좋다. 검증 손실이 훈련 손실보다 꾸준히 낮은 경우다. 규제가 훈련에만 걸려 있어서 그런 것이라면 정상이지만, 차이가 크다면 검증 세트가 너무 쉽거나 두 세트가 제대로 갈리지 않았다는 신호다. 같은 데이터가 양쪽에 섞여 들어간 경우가 대표적이다.

import matplotlib.pyplot as plt

fig, axes = plt.subplots(1, 2, figsize=(12, 4))

axes[0].plot(train_losses, label="Train")
axes[0].plot(val_losses,   label="Val")
axes[0].set_title("Loss Curve")
axes[0].legend()

import seaborn as sns
sns.heatmap(conf_matrix, annot=True, fmt="d", ax=axes[1])
axes[1].set_title("Confusion Matrix")

plt.tight_layout()
plt.savefig("training_result.png", dpi=150)

혼동 행렬

정확도 하나로는 어떤 오류를 내는지 알 수 없다. 혼동 행렬은 실제 라벨과 예측 라벨을 교차해 센 표라, 어느 클래스가 어느 클래스로 잘못 가는지를 그대로 보여 준다. 클래스가 불균형한 데이터에서 특히 중요하다. 99%가 정상인 데이터에서 전부 정상이라고 답하는 모델도 정확도 99%를 받지만, 혼동 행렬을 보면 비정상 행이 통째로 비어 있어 바로 들통난다.

그래서 불균형한 데이터에서는 정확도 대신 클래스별 정밀도와 재현율을 본다. 어느 쪽이 더 중요한지는 문제가 정한다. 불량품을 놓치면 안 되는 검사에서는 재현율을 올리고, 잘못된 경보가 비싼 시스템에서는 정밀도를 올린다. 이 판단을 하지 않고 한 숫자만 올리는 것이 모델을 배포한 뒤에 가장 자주 후회하는 자리다.

Scikit-learn 파이프라인

딥러닝 이전의 알고리즘과 전처리는 Scikit-learn이 맡는다. API가 일관돼서 어떤 알고리즘이든 학습하고 변환하고 예측하는 세 메서드로 다뤄진다.

데이터 누수

데이터 누수는 모델이 평가 시점에 알 수 없었어야 할 정보를 학습에 쓰는 상태다. 점수는 좋아지는데 실제 성능은 나빠지므로 가장 비싼 버그에 속한다.

# 누수가 나는 코드
scaler = StandardScaler()
X_all = scaler.fit_transform(X)          # 전체로 평균·표준편차를 계산
X_train, X_test = train_test_split(X_all)

이 코드가 왜 틀렸는지가 핵심이다. 평균과 표준편차를 전체 데이터로 구했으므로, 그 값 안에 테스트 데이터의 정보가 이미 섞였다. 테스트 세트는 「아직 본 적 없는 데이터」를 흉내 내는 자리인데 흉내가 깨진 것이다. 같은 실수가 결측값 채우기, 특성 선택, 오버샘플링에서도 똑같이 일어난다.

특성 선택에서의 누수가 특히 눈에 안 띈다. 전체 데이터로 상관관계를 재서 좋은 특성 스무 개를 고른 다음 그 특성으로 교차 검증을 돌리면, 이미 테스트 부분의 정답을 보고 고른 특성이므로 점수가 부풀려진다. 모델을 아무리 단순하게 잡아도 이 점수는 실제보다 좋게 나온다.

Pipeline

Pipeline은 이 실수를 구조로 막는다. 전처리와 모델을 한 객체로 묶으면 교차 검증이 각 분할마다 전처리를 다시 학습하므로, 사람이 순서를 기억하지 않아도 누수가 생기지 않는다.

from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import cross_val_score

pipe = Pipeline([
    ("scaler",     StandardScaler()),
    ("classifier", RandomForestClassifier(n_estimators=100)),
])

scores = cross_val_score(pipe, X, y, cv=5, scoring="f1_macro")
print(f"F1: {scores.mean():.3f} ± {scores.std():.3f}")

교차 검증

교차 검증은 데이터를 여러 조각으로 나눠 번갈아 평가하는 방식이고, 한 번의 분할에서 운 좋게 나온 점수에 속지 않기 위한 장치다. 점수의 평균만 보지 말고 표준편차도 함께 본다. 평균이 같아도 흔들림이 큰 모델은 배포 후의 결과를 예측하기 어렵다. 조각 수를 정하는 기준도 있다. 데이터가 적으면 조각을 늘려 학습에 쓰는 비율을 높이고, 데이터가 많으면 조각을 줄여 계산 시간을 아낀다.

분할 방식도 데이터에 맞춰야 한다. 클래스가 불균형하면 각 조각의 클래스 비율을 맞추는 방식을 쓰고, 시간 순서가 있는 데이터는 과거로 학습해 미래를 맞히는 방식으로 나눈다. 시계열을 무작위로 섞어 나누는 것 자체가 미래의 정보를 학습에 넣는 누수다.

라이브러리 선택 지도

작업별 갈래

표를 외우는 것보다 「이 작업에는 이것」으로 갈래를 잡는 편이 쓸모 있다.

지금 하려는 일 쓰는 것
표 형태 데이터로 예측 Scikit-learn
이미지·텍스트로 모델을 직접 학습 PyTorch
공개된 사전학습 모델을 가져다 쓰기 HuggingFace Transformers
상용 모델을 API로 부르기 Anthropic·OpenAI·Google SDK
학습 과정 진단과 결과 시각화 Matplotlib·Seaborn

표 데이터에 딥러닝을 먼저 꺼내는 것이 초보자의 흔한 우회로다. 수만 행 규모의 표 데이터에서는 부스팅 계열 모델이 더 빠르고 더 정확한 경우가 많으므로, 그 기준선을 먼저 세우고 넘어설 이유가 있을 때 딥러닝으로 간다.

기준선을 세우는 일 자체가 값하는 이유가 하나 더 있다. 나중에 복잡한 모델이 잘 나왔을 때 그것이 정말 나아진 것인지, 아니면 어딘가에서 정보가 새고 있는지를 가르는 자가 기준선이기 때문이다. 단순한 모델이 이미 0.95를 내던 문제에서 복잡한 모델이 0.99를 냈다면 그 차이가 어디서 왔는지를 설명할 수 있어야 한다.

딥러닝과 LLM SDK

pip install torch transformers datasets            # 오픈소스 딥러닝
pip install anthropic openai google-genai          # API SDK

가중치를 내 손에 두는 길과 API로 부르는 길은 성격이 다르다. 앞쪽은 데이터를 밖으로 내보내지 않아도 되고 비용이 장비에 고정되며, 뒤쪽은 장비가 필요 없고 성능이 좋은 대신 요청마다 요금이 나간다. 데이터를 밖으로 보낼 수 있는지가 대개 이 선택을 먼저 정한다.

둘을 섞어 쓰는 구성도 흔하다. 분류나 요약처럼 양이 많고 단순한 작업은 작은 모델을 직접 돌리고, 판단이 필요한 작업만 API로 보내는 식이다. 이때 두 경로의 출력 형식을 같게 맞춰 두면 나중에 어느 쪽으로 옮겨도 그 뒤의 코드가 그대로 돈다.

환경 재현

가상환경

프로젝트마다 환경을 따로 두는 것은 취향이 아니라 필수다. 한 환경에 여러 프로젝트를 담으면 한쪽에서 패키지를 올리는 순간 다른 쪽이 조용히 깨진다. 셋의 자리도 갈린다. 순수 파이썬 패키지만 쓰면 가벼운 도구로 충분하고, 파이썬 버전 자체를 프로젝트마다 다르게 두거나 컴파일된 비파이썬 의존성이 얽히면 그것까지 관리하는 도구가 편하다.

# uv — 빠르고 요즘 기본값에 가깝다
uv venv .venv && source .venv/bin/activate
uv pip install numpy pandas torch transformers

# conda — 파이썬 자체 버전과 비파이썬 의존성까지 관리한다
conda create -n aidev python=3.11
conda activate aidev

CUDA와 PyTorch

GPU를 쓸 때 가장 자주 막히는 자리다. PyTorch는 CUDA 버전에 맞춰 빌드된 패키지가 따로 있으므로, 그냥 설치하면 GPU를 못 보는 CPU 전용 빌드가 깔릴 수 있다. 설치 전에 드라이버가 지원하는 CUDA 버전을 확인하고, 그에 맞는 인덱스를 지정해 받는다.

nvidia-smi                      # 드라이버가 지원하는 CUDA 버전 확인
pip install torch --index-url https://download.pytorch.org/whl/cu121

버전 고정

몇 주 뒤에 같은 결과가 나오게 하려면 버전을 적어 두어야 한다. 설치 목록을 그대로 뽑아 두는 것이 가장 간단한 방법이고, 그 파일을 저장소에 커밋한다. 여기에 더해 실험 스크립트의 맨 위에서 파이썬과 주요 라이브러리의 버전, GPU 이름을 찍어 두면 나중에 결과가 갈렸을 때 무엇이 달라졌는지 바로 보인다.

처음 겪는 오류

import 실패

「분명히 설치했는데 못 찾는다」는 상황의 원인은 거의 언제나 하나다. 설치한 파이썬과 실행하는 파이썬이 다른 것이다. 가상환경을 켜지 않은 터미널에서 설치했거나, 노트북의 커널이 다른 환경을 가리키고 있다.

import sys
print(sys.executable)       # 지금 이 코드를 돌리는 파이썬

이 한 줄을 찍어 보고 설치할 때 쓴 경로와 같은지 확인한다. 노트북에서는 설치 명령도 지금 커널에 설치하는 형태로 쓰는 편이 안전하다. 같은 이름의 모듈이 작업 디렉터리에 있는 경우도 드물게 있다. 직접 만든 파일 이름이 라이브러리와 겹치면 파이썬이 그쪽을 먼저 집으므로, 설치한 라이브러리가 있는데도 엉뚱한 오류가 난다.

GPU 인식 실패

torch.cuda.is_available()이 거짓을 돌려줄 때 확인할 순서가 있다. 먼저 드라이버가 보이는지를 nvidia-smi로 확인하고, 다음으로 설치된 PyTorch가 CUDA 빌드인지 확인한다. 드라이버는 보이는데 PyTorch가 못 본다면 앞 절의 설치 문제이고, 드라이버조차 안 보이면 장비나 컨테이너 설정 문제라 파이썬 쪽에서 할 일이 없다.

이 순서를 지키지 않으면 멀쩡한 코드를 붙들고 몇 시간을 보내게 된다. 확인은 두 줄이면 끝나고, 어디서 끊겼는지를 알면 물어볼 사람도 달라진다.

버전 충돌

패키지를 하나 설치했는데 다른 것이 깨지는 상황이다. 대개 같은 의존성의 서로 다른 버전을 두 패키지가 요구할 때 생긴다. 해결의 첫걸음은 무엇이 어긋났는지 확인하는 것이고, 대부분의 경우 새 환경을 만들어 필요한 것만 한 번에 설치하는 편이 하나씩 되돌리는 것보다 빠르다. 한 번에 설치하면 의존성 해결기가 모든 조건을 함께 보고 답을 찾지만, 하나씩 덧붙이면 앞에서 고른 버전에 묶여 답이 없어지는 일이 생긴다. 그래서 환경을 가볍게 버릴 수 있게 만들어 두는 것이 결국 시간을 아낀다.


읽어주셔서 감사합니다. 😊

LATEST

개발·프레임워크의 최신 글

개발·프레임워크2026.05.28

Google Gemini SDK 활용 가이드

google-genai 패키지의 Client 하나로 Gemini API를 부르는 법 — 응답 객체와 finish_reason, 생성 설정과 구조화 출력, 인라인 데이터와 Files API, 도구 호출 왕복, 안전 설정과 재시도, 대화 비용과 컨텍스트 캐싱까지 정리한다.

26 MIN
개발·프레임워크2026.05.28

OpenAI SDK 완전 정복

Python openai 패키지로 OpenAI API를 다루는 법 — 클라이언트 설정과 재시도, 메시지와 응답 구조, Responses API 대응, 모델 고르는 축, 도구 호출 루프, 구조화 출력, 임베딩·이미지 입력, 토큰 비용과 한도까지 정리

30 MIN