Stimulator

機械学習とか好きな技術話とかエンジニア的な話とかを書く

ダジャレを判定する

- はじめに -

近年、IT業界のダジャレは熾烈の一途を辿っている(ITだけに) 。

類義語を巧みに取り入れたダジャレ、難読化されたダジャレなどが増加し、一体どれで「初笑い」すれば良いのか悩む若者も少なくない。


そのような背景があり、ダジャレを判定するアルゴリズムの開発も盛んである。
ルールベースによる判定では、@kurehajimeが提案、開発したdajarep *1 や、@fujit33によるShareka *2が存在する。特にSharekaは、ルールベースのロジックにも関わらず、反復型とされる種類のダジャレに対して高い精度での判定を可能にしている。また、機械学習モデルを用いた判定手法として、谷津(@tuu_yaa)らが開発したDajaRecognizer *3がある。DajaRecognizerは、多くのルールベースによって子音音韻類似度をPMIとして定義、Bag-of-Words、SVMを利用し、高い精度での駄洒落の機械学習モデルによる抽出を可能としている。


先行事例においては、一部の手法については触れられているものの、具体的な実装、ダジャレの解析まで踏み込んで、ダジャレの判定を行った事例は少ない。本記事は、ダジャレの判定、検出のタスクを例に、Pythonを利用した自然言語処理機械学習による実装例を紹介し、ダジャレ研究ないし「面白いとは何か」について計算機科学の側面から言及するものである(研究だけに)。

 

- ダジャレについて -

本記事で検出対象とするダジャレとは、ユーモアを含む1文ないし2文の短な文章の事である。

ダジャレに内包される「ユーモア」については、既に表層的な考察がいくつか行われ、様々な分類方法が提案されている。
谷津、荒木らの『駄洒落の面白さにおける要因の分析』*4 では、fig.1のように「音韻・統語的要因」「語彙的要因」「複合要因」の3つのセクションに分類している。@fujit33によるSharekaにおいては、fig.2のように「反復型」「潜在表現重複型」の2つの大きなセクションに分けた上で、反復型を更に「完全反復型」「不完全反復型」「変形反復型」の3つに分類している。

f:id:vaaaaaanquish:20201130222645p:plainf:id:vaaaaaanquish:20201130222928p:plain
左:fig1. 谷津、荒木らによる分類 右:fig2. @fujit33による分類

中でも音に関する分類、考察は非常に多く、@_329_は「ダジャレは音が非常に重要である」としながら、「潜在的な意味からくるダジャレもある」「人間の知識の成り立ちにも通づる」*5としている。その他、オノマトペに着目した報告 *6 や、対話や自己完結といった形で分類する考察 *7 もある。


実際に筆者も所属企業内向けのLTにて、「マルコフ連鎖によるダジャレ生成」「BERTによるダジャレ生成」「Elasticsearchを用いたダジャレ対話システムの構築」について話している。

文字列(ダジャレを言いシャレ) - Speaker Deck

上記スライドP11、P13にはそれぞれの生成モデルの結果サンプルを掲載しているが、これらの結果から「音に関する要素を含まないダジャレはダジャレと判断しづらい」事や「逆に意味空間からだけではダジャレの生成は難しい」事が見て取れる。また、後半の検索によるダジャレ対話システム実運用では、音や単語でのfuzzy searchだけでなく、Elasticsearchにおけるvector fieldsに埋め込み表現を入れる事で、対話相手に「上手い」と思わせるようなダジャレの検索、評価を行えた例を示している。

以上から、ダジャレにおけるユーモアとは、以下のような要素で構成されると筆者は考えている。

  • 音の繰り返し
  • 類似語、同意義語などの意味の側面
  • 関連単語を利用した勢い(文脈崩壊ダジャレなど)
  • 対話における前後の文脈

こういった先行研究については、以下のRepositoryのREADMEにまとめてある。
github.com
また、上記Repositoryには、複数のダジャレ掲載サイトをクロールするスクリプト群が含まれている。
学術分野では、荒木氏が公開している6万件ダジャレを含むダジャレデータセットが一般的に利用される*8。本記事では、そちらのデータセットを利用せず、上記スクリプトで取得したデータと、独自に社内外で収集したデータを合わせた121789件のダジャレデータを取り扱い、実験を進める。

クローラは、sleepの設定により全体のデータを収集するまでに2週間程かかるため、再現を行う際は注意が必要となる。学術目的であれば、ダジャレデータセットを申請すると良い*9

 

- ルールベースによる判定 -

本記事では、ルールベースのベースラインとして、@fujit33によるSharekaを参考に進める。
qiita.com
上記記事では、Sharekaクラスの一部メソッドが記述されておらず、記事の更新も見られないため、sentence_max_dup_rate、list_max_dupという2つのメソッド名から推察される動作を自前で書いたものを以下に設置した。
dajare-detector/shareka_v1.py at main · vaaaaanquish/dajare-detector · GitHub


Sharekaの挙動は、文章をカタカナに変換し正規化した後、n文字分のwindowをズラしながらカタカナの繰り返しを探索するものである。

f:id:vaaaaaanquish:20201211122321p:plain:w500
fig3. 先行事例Sharekaのロジック(n=3の例)

 

先行事例の再現と考察

実際に上述したスクリプト用いてSharekaアルゴリズムを、window sizeとなるnを変化させながら121789件のダジャレデータに対してのみ適応した結果を、以下に示す。

window size true false 検出率
n = 2 111924 9865 0.9190
n = 3 88199 33590 0.7242
n = 4 61807 59982 0.5075
n = 5 40348 81441 0.3313

また、上記の結果から、以下のような考察が立った。

  • 元記事にある通りnが小さい時の方がダジャレを検出できる
    • n=2はダジャレでない分を混ぜた場合に「ダジャレと判定されたが実はダジャレじゃなかった(False Positive)」が増大する事が知られている
    • nが小さい時ダジャレ文のダジャレではない所にも反応している可能性があるのではないか
    • nが大きい場合は検出数こそ少ないもののより確実にダジャレと言えるのではないか
    • validationデータを作成する時はこのnも考慮した方が良いのではないか
  • n=2でも抽出できない1万件弱はどういったデータか
    • それらに対する改善策はあるか
  • カタカナの正規化、繰り返しで9割のダジャレ候補が抽出できる
    • Webのようなダジャレ掲載サイトには「音」のような分かりやすい例が大半を占めているのではないか
    • 実際のダジャレ空間は広く「音」で検出できないデータこそ拾えると面白いのではないか
    • ダジャレでないデータセットを追加する時は音、意味それぞれの属性、量を考える必要がある

繰り返し判定の精度の確認

1つ目の考察「window sizeが小さい時の方がダジャレを検出できるが、小さすぎると誤検出が増える」については、実際にデータを見る事で確認できた。

キャラメル好きな人から、空メール!

このダジャレは「キャラメル」と「空メール」の語感が似ているというダジャレであるが、n=2の場合「好きな人から」と「空メール」のカラの部分が繰り返しとみなされ、ダジャレであると判定されてしまう。こういったダジャレが、実空間での判定時に「ダジャレと判定されたが実はダジャレじゃなかった(False Positive)」が増大する原因になっていると言える。
元記事は「できる限りダジャレを救う」ことを目的としているため良さそうであるが、実際にモデルに落とし込む際には考慮が必要となる。

また、window sizeが大きい場合の検出ダジャレ例をいくつか以下に示す。

州駅、工場で収益向上
家なかったなんて、言えなかった!
生憎、揉んだ愛に苦悶だ
SCANDALが「スキャン、だるっ!」
そう来た!異色の早期退職!
...

どれも短いながら工夫が凝らされたダジャレになっており、100件ランダムにサンプリングして目視した結果では、全て上記のような「どう取ってもダジャレと言える」ものであった。このことから、window sizeが大きい程、確実にダジャレを取得できる事がわかる。

 

検出ルールの改良

2つ目の考察である「n=2でも抽出できない1万件弱はどういったデータか、それらに対応可能か」について、実際にSharekaアルゴリズムが検出できなかった例を目視した所、いくつかのパターンによって検出できない例がある事がわかった。それらの中で、改善可能であるいくつかの点については改良を行った。

 

英単語の読み

 
第一に、以下のような英単語入りのダジャレが目立った。

Missは見捨てよう!
斧が折れちゃった…「Oh!No!!」
Hey!妖怪も併用かい?

ルールベースロジックでは、内部で文章を読みに変換する部分に利用しているmecabの辞書に以下のように英単語に対して日本語の読みが入っている場合があり、その場合は対応できていた。

Docker 名詞,固有名詞,一般,*,*,*,Docker,ドッカー,ドッカー
yesterday 名詞,固有名詞,組織,*,*,*,*

この辞書データを利用することで、現状のロジックでも、IT業界で使い古された著名なダジャレである「Dockerでどっかーん」は検出可能である。一方、heyやyesterdayのような一般的な単語を用いたダジャレは対応できていない。そのため別途、英単語をカタカナに変換する必要がある。幾つかの方法があるが、今回は簡易にalkanaモジュールを試した。

import alkana

print(alkana.get_kana("Hey".lower()))  # ヘイ
print(alkana.get_kana("Amazing".lower()))  # アメイジング
print(alkana.get_kana("yesterday".lower()))  # イエスタデイ

alkanaモジュールの内部では、事前に用意された辞書に対して単語を当てているだけではあるものの、こちらを利用することで6万単語以上をカバーすることができた。alkanaの中に単語はあるかな状態である。
こちらを組み込んだモデルをSharekaV2とし、以下に設置した。
dajare-detector/shareka_v2.py at main · vaaaaanquish/dajare-detector · GitHub
 
SharekaV2によって、新たに202個のダジャレの検出が可能となった。

 

形態素解析の複数パターン生成

第二に、mecabによるカタカナ変換時に形態素解析にい失敗しているパターンが見られた。

お骨を置こつ → オホネヲオコツ
さかなクン魚食う → サカナクンギョクウ
オタ会ってお高い? → オタアテオコウイ?

そもそもダジャレは日本語の文章としては崩壊している場合が多く、無茶な読ませ方をする事でダジャレとして成立させている場合も多い。こちらも辞書拡張など幾つか方法論が考えられるが、mecab形態素解析のパターンをparseNBestInitを利用して複数出して探索する事で解消した。

import MeCab

text = 'オタ会ってお高い?'
print(text)
mecab = MeCab.Tagger("-Ochasen -d /usr/local/lib/mecab/dic/mecab-ipadic-neologd")
mecab.parseNBestInit(text)
for i in range(11):
    node = mecab.nextNode()
    while node:
        n = node.feature.split(",")
        if len(n)==9:
            print(n[7].replace('*', ''), end=' ')
        node = node.next
    print()

オタ会ってお高い?
オタ アッ テ オ コウイ
オタ カイ ッテ オ コウイ
アッ テ オ コウイ
カイ ッテ オ コウイ
アッ テ オ コウイ
アッ テ オ コウイ
カイ ッテ オ コウイ
カイ ッテ オ コウイ
オタ カイ ッテ オ コウイ
オタ アッ テ オ タカイ
オタ カイ ッテ オ タカイ

この方法は、読みのパターンを出せば出すほど、先程示したような誤検出を増やす可能性があるが、ルールベースでは一度許容するものとした。こちらを組み込んだモデルをSharekaV3とし、以下に設置した。4,5件の誤検出はご検討願おう。
dajare-detector/shareka_v3.py at main · vaaaaanquish/dajare-detector · GitHub
 
SharekaV3によって、読みパターンを出来る限り出しどれかがダジャレと判定されるか、というロジックで新たに1979個のダジャレの検出が可能となった。

 

ローマ字読みによる音の繰り返し判定

第三に、以下のような長音を含むダジャレや、文中の一音を変更しリズムをとるダジャレに注目した。

モーターを壊してもうた
船を呼ぶね
孫がマンゴーを食べる

ここで「モーターを壊してもうた」は、Sharekaロジックでは「モタヲコワシテモウタ」に変換され、繰り返しが発生しないのでダジャレとして検出できない。

そこで、先程のカタカナ変換ロジック適応後、pykakasiモジュールを用いてローマ字表記に変換した。

from pykakasi import kakasi

k = 'モーター ヲ コワシ テ モ ウタ'
print(k)
for r in ['Hepburn', 'Kunrei', 'Passport']:
    kks = kakasi()
    kks.setMode('K', 'a')
    kks.setMode('r', r)
    conv = kks.getConverter()
    print(conv.do(k))

当たる範囲を広げるためにHepburn式以外も採用した。

モーター ヲ コワシ テ モ ウタ
mootaa wo kowashi te mo uta
mootaa o kowasi te mo uta
mootaa wo kowashi te mo uta

このローマ字列に対し、前述のようなwindow sizeのロジックをローマ字に適応すると、過剰にマッチが発生してしまうため「全ての単語に対してLevenshtein距離が1以内の文字列が文中にあるかどうか」を判断基準とした。

f:id:vaaaaaanquish:20201211122132p:plain:w500
fig4. ローマ字パターン判定
from fuzzysearch import find_near_matches

def roman_match(sentence, min_length=3):
    words = [x for x in sentence.split(' ') if len(x)>=min_length]
    choices = ''.join(sentence.split(' '))
    for word in words:
        if len([x for x in find_near_matches(word, choices, max_l_dist=1) if len(x.matched)>=len(word) and x.matched!=word])>1:
            return True
    return False

roman_match( 'keeki de keiki zuke' )  # True

このロジックによって ouoo のような長音、カタカナによって発生する違いを吸収しつつ、「音を変化させるダジャレ」についても対応した。

おっと、こちらを組み込んだモデルもSharekaV4として以下に設置した(音だけに)。
dajare-detector/shareka_v4.py at main · vaaaaanquish/dajare-detector · GitHub
 
SharekaV4によって、新たに4848個のダジャレの検出が可能となった。音を変化させるダジャレがそれなりの数あることが分かった。

ルールベースの改良結果

ここで、V4までの差分を以下に示す。V4では、V3までのカタカナの反復を想定したアルゴリズムとは別で単体で検出数を測る事ができるため、そちらも別途測定を行った。

前バージョンとの差分 取得数割合 V3,V4のみの取得数
Shareka (origin, カタカナ反復) 111924 0.9190 -
SharekaV2 (英単語読み) 202 0.9207 -
SharekaV3 (読み複数生成) 1979 0.9369 114105
SharekaV4 (ローマ字fuzzysearch) 4848 0.9767 108508

実際にダジャレデータの中の97.6%を判定する事ができた。また、Sharekaのようにwindowをズラして反復を検出した場合とローマ字に直した場合では、ローマ字に直す時点で情報が落ちているダジャレも多いため、ダジャレの検出という意味で前者が良い事がV3、V4の比較でわかった。このルールベースの手法は再現率(recall)を極端に高める方法であるため、実世界での応用については、後述するようなダジャレでないデータと混ぜた上での検証が必要となる。

そもそものデータに関する考察

3つ目の考察である「反復と音を利用したルールベースロジックで9割以上ものダジャレが検出できる事は本当に正しいのか」という点について、実際に目視で検出できなかった例を確認した。SharekaV4でダジャレデータセット内で検出できなかったダジャレは2836件であった。検出できなかった例を目視で見ていくと、幾つかのパターンが見受けられた。

# 日英翻訳という知識ベースの例
3月のテーマソングは、マーチ!
日曜は起こさんでー!
猫がキャッと鳴く
驚異のバスト
手紙が破れたー

# 音が遠い、判定できない例
メガネのメーカーね
斜めに7メートル進む
令和の0話

# カッコや+、&等の記号の読みを含む例
(E)←この文字、かっこいい
バター&アンドーナッツ
3+5が解けるサンタすご

# 文脈が崩壊しているが定型句と勢いをユーモアとする例
時すでにお寿司!
今、何時?ゼロ歳児!
飢えすぎ謙信

# 「滋賀が近江国であった」のような事前知識を要求する例
これ滋賀のおーみやげ!
葡萄が驚いた「キョホー!」
このガンダム、起動せんし!

日英翻訳については、名詞に絞って翻訳する事で対応が可能そうである。遠い音についても、数字、記号、アルファベットの対応表を作る事で更に精度改善が見込めそうである。一方、勢いを重視するダジャレ、事前知識を要求するダジャレについては、ルールベースでの対応の難易度は高い。古典的にはWordNetのような単語の類義関係を用いて語を増強するといった解決策がありそうではあるし、最近だと単語埋め込みにするのが一般的であるが、いずれにしても「起動せんし → 機動戦士」「キョホー → 巨峰」のような揺れの吸収を行う複雑なタスクとなる事が考えられる。

また、ダジャレデータセット自体の課題として以下のようなノイジーなデータがある事もわかった。

# ユーザ投稿型のサービスでユーザ同士のやり取りがダジャレとして投稿される場合
~~は面白くないから, (・∀・)イイネ!!
 
# 汎用的、暴力的などのワードを伏せ字で置き換えた場合
雑煮は○臓にいい

自前で作成したダジャレデータセットは、ある程度事前にノーマライズ処理をかけているが、こういったデータが含まれる事があり、Sharekaのようなルールベースロジックでご検出されている場合や、形態素解析が難しくなる事も考慮する必要がある事がわかった。先に述べた荒木らが公開している6万件ダジャレを含むダジャレデータセットでは、目視やクラウドソーシングによるチェックが行われているらしく、そちらによる評価もfuture workとなると考えられる。


本記事では、ダジャレデータセット内の非検出ダジャレの数が少ないことから、一度ルールベースでの精度向上については、ここまでとする。

ダジャレ、非ダジャレデータセットの作成

まず試しに青空文庫の作品名を対象とし、前述のルールベースロジックを適応してデータセットを構築した。青空文庫のデータのみでは課題がある事がわかったため、別途Twitterを利用したデータセット作成にも取り組んだ。

また、ここで前述のルールベースのクラスが若干使いにくく速度面でも課題が出てきていたため、一度リファクタを実施した。スクリプトを以下に示す。
dajare-detector/dajare_detector.py at main · vaaaaanquish/dajare-detector · GitHub


青空文庫を利用したデータセット作成

ルールベースロジックには「カナの繰り返しを判定するワード長(window_size)」「fuzzysearch元となるローマ字節の最小単位(min_roman)」「mecabで候補パターンを生成する最大数(mecab_pattern_num)」の3つのパラメータがある。ここでは、上記スクリプトを利用し、一度mecab_pattern_numを出来る限り大きく50で固定した上で青空文庫タイトル17493件、ダジャレデータセットを各パラメータで判定した結果が以下のようになった。

parrameter 青空文庫ダジャレからの検出数 ダジャレデータセットからのダジャレ検出数
window_size=2, min_roman=2 2853 (0.16) 118953 (0.98)
window_size=3, min_roman=2 1389 (0.079) 95932 (0.79)
window_size=4, min_roman=2 1138 (0.065) 80485 (0.66)
window_size=2, min_roman=3 2310 (0.13) 95382 (0.78)
window_size=3, min_roman=3 601 (0.034) 78014 (0.64)
window_size=4, min_roman=3 297 (0.017) 57647 (0.47)
window_size=2, min_roman=4 2185 (0.12) 95316 (0.78)
window_size=3, min_roman=4 439 (0.025) 77495 (0.64)
window_size=4, min_roman=4 127 (0.0073) 56483 (0.46)

元ロジックとなっている先行事例のSharekaでの検証同様、繰り返し判定の幅の単位が小さい程、ダジャレ検出数が増えるが誤検出も増える結果となった。また、前述の通り、ダジャレデータベースからも抽出できていなダジャレが存在しており、このダジャレを判定しながら誤検出を下げる試みが必要である事も示された。

実際に青空文庫から間違った例2853件を目視で確認したが、ダジャレと思えるタイトルは見当たらなかった。実際に誤検出された例を以下に示す。

百万人のそして唯一人の文学 - 1973 - 青野季吉

このタイトルは以下のように分解され、「ニンノ」が繰り返されている事からルールベースではダジャレと判定される。

ヒヤクマンニンノソシテタダイチニンノブンガク
['ヒヤク', 'ヤクマ', 'クマン', 'マンニ', 'ンニン', 'ニンノ', 'ンノソ', 'ノソシ', 'ソシテ', 'シテタ', 'テタダ', 'タダイ', 'ダイチ', 'イチニ', 'チニン', 'ニンノ', 'ンノブ', 'ノブン', 'ブンガ', 'ンガク']

この例から分かるように、音の繰り返しは日常的に発生し得る事がわかる。

 
上記の結果を参考に、ここでは、青空文庫およびダジャレデータセットのデータを活用し、以下の2つのデータセットを作成した。

青空文庫(負例) ダジャレデータセット(正例)
データセットA 全データ 17493 ランダムサンプリング 17493
データセットB ルールベースで誤検出したもの 2853
誤検出されなかったデータからサンプリング 2853
ルールベースで検出できなかったもの 2836
検出できたものからサンプリング 2836

このデータセットでは、人類は半分以上の会話でダジャレを言いたいものであると仮定し、実世界内でのダジャレの分布などは加味しない。

 

Twitterを利用したデータ作成

実世界上での応用に向け、TwitterAPIを利用し、別のデータセットも作成した。上記のデータセットは、実世界でのダジャレの割合だけでなく、文章内に出てくる単語の分布も大きく変わってしまう課題が存在する。そのため、ダジャレデータセットから形態素解析によりダジャレに出てくる名詞 51551 件を抽出、Twitterから1単語に対して100件のツイートを取得する検索スクリプトを以下のように作成した。

# RTやリンク付きツイートにダジャレが少ないと仮定
import tweepy
api = tweepy.API(...)
q = f'{word} -RT lang:ja -filter:links -filter:retweets'
api.search(q=q, tweet_mode='extended', result_type='recent', count=100)

dajare-detector/twitter_crawler.py at main · vaaaaanquish/dajare-detector · GitHub

このスクリプトを一週間程動作させ、2020/11/10日時点で最新の、ダジャレデータに使われる単語を含む、50字以内の 1916748件 のユニークなツイートデータを取得した*10。このデータにおいては、真の正解(このデータ内にダジャレが本当はいくつ含まれるか)の判断が難しいため、データセットA, Bでモデルを作成した後、実世界でのサンプルとして利用し、検出できたもののみ黙々と目視で黙視する。

ツイートデータ
データセットC 全データ 1916748
データセットD ルールベースで検出されたもの 20000
検出されなかったものからサンプリング20000

データセットCに対してルールベースの判定ロジック (window_size=2, min_roman=2) を適応したところ、1574616件 (0.821) が抽出された。実世界に先述のルールベースロジックを適応した時、その結果の多くが誤検出となる事がわかる*11。そのためデータセットDは全体の1%である2万件をルールベースの判定を基準としてランダムサンプリングし作成した。データセットC、Dは殆どがダジャレでないが一部ダジャレの可能性があるノイジーなデータとして扱う。

機械学習を用いたダジャレの判定

先行事例であるDajaRecognizerでは、子音に関する素性およびBag-of-Words、SVMを用いた識別でAUC 90%を達成したとの報告されている。下記のRepository内にあたる。
gitlab.com
ロジックに関する解説のdocumentにも解説がある通り、ロジックとしては以下のようになっている。

  • ルールベース
    • 文章をmecab形態素解析 (mecabによる変形、連続をどちらも取得)
    • それぞれモーラ音素に変換 (1字ごとのローマ字に)
    • Needleman-Wunsch algorithmで子音と母音それぞれでアライメント
    • アライメントした音のペア対応から共起頻度を取る
  • BoW

ルールベースを本記事内で利用しているロジックと比較すると、NWで単語の音が似たペア行列を作成するまではほぼ同等の作業であるが*12、ペアの共起頻度を取り出現頻度を用いた自己相互情報量のように扱っている点が工夫されている( 前述のような「3 人の (ninno)」は一般的な文章において発生しがちで「 麦の向き(mugi no muki)」のような音の繰り返しは一般的に発生しにくい事からダジャレの可能性が高い、という情報を数値化し閾値で判定するイメージ )。

BoWは単語の出現回数をベースとした手法であり、事前に実施したマルコフ連鎖によるダジャレ生成がそれなりのダジャレを生成していた事からも、出現ベースの分類はある程度上手くいくだろうという事がわかる(「布団」と「吹っ飛ぶ」という単語を同じ文章に入れる人がダジャレ以外を想定している可能性は限りなくゼロに近い事を判定基準にするイメージ)。

DajaRecognizerにはNgram、RNNによる素性生成の記述があるが、コードを見る限り実際は実装されていないようである。

今回は、上記の手法を参考にしつつ、お手製featureおよび、Deep Neural Networkベースの手法によって判定を試みた。

 

Sentence-BERTを用いたベクトル化

一度DNNベースのモデルによる特徴量を生成しEDAを行う。特徴量化にはSentence-BERT*13を用いた。*14

実装と学習済みモデルを簡易に利用する方法として、以下が公開されている。
github.com

上記のsentence bert modelは、日本語の学習済みモデルを@sonoisa氏が公開している*15。そちらのモデルをロードし、文章をfeature vectorに変換するスクリプトを以下に示す。

# pip install git+https://github.com/sonoisa/sentence-transformers
import tempfile
import tarfile
import os
import requests
from sentence_transformers import SentenceTransformer

def load_sentence_bert_ja():
    url = "https://www.floydhub.com/api/v1/resources/JLTtbaaK5dprnxoJtUbBbi?content=true&download=true&rename=sonobe-datasets-sentence-transformers-model-2"
    with tempfile.TemporaryDirectory() as d:
        model_path = os.path.join(d, 'training_bert_japanese.tar')
        with open(model_path, 'wb') as f:
            for chunk in requests.get(url, stream=True).iter_content(chunk_size=1024):
                f.write(chunk)
        tarfile.open(model_path).extractall(d)
        model = SentenceTransformer(model_path.strip('.tar'), show_progress_bar=False)
    return model

model = load_sentence_bert_ja()
sentence_vectors = model.encode(['ウシがあばれてモーたいへん'])
print(type(sentence_vectors[0]), len(sentence_vectors[0]))    # <class 'numpy.ndarray'> 768

上記でデータセットAを特徴量化し、正例負例それぞれ1000件ランダムサンプリング、t-SNE*16によって2次元で可視化した所、以下の結果が得られた。

f:id:vaaaaaanquish:20201205143053p:plain:w400
fig5. 正例負例1000件ずつt-SNEで可視化

綺麗に分割とまではいかないが、学習済みのsentence bertから得られる特徴で分類タスクを実施できそうな事がわかる。BERTでバーっとやれば良いわけだ。

 

頻度ベースの特徴量生成

先行事例であるNeedleman-Wunsch algorithmによるアライメントと同等の処理を行うため、ルールベースで抽出した繰り返しカタカナ、繰り返しローマ字から、相互情報量を算出し、ルールベースで繰り返しが見つかる場合は特徴量に追加、見つからない場合はnp.zerosを追加した。

f:id:vaaaaaanquish:20201211041050p:plain:w500
fig6. 繰り返しをmatrixにしてPMIへ

その他お手軽特徴量

一般的なNLPタスクで利用されるものとして、ダジャレ判定において有用そうな以下の特徴量を利用した。

  • 文字列の長さ
  • mecab分割単語数
  • TF-IDF vector
  • 音の繰り返しの有無(カナ、ローマ字をwindow_size、min_roman 2~4ごとに)

実験

識別器にはLightGBM (LGBM) を利用した。またハイパーパラメータは全てOptunaによる最適化を実施した。
また、データセットよりtest, validationとして20%ずつ分割した。

BERT+LGBM

BERTによる特徴量のみの場合の、データセットAのtestセットに対するclassification_reportを以下に示す。

              precision    recall  f1-score   support
           0       0.92      0.80      0.85      3378
           1       0.97      0.99      0.98     20480
    accuracy                           0.96     23858
   macro avg       0.94      0.89      0.92     23858
weighted avg       0.96      0.96      0.96     23858

t-SNEでの可視化の通り、BERTを利用する事のみで、ある程度の精度でダジャレの認識を行える事が示された。

実際にルールベースで検出できず、BERT+LGBMにより検出されたダジャレを以下に示す。

この石はなんでストーン
雪が積もってまスノー!
今、何時?法隆寺!

「今何時?〇〇zi」「君何部?〇〇bu」のようなテンプレートが若干過学習されている傾向が見られたものの、「石」と「ストーン」、「雪」と「スノー」のような日英翻訳という事前知識を必要とするダジャレを検出できていると言える。

BERT+お手製feature+LGBM

各特徴を作成しconcatしたものをLGBMで学習した。データセットAのtestセットに対するclassification_reportを以下に示す。

              precision    recall  f1-score   support
           0       0.95      0.93      0.94      3378
           1       0.99      1.00      0.99     20480
    accuracy                           0.98     20480
   macro avg       0.97      0.96      0.96     20480
weighted avg       0.98      0.98      0.98     20480

BERT単体に比べ、featureを追加した場合に、高い精度でダジャレと非ダジャレが分類できると言える。特にランダムに選ばれたtest setに対しては、recallが1.0となっており、全てのダジャレを検出する事ができた。ダジャレを過剰に抽出していた例を以下に示す。

text    rule_base
職業婦人に生理休暇を!       True
ボン・ボヤージ!       True
露西亜よ汝は飛ぶ       True
ヒューメーンということに就て       False
私が占ひに観て貰つた時       False
わが工夫せるオジヤ       False

実際ルールベースで繰り替えしが判定された上で、文章的に感嘆符が付くなどして勢いがあるもの。もしくは、少し複雑な仮名遣いをしている場合に誤検出してしまう事が見て取れる。

  

データセットB

データセットBに対して、BERT+お手製feature+LGBMを利用し、同様に実験を行った。データセットBのtestセットに対するclassification_reportを以下に示す。

           0       0.93      0.89      0.91      5706
           1       0.94      0.96      0.95      5672
    accuracy                           0.94      3482
   macro avg       0.94      0.93      0.93      3482
weighted avg       0.94      0.94      0.94      3482

実際にルールベースで事前に判定した上でサンプリングしており、誤検出の割合が0.9を下回っていることから、BERTやfeatureが精度に貢献している事がわかる。また、データセット内の偏りがない場合でも、分類性能のあるモデルになっている事がわかる。

 

データセットCおよびD

実空間上のデータへの適応として、データセットCおよびDに対して、データセットAで作成したモデルを適応する。

データセットCでダジャレとして検出された 328 (0.0082)
データセットCでルールベースで検出されなかったがモデルにより検出された 91 (0.0022)
データセットDでダジャレとして検出できた 1998 (0.0104)

ダジャレモデルで検出されるデータは、概ねツイートデータの1%以下となる事がわかった。
また、データセットCで抽出された328件のツイートを目視で確認した。その時ダジャレであると判断できたものは151件であり、目視したうちの46.0%、ツイート全体の0.38%がダジャレであった。データセットDにおいては、事前のルールベースによるデータの偏りがないため、多くが誤検出であった。モデルの学習を対象ドメインにfitするよう工夫する必要がある事もわかった。

このモデルがrecallが高いモデルであることから、筆者の体感に比べ、Twitterでダジャレ言ってる人はかなり少ないということになる。20%は居ると思っていたが。この記事を見ているあなたはもっとユーモアを出して欲しい(you moreだけに)。

 
参考までに実際に抽出されたダジャレツイートの例を以下に示す。
 
クロール期間中、ちょうどユリ・ゲラー氏と任天堂が和解した件で盛り上がっていた。


 
Twitterっぽい。好き。
 
ダジャレbotのような存在も複数検出された。
 
そうだね、プロテインだね。

まとめ

本記事では、既存のルールベースを拡張するだけでなく、NLPを利用した機械学習モデルによるダジャレ判定を行った。また、ダジャレ界隈において今日まで行われていなかったDocker、poetryを利用し、データ以外のスクリプト部分について再現可能な状態を作成した。
github.com

結果として、人工的なデータセットであれば98.9%の精度でダジャレかどうかを判定できた。Twitterのような実世界でも、ルールベースで判定した後であれば、46.0%がダジャレとして見なせるよう抽出できた。一体誰が嬉しいのか全く分からないができた。

副次的に、Twitterでダジャレを言っているユーザが少ない事も示唆された。SNSとしてかなりマシさ(示唆だけに)。

おわりに

このような実験の先に、コントやTV番組、ラジオ、YouTube、日常会話において面白いと感じるポイントの検出や、なぜ人が面白いと感じるのか、面白さは生成できるのか、笑いの歴史ごとの違いは何か、今バズっている面白いものは何か、自分を面白くするにはどうトレーニングすべきか、ダジャレの起源、AIとは、といった課題があったらいい気がする知らんけど。

感想や指摘はTwitterはてブにくれたら見て対応するかも知らんけど。

先行事例を世に送り出した各位に心から謝礼を送りたい(シャレだけに)。

f:id:vaaaaaanquish:20201211051055p:plain:w0

 

*1:kurehajime. 文章からダジャレのみを抜き出すコマンドを作ってみた, https://qiita.com/kurehajime/items/a922d42dff5e0f03d32c, Qiita, 2015/8/27

*2:fujit33. おもしろいダジャレを入力すると布団が吹っ飛ぶ装置を作った, https://qiita.com/fujit33/items/dbfbd7a2aa3858067b6c, Qiita, 2019/1/9

*3:青山学院大学, 谷津 元樹. 音韻類似性を考慮する教師あり機械学習を用いた駄洒落検出環境, GitLab: https://gitlab.com/m-yatsu/djr_wpsm, ニコニココメントデータからの駄洒落検出: https://www.nii.ac.jp/dsc/idr/userforum/startup/IDR-UF2019_S03.pdf, http://arakilab.media.eng.hokudai.ac.jp/~araki/2018/2018-D-11.pdf

*4:谷津元樹, 荒木健治. "駄洒落の面白さにおける要因の分析." 日本知能情報ファジィ学会 講演論文集 第32回ファジィシステムシンポジウム. 2016. https://www.jstage.jst.go.jp/article/fss/32/0/32_237/_article/-char/ja/

*5:ダジャレ TechTalk - エムスリーテックブログ https://www.m3tech.blog/entry/2018/08/03/182447, 2018/8/3

*6:内田ゆず, 荒木健治, "オノマトペに着目した駄洒落の面白さの分析―駄洒落の自動生成に向けて―." 日本知能情報ファジィ学会 第35回ファジィシステムシンポジウム, 2019, https://www.jstage.jst.go.jp/article/fss/35/0/35_332/_article/-char/ja/

*7:KOBAYASHI Yoshitomo. "駄洒落の基本構造と笑い" 東京外国語大学 http://www.tufs.ac.jp/st/personal/03/conanweb/dajare.htm

*8:荒木健治. "駄洒落データベースの構築及び分析" ことば工学研究会: 人工知能学会第 2 種研究会ことば工学研究会 57 (2018): 39-48. http://arakilab.media.eng.hokudai.ac.jp/~araki/2017/2017-C-3.pdf

*9:ブログに書くネタ記事のために学術目的としてデータセット申請するのは私は流石に恐れ多くて無理の助

*10:100字以上のダジャレはfuture workとする

*11:この世のツイートの82%がダジャレであるなら別である

*12:fuzzysearchで使ったLevenshtein距離による類似性とNWによる類似性はequivalentである事が知られている Sellers PH (1974). "On the theory and computation of evolutionary distances". SIAM Journal on Applied Mathematics. 26 (4): 787–793. doi:10.1137/0126070. https://epubs.siam.org/doi/10.1137/0126070

*13:Sentence-BERT: Sentence Embeddings using Siamese BERT-Networks,Nils Reimers, Iryna Gurevych, EMNLP 2019, https://arxiv.org/abs/1908.10084

*14:筆者は最近何も考えずsentence bertに入れて目的のタスクが解けそうか試してからNLPタスクを始める事が多い

*15:【日本語モデル付き】2020年に自然言語処理をする人にお勧めしたい文ベクトルモデル - Qiita https://qiita.com/sonoisa/items/1df94d0a98cd4f209051

*16:Maaten, Laurens van der, and Geoffrey Hinton. "Visualizing data using t-SNE." Journal of machine learning research 9.Nov (2008): 2579-2605. https://lvdmaaten.github.io/publications/papers/JMLR_2008.pdf