返回列表

4th Place Solution

620. LMSYS - Chatbot Arena Human Preference Predictions | lmsys-chatbot-arena

开始: 2024-05-02 结束: 2024-08-12 自然语言处理 数据算法赛
第四名解决方案 - LMSYS Chatbot Arena

第四名解决方案

作者: fufu2022
发布日期: 2024-08-18
竞赛排名: 第 4 名
团队成员: @distiller, @xxycbadpanda, @w1623843225

我要感谢 Kaggle 和 LMSYS 提供这次出色的竞赛。通过这次比赛,我们学到了很多关于大语言模型(LLM)LoRA 训练的技术。我也想感谢每一位团队成员,@distiller, @xxycbadpanda, @w1623843225,感谢他们的辛勤工作。如果没有他们的创造力、想法和工程技能,我们很难取得如此优异的成绩。

数据准备

首先,我们使用了官方数据(55k)+ 33k 去重数据,设置 fold n_splits=20,并且只在一个 fold 上进行训练以确保有更多的训练数据。此外,我们为 ultrafeedback 数据集中的 30,000 个样本创建了伪标签作为补充数据。

提示词设计 (Prompt)

我们设计了一个独特的提示词。这个提示词的优势在于,当对话长度超过 max_length 时,它可以合理地截断最后一轮对话。这确保了 prompt、response_a 和 response_b 都有一定的比例显示,避免了只有 prompt 或 response_a 在最后一轮被截断的情况。我们甚至设定了一个规则,如果最后一轮剩余的 token 数量少于 80 个,我们将完全丢弃该轮(及其之后的轮次)。这些阈值和比例是通过观察训练集确定的。

def tokenize_cls_p3(example, tokenizer, max_length, is_train):
    input_ids = []
    attention_mask = []
    dot_tokens = tokenizer("......", add_special_tokens=False)["input_ids"]
    final_p_tokens = tokenizer("\n\n---\nWhich response is better? [A or B or tie]\nAnswer: ", add_special_tokens=False)["input_ids"]
    for ps, ras, rbs in zip(example['prompt'], example['response_a'], example['response_b']):
        one_input_ids = [tokenizer.bos_token_id]
        prev_tokens_num = 2 + len(final_p_tokens) # 2 for bos_token and eos_token
        for idx, (p, ra, rb) in enumerate(zip(ps, ras, rbs)):
            r_tokens  = tokenizer(f'\n\n## Round {idx+1}:' if idx else f'## Round {idx+1}:', add_special_tokens=False)["input_ids"]
            p_tokens  = tokenizer(f'\n### Prompt:\n{p}', add_special_tokens=False)["input_ids"]
            ra_tokens = tokenizer(f'\n\n### Response A:\n{ra}', add_special_tokens=False)["input_ids"]
            rb_tokens = tokenizer(f'\n\n### Response B:\n{rb}', add_special_tokens=False)["input_ids"]
            all_tokens_num = prev_tokens_num + len(r_tokens) + len(p_tokens) + len(ra_tokens) + len(rb_tokens)

            if all_tokens_num > max_length:
                remain_tokens_num = max_length - prev_tokens_num - len(r_tokens) - 3*len(dot_tokens) 
                if remain_tokens_num >= 80:
                    p_tokens  =  p_tokens[:int(remain_tokens_num*0.2)] + dot_tokens if len( p_tokens) > int(remain_tokens_num*0.2) else  p_tokens
                    ra_tokens = ra_tokens[:int(remain_tokens_num*0.4)] + dot_tokens if len(ra_tokens) > int(remain_tokens_num*0.4) else ra_tokens
                    rb_tokens = rb_tokens[:int(remain_tokens_num*0.4)] + dot_tokens if len(rb_tokens) > int(remain_tokens_num*0.4) else rb_tokens
                    one_input_ids += r_tokens + p_tokens + ra_tokens + rb_tokens
                break
            else:
                prev_tokens_num = all_tokens_num
                one_input_ids += r_tokens + p_tokens + ra_tokens + rb_tokens
        
        one_input_ids += final_p_tokens + [tokenizer.eos_token_id]
        one_attention_mask = [1] * len(one_input_ids)

        input_ids.append(one_input_ids)
        attention_mask.append(one_attention_mask)
    
    if is_train:
        labels = [0 if a_win else 1 if b_win else 2 for a_win, b_win, tie in zip(example['winner_model_a'], example['winner_model_b'], example['winner_tie'])]

        return {
            "input_ids": input_ids,
            "attention_mask": attention_mask,
            "labels": labels,
        }
    else:
        return {
            "input_ids": input_ids,
            "attention_mask": attention_mask,
        }

训练

模型

我们选择了 gemma-2-9b-it 作为我们的起始模型,它的表现比我们测试的 Llama3 8b 和 Llama3.1 8b 都要好得多。

我们使用 Gemma2ForSequenceClassification 进行 3 分类,并使用 lora bf16 对模型进行微调。我们团队成员有不同的 GPU 设置,但得分最高的实验是在四个 A100 GPU 上进行的。

Max_length: 2048

LoRA 参数

  freeze_layers: 0
  lora_r: 64
  lora_alpha: 16
  lora_dropout: 0.05
  lora_bias: "none"
  lora_target_modules:
    - "q_proj"
    - "k_proj"
    - "v_proj"
    - "o_proj"
    - "gate_proj"
    - "up_proj"
    - "down_proj"

流程

  1. 第一阶段: 我们使用了官方数据(55k)+ 33k 去重数据,设置 fold n_splits=20,并且只在一个 fold 上进行训练。
  2. 第二阶段: 我们使用第一阶段的模型为 ultrafeedback 数据集中的 30,000 个样本创建伪标签。然后,我们将这些数据与第一阶段的数据相结合(总计超过 100k),从头训练一个新模型。

第一阶段的每个实验大约需要 10 小时,第二阶段在 4*A100 40G GPU 上大约需要 15 小时。

推理与后处理

推理代码大体上与训练代码相似。然而,一些区别包括在推理期间将 max_length 增加到 3072,并交换 response_aresponse_b 进行 TTA(测试时增强)。最终结果是两者输出的平均值。

我们对两种情况应用了后处理(两者之间有一些重叠):

  1. 如果 response_aresponse_b 为空(例如 '[null]', '[]', '[ ]'),我们假设非空响应为获胜者。然而,考虑到对数损失对极端值的极端敏感性以及标签中的噪声,我们根据训练集的观察,将空、非空和平局的预测值固定为 [0.04, 0.88, 0.08]。
  2. 如果 response_aresponse_b 相同,我们将其视为平局,并将预测值固定为 [0.06, 0.06, 0.88]。

具体的后处理代码如下:

df2 = pd.read_csv('/kaggle/input/lmsys-chatbot-arena/test.csv')
df2['id'] = df2['id'].astype(str)

a_null_df = df2[(df2["response_a"]== '[null]') | (df2["response_a"]== '[]') | (df2["response_a"]== '[ ]') | (df2["response_a"]== '[  ]') | (df2["response_a"]== '[""]') | (df2["response_a"]== '["",""]')]
a_null_id_list = a_null_df["id"].tolist()
submission_df.loc[submission_df['id'].isin(a_null_id_list), ['winner_model_a', 'winner_model_b', 'winner_tie']] = [0.04, 0.88, 0.08]

b_null_df = df2[(df2["response_b"]== '[null]') | (df2["response_b"]== '[]') | (df2["response_b"]== '[ ]') | (df2["response_b"]== '[  ]') | (df2["response_b"]== '[""]') | (df2["response_b"]== '["",""]')]
b_null_id_list = b_null_df["id"].tolist()
submission_df.loc[submission_df['id'].isin(b_null_id_list), ['winner_model_a', 'winner_model_b', 'winner_tie']] = [0.88, 0.04, 0.08]

same_a_b_df2 = df2[(df2["response_a"]==df2["response_b"])]
same_a_b_id_list = same_a_b_df2["id"].tolist()
submission_df.loc[submission_df['id'].isin(same_a_b_id_list), ['winner_model_a', 'winner_model_b', 'winner_tie']] = [0.06, 0.06, 0.88]
同比赛其他方案