「酷文」小说创作大模型挑战赛baseline精读

sentences = []

for sentence in data.split('。'):  # 使用句号作为切分符
    sentences.append(sentence)

# 将句子合并成800字一段的段落
paragraphs = []
current_paragraph = ''
for sentence in sentences:
    if len(current_paragraph) + len(sentence) <= 800:
        current_paragraph += sentence+'。'
    else:
        paragraphs.append(current_paragraph.strip())
        current_paragraph = sentence

# 将最后一段加入到段落列表中
if current_paragraph:
    paragraphs.append(current_paragraph.strip())

赛题要求生成800字左右的文本,所以训练集也切一下

messages=[
            {
                'role': 'system', 
                'content': '你是一个熟读各类小说的专家,请你用一句话总结这段小说的情节。仅回答总结,不需要添加其他内容'
            },
            {
                'role': 'user', 
                'content': "他承认班纳特小姐是漂亮的,可惜她笑得太多。赫斯脱太太姐妹同意他这种看法……可是她们仍然羡慕她,喜欢她,说她是个甜姐儿,她们并不反对跟她这样的一位小姐做个深交。班纳特小姐就这样成为一个甜姐儿了,她们的兄弟听到了这番赞美,便觉得今后可以爱怎么样想她就怎么样想她了。\n距离浪博恩不远的地方,住着一家人家,这就是威廉·卢卡斯爵士府上。班纳特府上跟他们特别知已。爵士从前是在麦里屯做生意起家发迹的,曾在当市长的任内上书皇上,获得了一个爵士头衔;这个显要的身份使他觉得太荣幸,从此他就讨厌做生意,讨厌住在一个小镇上,于是歇了生意,告别小镇,带着家属迁到那离开麦里屯大约一英里路的一幢房子里去住,从那时候起就把那地方叫做卢家庄。他可以在这儿自得其乐,以显要自居,而且,既然摆脱了生意的纠缠,他大可以一心一意地从事社交活动。他尽管以自己的地位欣然自得,却并不因此而目空一切,反而对什么人都应酬得非常周到。他生来不肯得罪人,待人接物总是和蔼可亲,殷勤体贴,而且自从皇上觐见以来,更加彬彬有礼。卢卡斯太太是个很善良的女人,真是班纳特太太一位宝贵的邻居。卢府上有好几个孩子。大女儿是个明理懂事的年轻小姐,年纪大约二十六七岁,她是伊丽莎白的要好朋友。且说卢府上几位小姐跟班府上几位小姐这回非要见见面,谈谈这次跳舞会上的事业不可。于是在开完了跳舞会的第二天上午,卢府上的小姐们到浪博恩来跟班府上的小姐交换意见。\n班纳特太太一看见卢卡斯小姐,便客客气气,从容不迫地说:“那天晚上全靠你开场开得好,你做了彬格莱先生的第一个意中人。”“是呀;可是他喜欢的倒是第二个意中人。”“哦,我想你是说吉英吧,因为他跟她跳了两次。看起来,他是真的爱上她呢……我的确相信他是真的……我听到了一些话……可是我弄不清究竟……我听到了一些有关鲁宾逊先生的话。”“说不定你指的是我喻听到他和鲁宾逊先生的谈话吧;我不是跟你说过了吗?鲁宾逊先生问他喜欢不喜欢我们麦里屯的跳舞会,问他是否觉得到场的女宾们中间有许多人很美,问他认为哪一个最美?"
            },
            {
                'role': 'assistant', 
                'content': "浪博恩的班纳特家与卢卡斯家交好,班纳特家的二小姐伊丽莎白在舞会上因笑容过多而未得到达西的好感,但得到了彬格莱的青睐,卢卡斯家的大女儿夏洛特则是伊丽莎白的好友,两家人在舞会后讨论着舞会上的趣事和可能的姻缘。"
            },
            {
                'role': 'user', 
                'content': text
            }
        ])

给个示例one-shot一下

# 批量给指定的小说打标签的接口函数
def build_dataset(novel,texts):
    instruction_prompt = "你是一个熟读各类小说的专家,请你根据要求写一段800字左右的小说。"
    dataset = []
    dataset_error = []
    for text in tqdm(texts, desc=f"Processing {novel}", total=len(texts)):
        try:
            summary = get_summary_with_retry(text)
            print(summary)
            dataset.append({
                "instruction": instruction_prompt,
                "input": summary,
                "output": text
            })
        except Exception as e:
            dataset_error.append(text)
            logger.error(f"Failed to process text: {text}. Error: {e}")
    
    with open(f"./data/{novel}.json", "w") as f:
        f.write(json.dumps(dataset, ensure_ascii=False, indent=4))

    with open(f"./data/{novel}_error.txt", "w") as f:
        f.write(json.dumps(dataset_error, ensure_ascii=False, indent=4))
    return dataset

在调用get_summary_with_retry中get_response得到总结后的情节,把总结前的原文当成正确输出样例

dataset作为微调指令数据

def process_func(example):
    # Llama分词器会将一个中文字切分为多个token,
    # 因此需要放开一些最大长度,保证数据的完整性,生成小说长度为800,因此我们采用了2048的最大长度
    MAX_LENGTH = 2048   
    input_ids, attention_mask, labels = [], [], []
    instruction = tokenizer(f"<|im_start|>system\n你是一个熟读各类小说的专家,请你根据要求写一段800字左右的小说。<|im_end|>\n<|im_start|>user\n{example['instruction'] + example['input']}<|im_end|>\n<|im_start|>assistant\n", add_special_tokens=False)  # add_special_tokens 不在开头加 special_tokens
    response = tokenizer(f"{example['output']}", add_special_tokens=False)
    input_ids = instruction["input_ids"] + response["input_ids"] + [tokenizer.pad_token_id]
    attention_mask = instruction["attention_mask"] + response["attention_mask"] + [1]  # 因为eos token咱们也是要关注的所以 补充为1
    labels = [-100] * len(instruction["input_ids"]) + response["input_ids"] + [tokenizer.pad_token_id]  
    if len(input_ids) > MAX_LENGTH:  # 做一个截断
        input_ids = input_ids[:MAX_LENGTH]
        attention_mask = attention_mask[:MAX_LENGTH]
        labels = labels[:MAX_LENGTH]
    return {
        "input_ids": input_ids,
        "attention_mask": attention_mask,
        "labels": labels
    }

在 PyTorch 的 CrossEntropyLoss 中,-100 被用作忽略索引(ignore index),表示这些位置的损失不会被计算。这是因为在训练语言模型时,我们只关心模型生成的部分(即 response 部分)的损失,而不关心输入部分的损失。

Qwen1.5Qwen2 采用的Prompt Template格式如下:

<|im_start|>system You are a helpful assistant.<|im_end|> <|im_start|>user 你是谁?<|im_end|> <|im_start|>assistant 我是一个有用的助手。<|im_end|>

print(tokenizer.decode(tokenized_id[0]['input_ids']))

  • tokenizer.decode 方法将 token IDs 转换回可读的文本。
  • 这行代码用于打印第一个样本的 input_ids 对应的文本,验证分词和拼接是否正确。
print(tokenizer.decode(list(filter(lambda x: x != -100, tokenized_id[1]["labels"]))))
  • 这行代码用于打印第二个样本的 labels 对应的文本,验证标签是否正确。
from peft import LoraConfig, TaskType, get_peft_model

config = LoraConfig(
    task_type=TaskType.CAUSAL_LM, 
    target_modules=["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"],
    inference_mode=False, # 训练模式
    r=8, # Lora 秩
    lora_alpha=32, # Lora alaph,具体作用参见 Lora 原理
    lora_dropout=0.1# Dropout 比例
)
config
  • task_type:模型类型

  • target_modules:需要训练的模型层的名字,主要就是attention部分的层,不同的模型对应的层的名字不同,可以传入数组,也可以字符串,也可以正则表达式。

  • rlora的秩,具体可以看Lora原理

  • lora_alphaLora alaph,具体作用参见 Lora 原理

lora_path = "./output/Qwen2_1.5b_chat_novel_all"

args = TrainingArguments(
    output_dir=lora_path,
    per_device_train_batch_size=4,
    gradient_accumulation_steps=4,
    logging_steps=10,
    num_train_epochs=3,
    save_steps=100,
    learning_rate=1e-4,
    save_on_each_node=True,
    gradient_checkpointing=True
)
  • output_dir

    • 指定保存训练输出的目录,包括检查点、日志等。。
  • per_device_train_batch_size

    • 设置每个设备(如 GPU)上的训练批量大小为 4。
    • 如果使用多个 GPU,这个值会乘以 GPU 的数量,得到总的批量大小。
  • gradient_accumulation_steps

    • 设置梯度累积步数为 4。
    • 这意味着模型每进行 4 个小批量的前向和后向传播后,才会进行一次梯度更新。
    • 这种技术用于在显存有限的情况下,模拟更大的批量训练。
  • logging_steps:每经过 10 步训练,记录一次日志信息。

  • num_train_epochs

    • 设置训练的总轮数为 3。
  • save_steps:每经过 100 步训练,保存一次模型检查点。

  • learning_rate

    • 设置学习率为 1e-4。
  • save_on_each_node

    • 设置为 True,表示在每个节点上保存模型。
    • 适用于分布式训练环境。
  • gradient_checkpointing

    • 设置为 True,表示启用梯度检查点。
    • 这是一种节省内存的技术,通过在向前传播过程中保存中间激活值,在需要时重新计算这些值,减少内存消耗。
trainer = Trainer(
    model=model,
    args=args,
    train_dataset=tokenized_id,
    data_collator=DataCollatorForSeq2Seq(tokenizer=tokenizer, padding=True),
)

数据收集器的作用

  1. 批量数据处理:

    • 将多个样本的数据整合成一个批次。
    • 包括将输入、输出序列转换为模型输入格式(token IDs)并对齐长度。
  2. 填充(Padding):

    • 对齐序列长度,确保同一个批次中的所有序列具有相同的长度。
    • 对较短的序列添加填充 token,使其与最长序列对齐。
  3. 处理注意力掩码(Attention Mask):

    • 生成对应的注意力掩码,用于指示模型在计算注意力时应关注哪些位置。
    • 填充值通常对应的掩码值为 0,而有效 token 对应的掩码值为 1。
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
from peft import PeftModel, LoraConfig, TaskType, get_peft_model

model_path = './Qwen/Qwen2-1___5B-Instruct'
lora_path = "./output/Qwen2_1.5b_chat_novel_all/final"

max_new_tokens = 2048

config = LoraConfig(
    task_type=TaskType.CAUSAL_LM, 
    target_modules=["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"],
    inference_mode=False, # 训练模式
    r=8, # Lora 秩
    lora_alpha=32, # Lora alaph,具体作用参见 Lora 原理
    lora_dropout=0.1# Dropout 比例
)

# 加载tokenizer
tokenizer = AutoTokenizer.from_pretrained(model_path)

# 加载模型
model = AutoModelForCausalLM.from_pretrained(model_path, device_map="auto",torch_dtype=torch.bfloat16)

# 加载lora权重
model = PeftModel.from_pretrained(model, model_id=lora_path, config=config)

# 原始数据
## 傲慢与偏见 data[0]
    # {
    #     "instruction": "你是一个熟读各类小说的专家,请你根据要求写一段800字左右的小说。",
    #     "input": "一个有钱的单身汉必定想要娶妻,这是社会普遍认同的事实。班纳特太太兴奋地告诉丈夫,尼日斐花园被一位名叫彬格莱的富绅租下,她希望他能成为女儿们的潜在配偶,而班纳特先生则以幽默的方式回应她的期望。",
    #     "output": "凡是有钱的单身汉,总想娶位太太,这已经成了一条举世公认的真理。这样的单身汉,每逢新搬到一个地方,四邻八舍虽然完全不了解他的性情如何,见解如何,可是,既然这样的一条真理早已在人们心目中根深蒂固,因此人们总是把他看作自己某一个女儿理所应得的一笔财产。\n有一天班纳特太太对她的丈夫说:“我的好老爷,尼日斐花园终于租出去了,你听说过没有?”班纳特先生回答道,他没有听说过。\n“的确租出去了,”她说,“朗格太太刚刚上这儿来过,她把这件事的底细,一五一十地告诉了我。”班纳特先生没有理睬她。\n“你难道不想知道是谁租去的吗?”太太不耐烦地嚷起来了。\n“既是你要说给我听,我听听也无妨。”这句话足够鼓励她讲下去了。\n“哦!亲爱的,你得知道,郎格太太说,租尼日斐花园的是个阔少爷,他是英格兰北部的人;听说他星期一那天,乘着一辆驷马大轿车来看房子,看得非常中意,当场就和莫理斯先生谈妥了;他要在‘米迦勒节’以前搬进来,打算下个周未先叫几个佣人来住。”“这个人叫什么名字?”“彬格莱。”“有太太的呢,还是单身汉?”“噢!是个单身汉,亲爱的,确确实实是个单身汉!一个有钱的单身汉;每年有四五千磅的收入。真是女儿们的福气!”“这怎么说?关女儿女儿们什么事?”“我的好老爷,”太太回答道,“你怎么这样叫人讨厌!告诉你吧,我正在盘算,他要是挑中我们一个女儿做老婆,可多好!”“他住到这儿来,就是为了这个打算吗?”“打算!胡扯,这是哪儿的话!不过,他倒作兴看中我们的某一个女儿呢。他一搬来,你就得去拜访拜访他。”“我不用去。你带着女儿们去就得啦,要不你干脆打发她们自己去,那或许倒更好些,因为你跟女儿们比起来,她们哪一个都不能胜过你的美貌,你去了,彬格莱先生倒可能挑中你呢?”“我的好老爷,你太捧我啦。从前也的确有人赞赏过我的美貌,现在我可有敢说有什么出众的地方了。一个女人家有了五个成年的女儿,就不该对自己的美貌再转什么念头。”“这样看来,一个女人家对自己的美貌也转不了多少念头喽。"
    # },

prompt = "一个有钱的单身汉必定想要娶妻,这是社会普遍认同的事实。班纳特太太兴奋地告诉丈夫,尼日斐花园被一位名叫彬格莱的富绅租下,她希望他能成为女儿们的潜在配偶,而班纳特先生则以幽默的方式回应她的期望。"
messages = [
    {"role": "system", "content": "你是一个熟读各类小说的专家,请你根据要求写一段800字左右的小说。"},
    {"role": "user", "content": prompt}
]

text = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)

model_inputs = tokenizer([text], return_tensors="pt").to('cuda')

generated_ids = model.generate(
    model_inputs.input_ids,
    max_new_tokens=max_new_tokens
)
generated_ids = [
    output_ids[len(input_ids):] for input_ids, output_ids in zip(model_inputs.input_ids, generated_ids)
]

response = tokenizer.batch_decode(generated_ids, skip_special_tokens=True)[0]

print(response)

tokenizer.apply_chat_template 是一个用于将消息转换为模型可以理解的输入格式的方法。它使用特定的模板来组织输入,使其适合对话或生成任务

使用模型生成新的token,生成长度最多为max_new_tokens,去除输入部分,只保留生成的输出

解码生成的文本,将生成的token转换回人类可读的文本,跳过特殊token

merged_model = model.merge_and_unload()
  • 合并权重(Merge Weights):将 LoRA 权重与基础模型的权重进行合并。这一步骤将 LoRA 的适配效果直接应用到基础模型的权重上,使得基础模型本身已经包含了通过 LoRA 微调得到的所有信息

  • 卸载配置(Unload Configuration):在合并权重之后,LoRA 的配置和相关的低秩矩阵不再需要。因此,卸载这些配置可以减少模型的复杂性和内存占用

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值