TextRankによるR言語でのキーワード抽出と文書要約

TextRankは、PageRankから派生したグラフベースの自然言語処理アルゴリズムであり、単語や文をノード、それらの共起・類似関係をエッジとして構築されたネットワーク上で反復的に重要度(スコア)を計算します。この手法は、TF-IDFとは異なり、文書内の局所的およびグローバルな構造情報を活用して、意味的に重要な単語や文を自動抽出します。

本実装では、中国語テキストを対象に、textrankパッケージとjiebaRを組み合わせて以下の2つのタスクを遂行します:

  • キーワード抽出:名詞中心の2-gramまでを考慮し、出現頻度とn-gram長を複合基準でランキング
  • 文書要約(キーセンテンス抽出):文単位で分割し、文間の語彙重複に基づく類似度行列を構築してTextRankスコアを算出

環境準備とデータ読み込み

必要なライブラリをインストール・読み込みます。

library(pacman)
p_load(tidyverse, tidytext, textrank, rio, jiebaR)

サンプルデータ(IT企業の求人記述4102件)を読み込みます。

import("hire_text.rda") -> job_descriptions
glimpse(job_descriptions)

名詞限定の分語処理

キーワード抽出の精度向上のため、品詞タグ付き分語を実施。名詞("n")のみを対象とします。

job_descriptions %>%
  mutate(doc_id = row_number()) -> docs_with_id

# 品詞解析可能なワーカーを初期化
pos_tagger <- worker(type = "tag")

docs_with_id %>%
  mutate(tokens_with_pos = map(hire_text, tagging, jieba = pos_tagger)) %>%
  mutate(pos_df = map(tokens_with_pos, ~enframe(.x, name = "pos", value = "term"))) %>%
  select(doc_id, pos_df) -> pos_annotated

TextRankによるキーワード抽出

各ドキュメントごとに名詞のみを抽出し、2-gramまでの共起パターンを考慮してTextRankを適用します。

extract_noun_keywords <- function(pos_df) {
  # 名詞のみフィルタ + ngram生成(1〜2語)
  noun_terms <- pos_df %>%
    filter(str_starts(pos, "n")) %>%
    pull(term)
  
  textrank_keywords(
    terms = noun_terms,
    relevant = TRUE,
    ngram_max = 2
  )$keywords
}

pos_annotated %>%
  mutate(keywords = map(pos_df, extract_noun_keywords)) %>%
  select(-pos_df) -> keyword_results

各文書から上位3キーワードを抽出。優先順位は「出現回数>1」かつ「n-gram長が大きいほど高ランク」とします。

keyword_results %>%
  unnest(keywords) %>%
  filter(freq > 1) %>%
  group_by(doc_id) %>%
  slice_max(ngram, n = 3) %>%
  ungroup() -> top3_per_doc

# 全体で最も頻出するキーワードTOP10
top3_per_doc %>%
  count(keyword, sort = TRUE) %>%
  head(10)

文単位の要約生成

文の区切りには空白文字列(全角・半角スペース、タブ、改行など)を用い、[:space:]+で分割します。

split_into_sentences <- function(text) {
  str_split(text, "[:space:]+")[[1]] |>
    enframe(name = NULL, value = "sentence") |>
    mutate(sent_id = row_number())
}

segment_sentences <- function(text) {
  sentences <- split_into_sentences(text)
  word_worker <- worker()
  
  sentences %>%
    mutate(words = map(sentence, segment, jieba = word_worker)) %>%
    select(-sentence) %>%
    unnest(words) %>%
    distinct(sent_id, words)
}

TextRankを用いた要約生成関数を定義します。単一文のみのドキュメントはスキップします。

summarize_document <- function(text) {
  sents <- split_into_sentences(text)
  if (nrow(sents) == 1) return(NA_character_)
  
  terms <- segment_sentences(text)
  textrank_sentences(data = sents, terminology = terms) %>%
    summary(n = 1)
}

# テスト用に10〜20件を処理(実際の運用時は全体を対象)
job_descriptions %>%
  slice(10:20) %>%
  mutate(
    doc_id = row_number(),
    sentence_count = str_count(hire_text, "[:space:]+") + 1
  ) %>%
  filter(sentence_count > 1) %>%
  mutate(summary = map_chr(hire_text, summarize_document)) %>%
  select(doc_id, sentence_count, summary)

TextRankはグラフ構造を前提とするため、大規模文書群への適用には計算コストが課題となります。しかし、中規模(数百〜数千文書)の業務用テキスト分析では、解釈性・再現性・非依存性(学習不要)という点で、TF-IDFやBERTベースのアプローチと比較しても十分な実用価値を持ちます。

タグ: TextRank r-language jiebaR natural-language-processing text-summarization

8月1日 18:22 投稿