The jonki

呼ばれて飛び出てじょじょじょじょーんき

ギブスサンプリング入門

前回2次元のガウス分布を条件付き確率により得られた1次元ガウス分布から推定する記事を書きましたが,今回はもっと単純な例で説明します. www.jonki.net

というのもGraham先生のチュートリアル資料(トピックモデル)にかなり分かりやすい例があったので,それを利用させていただいて説明します.チュートリアル資料の例題と対応しているので,それと合わせて本記事を見てみてください.

単純なサンプリング

まずサンプルする,ということはどういうことなのか,を説明します.これは何らかの確率分布から,その確率に応じてサンプル(標本)を抽出することを意味します.例えばある確率分布にはA, B, Cの3つの値がそれぞれ0.5, 0.3, 0.2で生起するような分布を考えます.そのような確率分布から1つののサンプルを得るには下記のようなコードになります.

import random

def sampleOne(probs):
    z = sum(probs.values())
    remaining = random.uniform(0, z)
    for k, v in probs.items():
        remaining -= v
        if remaining <= 0:
            return k

def main():
    probs = {
        'A': 0.5,
        'B': 0.3,
        'C': 0.2
    }

    N = 10000
    samples = {k: 0 for k in probs.keys()}
    for _ in range(N):
        sample = sampleOne(probs)
        # print('Try sample:', sample)
        samples[sample] += 1

    print('Result:')
    for k, v in samples.items():
        print('{}: {:.3f} ({}/{})'.format(k, v/N, v, N))

if __name__ == '__main__':
    main()

プログラムでは1万回サンプルを行い,A, B, Cがどれぐらいの確率で生起したか見てみましょう.だいたい元の分布の値と等しいですね.

$ python sample.py
Result:
A: 0.500 (5005/10000)
B: 0.302 (3016/10000)
C: 0.198 (1979/10000)

ちなみに上記コードのNを100に変えて,100回サンプルのときの例を見てみます.そうすると,先程よりも少しずれた値になってしまいました.このようにサンプリングでは基本的にサンプルする回数を多くすればするほど,元の分布に近しい値を獲得することができます.

Result:
A: 0.460 (46/100)
B: 0.300 (30/100)
C: 0.240 (24/100)

ギブスサンプリング

では,次にギブスサンプリングの説明をします.ギブスサンプリングでは,解析的に解けない同時確率を,条件付き確率に分解し,サンプリングを行うことで,元の確率(同時確率)を近似的に得ようというものです.その際に,各々の確率変量は同時には扱えないので,1つ以外を残して固定することで(観測値とする),サンプリングを行うものです.

例題として,Graham先生の資料を利用します.この問題では,親A(父or母)と子B(息子or娘)が買い物をしている状況を考え,それぞれの同時確率(例えばP(母,娘))などをギブスサンプリングで求めます.Givenの情報として,AとBの条件付き確率は与えられているところからスタートしています.この例では同時確率は,解析的に計算できてしまう(後述)のですが,まずはギブスサンプリングで得てみましょう.

今回はAとBの2変量なので,Bを固定した状態でAをサンプル(A'〜P(A|B)),Aを固定した状態でBをサンプル(B'〜P(B|A)を交互に行います(〜は確率分布からのサンプルを表します).そしてサンプルしたA'とB'が同時に買い物をしていた,としてカウントをしていき,各ケースの頻度を計算することで,擬似的に同時確率を求めます.このとき,AとBの初期値が必要になりますが,適当で良いです.

import random

# given probabilities
probs = {
    'mom|dag': 5/6,
    'fat|dag': 1/6,

    'mom|son': 5/8,
    'fat|son': 3/8,

    'dag|mom': 2/3,
    'son|mom': 1/3,

    'dag|fat': 2/5,
    'son|fat': 3/5
}

def sampleOne(probs):
    z = sum(probs.values())
    remaining = random.uniform(0, z)
    for k, v in probs.items():
        remaining -= v
        if remaining <= 0:
            return k

def gibbsSampling():
    samples = {
        'mom,dag': 0,
        'mom,son': 0,
        'fat,dag': 0,
        'fat,son': 0,
    }

    # initial values
    A = 'mom'
    B = 'dag'

    # sampling count
    N = 10000

    for _ in range(N):
        # Bを固定でAをサンプル
        _A = sampleOne({k:v for k, v in probs.items() if k.endswith(B)}).split('|')[0]
        # Aを固定でBをサンプル
        _B = sampleOne({k:v for k, v in probs.items() if k.endswith(A)}).split('|')[0]
        samples['{},{}'.format(_A, _B)] += 1

    print('Result:')
    for k, v in samples.items():
        print('P({}) = {:.3f} ({}/{})'.format(k, v/N, v, N))

if __name__ == '__main__':
    gibbsSampling()

これを実行すると,下記のようになりました.どうやらA=母,B=娘である確率が大きそうですね.

$ python gibbs_sampling.py
Result:
P(mom,dag) = 0.550 (5503/10000)
P(mom,son) = 0.278 (2776/10000)
P(fat,dag) = 0.111 (1107/10000)
P(fat,son) = 0.061 (614/10000)

ところでこの同時確率はギブスサンプリングを使わなくても解析的に解けるので,先程の事例の答え合わせができます.ベイズの定理より,下記が得られます.

P(母, 娘) = P(母|娘)P(娘)

P(娘)は明に与えられていませんが,娘と息子が選ばれる内,娘が選ばれる確率であるので,\frac{P(娘|母)+P(娘|父)}{P(娘|母)+P(娘|父)+P(息子|母)+P(息子|父)}=\frac{2/3+2/5}{2}=\frac{8}{15} \fallingdotseq 0.533で先程求めた値(0.55)と近しい値であり,正しくギブスサンプリングが出来ていそうです.他の同時確率についても同様に計算ができます.

まとめ

サンプリング及びギブスサンプリングについて,Graham先生の資料を元に説明してみました.条件付き確率にうまく分解し,ギブスサンプリングを行うことで,元の確率に近しいものを獲得できることを,実例で見てみました.名前は仰々しいですが,意外と単純なしくみですよね.

今回のプログラムは下記にまとめています.

github.com

2次元ガウス分布をギブスサンプリングする

2019/5/3 更新

ギブスサンプリングの簡単な例題を説明した記事を書いたので,こちらを先に見たほうが理解が進むかもしれません. www.jonki.net

ベイズ推論を勉強中に,サンプリングの1つ,ギブスサンプリングが出てきてよく分からず色々調べていたのだけど,ようやく少しわかったので,数式展開及びPythonのコードをかなり丁寧に書いてみた. はてなブログで数式を書こうと思ったけど,さすがにつらすぎたのでTeXで書いてみましたので,下記のリンクからPDFを見てみてください.

PDFには下記を含みます.

github.com

月次目標のおすすめ

はじめに

今回は大した記事じゃないんですが,月単位の目標を予め月初以前に作っていたらなかなか良かったという話です. もともとはRebuild.fmによく出てるhigeponさんが,何らかの目標を毎月立てて,あとで振り返りをしてるという話を聞いたことからなんですが,ちょうどやろうとしていたことがいつまで経っても消化されないなぁ,と思ってもいたのでやってみました.

月次目標の立て方

仕事にしろプライベートにしろなんでも良いと思うのですが,箇条書きにさっと書いてみます.月が始まる前に書くのが良いですが,月の途中でもやりたいことなどが出てきた場合は更新していました.ちなみに私の場合は,月の目標は仕事とプライベートで分けて管理しています.個人的にはGoogle Keepがおすすめです.Google Keepは他のノートアプリと比べて,整理する,ノートブックを作る,といった整頓作業がないので気持ちがだいぶ楽です.

3月の目標振り返り

私の立てた3月の目標は下記でした.打ち消し線は完了項目です.

内容的にはこんな感じでした. もともとはダラダラと読んでいた形態素解析本を早く読み切りたいなぁ,と思ったのがモチベーションとして大きかったので,個人的には満足行く結果です.だいたい80%ぐらい体感的に完了しました.

月次目標のメリット

やっぱり目標として明記されるので,その月の予定を管理しやすいですし,達成したかどうかが後から振り返って判断できるのでとてもおすすめです.実は1月からやっていたのですが,このときは目標を詰め込みすぎて,1つも達成できなかった,ということもありました.なんやかんやで平日働いているわけですし,それなりに現実的な項目に落とすのもテクニックかなと思います.

これを続けていけば,年末などに自分のやってきたことを振り返れますし,今年になって何を習得したか?といったことも定性的でなく定量的にある程度見れて非常に良いです.

よく年始に「〇〇をする!」みたいな目標を立てたりしますが,年だと正直単位が大きすぎるので,うまくブレークダウンして,スケジューリングしないと,結局立てた目標すら忘れてしまう事態になりがちです.その意味では月という期間はモチベーションや記憶の維持にちょうどよい期間なのではないかと思います.

毎日目標もおすすめ

これは余談ですが,毎日のやることも実はGoogle Keepに箇条書きで前日に書いておくと,明らかにその日のパフォーマンスは良いです.今日最低限これやろう!というぐらいで書いておいて,それが終わったらさっさと退社する,という風にしたところ残業も減りましたし,プライベートな時間で色々開発やらゲームやら出来て,両面で充実して良いです. おしりを決めて,作業するという事を大事に今年は過ごしていこうかなと思います.

まとめ

とまぁ,なんだか意識が高そうな人の記事になってしまったのですが,実はこれは逆でこのようにシステマティックにやらないと何もやらない性格なので,こういう風なやり方を実践してみました,という報告記事になりました. 明日から4月ですし,ぜひ皆さんも月次目標立ててみてはいかがでしょうか?

2018年に買ってよかったもの(日用品とガジェット)

今年も色々なことがあり,いろいろ言い訳を立てて色々買いました.今年買ってよかったものを振り返ります.

日用品

  • 折りたたみ傘

90gとアホみたいに軽いです.2回ほど使いましたが問題なく使えます.常に持ち歩きもできるレベルの軽さなので重宝しています.

  • TRUSCO工具箱

Youtubeで知ったこの箱.工具や筆記用具など様々な物が入れられます.ただの箱なんですが,無骨なデザインがグッとくる,素敵な商品でした.

  • 昼寝枕

会社で昼寝とかするときのために買ったものです.手が痺れないので,ぐっすりと寝れます(!?)

カッターでいいじゃん,と思うかもしれませんが,適度な切れ味のカッターなので,ガムテープの裁断にぴったりで,中の商品を傷つける心配も少ないです.使っていてとても気持ちが良いです.ネットショッピングたくさんする人は必須.

  • デカビタ

湯船に浸かりながらほぼ毎日飲んでます.適量なので最高.

サントリー デカビタC 160g×30本

サントリー デカビタC 160g×30本

ガジェット

  • 電源タップ+USB

出張/旅行用に購入.今まで電源タップとUSB-ACアダプタは個別に持っていましたが,正直これだけで済むようになりました.やや電源ケーブルが短いですが,軽くてコンパクトなので邪魔になりません.

写真のとおりです.Google Home miniの置き場所に困っていたのですが,これだとケーブルも露出せず見た目もスッキリ.音声認識も問題なくできて便利です.Echo dotでも類似商品を使っています.

  • 赤外線コントローラ

安価な赤外線のユニバーサルコントローラです.Google Home/Alexaでの声の操作やスマホからアプリ操作もできます.複数購入して部屋ごとに細かい設定もできて便利です.

  • ディスプレイ

USB C経由でMacbook proに接続でき,映像出力と給電ができて非常にスッキリして便利です.ディスプレイにUSBのハブ機能も持っているので,周辺機器も接続できて更に便利です.

初代がボロボロになって買い替え.毎日使ってます.冬は耳が寒くないので防寒グッズにもなります.

  • スタンディングデスク

腰痛持ちなので購入.意外と安いです.机もしっかりしていて広々快適に使えます.高さ調節もメモリ機能があり,気軽にできます.

  • aston origin

backspace fm御用達のマイク.高いですが,ポッドキャスト配信には最適なマイクの1つだと思います.

Macbook proをUSB Cケーブル一本で,映像出力,給電,USBハブ,e-SATAなど色々つなげるハブです.高いですが,しっかりと給電でき,熱問題もありません.ただEIZOのディスプレイを買ったので私の中ではオワコンに..

  • iPad Pro 2018 12.9 inch

軽い作業をしたいとき,簡単なメモを取りたい時,旅行先,などで活躍してくれてます.ディスプレイが大きくなったので論文もかなり読みやすいです.

Apple iPad Pro (12.9インチ, Wi-Fi, 512GB) - スペースグレイ

Apple iPad Pro (12.9インチ, Wi-Fi, 512GB) - スペースグレイ

まとめ

2018年,買ってよかったものをまとめてみました.今年前半は米国にいたためAmazon.comでたくさん買い物もできて楽しかったです.

来年もどうぞよろしくおねがいします!

arXiv論文のためのChrome拡張を作った

皆さんはarXivの論文を読んでまとめようとするとき,どうやってその論文情報をEvernoteなりOneNoteなりに書いていますか?私はいちいちコピペしていたのですが,arXivは更新頻度が高いので面倒だなと思い,Chromeの拡張を作って単純作業は自動化しました.

作ったのは2つ.1つずつ概要を説明していきます.もし興味あればChrome拡張のコード説明も後半に載せます.

  1. 論文の主要情報をテキストとしてコピーするもの(arXiv clip

  2. 論文の主要情報を所定のGitbhubにissueとしてポストするもの(arxiv2issue

arXiv clip

下記のような特定の論文ページを開いた状態で,arXiv clipのアイコンをクリックすることで,論文情報(タイトル,著者,コメント部(学会情報が書かれること多い,未記入の場合はスキップ),URL)が下記のようにコピーされます.論文を共有したい時とかに便利です. f:id:jonki:20181228111051j:plain

Learning End-to-End Goal-Oriented Dialog
Antoine Bordes, Y-Lan Boureau, Jason Weston    
Accepted as a conference paper at ICLR 2017
https://arxiv.org/abs/1605.07683

arxiv2issue

これは先程の応用編のようなものです.私は論文をGithubのissueにまとめています.気になった論文をこれまではarXiv clipでコピーした情報を,いちいちIssue生成→貼り付け,とやっていたのですが,面倒なのでissueの生成も自動化するようにしました.こちらも先程と同様に所定のarXivの論文のページでarxiv2issueのアイコンをクリックするだけです.issueに投稿するにあたって,GithubのPersonal Access Tokenというものを使用しています.arxiv2issueのアイコンを右クリックして,生成したトークン,Githubユーザー名,リポジトリ名をセットしておくことで,好きなリポジトリでissueを生成することができます.

Personal Access Tokenの取得方法などは,下記のREADMEを参考にしてみてください. github.com

f:id:jonki:20181228111723p:plain
投稿イメージ

以下コード解説をしてみますが,Chrome拡張に興味がある人は見てみてください.私自身,本機能のためだけにChrome拡張を作ったのでちゃんと勉強していません.下手な書き方など大いにあると思いますがご容赦ください.

コード解説

Chromeの拡張作成の細かいところは,公式のドキュメントに譲り,ここでは機能のコア部分について説明します.

まずarxiv-clipは下記のような構成になっています.ソースコードはこちら

$ tree
.
├── background.js    // 所定のページ(今回はarxiv)を現在のタブが開いているかチェックし,開いていればアイコンを有効化
├── jquery-3.3.1.slim.min.js    // HTMLのパース用に追加
├── manifest.json   // Chrome拡張の宣言ファイルのようなもの
├── popup.html   // アイコンを押したときに表示されるGUI.
├── popup.js  // 下記に詳述

もっとも重要なpopup.jsのコードを貼ります.やっている内容は,カレントタブのHTMLを読み込んでjQueryに流し込み,欲しい情報を取得.取得した論文情報をcopyToClipboard関数を使ってクリップボードにコピーしています.このコピー部分はトリッキーなことをやっていますが,ブラックボックスな関数として使って良いと思います.

ARXIV_URL = 'https://arxiv.org/*';

function getCurrentTabUrl(callback) {
    var queryInfo = {
        url: ARXIV_URL,
        active: true,
        currentWindow: true
    };

    chrome.tabs.query(queryInfo, (tabs) => {
        if (tabs.length > 0) {
            var tab = tabs[0];
            var url = tab.url;
            console.assert(typeof url == 'string', 'tab.url should be a string');
            callback(url);
        } else {
            $('#result').text('not arXiv!');
        }
    });
}

function modifyDOM() {
    return document.body.innerHTML;
}

function copyToClipboard(text) {
    const input = document.createElement('textarea');
    input.style.position = 'fixed';
    //input.style.opacity = 0;
    input.value = text;
    document.body.appendChild(input);
    input.select();
    document.execCommand('Copy');
    document.body.removeChild(input);
};

document.addEventListener('DOMContentLoaded', () => {
    getCurrentTabUrl((url) => {
        chrome.tabs.executeScript({
            code: '(' + modifyDOM + ')();' //argument here is a string but function.toString() returns function's code
        }, (results) => {
            var $dom = $($.parseHTML(results[0]));
            title = $dom.find('h1.title').text().split('Title:')[1];
            authors = $dom.find('div.authors').text().split('Authors:')[1];
            authors = authors.replace(/\n/g, '');
            comment = $dom.find('div.metatable').find('.comments').text();
            if (comment != '') {
                info = [title, authors, comment, url].join('\n');
            } else {
                info = [title, authors, url].join('\n');
            }

            copyToClipboard(info);
            $('#result').text('copied!');

            // hide popup automatically
            setTimeout(function () {
                window.close();
            }, 3000);
        });
    });
});

次にarxiv2issueの説明に入ります.コードはこちら.こちらの拡張はarxiv clipを拡張する形で作ります.追加で必要な機能として,トークンやリポジトリ情報を保存する機構とissueにポストする機能の2つを説明します.

情報を保存するためOptionsを呼ばれるページを作ります.アイコンを右クリックしたときに現れる設定ページのようなものです.基本的にはチュートリアルの例をコピペして,自分用にカスタムしただけです. Give Users Options - Google Chrome

情報の保存には,chrome.storage.syncを利用します.これを利用するとChromeで同期しているアカウント内ではデータが共有され,複数台のマシンを持っている人はイチイチ設定し直さなくてよいので便利です.下記のoptions.html/jsでは,3つのテキスト情報をchromeに保存しています.

<!DOCTYPE html>
<html>
    <head><title>arxiv2issue Options</title></head>
    <body>

        Github User Name:
        <p><input id="user-name"></p>
        Github Repository Name:
        <p><input id="repo"></p>
        Github Personal Access Token (<a href="https://github.com/settings/tokens/new">Get your token.</a> "repo" option must be checked for this extension.):
        <p><input id="token"></p>

        <div id="status"></div>
        <button id="save">Save</button>

        <script src="options.js"></script>
    </body>
</html>
// Saves options to chrome.storage
function save_options() {
    var uname = document.getElementById('user-name').value;
    var repo  = document.getElementById('repo').value;
    var token = document.getElementById('token').value;
    chrome.storage.sync.set({
        uname : uname,
        repo  : repo,
        token : token
    }, function() {
        var status = document.getElementById('status');
        status.textContent = 'Options saved.';
        setTimeout(function() {
            status.textContent = '';
        }, 750);
    });
}

// Restores values
function restore_options() {
    chrome.storage.sync.get({
        uname: '',
        repo: '',
        token: ''
    }, function(items) {
        document.getElementById('user-name').value = items.uname;
        document.getElementById('repo').value = items.repo;
        document.getElementById('token').value = items.token;
    });
}
document.addEventListener('DOMContentLoaded', restore_options);
document.getElementById('save').addEventListener('click', save_options);

f:id:jonki:20181228120401p:plain
設定画面

そしてissueにポストします.先程のpopup.jsを改変するだけで良いです.issue作成のために,Githubのv3 APIを使っています.非常に簡単で,先程のトークンをGETパラメタにセットしているだけです.注意としては,Acceptヘッダにjsonを入れるようにする必要があります.issueはJSON文字列として作り,タイトルや本文などの設定ができます.v3 APIのページにセットできるパラメタリストが載っています.私はtitleとbodyのみを設定して,ポストしています.

// 略
function create_issue(title, body, year, callback) {
    var base_url = 'https://api.github.com/repos';
    chrome.storage.sync.get(['uname', 'repo', 'token'], function(data) {
        var uname = data.uname;
        var repo = data.repo;
        var token = data.token;
        var url = [base_url, uname, repo, 'issues'].join('/');

        var url = url += '?access_token=' + token;
        console.log('URL: ' + url);

        var data = JSON.stringify({
            'title': ['🚧', year + ':', title].join(' '),
            'body': body
        });
        var request = new XMLHttpRequest();
        request.open('POST', url);
        request.onreadystatechange = function () {
            if (request.readyState != 4) {
            } else if (request.status != 201) {
                console.log(request.responseText);
                callback('Failed to post an issue.');
            } else {
                var resp = JSON.parse(request.responseText);
                callback('Issue posted!: #' + resp.number);
            }
        };
        request.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
        request.setRequestHeader('Accept', 'application/vnd.github.symmetra-preview+json');
        request.send(data);
    });
}
// 略

まとめ

今回はarXivの論文をまとめるための簡単なツールを紹介しました.arXivは更新頻度が多いので見るのが大変です.こういったツールを活かして,手作業コピペなどの余計な作業時間は極力減らして,うまく効率的に論文を読みたいものですね.