自然语言处理(nlp)

词嵌入(word embedding)

one-hot:图像分类问题主要使用one-hot编码,比如一共有五类,那么属于第二类的话,他的编码为(0,1,0,0,0),但是在nlp中自然是行不通的,假如一篇文章有2000个词,那么任意一个词的编码表示为(0,0,…,0,1,0,…,0),一个长2000的向量中只有一个1,其余全是0。这未免太稀疏了,且也不能体现词的特性,所以一般对nlp词的编码使用word embedding

embedding:对于每个词,可以使用一个高维向量去表示,这里的高维向量不再是全为0和1的形式,向量的每一位都是一些实数,而这些实数隐含着这个单词的某种属性。两个词之间的 相似性就可以通过 embedding向量之间的夹角表示
$$
\cos \theta=\frac{\vec a·\vec b}{\vert \vec a \vert \vert \vec b\vert}
$$
我们举一个例子,下面有 4 段话

  1. The cat likes playing wool.

  2. The kitty likes playing wool.

  3. The dog likes playing ball.

  4. The boy does not like playing ball or wool.

这里面有 4 个词,分别是 cat, kitty, dog 和 boy。下面我们使用一个二维的词向量 (a, b) 来表示每一个词,其中 a,b 分别代表着这个词的一种属性,比如 a 代表是否喜欢玩球,b 代表是否喜欢玩毛线,数值越大表示越喜欢,那么我们就能够用数值来定义每一个单词。

对于 cat,我们可以定义它的词嵌入为 (-1, 4),因为他不喜欢玩球,喜欢玩毛线,同时可以定义 kitty 为 (-2, 5),dog 为 (3, 2) 以及 boy 为 (-2, -3)。

那么问题来了:如何对词进行embedding编码呢?这个问题可以交给神经网络去做,我们只需要定义我们需要的维度。词嵌入的每个元素表示一种属性,维度较低的时候我们能够推断出每一维度的具体含义,然而维度较高之后,我们并不需要指定每一维度代表什么,因为每个维度都是网络自己学习出来的属性。具体做法是skip-garm模型,该模型是Word2Vec这篇论文的网络架构,skip-gram 模型非常简单,我们在一段文本中训练一个简单的网络,这个网络的任务是通过一个词周围的词来预测这个词,然而我们实际上要做的就是训练我们的词嵌入。

比如我们给定一句话中的一个词,看看它周围的词,然后随机挑选一个,我们希望网络能够输出一个概率值,这个概率值能够告诉我们到底这个词离我们选择的词的远近程度,比如这么一句话 ‘A dog is playing with a ball’,如果我们选的词是 ‘ball’,那么 ‘playing’ 就要比 ‘dog’ 离我们选择的词更近。

对于一段话,我们可以按照顺序选择不同的词,然后构建训练样本和 label,比如:

85sKM.png

对于这个例子,我们依次取一个词以及其周围的词构成一个训练样本,比如第一次选择的词是 ‘the’,那么我们取其前后两个词作为训练样本,这个也可以被称为一个滑动窗口,对于第一个词,其左边没有单词,所以训练集就是三个词,然后我们在这三个词中选择 ‘the’ 作为输入,另外两个词都是他的输出,就构成了两个训练样本,又比如选择 ‘fox’ 这个词,那么加上其左边两个词,右边两个词,一共是 5 个词,然后选择 ‘fox’ 作为输入,那么输出就是其周围的四个词,一共可以构成 4 个训练样本,通过这个办法,我们就能够训练出需要的词嵌入。

词嵌入的pytorch实现是通过函数nn.Embedding(m,n)实现的,其中m表示所有的单词数目,n表示词嵌入的维度,其返回一个初始化的embdding矩阵,在RNN训练时会对其不断更新:

import torch
from torch import nn
word_to_ix={'hello':0,'how':1,'are':2,'you':3}
embeds=nn.Embedding(4,5)
hello_idx=torch.IntTensor([word_to_ix['hello']])
hello_embeds=embeds(hello_idx)
print(hello_embeds)

我们也可以使用别人已经训练好的词嵌入向量,比如:

github.com

中有已训练好的300维中文embedding。

但是在训练时需要设置对其导数不跟新:

self.embedding.weight.data.copy_(torch.from_numpy(embeding_vector))
self.embedding.weight.requires_grad = False

第一句加载词向量,第二句设置不对其更新

加载词向量也可以使用:

self.embdeeing.weight=nn.parameter(torch.Tensor(embedding_weight))

N Gram模型

N-Gram是一种基于统计语言模型的算法。它的基本思想是将文本里面的内容按照字节进行大小为N的滑动窗口操作,形成了长度是N的字节片段序列。

每一个字节片段称为gram,对所有gram的出现频度进行统计,并且按照事先设定好的阈值进行过滤,形成关键gram列表,也就是这个文本的向量特征空间,列表中的每一种gram就是一个特征向量维度。

N Gram解决一个什么问题呢?就是通过前文对后文出现词进行预测,比如现在需要预测一句:

’‘I lived in china for 10 years, I can speak ___."

很明显我们希望预测这个词为chinese,那么N Gram模型是如何实现此功能的呢?

首先该模型对此句话引入了某种马尔可夫假设,即当前单词仅有前面n个词有关,而不是对于前面所有词有关,那么对于一句由$w_1,w_2,…w_n$n个单词组成的句子T,其概率为:
$$
P(T)=P(w_1)P(w_2\vert w_1)P(w_3 \vert w_1w_2)…P(w_n \vert w_{n-1}w_{n-1}…w_1)
$$
对于这里的条件概率,传统的方法是统计语料中每个词出现的频率,根据贝叶斯定理来估计这个条件概率,这里我们就可以用词嵌入对其进行代替,然后使用 RNN 进行条件概率的计算,然后最大化这个条件概率不仅修改词嵌入,同时能够使得模型可以依据计算的条件概率对其中的一个单词进行预测。

一个N Gram模型例子:

import torch
from torch import nn,optim
import jieba
context_size=2
embedding_size=10
txt="""天也欢喜,地也欢喜,人也欢喜,
欢喜我遇到了你,你也遇到了我。
当时是你心里有了一个我,
我心里也有了一个你,
从今后是朝朝暮暮在一起。
地久天长,同心比翼,相敬相爱相扶持,
偶然发脾气,也要规劝勉励。
在工作中学习,在服务上努力,
追求真理,抗战到底。
为着大我忘却小己,直等到最后胜利。
再生一两个孩子,一半儿像我,一半儿像你."""#这里采用陶行知老先生写给妻子的诗作文语料库
word_list=jieba.lcut(txt)
for i in [',',' ','\n','。']:
while i in word_list:
word_list.remove(i)
trigram = [((word_list[i], word_list[i+1]), word_list[i+2]) for i in range(len(word_list)-2)]
vocb=set(word_list)
word_idx={word: i for i,word in enumerate(vocb)}
inx_to_word={word_idx[word]:word for word in word_idx}

class NgramModel(nn.Module):
def __init__(self,vocb_size,context_size=context_size,n_dim=embedding_size) -> None:
super(NgramModel,self).__init__()
self.n_word=vocb_size
self.embedding=nn.Embedding(self.n_word,n_dim)
self.linear1=nn.Sequential(nn.Linear(context_size*n_dim,128),nn.ReLU(True))
self.linear2=nn.Linear(128,self.n_word)

def forward(self,x):
emb=self.embedding(x)
emb=emb.reshape(1,-1)
out=self.linear1(emb)
out=self.linear2(out)

return out

net = NgramModel(len(word_idx)).cuda()

criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(net.parameters(), lr=1e-2, weight_decay=1e-5)


for e in range(100):
train_loss = 0
for word, label in trigram: # 使用前 100 个作为训练集
word = torch.LongTensor([word_idx[i] for i in word]).cuda() # 将两个词作为输入
label = torch.LongTensor([word_idx[label]]).cuda()
# 前向传播
out = net(word)
loss = criterion(out, label)
train_loss += loss.item()
# 反向传播
optimizer.zero_grad()
loss.backward()
optimizer.step()
if (e + 1) % 20 == 0:
print('epoch: {}, Loss: {:.6f}'.format(e + 1, train_loss / len(trigram)))


net = net.eval()
word, label = trigram[30]
print('input: {}'.format(word))
print('label: {}'.format(label))
print()
word = torch.LongTensor([word_idx[i] for i in word]).cuda()
out = net(word)
pred_label_idx = out.max(1)[1].item()
predict_word = inx_to_word[pred_label_idx]
print('real word is {}, predicted word is {}'.format(label, predict_word))

857P7.png

​ 可以看到我们的N Gram模型中,只使用单边单词预测当前词汇,而还有一种使用双边单词预测当前词汇的模型:Continuous Bag-of-words model(CBOW),感兴趣的朋友可以查看Word2Vec之CBOW - 知乎 (zhihu.com)

词性判断

很明显我们在N Gram中只使用了word embedding和Linear层,并没有使用RNN,接下来我们将使用LSTM进行词性判断.

原理:其实词性判断说到底还是一个分类问题,输入一句话可以看作一个序列,序列中每个词由高维embedding表示,其输出与该序列等长,每个输出表示对词性的判断。使用LSTM做词性判断很好理解,因为单独对一个词做词性判断显然是不现实 的,我们需要结合上下文。

字符增强:还可以引入字符增强来实现词性预测,何谓字符增强呢?很简单,比如一个词后缀为ly,那么我们大概率判定其为副词。将这两种方法结合起来,就能更好的做词性判断。

我们还是使用LSTM,但是这次不再使用句子作为输入序列,二十将每个单词作为输入。每个单词由不同字母组成,我们将其看作一个序列,对每个字母做词向量,然后传入LSTM网络,接着我们把这个单词的输出(我们不需要关心其输出是什么,其只是一种抽象特征,能更好地预测结果)和其前面几个单词构成序列,可以对这些单词构建新的词嵌入,最后输出结果是单词的词性,也就是根据前面几个词的信息对这个词的词性进行分类。

综上,就是先将一个单词拆分为一个个字母序列,然后输入进入一个LSTM网络,将其输出与句子中该单词的embedding组合为一个更高维向量,然后再输入一个LSTM网络做词性预测。


from unittest import result
import torch
from torch import nn
from torch.autograd import Variable
import numpy as np

training_data = [("The dog ate the apple".split(),
["DET", "NN", "V", "DET", "NN"]),
("Everybody read that book".split(),
["NN", "V", "DET", "NN"])]

word_to_idx = {}
tag_to_idx = {}
for context, tag in training_data:
for word in context:
if word.lower() not in word_to_idx:
word_to_idx[word.lower()] = len(word_to_idx)
for label in tag:
if label.lower() not in tag_to_idx:
tag_to_idx[label.lower()] = len(tag_to_idx)
word_to_idx
tag_to_idx

alphabet = 'abcdefghijklmnopqrstuvwxyz'
char_to_idx = {}
for i in range(len(alphabet)):
char_to_idx[alphabet[i]] = i


def make_sequence(x, dic): # 字符编码
idx = [dic[i.lower()] for i in x]
idx = torch.LongTensor(idx)
return idx
class char_lstm(nn.Module):
def __init__(self, n_char, char_dim, char_hidden):
super(char_lstm, self).__init__()
self.char_embed = nn.Embedding(n_char, char_dim)
self.lstm = nn.LSTM(char_dim, char_hidden)

def forward(self, x):

x = self.char_embed(x)
out, _ = self.lstm(x)
return out[-1] # (batch, hidden)

class lstm_tagger(nn.Module):
def __init__(self, n_word, n_char, char_dim, word_dim,
char_hidden, word_hidden, n_tag):
super(lstm_tagger, self).__init__()
self.word_embed = nn.Embedding(n_word, word_dim)
self.char_lstm = char_lstm(n_char, char_dim, char_hidden)
self.word_lstm = nn.LSTM(word_dim + char_hidden, word_hidden)
self.classify = nn.Linear(word_hidden, n_tag)

def forward(self, x, word):
char = []
for w in word: # 对于每个单词做字符的 lstm
char_list = make_sequence(w, char_to_idx)

char_list = char_list.unsqueeze(1) # (seq, batch, feature) 满足 lstm 输入条件
char_infor = self.char_lstm(Variable(char_list)) # (batch, char_hidden)
char.append(char_infor)
char = torch.stack(char, dim=0) # (seq, batch, feature)
x = self.word_embed(x) # (batch, seq, word_dim)
x = x.permute(1, 0, 2) # 改变顺序(seq,batch,word_dim)
x = torch.cat((x, char), dim=2) # 沿着特征通道将每个词的词嵌入和字符 lstm 输出的结果拼接在一起(seq,batch,word_dim+char_dim)
x, _ = self.word_lstm(x)
# s,b,h=x.shape
# x=x.reshape(-1,h)# 重新 reshape 进行分类线性层
out = self.classify(x)
return out
net = lstm_tagger(len(word_to_idx), len(char_to_idx), 10, 100, 50, 128, len(tag_to_idx))
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(net.parameters(), lr=1e-2)
# 开始训练
for e in range(300):
train_loss = 0
for word, tag in training_data:
word_list = make_sequence(word, word_to_idx).unsqueeze(0) # 添加第一维 batch
tag = make_sequence(tag, tag_to_idx)
word_list = Variable(word_list)
tag = Variable(tag)
# 前向传播
out = net(word_list, word)
out=out.reshape(-1,out.size(2))
# print(out.shape,tag.shape)
loss = criterion(out, tag)
train_loss += loss.item()
# 反向传播
optimizer.zero_grad()
loss.backward()
optimizer.step()
if (e + 1) % 50 == 0:
print('Epoch: {}, Loss: {:.5f}'.format(e + 1, train_loss / len(training_data)))

net = net.eval()
test_sent = 'Everybody ate the apple'
test = make_sequence(test_sent.split(), word_to_idx).unsqueeze(0)
out = net(Variable(test), test_sent.split())
out=out.detach().cpu().numpy()
test_word=test_sent.split()
def get_key(d,value):
return [k for k,v in d.items() if v==value]
result={}
for i in range(out.shape[0]):
max_idx=np.argmax(out[i])
result[test_word[i]]=get_key(tag_to_idx,max_idx)

print(result)

8zk8m.png