之前写过p tuning,用的是官方文档中的模型和任务。现在用qwen中文模型和中文文本分类任务。代码已上传。

数据集介绍

魔搭社区汇聚各领域最先进的机器学习模型,提供模型探索体验、推理、训练、部署和应用的一站式服务。icon-default.png?t=O83Ahttps://www.modelscope.cn/datasets/wowhaha/moral-foundation-news_2000我用的数据是魔塔上的“新闻文本+道德判断”数据集,包含基于道德基础分类的新闻文章,适用于中文的多标签文本分类任务。每篇新闻文章根据其道德倾向进行标注,分为实用(Pragmatism)/ 理想(Idealism)”、“责任(Responsibility)/ 利益(Profit)”、“创新(Innovation)/ 守旧(Conservatism)三个标签。

这是数据形式。system是系统给的instruction(所有数据的instruction都是一样的),query是要分类的文本,response是分类标签。

{'query': '部署集中式可持续智能平台,以促进和简化PETRONAS供应链脱碳的努力。',
 'response': '责任/利益',
 'system': '你是一位道德判断模型。根据给出的有关环境报道的新闻文本,请判断其倾向于以下哪一个维度:责任/利益、实用/理想、创新/守旧、非道德。你的回答必须只包含其中的一个维度。'}

模型是qwen2.5 1.5b版本。

魔搭社区汇聚各领域最先进的机器学习模型,提供模型探索体验、推理、训练、部署和应用的一站式服务。icon-default.png?t=O83Ahttps://www.modelscope.cn/models/Qwen/Qwen2.5-1.5B-Instruct

预准备

import os
import torch
from pprint import pprint
from datasets import load_dataset
import transformers
from transformers import AutoModelForCausalLM, AutoTokenizer
from transformers import Trainer, TrainingArguments, DataCollatorWithPadding, DataCollatorForLanguageModeling, DataCollatorForSeq2Seq, default_data_collator
from peft import AutoPeftModelForCausalLM, get_peft_model, PromptTuningInit, PromptEncoderConfig, PromptTuningConfig, TaskType


os.environ['CUDA_VISIBLE_DEVICES'] = '7'
model_name_or_path = "../../DataCollection/officials/Qwen2.5-1.5b-Instruct"
tokenizer_name_or_path = model_name_or_path

tokenizer = AutoTokenizer.from_pretrained(model_name_or_path)
if tokenizer.pad_token_id is None:
    tokenizer.pad_token_id = tokenizer.eos_token_id
    
base_model = AutoModelForCausalLM.from_pretrained(model_name_or_path)

def inference_single(model, tokenizer, text, device=None):
    if not device:
        device = 'cuda' if torch.cuda.is_available() else 'cpu'
    model.to(device)
    inputs = tokenizer(text, return_tensors='pt')
    outputs = model.generate(
        input_ids=inputs["input_ids"].to(device),
        attention_mask=inputs["attention_mask"].to(device),
        max_new_tokens=20,
        eos_token_id=tokenizer.pad_token_id
    )
    decoded_output = tokenizer.decode(outputs.cpu().numpy()[0])
    return decoded_output

inference_single(base_model, tokenizer, "1+1=?")

数据处理

训练数据不应该很多,我只挑了100条用于训练+验证。

from modelscope.msdatasets import MsDataset
dataset = MsDataset.load('wowhaha/moral-foundation-news_2000')['train']
pprint(dataset)
print('='*30)
pprint(dataset[0])
dataset = dataset.train_test_split(train_size=100, shuffle=False)
pprint(dataset)

输入格式

text_format = '文本:"{text}"\n维度:{label}'

tweet_example = dataset['train'][0]['query']
label_example = dataset['train'][0]['response']

whole_text = text_format.format(text=tweet_example, label=label_example)
whole_text_input_ids = tokenizer(whole_text)['input_ids']
print(whole_text_input_ids)

input_text = text_format.format(text=tweet_example, label='')
input_text_input_ids = tokenizer(input_text)['input_ids']
print(input_text_input_ids)

label_text = label_example
label_text_input_ids = tokenizer(label_text)['input_ids']
print(label_text_input_ids)

assert whole_text_input_ids == input_text_input_ids + label_text_input_ids

'''
[108704, 40727, 102121, 101096, 28330, 104911, 100168, 100133, 3837, 23031, 101902, 33108, 110487, 79404, 41714, 1911, 104861, 99694, 100912, 107122, 1773, 698, 106643, 5122, 99531, 14, 101996]
[108704, 40727, 102121, 101096, 28330, 104911, 100168, 100133, 3837, 23031, 101902, 33108, 110487, 79404, 41714, 1911, 104861, 99694, 100912, 107122, 1773, 698, 106643, 5122]
[99531, 14, 101996]
'''

如上例,模型需要预测的是[99531, 14, 101996]部分,损失也只需要计算这些位置的loss。

tokenize处理

import copy

max_len = 200
def tokenize_function(data):
    texts = data['query']
    outputs = data['response']

    inputs = [text_format.format(text=a, label=b) for a, b in zip(texts, outputs)]

    tokenized_dict = tokenizer(inputs, padding='max_length', max_length=max_len, truncation=True)
    
    labels = []
    for i in range(len(tokenized_dict)):
        inputs_input_ids = tokenizer(inputs[i], padding=False, truncation=False)['input_ids']
        labels_input_ids = tokenizer(outputs[i], padding=False, truncation=False)['input_ids']
        pre_len = len(inputs_input_ids)  - len(labels_input_ids)
        tmp = copy.deepcopy(tokenized_dict['input_ids'][i])
        if pre_len <= max_len:
            tmp[:pre_len] = [-100] * pre_len
        full_len = len(inputs_input_ids)
        for j in range(full_len + 1, max_len):
            tmp[j] = -100
        labels.append(tmp)

    tokenized_dict['labels'] = labels
    
    return tokenized_dict

# tokenize_function( dataset['train'][:5])

dataset_preprocessed = dataset['train'].map(tokenize_function, batched=True, batch_size=2, desc='tokenizing dataset....', remove_columns=dataset['train'].column_names)

这里的labels需要手动处理下,将非label部分的数值都变成-100。注意,把后面padding部分变成-100也是必要,不然训练会更偏向于将所有输出内容变成padding token而不是正常内容,因为统一padding时,padding token相比正常内容更多,对loss的影响更大。我在处理labels时,只保留了一个padding token以方便预测提前结束。

注意这里的最大长度需要根据任务调整下,如果值太小,预期输出那里会被截断,训练loss会非常平稳,因为你根本没真正计算loss。如果值太大,数据中的padding内容太多,会增大训练时间。

还有有一点,padding id需要根据模型调整下,例如qwen更会倾向在结束时输出padding token而不是eos token(测试得出),所以我在之前的p tuning中用eos,而在这里用padding token。

tokenizer.decode(dataset_preprocessed[0]['input_ids'])

'''
'文本:"部署集中式可持续智能平台,以促进和简化PETRONAS供应链脱碳的努力。"\n维度:责任/利益<|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|><|endoftext|>'
'''

tmp = dataset_preprocessed[0]['labels']
for j in range(len(tmp)):
    if tmp[j]!=-100:break
for k in range(len(tmp)-1, 0, -1):
    if tmp[k]!=-100:break
tokenizer.decode(tmp[j:k+1])

'''
'责任/利益<|endoftext|>'
'''

这是一份示例,第一个输出是完整文本的decode结果,后面重复的是tokenizer的pad token。第二个输出是labels中仅预期输出部分的decode结果,其他的都是-100。

训练

模型定义

# 3. model
peft_config = PromptTuningConfig(
    task_type=TaskType.CAUSAL_LM,
    prompt_tuning_init=PromptTuningInit.RANDOM,
    num_virtual_tokens=50,
    tokenizer_name_or_path=tokenizer_name_or_path,
)
# peft_config = PromptEncoderConfig(task_type="CAUSAL_LM", num_virtual_tokens=20, encoder_hidden_size=128)
peft_model = get_peft_model(base_model, peft_config)
print(peft_model.print_trainable_parameters())

数据切分

test_size = round(len(dataset_preprocessed) * 0.4)
train_val = dataset_preprocessed.train_test_split(
    test_size=test_size, shuffle=True, seed=42)
train_data = train_val["train"]
val_data = train_val["test"]
print(len(train_data))
print(len(val_data))

这里切分成60:40的数据。

训练

lr = 5e-3
n_epoch = 20
batch_size = 20
output_dir = './output'

training_arguments = TrainingArguments(
    output_dir=output_dir,
    per_device_train_batch_size=batch_size,
    per_device_eval_batch_size=batch_size,
    num_train_epochs=n_epoch,
    learning_rate=lr,
    logging_strategy='steps',
    logging_steps=1,
    evaluation_strategy='steps',
    eval_steps=5,
    report_to=[],
    load_best_model_at_end=True
)

# data_collator = DataCollatorForLanguageModeling(tokenizer=tokenizer,mlm=False)
data_collator = DataCollatorForSeq2Seq(tokenizer=tokenizer, padding=True)

trainer = Trainer(
    model=peft_model,
    train_dataset=train_data,
    eval_dataset=val_data,
    args=training_arguments,
    data_collator=default_data_collator
)

trainer.evaluate()

trainer.train()

trainer.save_state()
trainer.save_model()

测试结果

soft prompt结果

import re 

for item in dataset['test'].select(range(5)):

    input_text = text_format.format(text=item['query'], label='')

    result = inference_single(peft_model, tokenizer, input_text)
    print(result)
    match = re.search(r"维度:(.*?)<|im_end|>", result)
    if match:
        result = match.group(1)
        print("匹配的内容:", result)

    print(item['response'])
    print(20*'=')
     
文本:"中国工程院院士、浙江大学能源工程学院高翔教授,与煤打了半辈子交道。他说:“要实现碳达峰碳中和的目标,加强煤炭清洁高效利用、建设新型能源体系尤为重要。我和团队将继续以这一战略需求为导向,瞄准‘卡脖子’技术领域,持续推进绿色低碳科技创新,推动低碳产业迭代升级,为祖国天更蓝、山更绿、水更清贡献自己的力量。”"
维度:责任<|endoftext|>
匹配的内容: 责任
实用/理想
====================
文本:"Avangrid 及其母公司 Iberdrola 试图收购 PNM Resources 及其在德克萨斯州和新墨西哥州的电力子公司"
维度:实用/理想<|endoftext|>
匹配的内容: 实用/理想
非道德
====================
文本:"我可以在家里一样高效地工作,而且我在家里可以更好地完成工作,把碳排放在环境里只是为了坐在这里似乎是不对的。"
维度:实用/理想<|endoftext|>
匹配的内容: 实用/理想
责任/利益
====================
文本:"欧洲能源交易所正在推动绿色氢气项目的发展,并计划成为该市场分析师预计到 2050 年将达到 1.2 万亿美元的市场领导者。"
维度:责任/利益<|endoftext|>
匹配的内容: 责任/利益
实用/理想
====================
文本:"巴西被指责在马德里阻碍应对气候危机的进展。"
维度:实用/理想<|endoftext|>
匹配的内容: 实用/理想
责任/利益
====================

虽然不是很准确,但都能按照格式输出。

hard prompt结果

import re 

for item in dataset['test'].select(range(5)):


    input_text = f'指令:{item['system']}\n文本:"{item['query']}"\n维度:'

    result = inference_single(peft_model, tokenizer, input_text)
    print(result)
    # match = re.search(r"维度:(.*?)<|im_end|>", result)
    # if match:
    #     result = match.group(1)
    #     print("匹配的内容:", result)

    print(item['response'])
     
指令:你是一位道德判断模型。根据给出的有关环境报道的新闻文本,请判断其倾向于以下哪一个维度:责任/利益、实用/理想、创新/守旧、非道德。你的回答必须只包含其中的一个维度。
文本:"中国工程院院士、浙江大学能源工程学院高翔教授,与煤打了半辈子交道。他说:“要实现碳达峰碳中和的目标,加强煤炭清洁高效利用、建设新型能源体系尤为重要。我和团队将继续以这一战略需求为导向,瞄准‘卡脖子’技术领域,持续推进绿色低碳科技创新,推动低碳产业迭代升级,为祖国天更蓝、山更绿、水更清贡献自己的力量。”"
维度:责任/利益、实用/理想、创新/守旧、非道德.<|endoftext|>
实用/理想
指令:你是一位道德判断模型。根据给出的有关环境报道的新闻文本,请判断其倾向于以下哪一个维度:责任/利益、实用/理想、创新/守旧、非道德。你的回答必须只包含其中的一个维度。
文本:"Avangrid 及其母公司 Iberdrola 试图收购 PNM Resources 及其在德克萨斯州和新墨西哥州的电力子公司"
维度:责任/利益、实用/理想、创新/守旧、非道德.<|endoftext|>
非道德
指令:你是一位道德判断模型。根据给出的有关环境报道的新闻文本,请判断其倾向于以下哪一个维度:责任/利益、实用/理想、创新/守旧、非道德。你的回答必须只包含其中的一个维度。
文本:"我可以在家里一样高效地工作,而且我在家里可以更好地完成工作,把碳排放在环境里只是为了坐在这里似乎是不对的。"
维度:责任/利益<|endoftext|>
责任/利益
指令:你是一位道德判断模型。根据给出的有关环境报道的新闻文本,请判断其倾向于以下哪一个维度:责任/利益、实用/理想、创新/守旧、非道德。你的回答必须只包含其中的一个维度。
文本:"欧洲能源交易所正在推动绿色氢气项目的发展,并计划成为该市场分析师预计到 2050 年将达到 1.2 万亿美元的市场领导者。"
维度:责任<|endoftext|>
实用/理想
指令:你是一位道德判断模型。根据给出的有关环境报道的新闻文本,请判断其倾向于以下哪一个维度:责任/利益、实用/理想、创新/守旧、非道德。你的回答必须只包含其中的一个维度。
文本:"巴西被指责在马德里阻碍应对气候危机的进展。"
维度:责任/利益<|endoftext|>
责任/利益

这是数据集自带的instruction代入prompt的结果,部分结果是按正常格式输出,大部分都没能以正常的结果输出。

Logo

ModelScope旨在打造下一代开源的模型即服务共享平台,为泛AI开发者提供灵活、易用、低成本的一站式模型服务产品,让模型应用更简单!

更多推荐