#はじめに
Nishikaさんの自社開催コンペ「AIは芥川龍之介を見分けられるのか?」(このコンペは、すでに終了)を今更ながらやってみました。TF-IDFベクトライザで分類器をRandomForestでやったところ、スコアが0.53でした。分類器をLightGBMにしたところスコアが0.93と爆上がり。そこで疑問に思ったんです。LightGBMは、芥川の文章をどのようにして他の文書から抽出しているのでしょうか?SHA P値を使って考えてみました。
#手順
作業手順は次の通り。
データの読み込み→MeCabでトークン化(文章の「名詞」、「動詞」、「副詞」、「形容詞」のみ抽出)→TF-IDFベクトライザ→LightGBMで学習→SAHP値をみてみる
##ライブラリインポート
import pandas as pd
import MeCab
import re
from wordcloud import WordCloud
import matplotlib.pyplot as plt
import japanize_matplotlib
%matplotlib inline
from sklearn.feature_extraction.text import TfidfVectorizer
import lightgbm as lgb #LightGBM
import numpy as np
import shap
shap.initjs()
##データ読み込み
データは著作権フリーの「青空文庫」から。
# train データ読み込み
train_df = pd.read_csv('./data/train.csv')
print(train_df.shape)
print(train_df.head())
#(3312, 3)
# writing_id body #author
#0 0 \r\n 先ごろの本欄に僕の「風報」にかいた「天皇陛下に捧ぐる言葉」を評して俗うけを狙った媚... 0
#1 1 \r\n 旅の眼に映じた外国の正月をといふお需めで、一昔前の記憶から探してみたが、其処にはほ... 0
#2 2 \r\n 或る心持のよい夕方、日比谷公園の樹の繁みの間で、若葉楓の梢を眺めていたら、どこから... 0
#3 3 \r\n\r\n[#3字下げ]一[#「一」は中見出し]\r\n\r\n 島々《しま/\》と云... 1
#4 7 \r\n\r\n 或る田舎に母と子とが住んでいた。そして或る年の秋、次のようなことがあった。... 0
著者の確認
authorの1が芥川、0がその他の方。
芥川の文章が252、その他が3060あります。
train_df['author'].value_counts()
# 0 3060
# 1 252
# Name: author, dtype: int64
MeCabトークナイズ
文書から、名詞、動詞、副詞、形容詞のみを抽出。
# ユニコード正規化
train_df["body"] = train_df["body"].str.normalize("NFKC") # trainデータ
# アルファベットを小文字に統一
train_df["body"] = train_df["body"].str.lower() # train データ
# Tagger
tagger = MeCab.Tagger('-d /usr/local/lib/mecab/dic/ipadic')
# ひらがなのみの文字列にマッチする正規表現
kana_re = re.compile("^[ぁ-ゖ]+$")
# 記号の削除
code_re = re.compile('[\t\s!"#$%&\'\\\\()*+,-./:;;:<=>?@[\\]^_`{|}~○「」「」〔〕“”〈〉''『』【】&*()$#@?!`+¥¥%♪…◇→←↓↑。・ω・。゚´∀`ΣДx⑥◎©︎♡★☆▽※ゞノ〆εσ><┌┘]')
# 数字の削除
num_re = re.compile('\d+,?\d*')
def mecab_tokenizer(text):
# テキストを分かち書きする関数を準備する
parsed_lines = tagger.parse(text).split("\n")[:-2]
surfaces = [l.split('\t')[0] for l in parsed_lines]
features = [l.split('\t')[1] for l in parsed_lines]
# 原型を取得
bases = [f.split(',')[6] for f in features]
# 品詞を取得
pos = [f.split(',')[0] for f in features]
# 各単語を原型に変換する
token_list = [b if b != '*' else s for s, b in zip(surfaces, bases)]
# 名刺、動詞、形容詞、副詞 に絞り込み
target_pos = ["名詞", "動詞", "形容詞", "副詞"]
token_list = [t for t, p in zip(token_list, pos) if p in target_pos]
# アルファベットを小文字に統一
token_list = [t.lower() for t in token_list]
# ひらがなのみの単語を除く
token_list = [t for t in token_list if not kana_re.match(t)]
# 記号の削除
token_list = [t for t in token_list if not code_re.match(t)]
# 算用数字の削除
token_list = [t for t in token_list if not num_re.match(t)]
return token_list
# 分かち書きしたデータを作成する
sentences = train_df.body.apply(mecab_tokenizer)
print(sentences[:5])
# 0 [先ごろ, 本欄, 僕, 風, 報, 天皇陛下, 捧ぐ, 言葉, 評す, 俗, 狙う, 媚態...
# 1 [旅, 眼, 映じる, 外国, 正月, 需, 一昔, 前, 記憶, 探す, 其処, お正月,...
# 2 [心持, 夕方, 日比谷公園, 樹, 繁み, 間, 若葉, 楓, 梢, 眺める, ラジオ, ...
# 3 [字, 下げ, 一, 一, 見出し, 島々, 云, 町, 宿屋, 着く, 午, 過ぎ, 夕方...
# 4 [田舎, 母, 子, 住む, 年, 秋, 次, 本当に, 天気, 母, 子, 朝, 会話, ...
# Name: body, dtype: object
WordCloudしてみる
試しにWordCloud。
芥川文書とそれ以外を比較するためデータを分離。
# 芥川テキスト
akutagawa_text = train_df[train_df['author']==1]
# それ以外
other_text = train_df[train_df['author']==0]
WordCloud
FONT_PATH = '/Library/Fonts/Arial Unicode.ttf'
wc_akutagawa = WordCloud(font_path = FONT_PATH,
width = 300,
height = 200,
max_font_size=40).generate(str(akutagawa_text.body.apply(mecab_tokenizer)))
wc_other = WordCloud(font_path = FONT_PATH,
width = 300,
height = 200,
max_font_size=40).generate(str(other_text.body.apply(mecab_tokenizer)))
WordCloudを表示
plt.figure(figsize=(100,200))
plt.subplot(121)
plt.title('芥川ワードクラウド', fontsize=200)
plt.imshow(wc_akutagawa)
plt.axis('off')
plt.subplot(122)
plt.title('その他ワードクラウド', fontsize =200)
plt.imshow(wc_other)
plt.axis('off')
plt.tight_layout()
plt.show()
TF-IDFベクトル化
vectorizer = TfidfVectorizer(min_df=1,lowercase=False,tokenizer=mecab_tokenizer)
# trainデータのベクトル化
train_tfidf_weighted_matrix = vectorizer.fit_transform(train_df['body'])
LightGBMで学習
train_x = train_tfidf_weighted_matrix.toarray()
train_y = train_df['author']
lgb_train = lgb.Dataset(train_x, train_y)
# lgb_val = lgb.Dataset(val_x, val_y, reference=lgb_train)
lgbm_params = {
'objective': 'regression',
'metric': 'rmse',
'verbose': -1,
}
clf_lgb = lgb.train(lgbm_params,
train_set=lgb_train,
num_boost_round=1000,
verbose_eval=50,
)
SHAP値
explainerを構築、SHAP値取得。
explainer = shap.TreeExplainer(clf_lgb)
shap_values = explainer.shap_values(train_x)
上位特徴量を表示
shap.summary_plot(shap_values,
train_x,
feature_names=vectorizer.get_feature_names(),
plot_type='dot')
Feature Importance
Feature Importanceも。
shap.summary_plot(shap_values,
train_x,
feature_names=vectorizer.get_feature_names(),
plot_type='bar')
# savefig用 f3.savefig('./feature_impo
特徴量の特徴を考える
特徴量のトップは「大正」。芥川は、1892年〈明治25年〉-1927年〈昭和2年〉の人なので、大正をまるまる生きた人となります。ただ芥川以外の作者がどの時代かはわからないので、この特徴量が本当に効いているかどうかは不明。一人称は「僕」中心?「唯」とか「心もち」という言葉を多用しているのか?下の方の「莫迦」って、「馬鹿」でも「ばか」でもなく「莫迦」をよく使う?「テエブル」は何かと思ったら「テーブル」らしい。芥川特有の使い方なのか?他の作者がどう使っているかはチェックしていません。ワードクラウドにある「谷崎」は谷崎潤一郎(1886年(明治19年)-1965年(昭和40年))?だとすると、文書中には小説以外も含まれるのか?
しかし、これくらいで文書の仕分けができちゃうのが結構驚きではありますね。
参考文献
これのLogistic RegressionをLightGBMで置き換えました。
Sentiment Analysis by SHAP with Logistic Regression