みらいテックラボ

音声・画像認識や機械学習など, 週末プログラマである管理人が興味のある技術の紹介や実際にトライしてみた様子などメモしていく.

第1回FR FRONTIER:ファッション画像における洋服の「色」分類にチャレンジ!!(4)

これは, 人工知能技術戦略会議等主催 第1回AIチャレンジコンテスト[1]に引き続き, ユニクロを展開しているファーストリテイリング主催で今年4月~7月に開催された「第1回 FR FRONTIER :ファッション画像における洋服の「色」分類」[2][3]にチャレンジしたときの取組みについて, 数回に分けて紹介するものである.


関連記事:
第1回FR FRONTIER:ファッション画像における洋服の「色」分類にチャレンジ!!(1)
第1回FR FRONTIER:ファッション画像における洋服の「色」分類にチャレンジ!!(2)
第1回FR FRONTIER:ファッション画像における洋服の「色」分類にチャレンジ!!(3)
・第1回FR FRONTIER:ファッション画像における洋服の「色」分類にチャレンジ!!(4)
第1回FR FRONTIER:ファッション画像における洋服の「色」分類にチャレンジ!!(5)



前回に続き, 今回もモデリングについて試したことの一つを紹介する.

4. モデリング(2)

今回は, RGB画像と「2. データ利用方法」[4]で検討したストグラム画像の両方を使って「色」識別する方法を試してみた.

4.1 モデル
領域の「色」識別を行うモデルの構造は, 以下のような2種類の入力を扱うCNNモデルとした.

f:id:moonlight-aska:20170803000812p:plain:w500

コード:

# CNNモデル
left = Sequential()
left.add(Conv2D(64, (3, 3),
                padding='same',
                input_shape=(IMG_HEIGHT, IMG_WIDTH, IMG_CHANNELS),
                activation='relu'))
left.add(MaxPooling2D(pool_size=(2, 2)))
left.add(Conv2D(128, (3, 3), padding='same', activation='relu'))
left.add(MaxPooling2D(pool_size=(2, 2)))
left.add(Conv2D(256, (3, 3), padding='same', activation='relu'))
left.add(MaxPooling2D(pool_size=(2, 2)))
left.add(Dropout(0.5))
left.add(Flatten())

right = Sequential()
right.add(Conv2D(64, (3, 3),
                 padding='same',
                 input_shape=(IMG_HEIGHT, IMG_WIDTH, IMG_CHANNELS),
                 activation='relu'))
right.add(MaxPooling2D(pool_size=(2, 2)))
right.add(Conv2D(128, (3, 3), padding='same', activation='relu'))
right.add(MaxPooling2D(pool_size=(2, 2)))
right.add(Conv2D(256, (3, 3), padding='same', activation='relu'))
right.add(MaxPooling2D(pool_size=(2, 2)))
right.add(Dropout(0.5))
right.add(Flatten())

model = Sequential()
model.add(Merge([left, right], mode='concat'))
model.add(Dense(512, activation='relu'))
model.add(Dense(512, activation='relu'))
model.add(Dropout(0.5))
model.add(Dense(24, activation='softmax'))
          
model.compile(loss='categorical_crossentropy',
              optimizer=Adam(lr=5e-4, beta_1=0.9, beta_2=0.999, epsilon=1e-08, decay=0.),
              metrics=['accuracy'])

4.2 学習
学習する際に2入力の画像を受け付けるように, keara/preprocessing/image.py内のImageDataGeneratorクラス及びDirectoryIteratorクラスの一部を修正して使用した.
ただし, 簡便に実装するため, 以下を前提条件とした.
・2入力の画像のファイル名は同一とする.
・2入力の学習条件等は同一とする.

修正内容:

# <ImageDataGenerator>
#
# 2つのディレクトリを受け付ける
#
    def flow_from_directory(self, directory1, directory2,
                            target_size=(256, 256), color_mode='rgb',
                            classes=None, class_mode='categorical',
                            batch_size=32, shuffle=True, seed=None,
                            save_to_dir=None,
                            save_prefix='',
                            save_format='png',
                            follow_links=False):
        return DirectoryIterator(
            directory1, directory2, self,
            target_size=target_size, color_mode=color_mode,
            classes=classes, class_mode=class_mode,
            data_format=self.data_format,
            batch_size=batch_size, shuffle=shuffle, seed=seed,
            save_to_dir=save_to_dir,
            save_prefix=save_prefix,
            save_format=save_format,
            follow_links=follow_links)

# <DirectoryIterator>
#
# 2つのディレクトリからそれぞれるファイル名をリスト化する.
#
    def __init__(self, directory1, directory2, image_data_generator,
                 target_size=(256, 256), color_mode='rgb',
                 classes=None, class_mode='categorical',
                 batch_size=32, shuffle=True, seed=None,
                 data_format=None,
                 save_to_dir=None, save_prefix='', save_format='png',
                 follow_links=False):
        if data_format is None:
            data_format = K.image_data_format()
        self.directory1 = directory1
        self.directory2 = directory2
        self.image_data_generator = image_data_generator
        self.target_size = tuple(target_size)
        if color_mode not in {'rgb', 'grayscale'}:
            raise ValueError('Invalid color mode:', color_mode,
                             '; expected "rgb" or "grayscale".')
        self.color_mode = color_mode
        self.data_format = data_format
        if self.color_mode == 'rgb':
            if self.data_format == 'channels_last':
                self.image_shape = self.target_size + (3,)
            else:
                self.image_shape = (3,) + self.target_size
        else:
            if self.data_format == 'channels_last':
                self.image_shape = self.target_size + (1,)
            else:
                self.image_shape = (1,) + self.target_size
        self.classes = classes
        if class_mode not in {'categorical', 'binary', 'sparse',
                              'input', None}:
            raise ValueError('Invalid class_mode:', class_mode,
                             '; expected one of "categorical", '
                             '"binary", "sparse", "input"'
                             ' or None.')
        self.class_mode = class_mode
        self.save_to_dir = save_to_dir
        self.save_prefix = save_prefix
        self.save_format = save_format

        white_list_formats = {'png', 'jpg', 'jpeg', 'bmp'}

        # first, count the number of samples and classes
        self.samples = 0

        if not classes:
            classes = [] # set classes from directory1
            for subdir in sorted(os.listdir(directory1)):
                if os.path.isdir(os.path.join(directory1, subdir)):
                    classes.append(subdir)
        self.num_class = len(classes)
        self.class_indices = dict(zip(classes, range(len(classes))))

        def _recursive_list(subpath):
            return sorted(os.walk(subpath, followlinks=follow_links), key=lambda tpl: tpl[0])

        pool = multiprocessing.pool.ThreadPool()
        function_partial = partial(_count_valid_files_in_directory,
                                   white_list_formats=white_list_formats,
                                   follow_links=follow_links)
        # set samples from directory1
        self.samples = sum(pool.map(function_partial,
                                    (os.path.join(directory1, subdir)
                                     for subdir in classes)))

        print('Found %d images belonging to %d classes.' % (self.samples, self.num_class))

        # second, build an index of the images in the different class subfolders
        results1 = []
        results2 = []

        self.filenames1 = []
        self.filenames2 = []
        self.classes = np.zeros((self.samples,), dtype='int32')
        i = 0
        for dirpath in (os.path.join(directory1, subdir) for subdir in classes):
            results1.append(pool.apply_async(_list_valid_filenames_in_directory,
                                            (dirpath, white_list_formats,
                                             self.class_indices, follow_links)))
        for res in results1:
            clses, filenames = res.get()
            self.classes[i:i + len(clses)] = clses
            self.filenames1 += filenames
            i += len(clses)
        for dirpath in (os.path.join(directory2, subdir) for subdir in classes):
            results2.append(pool.apply_async(_list_valid_filenames_in_directory,
                                            (dirpath, white_list_formats,
                                             self.class_indices, follow_links)))
        for res in results2:
            clses, filenames = res.get()
            # self.classes[i:i + len(clses)] = clses
            self.filenames2 += filenames
            # i += len(clses)

        pool.close()
        pool.join()
        super(DirectoryIterator, self).__init__(self.samples, batch_size, shuffle, seed)

#
# 2つのファイル名リストの中からミニバッチ用のファイルを抽出する.
#
    def next(self):
        """For python 2.x.

        # Returns
            The next batch.
        """
        with self.lock:
            index_array, current_index, current_batch_size = next(self.index_generator)
        # The transformation of images is not under thread lock
        # so it can be done in parallel
        batch_x1 = np.zeros((current_batch_size,) + self.image_shape, dtype=K.floatx())
        batch_x2 = np.zeros((current_batch_size,) + self.image_shape, dtype=K.floatx())
        grayscale = self.color_mode == 'grayscale'
        # build batch of image data
        for i, j in enumerate(index_array):
            fname = self.filenames1[j]
            img = load_img(os.path.join(self.directory1, fname),
                           grayscale=grayscale,
                           target_size=self.target_size)
            x = img_to_array(img, data_format=self.data_format)
            x = self.image_data_generator.random_transform(x)
            x = self.image_data_generator.standardize(x)
            batch_x1[i] = x
            fname = self.filenames2[j]
            img = load_img(os.path.join(self.directory2, fname),
                           grayscale=grayscale,
                           target_size=self.target_size)
            x = img_to_array(img, data_format=self.data_format)
            x = self.image_data_generator.random_transform(x)
            x = self.image_data_generator.standardize(x)
            batch_x2[i] = x

        # optionally save augmented images to disk for debugging purposes
        if self.save_to_dir:
            for i in range(current_batch_size):
                img = array_to_img(batch_x1[i], self.data_format, scale=True)
                fname = '{prefix}_{index}_{hash}.{format}'.format(prefix=self.save_prefix,
                                                                  index=current_index + i,
                                                                  hash=np.random.randint(1e4),
                                                                  format=self.save_format)
                img.save(os.path.join(self.save_to_dir, fname))
        # build batch of labels
        if self.class_mode == 'input':
            batch_y = batch_x.copy()
        elif self.class_mode == 'sparse':
            batch_y = self.classes[index_array]
        elif self.class_mode == 'binary':
            batch_y = self.classes[index_array].astype(K.floatx())
        elif self.class_mode == 'categorical':
            batch_y = np.zeros((len(batch_x1), self.num_class), dtype=K.floatx())
            for i, label in enumerate(self.classes[index_array]):
                batch_y[i, label] = 1.
        else:
            return [batch_x1, batch_x2]
        return [batch_x1, batch_x2], batch_y

また, 学習時の呼び出し方は以下の通り.

# 学習データ準備
train_data_generator = im.ImageDataGenerator(
    rescale = 1.0 / 255)

train_data = train_data_generator.flow_from_directory(
    TRAIN_DIR1, TRAIN_DIR2,
    target_size = (IMG_HEIGHT, IMG_WIDTH),
    color_mode = 'rgb',
    classes = classes,
    class_mode = 'categorical',
    batch_size = BATCH_SIZE,
    seed = seed,
    shuffle = True)

4.3 評価
テストデータで, 評価を行った.

テストデータ:9801画像

モデル入力画像学習(epoch)精度
2入力CNNRGB画像+hist画像50.666
(参考)VGG19RGB画像50.655
(参考)VGG19hist画像50.670

RGB画像とヒストグラム画像の2つの画像を入力とすることにより, 少ない層数のモデルで学習済モデル+Fine Tuningとほぼ同じ識別精度を得ることができた.

これをベースにもう少し工夫したいところだが, コンテスト締め切り間近で時間がな~い.
残り数日は, これまでの結果を組み合わせて少しでもコンテストの上位を目指すことに...
(コンテストへの分類結果提出は1日に3回までなので)

今回は, RGB画像とヒストグラム画像の2つの画像を用いた識別方法について紹介した.
次回は, コンテストの結果や表彰式の状況(もし参加できれば)などについて紹介する予定.

----
参照URL:
[1] 人工知能技術戦略会議等主催 第1回AIチャレンジコンテスト
[2] 第1回 FR FRONTIER :ファッション画像における洋服の「色」分類
[3] 最先端のビジネス課題にチャレンジ!
[4] 第1回FR FRONTIER:ファッション画像における洋服の「色」分類にチャレンジ!!(2)






さわってわかる機械学習 Azure Macine Learning 実践ガイド

さわってわかる機械学習 Azure Macine Learning 実践ガイド


クラウドではじめる機械学習 Azure MLでらくらく体験

クラウドではじめる機械学習 Azure MLでらくらく体験