Synthetic Station

saken649 / イチカベサケンの音楽とプログラミングのブログ。それ以外もあるかも。

形態素解析を用いて、夢川ゆいのユメ語録を再現してみる試み

ユメークリスマス。saken649です。
プリッカソン Advent Calendar 2018の22日目です。

プリッカソンのアドベントカレンダー記事2つ目となりました。
7日目にプリパラ ULTRA MEGAMIXのレビューをやりましたので、ご興味ある方はこちらも合わせてどうぞ。

syn-station.hatenablog.com

また2018年のアドベントカレンダーとしては3つ目の記事となります。
先日会社のアドベントカレンダーに参加させて頂いて、こんな記事を書いてましたので、ご興味ある方はこちらも合わせてどうぞ。

qiita.com

今回は、技術系かつプリティーシリーズに関連する記事を書いてみます。


【やる気!元気!もくじ〜】


テーマ

テーマは「形態素解析を用いて、夢川ゆいのユメ語録を再現してみる試み」です。

毎回プリッカソンに参加させて頂くたびに、個人プロジェクトである「プリスタグラムのパクリ」を進めているのですが、今回は趣向を変えてみました。
プリスタグラムのパクリについては、いずれ改めて記事にしてみようと思います。 ずっとこればっかりやってると疲れるから、というわけではありません

本題に戻りまして。

今回は、文章を入力したら、アイドルタイムプリパラの主人公の片割れである「夢川ゆい」ちゃんのあの独特な喋り方っぽく変換して返す、というちょっとしたアプリを作ってみます。
何かと言葉の頭に「 ユメ 」を付けて喋る、アレです。
夢川ゆい (ゆめかわゆい)とは【ピクシブ百科事典】

なんで急にそんなテーマを?というと、単純に、アイドルタイムプリパラを見終えて数ヶ月経った今になって、非常にクセになってきてしまったからです(理由になっていない

How To ユメ

さて、南みれぃちゃんの「ぷり」のように、語尾に特定の言葉を付ける娘っぽくするのは、言ってしまえば語尾に「ぷり」を付ければ簡単に真似出来ます。*1
ところが、夢川ゆいちゃんの喋り方は、単純に語頭に「ユメ」を付ければいい、という簡単なものではないのが若干大変です。一例を挙げてみましょう。

「お兄ちゃんなんかユメ大嫌い!」

ユメ お兄ちゃんなんか大嫌い!」ではなく「お兄ちゃんなんか ユメ 大嫌い!」なのがポイントです。
もう一例挙げてみましょう。

「誰1人寂しかったり、悲しかったりしない、ユメハッピーでユメスマイルなプリパラ!」

「ユメ」ハッピー、「ユメ」スマイルな、となっているのがポイントです。

ここから分かることは、 「ユメ」は品詞分類上「副詞」であること です。

kotobank.jp

一例目では、「ユメ」は「大嫌い」を修飾する言葉になっています。「大嫌い」という言葉は品詞としては形容動詞に該当します。

もう一例では、「ハッピー」と「スマイルな」を修飾しています。「ハッピー」は、幸せである様、なので品詞はやはり形容動詞。
「スマイル」は単独だと名詞になってしまいますが、ここでは「プリパラ」という名詞を修飾する「スマイルな」という形で使われていることを考えると、形容詞に分類するのが正しいと思われます。

例が少ないのでこれだけで言い切るのは無理があるかもですが、形容動詞や形容詞を修飾する品詞って何よ、と考えると「副詞」として考えるのが妥当であると考えます。

つまり、夢川ゆいちゃんのあの喋り方を再現するためには、最低限 「動詞・形容動詞・形容詞の前に『ユメ』を差し込む」 ことが必要になります。

というわけで、それをプログラムで再現してみます。

やってみた

品詞分類

プログラム的に品詞分類を行うためには、形態素解析というものを行えば良さそうです。

形態素解析 - Wikipedia

形態素解析エンジンにはいくつか種類があるようですが、とりあえずいろいろ調べてみてよく名前を見かけた「MeCab」を使用してみることにしました。
Node.jsから呼び出して使えるのも有り難い。

言語類

以下の環境で、まずは文章をユメ変換する「ユメAPI」を作ってみます。

linqを入れたのは、Node.jsでもLINQ使えないのかなーと思って調べてみたらあったので、試しに使ってみただけです。

qiita.com

インストールの方法などは割愛。
MeCab周りだけリンク貼っておきます。

github.com

書いてみた

こんな感じでまずは書いてみました。

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です。
やってることは大したこと無く、

  • MeCab形態素解析させる
  • 解析された単位ごとに、副詞が修飾する対象(形容動詞、形容詞、副詞、動詞)であるか判定する
  • 修飾対象であれば「ユメ」を言葉の手前に足す

これだけです。

リクエストボディはJSONで、 textValueとして、変換したい文章を指定します。

$ 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リポジトリは一応晒しておきますので、各種ご指摘あればよろしくお願い致します。

github.com

今回公開出来なかったNuxtのフロントアプリは後日公開します。

ユメくやしい。

※2019/01/16 公開しました。

syn-station.hatenablog.com

*1:「語尾アイドルなめるんじゃないわよ」「ぷりが抜けてるぞ、ぷりが」「ぷり〜!!」