【効果検証入門】が良かったのでまとめてみた②
前回に引き続き、効果検証入門の内容をまとめていこうと思います。本記事では2章の介入効果を測るための回帰分析について説明していきます。なお本記事ではRによる実装部分は割愛いたします。
はじめに
前回の記事では、効果検証を行う上で引き起こりやすいセレクションバイアスについての説明とその理想的な対応策であるRCT(無作為化比較実験)について説明しました。RCTは分析を行う上で、非常に都合の良い反面、ビジネスとしては大きなコストになってしまう可能性があります。
そこで本記事ではセレクションバイアスが存在している状態でもその影響を少なくすることのできる回帰分析について説明していきます。回帰分析は最も基本的なセレクションバイアスの削減方法です。
効果分析のための回帰分析
セレクションバイアスの入ったデータで効果検証を行う場合、何も対処せず分析を行うとセレクションバイアスを含んだ介入効果を導出することになってしまい、真の効果がわかりません。効果分析のための回帰分析では、データに存在するセレクションバイアスの影響をなるべく減らして
・被説明変数 : 介入による効果を確認したい変数
・介入変数 : 施策の有無を示すバイナリ変数
・共変量 : セレクションバイアスを発生させていると分析者が想定する変数
は統計学では説明変数、機械学習の分野では特徴量と呼ばれたりもします。効果を検証する際の回帰分析においては、説明変数が複数あることが一般的です。このように複数の種類の変数をモデルに含む回帰分析を重回帰分析と言います。今回はを説明変数とした重回帰分析について説明していきます。以下にを説明変数としたに対する回帰式を示します。
左辺はの場合のの期待値(条件つき期待値)を示しています。これはが与えられたときにがどのような値を取りうるかを表したもので、これが今回の回帰分析で対象となる回帰式です。なお回帰式の係数は未知のため、後ほど説明する最小2乗法によってこれらのパラメータを推定する必要があります。
こちらは上記の式を少しだけ変形したものです。は実際のデータを示しており、は実際のデータと回帰式との間の誤差を示しています。
(図)回帰(図簡単のためX,Zは1次元にまとめ疑似的な単回帰で説明)
上記で示した誤差は以下の式のように表すことができます。
回帰式を求めるにはこの誤差を最小化するように計算をする必要があります。このとき使われるのが最小2乗法です。最小2乗法は誤差の2乗を最小にすることで、誤差がプラスの値であろうがマイナスの値であろうが関係なく全ての誤差の和が最小となるパラメータを求めることができます。
ここで本題である介入効果の分析において調べたいのは、介入した場合としなかった場合の期待値の差です。それぞれの期待値は以下のように表せます。
介入の効果はこれらの差分であるので
となります。これは回帰分析において介入の効果はであることを示しています。つまり回帰分析では介入変数の係数のみが介入の効果を表すということになります。よって最小2乗法によってを求めれば、回帰分析によって介入の効果を推定することができます。仮にと導出された場合、介入したグループのの平均は介入していないグループの平均より40%高いであろうということを意味しています。ただ求めた値はあくまで推定値であるため、それが偶然得られたものであるかどうかを有意差検定を用いて検証する必要もあります。なおプログラムでパラメータの推定を行うと今回の場合、も同時に推定されますが、効果検証のための回帰分析で興味があるのはあくまで介入変数の係数だけであるため、それ以外の値に対して何らかの解釈を行うことはありません。
回帰分析におけるバイアス
上記で示したのはあくまで回帰分析による平均的な効果の比較になります。そのため得られた推定値はセレクションバイアスと実際の効果の和となります。回帰分析でセレクションバイアスが小さくなるような推定を行うためには、共変量を正しく選択する必要があります。セレクションバイアスが発生しているデータにおいて、新たな共変量を加えて回帰分析を行うことで、セレクションバイアスの影響がより少ない分析結果を得ることができます。
脱落変数バイアス(OVB)
ではセレクションバイアスの影響をより小さくするためにはどのような共変量をモデルに追加するべきか。結論から述べると「目的関数Yと介入関数Zに対して相関のある変数を与えるべき」ということになります。以下に2つのモデルを示します。
この2つのモデルの差は共変量が追加されているか否かです。モデルBはセレクションバイアスの影響を軽減した結果が得られるとします。
このことからこの変数を加えたことによってモデルBのセレクションバイアスが軽減されたことが伺えます。このような本来は必要であるがモデルから抜け落ちている変数を脱落変数と呼びます。を無視したモデルAにおいて介入効果を示すパラメータは
となることが知られています。*1
上式に示すようにモデルAの介入効果はセレクションバイアスの影響を取り除けているモデルBの介入効果に加えて、余分なバイアスが存在していることが分かります。こののように必要な共変量がモデルから抜け落ちていることによって起こるバイアスを脱落変数バイアス(Omitted Variable Bias: OVB)と言います。このモデルA,Bの比較は必要な共変量がモデルに含まれない場合には推定される効果にはOVBが含まれていることを示すとともに、一方ではそのような変数をモデルに加えることでOVBの影響を取り除くことが可能であるという回帰分析の基本的な仕組みを説明しています。
なお詳細な説明は省きますが、OVBは介入量にも目的変数にも相関のある共変量が抜け落ちることによって生じます。このような,の両方に関係のあるような変数のことを交絡因子といいます。目的変数との相関が0でない変数は、を予測する上では含まれるべき重要な存在といえます。しかしながらその変数ととの相関が0であるような場合はその変数を加えてもの効果には変化がない、つまりはOVBの減少にはつながらないということになります。よってとどちらに対してもある程度相関のある変数を加えることにより、バイアスの少ない効果推定を行うことができます。
Conditional Independence Assumption(CIA)
効果検証のための回帰分析における共変量の選択は、理想的にはOVBが0になるような状態を目指します。この状況を、CIA(Conditional Independence Assumption)といいます。直感的な解釈としては、共変量の値が同一のサンプルにおいて、介入量はランダムに割り振られているのに等しい状態というものです。
CIAは以下の式で表されます。
回帰分析で推定した効果の値が本当に正しいのかを考える場合、CIAが満たされているかどうかを考える必要があります。
変数の選び方とモデルの評価
回帰分析において分析者は以下のような手順でモデルを作ることになります。
1.介入の割り当てがどのように決定されるのかを考える
2.想定される決定方法を表現できるような共変量を選択する
3.選択した共変量とYとの関係性を考慮してを決める
このとき推定された効果の妥当性を主張するには作ったモデルの共変量がCIAを満たしているかと考える必要があります。しかしこれを主張する上で2つの問題点があります。
①バイアスの評価ができないという問題
この場合、基本的に分析者がセレクションバイアスの発生原因、どの変数が有効かを仮定する必要があります。
②必要な共変量がデータにはないという問題
この場合、基本的には回帰分析ではセレクションバイアスの影響を取り除いて効果を推定するのは不可能になります。
Post Treatment Bias
介入によって影響を受けた変数を分析に入れることによって起こるバイアスのことをPost Treatment Biasと呼びます。Post Treatment Biasは、元々の介入自体がどのような方法で割り当てられていたとしても発生してしまう問題です。よって仮にRCTによって介入をランダムに選んだユーザに配信したとしても、介入によって影響を受けた変数を投入して作成されたモデルはPost Treatment Biasを含んだ状態となってしまいます。この問題を避けるためには介入よりも後のタイミングで値が決まるような変数を分析から除外する必要があります。
回帰分析に関する様々な議論
分析の現場では、しばしば目的変数に対する予測能力の低いモデルは効果の検証として利用価値がないといった議論が発生します。これは機械学習のような予測を目的として捉える分野においては重要な視点であり、また多くの場合モデルの予測能力の計測が直感的なモデルの評価になると考えられています。一方、今回のような回帰分析のプロセスでは、予測能力や手元のデータに対する説明能力は一切考慮しません。この理由は「モデルのデータに対する説明能力や未知のサンプルに対する予測能力を高めることが効果検証において有用であるという保証にはならない」という点にあります。
脱落変数バイアスの式に注目すると介入変数と目的関数に相関するような変数を含めればより正確なに関する効果を得ることができる一方で単にの予測を改善するような変数を追加してもその変数がと相関がないようであれば効果の推定は改善しないということになります。このような事情からに対する予測能力が一定以上なければ効果検証としてもモデルの価値がないという批判は、本質をついたものではないことが分かります。
おわりに
次回は3章の傾向スコアを用いた分析についてまとめたいと思います。
*1:OVBの詳細はこちら: www.amazon.co.jp
【効果検証入門】が良かったのでまとめてみた①
はじめに
先日効果検証入門という本を読みました。こちらの内容が非常に素晴らしかったので、内容を整理するために本記事を書いています。
現在身の回りで『効果』という言葉を様々な場所で目にするようになっています。「ある食品はダイエットに効果がある」「広告は売上を増加させる効果がある」など。
効果は私たちが意思決定をする上で非常に重要なものですが、その多くは、測り方を間違っていたり思い込み(バイアス)が入っていたりと正しく検証するのは難しいものでもあります。効果検証を行う上で問題となるのは、比較が正しくできていないために、因果関係を示すことができていないという点です。この本では比較を正しく行うためにどのようにしてバイアスを取り除くか、その手法をまとめたものです。
この記事では本著1章に該当するセレクションバイアスとRCT(無作為化比較実験)について自分なりに重要だと思ったところを簡潔にまとめます。なお本著ではRによる検定手法の実装なども含まれていますが、本記事では割愛します。
セレクションバイアス
セレクションバイアスとは、比較実験を行う際に、観察対象のグループ分けによって引き起こされるバイアスのことを言います。 メールマーケティングの例をもとに説明していきます。 Amazonがユーザに、Amazon内の商品を宣伝するメールを送信することで、購買を促すマーケティング施策を行うとします。Amazonは売上を伸ばすために、メールに割引クーポンを添付するという施策を行います。クーポンをメールにて受け取ったユーザは普段より安い価格に反応し、本来より多くの購買に至る可能性があると考えられます。よってメールを配信することで、潜在的な購買量(何も施策を行わない場合の購買量)に加えて、メールの効果によって増えた購買量が得られると想定しています。この効果検証は「クーポン付きのメールを送信するという介入が、ユーザ1人あたりの売上をどう変化させるか」を知りたいものとします。
(図) メールマーケティングの例
既にメールは送付済みのものとする場合、単純に、①メールを受け取ったユーザ と ②受け取らなかったユーザ の売上の傾向を比較すれば、メールの効果を検証できそうですが、これがセレクションバイアスの罠です。その理由として多くの場合ではメールマーケティングをより効率的に実施する目的から、ある程度購買の見込みのあるユーザにメールを送信していることが考えられます。つまりそもそも①メールを受け取ったユーザ と ②受け取らなかったユーザの選択には恣意性が含まれており、これによってセレクションバイアスを招いている可能性があるのです。
(図) メール割り振りの仕組み セレクションバイアスが引き起こされているとどうなるか。①メールを受け取ったユーザはもともと購買量の多い傾向のグループであり②受け取らなかったユーザはもともと購買量の少ない傾向のグループであると言えます。よってメールの介入による効果がなくても潜在的な購買量の差によってあたかもメールマーケティングに絶大な効果があるように思える状況が発生してしまいます。
(図) グループ間のセレクションバイアス
このセレクションバイアスに対して何らかの対処を施した上でデータ分析を行わなければ、得られた効果はバイアスを含んでしまい、本来取るべき意思決定とは異なる決定を下してしまう可能性があります。たまに「分析のバイアスはデータ数の増加によって解決される」と誤解されていることがあるそうです。比較対象にセレクションバイアスが含まれている場合、推定している値がそもそも興味のない値(バイアスを含んだ値)であるため、データ数を増やしたところで興味のない値をより正確に推定するようになるだけであることに注意する必要があります。
RCT (Randomized Controlled Trial)
バイアスを取り除くために実行可能で最も信頼のおける効果の検証方法が、介入を無作為にすることです。つまり、介入を実施する対象をランダムに選択して実験し、その結果得られたデータを分析することです。先程の例で言うと、メールを送る対象をランダムに選択することで①メールを受け取ったユーザ と ②受け取らなかったユーザ間のセレクションバイアスを取り除き正しい効果検証を行うことが可能となります。
(図)RCTのイメージ
なぜこれだけでバイアスを取り除くことができるかと言うと、介入が行われる対象と行われない対象におけるその他の要因も平均的に同一となるからです。メールの例で言うと、送る対象をランダムに選別することで、先程問題となった購買量の差も平均的には無くなりますし、その他の属性(性別、年齢など)も平均的には同一となることが期待されます。よってバイアスを気にすることなく効果を検証することが可能となるのです。このように効果を知りたい施策をランダムに割り振り、その結果として得られたデータを分析して比較することをRCT(無作為化比較試験、Randomized Controlled Trial)と言います。
有意差検定の注意点
統計学では有意差検定というものが存在します。詳細な説明は省きますが、推定結果が偶然得られた可能性について検証するものです。 ここでは本著で記載されていた有意差検定の注意点についてまとめます。
p値
有意差検定では最終的な評価を下すためにp値と呼ばれる値に変換します。p値は得られた推定結果が偶然得られてしまう確率を示します。 有意差検定ではデータから得られたp値が有意水準(5%や1%)より低い場合は、得られた推定結果が偶然である可能性は十分に低いとして統計的に有意な値であると評価します。一方で、有意水準を上回るようなp値が得られた場合は、推定結果が得られたのは偶然であるということを否定しきれないという解釈になります。これは推定結果が偶然得られたとみなしているわけではないことに注意する必要があります。
信頼区間
p値以外に、信頼区間を利用した意思決定が行われていることもあります。95%の信頼区間と言ったとき、95%の意味合いは100回データを変えて同じ推定を行ったとき、母集団におけるパラメータの真の値が95%ほどはその区間に含まれるということを示します。加えて95%の信頼区間内に0を含む場合には, p値が5%よりも高い状態と同じ意味を持つことになります。
有意差検定について
有意差検定とは不確実性に関する評価を簡略化して考えるためにのルールであり、絶対的な判断基準というわけではありません。また有意差検定はあらゆる分析の結果に保証を与えるものではなく、例えばRCTを行っていないデータで有意差検定を行う場合、セレクションバイアスが大きいと有意差検定の結果は有意になりやすいことがあります。これらのことから、有意差検定は何でもかんでも効果を保証するような道具ではないことが分かります。
ビジネスにおける因果推論の必要性
RCTはバイアスを取り除く上で非常に有効な方法ですが、RCTは効果を検証するために介入がランダムに割り当てられるという状況を作る必要があります。これは分析の都合を最優先した介入の割り当てを行うことになるため、ビジネスの面を度外視したものになります。メールの例で言うと、購買量が少ない人にもメールを送ることになり、その施策はビジネスの観点からは非合理的で、短期的には売上が下がってしまうことも考えられます。つまりRCTは分析を行う上で、非常に都合の良い反面、ビジネスとしては大きなコストになってしまう可能性があります。 計量経済学や因果推論は、このように理想的にはRCTでデータをデザインして分析したいがそれが不可能という状態においてRCTの結果を近似するような方法論を提供してくれます。
おわりに
本記事では効果検証入門の1章部分を簡潔にまとめてみました。次は2章の回帰分析についてまとめたいと思います。
GCPとdockerによるkaggle環境の構築手順
はじめに
画像データを扱うkaggleコンペに参加しようと思い、クラウドでGPU環境を構築しようと考えて以下のサイトを見つけました。
非常に丁寧で分かりやすい記事で参考にさせていただいたのですが、いくつかつまずくポイントもあったため、記録として残しておこうと思います。 今回はGCP(Google Cloud Pratform)上でdockerとkaggleAPIを用いて行うkaggle環境の構築手順についてまとめたいと思います。
GCEインスタンスの作成
まずはクラウド上にインスタンスを作成します。この手順は非常に簡単です。
GCPの公式サイトにログイン(登録が済んでいない方はサインイン)します。 console.cloud.google.com
次にGPUの割り当て設定を確認します。 ナビゲーションメニュー → 「IAMと管理」 → 「割り当て」を選択し、指標のところでGPUs(all regions)を選択する この時一番右側に表示される上限の値が使えるGPU数を示しています。 何も設定していない段階ではおそらく0になっているためCompute Engine API GPUs(all regions)の項目にチェックを入れ「割り当ての編集」を選択し,手順に従って割り当て数を1以上に変更します。これでGPUをGCEインスタンス上に加えることが可能になります。
ナビゲーションメニュー → Marketplace → 「deep learning VM」という環境を選択しLAUNCHボタンをクリック
すると以下の画面が出てきます。
Deployment name: 任意の環境名
Zone: 特に変更なし
Machine type: CPUの数とメモリをカスタマイズする。
この時カスタマイズをするとGPUs aren’t available for the selected Machine Type という警告が出る場合があります。GPUと選択できるMachine typeには制限があるようなので、もし選択したMachine typeがGPUを利用できない場合はそれに近い別のものを選択する必要があります。今回はvCPU×4 メモリ26GBのものを選択しました。
GPUs: 使いたいGPUを選択
Framework: 今回はdockerで環境を作るためフレームワークのインストールは不要です。そこでIntel(R)のCUDA10.1のものを選択することにします。
GPU: 両方にチェック
Boot disk: 特に変更なし
Networking: 特に変更なし
設定が終わったら「デプロイ」をクリック
デプロイされていればGCEインスタンスの作成は完了です。 私の場合、警告が表示されていますが、そのまま続行しました。今のところ問題は起きていませんがもし解決方法がわかる方がいらっしゃればご教授いただけると幸いです。
gcloudのインストール
qiita.com
この記事に従ってgcloudコマンドをインストールするとローカルのターミナルから作成したインスタンスに簡単にssh接続できるようになります。
(gcloudを入れなくてもブラウザ上のターミナルから作成したインスタンスに接続することはできます。その方法については他記事を参照ください。)
インスタンスにssh接続
ナビゲーションメニュー → Compute Engine → 「VMインスタンス」で先ほど作成したインスタンスを開始
インスタンスが開始したらssh → gcloudコマンドを表示を選択
出てきたポップアップのscriptをローカルのターミナルにコピペして実行しパスワードを入力することでインスタンス内に接続することができます。
dockerによる環境構築
次にdockerによる環境構築を行なっていきます。 まずは接続したディレクトリでDockerfileとrequirements.txtを作成します。 以下をインスタンス内に入ったターミナルで実行します。
$ touch Dockerfile
$ touch requirements.txt
Dockerfileは環境構築のための仕様書のようなもので、requirements.txtにはインストールするモジュールを記載します。 vimがデフォルトで入っているのでvimを用いて以下のようにDockerfileとrequirements.txtをターミナル上で書きます。内容は冒頭で紹介した記事を参考にしています。
Dockerfile
FROM nvidia/cuda:10.0-cudnn7-devel-ubuntu16.04 # install basic dependencies RUN apt-get update && apt-get upgrade -y && apt-get install -y --no-install-recommends \ sudo git wget cmake nano vim gcc g++ build-essential ca-certificates software-properties-common \ && rm -rf /var/lib/apt/lists/* # install python RUN add-apt-repository ppa:deadsnakes/ppa \ && apt-get update \ && apt-get install -y python3.6 \ && wget -O ./get-pip.py https://bootstrap.pypa.io/get-pip.py \ && python3.6 ./get-pip.py \ && ln -s /usr/bin/python3.6 /usr/local/bin/python3 \ && ln -s /usr/bin/python3.6 /usr/local/bin/python # install common python packages ADD ./requirements.txt /tmp RUN pip install pip setuptools -U && pip install -r /tmp/requirements.txt # set working directory WORKDIR /root/user # config and clean up RUN ldconfig \ && apt-get clean \ && apt-get autoremove
requirements.txt
pip==20.0.2 setuptools==39.1.0 wheel==0.30.0 jupyter==1.0.0 matplotlib==2.2.2 seaborn==0.8.1 scikit_learn==0.20.0 scipy==1.1.0 numpy==1.15.2 pandas==0.23.4 gensim==3.6.0 kaggle==1.5.6 torch==1.4.0 torchvision==0.5.0
今回はフレームワークにPyTorchを選択しています。 PyTorchとCUDAの対応バージョンが一致していないと環境構築でつまずきます。 今回はインスタンスの作成時にCUDA 10.1をインストールしたのでそれに対応するバージョンのPyTorchモジュール(torch, torchvision)をインストールします。
今いるディレクトリ内にここで作成した2つのファイルが存在していればOKです。 ではDockerfileに従ってdockerイメージをbuildします。イメージ名は任意の名前をつけることができます。
$ docker build ./ -t {イメージ名}
上記のスクリプトを実行するとbuildが始まります。完了にはしばらくかかります。 終了時にSuccessfully built ~とかかれていればdockerイメージのbuildは成功です。
ではprojectというフォルダを作成しその後イメージからコンテナを作成します。 コードの意味については冒頭の記事を参照ください。
$ mkdir project $ docker run --runtime=nvidia -p 8888:8888 -d -v ~/project:/root/user/project --name {コンテナ名} {イメージ名} /sbin/init
上記で任意の名前のコンテナができたので下記のスクリプトでコンテナ内に入ります。
$ docker exec -it {コンテナ名} /bin/bash
これでdockerの設定はおしまいです。コンテナを抜けたいときはexitを入力するとコンテナを抜けれます。 またコンテナに入り直したいときは dicker exec~ を再び実行すれば同じコンテナに入ることができます。
Kaggle APIの導入
kaggle APIを利用してデータセットをターミナル上で取得することが可能です。
ただしkaggle APIの導入作業をする必要があります。
まずkaggleの公式サイトからMy accountに移動します。APIという項目の部分のCreate New API Tokenというボタンをクリックするとkaggle.jsonというファイルがダウンロードされます。 このファイルの中身をコピーしておきます。
次にインスタンスと接続しているターミナルからコンテナ内に入ります。 root直下に.kaggleというフォルダが既に作成されていると思うので
$ touch ~/.kaggle/kaggle.json
を実行しkaggle.jsonという空のファイルを作成します。 この空のファイルにさきほどダウンロードしコピーしたものを貼り付けます。vimとかでターミナル上で開いてコピペするのが早いと思います。 その後以下のコマンドを入力し実行権限を付与します。
$ chmod 600 ~/.kaggle/kaggle.json
この設定をすることでdocker内でkaggleコマンドが使えるようになります。
kaggleコマンドを用いることでデータセットのダウンロードを簡単にできます。
以下使いそうなコマンドをメモしておきます。
特定のコンペのデータをダウンロード
$ kaggle competitions download -c [コンペ名]
特定のkeywordでdatasetを検索
$ kaggle datasets list -s [KEYWORD]
データセットをダウンロード
$ kaggle datasets download -d [DATASET_NAME]
ダウンロードされたデータセットはzipファイルになっているので解凍します。 zip, unzipコマンドのインストール
$ sudo apt-get install zip unzip
zipファイルの解凍
$ unzip {zipファイル名}
Jupyter Notebookの設定
jupyterを起動するにはインスタンス内でコンテナに入った状態で以下を実行します。
$ jupyter notebook --ip=0.0.0.0 --allow-root
すると~ token= ~と書かれた文が出力されると思うので,tokenをコピーしておきます。
その後インスタンスに入っていないローカルのターミナルを開き以下を実行します。
$ gcloud compute ssh "{インスタンス名}" -- -N -f -L 7777:localhost:8888
その後localhost:7777をURLとしてブラウザに入力するとパスワードもしくはtokenが求められるので、先程のtokenを入力すると jupyterへの接続が完了します。
以上で環境構築は終了です。
おわりに
web上でGCPでの環境構築に関する有益な記事は存在しているものの、つまずきポイントが多かったので自分なりにまとめてみました。 環境構築に関してはdockerを使わない方法も試してみたんですけど、結果的にはdockerを用いての環境構築の方がいろいろと楽な気がしました。
2019年の振り返り
2019年も残りわずかとなったので、今年の学びと2020年の目標を記録として残しておこうと思います。
純粋な興味から機械学習を勉強し始めたのが昨年の11月で、そこから非常に多くのことを学んできました。1年の振り返りなので時系列で整理していきたいと思います。
1月
Couseraの機械学習コースを受講しました。
スタンフォード大学のAndrew Ng氏が講師を務めるオンライン講義で機械学習の入門として評判が高い+無料ということで受講しました。講義は英語でしたが、有志の方のおかげで日本語字幕もあったため、言語による障害はほとんどありませんでした。
Andrew氏の講義は非常に分かりやすく、動画学習の良さというのをここで初めて感じました。ただ使用するプログラミング言語がOctaveという言語で進行していたため、実装をそれほど重要視せずあくまで理論の方を理解するというスタンスで受講しました。
今考えると理論だけでなく、別の教材やコンペ等で実装を試しつつ、理解を深めた方が良かったのかなとも思いました。
2月
日本ディープラーニング協会が主催するG検定を受験しました。
機械学習分野に関して全くの無知であったため、分野に関する概要を浅くはあるものの幅広く知るという意味でいい動機づけになると思い、受験しました。
公式の問題集とStudy-Alというサイトにある模試を基本的には行なっていました。
その他にもQiitaのまとめ記事やAI白書等を読み、検定に臨んだ結果無事合格することができました。資格自体が役に立っているとは言い難いですが、画像認識、自然言語処理、強化学習、生成モデル、ベイズ学習など、非常に幅広い機械学習分野にどういう技術があるのか、その大枠を知ることができたのは良かったと思っています。
3月
機械学習の世界的コンペKaggleに初めて参加しました。
1-2月でどちらかというと知識のインプットばかりを行なっているように感じていたので思い切って参加してみました。当時開催されていたコンペの中で最も規模の大きかった『Santander Customer Transaction Prediction』というコンペに参加しました。
このコンペは銀行のユーザーが商品あるいはローン等に対して契約をするか否かを、200個もの匿名の特徴量から判定するというものでした。初めてのKaggleであったので公開されているKernel(現Notebook)を辿ることしかできませんでしたが、EDA(探索的データ分析)の重要性をこのコンペから学びました。
4月
就職活動が終了した解放感から新しいことを学びたいと思い、以下の書籍を購入し勉強しました。
・機械学習のエッセンス
機械学習のエッセンスは数学的な機械学習の基礎部分を丁寧に解説されていてPython実装もある良書でした。個人的にはCouseraの機械学習コースよりもこちらの本の方が基礎を学ぶ目的であれば取り組みやすいかなと思いました。
ベイズ本は途中まで読んでいたのですが、挫折してしまい半分までしか読めていないので2020年に改めて読み返してみようと思っています。
5月
Kaggleのコンペ『Freesound Audio Tagging 2019』に参加しました。
内容は与えられた音声データのラベルを予測するというものでした。このコンペでも正直Kernelを参考に少し変更を加える程度しかできなかったのですが、そのKerneがPyTorchで書かれており、そこで初めてPyTorchに触れました。もともとはKerasしか使ったことがなかったのですが、KaggleではPyTorchを利用する人が多い印象を受けたのでここらへんからPyTorchを使うようになりました。
6月
研究で機械学習を使うことになりました。
私の所属する研究室は鉄鋼材料の研究をメインに行なっているところで機械学習とは無縁のところでした。しかし縁あって機械学習を用いた研究に参加できることになり、そこから4ヶ月ほど研究でも機械学習を使いました。当時プログラミングによる機械学習の実装技術はそれほどなかったのですが、研究で用いたのを機にステップアップできたように感じています。やはり勉強のための勉強ではなく、目的がありそれを達成するために学んでいくことが一番学習効率が良いことを肌で感じました。
7-8月
Kaggleコンペ 『Generative Dog Images』に参加
今年参加した機械学習コンペの中で個人的に最も楽しかったコンペです。
GAN(敵対的生成ネットワーク)を使用して与えられた犬のデータセットをもとに犬の画像を生成し、その生成画像の精巧さと多様性の観点から競い合うというものです。ただしKaggle内のKernel環境(Google Colabのようなもの)でのみ学習可能という条件付きであったため制限時間のある中でより良い画像を生成するというのがこのコンペの肝でした。このコンペが自分の中で最もコミットしたもので、DiscussionやKernelは毎日追い、GANの論文も1日1本ペースで読んでいました。結果は残念ながら203/927位とメダル獲得はできませんでしたが、KaggleそしてGANという技術の面白さを体感することができました。反省としては、当時比較的新しいGANのモデルであったBigGANやSAGANが上位入賞者に使われていたのですが、私は検討すらしていませんでした。Kaggleではある技術を取り入れるだけでメダル入賞も十分ありうるので、日本語の記事だけでなく新しい文献の調査も今後コンペに参加する際は取り組んでいこうと思いました。
9月
ベイズに挫折した私ですが再び挑戦してみようと思い、当時話題となっていた以下の新著を購入し勉強しまたもや挫折しました。
この本も2020年度に持ち越したいと思います。
10月
SIGNATEで学生限定の家賃予測コンペに参加
学生限定コンペがあるという情報を聞き、初めてSIGNATEに参加しました。内容は東京23区内に存在する家の家賃を予測するというものでした。初めて真剣に取り組んだテーブルデータのコンペで学びが非常に多かったのと同時に、コンペ終了後の上位者の解法を見て、同年代の人と現在の自分との差を痛感したコンペでもありました。
12月
初めてAdventCalender投稿しました。
KaggleのGANコンペに参加し興味を持ったのと、発信する機会を強制的に作りたいという理由からDeep learning 論文紹介AdventCalenderにてGANの論文まとめを投稿することに決めました。内容は当時ちょうど開催されていたICCV2019でSinGANというGANが話題となっていたのでその論文についてまとめることにしました。投稿後自分が思っていた以上に反応があって、アウトプットすることのモチベーションになりました。私はもともと情報系の専門ではないので、GitHubやQiita、Arxivなどのように情報を共有する環境・雰囲気が整っており、それに対して反応がくるという文化がすごく羨ましく感じました。
2020年の展望
現在考えている私の来年度の目標はこんな感じです。
[勉強面]
[Kaggle]
- Kaggle expertになる
- Kaggleにチームを組んで参加する
[実績作り]
- 機械学習を使った成果物を作る
- 勉強会等の外部のイベントに参加してみる
- ブログ等での発信の頻度を増やす
まず勉強面に関してです。強化学習は以前から興味があったのですが、取り組めていなかったので見識を広げるという意味でも勉強していこうと思っています。また統計の知識が圧倒的に不足していると感じているので統計学を基礎から学びつつ、今年挫折してしまったベイズやガウス過程への理解を深めれればと思っています。
Kaggleに関しては実績としてまだ何も残せていないので、次の称号であるexpertをまずは目標として取り組みたいと思います。また複数人でのコンペ参加を体験したことがないので来年度はチームを組んでの参加にも挑戦します。ただKaggle等のコンペというのはあくまで与えられた課題に対しての取り組みであることに留意する必要があるとも考えています。
そこでコンペ以外での実績づくりも行なっていきたいと考えています。最近以下のような記事を拝見しました。この取り組みは非常に面白いなと思います。
このように自分で一から問題を設定して、〇〇×AlというようにAlを別の分野に応用する形で面白い成果物を作りたいなと考えています。この方のように自分の興味のある分野に応用するのが一番熱中して取り組めると思うので、今思いつくあたりだと、私はもともと野球をやっていたので投手の癖をAlで見抜いたり、投球フォームからボールの軌道を予測するというようなことができたら非常に面白いのかなと思っています。そういう取り組みの過程をブログやイベントで発信することで、実績を積み重ねていきたいと思っています。
以上が今年の振り返りと来年の展望です。
1年後の今頃、自分がどのような振り返りをしているのかとても楽しみです。
PyTorchでDCGANを実装する
はじめに
この記事ではPyTorchを使ってDCGANの解説および実装を行います。 今回はMNISTのデータセットを利用して、手書き数字の0~2の画像生成を行います。 DCGANの解説には元論文とDCGANの解説が非常にわかりやすい以下のスライドを使用します。 DCGANの実装には書籍『PyTorchで作る発展ディープラーニング』を使用しています。なお本記事でのコードの紹介はモデルの定義と訓練の課程のみとさせていただきます。詳細はGitHubをご確認ください。
www.slideshare.net
本記事で紹介するコードの全体 github.com
参考にした本の著者のGitHub github.com
DCGANの概要
DCGANは2016年に出たGANの派生系です。オリジナルのGANが多層パーセプトロンでモデルを作成していたのに対してDCGANでは画像認識タスクで有効とされているDeepなCNN構造をGANに適応することでGANの表現力をあげたGANの歴史の中でも非常に重要な存在です。 ただしGANの構造自体はそれほど変わりません。生成モデルであるGeneratorは多次元ノイズを入力とし、画像を生成し出力します。識別モデルであるDiscriminatorは、Generatorによって生成された画像と本物の画像を入力とし、各画像の識別結果を出力します。GeneratorはDiscriminatorを騙すために本物そっくりの画像を生成するように、DiscriminatorはGeneratorが生成した画像と本物画像をしっかり識別するように、2つのモデルが互いに敵対しながら学習するのがGANの基本的な構造です。
話をDCGANに戻します。GANにCNNを適応しようという取り組みはDCGAN以前にも行われていましたが、GANの学習における不安定性がCNNの適応を困難なものにしていたようです。学習の安定化のためにDCGANでは以下の事項に変更が加えられています。
- GANにPooling層のないCNNを導入
- Batch Normalizationを導入
- 隠れ層では全結合層を使用しない
- Generator/Discriminatorの活性化関数にはReLU/LeakyReLUをそれぞれ使用(ただしGeneratorの出力層はTanh)
以上がDCGANの概要となります。
Generator と Disicriminatorの解説と実装
それではGeneratorとDiscriminatorの解説をしていきます。
Generator(生成モデル)
こちらの図がGeneratorの構造を示したものです。
Generatorは100次元のノイズzを入力し4層のCNNによって64*64の画像G(z)を生成します。
CNNの1つの層は①転置畳み込み層 + ②Batch Norm + ③ReLUの3つで構成されています。
転置畳み込み層はCNNで一般的に用いられる畳み込み層の逆っぽい操作をします。
こちらのサイトが畳み込み層と転置畳み込み層をアニメーションで示しており非常にわかりやすいです。
(上記サイトより引用)
このアニメーションではpadding=1, stride=1, kernel=(4*4)の条件で転置畳み込みを行なっていますが、今回のGeneratorで用いる転置畳み込み層はstride=2です。
以下、Generatorの実装になります。
class Generator(nn.Module): # nzは入力ノイズの次元数 def __init__(self, nz, image_size): super(Generator, self).__init__() self.layer1 = nn.Sequential( nn.ConvTranspose2d(nz, image_size*8, kernel_size=4, stride=1), nn.BatchNorm2d(image_size*8), nn.ReLU(inplace=True)) self.layer2 = nn.Sequential( nn.ConvTranspose2d(image_size*8, image_size*4, kernel_size=4, stride=2, padding=1), nn.BatchNorm2d(image_size * 4), nn.ReLU(inplace=True)) self.layer3 = nn.Sequential( nn.ConvTranspose2d(image_size*4, image_size*2, kernel_size=4, stride=2, padding=1), nn.BatchNorm2d(image_size * 2), nn.ReLU(inplace=True)) self.layer4 = nn.Sequential( nn.ConvTranspose2d(image_size*2, image_size, kernel_size=4, stride=2, padding=1), nn.BatchNorm2d(image_size), nn.ReLU(inplace=True)) # MNISTなので出力のチャンネルは1 self.last = nn.Sequential( nn.ConvTranspose2d(image_size, 1, kernel_size=4, stride=2, padding=1), nn.Tanh()) # zは乱数で生成するノイズ def forward(self, z): out = self.layer1(z) out = self.layer2(out) out = self.layer3(out) out = self.layer4(out) out = self.last(out) return out
zを入力としlayer1から出力層まで順に流していく実装になります。 4つのlayerを通ったあと、活性化関数Tanhを使用して-1から1の出力結果になるようにします。
Discriminator(識別モデル)
こちらの図は元論文には掲載されていなかったのですがこちらのサイトより引用したDiscriminatorの構造を示した図です。
Discriminatorは一般的に画像分類タスクに使用されるCNNと同様のものと考えてもらって構いません。
異なる点としては各layerの活性化関数ReLUの代わりにleakyReLUを使用していることです。
以下、 Discriminatorの実装です。
class Discriminator(nn.Module): def __init__(self, nz, image_size): super(Discriminator, self).__init__() # MNISTなので入力チャンネルは1 self.layer1 = nn.Sequential( nn.Conv2d(1, image_size, kernel_size=4, stride=2, padding=1), nn.LeakyReLU(0.1, inplace=True)) self.layer2 = nn.Sequential( nn.Conv2d(image_size, image_size*2, kernel_size=4, stride=2, padding=1), nn.LeakyReLU(0.1, inplace=True)) self.layer3 = nn.Sequential( nn.Conv2d(image_size*2, image_size*4, kernel_size=4, stride=2, padding=1), nn.LeakyReLU(0.1, inplace=True)) self.layer4 = nn.Sequential( nn.Conv2d(image_size*4, image_size*8, kernel_size=4, stride=2, padding=1), nn.LeakyReLU(0.1, inplace=True)) self.last = nn.Conv2d(image_size*8, 1, kernel_size=4, stride=1) # 入力xは本物画像or生成画像 def forward(self, x): out = self.layer1(x) out = self.layer2(out) out = self.layer3(out) out = self.layer4(out) out = self.last(out) return out
損失関数の解説と実装
ここではDCGANの損失関数について解説していきます。なお実装では訓練の過程やデータの受け渡しも行っておりますが、解説は損失関数のみに絞らさせていただきます。 はじめにDiscriminatorの損失関数について考えていきます。 画像を入力としたときで表せます。また正しいラベルはGが生成した画像を、訓練データセット画像をと定義します。 そうした場合Discriminatorの出力は
で表せます。実際のDiscriminatorの出力はバッチサイズ個分の同時確率となるので、これの対数をとって
この式を最大化するように最適化することがDiscriminatorの学習です。 ちなみにこの式を具体的に考えたとき、Discriminatorの学習とは のラベルを持つ本物画像を入力したとき、を最大化することであり、 のラベルを持つ偽物画像を入力したとき、を最大化することです。 これは本物画像を与えたときは1(本物ラベルと同じ)を出力し、偽物画像を入力したときは0(偽物ラベルと同じ)を出力するように学習することを示しており、直感的にもわかりやすくDiscriminatorの学習を示しています。
次にGeneratorの損失関数について考えていきます。 GeneratorはDiscriminatorをだましたいのでDiscriminatorが最大化しようとしている式を最小化すればよいです。 つまりDiscriminatorが最大化しようとしていた以下の式、
この式を最小化することがGeneratorの学習となります。
Generatorの損失関数を考える際は入力はGeneratorが生成した画像のみで良いので、の時を考えます。 このとき
となります。ただしこの式はGeneratorの学習が進みずらいことが分かっています。そこで要はが1と判定してくれればよいだろうと考えてDCGANではGの損失関数を
としています。
両者の損失関数は2値の交差エントロピーを示しているため、PyTorchの実装ではnn.BCEWithLoss()を使用することで簡単に記述できます。 以下損失関数を含めた訓練課程の実装です。 なおここではDatasetやDataloaderの定義の実装コードは掲載しておらず、あくまで訓練の流れを理解するために訓練課程のコードを載せています。
def train_model(G, D, dataloader, num_epochs, nz, mini_batch_size, device): # 最適化手法の設定 G_lr, D_lr = 0.0001, 0.0004 beta1, beta2 = 0.0, 0.9 optimizerG = torch.optim.Adam(G.parameters(), G_lr, [beta1, beta2]) optimizerD = torch.optim.Adam(D.parameters(), D_lr, [beta1, beta2]) # 誤差関数の定義 criterion = nn.BCEWithLogitsLoss(reduction="mean") # ネットワークをGPUへ G.to(device) D.to(device) # 訓練モードへ切り替え G.train() D.train() num_train_imgs = len(dataloader.dataset) batch_size = dataloader.batch_size iteration = 1 # 各epochでの損失を記録 G_losses = [] D_losses = [] print("Start training!") for epoch in tqdm(range(num_epochs)): for data in dataloader: # -------------------- # 1. Update D network # -------------------- # ミニバッチが1だとBatchNormでエラーが出るので回避 if data.size()[0] == 1: continue # GPU使えるならGPUにデータを送る data = data.to(device) # ラベルの作成 mini_batch_size = data.size()[0] # smoothing label を使って学習の安定化を図る real_label = torch.full((mini_batch_size,), 0.8).to(device) fake_label = torch.full((mini_batch_size,), 0).to(device) # 真の画像を判定 D_real_output = D(data) # 偽画像を生成して判定 z = torch.randn(mini_batch_size, nz).to(device) z = z.view(z.size(0), z.size(1), 1, 1) fake_imgs = G(z) D_fake_output = D(fake_imgs) # 誤差を計算 lossD_real = criterion(D_real_output.view(-1), real_label) lossD_fake = criterion(D_fake_output.view(-1), fake_label) lossD = lossD_real + lossD_fake # 誤差逆伝播 optimizerG.zero_grad() optimizerD.zero_grad() lossD.backward() optimizerD.step() # -------------------- # 2. Update G network # -------------------- # 偽画像を生成して判定 z = torch.randn(mini_batch_size, nz).to(device) z = z.view(z.size(0), z.size(1), 1, 1) fake_imgs = G(z) D_fake_output = D(fake_imgs) # 誤差を計算 lossG = criterion(D_fake_output.view(-1), real_label) # 誤差逆伝播 optimizerG.zero_grad() optimizerD.zero_grad() lossG.backward() optimizerG.step() # ----------- # 3. 記録 # ----------- D_losses.append(lossD.item()) G_losses.append(lossG.item()) iteration += 1 # 画像生成 if epoch % 20 == 0: # 画像表示用の自作関数 generate_img(G, dataloader, epoch, batch_size=16, nz=nz, device=device) return G, D, G_losses, D_losses
流れとしてはDataloaderからデータをバッチサイズごとに読み込みDiscriminatorの学習、Generatorの学習を行い、これを200epoch繰り返します。
画像は64px四方の0~2のMNISTの画像を各クラス200枚ずつ用意しています。最適化手法はAdamです。
訓練過程の実装で分かりにくいのはGeneratorの学習部分の損失関数の定義のところです。 Generatorの損失関数を先程のように少し変形しているためコードに起こす際はこのように記述します。
lossG = criterion(D_fake_output.view(-1), real_label)
画像生成結果
それでは学習をしてみた結果を示します。 20epochごとに画像生成したものがこちらです。 画像は学習が進むにつれて綺麗に数字を表現しています。しかし数字の2を訓練データに含んでいたにも関わらず0と1しか生成されていません。 これがモード崩壊と呼ばれる現象で、簡単に生成できる0と1のみを学習してしまった過学習のような状態です。
こちらは学習の損失関数の様子をプロットしたものです。 学習が上手くいっていると2つの損失関数が競合しながら減少していきます。このバランスが崩れるとモード崩壊となって全く同じ画像しか生成しなくなったり、綺麗な画像が生成されなかったりします。今回の結果を見ると途中からGeneratorの損失が増加しているのがみて取れます。このような場合は、パラメータを調整したりGANの構造に変更を加えることでDiscriminatorを弱くする必要があります。少し昔ではありますがDCGANなどの基本的な構造のGANで学習を安定化させる方法はこちらの論文が参考になります。 arxiv.org
おわりに
本記事ではDCGANの構造の解説とPyTorchを用いて簡単に実装の説明を行いました。 実際に自分で画像生成をしてみるとGANの不安定性が明らかになったように感じます。オリジナルのGANよりも安定性が向上したDCGANにおいても, 学習の安定性への工夫は重要そうです。
【SIGNATE】 (学生限定)マイナビコンペの振り返り
今回学生限定のコンペが開催されるということで、初めてSIGNATEに参加しました。
結果から言うと300人中90位と思うような結果は得られませんでしたが、初めてテーブルコンペにまともに取り組んでみて、学びが非常に多かったため今回ここにまとめておこうと思います。
コンペの概要
今回のコンペの目的は東京23区内の家賃予測でした。約3万件の訓練データとテストデータが与えられており、説明変数には、住所、築年数、土地面積などを含む15個の特徴量が与えられていました。
KaggleのHouce pricesやSIGNATEの土地の販売価格の予測など比較的似た内容のコンペに関する情報が公開されていたので、これらを参考に進めていきました。
signate.jp
序盤で行ったこと
データの前処理
序盤は主にデータの前処理ばかりを行っていたような気がします。与えられているデータはほとんどが日本語で書かれた未処理のデータであったため、正規表現とpandasのモジュールを使用してデータ整形を行いました。ここでの作業でpandasの処理にだいぶ慣れることができました。個人的にはデータを可視化する作業がとても好きなので、序盤にEDAをしている時はワクワクしていたように思います。
学習モデルはとりあえずGBDT*1を使っとこうという理由でlightGBMを使用しました。
この段階ではベンチマークを僅かに超えたくらいでした。ただ既存のデータを前処理してパラメータを適当に選んだ状態で学習させた程度であるにも関わらず、それなりの精度がでるGBDTには正直驚きました。のちに知ることとなるのですがGBDTモデルでは欠損値を補完する必要がないことを知らなかったため、当初は全ての欠損値を律儀に補完していました。またデータの特徴量化に関しては当時はone-hot encodingしか知らなかったため、カテゴリ変数は全てone-hot-encodingで処理していました。
中盤で行ったこと
緯度経度のデータ取得
今回のコンペは外部データの利用も可であったので、与えられている住所から緯度と経度をgeocodingというサイトを利用して取得しました。
しかしこの作業がかなり大変で、サーバーに負担をかけないために10秒間隔でスクレイピングを行う必要がありました。テストデータも含めると約6万件だったので全てのデータを取得するのに1週間ほどかかる計算でした。しかも試してみるとうまくデータを取得できなかったり、予期せぬエラーが発生して計算が途中で止まり取得したデータが消える場面もありました。
そこでデータを1000件ずつに分けて1000件データを取得できたらcsvにデータを保存するを繰り返すという方法を取りました。試行錯誤を繰り返しながらなんとか無事に全データの取得が終わったのがコンペ終了の1週間前でした。
Kaggle本を読んでとりあえず色々試す
2019年10月9日(コンペ開催期間)に『Kaggleで勝つデータ分析の技術』が発売されました。twitter等で話題になっていた本で、u++さんの書評を見てこれは今読むべき本だと思い、購入しました。
読んでみると、テーブルデータに対してのありとあらゆる手法が網羅されている印象で非常に買ってよかった1冊でした。またコードも掲載されていたためこの本を見ながらすぐに様々な手法を試すことができました。
今回私がこの本から学んだことをいかに整理しておきます。
- target encoding, label encoding等のカテゴリ変数をencodingする手法
- 評価指標RMSEは外れ値に影響を受けやすい
- 特徴量の作成手順
- GBDTは欠損値補完や数値の変換をしなくてもいい
- k分割交差検証モデルの予測値の平均を予測値とする方法
- スタッキング
特にtarget encodingとスタッキングはリークの可能性があるためout-of-foldで行う点は非常に重要であると感じました。
特徴量の作成
データが3万件と非常に多いのに対して特徴量が15個と少ないこと、加えてGBDTはある程度意味のない特徴量の追加に対してもロバストであることから特徴量をとりあえず大量に増やす方針に変えました。具体的に作成した特徴量は
- 家賃÷面積=地価とみなし、各カテゴリ変数の水準ごとに算出した地価の平均を各物件の地価から引き算、割り算
- 面積×階数=建物の体積(建設費用と相関がありそうと仮定)
- 居住階÷最上階=建物内での相対的な位置
- 面積÷間取り(2LDKなど)から得た部屋数=1部屋あたりの面積
これらを組み合わせながら特徴量を200個ほどに増やしていったのが中盤でのハイライトです。
終盤で行ったこと
クラスタリング
取得した緯度経度のデータを用いてより細かく土地区分を行おうと思いクラスタリングを行いました。
上記の図はクラスタ数が100のもので、その他に50, 500, 1000も特徴量に加え他の特徴量と組み合わせて特徴量を計290個にまで増加しました。
スタッキング
上記のKaggle本で掲載されていたスタッキングをコンペ終了前日から取り組みました。
lightGBMで予測した値を特徴量として、線形モデル・lightGBMで再度学習を行いました。しかしここではスタッキング後の精度の方が悪くなってしまい、2層目にlightGBMを用いたモデルに関しては精度が倍以上に悪くなったため、おそらくリークしていたのだろうと思われます。
ここをごちゃごちゃ触っているうちに制限時間がきて90位でコンペを終えました。
コンペを終えての所感
正直最初は「自分上位食い込めるのでは」という謎の期待を抱いていましたが、あえなく散ってしまいました。ただ学びは非常に多く参加したことには意義があったと思います。あわよくば賞金が欲しかった。笑
またこれが初めてのブログ投稿でしたが、ただ「コンペに参加して90位でした」だけだと周りからも特に評価されないし、自分としてもやっただけで終わってしまう気がしたので、今後はこういった取り組みの過程も逐一残していこうと思いました。
[GitHub] https://github.com/kuto5046/signate
*1:Gradient Boosting Decceision Tree