pop-web

スマートかつクールでアトラクティブなブログです

CommonLit Readability Prizeから学ぶSentence BERTによるnoisy labelの吸収

はじめに

この記事はKaggle Advent Calendar 2021の15日目の記事です。 qiita.com

もともと書こうと思っていたことと少し方向転換して書いてるのでかなりきついです(なんでこんなことに...)。

コンテント

対象読者

「本記事は初心者に向けて」をイメージとして書いています。最近はKaggleでは単純なテーブルデータのコンペはあまり出ないので、タイタニックの次を探すのも難しいでしょう。 そういった方に向けて、次は自然言語処理コンペはいかがでしょうか?(なお、残念ながら自然言語処理コンペも単純なタスクのものはもうあまりない印象ですが... タスクの性質からノイジーラベルコンペになりがちな印象)

最近あった自然言語処理コンペの入賞者も書くことないと嘆いていました(私は参加していないのでどんなものだったのか概要しか知らないのですが)。

上記ツイートからも読み取れるように、単純なタスクではコンペとしては差を出しづらく、コンペとしてはもうなくなりつつあるのかも知れません(本当はここら辺の、機械学習アルゴリズムの成熟とデータ分析コンペの複雑化を絡めて、今後MLエンジニアを目指すに当たって求められていきそうなスキル、みたいなことをお題目にして記事を書きたかったが文章の神が降りてこなかった)。

少し話がそれました。次へ行きましょう。

(丁度この方とチームを組んでた方が、ソリューションを公開したようです。タイムリー!)

自然言語とは

いや、そんなこと知ってるよという方が多いでしょうが、初心者向けなのでここからいきましょう。

自然言語とは、人間が自然に話すようになった言語です。

この説明では少しわかりにくいので、反対に人工言語について考えてみましょう。 人工言語は、人間が新しくルールを考えて設計した言語のことです。

一番わかりやすい例はプログラミング言語です。このほかに、エスペラント語や手話、ファンタジーで使われる架空言語人工言語に入るようです。 人工言語 - Wikipedia

人工言語以外は全て自然言語、そう考えてみてはいかがでしょうか?

自然言語処理とは

自然言語処理(Natural Language Processing: NLP)とは読んで字の如く、自然言語を処理する技術一般のことをいいます。

自然言語処理を用いて解かれる一般的な大きな目標として下記のものがあります。

  • 分類
  • 翻訳
  • 要約
  • 対話

こうした大きな目標を達成するために、つまり文章の内容を機械に理解させるために、さまざまな技術が発展して来ました。

自然言語処理とデータ分析コンペ

さて、Kaggleのアドカレなので前座はほどほどにして、さっさとデータ分析コンペの話に入りましょう。

自然言語処理は人間が意思疎通の手段として広く使っている手法なだけに、応用範囲もかなり広いです。 上にあげたような分かりやすい「文章になんらかの処理」を行う以外に、意外なものに自然言語処理の手法を適用することでモデルの精度を向上させることが出来ます。

いくつか例として挙げると、商品の購入データは商品IDと紐付けて記号化されますが、この商品IDの列を文章として見なすことで、自然言語処理を用いて、購買行動を特徴量とすることが出来ます。ほかにも、ある単語の特徴が周囲の単語によって決まるという分布仮説(「走れ」の次は「メロス」が出て来やすいなど)と同様に、動画にIDを紐付けて、ある動画を見たユーザが次に見た動画群と結びつけることで動画の分散表現を得て、推薦アルゴリズムに用いる、などといったことも行われています。

といった感じで、ここまで記事を書いて、みんなアドカレどんなこと書いているのかなー?と見ていたらこんな記事を発見してしまったのだが...

zenn.dev

私、こういう感じのものを書こうと思ってたのよね...もう記事書くの諦めて良いですか???

普段のブログ記事ならある程度内容被っても良いけど...アドカレでそれはなぁ......

さて、どうしようかな。

CommonLit振り返り

ということで急遽、中身を少し変更します。 今年はCommonLit Readability Prizeという自然言語処理のコンペがありました。これの振り返りということでいきましょう。 www.kaggle.com

このコンペのタスクは、文章のReadability(読みやすさ)を推定するというタスクでした。 特徴として、このReadabilityが小データかつnoisy(というとちょっと語弊がある気がするけど)というものがありました。

本コンペではReadabilityを「Bradley-Terry analysis」というものでスコアリングしているという大きな特徴がありました。 CommonLit Readability Prize: Target scores

Bradley-Terryアルゴリズムとは、今回のタスクにおいては、大雑把にいうと文章同士をどっちが読みやすいかで戦わせてレーティングをつける、いわゆるゲームでいうEloレーティングシステムのようなものでした。

3rd place solutionCommonLit Readability Prize: 3rd place solution (0.447 private - 3 models simple average)においても、以下のように語られています。

レーニングデータ+公開・非公開データで5k弱のみのサンプル、そして、各サンプルは平均で46.47回、他のサンプルと比較されます。つまり、各テキストはデータセット全体の平均1%程度としか比較されていないことになります。チェスプレイヤーのEloを想像してみてください。そのプレイヤーは、全チェスプレイヤー人口の1%としか対戦しておらず、対戦相手は無作為に選ばれています。ノイズのほとんどは、比較回数の少なさによるものだと思います。

その上、どっちが読みやすいかを比較するのは人力。これじゃあ、そりゃnoisyになるわなというタスクでした。

さて、この問題を解決するにはどうすれば良いでしょうか?考えてみましょう。

Sentence Transformers

みなさん考えましたか?

私はコンペ中思いつきませんでした。データの分布の仕方が重要だろと思いKL Divの最小化に力を入れていたのですが全く成果を出せなくて泣きました。これに時間かけすぎたせいで、他の手法も試せず...うまくいかない手法に対しての損切りの感度みたいなものも身につけたいものです。

コンペ後にいくつかソリューションを読みましたが、自分が知っていた手法で、かつ使用したこともあるのに思いつかなかったということで、自戒を込めて今回は「Sentence Transformers」について紹介を行います。

github.com

Sentence Transformer、もう少しいうとSentence BERTですね。 これはBERTにPooling層をかまして、コサイン類似度などによって文章の類似度を計算することでより文章の類似度に着目したEmbeddings表現を得ることが出来ます。

経験則から言っても、先ほど紹介したHidehisa Araiさんの記事で紹介されている下記コードで得られるBERTのEmbeddings表現よりも文章の類似度を考慮したEmbeddings表現を得られることが多いです。 テーブルデータ向けの自然言語特徴抽出術

import torch
import transformers

from transformers import BertTokenizer


class BertSequenceVectorizer:
    def __init__(self, model_name="bert-base-uncased", max_len=128):
        self.device = "cuda" if torch.cuda.is_available() else "cpu"
        self.model_name = model_name
        self.tokenizer = BertTokenizer.from_pretrained(self.model_name)
        self.bert_model = transformers.BertModel.from_pretrained(self.model_name)
        self.bert_model = self.bert_model.to(self.device)
        self.max_len = max_len

    def vectorize(self, sentence: str) -> np.array:
        inp = self.tokenizer.encode(sentence)
        len_inp = len(inp)

        if len_inp >= self.max_len:
            inputs = inp[:self.max_len]
            masks = [1] * self.max_len
        else:
            inputs = inp + [0] * (self.max_len - len_inp)
            masks = [1] * len_inp + [0] * (self.max_len - len_inp)

        inputs_tensor = torch.tensor([inputs], dtype=torch.long).to(self.device)
        masks_tensor = torch.tensor([masks], dtype=torch.long).to(self.device)

        bert_out = self.bert_model(inputs_tensor, masks_tensor)
        seq_out, pooled_out = bert_out['last_hidden_state'], bert_out['pooler_output']

        if torch.cuda.is_available():    
            return seq_out[0][0].cpu().detach().numpy() # 0番目は [CLS] token, 768 dim の文章特徴量
        else:
            return seq_out[0][0].detach().numpy()

ここでいくつか参考となりそうなサイトを紹介しておきましょう。

(個人的に近傍探索が好きなので、この中だとエムスリーテックブログの話が好きです)

えーっと、でなんでしたっけ...?

そうそう、CommonLitでしたね。1位の解法がこちらなのですが1st place solution - external data, teacher/student and sentence transformers

この解法の1番の差別化ポイントが、sentence bertを用いて、外部データから学習用データセットの各サンプルに対して、最も類似しているデータを5件ずつ引っ張ってきて追加データとして用いた、というところです。

他の回答は割と、モデルめっちゃ作ってアンサンブルしたよみたいなのばかりなのですが、1位を取れるかどうかの差はここにあったのじゃないかなと考えています。

外部データが使えないコンペも多いのですが、その場合でも、例えば今回でいえば、似た文章を集めてきてその平均値をその文章のReadabilityとする、などでlabelのnoisyさを吸収できるのではないかなと思っています。(実験できてないので本当か?みたいなところありますがすみません)

というわけで、noisy labelに対する対応策としてのSentence BERT、皆さんも試してみてはいかがでしょうか?