3.3 ベイズ線形回帰
昨日は線形基底関数モデルを用いて回帰を行った。
telltale.hatenablog.com
今日は同じ回帰問題をベイズ的アプローチで解くベイズ線形回帰について勉強する。
ベイズ線形回帰
ベイズ的なアプローチの最終目標は、を観測した条件の下でのの分布を得ることだ。
そのために、の事前分布と、尤度関数を求め、ベイズの定理を用いてを評価するとよいことは過去に学んだ通りだ。
例のために、事前分布と尤度関数がともにガウス分布で与えられる場合を考える。
尤度関数については昨日出てきたものと同じものを使用している。
事後分布は2つの分布の積を正規化したものであり、共役なガウス事前分布を用いているため事後分布もガウス分布になる。
よって、積をガウス型に変形し平均と共分散行列を整理すると、
が得られる。
この事後分布の対数を取ること、
が得られる。
第一項は二乗和誤差関数を、第二項は正則化項を意味している。
線形基底関数モデルのときは二乗和誤差関数のみでを評価していた。
そのため、過学習の発生が問題となっていた。
一方で、ベイズの枠組みでは正規化項についても評価する。
これは1.1節で述べられていた、誤差関数に正規化項を付与することで過学習を回避するという方法に対応している。
3.1 線形基底関数モデル
PRML上巻の主題は、代表的な学習問題である「回帰」と「識別」である。
このうち「回帰」問題の目標は、ある観測値とそれに対する目標値からなる訓練集合が与えられたとき、新たな入力に対する未知の目標値を予測することである。
例えば、
1、4、5、2
という入力に対して、
1、16、25、4
という出力からなる集合を与えられたとき、次の入力「3」に対する出力を予測することなどが挙げられる。
このことは、暗にこの問題の裏に存在する関数を見つけ出すことを意味している。
実際、回帰問題の最も簡単なアプローチは、新しい入力変数に対応する値が予測変数の予測値となるような適当な関数を直接構成することである。
この問題をより確率論的に解くならば、予測分布をモデル化するベイズ的なアプローチが利用できる。
2つのアプローチについては、それぞれ3.1節と3.3節で詳しく解説されている。
今日は前者のアプローチ、線形基底関数モデルについて学習する。
線形基底関数モデル
線形基底関数モデルでは、予測のための関数を、
とモデル化する。
は基底関数と呼ばれ、文字通り関数の基底、すなわち表現を決定するものである。
基底関数の例としては、
- 多項式基底
- ガウス基底
- シグモイド基底
などがある。
当然、回帰のためには基底関数をどのように推定するかも重要となってくるが、本節を含めしばらくは基底関数は与えられているものとし、基底関数を倍するパラメータベクトルを推定することを目標とする。
演算子オーバーロードとfriend関数
行列の計算を今までEigenにやらせてたんだけど、やっぱり自分で作るかーと思ったのでここ数日行列クラスを実装する作業に明け暮れてた。
こんな感じ。
// TEMPLATE CLASS Matrix template <class Ty_> class Matrix { // define my matrix class std::vector<std::vector<Ty_>> mat_; public: explicit Matrix(const int rows = 0, const int cols = 0) : mat_(rows, std::vector<Ty_>(cols, 0)) { // construct from parameters } Matrix(const std::initializer_list<std::initializer_list<Ty_>> right) { // construct from initializer for (auto iter = right.begin(); iter < right.end(); iter++) this->mat_.push_back(*iter); } int rows(void) const { // get row size return this->mat_.size(); } // ...... }
これがすごい勉強になる。
やっぱり汎用的なライブラリを1から作るのは大変だけど、色々テクニックを学べるから、本気でC++やるなら一度はやってみるといいと思う。
特にfriend関数とかは普通にコーディングしてるだけではなかなか出会えないけど、ここに来て多用せざるを得なくなり、改めて勉強することになった。
というわけで今日は演算子オーバーロードとfriend関数ついて。
演算子オーバーロード
こういう算術系のクラスを作る場合、コードの半分ぐらいは演算子オーバーロードで埋め尽くされる。
例えば任意の型の変数を1つ保持し、それについての処理を記述するScalarクラスがあったとする。
これにScalar+変数の演算をオーバーロードする。
template <class Ty_> class Scalar { Ty_ var_; public: Scalar(const Ty_ var) : var_(var) {} Ty_ operator+(const Ty_ right) const { return var_ + right; } };
こんな感じで、object+何かという演算をしたい場合は、operator+をオーバーロードして、引数に何かの方を与えればよい。
しかし、このやり方では何か+objectという演算を定義できない。
そもそもメンバ関数による演算子オーバーロードは必ず左辺に自身のオブジェクトがくる必要があるため、メンバ変数としてこれを実装することはできない。
方法として、クラスの外に
template <class Ty_> Ty_ operator+(const Ty_ left, Scalar<Ty_> right);
を定義するというのが最初に思い浮かぶけど、var_は非公開メンバであるため、この関数内でvar_を直接参照することができず、別途var_のゲッタ等を準備する必要がある。
というわけで察しはつくだろうけど、こういう時にfriendを使いましょうという話。
template <class Ty_> class Scalar { Ty_ var_; public: Scalar(const Ty_ var) : var_(var) {} Ty_ operator+(const Ty_ right) const { return var_ + right; } friend Ty_ operator+(const Ty_ left, const Scalar<Ty_> right) { return left + right.var_; } };
これでOK。
ちなみにfriendはその性質上、宣言だけクラス内でして実装はクラス外でやるのが普通だけど、
クラスとの論理的な関わりの強い演算子オーバーロードの場合、見た目にも綺麗だし、inline化もされるから実装もクラス内でやったほうが良さそう。
C++/CLIでarrayが使えねーと思ったらstd::arrayはダメだったという話
タイトルで内容完結してるけどつまりそういうこと。(手抜き)
久しぶりにC++/CLIをさわる機会があったときに、Point型の配列を作りたかったから
array<Point>^ points = gcnew array<Point>(N);
みたいなコードを書いたんだけどどうもコンパイルが通らない。
で、調べてみても全く同じコードしかヒットしなくて頭抱えてたんだけど、どうやらこのarrayコンテナはstd::arrayで、cliは他にcli::arrayがあるらしく、Pointみたいな.NETの構造体やクラスを格納する場合はこっちを使わないとダメらしい。
というわけで
cli::array<Point>^ points = gcnew cli::array<Point>(N);
でめでたく動いた。
C++/CLIはstd::vectorとかも使えないから大変不便だし、何が使えて何が使えないのかの基準もよくわからんから慣れるまでまだまだ掛かりそうだ。あんまりコイツに慣れたいとも思わないが。
そのうちそこらをまとめたいけど、最近どうにも時間がなく寝不足なのでこの辺で。
C++の仮想関数を復習する
前回も登場した同僚のC#erと仮想関数の使い方について話していたとき、主にvirtualの意味についてこんがらがってきたので復習することにした。
このあたりはC++を勉強する初学者が最も躓きやすいところだよなぁとつくづく思う。
さて、これから女の子の名前を保持し、helloメソッドを呼ぶと自己紹介をしてくれるクラスを作ろうと思う。
メンバ変数は名前だけでいいよね。初期化はコンストラクタで行えばいいか。
あとは自己紹介するメソッドを作って、とやるとこんな感じになる。
class Girl { // 女の子の基底クラス protected: std::string name_; public: Girl(const std::string name) : name_(name) {} void hello(void) { // ごあいさつ std::cout << "私の名前は " + name_ + " だよ!" << std::endl; } };
うむうむ綺麗。
しかし、すべての女の子がこのような元気っ子とは限らない。
お嬢様だったらもっとおしとやかに挨拶するだろうし、幼女ならもっと舌足らずなはずだ。
しかし、だからと言ってこんなクラスを改めて作るのはナンセンスだ。
class Ojousama { // お嬢様クラス protected: std::string name_; public: Ojousama(const std::string name) : name_(name) {} void hello(void) { // ごあいさつ std::cout << "わたくしの名前は " + name_ + " ですのよ?" << std::endl; } };
確かにこれでも動作はするが、Girlクラスをコピーしてきてhelloメソッドの中を書き換えただけだ。
しかもGirlとOjousamaは、name_やその他今後増やしていく関数など共通して利用できるところがあるはずなのに互いに関連が無いため、拡張性にも乏しい。
そもそもプログラマーは可能な限りコーディングをサボりたいから、helloメソッドを再定義するだけでGirlクラスと同様の働きができるのならば嬉しいよねという考えに行き着く。
そこでオブジェクト指向プログラミングに用意されている継承を用いて、GirlクラスとOjousamaクラスをこのように書き換える。ついでにYoujoクラスも作ってあげよう。
class Girl { // 女の子の基底クラス protected: std::string name_; public: Girl(const std::string name) : name_(name) {} virtual void hello(void) { // ごあいさつ std::cout << "私の名前は " + name_ + " だよ!" << std::endl; } }; class Ojousama : public Girl { // お嬢様クラス public: Ojousama(const std::string name) : Girl(name) {} void hello(void) { // ごあいさつ std::cout << "わたくしの名前は " + name_ + " ですのよ?" << std::endl; } }; class Youjo : public Girl { // 幼女クラス public: Youjo(const std::string name) : Girl(name) {} void hello(void) { // ごあいさつ std::cout << "ふぇぇ...わたしのなまえは " + name_ + " だよぉ..." << std::endl; } };
変化したところは、Girl::helloにvirtualキーワードが追加されたことと、Ojousamaクラス及びYoujoクラスがGirlクラスを継承しコンストラクタもそれ用に書き換えたことのみ。
このようにvirtualをつけた基底クラスの関数を仮想関数と呼び、仮想関数を派生クラスで再定義することをオーバーライドと呼ぶ。
これを実行してみる。
Girl girl("夜ノ森小紅"); Ojousama ojousama("夜ノ森紅緒"); Youjo youjo("三峰真白"); girl.hello(); ojousama.hello(); youjo.hello();
ちゃんと自己紹介してくれた!!
これで、継承をうまく使ってGirlクラスを継承したお嬢様クラスと幼女クラスが出来ましたーちゃんちゃん。
……と、もちろんこれでおしまいではない。というか本題に入ってすらいない。
実はこれだけで終わるなら、Girlクラスに書いたvirtualキーワードは必要ない。
仮にvirtualがなかったとしても、さっきの結果と全く同じ結果が出力されるはずだ。
virtualキーワードをつけた仮想関数が真の力を発揮するのは、ポインタを介してhelloメソッドを呼び出したときにある。
しかもただポインタを使ったときではなく、基底クラス型のポインタを用いて派生クラスで再定義した関数を呼び出した際に効果が出る。
なんのこっちゃという感じだし、とりあえずその状況を再現したコードを見てみる。
int main(int argc, char const *argv[]) { Girl* girl[3]; girl[0] = new Girl("夜ノ森小紅"); girl[1] = new Ojousama("夜ノ森紅緒"); girl[2] = new Youjo("三峰真白"); for (int i = 0; i < 3; i++) { girl[i]->hello(); delete girl[i]; } return EXIT_SUCCESS; }
これを実行すると、さっきと同じ結果が出力される。
しかしプログラムをよく見ると、Girl型のポインタにOjousama型のインスタンスやYoujo型のインスタンスを代入していたり、Girl型であるポインタを使ってhelloメソッドにアクセスしているにも関わらず、出力結果が様々なクラスのhelloメソッドによるものになっていたりと、一見して不可解な点が多い。
今日はこれについて勉強してみる。
さて、このプログラムではGirl型のポインタを、配列を使って3つ用意している。
そのうち1つにはGirl型のインスタンスを、1つにはOjousama型のインスタンスを、そして最後の1つにはYoujo型のインスタンスを代入している。
これをはじめに見た時は大変奇妙に思えると思う。
感覚として逆なら理解出来そうだ。
つまり、Ojousama型のポインタにGirl型のインスタンスを代入するなら出来そうに思える。しかし現実としてそれはコンパイルエラーとなり不可能だ。
これの意味を感覚で理解するのは難しいと思う。
しかし、出来ることと出来ないことをはっきりさせると理解が進むのではと思う。
例えばGirl型のポインタは、Girlクラスで定義されている関数しか呼ぶことが出来ない。
なぜなら、中身はOjousamaクラスやYoujoクラスのインスタンスでも、見かけの型はGirl型であるため、Girl型である以上Girlクラスに無い関数についての知識を持ち得ないからだ。
だから仮にYoujoクラスにomorashi関数があったとしても、girl[2]はomorashi関数を呼ぶことは出来ない。
逆にGirl型で定義されているhelloメソッドにはもちろんアクセスできる。
しかし注意すべきは、helloメソッドの中身がポインタに代入されているオブジェクトの型によって変更されていることだ。
この時何が起きているかのイメージとしては、Girl型であるgirl[1]やはまずGirlクラスのhelloメソッドを呼ぼうとするが、Girl::helloについているvirtualキーワードは、この関数は継承先で再定義されているかもしれないから確認してね、再定義されているならそっちを呼んでねということを表しており、これを知ったgirl[1]は次に実際のクラスであるOjusamaクラスのメンバ関数にhelloが無いかを確認しに行き、そちらでhelloの再定義を確認したからそちらを呼んだ、と言った感じか。
だから、もしvirtualキーワードを付け忘れると、girl[1]やgirl[2]は特に何も考えずにGirlクラスのhelloを呼ぶため、
というように全部基底クラスのhelloを呼んでしまう。
まあこの辺は教科書の内容。
でもこれなにに使うの?ってところがよくわからないままだった。というか今でもよくわからない。
おそらく最大の特徴は、コンパイル時ではなく実行時にその振る舞いを変えられることだろう。
例えばこんな(糞)コード
Girl* girl[3]; for (int i = 0; i < 3; i++) { std::string name; int age; std::cout << "名前:"; std::cin >> name; std::cout << "年齢:"; std::cin >> age; if (age < 6) girl[i] = new Youjo(name); else girl[i] = new Girl(name); girl[i]->hello(); }
年齢と名前の入力に対し、年齢が6才未満であればYoujoクラスとして働き、それ以上であればGirlクラスとして働く。
同一の変数を用いつつ、実際の型や呼び出す関数を実行時に割り当てるなど、ここまで柔軟な振る舞いが可能であるところがすごいところなんだと思う。
というわけでvirtualについて復習してみた。
ちなみに実行時に呼び出す関数を変えるだけなら、関数ポインタ・関数オブジェクト・ラムダ式など他の手段はいくらでもある。
僕はこれまで主に関数ポインタで頑張ってたな。
どれが良いのかは人の価値観に依りそうだけど、役割によって厳密なクラス分割を行いたいのであれば、割りと重要な知識なのかなとも思って復習してみた。
なんか自分のなかでもきっちり整理できて、こういう基礎に立ち返るのもなかなか楽しい試みだ。
overrideキーワードをつける
先日同僚のC#erのコード見てたら、
public override void func() { // code }
こんな風にメンバ関数の定義を書いていた。
その時は、へーC#にはoverrideキーワードなんてあるんだーと思うだけだったけど、実はC++にもあるらしい。
使い方はこんな感じ。
class Base { public: virtual void virtual_func() { // code } void func() { // code } }; class Sub1 : public Base { public: void virtual_func() override // ok { // code } }; class Sub2 : public Base { public: void hoge() override // error { // code } void func() override // error { // code } };
Sub1のように、基底クラスの仮想関数をオーバーライドしている関数にoverrideキーワードを付ける。
この時Sub2のように、Baseにない関数や、仮想関数で無い関数をオーバーライドしている関数に対してoverrideをつけると、コンパイル時にエラーを知らせてくれる。
このoverrideキーワードは必須ではなく、あってもなくてもいいというのが現状。
ただオーバーライドさせることを前提とした関数にvirtualキーワードを付け忘れた場合、意図とは異なる動作をする可能性があるけどコンパイルエラーとはならないから、バグを見逃さないためにもこれからは付けておくといいなと思った。
autoキーワードとアロー演算子
昨日なんか書くネタあるかなーって、C++のことを色々調べてたらこんなコード見つけた。
auto add(const int x, const int y) -> int { return x + y; }
???
なんやこのコード……。
まあ予想はつくけど、戻り値の型を明示的にするための演算子のようだ。
autoで推論してるのにわざわざ明示する意味とは……という感じだし、
int add(const int x, const int y) { return x + y; }
との違いも全くわからなかったけど、そういえば似たような使い方をどこかで見たなとふと思った。
const auto add = [](const int x, const int y) -> int { return x + y; };
そうラムダ式の戻り値の型指定。
ここにも「-> 型名」という形式が現れてる。
そしてこれも、autoキーワードを使えば、多くの場合で省略可能である。
共通している点は、
- autoキーワードがついてる
- 関数の戻り値の型を表している
- 関数の引数を記述する括弧と関数本体の間に書かれている
かな。
1.が直接関わってそうだけど、うーん、関数の方のautoは戻り値の型を指してるのに対して、ラムダ式のはラムダ式自体の型を指してるわけだから、微妙に役割が違う気もする。
ただ、ラムダ式の方の型指定の方は、省略出来ないパターンもあるから意義自体は理解できる。
例えば次のような場合戻り値の指定部を省略できない。
const auto init_vector = [](const int x, const int y) -> std::vector<int> { return{ x, y }; };
でもなー、ラムダ式はともかく関数の方の「-> 型名」は別に省略してもいいし、どうしても戻り値の型を書きたければautoのとこを書き換えればいい。
やはりこの構文の意義が全くわからん。