3. 网络定义
定义skip-gram的网络结构,用于模型训练。在飞桨动态图中,对于任意网络,都需要定义一个继承自paddle.nn.layer
的类来搭建网络结构、参数等数据的声明。同时需要在forward
函数中定义网络的计算逻辑。值得注意的是,我们仅需要定义网络的前向计算逻辑,飞桨会自动完成神经网络的后向计算。
在skip-gram的网络结构中,使用的最关键的API是paddle.nn.Embedding函数,可以用其实现Embedding的网络层。通过查询飞桨的API文档,可以得到如下更详细的说明:
paddle.nn.Embedding(numembeddings, embeddingdim, paddingidx=None, sparse=False, weightattr=None, name=None)
该接口用于构建 Embedding 的一个可调用对象,其根据input中的id信息从embedding矩阵中查询对应embedding信息,并会根据输入的size (num_embeddings, embedding_dim)自动构造一个二维embedding矩阵。 输出Tensor的shape是在输入Tensor shape的最后一维后面添加了emb_size的维度。注:input中的id必须满足 0 =< id < size[0],否则程序会抛异常退出。
#定义skip-gram训练网络结构 #使用paddlepaddle的2.0.0版本 #一般来说,在使用paddle训练的时候,我们需要通过一个类来定义网络结构,这个类继承了paddle.nn.layer class SkipGram(nn.Layer): def __init__(self, vocab_size, embedding_size, init_scale=0.1): # vocab_size定义了这个skipgram这个模型的词表大小 # embedding_size定义了词向量的维度是多少 # init_scale定义了词向量初始化的范围,一般来说,比较小的初始化范围有助于模型训练 super(SkipGram, self).__init__() self.vocab_size = vocab_size self.embedding_size = embedding_size # 使用Embedding函数构造一个词向量参数 # 这个参数的大小为:[self.vocab_size, self.embedding_size] # 数据类型为:float32 # 这个参数的初始化方式为在[-init_scale, init_scale]区间进行均匀采样 self.embedding = Embedding( num_embeddings = self.vocab_size, embedding_dim = self.embedding_size, weight_attr=paddle.ParamAttr( initializer=paddle.nn.initializer.Uniform( low=-init_scale, high=init_scale))) # 使用Embedding函数构造另外一个词向量参数 # 这个参数的大小为:[self.vocab_size, self.embedding_size] # 这个参数的初始化方式为在[-init_scale, init_scale]区间进行均匀采样 self.embedding_out = Embedding( num_embeddings = self.vocab_size, embedding_dim = self.embedding_size, weight_attr=paddle.ParamAttr( initializer=paddle.nn.initializer.Uniform( low=-init_scale, high=init_scale))) # 定义网络的前向计算逻辑 # center_words是一个tensor(mini-batch),表示中心词 # target_words是一个tensor(mini-batch),表示目标词 # label是一个tensor(mini-batch),表示这个词是正样本还是负样本(用0或1表示) # 用于在训练中计算这个tensor中对应词的同义词,用于观察模型的训练效果 def forward(self, center_words, target_words, label): # 首先,通过self.embedding参数,将mini-batch中的词转换为词向量 # 这里center_words和eval_words_emb查询的是一个相同的参数 # 而target_words_emb查询的是另一个参数 center_words_emb = self.embedding(center_words) target_words_emb = self.embedding_out(target_words) # 我们通过点乘的方式计算中心词到目标词的输出概率,并通过sigmoid函数估计这个词是正样本还是负样本的概率。 word_sim = paddle.multiply(center_words_emb, target_words_emb) word_sim = paddle.sum(word_sim, axis=-1) word_sim = paddle.reshape(word_sim, shape=[-1]) pred = F.sigmoid(word_sim) # 通过估计的输出概率定义损失函数,注意我们使用的是binary_cross_entropy_with_logits函数 # 将sigmoid计算和cross entropy合并成一步计算可以更好的优化,所以输入的是word_sim,而不是pred loss = F.binary_cross_entropy_with_logits(word_sim, label) loss = paddle.mean(loss) # 返回前向计算的结果,飞桨会通过backward函数自动计算出反向结果。 return pred, loss
4. 网络训练
完成网络定义后,就可以启动模型训练。我们定义每隔100步打印一次Loss,以确保当前的网络是正常收敛的。同时,我们每隔10000步观察一下skip-gram计算出来的同义词(使用 embedding的乘积),可视化网络训练效果,代码如下:
# 开始训练,定义一些训练过程中需要使用的超参数 batch_size = 512 epoch_num = 3 embedding_size = 200 step = 0 learning_rate = 0.001 #定义一个使用word-embedding查询同义词的函数 #这个函数query_token是要查询的词,k表示要返回多少个最相似的词,embed是我们学习到的word-embedding参数 #我们通过计算不同词之间的cosine距离,来衡量词和词的相似度 #具体实现如下,x代表要查询词的Embedding,Embedding参数矩阵W代表所有词的Embedding #两者计算Cos得出所有词对查询词的相似度得分向量,排序取top_k放入indices列表 def get_similar_tokens(query_token, k, embed): W = embed.numpy() x = W[word2id_dict[query_token]] cos = np.dot(W, x) / np.sqrt(np.sum(W * W, axis=1) * np.sum(x * x) + 1e-9) flat = cos.flatten() indices = np.argpartition(flat, -k)[-k:] indices = indices[np.argsort(-flat[indices])] for i in indices: print('for word %s, the similar word is %s' % (query_token, str(id2word_dict[i]))) # 通过我们定义的SkipGram类,来构造一个Skip-gram模型网络 skip_gram_model = SkipGram(vocab_size, embedding_size) # 构造训练这个网络的优化器 adam = paddle.optimizer.Adam(learning_rate=learning_rate, parameters = skip_gram_model.parameters()) # 使用build_batch函数,以mini-batch为单位,遍历训练数据,并训练网络 for center_words, target_words, label in build_batch( dataset, batch_size, epoch_num): # 使用paddle.to_tensor,将一个numpy的tensor,转换为飞桨可计算的tensor center_words_var = paddle.to_tensor(center_words) target_words_var = paddle.to_tensor(target_words) label_var = paddle.to_tensor(label) # 将转换后的tensor送入飞桨中,进行一次前向计算,并得到计算结果 pred, loss = skip_gram_model( center_words_var, target_words_var, label_var) # 程序自动完成反向计算 loss.backward() # 程序根据loss,完成一步对参数的优化更新 adam.step() # 清空模型中的梯度,以便于下一个mini-batch进行更新 adam.clear_grad() # 每经过100个mini-batch,打印一次当前的loss,看看loss是否在稳定下降 step += 1 if step % 1000 == 0: print("step %d, loss %.3f" % (step, loss.numpy()[0])) # 每隔10000步,打印一次模型对以下查询词的相似词,这里我们使用词和词之间的向量点积作为衡量相似度的方法,只打印了5个最相似的词 if step % 10000 ==0: get_similar_tokens('movie', 5, skip_gram_model.embedding.weight) get_similar_tokens('one', 5, skip_gram_model.embedding.weight) get_similar_tokens('chip', 5, skip_gram_model.embedding.weight)
step 1000, loss 0.691 step 2000, loss 0.684 step 3000, loss 0.619 step 4000, loss 0.505 step 5000, loss 0.452 step 6000, loss 0.269