TF_IDF
词向量
句向量
Seq2Seq 语言生成模型
CNN的语言模型
语言模型的注意力
Transformer 将注意力发挥到极致
ELMo 一词多义
GPT 单向语言模型
BERT 双向语言模型
NLP模型的多种应用
机器怎么理解句子一直是一个难题,以前有人尝试用句子中出现的词语频率来表达这个句子的含义(TF-IDF)。 也有人想把句子中的词语先向量化,然后再叠加句子中所有向量化的词语来表达一句话。 这些的确都是一种计算机表达句子含义的方式,但是不一定会非常准确。因为他们都只是一种对词语理解的简单加工方式,有的忽略了词语的表达顺序, 有的忽略了词语的组合模式。这往往导致计算机不能非常准确的理解句子。
后来随着深度学习的快速发展,我们也能利用像循环神经网络或者自注意力这样的机制,去用模型直接理解整个句子,最终实现了End-to-End的句子理解。 如果用一句话来说明这样技术的核心,我会这样描述:向量表示是深度学习成功的关键。对句子的理解,就是在多维空间中给这个句子安排一个合适的位置。
每一个空间上的点,就代表了计算机对事物的某种理解。我们再将空间信息转换成其他信息,就能完成对这个句子理解后的应用了。
什么是Encoder和Decoder在深度学习中,万物都可向量化,其中有两个问题:
- 怎么样从原始的数据变成向量数据
- 怎么样加工向量化的数据,将其变成各种其他的表达形式
上面这张图就诠释了这样的一个过程:
- 我们用
Encoder
将原始的我爱莫烦Python
转变成标准的向量表达; - 在用各种
Decoder
将向量化表达转变成其他的表达形式,其中就可以是翻译,图片,情感和对话。
所以从上面例子可以看出,Encoder
是一种压缩器,去繁从简,将最有用的特征给识别出来,用最简练的信息表达他们。 这一点在AutoEncoder
中也有很好的体现。 而Decoder
是解压器,但是它并不是将压缩好的信息还原,而是解压成另外一种形式,换一种表达方式。
在这节内容中,以翻译为例,将这种句子理解的Encoder
和Decoder
机制给大家贯穿一下。在翻译的这个例子中,我们有个更具体的名词, 叫做seq2seq
. 意思是将一个 sequence
转换成另一个 sequence
。也就是用Encoder
压缩并提炼第一个sequence
的信息,然后用Decoder
将这个信息转换成另一种语言。
我使用一个非常简单,好训练的日期转换的例子来展示一下seq2seq的威力。需要实现的功能如下:
# 中文的 "年-月-日" -> "day/month/year"
"98-02-26" -> "26/Feb/1998"
我们将中文的顺序日期,转成英文的逆序日期,数据区间是20世纪后期到21世纪前期。 为了施加一些难度,在中文模式下,我不会告诉机器这是哪一个世纪,需要计算机自己去判断转成英文的时候是 20 世纪还是 21 世纪。
先来看训练过程, 其实也很简单,生成数据,建立模型,训练模型。
def train():
# 我已经帮大家封装了日期生成器代码
data = utils.DateData(4000)
# 建立模型
model = Seq2Seq(...)
# training
for t in range(1500):
bx, by, decoder_len = data.sample(32)
loss = model.step(bx, by, decoder_len)
最后你能看到它的整个训练过程。最开始预测成渣渣,但是后面预测结果会好很多。
t: 0 | loss: 3.289 | input: 96-06-17 | target: 17/Jun/1996 | inference: /2222222222
t: 70 | loss: 1.132 | input: 91-08-19 | target: 19/Aug/1991 | inference: 13/Mar/2001<EOS>
t: 140 | loss: 0.880 | input: 11-04-30 | target: 30/Apr/2011 | inference: 11/May/2003<EOS>
t: 210 | loss: 0.732 | input: 76-03-14 | target: 14/Mar/1976 | inference: 24/May/1988<EOS>
....
t: 910 | loss: 0.064 | input: 29-10-12 | target: 12/Oct/2029 | inference: 12/Oct/2029<EOS>
t: 980 | loss: 0.035 | input: 79-06-17 | target: 17/Jun/1979 | inference: 17/Jun/1979<EOS>
接着我们就来具体看看模型是怎样搭建的。首先第一步要搭建压缩器encoder
,有了encoder
就可以将原始的词向量按顺序组装起来变成句向量。 有了这个句向量之后才能进行decode
工作。
import tensorflow as tf
from tensorflow import keras
import numpy as np
import tensorflow_addons as tfa
class Seq2Seq(keras.Model):
def __init__(self, ...):
super().__init__()
self.units = units
# encoder
self.enc_embeddings = keras.layers.Embedding() # [enc_n_vocab, emb_dim]
self.encoder = keras.layers.LSTM(units=units, return_sequences=True, return_state=True)
...
def encode(self, x):
embedded = self.enc_embeddings(x)
init_s = [tf.zeros((x.shape[0], self.units)), tf.zeros((x.shape[0], self.units))]
o, h, c = self.encoder(embedded, initial_state=init_s)
return [h, c] # state
在seq2seq中,decoder
在训练句子生成时是不同的。为了方便训练,尤其是在刚开始训练时,decoder
的输入如果是True label,那么就能大大减轻训练难度。 不管在训练时有没有预测错,下一步在decoder的输入都是正确的。
class Seq2Seq(keras.Model):
def __init__(self, ...):
...
# decoder
self.dec_embeddings = keras.layers.Embedding() # [dec_n_vocab, emb_dim]
self.decoder_cell = keras.layers.LSTMCell(units=units)
decoder_dense = keras.layers.Dense(dec_v_dim)
# 训练时的 decoder
self.decoder_train = tfa.seq2seq.BasicDecoder(
cell=self.decoder_cell,
sampler=tfa.seq2seq.sampler.TrainingSampler(), # sampler for train
output_layer=decoder_dense
)
self.cross_entropy = keras.losses.SparseCategoricalCrossentropy(from_logits=True)
self.opt = keras.optimizers.Adam(0.01)
def train_logits(self, x, y, seq_len):
s = self.encode(x)
dec_in = y[:, :-1] # ignore <EOS>
dec_emb_in = self.dec_embeddings(dec_in)
o, _, _ = self.decoder_train(dec_emb_in, s, sequence_length=seq_len)
logits = o.rnn_output
return logits
def step(self, x, y, seq_len):
with tf.GradientTape() as tape:
logits = self.train_logits(x, y, seq_len)
dec_out = y[:, 1:] # ignore <GO>
loss = self.cross_entropy(dec_out, logits)
grads = tape.gradient(loss, self.trainable_variables)
self.opt.apply_gradients(zip(grads, self.trainable_variables))
return loss.numpy()
而在生产环境中预测时,真的在做翻译时,我们就希望有另一种decode的sample方式。使decoder下一步的预测基于decoder上一步的预测,而不是true label。
class Seq2Seq(keras.Model):
def __init__(self):
...
# predict decoder
self.decoder_eval = tfa.seq2seq.BasicDecoder(
cell=self.decoder_cell,
sampler=tfa.seq2seq.sampler.GreedyEmbeddingSampler(), # sampler for predict
output_layer=decoder_dense
)
...
def inference(self, x):
s = self.encode(x)
done, i, s = self.decoder_eval.initialize(
self.dec_embeddings.variables[0],
start_tokens=tf.fill([x.shape[0], ], self.start_token),
end_token=self.end_token,
initial_state=s,
)
pred_id = np.zeros((x.shape[0], self.max_pred_len), dtype=np.int32)
for l in range(self.max_pred_len):
o, s, i, done = self.decoder_eval.step(
time=l, inputs=i, state=s, training=False)
pred_id[:, l] = o.sample_id
return pred_id
所以在seq2seq
中,为了加快训练速度,我们一般使用的training
和inference
的decode
方式是有所不同的。 inference
的时候,那没办法,只能拿着上次decode
的词作为下一步的input
,我们并没有多少额外的信息,也没有真实label
可以参考。 但是在training
时却可以拿着label
过来加强训练的有效性,达到快速收敛的效果,training
如果沿着错误的方向一直错下去,是很难纠正的。 所以我们对training的预测错误视而不见,每次还是用正确的标签作为decoder输入
但是使用GreedyEmbeddingSampler()
作为decode
的方法是有局限性的,有时候会因为忽略了前期的低分数而错过了后期的整体高分策略, 类似于前面芝麻最好,所以捡了芝麻,但后面却错过了捡西瓜的机会。而这种因局部信息不全而导致的策略不优,可以靠Beam search
的筛选策略弥补。 如果使用 beam search, 我们不仅仅关注当前最优策略, 而且每预测一个词时,还保持关注当前时刻所有候选词的N个最优策略,结束预测时,就有很大概率能够找到全局比较优的路径。 举个例子,如果我们用beam search size = 2
, 意味着每次预测都记录最优的两个预测,然后沿着这两个预测继续预测, 每次后续的预测都只挑选下一步最好的两个预测。 这样加大了搜索范围,使我们有机会接触到全局较优路径。
这一节内容我们使用的是一种RNN模型来产生句向量的embedding,RNN是我们的encoder。那么通常在图像领域比较有优势的CNN能不能也拿来做encoder呢? 答案是肯定的,下节内容我们来见识一下CNN是怎么做encoder或者语言模型的。
全部代码utils.py
import numpy as np
import datetime
PAD_ID = 0
class DateData:
def __init__(self, n):
np.random.seed(1)
self.date_cn = []
self.date_en = []
for timestamp in np.random.randint(143835585, 2043835585, n):
date = datetime.datetime.fromtimestamp(timestamp)
self.date_cn.append(date.strftime("%y-%m-%d"))
self.date_en.append(date.strftime("%d/%b/%Y"))
self.vocab = set(
[str(i) for i in range(0, 10)] + ["-", "/", "<GO>", "<EOS>"] + [
i.split("/")[1] for i in self.date_en])
self.v2i = {v: i for i, v in enumerate(sorted(list(self.vocab)), start=1)}
self.v2i["<PAD>"] = PAD_ID
self.vocab.add("<PAD>")
self.i2v = {i: v for v, i in self.v2i.items()}
self.x, self.y = [], []
for cn, en in zip(self.date_cn, self.date_en):
self.x.append([self.v2i[v] for v in cn])
self.y.append(
[self.v2i["<GO>"], ] + [self.v2i[v] for v in en[:3]] + [
self.v2i[en[3:6]], ] + [self.v2i[v] for v in en[6:]] + [
self.v2i["<EOS>"], ])
self.x, self.y = np.array(self.x), np.array(self.y)
self.start_token = self.v2i["<GO>"]
self.end_token = self.v2i["<EOS>"]
def sample(self, n=64):
bi = np.random.randint(0, len(self.x), size=n)
bx, by = self.x[bi], self.y[bi]
decoder_len = np.full((len(bx),), by.shape[1] - 1, dtype=np.int32)
return bx, by, decoder_len
def idx2str(self, idx):
x = []
for i in idx:
x.append(self.i2v[i])
if i == self.end_token:
break
return "".join(x)
@property
def num_word(self):
return len(self.vocab)
import tensorflow as tf
from tensorflow import keras
import numpy as np
import utils # 上面的代码
import tensorflow_addons as tfa
class Seq2Seq(keras.Model):
def __init__(self, enc_v_dim, dec_v_dim, emb_dim, units, max_pred_len, start_token, end_token):
super().__init__()
self.units = units
# encoder
self.enc_embeddings = keras.layers.Embedding(
input_dim=enc_v_dim, output_dim=emb_dim, # [enc_n_vocab, emb_dim]
embeddings_initializer=tf.initializers.RandomNormal(0., 0.1),
)
self.encoder = keras.layers.LSTM(units=units, return_sequences=True, return_state=True)
# decoder
self.dec_embeddings = keras.layers.Embedding(
input_dim=dec_v_dim, output_dim=emb_dim, # [dec_n_vocab, emb_dim]
embeddings_initializer=tf.initializers.RandomNormal(0., 0.1),
)
self.decoder_cell = keras.layers.LSTMCell(units=units)
decoder_dense = keras.layers.Dense(dec_v_dim)
# train decoder
self.decoder_train = tfa.seq2seq.BasicDecoder(
cell=self.decoder_cell,
sampler=tfa.seq2seq.sampler.TrainingSampler(), # sampler for train
output_layer=decoder_dense
)
# predict decoder
self.decoder_eval = tfa.seq2seq.BasicDecoder(
cell=self.decoder_cell,
sampler=tfa.seq2seq.sampler.GreedyEmbeddingSampler(), # sampler for predict
output_layer=decoder_dense
)
self.cross_entropy = keras.losses.SparseCategoricalCrossentropy(from_logits=True)
self.opt = keras.optimizers.Adam(0.01)
self.max_pred_len = max_pred_len
self.start_token = start_token
self.end_token = end_token
def encode(self, x):
embedded = self.enc_embeddings(x)
init_s = [tf.zeros((x.shape[0], self.units)), tf.zeros((x.shape[0], self.units))]
o, h, c = self.encoder(embedded, initial_state=init_s)
return [h, c]
def inference(self, x):
s = self.encode(x)
done, i, s = self.decoder_eval.initialize(
self.dec_embeddings.variables[0],
start_tokens=tf.fill([x.shape[0], ], self.start_token),
end_token=self.end_token,
initial_state=s,
)
pred_id = np.zeros((x.shape[0], self.max_pred_len), dtype=np.int32)
for l in range(self.max_pred_len):
o, s, i, done = self.decoder_eval.step(
time=l, inputs=i, state=s, training=False)
pred_id[:, l] = o.sample_id
return pred_id
def train_logits(self, x, y, seq_len):
s = self.encode(x)
dec_in = y[:, :-1] # ignore <EOS>
dec_emb_in = self.dec_embeddings(dec_in)
o, _, _ = self.decoder_train(dec_emb_in, s, sequence_length=seq_len)
logits = o.rnn_output
return logits
def step(self, x, y, seq_len):
with tf.GradientTape() as tape:
logits = self.train_logits(x, y, seq_len)
dec_out = y[:, 1:] # ignore <GO>
loss = self.cross_entropy(dec_out, logits)
grads = tape.gradient(loss, self.trainable_variables)
self.opt.apply_gradients(zip(grads, self.trainable_variables))
return loss.numpy()
def train():
# get and process data
data = utils.DateData(4000)
print("Chinese time order: yy/mm/dd ", data.date_cn[:3], "\nEnglish time order: dd/M/yyyy ", data.date_en[:3])
print("vocabularies: ", data.vocab)
print("x index sample: \n{}\n{}".format(data.idx2str(data.x[0]), data.x[0]),
"\ny index sample: \n{}\n{}".format(data.idx2str(data.y[0]), data.y[0]))
model = Seq2Seq(
data.num_word, data.num_word, emb_dim=16, units=32,
max_pred_len=11, start_token=data.start_token, end_token=data.end_token)
# training
for t in range(1500):
bx, by, decoder_len = data.sample(32)
loss = model.step(bx, by, decoder_len)
if t % 70 == 0:
target = data.idx2str(by[0, 1:-1])
pred = model.inference(bx[0:1])
res = data.idx2str(pred[0])
src = data.idx2str(bx[0])
print(
"t: ", t,
"| loss: %.3f" % loss,
"| input: ", src,
"| target: ", target,
"| inference: ", res,
)
if __name__ == "__main__":
train()