Hugging Face中transformers库

目录

使用预训练好的bert来对你的文本做嵌入向量

我们需要用一个向量来代表整句话的意思。通常有两种做法:

[取 CLS 向量:](#取 [CLS] 向量:)

[2.平均池化(Mean Pooling): 将句子中除了填充符(padding)之外所有 token 的向量在 768 这个维度上求平均值。这在很多检索任务中效果比直接取 CLS 更好。](#2.平均池化(Mean Pooling): 将句子中除了填充符(padding)之外所有 token 的向量在 768 这个维度上求平均值。这在很多检索任务中效果比直接取 [CLS] 更好。)

[更严谨的 Mean Pooling 操作](#更严谨的 Mean Pooling 操作)

知乎文章的介绍(具体例子看链接)

分词:

BERT对中文是字符级别的分词,对待英文是到sub-word级别的​编辑

MLM任务:

情感分类任务:


使用预训练好的bert来对你的文本做嵌入向量

与传统的固定词向量(如 Word2Vec,一个词永远对应一个向量)不同,BERT 提取的是动态的、基于上下文的 Embedding。也就是说,同一个词在不同句子中,BERT 会为它生成不同的向量。

https://zhuanlan.zhihu.com/p/535100411

https://blog.csdn.net/qq_38463737/article/details/120743532?fromshare=blogdetail&sharetype=blogdetail&sharerId=120743532&sharerefer=PC&sharesource=qq_52122048&sharefrom=from_linkhttps://blog.csdn.net/qq_38463737/article/details/120743532?fromshare=blogdetail&sharetype=blogdetail&sharerId=120743532&sharerefer=PC&sharesource=qq_52122048&sharefrom=from_link

要使用预训练好的 BERT 来对文本做嵌入向量,在 Python 中最标准、最简单的做法是使用 Hugging Face 的 transformers 库和 PyTorch

python 复制代码
import torch
from transformers import BertTokenizer, BertModel

# 1. 加载预训练的 Tokenizer (分词器) 和 Model (模型)
# "bert-base-chinese" 是 Hugging Face 上专门针对中文的预训练模型
model_name = "bert-base-chinese"
tokenizer = BertTokenizer.from_pretrained(model_name)
model = BertModel.from_pretrained(model_name)

# 2. 准备你的文本
text = "老师能否再解释一下什么是embedding?"

# 3. 对文本进行 Tokenize 处理
# 这步会把文本切分成词汇,并转换为模型需要的输入格式 (包含 input_ids, attention_mask 等)
# return_tensors="pt" 表示返回 PyTorch 的张量 (Tensor)
inputs = tokenizer(text, return_tensors="pt", padding=True, truncation=True)

# 4. 将处理好的数据输入给 BERT 模型
# 使用 torch.no_grad() 是因为我们只是用模型做推理提取向量,不需要更新权重,这样可以节省内存并加速
with torch.no_grad():
    outputs = model(**inputs)

# 5. 获取 Embedding 向量!
# outputs.last_hidden_state 包含了文本中每一个 token 的向量表示
token_embeddings = outputs.last_hidden_state

print("Token Embeddings 的形状:", token_embeddings.shape)

当你运行上面的代码后,token_embeddings 的形状通常是类似这样的:[1, 18, 768]。这三个数字分别代表:

  • 1 (Batch Size): 表示我们输入了 1 个句子。

  • 18 (Sequence Length): 表示这个句子被切分成了 18 个 token(包括 BERT 自动添加的特殊符号 [CLS][SEP])。

  • 768 (Hidden Size): 这是 BERT-base 模型的默认维度。也就是说,每一个 token 都被转换成了一个长度为 768 的实数向量。

场景 A:我想要"单个词"的向量

如果你想知道句子中"解释"这个词的向量,你只需要在 token_embeddings 中找到对应位置的向量即可。这就等同于我们上一个问题中提到的,赋予了上下文含义的词向量 u。

场景 B:我想要"整个句子"的向量(最常见)

在文本分类、语义相似度匹配(比如计算两个句子意思近不近)中,

**我们需要用一个向量来代表整句话的意思。**通常有两种做法:

[CLS] 向量:

BERT 强制在每个输入的开头添加一个特殊占位符叫 [CLS]。经过注意力机制的层层计算,这个位置的输出向量通常被认为聚合了整个句子的全局语义信息。

python 复制代码
# 提取第0个位置的向量,即 [CLS] token 的向量
sentence_embedding_cls = token_embeddings[0, 0, :] 
print("句子向量(CLS)形状:", sentence_embedding_cls.shape) # 输出: torch.Size([768])

2.平均池化(Mean Pooling): 将句子中除了填充符(padding)之外所有 token 的向量在 768 这个维度上求平均值。这在很多检索任务中效果比直接取 [CLS] 更好。

python 复制代码
# 将所有 token 的向量求平均
sentence_embedding_mean = torch.mean(token_embeddings[0], dim=0)
print("句子向量(Mean)形状:", sentence_embedding_mean.shape) # 输出: torch.Size([768])
  1. token_embeddings[0]:取出第一句话

BERT 输出的 token_embeddings 默认包含了"批次(Batch)"的概念,形状是 [句子数量, 字数, 特征维度](例如 [1, 18, 768])。

  • 加了 [0],意思就是:"我只要这批数据里的第 1 句话"。

  • 取出来之后,数据就变成了一个二维表格,形状是 [18, 768]。你可以把它想象成一个 Excel 表:有 18 行(代表 18 个字),768 列(代表每个字的 768 个特征)。

  1. dim=0:指定求平均的方向

[18, 768] 这个表格里:

  • dim=0 指的是竖直方向(跨越不同的行)

  • dim=1 指的是水平方向(跨越不同的列)。

  1. torch.mean(...):执行平均操作

当我们设定 dim=0 时,torch.mean 就会沿着竖直方向,把这 18 个字在第 1 列的特征求平均,第 2 列的特征求平均......一直算到第 768 列。

最终结果: 原本 18 行的字向量,被上下"压扁"成了一行。你得到了一个形状为 [768] 的一维向量,这个向量就是这句话的句向量(Sentence Embedding)

更严谨的 Mean Pooling 操作

虽然上面的代码能帮助你直观理解 dim=0 的作用,但在实际工程中,句子通常长短不一。为了组成 Batch,我们会用特殊的 [PAD](填充符)把短句子补齐。

直接使用 torch.mean 会把这些毫无意义的 [PAD] 向量也算进平均值里,从而"污染"句子的语义。

因此,更严谨的 Mean Pooling 会利用前一步 tokenizer 输出的 attention_mask,把 padding 的位置遮蔽掉,只对真正有意义的词求平均:

python 复制代码
# 假设 token_embeddings 形状为 [1, 18, 768]
# inputs['attention_mask'] 形状为 [1, 18],真实词为 1,PAD 为 0

# 1. 扩展 mask 的维度,使其能够与 embeddings 相乘:[1, 18] -> [1, 18, 1]
input_mask_expanded = inputs['attention_mask'].unsqueeze(-1).expand(token_embeddings.size()).float()

# 2. 将 padding 位置的向量清零,然后求和
sum_embeddings = torch.sum(token_embeddings * input_mask_expanded, dim=1) 

"""
pytorch.sum(..., dim=1) 发生了什么:

既然我们要计算"一句话内部所有字的平均值",我们就应该把这 18 个字融合在一起。
所以我们要指定 dim=1(沿着字的维度)。

PyTorch 会顺着 dim=1 的方向,把这 18 个字的特征加起来(变成 1 个大一统的字)。
原本 [1, 18, 768] 的形状,中间那个 18 被"压扁"了,计算后的结果形状变成了 [1, 768]。
"""

# 3. 计算每个句子实际的非 padding token 数量 (防止除以 0)
sum_mask = torch.clamp(input_mask_expanded.sum(dim=1), min=1e-9)

# 4. 求真正的平均值 (此时按 batch 维度处理,针对第1个维度 seq_len 求和/平均)
sentence_embedding_mean = sum_embeddings / sum_mask
  1. expand() 是什么意思?为什么要把 mask 扩展?

目的:为了让"开关"和"数据"的形状完全一样,才能相乘。

  • 初始状态: inputs['attention_mask'] 的形状是 [1, 18]。里面装的是 1(代表真实的字)和 0(代表为了补齐句子而填充的无意义占位符 PAD)。你可以把它当成 18 个"开关"。

  • 遇到的问题: 我们的 token_embeddings 形状是 [1, 18, 768]。也就是说,这 18 个字,每一个字背后都有 768 个特征数字。你不能直接把一个一维的开关 [18] 去乘以一个三维的矩阵 [1, 18, 768]

  • unsqueeze(-1) 的作用: 先给 mask 增加一个空维度,把它变成 [1, 18, 1]。现在的意思是:我们有 18 个字,每个字分配了 1 个开关。

  • expand(token_embeddings.size()) 的作用: token_embeddings.size() 就是 [1, 18, 768]expand 会把那个空维度 1 复制、拉伸768

例子:

我的手写模拟图:

知乎文章的介绍(具体例子看链接)

(29 封私信) Huggingface 超详细介绍 - 知乎https://zhuanlan.zhihu.com/p/535100411

分词:

python 复制代码
# 上文的示例代码已经实例话了,这里不重复了;
print(tokenizer.encode("生活的真谛是美和爱"))  # 对于单个句子编码
print(tokenizer.encode_plus("生活的真谛是美和爱","说的太好了")) # 对于一组句子编码
# 输出结果如下:
[101, 4495, 3833, 4638, 4696, 6465, 3221, 5401, 1469, 4263, 102]
{'input_ids': [101, 4495, 3833, 4638, 4696, 6465, 3221, 5401, 1469, 4263, 102, 6432, 4638, 1922, 1962, 749, 102],
 'token_type_ids': [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
1, 1, 1, 1, 1], 
'attention_mask': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]}

# 也可以直接这样用
sentences = ['网络安全开发分为三个层级',
             '车辆系统层级网络安全开发',
             '车辆功能层级网络安全开发',
             '车辆零部件层级网络安全开发',
             '测试团队根据车辆网络安全目标制定测试技术要求及测试计划',
             '测试团队在网络安全团队的支持下,完成确认测试并编制测试报告',
             '在车辆确认结果的基础上,基于合理的理由,确认在设计和开发阶段识别出的所有风险均已被接受',]
test1 = tokenizer(sentences)

print(test1)  # 对列表encoder
print(tokenizer("网络安全开发分为三个层级"))  # 对单个句子encoder

BERT对中文是字符级别的分词,对待英文是到sub-word级别的

MLM任务:

情感分类任务:

python 复制代码
import torch
from torch.utils.data import DataLoader,Dataset
import os
import re
from random import sample
import numpy as np
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from transformers import BertModel, BertTokenizer
from tqdm import tqdm
 
# 路径需要根据情况修改,要看你把数据下载到哪里了
# 数据下载地址在斯坦福官网,网上搜索就有
data_base_path = r"./imdb_test/aclImdb"

# 这个里面是存储你训练出来的模型的,现在是空的
model_path = r"./imdb_test/aclImdb/mode"
        
#1. 准备dataset,这里写了一个数据读取的类,并把数据按照不同的需要进行了分类;
class ImdbDataset(Dataset):
    def __init__(self,mode,testNumber=10000,validNumber=5000):

        # 在这里我做了设置,把数据集分成三种形式,可以选择 "train"默认返回全量50000个数据,"test"默认随机返回10000个数据,
        # 如果是选择"valid"模式,随机返回相应数据
        super(ImdbDataset,self).__init__()

        # 读取所有的训练文件夹名称
        text_path =  [os.path.join(data_base_path,i)  for i in ["test/neg","test/pos"]]
        text_path.extend([os.path.join(data_base_path,i)  for i in ["train/neg","train/pos"]])

        if mode=="train":
            self.total_file_path_list = []
            # 获取训练的全量数据,因为50000个好像也不算大,就没设置返回量,后续做sentence的时候再做处理
            for i in text_path:
                self.total_file_path_list.extend([os.path.join(i,j) for j in os.listdir(i)])
        if mode=="test":
            self.total_file_path_list = []
            # 获取测试数据集,默认10000个数据
            for i in text_path:
                self.total_file_path_list.extend([os.path.join(i,j) for j in os.listdir(i)])
            self.total_file_path_list=sample(self.total_file_path_list,testNumber)
       
        if mode=="valid":
            self.total_file_path_list = []
            # 获取验证数据集,默认5000个数据集
            for i in text_path:
                self.total_file_path_list.extend([os.path.join(i,j) for j in os.listdir(i)])
            self.total_file_path_list=sample(self.total_file_path_list,validNumber)
   
    def tokenize(self,text):
    
        # 具体要过滤掉哪些字符要看你的文本质量如何
       
        # 这里定义了一个过滤器,主要是去掉一些没用的无意义字符,标点符号,html字符啥的
        fileters = ['!','"','#','$','%','&','\(','\)','\*','\+',',','-','\.','/',':',';','<','=','>','\?','@'
            ,'\[','\\','\]','^','_','`','\{','\|','\}','~','\t','\n','\x97','\x96','"','"',]
        # sub方法是替换
        text = re.sub("<.*?>"," ",text,flags=re.S)	# 去掉<...>中间的内容,主要是文本内容中存在<br/>等内容
        text = re.sub("|".join(fileters)," ",text,flags=re.S)	# 替换掉特殊字符,'|'是把所有要匹配的特殊字符连在一起
        return text	# 返回文本

    def __getitem__(self, idx):
        cur_path = self.total_file_path_list[idx]
		# 返回path最后的文件名。如果path以/或\结尾,那么就会返回空值。即os.path.split(path)的第二个元素。
        # cur_filename返回的是如:"0_3.txt"的文件名
        cur_filename = os.path.basename(cur_path)
        # 标题的形式是:3_4.txt	前面的3是索引,后面的4是分类
        # 如果是小于等于5分的,是负面评论,labei给值维1,否则就是1
        labels = []
        sentences = []
        if int(cur_filename.split("_")[-1].split(".")[0]) <= 5 :
            label = 0
        else:
            label = 1
        # temp.append([label])
        labels.append(label)
        text = self.tokenize(open(cur_path,encoding='UTF-8').read().strip()) #处理文本中的奇怪符号
        sentences.append(text)
        # 可见我们这里返回了一个list,这个list的第一个值是标签0或者1,第二个值是这句话;
        return sentences,labels
 
    def __len__(self):
        return len(self.total_file_path_list)
    
# 2. 这里开始利用huggingface搭建网络模型
# 这个类继承再nn.module,后续再详细介绍这个模块
# 
class BertClassificationModel(nn.Module):
    def __init__(self,hidden_size=768):
        super(BertClassificationModel, self).__init__()
        # 这里用了一个简化版本的bert
        model_name = 'distilbert-base-uncased'

        # 读取分词器
        self.tokenizer = BertTokenizer.from_pretrained(pretrained_model_name_or_path=model_name)
        
        # 读取预训练模型
        self.bert = BertModel.from_pretrained(pretrained_model_name_or_path=model_name)

        for p in self.bert.parameters(): # 冻结bert参数
                p.requires_grad = False
        self.fc = nn.Linear(hidden_size,2)

    def forward(self, batch_sentences):   # [batch_size,1]
        sentences_tokenizer = self.tokenizer(batch_sentences,
                                             truncation=True,
                                             padding=True,
                                             max_length=512,
                                             add_special_tokens=True)
        input_ids=torch.tensor(sentences_tokenizer['input_ids']) # 变量
        attention_mask=torch.tensor(sentences_tokenizer['attention_mask']) # 变量
        bert_out=self.bert(input_ids=input_ids,attention_mask=attention_mask) # 模型

        last_hidden_state =bert_out[0] # [batch_size, sequence_length, hidden_size] # 变量
        bert_cls_hidden_state=last_hidden_state[:,0,:] # 变量
        fc_out=self.fc(bert_cls_hidden_state) # 模型
        return fc_out

# 3. 程序入口,模型也搞完啦,我们可以开始训练,并验证模型的可用性

def main():

    testNumber = 10000    # 多少个数据参与训练模型
    validNumber = 100   # 多少个数据参与验证
    batchsize = 250  # 定义每次放多少个数据参加训练
    
    trainDatas = ImdbDataset(mode="test",testNumber=testNumber) # 加载训练集,全量加载,考虑到我的破机器,先加载个100试试吧
    validDatas = ImdbDataset(mode="valid",validNumber=validNumber) # 加载训练集

    train_loader = torch.utils.data.DataLoader(trainDatas, batch_size=batchsize, shuffle=False)#遍历train_dataloader 每次返回batch_size条数据

    val_loader = torch.utils.data.DataLoader(validDatas, batch_size=batchsize, shuffle=False)

    # 这里搭建训练循环,输出训练结果

    epoch_num = 1  # 设置循环多少次训练,可根据模型计算情况做调整,如果模型陷入了局部最优,那么循环多少次也没啥用

    print('training...(约1 hour(CPU))')
    
    # 初始化模型
    model=BertClassificationModel()
  
    optimizer = optim.AdamW(model.parameters(), lr=5e-5) # 首先定义优化器,这里用的AdamW,lr是学习率,因为bert用的就是这个

    # 这里是定义损失函数,交叉熵损失函数比较常用解决分类问题
    # 依据你解决什么问题,选择什么样的损失函数
    criterion = nn.CrossEntropyLoss()
    
    print("模型数据已经加载完成,现在开始模型训练。")
    for epoch in range(epoch_num):
        for i, (data,labels) in enumerate(train_loader, 0):

            output = model(data[0])
            optimizer.zero_grad()  # 梯度清0
            loss = criterion(output, labels[0])  # 计算误差
            loss.backward()  # 反向传播
            optimizer.step()  # 更新参数

            # 打印一下每一次数据扔进去学习的进展
            print('batch:%d loss:%.5f' % (i, loss.item()))

        # 打印一下每个epoch的深度学习的进展i
        print('epoch:%d loss:%.5f' % (epoch, loss.item()))
    
    #下面开始测试模型是不是好用哈
    print('testing...(约2000秒(CPU))')

    # 这里载入验证模型,他把数据放进去拿输出和输入比较,然后除以总数计算准确率
    # 鉴于这个模型非常简单,就只用了准确率这一个参数,没有考虑混淆矩阵这些
    num = 0
    model.eval()  # 不启用 BatchNormalization 和 Dropout,保证BN和dropout不发生变化,主要是在测试场景下使用;
    for j, (data,labels) in enumerate(val_loader, 0):

        output = model(data[0])
        # print(output)
        out = output.argmax(dim=1)
        # print(out)
        # print(labels[0])
        num += (out == labels[0]).sum().item()
        # total += len(labels)
    print('Accuracy:', num / validNumber)

if __name__ == '__main__':
    main()
相关推荐
2601_957418801 小时前
AI美颜本地跑支持手机、电脑、苹果系统
人工智能·ai
AI_Auto1 小时前
工业与AI融合应用 | 四个实战用例!机械装备行业AI+数字孪生+机器人落地全景
大数据·人工智能·机器人·制造
Ww.xh1 小时前
AI助手高并发性能优化四策
人工智能
浩哥学JavaAI1 小时前
2026年最新AI agent面试(07)_大模型架构基础
人工智能·面试·架构
灵机一物1 小时前
内存虚拟化软件哪家好:AI 与 CXL 时代的选型方法
人工智能
our_times2 小时前
2026年Java开发者破局指南:Spring AI 2.0 与 Agent 开发实战
java·人工智能·spring
程序员cxuan2 小时前
离谱,GPT-5.6 竟然在后台执行了 rm -rf
人工智能·后端·程序员
ForDreamMusk2 小时前
高级优化器
人工智能·深度学习
Land03292 小时前
AI网页元素变化无法自动修复?自带元素自愈的自动化解决方案
运维·人工智能·ai·自动化·rpa