はじめに
最近、とある動画シリーズ1の影響で
「〇〇すぎて、、、〇〇になった」
という表現にハマっています。
最近だと、朝家を出る時に寒すぎて、、、サムギョプサルになった。みたいな。
ただ、後半の「〇〇になった」の部分は意外といい感じの単語が出てこなくて、
「眩しすぎて、、、マーブルチョコになった」
みたいに、ニアミスな単語セレクトで妥協してしまうこと、あると思います。
そんな自分を戒めるために、COTOHA APIを使って、自分が考えたウマすぎて構文(リスペクトを込めてこう呼びたいと思います)を採点してもらうプログラムを書きました。
採点方法
ウマすぎて構文は要するに、
「(形容詞語幹)すぎて、、、(名詞)になった」
この、形容詞語幹と名詞の類似度が高いほど良い構文である、ということができるでしょう。
しかし、名詞が意味のわからない単語であってもいけません。
名詞部分が一般的な単語でない場合は0点としたいと思います。
形容詞語幹と名詞の類似度についてはとりあえずレーベンシュタイン距離を採用したいと思いますが、
冒頭で申し上げた「寒すぎて、サムギョプサルになった」のように、名詞側が無駄に長くなってしまっても減点をしたくありません。そのため名詞側は形容詞語幹側の文字数までしか見ないことにします2。
使用例
$ echo "ウマすぎてウマになった" | python orochimaru.py
100.0点ね
ウマすぎて、、、ウマになったわね、、、
$ echo "眩しすぎてマーブルチョコになった" | python orochimaru.py
33.3点ね
マブシすぎて、、、マーブルチョコになったわね、、、
$ echo "面白すぎてオモシロになった" | python orochimaru.py
0点ね
オモシロすぎて、、、オモシロになったわね、、、
準備
入力テキストの構文解析と、名詞が一般名詞かどうかの判定をするために、COTOHA API を使用します。
COTOHA API Portal からDevelopersアカウントを作成し、Client IDとClient secretをメモしておきます。
また、PyPi には登録されていませんが、GitHub で python ライブラリが公開されているので、こちらの記事を参考にインストールします。
もちろん直接APIを叩く場合は必要ありませんし、現時点では構文解析と類似度算出にしか対応していないようなので注意してください。
python は3.6を使っています。pyenvとかを使っている場合はよしなにしてください。
$ git clone https://github.com/obilixilido/cotoha-nlp.git
$ cd cotoha-nlp/
$ pip install -e .
これで、COTOHA APIの構文解析が使えるようになりました。
実装
レーベンシュタイン距離の実装
wikipediaのアルゴリズムをそのまま実装しただけなので、詳細は割愛させていただきます。
def levenshtein_distance(s1, s2):
l1 = len(s1)
l2 = len(s2)
dp = [[0 for j in range(l2+1)] for i in range(l1+1)]
for i in range(l1+1):
dp[i][0] = i
for i in range(l2+1):
dp[0][i] = i
for i in range(1, l1+1):
for j in range(1, l2+1):
cost = 0 if (s1[i-1] == s2[j-1]) else 1
dp[i][j] = min([dp[i-1][j]+1, dp[i][j-1]+1, dp[i-1][j-1]+cost])
return dp[l1][l2]
ウマすぎて構文解析
すごい雑な実装ですが、「〇〇すぎて〇〇になった」にマッチするテキストを入力すると、点数と解析結果の文章が出力されます。
from cotoha_nlp.parse import Parser
import levenshtein_distance
def find_orochi_sentence(tokens):
form_list = ["", "すぎ", "て", "", "に", "な", "っ", "た"]
pos_list = ["", "形容詞接尾辞", "動詞接尾辞", "", "格助詞", "動詞語幹", "動詞活用語尾", "動詞接尾辞"]
i = 0
s1 = ""; s2 = ""
is_unknown = False
for token in tokens:
if (i > 7): return 1
if (i == 0):
if not (token.pos == "形容詞語幹"): return 1
s1 = token.kana
elif (i == 3):
if not (token.pos == "名詞"): return 1
s2 = token.kana
if ("Undef" in token.features):
is_unknown = True
else:
if (i == 4 and token.pos == "名詞"):
s2 += token.kana
if ("Undef" in token.feautes):
is_unknown = True
continue
if not (token.pos == pos_list[i] and token.form == form_list[i]): return 1
i += 1
if is_unknown:
print("0点ね")
else:
dist = levenshtein_distance.levenshtein_distance(s1, s2[:len(s1)])
print(f"{(100 * (len(s1) - dist) / len(s1)):.1f}点ね")
print(f"{s1}すぎて、、、{s2}になったわね、、、")
return 0
parser = Parser("YOUR_CLIENT_ID",
"YOUR_CLIENT_SECRET",
"https://api.ce-cotoha.com/api/dev/nlp",
"https://api.ce-cotoha.com/v1/oauth/accesstokens"
)
s = parser.parse(input())
if find_orochi_sentence(s.tokens) == 1:
print("これはウマすぎて構文ではありません")
COTOHA API の構文解析では、各単語の形態素情報3が得られるのですが、当該単語が未知語の場合、その中の "features" に "Undef" という情報が付与されます。
その情報をもとに、ウマすぎて構文の名詞部分が一般名詞かどうかを判定しています。
また、類似度算出の際に漢字が含まれると、表記揺れの問題があるので、カタカナの読みを用いて比較を行っています。そのため、COTOHA APIで想定と異なる読み方だと認識されると正しく判定できないことになります。(例:辛すぎて面になった)
ウマすぎて構文マスターの中には、「〇〇すぎて杉になった」 とすることで、いい感じの単語を思いつかない問題に対応する人もいますが、これはズルなので評価しません。
おわりに
これで、ウマすぎて構文を思いついた時に、いつでも客観的に評価してもらえようになりました。
今回は形態素解析のために、COTOHA API を使ってみましたが、簡単に使えて、結構いろんな単語にも対応しているので便利だなーと感じました。「〇〇になった」部分で、〇〇が未知語でもちゃんと名詞だと推定されているのも偉いなと思います。無料版だとAPIリクエスト数の制約(1日1000回)はありますが、遊びで使う分には問題ないレベルかなと思います。
皆さんも、ウマすぎて構文を使ってみてください。
ありがとうございました。
参考
-
ここはツッコミポイントで、もっとちゃんとした実装方法はあると思います。 ↩