HuggingFace从入门到放弃

341 阅读1小时+

一、HuggingFace 是什么?

HuggingFace是一个提供先进自然语言处理(NLP)和人工智能开源工具的平台,支持Transformer模型的开发和应用,为开发者和研究人员提供了丰富的工具、庞大的模型库和资源,极大降低了AI大模型应用开发的门槛,能够满足从研究到工业应用的各种需求。

二、HuggingFace 的核心组件

1. Transformers 库

用途:提供数千种预训练模型(如 BERT、GPT、Qwen、DeepSeek-MoE),支持文本分类、翻译、生成等任务,并处理模型的加载、微调与部署。

功能:

提供 开箱即用的模型架构(如 BERT、T5、Qwen-7B、DeepSeek),覆盖文本分类、翻译、问答、生成等任务,支持 PyTorch、TensorFlow 等框架,简化模型调用,例如通过 pipeline 函数一键完成文本分类、生成等任务,支持 模型微调(Fine-tuning),适配自定义数据。
核心特性

  • 一行代码推理:通过 pipeline API 实现零代码部署。
  • 统一的模型接口:使用 AutoModelAutoTokenizer 自动加载模型与分词器,无需关心底层框架(PyTorch/TensorFlow)。

典型代码示例:

在此之前需要先安装 Hugging Face的库:

****pip install transformers

如果还需要安装其他依赖库,如datasets和tokenizers ,可以使用以下命令:

pip install transformers datasets tokenizers

from transformers import pipeline

# 情感分析(无需训练代码)
classifier = pipeline("sentiment-analysis")
result = classifier("Hello HuggingFace!") 
print(result)  # [{'label': 'POSITIVE', 'score': 0.9986258745193481}]

# 使用国产模型 Qwen-7B 生成文本 需要先pip install transformers_stream_generator einops
generator = pipeline("text-generation", model="Qwen/Qwen-7B", trust_remote_code=True)
response = generator("人工智能的未来是", max_length=50)
print(response[0]['generated_text'])

2. Datasets 库

**功能:高性能数据管理,提供高效的数据集加载和预处理工具,支持超过数千个个公开数据集(如GLUE、SQuAD)一键加载与高效预处理。
**优势:

    • 基于 Apache Arrow 的内存优化设计,高效处理超大规模数据;
    • 支持流式加载(惰性加载),避免内存爆炸;
    • 提供数据清洗(如过滤噪声、标准化)、映射操作(如分词、特征提取)等工具,支持自定义处理流程;
    • 跨框架兼容性(PyTorch、TensorFlow 等)和多模态数据支持(文本、图像、音频)。

**Model Hub(模型仓库大模型的GitHub)
**功能: 开放的模型托管平台,涵盖 NLP、语音、多模态等领域 用户可:

    • 一键下载模型(如 Qwen-7B、ChatGLM3)
    • 上传自定义模型(支持 Git LFS 大文件托管)

**
**使用方式:通过from_pretrained()方法直接加载模型:


from transformers import AutoModel, AutoTokenizer 
model = AutoModel.from_pretrained("bert-base-uncased") 
tokenizer = AutoTokenizer.from_pretrained("bert-base-uncased")

# 从 Model Hub 加载自定义模型
model = AutoModelForCausalLM.from_pretrained("your-username/your-model")

3. Tokenizers:工业级分词引擎

功能:处理文本与模型输入间的转换,支持 BPE、WordPiece 等多种分词算法。

关键方法:

a. 加载预训练的分词器(下载语言翻译器)

    • from_pretrained(model_name): 加载预训练分词器(如 bert-base-uncased)。
    • 以下代码用的分词器为:bert-base-uncased 分词器
from transformers import BertTokenizer
# 就像是网购一个“BERT官方翻译器”
tokenizer = BertTokenizer.from_pretrained("bert-base-uncased")
# 现在可以用这个“翻译器”处理文本了
    • from_file(json_path): 从本地 JSON 文件加载自定义分词器。

b. 文本编码(把文本转换为数字)

    • encode(text): 将单条文本转换为 Encoding 对象(包含 input_idsattention_mask 等属性)。
from transformers import AutoTokenizer
# 单条文本转数字(想象成把句子变成暗号)
result = tokenizer.encode("I love NLP!")
print(result.input_ids)  
# 输出:[101, 1045, 2293, 2371, 999, 102]
# 解释:"I" -> 1045, "love" -> 2293, ... 类似字典查单词

tokenizer.encode() 将输入文本(I love NLP!")转换为模型可以理解的 token ID 序列。这些 token ID 是分词器根据模型的词汇表(模型训练时定义的一个词或子词的集合)生成的。

分词器会将文本分割为词或子词,并为每个词或子词分配一个唯一的 token ID,每一个 token ID 对应于词汇表中的一个词或子词。

这里输出的结果为:[101, 1045, 2293, 2371, 999, 102]

(1) 特殊token

    • 101:代表起始 token,即 [CLS]。它用于分类任务或表示整个序列的开头。
    • 102:代表结束 token,即 [SEP]。它用于分隔句子或表示序列的结束。

(2) 文本tonken

    • 1045:对应单词 "I"
    • 2293:对应单词 "love"
    • 17953:对应单词 "NLP"
    • 1062:对应符号 "!"
    • encode_batch(texts1,text2): 批量编码多条文本,显著提升效率。
# 使用 AutoTokenizer 进行分词
tokenizer = AutoTokenizer.from_pretrained("bert-base-uncased")
batch=tokenizer.encode(["我是张三","正在学习"])
print(batch) #输出:[101, 1855, 100, 100, 1741, 102, 1888, 100, 1817, 100, 102]

以上代码中tokenizer调用有两种写法分别是:

tokenizer.encode()

  • 用途:快速将单条文本转换为模型所需的 input_ids(即词元ID序列)。
  • 关键限制
    • 仅返回 input_ids(词元id序列, 将文本分词后,每个词元对应的唯一ID列表 ) 无法直接获取 attention_mask或 oken_type_ids
    • 不支持填充:即使指定 max_length,不会自动补零。
    • 单条处理:若需批量处理,需用循环或转为使用 batch_encode_plus().

attention_mask(注意力掩码):一个二进制张量(多维数组),用于标识哪些位置是实际内容(1),哪些是填充的占位符(0)。它的存在是为了让模型在处理批量数据时忽略填充的部分。

(1)作用场景

      • 当批量处理不同长度的文本时,较短的文本会被填充(Padding)到统一长度。
      • 模型通过attention_mask知道哪些位置需要被关注,哪些需被忽略。

(2)示例
假如有两个句子:

      • 句子1: “Hello” → 编码后长度为3(包含[CLS][SEP])。
      • 句子2: “Hi there” → 编码后长度为4。

填充到长度5后,它们的内容及掩码如下:

input_ids = [
    [101, 7592, 102, 0, 0],       # 填充两个0
    [101, 7634, 2154, 102, 0]     # 填充一个0
]
attention_mask = [
    [1, 1, 1, 0, 0],
    [1, 1, 1, 1, 0]
]

token_type_ids(段标记)

    • 用途:token_type_ids 用于标记文本中不同句子或段落的位置。常见的应用场景是“句子对任务”(如问答、文本相似度)。
    • 模型依赖BERT等模型需要token_type_ids区分两个句子:用0表示第一个句子,1表示第二个句子。
      • RoBERTaGPT-2等模型不依赖此参数,因为它们的设计不需要显式区分句子。
    • 示例
      对于句子对 “How are you? I am fine.”
tokens = ["[CLS]", "How", "are", "you", "?", "[SEP]", "I", "am", "fine", ".", "[SEP]"]
token_type_ids = [0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1]

tokenizer()(即 call 方法)

  • 用途:完整的文本预处理工具,适用于训练、推理等复杂场景。
  • 示例代码
# 单条文本 + 动态填充 + 返回PyTorch Tensor
encoded = tokenizer(
    "Hello world!",
    padding="max_length",      # 填充到固定长度(或可设为 True)
    truncation=True,           # 自动截断到模型最大长度
    max_length=512,
    return_tensors="pt"        # 返回字典包含张量('input_ids', 'attention_mask'等)
)

# 批量输入(支持自动并行处理)
batched = tokenizer(
    ["Text1", "Text2 is longer"],
    padding=True,              # 按批次动态填充到最长文本
    truncation=True,
    return_tensors="pt"
)

底层机制解析:

  • encode() :本质上是一层简化封装,直接调用 convert_tokens_to_ids() 处理分词后的词元。
  • tokenizer() :底层通过 encode_plus()(单条)或 batch_encode_plus()(批量)实现,整合了以下流程:
    1. 分词(Tokenization) : 文本拆分为词元(token)。
    2. 添加特殊标记: 如BERT的 [CLS][SEP]
    3. 截断与填充: 根据参数截断长文本,补齐短文本。
    4. 生成掩码与分段: 用于模型识别实际内容(attention_mask)和句子边界(token_type_ids)。

总结:

  • 优先使用 ****tokenizer() :现代Hugging Face代码风格更倾向于直接调用 tokenizer(),因其功能全面且与训练/推理流程无缝衔接。
  • 了解 ****encode() ****的局限性:仅适用于简单调试或无需复杂预处理(如填充、掩码)的场景。
  • 需要更灵活控制时(如动态修改分词结果),可结合 tokenize() + convert_tokens_to_ids() 实现底层操作。

convert_tokens_to_ids():将词元(Token)转换为对应的ID,将输入的词元(Token)列表转换为模型词汇表中的唯一整数ID,这些ID是模型内部用于表示文本的数值形式。

c. 解码与转换

    • decode(ids): 将 Token ID 序列还原为原始文本(自动合并子词)。

delist=[101, 1045, 2293, 17953, 2361, 999, 102]#对应 [CLS] i love nlp! [SEP]
decodeStr=tokenizer.decode(delist)
print(decodeStr)#输出结果为 [CLS] i love nlp! [SEP] ,上面转的时候是I love NLP!

在上面的代码中发现:原本是I love NLP! 生成的[101, 1045, 2293, 2371, 999, 102] 解码后变成了[CLS] i love nlp! [SEP]多出来了[CLS][SEP]并原因是: 在 BERT 模型中,[CLS] 和 [SEP] 是特殊标记,分别表示 分类标记 和 分隔标记。它们是由分词器自动添加到输入文本中的,以便模型能够正确地处理输入序列。解码后出现 [CLS] 和 [SEP],是因为解码器默认会还原分词器生成的全部内容,包括这些特殊标记。

在解码时可以选择去掉[CLS]和[SEP]

# 解码并去掉特殊标记 decodeStr = tokenizer.decode(delist, skip_special_tokens=True)

变成小写的原因是: 使用的是 uncased 模型(如 bert-base-uncased),解码后的文本会自动转换为小写。如果要保留大小写,可以使用 cased 模型比如:bert-base-cased

    • convert_ids_to_tokens(ids): 仅将 ID 转为 Token 字符串(保留子词分隔符)。
# 将ID序列转换为Token字符串
tokens = tokenizer.convert_ids_to_tokens(delist)
print(tokens)#输出结果为['i', 'love', 'nl', '##p', '!']

为什么p的前面有两个##:因为在 BERT 的分词器中,## 是 子词标记(subword token)的一部分,用来表示该子词是一个单词的后续部分,而不是一个独立的单词。在你的输出中,'##p' 前面的 ## 表示 p 是单词 nlp 的后续部分,而tokenizer.decode会自动处理子词合并。

  1. 分词器训练
    • train(files, trainer): 根据语料文件训练自定义分词器(需指定训练器如 BpeTrainer)。
    • save(directory): 保存训练好的分词器到本地。

首先需要安装pip install tokenizers

from tokenizers import Tokenizer
from tokenizers.models import BPE
from tokenizers.trainers import BpeTrainer
from tokenizers.pre_tokenizers import Whitespace

# 定义训练函数
def train(files, trainer):
    # 初始化分词器模型(使用 BPE 算法)
    tokenizer = Tokenizer(BPE(unk_token="[UNK]"))

    # 设置预分词器(按空格分割)
    tokenizer.pre_tokenizer = Whitespace()

    # 训练分词器
    tokenizer.train(files, trainer)

    return tokenizer

# 定义保存函数
def save(tokenizer, directory):
    # 将分词器保存到指定目录
    tokenizer.save(directory)

# 定义语料文件路径
files = [r"D:\AILearn\HuggingFace\corpus.txt"]  # 替换为你的语料文件路径,使用原始字符串避免转义

# 定义 BPE 训练器
trainer = BpeTrainer(
    special_tokens=["[PAD]", "[UNK]", "[CLS]", "[SEP]", "[MASK]"],  # 指定特殊标记
    vocab_size=30000  # 指定词汇表大小
)

# 训练分词器
custom_tokenizer = train(files, trainer)

# 保存分词器
save(custom_tokenizer, "custom_tokenizer.json")

print("自定义分词器训练并保存完成!")#文件名是custom_tokenizer.json

2. 实用工具

    • get_vocab_size(): 获取词汇表大小。
    • token_to_id(token): 查询 Token 对应的 ID。
    • post_processor: 配置后处理逻辑(如添加特殊 Token)。

4. Spaces(应用部署)

功能:快速部署AI应用(如聊天机器人、文本生成),支持Gradio、Streamlit等交互界面。

三、Hugging Face的使用

1.Hugging Face API 使用

先登录进去找到AccessTonkens

选择右上角的:Create new token

把能钩的框全部钩上

点击创建 token

在此处复制,不然关了就没有了

下面我们使用Token进行访问

import requests
#使用Token访问在线模型

API_URL = "https://api-inference.huggingface.co/models/uer/gpt2-chinese-cluecorpussmall"
API_TOKEN = "你的Token"
headers = {"Authorization": f"Bearer {API_TOKEN}"}

response = requests.post(API_URL,headers=headers,json={"inputs":"你好,Hugging face"})
print(response.json())

输出结果一个胡说八道的人工智障:

2.Hugging Face 模型的下载与使用

1.下载与加载模型

在模型库中搜索模型 Hugging Face 提供了一个庞大的模型库,你可以通过以下步骤来查找所需的模型:

  1. 先解决一下科学上网再访问模型库页面:huggingface.co/models

  2. 在搜索栏中输入关键字,如 "GPT-2" 或 "BERT",然后点击搜索。

  3. 你可以使用左侧的过滤器按任务、框架、语言等条件筛选模型。

到指定文件夹 找到所需模型后,你可以通过代码将模型下载到指定的文件夹,并加载模型:

#将模型下载到本地调用
from transformers import AutoModelForCausalLM,AutoTokenizer

#将模型和分词器下载到本地,并指定保存路径
model_name = "uer/gpt2-chinese-cluecorpussmall"
#将模型放在本项目的model下
cache_dir = "model/uer/gpt2-chinese-cluecorpussmall"

#下载模型
AutoModelForCausalLM.from_pretrained(model_name,cache_dir=cache_dir)
#下载分词工具
AutoTokenizer.from_pretrained(model_name,cache_dir=cache_dir)

print(f"模型分词器已下载到:{cache_dir}")

这里导入了两个类AutoModelForCausalLM、AutoTokenizer作用如下:

AutoTokenizer:

用于自动加载与预训练模型匹配的分词器。例如,加载 GPT-2 的文本分词工具,将输入的文本分割成模型能理解的 token(如单词、子词等)。

AutoModelForCausalLM:

用于自动加载因果语言模型(Causal Language Model),这类模型的特点是仅基于上文生成文本(如 GPT 系列)。常用于文本生成任务(如续写、对话等)。

传统方式需要根据具体模型手动选择对应的类和配置(例如 GPT2Tokenizer, GPT2LMHeadModel),而使用 Auto 类可以自动推断模型结构,只需指定模型名称(如 "gpt2"、"gpt-neo"),无需手动调整代码。

如果需要处理其他任务(如文本分类、翻译),可替换为 AutoModelForSequenceClassification AutoModelForSeq2SeqLM 等类。

下载后的模型在snapshots下面,大多数模型是放在一起的,gpt2模型分开的所有2个,model.safetensors就是下面需要用到的模型,config.json就是模型的配置文件。

在config.json中有个属性需要注意:

"vocab_size": 21128 现在使用的模型是文本语言模型,他识别的文字字符是有限的

pythorch_model.bin表示模型是pytorch格式的,上面的model.safetensors是HuggingFace格式的基于pytorch

special_tokens_map.json 里面定义了一些特殊字符,大多数的gpt2或者bert模型常见的特殊字符就这5个

{

"unk_token": "[UNK]",

"sep_token": "[SEP]",

"pad_token": "[PAD]",

"cls_token": "[CLS]",

"mask_token": "[MASK]"

}

  1. unk_token: [UNK](Unknown Token)
    含义:表示 未知词汇(模型词汇表中未包含的词)。
    用途:
    当输入的文本中有词不在预训练模型的词汇表中时,会用 [UNK] 替换。
    例如:如果模型词汇表没有"ChatGPT",这个词会被替换为 [UNK]。
    重要性:避免模型因为未知词汇而报错,保证输入格式有效。
  2. sep_token: [SEP](Separator Token)
    含义:表示 分隔符,用于分割不同的片段(如句子对)。
    用途:
    在需要处理两个句子的任务中使用(如问答、文本对分类)。
    例如:BERT处理句子对时格式为:"[CLS] 句子1 [SEP] 句子2 [SEP]"。
    某些模型(如RoBERTa)可能用 代替。

重要性:让模型明确区分不同的文本片段。

  1. pad_token: [PAD](Padding Token)
    含义:表示 填充符,用于将不同长度的序列补齐到相同长度。
    用途:
    当批量处理文本时,短于最大长度的序列用 [PAD] 填充。
    例如:输入序列 ["Hello", "world"] 和 ["How", "are", "you"],

假设最大长度为3,填充后变为 ["Hello", "world", "[PAD]"] 和 ["How", "are", "you"]。
重要性:使批量输入的张量形状一致,便于并行计算。
注意:如果模型没有显式定义 pad_token,可能需要手动指定(通常会借用 [PAD])。

  1. cls_token: [CLS](Classification Token)
    含义:表示 分类标记,通常位于输入序列的开头。
    用途:
    文章开头的标签,告诉模型:“重点从这里开始”,常用于分类任务。
    例如:BERT对 "[CLS] 今天天气很好 [SEP]" 的分类会取第一个位置的输出。
    重要性:为分类任务提供“全局”语义特征。
  2. mask_token: [MASK](Mask Token)
    含义:表示 遮罩标记,用于预训练任务中的掩码语言模型(MLM)。
    用途:
    在BERT预训练时,随机遮盖输入中15%的词,用 [MASK] 替换,模型预测被遮盖的词。
    例如:输入 "我[MASK]北京" → 模型预测被遮盖的词可能为 "爱"。
    重要性:帮助模型学习上下文语义关系。

tokenizer_config.json 是 Tokenizer(分词器)的配置文件,用来告诉HuggingFace库如何正确加载和使用对应的分词器。可以把它理解为分词器的"使用说明书"。

文件中字段作用解释:

tokenizer_class:告诉程序用哪个分词类(如BertTokenizer)。

do_lower_case:输入是否全转小写(影响词汇表匹配)。

model_max_length:允许输入的最大长度。

现在使用的自然语义模型都是有长度限制的,deepSeek,Kimi等,gpt2模型也有长度限制默认是 1024,如果超过1024他会自动截断。

vocab.txt:这个里面存放的是模型能够识别的所有字符

2.本地离线调用下载的gpt2模型

#本地离线调用GPT2
from transformers import AutoModelForCausalLM,AutoTokenizer,pipeline

#设置具体包含config.json的目录,只支持绝对路径
model_dir = r"D:\AILearn\demo_1\trsanformers_test\model\uer\gpt2-chinese-cluecorpussmall\models--uer--gpt2-chinese-cluecorpussmall\snapshots\c2c0249d8a2731f269414cc3b22dff021f8e07a3"

#加载模型和分词器
#model 模型:就像一个通过阅读海量文章成为「接龙高手」的大脑。这个 GPT-2 模型已经学习过中文书籍、网页等数据,知道如何接续句子。
model = AutoModelForCausalLM.from_pretrained(model_dir)
#tokenizer 分词器:相当于一台「句子切碎机」。它把输入的句子拆成小碎片(如 "你好"→ ["你","好"]),让模型能理解并处理。
tokenizer = AutoTokenizer.from_pretrained(model_dir)

#使用加载的模型和分词器创建生成文本的pipeline
#这里device="cpu" 设置为cpu,有N卡可以用cuda
generator = pipeline("text-generation",model=model,tokenizer=tokenizer,device="cpu")

#生成文本
# output = generator("你好,我是一款语言模型,",max_length=50,num_return_sequences=1)
output = generator(
    "你好,我是一款语言模型,",#生成文本的输入种子文本(prompt)。模型会根据这个初始文本,生成后续的文本
    max_length=50,#指定生成文本的最大长度。这里的 50 表示生成的文本最多包含 50 个标记(tokens)
    num_return_sequences=1,#参数指定返回多少个独立生成的文本序列。值为 1 表示只生成并返回一段文本。
    truncation=True,#该参数决定是否截断输入文本以适应模型的最大输入长度。如果 True,超出模型最大输入长度的部分将被截断;如果 False,模型可能无法处理过长的输入,可能会报错。
    temperature=0.7,#该参数控制生成文本的随机性。值越低,生成的文本越保守(倾向于选择概率较高的词);值越高,生成的文本越多样(倾向于选择更多不同的词)。0.7 是一个较为常见的设置,既保留了部分随机性,又不至于太混乱。
    top_k=50,#该参数限制模型在每一步生成时仅从概率最高的 k 个词中选择下一个词。这里 top_k=50 表示模型在生成每个词时只考虑概率最高的前 50 个候选词,从而减少生成不太可能的词的概率。
    top_p=0.9,#该参数(又称为核采样)进一步限制模型生成时的词汇选择范围。它会选择一组累积概率达到 p 的词汇,模型只会从这个概率集合中采样。top_p=0.9 意味着模型会在可能性最强的 90% 的词中选择下一个词,进一步增加生成的质量。
    clean_up_tokenization_spaces=True#该参数控制生成的文本中是否清理分词时引入的空格。如果设置为 True,生成的文本会清除多余的空格;如果为 False,则保留原样。默认值即将改变为 False,因为它能更好地保留原始文本的格式。
)
print(output)

输出的结果:

那么这个模型是如何生成文本的:

首先上面使用的是cluecorpussmall 版本的 GPT-2 参数量小(约数百万量级),适合在 CPU 上运行,但生成能力弱于原始 GPT-2 (1.5 亿参数)大规模的版本

假想模型就是你的大脑,你已经把上面的句子拆成了这样:原始句子 → ["你","好",",","我","是","一款","语言","模型",","]

想象模型是一个拥有超强阅读记忆的概率计算器

  1. 拆解输入
    当你输入"你好,我是一款语言模型,",分词器像切肉丁一样将句子切成小碎片(Token),例如:
    ["你", "好", ",", "我", "是", "一款", "语言", "模型", ","]
  2. 词汇表限制
    拿到提示词后他要进行续写,但是他不会写字也不会生成文本,模型他只认识字典vocab.txt里的 21,128 个符号(由config.json里vocab_size属性决定),这些符号可能是单字、词语或标点。例如“可以”在字典中的编号是 305,“猫咪”可能不在字典里(会被拆成“猫”+“咪”)。
  3. 概率计算
    每生成一个新词时,模型会输出 21,128 个分数值(Logits) ,代表每个符号的潜在可能性。例如:
"基于": 8.2"基本": 7.5"可能": 4.1分
...
"企鹅": -2.3分(极不可能)

这21128个分数值就是由temperature、top_k、top_p来控制

temperature(温度): 调节评分转化为概率的「锐利度」。

概率 = softmax(分数 / temperature) # 温度=0.7 会将高分变得更突出

  • 温度低(如0.3)→ 只看重高分选项,生成结果更保守(如选“基于”)
  • 温度高(如1.5)→ 分数差异被缩小,可能选中低分选项(如“可能”甚至“企鹅”)
  • 改成0 模型不再计算概率分布(即不考虑候选词的多样性),而是直接在 21,128 个候选词永远选择得分最高的词

每生成一个词都会去计算概率循环往复将新生成的词(比如“可以”)拼接到原句末尾,重新运行上述步骤,直到生满max_length50个词或遇到终止符。就像强迫症患者不停玩接龙游戏。

top_k: 控制模型在每一步生成时,仅从概率最高的 k 个词中选择下一个词,这里 top_k=50 表示模型在生成每个词时筛选概率最高的前50个候选词(例如剔除“企鹅”这种低概率选项),从而减少生成不太可能的词的概率。

top_p: 这里top_p=09,代表前50名中,按概率从高到低累加,直到总和超过0.9时截断,只在这部分词中选择。
(例如前5个词的概率加起来已有0.9,则排除后45个)一般top_p模型中都是固定的只需要调整前两个。

3.远程调用下载的Bert模型做文本分类

from transformers import BertTokenizer, BertForSequenceClassification, pipeline

# 设置包含预训练模型文件的本地目录路径(推荐绝对路径)
# 注意:实际使用中需确保该目录包含 config.json/pytorch_model.bin 等必要文件
# Windows系统建议使用 r"" 原始字符串格式处理路径中的反斜杠
model_dir = r"model\bert-base-chinese"

# 加载模型和分词器
# from_pretrained参数说明:
# - 第一个参数指定预训练模型名称(此处使用官方中文BERT)
# - cache_dir指定模型文件的存储目录
model = BertForSequenceClassification.from_pretrained(
    "bert-base-chinese",  # 预训练模型标识符
    cache_dir=model_dir   # 显式指定模型文件加载路径
)
tokenizer = BertTokenizer.from_pretrained(
    "bert-base-chinese",  # 需与模型匹配的分词器
    cache_dir=model_dir
)

# 创建文本分类任务管道(pipeline)
# 参数说明:
# - "text-classification": 指定任务类型为文本分类
# - model: 绑定已加载的模型对象
# - tokenizer: 绑定已加载的分词器对象
# - device="cpu": 强制在CPU上运行(如使用GPU可改为 device=0 等)
classifier = pipeline(
    task="text-classification",
    model=model,
    tokenizer=tokenizer,
    device="cpu"  # 可选值为 "cpu", "cuda", "cuda:0"等
)

# 对待分类文本进行推理
# 输入格式可以是字符串或字符串列表(批量处理)
input_text = "你好,我是一款语言模型"
result = classifier(input_text)

# 输出分类结果(列表结构,每个元素对应一个输入样本)
# 典型输出示例:[{'label': 'LABEL_0', 'score': 0.9998}]
print("分类结果:", result)

# 打印模型结构信息(可选,调试用)
# 输出模型各层架构及参数数量统计
print("\n模型结构摘要:")
print(model)

结果输出意思:

[{'label': 'LABEL_0', 'score': 0.5633}] score:越大越正

模型结构输出 BertForSequenceClassification(...)

展示模型内部组成的层次结构:

1. 主干架构分解
组件作用
bert (BertModel)BERT基础模型
embeddings将输入token转换为向量,包含: - word_embeddings:词向量表(21128词 x 768维)这个768维也是config文件中配置的hidden_size属性,指的是神经网络中隐藏层的神经元数量或者隐藏状态的维度大小 - position_embeddings:将上面768维的词向量进行位置编码的。在Transformer模型中,核心模块是自注意力机制和前馈神经网络(全连接层)。其中,自注意力机制计算与与位置无关的(即无论词序如何打乱,输出都可能相同,具有位置不变性),本身不具备捕捉序列顺序的能力,因此需要通过位置编码对词的位置信息进行显式编码。位置编码直接与词嵌入向量相加,从而让模型能够感知词(文本)在序列中的相对或绝对位置。 - token_type_embeddings:段落标记向量(如问答任务区分提问/回答)
encoder编码器: 将embeddings转化的向量代表的意思,进行分析和理解,逐层提取语义特征 它的核心目标是将原始输入(如词汇序列)转化为能全面捕捉上下文信息的高维向量表示。这种表示不仅包含词本身的含义,还编码了词与词之间的复杂关系(如语法、语义、共现模式等)。
pooler提取[CLS]标记的表示作为全文语义摘要BERT 的答案精简器,提炼模型认为最重要的768维特征
dropout防止模型过拟合的
2. 分类头详情
组件数学维度功能
classifierLinear(768 → 2)真正干活的组件:将[CLS]向量映射到分类结果 - in_features=768: 输入维度(BERT隐藏层尺寸) - out_features=2: 二分类任务的输出维度(说人话就是这个模型输出的结果只有两个)

四、基于HuggingFace的模型微调与训练

1.模型微调的基本概念

什么是模型微调(Fine-tuning):

是指在一个已经在大规模数据集上预训练好的模型(如 BERT、GPT、ResNet 等)的基础上,针对特定任务和目标数据集,调整模型参数(或部分参数)以优化模型在该任务上的性能。

微调的核心是迁移学习(Transfer Learning),基于预训练模型的高维特征提取能力(模型的通用知识:如语言理解或图像特征提取能力),通过调整少量参数(而非从头训练),使模型快速适应新任务,解决样本不足导致的过拟合问题。快速适配到新任务上。

2.为什么要进行模型微调:

1. 避免“从头训练”的时间和资源浪费

预训练模型已经投入大量计算资源(比如 BERT 在大规模文本上训练了很长时间),直接复用其参数无需从头学习低级特征(如边缘、纹理、通用语法)。

场景示例:训练一个中文情感分析模型时,直接使用预训练 BERT 的词汇、句法理解能力,无需从零开始训练语言模型。

2. 解决小样本任务的过拟合问题

如果任务数据量小(例如只有几千条标注的情感分析数据),直接训练复杂模型容易过拟合。

微调时,预训练模型提供了强初始化参数,仅用少量数据调整即可适应新任务。

场景示例:医疗领域的文本分类任务通常数据稀缺,微调可以充分利用通用语言模型的泛化能力。

3. 适配任务差异

预训练模型的原始目标和下游任务通常不同(例如 BERT 的预训练任务是掩码语言模型,而实际任务是情感分类)。通过微调,可以:

调整模型输出:添加任务特定的输出层(如分类层)。

捕捉任务相关特征:微调特征提取权重,使模型适应任务数据分布(如情感分析中的情感关键词)。

4. 跨领域的知识迁移

预训练模型的通用知识可以在不同领域间迁移。例如:

基于通用中文 BERT 模型,可微调出金融领域、法律领域的情感分析模型。

3.模型微调的作用机制

1. 参数调整范围

全量微调:调整预训练模型的所有参数(例如 BERT 的每一层权重)。

部分微调(更高效) :仅调整顶层(如最后几层 Transformer 块)和分类层。

使用轻量适配器(Adapter)或冻结部分参数。

2. 学习率设计

预训练参数:通常用较小的学习率(例如 BERT 参数的学习率设为 2e-5),避免破坏已有知识。

新添加的层(如分类层):可以用较大的学习率(例如 1e-3),快速适应任务。

上面的学习率中符号 e 表示“10的幂次方”,即:

2e-5 = = 0.00002

1e-3 = = 0.001

预训练参数用较小的原因是因为:学习率过大(如1e-3)可能破坏预训练特征,导致灾难性遗忘(Catastrophic Forgetting)

3. 数据驱动的权重更新

权重更新: 模型根据数据计算的梯度调整参数,减少预测误差的过程。

数据驱动: 梯度和参数调整的方向完全由输入的数据及其标签主导,没有数据输入则无法完成有效更新。

通过任务数据及其标注(例如带标签的评论)的反向传播来校准模型的参数,逐步优化模型权重(参数),最终使预训练模型模型特征提取能力被"校准"到目标任务的语义方向(例如从通用语言理解转化为情感极性判断)

权重:就是模型的参数

梯度:梯度是一个向量(矢量),就是参数调整的方向和幅度

反向传播: 是神经网络训练的核心算法,其本质是通过链式法则计算损失函数对模型参数的梯度,是量化「误差来源」的工具,精准定位每个参数的责任大小。

4.模型训练流程(评论区情感分析)

1.获取数据集(前面已经下载过模型了这里就不下载了)

lansinuote/ChnSentiCorp这个数据集

首先需要安装pip install datasets

from datasets import load_dataset,load_from_disk

#在线加载数据
# dataset = load_dataset(path="lansinuote/ChnSentiCorp",cache_dir="data/")
# print(dataset)
#转为csv格式
# dataset.to_csv(path_or_buf=r"D:\AICode\demo_02\data\ChnSentiCorp.csv")

#加载缓存数据
datasets = load_from_disk(r"D:\AICode\demo_02\data\ChnSentiCorp")
print(datasets)

train_data = datasets["test"]
for data in train_data:
    print(data)

#扩展:加载CSV格式数据
# dataset = load_dataset(path="csv",data_files=r"D:\AICode\demo_02\data\hermes-function-calling-v1.csv")
# print(dataset)

2.创建自己的数据集

from torch.utils.data import Dataset
from datasets import load_from_disk

class MyDataset(Dataset):
    #初始化数据集
    def __init__(self,split):
        #从磁盘加载数据
        self.dataset = load_from_disk(r"D:\AICode\demo_02\data\ChnSentiCorp")
        if split == "train":
            self.dataset = self.dataset["train"]
        elif split == "test":
            self.dataset = self.dataset["test"]
        elif split == "validation":
            self.dataset = self.dataset["validation"]
        else:
            print("数据名错误!")

    #返回数据集长度
    def __len__(self):
        return len(self.dataset)

    #对每条数据单独做处理
    def __getitem__(self, item):
        text = self.dataset[item]["text"]
        label = self.dataset[item]["label"]

        return text,label

if __name__ == '__main__':
    dataset = MyDataset("train")
    for data in dataset:
        print(data)

3.模型构建

import torch
from transformers import BertModel

#定义设备信息
DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(DEVICE)

#加载预训练模型,将模型加载到DEVICE这个设备上
pretrained = BertModel.from_pretrained(r"D:\AICode\demo_02\model\bert-base-chinese\models--bert-base-chinese\snapshots\c30a6ed22ab4564dc1e3b2ecbf6e766b0611a33f").to(DEVICE)
print(pretrained)

#定义下游任务模型(增量模型)
class Model(torch.nn.Module):
    #模型初始化
    def __init__(self):
        #初始化父类的模型
        super().__init__()
        #设计全连接网络,实现二分类任务,这个模型的输出是2维就两个值,输入是上一个模型的输出因为是768维的所以传768
        self.fc = torch.nn.Linear(768,2)
    #使用模型处理数据(这里仅前向计算规则),这个数据传进来之后先给到Bert模型在到我们写的这个模型
    #所以第一个参数是self,而后面的参数就是分词器返回的input_ids 就是编码后的词,attention_mask,token_type_ids
    def forward(self,input_ids,attention_mask,token_type_ids):
        #冻结Bert模型的参数,让其不参与训练,不参与反向传播算法
        with torch.no_grad():
            out = pretrained(input_ids=input_ids,attention_mask=attention_mask,token_type_ids=token_type_ids)
        #增量模型参与训练,out.last_hidden_state[:,0]这句意思是:在神经网络算法里面输出结果是NSV的序列特征,我们需要的是他输出的最后的序列特征,
        #目前Transformer模型还是延用了RNN的数据模型,他对数据是N(批次)S(数据的长度)V(数据的特征),因为序列信息很特殊,最后一段序列是包含所有完整的信息的
        out = self.fc(out.last_hidden_state[:,0])
        return out

4.模型增量微调与训练

#模型训练
import torch
from MyData import MyDataset
from torch.utils.data import DataLoader
from net import Model
from transformers import BertTokenizer,AdamW

#定义设备信息
DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
#定义训练的轮次(将整个数据集训练完一次为一轮),给大一点因为不知道他啥时候能训练完
EPOCH = 200

#加载字典和分词器
token = BertTokenizer.from_pretrained(r"D:\AICode\demo_02\model\bert-base-chinese\models--bert-base-chinese\snapshots\c30a6ed22ab4564dc1e3b2ecbf6e766b0611a33f")

#将我们的数据集(传入的字符串)进行编码
def collate_fn(data):
    sents = [i[0]for i in data]
    label = [i[1] for i in data]
    #编码
    data = token.batch_encode_plus(
        batch_text_or_text_pairs=sents,
        # 当句子长度大于max_length(上限是model_max_length)时,截断
        truncation=True,
        #这个长度是编码之后字符的长度,给到最大,给最大会对你运行时显存或内存占用里更高,如果想要调小点要注意文本的平均长度,
        #如果比平均长度低太多会导致模型输入的句子没有一个是完整的最后的训练效果也会很差
        max_length=512,
        # 一律补0到max_length
        padding="max_length",
        # 可取值为tf,pt,np,默认为list
        return_tensors="pt",
        # 返回序列长度
        return_length=True
    )
    input_ids = data["input_ids"]
    attention_mask = data["attention_mask"]
    token_type_ids = data["token_type_ids"]
    label = torch.LongTensor(label)
    return input_ids,attention_mask,token_type_ids,label

#创建数据集
train_dataset = MyDataset("train")
train_loader = DataLoader(
    dataset=train_dataset,
    #训练批次
    batch_size=90,
    #打乱数据集
    shuffle=True,
    #舍弃最后一个批次的数据,防止形状出错
    drop_last=True,
    #对加载的数据进行编码
    collate_fn=collate_fn
)
if __name__ == '__main__':
    #开始训练
    print(DEVICE)
    model = Model().to(DEVICE)
    #定义优化器,大模型训练一般都用这个AdamW优化器
    optimizer = AdamW(model.parameters())
    #定义损失函数
    loss_func = torch.nn.CrossEntropyLoss()

    for epoch in range(EPOCH):
        for i,(input_ids,attention_mask,token_type_ids,label) in enumerate(train_loader):
            #将数据放到DVEVICE上面
            input_ids, attention_mask, token_type_ids, label = input_ids.to(DEVICE),attention_mask.to(DEVICE),token_type_ids.to(DEVICE),label.to(DEVICE)
            #前向计算(将数据输入模型得到输出)
            out = model(input_ids,attention_mask,token_type_ids)
            #根据输出计算损失
            loss = loss_func(out,label)
            #根据误差优化参数
            optimizer.zero_grad()
            loss.backward()
            optimizer.step()

            #每隔5个批次输出训练信息
            if i%5 ==0:
                out = out.argmax(dim=1)
                #计算训练精度
                acc = (out==label).sum().item()/len(label)
                print(f"epoch:{epoch},i:{i},loss:{loss.item()},acc:{acc}")
        #每训练完一轮,保存一次参数
        torch.save(model.state_dict(),f"params/{epoch}_bert.pth")
        print(epoch,"参数保存成功!")

上面代码已经完成,接下来就是执行,可以在你本地执行,本地执行比较慢可以选择租个算力。接下来用租的算力来训练,这里选择基石智算平台:account.coreshub.cn/signup?invi…

1.创建本地密钥

在windows终端使用ssh-keygen命令密钥创建密钥一路回车即可,成功后会存放在C:\Users\你的用户名.ssh的目录下

2.将公钥配置到平台

将本地密钥中的公钥id_ed25519.pub以txt打开复制到SSH密钥的公钥中

3.登录容器实例,检查SSH路径是否存在

使用jupyter打开服务器执行:mkdir ~/.ssh,返回结果提示相应目录已经存在即可。

4.将公钥写入实例的 authorized_keys 中并检查

写入:"引号中的是你自己的 id_rsa.pub 的内容 "

echo "ssh-rsa AAAA*********UOeArE=@mac1.local" >> ~/.ssh/authorized_keys

检查: cat ~/.ssh/authorized_keys

5.安装openssh-server 服务

执行命令:apt-get update && apt install openssh-server

中途直接选Y就行

6.检查

a.检查SSH进程:ps -e | grep ssh

b.检查安装包:dpkg -l | grep ssh

7.启动openssh-server服务

service ssh start

8.检查openssh-server服务状态

9.连接服务器传文件

ip:117.190.234.19 port:31972 用户名密码直接用root的

10.开始训练

这里执行遇见了没有datasets库,我们直接装一把:pip install datasets

装完又遇见了transform库没有,接着装 :pip install transformers

装完后遇见了路径的错误:接着修改

vi train.py打开文件 然后 i 开始修改

EPOCH = 10 改为 EPOCH = 30000

batch_size=30 改为 batch_size=500

token = BertTokenizer.from_pretrained(r"D:\AICode\demo_02\model\bert-base-chinese\models--bert-base-chinese\snapshots\c30a6ed22ab4564dc1e3b2ecbf6e766b0611a33f")

改为:

token = BertTokenizer.from_pretrained("/root/AICode/demo_02/model/bert-base-chinese/models--bert-base-chinese/snapshots/c30a6ed22ab4564dc1e3b2ecbf6e766b0611a33f")

改完:wq!

vi net.py 打开文件 然后 i 开始修改

pretrained = BertModel.from_pretrained(r"D:\AICode\demo_02\model\bert-base-chinese\models--bert-base-chinese\snapshots\c30a6ed22ab4564dc1e3b2ecbf6e766b0611a33f").to(DEVICE)

改为:

pretrained = BertModel.from_pretrained("/root/AICode/demo_02/model/bert-base-chinese/models--bert-base-chinese/snapshots/c30a6ed22ab4564dc1e3b2ecbf6e766b0611a33f").to(DEVICE)

vi MyDate

self.dataset = load_from_disk(r"D:\AICode\demo_02\data\ChnSentiCorp")

改为:

self.dataset = load_from_disk("/root/AICode/demo_02/data/ChnSentiCorp")

vi token_test.py

token = BertTokenizer.from_pretrained(r"D:\PycharmProjects\demo_02\model\bert-base-chinese\models--bert-base-chinese\snapshots\c30a6ed22ab4564dc1e3b2ecbf6e766b0611a33f")

改为:

token = BertTokenizer.from_pretrained("/root/AICode/demo_02/model/bert-base-chinese/models--bert-base-chinese/snapshots/c30a6ed22ab4564dc1e3b2ecbf6e766b0611a33f")

然后:nohup python -u train.py > output.log 2>&1 &

然后:tail -f /root/AICode/demo_02/output.log

快的飞起

11.创建镜像仓库

12.训练完成后算力服务器保存镜像并关机

仓库名写命名空间

5.模型在训练过程中的训练损失:

以我们这个二分类的文本数据集为例,下面代码基于上面的简要部分:

1.编码输入:

首先我们在使用Bert模型前通过BertTokenizer.from_pretrained来将句子转换成了Bert模型可以看懂的数字序列:如:"我爱自然语言处理" --转换成了-->[101, 2769, 4263, 5381, 3441, 2334, 7579, 1920, 2418, 102]

然后我们创建数据集,按 batch_size=30 生成批次,drop_last=True 确保每个批次完整。

#创建数据集
train_dataset = MyDataset("train")
train_loader = DataLoader(
    dataset=train_dataset,
    #训练批次
    batch_size=30,
    #打乱数据集
    shuffle=True,
    #舍弃最后一个批次的数据,防止形状出错
    drop_last=True,
    #对加载的数据进行编码
    collate_fn=collate_fn
)

然后在组批次的过程中我们使用自定义的collate_fn(data)方法进行了如下几步操作:

2.拆数据
# 取出所有评价的句子(如:["这本书很好","这本书很差","我讨厌这本书"])
sents = [i[0] for i in data] 
# 取出所有标签(如:[0,1,0]label = [i[1] for i in data] 
3.批量编码文本:

使用BERT分词器批量处理句子,然后返回了张量:return_tensors= "pt" ****默认在创建PyTorch张量

data = token.batch_encode_plus(
  batch_text_or_text_pairs=sents,#文本
  truncation=True,      # 文本超长时截断保留512个token(BERT最大限制) 
  max_length=512,       # 限定编码后的序列最长512
  padding="max_length", # 所有句子统一填充到512长度(短则填0,长则截断)
  return_tensors="pt",  # 返回PyTorch张量格式(适合GPU计算)
  return_length=True    # 返回真实序列长度(未被填充部分的长度)
)
input_ids = data["input_ids"]
attention_mask = data["attention_mask"]
token_type_ids = data["token_type_ids"]
label = torch.LongTensor(label)
return input_ids,attention_mask,token_type_ids,label

这一步相当于一次性把多个句子打包转换为数字序列

从编码结果中分离出:

input_ids:词表映射后的数字序列(如 [101, 2769, 4263, ..., 102])

attention_mask:标识哪些位置是有效内容(1有效,0填充部分)

token_type_ids:区分句子对时的界限(单句任务中通常全0)

举个例子看的更清楚假设:

sents=["这本书没读到底,不是特别喜欢。完全可以用序中的评价来表达我的感受:可以包容,却不想实践。除了物质,我们要追求的还有很多。"]

最后在模型中的张量对应关系图是这样的:

Tokeninput_idsattention_masktoken_type_ids说明
[CLS]10110句子开始标记
682110原始句子第一个字
329110
192110
258210
326110
134210
223410
12210中文逗号
79910
322110
...(中间省略部分).........跳过中间约50个有效词
106810
11910中文句号
[SEP]10210句子结束标记
[PAD]000填充开始位置
[PAD]000
[PAD]000
...(填充到 512)000直至长度512

当然这只是一条数据的,我们是批量处理的所以他会是一堆这样形式的数据,

label = torch.LongTensor(label)这句则是把[0,1,0,1,1,1,1,....0]所有测试集的标签转化成了 PyTorch 的长整型张量(64位整数),因为后面在损失函数中要用到交叉熵损失 CrossEntropyLoss

在CPU上创建PyTorch张量

4.模型训练

我们在模型训练模块里面,导包from net import Model里面的定义了下游任务模型:

1.初始化父类的模型

2.设计全连接网络:任务特定的分类头(Classifier Head), 全连接层通过可学习的权重参数,来达到我们的目的将输入的 768维向量转换为2维输出

3.进行定义了模型前向计算的逻辑:

冻结Bert模型的参数。

调用预训练模型(BERT)进行特征提取[CLS]的向量作为句子表示,获得out

out = pretrained(input_ids=input_ids,attention_mask=attention_mask,token_type_ids=token_type_ids)

通过全连接层完成分类:

#增量模型参与训练
out = self.fc(out.last_hidden_state[:,0])

将句子表示(768维)映射到类别标签维度(2维)。

参数细节:

self.fc 是一个 nn.Linear(768, num_classes) 层。

768 对应BERT的隐藏层维度,num_classes 是分类的类别数(比如二分类为2)。

最终输出结果:形状为 [BatchSize, num_classes],每行是样本属于各个类别的原始分数(Logits)。

完整的流程如下:

input_ids 形状: [2, 512]

→ BERT处理后 last_hidden_state 形状: [2, 512, 768]

→ 取 [CLS] 后的向量形状: [2, 768]

→ 经过全连接层 out 形状: [2, 2]

创建数据集 时使用了DataLoader默认将批次的张量放在了CPU上,所以后面需要挪到GPU去,除非你就在CPU上进行跑模型,主要是因为 PyTorch官方设计时有内存安全性考虑:大规模数据直接加载到GPU可能导致显存溢出。

4.定义优化器:

优化器负责根据模型的反向传播梯度(loss.backward())调整模型的参数(权重和偏置),逐步最小化损失函数(如交叉熵损失),最终达到让模型参数以更鲁棒(面对各种意外情况时仍然能保持稳定性和有效性,不会轻易崩溃或失效)的方式更新,提升微调效果并简化超参数调优。

AdamW 是 Adam 优化算法的改进版本,专门解决了传统 Adam 算法中权重衰减实现方式的问题****。

#定义优化器
    optimizer = AdamW(model.parameters())
5.定义多分类任务的交叉熵损失函数:

衡量模型输出的概率分布与真实标签的分布之间的差异,作为反向传播信号来源提供损失值,指导模型参数通过梯度下降优化方向,促使模型对正确类别的预测概率最大化,错误类别的概率最小化。

损失越小,模型对于当前数据的处理能力就越好

#定义损失函数
loss_func = torch.nn.CrossEntropyLoss()
6.开始训练
1.out的返回值是什么

前向计算out返回的是out = tensor([

[ 0.8, -0.3], # 第1条文本的 logits :未经归一化的预测分数 没有经过 softmax 激活函数之前的值就是logits

[-0.2, 1.5], # 第2条文本的 logits

... # 共30行(批次)

])

我们要通过损失函数来进行求导,根据导数更新参数,每更新一次参数,模型下一次的loss就会变小(也称为梯度下降):

在这个过程中发生了下面这些事情:

假设:我们的两条数据对应返回的out是这样的

文本1:"这本书很棒" ,真实标签为 1(正面)

文本2:"书的内容剧情拖沓,毫无新意" → 真实标签为 0(负面)

out = torch.tensor([

[0.8, -0.3], # 句子1的 logits:模型认为属于类别1的强度更高

[1.2, -1.0] # 句子2的 logits:模型错误偏向类别0

])

对应的张量 label = torch.tensor([1, 0]) # 两个样本的真实标签

2.损失函数(交叉熵损失)的步骤:

1.Softmax 归一化概率:将 logits 转换为概率分布(每个样本的概率和为1)

文本1的 logits[0.8, -0.3]

prob = torch.exp([0.8, -0.3]) / sum(torch.exp([0.8, -0.3])) 
# 计算后得到 [0.7568, 0.2432] → 模型预测样本1为类别1的概率是75.68%

文本2的 logits[1.2, -1.0]

prob = torch.exp([1.2, -1.0]) / sum(torch.exp([1.2, -1.0])) 
# 计算后得到 [0.9002, 0.0998] → 模型预测样本2为类别0的概率是90.02%(与真实标签0一致)

sum(torch.exp([1.2, -1.0]) :先对张量的每个元素进行指数化(取自然底数 e 的幂),再对结果求和

torch.exp([0.8, -0.3])
输入:Python 列表 [0.8, -0.3](隐式转换为张量)
操作:对每个元素计算自然指数 e^x
输出:张量 [e0.8, e-0.3] ≈ [2.2255, 0.7408]

sum(...)
功能:对张量的所有元素求和
结果:2.2255 + 0.7408 = 2.9663

2.进行交叉熵计算:将预测概率与真实标签比对,计算信息熵的差距:

句子1的损失

真实标签是 1 → 对应概率 0.2432。

交叉熵 = -log(0.2432) ≈ 1.414

这个值越小,表示预测值越接近真实标签(如果预测概率为1,损失为0)。

句子2的损失

真实标签是 0 → 对应概率 0.9002。

交叉熵 = -log(0.9002) ≈ 0.105

这个损失较小,因为模型对样本2的判断正确且置信度高。

此时,两个样本的总损失均值为 (1.414 + 0.105)/2 ≈ 0.7595。

在 PyTorch 中,CrossEntropyLoss 的默认行为是对所有样本的损失计算均值(mean)(如果设置 reduction='mean')。

为何默认使用均值而非总和?

(1)Batch Size 的鲁棒性

当批量大小变化时,均值的量纲是稳定的,而总和会随样本数量线性增长。

示例场景:

Batch Size=32:总损失可能是 32 左右的数

Batch Size=64:总损失可能达到 64 上下

但使用均值时,两者的损失值在同一量级,便于:

监控训练过程(损失变化曲线更平滑)

固定学习率的效果稳定(梯度更新幅度不受批量大小影响)

(2)梯度更新的公平性

反向传播时,损失值的量级直接影响梯度的大小。假设两个不同的批处理:

Batch1:样本数=2,损失总和=3.8,均值=1.9

Batch2:样本数=4,损失总和=7.6,均值=1.9

如果使用总和,Batch2的梯度会是Batch1的2倍,而学习率保持不变的情况下,参数更新会更剧烈。

均值的优势:无论批量大小如何,单样本对梯度的平均贡献相同。

(3)学习率设计的独立性

如果固定使用均值损失,你可以独立地根据模型复杂度选择学习率(例如 lr=0.001),无需关心批量大小。

如果使用总和损失,则需根据批量大小调整学习率(例如 BatchSize=64 时可能需要将 lr 缩小64倍),增加超参数调节难度。

3.清除模型参数的梯度optimizer.zero_grad()

梯度是什么?

上面也说过梯度是一个向量(矢量),就是参数调整的方向和幅度。在训练神经网络时,梯度是损失函数对模型参数的偏导数,它指示了如何调整参数以减少损失。

loss = loss_func(out,label) 得出损失后,我们执行了optimizer.zero_grad() 清除模型参数的梯度

他的作用是清除优化器管理的所有参数(模型权重)的梯度。这一步是训练过程中不可或缺的关键操作,因为 PyTorch 的梯度默认是累积的(accumulated),而非覆盖的。

如果不清除梯度,梯度会不断累积,导致参数更新错误,模型训练效果变差。

optimizer.zero_grad() 的作用是将模型参数的梯度归零,确保每次反向传播时,梯度是基于当前批次的数据重新计算的。

具体来说:

在每次前向传播和反向传播之间调用 optimizer.zero_grad()。

清除梯度后,loss.backward() 会基于当前损失重新计算梯度。

梯度的大小(向量的模)表示损失函数对参数变化的敏感性

  • 梯度越大,说明当前参数对损失函数的影响越大,可以更大幅度地调整参数。
  • 梯度越小,说明当前参数对损失函数的影响越小,需要更谨慎地调整参数。
4.反向传播 loss.backward()

在 PyTorch 中 反向传播loss.backward(),通过自动微分(Autograd)机制,从损失函数(Loss)出发,反向计算模型中所有参数的梯度(即损失函数对参数的偏导数)。梯度表达了“如何调整参数才能最快速降低损失”,为优化器(如 SGD、Adam)的更新提供依据。从而指引优化器更新模型参数的方向和幅度。

5.更新参数 optimizer.step()

在 PyTorch 中,optimizer.step() 是优化器的核心方法,它根据反向传播计算的梯度(loss.backward())更新模型的参数(例如权重和偏置),使模型朝着降低损失的方向改进。优化器的实现细节依赖于具体的算法(如 SGD、Adam 等),但它们的目标一致:依据梯度信息调整参数,逐步逼近损失函数的最小值。

6.监控并保存参数

计算 acctorch.save(model.state_dict(), ...)

#定义损失函数
loss_func = torch.nn.CrossEntropyLoss()
for epoch in range(EPOCH):
        for i,(input_ids,attention_mask,token_type_ids,label) in enumerate(train_loader):
            ## 实例化模型,并将模型参数迁移到相应设备(GPU或CPU)
            input_ids, attention_mask, token_type_ids, label = input_ids.to(DEVICE),attention_mask.to(DEVICE),token_type_ids.to(DEVICE),label.to(DEVICE)
            #前向计算(将数据输入模型得到输出)
            out = model(input_ids,attention_mask,token_type_ids)
            #根据输出计算损失
            loss = loss_func(out,label)
            #根据误差优化参数
            optimizer.zero_grad()
            loss.backward()
            optimizer.step()

            #每隔5个批次输出训练信息
            if i%5 ==0:
                out = out.argmax(dim=1)
                #计算训练精度
                acc = (out==label).sum().item()/len(label)
                print(f"轮次epoch:{epoch},i:{i},损失loss:{loss.item()},精度acc:{acc}")
        #每训练完一轮,保存一次参数
        torch.save(model.state_dict(),f"params/{epoch}_bert.pth")
        print(epoch,"参数保存成功!")

5.如何判断模型的训练是有效的

模型在刚开始训练时要关注损失(loss),根据训练前几次的损失是否呈下降趋势,如果呈下降趋势模型的训练一般是正确的

6.模型训练中的常见问题及解决方法

1.模型本身错误

模型本身错误导致训练效果越来越差:如层连接或参数设计错误、模型容量不当(过于简单或者过于复杂导致的过拟合、欠拟合),参数初始化错误

2. 数据问题

实际上大多数的问题都来自于数据问题:

1.质量差

噪声数据、标注错误。

解决:数据清洗、人工复审。

2.分布偏差

训练集与验证/测试集分布不一致。

解决:确保数据划分的随机性,使用交叉验证。

3.数据泄露

验证集或测试集信息混入训练集。

解决:严格隔离数据集,检查预处理流程。

3. 训练不稳定

1.梯度爆炸

常见于深度神经网络的反向传播过程中,梯度在传播过程中逐渐增大,导致梯度计算过大,使得模型的参数更新异常,最终导致梯度下降算法不稳定或崩溃。

    • 表现:损失函数值急剧增大,模型参数变得极大或极小:参数更新的公式通常是

参数 = 参数 - 学习率*梯度。当梯度爆炸时,由于梯度的值过大,会导致参数在更新过程中出现极大的变化。

    • 解决
      • 使用梯度裁剪(Gradient Clipping)           
import torch
import torch.nn as nn

# 定义模型
model =torch.nn.Linear(768,2)
#定义优化器:一个随机梯度下降的优化器对象,也可以是其他的优化器optimizer = AdamW(model.parameters())
optimizer = torch.optim.SGD(model.parameters(), lr=0.01)

# 前向传播和
#反向传播

# 梯度裁剪
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)

# 参数更新
optimizer.step()
      • 批次归一化: 通过对每一层的输入进行归一化处理,使得输入数据的均值为 0,方差为 1。这样可以减少数据分布的变化,使得梯度在反向传播过程中更加稳定,从而避免梯度爆炸的发生。

以我们这个为例在全连接层之前添加:归一化后的特征输入全连接层

# 添加 批次归一化层(BatchNorm)(输入特征维度768)        
self.bn = torch.nn.BatchNorm1d(768)
#全连接层
self.fc = torch.nn.Linear(768, 2)
      • 更稳定的激活函数,我们的代码中默认使用 torch.nn.CrossEntropyLoss,其内部已集成 LogSoftmax,无需额外添加激活函数。
2.梯度消失

在深层神经网络中,由于权重的累积,梯度在反向传播过程中会逐渐趋于零,导致靠近输入层的网络参数无法得到有效的更新,从而使模型的训练效果不佳。

    • 表现:损失函数下降缓慢甚至停滞,模型参数几乎不更新:由于梯度非常小,根据参数更新公式参数 = 参数 - 学习率*梯度,参数的更新量也非常小。。
    • 解决:使用更合适的激活函数、残差连接、初始化合适的权重、选择合适的网络架构。
3.损失震荡

学习率过高或数据噪声大。

    • 解决:采用学习率调度(如余弦退火)、增大批次大小。

余弦退火:

from torch.optim.lr_scheduler import CosineAnnealingLR
# 定义模型
model =torch.nn.Linear(768,2)
#定义优化器
optimizer = AdamW(model.parameters())
scheduler = CosineAnnealingLR(optimizer, T_max=100)  # T_max 是余弦退火的周期
# 模拟训练过程
for epoch in range(100):
    # 前向传播、反向传播和参数更新
    # ...
    optimizer.step()
    scheduler.step()

4. 过拟合(Overfitting)

  • 表现:训练集效果优秀,但验证集性能停滞或下降。
  • 原因:数据量不足、模型复杂度过高、训练轮次过多。
  • 解决:增加数据增强、引入正则化(如Dropout、权重衰减)、早停法(Early Stopping)、简化模型结构。

5. 欠拟合(Underfitting)

  • 表现:训练集和验证集效果均不佳。
  • 原因:模型容量不足、训练不充分(如学习率过低、轮次过少)。
  • 解决:扩大模型规模、增加训练轮次、调整优化器参数(如增大学习率)。

7.模型效果评估

1.客观评估

通过固定指标,客观数据来判断模型是否有效

2.主观评估

人为挑选部分代表性数据,观察模型的输出结果

8.客观评估

客观评估代码:训练和评估时要在同一台设备上,并且批次也不能变,因为模型在评估时,批次和设备的不同会导致结果有一定的偏差,这个偏差不会很大,但是会有,因为设备上不同的显卡,cuda的版本不一样,驱动不一样,他们在处理浮点运算的计算精度会有一定的差异,甚至于Pytorch版本不一样也会有差异。训练的参数要和评估的参数保持一致。

模型评估时不需要优化器也不需要损失:一般关注精度,评估时之只用评估一轮的数据

#模型训练
import torch
from MyData import MyDataset
from torch.utils.data import DataLoader
from net import Model
from transformers import BertTokenizer,AdamW

#定义设备信息
DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")

#加载字典和分词器
token = BertTokenizer.from_pretrained(r"D:\AICode\demo_02\model\bert-base-chinese\models--bert-base-chinese\snapshots\c30a6ed22ab4564dc1e3b2ecbf6e766b0611a33f")

#将传入的字符串进行编码
def collate_fn(data):
    sents = [i[0]for i in data]
    label = [i[1] for i in data]
    #编码
    data = token.batch_encode_plus(
        batch_text_or_text_pairs=sents,
        # 当句子长度大于max_length(上限是model_max_length)时,截断
        truncation=True,
        #现在用的是自然语义模型,在处理数据长度时,数据的处理的方法必须与训练时保持一致,给的不一样会导致模型最终的结果有偏差不客观
        max_length=512,
        # 一律补0到max_length
        padding="max_length",
        # 可取值为tf,pt,np,默认为list
        return_tensors="pt",
        # 返回序列长度
        return_length=True
    )
    input_ids = data["input_ids"]
    attention_mask = data["attention_mask"]
    token_type_ids = data["token_type_ids"]
    label = torch.LongTensor(label)
    return input_ids,attention_mask,token_type_ids,label

#创建数据集
test_dataset = MyDataset("test")
test_loader = DataLoader(
    dataset=test_dataset,
    #训练批次
    batch_size=10,
    #打乱数据集
    shuffle=True,
    #舍弃最后一个批次的数据,防止形状出错
    drop_last=True,
    #对加载的数据进行编码
    collate_fn=collate_fn
)
if __name__ == '__main__':
    acc = 0.0
    total = 0
    #开始测试
    print(DEVICE)
    model = Model().to(DEVICE)
    #加载模型训练参数
    model.load_state_dict(torch.load("params/9_bert.pth"))
    #开启测试模式(让模型切换到评估模式)
    model.eval()

    for i,(input_ids,attention_mask,token_type_ids,label) in enumerate(test_loader):
        #将数据放到DVEVICE上面
        input_ids, attention_mask, token_type_ids, label = input_ids.to(DEVICE),attention_mask.to(DEVICE),token_type_ids.to(DEVICE),label.to(DEVICE)
        #前向计算(将数据输入模型得到输出)
        out = model(input_ids,attention_mask,token_type_ids)
        out = out.argmax(dim=1)
        acc += (out==label).sum().item()
        print(i,(out==label).sum().item())
        total+=len(label)
    print(f"test acc:{acc/total}")

out.argmax(dim=1) : 他的作用是从out返回的两个预测分数里面取最大的那个类别的索引,从而得到模型的预测结果,一般在分类任务中使用。

在使用out.argmax(dim=1) 我出现了如下错误:IndexError: Dimension out of range (expected to be in range of [-1, 0], but got 1)

打印输出了:

print("Shape验证:", out.shape)-->Shape验证: torch.Size([10,2])

print("维度数(dim):", out.dim())-->维度数(dim): 2

print("张量具体值:", out)-->张量具体值: tensor([[-1.4389, 0.4494]], device='cuda:0', grad_fn=)

这三个值都是正常的,并且out维度也是2维,但是依然报错,这叫「维度元数据(dimension metadata)损坏」于是又排查了

print("张量是否连续:", out.is_contiguous())# 如果输出 False,则说明存储不连续,我的结果是连续的

又打印了

print("形状:", out.shape) # 预期为 [10,2]

print("步幅:", out.stride()) # 二维张量为 (2,1)

print("存储偏移:", out.storage_offset())#存储偏移 是0

然后强制把张量从用cpu弄出来

cpu_out = out.cpu()

print(cpu_out.argmax(dim=1)) # 若 CPU 下正常,说明是 CUDA 相关 bug

 print("张量实际设备:",out.device),然后好了。

model.eval() 的作用是 **让模型切换到「评估模式」**主要为了解决以下两个问题:

1.关闭特定网络层的行为(关键作用)

常见网络层:

Dropout 层(随机丢弃神经元):在训练时防止过拟合,但测试时需要关闭,否则会随机丢弃特征,导致结果不稳定。

Batch Normalization 层(归一化):训练时基于当前批次数据计算均值和方差,测试时需使用训练集中统计的全局均值和方差。

model.eval() 的原理:

它会自动遍历模型的所有子模块(如 Dropout、BatchNorm),将它们的 training 属性设置为 False,使这些层在推理时行为一致且稳定。

示例:若模型有 Dropout,测试时不调用 model.eval(),每次推理结果可能不同(随机性)。

2.规范代码习惯(次要但重要)

即使你的模型没有 Dropout 或 BatchNorm,也应加上 model.eval()。
这是一种良好的编程习惯,明确告知他人这是推理阶段而非训练阶段。

3.精度判断

最终out的argmax取到的张量 tensor([1, 0, 1, 1, 1, 1, 0, 1, 0, 1])

标签的张量 tensor([1, 0, 1, 1, 1, 1, 0, 1, 0, 1])

我们精度的评判方法就是统计,out的argmax取到的张量和标签的张量对应位置的值是否相同:

acc += (out==label).sum().item()

total+=len(label)

获取平均精度 print(f"test acc:{acc/total}")

4.混淆矩阵

分类模型评判精度的标准实际上是混淆矩阵里面包含了分类模型的所有评价指标,我们上面用的只是其中一种,以上是客观评估,只针对分类模型,对生成模型没有参考价值

9.主观评估
#模型使用接口(主观评估)
#模型训练
import torch
from net import Model
from transformers import BertTokenizer

#定义设备信息
DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")

#加载字典和分词器
token = BertTokenizer.from_pretrained(r"D:\AICode\demo_02\model\bert-base-chinese\models--bert-base-chinese\snapshots\c30a6ed22ab4564dc1e3b2ecbf6e766b0611a33f")
model = Model().to(DEVICE)
names = ["负向评价","正向评价"]

#将传入的字符串进行编码
def collate_fn(data):
    sents = []
    sents.append(data)
    #编码
    data = token.batch_encode_plus(
        batch_text_or_text_pairs=sents,
        # 当句子长度大于max_length(上限是model_max_length)时,截断
        truncation=True,
        max_length=512,
        # 一律补0到max_length
        padding="max_length",
        # 可取值为tf,pt,np,默认为list
        return_tensors="pt",
        # 返回序列长度
        return_length=True
    )
    input_ids = data["input_ids"]
    attention_mask = data["attention_mask"]
    token_type_ids = data["token_type_ids"]
    return input_ids,attention_mask,token_type_ids

def test():
    #加载模型训练参数
    model.load_state_dict(torch.load("params/16_bert.pth"))
    #开启测试模型
    model.eval()

    while True:
        data = input("请输入测试数据(输入‘q’退出):")
        if data=='q':
            print("测试结束")
            break
        input_ids,attention_mask,token_type_ids = collate_fn(data)
        input_ids, attention_mask, token_type_ids = input_ids.to(DEVICE),attention_mask.to(DEVICE),token_type_ids.to(DEVICE)

        #将数据输入到模型,得到输出
        with torch.no_grad():
            out = model(input_ids,attention_mask,token_type_ids)
            out = out.argmax(dim=1)
            print("模型判定:",names[out],"\n")

if __name__ == '__main__':
    test()

服务器上400轮测试,还算准确

7.模型的泛化性

定义: 模型在训练数据之外的新数据上的预测能力。

对于AI模型而言泛化性是个非常重要的指标,精度、性能也是衡量AI模型能力的指标。在同一个设备上,模型越大精度越高,性能越慢,模型越小性能越好,精度也会降低。

上面这段中,我使用了林黛玉式书评,给出的结果判断精确率还算准确,这并不是因为我这400轮的训练很好,而是因为冻结了Bert的原有参数,没有参与训练,bert-base-chinese的泛化性较好,证明基座模型在训练时是有类似这种的数据参与进去了的。

8.模型在训练中的状态:

1.拟合

  • 表现:模型的分布恰巧能够表达数据的核心分布规律,在训练集和测试集上都有不错的表现且稳定学到通用的规律。模型像个学霸既能做原题,也能解新题
  • 关键:找到模型复杂度和数据的平衡

2. 欠拟合(Underfitting)

  • 表现:训练集和验证集效果均不佳,模型的分布弱于数据的分布。模型像个学渣训练集和测试集 都表现差(比如考试不及格,课本原题也不会做)
  • 原因:模型过于简单、训练时间不够(如学习率过低、轮次过少)。
  • 解决:扩大模型规模、增加训练轮次、调整优化器参数(如增大学习率)。
  • 如何避免:loss损失较大就是欠拟合

3. 过拟合(Overfitting)

  • 表现:训练集效果优秀,模型过度拟合数据的分布规律,会使得模型的结果依赖于数据中的噪声信息,一旦数据的场景发生细微的变化,就可能导致结果错误。模型像书呆子
    在训练集上得分超神,测试集上得分暴跌(课本题全对,没见过的题全蒙)。

假设有让模型学习的是图片,白色的猫背景是红的,黄狗背景是蓝的,噪声数据就是背景颜色,过拟合就有可能会白猫背景是蓝色识别成了狗

  • 原因:数据量不足、模型复杂度过高、训练轮次过多。
  • 解决:增加数据增强、引入正则化(如Dropout、权重衰减)、早停法(Early Stopping)、简化模型结构。
  • 过拟合是无法逆转要么重新训练,要么拿到之前的权重训练,上面的代码也可以防止过拟合,每一轮都进行了保存,需要的地方再获取就行。
  • 通过下面代码边训练边验证防止过拟合:best_bert.pth是拟合状态的权重,损失往上涨的时候不进行存储刚好存在最优点。

#根据验证准确率保存最优参数

if val_acc > best_val_acc:

best_val_acc = val_acc

torch.save(model.state_dict(),"params1/best_bert.pth")

print(f"EPOCH:{epoch}:保存最优参数:acc{best_val_acc}")

前期先看训练集损失的下降趋势判断训练是否有效,然后看验证集损失,因为大多数时候验证集的损失在前期是会往上涨的(这是正常的),后期关注验证集的损失。

验证集损失为什么会往上涨:因为这就等于让一个学生在学校里第一天上课第二天就闭卷考试了(边训练边验证),肯定会考不好,甚至有可能连续的三次考试,第一天考好了,第二天考的烂。

#模型训练
import torch
from MyData import MyDataset
from torch.utils.data import DataLoader
from net import Model
from transformers import BertTokenizer,AdamW

#定义设备信息
DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
#定义训练的轮次(将整个数据集训练完一次为一轮)
EPOCH = 30000

#加载字典和分词器
token = BertTokenizer.from_pretrained(r"D:\AICode\demo_02\model\bert-base-chinese\models--bert-base-chinese\snapshots\c30a6ed22ab4564dc1e3b2ecbf6e766b0611a33f")

#将传入的字符串进行编码
def collate_fn(data):
    sents = [i[0]for i in data]
    label = [i[1] for i in data]
    #编码
    data = token.batch_encode_plus(
        batch_text_or_text_pairs=sents,
        # 当句子长度大于max_length(上限是model_max_length)时,截断
        truncation=True,
        max_length=512,
        # 一律补0到max_length
        padding="max_length",
        # 可取值为tf,pt,np,默认为list
        return_tensors="pt",
        # 返回序列长度
        return_length=True
    )
    input_ids = data["input_ids"]
    attention_mask = data["attention_mask"]
    token_type_ids = data["token_type_ids"]
    label = torch.LongTensor(label)
    return input_ids,attention_mask,token_type_ids,label



#创建数据集
train_dataset = MyDataset("train")
train_loader = DataLoader(
    dataset=train_dataset,
    #训练批次
    batch_size=50,
    #打乱数据集
    shuffle=True,
    #舍弃最后一个批次的数据,防止形状出错
    drop_last=True,
    #对加载的数据进行编码
    collate_fn=collate_fn
)
#创建验证数据集
val_dataset = MyDataset("validation")
val_loader = DataLoader(
    dataset=val_dataset,
    #训练批次
    batch_size=50,
    #打乱数据集
    shuffle=True,
    #舍弃最后一个批次的数据,防止形状出错
    drop_last=True,
    #对加载的数据进行编码
    collate_fn=collate_fn
)
if __name__ == '__main__':
    #开始训练
    print(DEVICE)
    model = Model().to(DEVICE)
    #定义优化器
    optimizer = AdamW(model.parameters())
    #定义损失函数
    loss_func = torch.nn.CrossEntropyLoss()

    #初始化验证最佳准确率
    best_val_acc = 0.0

    for epoch in range(EPOCH):
        for i,(input_ids,attention_mask,token_type_ids,label) in enumerate(train_loader):
            #将数据放到DVEVICE上面
            input_ids, attention_mask, token_type_ids, label = input_ids.to(DEVICE),attention_mask.to(DEVICE),token_type_ids.to(DEVICE),label.to(DEVICE)
            #前向计算(将数据输入模型得到输出)
            out = model(input_ids,attention_mask,token_type_ids)
            #根据输出计算损失
            loss = loss_func(out,label)
            #根据误差优化参数
            optimizer.zero_grad()
            loss.backward()
            optimizer.step()

            #每隔5个批次输出训练信息
            if i%5 ==0:
                out = out.argmax(dim=1)
                #计算训练精度
                acc = (out==label).sum().item()/len(label)
                print(f"epoch:{epoch},i:{i},loss:{loss.item()},acc:{acc}")
        #验证模型(判断模型是否过拟合)
        #设置为评估模型
        model.eval()
        #不需要模型参与训练
        with torch.no_grad():
            val_acc = 0.0
            val_loss = 0.0
            for i, (input_ids, attention_mask, token_type_ids, label) in enumerate(val_loader):
                # 将数据放到DVEVICE上面
                input_ids, attention_mask, token_type_ids, label = input_ids.to(DEVICE), attention_mask.to(
                    DEVICE), token_type_ids.to(DEVICE), label.to(DEVICE)
                # 前向计算(将数据输入模型得到输出)
                out = model(input_ids, attention_mask, token_type_ids)
                # 根据输出计算损失
                val_loss += loss_func(out, label)
                #根据数据,计算验证精度
                out = out.argmax(dim=1)
                val_acc+=(out==label).sum().item()
            val_loss/=len(val_loader)
            val_acc/=len(val_loader)
            print(f"验证集:loss:{val_loss},acc:{val_acc}")
        # #每训练完一轮,保存一次参数
        # torch.save(model.state_dict(),f"params/{epoch}_bert.pth")
        # print(epoch,"参数保存成功!")
            #根据验证准确率保存最优参数
            if val_acc > best_val_acc:
                best_val_acc = val_acc
                torch.save(model.state_dict(),"params1/best_bert.pth")
                print(f"EPOCH:{epoch}:保存最优参数:acc{best_val_acc}")
        #保存最后一轮参数
        torch.save(model.state_dict(), "params1/last_bert.pth")
        print(f"EPOCH:{epoch}:最后一轮参数保存成功!")