[Python]対戦を通じて語彙力を高めるしりとりAIを作ってみた

# はじめに
Qiita記事初投稿です。

Pythonを用いてしりとりAIを作ってみました。
ソースコードを見たい方はこちらから
https://github.com/takumi13/SiritoriAI

概要は以下の通りです。

- AIと対話型のしりとりをCUIで遊ぶことができる
- AIは、単語辞書に存在しない単語 (以下、未知単語) を適宜学習し、語彙力を増やしていく
- Twitterから適当なツイートを取得し、そのツイートから同様に未知単語を学習することもできる
- スクレイピングをする関係上、**インターネット環境**と**TwitterAPI**が必要

これを昨年の大学のオープンキャンパスにて、研究室の出し物として来場客にデモプレイしてもらったところ、思ったよりもウケが良かったので、(結構な時間が経ちましたが) Qiitaの記事にしてみることにしました。

この記事では、しりとりAIの遊び方やコードの概要を説明します。

※AIというネーミングをしていますが、機械学習等は一切使っていません。ご容赦ください。

#目次
- 環境
- 事前準備
 - Python3
 - TwitterAPI & tweepy
 - janome
 - その他モジュール
- しりとりAIの動作
- AIの単語辞書の構成
- tweepyとjanomeを用いてツイートから未知単語を学習
- まとめ
- 参考URL

#1 環境
```
> ver
Microsoft Windows [Version 10.0.18362.900]

> python -V
Python 3.7.5

> pip -V
pip 20.1.1
```

#2 事前準備
プログラムの実行には、以下が必要です。

- Python3
- TwitterAPI & tweepy
- janome
- その他モジュール

##2.1 Python3
Pythonは以下のリンクよりインストールできます。
https://www.python.org/downloads/

インストールの手順は、例えば以下のリンクを参考にしてください。
[Pythonのインストール方法（Windows）](https://gammasoft.jp/python/python-install-on-windows/)

##2.2 TwitterAPI & tweepy
**TwitterAPI**を利用するためには、Twitter Developperのページ (https://developer.twitter.com/en) からTwitterAPIの利用申請をする必要があります。

申請には数日かかる場合が多く、私の場合、2019年4月に取得し、手続きに4~5日間かかりました。

取得の手順等は以下のリンクを参考にしてください。
[Twitter API 登録 (アカウント申請方法) から承認されるまでの手順まとめ　※2019年8月時点の情報](https://qiita.com/kngsym2018/items/2524d21455aac111cdee)


上記が完了した後、PythonでTwitterAPIを呼び出すための便利なライブラリである[**tweepy**](https://kurozumi.github.io/tweepy/)をインストールしてください。`tweepy`は`pip`コマンドにて簡単にインストールできます。

```
> pip install tweepy
> pip show tweepy
Name: tweepy
Version: 3.8.0
```

また、TwitterAPIの利用申請が通ると、
自身のTwitterアカウントをAPIから操作するためのアクセストークン情報が得られます。
得られるトークンは以下の4つです。

- API key
- Consumer_secret
- Access_token
- Access_secret

これらを`config.py`の該当する箇所に文字列としてコピペしてください。
ここまでの作業が完了すれば、無事PythonからTwitterAPIを呼び出し、
Twitterを操作することができるようになります。

```python:config.py
CONSUMER_KEY        = "##############################"
CONSUMER_SECRET     = "##############################"
ACCESS_TOKEN        = "##############################"
ACCESS_TOKEN_SECRET = "##############################"
```

ところで、TwitterAPIを取得するためには、Twitter社と1~2回Eメールのやりとり (**APIの使用目的を英語200字以上で説明するなど**) を行う必要があり、正直少し面倒です。私のプログラムでは、AIとのしりとり自体はTwitterAPIが無くても動作しますので、申請は必須事項ではありません。ただし、取得しない場合は、プログラム中の`tweepy`に関わる部分をコメントアウトする必要があります。

##2.3 janome
[**janome**](https://mocobeta.github.io/janome/)とは、数ある形態素解析器のうちの1つであり、Pythonのライブラリです。
こちらも`tweepy`同様、`pip`コマンドにてインストールできます。

```
> pip install janome
> pip show janome
Name: Janome
Version: 0.3.10
```

`janome`の使い方は後述します。

##2.4 その他モジュール
主にHTTPリクエストとスクレイピングをするために、以下のモジュールをインストールする必要があります。

- requests
- beautifulsoup4
- lxml

まずは[**requests**](https://requests-docs-ja.readthedocs.io/en/latest/user/quickstart/)について
[Requests の使い方 (Python Library)](https://qiita.com/sqrtxx/items/49beaa3795925e7de666) によると、
> Requests とは Python の 今風な HTTP ライブラリである.
> `requests.get('URL')` で GET リクエストができる.
> レスポンスに対して `.text` とすることで, レスポンスボディをテキスト形式で取得できる.

次に、[**beautifulsoup4**](http://kondou.com/BS4/)について
> Beautiful Soup はHTMLやXMLファイルからデータを取得するPythonのライブラリです。あなたの好きなパーサー(構文解析器)を使って、パースツリー(構文木)の探索、検索、修正を行います。 これはプログラマーの作業時間を大幅に短縮してくれます。

また、`beautifulsoup4`の内部で[**lxml**](https://lxml.de/api/)を使用するため、こちらも別途インストールが必要です。

以上の3つの簡単な使い方を記します。
まずは`pip`コマンドでそれぞれをインストールします。

```
> pip install requests
> pip install beautifulsoup4
> pip install lxml

> pip show requests
Name: requests
Version: 2.23.0

> pip show beautifulsoup4
Name: beautifulsoup4
Version: 4.9.1

> pip show lxml
Name: lxml
Version: 4.3.3
```

実際にコードを書いてみます。
以下のコードは、リクエストを通じてHTMLをテキストとして取得し、そこから必要な情報をスクレイピングします。
例として、コトバンクの「単語」の解説ページ 
(https://kotobank.jp/word/%E5%8D%98%E8%AA%9E)
から、`単語(たんご)`という情報をスクレイピングしてみます。

```python:sample_scraping.py
import requests
from bs4 import BeautifulSoup

url = 'https://kotobank.jp/word/%E5%8D%98%E8%AA%9E'
html = requests.get(url).text              # リクエストモジュールによってHTMLをテキストとして取得する
soup = BeautifulSoup(html, 'html.parser')  # BeautifulSoupの初期化
real_page_tag = soup.find("title")         # titleタグの部分を見つけて格納する
title_read_tmp  = real_page_tag.string     # 文字列にして<title>, </title>を削除する
title_read = title_read_tmp[:-10]          # 無駄な部分を取り除く(正規化)
print(title_read)

'''
> python sample_scraping.py
単語(たんご)
'''
```

さりげなく本題に足を踏み入れていますが、プログラム中でAIは、人間の入力したワードから、そのワードのコトバンクのページのURLを生成し、以上のような操作を経てワードが実在するものか否かを判定します。また、その時同時にワードの読み仮名の先頭と末尾も記憶してしまいます。

さらに、コトバンクは
`https://kotobank.jp/word/[ワードをutf-8変換した文字列]`
というフォーマットでワードの解説ページURLを管理しています。

それゆえ、utf-8への変換プロセスは以下のようにすべてコード化できます。

```python:sample_make_url.py
import requests
from bs4 import BeautifulSoup
import binascii

word = '単語'
url_top = 'https://kotobank.jp/word/'
word_byte = word.encode('utf-8')
hex_string = str(binascii.hexlify(word_byte), 'utf-8')  # byte列を文字列に変換する
hex_string = hex_string.upper()         # 大文字に変換する

words = []                              # byte列を2文字ずつ格納する
for i in range(len(hex_string)//2):     # byte列の文字数/2回繰り返す
    words.append(hex_string[i*2:i*2+2]) # 先頭から2文字ずつ切り取ってwords[i]に格納する

url_latter = ""                         # URLの後半部分. 人間が入力した文字列をutf-8に変換したもの

for i in range(len(words)):
    words[i] = '%' + words[i]           # 2文字ごとに%をつける(仕様)
    url_latter = url_latter + words[i]  # 末尾に連結

url = url_top + url_latter              # 完成したURL
print("URL : " + url)

'''
> python sample_make_url.py
URL : https://kotobank.jp/word/%E5%8D%98%E8%AA%9E
'''
```

#3 しりとりAIの動作
いよいよ本題です。
まずはデモプレイの様子から見ていきましょう。

```
>python main.py
[メニュー]
AIとしりとりがしたい　　　　　　　 ：1
AIとしりとりがしたい(debug mode)  ：2
ツイートから未知単語を学習したい　 ：3
Twitterからツイートを取得したい　 ：4
選択：1

しりとりを終了したいときは, Enterキーを3回連続で押してください(ゲーム中もこの操作は有効です).
あなたの名前を教えてください：人間

人間さんですね. よろしくお願いします.
しりとりはあなたからです. 好きな単語から始めてください.
----------------------------------------------------------------------------------------------
人間 : しりとり
AI : 竜宮城(りゅうぐうじょう)

人間 : うなぎ
AI : ギルバート

人間 : 都会
AI : イシュー

人間 : ゆきぐに
ごめんさない. その単語はこのしりとりには使えません.
もし漢字に変換できる単語であれば, お手数ですが, 漢字に変換して再度入力してください

人間 : 雪国
AI : 忍耐(にんたい)

人間 : 語彙
しりとりになってません
「い」で始まる言葉を入力してください

人間 : 院
「ん」で終わりました
4 回続いたよ
私の勝ちです
----------------------------------------------------------------------------------------------
```

前節で述べた通り、AIは単語の実在判定をコトバンクへのリクエストを通じて行います。したがって、たとえば`しっぱい`などの一般的な表記が漢字のワードはURL作成に失敗します。
また、しりとり終了時にはラリーの回数を表示します。
最後に、一連のしりとりの中で、未知単語があれば新たな語彙としてAIの辞書に追加します。
この時点では、AIは単語を5169個知っている状態です。

一連の流れを図示すると、以下のようになります。
![Qiita_siritoriAI.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/366470/320b48a6-251a-1593-231f-e3681bd0cfd6.png)



説明の単純化のために人間の勝利判定を図から省いていますが、プログラムの中では実装されています。

#4 AIの単語辞書の構成
AIは、人間の入力単語に対する存在フラグと読み方をコトバンクへのリクエストを通じて取得していることを前節で説明しました。一方で、AIは自身が知っている単語 (以下、既知単語) のリストを`dictionary.txt`として保持しています。
辞書ファイルのフォーマットは以下の通りです。

```dictionary.txt
あ:あさひ,あみだくじ,相合傘,アカウント,遊び,阿佐,案内,アニメ,アイコ,アド,圧倒的,青山,朝霧,荒木田,相性,アカ,...
い:入間,以降,以上,一緒,以外,以内,飯塚,井上,一時,和泉,祈り,いや,移住,伊野,イベント,以下,印象,飲料,色紙,今更,...
...
ぺ:ペア,ペット,ペースト,ペイ,ペンチ,ペリー,ペンネーム,ページ,ペンシル,ペーパー
ぽ:ポテトチップス,ポスター,ポケット,ポシェット,ポルノ,ポリス,ポイント,ポーズ
```

単語の存在判定や読み方の理解をすべてコトバンクへ委託しているため、単語辞書はこのような単純構造をもつことができます。プログラム中ではこの`dictionary.txt`を辞書型として読み込むことで、
`d['あ'][0] (=あさひ)`
のように単語へアクセスできます。
実際には、辞書型のあるキーに対する引数はランダム決定しています。

#5 tweepyとjanomeを用いてツイートから未知単語を学習
AIに未知単語を学習させるために毎回しりとりに付き合うのはかなり面倒くさいです。
そこで、`tweepy`を用いて適当なツイートをいくつか取得し、そこから単語のみを抽出し、さらにそこから未知単語を学習してみます。

```
> python .\main.py
[メニュー]
AIとしりとりがしたい　　　　　　 　：1
AIとしりとりがしたい(debug mode)  ：2
ツイートから未知単語を学習したい　 ：3                                                                                       
Twitterからツイートを取得したい　 ：4
選択：3
取得したいツイート数(最大10個)：2
----------------------------------------------------------------------------------------------取得したツイートの表示部
プライバシー保護のため、取得したツイートは非表示
----------------------------------------------------------------------------------------------[ 0 ]みな(み)
[ 1 ]団扇(う)
[ NG ]ダメ
[ 2 ]著作(ち)
元の辞書の単語数　　： 5179
取得した単語の数　　： 3
実際に増えた単語数　： 2
更新後の辞書の単語数： 5181
単語の学習率　　　　： 66.66666666666667 %
----------------------------------------------------------------------------------------------
```
このように、`tweepy`を用いて取得したツイートから`janome`を用いて名詞のみを抽出し、
その中から未知単語のみを学習しています。
ここで、該当単語はすべて、コトバンクへのリクエストを通じて読み方を取得しています。
短時間で大量のリクエストを1つのWebサイトへ行うと、Webサイトには高負荷がかかります。
念のため、プログラム中では**ツイートの最大取得件数は10件まで**に制限しています。

`tweepy`の使用例を以下のコードに示します。

```python:sample_tweepy.py
import tweepy
import config
from janome.tokenizer import Tokenizer

##################################################333
CK = config.CONSUMER_KEY
CS = config.CONSUMER_SECRET
AT = config.ACCESS_TOKEN
ATK = config.ACCESS_TOKEN_SECRET
##################################################333

#--------------------------------------------------
# TwitterAPIを取得
#--------------------------------------------------
def get_twieetr_api(CK, CS, AT, ATK):
    try:                                    #例外処理
        auth = tweepy.OAuthHandler(CK, CS)  #authにCONSUMER_KEYとCONSUMER_SECRETを渡す
        auth.set_access_token(AT, ATK)      #authにアクセストークンをセットする
        API = tweepy.API(auth)              #記述を楽にする
    except tweepy.TweepError as e:          #エラーが発生した場合TweepErrorが返ってくる
        print(e.reason)                     #エラー内容を出力
    return API

###########################################################################
api = get_twieetr_api(CK, CS, AT, ATK)
############################################################################

#-----------------------------------------------------
# TwitterAPIを用いてツイートをテキストとしてcount数取得
#-----------------------------------------------------
def get_text(q, count=100):
    text_list = []
    search_results = api.search(q=q, count=count)
    for tweet in search_results:
        text = tweet.text.encode('cp932', "ignore")
        text = text.decode('cp932')
        text = text.encode('utf-8', "ignore")
        text_list.append(text.decode('utf-8'))
    return text_list

text_list = get_text(q='楽しい', count=3)
for text in text_list:
    print('-------------------------------------------------')
    print(text)
```
以上のコードでツイートをテキストとして取得できます。
また、`janome`の使用例を以下のコードにて示します。

```python:sample_janome.py
from janome.tokenizer import Tokenizer
t = Tokenizer()
# 以下のテキストは架空のツイート
text_list = ['ありがとう！これからも大事に使わせていただきます！', 'だめだ、これ以上は何もできない気がする...むー']
for text in text_list:
    for token in t.tokenize(text):
        if token.part_of_speech.split(',')[0] == '名詞' and len(token.surface) >= 2 and token.reading != '*':
            print(token.surface)

'''
> python sample_janome.py
大事
だめ
これ
以上
'''
```

このように、読みの長さが2以上かつ名詞の単語をテキストから取得することができます。
上記の単語`token.surface`を別途リストに一時記憶し、その後、`dictionary.txt`を読み込んだ辞書型変数の中身と比較しつつ、辞書に存在しない単語のみを新たに記憶することで、ツイートから未知単語を学習できます。

#6 まとめ
以上です。
実際にしりとりを行うシステム部分や、未知単語の特定等の処理をここに記述すると長くなってしまうため
今回は割愛します。
改めて、興味がある人は[GitHub](https://github.com/takumi13/SiritoriAI)にてソースコードを確認してください。

機会があればまた記事として投稿するかもしれません。
特に、新機能等を実装できたら、別途記事を書こうと思います。

ここまで読んでくださりありがとうございました。

#7 参考URL
プログラム作成に当たり、参考にした主要なWebサイトのURLを添付します。
ありがとうございます。

[日本語文字列操作　ひらがな判定など Remrinのpython攻略日記](http://python-remrin.hatenadiary.jp/entry/2017/04/26/123458)
[[保存版] Pythonでスクレイピングする方法を初心者向けに徹底解説！](https://dividable.net/python/python-scraping/)
[Beautiful Soup での スクレイピング基礎まとめ [初学者向け]](https://qiita.com/U-MA/items/896c49d46585e32ff7b1)
[[Python] byte配列を文字列に変換する](https://qiita.com/k28@github/items/87c474a8658fd9a2470a)
[WindowsでCP932(Shift-JIS)エンコード以外のファイルを開くのに苦労した話](https://qiita.com/Yuu94/items/9ffdfcb2c26d6b33792e)