[Python] 漫画のWikipediaの説明文から発表年を推定する その2 どの単語が分類に有用かを調べる

こんにちはLink-Uの町屋敷です。

前回はWikipediaの漫画の説明文から発表年を推定しました。

そこそこ推定できましたが、そもそも漫画の説明文から発表年を推定してなにがうれしいかって特に生産性は無いんですよね、

しかし、学習器自体に生産性が皆無でも学習器がどんな基準で推定したかがわかれば何か別の有意義な情報が得られる可能性があります。

例えば今回で言うと入力は文章中に出てくる単語なので、時代とともに出現する単語の傾向を知ることが出来ます。

また、学習器の精度を上げていくには、入力する特徴量と答えの関係を色んな角度から見てある仮説を立てて、特徴量を削ったり加工していくんですが、そのあたりとして使うことも出来ます。

そこで前回はアンサンブル系の学習機を使ったときに学習に利用されるfeature_importanceというパラメーターで単語の重要度を見ましたが、

今回は別の方法でやってみます、そこで使うのがLIMEです。

LIMEはどんなアルゴリズムで学習器を作っても入力された特徴量の重要度を出力してくれるスグレモノで、マルチクラスにも対応しています。

LIMEを使う

LIMEは回帰問題でも使えるんですが、マルチクラス分類のほうがわかりやすいのでそちらを使います。

もともと回帰の問題だったのでこれをある期間ごとに分け直します。

def _ConvertLabelForClf(labels):
    for i , v in enumerate(labels):
        if v <= 1990:
            labels[i] = 0  
        elif v <= 2005:
            labels[i] = 1
        elif v <= 2010:
            labels[i] = 2 
        else:
            labels[i] = 3
    return labels

今回は1990年以前、1996-2005,2006-2010,2011-の4クラスに分けました。

前回はGradiantBoostingの回帰の学習器を作りましたが、今回は分類の学習器を作ります。

パワメータ調整などやることはほぼ変わらないです。4クラスの数はすべて同じにはならないので、不均等データのための処理(アンダーサンプリングなど)を行う必要も考えましたが、何もせずにとりあえず回した結果がそんなに悪くなかったのでやってません。

ライブラリはpipで入ります。

def LearnClassifier():
    
    features = joblib.load('{0}/features.pkl'.format(WRITE_JOBLIB_DIR))
    labels  = joblib.load('{0}/labels.pkl'.format(WRITE_JOBLIB_DIR))
     
    print(np.shape(features))
    print(np.shape(labels))
    
    tf = TfidfTransformer()
    features = tf.fit_transform(features)
    #features = _CompressWithAutoencoder(features)
    labels   = _ConvertLabelForClf(labels)
    
X_train, X_test, y_train, y_test = train_test_split(features, labels, test_size=0.1, random_state=1234)
    
    _, counts = np.unique(y_train, return_counts=True)
    weights = counts/len(y_train)
    weights[0] =  1 - weights[1] - weights[2] - weights[3]
    
    clf = LGBMClassifier(
        learning_rate =0.1, n_estimators=1000,
        max_depth=3,
        objective='multiclass',
    )
    
    print(weights)
    print('Learning Start')
    time = timer()
    clf.fit(X_train,y_train)
    time = timer() - time
    print('Learning Finishn Time: {0}'.format(time))
    
    joblib.dump(clf, '{0}/gradient_boosting_classifier.pkl'.format(WRITE_JOBLIB_DIR))
    y_pred = clf.predict(X_test)
    #for yt, yp in zip (y_test, y_pred):
    #    print((yt,yp))
    
    cm = confusion_matrix(y_test, y_pred, [0,1,2,3])
    print('n')
    print(cm)
    
    f1 = f1_score(y_test, y_pred, [0,1,2,3], average='macro')
    acx = accuracy_score(y_train, clf.predict(X_train), [0,1,2,3])
    acy = accuracy_score(y_test, y_pred, [0,1,2,3])
    
    print('f1 = {0},train_accuracy = {1}, test_accuracy = {2}'.format(f1, acx, acy))

コンフュージョンマトリックスとF1値はこんな感じ

そこそこといったところ。この学習器を使ってLIMEを試してみよう。

def InspectClassifier(dict_param = [5, 0.1]):
    dictionary = corpora.Dictionary.load_from_text('filtered_dic_below{0}_above{1}.txt'.format(dict_param[0], dict_param[1]))
    
    labels  = joblib.load('{0}/labels.pkl'.format(WRITE_JOBLIB_DIR))
    features = joblib.load('{0}/features.pkl'.format(WRITE_JOBLIB_DIR))
    clf = joblib.load('{0}//gradient_boosting_classifier.pkl'.format(WRITE_JOBLIB_DIR))
    
    
    X_train, X_test, y_train, y_test = train_test_split(features, labels, test_size=0.1, random_state=1234)

まず、データのロード。train_test_splitのシードには先程の学習時に使ったものと同じものを使うこと。

    from lime import lime_tabular
    from sklearn.pipeline import make_pipeline
    c = make_pipeline(clf)
    
    class_names = ['< 1990', '< 2005', '< 2010', '>= 2010']
    
    id2token = np.empty(len(dictionary.token2id), dtype='U64')
    for k, v in dictionary.token2id.items():
        id2token[int(v)] = k
    
    exp = lime_tabular.LimeTabularExplainer(X_train, feature_names=id2token, class_names=class_names)

特徴の列と単語のマッピングにはとgensimで作った辞書はそのまま使えないので加工する。

LimeTextExplainer()があるのにLimeTabularExplainer()を使ってる理由は、テキストのほうに分かち書きされた原文を入力に要求されるため、

英語ならそのままぶち込めばいいが日本がだといちいち分けて原型に戻してやらないといけない。さすがにめんどくさい。あとマルチバイト対応しているのか謎。

あとは公式ドキュメントに沿ってやるだけ。

    idx = 0 
    for idx in range(100):
        lime_result = exp.explain_instance(X_test[idx], c.predict_proba, num_features=100, labels=(0,1,2,3))
        
        lime_result.save_to_file('./exp/exp_{0}_{1}.html'.format(idx, y_train[idx]))
        print('# {0} finished'.format(idx))

公式は結果の表示に

show_in_notebook()

を使ってるけどiPython入れてないと何も起こらないので注意!

また、入力する単語数が多すぎると

こんなのが出て進まない、単語数を減らすために、gensimの辞書生成のパラメーターを調整して単語数を減らす。

試しに単語数を減らす代わりにオートエンコーダーで次元圧縮して突っ込んでみたけど爆死したのでこっちのほうが確実?

結果としてhtmlファイルで出力されるのでブラウザーでみてみよう。

各文章の分類に利用された単語が影響度の高い順に出力されている。

つまり、1990年以下かどうかを表す一眼左の青いグラフが書かれているところに上から順に現像、全集、ビデオとあるが、これは文章中にこれらの単語が一度も出現しないとき、1990年以前でないクラスに文章が分類される可能性が高くなることを示している。条件が0以下つまり文章に無いときなってしまっているのでややこしいが、要は現像とか全集とかの単語がなかったらそんなに昔の作品じゃないんじゃない?ってこと。左上にどの程度の自信があって分類しているかがわかるのも地味に嬉しい。

まとめ

LIMEを使うとどの特徴量がどれだけ分類に貢献しているかがわかりやすい。SVMとかにも使えるのも良い点。

参考サイト

https://github.com/marcotcr/lime

https://qiita.com/fufufukakaka/items/d0081cd38251d22ffebf