一、基本概念

自然语言处理(Natural Language Processing,简称NLP)是人工智能和语言学领域的一个分支,它涉及到计算机和人类(自然)语言之间的相互作用。它的主要目标是让计算机能够理解、解释和生成人类语言的数据。NLP结合了计算机科学、人工智能和语言学的技术和理论,旨在填补人与机器之间的交流隔阂。

1. 自然语言处理的基本介绍

1.1 与语言相关的概念

在定义NLP之前,先了解几个相关概念:

  • 语言(Language):是人类用于沟通的一种结构化系统,可以包括声音、书写符号或手势。

  • 自然语言(Natural Language):是指自然进化中通过使用和重复,无需有意计划或预谋而形成的语言。

  • 计算语言学(Computational Linguistics):是语言学和计算机科学之间的跨学科领域,它包括:

    a.计算机辅助语言学(Computer-aided Linguistics):利用计算机研究语言的学科,主要为语言学家所实践。

    b.自然语言处理(NLP):使计算机能够解决以自然语言表达的数据问题的技术,主要由工程师和计算机科学家实践。

NLP的研究范围广泛,包括但不限于语言理解(让计算机理解输入的语言)、语言生成(让计算机生成人类可以理解的语言)、机器翻译(将一种语言翻译成另一种语言)、情感分析(分析文本中的情绪倾向)、语音识别和语音合成等。

在中文环境下,自然语言处理的定义和应用也与英文环境相似,但需要考虑中文的特殊性,如中文分词、中文语法和语义分析等,因为中文与英文在语言结构上有很大的不同,这对NLP技术的实现提出了特殊的挑战。自然语言处理使计算机不仅能够理解和解析人类的语言,还能在一定程度上模仿人类的语言使用方式,进行有效的沟通和信息交换。

1.2 为什么使用NLP

  • 每种动物都有自己的语言,机器也是!

  • 自然语言处理(NLP)就是在机器语言和人类语言之间沟通的桥梁,以实现人机交流的目的。人类通过语言来交流,猫通过喵喵叫来交流。

  • 机器也有自己的语言,那就是数字信息。

  • NLP 就是让机器学会处理我们的语言!

  • 人类和机器之间是否可以通过“翻译”的方式来直接交流呢?

2. NLP的应用方向

  • 多种多样的NLP研究方向,paperwithcode网站上总结的有308项。

2.1 自然语言理解

典型的自然语言理解(NLU)任务包括:

  • 情感分析:对给定的文本输入,在给定的选项范围内分析文本的情绪是正面还是负面;

  • 文本分类:对给定的文本输入,在给定的选项范围内对文本进行二分类或多分类;

  • 信息检索:搜索引擎依托于多种技术,如网络爬虫技术、检索排序技术、网页处理技术、大数据处理技术、自然语言处理技术等,为信息检索用户提供快速、高相关性的信息服务;

  • 抽取式阅读理解:对给定的文本输入,用文本中的内容回答问题;

  • 语义匹配:对给定的两个文本输入,判断是否相似;

  • 自然语言推理:对给定的两个文本输入,判断是蕴涵、矛盾还是无关;

  • 命名实体识别:对给定的文本输入,返回含有命名实体及其对应标签的映射,例如{'糖尿病':'疾病'};

  • 文本摘要:对给定的文本输入,用文本中的内容对文本进行摘要。

2.2 自然语言转换

自然语言转换(NLT)任务包括但不限于:

  • 机器翻译:将一种自然语言转换为另一种自然语言,包括从源语言到目标语言的文本或语音的转换;

    机器学习模型目前常用seq2seq模型来实现,由Encoder和Decoder组成。

  • 非抽取式阅读理解:接受给定文本的输入,能够理解自然语言问题,并回答问题;

  • 文本风格转换:将文本从一种风格转换为另一种风格,如将正式文本转换为非正式文本;

  • 语音识别:将人类的语音转换为文本,用于语音指令、口述文本、会议记录等。

    苹果的用户肯定都体验过,就是典型的语音识别,微信里有一个功能是"文字语音转文字" ,也利用了语音识别,最近流行的智能音箱就是以语音识别为核心的产品,比较新款的汽车基本都有语音控制的功能

  • 意图改写:对给定的文本输入,将原始文本中的意图或核心信息重新表述,以不同的词汇和句式表达相同的意思,同时保持原意的准确性和完整性;

3. NLP基础概念

下面总结一些NLP中常用的概念名词,便于理解任务。

(1)词表/词库(Vocabulary):文本数据集中出现的所有单词的集合。

(2)语料库(Corpus):用于NLP任务的文本数据集合,可以是大规模的书籍、文章、网页等。

(3)词嵌入(Word Embedding):将单词映射到低维连续向量空间的技术,用于捕捉单词的语义和语法信息。

(4)停用词(Stop Words):在文本处理中被忽略的常见单词,如"a"、"the"、"is"等,它们通常对文本的意义贡献较 小。

(5)分词(Tokenization):将文本分割成一个个单词或标记的过程,为后续处理提供基本的单位。

(6) 词频(Term Frequency):在给定文档中,某个单词出现的次数。

(7)逆文档频率(Inverse Document Frequency):用于衡量一个单词在整个语料库中的重要性,是将词频取倒数并取 对数的值。

(8) TF-IDF(Term Frequency-Inverse Document Frequency):一种常用的文本特征表示方法,综合考虑了词频和逆文档频率。

(9) 词袋模型(Bag of Words):将文本表示为一个单词的集合,忽略了单词的顺序和语法结构。

(10)N-gram:连续的N个单词构成的序列,用于捕捉文本中的局部特征和上下文信息。

(11)序列:指的是一个按顺序排列的元素集合。这些元素可以是字符、单词、句子,甚至更抽象的结构。序列的每个元素都有特定的顺序和位置,这意味着它们不能随意重排,否则会影响其意义或功能。

序列的常见类型

  1. 字符序列

    • 一个字符串就是一个字符序列,每个字符按顺序排列。

    • 例子:"hello" 是一个由 hello 组成的字符序列。

  2. 单词序列

    • 一句话可以看作是一个单词序列,每个单词按照一定顺序排列。

    • 例子:"I love NLP" 是一个由 IloveNLP 组成的单词序列。

  3. 时序数据

    • 在时间序列中,元素是按时间顺序排列的,常用于预测问题。

    • 例子:股票价格数据可以看作是随时间变化的数值序列。

  4. 语音序列

    • 在语音处理任务中,语音信号可以被分解为按时间顺序排列的帧序列(特征向量序列)。

  5. 其他序列

    • 序列还可以表示一些更抽象的结构,比如DNA序列(由碱基组成的序列)、事件序列等。

二、NLP中的特征工程

特征是数据中抽取出来的对结果预测有用的信息。

在自然语言处理(NLP)中,特征工程是指将文本数据转换为适合机器学习模型使用的数值表示的过程。文本是一种非结构化数据,机器学习模型无法直接处理,因此必须通过特征工程来提取有用的信息。

通过特征工程能让机器学习到文本数据中的一些特征,比如词性、语法、相似度等

比如在英文中可以学到

我希望得到的特征工程训练后的结果,从向量空间里面的分布角度看

1. 词向量的引入

词向量(Word Vector)是对词语义或含义的数值向量表示,包括字面意义和隐含意义。 词向量可以捕捉到词的内涵,将这些含义结合起来构成一个稠密的浮点数向量,这个稠密向量支持查询和逻辑推理。

词向量也称为词嵌入,其英文均可用 Word Embedding,是自然语言处理中的一组语言建模和特征学习技术的统称,其中来自词表的单词或短语被映射为实数的向量,这些向量能够体现词语之间的语义关系。从概念上讲,它涉及从每个单词多维的空间到具有更低维度的连续向量空间的数学嵌入。当用作底层输入表示时,单词和短语嵌入已经被证明可以提高 NLP 任务的性能,例如文本分类、命名实体识别、关系抽取等。

词嵌入实际上是一类技术,单个词在预定义的向量空间中被表示为实数向量,每个单词都映射到一个向量。举个例子,比如在一个文本中包含“猫”“狗”“爱情”等若干单词,而这若干单词映射到向量空间中,“猫”对应的向量为(0.1 0.2 0.3),“狗”对应的向量为(0.2 0.2 0.4),“爱情”对应的映射为(-0.4 -0.5 -0.2)(本数据仅为示意)。像这种将文本X{x1,x2,x3,x4,x5……xn}映射到多维向量空间Y{y1,y2,y3,y4,y5……yn },这个映射的过程就叫做词嵌入。 ​ 之所以希望把每个单词都变成一个向量,目的还是为了方便计算,比如“猫”,“狗”,“爱情”三个词。对于我们人而言,我们可以知道“猫”和“狗”表示的都是动物,而“爱情”是表示的一种情感,但是对于机器而言,这三个词都是用0,1表示成二进制的字符串而已,无法对其进行计算。而通过词嵌入这种方式将单词转变为词向量,机器便可对单词进行计算,通过计算不同词向量之间夹角余弦值cosine而得出单词之间的相似性。

此外,词嵌入还可以做类比,比如:v(“国王”)-v(“男人”)+v(“女人”)≈v(“女王”),v(“中国”)+v(“首都”)≈v(“北京”),当然还可以进行算法推理。有了这些运算,机器也可以像人一样“理解”词汇的意思了。

词向量的发展历程

词向量作为词的分布式表示方法,经过多年研究,产生了非常多的词向量的生成模型。

2. 传统NLP中的特征工程

2.1 独热编码 one - hot

独热编码(One-Hot Encoding) 是一种常见的特征表示方法,通常用于将离散的类别型数据转换为数值型表示,以便输入到机器学习模型中。它的特点是将每个类别表示为一个向量,在该向量中,只有一个元素为1,其余元素全部为0。

One-Hot Encoding 的工作原理

假设你有一个包含以下类别的分类任务:

  • 红色(red)

  • 绿色(green)

  • 蓝色(blue)

要将这些类别转换为 One-Hot 编码,我们会为每个类别创建一个独特的二进制向量:

  • 红色:[1, 0, 0]

  • 绿色:[0, 1, 0]

  • 蓝色:[0, 0, 1]

例如,如果输入数据是“红色”,在使用 One-Hot 编码后,它将被表示为 [1, 0, 0]

在NLP当中

  • Time flies like an arrow.

  • Fruit flies like a banana.

构成词库{time, fruit, flies, like, a, an, arrow, banana}

banana的one-hot表示就是:[0,0,0,0,0,0,0,1],"like a banana” 的one-hot表示就是:[0,0,0,1,1,0,0,1]。

2.2 词频-逆文档频率(TF-IDF)

(1)词频

将文本中的每个单词视为一个特征,并将文本中每个单词的出现次数除以该单词在所有文档中的出现次数,以调整单词的权重。

注意:在计算词频(TF)时,分母是文档中的总词数,而不考虑每个词是否重复。这意味着无论词是否重复,分母始终是文档中所有词的数量总和。

(2)逆文档频率(Inverse Document Frequency, IDF)

逆文档频率用来衡量一个词在整个文档集合(语料库)中的重要性。它的目的是降低那些在很多文档中频繁出现的词的权重,例如“the”、“is”这种常见词,或者低频罕见词tetrafluoroethylene(四氟乙烯)。计算公式如下:

(3)TF-IDF 计算

最终,TF-IDF 是将 TF 和 IDF 相乘得出的结果,公式如下:

通过这个方法,一个词在特定文档中出现的频率越高(TF高),并且在整个语料库中出现得越少(IDF高),它的 TF-IDF 值就越高。这样可以使模型更加关注那些在某篇文档中特别重要但不常见的词。

2.3 n-grams

n-grams 是特征工程中的一种技术,它通过将文本中的连续 n 个词(或字符)组合起来,形成一个短语来捕捉文本中的局部上下文信息。n 可以为 1、2、3 等,具体取决于希望捕捉的上下文范围。

什么是 n-grams?

  • 1-gram(Unigram):每个单独的词作为一个单位。例如,"I love NLP" 的 1-gram 是 ["I", "love", "NLP"]

  • 2-grams(Bigram):相邻的两个词组合成一个短语。例如,"I love NLP" 的 2-grams 是 ["I love", "love NLP"]

  • 3-grams(Trigram):相邻的三个词组合成一个短语。例如,"I love NLP" 的 3-grams 是 ["I love NLP"]

n-grams 的作用

使用 n-grams 可以捕捉词之间的局部上下文关系。例如,1-gram 只关心词的独立出现频率,而 bigram 和 trigram 能捕捉到词之间的顺序关系。例如,bigram "love NLP" 表示词 "love" 和 "NLP" 是一起出现的,这种信息在建模中会比仅仅知道 "love" 和 "NLP" 出现频率更有价值。

n-grams 的示例

假设句子为 "I love NLP and machine learning":

  • 1-gram(Unigram): ["I", "love", "NLP", "and", "machine", "learning"]

  • 2-grams(Bigram): ["I love", "love NLP", "NLP and", "and machine", "machine learning"]

  • 3-grams(Trigram): ["I love NLP", "love NLP and", "NLP and machine", "and machine learning"]

通过这些 n-grams,模型可以捕捉到词与词之间的局部依赖关系。

n-gramsTF-IDF 相结合是文本特征工程中非常常见的做法,它不仅能够捕捉词与词之间的局部关系,还能通过 TF-IDF 来衡量这些短语在整个语料库中的重要性。结合的过程基本上是先生成 n-grams,然后对这些 n-grams 计算 TF-IDF 权重。

结合 n-grams 与 TF-IDF 的步骤:

  1. 生成 n-grams:首先从文本中生成 n-grams(n 可以是 1, 2, 3 等)。这些 n-grams 就像是词的组合,通常使用 CountVectorizer 或类似的工具生成。

  2. 计算词频 (TF):统计每个 n-gram 在文本中出现的频率。

  3. 计算逆文档频率 (IDF):计算 n-gram 在所有文档中出现的频率,稀有的 n-grams 会得到较高的权重,而常见的 n-grams 权重较低。

  4. 计算 TF-IDF:将每个 n-gram 的 TF 和 IDF 相乘,得到 TF-IDF 权重,表示该 n-gram 对特定文本的重要性。

注意:当使用 2-grams 时,I lovelove NLP 被看作是两个单独的特征,总共有两个特征(总特征数 = 2)。

传统NLP中的特征工程缺点

3. 深度学习中NLP的特征输入

深度学习使用分布式单词表示技术(也称词嵌入表示),通过查看所使用的单词的周围单词(即上下文)来学习单词表示。这种表示方式将词表示为一个粘稠的序列,在保留词上下文信息同时,避免维度过大导致的计算困难。

3.1 稠密编码(特征嵌入)

稠密编码(Dense Encoding),在机器学习和深度学习中,通常指的是将离散或高维稀疏数据转化为低维的连续、密集向量表示。这种编码方式在特征嵌入(Feature Embedding)中尤为常见。

稠密向量表示:不再以one-hot中的一维来表示各个特征,而是把每个核心特征(词,词性,位置等)都嵌入到d维空间中,并用空间中的一个向量表示。通常空间维度d远小于每个特征的样本数(40000的词表,100/200维向量)。嵌入的向量(每个核心特征的向量表示)作为网络参数与神经网络中的其他参数一起被训练。

特征嵌入(Feature Embedding)

特征嵌入,也成为词嵌入,是稠密编码的一种表现形式,目的是将离散的类别、对象或其他类型的特征映射到一个连续的向量空间。通过这种方式,嵌入后的向量可以捕捉不同特征之间的语义关系,并且便于在后续的机器学习模型中使用。

特点:

  • 低维度:相比稀疏表示(如独热编码),稠密编码的维度更低,能够减少计算和存储成本。

  • 语义相似性:嵌入向量之间的距离(如欧氏距离或余弦相似度)可以表示这些对象之间的语义相似性。

  • 可微学习:嵌入表示通常通过神经网络进行学习,并且通过反向传播算法进行优化。

3.2 词嵌入算法

3.2.1 Embedding Layer

由于缺乏更好的名称,Embedding Layer是与特定自然语言处理上的神经网络模型联合学习的单词嵌入。该嵌入方法将清理好的文本中的单词进行one hot编码(独热编码),向量空间的大小或维度被指定为模型的一部分,例如50、100或200维。向量以小的随机数进行初始化。Embedding Layer用于神经网络的前端,并采用反向传播算法进行监督。

那么怎么得到词向量?

比如我们的目标是希望神经网络发现如下这样的规律:已知一句话的前几个字,预测下一个字是什么,于是有了NNLM 语言模型搭建的网络结构图:

具体怎么实施呢?先用最简单的方法来表示每个词,one-hot 表示为︰

dog=(0,0,0,0,1,0,0,0,0,...);cat=(0,0,0,0,0,0,0,1,0,...) ;eat=(0,1,0,0,0,0,0,0,0,...)

映射之后的向量层如果单独拿出来看,还有办法找到原本的词是什么吗?

One-hot表示法这时候就作为一个索引字典了,可以通过映射矩阵对应到具体的词向量。

词嵌入层的使用

词嵌入层首先会根据输入的词的数量构建一个词向量矩阵,例如: 我们有 100 个词,每个词希望转换成 128 维度的向量,那么构建的矩阵形状即为: 100*128,输入的每个词都对应了一个该矩阵中的一个向量。

在 PyTorch 中,我们可以使用 nn.Embedding 词嵌入层来实现输入词的向量化。接下来,我们将会学习如何将词转换为词向量,其步骤如下:

  1. 先将语料进行分词,构建词与索引的映射,我们可以把这个映射叫做词表,词表中每个词都对应了一个唯一的索引;

  2. 然后使用 nn.Embedding 构建词嵌入矩阵,词索引对应的向量即为该词对应的数值化后的向量表示。

例如,我们的文本数据为: "北京冬奥的进度条已经过半,不少外国运动员在完成自己的比赛后踏上归途。",接下来,我们看下如何使用词嵌入层将其进行转换为向量表示,步骤如下:

  1. 首先,将文本进行分词;

  2. 然后,根据词构建词表;

  3. 最后,使用嵌入层将文本转换为向量表示。

nn.Embedding 对象构建时,最主要有两个参数:

  1. num_embeddings 表示词的数量

  2. embedding_dim 表示用多少维的向量来表示每个词

    nn.Embedding(num_embeddings=10, embedding_dim=4)

    接下来,我们就实现下刚才的需求:

    import torch
    import torch.nn as nn
    import jieba
    
    if __name__ == '__main__':
    
        text = '北京冬奥的进度条已经过半,不少外国运动员在完成自己的比赛后踏上归途。'
    
        # 1. 文本分词
        words = jieba.lcut(text)
        print('文本分词:', words)
    
        # 2. 构建词表
        index_to_word = {}
        word_to_index = {}
    
        # 分词去重并保留原来的顺序
        unique_words = list(set(words))
        for idx, word in enumerate(unique_words):
            index_to_word[idx] = word
            word_to_index[word] = idx
    
        # 3. 构建词嵌入层
        # num_embeddings: 表示词表词的总数量
        # embedding_dim: 表示词嵌入的维度
        embed = nn.Embedding(num_embeddings=len(index_to_word), embedding_dim=4)
    
        # 4. 文本转换为词向量表示
        print('-' * 82)
        for word in words:
            # 获得词对应的索引
            idx = word_to_index[word]
            # 获得词嵌入向量
            word_vec = embed(torch.tensor(idx))
            # '%3s\t': 这是一个格式化字符串。%s 表示一个占位符,用于字符串类型的参数。3 表示至少分配3个字符的宽度给字符串,如果字符串短于3个字符,它会在左侧填充空格以保持对齐。\t 表示制表符,它会在输出中添加一个水平制表位,通常用于列之间的对齐。
            print('%3s\t' % word, word_vec)

    输出的结果:

归纳:

  • Embedding类的本质是一个大小为 (num_embeddings, embedding_dim) 的矩阵,每一行是某个词汇的嵌入向量。

  • 通过索引,可以高效地从这个矩阵中提取对应词汇的向量表示,因为 nn.Embedding 在内部通过索引直接查找矩阵中的行,这种操作非常快速且方便。

练习

假设现在有语料库sentences = ["i like dog", "i love coffee", "i hate milk", "i do nlp"] 通过词嵌入层算法NNLM模型得到以下结果

[['i', 'like'], ['i', 'love'], ['i', 'hate'], ['i', 'do']] -> ['dog', 'coffee', 'milk', 'nlp']

import numpy as np
import torch
import torch.nn as nn
import torch.optim as optim
from tqdm import tqdm
from torch.autograd import Variable

sentences = ["i like dog", "i love coffee", "i hate milk", "i do nlp"]

# 创建词汇表和映射关系
word_list = ' '.join(sentences).split()  # 将所有句子合并并分割成单词列表
word_list = list(set(word_list))  # 去重
word_dict = {w: i for i, w in enumerate(word_list)}  # 单词到索引的映射
number_dict = {i: w for i, w in enumerate(word_list)}  # 索引到单词的映射

n_class = len(word_dict)  # 词汇表大小,即类别数

# 设置模型超参数
m = 2  # 嵌入向量维度
n_step = 2  # 输入的步长(前两个单词)
n_hidden = 2  # 隐层的神经元数量

# 创建输入和目标批次
def make_batch(sentence):
    input_batch = []
    target_batch = []

    for sen in sentence:
        word = sen.split()  # 将句子分割成单词
        input = [word_dict[n] for n in word[:-1]]  # 输入为句子中的前n_step个单词
        target = word_dict[word[-1]]  # 目标为句子的最后一个单词

        input_batch.append(input)  # 添加输入
        target_batch.append(target)  # 添加目标

    return input_batch, target_batch  # 返回输入批次和目标批次

# 定义神经网络语言模型
class NNLM(nn.Module):
    def __init__(self, n_class, m, n_step, n_hidden):
        super(NNLM, self).__init__()
        self.embed = nn.Embedding(n_class, m)  # 定义嵌入层,将单词索引映射为嵌入向量
        self.linear1 = nn.Linear(n_step * m, n_hidden)  # 第一层线性层,输入大小为n_step*m,输出为n_hidden
        self.linear2 = nn.Linear(n_hidden, n_class)  # 第二层线性层,输出大小为n_class

    def forward(self, x):
        x = self.embed(x)  # 通过嵌入层得到嵌入向量,形状为 batch_size x n_step x m
        x = x.view(-1, x.size(1) * x.size(2))  # 将嵌入向量展平成二维,大小为 batch_size x (n_step * m)
        tanh = torch.tanh(self.linear1(x))  # 通过第一层线性层,并使用tanh激活函数
        output = self.linear2(tanh)  # 通过第二层线性层,得到分类输出
        return output  # 返回输出

# 初始化模型
model = NNLM(n_class, m, n_step, n_hidden)

# 定义损失函数和优化器
# 不需要 softmax:nn.CrossEntropyLoss 已经隐含了 softmax 操作
criterion = nn.CrossEntropyLoss()  # 使用交叉熵损失
optimizer = optim.Adam(model.parameters(), lr=0.001)  # 使用Adam优化器,学习率为0.001

# 准备输入和目标数据
input_batch, target_batch = make_batch(sentences)
"""
为什么要转换为 LongTensor?
input_batch 和 target_batch 包含的是单词的索引(整数值),而 PyTorch 的 nn.Embedding 层和损失函数(如 CrossEntropyLoss)要求输入的索引必须是 LongTensor 类型(即整数类型的张量),而不是 FloatTensor 或其他类型。因此,必须将这些输入和目标数据显式转换为 LongTensor。"""
input_batch = torch.LongTensor(input_batch)  # 直接转换为 LongTensor
target_batch = torch.LongTensor(target_batch)  # 直接转换为 LongTensor

# 开始训练
for epoch in range(5000):
    optimizer.zero_grad()  # 清零梯度
    output = model(input_batch)  # 前向传播,得到输出
    loss = criterion(output, target_batch)  # 计算损失

    if (epoch + 1) % 1000 == 0:  # 每1000个epoch打印一次损失
        print(f'epoch: {epoch + 1:04d}, cost = {loss.item():.6f}')

    loss.backward()  # 反向传播计算梯度
    optimizer.step()  # 更新模型参数

# 使用训练好的模型进行预测
predict = model(input_batch).max(1, keepdim=True)[1]  # 得到最大概率对应的索引(即预测的单词类别)

# 打印输入的前两个单词和模型预测的最后一个单词
print([sen.split()[:2] for sen in sentences], '->', [number_dict[n.item()] for n in predict.squeeze()])

说明:

(1) predict = model(input_batch).data.max(1, keepdim=True)[1]
  1. model(input_batch)

  • 这部分是将 input_batch 输入到模型中,经过 forward 函数计算,得到网络的输出。

  • 输出是一个二维张量,形状为 (batch_size, n_class),其中每行代表一个样本的预测得分(未归一化的类别概率),每列对应词汇表中的一个单词类别。

    2..max(1, keepdim=True)

  • max 是一个 PyTorch 的函数,返回沿指定维度的最大值及其索引。

  • 1:表示在第 1 维(即每行)上寻找最大值。第 0 维是批次(样本),第 1 维是类别的得分(对于每个单词的概率分布)。

  • keepdim=True:表示保持原始维度,即结果张量的形状与原来的张量在没有被压缩的维度上保持一致。

返回的结果是一个包含两个元素的元组:

  • 第一个元素:每行的最大值(在预测任务中通常是没有用到的)。

  • 第二个元素:每行最大值的索引(即预测的类别)。

    3.[1]

  • 通过 [1] 取出最大值对应的索引,即上一步中返回的最大值的索引部分。这个索引对应预测的类别编号,也就是词汇表中的单词索引。

(2) [number_dict[n.item()] for n in predict.squeeze()]
  1. predict.squeeze()

  • predict 是模型预测的结果,形状是 (batch_size, 1)predict.squeeze() 用于去掉维度为 1 的那一维,使其从 (batch_size, 1) 变为 (batch_size)

  • squeeze() 函数会去掉张量中所有大小为 1 的维度。在这里,predict 的维度是 (batch_size, 1),经过 squeeze() 后变成一维张量,形状为 (batch_size),即包含 batch_size 个预测的类别索引。

    2.n.item()

  • n 是张量中的一个元素(预测的类别索引),n.item() 将这个单元素张量转换为普通的 Python 标量类型(整数)。

  • item() 是 PyTorch 张量的方法,用于从单个元素的张量中提取值。它确保索引 n 是一个纯 Python 的整数,可以用于字典查找。

    3.number_dict[n.item()]

  • number_dict 是一个字典,它将单词索引映射回对应的单词,即 {index: word} 的映射。

  • number_dict[n.item()] 用来通过索引 n.item() 从字典 number_dict 中查找对应的单词,将模型预测出的索引转换为实际的单词。

神经语言模型构建完成之后,就是训练参数了,这里的参数包括:

  • 词向量矩阵C;

  • 神经网络的权重;

  • 偏置等参数

训练数据就是大堆大堆的语料库。训练结束之后,语言模型得到了:通过“w_{t-(n-1)},\cdots ,w_{t-2},w_{t-1}”去预测第 t 个词是 w_t 的概率,但有点意外收获的是词向量“ w_{t-(n-1)},\cdots ,w_{t-2},w_{t-1}”也得到了。换言之,词向量是这个语言模型的副产品。

预训练模型

介绍:通常是一个LM(语言模型)的副产物模型指的是对事物的数学抽象,那么语言模型指的就是对语言现象的数学抽象。

在自然语言处理(NLP)中,预训练模型是指在大量通用文本数据集上训练好的模型,它可以捕捉到语言的广泛特征,例如词汇、句法、语境相关性等。这些模型训练完成后,可以在特定的下游任务上进行微调(fine-tuning),以适应特定的应用场景,如情感分析、问答系统、文档摘要等。通过这种方式,预训练模型可以显著提高NLP任务的性能,尤其是在标注数据有限的情况下。

预训练模型的核心思想是利用大规模语料库中的无标签文本数据,学习到一般性的语言表示,然后将这些知识迁移到数据量较小的特定任务中。这种方法背后的假设是,语言中存在大量的通用性规律可被学习并用于各种具体任务。

3.2.2 word2vec

word2vec是Google研究团队里的Tomas Mikolov等人于2013年的《Distributed Representations ofWords and Phrases and their Compositionality》以及后续的《Efficient Estimation of Word Representations in Vector Space》两篇文章中提出的一种高效训练词向量的模型,基本出发点是上下文相似的两个词,它们的词向量也应该相似,比如香蕉和梨在句子中可能经常出现在相同的上下文中,因此这两个词的表示向量应该就比较相似。

word2vec一般分为CBOW(Continuous Bag-of-Words)与 Skip-Gram 两种模型:

1、CBOW:根据中心词周围的词来预测中心词,有negative sample和Huffman两种加速算法;

2、Skip-Gram:根据中心词来预测周围词

二者的结构十分相似,理解了CBOW,对于Skip-Gram也就基本理解了。

论文里面的原图如下,左边CBOW,右边skip-gram。

3.2.2.1 Skip-gram 模型

Skip-gram 模型是一种根据目标单词来预测上下文单词的模型。具体来说,给定一个中心单词,Skip-gram 模型的任务是预测在它周围窗口大小为 n 内的上下文单词。

Skip-gram 模型的优点是:由于它是基于目标单词来预测上下文单词的,因此它可以利用目标单词的语义和语法特征来预测上下文单词;模型能够生成更多的训练数据,因此可以更好地训练低频词汇的表示;Skip-gram 模型在处理大规模语料库时效果比 CBOW 模型更好

代码实现skip-gram(初级版)


import numpy as np
import torch
import torch.nn as nn
import torch.optim as optim
from tqdm import tqdm
from torch.autograd import Variable
import matplotlib.pyplot as plt

# 定义数据类型为浮点数
dtype = torch.FloatTensor

# 语料库,包含训练模型的句子
sentences = ["i like dog", "i like cat", "i like animal",
             "dog cat animal", "apple cat dog like", "cat like fish",
             "dog like meat", "i like apple", "i hate apple",
             "i like movie book music apple", "dog like bark", "dog friend cat"]

# 将所有句子拼接为一个字符串并按空格分词
word_sequence = ' '.join(sentences).split()
# 获取词汇表中的所有唯一词
word_list = list(set(word_sequence))
# 创建词典,词汇表中的每个词都分配一个唯一的索引
word_dict = {w: i for i, w in enumerate(word_list)}

# 创建跳字模型的训练数据
skip_grams = []  # 训练数据
for i in range(1, len(word_sequence) - 1):
    # 当前词对应的id
    target = word_dict[word_sequence[i]]
    # 获取当前词的前后两个上下文词对应的id
    context = [word_dict[word_sequence[i - 1]], word_dict[word_sequence[i + 1]]]

    # 将目标词与上下文词配对,添加到训练数据中
    for w in context:
        skip_grams.append([target, w])

# 定义嵌入维度(嵌入向量的大小)为2
embedding_size = 2
# 词汇表大小
voc_size = len(word_list)
# 每次训练的批量大小
batch_size = 5


# 定义Word2Vec模型
class Word2Vec(nn.Module):
    def __init__(self):
        super(Word2Vec, self).__init__()
        # 定义词嵌入矩阵W,随机初始化,大小为(voc_size, embedding_size)
        self.W = nn.Parameter(torch.rand(voc_size, embedding_size)).type(dtype)
        # 定义上下文矩阵WT,随机初始化,大小为(embedding_size, voc_size)
        self.WT = nn.Parameter(torch.rand(embedding_size, voc_size)).type(dtype)

    # 前向传播
    def forward(self, x):
        # 通过乘以嵌入矩阵W得到词向量
        weight_layer = torch.matmul(x, self.W)
        # 通过上下文矩阵WT得到输出
        output_layer = torch.matmul(weight_layer, self.WT)
        return output_layer


# 创建模型实例
model = Word2Vec()

# 定义损失函数为交叉熵损失
criterion = nn.CrossEntropyLoss()
# 使用Adam优化器
optimizer = optim.Adam(model.parameters(), lr=0.001)


# 定义随机批量生成函数
def random_batch(data, size):
    random_inputs = []  # 输入批次
    random_labels = []  # 标签批次
    # 从数据中随机选择size个索引
    random_index = np.random.choice(range(len(data)), size, replace=False)

    # 根据随机索引生成输入和标签批次
    for i in random_index:
        # 目标词one-hot编码
        random_inputs.append(np.eye(voc_size)[data[i][0]])
        # 上下文词的索引作为标签
        random_labels.append(data[i][1])

    return random_inputs, random_labels


# 训练模型
for epoch in range(10000):
    # 获取一批随机的输入和目标
    input_batch, target_batch = random_batch(skip_grams, batch_size)

    # 转换为张量
    input_batch = torch.Tensor(input_batch)
    target_batch = torch.LongTensor(target_batch)

    # 梯度清零
    optimizer.zero_grad()

    # 前向传播得到模型输出
    output = model(input_batch)

    # 计算损失
    loss = criterion(output, target_batch)

    # 每1000个epoch打印一次损失
    if (epoch + 1) % 1000 == 0:
        print('Epoch:', '%04d' % (epoch + 1), 'cost =', '{:.6f}'.format(loss))

    # 反向传播
    loss.backward()
    # 更新参数
    optimizer.step()

# 可视化词嵌入
for i, label in enumerate(word_list):
    W, WT = model.parameters()  # 获取模型参数
    x, y = float(W[i][0]), float(W[i][1])  # 获取词嵌入的二维坐标
    plt.scatter(x, y)  # 绘制散点图
    plt.annotate(label, xy=(x, y), xytext=(5, 2), textcoords='offset points', ha='right', va='bottom')  # 标注词汇
plt.show()  # 显示图形

注释:

 # 根据随机索引生成输入和标签批次
    for i in random_index:
        # 目标词one-hot编码
        random_inputs.append(np.eye(voc_size)[data[i][0]])
        # 上下文词的索引作为标签
        random_labels.append(data[i][1])

for i in random_index: 遍历random_index列表中的每个索引i。这些索引用于从数据集中随机选择样本。

random_inputs.append(np.eye(voc_size)[data[i][0]])

  • np.eye(voc_size):生成一个大小为voc_size的单位矩阵。这里的voc_size通常表示词汇表的大小。

  • data[i][0]:从数据集中获取第i个样本的第一个元素,通常代表目标词的索引。

  • np.eye(voc_size)[data[i][0]]:通过索引获取目标词的one-hot编码,结果是一个长度为voc_size的向量,其中目标词的位置为1,其余位置为0。

  • random_inputs.append(...):将生成的one-hot编码添加到random_inputs列表中。

random_labels.append(data[i][1])

  • data[i][1]:从数据集中获取第i个样本的第二个元素,通常代表上下文词的索引(作为标签)。

  • random_labels.append(...):将获取的标签添加到random_labels列表中。

3.2.2.2 CBOW模型

连续词袋模型(CBOW)是一种根据上下文单词来预测目标单词的模型。具体来说,给定一个窗口大小为 n 的上下文单词序列,连续词袋模型的任务是预测中间的目标单词。

CBOW模型∶使用文本的中间词作为目标词(标签),去掉了隐藏层。用上下文各词的词向量的均值代替NNLM拼接起来的词向量。

连续词袋模型的优点是:由于它是基于上下文单词来预测目标单词的,因此它可以利用上下文单词的信息来推断目标单词的语义和语法特征;模型参数较少,训练速度相对较快。CBOW对小型数据库比较合适。

输入∶上下文的语义表示

输出∶中间词是哪个词

练习:

之前案例中的模型换成CBOW模型

import numpy as np
import torch
import torch.nn as nn
import torch.optim as optim
from tqdm import tqdm
import matplotlib.pyplot as plt

dtype = torch.FloatTensor

# 语料库
sentences = ["i like dog", "i like cat", "i like animal",
             "dog cat animal", "apple cat dog like", "cat like fish",
             "dog like meat", "i like apple", "i hate apple",
             "i like movie book music apple", "dog like bark", "dog friend cat"]

word_sequence = ' '.join(sentences).split()  # 将所有句子合并成一个长的词序列
word_list = list(set(word_sequence))  # 获取去重的词汇列表
word_dict = {w: i for i, w in enumerate(word_list)}  # 创建词典,词对应的索引

# 创建CBOW训练数据
cbow_data = []
for i in range(1, len(word_sequence) - 1):
    context = [word_dict[word_sequence[i - 1]], word_dict[word_sequence[i + 1]]]  # 上下文词的索引
    target = word_dict[word_sequence[i]]  # 当前词(目标词)的索引
    cbow_data.append([context, target])  # 将上下文和目标词加入训练数据

embedding_size = 2  # 词向量的维度
voc_size = len(word_list)  # 词汇表的大小
batch_size = 5  # 每个批次的大小

# 定义CBOW模型
class CBOW(nn.Module):
    def __init__(self):
        super(CBOW, self).__init__()
        self.embeddings = nn.Embedding(voc_size, embedding_size)  # 词嵌入层
        self.linear1 = nn.Linear(embedding_size, voc_size)  # 输出层,将嵌入映射回词汇表大小

    def forward(self, x):
        embeds = self.embeddings(x)  # 查找上下文词的嵌入向量
        avg_embeds = torch.mean(embeds, dim=1)  # 对上下文词向量取平均值
        out = self.linear1(avg_embeds)  # 通过全连接层,预测词汇表中的每个词的概率
        return out

# 实例化模型
model = CBOW()

criterion = nn.CrossEntropyLoss()  # 使用交叉熵损失
optimizer = optim.Adam(model.parameters(), lr=0.001)  # 使用Adam优化器

# 随机批次生成函数
def random_batch(data, size):
    random_inputs = []  # 上下文词
    random_labels = []  # 目标词
    random_index = np.random.choice(range(len(data)), size, replace=False)  # 随机选择数据索引

    for i in random_index:
        random_inputs.append(data[i][0])  # 上下文词加入输入列表
        random_labels.append(data[i][1])  # 目标词加入标签列表

    return random_inputs, random_labels

# 训练模型
for epoch in range(10000):
    input_batch, target_batch = random_batch(cbow_data, batch_size)  # 获取随机批次数据

    input_batch = torch.LongTensor(input_batch)  # 转换为LongTensor,因为Embedding需要整数索引
    target_batch = torch.LongTensor(target_batch)  # 转换为LongTensor

    optimizer.zero_grad()  # 梯度清零
    output = model(input_batch)  # 前向传播,预测输出
    loss = criterion(output, target_batch)  # 计算损失

    if (epoch + 1) % 1000 == 0:  # 每1000个epoch打印一次损失
        print('Epoch:', '%04d' % (epoch + 1), 'cost =', '{:.6f}'.format(loss.item()))

    loss.backward()  # 反向传播
    optimizer.step()  # 更新参数

# 可视化嵌入结果
for i, label in enumerate(word_list):
    W = model.embeddings.weight.data.numpy()  # 获取词嵌入的权重
    x, y = W[i][0], W[i][1]  # 获取每个词的嵌入向量的两个维度
    plt.scatter(x, y)  # 在二维空间中绘制点
    plt.annotate(label, xy=(x, y), xytext=(5, 2), textcoords='offset points', ha='right', va='bottom')  # 标注词汇
plt.show()  # 显示图形
3.2.2.3 gensim API调用

Word2vec是一个用来产生词向量的模型。是一个将单词转换成向量形式的工具。 通过转换,可以把对文本内容的处理简化为向量空间中的向量运算,计算出向量空间上的相似度,来表示文本语义上的相似度。

参数 说明
sentences 可以是一个list,对于大语料集,建议使用BrownCorpus,Text8Corpus或lineSentence构建。
vector_size word向量的维度,默认为100。大的size需要更多的训练数据,但是效果会更好。推荐值为几十到几百。
alpha 学习率
window 表示当前词与预测词在一个句子中的最大距离是多少。
min_count 可以对字典做截断。词频少于min_count次数的单词会被丢弃掉,默认值为5。
max_vocab_size 设置词向量构建期间的RAM限制。如果所有独立单词个数超过这个,则就消除掉其中最不频繁的一个。每一千万个单词需要大约1GB的RAM。设置成None则没有限制。
sample 高频词汇的随机降采样的配置阈值,默认为1e-3,范围是(0,1e-5)
seed 用于随机数发生器。与初始化词向量有关。
workers 参数控制训练的并行数。
sg 用于设置训练算法,默认为0,对应CBOW算法;sg=1则采用skip-gram算法。
hs 如果为1则会采用hierarchica·softmax技巧。如果设置为0(default),则negative sampling会被使用。
negative 如果>0,则会采用negative samping,用于设置多少个noise words。
cbow_mean 如果为0,则采用上下文词向量的和,如果为1(default)则采用均值。只有使用CBOW的时候才起作用。
hashfxn hash函数来初始化权重。默认使用python的hash函数。
epochs 迭代次数,默认为5。
trim_rule 用于设置词汇表的整理规则,指定那些单词要留下,哪些要被删除。可以设置为None(min_count会被使用)或者一个接受()并返回RULE_DISCARD,utils。RULE_KEEP或者utils。RULE_DEFAULT的函数。
sorted_vocab 如果为1(default),则在分配word index 的时候会先对单词基于频率降序排序。
batch_words 每一批的传递给线程的单词的数量,默认为10000
min_alpha 随着训练的进行,学习率线性下降到min_alpha
常用方法

model.wv: 这个对象包含了所有单词的词嵌入向量。常用的方法有:

  • model.wv[word]:返回某个特定单词的向量。

  • model.wv.most_similar(word):获取与某个单词最相似的词。

  • model.wv.similarity(word1, word2):计算两个词之间的相似度。

  • model.save("word2vec.model")

    model = Word2Vec.load("word2vec.model")

    保存和加载模型

使用实例:

import numpy as np
from gensim.models import Word2Vec
import matplotlib.pyplot as plt

# 语料库
sentences = ["i like dog", "i like cat", "i like animal",
             "dog cat animal", "apple cat dog like", "cat like fish",
             "dog like meat", "i like apple", "i hate apple",
             "i like movie book music apple", "dog like bark", "dog friend cat"]

# 将每个句子分成单词列表(Gensim的Word2Vec API要求输入格式为二维列表:每个句子是一个单词列表)
tokenized_sentences = [sentence.split() for sentence in sentences]

# 定义Word2Vec模型
model = Word2Vec(sentences=tokenized_sentences, vector_size=2, window=2, min_count=1, sg=0)

# 获取词汇表
word_list = list(model.wv.index_to_key)

# 可视化嵌入结果
for i, word in enumerate(word_list):
    W = model.wv[word]  # 获取每个单词的词向量
    x, y = W[0], W[1]  # 由于向量是二维的,提取前两个维度
    plt.scatter(x, y)  # 在二维空间中绘制点
    plt.annotate(word, xy=(x, y), xytext=(5, 2), textcoords='offset points', ha='right', va='bottom')  # 标注词汇

plt.show()  # 显示图形

示例:

import numpy as np
from gensim.models import Word2Vec
from nltk.tokenize import word_tokenize
# import nltk
# nltk.download('punkt')  # 如果未安装nltk的punkt分词器,请先下载

# 示例文本数据
sentence = "Word embedding is the collective name for a set of language modeling and feature learning techniques in natural language processing (NLP) where words or phrases from the vocabulary are mapped to vectors of real numbers."

# 使用NLTK进行分词
tokenized_text = word_tokenize(sentence.lower())  # 将文本转换为小写并进行分词

# 定义Word2Vec模型并进行训练
model = Word2Vec(sentences=[tokenized_text], vector_size=100, window=5, min_count=1, workers=4)

# 计算整句话的嵌入向量
def sentence_embedding(sentence, model,tokenized_text):
    # 获取每个单词的嵌入向量
    word_embeddings = [model.wv[word] for word in tokenized_text if word in model.wv]
    # 如果句子中没有任何一个单词在词汇表中,则返回None
    if len(word_embeddings) == 0:
        return None
    # 计算平均单词嵌入
    sentence_embedding = np.mean(word_embeddings, axis=0)
    return sentence_embedding

# 获取整句话的嵌入向量
sentence_emb = sentence_embedding(sentence, model,tokenized_text)
print("Embedding for the sentence:", sentence_emb)

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐