400 lines
18 KiB
Python
400 lines
18 KiB
Python
# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
|
|
# Copyright 2020 The HuggingFace Team. All rights reserved.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
from typing import Optional
|
|
|
|
from parameterized import parameterized_class
|
|
|
|
from paddlenlp.transformers import AutoTokenizer
|
|
from paddlenlp.transformers.tokenizer_utils import ChatTemplate
|
|
|
|
|
|
class ChatTemplateTest(unittest.TestCase):
|
|
chat_template_config_file = "./tests/fixtures/chat_template.json"
|
|
|
|
@property
|
|
def chat_template(self):
|
|
return ChatTemplate.from_file(self.chat_template_config_file)
|
|
|
|
def test_inference_template(self):
|
|
query = "你好"
|
|
final_query = self.chat_template(query)
|
|
expected_query = f"你是一个人工智能助手\nHuman: {query}<sep> Bot:"
|
|
self.assertEqual(final_query, expected_query)
|
|
|
|
def test_inference_conversation_template(self):
|
|
conversations = [["你好", "您好,我是个人人工智能助手,请问有什么可以帮您。"], ["今天的天气怎么样?"]]
|
|
final_query = self.chat_template(conversations)
|
|
expected_query = "你是一个人工智能助手\nHuman: 你好<sep> Bot:您好,我是个人人工智能助手,请问有什么可以帮您。\nHuman: 今天的天气怎么样?<sep> Bot:"
|
|
self.assertEqual(final_query, expected_query)
|
|
|
|
def test_inference_conversation_template_with_one_part(self):
|
|
conversations = [["你好"], ["今天的天气怎么样?"]]
|
|
with self.assertRaises(AssertionError):
|
|
self.chat_template(conversations)
|
|
|
|
def test_null_chat_template(self):
|
|
chat_template = ChatTemplate()
|
|
query = "今天吃啥"
|
|
final_query = chat_template(query)
|
|
assert final_query == query
|
|
|
|
def test_system_query(self):
|
|
system = "你是一个人工智能助手:"
|
|
query_template = "Human: {{query}}"
|
|
chat_template = ChatTemplate(system=system, query=query_template)
|
|
query = "今天吃啥"
|
|
final_query = chat_template(query)
|
|
assert final_query == system + query_template.replace("{{query}}", query)
|
|
|
|
def test_conversation(self):
|
|
conversation = ["Human: {{user}}<sep>", "Bot: {{bot}}\n\n"]
|
|
chat_template = ChatTemplate(conversation=conversation)
|
|
|
|
query = "今天吃啥"
|
|
final_query = chat_template(query)
|
|
assert final_query == query
|
|
|
|
second_query = [["你好", "您好,我是个人人工智能助手"], [query]]
|
|
final_query = chat_template(second_query)
|
|
assert final_query == "Human: 你好<sep>Bot: 您好,我是个人人工智能助手\n\n" + query
|
|
|
|
|
|
class ChatTemplateContextDataTest(unittest.TestCase):
|
|
chat_template_config_file = "./tests/fixtures/chat_template_with_context.json"
|
|
|
|
@property
|
|
def chat_template(self):
|
|
return ChatTemplate.from_file(self.chat_template_config_file)
|
|
|
|
def test_inference_template(self):
|
|
query = [["你好"]]
|
|
context_data = {
|
|
"system": "<<SYSTEM-MESSAGE>>",
|
|
"instruction": "<<INSTRUCTION-MESSAGE>>",
|
|
}
|
|
final_query = self.chat_template(query, context_data=context_data)
|
|
expected_query = "你是一个人工智能助手<<SYSTEM-MESSAGE>>-<<INSTRUCTION-MESSAGE>>\nHuman: 你好<sep> Bot:"
|
|
self.assertEqual(final_query, expected_query)
|
|
|
|
|
|
class ChatTemplateIntegrationTest(unittest.TestCase):
|
|
def test_linlyai_chinese_llama_2_chat_template(self):
|
|
tokenizer = AutoTokenizer.from_pretrained("linly-ai/chinese-llama-2-7b")
|
|
query = "你好"
|
|
final_query = tokenizer.apply_chat_template(query, tokenize=False)
|
|
expected_query = f"<s>### Instruction:{query} ### Response:"
|
|
self.assertEqual(final_query, expected_query)
|
|
|
|
# test multi turns conversation
|
|
query = [["你好", "您好,我是个人人工智能助手"], ["今天吃啥"]]
|
|
final_query = tokenizer.apply_chat_template(query, tokenize=False)
|
|
expected_query = "<s>### Instruction: 你好 ### Response:您好,我是个人人工智能助手 </s>### Instruction:今天吃啥 ### Response:"
|
|
self.assertEqual(final_query, expected_query)
|
|
|
|
def test_linlyai_chinese_llama_2_chat_template_with_none_saved(self):
|
|
tokenizer = AutoTokenizer.from_pretrained("linly-ai/chinese-llama-2-7b")
|
|
tokenizer.chat_template = None
|
|
with tempfile.TemporaryDirectory() as tempdir:
|
|
tokenizer.save_pretrained(tempdir)
|
|
|
|
chat_template_file = os.path.join(tempdir, "chat_template.json")
|
|
self.assertFalse(os.path.exists(chat_template_file))
|
|
|
|
def test_chatglm_bellegroup(self):
|
|
# refer to: https://huggingface.co/THUDM/chatglm-6b/blob/main/modeling_chatglm.py#L1267
|
|
tokenizer = AutoTokenizer.from_pretrained("THUDM/chatglm-6b-v1.1")
|
|
query = [["你好", "您好,我是个人人工智能助手"], ["今天吃啥"]]
|
|
final_query = tokenizer.apply_chat_template(query, tokenize=False)
|
|
expected_query = "[Round 0]\n问:你好\n答:您好,我是个人人工智能助手\n[Round 1]\n问:今天吃啥\n答:[gMASK]<sop>"
|
|
self.assertEqual(final_query, expected_query)
|
|
|
|
def test_bloom_bellegroup(self):
|
|
# refer to: https://huggingface.co/BelleGroup/BELLE-7B-2M#use-model
|
|
tokenizer = AutoTokenizer.from_pretrained("bellegroup/belle-7b-2m")
|
|
query = "你好"
|
|
final_query = tokenizer.apply_chat_template(query, tokenize=False)
|
|
expected_query = f"Human: {query}\n\nAssistant:"
|
|
self.assertEqual(final_query, expected_query)
|
|
|
|
def test_qwen_14b_chat(self):
|
|
# refer to: https://huggingface.co/Qwen/Qwen-14B-Chat/blob/main/qwen_generation_utils.py#L119
|
|
|
|
# 1. test render base on query & conversation data
|
|
tokenizer = AutoTokenizer.from_pretrained("qwen/qwen-14b-chat")
|
|
query = "你好"
|
|
final_query = tokenizer.apply_chat_template(query, tokenize=False)
|
|
|
|
expected_query = "<|im_start|>system\nYou are a helpful assistant.<|im_end|>\n<|im_start|>user\n你好<|im_end|>\n<|im_start|>assistant\n"
|
|
self.assertEqual(final_query, expected_query)
|
|
|
|
query = [["你好", "您好,我是个人人工智能助手"], ["今天吃啥"]]
|
|
final_query = tokenizer.apply_chat_template(query, tokenize=False)
|
|
|
|
expected_query = (
|
|
"<|im_start|>system\nYou are a helpful assistant.<|im_end|>\n<|im_start|>user\n你好<|im_end|>\n"
|
|
"<|im_start|>assistant\n您好,我是个人人工智能助手<|im_end|>\n"
|
|
"<|im_start|>user\n今天吃啥<|im_end|>\n<|im_start|>assistant\n"
|
|
)
|
|
self.assertEqual(final_query, expected_query)
|
|
|
|
|
|
@parameterized_class(
|
|
["model_name"],
|
|
[
|
|
["linly-ai/chinese-llama-2-7b"],
|
|
# ["THUDM/chatglm-6b-v1.1"],
|
|
["bellegroup/belle-7b-2m"],
|
|
],
|
|
)
|
|
class TestChatTemplateSpecialTokens(unittest.TestCase):
|
|
model_name: str
|
|
|
|
def common_prefix(self, arr1, arr2):
|
|
min_length = min(len(arr1), len(arr2))
|
|
for i in range(min_length):
|
|
if arr1[i] != arr2[i]:
|
|
return arr1[:i]
|
|
return arr1[:min_length]
|
|
|
|
def get_common_prefix(self, tokenizer):
|
|
first_ids = tokenizer("欢迎使用 PaddlePaddle")["input_ids"]
|
|
second_ids = tokenizer("")["input_ids"]
|
|
prefix_ids = self.common_prefix(first_ids, second_ids)
|
|
return prefix_ids
|
|
|
|
def test_prefix(self):
|
|
prompt = "欢迎使用 PaddleNLP 大模型开发套件"
|
|
tokenizer = AutoTokenizer.from_pretrained(self.model_name)
|
|
result = tokenizer.apply_chat_template(prompt, tokenize=False)
|
|
|
|
result_ids = tokenizer(result, add_special_tokens=False)["input_ids"]
|
|
special_token_prefix_ids = self.get_common_prefix(tokenizer)
|
|
assert result_ids[: len(special_token_prefix_ids)] == special_token_prefix_ids
|
|
|
|
|
|
class TestChatTemplateTruncation(unittest.TestCase):
|
|
class DataArg:
|
|
def __init__(self, max_length, src_length: Optional[int] = None):
|
|
self.max_length: int = max_length
|
|
if src_length is None:
|
|
src_length = self.max_length - 8
|
|
|
|
self.src_length: int = src_length
|
|
|
|
chat_template_config_file = "./tests/fixtures/chat_template.json"
|
|
|
|
def setUp(self):
|
|
sys.path.insert(0, "./llm")
|
|
|
|
def tearDown(self):
|
|
sys.path.remove("./llm")
|
|
|
|
@property
|
|
def chat_template(self):
|
|
return ChatTemplate.from_file(self.chat_template_config_file)
|
|
|
|
def test_must_have_system(self):
|
|
tokenizer = AutoTokenizer.from_pretrained("qwen/qwen-14b-chat")
|
|
|
|
# get the length of system
|
|
system = tokenizer.chat_template.render_system()
|
|
system_ids = tokenizer.encode(system, add_special_tokens=False)["input_ids"]
|
|
|
|
from utils.data import tokenize_rounds_example
|
|
|
|
fake_data_args = self.DataArg(len(system_ids) + 5, src_length=len(system_ids) + 5)
|
|
|
|
example = {"src": ["你好"], "tgt": ["您好,我是个人人工智能助手"]}
|
|
result, _ = tokenize_rounds_example(tokenizer, example, fake_data_args)
|
|
sentence_result = tokenizer.convert_tokens_to_string(tokenizer.convert_ids_to_tokens(result["input_ids"]))
|
|
expected_sentence = tokenizer.chat_template.system + "\n<|im_start|>user\n你好"
|
|
self.assertEqual(expected_sentence, sentence_result)
|
|
|
|
def test_at_least_one_turn(self):
|
|
query = [["你好", "您好,我是个人人工智能助手"], ["今天吃啥", "你可以选择不同的菜系"]]
|
|
tokenizer = AutoTokenizer.from_pretrained("linly-ai/chinese-llama-2-7b")
|
|
# tokenizer.init_chat_template(self.chat_template_config_file)
|
|
|
|
# get all query sentence
|
|
all_sentence = tokenizer.chat_template.render_system()
|
|
all_sentence += "".join(
|
|
[
|
|
" ".join(tokenizer.chat_template.render_conversation(one_turn, index=index))
|
|
for index, one_turn in enumerate(query)
|
|
]
|
|
)
|
|
all_sentence_ids = tokenizer(all_sentence, add_special_tokens=False)["input_ids"]
|
|
|
|
# get the max_length of conversation
|
|
from utils.data import tokenize_rounds_example
|
|
|
|
fake_data_args = self.DataArg(1024)
|
|
example = {"src": ["你好", "今天吃啥"], "tgt": ["您好,我是个人人工智能助手", "你可以选择不同的菜系"]}
|
|
tokenized_result, _ = tokenize_rounds_example(tokenizer, example, fake_data_args)
|
|
|
|
assert len(all_sentence_ids) == len(tokenized_result["input_ids"])
|
|
|
|
fake_data_args = self.DataArg(len(all_sentence_ids) - 4)
|
|
expected_example = {"src": ["你好"], "tgt": ["您好,我是个人人工智能助手"]}
|
|
expected_tokenized_result, _ = tokenize_rounds_example(tokenizer, expected_example, fake_data_args)
|
|
|
|
sentence_result = tokenizer.convert_tokens_to_string(
|
|
tokenizer.convert_ids_to_tokens(expected_tokenized_result["input_ids"])
|
|
)
|
|
|
|
# https://github.com/PaddlePaddle/PaddleNLP/blob/v2.6.1/paddlenlp/transformers/llama/tokenizer.py#L119
|
|
# should use blank string to join
|
|
expected_sentence = " ".join(tokenizer.chat_template.render_conversation(["你好", "您好,我是个人人工智能助手"]))
|
|
expected_sentence = expected_sentence.replace("<s>", "<s> ")
|
|
self.assertEqual(
|
|
sentence_result,
|
|
expected_sentence,
|
|
)
|
|
|
|
def test_inference_template_with_context_data(self):
|
|
tokenizer = AutoTokenizer.from_pretrained("__internal_testing__/tiny-random-llama")
|
|
chat_template_config_file = "./tests/fixtures/chat_template_with_context.json"
|
|
tokenizer.init_chat_template(chat_template_config_file)
|
|
|
|
query = "你好"
|
|
context_data = {
|
|
"system": "<<SYSTEM-MESSAGE>>",
|
|
"instruction": "<<INSTRUCTION-MESSAGE>>",
|
|
}
|
|
final_query = tokenizer.apply_chat_template(query, context_data=context_data, tokenize=False)
|
|
expected_query = "你是一个人工智能助手<<SYSTEM-MESSAGE>>-<<INSTRUCTION-MESSAGE>>\nHuman: 你好<sep> Bot:"
|
|
self.assertEqual(final_query, expected_query)
|
|
|
|
|
|
class TemplateIntegrationTest(unittest.TestCase):
|
|
class DataArg:
|
|
def __init__(self, max_length, src_length: Optional[int] = None):
|
|
self.max_length: int = max_length
|
|
if src_length is None:
|
|
src_length = self.max_length - 8
|
|
|
|
self.src_length: int = src_length
|
|
|
|
def setUp(self) -> None:
|
|
self.tokenizer = AutoTokenizer.from_pretrained("qwen/qwen-7b-chat")
|
|
qwen_jinja = "{% for message in messages %}{% if loop.first and messages[0]['role'] != 'system' %}{{ '<|im_start|>system\nYou are a helpful assistant<|im_end|>\n' }}{% endif %}{{'<|im_start|>' + message['role'] + '\n' + message['content'] + '<|im_end|>' + '\n'}}{% endfor %}{% if add_generation_prompt %}{{ '<|im_start|>assistant\n' }}{% endif %}"
|
|
self.tokenizer.init_chat_template(qwen_jinja)
|
|
sys.path.insert(0, "./llm")
|
|
return super().setUp()
|
|
|
|
def tearDown(self):
|
|
sys.path.remove("./llm")
|
|
|
|
def test_chat_template(self):
|
|
# test single turn
|
|
query = "你好"
|
|
final_query = self.tokenizer.apply_chat_template(query, tokenize=False)
|
|
expected_query = f"<|im_start|>system\nYou are a helpful assistant<|im_end|>\n<|im_start|>user\n{query}<|im_end|>\n<|im_start|>assistant\n"
|
|
self.assertEqual(final_query, expected_query)
|
|
|
|
# test multi turns conversation
|
|
query = [["你好", "您好,我是个人人工智能助手"], ["今天吃啥"]]
|
|
final_query = self.tokenizer.apply_chat_template(query, tokenize=False)
|
|
expected_query = "<|im_start|>system\nYou are a helpful assistant<|im_end|>\n<|im_start|>user\n你好<|im_end|>\n<|im_start|>assistant\n您好,我是个人人工智能助手<|im_end|>\n<|im_start|>user\n今天吃啥<|im_end|>\n<|im_start|>assistant\n"
|
|
self.assertEqual(final_query, expected_query)
|
|
|
|
def test_system_error(self):
|
|
# test system messaage error
|
|
error_jinja = "{{ bos_token }}{% if messages[0]['role'] == 'system' %}{{ raise_exception('System role not supported') }}{% endif %}"
|
|
self.tokenizer.init_chat_template(error_jinja)
|
|
from jinja2.exceptions import TemplateError
|
|
|
|
with self.assertRaises(TemplateError):
|
|
self.tokenizer.apply_chat_template([{"role": "system", "content": ""}])
|
|
|
|
def test_round_error(self):
|
|
# error round, 1 is not a valid role.
|
|
query = [["你好", "您好,我是个人人工智能助手"], ["今天吃啥"], ["你好", "您好"]]
|
|
with self.assertRaises(ValueError):
|
|
self.tokenizer.apply_chat_template(query, tokenize=False)
|
|
|
|
def test_jinja_syntax_error(self):
|
|
# test system messaage error
|
|
error_jinja = (
|
|
"{ bos_token }{% if messages[0]['role'] == 'system' %}{ raise_exception('System role not supported')}"
|
|
)
|
|
from jinja2.exceptions import TemplateSyntaxError
|
|
|
|
with self.assertRaises(TemplateSyntaxError):
|
|
self.tokenizer.init_chat_template(error_jinja)
|
|
|
|
def test_train_format(self):
|
|
from utils.data import tokenize_rounds_example
|
|
|
|
fake_data_args = self.DataArg(50, src_length=50)
|
|
example = {"src": ["你好"], "tgt": ["您好,我是个人人工智能助手"]}
|
|
result, tgt_id = tokenize_rounds_example(self.tokenizer, example, fake_data_args, add_generation_prompt=True)
|
|
sentence_result = self.tokenizer.decode(result["input_ids"])
|
|
expected_sentence = "<|im_start|>system\nYou are a helpful assistant<|im_end|>\n<|im_start|>user\n你好<|im_end|>\n<|im_start|>assistant\n您好,我是个人人工智能助手<|im_end|>\n"
|
|
self.assertEqual(expected_sentence, sentence_result)
|
|
|
|
tgt_idx = len(
|
|
self.tokenizer.encode(
|
|
"<|im_start|>system\nYou are a helpful assistant<|im_end|>\n<|im_start|>user\n你好<|im_end|>\n<|im_start|>assistant\n"
|
|
)["input_ids"]
|
|
)
|
|
self.assertEqual(tgt_id[tgt_idx - 1], -100)
|
|
self.assertNotEqual(tgt_id[tgt_idx], -100)
|
|
|
|
def test_train_format_multi(self):
|
|
from utils.data import tokenize_rounds_example
|
|
|
|
fake_data_args = self.DataArg(50, src_length=50)
|
|
example = {"src": ["用户Round 1", "用户Round 2"], "tgt": ["回答Round 1", "回答Round 2"]}
|
|
result, tgt_id = tokenize_rounds_example(self.tokenizer, example, fake_data_args, add_generation_prompt=True)
|
|
|
|
tgt_idx_1 = len(
|
|
self.tokenizer.encode(
|
|
"<|im_start|>system\nYou are a helpful assistant<|im_end|>\n<|im_start|>user\n用户Round 1<|im_end|>\n<|im_start|>assistant\n"
|
|
)["input_ids"]
|
|
)
|
|
tgt_idx_2 = len(
|
|
self.tokenizer.encode(
|
|
"<|im_start|>system\nYou are a helpful assistant<|im_end|>\n<|im_start|>user\n用户Round 1<|im_end|>\n<|im_start|>assistant\n"
|
|
"回答Round 1<|im_end|>\n<|im_start|>user\n用户Round 2<|im_end|>\n<|im_start|>assistant\n"
|
|
)["input_ids"]
|
|
)
|
|
|
|
self.assertEqual(tgt_id[tgt_idx_1 - 1], -100)
|
|
self.assertNotEqual(tgt_id[tgt_idx_1], -100)
|
|
self.assertEqual(tgt_id[tgt_idx_2 - 1], -100)
|
|
self.assertNotEqual(tgt_id[tgt_idx_2], -100)
|
|
|
|
def test_split_answer(self):
|
|
original_msg = [
|
|
{"role": "user", "content": "用户Round 1"},
|
|
{"role": "assistant", "content": "|回答Round 1|"},
|
|
{"role": "user", "content": "用户Round 2"},
|
|
{"role": "assistant", "content": "_回答Round 2?"},
|
|
]
|
|
answer = ["|回答Round 1|<|im_end|>\n", "_回答Round 2?<|im_end|>\n"]
|
|
split_part = self.tokenizer._extract_non_learnable_parts(original_msg, answer)
|
|
self.assertEqual(len(split_part), 2)
|
|
self.assertEqual(
|
|
split_part[0],
|
|
"<|im_start|>system\nYou are a helpful assistant<|im_end|>\n<|im_start|>user\n用户Round 1<|im_end|>\n<|im_start|>assistant\n",
|
|
)
|