見出し画像

PositionRank: An Unsupervised Approach to Keyphrase Extraction from Scholarly Documents (2017)

著者:Corina Florescu and Cornelia Caragea
機関:University of North Texas
会議:ACL17
URL:https://aclanthology.org/P17-1102/

一言で:PageRankのアルゴリズムを応用したグラフベースの教師なしキーフレーズ抽出器

コード

!pip install nltk
import math
import numpy as np
import copy
from collections import Counter, defaultdict
from nltk.tokenize import word_tokenize
from nltk.corpus import stopwords
from nltk.stem import PorterStemmer
import nltk

# 必要なデータのダウンロード
nltk.download('punkt')
nltk.download('stopwords')

def position_rank(text, alpha=0.85, window_size=6, num_keyphrases=10, lang="en"):
    """
    PositionRankアルゴリズムを用いてキーフレーズを抽出する関数。

    Args:
        text: タイトルとアブストラクトを結合したテキスト
        alpha: Damping factor
        window_size: 共起単語の窓幅
        num_keyphrases: 抽出するキーフレーズの数
        lang: ターゲット言語

    Returns:
        キーフレーズのリスト
    """
    if lang == "en":
        stemmer = PorterStemmer()
        stem = stemmer.stem
        stop_words = set(stopwords.words('english'))
    else:
        stem = lambda word: word
        stop_words = set()

    # トークナイズとストップワードの除去
    words = word_tokenize(text)
    original_words = [word for word in words if word.isalpha() and word.lower() not in stop_words]
    stemmed_words = [stem(word.lower()) for word in original_words]
    unique_words = list(set(stemmed_words))
    n = len(unique_words)

    # 隣接行列の初期化
    adj_matrix = np.zeros((n, n))
    word2idx = {w: i for i, w in enumerate(unique_words)}
    p_vec = np.zeros(n)
    co_occ_dict = {w: [] for w in unique_words}

    # pベクトルの計算と共起単語の収集
    for i, word in enumerate(stemmed_words):
        p_vec[word2idx[word]] += 1 / (i + 1)
        for window_idx in range(1, math.ceil(window_size / 2) + 1):
            if i - window_idx >= 0:
                co_occ_dict[word].append(stemmed_words[i - window_idx])
            if i + window_idx < len(stemmed_words):
                co_occ_dict[word].append(stemmed_words[i + window_idx])

    # 隣接行列の生成
    for word, co_list in co_occ_dict.items():
        count = Counter(co_list)
        for co_word, freq in count.items():
            adj_matrix[word2idx[word]][word2idx[co_word]] = freq

    # 正規化隣接行列の生成
    adj_matrix = adj_matrix / adj_matrix.sum(axis=0)
    p_vec = p_vec / p_vec.sum()
    s_vec = np.ones(n) / n

    # PageRankアルゴリズムの適用
    lambda_val = 1.0
    iterations = 0

    while lambda_val > 0.001 and iterations < 100:
        next_s_vec = copy.deepcopy(s_vec)
        for i in range(n):
            next_s_vec[i] = (1 - alpha) * p_vec[i] + alpha * np.sum(adj_matrix[i] * s_vec)
        lambda_val = np.linalg.norm(next_s_vec - s_vec)
        s_vec = next_s_vec
        iterations += 1

    # スコアに基づいてキーフレーズを抽出
    word_scores = {word: s_vec[word2idx[word]] for word in unique_words}
    sorted_words = sorted(word_scores.items(), key=lambda item: item[1], reverse=True)

    # ステムから元の単語に戻す処理
    original_to_stemmed = defaultdict(list)
    for original, stemmed in zip(original_words, stemmed_words):
        original_to_stemmed[stemmed].append(original)

    keyphrases = []
    for stemmed_word, score in sorted_words[:num_keyphrases]:
        original_words = original_to_stemmed[stemmed_word]
        # 一番頻度が高い元の形をキーフレーズとして使用
        most_common_original = Counter(original_words).most_common(1)[0][0]
        keyphrases.append((most_common_original, score))

    return keyphrases

# サンプルテキスト(ImageNetのアブスト)
sample_text = """
The explosion of image data on the Internet has the potential to foster more sophisticated and robust models and algorithms to index, retrieve, organize and interact with images and multimedia data. But exactly how such data can be harnessed and organized remains a critical problem. We introduce here a new database called “ImageNet”, a large-scale ontology of images built upon the backbone of the WordNet structure. ImageNet aims to populate the majority of the 80,000 synsets of WordNet with an average of 500–1000 clean and full resolution images. This will result in tens of millions of annotated images organized by the semantic hierarchy of WordNet. This paper offers a detailed analysis of ImageNet in its current state: 12 subtrees with 5247 synsets and 3.2 million images in total. We show that ImageNet is much larger in scale and diversity and much more accurate than the current image datasets. Constructing such a large-scale database is a challenging task. We describe the data collection scheme with Amazon Mechanical Turk. Lastly, we illustrate the usefulness of ImageNet through three simple applications in object recognition, image classification and automatic object clustering. We hope that the scale, accuracy, diversity and hierarchical structure of ImageNet can offer unparalleled opportunities to researchers in the computer vision community and beyond.
"""
keyphrases = position_rank(sample_text, lang="en")
for i, (phrase, score) in enumerate(keyphrases):
    print(f"{i+1}. {phrase} (Score: {score})")

出力

1. images (Score: 0.08558922889446646)
2. data (Score: 0.06450110158091016)
3. explosion (Score: 0.036316432181914216)
4. ImageNet (Score: 0.033240174145587865)
5. organized (Score: 0.03298064706250361)
6. Internet (Score: 0.03107386763269467)
7. foster (Score: 0.02358834346408973)
8. potential (Score: 0.023403567982624586)
9. sophisticated (Score: 0.023085874244827408)
10. robust (Score: 0.020798992842924937)

日本語対応

!pip install mecab-python3
!pip install unidic-lite
import math
import numpy as np
import copy
from collections import Counter, defaultdict
import MeCab
from nltk.corpus import stopwords
from nltk.stem import PorterStemmer
import nltk

# 必要なデータのダウンロード
nltk.download('stopwords')

# 日本語のストップワードリストのサンプル
japanese_stopwords = set([
    "の", "に", "は", "を", "た", "が", "で", "て", "と", "し", "れ", "さ", "ある", "いる", "も", "する", "から", "な", "こと", "として", "いく", "よう", "また", "もの", "これ", "その", "それ"
])

def position_rank(text, alpha=0.85, window_size=6, num_keyphrases=10, lang="en"):
    """
    PositionRankアルゴリズムを用いてキーフレーズを抽出する関数。

    Args:
        text: タイトルとアブストラクトを結合したテキスト
        alpha: Damping factor
        window_size: 共起単語の窓幅
        num_keyphrases: 抽出するキーフレーズの数
        lang: ターゲット言語

    Returns:
        キーフレーズのリスト
    """
    if lang == "en":
        stemmer = PorterStemmer()
        stem = stemmer.stem
        stop_words = set(stopwords.words('english'))
    elif lang == "jp":
        tagger = MeCab.Tagger()
        stem = lambda word: word  # 日本語にはステミングを適用しない
        stop_words = japanese_stopwords
    else:
        stem = lambda word: word
        stop_words = set()

    # トークナイズとストップワードの除去
    if lang == "jp":
        words = [word.split('\t')[0] for word in tagger.parse(text).split('\n') if '\t' in word]
    else:
        words = word_tokenize(text)

    original_words = [word for word in words if word.isalpha() and word not in stop_words]
    stemmed_words = [stem(word.lower()) for word in original_words]
    unique_words = list(set(stemmed_words))
    n = len(unique_words)

    # 隣接行列の初期化
    adj_matrix = np.zeros((n, n))
    word2idx = {w: i for i, w in enumerate(unique_words)}
    p_vec = np.zeros(n)
    co_occ_dict = {w: [] for w in unique_words}

    # pベクトルの計算と共起単語の収集
    for i, word in enumerate(stemmed_words):
        p_vec[word2idx[word]] += 1 / (i + 1)
        for window_idx in range(1, math.ceil(window_size / 2) + 1):
            if i - window_idx >= 0:
                co_occ_dict[word].append(stemmed_words[i - window_idx])
            if i + window_idx < len(stemmed_words):
                co_occ_dict[word].append(stemmed_words[i + window_idx])

    # 隣接行列の生成
    for word, co_list in co_occ_dict.items():
        count = Counter(co_list)
        for co_word, freq in count.items():
            adj_matrix[word2idx[word]][word2idx[co_word]] = freq

    # 正規化隣接行列の生成
    adj_matrix = adj_matrix / adj_matrix.sum(axis=0)
    p_vec = p_vec / p_vec.sum()
    s_vec = np.ones(n) / n

    # PageRankアルゴリズムの適用
    lambda_val = 1.0
    iterations = 0

    while lambda_val > 0.001 and iterations < 100:
        next_s_vec = copy.deepcopy(s_vec)
        for i in range(n):
            next_s_vec[i] = (1 - alpha) * p_vec[i] + alpha * np.sum(adj_matrix[i] * s_vec)
        lambda_val = np.linalg.norm(next_s_vec - s_vec)
        s_vec = next_s_vec
        iterations += 1

    # スコアに基づいてキーフレーズを抽出
    word_scores = {word: s_vec[word2idx[word]] for word in unique_words}
    sorted_words = sorted(word_scores.items(), key=lambda item: item[1], reverse=True)

    # ステムから元の単語に戻す処理
    original_to_stemmed = defaultdict(list)
    for original, stemmed in zip(original_words, stemmed_words):
        original_to_stemmed[stemmed].append(original)

    keyphrases = []
    for stemmed_word, score in sorted_words[:num_keyphrases]:
        original_words = original_to_stemmed[stemmed_word]
        # 一番頻度が高い元の形をキーフレーズとして使用
        most_common_original = Counter(original_words).most_common(1)[0][0]
        keyphrases.append((most_common_original, score))

    return keyphrases

# サンプルテキスト
sample_text_jp = """
2025年大阪・関西万博への子どもの無料招待をめぐり、大阪府は3日、府内の学校への来場意向調査の結果を発表した。招待対象の学校のうち約73%が「希望する」とした一方、約18%が「未定・検討中」と回答。約8%が期限までに回答しなかった。
 意向調査は、府内の小中高、支援学校の計約1900校(児童・生徒計約88万人)が対象。府側が各学校に来場の意向があるかどうかや、来場を希望する日時、会場までの交通手段などについて先月末までに回答するよう求めている。
 府によると、5月末日時点で調査対象の1900校のうち約73%にあたる約1390校が来場を希望し、約18%(約350校)が「未定・検討中」と回答。約8%(約160校)は回答がなかった。
 府は学校単位での参加を見送った学校の児童・生徒についても無料で入場できるように対応するとしている。
 吉村洋文知事は3日、記者団に「非常に多くの参加希望があった。大きな教育的な意義がある、ということで多くの学校が参加の希望をされたと思う」と述べた。回答しなかった160校については「おそらく不参加の意向かなと思っているが、ここは最終確認する必要があるので確認する」とした。
 無料招待事業をめぐっては、大阪府交野市が保護者側に交通費が生じることなどから、市内13校の学校単位での参加を見送ると表明。山本景市長は、府の意向調査では「希望する」と「未定・検討中」としか尋ねていないことに「『希望しない』という選択肢がなかった」とし「極めて誤解を招き事実と異なるもの」と批判している。
 一方、滋賀、京都、兵庫、奈良、和歌山の5府県も子どもの無料招待事業を実施する予定で、いずれも今後、学校の意向調査を行う方針だ。
"""
keyphrases_jp = position_rank(sample_text_jp, lang="jp")
for i, (phrase, score) in enumerate(keyphrases_jp):
    print(f"{i+1}. {phrase} (Score: {score})")

出力

1. 府 (Score: 0.048439245316226606)
2. 大阪 (Score: 0.04748262768668639)
3. 約 (Score: 0.036870891482113664)
4. 年 (Score: 0.036312014155193306)
5. 学校 (Score: 0.034244058199156956)
6. 無料 (Score: 0.03038347965623244)
7. 招待 (Score: 0.030023932330111386)
8. 関西 (Score: 0.028550779638791057)
9. 万博 (Score: 0.02836712102695662)
10. へ (Score: 0.02548225589958707)

この記事が気に入ったらサポートをしてみませんか?