SIGNATE Student Cup 2020 【予測部門】入門編チュートリアル

create date : Aug. 15, 2020 at 04:25:06
このチュートリアルは、SIGNATE Student Cup 2020【予測部門】における入門編のチュートリアルです。

概要

SIGNATE Student Cup 2020【予測部門】(https://signate.jp/competitions/281) のチュートリアルです.
予測部門では, 英語圏の求人情報に含まれるテキストデータ(職務内容に関する記述)をもとに, 職種を予測するアルゴリズムを作成していただきます.
今回用いる評価尺度はF1Score(マクロ平均)です. F1ScoreとはPrecisionとRecallの調和平均であり, 全クラスのF1Scoreの平均を取ることでF1Score(マクロ平均)が算出されます.
テストデータに対するF1Score(マクロ平均)をなるべく大きくするモデルを作ることが今回の目的です.
このチュートリアルでは古典的な自然言語処理の手法であるBag of Wordsを用いて特徴量を作成し, 勾配ブースティング決定木のアルゴリズムの1つであるXGBoostを用いてモデリングを行なっています.
これをきっかけに, 今まで自然言語処理に馴染みのなかった方も是非モデリングに取り組んでみてください.

分析環境

  • OS: MacOS Mojave 10.14.6
  • 言語: Python==3.7.4
  • ライブラリ
    • pandas==0.24.2
    • matplotlib==3.2.1
    • seaborn==0.10.0
    • nltk==3.5
    • re==2.2.1
    • scikit-learn==0.23.2
    • xgboost==0.90

データの読み込みとEDA

まずはデータの読込や可視化に必要なライブラリをインポートします.

In [1]:
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns

学習用データ(train.csv)と評価用データ(test.csv)を読み込み, 中身を確認してみます.

In [2]:
#学習用データと評価用データの読み込み
train = pd.read_csv("./train.csv")
test = pd.read_csv("./test.csv")
In [3]:
#データのサイズ確認
print(train.shape)
print(test.shape)
(2931, 3)
(1743, 2)
In [4]:
#学習用データの中身を確認
train.head()
Out[4]:
id description jobflag
0 0 Executes and writes portions of testing plans,... 2
1 1 Maintain Network Performance by assisting with... 3
2 2 Supports the regional compliance manager with ... 4
3 3 Keep up to date with local and national busine... 1
4 4 Assist with Service Organization Control (SOC)... 4
In [5]:
#評価用データの中身を確認
test.head()
Out[5]:
id description
0 2931 Work with the implementation teams
1 2932 Set technology direction, strategy, policies, ...
2 2933 Experience with Orchestration and Automation p...
3 2934 Apply your expertise in quantitative analysis,...
4 2935 Provide regular maintenance for knowledge rete...

次に学習用データに含まれる, 職業ラベル(jobflag)のデータ数を確認してみます

In [6]:
#学習用データに含まれるjobflagをカウント(+可視化)
print(train['jobflag'].value_counts())
train['jobflag'].value_counts().plot(kind = 'bar')
3    1376
1     624
4     583
2     348
Name: jobflag, dtype: int64
Out[6]:

職種はSoftware engineer(jobflag=3)が最も多く, Machiner learning engieer(jobflag=2)が最も少なくなっています.
Data Scientist(jobflag=1)とConsultant(jobflag=4)は, ともに600件前後のデータとなっています.
続いて, descriptionに含まれる文字の長さも確認してみましょう.

In [7]:
#学習用データ, 評価用データのdescriptionに含まれる文字数を確認
train_length = train['description'].str.len()
test_length = test['description'].str.len()
In [8]:
#可視化
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)
ax.violinplot([train_length, test_length])
ax.set_xticks([1, 2])
ax.set_xticklabels(['train', 'test'])
ax.set_ylabel('word count')
Out[8]:
Text(0, 0.5, 'word count')

descriptionの文字数は, 一部で1000文字を超えるものなどばらつきがありますが, 概ね100文字前後の文字数が多くなっています.
学習用データの職業ラベル(jobflag)ごとの文字数も合わせて確認してみましょう.

In [9]:
#学習用データの職業ラベル(jobflag)ごとの文字数を確認
fig = plt.figure(figsize = (15, 4))
for flag in [1, 2, 3, 4]:
    train_length_flag = train[train['jobflag'] == flag]['description'].str.len()
    ax = fig.add_subplot(1, 4, flag)
    ax.violinplot(train_length_flag)
    ax.set_xticks([1])
    ax.set_xticklabels([flag])
    ax.set_ylabel('word count')
plt.tight_layout()

前処理

続いてデータの前処理(クリーニング)を行います.
ここでは, 不要文字(アルファベット以外の文字)や3文字以下の単語の除去, およびステミング(単語の語幹を取り出す作業のこと. 派生語を同じ単語として扱えるようにする)を行います.
学習用データと評価用データのdescriptionを一括処理するため, 一度データを結合した上でクリーニングを行っていきます.

In [10]:
#学習用データと評価用データを結合する (両データに対し一括で前処理を行うため)
combined = train.append(test, ignore_index=True, sort=True)
In [11]:
#以下の手順でdescriptionデータのクリーニングを行う.
# アルファベット以外の文字をスペースに置き換える
# 単語長が3文字以下のものは削除する
# ステミング(単語の語幹を取り出す作業のこと. 派生語を同じ単語として扱えるようにする)

import re
from nltk.stem.porter import PorterStemmer
stemmer = PorterStemmer()

def cleaning(texts):
    clean_texts = []
    for text in texts:
        #アルファベット以外をスペースに置き換え
        clean_punc = re.sub(r'[^a-zA-Z]', ' ', text)
        #単語長が3文字以下のものは削除する
        clean_short_tokenized = [word for word in clean_punc.split() if len(word) > 3]
        #ステミング
        clean_normalize = [stemmer.stem(word) for word in clean_short_tokenized]
        #単語同士をスペースでつなぎ, 文章に戻す
        clean_text = ' '.join(clean_normalize)
        clean_texts.append(clean_text)
    return clean_texts

combined_cleaned = combined.copy()
combined_cleaned['description'] = cleaning(combined['description'])
In [12]:
# クリーニング結果の確認
print('#original\n', combined['description'][0])
print("-----")
print('#cleaned\n', combined_cleaned['description'][0])
#original
 Executes and writes portions of testing plans, protocols, and documentation for assigned portion of application; identifies and debugs issues with code and suggests changes or improvements.
-----
#cleaned
 execut write portion test plan protocol document assign portion applic identifi debug issu with code suggest chang improv

セミコロン(;)や文字長3文字以下の単語が除去され, 単語の語幹が抽出されていることが分かります.
次に, このクリーニング済みのデータに対して, Bag of Words (文章中の単語の登場回数をカウントし, 特徴量化する手法)を適用して, descriptionをベクトル化します.

In [13]:
#bag of words(文章中の単語の登場回数をカウントし, 特徴量化する手法)を用いて、descriptionをベクトル化する

from sklearn.feature_extraction.text import CountVectorizer

bow_vectorizer = CountVectorizer(max_df=0.90, min_df=2, max_features=2000, stop_words='english')
bow = bow_vectorizer.fit_transform(combined_cleaned['description'])
print(bow.shape)
(4674, 1802)

モデリング

次に, Bag of Wordsで生成した特徴量を用いて, モデルの学習・予測を行っていきましょう.
今回は, 勾配ブースティング決定木のアルゴリズムの1つで, オープンソースのライブラリが公開されているXGBoostを用いてモデリングを行います.
投稿前の精度検証では, ホールドアウト法を用いて学習用データの70%をモデルの学習に、残り30%を精度検証用に使いたいと思います.

In [14]:
#学習用データを分割して, 投稿前の精度検証を行う

from sklearn.model_selection import train_test_split

delimit_num = train.shape[0]
train_bow = bow[:delimit_num, :]
test_bow = bow[delimit_num:, :]

x_bow_train, x_bow_valid, y_bow_train, y_bow_valid = train_test_split(train_bow, train['jobflag'], test_size=0.3, random_state=0)
In [15]:
#XGBを用いて分類器を作成し、学習・予測を行う

from xgboost import XGBClassifier

mod = XGBClassifier(max_depth=6, n_estimators=1000, n_jobs=-1)
mod.fit(x_bow_train, y_bow_train)
pred = mod.predict(x_bow_valid)

予測結果の可視化

In [16]:
from sklearn.metrics import confusion_matrix, classification_report
labels = [1, 2, 3, 4]
re_labels = ["DS", "ML", "SE", "Cons"]

#混同行列の作成
conf_mx = confusion_matrix(y_bow_valid, pred, labels=labels)
conf_df = pd.DataFrame(data=conf_mx, index=[x + "(act)" for x in re_labels], columns=[x + "(pred)" for x in re_labels])

#可視化
plt.figure(figsize=(4, 4), dpi=150)
sns.heatmap(conf_df, cmap='Blues', annot=True, fmt='d', annot_kws={"size": 12}).invert_yaxis()
plt.tight_layout()
 
print(classification_report(y_bow_valid, pred, labels=labels, digits=3))
              precision    recall  f1-score   support

           1      0.612     0.552     0.580       183
           2      0.411     0.309     0.353        97
           3      0.688     0.792     0.736       423
           4      0.548     0.480     0.512       177

    accuracy                          0.626       880
   macro avg      0.565     0.533     0.545       880
weighted avg      0.614     0.626     0.617       880

予測結果を見ると,
・「Software engineer (jobflag=3)」は比較的上手く判別ができている
・「Machine learning engieer(jobflag=2)」は, 他の職種と満遍なく誤判定が発生している
・「Data Scientist (jobflag=1)」や「Consultant (jobflag=4)」は, Software Engineerと誤判定しているがケースも多い
ことが分かります.

今回は, 古典的な手法(BoW)を用いて特徴量生成⇒モデル学習を行いましたが,
これらの誤判定を抑制しさらに精度を改善するために,
TF-IDFやWord2Vecといった特徴量生成, あるいはBERT(Bidirectional Encoder Representations from Transformers)といった最新の自然言語処理モデルに挑戦するのも良いでしょう.

投稿用ファイルの作成

改めて学習用データ(train)全体を使って学習し, 評価用データ(test)に対して予測を行います.

In [17]:
mod.fit(train_bow, train['jobflag'])
pred_sub = mod.predict(test_bow)

コンペ投稿用のファイルは, id列とjobflag列(予測結果)の2列で, ヘッダなしの形で作成します.

In [18]:
sample_submit_df = pd.DataFrame([test['id'], pred_sub]).T
sample_submit_df.to_csv('./sample_submit.csv', header=None, index=None)

まとめ

本チュートリアルでは, Bag of Wordsによる特徴抽出⇒モデル学習という古典的な手法でモデリングを試してきました.
一部の職種(Software Engineer)では今回の手法でも比較的上手く判別ができましたが, Machine learning engineerをはじめ他の職種はまだ誤判定が多くなっています.
さらなるスコア改善に向けて, 判別が難しい職種固有の特徴量の探索や最新のモデリング手法など, 様々な改善策を試して学生チャンピオンを目指して下さい!!

create date : Aug. 15, 2020 at 04:25:06