Theme NexT works best with JavaScript enabled
0%

动态词向量预训练

^ _ ^

Motivation

在静态词向量学习算法中,无论是基于局部上下文预测的 word2vec 算法,还是基于全局共现信息预测的 GloVe 算法,其本质都是将一个词在整个语料库中共现上下文信息聚合至该词的向量表示。因此,在一个给定的语料库上训练得到的词向量可以认为是“静态”的,即:对于任意一个词,其向量表示是恒定的,不随其上下文的变化而变化。

然而,在自然语言中,同一个词在不同的上下文或语境下可能呈现出多种不同的词义、语法性质或者属性。以“下场”一词为例,其在句子“他 亲自 下场 参加 比赛”和“竟 落得 这样 的 下场”中的词义截然不同,而且具有不同的词性(前者为动词,后者为名词)。

在静态词向量表示中,由于词的所有上下文信息都被压缩、聚合至单个向量表示内,因此难以刻画一个词在不同上下文或不同语境下的不同词义信息。为了解决这一问题,研究人员提出了上下文相关的词向量(Contextualized WordEmbedding)。顾名思义,在这种表示方法中,一个词的向量将由其当前所在的上下文计算获得,因此是随上下文而动态变化的,也将其称为 动态词向量(Dynamic Word Embedding)

双向语言模型

对于给定的一段输入文本 $w_1w_2 \cdots w_n$,双向语言模型从前向(从左到右)和后向(从右到左)两个方向同时建立语言模型。具体地,模型首先对每个词单独编码。这一过程是上下文无关的,主要利用了词内部的字符序列信息。基于编码后的词表示序列,模型使用两个不同方向的多层长短时记忆网络(LSTM)分别计算每一时刻词的前向、后向隐含层表示,也就是上下文相关的词向量表示。利用该表示,模型预测每一时刻的目标词。

(1)输入层

首先,字符向量层将输入层中的每个字符(含额外添加的起止符)转换为向量表示。假设 $w_t$ 由字符序列 $c_1 c_2 \cdots c_t$构成,对于其中的每个字符 $c_i$,可以表示为 $v_{c_i} = E^{char} e_{c_i}$。其中,$E^{char} \in R^{d^{char} \times |V^{char}|}$ 表示字符向量矩阵;$V^{char}$ 表示所有字符集合;$d^{char}$ 表示字符向量维度;$e_{c_i}$ 表示字符 $c_i$ 的独热编码。

记 $w_t$ 中所有字符向量组成的矩阵为 $C_t \in R^{d^{char} \times l}$,即 $C_t = [v_{c_1};v_{c_2};\cdots;v_{c_l}]$。接下来,利用卷积神经网络对字符级向量表示序列进行语义组合(Semantic Composition)。

这里使用一维卷积神经网络,将字符向量的维度 $d^{char}$ 作为输入通道的个数,记为 $N^{in}$,输出向量的维度作为输出通道的个数,记为 $N^{out}$。另外,通过使用多个不同大小的卷积核,可以利用不同粒度的字符级上下文信息,并得到相应的隐含层向量表示,这些隐含层向量的维度由每个卷积核对应的输出通道个数确定。拼接这些向量,就得到了每一位置的卷积输出。然后,池化操作隐含层所有位置的输出向量,就可以得到对于词 $w_t$ 的定长向量表示,记为 $f_t$。

接着,模型使用两层 Highway 神经网络对卷积神经网络输出作进一步变换,得到最终的词向量表示 $x_t$。Highway 神经网络在输入与输出之间直接建立”通道”,使得输出层可以直接将梯度回传至输入层,从而避免因网络层数过多而带来的梯度爆炸或弥散的问题。单层 Highway 神经网络的具体计算方式如下:$x_t = g \odot f_t + (1-g) \odot ReLU(W f_t + b)$。式中,$g$ 为门控向量,其以 $f_t$ 为输入,线性变换后通过 $Sigmoid$ 函数 $\sigma$ 计算得到:$g = \sigma(W^g f_t + b^g)$。式中,$W^g$ 和 $b^g$ 为门控网络中的线性变换矩阵和偏置向量。可见,Highway 神经网络的输出实际上是输入层与隐含层的线性插值结果。

接下来,在由上述过程得到的上下无关词向量的基础之上,利用双向语言模型分别编码前向与后向上下文信息,从而得到每一时刻的动态词向量表示。

Code

prepare dataset

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
def load_corpus(path, max_token_len=None, max_seq_len=None):
'''load data from text corpus and create vocabulary
max_token_len: max length of a word
max_seq_len: max length of a sequence
'''
text = []
charset = {BOS_TOKEN, EOS_TOKEN, PAD_TOKEN, BOW_TOKEN, EOW_TOKEN}
with open(path, "r") as f:
for line in tqdm(f):
tokens = line.rstrip().split(" ")
# cut long sequence
if max_seq_len is not None and len(tokens) + 2 > max_seq_len:
tokens = line[:max_seq_len-2]
sent = [BOS_TOKEN]
for token in tokens:
if max_token_len is not None and len(token+2) > max_token_len:
token = token[:max_token_len-2]
sent.append(token)
for ch in token:
charset.append(ch)
sent.append(EOS_TOKEN)
text.append(sent)
# create word-level vocabulary
vocab_w = Vocab.build(text, min_freq=2, reserved_tokens=[PAD_TOKEN, BOS_TOKEN, EOS_TOKEN])
# create char-level vocabulary
vocab_c = Vocab(tokens=list(charset))

# create word-level corpus
corpus_w = [vocab_w.convert_tokens_to_ids(sent) for sent in text]
# create char-level corpus
corpus_c = []
bow = vocab_c[BOW_TOKEN]
eow = vocab_c[EOS_TOKEN]
for i, sent in enumerate(text):
sent_c = []
for token in sent:
if token == BOS_TOKEN or token == EOS_TOKEN:
token_c = [bow, vocab_c[token], eow]
else:
token_c = [bow] + vocab_c.convert_tokens_to_ids(token) + [eow]
sent_c.append(token_c)
corpus_c.append(sent_c)
return corpus_w, corpus_c, vocab_w, vocab_c

creating dataset

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 BiLMDataset(Dataset):
def __init__(self, corpus_w, corpus_c, vocab_w, vocab_c):
super(BiLMDataset, self).__init__()
self.pad_w = vocab_w[PAD_TOKEN]
self.pad_c = vocab_c[PAD_TOKEN]
self.data = []
for sent_w, sent_c in zip(corpus_w, corpus_c):
self.data.append((sent_w, sent_c))

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

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

def collate_fn(self, examples):
seq_lens = torch.LongTensor([len(ex[0]) for ex in examples])
# word-level input
inputs_w = [torch.tensor(ex[0]) for ex in examples]
# padding
inputs_w = pad_sequence(inputs_w, batch_first=True, padding_value=self.pad_w)
# calculate max(sentence) and max(word)
batch_size, max_seq_len = inputs_w.shape
max_token_len = max(max([len(token) for token in ex[1]]) for ex in examples))
# char-level input
inputs_c = torch.LongTensor(batch_size, max_seq_len, max_token_len).fill_(self.pad_c)
for i, (sent_w, sent_c) in enumerate(examples):
for j, token in enumerate(sent_c):
inputs_c[i][j][:len(token)] = torch.LongTensor(token)
# forward-output & backward-output
targets_fw = torch.LongTensor(inputs_w.shape).fill_(self.pad_w)
targets_bw = torch.LongTensor(inputs_w.shape).fill_(self.pad_w)
for i, (sent_w, sent_c) in enumerate(examples):
targets_fw[i][:len(sent_w)-1] = torch.LongTensor(sent_w[1:])
targets_bw[i][1:len(sent_w)] = torch.LongTensor(sent_w[:len(sent_w)-1])\
return inputs_w, inputs_c, seq_lens, targets_fw, targets_bw