본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.
본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.
AI Generated
2025. 12. 2. · 65 Views
TensorFlow 데이터 파이프라인 완벽 가이드
TensorFlow의 tf.data API를 활용하여 효율적인 데이터 파이프라인을 구축하는 방법을 알아봅니다. 대용량 데이터 처리부터 성능 최적화까지, 실무에서 바로 적용할 수 있는 핵심 기법들을 다룹니다.
목차
- tf.data.Dataset 소개
- from_tensor_slices로 데이터셋 생성
- map, batch, shuffle 연산
- prefetch로 성능 최적화
- 대용량 데이터 처리 전략
- TFRecord 포맷 이해
1. tf.data.Dataset 소개
김개발 씨는 첫 머신러닝 프로젝트를 맡게 되었습니다. 수십만 개의 이미지 데이터를 모델에 학습시켜야 하는데, 데이터를 메모리에 한꺼번에 올리니 컴퓨터가 멈춰버렸습니다.
"도대체 이 많은 데이터를 어떻게 처리해야 하지?"
tf.data.Dataset은 TensorFlow에서 데이터를 효율적으로 처리하기 위한 핵심 도구입니다. 마치 도서관의 컨베이어 벨트처럼, 필요한 데이터를 필요한 만큼만 순차적으로 전달해줍니다.
이것을 제대로 이해하면 아무리 큰 데이터셋도 메모리 걱정 없이 처리할 수 있습니다.
다음 코드를 살펴봅시다.
import tensorflow as tf
# 간단한 Dataset 생성
data = [1, 2, 3, 4, 5]
dataset = tf.data.Dataset.from_tensor_slices(data)
# Dataset의 각 요소 순회
for element in dataset:
print(element.numpy())
# Dataset은 반복 가능한 객체입니다
# 필요할 때마다 데이터를 하나씩 가져옵니다
print(f"데이터셋 크기: {len(list(dataset))}")
김개발 씨는 입사 6개월 차 주니어 개발자입니다. 이번에 처음으로 딥러닝 프로젝트를 담당하게 되었는데, 문제가 생겼습니다.
10만 장의 이미지 데이터를 학습시켜야 하는데, 평소처럼 리스트에 모든 데이터를 담으니 메모리가 터져버린 것입니다. 선배 개발자 박시니어 씨가 다가와 화면을 살펴봅니다.
"아, 데이터를 전부 메모리에 올리면 안 되지. tf.data.Dataset을 써봐." 그렇다면 tf.data.Dataset이란 정확히 무엇일까요?
쉽게 비유하자면, Dataset은 마치 초밥집의 회전 벨트와 같습니다. 주방에서 초밥을 만들어 벨트 위에 올려놓으면, 손님은 필요한 초밥만 집어서 먹습니다.
모든 초밥을 한꺼번에 테이블에 올려놓을 필요가 없는 것이죠. Dataset도 마찬가지로, 전체 데이터를 메모리에 올리지 않고 필요한 만큼만 순차적으로 제공합니다.
Dataset이 없던 시절에는 어땠을까요? 개발자들은 NumPy 배열이나 Python 리스트로 데이터를 관리했습니다.
작은 데이터셋에서는 문제가 없었지만, 데이터가 커지면 메모리 부족 오류가 발생했습니다. 더 큰 문제는 데이터 전처리, 셔플, 배치 처리를 모두 수동으로 구현해야 했다는 점입니다.
바로 이런 문제를 해결하기 위해 tf.data API가 등장했습니다. tf.data.Dataset을 사용하면 **지연 실행(lazy evaluation)**이 가능해집니다.
데이터가 실제로 필요한 순간에만 메모리에 로드됩니다. 또한 다양한 변환 연산을 체인처럼 연결할 수 있어 코드가 깔끔해집니다.
무엇보다 GPU 학습과 데이터 로딩을 병렬로 처리할 수 있어 학습 속도가 크게 향상됩니다. 위의 코드를 살펴보겠습니다.
먼저 from_tensor_slices 메서드로 리스트를 Dataset으로 변환합니다. 이렇게 만들어진 Dataset은 Python의 이터러블 객체처럼 for 문으로 순회할 수 있습니다.
각 요소는 텐서 형태로 반환되므로, numpy() 메서드로 일반 값으로 변환할 수 있습니다. 실제 현업에서는 어떻게 활용할까요?
예를 들어 이미지 분류 모델을 학습한다고 가정해봅시다. 수십만 장의 이미지를 Dataset으로 만들면, 학습 루프가 돌 때마다 필요한 이미지만 디스크에서 읽어옵니다.
메모리 효율이 극대화되는 것이죠. 구글, 네이버 등 대형 IT 기업에서 이 패턴을 표준으로 사용하고 있습니다.
하지만 주의할 점도 있습니다. Dataset은 한 번 순회하면 소진됩니다.
여러 번 사용하려면 repeat() 메서드를 호출하거나 매번 새로 생성해야 합니다. 초보자들이 이 점을 간과해서 두 번째 에포크부터 데이터가 없다고 당황하는 경우가 많습니다.
다시 김개발 씨의 이야기로 돌아가 봅시다. 박시니어 씨의 설명을 들은 김개발 씨는 고개를 끄덕였습니다.
"아, 그래서 데이터를 스트리밍처럼 처리하는 거군요!" tf.data.Dataset을 제대로 이해하면 대용량 데이터도 두렵지 않습니다. 이제 다음 장에서 Dataset을 생성하는 구체적인 방법을 알아보겠습니다.
실전 팁
💡 - Dataset은 기본적으로 한 번만 순회 가능하므로, 여러 에포크 학습 시 repeat()을 활용하세요
- take() 메서드로 일부 데이터만 가져와 테스트해볼 수 있습니다
2. from tensor slices로 데이터셋 생성
박시니어 씨가 김개발 씨에게 물었습니다. "그런데 실제 데이터는 어떻게 Dataset으로 만들 거야?
이미지 파일이 폴더에 있고, 라벨은 CSV에 있잖아." 김개발 씨는 멈칫했습니다. 다양한 형태의 데이터를 어떻게 하나의 Dataset으로 묶을 수 있을까요?
from_tensor_slices는 가장 많이 사용되는 Dataset 생성 메서드입니다. 마치 빵을 슬라이스하듯이, 주어진 데이터를 첫 번째 차원을 기준으로 잘라 개별 요소로 만들어줍니다.
배열, 튜플, 딕셔너리 등 다양한 형태의 데이터를 Dataset으로 변환할 수 있습니다.
다음 코드를 살펴봅시다.
import tensorflow as tf
import numpy as np
# 특성과 라벨을 함께 담은 Dataset 생성
features = np.array([[1, 2], [3, 4], [5, 6], [7, 8]])
labels = np.array([0, 1, 0, 1])
# 튜플로 묶어서 Dataset 생성
dataset = tf.data.Dataset.from_tensor_slices((features, labels))
# 각 요소는 (특성, 라벨) 쌍입니다
for feature, label in dataset:
print(f"특성: {feature.numpy()}, 라벨: {label.numpy()}")
# 딕셔너리 형태로도 생성 가능
dict_dataset = tf.data.Dataset.from_tensor_slices({
"image": features,
"label": labels
})
김개발 씨는 머신러닝 프로젝트에서 가장 흔한 상황에 직면했습니다. 입력 데이터(특성)와 정답(라벨)이 분리되어 있는 것입니다.
이 둘을 어떻게 하나로 묶어서 모델에 전달할 수 있을까요? 박시니어 씨가 화이트보드에 그림을 그리며 설명했습니다.
"from_tensor_slices를 떠올릴 때는 식빵을 생각해봐." 그렇다면 from_tensor_slices의 동작 원리는 무엇일까요? 쉽게 비유하자면, 이 메서드는 마치 식빵을 슬라이스하는 것과 같습니다.
한 덩어리의 식빵(배열)을 첫 번째 차원을 따라 잘라서 여러 조각(개별 샘플)으로 만듭니다. 4x2 크기의 배열이 있다면, 4개의 샘플로 나뉘는 것이죠.
여러 개의 배열을 함께 슬라이스하면 어떻게 될까요? 위 코드에서 features와 labels를 튜플로 묶어 전달했습니다.
이 경우 두 배열이 동시에 슬라이스됩니다. 첫 번째 특성은 첫 번째 라벨과 짝지어지고, 두 번째 특성은 두 번째 라벨과 짝지어집니다.
마치 지퍼처럼 두 배열이 맞물리는 것입니다. 이 방식이 강력한 이유가 있습니다.
학습 데이터를 셔플하거나 배치로 묶을 때, 특성과 라벨이 항상 같이 움직입니다. 따로 관리하면 순서가 뒤섞여 엉뚱한 라벨이 붙을 수 있는데, Dataset으로 묶으면 그런 걱정이 없습니다.
딕셔너리 형태도 유용합니다. 코드의 마지막 부분처럼 딕셔너리로 Dataset을 만들 수 있습니다.
이 방식은 여러 종류의 입력이 필요한 복잡한 모델에서 특히 유용합니다. 키 이름으로 각 데이터에 접근할 수 있어 코드 가독성도 좋아집니다.
실제 프로젝트에서는 어떻게 활용할까요? 예를 들어 이미지 파일 경로와 라벨이 있다고 가정해봅시다.
먼저 파일 경로 리스트와 라벨 리스트를 from_tensor_slices에 전달합니다. 그 다음 map 함수로 경로에서 실제 이미지를 로드하면 됩니다.
이 패턴은 거의 모든 이미지 분류 프로젝트에서 사용됩니다. 주의할 점이 있습니다.
튜플이나 딕셔너리로 묶을 때, 모든 배열의 첫 번째 차원 크기가 동일해야 합니다. 샘플 수가 다르면 오류가 발생합니다.
예를 들어 특성이 100개인데 라벨이 99개면 Dataset을 생성할 수 없습니다. 박시니어 씨가 마무리했습니다.
"from_tensor_slices는 Dataset의 시작점이야. 이걸 기반으로 다양한 변환을 적용하게 될 거야." 김개발 씨는 이제 자신의 데이터를 Dataset으로 만드는 방법을 이해했습니다.
다음 단계는 이 Dataset을 가공하는 것입니다.
실전 팁
💡 - 튜플로 묶을 때 모든 배열의 샘플 수(첫 번째 차원)가 같아야 합니다
- 파일 경로 리스트를 Dataset으로 만들고, map으로 실제 파일을 로드하는 패턴을 익혀두세요
3. map, batch, shuffle 연산
김개발 씨는 Dataset을 만드는 데 성공했습니다. 하지만 원본 이미지는 크기도 제각각이고, 픽셀 값도 0~255 범위입니다.
"이 데이터를 그대로 모델에 넣어도 되나요?" 박시니어 씨가 고개를 저었습니다. "데이터 전처리를 해야지.
map, batch, shuffle을 배워볼까?"
map, batch, shuffle은 Dataset을 가공하는 핵심 연산입니다. map은 각 요소에 함수를 적용하고, batch는 여러 샘플을 묶어주며, shuffle은 순서를 무작위로 섞습니다.
이 세 가지만 알면 대부분의 데이터 전처리를 해결할 수 있습니다.
다음 코드를 살펴봅시다.
import tensorflow as tf
# 원본 데이터
dataset = tf.data.Dataset.from_tensor_slices([1, 2, 3, 4, 5, 6, 7, 8])
# map: 각 요소에 함수 적용 (여기서는 2배)
dataset = dataset.map(lambda x: x * 2)
# shuffle: 데이터 순서 섞기 (buffer_size가 클수록 무작위성 증가)
dataset = dataset.shuffle(buffer_size=4)
# batch: 여러 샘플을 하나로 묶기
dataset = dataset.batch(batch_size=3)
# 결과 확인
for batch in dataset:
print(batch.numpy())
# 출력 예: [4 2 8], [6 10 14], [12 16] (셔플로 인해 달라질 수 있음)
김개발 씨는 드디어 본격적인 데이터 파이프라인 구축에 들어갔습니다. 원본 데이터를 모델이 좋아하는 형태로 바꿔야 합니다.
이때 필요한 것이 바로 변환 연산입니다. 박시니어 씨가 세 가지 연산을 순서대로 설명하기 시작했습니다.
첫 번째는 map 연산입니다. map은 마치 공장의 컨베이어 벨트 위 작업자와 같습니다.
데이터가 지나갈 때마다 정해진 작업을 수행합니다. 이미지라면 크기를 조정하고, 숫자라면 정규화를 적용합니다.
위 코드에서는 각 숫자에 2를 곱하는 간단한 변환을 수행했습니다. map의 강력한 점은 병렬 처리가 가능하다는 것입니다.
num_parallel_calls 인자를 추가하면 여러 CPU 코어가 동시에 변환 작업을 수행합니다. 대용량 데이터에서 속도 차이가 극명하게 나타납니다.
두 번째는 shuffle 연산입니다. shuffle은 왜 필요할까요?
만약 고양이 이미지 1000장, 그 다음 개 이미지 1000장이 순서대로 정렬되어 있다면, 모델은 처음에는 고양이만, 나중에는 개만 학습하게 됩니다. 이러면 학습이 불안정해집니다.
데이터를 섞어서 골고루 학습시켜야 합니다. 여기서 buffer_size라는 개념이 등장합니다.
shuffle은 메모리 효율을 위해 버퍼를 사용합니다. 버퍼에 일정량의 데이터를 담아두고, 그 안에서 무작위로 하나를 선택합니다.
버퍼가 작으면 진정한 무작위가 아니고, 버퍼가 크면 메모리를 많이 씁니다. 보통 전체 데이터 크기 이상으로 설정하면 완벽한 셔플이 됩니다.
세 번째는 batch 연산입니다. 딥러닝 모델은 보통 한 번에 여러 샘플을 처리합니다.
이를 미니배치 학습이라고 합니다. batch 연산은 개별 샘플들을 지정된 크기로 묶어줍니다.
batch_size가 32라면 32개의 샘플이 하나의 텐서로 합쳐집니다. 연산의 순서가 중요합니다.
일반적으로 map, shuffle, batch 순서로 적용합니다. 먼저 데이터를 변환하고, 순서를 섞은 다음, 배치로 묶는 것이죠.
순서가 바뀌면 결과가 달라질 수 있습니다. 예를 들어 batch 후에 shuffle을 하면 배치 단위로 섞이므로 셔플 효과가 줄어듭니다.
실무에서의 활용 예시를 보겠습니다. 이미지 분류 프로젝트라면 map에서 이미지 리사이즈, 정규화, 데이터 증강을 수행합니다.
그 다음 shuffle로 순서를 섞고, batch로 32개씩 묶습니다. 이것이 가장 흔한 패턴입니다.
김개발 씨가 물었습니다. "그럼 이 세 가지만 알면 되나요?" 박시니어 씨가 웃으며 대답했습니다.
"기본은 이 세 가지야. 하지만 성능을 높이려면 하나 더 알아야 해.
바로 prefetch야."
실전 팁
💡 - shuffle의 buffer_size는 데이터셋 크기 이상으로 설정하면 완벽한 셔플이 됩니다
- map에서 num_parallel_calls=tf.data.AUTOTUNE을 사용하면 자동으로 최적의 병렬 처리 수를 결정합니다
4. prefetch로 성능 최적화
김개발 씨는 데이터 파이프라인을 완성하고 학습을 시작했습니다. 그런데 이상합니다.
GPU 사용률이 50%도 안 됩니다. 분명 비싼 GPU인데 왜 놀고 있을까요?
박시니어 씨가 모니터를 보더니 말했습니다. "데이터 로딩이 병목이야.
prefetch를 써봐."
prefetch는 데이터 로딩과 모델 학습을 병렬로 처리하는 최적화 기법입니다. 마치 식당에서 다음 손님 음식을 미리 준비해두는 것처럼, GPU가 현재 배치를 학습하는 동안 CPU가 다음 배치를 미리 준비합니다.
단 한 줄의 코드로 학습 속도를 크게 향상시킬 수 있습니다.
다음 코드를 살펴봅시다.
import tensorflow as tf
# 기본 데이터 파이프라인
dataset = tf.data.Dataset.from_tensor_slices(range(1000))
# 무거운 전처리를 시뮬레이션하는 map 함수
def heavy_preprocessing(x):
# 실제로는 이미지 로드, 리사이즈 등의 작업
return x * 2
dataset = dataset.map(heavy_preprocessing,
num_parallel_calls=tf.data.AUTOTUNE)
dataset = dataset.batch(32)
# prefetch로 성능 최적화 (핵심!)
dataset = dataset.prefetch(buffer_size=tf.data.AUTOTUNE)
# 이제 GPU 학습과 데이터 로딩이 병렬로 수행됩니다
for batch in dataset:
# 모델 학습 (GPU)
# 동시에 다음 배치 준비 (CPU)
pass
김개발 씨는 당황스러웠습니다. 회사에서 비싼 GPU를 할당받았는데, 정작 GPU가 대부분의 시간을 놀고 있었기 때문입니다.
문제가 뭘까요? 박시니어 씨가 그림을 그리며 설명했습니다.
"지금 일어나는 일을 시간 순서로 보여줄게." prefetch 없이 학습하면 어떻게 될까요? 첫 번째, CPU가 배치 1의 데이터를 로드합니다.
GPU는 대기합니다. 두 번째, GPU가 배치 1을 학습합니다.
CPU는 대기합니다. 세 번째, CPU가 배치 2의 데이터를 로드합니다.
GPU는 또 대기합니다. 이렇게 CPU와 GPU가 번갈아가며 일하고, 번갈아가며 쉽니다.
비효율의 극치입니다. prefetch는 이 문제를 해결합니다.
비유하자면, prefetch는 마치 뷔페 식당의 백업 접시와 같습니다. 손님이 음식을 먹는 동안 주방에서는 다음 접시를 미리 준비해둡니다.
접시가 비면 바로 새 접시를 내놓을 수 있죠. 기다림이 없습니다.
prefetch를 적용하면 어떻게 달라질까요? GPU가 배치 1을 학습하는 동안, CPU는 배치 2를 준비합니다.
GPU가 배치 1 학습을 마치면, 배치 2가 이미 준비되어 있어 바로 학습을 시작합니다. 동시에 CPU는 배치 3을 준비합니다.
CPU와 GPU가 동시에 일합니다. 코드에서 tf.data.AUTOTUNE이 눈에 띕니다.
AUTOTUNE은 TensorFlow가 런타임에 최적의 버퍼 크기를 자동으로 결정하게 합니다. 시스템 리소스와 데이터 특성에 따라 동적으로 조정됩니다.
직접 숫자를 지정할 수도 있지만, 대부분의 경우 AUTOTUNE이 좋은 선택입니다. 얼마나 빨라질까요?
데이터 로딩이 학습 시간의 병목인 경우, prefetch만으로 2배 이상 빨라지는 경우도 있습니다. 특히 이미지나 오디오처럼 전처리가 무거운 데이터에서 효과가 큽니다.
단 한 줄의 코드로 이런 효과를 얻을 수 있다니, 사용하지 않을 이유가 없습니다. 주의할 점이 있습니다.
prefetch는 메모리를 추가로 사용합니다. 미리 로드해둔 데이터가 메모리에 상주하기 때문입니다.
메모리가 부족한 환경에서는 버퍼 크기를 작게 설정해야 할 수 있습니다. 완벽한 파이프라인의 순서를 정리하면 이렇습니다.
map(num_parallel_calls=AUTOTUNE)으로 병렬 전처리, shuffle로 데이터 셔플, batch로 배치 생성, 마지막으로 prefetch(AUTOTUNE)로 파이프라이닝. 이 순서를 기억하세요.
김개발 씨는 prefetch 한 줄을 추가했습니다. GPU 사용률이 95%까지 치솟았습니다.
"와, 이렇게 간단한 거였어요?" 박시니어 씨가 미소지었습니다. "간단하지만 강력하지."
실전 팁
💡 - prefetch는 항상 파이프라인의 마지막에 위치해야 합니다
- AUTOTUNE 대신 buffer_size=1로도 충분한 효과를 볼 수 있는 경우가 많습니다
5. 대용량 데이터 처리 전략
프로젝트가 잘 진행되던 중, 새로운 과제가 떨어졌습니다. 이번에는 1TB 규모의 데이터입니다.
김개발 씨는 막막해졌습니다. "아무리 Dataset을 써도 1TB는 무리 아닌가요?" 박시니어 씨가 말했습니다.
"걱정 마. TensorFlow에는 대용량 데이터를 위한 전략이 있어."
대용량 데이터를 처리할 때는 제너레이터, 인터리브, 캐싱 등의 고급 전략이 필요합니다. 디스크에서 데이터를 조금씩 읽어오거나, 여러 파일을 동시에 읽거나, 자주 쓰는 데이터를 메모리에 캐싱할 수 있습니다.
이런 전략들을 조합하면 어떤 크기의 데이터도 효율적으로 처리할 수 있습니다.
다음 코드를 살펴봅시다.
import tensorflow as tf
# 전략 1: 제너레이터로 데이터를 조금씩 생성
def data_generator():
for i in range(1000000):
yield i, i * 2 # 필요할 때만 데이터 생성
dataset = tf.data.Dataset.from_generator(
data_generator,
output_signature=(
tf.TensorSpec(shape=(), dtype=tf.int32),
tf.TensorSpec(shape=(), dtype=tf.int32)
)
)
# 전략 2: 여러 파일을 인터리브로 병렬 읽기
file_paths = ["data1.csv", "data2.csv", "data3.csv"]
files_dataset = tf.data.Dataset.from_tensor_slices(file_paths)
dataset = files_dataset.interleave(
lambda path: tf.data.TextLineDataset(path),
cycle_length=3, # 동시에 3개 파일 읽기
num_parallel_calls=tf.data.AUTOTUNE
)
# 전략 3: 작은 데이터셋은 캐싱으로 속도 향상
dataset = dataset.cache() # 메모리 캐싱
김개발 씨는 새로운 도전에 직면했습니다. 지금까지 다룬 데이터는 메모리에 올릴 수 있는 수준이었습니다.
하지만 1TB는 차원이 다릅니다. 어떻게 해야 할까요?
박시니어 씨가 세 가지 전략을 소개했습니다. 첫 번째 전략은 from_generator입니다.
제너레이터는 마치 주문 즉시 조리하는 레스토랑과 같습니다. 모든 음식을 미리 만들어두지 않고, 주문이 들어올 때마다 그때그때 만듭니다.
from_generator도 마찬가지로, 데이터가 필요할 때만 생성합니다. 1억 개의 데이터가 있어도 메모리에는 현재 처리 중인 것만 올라갑니다.
단, 제너레이터 사용 시 output_signature를 반드시 지정해야 합니다. TensorFlow가 데이터의 형태와 타입을 미리 알아야 최적화를 수행할 수 있기 때문입니다.
두 번째 전략은 interleave입니다. 대용량 데이터는 보통 여러 파일로 나뉘어 저장됩니다.
한 파일씩 순차적으로 읽으면 느립니다. interleave는 여러 파일을 동시에 읽습니다.
cycle_length가 3이면 3개 파일을 번갈아가며 읽습니다. 비유하자면, 도서관에서 책 한 권을 다 읽고 다음 책을 빌리는 것보다, 여러 권을 동시에 빌려서 조금씩 번갈아 읽는 것이 효율적인 것과 같습니다.
세 번째 전략은 cache입니다. cache는 처음 읽은 데이터를 저장해둡니다.
에포크가 반복될 때 디스크에서 다시 읽지 않고 캐시된 데이터를 사용합니다. 작은 데이터셋에서는 메모리에, 큰 데이터셋에서는 로컬 파일에 캐싱할 수 있습니다.
cache의 위치가 중요합니다. 일반적으로 map 전에 cache를 배치합니다.
원본 데이터를 캐싱하고, 매번 다른 데이터 증강을 적용하는 것이죠. map 후에 cache를 두면 증강된 결과가 고정되어 매 에포크마다 같은 데이터를 보게 됩니다.
이 전략들을 조합하면 어떻게 될까요? 실무에서는 이렇게 사용합니다.
먼저 from_tensor_slices로 파일 경로 목록을 Dataset으로 만듭니다. 그 다음 interleave로 여러 파일을 병렬로 읽습니다.
map으로 전처리하고, shuffle, batch, prefetch를 적용합니다. 필요하다면 중간에 cache를 넣습니다.
주의할 점도 있습니다. cache를 남용하면 메모리가 부족해질 수 있습니다.
대용량 데이터 전체를 메모리에 캐싱하려 하면 시스템이 멈춥니다. 캐싱은 데이터 크기와 가용 메모리를 고려해서 신중하게 사용해야 합니다.
김개발 씨가 고개를 끄덕였습니다. "결국 핵심은 필요한 만큼만, 병렬로 처리하는 거네요." 박시니어 씨가 웃으며 말했습니다.
"정확해. 그리고 정말 큰 데이터를 다룬다면, TFRecord 포맷을 알아야 해."
실전 팁
💡 - cache()에 파일 경로를 전달하면 디스크에 캐싱됩니다: cache("/tmp/cache")
- interleave의 cycle_length는 보통 사용 가능한 CPU 코어 수로 설정합니다
6. TFRecord 포맷 이해
박시니어 씨가 마지막 주제를 꺼냈습니다. "대용량 데이터를 정말 효율적으로 다루려면 TFRecord를 알아야 해." 김개발 씨가 물었습니다.
"그냥 이미지 파일을 그대로 쓰면 안 되나요?" 박시니어 씨가 고개를 저었습니다. "수백만 개의 작은 파일은 디스크 I/O의 악몽이야."
TFRecord는 TensorFlow의 자체 바이너리 데이터 포맷입니다. 여러 개의 작은 파일을 하나의 큰 파일로 묶어 저장하므로 디스크 읽기 효율이 크게 향상됩니다.
또한 압축을 지원하고, 스트리밍 읽기가 가능하며, 분산 학습에 최적화되어 있습니다. 대규모 머신러닝 프로젝트에서 사실상 표준으로 사용됩니다.
다음 코드를 살펴봅시다.
import tensorflow as tf
# TFRecord 파일 쓰기
def serialize_example(feature, label):
example = tf.train.Example(features=tf.train.Features(feature={
'feature': tf.train.Feature(float_list=tf.train.FloatList(value=feature)),
'label': tf.train.Feature(int64_list=tf.train.Int64List(value=[label]))
}))
return example.SerializeToString()
# TFRecord 파일에 저장
with tf.io.TFRecordWriter('data.tfrecord') as writer:
for i in range(100):
example = serialize_example([1.0, 2.0, 3.0], i % 2)
writer.write(example)
# TFRecord 파일 읽기
def parse_example(serialized):
feature_description = {
'feature': tf.io.FixedLenFeature([3], tf.float32),
'label': tf.io.FixedLenFeature([], tf.int64)
}
return tf.io.parse_single_example(serialized, feature_description)
dataset = tf.data.TFRecordDataset('data.tfrecord')
dataset = dataset.map(parse_example)
프로젝트도 막바지에 접어들었습니다. 김개발 씨는 지금까지 배운 것들로 데이터 파이프라인을 구축했습니다.
하지만 박시니어 씨가 한 가지를 더 알려주고 싶어 했습니다. "100만 개의 이미지 파일이 있다고 생각해봐." 박시니어 씨가 말했습니다.
"매번 파일을 열고 닫는 것만으로도 엄청난 시간이 걸려." 왜 수많은 작은 파일이 문제일까요? 운영체제가 파일을 열 때마다 오버헤드가 발생합니다.
파일 시스템에서 위치를 찾고, 권한을 확인하고, 핸들을 생성합니다. 파일이 작을수록 이 오버헤드의 비중이 커집니다.
1KB 파일 100만 개를 읽는 것은 1GB 파일 하나를 읽는 것보다 훨씬 느립니다. TFRecord는 이 문제를 해결합니다.
비유하자면, TFRecord는 택배 묶음 배송과 같습니다. 물건 하나당 배송비를 내는 것보다, 여러 물건을 하나의 상자에 담아 한 번에 배송하는 것이 효율적이죠.
TFRecord도 마찬가지로 여러 데이터를 하나의 파일에 순차적으로 저장합니다. TFRecord의 구조를 이해해봅시다.
TFRecord 파일은 tf.train.Example의 연속입니다. 각 Example은 여러 Feature를 담고 있습니다.
Feature의 타입은 bytes, float, int64 세 가지입니다. 이미지는 bytes로, 라벨은 int64로 저장하는 식입니다.
위의 코드에서 serialize_example 함수가 핵심입니다. 먼저 tf.train.Features로 여러 Feature를 딕셔너리로 묶습니다.
그것을 tf.train.Example로 감쌉니다. 마지막으로 SerializeToString()으로 바이트 문자열로 변환합니다.
이 바이트열이 TFRecord 파일에 저장됩니다. 읽을 때는 parse_example 함수를 사용합니다.
feature_description에 각 Feature의 형태와 타입을 정의합니다. FixedLenFeature는 고정 크기 데이터에, VarLenFeature는 가변 크기 데이터에 사용합니다.
tf.io.parse_single_example로 바이트열을 다시 텐서로 복원합니다. TFRecord의 추가적인 장점이 있습니다.
압축을 지원합니다. TFRecordWriter에 compression_type='GZIP'을 전달하면 자동으로 압축됩니다.
스토리지를 절약하면서도 읽기 성능은 크게 떨어지지 않습니다. 샤딩도 가능합니다.
하나의 거대한 TFRecord 대신 여러 개의 작은 TFRecord로 나눌 수 있습니다. 분산 학습 시 각 워커가 다른 샤드를 읽으면 됩니다.
구글, 페이스북 등 대형 IT 기업의 ML 파이프라인은 대부분 TFRecord를 사용합니다. 주의할 점이 있습니다.
TFRecord는 생성하는 데 시간이 걸립니다. 원본 데이터를 한 번 변환해야 하니까요.
하지만 이 초기 투자는 학습 시간 단축으로 충분히 보상받습니다. 대규모 프로젝트에서는 반드시 고려해야 할 옵션입니다.
김개발 씨는 모든 내용을 메모했습니다. 이제 데이터가 아무리 커도 두렵지 않습니다.
tf.data.Dataset부터 TFRecord까지, 완벽한 데이터 파이프라인을 구축할 준비가 되었습니다. 박시니어 씨가 마무리했습니다.
"데이터 파이프라인은 ML 프로젝트의 기초야. 오늘 배운 것들을 잘 활용하면 어떤 프로젝트든 자신 있게 시작할 수 있을 거야."
실전 팁
💡 - TFRecord 파일은 보통 100MB~1GB 크기로 샤딩하는 것이 좋습니다
- 이미지는 tf.io.encode_jpeg로 압축 후 bytes_list로 저장하면 용량을 크게 줄일 수 있습니다
이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!
댓글 (0)
함께 보면 좋은 카드 뉴스
vLLM 통합 완벽 가이드
대규모 언어 모델 추론을 획기적으로 가속화하는 vLLM의 설치부터 실전 서비스 구축까지 다룹니다. PagedAttention과 연속 배칭 기술로 GPU 메모리를 효율적으로 활용하는 방법을 배웁니다.
Web UI Demo 구축 완벽 가이드
Gradio를 활용하여 머신러닝 모델과 AI 서비스를 위한 웹 인터페이스를 구축하는 방법을 다룹니다. 코드 몇 줄만으로 전문적인 데모 페이지를 만들고 배포하는 과정을 초급자도 쉽게 따라할 수 있도록 설명합니다.
Sandboxing & Execution Control 완벽 가이드
AI 에이전트가 코드를 실행할 때 반드시 필요한 보안 기술인 샌드박싱과 실행 제어에 대해 알아봅니다. 격리된 환경에서 안전하게 코드를 실행하고, 악성 동작을 탐지하는 방법을 단계별로 설명합니다.
Voice Design then Clone 워크플로우 완벽 가이드
AI 음성 합성에서 일관된 캐릭터 음성을 만드는 Voice Design then Clone 워크플로우를 설명합니다. 참조 음성 생성부터 재사용 가능한 캐릭터 구축까지 실무 활용법을 다룹니다.
Tool Use 완벽 가이드 - Shell, Browser, DB 실전 활용
AI 에이전트가 외부 도구를 활용하여 셸 명령어 실행, 브라우저 자동화, 데이터베이스 접근 등을 수행하는 방법을 배웁니다. 실무에서 바로 적용할 수 있는 패턴과 베스트 프랙티스를 담았습니다.