ナイーブベイズ分類器を使って求人情報から業種を推測する

[キュウサク](https://kyusaku.jp/)という求人検索エンジンを運営しているサーチメディア株式会社の望月と申します。
弊社では大量の求人データを取り扱っているのですが、それらを分析する際に様々な機械学習を取り入れています。
今回は求人データの求人名や仕事内容などから業種を推測する分類器を作ってみようかと思います。
ご興味がある方はぜひ参考にしてみてください。

## 使用するデータ

ここではハローワークのデータを使用する事にします。
求人データの入手方法についてはこちらでは記載しませんが、各自スクレイピングやダウンロード（申請を行えば可能です）などをしてご用意ください。

https://www.hellowork.mhlw.go.jp/

ハローワークの求人票には様々な項目があります。
その中の１つに産業分類コードというものがあるのですが、今回はこれを求人企業の業種として学習させます。

産業分類コードの種類については、こちらをご覧ください。
https://www.hellowork.mhlw.go.jp/info/industry_list01.html

また判別するための情報としては、求人名、会社名、仕事内容、必要な経験、必要な資格、事業内容を使用しました。


## 形態素解析とストップワード

最初に準備として形態素解析とストップワードの設定を行います。

```python:make.py
import MeCab
import re
mecab = MeCab.Tagger("-Owakati")

# ストップワードの読み込み
with open("./stop.txt","r") as f:
    stop = f.read().split("\n")

def parse(text):
    tagger = MeCab.Tagger("-d /usr/lib64/mecab/dic/ipadic/")
    parsed = tagger.parse(text)
    nouns = []

    for line in parsed.split('\n'):
        if line == 'EOS' or line == '':
            break
        parts = line.split('\t')
        if len(parts) < 2:
            continue
        word_info = parts[1].split(',')
        # 名詞だけを抽出
        if word_info[0] == '名詞':
            nouns.append(parts[0])

    # ストップワードの除去
    nouns = [t for t in nouns if t  not in stop]
    return nouns
```

求人名や仕事内容などを分析するためには、文章を単語レベルに分解する必要があります（＝形態素解析）。
今回はMecabを使って文章を分解しており、parseという関数がその解析処理の部分となります。

注）/usr/lib64/mecab/dic/ipadic/は辞書を保管しているディレクトリでここは各自の環境に置き換えてください。

また解析する際に不要となるキーワード（＝ストップワード）を省く処理も入れています。
これはノイズになりそうなキーワードを事前にテキストファイルへ保存しておき、形態素解析のついでにそのワードを省いてます。
（記号や地名、その他判定の邪魔になりそうな単語などが当てはまります）

```
例）
〇
東京
おなじみ
お知らせ
有給
経歴
```

このような感じで１単語ごとに改行を入れたテキストファイルをご用意してください。
キーワードはどの業種の求人でも出現しそうな単語を並べます。


## 早速テスト

```python:make.py
import pandas as pd
# 求人データの読み込み
df = pd.read_csv("hello.txt", sep="\t", names=["label", "text"])

from sklearn.feature_extraction.text import TfidfVectorizer, CountVectorizer
from sklearn.naive_bayes import MultinomialNB
from sklearn.metrics import accuracy_score
from sklearn.model_selection import train_test_split
import pickle

model = MultinomialNB()

# Vectorizerの設定
vectorizer = CountVectorizer(tokenizer=parse, ngram_range=(1,1), token_pattern=None)

# 行列を作成
X = vectorizer.fit_transform(df.text)

# 学習データとテストデータに分ける
X_train, X_test, y_train, y_test = train_test_split(X, df.label, test_size=0.5, shuffle=True)

# 学習
model.fit(X_train, y_train)

# 予測
y_pred = model.predict(X_test)

# 結果の表示
accuracy = accuracy_score(y_test, y_pred)
print(accuracy)
```

用意した求人データを使ってテストをしたいと思います。
ここではデータを学習用とテスト用に分割し、学習と正解率の計測をします。

1.求人データの読み込み

求人の情報はタブ区切りでhello.txtというファイルに保存しています。
１列目が業種、２列目は求人名、会社名、仕事内容、必要な経験、必要な資格、事業内容をスペースでつなげて入れてあります。
サンプルとなる求人数は10万件ほど用意しました。

2.求人を形態素解析し、ベクトル化する

CountVectorizerの所で形態素解析とベクトル化を行っています。
ベクトル化し行列にする事でナイーブベイズ分類器にかけられる形となります。

3.半分を学習用、もう半分をテスト用に分割

train_test_splitの所でデータを分割します。
この0.5を変える事で学習用、テスト用のデータ比率を変えられます。

4.学習、予測、結果の表示

fit,predictで学習と予測を行い、結果を表示します。

## テスト結果

テキストのベクトル化にCountVectorizerを使用したところ結果は
```
0.6118869620502927
```
となりました。
期待していたよりはやや低い感じです・・・。

ちなみにVectorizerにはいくつか種類があり
CountVectorizerをTfidfVectorizerに置き換えたパターンも試しました。
```
0.48019205051191544
```
こちらはさらに低くなります。
個人的にはTfidfの方が精度は上かと思っていたのですが、結果は逆でした。
（CountVectorizerとTfidfVectorizerの違いについては他の方の説明をご確認ください。）


## 低くなっている原因の調査

なぜ低いのか調査したいと思います。
まずは定常的に利用するためモデルを保存します

```python:make.py
# Vectroizerとモデルの保存
pickle.dump(vectorizer.vocabulary_,open("hello.vec", 'wb'))
pickle.dump(model,open("hello.model", 'wb'))
```

ここではvectorizerをhello.vecに、モデルをhello.modelに保存しています。
これでいつでもこのモデルを呼び出せます。

次にダミーの求人情報を用意し、それがどのような業種に分類されるのかテストしてみようかと思います。

``` check.py
from sklearn.feature_extraction.text import TfidfVectorizer, CountVectorizer
from sklearn.feature_extraction.text import TfidfTransformer
from sklearn.naive_bayes import MultinomialNB
import pickle

import MeCab
import re
mecab = MeCab.Tagger("-Owakati")

# ストップワードの読み込み
with open("./stop.txt","r") as f:
    stop = f.read().split("\n")

def parse(text):
    tagger = MeCab.Tagger("-d /usr/lib64/mecab/dic/ipadic/")
    parsed = tagger.parse(text)
    nouns = []

    for line in parsed.split('\n'):
        if line == 'EOS' or line == '':
            break
        parts = line.split('\t')
        if len(parts) < 2:
            continue
        word_info = parts[1].split(',')
        # 名詞だけを抽出
        if word_info[0] == '名詞':
            nouns.append(parts[0])

    # ストップワードの除去
    nouns = [t for t in nouns if t  not in stop]
    return ' '.join(nouns)


model = MultinomialNB()

#モデルの読み込み
model = pickle.load(open('hello.model','rb'))

#Vectorizerの読み込み
transformer = TfidfTransformer()
loaded_vec = TfidfVectorizer(decode_error="replace",vocabulary=pickle.load(open("hello.vec", "rb")))

#ダミー求人
kyujin = []
kyujin.append(parse("サーチメディア株式会社ではpythonができるエンジニアを募集しています。簡単なコードはもちろん新入社員の教育などもお願いできると助かります"))
kyujin.append(parse("弊社スーパーの食品加工部門での募集です。お弁当やお惣菜の作成、野菜のカットなど作業内容は多岐にわたります"))
kyujin.append(parse("測量士 測量に関する知識 土地家屋調査士の資格があれば尚可。普通運転免許必須"))
kyujin.append(parse("１クラス１６名の教室で保育業務に従事していただきます。子供たちと一緒にふれあい、楽しく仕事をしてみませんか。持ち帰り業 務は一切ありません。子どもたちと一緒にふれあい、楽しく仕事してみませんかブランクのある方、未経験の方も歓迎します。幅広い年齢層の保育士が活躍してる保育園です。"))

#予測
count = transformer.fit_transform(loaded_vec.fit_transform(kyujin))
y = model.predict(count)
print(y)
```

今回は４件のダミー求人を用意しました。

> サーチメディア株式会社ではpythonができるエンジニアを募集しています。簡単なコードはもちろん新入社員の教育などもお願いできると助かります

> 弊社スーパーの食品加工部門での募集です。お弁当やお惣菜の作成、野菜のカットなど作業内容は多岐にわたります

> 測量士 測量に関する知識 土地家屋調査士の資格があれば尚可。普通運転免許必須

>１クラス１６名の教室で保育業務に従事していただきます。子供たちと一緒にふれあい、楽しく仕事をしてみませんか。持ち帰り業 務は一切ありません。子どもたちと一緒にふれあい、楽しく仕事してみませんかブランクのある方、未経験の方も歓迎します。幅広い年齢層の保育士が活躍してる保育園です

結果は以下の通りです。

```
['ソフトウェア業' 'その他の食料品製造業' '一般土木建築工事業' '児童福祉事業']
```
いかがでしょうか。
大体合っているように思います。
スーパーの求人に関しては小売店のような業種を期待していたのですが、仕事内容的に食料品製造業と出てもおかしくない気がします。

注）もし似たような結果が出ないようでしたら学習データの量を増やすかストップワードの数を調整してみてください。

ではなぜ先ほどのテストは期待したほどの数値になっていないのでしょうか。

## 結果をTSVファイルに書き出す

```python:result.py
import pandas as pd
# 求人データの読み込み
df = pd.read_csv("hello2.txt", sep="\t", names=["label", "text"])

from sklearn.feature_extraction.text import TfidfVectorizer, CountVectorizer, HashingVectorizer
from sklearn.feature_extraction.text import TfidfTransformer
from sklearn.naive_bayes import MultinomialNB
import pickle

import MeCab
import re
mecab = MeCab.Tagger("-Owakati")

# ストップワードの読み込み
with open("./stop.txt","r") as f:
    stop = f.read().split("\n")

def parse(text):
    tagger = MeCab.Tagger("-d /usr/lib64/mecab/dic/ipadic/")
    parsed = tagger.parse(text)
    nouns = []

    for line in parsed.split('\n'):
        if line == 'EOS' or line == '':
            break
        parts = line.split('\t')
        if len(parts) < 2:
            continue
        word_info = parts[1].split(',')
        # 名詞だけを抽出
        if word_info[0] == '名詞':
            nouns.append(parts[0])

    # ストップワードの除去
    nouns = [t for t in nouns if t  not in stop]
    return ' '.join(nouns)


model = MultinomialNB()

#モデルの読み込み
model = pickle.load(open('hello.model','rb'))

#vectorizerの読み込み
transformer = TfidfTransformer()
loaded_vec = TfidfVectorizer(decode_error="replace",vocabulary=pickle.load(open("hello.vec", "rb")))

f = open('result.tsv','w')

#求人データを1行ずつチェック
for index,row in df.iterrows():
  txt = []
  txt.append(parse(row['text']))

  #予測
  tfidf = transformer.fit_transform(loaded_vec.fit_transform(txt))
  y = model.predict(tfidf)

  #正解と予測が違っていたらファイルに出力
  if row['label'] != y[0]:
    f.write(row['label'] + '\t' + y[0] + '\t' + row['text'][0:100] + '\n')
```

今度は予測結果が間違っていた求人を一覧化してみようと思います。

今回は新たにハローワークの求人情報を取得したhello2.txtというファイルを用意し、その分類結果をresult.tsvに書き出してみました。
結果ファイルには「元の業種」「予測した業種」「求人情報の先頭100文字」を書き出しています。
（元の業種と予測結果が違う場合だけ出力されます）


こちらのファイルをチェックしてみたところ、一致しない原因には以下の物が考えられました。

1. 業種の分類が細かすぎる

一致しないデータで一番多かったのが
「その他の社会保険・社会福祉・介護事業」と「老人福祉・介護事業」を間違えている求人でした。
うーん。ハローワークの分類が細かく分かれすぎているせいか、この２つの違いがよくわかりません・・・。
これ以外にも似た名前の業種が多く、その微妙な違いのせいで数値を下げているように思います。

2.派遣会社の求人

ハローワークには派遣会社の求人も掲載されます。
派遣会社の求人票には派遣先の仕事内容が記載されるのですが、この時の業種分類は労働者派遣業になります。
これは問題でして、例えば建設会社の事務に関する派遣の場合、元データは労働者派遣業になるのですが、この分類器では建築工事業と分類されてしまいます。

3.会社の本業とは別と思われる求人

大きい会社の場合、メインの業種以外に別の業種・サービスを行っているケースがあります。
たとえば本業はビルメンテナンスでも、ビル内の社員食堂も運営していたり、
病院を運営しながら介護事業も一緒にしていたり。
こういった場合、求人票には業種＝本業、仕事内容＝別の業種と書かれる事もあります。
そうなると、この分類器でうまくマッチしません。

## 結論

今回の結果ですが、分類の精度としてはテストで出た数値より使えるものだと思います。
ただ学習元の業種分類が細かすぎるため、いくつかの問題が出ています。
あとはパラメータの調整やストップワードファイルの追加、派遣会社などの除去を行えば、さらに数値はよくなるのでないでしょうか。

次回は職種や雇用形態、求人以外の情報でも実験してみようかと思っています。
ご興味がある方はフォローしていただけると幸いです。