難しいことはCOTOHA APIにやらせよう ～『使って学ぶ』自然言語処理入門～

最近、目覚ましい発展を続けている自然言語処理（NLP）の世界。そんな自然言語処理の世界に入ってみたいという方も多いのではないだろうか。しかし一概に自然言語処理といえど、その言葉の中に含まれるタスクの数は膨大。

「結局何から始めればいいのー！？」

そんな声にお応えするのが本記事だ。

# 1 はじめに

本記事は自然言語処理ド素人でも「自然言語処理ね。色んなタスク知ってるよ、しかも使ったことある！」というレベルに引き上げることを目的としている。しかし全てのタスクを1つ1つ解説すると、とても1記事では収まらないだろう。それだけでなく、解説が詳細になるほど難易度が高くなり、結局何も得られなかったという事態にもなりかねない。

## 1.1 本記事のねらい

そこで本記事では<font color="Red">難しいアルゴリズムの話はとりあえず置いておいて、誰でも気軽に自然言語処理を「使える」</font>ことを目指す。これはただ提示するコピペを提示するという意味ではなく、便利なAPIを使うことで実装を簡略化し、その詳細を説明することで難易度を易しくするという意味だ。

難しいことは[COTOHA API](https://api.ce-cotoha.com/contents/index.html)にやらせよう。

COTOHA APIはNTTグループの研究成果をもとに作られた手軽に自然言語処理を利用できるAPIだ[^1]。難しいアルゴリズムの実装はこのAPIを叩くことで簡略化する。

APIというと難しいイメージが湧くかもしれない。しかし本APIは非常に利用が簡単であり、加えて本記事ではより丁寧・詳細に使い方を解説した（つもり）。また、将来的に難しいアルゴリズムを自分で実装してみたいと思ったときに役立つよう、本記事のサンプルコードは全てPythonで実装した。APIに関する事前知識は全く必要としない！（ので安心してほしい）

## 1.2 本記事で使えるようになる自然言語処理の種類

**文章要約、構文解析、固有表現抽出、照応解析、キーワード抽出、2文間の類似度計算、文タイプ推定、年代・職業等の人の属性推定、言い淀み除去（あー、えーと等）、音声認識誤り検知、感情分析**

これらは全てCOTOHA APIの無料ユーザー（for Developpers）で使える自然言語処理である。本記事ではこれらのタスクについて紹介し、実際に動くコードを解説する。（ただしコピペで終わらせるのは本記事の意図に反しているので、全ての自然言語処理が使える実装は提供していない。実際にコピペで動かせるのは要約、構文解析、固有表現抽出の3種類である。）

## 1.3 最終的な成果

例として文章要約の結果を見てみよう。以前に僕（@MonaCat）が書いた[Qiitaの記事](https://qiita.com/MonaCat/items/5123d2f970cba1acd897)を入力テキストとして要約させた。

```terminal
> python cotoha_test.py -f data/input.txt -s 3

＜要約＞

文章要約は抽出型と生成型（抽象型）に分かれますが、現在は生成型（と抽出型を組み合わせたもの）が主流となっています。
ニューラル文章要約モデルの紹介 - エムスリーテックブログseq2seqベースの自動要約手法がまとめられています。
論文解説 Attention Is All You Need (Transformer) - ディープラーニングブログこれをまず読めば間違いありません。
```

簡単に説明しておこう。

`python cotoha_test.py`でプログラムを実行させている。また様々なオプションを引数に渡すことで、どのAPIを呼び出すかを指定できるようにした。上図では`-f data/input.txt -s 3`の部分がオプションである。これは`input.txt`というファイルを参照し、自動要約APIを要約文数3で使用するということを指定している。そのため出力結果は3文の要約になっているはずだ。

このオプションを変えることで他のAPIを使ったり、設定（例えば3文ではなく5文で要約したり）を変更できるようにした。その分、多少難しくなってしまったが、オプションの実装解説については補足とした。本筋では関係ないように配慮しているので余裕のある方だけ読んでいただければ大変うれしい。

# 2 自然言語処理を知ろう

実装する前に、自然言語処理について少しお勉強しておこう。自然言語とは私たちが日常で使う日本語や英語のような言語のことだ。すなわち自然言語処理とは「自然言語」を「処理」する技術分野を指す。

この章ではCOTOHA APIで使える自然言語処理技術について簡単に説明する。（一部割愛）

- 構文解析

構文解析とは文章（文字列）を形態素に分割し、その間にある構造関係を解析することをいう。例えば「私は見た」という文章は「名詞」＋「助詞」＋「動詞」で構成されている。構文解析することでマークアップされていない文字列の構造関係を明確にすることができる。

- 固有表現抽出

固有表現抽出は、固有名詞（人名や地名等）を自動的に抽出する技術だ。世の中には数えられないほどの固有名詞が存在するが、その全てを辞書に登録することは現実的ではない。そこで抽出を自動化することで大量のテキストを処理できるようになる。

- 照応解析

「これ」「彼」などの指示詞や代名詞は、先行詞と同じものとして使用されるが、それを読み取れるのは我々が文章を理解するときに自然とその関係性を把握しているからである。この関係を照応関係といい、これを解析する技術を照応解析という。

- キーワード抽出

キーワードは様々な意味があるが、COTOHA APIでは特徴的なフレーズ・単語を算出し、キーワードとして抽出する。つまり文章における代表的なフレーズ、単語であるとみなすことができる。

- 属性推定

属性（年代、性別、趣味、職業等）を推定する技術だ。COTOHA APIではTwitterユーザーの属性推定を目的としている。

- 言い淀み除去

言い淀み（いいよどみ）とは「あー」「うー」等の話すときに文間に挟んで使用する言葉のことである。例えば音声認識で書き起こされた文章には、テキスト化する上で不要な言い淀みが含まれてしまうことがある。このようなときに言い淀みを除去することでテキストを活用しやすいデータに処理できるようになる。

- 感情分析

感情分析（ネガポジ判定）は文章から書きての感情をネガティブ・ポジティブの2値に分類することをいう。上記で紹介した分野以上に深層学習を用いた解析が盛んな分野であろう。

- 文章要約

文章を1文or複数の文に自動で要約する技術である。最近はニュースサイトでも3文要約が付属しているのが一般的になってきた。長文から重要なフレーズ・文を解析するという点ではキーワード抽出と似ているタスクといえるだろう。

もし文章要約に興味があればこちらの記事がお勧めだ。論文紹介というタイトルをつけてはいるが、基礎から最新に近い技術までの情報をまとめたので参考になるはずだ：[BERTで自動要約を行う論文「BERTSUM」を紹介する＋α - Qiita](https://qiita.com/MonaCat/items/5123d2f970cba1acd897)

# 3 実際に使ってみよう

お待たせした。ではCOTOHA APIを使って自然言語処理を体験してみよう。

## 3.1 共通部分

まずは使いたいAPIに関わらず必要な共通部分から。

COTOHA APIは無料で登録して使えるので[COTOHA API](https://api.ce-cotoha.com/contents/index.html)から登録しよう。

登録情報には氏名や所属が必要だが、クレジットカード等の情報は必要ない。登録すると下図のような情報がもらえるだろう。

![img012.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/167534/5d5a865f-3e77-4454-4195-ad1ad63d0d93.png)

人によって異なるのはこのうち「Client ID」と「Client secret」なので後でコピペできるように準備しておこう。

### 3.1.1 アクセストークンの取得

ここから先はPythonで実装する。Python環境はお任せするが、必要なモジュールは使用できるようにpipなりcondaなりで適宜インストールしていただきたい。

まず、アクセストークンを取得する関数を定義する。

```python
CLIENT_ID = '適宜書き換え'
CLIENT_SECRET = '適宜書き換え'


def auth(client_id, client_secret):
    token_url = 'https://api.ce-cotoha.com/v1/oauth/accesstokens'
    headers = {
        'Content-Type': 'application/json',
        'charset': 'UTF-8'
    }
    data = {
        'grantType': 'client_credentials',
        'clientId': client_id,
        'clientSecret': client_secret
    }
    r = requests.post(token_url,
                      headers=headers,
                      data=json.dumps(data))

    return r.json()['access_token']


if __name__ == "__main__":

    access_token = auth(CLIENT_ID, CLIENT_SECRET)  # アクセストークン取得
```

突然難しく思ったかもしれない。が、至ってシンプルだ。順番に見ていこう。

まずauth()が上述の認証情報を引数として呼び出される。

auth()では認証情報を利用してアクセストークンを取得する。[スタートガイド](https://api.ce-cotoha.com/contents/gettingStarted.html)ではコマンドで取得しているが、今回は同じことをPythonで実装している。成功するとjsonファイルが取得できるが、ここで欲しいのはアクセストークンのみであることに注意しよう。そのため`r.json['access_token']`を返している。

このアクセストークンを利用してAPIを使用できるようになる。

### 3.1.2 引数を指定する（オプション）

今回のサンプルコードでは`argparse`モジュールを使って引数指定できるようにしている。これは1.3節でも説明したが、引数指定できるようにしたのは、複数のAPIを使うことを想定しているので切り替えが便利だからである。

本節では簡単に`argparse`の説明するが、本筋とは関係ないので余裕のない方は飛ばしてほしい。

ではまず先程のアクセストークンを取得する前に

```python
if __name__ == "__main__":

    document = '入力テキストを指定しなかった時に使われるテキスト。本実装では「引数でテキストを指定する」「引数でテキストファイルを指定する」の2パターンが使えます。是非お試しあれ。'
    args = get_args(document)  # 引数取得

    access_token = auth(CLIENT_ID, CLIENT_SECRET)  # アクセストークン取得
```

でget_args()を呼び出そう。この関数の中身は次のようになっている。

```python
def get_args(document):
    argparser = argparse.ArgumentParser(description='Cotoha APIを使って自然言語処理を遊ぶコード')
    argparser.add_argument('-t', '--text', type=str, default=document, help='解析したい文')
    argparser.add_argument('-f', '--file_name', type=str, help='解析したい.txtのpath')
    argparser.add_argument('-p', '--parse', action='store_true', help='構文解析するなら指定')
    argparser.add_argument('-s', '--summarize', type=int, help='自動要約するなら要約文数を指定')

    return argparser.parse_args()
```

1行目はdescriptionでコードの説明を記述している。これは`-h`をコマンドライン引数としたときに表示される。

`-h`を指定したときの例。

```terminal
> python cotoha_test.py -h      
usage: cotoha_test.py [-h] [-t TEXT] [-f FILE_NAME] [-p] [-s SUMMARIZE]

Cotoha APIを使って自然言語処理を遊ぶコード

optional arguments:
  -h, --help            show this help message and exit
  -t TEXT, --text TEXT  解析したいテキスト
  -f FILE_NAME, --file_name FILE_NAME
                        解析したいテキストファイルのpath
  -p, --parse           構文解析
  -n, --ne              固有表現抽出
  -s SUMMARIZE, --summarize SUMMARIZE
                        自動要約における要約文数
```

それ以降の行は`add_argument`で引数を順に定義している。

当然だが、引数を設定したところで、その引数が指定された時に何が起きるか実装しなければ何も起きない。今回は`-p`が指定された時には構文解析を行うようにしたい。これだけならif文で条件分岐させるだけでうまくいきそうだ。早速やってみよう。

```python
if (args.parse):
    parse(doc, access_token)  # 関数呼び出し
```

もし`args.parse`がTrueであれば関数parseを呼び出す。関数parseはここまでで登場していないため、中身がわからないだろう。ここでは単に構文解析を実行する関数だと思ってほしい。詳しくは後の章で解説する。

`args.parse`こそが先程`argparser.add_argument`で追加したオプションだ。`args.p`ではなく`args.parse`でなければならないことに注意。同様に例えば要約であれば`args.summarize`でいい。

ところで今後、構文解析や要約の他に多くのタスクをif文で条件分岐すると少し見栄えが悪い。そこで少し簡略化しておく。

```python
# API呼び出し
l = [doc, access_token]  # 共通の引数
parse(*l) if (args.parse) else None  # 構文解析
```

やっていることはさっきと何も変わらないが、if文を1行で書き、共通の引数が多いことを考えlistにまとめ、それを展開して関数に渡して呼び出している。

最後に`-f`でファイルを指定した時の実装をしよう。ファイルを指定した時にはそちらを優先するが、その前に指定したファイルが存在するかどうかを調べなければエラーが発生する可能性がある。

```python
# ファイルを指定した場合はそちらを優先
if (args.file_name != None and os.path.exists(args.file_name)):
    with open(args.file_name, 'r', encoding='utf-8') as f:
        doc = f.read()
else:
    doc = args.text
```

ここまで追ってこれていれば次のような実装になっているはずだ。

```python
import argparse
import requests
import json
import os


BASE_URL = 'https://api.ce-cotoha.com/api/dev/'
CLIENT_ID = ''  # 適宜書き換え
CLIENT_SECRET = ''  # 適宜書き換え


def get_args(document):
    argparser = argparse.ArgumentParser(description='Cotoha APIを使って自然言語処理を遊ぶコード')
    argparser.add_argument('-t', '--text', type=str, default=document, help='解析したいテキスト')
    argparser.add_argument('-f', '--file_name', type=str, help='解析したいテキストファイルのpath')
    argparser.add_argument('-p', '--parse', action='store_true', help='構文解析')
    argparser.add_argument('-n', '--ne', action='store_true', help='固有表現抽出')
    argparser.add_argument('-c', '--coreference', action='store_true', help='照応解析')
    argparser.add_argument('-k', '--keyword', action='store_true', help='キーワード抽出')
    argparser.add_argument('-s', '--summarize', type=int, help='自動要約における要約文数')

    return argparser.parse_args()


def auth(client_id, client_secret):
    """
        アクセストークンを取得する関数
    """
    token_url = 'https://api.ce-cotoha.com/v1/oauth/accesstokens'
    headers = {
        'Content-Type': 'application/json',
        'charset': 'UTF-8'
    }
    data = {
        'grantType': 'client_credentials',
        'clientId': client_id,
        'clientSecret': client_secret
    }
    r = requests.post(token_url,
                      headers=headers,
                      data=json.dumps(data))

    return r.json()['access_token']


def base_api(data, document, api_url, access_token):
    """
        全てのAPIで共通となるheader等
    """
    base_url = BASE_URL
    headers = {
        'Content-Type': 'application/json; charset=UTF-8',
        'Authorization': 'Bearer {}'.format(access_token)
    }
    r = requests.post(base_url + api_url,
                      headers=headers,
                      data=json.dumps(data))

    return r.json()


if __name__ == "__main__":

    doc = '昨日、マイケルと東京駅で待ち合わせした。彼とはひと月前から付き合い始めた。'
    args = get_args(doc)  # 引数取得

    # ファイルを指定した場合はそちらを優先
    if (args.file_name != None and os.path.exists(args.file_name)):
        with open(args.file_name, 'r', encoding='utf-8') as f:
            doc = f.read()
    else:
        doc = args.text

    access_token = auth(CLIENT_ID, CLIENT_SECRET)  # アクセストークン取得
    
    # API呼び出し
    l = [doc, access_token]  # 共通の引数

    parse(*l) if (args.parse) else None  # 構文解析
    ne(*l) if (args.ne) else None  # 固有表現抽出
    coreference(*l) if (args.coreference) else None  # 照応解析
    keyword(*l) if (args.keyword) else None  # キーワード抽出
    summarize(*l, args.summarize) if (args.summarize) else None  # 要約
```

当然まだこのプログラムは動かない。parse()やne()などの関数を定義していないからだ。それは次の節以降で実装する。


## 3.2 タスクごとに異なる部分

3.1.2を飛ばした方のためにここでは次のプログラムのように、構文解析のみを実行するプログラムを使用する。

```python
import argparse
import requests
import json
import os


BASE_URL = 'https://api.ce-cotoha.com/api/dev/'
CLIENT_ID = '適宜書き換え'
CLIENT_SECRET = '適宜書き換え'

def auth(client_id, client_secret):
    """
        アクセストークンを取得する関数
    """
    token_url = 'https://api.ce-cotoha.com/v1/oauth/accesstokens'
    headers = {
        'Content-Type': 'application/json; charset: UTF-8',
    }
    data = {
        'grantType': 'client_credentials',
        'clientId': client_id,
        'clientSecret': client_secret
    }
    r = requests.post(token_url,
                      headers=headers,
                      data=json.dumps(data))

    return r.json()['access_token']


if __name__ == "__main__":

    doc = '昨日、マイケルと東京駅で待ち合わせした。彼とはひと月前から付き合い始めた。'

    access_token = auth(CLIENT_ID, CLIENT_SECRET)  # アクセストークン取得
    parse(doc, access_token)  # 構文解析
```

3.1.2を追ってこれた人はそっちの実装をそのまま使ってもらえればいい。どちらにしてもここで実装するのはparse(), ne()などのAPIを呼び出して自然言語処理を行う関数だ。

### 3.2.1 構文解析

[構文解析のリファレンス](https://api.ce-cotoha.com/contents/reference/apireference.html#entity)を読むとリクエストヘッダ、リクエストボディなどがあり、何やらキーを指定する必要があることがわかるだろう。しかし実はリクエストヘッダは全てのCOTOHA APIで共通している。そこでリクエストヘッダなどの共通部分はbase_api()という関数で実装し、構文解析ならではの部分はparse()という関数で実装する。

```python
def base_api(data, document, api_url, access_token):
    """
        全てのAPIで共通となるheader等
    """
    base_url = BASE_URL
    headers = {
        'Content-Type': 'application/json; charset=UTF-8',
        'Authorization': 'Bearer {}'.format(access_token)
    }
    r = requests.post(base_url + api_url,
                      headers=headers,
                      data=json.dumps(data))

    return r.json()


def parse(sentence, access_token):
    """
        構文解析
    """
    data = {'sentence': sentence}
    result = base_api(data, sentence, 'nlp/v1/parse', access_token)

    print('\n<構文解析>\n')
    result_list = list()
    
    for chunks in result['result']:
        for token in chunks['tokens']:
            result_list.append(token['form'])

    print(' '.join(result_list))
```

base_api()から見ていこう。headersで指定しているのが先程見たばかりのリクエストヘッダだ。この値とリクエストボディ、解析したい文章、APIのURLを引数にしてリクエストを送っていることがわかる。最後に得られたjsonファイルを返している。

parse()では最初に変数dataでリクエストボディを指定している。今回はキーsentenceを変数sentenceで指定している（何言ってるんだと思うかもしれないが、そのままの意味だ）。

続いてresult変数の中にbase_api()の値、つまりjsonファイルがはいっている。

このjsonファイルには構文解析の実行結果がはいっているので、これを読み取る。どんな形式で書かれているかは同じリファレンスのレスポンスサンプルに書かれている。構文解析のレスポンスサンプルを読むと、どうやら形態素情報オブジェクトのキーformが求めていたもののようなので、これをリストに追加していく。

これを実行すると次のような結果が得られるだろう。

```terminal
> python cotoha_test.py

<構文解析>

昨日 、 マイケル と 東京駅 で 待ち 合わせ し た 。 彼 と は ひと月 前 から 付き合 い 始め た 。
```

### 3.2.2 固有表現抽出

続いて固有表現抽出だが、先程と同じ説明を繰り返すだけになるので割愛する。実はさっきと同じように他のほとんどのAPIも使えてしまう。

```python
def ne(sentence, access_token):
    """
        固有表現抽出
    """
    data = {'sentence': sentence}
    result = base_api(data, sentence, 'nlp/v1/ne', access_token)
    
    print('\n<固有表現抽出>\n')
    result_list = list()

    for chunks in result['result']:
        result_list.append(chunks['form'])

    print(', '.join(result_list))
```

```terminal
> python cotoha_test.py

<固有表現抽出>

昨日, マイケル, 東京駅, ひと月
```

### 3.2.3 要約

せっかくなので少し違うことをしてみよう。要約のリファレンスを読むとリクエストボディに要約文数が指定できる`sent_len`があるだろう。これもせっかくなので使ってみたい。

```python
def summarize(document, access_token, sent_len):
    """
        要約
    """
    data = {
        'document': document,
        'sent_len': sent_len
    }
    result = base_api(data, document, 'nlp/beta/summary', access_token)

    print('\n＜要約＞\n')
    result_list = list()

    for result in result['result']:
        result_list.append(result)
    
    print(''.join(result_list))
```

これで`sent_len`に具体的な値を入れれば好きな要約文数で要約できるようになった。もし3.1.2のオプションを実装できているならば引数で`sent_len`を指定できるので便利だろう。

```terminal
> python cotoha_test.py -s 1

＜要約＞

昨日、マイケルと東京駅で待ち合わせした。
```

### 3.3.3 その他の自然言語処理

他のAPIについても同様に実装できるので割愛する。

# 4 まとめ

最後に構文解析、固有表現抽出、要約の実行ができるプログラムを提示してまとめとする。繰り返しになるが、他のAPIについても同様に実装できるので、自然言語処理に興味を持っていただけた方はぜひ自分の力で実装してみてほしい。

```python
import argparse
import requests
import json
import os


BASE_URL = 'https://api.ce-cotoha.com/api/dev/'
CLIENT_ID = ''  # 適宜書き換え
CLIENT_SECRET = ''  # 適宜書き換え


def get_args(document):
    argparser = argparse.ArgumentParser(description='Cotoha APIを使って自然言語処理を遊ぶコード')
    argparser.add_argument('-t', '--text', type=str, default=document, help='解析したいテキスト')
    argparser.add_argument('-f', '--file_name', type=str, help='解析したいテキストファイルのpath')
    argparser.add_argument('-p', '--parse', action='store_true', help='構文解析')
    argparser.add_argument('-n', '--ne', action='store_true', help='固有表現抽出')
    argparser.add_argument('-s', '--summarize', type=int, help='自動要約における要約文数')

    return argparser.parse_args()


def auth(client_id, client_secret):
    """
        アクセストークンを取得する関数
    """
    token_url = 'https://api.ce-cotoha.com/v1/oauth/accesstokens'
    headers = {
        'Content-Type': 'application/json',
        'charset': 'UTF-8'
    }
    data = {
        'grantType': 'client_credentials',
        'clientId': client_id,
        'clientSecret': client_secret
    }
    r = requests.post(token_url,
                      headers=headers,
                      data=json.dumps(data))

    return r.json()['access_token']


def base_api(data, document, api_url, access_token):
    """
        全てのAPIで共通となるheader等
    """
    base_url = BASE_URL
    headers = {
        'Content-Type': 'application/json; charset=UTF-8',
        'Authorization': 'Bearer {}'.format(access_token)
    }
    r = requests.post(base_url + api_url,
                      headers=headers,
                      data=json.dumps(data))

    return r.json()


def parse(sentence, access_token):
    """
        構文解析
    """
    data = {'sentence': sentence}
    result = base_api(data, sentence, 'nlp/v1/parse', access_token)

    print('\n<構文解析>\n')
    result_list = list()
    
    for chunks in result['result']:
        for token in chunks['tokens']:
            result_list.append(token['form'])

    print(' '.join(result_list))


def ne(sentence, access_token):
    """
        固有表現抽出
    """
    data = {'sentence': sentence}
    result = base_api(data, sentence, 'nlp/v1/ne', access_token)
    
    print('\n<固有表現抽出>\n')
    result_list = list()

    for chunks in result['result']:
        result_list.append(chunks['form'])

    print(', '.join(result_list))


def summarize(document, access_token, sent_len):
    """
        要約
    """
    data = {
        'document': document,
        'sent_len': sent_len
    }
    result = base_api(data, document, 'nlp/beta/summary', access_token)

    print('\n＜要約＞\n')
    result_list = list()

    for result in result['result']:
        result_list.append(result)
    
    print(''.join(result_list))


if __name__ == "__main__":

    doc = '昨日、マイケルと東京駅で待ち合わせした。彼とはひと月前から付き合い始めた。'
    args = get_args(doc)  # 引数取得

    # ファイルを指定した場合はそちらを優先
    if (args.file_name != None and os.path.exists(args.file_name)):
        with open(args.file_name, 'r', encoding='utf-8') as f:
            doc = f.read()
    else:
        doc = args.text

    access_token = auth(CLIENT_ID, CLIENT_SECRET)  # アクセストークン取得
    
    # API呼び出し
    l = [doc, access_token]  # 共通の引数

    parse(*l) if (args.parse) else None  # 構文解析
    ne(*l) if (args.ne) else None  # 固有表現抽出
    summarize(*l, args.summarize) if (args.summarize) else None  # 要約
```

[^1]: 商用プラン(for Enterpriseプラン)では辞書の種類やコール数の制限が緩くなるだけでなく、音声認識や音声合成のAPIも使えるようになります。しかし個人で使うには現実的でないので本記事では扱っていません。