形態素解析を用いて、夢川ゆいのユメ語録を再現してみる試み
ユメークリスマス。saken649です。
プリッカソン Advent Calendar 2018の22日目です。
プリッカソンのアドベントカレンダー記事2つ目となりました。
7日目にプリパラ ULTRA MEGAMIXのレビューをやりましたので、ご興味ある方はこちらも合わせてどうぞ。
また2018年のアドベントカレンダーとしては3つ目の記事となります。
先日会社のアドベントカレンダーに参加させて頂いて、こんな記事を書いてましたので、ご興味ある方はこちらも合わせてどうぞ。
今回は、技術系かつプリティーシリーズに関連する記事を書いてみます。
【やる気!元気!もくじ〜】
テーマ
テーマは「形態素解析を用いて、夢川ゆいのユメ語録を再現してみる試み」です。
毎回プリッカソンに参加させて頂くたびに、個人プロジェクトである「プリスタグラムのパクリ」を進めているのですが、今回は趣向を変えてみました。
プリスタグラムのパクリについては、いずれ改めて記事にしてみようと思います。 ずっとこればっかりやってると疲れるから、というわけではありません
本題に戻りまして。
今回は、文章を入力したら、アイドルタイムプリパラの主人公の片割れである「夢川ゆい」ちゃんのあの独特な喋り方っぽく変換して返す、というちょっとしたアプリを作ってみます。
何かと言葉の頭に「 ユメ 」を付けて喋る、アレです。
夢川ゆい (ゆめかわゆい)とは【ピクシブ百科事典】
なんで急にそんなテーマを?というと、単純に、アイドルタイムプリパラを見終えて数ヶ月経った今になって、非常にクセになってきてしまったからです(理由になっていない
How To ユメ
さて、南みれぃちゃんの「ぷり」のように、語尾に特定の言葉を付ける娘っぽくするのは、言ってしまえば語尾に「ぷり」を付ければ簡単に真似出来ます。*1
ところが、夢川ゆいちゃんの喋り方は、単純に語頭に「ユメ」を付ければいい、という簡単なものではないのが若干大変です。一例を挙げてみましょう。
「お兄ちゃんなんかユメ大嫌い!」
「 ユメ お兄ちゃんなんか大嫌い!」ではなく「お兄ちゃんなんか ユメ 大嫌い!」なのがポイントです。
もう一例挙げてみましょう。
「誰1人寂しかったり、悲しかったりしない、ユメハッピーでユメスマイルなプリパラ!」
「ユメ」ハッピー、「ユメ」スマイルな、となっているのがポイントです。
ここから分かることは、 「ユメ」は品詞分類上「副詞」であること です。
一例目では、「ユメ」は「大嫌い」を修飾する言葉になっています。「大嫌い」という言葉は品詞としては形容動詞に該当します。
もう一例では、「ハッピー」と「スマイルな」を修飾しています。「ハッピー」は、幸せである様、なので品詞はやはり形容動詞。
「スマイル」は単独だと名詞になってしまいますが、ここでは「プリパラ」という名詞を修飾する「スマイルな」という形で使われていることを考えると、形容詞に分類するのが正しいと思われます。
例が少ないのでこれだけで言い切るのは無理があるかもですが、形容動詞や形容詞を修飾する品詞って何よ、と考えると「副詞」として考えるのが妥当であると考えます。
つまり、夢川ゆいちゃんのあの喋り方を再現するためには、最低限 「動詞・形容動詞・形容詞の前に『ユメ』を差し込む」 ことが必要になります。
というわけで、それをプログラムで再現してみます。
やってみた
品詞分類
プログラム的に品詞分類を行うためには、形態素解析というものを行えば良さそうです。
形態素解析エンジンにはいくつか種類があるようですが、とりあえずいろいろ調べてみてよく名前を見かけた「MeCab」を使用してみることにしました。
Node.jsから呼び出して使えるのも有り難い。
言語類
以下の環境で、まずは文章をユメ変換する「ユメAPI」を作ってみます。
- Node.js v11.5.0
- MeCab v0.996
linqを入れたのは、Node.jsでもLINQ使えないのかなーと思って調べてみたらあったので、試しに使ってみただけです。
インストールの方法などは割愛。
MeCab周りだけリンク貼っておきます。
書いてみた
こんな感じでまずは書いてみました。
const MeCab = new require('mecab-async') , mecab = new MeCab() ; mecab.command = 'mecab -d /usr/local/lib/mecab/dic/mecab-ipadic-neologd/' const express = require('express') const app = express() const bodyParser = require('body-parser') const Enumerable = require('linq') // yume convert target const targetTypes = ['形容動詞語幹', '動詞', '形容詞', '副詞'] // init app.use(bodyParser.json()) app.use(bodyParser.urlencoded({ extended: true })) app.post('/api', (req, res) => { const body = req.body // check "text" exists if (body.text === undefined) { res.status(500).send({ msg: "'text' param is required"}) return } // mecab process mecab.parse(body.text, (err, parsedList) => { try { if (err) throw err // yume hantei console.log(parsedList) const yumed = Enumerable.from(parsedList) .select(parsed => { const res = Enumerable.from(targetTypes) .any(type => type === parsed[1] || type === parsed[2]) if (res) { parsed[0] = 'ユメ' + parsed[0] } return parsed[0] }) .toArray() .join('') res.send({ text: yumed }) } catch (e) { res.status(500).send({ msg: 'text processing error'}) } }) }) app.listen(3000, () => console.log('listen port 3000'))
3000番ポートで待ち受け、 /api
にPOSTでリクエストを投げると、ユメ語変換して返すAPIです。
やってることは大したこと無く、
これだけです。
リクエストボディはJSONで、 text
のValueとして、変換したい文章を指定します。
$ curl -X POST \ > http://localhost:3000/api \ > -H 'Content-Type: application/json' \ > -d '{ > "text": "お兄ちゃんなんか大嫌い!" > }' {"text":"お兄ちゃんなんかユメ大嫌い!"}
こんな感じになりました。良い感じ。
MeCabさんはこんな感じで形態素解析をしてくれています。
[ [ 'お兄ちゃん', '名詞', '固有名詞', '人名', '一般', '*', '*', 'お兄ちゃん', 'オニイチャン', 'オニーチャン' ], [ 'なんか', '助詞', '副助詞', '*', '*', '*', '*', 'なんか', 'ナンカ', 'ナンカ' ], [ '大嫌い', '名詞', '形容動詞語幹', '*', '*', '*', '*', '大嫌い', 'ダイキライ', 'ダイキライ' ], [ '!', '記号', '一般', '*', '*', '*', '*', '!', '!', '!' ] ]
いろいろ試す
夢川ゆい語録から 「ユメ」を取り除いた文章 をAPIに投げてみて、どこまで再現されるか試してみます。
なお、語録はアイドルタイムプリパラ キャラ感想&名セリフ集を参照させて頂きました。
U-NEXTでの配信が終わってしまったので見れないのです。おのれ。
誰1人寂しかったり、悲しかったりしない、ハッピーでスマイルなプリパラ !
期待値: 誰1人寂しかったり、悲しかったりしない、ユメハッピーでユメスマイルなプリパラ !
$ curl -X POST \ > http://localhost:3000/api \ > -H 'Content-Type: application/json' \ > -d '{ > "text": "誰1人寂しかったり、悲しかったりしない、ハッピーでスマイルなプリパラ !" > }' {"text":"誰1人ユメ寂しかったり、ユメ悲しかったりしない、ユメハッピーでスマイルなプリパラ!"}
ユメ過剰でユメ微妙。 MeCabさんがどのように形態素解析したのかを見てみます。
[ [ '誰', '名詞', '代名詞', '一般', '*', '*', '*', '誰', 'ダレ', 'ダレ' ], [ '1', '名詞', '数', '*', '*', '*', '*', '1', 'イチ', 'イチ' ], [ '人', '名詞', '接尾', '助数詞', '*', '*', '*', '人', 'ニン', 'ニン' ], [ '寂しかっ', '形容詞', '自立', '*', '*', '形容詞・イ段', '連用タ接続', '寂しい', 'サビシカッ', 'サビシカッ' ], [ 'たり', '助詞', '並立助詞', '*', '*', '*', '*', 'たり', 'タリ', 'タリ' ], [ '、', '記号', '読点', '*', '*', '*', '*', '、', '、', '、' ], [ '悲しかっ', '形容詞', '自立', '*', '*', '形容詞・イ段', '連用タ接続', '悲しい', 'カナシカッ', 'カナシカッ' ], [ 'たり', '助詞', '並立助詞', '*', '*', '*', '*', 'たり', 'タリ', 'タリ' ], [ 'しない', '名詞', '一般', '*', '*', '*', '*', 'しない', 'シナイ', 'シナイ' ], [ '、', '記号', '読点', '*', '*', '*', '*', '、', '、', '、' ], [ 'ハッピー', '名詞', '形容動詞語幹', '*', '*', '*', '*', 'ハッピー', 'ハッピー', 'ハッピー' ], [ 'で', '助動詞', '*', '*', '*', '特殊・ダ', '連用形', 'だ', 'デ', 'デ' ], [ 'スマイル', '名詞', '一般', '*', '*', '*', '*', 'スマイル', 'スマイル', 'スマイル' ], [ 'な', '助動詞', '*', '*', '*', '特殊・ダ', '体言接続', 'だ', 'ナ', 'ナ' ], [ 'プリパラ', '名詞', '固有名詞', '一般', '*', '*', '*', 'プリパラ', 'プリパラ', 'プリパラ' ], [ '!', '記号', '一般', '*', '*', '*', '*', '!', '!', '!' ] ]
「寂しい」「悲しい」が形容詞なので、足されるのは当たり前ですね。これは仕方ないとして。
逆に「スマイルな」ではなく「スマイル」として解析されて、名詞扱いになっていますね。まあ、普通に考えればそうですよね。
アイドルをやめる事なんてできない!
期待値: アイドルをやめる事なんてユメできない!
$ curl -X POST \ > http://localhost:3000/api \ > -H 'Content-Type: application/json' \ > -d '{ > "text": "アイドルをやめる事なんてできない!" > }' {"text":"アイドルをユメやめる事なんてユメできない!"}
「ユメやめる」が過剰ですが、それくらい。「やめる」も動詞ですから、これも仕方ない。解析結果は以下の通り。
[ [ 'アイドル', '名詞', '一般', '*', '*', '*', '*', 'アイドル', 'アイドル', 'アイドル' ], [ 'を', '助詞', '格助詞', '一般', '*', '*', '*', 'を', 'ヲ', 'ヲ' ], [ 'やめる', '動詞', '自立', '*', '*', '一段', '基本形', 'やめる', 'ヤメル', 'ヤメル' ], [ '事', '名詞', '非自立', '一般', '*', '*', '*', '事', 'コト', 'コト' ], [ 'なんて', '助詞', '副助詞', '*', '*', '*', '*', 'なんて', 'ナンテ', 'ナンテ' ], [ 'でき', '動詞', '自立', '*', '*', '一段', '未然形', 'できる', 'デキ', 'デキ' ], [ 'ない', '助動詞', '*', '*', '*', '特殊・ナイ', '基本形', 'ない', 'ナイ', 'ナイ' ], [ '!', '記号', '一般', '*', '*', '*', '*', '!', '!', '!' ] ]
らぁらがパパラ宿に来てから、ずっと一緒だったもん!
期待値: らぁらがパパラ宿に来てから、ユメずっと一緒だったもん!
$ curl -X POST \ > http://localhost:3000/api \ > -H 'Content-Type: application/json' \ > -d '{ > "text": "らぁらがパパラ宿に来てから、ずっと一緒だったもん!" > }' {"text":"らぁらがパパラ宿にユメ来てから、ずっと一緒だったもん!"}
大外れでユメがっかり。「ずっと」は副詞のはずなので、ユメっても良いのですが。MeCabさんの解析結果は以下の通り。
[ [ 'らぁら', '名詞', '固有名詞', '人名', '一般', '*', '*', 'らぁら', 'ラァラ', 'ラァラ' ], [ 'が', '助詞', '格助詞', '一般', '*', '*', '*', 'が', 'ガ', 'ガ' ], [ 'パパラ', '名詞', '一般', '*', '*', '*', '*', '*' ], [ '宿', '名詞', '一般', '*', '*', '*', '*', '宿', 'ヤド', 'ヤド' ], [ 'に', '助詞', '格助詞', '一般', '*', '*', '*', 'に', 'ニ', 'ニ' ], [ '来', '動詞', '自立', '*', '*', 'カ変・来ル', '連用形', '来る', 'キ', 'キ' ], [ 'て', '助詞', '接続助詞', '*', '*', '*', '*', 'て', 'テ', 'テ' ], [ 'から', '助詞', '格助詞', '一般', '*', '*', '*', 'から', 'カラ', 'カラ' ], [ '、', '記号', '読点', '*', '*', '*', '*', '、', '、', '、' ], [ 'ずっと一緒', '名詞', '固有名詞', '一般', '*', '*', '*', 'ずっと一緒', 'ズットイッショ', 'ズットイッショ' ], [ 'だっ', '助動詞', '*', '*', '*', '特殊・ダ', '連用タ接続', 'だ', 'ダッ', 'ダッ' ], [ 'た', '助動詞', '*', '*', '*', '特殊・タ', '基本形', 'た', 'タ', 'タ' ], [ 'もん', '助詞', '終助詞', '*', '*', '*', '*', 'もん', 'モン', 'モン' ], [ '!', '記号', '一般', '*', '*', '*', '*', '!', '!', '!' ] ]
「ずっと一緒」で一括りにされて名詞の扱いにされています。
「ずっと」単独であれば想定通りになるので、これはいかんとも。
$ curl -X POST \ > http://localhost:3000/api \ > -H 'Content-Type: application/json' \ > -d '{ > "text": "ずっと" > }' {"text":"ユメずっと"}
まとめ
なんともユメ微妙な結果で終わってしまいました。
MeCabの形態素解析に依存しているので、解析結果が想定通りにならないと結果が芳しくならないようです。
ユメって欲しいのにユメらなかったパターン
「ずっと」「一緒」だと思っていたのが「ずっと一緒」で一括りにされたり、「スマイルな」で解析して欲しいのに「スマイル」「な」となっていると発生するパターンです。
MeCabの解析は、いくつかの解析パターンから最適解っぽいものを出力しているようなので、公式に載っている「N-Best 解の出力」機能を併用することで、ユメるパターンを複数作ること自体は出来そうです(未検証)
ただ「スマイル」「な」を「スマイルな」と解釈した上で形容詞と見なすためには、文脈から判断しないといけないところもあるので、形態素解析でそこまで解析させるのは難しそうな気もしています。
あと、複数パターン出したところで、ではユメるパターンとしてどれが最適解か?という判断は難しそうです。 というかどれが最適解なんて誰が分かるんだ。
ユメ過剰なパターン
これはちょっと考えどころです。
というのも、元のセリフで、副詞の修飾対象でありながら、ユメったりユメらなかったりしてるのは 脚本のさじ加減 としか言えないので、その法則を割り出すのは難しそうです。
考えうる改善案
- 複数の解析結果から、出来るだけユメらせるように結果をマージする
- 「N-Best 解の出力」機能にて、複数の解析結果を取得
- それぞれの結果をユメらせる
- 複数結果から、出来るだけユメった状態になるよう、結果をマージして返す
- ユーザー側で、よしなに「ユメ」を取り除く
- 別の形態素解析エンジンを使用 or 併用してみる
- 特に理由なくMeCabを使用していたため、他のエンジンを使ってみる手はある
- ただしエンジンを変えたところで、その解析結果に依存することは変わらない
- 機械学習を併用してみる?
- 夢川ゆいのセリフを取り込んで学習させる
- 学習結果を元に、ユメるかユメらないか判断させる?
- 形態素解析結果の学習は出来るのか?
機械学習はまだ手を付けたことが無いので、全く頓珍漢なことを言っているかもしれません。
ただ、一定のルールがありつつも、さじ加減
でユメったりユメらなかったりするのを、機械学習でよしなにやってくれ、というアプローチは、アリなのかもしれません。
とりあえずで実現可能そうなのは、「複数の解析結果から、出来るだけユメらせるように結果をマージ」するパターンになりそうです。
結論、 ユメ語録を再現するのは、意外とユメ大変。
GitHub
本当はNuxt.js + ExpressなWebアプリにしたかったのですが、いざデプロイしてみたらExpressのAPIが呼び出せず、タイムアップになってしまいました。
この記事で載せているコードは、Nuxt.jsに組み込む前に単独で書いてた時のコードです。
GitHubのリポジトリは一応晒しておきますので、各種ご指摘あればよろしくお願い致します。
今回公開出来なかったNuxtのフロントアプリは後日公開します。
ユメくやしい。
※2019/01/16 公開しました。
*1:「語尾アイドルなめるんじゃないわよ」「ぷりが抜けてるぞ、ぷりが」「ぷり〜!!」