TensorFlowの最近の記事

概要

DeepArtのようなアーティスティックな画像を作れるサービスをご存知でしょうか?
こういったサービスではディープラーニングが使われており、コンテンツ画像とスタイル画像を元に次のような画像の画風変換を行うことができます。この記事では画風変換の基礎となるGatysらの論文「Image Style Transfer Using Convolutional Neural Networks」[1]の解説と実装を行っていきます。

examples_of_style_transfer

引用元: Gatys et al. (2016)[1]


手法

モデルにはCNN(Convolutional Neural Network) が用いられており、VGG[2]という物体認識のための事前学習済みのモデルをベースとしています。


こちらの図はCNNが各層においてどのようにコンテンツ画像とスタイル画像を表現するか示しています。

image_representaion_on_each_layers_in_CNN

引用元: Gatys et al. (2016)[1]


Content Reconstructions (下段) のa, b, c を見ると入力画像がほぼ完璧に復元されていることがわかります。一方で d, e を見ると詳細な情報は失われているものの、物体認識をする際に重要な情報が抽出されていることがわかります。これは画像からコンテンツが抽出され、画風を表す情報が落とされていると考えられます。よって画風変換のモデルでは画像からコンテンツを捉えるためにCNNの深い層を利用します。
次にStyle Reconstructions (上段) を見てみましょう。a からe はそれまでの各層の特徴マップの相関をもとに復元されています。例えば c は第1層から第3層までの各層における特徴マップの相関から生成されています。こうすることで画像内のコンテンツの配置などによらない画風を抽出することができます[3]。


画風変換のモデルではコンテンツ画像とスタイル画像を入力として受け取り、上記のことを利用してスタイル画像の画風をコンテンツ画像に反映した画像を新たに生成します。


では具体的にモデルの中身を見ていきましょう。


モデルのアーキテクチャ

algorithm_of_style_transfer_model

引用元: Gatys et al. (2016)[1]


まずコンテンツ画像 (\(\vec{p}\)) とスタイル画像 (\(\vec{a}\)) から特徴マップが抽出されます。次にこれらの特徴マップと生成画像 (\(\vec{x}\))の特徴マップとの損失が計算されます。この計算で求められる損失をそれぞれコンテンツ損失、スタイル損失と呼ぶことにします。そしてコンテンツ損失とスタイル損失の合計が最終的な損失となり、これを最小化していきます。
ここで注意しなければならないことが1つあります。通常、ディープラーニングでは重みが最適化の対象になりますが、今回は重みは固定して生成画像のピクセルを最適化します。


コンテンツ損失

コンテンツ損失はコンテンツ画像と生成画像のVGGのある1層から出力された特徴マップの平均二乗誤差によって計算されます。

$$ \mathcal{L}_{content} (\vec{p}, \vec{x}, l) = \frac{1}{2} \sum_{i, j}^{} (F_{ij}^{l} - P_{ij}^{l})^2 $$ ここで\(F_{ij}^{l}\) は生成画像の\(l\)層における\(i\) 番目のフィルターの位置\(j\) でのアクティベーションを表しています。\(P_{ij}^{l}\)についても同様ですが、こちらはコンテンツ画像についてのアクティベーションを表しています。


スタイル損失

スタイル画像から画風を捉えるためにまず各層における特徴マップの相関を計算します。

$$ G_{ij}^{l} = \sum_{k}^{} F_{ik}^{l}F_{jk}^{l} $$ これはグラム行列と呼ばれ、\(l\) 層におけるフィルター間の特徴マップの相関をとっています。このグラム行列を用いることで画風を表現することができます[3]。 そして生成画像のグラム行列とスタイル画像のグラム行列の平均二乗誤差を求めます。 $$ E_{l} = \frac{1}{4N_{l}^{2}M_{l}^{2}} \sum_{i,j}^{} (G_{ij}^{l} - A_{ij}^{l})^2 $$ ここで\(N_{l}\)は特徴マップの数、\(M_{l}\)は特徴マップのサイズを表します。また\(G_{ij}^{l}\)、\(A_{ij}^{l}\) はそれぞれ\(l\) 層における生成画像のグラム行列とスタイル画像のグラム行列を表します。  最後に各層の損失の線形和をとってスタイル損失とします。 $$ \mathcal{L}_{style} (\vec{a}, \vec{x}) = \sum_{l=0}^{L} w_{l}E_{l} $$ このとき\(w_{l}\) は\(l\) 層の損失の重みを表します。


最適化

コンテンツ損失とスタイル損失から合計の損失を求めます。合計損失は\(\alpha\) と\(\beta\) をそれぞれコンテンツ損失とスタイル損失の重みとして $$ \mathcal{L}_{total}(\vec{p}, \vec{a}, \vec{x}) = \alpha\mathcal{L}_{content}(\vec{p}, \vec{x}) + \beta\mathcal{L}_{style}(\vec{a}, \vec{x}) $$ この損失を最小化する形で生成画像の最適化を行っていきます。ですので最適化には\(\frac{\partial L_{totla}}{\partial \vec{x}}\) を用いることになります。 論文中ではL-BFGSで最も良い結果になったと記述されていましたが、今回の実装ではAdam を使って最適化を行いました。 この最適化によって、コンテンツ画像をスタイル画像に合わせて画風変換した新たな画像が生成されます。


実装

実装にはTensorFlowを用いました。また実装に際してTensorFlowのチュートリアル[4]を参考にしました。


まずは必要なライブラリをインストールします。

import numpy as np
import matplotlib.pyplot as plt
import matplotlib as mpl
%matplotlib inline
mpl.rcParams['figure.figsize'] = (12,12)
mpl.rcParams['axes.grid'] = False
import time
import IPython.display as display
import PIL.Image

import tensorflow as tf
from keras.preprocessing import image


画像を読み込んで配列に変換する関数、テンソルを画像に変換する関数を定義します。

# 画像を読み込み配列に変換し正規化する
def load_image(input_path, size):
    image = tf.keras.preprocessing.image.load_img(input_path, target_size=size)
    image = tf.keras.preprocessing.image.img_to_array(image)
    image = np.expand_dims(image, axis=0)
    image /= 255
    return image


# テンソルを画像に戻す
def tensor_to_image(tensor):
    tensor = tensor.numpy()
    tensor *= 255
    tensor = np.array(tensor, dtype=np.uint8)
    if np.ndim(tensor)>3:
        assert tensor.shape[0] == 1
        tensor = tensor[0]
    return PIL.Image.fromarray(tensor)


このクラスは配列に変換された画像を受け取り、VGG19の各層からの出力を返します。この時点でスタイルの表現に使われる特徴マップはグラム行列に変換されます。

class StyleContentModel():
    def __init__(self):
        # VGG19のどの層の出力を使うか指定する
        self.style_layers = ['block1_conv1', 'block2_conv1', 'block3_conv1', 'block4_conv1', 'block5_conv1']
        self.content_layers = ['block5_conv2']

        self.num_style_layers = len(self.style_layers)        
        self.vgg = self.get_vgg_model()
        self.vgg.trainable = False

    def __call__(self, inputs):
        preprocessed_input = tf.keras.applications.vgg19.preprocess_input(inputs * 255)
        vgg_outputs = self.vgg(preprocessed_input)

        style_outputs, content_outputs = (vgg_outputs[:self.num_style_layers], vgg_outputs[self.num_style_layers:])
        style_outputs = [self.gram_matrix(style_output) for style_output in style_outputs]

        style_dict = {style_name:value for style_name, value in zip(self.style_layers, style_outputs)}
        content_dict = {content_name:value  for content_name, value in zip(self.content_layers, content_outputs)}

        return {'style':style_dict, 'content':content_dict}

    # Keras API を利用してVGG19を取得する  
    def get_vgg_model(self):
        vgg = tf.keras.applications.vgg19.VGG19(include_top=False, weights='imagenet')
        vgg.trainable = False

        outputs = [vgg.get_layer(name).output for name in (self.style_layers + self.content_layers)]
        model = tf.keras.Model(vgg.input, outputs)

        return model

    # グラム行列を計算する
    def gram_matrix(self, input_tensor):
        result = tf.linalg.einsum('bijc,bijd->bcd', input_tensor, input_tensor)
        input_shape = tf.shape(input_tensor)
        num_locations = tf.cast(input_shape[1] * input_shape[2], tf.float32)
        return result / num_locations


そして合計損失を計算する関数、計算した損失から勾配を計算する関数を定義します。

# 合計損失を計算する
def compute_loss(model, base_image, style_targets, content_targets, style_weight, content_weight):
    model_outputs = model(base_image)
    style_outputs = model_outputs['style']
    content_outputs = model_outputs['content']

    style_loss = tf.add_n([tf.reduce_mean((style_outputs[name]-style_targets[name])**2) for name in style_outputs.keys()])
    style_loss *= style_weight / len(style_outputs)

    content_loss = tf.add_n([tf.reduce_mean((content_outputs[name]-content_targets[name])**2) for name in content_outputs.keys()])
    content_loss *= content_weight / len(content_outputs)

    loss = style_loss + content_loss
    return loss, style_loss, content_loss

# 損失を元に勾配を計算する
@tf.function()
def compute_grads(params):
    with tf.GradientTape() as tape:
        all_loss = compute_loss(**params)

    grads = tape.gradient(all_loss[0], params['base_image'])
    return grads, all_loss


最後に画像の生成を行う関数を定義していきます。
生成画像のベースとなる画像にはノイズ画像を指定しています。ベース画像にコンテンツ画像やスタイル画像を指定するとまた違った結果が得られます。

def run_style_transfer(style_path, content_path, num_iteration, style_weight, content_weight, display_interval):
    size = image.load_img(content_path).size[::-1]
    noise_image = np.random.uniform(-20, 20, (1, size[0], size[1], 3)).astype(np.float32) / 255
    content_image = load_image(content_path, size)
    style_image = load_image(style_path, size)

    model = StyleContentModel()
    style_targets = model(style_image)['style']
    content_targets = model(content_image)['content']

    # 生成画像のベースとしてノイズ画像を使う
    # ベースにはコンテンツ画像またはスタイル画像を用いることもできる
    base_image = tf.Variable(noise_image)

    opt = tf.optimizers.Adam(learning_rate=0.02, beta_1=0.99, epsilon=1e-1)

    params = {
        'model': model,
        'base_image': base_image,
        'style_targets': style_targets,
        'content_targets': content_targets,
        'style_weight': style_weight,
        'content_weight': content_weight
    }

    best_loss = float('inf')
    best_image = None

    start = time.time()
    for i in range(num_iteration):
        grads, all_loss = compute_grads(params)
        loss, style_loss, content_loss = all_loss

        opt.apply_gradients([(grads, base_image)])
        clipped_image = tf.clip_by_value(base_image, clip_value_min=0., clip_value_max=255.0)
        base_image.assign(clipped_image)

        # 損失が減らなくなったら最適化を終了する        
        if loss < best_loss:
            best_loss = loss
            best_image = base_image
        elif loss > best_loss:
            tensor_to_image(base_image).save('output_' + str(i+1) + '.jpg')
            break

        if (i + 1) % display_interval == 0:
            display.clear_output(wait=True)
            display.display(tensor_to_image(base_image))
            tensor_to_image(base_image).save('output_' + str(i+1) + '.jpg')
            print(f'Train step: {i+1}')
            print('Total loss: {:.4e}, Style loss: {:.4e}, Content loss: {:.4e}'.format(loss, style_loss, content_loss))

    print('Total time: {:.4f}s'.format(time.time() - start))
    display.clear_output(wait=True)
    display.display(tensor_to_image(base_image))

    return best_image


では実際に画風変換を行ってみましょう。styleweightとcontentweightはそれぞれスタイル損失とコンテンツ損失の重みを表します。

style_path = '../input/neural-image-transfer/StarryNight.jpg'
content_path = '../input/neural-image-transfer/FlindersStStation.jpg'
num_iteration = 5000
style_weight = 1e-2
content_weight = 1e4
display_interval = 100

best_image = run_style_transfer(style_path, content_path, num_iteration, style_weight, content_weight, display_interval)


結果

コンテンツ画像とスタイル画像はこれらの画像を使いました。


コンテンツ画像

Flinders_Street_Station_in_Melbourne

引用元: https://commons.wikimedia.org/wiki/File:Flinders_Street_Station_3.jpg


スタイル画像

The_Starry_Night_art_of_Gogh

引用元: https://commons.wikimedia.org/wiki/File:Van_Gogh_-_Starry_Night_-_Google_Art_Project.jpg


上記のコードを走らせて生成した画像がこちらになります。 スタイル損失とコンテンツ損失の重みを変えて4種類の画像を生成しました。

  • \(\alpha = 10, \beta = 10^{-2}\) generated_image_with_content_loss_weight_1e1_style_loss_weight_1e-2



  • \(\alpha = 10^{2}, \beta = 10^{-2}\) generated_image_with_content_loss_weight_1e2_style_loss_weight_1e-2



  • \(\alpha = 10^{3}, \beta = 10^{-2}\) generated_image_with_content_loss_weight_1e3_style_loss_weight_1e-2



  • \(\alpha = 10^{4}, \beta = 10^{-2}\) generated_image_with_content_loss_weight_1e4_style_loss_weight_1e-2



結果からわかるように \(\frac{\alpha}{\beta}\) が大きくなればなるほどコンテンツ画像がはっきりと生成画像に反映されていることがわかります。これは\(\frac{\alpha}{\beta}\)が大きいとコンテンツ損失に対してモデルが敏感になるからです。 逆に\(\alpha = 10, \beta = 10^{-2}\) の場合はスタイル損失に敏感になりすぎて、生成画像からコンテンツを見つけることができなくなっています。


まとめ

今回は画風変換の基礎となる論文の解説と実装を行いました。ベース画像やパラメーター、VGGのどの層の出力を使うかなどによって違った結果が得られるので色々といじってみるのも面白いかもしれません。またこの論文以降にも画風変換の研究が進められていますので、それらを試す助けになればと思います。


参考文献

[1] Image Style Transfer Using Convolutional Neural Networks
[2] Very Deep Convolutional Networks For Large-scale Image Recognition
[3] Texture Synthesis Using Convolutional Neural Networks
[4] TensorFlow Core: Neural style transfer


Twitter・Facebookで定期的に情報発信しています!

はじめに

前回の「画像セグメンテーションのためのU-net概要紹介」では画像のクラス分類のタスクを、画像のSegmentationのタスクにどう発展させるかを解説し、SegmentationのネットワークであるU-netの理論ついて簡単に解説しました。
今回はTensorFlowのSegmentationのチュートリアルを行いながら、実際にU-netを学習させてみたいと思います。
尚、本記事ではTensorflowの詳しい解説は行いません。
参考 : https://www.tensorflow.org/tutorials/images/segmentation

Segmentationとは

ある物体が画像内に含まれている時、画像のどこにあるのかを推定するタスクのことです。
言い換えると「画像のピクセルがそれぞれ何かを推定する」タスクのことです。
今回はOxford-IIIT Pet Datasetというデータセットを用いて学習を行い、ピクセルごとに以下のようなクラス分けを行います。

  • Class 0 : 動物のピクセル
  • Class 1 : 動物とその他の境界線のピクセル
  • Class 2 : その他のピクセル


目標は以下のような出力を得ることです。(左:入力画像 右:出力画像)
Alt text


ライブラリのインポート

import tensorflow as tf
import sys
from IPython.display import display
from IPython.display import HTML
from PIL import Image
# sys.modules['Image'] = Image 
from __future__ import absolute_import , division, print_function, unicode_literals
from tensorflow_examples.models.pix2pix import pix2pix

import tensorflow_datasets as tfds
tfds.disable_progress_bar()

from IPython.display import clear_output
import matplotlib.pyplot as plt


データの読み込み&可視化

dataset, info = tfds.load('oxford_iiit_pet:3.0.0', with_info=True)

def normalize(input_image,input_mask):
    input_image = tf.cast(input_image, tf.float32) / 255
    input_mask -= 1
    return input_image,input_mask

@tf.function
def load_image_train(datapoint):
    input_image = tf.image.resize(datapoint['image'],(128,128))
    input_mask = tf.image.resize(datapoint['segmentation_mask'],(128,128))

    if tf.random.uniform(()) > 0.5:
        input_image = tf.image.flip_left_right(input_image)
        input_mask = tf.image.flip_left_right(input_mask)
    input_image,input_mask = normalize(input_image, input_mask)
    return input_image, input_mask

def load_image_test(datapoint):
    input_image = tf.image.resize(datapoint['image'], (128, 128))
    input_mask = tf.image.resize(datapoint['segmentation_mask'], (128, 128))
    input_image, input_mask = normalize(input_image, input_mask)
    return input_image, input_mask

TRAIN_LENGTH = info.splits['train'].num_examples
BATCH_SIZE = 64
BUFFER_SIZE = 1000
STEPS_PER_EPOCH = TRAIN_LENGTH // BATCH_SIZE

train = dataset['train'].map(load_image_train, num_parallel_calls=tf.data.experimental.AUTOTUNE)
test = dataset['test'].map(load_image_test)

train_dataset = train.cache().shuffle(BUFFER_SIZE).batch(BATCH_SIZE).repeat()
train_dataset = train_dataset.prefetch(buffer_size=tf.data.experimental.AUTOTUNE)
test_dataset = test.batch(BATCH_SIZE)

def display(display_list):
    plt.figure(figsize=(15, 15))

    title = ['Input Image', 'True Mask', 'Predicted Mask']

    for i in range(len(display_list)):
        plt.subplot(1, len(display_list), i+1)
        plt.title(title[i])
        plt.imshow(tf.keras.preprocessing.image.array_to_img(display_list[i]))
        plt.axis('off')
    plt.show()

for image, mask in train.take(88):
    sample_image, sample_mask = image, mask
display([sample_image, sample_mask])

Alt text


U-netの構築

U-netのencorder部分には今回のデータセットとは別のデータセットを学習したのMovileNetを用います。
MovileNetの重みは学習で更新しないように更新しておきます。
このようにすることU-netの精度を上げることができます。(転移学習)
decorder部分には未学習のpix2pixを用います。
pix2pixの重みは学習により更新されます。

OUTPUT_CHANNELS = 3

# encorder部分には学習済みのMovileNet
base_model = tf.keras.applications.MobileNetV2(input_shape=[128,128,3],include_top=False)
layer_names = [
    'block_1_expand_relu',
    'block_3_expand_relu',
    'block_6_expand_relu',
    'block_13_expand_relu',
    'block_16_project'
]
layers = [base_model.get_layer(name).output for name in layer_names]
down_stack = tf.keras.Model(inputs=base_model.input, outputs=layers)
# MovileNetの重みは固定
down_stack.trainable = False

# decorder部分にはpix2pixを用いる
up_stack = [
    pix2pix.upsample(512, 3),  # 4x4 -> 8x8
    pix2pix.upsample(256, 3),  # 8x8 -> 16x16
    pix2pix.upsample(128, 3),  # 16x16 -> 32x32
    pix2pix.upsample(64, 3),   # 32x32 -> 64x64
]

def unet_model(output_channels):

    # This is the last layer of the model
    last = tf.keras.layers.Conv2DTranspose(
        output_channels, 3, strides=2,
        padding='same', activation='softmax')  #64x64 -> 128x128

    inputs = tf.keras.layers.Input(shape=[128, 128, 3])
    x = inputs

    # Downsampling through the model
    skips = down_stack(x)
    x = skips[-1]
    skips = reversed(skips[:-1])

    # Upsampling and establishing the skip connections
    for up, skip in zip(up_stack, skips):
        x = up(x)
        concat = tf.keras.layers.Concatenate()
        x = concat([x, skip])

    x = last(x)

    return tf.keras.Model(inputs=inputs, outputs=x)


U-netの学習

実際にU-netが学習していく様子を眺めてみましょう。
epochが進むにつれ、正しく予測できているのがわかります。

class DisplayCallback(tf.keras.callbacks.Callback):
    def on_epoch_end(self, epoch,logs=None):
        clear_output(wait=True)
        ims.append([sample_image, sample_mask,
                 create_mask(model.predict(sample_image[tf.newaxis, ...]))])
        print ('\nSample Prediction after epoch {}\n'.format(epoch+1))

def create_mask(pred_mask):
    pred_mask = tf.argmax(pred_mask, axis=-1)
    pred_mask = pred_mask[..., tf.newaxis]
    return pred_mask[0]

def show_predictions(dataset=None, num=1):
    if dataset:
        for image, mask in dataset.take(num):
            pred_mask = model.predict(image)
            display([image[0], mask[0], create_mask(pred_mask)])
            ims.append([image[0], mask[0], create_mask(pred_mask)])
    else:
        display([sample_image, sample_mask,
                 create_mask(model.predict(sample_image[tf.newaxis, ...]))])
        ims.append([sample_image, sample_mask,
                 create_mask(model.predict(sample_image[tf.newaxis, ...]))])

ims = []
EPOCHS = 100
VAL_SUBSPLITS = 5
VALIDATION_STEPS = info.splits['test'].num_examples//BATCH_SIZE//VAL_SUBSPLITS

model = unet_model(OUTPUT_CHANNELS)
model.compile(optimizer='adam', loss='sparse_categorical_crossentropy',
              metrics=['accuracy'])
model_history = model.fit(train_dataset, epochs=EPOCHS,
                          steps_per_epoch=STEPS_PER_EPOCH,
                          validation_steps=VALIDATION_STEPS,
                          validation_data=test_dataset,
                          callbacks=[DisplayCallback()])

import matplotlib.animation as animation

def make_animation(ims):
    %matplotlib nbagg
    fig, (ax0, ax1, ax2) = plt.subplots(1,3,figsize=(14.0, 8.0))
    ax0.axis('off')
    ax1.axis('off')
    ax2.axis('off')
    ax0.set_title('Input Image')
    ax1.set_title('True Mask')
    ax2.set_title('Predicted Mask')

    ims2 = []
    for epoch,im in enumerate(ims):   

        im0, = [ax0.imshow(tf.keras.preprocessing.image.array_to_img(im[0]))]
        im1, = [ax1.imshow(tf.keras.preprocessing.image.array_to_img(im[1]))]
        im2, = [ax2.imshow(tf.keras.preprocessing.image.array_to_img(im[2]))]
        ims2.append([im0,im1,im2])
    ani = animation.ArtistAnimation(fig, ims2, interval=50, repeat_delay=1000)
    return ani

学習の様子

学習の様子を見てみます。epochが進むごとに精度が増していることがわかります。

Alt text

ani1 = make_animation(ims)
HTML(ani1.to_jshtml())


おまけ

U-netにはskip-conectionという手法が使われています。
encorder部分で畳み込みをして失ってしまった画像内の位置情報を保持する役割を持ちます。
U-netにskip-conectionが無い場合も比較してみましょう。

up_stack = [
    pix2pix.upsample(512, 3),  # 4x4 -> 8x8
    pix2pix.upsample(256, 3),  # 8x8 -> 16x16
    pix2pix.upsample(128, 3),  # 16x16 -> 32x32
    pix2pix.upsample(64, 3),   # 32x32 -> 64x64
]

def unet_model_no_sc(output_channels):

    # This is the last layer of the model
    last = tf.keras.layers.Conv2DTranspose(
        output_channels, 3, strides=2,
        padding='same', activation='softmax')  #64x64 -> 128x128

    inputs = tf.keras.layers.Input(shape=[128, 128, 3])
    x = inputs
    x = down_stack(x)
    x = x[-1]
    for up in up_stack:
        x = up(x)
    x = last(x)

    return tf.keras.Model(inputs=inputs, outputs=x)

ims = []
EPOCHS = 100
VAL_SUBSPLITS = 5
VALIDATION_STEPS = info.splits['test'].num_examples//BATCH_SIZE//VAL_SUBSPLITS

model = unet_model_no_sc(OUTPUT_CHANNELS)
model.compile(optimizer='adam', loss='sparse_categorical_crossentropy',
              metrics=['accuracy'])
model_history = model.fit(train_dataset, epochs=EPOCHS,
                          steps_per_epoch=STEPS_PER_EPOCH,
                          validation_steps=VALIDATION_STEPS,
                          validation_data=test_dataset,
                          callbacks=[DisplayCallback()])


学習の様子

こちらも学習の経過を眺めてみます。
skip-conectionがある場合と比べ、学習が遅いばかりか精度が悪いことがわかります。

Alt text


Twitter・Facebookで定期的に情報発信しています!

概要

ビルド済みのTensorFlowをpipでインストールすると、CPUの命令セットのAVX2やFMAが有効にならない。 AVX2やFMAを用いると、演算速度や精度の向上が期待できる。

参考 MMX, SSE, AVX, FMA命令とは何かを明らかにしたい会

そこで今回は、ソースコードからビルドしてAVX2とFMAを有効にしたTensorFlowを用意し、ビルド済みのパッケージをインストールしたものと、速度や精度の比較を行った。

環境

  • プロセッサ Intel Core i7-5557U 3.1 GHz
  • メモリ 16GB 1867 MHz DDR3
  • macOS Sierra 10.12.6
  • Python 3.6.8
  • TensorFlow 1.13.1

方法

学習は以下のようなCNNで行った。 データセットはMNIST、Fashion MNIST、CIFAR-10を用いた。

import tensorflow as tf

# データセットの選択
dataset = tf.keras.datasets.mnist
# dataset = tf.keras.datasets.fashion_mnist
# dataset = tf.keras.datasets.cifar10

(x_train, y_train), (x_test, y_test) = dataset.load_data()
x_train, x_test = x_train / 255.0, x_test / 255.0

# shapeをモデルの入力に合わせる
if len(x_train.shape) == 3:
    x_train = x_train.reshape(x_train.shape + (1,));
    x_test = x_test.reshape(x_test.shape + (1,));

# CNNモデルの作成
model = tf.keras.models.Sequential([
    tf.keras.layers.Conv2D(32,
        kernel_size=(3, 3),
        activation=tf.nn.relu,
        input_shape=x_train.shape[1:]),
    tf.keras.layers.Conv2D(64, (3, 3), activation=tf.nn.relu),
    tf.keras.layers.MaxPooling2D(pool_size=(2, 2)),
    tf.keras.layers.Dropout(0.25),
    tf.keras.layers.Flatten(),
    tf.keras.layers.Dense(128, activation=tf.nn.relu),
    tf.keras.layers.Dropout(0.5),
    tf.keras.layers.Dense(10, activation=tf.nn.softmax)
])
model.compile(optimizer='adam',
    loss='sparse_categorical_crossentropy',
    metrics=['accuracy'])

# 訓練
model.fit(x_train, y_train, epochs=12)
# 評価
model.evaluate(x_test, y_test)

計測は以下のコマンドで行った。

$ time python cnn.py

結果

MNIST

AVX2, FMA 有 AVX2, FMA 無
学習時間 26m37s 28m32s 6.7%
精度 0.9939 0.9928 +0.0011

Fashion MNIST

AVX2, FMA 有 AVX2, FMA 無
学習時間 25m30s 27m59s 8.9%
精度 0.9218 0.9241 -0.0023

CIFAR-10

AVX2, FMA 有 AVX2, FMA 無
学習時間 32m00s 37m04s 13.7%
精度 0.7049 0.7034 +0.0015

まとめ

AVX2, FMAが有効であると実行時間が短くなった。一方、精度はあまり変わらなかった。

概要

TensorFlowをpipでインストールして実行したところ、以下のようなログが表示され、AVX2FMAが有効でないことがわかった。

 2019-04-09 10:28:05.680156: I tensorflow/core/platform/cpu_feature_guard.cc:141] Your CPU supports instructions that this TensorFlow binary was not compiled to use: AVX2 FMA
 Your CPU supports instructions that this TensorFlow binary was not compiled to use: AVX2 FMA

AVX2FMAは、SIMDの拡張命令セットで、これらを使用することで速度の向上が期待できる。 今回は、TensorFlowでAVX2FMAを有効にするため、ソースコードからビルドをした。

環境

  • macOS Sierra 10.12.6
  • Python 3.6.8
  • Xcode 9.2
  • Bazel 0.21.0
  • TensorFlow 1.13.1

手順

1. Pythonの仮想環境の作成

pyenv, pyenv-virtualenvを使用する。

 $ pyenv virtualenv 3.6.8 TensorFlow-build # python 3.6.8、「TensorFlow-build」という名前の仮想環境を作成する
 $ pyenv local TensorFlow-build # 現在のディレクトリでTensorFlow-buildを使用するよう設定する 

2. TensorFlowの依存パッケージのインストール

 $ pip install -U pip six numpy wheel setuptools mock # 最新版をインストールする
 $ pip install -U keras_applications==1.0.6 --no-deps # keras_applications 1.0.6のみのインストール(依存パッケージはインストールしない)
 $ pip install -U keras_preprocessing==1.0.5 --no-deps # keras_preprocessing 1.0.5のみのインストール 

3. Bazelのインストール

Bazelとは、Googleが開発したビルドツールで、TensorFlowのビルドにも用いられている。

参考 Googleが開発する最新ビルドツール「Bazel」を使ってみよう

今回は、公式の推奨する、バイナリインストーラー方式でインストールする。

3.1 Xcodeをインストールする

Xcodeは、Bazel でobjc_*ios_*をサポートするのに用いられる。 インストールはApp Storeから行う。XcodeはiOS SDK 8.1、本体6.1以降のバージョンが必要となる。

インストール後、下記のコマンドでライセンスに同意する。

 $ sudo xcodebuild -license accept 

3.2 Bazelインストーラーをダウンロードする

bazel-0.21.0-installer-darwin-x86_64.sh を下記URLからダウンロードする。 https://github.com/bazelbuild/bazel/releases

0.21.0よりも新しいバージョンを使用すると、TensorFlowをビルドできない。

3.3 インストーラーを実行する

下記コマンドで実行する。--userフラグをつけると、Bazelが$HOME/binにインストールされ、.bazelrcのパスが $HOME/.bazelrcになる。

 $ chmod +x bazel-0.21.0-installer-darwin-x86_64.sh # 実行権限を付与
 $ ./bazel-0.21.0-installer-darwin-x86_64.sh --user # インストールを実行 

3.4 環境変数を設定する

~/.bash_profileに下記コマンドを追記して、Bazelのバスを通す。

 $ export PATH="$PATH:$HOME/bin" 

3.5 動作を確認する

 $ bazel version # バージョン情報を出力する 

出力結果

 Starting local Bazel server and connecting to it... 
 INFO: Invocation ID: 06b87c44-ed30-4d0d-8211-cb25664ca345
 Build label: 0.21.0
 Build target: bazel-out/darwin-opt/bin/src/main/java/com/google/devtools/build/lib/bazel/BazelServer_deploy.jar
 Build time: Wed Dec 19 12:57:09 2018 (1545224229)
 Build timestamp: 1545224229
 Build timestamp as int: 1545224229 

4. TensorFlowのソースコードのダウンロード

gitでソースコードを取得する。

 $ git clone https://github.com/tensorflow/tensorflow.git # ソースを取得する
 $ cd tensorflow # ソースコードのディレクトリに移動する 

ビルドするTensorFlowのバージョンのブランチをチェックアウトする。 利用可能なバージョンは、リリースページから確認できる。

 $ git checkout branch_name # r1.9, r1.10, etc. 

5. オプションの設定

下記コマンドでオプションを設定する。

 $ ./configure 

今回はすべてデフォルトの設定にした。

前述の通り、Bazelのバージョンが0.21.0よりも新しいと、ここでエラーとなる。

 You have bazel 0.24.1 installed.
 Please downgrade your bazel installation to version 0.21.0 or lower to build TensorFlow! 

6. ビルド

以下のコマンドでビルドする。筆者の環境では1.5時間かかった。

 $ bazel build --config=opt --copt=-mavx2 --copt=-mfma //tensorflow/tools/pip_package:build_pip_package 

ビルドに成功すると、最後に以下のようなログが出力される。

 Target //tensorflow/tools/pip_package:build_pip_package up-to-date:
   bazel-bin/tensorflow/tools/pip_package/build_pip_package
 INFO: Elapsed time: 5396.832s, Critical Path: 348.37s
 INFO: 3499 processes: 3499 local.
 INFO: Build completed successfully, 3500 total actions 

続いてpipパッケージをビルドする。

 $ ./bazel-bin/tensorflow/tools/pip_package/build_pip_package /tmp/tensorflow_pkg # /tmp/tensorflow_pkg に配置する 

7. インストール

以下のコマンドでビルドしたTensorFlowをインストールする。

 $ pip install /tmp/tensorflow_pkg/tensorflow-<version-tags>.whl 

動作確認をする。

 $ cd .. # ソースコードのディレクトリから抜ける
 $ python -c "import tensorflow as tf; tf.enable_eager_execution(); print(tf.reduce_sum(tf.random_normal([1000, 1000])))" # TensorFlowを動かす 

出力結果

 tf.Tensor(3945.0461, shape=(), dtype=float32) 

AVX2FMAに関するログが出力されていないことが確認できる。

なお、TensorFlowのソースコードがあるディレクトリでTensorFlowをインポートすると、以下のようにエラーになるので注意。

 ImportError: Could not import tensorflow. Do not import tensorflow from its source directory; change directory to outside the TensorFlow source tree, and relaunch your Python interpreter from there. 

参考

このアーカイブについて

このページには、過去に書かれた記事のうちTensorFlowカテゴリに属しているものが含まれています。

前のカテゴリはAzure Machine Learningです。

次のカテゴリは統計学です。

最近のコンテンツはインデックスページで見られます。過去に書かれたものはアーカイブのページで見られます。