機械学習よりのインターンに行ってきた感想と特徴量について

タイトル通り機械学習よりのインターンに行ってきた。理由は3年生だからそろそろインターンの体験を積みたいというのが一番でかかったりする。

どこのインターンに行ったのかは言えません。東京湾に沈められますからね。
オフィスは綺麗でリラックマのひよこのぬいぐるみがふかふかしてたのが印象に残ってます。おやつが出て、ま○泉というお店の小さいバーガーが個人的にめっちゃくちゃ美味しくて感動した。東京はすごい。

インターン内容

データセットを渡されて何かを出力するモデルを構築して精度を競争しました。
何のデータセット、何を出力するモデルなのかは言えません。NDAもありますが、東京湾にコンクリートで詰められますからね。

モデルの構築、試行錯誤について

最初はデータの観察方法の講義があったので、それを参考にして各データが何を表すのか、散布図を表示して相関はあるのかなどを観察した。
散布図見ても理屈ではなく直感でこれとこれは相関ありそう!!!と判断したりしてた。後はモデルを構築するにおいてデータセットにおけるどの特徴量が一番出力に貢献しているかの確認もしたりした。
あまり識別には関係ないデータを特徴として渡しても次元が増えてモデルが無駄に複雑になるだけな可能性もありうる。いらないと思っていたデータが実は他のデータと組み合わせると精度が向上した、なんてケースもありえそうだし難しそうなとこだ思う。

そんなこんなで使う特徴を選択しつつモデルの構築に取り掛かった。最初はTensorflowを使ってロジスティック回帰のモデルを作った。
結果としては精度は悪かった。多層にしたり、単層にしたり、重み減衰、L2正則化などを追加したりしたり試行錯誤した。

精度が上がらなくてしょんぼりして帰り道を歩いていた時、S○nS○nのCMが店のモニターで流れていた。有名な白いTシャツの人たちがスタイリッシュな動きをするやつ。
実はそこの会社の勉強会の生放送を視聴していて、その時にXGBoostStackingという単語が出たのをCMを見たその時思い出した
ニューラルネットのモデルで精度が出ないなら、線形SVMだったりランダムフォレストとやらを使えばいいじゃないかと閃いたので早速XGBoostを使ってモデルを構築した。

そしたらめっちゃ順位が上がった。XGBoostすげえ!!!!!

次に悩むのは過適合についてだった。高い順位に行くためには汎化能力の高さも大事だと思ったからである。

ブログ記事にもしたが自分はSPPnetの論文を読んだ。そこで

「XGBoost,SVM,ロジスティック回帰など複数のモデルからそれぞれ予測値を出して、その平均をとったものを出力として渡したらより精度が向上するかもしれないし、一つのモデルが過適合起こしても平均をとるからいい感じに過適合な予測値を避けることができるんじゃないか!?」

とSPPでのプーリング処理もどきをやってみようと思いついた。

そこでXGBoostElasticnet,MLP,単層のロジスティック回帰この4つのモデルを最終的に使用することにした。
この考えが後の思考を鈍らせるとはこの時の自分は思いもよらなかった・・・

最終順位

結構下でした^^
とても残念。しかし1位の人もXGBoostを使用しているらしかったので
「自分もXGBoost使ってるから実質優勝じゃん」

3秒で開き直りました。

実はXGBoost単体と複数のモデルの組み合わせではXGBoost単体のが精度が出ているのはわかっていました^^
しかし過適合を恐れて後者の方を提出してしまった。結果として平均を取った自分の作戦は他のモデルが足を引っ張ったという感じか。

特徴の整形や選択において、形態素解析を行なっている人もいたらしく、何というかモデルの構築って芸術センスがものを言うところあるよな・・・と思った。

振り返り

特徴の観察、抽出、選択の大切さが一番身にしみた。MNISTとか加工せずそのままモデルに渡せばいいデータセットしか使用していなかったため自分で特徴量を観察、抽出という行為をあまりしたことがなかったからである。
モデルのハイパーパラメータをいじるのではなく、もっと特徴量の観察をすればよかった。

あとは機械学習インターンってメンターからのフィードバックを得るのが難しいと感じた。モデル構築においてこれが正解、というのがないからアドバイスを得難い。。自分からガシガシ質問に行けばよかったのかなという反省もあった。

機械学習と深層学習の違いって個人的には

  • データからパターンをが見つけて、モデルを構築したりするのが機械学習

  • データからパターンを機械が見つけて、モデルを構築するのが深層学習

こんな感じだと判断している。だから機械学習インターンでは良い特徴量、パターンを見つけることができるかが一番大切だと思った。
深層学習の勉強もいいが、特徴量エンジニアリング、データセットの可視化、観察あたりの知識もつけるべきということに気づけてとてもよかった。

外れ値検出、OneClassSVMについて

統計データにはよく外れ値なるものが含まれていることが多いそう。
外れ値とはデータの分布において他の観測値から大きく外れた値のことで、異常値とやらもあるけどそれとは異なるっぽい

外れ値検出、処理することは機械学習を行う上で学習の妨げをなくす大切な過程だそうで、行う必要があるので試してみた。

一つ目は単純にデータの集合から、第一四分位数、第三四分位数、四分位範囲(IQR)を求めて外れ値を検出する方法を試す。
大体のことはこのサイトを参考にした。

結果としてはこんな感じに
f:id:Owatank:20170907143825p:plain
青が元のデータ集合、赤が外れ値の集合
こうしてみると外れ値じゃない範囲がとても狭い・・・

次に scikit-learn というライブラリにあるOneClassSVMを試す
SVMってRCNNの論文とか読んだ限り、クラス分類に使われているイメージなので最低でも2クラス以上で使用するんじゃないとかと思ったのですが調べる限りSVMの性質を使った外れ値のようだ。

実装についてはまずscikit-learnの公式にあるチュートリアルを理解することに。

Outlier detection on a real data set — scikit-learn 0.19.0 documentation
チュートリアルで使用するデータをまずは表示してみる

# Get data
X1 = load_boston()['data'][:, [8, 10]]  # two clusters
X2 = load_boston()['data'][:, [5, 12]]  # "banana"-shaped

plt.xlim((-8, 28))
plt.ylim((3, 40))
plt.scatter(X1[:, 0], X1[:, 1], color='red')

plt.xlim((3, 10))
plt.ylim((-5, 45))
plt.scatter(X2[:, 0], X2[:, 1], color='blue')

f:id:Owatank:20170907144742p:plainf:id:Owatank:20170907144752p:plain

目で見てもわかるくらいの外れ値が確認できる

次にOCSVMを行うための下準備

# Define "classifiers" to be used
classifiers = {"OCSVM": OneClassSVM(nu=0.261, gamma=0.05)}
colors = ['m', 'g', 'b']
legend1 = {}
legend2 = {}

xx1, yy1 = np.meshgrid(np.linspace(-8, 28, 500), np.linspace(3, 40, 500))
xx2, yy2 = np.meshgrid(np.linspace(3, 10, 500), np.linspace(-5, 45, 500))

下2つのnp.meshgridは外れ値を除いたデータの集合を囲むための方眼紙のようなグリッド線を定義しているはず

赤い方のデータセット X1 からOCSVMを試す

for i, (clf_name, clf) in enumerate(classifiers.items()):
    plt.figure(1)
    # OCSVMにサンプル集合X1を与え、サンプル集合の境界を検出する
    clf.fit(X1)
    # xx1.ravel(), yy1.ravel() : 配列を1次元配列に変換する [[1,2,3],[1,2,3]] -> [1,2,3,4,5,6]
    # np.c_() : axis=1の配列の値で連結を行う
    # 各データとの距離が最大となる超平面を求める
    Z1 = clf.decision_function(np.c_[xx1.ravel(), yy1.ravel()])
    Z1 = Z1.reshape(xx1.shape)
    plt.title("Outlier detection on a real data set (boston housing)")
    plt.scatter(X1[:, 0], X1[:, 1], color='red')
    bbox_args = dict(boxstyle="round", fc="0.8")
    arrow_args = dict(arrowstyle="->")
    plt.annotate("several confounded points", xy=(24, 19),
                 xycoords="data", textcoords="data",
                 xytext=(13, 10), bbox=bbox_args, arrowprops=arrow_args)
    plt.scatter(X1[:, 0], X1[:, 1], color='red')
    # 境界の輪郭を描写する
    legend1[clf_name] = plt.contour(
        xx1, yy1, Z1, levels=[0], linewidths=2, colors=colors[i])

OneClassSVM()にあるfit()にデータを渡して検出を行っている
plt.contourにて描いた輪郭は以下のような結果になった
f:id:Owatank:20170907145719p:plain

青のデータ X2 についてはこのような結果になった
f:id:Owatank:20170907145902p:plain

どっちもデータが密集しているような場所を囲っているから大体学習は成功しているのかな

四分位数で使ったデータに対して同じようにOCSVMを試して見たところこんな感じになった
f:id:Owatank:20170907150042p:plain

赤の外れ値をみる限りはいい感じかな囲みがちょっと狭いかも?

外れ値を気にするようなデータを扱ったことがないから実感わかないが、魑魅魍魎跋扈するこの地獄変、外れ値を含む実データなんて腐る程あるんだろうしそんなデータセットを用いてお仕事してる人がたくさんいるんだろう。統計についても勉強すべきだ・・・

SPPnetについてとTensorflowでの畳み込みフィルターの観察

前回の続き

SPPnetを利用した物体検出では入力画像から1回だけ畳み込みの操作を行うことで、大幅な計算時間の削減が可能ということで感動した。

他のサイトの情報や、論文の内容を読み直したりして以下のような手順でSPPnetによる物体検出器を作成しているのかなと考えた。

  1. 入力画像をCNNに渡して畳み込みの操作まで行い、特徴マップを得る。
  2. 入力画像からSelectiveSearch法などで、Region Proposal(物体の候補位置、領域)を得る。
  3. step1で得た特徴マップの対応するRegion Proposal の部分でSPP層の操作を行う。
  4. 各Region Proposalのプーリング結果を出力層に渡して分類結果を得る。

こんな感じかな。
いざTensorflowで実装するぞと意気込んだけど、step3をどう実装すればいいか詰まった。
f:id:Owatank:20170904181723p:plain
入力画像を 224x224 サイズ、特徴マップのサイズを 14x14 とすると入力画像のRegionProposalの座標をどう特徴マップのサイズ内で変換すればいいかわからない。特徴マップのある始点と終点での切り抜きもできるかわかってないけれども。
単純に 224 -> 14 みたいに変換すればいいのかな?特徴マップはフィルター数も多いので悩む。設計するための知識が圧倒的に足りない。。

Fast-RCNNの論文を読み進めてまた悩むことに。

それとは別によく参考書などに畳み込みフィルターなどの小さいマップの画像がよく載っている。
観察してみたかったのでSPPnetでのネットワークで畳み込みフィルターの観察をしてみることにした。

コードはここ
github.com

大体はフィルター適用後の結果を出力してしまった。最後にフィルター自体を出力した。
フィルター自体はf:id:Owatank:20170904183222p:plain
モザイクばかりでエッジなどは見た限りわからない。こんなフィルターでも適用後は
f:id:Owatank:20170904183413p:plain
入力画像の形が残っているので不思議だ。。

物体検出の実装を目指す-SPPnetについて

RCNNに続き、次はSPPnetの理解、実装を目指す。

今回も頑張って論文を読んだ。気のせいかRCNNの論文より読みやすかった。アジアの人が書いた論文だからかな。

まずRCNNの問題点として以下が挙げられる。

  • RCNNはSelectiveSearchなどで得た候補領域の1つ1つをクロッピング、ワーピングの処理を施してCNNの入力に渡すが、クロッピング、ワーピングの処理は望ましくない幾何学的な歪みをもたらす可能性があること。

  • SelectiveSearchで得る約2000件ほどの候補領域1つ1つをCNNにかけるのでとても時間がかかること。

2つめにおいては、認識に関係ないような候補領域までCNNにかけていたら時間が無駄になってしまう。

SPPnetはこれを改善し、なんと畳み込みの処理を画像全体から一度だけ行うことで計算時間の短縮ができると書いてあった。しゅごい。。。。

特に最大の特徴はSPP(Spatial Pyramid Pooling)と呼ばれるプーリングを実装することで、入力の画像サイズに関わらず固定長の出力を得ることができるので入力が固定ではなく可変サイズに変更できること。

RCNNの論文を読んだ自分からすれば胸が熱くなる内容しか書いてない。素晴らしい。。。。

実装としてはあるCNNの最後のConv層に、SPP層を追加するだけっぽい。

f:id:Owatank:20170829174954p:plain

上の方法がRCNNでの手順で、下の方法がSPPnetでの手順。

SPP層で何を行うかというと以下のような処理をする。

f:id:Owatank:20170829175307p:plain

畳み込み層からの特徴マップを それぞれ 16x16, 4x4, 1x1 のウィンドウでmaxプーリングを行う。(ウィンドウサイズの設定は条件がある。)
行った結果をreshapeして固定長の出力にして全結合層へ渡すといった形かな。

実装があっているか確信はないけど、SPP層らしきものを作り結果を試してみた。
学習率1e-5,ミニバッチ30枚でのAlexnetで試す。SPP層なしの方では
f:id:Owatank:20170829181447p:plain f:id:Owatank:20170829181455p:plain f:id:Owatank:20170829181458p:plain

ミニバッチのせいもあるけど損失値が上がったり下がったり不安定。でもちゃんと学習している。

ありの方では
f:id:Owatank:20170829181709p:plainf:id:Owatank:20170829181713p:plainf:id:Owatank:20170829181716p:plain

0~20stepからかなり小さい損失の値を出している。でも200stepくらいではなしの方と値はどっこいといった感じだろうか。ありの方が順調に学習している感じがする。実装が間違っていなければ

このSPP層による物体検出のプログラムを作りたいがいくつかまだ疑問が残っていて、

  • 畳み込みの入力は画像一枚からだが、何を学習したCNNを選べばいいのか

  • SPP層の操作を行った出力か、それとも元の入力画像をSelectiveSearchにかけるのか。(SPP層の出力で強く発火した元の入力画像の候補領域を全結合層に渡すのか)

プログラムの構築も難しそうだが、まだ理解できていない部分が多い。道のりは長そう。

上記の画像はSPPnetの論文から引用。
論文のリンク

深層学習5章の白色化とデノイジング自己符号化器を試してみた

お久しぶりです。

SPPnetの論文をよみつつ深層学習の青い本も読み進めてます。
青い本の5章における白色化とデノイジング自己符号化器が気になったので作って見ることにした。

白色化というのは機械学習における訓練データに偏りがあると学習の妨げになる場合があるので学習前に訓練データに何らかの処理を施し、偏り(相関でいいのかな)を除去する処理のこと。

訓練データの成分間の相関は共分散行列で与えられるっぽい
f:id:Owatank:20170826184205p:plain

この共分散行列の(p,q)成分は、訓練データのサンプルXの(p,q)成分がどの程度同じように変化するか、相関があるかを示す。

ある確率変数 X1,X2 に対しての共分散行列を以下のように定めることにする。
f:id:Owatank:20170826184749p:plain
ρ11,ρ22 はそれぞれ X1の分散、X2の分散を、ρ12 は X1とX2 の共分散を表す。
つまり対角成分に分散が、非対角成分には今日分散が並ぶ行列を共分散行列と呼ぶんだな。

確率変数が互いに独立(無相関)ならば、共分散の値は 0 になるという性質から、白色化はこの共分散行列の非対角成分を 0 にする、つまり対角行列に線形変換する操作を行っている、で合っているかな?。

手順を本通り行い、乱数のデータセットを作り試してみた。
結果はここ

元の訓練データ(正規化済み)の分布が
f:id:Owatank:20170826185721p:plain
こんな感じになる
f:id:Owatank:20170826185743p:plain

線形変換後の共分散行列が対角行列になっているかの確認が取れなかった。

おまけでデノイジング自己符号化器を作った。こっちはTensorflowで実装した。
デノイジング自己符号化器は訓練データをノイズありのもので学習し、出力でノイズなしの結果が得られることを期待する感じ。
訓練データはMNISTのデータを使った。

github.com

学習をガウス分布に従うノイズありのみのデータで行ったため、最後のソルトペッパーノイズでの出力はあまりよろしくなかった。
これを実装している時に、配列を a = b のような形でコピーして、 a の値を変える操作を行うと参照元 b の値も変化するということに気づいた。
ポインタとかそこらへんの問題かな。a = b.copy() のように copy を使うと値だけを得られるということを知った。気をつける。

深層学習5章の自己符号化器をTensorflowで作ってみる

間がかなり空いてしまった。
イルカの深層学習5章にて自己符号化器なるものの説明があった。これは入力層よりノードの少ない中間層で符号化し、入力と同じノード数の出力層にて復号化を行う方法でいいのだろうか。入力のノード数より少ないノード数に落とした時、どれだけ入力と近い結果が得られるかが大事なのかな?
読んでいて「復号できんの!すげえ!!!!!!!!」とワクワクしたのと、誤差関数などが自分でも実装できそうだったので、Tensorflowで構築してみることにした。
本通り、損失関数には二乗誤差を、出力の活性化関数には恒等関数を使用した。

MNISTのデータセットを用いたコードはここ
学習を続けることに、徐々に復号の結果が入力に近くなっていくのがわかった。

次に 64x64 の道路標識の「止まれ」でやってみようと思った。
最初はRGBでやっていたが

f:id:Owatank:20170813143647p:plain

禍々しいものが生まれた。何を学習したのだろうか。

損失の値もいい値にはならなかったので、諦めてグレースケールの方でやってみることにした。
コードはここ

こっちはノード数を 50x50 にした。 32x32 の半分のノード数では上手くいかなかった。
最後の結果からわかるように訓練データの結果は
f:id:Owatank:20170813152429p:plain
そこそこ形がわかるような出力になってくれたが、テストデータの結果は
f:id:Owatank:20170813152508p:plain
モザイクのような結果になってしまった。
訓練データが少なくて過適合を起こしているのかもしれない。

結果を観察するのは楽しかった。

深層学習4章の誤差逆伝播について

深層学習という青いイルカさんが表紙の本がありまして最近読んでいる。
重み上限など自分の知らないことが載っていてまだまだ勉強不足と感じる。

4章の誤差逆伝播について、二乗誤差の第n層の重みの更新を考えるとき
f:id:Owatank:20170801132533p:plain
こんな式が載っていた。右辺の第二項の微分を計算したいが、出力層から第n層までを展開、微分しなくてはいけなくなり計算量が大きくなる問題があるということが書いてあった。そしてその問題を解決するために出てきたのが誤差逆伝播

読み進めていった結果

(;^ω^)
こんな感じの顔になった。連鎖律からどういう微分をするのかはわかった気はするがスッキリしなかった。こっちも微分の計算大変じゃね?という感想。
この資料を参考に実際計算をしてみる。

ネットワークとしては中間層が1つの以下を考える。
f:id:Owatank:20170801133428p:plain
まずは順伝播、h1,z1,h2,z2,p1,p2を求める。
中間層。活性化関数としてシグモイド関数を使うことにする。
f:id:Owatank:20170801135358p:plain
出力層。活性化関数は恒等関数。
f:id:Owatank:20170801135736p:plain

次は逆伝播。重みの更新を試みる。まずは誤差関数の値を計算する
f:id:Owatank:20170801140531p:plain
次に出力層に近い w5 ~ w8 の重みの勾配を求める。
w5はネットワークの図から、p1を通過するのがわかる。連鎖律から以下のように式で表せるはず。
f:id:Owatank:20170801142315p:plain
右辺の第1項は
f:id:Owatank:20170801142403p:plain
第2項は
f:id:Owatank:20170801150506p:plain
右辺が計算できたので、勾配を求めてそのまま重み w5 を更新する。
f:id:Owatank:20170801151130p:plain
元の w5 の値より若干増加した。同様に w6 を更新する。右辺の第1項は共通、計算してあるので
f:id:Owatank:20170801151744p:plain
w7,w8については、この2つは p2 を通過しているので dE/dp2 を計算する。
f:id:Owatank:20170801152158p:plain
w7,w8を更新する
f:id:Owatank:20170801152517p:plainf:id:Owatank:20170801152753p:plain
w7とw8はw5,w6に比べてかなり減少してしまった。あってるのか不安になってきた。

最後に入力と中間の間にある w1~w4の重みを更新する。
w1 は z1とh1 を通過しているので
f:id:Owatank:20170801155009p:plain
右辺の第1項から計算していく。z1はp1とp2に値を伝播しているので以下のような連鎖律で表せるはず。
f:id:Owatank:20170801154532p:plain f:id:Owatank:20170801154616p:plain
dE1/dp1とdE2/dp2はw5~w8の重みの更新をする際に計算したのでやるべき計算は dp1/dz1 と dp2/dz1 のみ

右辺第2項と第3項を計算する。
f:id:Owatank:20170801162018p:plain
こっちは比較的楽。右辺が計算できたので勾配を求め、w1の重みを更新する。
f:id:Owatank:20170801162520p:plain
w2も同様に
f:id:Owatank:20170801162859p:plain
w3はz2,h2を通過してきているので、
f:id:Owatank:20170801164224p:plain
右辺第1項を計算する
f:id:Owatank:20170801164309p:plain
右辺第2項と第3項も計算する
f:id:Owatank:20170801164434p:plain
右辺が全て計算できたので w3 の重みを更新する。
f:id:Owatank:20170801164735p:plain
w4も同様に
f:id:Owatank:20170801165035p:plain

これで w1 ~ w8 全ての重みが更新できた。
誤差逆伝搬における入力と中間の間の w1 ~ w4 の重みに更新については出力層から微分の値を受け取ることで、局所的な計算のみで勾配を計算できる認識でいいのかな?
計算が苦手な自分でも求められたから楽にはなってるだろう。多層になればなおさらな気がした。