🤖

松尾研究所主催分析コンペ [DS Dojo #1] 回答したLLMの分類 - 1st Solution -

2024/06/21に公開

はじめに

はじめまして、株式会社松尾研究所で働いている長谷です。
2024年4月に入社し、AIソリューション開発のプロジェクトマネージャーとして活動しています。
これまでは企業の研究所でデータ解析や機械学習アルゴリズムの研究者として働きつつ、AI・データ分析の教育の会社を立ち上げて企業や個人のデータ分析リテラシーの底上げに尽力してきました。
松尾研の構築するエコシステムに感銘を受け、研究者、起業家、教育者として松尾研から世界をより良くしていきたいと思い、ジョインしました。

今回は、先日松尾研究所で開催されたデータ分析コンペティションDS Dojo#1において、からあげさんと私(chome)のチームが1位に入賞しましたので、その解法をご紹介いたします。

DS Dojo#1とは

DS Dojoの位置付けやコンペの詳細に関してはこちらの記事を参照いただき、本記事では概要を紹介します。

今回のタスクは、「あるプロンプトに対して回答したテキストデータがどのLLMから生成されたかを分類すること」です。
与えられた学習データ(train.csv)のカラムは以下のようになっていました。

  • prompt: 生成AIに入力されたプロンプト
  • text: 生成AIによる回答
  • label: textを生成したLLMに対応するラベル
    • 0: GPT-3.5, 1: Claude-Haiku, 2: Gemini

このデータを学習し、テストデータ(test.csv)のlabelを予測し、その精度を競います。
今回の評価指標はF1-Score(macro)でした。
F1-Scoreとは、分類タスクの精度を評価する評価指標の1つで、0から1の間の値を取り、値が大きい方がより精度高く分類できているといえます。

取り組み内容

終盤でのコンペ参加となったため、shigeさんが公開していたベースラインの手法であるLightGBMの分類モデルを流用し、大きく特徴量作成と特徴量選択の2つに取り組みました。
それぞれ説明していきます。

特徴量作成

作成したfeatureは以下です。

  • textの文字、単語、行、文章の数とpromptとの比較
  • promptとtextで同じ単語を使っている数
  • 1文に含まれる単語の数
  • textに存在するwordのユニーク数
  • promptとtextの最初の1単語
  • 数字に関する特徴
  • コードのマークダウン記法(```)とプログラミング言語の特徴量
  • 記号に関する特徴
  • 同じpromptに対するtextの比較特徴
  • TF-IDF特徴量

特徴量選択まで実施した後の特徴量重要度の上位は以下のようになりました。

上位には同じpromptに対するtextの比較特徴がきていました。また、いくつかのTF-IDF特徴も有効でした。

以下では特に有効だった「記号に関する特徴」と「同じpromptに対するtextの比較特徴」について詳しく説明します。

記号に関する特徴

学習データをパラパラとみてみると、2:Geminiの回答文にやたら記号が使われていることに気づきました。
1つサンプルデータを共有します。

  • prompt
    Are there any new or emerging employee benefits that employers should consider offering?
    
  • 0: GPT-3.5
      Some new or emerging employee benefits that employers may consider offering include wellness programs, mental health support, flexible work arrangements, remote work opportunities, financial wellness resources, and continued learning opportunities. These benefits can help improve employee satisfaction, well-being, and productivity.
    
  • 1: Claude-Haiku
      Here are a few new or emerging employee benefits that employers should consider offering:
    
    1. Remote work options: With the rise of remote work during the pandemic, employers should consider offering flexible work arrangements, such as remote work or hybrid models.
    
    2. Mental health support: Providing access to mental health resources, counseling, or employee assistance programs can help support employee well-being.
    (以下省略)
    
  • 2: Gemini
      **Emerging Employee Benefits:**
      
      **1. Well-being and Mental Health Support:**
      * Access to therapy and counseling services
      * Flexible work arrangements (e.g., remote work, reduced hours)
      * Mindfulness and stress management programs
      
      **2. Financial Wellness:**
      * Student loan repayment assistance
      * Financial planning and counseling services
      * Equity incentives and profit-sharing programs
      
    (以下省略)
    

回答文はMarkdown記法で書かれており、アスタリスク2つ(**)で囲まれた文字は太字で表記されます。
2:Geminiではこのアスタリスクの使用が他のLLMと比べても多い、また、見出しに使用する#も多用されていたため、記号の使用に関する特徴量を作成することで分類精度を上げることができるのではないかと仮説を立てました。
これを踏まえ、記号の多さや記号から始まっているかどうかを特徴量として作成しました。

以下、サンプルコードです。

# symbol count, and whether text starts from symbol
pattern = r'[^\w]'

def symbol_count(x):
    matches = re.findall(pattern, x)
    return len(matches)

def is_symbol_first(x):
    matches = re.findall(pattern, x[0])
    return len(matches)

# 記号から始まるtextか否か
df_train['is_symbol_first_text'] = df_train['text'].apply(lambda x: is_symbol_first(x))

# textの記号の数
df_train['symbol_count_text'] = df_train['text'].apply(lambda x: is_symbol_first(x))

# text内のcharacter数に対する記号数の割合
df_train['symbol_per_character_text'] = df_train['symbol_count_text'] / df_train['character_count_text']

上の方に共有した特徴量重要度の上位に記号に関する特徴量がきていることから、この特徴量が有効だったといえます。

同じpromptに対するtextの比較特徴

この特徴量は、今回の問題設定に特化した特徴量です。
今回のタスクは「あるプロンプトに対して回答したテキストデータがどのLLMから生成されたかを分類すること」なのですが、学習データとテストデータには1つのプロンプトに対して3つの生成AIの回答文が必ず存在していました。
本来であれば与えられたプロンプトと回答文のみから回答したLLMを予測すべきなのですが、この問題設定においては、他のLLMが回答した回答文を加味した上でLLMを予測することができます。
なので、同じpromptに対し、各回答文で作成した特徴量を比較した結果(割合や最大、最小等)を新たに特徴量として作成しました。

以下、サンプルコードです。

# compared texts by prompt
def compare_by_prompt(df, column='character_count_text'):
    # feature aggregation group by prompt
    df = pd.merge(
        df,
        df.groupby('prompt')[column].agg(['mean', 'max', 'min']).add_prefix(f'{column}_'),
        on='prompt', how='left'
    )

    df[f'{column}_per_mean'] = df[column] / (df[f'{column}_mean'] + 1e3)
    df[f'{column}_per_max'] = df[column] / (df[f'{column}_max'] + 1e3)
    df[f'{column}_per_min'] = df[column] / (df[f'{column}_min'] + 1e3)
    df[f'{column}_ismax'] = (df[column] == df[f'{column}_max']).astype(int)
    df[f'{column}_ismin'] = (df[column] == df[f'{column}_min']).astype(int)

    return df

df_train = compare_by_prompt(df_train, column='character_count_text')
(以下略)

比較特徴量はTF-IDF特徴量以外のすべての回答文に関する特徴量で作成しました。
特徴量重要度のグラフを見ると、TF-IDF特徴量以外のほとんどの特徴量がこの比較特徴量となっており、有効だったといえます。

特徴量選択

特に何も考えずに特徴量を量産したため相関が高い特徴量も多分にあると思ったのと、TF-IDF特徴も含めると5,000件くらいの特徴量となっていて過学習してしまうと思い、特徴量選択を行いました。

特徴量選択は、以下のステップで実施しました。

  • 1回目のCVでfeature importanceが0のデータと重要度上位100件以外で相関係数が0.95より大きいペアの特徴量が小さい方を削除
  • 再度CVを回してテストのSubmissionファイルを作成

以下、特徴量選択のコードです。

# df_imp: CVの結果の特徴量重要度の合計の値を格納したDataFrame
# feature selection
features_use = df_imp[df_imp['lgb_gain'] != 0]['feature'].values
top_100_features = set(df_imp.head(100)['feature'].values.tolist())

thld = 0.95
# 相関行列を計算
corr_matrix = df_train[features_use].corr().abs()

# 相関の高いfeatureペアのうち、上位100位以外の特徴量ペアの特徴量が低い方を削除
to_drop = []
for i in range(len(corr_matrix.columns)):
    for j in range(i+1, len(corr_matrix.columns)):
        if corr_matrix.iloc[i, j] > thld:
            feat1 = corr_matrix.columns[i]
            feat2 = corr_matrix.columns[j]
            if (feat1 in top_100_features) and (feat2 in top_100_features):
                continue

            score_feat1 = df_imp[df_imp['feature'] == feat1]['lgb_gain'].values[0]
            score_feat2 = df_imp[df_imp['feature'] == feat2]['lgb_gain'].values[0]
            print(f'{feat1}: {score_feat1}, {feat2}: {score_feat2}')

            if score_feat1 > score_feat2:
                to_drop.append(feat2)
            else:
                to_drop.append(feat1)

features_use = list(set(features_use.tolist()).difference(set(to_drop)))
print(len(features_use))

これを実施したことによりCVスコアが微増しました。

結果

Publicでは1位と大差をつけられていて、諦めていたのですが、、、

Privateでは逆転し、僅差で1位になりました!

Submission回数も我々のチームが1番多かったです笑

実施したかったこと

今回はめんどくさかったから時間の兼ね合いで実施できなかったですが、以下実装してみたかったです。

  • 言語モデル系:NLPコンペで強い言語モデルであるDeBERTaを試してみたかったです
  • 特徴量選択:RFE(Recursive Feature Elimination)というもう少しテクニカルな特徴量選択の手法を使ってみたかったです
  • 後処理:今回のデータの特性からテストデータにも1つのプロンプトに対して3つの回答文があるため、予測結果をダブらせないなど後処理面での工夫も入れたかったです

さいごに

久々のコンペ参加でしたが、やはり短期間で学びが多いなと感じました。
業務においては同じタスクをたくさんのデータサイエンティストで解くケースはほぼないので、たくさんの実装アイディアを得られるのはコンペ参加の魅力ですね!
特にモデル実装面において成長できる最短コースであることは間違い無いと思うので、Kaggleの分析コンペも含め引き続き参加していきたいと思います!!

松尾研究所テックブログ

Discussion