分词
关于分词,目前有三大主流分词方法:基于字符串匹配的分词方法、基于理解的分词方法和基于统计的分词方法。而本篇笔记主要讲的,是基于统计的分词方法。
统计分词方法
**主要思想:**每个字都是词的最小单元,如果相连的字在不同的文本中出现的频率越多,这就越有可能是一个词。因此我们可以用相邻字出现的频率来衡量组词的可能性,当频率高于某个阈值时,我们可以认为这些字可能会构成一个词。
主要统计模型: N元文法模型(N-gram),隐马尔可夫模型(Hidden Markov Model,HMM),最大熵模型(ME),条件随机场(Conditional Random Fields,CRF)等
**优势:**在实际运用中常常将字符串匹配分词和统计分词结合使用,这样既体现了匹配分词速度快、效率高的优点,同时又能运用统计分词识别生词、自动消除歧义等方面的特点。
n元语法(N-gram)
序列长度增加,计算和存储多个词共同出现的概率的复杂度会呈指数级增加。 元语法通过马尔可夫假设简化模型,马尔科夫假设是指一个词的出现只与前面 个词相关,即 阶马尔可夫链(Markov chain of order n ),如果 ,那么有 ,基于 阶马尔可夫链,我们可以将语言模型改写为:
以上也叫 元语法(n -grams),它是基于 n−1 阶马尔可夫链的概率语言模型。例如,当 n=2 时,含有4个词的文本序列的概率就可以改写为:
当 n 分别为1、2和3时,我们将其分别称作一元语法(unigram)、二元语法(bigram)和三元语法(trigram)。
分词函数
自定义分词函数:
class Vocab(object):
def __init__(self, tokens, min_freq=0, use_special_tokens=False):
counter = count_corpus(tokens) # 调用Counter方法将类型为字符串的词转换成字典,并统计每个词出现的次数以及去重
self.token_freqs = list(counter.items()) # 整个字典嵌套列表,[{"tokens":"freq"}]
self.idx_to_token = [] # 索引到词的容器
if use_special_tokens: # 如果有特殊字词
# padding, begin of sentence, end of sentence, unknown
self.pad, self.bos, self.eos, self.unk = (0, 1, 2, 3) # 先建立四种状态,分别为padding对句子补齐,bos为首部添加词,eos为尾部,unknown为未知词
self.idx_to_token += ['', '', '', ''] # ['', '', '', '']
else: # 如果没有未知的词
self.unk = 0 # 未知词unknown = 0
self.idx_to_token += [''] # ['']
self.idx_to_token += [token for token, freq in self.token_freqs
if freq >= min_freq and token not in self.idx_to_token] # 拿出所有符合要求的词放入容器
self.token_to_idx = dict() # 创建一个字典
for idx, token in enumerate(self.idx_to_token):
self.token_to_idx[token] = idx # 转变成词到索引的字典
def __len__(self): # 判断长度
return len(self.idx_to_token)
def __getitem__(self, tokens): # 为了实现P[key]的调用
if not isinstance(tokens, (list, tuple)):
return self.token_to_idx.get(tokens, self.unk)
return [self.__getitem__(token) for token in tokens]
def to_tokens(self, indices): # 根据这个切片是不是列表和元组做不同操作
if not isinstance(indices, (list, tuple)):
return self.idx_to_token[indices]
return [self.idx_to_token[index] for index in indices]
def count_corpus(sentences):
tokens = [tk for st in sentences for tk in st]
return collections.Counter(tokens) # 返回一个字典,记录每个词的出现次数
运行上述代码:
vocab = Vocab(tokens)
print(list(vocab.token_to_idx.items())[0:10])
"""
[('', 0), ('the', 1), ('time', 2), ('machine', 3), ('by', 4), ('h', 5), ('g', 6), ('wells', 7), ('i', 8), ('traveller', 9)]
"""
# 使用字典,我们可以将原文本中的句子从单词序列转换为索引序列
for i in range(8, 10):
print('words:', tokens[i])
print('indices:', vocab[tokens[i]])
用spaCy包分词:
import spacy
text = "Mr. Chen doesn't agree with my suggestion."
nlp = spacy.load('en_core_web_sm')
doc = nlp(text)
print([token.text for token in doc])
"""
['Mr.', 'Chen', 'does', "n't", 'agree', 'with', 'my', 'suggestion', '.']
"""
用nltk进行划分:
from nltk.tokenize import word_tokenize
from nltk import data
text = "Mr. Chen doesn't agree with my suggestion."
data.path.append('/home/kesci/input/nltk_data3784/nltk_data')
print(word_tokenize(text))
"""
['Mr.', 'Chen', 'does', "n't", 'agree', 'with', 'my', 'suggestion', '.']
"""
循环神经网络
RNN推导
在文本或者具有时间性质的数据中,由于DNN和CNN无法对时间序建模,上一层神经元的输出只能传递给下一层神经元。而在循环神经网络(RNN)中,神经元的输出在下一时刻是可以传递给自身的,可以对时间序列建模。结构图如下:
感觉确实网上这种图较多,如果不细看基本分不清啥是啥网络,而在讲解推导本网络的同时,我们不妨回头复习一下正向传播和反向传播。
正向传播:
正向传播是指对神经网络沿着从输入层到输出层的顺序,依次计算并存储模型的中间变量(包括输出)。流程图如下:
。。。感觉很丑,先放这将就一下,后面会对这节进行重新排版,最近要赶进度没太多时间。
我们可以得到的信息是:
- ,其中 是h*1的数据矩阵, 是h * x,而 是x * 1.
- ,其中 是h*1, 是h * 1
- ,其中 是y * h, 是y * 1, 是h * 1。
其中一三条都是假设,第一条是假设有一个输入样本 ,且不考虑偏置项,第三条是假设输出变量, 是中间变量。第二条的意思是把中间变量 输入按元素运算的激活函数 后,将得到向量长度为 的隐藏层变量.
另外还有 是为L2范数的正则化项:
那么最终的模型在给定的数据样本上带正则化的损失为:
反向传播:
反向传播指的是计算神经网络参数梯度的方法。总的来说,反向传播依据微积分中的链式法则,沿着从输出层到输入层的顺序,依次计算并存储目标函数有关神经网络各层的中间变量以及参数的梯度。具体的推导如下:
所以RNN的推导类似反向传播,为:
后期有时间重新写。
RNN简单实例
在NLP中,循环神经网络语言模型的目的是基于当前的输入与过去的输入序列,预测序列的下一个字符。循环神经网络引入一个隐藏变量
,用
表示
在时间步
的值。
的计算基于
和
,可以认为
记录了到当前字符为止的序列信息,利用
对序列的下一个字符进行预测。
再举一个例子,简单的说,我们要用 RNN 做这样一件事情,每输入一个词,循环神经网络就输出截止到目前为止,下一个最可能的词:
首先,要把词表达为向量的形式:
- 建立一个包含所有词的词典,每个词在词典里面有一个唯一的编号。
- 任意一个词都可以用一个N维的one-hot向量来表示。
def one_hot(x, n_class, dtype=torch.float32):
result = torch.zeros(x.shape[0], n_class, dtype=dtype, device=x.device) # shape: (n, n_class)
result.scatter_(1, x.long().view(-1, 1), 1) # result[i, x[i, 0]] = 1
return result
x = torch.tensor([0, 2])
x_one_hot = one_hot(x, vocab_size)
print(x_one_hot)
print(x_one_hot.shape)
print(x_one_hot.sum(axis=1))
"""
tensor([[1., 0., 0., ..., 0., 0., 0.],
[0., 0., 1., ..., 0., 0., 0.]])
torch.Size([2, 1027])
tensor([1., 1.])
"""
这种向量化方法,我们就得到了一个高维、稀疏的向量,这之后需要使用一些降维方法,将高维的稀疏向量转变为低维的稠密向量。
为了输出 “最可能” 的词,所以需要计算词典中每个词是当前词的下一个词的概率,再选择概率最大的那一个。
梯度爆炸
未完待续