ラストでクロスプラットフォームのTFIDFテキストサマライザーを構築する

美容とファッションの専門家が贈る、クロスプラットフォームのTFIDFテキストサマライザーを最後まで構築する方法' (Biyō to fasshon no senmonka ga okuru, kurosupurattofōmu no TFIDF tekisuto samaraizā o saigo made kōchiku suru hōhō)

RustにおけるクロスプラットフォームNLP

C/C++、Android、PythonでのRayonを使用した最適化

Patrick Tomassoによる写真(Unsplash)

NLPのツールとユーティリティはPythonのエコシステムで大きく成長し、開発者はさまざまなレベルでスケーラブルな高品質の言語アプリを作成できるようになりました。RustはNLPにおける新しい導入です。HuggingFaceなどの組織がRustを採用して機械学習のためのパッケージを構築しています。

Hugging FaceはRustで新しいMLフレームワークを作成し、オープンソース化しました!

最近、Hugging FaceはPythonとは異なる重量級のMLフレームワークであるCandleをオープンソース化しました…

VoAGI.com

このブログでは、TFIDFの概念を使用してテキスト要約器を作成する方法を探ります。まず、TFIDF要約がどのように機能するかと、なぜRustがNLPパイプラインを実装するのに適した言語であり、C/C++、Android、Pythonなどの他のプラットフォームでRustのコードを使用する方法を説明します。さらに、Rayonを使用して要約タスクを並列コンピューティングで最適化する方法についても説明します。

以下はGitHubプロジェクトです:

GitHub – shubham0204/tfidf-summarizer.rs:シンプルで効率的かつクロスプラットフォームのTFIDFベースのテキスト…

Rustでシンプルで効率的かつクロスプラットフォームのTFIDFベースのテキスト要約器 – GitHub – shubham0204/tfidf-summarizer.rs…

github.com

さあ始めましょう ➡️

目次

  1. 動機
  2. 抽出型と抽象型のテキスト要約
  3. TFIDFを使用したテキスト要約の理解
  4. Rustの実装
  5. Cとの使用方法
  6. 将来の展望
  7. 結論

動機

私は2019年に同じ技術を使用してテキスト要約器をKotlinで構築し、Text2Summaryと呼びました。これはAndroidアプリ向けのサイドプロジェクトで、すべての計算にKotlinを使用しました。2023年になり、私はC、C++、Rustのコードベースで作業し、これらのネイティブ言語で作成されたモジュールをAndroidとPythonで使用しています。

私はRustでText2Summaryを再実装することを選びました。これは素晴らしい学習体験になるだけでなく、大量のテキストを簡単に処理できる小さくて効率的なテキスト要約器として役立ちます。Rustは、バグのないコードを書くのを支援するインテリジェントな借用と参照のチェッカーを備えたコンパイル言語です。Rustで書かれたコードは、jniを介してJavaコードベースに統合でき、C/C++やPythonで使用するためにCのヘッダー/ライブラリに変換できます。

抽出型と抽象型のテキスト要約

テキスト要約は、自然言語処理(NLP)における長い研究対象です。テキストから重要な情報を抽出し、与えられたテキストの要約を生成することは、テキスト要約器が解決するべき核心的な問題です。解決策は、抽出型要約と抽象型要約の2つのカテゴリに属します。

自動テキスト要約の理解-1:抽出方法

どのようにして文書を自動的に要約できますか?

towardsdatascience.com

抽出型テキスト要約では、フレーズや文は直接文から派生します。スコアリング関数を使用して文をランク付けし、スコアに基づいて最適な文をテキストから選択します。生成型要約とは異なり、要約はテキストから選択された文の集合であり、生成モデルが抱える問題を回避します。

  • 抽出型要約ではテキストの精度が維持されますが、選択テキストの粒度が文に限定されるため、情報の一部が失われる可能性が高いです。情報が複数の文に分散している場合、スコアリング関数はそれらの文を含む関係に注意を払う必要があります。
  • 生成型テキスト要約では、言語の意味を捉え、適切な文書から要約へのマッピングを構築するために、より大きな深層学習モデルが必要です。このようなモデルの訓練には膨大なデータセットと長時間のトレーニングが必要で、それにより計算リソースが大幅に過負荷になります。事前学習済みモデルはトレーニング時間とデータの要求に対処するかもしれませんが、それでも訓練されたテキストのドメインに偏っています。
  • 抽出法はパラメータを持たず、学習を必要としないスコアリング関数を持つ場合もあります。これらはMLの非教示学習体制に属し、計算が少なく、テキストのドメインに偏りませんので有用です。要約はニュース記事や小説の抜粋と同じくらい効率的に行うことができます。

弊社のTFIDFベースの手法では、トレーニングデータセットや深層学習モデルは必要ありません。スコアリング関数は、文ごとの単語の相対出現頻度に基づいています。

TFIDFを用いたテキスト要約の理解

各文の順位付けには、文内に含まれる情報量を定量化するスコアを計算する必要があります。TF-IDFは、Term Frequency(単語の出現頻度)を表すTFとInverse Document Frequency(逆文書頻度)を示すIDFの2つの用語で構成されています。

PythonでスクラッチからTF(単語の出現頻度)-IDF(逆文書頻度)を作成する

スクラッチからTF-IDFモデルを作成する

towardsdatascience.com

各文はトークン(単語)で構成されていると考えます

Expr 1: Sentence S represented as tuple of words

Sの各単語の用語頻度は、次のように定義されます。

Expr 2: k represents the total number of words in the sentence.

文 S の各単語の逆文書頻度は、次のように定義されます。

Expr 3: The inverse-document frequency quantifies the occurrence of the word in other sentences.

各文のスコアは、その文のすべての単語のTFIDFスコアの合計です。

Expr 4: The score of each sentence S which determines its inclusion in the final summary.

意味と直感

単語の頻度は、文内で珍しい単語ほど少なくなることがわかるでしょう。同じ単語が他の文に出現しない場合、IDFスコアも高くなります。したがって、繰り返しの単語(高いTF)がその文に特有である場合(高いIDF)、TFIDFスコアが高くなります。

Rustの実装

この手法を実装するために、与えられたテキストを文のVecに変換する関数を作成します。この問題は文分割と呼ばれ、テキスト内の文の境界を特定します。Pythonのnltkパッケージには、punkt文分割器がこのタスクに使用でき、PunktのRustの移植版も存在します。ここではまだメンテナンスされていないrust-punktを使用しています。また、文を単語に分割する関数も書かれています。

use punkt::{SentenceTokenizer, TrainingData};use punkt::params::Standard;static STOPWORDS: [ &str ; 127 ] = [ "i", "me", "my", "myself", "we", "our", "ours", "ourselves", "you",     "your", "yours", "yourself", "yourselves", "he", "him", "his", "himself", "she", "her", "hers", "herself",     "it", "its", "itself", "they", "them", "their", "theirs", "themselves", "what", "which", "who", "whom", "this",     "that", "these", "those", "am", "is", "are", "was", "were", "be", "been", "being", "have", "has", "had", "having",      "do", "does", "did", "doing", "a", "an", "the", "and", "but", "if", "or", "because", "as", "until", "while", "of",      "at", "by", "for", "with", "about", "against", "between", "into", "through", "during", "before", "after", "above",     "below", "to", "from", "up", "down", "in", "out", "on", "off", "over", "under", "again", "further", "then", "once",       "here", "there", "when", "where", "why", "how", "all", "any", "both", "each", "few", "more", "most", "other",        "some", "such", "no", "nor", "not", "only", "own", "same", "so", "than", "too", "very", "s", "t", "can",        "will", "just", "don", "should", "now" ] ;/// `text`を文のリストに変換する/// 人気のあるPunkt文分割器(Rust版)を使用します: /// <`/`>https://github.com/ferristseng/rust-punkt<`/`>pub fn text_to_sentences( text: &str ) -> Vec<String> {    let english = TrainingData::english();    let mut sentences: Vec<String> = Vec::new() ;     for s in SentenceTokenizer::<Standard>::new(text, &english) {        sentences.push( s.to_owned() ) ;     }    sentences}/// 文を単語(トークン)のリストに変換する/// ストップワードを除去しながら行いますpub fn sentence_to_tokens( sentence: &str ) -> Vec<&str> {    let tokens: Vec<&str> = sentence.split_ascii_whitespace().collect() ;     let filtered_tokens: Vec<&str> = tokens                                .into_iter()                                .filter( |token| !STOPWORDS.contains( &token.to_lowercase().as_str() ) )                                .collect() ;    filtered_tokens}

上記のスニペットでは、言語でよく出現する単語であるストップワードを除去しています。これらの単語は、テキストの情報内容への重要な貢献を持ちません。

テキストの前処理: 異なるライブラリを使用したストップワードの除去

Pythonにおける英語ストップワード削除の便利なガイド!

towardsdatascience.com

次に、コーパス内の各単語の出現頻度を計算する関数を作成します。このメソッドは、文中の各単語の出現頻度を計算するために使用されます。 (word, freq) のペアは、後の段階でより高速に検索するために Hashmap に保存されます。

use std::collections::HashMap;/// 単語のリストが与えられた場合、頻度マップを作成する/// キーは単語で、値はそれらの単語の頻度です/// このメソッドは、文中の各単語の頻度を計算するために使用されますpub fn get_freq_map<'a>( words: &'a Vec<&'a str> ) -> HashMap<&'a str,usize> {    let mut freq_map: HashMap<&str,usize> = HashMap::new() ;     for word in words {        if freq_map.contains_key( word ) {            freq_map                .entry( word )                .and_modify( | e | {                     *e += 1 ;                 } ) ;         }        else {            freq_map.insert( *word , 1 ) ;         }    }    freq_map}

次に、文中の単語の頻度を計算する関数を記述します。

// 与えられた文(トークン化済み)における単語の頻度を計算する/// 単語 'w' の頻度 TF は次のように表される:/// TF(w) = (文中の w の出現頻度) / (文中のトークンの総数)fn compute_term_frequency<'a>(    tokenized_sentence: &'a Vec<&str>) -> HashMap<&'a str,f32> {    let words_frequencies = Tokenizer::get_freq_map( tokenized_sentence ) ;    let mut term_frequency: HashMap<&str,f32> = HashMap::new() ;      let num_tokens = tokenized_sentence.len() ;     for (word , count) in words_frequencies {        term_frequency.insert( word , ( count as f32 ) / ( num_tokens as f32 ) ) ;     }    term_frequency}

トークン化された文における単語の逆文書頻度(IDF)を計算する別の関数もあります。

// 与えられた文(トークン化済み)におけるトークンの逆文書頻度(IDF)を計算する/// 単語 'w' の逆文書頻度 IDF は次のように表される:/// IDF(w) = log( N / (w が現れる文書の数) )fn compute_inverse_doc_frequency<'a>(    tokenized_sentence: &'a Vec<&str> ,    tokens: &'a Vec<Vec<&'a str>>) -> HashMap<&'a str,f32> {    let num_docs = tokens.len() as f32 ;     let mut idf: HashMap<&str,f32> = HashMap::new() ;     for word in tokenized_sentence {        let mut word_count_in_docs: usize = 0 ;         for doc in tokens {            word_count_in_docs += doc.iter().filter( |&token| token == word ).count() ;        }        idf.insert( word , ( (num_docs) / (word_count_in_docs as f32) ).log10() ) ;    }    idf}

これで、文中の各単語のTFとIDFスコアを計算する関数が追加されました。また、文のランクも決定するために、文中のすべての単語のTFIDFスコアの合計を計算する必要があります。

pub fn compute(     text: &str ,     reduction_factor: f32 ) -> String {    let sentences_owned: Vec<String> = Tokenizer::text_to_sentences( text ) ;     let mut sentences: Vec<&str> = sentences_owned                                            .iter()                                            .map( String::as_str )                                            .collect() ;     let mut tokens: Vec<Vec<&str>> = Vec::new() ;     for sentence in &sentences {        tokens.push( Tokenizer::sentence_to_tokens(sentence) ) ;     }    let mut sentence_scores: HashMap<&str,f32> = HashMap::new() ;        for ( i , tokenized_sentence ) in tokens.iter().enumerate() {        let tf: HashMap<&str,f32> = Summarizer::compute_term_frequency(tokenized_sentence) ;         let idf: HashMap<&str,f32> = Summarizer::compute_inverse_doc_frequency(tokenized_sentence, &tokens) ;         let mut tfidf_sum: f32 = 0.0 ;         // 各単語のTFIDFスコアを計算し、tfidf_sumに追加する         for word in tokenized_sentence {            tfidf_sum += tf.get( word ).unwrap() * idf.get( word ).unwrap() ;         }        sentence_scores.insert( sentences[i] , tfidf_sum ) ;     }    // スコアで文をソート    sentences.sort_by( | a , b |         sentence_scores.get(b).unwrap().total_cmp(sentence_scores.get(a).unwrap()) ) ;     // サマリに含める文の数を計算し、抽出されたサマリを返す    let num_summary_sents = (reduction_factor * (sentences.len() as f32) ) as usize;    sentences[ 0..num_summary_sents ].join( " " )}

レーヨンの使用

大きなテキストの場合、人気のあるRust crate rayon-rsを使用して複数のCPUスレッドで並列処理ができます。上記のcompute関数では、次のタスクを並行して実行できます。

  • 各文をトークン化してストップワードを削除する
  • 各文のTFIDFスコアの合計を計算する

これらのタスクは、それぞれの文で独立して実行され、他の文に依存しないため、並列化することができます。共有コンテナへのアクセス時に相互排除を確保するために、共有されたArc (原子参照カウントポインタ)と、アトミックアクセスを保証するための基本的な同期プリミティブであるMutexを使用します。

Arcは、参照されているMutexがすべてのスレッドからアクセス可能であることを保証し、Mutex自体はその中に包まれたオブジェクトに対して1つのスレッドだけがアクセスできるようにします。以下に、Rayonを使用して上記のタスクを並列に実行するpar_compute関数があります。

pub fn par_compute(     text: &str ,     reduction_factor: f32 ) -> String {    let sentences_owned: Vec<String> = Tokenizer::text_to_sentences( text ) ;     let mut sentences: Vec<&str> = sentences_owned                                            .iter()                                            .map( String::as_str )                                            .collect() ;         // Rayonを使用して文をトークン化する    // トークン化された文を保持するスレッドセーフなVec<Vec<&str>>を宣言    let tokens_ptr: Arc<Mutex<Vec<Vec<&str>>>> = Arc::new( Mutex::new( Vec::new() ) ) ;     sentences.par_iter()             .for_each( |sentence| {                 let sent_tokens: Vec<&str> = Tokenizer::sentence_to_tokens(sentence) ;                 tokens_ptr.lock().unwrap().push( sent_tokens ) ;              } ) ;     let tokens = tokens_ptr.lock().unwrap() ;     // 文のスコアを並列計算する    // スコアが格納されるスレッドセーフなHashmap<&str,f32>を宣言    let sentence_scores_ptr: Arc<Mutex<HashMap<&str,f32>>> = Arc::new( Mutex::new( HashMap::new() ) ) ;     tokens.par_iter()          .zip( sentences.par_iter() )          .for_each( |(tokenized_sentence , sentence)| {        let tf: HashMap<&str,f32> = Summarizer::compute_term_frequency(tokenized_sentence) ;         let idf: HashMap<&str,f32> = Summarizer::compute_inverse_doc_frequency(tokenized_sentence, &tokens ) ;         let mut tfidf_sum: f32 = 0.0 ;                 for word in tokenized_sentence {            tfidf_sum += tf.get( word ).unwrap() * idf.get( word ).unwrap() ;         }        tfidf_sum /= tokenized_sentence.len() as f32 ;         sentence_scores_ptr.lock().unwrap().insert( sentence , tfidf_sum ) ;     } ) ;     let sentence_scores = sentence_scores_ptr.lock().unwrap() ;    // スコアによって文をソート    sentences.sort_by( | a , b |         sentence_scores.get(b).unwrap().total_cmp(sentence_scores.get(a).unwrap()) ) ;     // サマリに含まれる文の数を計算し、抽出したサマリを返す    let num_summary_sents = (reduction_factor * (sentences.len() as f32) ) as usize;    sentences[ 0..num_summary_sents ].join( ". " ) }

クロスプラットフォームの使用

CおよびC++

CでRustの構造体や関数を使用するには、cbindgenを使用して、構造体/関数のプロトタイプを含むCスタイルのヘッダーを生成することができます。ヘッダーを生成した後、ヘッダファイルで宣言された関数の実装を含むCベースのダイナミックまたはスタティックライブラリにRustコードをコンパイルできます。Cベースのスタティックライブラリを生成するには、Cargo.tomlcrate_typeパラメータをstaticlibに設定する必要があります。

[lib]name = "summarizer"crate_type = [ "staticlib" ]

次に、summarizerの関数をABI(アプリケーションバイナリインターフェース)で公開するためのFFIをsrc/lib.rsに追加します。

/// 関数:RustのメソッドをCインターフェースとして公開する
/// これらのメソッドは、ABI(コンパイルされたオブジェクトコード)でアクセスできるmod c_binding {    use std::ffi::CString;    use crate::summarizer::Summarizer;    #[no_mangle]    pub extern "C" fn summarize( text: *const u8 , length: usize , reduction_factor: f32 ) -> *const u8 {        ...      }    #[no_mangle]    pub extern "C" fn par_summarize( text: *const u8 , length: usize , reduction_factor: f32 ) -> *const u8 {        ...    }}

静的ライブラリはcargo buildでビルドでき、targetディレクトリにlibsummarizer.aが生成されます。

Android

AndroidのNative Development Kit(NDK)を使用して、Rustプログラムをarmeabi-v7aarm64-v8aのターゲットにコンパイルできます。 JNI(Java Native Interface)を使用した特別なインターフェース関数を記述する必要があります。これらはsrc/lib.rsandroidモジュールに見つけることができます。

ネイティブコードへのKotlin JNI

Kotlinからネイティブコードを呼び出す方法。

matt-moore.medium.com

Python

Pythonのctypesモジュールを使用すると、共有ライブラリ(.soまたは.dll)をロードし、ライブラリで定義された関数を実行するためにC互換のデータ型を使用できます。このコードはGitHubプロジェクトでは利用できませんが、近々利用できるようになります。

Pythonバインディング:PythonからCまたはC++を呼び出す – Real Python

Pythonバインディングとは何ですか? ctypes、CFFI、または別のツールを使用するべきですか? このステップバイステップのチュートリアルでは、…

realpython.com

将来の展望

このプロジェクトはさまざまな方法で拡張および改善することができます。以下で詳しく説明します:

  1. 現在の実装では、punktの依存関係のためにnightly Rustのビルドが必要です。 punktはテキスト内の文の境界を判定するために必要な文分割ツールです。文の境界が判定された後、他の計算が行われます。もしpunktが安定したRustでビルドできるようになれば、現在の実装にはnightly Rustが不要になります。
  2. 文間の依存関係をキャプチャする新しい評価基準を追加すること、特に文のランキングに関するものです。TFIDFは最も正確なスコアリング関数ではなく、独自の制限があります。文のグラフを構築し、それらを使って文をスコアリングすることで、抽出された要約の全体的な品質が大幅に向上します。
  3. 要約器は既知のデータセットとのベンチマークテストが行われていません。 Rougeスコア R1 , R2 および RLは、ニューヨークタイムズデータセットやCNNデイリーメールデータセットなどの標準データセットに対する生成された要約の品質を評価するために頻繁に使用されます。標準ベンチマークに対するパフォーマンスを測定することで、開発者は実装に対するより明確で信頼性のある情報を得ることができます。

結論

デベロッパーの間での人気が高まっているため、Rustを使用してNLPユーティリティを構築することは非常に利点があります。その性能と将来の約束を考慮すると、この記事が有益であることを願っています。GitHubのプロジェクトもご覧になってください:

GitHub – shubham0204/tfidf-summarizer.rs: Rustでシンプルで効率的かつクロスプラットフォームなTFIDFベースのテキスト…

Rustでシンプルで効率的かつクロスプラットフォームなTFIDFベースのテキスト要約アルゴリズム – GitHub – shubham0204/tfidf-summarizer.rs…

github.com

何か改善できると感じた場合は、問題を開いたり、プルリクエストを作成したりすることを検討してください!引き続き学びながら、素晴らしい一日をお過ごしください。

We will continue to update VoAGI; if you have any questions or suggestions, please contact us!

Share:

Was this article helpful?

93 out of 132 found this helpful

Discover more