テキストをベクトルに変換する:TSDAEによる強化埋め込みの非教示アプローチ

「テキストのベクトル変換:TSDAEを用いた非教示の強化埋め込み手法」

Freepikによるデザイン

専門分野の埋め込みを強化するために、ターゲットドメインでのTSDAE事前学習と一般的なコーパスでの教師付き微調整を組み合わせます。

イントロダクション

埋め込みは、単語を表すための密なベクトルを使用して、テキストを高次元のベクトル空間にエンコードし、単語間の意味的な関係を捉えます。コンテキスト検索やRAGなど、生成AIとLLMの最新の開発では、その基盤となる埋め込みの品質が重要です。類似検索はコサイン類似性などの基本的な数学的概念を使用しますが、埋め込みベクトルの構築方法は後続の結果に大きな影響を与えます。

ほとんどの場合、事前学習済みのセンテンストランスフォーマーはそのまま使え、合理的な結果が得られます。これらの場合、BERTベースの事前学習済みコンテキスト埋め込みの選択肢も多くありますが、いくつかは特定のドメインに特化しており、HuggingFaceなどのプラットフォームからダウンロードできます。

しかし、コーパスには狭いドメインに特有の技術用語や低リソース言語のテキストが含まれる場合、問題が発生します。このような場合、事前学習や微調整の際に見られなかった未知の単語に対処する必要があります。

例えば、一般的なテキストで事前トレーニングされたモデルが数学の研究論文のタイトルに正確にベクトルを割り当てるのは難しいです。

これらの場合、ドメイン固有の単語にモデルが触れていないため、その意味を正確に判断し、コーパスの他の単語とのベクトル空間内で正確に配置することが困難です。未知の単語の数が多いほど、影響が大きく、モデルのパフォーマンスが低下します。

そのため、箱から出して使える事前学習済みモデルは、このようなシナリオでは性能が低くなります。一方、カスタムモデルの事前トレーニングは、ラベル付きデータの欠如と大量の計算リソースの必要性により、困難を伴います。

動機

この研究は、航空ドメインを対象とした最近の研究[aviation_article]に触発されたもので、そのデータには技術用語、省略形、非伝統的な文法など、独自の特徴があります。

ラベル付きデータの不足に対処するために、著者たちは事前学習埋め込み(TSDAE)を用いた事前トレーニングと一般コーパスからのラベル付きデータを用いた微調整段階を組み合わせました。適応型センテンストランスフォーマーは、一般的なトランスフォーマーよりも優れた性能を発揮し、航空ドメインデータの特徴を捉える手法の効果を実証しています。

アウトライン

ドメイン適応は、ラベル付きトレーニングデータを必要とせずにテキスト埋め込みを特定のドメインに合わせることです。この実験では、対象ドメインでのトレーニングだけよりも効果的とされる2段階のアプローチを使用しています。

著者によるイメージ

第一に、適合型事前トレーニングとも呼ばれる対象ドメインに焦点を当てた事前トレーニングから始めます。この段階では、データセットからの文の収集が求められます。このステージでは、TSDAEを使用します。この方法は事前トレーニングタスクとしてのドメイン適応に優れており、他のマスクされた言語モデルを含む他の方法を大幅に上回ると、[tsdae_article]で強調されています。私は、train_tsdae_from_file.pyスクリプトに厳密に従います。

その後、一般的なラベル付きAllNLIデータセットでモデルを微調整し、マルチネガティブランキング損失戦略を使用します。このステージでは、training_nli_v2.pyのスクリプトを使用します。[tsdae_article]に記載されているように、この追加ステップは過学習を防ぐだけでなく、モデルの性能を大幅に向上させます。

TSDAE — ターゲットドメインでの事前トレーニング

TSDAE(トランスフォーマーベースのシーケンシャルデノイジングオートエンコーダー)は、非教示学習の文章埋め込み手法であり、最初にK. Wang、N. Reimers、I. Gurevychによって[tsdae_article]で紹介されました。

TSDAEは、クロスアテンションのキーと値が文の埋め込みに制限された修正されたエンコーダーデコーダーのトランスフォーマーデザインを使用します。オリジナルの論文[tsdae_article]で強調された最適なアーキテクチャの選択肢の文脈で詳細を説明します。

Image by the Author
  • データセットはラベルなしの文章で構成されており、前処理中に60%の内容を削除して入力ノイズを導入します。
  • エンコーダーは、単語の埋め込みをプーリングして固定サイズのベクトルに変換します。[tsdae_article]によれば、文ベクトルを抽出するためにCLSプーリングメソッドの使用が推奨されています。
  • デコーダーは、損傷した文の埋め込みから元の入力文を再構築する必要があります。著者は、トレーニング中にエンコーダーとデコーダーのパラメータを結びつけることで、モデルのパラメータ数を減らし、トレーニングが容易になり、過学習しにくくなり、パフォーマンスに影響を与えないようにすることをアドバイスしています。

良好な再構築品質のために、エンコーダーからの文の埋め込みは意味を最適に捉える必要があります。エンコーダーには、bert-base-uncasedなどの事前トレーニング済みのトランスフォーマーが使用され、デコーダーの重みはそれからコピーされます。

デコーダーのアテンションメカニズムは、エンコーダーによって生成された文の表現に制限されます。これはオリジナルのトランスフォーマーエンコーダーデコーダーアーキテクチャとの変更であり、デコーダーがエンコーダーから取得する情報を制限し、エンコーダーが意味のある文の表現を生成するようにするボトルネックを導入します。

推論では、エンコーダーのみが使用されて文の埋め込みが作成されます。

モデルは、損傷した文からクリーンな文を再構築するようにトレーニングされ、これは目的関数を最大化することによって達成されます:

Image by the Author

AllNLI — ナチュラルランゲージインフェレンスデータセット

ナチュラルランゲージインフェレンス(NLI)は、2つの文の関係を決定します。それは、仮説(2番目の文)の真偽を、推論(前提に基づいて true)、矛盾(前提に基づいて false)、または中立(前提に保証も否定もされない)のいずれかに分類します。NLIデータセットは、文のペアが関係クラスで注釈付けされた大規模なラベル付きデータセットです。

この実験では、Stanford Natural Language Inference(SNLI)とMultiNLIデータセットを組み合わせた、900,000以上のレコードを含むAllNLIデータセットを使用しています。このデータセットは以下からダウンロードできます: AllNLI download site

事前トレーニングデータの読み込みと準備

ドメイン固有のデータを構築するために、私たちはKaggle arXivデータセットを使用しています。このデータセットは、確立されたエレクトロニックプレプリントプラットフォームであるarXivから収集された約1.7Mの学術STEM論文で構成されています。タイトル、要約、著者以外にも、各記事には多くのメタデータが関連付けられています。ただし、ここではタイトルのみに関心があります。

ダウンロード後、数学のプレプリントを選択します。Kaggleファイルのサイズが大きいため、Githubに数学の論文ファイルの縮小版を追加しました。ただし、別の主題に興味がある場合は、データセットをダウンロードし、次のコード内のmathを希望するトピックに置き換えてください:

# サブジェクトが "math" の論文を収集するdef extract_entries_with_math(filename: str) -> List[str]:    """    文字列 'math' を 'id' に含むエントリを抽出する関数。    """    # 抽出したエントリを格納する空のリストを初期化します。    entries_with_math = []    with open(filename, 'r') as f:        for line in f:            try:                # JSONオブジェクトを行から読み込む                data = json.loads(line)                # "id" キーが存在し、それが "math" を含むかどうかをチェック                if "id" in data and "math" in data["id"]:                    entries_with_math.append(data)            except json.JSONDecodeError:                # この行が有効なJSONではない場合はエラーメッセージを出力する                print(f"解析できませんでした:{line}")    return entries_with_math# 数学の論文を抽出します。entries = extract_entries_with_math(arxiv_full_dataset)# データセットをJSONオブジェクトとして保存します。arxiv_dataset_math = file_path + "/data/arxiv_math_dataset.json"with open(arxiv_dataset_math, 'w') as fout:    json.dump(entries, fout)

私はデータセットをPandasのデータフレームdfにロードしました。素早い検査で、縮小されたデータセットには55,497件の予稿が含まれており、より実用的なサイズであることが分かりました。[tsdae_article]では約10,000のエントリーが十分であると示唆されていますが、私は縮小されたデータセット全体を保持します。数学のタイトルにはLaTeXコードが含まれている場合がありますので、処理を最適化するためにISOコードと交換します。

parsed_titles = []for i,a in df.iterrows():    """    LaTeXスクリプトをISOコードに置換するための関数。    """    try:        parsed_titles.append(LatexNodes2Text().latex_to_text(a['title']).replace('\\n', ' ').strip())     except:        parsed_titles.append(a['title'].replace('\\n', ' ').strip())# 解析されたタイトルを持つ新しい列を作成df['parsed_title'] = parsed_titles

トレーニングにはparsed_titleのエントリーを使用するので、リストとして抽出しましょう:

# 解析されたタイトルをリストとして抽出train_sentences = df.parsed_title.to_list()

次に、各エントリーからトークンの約60%を削除して、破損した文を形成しましょう。さらに探索したり、異なる削除割合を試したりする場合は、denoising scriptをご覧ください。

# データにノイズを追加train_dataset = datasets.DenoisingAutoEncoderDataset(train_sentences)

処理後のエントリーの例を確認しましょう:

print(train_dataset[2010])初期のテキスト: "On solutions of Bethe equations for the XXZ model"破損したテキスト: "On solutions of for the XXZ"

ご覧のように、初期のテキストからBethe equationsmodelが削除されました。

データ処理の最後のステップは、データセットをバッチでロードすることです:

train_dataloader = DataLoader(train_dataset, batch_size=8,                              shuffle=True, drop_last=True)

TSDAEトレーニング

私はtrain_tsdae_from_file.pyのアプローチに従っていますが、より理解しやすいようにステップバイステップで構築します。

まず、事前学習済みのトランスフォーマーチェックポイントを選択し、デフォルトオプションに従います:

model_name = 'bert-base-uncased'word_embedding_model = models.Transformer(model_name)

プーリング方法としてCLSを選択し、構築されるベクトルの次元を指定します:

pooling_model = models.Pooling(word_embedding_model.get_word_embedding_dimension(),                              "cls")                                           'cls')

次に、2つのレイヤーを組み合わせて文の変換モデルを構築します:

model = SentenceTransformer(modules=[word_embedding_model,                           pooling_model])                                                 pooling_model])

最後に、損失関数を指定し、エンコーダーとデコーダーのパラメータを結びつけます。

train_loss = losses.DenoisingAutoEncoderLoss(model,                                            decoder_name_or_path=model_name,                                            tie_encoder_decoder=True)

そして、fitメソッドを呼び出してモデルをトレーニングします。その後の手順のためにモデルを保存します。実験を最適化するためにハイパーパラメータを調整しても構いません。

model.fit(    train_objectives=[(train_dataloader, train_loss)],    epochs=1,    weight_decay=0,    scheduler='constantlr',    optimizer_params={'lr': 3e-5},    show_progress_bar=True,    use_amp=True # GPUがFP16コアをサポートしていない場合はFalseに設定)pretrained_model_save_path = 'output/tsdae-bert-uncased-math'model.save(pretrained_model_save_path)

プレトレーニングの段階は、High-RAMに設定されたA100 GPUを使用したGoogle Colab Proインスタンスで約15分かかりました。

AllNLIデータセットでのファインチューニング

まず、AllNLIデータセットをダウンロードしましょう:

nli_dataset_path = 'data/AllNLI.tsv.gz'if not os.path.exists(nli_dataset_path):    util.http_get('<https://sbert.net/datasets/AllNLI.tsv.gz>',                   nli_dataset_path)

次に、ファイルを解凍し、トレーニングのためにデータを解析します:

def add_to_samples(sent1, sent2, label):    if sent1 not in train_data:        train_data[sent1] = {'contradiction': set(),                             'entailment': set(),                              'neutral': set()}                                                       'entailment': set                                               'neutral': set()}    train_data[sent1][label].add(sent2)train_data = {}with gzip.open(nli_dataset_path, 'rt', encoding='utf8') as fIn:    reader = csv.DictReader(fIn, delimiter='\\t',                             quoting=csv.QUOTE_NONE)    for row in reader:        if row['split'] == 'train':            sent1 = row['sentence1'].strip()            sent2 = row['sentence2'].strip()                        add_to_samples(sent1, sent2, row['label'])            add_to_samples(sent2, sent1, row['label'])  # Also add the oppositetrain_samples = []for sent1, others in train_data.items():    if len(others['entailment']) > 0 and len(others['contradiction']) > 0:        train_samples.append(InputExample(texts=[sent1,                      random.choice(list(others['entailment'])),                      random.choice(list(others['contradiction']))]))        train_samples.append(InputExample(texts=[random.choice(list(others['entailment'])),                      sent1,                      random.choice(list(others['contradiction']))]))                                                            random.choice(list(others['contradiction']))]))

トレーニングデータセットは約563Kのトレーニングサンプルがあります。最後に、データをバッチでロードし、バッチ内の重複を回避する特別なローダーを使用します:

train_dataloader = datasets.NoDuplicatesDataLoader(train_samples,                                                   batch_size=32)

ここで使用するバッチサイズは、スクリプトのデフォルトサイズである「128」よりも小さいです。より大きなバッチサイズはより良い結果をもたらしますが、より多くのGPUメモリを必要とし、私の計算リソースの制約のため、小さいバッチサイズを選びました。

最後に、AllNLIデータセット上で事前トレーニングされたモデルをMultipleRankingLossを使用して調整します。エンテイールメントのペアはポジティブであり、矛盾のペアはハードネガティブです。

# モデルパラメータを設定model_name = 'output/tsdae-bert-uncased-math'train_batch_size = 32 max_seq_length = 75num_epochs = 1# 事前トレーニングされたモデルをロードlocal_model = SentenceTransformer(model_name)# 損失関数を選択train_loss = losses.MultipleNegativesRankingLoss(local_model)# データの10%をウォームアップに使用warmup_steps = math.ceil(len(train_dataloader) * num_epochs * 0.1)# モデルをトレーニングlocal_model.fit(train_objectives=[(train_dataloader, train_loss)],          #evaluator=dev_evaluator,          epochs=num_epochs,          #evaluation_steps=int(len(train_dataloader)*0.1),          warmup_steps=warmup_steps,          output_path=model_save_path,          use_amp=True  # Set True, if your GPU supports FP16 operations          )# モデルを保存finetuned_model_save_path = 'output/finetuned-bert-uncased-math'local_model.save(finetuned_model_save_path)

Google Colab Proでは、32のバッチサイズで1エポックに対して約40分かかり、全体の500Kデータセットでモデルを微調整しました。

TSDAE事前トレーニングモデルと微調整モデルの評価

私はHuggingFaceのSTS(シーマンティックテキストシミラリティ)データセットを使用して、EmbeddingSimilarityEvaluatorを使用して予備的な評価を行います。これにより、Spearmanの順位相関が返されます。ただし、これらの評価は私が特に焦点を当てている特定のドメインを使用していないため、モデルの真のパフォーマンスを示すものではない可能性があります。詳細については、[tsdae_article]のセクション4を参照してください。

まず、HuggingFaceからデータセットをダウンロードし、validationサブセットを指定します:

import datasets as dtsfrom datasets import load_dataset# HuggingFaceからSTSベンチマークデータセットをインポートsts = dts.load_dataset('glue', 'stsb', split='validation')

これは以下の形式のデータセットオブジェクトです:

Dataset({    features: ['sentence1', 'sentence2', 'label', 'idx'],    num_rows: 1379})

理解するために、特定のエントリを1つ見てみましょう

# エントリsts['idx'][100]、sts['sentence1'][100]、sts['sentence2'][100]、sts['label'][100]をのぞいてみましょう>>>(100, '女性が馬に乗っています。', '男性が怒ってテーブルをひっくり返しています。', 0.0)

この例からわかるように、各エントリには4つの特徴があります。1つはインデックス、2つの文、そしてラベル(人間の注釈者によって作成されたもの)です。ラベルは0から5までの値を取り、2つの文の類似度レベルを示します(5が最も類似しています)。この例では、2つの文は完全に異なるトピックです。

モデルを評価するために、文のペアの文の埋め込みを作成し、各ペアのコサイン類似度スコアを計算します。ラベルと類似度スコアのスピアマン順位相関を評価スコアとして計算します。

値が0から1のコサイン類似度を使用するため、ラベルを正規化する必要があります:

# [0, 5]の範囲を[0, 1]に正規化sts = sts.map(lambda x: {'label': x['label'] / 5.0})

HuggingFaceのInputExampleクラスでデータを包みます:

# 解析済みのデータを格納するリストを作成samples = []for sample in sts:    # InputExampleクラスを使用するように書式を変更    samples.append(InputExample(        texts=[sample['sentence1'], sample['sentence2']],        label=sample['label']    ))

sentence-transformersライブラリのEmbeddingSimilarityEvaluatorクラスに基づいて評価者を作成します。

# 評価モジュールをインスタンス化evaluator = EmbeddingSimilarityEvaluator.from_input_examples(samples)

TSDAEモデル、ファインチューニングモデル、およびいくつかの事前学習済み文の埋め込みのスコアを計算します:

著者による画像

したがって、一般的な範囲のデータセットでは、all-mpnet-base-v2などの事前学習済みモデルの性能がTSDAEファインチューニングモデルを上回ることがあります。ただし、事前学習により、初期モデルbert-base-uncasedの性能は2倍以上に向上しました。ファインチューニングのハイパーパラメータをさらに調整することで、さらなる改善が可能であると考えられます。

結論

リソースが少ないドメインでは、TSDAEとファインチューニングの組み合わせは埋め込みの作成に非常に効果的な戦略です。ここで得られた結果は、データの量と計算手段を考慮すると注目に値するものです。ただし、特に異常なまたはドメイン固有のデータセットに対しては、効率性とコストを考慮すると、同等のパフォーマンスを提供できる事前学習済みの埋め込みを選択することが望ましいでしょう。

Gihubリンク Colabノートブックとサンプルデータセットへ。

そして、私たちの学習の旅には、常に良いこと、悪いこと、そして混沌としたことを受け入れるべきです!

参考文献

[tsdae_article]. K. Wang, et al., TSDAE: Using Transformer-based Sequential Denoising Auto-Encoder for Unsupervised Sentence Embedding Learning (2021) arXiv:2104.06979

[aviation_article]. L. Wang, et al., Adapting Sentence Transformers for the Aviation Domain (2023) arXiv:2305.09556

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