【転職会議】クチコミをword2vecで自然言語処理して会社を分類してみる

## はじめに
LivesenseAdventCalendar 2016 の20日目を担当する @naotaka1128 です。
現在、転職会議という転職クチコミサービスのデータアナリストを担当しております。

[転職会議](https://jobtalk.jp/)は会社のクチコミが数百万件集まっている日本最大級の転職クチコミサービスです。現状はクチコミや評点を表示しているだけなのですが、今後はクチコミを自然言語処理などで分析して今までは手に入らなかったような有益な情報を世の中に提供していきたいと思っております。

今回はその取っ掛かりとして word2vec および doc2vec という自然言語処理の技術を用いてクチコミを分析し、会社の分類などを行ってみようと思います。

## 使用する自然言語処理技術
### word2vec
昨今、word2vecという自然言語処理の技術が話題です。ご存じの方も多いかと思いますが、大量の文章をもちいて単語をベクトル表現で数値化し、以下のような単語間の計算を可能にします。

- 王様 - 男 + 女 = 女王
- パリ - フランス + 日本 = 東京

### doc2vec
word2vecを拡張し、文書そのもので上記のような計算を可能にしたdoc2vecというものもあります。
雑に言うと word2vec で獲得した単語のベクトルを合算したようなものでしょうか。

doc2vecでは文書の数値化により異なる文書同士の類似度を計算できるようになります。会社のクチコミを一つの文書と捉えdoc2vecで分析して、会社同士の関係性を分析してみようと思います。

## 使用する技術
- Python
- gensim
    - word2vec / doc2vec
    - 他にトピックモデルという自然言語処理技術も実装されている
        - 以前、トピックモデルを用いて[自然言語処理でラーメン屋を分類してみる](http://qiita.com/n_uchida/items/87d717961bd0c34e7a64)をやりました。ご興味があれば見てみて下さい。
- MeCab

## 流れ
以下のような流れでやっていきます。
1. 会社の口コミを形態素解析
2. gensim で doc2vec のモデル構築
　・ doc2vec のモデル構築時に word2vec モデルも同時に構築される
3. 構築したモデルで
　3-1. クチコミに登場した単語で word2vec
　3-2. doc2vecで会社同士の類似度計算
　3-3. doc2vecで会社同士の加減算

## 1. 会社の口コミを形態素解析
[自然言語処理でラーメン屋を分類してみる](http://qiita.com/n_uchida/items/87d717961bd0c34e7a64)とほぼ同じです。

```py3
# クチコミデータの読み込み
from io_modules import load_data  # 自作のDB読み込みライブラリ
rows = load_data(LOAD_QUERY, KUCHIKOMI_DB)  # [会社名, クチコミ]

# 参考記事のstem関数で語幹を抽出
from utils import stems  # 参考記事の実装ほぼそのまま
companies = [row[0] for row in rows]
docs = [stems(row[1]) for row in rows]

"""
以下のようなデータを作っています
companies = ['株式会社ブラックカンパニー', 'やりがい有限会社', ...]
docs = [
  ['やりがい', '足りない', '残業', 'とても', '多い', ...
  ['深夜残業', '当たり前', '辛い', '死にたい', '無理', ...
   ...
]
"""
```
* 全体として単なる前処理
* 語幹の抽出: [この記事](http://qiita.com/katryo/items/f86971afcb65ce1e7d40#3-5)を参考にしました
* 備考: 特別な辞書は準備していません(neologdを使っただけ)

## 2. gensim で doc2vec のモデル構築
さて、ここが自然言語処理のキモです。

と言いたいところですがライブラリ呼び出すだけなので大したことやりません。
また計算も早く、あっけなく終わって拍子抜けでした。

```py3
# ライブラリ読み込み
from gensim import models

# gensim にクチコミを登録
# クチコミに会社名を付与するため、参考記事で実装されていた拡張クラスを使っています
sentences = LabeledListSentence(docs, companies)

# doc2vec の学習条件設定
# alpha: 学習率 / min_count: X回未満しか出てこない単語は無視
# size: ベクトルの次元数 / iter: 反復回数 / workers: 並列実行数
model = models.Doc2Vec(alpha=0.025, min_count=5,
                       size=100, iter=20, workers=4)

# doc2vec の学習前準備(単語リスト構築)
model.build_vocab(sentences)

# Wikipedia から学習させた単語ベクトルを無理やり適用して利用することも出来ます
# model.intersect_word2vec_format('./data/wiki/wiki2vec.bin', binary=True)

# 学習実行
model.train(sentences)

# セーブ
model.save('./data/doc2vec.model')

# 学習後はモデルをファイルからロード可能
# model = models.Doc2Vec.load('./data/doc2vec.model')

# 順番が変わってしまうことがあるので会社リストは学習後に再呼び出し
companies = model.docvecs.offset2doctag
```

LabeledListSentence の実装は以下のとおりです。

```py3
# 参考記事： http://qiita.com/okappy/items/32a7ba7eddf8203c9fa1
class LabeledListSentence(object):
    def __init__(self, words_list, labels):
        self.words_list = words_list
        self.labels = labels

    def __iter__(self):
        for i, words in enumerate(self.words_list):
            yield models.doc2vec.LabeledSentence(words, ['%s' % self.labels[i]])
```

## 3-1. クチコミに登場した単語で word2vec
さて、あっという間にモデルが構築できました。

word2vecの精度が悪ければdoc2vecの結果も必然的に悪くなるので、
word2vec ~~で遊びながら~~ 精度を確かめていきます。

### 似ている単語
まずは転職クチコミで大人気の「残業」あたりから行きましょう。

```py3
# model.most_similar(positive=[単語]) で似ている単語が出せる
>> model.most_similar(positive=['残業'])
[('時間外労働', 0.8757208585739136),
 ('サービス残業', 0.8720364570617676),
 ('サビ残', 0.7500427961349487),
 ('残業代', 0.6272672414779663),
 ('残響', 0.6267948746681213),
 ('休日出勤', 0.5998174548149109),
 ('長時間労働', 0.5923150777816772),
 ('作業量', 0.5819833278656006),
 ('超勤', 0.5778118371963501),
 ('残業手当', 0.5598958730697632)]
```

似た単語が並んでいますね…！

「サビ残」といった略称や、「残響」というタイプミスまで拾っていてすごいです。
コンピューターが「残業」という概念を理解した！と思って感慨深い瞬間でした。

ユーザーさんには前向きな転職をしてほしいと日々思っているので、
前向きな単語の確認もやっていきましょう。

```py3
>> model.most_similar(positive=['やりがい'])
[('甲斐', 0.9375230073928833),
 ('醍醐味', 0.7799979448318481),
 ('面白い', 0.7788150310516357),
 ('おもしろい', 0.7710426449775696),
 ('楽しい', 0.712959885597229),
 ('生きがい', 0.6919904351234436),
 ('面白み', 0.6607719659805298),
 ('喜び', 0.6537446975708008),
 ('誇り', 0.6432669162750244),
 ('つまらない', 0.6373245120048523)]
```

最後に気になる感じの単語がありますがいい感じですね。
単語そのものの理解は出来ているようなので次に単語の加減算を行います。

### 単語の加減算
転職クチコミを見ていると、女性の働きやすさに関わる内容などが人気です。
「独身女性 - 女性 + 男性 = ?」などをやってみましょう。

まずは基本的な単語理解の確認。

```py3
# 女性に似ている単語
>> model.most_similar(positive=['女性'])
[('女性社員', 0.8745297789573669),
 ('働く女性', 0.697405219078064),
 ('独身女性', 0.6827554106712341),
 ('じょせい', 0.5963315963745117)]

# 男性に似ている単語
>> model.most_similar(positive=['男性'])
[('管理職', 0.7058243751525879),
 ('活躍', 0.6625881195068359),
 ('特別扱い', 0.6411184668540955),
 ('優遇', 0.5910355448722839)]

# 独身女性に似ている単語
>> model.most_similar(positive=['独身女性'])
[('女性社員', 0.7283456325531006),
 ('シングルマザー', 0.6969124674797058),
 ('未婚', 0.6945561170578003),
 ('女性', 0.6827554106712341)]
```

大丈夫そうなので加減算の実行。

```py3
# 独身女性 - 女性 + 男性 = ?
# model.most_similar(positive=[足す単語], negative=[引く単語])
>> model.most_similar(positive=['独身女性', '男性'], negative=['女性'])
[('未婚', 0.665600597858429),
 ('管理職', 0.6068357825279236),
 ('子持ち', 0.58555006980896),
 ('男子', 0.530462384223938),
 ('特別扱い', 0.5190619230270386)]
```

若干怪しい結果ながらも、「独身女性」から「女性社員」や「シングルマザー」といった女性的な単語がなくなり、「未婚」が一番似ている単語として推測されました。

word2vecがそれなりに正しく出来てそうなので、
このまま会社の分類へと進んでいきます。

## 3-2. doc2vecで会社同士の類似度計算
次に、会社同士の関係性を調べてみましょう。

まずは系列会社がたくさんある簡単な企業で確認します。

```py3
# model.docvecs.most_similar(positive=[ベースの会社のID])
# ID 53 : リクルートホールディングス
>> model.docvecs.most_similar(positive=[53])
[('株式会社リクルートライフスタイル', 0.9008421301841736),
 ('株式会社リクルートジョブズ', 0.8883105516433716),
 ('株式会社リクルートキャリア', 0.8839867115020752),
 ('株式会社リクルート住まいカンパニー', 0.8076469898223877),
 ('株式会社リクルートコミュニケーションズ', 0.7945607900619507),
 ('株式会社キャリアデザインセンター', 0.7822821140289307),
 ('エン・ジャパン株式会社', 0.782017707824707),
 ('株式会社リクルートマーケティングパートナーズ', 0.7807818651199341),
 ('株式会社サイバーエージェント', 0.7434782385826111),
 ('株式会社クイック', 0.7397039532661438)]
```

簡単すぎたようですが、リクルートホールディングスさんに似ている会社として、
リクルート系列各社が出てきました。

大丈夫そうなので、一般的な会社も見てみましょう。

```py3
# ID 1338 : DeNA
>> model.docvecs.most_similar(positive=[1338])
[('グリー株式会社', 0.8263522386550903),
 ('株式会社サイバーエージェント', 0.8176108598709106),
 ('株式会社ドリコム', 0.7977319955825806),
 ('株式会社Ｓｐｅｅｅ', 0.787316083908081),
 ('株式会社サイバード', 0.7823044061660767),
 ('株式会社ドワンゴ', 0.767551064491272),
 ('ヤフー株式会社', 0.7610974907875061),
 ('ＫＬａｂ株式会社', 0.7593647837638855),
 ('株式会社ｇｌｏｏｐｓ', 0.7475718855857849),
 ('ＮＨＮ\u3000ｃｏｍｉｃｏ株式会社', 0.7439380288124084)]
```

最近話題になっていたDeNAさんですが、グリーさんと似ていると出てきました。
ゲーム関係と判定されたようで、その他にもサイバーさんやドリコムさんも出てきました。

なかなか良さそうです。
Web系の会社ばかりだと結果が偏る可能性があるので、
ぜんぜん違う会社も見ておきます。

```py3
# ID 862 : ホンダ
>> model.docvecs.most_similar(positive=[862])
[('トヨタ自動車株式会社', 0.860333263874054),
 ('マツダ株式会社', 0.843244194984436),
 ('株式会社デンソー', 0.8296780586242676),
 ('富士重工業株式会社', 0.8261093497276306),
 ('日野自動車株式会社', 0.8115691542625427),
 ('日産自動車株式会社', 0.8105560541152954),
 ('ダイハツ工業株式会社', 0.8088374137878418),
 ('アイシン精機株式会社', 0.8074800372123718),
 ('株式会社本田技術研究所', 0.7952905893325806),
 ('株式会社豊田自動織機', 0.7946352362632751)]

# ID 38 : ソニー
>> model.docvecs.most_similar(positive=[38])
[('パナソニック株式会社', 0.8186650276184082),
 ('株式会社東芝', 0.7851587533950806),
 ('オムロン株式会社', 0.7402874231338501),
 ('日本電気株式会社', 0.7391767501831055),
 ('株式会社ニコン', 0.7331269383430481),
 ('ソニーグローバルマニュファクチャリング＆オペレーションズ株式会社', 0.7183523178100586),
 ('太陽誘電株式会社', 0.7149790525436401),
 ('シャープ株式会社', 0.7115868330001831),
 ('パイオニア株式会社', 0.7104746103286743),
 ('キヤノン株式会社', 0.7103182077407837)]

# ID 1688 : マッキンゼー(コンサルティング・ファーム)
>> model.docvecs.most_similar(positive=[1688])
[('アクセンチュア株式会社', 0.7885801196098328),
 ('株式会社ボストン・コンサルティング・グループ', 0.7835338115692139),
 ('ゴールドマン・サックス証券株式会社', 0.7507193088531494),
 ('デロイトトーマツコンサルティング合同会社', 0.7278151512145996),
 ('株式会社シグマクシス', 0.6909163594245911),
 ('ＰｗＣアドバイザリー合同会社', 0.6522221565246582),
 ('株式会社リンクアンドモチベーション', 0.6289964914321899),
 ('モルガン・スタンレーＭＵＦＧ証券株式会社', 0.6283067464828491),
 ('ＥＹアドバイザリー株式会社', 0.6275663375854492),
 ('アビームコンサルティング株式会社', 0.6181442737579346)]
```

おおむね大丈夫そうですね。

このように会社間の類似度が計算できる(=距離が計算できる)ため、
以下のような分析が簡単に行なえます。

- K-means等のクラスタリング手法を用いて会社をカテゴリ分け
- 多次元尺度法などの手法を用いて会社の分布を可視化

上記のような処理は scikit-learn でとても簡単に実装できます。

今回、実際に多次元尺度法で分布の可視化も行ってみましたが、
その内容も書くと本稿がとても長くなってしまうため、
また別の機会にでもご紹介できればと思います。

## 3-3. doc2vecで会社同士の加減算
doc2vecはword2vecと同様に文書同士の加減算が出来ます。
とりあえずやってみましょう。

先述の通り、リクルートホールディングスさんに似ている企業はリクルート各社でした。

```py3
# ID 53: リクルートホールディングスに似ている企業 (再掲)
>> model.docvecs.most_similar(positive=[53])
[('株式会社リクルートライフスタイル', 0.9008421301841736),
 ('株式会社リクルートジョブズ', 0.8883105516433716),
 ('株式会社リクルートキャリア', 0.8839867115020752),
 ('株式会社リクルート住まいカンパニー', 0.8076469898223877),
 ('株式会社リクルートコミュニケーションズ', 0.7945607900619507),
 ('株式会社キャリアデザインセンター', 0.7822821140289307),
 ('エン・ジャパン株式会社', 0.782017707824707),
 ('株式会社リクルートマーケティングパートナーズ', 0.7807818651199341),
 ('株式会社サイバーエージェント', 0.7434782385826111),
 ('株式会社クイック', 0.7397039532661438)]
```

ここで、「転職情報DODA」「アルバイト情報an」などを運営する
人材系大手企業のインテリジェンスさんを足してみましょう。

```py3
# model.docvecs.most_similar(positive=[ベースの会社のID, 複数入れると足す])
# 「ID 53: リクルートホールディングス」 + 「ID 110: インテリジェンス」 = ？
>> model.docvecs.most_similar(positive=[53, 110])
[('株式会社リクルートキャリア', 0.888693630695343),
 ('株式会社リクルートジョブズ', 0.865821123123169),
 ('株式会社リクルートライフスタイル', 0.8580507636070251),
 ('株式会社キャリアデザインセンター', 0.8396339416503906),
 ('エン・ジャパン株式会社', 0.8285592794418335),
 ('株式会社マイナビ', 0.7874248027801514),
 ('株式会社クイック', 0.777060866355896),
 ('株式会社リクルート住まいカンパニー', 0.775804877281189),
 ('株式会社サイバーエージェント', 0.7625365257263184),
 ('株式会社ネオキャリア', 0.758436381816864)]
```

上記結果は、以下のような考察ができます。

* リクルート系企業では人材系の2社が上位に上がってきた
    * リクルートキャリアは「リクナビ」「リクナビNEXT」運営
    * リクルートジョブズは「タウンワーク」「とらばーゆ」などを運営
* 人材系以外のリクルート系列会社を押しのけて人材系の会社が上位に上がってきた
    * キャリアデザインセンターは＠typeなどを運営
    * エン・ジャパンはエン転職を運営

だいぶ分かりやすい例を恣意的に出した感じはありますが、
それなりに上手く行っていると判断しても大丈夫そうです。

## まとめと今後の課題
今回の記事では転職会議のクチコミを用いて以下の内容を実現しました。

- word2vecにより、クチコミに登場する単語の概念を機械に理解させ、単語の類似度計算や加減算を行った
- doc2vecにより、会社の概要を機械に理解させ、会社の (以下同文

今後は 「単語 + 単語 => 似ている会社」 (例： やりがい + 成長 => リブセンス) のような計算も可能にして、ユーザーさんの好む社風の会社や[求人](https://career.jobtalk.jp/)を検索できるような技術にもトライしていきたいと思っています。

ただ現状では1点、致命的な課題があるので最後に簡単にご紹介します。
以下に、分かりやすい実例を示します。

```py3
# 「ブラック」に似ている単語は？
>> model.most_similar(positive=['ブラック'])
[('ブラック企業', 0.8150135278701782),
 ('ホワイト', 0.7779906392097473),
 ('ホワイト企業', 0.6732245683670044),
 ('ブラック会社', 0.5990744829177856),
 ('ブラックブラック', 0.5734715461730957),
 ('名高い', 0.563334584236145),
 ('クリーン', 0.5561092495918274),
 ('グレー', 0.5449624061584473),
 ('不夜城', 0.5446360111236572),
 ('宗教団体', 0.5327660441398621)]
```

この例に示すように、今回ご紹介した簡易的な方法では「ブラック」「ホワイト」を似た単語として認識してしまっています。

同じような文脈で使われる言葉を同じと捉えてしまって、その極性判定が出来ていない状態になっており、機械は以下のような認識をしていると思われます。

- 何か残業の話をしているのはわかるけど
- 残業が多いか少ないかはよくわかんない

実は「単語 + 単語 => 似ている会社」の結果を出力するために、gensimライブラリに手を加えて拡張実装も行っていました。

しかし、このような課題があり結果の正確性が保証できないなかで発表するのはあまりに不誠実だと考え、今回はご紹介しないでおきました。(「リブセンス + 残業 + つらい => ？？」みたいなハードな例を準備していました…！)

この課題感は日本語に関わらず存在しており、世界では色々な研究が進んでいるようです。その中でも、係り受けを考慮したうえでword2vecを学習させるとかなり結果が変わるという研究([参考文献](https://levyomer.wordpress.com/2014/04/25/dependency-based-word-embeddings/))もあるらしく、今後、そのような取り組みを行っていければと考えています。