自然言語処理 に関する投稿を表示しています

日本語の折り返しを正規表現で解決する mikan.js を PHP に書き換えた

できたものがこちらです

作った背景

日本語の折り返しが中途半端になってつらい!機械学習で改善するぞ!という話が過去にありました。
google/budou: Budou is an automatic organizer tool for beautiful line breaking in CJK (Chinese Japanese and Korean).

それからしばらくして、いやいや機械学習じゃなくてもいいのでは?というものが出てきました。
mikan.js : 機械学習なしで、日本語の単語の改行を処理するライブラリを書いた

これって別に JS で表示するときにアレコレしなくても、普段のサーバサイドでいい感じにしてもいいのでは??と思い PHP に移植してみた次第です。

移植する流れ

元のコードを眺めて、同じような処理に書き換えていく簡単なおしごと。
幸いにも、そこまで難しいロジックでは無いので、動作をみながら書き換えていきました。

正規表現のあたりだけ、言語の違いでちょっと詰まったので、ドキュメントを見ながら動作を見ながら随時書き換えていくようなコツコツ作業でした。

言語の書き換え、双方の言語について理解が深まるのでオススメです。

「出現頻度と連接頻度に基づく専門用語抽出」という論文に出てくる MC-value について理解した(つもり)

FLR の件と同じ論文で出ていた MC-value について。「単名詞バイグラムによらない用語スコア付け」として挙げられている

「出現頻度と連接頻度に基づく専門用語抽出」という論文に出てくる FLR について読んだ | ごみばこいん Blog

元の論文

FLR と同様に「出現頻度と連接頻度に基づく専門用語抽出」という論文で語られているのでスキップ。
そして MC-value を語る前に、その元になる C-value を語らないと行けない。

C-value のものすごいざっくりした理解

  • コンテンツにおける専門用語って名詞が連続すること多いよね
  • 連続する名詞って入れ子になることあるよね
  • 形態素解析の結果って名詞かどうかわかるよね
  • → できるじゃん!!

C-value の仕組み

例として以下の連続する名詞が上がっているとして…。

トライグラム 統計、トライグラム、単語 トライグラム、クラス トライグラム、単語 トライグラム、トライグラム、トライグラム 抽出、単語 トライグラム 統計、トライグラム、文字 トライグラム

  1. CN = 複合名詞
    1. 例) トライグラム 統計
  2. length(CN) = CNの長さ(構成する単名詞の数)
    1. 例) length(トライグラム 統計) = 2
  3. n(CN) = コーパスにおけるCNの出現回数
    1. 例) n(トライグラム 統計) = 2
  4. t(CN) = CN を含むより長い複合名詞の出現回数
    1. 例) t(トライグラム 統計) = 1
  5. c(CN) = CN を含むより長い複合名詞の種類数
    1. 例) n(トライグラム 統計) = 1
  6. C-value(CN) = (length(CN) - 1) * (n(CN) - (t(CN) / c(CN)))
    1. 例) C-value(トライグラム 統計) = (2 - 1) * (2 - (1 / 1)) = 1

このとき length(CN) = 1 、つまり単名詞のときに数値が 0 になってしまうという問題がある。専門用語は単名詞になることもあるだろう。

MC-value のものすごいざっくりした理解

  • C-value で連続する名詞のスコア計算が出来たけど単名詞…
  • -1 してたものをなくせばいいじゃん!

MC-value の仕組み

例として以下の連続する名詞が上がっているとして…。

トライグラム 統計、トライグラム、単語 トライグラム、クラス トライグラム、単語 トライグラム、トライグラム、トライグラム 抽出、単語 トライグラム 統計、トライグラム、文字 トライグラム

  1. CN = 名詞
    1. 例) トライグラム
  2. length(CN) = CNの長さ(構成する単名詞の数)
    1. 例) length(トライグラム) = 1
  3. n(CN) = コーパスにおけるCNの出現回数
    1. 例) n(トライグラム) = 10
  4. t(CN) = CN を含むより長い複合名詞の出現回数
    1. 例) t(トライグラム) = 7
  5. c(CN) = CN を含むより長い複合名詞の種類数
    1. 例) n(トライグラム) = 5
  6. MC-value(CN) = length(CN) * (n(CN) - (t(CN) / c(CN)))
    1. 例) MC-value(トライグラム) = 1 * (10 - (7 / 5)) = 8.6

論文上では 5.6 と書かれているけどこの数字がどうやって出てきたかわからんかった…。
n(CN) の計算がもしかしたら違うかも。

例1(図2)の場合,MC-value(トライグラム)=(7−7/5) = 5.6である

まとめと感想

そもそも C-value が TF-IDF や FLR と異なり、全ての文字列に対して下準備をする必要がないのと、数えることがメインなので計算量すくなく、データ量をどんどん増やすようなことをしてもお手軽に使えそうな気がする。

論文中でもいい感じに取れるぜ!的なことが書いてあるので、もうちょっと実践的に?入れて様子を見ようかなあと思いましたとさ。

「出現頻度と連接頻度に基づく専門用語抽出」という論文に出てくる FLR について読んだ

以前 TF-IDF について調べた。

TF-IDF ってのを使うと単語の重要度がわかるよって聞いたので調べた | ごみばこいん Blog

まったく違うアプローチをしている重要語を抽出するようなものがないかなーと調べてたら FLR というものに行き着いた。

元の論文

「出現頻度と連接頻度に基づく専門用語抽出」という論文で語られている。

機関リポジトリ内のページ
UTokyo Repository - 東京大学学術機関リポジトリ

関連サイト
専門用語(キーワード)自動抽出システム”のページ

ここで実際どうなるのよ、が試せるっぽいのと、ライブラリが配布されているので組み込むのもまあまあ容易に行けそう。

FLR のものすごいざっくりした理解

  • コンテンツにおける専門用語って名詞が連続すること多いよね
  • 形態素解析の結果って名詞かどうかわかるよね
  • → できるじゃん!!

FLR の仕組み

  1. 単名詞バイグラム、LNnとRNnのリストを作る
    1. [LNn N](#Ln)
    2. [N RNn](#Rn)
    3. 例)トライグラム 統計、トライグラム、単語 トライグラム、クラス トライグラム、単語 トライグラム、トライグラム、トライグラム 抽出、単語 トライグラム 統計、トライグラム、文字 トライグラム
      1. LNn
        1. [単語 トライグラム](3)
        2. [クラス トライグラム](1)
        3. [文字 トライグラム](1)
      2. RNn
        1. [トライグラム 統計](2)
        2. [トライグラム 抽出](1)
  2. 単名詞スコア1 として、連接種類数 LDN(N)とRDN(N) を作る
    1. 種類の数なので、単語の出現頻度にはさほど影響されない
    2. 例)
      1. LDN(トライグラム) = 3
      2. RDN(トライグラム) = 2
  3. 単名詞スコア2 として、連接頻度 LN(N)とRN(N)を作る
    1. 単名詞バイグラムを特徴付けるものとして、全ての出現回数の合計を取る
    2. 例)
      1. LN(トライグラム) = 3 + 1 + 1 = 5
      2. RN(トライグラム) = 2 + 1 = 3
  4. 複合名詞スコア LR(CN) を作る
    1. 専門用語は複合名詞が多いが、その複合名詞の長さによって重要度はかわらない
    2. 単名詞Nに対し、左側スコアFL(N)、右側スコアFR(N)
      1. 単名詞スコア1 か 2 を利用する
      2. 複合名詞 CN = N1, N2, ... NL
      3. 複合名詞に含まれる各単名詞について FL, FR を計算し、相乗平均を取り、CNの長さLの逆数のべき乗を取ったものが LR(CN)
      4. LR(CN) = pow( GeometricMean( (FL(Ni) + 1) * (FR(Ni) + i) ) , 1/2L)
      5. 例)連接頻度をスコアとすると
      6. LR(トライグラム) = pow( (5+1) * (3+1), 1/2 ) = sqrt(6 * 4) ~= 4.90
  5. 出現回数を複合名詞スコアに加える FLR(CN)
    1. f(CN) = CNが単独で出現した頻度、他の複合名詞に含まれていないこと
    2. FLR(CN) = f(CN) * LR(CN)
    3. 例)連接頻度をスコアとすると
      1. FLR(トライグラム) = 3 * 4.90 = 14.70

まとめと感想

名詞に対して、よくでる連続した名詞がスコア高くなるような仕組みでスコアを付けて、それをピックアップできるものだった。
何かに特化した専門的なものを説明するようなコンテンツだとその節があると感じる。(例えばこの記事だと "名詞" は上位にピックアップされるんじゃないかな)

というか、最初に重要語が~みたいな話を書いたけど、「重要とは何か?」という考え方、指標、スコアを設ければそれで重要ってキメになるんだろうな。

  • 出現回数「よく出る単語は重要だ!」
  • TF-IDF「このドキュメントだけよく出るから重要だ!」
  • FLR「この名詞はよくくっついて出るから重要だ!」

ちなみにこの論文では MC-value というものも提案されており、それはまた書く。

PHP で本文抽出したいよね、という

前回の記事: ExtractContent を PHP で書き換えた | ごみばこいん Blog

「今回 PHP に書き換えてみた ExtractContent も含めて、これらの比較は次の記事でやりたい」といいつつ、他のネタが挟まり、ようやくにして比較しました。

比較した結論

お試しにはこちらのリポジトリをどうぞ。
sters/compare-article-extractors: Compare web article extractors.

日本語環境下で難しい話

php-goose 、または php-web-article-extractor では、どちらも内部にストップワード辞書を持っていて、プログラム上で言語を指定、あるいは lang 属性や meta タグから読み取った言語名から、適切なストップワード辞書を選択しています。そうして決まったストップワードの一覧が、文字列にどれくらいあるかを確認して、本文かどうか?といった判断を進めているようです。

ストップワードとはよく使われがちな単語のことで、自然言語処理の前処理として行なうことでデータ量を減らしたり、精度を上げたり出来ます。

自然言語処理における前処理の種類とその威力 - Qiita

例えば英語では「This is a pen.」といったようにスペースで区切られているので「ストップワードの is と a の 2 文字がある!こいつはコンテンツだ!」と簡単に出来るのですが、日本語の場合はそう簡単な話ではありません。日本語はスペース区切りではありませんし、前後にある文字によって、まったく異なる意味合いになったりもしますので、一概に辞書でドバーッと指定することは適切な処理でない可能性が高いです。

じゃあどうするかというと分かち書きの出番なのですが、分かち書きも簡単なものではありません。 Chasen や Mecab といった既存の技術を利用すればお手軽にもできますが php-goose や php-web-article-extractor がそれを利用して日本語対応するかというとちょっと違いそうだなあ、という気持ちです。

php-goose に出したプルリクは諦めてドバーッと処理する形ではありますが、日本語だけ特殊化せざるを得ないので、そうやってみて出してみたものの、なんだかなあと。いやそもそも 元になってる Goose はそういう実装になっていないだろうし…。

そのほかのアプローチ方法

DOM構造や、画面上の位置情報も利用するのはめっちゃ有効的だと思います。
PuppeteerでWebページからメインコンテンツっぽいところを抽出してみる - Qiita

上記の記事の「まとめ」でも出てはいますが、機械学習して取るようにするのもよさそうです。
seomoz/dragnet: Just the facts -- web page content extraction

ExtractContent を PHP で書き換えた

いや Ruby でええやん、 Python でええやん、みたいな話だとは思うのだが、やっぱり PHP でやりたいよね、という一定の需要がギョームで発生してしまったので、 Ruby のコードを見ながら PHP に書き換えた。 Packagist にも登録してあるので composer からどうぞ。

sters/extract-content - Packagist

$url = 'http://labs.cybozu.co.jp/blog/nakatani/2007/09/web_1.html';
$extractor = new \ExtractContent\ExtractContent(file_get_contents($url));
$result = $extractor->analyse();
file_put_contents(__DIR__ . '/result', $result);

// // 抽出結果
// Webページの自動カテゴライズ の続き。
// 前回書いたとおり、パストラックで行っている Web ページのカテゴライズでは、Web ページの本文抽出がひとつの鍵になっています。今回はその本文抽出モジュールを公開しつつ、使っている技法をざっくり解説などしてみます。
// 本モジュールの利用は至極簡単。require して analyse メソッドに解析したい html を与えるだけ。文字コードは UTF-8 です。
// ...

ExtractContent は Cybozu の Nakatani Shuyo さんが2009年に作成したもので、正規表現を主として Web 記事上の本文に関するちょっとの知見が加わることで成り立っている。以下の記事へ。

Webページの本文抽出 (nakatani @ cybozu labs)

今回、移す際に参考にしたのは、もともとのものではなく、Ruby 1.9 に対応したソース。というのもこっちの記事を見つけたのは後で、 Github 上で ExtractContent を先に見つけていたので、まあいっか、と。

mono0x/extractcontent: ExtractContent for Ruby 1.9+

Ruby はチョットヨメルので inject とかわからないメソッドだけドキュメント見つつ脳内補完して、同じような感じの処理になるよう PHP へ書き直した。工夫したところはとくになく、クラスで扱うようにした、チョットテスト書いたくらいで、ほぼそのまま移してきた。Wikipedia と Medium、はてなブログあたりで試してみたところで、おおよそうまくいっているように見えたので、たぶん大丈夫。

ただ、試していて、本文っぽいと判断されるのが 2 つ以上あるような 1 記事ページ(例えば記事中に section タグがあってーとか、そいういう)ではうまく取り出せず、スコアが高くなったほうのみ抽出されてしまう。本文しきい値のようなものを設けてそれを超えていたら、結合して出す、とかしないといけないなあと思う。とはいえ、正確に本文が欲しいのか、その記事中の重要な部分にフォーカスするのか、などなど要件にもよるので、とりあえずはいいんじゃないかなの気持ち。

ちなみに PHP による実装もいたのだが、おそらく上記のようなところで、元々のものには無いオプションが増えていたりでチョットわからなかったので、一から書いた次第。

aoiaoi/ExtractContent: extract content from HTML

なお記事解析、本文抽出について Packagist を調べると他にも 3 つのライブラリが出てくる。ざっくり紹介するとこんな感じ。

今回 PHP に書き換えてみた ExtractContent も含めて、これらの比較は次の記事でやりたい。

TF-IDF ってのを使うと単語の重要度がわかるよって聞いたので調べた

この記事は公開されてから1年以上経過しており、情報が古い可能性があります。

TF-IDF は文章における、単語の重みづけを行うもの。
処理した文章中のうち、ある単語はどの程度重要なものか、出現度合いから重み付けを計算する。
プログラム、機械からは文章の分析っていってもよくわからないので、そのよくわからない特徴をわかるようにするための、数値化する方法の1つ。

前提として TF-IDF には複数の文章を入力する必要がある。

TF-IDF = TF * IDF

TF = 1つの文章において、ある単語の出現回数 / 文章内の単語数
→ 文章1つずつの単語の出現頻度がわかる。

IDF = log( 文章数 / ある単語が出現する文章数 ) + 1
→ 横断的に使われる単語は低い値になる。

簡単な文章を入れつつ実際の数字を計算してみる。

 
step1:文章の入力

文章
リンゴとレモンとレモン
リンゴとミカン

 
step2:単語に分割(ここでは形態素解析し、名詞だけに絞ったとする)

文章 分割した結果
リンゴとレモンとレモン リンゴ レモン レモン
リンゴとミカン リンゴ ミカン

 
step3:TF値、IDF値を計算する

文章 TF:リンゴ TF:レモン TF:ミカン
リンゴとレモンとレモン 1 / 3 = 0.33 2 / 3 = 0.67 0 / 3 = 0
リンゴとミカン 1 / 2 = 0.5 0 / 2 = 0 1 / 2 = 0.5
単語 IDF
リンゴ log(2 / 2) + 1 = 1
レモン log(2 / 1) + 1 = 1.3
ミカン log(2 / 1) + 1 = 1.3

 
step4:TF-IDFを計算する

文章 TF-IDF:リンゴ TF-IDF:レモン TF-IDF:ミカン
リンゴとレモンとレモン 0.33 * 1 = 0.33 0.67 * 1.3 = 0.87 0 * 1.3 = 0
リンゴとミカン 0.5 * 1 = 0.5 0 * 1.3 = 0 0.5 * 1.3 = 0.65

 

この結果からわかることとして。

  • 「リンゴ」は横断的に出現するので値が低くなる。
  • 「レモン」は文章1だけに多く使われるので値に特徴が強く出る。
  • 「ミカン」は文章2だけに使われているが、リンゴと同程度しか使われていないので、特徴が大きく出るわけではない。
  • この2つの文章を切り分けるには レモン の様子を見ればよいらしい。ということで、確かに単語の重要度がわかるよ、っていうのはあっているらしい。

ただ、全ての文章・全ての単語を利用するという計算ロジックの都合、文章を逐次増やして差分計算していくー、というのは出来なさそうなので、そういう用途はむりそうだなー。
例えばこの計算済み TF-IDF の結果に加えて「バナナとレモン」を追加したときに困っちゃうね、という話。この量なら良いけど、もっと文章量が増えてもっと単語量が増えると、毎回計算し直すコストがかかる。

形態素解析した後に結合処理をしてあげると良いかもね

この記事は公開されてから1年以上経過しており、情報が古い可能性があります。

こんにちは、ごみばこです。

MeCab や Kuromoji などで行える形態素解析、すごいですよねー。ただ新しい語が増えたり、なんだりーってしたときに、辞書を更新しないとちょっとだけ直感的ではない分割のされ方になりますよね。まあ、辞書を元に分割の仕方を学習するのでそうなんですが。。
そこに対して mecab-ipadic-neologd という素晴らしいプロジェクトがあって、最近の語もどんどん追加してくれています。

neologd/mecab-ipadic-neologd: Neologism dictionary based on the language resources on the Web for mecab-ipadic

これを使うと Github のサンプルにもかいてありますが「中居正広のミになる図書館」が固有名詞として認識されたりします!(スゴイ)

$ echo "10日放送の「中居正広のミになる図書館」(テレビ朝日系)で、SMAPの中居正広が、篠原信一の過去の勘違いを明かす一幕があった。" | mecab -d /usr/local/lib/mecab/dic/mecab-ipadic-neologd
10日    名詞,固有名詞,一般,*,*,*,10日,トオカ,トオカ
放送    名詞,サ変接続,*,*,*,*,放送,ホウソウ,ホーソー
の      助詞,連体化,*,*,*,*,の,ノ,ノ
「      記号,括弧開,*,*,*,*,「,「,「
中居正広のミになる図書館        名詞,固有名詞,一般,*,*,*,中居正広のミになる図書館,ナカイマサヒロノミニナルトショカン,ナカイマサヒロノミニナルトショカン
」      記号,括弧閉,*,*,*,*,」,」,」
(      記号,括弧開,*,*,*,*,(,(,(
テレビ朝日      名詞,固有名詞,組織,*,*,*,テレビ朝日,テレビアサヒ,テレビアサヒ
系      名詞,接尾,一般,*,*,*,系,ケイ,ケイ
)      記号,括弧閉,*,*,*,*,),),)
で      助詞,格助詞,一般,*,*,*,で,デ,デ
、      記号,読点,*,*,*,*,、,、,、
SMAP    名詞,固有名詞,一般,*,*,*,SMAP,スマップ,スマップ
の      助詞,連体化,*,*,*,*,の,ノ,ノ
中居正広        名詞,固有名詞,人名,*,*,*,中居正広,ナカイマサヒロ,ナカイマサヒロ
が      助詞,格助詞,一般,*,*,*,が,ガ,ガ
、      記号,読点,*,*,*,*,、,、,、
篠原信一        名詞,固有名詞,人名,*,*,*,篠原信一,シノハラシンイチ,シノハラシンイチ
の      助詞,連体化,*,*,*,*,の,ノ,ノ
過去    名詞,副詞可能,*,*,*,*,過去,カコ,カコ
の      助詞,連体化,*,*,*,*,の,ノ,ノ
勘違い  名詞,サ変接続,*,*,*,*,勘違い,カンチガイ,カンチガイ
を      助詞,格助詞,一般,*,*,*,を,ヲ,ヲ
明かす  動詞,自立,*,*,五段・サ行,基本形,明かす,アカス,アカス
一幕    名詞,一般,*,*,*,*,一幕,ヒトマク,ヒトマク
が      助詞,格助詞,一般,*,*,*,が,ガ,ガ
あっ    動詞,自立,*,*,五段・ラ行,連用タ接続,ある,アッ,アッ
た      助動詞,*,*,*,特殊・タ,基本形,た,タ,タ
。      記号,句点,*,*,*,*,。,。,。
EOS

(サンプルそのまま)

もうこれを使ったら良いんじゃないかなあと思います。

が!やっぱり実際に様々な文言をいれていくと、そういう分け方になるのねーだったり、それはちょっといらないんだよなー、というパターンは発生します。なんていうんでしょうか、特徴語抽出とか、なんかそういうときにちょっと不便になるんですよきっと。
そんなころに、形態素解析した後に品詞を見て結合処理をすると良い、と風の噂で聞いたので試しました。

※ここからは都合によって mecab-ipadic を使っています。 neologd の結果ではないので注意。※

まずは結合処理無しのものを見てみましょう。

Target: 「3Dブロー方式」でスピード乾燥。のびる、ひろがる「ふとん乾燥アタッチメント」

[   ' BOS/EOS *',
    '「 記号 括弧開',
    '3 名詞 数',
    'D 名詞 一般',
    'ブロー 名詞 一般',
    '方式 名詞 一般',
    '」 記号 括弧閉',
    'で 助詞 格助詞',
    'スピード 名詞 一般',
    '乾燥 名詞 サ変接続',
    '。 記号 一般',
    ' BOS/EOS *',
    ' BOS/EOS *',
    'のびる 動詞 自立',
    '、 記号 一般',
    'ひろがる 動詞 自立',
    '「 記号 括弧開',
    'ふとん 動詞 自立',
    '乾燥 名詞 サ変接続',
    'アタッチメント 名詞 一般',
    '」 記号 括弧閉',
    ' BOS/EOS *']

短い文章ですが、「3」「D」と分かれていたり、「3」「D」「ブロー」「方式」はくっついていたほうがいいんじゃないかなあ、などなど。もうちょっと、なあ、という部分がありそうです(主観)
こういったものをはじめ、色々な文章をいれつつ、こっちのほうがいいなー、これはためだなー、などと、目視で結合処理を増やしていったロジックがこれです。
 

形態素解析の結果を1語ずつループし、以下の処理をしていく

  1. ()[]’" が出てきたら
    1. 次のループへ※
  2. 記号が出てきたら
    1. ただし、今が半角で構成されている、かつ、1つ前も半角であれば
      1. 1つ前に今の語を結合
      2. 次のループへ
    2. 次のループへ※
  3. 今が半角で構成されている、かつ、1つ前も半角であれば
    1. 1つ前に今の語を結合する
    2. 次のループへ
  4. 1つ前が接頭詞なら
    1. 1つ前に今の語を結合する
    2. 品詞を今のものに置き換える
    3. 次のループへ
  5. 今が接尾なら
    1. 1つ前に今の語を結合する
    2. 次のループへ
  6. 1つ前が名詞、かつ、形容動詞語幹なら
    1. 今が助詞、助動詞、副詞、非自立のいずれか
      1. 次のループへ※
    2. 新しい語として追加
    3. 次のループへ
  7. 今が非自立なら
    1. 1つ前に結合する
    2. 次のループへ
  8. 今が終助詞なら
    1. 1つ前に結合する
    2. 次のループへ
  9. 1つ前が名詞、かつ、今が名詞なら
    1. 1つ前が接尾なら
      1. 新しい語として追加
      2. 次のループへ
    2. 今が数なら
      1. 新しい語として追加
      2. 次のループへ
    3. 1つ前に結合する
    4. 次のループへ
  10. 1つ前が動詞または助動詞、かつ、今が動詞または助動詞なら
    1. 1つ前に結合する
    2. 次のループへ
  11. 1つ前が動詞または助動詞、かつ、今が助詞なら
    1. 1つ前に結合する
    2. 次のループへ
  12. 1つ前が形容詞、かつ、今が助動詞なら
    1. 1つ前に結合する
    2. 次のループへ
  13. 1つ前が助詞類接続、かつ、今が動詞なら
    1. 1つ前に結合する
    2. 次のループへ
  14. 1つ前が助詞、かつ、今が助詞なら
    1. 1つ前に結合する
    2. 次のループへ
  15. 今が接頭詞、あるいは、助詞類接続、あるいは、数なら
    1. 新しい語として追加
    2. 次のループへ
  16. 今が助詞、あるいは、助動詞、あるいは、非自立なら
    1. 次のループへ※
  17. 新しい語として追加
  18. 次のループへ

 

それではこのロジックで同じ文章を試してみましょう。

Target: 「3Dブロー方式」でスピード乾燥。のびる、ひろがる「ふとん乾燥アタッチメント」

[   '3Dブロー方式 名詞 数',
    'スピード乾燥 名詞 一般',
    'のびる 動詞 自立',
    'ひろがる 動詞 自立',
    'ふとん 動詞 自立',
    '乾燥アタッチメント 名詞 サ変接続']

結合したので、品詞や種類などはもう完全に意味をなしていませんが、先ほどよりも、なんだか自然な感じで単語が取れているのではないでしょうか?(主観)

というわけで結合するとよくなりそうだよ、って紹介でした。

sentencepiece ニューラルネット時代における新しトークナイザ

この記事は公開されてから1年以上経過しており、情報が古い可能性があります。

こんにちは、ごみばこです。

自然言語処理をするとき n-gram や形態素解析などの処理をし、文章から語にすることがよくあると思います。 n-gram はシンプルですが、分け具合、分け方によって情報量がなくなってしまうこともありますし、データ量も大きくなりがちです。形態素解析は簡単お手軽そうに見えますが、裏の処理はなかなかコストが高く、ちりもつもればなんとやら。

というところで、新しい選択肢の sentencepiece だそうです。

https://github.com/google/sentencepiece

私の知識や理解力の不足から雑な説明にはなってしまいますが…。sentencepiece が行うことは次のことです。

1. 文章をよろしくニューラルネットワークで処理し語に分割するためのモデルを生成
2. そのモデルを使って文章を分割する
3. n-gram や 形態素解析のような何かが得られる!やったー!

この何かで機械翻訳をすると n-gram や形態素解析などと比べて劣ることなく十分に効果を発揮した、との記録もあります。

https://github.com/google/sentencepiece#results-bleu-scores

学習する、という初期コストがかかることについては、形態素解析も同じですが、その後の分割することについては、形態素解析よりもコスト低く行うことができます。また、語彙も形態素解析で扱う辞書に比べて遥かに小さくできるそうで、パフォーマンスよし、効果よし、ととても魅力的に見えますね!

実際に収録されているサンプルを試してみました。

環境構築は docker でお手軽にやってしまいます。

$ docker run -it -d centos:latest
$ docker ps

$ docker attach ...

コンテナの中で必要なツールの準備を進めていきます。

# yum install git make gcc-c++ autoconf automake libtool protobuf protobuf-devel
# git clone https://github.com/google/sentencepiece.git
# cd sentencepiece
# ./autogen.sh
# ./configure
# make

ここまでで実行ファイルが作られます。それでは文章から学習をしてみます。
サンプルとして吾輩は猫である...が収録されているので、これを使ってみます。

# src/spm_train --input=data/wagahaiwa_nekodearu.txt --model_prefix=neko --vocab_size=8000 --model_type=unigram
unigram_model_trainer.cc(494) LOG(INFO) Starts training with :
input: "data/wagahaiwa_nekodearu.txt"
model_prefix: "neko"
model_type: UNIGRAM
vocab_size: 8000
character_coverage: 0.9995
input_sentence_size: 10000000
mining_sentence_size: 2000000
training_sentence_size: 10000000
seed_sentencepiece_size: 1000000
shrinking_factor: 0.75
num_threads: 16
num_sub_iterations: 2
max_sentencepiece_length: 16
split_by_unicode_script: true
split_by_whitespace: true

trainer_interface.cc(109) LOG(INFO) Loading corpus: data/wagahaiwa_nekodearu.txt
trainer_size=0ace.cc(126) LOG(INFO) Loading: ▁吾輩は猫である
trainer_interface.cc(148) LOG(INFO) Loaded 2246 sentences
trainer_interface.cc(166) LOG(INFO) all chars count=182657
trainer_interface.cc(173) LOG(INFO) Done: 99.9502% characters are covered.
trainer_interface.cc(181) LOG(INFO) alphabet size=2507
trainer_interface.cc(211) LOG(INFO) Done! 2246 sentences are loaded
unigram_model_trainer.cc(121) LOG(INFO) Using 2246 sentences for making seed sentencepieces
unigram_model_trainer.cc(149) LOG(INFO) Making suffix array...
unigram_model_trainer.cc(153) LOG(INFO) Extracting frequent sub strings...
unigram_model_trainer.cc(204) LOG(INFO) Initialized 34678 seed sentencepieces
trainer_interface.cc(215) LOG(INFO) Tokenizing input sentences with whitespace: 2246
trainer_interface.cc(224) LOG(INFO) Done! 2193
unigram_model_trainer.cc(513) LOG(INFO) Using 2193 sentences for EM training
unigram_model_trainer.cc(529) LOG(INFO) EM sub_iter=0 size=19246 obj=303.949 num_tokens=81388 num_tokens/piece=4.22883
unigram_model_trainer.cc(529) LOG(INFO) EM sub_iter=1 size=17546 obj=276.547 num_tokens=81681 num_tokens/piece=4.65525
unigram_model_trainer.cc(529) LOG(INFO) EM sub_iter=0 size=13133 obj=282.685 num_tokens=85486 num_tokens/piece=6.50925
unigram_model_trainer.cc(529) LOG(INFO) EM sub_iter=1 size=13089 obj=279.841 num_tokens=85549 num_tokens/piece=6.53595
unigram_model_trainer.cc(529) LOG(INFO) EM sub_iter=0 size=9814 obj=292.604 num_tokens=91023 num_tokens/piece=9.27481
unigram_model_trainer.cc(529) LOG(INFO) EM sub_iter=1 size=9813 obj=289.388 num_tokens=91093 num_tokens/piece=9.28289
unigram_model_trainer.cc(529) LOG(INFO) EM sub_iter=0 size=8799 obj=294.594 num_tokens=93339 num_tokens/piece=10.6079
unigram_model_trainer.cc(529) LOG(INFO) EM sub_iter=1 size=8799 obj=293.481 num_tokens=93368 num_tokens/piece=10.6112
trainer_interface.cc(284) LOG(INFO) Saving model: neko.model
trainer_interface.cc(293) LOG(INFO) Saving vocabs: neko.vocab

学習が完了しました。適当な文章を入れて、分割の様子を見てみましょう。

# echo "吾輩は猫である。名はまだ無い。" | src/spm_encode --model=neko.
▁吾輩は 猫 である 。 名 はまだ 無い 。

# echo "吾輩は箱が好きで好きでたまらない。" | src/spm_encode --model
▁吾輩は 箱 が 好き で 好き で たま らない 。

# echo "ニャーと声を出せばで飯が提供され、ニャッと声を出せば頭を撫でられる。" | src/spm_encode --model=neko.model
▁ ニ ャ ー と 声を 出 せば で 飯 が 提 供 され 、 ニ ャ ッ と 声を 出 せば 頭 を撫 で ら れる 。

私自信の知識と能力が追いついていないのもあって、どんなところで使えそうなのかあまりイメージわかないですが、トークナイザの1つの選択肢として考えたいですねー!

関連リンク)
http://qiita.com/taku910/items/7e52f1e58d0ea6e7859c
どういった背景から作られたかが解説されています!