news 2026/4/24 10:24:52

大模型微调实战:Hugging Face Transformers全流程解析与代码实现 | 程序员必学收藏

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
大模型微调实战:Hugging Face Transformers全流程解析与代码实现 | 程序员必学收藏

本文详细介绍了使用Hugging Face Transformers库微调大语言模型的完整流程。从Transformer模型基础概念讲起,通过问答模型微调案例,展示数据准备、预处理、训练和验证全过程。理论与实践相结合,提供可直接运行的代码示例,帮助读者理解如何将通用大模型适应特定业务场景,解决实际问题。


在自然语言处理(NLP)的浪潮中,大型预训练模型(如 BERT、GPT 等)已成为驱动各类应用的核心引擎。然而,如何让这些通用模型更好地适应我们特定的业务场景?答案便是微调(Fine-tuning)。Hugging Face 推出的 Transformers 库,凭借其无与伦比的易用性和丰富的模型生态,极大地降低了微调的技术门槛。

本文不满足于对 API 的浅尝辄止,而是希望为您提供一份兼具深度与可操作性的“食谱”。读完本文,您将不仅能成功运行代码,更能洞悉其背后的“为什么”,并具备独立解决实际问题的能力。

很多同学可能对模型的认知停留在应用层,本文意在让大家能够有一些针对模型的认知,以及训练一个模型究竟需要分为多少步骤?

基础概念

在动手编码之前,我们有必要先花些时间理解 Transformers 的核心设计哲学。这能帮助我们在遇到问题时,不仅知其然,更能知其所以然。

Transformers 模型

Transformers 模型通常规模庞大。包含数以百万计到数千万计数十亿的参数,训练和部署这些模型是一项复杂的任务。再者,新模型的推出几乎日新月异,而每种模型都有其独特的实现方式,尝试全部模型绝非易事。

Transformers 库应运而生,就是为了解决这个问题。它的目标是提供一个统一的 API 接口,通过它可以加载、训练和保存任何 Transformer 模型。

Transformers 模型用于解决各种 NLP 问题,如

  • feature-extraction(获取文本的向量表示)
  • fill-mask(完形填空)
  • ner(命名实体识别)
  • question-answering(问答)
  • sentiment-analysis(情感分析)
  • summarization(提取摘要)
  • text-generation(文本生成)
  • translation(翻译)
  • zero-shot-classification(零样本分类)

Transformers 模型主要分为 2 层 :编码器解码器,我们可以将其简单理解为输入 -> 输出

编码器和解码器
  • 编码器 (Encoder): 专职“理解”。它负责将输入文本(如一个句子)转换成富含语义信息的数字表示。非常适合做文本分类、命名实体识别等任务。代表选手:BERT、RoBERTa。
  • 解码器 (Decoder): 专职“生成”。它能根据一个初始指令(Prompt),逐字逐句地创造出新的文本。我们熟知的 GPT 系列就是典型的解码器架构。
  • 编码器-解码器 (Encoder-Decoder): “理解”与“生成”的结合体。先用编码器消化输入文本,再用解码器产出目标文本。是翻译、摘要等任务的标配。代表选手:BART、T5。
模型示例任务
编码器BERT, DistilBERT, ELECTRA, RoBERTa句子分类、命名实体识别、抽取式问答 (从文本中提取答案)
解码器CTRL, GPT, GPT-2, Transformer XL文本生成
编码器-解码器BART, T5, Marian, mBART文本摘要、翻译、生成式问答 (生成问题的回答类似 chatgpt)
架构和检查点(Checkpoints)
  • 架构:这是模型的骨架 —— 即每个层的定义以及模型中发生的每个操作。
  • Checkpoints(检查点):这些是将在给架构中结构中加载的权重参数,是一些具体的数值。

举个例子:BERT 是一个架构,而 bert-base-cased ,这是谷歌团队为 BERT 的第一个版本训练的一组权重参数,是一个参数。我们可以说“BERT 模型”和“ bert-base-cased 模型。”

Tokenizer

与其他神经网络一样,Transformers 模型无法直接处理原始文本,因此我们需要引入Tokenizer

**Tokenizer**是人类语言与机器语言之间的“翻译官”。其职责重大,主要包括:

  1. 分词 (Tokenization): 将 “今天天气真好” 这样的句子拆分成模型能认识的最小单元,如["今", "天", "天", "气", "真", "好"]
  2. ID 转换: 将每个词元(Token)映射成一个独一无二的数字 ID,即input_ids
  3. 添加特殊标记: 插入模型必需的特殊符号,比如[CLS]用于分类任务,[SEP]用于分隔句子。
  4. 生成注意力掩码 (Attention Mask): 当句子长度不一时,短句子需要被“填充”(Padding)到与长句同样的长度。注意力掩码就是一个由 0 和 1 组成的列表,告诉模型哪些是真实词元(值为 1),哪些是填充物(值为 0),计算时应忽略后者。

我们先通过分词器(Tokenizer)把文本转换为模型能够读懂的数字。

def run_tokenizer(): checkpoint = "distilbert-base-uncased-finetuned-sst-2-english" tokenizer = AutoTokenizer.from_pretrained(checkpoint) result = tokenizer( ["I am very urgent!", "I want to complain!"], padding=True, truncation=True, return_tensors="pt", ) # { # 'input_ids': # tensor([[ 101, 1045, 2572, 2200, 13661, 999, 102],[ 101, 1045, 2215, 2000, 17612, 999, 102]]), # 'attention_mask': # tensor([[1, 1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1, 1]]) # } # 输出是一个包含两个键, input_ids 和 attention_mask # input_ids 包含两行整数(每个句子一行),它们是每个句子中 token 的 ID。 print(result)
Model

模型会接收 Tokenizer 生成的数字,通过模型头等进行处理,最终生成对应任务的输出结果。例如,在情感分类任务中生成类别概率分布,分别是正面和负面。

但这些不是概率,而是logits(对数几率),是模型最后一层输出的原始的、未标准化的分数。要转换为概率,它们需要经过 SoftMax 层

下面是一个情感分类的一个例子:

def run_model(): from transformers import AutoModel, AutoModelForSequenceClassification, AutoTokenizer import torch.nn.functional as F checkpoint = "distilbert-base-uncased-finetuned-sst-2-english" model = AutoModelForSequenceClassification.from_pretrained(checkpoint) tokenizer = AutoTokenizer.from_pretrained(checkpoint) # 分词得到 input_ids input = tokenizer(["I am very urgent!", "I want to complain!"], padding=True, return_tensors="pt") res = model(**input) # 处理后序输出 probabilities = F.softmax(res.logits, dim=-1) # 获取模型输出对应的label labels = sequence_classication_model.config.id2label

从微调一个小模型学起

学习大模型最好的办法就是动手实践。下面从微调一个简单的问答模型为例子,打开学习大模型的大门吧。

智能流程总结

  1. 原料(数据)准备:我们需要一批包含context(上下文)、question(问题)和answers(答案文本及其在上下文中的起始位置)的数据集。
  2. 预处理(分词):调用与预训练模型配套的Tokenizer,将questioncontext转化为模型可消化的input_idsattention_mask等数值输入。对于 QA 任务,这一步至关重要,它还需要计算出答案在分词后序列中所对应的start_positionsend_positions
  3. 送入模型:将处理好的数据喂给一个专用于问答任务的模型,如AutoModelForQuestionAnswering
  4. 训练(微调)
  • 配置TrainingArguments,用于设定学习率、批次大小(Batch Size)等超参数。
  • 启动Trainer,它会自动处理设备分配(CPU/GPU)、梯度更新、日志记录等一系列繁杂的后台工作。
  • 训练的核心目标是:通过不断调整模型权重,使得模型预测的答案起止位置,与我们提供的真实标签越来越接近。
  1. 出厂(后处理):将新的问题和上下文输入给微调完毕的模型。模型会输出两组分数(start_logitsend_logits),分别代表每个词元作为答案开头和结尾的可能性。我们通过一个简单的后处理逻辑,找到分数最高的组合,便能解码出最终的答案文本。
获取数据集

训练模型最重要的事情是**数据集!**我们可以从Hugging Face等渠道获取各种各样的数据集。但是这里我们为了效果明显,自己去构建一个极为简单的数据集。

ctx = """ 权限管理平台 ACC(Auth Config Center) 为中台提供一套运行稳定、安全可靠、界面简洁的可视化权限配置能力。包括:权限配置、权限下发及鉴权功能。 其涉及了一些名词: - 权限点(keyword):权限系统中最小的权限粒度映射到业务系统对应系统操作功能。例如:查询,搜索,删除等操作 - 功能权限树:为用户提供权限下发与系统权限管控 - 菜单权限树:提供页面及菜单的权限下发与管控 - 白名单:无需登录、需要登录无需鉴权赋予某一特定角色功能 """ # 原始数据列表 raw_data = [ { "id": "001", "context": ctx, "question": "什么是 ACC", "answer_text": "权限管理平台", }, { "id": "002", "context": ctx, "question": "ACC 有哪些功能", "answer_text": "权限配置、权限下发及鉴权功能", }, { "id": "003", "context": ctx, "question": "ACC 有哪些名词", "answer_text": "权限点", }, { "id": "004", "context": ctx, "question": "权限点是干嘛的", "answer_text": "权限系统中最小的权限粒度映射到业务系统对应系统操作功能", }, ]

有了原始数据,我们需要对其进行格式转换。在学术领域,用于抽取式问答的最常用基准数据集是SQuAD,我们可以去下载一下,看看它的格式是怎样的

from datasets import load_datasetraw_datasets = load_dataset("squad")# DatasetDict({# train: Dataset({# features: ['id', 'title', 'context', 'question', 'answers'],# num_rows: 87599# })# validation: Dataset({# features: ['id', 'title', 'context', 'question', 'answers'],# num_rows: 10570# })# })# 其中answers格式为{text:string[], answer_start:int[]}

contextquestion字段的使用非常简单直接。answers字段稍显复杂,因为它包含一个带有两个字段的字典,而这两个字段都是列表。这是评估过程中squad指标所期望的格式;如果你使用自己的数据,则不一定需要费心将答案设置为相同的格式。text字段的含义相当明显,answer_start字段包含每个答案在上下文中的起始字符索引。

我们可以把我们的原始数据也转换成这种格式。完整代码如下:

def create_toy_qa_dataset() -> DatasetDict: ctx = """ 权限管理平台 ACC(Auth Config Center) 为中台提供一套运行稳定、安全可靠、界面简洁的可视化权限配置能力。包括:权限配置、权限下发及鉴权功能。 其涉及了一些名词: - 权限点(keyword):权限系统中最小的权限粒度映射到业务系统对应系统操作功能。例如:查询,搜索,删除等操作 - 功能权限树:为用户提供权限下发与系统权限管控 - 菜单权限树:提供页面及菜单的权限下发与管控 - 白名单:无需登录、需要登录无需鉴权赋予某一特定角色功能 """ # 原始数据列表 raw_data = [ { "id": "001", "context": ctx, "question": "什么是 ACC", "answer_text": "权限管理平台", }, { "id": "002", "context": ctx, "question": "ACC 有哪些功能", "answer_text": "权限配置、权限下发及鉴权功能", }, { "id": "003", "context": ctx, "question": "ACC 有哪些名词", "answer_text": "权限点", }, { "id": "004", "context": ctx, "question": "权限点是干嘛的", "answer_text": "权限系统中最小的权限粒度映射到业务系统对应系统操作功能", }, ] # 格式化数据,自动计算 answer_start formatted_data = {"id": [], "context": [], "question": [], "answers": []} for item in raw_data: context = item["context"] answer_text = item["answer_text"] # 找到答案在原文中的起始位置 start_idx = context.find(answer_text) formatted_data["id"].append(item["id"]) formatted_data["context"].append(context) formatted_data["question"].append(item["question"]) formatted_data["answers"].append( {"text": [answer_text], "answer_start": [start_idx]} ) # 创建 Dataset ds = Dataset.from_dict(formatted_data) validation_ds = ds.select(range(4)) dsd = DatasetDict({"train": ds, "validation": validation_ds}) return dsd

我们这次训练使用google-bert/bert-base-chinese模型,虽然它并不是专门用于问答任务。先展示一下微调前的效果:

model_checkpoint = "google-bert/bert-base-chinese"# 加载 Tokenizertokenizer = AutoTokenizer.from_pretrained(model_checkpoint)# 加载 Datasetraw_datasets = create_toy_qa_dataset()model = AutoModelForQuestionAnswering.from_pretrained(model_checkpoint)test_context = raw_datasets["train"][1]["context"]test_question = raw_datasets["train"][1]["question"]answer = get_answer(test_question, test_context, model, tokenizer)# answer: ''

这里的 get_answer 的具体代码在下文会详细讲解,这里认为是模型推理即可。

这里大概率为空或乱码(因为该模型没学过这个任务),我们需要对它进行微调来让模型能够满足我们的效果。

预处理数据集

我们需要将数据集中的文本信息处理成Input IDs。利用DatasetDict中的map方法,可以对整个数据集做批处理:

tokenized_datasets = raw_datasets.map( lambda x: preprocess_function(x, tokenizer), batched=True, # 是否批处理 remove_columns=raw_datasets[ "train" ].column_names, # 移除原始文本列,只保留分词后的列 即 Input ID 等 ) ``````plaintext def preprocess_function(samples, tokenizer): tokenized_inputs = tokenizer( examples["question"], examples["context"], max_length=384, truncation="only_second", return_offsets_mapping=True, padding="max_length", ) # ...

首先tokenizer中我们传入了每一个样本 (数据集中的每一条数据) 的questioncontext,这样将会对同时对两者进行处理。

  • max_length:表示tokenizer处理的最大长度,这里假设答案一定在前 384 个 Token 里。如果文章很长,超出部分直接扔掉。
  • truncation="only_second":跟上述一样,如果超长,直接对context进行丢弃。
  • return_offsets_mapping=True: 返回每个 Token 对应原文的字符位置 (start_char, end_char)

最终输入的tokenizied_inputs.input_ids是一个长度为5的数组(因为数据集只有5条数据,每一项都包含 question + context)

我们可以通过tokenized_inputs.sequence_ids(i)去获取具体某条input_id中,哪一个部分代表question、哪一个部分代表context

tokenized_inputs.sequence_ids(0) # [None, 0, 0, 0, 0, None, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, ...]

tokenized_inputs["offset_mapping"]也是一个长度为5的数组,它包含了每一个Token所在的索引

# 对应索引0[(0, 0), (0, 1), (1, 2), (2, 3), (4, 7), (0, 0), (5, 6), (6, 7), (7, 8), (8, 9), (9, 10)...]

可以看到,(0, 0)刚好对应的是 None,其实就是questioncontext之间的特殊标记。

最后我们要做的,就是需要找到Token**级别下,**答案所在的位置:

  1. 先排除question部分,找到context所在的Token下的位置索引
  2. context的首尾同时遍历,直到找到包含start_char(原始数据中答案所在的位置索引) 的Token

这部分相对简单,代码如下。

def preprocess_function(examples, tokenizer): tokenized_inputs = tokenizer( examples["question"], examples["context"], max_length=384, truncation="only_second", return_offsets_mapping=True, padding="max_length", ) # 2. 处理答案位置 offset_mapping = tokenized_inputs.pop("offset_mapping") answers = examples["answers"] start_positions = [] end_positions = [] for i, offset in enumerate(offset_mapping): answer = answers[i] start_char = answer["answer_start"][0] end_char = start_char + len(answer["text"][0]) # sequence_ids 用于区分哪部分是问题,哪部分是上下文 # 0 代表问题,1 代表上下文,None 代表特殊符号([CLS], [SEP], [PAD]) sequence_ids = tokenized_inputs.sequence_ids(i) # 找到上下文在 Token 序列中的起始和结束索引 idx = 0 while sequence_ids[idx] != 1: idx += 1 context_start = idx while sequence_ids[idx] == 1: idx += 1 context_end = idx - 1 # 如果答案不在当前截断的片段中(针对超长文本),这就标记为 (0, 0) # 这里的 offset[context_end][1] 是当前片段最后一个 Token 对应的原文结束字符位置 if offset[context_start][0] > start_char or offset[context_end][1] < end_char: start_positions.append(0) end_positions.append(0) else: # 否则,我们需要找到 Token 的 start_index 和 end_index # 从上下文的第一个 Token 开始往后找,直到找到包含 start_char 的 Token idx = context_start while idx <= context_end and offset[idx][0] <= start_char: idx += 1 start_positions.append(idx - 1) # 从上下文的最后一个 Token 开始往前找,直到找到包含 end_char 的 Token idx = context_end while idx >= context_start and offset[idx][1] >= end_char: idx -= 1 end_positions.append(idx + 1) # 将计算好的 Token 级别的起始和结束位置放入 inputs 中 tokenized_inputs["start_positions"] = start_positions tokenized_inputs["end_positions"] = end_positions return tokenized_inputs
构建超参数,开始训练
args = TrainingArguments( output_dir="qa-model-finetuned", eval_strategy="no", # 数据太少,不进行分步评估 save_strategy="no", learning_rate=5e-5, per_device_train_batch_size=2, # 小批量 per_device_eval_batch_size=2, num_train_epochs=50, # 增加 epoch 以确保拟合 weight_decay=0.01, push_to_hub=False, logging_steps=10, use_mps_device=False, )

我们先来看一下模型训练时的一些重要参数:

num_train_epochs:要执行的训练轮数总和。通俗来说,1 Epoch表示模型完完整整的看过进行训练的数据集一次。

  • 如果num_train_epochs设置过多,训练出来的模型将会过拟合,即反反复复多次背诵训练的数据集,对数据集就很熟悉,但是如果遇到新的问题则可能回答不出来,泛化能力差。反之,则欠拟合。

学习率 learning_rate: 决定了每次模型训练时参数的更新幅度(0-1)。**简单来说,模型在训练过程中的效果不够好时,模型需要调整的幅度。

  • 学习率太大 (比如 0.1):老师大吼一声“全错!重写!”,学生吓得不知所措,可能下次走向另一个极端,永远找不到正确答案(模型不收敛,Loss 震荡)。
  • 学习率太小 (比如 1e-8):老师极其温柔地说“这里稍微改一点点…”,学生改了一万次才改对,等到毕业了还没学会(训练太慢,收敛不了)。
  • 合适的值 (5e-5):老师指出关键错误,让学生做适度的调整。BERT 微调通常都用这个量级(2e-5 到 5e-5),因为它已经“预习”(预训练)过了,不需要从头学,只需要微调。

批量大小 per_device_train_batch_size:指每台设备训练时的批量大小,在多GPU或分布式训练中,总**Batch size = per_device_train_batch_size * number_of_devices**

  • Batch Size = 1 :学生做完一题,老师马上批改一题。学生能立刻得到反馈,但老师会很累(计算慢),而且如果某道题出错了(脏数据),学生会被带偏。
  • Batch Size = 100 :学生做完100题,老师统一批改,告诉他“总体方向对了没有”。这样比较稳(梯度稳定),但对老师的脑容量(显存)要求很高。

权重衰减 weight_decay: 通俗来说 ,给“死记硬背”的学生扣分(惩罚项)。

它强制模型不要过于依赖某几个特定的特征(比如不要看到“因为”两个字就无脑选后面的句子做答案)。它让模型的参数尽量小且分散,这样模型的泛化能力更强。

如果模型过拟合了,可以适当调大weight_decay****。

最后,我们就可以通过Trainer,对模型进行训练啦:

... # 实例化 Trainer data_collator = DefaultDataCollator() trainer = Trainer( model=model, args=args, train_dataset=tokenized_datasets["train"], eval_dataset=tokenized_datasets["validation"], tokenizer=tokenizer, data_collator=data_collator, ) # 开始训练 trainer.train()

在模型训练过程中,终端会输出一些训练时参数

{'loss': 3.9616, 'grad_norm': 17.661741256713867, 'learning_rate': 4.7e-05, 'epoch': 3.33} {'loss': 2.0221, 'grad_norm': 26.38640594482422, 'learning_rate': 4.3666666666666666e-05, 'epoch': 6.67} {'loss': 1.7888, 'grad_norm': 30.200885772705078, 'learning_rate': 4.0333333333333336e-05, 'epoch': 10.0} {'loss': 1.2302, 'grad_norm': 46.9060173034668, 'learning_rate': 3.7e-05, 'epoch': 13.33} {'loss': 0.5915, 'grad_norm': 51.04061508178711, 'learning_rate': 3.366666666666667e-05, 'epoch': 16.67} {'loss': 0.3984, 'grad_norm': 0.5519830584526062, 'learning_rate': 3.0333333333333337e-05, 'epoch': 20.0} {'loss': 0.5358, 'grad_norm': 16.61482810974121, 'learning_rate': 2.7000000000000002e-05, 'epoch': 23.33} {'loss': 0.0591, 'grad_norm': 20.124296188354492, 'learning_rate': 2.3666666666666668e-05, 'epoch': 26.67} {'loss': 0.009, 'grad_norm': 0.022040903568267822, 'learning_rate': 2.0333333333333334e-05, 'epoch': 30.0} {'loss': 0.0018, 'grad_norm': 0.025523267686367035, 'learning_rate': 1.7000000000000003e-05, 'epoch': 33.33} {'loss': 0.0054, 'grad_norm': 0.21060892939567566, 'learning_rate': 1.3666666666666666e-05, 'epoch': 36.67} {'loss': 0.0004, 'grad_norm': 0.01707434467971325, 'learning_rate': 1.0333333333333333e-05, 'epoch': 40.0}

loss表示模型的预测与真实答案之间的差距。这个差距值,就是我们所说的“损失值”。在训练过程中我们可以发现loss逐渐减少,这证明模型在训练过程中的效果越来越符合验证集中的数据。

为什么这里的**epoch**是小数?

在上述例子中:

  • 数据总量 :只有 5 条数据。
  • 批次大小 ( per_device_train_batch_size ) :设为 2 。

那么,完成 1 个 Epoch (遍历所有数据一遍)需要走几步(Steps)?

(注:第1步取2条,第2步取2条,第3步取最后1条)

设置了logging_steps=10 。

这意味着 Trainer 每走 10 步 (Steps)就会打印一次日志。

我们来算算 10 步 相当于跑了多少个 Epoch:

所以:

  • 第 10 步时,打印日志,此时 Epoch = 10 / 3 ≈ 3.33
  • 第 20 步时,打印日志,此时 Epoch = 20 / 3 ≈ 6.67
  • 第 30 步时,打印日志,此时 Epoch = 30 / 3 = 10.0

这就是为什么你会看到 3.33 , 6.67 这种非整数的 Epoch。

效果验证和总结

我们以本节开头的例子来验证一下,那么如何去验证呢?

test_context = raw_datasets["train"][1]["context"]test_question = raw_datasets["train"][1]["question"]answer = get_answer(test_question, test_context, model, tokenizer)print(f"问题: {test_question}")print(f"回答: {answer}")

get_answer函数中,我们需要进行 **tokenizermodelc处理以及后处理**三步骤。

def get_answer(question, context, model, tokenizer): # 将模型设为评估模式 model.eval() # 1. 分词 inputs = tokenizer(question, context, return_tensors="pt") # 2. 模型前向传播 with torch.no_grad(): outputs = model(**inputs) # 3. 后处理:获取预测结果 # 模型输出包含 start_logits 和 end_logits,分别表示每个 Token 是答案开头的概率和结尾的概率 answer_start_index = torch.argmax(outputs.start_logits) answer_end_index = torch.argmax(outputs.end_logits) + 1 # +1 是因为切片是左闭右开 # 4. 将 Token ID 转换回文本 predict_answer_tokens = inputs.input_ids[0, answer_start_index:answer_end_index] predicted_answer = tokenizer.decode(predict_answer_tokens, skip_special_tokens=True) return predicted_answer

要注意的是,这里模型输出的是start_logitsend_logits,分别表示每个 Token 是答案开头的概率和结尾的"概率"(对数几率)

在这里,我们直接通过torch.argmax分别获取两者概率最大的索引,最后在Input ids中去获取相对应的Token,最后再由tokenizer解码成文本内容。

# 问题: ACC 有哪些功能# 回答: 权 限 配 置 、 权 限 下 发 及 鉴 权 功 能

最后看上去效果还不错,这次次模型微调就结束啦~

拓展内容

注意

本次微调只是一个“玩具”流程,有一些需要注意的地方:

  • 正常训练时的数据集不可能这么少,动则上万条文本数据的数据集只是门槛。。
  • 由于训练集小,因此我们的epoch设置了50轮,让模型过拟合来更好的去验证结果。正常模型训练时一般取3轮左右。

拓展内容

预处理训练集(滑动窗口)

在预处理训练集过程中,我们采用简单截断的方式:我假设你的文本很短,或者答案一定在前 384 个 Token 里。如果文章很长,超出部分直接扔掉 (truncation="only_second")。如果答案不幸在被扔掉的那部分里,模型就永远找不到了。

因此模型不需要处理“一个样本变成多个片段”的情况,代码逻辑是一对一的,非常简单。

如果训练集中的文本内容很长,我们可以给tokenizer设置stride(步长),表示将文章分割为若干个切片,每一个切片都有重合的一部分。这就导致了“ 一对多 ”的关系(1 个问题 -> N 个输入片段)。在预测时,则需要收集这 N 个片段的所有输出Logits,统一比较,找出在这个长文章中到底哪一段的哪个位置分数最高。

def preprocess_function(examples, tokenizer): tokenized_inputs = tokenizer( examples["question"], examples["context"], max_length=384, truncation="only_second", return_offsets_mapping=True, return_overflowing_tokens=Ture stride=100, padding="max_length", ) # 2. 处理答案位置 offset_mapping = tokenized_inputs.pop("offset_mapping") answers = examples["answers"] start_positions = [] end_positions = [] sample_idxs = [] for i, offset in enumerate(offset_mapping): # 表示当前片段所对应的样本索引 sample_idx = inputs["overflow_to_sample_mapping"][i] sample_idxs.append(sample_idx) answer = answers[sample_idx] start_char = answer["answer_start"][0] end_char = start_char + len(answer["text"][0]) # sequence_ids 用于区分哪部分是问题,哪部分是上下文 # 0 代表问题,1 代表上下文,None 代表特殊符号([CLS], [SEP], [PAD]) sequence_ids = tokenized_inputs.sequence_ids(i) # 找到上下文在 Token 序列中的起始和结束索引 idx = 0 while sequence_ids[idx] != 1: idx += 1 context_start = idx while sequence_ids[idx] == 1: idx += 1 context_end = idx - 1 # 如果答案不在当前截断的片段中(针对超长文本),这就标记为 (0, 0) # 这里的 offset[context_end][1] 是当前片段最后一个 Token 对应的原文结束字符位置 if offset[context_start][0] > start_char or offset[context_end][1] < end_char: start_positions.append(0) end_positions.append(0) else: # 否则,我们需要找到 Token 的 start_index 和 end_index # 从上下文的第一个 Token 开始往后找,直到找到包含 start_char 的 Token idx = context_start while idx <= context_end and offset[idx][0] <= start_char: idx += 1 start_positions.append(idx - 1) # 从上下文的最后一个 Token 开始往前找,直到找到包含 end_char 的 Token idx = context_end while idx >= context_start and offset[idx][1] >= end_char: idx -= 1 end_positions.append(idx + 1) # 将计算好的 Token 级别的起始和结束位置放入 inputs 中 tokenized_inputs["start_positions"] = start_positions tokenized_inputs["end_positions"] = end_positions tokenized_inputs["sample_idxs"] = sample_idxs return tokenized_inputs
模型后处理

在上述案例中,我们是基于最简单的“贪心”策略去实现的。我们假设分别获取作为开头、结尾时概率最大的Token,两者中间所包含的文本就是最佳答案。

这样的处理方式会有很大的问题:

  1. 问题1:开始索引大于结束索引
  • 现象:当模型预测的始位置(start_index)大于结束位置(end_index)时,切片input_ids[0, start_index:end_index]会返回空张量,导致解码后得到空字符串
  • 原因:独立使用argmax选择startend位置,未考虑两者的依赖关系(答案必须是连续片段,start <= end
  1. 问题2:置信度过低
  • 现象:即使模型对所有位置的预测置信度都很低(如context中无相关答案),代码仍会返回一个答案
  • 原因:直接使用 argmax 强制选择一个位置,未考虑模型的预测不确定性
  • 解决方式:通过遍历每一个 Token,寻找出所有的开头和结尾的组合,并计算其概率,找出概率最大的那对组合。
def get_answer(question, context, model, tokenizer): # 将模型设为评估模式 model.eval() # 1. 分词 inputs = tokenizer( question, context, max_length=384, truncation="only_second", return_offsets_mapping=True, return_overflowing_tokens=Ture stride=100, padding="max_length" ) # 2. 模型前向传播 with torch.no_grad(): outputs = model(**inputs) transform_res = [] start_logits = outputs.start_logits.cpu().numpy() end_logits = outputs.end_logits.cpu().numpy() logits_size = start_logits.shape[0] for feature_idx in range(logits_size): sample_idx = inputs["overflow_to_sample_mapping"][feature_idx] offset = inputs["offset_mappings"][feature_idx] start_logit = start_logits[feature_idx] end_logit = end_logits[feature_idx] sequence_ids = inputs.sequence_ids(feature_idx) for start_idx in start_logit: for end_idx in end_logit: if start_idx > end_idx: continue if sequence_ids[start_idx] == 0 and sequence_ids[end_idx] == 0: continue start_token = offset[start_idx][0] end_token = offset[end_idx][1] if start_token == 0 and end_token == 0: continue if end_token < start_token: continue ans_from_ctx = context[start_token:end_token] # 从原始logits中取 scroe = start_logits[logit_idx][start_idx] + end_logits[logit_idx][end] transform_res.append( { "answer": ans_from_ctx, "score": scroe, "start_token": start_token, "end_token": end_token, } ) return transform_res

​最后

我在一线科技企业深耕十二载,见证过太多因技术更迭而跃迁的案例。那些率先拥抱 AI 的同事,早已在效率与薪资上形成代际优势,我意识到有很多经验和知识值得分享给大家,也可以通过我们的能力和经验解答大家在大模型的学习中的很多困惑。

我整理出这套 AI 大模型突围资料包:

  • ✅AI大模型学习路线图
  • ✅Agent行业报告
  • ✅100集大模型视频教程
  • ✅大模型书籍PDF
  • ✅DeepSeek教程
  • ✅AI产品经理入门资料

完整的大模型学习和面试资料已经上传带到CSDN的官方了,有需要的朋友可以扫描下方二维码免费领取【保证100%免费】👇👇
​​

为什么说现在普通人就业/升职加薪的首选是AI大模型?

人工智能技术的爆发式增长,正以不可逆转之势重塑就业市场版图。从DeepSeek等国产大模型引发的科技圈热议,到全国两会关于AI产业发展的政策聚焦,再到招聘会上排起的长队,AI的热度已从技术领域渗透到就业市场的每一个角落。


智联招聘的最新数据给出了最直观的印证:2025年2月,AI领域求职人数同比增幅突破200%,远超其他行业平均水平;整个人工智能行业的求职增速达到33.4%,位居各行业榜首,其中人工智能工程师岗位的求职热度更是飙升69.6%。

AI产业的快速扩张,也让人才供需矛盾愈发突出。麦肯锡报告明确预测,到2030年中国AI专业人才需求将达600万人,人才缺口可能高达400万人,这一缺口不仅存在于核心技术领域,更蔓延至产业应用的各个环节。

​​

资料包有什么?

①从入门到精通的全套视频教程⑤⑥

包含提示词工程、RAG、Agent等技术点

② AI大模型学习路线图(还有视频解说)

全过程AI大模型学习路线

③学习电子书籍和技术文档

市面上的大模型书籍确实太多了,这些是我精选出来的

④各大厂大模型面试题目详解

⑤ 这些资料真的有用吗?

这份资料由我和鲁为民博士共同整理,鲁为民博士先后获得了北京清华大学学士和美国加州理工学院博士学位,在包括IEEE Transactions等学术期刊和诸多国际会议上发表了超过50篇学术论文、取得了多项美国和中国发明专利,同时还斩获了吴文俊人工智能科学技术奖。目前我正在和鲁博士共同进行人工智能的研究。

所有的视频教程由智泊AI老师录制,且资料与智泊AI共享,相互补充。这份学习大礼包应该算是现在最全面的大模型学习资料了。

资料内容涵盖了从入门到进阶的各类视频教程和实战项目,无论你是小白还是有些技术基础的,这份资料都绝对能帮助你提升薪资待遇,转行大模型岗位。


智泊AI始终秉持着“让每个人平等享受到优质教育资源”的育人理念‌,通过动态追踪大模型开发、数据标注伦理等前沿技术趋势‌,构建起"前沿课程+智能实训+精准就业"的高效培养体系。

课堂上不光教理论,还带着学员做了十多个真实项目。学员要亲自上手搞数据清洗、模型调优这些硬核操作,把课本知识变成真本事‌!

​​​​

如果说你是以下人群中的其中一类,都可以来智泊AI学习人工智能,找到高薪工作,一次小小的“投资”换来的是终身受益!

应届毕业生‌:无工作经验但想要系统学习AI大模型技术,期待通过实战项目掌握核心技术。

零基础转型‌:非技术背景但关注AI应用场景,计划通过低代码工具实现“AI+行业”跨界‌。

业务赋能 ‌突破瓶颈:传统开发者(Java/前端等)学习Transformer架构与LangChain框架,向AI全栈工程师转型‌。

👉获取方式:

😝有需要的小伙伴,可以保存图片到wx扫描二v码免费领取【保证100%免费】🆓**

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/22 17:40:30

深度解析79.5k星AI代理Clawdbot的持久记忆系统架构与实现

文章介绍了开源AI代理Clawdbot的持久记忆系统。该系统采用本地Markdown存储&#xff0c;结合向量搜索和BM25关键字检索&#xff0c;构建双层记忆架构&#xff08;每日日志长期记忆&#xff09;。系统支持自动压缩、记忆刷新和会话管理&#xff0c;确保信息持久性和上下文连贯性…

作者头像 李华
网站建设 2026/4/21 20:24:39

云计算第四次作业

<!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8" /><title>微博发布</title><style>* { margin: 0; padding: 0; }ul { list-style: none; }.w { width: 900px; margin: 0 auto; }.controls texta…

作者头像 李华
网站建设 2026/4/23 12:10:11

基于深度学习YOLOv11的食物检测系统(YOLOv11+YOLO数据集+UI界面+登录注册界面+Python项目源码+模型)

一、项目介绍 本文介绍了一个基于深度学习YOLOv11算法的食物检测系统&#xff0c;能够准确识别30类常见食物及饮品。系统整合了完整的YOLO数据集、用户友好的UI界面&#xff08;含登录注册功能&#xff09;以及Python项目源码与预训练模型。该模型在包含14,661张图像的数据集上…

作者头像 李华
网站建设 2026/4/20 19:23:01

《唐朝诡事录之西行》——独孤羊放妻春条书

前年暑期&#xff0c;电视剧《唐朝诡事录之西行》播出&#xff0c;其中“仵作之死”单元令我印象深刻&#xff0c;尤其是独孤羊写给妻子春条的那封休书。基于这份触动&#xff0c;我使用 Unity3D 引擎制作了一个小项目&#xff0c;通过 TextMeshPro 实现文本横竖排显示&#xf…

作者头像 李华
网站建设 2026/4/18 8:21:58

一文讲清楚Java中的抽象类、接口和内部类三大特性

目录 第一章 抽象类 1.1 概述 1.1.1 抽象类引入 1.2 abstract使用格式 1.2.1 抽象方法 1.2.2 抽象类 1.2.3 抽象类的使用 1.3 抽象类的特征 1.4 抽象类的细节 1.5 抽象类存在的意义 第二章 接口 2.1 概述 2.2 定义格式 2.3 接口成分的特点 2.3.1.抽象方法 2.3.…

作者头像 李华