Dropoutのハイパーパラメータの決定について
ここ1ヶ月ほど、会社のアルバイトで6クラスの画像分類モデルをTensorflowを使って作ることを任された
色々詰まった、考えさせられたことがあったのでメモ
画像分類だったらCNNで、ネットワークはVGG16だったりAlexNetで作りましたべろ〜んwって感じでいいと思うが、そのモデルをProtocolBufferファイルにしてラズパイなどに乗せて動かすということなのでネットワークの層が深すぎるとProtocolBufferファイルが400~700MBになって重すぎて敵わん...となってしまうので6層ほどで適当に作ることにした
全然6層じゃなかった
畳み込みのkernel_sizeは3~5で、Dropoutは0.5で調整した
いざ学習させたらなぜか学習が進まなかった。損失は減っているが精度が0.23~0.25間で向上しなかった。
別の2層の畳み込み層と2層の全結合層のモデルは学習が進んでいたことから、自分のモデルのパラメータの設定がおかしいのだろうかと悩んだ
畳み込み層、学習係数のハイパーパラメータは同じだったので、全結合層のパラメータやLocal response Normalization
層が原因なのかと考えたりした
試行錯誤した結果、上記のせいで学習が滞っていたのではなく、Dropoutのパラメータが原因だったということがわかった
上のネットワーク図からわかるように2つの隠れ層である全結合層にはDropoutを適用している。どちらもkeep_prob
は0.5に設定した
このkeep_prob
をどちらも0.8に変更した結果、学習が進んだ
Dropoutについては青いイルカさんの本とかでは仕組みや嬉しいことの説明はあったが最適なパラメータの設定値の説明はなかった。なのでいつも0.5で設定していた
調べたところ最初に来る全結合層に対してDropoutを適用するとき、そのパラメータを0.5にするのはあまり良くないそうだ
というわけで学習が進まない問題は回避できてなんとかなった
その後、全結合層のノード数とDropoutのkeep_probパラメータの間に何か関係があるのか気になったので調べることにした
使うデータはTensorflowのチュートリアルで有名なMNISTで全結合層のみのネットワークを構築した
mport tensorflow as tf import numpy as np from tensorflow.examples.tutorials.mnist import input_data mnist = input_data.read_data_sets('MNIST_data', one_hot=True) t = tf.placeholder(tf.float32, shape=(None,10)) X = tf.placeholder(tf.float32, shape=(None,28*28),name="input") keep_prob_1 = tf.placeholder(tf.float32) # ドロップアウトする割合 keep_prob_2 = tf.placeholder(tf.float32) # ドロップアウトする割合 stddev = np.sqrt(2.0 / 28*28) h_W1 = tf.Variable(tf.truncated_normal([28*28,256], stddev=stddev)) h_b1 = tf.Variable(tf.constant(0.1, shape=[256])) h_fc1 = tf.nn.relu(tf.matmul(X,h_W1) + h_b1) h_fc1_drop = tf.nn.dropout(h_fc1, keep_prob_1) stddev = np.sqrt(2.0 / 256) h_W_fc2 = tf.Variable(tf.truncated_normal([256,512], stddev=stddev)) h_b_fc2 = tf.Variable(tf.constant(0.1, shape=[512])) h_fc2 = tf.nn.relu(tf.matmul(h_fc1_drop, h_W_fc2) + h_b_fc2) h_fc2_drop = tf.nn.dropout(h_fc2, keep_prob_2) stddev = np.sqrt(2.0 / 512) W_fc3 = tf.Variable(tf.truncated_normal([512,10], stddev=stddev)) b_fc3 = tf.Variable(tf.constant(0.1, shape=[10])) fc3 = tf.matmul(h_fc2_drop, W_fc3) + b_fc3 p = tf.nn.softmax(fc3,name="output") loss = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits=fc3, labels=t)) optimizer = tf.train.AdamOptimizer(1e-4) train_step = optimizer.minimize(loss) correct_prediction = tf.equal(tf.argmax(fc3,1), tf.argmax(t,1)) accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32)) saver = tf.train.Saver() ### 学習の実行 sess = tf.Session() sess.run(tf.global_variables_initializer()) i = 0 for i in range(0,10000): batch = mnist.train.next_batch(30) sess.run(train_step, feed_dict={X:batch[0],t:batch[1],keep_prob_1:0.5,keep_prob_2:0.5}) if i % 1000 == 0: train_acc, train_loss = sess.run([accuracy,loss], feed_dict={X:batch[0],t:batch[1],keep_prob_1:1.0,keep_prob_2:1.0}) print("[Train] step: %d, loss: %f, acc: %f" % (i, train_loss, train_acc)) # テストデータによるモデルの評価 test_acc, test_loss = sess.run([accuracy,loss], feed_dict={X:mnist.test.images,t:mnist.test.labels,keep_prob_1:1.0,keep_prob_2:1.0}) print("[Test] loss: %f, acc: %f" % (test_loss, test_acc)) saver.save(sess, "./ckpt/mnist") sess.close()
h_fc1
とh_fc2
にdropoutを適用したりしなかったり、keep_prob_1
とkeep_prob_2
の値を変えて訓練結果を確認した
まずh_fc1
とh_fc2
のノード数が、48,96でdropoutを適用しなかった時の結果(多いので2000step毎だけ)
[Train] step: 0, loss: 15.007373, acc: 0.066667 [Test] loss: 12.547914, acc: 0.097100 [Train] step: 2000, loss: 0.960308, acc: 0.666667 [Test] loss: 0.894258, acc: 0.735600 [Train] step: 4000, loss: 0.381689, acc: 0.800000 [Test] loss: 0.603150, acc: 0.818700 [Train] step: 6000, loss: 0.460051, acc: 0.866667 [Test] loss: 0.484288, acc: 0.855700 [Train] step: 8000, loss: 0.595815, acc: 0.866667 [Test] loss: 0.419885, acc: 0.873900
精度はいまいち...次にノード数はそのままでdropoutを適用し、keep_prob_1
、keep_prob_2
が共に0.5に設定したとき
[Train] step: 0, loss: 18.346718, acc: 0.033333 [Test] loss: 12.756997, acc: 0.122000 [Train] step: 2000, loss: 1.504397, acc: 0.500000 [Test] loss: 1.692765, acc: 0.451800 [Train] step: 4000, loss: 1.380499, acc: 0.566667 [Test] loss: 1.422444, acc: 0.552400 [Train] step: 6000, loss: 1.460326, acc: 0.600000 [Test] loss: 1.400168, acc: 0.609900 [Train] step: 8000, loss: 1.548799, acc: 0.533333 [Test] loss: 1.289341, acc: 0.667200
適用なしの時より悪化しているのがわかる
色々試した結果、keep_prob_1
は1.0(dropoutを適用しない)keep_prob_2
を0.8に設定した結果がdropout適用しないときより良かった
mnist-dropout-48-96-10-08-ckpt [Train] step: 0, loss: 17.872959, acc: 0.066667 [Test] loss: 16.914766, acc: 0.103000 [Train] step: 2000, loss: 1.499189, acc: 0.666667 [Test] loss: 1.236634, acc: 0.663800 [Train] step: 4000, loss: 0.986408, acc: 0.666667 [Test] loss: 0.778916, acc: 0.762500 [Train] step: 6000, loss: 0.436568, acc: 0.866667 [Test] loss: 0.605089, acc: 0.813300 [Train] step: 8000, loss: 0.290827, acc: 0.933333 [Test] loss: 0.504056, acc: 0.846000
次に上記の結果はノード数が少なすぎるからkeep_prob_1
、keep_prob_2
が共に0.5の結果は悪かったのでは?と疑問に思い、各ノード数を256,512にした時でも同様に結果を観察することにした
まずはdropout適用しないときの結果
[Train] step: 0, loss: 9.332975, acc: 0.166667 [Test] loss: 10.855021, acc: 0.127700 [Train] step: 2000, loss: 0.380751, acc: 0.966667 [Test] loss: 0.349467, acc: 0.905500 [Train] step: 4000, loss: 0.222563, acc: 0.966667 [Test] loss: 0.241931, acc: 0.931800 [Train] step: 6000, loss: 0.108663, acc: 0.966667 [Test] loss: 0.199128, acc: 0.943000 [Train] step: 8000, loss: 0.077383, acc: 0.966667 [Test] loss: 0.172284, acc: 0.948200
元々の表現力が上がったせいか48,96のときより精度が良くなっている
次にkeep_prob_1
、keep_prob_2
が共に0.5のときの結果
[Train] step: 0, loss: 7.612897, acc: 0.200000 [Test] loss: 9.190090, acc: 0.129300 [Train] step: 2000, loss: 0.934076, acc: 0.733333 [Test] loss: 0.541602, acc: 0.850500 [Train] step: 4000, loss: 0.303545, acc: 0.966667 [Test] loss: 0.453306, acc: 0.869100 [Train] step: 6000, loss: 0.434249, acc: 0.900000 [Test] loss: 0.529966, acc: 0.884700 [Train] step: 8000, loss: 0.707538, acc: 0.866667 [Test] loss: 0.521837, acc: 0.899100
精度自体はdropoutなしの方より悪い気するけど48,96でkeep_prob_1
、keep_prob_2
が共に0.5のときの結果と比べて、学習が停滞しているようには感じない
やはりノード数が多いからだろうか?
最後にkeep_prob_1
は1.0(dropoutを適用しない)keep_prob_2
を0.8に設定した結果
[Train] step: 0, loss: 15.822262, acc: 0.000000 [Test] loss: 15.912350, acc: 0.069200 [Train] step: 2000, loss: 0.096453, acc: 0.966667 [Test] loss: 0.338137, acc: 0.914300 [Train] step: 4000, loss: 0.138627, acc: 0.966667 [Test] loss: 0.221914, acc: 0.936700 [Train] step: 6000, loss: 0.005314, acc: 1.000000 [Test] loss: 0.170819, acc: 0.948000 [Train] step: 8000, loss: 0.288768, acc: 0.866667 [Test] loss: 0.142067, acc: 0.958000
dropoutなしよりとても良くなった
Dropoutのパラメータの設定でここまで結果が変わるとは思っていなかったので調べてて面白かった
ベイズの定理とソフトマックス関数について
授業が始まって中々勉強に時間が取れないがちょっとだけPRML上巻を読んだ。
ベイズの定理について自分なりの解釈としてのメモをかくことに
中学で確率における同時確率(または結合確率)というのを習った。これは事象XとYがあるとして、XとYが同時に起こる確率を示したもの。
数式で表すとこうなる
重要なのはこの等式は事象XとYが独立のとき成立するということ。
XとYが独立であるとは、片方の事象が起こる可能性がわかっていても、もう片方の可能性に何も影響しないということを意味している。
独立の場合は上の等式のようにそれぞれの事象の確率の積で同時確率を計算できる。
中学の問題で言えば裏表のコインやサイコロを振るとかがあった。
実際の状況、問題においては独立でない場合の事象を考えることが多いからこの等式では同時確率を求めるのは難しい。 困った。
そこで、事象の依存性を考慮した同時確率を考える。これは以下のようになる。
確率の乗法定理と呼ばれる。この式が意味することは「事象XとYが同時に起こる確率」は「Xが起こる確率」と「Xが起きた時にYが起こる確率」の積で求められるということ。
右辺にあるP(Y|X)
は「Xが起きた時にYが起こる確率」からYの事前確率だったりYの条件付き確率だったり呼ばれる。らしい。。
またこの乗法定理において事象XとYが独立とわかっているなら、P(Y|X)
はP(Y)
と書き換えることができる。
最初この乗法定理を見た時、Yの事前確率にP(X)
というXが起こる確率で重み付けしたものが、同時確率P(X,Y)
になると考えてしまった。
機械学習の勉強をしているとどっかしらの項を重みと見てしまう悪い発作だ。。。
同時確率P(X,Y)
はP(Y,X)
と書き換えることもできる。そしてこんな感じになる。
つまり以下の関係が得られる。
2枚目の方は有名なベイズの定理。何度も見て何度も忘れた。
式だけを見れば、「Yの事後確率」は「Yの起こる確率」と「Xの事後確率」を掛け合わせたものを「Xの起こる確率」で割ったもので求めることができる、という意味になる。
正直こんな感じだ。でも嬉しいことはおそらくP(Y|X)
を求めるには他の3つの確率を調べれば良いということなんだろう。
こういう時は例で考える。事象Xを年間の行事と考える。ハロウィンだったりプレミアムフライデーだったり色々だ。そして事象Yを美味しい料理と考える。これもケーキだったり厚焼き玉子だったり寿司だったり色々ある。
ある人がクリスマスでうきうきしていて、ケンタッキーのフライドチキンを食べたいとする。その人は「そもそもクリスマスのとき、ケンタッキーのフライドチキンがでる確率っていくらぐらいなんだ・・・?」と疑問に思ったので調べたいとする。
つまりP(Y=ケンタッキー|X=クリスマス)
を求めたい。
求めるにはクリスマスに出てくる全ての料理を挙げて、そのうちケンタッキーがどれくらいの割合を占めるかを考えないと求めることはできない。
ベイズの定理から、他の3つの確率で求めてみる。
P(Y)
はケンタッキーが出てくる確率。年間の売り上げ数から求められるはず。P(X|Y)
はケンタッキーが出てきたとき、その日の行事がクリスマスである確率となる。これもケンタッキーの年間売り上げからクリスマスの日がどれだけの割合を占めているかで求められそうだ。P(X)
はクリスマスが起こる確率。クリスマスは年に一度のため悩む必要はない。
P(Y=ケンタッキー|X=クリスマス)
で求めるか、他の3つの確率で求めるか、どちらのが簡単だろうか。
前者はクリスマスに出てくる全ての料理を挙げ切るのはとても厳しい気がする。後者も大変かもしれないがケンタッキーの年間売り上げあたりを調べればいいだけだ。
P(Y|X)
の測定が難しいなら、他の容易な3つの確率から求めることができる。すごいぞベイズの定理。
もう一つ疑問に思ったことがある。ベイズの定理の右辺にある分母のP(X)
は何をしているのだろうか。
純粋に式だけを見ればP(Y)P(X|Y)
を割っているだけだが、なぜ割ればP(Y|X)
が求まるのだろう。。そういう定理だからで片付くが今いちしっくりこない。
右辺の分母について考える。P(X)
は「Xが起こる確率」だが「事象Yの値に関係なく事象Xが起こる確率」とも言える。これは式で表すと
Nが表すのは上の例(事象Yを美味しい料理)で言うならば y1 = ケーキ、 y2 = 玉子焼き、... といった事象Yが取りうる数。
P(X=クリスマス,Y=ケーキ) + P(X=クリスマス,Y=卵焼き) + ... P(X=クリスマス,Y=yj)
まで事象Xがクリスマスの時に起こりうる同時確率の総和がP(X)
だ。
P(X)
をこの数列で表した時のベイズの定理はこうなる
分子は事象X=クリスマス、事象Y=ケンタッキーの同時確率なのでこう書き換えられる
こうみるとどうだろう。分子は分母の数列の中にある一部の同時確率である。
これは(全体の一部、あるいは比べる量) ÷ (元、全体の量)
といった割合として見ることができるじゃないか。
百分率ではないので右辺の値は必ず [0,1] 範囲に収まる。確率としての値が出てくる。
分母のP(X)
は右辺が [0,1] 範囲に収まるようにある意味での正規化としての役割を担ってくれていたのだ・・・
そしてこの時何かに似ていると気づいた。ニューラルネットワークの活性化関数の一つであるソフトマックス関数のことである。
分類問題における出力が [o1,o2,...,on]
となっているとき、ソフトマックス関数を適用すると各出力の総和が 1 になるように o1,o2,..,on
を変換してくれる。
ベイズの定理にある分母のP(X)
と同じように正規化をしてくれる。
青いイルカの深層学習本ではベイズの定理の説明がなかったので自分がベイズの定理についてあやふやだったソフトマックス関数のとこは「ほーん」って感じに読んでいたがベイズの定理がベースになってるじゃーんと今さらだが興奮した。
KaggleのTitanicで上位10%に入った手法のまとめ
初心者向けですが深層学習の講師を最近やりました。
講義の中で実際にKaggleのコンペで腕試しをするということをしたかったので講義をやる前にチャレンジした。
今回腕試しをするコンペはkaggleのチュートリアルで有名なtitanicを選んだ。
Titanic: Machine Learning from Disaster | Kaggle
どういった内容かというと、タイタニック号の乗客のデータからある乗客は生存したか否かを判定し、その正解率を競う感じ。
試行錯誤して自分の最高スコアは 0.81339 になった。上位10%だしそこそこ頑張れたはず。
もう少し上を目指したいとこだが授業も始まったので一旦打ち切り
コードはここ
データ分析にはpandas
を用いた。最初はdescribe()
やdf.isnull().sum()
とかで統計や欠損値の確認をする。
訓練データのAge、Cabinなどに欠損値があるのがわかる。
次に仮説をいくつか立ててその条件でデータを操作し、その時の生存者の割合を確認していった。仮説は主に
子供や赤ん坊など年齢が若い人の方が優先して救助されるのではないか
女性のが優先して救助されるのではないか
Pclass1(だいたいお金持ちの乗客)の人たちは優先して救助されるのではないか
などなど
年齢はやっぱりというか子供のが生存率が高いのは確認できた
生存率に関しては性別の違いが一番別れていた。
後は名前の属性を何かしら使いたい(文字列の操作を練習したかった)ので名前からMr
やMiss
などを抽出してそれらの生存率を確認したりした。
生存の判別モデル構築の問題として年齢を特徴として扱いたいけど欠損している乗客がいる。
この問題に対して訓練データの年齢における欠損値を除いた年齢の中央値で穴埋めか年齢自体を予測するモデルを作り、そのモデルの予測結果で穴埋めするといった2つの手法をとった。
良い精度が出たのは後者の方だった。年齢予測モデルはXGBoostで構築した。
最終的に使う特徴はこれらに絞った
one_hot
系は文字列カテゴリを数値に変換したもの
SibParch
はSibSp
とParch
を合わせたもの
Fare_rounddown_split
はFare
の小数点を打ち切り、10ドルごとにカテゴリ分けしたもの
By_Age_class
はAge
を10歳ごとにカテゴリ分けしたもの
生存者の判別モデルとしてTensorflowでロジスティック回帰モデルとXGBoostモデルの2つを構築し、比較した。
Tensorflowのロジスティック回帰モデルは以下のように構築した
# 入力層 X = tf.placeholder(tf.float32, shape=[None, 7], name="input") t = tf.placeholder(tf.float32, shape=[None, 1]) # パラメータ1 stddev = np.sqrt(2.0 / 7) W1 = tf.Variable(tf.truncated_normal([7,24], stddev=stddev)) b1 = tf.Variable(tf.constant(0.1, shape=[24])) # パラメータ2 stddev = np.sqrt(2.0 / 24) W2 = tf.Variable(tf.truncated_normal([24,48], stddev=stddev)) b2 = tf.Variable(tf.constant(0.1, shape=[48])) keep_prob = tf.placeholder(tf.float32) # ドロップアウトする割合 # パラメータ3 stddev = np.sqrt(2.0 / 48) W3 = tf.Variable(tf.truncated_normal([48,1], stddev=stddev)) b3 = tf.Variable(tf.constant(0.1, shape=[1])) layer1 = tf.nn.relu(tf.matmul(X,W1) + b1) layer2 = tf.nn.relu(tf.matmul(layer1,W2) + b2) layer2_drop = tf.nn.dropout(layer2, keep_prob) layer3 = tf.matmul(layer2_drop,W3)+b3 p = tf.nn.sigmoid(layer3,name="output") # 荷重減衰 norm_term = tf.nn.l2_loss(layer1) + tf.nn.l2_loss(layer2_drop) # 正則項 lambda_ = 0.001 # 損失関数 loss = tf.reduce_mean(tf.square(p - t))+ lambda_*norm_term # 学習アルゴリズム optimizer = tf.train.AdamOptimizer() train_step = optimizer.minimize(loss) # 精度 correct_prediction = tf.equal(tf.sign(p-0.5), tf.sign(t-0.5)) accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
XGboostとlog_lossで誤差の値を比べたがXGboostの勝利だった
NNと決定木における表現力の問題だろうか・・・
ハイパーパラメータをいじって提出していたら 0.81339 という結果が出た。
終わった後に他の人と話してタイタニックは実際にあった話、つまり背景があるから背景から得られる情報を使えばよかったとか元のタイタニック号の船構造を見つけて救命艇がどこにあるかや部屋番号の割り当て(Cabin)を確認するのもアリだったよねーとか灯台下暗しって感じに知見を得た。
他にもstackingや交差検証など学習方法の見直しも大事
物体検出の実装を目指す-Fast R-CNNについて
結構間が空いたが、Fast R-CNNの論文を読んだ。適度にメモする。
SPPnetではSelectiveSearchから得た候補領域一つ一つをCNNにかけるというのを無くし、対象とする画像データ一枚からCNN(畳み込みの処理のみ)にかけて得た特徴マップから物体検出を行う手法をとることで、大幅な時間短縮に成功した。大体RCNNから24~102倍(テスト時)速くなった。
Fast-RCNNはSPPnetより10倍(テスト時)速くなっている。まだ速くなるのか。
物体検出は画像分類と比べて複雑な2つの主要な課題がある。(というよりは発生する)
- SelectiveSearchなどで候補領域を処理しなければいけないこと
- これらの候補領域は正確な位置特定を達成するための大まかな位置情報しか提供しないこと。つまり洗練しなければいけない。
Fast-RCNNではこの2つの問題を考慮し、物体の候補領域から分類を行うこと、空間的位置を改善することを共同で学習する単一段階のアルゴリズムが導入されている。
やっと分類と位置特定の2つが一度に学習できる段階に来た。これは胸が熱い。。。
SPPnetも十分すごいが、かなり大きい欠点がある。それはSPP層つまり空間ピラミッドプーリング処理より前の畳み込み層を更新することができないということ。この欠点によってディープネットワークの精度に限界が発生してしまう。
Fast-RCNNではこの問題も解決しており、全てのネットワークの層を更新できる。
学習の手順としては以下のようになる。
SPPnetと同様に画像全体と物体の候補位置を受け取り、画像全体から畳み込み層と、Maxプーリング層にて一枚のConv特徴マップを生成する。
次に、その特徴マップから受け取った各候補位置に対応する領域を抜き出し(RoI Projectionのあたり)、RoI(region of interest)プーリングと呼ばれる処理を行い固定長の特徴ベクトルを出力する。
出力した固定長の特徴ベクトルは2つのfc層に分岐する。
一つ目はクラス分類を行う出力のsoftmaxに
二つ目はより洗練されたbounding-boxの位置を符号化するbbox-regressorへ
Fast-RCNNにて新たにRoIプーリングが出てきた。これは簡単にいうとSPP層におけるピラミッドレベルが一つしかないプーリング処理。どっちも任意の領域内で決まった長さの特徴ベクトルを出力することに変わりはない。
そして分類とbounding-box回帰の訓練において2つを合わせたマルチタスクの損失関数を定義することでこの2つを共同に訓練することができる。
またこのマルチタスクの損失関数の定義によって全てのネットワークの層を更新することが可能となった。すごいぞお
次はいよいよFaster-RCNN
参考文献及び画像(Fast-RCNNの論文先)
SECCON Beginners2017 東京に行ってきた感想
滅多に人が来ないこのブログのアクセス数が1000超えてた。あったけえ・・・
ベリベリサンクスヾ(≧∀≦)ノ
タイトル通りctf4bの東京会場行ってきた。去年の2月くらいにCicada3301という話をネットで読んですごいワクワクして謎解きみたいで面白そうと思い、その後で似たような感じのCTFというものを知った。
機械学習の方の勉強を優先していたのでCTFの勉強に時間は取れてなくて初心者向けの講義受けて見たいと申し込みしたら当選してびびった。
内容としては最初にセキュリティに関しての倫理の話から始まってWeb、フォレンジック、リバースエンジニアリングの講義があった。
Web
検証ツールを使ったソースコードから手掛かりを探す方法や脆弱性の種類についての説明があった。
Request/Response Headerにも手掛かりがあるとは知らなかった。
あとはSQLインジェクションだったり、Directory Traversal、Local File Inclusionなどについても説明があった。
Djangoのデバッグモードからソースコードを入手してflagを取得する解法もあったりしたり疑うことはたくさんあるな。。
典型的な脆弱性は覚えて置いて損はなさそう
フォレンジック
フォレンジックはpcapのログからflagを探したりファイルを復元してflagを入手したりする。CTFにおいてはフォレンジックの問題が解いてて一番楽しい。
pcapのログを見るのに必要なWiresharkの機能の使い方やデータの抽出方法の解説があった。バイナリについては与えられたファイルの拡張子が何であるか確認するfileコマンドや解析に便利なstrings,binwalkコマンドなどの説明があった。icatコマンドは知らなかったのでありがてえ
CSAWCTF2017のフォレンジックの問題でpcapの通信ログに謎のURLパラメータが大量にあるものがあった。その時は解けなかったがWriteupでは最初のパラメータにBMPの拡張子であるマジックナンバーが記述されていたらしく、パラメータを連結してBMPファイルを構築してflagを得るというものだった。
マジックナンバーは暗記しよう( ;∀;)
リバースエンジニアリング
実行可能なバイナリファイルの解析をする。アセンブリ言語を読めないと話にならないので、汎用、特殊レジスタの説明や代入や算術命令の説明があった。
callやjmp命令においてのスタックの構造把握が難しい。
講義が終わったあとはオリエンテーションというか1時間半くらいの問題を解く競争があった。結果は100人弱の中で27位だった。
時間が経つにつれて脳が停止してしまい、まともな思考ができなかった。終わった後すぐに別の200点問題の解き方がわかって悔しかった。。。
Writeupの解説を聞いて、自動で計算を処理して結果をPOSTするスクリプトを書く能力もいるのとバイナリ問題については処理の流れをしっかり掴むように読むことが大事なんだと思った。
Web問から行かずに、時間が経ってから正解者が多いWeb問を把握してそれから解いていく戦法使っている人がいて賢い。。。
まとめ
どの講義も全くの初心者の自分でもわかりやすい講義で最高だった。
リバースエンジニアリングについては他より覚えることが多い気がするので大変なのに1時間ちょっとで覚えるべきことだけ教えるといったスライドを作るのはめっちゃすごいと思った。
かっこいい原価割れしたステッカーも貰えるし、プロの人に懇親会で話を聞けるのでおすすめです。
まだ機械学習のするべき勉強は沢山あるけど、CTFの問題を解くのはすごく楽しいのでこっちもぼちぼちやってきたい。
隣の席で話しかけてくれた美人の女性さんと運営の皆さん、ありがとうございました。
日付データから得られる特徴量の観察
インターンの懇親会に参加したとき、「解決したい問題によっては日付のデータが重要になることもある」という助言がプロの人からぽろっと出ていた。
インターンの時には日付の扱いがよくわからず使わずに終わってしまった。
さりげなくどんな感じに変換して扱うのがよいか聞いておいた。日付の月や時間だけを抽出して扱うだけでも特徴量として使えるとのことだった。
扱うのが上手い人は日付から曜日に変換して曜日を特徴量として追加する人もいるらしい。さらには気象庁から天気のデータを持ってくる人もいるだとか。
日付の扱い方を練習したいと思い、またKaggleからいい感じのデータセットを漁って分類モデルを作ることにした。
コードはここ
今回使ったのはこのデータセット
Uberというアプリを利用した顧客さんのデータのようだ。利用した日、終了日、ドライブの距離などがあるっぽい。
分類に使うモデルではなくデータを解析して新たな発見を得るという感じのデータセットな気がするが、このデータセットの属性からドライブの利用目的を判定するモデルを作ることにする。利用目的はビジネス、パーソナルとカテゴリで分類されていた。
まずはどんなデータが格納されているか5行ほど見ることにする。
ここで、利用目的として平日ならビジネス、休日ならパーソナルが多いんじゃないか?、乗った日の時刻によって利用目的は変わるのではないか、月によって利用目的に変動がありそうだと3つほど仮説を立てて新しい属性として開始日から月と時刻をげちっとする。
def time_hour_split(x): t = str(x['START_DATE*']) t = t.split() ret = t[1].split(':') return ret[0] def date_month_split(x): t = str(x['START_DATE*']) t = t.split() ret = t[0].split('-') return ret[1] # START_DATE* から日付の時刻と月を分けて新たな属性として追加する df['START_HOUR_TIME'] = df.apply(lambda x: time_hour_split(x), axis=1) df['MONTH'] = df.apply(lambda x: date_month_split(x), axis=1)
計算量を考えない雑なコードだとこれでできた。
折角なので曜日も取得することにする。
df['START_DATE*'] = pd.to_datetime(df['START_DATE*']) # 日付から曜日を取得 df['weekday'] = df['START_DATE*'].dt.dayofweek
対象とする属性をtp_datatime
で型変換を行い、dayofweek
というのを使うことで曜日を得られるそうだ。便利!!!
新たな属性を追加したデータセットは以下のような感じになった。
曜日は 0 = 月曜日 であってるはず。
判別に必要なカテゴリの分布はこんな感じだった。
0がBusinessで、1がPersonal。偏ってるというレベルではない。
各属性からエントロピーを計算する。
親のエントロピーが0.3533593
なため、0.07とかはかなり大きい気がしなくもない。
日付から取り出した月がかなり貢献できそうなことがわかる。
weekday
は利用しないことにする。
これらの属性を特徴量としてモデルを構築していく。
キノコと同じようにTensorflowを使ったロジスティック回帰とXGBoostで比べっこする。
Log_lossによる評価の結果
決定木の勝利だった。
かなり正と負の偏りがあったのでダウンサンプリングを行ってみた。コードはここ
二乗誤差による評価の結果
Log_lossではXgboostの方でエラーが出てしまった。
ロジスティック回帰の方の二乗誤差は0.0476177
だったので、ダウンサンプリングを行なった結果の方が悪いとわかる。
これは現実においてもBusinessで利用する人のが多く、データセットのカテゴリの事前確率と現実の事前確率がほぼ同じという考えでいいのかな?
もし半分の確率でBusinessかPersonalが発生するならダウンサンプリングで偏りを無くした方がいい時もあるのかも。
ROC曲線や累積反応曲線について
前回の記事で、モデルの性能評価として期待利益という値を使った。
データサイエンスの基本コンセプトのひとつに「モデルの性能の比較対象となる適切な基準は何であるかを考察することは重要である」というものがあるらしい。ビジネスとしては予想精度の最大化よりもモデルが利益を向上させているかがモデルの性能の改善として見るからかな?
今回はモデルの性能を可視化する方法をいくつか試したのでメモにする。コードはここ
まずは利益曲線。これはクラスの事前分布とコストと利益の推定が両方明らかな場合に利用するといいらしい。
前回の損益行列を使うと曲線は以下のようになった。
横軸はデータの数(キノコの数ともいえる)。食用と判断したキノコのうち、スコア(安全度)が高いと判断したものほど左に、スコアが低いものほど右にある。
精度をわざと悪くしたため、少し下がっている部分がある。これは偽陰性(食用だと思ったらほんとは毒キノコ)と判断したものがあるからだな。
お客さんにキノコを売るとしては食用と判断した内の半分ほどは大丈夫、黒字ということで判断していいのだろうか。
現実ではモデルを作成する前にクラスの事前分布、利益・コストがわからない場合もある。そんな不確実性を許容できる取り得る性能の全体を示す方法がある。
これはROC(受信者動作特性)曲線と呼ばれるそうだ。(x,y)軸の組として(偽陽性,真陽性)または(偽陰性,真陰性)いずれかの組を取る。
つまり利益(tp,tn)とコスト(fp,fn)の間に生まれるトレードオフを示す。グラフはこんな感じになった。
青い点が今回作ったモデルを示す。また組としては(偽陰性,真陰性)をとった。
ROC空間内においてはいくつか大事な座標がある。(0,0)は陰性の分類を全く行わないことを表し、(1,0)では無条件に陰性として分類を行う分類器ということを示す。
そして(0,1)が全て真陰性に分類する完璧な分類器ということになるんだな。
赤の対角線は半分の確率で陰性と分類するランダムな分類器を表す。そのためこの対角線より上にあるほどランダムではない、データ内の情報を活用できている分類器であることを意味して、逆にこの対角線より下にあるほど何でも陰性と判断する「ゆるい」分類器、ランダム分類器より悪いともいえる。
今回は座標の位置からしてランダムよりそこそこ良いといえるモデルができたかな。
データの数を一定の数で増やし、その時のモデルの性能としてROCグラフに点でプロットして線でつなげると曲線ができる。この曲線の下の面積を全体の面積で割った割合をAUCと呼び、モデルの性能を1つの数値で評価したい時や分類器の使用条件以外は何もわからないときに便利だそうだ。
社会人ではないのでわからないが、現実だとデータ分析がよくわからない営業の人にモデルの性能を説明しなくてはいけないときがあるそうで。そんなとき利益曲線やROC曲線を見せても、よくわからんヾ(`Д´)ノ" といった感じでコミュニケーションが取れないこともあるとか。
そのため人に説明しやすい一般的な可視化の手段として累積反応曲線がある。これはROC曲線と密接に関連しているがこっちの方が直感的でわかりやすいらしい。
累積反応曲線は縦軸(y軸)にヒット率(tpまたはtn率)正しく分類されたデータの割合をとる。横軸(x軸)には対象としている母集団の割合をとる。
ここだとスコアが0.5未満のものは陽性(毒キノコ)としているため、途中でグラフの変化がなくなってしまった。
それでも食用と判断している x = 50 あたりまでのとこだと、青い対角線(ランダム分類器)よりも上にあることから何らかの長所があると見てもいいはず。
ここでモデル性能を表す曲線が、ランダム推定より優れているということを表すリフト値なるものがある。
10個のデータのうち5個が食用、残りの5個が毒キノコとする。このときランダム分類器で5個選んだら、陰性である食用キノコが半分あると判断してもいい。
このときリフト値は (食用キノコの割合) / (データの割合) より 0.5 / 0.5 = 1 となる。
次に自分が作ったモデルでスコア(安全度)が高い上位5個のキノコを選ぶ。このとき食用キノコがほぼ占めていると判断してもいい。
このときリフト値は 1.0 / 0.5 = 2 となる。
このリフト値をプロットしたものがリフト曲線となる。各モデルの(ランダムより)相対的な優位性を示す。
実際には累積反応曲線上にある x座標の値を同じ対角線でのx座標の値で割ったもの。
累積反応曲線とリフト曲線は対象とするデータセットのうち陰性、または陽性の割合が明確でないと使えない。
本通りのいい感じな曲線がうまいこと引けなかったかも。プヨグラミング力が足りない。