写在前面
本文是笔者学习《自然语言处理入门》(何晗著)的最后一篇学习笔记。在学习本书的过程中,我初步走进了NLP的世界,也尝试了不同的学习、笔记方法。最开始是采用手写笔记的形式,后来决定使用博客的方式做笔记。因此博客上的内容只涉及该书的后面几章。本文是该书读书笔记的最后一篇。
一、传统方法的局限
通过前面的学习,我学习到了隐马尔可夫模型、感知机、条件随机场、朴素贝叶斯模型、支持向量机模型等传统机器学习模型。同时,为了将这些机器学习模型应用于NLP,我们掌握了特征模板、TF-IDF、词袋向量等特征提取方法。现在需要回过头来思索一下,这些方法的局限性在哪里。
1.1 数据稀疏
首先,传统机器学习不适合处理数据稀疏问题。在语言学中,每个单词、字符都是离散型随机变量。然而任何机器学习模型只接受向量,为了将文本转换为向量,我们通常将离散符号按照其索引编码为独热向量。而现实世界的单词种数可以达到数十万,会带来显著的数据稀疏问题。
如果能将任意词语表示为固定长度为n的稠密向量,而且这个长度还比词表体积更小的话,那就会带来额外的好处。此外,如果任意单词都能被表示为具有合理相似度的向量,那么OOV的问题也就不复存在了。因为模型看到的永远是向量,如果这个向量和训练集中 的某个单词相似度很高的话,模型就能以处理相似单词的方式处理OOV。这种词嵌入的方式,是深度学习的起点,
1.2 特征模板
语言具有高度的复合性。汉字构成单词,单词构成短语,短语构成句子,句子构成段落,段落构成文章。为了建模语言的复合性,传统自然语言处理依赖于手工定制的特征模板。这样的特征模板同样带来数据稀疏的问题。一个特定单词很常见,两个单词的特定组合则很少见,三个单词更是如此。以感知机为例,许多特征在训练集中仅仅出现一次,这样的特征在统计学上毫无意义。然而在类似情感分析等任务中,则需要提取复杂的,长距离的特征以建模否定和反讽等语言现象。一方面,复杂的特征模板会加重数据稀疏的问题。另一方面,高级的NLP任务需要复杂的特征。
即使数据充裕,标注语料足够,许多高级的NLP任务依然无法通过传统的机器学习模型解决。因为这些任务是如此之难,即便是经验最丰富的语言学家也无法设计出有效的特征模板。比如自动问答和机器翻译等领域,人们尚不知道人类思考与推理 问题的过程、信达雅地遣词造句机理,所以无法手工选择合理的特征。
哪怕是类似命名实体识别和依存句法分析等相对简单的任务,特征工程也是一件费时费力,难以复用的工作。虽然许多研究工作公开了在某个特定数据集上的特征模板,但是这些特征模板不适合所有的领域。在传统NLP方法中,并不存在一种四海皆准的特征模板。人们需要对特定领域具备相当的领域知识,才能对阵下药地设计特征模板。
1.3 误差传播
现实世界的项目需求通常并不是中文分词那么简单。往往涉及多个自然语言处理模块的组合。比如在情感分析中,通常需要先进行分词,然后在中文分词基础上进行词性标注,过滤停用词,并用卡方检验筛选特征,然后传入分类模型进行预测。这种误差传播随着流水线系统复杂度的提升而恶化,然而传统自然语言处理缺乏一种从问题直接到答案的处理方法。
二、深度学习与优势
2.1 深度学习
深度学习属于表示学习的范畴,指的是利用具有一定“深度”的模型来自动学习事物的向量表示。如果说传统机器学习中,事物的向量表示是利用手工特征模板来提取稀疏的二进制向量的话,那么在深度学习中,特征模板被多层感知机代替。而一旦问题被表达为向量,接下来的分类器可以考虑使用单层感知机等模型。所以说深度学习并不神秘,通过多层感知机提取向量才是深度学习的精髓。
我们知道感知机的分类原理。在多分类情景下,只需使用多个感知机,每个感知机负责判断某一类别。比如n分类情况下,就需要n个感知机为“样本属于第i个分类”这个假设输出一个分数。相应的,判断时只需要取分数最大的类别作为预测结果即可。
在深度学习中,一个感知机通常被称为一个神经元。神经元在接受足够强度的刺激时,会被激活。
类似的,对单层感知机中的每个感知机执行此项操作,单层感知机就形成了单层神经元。
每个隐藏层都是样本的一个特征表示,多层感知机通过权重矩阵对样本的上一个特征表示进行线性变换,通过非线性函数对特征进行激活,可以产生多种灵活的特征向量。这些多次非线性变换可以模拟任意函数,解决包括著名的XOR函数在内的许多线性分类器无法解决的问题。
多层感知机也称为神经网络,是深度学习的基本元件。而深度学习的“深度”指的是多层感知机对特征向量的多层次提取。这也是深度学习被称作表示学习的原因之一。
2.2 用稠密向量解决数据稀疏
词向量是对词语乃至其他样本的抽象表示,含有高度浓缩的信息。相较于独热编码,特征向量的每一维度不再对应特征模板中的某个特征。而可能代表某些特征的组合强度。虽然现在神经网络缺乏解释性,但根据矩阵乘法的性质,一个d*k的权重矩阵通过k个权重向量加权求和了特征向量个维度中的每一维。可以视作对原始特征进行了k次重组或学习。因此,一个具备n个隐藏层的神经网络对原始特征进行了次重组,其对特征的表示学习能力是惊人的。
正因为多层学习得到的稠密向量短小精悍,其对应了低维空间的一个点。无论数据所处的原始空间的维数有多高,数据的分布有多稀疏,其映射到低维空间后,彼此的距离就会缩小,相似度就会体现出来。
在传统机器学习中,一个单词对应的是数十万维的向量,一篇文章也是一个数十万维的二进制向量。然而在深度学习中,他们都可以表示为100维的稠密向量。由表示学习带来的这一切,是传统机器学习难以实现的。
三、word2vec
有人提出,通过一个单词的上下文可以得到它的意思。如果你能把单词放到正确的上下文中去,就说明你掌握了它的意义。看来意义相似的词语的上下文是相似的,或者说近义词可以互相替换。如果每个单词都存在一个特征向量,使得分类器能够根据某个单词的上下文特征向量预测这个单词是什么,那么这个向量就很好地表示了这个词。最常用的word2vec模型为skip-gram和CBOW。CBOW模型为例,是通过上下文向量预测中心单词向量的模型。CBOW算法的流程如下所示:
1.输入层:上下文单词的onehot. {假设单词向量空间dim为V,上下文单词个数为C}
2.所有onehot分别乘以共享的输入权重矩阵W. {VN矩阵,N为自己设定的数,初始化权重矩阵W}
3.所得的C个向量相加求平均作为隐层向量, size为1N.
4.乘以输出权重矩阵W’ {NV}
5.得到向量 {1V} 激活函数处理得到V-dim概率分布 {PS: 因为是onehot嘛,其中的每一维代表着一个单词},概率最大的index所指示的单词为预测出的中间词(target word)
6.与true label的onehot做比较,误差越小越好
四、pytorch实现CBOW模型
import torch.nn as nn
import torch.nn.functional as F
CONTEXT_SIZE = 2 # 2 words to the left, 2 to the right
EMBEDDING_DIM = 100
raw_text = """We are about to study the idea of a computational process.
Computational processes are abstract beings that inhabit computers.
As they evolve, processes manipulate other abstract things called data.
The evolution of a process is directed by a pattern of rules
called a program. People create programs to direct processes. In effect,
we conjure the spirits of the computer with our spells.""".split()
# By deriving a set from `raw_text`, we deduplicate the array
vocab = set(raw_text)
vocab_size = len(vocab)
word_to_ix = {word:i for i, word in enumerate(vocab)}
ix_to_word={i:word for i, word in enumerate(vocab)}
data = []
for i in range(2, len(raw_text)-2):
context = [raw_text[i-2], raw_text[i-1],
raw_text[i+1], raw_text[i+2]]
target = raw_text[i]
data.append((context, target))
class CBOW(nn.Module):
def __init__(self,vocal_size,n_dim,context_size):#窗口大小):
super(CBOW,self).__init__()
self.embeddings=nn.Embedding(vocal_size,n_dim)#embedding第一个参数是词表总长,第二个参数是目标向量的维数。
#输入是batch_size*序列长度(其实就是batch_size个句子,每个元素是一个词,embedding将每个词转化为一个n_dim的向量)
#embedding层的输出是一个batch_size*序列长度*n_dim
self.linear1=nn.Linear(2*context_size*n_dim,128)
self.linear2=nn.Linear(128,vocal_size)
def forward(self,inputs):
embeds=self.embeddings(inputs).view(1,-1)
out=F.relu(self.linear1(embeds))
out=self.linear2(out)
log_prob=F.log_softmax(out,dim=1)
return log_prob
def make_context_vector(context,word_to_ix):
idxs=[word_to_ix[w] for w in context]
return torch.tensor(idxs,dtype=torch.long)
model=CBOW(len(vocab),EMBEDDING_DIM,CONTEXT_SIZE)
losses=[]
loss_function=nn.NLLLoss()
optimizer = torch.optim.SGD(model.parameters(), lr=0.001)
for epoch in range(200):
total_loss=0
context_one_hots=[]
for context,target in data:
context_vector=make_context_vector(context,word_to_ix)
target=torch.tensor([word_to_ix[target]],dtype=torch.long)
optimizer.zero_grad()
log_probs=model(context_vector)
loss=loss_function(log_probs,target)
loss.backward()
optimizer.step()
total_loss+=loss.item()
print("epoch", epoch, " -->", total_loss)
losses.append(total_loss)
for context,target in data:
max_idx=(model(make_context_vector(context,word_to_ix)).argmax())
print('target:',target,'predict:',ix_to_word[int(max_idx)])
#下面这行代码可以获取训练好的词表里任意词的词向量
print(model.embeddings(make_context_vector(['In'],word_to_ix)))