中文NLP的第三步:获得词向量/词嵌入 word embeddings,基于 PaddleHub 实现(学习心得)

之前的步骤中,我们已经把句子进行了词语切分
中文NLP的第一步:分词,基于 PaddleHub 实现,绝对小白友好(学习心得)

然后把这些切好的词,根据现成的 词表,转化成了 ID
中文NLP的第二步:分词转词表ID,基于 PaddleHub 实现(学习心得)

第三步,把词语转化为 word embeddings

这里我们依然使用 PaddleHub 提供的 word2vec_skipgram 模型,模型的介绍可以见上一篇

程序实现:

import numpy as np
from scipy.spatial import distance
from paddlehub.reader.tokenization import load_vocab
import paddle.fluid as fluid
import paddlehub as hub

raw_data = [
    ["今天的天气不错啊", "明天是个下雨天吧"],
    ["我在谷歌工作", "你在百度上班"],
    ["国王", "王后"],
    ["这个餐馆的服务还行,但是菜的味道实在是不怎么样","我不喜欢这家餐馆"],
    ["这个餐馆的服务还行,但是菜的味道实在是不怎么样","我好喜欢这家餐厅"]
]

lac = hub.Module(name="lac")

processed_data = []
for text_pair in raw_data:
    inputs = {"text" : text_pair}
    results = lac.lexical_analysis(data=inputs, use_gpu=True, batch_size=2)
    data = []
    for result in results:
        data.append(" ".join(result["word"]))
    processed_data.append(data)

print(processed_data)


# 这是把 中文词语 转化为 词表 中对应 ID 的函数
def convert_tokens_to_ids(vocab, text):  # 输入为词表,和要转化的 text
    wids = []  # 初始化一个空的集合,用于存放输出
    tokens = text.split(" ")  # 将传入的 text 用 空格 做分割,变成 词语字符串 的列表
    for token in tokens:  # 每次从列表里取出一个 词语
        wid = vocab.get(token, None)
        if not wid:
            wid = vocab["unknown"]
        wids.append(wid)
    return wids


module = hub.Module(name="word2vec_skipgram")  # 实例化 word2vec_skipgram 模型
inputs, outputs, program = module.context(trainable=False)
# 利用 模型实例化后的 API,把接口导出来,这里有 3 个接口,分别是 输入,输出,程序主体

vocab = load_vocab(module.get_vocab_path())  # 获得 词表 字典

word_ids = inputs["word_ids"]
# 输入接口,是字典, 字典下面的 key 需要有 "word_ids" 字段,对应的 value 是 组成句子的 词语 的 ID 列表
# 生成一个 paddle 框架下面的变量 word_ids

embedding = outputs["word_embs"]
# 输出接口,也是字典,里面有 key 为 "word_embs",对应的 value 是表示原输入文本各单词对应的预训练 word embedding
# 生成一个 paddle 框架下的变量 embedding

place = fluid.CPUPlace()  # 设备描述符,表示 CPU 设备
exe = fluid.Executor(place)  # 将执行器的类 进行实例化
feeder = fluid.DataFeeder(feed_list=[word_ids], place=place)
# 这里 实例化一个 DataFeeder 类,负责将reader(读取器)返回的数据转成一种特殊的数据结构,使它们可以输入到 Executor 和 ParallelExecutor 中。
# reader 通常返回一个 minibatch 条目 列表。
# 在列表中每一条目都是一个样本(sample),它是由具有一至多个特征的列表或元组组成的。
# 这里我们只有一个输入变量,那就是 word_ids,所以相当于 feed_list 中只有一个元素(而这个变量后面是需要用feed进行迭代的)

## feed_list (list) – 向模型输入的变量表或者变量表名
## place (Place) – place表明是向GPU还是CPU中输入数据。如果想向GPU中输入数据, 请使用 fluid.CUDAPlace(i) (i 代表 the GPU id);如果向CPU中输入数据, 请使用 fluid.CPUPlace()
## program (Program) – 需要向其中输入数据的Program。如果为None, 会默认使用 default_main_program()。 缺省值为None

for item in processed_data:
    text_a = convert_tokens_to_ids(vocab, item[0])  # 获得组成句子的 词语 的 ID 列表

    text_b = convert_tokens_to_ids(vocab, item[1])

    vecs_a, = exe.run(
        program,
        feed=feeder.feed([[text_a]]),
        fetch_list=[embedding.name],
        return_numpy=False)
    # 运行 执行器 exe,用的是 fluid.Executor() 下的 run 方法,即运行这个执行器
    # program 就是 word2vec_skipgram 该 Module 计算图的 Program

    # DataFeeder 类下面的 feed(iterable) 方法就是来进行具体 转化操作的
    # 根据feed_list(数据输入表)和iterable(可遍历的数据)提供的信息,将输入数据转成一种特殊的数据结构,使它们可以输入到 Executor 和 ParallelExecutor 中。
    # 这里我们 给变量 word_ids 进行迭代,feed([[text_a]]) 中的 外层[] 针对的是每次的迭代,这里只有[text_a]这一个元素,相当于只进行了一次迭代
    # feed([[text_a]]) 内层[] 内的元素表示的是对 多个变量 进行迭代,这里我们就一个变量 word_ids,所以 只有 text_a 这一个元素

    # fetch_list 这里用 [embedding.name],或者 [embedding] 都可以,这里我们就迭代了一次,其实最终就一个元素会返回给 vecs_a,如果有多个元素,那最终 vecs_a 会多一个维度

    ## 参数: iterable (list|tuple) – 要输入的数据
    ## 返回: 转换结果。返回类型: dict

    ## program (Program|CompiledProgram) – 该参数为被执行的Program或CompiledProgram,如果未提供该参数,即该参数为None,在该接口内,main_program将被设置为fluid.default_main_program()。默认为:None。
    ## feed (list|dict) – 该参数表示模型的输入变量。如果是单卡训练,feed 为 dict 类型,如果是多卡训练,参数 feed 可以是 dict 或者 list 类型变量,如果该参数类型为 dict ,feed中的数据将会被分割(split)并分送给多个设备(CPU/GPU),即输入数据被均匀分配到不同设备上;如果该参数类型为 list ,则列表中的各个元素都会直接分别被拷贝到各设备中。默认为:None。
    ## fetch_list (list) – 该参数表示模型运行之后需要返回的变量。默认为:None。
    ## return_numpy (bool) – 该参数表示是否将返回返回的计算结果(fetch list中指定的变量)转化为numpy;如果为False,则每个变量返回的类型为LoDTensor,否则返回变量的类型为numpy.ndarray。默认为:True。
    ## 返回:返回fetch_list中指定的变量值,返回的类型为列表

    vecs_a = np.array(vecs_a)
    # 我们得到的 vecs_a 是一个 LoDTensor,无法直接使用,需要转化成 numpy 数组
    # 在这个 LoDTensor 中包含了维度信息 dim:11,128 , 还有数据列表 data,所以转化成 array 后的 shape 就是 (11,128)
    # 其中 11 表示的是 11 个词语,128 代表的是每个词语的 128 维的词嵌入向量

    # LoDTensor是一个具有LoD(Level of Details)信息的张量(Tensor),可用于表示变长序列
    # LoDTensor可以通过 np.array(lod_tensor) 方法转换为numpy.ndarray。

    vecs_b, = exe.run(
        program,
        feed=feeder.feed([[text_b]]),
        fetch_list=[embedding.name],
        return_numpy=False)
    vecs_b = np.array(vecs_b)

    sent_emb_a = np.sum(vecs_a, axis=0)  # 把句子所有 词语 的 word embedding 对应的特征值求和
    sent_emb_b = np.sum(vecs_b, axis=0)

    cos_sim = 1 - distance.cosine(sent_emb_a, sent_emb_b)  # 比较两个句子的余弦相似度,直接用 scipy.spatial 即可

    print("text_a: %s; text_b: %s; cosine_similarity: %.5f" %
          (item[0], item[1], cos_sim))

运行结果:

text_a: 今天 的 天气 不错 啊; text_b: 明天 是 个 下雨天 吧; cosine_similarity: -0.10378
text_a: 我 在 谷歌 工作; text_b: 你 在 百度 上班; cosine_similarity: 0.36045
text_a: 国王; text_b: 王后; cosine_similarity: 0.02589
text_a: 这个 餐馆 的 服务 还行 , 但是 菜 的 味道 实在 是 不 怎么样; text_b: 我 不 喜欢 这家 餐馆; cosine_similarity: 0.18525
text_a: 这个 餐馆 的 服务 还行 , 但是 菜 的 味道 实在 是 不 怎么样; text_b: 我 好喜欢 这家 餐厅; cosine_similarity: -0.12159

结果分析:

词语 转化成的 word embeddings 代表的是这个词 和所有 别的词在 高维空间(特征向量维度)中的 相对关系

换句话说,我们根据这个 word embeddings ,可以通过词语间的相关性,侧面地去理解 “词语” 的含义(特征)

所以在上面的案例中,是一个最简单的应用:语意相似度计算

可以看到最后两组语句的对比:正面评价之间的相关性是正的,而正面评价和负面评价的相关性就是负的

实际应用中虽然不会累加做余弦相似度这么粗暴来做,但是逻辑理念是类似的,我们可以把这种语义的分析应用到比如网站评价的分析之类的场景中


关于 PaddleHub 的一些想法:

对于刚入门的小白来说,PaddleHub 无疑是让你可以快速了解深度学习可以“做到什么”的有力工具

因为你不需要了解背后的原理,不需要去搭建模型框架,不需要整理获取大量的训练数据,不需要繁复的背后劳动~~,你甚至连Python都只要会一丢丢基础即可~~

但是局限也是非常明显的,那就是这东西只能在最普及和应用最广泛的几类任务中使用,比如分类任务(当然啦,很多任务其实都可以转化为分类任务,所以某种程度上说,能应用的地方还是不少的)

如果你想要把数据按照自己的方式进行流转处理,比如把 word embeddings 的矩阵用到自己搭建的神经网络某些层中,又或是你想要按照自己的方式进行实时处理,那 PaddleHub 的模块封装就不是你的福音,而是你的束缚

所以建议大家可以看到一些效果以后,回头再自己搭一遍模型,从头来一遍,会对深度学习理念有更深的理解,也会更适合从事研究领域的人(虽然这个过程对于新手来说是非常痛苦而且要耗费大量时间)

你可能感兴趣的:(NLP,PaddlePaddle)