无监督关键短语的生成问题博客03--extract.py的分析

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。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值