Task02:文本预处理;语言模型;循环神经网络基础

注意:以下图片均引用自 《动手学深度学习》

一、文本预处理

预处理一般有四步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))
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章