私はキーワード抽出タスクのための効率的なアルゴリズムを探していました。 目標は、データ コーパスが急速に増加し、すでに数百万行に達しているため、効率的にキーワードを抽出し、抽出品質と実行時間のバランスをとることができるアルゴリズムを見つけることです。 アルゴリズムに対する私の主な要件の 1 つは、抽出されたキーワード自体が常に意味を持ち、文脈から外れても特定の意味を表現できることです。 [[437010]] この記事では、2000 件のドキュメントのコーパスを使用して、いくつかのよく知られているキーワード抽出アルゴリズムをテストおよび実験します。 使用されるライブラリのリスト私は研究のために以下のPythonライブラリを使用しました 前処理段階といくつかのヘルパー関数を支援するNLTK Pandas、Matplotlib、その他の一般的なライブラリ 実験手順ベンチマークは次のように機能します まず、テキスト データを含むデータセットをインポートします。 次に、各アルゴリズムごとにロジックを抽出する個別の関数を作成します。 algorithm_name(str: text) → [キーワード1, キーワード2, ..., キーワードn] 次に、コーパス全体からキーワードを抽出する機能を作成しました。 extract_keywords_from_corpus(アルゴリズム、コーパス) → {アルゴリズム、コーパスキーワード、経過時間} 次に、Spacy を使用して、キーワードがタスクに適している場合に true または false を返すマッチャー オブジェクトを定義します。 最後に、最終レポートを出力する関数ですべてをまとめます。 データセット私はインターネットからの小さなテキスト データ セットを使用しています。これはサンプルです - [ '前回の質問の続きです。これが結果です!\n' 、
- 「ヨーロッパのミード コンテスト?\nミードについてのフィードバックが欲しいのですが、ヨーロッパから米国にアルコールを輸送するのは違法なので、Mazer Cup に参加するのは私には無理です。(おそらく捕まったり起訴されたりはしないと思いますが、問題の公式記録が残ると、今後の市民権申請に支障が出る可能性があり、そのリスクを冒すつもりはありません)。\n\nヨーロッパのミード コンテストはありますか? または、少なくともミード カテゴリーへのエントリーを受け付け、経験豊富なミード審査員がいる可能性のある大規模なビール コンテストはありますか?' , 'オレンジ ローズマリー ブーチ\n' , 'ついに実現しました。休暇で出かけ、家に帰ったらカビが生えていました。\n' , '金曜日にロンドンでジェラート ショップをオープンするので、ずっとフレーバーの練習をしていました。これが最近の試みの 1 つです!\n' , "常温保存可能なホット ソースを作るためのリソースを持っている人はいませんか? 発酵させてから水に浸すか、圧力缶に入れるか?\n新鮮な唐辛子が何十本もありますホットソースを作るのに使いたいのですが、最終的な目標はレシピをカスタマイズしてアメリカ中の友達に送ることです。缶詰にするのが一番いい方法だと思うのですが、詳しい情報があまり見つかりません。何かアドバイスはありますか? 、ワインフィルターとウォーターフィルターの実際的な違いは何ですか?\nどちらも使えるか知りたいです'、'最高のカスタードベースは何ですか?\nカルバーズフローズンカスタードに似た味のレシピを持っている人はいますか? '、'カビ?\n'
ほとんどが食品に関するものです。アルゴリズムをテストするために、2000 件のドキュメントのサンプルを使用します。 一部のアルゴリズムの結果はストップワードと句読点に基づいているため、テキストはまだ前処理されていません。 アルゴリズムキーワード抽出関数を定義しましょう。 - #関数外でBERTを開始する
- bert = キーBERT()
- # 1. レーキ
- def rake_extractor(テキスト):
- 「」 「 」
- Rakeを使用してテキストから上位5つのキーワードを抽出します
- 引数: テキスト (str)
- 戻り値:キーワードのリスト(リスト)
- 「」 「 」
- r = レーキ()
- r.extract_keywords_from_text(テキスト)
- r.get_ranked_phrases()[:5]を返す
- # 2. ヤケ
- def yake_extractor(テキスト):
- 「」 「 」
- YAKEを使用してテキストから上位5つのキーワードを抽出します
- 引数: テキスト (str)
- 戻り値:キーワードのリスト(リスト)
- 「」 「 」
- キーワード = yake.KeywordExtractor(lan= "en" 、n=3、windowsSize=3、 top =5).extract_keywords(テキスト)
- 結果 = []
- キーワードのscored_keywordsの場合:
- scored_keywords内のキーワード:
- if isinstance(キーワード, str):
- results.append(キーワード)
- 結果を返す
- # 3. ポジションランク
- def position_rank_extractor(テキスト):
- 「」 「 」
- PositionRankを使用してテキストから上位5つのキーワードを抽出します
- 引数: テキスト (str)
- 戻り値:キーワードのリスト(リスト)
- 「」 「 」
- #グラフに出現する有効な品詞を定義する
- pos = { '名詞' 、 '固有名詞' 、 '形容詞' 、 '副詞' }
- 抽出子 = pke.unsupervised.PositionRank()
- extractor.load_document(テキスト、言語= 'en' )
- extractor.candidate_selection(pos=pos, 最大単語数=5)
- # 4.合計を使用して候補に重み付けする 彼らの単語のスコアは
- #単語の位置に応じてランダムウォークを使用して計算
- #ドキュメント内のノード。グラフでは、ノードは単語(名詞と
- #形容詞のみ)が、
- # 3 単語。
- extractor.candidate_weighting(ウィンドウ=3、pos=pos)
- # 5. 最もスコアの高い5つの候補をキーフレーズとして取得する
- キーフレーズ = extractor.get_n_best(n=5)
- 結果 = []
- キーフレーズ内のscored_keywordsの場合:
- scored_keywords内のキーワード:
- if isinstance(キーワード, str):
- results.append(キーワード)
- 結果を返す
- # 4. シングルランク
- def single_rank_extractor(テキスト):
- 「」 「 」
- SingleRankを使用してテキストから上位5つのキーワードを抽出します
- 引数: テキスト (str)
- 戻り値:キーワードのリスト(リスト)
- 「」 「 」
- pos = { '名詞' 、 '固有名詞' 、 '形容詞' 、 '副詞' }
- 抽出器 = pke.unsupervised.SingleRank()
- extractor.load_document(テキスト、言語= 'en' )
- 抽出子.候補選択(pos=pos)
- extractor.candidate_weighting(ウィンドウ=3、pos=pos)
- キーフレーズ = extractor.get_n_best(n=5)
- 結果 = []
- キーフレーズ内のscored_keywordsの場合:
- scored_keywords内のキーワード:
- if isinstance(キーワード, str):
- results.append(キーワード)
- 結果を返す
- # 5. マルチパートランク
- def multipartite_rank_extractor(テキスト):
- 「」 「 」
- MultipartiteRankを使用してテキストから上位5つのキーワードを抽出します
- 引数: テキスト (str)
- 戻り値:キーワードのリスト(リスト)
- 「」 「 」
- 抽出器 = pke.unsupervised.MultipartiteRank()
- extractor.load_document(テキスト、言語= 'en' )
- pos = { '名詞' 、 '固有名詞' 、 '形容詞' 、 '副詞' }
- 抽出子.候補選択(pos=pos)
- # 4. 多部グラフを構築し、ランダムウォークを使用して候補をランク付けする。
- # アルファは重み調整メカニズムを制御します。
- # しきい値/メソッド パラメータ。
- extractor.candidate_weighting(アルファ=1.1、しきい値=0.74、方法= '平均' )
- キーフレーズ = extractor.get_n_best(n=5)
- 結果 = []
- キーフレーズ内のscored_keywordsの場合:
- scored_keywords内のキーワード:
- if isinstance(キーワード, str):
- results.append(キーワード)
- 結果を返す
- # 6. トピックランク
- def topic_rank_extractor(テキスト):
- 「」 「 」
- TopicRankを使用してテキストから上位5つのキーワードを抽出します
- 引数: テキスト (str)
- 戻り値:キーワードのリスト(リスト)
- 「」 「 」
- 抽出子 = pke.unsupervised.TopicRank()
- extractor.load_document(テキスト、言語= 'en' )
- pos = { '名詞' 、 '固有名詞' 、 '形容詞' 、 '副詞' }
- 抽出子.候補選択(pos=pos)
- 抽出子.候補の重み付け()
- キーフレーズ = extractor.get_n_best(n=5)
- 結果 = []
- キーフレーズ内のscored_keywordsの場合:
- scored_keywords内のキーワード:
- if isinstance(キーワード, str):
- results.append(キーワード)
- 結果を返す
- # 7. キーバート
- def keybert_extractor(テキスト):
- 「」 「 」
- KeyBERTを使用してテキストから上位5つのキーワードを抽出します
- 引数: テキスト (str)
- 戻り値:キーワードのリスト(リスト)
- 「」 「 」
- キーワード = bert.extract_keywords(テキスト、キーフレーズnグラム範囲=(3, 5)、ストップワード= "英語" 、トップn=5)
- 結果 = []
- キーワードのscored_keywordsの場合:
- scored_keywords内のキーワード:
- if isinstance(キーワード, str):
- results.append(キーワード)
- 結果を返す
各抽出機能はテキストを入力として受け取り、キーワードのリストを返します。使い方はとても簡単です。 注: 何らかの理由により、関数の外部ですべての抽出オブジェクトを初期化することはできません。これを実行するたびに、TopicRank と MultiPartiteRank でエラーが発生します。パフォーマンスの点では完璧ではありませんが、ベンチマークでは十分な成果が得られます。 pos = {'NOUN', 'PROPN', 'ADJ', 'ADV'} を渡すことで、許容される文法パターンを制限しました。これを Spacy と組み合わせることで、ほぼすべてのキーワードが人間の言語の観点から選択されるようになります。 また、キーワードをより具体的にし、一般的になりすぎないようにするために、キーワードには 3 つの単語を含める必要があります。 コーパス全体からキーワードを抽出するここで、いくつかの情報を出力しながら、コーパス全体に単一の抽出器を適用する関数を定義しましょう。 - def extract_keywords_from_corpus(抽出器、コーパス):
- "" "この関数は、抽出器を使用してドキュメントのリストからキーワードを取得します" ""
- extractor_name = extractor.__name__. replace ( "_extractor" , "" )
- logging.info(f "{extractor_name} でキーワード抽出を開始しています" )
- コーパス_kws = {}
- 開始 =時間.時間()
- #logging.info(f "タイマーが開始されました。" ) <
- idxの場合、text in tqdm(enumerate(corpus), desc = "コーパスからキーワードを抽出しています..." ):
- corpus_kws[idx] = 抽出子(テキスト)
- 終了=時間.時間()
- #logging.info(f "タイマーが停止しました。" ) <
- 経過時間 = time .strftime( "%H:%M:%S" , time .gmtime(終了- 開始))
- logging.info(f "経過時間: {elapsed}" )
-
- 戻り値{ "アルゴリズム" : extractor.__name__,
- "コーパス_kws" : コーパス_kws,
- "elapsed_time" : 経過時間}
この関数は、受信した抽出データを、タスクの実行にかかった時間などの一連の有用な情報を含む辞書に結合して、後でレポートを生成しやすくします。 構文マッチング関数この関数は、抽出器によって返されるキーワードが常に(ほぼ?)意味を成すことを保証します。 例えば、 最初の 3 つのキーワードは、単独でも意味を成すものであることがはっきりとわかります。キーワードの意味を理解するためにこれ以上の情報は必要ありませんが、4番目は意味がないので、これをできるだけ避ける必要があります。 Spacy と Matcher オブジェクトは、これを実現するのに役立ちます。 キーワードを受け取り、定義されたパターンが一致する場合に True または False を返す match 関数を定義します。 - def match(キーワード):
- "" "この関数は、キーワードのリストが特定の POS パターンに一致するかどうかを確認します" ""
- パターン = [
- [{ 'POS' : 'PROPN' }, { 'POS' : 'VERB' }, { 'POS' : 'VERB' }],
- [{ 'POS' : '名詞' }, { 'POS' : '動詞' }, { 'POS' : '名詞' }],
- [{ 'POS' : '動詞' }, { 'POS' : '名詞' }],
- [{ 'POS' : 'ADJ' }, { 'POS' : 'ADJ' }, { 'POS' : '名詞' }],
- [{ 'POS' : '名詞' }, { 'POS' : '動詞' }],
- [{ 'POS' : 'PROPN' }, { 'POS' : 'PROPN' }, { 'POS' : 'PROPN' }],
- [{ 'POS' : 'PROPN' }, { 'POS' : 'PROPN' }, { 'POS' : 'NOUN' }],
- [{ 'POS' : 'ADJ' }, { 'POS' : '名詞' }],
- [{ 'POS' : 'ADJ' }, { 'POS' : '名詞' }, { 'POS' : '名詞' }, { 'POS' : '名詞' }, { 'POS' : '名詞' }],
- [{ 'POS' : 'PROPN' }, { 'POS' : 'PROPN' }, { 'POS' : 'PROPN' }, { 'POS' : 'PROPN' }, { 'POS' : 'ADV' }, { 'POS' : 'PROPN' }],
- [{ 'POS' : 'PROPN' }, { 'POS' : 'PROPN' }, { 'POS' : 'PROPN' }, { 'POS' : 'VERB' }],
- [{ 'POS' : 'PROPN' }, { 'POS' : 'PROPN' }],
- [{ 'POS' : '名詞' }, { 'POS' : '名詞' }],
- [{ 'POS' : 'ADJ' }, { 'POS' : 'PROPN' }],
- [{ 'POS' : 'PROPN' },{ 'POS' : 'ADP' },{ 'POS' : 'PROPN' }],
- [{ 'POS' : '属性' }, { 'POS' : '形容詞' }, { 'POS' : '名詞' }],
- [{ 'POS' : '属性' }, { 'POS' : '動詞' }, { 'POS' : '名詞' }],
- [{ 'POS' : '名詞' }, { 'POS' : 'ADP' }, { 'POS' : '名詞' }],
- [{ 'POS' : 'PROPN' }, { 'POS' : '名詞' }, { 'POS' : 'PROPN' }],
- [{ '品詞' : '動詞' }, { '品詞' : '副詞' }],
- [{ 'POS' : '属性' }, { 'POS' : '名詞' }],
- ]
- マッチャー = Matcher(nlp.vocab)
- matcher. add ( "pos-matcher" , パターン)
- #スペースオブジェクトを作成する
- doc = nlp(キーワード)
- # 一致した項目を反復処理する
- マッチ = マッチャー(doc)
- # 一致する場合は 空でない場合は、少なくとも一致が見つかったことを意味します
- len(matches) > 0の場合:
- 戻る 真実
- 戻る 間違い
ベンチマーク機能もうすぐそこに着きます。 これは、スクリプトを起動して結果を収集する前の最後のステップです。 コーパスとデータをシャッフルするためのブール値を受け取るベンチマーク関数を定義します。 各抽出器に対して、 extract_keywords_from_corpus 関数は、この抽出器の結果を含む辞書を返します。 値をリストに保存します。 リスト内の各アルゴリズムについて、計算する。 - 抽出されたキーワードの平均数
- 一致するキーワードの平均数
- 見つかった一致の平均数を操作の実行にかかった時間で割ったスコアを計算します。
すべてのデータを Pandas DataFrame に保存し、.csv としてエクスポートします。 - get_sec(time_str)を定義します。
- 「時間から秒数を取得します。」 「」
- h, m, s = time_str.split( ':' )
- 戻る 整数(h) * 3600 +整数(m) * 60 +整数(s)
- def benchmark(コーパス、シャッフル= True ):
- "" "この関数は、キーワード抽出アルゴリズムのベンチマークを実行します" ""
- logging.info( "ベンチマークを開始しています...\n" )
-
- # コーパスをシャッフルする
- シャッフルの場合:
- ランダムシャッフル(コーパス)
- #コーパスからキーワードを抽出
- 結果 = []
- 抽出器 = [
- rake_extractor、
- yake_extractor、
- トピックランク抽出器、
- ポジションランク抽出器、
- シングルランク抽出器、
- マルチパートランク抽出器、
- キーバート抽出器、
- ]
- 抽出器内の抽出器の場合:
- 結果 = extract_keywords_from_corpus(抽出器、コーパス)
- 結果.append(結果)
- #抽出されたキーワードの平均数を計算する
- 結果の結果として:
- len_of_kw_list = []
- result[ "corpus_kws" ] .values ()内のkwsについて:
- len_of_kw_list.append(len(kws))
- 結果[ "文書あたりの平均キーワード数" ] = np.mean(len_of_kw_list)
- # 一致するキーワード
- 結果の結果として:
- result[ "corpus_kws" ].items()のidx、kwsの場合:
- マッチ結果 = []
- kws内のkwの場合:
- match_results.append(match(kw))
- 結果[ "corpus_kws" ][idx] = 一致結果
- #一致したキーワードの平均数を計算
- 結果の結果として:
- 一致するキーワードリストの長さ = []
- result[ "corpus_kws" ].items()のidx、kwsの場合:
- len_of_matching_kws_list.append(len([kw for kw in kws if kw]))
- 結果[ "ドキュメントあたりの平均一致キーワード数" ] = np.mean(一致するキーワードリストの長さ)
- # 一致するキーワードの平均パーセンテージを計算し、小数点第 2 位を四捨五入します
- 結果[ "一致したキーワードの平均割合" ] = round(結果[ "文書あたり一致したキーワードの平均割合" ] / 結果[ "文書あたり一致したキーワードの平均割合" ], 2)
-
- #一致したキーワードの平均割合を 経過時間(秒)
- 結果の結果として:
- 経過秒数 = get_sec(結果[ "経過時間" ]) + 0.1
- #経過時間に基づいてスコアを重み付けする
- 結果[ "パフォーマンススコア" ] = round(結果[ "ドキュメントあたりの平均一致キーワード数" ] / 経過秒数、2)
-
- # corpus_kwを削除
- 結果の結果として:
- del結果[ "corpus_kws" ]
- #結果データフレームを作成する
- df = pd.DataFrame(結果)
- df.to_csv( "results.csv" 、インデックス= False )
- logging.info( "ベンチマークが終了しました。結果は results.csv に保存されました" )
- リターンDF
結果- 結果 = ベンチマーク(テキスト[:2000]、シャッフル= True )
生成されたレポートはこちら これを視覚化してみましょう: 私たちが定義したスコアリング式によれば( avg_matched_keywords_per_document/time_elapsed_in_seconds)、Rake は 2 秒で 2000 件のドキュメントを処理し、精度は KeyBERT ほど良くないものの、時間要因で勝者になります。 正確さだけを考えれば、計算は avg_matched_keywords_per_documentとavg_keywords_per_documentの比率から次の結果が得られます。 精度の観点から見ると、Rake のパフォーマンスもかなり優れています。時間を考慮しなければ、KeyBERT は間違いなく最も正確で意味のあるキーワード抽出アルゴリズムになるでしょう。 Rake は精度では 2 位にランクされましたが、大きく遅れていました。 精度が必要な場合は、KeyBERT が間違いなく第一の選択肢です。速度が必要な場合は、Rake が間違いなく第一の選択肢です。これは、速度が速く、精度も許容範囲内であるためです。 |