Kaggle参戦記：生成AIと人間の文章を区別できるか？

# 「Titanicの次」を探す

　3日前に[Kaggleデビュー](https://qiita.com/rho-guy/items/f9b384fa8e0ad9964713)して、Titanicのチュートリアルでひとしきり遊んでみました。何回かsubmitしてLeadersboardを眺めたり、DiscussionやCodeをのぞいたりもしました。データやモデルをこねくりまわしていると、いろいろと開発環境を整備したり、過去の成績上位者の情報をまとめてみたくなってきました。だがしかし、いつまでも「お勉強モード」でいてはいけない。それらは実践で戦いながらでも身につくもののはずだ。

 というわけで「Titanicの次」として、何か手ごろなコンペはないかと探してみました。最初はまごついたのですが、コンペのページの「Filter機能」を見つけたおかげで、サクッと次に参戦する舞台を選定できました！

 やり方はこんな感じです。
 ①[コンペのページ](https://www.kaggle.com/competitions)の検索窓の左端にある「Filters」を押す
 ![コンペ画面.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3666147/45a54acd-b9f0-74ea-ca8a-5c87f66213e4.png)

②フィルタ条件指定用のパネルが出るので、「Active」と「Medals」にチェックを入れる（現在参加可能で、メダルが対象のコンペを絞り込むことができる）
<img width=300 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3666147/4c10d65b-a0dd-3155-13a1-0900154008a6.png">

③2024年1月5日現在で条件にマッチしたのは6つのコンペだけでした。意外と選択肢は少ない。。。
![検索結果.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3666147/02c5122f-ae28-167f-4e33-10c6df4ee2d7.png)

結局、スケジュールの残り期間＆自分にとって面白そうなもの、という点で「[LLM - Detect AI Generated Text](https://www.kaggle.com/competitions/llm-detect-ai-generated-text)」に参加することに決めました。どうやら「エッセイの文章を入力として、それを書いたのが生成AIなのか、人間（学生）なのかを判別する」タスクらしいです。残り期間が18日と少ないのが気がかりですが、とりあえず「短期決戦」の経験を積むには、ほどよいテーマだと考えました。まだよく分かってないこといっぱいだけど！

# 参加してびっくり！データがない！

実際にコンペに参加してみると、とまどうことがいっぱいありました。まずはデータセット。コンペで使われるのは人間と生成AIが7つのプロンプトの指示のもとに書かれた約10,000件のエッセイなのですが、なんと、トレーニングデータとして提供されているのは、そのうち1,000件くらいしかない。しかも、大半は人間が書いたエッセイで、生成AIが書いた文章はたったの3件！7つのプロンプトについても、そのうち2つしか開示されていません。ええ？自分で生成AIを使ってデータ拡張しろということ？

さらに、テストデータはデータフォーマットのサンプルとして3件しかなく、ソースコードが提出された時点で中身が本物（約9000件のデータ）に置き換わり、処理を行うとのこと。これまでの話を図にすると、下記のような感じです。
![データ内訳.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3666147/411274f6-2788-9e90-2600-d645bd449b9a.png)

Titanicと同じように、多めのトレーニングデータと少なめのテストデータが与えられて、判別結果を提出するのをイメージしていた初心者としては大慌てです。いったいこんな条件でどうやって戦えというのか。。。

<img width=200 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3666147/dc72918a-6f7e-ff52-5182-ec8def145d50.png">


# ソースコンペティション？どうやって投稿するの？
いろいろ調べてみたところ、テストデータが非開示なのは「ソースコンペティション」というタイプのコンペで、割と普通に広く実施されているもののようでした。応募の手順は下記の通り。

①Kaggle Notebookで、test_essays.csvを読み込んで判別処理をし、submission.csvを出力するようプログラムを書く

```mermaid
graph LR
  A([test_essays.csv])-->B[Kaggle Notebook]
  B-->C([submission.csv])
```


②Notebookの右上の「Save Version」ボタンを押すと、検証用Runが走り、エラーが出なければ提出可能になる
![SaveVersion.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3666147/82c275fb-1da1-83fe-52fa-ee9a22c87f27.png)


③コンペのページの右上の「Submit Prediction」ボタンを押し、提出するNotebookとそのバージョン、出力ファイルの名前（デフォルトだとsubmission.csv）を指定して提出
![Submit.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3666147/6aa60ecb-1c5e-17ab-825c-4f886c727674.png)


④Kaggle側でNotebookが実行される（結果が出るまで数時間程度がかかります。テスト用の実データがそれなりのボリュームあるからだと思います。）

# みんな、どうやってるんだろう？
とりあえず、投稿と評価の方法は分かりました。それでは、トップ集団がどんな手法を使っているのかを見ていきます。[Leaderboard](https://www.kaggle.com/competitions/llm-detect-ai-generated-text/leaderboard)を見ると、2024年1月5日現在では、0.982の人がトップを走ってますね。ほぼ100%の精度じゃないですか。案外、人間と生成AIの文章は区別しやすいということなのか。

[Code](https://www.kaggle.com/competitions/llm-detect-ai-generated-text/code)を見ると、スコア0.96周辺のNotebookがちらほら。開いて中身を見ると、なんと、どれもほとんど同じようなソースコード！どうやら、多くの人がトップクラスの人のNotebookを「Copy&Edit」して使っている模様。なるほど、これが「巨人の肩に乗る」ってやつか。

[Discussion](https://www.kaggle.com/competitions/llm-detect-ai-generated-text/discussion)を見ると、「[To newcomers](https://www.kaggle.com/competitions/llm-detect-ai-generated-text/discussion/465227)（新参者どもへ）」という、まさに私宛のような投稿を見つけました。どうやら、コンペの序盤でdatafan07というユーザーが金メダルレベルのソリューションを共有したらしい。これが例の「スコア0.96のNotebook」の起源のようです。この投稿によると、「テストデータのみでトークナイザーをトレーニングする」ことがキーアイデアらしい。ん？テストデータでトレーニング？なんだかまだよく分からない。これは、ソースコードを読んでみなくては。。。

# スコア0.96のNotebookの中身を理解する
いよいよ、スコア0.96のNotebookの中身を見ていきます。幸い、[詳細な解説つきのNotebook](https://www.kaggle.com/code/erikfolkesson/detailed-explanation-custom-tokenizer/notebook)を見つけましたので、それに沿って、流れを把握していきましょう。

ソースコードでまず面食らったのは、コンペで提供されているトレーニングデータであるtrain_essays.csvには一切目もくれず、別のデータセットを読み込んでトレーニングデータとして使っている点でした。調べてみると、使っているのは[DAIGTデータセット](https://www.kaggle.com/datasets/thedrcat/daigt-v2-train-dataset)で、これも、生成AIと人間のエッセイを判別するタスクのために構築されたデータセットだったようです。データセットの中身はこんな感じで、人間が書いたエッセイ（persuade_corpus）が25,996件、ChatGPTやLLama2 Chatなどのもろもろの生成AIが書いたエッセイが合計19,909件、ラベル付きで含まれているようです。なるほど、これなら判別用のトレーニングに使えそうです。

|データ名|データ件数|
|:-----|------:|
|persuade_corpus|25996|
|chat_gpt_moth|2421|
|llama2_chat|2421|
|mistral7binstruct_v2|2421|
|mistral7binstruct_v1|2421|
|original_moth|2421|
|train_essays|1378|
|llama_70b_v1|1172|
|falcon_180b_v1|1055|
|darragh_claude_v7|1000|
|darragh_claude_v6|1000|
|radek_500|500|
|NousResearch/Llama-2-7b-chat-hf|400|
|mistralai/Mistral-7B-Instruct-v0.1|400|
|cohere-command|350|
|palm-text-bison1|349|
|radekgpt4|200|


[詳細な解説つきのNotebook](https://www.kaggle.com/code/erikfolkesson/detailed-explanation-custom-tokenizer/notebook)を読んで、私が理解した構成は下記の通りでした。


```mermaid
graph LR
  TRAIN([DAIGTデータ])-->BPE[Byte Pair Encoding]
  TEST([テストデータ])-->BPE[Byte Pair Encoding]
  BPE-->TFIDF[TF-IDFベクトル化]
  TFIDF-->C1[分類器1]
  TFIDF-->C2[分類器2]
  TFIDF-->C3[分類器3]
  TFIDF-->C4[分類器4]
  C1-->VOTE[投票]
  C2-->VOTE
  C3-->VOTE
  C4-->VOTE
  VOTE-->RES([submission.csv])
```

以下、ざっくりと説明していきます。

# Byte Pair Encoding

[Byte Pair Encoding](https://huggingface.co/learn/nlp-course/chapter6/5?fw=pt)（BPE）は、当初、テキスト圧縮アルゴリズムとして開発された手法なのですが、その後、OpenAIがGPTモデルを事前学習する際のトークン化に採用したそうです。GPT、GPT-2、RoBERTa、BART、DeBERTaを含む多くのTransformerモデルで使われているとのこと。

圧縮アルゴリズムなので、日本語や英語の区別なく、文字通り「文字のバイトコードの並び」の頻度によって、どこからどこまでを「1つの語彙」と認識し、トークンとして分割するかを決定するアルゴリズムになっています。

例えば、テキストファイルに、下記の5つの単語が並んでいるとしましょう。
~~~
"hug", "pug", "pun", "bun", "hugs"
~~~

この場合、基本となる「語彙」は1文字ずつ切り出した["b", "g", "h", "n", "p", "s", "u"]となります。

それでは、これら5つの単語がそれぞれ異なる回数記述されている、もう少し長めのテキストファイル（コーパス）があったとします。各単語の出現頻度を調べると下記のようになりました。
~~~
("hug", 10), ("pug", 5), ("pun", 12), ("bun", 4), ("hugs", 5)
~~~

1文字ずつの基本語彙で分割した場合の出現頻度は下記のようになりますね。
~~~
("h" "u" "g", 10), ("p" "u" "g", 5), ("p" "u" "n", 12), ("b" "u" "n", 4), ("h" "u" "g" "s", 5)
~~~

次に、2文字のペアの語彙の出現頻度を考えます。例えば、"hu"という語彙は"hug"と"hugs"に含まれているので、10 + 5 = 15回出現しています。一番多く出現している2文字の語彙は、"ug"になります。"hug"、"pug"、hugs"に含まれるので10 + 5 + 5 = 20回です。そこで、"ug"を新たな語彙として採用すると、語彙とコーパスの関係は下記のようになります。

~~~
語彙: ["b", "g", "h", "n", "p", "s", "u", "ug"]
コーパス: ("h" "ug", 10), ("p" "ug", 5), ("p" "u" "n", 12), ("b" "u" "n", 4), ("h" "ug" "s", 5)
~~~

同様に、出現頻度の高い語彙のペアをどんどんまとめていくと、下記のようになります。
~~~
語彙: ["b", "g", "h", "n", "p", "s", "u", "ug", "un", "hug"]
コーパス: ("hug", 10), ("p" "ug", 5), ("p" "un", 12), ("b" "un", 4), ("hug" "s", 5)
~~~

こうやって求められた語彙を使って、出現頻度が多く文字数の長い語彙を、より短いシンボルで置き換えることで、テキストの容量を圧縮できるわけですね。また、自然言語処理の観点では「出現頻度の高い文字の並び」を一つのまとまりとして扱うことにつながるため、「生成AIがよく使う言い回し」や「人間がよく使う言い回し」の手掛かりを検知する特徴量になる可能性があるわけです。この、一連の語彙選択の処理を「トークナイザのトレーニング」と呼ぶそうです。

[スコア0.96のNotebook](https://www.kaggle.com/code/erikfolkesson/detailed-explanation-custom-tokenizer/notebook)では、この「トークナイザのトレーニング用データ」として、テストデータの文章を使っているのがキモです。つまり、「テストデータに繰り返し出てくる特徴的な文字の並び」を使って、ラベル付きのトレーニングデータで判別モデルを構築するため高い精度をたたき出しているというわけです。「テストデータに頻出するパタンを特徴量として使う」ため、「どういうパタンが出たら、どのくらいの確率で生成AIなのか」を知ることができるわけです。なるほど～。賢いなあ！


# TF-IDFベクトル化

　さて、キモの部分はだいたい把握したので、ここからは説明を少しはしょります。TF-IDFというのは、自然言語処理でよく使う概念で、ある文書におけるある単語の重要度をスコア化したものです。ある文書の中で、高頻度に出てくる単語が重要というのはイメージしやすいかと思います。例えば、この文書だと「データ」「データ」と何度も使っているので、この文書の中での「データ」の重要度は高くなります。これがTFの考え方です。
 
 ところが、単純に出現頻度だけで重要度を決めると、何の変哲もない普通の単語の重要度が高くなってしまいます。例えば「これ」「この」「です」「ます」などのような単語です。これらの単語は、どんな内容の文書にも必ず出現しますよね。だから、単語がどれだけ特定の文書だけで出現しているか、といった「レア度」を数値化したものがIDFです。「です」「ます」よりも、「データ」の方が出現する文書数は少ないですよね。もっというと「Kaggle」が出現する文書数はもっと少ないはず。この場合、IDFの大きさは、
 
 「です」 < 「データ」 < 「Kaggle」
 
 という並びになるはずです。「レア度の高い単語で、何度もその文書で出現している単語ほど、その文書にとっての重要語である」という考え方のもとにスコア化する手法がTF-IDFというわけです。それぞれの単語について、TF-IDFを求めることで、文書をベクトル化することができます。TF-IDFの計算式やベクトル化の方法は、[こちら](https://atmarkit.itmedia.co.jp/ait/articles/2112/23/news028.html)の記事が分かりやすいと思います。

今、分かりやすく説明するために、「です」「データ」「Kaggle」などの単語を使って説明しましたが、 [スコア0.96のNotebook](https://www.kaggle.com/code/erikfolkesson/detailed-explanation-custom-tokenizer/notebook)では、単語ではなく、バイトの並びの語彙に対してTF-IDFを求めているわけです。なので、この手法は英語でも日本語でも、言語を問わず適用可能な気がします。日本語でも同様のコンペがそのうち開催されるでしょうから、その時はこれを引っ提げて参戦してもいいかもしれませんね。

# 分類器1～4と投票
[一昨日の記事](https://qiita.com/rho-guy/items/d82bb51b5ae8728e5fb0)と[昨日の記事](https://qiita.com/rho-guy/items/7c3ef9fca030732b0c86)でさんざん書いていた、弱学習器によるアンサンブルですね。[スコア0.96のNotebook](https://www.kaggle.com/code/erikfolkesson/detailed-explanation-custom-tokenizer/notebook)では、[Multinomial Naive Bayes](https://qiita.com/fujin/items/bd58fc7a93dc6e001045)、[確率的勾配降下法（SGD）](https://ja.wikipedia.org/wiki/%E7%A2%BA%E7%8E%87%E7%9A%84%E5%8B%BE%E9%85%8D%E9%99%8D%E4%B8%8B%E6%B3%95)、[LightGBM](https://www.nri.com/jp/knowledge/glossary/lst/alphabet/light_gbm#:~:text=LightGBM%E3%81%A8%E3%81%AF&text=%E6%A9%9F%E6%A2%B0%E5%AD%A6%E7%BF%92%E3%81%AB%E3%81%8A%E3%81%91%E3%82%8B%E5%88%86%E6%9E%90%E3%82%A2%E3%83%AB%E3%82%B4%E3%83%AA%E3%82%BA%E3%83%A0,%E3%82%82%E9%AB%98%E3%81%84%E3%81%93%E3%81%A8%E3%81%8C%E7%89%B9%E5%BE%B4%E3%80%82)、[CatBoost](https://qiita.com/shuva/items/50151e5adb13385e4b2c)の4つを使っているようです。 

投票は、[scikit-learnのVotingclass](https://qiita.com/bianca26neve/items/dc909fed3f708ecc547f)を使っていました。（voting=softでの統合）

# これからどうする？
なんとか、トップ集団が何をやっているかのキャッチアップはできましたが、ここからどうしましょう。とりあえず、私も例のNotebookをコピーして投稿することで、投稿～評価までのプロセスを理解することができました。そして、自分の実力ではないながらも一応スコア0.96をゲットしてTop33%に入りました。締め切りまで残り2週間ほどでようやくスタートラインに立ったかというところ。

まあ、DAIGTのデータを観察しながらちょこちょこといじって、スコアを挙げていければラッキーかなあ、くらいに考えてます。カリカリにチューニングしたと思われるマジックナンバーも盛りだくさんなコードなので、そんな簡単にはいかないと思いますが。。。下手をすると、締め切りまでに0.96を越えられない可能性も十分にありえます。

まずは取り急ぎ、手元で精度評価＆チューニングできる環境を整備しなくては！（そして、せっかくGoogle Colabに環境作ったのに、またKaggle Notebookに逆戻りすることになるとは。。。）