さて、前回記事(TinySegmenterで分かち書き(簡易形態素解析)。)で文章を言葉の最小単位に分割する事が出来ました。

今回はそれらをランダムに再構成して新たに文章を作ってみます。

マルコフ連鎖

Wikipediaにはこんな風に書かれています。

マルコフ連鎖(マルコフれんさ)とは、確率過程の一種であるマルコフ過程のうち、とりうる状態が離散的(有限または可算)なもの(離散状態マルコフ過程)をいう。また特に、時間が離散的なもの(時刻は添え字で表される)を指すことが多い(他に連続時間マルコフ過程というものもあり、これは時刻が連続である)。マルコフ連鎖は、未来の挙動が現在の値だけで決定され、過去の挙動と無関係である(マルコフ性)。各時刻において起こる状態変化(遷移または推移)に関して、マルコフ連鎖は遷移確率が過去の状態によらず、現在の状態のみによる系列である。特に重要な確率過程として、様々な分野に応用される。

ふむ。さっぱり分からん。試しにこの文章をマルコフ連鎖を使って要約してみます。

「各時刻が離散的なもの に関して起こる状態変化 。」

なるほど。やっぱり分からん。けれどもこのマルコフ性(次の状態は過去に依存するのではなく現在の状態によってのみ決まる)を利用することでこの様に文章の要約などを行うことが出来る様です。

まずは公開されているマルコフ連鎖の要約プログラムを参考にして試してみることにします。

文章要約の仕組み

様々な方がサンプルを公開してくれていますが、以下の記事が分かりやすかったので参考にさせて頂きました。

はてブのホッテントリのタイトルを要約してWebの今を見つめる - Qiita

文章要約の仕組みは大まかに言うとこんな感じです。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//tinysegmenterとtextファイルを読み込む
var segmenter = require('./lib/tiny_segmenter.js');
var data = require('fs').readFileSync('./lib/sample.txt', 'utf-8');
//形態素解析後辞書に追加、マルコフ連鎖を使いシャッフルする
var segs = segmenter.segment(data);
var dic = makeDic(data);
console.log(dic);
var sentence = makeSentence(dic);
console.log(sentence);
//makeDic() で辞書に追加
function makeDic(morphemes){
 //ここに辞書を作る処理
}
// makeSentence()文章をシャッフル
function makeSentence(morpheme){
 //ここに文章をシャッフルする処理
}

前回記事で以下の様な文章を用意しました。

1
2
3
私の名前はポンダッドです。
私は双子のパパです。
ポンダッドは双子が大好きです。

これを(簡易)形態素解析により分かち書きします。

1
2
3
4
$ node app.js
私 | の | 名前 | は | ポンダッド | です | 。
| 私 | は | 双子 | の | パパ | です | 。
| ポンダッド | は | 双子 | が | 大好き | です | 。

上記の「辞書を作る処理」を行うことでこの様に配列の中に格納します。

1
2
3
4
5
6
7
8
9
10
11
{ _BOS_: [ '私', '私', 'ポンダッド' ],
'私': [ 'の', 'は' ],
'の': [ '名前', 'パパ' ],
'名前': [ 'は' ],
'は': [ 'ポンダッド', '双子', '双子' ],
'ポンダッド': [ 'です', 'は' ],
'です': [ '_EOS_', '_EOS_', '_EOS_' ],
'双子': [ 'の', 'が' ],
'パパ': [ 'です' ],
'が': [ '大好き' ],
'大好き': [ 'です' ] }

この辞書を元にランダム「文書をシャッフルする処理」をすることで文章を生成します。

1
2
$ node app.js
私は双子が大好きです。

ポイントはランダムに文章を再構成する為の辞書を作ることです。「現在の状態」から予測される形態素(文章の最小単位)をこの辞書から推測する訳ですね。

上記の例では「双子」の後に続く形態素は「の」か「が」であると推測されます。

また、文頭は「私」か「ポンダッド」であること、この2つのうちでも「私」の頻度が高いことが分かります。

文末はサンプルテキストが皆「です」で終わっているので「です」になっています。

辞書を作る

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
//makeDic() で辞書に追加
function makeDic(morphemes){
morphemes = nonoise(morphemes);
var lines = morphemes.split("。");
var morpheme = new Object();
for(var i = 0; i <= lines.length-1; i++){
var words = segmenter.segment(lines[i]);
if(! morpheme["_BOS_"] ){morpheme["_BOS_"]=new Array();}
if(words[0]){morpheme["_BOS_"].push(words[0])};//文頭
for(var w=0; w<=words.length-1; w++){
var now_word = words[w];//今の単語
var next_word = words[w+1];//次の単語
if(next_word == undefined){//文末
next_word = "_EOS_"
}
if(! morpheme[now_word] ){
morpheme[now_word]=new Array();
}
morpheme[now_word].push(next_word);
if(now_word == "、"){//「、」は文頭として使える。
morpheme["_BOS_"].push(next_word);
}
}
}
return morpheme;
}

元の文章の「。」を区切りとして一度文字の塊として配列に入れた後、形態素に続く形態素を仕分けていきます。

これによって「双子」の後に続く形態素「の」と「が」を格納します。

TinySegmenterでは品詞の解析は行わないので、シンプルに元の文章の先頭にある形態素を「文頭」に、最後にある形態素を「文末」に設置します。

文章をシャッフルする

1
2
3
4
5
6
7
8
9
10
11
12
13
// makeSentence()文章をシャッフル
function makeSentence(morpheme){
var now_word = ""
var morphemes = ""
now_word = morpheme["_BOS_"][Math.floor( Math.random() * morpheme["_BOS_"].length )];
morphemes += now_word;
while(now_word != "_EOS_"){
now_word = morpheme[now_word][Math.floor( Math.random() * morpheme[now_word].length )];
morphemes += now_word;
}
morphemes = morphemes.replace(/_EOS_$/,"。")
return morphemes;
}

「文頭」をランダムに接ししたのち、その「現在の状態」に当てはまる次の形態素をランダムに当てはめていきます。「文末」の形態素にたどり着いたら文章が完成です。

文章の長さとシャッフルの回数を決める

出来上がりの文章の長さとシャッフルの回数を指定します。

1
2
3
4
5
6
7
//文章の長さとシャッフル回数を定義する
for(var i=0; i<=40; i++){
output = makeSentence(dic).replace(/\n/g,"");
if(Math.abs(8 - output.length) < Math.abs (8 - sentence.length)){
sentence = output
}
}

生成された文章の長さを指定することで大分印象が変わりますのでここで調整します。

形態素のノイズ除去

辞書を作る前に鉤括弧やダッシュマークなど、通常の文章に混ざると違和感が出そうなものは取り除いておきます。

1
2
3
4
5
6
7
8
//nonoise()ノイズ除去
function nonoise(morphemes){
morphemes = morphemes.replace(/\n/g,"。");
morphemes = morphemes.replace(/[\?\!?!]/g,"。");
morphemes = morphemes.replace(/[-||::・]/g,"。");
morphemes = morphemes.replace(/[「」()\(\)\[\]【】]/g," ");
return morphemes
}

サンプルコード

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
//tinysegmenterとtextファイルを読み込む
var segmenter = require('./lib/tiny_segmenter.js');
var data = require('fs').readFileSync('./lib/sample.txt', 'utf-8');
//形態素解析後辞書に追加、マルコフ連鎖を使いシャッフルする
var segs = segmenter.segment(data);
var dic = makeDic(data);
console.log(dic);
var sentence = makeSentence(dic);
console.log(sentence);
//文章の長さとシャッフル回数を定義する
for(var i=0; i<=40; i++){
output = makeSentence(dic).replace(/\n/g,"");
if(Math.abs(8 - output.length) < Math.abs (8 - sentence.length)){
sentence = output
}
}
//makeDic() で辞書に追加
function makeDic(morphemes){
morphemes = nonoise(morphemes);
var lines = morphemes.split("。");
var morpheme = new Object();
for(var i = 0; i <= lines.length-1; i++){
var words = segmenter.segment(lines[i]);
if(! morpheme["_BOS_"] ){morpheme["_BOS_"]=new Array();}
if(words[0]){morpheme["_BOS_"].push(words[0])};//文頭
for(var w=0; w<=words.length-1; w++){
var now_word = words[w];//今の単語
var next_word = words[w+1];//次の単語
if(next_word == undefined){//文末
next_word = "_EOS_"
}
if(! morpheme[now_word] ){
morpheme[now_word]=new Array();
}
morpheme[now_word].push(next_word);
if(now_word == "、"){//「、」は文頭として使える。
morpheme["_BOS_"].push(next_word);
}
}
}
return morpheme;
}
//nonoise()ノイズ除去
function nonoise(morphemes){
morphemes = morphemes.replace(/\n/g,"。");
morphemes = morphemes.replace(/[\?\!?!]/g,"。");
morphemes = morphemes.replace(/[-||::・]/g,"。");
morphemes = morphemes.replace(/[「」()\(\)\[\]【】]/g," ");
return morphemes
}
// makeSentence()文章をシャッフル
function makeSentence(morpheme){
var now_word = ""
var morphemes = ""
now_word = morpheme["_BOS_"][Math.floor( Math.random() * morpheme["_BOS_"].length )];
morphemes += now_word;
while(now_word != "_EOS_"){
now_word = morpheme[now_word][Math.floor( Math.random() * morpheme[now_word].length )];
morphemes += now_word;
}
morphemes = morphemes.replace(/_EOS_$/,"。")
return morphemes;
}

まとめ

当初の目的である「私は双子が大好きです。」は3〜4回生成したら出現しました。まずは目的達成というところです。

細かい計算式は全く分かっていないのですが、大まかな仕組みが少し分かってきたので自作の人工無能に組み込んでみたいですね。