Watson NLP（自然言語処理ライブラリー）：テキスト分類

[Watson NLP (Natural Language Processing)](https://dataplatform.cloud.ibm.com/docs/content/wsj/analyze-data/watson-nlp.html) ライブラリーは、エンティティ抽出、テキスト分類、感情分類、キーワード抽出などのさまざまな自然言語処理のモデルを提供しています。
Watson NLP は、次の3つの分類アルゴリズムを実装しています
* SVM (Support Vector Machines) を使用した古典的な機械学習
* CNN (Convolutional Neural Networks) を使用した深層学習
* Google BERT 多言語モデルを使用したtransformer-basedのアルゴリズム

ここではWatson NLP を使用してさまざまなテキスト分類器のトレーニング及び分類結果を確認したいと思います。

# 1. livedoorニュースをダウンロードしてCSVに変換

今回は [Livedoorニュースコーパス](https://www.rondhuit.com/download.html) を使用して９種類のニュースカテゴリーを作成し、新たな記事を分類します。

# 2. 学習用データの準備
カスタム分類モデルを作成するために、学習用の分類ラベル付きデータをCSV形式で準備します。
CSVファイルはUTF-8形式で、以下の形式となります。
・形式は **text,labels** （textはサンプル文書、labelsはカテゴリー名。）
今回はデータとしてニュース記事をtext、記事カテゴリーをlabelsとしてCSVファイルを作成しています。
学習用データ：livedoor-train.csv
評価用データ：livedoor-eval.csv
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1322004/7a464cac-084c-4f12-0ece-cbf3f234ac3b.png)

## 3. データの読み込み
関連するライブラリを取得します
```py
from ibm_watson_studio_lib import access_project_or_space
wslib = access_project_or_space()
import json
import seaborn as sn
import matplotlib.pyplot as plt
import pandas as pd
# 関連するテキストを探せるように、テキスト表示を大きくします
pd.options.display.max_colwidth = 400
import watson_nlp
from watson_core.data_model.streams.resolver import DataStreamResolver
from watson_core.toolkit import fileio
from watson_nlp.blocks.classification.svm import SVM
from watson_nlp.workflows.classification import Ensemble
```
準備してある学習用データと評価用データを読み込みます
```py
# 学習データと評価データの２つのCSVファイルを準備します。ヘッダー行が必要で、今回は"text"と"labels"を前提としています
training_data_file = wslib.load_data("livedoor-train.csv")
eval_data_file = wslib.load_data("livedoor-eval.csv")
# 学習データの読み込み
train_orig_df = pd.read_csv(training_data_file)
# 評価データの読み込み（分類対象とするフィールドは text とします）
eval_df = pd.read_csv(eval_data_file)
eval_col = 'text'

train_orig_df.head(2)
```

## 4. SVM分類モデルの学習

```py
def prepare_data(df):
    # textカラムと分類カテゴリの*labels*が必要
    df_out = df[['text', 'labels']].reset_index(drop=True)
    # labels列は配列でなければなりません (1つしかない場合でも同様)
    df_out['labels'] = df_out['labels'].map(lambda label: [label,])
    return df_out
    
train_df = prepare_data(train_orig_df)
train_file = './train_data.json'
train_df.to_json(train_file, orient='records')

train_df.head(2)
```

```py
# 構文モデル
syntax_model = watson_nlp.load('syntax_izumo_ja_stock')
# USE埋め込みモデル
use_model = watson_nlp.load('embedding_use_multi_small')
training_data_file = train_file

# 学習データからデータストリームを作成
data_stream_resolver = DataStreamResolver(target_stream_type=list, expected_keys={'text': str, 'labels': list})
training_data = data_stream_resolver.as_data_stream(training_data_file)

# 構文ストリームを作成
text_stream, labels_stream = training_data[0], training_data[1]
syntax_stream = syntax_model.stream(text_stream)

use_train_stream = use_model.stream(syntax_stream, doc_embed_style='raw_text')
use_svm_train_stream = watson_nlp.data_model.DataStream.zip(use_train_stream, labels_stream)
```
USE (Universal Sentence Encoder) 埋め込みを使用して分類モデルを学習します。
```py
# SVMモデルの学習
svm_model = SVM.train(use_svm_train_stream)
```

# 5. アンサンブル分類モデルの学習
Watson NLPは、さまざまな分類アルゴリズムを組み合わせたアンサンブル分類器も提供しています。ここでは、次の3つの分類モデルを組み合わせます。

+ CNN
+ TF-IDF 機能を備えた SVM
+ USE (Universal Sentence Encoder) 機能を備えた SVM

アンサンブル分類器を学習させます。時間を短縮するために、CNN分類器をトレーニングするエポックを 5 に制限しています。デフォルトは 30エポックになります
```py
glove_model = watson_nlp.load('embedding_glove_ja_stock')
glove_model, use_model, cnn_epochs=5)

# ストップワードの削除は、SVM分類器のオプションの前処理ステップとして適用することもできます。 これはユースケースごとに試す必要があります。
stopwords = watson_nlp.load('text_stopwords_classification_ensemble_ja_stock')
ensemble_model = Ensemble.train(training_data_file, syntax_model, glove_model, use_model, stopwords=stopwords, cnn_epochs=5)
```

# 6. 評価用データで分類してモデルの品質を比較

学習済みのモデルを新しいデータで実行できるようになったので、評価用データで分類モデルを実行し、結果をモデルの評価にも使用できるようにします。
評価用データで分類モデルを実行します
```py
def predict_product(text):
    # 最初に構文モデルを実行
    syntax_result = syntax_model.run(text)
    # 構文結果に基づいてSVMモデルを実行
    svm_preds = svm_model.run(use_model.run(syntax_result, doc_embed_style='raw_text'))
    
    predicted_svm = svm_preds.to_dict()["classes"][0]["class_name"]
    
    ensemble_preds = ensemble_model.run(text)
    predicted_ensemble = ensemble_preds.to_dict()["classes"][0]["class_name"]
    return (predicted_svm, predicted_ensemble)
predictions = eval_df["text"].apply(lambda text: predict_product(text))
predictions_df = pd.DataFrame.from_records(predictions, columns=('SVM予測値', 'アンサンブル予測値'))
   
result_df = eval_df[["text", "labels"]].merge(predictions_df, how='left', left_index=True, right_index=True)
result_df.head()
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1322004/9838def5-eac6-2ba8-0227-811edb34f33f.png)
評価用データの右に分類結果として、分類カテゴリの予測値が２列追加されています。 answerが正解の列なのでそれと比較することで精度評価が行えます。以降では混同行列(Confusion Matrix)を使用して結果を可視化します。

## 7. 混同行列の作成とプロット
pandas crosstab を使用して、SVMとアンサンブルモデル両方の混同行列を作成して、Seaborn ヒートマップとしてプロットします。
```py
SVM_confusion_df = pd.crosstab(result_df['labels'], result_df['SVM予測値'], rownames=['Actual'], normalize='index')
ensemble_confusion_df = pd.crosstab(result_df['labels'], result_df['アンサンブル予測値'], rownames=['Actual'], normalize='index')

figure, (ax1, ax2) = plt.subplots(ncols=2, figsize=(15,7))

sn.heatmap(SVM_confusion_df, annot=True, cmap="YlGnBu", ax=ax1, cbar=False)
sn.heatmap(ensemble_confusion_df, annot=True, cmap="YlGnBu", ax=ax2, cbar=False)
ax1.title.set_text("SVM")
ax2.title.set_text("Ensemble")
ax2.set_yticklabels([])

plt.show()
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1322004/8ccc5c04-def7-c334-fc00-dc091aa25887.png)
全体として、アンサンブルモデルは、SVMモデルよりも優れた結果が得られているのがわかります。一方、SVMモデルは高速に処理を行うことができます。

---
# 参考情報
**Watson NLP (Natural Language Processing) ライブラリ**
https://dataplatform.cloud.ibm.com/docs/content/wsj/analyze-data/watson-nlp.html

**カスタム分類モデルによるテキストの分類**
***IBM Slate モデルを使用した変換プログラム・アルゴリズムのトレーニング***
https://dataplatform.cloud.ibm.com/docs/content/wsj/analyze-data/watson-nlp-classify-text.html?locale=ja&context=cpdaas&audience=wdp#train-slate

**Watson NLP（自然言語処理ライブラリー）：埋め込み**
https://qiita.com/Jungobu/private/e085fe75ebcc322b3629