목차

1. 소스코드로 텐서플로우 맛보기 : [CNN] CIFAR-10 ~ cifar10_input.py (이번 글)



소스코드로 텐서플로우 맛보기 : [CNN] CIFAR-10


나름 직장 동료들과 열심히 공부를 하고 있고 또 이 딥러닝이라는 분야의 공부를 시작한지도 어언 1년이 다되간다.
하지만 한 때 유행했던 유머처럼 ‘딥러닝을 글로만 배웠어요~’인 상태이다보니 제대로 뭔가를 알고 있는 것인지
감조차 오지 않았다. 그래서 이제야 비로소 예제 코드를 돌려보기로 했다. 


다만 그저 샘플 소스를 다운로드 받고 실행하고 끝! 하는 것이 아닌 적어도 소스 코드가 어떤 의미인지는 알고
돌려보기로 했다. 그 시작으로 CNN쪽에 있는 CIFAR-10 예제를 대상으로 삼았다.


처음에는 함께 공부하는 직장 동료들과 직독직해 식으로 소스를 분석해보려고 했으나…
이런 상황을 ‘자만심 오졌다리~’라고 표현해야 하나…처음 import부터 막혀서 쩔쩔매다가 일단 내가
분석을 좀 하고 내용을 공유하기로 한 것이다.


이러한 형편이니 혹시라도 잘못된 내용이 있으면 따끔한 충고 부탁드린다…^^;;


cifar10_input.py


# sys.path 상의 가장 상위 모듈을 import 하는 것을 보장해 줌. 
from __future__ import absolute_import
# /연산자와 더불어 // 연산자 사용 가능, / 연산자는 실수형을 리턴, // 연산자는 몫 부분만 정수로 리턴
from __future__ import division
# print 함수에 ()를 사용할 수 있게 함
from __future__ import print_function


__future __의 의미 : Python 2에서 Python 3 함수를 사용할 수 있게 해줌
위의 3줄은 Python 2와 Python 3의 호환성을 위한 import이다.


# OS의 자원을 사용할 수있게 해주는 모듈
import os

# six(2 * 3)는 Python 2와 Python 3에서 차이나는 함수들을 함께 사용할 수 있게 해줌
# xrange는 3에서는 range
from six.moves import xrange  # pylint: disable=redefined-builtin
# 아기다리고기다리던 텐서플로우
import tensorflow as tf


데이터를 읽어들이기 위해 OS 자원을 사용하도록 해주고 range의 하위호환성을 위해 xrange를 import 했으며
마지막으로 텐서플로우를 import 함


IMAGE_SIZE = 24


32 X 32 사이즈의 이미지를 랜덤하게 24 X 24 사이즈로 Corp함으로써 전체 데이터셋의 크기가 커진다.


NUM_CLASSES = 10
NUM_EXAMPLES_PER_EPOCH_FOR_TRAIN = 50000
NUM_EXAMPLES_PER_EPOCH_FOR_EVAL = 10000


CIFAR-10 데이터 셋 관련 상수로 총 10개의 클래스(비행기, 자동차, 새, 고양이, 사슴, 개, 개구리, 말, 배, 트럭)가
있으며 학습을 위한 데이터 50000건 테스트를 위한 데이터 10000건으로 구성된다.


이 파일에는 총 4개의 함수가 있으며 각각 다음과 같다.

  • read_cifar10(filename_queue) : 파일 이름 목록을 받아와 CIFAR-10의 바이너리 데이터를 읽고 파싱하여 단일 오브젝트 형태로 반환한다. 이 오브젝트에는 height, width, depth, key, label, uint8image 등의 필드가 있다.
  • _generate_image_and_label_batch(image, label, min_queue_examples, batch_size, shuffle) : image와 label들을 담은 배치용 queue를 만들어 리턴한다.
  • distorted_inputs(data_dir, batch_size) : 데이터셋 확대를 위한 이미지 왜곡 작업을 진행한다.
    read_cifar10 함수를 호출하여 그 리턴 값을 가지고 작업한다. 학습 시 사용.
  • inputs(eval_data, data_dir, batch_size) : 평가를 위한 input에 사용하며 역시 read_cifar10
    함수를 호출하여 사용하며 Crop 외에 다른 조작은 하지 않는다. 


이미 코드에 영문 주석이 다 있지만 추가로 한글 주석을 추가하며 알아보자.


distorted_inputs(data_dir, batch_size)

def distorted_inputs(data_dir, batch_size):
  """Construct distorted input for CIFAR training using the Reader ops.
  Args:
    data_dir: Path to the CIFAR-10 data directory.
    batch_size: Number of images per batch.
  Returns:
    images: Images. 4D tensor of [batch_size, IMAGE_SIZE, IMAGE_SIZE, 3] size.
    labels: Labels. 1D tensor of [batch_size] size.
  """

# os.path.join 함수는 전달받은 파라미터를 이어 새로운 경로를 만드는 함수
# 아래 코드는 이 함수에서 파라미터로 받은 data_dir 경로와 그 경로 아래에 있는
# CIFAR-10의 이미지 파일이 담긴 data_batch_1.bin ~ data_batch_5.bin의
# 5개 파일에 대한 전체 경로를 요소로 하는 벡터(텐서)를 만드는 것이다.
  filenames = [os.path.join(data_dir, 'data_batch_%d.bin' % i)
               for i in xrange(1, 6)]

# 만일 배열 내에 파일 경로가 없으면 에러 발생
  for f in filenames:
    if not tf.gfile.Exists(f):
      raise ValueError('Failed to find file: ' + f)

# string_input_producer 함수는 필수 파라미터인 첫 번째 파라미터에 string 타입의 요소로 만들어진 
# 텐서 타입을 받아서 각 요소 문자열로 구성된 Queue 형태로 리턴을 해준다.
  # Create a queue that produces the filenames to read.
  filename_queue = tf.train.string_input_producer(filenames)

  with tf.name_scope('data_augmentation'):
    # Read examples from files in the filename queue.
# 아래 설명할 read_cifar10 함수로부터 라벨, 이미지 정보 등을 포함한 
# CIFAR10Record 클래스 타입을 톨려받는다.
    read_input = read_cifar10(filename_queue)

# cast 함수는 첫 번째 인자로 받은 텐서 타입의 파라미터를 두 번째 인자로 받은
# 데이터 타입의 요소를 가진 텐서로 돌려준다.
    reshaped_image = tf.cast(read_input.uint8image, tf.float32)

    height = IMAGE_SIZE
    width = IMAGE_SIZE

    # Image processing for training the network. Note the many random
    # distortions applied to the image.

    # Randomly crop a [height, width] section of the image.
# tf.random_crop 함수는 첫 번째 파라미터로 받은 텐서타입의 이미지들을 
# 두 번째 파라미터로 받은 크기로 무작위로 잘라 첫 번째 받은 파라미터와 같은 rank의
# 텐서 형태로 돌려준다. 
    distorted_image = tf.random_crop(reshaped_image, [height, width, 3])

    # Randomly flip the image horizontally.
# 좌우를 랜덤하게 뒤집은 형태의 텐서를 돌려준다.
    distorted_image = tf.image.random_flip_left_right(distorted_image)

    # Because these operations are not commutative, consider randomizing
    # the order their operation.
    # NOTE: since per_image_standardization zeros the mean and makes
    # the stddev unit, this likely has no effect see tensorflow#1458.
# 밝기와 콘트라스트를 랜텀하게 변형시킨 텐서를 돌려준다.
    distorted_image = tf.image.random_brightness(distorted_image,
                                                 max_delta=63)
    distorted_image = tf.image.random_contrast(distorted_image,
                                               lower=0.2, upper=1.8)
# random_crop부터 random_contrast까지는 데이터 셋 확장을 위해 이미지를 임의 조작하는
# 과정이다.

    # Subtract off the mean and divide by the variance of the pixels.
# 이미지를 표준화 하는 과정인 듯한데...어려워서 패쓰~
    float_image = tf.image.per_image_standardization(distorted_image)

    # Set the shapes of tensors.
# 텐서의 shape 설정
    float_image.set_shape([height, width, 3])
    read_input.label.set_shape([1])

    # Ensure that the random shuffling has good mixing properties.
# 전체 테스트용 이미지의 40%, 즉, 총 50000개의 테스트 이미지 중 20000개를 사용
    min_fraction_of_examples_in_queue = 0.4
    min_queue_examples = int(NUM_EXAMPLES_PER_EPOCH_FOR_TRAIN *
                             min_fraction_of_examples_in_queue)
    print ('Filling queue with %d CIFAR images before starting to train. '
           'This will take a few minutes.' % min_queue_examples)

  # Generate a batch of images and labels by building up a queue of examples.
# 배치 작업에 사용할 128개의 이미지를 shuffle하여 리턴함
  return _generate_image_and_label_batch(float_image, read_input.label,
                                         min_queue_examples, batch_size,
                                         shuffle=True)


read_cifar10(filename_queue)

"""Reads and parses examples from CIFAR10 data files.
  Recommendation: if you want N-way read parallelism, call this function
  N times.  This will give you N independent Readers reading different
  files & positions within those files, which will give better mixing of
  examples.
  Args:
    filename_queue: A queue of strings with the filenames to read from.
  Returns:
    An object representing a single example, with the following fields:
      height: number of rows in the result (32)
      width: number of columns in the result (32)
      depth: number of color channels in the result (3)
      key: a scalar string Tensor describing the filename & record number
        for this example.
      label: an int32 Tensor with the label in the range 0..9.
      uint8image: a [height, width, depth] uint8 Tensor with the image data
  """

# 이 함수의 리턴 값은 CIFAR10Record라는 class임 pass는 비어있는 클래스 선언 시 사용
# 이미 아는 바와 같이 텐서플로우의 Session.run이 실행되기 전까지는 비어있는 클래스이며
# Session.run이 실행된 이후에야 데이터 파일의 레코드들이 클래스에 들어가게 된다.
  class CIFAR10Record(object):
    pass
  result = CIFAR10Record()

# label_bytes는 말 그대로 라벨의 길이이고 1byte이다.
# result.height는 이미지의 높이
# result.width는 이미지의 넓이
# result.depth는 이미지를 구성하는 색상 채널
# image_bytes 결국 이미지를 구성하는 총 byte 수는 높이 * 넓이 * 색상 채널

  # Dimensions of the images in the CIFAR-10 dataset.
  # See http://www.cs.toronto.edu/~kriz/cifar.html for a description of the
  # input format.
  label_bytes = 1  # 2 for CIFAR-100
  result.height = 32
  result.width = 32
  result.depth = 3
  image_bytes = result.height * result.width * result.depth

# 모든 레코드는 라벨과 라벨에 해당하는 이미지로 구성되어있으므로  
# 전체 레코드 크기는 label_bytes + image_bytes로 고정
  # Every record consists of a label followed by the image, with a
  # fixed number of bytes for each.
  record_bytes = label_bytes + image_bytes

# tf.FixedLengthRecordReader는 파일로부터 고정길이의 레코드를 출력해주는 클래스
# 생성 시 첫 번째 파라미터는 읽어올 레코드의 바이트 수
  # Read a record, getting filenames from the filename_queue.  No
  # header or footer in the CIFAR-10 format, so we leave header_bytes
  # and footer_bytes at their default of 0.
  reader = tf.FixedLengthRecordReader(record_bytes=record_bytes)

# Queue 타입(FIFO)의 자료 구조를 파라미터로 받아 그 안의 레코드로부터
# Key와 Value를 받아오는 처리. key는 레코드가 포함된 파일명과 index의 구성으로
# 되어있으며, value는 사용할 라벨과 이미지가 포함된 텐서임.
  result.key, value = reader.read(filename_queue)

  # Convert from a string to a vector of uint8 that is record_bytes long.
# byte 타입의 문자열을 숫자형 벡터로 변환. 첫 번째 인자는 문자열로 구성된 텐서이며
# 모든 요소들은 동일한 길이여야 함. 두 번째 인자는 변환할 데이터 타입
  record_bytes = tf.decode_raw(value, tf.uint8)

  # The first bytes represent the label, which we convert from uint8->int32.
# 첫 번째 인자로 받은 텐서를 두 번째 인자로 받은 데이터 타입으로 형변환 함.
# 즉, 아래 코드는 위에서 구성된 record_bytes에서 첫 번째 바이트를 가져와 int32
# 타입으로 변환하여 리턴한다. 따라서 result.label은 1바이트 크기의 int32 타입 요소를
# 갖는 벡터이다.
  result.label = tf.cast(
      tf.strided_slice(record_bytes, [0], [label_bytes]), tf.int32)

  # The remaining bytes after the label represent the image, which we reshape
  # from [depth * height * width] to [depth, height, width].
# tf.reshape는 첫 번째 파라미터의 shape를 두 번째 파라미터로 받은 형태로 바꾼다.
# 아래 코드의 첫 번째 인자는 record_bytes에서 첫 바이트인 라벨을 제외한 나머지
# 바이트(이미지 부분)를 가져와 [3, 32, 32] 형태의 shape로 바꾼다. 
  depth_major = tf.reshape(
      tf.strided_slice(record_bytes, [label_bytes],
                       [label_bytes + image_bytes]),
      [result.depth, result.height, result.width])
  # Convert from [depth, height, width] to [height, width, depth].
# tf.transpose는 첫 번째 파라미터로 받은 텐서의 각 차원 값을 두 번째 파라미터로 전달받은
# 순서로 바꾼 텐서를 리턴한다. 위의 depth_major의 shape는 [3, 32, 32]이다.
# 즉, shape의 0번째 요소는 3, 1번째 요소는 32, 2번째 요소는 32이다. 이 것을 두 번째
# 파라미터처럼 인덱스를 [1, 2, 0]로 바꾸는 것이므로 1 번째 요소인 32가 맨 앞으로, 다음으로
# 2 번째 요소인 32가 오고 0번째 요소인 3은 맨 마지막으로 가게 되는 것이다.
# 결국 최초에 [depth, height, width]의 순서가 [height, width, depth]가 된다.
  result.uint8image = tf.transpose(depth_major, [1, 2, 0])

# 테스트 코드 시작 ##############################################
# 원본 코드에는 없는 내용이지만 아래 코드를 이용하여 간단하게 데이터를 정상적으로 불러왔는지
# 확인할 수 있다. 아래 코드를 싫행하면 총 100개의 이미지가 10 X 10 형태로 배열된 1개의 이미지가
# 만들어지며, label, key, value 값을 확인할 수 있다.
# 이 코드를 사용하려면 matplotlib.pyplot을 import해야 한다.
 fig, ax = plt.subplots(10, 10, figsize=(10, 10))
  with tf.Session() as sess:
      coord = tf.train.Coordinator()
      threads = tf.train.start_queue_runners(coord=coord, sess=sess)

      for i in range(10):
          for j in range(10):
              print(sess.run(result.label), sess.run(result.key), sess.run(value))
              img = sess.run(result.uint8image)
              ax[i][j].set_axis_off()
              ax[i][j].imshow(img)

      dir = os.path.abspath("cifar10_image")
      plt.savefig(dir + "/" + "image")
      print(dir)

      coord.request_stop()
      coord.join(threads)
# 테스트 코드 끝 ############################################

  return result


_generate_image_and_label_batch(image, label, min_queue_examples, batch_size, shuffle)

"""Construct a queued batch of images and labels.
  Args:
    image: 3-D Tensor of [height, width, 3] of type.float32.
    label: 1-D Tensor of type.int32
    min_queue_examples: int32, minimum number of samples to retain
      in the queue that provides of batches of examples.
    batch_size: Number of images per batch.
    shuffle: boolean indicating whether to use a shuffling queue.
  Returns:
    images: Images. 4D tensor of [batch_size, height, width, 3] size.
    labels: Labels. 1D tensor of [batch_size] size.
  """
  # Create a queue that shuffles the examples, and then
  # read 'batch_size' images + labels from the example queue.
# 각각 배치를 생성하는 코드로 shuffle_batch는 무작위로 뒤섞은 배치를 생성하며
# batch는 입력 텐서와 레코드 순서가 동일한 배치를 생성한다. 배치 생성 시 16개의
# thread를 사용한다.
  num_preprocess_threads = 16
  if shuffle:
    images, label_batch = tf.train.shuffle_batch(
        [image, label],
        batch_size=batch_size,
        num_threads=num_preprocess_threads,
        capacity=min_queue_examples + 3 * batch_size,
        min_after_dequeue=min_queue_examples)
  else:
    images, label_batch = tf.train.batch(
        [image, label],
        batch_size=batch_size,
        num_threads=num_preprocess_threads,
        capacity=min_queue_examples + 3 * batch_size)

  # Display the training images in the visualizer.
# 텐서보드에서 이미지를 보여주긴 위한 코드
  tf.summary.image('images', images)

# 배치 과정을 거친 이미지와 라벨의 최종 shape는 각각 [128, 32, 32, 3]과 [128]이다.
  return images, tf.reshape(label_batch, [batch_size])


inputs(eval_data, data_dir, batch_size)

"""Construct input for CIFAR evaluation using the Reader ops.
  Args:
    eval_data: bool, indicating if one should use the train or eval data set.
    data_dir: Path to the CIFAR-10 data directory.
    batch_size: Number of images per batch.
  Returns:
    images: Images. 4D tensor of [batch_size, IMAGE_SIZE, IMAGE_SIZE, 3] size.
    labels: Labels. 1D tensor of [batch_size] size.
  """

# eval_data 값에 따라 학습용 데이터를 불러올지 평가용 데이터를 불러올지 결정한다.
  if not eval_data:
    filenames = [os.path.join(data_dir, 'data_batch_%d.bin' % i)
                 for i in xrange(1, 6)]
    num_examples_per_epoch = NUM_EXAMPLES_PER_EPOCH_FOR_TRAIN
  else:
    filenames = [os.path.join(data_dir, 'test_batch.bin')]
    num_examples_per_epoch = NUM_EXAMPLES_PER_EPOCH_FOR_EVAL

# 이후 코드는 이미지 변형 (random_flip_left_right, random_brightness,
# random_contrast) 처리를 제외하고는 distorted_inputs(data_dir, batch_size)
# 함수와 동일하다.
  for f in filenames:
    if not tf.gfile.Exists(f):
      raise ValueError('Failed to find file: ' + f)

  with tf.name_scope('input'):
    # Create a queue that produces the filenames to read.
    filename_queue = tf.train.string_input_producer(filenames)

    # Read examples from files in the filename queue.
    read_input = read_cifar10(filename_queue)
    reshaped_image = tf.cast(read_input.uint8image, tf.float32)

    height = IMAGE_SIZE
    width = IMAGE_SIZE

    # Image processing for evaluation.
    # Crop the central [height, width] of the image.
    resized_image = tf.image.resize_image_with_crop_or_pad(reshaped_image,
                                                           height, width)

    # Subtract off the mean and divide by the variance of the pixels.
    float_image = tf.image.per_image_standardization(resized_image)

    # Set the shapes of tensors.
    float_image.set_shape([height, width, 3])
    read_input.label.set_shape([1])

    # Ensure that the random shuffling has good mixing properties.
    min_fraction_of_examples_in_queue = 0.4
    min_queue_examples = int(num_examples_per_epoch *
                             min_fraction_of_examples_in_queue)

  # Generate a batch of images and labels by building up a queue of examples.
  return _generate_image_and_label_batch(float_image, read_input.label,
                                         min_queue_examples, batch_size,
                                         shuffle=False)


정리


늘 어처구니 없는 실수가 따라다닌다.
CIFAR-10 홈페이지에 가면 다음과 같이 데이터 셋이 3가지 버전이 있다.

  • CIFAR-10 python version
  • CIFAR-10 Matlab version
  • CIFAR-10 binary version (suitable for C programs)


나는 Tensorflow가 python 기반으로 코딩이 되므로 당연히 python versiond을 받아야 한다고 생각했다.
그런데 python 버전을 사용하여 코드를 실행하다보니 뭔가 이상했다. 간간히 데이터를 제대로 불러왔는지
확인하기 위한 print문에 이상한 결과가 찍히는 것이다. CIFAR10Record 클래스의 멤버들에 대한 shape나
rank는 물론 중간에 시험삼아 100개의 이미지를 출력한 것도 모든 이미지가 깨져서 나왔다.



주말 2일을 고민하다가 문득 원래의 코드에는 파일명을 가져올 때 .bin이라는 확장자가 있었는데 내가 사용하는
데이터 파일에는 확장자가 없는 것을 발견했다. 그리고 겨우 내가 잘못된 버전의 데이터 셋을 받았다는 것을 
깨달았다…ㅠ.ㅠ


새로 받은 버전의 데이터 셋은 아래와 같이 이미지가 정상적으로 나왔다.



이제 겨우 파일 하나 분석해봤을 뿐인데 벌써 지친다…특히나 텐서라는 개념과 행렬 연산 그리고 Tensorflow의
지연 실행이라는 메커니즘은 정말 적응이 안된다…ㅠ.ㅠ 다음 포스팅에서는 cifar10.py 파일을 분석해보자.

블로그 이미지

마즈다

이미 마흔을 넘어섰지만 아직도 꿈을 좇고 있습니다. 그래서 그 꿈에 다가가기 위한 단편들을 하나 둘 씩 모아가고 있지요. 이 곳에 그 단편들이 모일 겁니다...^^

티스토리 툴바