go-active-learningを改良している話(Slackからのアノテーションをサポートなど)

以下の話の続きです、地味に続いています。自分が普段使うツールを改良していくのは楽しいですね。

自分で使ってみて、これは欲しいといったものを追加していってます。社内で紹介したところフィードバックをもらったので、それを踏まえてどうしていきたいか(どうしたか)も書いてみます。

Slackからのアノテーションをサポート

以前はコマンドラインでのアノテーションのみをサポートしていました。Goで書いているので、macでもwindowsでも簡単に動かせるという利点はありましたが、エンジニア以外の人にもアノテーションをしてもらおうと思うとコマンドラインはハードルが高いのも事実です。そこで、slackからもアノテーションできるようにしてみました。もちろん、事例をアノテーションする順番は能動学習で決めています。

使い方はヘルプを見てもらうとよいですが、提示されたURLを見て、pとタイプすると正例としてアノテーション、nとタイプすると負例としてアノテーションされます。リアクションのところについているheavy_plus_sign(プラスのアイコン)やheavy_minus_sign(マイナスのアイコン)は正例/負例として能動学習のモデルに反映されたということを意味しています。slackはURLを投げるとスニペット的なものを表示してくれるので、アノテーションがしやすくなる(ブラウザで開きなおさなくてよい)という効用もありました。

有効な素性の提示

能動学習を使って事例をどんどんアノテーションしていくわけですが、わけも分からずどんどん事例を提示されても「なんでその事例が選ばれてきているんだよ!」という気持ちになってきます。そこで、show-active-featuresオプションを付けて、どういう素性が有効になってその事例が選ばれたのかを表示するようにしました。

% ./go-active-learning annotate cli --input-filename INPUT_FILE --output-filename OUTPUT_FILE --open-url

学習器の出力と正解ラベルが一貫していない事例を見る

数百件以上アノテーションをしていると、「あっ、これと似たような事例、さっき反対側のラベルに付けた気がする…」ということがときどきあります。一貫していない正解データは学習器の精度を落とす原因になるにもなるので、そういう事例はさっとラベルを修正したい。しかし、そういうのをいざ探そうと思うと結構大変です。

こういうのを探す助けになるように、学習器の出力と正解ラベルが一貫していない事例を見るコマンドを作りました。つまり

  • 正解ラベルは1なのに、スコアが負の大きい値
  • 正解ラベルは-1なのに、スコアが正の大きい値

といった事例を表示するということができます。使い方はこんな感じで。不一致の大きい順に出していきます。

下の例では、技術エントリかどうかを判定するためのデータに対して適用しています。facebookがマイナス(技術エントリではない)で出ているけど、正解のラベルは1になっていて、ラベルを付け間違えているっぽい、ということが分かります。

% ./go-active-learning diagnose label-conflict --input-filename INPUT_FILE
Index   Label   Score   URL Title
0   1   -0.400  https://www.facebook.com/   ログイン (日本語)
1   -1  -1.400  http://suumo.jp/town/   SUUMOタウン
2   1   2.000   http://hakobe932.hatenablog.com/    hakobe-blog ♨
3   -1  -2.000  http://r.gnavi.co.jp/g-interview/   ぐるなび みんなのごはん
4   1   32.400  http://www.songmu.jp/riji/  おそらくはそれさえも平凡な日々
5   1   90.000  http://motemen.hatenablog.com/  詩と創作・思索のひろば

もらったフィードバック

最近、go-active-learningを社内で紹介する機会があったので、そこでもらったフィードバックも紹介しておきます。

能動学習で使っている分類器/特徴量抽出器と実際に使う分類器/特徴量抽出器のズレ

go-active-learningでは能動学習をやるために

  • 特徴量抽出にタイトル、本文のBoW
  • 学習器に平均化パーセプトロン

を使用しています。しかし、本番ではもっとリッチな特徴量抽出を使っているかもしれませんし、学習器もSVMやロジステック回帰を使っているかもしれません。こういったズレがある場合、能動学習で提示された順にアノテーションしていっても思ったように効率が上がらないということがありえます(go-active-learningでは難しい事例だと思ったけど、本番の分類器にとっては簡単な事例だった、などなど)。これを防ぐためには、能動学習で使うもろもろと本番で使うもろもろが一緒になっているのが一番ですが、そうすると本番とべったりなツールになってしまいます。色んなところで使える能動学習のツールにしたいなと思うと、完全に一致しないのは仕方ないので、やり方としては

  • 本番に使われていそうな学習器をサポートして、なるべくズレが小さくなるようにする
  • 本番で使われていそうなURLに対する特徴量抽出(キーワードなどのメタデータなどなど)をなるべくサポート、選択できるようにする

などがあるかなと思います。goで数百行程度なので、本番用と一致するように移植するのもそんなに難しくはないとは思います。

ユーザーインターフェイス

便利そうだけど、CLIのみだとエンジニア以外にはハードルが高いという声があったので、今回slackからの入力をサポートしました。tokenの取得の手間はありますが、そこだけエンジニアがやってしまえばアノテーションはslackの画面のみで完結するので、大分ハードルは下げられたのではと思っています。

深層学習による自然言語処理 (機械学習プロフェッショナルシリーズ)

深層学習による自然言語処理 (機械学習プロフェッショナルシリーズ)

劣モジュラ最大化によるエントリの推薦をやってみた

背景

半年前から機械学習に関するよさそうなエントリを提示してくれるbot(ML君)を運用しています。

大量のtweetの中から関連するエントリを人手で探す手間は省けるようになったのですが、最近別の問題が起こっています。以下の画像はある日に提示されたエントリの結果ですが、arxivの論文(しかもほぼ深層学習関連のもの)ばかりになっています…。ML君はURLが与えられたときに、それが機械学習に関連するいいエントリかどうかを判定しますが、提示したエントリの話題が重複しているなど条件は全く考慮していないので、当然と言えば当然の結果です。ML君を責めてはいけない。

上のような推薦結果は私が深層学習研究者/エンジニアなら喜ぶかもしれませんが、残念ながらそうではありません。機械学習/自然言語処理に関連する企業のニュース/githubのライブラリなど、色々なトピックについてカバーして欲しいものです。問題設定が抽出型の複数文書要約と似ている(めっちゃ大雑把に説明すると、様々なトピックをカバーしつつ、重要な文を選択するタスク)なと思ったので、要約業界で使われる手法でこの問題を解決してみることにしました。

最適化問題としての自動要約/エントリ選択

X語以内でなるべく多くのトピックをカバーする(例えば異なり語をできるだけ多く)という問題は、典型的な離散最適化問題です。可能な解を全列挙すると

  • エントリ1を解に入れる/入れない
  • エントリ2を解に入れる/入れない
  • エントリNを解に入れる/入れない

となり、愚直にやると2^N通りについて考えなければなりません(これに加えて、その解が制約を満たしているかチェックする必要があります)。エントリは50個以上はあるので、全列挙は厳しいです。このような問題について研究している最適化理論の分野では、分枝限定法やラグランジュ緩和、劣モジュラ最適化などがよく使われます。今回は劣モジュラ最適化の方法を実装してみました。理由は以下の通りです。

  • 実装が簡単
    • 貪欲法の亜種のような形になるので、すぐ書ける
  • 高速
    • 線形計画法(ILP)のソルバーを使うと解が出るまで結構時間がかかります
  • 最悪ケースが保証されている
    • 目的関数が最適解の場合と比べて(1 - 1 / e) / 2以下にはならないってやつですね
    • かなり緩い下限ではあるものの、ないよりは安心できるし、実験的にはかなり最適解に近い解を出すことが多い

劣モジュラ最適化の欠点としては、目的関数が劣モジュラ関数である必要がある、というのがありますが、自分に必要なCoverage functionは劣モジュラ関数なので大丈夫です。

劣モジュラ最適化(最大化)のアルゴリズムは、あるエントリxを解に含めたときに目的関数が最も上昇するようなxを貪欲的に制約を越えるまで追加していく、という簡単なものです。目的関数はあるエントリの部分集合をS、元々のエントリ集合をVとすると、Vに含まれるエントリとSに含まれるエントリとの類似度の和のような形で表わされます。詳しくは元論文を参照してください。学生時代に紹介エントリも書いていました。

元論文では文iと文jの類似度を計算するために、それらの文に含まれている単語のtf-idfのコサイン類似度を計算していましたが、ML君でエントリから特徴抽出したベクトルに対する重みを保持していたので、エントリiとエントリjに関連する重みベクトルのコサイン類似度を今回は使ってみました(\mathbf{f}_i \odot \mathbf{w}\mathbf{f}_j \odot \mathbf{w}のコサイン類似度)。詳しい実装は以下を参照してください。

元論文ではCoverage function以外に多様性を考慮した項も入っています。しかし、論文の実験結果を見ると、この項がなくてもそれほど悪くならないようなので、今回は実装していません。

実験結果

元々100件程度あるML君の提示結果を、劣モジュラ最大化で20件に絞ってみました。結果はこちら。

arxivの論文もありつつ、最近出た本の話題や、人工知能系のニュースも入っていて、色々なトピックがカバーできたような気がします。自分で運用しているbotなので、評価用データセットなどは特に用意していないですが、しばらくこれで運用してみようと思います。

個々のアイテムのよしあしだけではなく、一覧や集合として出力されたもののよしあしを考慮したいケースはWebエンジニアでも結構あると思うので、他にも使いどころを探ってみたいところです。

劣モジュラ最適化と機械学習 (機械学習プロフェッショナルシリーズ)

劣モジュラ最適化と機械学習 (機械学習プロフェッショナルシリーズ)

mattn/memoでさらに快適にメモを書くためにやっている設定について

公開用のテキストはブログで書くことが多いですが、公開できない話や落書きのような内容はローカルのテキストファイルに書き散らしているという人は多いと思います。私もそういう活用をしていて、今年からmattn/memoを使っています。

mattn/memoはGoで書かれていて、800行程度なのですぐに読めますし、気になる挙動があったらしゅっと直せます(自分も細かいPull Requestをいくつか送ってマージしてもらった)。特に以下の点が気にいっています。

  • Goで書かれていて、高速に立ち上がる
  • 簡単にgrepしたり(memo g)、pecoでファイル選択(memo e)ができる
  • httpサーバーが立ち上がって(memo s)、markdownがいい感じに見れる
    • テンプレートファイルをいじると自分の好きなようにできる(後述)

半年ほど使って、メモも大分書いたので、より使い勝手を増すために自分がやっている設定を書いてみようと思います(ディフォルトの設定で使っていても十分使いやすいとは思います)。

Emacsからさっと開く

memo nから新しいテキストファイルを作れますが、同じ日付けの事柄は1つのテキストファイルに日報として書いています。なので、その日の日報ファイルが3秒で開けることが大事です。EmacsからM-x memoですぐに開けるようにしておきます。

(defun memo ()
  (interactive)
  (find-file
   (concat "~/Dropbox/_posts/" (format-time-string "%Y-%m-%d") "-日報.md")))

vimの人も似たようなことやってそう。

数式の見栄えやコードのシンタックスハイライト

論文を読んだときのちょっとした数式のメモや、スニペット的にメモにコードを貼っておくということは結構あると思います。その数式が綺麗に描画されたり、スニペットがシンタックスハイライトされていると、大したことないものでもテンションが上がってきます(大事!)。mattn/memoではテンプレートファイルを自分で自由に書けるので、私は数式描画にはKaTeXを、シンタックスハイライトにはhighlight.jsを使っています。cdnから読み込んでもいいですが、ダウンロードしてローカルに置いておくと、ネットに繋らない環境でも全く問題なく使えるので、そちらがよいでしょう。特にKaTeXは描画も相当早いし、aligned環境も使えてなんだこりゃ…という感じでした。

f:id:syou6162:20170520174939p:plain

そんなに凝ってないLaTeXだったら、ほぼ変えずにいい感じにmarkdownで書ける。markdownで雑に書いておいて、LaTeXに持っていくこともできそう。

f:id:syou6162:20170520174703p:plain

tableの見た目などを整えるためにlessが書きたかったので、less.jsも使っています。

テンプレートファイルの内容

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>{{.Name}}</title>

    <link rel="stylesheet/less" type="text/css" href="/assets/style.less">
    <script src="/assets/less.js" type="text/javascript"></script>

    <link rel="stylesheet" type="text/css" href="/assets/atelier-dune-light.min.css">
    <script src="/assets/highlight.min.js" type="text/javascript"></script>

    <link rel="stylesheet" type="text/css" href="/assets/katex.min.css">
    <script src="/assets/katex.min.js" type="text/javascript"></script>

    <script src="/assets/auto-render.min.js"  type="text/javascript"></script>

    <script>
     document.addEventListener("DOMContentLoaded", () => {
       var katex_opts = {
         displayMode: true
       };

       Array.from(document.querySelectorAll("div"), elem => {
         if (elem.className === "highlight highlight-math") {
           var e = elem.querySelector("pre");
           katex.render(e.textContent, e, katex_opts);
         } else {
           hljs.highlightBlock(elem.querySelector("pre"));
         }
       });

       renderMathInElement(document.body, {
         delimiters: [
           {left: "$", right: "$", display: false},
         ]
       });
     });
    </script>
  </head>
  <body>{{.Body}}</body>
</html>

空のテキストファイルを削除するサブコマンド

mattn/memoではサブコマンドを簡単に実装することができます。私の場合、日報ファイルを開いたもののその日は力尽きて空ファイルができて放置されてしまっていたので、それらを掃除するサブコマンドを実装しました。Goの練習にちょうどいい感じで簡単に書ける。

サブコマンドのコード

package main

import (
    "fmt"
    "io/ioutil"
    "os"
    "path/filepath"
    "regexp"
    "sort"
    "strings"

    "github.com/fatih/color"
    tty "github.com/mattn/go-tty"
)

const (
    memoDir            = "/Users/yasuhisa/Dropbox/_posts"
    emptyContentRegexp = `(?s)^# 日報[\n\s]*$`
)

type File struct {
    Path    string
    Content string
}

func filterMarkdown(files []string) []string {
    var newfiles []string
    for _, file := range files {
        if strings.HasSuffix(file, ".md") {
            newfiles = append(newfiles, file)
        }
    }
    sort.Sort(sort.Reverse(sort.StringSlice(newfiles)))
    return newfiles
}

func isEmptyContent(content string) bool {
    return regexp.MustCompile(emptyContentRegexp).MatchString(content)
}

func ask(prompt string) (bool, error) {
    fmt.Print(prompt + ": ")
    t, err := tty.Open()
    if err != nil {
        return false, err
    }
    defer t.Close()
    var r rune
    for r == 0 {
        r, err = t.ReadRune()
        if err != nil {
            return false, err
        }
    }
    fmt.Println()
    return r == 'y' || r == 'Y', nil
}

func deleteFile(f File) error {
    color.Red("%s", "Will delete the following entry. Are you sure?")
    fmt.Println("File: " + f.Path)
    fmt.Println(f.Content)
    answer, err := ask("Are you sure? (y/N)")
    if answer == false || err != nil {
        return err
    }
    answer, err = ask("Really? (y/N)")
    if answer == false || err != nil {
        return err
    }
    err = os.Remove(f.Path)
    if err != nil {
        return err
    }
    color.Yellow("Deleted: %v", f.Path)
    return nil
}

func main() {
    f, _ := os.Open(memoDir)
    defer f.Close()
    files, _ := f.Readdirnames(-1)
    files = filterMarkdown(files)
    for _, file := range files {
        path := filepath.Join(memoDir, file)
        b, _ := ioutil.ReadFile(path)
        content := string(b)
        if isEmptyContent(content) {
            deleteFile(File{path, content})
        }
    }
}

簡易markdown previewerとしてmemoを使う

~/.config/memo/config.tomlの設定項目の中のmemodirでmarkdownを置いているディレクトリを指定できます。しかし、たまにそのディレクトリ以外のREADME.mdなどのプレビューを見たいときがあります。そういうときはシンボリックリンクを貼ってごまかしながら使っています。

% ln -s ${PWD}/README.md ~/Dropbox/_posts/2017-05-20-hoge.md

これで、http://localhost:3456/の一覧にファイル(2017-05-20-hoge.md)が出てくるようになるので、html描画後の様子を見ながら編集します。いい感じに編集が終わったらunlinkで掃除。

% unlink ~/Dropbox/_posts/2017-05-20-hoge.md

supervisorでmemoをサーバーとして起動時に立ち上げる

memo s --addr :3456でhttpサーバーがさっと立ち上がるのですが、ずっと使っていると立ち上げるのすら面倒で、PC起動したら立ち上がっていてくれという気持ちになりました。そこで、supervisorでmemoをサーバーとして起動時に立ち上げるようにしました。/usr/local/share/supervisor/conf.d/memo.confにこんな感じに書いておく。

設定ファイルの内容

[program:memo]
command=zsh -c 'direnv exec . memo s --addr :3456'
directory=%(ENV_HOME)s
user=yasuhisa
numprocs=1
stdout_logfile=/tmp/memo_out.log
stdout_logfile_maxbytes=10MB
stdout_logfile_backups=3
stderr_logfile=/tmp/memo_err.log
stderr_logfile_maxbytes=10MB
stderr_logfile_backups=3
autostart=true
autorestart=true

~/.config/memo/config.toml等を編集した場合、再起動しないと反映されないと思うので、適当にkillします。supervisorでautorestartがかかるので、ちょっと待っていると設定が反映されます。

能動学習で効率的に教師データを作るツールをGoで書いた

みなさん、教師データ作ってますか?!

機械学習のツールも多くなり、データがあれば簡単に機械学習で問題を解ける環境が整ってきました。しかし、データ作成は重要ながらも未だに大変な作業です。最近、私もいくつかのドメインで教師データを作る機会があったので、能動学習を使ってコマンドラインから簡単に教師データ作成(アノテーション)ができるツールを作ってみました。

今回は能動学習で教師データを作る意義と、作ったツールの使い方について簡単に書きます。

問題設定

例があったほうが説明しやすいので、問題設定を先に。

あなたはエンジニアが集まるSlackチャンネルに、最近人気の技術エントリを定期的に投稿するbotを作りたくなったとします。URLが与えられたときに、それが技術エントリかどうかを判定する機械学習の分類器があると便利ですね。これを実現するために、あなたはtwitterに流れてくるURLやはてなブックマークの新着エントリのURLから教師データの候補となるURLのリストを10万件取得しました。しかし、あなたは仕事も忙しいので、教師データはせいぜい300件程度しか付ける時間はなさそうです。同じ300件の教師データを作るならば、なるべく精度が高くなるような300件を選んで教師データを作りたい、というのが今回の問題設定です。

能動学習を使って教師データを効率的にアノテーション

優先的にアノテーションすべき300件のデータを能動学習に選んでもらいましょう。以下が説明用の図です。赤が正例(技術エントリ)、青が負例(それ以外のエントリ)を表わしています(2次元で書いていますが、実際は数千から数万次元程度です)。正例/負例を分離する境界のことを分離平面と呼びますが、分離平面の近くにいる事例や遠くにいる事例が存在します。

f:id:syou6162:20170517160045p:plain

分離平面から近い/遠いは以下のことを表わしています。

  • 分離平面の近くにいる事例
    • 分類器があまり自信のない事例
    • 「iPhone Swift」や「エンジニア 転職」のような事例
  • 分離平面から遠くにいる事例
    • 分類器が自信のある事例
    • 右上や左下に固まっているもの

Webデータでは実際には左下のような事例が非常に多く、これらをずっとアノテーションしていると鬱々としてきます。そういったものはなるべく見ず、分離平面に近い事例を中心的にアノテーションすることは、精度向上の観点だけでなくアノテーターの精神的な観点からも重要です。

能動学習には様々なアルゴリズムがあります。今回実装したものはかなりシンプルなアルゴリズムで、分離平面から最も近い(自信のない)事例からアノテーションして教師データに追加し、再学習するというものです。シンプルなのでいくらでも改善の余地がありますが、これだけでも大きな効果が出る場合があります。過去に実験したエントリがあるので、気になる人はそちらを参照してください。

go-active-learningを試してみる

ダウンロード/インストール

githubにコードを置いています。git cloneしてmakeすれば簡単に使えます。

% git clone https://github.com/syou6162/go-active-learning.git
% cd go-active-learning
% make build

もしくはgo getでもOK。

% go get github.com/syou6162/go-active-learning

能動学習でアノテーション

考え方が分かって、ツールもビルドできたところで実際に試してみましょう。まず、少量の教師データとまだアノテーションされていないURLが入ったファイルを用意します。今回ははてなのエンジニアのBlogを正例(2列目が1のもの)、nhkニュースなどを負例(2列目が-1のもの)としました。アノテーションされていないURLとしては「それどこ」や「メシ通」を入れています。

% cat tech_input_example.txt
http://www.yasuhisay.info/entry/2017/05/09/080000   1
http://www.songmu.jp/riji/entry/2016-12-26-yapc-hokkaido.html   1
http://blog.yuuk.io/entry/web-operations-isucon 1
http://hakobe932.hatenablog.com/entry/2016/11/25/141348 1
http://motemen.hatenablog.com/entry/2016/10/gofind  1
http://www3.nhk.or.jp/news/ -1
https://www.facebook.com/   -1
http://r.gnavi.co.jp/g-interview/   -1
https://cookpad.com/    -1
http://suumo.jp/town/   -1
http://www.rakuten.co.jp/   -1
https://www.hotpepper.jp/mesitsu/entry/bbshin/17-00009  -1
http://srdk.rakuten.jp/
https://www.hotpepper.jp/mesitsu/entry/kekkojin/17-00138

go-active-learningを使って、アノテーションしてみましょう。

% ./go-active-learning annotate --input-filename tech_input_example.txt --output-filename additionaly_annotated_examples.txt --open-url
Loading cache...
Label this example (Score: 0.000): http://srdk.rakuten.jp/ (それどこ)

「メシ通」のラーメンの記事(入力ファイルの一番下のもの)より自信がないということで、「それどこ」の記事がアノテーションする最初のエントリとして提示されました。技術エントリではないのでnを押して負例としてアノテーションします(pは正例としてアノテーション)。hを押すと、可能なコマンドの一覧が見れます。sを押すと、output-filenameで指定したファイルにこれまでアノテーションした事例が保存されます。アノテーションされ、教師データに追加された事例も含めて再学習が行なわれ、再度最も自信がない事例が提示される…といった感じでアノテーションを進めていきます。

現状、テキストが入力で二値分類でしか動かないですが、簡単なgoのコードなので多値分類への拡張や(特徴量抽出を頑張れば)画像への応用もできるんじゃないかなと思います。

おまけ: Go言語を使って得られた効用/感想

Goで書いていて、これはよかったなぁと思ったことをいくつか挙げてみます。

  • マルチプラットフォームで動くシングルバイナリになるので、他の人にもアノテーションをお願いしやすい
    • 他職種の人にあれこれインストールして、、、だと頼みにくい
    • kagomeがself-containedになっているのが大きい
  • 再学習が早い
    • 再学習は時間がかかるので10事例毎にしようかなと思っていたが、1事例毎にやっても全然問題にならない速さだった
  • たくさんのURLを取得しにいくのにgoroutine便利
    • goroutine使わないとアノテーションまで大分待たないといけない
    • 簡単に並列化できるので、待ち時間が減る
  • Goでも本文抽出のライブラリがあったので、そんなに困らなかった

Go言語にさらに入門するために係り受け解析器を書いた話

今年からGo言語に入門していますが、もう少し複雑なものをものを書いてみたいと思ったので、係り受け解析器を書きました。その過程で工夫したこと、苦労したことをまとめます。作ったものはこちら。

一人で作っているプロジェクトですが、100行以下の細かめの修正毎にPull Requestを作っているので、どのような過程でやっているかが比較的分かりやすいと思います。

easy-first algorithmについて

  • 係り受け解析アルゴリズムの1つです
  • 最も自信のある(つまり簡単な)ところから係り受け関係を付けていくgreedyな方法です
  • コーパスには正解の係り受けは付与されていますが、簡単さの順序は当然分からないので、構造化パーセプトロンでそれを含めて学習します
    • 構造化パーセプロトンについては入門エントリを書いているので、興味がある方はどうぞ

なぜ係り受け解析器を書いてGo言語の入門をするのか

  • 今年の初めからGo言語に入門していた
  • おかげでAPI wrapper的なcliツールは作れるようになった
  • しかし、以下の経験をもっと積みたい
    • もっとCPUやメモリに負荷がかかる計算をやらせたい
      • Go言語でどの辺がボトルネックになりやすいのか知りたい
      • プロファイリングで地道に改善していく経験がしたい
    • もう少し複雑なデータ構造を扱いたい
    • goroutineやchannelについての知識を、本を読むだけではなく、実装しながら掴みたい
  • 係り受け解析器を作るのは(自分にとっては)よい選択
    • ドメイン知識があるので、係り受け解析器のこと自体はそれほど心配しなくてよい
      • clojureでも実装経験済み
      • 初めてのタスクでやるとタスク固有のことではまっているのか言語のことではまっているのかの切り分けが必要
    • 特に学習部分はあまりにナイーブな実装をするとそこそこ時間もかかるし、メモリも食う

学んだこと

Goroutineによる並列処理

Go言語の定石的なところ。sync.WaitGroupを使って並列に処理をする、channelを使ってCPU数に合わせた並列度にするといったところが学べました。元の関数にはほとんど手を入れずに並列化できているので、お手軽感が分かるかと思います。

文字列連結のパフォーマンス

最初は何も考えずにfmt.Sprintfで丁寧に文字列の特徴量を作っていましたが、案の定ボトルネックになりました。読みやすさも考えて妥協できるライン(ハードコーディング)で落ち着きました。

プロファイリングの取り方

文字列連結のパフォーマンス改善をしたほうがよさそうと決めたのも、プロファイリングの結果を見てからでした。Goは標準のツールセットがよくできていますが(gofmtなど)、プロファイリングも標準のpprofで十分でした。pprofの使い方については、以下のサイトを参考にしました。

プロファイラを取りたいコードに以下のコードを仕込みます。

defer profile.Start(profile.CPUProfile).Stop()

仕込んだバイナリを実行すると、cpu.pprofができあがります。

% go build
% ./go-easy-first ...
% go tool pprof go-easy-first /var/folders/pk/zk906vvd7rx7ls8g3sv3vs800000gn/T/profile498633669/cpu.pprof

私の場合はtop 20 -cum main.list main.を実行するとボトルネックになっている箇所が大まかに分かり、そこを修正すると速度が改善できました。最初は7分程度かかっていたものが2分程度まで縮んだので、まあまあのインパクトです。また、実行する関数を文字列で取りたい場所でreflectionを使っていて、パフォーマンスが若干気になったのですが、プロファイリングを取ることでそこはほぼ問題にならない程度の時間であることが分かってよかったです。

mapの辞書引きのパフォーマンス

同じくプロファイリングを取っていて少しずつ改善していった箇所です。文字列連結のパフォーマンスを改善した後も、内積計算(DotProd)で計算時間がかかっていることが分かりました。初期は内積計算をするための重みベクトルをmap[string]float64で保持していましたが、keyが文字列で引いてくるのは明らかに時間がかかります。そこで、keyをid化して辞書引きを高速にするように変更しました。また、Feature Hashingも使い、最大の次元数を決めることで重みベクトルも[]float64となり、使用するメモリも削減&高速になりました(最大の次元数が小さすぎるとhashが頻繁に衝突してしまいますが、100万次元程度確保しておけばこのタスクではほぼ精度は落ちずに済みます)。

外部のciサービスを使う

仕事ではcircleciやtravis-ciにお世話になっていますが(mackerel-agent関係)、個人のプロジェクトでは使ったことがなかったため導入してみました。CircleCIのようなバッチをREADME.mdに付けたかったというミーハー的な動機もあります。いくつか似たようなものがありましたが、今回は以下の3つを導入しました。

ずぼらな性格なので、一人でやっているとどんどん適当になってしまいがちですが、ciを入れると適度に秩序が保てるのでよいですね。

タスクランナーとしてMakefileを使う

これまで小規模なGo言語のアプリしか使っていなかったため、go rungo buildで十分でした。しかし、後述するgo-bindata等をビルドの前に行ないたい等、色々やることが増えてくるとタスクランナーが欲しくなってきます。みんGoでも我等がid:SongmuさんがMakefileでやることをオススメしていたので、Makefileを導入することにしました。

はまったこと/困ったこと

パラメータファイルの読み書き

パラメータファイルはただのベクトル([]float64)なので、human readableなjsonである必要は特にありません。バイナリファイルに書いて、デコードのときに読み込ませるようにしようと思いましたが、イマイチbest practiceがよく分かりませんでした。試行錯誤をした結果、encoding/gobのEncoder/Decoderでやりすごしました。

この程度のことだったら、gopher的にはこの程度だったらencoding/binaryを使うのがよい?

パラメータファイルをバイナリに埋め込む

係り受け解析器を走らせるんだったら、バイナリを一個落としてくるとそれでOKな形になっているとかっこいいです。Goで書かれた形態素解析器のkagomeは実際そのようになっています。

みんGoでこういうときにはgo-bindataを使うべしとあったので、導入してみました。

しかし、ちょっと困りました。学習済みのパラメータファイルがすでにあれば普通にビルドできますが、そもそもビルドしないと学習ができないのでした。ひとまず空ディレクトリを置いとくことでビルドは通るようにしました。普通にgo-bindataを使うだけならそんなに困らないけど、機械学習のモデルもバイナリに組み込んで配布したいといった場合にはちょっと考えることが増えそうです。

副作用

ここは当てはまる人はそんなにいなさそう。前職ではClojure、現在はScalaを書いていることが多く、immutableなデータ構造を扱うことが多いのですが、Goは当然ながらそうではないので、副作用関連ではまりました。思っていなかったところが書き変わっていたり、あるいは変更できたと思っていたところが書き換えられていなかったり。immutableな場合は考えるスコープがかなり局所的ですが、mutableだと広くなりがちなので頭のスイッチに時間がかかりました。

まとめ

係り受け解析器をGo言語で書くことで学んだ様々なことについて紹介しました。慣れない言語は自分が慣れている言語と比べるとどうしても実装速度等は落ちてしまいがちですが、自分がなじんでいるドメインで実験すると新しい言語のこと自体に集中できて、なかなかよかったです。そろそろGo初心者を脱出したい…!

みんなのGo言語【現場で使える実践テクニック】

みんなのGo言語【現場で使える実践テクニック】

実タスクで機械学習を導入するまでの壁とその壁の突破方法

社内で機械学習の案件があった際に、機械学習の経験者しか担当できないと後々の引き継ぎで問題が起こりがちです。これを防ぐために、機械学習に興味があり、これまで機械学習を経験したことがないエンジニアにも担当できる体制を整えられることが望ましいです。しかし、機械学習のことに詳しく知らないディレクターやエンジニアにとっては、どのような機械学習の理解段階ならばタスクを任せられるかの判断をするのはなかなか困難です。そこで、このエントリでは機械学習を実タスクでやるまでに乗り越えるべき壁だと私が思っているものについて説明します。

第一の壁: 綺麗なデータで機械学習の問題を解ける

  • 講義で扱われるような綺麗なデータを扱える
    • 行列形式になっていて、欠損値や異常値もない
  • 上記のデータを回帰や分類問題として解くことができる
    • 実際に解く際にはライブラリを使って解いてよい
    • 手法を評価する上で何を行なえばよいか(PrecisionやRecallやRMSEなどを計測する)知っている
  • アルゴリズムが導出できなくてもよいが、その手法がどういう問題を解いているのかは大雑把に分かる
    • 回帰/分類だったらどういう目的関数を最適化しているか
    • その目的関数の心はどういうものか
    • 学習や予測がうまくいかないとき、どこに着目すればよいか分かる

第二の壁: 綺麗でない現実のデータで機械学習の問題を解ける

  • 整形されていないデータを機械学習で扱える特徴量の形式特徴量に落としこめる
    • 例: テキストデータをBoW形式に落とせる
    • 例: 異常値、外れ値は前処理で落とせる
  • 必要なパフォーマンスが出るようにチューニングができる
    • 特徴量選択、正則化項、頻度での足切りによるチューニング、ラベル数の偏りがある場合などへの対応
    • 「多少のゴミがあってもいいから取りこぼしが少ないようにして欲しい」「取りこぼしがあってもいいからとにかく綺麗な結果を見せたい」等の要望があったときにどうチューニングすればいいか想像が付く

第三の壁: 機械学習の問題としてどう定義し、サービスに導入していくか

  • その問題は機械学習で解ける問題なのか、そもそも機械学習で解くべき問題なのかの判断が付く
  • 機械学習で解くことが決まったとして、どのように定式化するのか
    • 例: ランキング問題として定式化するのか、分類問題の組み合わせで対処するのか
  • 学習用データや評価用データをどうやって作る/集めるか
    • そもそもどの程度学習/評価データがあればある程度安定して学習/評価できるか知っている
    • 一貫性のあるデータ作りのガイドラインを作れるか(結構難しい…)
    • 闇雲に教師データを追加しても精度は上がるとは限らない

これらの壁をどう乗り越えていくか?

  • 第二段階までの壁は機械学習に関する書籍やBlogやライブラリが最近は山のように存在するので、ハードルは相当下がっている
    • はてなの教科書もあります!
    • 逆に教材がありすぎて何からやればよいか分からない…、という悩みは最近はあると思うので、問題に合わせた最短経路を経験者が示せるとよい
    • kaggle等で未経験者と経験者で同じ問題を解き、どのような工夫をすると精度が上がるか等を一緒にやってみる
  • 第三の壁は経験によるところが大きいため、経験者がメンター的に付いてアドバイスしながら手を動かせるとよい

第四の壁: 機械学習導入後の運用

言語処理のための機械学習入門 (自然言語処理シリーズ)

言語処理のための機械学習入門 (自然言語処理シリーズ)

タスクに合わせたトークナイザ、単語分割に関連したポエム

ポエムを適当に書きます。2本立て。週末のノリなので、適当です。

Sentencepieceの紹介記事を読んだ

ニューラル言語処理向けトークナイザのSentencepieceについて書かれた紹介記事を読みました。

自分用の要約すると

  • ニューラル言語処理では語彙数が大きくなると扱いにくい
  • 単語をサブワードに分割できるものは分割して、語彙数を制限する(数千から数万)方法がよく使われる
    • 尤度を最大にするエントロピー圧縮の一部と見なせる
  • スペースもメタ文字に置き換えて生文を食わせることにより、detokenizeが言語によらず簡単になる
    • 翻訳等のタスクで助かる!
    • こういうのが必要なくなる
    • 単語分割されたものからさらに分割するわけではなく、生文からやるために計算量オーダーの削減が行なわれている
  • 従来の単語分割手法を凌ぐBLEUスコアを達成している

ということでした。ちまたで話題のneologdを使うと逆にBLEUスコアが(mecabのときよりも)落ちてしまうという実験結果もあって、応用に即した単語分割を使うべきという工藤さんの主張に(再び)納得でした。

余談ですが、生命科学系の研究所(DBCLS)でバイトした経験から、M1の初期は応用タスクに最適化された単語分割を学習できないかと思って少し考えた時期がありました。Sentencepieceのようなシンプルな手法がニューラル言語処理向けトークナイザとして役に立つのは面白いなあと、一人でテンションが上がりました。ちなみに、M1の初期のやつは、Joint Learning的なノリで解けないかと思っていましたが、挫折で終わりました…。

文書分類でneologdとmecabを比較した

「応用に即した単語分割を使うべき」というエントリを読んだのと、週末のノリで単語分割を変えてあれこれ遊びたいという欲求が湧いたので、自分のbotで試してみました。機械学習のエントリかどうかを判定してくれるbot君です。

タイトルと本文をそれぞれ形態素解析、BoWとして放り込むだけの手抜き実装です。この形態素解析の部分をneologdを使う/使わないで比較してみました。学習/テストデータの分け方で精度が変わってくるため、10回ランダムにデータを分割、F値の平均と標準偏差を出してみました。

平均 標準偏差
mecab 0.747 0.0056
neologd 0.766 0.0066

neologdを用いた場合、BoWの次元数がmecabの場合よりも増加していました。人工知能の話題でよく出てくる「東ロボくん」もneolodgでは一単語として認識されているので、このような単語により精度が上がっていそうかな。

% echo "東ロボくん" | mecab -d /usr/local/lib/mecab/dic/mecab-ipadic-neologd
東ロボくん      名詞,固有名詞,一般,*,*,*,東ロボくん,トウロボクン,トウロボクン
EOS
% echo "東ロボくん" | mecab
東      名詞,一般,*,*,*,*,東,ヒガシ,ヒガシ
ロボ    名詞,一般,*,*,*,*,*
くん    名詞,接尾,人名,*,*,*,くん,クン,クン
EOS

@overlastさんが過去のNL研等で報告されているように、文書分類のタスクではneologdは有効そうなことが分かりました。

まとめ

謎のテンションなので特にまとめはないですが、応用タスクによって単語分割を使い分けることを検討してみてもよさそうです。

言語処理のための機械学習入門 (自然言語処理シリーズ)

言語処理のための機械学習入門 (自然言語処理シリーズ)