2021SC@SDUSC
经过分工之后,extract.py文件的分析由小组中的其他成员实现,在论文中提到了嵌入相似性的评价指标需要用doc2vec模型将文本转化为向量后计算余弦相似度,在extract.py文件只是使用了预先训练好的doc2vec模型,却并没有附相应的bin文件,我负责用wiki训练doc2vec模型以便进行后续的推进。我们先从word2vec模型入手,分析文本向量化。
一、关于文本的张量表示
将一段文本使用张量表示,其中一般将词汇表示成向量,称作词向量,再由各个词向量按顺序组成矩阵形成文本表示。这样处理能够使语言文本可以作为计算机处理程序的输入,进行接下来一系列工作的解析。
文本张量的表示方法:
- one-hot编码
- word2vec编码
- word embedding
其中one-hot编码将在后续博客对model.py进行分析时介绍。这里我们先分析一下one-hot编码的代码实现。
# 导入用于对象保存与加载的joblib
import joblib
# 导入keras中的词汇映射器Tokenizer
from tensorflow.keras.preprocessing.text import Tokenizer
# 假定vocab为语料集中不同词的集合
vocab={"春天","夏天","秋天","冬天"}
# 实例化词汇映射器对象
t=Tokenizer(num_words=None,char_level=False)
# 使用映射器拟合现有文本数据
t.fit_on_texts(vocab)
for token in vocab:
zero_list = [0]*len(vocab)
# 使用映射器转化现有文本数据,每个词汇对应1开始的自然数
# 返回样式如[[2]],取出其中的数字需要用[0][0]
token_inedx = t.texts_to_sequences([token])[0][0]-1
zero_list[token_inedx]=1
print(token,"的one-hot编码为:",zero_list)
# 使用joblib工具保存映射器,以便以后使用
tokenizer_path = "./Tokenizer"
joblib.dump(t,tokenizer_path)
在当前文件夹下生成了映射器Tokenizer,如图
这里首先初始化zero_ list为全为数字0的列表,长度为vocab所含元素的个数,得到token_index后令zero_list中对应token_index处的元素为1,实现one-hot编码。最后调用joblib的dump函数保存映射器。需要注意的是映射器对象(Tokenizer)t的texts_to_sequences方法(传入参数是token)返回的是列表的列表,如[[2]],要取出其中的index,需要用[0][0] 取出index,还要-1,因为每个词汇对应从1开始。
代码的运行结果如下:
图1:one-hot编码结果
之后加载保存的映射器对象调用相应函数便可直接对vocab的词进行编码。可以看出,one-hot操作简单、容易理解,但完全割裂了词与词之间的联系,而且在大语料集下,每个向量的长度过大,占据很大内存。我们更多地采用稠密向量的表示方法word2vec和word embedding。
word2vec是一种流行的将词汇表示成向量的无监督训练方法,该方法将构建神经网络模型,将网络参数作为词汇的向量表示,它包含 CBOW和skipgram两种训练模式。
CBOW(Continuous bag of words)模式:给定一段用于训练的文本预料,再选定某段长度(窗口)作为研究对象,使用上下文预测目标词汇。其大致原理见下图。
图2:CBOW模式图示
skipgram模式:给定一段用于训练的文本预料,再选定某段长度(窗口)作为研究对象,使用目标词汇预测上下文词汇。其大致原理见下图。
图3:skipgram模式图示
word2vec的两种模式的具体机器学习的数学原理这里我们就不再分析了,这里我们将分析用fasttext训练word2vec模型并保存训练好的模型的代码。
import fasttext
# 采用无监督训练test.txt文本
model = fasttext.train_unsupervised('test.txt')
# 得到单词anarchist的词向量
print(model.get_word_vector("anarchist"))
# 得到单词anarchist的近邻居,用来评估模型
print(model.get_nearest_neighbors("anarchist"))
# 保存训练好的模型为bin文件
model.save_model("text.bin")
test.txt文件是测试文本,选自从wiki上下载的数据的一小部分。大致内容如下。
图5:test.txt的内容
由于fasttext训练word2vec的过程较为简单,相关代码的分析已写在注释中,下附运行结果。
图6:训练结果
图7:向量编码结果
图8:get_nearest_neighbors结果
get_nearest_neighbors结果可以用来评估模型的好坏。找到与给定word较为相似的单词,返回的列表元素的第一个参数是相似度,第二个元素是给出的相似的单词,我们想要得到anarchist的近邻,得到了anarchismde等词,可以看到,由于训练模型的单词数较少,得到的邻近单词并不完全准确。最后将model保存为bin文件。
关于模型超参数的设定:在训练词向量中,我们可以设定许多常用的超参数调节模型效果,比如关于无监督训练模式可以选择“skipgram”或者“cbow”,默认为“skipgram”。
- 词嵌入维度dim:默认为100,随着语料库的增大,词嵌入的维度往往也要增大
- 数据循环次数epoch:默认为5,当数据集足够大,可能不需要这么多次
- 学习率lr:默认为0.05,一般选在[0.01,1]范围内
- 使用的线程数thread:默认为12个线程,一般与cpu核数相同
下面是一个设定相关参数的示例:
model = fasttext.train_unsupevised('test.txt',"cbow",dim=300,epoch=1,lr=0.05,thread=12)
我们刚分析了word2vec模型,但是word2vec只是基于词的维度进行”语义分析”的,而并不具有上下文的”语义分析”能力。Doc2Vec方法可以处理可变长度的文本,除了增加一个段落向量以外,这个方法几乎等同于 Word2Vec。和 Word2Vec 一样,该模型也存在两种方法:Distributed Memory(DM) 和 Distributed Bag of Words(DBOW)。DM 试图在给定上下文和段落向量的情况下预测单词的概率。在一个句子或者文档的训练过程中。DBOW 则在仅给定段落向量的情况下预测段落中一组随机单词的概率。
二、用wiki数据训练Doc2vec模型
项目网址:GitHub - jhlau/doc2vec: Python scripts for training/testing paragraph vectors
该项目使用gensim库训练doc2vec模型,首先需要下载wiki数据压缩包。网址:https://dumps.wikimedia.org/enwiki/latest/
将下载后的文件放到项目的文件下,运行process_wiki.py得到wiki_en.txt并用该文件训练train_model.py,项目还提供了测试infer_test.py.
项目包含文件如下:
模型训练完后便可得到预训练好的doc2vec.bin文件,见下图:
我们先分析process_wiki.py
import logging
import os.path
import sys
from gensim.corpora import WikiCorpus
if __name__ == '__main__':
program = os.path.basename(sys.argv[0]) # 获取程序名
logger = logging.getLogger(program) # 创建一个日志器
# 设置日志器输出格式
logging.basicConfig(format='%(asctime)s: %(levelname)s: %(message)s')
# 设置日志器输出级别,包括INFO, WARNING, ERROR
logging.root.setLevel(level=logging.INFO)
i = 0
# 输入文件路径inp
inp = 'enwiki-latest-pages-articles1.xml-p10p30302.bz2'
# 输出文件为wiki_en.txt
outp = 'wiki_en.txt'
# 通过utf-8编码对outp文件进行写操作
output = open(outp, 'w', encoding='utf-8')
wiki = WikiCorpus(inp, dictionary={})
for text in wiki.get_texts():
output.write(bytes(' '.join(text), 'utf-8').decode('utf-8') + '\n')
i = i + 1
if i % 10000 == 0:
logger.info('Saved ' + str(i) + ' articles')
# 写日志,记录了保存了多少文章
output.close()
# 写日志,文章记录结束
logger.info('Finished ' + str(i) + ' articles')
该文件主要实现了对下载的wiki语料的处理,将其解压为txt文件, 并在日志上记录保存的信息,结束时也有相应记录,这里主要是获取程序名然后依此创建了一个日志器,在规定了日志器的格式后记录信息。下一篇博客中我们将分析doc2vec的训练代码,也就是train_model.py的分析。
三、基于TaggedDocument进行语料预处理的gensim的Doc2Vec模型
下面我们来看一段基于TaggedDocument来进行语料处理,训练doc2vec模型并保存的示例,该段代码将详细演示模型的构建和训练过程,最后也将加载模型进行文本向量化,也可以寻找相近单词。
from gensim.models.doc2vec import Doc2Vec, TaggedDocument
from nltk.tokenize import word_tokenize
import nltk
import ssl
ssl._create_default_https_context = ssl._create_unverified_context
data = ["I love machine learning. Its awesome.",
"I love coding in python",
"I love building chatbots",
"they chat amagingly well"]
首先是导入相关库,我们将使用gensim中的Doc2Vec, TaggedDocument,并用word_tokenize实现分词。由于只是示例,训练模型的数据由列表形式给出,由列表的字符串构成,只是用引号分句。接下来是文本预处理。
tagged_data = [TaggedDocument(words=word_tokenize(_d.lower()), tags=[str(i)]) for i, _d in enumerate(data)]
for i, _d in enumerate(data):
# 展示分词结果
print (word_tokenize(_d.lower()))
这里是用enumerate(data)遍历data数据,取出标号和句子,对于句子,先转换为小写,再用word_tokenize分词,我们为了展示结果,用了一个print展示分词结果。
['i', 'love', 'machine', 'learning', '.', 'its', 'awesome', '.']
['i', 'love', 'coding', 'in', 'python']
['i', 'love', 'building', 'chatbots']
['they', 'chat', 'amagingly', 'well']
tagged_data如下,接下来我们设定模型的参数来初始化模型。
[TaggedDocument(words=['i', 'love', 'machine', 'learning', '.', 'its', 'awesome', '.'], tags=['0']),
TaggedDocument(words=['i', 'love', 'coding', 'in', 'python'], tags=['1']),
TaggedDocument(words=['i', 'love', 'building', 'chatbots'], tags=['2']),
TaggedDocument(words=['they', 'chat', 'amagingly', 'well'], tags=['3'])]
max_epochs = 100 # 迭代次数
vec_size = 20 # 文本向量大小
alpha = 0.025
model = Doc2Vec(vector_size=vec_size,
alpha=alpha,
min_alpha=0.00025,
min_count=1,
dm=1)
model.build_vocab(tagged_data)
Doc2Vec源码中参数如下,对于参数的详细说明,在源码文档里也给出了,这里不再赘述。
# 此处仅是函数头部分,不含实现
def __init__(self, documents=None, corpus_file=None, vector_size=100, dm_mean=None, dm=1, dbow_words=0, dm_concat=0,
dm_tag_count=1, dv=None, dv_mapfile=None, comment=None, trim_rule=None, callbacks=(),
window=5, epochs=10, shrink_windows=True, **kwargs)
#调用父类的构造方法部分
super(Doc2Vec, self).__init__(
sentences=corpus_iterable,
corpus_file=corpus_file,
vector_size=self.vector_size,
sg=(1 + dm) % 2,
null_word=self.dm_concat,
callbacks=callbacks,
window=window,
epochs=epochs,
shrink_windows=shrink_windows,
**kwargs,
)
# 循环迭代
for epoch in range(max_epochs):
print('iteration {0}'.format(epoch))
# 用tagged_data数据训练模型
model.train(tagged_data,
total_examples=model.corpus_count,
epochs=model.epochs)
# 下降学习率
model.alpha -= 0.0002
# 固定学习率,无下降
model.min_alpha = model.alpha
# 保存训练好的模型便于以后使用
model.save("d2v.model")
print("Model Saved")
iteration 0
iteration 1
iteration 2
iteration 3
……
iteration 99
接下来我们用处理好的数据训练指定参数的doc2vec模型,并保存训练好的模型,通过打印迭代展示了训练过程。之后加载训练好的模型,就可以得到指定句子的向量表示。
from gensim.models.doc2vec import Doc2Vec
model = Doc2Vec.load("d2v.model")
# 找到不属于训练数据的文档的向量
test_data = word_tokenize("I love chatbots".lower())
# 调用infer_vector得到向量
v1 = model.infer_vector(test_data)
print("V1_infer", v1)
结果如下:
V1_infer [-0.02847388 0.01452272 0.00052274 -0.02478364 0.02990589 -0.00382523
-0.024689 0.00994809 -0.01691242 -0.01769686 0.01749279 0.00280092
-0.02532934 -0.0132775 -0.01324665 0.01840881 -0.00253014 0.00109329
-0.02966217 -0.01578626]
加载训练好并保存的模型,对于test_data,指定新的文本,先进行分词,然后调用model的infer_vector函数就可以得到文本向量。
# 使用标签找到最相似的文档
similar_doc = model.dv.most_similar('1')
print(similar_doc)
# 使用标签在训练数据中找到doc向量,也就是在训练数据中打印索引1处的文档向量
print(model.dv['1'])
我们还可以调用most_similar方法找到与指定标签的文档最相似的文档,也可以找到我们训练过程中标记为1的文档。结果如下,首先是与标签1文档相似的文档,由文档标号和相似度的元组对组成,接下来是我们打标签1的文档的向量表示。
[('2', 0.9064693450927734), ('0', 0.7501382231712341), ('3', 0.7171176075935364)]
[-0.43908677 0.02307647 -0.46422952 0.09723601 0.5859765 -0.67359906
-0.60847586 -0.73937464 0.10058356 -0.7472883 0.29807988 0.47442636
-0.76889193 -0.72227216 -0.30133876 0.11609573 -0.04925952 -0.3589569
-0.6147125 0.20489198]
通过这个demo,我们大致了解了gensim中doc2vec模型的训练和加载使用过程,对于nlp而言,需要用doc2vec处理大量的语料,所以模型的训练就非常重要,需要不断调整参数,也要给出最合适的向量长度,之后我们也会用wiki数据训练doc2vec模型。
注:原计划的博客04未发送成功,04~06为我们深度关键词抽取的核心代码model.py的具体分析,具体分析了Encoder-Decoder和Seq2Seq的具体实现,07是对create_vocabulary.py创建词典的分析,08是对用wiki训练doc2vec的train_model.py的具体分析,请移步博客08。