[Dd]enzow(ill)? with DB and Python

DBとか資格とかPythonとかの話をつらつらと

scouty 機械学習講習会 〜自然言語処理入門「Pythonでつくる言語モデル」〜 に参加してきた

ISUCONショックから1週間ほど更新が空いてしまいました。先日表題のイベントに参加してきたのでその備忘録です。

scouty.connpass.com

概要

scoutyというHRTechのスタートアップの創業者である島田 寛基さんが講師となり、自然言語処理の基礎的な内容について講義されました。期待成果としてはconnpassのサイトに以下をあげられていました。

  • NLPの基礎である「言語モデル」とその代表的な構築法「Ngram」についての理解
  • 複数の作者の小説の文章を学習して、文体から著者を判別する「著者判定アルゴリズム」の作成
  • 外国人と日本人のアルファベット表記された名前を学習して、名前から国籍を判別する「国籍判定アルゴリズム」の作成
  • Twitter上の不適切な言葉遣いを教師データにして、文章の不適切度合いを判別する「暴言チェッカー」の作成
  • 正しいスペルの単語を学習して、単語を入れたらスペルの間違いがあるかどうかを判別する「スペルチェッカー」の作成

これらを実現する上での基礎知識についての講義を行い、その後実際にコードを1hほど書く実装タイムが設けられていました。

実際に手を動かして、宮沢賢治っぽい文章の判定器を実装しました。また早めに終わった人向けに、人名が日本人かどうかの判定器の課題もありました。

内容

以下は講義中のメモです。

言語モデルとは

文を入力し、その文が起こる確立(生起確率)をもとめる。ある単語の羅列が発生しやすいかどうかを予測するもの。

  • 今日はいい天気ですね
  • いい天気は今日ですね

という2文において、["いい","天気","は","今日"]という並びよりも["今日は", "いい", "天気"]という並びのほうが発生しやすいと判断する。

これは、学習元の文章にどの程度そういった並びの文章があるかをもとに判定するので、学習元によって生起確率は異なる。

言語モデルの作成方法

  • 大量に文章データ(コーパス)を学習させ、その学習結果に対して入力を与えた時にその入力の自然さを判定する。
  • もっともシンプルなものは入力された文章自体が元データに含まれているかどうかで判定
    • シンプルだが、文章が少しでも異なれば判定できなくなってしまう
    • 似た文章であっても確率が求められなくなる
      • データがスパースすぎる
      • 汎化能力がない

N-gram

単語の出現確率はその前のN単語に依存するという仮定をもとにした考え方。2-gram(バイグラム)や3-gram(トライグラム)等があり、Nの最適値はデータ次第。

渋谷はどこですかという文章の生起確率は以下でもとめることができる。

P("渋谷はどこですか") =
 P(<渋谷>|文頭) * P(は|渋谷) * P(どこ|は) * P(ですか|どこ) * P(<文末>|ですか)

P(内容)はその内容が発生する確率であり、 P(内容|条件)はその条件下で内容が発生する条件付き確率を示す。例えば、P(は|渋谷)は直前の文字が渋谷だった時に次にという単語が来る確率をもとめている。

この様に前後2文字単位で条件付き確率をもとめ、それを乗ずる事で文章自体の生起確率をもとめることができる。

この考え方の場合、確率を求める文章自体がコーパスに含まれていなくとも、部分部分で似ていれば生起確率が求まる。

エントロピー

エントロピーとは不確かさの尺度。いい言語モデルは、実際に生起されやすい文章に対して高い確率を出力するはずであり、エントロピーは低くなるはず。 ※このあたりはエントロピーが低い方がいい!くらいしか理解できていない

言語モデルは単語に対しても適用可能

文章では渋谷はどこですか["渋谷","は","どこ","ですか"]と区切って単語の配列を処理している。これをAppleという単語について["A", "p", "p", "l", "e"]と文字単位の配列を生成すれば、ほぼ同じ考え方でその単語の自然さをもとめることができる。

平滑化

P(渋谷|は) が0を戻す場合、その文章全体が0になってしまう。なぜなら文章の確率はN単語間の確率の掛け算なので、何れかの箇所が0を戻すと式自体が0になってしまう。

これを避けるために平滑化というものを行う。平準化についてはいくつか手法が存在する。

Add-One Smoothing(Laplace Smoothing)

分子に1を足し、全体の確率が破綻しないように分母にもボキャブラリーサイズ加算することで0になるのを避ける。

Add-α Smoothing(Lidstone Smoothing)

先程は分母に1を加算したが、それだと影響が大きいため1未満の値を加算するもの。 こちらでも分母が大きくなるため頻出単語の出現確率の評価が下がってしまう。

※ちょっとこのあたりも怪しめ

実装編

40分ほど上の講義を受けた後に、githubからソースやコーパスの入ったリポジトリをCloneして1時間ほどコードをかきました。

課題1:宮沢賢治っぽさの判定

課題は、青空文庫から入手した宮沢賢治の銀河鉄道の夜を学習してつくったモデルを用いて宮沢賢治の他の作品や、太宰治等の作品を評価するというものでした。最終的には宮沢賢治の他の作品については宮沢賢治っぽいという判定を下せるところまで行きました。

課題的には

def train_model(corpus_path, n=2):
    """
    corpus_path(str)に指定されたファイルのテキストから言語モデルを学習し、n-gramで学習してモデルオブジェクトを返す
    """

    estimator = lambda fdist, bins: LidstoneProbDist(fdist, 0.2)

    # ヒント
    # >>> model = NgramModel(n, words, estimator)
    # で、NgramModelオブジェクトが学習&生成される。
    # words(list of str) は訓練データとなる文を、単語ごとに切ったもの。
    # estimatorは、Smoothing(平滑化)といって、1度も出ていない単語の出現確率を他の単語から推定する手法のための推定器で、ここではおまじないと考えて良い

    return model

こんな感じで実装されていない関数が6個ほど並んだソースを渡されるので、コメントを見ながらその関数を順に実装していくと最終的に形になります。

私はPythonは多少書けますが、機械学習のコードはあまり書いたことがなかったので初見では知恵熱が出ましたが、ヒント等が親切なので意外とサクサク進めることができました。

なお、判定にはエントロピーがX以内なら宮沢賢治っぽいっという実装をしましたがN-gramのNを変更するとしきい値がかわるのでそこは調整が必要でした。

課題2:人名が日本語圏かどうか

課題1が終わった人向けのオプションの課題で、人名を渡した時にそれが日本人っぽいかを判定するモデルを作成しました。

こちらは、英語圏と日本語圏の人名のサンプルコーパスは与えられていましたが、課題1の様に空の実装等は用意されていないのでスクラッチする必要がありました。

と、いっても講義中にもあったようにN-gramでは文章でも単語でも同じ考え方なので課題1のコードをもとに割とサクッと作れました。

結局、yamada taroがあったときに、

P(yamada taro) = P(y|head) * P(a|y) * P(m|a) * P(a|m) * P(d|a) * .... 

の確率を求めればよいのでコードはほとんど流用できます。最初は、日本語圏のデータを元に言語モデルを作成し、入力された人名の評価結果について一定値以内であれば日本語圏という判定をしていました。

しかし、最終的には英語圏のモデルも用意し、入力について両モデルで評価した結果を比較して判定したほうが良い結果が得られたのでその実装としました。 ※日本語・英語圏ともに95%くらいの判定率だったので満足です。

まとめ

機械学習系のセミナーだと、座学だけでおわってしまうものも多いと思います。座学はなんとなく理論はわかった気になるのですが(実際私もそうなる)帰って一人で試すと結構理解できてないってことが多いのです。しかし、講義の直後にそれを実践する機会があるので理解が深まって楽しめるセミナーでした。

1時間の実装タイムは普段Pythonを書き慣れていない人には少しつらめの難度かもしれませんが、講師の方が質問に答えてくれるので1時間頭かかえっぱなしということは無いと思います。