词的向量化

词的向量化

自然语言理解的问题要转化为机器学习的问题,第一步肯定是要找一种方法把这些符号数学化。词向量是自然语言处理中常见的一个操作,是搜索引擎、广告系统、推荐系统等互联网服务背后常见的基础技术。它分为One-hot Representation、基于SVD(奇异值分解)的方法、基于迭代的方法——Word2vec等三类,并基于飞浆(paddlepaddle)平台实现。在展开介绍之前,先补充一些基本概念和数学知识。

重要概念

词项-文档矩阵,TF-IDF(term frequency times inverse document frequency):

原理:一个词对于一篇文章是否重要,体现在两个方面:
1)一个词项在一个文档中出现次数越多,他相对于文档的重要性就越大,这个指标就是我们的TF值,TF = 词项在该文档中出现的次数/这篇文档所有的词项数。
2)若词项在整个语料库中出现的次数越多,那么对于某一篇文章而言这个词就越没有意义,即越不重要。我们用逆文档频率idf来表示这个词在整个语料库中的重要程度,故,出现越多的词,idf值会越低,出现越少的词,idf值会越高 IDF = log(N/d) ,其中N为所有文档的总数;d为出现过某个单词的文档的总数。
在现实中,词项在语料中的频率往往呈指数型。一个常用词出现的次数往往是一个次常用词出现次数的几十倍,这样常用词的权重会非常低(如“的”这样的词N/D几乎可能几乎等于1),故我们对逆文档频率取对数log,如此,文档频率的差别就从乘数变成了加数级了。即,我们在计算词项在一个文档中的重要程度,要考虑以上两个因素,用TF-IDF值来共同衡量 :
TF-IDF = TF * IDF

数学概念:

1)马尔科夫假设是指,每个词出现的概率只跟它前面的少数几个词有关。比如,二阶马尔科夫假设只考虑前面两个词,相应的语言模型是三元模型。引入了马尔科夫假设的语言模型,也可以叫做马尔科夫模型,产生了n-gram语言模型

2)统计语言模型(Statistical Language Model)

假定S表示某个有意义的句子,由一连串特定顺序排列的词ω1,ω2,...,ωn组成,这里n是句子的长度。现在,我们想知道S在文本中出现的可能性,即S的概率P(S),则P(S)=P1,ω2,...,ωn)。利用条件概率的公式:

1、One-hot Representation

这种方法把每个词表示为一个很长的向量。这个向量的维度是词表大小,其中绝大多数元素为 0,只有一个维度的值为 1,这个维度就代表了当前的词。方法简单,然这种表示方法也存在一个重要的问题就是“词汇鸿沟”现象:任意两个词之间都是孤立的。从数学上解释,正交基的积为0。

参数:
    • input (Variable)-输入指数,最后维度必须为1
    • depth (scalar)-整数,定义one-hot维度的深度

返回:输入的one-hot表示

返回类型:变量(Variable)

例如:有如下三个特征属性:
性别:[“male”,”female”]
地区:[“Europe”,”US”,”Asia”]
浏览器:[“Firefox”,”Chrome”,”Safari”,”Internet Explorer”]。对于这样的特征,如果采用机器学习进行学习,通常我们需要对其进行特征数字化。

 import paddle.fluid as fluid
 import numpy
 label = fluid.layers.data(name="X", shape=[1], dtype="int64")
 one_hot_label = fluid.layers.one_hot(input=label, depth=10)

 结果解释:可以采用One-Hot编码的方式对上述的样本“[“male”,”US”,”Internet Explorer”]”编码,“male”则对应着[1,0],同理“US”对应着[0,1,0],“Internet Explorer”对应着[0,0,0,1]。则完整的特征数字化的结果为:[1,0,0,1,0,0,0,0,1]。这样导致的一个结果就是数据会变得非常的稀疏。

2、基于SVD(奇异值分解)的方法

 为了找到词嵌入(word embeddings,可以简单的理解为与词向量等价),可以首先遍历巨大的语料库,统计两个词之间某种形式的共现次数,并将统计值保存在一个矩阵XX中。然后应用奇异值分解:X=USVTX=USVT。我们使用UU中的每一行作为每个词的词嵌入

用这种方式生成的词向量保留了足够多的语法和语义信息,但也还是存在一些问题:

    • 矩阵维度经常变动,比如新词频繁加入。
    • 由于绝大部分词并不会共现,造成矩阵过于稀疏。
    • 矩阵维度一般很高,大约106×106106×106。
    • 并且难以合并新词或新的文档。对于一个m×nm×n矩阵,训练时的计算复杂度是O(mn2)O(mn2)
    • 由于词频的极度不平衡,需要对矩阵XX应用一些黑科技。

上述问题的一些解决方案:

    • 忽略the、he、has等虚词。
    • 应用一个斜坡窗口(ramp window,也就是说,不再对窗口内的所有词一视同仁)——比如,根据距离当前词的距离,对共现次数赋予相应的权重。
    • 使用皮尔逊相关系数(Pearson correlation coefficient),取代直接计数。

3、基于迭代的方法——Word2vec

  • 两个算法:continuous bag-of-words (CBOW)和skip-gram。CBOW是给定上下文环境时,预测该环境中间的那个中心词。Skip-gram正好相反,预测一个中心词上下文环境的分布,其图示如下。
  • 两个训练方法:负采样(negative sampling)和分层softmax。

3.1 n-gram model

n-gram模型也是统计语言模型中的一种重要方法,用n-gram训练语言模型时,一般用每个n-gram的历史n-1个词语组成的内容来预测第n个词。

3.2 Neural Network Language Model,NNLM 

通过一个线性映射和一个非线性隐层连接,同时学习了语言模型和词向量,即通过学习大量语料得到词语的向量表达,通过这些向量得到整个句子的概率。因所有的词语都用一个低维向量来表示,用这种方法学习语言模型可以克服维度灾难(curse of dimensionality)。

 

3.3 Continuous Bag of Words Model,CBOW

CBOW模型通过一个词的上下文(各N个词)预测当前词。当N=2时,模型如下图所示:




 CBOW的好处是对上下文词语的分布在词向量上进行了平滑,去掉了噪声,因此在小数据集上很有效。假设cc是目标值one-hot向量为1的位置的索引,损失函数:

3.4 Skip-gram model

而Skip-gram的方法中,用一个词预测其上下文,得到了当前词上下文的很多样本,因此可用于更大的数据集。

图4. Skip-gram模型




在条件概率相互独立下,定义损失函数:


4、案例分析及实现:

本配置的N-gram 神经网络模型结构如下图所示:


 

 PaddlePaddle提供了一个内置的方法fluid.layers.embedding,我们就可以直接用它来构造 N-gram 神经网络。

 

嵌入层(Embedding Layer)

该层用于查找由输入提供的id在查找表中的嵌入矩阵。查找的结果是input里每个ID对应的嵌入矩阵。 所有的输入变量都作为局部变量传入LayerHelper构造器

参数:
    • input (Variable)-包含IDs的张量
    • size (tuple|list)-查找表参数的维度。应当有两个参数,一个代表嵌入矩阵字典的大小,一个代表每个嵌入向量的大小。
    • is_sparse (bool)-代表是否用稀疏更新的标志
    • is_distributed (bool)-是否从远程参数服务端运行查找表
    • padding_idx (int|long|None)-如果为 None ,对查找结果无影响。如果padding_idx不为空,表示一旦查找表中找到input中对应的 padding_idz,则用0填充输出结果。如果 padding_idx<0padding_idx<0 ,在查找表中使用的 padding_idx 值为 size[0]+dimsize[0]+dim 。
    • param_attr (ParamAttr)-该层参数
    • dtype (np.dtype|core.VarDesc.VarType|str)-数据类型:float32,float_16,int等。

返回:张量,存储已有输入的嵌入矩阵。

返回类型:变量(Variable)

 1.训练代码,包括数据预处理

import paddle as paddle
import paddle.fluid as fluid
import six
import numpy
import math


EMBED_SIZE = 32      # embedding维度
HIDDEN_SIZE = 256    # 隐层大小
N = 5                # ngram大小,这里固定取5
BATCH_SIZE = 100     # batch大小
PASS_NUM = 100       # 训练轮数

use_cuda = False  # 如果用GPU训练,则设置为True
word_dict = paddle.dataset.imikolov.build_dict()
dict_size = len(word_dict)


def inference_program(words, is_sparse):

    embed_first = fluid.layers.embedding(
        input=words[0],
        size=[dict_size, EMBED_SIZE],
        dtype='float32',
        is_sparse=is_sparse,
        param_attr='shared_w')
    embed_second = fluid.layers.embedding(
        input=words[1],
        size=[dict_size, EMBED_SIZE],
        dtype='float32',
        is_sparse=is_sparse,
        param_attr='shared_w')
    embed_third = fluid.layers.embedding(
        input=words[2],
        size=[dict_size, EMBED_SIZE],
        dtype='float32',
        is_sparse=is_sparse,
        param_attr='shared_w')
    embed_fourth = fluid.layers.embedding(
        input=words[3],
        size=[dict_size, EMBED_SIZE],
        dtype='float32',
        is_sparse=is_sparse,
        param_attr='shared_w')

    concat_embed = fluid.layers.concat(
        input=[embed_first, embed_second, embed_third, embed_fourth], axis=1)
    hidden1 = fluid.layers.fc(input=concat_embed,
                              size=HIDDEN_SIZE,
                              act='sigmoid')
    predict_word = fluid.layers.fc(input=hidden1, size=dict_size, act='softmax')
    return predict_word


def train_program(predict_word):
    # 'next_word'的定义必须要在inference_program的声明之后,
    # 否则train program输入数据的顺序就变成了[next_word, firstw, secondw,
    # thirdw, fourthw], 这是不正确的.
    next_word = fluid.layers.data(name='nextw', shape=[1], dtype='int64')
    cost = fluid.layers.cross_entropy(input=predict_word, label=next_word)
    avg_cost = fluid.layers.mean(cost)
    return avg_cost

def optimizer_func():
    return fluid.optimizer.AdagradOptimizer(
        learning_rate=3e-3,
        regularization=fluid.regularizer.L2DecayRegularizer(8e-4))

def train(if_use_cuda, params_dirname, is_sparse=True):
    place = fluid.CUDAPlace(0) if if_use_cuda else fluid.CPUPlace()

    train_reader = paddle.batch(
        paddle.dataset.imikolov.train(word_dict, N), BATCH_SIZE)
    test_reader = paddle.batch(
        paddle.dataset.imikolov.test(word_dict, N), BATCH_SIZE)

    first_word = fluid.layers.data(name='firstw', shape=[1], dtype='int64')
    second_word = fluid.layers.data(name='secondw', shape=[1], dtype='int64')
    third_word = fluid.layers.data(name='thirdw', shape=[1], dtype='int64')
    forth_word = fluid.layers.data(name='fourthw', shape=[1], dtype='int64')
    next_word = fluid.layers.data(name='nextw', shape=[1], dtype='int64')

    word_list = [first_word, second_word, third_word, forth_word, next_word]
    feed_order = ['firstw', 'secondw', 'thirdw', 'fourthw', 'nextw']

    main_program = fluid.default_main_program()
    star_program = fluid.default_startup_program()

    predict_word = inference_program(word_list, is_sparse)
    avg_cost = train_program(predict_word)
    test_program = main_program.clone(for_test=True)

    sgd_optimizer = optimizer_func()
    sgd_optimizer.minimize(avg_cost)

    exe = fluid.Executor(place)

    def train_test(program, reader):
        count = 0
        feed_var_list = [
            program.global_block().var(var_name) for var_name in feed_order
        ]
        feeder_test = fluid.DataFeeder(feed_list=feed_var_list, place=place)
        test_exe = fluid.Executor(place)
        accumulated = len([avg_cost]) * [0]
        for test_data in reader():
            avg_cost_np = test_exe.run(
                program=program,
                feed=feeder_test.feed(test_data),
                fetch_list=[avg_cost])
            accumulated = [
                x[0] + x[1][0] for x in zip(accumulated, avg_cost_np)
            ]
            count += 1
        return [x / count for x in accumulated]

    def train_loop():
        step = 0
        feed_var_list_loop = [
            main_program.global_block().var(var_name) for var_name in feed_order
        ]
        feeder = fluid.DataFeeder(feed_list=feed_var_list_loop, place=place)
        exe.run(star_program)
        for pass_id in range(PASS_NUM):
            for data in train_reader():
                avg_cost_np = exe.run(
                    main_program, feed=feeder.feed(data), fetch_list=[avg_cost])

                if step % 10 == 0:
                    outs = train_test(test_program, test_reader)

                    print("Step %d: Average Cost %f" % (step, outs[0]))

                    # 整个训练过程要花费几个小时,如果平均损失低于5.8,
                    # 我们就认为模型已经达到很好的效果可以停止训练了。
                    # 注意5.8是一个相对较高的值,为了获取更好的模型,可以将
                    # 这里的阈值设为3.5,但训练时间也会更长。
                    if outs[0] < 5.8:
                        if params_dirname is not None:
                            fluid.io.save_inference_model(params_dirname, [
                                'firstw', 'secondw', 'thirdw', 'fourthw'
                            ], [predict_word], exe)
                        return
                step += 1
                if math.isnan(float(avg_cost_np[0])):
                    sys.exit("got NaN loss, training failed.")

        raise AssertionError("Cost is too large {0:2.2}".format(avg_cost_np[0]))

    train_loop()

预测代码:

def infer(use_cuda, params_dirname=None):
    place = fluid.CUDAPlace(0) if use_cuda else fluid.CPUPlace()

    exe = fluid.Executor(place)

    inference_scope = fluid.core.Scope()
    with fluid.scope_guard(inference_scope):
        # 使用fluid.io.load_inference_model获取inference program,
        # feed变量的名称feed_target_names和从scope中fetch的对象fetch_targets
        [inferencer, feed_target_names,
         fetch_targets] = fluid.io.load_inference_model(params_dirname, exe)

        # 设置输入,用四个LoDTensor来表示4个词语。这里每个词都是一个id,
        # 用来查询embedding表获取对应的词向量,因此其形状大小是[1]。
        # recursive_sequence_lengths设置的是基于长度的LoD,因此都应该设为[[1]]
        # 注意recursive_sequence_lengths是列表的列表
        data1 = numpy.asarray([[211]], dtype=numpy.int64)  # 'among'
        data2 = numpy.asarray([[6]], dtype=numpy.int64)  # 'a'
        data3 = numpy.asarray([[96]], dtype=numpy.int64)  # 'group'
        data4 = numpy.asarray([[4]], dtype=numpy.int64)  # 'of'
        lod = numpy.asarray([[1]], dtype=numpy.int64)

        first_word = fluid.create_lod_tensor(data1, lod, place)
        second_word = fluid.create_lod_tensor(data2, lod, place)
        third_word = fluid.create_lod_tensor(data3, lod, place)
        fourth_word = fluid.create_lod_tensor(data4, lod, place)

        assert feed_target_names[0] == 'firstw'
        assert feed_target_names[1] == 'secondw'
        assert feed_target_names[2] == 'thirdw'
        assert feed_target_names[3] == 'fourthw'

        # 构造feed词典 {feed_target_name: feed_target_data}
        # 预测结果包含在results之中
        results = exe.run(
            inferencer,
            feed={
                feed_target_names[0]: first_word,
                feed_target_names[1]: second_word,
                feed_target_names[2]: third_word,
                feed_target_names[3]: fourth_word
            },
            fetch_list=fetch_targets,
            return_numpy=False)

        print(numpy.array(results[0]))
        most_possible_word_index = numpy.argmax(results[0])
        print(most_possible_word_index)
        print([
            key for key, value in six.iteritems(word_dict)
            if value == most_possible_word_index
        ][0])

  5.存在问题及改进工作

存在问题:现在主要理解别人的实现,处于归纳、总结,还没有个人的创新

后续工作还有基于循环神经网络的语言模型的实现与tensorflow的对比实验

从目前我个人的使用情况,觉得tensorflow暂时要比paddlepaddle好用,主要是paddlepaddle真正的参考书籍少,还有功能方面前者要灵活一些。至于性能对比,后续工作将进行对比实验。

       

 



猜你喜欢

转载自www.cnblogs.com/bigdata-sanya/p/11241077.html