astamuse Lab

astamuse Labとは、アスタミューゼのエンジニアとデザイナーのブログです。アスタミューゼの事業・サービスを支えている知識と舞台裏の今を発信しています。

名寄せの仕組み

この記事は 自然言語処理 Advent Calendar 2020 の25日目の記事です。

こんにちは、rinoguchi です。今年の4月に こちらの記事 を書いて以来、半年ぶりの投稿になります。
当社では、特許・研究課題・論文など多くの知的財産データを保持しています。これらのデータを活用するには、データに含まれる同一組織・同一人物に対して同一IDを付与してデータをグルーピングすることが必要であり、この作業のことを名寄せと呼んでいます。
今回はこの名寄せの仕組みについて紹介したいと思います。

大まかな処理フロー

当社では名寄せ処理を、まずそれぞれのデータソース(例えば特許や論文など)の中で実行し、次に異なるデータソース間で実行することで、最終的に組織ID・人物IDに対して特許・研究課題・論文などを紐づけたデータを生成しています。
とはいえ、データソース内名寄せもデータソース間名寄せも仕組みとしては同じで、以下の4つのステージで構成されています。

f:id:astamuse:20201222215021p:plain

ここからは4つのステージについて、特定のデータソース内の人物名寄せを行うイメージで、処理内容を説明していきます。

1. クレンジング

クレンジングステージでは、生データに含まれる組織名や人物名を綺麗にして、プログラムで比較可能な状態にしていきます。

まずは、以下のような形で生データを候補データとして取り込みます。候補IDは、例えば特許の場合「出願番号×人物名」の組合せに対して一意になるように採番した値です。

候補ID 生データID 人物名(英) 人物名(ローカル)
c01 r01 TAKAHASHI ICHIRO 高橋 一郎
c11 r01 Yamada Jiro 山田 二郎
c02 r02 Pf. TAKAHASHI ICHIRO 高梁 一郎
c03 r03 Takahasi Ichiro
c04 r04 髙橋一郎

クレンジングステージでは、例えば以下のような変換処理を地道に行っていきます。

  • 名称の単純正規化(半角化、大文字化、不要記号などの除去)
  • 不要文字除去(部署名、役職名など)
  • 日英判断(日本語名称欄に英語が入っていたら英語名称欄に移動)
  • 姓名分割(わかち書き)
  • 英語翻訳(社内で保持している辞書およびGoogle Translate API)
  • エイリアス変換(組織のみ。社内マスタおよび公開データを利用)

クレンジング後のデータは以下のような感じになりました。ここまでくれば、人物名(英)を元に同一人物の可能性が高い候補IDをグルーピングできそうです。

候補ID 生データID 人物名(英) 人物名(日) 処理内容
c01 r01 TAKAHASHI ICHIRO 高橋 一郎 -
c11 r01 YAMADA JIRO 山田 二郎 大文字化
c02 r02 TAKAHASHI ICHIRO 高梁 一郎 不要文字除去
c03 r03 TAKAHASI ICHIRO 日/英判断、半角化
c04 r04 TAKAHASHI ICHIRO 髙橋 一郎 姓名分割、翻訳

2. メタ情報追加

クレンジングステージで名称が綺麗になったら、次は候補IDに対してメタ情報を追加していきます。
メタ情報は様々なデータソース(特許や論文など)を同じデータモデルで扱えるように、ある程度抽象化した名前(開始日、タイトルなど)にしてあります。

単純追加

生データに紐づく属性情報は、生データIDをキーに検索・抽出して候補IDに付加します。
例えば、共同研究者や組織名はそれだけである程度同一と判断できる非常に重要なメタ情報ですし、開始日もあまりに離れている場合は別人の可能性が高まりますので重要です。属性情報自体は数多くあるのでその中から重要そうなものをピックアップして取り込んでいきます。

候補ID 生データID 人物名(英) 共同研究者 組織名 開始日 タイトル ...
c01 r01 TAKAHASHI ICHIRO [YAMADA JIRO] A会社 2020-01-01 〇〇に関する研究 ...
c11 r01 YAMADA JIRO [TAKAHASHI ICHIRO] B会社 2020-01-01 〇〇に関する研究 ...
c02 r02 TAKAHASHI ICHIRO [ ] A会社 2019-01-01 ××技術について ...
c03 r03 TAKAHASI ICHIRO [YAMADA JIRO] A会社 2008-01-01 △△について ...
c04 r04 TAKAHASHI ICHIRO [ ] C会社 1995-01-01 □□について ...

タイトル抽出・ベクトル化

タイトルや本文も人間が判断する時にはかなり参考になりますが、そのままでは機械的には比較できませんので、タイトルはキーワード抽出、本文はベクトル化しました。

  • 事前処理(小文字化、stopword除去、ステミング、単語分割)
  • doc2bowを使ってBoW化(コーパス化)
  • TF-IDF(gensimのTfidfModel)で、単語の特徴度を計算
    • => 特徴度が一定以上の単語をタイトルキーワードとする
  • LSI(gensimのLsiModel)で、300次元に圧縮
    • => 本文ベクトルとする

自分は自然言語処理に詳しくないのですが、社内の自然言語処理の専門家である じんさん に相談したところ、特許や論文などのデータは特徴的な単語が使われていることが多く、それだけでかなり同一性を判断できるので、TF-IDFを用いるのは理にかなってるというアドバイスをもらいました。
実際のデータで検証したところ、タイトルキーワードや本文ベクトルから同一性が高いと判断したものは、人間の判断をかなりトレースできていることも分かりました。

少しシステム的なことを言及すると、このTF-IDFおよびLSIを用いたベクトル化の処理は、何も考えずに数億件のデータに対して適用すると計算量が大きくなりすぎますので、システム的には以下の2点を工夫をしました。

  1. 同一可能性が高いデータをグルーピングして、そのグループの中でベクトル化する
    • イメージ的には「人物名(英)のイニシャルが同じ」ものをグルーピングする感じ
    • Nが小さすぎるとベクトルが偏るので、グループを足し合わせて N=50000 ぐらいになるように調整
  2. グループ毎のベクトル化の処理を、Apache Spark 環境のフルマネージドサービスであるDataproc上で並列実行する

これにより、1億件を数日で処理できるようになりました。

3. 名寄せ

名寄せステージが全体の本丸的な位置付けなのですが、同一の可能性が高い候補同士をつなぎ合わせる処理を行います。

f:id:astamuse:20201223092642p:plain

同一の可能性がある候補同士を連結(組合せを列挙)

これは、同一の可能性がある候補IDの組合せを抽出するという処理になります。後続の処理でこの組合せ一つ一つに対してスコアリングを行うのですが、その際の計算量を抑えるための処理になります。
やりたいことは「人物名(英)の編集距離が閾値以下 or イニシャルが一致」するような候補IDを組合せを抽出するだけなのですが、この組合せを抽出する処理自体がかなりの計算量になります。
n個の候補IDから2つを取り出す組合せの数の計算式は  _nC_2 なので、例えば1億件の候補IDが存在する場合、1億 × (1億 - 1) / 2= 約5000兆 回の計算(比較処理)が必要になります。
この計算量を現実的な時間で終わらせるため、大きく3つ試してみました。

  • BigQuery上でSQLで比較する【不採用】
    • => 1億件×1億件のcross join。CPUがオーバーフロー
  • グラフDB(neo4j)に突っ込んで、条件に一致するサブグラフを抽出する【不採用】
    • => 試しに15万ノード3億エッジぐらいで試したが応答なし
  • Dataprocを利用し、pysparkで分散処理【採用】
    • => 現実的な時間で結果が返る

ここでも結局、分散処理を導入して解決しました。
結果として、一番データ件数が多い特許のケースで、同一の可能性がある組合せの件数は数百億件になりました。

候補の組合せ(エッジ)に対してスコアリング

候補の組合せを列挙できたので、次は各組合せのスコア(同一可能性)を数値化していきます。

この部分は、データソースによっても利用できるメタ情報が異なり、後からメタ情報を追加するケースもありそうなので、比較ルール(使用するメタ情報と比較ロジックのセット)をプラグインのように差し込めるように設計しました。

ここで比較ルールは例えば以下のようなもので、これらの比較ルールは互いに依存関係はなく独立して計算可能にしてあります。

  • 比較ルール1: 本文ベクトルの類似度
  • 比較ルール2: 共同研究者の一致
  • 比較ルール3: 開始日の近さ

それぞれの比較ルールの処理結果が出たら、最後にこれらの数値を元に総合的なトータルスコアを算出します。

候補ID
FROM
候補ID
TO
比較ルール1
スコア
比較ルール2
スコア
... トータルスコア
c01 c02 0.51 0.45 ... 0.57
c01 c03 0.90 0.58 ... 0.67
c02 c03 0.20 0.49 ... 0.25

スコアを元に候補(ノード)を繋げてグルーピング

候補の組合せに対して同一可能性(=トータルスコア)を数値化することができたので、スコアを元に候補を繋いでいく処理(名寄せ)を行います。

この候補の繋がりは、グラフ理論における重み付きの無向グラフと考えると以下のように可視化することができます。候補がノード、候補組合せがエッジ、トータルスコアが重みになります。

f:id:astamuse:20201226185948p:plain

エッジを特定条件で繋いでいく処理は、グラフDBでサブグラフを抽出する処理にあたるのでグラフDBを導入しようかとも思ったのですが、pythonで実装しても数時間程度で完了したので、pythonで実装しました。

4. 名分け

当初「3. 名寄せ」で処理終了の予定だったのですが、実際にやってみると明らかに別人が紐づけられているケースがあることが分かりました。

例えば、以下のようなケースではc01とc02は漢字表記が異なり、c03は漢字表記がありません。

候補ID 人物名(英) 人物名(日) メタ情報
c01 TAKAHASHI ICHIRO 高橋 一郎 ...
c02 TAKAHASHI ICHIRO 高梁 一郎 ...
c03 TAKAHASHI ICHIRO ...

このケースでは、c01-c02 は別人と判断できますが、c01-c03c02-c03 はc03側に漢字表記が存在しないためその他のメタ情報だけで判断することになり、結果として同一と判断されることがあります。そうなると、c03を経由して c01-c03-c02 が同一人物として繋がってしまうようなことが発生します。

というわけで、明らかに別人同士が繋がっている場合にそれを別IDに分ける名分け処理が必要になります。

この名分け処理も、グラフ理論のグラフカット問題として捉えると取り組みやすかったです(これも前述のじんさんに教えてもらいました。感謝!)。
具体的にはトータルスコアが閾値以下のエッジの両端のノードを別グラフに分ける処理になり、最大フロー最小カット定理を導入して、カットするエッジの重みが最小になるように最小カットをしてあげることで実現できました。

f:id:astamuse:20201226192616p:plain

ちなみに、この処理pythonで実装してみたところ異常に時間がかかったので、C++ で実装し直しました。pybind11を使うと C++ で実装した関数をpythonから呼び出せるので便利です。

最終的にグラフ上で繋がっている候補IDに対して同一の人物IDや組織IDを採番することで名寄せ完了になります。

さいごに

今回、当社の名寄せの仕組みについて紹介してみました。説明の都合上実態よりシンプルに書いてはありますが、実際に動いている仕組みなので、ある程度は参考になるんじゃないかと思っています。

サンプル母集団での精度は、F値で0.90〜0.99とデータソースによって異なるものの当初の目標値(F値0.9)は達成できました。とはいえ、教師データを作りきれてない本番データではこれを下回る可能性が高く、今後も改善を続けていく必要があります。

弊社は、一緒にデータ整備を手伝ってくれる方を募集中です!自分も入社してすぐ名寄せという結構コアな領域を任せてもらえたぐらい人手が全く足りてない状況ですので、興味がある方はぜひ以下よりご応募ください!心よりお待ちしてます。

recruit.astamuse.co.jp

[参考] 機械学習を導入してない理由

今回の名寄せの処理は、計算量の問題を解決できれば普通に機械学習にとてもマッチするように思えます。実際、導入するかかなり悩みました。しかし、今回は以下の理由から導入を見送りました。

  • ブラックボックスになってしまう
    • データを利用するお客様に対して、データがそうなっている理由を明確な説明できなくなる
  • 教師データ作成が非常に困難で教師データを100%信じられない
    • 人間が判断しても人によって同一人物かどうかの判断に迷うケースは多く、特に同姓同名に対する候補IDが100件を超えだすと普通に見落としも多くなり不正な教師データができてしまう
  • 明確に別人扱いにしたいケースもある

とはいえ、部分的に機械学習を導入することでより正解に近づける可能性もあり、今後導入する可能性もあると思っています。

データ分析ことはじめ 〜はじめてのデータ分析やってみたよ〜

f:id:astamuse:20201217033839j:plain

ご挨拶

どうもお久しぶりです、gucciです。
気づけばもう12月も半ば…今年は本当に色々なことがあった年でしたね。

新型コロナによってこれまでの日常が一変し、当たり前だったものが当たり前でなくなるという本当に大変な年だったと思います。

私の好きなプロ野球ももちろん多大な影響を受けて、シーズンの開始が遅れ、無観客試合や手拍子での応援など、これまでとは異なるシーズンでしたが、何はともあれ無事にシーズンが終わることができたのは本当に素晴らしいことだと思います。

データエンジニアとしてやってきて

さて、そんな中で私はデータエンジニアになり2年が経ちました。
(入社して1年弱アプリエンジニア〜その後データエンジニアにジョブチェンジ)

弊社はとにかく様々なデータを集めています。
技術情報と他のデータを組み合わせたり、新しい分析手法を用いたり、独自の切り口で分類を行なったりすることで、弊社にしかできないソリューションを提供しております。

非常に優秀なデータアナリストがおりますので、分析するためのデータを我々データエンジニアが整備していくといった塩梅でございます。

これまでデータを色々な形で料理できるようにあれこれ設計・整備して参りましたが、おやおや、データを実際に使う側にも少し興味が湧いてきました。

そこで今回は、
データ分析ことはじめ 〜はじめてのデータ分析やってみたよ〜
というお題でお送りしたいと思います。

ちなみに、本当に初歩の初歩のことしかしないので、今回の内容はどちらかといえば非エンジニア向けのライトな内容です。
「データで分析っていってもまったく想像がつかない!」
「プログラミングでデータを使うと何ができるの?」
「野球が好き!」
という方におすすめです。

そうだ、野球のデータを使ってみよう

さて、データの分析をやってみようかなと思ったものの何を題材にして良いのかさっぱりわかりません。
悩み抜いた末に、日頃からこっそり集めていたプロ野球のデータを活用してみることにしました。
それでは簡単クッキング!!

使うもの

  • プロ野球のデータ: csvファイル
  • 実行環境: JupyterLab
  • データごにょごにょ: Pandas
  • 可視化ライブラリ: Plotly Express

プロ野球のデータ

いつもお世話になっているサイトがこちらの 「プロ野球データFREAK」さんです!!
こちらのサイトから「打者成績」「選手一覧」のデータを拝借させていただきました。
いつもありがとうございます。

実行環境

今回 python で分析を行なっていきます。
JupyterLab は Jupyter Notebook の進化版で、ブラウザ上で動作するプログラムの対話型実行環境です。
簡単に実行結果が確認でき、またグラフなどの描画も画面上でできるのでデータ分析には欠かせないツールと言えそうです。

データごにょごにょ

データ分析といえば Pandas (実はあまり使ったことがなかった)。
Dataframe という「表」でデータを扱うことができますので、とりあえずこいつでやっていきましょう。

可視化ライブラリ

今回はいわゆる有名な Matplotlib でなく Plotly Express というのを使っていきます。

plotly.com

弊社自慢の若手超優秀機械学習エンジニアのNくんが以前紹介されていたので、使ってみたくて選びました。
元々は Plotly というオープンソースのデータ分析・グラフ可視化ツールがあって、それのラッパーライブラリが Plotly Express だそうです。
より簡単にインタラクティブな描画ができるという優れモノです。
3Dやアニメーションも使えるみたいなのでなんだかとっても良さげ。
ただ、JupyterLab で描画すると最初は真っ白で何も表示されなくてビビります。
ちゃんとチュートリアルを読まないとですね。

plotly.com

給料泥棒を探せ

それでは、さっそくやっていきましょう。

  • 2020年の年俸
  • 2020年の成績

これらを組み合わせて可視化することで、給料泥棒お金を貰っている割にあまり結果を出せていないのは誰か、というのを調べちゃいましょう。
今回は打者の指標としてはOPSを用います。

OPS(オプス、オーピーエス)は On-base plus slugging の略であり、野球において打者を評価する指標の1つ。出塁率と長打率とを足し合わせた値である。

いっぱい塁に出たり長打を打てる人が打者として優れているよね、って指標です。
1を超えたら本当にすごい。

ここからは簡単にコードのご紹介!!(いい感じにパスとか汲んでくださいませ)

必要なライブラリの読み込み

import plotly.express as px
import glob
import pandas as pd
import re

CSVデータ読み込み

# 2020年の打者成績一覧を読み込み
df_all_batter_stat_2020 = pd.read_csv('{適切なパス}/Baseball/Batting/2020_stats.csv', sep = ',')

# 2020年の各球団の選手一覧を読み込み
# 「2020_{チーム名}.csv」で格納してある
dir_path = '{適切なパス}/Baseball/Player/'
all_files = glob.glob(dir_path + "2020*.csv")

# 一つ一つのデータを読み込んで dataframe に追加していく(その際チーム名も追加)
df_all_player = pd.DataFrame()
for filename in all_files:
    df = pd.read_csv(filename, sep=',')
    team_name = re.sub('\d+_', '', filename.split('/')[-1].replace('.csv', ''))
    df['チーム'] = team_name
    df_all_player = pd.concat([df_all_player, df]).reset_index(drop=True)

打者成績一覧と選手一覧とがっちゃんこ

df_all_batter_data_merged = \
    pd.merge(df_all_batter_stat_2020, df_all_player, how='inner', on=['選手名', 'チーム'])

'年俸(推定)' の文字列を'年俸(万円)'の数値に変換
OPSを算出して dataframe に追加

df_all_batter_data_merged['年俸(万円)'] = \
    df_all_batter_data_merged['年俸(推定)'].str.replace(',', '').str.replace('万円', '').astype(int)
df_all_batter_data_merged['OPS'] = \
    df_all_batter_data_merged['長打率'] + df_all_batter_data_merged['出塁率']

全選手表示すると大変なことになるので、
『規定打席を超えた人(372打席)または年俸が1億円より多い人』を抽出

df_all_batter_data_merged_over_10000 = \
    df_all_batter_data_merged[(df_all_batter_data_merged['年俸(万円)'] > 10000) | (df_all_batter_data_merged['打席数'] > 372)]

いざ、散布図を表示

fig = px.scatter(
    df_all_batter_data_merged_over_10000,
    x='OPS',
    y='年俸(万円)',
    text='選手名',
    color='チーム',
    size_max=30,
    height=500,
    width=800,
    size='本塁打',
    color_discrete_map={
        '巨人': '#F97709',
        '阪神': '#ffff00',
        '広島': '#FF0000',
        '中日': '#00008b',
        'DeNA': '#00bfff',
        'ヤクルト': '#7cfc00',
        'ソフトバンク': '#f9ca00',
        'オリックス': '#b08f32',
        '日本ハム': '#02518c',
        'ロッテ': '#221815',
        '西武': '#102961',
        '楽天': '#85010f'
    }
)

fig.show()

その結果がこちら↓↓

いかがでしょう!!
この描画されたグラフ、マウスホバーで情報がでたりドラッグでズームインとかできちゃうんです!!
すごいお便利。
ちなみに、円の大きさが本塁打の数を表しています。
本当はもっと大きく出したかったのですが、サイトのページサイズの関係でこの形になりました…。
こちらの画像の方が見やすいかもしれません。

f:id:astamuse:20201217023224p:plain
2020年俸/2020打者OPS

この散布図を見るだけで、

  • 図右上:期待通りゾーン
    • 柳田えぐい(一番年俸をもらっていて一番OPSが高い。すばらしい。)
  • 図左上:年俸の割にOPSが高くないゾーン
    • バレンティンがちょっと…年俸の割に…残念…
  • 図右下:年俸の割によくやっているゾーン
    • 村上くんは年俸の割にものすごいOPS

といったことが簡単にわかってしまいました。データを可視化するってのはすごいことですね。
これまでの貢献やOPS以外での活躍というのももちろんありますので一概には言えませんが、一つの見方としては面白いですね。
もうすっかり分析した気分になれちゃいます。

きちんとお給料もらってる?

さぁ乗ってきたところで、今度は活躍に応じてきちんと年俸が支払われているのかチェックしてみましょう。
前述の検証では、2020年の年俸と2020年の成績を使いましたが、
本来年俸というのは結果に対して「よう頑張った!!次も期待してるで!!」の側面が強い(成果)と思いますので、ためしに1年ずらして計算してみましょう。

  • 2015〜2019年の成績の平均
  • 2016〜2020年の年俸の平均

これらを使ってみます!! 基本的にはほぼ同じコードですので、新しいとこだけ書いておきます!!

集約して平均をだす

grouped_batter_detail = df_all_batter.groupby(['選手名', 'チーム'], as_index=False)
grouped_batter_detail_avg = grouped_batter_detail.mean()

新しいのはこの子ぐらいでした。
5年に増やしたことによりさらに選手がごちゃくつので、
『平均400打席を超えた人または平均年俸が2億円より多い人』に条件を変更して・・・抽出!!

その結果がこちら↓↓

いかがでしょう!!
いちおおっきいのも貼っておきます!!

f:id:astamuse:20201217025510p:plain
5年平均での図

この散布図からは、

  • 図右下の広島の丸選手が、図右上の巨人に移籍してしまうのも仕方がないかも…
  • 近似曲線をイメージすると、やはり全体的にソフトバンクと巨人はそれよりも上にいそうな気がする…
  • 気のせいかもしれませんが、中日は少し給料が低めなイメージが…

といったようなことがパッと思いつきますね!!
(個人の考えなので、ご気分を害された方がいらっしゃいましたら申し訳ございません。)

まとめ

いかがだったでしょうか。
大した分析はできていませんが、データを元にパッと可視化するだけで新たな気付きや発見があるというのが体験できたかと思います!!
今回はほんの一部の可視化〜分析でしたが、機械学習とかも絡めて予測できたらさらに幅が広がりそうですね!!

最後に、アスタミューゼではアプリエンジニア、デザイナー、プロダクトマネージャー、データエンジニア、機械学習エンジニアなどなど絶賛大募集中です!!
どしどしご応募ください!!お待ちしております!!

最後までお読みいただき、ありがとうございました!!

ニューラル機械翻訳モデルを自作してみる

f:id:astamuse:20201208203648p:plain

こんにちは、師走ですね。業務に関連した技術トピックをということで、今回は翻訳について書こうと思います。

機械翻訳について

弊社アスタミューゼは知の「流通・活用・民主化」を掲げており、世界各国の特許やグラントその他のデータを収集・分析・提供しています。これらの文書は当然各国語で記述されているため、統一した分析処理のためには機械翻訳や多言語処理が必要不可欠です。

ニューラル機械翻訳

2020年を振り返って翻訳関連の話題といえばDeepLを思い浮かべる方が多いのではないでしょうか。2018年頃から欧州を中心にGoogle Translateと比べた品質の優位性が話題になり、3月に日本語対応が発表されたことで国内での認知度が一気に向上しました。試しにつかってみたという方も多いはずです。

www.deepl.com

このDeepLに代表されるように、ここ数年の機械翻訳の発展は、自然言語処理のなかでもニューラル機械翻訳(Neural Machine Translation, 以下NMT)という分野の研究成果が寄与しているようです。従来の統計的機械翻訳と比べた性能向上やNMT固有の誤りとその対処法などが、国際会議でもさかんに報告されています。

この記事では、ニューラル機械翻訳(NMT)システムがその内側でどんな処理をしているかを理解することを目的に、実際に簡単な翻訳モデルを手元で作成してみて、その品質を評価するところまでをご紹介します。

続きを読む

Copyright © astamuse company, ltd. all rights reserved.