ランダムに取得したWikipediaの記事テキストの類似度を求める

WikipediaのAPIを使ってランダムに記事本文を取得する – 浅野直樹の学習日記JanomeとMeCabで日本語のテキストを単語に分ける – 浅野直樹の学習日記で下準備ができました。

これでいよいよランダムに取得したWikipediaの記事テキストの類似度を求めるというやりたかったことができます。

 

1.WikipediaのAPIを使って本文が長い記事をランダムに取得する

テキストの類似度を求めてそれっぽい結果を得るためには記事本文が長いほうがよいです。

そこで、ランダムに100件の記事を取得し、記事本文が長いほうから上位20件だけを抽出します。

まずはランダムに100件の記事のリストを取得し、それぞれの長さも取得して、長い順に並べます。

import pandas as pd
import requests

#結果を格納するデータフレームを作成し、表示行数を設定
df = pd.DataFrame(columns=["page_id", "title", "linked_title", "length"])
pd.set_option('display.max_rows', 6)

#wikipediaに接続するための基本設定
S = requests.Session()
URL = "https://ja.wikipedia.org/w/api.php"

#ランダム記事リストから記事本体を取得するためのパラメータの設定
GENERATOR_PARAMS = {
    #共通パラメータ
    "action": "query",
    "format": "json",
    #generatorパラメータ
    "generator": "random",
    "grnlimit": "100",
    "grnnamespace": "0",
    #記事情報パラメータ
    "prop": "info",
    "inprop": "url",
}

#ランダム100件の記事情報の取得
GENERATOR_R = S.get(url=URL, params=GENERATOR_PARAMS)
GENERATOR_DATA = GENERATOR_R.json()
for k, v in GENERATOR_DATA["query"]["pages"].items():
    page_id = k
    (title, fullurl, length) = (v["title"], v["fullurl"], v["length"])
    linked_title = "<a href='{}'>{}</a>".format(fullurl, title)    
    df.loc[len(df)+1] = [page_id, title, linked_title, length]

#長さ(length)の長い順に並べ替えて表示
sorted_df = df.sort_values("length", ascending=False)
ranked_df = sorted_df.set_axis(range(1, (len(df)+1) ), axis="index")
print(ranked_df[["page_id", "title", "length"]])

これで簡易的に上位3件と下位3件が表示されるはずです。

せっかくなのできれいに表示してみましょう。

from IPython.display import HTML

longest_3_df = ranked_df.query('index <= 3')

#ブログ記事執筆用にhtmlタグを出力
print(longest_3_df[["page_id", "linked_title", "length"]].to_html(escape=False))

#jupyter notebook内での確認用に整形された表を出力
HTML(longest_3_df[["page_id", "linked_title", "length"]].to_html(escape=False))
page_id linked_title length
1 97822 ジェンソン・バトン 118405
2 1296390 A.J.フォイト 45067
3 4249835 ブカレスト市電 43454
shortest_3_df = ranked_df.query('index >= 98')

#ブログ記事執筆用にhtmlタグを出力
print(shortest_3_df[["page_id", "linked_title", "length"]].to_html(escape=False))

#jupyter notebook内での確認用に整形された表を出力
HTML(shortest_3_df[["page_id", "linked_title", "length"]].to_html(escape=False))
page_id linked_title length
98 2933094 パルラディ 591
99 833807 Uボート (曖昧さ回避) 341
100 857624 赤えい 224

例えばこのように表示されます。

毎回結果は異なります。

それでは上位20件の記事本文を取得しましょう。

import time

#記事本体を取得するためのパラメータの設定
ARTICLE_PARAMS = {
    "action": "query",
    "format": "json",
    "prop": "extracts",
    "explaintext": True,
    "exsectionformat": "plain",
}

#長さの上位20件を抽出
longest_20_df = ranked_df.query('index <= 20')

#記事本文を一時的に保存するリストの作成
contents = []

for page_id in longest_20_df["page_id"]:
    #ページIDから記事情報の取得
    ARTICLE_PARAMS["pageids"] = str(page_id)
    ARTICLE_R = S.get(url=URL, params=ARTICLE_PARAMS)
    ARTICLE_DATA = ARTICLE_R.json()

    #記事本文をリストに追加
    content = ARTICLE_DATA["query"]["pages"][str(page_id)]["extract"]
    contents.append(content)

    #高速でリクエストを繰り返すことで負担をかけないように1秒待つ
    time.sleep(1)

#データフレームに記事本文の列を追加
contents_df = longest_20_df.assign(content=contents)
print(contents_df)

これで必要なデータを用意することができました。

 

2.テキストのベクトル化

機械学習をするためには、サンプル数×特徴量数の2次元配列(ベクトル)を作ります。

テキストをベクトル化する際の考え方や実際のコードはPythonで文章の類似度を計算する方法〜TF-IDFとcos類似度〜 | データサイエンス情報局がわかりやすかったです。

今回のサンプル数は20です。特徴量数は全記事に含まれる単語の種類の数になります。

基本的にsklearn.feature_extraction.text.TfidfVectorizer — scikit-learn 0.24.2 documentationを使えばよいのですが、これを日本語に適用するためにはそれなりの準備が求められます。

投入するデータはサンプルごとの文字列のシーケンス(リストなど)なのでもう用意できているとして、日本語では単語に分割(分かち書き)するためのanalyzerが必須となります。ここでjanomeかmecabを使います。

また、あまりにありふれすぎていてテキストの特徴を判別する手がかりにならないストップワードも設定したほうがよいです。【自然言語処理入門】日本語ストップワードの考察【品詞別】 – ミエルカAI は、自然言語処理技術を中心とした、RPA開発・サイト改善・流入改善レコメンドエンジンを開発からそのまま使わせていただきました。

英語であればTfidfVectorizerのパラメータにストップワードを設定できるのですが、日本語ではそれができないため、単語に分割(分かち書き)するときにフィルターにかけます。

import MeCab
from sklearn.feature_extraction.text import TfidfVectorizer

#ストップワードの設定
stop_words_str = "。 は 、 の ( ) に で を た し が と て ある れ さ する いる から も ・ として 「 」 い こと – な なっ や れる など ため この まで また あっ ない あり なる その られ 後 『 』 へ 日本 という よう ( 現在 もの より だ おり 的 中 により ) 2 による 第 なり によって 1 これ その後 ず , か 時 なく られる だっ において 者 なかっ 行わ 多く しかし 3 せ 他 名 出身 それ について 間 当時 上 ば 存在 受け . 呼ば 同 なお できる 目 行っ 内 う 数 のみ 前 以下 き : 元 化 4 等 および 使用 でき 同年 主 場合 際 一 約 における さらに 一部 所属 人 以降 ら 活動 5 中心 作品 いう 知ら 同じ 初 だけ 多い 時代 以上 生まれ 発表 2010年 にて 見 務め 持つ とともに 大 参加 頃 位置 2007年 2009年 2008年 開始 うち 行う ほか 特に 全 ながら 当初 発売 せる 2011年 家 かつて 下 卒業 一つ 2006年 6 でも 年 2012年 形 用い に対して 最初 / 本 考え なら 以外 関係 一方 それぞれ 各 同様 4月 経 2013年 と共に 2005年 そして 3月 地域 必要 これら 及び 一般 用 2014年 結果 可能 現 開催 事 ものの 利用 にかけて 部 影響 設立 記録 得 アメリカ 通り とも 彼 2015年 自身 登場 始め または 担当 変更 意 味 たり 側 とき 開発 設置 代表 ほど ので 構成 ただし 二 2004年 郡 初めて たち 部分 2016年 最も 放送 7 旧 地 最後 アメリカ合衆国 10月 世界 研究 大学 8 系 大きな 活躍 獲得 続け 以前 全て 問題 性 与え 9月 父 含む といった ほとんど 7月 ところ 2017年 2003年 向け 持っ 2000年 加え 使わ 型 6月 に関する 出場 12月 目的 高い 名称 に対する 1月 万 実際 5月 -1 名前 様々 再び 10"
stop_words = stop_words_str.split()

#単語に分割してストップワードをフィルタリングする関数の作成
def analyzer(text):
    tagger = MeCab.Tagger("-Owakati")
    wakati = tagger.parse(text)
    filtered_wakati = filter(lambda x: x not in stop_words, wakati.split())
    return filtered_wakati

#テキストのベクトル化
documents = contents_df["content"]
vectorizer = TfidfVectorizer(analyzer=analyzer)
X = vectorizer.fit_transform(documents)
print(X.toarray())

これでテキストのベクトル化ができました。非常に疎(0の割合が多い)なベクトルが出力されます。

サンプル数×特徴量数の2次元配列(ベクトル)になっているか確認してみましょう。

print(X.toarray().shape)
(20, 8302)

よさそうです。

特徴量の内訳も見てみましょう。

print(vectorizer.vocabulary_)
{'ジェンソン': 2723, 'アレクサンダー': 2235, 'ライオンズ': 3690, 'バトン': 3183, 'Jenson': 821, 'Alexander': 484, 'Lyons': 883, 'Button': 589,
【以下略】

うまくいっているようです。

 

3.コサイン類似度を計算する

ここまでできたらあともう少しです。

作成したベクトルのコサイン類似度を求めます。

from sklearn.metrics.pairwise import cosine_similarity

#類似度行列の作成
cs_array = cosine_similarity(X)
print(cs_array)

20×20の行列が出力されます。

値が1に近いほど類似しており、0に近いほど類似していません。

当然ながら、同じテキストを比べている対角成分は1になっています。

この類似度行列から、その記事自体は除外して、最も似ている3つの記事と、最も似ていない3つの記事のタイトルを表示してみます。

cs_arrayはインデックスが0から始まり、contents_dfは記事の長さの順位を表わすために1から始まっていることに注意してください。

import numpy as np

for index, item in enumerate(cs_array):
    #その記事のタイトル
    print("・「{}」".format(titles[index+1]))

    #似ている3つ
    print("<似ている3つの記事>")
    best_3 = sorted(item, reverse=True)[1:4]
    for best in best_3:
        best_index = np.where(item == best)[0][0]
        print("{}({})".format(contents_df["title"][best_index+1], best))

    #似ていない3つ
    print("<似ていない3つの記事>")
    worst_3 = sorted(item)[0:3]
    for worst in worst_3:
        worst_index = np.where(item == worst)[0][0]
        print("{}({})".format(contents_df["title"][worst_index+1], worst))

    print("\n")

これで簡易的に結果が出力されます。

似ているほうだけを見やすく整形して表示します。

#結果を格納するデータフレームの作成
best_df = pd.DataFrame(columns=["title", "1", "2", "3"])

for index, item in enumerate(cs_array):
    #その記事のタイトルを最初の要素とするリストの作成
    results = [titles[index+1]]

    #似ている3つ
    best_3 = sorted(item, reverse=True)[1:4]
    for best in best_3:
        best_index = np.where(item == best)[0][0]
        result = "{}({})".format(contents_df["linked_title"][best_index+1], round(best, 3))
        results.append(result)

    #データフレームに追加
    best_df.loc[len(best_df)+1] = results

#ブログ記事執筆用にhtmlタグを出力
print(best_df.set_index("title").to_html(escape=False))

#jupyter notebook内での確認用に整形された表を出力
HTML(best_df.set_index("title").to_html(escape=False))

結果は以下です。

title 1 2 3
ジェンソン・バトン A.J.フォイト(0.222) サンインロー(0.065) 松平康英(0.046)
A.J.フォイト ジェンソン・バトン(0.222) サンインロー(0.068) コロンビアの戦い(0.036)
ブカレスト市電 コラディア・コンチネンタル(0.299) 茨城県道168号静常陸大宮線(0.075) 第56回国民体育大会(0.057)
矢野まき 甲本ヒロト(0.29) 松平康英(0.044) コロンビアの戦い(0.038)
ホオジロ科 ワレカラ(0.037) コラディア・コンチネンタル(0.014) ブカレスト市電(0.009)
コラディア・コンチネンタル ブカレスト市電(0.299) 茨城県道168号静常陸大宮線(0.123) コロンビアの戦い(0.057)
甲本ヒロト 矢野まき(0.29) 心はロンリー気持ちは「…」(0.07) 松平康英(0.051)
ギルドウォーズ コロンビアの戦い(0.04) ジェンソン・バトン(0.035) ワレカラ(0.028)
クィントゥス・マルキウス・ピリップス (紀元前186年の執政官) コロンビアの戦い(0.049) 近代エジプトの国家元首の一覧(0.03) 江畑謙介(0.027)
コロンビアの戦い 茨城県道168号静常陸大宮線(0.064) 松平康英(0.057) コラディア・コンチネンタル(0.057)
ワレカラ コロンビアの戦い(0.045) ブカレスト市電(0.04) ホオジロ科(0.037)
心はロンリー気持ちは「…」 甲本ヒロト(0.07) 松平康英(0.041) 矢野まき(0.035)
潜水空母 江畑謙介(0.068) ブカレスト市電(0.044) コラディア・コンチネンタル(0.03)
サンインロー A.J.フォイト(0.068) ジェンソン・バトン(0.065) ワレカラ(0.032)
近代エジプトの国家元首の一覧 クィントゥス・マルキウス・ピリップス (紀元前186年の執政官)(0.03) ブカレスト市電(0.02) 松平康英(0.015)
松平康英 茨城県道168号静常陸大宮線(0.071) 第56回国民体育大会(0.058) コロンビアの戦い(0.057)
茨城県道168号静常陸大宮線 第56回国民体育大会(0.175) コラディア・コンチネンタル(0.123) ブカレスト市電(0.075)
第56回国民体育大会 茨城県道168号静常陸大宮線(0.175) 松平康英(0.058) ブカレスト市電(0.057)
江畑謙介 潜水空母(0.068) 葉桜が来た夏(0.065) コロンビアの戦い(0.052)
葉桜が来た夏 江畑謙介(0.065) 甲本ヒロト(0.032) 松平康英(0.031)

()内はコサイン類似度を小数第3位にまるめた値です。

その値が高い組み合わせである、ジェンソン・バトンとA.J.フォイト、ブカレスト市電とコラディア・コンチネンタル、矢野まきと甲本ヒロトあたりは納得できる結果です。

何度も繰り返してよさそうに見える結果を示したのではなく、一発勝負でこの結果が得られました。




コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です