PythonでXGBoostを使う

最近XGBoostに触れる機会がありましたので、使い方を確認します。
(今まで使わなかったことの方がどちらかというと珍しいのかもしれません。)

XGBoost

XGBoost (eXtreme Gradient Boosting) は、単純な分類器 (ex. 決定木) を組み合わせたアンサンブル学習モデルの実装 (フレームワーク) です。Pythonをはじめとした各種言語のライブラリで提供されています。

github.com

乳がんデータセットを分類する

それではPythonでXGBoostを使ってみます。予め pip install xgboost でインストールしておきます。

今回はUCI Machine Learning Repositoryの乳がんデータセットを使います。

archive.ics.uci.edu

ロードすると569行 x 32列のデータとなりますので、整形して学習データとテストデータに分離します。

  • 1列目はID (なのでここで捨ててます)
  • 2列目はラベルで、"M"が悪性、"B"が良性を意味します -> 変数y
  • 3〜32列目 (30列) は実数の特徴量となってます (細胞核の計測値のようです) -> 変数X
import pandas as pd

from sklearn.model_selection import train_test_split

# UCI Machine Learning Repositoryから乳がんデータセットをダウンロード
url = 'https://archive.ics.uci.edu/ml/machine-learning-databases/breast-cancer-wisconsin/wdbc.data'
df = pd.read_csv(url, header=None)

# Mは1, Bは0に置き換える
y = df[1]
y = y.str.replace('M', '1').str.replace('B', '0').astype(int)

# 3〜32列目を特徴量として使う
X = df.iloc[:, 2:]

# 学習データとテストデータを4:1で分離
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=1)

それではXGBoostを使っていきます。

XGBoostはDMatrixに整形したデータとパラメータをtrainメソッドに渡してモデルを作成します。

  • DMatrix型はPandasのDataFrame以外に、numpyのarrayやCSVファイルなどを入力ソースとして扱えます
  • パラメータの詳細はこちら
    • max_depthは決定木の高さです
    • etaは学習率で、0〜1の実数で指定します
    • 0/1の予測ですので、objectiveには"binary:logistic"を渡してます
    • デフォルトでは学習過程が出力されます (パラメータsilentに1を渡すと非表示になります)
import xgboost as xgb

# 学習データからXGBoost用のデータを生成
dm_train = xgb.DMatrix(X_train, label=y_train)

# パラメータ
param = {
    'max_depth': 2, 
    'eta': 1, 
    'objective': 'binary:logistic'
}

# XGBoostで学習
model = xgb.train(param, dm_train)
# [05:48:27] /workspace/src/tree/updater_prune.cc:74: tree pruning end, 1 roots, 6 extra nodes, 0 pruned nodes, max_depth=2
# [05:48:27] /workspace/src/tree/updater_prune.cc:74: tree pruning end, 1 roots, 6 extra nodes, 0 pruned nodes, max_depth=2
# [05:48:27] /workspace/src/tree/updater_prune.cc:74: tree pruning end, 1 roots, 6 extra nodes, 0 pruned nodes, max_depth=2
# [05:48:27] /workspace/src/tree/updater_prune.cc:74: tree pruning end, 1 roots, 6 extra nodes, 0 pruned nodes, max_depth=2
# [05:48:27] /workspace/src/tree/updater_prune.cc:74: tree pruning end, 1 roots, 4 extra nodes, 0 pruned nodes, max_depth=2
# [05:48:27] /workspace/src/tree/updater_prune.cc:74: tree pruning end, 1 roots, 6 extra nodes, 0 pruned nodes, max_depth=2
# [05:48:27] /workspace/src/tree/updater_prune.cc:74: tree pruning end, 1 roots, 6 extra nodes, 0 pruned nodes, max_depth=2
# [05:48:27] /workspace/src/tree/updater_prune.cc:74: tree pruning end, 1 roots, 4 extra nodes, 0 pruned nodes, max_depth=2
# [05:48:27] /workspace/src/tree/updater_prune.cc:74: tree pruning end, 1 roots, 6 extra nodes, 0 pruned nodes, max_depth=2
# [05:48:27] /workspace/src/tree/updater_prune.cc:74: tree pruning end, 1 roots, 6 extra nodes, 0 pruned nodes, max_depth=2

plot_importanceメソッドで特徴量の重要度を可視化できます。

import matplotlib

# 特徴量の重要度を表示
xgb.plot_importance(model)

また、to_graphvizメソッドで学習した木も表示できます。別途graphvizをインストールしておく必要があります。
(アンサンブル学習と言いながら、木1本だけになってしまってます。上の学習過程の出力のとおりですね。)

# 木の表示
xgb.to_graphviz(model)

学習したモデルでpredictメソッドにデータを渡して予測します。精度としては95.6%でした。

  • テストデータも、忘れずにDMatrixにします
  • ロジスティック回帰の値ですので、0〜1の実数が返ってきます
# テスト用のデータを生成
dm_test = xgb.DMatrix(X_test)

# 予測
y_pred = model.predict(dm_test)
# array([2.4190381e-01, 9.9575341e-01, 3.6941297e-04, 9.9836344e-01,
#        ...

# 精度
accuracy = sum(((y_pred > 0.5) & (y_test == 1)) | ((y_pred <= 0.5) & (y_test == 0))) / len(y_pred)
# 0.956140350877193

まとめ

PythonでのXGBoostの使い方をざっくりみていきました。

形態素解析前の日本語文書の前処理 (Python)

日本語の文書を扱っていますと、モデルやパラメータよりも、前処理を改善する方が精度が改善し、かつ、頑健になることがしばしばあります。

本投稿では形態素解析 (分かち書き) する前、つまり文字レベルでの前処理でよく使っているテクニックを紹介します。

お題

少し極端な例ですが、題材として架空のレビュー文を使います。

お友達の紹介で、女子2人で三時のティータイムに利用しました。
2人用のソファに並んでいただきま〜す v(^^)v なかよし(笑)
最後に出された,モンブランのケーキ。
やばっっっ!!これはうまーーーい!!
とってもDeliciousで、サービスもGoodでした😀
これで2,500円はとってもお得です☆
http://hogehoge.nantoka.blog/example/link.html

前処理のポイントがいくつかありますね。いずれも、どちらかに統一したり除外したりするほうが、意味的な分析を行うタスクには有効です。
ただし、感情分析を行いたい場合は、4つ目以下はそのまま使った方が良いかもしれません。その場合でも、辞書とマッチしやすいように加工しておくべきです。

  • 全角と半角の混在 ("2"と"2", "Delicious"と"Good", "モンブラン"と"ケーキ")
  • URLを含んでいる
  • 桁区切りの"," ("3,000")
    • このまま形態素解析にかけると、"3"と"000"に分離してしまいます
  • 文字を重ねた感情表現 ("やばっっっ!!" と "うまーーーい!!")
  • 顔文字などの感情表現テキスト ("v(^^)v" と "(笑)" )
  • 絵文字 ("😀")

全角・半角の統一と重ね表現の除去 (neologdn)

全角・半角の統一 (大括りに言えばUnicode正規化) と重ね表現の除去には、neologdnを使えます。
neologdの正規化処理を、Cythonで実装されたライブラリで、Pythonからもpip install neologdnで楽に導入できます。

github.com

使い方はシンプルで、normalizeメソッドに文字列を渡して呼び出すだけでOKです。

import neologdn

normalized_text = neologdn.normalize(text)
お友達の紹介で、女子2人で三時のティータイムに利用しました。
2人用のソファに並んでいただきますv(^^)vなかよし(笑)
最後に出された,モンブランのケーキ。
やばっっっ!!これはうまーい!!
とってもDeliciousで、サービスもGoodでした😀
これで2,500円はとってもお得です☆
http://hogehoge.nantoka.blog/example/link.html
  • アルファベットやアラビア数字、括弧やエクスクラメーションマークなどの記号は、半角に統一
  • カタカナは、全角に統一
  • "うまーーーい!!" → "うまーい!" など、重ね表現を除去
    • 一方で、"やばっっっ!!" は除去できてません
    • repeat引数に1を渡すと、2文字以上の重ね表現は1文字にできますが、そうすると"Good"は"God"になってしまったりします
  • ”〜”などの記号も除去されてます

URLの除去

正規表現でURLを除去します。

import re

text_without_url = re.sub(r'https?://[\w/:%#\$&\?\(\)~\.=\+\-]+', '', normalized_text)
お友達の紹介で、女子2人で三時のティータイムに利用しました。
2人用のソファに並んでいただきますv(^^)vなかよし(笑)
最後に出された,モンブランのケーキ。
やばっっっ!!これはうまーい!!
とってもDeliciousで、サービスもGoodでした😀
これで2,500円はとってもお得です☆

絵文字の除去 (emoji)

絵文字の判定にはそのものズバリの emoji というライブラリがあります。

github.com

絵文字のリストはemoji.UNICODE_EMOJIにありますので、それに含まれる文字は除外するようにします。

import emoji

text_without_emoji = ''.join(['' if c in emoji.UNICODE_EMOJI else c for c in text_without_url])
お友達の紹介で、女子2人で三時のティータイムに利用しました。
2人用のソファに並んでいただきますv(^^)vなかよし(笑)
最後に出された,モンブランのケーキ。
やばっっっ!!これはうまーい!!
とってもDeliciousで、サービスもGoodでした
これで2,500円はとってもお得です☆

桁区切りの除去と数字の置換

桁区切り (ついでに小数点) の数字を除去します。
あわせて、数字も全て0に置き換えてしまいます。意味的な分析ででは、数字の具体的な値を使えないことが多く、いたずらにボキャブラリを増やすだけで、後のタスクでは役に立たないことが多いためです。

import re

tmp = re.sub(r'(\d)([,.])(\d+)', r'\1\3', text_without_emoji)
text_replaced_number = re.sub(r'\d+', '0', tmp)
お友達の紹介で、女子0人で三時のティータイムに利用しました。
0人用のソファに並んでいただきますv(^^)vなかよし(笑)
最後に出された,モンブランのケーキ。
やばっっっ!!これはうまーい!!
とってもDeliciousで、サービスもGoodでした
これで0円はとってもお得です☆

記号の置き換え

記号によって割り当てられているブロックが異なりますので、タスクの性質を見極めながら適宜追加していく必要があります。ブロックはこちらの記事で整理されています。

Unicode 内のそれぞれの文字種の範囲 - みちのぶのねぐら 工作室 旧館

記号は半角スペースに置き換えてます。文中では区切り文字として使われていることが多く、完全に除去してしまうと、文や単語の区切りがわからなくなってしまうことがあるためです。

# 半角記号の置換
tmp = re.sub(r'[!-/:-@[-`{-~]', r' ', text_replaced_number)

# 全角記号の置換 (ここでは0x25A0 - 0x266Fのブロックのみを除去)
text_removed_symbol = re.sub(u'[■-♯]', ' ', tmp)

"("や"!", "☆"などが除去されていることを確認できます。

お友達の紹介で、女子0人で三時のティータイムに利用しました。
0人用のソファに並んでいただきますv    vなかよし 笑 
最後に出された モンブランのケーキ。
やばっっっ  これはうまーい  
とってもDeliciousで、サービスもGoodでした
これで0円はとってもお得です

まだ気になる点はいくつかありますが、ひとまず形態素解析にかけて様子を見ながら、ルールの追加・変更を行っていけばよいかと思います。

まとめ

自分がよく使う形態素解析前の前処理についてまとめました。

spaCyで英文の固有表現認識

今回はspaCyを使って英文の固有表現認識を行ってみます。

GiNZAを使った日本語の固有表現認識はこちら↓です。

ohke.hateblo.jp

固有表現抽出

固有表現認識 (named entity recognition: NER) は、文書から固有表現 (named entity) を抽出・分類することです。

固有表現には、固有名詞や、数字を含む表現などが該当します。固有表現は、時事性を持っていたり、数字によって膨大なパターンが存在していたりするため、辞書化が難しいものです。
そのため「辞書には無いけどこれはXに分類される単語だな」ということだけでもわかると、この後のタスクの精度改善に寄与できます。

分類 (ラベル) はいくつか定義がありますが、例えばMUC (参考) で定義されているのは7種類です。

ラベル
組織名 IEEE, 阪神タイガース
人名 田中, 所ジョージ
地名 蘇我, 東京タワー
日付 2月2日, 2/2/2019
時間 19時, 2分
金額 ¥12,000, 65M
割合 33%, 五分

固有表現抽出タスクは、例えばこんな感じで固有表現を抽出・分類します。

去年の4月、友人・安田と新幹線で京都を旅行した。
↓
去年の 4月[日付] 、友人・ 安田[人名] と新幹線で 京都[地名] を旅行した。

spaCyを使った固有表現認識

spaCyはNLPタスクのために作られたPythonライブラリで、固有表現認識以外にも、品詞タグ付けや構文解析などにも用いられます。

github.com

何はともあれspaCy本体をインストール。

pip install spacy

一緒に学習済みもダウンロードしておきます (利用可能なモデルの一覧は こちら ) 。後ほどモデル名で参照します。
en_core_web_smは、OntoNotesコーパスから学習したモデルです。

python -m spacy download en_core_web_sm

それではPythonの実装です。今回はCNNの記事を使いました。

  • 最初にダウンロードしたモデル "en_core_web_sm" をロードして、それをメソッド呼び出し (__call__) するだけで引数の文書を解析してくれます
  • entはSpan型になっており、label_プロパティに固有表現のラベルが入ってます
import spacy

# モデルをロード
nlp = spacy.load('en_core_web_sm')

# 抜粋: https://edition.cnn.com/2019/02/01/asia/japan-hacking-cybersecurity-iot-intl/index.html
news = """Beginning on February 20, Japanese officials will start probing 200 million IP addresses linked to the country, sniffing out devices with poor or little security.A law was passed last year to enable the mass hack, as part of security preparations ahead of the Tokyo 2020 Olympics.According to the Ministry of Internal Affairs and Communications (MIAC), two-thirds of cyber attacks in Japan in 2016 targeted IOT devices. Officials fear some kind of IOT-related attack could be used to target or disrupt the Olympics."""

# 解析
doc = nlp(news)

# 固有表現を見てみる
for ent in doc.ents:
    # spacy.tokens.span.Span
    print(ent.text, ent.label_, ent.start_char, ent.end_char)

結果はこうなりました。ラベルの意味は こちら ですが、良い感じに分類されています。

  • "the Tokyo 2020 Olympics" もちゃんとイベントとしてラベリングされてて、驚きです
  • "two-thirds"が数字、"last year"が日付として柔軟にラベリングしてます
  • "IOT"が組織として認識されている点が惜しいです
February 20 DATE 13 24
Japanese NORP 26 34
200 million CARDINAL 64 75
last year DATE 179 188
the Tokyo 2020 Olympics EVENT 256 279
the Ministry of Internal Affairs and Communications ORG 293 344
MIAC ORG 346 350
two-thirds CARDINAL 353 363
Japan GPE 384 389
2016 DATE 393 397
IOT ORG 407 410
IOT ORG 448 451
Olympics EVENT 506 514

文章を少し変えて、実在しない組織 "Ministry of Hogehoge and Fugafuga" に置き換えて解析させてみました。
それでもちゃんと組織 ("ORG") として認識されています。

According to the Ministry of Hogehoge and Fugafuga, two-thirds of cyber attacks in Japan in 2016 targeted IOT devices.
↓
the Ministry of Hogehoge and Fugafuga ORG

もうちょっと遊んでみます。文章のTokyoをChibaに変えてみたところ、"the Tokyo 2020 Olympics"として1語で認識されていたものが、Chiba, 2020, Olympicsの3語に分割されてそれぞれでラベル付されていました。学習データに東京オリンピックが入っていたのでしょう。

A law was passed last year to enable the mass hack, as part of security preparations ahead of the Chiba 2020 Olympics.
↓
Chiba GPE 260 265
2020 DATE 266 270
Olympics EVENT 271 279

まとめ

今回はspaCyで固有表現認識を行いました。日本語で同じことをするにはどうすれば良いのか、また次の機会で調べてみたいと思います。