赞
踩
HuggingFace
是一个开源社区,提供了统一的 AI 研发框架、工具集、可在线加载的数据集仓库和预训练模型仓库
。HuggingFace
把研发大致分为以下几个部分:准备数据集,定义模型,训练,测试
;每个部分都提供了相应的工具集
HuggingFace
提供了⼀套统⼀的编码 API
,由每个模型各⾃提交实现;由于统⼀了API,所以调⽤者能快速地使⽤不同模型的编码⼯具
编码
通常包含:定义字典、句⼦预处理、分词、编码 4 个步骤;编码工作流程示意图如下:
# 字典:这只是⼀个⽰意的字典,所以只有 11 个词,在实际项⽬中的字典可能会有成千上万个词
vocab = {
'<SOS>': 0, # Start Of Seq
'<EOS>': 1, # End Of Seq
'the': 2,
'quick': 3,
'brown': 4,
'fox': 5,
'jumps': 6,
'over': 7,
'a': 8,
'lazy': 9,
'dog': 10,
}
分词
之前,⼀般会对句⼦进⾏⼀些特殊的操作,例如把太长的句⼦截短
,或在句⼦中添加⾸尾标识符
等。# 简单编码
sent = 'the quick brown fox jumps over a lazy dog'
sent = '<SOS> ' + sent + ' <EOS>'
print(sent) # <SOS> the quick brown fox jumps over a lazy dog <EOS>
把句⼦分成⼀个⼀个的词
。对于中⽂来讲,这是个复杂的问题,但是对于英⽂来讲这个问题⽐较容易解决,因为英⽂有⾃然的分词⽅式,即以空格来分词
,代码如下:# 英⽂分词
words = sent.split()
print(words) # ['<SOS>', 'the', 'quick', 'brown', 'fox', 'jumps', 'over', 'a', 'lazy', 'dog', '<EOS>']
jieba 分词、 LTP 分词
等,但是在这里我们不会使⽤这些⼯具,因为 HuggingFace
的编码⼯具已经包括了分词这⼀步⼯作,由各个模型⾃⾏实现,对于调⽤者来讲这些⼯作是透明的,不需要关⼼具体的实现细节# 编码为数字
encode = [vocab[i] for i in words]
print(encode) # [0, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1]
# 1、加载⼀个编码⼯具,这里使⽤ bert-base-chinese 的实现:在BERT的实现中,中⽂分词处理⽐较简单,就是把每个字都作为⼀个词来处理 from transformers import BertTokenizer tokenizer = BertTokenizer.from_pretrained(pretrained_model_name_or_path='bert-base-chinese', cache_dir=None, force_download=False,) # pretrained_model_name_or_path: 指定要加载的编码⼯具,⼤多数模型会把⾃己提交的编码⼯具命名为和模型⼀样的名字 # cache_dir: ⽤于指定编码⼯具的缓存路径,这里指定为None(默认值),也可以指定想要的缓存路径 # force_download: 为 True 时表明⽆论是否已经有本地缓存,都强制执⾏下载⼯作,建议设置为 False # 2、准备实验数据 sents = [ '你站在桥上看⻛景', '看⻛景的⼈在楼上看你', '明⽉装饰了你的窗⼦', '你装饰了别⼈的梦', ] # 3、基本的编码函数 out = tokenizer.encode( text=sents[0], # ⼀次编码⼀个或者⼀对句⼦,在这个例⼦中,编码了⼀对句⼦ text_pair=sents[1], # 如果只想编码⼀个句⼦,则可让 text_pair 传 None truncation=True, # 当句⼦长度⼤于 max_length 时截断 padding='max_length', # 当句⼦长度不⾜ max_length 时,在句⼦的后⾯补充 PAD(0),直到 max_length 长度 add_special_tokens=True, # 需要在句⼦中添加特殊符号,如逗号分隔符 SEP max_length=25, # 定义了 max_length 的长度 return_tensors=None, # 表明返回的数据类型为list格式,也可以赋值为 tf、pt、np,分别表⽰ TF、PyTorch、NumPy 数据格式 ) # 编码的输出为⼀个数字的list print(out) # 使⽤编码⼯具的decode()函数把这个list还原为分词前的句⼦,可看出编码工具对句⼦做了哪些预处理⼯作 print(tokenizer.decode(out)) # 运行结果如下: [101, 872, 4991, 1762, 3441, 677, 4692, 7599, 3250, 102, 4692, 7599, 3250, 4368, 782, 1762, 3517, 677, 4692, 872, 102, 0, 0, 0, 0] [CLS] 你 站 在 桥 上 看 ⻛ 景 [SEP] 看 ⻛ 景 的 ⼈ 在 楼 上 看 你 [SEP] [PAD] [PAD] [PAD] [PAD] [CLS]=101 [SEP]=102 [PAD]=0 # 4、进阶的编码函数 out = tokenizer.encode_plus( text=sents[0], # ⼀次编码⼀个或者⼀对句⼦,在这个例⼦中,编码了⼀对句⼦ text_pair=sents[1], # 如果只想编码⼀个句⼦,则可让 text_pair 传 None truncation=True, # 当句⼦长度⼤于 max_length 时截断 padding='max_length', # 当句⼦长度不⾜ max_length 时,在句⼦的后⾯补充 PAD(0),直到 max_length 长度 add_special_tokens=True, # 需要在句⼦中添加特殊符号,如逗号分隔符 SEP max_length=25, # 定义了 max_length 的长度 return_tensors=None, # 表明返回的数据类型为list格式,也可以赋值为 tf、pt、np,分别表⽰ TF、PyTorch、NumPy 数据格式 # 进阶参数 return_token_type_ids=True, # 因为编码的是两个句⼦,这个list⽤于表明编码结果中哪些位置是第1个句⼦,哪些位置是第2个句⼦。具体表现为,第2个句⼦的位置是1,其他位置是0 return_attention_mask=True, # ⽤于表明编码结果中哪些位置是 PAD;PAD 的位置是 0,其他位置是 1 return_special_tokens_mask=True, # ⽤于表明编码结果中哪些位置是特殊符号,具体表现为,特殊符号的位置是 1,其他位置是0 return_length=True, # 返回句子长度 ) # 返回一个字典 for k, v in out.items(): print(k, ':', v) tokenizer.decode(out['input_ids'])
#第2章/批量编码成对的句⼦ out = tokenizer.batch_encode_plus( # 编码成对的句⼦,若需要编码的是⼀个⼀个的句⼦,则修改为 batch_text_or_text_pairs=[sents[0], sents[1]] 即可 batch_text_or_text_pairs=[(sents[0], sents[1]), (sents[2], sents[3])], truncation=True, # 当句⼦长度⼤于 max_length 时截断 padding='max_length', # 当句⼦长度不⾜ max_length 时,在句⼦的后⾯补充 PAD(0),直到 max_length 长度 add_special_tokens=True, # 需要在句⼦中添加特殊符号,如逗号分隔符 SEP max_length=25, # 定义了 max_length 的长度 return_tensors=None, # 表明返回的数据类型为list格式,也可以赋值为 tf、pt、np,分别表⽰ TF、PyTorch、NumPy 数据格式 # 进阶参数 return_token_type_ids=True, # 因为编码的是两个句⼦,这个list⽤于表明编码结果中哪些位置是第1个句⼦,哪些位置是第2个句⼦。具体表现为,第2个句⼦的位置是1,其他位置是0 return_attention_mask=True, # ⽤于表明编码结果中哪些位置是 PAD;PAD 的位置是 0,其他位置是 1 return_special_tokens_mask=True, # ⽤于表明编码结果中哪些位置是特殊符号,具体表现为,特殊符号的位置是 1,其他位置是0 return_length=True, # 返回句子长度 ) # input_ids 编码后的词 # token_type_ids 第1个句⼦和特殊符号的位置是0,第2个句⼦的位置是1 # special_tokens_mask 特殊符号的位置是1,其他位置是0 # attention_mask PAD的位置是0,其他位置是1 # length 返回句⼦长度 for k, v in out.items(): print(k, ':', v) tokenizer.decode(out['input_ids'][0])
# 获取字典 vocab = tokenizer.get_vocab() print(type(vocab), len(vocab), '明⽉' in vocab) # (dict, 21128, False) # 添加新词 tokenizer.add_tokens(new_tokens=['明⽉', '装饰', '窗⼦']) # 添加新符号 tokenizer.add_special_tokens({'eos_token': '[EOS]'}) # 编码新添加的词 out=tokenizer.encode( text='明⽉装饰了你的窗⼦[EOS]', text_pair=None, truncation=True, padding='max_length', add_special_tokens=True, max_length=10, return_tensors=None, ) print(out) tokenizer.decode(out) # 输出如下:可以看到,“明⽉” 已经被识别为⼀个词,而不是两个词,新的特殊符号 [EOS] 也被正确识别 [101, 21128, 21129, 749, 872, 4638, 21130, 21131, 102, 0] '[CLS] 明⽉ 装饰 了 你 的 窗⼦ [EOS] [SEP] [PAD]'
# 1、在线加载数据集:由于 HuggingFace 把数据集存储在⾕歌云盘上,在国内加载时可能会遇到⽹络问题,可离线加载使用 from datasets import load_dataset from datasets import load_from_disk dataset = load_dataset(path='seamew/ChnSentiCorp') print(dataset) # 输出如下所示: DatasetDict({ train: Dataset({ features: ['text', 'label'], num_rows: 9600 }) validation: Dataset({ features: ['text', 'label'], num_rows: 0 }) test: Dataset({ features: ['text', 'label'],}) num_rows: 1200 }) }) # 2、将数据集保存到本地磁盘 dataset.save_to_disk(dataset_dict_path='./data/ChnSentiCorp') # 3、从磁盘加载数据集 dataset = load_from_disk('./data/ChnSentiCorp')
# 1、取出数据部分 dataset = dataset['train'] # 使⽤train数据⼦集做后续的实验 # 2、查看数据样例 for i in [12, 17, 20, 26, 56]: print(dataset[i]) # 输出结果如下:字段 text 表⽰消费者的评论,字段 label 表明这是⼀段好评还是差评 {'text': '轻便,⽅便携带,性能也不错,能满⾜平时的⼯作需要,对出差⼈员来讲⾮常不错','label': 1} {'text': '很好的地理位置,⼀塌糊涂的服务,萧条的酒店。', 'label': 0} {'text': '⾮常不错,服务很好,位于市中⼼区,交通⽅便,不过价格也⾼!', 'label': 1} {'text': '跟住招待所没什么太⼤区别。绝对不会再住第2次的酒店!', 'label': 0} {'text': '价格太⾼,性价⽐不够好。我觉得今后还是去其他酒店⽐较好。', 'label': 0} # 3、打乱数据顺序 shuffled_dataset=dataset.shuffle(seed=42) # 4、将训练集切分训练集和测试集 dataset.train_test_split(test_size=0.1) DatasetDict({ train: Dataset({ features: ['text', 'label'], num_rows: 8640 }) test: Dataset({ features: ['text', 'label'], num_rows: 960 }) }) # 5、使⽤批处理加速 def f(data): text=data['text'] text=['My sentence: ' + i for i in text] data['text']=text return data maped_datatset=dataset.map(function=f, batched=True, batch_size=1000, # 以 1000 条数据为⼀个批次进⾏⼀次处理;把函数执⾏的次数削减约 1000 倍,提⾼了运⾏效率 num_proc=4) # 在 4 条线程上执⾏该任务 print(dataset['text'][20]) print(maped_datatset['text'][20]) # 6、将数据保存为 CSV 或 JSON 格式 dataset.to_csv(path_or_buf='./data/ChnSentiCorp.csv') dataset.to_json(path_or_buf='./data/ChnSentiCorp.json')
# 1、列出可⽤的评价指标 from datasets import list_metrics metrics_list = list_metrics() print(len(metrics_list), metrics_list[:5]) # (51, ['accuracy', 'bertscore', 'bleu', 'bleurt', 'cer']) # 2、加载⼀个评价指标:加载⼀个评价指标和加载⼀个数据集⼀样简单 # 将对应数据集和⼦集的名字输⼊load_metric()函数即可得到对应的评价指标,但并不是每个数据集都有对应的 # 评价指标,在实际使⽤时以满⾜需要为准则选择合适的评价指标即可。 from datasets import load_metric metric = load_metric(path='glue', config_name='mrpc') # 3、获取评价指标的使⽤说明:评价指标的 inputs_description 属性为⼀段⽂本,描述了评价指标的使⽤⽅法 print(metric.inputs_description) # 4、计算⼀个评价指标 predictions=[0, 1, 0] references=[0, 1, 1] print(metric.compute(predictions=predictions, references=references)) # 输出:{'accuracy': 0.666666666666, 'f1': 0.666666666666}
使⽤管道⼯具时,调⽤者需要做的只是告诉管道⼯具要进⾏的
任务类型
,管道⼯具会⾃动分配合适的模型
(也可以指定模型),直接给出预测结果,如果这个预测结果对于调⽤者已经可以满⾜需求,则不再需要再训练。
# 1、⽂本分类 from transformers import pipeline classifier = pipeline("sentiment-analysis") result = classifier("I hate you")[0] print(result) # {'label': 'NEGATIVE', 'score': 0.9991} result = classifier("I love you")[0] print(result) # {'label': 'POSITIVE', 'score': 0.9998} # 2、阅读理解 from transformers import pipeline question_answerer=pipeline("question-answering") context=r""" Extractive Question Answering is the task of extracting an answer from a text given a question. An example of a question answering dataset is the SQuAD dataset, which is entirely based on that task. If you would like to fine-tune a model on a SQuAD task, you may leverage the examples/PyTorch/question- answering/run_squad.py script. """ result=question_answerer(question="What is extractive question answering?",context=context,) print(result) # 输出如下: {'score': 0.61 'start': 34, 'end': 95, 'answer': 'the task of extracting an answer from a text given a question'} # 3、完形填空: sentence是⼀个句⼦,其中某些词被<mask>符号替代了,表明这是需要让模型填空的空位 from transformers import pipeline unmasker=pipeline("fill-mask") from pprint import pprint sentence='HuggingFace is creating a <mask> that the community uses to solve NLP tasks.' print(unmasker(sentence)) # 4、⽂本续写:⼊参为⼀个句⼦的开头,让text_generator接着往下续写,参数max_length=��表明要续写的长度 from transformers import pipeline text_generator=pipeline("text-generation") text_generator("As far as I am concerned, I will", max_length=50, do_sample=False) # 输出如下: [{'generated_text': 'As far as I am concerned, I will be the first to admit that I am not a fan of the idea of a "free market." I think that the idea of a free market is a bit of a stretch. I think that the idea'}] # 5、命名实体识别(Named Entity Recognition):找出⼀段⽂本中的⼈名、地名、组织机构名等 from transformers import pipeline ner_pipe=pipeline("ner") sequence = """Hugging Face Inc. is a company based in New York City. Its headquarters are in DUMBO, therefore very close to the Manhattan Bridge which is visible from the window.""" for entity in ner_pipe(sequence): print(entity) # 6、文本摘要:使⽤⽂本总结⼯具对这段长⽂本进⾏摘要 from transformers import pipeline summerizer=pipeline("summerization") # 7、翻译 from transformers import pipeline translator=pipeline("translation_en_to_de") sentence="Hugging Face is a technology company based in New York and Paris" translator(sentence, max_length=40) # 8、QA:使用本地模型 from awq import AutoAWQForCausalLM from transformers import AutoTokenizer from transformers import pipeline model_name_or_path = "TheBloke/CodeLlama-7B-Instruct-AWQ" # Load model model = AutoAWQForCausalLM.from_quantized(model_name_or_path, fuse_layers=True, trust_remote_code=True, safetensors=True) tokenizer = AutoTokenizer.from_pretrained(model_name_or_path, trust_remote_code=True) prompt = "Tell me about AI" prompt_template=f'''[INST] Write code to solve the following coding problem that obeys the constraints and passes the example test cases. Please wrap your code answer using ```: {prompt} [/INST] ''' pipe = pipeline( "text-generation", # 指定任务类型 model=model, tokenizer=tokenizer, max_new_tokens=512, # 最大长度 do_sample=True, temperature=0.7, top_p=0.95, top_k=40, repetition_penalty=1.1 ) print(pipe(prompt_template)[0]['generated_text'])
# 加载 tokenizer:编码⼯具和模型往往是成对使⽤的
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained('hfl/rbt3')
# 编码句⼦
print(tokenizer.batch_encode_plus(['明⽉装饰了你的窗⼦', '你装饰了别⼈的梦'],truncation=True,))
# 输出如下图所示:
# 从磁盘加载数据集 from datasets import load_from_disk dataset = load_from_disk('./data/ChnSentiCorp') # 缩小数据规模,便于测试 dataset['train'] = dataset['train'].shuffle().select(range(2000)) dataset['test'] = dataset['test'].shuffle().select(range(100)) print(dataset) DatasetDict({ train: Dataset({ features: ['text', 'label'], num_rows: 2000 }) validation: Dataset({ features: ['text', 'label'], num_rows: 0 }) test: Dataset({ features: ['text', 'label'], num_rows: 100 }) }) # 批处理编码 def f(data): return tokenizer.batch_encode_plus(data['text'], truncation=True) dataset=dataset.map(f, batched=True, batch_size=1000, num_proc=0, remove_columns=['text']) print(dataset) # 运行结果如下 DatasetDict({ train: Dataset({ features: ['label', 'input_ids', 'token_type_ids', 'attention_mask'], num_rows: 2000 }) validation: Dataset({ features: ['text', 'label'], num_rows: 0 }) test: Dataset({ features: ['label', 'input_ids', 'token_type_ids', 'attention_mask'], num_rows: 100 }) }) # 移除太长的句⼦:把数据集中长度超过 512 个词的句⼦过滤掉 # 也可以把超出长度的部分截断,留下符合模型长度要求的数据 def f(data): return [len(i)<=512 for i in data['input_ids']] dataset=dataset.filter(f, batched=True, batch_size=1000, num_proc=4) # 自定义数据集 import torch from datasets import load_from_disk class Dataset(torch.utils.data.Dataset): def __init__(self, split): self.dataset = load_from_disk('./data/ChnSentiCorp')[split] def __len__(self): return len(self.dataset) def __getitem__(self, i): text = self.dataset[i]['text'] label = self.dataset[i]['label'] return text, label dataset = Dataset('train')
# 加载模型:
from transformers import AutoModelForSequenceClassification
model=AutoModelForSequenceClassification.from_pretrained('hfl/rbt3', num_labels=2)
# 模型试算:模拟⼀批数据并进行试算
data = {
'input_ids': torch.ones(4, 10, dtype=torch.long),
'token_type_ids': torch.ones(4, 10, dtype=torch.long),
'attention_mask': torch.ones(4, 10, dtype=torch.long),
'labels': torch.ones(4, dtype=torch.long)}
out = model(**data)
print(out['loss'], out['logits'].shape) # (tensor(0.3597, grad_fn=<NllLossBackward0>), torch.Size([4, 2]))
# 加载评价指标 from datasets import load_metric metric = load_metric('accuracy') # 定义评价函数 import numpy as np from transformers.trainer_utils import EvalPrediction def compute_metrics(eval_pred): logits, labels = eval_pred logits = logits.argmax(axis=�) return metric.compute(predictions=logits, references=labels) # 模拟输出 eval_pred = EvalPrediction( predictions=np.array([[0, 1], [2, 3], [4, 5], [6, 7]]), label_ids=np.array([1, 1, 0, 1]), ) print(compute_metrics(eval_pred)) # {'accuracy': 0.75}
# 定义训练参数:HuggingFace使⽤TrainingArguments对象来封装超参数 from transformers import TrainingArguments # 定义训练参数 args = TrainingArguments( output_dir='./output_dir', # 定义临时数据保存路径 evaluation_strategy='steps', # 定义测试执⾏的策略,可取值为no、epoch、steps eval_steps=30, # 定义每隔多少个step执⾏⼀次测试 save_strategy='steps', # 定义模型保存策略,可取值为no、epoch、steps save_steps=20, # 定义每隔多少个step保存⼀次 num_train_epochs=1, # 定义共训练几个轮次 learning_rate=1e-4, # 定义学习率 weight_decay=1e-2, # 加⼊参数权重衰减,防⽌过拟合 per_device_eval_batch_size=16, # 定义测试和训练时的批次⼤小 per_device_train_batch_size=16, no_CUDA=True, # 定义是否要使⽤GPU训练 )
from transformers import Trainer
from transformers.data.data_collator import DataCollatorWithPadding
# 定义训练器:需要传递要训练的模型、超参数对象、训练和验证数据集、评价函数,以及数据整理函数
trainer = Trainer(
model=model,
args=args,
train_dataset=dataset['train'],
eval_dataset=dataset['test'],
compute_metrics=compute_metrics,
data_collator=DataCollatorWithPadding(tokenizer),
)
HuggingFace
提供的 DataCollatorWithPadding
对象,它能把⼀个长度取决于这个批次中最长的句⼦有多长
# 测试数据整理函数 data_collator = DataCollatorWithPadding(tokenizer) data = dataset['train'][:5] # 获取⼀批数据 # 输出这些句⼦的长度 for i in data['input_ids']: print(len(i)) data = data_collator(data) # 调⽤数据整理函数 # 查看整理后的数据 for k, v in data.items(): print(k, v.shape) 62 34 185 101 40 input_ids torch.Size([5, 185]) token_type_ids torch.Size([5, 185]) attention_mask torch.Size([5, 185]) labels torch.Size([5]) # 通过如下代码可以查看数据整理函数是如何对句⼦进⾏补长的 tokenizer.decode(data['input_ids'][0])
# 训练
trainer.train()
# 从某个存档⽂件继续训练
trainer.train(resume_from_checkpoint='./output_dir/checkpoint-90')
# 评价模型
trainer.evaluate()
# ⼿动保存模型参数
trainer.save_model(output_dir='./output_dir/save_model')
# ⼿动加载模型参数
import torch
model.load_state_dict(torch.load('./output_dir/save_model/PyTorch_model.bin'))
model.eval()
# 从测试数据集中获取1个批次的数据⽤于预测
for i, data in enumerate(trainer.get_eval_dataloader()):
break
out = model(**data)
out = out['logits'].argmax(dim=1)
for i in range(8):
print(tokenizer.decode(data['input_ids'][i], skip_special_tokens =True))
print('label=', data['labels'][i].item())
print('predict=', out[i].item())
AdamW
优化器比 Adam
效果要好上万
)也容易出现梯度消失的问题,所以在下游任务的输出时不能使⽤Softmax使用两段式训练;它是⼀种训练技巧,指先
单独对下游任务模型
进⾏⼀定的训练,待下游任务模型掌握了⼀定的知识以后,再连同预训练模型和下游任务
模型⼀起进⾏训练的模式。
- 可以把这个过程想象为⼀条流水线上的两个⼯作,上游的是熟练⼯,下游的是⽣疏⼯⼈。⼀开始生疏的⼯⼈没有任何知识,当⽣产出错时,我们就会要求⽣疏的⼯⼈改进⼯作⽅法,而不会怀疑熟练⼯的⼯作⽅法。
- 在这个阶段如果要求熟练⼯⼈改进,则反而会导致他怀疑以往积累的知识是否是正确的,他会为了配合糟糕的⽣疏⼯⼈而错误地修改⾃己的⽣产⽅法,这显然并不是我们想要的。
- 所以应该先训练⽣疏⼯⼈,把⽣疏⼯⼈训练成⼀个半熟练的⼯⼈,此时⽣产的正确率已经难以上升,再让两个⼯⼈共同训练,以优化⽣产的正确率,这就是
两段式训练的思想
。
上下⽂词嵌⼊、多头注意⼒机制、位置编码、并⾏体系结构、模型压缩、迁移学习、跨语⾔模型
等。编码器-解码器
”体系结构,并持续发展到今天。现在,我们在⽂献中看到了这种体系结构的新的成功变体。⽬前研究已经发现了 只使⽤ Transformer 架构中编码器部分(自编码 BERT-like) 的出⾊模型,如 BERT(BidirectionalEncoder Representations from Transformers
,Transformers 双向编码表⽰
);或者 只使⽤ Transformer 架构中解码器部分(自回归 GPT-like) 的出⾊模型,如 GPT(Generated Pre-trained Transformer,⽣成式的预训练 Transformer
);以及 BART/T5-like
(也被称作序列到序列
的 Transformer模型)
- 在Transformer被提出之前,普遍使⽤的⽂本特征抽取层是RNN,RNN的缺点是能表达的⽂本复杂度很有限,尤其
针对长⽂本的处理能⼒更差
,虽然在 LSTM 和 GRU 模型被提出后 RNN 的这个缺点在很⼤程度上被弥补了,但依然没有得到彻底解决。- Transformer使⽤注意⼒模型抽取⽂本特征,很好地解决了RNN的两个缺点,Transformer的注意⼒模型就是要找
出词与词之间的相互对应关系
,所以对长⽂本有较好的处理能⼒,Transformer的计算过程是可并⾏
的,效率⽐RNN要⾼很多Transformer
内部有⼀个编码器和⼀个解码器。编码器负责读取原⽂
,从原⽂中抽取特征
后交给解码器;解码器负责⽣成译⽂
- 编码器和解码器的内部都是多层结构,图中画出的是3层,实际情况中可能多于这个数字。编码器在计算时,多层编码器是前后串⾏的结构,最后⼀层抽取的⽂本特征作为最终的⽂本特征;解码器同样是前后串⾏的结构,每次的计算输⼊除了
前⼀层的计算输出
,还包括了编码器抽取的⽂本特征
通⽤的 backbone
使⽤,即提取⽂本的特征向量
,有了特征向量后就可以接⼊各种各样的下游任务
,包括翻译任务、分类任务、回归任务等
- 传统的
Greedy Search
具有很大的限制(会出现大量的重复),表现并不好,通常不会在实际场景中使用。Beam Search
的生成结果更加确定,并且更接近整体概率最高的序列。但是有可能生成循环的文本
。当然也有研究指出这一问题与模型的训练方式关系更大。它主要应用于 机器翻译和文本摘要 等需要确定性结果
的任务中。Top-K Sampling
和Top-P Sampling
可以生成更具随机性和更具创意的结果,并且可调节的范围很大。Top-P Sampling
的适应性更强,但是也意味这它的性能会弱于使用固定k
进行筛选的Top-K Sampling
。他们通常应用在 对话系统或者故事生成 等更需要创意
的任务中。
Greedy Search
是指每一步解码都选取可能性最高的单词(i.e. argmax
),把选取的单词补充到 input
中再继续下一步解码直到产生[EOS]
或者达到了事先定义的最大生成长度
后停止解码。它的缺点也很明显:
Beam Search
是一种启发式图搜索算法,具有更大的搜索空间,可以减少遗漏隐藏在低概率单词后面的高概率单词的可能性,他会在每步保持最可能的 num_beams
个 hypotheses
,最后选出整体概率最高或者平均得分最大(除以各自的 token 数)的 hypotheses
。下面以 num_beams=2
为例:
两个 beam
分别选择了『起』和『框』。这时我们发现『飞机起』这一序列的概率为 0.2
,而『飞桨框』序列的概率为 0.32
。我们找到了整体概率更高的序列。在我们这个示例中继续解下去,得到的最终结果为『飞桨框架』Greedy Search
,Beam Search
几乎总能找到整体概率更高的结果。当然由于它的搜索空间也不是无限的,它难以找到所谓的最优解Beam Search
缺点:
n-grams
(即连续 n 个词的词序列) 惩罚。最常见的 n-grams
惩罚是确保每个 n-gram
都只出现一次,方法是如果看到当前候选词与其上文所组成的 n-gram
已经出现过了,就将该候选词的概率设置为 0
。我们可以通过设置 no_repeat_ngram_size=2
来试试,这样任意 2-gram
不会出现两次。但是惩罚太高,生成的文章会不达意,惩罚太少,容易出现大量循环的句子Sampling 简介:
temperature
的本质是降低了采样随机性,该值越小随机性越低,当 temperature=0
时,解码的效果就等同与 Greedy Search
了
temperature
变大时,模型在生成文本时更倾向于比较少见的词汇。越大,重新缩放后的分布就越接近均匀采样(让大概率和小概率之间差别没那么明显)。temperature
变小时,模型在生成文本时更倾向于常见的词。越大,重新缩放后的分布就越接近我们最开始提到的贪婪生成方法(即总是去选择概率最高的那个词,让概率大的更大、让小的变的更小)文本序列的概率分布:
temperature
外,还有一个更简单更常用的方法可以避免生成离谱的结果,这就是 Top-K Sampling。Top-K Sampling 的原理如下图所示,可以看到,Top-K Sampling
就是每一步取条件概率前 k 大(这里为 5)
的结果,将他们的概率重新归一化
后再进行采样,这样做是希望在 “得分高
” 和 “多样性
” 方面做一个折中。显然,当 k=1
时,其实就等价于贪心搜索。加大 k
会产生更多样化、有风险的结果,减小 k
则会产生更通用、安全的结果k 值
在整个解码中是固定
的,所以在所有词的概率分布比较均匀时,Top-K 会过滤掉很多合理的词,而在概率分布非常不平均时(比如前一两个词占据了绝大部分概率),Top-K 又会将一些不合理的词纳入选择Top-P Sampling
可以根据每步的概率分布动态调整采样范围。原理如下图所示,可以看到,p
代表采样的阈值,每一步只保留概率最高(sorted
)的且概率和刚好超过 p
的若干个 token
,下图第一步保留了 6 个;第二部保留了 3 个Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。