TypeScript + TinySegmenterでマルコフ連鎖
ちょっとNLUまわりを触ってみたくなったので、いろいろ手を出してみました。 マルコフ連鎖とは? こういうのはとりあえずWikipediaから引用しておきます。 マルコフ連鎖は、未来の挙動が現在の値だけで決定され、過去の挙 […]
目次
ちょっとNLUまわりを触ってみたくなったので、いろいろ手を出してみました。
マルコフ連鎖とは?
こういうのはとりあえずWikipediaから引用しておきます。
マルコフ連鎖は、未来の挙動が現在の値だけで決定され、過去の挙動と無関係である(マルコフ性)。各時刻において起こる状態変化(遷移または推移)に関して、マルコフ連鎖は遷移確率が過去の状態によらず、現在の状態のみによる系列である。特に重要な確率過程として、様々な分野に応用される。
https://ja.wikipedia.org/wiki/%E3%83%9E%E3%83%AB%E3%82%B3%E3%83%95%E9%80%A3%E9%8E%96
あまりよくわかっていないのですが、要約や文章の再構築などに使えそうだという話なので、体当たりで触ってみます。
TinySegmenterとは?
TinySegmenterはJavascriptだけ書かれた極めてコンパクトな日本語分かち書きソフトウェアです。
MeCabの開発者工藤氏が作成された様子です。(配布元サイト)
セットアップ
配布元サイトからJSファイルをDLする以外に、npmに第三者がミラーとして公開されているものも存在します。
npmからいれる場合は、npm i -S tiny-segmenter
で追加できます。
実装
過去にマルコフ連鎖の実装をJSで試された方の記事がありました。
これを元にTypeScriptで動くように書き換えます。
コード
data.ts
テキストファイルの量が大きくなったので切り出しました。青空文庫から「吾輩は猫である」を利用しています。
export default `
吾輩わがはいは猫である。名前はまだ無い。
どこで生れたかとんと見当けんとうがつかぬ。
...
`
index.ts
こちらが書き直したものです。せっかくなのでクラス化しましたが、やってみたかっただけなので特に意味はありません。
ユニットテストとかを考えると関数のままの方がよかったかも・・・?
import 'tslib'
import data from './data'
// tinysegmenterとtextファイルを読み込む
const Segmenter = require('tiny-segmenter')
interface Morpheme {
[key: string]: string[];
}
class MarkovChain {
private dict: Morpheme
constructor(data: string) {
this.dict = this.makeDic(data)
}
private nonoise (morphemes: string): string {
morphemes = morphemes.replace(/\n/g, '。')
morphemes = morphemes.replace(/[\?\!?!]/g, '。')
morphemes = morphemes.replace(/[-||::・]/g, '。')
morphemes = morphemes.replace(/[「」()\(\)\[\]【】]/g, ' ')
return morphemes
}
private makeDic (data: string): Morpheme {
const morphemes = this.nonoise(data)
const lines = morphemes.split('。')
let morpheme: Morpheme = {}
for (let i = 0; i <= lines.length - 1; i++) {
let words = segmenter.segment(lines[i])
if (!morpheme['_BOS_']) { morpheme['_BOS_'] = [] }
if (words[0]) { morpheme['_BOS_'].push(words[0]) };// 文頭
for (let w = 0; w <= words.length - 1; w++) {
let nowWord = words[w]// 今の単語
let nextWord = words[w + 1]// 次の単語
if (nextWord === undefined) { // 文末
nextWord = '_EOS_'
}
if (!morpheme[nowWord]) {
morpheme[nowWord] = []
}
morpheme[nowWord].push(nextWord)
if (nowWord === '、') { // 「、」は文頭として使える。
morpheme['_BOS_'].push(nextWord)
}
}
}
return morpheme
}
public makeSentence (): string {
const morpheme: Morpheme = this.dict
let nowWord = ''
let morphemes = ''
nowWord = morpheme['_BOS_'][Math.floor(Math.random() * morpheme['_BOS_'].length)]
morphemes += nowWord
while (nowWord !== '_EOS_') {
nowWord = morpheme[nowWord][Math.floor(Math.random() * morpheme[nowWord].length)]
morphemes += nowWord
}
morphemes = morphemes.replace(/_EOS_$/, '。')
return morphemes
}
}
let segmenter = new Segmenter()
const makeResponse = (data: string): string => {
// 形態素解析後辞書に追加、マルコフ連鎖を使いシャッフルする
const client = new MarkovChain(data)
return client.makeSentence()
}
console.log(makeResponse(data))
動かす
早速動かしてみます。
$ ./node_modules/.bin/ts-node index.ts
君一夫多妻主義もなくっちゃあ いいよ 。
小督こごうの系統的でもないした。
本論はかえって来るにやら、少々軽侮の哲学者も出来ますか は随分妙だね 。
それも落ちついて、御話は僕は迷亭は、朝鮮仁参を煖あたないくらい小さい子の日記などはないです こない。
主人の関係なく、口はどを出す。
苟かりそめに、全力を除いてごたえが事実は延命息災の寝言ね。
まだなかなか健気けなげ込んでも知らねえ 今日こんにち交通が行ったずさえて、とうて、老梅君はそう手軽には直ちには利口だぜ、どうでも這裏しゃりの話はたしか暮と云います といいが言い付けて行かない。
主人はない、アーキミジスの事さ。
これも耳もねえ。
すでに髭ひげを怒おこる、馬鹿な いいじゃないんよ談判の太刀たちあおいのです と東風君の名じゃないと大変な黒木綿くろもめんのごとく考える気遣きづかいは出来て談じましょう。
悪口わるには決して誰に我慢のようなものの、もうやめに作ったの至境を公けにする。
なかなかな感じになりました。
元のデータと文章をシャッフルする部分、それぞれチューニングしていく必要がありそうですので、このあたりは今後の課題です。
おまけ
元データをとある番組から引用した結果、こうなりました。
$ ./node_modules/.bin/ts-node libs/dev.ts
ロー入っちゃってる時点でどこ行くの大泉ですよ ブリーフ。
痔覚に初陣。
オール。
寝技のかよ。
九州もっと見たかったぁ。
帰りたいんだからパンツ一生どうでしょ。
どーも いっぱいいんじゃん。
雪面の大泉で。
なんでも出ません。
ニャンですね トップガンみたい。
聞いてよ。
お前の跡だな暑さ。
バカだよ。
すごい元気です。
鹿部は裸だ。
デブでつくってやろうします。
ロビンソンもないよ。
九州もっと見たかった。
寝れない 直で君ら何やってもうウィリーさ。
ウソだからです。
シカ。
オール。
・・・ちょいちょい言ってそうなワードが混ざってる気がします。