学习路径:自然语言处理怎么最快入门? - 知乎
在NLP领域, HMM用来解决文本序列标注问题. 如分词, 词性标注, 命名实体识别都可以看作是序列标注问题。一般以文本序列数据为输入, 以该序列对应的隐含序列为输出。HMM属于生成模型,是有向图。
1. HMM的基本定义: HMM是用于描述由隐藏的状态序列和显性的观测序列组合而成的双重随机过程
2. HMM的假设
3. HMM的应用目的:通过可观测到的数据,预测不可观测到的数据。标注任务中,状态值对应着标记,任务会给定观测序列,以预测其对应的标记序列。
4. 使用HMM的三个问题:
HMM三个参数:A:状态转移概率矩阵。表征转移概率,维度为N*N。B:观测概率矩阵。表征发射概率,维度为N*M。π:初始状态概率向量。维度为N*1。
HMM两种学习方式:如果训练数据包含观测序列和状态序列则为有监督(通过统计方法,求得模型的状态转移概率矩阵A、观测概率矩阵B、初始状态概率向量π),
如果训练数据只包含观测序列则为无监督(模型使用Baum-Welch算法来求得模型参数)
CRF(Conditional Random Fields), 中文称作条件随机场, 同HMM一样, 它一般也以文本序列数据为输入, 以该序列对应的隐含序列为输出。CRF用来解决文本序列标注问题. 如分词, 词性标注, 命名实体识别.
学习训练问题:CRF模型采用正则化的极大似然估计最大化概率。
预测解码问题:和HMM完全一样,采用维特比算法进行预测解码
命名实体识别任务要在特征编码层(如RNN、CNN、BERT等 )后接CRF:
双向的LSTM后面接softmax,但此时输出标签之间是没有关系的,加了CRF后,可以建立起输出标签之间的关联关系。
多种多样的NLP模型
BERT:处理各式NLP 任务的通用架构
NLP领域非常流行的两阶段迁移学习:
预训练BERT时让它同时进行两个任务:
BERT使用当初Google NMT提出的WordPiece Tokenization,将本来的words拆成更小粒度的wordpieces,有效处理不在字典里头的词汇(OOV)。中文的话大致上就像是character-level tokenization,而有##前缀的tokens即为wordpieces。
以词汇fragment来说,其可以被拆成frag与##ment两个pieces,而一个word也可以独自形成一个wordpiece。wordpieces可以由搜集大量文本并找出其中常见的pattern取得。
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained('bert-base-cased')
text = "[CLS] 等到潮水 [MASK] 了,就知道谁沒穿裤子。"
tokens = tokenizer.tokenize(text) #中文断句
ids = tokenizer.convert_tokens_to_ids(tokens) #转换为文本id
除了一般的wordpieces 以外,BERT 里头有5 个特殊tokens 各司其职:
[CLS]
:在做分类任务时其最后一层的repr. 会被视为整个输入序列的repr.[SEP]
:有两个句子的文本会被串接成一个输入序列,并在两句之间插入这个token 以做区隔[UNK]
:没出现在BERT 字典里头的字会被这个 token 取代[PAD]
:zero padding 遮罩,将长度不一的输入序列补齐方便做batch 运算[MASK
]:未知遮罩,仅在预训练阶段会用到transformers库设计:
- Pretraining Head
- Casual Language Modeling(普通自回归的语言模型):GPT, GPT-2,CTRL
- Masked Language Modeling(掩码语言模型):BERT, RoBERTa
- Permuted Language Modeling(乱序重排语言模型):XLNet
- Fine-tuning Head
- Language Modeling:语言模型训练,预测下一个词。主要用于文本生成
- Sequence Classification:文本分类任务,情感分析任务
- Question Answering:机器阅读理解任务,QA
- Token Classification:token级别的分类,主要用于命名实体识别(NER)任务,句法解析Tagging任务
- Multiple Choice:多选任务,主要是文本选择任务
- Masked LM:掩码预测,随机mask一个token,预测该 token 是什么词,用于预训练
- Conditional Generation:条件生成任务,主要用于翻译以及摘要任务。
#导入模型和对应分词器
from transformers import BertModel, BertTokenizer, CTRLModel, CTRLTokenizer, TransfoXLModel, TransfoXLTokenizer, XLNetModel, XLNetTokenizer, XLMModel, XLMTokenizer, DistilBertModel, DistilBertTokenizer, RobertaModel, RobertaTokenizer
from transformers import AutoTokenizer, AutoModelForMaskedLM
#载入预训练数据
tokenizer = AutoTokenizer.from_pretrained("bert-base-chinese")
model = AutoModelForMaskedLM.from_pretrained("bert-base-chinese")
vocab = tokenizer.vocab #字典大小
#进行下游的文本分类任务
from transformers import AutoModelForSequenceClassification
model = AutoModelForSequenceClassification.from_pretrained(checkpoint, num_labels=2
Tokenizer 返回的是一个字典,里面的列表包含了int类别的数据。
encoded_input = tokenizer("Hello, I'm a single sentence!")
print(encoded_input)
# {'input_ids': [101, 138, 18696, 155, 1942, 3190, 1144, 1572, 13745, 1104, 159, 9664, 2107, 102],
# 'token_type_ids': [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
# 'attention_mask': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]}
tokenizer解码:
tokenizer.decode(encoded_input["input_ids"])
# "[CLS] Hello, I'm a single sentence! [SEP]"
[CLS]字符就是大多数预训练语言模型会自动加入的特殊token。tokenizer会自动添加了模型期望的一些特殊token。可以通过传递add_special_tokens = False来禁用加入特殊token
tokenizer也可输入列表:
batch_sentences = ["Hello I'm a single sentence",
"And another sentence",
"And the very very last one"]
encoded_inputs = tokenizer(batch_sentences)
print(encoded_inputs)
# {'input_ids': [[101, 8667, 146, 112, 182, 170, 1423, 5650, 102],
# [101, 1262, 1330, 5650, 102],
# [101, 1262, 1103, 1304, 1304, 1314, 1141, 102]],
# 'token_type_ids': [[0, 0, 0, 0, 0, 0, 0, 0, 0],
# [0, 0, 0, 0, 0],
# [0, 0, 0, 0, 0, 0, 0, 0]],
# 'attention_mask': [[1, 1, 1, 1, 1, 1, 1, 1, 1],
# [1, 1, 1, 1, 1],
# [1, 1, 1, 1, 1, 1, 1, 1]]}
一个batch中的序列长度不一致时,
三个参数padding
,truncation
和max_length
1. encode仅返回input_ids
2. encode_plus返回所有的编码信息,具体如下:
’input_ids:是单词在词典中的编码
‘token_type_ids’:区分两个句子的编码(上句全为0,下句全为1)
‘attention_mask’:指定对哪些词进行self-Attention操作
import torch
from transformers import BertTokenizer
model_name = 'bert-base-uncased'
# a.通过词典导入分词器
tokenizer = BertTokenizer.from_pretrained(model_name)
sentence = "Hello, my son is laughing."
print(tokenizer.encode(sentence))
print(tokenizer.encode_plus(sentence))
#[101, 7592, 1010, 2026, 2365, 2003, 5870, 1012, 102]
#{'input_ids': [101, 7592, 1010, 2026, 2365, 2003, 5870, 1012, 102],
#'token_type_ids': [0, 0, 0, 0, 0, 0, 0, 0, 0],
#'attention_mask': [1, 1, 1, 1, 1, 1, 1, 1, 1]}
from transformers import get_cosine_schedule_with_warmup,
get_linear_schedule_with_warmup
sch = get_linear_schedule_with_warmup(
optimizer, #优化器 包含lr参数
num_warmup_steps= 0, #预热阶段的步骤数
num_training_steps, #训练的总步骤数
last_epoch=-1 #恢复训练时最后一个epoch的索引
)
对抗训练有两个作用,一是提高模型对恶意攻击的鲁棒性,二是提高模型的泛化能力。
对抗训练旨在对原始输入样本 x 上施加扰动 r_adv,得到对抗样本后用其进行训练:
这个被构造出来的“对抗样本”并不能具体对应到某个单词,因此,反过来在推理阶段是没有办法通过修改原始输入得到这样的对抗样本。
常用的几种对抗训练方法有FGSM、FGM、PGD、FreeAT、YOPO、FreeLB、SMART。
import torch
class FGM():
def __init__(self, model):
self.model = model
self.backup = {} # 用于保存模型扰动前的参数
def attack(self, epsilon=1.,
emb_name='word_embeddings' # emb_name表示模型中embedding的参数名
):
'''
生成扰动和对抗样本
'''
for name, param in self.model.named_parameters(): # 遍历模型的所有参数
if param.requires_grad and emb_name in name: # 只取word embedding层的参数
self.backup[name] = param.data.clone() # 保存参数值
norm = torch.norm(param.grad) # 对参数梯度进行二范式归一化
if norm != 0 and not torch.isnan(norm): # 计算扰动,并在输入参数值上添加扰动
r_at = epsilon * param.grad / norm
param.data.add_(r_at)
def restore(self,
emb_name='word_embeddings' # emb_name表示模型中embedding的参数名
):
'''
恢复添加扰动的参数
'''
for name, param in self.model.named_parameters(): # 遍历模型的所有参数
if param.requires_grad and emb_name in name: # 只取word embedding层的参数
assert name in self.backup
param.data = self.backup[name] # 重新加载保存的参数值
self.backup = {}
训练的时候再添加五行:
fgm = FGM(model) # (#1)初始化
for batch_input, batch_label in data:
loss = model(batch_input, batch_label) # 正常训练
loss.backward() # 反向传播,得到正常的grad
# 对抗训练
fgm.attack() # (#2)在embedding上添加对抗扰动
loss_adv = model(batch_input, batch_label) # (#3)计算含有扰动的对抗样本的loss
loss_adv.backward() # (#4)反向传播,并在正常的grad基础上,累加对抗训练的梯度
fgm.restore() # (#5)恢复embedding参数
# 梯度下降,更新参数
optimizer.step()
model.zero_grad()
Project Gradient Descent(PGD)是一种迭代攻击算法,相比于普通的FGM 仅做一次迭代,PGD是做多次迭代,每次走一小步,每次迭代都会将扰动投射到规定范围内。形式化描述为:
扰动约束空间为一个球体,原始的输入样本对应的初识点为球心,迭代多次后,避免扰动超过球面。
import torch
class PGD():
def __init__(self, model):
self.model = model
self.emb_backup = {}
self.grad_backup = {}
def attack(self, epsilon=1., alpha=0.3, emb_name='word_embeddings', is_first_attack=False):
for name, param in self.model.named_parameters():
if param.requires_grad and emb_name in name:
if is_first_attack:
self.emb_backup[name] = param.data.clone()
norm = torch.norm(param.grad)
if norm != 0 and not torch.isnan(norm):
r_at = alpha * param.grad / norm
param.data.add_(r_at)
param.data = self.project(name, param.data, epsilon)
def restore(self, emb_name='word_embeddings'):
for name, param in self.model.named_parameters():
if param.requires_grad and emb_name in name:
assert name in self.emb_backup
param.data = self.emb_backup[name]
self.emb_backup = {}
def project(self, param_name, param_data, epsilon):
r = param_data - self.emb_backup[param_name]
if torch.norm(r) > epsilon:
r = epsilon * r / torch.norm(r)
return self.emb_backup[param_name] + r
def backup_grad(self):
for name, param in self.model.named_parameters():
if param.requires_grad:
self.grad_backup[name] = param.grad.clone()
def restore_grad(self):
for name, param in self.model.named_parameters():
if param.requires_grad:
param.grad = self.grad_backup[name]
训练的时候添加:
pgd = PGD(model)
K = 3
for batch_input, batch_label in data:
# 正常训练
loss = model(batch_input, batch_label)
loss.backward() # 反向传播,得到正常的grad
pgd.backup_grad()
# 累积多次对抗训练——每次生成对抗样本后,进行一次对抗训练,并不断累积梯度
for t in range(K):
pgd.attack(is_first_attack=(t==0)) # 在embedding上添加对抗扰动, first attack时备份param.data
if t != K-1:
model.zero_grad()
else:
pgd.restore_grad()
loss_adv = model(batch_input, batch_label)
loss_adv.backward() # 反向传播,并在正常的grad基础上,累加对抗训练的梯度
pgd.restore() # 恢复embedding参数
# 梯度下降,更新参数
optimizer.step()
model.zero_grad()
抽取一个随机标准正态扰动,加到embedding上,并用KL散度计算扰动的梯度,然后用得到的梯度,计算对抗扰动,并进行对抗训练,实现方法跟FGM差不多。特别提到的一点是,因为其思路也有额外的一致性损失的loss,因此可以用于半监督学习,在无监督数据集合上计算一致性的loss。
EMA在深度学习的优化过程中,是t时刻的模型权重weights,是t时刻的影子权重(shadow weights)。在梯度下降的过程中,会一直维护着这个影子权重,但是这个影子权重并不会参与训练。基本的假设是,模型权重在最后的n步内,会在实际的最优点处抖动,所以我们取最后n步的平均,能使得模型更加的鲁棒。
EMA对第i步的梯度下降的步长增加了权重系数 ,相当于做了一个learning rate decay。
在保存模型或者评估模型时,会利用影子权重进行评估,如果效果比当前效果好,则保存影子权重的参数,但是之后在继续训练的时候会还原之前的参数进行训练。
class EMA():
def __init__(self, model, decay):
self.model = model
self.decay = decay
self.shadow = {}
self.backup = {}
def register(self):
for name, param in self.model.named_parameters():
if param.requires_grad:
self.shadow[name] = param.data.clone()
def update(self):
for name, param in self.model.named_parameters():
if param.requires_grad:
assert name in self.shadow
new_average = (1.0 - self.decay) * param.data + self.decay * self.shadow[name]
self.shadow[name] = new_average.clone()
def apply_shadow(self):
for name, param in self.model.named_parameters():
if param.requires_grad:
assert name in self.shadow
self.backup[name] = param.data
param.data = self.shadow[name]
def restore(self):
for name, param in self.model.named_parameters():
if param.requires_grad:
assert name in self.backup
param.data = self.backup[name]
self.backup = {}
# 初始化
ema = EMA(model, 0.999)
ema.register()
# 训练过程中,更新完参数后,同步update shadow weights
def train():
optimizer.step()
ema.update()
# eval前,apply shadow weights;eval之后,恢复原来模型的参数
def evaluate():
ema.apply_shadow()
# evaluate
ema.restore()
在正常的监督学习损失外,增加一个一致性损失((一般是kl散度)),也有多drop层这种
Kaggle 专利匹配比赛赛后总结
杂文:NLP竞赛&竞赛tricks整理 - 知乎
新手入门 Kaggle NLP类比赛总结 - 知乎
SWA, Apex AMP & Interpreting Transformers in Torch | Kaggle
Utilizing Transformer Representations Efficiently | Kaggle
NLP比赛中有哪些常用的Trick? - 墨天轮
tokens : [CLS] [CAT=CULTURE] question [SEP] answer [SEP]
input_ids : 101 1 3322 102 9987 102
segment_ids: 0 0 0 0 1 1
emb = nn.Embedding(10, 32) # 初始化一个 Embedding 层
meta_vector = emb(cat) # 将类别编码成 vector
logits = torch.cat([txt_vector, meta_vector], dim=-1) # 文本向量和类别向量融合
1. 训练集上训练得到 model1;
2. 使用 model1 在测试集上做预测得到有伪标签的测试集;
3. 使用训练集+带伪标签的测试集训练得最终模型 model2;
伪标签数据可以作为训练数据而被加入到训练集中,是因为神经网络模型有一定的容错能力。需要注意的是伪标签数据质量可能会很差,在使用过程中要多加小心,比如不要用在 validation set 中。
Transformers 三种输出形式:
- pooler output (batch size, hidden size) : 句嵌入,即CLS token的embedding
- last hidden state (batch size, seq Len, hidden size) 最后隐藏层
- hidden states (n layers, batch size, seq Len, hidden size) - 所有的隐藏层
Weighted Layer Pooling
5. 随机权重平均(SWA)
SWA通过对训练过程中多个时间点的模型权重(checkpoint)求平均达到集成的效果,并且该方法不会为训练增加额外的消耗,也不会增加计算量,同时该方法还可以嵌入到Pytorch中的任何优化器类中。
Stochastic Weight Averaging in PyTorch | PyTorch (即插即用?)
6. Multi-Sample Dropout
可以看做是对传统dropout的一种改进,同一样本经过多次dropout, 由于dropout具有随机性,可以得到多个不同的样本。基于连续的dropout,加快模型收敛,提升泛化能力。
7. 分层学习率
def get_grouped_params(args, model):
no_decay = ["bias", "LayerNorm.weight"]
group1 = ['layer.0.', 'layer.1.', 'layer.2.', 'layer.3.']
group2 = ['layer.4.', 'layer.5.', 'layer.6.', 'layer.7.']
group3 = ['layer.8.', 'layer.9.', 'layer.10.', 'layer.11.']
group_all = ['layer.0.', 'layer.1.', 'layer.2.', 'layer.3.', 'layer.4.', 'layer.5.', 'layer.6.', 'layer.7.','layer.8.', 'layer.9.', 'layer.10.', 'layer.11.']
optimizer_grouped_parameters = [
{'params': [p for n, p in model.bert_named_params() if not any(nd in n for nd in no_decay) and not any(nd in n for nd in group_all)],'weight_decay': args.weight_decay},
{'params': [p for n, p in model.bert_named_params() if not any(nd in n for nd in no_decay) and any(nd in n for nd in group1)],'weight_decay': args.weight_decay, 'lr': args.bert_lr 2},
{'params': [p for n, p in model.bert_named_params() if
not any(nd in n for nd in no_decay) and any(nd in n for nd in group2)],
'weight_decay': args.weight_decay, 'lr': args.bert_lr},
{'params': [p for n, p in model.bert_named_params() if
not any(nd in n for nd in no_decay) and any(nd in n for nd in group3)],
'weight_decay': args.weight_decay, 'lr': args.bert_lr * 2},
{'params': [p for n, p in model.bert_named_params() if
any(nd in n for nd in no_decay) and not any(nd in n for nd in group_all)], 'weight_decay': 0.0},
{'params': [p for n, p in model.bert_named_params() if
any(nd in n for nd in no_decay) and any(nd in n for nd in group1)], 'weight_decay': 0.0,
'lr': args.bert_lr 2},
{'params': [p for n, p in model.bert_named_params() if
any(nd in n for nd in no_decay) and any(nd in n for nd in group2)], 'weight_decay': 0.0,
'lr': args.bert_lr},
{'params': [p for n, p in model.bert_named_params() if
any(nd in n for nd in no_decay) and any(nd in n for nd in group3)], 'weight_decay': 0.0,
'lr': args.bert_lr * 2},
{'params': [p for n, p in model.base_named_params()], 'lr': args.learning_rate, "weight_decay": args.weight_decay}]
return optimizer_grouped_parameters
optimizer_bert_parameters = get_grouped_params(args, model)
optimizer = torch.optim.AdamW(optimizer_bert_parameters, lr=args.bert_lr, eps=args.eps)
Transformers 优化方法汇总 - 知乎
NLP硬核入门-隐马尔科夫模型HMM - 知乎
NLP几种常用的对抗训练方法_华师数据学院·王嘉宁的博客-CSDN博客_nlp对抗训练
NLP炼丹技巧合集