世間的に人工知能というかディープラーニングがとても注目されていて、僕も色々読み漁ったり実際に動かしてみたりして勉強しています。
ディープラーニングの勉強を始めたのは4月とかだったのでもうその頃にはTensorFlowに関する情報も多く、皆さんが使っているなら、ということでライブラリはTensorFlowを使っています。
TensorFlowに関する情報だとやっぱり画像の分類に関するものがとても多いように感じます。機械学習させるときに今まで人間が行っていた特徴量抽出を、少なくとも画像の分野ではディープラーニングが自ら行えるようになったということ、画像はトピックとしてやってて面白いということが理由なのかなと理解していますが、画像以外も扱いたい!と思うようになりました。
(画像分類もmnist、cifer10、cifer10に独自のデータセットを利用する、さらにモデルを改良する、みたいなことも楽しくてやってたんですが)
ということで、今回は以前の記事でも取り上げたタイタニック号に乗っていた乗客の生存予想を題材に、TensorFlowで乗客が生き残ったか否かを判定する分類器を作ってみました。
タイタニック号の生存予測
タイタニック号は映画でもおなじみ、氷山に追突し沈没した豪華客船です。1500人程いた乗客は性別や階級、その他様々な要因が合わさり、脱出用のボートに乗れて生き残った、もしくは亡くなったという命運が別れました。映画でもそういった血みどろの描写が多くされています。
この題材はKaggleの例題の一つとして取り上げられていて、様々なデータが数値や文字列で与えられ、最終的に生き残ったか否かを予測する二項分類の分類器を作るという設定のものです。具体的なデータは下記の通りです。
PassengerID | 乗客ID |
Survived | 生存結果 |
Pclass | 乗客の階級 1が一番位が高い |
Name | 乗客の名前 |
Sex | 性別 |
Age | 年齢 |
SibSp | 兄弟、配偶者の数 |
Parch | 両親、子供の数 |
Ticket | チケット番号 |
Fare | 乗船料金 |
Cabin | 部屋番号 |
Embarked | 乗船した港 Cherbourg、Queenstown、Southamptonの3種類 |
今回はこの題材で与えられている訓練データ891人分を使って、ディープラーニングでデータを学習し、テストを行う、までを行いました。本来は訓練データでモデルを作り、テストデータをそのモデルに与えて予測結果を作成、そして予測結果をKaggleにアップしてスコアを競う、という形で進めるのですが、予測結果の正答率を出す所まで自分で行いたかったので、Kaggleからは訓練データをもらうだけとしました。
機械学習系の勉強をしていると、扱うためのデータを集めるまでがやはりかなり大変です。そのせいで機械学習やその中のロジックの理解を始めるまでにとても時間がかかるので、今回は良さそうなデータセットを探した結果タイタニックのデータに行き着いたという感じです。
作ったモデル
作ったモデルはTensorFlowのチュートリアルの一つであるDeep MNIST for Experts を比較的簡単に実装している方がいたのでそのコードを参考にしました。活性化関数にReLU関数を持つ隠れ層が2層あり、コスト関数はクロスエントロピー、最適化手法はADAMというものです。
プーリング層とかドロップアウトとか使ってなくあまり画像画像していないので、それ以外のデータにも流用できるかな、と考えてこれを元にしました。
カテゴライズ可能な文字列データも扱う
上記訓練データ項目一覧のうち、Pclass(階級)やAge(年齢)は元々数字で与えられているのでそのまま活用できますが、Sex(性別)、Cabin(部屋番号)、Embarked(乗船した港)などは文字列で与えられています。これらのデータは生存予測のために有効なデータだと考えれれるので、数字に書き換えてモデルに与えます。
df = pd.read_csv('data/titanic_train.csv') labelEncoder = preprocessing.LabelEncoder() df['Sex'] = labelEncoder.fit_transform(df['Sex']) df['Cabin'] = labelEncoder.fit_transform(df['Cabin']) df['Embarked'] = labelEncoder.fit_transform(df['Embarked']) x_np = np.array(df[['Age', 'Pclass', 'Sex', 'SibSp', 'Parch', 'Fare', 'Cabin', 'Embarked']].fillna(0)) d = df[['SurvivedText']].to_dict('record') vectorizer = DictVectorizer(sparse=False) y_np = vectorizer.fit_transform(d) [x_train, x_test] = np.vsplit(x_np, [train_size]) # 入力データを訓練データとテストデータに分ける [y_train, y_test] = np.vsplit(y_np, [train_size]) # ラベルを訓練データをテストデータに分ける
pandasでデータを読み込んで、DataFrameのまま文字列情報を数値情報に変換しています。そしてその後NumPyを使って入力する情報(x_np)とラベル(y_np)を行列で生成しています。
ここで予測する項目をSurvivedTextと指定していますが、これは別途追加した項目です。Sruvivedは0か1で与えられているのですが数値のままだとDictVectrizerで加工するときにTensorのShapeが891*2にならず891*1になってしまい、用意したグラフに合わないということが起きました。なので無理やり一旦文字列に変換して、そこからDictVectorizerを当てています。ここは多分他になんかいい方法があるはずです。
モデルにデータを与える
mnistチュートリアルでは、データは元々加工された状態で与えられています。具体的には入力データ(画像)は28ピクセル*28ピクセル=784次元の数値ベクトルが訓練データで60,000、テストデータで10,000あります。またラベルは10次元のワンホットベクトルがこれも同様に訓練データで60,000、テストデータで10,000あります。
さらにmnistでは丁寧にこのデータを任意の数だけランダムにとってくるという関数が与えられています(mnist.train.next_batch(batch_size))
これを自前のデータを適応するために下記のような感じにしました。
# Loop over step_size for i in range(step_size): # 訓練データから batch_size で指定した数をランダムに取得 ind = np.random.choice(batch_size, batch_size) x_train_batch = x_train[ind] y_train_batch = y_train[ind] # Run optimization op (backprop) and cost op (to get loss value) _, c = sess.run([optimizer, cost], feed_dict={x: x_train_batch, y: y_train_batch})
ただこれだけだと784次元のベクトルを入れて10次元のベクトルを得る、というモデルのままなのでモデルの方を修正する必要があります。今回は8次元のベクトル(活用する変数が’Age’, ‘Pclass’, ‘Sex’, ‘SibSp’, ‘Parch’, ‘Fare’, ‘Cabin’, ‘Embarked’の8つなので)を入力して2次元のベクトル(生き残ったか否か)を得たいので、そのように修正します。次元数を合わせるために必要なのは、最初の隠れ層で入力の次元数と同じ数の重みとバイアスを作ってあげること、そして出力層で得たい値の次元数と同じ数の重みとバイアスを作ってあげることです。通常はこの各層(レイヤー)の構築は inference() という関数に定義されているはずなので、サンプルのコードとかをいじる際にはここを見るとわかるはずです。今回のコードだと multilayer_perceptron() という関数の中で定義しています。
全コード
全コードはこんな感じです。
# -*- coding: utf-8 -*- import random import numpy as np import pandas as pd import tensorflow as tf from sklearn.cross_validation import train_test_split from sklearn.feature_extraction import DictVectorizer from sklearn import preprocessing # Parameters learning_rate = 0.01 # 学習率 高いとcostの収束が早まる training_epochs = 10 # 学習全体をこのエポック数で区切り、区切りごとにcostを表示する batch_size = 100 # 学習1回ごと( sess.run()ごと )に訓練データをいくつ利用するか display_step = 1 # 1なら毎エポックごとにcostを表示 train_size = 800 # 全データの中でいくつ訓練データに回すか step_size = 1000 # 何ステップ学習するか # Network Parameters n_hidden_1 = 64 # 隠れ層1のユニットの数 n_hidden_2 = 64 # 隠れ層2のユニットの数 n_input = 8 # 与える変数の数 n_classes = 2 # 分類するクラスの数 今回は生き残ったか否かなので2 df = pd.read_csv('data/titanic_train.csv') labelEncoder = preprocessing.LabelEncoder() df['Sex'] = labelEncoder.fit_transform(df['Sex']) df['Cabin'] = labelEncoder.fit_transform(df['Cabin']) df['Embarked'] = labelEncoder.fit_transform(df['Embarked']) x_np = np.array(df[['Age', 'Pclass', 'Sex', 'SibSp', 'Parch', 'Fare', 'Cabin', 'Embarked']].fillna(0)) d = df[['SurvivedText']].to_dict('record') vectorizer = DictVectorizer(sparse=False) y_np = vectorizer.fit_transform(d) [x_train, x_test] = np.vsplit(x_np, [train_size]) # 入力データを訓練データとテストデータに分ける [y_train, y_test] = np.vsplit(y_np, [train_size]) # ラベルを訓練データをテストデータに分ける # tf Graph input x = tf.placeholder("float", [None, n_input]) y = tf.placeholder("float", [None, n_classes]) # Create model def multilayer_perceptron(x, weights, biases): # Hidden layer with RELU activation layer_1 = tf.add(tf.matmul(x, weights['h1']), biases['b1']) layer_1 = tf.nn.relu(layer_1) # Hidden layer with RELU activation layer_2 = tf.add(tf.matmul(layer_1, weights['h2']), biases['b2']) layer_2 = tf.nn.relu(layer_2) # Output layer with linear activation out_layer = tf.matmul(layer_2, weights['out']) + biases['out'] return out_layer # Store layers weight & bias weights = { 'h1': tf.Variable(tf.random_normal([n_input, n_hidden_1])), 'h2': tf.Variable(tf.random_normal([n_hidden_1, n_hidden_2])), 'out': tf.Variable(tf.random_normal([n_hidden_2, n_classes])) } biases = { 'b1': tf.Variable(tf.random_normal([n_hidden_1])), 'b2': tf.Variable(tf.random_normal([n_hidden_2])), 'out': tf.Variable(tf.random_normal([n_classes])) } # Construct model pred = multilayer_perceptron(x, weights, biases) # Define loss and optimizer cost = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(pred, y)) optimizer = tf.train.AdamOptimizer(learning_rate=learning_rate).minimize(cost) # Initializing the variables init = tf.initialize_all_variables() # Launch the graph with tf.Session() as sess: sess.run(init) # Training cycle for epoch in range(training_epochs): avg_cost = 0. # Loop over step_size for i in range(step_size): # 訓練データから batch_size で指定した数をランダムに取得 ind = np.random.choice(batch_size, batch_size) x_train_batch = x_train[ind] y_train_batch = y_train[ind] # Run optimization op (backprop) and cost op (to get loss value) _, c = sess.run([optimizer, cost], feed_dict={x: x_train_batch, y: y_train_batch}) # Compute average loss avg_cost += c / step_size # Display logs per epoch step if epoch % display_step == 0: print "Epoch:", '%04d' % (epoch+1), "cost=", \ "{:.9f}".format(avg_cost) print "Optimization Finished!" # Test model correct_prediction = tf.equal(tf.argmax(pred, 1), tf.argmax(y, 1)) # Calculate accuracy accuracy = tf.reduce_mean(tf.cast(correct_prediction, "float")) print "Accuracy:", accuracy.eval({x: x_test, y: y_test})
学習結果
上記モデルを実際に動かしてみて、結果を見てみます
変数を8つ活用する場合
Epoch: 0001 cost= 9.773335212 Epoch: 0002 cost= 1.670696453 Epoch: 0003 cost= 0.040670191 Epoch: 0004 cost= 1.897254459 Epoch: 0005 cost= 0.424092289 Epoch: 0006 cost= 0.036816019 Epoch: 0007 cost= 0.035115667 Epoch: 0008 cost= 1.862647750 Epoch: 0009 cost= 0.034607625 Epoch: 0010 cost= 0.029629484 Optimization Finished! Accuracy: 0.813187
正答率81%という結果になりました。 Epoch: 0002 (学習ステップ200回程)でコストがかなり下がっているので、データ数も少ないことからこの辺りで学習が済んでいるようです。
変数を5つに絞った場合
次に、元々文字列で与えられていた変数(Sex(性別)、Cabin(部屋番号)、Embarked(乗船した港))を削って学習させてみました。
Epoch: 0001 cost= 12.638146183 Epoch: 0002 cost= 5.629146595 Epoch: 0003 cost= 3.115322802 Epoch: 0004 cost= 3.658501392 Epoch: 0005 cost= 2.682972088 Epoch: 0006 cost= 1.243319633 Epoch: 0007 cost= 1.275291908 Epoch: 0008 cost= 1.076138143 Epoch: 0009 cost= 1.081290434 Epoch: 0010 cost= 0.920497378 Optimization Finished! Accuracy: 0.626374
正答率63%と出ました。やはり与えてあげる変数が多いほうが正答率が上がるようです。
正答率を上げるための考察
今回はレイヤー構成、活性化関数、コスト関数、最適化手法はあまり考えずにもともとあったものを使っているので、それぞれの特徴や内部の計算をしっかり理解して応用出来れば、より正答率をあげられるかもしれません。
あとは今回使わなかったName(名前)やTicket(チケット番号)を扱うということも考えれて、例えば名前から敬称を取ってカテゴライズしたり、とか同じ部屋の人はチケット番号が同じらしいので部屋の人数を新しい特徴として与える、というようなアプローチも有効なようです。