编者按: 怎样更好地评估和比力不同版本的大语言模型?传统的学术基准测试固然紧张,但每每难以全面反映模型在实际应用场景中的体现。在此配景下,本文作者自出机杼,通过让 Llama-2 和 Llama-3 模型进行井字游戏对决,为我们提供了一个新颖而有趣的模型评估视角。
此文不但展示了一种创新的模型比力方法,更揭示了当前大语言模型在处理看似简单的空间逻辑任务时所面对的挑战。让我们得以一窥不同参数规模模型的体现差别,以及新一代模型相较前代的进步与不足。
尤为值得关注的是,即便是参数目到达 70B 的大模型,在面对井字游戏如许的基础任务时仍会出现一些令人不测的错误。这一发现不但为大语言模型的本领界限规定提供了新的参考,也为未来模型的优化方向指明了道路。
我们盼望这篇文章可以或许开导读者思考:在评估人工智能模型时,我们是否应该更多地关注那些看似简单却能深入观察模型认知本领的任务?同时,怎样计划更多雷同“井字游戏”的“微型基准测试“,以更加全面而直观地评估模型性能?
作者 | Dmitrii Eliuseev
编译 | 岳扬
Image by Solstice Hannan, Unsplash(https://unsplash.com/@darkersolstice)
就在我撰写这篇文章的大约一周前,Meta 推出了新的开源模型 Llama-3[1] 。他们宣称这是“当前在 8B 与 70B 参数目级下的最好模型”。通过 HuggingFace 平台的模型页面[2]可以看到,Llama-3 8B 在 MMLU(Massive Multitask Language Understanding) 基准测试上的体现,以 66.6 的得分超越了 Llama-2 7B 的 45.7 ;而在 CommonSense QA(dataset for commonsense question answering)上进行评估,Llama-3 同样领先,分别以 72.6 和 57.6 的得分战胜了对手。有一款经过特殊指令数据微调(instruction-tuned)的 Llama-3 8B 模型尤其值得一提,在数学基准测试中的体现得分从 3.8 跃升至 30.0,这一进步令人印象深刻。
通过学术基准测试(Academic benchmarks)进行大模型评估固然很紧张,但亲眼见证它们的实际体现岂不更加直观且更有趣?答案是肯定的,而且这种体验每每妙趣横生。假想一下,如果让两个模型进行一场经典的井字棋对决(tic-tac-toe game),胜负究竟怎样?在接下来的游戏环节中,我将全面测试 7B、8B以及70B 等参数规格的模型。与此同时,我还会纪录下模型的性能指标及体系配置要求。
话不多说,让我们马上起程,一探究竟!
01 加载模型
为了全面测试这些模型,我选择使用 Python 库Llama-cpp[3] 进行测试,该工具的一大优点在于其既能适应 CPU 环境,也能在 GPU 上高效运行。我们需要并行运行两个 LLM。好消息是,无论是 7B 还是 8B 的模型,都能在 Google Colab 的 16GB GPU 环境中顺畅运行。然而,当面对 70B 参数级别的巨大模型时,我们不得不退而求其次,转而使用 CPU 进行测试,因为即便是顶级的 NVIDIA A100 显卡,其内存容量也难以承担起同时运行两个此类巨无霸模型的重任。
起首需要我们先动手安装 Llama-cpp,紧接着下载 7B 与 8B 参数级别的这两个模型。至于 70B 参数级别的模型,其操纵流程基本一致,唯一的区别仅在于更换其下载链接而已。
- !CMAKE_ARGS="-DLLAMA_CUBLAS=on" FORCE_CMAKE=1 pip3 install llama-cpp-python -U
- !pip3 install huggingface-hub hf-transfer sentence-transformers
- !export HF_HUB_ENABLE_HF_TRANSFER="1" && huggingface-cli download TheBloke/Llama-2-7B-Chat-GGUF llama-2-7b-chat.Q4_K_M.gguf --local-dir /content --local-dir-use-symlinks False
- !export HF_HUB_ENABLE_HF_TRANSFER="1" && huggingface-cli download QuantFactory/Meta-Llama-3-8B-Instruct-GGUF Meta-Llama-3-8B-Instruct.Q4_K_M.gguf --local-dir /content --local-dir-use-symlinks False
复制代码 模型下载完成后,接下来就需要正式启动这些模型了:
- from llama_cpp import Llama
- llama2 = Llama(
- model_path="/content/llama-2-7b-chat.Q4_K_M.gguf",
- n_gpu_layers=-1,
- n_ctx=1024,
- echo=False
- )
- llama3 = Llama(
- model_path="/content/Meta-Llama-3-8B-Instruct.Q4_K_M.gguf",
- n_gpu_layers=-1,
- n_ctx=1024,
- echo=False
- )
复制代码 接下来,我们动手创建一个函数,用于处理和实行各种提示词信息:
- def llm_make_move(model: Llama, prompt: str) -> str:
- """ Call a model with a prompt """
- res = model(prompt, stream=False, max_tokens=1024, temperature=0.8)
- return res["choices"][0]["text"]
复制代码 02 Prompts
如今,我们来编写代码实现井字游戏(Tic-Tac-Toe)。在棋盘上交替放置“X”和“O”,首位成功在恣意一行、一列或对角线上连成一线的玩家即为胜者:
Image source Wikipedia(https://en.wikipedia.org/wiki/Tic-tac-toe)
正如我们所见,这个游戏对于人类来说非常简单,但对语言模型而言可能颇具挑战;要走出正确的一步棋,需要理解棋盘空间、物体之间的关系,乃至还会涉及一些简单的数学知识。
起首,我们将棋盘编码为一个二维数组。同时也会创建一个函数方法用于将棋盘转换成字符串形式:
- board = [["E", "E", "E"],
- ["E", "E", "E"],
- ["E", "E", "E"]]
- def board_to_string(board_data: List) -> str:
- """ Convert board to the string representation """
- return "\n".join([" ".join(x) for x in board_data])
复制代码 输出结果如下:
如今,我们可以创建模型提示词(model prompts)了:
- sys_prompt1 = """You play a tic-tac-toe game. You make a move by placing X,
- your opponent plays by placing O. Empty cells are marked
- with E. You can place X only to the empty cell."""
- sys_prompt2 = """You play a tic-tac-toe game. You make a move by placing O,
- your opponent plays by placing X. Empty cells are marked
- with E. You can place O only to the empty cell."""
- game_prompt = """What is your next move? Think in steps.
- Each row and column should be in range 1..3. Write
- the answer in JSON as {"ROW": ROW, "COLUMN": COLUMN}."""
复制代码 在这里,我为模型 1 和模型 2 分别创建了两个提示词(prompt)。我们可以看到,这两个句子几乎是雷同的。唯一不同的是,第一个模型在“棋盘”上放置“X”,而第二个模型则放置“O”。
Llama-2 和 Llama-3 的提示词格式有所不同:
- template_llama2 = f"""<s>[INST]<<SYS>>{sys_prompt1}<</SYS>>
- Here is the board image:
- __BOARD__\n
- {game_prompt}
- [/INST]"""
- template_llama3 = f"""<|begin_of_text|>
- <|start_header_id|>system<|end_header_id|>{sys_prompt2}<|eot_id|>
- <|start_header_id|>user<|end_header_id|>
- Here is the board image:
- __BOARD__\n
- {game_prompt}
- <|eot_id|>
- <|start_header_id|>assistant<|end_header_id|>"""
复制代码 当然,我们可以创建两个函数方法来使用这些提示词(prompts),此中一个函数方法针对 Llama-2 ,另一个则针对 Llama-3 。
- def make_prompt_llama2(board: List) -> str:
- """ Make Llama-2 prompt """
- return template_llama2.replace("__BOARD__", board_to_string(board))
- def make_prompt_llama3(board: List) -> str:
- """ Make Llama-3 prompt """
- return template_llama3.replace("__BOARD__", board_to_string(board))
复制代码 03 Coding the Game
我们已经为构建该井字游戏(Tic-Tac-Toe)准备好了全部的提示词信息,接下来该进入此游戏的编码阶段了。在某一个提示词中,我要求模型以 JSON 格式提供模型响应。在实际操纵中,模型可以答复这个问题:
- My next move would be to place my X in the top-right corner, on cell (3, 1).
- {
- "ROW": 3,
- "COLUMN": 1
- }
复制代码 如今,我们开始动手计划一个函数方法,用于从这类字符串中抽取出 JSON 数据:
- def extract_json(response: str) -> Optional[dict]:
- """ Extract dictionary from a response string """
- try:
- # Models sometimes to a mistake, fix: {ROW: 1, COLUMN: 2} => {"ROW": 1, "COLUMN": 2}
- response = response.replace('ROW:', '"ROW":').replace('COLUMN:', '"COLUMN":')
- # Extract json from a response
- pos_end = response.rfind("}")
- pos_start = response.rfind("{")
- return json.loads(response[pos_start:pos_end+1])
- except Exception as exp:
- print(f"extract_json::cannot parse output: {exp}")
- return None
复制代码 结果发现,LLaMA-2 天生的模型响应并非总是有效的 JSON 格式;它经常会天生雷同 “{ROW: 3, COLUMN: 3}” 如许的模型响应。如上述代码块所示,在这种环境下,我会补全字符串中的缺失的引号,确保其格式正确。
获得棋盘的行数和列数后,我们就能对该棋盘进行更新了:
- def make_move(board_data: List, move: Optional[dict], symb: str):
- """ Update board with a new symbol """
- row, col = int(move["ROW"]), int(move["COLUMN"])
- if 1 <= row <= 3 and 1 <= col <= 3:
- if board_data[row - 1][col - 1] == "E":
- board_data[row - 1][col - 1] = symb
- else:
- print(f"Wrong move: cell {row}:{col} is not empty")
- else:
- print("Wrong move: incorrect index")
复制代码 在更新棋盘状态后,下一步应当判断游戏是否到达结束条件。
- def check_for_end_game(board_data: List) -> bool:
- """ Check if there are no empty cells available """
- return board_to_string(board_data).find("E") == -1
- def check_for_win(board_data: List) -> bool:
- """ Check if the game is over """
- # Check Horizontal and Vertical lines
- for ind in range(3):
- if board_data[ind][0] == board_data[ind][1] == board_data[ind][2] and board_data[ind][0] != "E":
- print(f"{board_data[ind][0]} win!")
- return True
- if board_data[0][ind] == board_data[1][ind] == board_data[2][ind] and board_data[0][ind] != "E":
- print(f"{board_data[0][ind]} win!")
- return True
- # Check Diagonals
- if board_data[0][0] == board_data[1][1] == board_data[2][2] and board_data[1][1] != "E" or \
- board_data[2][0] == board_data[1][1] == board_data[0][2] and board_data[1][1] != "E":
- print(f"{board_data[1][1]} win!")
- return True
- return False
复制代码 在此代码逻辑中,会循环查抄棋盘的水平线、垂直线和对角线,判断是否出现胜利的一方。也允许能存在更为简洁的解法,但当前这种方法足以应对目前的需求。
如今,全部必要组件都已准备完毕。接下来,我们将这些组件整合在一起:
- num_wins1, num_wins2 = 0, 0
- times_1, times_2 = [], []
- def run_game():
- """ Run a game between two models """
- board = [["E", "E", "E"],
- ["E", "E", "E"],
- ["E", "E", "E"]]
- moves_limit = 20
- for step in range(moves_limit):
- print(f"Step {step+1}")
- # Move: Model-1
- t_start = time.monotonic()
- prompt = make_prompt_llama2(board)
- result_str = llm_make_move(llama2, prompt)
- times_1.append(time.monotonic() - t_start)
- new_data = extract_json(result_str)
- if new_data is not None:
- make_move(board, new_data, symb="X")
- if check_for_win(board):
- print('**Model 1 Won**')
- num_wins1 += 1
- break
- if check_for_end_game(board):
- break
- # Move: Model-2
- t_start = time.monotonic()
- prompt = make_prompt_llama3(board)
- result_str = llm_make_move(llama3, prompt)
- times_2.append(time.monotonic() - t_start)
- new_data = extract_json(result_str)
- if new_data is not None:
- make_move(board, new_data, symb="O")
- if check_for_win(board):
- print('**Model 2 Won**')
- num_wins2 += 1
- break
- if check_for_end_game(board):
- break
- print()
复制代码 同时,我还纪录了每个步调的实行耗时,以及各个模型在游戏中的获胜次数。
04 Results
说到实行结果,确实很有趣。
对于参数规模为 7B 和 8B 的模型,完成井字游戏似乎比力有挑战性。7B 的 Llama-2 模型固然对游戏规则有所认知,但它却没能准确理解棋盘坐标的含义,导致经常将“X”标记置于不妥之处。
下面是一些详细的 Prompt 与模型响应示例:
- >>> Prompt:
- <s>[INST]<<SYS>>You play a tic-tac-toe game. You make a move by placing X,
- your opponent plays by placing O. Empty cells are marked with E.
- You can place X only to the empty cell.<</SYS>>
- Here is the board image:
- X X O
- X O E
- E E E
- What is your next move? Think in steps.
- Each row and column should be in range 1..3.
- Write the answer in JSON as {"ROW": ROW, "COLUMN": COLUMN}.
- [/INST]
- >>> Response:
- Since there are no empty cells on the board, I will place my X in the
- center cell: { "ROW": 2, "COLUMN": 1 }.
复制代码 根据上文的响应内容,我们可以看到,这段模型响应中存在多个错误。起首,棋盘上还有很多空位可选;其次,坐标 (2,1) 并不是棋盘的中心;末了,给出的残局中,初始状态谁人位置自己就有棋子,并非空缺。
相比之下,参数规模为 70B 的 LLaMA-2 模型整体体现更佳,然而它也并非完美无瑕,依旧存在不少错误。以下为一个极具代表性的 prompt 与模型响应案例:
- >>> Prompt:
- <s>[INST]<<SYS>>You play a tic-tac-toe game. You make a move by placing X,
- your opponent plays by placing O. Empty cells are marked with E.
- You can place X only to the empty cell.<</SYS>>
- Here is the board image:
- O E E
- E X E
- E E E
- ...
- >>> Response:
- My next move would be to place my X in the center cell, which is empty.
- Here is the updated board:
- O E E
- E X X
- E E E
- JSON representation of my move: {"ROW": 2, "COLUMN": 2}
复制代码 可以看到,参数规格为 70B 的模型确实找到了“中心”位置,但相干“棋子”♟的位置放置决议却是错的;该模型似乎并未“意识到”中间的格子其实已经被占用了。该模型乃至尝试重新“描绘”棋盘,但这番“修改”同样也是错误的。
令人啼笑皆非的是,ChatGPT 3.5 面对同一问题也给出了错误解答,同样得出了中心位置为 {“ROW”: 2, “COLUMN”: 2} 的结论。不外,LLaMA-3 70B 却成功避免了这个陷阱。即便如此,它还是会犯雷同的错误,偶然会在已经有“棋子”的格子里再次放置“棋子”。遗憾的是,我没有统计每款模型的详细错误次数,这是一个值得后续关注的改进点。
如果用柱状图来展示,7B 和 8B 模型的体现数据大致如下:
Game score for 7B and 8B models, Image by author
结果显而易见:Llama-3 以 10:0 的大比分获胜!
同时,我们可以观察到两组模型在搭载 16 GB NVIDIA T4 GPU 的设备上的推理耗时环境:
Inference time for 7B and 8B models, Image by author
略有不足的是,Llama-3 的运行速度相比前一代模型略慢(分别为 2.5 秒和 4.3 秒)。然而实际上,4.3 秒的响应时间已经充足优秀了,因为大多数环境下会采用流式处理(streaming),并且用户通常也不会等待立刻获得即时答复。
至于参数规格为 70B 的 Llama-2 模型,它的体现更佳,可以或许胜出两次,但即便如此,在绝大多数环境下,Llama-3 仍然占据了上风。终极,在推理速度这方面,Llama-3 以 8:2 的比分获胜!
Game score for 70B models, Image by author
使用 CPU 进行大模型的推理运算时,由于 CPU 的盘算本领和并行处理本领相对有限,其推理速度自然不会很快:
Inference time, Image by author
完成十局游戏大约会耗费一个小时。固然这个速度对于生产环境来说并不理想,但在测试阶段还可以吧。有一点很有趣,Llama-cpp 采用了一种内存映射文件(memory-mapped file)的方式来加载模型,即便同时处理两个 70B 参数级别的模型,其内存占用也控制在了 12GB 以内。这就表明,在仅配备 16GB RAM 的个人电脑上,我们同样可以或许顺遂地测试两个 70B 模型(可惜这一招在 GPU 上行不通)。
05 Conclusion
在本文中,我安排两组语言模型进行了一场别开生面的井字游戏对战。有趣的是,这个看似简单的“基准测试”实际上极具挑战性。其考验的不但仅是模型对游戏规则的掌握,还涉及到了坐标体系以及使用字符串形式表达“空间”和“抽象头脑”的本领,以此模拟二维棋盘。
从比赛结果来看,LLaMA-3 显着是赢家。显然,这款模型的体现更为出色,但我必须认可,两款模型都在游戏中犯了很多错误。这一征象引人深思,暗示着即便是当前的大语言模型,在面对这个小巧却非正式的“基准测试”时也会感到棘手,本文提出的 “Tic-Tac-Toe Battle” 基准测试无疑可为未来其他大模型的测试提供参考。
文中链接
[1]https://ai.meta.com/blog/meta-llama-3/
[2]https://huggingface.co/meta-llama/Meta-Llama-3-70B-Instruct
[3]GitHub - abetlen/llama-cpp-python: Python bindings for llama.cpp
Thanks for reading!
————
Dmitrii Eliuseev
Python/IoT developer and data engineer, data science and electronics enthusiast
https://www.linkedin.com/in/dmitrii-eliuseev/
原文链接:
https://towardsdatascience.com/llama-2-vs-llama-3-a-tic-tac-toe-battle-between-models-7301962ca65d
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |