다음 글을 참조하여 번역합니다(+ 개인 공부), 예제는 tf 2.0을 기준으로 합니다.

https://www.tensorflow.org/guide/data?hl=en

 

tf.data: Build TensorFlow input pipelines  |  TensorFlow Core

The tf.data API enables you to build complex input pipelines from simple, reusable pieces. For example, the pipeline for an image model might aggregate data from files in a distributed file system, apply random perturbations to each image, and merge random

www.tensorflow.org


Reading Input Data

Consuming NumPy arrays

다양한 NumPy array를 로딩하는 예제는 다음을 참조하세요.

https://www.tensorflow.org/tutorials/load_data/numpy

만약 모든 데이터가 메모리에 존재한다면, 이들로부터 Dataset을 만드는 가장 간단한 방법은 Dataset.from_tensor_slices()를 사용하여 tf.Tensor로 변환하는 것입니다.

train, test = tf.keras.datasets.fashion_mnist.load_data()

images, labels = train
images = images/255

dataset = tf.data.Dataset.from_tensor_slices((images, labels))
dataset
  • <TensorSliceDataset shapes: ((28, 28), ()), types: (tf.float64, tf.uint8)>
  • (28, 28)은 이미지이며, ()은 스칼라 형태로 label을 나타냅니다.

Consuming Python generators

tf.data.Dataset으로 쉽게 데이터를 살펴 볼 수 있는 또다른 방법은 파이썬 제네레이터를 사용하는 것입니다.

def count(stop):
  i = 0
  while i<stop:
    yield i
    i += 1
for n in count(5):
  print(n)
  • 0 1 2 3 4

Dataset.from_generator 생성자는 tf.data.Dataset을 제네레이터처럼 사용할 수 있게 합니다.

이 생성자는 반복자가 아닌, 입력을 사용합니다. 이는 데이터의 끝에 도달했을 때, 제네레이터가 재시작할 수 있도록 도와줍니다. 선택적 args 인자를 가지는데, 이는 호출이 가능합니다.

output_types 인자는 tf.data가 내부적으로 tf.Graph를 빌드하고, graph edge에 tf.dtype을 요구하기 때문에 필요합니다.

ds_counter = tf.data.Dataset.from_generator(count, args=[25], output_types=tf.int32, output_shapes = (), )
  • count는 위에서 선언한 파이썬 제네레이터입니다.
  • arg는 인자로서 count 함수의 stop 인자로 통합니다.
for count_batch in ds_counter.repeat().batch(10).take(10):
  print(count_batch.numpy())
  • [0 1 2 3 4 5 6 7 8 9] [10 11 12 13 14 15 16 17 18 19] [20 21 22 23 24 0 1 2 3 4] [ 5 6 7 8 9 10 11 12 13 14] [15 16 17 18 19 20 21 22 23 24] [0 1 2 3 4 5 6 7 8 9] [10 11 12 13 14 15 16 17 18 19] [20 21 22 23 24 0 1 2 3 4] [ 5 6 7 8 9 10 11 12 13 14] [15 16 17 18 19 20 21 22 23 24]
  • 잘 보면 batch(10).take(10)이므로 총 10번에 10개씩 받아오며, stop 25까지 작동합니다. 25에 도달한 경우는 제네레이터가 restart하는 것을 3번째 배열에서 볼 수 있습니다.

output_shapes 인자는 필요하진 않지만, tensorflow의 많은 연산이 알 수 없는 rank 단위는 지원하지 않으므로 권장 사항입니다. 특정 축 또는 길이가 알 수 없거나, 가변인 경우, None 으로 지정하세요.

다른 dataset 메소드를 사용하기 위해 output_shapesoutput_types는 중요한 요소입니다.

다음은 두 가지 요소의 필요성을 보여주는 제네레이터 예제입니다. 두 배열은 길이가 알려지지 않은 벡터입니다.

def gen_series():
  i = 0
  while True:
    size = np.random.randint(0, 10)
    yield i, np.random.normal(size=(size,))
    i += 1
for i, series in gen_series():
  print(i, ":", str(series))
  if i > 5:
    break
  • 0 : [ 0.3464 -0.306 ] 1 : [-0.7686] 2 : [1.9423] 3 : [] 4 : [ 0.5828 -0.0588 0.0094 -0.9467] 5 : [-0.8972 -0.4949 1.1115 0.8208 0.843 0.2968 -2.7236 -0.844 -1.7327] 6 : [ 1.2727 -0.6278 0.1622 -1.4087 -0.7683 -0.3966 0.3112]

첫 번째 출력값의 형태는 int32, 두 번째는 float32입니다.

첫 번째 요소는 스칼라이며, () shape입니다. 두 번째 요소는 길이가 알려지지 않은 벡터로 (None, ) shape입니다.

ds_series = tf.data.Dataset.from_generator(
    gen_series, 
    output_types=(tf.int32, tf.float32), 
    output_shapes=((), (None,)))

ds_series
  • <FlatMapDataset shapes: ((), (None,)), types: (tf.int32, tf.float32)>

이제 일반적인 tf.data.Dataset처럼 사용할 수 있습니다. variable shape과 함께 배치를 뽑아올 때는 Dataset.padded_batch를 사용합니다.

ds_series_batch = ds_series.shuffle(20).padded_batch(10)

ids, sequence_batch = next(iter(ds_series_batch))
print(ids.numpy())
print()
print(sequence_batch.numpy())
  • [ 3 2 5 8 0 11 24 26 16 12] [[ 3.3757 0.791 -0.7864 -0.5299 -0.5024 0. 0. 0. 0. ] [-0.8493 0. 0. 0. 0. 0. 0. 0. 0. ] [-0.3736 0.2187 0.3256 -0.8628 2.3045 0.7726 1.9534 0.1123 0.3906] [ 0.3752 1.0399 -1.6983 -1.2217 -1.2176 -1.1055 0.7014 0. 0. ] [ 0.2049 -0.5775 -1.5055 0. 0. 0. 0. 0. 0. ] [-2.0829 0.7266 -0.0104 -1.2408 -0.715 -0.232 0.2391 0. 0. ] [-0.0439 -0.3391 1.5569 -0.7063 1.3729 -0.31 0.9572 -0.0446 0.0635] [ 0. 0. 0. 0. 0. 0. 0. 0. 0. ] [-0.5468 0.3916 -0.432 0.6168 -1.0789 0.8624 -1.2116 -1.1322 0.2158] [ 0. 0. 0. 0. 0. 0. 0. 0. 0. ]]
  • padded_batch(10)이기 때문에 10개씩 가져옵니다(첫 번째 int 요소가 10개인 것을 보면 알 수 있습니다). padded_batch는 가변길이(최소 0부터 최대 10까지)에서 나머지 요소에 0을 넣어줍니다.
  • ds_series는 (0, 10) 범위의 값을 뽑아내니까요. shuffle(20)은 버퍼 사이즈가 20이라는 의미입니다. 아마 데이터의 개수가 21개를 넘어갈 경우 elements가 뽑히지 않을 것입니다.

좀 더 현실적인 예시를 위해 precessing.image.ImageDataGenerator를 사용해봅니다. 데이터를 다운로드받습니다.

flowers = tf.keras.utils.get_file(
    'flower_photos',
    'https://storage.googleapis.com/download.tensorflow.org/example_images/flower_photos.tgz',
    untar=True)

image.ImageDataGenerator를 만듭니다.

img_gen = tf.keras.preprocessing.image.ImageDataGenerator(rescale=1./255, rotation_range=20)
images, labels = next(img_gen.flow_from_directory(flowers))
print(images.dtype, images.shape)
print(labels.dtype, labels.shape)
  • float32 (32, 256, 256, 3)
  • float32 (32, 5)
ds = tf.data.Dataset.from_generator(
    img_gen.flow_from_directory, args=[flowers], 
    output_types=(tf.float32, tf.float32), 
    output_shapes=([32,256,256,3], [32,5])
)

ds
  • <FlatMapDataset shapes: ((32, 256, 256, 3), (32, 5)), types: (tf.float32, tf.float32)>
  • ImageDataGenerator를 tf.data.Dataset으로 래핑하여 사용하는 것을 볼 수 있습니다.

Consuming TFRecord data

end-to-end example을 원한다면 Loading TFRecords를 참고하세요.

tf.data API는 메모리에 적재하기 힘든 매우 큰 데이터셋을 다룰 때, 다양한 file format을 다룰 수 있도록 도와줍니다. 예를 들어, TFRecord file format은 많은 TF app가 학습 데이터에 사용하는 간단한 record-oriented 이진 형식입니다. tf.data.TFRecordDataset 클래스는 인풋 파이프라인에서 하나 또는 그 이상의 TFRecord 파일의 내용이 흐르도록 합니다.

French Street Name Signs (FSNS)을 사용하는 예제입니다.

# Creates a dataset that reads all of the examples from two files.
fsns_test_file = tf.keras.utils.get_file("fsns.tfrec", "https://storage.googleapis.com/download.tensorflow.org/data/fsns-20160927/testdata/fsns-00000-of-00001")

TFRecordDataset을 초기화하는 filenames 인자는 string, string 배열 또는 string tf.Tensor를 전달받을 수 있습니다. 만약 학습과 검증을 위해 두 개의 파일을 사용한다면, 파일 이름을 입력으로 사용하여 데이터셋을 생성하는 팩토리 메소드로 만들 수 있습니다.

dataset = tf.data.TFRecordDataset(filenames = [fsns_test_file])
dataset
  • <TFRecordDatasetV2 shapes: (), types: tf.string>

많은 TensorFlow 프로젝트는 TFRecord에서 직렬화된 tf.train.Example을 사용합니다. 따라서 사용하기 전에 디코딩해야 합니다.

raw_example = next(iter(dataset))
parsed = tf.train.Example.FromString(raw_example.numpy())

parsed.features.feature['image/text']
  • bytes_list { value: "Rue Perreyon" }

Consuming text data

end to end example은 다음을 참고하세요.

많은 데이터셋은 하나 또는 그 이상의 text 파일에 분산되어 있습니다. tf.data.TextLineDataset은 준비된 텍스트 파일에서 line 단위로 추출하는 쉬운 방법을 제공합니다. 주어진 하나 또는 그 이상의 파일 이름에서, TExtLineDataset은 line 단위로 string-value를 생성해 줄 것입니다.

directory_url = 'https://storage.googleapis.com/download.tensorflow.org/data/illiad/'
file_names = ['cowper.txt', 'derby.txt', 'butler.txt']

file_paths = [
    tf.keras.utils.get_file(file_name, directory_url + file_name)
    for file_name in file_names
]
dataset = tf.data.TextLineDataset(file_paths)

첫 번째 파일의 5개 행을 보여줍니다.

for line in dataset.take(5):
  print(line.numpy())
  • b"\xef\xbb\xbfAchilles sing, O Goddess! Peleus' son;"
    b'His wrath pernicious, who ten thousand woes'
    b"Caused to Achaia's host, sent many a soul"
    b'Illustrious into Ades premature,'
    b'And Heroes gave (so stood the will of Jove)'

Dataset.interleave는 파일을 번갈아 가면서 사용할 수 있게 해줍니다. 다음은 각 파일에서 나오는 문장의 예를 보여줍니다. cycle_length=3이므로 파일당 3개의 행씩 번갈아가면서 보여주겠군요.

files_ds = tf.data.Dataset.from_tensor_slices(file_paths)
lines_ds = files_ds.interleave(tf.data.TextLineDataset, cycle_length=3)

for i, line in enumerate(lines_ds.take(9)):
  if i % 3 == 0:
    print()
  print(line.numpy())
  • b"\xef\xbb\xbfAchilles sing, O Goddess! Peleus' son;"
    b"\xef\xbb\xbfOf Peleus' son, Achilles, sing, O Muse,"
    b'\xef\xbb\xbfSing, O goddess, the anger of Achilles son of Peleus, that brought'

    b'His wrath pernicious, who ten thousand woes'
    b'The vengeance, deep and deadly; whence to Greece'
    b'countless ills upon the Achaeans. Many a brave soul did it send'

    b"Caused to Achaia's host, sent many a soul"
    b'Unnumbered ills arose; which many a soul'
    b'hurrying down to Hades, and many a hero did it yield a prey to dogs and'

기본적으로 TextLineDataset은 파일의 모든 line을 살펴보기 때문에 만약 파일에 header 행이나 주석이 포함된 경우 사용이 바람직하지 않을 수 있습니다. header 행이나 주석과 같은 불필요한 내용은 Dataset.skip(), Dataset.filter()를 사용하여 배제할 수 있습니다. 다음 예제는 첫 번째 행을 건너뛰고, 생존자 데이터만 찾는 경우입니다.

titanic_file = tf.keras.utils.get_file("train.csv", "https://storage.googleapis.com/tf-datasets/titanic/train.csv")
titanic_lines = tf.data.TextLineDataset(titanic_file)
for line in titanic_lines.take(10):
  print(line.numpy())
  • b'survived,sex,age,n_siblings_spouses,parch,fare,class,deck,embark_town,alone'
    b'0,male,22.0,1,0,7.25,Third,unknown,Southampton,n'
    b'1,female,38.0,1,0,71.2833,First,C,Cherbourg,n'
    b'1,female,26.0,0,0,7.925,Third,unknown,Southampton,y'
    b'1,female,35.0,1,0,53.1,First,C,Southampton,n'
    b'0,male,28.0,0,0,8.4583,Third,unknown,Queenstown,y'
    b'0,male,2.0,3,1,21.075,Third,unknown,Southampton,n'
    b'1,female,27.0,0,2,11.1333,Third,unknown,Southampton,n'
    b'1,female,14.0,1,0,30.0708,Second,unknown,Cherbourg,n'
    b'1,female,4.0,1,1,16.7,Third,G,Southampton,n'
  • titanic 데이터에서 10개의 행을 불러오고 있습니다. 또, 우리에게 불필요한 header 행이 포함되어 있는 것을 볼 수 있습니다.
def survived(line):
  return tf.not_equal(tf.strings.substr(line, 0, 1), "0")

survivors = titanic_lines.skip(1).filter(survived)
for line in survivors.take(10):
  print(line.numpy())
  • b'1,female,38.0,1,0,71.2833,First,C,Cherbourg,n'
    b'1,female,26.0,0,0,7.925,Third,unknown,Southampton,y'
    b'1,female,35.0,1,0,53.1,First,C,Southampton,n'
    b'1,female,27.0,0,2,11.1333,Third,unknown,Southampton,n'
    b'1,female,14.0,1,0,30.0708,Second,unknown,Cherbourg,n'
    b'1,female,4.0,1,1,16.7,Third,G,Southampton,n'
    b'1,male,28.0,0,0,13.0,Second,unknown,Southampton,y'
    b'1,female,28.0,0,0,7.225,Third,unknown,Cherbourg,y'
    b'1,male,28.0,0,0,35.5,First,A,Southampton,y'
    b'1,female,38.0,1,5,31.3875,Third,unknown,Southampton,n'
  • tf.strings.substr(line, 0, 1): 0번째의 str 형태의 문자가 "0"인 것을 모두 걸러내고 있습니다. 아마 1이 생존자를 타나내는 것 같습니다. 또 skip(1)을 통해 header 행을 걸러내었음을 볼 수 있습니다.
  • tf.not_equal(x, y)는 (x != y)에 대한 boolean 값을 반환합니다. 즉, 1인 경우는 True를 반환하겠군요. filter는 false 값은 전부 제외 처리하는 것 같습니다.

Consuming CSV data

더 많은 예제는 다음_1과 다음_2를 참조하세요.


CSV 파일 포맷은 일반 텍스트를 테이블 형태의 데이터로 저장하기 위해 사용하는 대중적인 방법입니다.

titanic_file = tf.keras.utils.get_file("train.csv", "https://storage.googleapis.com/tf-datasets/titanic/train.csv")

df = pd.read_csv(titanic_file, index_col=None)
df.head()

만약 메모리에 데이터가 존재한다면 Dataset.from_tensor_slices를 사용하여 사전 형태로 쉽게 불러올 수 있습니다.

titanic_slices = tf.data.Dataset.from_tensor_slices(dict(df))

for feature_batch in titanic_slices.take(1):
  for key, value in feature_batch.items():
    print("  {!r:20s}: {}".format(key, value))
  • take(1)을 통해 1 크기의 배치를 불러오고, dict 형태이기 때문에 items()로 value, key를 받습니다.
  •  'survived'          : 0 
     'sex'               : b'male' 
     'age'               : 22.0 
     'n_siblings_spouses': 1 
     'parch'             : 0 
     'fare'              : 7.25 
     'class'             : b'Third' 
     'deck'              : b'unknown' 
     'embark_town'       : b'Southampton' 
     'alone'             : b'n'

보다 확장 가능한 방법은 필요에 따라 디스크에서 로드하는 것입니다.
tf.data 모듈은 RFC 4180을 준수하는 하나 또는 그 이상의 CSV 파일로부터 데이터를 추출하기 위한 메소드를 제공합니다. + RFC 4180은 CSV 파일 구축을 위해 제안되는 규칙입니다.

experimental.make_csv_dataset는 csv 파일을 읽어오는 고수준 인터페이스 함수입니다. 이 함수는 column type을 추론하거나
batching, shuffling과 같은 많은 특성들을 쉽게 사용할 수 있도록 도와줍니다.

titanic_batches = tf.data.experimental.make_csv_dataset(
    titanic_file, batch_size=4,
    label_name="survived")
for feature_batch, label_batch in titanic_batches.take(1):
  print("'survived': {}".format(label_batch))
  print("features:")
  for key, value in feature_batch.items():
    print("  {!r:20s}: {}".format(key, value))
  • 'survived': [1 0 1 0]
    features:
      'sex'               : [b'female' b'male' b'female' b'male']
      'age'               : [30. 28.  2. 28.]
      'n_siblings_spouses': [3 0 0 0]
      'parch'             : [0 0 1 0]
      'fare'              : [21.      7.7958 12.2875 26.55  ]
      'class'             : [b'Second' b'Third' b'Third' b'First']
      'deck'              : [b'unknown' b'unknown' b'unknown' b'C']
      'embark_town'       : [b'Southampton' b'Southampton' b'Southampton' b'Southampton']
      'alone'             : [b'n' b'y' b'n' b'y']

select_columns 인자를 사용해서 원하는 column만 사용할 수 있습니다.

titanic_batches = tf.data.experimental.make_csv_dataset(
    titanic_file, batch_size=4,
    label_name="survived", select_columns=['class', 'fare', 'survived'])
for feature_batch, label_batch in titanic_batches.take(1):
  print("'survived': {}".format(label_batch))
  for key, value in feature_batch.items():
    print("  {!r:20s}: {}".format(key, value))
  • 'survived': [0 0 0 0]
      'fare'              : [29.125   7.8958 77.2875  7.75  ]
      'class'             : [b'Third' b'Third' b'First' b'Third']

섬세한 제어를 가능하게 하는 low-level의 experimental.CsvDataset도 있습니다. 이는 column type 추론을 제공하지 않습니다. 대신 각 컬럼의 type을 꼭 구체화해야 합니다.

titanic_types  = [tf.int32, tf.string, tf.float32, tf.int32, tf.int32, tf.float32, tf.string, tf.string, tf.string, tf.string] 
dataset = tf.data.experimental.CsvDataset(titanic_file, titanic_types , header=True)

for line in dataset.take(10):
  print([item.numpy() for item in line])
  • [0, b'male', 22.0, 1, 0, 7.25, b'Third', b'unknown', b'Southampton', b'n']
    [1, b'female', 38.0, 1, 0, 71.2833, b'First', b'C', b'Cherbourg', b'n']
    [1, b'female', 26.0, 0, 0, 7.925, b'Third', b'unknown', b'Southampton', b'y']
    [1, b'female', 35.0, 1, 0, 53.1, b'First', b'C', b'Southampton', b'n']
    [0, b'male', 28.0, 0, 0, 8.4583, b'Third', b'unknown', b'Queenstown', b'y']
    [0, b'male', 2.0, 3, 1, 21.075, b'Third', b'unknown', b'Southampton', b'n']
    [1, b'female', 27.0, 0, 2, 11.1333, b'Third', b'unknown', b'Southampton', b'n']
    [1, b'female', 14.0, 1, 0, 30.0708, b'Second', b'unknown', b'Cherbourg', b'n']
    [1, b'female', 4.0, 1, 1, 16.7, b'Third', b'G', b'Southampton', b'n']
    [0, b'male', 20.0, 0, 0, 8.05, b'Third', b'unknown', b'Southampton', b'y']

만약 컬럼에서 몇 가지 데이터가 비어있을 경우, low-level 인터페이스는 column type 대신에 기본값을 제공하도록 할 수 있습니다.

%%writefile missing.csv
1,2,3,4
,2,3,4
1,,3,4
1,2,,4
1,2,3,
,,,
  • Writing missing.csv
# Creates a dataset that reads all of the records from two CSV files, each with
# four float columns which may have missing values.

record_defaults = [999,999,999,999]
dataset = tf.data.experimental.CsvDataset("missing.csv", record_defaults)
dataset = dataset.map(lambda *items: tf.stack(items))
dataset
  • <MapDataset shapes: (4,), types: tf.int32>
for line in dataset:
  print(line.numpy())
  • [1 2 3 4]
    [999   2   3   4]
    [  1 999   3   4]
    [  1   2 999   4]
    [  1   2   3 999]
    [999 999 999 999]

기본적으로 CsvDataset은 모든 행과 열을 반환합니다. 이는 header 행 또는 원하는 column이 포함되어 있는 경우 바람직하지 않을 수 있습니다. header와 select_cols 인자를 통해 제거할 수 있습니다.

# Creates a dataset that reads all of the records from two CSV files with
# headers, extracting float data from columns 2 and 4.
record_defaults = [999, 999] # Only provide defaults for the selected columns
dataset = tf.data.experimental.CsvDataset("missing.csv", record_defaults, select_cols=[1, 3])
dataset = dataset.map(lambda *items: tf.stack(items))
dataset
  • <MapDataset shapes: (2,), types: tf.int32>
for line in dataset:
  print(line.numpy())
  • [2 4]
    [2 4]
    [999   4]
    [2 4]
    [  2 999]
    [999 999]

Consuming sets of files

데이터셋은 여러 가지 파일에 분산되어 저장되어 있을 수 있습니다.

flowers_root = tf.keras.utils.get_file(
    'flower_photos',
    'https://storage.googleapis.com/download.tensorflow.org/example_images/flower_photos.tgz',
    untar=True)
flowers_root = pathlib.Path(flowers_root)

root directory는 각 클래스의 directory를 포함합니다.

for item in flowers_root.glob("*"):
  print(item.name)

다음 예는 각 클래스의 directory를 보여줍니다.

list_ds = tf.data.Dataset.list_files(str(flowers_root/'*/*'))

for f in list_ds.take(5):
  print(f.numpy())
  • b'/home/kbuilder/.keras/datasets/flower_photos/roses/6409000675_6eb6806e59.jpg' b'/home/kbuilder/.keras/datasets/flower_photos/tulips/4520577328_a94c11e806_n.jpg' b'/home/kbuilder/.keras/datasets/flower_photos/sunflowers/4933229889_c5d9e36392.jpg' b'/home/kbuilder/.keras/datasets/flower_photos/roses/22506717337_0fd63e53e9.jpg' b'/home/kbuilder/.keras/datasets/flower_photos/daisy/20182559506_40a112f762.jpg'
  • Dataset.list_file() 함수는 클래스 디렉토리를 받아 하위에 존재하는 이미지의 경로를 가져다 주는 것 같아보입니다.

tf.io.read_file 함수를 사용해서 경로에서 레이블을 추출하고, (image, label) 쌍을 반환합니다.

def process_path(file_path):
  label = tf.strings.split(file_path, '/')[-2]
  return tf.io.read_file(file_path), label

labeled_ds = list_ds.map(process_path)
  • tf.strings.split을 통해 label만 추출하고 있습니다.
for image_raw, label_text in labeled_ds.take(1):
  print(repr(image_raw.numpy()[:100]))
  print()
  print(label_text.numpy())
  • b'\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01\x01\x00\x00\x01\x00\x01\x00\x00\xff\xdb\x00C\x00\x03\x02\x02\x03\x02\x02\x03\x03\x03\x03\x04\x03\x03\x04\x05\x08\x05\x05\x04\x04\x05\n\x07\x07\x06\x08\x0c\n\x0c\x0c\x0b\n\x0b\x0b\r\x0e\x12\x10\r\x0e\x11\x0e\x0b\x0b\x10\x16\x10\x11\x13\x14\x15\x15\x15\x0c\x0f\x17\x18\x16\x14\x18\x12\x14\x15\x14\xff\xdb\x00C\x01\x03\x04\x04\x05\x04\x05'

    b'sunflowers'