语言模型
在上篇【人工智能学习】【四】文本预处理中,我们最后把一行文本信息通过分词,创建字典,转换成了一行向量表示。本篇要介绍的是语言模型,一言以蔽之就是输入一行文本,预测下一个文本信息。是不是和输入法的提示很像?
在神经网络体系之前,人们尝试过各种方式来对文本进行预测。基于概率模型的隐含马尔可夫模型是以前较为常用的。这在吴军的数学之美里有提到。此外文本数据的输入具有顺序性的特点。为了解决这个问题有了后来的RNN,LSTM,Bi-LSTM等越来越先进的模型,这里先不展开。
隐含马尔可夫模型
隐含马尔可夫模型的含义是,文本的下一个单词和已经出现的单词应该是有关系的,且和前n个词有关系。所以要预测第
n
n
n个词是什么,需要知道前
n
−
1
n-1
n−1个词都是什么,然后查找前
n
−
1
n-1
n−1个词所记录的下一个词的概率。当然不可能取到
n
n
n,据说
n
=
4
n=4
n=4就已经使模型足够大了。你的数据字典中至少包括
n
+
n
2
+
n
3
+
n
4
n+n^2+n^3+n^4
n+n2+n3+n4个词的组合数据。(这完全是暴力的排列组合问题)
来看
n
n
n阶马尔可夫链(Markov chain of order n),也叫
n
n
n元语法(n-grams)。假设
w
1
,
w
2
,
w
3
,
w
4
.
.
.
w
n
w_1,w_2,w_3,w_4...w_n
w1,w2,w3,w4...wn是依次出现的,那么
P
(
w
1
,
w
2
,
w
3
,
.
.
.
w
n
)
=
∏
i
=
1
n
P
(
w
n
∣
w
1
,
w
2
,
w
3
,
.
.
.
w
n
−
1
)
P(w_1,w_2,w_3,...w_n)=\prod_{i=1}^nP(w_n|w_1,w_2,w_3,...w_{n-1})
P(w1,w2,w3,...wn)=i=1∏nP(wn∣w1,w2,w3,...wn−1)
举个例子,当
n
=
2
n=2
n=2时,含有4个词的文本序列出现的概率为
P
(
w
1
,
w
2
,
w
3
,
w
4
)
=
P
(
w
1
)
P
(
w
2
∣
w
1
)
P
(
w
3
∣
w
2
)
P
(
w
4
∣
w
3
)
P(w_1,w_2,w_3,w_4)=P(w_1)P(w_2|w_1)P(w_3|w_2)P(w_4|w_3)
P(w1,w2,w3,w4)=P(w1)P(w2∣w1)P(w3∣w2)P(w4∣w3)
中文解释:首先要计算的(概率)这个序列是
w
1
,
w
2
,
w
3
,
w
4
w_1,w_2,w_3,w_4
w1,w2,w3,w4,他是有顺序的。
w
1
w_1
w1出现的概率为
P
(
w
1
)
P(w_1)
P(w1),由于
n
=
2
n=2
n=2,说明
w
2
w_2
w2出现的概率,和
w
1
w_1
w1有关。(头晕不,明明
n
=
2
n=2
n=2,却只和前1个有关。这个无关紧要,就是取以
w
2
w_2
w2自己为参照,
w
2
w_2
w2占了1个位置)。所以
w
2
w_2
w2出现的概率不应该是
P
(
w
2
)
P(w_2)
P(w2),而是条件概率,当
w
1
w_1
w1出现时,再出现
w
2
w_2
w2的概率
P
(
w
2
∣
w
1
)
P(w_2|w_1)
P(w2∣w1)。其他的也同理可得。(我觉得这个可比推公式简单多了)
上面这个模型有两个缺陷
- 模型占内存太大,如果n取得小又没什么价值了
- 矩阵过于稀疏。因为暴力排列组合,总有没有价值的句子组合出现,他们出现的概率几乎为0
ps.有没有大牛用hidden markov弄个预测彩票的,反正大家买彩票也是看看前几期都出了啥。
数据读取
读文本数据
with open('/home/words.txt') as f:
corpus_chars = f.read()
# 预处理一下把换行符都变成空格
corpus_chars = corpus_chars.replace('\n', ' ').replace('\r', ' ')
建立索引
idx_to_char = list(set(corpus_chars)) # set集合去重,得到一个list,该字符的索引就是它在list的位置,此时idx_to_char中的字符顺序和corpus_chars不一样了。set进行了排序。
char_to_idx = {char: i for i, char in enumerate(idx_to_char)} # 字符到索引的映射
print(idx_to_char)
print(char_to_idx)
# 然后看一下原来的句子和对应的索引
corpus_indices = [char_to_idx[char] for char in corpus_chars] # 将每个字符转化为索引,得到一个索引的序列
print('chars:', ''.join([idx_to_char[idx] for idx in sample]))
print('indices:', corpus_indices)
idx_to_char
[‘逗’, ‘血’, ‘些’, ‘好’, ‘碑’, ‘经’, ‘回’, ‘封’, ‘银’, ‘?’, ‘补’, ‘鸠’, ‘联’, ‘古’, ‘爬’, ‘怯’, ‘谢’, ‘跑’, ‘作’, ‘阻’, ‘意’, ‘汉’, ‘抢’, ‘间’, ‘养’, ‘于’, ‘吸’, ‘而’, ‘篇’, ‘方’, ‘丹’, ‘躲’,
char_to_idx
{‘逗’: 0, ‘血’: 1, ‘些’: 2, ‘好’: 3, ‘碑’: 4, ‘经’: 5, ‘回’: 6, ‘封’: 7, ‘银’: 8, ‘?’: 9, ‘补’: 10, ‘鸠’: 11, ‘联’: 12, ‘古’: 13, ‘爬’: 14, ‘怯’: 15,
chars: 我想要带你去浪漫的土耳其
indices: [361, 139, 266, 61, 270, 75, 187, 361, 139, 913, 567, 588]
时序数据采样
首先解释什么是时序数据,时序数据是说数据的出现时间是有顺序的。“我想要带你去浪漫的土耳其”这句话,‘想’是出现在‘我’后面的,‘土耳其’是出现在‘浪漫的’后面的,不然就不是这句话了。(这仿佛是句废话)
这里引出一个概念:时间步数。举个例子和好理解,当时间步数=5时,输入的字符就是5个,且是一个一个输入的,即‘我’、‘想’、‘要’、‘带’、‘你’。因为这是训练集嘛,训练集就是提前已经知道答案了。这时候输出的就是是‘去’,也可是’想’、‘要’、‘带’、‘你’、‘去’。这就源自于你的输出是多对多还是多对一的。这个在后面要介绍的RNN中会有样例。总之要输出什么,是看你的模型要做什么。
我们以输出’想’、‘要’、‘带’、‘你’、‘去’为例(多对多)。这时候上面的歌词会变成这样的训练集:
‘我’、‘想’、‘要’、‘带’、‘你’——>‘想’、‘要’、‘带’、‘你’、‘去’
‘想’、‘要’、‘带’、‘你’、‘去’——>‘要’、‘带’、‘你’、‘去’、‘浪’
‘要’、‘带’、‘你’、‘去’、‘浪’——>‘带’、‘你’、‘去’、‘浪’、‘漫’
此处省略…
‘浪’、‘漫’、‘的’、‘土’、‘耳’——>‘漫’、‘的’、‘土’、‘耳’、‘其’
可见这些样本有大量重合的序列,所以要用一定的采样方式来提高训练效率。
随机采样
特点:
- 训练数据中的每个字符最多可以出现在一个样本中
- 每个小批量包含的样本数是batch_size,每个样本的长度为num_steps
- 在一个样本中,前后字符是连续的
实现步骤
- 取小于时间步数的值,对序列进行分段。比如取corpus_indices=30,时间步数为num_steps=6,可通过如下方式计算。最后算出来为4。
num_examples = (len(corpus_indices) - 1) // num_steps # 下取整,得到不重叠情况下的样本个数
print(num_examples )
4
- 计算每个样本的第一个字符在corpus_indices中的下标。就是分的这4段,每段从哪个位置开始。
example_indices = [i * num_steps for i in range(num_examples)]
print(example_indices )
[0, 6, 12, 18]
- 体现随机性的操作来了
random.shuffle(example_indices)
- 取数据
def _data(i):
# 返回从i开始的长为num_steps的序列
return corpus_indices[i: i + num_steps]
for i in range(0, num_examples, batch_size):
# 每次选出batch_size个随机样本
# example_indices = [0, 6, 12, 18]
# batch_size = 2
# 所以实际取的值是example_indices[0,2]
# 集合操作左闭右开,实际取的是index=0和1的数,也就是batch_indices =[0, 6]
batch_indices = example_indices[i: i + batch_size] # 当前batch的各个样本的首字符的下标
X = [_data(j) for j in batch_indices]
Y = [_data(j + 1) for j in batch_indices]
yield torch.tensor(X), torch.tensor(Y)
这个函数整体长这样
import torch
import random
def data_iter_random(corpus_indices, batch_size, num_steps, device=None):
# 减1是因为对于长度为n的序列,X最多只有包含其中的前n - 1个字符
num_examples = (len(corpus_indices) - 1) // num_steps # 下取整,得到不重叠情况下的样本个数
example_indices = [i * num_steps for i in range(num_examples)] # 每个样本的第一个字符在corpus_indices中的下标
#random.shuffle(example_indices)
def _data(i):
# 返回从i开始的长为num_steps的序列
return corpus_indices[i: i + num_steps]
for i in range(0, num_examples, batch_size):
# 每次选出batch_size个随机样本
print(example_indices)
print(i,batch_size)
batch_indices = example_indices[i: i + batch_size] # 当前batch的各个样本的首字符的下标
print(batch_indices)
X = [_data(j) for j in batch_indices]
Y = [_data(j + 1) for j in batch_indices]
yield torch.tensor(X), torch.tensor(Y)
my_seq = list(range(30))
for X, Y in data_iter_random(my_seq, batch_size=2, num_steps=6):
print('X: ', X, '\nY:', Y, '\n')
相邻采样
特点:前一个小批量数据和后一个小批量数据是连续的
- 裁剪数据,要能被batch-size整除。如果序列长度为30,batch-size=2,,30本来就能被2整除,所以不用裁剪。如果batch-size=11,那么只保留前22个字符。
corpus_len = len(corpus_indices) // batch_size * batch_size # 保留下来的序列的长度
corpus_indices = corpus_indices[: corpus_len] # 仅保留前corpus_len个字符
- 将序列resize成batch-size行,n列的矩阵。
indices = torch.tensor(corpus_indices)
print(indices)
indices = indices.view(batch_size, -1) # resize成(batch_size, )
print(indices)
tensor([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17,
18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29])
resize后:
tensor([[ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14],
[15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29]])
- 上面已经将原序列分成了两段,接下来要根据num_steps计算每段能取多少次。(因为你不能取的比num_steps还要小吧)
batch_num = (indices.shape[1] - 1) // num_steps
print(batch_num )
2
说明每段可以取2次,indices.shape[1]取的是有多少列。
- 开始取,取两次,每次num_steps个
for i in range(batch_num):
i = i * num_steps
X = indices[:, i: i + num_steps]
Y = indices[:, i + 1: i + num_steps + 1]
yield X, Y
这个函数整体长这样
def data_iter_consecutive(corpus_indices, batch_size, num_steps, device=None):
corpus_len = len(corpus_indices) // batch_size * batch_size # 保留下来的序列的长度
corpus_indices = corpus_indices[: corpus_len] # 仅保留前corpus_len个字符
indices = torch.tensor(corpus_indices)
indices = indices.view(batch_size, -1) # resize成(batch_size, )
batch_num = (indices.shape[1] - 1) // num_steps
for i in range(batch_num):
i = i * num_steps
X = indices[:, i: i + num_steps]
Y = indices[:, i + 1: i + num_steps + 1]
yield X, Y
for X, Y in data_iter_consecutive(my_seq, batch_size=2, num_steps=6):
print('X: ', X, '\nY:', Y, '\n')
总结:我理解的采样就是尽量平均的覆盖到序列的各个位置。相邻采样和RNN网络有关,接下来会介绍。