【自然言語処理】Pythonを使って簡単なチャットボット を作ってみた。

## はじめに

会社でチャットボットを導入するということでチャットボットってこんなものかと勉強がてらルールベースチャットボットを作ってみたのでこの記事はそのアウトプットです。

ひとまず今回はアプリとしてではなくnote book形式で動かすことを目標にやっています。(最終的にはLINE Botにしたい...)

参考記事

* [【チュートリアル】機械学習を使って30分で固有表現抽出器を作る](https://qiita.com/Hironsan/items/326b66711eb4196aa9d4)

### 全体の流れ

今回の記事の流れとしてはこんな感じです。

ラベリングするためのモデルを作る
　　　　　　　↓
実際にラベリングしてみる
　　　　　　　↓
ラベリングしたデータをPythonでチャットボットっぽくしてみる

## 動作環境

* Python3.6
* MacOS

## 使ったモジュール

モジュールについての説明は省きます。

```python
from itertools import chain
import pycrfsuite
import sklearn
from sklearn.metrics import classification_report
from sklearn.preprocessing import LabelBinarizer
import codecs
from janome.tokenizer import Tokenizer
```



## 固有表現抽出をする

取り敢えず文章から単語を取得してラベリングする必要があるので機械学習でモデルを作っていきます。

まずは機械学習するためのデータセットです。

今回はチャットボット作成の参考にさせていただいた[@Hironsan](https://qiita.com/Hironsan)さんのデータセットを使わせていただきました。

[こちら](https://github.com/Hironsan/IOB2Corpus)からダウンロードできます。


[@Hironsan](https://qiita.com/Hironsan)様ありがとうございます、深く感謝しております。

ただこのデータセットだけだと面白みがないので学習後のモデルを使ってスクレイピングした文章をラベリングしてそれをデータセットに加えて再学習というのを何度か繰り返してみました。

それについては気が向いたらまとめてみようと思うので今回は飛ばして先に進みます。
(学習後のモデルはGitHubに置いておきます。)


ダウンロードしたデータセットを読み込んでトレーニングデータを作ります。

本来はテストデータとトレーニングデータを分けて検証などしますが、今回は取り敢えず動かすというのとデータセットもそこまで多くないので全てトレーニング用にブチ込みます。

```python
class CorpusReader(object):

    def __init__(self, path):
        with codecs.open(path, encoding='utf-8') as f:
            sent = []
            sents = []
            for line in f:
                if line == '\n':
                    sents.append(sent)
                    sent = []
                    continue
                morph_info = line.strip().split('\t')
                sent.append(morph_info)
            train_num = int(len(sents))
            self.train_sents = sents[:train_num]

```

早速データセットをを見込んでみます。

```python
c = CorpusReader('corpus.txt') # データセットの名前変えてます。
train_sents = c.train_sents
print(c.train_sents[0][0])
=>['2005', '名詞', '数', '*', '*', '*', '*', '*', 'B-DAT']
```

データセットから取り出した内容はこんな感じ。

今回は形態素解析された文章を突っ込まれるとそれをラベリングするというものを作ります。

ではこのデータセットを学習させるための準備をしていきます。

## 文字種の判定

```python
def is_hiragana(ch):
    return 0x3040  <= ord(ch) <= 0x309F

def is_katakana(ch):
    return 0x30A0 <= ord(ch) <= 0x30FF

def get_character_type(ch):
    if ch.isspace():
        return 'ZSPACE'
    elif ch.isdigit():
        return 'ZDIGIT'
    elif ch.islower():
        return 'ZLLET'
    elif ch.isupper():
        return 'ZULET'
    elif is_hiragana(ch):
        return 'HIRAG'
    elif is_katakana(ch):
        return 'KATAK'
    else:
        return 'OTHER'
    
def get_character_types(string):
    character_types = map(get_character_type, string)
    character_type_str = '-'.join(sorted(set(character_types)))
    return character_type_str
```

## 学習前の準備

素性抽出やラベル抽出をするための処理を書きます。

```python
def extract_pos_with_subtype(morph):
    idx = morph.index('*')

    return '-'.join(morph[1:idx])

def word2features(sent, i):
    word = sent[i][0]
    chtype = get_character_types(sent[i][0])
    postag = extract_pos_with_subtype(sent[i])
    features = [
        'bias',
        'word=' + word,
        'type=' + chtype,
        'postag=' + postag,
    ]
    if i >= 2:
        word2 = sent[i-2][0]
        chtype2 = get_character_types(sent[i-2][0])
        postag2 = extract_pos_with_subtype(sent[i-2])
        iobtag2 = sent[i-2][-1]
        features.extend([
            '-2:word=' + word2,
            '-2:type=' + chtype2,
            '-2:postag=' + postag2,
            '-2:iobtag=' + iobtag2,
        ])
    else:
        features.append('BOS')

    if i >= 1:
        word1 = sent[i-1][0]
        chtype1 = get_character_types(sent[i-1][0])
        postag1 = extract_pos_with_subtype(sent[i-1])
        iobtag1 = sent[i-1][-1]
        features.extend([
            '-1:word=' + word1,
            '-1:type=' + chtype1,
            '-1:postag=' + postag1,
            '-1:iobtag=' + iobtag1,
        ])
    else:
        features.append('BOS')

    if i < len(sent)-1:
        word1 = sent[i+1][0]
        chtype1 = get_character_types(sent[i+1][0])
        postag1 = extract_pos_with_subtype(sent[i+1])
        features.extend([
            '+1:word=' + word1,
            '+1:type=' + chtype1,
            '+1:postag=' + postag1,
        ])
    else:
        features.append('EOS')

    if i < len(sent)-2:
        word2 = sent[i+2][0]
        chtype2 = get_character_types(sent[i+2][0])
        postag2 = extract_pos_with_subtype(sent[i+2])
        features.extend([
            '+2:word=' + word2,
            '+2:type=' + chtype2,
            '+2:postag=' + postag2,
        ])
    else:
        features.append('EOS')
    return features


def sent2features(sent):
    return [word2features(sent, i) for i in range(len(sent))]

def sent2labels(sent):
    return [morph[-1] for morph in sent]

def sent2tokens(sent):
    return [morph[0] for morph in sent]
```

これで素性とラベルを抽出できるようになったのでトレーニング用に素性とラベルに分けます。

```python
X_train = [sent2features(s) for s in train_sents]
y_train = [sent2labels(s) for s in train_sents]
```

分けたデータを入れてパラメーターをセットして学習させます。

```python
trainer = pycrfsuite.Trainer(verbose=False)
for xseq, yseq in zip(X_train, y_train):
    trainer.append(xseq, yseq)

trainer.set_params({
    'c1': 1.0,   # coefficient for L1 penalty
    'c2': 1e-3,  # coefficient for L2 penalty
    'max_iterations': 50,  # stop earlier

    # include transitions that are possible, but not observed
    'feature.possible_transitions': True
})

trainer.train('model.crfsuite')
```

これでカレントディレクトリに学習済のモデルが生成されるのでそれを使ってラベリングしてみようと思います。

取り敢えず今回使ったモデルを使ってラベリングする為にjanomeを使って渡した文章を形態素解析してリストにしてくれる関数を準備

```python
def dump_morph(word):    
    t = Tokenizer()
    name_type_list = []
    for i in t.tokenize(word):
        name_type = str(i).replace('\t', ',')
        name_type_list.append(name_type.split(','))
    return name_type_list
```

これを使うとこんな感じの出力が得られます。

```python
words=dump_morph('私の名前は田中です。')
for word in words:
    print(word)
```

```
['私', '名詞', '代名詞', '一般', '*', '*', '*', '私', 'ワタシ', 'ワタシ']
['の', '助詞', '連体化', '*', '*', '*', '*', 'の', 'ノ', 'ノ']
['名前', '名詞', '一般', '*', '*', '*', '*', '名前', 'ナマエ', 'ナマエ']
['は', '助詞', '係助詞', '*', '*', '*', '*', 'は', 'ハ', 'ワ']
['田中', '名詞', '固有名詞', '人名', '姓', '*', '*', '田中', 'タナカ', 'タナカ']
['です', '助動詞', '*', '*', '*', '特殊・デス', '基本形', 'です', 'デス', 'デス']
['。', '記号', '句点', '*', '*', '*', '*', '。', '。', '。']
```

ではこれを先ほど学習させたモデルに突っ込んで元の文章と並べてみましょう。

```python
t = Tokenizer()
words=dump_morph('私の名前は田中です。')
print(t.tokenize('私の名前は田中です。', wakati=True))
print(tagger.tag(sent2features(words)))
```

```
['私', 'の', '名前', 'は', '田中', 'です', '。']
['O', 'O', 'O', 'O', 'B-PSN', 'O', 'O']
```

人名であろう田中にB-PSNとラベルが振られています。

これで系列ラベリングする為のモデルは完成です。

## 特定のラベルでラベリングされた単語を取得する

ではここまで作ってきたプログラムを使ってチャットボットっぽいことができるプログラムを作ってみます。


```python

def classifying(type_list, kind):
    tag = tagger.tag(sent2features(type_list))
    
    for i in range(len(tag)):
        type_list[i].append(tag[i])
    for l in type_list:
        if l[-1] == kind:
            return l[0]

def free_word_bot(text_set):
    target_list = []
    for i in text_set:
        text = i[0]
        label = i[1]
        print(text)
        ans = input('あなた：　')
        target = classifying(dump_morph(ans), label)
        target_list.append(target)
    print(target_list)
```

質問とその質問が答えとして欲しいラベルを２次元配列で渡してあげるとその質問を投げてユーザーから入力された内容をレベリングしてそのラベルの中から予め渡しているラベルと一致する単語をリストとして保持して最終的にそのリストの内容をprintしてくれる関数を作ってみました。

ではこの関数を使って実際に欲しい単語を抽出してみましょう。

![bot.mov.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/421064/b31a2cd4-28b8-715e-0c03-8015683e550a.gif)

こんな感じで文章から必要としているラベルを見つけてリストに格納することができました。

これを発展させてばウェブサイトとかにあるチャットボットとかも作れるのかな？と思たりしています。

## まとめ

今回はとりあえずチャットボット作ってみようという所から、qiitaに記事を投稿してアウトプットしてみたいということでこの記事を書いてみました。

超ド新米エンジニア（もはやエンジニアとは呼べない）なので色々アレなところもあると思いますので何かあったらコメントを頂けると幸いです。

次回はこれを元に今作っているLINE Botができたらそれも書いてみようと思っているのですがデプロイするのでつまずいているので一度おうむ返しのLINE Botを作ってデプロイしてみることにしたので先にその記事を書いてみようと思います。

それでは！！！

GitHubにはまだpushしていないのであげたら追記します！