본문 바로가기
Artificial Intelligence/60. Python

[PYTHON] 대규모 데이터 셔플링 시 메모리 부족을 해결하는 np.memmap 활용 방법 7가지

by Papa Martino V 2026. 4. 19.
728x90

Memory Mapping(np.memmap)
Memory Mapping (np.memmap)

 

파이썬을 활용한 데이터 사이언스와 머신러닝 워크플로우에서 가장 흔히 마주치는 벽은 바로 '메모리 부족(OutOfMemory, OOM)'입니다. 특히 수백 GB에 달하는 데이터셋을 학습시키기 전, 데이터의 편향을 제거하기 위해 수행하는 '셔플링(Shuffling)' 단계는 모든 데이터를 RAM에 올려야 한다는 고정관념 때문에 시스템 전체를 멈추게 하기도 합니다. 본 포스팅에서는 일반적인 np.random.shuffle의 한계를 뛰어넘어, 디스크를 RAM처럼 활용하는 Memory Mapping(np.memmap) 기술을 통해 저사양 환경에서도 대규모 데이터를 효율적으로 처리하는 전문적인 해결 방법을 제시합니다.


1. 왜 일반적인 셔플링은 대용량 데이터에서 실패하는가?

일반적으로 사용하는 numpy.array는 모든 데이터를 물리적 메모리(RAM)에 할당합니다. 예를 들어, float64 형식의 데이터 1,000억 개가 있다면 약 745GB의 RAM이 필요합니다. 이를 셔플링하기 위해 복사본을 만들거나 인덱싱을 수행하는 순간 메모리 점유율은 2배로 치솟습니다. 반면 np.memmap은 파일을 메모리에 직접 매핑하여 필요한 부분만 페이징(Paging)하므로 시스템 부하를 획기적으로 줄입니다.

메모리 관리 방식의 근본적 차이 비교

비교 항목 일반 numpy.ndarray numpy.memmap (Memory Mapping)
데이터 위치 RAM (주기억장치) Disk (보조기억장치) + OS 캐시
최대 처리 용량 사용 가능한 RAM 크기로 제한 남은 디스크 용량만큼 확장 가능
초기 로딩 속도 느림 (전체 데이터를 RAM에 적재) 매우 빠름 (매핑 선언만 수행)
셔플링 시 안정성 OOM 발생 위험 매우 높음 매우 안정적 (Virtual Memory 활용)
I/O 발생 시점 최초 로드 시 집중 발생 데이터 액세스 시점에 순차 발생

2. 실무 적용을 위한 핵심 개발자 샘플 코드 (7가지 케이스)

현업에서 즉시 활용할 수 있도록 다양한 시나리오별 np.memmap 활용 예제를 구성했습니다.

Example 1: 대용량 memmap 파일 생성 및 초기화 방법

import numpy as np
import os

# 10GB 규모의 가상 데이터셋 구조 설계 (1억 행, 10열)
filename = 'large_dataset.dat'
shape = (100000000, 10)
dtype = 'float32'

# 'w+' 모드로 디스크에 매핑된 배열 생성
fp = np.memmap(filename, dtype=dtype, mode='w+', shape=shape)

# 데이터 쓰기 (RAM을 거의 사용하지 않음)
for i in range(10):
    fp[:, i] = np.random.rand(shape[0])

# 변경사항 저장 및 닫기
del fp 

Example 2: 메모리 제약 환경에서의 In-place 인덱스 셔플링

실제 데이터를 직접 섞는 대신 인덱스를 먼저 섞은 뒤 매핑된 파일을 재배열하는 방식입니다.

# 기존 파일 읽기
data = np.memmap('large_dataset.dat', dtype='float32', mode='r+', shape=(100000000, 10))

# 인덱스만 메모리에 생성 (메모리 절약)
indices = np.arange(data.shape[0])
np.random.shuffle(indices)

# 매우 큰 데이터의 경우 한 번에 셔플링하기보다 청크 단위로 접근 권장
print("Index Shuffling Completed.")

Example 3: 청크(Chunk) 단위의 하이브리드 셔플링 해결 방법

디스크 I/O 속도를 최적화하기 위해 특정 크기만큼 데이터를 읽어 섞는 방식입니다.

def chunk_shuffle(filename, shape, dtype, chunk_size=1000000):
    data = np.memmap(filename, dtype=dtype, mode='r+', shape=shape)
    num_chunks = shape[0] // chunk_size
    
    for i in range(num_chunks):
        start = i * chunk_size
        end = start + chunk_size
        temp = np.copy(data[start:end])
        np.random.shuffle(temp)
        data[start:end] = temp
    
    data.flush()
    print("Chunk-wise shuffling finished.")

Example 4: 다중 memmap 파일을 이용한 레이블-데이터 동기화 셔플링

피처 데이터와 레이블 데이터가 분리되어 있을 때 동기화를 유지하며 섞는 방법입니다.

features = np.memmap('X_data.dat', dtype='float32', mode='r', shape=(5000000, 256))
labels = np.memmap('y_data.dat', dtype='int32', mode='r', shape=(5000000,))

# 셔플링된 인덱스 생성
s_idx = np.random.permutation(len(features))

# 사용 시점에만 호출 (메모리 효율적 적재)
batch_x = features[s_idx[:128]]
batch_y = labels[s_idx[:128]]

Example 5: 대규모 정렬(Sorting)과 연계된 셔플링 복구 방법

특정 기준에 따라 정렬된 데이터를 다시 무작위로 복구할 때 메모리 부하를 줄이는 기법입니다.

# 정렬된 데이터를 무작위로 재배치하기 위해 빈 memmap 생성
output = np.memmap('shuffled_output.dat', dtype='float32', mode='w+', shape=shape)
indices = np.random.permutation(shape[0])

for i, idx in enumerate(indices):
    output[i] = data[idx] # OS 레벨의 Page Cache가 최적화 수행

Example 6: PyTorch DataLoader와의 통합 사용 예시

딥러닝 학습 시 커스텀 데이터셋에서 memmap을 로드하여 사용하는 실무 패턴입니다.

from torch.utils.data import Dataset, DataLoader

class MemmapDataset(Dataset):
    def __init__(self, path, shape):
        self.data = np.memmap(path, dtype='float32', mode='r', shape=shape)
        
    def __len__(self):
        return self.data.shape[0]
        
    def __getitem__(self, idx):
        return self.data[idx].copy() # copy()를 통해 독립적 텐서 변환

# shuffle=True 설정 시 내부적으로 인덱스 셔플링 수행
dataset = MemmapDataset('large_dataset.dat', (100000000, 10))
loader = DataLoader(dataset, batch_size=256, shuffle=True)

Example 7: 사용이 끝난 메모리 맵 안전하게 삭제 및 리소스 해제

import gc

def close_memmap(mm_obj):
    filename = mm_obj.filename
    # 플러시를 통한 데이터 강제 기록
    mm_obj.flush()
    # 객체 참조 제거
    del mm_obj
    # 가비지 컬렉터 강제 실행으로 핸들 해제
    gc.collect()
    print(f"File {filename} is now safe to delete or move.")

3. 독창적인 성능 최적화 팁: SSD vs HDD 환경

np.memmap은 운영체제의 가상 메모리 관리 시스템에 의존합니다. 따라서 하드웨어 환경에 따라 셔플링 전략을 달리해야 합니다.

  • NVMe SSD: 랜덤 액세스 속도가 빠르므로 np.random.permutation 인덱스를 이용한 직접 참조 방식을 사용해도 성능 저하가 적습니다.
  • HDD: 헤더의 물리적 이동 시간이 길기 때문에 랜덤 셔플링 시 성능이 급격히 떨어집니다. 이 경우 데이터를 일정한 블록(Block) 단위로 묶어 블록 내에서만 셔플링하거나, 선형적으로 읽어 다른 파일에 쓰는 방식을 권장합니다.

4. 결론 및 요약

데이터가 커질수록 알고리즘의 효율성보다 데이터 로드와 메모리 관리 전략이 프로젝트의 성패를 결정짓습니다. numpy.memmap은 Python 개발자가 고가의 워크스테이션 없이도 테라바이트급 데이터를 핸들링할 수 있게 해주는 마법 같은 도구입니다.

전문가 조언: 셔플링 후에는 반드시 flush() 메서드를 호출하여 파일 시스템과의 동기화를 확인하십시오. 또한 copy-on-write (mmap='c') 모드를 활용하면 원본 데이터를 보호하면서 다양한 셔플링 실험을 안전하게 수행할 수 있습니다.

참고 문헌 및 출처

1. NumPy Documentation: "Memory-mapped file objects" (v1.26)
2. Python Software Foundation: "mmap — Memory-mapped file support"
3. High Performance Python, 2nd Edition (O'Reilly Media) - Micha Gorelick & Ian Ozsvald
4. SciPy Lecture Notes: "Advanced NumPy - Memory-mapped files"

728x90