注意:以下图片均引用自 《动手学深度学习》
一、文本预处理
预处理一般有四步1.读入文本 2.分词 3.建立字典,将每个词映射到一个唯一的索引(index)
4.将文本从此的序列转换为索引序列
1.读入文本
import collections
import re
def read_time_machine():
with open('/home/kesci/input/timemachine7163/timemachine.txt', 'r') as f: ##将文本打开
lines = [re.sub('[^a-z]+', ' ', line.strip().lower()) for line in f] ##运用正则表达式处理文本
return lines
re.sub 表示对 符合 [^a-z]+ 规则的文本 , 用‘ ’ 进行替换,也就是替换成空格
并且用 line.strip().lower() 将每行文本开头或结尾的空格或者换行符去除,并且将所有字母小写化
上面的正则表达式 是指 所有非a-z的字符并且长度大于等于1
经过此次变换我们 只剩下 所有英文字符和空格。
当然 对于 类似于 doesn’t 的处理 会出现 [‘doesn’, ’ ', ‘t’]的结果,这个并不是我们想要的
这里展示一下部分 读取结果
2.分词
现在我们需要把这个list变成每个元素都是一个单词,而不是一个元素有好几个单词的情况,这就是我们要做的分词
def tokenize(setences, token='word'):
if token == 'word': ## 词分词,列表中每个最小元素是个单词
return [setence.split() for setence in setences]
elif token == ‘char’: ##字母级分词,每个最小元素是个字母
return [list(sentence) for sentence in sentences]
else:
print('ERROR: unkown token type '+token)
我们现在做 词分词 所以用默认展示结果
3.建立字典
我们要将上面每个词建立独立的索引编号,并且每个词在字典里不会重复出现
def vocab(tokens):
##set(sum(tokens, [])) sum(list, []) 对列表含列表情况进行去除, 只剩下一个列表
a = {}
for i,j in enumerate(list(set(sum(tokens[0:52], [])))):
a[j] = i
return a
4.将文本从此的序列转换为索引序列
b = []
for i in range(len(tokens[0])):
b.append(a[tokens[0][i]])
b
5.分词工具推荐
现在有几个比较好的现成库,可以直接对语句进行分词
英文分词库我们推荐spacy和nltk
中文分词库我们推荐jieba
现在对jieba进行演示
二、语言模型
学习提问:动手学深度学习 P208 页上原文对 P(W2|W1)的 解释 是 w1,w2两个词相邻的频率和 w1 词频的比值。 p(w1)是w1在训练集中词出现的次数与总次数的比的
而 视频上是说 其中 n(w1) 为语料库中以 w1 作为第一个词的文本的数量, n 为语料库中文本的总数量。其中 n(w1,w2) 为语料库中以 w1 作为第一个词, w2 作为第二个词的文本的数量。
两种说法不一致,是否应该以书本上为准。
个人认为1. P(W1)说法应该以书本为准,是单个单词占所有单词的比重。
2. P(W2|W1)的 解释 是 w1,w2两个词相邻的频率和 w1 词频的比值。 书本上的相邻是否应该改为,以 w1 作为第一个词, w2 作为第二个词的文本的数量。
1.n元语法
在建立语言模型之前,我们需要对句子中的词出现概率,以及一个词在给定前几个词的情况下出现的条件概率。这里我们举个例:
一段含有4个词的文本序列在训练数据集中出现的概率
P(w1)为词w1在训练集中出现的总次数和训练集中总词数的比,即词频。
P(W2|W1)是 给定第一个词是w1的情况下,第二个词是w2的出现总次数,占w1出现总次数的比。以此类推。
然而随着序列长度增加,长词的计算复杂度会指数级增加。n元语法通过马尔科夫假设简化了语言模型的计算,即假设一个词的出现只与前面n个词相关。举例:
n =1时, p(w3|w1,w2) = p(w3|w2)
在实践的时候我们习惯基于n-1阶马尔科夫链
当n为1,2,3时 ,我们将其分别称作一元语法(unigram)、二元语法(bigram)和三元语法(trigram)。注意这时候n-1为0,1,2,所以一元语法与前面词无关,二元语法与前面一个词相关,三元与前面2个词相关。
时序数据采样
现在我们先界定一下我们的输入 以及想要的输出.我们现在做的就是希望通过输入一段文字,来预测他将会要出现的下一段文字是什么.
假设 我们完整的一个训练集是 “想要有直升机,想要和你飞到宇宙去”
对于时序数据,我们还有个 时间步数 作为额外的维度, 简单来说,时间步数就是指我们单次采样的大小, 比如我们这次采样 指定步长为5,就是每一次采样 抽取5个字符做为一个样本.这里我们展示一下 步长为5的可能样本和标签:
随机采样
随机采样 顾名思义,就是在给定的训练集中以及给定步长下, 随机抽取连续的字符且长度等于步长的抽样方法.
每次抽取的批量样本 没有关系
相邻采样
在相邻采样中,相邻的两个随机小批量在原始序列上的位置相邻
举个例子比如有一段序列[0,1,2,3,4,5,6,7,8,9,10,11,12]
我们每次抽取3步长的样本, 每个批量抽取三个样本
第一批的数值 可能为:[[0,1,2], [6,7,8],[3,4,5]]
那么第二批的数值 必然要与 第一批数值相邻 则[[3,4,5],[9,10,11],[6,7,8]]
三、循环神经网络基础
虽然n元语法简化了计算难度,n-1个词前面的词也可能对要出现的词出现影响,为了精度我们就需要扩大n,但这样又会指数级增加计算量。这时候我们又引入了循环神经网络。现在我们简单举例一个字符级循环神经网络(RNN), 字符级也就是单个字为分词, 不以词组为分词。
原句是“想要有直升机”, 现在我们依次输入“想要有直升”,来让模型预测每个字符的下一个字。
如图,在每次预测下一个词是什么的时候,我们总是有两个输入一个是下一个词的前面一个字 比如想要预测“有”, 则一个输入信息是“要”,另一个输入信息是H1,而H1又是又“想”作为输入得到的。所以另一个信息是包含了之前的输入,到最后输入“升”的时候,我们会发现另一个信息包含了之前所有字符的输入。
知识点:one-hot向量, 梯度裁剪, 困惑度
困惑度是对交叉熵损失函数做指数运算后得到的值。特别地,
最佳情况下,模型总是把标签类别的概率预测为1,此时困惑度为1;
最坏情况下,模型总是把标签类别的概率预测为0,此时困惑度为正无穷;
基线情况下,模型总是预测所有类别的概率都相同,此时困惑度为类别个数。
循环神经网络从零实现
def train_and_predict_rnn(rnn, get_params, init_rnn_state, num_hiddens,
vocab_size, device, corpus_indices, idx_to_char,
char_to_idx, is_random_iter, num_epochs, num_steps,
lr, clipping_theta, batch_size, pred_period,
pred_len, prefixes):
if is_random_iter: ## 选用的随机采样还是相邻采样
data_iter_fn = d2l.data_iter_random
else:
data_iter_fn = d2l.data_iter_consecutive
params = get_params() ## 获取(W_xh, W_hh, b_h, W_hq, b_q) 5个参数, 并已经予以初始化
loss = nn.CrossEntropyLoss() ##用交叉熵定义损失函数
for epoch in range(num_epochs):
if not is_random_iter:
state = init_rnn_state(batch_size, num_hiddens, device) ## 如果采样相邻采样, 将初始隐藏层状态初始化, 全为0
l_sum, n, start = 0.0, 0, time.time()
data_iter = data_iter_fn(corpus_indices, batch_size, num_steps, device) ## 获取inputs 样本和标签
for x,y in data_iter:
if is_random_iter:
state = init_rnn_state(batch_size, num_hiddens, device) ## 如果采用随机采样, 在每个小批量更新前初始化隐藏状态
else:
for s in state:
s.detach_()
inputs = to_onehot(x, vocab_size) ## inputs.shape = 时间步长, 字典大小 输入
(outputs, state) = rnn(inputs, state, params) ## outputs有num_steps个形状为(batch_size, vocab_size)的矩阵!!!!! 运行网络
outputs = torch.cat(outputs, dim=0)
# Y的形状是(batch_size, num_steps),转置后再变成形状为
# (num_steps * batch_size,)的向量,这样跟输出的行一一对应
y = torch.flatten(Y.T)
l = loss(outputs, y.long()) ## 使用交叉熵损失计算平均分类误差 计算误差
# 梯度清0
if params[0].grad is not None:
for param in params:
param.grad.data.zero_()
l.backward()
grad_clipping(params, clipping_theta, device) # 裁剪梯度
d2l.sgd(params, lr, 1) # 因为误差已经取过均值,梯度不用再做平均
l_sum += l.item() * y.shape[0]
n += y.shape[0]
if (epoch + 1) % pred_period == 0:
print('epoch %d, perplexity %f, time %.2f sec' % (
epoch + 1, math.exp(l_sum / n), time.time() - start))
for prefix in prefixes:
print(' -', predict_rnn(prefix, pred_len, rnn, params, init_rnn_state,
num_hiddens, vocab_size, device, idx_to_char, char_to_idx))
循环神经网络简洁实现
我们将 字典里的每个词 用one-hot 向量进行编码,所以每个词的input_size会等于字典里词的个数, 之后的隐藏层钟神经单元的个数 是个超参数,这里我们假定个数为256个
num_hiddens = 256
num_steps, batch_size = 35, 2
rnn_layer = nn.RNN(input_size=vocab_size, hidden_size=num_hiddens) ##创建RNN
X = torch.rand(num_steps, batch_size, vocab_size) ##输入 (步长即每次的单词个数, 批量大小即每次样本数量, one-hot向量长度)
state = None ##隐藏层初始状态 None
Y, state_new = rnn_layer(X, state) ## 在RNN网络中 放入 X和state两个输入, 生成Y和 state_new两个新输出
class RNNModel(nn.Module):
def __init__(self, rnn_layer, vocab_size):
super(RNNModel, self).__init__()
self.rnn = rnn_layer
self.hidden_size = rnn_layer.hidden_size * (2 if rnn_layer.bidirectional else 1)
self.vocab_size = vocab_size
self.dense = nn.Linear(self.hidden_size, vocab_size) ## 定义hidden的输出 转为y
def forward(self, inputs, state):
# inputs.shape: (batch_size, num_steps)
X = to_onehot(inputs, vocab_size)
X = torch.stack(X) # X.shape: (num_steps, batch_size, vocab_size)
hiddens, state = self.rnn(X, state)
hiddens = hiddens.view(-1, hiddens.shape[-1]) ## hiddens.shape: (num_steps * batch_size, hidden_size) 调整hiddens 的大小
output = self.dense(hiddens) ## 将hiddens输出 转为Y类型输出
return output, state
def train_and_predict_rnn_pytorch(model, num_hiddens, vocab_size, device,
corpus_indices, idx_to_char, char_to_idx,
num_epochs, num_steps, lr, clipping_theta,
batch_size, pred_period, pred_len, prefixes):
loss = nn.CrossEntropyLoss() ##损失函数
optimizer = torch.optim.Adam(model.parameters(), lr=lr) ##优化器
model.to(device) ## RNN网络构建
for epoch in range(num_epochs):
l_sum, n, start = 0.0, 0, time.time()
data_iter = d2l.data_iter_consecutive(corpus_indices, batch_size, num_steps, device) # 相邻采样
state = None
for X, Y in data_iter:
if state is not None:
# 使用detach函数从计算图分离隐藏状态
if isinstance (state, tuple): # LSTM, state:(h, c)
state[0].detach_()
state[1].detach_()
else:
state.detach_()
(output, state) = model(X, state) # output.shape: (num_steps * batch_size, vocab_size)
y = torch.flatten(Y.T)
l = loss(output, y.long()) ##计算损失
optimizer.zero_grad() ##梯度归零
l.backward()
grad_clipping(model.parameters(), clipping_theta, device) ##梯度裁剪
optimizer.step() ##运行优化器
l_sum += l.item() * y.shape[0]
n += y.shape[0]
if (epoch + 1) % pred_period == 0:
print('epoch %d, perplexity %f, time %.2f sec' % (
epoch + 1, math.exp(l_sum / n), time.time() - start))
for prefix in prefixes:
print(' -', predict_rnn_pytorch(
prefix, pred_len, model, vocab_size, device, idx_to_char,
char_to_idx))