tomabouの日記

Haskellなどで勉強したことなどを書いていきます

AIの「語彙力」を制御する

「よくある単語」判定

りんなというMicrosoftの作ったチャットAIがあります。りんなにしりとりを挑むと「そんな単語知るかよ」という単語を連発してきて人間はとてもではないですが勝てません。しりとりのような言葉を用いた遊びをするAIを作成するには適切にAIの語彙力を制限する必要があります。今回は日本語コーパスを利用することで「簡単な単語」かどうかを簡単に判定できる、ということを紹介したいと思います。

日本語コーパス

コーパスというのは言語の研究などをするために大量の文章を単語ごとに分けて、単語の働きで分類してタグ付けしたものです。みるからに死ぬほど手間がかかりますが、国立国語研究所が作成しており、単語リストは無料で公開されています。
その単語リストを利用すると各単語がどれほどの頻度で出現するかを知ることが出来るので、簡単に「よくある単語」と「難しい単語」の識別が出来ます。

具体例

試しに次のようなアプリを作ってみました。ある三文字の単語から別の三文字の単語まで、一文字づつ変えることで到達するルート(例えば、課題→指定→視点→資金→付近→布団)を教えてくれます。
以下のコードをダウンロードしたBCCWJ_frequencylist_suw_ver1_0.tsvと同じディレクトリにおいて実行すると、できるだけ簡単な単語を使ってできるだけ短いルートでスタートからゴールまで変化させてくれます。
なお出力される二番目の数字は一億単語中何回現れるかの頻度を表しています。

出力例
('カダイ', 10, 'スタート')
('カテイ', 13306.0, '家庭')
('シテイ', 13030.0, '指定')
('シテン', 3687.0, '視点')
('シキン', 11181.0, '資金')
('フキン', 3552.0, '付近')
('フトン', 10, 'ゴール')
コード

3000という値はパスの長さと単語の簡単さどちらを重視するかのパラメータです。小さくすると短いものを重視します。最短パスはダイクストラで求めています。

import re
import heapq

start_word = "カダイ"
goal_word = "フトン"
length_or_frequency = 3000

PATTERN = re.compile("普通名詞")
print("hello")
KATAKANA = [chr(i) for i in range(12449, 12532+1)]
FILE = open('BCCWJ_frequencylist_suw_ver1_0.tsv', 'r', errors='replace', encoding='utf-8')
index_profile = list()
yomi_to_index = dict() 

def make_node(index, yomi, frequency, kanji):
    """make node for Dijkstra"""
    index_profile.append((yomi, frequency, kanji))
    if not yomi in yomi_to_index:
        yomi_to_index[yomi] = [index]
    else:
        yomi_to_index[yomi].append(index)


i = 0
for line in FILE:
    l = line.split('\t')
    mOB = PATTERN.search(l[3])
    if mOB:
        if len(l[1]) == 3:
            make_node(i, l[1], float(l[6]), l[2])
            i = i+1


START = i
GOAL = i+1

def search_path():
    """do Dijkstra"""
    make_node(START, goal_word, 10, "ゴール")
    make_node(GOAL, start_word, 10, 'スタート')

    que = []
    heapq.heappush(que, (0, START))

    costs = dict()
    preindex = dict()

    while que != []:
        (cost, v) = heapq.heappop(que)
        (oyomi, ofreq, ka) = index_profile[v]

        for nyomi in [oyomi[0]+oyomi[1]+c for c in KATAKANA] \
            + [oyomi[0]+c+oyomi[2] for c in KATAKANA] \
            + [c+oyomi[1]+oyomi[2] for c in KATAKANA]:

            if oyomi != nyomi and nyomi in yomi_to_index:
                for nindex in yomi_to_index[nyomi]:
                    ncost = cost + 1 + length_or_frequency/index_profile[nindex][1]
                    if not nindex in costs:
                        costs[nindex] = ncost
                        preindex[nindex] = v
                        heapq.heappush(que, (ncost, nindex))
                    elif ncost < costs[nindex]:
                        heapq.heappush(que, (ncost, nindex))
                        costs[nindex] = ncost
                        preindex[nindex] = v

    if GOAL not in costs:
        print("cannot reach")
    else:
        ind = GOAL
        while True:
            print(index_profile[ind])
            if ind==START:
                break
            ind = preindex[ind]
        
search_path()