おもしろいダジャレを入力すると布団が吹っ飛ぶ装置を作った

面白いダジャレを言うと、何が起こるでしょうか。


そうです。布団が吹っ飛びます。

![ふっとぶとん.gif](https://qiita-image-store.s3.amazonaws.com/0/135663/18122aa6-58bf-3ed4-db1e-a6dca2ec6564.gif)





今回は、ダジャレを心から愛するブレインパッドのメンバー4人が制作した、最新ダジャレAIを搭載した次世代型おもしろダジャレ検知マシン『**オフトゥンフライングシステム**』のご紹介をさせて頂きます。

<img width="500" alt="スクリーンショット 2018-12-19 12.45.58.png" src="https://qiita-image-store.s3.amazonaws.com/0/135663/463b4cab-bbc5-4e30-061c-c4d9bae26aba.jpeg">


※補足＆感謝

- 面白いと布団が吹っ飛ぶという発想は日テレ系列の大喜利番組「[フットンダ](https://www.ctv.co.jp/futtonda/)」のリスペクトです
- 「オフトゥンフライングシステム」という名前はボーカロイドソング、『[オフトゥンフライングシステム](https://www.nicovideo.jp/watch/sm26086747)』があまりにもイメージとぴったり合ったため、名前を使わせていただきました。こちらの曲を無限ループしながら記事を読んでいただけると、より楽しめる仕組みになっております


# Product Summary

オフトゥンフライングシステムとは何か。分かりやすく説明すると、ダジャレ検知AI『Shareka』とダジャレ評価AI『Ukeruka』が搭載された、布団がふっとんだマシンです。

<img width="1135" alt="スクリーンショット 2018-12-19 12.45.58.png" src="https://qiita-image-store.s3.amazonaws.com/0/135663/9db54a71-a017-4cef-43ad-b98a5e0ca6bd.png">



## 動作の流れ
1. ブラウザ上のフォームからダジャレを入力する
2. **ダジャレかどうかの判別**
3. （ダジャレと判定された場合）**ダジャレの面白さ評価**
4. 結果が画面に表示される
5. さらにダジャレ評価が50点を超えた場合、**布団が吹っ飛ぶ**という仕組みになっています。
6. 投稿されたダジャレと評価点はGCPのdatastoreに保存

![全体像.jpg](https://qiita-image-store.s3.amazonaws.com/0/135663/324538f8-7953-4b5d-ead5-88696a757fba.jpeg)


### 面白いダジャレ（例：裏面のラーメン）を入力した場合
表示画面
![2018-12-19-035513_1247x914_scrot.png](https://qiita-image-store.s3.amazonaws.com/0/135663/4a2591a0-870f-660e-f1e9-c030603d20cb.png)
ふとん
![iOS のファイル (3).gif](https://qiita-image-store.s3.amazonaws.com/0/135663/8e29972c-98b1-d188-d430-ea1cc689e4ea.gif)




### つまらないダジャレ（例：バラバラの薔薇）を入力した場合
表示画面
![2018-12-19-041201_1247x914_scrot.png](https://qiita-image-store.s3.amazonaws.com/0/135663/1439d805-97d4-37e9-5413-84c62137ae44.png)

ふとん
![image.png](https://qiita-image-store.s3.amazonaws.com/0/135663/6328d027-459a-3469-d47f-5dd470b527fc.png)

### ダジャレじゃない文（例：ダジャレが思いつきません）を入力した場合
表示画面
![2018-12-19-040044_1247x914_scrot.png](https://qiita-image-store.s3.amazonaws.com/0/135663/4195aa93-6d37-76f7-39d4-9c4a06b18af6.png)
ふとん
![image.png](https://qiita-image-store.s3.amazonaws.com/0/135663/4afbe845-d800-3b54-2d77-a5243f8d9d1f.png)

また、自分のスコアの下には過去の投稿ダジャレが並んでおり、過去の投稿と比較可能となっています。
そのため、より面白いダジャレを生み出すための**ダジャレPDCA**を高速に回すことができます。


# Our Mission

『自分はこんなにもおもしろいダジャレを言っているのに、誰も笑ってくれない。。。むしろスベったみたいな空気になってる。。。』

こんなダジャレに悩むすべての人のためにオフトゥンフライングシステムは作られました。

ダジャレを言ったのに、いまいちウケないと感じたときはすぐさまオフトゥンフライングシステムにそのダジャレを入力することで、AIがダジャレの面白さを判定してくれます。

面白いと判定された場合、「AIが面白いと言っているから笑わないお前がおかしい。」と相手にマウントをとることができます。

また、ダジャレの内容関係なく **そもそも布団が吹っ飛ぶことが面白い** ので、相手も笑ってくれるでしょう。

え、面白く無いと判定されたときはどうすればいいかって？
「AIにもすべったわ〜ww」
と自虐ネタで笑いを取ることができます。

# Our Technology
オフトゥンフライングシステムは４つのテクノロジーによって成り立っています。
![プレゼンテーション1.jpg](https://qiita-image-store.s3.amazonaws.com/0/135663/d78bd8bf-648a-8286-cc52-84ceedbf431f.jpeg)


ひとつずつ解説していきます。

## Shareka　〜ダジャレ判別AI〜
Sharekaは入力された文章がダジャレかどうかを判定するAIです。

### ダジャレとはなにか
そもそも、ダジャレとはなにか。ダジャレは一般に知られているよりも幅が広いです。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/135663/6e18e676-e9d4-00ed-4087-1141105be6f0.png)

2018年12月現在、Sharekaで対応可能なダジャレは、「**完全反復型**」「**不完全反復型**」のみであり、「**変形反復型**」「**潜在表現重複型**」は対応していません。
これらに対応するためには、音韻（読み）だけでなく、語の「意味」や「知識」をモデルに入れ込む必要があり、難易度がぐっと高くなります。

### 利用するデータ
以降で利用する**ダジャレデータ**は以下サイトからスクレイピングで集めました。
[ダジャレ・ステーション](https://dajare.jp/)
このサイトでは、投稿されたダジャレに対して５点満点で評価する事もできるため、各ダジャレに対して評価点がついています。
結果、45,000ものダジャレとその評価点を集めることができました。

また、ダジャレ判別モデルの評価をするために**ダジャレじゃないデータ**も集める必要があります。ダジャレじゃなければ何でも良いわけではなく、ダジャレとだいたい同じ長さの文章である必要があります。
今回は、以下のサイトで用いられている、ラノベ・小説のタイトルを**ダジャレじゃないデータ**として用いました。
https://qiita.com/fj-th/items/3868a4aef834a9bee980
結果、全部で約50,000もの**ダジャレじゃない文**が集まりました。

### ルールベースによるアプローチ
※ルールベースは人工知能ではないと言い張る人は、第２次人工知能ブームでググってください。

#### 手法
以下のようなルールベースで入力した文がダジャレかどうかを判別します。

１．　入力文をすべてカタカナに変える
２．　記号、長音（ー）、促音（ッ）を削除、小文字（ァ・ェ・ャ）などを大文字に変換
３．　1字ずつずらしてn文字ずつの要素を作成し、重複している要素がある場合にダジャレと判定する

例えば、**布団がふっとんだ**という超おもしろダジャレを判別する場合、
布団がふっとんだ
↓
フトンガフットンダ
↓
フトンガフトンダ
↓
['フトン', 'トンガ', 'ンガフ', 'ガフト', 'フトン', 'トンダ']
⇒**'フトン'が重複しているため、ダジャレと判別**


#### コード
こちらがダジャレを分類するコード全文になります。

```python

import MeCab
import collections
from flask import Blueprint

class Shareka:
    
    def __init__(self, sentence, n=3):
        """置き換える文字リストが格納されたクラス変数"""
        self.replace_words = [["。", ""],["、", ""], [",", ""],[".", ""],["!", ""],
            ["！", ""],["・", ""],["「", ""],["」", ""], ["「", ""],["｣", ""],["『", ""],["』", ""],[" ", ""],["　", ""],
            ["ッ", ""],["ャ", "ヤ"], ["ュ", "ユ"],["ョ", "ヨ"],
            ["ァ", "ア"],["ィ", "イ"],["ゥ", "ウ"],["ェ", "エ"],["ォ", "オ"],["ー", ""]]
        self.kaburi = n
        self.sentence = sentence

        mecab = MeCab.Tagger("-Oyomi")
        self.kana = mecab.parse(sentence)[:-1]
        self.preprocessed = self.preprocessing(self.kana)
        self.devided = self.devide(self.preprocessed)

    def preprocessing(self, sentence):
        for i, replace_word in enumerate(self.replace_words):
            sentence = sentence.replace(replace_word[0],replace_word[1])
        return sentence

    def devide(self, sentence):
        elements = []
        repeat_num = len(sentence) - (self.kaburi - 1)
        for i in range(repeat_num):
            elements.append(sentence[i:i+self.kaburi])
        return elements

    def dajarewake(self):
        if len(self.devided) == 0:
            return False
        elif self.list_max_dup()[1] > 1 and self.sentence_max_dup_rate(self.list_max_dup()[0]) <= 0.5:
            return True
        else:
            return False
```
##### 結果（精度）
ダジャレ45,000件と、ダジャレじゃない文50,000件を上記のコードを用いて、複数パターンのパラメータでそれぞれ判別した結果、以下のようになりました。

##### n=2（２文字以上の並びが繰り返されている場合にダジャレと判定）

全体の正解率：89%
ダジャレをダジャレと正しく判定する割合：**93**%
通常文を通常文と正しく判定する割合：87%
<img width="383" alt="スクリーンショット 2018-12-18 0.15.44.png" src="https://qiita-image-store.s3.amazonaws.com/0/135663/1174eb2c-f9e1-c3c8-6c68-8ba499b11ebb.png">

##### n=3（３文字以上の並びが繰り返されている場合にダジャレと判定）
（『猫が寝転んだ』はダジャレではないと判定）
全体の正解率：88%
ダジャレをダジャレと正しく判定する割合：79%
通常文を通常文と正しく判定する割合：**97**%
<img width="372" alt="スクリーンショット 2018-12-18 0.15.37.png" src="https://qiita-image-store.s3.amazonaws.com/0/135663/7cc85c35-1b17-ac49-53d2-7ad6b1701afb.png">

どちらも正解率はほぼ変わらず約89%となりましたが、
ダジャレ判定判定が緩い場合（n=2）、通常文をダジャレと誤判定する場合が多く、
ダジャレ判定判定が厳しい場合（n=3）、ダジャレを通常文と誤判定する場合が多くなります。
せっかくダジャレを入力したのに「ダジャレではない」と判別されてしまうのはあまりにも可哀想なので、今回はSharekaのパラメータn=2を採用しています。

## Ukeruka 〜ダジャレ評価AI〜
Ukerukaは、入力されたダジャレに対して、「面白い」もしくは「面白くない」と評価をするAIです。こちらはダジャレかどうかを判別するよりもかなり難易度が高いことです。

### 何を面白い、何を面白くないと定義するか
難易度が高い理由のひとつに、**人間でもダジャレの面白さを判定できない**、という大問題があります。

今回は、ダジャレ・ステーションに投稿されたダジャレそれぞれに付いている平均評価点を「面白さ」と定義し

平均評価点３以上　⇒　面白い
平均評価点３未満　⇒ 面白くない

の２値分類でディープラーニングのモデルを構築します。


### 手法
言語処理に対してディープラーニングを実施する際は、まず文を単語単位で区切る処理（形態素解析）を施すことが多いです。
例えば、『今日はいい天気ですね』という文を単語で区切った場合このようになります。

```
今日	名詞,副詞可能,*,*,*,*,今日,キョウ,キョー
は	助詞,係助詞,*,*,*,*,は,ハ,ワ
いい	形容詞,自立,*,*,形容詞・イイ,基本形,いい,イイ,イイ
天気	名詞,一般,*,*,*,*,天気,テンキ,テンキ
です	助動詞,*,*,*,特殊・デス,基本形,です,デス,デス
ね	助詞,終助詞,*,*,*,*,ね,ネ,ネ
```

ただし、ダジャレに対して同じことをすると問題が生じます。
例えば、『アルミ缶の上にあるみかん』に対して同じことをすると。

```
アルミ	名詞,一般,*,*,*,*,アルミ,アルミ,アルミ
缶	名詞,一般,*,*,*,*,缶,カン,カン
の	助詞,連体化,*,*,*,*,の,ノ,ノ
上	名詞,非自立,副詞可能,*,*,*,上,ウエ,ウエ
に	助詞,格助詞,一般,*,*,*,に,ニ,ニ
ある	動詞,自立,*,*,五段・ラ行,基本形,ある,アル,アル
みかん	名詞,一般,*,*,*,*,みかん,ミカン,ミカン
```
のように分解されますが、「アルミ缶」と「あるみかん」が反復されているダジャレなのに、単語で分類することによってバラバラになってしまっています。

この問題を解決するため、今回はcharacter-level CNNという手法に目をつけました。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/135663/d1bd562c-03e9-9882-f098-71a9ee6d7d02.png)
[元論文](https://arxiv.org/abs/1509.01626)

character-level CNNの特徴をズバリ説明すると、文章を単語単位ではなく、１文字単位で処理することにあります。
<img width="753" alt="スクリーンショット 2018-12-19 2.37.05.png" src="https://qiita-image-store.s3.amazonaws.com/0/135663/325750b5-e06f-17cf-d62f-0e76e996f074.png">


単語で潰さず文字単位で学習することでダジャレ特有の音韻の反復を学習することができるのではないかと考えました。

（追記）character-level CNNを利用した真の理由は、この手法の日本語名が『畳み込みニューラルネットワーク』であり、畳が使われている本システムとの相性が良いから、ということをすっかり忘れていました。上記は参考程度にお読みください。

### コード
学習に用いたコードはこちらの記事を参考に作成しました。
[Character-level CNNでライトノベルっぽさを定量化する](https://qiita.com/fj-th/items/3868a4aef834a9bee980)
学習部のコードのみ掲載します。

```python

#coding: utf-8
import numpy as np
import keras
from keras.optimizers import *
from keras.layers import *
from keras.callbacks import *
from keras.models import *
from numpy import *
import codecs

def create_model(embed_size=32, max_length=30, filter_sizes=(2, 3, 4, 5), filter_num=64):
    inp = Input(shape=(max_length,))
    emb = Embedding(0xffff, embed_size,embeddings_regularizer=regularizers.l1(0.01))(inp)
    emb_ex = Reshape((max_length, embed_size, 1))(emb)
    convs = []

    for filter_size in filter_sizes:
        conv = Conv2D(filter_num, (filter_size, embed_size), activation="relu")(emb_ex)
        pool = MaxPooling2D(pool_size=(max_length - filter_size + 1, 1))(conv)
        convs.append(pool)

    convs_merged = Concatenate()(convs)
    reshape = Reshape((filter_num * len(filter_sizes),))(convs_merged)
    fc1 = Dense(32, activation="relu")(reshape)
    bn1 = BatchNormalization()(fc1)
    fc2 = Dense(1, activation='sigmoid')(bn1)
    model = Model(input=inp, output=fc2)
    return model

def load_data(filepath, targets, max_length=30, min_length=1):
    titles = []
    tmp_comments = []
    with codecs.open(filepath, 'r', 'utf-8', 'ignore') as f:
        for l in f:
            label_id, title = l.split("\t", 1)

            if label_id != "0" and label_id!="1":
                continue
            
            title = [ord(x) for x in title]
            # 長い部分は打ち切り
            title = title[:max_length]
            title_len = len(title)

            if title_len < max_length:

                title += ([0] * (max_length - title_len))

            titles.append((int(label_id), title))

    return titles

def train(inputs, targets, batch_size=1000, epoch_count=1, max_length=30, model_filepath="model.h5", learning_rate=0.0005):

    
    start = learning_rate
    stop = learning_rate * 0.001
    learning_rates = np.linspace(start, stop, epoch_count)

    # モデル作成
    model = create_model(max_length=max_length)

    optimizer = Adam(lr=learning_rate)
    model.compile(loss='binary_crossentropy',
                  optimizer=optimizer,
                  metrics=['accuracy'])

    #es_cb = keras.callbacks.EarlyStopping(monitor='val_loss', patience=0, verbose=0, mode='auto')

    
    # 学習
    history=model.fit(inputs, targets,
              nb_epoch=epoch_count,
              batch_size=batch_size,
              verbose=1,
              validation_split=0.2,
              shuffle=True,
              callbacks=[
                  LearningRateScheduler(lambda epoch: learning_rates[epoch]),
              ])

    # モデルの保存
    model.save(model_filepath)
    return history


if __name__ == "__main__":
    comments = load_data("../data/preprocessed_dajare.tsv",[0])
    np.random.shuffle(comments) # shuffle the data (note: validation_split does not shuffle the data before the splitting)

    input_values = []
    target_values = []
    for target_value, input_value in comments:
        input_values.append(input_value)
        target_values.append(target_value)
    input_values = np.array(input_values)
    target_values = np.array(target_values)
    history=train(input_values, target_values, epoch_count=250)

```

### 結果（精度）
学習の過程を可視化したものがこちらになります。
<img width="655" alt="スクリーンショット 2018-12-19 2.00.19.png" src="https://qiita-image-store.s3.amazonaws.com/0/135663/9230304b-b3b2-1729-6107-485194cee710.png">

左の図の見方を説明すると、右に行くほどモデルを反復して学習していることを示し、オレンジの線が上に行くほど正解率が高くなっていることを示します。

**つまり、驚くほど学習できていません！！！！**
正解率が67%付近からびた一文上がりません！！！！

もともと『面白い』とラベルが付けられているデータの割合が６５％程度なので、ほぼランダムと変わらないということになります。

character-level CNN以外にも、以下のような特徴量を作成してlightGBMで学習もしてみましたが結果は同じく、全く学習できませんでした。

- 全体の文字列の長さ
- 分かち書きされたときの要素数
- 全体の文字列の長さに対してかかっている文字数の割合
- かかっている文字数
- 何回かかっているか

### 考察
ここまで、読みの情報（つまり音）のみで判別、評価を行ってきました。判別に関しては89%と、高い精度で予測できていたのですが、評価に関しては全く予測できないという結果になりました。
これらの結果から、ダジャレかどうかの判別に最も重要な要素は**「音」**であるが、面白いかどうかの評価に**「音」は重要ではなく、「意味」「知識」が重要である**という知見が得られました。（下記図参考）

これらは**ダジャレで得た（ダジャレデータ）知見です**。
<img width="621" alt="スクリーンショット 2018-12-19 2.27.02.png" src="https://qiita-image-store.s3.amazonaws.com/0/135663/19fb5ab7-ad38-e9a0-4ca0-11ab333df204.png">
ここで言う意味・知識とは以下のようなものです。
「布団」の意味：布製の寝具
「布団」の知識：入ると暖かい・通常吹っ飛ぶものではない　など

確かに、「布団が吹っ飛んだ」というダジャレでなぜ人が笑うのかを考えてみると、
音が反復している点に加え、布団が吹っ飛ぶという日常ではありえない光景を思い浮かべて笑うと想像できます。このことからも、「意味」「知識」を加えることは必要だと考えます。


## Kurohige 〜次世代型布団ふっとばしエンジン〜
ここから先はハードウェアの話になります。
布団を吹っ飛ばすエンジンとして、今回は「黒ひげ危機一発」ライブラリを改変し、次世代型ふっとばしエンジン、**Kurohige**を作成いたしました。

装置の外枠を外すと、内側には黒ひげ危機一発の樽先にローソンの割り箸がついた装置が入っています。
ローソンの割り箸が布団を押し出すことで、布団が吹っ飛ぶ仕組みになっています。
まさに、**寝具larity**の到来です。

<img width="283" alt="スクリーンショット 2018-12-19 2.27.02.png" src="https://qiita-image-store.s3.amazonaws.com/0/135663/3cbd18dc-ae65-9114-18d5-393417a25da5.jpeg">


更に中を解剖すると、剣を刺す代わりにモーターが動くことで、スイッチが起動して棒が飛び出ることがわかります。モーターの土台には黒ひげ危機一発の剣を用いるため、装置との相性がかなり良くなっています。

<img width="283" src="https://qiita-image-store.s3.amazonaws.com/0/135663/3828c1c9-59fc-11fb-b5f7-43a83494873a.gif">

<img width="283" src="https://qiita-image-store.s3.amazonaws.com/0/135663/bf98dfaf-08df-ada6-b1a8-4541b3d4f8f1.png">


モーターはRaspberry Piから制御できるようになっています。
配線は以下の通りです。
<img width="283" alt="スクリーンショット 2018-12-18 1.04.47.png" src="https://qiita-image-store.s3.amazonaws.com/0/135663/47de704f-7814-ba04-c76c-9cee21750e73.png">

（参考）：http://windvoice.hatenablog.jp/entry/2016/01/10/224043

## "AI"rWeave 〜ふっとぶとん
ふっとぶとん**"AI"rWeave**は通常のふとんよりも250倍吹っ飛びます。（独自調べ）
もちろん、手縫いで作成しています。弊社は**裁縫力**の高いデータサイエンティストが多数存在します。
<img width="734" alt="スクリーンショット 2018-12-19 1.17.25.png" src="https://qiita-image-store.s3.amazonaws.com/0/135663/6f261b93-8c8f-9b62-049c-b5f3cc31641f.png">


格段に吹っ飛ぶ秘密は、独自開発の内部構造にあります。
敷布団の底に「[板紙を多層構造で強靭にし、包装資材などに使用できるよう加工した板状の紙製品](https://ja.wikipedia.org/wiki/%E6%AE%B5%E3%83%9C%E3%83%BC%E3%83%AB)」を入れ込むことで、柔らかい寝心地を保ちつつ、吹っ飛ぶ高さを何倍にも引き上げています。
<img width="642" alt="スクリーンショット 2018-12-19 2.33.37.png" src="https://qiita-image-store.s3.amazonaws.com/0/135663/f5555c1b-eea4-1fe4-5f71-f61244fb8cf4.png">

# 今後の発展
今後は以下のような機能をアップデートしていきたいと考えております。

- **ダジャレの評価ができるようになる**（最重要）
- ダジャレ判別の精度を上げる
- WEBブラウザで誰でもアクセスできるようにする
- 吹っ飛ばすパワーをさらに上げる

# 制作チームについて
今回の制作チームは、会社非公認slackに密かに存在する「思いついたダジャレを投稿し、全力で褒め合うチャンネル」#who_says_dajare（邦訳：ダジャレを言うのは誰じゃ）で呼びかけたことから始まりました。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/135663/0ad7c5b7-054d-1e50-3efd-909a45174d96.png)
※画面に写っている４人が制作メンバー

「ダジャレをたくさん思いつくんだけど、誰にも褒めてもらえない(´；ω；｀)」という人はぜひ[ブレインパッド](http://www.brainpad.co.jp/recruit/)で一緒に働きましょう！


# 参考文献

- [ダジャレ TechTalk](https://www.m3tech.blog/entry/2018/08/03/182447)
- [文章からダジャレのみを抜き出すコマンドを作ってみた](https://qiita.com/kurehajime/items/a922d42dff5e0f03d32c)
- [A Method for Detecting Japanese Puns using a Support Vector Machine and Consonantal Phonetic Similar Features](https://www.researchgate.net/publication/309962730_A_Method_for_Detecting_Japanese_Puns_using_a_Support_Vector_Machine_and_Consonantal_Phonetic_Similar_Features/download)
- https://mobile.twitter.com/takabosoft/status/1075550414601256960?s=12