mots quotidiens.
Daichi Mochihashi (持橋大地) daichi <at> ism.ac.jp by hns, version 2.10-pl1.

先月 2021年06月 来月
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30

2021年04月14日(水) [n年日記]

#1 NPMIによる教師なしフレーズ認識

Mikolov+(2013)の有名な Word2Vecの論文 では, 単語ベクトルを作る際に, "New York" や "Toronto Maple Leafs" (アイスホッケーチーム)の意味は要素である "new" や "maple" "leafs" とは基本的に 関係ないので, 先にフレーズを認識して "new_york", "toronto_maple_leafs" と 単語をまとめてからWord2Vecを適用する方法が述べられています。 もちろん固有表現認識(NER)を動かせばできますが, NERは事前に人が作成した教師データに依存する ため, 教師データを使わない方法として, word2vecの論文では単語vと単語wがフレーズとなる スコアを
score(v,w) = (n(v,w) - δ)/(n(v)*n(w))
とする, という方法が述べられています((6)式)。 ここでn(v,w),n(v),n(w)はバイグラムおよびユニグラム頻度で, δは, 頻度の低いペアが上位に来るのを抑えるためのディスカウント係数です。
このスコアが閾値以上になる単語バイグラムを一単語としてまとめれば, フレーズを自動的に認識することができます。

上のスコアは, 確率として考えると, ユニグラムの総和をNとして p(v,w)=n(v,w)/N, p(v)=n(v)/N, p(w)=n(w)/N なので, δの部分を除くと,

score(v,w) = n(v,w)/(n(v)*n(w)) = (p(v,w)*N)/(p(v)*N * p(w)*N) = p(v,w)/(p(v)*p(w)*N)
となり, ほぼ, 自己相互情報量 PMI(v,w) = log p(v,w)/(p(v)*p(w)) と同じスコアを計算していることになります。

ただしよく見ると, 元の式には 1/N というファクターが入っており, これは コーパスに依存する ので, スコアが基本的にコーパスの長さに依存したものになってしまいます。 コーパスを固定して閾値を探索するのであれば問題ありませんが, 閾値の意味がわかりにくい上, 長さの違うテキストを使って結果を比べたい場合などでは, 同じ基準のフレーズにはならなくなってしまいます。

元のスコアにNを掛けてコーパス非依存にすることもできますが, そもそもこのスコアは本質的にPMIを計算しているので,

という欠点を解消した, Normalized PMI (NPMI)(Bouma 2009) を使えばよいような気がします。NPMIは, 次のようにして定義されます。
NPMI(v,w) = log p(v,w)/(p(v)*p(w)) / (-log p(v,w))
NPMIは -1≦NPMI(v,w)≦1 の範囲で定義され, 1のときにvとwは完全に相関, -1のときに完全に逆相関という, 非常にわかりやすい基準になっています。 しかもPMIにあった「p(v)やp(w)が非常に小さい時にスコアがインフレしてしまう」 という欠点も解消されているので, こうすると, word2vecの元論文にあったヒューリスティックなディスカウント係数 δも不要になります。

というわけで, 少しスクリプトを書いて, フレーズの自動認識を試してみました。 核心部だけ書くと, unigram, bigram にそれぞれユニグラムとバイグラムの頻度が 辞書として保存されているとき, 単語バイグラム(v,w)がフレーズとなるかどうかは次のようにして 判定できます。

def compute_phrase (unigram, bigram, threshold=0.5):
    N = sum (list(unigram.values()))
    phrases = {}
    for bi,freq in bigram.items():
        if freq > 1:
            v = bi[0]; w = bi[1]
            npmi = (log(N) + log(freq) - log(unigram[v]) - log(unigram[w])) \
                    / (log(N) - log(freq))
            if npmi > threshold:
                phrases[bi] = npmi
    return phrases
閾値は, 試したところでは0.5くらいにするのがよいようです。

これをテキストについて1パス動かすと2単語のフレーズが得られ, それを入力にして2パス目を動かすと2〜4単語のフレーズが得られます。 同様にしてnパス動かすと, 2〜2^n単語からなるフレーズが得られます。
日本語版text8 (100MB, 56万文, 1694万語)について4パスで動かしてみたところ, 次のようになりました。

甲_越_同盟 ( こう え つどう めい ) は 、 天正 7 年 ( 1579 年 ) に 甲斐 の
戦国_大名 武田_勝頼 と 越後 の 戦国_大名 上杉_景勝 と の 間 で 成立 し_た 同盟 。
定義 上 は 超_硬_合金 と 呼ば_れる 炭化_タングステン ( wc ) を 主成分 と し_た
もの も 含ま_れる が 、 これ を 別 の もの として 扱う こと が 多い 。
ここでは「甲越同盟」「戦国大名」「武田勝頼」「した」「超硬合金」「炭化タングステン」「呼ばれる」「含まれる」 などが, 統計的に自動的にフレーズとして認識されています。 通常のNERでは教師データに依存するため, 「甲越同盟」「超硬合金」などを認識するのは難しいと思われるため, これは非常に有用だと思います。 もちろんこれは word2vec の元論文のときからできていたわけですが, 上に書いたように基準がデータ依存で, 低頻度語に弱い(&それを避けるためのヒューリスティックなパラメータに依存する)という問題があったため, NPMI>0.5という基準は非常にわかりやすく, 有用なのではないかと思います。

なお, 4パスなので原理的には16単語までの単語がフレーズとして認識される可能性が あるわけですが, 調べてみると, 「福島_第_一_原子力_発電_所」などはもちろん フレーズとして認識されている他, 非常に長いものとしては以下のようなものがありました。(一部抜粋)

第_二_次_世界_大戦_末期
福島_第_一_原子力_発電_所_事故
連合_国軍_最高_司令_官_総_司令_部
全国_高等_学校_野球_選手権_大会
学研_奈良_登美_ヶ_丘_駅
国際_連合_安全_保障_理事_会
ユーゴスラビア_社会_主義_連邦_共和_国
全国_高等_学校_サッカー_選手権_大会
ニューヨーク_州立_大学_バッファ_ロー_校
全日本_実業_団_対抗_駅伝_競走_大会
芸術_選奨_文部_科学_大臣_賞_受賞
mf_文庫_j_ライト_ノベル_新人_賞
〜_っと_!_お_ジャ_魔女_どれ_み
ufc_世界_女子_ストロー_級_タイトルマッチ
pc_エンジン_オール_カタログ_'_93
独立_行政_法人_産業_技術_総合_研究所
国際_連合_難民_高等_弁務_官
ほっ_かい_どう_さ_っぽ_ろ
ほっ_か_ほっ_か_亭_総本部
『_機動_戦士_ガン_ダム_seed
学_研究_科_修士_課程_修了
御_樋_代_木_奉_曳式
絶叫_カオス_傑作_選_大声_クイズ_vs_谷_桃子_vs_ヒム_子
「福島_第_一_原子力_発電_所_事故」は7単語, 「連合_国軍_最高_司令_官_総_司令_部」は8単語がまとまって1つのフレーズと認識されています。「御樋代木奉曳式」(みひしろぎほうえいしき)は木曽で切り出された御料木が伊勢神宮に運ばれる儀式のことのようですが, こうした特殊な語彙のため形態素解析に失敗していても, フレーズ認識で一単語と認識することができています。
ただ今回, 先頭から最長一致でフレーズ化しているため, 「〜_っと_!_お_ジャ_魔女_どれ_み」「『_機動_戦士_ガン_ダム_seed」は本来は, 後から伸ばすことでもっと正確に認識できる名前ではないかと思います。 なお, 「絶叫_カオス_傑作_選_大声_クイズ_vs_谷_桃子_vs_ヒム_子」は何と12単語からなる フレーズ(!)で, これは こういうバラエティ番組のDVD の名前であるようです。
認識された長いフレーズのリストの例は, こちらに置いておきました。 バイグラムを結合するときの頻度を10以上などにすると, より綺麗なフレーズが 得られますが, 頻度の低い長いフレーズは認識されなくなります。 頻度を1以上にした場合の, 面白い長いフレーズの例を, 自分用メモも兼ねて こちらに置いておきます。

下に, フレーズを認識するPythonスクリプトを置いておきます。 使い方はそのまま実行してヘルプを読むか, 中身をご覧下さい。


2020年10月07日(水) [n年日記]

#1 getopt(1) in R

Rにも ライブラリ で getopt(1) があるようですが, 元のgetoptと違い, % foo.R --flag=xxx file1 file2 の file1 file2 のように, オプションの引数でない引数を許さない(!)ようなので, 簡単なラッパーを書いてみました。
使い方は中の main () にある通りですが, 元のように byrow=T のように余計なこと を書く必要がなく,
opts <- getopts ( c(
    "alpha",  "a", 1, "double",
    "iters",  "N", 2, "integer",
    "output", "o", 2, "character",
    "help",   "h", 0, "logical"
), usage, 2)
のように書くだけで使えます。&, opts にある名残り opt$ARGS (上の一般引数に 対応しようとした名残りだと思われます) を消し, 指定されなかったオプションには NAを代入するので, opt$foo のようにそのまま他に引数として渡すことができます。
最後の2は一般引数の数で, さすがにオプションと混じった場合には使えませんが, 一般引数は最初に置いても最後に置いてもよいようにしておきました。

#!/usr/local/bin/Rscript
#
#    opts.R
#    for easy use of getopt(1).
#    $Id: opts.R,v 1.5 2020/10/07 00:21:00 daichi Exp $
#
library("getopt")

getopts <- function (spec, usage, n, ...)
{
    res <- list()
    tokens <- commandArgs (T)
    
    if (length(tokens) < n) {
        usage ()
    }

    if (substr(tokens[1], 1, 1) == "-") {
        res$args <- tail (tokens, n)
        res$opts <- getopt (matrix (spec, byrow=T, ncol=4),
                            head (tokens, -n), ...)
    } else {
        res$args <- head (tokens, n)
        res$opts <- getopt (matrix (spec, byrow=T, ncol=4),
                            tail (tokens, -n), ...)
    }

    # fill unused options by NA
    names <- spec [seq(1,length(spec),by=4)]
    for (name in names) {
        if (is.null(res$opts[[name]])) {
            res$opts[[name]] <- NA
        }
        res$opts$ARGS <- NULL
    }

    res
}

main <- function ()
{
    usage <- function ()
    {
        cat ('usage: % opts.R --iter=iters --output=file train model\n')
        quit ()
    }

    opts <- getopts ( c(
        "alpha",  "a", 1, "double",
        "iters",  "N", 2, "integer",
        "output", "o", 2, "character",
        "help",   "h", 0, "logical"
        ), usage, 2)

    if (!is.na(opts$opts$help)) {
        usage ()
    }

    print (opts$args)
    print (opts$opts)

}

if (!interactive() & sys.nframe() == 0) {
    main ()
}

2020年05月21日(木) [n年日記]

#1 ページャーとTcl/Tk

Macintosh や X-window でGUIを使い始めた1990年代の頃から, 「GUI環境にはまともなページャーがない」ということがずっと気になっていた。
Windowsでは *.txt のテキストファイルをクリックするとメモ帳が, 現在のMacではテキストエディットが立ち上がるが, これらは基本的にエディタ なので, 単純にテキストの中身を見るには次のような欠点がある。 一方でDOSには MIEL, 端末では lesslv のような高機能ページャーがあるので, GUIでも何とかならないかと考えていた。 *1 X-windowでは, 学部生の頃に買ったTcl/Tkの最初の本, Ousterhoutの "Tcl and Tk Toolkit" のサンプルプログラムにテキストを表示するサンプルがあったため, 自分はそれをほぼコピペして, テキストファイルを別の窓で見たい時に使っていた。

ただしこれはコピペなので, 内容を深く理解しているわけではなく, メニューがないなど使いやすさには大分距離があった。 今年のゴールデンウィーク中, 思い立ってTcl/Tkを学んでみることにしたので, それでこのプログラム Tkv を書き直してみた。

これはメニューから新しいファイルもオープンできる他, 下部に行数やバイト数も表示される。 特にGUI環境では, スクロールバーがあるので現在テキストのどこを見ているかが明確で, 大きな利点ではないかと思う。
簡単に使えるようにするため, less等と同様に単に q を押す (またはメニューからFile->Quitを選ぶ)と終了し, スペースまたは C-f で1画面先へ, バックスペースまたは C-b で1画面戻り, j,k (viバインド)で1行下/上に進むことができる。 必要な人がいるのか分かりませんが, 一応下に置いておきます。

なお, 上の画像は Tcl7.6jp/Tk4.2jp のもので, 現在のTk8.6では同じファイルでも, 以下のように全く話にならないユーザーインターフェイスになってしまう。

これはTcl/Tkが8.5あたりから, ネイティブのインターフェイスと統合するように なったからで, 正直なところ, 最初の画像のようにMotif風の美しいUIが 簡単に作れることがTkの一番の魅力だったのに, 論理的に合っているだけの 下のようなインターフェイスでは, まったく作る気がしません。 少し調べたところ, Tcl7.6jp/Tk4.2jpをコンパイルすることは今でも可能だった ようなので, 自分でコンパイルして実行することにしました。
なお, 現在はThemed Tk (ttk)というTkの拡張を使うと, 上のようなMotif風の インターフェイスにすることは原理的には可能なようですが, プログラムが 入れ替えになる上, 細かいところが違っているので, やはり元の方がよいように 私は思っています。ただし, Tk4.2jpではUTF-8に対応していないため, 本当は必要な場合には iconv で変換して読み込む, などの対策が必要では ないかと思います。

いずれにしても, 学部生の頃から気になっていた Tcl/Tk をようやく少し書ける ようになり, *2 これでGUIも必要に応じて作れるようになったので, 大変嬉しく思っています。


*1: 当時探したところ, WindowsではttPageというページャーがあったが, GUIのセンスといい, 使いやすさといい, 喜んで使えるソフトウェアではなかった。 一応XにはXlessというプログラムがあるようですが (画像) , これはlessのキーバインドは全然使えない上, 日本語は通らず, GUIとしても 完全に適当なので考慮の対象外になってしまう。リンクの画像は私がXawの代わりに NeXT風のlibneXtawに入れ替えているのでまだましな方で, 標準ではスクロールさえ難しいUIになっている。
*2: Pythonに付属している Tkinter で研究に必要な絵を書く, というくらいのことは 既にやっていました。

3 days displayed.
タイトル一覧
カテゴリ分類
 なかのひと
Powered by hns-2.10-pl1, HyperNikkiSystem Project