• 主页
  • 算法
  • 小桥流水人家
  • python
  • linux
  • mysql
所有文章 友链 关于我

  • 主页
  • 算法
  • 小桥流水人家
  • python
  • linux
  • mysql

self-attention解析

2021-12-24
  • 算法

展开全文 >>

静态词向量之word2vec-cbow

2021-12-24

介绍

如果你看懂了skipgram和cbow的区别,那么实现上面就很简单了。skipgram是中心词预测周围词,cbow是周围词预测中心词,即dataset那里更换下input和target即可。

具体就不细讲了,大家看源码吧~。

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92

# Defined in Section 5.2.3.1

import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torch.utils.data import Dataset
from torch.nn.utils.rnn import pad_sequence
from tqdm.auto import tqdm
from utils import BOS_TOKEN, EOS_TOKEN, PAD_TOKEN
from utils import load_reuters, save_pretrained, get_loader, init_weights

class CbowDataset(Dataset):
def __init__(self, corpus, vocab, context_size=2):
self.data = []
self.bos = vocab[BOS_TOKEN]
self.eos = vocab[EOS_TOKEN]
for sentence in tqdm(corpus, desc="Dataset Construction"):
sentence = [self.bos] + sentence+ [self.eos]
if len(sentence) < context_size * 2 + 1:
continue
for i in range(context_size, len(sentence) - context_size):
# 模型输入:左右分别取context_size长度的上下文
context = sentence[i-context_size:i] + sentence[i+1:i+context_size+1]
# 模型输出:当前词
target = sentence[i]
self.data.append((context, target))

def __len__(self):
return len(self.data)

def __getitem__(self, i):
return self.data[i]

def collate_fn(self, examples):
inputs = torch.tensor([ex[0] for ex in examples])
targets = torch.tensor([ex[1] for ex in examples])
return (inputs, targets)

class CbowModel(nn.Module):
def __init__(self, vocab_size, embedding_dim):
super(CbowModel, self).__init__()
# 词嵌入层
self.embeddings = nn.Embedding(vocab_size, embedding_dim)
# 线性变换:隐含层->输出层
self.output = nn.Linear(embedding_dim, vocab_size)
init_weights(self)

def forward(self, inputs):
embeds = self.embeddings(inputs)
# 计算隐含层:对上下文词向量求平均
hidden = embeds.mean(dim=1)
output = self.output(hidden)
log_probs = F.log_softmax(output, dim=1)
return log_probs

embedding_dim = 64
context_size = 2
hidden_dim = 128
batch_size = 1024
num_epoch = 10

# 读取文本数据,构建CBOW模型训练数据集
corpus, vocab = load_reuters()
dataset = CbowDataset(corpus, vocab, context_size=context_size)
data_loader = get_loader(dataset, batch_size)

nll_loss = nn.NLLLoss()
# 构建CBOW模型,并加载至device
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = CbowModel(len(vocab), embedding_dim)
model.to(device)
optimizer = optim.Adam(model.parameters(), lr=0.001)

model.train()
for epoch in range(num_epoch):
total_loss = 0
for batch in tqdm(data_loader, desc=f"Training Epoch {epoch}"):
inputs, targets = [x.to(device) for x in batch]
optimizer.zero_grad()
log_probs = model(inputs)
loss = nll_loss(log_probs, targets)
loss.backward()
optimizer.step()
total_loss += loss.item()
print(f"Loss: {total_loss:.2f}")

# 保存词向量(model.embeddings)
save_pretrained(vocab, model.embeddings.weight.data, "cbow.vec")


  • 算法

展开全文 >>

静态词向量之FFNN训练词向量

2021-12-24

介绍

本文分享几个好玩的知识点:

  1. 前馈神经网络
  2. bag of words(词袋)
  3. 使用ffnn获取词向量

前馈神经网络

什么叫前馈神经网络呢,emmm,自个去看百度百科定义前馈神经网络。简单来说,就是两个linear加一个激活函数,简单结构如下:

1
2
3
4
5
6
7
8
class FFNN(nn.Module):
def __init__(self):
self.linear1 = nn.Linear()
self.active_func = F.relu
self.linear2 = nn.Linear()

def forward(self, x):
return self.linear2(self.active_func(self.linear1(x)))

其中大名鼎鼎的transformer中也用到了FFNN,所以要认真对待每一种结构哦。

bag of words(词袋)

啥叫词袋呢,emmmm,这个咋解释呢?就是从一堆词取context_size大小的词回来。它没有顺序,所以叫做词袋。比如unigram, bigram, trigram,ngram,都是属于词袋。

而大名鼎鼎的word2vec也是属于词袋这种的哦!这里画重点。

使用ffnn获取词向量

这里就不难理解了,就是换一种方式来实现词向量的获取方式。我在这两采用了两种方式,第一种是以前面两个词为准,获取当前词,这叫做用过去的词来预测未来的词。嘿嘿,如果脑洞大开点的话,是不是有种transformer encoder的感觉😂😂😂。

1. 使用过去词预测当前词

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
# Defined in Section 5.3.1.2

import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torch.utils.data import Dataset
from tqdm.auto import tqdm

from utils import BOS_TOKEN, EOS_TOKEN
from utils import load_reuters, save_pretrained, get_loader, init_weights


def cal_similar(w):
v = model.embeddings.weight[vocab[w]]
values, indices = torch.mm(model.embeddings.weight, v.view(-1, 1)).topk(dim=0, k=3)
similar_tokens = vocab.convert_ids_to_tokens(indices.view(-1).tolist())
return similar_tokens


def demos():
tokens = ['china', 'august', 'good', 'paris']
for token in tokens:
s = cal_similar(token)
print(f'{token}: {s}')
class NGramDataset(Dataset):
def __init__(self, corpus, vocab, context_size=2):
self.data = []
self.bos = vocab[BOS_TOKEN]
self.eos = vocab[EOS_TOKEN]
for sentence in tqdm(corpus, desc="Dataset Construction"):
# 插入句首句尾符号
sentence = [self.bos] + sentence + [self.eos]
if len(sentence) < context_size:
continue
for i in range(context_size, len(sentence)):
# here,只取之前的词
# 模型输入:长为context_size的上文
context = sentence[i-context_size:i]
# 模型输出:当前词
target = sentence[i]
self.data.append((context, target))

def __len__(self):
return len(self.data)

def __getitem__(self, i):
return self.data[i]

def collate_fn(self, examples):
# 从独立样本集合中构建batch输入输出
inputs = torch.tensor([ex[0] for ex in examples], dtype=torch.long)
targets = torch.tensor([ex[1] for ex in examples], dtype=torch.long)
return (inputs, targets)

class FeedForwardNNLM(nn.Module):
def __init__(self, vocab_size, embedding_dim, context_size, hidden_dim):
super(FeedForwardNNLM, self).__init__()
# 词嵌入层
self.embeddings = nn.Embedding(vocab_size, embedding_dim)
# 线性变换:词嵌入层->隐含层
self.linear1 = nn.Linear(context_size * embedding_dim, hidden_dim)
# 线性变换:隐含层->输出层
self.linear2 = nn.Linear(hidden_dim, vocab_size)
# 使用ReLU激活函数
self.activate = F.relu
init_weights(self)

def forward(self, inputs):
embeds = self.embeddings(inputs).view((inputs.shape[0], -1))
hidden = self.activate(self.linear1(embeds))
output = self.linear2(hidden)
# 根据输出层(logits)计算概率分布并取对数,以便于计算对数似然
# 这里采用PyTorch库的log_softmax实现
log_probs = F.log_softmax(output, dim=1)
return log_probs

embedding_dim = 64
context_size = 2
hidden_dim = 128
batch_size = 1024
num_epoch = 10

# 读取文本数据,构建FFNNLM训练数据集(n-grams)
corpus, vocab = load_reuters()
dataset = NGramDataset(corpus, vocab, context_size)
data_loader = get_loader(dataset, batch_size)

# 负对数似然损失函数
nll_loss = nn.NLLLoss()
# 构建FFNNLM,并加载至device
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = FeedForwardNNLM(len(vocab), embedding_dim, context_size, hidden_dim)
model.to(device)
# 使用Adam优化器
optimizer = optim.Adam(model.parameters(), lr=0.001)

model.train()
total_losses = []
for epoch in range(num_epoch):
total_loss = 0
for batch in tqdm(data_loader, desc=f"Training Epoch {epoch}"):
inputs, targets = [x.to(device) for x in batch]
optimizer.zero_grad()
log_probs = model(inputs)
loss = nll_loss(log_probs, targets)
loss.backward()
optimizer.step()
total_loss += loss.item()
print(f"Loss: {total_loss:.2f}")
total_losses.append(total_loss)
demos()
# 保存词向量(model.embeddings)
save_pretrained(vocab, model.embeddings.weight.data, "ffnnlm.vec")


2. 使用过去和未来的词预测当前词

是不是像cbow~

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124

# Defined in Section 5.3.1.2

import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torch.utils.data import Dataset
from tqdm.auto import tqdm
from utils import BOS_TOKEN, EOS_TOKEN
from utils import load_reuters, save_pretrained, get_loader, init_weights
from torch.optim.lr_scheduler import ExponentialLR
def cal_similar(w):
v = model.embeddings.weight[vocab[w]]
values, indices = torch.mm(model.embeddings.weight, v.view(-1, 1)).topk(dim=0, k=3)
similar_tokens = vocab.convert_ids_to_tokens(indices.view(-1).tolist())
return similar_tokens


def demos():
tokens = ['china', 'august', 'good', 'paris']
for token in tokens:
s = cal_similar(token)
print(f'{token}: {s}')

class NGramDataset(Dataset):
def __init__(self, corpus, vocab, context_size=2):
self.data = []
self.bos = vocab[BOS_TOKEN]
self.eos = vocab[EOS_TOKEN]
for sentence in tqdm(corpus, desc="Dataset Construction"):
# 插入句首句尾符号
sentence = [self.bos] + sentence + [self.eos]
if len(sentence) < context_size:
continue
for i in range(context_size, len(sentence) - context_size):
# 就这里哦
# 模型输入:长为context_size的上文
left_context = sentence[i-context_size:i]
right_context = sentence[i+1: i+context_size + 1]
context = [*left_context, *right_context]
# 模型输出:当前词
target = sentence[i]
self.data.append((context, target))

def __len__(self):
return len(self.data)

def __getitem__(self, i):
return self.data[i]

def collate_fn(self, examples):
# 从独立样本集合中构建batch输入输出
inputs = torch.tensor([ex[0] for ex in examples], dtype=torch.long)
targets = torch.tensor([ex[1] for ex in examples], dtype=torch.long)
return (inputs, targets)

class FeedForwardNNLM(nn.Module):
def __init__(self, vocab_size, embedding_dim, context_size, hidden_dim):
super(FeedForwardNNLM, self).__init__()
# 词嵌入层
self.embeddings = nn.Embedding(vocab_size, embedding_dim)
# 线性变换:词嵌入层->隐含层
self.linear1 = nn.Linear(context_size * embedding_dim * 2, hidden_dim)
# 线性变换:隐含层->输出层
self.linear2 = nn.Linear(hidden_dim, vocab_size)
# 使用ReLU激活函数
self.activate = F.relu
# init_weights(self)
self.dp = nn.Dropout(0.1)

def forward(self, inputs):
embeds = self.embeddings(inputs).view((inputs.shape[0], -1))
hidden = self.activate(self.linear1(embeds))
output = self.linear2(hidden)
# 根据输出层(logits)计算概率分布并取对数,以便于计算对数似然
# 这里采用PyTorch库的log_softmax实现
# output = self.dp(output)
log_probs = F.log_softmax(output, dim=1)
return log_probs

embedding_dim = 64
context_size = 2
hidden_dim = 128
batch_size = 10240
num_epoch = 10

# 读取文本数据,构建FFNNLM训练数据集(n-grams)
corpus, vocab = load_reuters()
dataset = NGramDataset(corpus, vocab, context_size)
data_loader = get_loader(dataset, batch_size)

# 负对数似然损失函数
nll_loss = nn.NLLLoss()
# 构建FFNNLM,并加载至device
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = FeedForwardNNLM(len(vocab), embedding_dim, context_size, hidden_dim)
model.to(device)
# 使用Adam优化器
optimizer = optim.Adam(model.parameters(), lr=0.01)
scheduler = ExponentialLR(optimizer, gamma=0.9)
model.train()
total_losses = []
for epoch in range(num_epoch):
total_loss = 0
for batch in tqdm(data_loader, desc=f"Training Epoch {epoch}"):
inputs, targets = [x.to(device) for x in batch]
optimizer.zero_grad()
log_probs = model(inputs)
loss = nll_loss(log_probs, targets)
loss.backward()
optimizer.step()

total_loss += loss.item()

print(f"Loss: {total_loss:.2f}, LR: {scheduler.get_last_lr()[0]}")
scheduler.step()
demos()
total_losses.append(total_loss)

# 保存词向量(model.embeddings)
save_pretrained(vocab, model.embeddings.weight.data, "ffnnlm.vec")


总结

这两者之间就以下几点不同:

  1. NGramDataset那里在获取context_size的词时不一样
  2. 训练时linear1的in_feature大小变了。

其余都一样哦,可以自己跑一跑呢。

  • 算法

展开全文 >>

静态词向量之word2vec-skipgram

2021-12-22

介绍

当当当,欢迎来学习word2vec skipgram,关于word2vec,网上介绍的例子一大堆,这里就简单说明下。
最开始进行tokenizer的时候,是使用onehot编码,缺点就是矩阵太大,另外太稀疏,而且词和词之前是不具备语义信息的。
你说什么叫语义?语义没有官方定义,可以简单理解成更符合人类认知的,我觉得就可以理解成语义。

而word2vec带来了稠密向量,并且词和词之间有了语义关联,可以用于计算词和词之间的空间距离,也可以叫做相似度。
实现word2vec的方式有两种,一个是Hierarchical Softmax(也是softmax),另外一种是negative sampling。
下面将分别介绍这两种方式的实现思路。

偷懒群众可以直接看:

  1. Hierarchical Softmax example
  2. negative sampling example
  3. 也可以看我之前的negative实现

Hierarchical Softmax

1. 介绍

啥叫Hierarchical Softmax,嘿嘿,这个我没看😂😂😂。总而言之也是softmax,不过应用场景主要在对大规模语料进行softmax的时候加速计算结果,比如softmax(100w个),那么他的优势就体现出来了。
如果感兴趣其实现原理的可以自行百度,我就葛优躺了😂😂😂。

2. 加载数据

1
2
3
4
5
6
7
8
9
10
11
12
def load_reuters():
nltk.set_proxy('http://192.168.0.28:1080')
nltk.download('reuters')
nltk.download('punkt')
from nltk.corpus import reuters
text = reuters.sents()
# lowercase (optional)
text = [[word.lower() for word in sentence] for sentence in text]
vocab = Vocab.build(text, reserved_tokens=[PAD_TOKEN, BOS_TOKEN, EOS_TOKEN])
corpus = [vocab.convert_tokens_to_ids(sentence) for sentence in text]

return corpus, vocab

其中corpus长下面这个样子:

1
2
3
4
5
6
7
8
corpus[:2]
Out[3]:
[
[4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,9,10,11,10,20,13,21,22,23,24,25,26,27,28,11,29,30,31,19,32,33,34,35,36,37,38,7,39,40,20,41,42,10],
[43,44,45,46,47,4,48,49,9,10,11,10,50,51,13,52,53,54,55,47,19,9,10,11,10,20,56,57,58,59,60,61,26,62,63,10]

]

构建训练数据集的步骤如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class SkipGramDataset(Dataset):
def __init__(self, corpus, vocab, context_size=2):
self.data = []
self.bos = vocab[BOS_TOKEN]
self.eos = vocab[EOS_TOKEN]
for sentence in tqdm(corpus, desc="Dataset Construction"):
sentence = [self.bos] + sentence + [self.eos]
for i in range(1, len(sentence)-1):
# 模型输入:当前词
w = sentence[i]
# 模型输出:一定窗口大小内的上下文
left_context_index = max(0, i - context_size)
right_context_index = min(len(sentence), i + context_size)
context = sentence[left_context_index:i] + sentence[i+1:right_context_index+1]
self.data.extend([(w, c) for c in context])

def __len__(self):
return len(self.data)

def __getitem__(self, i):
return self.data[i]

def collate_fn(self, examples):
inputs = torch.tensor([ex[0] for ex in examples])
targets = torch.tensor([ex[1] for ex in examples])
return (inputs, targets)

看这段代码,其核心在于,取当前词的index,然后以当前词取左右大小为context_size的词出来来作为他的中心词,然后在collate_fn那里就转换成当前词的id,和周围词的id作为其预测目标。

是不是瞬间明白他想干什么事儿了吧!!
没明白是吧,继续往下看。

3. 模型结构

1
2
3
4
5
6
7
8
9
10
11
12
13
class SkipGramModel(nn.Module):
def __init__(self, vocab_size, embedding_dim):
super(SkipGramModel, self).__init__()
self.embeddings = nn.Embedding(vocab_size, embedding_dim)
self.output = nn.Linear(embedding_dim, vocab_size)
init_weights(self)

def forward(self, inputs):
embeds = self.embeddings(inputs)
output = self.output(embeds)
log_probs = F.log_softmax(output, dim=1)
return log_probs

这个结构是不是相当简单,从今天来看,这个结构很简单,另外pytorch已经帮忙做了很多,但是放到实现那年,不得不佩服这些研究者。

4. 训练部分

训练部分就很直白了,每次取batch_size=1024个样本,然后输入模型,获得log_probs=(1024, 31081)结果,其中31081是指vocab_size,然后交叉熵算loss。
到这是不是明白了Hierarchical Softmax而不是softmax的意义了吧。

negative sampling

1. 介绍

当面对百万级的文本,就算是隐藏层是检索功能,其计算量也是相当大,而且还会造成冗余计算,这时候对高频词抽样以及负采样就应运而生了。

他的做法是对文档中出现的每个词计算其出现频率,然后以当前词周围context_size大小的作为postive sampling,和选中的词无关的并且以词出现频率高为negative sampling。分别计算当前词和positive以及negative的相关度,就是直接计算其点积,将其loss最低即完成训练。

2. 数据处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 读取文本数据
corpus, vocab = load_reuters()
# 计算unigram概率分布
unigram_dist = get_unigram_distribution(corpus, len(vocab))
# 根据unigram分布计算负采样分布: p(w)**0.75
negative_sampling_dist = unigram_dist ** 0.75
negative_sampling_dist /= negative_sampling_dist.sum()
# 构建SGNS训练数据集
dataset = SGNSDataset(
corpus,
vocab,
context_size=context_size,
n_negatives=n_negatives,
ns_dist=negative_sampling_dist
)
data_loader = get_loader(dataset, batch_size)

其中get_unigram_distribution是获取每个词的出现概率。每次记不住这个unigram,bigram,还有trigram。。。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class SGNSDataset(Dataset):
def __init__(self, corpus, vocab, context_size=2, n_negatives=5, ns_dist=None):
self.data = []
self.bos = vocab[BOS_TOKEN]
self.eos = vocab[EOS_TOKEN]
self.pad = vocab[PAD_TOKEN]
for sentence in tqdm(corpus, desc="Dataset Construction"):
sentence = [self.bos] + sentence + [self.eos]
for i in range(1, len(sentence)-1):
# 模型输入:(w, context) ;输出为0/1,表示context是否为负样本
w = sentence[i]
left_context_index = max(0, i - context_size)
right_context_index = min(len(sentence), i + context_size)
context = sentence[left_context_index:i] + sentence[i+1:right_context_index+1]
context += [self.pad] * (2 * context_size - len(context))
self.data.append((w, context))

# 负样本数量
self.n_negatives = n_negatives
# 负采样分布:若参数ns_dist为None,则使用uniform分布
self.ns_dist = ns_dist if ns_dist is not None else torch.ones(len(vocab))

def __len__(self):
return len(self.data)

def __getitem__(self, i):
return self.data[i]

def collate_fn(self, examples):
words = torch.tensor([ex[0] for ex in examples], dtype=torch.long)
contexts = torch.tensor([ex[1] for ex in examples], dtype=torch.long)
batch_size, context_size = contexts.shape
neg_contexts = []
# 对batch内的样本分别进行负采样
for i in range(batch_size):
# 保证负样本不包含当前样本中的context
ns_dist = self.ns_dist.index_fill(0, contexts[i], .0)
neg_contexts.append(torch.multinomial(ns_dist, self.n_negatives * context_size, replacement=True))
neg_contexts = torch.stack(neg_contexts, dim=0)
return words, contexts, neg_contexts

这地方有两个地方需要注意的:
第一是获取positive sampling,也就是以当前词周围context_size大小的作为postive sampling,这个在__init__函数中完成。
第二是获取negative sampling,这个是在collate_fn中完成,其中涉及到一个采样函数multinomial,这个大家可以参考这篇文章。

3. 模型结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class SGNSModel(nn.Module):
def __init__(self, vocab_size, embedding_dim):
super(SGNSModel, self).__init__()
# 词嵌入
self.w_embeddings = nn.Embedding(vocab_size, embedding_dim)
# 上下文嵌入
self.c_embeddings = nn.Embedding(vocab_size, embedding_dim)

def forward_w(self, words):
w_embeds = self.w_embeddings(words)
return w_embeds

def forward_c(self, contexts):
c_embeds = self.c_embeddings(contexts)
return c_embeds

4. 训练

这部分可以直接看源代码了,其中在计算负样本的分类(对数)似然地方,我改了下,更方便理解,其实本质没啥~

1
2
3
4
# 负样本的分类(对数)似然
neg_context_loss = F.logsigmoid(torch.bmm(neg_context_embeds, word_embeds).squeeze(dim=2).neg())
# neg_context_loss = neg_context_loss.view(batch_size, -1, n_negatives).sum(dim=2)
neg_context_loss = neg_context_loss.mean(dim=1)

5. 扩展与思考

这部分属于个人猜测部分,不保证其准确性。
抛开所有的理论,单纯看代码思考一个问题,源码这里使用了两个embedding,分别是词嵌入和上下文嵌入,那我们能不能只用一个embedding呢?
那既然如此,我们需要修改下源码,有两个地方需要修改:

  1. 在选择负样本的时候是有可能选择到当前词的,所以我们要将负样本中出现当前词的去掉。
  2. 把c_embeddings去掉。

关于第一点,为什么要在负样本中去掉当前词?
不可能让当前词和当前词在计算相似度时出现悖论吧😂😂😂。

关于第二点,这个没啥好解释的了。

修改后的完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
# Defined in Section 5.2.3.3

import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torch.utils.data import Dataset
from tqdm.auto import tqdm

from utils import BOS_TOKEN, EOS_TOKEN, PAD_TOKEN
from utils import load_reuters, get_loader


def cal_similar(w):
v = model.w_embeddings.weight[vocab[w]]
values, indices = torch.mm(model.w_embeddings.weight, v.view(-1, 1)).topk(dim=0, k=3)
similar_tokens = vocab.convert_ids_to_tokens(indices.view(-1).tolist())
return similar_tokens


def demos():
tokens = ['china', 'august', 'good', 'paris']
for token in tokens:
s = cal_similar(token)
print(f'{token}: {s}')

class SGNSDataset(Dataset):
def __init__(self, corpus, vocab, context_size=2, n_negatives=5, ns_dist=None):
self.data = []
self.bos = vocab[BOS_TOKEN]
self.eos = vocab[EOS_TOKEN]
self.pad = vocab[PAD_TOKEN]
for sentence in tqdm(corpus, desc="Dataset Construction"):
# sentence = [self.bos] + sentence + [self.eos]
for i in range(0, len(sentence)):
# 模型输入:(w, context) ;输出为0/1,表示context是否为负样本
w = sentence[i]
left_context_index = max(0, i - context_size)
right_context_index = min(len(sentence), i + context_size)
context = sentence[left_context_index:i] + sentence[i+1:right_context_index+1]
context += [self.pad] * (2 * context_size - len(context))
self.data.append((w, context))

# 负样本数量
self.n_negatives = n_negatives
# 负采样分布:若参数ns_dist为None,则使用uniform分布
self.ns_dist = ns_dist if ns_dist is not None else torch.ones(len(vocab))

def __len__(self):
return len(self.data)

def __getitem__(self, i):
return self.data[i]

def collate_fn(self, examples):
words = torch.tensor([ex[0] for ex in examples], dtype=torch.long)
contexts = torch.tensor([ex[1] for ex in examples], dtype=torch.long)

all_positive_contexts = torch.cat((words.view(-1, 1), contexts), dim=1)

batch_size, context_size = contexts.shape
neg_contexts = []
# 对batch内的样本分别进行负采样
for i in range(batch_size):
# 保证负样本不包含当前样本中的context
ns_dist = self.ns_dist.index_fill(0, all_positive_contexts[i], .0)
neg_contexts.append(torch.multinomial(ns_dist, self.n_negatives * context_size, replacement=True))
neg_contexts = torch.stack(neg_contexts, dim=0)
for index, word in enumerate(words):
assert word not in neg_contexts[index]
return words, contexts, neg_contexts

class SGNSModel(nn.Module):
def __init__(self, vocab_size, embedding_dim):
super(SGNSModel, self).__init__()
# 词嵌入
self.w_embeddings = nn.Embedding(vocab_size, embedding_dim, padding_idx=vocab[PAD_TOKEN])
# 上下文嵌入
# self.c_embeddings = nn.Embedding(vocab_size, embedding_dim)

def forward_w(self, words):
w_embeds = self.w_embeddings(words)
return w_embeds

def forward_c(self, contexts):
c_embeds = self.w_embeddings(contexts)
return c_embeds


def get_unigram_distribution(corpus, vocab_size):
# 从给定语料中统计unigram概率分布
token_counts = torch.tensor([0] * vocab_size)
total_count = 0
for sentence in corpus:
total_count += len(sentence)
for token in sentence:
token_counts[token] += 1
unigram_dist = torch.div(token_counts.float(), total_count)
return unigram_dist

embedding_dim = 64
context_size = 16
hidden_dim = 128
batch_size = 20480 * 2
num_epoch = 30
n_negatives = 10

# 读取文本数据
corpus, vocab = load_reuters()
# 计算unigram概率分布
unigram_dist = get_unigram_distribution(corpus, len(vocab))
# 根据unigram分布计算负采样分布: p(w)**0.75
negative_sampling_dist = unigram_dist ** 0.75
negative_sampling_dist /= negative_sampling_dist.sum()
# 构建SGNS训练数据集
dataset = SGNSDataset(
corpus,
vocab,
context_size=context_size,
n_negatives=n_negatives,
ns_dist=negative_sampling_dist
)
data_loader = get_loader(dataset, batch_size)

device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = SGNSModel(len(vocab), embedding_dim)

model.to(device)
optimizer = optim.Adam(model.parameters(), lr=0.1)
scheduler = optim.lr_scheduler.ExponentialLR(optimizer, gamma=0.99)

model.train()
for epoch in range(num_epoch):
total_loss = 0
for batch in tqdm(data_loader, desc=f"Training Epoch {epoch}"):
words, contexts, neg_contexts = [x.to(device) for x in batch]
optimizer.zero_grad()
batch_size = words.shape[0]
# 提取batch内词、上下文以及负样本的向量表示
word_embeds = model.forward_w(words).unsqueeze(dim=2)
context_embeds = model.forward_c(contexts)
neg_context_embeds = model.forward_c(neg_contexts)
# 正样本的分类(对数)似然
context_loss = F.logsigmoid(torch.bmm(context_embeds, word_embeds).squeeze(dim=2))
context_loss = context_loss.mean(dim=1)
# 负样本的分类(对数)似然
neg_context_loss = F.logsigmoid(torch.bmm(neg_context_embeds, word_embeds).squeeze(dim=2).neg())
# neg_context_loss = neg_context_loss.view(batch_size, -1, n_negatives).sum(dim=2)
neg_context_loss = neg_context_loss.mean(dim=1)
# 损失:负对数似然
loss = -(context_loss + neg_context_loss).mean()
loss.backward()
optimizer.step()

total_loss += loss.item()
scheduler.step()
print(f"Loss: {total_loss:.2f}, Lr: {scheduler.get_last_lr()[0]}")
demos()

# 合并词嵌入矩阵与上下文嵌入矩阵,作为最终的预训练词向量
# combined_embeds = model.w_embeddings.weight + model.c_embeddings.weight
# save_pretrained(vocab, combined_embeds.data, "sgns.vec")


在训练的过程中,发现其loss是往下不断的降低,那么我们可以认为是有用的,但是具体效果要再自行实验下。

  • 算法

展开全文 >>

transformer使用示例

2021-12-22

关于transformer的一些基础知识,之前在看李宏毅视频的时候总结了一些,可以看here,到写此文章时,也基本忘的差不多了,故也不深究,讲两个关于transformer的基本应用,来方便理解与应用。

序列标注

参考文件transformer_postag.py.

1. 加载数据

1
2
#加载数据
train_data, test_data, vocab, pos_vocab = load_treebank()

其中load_treebank代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def load_treebank():
# 需要翻墙下载,可以自行设置代码
nltk.set_proxy('http://192.168.0.28:1080')
# 如果没有的话那么则会下载,否则忽略
nltk.download('treebank')
from nltk.corpus import treebank

sents, postags = zip(*(zip(*sent) for sent in treebank.tagged_sents()))

vocab = Vocab.build(sents, reserved_tokens=["<pad>"])

tag_vocab = Vocab.build(postags)

train_data = [(vocab.convert_tokens_to_ids(sentence), tag_vocab.convert_tokens_to_ids(tags)) for sentence, tags in zip(sents[:3000], postags[:3000])]
test_data = [(vocab.convert_tokens_to_ids(sentence), tag_vocab.convert_tokens_to_ids(tags)) for sentence, tags in zip(sents[3000:], postags[3000:])]

return train_data, test_data, vocab, tag_vocab


加载后可以看到,train_data和test_data都是list,其中每一个sample都是tuple,分别是input和target。如下:

1
2
3
4
>>> train_data[0]
>>> Out[1]:
([2, 3, 4, 5, 6, 7, 4, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18],
[1, 1, 2, 3, 4, 5, 2, 6, 7, 8, 9, 10, 8, 5, 9, 1, 3, 11])

2. 数据处理

1
2
3
4
5
6
7
8
9
10

# 这个函数就是将其变成等长,填充使用<pad>,至于是0还是1还是其他值并不重要,因为还有mask~
def collate_fn(examples):
lengths = torch.tensor([len(ex[0]) for ex in examples])
inputs = [torch.tensor(ex[0]) for ex in examples]
targets = [torch.tensor(ex[1]) for ex in examples]
inputs = pad_sequence(inputs, batch_first=True, padding_value=vocab["<pad>"])
targets = pad_sequence(targets, batch_first=True, padding_value=vocab["<pad>"])
return inputs, lengths, targets, inputs != vocab["<pad>"]

3. 模型部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class PositionalEncoding(nn.Module):
def __init__(self, d_model, dropout=0.1, max_len=512):
super(PositionalEncoding, self).__init__()

pe = torch.zeros(max_len, d_model)
position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1)
div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model))
pe[:, 0::2] = torch.sin(position * div_term)
pe[:, 1::2] = torch.cos(position * div_term)
pe = pe.unsqueeze(0).transpose(0, 1)
self.register_buffer('pe', pe)

def forward(self, x):
x = x + self.pe[:x.size(0), :]
return x

class Transformer(nn.Module):
def __init__(self, vocab_size, embedding_dim, hidden_dim, num_class,
dim_feedforward=512, num_head=2, num_layers=2, dropout=0.1, max_len=512, activation: str = "relu"):
super(Transformer, self).__init__()
# 词嵌入层
self.embedding_dim = embedding_dim
self.embeddings = nn.Embedding(vocab_size, embedding_dim)
self.position_embedding = PositionalEncoding(embedding_dim, dropout, max_len)
# 编码层:使用Transformer
encoder_layer = nn.TransformerEncoderLayer(hidden_dim, num_head, dim_feedforward, dropout, activation)
self.transformer = nn.TransformerEncoder(encoder_layer, num_layers)
# 输出层
self.output = nn.Linear(hidden_dim, num_class)

def forward(self, inputs, lengths):
inputs = torch.transpose(inputs, 0, 1)
hidden_states = self.embeddings(inputs)
hidden_states = self.position_embedding(hidden_states)
attention_mask = length_to_mask(lengths) == False
hidden_states = self.transformer(hidden_states, src_key_padding_mask=attention_mask).transpose(0, 1)
logits = self.output(hidden_states)
log_probs = F.log_softmax(logits, dim=-1)
return log_probs

这里有几点可能需要注意的:

  • PositionalEncoding

因为self attention是没有像rnn位置信息编码的,所以transformer引入了positional encoding,使用绝对位置进行编码,对每一个输入加上position信息,可以看self.pe,这个一个static lookup table。目前也出现一些使用relative positional encoding的,也就是加入相对位置编码,这个在ner任务中挺常见,比如TENER和Flat-Lattice-Transformer。但是最近google证明这种相对位置编码只是引入了更多的信息特征进来😭😭😭😭。。

扯完上面这个,进入正题,那就是如何计算的。

看forward部分,发现首先进行了torch.transpose操作,然后进行self.position_embedding,这个transpose是否让你感到困惑呢?
如果没有就不用看了😭😭😭。。。

一般输入Embedding的shape是(batch_size, seq_length),然后对每个seq_length那维的token进行编码获取对应的feature。但是这里将其transpose了,变成了(seq_length, batch_size),这种操作是否理解呢?ok,举个例子:

1
2
tensor([[1, 2, 3],
[4, 5, 6]])

这个就是我们通常理解的(batch_size, seq_length),如果将其transpose下就变成了:

1
2
3
4
tensor([[1, 4],
[2, 5],
[3, 6]])

囔,是不是理解了呢,是对position进行embedding,然后接着看PositionalEncoding是如何forward的。

1
2
3
4
5
6
7
8
9
def forward(self, x):
x = x + self.pe[:x.size(0), :]
return x

>>> x.shape
Out[2]: torch.Size([70, 32, 128])
>>> self.pe.shape
Out[3]: torch.Size([512, 1, 128])

那么上述例子就是指获取self.pe前70个长度的位置编码信息,然后和x进行相加返回,从而带入了位置编码信息。

  • TransformerEncoder部分
1
2
3
>>> encoder_layer = nn.TransformerEncoderLayer(d_model=512, nhead=8)
>>> src = torch.rand(10, 32, 512)
>>> out = encoder_layer(src)

这部分就容易理解了,使用多少nhead和TransformerEncoder的num_layers。

句子极性二分类

这地方基本和之前一样,就是linear n_out=2,然后交叉熵算loss就行。
我稍微改动了下源码,这样理解起来会更方便。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class Transformer(nn.Module):
def __init__(self, vocab_size, embedding_dim, hidden_dim, num_class,
dim_feedforward=512, num_head=2, num_layers=2, dropout=0.1, max_len=128, activation: str = "relu"):
super(Transformer, self).__init__()
# 词嵌入层
self.embedding_dim = embedding_dim
self.embeddings = nn.Embedding(vocab_size, embedding_dim)
self.position_embedding = PositionalEncoding(embedding_dim, dropout, max_len)
# 编码层:使用Transformer
encoder_layer = nn.TransformerEncoderLayer(hidden_dim, num_head, dim_feedforward, dropout, activation)
self.transformer = nn.TransformerEncoder(encoder_layer, num_layers)
# 输出层
self.output = nn.Linear(hidden_dim, num_class)


def forward(self, inputs, lengths):
inputs = torch.transpose(inputs, 0, 1)
hidden_states = self.embeddings(inputs)
hidden_states = self.position_embedding(hidden_states)
lengths = lengths.cpu()
attention_mask = length_to_mask(lengths) == False
attention_mask = attention_mask.cuda()
hidden_states = self.transformer(hidden_states, src_key_padding_mask=attention_mask)

# 在这里,因为把seq_length那一维放前面,觉得有点怪怪的,所以这里transpose一下。
hidden_states = hidden_states.transpose(0, 1)
hidden_states = hidden_states[:, 0, :]
output = self.output(hidden_states)
log_probs = F.log_softmax(output, dim=1)
return log_probs

总结

目前nlp都变成了微调时代,关于transformer网络结构,感兴趣可以点击我上面链接,可以看看从代码层面如果实现encoder和decoder部分。

  • 算法

展开全文 >>

coreference resolution

2021-12-17

介绍

共指解析,按照百度的定义如下:

1
众所周知,人们为了避免重复,习惯用代词、称谓和缩略语来指代前面提到的实体全称。例如,在文章开始处会写“哈尔滨工业大学”,后面可能会说“哈工大”、“工大”等,还会提到“这所大学”、“她”等。这种现象称为共指现象。

简而言之,其目的在于自动识别表示同一实体的名词短语或代词等。

举个例子:

哈尔滨工业大学,一般学生或者大众喜欢简称为哈工大,工大等,她是一所美丽的大学。

实体(entity): 应是唯一定义的,并且具有共知的。哈尔滨工业大学即为这句话的实体。
指称(mention): 实体在自然语言文本中的另外一种表达形式,哈工大,工大,她都是指称。
共指(coreference): 如果文本或句子中的两个或多个mention指向同一个entity,那么则称为共指。

到这里可以看出,一个复杂的句子中可能会有多个实体以及对应的多个指称共指于不同的实体,这可以是一个分类任务也可以是一个聚类任务。

根据认知,中文里面能做实体的一般是专有名词,比如清华大学,《海蒂》,各行各业有不同的专有名词。另外就是名词或者名词短语以及代词,比如这人,他,它,她等。

下面介绍一种算法。

Word-Level Coreference Resolution

论文地址: Word-Level Coreference Resolution
代码地址:wl-coref

先说个人感受,这个咋感觉更像是提升速度,topK操作,而没有那么多骚操作来提升效果。代码质量杠杠的,但是不是batch训练,又有点怪怪的~

代码对于训练部分看完了,如果有说的不对的,或者没有涵盖到重点的,非常欢迎指教!

1. 获得word level embedding

  • 获取subtoken输入bert后得到的向量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
def _bertify(self, doc: Doc) -> torch.Tensor:
subwords_batches = bert.get_subwords_batches(doc, self.config,
self.tokenizer)

special_tokens = np.array([self.tokenizer.cls_token_id,
self.tokenizer.sep_token_id,
self.tokenizer.pad_token_id])
subword_mask = ~(np.isin(subwords_batches, special_tokens))

subwords_batches_tensor = torch.tensor(subwords_batches,
device=self.config.device,
dtype=torch.long)
subword_mask_tensor = torch.tensor(subword_mask,
device=self.config.device)

# Obtain bert output for selected batches only
attention_mask = (subwords_batches != self.tokenizer.pad_token_id)
out, _ = self.bert(
subwords_batches_tensor,
attention_mask=torch.tensor(
attention_mask, device=self.config.device))
del _

# [n_subwords, bert_emb]
return out[subword_mask_tensor]

  • 获取word level embedding
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
from typing import Tuple

import torch

from coref.config import Config
from coref.const import Doc


class WordEncoder(torch.nn.Module): # pylint: disable=too-many-instance-attributes
def __init__(self, features: int, config: Config):

super().__init__()
self.attn = torch.nn.Linear(in_features=features, out_features=1)
self.dropout = torch.nn.Dropout(config.dropout_rate)


def forward(self, # type: ignore # pylint: disable=arguments-differ #35566 in pytorch
doc: Doc,
x: torch.Tensor,
) -> Tuple[torch.Tensor, ...]:

word_boundaries = torch.tensor(doc["word2subword"], device=self.device)
# 每个token被tokenizer为subtokens后,starts记录每个token的起始位置
starts = word_boundaries[:, 0]
# ends记录每个token的结束位置
ends = word_boundaries[:, 1]

# [n_mentions, features]
words = self._attn_scores(x, starts, ends).mm(x)

words = self.dropout(words)

return (words, self._cluster_ids(doc))

def _attn_scores(self,
bert_out: torch.Tensor,
word_starts: torch.Tensor,
word_ends: torch.Tensor) -> torch.Tensor:

n_subtokens = len(bert_out)
n_words = len(word_starts)

# [n_mentions, n_subtokens]
# with 0 at positions belonging to the words and -inf elsewhere
# 只有start到end之间的为0,否则为-inf
attn_mask = torch.arange(0, n_subtokens, device=self.device).expand((n_words, n_subtokens))
attn_mask = ((attn_mask >= word_starts.unsqueeze(1))
* (attn_mask < word_ends.unsqueeze(1)))
attn_mask = torch.log(attn_mask.to(torch.float))
# 每一个subtoken被降维为1,比如一个句子有477个subtokens,bert_out为(477, 768),attn_scores就变成了(1,477)
attn_scores = self.attn(bert_out).T # [1, n_subtokens]
attn_scores = attn_scores.expand((n_words, n_subtokens))
attn_scores = attn_mask + attn_scores
del attn_mask
# 做归一化
return torch.softmax(attn_scores, dim=1) # [n_words, n_subtokens]

  • 这部分可以看我最后简单示例,说明实现方式。

假设tokens长度为455, 这里输出就变成了word level embedding。

粗排(rough score)

还是觉得,不考虑batch_size那一维,整个代码都方便理解许多😂😂😂😂😂😂😂😂😂😂。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class RoughScorer(torch.nn.Module):
def __init__(self, features: int, config: Config):
super().__init__()
self.dropout = torch.nn.Dropout(config.dropout_rate)
self.bilinear = torch.nn.Linear(features, features)

self.k = config.rough_k

def forward(self, # type: ignore # pylint: disable=arguments-differ #35566 in pytorch
mentions: torch.Tensor,
) -> Tuple[torch.Tensor, torch.Tensor]:

# 这里一共做了两件事情
# 1. 获取token和token之间的关联矩阵,可用于表示之间的关联程度。
# [n_mentions, n_mentions]
pair_mask = torch.arange(mentions.shape[0])
pair_mask = pair_mask.unsqueeze(1) - pair_mask.unsqueeze(0)
pair_mask = torch.log((pair_mask > 0).to(torch.float))
pair_mask = pair_mask.to(mentions.device) # -- 首先构建掩码矩阵,该矩阵为一个下三角矩阵,含义为每个词只能取该词之前的词作为候选词。

# 但是有啥说啥,我就搞不明白为啥这里还要加一个bilinear,这不有点多此一举么,维度没发生改变,谁能知道给我解答下~
# 不过没人看😂😂😂😂😂😂
bilinear_scores = self.dropout(self.bilinear(mentions)).mm(mentions.T)

rough_scores = pair_mask + bilinear_scores
# 2. 获取每个token的topK tokens。
return self._prune(rough_scores)

def _prune(self,
rough_scores: torch.Tensor
) -> Tuple[torch.Tensor, torch.Tensor]:
# 骚气吧,是不是又get到新操作。不过这个sorted,不管true还是false结果都不变😂😂😂😂😂😂
top_scores, indices = torch.topk(rough_scores,
k=min(self.k, len(rough_scores)),
dim=1, sorted=False)
return top_scores, indices

获取词对特征

看到么看到么,人家到这里才开始干活~

top_indices的维度为(405,50),表示这个句子一共有405个tokens,然后获取每个token最相关联的50个tokens的索引。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
def forward(self,  # type: ignore  # pylint: disable=arguments-differ  #35566 in pytorch
top_indices: torch.Tensor,
doc: Doc) -> torch.Tensor:
word_ids = torch.arange(0, len(doc["cased_words"]), device=self.device)
# 一、same speaker特征
speaker_map = torch.tensor(self._speaker_map(doc), device=self.device)
# 获取speaker_map对应位置的值,最终输出维度和top_indices一样
# 看这一步操作,妥妥让我难理解半个小时~
# 1. speaker_map[top_indices]这里,第一次见这种map操作,学习了
# 2. 这种广播写法让我觉得,emmmmm,还不如expand下来的更直接
same_speaker = (speaker_map[top_indices] == speaker_map.unsqueeze(1)) # 广播
same_speaker = self.speaker_emb(same_speaker.to(torch.long))

# 二、距离特征
# 这个特征我觉得还是挺有用的,1、加速了训练和推理速度 2、加快收敛速度
# bucketing the distance (see __init__())
distance = (word_ids.unsqueeze(1) - word_ids[top_indices]
).clamp_min_(min=1) # 小于1的变成1
log_distance = distance.to(torch.float).log2().floor_()
log_distance = log_distance.clamp_max_(max=6).to(torch.long) # 大于最大值的元素将变为最大值。 那么就是64
# 一会log_distance一会distance的,看着就不容易理解,直接到log_distance多好😂😂😂😂😂😂
distance = torch.where(distance < 5, distance - 1, log_distance + 2)
distance = self.distance_emb(distance)

# 三、同一文档特征
genre = torch.tensor(self.genre2int[doc["document_id"][:2]],
device=self.device).expand_as(top_indices)
genre = self.genre_emb(genre)

return self.dropout(torch.cat((same_speaker, distance, genre), dim=2))

AnaphoricityScorer

这部分看着有点绕,看如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
def forward(self, *,  # type: ignore  # pylint: disable=arguments-differ  #35566 in pytorch
all_mentions: torch.Tensor,
mentions_batch: torch.Tensor,
pw_batch: torch.Tensor,
top_indices_batch: torch.Tensor,
top_rough_scores_batch: torch.Tensor,
) -> torch.Tensor:

# [batch_size, n_ants, pair_emb]
pair_matrix = self._get_pair_matrix(
all_mentions, mentions_batch, pw_batch, top_indices_batch)

# [batch_size, n_ants]
scores = top_rough_scores_batch + self._ffnn(pair_matrix)
scores = utils.add_dummy(scores, eps=True)

return scores

def _ffnn(self, x: torch.Tensor) -> torch.Tensor:
"""
Calculates anaphoricity scores.

Args:
x: tensor of shape [batch_size, n_ants, n_features]

Returns:
tensor of shape [batch_size, n_ants]
"""
x = self.out(self.hidden(x))
return x.squeeze(2)

@staticmethod
def _get_pair_matrix(all_mentions: torch.Tensor,
mentions_batch: torch.Tensor,
pw_batch: torch.Tensor,
top_indices_batch: torch.Tensor,
) -> torch.Tensor:

emb_size = mentions_batch.shape[1]
n_ants = pw_batch.shape[1]
# 计算所有tokens和这50个tokens的关联度
a_mentions = mentions_batch.unsqueeze(1).expand(-1, n_ants, emb_size)
b_mentions = all_mentions[top_indices_batch]
similarity = a_mentions * b_mentions

out = torch.cat((a_mentions, b_mentions, similarity, pw_batch), dim=2)
return out

_get_pair_matrix中的b_mentions,可参考b_mentions。后续接了个ffnn,获得其最终得分。

整体代码作者挺喜欢参差网络和前馈神经网络这种操作。

loss计算

CorefLoss计算分成了两部分,一个是NLML,另外一个BCELoss.

1
2
3
4
5
6
@staticmethod
def _nlml(input_: torch.Tensor, target: torch.Tensor) -> torch.Tensor:
# gold这地方明显就想计算相关性。
gold = torch.logsumexp(input_ + torch.log(target), dim=1)
input_ = torch.logsumexp(input_, dim=1)
return (input_ - gold).mean()

附录

word-level实现demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# -*- coding: utf8 -*-
#

import torch


class WordEmbedding(torch.nn.Module):
def __init__(self, n_in, p):
super(WordEmbedding, self).__init__()
self.n_in = n_in
self.p = p
self.linear = torch.nn.Linear(in_features=n_in, out_features=1)

def forward(self, x, indices):
start_end_indices_to_tensor = torch.tensor(indices)
start_indices = start_end_indices_to_tensor[:, 0]
end_indices = start_end_indices_to_tensor[:, 1]
mask = torch.arange(5).expand(3, 5)

attn_mask = (mask >= start_indices.unsqueeze(1)) * (mask < end_indices.unsqueeze(1))
attn_mask = torch.log(attn_mask.to(torch.float))
attn_scores = self.linear(x).T
attn_scores = attn_scores.expand((len(indices), x.size(0)))
attn_scores = attn_scores + attn_mask
return torch.softmax(attn_scores, dim=1).mm(x)


if __name__ == '__main__':
# 假设为:我 是 中国人,5为每个subtoken,10为每个subtoken的embedding
word_feature = torch.arange(50, dtype=torch.float).view(5, 10)
# 那么index是:
start_end_indices = [(0, 1), (1, 2), (2, 5)]

we = WordEmbedding(n_in=10, p=0.1)
we.forward(word_feature, start_end_indices)

取索引操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
>>> a
tensor([[ 0, 1, 2, 3, 4],
[ 5, 6, 7, 8, 9],
[10, 11, 12, 13, 14]])

# b对应top50选出来的index
>>> b
tensor([[0, 1],
[1, 2],
[2, 2]])

>>> a[b]
tensor([[[ 0, 1, 2, 3, 4],
[ 5, 6, 7, 8, 9]],

[[ 5, 6, 7, 8, 9],
[10, 11, 12, 13, 14]],

[[10, 11, 12, 13, 14],
[10, 11, 12, 13, 14]]])
>>> a[b].shape
torch.Size([3, 2, 5])



精彩例子

这种写法我挺喜欢的,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

>>> aa
tensor([[0, 1, 2, 3, 4],
[5, 6, 7, 8, 9]])
>>> mm
tensor([[ True, True, False, True, True],
[False, True, True, True, True]])

# 一般会在最后计算loss时加上mask进行降维铺平
>>> aa[mm]
tensor([0, 1, 3, 4, 6, 7, 8, 9])

# 形式1: 作者将无关的score置为-inf
>>> aa * torch.log(mm.to(torch.float))
tensor([[0., 0., -inf, 0., 0.],
[-inf, 0., 0., 0., 0.]])

# 形式2:
>>> aa * mm
tensor([[0, 1, 0, 3, 4],
[0, 6, 7, 8, 9]])

  • 算法

展开全文 >>

活到老学到老之index操作

2021-12-16

快速想一想,你能想到torch有哪些常见的index操作??

1. gather

1
2
3
4
5
6
>>> a = torch.tensor([[1, 2, 3],
[4, 5, 6]])
>>> a.gather(dim=1, index=torch.tensor([[0,1], [1,2]]))
tensor([[1, 2],
[5, 6]])

2. index_select

1
2
3
4
5
6
7
>>> a
tensor([[1, 2, 3],
[4, 5, 6]])
>>> a.index_select(dim=1, index=torch.tensor([1,2]))
tensor([[2, 3],
[5, 6]])

3. 骚气的来了哦

根据上面例子可以看到,a为矩阵,选择a中的index,但是下面介绍一个map操作.

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> index
tensor([[1, 2, 3],
[4, 5, 6]])

>>> a = torch.tensor([11, 22, 33, 44, 55, 66, 77])
>>> a
tensor([11, 22, 33, 44, 55, 66, 77])
>>> index
tensor([[1, 2, 3],
[4, 5, 6]])
>>> a[index]
tensor([[22, 33, 44],
[55, 66, 77]])

这种操作有一个真实场景,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# 1. 这是两个特征
>>> words = ['我', '爱', '中', '国']
>>> pos = ['n', 'v', 'n', 'n']

# 2. 假设words变成了一个4 * 4的临接矩阵,用于表示每个token和其他token的一个关联重要程度

>>> words_attn = torch.rand(4,4)

>>> words_attn
tensor([[0.6279, 0.6234, 0.9831, 0.5267],
[0.2265, 0.8453, 0.5740, 0.4772],
[0.7759, 0.6952, 0.1758, 0.3800],
[0.9998, 0.3138, 0.5078, 0.5565]])


>>> scores, indices = words_attn.topk(k=2, dim=1)

>>> indices
tensor([[2, 0],
[1, 2],
[0, 1],
[0, 3]])

# 3. 假设pos转为了
>>> pos_tensor = torch.tensor([111, 222, 333, 444])

# 4. map操作
>>> pos_tensor[indices]
tensor([[333, 111],
[222, 333],
[111, 222],
[111, 444]])

# 5. 随后就可以接一个embedding搞事情了
pos_embedding(pos_tensor[indices])

# 6. 总结,这个示例的优点可以看出是快速计算,取topK然后再结合其他的特征进行操作。

4. batch_gather

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import torch


def batch_gather(data: torch.Tensor, index: torch.Tensor):
index = index.unsqueeze(-1).repeat_interleave(data.size()[-1], dim=-1) # (bs, n, hidden)
# index = index.unsqueeze(-1).expand(*(*index.shape, data.shape[-1]))
return torch.gather(data, 1, index)


if __name__ == '__main__':
a = torch.randn(3, 128, 312)
indices = torch.tensor([
[1, 2],
[5, 6],
[7, 7]
])
output = batch_gather(a, indices)
print(output.shape)
print((output[0][0] == a[0][1]).all())
print((output[0][1] == a[0][2]).all())
print((output[1][0] == a[1][5]).all())
print((output[1][1] == a[1][6]).all())
print((output[2][0] == a[2][7]).all())
print((output[2][1] == a[2][7]).all())

  • 算法

展开全文 >>

语义依存分析

2021-11-15

定义(ltp)

语义依存分析 (Semantic Dependency Parsing, SDP),分析句子各个语言单位之间的语义关联,并将语义关联以依存结构呈现。
使用语义依存刻画句子语义,好处在于不需要去抽象词汇本身,而是通过词汇所承受的语义框架来描述该词汇,而论元的数目相对词汇来说数量总是少了很多的。
语义依存分析目标是跨越句子表层句法结构的束缚,直接获取深层的语义信息。
例如以下三个句子,用不同的表达方式表达了同一个语义信息,即张三实施了一个吃的动作,吃的动作是对苹果实施的。



语义依存分析不受句法结构的影响,将具有直接语义关联的语言单元直接连接依存弧并标记上相应的语义关系。这也是语义依存分析与句法依存分析的重要区别。

如上例对比了句法依存和语义分析的结果,可以看到两者存在两个显著差别。
第一,句法依存某种程度上更重视非实词(如介词)在句子结构分析中的作用,而语义依存更倾向在具有直接语义关联的实词之间建立直接依存弧,非实词作为辅助标记存在。
第二,两者依存弧上标记的语义关系完全不同,语义依存关系是由论元关系引申归纳而来,可以用于回答问题,如我在哪里喝汤,我在用什么喝汤,谁在喝汤,我在喝什么。但是句法依存却没有这个能力。
第三,句法依存为树结构,语义依存为图结构,即是说当前词的依存弧可以有多个。

语义依存与语义角色标注之间也存在关联,语义角色标注只关注句子主要谓词的论元及谓词与论元之间的关系,而语义依存不仅关注谓词与论元的关系,还关注谓词与谓词之间、论元与论元之间、论元内部的语义关系。语义依存对句子语义信息的刻画更加完整全面。

实现

https://github.com/geasyheart/semantic-dependency-parser

欢迎Star!

1. 数据集

目前貌似公开的只有SEMEVAL2016数据集,地址在:HIT-SCIR/SemEval-2016,代码仓库中的数据集是将text和news两类合并而来。

额外插一句,对于一个算法项目来讲,不仅仅是算法部分,还要有数据,即使不能公开,也可以造一些例子,能够跑通算法,HanLP在这方面真的是无敌存在!

2. 模型结构

  • 这里使用到的模型结构:
1
2
3
4
5
6
7
8
9
10
11
semantic_dependency_parser.py [line:34] INFO SemanticDependencyModel(
(encoder): TransformerEmbedding(hfl/chinese-electra-180g-small-discriminator, n_layers=4, n_out=256, stride=256, pooling=mean, pad_index=0, dropout=0.33, requires_grad=True)
(tag_embedding): Embedding(41, 64)
(edge_mlp_d): MLP(n_in=320, n_out=600, dropout=0.33)
(edge_mlp_h): MLP(n_in=320, n_out=600, dropout=0.33)
(label_mlp_d): MLP(n_in=320, n_out=600, dropout=0.33)
(label_mlp_h): MLP(n_in=320, n_out=600, dropout=0.33)
(edge_attn): Biaffine(n_in=600, n_out=2, bias_x=True, bias_y=True)
(label_attn): Biaffine(n_in=600, n_out=158, bias_x=True, bias_y=True)
(criterion): CrossEntropyLoss()
)
  • HanLP使用的模型结构:
1
2
3
4
5
6
7
8
9
10
11
BiaffineDependencyModel(
(encoder): EncoderWithContextualLayer()
(biaffine_decoder): BiaffineDecoder(
(mlp_arc_h): MLP(n_in=256, n_out=500, dropout=0.33)
(mlp_arc_d): MLP(n_in=256, n_out=500, dropout=0.33)
(mlp_rel_h): MLP(n_in=256, n_out=100, dropout=0.33)
(mlp_rel_d): MLP(n_in=256, n_out=100, dropout=0.33)
(arc_attn): Biaffine(n_in=500, n_out=1, bias_x=True)
(rel_attn): Biaffine(n_in=100, n_out=136, bias_x=True, bias_y=True)
)
)

和dependency parser结构相同,但是loss计算和解码部分不同。

区别点在于,举个例子:

1
2
3
4
5
# pred_arcs.shape
(32, 49, 49)
# true_arcs.shape
(32, 49)

因为dependency parser有一个限制,即当前词只可能依存其他一个词,那么argmax(-1)即是在49那里获取最大的,表示和这49个词中最大的作为依存关系,使用交叉熵。

而semantic dependency parser没有这个限制,当前词可能和多个词存在依存关系,那么他的pred_arcs和true_arcs的维度是一样的,都是(32, 49, 49),所以使用BCELoss。

当然也可以用交叉熵,只需要将pred_arcs的维度转换成(32, 49, 49, 2)即可,也是我下面的做法。

3. loss计算和解码

在这里计算loss时,采用的是交叉熵,也就是说s_edge.size(-1) == 2,表示当前词和句子所有词之间是或者否,然后argmax(-1)进行解码。

在HanLP计算loss时,对于arc(即edge)的shape为(batch_size, seq_length, seq_length),因为biaffine的输出维度为1,所以这里计算loss时使用BCELoss,表示当前词和句子所有词之间是否存在关系。

另外一个两者的区别点在于计算rel时,HanLP采取的方式是各自计算各自的loss(即arc和rel),然后loss相加。
这里计算rel loss时融合了arc的信息进来,好处就在于能够快速收敛和提升准确度吧。

各模块技术指标

https://www.ltp-cloud.com/intro#benchmark

ltp关系类型

关系类型 Tag Description Example
施事关系 Agt Agent 我送她一束花 (我 <-- 送)
当事关系 Exp Experiencer 我跑得快 (跑 --> 我)
感事关系 Aft Affection 我思念家乡 (思念 --> 我)
领事关系 Poss Possessor 他有一本好读 (他 <-- 有)
受事关系 Pat Patient 他打了小明 (打 --> 小明)
客事关系 Cont Content 他听到鞭炮声 (听 --> 鞭炮声)
成事关系 Prod Product 他写了本小说 (写 --> 小说)
源事关系 Orig Origin 我军缴获敌人四辆坦克 (缴获 --> 坦克)
涉事关系 Datv Dative 他告诉我个秘密 ( 告诉 --> 我 )
比较角色 Comp Comitative 他成绩比我好 (他 --> 我)
属事角色 Belg Belongings 老赵有俩女儿 (老赵 <-- 有)
类事角色 Clas Classification 他是中学生 (是 --> 中学生)
依据角色 Accd According 本庭依法宣判 (依法 <-- 宣判)
缘故角色 Reas Reason 他在愁女儿婚事 (愁 --> 婚事)
意图角色 Int Intention 为了金牌他拼命努力 (金牌 <-- 努力)
结局角色 Cons Consequence 他跑了满头大汗 (跑 --> 满头大汗)
方式角色 Mann Manner 球慢慢滚进空门 (慢慢 <-- 滚)
工具角色 Tool Tool 她用砂锅熬粥 (砂锅 <-- 熬粥)
材料角色 Malt Material 她用小米熬粥 (小米 <-- 熬粥)
时间角色 Time Time 唐朝有个李白 (唐朝 <-- 有)
空间角色 Loc Location 这房子朝南 (朝 --> 南)
历程角色 Proc Process 火车正在过长江大桥 (过 --> 大桥)
趋向角色 Dir Direction 部队奔向南方 (奔 --> 南)
范围角色 Sco Scope 产品应该比质量 (比 --> 质量)
数量角色 Quan Quantity 一年有365天 (有 --> 天)
数量数组 Qp Quantity-phrase 三本书 (三 --> 本)
频率角色 Freq Frequency 他每天看书 (每天 <-- 看)
顺序角色 Seq Sequence 他跑第一 (跑 --> 第一)
描写角色 Desc(Feat) Description 他长得胖 (长 --> 胖)
宿主角色 Host Host 住房面积 (住房 <-- 面积)
名字修饰角色 Nmod Name-modifier 果戈里大街 (果戈里 <-- 大街)
时间修饰角色 Tmod Time-modifier 星期一上午 (星期一 <-- 上午)
反角色 r + main role 打篮球的小姑娘 (打篮球 <-- 姑娘)
嵌套角色 d + main role 爷爷看见孙子在跑 (看见 --> 跑)
并列关系 eCoo event Coordination 我喜欢唱歌和跳舞 (唱歌 --> 跳舞)
选择关系 eSelt event Selection 您是喝茶还是喝咖啡 (茶 --> 咖啡)
等同关系 eEqu event Equivalent 他们三个人一起走 (他们 --> 三个人)
先行关系 ePrec event Precedent 首先,先
顺承关系 eSucc event Successor 随后,然后
递进关系 eProg event Progression 况且,并且
转折关系 eAdvt event adversative 却,然而
原因关系 eCau event Cause 因为,既然
结果关系 eResu event Result 因此,以致
推论关系 eInf event Inference 才,则
条件关系 eCond event Condition 只要,除非
假设关系 eSupp event Supposition 如果,要是
让步关系 eConc event Concession 纵使,哪怕
手段关系 eMetd event Method
目的关系 ePurp event Purpose 为了,以便
割舍关系 eAban event Abandonment 与其,也不
选取关系 ePref event Preference 不如,宁愿
总括关系 eSum event Summary 总而言之
分叙关系 eRect event Recount 例如,比方说
连词标记 mConj Recount Marker 和,或
的字标记 mAux Auxiliary 的,地,得
介词标记 mPrep Preposition 把,被
语气标记 mTone Tone 吗,呢
时间标记 mTime Time 才,曾经
范围标记 mRang Range 都,到处
程度标记 mDegr Degree 很,稍微
频率标记 mFreq Frequency Marker 再,常常
趋向标记 mDir Direction Marker 上去,下来
插入语标记 mPars Parenthesis Marker 总的来说,众所周知
否定标记 mNeg Negation Marker 不,没,未
情态标记 mMod Modal Marker 幸亏,会,能
标点标记 mPunc Punctuation Marker ,。!
重复标记 mPept Repetition Marker 走啊走 (走 --> 走)
多数标记 mMaj Majority Marker 们,等
实词虚化标记 mVain Vain Marker
离合标记 mSepa Seperation Marker 吃了个饭 (吃 --> 饭) 洗了个澡 (洗 --> 澡)
根节点 Root Root 全句核心节点
  • 算法

展开全文 >>

bio-based语义角色标注

2021-11-12

定义

定义1:

1
Semantic Role Labeling (SRL) is defined as the task to recognize arguments for a given predicate and assign semantic role labels to them.

定义2(ltp):

1
语义角色标注 (Semantic Role Labeling, SRL) 是一种浅层的语义分析技术,标注句子中某些短语为给定谓词的论元 (语义角色) ,如施事、受事、时间和地点等。其能够对问答系统、信息抽取和机器翻译等应用产生推动作用。 仍然是上面的例子,语义角色标注的结果为:

实现

https://github.com/geasyheart/srl-parser

欢迎Star!

示例1:


看黄色那部分:

  • 他叫汤姆,他作为施事者,叫为谓语,汤姆为受事者
  • 去拿外衣(为一个完整语义)
  • 汤姆拿外衣,汤姆为施事者,拿为谓语,外衣为受事者

示例2:

‘各位/PN 好/VA ,/PU 欢迎/VV 您/PN 收看/VV 国际/NN 频道/NN 的/DEG 今日/NT 关注/NN 。/PU’

此图自己画的,如有需要可参考

  • 欢迎您,欢迎为谓语,您为受事者,收看国际频道的今日关注为语义角色
  • 其余忽略…

数据集

数据集来自ontonotes5.0,但是此为收费数据集,或者需要大学帐号注册,找到一个开源的https://github.com/GuocaiL/Coref_Resolution/archive/master.zip#data/,处理后的数据集以 jsonlines后缀存储,放到此处。

具体的处理逻辑可参考这里。

实现

目前常见的有span-based,bio-based,treecrf,treecrf是yzhangcs的实现方式。bio-based是用序列标注的方式来做(hanlp和ltp均以此实现),故是本文的重点。

数据处理

看这个文件,其中预测的label处理后是这个样子,解释如下:

  • 各位 好,好是第二个字,和第一个词有关系,关系为3。
  • 欢迎 您收看国际频道的今日关注, 对应关系为1, 13, 23, 23, 23, 23, 23。
1
2
3
4
5
6
7
8
9
10
11
12
13
tensor([[ 0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0],
[ 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[ 0, 0, 0, 0, 1, 13, 23, 23, 23, 23, 23, 0],
[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[ 0, 0, 0, 0, 0, 0, 1, 2, 2, 2, 2, 0],
[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]])

看到这里是否知道其模型结构了,biaffine+crf哇,biaffine转换成临接矩阵,用于预测谓词和论元的关系,论元用crf序列标注的方式来进行预测。

模型结构

1
2
3
4
5
6
7
SpanBIOSemanticRoleLabelingModel(
(transformer): TransformerEncoder(n_out=256,dropout=0.1)
(s_layer): MLP(n_in=256, n_out=300, dropout=0.1)
(e_layer): MLP(n_in=256, n_out=300, dropout=0.1)
(biaffine): Biaffine(n_in=300, n_out=64, bias_x=True, bias_y=True)
(crf): CRF(num_tags=64)
)

步骤

1. loss计算和解码

到biaffine这一层没什么需要特别注意的,bert获取词向量的方式从以前的求平均改成了以首字代表词向量。后面接两个mlp以及biaffine。重点在于如何和crf融合到一起?

  1. biaffine输出后维度为(batch_size,seq_length,seq_length,hidden_size),crf是用在第二个seq_length那一维。
  2. crf的输入为发射概率,此维度为(batch_size,seq_length,hidden_size)。

基于上述两个前提,将batch_size和第一个seq_length进行flatten,因为第一个seq_length为谓语,不影响论元的预测,转换后输入到crf中,就可以计算loss了,解码一样。

2. 评估指标

预测出来的结果示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# pred

[[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 1, 13, 23, 23, 23, 23, 23, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 1, 2, 2, 2, 2, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],

[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[12, 6, 3, 4, 4, 4, 4, 4, 4, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]

pred类型为List[List[int]],他的长度等于batch_tokens中每个词的长度,即:

1
len(pred) == sum([len(i) for i in batch["batch_tokens"]])

** 其中每一行表示的是当前词和整句每个词所呈现出来的关系。 **

基于上面结论,就不难写评估代码了,将其转成(token_index, start, end, label),然后set取交集,最终计算f1值,可看这里。

3. 解码预测

基本如上,具体可看。

性能

ltp v4

从这里也可以看出,语义角色标注任务任重道远。

ltp关系类型

ltp关系类型1
ltp关系类型2

关系类型

Tag

Description

Example

ARG0

causers or experiencers

施事者、主体、触发者

[政府 ARG0]鼓励个人投资服务业。

ARG1

patient

受事者

政府鼓励[个人 ARG1]投资服务业。

ARG2

range

语义角色2

政府鼓励个人[投资服务业 ARG2]。

ARG3

starting point

语义角色3

巴基斯坦[对谈判前景 ARG3]表示悲观。

ARG4

end point

语义角色4

产检部门将产检时间缩短到[一至三天 ARG4]。

ADV

adverbial

状语

我们[即将 ADV]迎来新年。

BNF

beneficiary

受益人

义务[为学童及老师 BNF]做超音波检查 。

CND

condition

条件

[如果早期发现 CND],便能提醒当事人注意血压的变化。

CRD

coordinated arguments

并列

跟南韩、[跟美国 CRD]谋求和平关系的举动也更加积极。

DGR

degree

程度

贫铀弹含有放射性比铀强[20万倍 DGR]。

DIR

direction

方向

[从此处 DIR] 我们可以发现寇克斯报告的精髓。

DIS

discourse marker

会话标记

警方上午针对目击者做笔录,[而 DIS]李士东仍然不见踪影。

EXT

extent

  • 算法

展开全文 >>

基于树形条件随机场的高阶句法分析

2021-11-02

此篇文章貌似没有重点,日常笔记吧。

基于树形条件随机场的高阶句法分析作者硕士毕业论文,关于句法分析的历史与实现基本讲了一遍,包括作者使用TreeCRF高阶建模等工作。
代码,这个项目包含了句法分析任务的实现。包括dependency parser,semantic dependency parser,constituency parser等。

对于句法分析工作,百度ddparser相比下来可能是工业上更好的选择,不过目前个人更倾向于语义句法工作,相比下来更接近直观感受(还是看任务啦~)。

不管是semantic role labeling(语义角色标注)或者semantic dependency parser(语义依存分析),看ltp的演示效果。

但是不能光看这种效果,从目前公开的论文水平来看,准确率并没有很高,另外高质量,高数量的标注数据集也相对少。

大家都是一样的模型,比的就是数据集喽,这点不得不夸HanLP,至少人家代码里面都有数据集,方便学习。

手动狗头

  • 算法

展开全文 >>

&laquo; Prev1…56789…28Next &raquo;
© 2025 张宇
Hexo Theme Yilia by Litten
  • 所有文章
  • 友链
  • 关于我

tag:

  • linux
  • 技术
  • 小桥流水人家
  • MySQL
  • flutter
  • 算法
  • machine learning
  • SQLAlchemy
  • python
  • CI
  • git
  • go
  • grpc
  • 机器学习

    缺失模块。
    1、请确保node版本大于6.2
    2、在博客根目录(注意不是yilia根目录)执行以下命令:
    npm i hexo-generator-json-content --save

    3、在根目录_config.yml里添加配置:

      jsonContent:
        meta: false
        pages: false
        posts:
          title: true
          date: true
          path: true
          text: false
          raw: false
          content: false
          slug: false
          updated: false
          comments: false
          link: false
          permalink: false
          excerpt: false
          categories: false
          tags: true
    

  • Github
  • Google
  • StackOverFlow
探索世界美好的存在。