[Basic NLP]sentent-Transformersライブラリを使用してSBERTを学習する方法


Intro
この記事は前の文章で紹介したSENTCEBERTモデルの微調整方法についての記事です.
まずSENTCEBERTを学習するデータセット(STS,NLI)を簡単に紹介し,STS単一データによるfinetunningとNLI学習モデルにSTSを追加するcontinue学習方法と結果を論文と文−変換器式を基準として紹介する.
本明細書で使用する実験室実践コード
  • SBERT-only-STS-training
  • SBERT-continue-learning-by-Softmaxloss-NLI
  • SBERT-continue-learning-by- MNRloss-NLI
  • 1.SBERT学習データ
    SBERTを学習するためのデータには、文間の類似度を測定するためのSTSデータセットと、文間の関係を理解するためのNLIデータセットが含まれる.
    この2つのデータセットの利用可能なソースは多くないが、現在公信力が使用されているデータセットは、Kaka Brainで公開されているKorNLUと、KLUEプロジェクトで公開されているKLUEデータムセット2種である.種々の開示された言語モデルから,主にこれらのデータセットによってモデルの性能を評価する.
    1.1. STS (Semantic Textual Similarity)
    STSデータは2つの文対と2つの文の間の類似度スコアからなり,これらのスコアを学習することで文と文の間の類似度を予測できる.KLUE-STSデータセットの例を次に示します.

    1.2. NLI (Natural Language Inference)
    NLIデータはまた、2つの文が互いに伴う(含む)関係、矛盾(矛盾)関係、中立(中性)関係であることを示すラベル値からなる2つの文対を提供している.
    次に、KLUE-NLL Iデータセットの例を示します.「label」値については、数値形式に変換され、0は含まれ、1は中立、2は矛盾を表します.(KornlIデータのラベル値はテキストです.)

    2.SBERT学習方法
    論文では,SBERTの学習方法は大きく2つあり,1つ目はSTSデータのみで学習する方法,2つ目は最適化されたモデルをNLIデータでSTSに追加して学習する継続学習方法であると考えられる.
    2.1. STS単一データによるFine-Tuning
    このセクションのcolab練習コードについては、リンクを参照してください.
    学習SBERTの最も基本的で最も強力な方法はSTSデータによる微調整であり,回帰目標関数による学習である.
    学習方法
    1.2つの文のペアを入力
    2.各入力シーケンスを予習BERTに従って埋め込みベクトルに変換する
    3.変換後の埋め込みベクトルに対してPoyoung演算(通常Mean-pooling)を行い、文埋め込みベクトルに変換する
    4.変換された2つの文imbedingベクトルをcosine類似度により2つの文ベクトルの類似度値を算出する(-1~1)

    2.1.1. Load Dataset
    実習に使用するデータはklue-stsデータセットです.このデータセットは「train」、「validation」のみで構成されますが、「train」の3つのデータセットの10%は検証のためにサンプリングされ、既存の「validation」はテストのために使用されます.
    from datasets import load_dataset
    
    # load KLUE-STS Dataset
    klue_sts_train = load_dataset("klue", "sts", split='train[:90%]')
    klue_sts_valid = load_dataset("klue", "sts", split='train[-10%:]') # train의 10%를 validation set으로 사용
    klue_sts_test = load_dataset("klue", "sts", split='validation')
    
    print('Length of Train : ',len(klue_sts_train)) # 10501
    print('Length of Valid : ',len(klue_sts_valid)) # 1167
    print('Length of Test : ',len(klue_sts_test)) # 519
    2.1.2. PreprocessingInputExample()クラスにより,2つの文対とラベルを組み合わせてモデルが学習できる形式に変換する.
    from sentence_transformers.readers import InputExample
    
    def make_sts_input_example(dataset):
        ''' 
        Transform to InputExample
        ''' 
        input_examples = []
        for i, data in enumerate(dataset):
            sentence1 = data['sentence1']
            sentence2 = data['sentence2']
            score = (data['labels']['label']) / 5.0  # normalize 0 to 5
            input_examples.append(InputExample(texts=[sentence1, sentence2], label=score))
    
        return input_examples
        
    sts_train_examples = make_sts_input_example(klue_sts_train)
    sts_valid_examples = make_sts_input_example(klue_sts_valid)
    sts_test_examples = make_sts_input_example(klue_sts_test)
    学習用trainデータをDataLoader()にグループ化してバッチ学習を行い、EmbeddingSimilarityEvaluator()により学習時に使用される検証検証検証検証器とモデル評価用test検証器を作成した.
    from torch.utils.data import DataLoader
    from sentence_transformers.evaluation import EmbeddingSimilarityEvaluator
    
    # Train Dataloader
    train_dataloader = DataLoader(
        sts_train_examples,
        shuffle=True,
        batch_size=train_batch_size, # 32 (논문에서는 16)
    )
    
    # Evaluator by sts-validation
    dev_evaluator = EmbeddingSimilarityEvaluator.from_input_examples(
        sts_valid_examples,
        name="sts-dev",
    )
    
    # Evaluator by sts-test
    test_evaluator = EmbeddingSimilarityEvaluator.from_input_examples(
        sts_test_examples,
        name="sts-test",
    )
    2.1.3. Load Pretrained Model
    STS fine−tuningのための事前学習言語モデルをロードするプロセスは,Huggingface model hubに開示されているklue/roberta-baseモデルを用いた.
    冷却層について,論文実験基準で最も性能の良い平均プールを定義した.
    from sentence_transformers import SentenceTransformer, models
    
    # Load Embedding Model
    embedding_model = models.Transformer(
        model_name_or_path="klue/robert-base", 
        max_seq_length=256,
        do_lower_case=True
    )
    
    # Only use Mean Pooling -> Pooling all token embedding vectors of sentence.
    pooling_model = models.Pooling(
        embedding_model.get_word_embedding_dimension(),
        pooling_mode_mean_tokens=True,
        pooling_mode_cls_token=False,
        pooling_mode_max_tokens=False,
    )
    
    model = SentenceTransformer(modules=[embedding_model, pooling_model])
    2.1.4. Training by STS
    STS学習時のloss関数はCosineSimilarityLoss()を用い,論文と同様に4つの期間を設定し,learning-rate warm-upはtrainの10%を設定した.
    from sentence_transformers import losses
    
    # config
    sts_num_epochs = 4
    train_batch_size = 32
    sts_model_save_path = 'output/training_sts-'+pretrained_model_name.replace("/", "-")+'-'+datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
    
    # Use CosineSimilarityLoss
    train_loss = losses.CosineSimilarityLoss(model=model)
    # linear learning-rate warmup steps
    warmup_steps = math.ceil(len(sts_train_examples) * sts_num_epochs / train_batch_size * 0.1) #10% of train data for warm-up
    # Training
    model.fit(
        train_objectives=[(train_dataloader, train_loss)],
        evaluator=dev_evaluator,
        epochs=sts_num_epochs,
        evaluation_steps=int(len(train_dataloader)*0.1),
        warmup_steps=warmup_steps,
        output_path=sts_model_save_path
    )
    2.1.5. Evaluation
    上記で定義したテストベリファイアを用いてモデル性能を評価した結果,約0.88の性能を示した.
    # evaluation sts-test
    test_evaluator(model, output_path=sts_model_save_path)
    2022-02-25 02:15:39 - EmbeddingSimilarityEvaluator: Evaluating the model on sts-test dataset:
    2022-02-25 02:15:43 - Cosine-Similarity :	Pearson: 0.8870	Spearman: 0.8873
    2022-02-25 02:15:43 - Manhattan-Distance:	Pearson: 0.8862	Spearman: 0.8835
    2022-02-25 02:15:43 - Euclidean-Distance:	Pearson: 0.8869	Spearman: 0.8844
    2022-02-25 02:15:43 - Dot-Product-Similarity:	Pearson: 0.8775	Spearman: 0.8745
    0.887279591001845
    2.2. NLIとSTSデータによる継続学習
    このセクションのcolab練習コードについては、リンクを参照してください.
    STS単一データを用いて学習を行うほか,NLIを用いて学習を行った後にSTSを用いて追加学習を行う方法もある.
    論文では,STS単一データ学習時と比較して,NLI学習後にSTS学習に続く手法が約3〜4点高い性能を示した(下表2参照),この戦略は特にBERTクロスコーディング方式に大きな影響を及ぼしたと考えられる.

    まず,NLIを学習するために,論文は基本的にSoftmax objective関数を用いて3つのクラス(含む,矛盾,中性)を学習する.
    しかし,文-transformerのNLI学習例によれば,実験結果は,ソフトmax lossを用いた場合と比較して,複数の負のRanking loss(MNRloss)学習により,より優れた性能を示した.
    (実際のklueデータセット学習の結果から,MNRlossで学習した場合,性能も若干向上した.
    MNR lossはtriple lossに似ており、(アンカー-文、正-文、負-文)フォーマットに従い、学習を導いてアンカーと正の距離を近づけ、アンカーと負の距離を遠くする.
    (本練習ではMNR lossも使用します.)
    学習方法
    1.NLIデータを三元グループ(アンカー、正、負)にまとめる
    2.各入力シーケンスは、予め学習されたBERTモデルに従って埋め込みベクトルに変換される
    3.変換後の埋め込みベクトルに対してPoyoung演算(通常Mean-pooling)を行い、文埋め込みベクトルに変換する
    2.MNR損失を目標とする関数でNLIデータセットを微調整する
    3.最適化されたモデルをNLIでロードし、STSデータとしてさらに学習する
    4.変換された2つの文imbedingベクトルをcosine類似度により2つの文ベクトルの類似度値を算出する(-1~1)

    以下の実践コードは、上述したSTS finetunningに関する内容を省略する.
    2.2.1. Load Dataset (NLI)
    学習を継続するために、すべてのnli、stsデータがロードされ、nliではトレーニングデータのみがロードされます.
    # load KLUE-NLI Dataset
    klue_nli_train = load_dataset("klue", "nli", split='train')
    
    print('Length of Train : ',len(klue_nli_train)) # 24998
    2.2.2. Preprocessing (NLI)
    MNRlossによる学習のためにtriple(アンカー文,正文,否定文)形式で調整した後,同様にInputExample()に変換した.
    def make_nli_triplet_input_example(dataset):
        ''' 
        Transform to Triplet format and InputExample
        ''' 
        # transform to Triplet format
        train_data = {}
        def add_to_samples(sent1, sent2, label):
            if sent1 not in train_data:
                train_data[sent1] = {'contradiction': set(), 'entailment': set(), 'neutral': set()}
            train_data[sent1][label].add(sent2)
    
        for i, data in enumerate(dataset):
            sent1 = data['hypothesis'].strip()
            sent2 = data['premise'].strip()
            if data['label'] == 0:
                label = 'entailment'
            elif data['label'] == 1:
                label = 'neutral'
            else:
                label = 'contradiction'
    
            add_to_samples(sent1, sent2, label)
            add_to_samples(sent2, sent1, label) #Also add the opposite
    
        # transform to InputExmaples
        input_examples = []
        for sent1, others in train_data.items():
            if len(others['entailment']) > 0 and len(others['contradiction']) > 0:
                input_examples.append(InputExample(texts=[sent1, random.choice(list(others['entailment'])), random.choice(list(others['contradiction']))]))
                input_examples.append(InputExample(texts=[random.choice(list(others['entailment'])), sent1, random.choice(list(others['contradiction']))]))
        
        return input_examples
    
    nli_train_examples = make_nli_triplet_input_example(klue_nli_train)
    nli_train_examples[0].texts  # ['힛걸 진심 최고다 그 어떤 히어로보다 멋지다', '힛걸 진심 최고로 멋지다.', '힛걸 그 어떤 히어로보다 별로다.']
    後続学習用のtrainデータをDataLoader()に変換してバッチ学習を行う.上記NLIを学習する際には、STSデータセットを検証データとして使用するため、個別のベリファイアは作成されません.
    # Train Dataloader
    train_dataloader = DataLoader(
        nli_train_examples,
        shuffle=True,
        batch_size=train_batch_size,
    )
    2.2.3. Load Pretrained Model
    NLI微調整のための事前学習言語モデルをロードするプロセスは,Huggingfaceモデルセンターに公開されたklue/roberta-baseモデルを用いた.
    冷却層について,論文実験基準で最も性能の良い平均プールを定義した.
    from sentence_transformers import SentenceTransformer, models
    
    # Load Embedding Model
    embedding_model = models.Transformer(
        model_name_or_path="klue/robert-base", 
        max_seq_length=256,
        do_lower_case=True
    )
    
    # Only use Mean Pooling -> Pooling all token embedding vectors of sentence.
    pooling_model = models.Pooling(
        embedding_model.get_word_embedding_dimension(),
        pooling_mode_mean_tokens=True,
        pooling_mode_cls_token=False,
        pooling_mode_max_tokens=False,
    )
    
    model = SentenceTransformer(modules=[embedding_model, pooling_model])
    2.2.4. Training by NLI
    前述したように,loss関数ではMultipleNegativesRankingLoss()を用い,論文と同様に,第1段階ではlearng−rate warm−upでtrainの10%を設定した.
    from sentence_transformers import losses
    
    # config
    sts_num_epochs = 1
    train_batch_size = 32
    nli_model_save_path = 'output/training_nli_by_MNRloss_'+pretrained_model_name.replace("/", "-")+'-'+datetime.now()
    
    # Use MultipleNegativesRankingLoss
    train_loss = losses.MultipleNegativesRankingLoss(model)
    # warmup steps
    warmup_steps = math.ceil(len(nli_train_examples) * nli_num_epochs / train_batch_size * 0.1) #10% of train data for warm-up
    logging.info("Warmup-steps: {}".format(warmup_steps))
    # Training
    model.fit(
        train_objectives=[(train_dataloader, train_loss)],
        evaluator=dev_evaluator,
        epochs=nli_num_epochs,
        evaluation_steps=int(len(train_dataloader)*0.1),
        warmup_steps=warmup_steps,
        output_path=nli_model_save_path,
        use_amp=False       #Set to True, if your GPU supports FP16 operations
    )
    2.2.5. Continue Learning by STS
    これはNLIデータセットを通して調整されたモデルにSTSを追加するプロセスである.学習が完了したモデルをロードすると、
    # Load model of fine-tuning by NLI
    model = SentenceTransformer(nli_model_save_path)
    STSデータセットで継続学習を実行します.
    # config
    sts_num_epochs = 4
    train_batch_size = 32
    sts_model_save_path = 'output/training_sts_continue_training-'+pretrained_model_name.replace("/", "-")+'-'+datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
    
    # Use CosineSimilarityLoss
    train_loss = losses.CosineSimilarityLoss(model=model)
    # warmup steps
    warmup_steps = math.ceil(len(sts_train_examples) * sts_num_epochs / train_batch_size * 0.1) #10% of train data for warm-up
    logging.info("Warmup-steps: {}".format(warmup_steps))
    # Training
    model.fit(
        train_objectives=[(train_dataloader, train_loss)],
        evaluator=dev_evaluator,
        epochs=sts_num_epochs,
        evaluation_steps=int(len(train_dataloader)*0.1),
        warmup_steps=warmup_steps,
        output_path=sts_model_save_path
    )
    2.2.6. Evaluation
    上記で定義したテストベリファイアを用いてモデル性能を評価した結果,モデル性能は約0.89であり,単一STS学習時と比較して約1%向上した.
    # evaluation sts-test
    test_evaluator(model, output_path=sts_model_save_path)
    2022-02-25 04:28:11 - EmbeddingSimilarityEvaluator: Evaluating the model on sts-test dataset:
    2022-02-25 04:28:15 - Cosine-Similarity :	Pearson: 0.8962	Spearman: 0.8964
    2022-02-25 04:28:15 - Manhattan-Distance:	Pearson: 0.8895	Spearman: 0.8845
    2022-02-25 04:28:15 - Euclidean-Distance:	Pearson: 0.8908	Spearman: 0.8859
    2022-02-25 04:28:15 - Dot-Product-Similarity:	Pearson: 0.8847	Spearman: 0.8810
    0.896394981925387
    3. Conclusion
    STSのみを学習し、ソフトmax lossのcontinue learning、MNR lossのcontinue learningの3つのケースを利用して実験を行った結果、わずかな差はあるものの、MNR lossを利用したcontinue learning方式の性能が最も優れていることが分かった.

    通常、stsパフォーマンス評価のために、上述したkornluデータセットが使用されています.
    しかし、kornluデータにとっては、klueよりもデータの数が多いものの、データの複雑さは非常に低く、各種の実際のデータの文埋め込みを目的とすれば、klueデータを利用した方が経験的により品質が良く、性能も良い.
    もしそうなら「klueもkornluも勉強していいんじゃないですか?」klueとkornluの類似性の測定基準,すなわちラベルの基準が異なると,かえってモデルに混乱の結果をもたらすと考える人もいるので,経験的には単一データを用いることが望ましい.