「宮沢みちひの短歌自動生成」を支える技術

2017年の夏ごろにつくったWebアプリ的なものをこのほどNuxt.jsで動かすかたちにしてリニューアルしました。見た目もAPI周りの処理も変わっていないのですが、何かを書きたい気分なので記事にしてみます。

宮沢みちひについて

宮沢みちひは、入力されたフレーズを使って短歌っぽい文字列を生成するWebアプリに、ヴァーチャル歌人という名目で名前をつけたものです。ただ、短歌自動生成というのはほぼ釣りみたいなもので、必ずしも57577のきれいな短歌に見える文字列が生成できるわけではありません。

以下のようなライブラリを使っています。

  • Nuxt.js + Express(フレームワーク)
  • bootstrap(CSS。ほかのライブラリの都合でUmiの3.3.7を使っています)
  • node-sqlite3(データベース)
  • kuromoji.js(形態素解析)
  • messaging-api-line(LINE bot機能)

短歌を自動生成する方法

「短歌 自動生成」で検索すると、短歌自動生成装置「犬猿」(星野しずる)が上位にヒットします。これは佐々木あららという歌人の方が書いた簡単なプログラムで、あらかじめ用意された構文にしたがって名詞や修飾語・述語を組み合わせることで短歌を生成するものです。簡単なわりによくできた短歌を生成するので、その界隈ではけっこう有名だったりします。

ガチ勢向けのアプローチとしては、文字単位のRNNなりLSTMなりを重ねて文字列を出力するという方法があるでしょう。「LDA(潜在的ディリクレ配分法)を使って単語のトピックが揃っているものだけを選り分ける」といった研究もあり、なかなか夢があります。Tensorflow.jsやそのラッパーライブラリ(ml5.jsなど)の登場で、Webアプリ上で深層学習を用いた文字列生成をおこなうのも手軽になってきた感があり、今後そんなプロダクトもぽつぽつ出てくるのかもしれません。

しかしながら、現状では学習済みモデルを用意するのにPythonを書かなきゃいけなかったりする点が個人的に手間であり、「文字列生成するならマルコフ連鎖でいいんじゃね?」という安易な発想から、宮沢みちひでは単語をマルコフ連鎖させて文字列を生成しています。

形態素解析と文字列の生成

短歌は音数を考慮しなければいけないので、入力されたフレーズを使いつつ短歌を生成するにはそのフレーズの音数を数えなければいけません。そこで、宮沢みちひではkuromoji.jsを使って形態素解析をおこなって入力されたフレーズの読みを取得しています。

受け取ったフレーズの音数を数えたあとで、その音数に応じてフレーズをあらかじめ決められた位置に詠み込み、前後に適当な文字列を付け足して短歌っぽいものをつくります。たとえば「これはペンです」というフレーズを与えた場合、このフレーズは7音なので57577の2句目に詠み込まれます。また、フレーズの最後の語は「です」なので、後続する3句目以降は「です」からマルコフ連鎖させて生成します。ただし、このとき後続すべき19音の文字列をまるごとマルコフ連鎖で生成してしまうと、57577の切れ目をあまり感じさせない文字列ができてしまうことがあり、あまり短歌っぽくなりません。そこで、付け足す文字列はあまり長いものを一度に生成しないようにして、ある程度の「句切れ」が感じられるように調節しています。

LINEから送られた画像の認識

ノリでLINE botもつくりました。LINEを通じて使うなら送られた画像をもとに返信する機能を付けたいと考え、そのへんの処理も実装しました。MicrosoftのAPIを叩いているだけですが。

async function replyImageMessage (events) {  
        const message = events.message;  
        // 画像の取得  
        const bin = await client.retrieveMessageContent(message.id).then(buffer => {  
            return buffer;  
        });  
        // 画像解析  
        const response = await axios.request({  
            url: 'https://westcentralus.api.cognitive.microsoft.com/vision/v1.0/analyze',  
            method: 'post',  
            headers: {  
                'Content-Type': 'application/octet-stream',  
                'Ocp-Apim-Subscription-Key': computerVisonSubKey,  
            },  
            params: {  
                visualFeatures: "description",  
                language: "en"  
            },  
            data: bin,  
            timeout: 3000,  
            responseType: 'json'  
        }).then(json => {  
            console.log(json.data);  
            return _.sample(json.data.description.tags)  
        }).catch(err => {  
            console.error(err.message);  
            return null;  
        });  
        // タグの翻訳  
        const xml = await axios.request({  
            url: 'https://api.microsofttranslator.com/V2/Http.svc/Translate',  
            method: 'get',  
            headers: {  
                'Ocp-Apim-Subscription-Key': translateSubKey  
            },  
            params: {  
                text: response,  
                from: "en",  
                to: "ja"  
            },  
            timeout: 3000,  
            responseType: 'text'  
        }).then(json => {  
            console.log(json.data);  
            return json.data;  
        }).catch(err => {  
            console.error(err.message);  
            return null;  
        });  
}  

messaging-api-lineが返す画像(buffer)をそのまま画像認識APIに投げれば、写っている可能性のあるもののタグが英語で返ってきます(日本語は非対応だったはず)。そのなかから適当にひとつだけ単語を選んで、翻訳APIに渡して日本語の文字列を得ます。あとは、そうして得た文字列をフレーズとして自前のAPIに渡して短歌っぽいやつをつくるだけです。生成した短歌っぽいやつを応答テキストとしてリプライします。

ただ、この方法で得られる日本語のフレーズは「人間」とか「犬」とか「車」といった雑で短い単語ばかりなので、できあがる短歌っぽい文字列もとても雑なものになります。

課題

  • kuromoji.jsが遅い。長い文字列を渡すとタイムアウトして死ぬ。
  • そんなに短歌っぽくならない。まあ、しょうがない。