Poincaré Embeddings による職種の類似度計算とその利用

scouty アルゴリズムエンジニアの高濱です。外部への情報発信はこの記事が最初なのでこの場を借りて自己紹介させていただきますが、私は scouty 代表の島田、リードエンジニアの伊藤と京都大学工学部情報学科での同期で、京都大学大学院情報学研究科鹿島研究室で修士課程を修了した後、株式会社リクルートホールディングスを経て scouty に入社しました。代表的な著作物は [Takahama et al., 2018]*1, [Takahama et al., 2016]*2, [Takahama et al., 2014]*3 などです。よろしくお願いします。

さて、本記事では、 Poincaré Embeddings*4 を用いた職種の関係の埋め込みに関してご紹介します。第一段階として、サービス適用の実現性を測る目的での実験を行ったので、その実験の詳細と実験結果について記したいと思います。

なお、本記事で紹介する内容は scouty インターンの山田くんに実装・実験してもらった結果をまとめたものです。

背景

職種間の関連度

scouty では、企業と候補者のマッチングや退職率予測を行うにあたり、候補者の職種情報を利用しています。 2つの職種(を表現する文字列)を考えたとき、職種間の関係には、 エンジニアEngineer のように等価なものと、 エンジニアインフラエンジニア のように包含関係(あるいは階層構造)にあるものなどがあり、これらをうまく扱える何らかの表現を利用したいと考えていました。

そこで今回、包含関係のデータから、適切な埋め込みを行い、例えば エンジニアインフラエンジニア意味的な距離を求める目的で、 Poincaré Embeddings の応用を検討しました。 乱暴に言うと、 Poincaré Embeddings を用いることで、職種間に定義された包含関係のみから、任意の職種間の意味的な距離を計算できるようになることを期待して検証を行います。

Poincaré Embeddings とその実装

Poincaré Embeddings は端的に言うと word2vec の埋め込み先をユークリッド空間ではなく双曲空間にするという手法で、階層構造やべき分布をもつデータを埋め込むという問題設定において、低次元でもよい表現を与えられるという特徴があります。また、実装が容易で、内積の計算式にちょっとした補正を加えることで利用することができます。 Poincaré Embeddings は2017年中頃に arXiv に投稿され、以上のような素晴らしい性質から非常に話題になりました。各所で参照されているので今更感はありますが、以下の記事でわかりやすく解説されています。

異空間への埋め込み!Poincare Embeddingsが拓く表現学習の新展開

ABEJA Tech Blog のNIPS2017参加報告にもある通り、NIPS2017にも Spotlight で採択されました*5。私見ですが、 Poincaré Embeddings に限らず、非ユークリッド幾何学を応用して機械学習手法を再考する試みは近年のトレンドのようで、NIPS2017でこれに関連する分野のチュートリアルも行われています*6

FAIR による公式の PyTorch での実装*7も公開されましたが、今回は Gensim を用いて実験を行いました。なお、Gensim の API, Exemple に関するドキュメントは以下になります。

gensim: topic modelling for humans

Poincaré Embeddings のAPI

Gensim で提供されている Poincaré Embeddings のAPIは以下のようになっています。

class gensim.models.poincare.PoincareModel(train_data,
                                           size=50,
                                           alpha=0.1,
                                           negative=10,
                                           workers=1,
                                           epsilon=1e-05,
                                           regularization_coeff=1.0,
                                           burn_in=10,
                                           burn_in_alpha=0.01,
                                           init_range=(-0.001, 0.001),
                                           dtype=<type 'numpy.float64'>,
                                           seed=0)

これら引数のうち、特に本記事と関連が深いものの定義を以下に挙げます。

  • train_data (iterable of (str, str)) – Iterable of relations, e.g. a list of tuples, or a PoincareRelations instance streaming from a file. Note that the relations are treated as ordered pairs, i.e. a relation (a, b) does not imply the opposite relation (b, a). In case the relations are symmetric, the data should contain both relations (a, b) and (b, a).
  • size (int, optional) – Number of dimensions of the trained model.
  • negative (int, optional) – Number of negative samples to use.

データセットの作成

本記事の実験で用意した、 gensim.models.poincare.PoincareModel に与える学習データ train_data の構造について述べます。 train_data は、関係を表すタプル (a, b) iterable です。 オブジェクト a がオブジェクト b に含まれる場合( ba の意味的な下位に該当する場合)に (a, b) というタプルで ab の関係を表現します。

scouty のデータベースにある職種のデータのうち、特に使用頻度の高いものを選び、関係がある職種のペアのリストを作成します。 「 ab に含まれる」という関係をもつ職種のペアには、例えば以下のようなものがあります。

  • Software EngineerEngineer に含まれる
  • Senior Software EngineerSoftware Engineer に含まれる
  • Web ProgrammerProgrammer に含まれる
  • UI DesignerDesigner に含まれる

また、 エンジニアEngineer のように、「 ab は等価」という関係をもつ職種のペアもあります。こういったペアの場合は、

  • エンジニアEngineer に含まれる
  • Engineerエンジニア に含まれる

という2つの包含関係のタプルに分解します。

上記の関係は、 Python では以下のように表現されることになります。

train_data = [
    ('Software Engineer', 'Engineer'),
    ('Senior Software Engineer', 'Software Engineer'),
    ('Web Programmer', 'Programmer'),
    ('UI Designer', 'Designer'),
    ('エンジニア', 'Engineer'),
    ('Engineer', 'エンジニア'),
]

実験

以下のコードは、 Jupyter Notebook 上で実行を行うためのものです。

最初に、必要なライブラリの import を行います。 今回はCSVファイルを読み込むために pandas を、可視化のために plotly を使用します。

from gensim.models.poincare import PoincareModel
from gensim.viz.poincare import poincare_2d_visualization
from IPython import display
from plotly.offline import init_notebook_mode, iplot
import pandas as pd

init_notebook_mode(connected=True)

続いて、データセットのロードと整形を行います。ここでは、前節に記したような訓練データが path/to/occupation_relations.csv に配置されているとします。 pandas でCSVファイルを読み込み、1つ目の要素が下位概念、2つ目の要素が上位概念になるようなタプルのリスト occupation_relations_list を定義します。

occupation_relations_list = [(a, b) for a, b in pd.read_csv('path/to/occupation_relations.csv', header=None).as_matrix()]

次に、 occupation_relations_list をモデルに与えて学習を行います。 PoincareModel の引数では、 size で分散表現の次元数、 negative で negative sampling の際に使用するデータ数をそれぞれ指定します。 本記事では、可視化を可能にするために二次元空間に埋め込みを行います (size=2)。また negative=8 とします。 学習は、メモリ16GBの2.3GHzデュアルコアIntelCore i5 プロセッサのCPUを搭載したMacBook Pro を用いたとき、500件程度の relation を含むサイズのデータセットに対して1分足らずで終了しました。

model = PoincareModel(occupation_relations_list, size=2, negative=8)
model.train(epochs=500)

最後に、学習の結果として実際にどのような埋め込みが行われるかを、 Gensim の poincare_2d_visualization を利用することで確認します。可視化は plotly を用いて行います。 poincare_2d_visualizationAPIは以下のようになっています。

gensim.viz.poincare.poincare_2d_visualization(model, tree, figure_title, num_nodes=50, show_node_labels=())

各引数で特に説明が必要なものは以下の通りです:

  • model (PoincareModel): 学習された model をそのまま渡します。
  • tree (set): 学習データで張られている relation の情報を渡します。(なぜか) set 型のみしか受け付けないので、本記事で言うところの occupation_relation_listset 型に変換します。
  • num_nodes (int or None): tree で渡した relation のうち、いくつのエッジを可視化するかを指定します。例えば num_node=10 とすると、エッジが10本のみ表示されます。 num_node=None とすると、全てのリレーションを可視化できます。
  • show_node_labels (iterable): 学習に用いたラベルのうち、可視化するもののリストを渡します。本記事では、500件以上の職種をデータセットとして与えたので、その中の代表的な職種のみを可視化するために、可視化するラベルのリスト major_occupation_list を指定します。
relations_set = set(occupation_relations_list)
# 代表的な職種のみをラベルとして可視化する
major_occupation_list = ['Engineer', 'Designer', 'Web', '取締役', 'Freelance', 'Intern', 'Programmer', 'Founder', 'Director', 'Marketer', 'Software Engineer']
figure_title = ''
iplot(poincare_2d_visualization(model, relations_set, figure_title, num_nodes=None, show_node_labels=major_occupation_list))

このコードを実行することによって、2次元の embedding を行った結果が以下のように可視化されます。

Poincaré Embeddings では、原点に近づけば近づくほど抽象的な概念であるといえますが、 Engineer取締役 といった、他の様々な職種を包含するような職種は比較的中心に近いことが確認できます。 また CEO のような特定の職種は他の職種をあまり包含しないので、中心から遠いところにプロットされており、これも妥当な結果であると言えると思います。 一方、(空間が歪んでいるので正しいか怪しいところですが) Software EngineerEngineer よりも Software Engineer取締役 の方が近く見えたりするなど、それぞれの職種の位置関係に関しては直感的に不自然な部分も見当たるため、もう少しデータの作り方や学習のチューニングを改善する余地が残されていると言えます。

おわりに

今回の記事では、 Poincaré Embeddings を利用することで、職種の類似度を計算するための(主観的に、概ね)尤もらしい embedding が得られることを確認しました。

現在、職種の類似度計算の結果を scouty のサービスへ適用する準備を進めています。具体的には、

  • 職種ごとに同様の退職傾向が見られることが確認されているため、等価あるいは近い職種に就いている人をうまく丸めることで、退職率予測の精度を高める
  • scouty を利用して候補者の職種を考慮した検索を行うとき、「ある職種にある程度近い職種の候補者を探す」といったような検索を行えるようにする

といった応用を検討しています。

また、 RubyRails, セールス法人営業 といった単語に関するデータセットを作成することで、職種ではなくスキルに関する embedding を作成することもできます。 これも、検索の際に「 Rails にある程度近いスキルをもった候補者」といった条件を指定できるようにするなどの応用が可能であり、今後取り組んでいきたいと考えています。

参考文献・脚注

*1: Ryusuke Takahama, Yukino Baba, Nobuyuki Shimizu, Sumio Fujita, Hisashi Kashima, "AdaFlock: Adaptive Feature Discovery for Human-in-the-loop Predictive Modeling", The 23nd AAAI Conference on Artificial Intelligence (AAAI-18)

*2: Ryusuke Takahama, Toshihiro Kamishima, Hisashi Kashima, "Progressive Comparison for Ranking Estimation", https://www.ijcai.org/Proceedings/16/Papers/546.pdf, The 25th International Joint Conference on Artificial Intelligence (IJCAI-16)

*3: Ryusuke Takahama, Naoki Otani, Sho Yokoi, Tomohiro Arai, Nozomi Nori, Norie Ugai, Koji Nakazawa, Hisashi Kashima, "私たちはお土産にどの八ッ橋を買えばよいのか", http://www.ml.ist.i.kyoto-u.ac.jp/wp/wp-content/uploads/2014/10/yatsuhashi.pdf

*4: Maximillian Nickel and Douwe Kiela, “Poincaré Embeddings for Learning Hierarchical Representations”, https://arxiv.org/abs/1705.08039

*5: Maximillian Nickel and Douwe Kiela, “Poincaré Embeddings for Learning Hierarchical Representations”, https://nips.cc/Conferences/2017/Schedule?showEvent=10034

*6: Michael Bronstein, Joan Bruna, Arthur Szlam, Xavier Bresson and Yann LeCun, “Geometric Deep Learning on Graphs and Manifolds”, https://nips.cc/Conferences/2017/Schedule?showEvent=8735

*7: https://github.com/facebookresearch/poincare-embeddings