BigQueryの何がすごいのか?

既に語り尽くされてそうな話ではありますが、自分なりにまとめないとアタマに入らないのでドキュメントの整理も兼ねて、ポイントをまとめてみます。

BigQueryとは何か?

Google Cloud Platformでは、"BigQuery is Google's fully managed, petabyte scale, low cost analytics data warehouse." と紹介されており、主要なユースケースとして、大規模データに対するアドホックで試行錯誤を要するインタラクティブなクエリが想定されています。

Technical White Paper: An Inside Look at Google BigQueryに書かれているケースがより具体的です。

Can you imagine how Google handles this kind of Big Data during daily operations? Just to give you an idea, consider the following scenarios:

  • What if a director suddenly asks, “Hey, can you give me yesterday’s number of impressions for AdWords display ads – but only in the Tokyo region?”.
  • Or, “Can you quickly draw a graph of AdWords traffic trends for this particular region and for this specific time interval in a day?”

普通のウェブサービスならいざ知らず、Googleの規模でこれを実現するのはなかなか大変です。例えば、Google Searchは世界中のウェブサイトを扱っているわけで。。

で、Googleの内部で開発されたのが、Dremel という技術で、これを外部に公開したのがBigQueryです。

BigQueryの何がすごいのか?

一言で言うなら、1000億行クラスのデータセットに対して、何の事前準備もなくフルスキャンしても数十秒のオーダーで結果が返ってくること。

この事実だけを聞いても最初はピンと来なかったんですが、 Anatomy of a BigQuery Query | Google Cloud Big Data and Machine Learning Blog  |  Google Cloud Platformを読むと次元の違うことをやっていることがよくわかります。

詳しくは記事参照ですが、100 billion rows のテーブルに対して正規表現によるマッチを行い、30秒以内に結果を返すということは少なくとも

  • About 330 100MB/sec dedicated hard-drives to read 1TB of data
  • A 330 Gigabit network to shuffle the 1.25 TB of data
  • 3,300 cores to uncompress 1TB of data and process 100 billion regular expressions at 1 μsec per

というリソースを同時稼働させてるということです。

この規模のリソースを事前準備なしにボタンを押すだけで扱えるとは。。。BigQuery恐るべし。。

BigQueryの中を覗く

じゃあ、一体どうやって実現しているんだろうかという疑問が湧きますが、

Technical White Paper: An Inside Look at Google BigQueryDremel: Interactive Analysis of Web-Scale Datasetsを読むと、BigQueryの内側を垣間見ることができます。White Paperによると、前例のないレベルのパフォーマンスは2つのコア技術により支えられているとのこと。

1. Columnar Storage

f:id:kotaroito2002:20171005083340p:plain 出典: Dremel: Interactive Analysis of Web-Scale Datasets

BigQueryはColumnar Storageを採用しており、アイデアそのものはデータウェアハウス向けのDBではよくある設計。 BigQuery(あるいはDremel)を特徴づけるのは、数千台のサーバーのコンピューティングパワーを活用し、クラウドサービスとして提供していること。

2. Tree Architecture

数千台のマシンでクエリを分散実行し、最終的に秒オーダーで結果を集約することが設計におけるチャレンジで、DremelではTree Architectureを採用しているらしいです。

f:id:kotaroito2002:20171005085043p:plain

出典: BigQuery under the hood | Google Cloud Big Data and Machine Learning Blog  |  Google Cloud Platform

Treeのリーフは「スロット」と呼ばれ、データの読み出しと計算を担当しており、Treeの枝は「ミキサー」と呼ばれ、集約を担当しています。

ネストされたデータ・繰り返しデータを扱える

Loading Data  |  BigQuery  |  Google Cloud PlatformQuerying Nested and Repeated Fields in Legacy SQL  |  BigQuery  |  Google Cloud Platform にて紹介されていますが、BigQueryはネストや繰り返しのあるJSONを扱うことができます。

f:id:kotaroito2002:20171005232751p:plain

まとめというか感想

ドキュメントを読んでまとめただけですが、BigQueryの中が少し分かった気になりました。 ともかく凄いの一言に尽きます。

References

RailsのログをfluentdでBigQuery、あるいはS3に取り込む

GCP BigQueryやAWS Athenaを実際に触る機会が欲しかったので、RailsのログをfluentdでBigQueryやS3に取り込んでみます。 とりあえず触ることが目的で、実用できるかはとりあえず脇においておきます。

なお、fluentd(td-agent)はOS Xに0.14.21 をインストールしています。

Rails fluent logger

Railsのログはデフォルトでは log ディレクトリに出力されるため、これをfluentdで扱えるようにする必要があります。

ドキュメント Collecting and Analyzing Ruby on Rails Logs | Fluentd に従って、logrageact-fluent-logger-rails を使ってみます。

ほぼドキュメント通りですが、以下のように設定しました。

config/application.rb

  class Application < Rails::Application
    config.log_level = :info
    config.logger = ActFluentLoggerRails::Logger.new
    config.lograge.enabled = true
    config.lograge.formatter = Lograge::Formatters::Json.new
    config.lograge.custom_options = lambda do |event|
      exceptions = %w(controller action format id)
      {
        params: event.payload[:params].except(*exceptions)
      }
    end
  end

custom_optionsでリクエストパラメタをログに出力するようにしています。

config/fluent-logger.yml

development:
  fluent_host:   '127.0.0.1'
  fluent_port:   24224
  tag:           'foo'
  messages_type: 'string'

アプリケーションの設定はこれで終わりです。

fluentd + BigQuery

次は、BigQueryにログを出力です。 fluent-plugin-bigquery を使います。

/etc/td-agent/td-agent.conf

<match foo>
  @type parser
  key_name messages
  format json
  tag rails
</match>

<filter rails>
  @type record_transformer
  remove_keys location 
</filter>

<match rails>
  @type copy
  <store>
    @type stdout
  </store>
  <store>
    @type bigquery
    method insert
    auth_method json_key
    json_key /path/to/json_key

    auto_create_table true

    project your-project-id
    dataset rails_playground
    table   logs
    schema [
      {"name": "method", "type": "STRING"},
      {"name": "path", "type": "STRING"},
      {"name": "format", "type": "STRING"},
      {"name": "controller", "type": "STRING"},
      {"name": "action", "type": "STRING"},
      {"name": "status", "type": "INTEGER"},
      {"name": "duration", "type": "FLOAT"},
      {"name": "view", "type": "FLOAT"},
      {"name": "db", "type": "FLOAT"},
      {"name": "params", "type": "STRING"}
    ]
  </store>
</match>

BigQueryのスキーマも設定します。

f:id:kotaroito2002:20170922090630p:plain

実際にクエリを実行してみた結果がこちら。

f:id:kotaroito2002:20170922091321p:plain

fluentd + S3

次はfluentdでS3にログを保存してみます。 まずはtd-agentの設定です。Amazon S3 Output Plugin | Fluentdに従えばOKです。

<match pattern>
  @type s3

  aws_key_id YOUR_AWS_KEY_ID
  aws_sec_key YOUR_AWS_SECRET_KEY
  s3_bucket YOUR_S3_BUCKET_NAME
  s3_region ap-northeast-1


  path kotaroito/rails-playground/dt=%Y-%m-%d/
  s3_object_key_format %{path}%{time_slice}_%{hostname}_%{index}.%{file_extension}

  <buffer tag,time>
    @type file
    path /var/log/td-agent/s3
    timekey 60
    timekey_wait 1m
    timekey_use_utc true # use utc
  </buffer>

  format json
  include_time_key true
</match>

検証を早くしたいので、timekey は60に設定しています。 これでログがS3に保存されていきます。

{"method":"GET","path":"/books","format":"html","controller":"BooksController","action":"index","status":200,"duration":184.47,"view":156.19,"db":1.52,"params":{},"time":"2017-09-27T14:00:11Z"}
{"method":"GET","path":"/books/6","format":"html","controller":"BooksController","action":"show","status":200,"duration":43.6,"view":37.93,"db":0.34,"params":{},"time":"2017-09-27T14:00:21Z"}

Athena

S3に保存しただけじゃ面白くないので、以前から気になっていたAthenaを使ってみることにします。

Amazon Athena (サーバーレスのインタラクティブなクエリサービス) | AWS

Amazon Athena はインタラクティブなクエリサービスで、Amazon S3 内のデータを標準的な SQL を使用して簡単に分析できます。Athena はサーバーレスなので、インフラストラクチャの管理は不要です。実行したクエリに対してのみ料金が発生します。

Athena は簡単に使えます。Amazon S3 にあるデータを指定して、スキーマを定義し、標準的な SQL を使ってデータのクエリを開始するだけです。多くの場合、数秒で結果が出てきます。Athena を使用すると、分析用データを準備するための複雑な ETL ジョブは不要になります。これによって、誰でも SQL のスキルを使って、大型データセットをすばやく、簡単に分析できるようになります。

Athenaには Getting Started — User Guideにてチュートリアルが用意されているので、これを最初にやると雰囲気が掴めます。

やるべきことは、CREATE TABLE をして、

CREATE EXTERNAL TABLE IF NOT EXISTS default.rails_logs (
  `method` string,
  `path` string,
  `format` string,
  `controller` string,
  `action` string,
  `status` int,
  `duration` float,
  `view` float,
  `db` float,
  `params` map<string,string>,
  `time` string 
)
ROW FORMAT SERDE 'org.openx.data.jsonserde.JsonSerDe'
WITH SERDEPROPERTIES (
  'serialization.format' = '1'
) LOCATION 's3://your-bucket-name/path_to_log_dir/'
TBLPROPERTIES ('has_encrypted_data'='false')

あとはクエリを実行するだけです。

SELECT * FROM default."rails_logs" limit 10

f:id:kotaroito2002:20170929085434p:plain

ね、簡単でしょ?

と言いたいところなんですが、色々調べると「S3にログだけ置いておけばあとはAthenaがいい感じやってくれるぜー」という夢のような話はなく、(まともに運用するなら)パーティショニング、ログフォーマット、実行結果の保存等、考えることは多そうです。

qiita.com

日常的に見ることはないログをS3に保存しておき、ad-hocに分析したいという時にはAthenaは1つの選択肢かもなと思いました。 (その他ユースケースでAthenaが最適解になるかはもうちょっと自身が勉強しないと、答が出なそう)

まとめというか、感想

logrageとact-fluent-logger-railsを使うと、RailsのログをかんたんにBigQueryやS3に取り込みできました。 ただし、logrageは例外ログまではサポートしていない(FAQ)ので、例外トラッキングサービス(airbrake.ioなど)別の方法を用意する必要がありそうです。

Athenaはどんなユースケースに使えるかもう少し研究したいところ。

Mac OS Xにtd-agentをインストールする

基本的にはドキュメントに従い、dmgからインストールすればOKです。

docs.fluentd.org

いくつか自分で調べたことがあったので、FAQ的にメモに残しておきます。

起動/終了はどうやればいい?

sudo launchctl start td-agent
sudo launchctl stop td-agent

プラグインはどうやってインストールすればよい?

sudo /opt/td-agent/embedded/bin/fluent-gem install fluent-plugin-parser

でよい。

DMARCについて調べた

必要に迫られていろいろ調べたので、メモしておく。

DMARCって何?

DMARC, which stands for “Domain-based Message Authentication, Reporting & Conformance”, is an email authentication, policy, and reporting protocol. It builds on the widely deployed SPF and DKIM protocols, adding linkage to the author (“From:”) domain name, published policies for recipient handling of authentication failures, and reporting from receivers to senders, to improve and monitor protection of the domain from fraudulent email.

https://dmarc.org/

ざっくり言うと、SPFDKIMによる認証が失敗したメールの取扱いポリシーや受信側から送信側へのレポートを規定したプロトコルです。

SendGridのBlogがとても分かりやすい。

sendgrid.kke.co.jp

sendgrid.kke.co.jp

具体的に何をすればいいの?

SPFDKIMの設定が済んでいれば、TXTレコードをDNSに登録するだけです。 設定値の意味するところは、GSuiteのヘルプが分かりやすいです。

support.google.com

正しく設定できたか検証したい!

例えば、

MX Lookup Tool - Check your DNS MX Records online - MxToolbox

を使うとよいです。

EmbulkとBigQueryと

遅ればせながら触ってみたので、自分用にメモを残しておきたいと思います。

Embulk

よく使うコマンド

embulk guess  seed.yml -o config.yml
embulk preview config.yml
embulk run     config.yml

bundle環境を利用する場合は -b オプションをつける。

embulk guess -b embulk_bundle seed.yml -o config.yml
embulk preview  -b embulk_bundle  config.yml
embulk run  -b embulk_bundle  config.yml

プラグイン

Embulkはpluginアーキテクチャを採用しており、gem をインストールする形になる。

embulk gem install embulk-plugin-foo_bar

Gemfileを使って、bundle 環境をつくることもできる。

embulk mkbundle embulk_bundle
cd embulk_bundle; vim Gemfile

embulk bundle # update gem

BigQuery

サインアップ

初めてGoogle Cloud Platform(GCP)とBigQueryを使う時(というかクラウドコンピューティング全般に言えますが…)は、料金的な意味でドキドキしますが、2017年9月現在 $300のクレジットがあるので安心です。

よくある質問  |  無料試用  |  Google Cloud Platform

Modify a project's billing settings - Cloud Platform Console Help

クイックスタート

アカウントのサインアップが終わったら、クイックスタートをやると雰囲気掴める。

Quickstart Using the Web UI  |  BigQuery  |  Google Cloud Platform

Embulk + BigQuery

クイックスタートだけでは自分でやった感がないので、EmbulkでMySQLに存在するデータをBigQueryにロードしてみます。 データは http://dev.mysql.com/doc/index-other.html の “world database” を使います。

Gemfile
source 'https://rubygems.org/'
gem 'embulk', '~> 0.8.0'

# Input
gem 'embulk-input-mysql'

# Output
gem 'embulk-output-bigquery'
config.yml
in: 
  type: mysql
  host: localhost
  user: root
  password: ''
  database: world
  table: city
  select: '*'
  order_by: ID

out: 
  type: bigquery
  mode: append
  auth_method: json_key
  json_keyfile: /path/to/json_keyfile.json
  auto_create_table: true
  project: your-project-id-1234
  dataset: world
  table: city 
  compression: GZIP
  source_format: NEWLINE_DELIMITED_JSON
  default_timezone: "Asia/Tokyo"

gemをインストールし、config.ymlを用意したら、あとは run するだけです。

embulk run -b embulk_bundle config.yml

実際の結果はこちら。

f:id:kotaroito2002:20170913084900p:plain

感想

小一時間あればできちゃうので、便利な時代になりました。 BigQueryであれば、Googleの他のプロダクト(例: Google DataStudio)と連携できるので、選択肢の1つとして常にアタマに入れておきたい。

「ゼロから作るDeep Learning」のその先へ... TensorFlowを使いこなす

「ゼロから作るDeep Learing」を読了しました。

深層学習の概観は掴めたので、次は自分で手を動かしてみたくなります。 ライブラリは色々ありますが今回はTensorFlowで「ゼロから作るDeep Learning」の次の一歩を踏み出してみたいと思います。

TensorFlow

TensorFlow is an open source software library for numerical computation using data flow graphs. Nodes in the graph represent mathematical operations, while the graph edges represent the multidimensional data arrays (tensors) that flow between them.

https://www.tensorflow.org

TensorFlowは深層学習に特化したものではなく、データフローグラフを使った数値計算のためのライブラリです。

GoogleのSatoさんのDistributed TensorFlowの話 によると、Google Brainチームで培った成果をオープンソースソフトウェアとして一般公開したもの、とのこと。

詳細までは追っていないのですが、Distributed TensorFlowに拠ると、クラスタを組んでTensorFlowを実行することもサポートされているようです。

Tensorflow Tutorial

ざっと概観を掴んだので、次はチュートリアルを見てみます。

チュートリアルは「Build a Softmax Regression Model」と「Build a Multilayer Convolutional Network」の2つのパートに分かれています。

(ちょっと図が雑ですが)Build a Softmax Regression Modelのイメージ図です。

f:id:kotaroito2002:20170316154146p:plain

入力Xと重みWを掛けたものに、バイアスbを足すという単純なモデルです。

書かれているコードを繋ぎ合わせるとこんな感じになります。 なお、tensorflowは0.12.1 を用いています。

import tensorflow as tf
from tensorflow.examples.tutorials.mnist import input_data
mnist = input_data.read_data_sets('MNIST_data', one_hot=True)

x = tf.placeholder(tf.float32, shape=[None, 784])
y_ = tf.placeholder(tf.float32, shape=[None, 10])

W = tf.Variable(tf.zeros([784,10]))
b = tf.Variable(tf.zeros([10]))
y = tf.matmul(x,W) + b

cross_entropy = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(y, y_))
train_step = tf.train.GradientDescentOptimizer(0.5).minimize(cross_entropy)

with tf.Session() as sess:
    sess.run(tf.global_variables_initializer())
        
    for i in range(1000):
      batch = mnist.train.next_batch(100)
      train_step.run(feed_dict={x: batch[0], y_: batch[1]})
        
        
    correct_prediction = tf.equal(tf.argmax(y,1), tf.argmax(y_,1))
    accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
    print(accuracy.eval(feed_dict={x: mnist.test.images, y_: mnist.test.labels}))

accuracyは0.9151でした。

なお、mnist.train.next_batch(100) から得られるデータはMNISTの画像をnumpy.ndarrayに変換したものです。

batch = mnist.train.next_batch(100)
print(type(batch)) # <class 'tuple'>
print(type(batch[0])) # <class 'numpy.ndarray'>
print(batch[0].shape) # (100, 784)
print(batch[1].shape) # (100, 10)

コードと出力を見ると、batch[0]は28x28=784のピクセルデータを格納した行列であることがわかります。行数はbatch sizeの100になっています。

「Build a Multilayer Convolutional Network」はコードをそのままコピーすれば動くし、単なる日本語訳に終始しそうなので割愛します。

Kaggleにチャレンジ

チュートリアルを終えたので、次はデータサイエンティストのためのコンペティションサイトであるKaggleにチャレンジです。

手頃そうな課題としてLeaf Classificationを見つけました。99種類ある葉を分類する問題です。画像は下記のようにグレースケールになっており、訓練データは計990(分類クラスごとに10サンプルずつ)あります。

f:id:kotaroito2002:20170316154050p:plain

Kaggleは画像から抽出した特徴量を用意していますが、それらは使わず元画像データからConvolutional Neural Networkで分類にチャレンジしてみます。

前処理

元画像はサイズがどれもバラバラなので、まずはサイズを合わせるところから始めます。

from PIL import Image
from skimage.transform import rescale, resize, rotate
from skimage.color import gray2rgb, rgb2gray

def load_image(path):        
    image_2d = np.array(Image.open(path))
    image_3d = gray2rgb(image_2d)
    
    return image_3d # np.array
    
def fit_image(image):
    fit_size = 138
    
    # rescale image
    max_size = np.maximum(image.shape[0], image.shape[1])
    scale = fit_size / max_size
    image_3d = rescale(image, scale, mode='reflect') 
    
    # fit
    margin = np.array((fit_size, fit_size)) - image_3d.shape[0:2]
    margin = np.round(margin / 2).astype(int)

    pos_x = (margin[0], margin[0] + image_3d.shape[0])
    pos_y = (margin[1], margin[1] + image_3d.shape[1])
    
    image_norm = np.zeros((fit_size, fit_size, 3))    
    image_norm[pos_x[0]:pos_x[1], pos_y[0]:pos_y[1], :] = image_3d
    
    return image_norm.astype(np.int32)

image = load_image('path/to/image')
image = fit_image(image)

ここでは幅と高さのうち長いほうがfit_size=138になるようrescaleし、正方形になるよう余白をゼロで埋めています。また、余白が上下(あるいは左右)均等になるよう、位置調整もしています。ライブラリはscikit-imageを用いました。

次に学習の出力となるラベルの処理です。Leaf Classificationの訓練データラベルは文字列なので、TensorFlowで扱いやすいよう数値に変換しておきます。scikit-learnのLabelEncoderを使えばよさそうです。

df_train = pd.read_csv('train.csv')

labels = df_train['species'].values

le = preprocessing.LabelEncoder()
le.fit(labels)
le.transform(labels)
array([ 3, 49, 65, 94, 84, 40, 54, 78, 53, 89, 98, 16, 74, 50, 58, 31, 43,
        4, 75, 44, 83, 84, 13, 66, 15,  6, 73, 22, 73, 31, 36, 27, 94, 88,
       12, 28, 21, 25, 20, 60, 84, 65, 69, ....])

TFRecords file

画像とラベルデータの前処理が終わったので、次はこれらをTensorFlowに読み込ませます。メモリに全て展開というわけにはいかないので、TensorFlowではTFRecordsというファイル形式が用意されています。

with tf.python_io.TFRecordWriter(path) as writer:
    image_raw = image.tostring()

    example = tf.train.Example(features=tf.train.Features(feature={
            'label': tf.train.Feature(int64_list=tf.train.Int64List(value=[label])),
            'image_raw': tf.train.Feature(bytes_list=tf.train.BytesList(value=[image_raw]))}))

    writer.write(example.SerializeToString())

これでTensorFlowで読み込めるようになりました。コードの完全版はgistにあります。

学習

TFRecordsを読み込み、ミニバッチを作成するところからスタートです。

inputs

IMAGE_SIZE = 138
INPUT_SIZE = 128

def inputs(files, distortion=True, batch_params={'size': 10, 'min_after_dequeue': 20}):
    fqueue = tf.train.string_input_producer(files, shuffle=True)
    reader = tf.TFRecordReader()
    key, value = reader.read(fqueue)

    features = tf.parse_single_example(value, features={
        'label': tf.FixedLenFeature([], tf.int64),
        'image_raw': tf.FixedLenFeature([], tf.string),
    })

    label = tf.cast(features['label'], tf.int32)
    image = tf.decode_raw(features['image_raw'], tf.int32)

    image = tf.reshape(image, [IMAGE_SIZE, IMAGE_SIZE, 3])
    image.set_shape([IMAGE_SIZE, IMAGE_SIZE, 3])
    image = tf.cast(image, tf.float32)
    
    if distortion:
        cropsize = random.randint(INPUT_SIZE, INPUT_SIZE + (IMAGE_SIZE - INPUT_SIZE) / 2)
        framesize = INPUT_SIZE + (cropsize - INPUT_SIZE) * 2
        image = tf.image.resize_image_with_crop_or_pad(image, framesize, framesize)
        image = tf.random_crop(image, [cropsize, cropsize, 3])
        image = tf.image.random_flip_left_right(image)
        image = tf.image.random_flip_up_down(image)
        
    one_hot_label = tf.one_hot(label, depth=99, dtype=tf.float32)

    capacity = batch_params['min_after_dequeue'] + 3 * batch_params['size']
    
    images, labels = tf.train.shuffle_batch(
        [image, one_hot_label],
        batch_size= batch_params['size'],
        capacity=capacity,
        min_after_dequeue=batch_params['min_after_dequeue']
    )
    
    images = tf.image.resize_images(images, [INPUT_SIZE, INPUT_SIZE])
    return images, labels

まず、filesとしてTFRecordsのpathのリストを与え、TFRecordファイルを読み込むキューを作成します。

次に訓練データに対してランダムに反転・切り取り処理を行い、バリエーションを増やします。また、tf.one_hotを利用して、ラベルのone-hot encodingを行います。

最後に、tf.train.shuffle_batchでミニバッチをつくります。

capacityは文字通りキューに格納できる最大要素数で、min_after_dequeue はバッチ作成後にキューに残っている最小要素数で、要素のミックスレベルを決めるものです。capacityはmin_after_dequeueとbatch_sizeから決めています。(この数式は確かどこかを参照したはず… )

inference

モデルはTensorFlowチュートリアルDeep MNIST for Expertsをベースにしています。

def conv2d(x, W):
  return tf.nn.conv2d(x, W, strides=[1, 1, 1, 1], padding='SAME')

def max_pool_2x2(x):
  return tf.nn.max_pool(x, ksize=[1, 2, 2, 1],
                        strides=[1, 2, 2, 1], padding='SAME')

def inference(images, keep_prob):
    x = tf.image.rgb_to_grayscale(images)
    x_image = tf.reshape(x, [-1,128, 128,1])

    with tf.variable_scope("conv1") as scope:
        stddev = 2.0 / math.sqrt(5 * 5 * 1)
        W_conv1 = tf.get_variable('weights', [5, 5, 1, 32], initializer=tf.random_normal_initializer(stddev=stddev))
        b_conv1 = tf.get_variable("biases", [32], initializer=tf.constant_initializer(0.0))
        
        h_conv1 = tf.nn.relu(conv2d(x_image, W_conv1) + b_conv1)
        h_pool1 = max_pool_2x2(h_conv1)

    with tf.variable_scope("conv2") as scope:
        stddev = 2.0 / math.sqrt(5 * 5 * 32)
        W_conv2 = tf.get_variable('weights', [5, 5, 32, 64], initializer=tf.random_normal_initializer(stddev=stddev))
        b_conv2  = tf.get_variable("biases", [64], initializer=tf.constant_initializer(0.0))
        
        h_conv2 = tf.nn.relu(conv2d(h_pool1, W_conv2) + b_conv2)
        h_pool2 = max_pool_2x2(h_conv2)

    with tf.variable_scope("fc1") as scope:
        stddev = 2.0 / math.sqrt(32 * 32 * 64)
        W_fc1 = tf.get_variable('weights', [32 * 32 * 64, 1024], initializer=tf.random_normal_initializer(stddev=stddev))
        b_fc1 = tf.get_variable("biases", [1024], initializer=tf.constant_initializer(0.0))

        h_pool2_flat = tf.reshape(h_pool2, [-1, 32*32*64])
        h_fc1 = tf.nn.relu(tf.matmul(h_pool2_flat, W_fc1) + b_fc1)

        h_fc1_drop = tf.nn.dropout(h_fc1, keep_prob)

    with tf.variable_scope("fc2") as scope:
        stddev = 1.0 / math.sqrt(1024)
        W_fc2 = tf.get_variable('weights', [1024, 99], initializer=tf.random_normal_initializer(stddev=0.1))
        b_fc2 = tf.get_variable("biases", [99], initializer=tf.constant_initializer(0.0))

    logits = tf.matmul(h_fc1_drop, W_fc2) + b_fc2

    return logits

チュートリアルでは重みの初期値が0.1でしたが、今回はHeの初期値を利用しています。

また、tf.Variableではなく、tf.get_variableを利用しています。tf.Variableのままだと、inferenceメソッドを呼び出す度に新しい変数が割り当てられてしまうためです。最初はここに気づかずだいぶハマりました。

なお、変数名は下記のように確認することができます。

variables = tf.trainable_variables()
for v in variables:
    print(v.name)

loss

全結合の第一層目にdropoutを入れていますが、overfitting が疑われたのでL2正則化も入れています。

def loss(logits, labels, l2=True):
    loss = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(labels=labels, logits=logits))
    
    if l2:
        variables = tf.trainable_variables()
        l2_loss = tf.add_n([ tf.nn.l2_loss(v) for v in variables if 'bias' not in v.name ]) * 0.001
        loss = loss + l2_loss
    
    return loss

Learning

あとは学習するだけです。コードが汚いのはご容赦を。

keep_prob = tf.placeholder(tf.float32)

images, labels = inputs(train_files,  batch_params={'size': 64,  'min_after_dequeue': 1000})
validation_images, validation_labels = inputs(validation_files, batch_params={'size': 100, 'min_after_dequeue': 1000})

with tf.variable_scope('inference') as scope:
    logits  = inference(images, keep_prob)

    scope.reuse_variables()
    validation_logits = inference(validation_images, keep_prob)

    cross_entropy = loss(logits, labels)
    train_step = tf.train.AdamOptimizer(1e-4).minimize(cross_entropy)
    correct_prediction = tf.equal(tf.argmax(logits,1), tf.argmax(labels,1))
    accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))

    validation_cross_entropy = loss(validation_logits, validation_labels)
    validation_correct_prediction = tf.equal(tf.argmax(validation_logits,1), tf.argmax(validation_labels,1))
    validation_accuracy = tf.reduce_mean(tf.cast(validation_correct_prediction, tf.float32))

    test_images, test_ids = test_inputs(test_files)
    test_logits = inference(test_images, keep_prob)
    test_prediction = tf.nn.softmax(test_logits)

変数を再利用できるよう scope.reuse_variables() を実行しています。詳しくはPROGRAMMER’S GUIDEのSharing Variablesを読むと良いです。

sess = tf.Session()
sess.run(tf.global_variables_initializer())


coord = tf.train.Coordinator()
threads = tf.train.start_queue_runners(sess=sess, coord=coord)

for i in range(2000):
    _, loss_value = sess.run([train_step, cross_entropy], feed_dict={keep_prob: 0.5})

    if i%100 == 0:
        train_acc, validation_acc = sess.run([accuracy, validation_accuracy], feed_dict={keep_prob: 1.0})
        print("step %d, loss value %g, training accuracy %g, validation accuracy %g"%(i, loss_value, train_acc, validation_acc))
        
        
coord.request_stop()
coord.join(threads)

sess.close()

コード全文はgistに上げています。

あとはひたすら学習を進めればよいだけのはず。 が、なかなか収束しない。。。 データを減らせば(過学習にはなるが)収束はするので、バグではないと思うのですが。。。

まとめ

TensorFlowのチュートリアルをベースにConvolutional Neural Networkをやってみました。 TFRecord、ミニバッチ作成、変数再利用などこれまで知らなかったことを学べました。 結果が出てないのは少々残念ですが。

References

Pandas Cheatsheet

数ヶ月ぶりにpandasを使うと基本すら忘れていることが多いので、チートシートを残しておきます。 なお、pandas (0.18.1) です。

データの読み込み

csvから読み込む。 先頭行のヘッダは自動で読み込んでくれます。

import pandas as pd

df_train = pd.read_csv('train.csv', index_col='id')

numpyからDataFrameに変換する。

import numpy as np
import pandas as pd

pd.DataFrame(np.random.rand(3,2), index=[10, 11, 12], columns= ['x', 'y'])

Index(行)とColumns(列)

indexとcolumnsを設定する。

df.index = [1, 2, 3]
df.columns = ['a', 'b']

columnでsliceする。

new_df = df['b']

新しいcolumnを追加する。

df['c'] = np.array(3)

columnを削除する。

df.drop('c', axis = 1)

データの書き出し

csv

df.to_csv('output.csv')

numpy

df.values