Hugging Face中transformers库
目录使用预训练好的bert来对你的文本做嵌入向量我们需要用一个向量来代表整句话的意思。通常有两种做法取 [CLS] 向量2.平均池化Mean Pooling 将句子中除了填充符padding之外所有 token 的向量在 768 这个维度上求平均值。这在很多检索任务中效果比直接取 [CLS] 更好。更严谨的 Mean Pooling 操作知乎文章的介绍(具体例子看链接)分词:BERT对中文是字符级别的分词对待英文是到sub-word级别的编辑MLM任务:情感分类任务:使用预训练好的bert来对你的文本做嵌入向量与传统的固定词向量如 Word2Vec一个词永远对应一个向量不同BERT 提取的是动态的、基于上下文的 Embedding。也就是说同一个词在不同句子中BERT 会为它生成不同的向量。https://zhuanlan.zhihu.com/p/535100411https://blog.csdn.net/qq_38463737/article/details/120743532?fromshareblogdetailsharetypeblogdetailsharerId120743532sharereferPCsharesourceqq_52122048sharefromfrom_linkhttps://blog.csdn.net/qq_38463737/article/details/120743532?fromshareblogdetailsharetypeblogdetailsharerId120743532sharereferPCsharesourceqq_52122048sharefromfrom_link要使用预训练好的 BERT 来对文本做嵌入向量在 Python 中最标准、最简单的做法是使用 Hugging Face 的transformers库和PyTorch。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_tensorspt 表示返回 PyTorch 的张量 (Tensor) inputs tokenizer(text, return_tensorspt, paddingTrue, truncationTrue) # 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]。经过注意力机制的层层计算这个位置的输出向量通常被认为聚合了整个句子的全局语义信息。# 提取第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]更好。# 将所有 token 的向量求平均 sentence_embedding_mean torch.mean(token_embeddings[0], dim0) 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 个特征。2.dim0指定求平均的方向在[18, 768]这个表格里dim0指的是竖直方向跨越不同的行。dim1指的是水平方向跨越不同的列。3.torch.mean(...)执行平均操作当我们设定dim0时torch.mean就会沿着竖直方向把这 18 个字在第 1 列的特征求平均第 2 列的特征求平均……一直算到第 768 列。最终结果原本 18 行的字向量被上下“压扁”成了一行。你得到了一个形状为[768]的一维向量这个向量就是这句话的句向量Sentence Embedding。更严谨的 Mean Pooling 操作虽然上面的代码能帮助你直观理解dim0的作用但在实际工程中句子通常长短不一。为了组成 Batch我们会用特殊的[PAD]填充符把短句子补齐。直接使用torch.mean会把这些毫无意义的[PAD]向量也算进平均值里从而“污染”句子的语义。因此更严谨的 Mean Pooling 会利用前一步tokenizer输出的attention_mask把 padding 的位置遮蔽掉只对真正有意义的词求平均# 假设 token_embeddings 形状为 [1, 18, 768] # inputs[attention_mask] 形状为 [1, 18]真实词为 1PAD 为 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, dim1) pytorch.sum(..., dim1) 发生了什么 既然我们要计算“一句话内部所有字的平均值”我们就应该把这 18 个字融合在一起。 所以我们要指定 dim1沿着字的维度。 PyTorch 会顺着 dim1 的方向把这 18 个字的特征加起来变成 1 个大一统的字。 原本 [1, 18, 768] 的形状中间那个 18 被“压扁”了计算后的结果形状变成了 [1, 768]。 # 3. 计算每个句子实际的非 padding token 数量 (防止除以 0) sum_mask torch.clamp(input_mask_expanded.sum(dim1), min1e-9) # 4. 求真正的平均值 (此时按 batch 维度处理针对第1个维度 seq_len 求和/平均) sentence_embedding_mean sum_embeddings / sum_mask1.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分词:# 上文的示例代码已经实例话了这里不重复了 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(网络安全开发分为三个层级)) # 对单个句子encoderBERT对中文是字符级别的分词对待英文是到sub-word级别的MLM任务:情感分类任务: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,testNumber10000,validNumber5000): # 在这里我做了设置把数据集分成三种形式可以选择 “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 modetrain: 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 modetest: 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_listsample(self.total_file_path_list,testNumber) if modevalid: 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_listsample(self.total_file_path_list,validNumber) def tokenize(self,text): # 具体要过滤掉哪些字符要看你的文本质量如何 # 这里定义了一个过滤器主要是去掉一些没用的无意义字符标点符号html字符啥的 fileters [!,,#,$,%,,\(,\),\*,\,,,-,\.,/,:,;,,,,\?, ,\[,\\,\],^,_,,\{,\|,\},~,\t,\n,\x97,\x96,”,“,] # sub方法是替换 text re.sub(.*?, ,text,flagsre.S) # 去掉...中间的内容主要是文本内容中存在br/等内容 text re.sub(|.join(fileters), ,text,flagsre.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,encodingUTF-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_size768): super(BertClassificationModel, self).__init__() # 这里用了一个简化版本的bert model_name distilbert-base-uncased # 读取分词器 self.tokenizer BertTokenizer.from_pretrained(pretrained_model_name_or_pathmodel_name) # 读取预训练模型 self.bert BertModel.from_pretrained(pretrained_model_name_or_pathmodel_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, truncationTrue, paddingTrue, max_length512, add_special_tokensTrue) input_idstorch.tensor(sentences_tokenizer[input_ids]) # 变量 attention_masktorch.tensor(sentences_tokenizer[attention_mask]) # 变量 bert_outself.bert(input_idsinput_ids,attention_maskattention_mask) # 模型 last_hidden_state bert_out[0] # [batch_size, sequence_length, hidden_size] # 变量 bert_cls_hidden_statelast_hidden_state[:,0,:] # 变量 fc_outself.fc(bert_cls_hidden_state) # 模型 return fc_out # 3. 程序入口模型也搞完啦我们可以开始训练并验证模型的可用性 def main(): testNumber 10000 # 多少个数据参与训练模型 validNumber 100 # 多少个数据参与验证 batchsize 250 # 定义每次放多少个数据参加训练 trainDatas ImdbDataset(modetest,testNumbertestNumber) # 加载训练集,全量加载考虑到我的破机器先加载个100试试吧 validDatas ImdbDataset(modevalid,validNumbervalidNumber) # 加载训练集 train_loader torch.utils.data.DataLoader(trainDatas, batch_sizebatchsize, shuffleFalse)#遍历train_dataloader 每次返回batch_size条数据 val_loader torch.utils.data.DataLoader(validDatas, batch_sizebatchsize, shuffleFalse) # 这里搭建训练循环输出训练结果 epoch_num 1 # 设置循环多少次训练可根据模型计算情况做调整如果模型陷入了局部最优那么循环多少次也没啥用 print(training...(约1 hour(CPU))) # 初始化模型 modelBertClassificationModel() optimizer optim.AdamW(model.parameters(), lr5e-5) # 首先定义优化器这里用的AdamWlr是学习率因为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(dim1) # print(out) # print(labels[0]) num (out labels[0]).sum().item() # total len(labels) print(Accuracy:, num / validNumber) if __name__ __main__: main()