Published on

使用imdb数据集对distilbert-base-uncased进行全量微调的情感分类模型

Authors

了解了dataset库的基本使用

获取数据集的基本信息

get_dataset_split_names("ShengbinYue/DISC-Law-SFT"), \
get_dataset_split_names("cornell-movie-review-data/rotten_tomatoes")
# 根据split_names返回的列表信息,可以在load_dataset指定split参数来加载特定数据集

(['train'], ['train', 'validation', 'test'])
configs1 = get_dataset_config_names("ShengbinYue/DISC-Law-SFT")
configs2 = get_dataset_config_names("PolyAI/minds14")
# 可以在load_data中指定name参数来加载特定的子数据集

(['all', 'cs-CZ', 'de-DE', 'en-AU', 'en-GB', 'en-US', 'es-ES', 'fr-FR', 'it-IT', 'ko-KR', 'nl-NL', 'pl-PL', 'pt-PT', 'ru-RU', 'zh-CN'], ['default'])

加载数据集

dataset1 = load_dataset("json", data_files="DISC-Law-SFT/DISC-Law-SFT-Pair-QA-released.jsonl", split="train")

对数据集进行操作

类似于对列表的操作,比如切片,索引之类的

dataset[0]
dataset[-1]
dataset[3:6]
dataset['input'][1]

创建迭代数据集

iterable_dataset = load_dataset("json", data_files="DISC-Law-SFT/DISC-Law-SFT-Pair-QA-released.jsonl", split="train", streaming=True)

#iterable_dataset = dataset1.to_iterable_dataset():这样创建也行

for example in iterable_dataset:
    print(example)
    break
    
{'id': 'legal_question_answering_0', 'input': '违章停车与违法停车是否有区别?', 'output': '对违反道路交通安全法律、法规关于机动车停放、临时停车规定的,可以指出违法行为,并予以口头警告,令其立即驶离。机动车驾驶人不在现场或者虽在现场但拒绝立即驶离,妨碍其他车辆、行人通行的处二十元以上二百元以下罚款。现在人们大多是称作违法停车,因此在法律责任上也会更多一些,不要以为违反交通规章制度问题不大,不要认为违法停车是罚款而已。'}

可迭代数据集它不像 Dataset 那样预先处理和存储所有数据。相反,它基于一个 Python 的迭代器 (iterator) 或生成器 (generator)。当你开始遍历 IterableDataset 时(例如在一个 for 循环中),它才会调用底层的生成器函数,逐一地生成或拉取数据。适合在超大数据集和流式数据(比如不断增长的服务器日志)以及复杂的动态数据采样策略中使用。

对迭代数据集中的数据进行操作

# “获取 iterable_dataset 这个可迭代对象的第一个元素。”
# iterable_dataset:可迭代对象 (Iterable)
# iter(...):获取迭代器 (Iterator)
# next(...):从迭代器中取下一个元素

next(iter(iterable_dataset)), \
next(iter(iterable_dataset['id']))

({'id': 'legal_question_answering_0', 'input': '违章停车与违法停车是否有区别?', 'output': '对违反道路交通安全法律、法规关于机动车停放、临时停车规定的,可以指出违法行为,并予以口头警告,令其立即驶离。机动车驾驶人不在现场或者虽在现场但拒绝立即驶离,妨碍其他车辆、行人通行的处二十元以上二百元以下罚款。现在人们大多是称作违法停车,因此在法律责任上也会更多一些,不要以为违反交通规章制度问题不大,不要认为违法停车是罚款而已。'}, 'legal_question_answering_0')
# 获取子集
list(iterable_dataset.take(3))
# list(iterable_dataset.take(3:6))迭代数据集不能这样进行这样切片

打印出来的内容是数据集的前三项

利用transformers库进行模型读取和分词

from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig

model_id = "./Qwen3-4B-Instruct-2507"

# 加载模型

# 特定的模型有特定的加载器,比如Qwen3是文笔生成模型,就用AutoModelForCausalLM,别的类型的模型像是分类之类的就用别的加载器
model = AutoModelForCausalLM.from_pretrained(
    model_id,
    device_map="auto",
    quantization_config=BitsAndBytesConfig(load_in_8bit=True),
    # 原模型的参数是16位的版本,这里为了减小硬件的压力使用BitsAndBytesConfig把16位精度的参数转化为8位
)

# 加载模型对应的分词器
tokenizer = AutoTokenizer.from_pretrained(model_id)
# 调用分词器进行分词
tokenizer(dataset1[0]['input'])

{'input_ids': [114952, 101531, 57218, 100872, 101531, 107189, 102665, 11319], 'attention_mask': [1, 1, 1, 1, 1, 1, 1, 1]}
# 对整个数据集进行分词

def encode(examples):
    return tokenizer(examples["input"], examples["output"], truncation=True, padding="max_length")

dataset1 = dataset1.map(encode, batched=True,  batch_size=16) # 设置较小的batch_size,以减少并发处理的数据量,进而防止爆显存

利用map函数可以批量对数据集进行分词,但这个操作很吃显存......今天没跑成

了解了fastapi如何设置基本请求

class Item(BaseModel):
    name: str
    price: float
    is_offer: Union[bool, None] = None

@app.put("/items/{item_id}") # 使用注解器声明请求类型
async def update_item(item_id: int, item: Item):
    return {"item_price": item.price, "item_id": item_id}
    

代码中的Item继承了Pydantic中的BaseModel,这样做可以让FastAPI 利用类型提示 (item_id: int) 自动进行数据转换和验证,比如我的URL是/items/55,其中55是字符型变量,pydantic可以自动把这个字符变量转化为整型

fastapi拥有/docs和/redoc,/docs是一个很强大的工具,有点类似postman,不过是fastapi内置的,可以通过docs发送请求,直接在网页端完成测试。 redoc更像是一个简化版的docs,它像是一个api手册,不包含发送请求的功能

下面是微调过程中发现的一些问题: 使用map函数对数据集进行批量分词时要注意分词的对象必须是文本,如果是标签或者其他类型的数据的话就会报错

def tokenize_dataset(dataset):
    return tokenizer(dataset["text"], padding="max_length", truncation=True) # 只有文本需要分词,标签是数字所以不能进行分词

dataset = dataset.map(tokenize_dataset)

相比于[[2025-10-2]],今天新增了训练的过程

data_collator = DataCollatorWithPadding(tokenizer=tokenizer)

training_args = TrainingArguments(
    output_dir="./distilbert-imdb-classify",
    learning_rate=2e-5,
    per_device_train_batch_size=8,
    per_device_eval_batch_size=8,
    num_train_epochs=2,

)

trainer = Trainer(
    model=model,
    args=training_args,
    train_dataset=dataset["train"],
    eval_dataset=dataset["test"],
    processing_class=tokenizer,
    data_collator=data_collator,
)

TrainingArguments用于设置模型的超参数,对于微调任务来说epochs为2足矣,DataCollatorWithPadding主要用于填充,可以传入相对于的分词器来确定填充规则 trainer.train()用于开始训练模型 trainer.evaluate()用于评估模型在测试集上的表现

训练结果 6250/6250 21:45, Epoch 2/2]

StepTraining Loss
5000.373200
10000.314200
15000.300600
20000.278300
25000.265100
30000.255000
35000.203500
40000.172700
45000.151400
50000.159600
55000.161600
60000.160800
TrainOutput(global_step=6250, training_loss=0.2303094775390625, metrics={'train_runtime': 1306.3977, 'train_samples_per_second': 38.273, 'train_steps_per_second': 4.784, 'total_flos': 6623369932800000.0, 'train_loss': 0.2303094775390625, 'epoch': 2.0})更多训练的结果数据可以通过向Trainer中设置某个参数来实现
评估结果
3125/3125 03:08

{'eval_loss': 0.28910955786705017, 'eval_runtime': 188.3729, 'eval_samples_per_second': 132.715, 'eval_steps_per_second': 16.589, 'epoch': 2.0}

评估的结果和训练结果的损失值接近,模型拟合程度较好,这里除了损失值还有一些其他参数可以衡量模型性能

保存并使用模型

trainer.save_model("./my_sentiment_classifier")
tokenizer.save_pretrained("./my_sentiment_classifier")

mymodel = AutoModelForSequenceClassification.from_pretrained("./my_sentiment_classifier")
mytokenizer = AutoTokenizer.from_pretrained("./my_sentiment_classifier")
myclassifier = pipeline("sentiment-analysis", model=mymodel, tokenizer=mytokenizer)

results = myclassifier(["I like it", "I hate this movie", "I hate it", "what the fuck", "I hate this", "shit film"])

Device set to use cuda:0

[{'label': 'LABEL_1', 'score': 0.9973964691162109}, {'label': 'LABEL_0', 'score': 0.9931268692016602}, {'label': 'LABEL_1', 'score': 0.5851984620094299}, {'label': 'LABEL_0', 'score': 0.9392573833465576}, {'label': 'LABEL_0', 'score': 0.9865531325340271}, {'label': 'LABEL_0', 'score': 0.9954352974891663}]

这里不知道为什么对“I hate it”预测错了

Reference