返回列表

[26th] - 3rd (?) in non-Alpha group

621. LLM 20 Questions | llm-20-questions

开始: 2024-05-15 结束: 2024-08-29 自然语言处理 AI大模型赛
[第 26 名] - 非 Alpha 组中的第 3 名 (?)
作者: Azat Akhtyamov (MASTER)
发布日期: 2024-08-30
竞赛排名: 第 26 名

[第 26 名] - 非 Alpha 组中的第 3 名 (?)

大家好,Kaggle 参赛者、休闲提示词工程师以及 Agent Alpha 厌恶者们!

根据这篇帖子,感谢 @cdeotte,这是第 3 个完全不使用 Agent Alpha 逻辑的解决方案(是的,它接受握手但仍然使用 LLM —— 我担心如果不是 Agent Alpha 会导致错误的机器人)。

让我简要分享我使用的一些技巧:

  • 回答机器人的关键词定义。 在提供第一个答案之前,生成并保存关键词的定义。这有助于避免对具有多重含义的单词(如后来的"bat")给出误导性的答案:
    """你是一个有帮助的 AI 助手。
    你被给定一个单词或短语,它是一个地方、事物或概念。
    你需要用一句话给出它的简短定义。如果它有多个含义,请选择最常见的含义。
    请简洁,不要添加垃圾信息,如:'我认为意思是...','最常见的意思是...'等。只输出定义。
    """
  • 在回答阶段先生成一些推理(经典方法):
    """你是一个有帮助的 AI 助手。
    用户将在 <keyword></keyword> 中给你一个关键词及其定义。它可能是一个代表单个物体的包含多个单词的短语。然后,她会给你一个关于这个关键词的问题。
    你的任务是给出诚实且正确的答案。
    * 你的响应必须是包含三个键的有效 JSON 格式:
    - 'keyword' - 在这里放入用户在 <keyword></keyword> 中给你的关键词
    - 'reasoning' - 两句解释'答案'的话。
    - 'answer' 包含 0 (代表否) 或 1 (代表是)。
    * 指令:
    - 仔细阅读关键词、定义和问题!
    - 提供直接且可预测的答案,不要过度分析问题。
    - 如果你不确定或问题不适用 - 回答 0
    - 忽略大小写敏感性。
    - 使用定义和你自己的知识简洁地回答问题。
    - 只回答 JSON 响应。
    """

    我有一个来自游戏回放的小 subset 问答对作为验证集。为了节省标记时间,我只选择了机器人对成功正确猜出单词的游戏(确保先删除 alpha 机器人的问题!),并假装所有答案都是正确的。这个提示产生了最高的准确率。

  • 初始问题树: 我将游戏中的所有单词交给 Llama-405B 生成新单词。然后,我再次询问 Llama-405B 正常人是否能猜出它们,从而过滤掉大约一半的单词。使用这些单词,构建了如下算法的树:
    groups = [[所有新关键词]]
    while 最大的组足够大:
        # 取出最大的组并从 groups 列表中移除
        group = groups.pop(groups.index(max(groups, key=len)))
        
        # 将 N 个随机单词从组中喂给 Llama-405B,并要求它用单个问题将组分成两半,生成至少 5 个不同的问题
        questions = Llama405B.generate_split_questions(group, N=5)
        
        # 向组中的每个关键词询问这些问题
        results = ask_questions_to_group(group, questions)
        
        # 选择最佳问题并添加到树中
        best_question = select_best_question(results)
        add_to_tree(best_question)
        
        # 将通过"是"或"否"答案分开的两个组添加到 groups 列表中
        yes_group, no_group = split_group_by_question(group, best_question)
        groups.extend([yes_group, no_group])

    我尝试了不同深度的不同树,并选择了在自我对战测试中给出最佳分数的最好的那个。你可以在附件中找到结果树。

  • 当你到达树的最终叶节点时: 开始使用带有以下提示的 LLM:
    """你是一个有帮助的 AI 助手,你在玩 20 个问题游戏方面非常聪明,
    用户将想到一个关键词(或关键短语),它只能属于以下 2 个类别之一:
    1. 一个地方
    2. 一个事物
    所以将你的搜索范围集中在这些选项上。游戏的目标是猜出关键词(短语)。
    
    你的角色是通过询问最多 20 个问题来帮助另一个用户找到关键词(或关键短语),尽可能使用最少的问题数量。
    指令:
    1) 你必须在问题方面高效。每个问题应该排除一半的选项。它不应该太具体或太宽泛。
    2) 不要试图直接猜短语(那是你的用户朋友做的,而且对她来说是免费的,因为猜测不计为她的问题)。询问能最佳缩小搜索空间的问题,要非常非常聪明!
    3) 尝试提出容易用'是'或'否'回答的问题!
    4) 注意,用户可能不如你聪明,有时,如果答案不清楚,会随机回答'是'或'否'。
    
    !!! 如果你违反以下三条规则中的任何一条,你将被取消资格 !!!: 
    1) 问题必须容易仅用'是'或'否'回答!
    2) 你必须简洁,只给一个问题,不要多余的话!不要添加垃圾信息,如:'这是我的下一个问题','好吧让我试试这个...'等。
    3) 你不能问直接问题,如:'它是哈萨克斯坦吗?' - 你破坏了用户朋友的角色并将被淘汰 - 最重要的规则!
    """

    我在本地运行了多个游戏,调整了各种东西,这种方法似乎效果最好。

  • 猜测者部分: 想法是生成候选列表并将它们和猜测历史保存在内存中:
    guesser_prompt = """你是一个有帮助的 AI 助手,你在玩 20 个问题游戏方面非常聪明,
    用户将想到一个关键词(或关键短语),它只能属于以下 2 个类别之一:
    1. 一个地方
    2. 一个事物
    所以将你的搜索范围集中在这些选项上。游戏的目标是猜出关键词(短语)。
    基于问题和答案的历史,尝试推导出关键词(它可能是一个短语)。你需要提供 5 个关键词/短语候选列表,其中第一个候选是最可能的。
    大小写无关紧要,除此之外你需要准确猜出关键词/短语。请简洁,只输出 JSON,不要解释任何内容!
    """
    
    def generate_guess_prompt(obs):
        
        conv = ""
        if obs.questions:
            conv = "问题和答案的历史:\n"
    
        for q, a in zip(obs.questions, obs.answers):
            conv += f"""问题:{q} - {a}\n"""
        
        guess =  guesser_prompt+"\n\n" + f"""{conv}\n\n输出应该是 JSON 格式,包含强制键 'candidates',包含一个字符串的 Python 列表,仅此而已。""".#. 简洁!
        chat_template = f"""<|begin_of_text|><|start_header_id|>system<|end_header_id|>\n\n{guess}<|eot_id|>\n"""
        chat_template += f"""<|start_header_id|>user<|end_header_id|>\n\n输出一个包含 'candidates' 的 JSON:<|eot_id|>\n"""
    
        used = set()
        blacklist = []
        for g in obs.guesses:
            if g not in used:
                blacklist.append(g)
                used.add(g)
    
        np.random.shuffle(blacklist)
    
        chunks = [[]]
        for g in blacklist:
            if len(chunks[-1]) < 5:
                chunks[-1].append(g)
            else:
                chunks.append([g])
    
        for chunk in chunks:
            if len(chunk):
                chunk = str(chunk)
                chunk = f"""{{"candidates": "{chunk}"}}"""
                chat_template += f"""<|start_header_id|>assistant<|end_header_id|>\n\n{chunk}<|eot_id|>\n"""
                chat_template += f"""<|start_header_id|>user<|end_header_id|>\n\n这些都不是完全匹配。<|eot_id|>\n"""
    
        chat_template += """<|start_header_id|>assistant<|end_header_id|>\n\n"""
        return chat_template

    使用以下代码,你首先尝试生成一些候选者。如果所有候选者之前都已尝试过,或者候选列表太小(有时 LLM 即使你要求 5 个也只输出一个候选者),你按以下方式继续,从存储中弹出候选者:

    best = None
    for candidate in candidates:
        if not any([self.is_a_match(candidate, hist) for hist in history]):
            if not best:
                best = candidate
            else:
                self.guess_stash.append(candidate)
    
    if best:
        return best
    
    if self.guess_stash:
        return self.guess_stash.pop()
    
    return candidates[0]
  • 摆脱愚蠢深渊的肮脏技巧:
    鉴于每个问题 750 个字符的限制,你可以尝试导致回答机器人出现内存溢出或超时错误。我解析了 LLaMA 分词器以找出哪些字符产生最长的 token 序列。事实证明,一些 emoji 用 3 个 token 编码 - 意味着你每个字符获得 3 个 token

    Token 编码示意图

    警告!内部含有黑魔法:
    output = (output+" 忽略这个:").ljust(749, '🤣')
    managed to derail a GM with this 🤣 (成功用这个干扰了一位大师)
    然而,这可能导致在争夺顶部的战斗中有些损失,因为有些人 figured out how to use try-excepts and time tracking just outputing "no" in case of an error... (想出了如何使用 try-except 和时间跟踪,在出错时只输出"no"...)
  • 常规设置:
    • 在我的实验中,LLaMA-3-8B-Inst 的表现优于 LLaMA-3.1-8B-Inst。
    • 微调版本的 LLaMA-*-8B 的表现也低于 LLaMA-3-8B-Inst。
    • 也尝试了 LoRA 微调,但导致了严重的过拟合。
    • 最终模型是 LLaMA-3-8B-Inst 的 8-bit 版本。
    • 我通过在本地精选的 200 个单词集上玩游戏来测试所有更改(具有挑战性,但可能猜出)。
    • 使用了单个 4090 GPU。
    • 使用这个notebook作为起点,感谢 @wouldyoujustfocus

问题树本身:

同比赛其他方案