HRTechするために会議情報を機械学習させる
11
- 1月
2020
Posted By : boomin
HRTechするために会議情報を機械学習させる
Advertisements

TL; DR

会議データを活用することでこんなものを作るための、学習モデルを作ります。

membersearch-min.png

1. 前提

  • ここを読んでいること
  • その上で、以下のデータを準備してあること
    • 形態素解析済の会議データ
      • filterred_mecabdata.pkl
    • メンバー詳細データ
      • memberdict.pkl
      • ただし、本記事ではこのファイルは使用しません

以下で使用するのは、国会議事録のデータを元にした会議データです。

2. 機械学習

各メンバに特徴的な文章と、各メンバ間の類似度を計算します。 何をもって文章間が類似していると判断するかはそれぞれ考えていただくとして、ここでは以下の2通りの方法で、類似度を評価することにします。

  • TF-IDFスコアを用いる方法
  • doc2vecを使う方法

ちょっと、そこあなた! TF-IDFは機械学習じゃないって思ったでしょ!!

はいその通りです。でも巷の人はその違いは知らないので、結果が何となくいい感じなっていれば気付かれません。ので、

「AIが頑張りました!(にっこり)」

と言っておけば、ほぼ問題はないでしょう。AIのイメージなんて、人の数だけありますしね(キリッ!

2.1 TF-IDFを用いた人物間の類似度算出

scikit-learnのパッケージを使います。

import pandas as pd
from sklearn.feature_extraction.text import TfidfVectorizer

# 会議データの読み込み
df = pd.read_pickle(f"filterred_mecabdata.pkl")

# TF-IDFするまえに、人:単語という行列を作る
termdb = []
people = list(set(df.Name.values))

print("### prepareing for tf-idf")
def getWordsForPeople(df, name):
  wordslist = [x for terms in df[df.Name==name]["sub_mecab"] for x in terms.split(" ")]
  wlist = ' '.join(wordslist)
  return wlist.strip()
termdb = [ getWordsForPeople(df, name) for name in people ]

print("### vectorizing for tf-idf")
vectorizer = TfidfVectorizer(
  use_idf=True,
  stop_words = "english",
  norm='l2',     # L2正則で初期化・学習
  max_df = 0.95, # min_dfの逆で、あまりにも出現している単語は排除
  min_df = 3     # 使用されている文章の割合、または使用されている文章数がパラメータ以下の単語を排除
)
veclist = vectorizer.fit_transform(termdb).toarray()
cols = vectorizer.get_feature_names()
termMatrix = pd.DataFrame(veclist, index=people, columns=cols)
termMatrix.head()

"""こんな感じの結果となる
Out[22]: 
                abenomics  adb        ai  aia  ...       黒字化   黒田   黙認   齋藤
山口那津男            0.013761  0.0  0.004045  0.0  ...  0.003385  0.0  0.0  0.0
蝦名邦晴             0.000000  0.0  0.000000  0.0  ...  0.000000  0.0  0.0  0.0
堀越啓仁             0.000000  0.0  0.036729  0.0  ...  0.000000  0.0  0.0  0.0
ソロモン・カランジャ・マイナ   0.000000  0.0  0.000000  0.0  ...  0.000000  0.0  0.0  0.0
大家敏志             0.027869  0.0  0.000000  0.0  ...  0.000000  0.0  0.0  0.0
"""

ソロモン・カランジャ・マイナさんて誰かと思ったら、駐日ケニア大使の方なんですね。国会で発言したことがあるってことか。。。。

それではscipyの疎行列演算ライブラリを利用して、TF-IDFスコア行列をもとにして全人物間のcosine類似度を算出します。ちゃんscipyライブラリ使えば、大きくなりがちな疎行列でもストレスない速度で処理が終わります。

from scipy.sparse import lil_matrix
import numpy as np

tmp = lil_matrix(termMatrix, dtype=np.float32)
norms = np.sqrt(tmp.multiply(tmp).sum(axis=1))
for i in range(tmp.shape[0]):
    tmp[i, :] /= norms[i]
sim_people = pd.DataFrame(tmp.dot(tmp.T).toarray(), index=people, columns=people)
# 保存
pd.to_pickle(df, f"sim_people.pkl")

sim_people.head()
Out[25]: 
                   山口那津男      蝦名邦晴      堀越啓仁  ...      雄谷良成       中谷元    うえの賢一郎
山口那津男           1.000000  0.038527  0.225793  ...  0.055729  0.033223  0.084661
蝦名邦晴            0.038527  1.000000  0.022246  ...  0.000000  0.004380  0.013861
堀越啓仁            0.225793  0.022246  1.000000  ...  0.059782  0.047006  0.111622
ソロモン・カランジャ・マイナ  0.104878  0.008498  0.055214  ...  0.011912  0.007220  0.045744
大家敏志            0.325383  0.013183  0.126845  ...  0.023060  0.026107  0.068655

Advertisements

2.2 doc2vecで人物間の類似度算出

gensimパッケージを使えば、簡単に学習できます。

from gensim import models
import pandas as pd

df = pd.read_pickle(f"filterred_mecabdata.pkl")

# tagに人名を使い、gensimのTaggedDocumentにする。
def createTaggedDoc(n,d):
    twt = d.split(" ")
    line = models.doc2vec.TaggedDocument(twt, tags=[n])
    return line
sentences = [ createTaggedDoc(n,d) for (n,d) in zip(df["Name"], df["sub_mecab"]) ]

num_ittr = 40
num_epochs = 100
vecsize = 200

# doc2vecの学習条件設定
# alpha: 学習率  高いほど収束が速いが、高すぎると発散する。低いほど精度が高いが、収束が遅くなる。
# min_count: X回未満しか出てこない単語は無視
# size: ベクトルの次元数 / iter: 反復回数 / workers: 並列実行数
model = models.Doc2Vec(min_count=2, workers=8, window=3, alpha=0.05, min_alpha=0.00, epochs=num_ittr, vector_size=vecsize)
# doc2vec の学習前準備(単語リスト構築)
model.build_vocab(sentences)
# doc2vec学習実行
model.train(sentences, total_examples=len(df), epochs=num_epochs)

# 学習結果の確認
model.docvecs.most_similar(["安倍晋三"], topn=10)
#[('山崎正昭', 0.6357505321502686),
# ('宮園司史', 0.5332638025283813),
# ('川上義博', 0.4964902997016907),
# ('滝崎成樹', 0.49032342433929443),
# ('山口那津男', 0.4845779538154602),
# ('松本文明', 0.48263484239578247),
# ('石原宏高', 0.47903451323509216),
# ('井上義久', 0.47557246685028076),
# ('左藤章', 0.47541213035583496),
# ('原田憲治', 0.47051531076431274)]

AIとか機械学習が関係するような発言では、安倍晋三首相は上記のような人たちと発言が似通っているようです。ふーん。。。。

ここでは、それっぽく結果を求めることが目的なので、精度は追いません。 気になる方は、適切なデータと適切な学習モデルを使って、適切にチューニングしてモデルを構築して下さい!

結局のところ

def getSimilality(知りたい人名):
    return 知りたい人名に近しい人を検索した結果

ということができる関数getSimilality(名前)があれば、中身の具体的なロジックは何でもいいです。

3. ネットワーク分析

上述のgetSimilality(名前)に相当する機能を使って、Network Graphを作ります。 便利なパッケージをインストールします。

pip install networkx
pip install community
pip install python-louvain

後半の2つは、Network Graphからクラスタリングを行うためのパッケージです。 louvain法という、計算量のわりにそこそこいい感じにクラスタリングしてくれる方法を使います。

import pandas as pd
import networkx as nx
import community
import math

# 会議データ読み込み
df = pd.read_pickle(f"filterred_mecabdata.pkl")

# TF-IDFから作った類似度行列読み込み
model = pd.read_pickle("sim_people.pkl")

G = nx.Graph()
# node追加
nameCountList = [(name, {"size":size}) for name,size in df["Name"].value_counts().items()]
G.add_nodes_from(nameCountList)

numMem = int(len(nameCountList))
for i,x in enumerate(nameCountList):
  try:
    # doc2vecのモデルを使う場合
    #simlist = model.docvecs.most_similar(x[0], topn=numMem) # getSimilality(名前)に相当
    #simlist = [ (sim[0], sim[1]) for sim in simlist if sim[1]>0.2 ] # ある程度似ている人だけ(ここでは0.2以上)を対象とする
      
    # TF-IDFモデルを使う場合
    ts = model[x[0]] # getSimilality(名前)に相当
    ts = ts[ts>0.2].sort_values(ascending=False)[1:] # ある程度似ている人だけ(ここでは0.2以上)を対象とする
    simlist = sorted([ (s,t) for (s,t) in zip(ts.index, ts ) ])
    simlist = [ (sim[0], sim[1]) for sim in simlist ]

    for tacc, sim in simlist:
      if not G.has_node(x[0]) or not G.has_node(tacc) or sim<0:
        continue
      if G.has_edge(x[0], tacc):
        G.edges[x[0],tacc]["weight"] += sim
      else:
        #エッジの追加
        G.add_edge(x[0], tacc, weight=sim )
  except:
    print("\terror: ", x)

これでNetwork Graphができました。 それではクラスタリングです。

part = community.best_partition(G, partition=None, weight='weight', resolution=1)
# modは1に近いほど精度がよいとされる
mod = community.modularity(part,G)
psize = float(len(set(part.values())))
print(f"partition size:{psize}, modularity:{mod}")

# nodeにクラスタ番号を追加
for name in part:
  if not G.has_node(name):
    print(name + " nothing")
    continue
  else:
    G.nodes[name]["cluster"] = part[name]

# network graphを保存
nx.write_gpickle(G, f"comm_analysis.pkl")

netowrkxのオブジェクトであるGには、全てのNetwork Graphが存在します。しかしこのまま全部描画するのは非現実的です。そこで、必要なnodeだけ指定して取り出し、その取り出したNetwork Graphだけを利用することにします。

targets=["安倍晋三","麻生太郎","世耕弘成","稲田朋美","塩崎恭久","高市早苗"]
H = G.subgraph(targets)

H.nodes(data=True)
#
#NodeDataView({'稲田朋美': {'size': 3, 'cluster': 1}, '麻生太郎': {'size': 54, 'cluster': 3}, '世耕弘成': {'size': 142, 'cluster': 0}, '塩崎恭久': {'size': 39, 'cluster': 4}, '高市早苗': {'size': 30, 'cluster': 1}, '安倍晋三': {'size': 95, 'cluster': 1}})

H.edges(data=True)
#
# EdgeDataView([('稲田朋美', '世耕弘成', {'weight': 0.5984722375869751}), ('稲田朋美', '安倍晋三', {'weight': 0.9666371941566467}), ('稲田朋美', '塩崎恭久', {'weight': 0.48173508048057556}), ('稲田朋美', '高市早苗', {'weight': 0.4896692633628845}), ('麻生太郎', '世耕弘成', {'weight': 0.7263149619102478}), ('麻生太郎', '安倍晋三', {'weight': 0.6178034543991089}), ('麻生太郎', '塩崎恭久', {'weight': 0.46518972516059875}), ('世耕弘成', '塩崎恭久', {'weight': 0.8961162567138672}), ('世耕弘成', '安倍晋三', {'weight': 1.2007122039794922}), ('世耕弘成', '高市早苗', {'weight': 0.945235550403595}), ('塩崎恭久', '安倍晋三', {'weight': 0.9955565333366394}), ('塩崎恭久', '高市早苗', {'weight': 0.9067516922950745}), ('高市早苗', '安倍晋三', {'weight': 1.053189754486084})])

描画すると、こんな感じのNetwork Graphとなります。

import matplotlib.pyplot as plt
fig = plt.figure(num=None, figsize=(5, 5))
plt.axis('off')
edges = H.edges()
weights = [H[u][v]['weight'] for u,v in edges]
nx.draw_networkx(H, edges=edges, width=weights)
plt.savefig("path_to_fig.png", bbox_inches='tight')

path_to_fig.png

あとは上記のような処理を実装したAPIを作って、いい感じに結果が返るように作ればいいです。 APIを作る話は、ここでは割愛します。 なお、結果をjson化するには、以下のようにすればいいです。

from networkx.readwrite import json_graph
jsonH = json_graph.node_link_data(H)

import numpy as np
import json, codecs
class MyEncoder(json.JSONEncoder):
  def default(self, obj):
    if isinstance(obj, np.integer):
      return int(obj)
    elif isinstance(obj, np.floating):
      return float(obj)
    elif isinstance(obj, np.ndarray):
      return obj.tolist()
    else:
      return super(MyEncoder, self).default(obj)

# jsonをファイル出力する
def write2Json(cl, fname):
    f = codecs.open(fname, "w", "utf-8")
    json.dump(cl, f, indent=2, sort_keys=False, ensure_ascii=False, cls=MyEncoder)
    f.close()

write2Json(jsonH, f"jsonH.json")

jsonの中身は以下のようになります。

{
  "directed": false,
  "multigraph": false,
  "graph": {},
  "nodes": [
    {
      "size": 3,
      "cluster": 1,
      "id": "稲田朋美"
    },
    {
      "size": 54,
      "cluster": 3,
      "id": "麻生太郎"
    },
    {
      "size": 142,
      "cluster": 0,
      "id": "世耕弘成"
    },
    {
      "size": 39,
      "cluster": 4,
      "id": "塩崎恭久"
    },
    {
      "size": 30,
      "cluster": 1,
      "id": "高市早苗"
    },
    {
      "size": 95,
      "cluster": 1,
      "id": "安倍晋三"
    }
  ],
  "links": [
    {
      "weight": 0.5984722375869751,
      "source": "稲田朋美",
      "target": "世耕弘成"
    },
    {
      "weight": 0.9666371941566467,
      "source": "稲田朋美",
      "target": "安倍晋三"
    },
    {
      "weight": 0.48173508048057556,
      "source": "稲田朋美",
      "target": "塩崎恭久"
    },
    {
      "weight": 0.4896692633628845,
      "source": "稲田朋美",
      "target": "高市早苗"
    },
    {
      "weight": 0.7263149619102478,
      "source": "麻生太郎",
      "target": "世耕弘成"
    },
    {
      "weight": 0.6178034543991089,
      "source": "麻生太郎",
      "target": "安倍晋三"
    },
    {
      "weight": 0.46518972516059875,
      "source": "麻生太郎",
      "target": "塩崎恭久"
    },
    {
      "weight": 0.8961162567138672,
      "source": "世耕弘成",
      "target": "塩崎恭久"
    },
    {
      "weight": 1.2007122039794922,
      "source": "世耕弘成",
      "target": "安倍晋三"
    },
    {
      "weight": 0.945235550403595,
      "source": "世耕弘成",
      "target": "高市早苗"
    },
    {
      "weight": 0.9955565333366394,
      "source": "塩崎恭久",
      "target": "安倍晋三"
    },
    {
      "weight": 0.9067516922950745,
      "source": "塩崎恭久",
      "target": "高市早苗"
    },
    {
      "weight": 1.053189754486084,
      "source": "高市早苗",
      "target": "安倍晋三"
    }
  ]
}
Advertisements

コメントを残す