序列模型
自回归模型
第一种策略,假设在现实情况下相当长的序列Xt-1 ,…,X1 可能是不必要的, 因此我们只需要满足某个长度为τ的时间跨度, 即使用观测序列Xt-1 ,…,Xτ。 当下获得的最直接的好处就是参数的数量总是不变的, 至少在t>τ时如此,这就使我们能够训练一个上面提及的深度网络。 这种模型被称为自回归模型(autoregressive models), 因为它们是对自己执行回归。
第二种策略,如 图所示, 是保留一些对过去观测的总结ht, 并且同时更新预测x"t和ht总结。 这就产生了基于x"t=p(Xt|ht)
估计Xt, 以及ht=g(ht-1,xt-1)公式更新的模型。 由于从未被观测到,这类模型也被称为 隐变量自回归模型(latent autoregressive models)。
小结
- 内插法(在现有观测值之间进行估计)和外推法(对超出已知观测范围进行预测)在实践的难度上差别很大。因此,对于你所拥有的序列数据,在训练时始终要尊重其时间顺序,即最好不要基于未来的数据进行训练。
- 序列模型的估计需要专门的统计工具,两种较流行的选择是自回归模型和隐变量自回归模型。
- 对于时间是向前推进的因果模型,正向估计通常比反向估计更容易。
- 对于直到时间步t的观测序列,其在时间步t+k的预测输出是“k步预测”。随着我们对预测时间值t的增加,会造成误差的快速累积和预测质量的极速下降。
读取数据集
从H.G.Well的时光机器中加载文本。 这是一个相当小的语料库,只有30000多个单词,
我们在这里忽略了标点符号和字母大写。
import collections
import re
from d2l import torch as d2l
#@save
d2l.DATA_HUB['time_machine'] = (d2l.DATA_URL + 'timemachine.txt',
'090b5e7e70c295757f55df93cb0a180b9691891a')
def read_time_machine(): #@save
"""将时间机器数据集加载到文本行的列表中"""
with open(d2l.download('time_machine'), 'r') as f:
lines = f.readlines()#以行为单位读
return [re.sub('[^A-Za-z]+', ' ', line).strip().lower() for line in lines]
#def sub(pattern, repl, string, count=0, flags=0),strip()用于移除字符串头尾指定的字符(默认为空格)或字符序列,lower()转为小写
lines = read_time_machine()
print(f'# 文本总行数: {len(lines)}')
print(lines[0])
print(lines[10])
词元化
下面的tokenize函数将文本行列表(lines)作为输入, 列表中的每个元素是一个文本序列(如一条文本行)。 每个文本序列又被拆分成一个词元列表,词元(token)是文本的基本单位。 最后,返回一个由词元列表组成的列表,其中的每个词元都是一个字符串(string)
def tokenize(lines, token='word'): #@save
"""将文本行拆分为单词或字符词元"""
if token == 'word':
return [line.split() for line in lines]
elif token == 'char':
return [list(line) for line in lines]
else:
print('错误:未知词元类型:' + token)
tokens = tokenize(lines)
for i in range(11):
print(tokens[i])
词表
词元的类型是字符串,而模型需要的输入是数字,因此这种类型不方便模型使用。 现在,让我们构建一个字典,通常也叫做词表(vocabulary), 用来将字符串类型的词元映射到从开始的数字索引中。 我们先将训练集中的所有文档合并在一起,对它们的唯一词元进行统计, 得到的统计结果称之为语料(corpus)。 然后根据每个唯一词元的出现频率,为其分配一个数字索引。 很少出现的词元通常被移除,这可以降低复杂性。 另外,语料库中不存在或已删除的任何词元都将映射到一个特定的未知词元“”。 我们可以选择增加一个列表,用于保存那些被保留的词元, 例如:填充词元(“”); 序列开始词元(“”); 序列结束词元(“”)。
class Vocab:
def __init__(self,tokens=None,min_freq=0,reserved_tokens=None):#reserved_tokens开头词
if tokens is None:
tokens = []
if reserved_tokens is None:
reserved_tokens = []
# 按出现频率排序
# 得到 {词元:出现次数} 形式的字典
counter = count_corpus(tokens)
# 对字典按值降序排序:词元:次数
self._token_freqs = sorted(counter.items(),key=lambda x:x[1],reverse=True)
# 定义词表,即 下标:词元 映射,其中未知词元的索引为0,保留的词元跟在未知词元后面,往后新增词元依次加到保留词元后面
self.idx_to_token = ['<unk>']+reserved_tokens
# 用字典存储 词元和对应的下标idx
self.token_to_idx = {token:idx for idx,token in enumerate(self.idx_to_token)}
for token,freq in self._token_freqs:
# 出现次数小于min_freq的词元我们就直接舍弃了
if freq < min_freq:
break
# 如果该词元不在 下标:词元 映射中就把该词元加进去,同时更新{词元:对应的下标idx}的字典
if token not in self.token_to_idx:
self.idx_to_token.append(token)
self.token_to_idx[token] = len(self.idx_to_token)-1
# 直接返回词表长度即可
def __len__(self):
return len(self.idx_to_token)
def __getitem__(self, tokens):
# 判断tokens是不是列表或元组,如果是,则返回tokens对应的idx值
if not isinstance(tokens,(list,tuple)):
# 如果token_to_idx里有tokens就返回对应的值,否则返回self.unk
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]
# 用@property修饰器修饰方法有两个作用,1是将方法变成属性调用的形式,2是属性是私有的,用户不可修改。
# 此处的意思是 未知词元的索引为0
@property
def unk(self):
return 0
@property
def token_freqs(self):
return self._token_freqs
# 统计词元的频率,这里的tokens是1维或2维列表
def count_corpus(tokens): #@save
"""统计词元的频率"""
# 这里的tokens是1D列表或2D列表,# 一维列表为空 or 是二维列表
if len(tokens) == 0 or isinstance(tokens[0], list):
# 将词元列表展平成一个列表
tokens = [token for line in tokens for token in line]
# 返回字典形式的key:value统计个数,注意没有排序哦!
return collections.Counter(tokens)
整合所有功能
在使用上述函数时,我们将所有功能打包到load_corpus_time_machine函数中, 该函数返回corpus(词元索引列表)和vocab(时光机器语料库的词表)。 我们在这里所做的改变是:
为了简化后面章节中的训练,我们使用字符(而不是单词)实现文本词元化;
时光机器数据集中的每个文本行不一定是一个句子或一个段落,还可能是一个单词,因此返回的corpus仅处理为单个列表,而不是使用多词元列表构成的一个列表。
def load_corpus_time_machine(max_tokens=-1): #@save
"""返回时光机器数据集的词元索引列表和词表"""
lines = read_time_machine()
tokens = tokenize(lines, 'char')
vocab = Vocab(tokens)
# 因为时光机器数据集中的每个文本行不一定是一个句子或一个段落,
# 所以将所有文本行展平到一个列表中
corpus = [vocab[token] for line in tokens for token in line]
if max_tokens > 0:
corpus = corpus[:max_tokens]
return corpus, vocab
corpus, vocab = load_corpus_time_machine()
len(corpus), len(vocab)
小结
文本是序列数据的一种最常见的形式之一。
为了对文本进行预处理,我们通常将文本拆分为词元,构建词表将词元字符串映射为数字索引,并将文本数据转换为词元索引以供模型操作。
语言模型和数据集
学习语言模型
为了训练语言模型,我们需要计算单词的概率, 以及给定前面几个单词后出现某个单词的条件概率。 这些概率本质上就是语言模型的参数。
一种(稍稍不太精确的)方法是统计单词“deep”在数据集中的出现次数, 然后将其除以整个语料库中的单词总数。假设序列长度为2
自然语言统计
一元语法
import random
import torch
from d2l import torch as d2l
tokens = d2l.tokenize(d2l.read_time_machine())
# 因为每个文本行不一定是一个句子或一个段落,因此我们把所有文本行拼接到一起
corpus = [token for line in tokens for token in line]
vocab = d2l.Vocab(corpus)
vocab.token_freqs[:10]
二元语法
bigram_tokens = [pair for pair in zip(corpus[:-1], corpus[1:])]
bigram_vocab = d2l.Vocab(bigram_tokens)
bigram_vocab.token_freqs[:10]
三元语法
trigram_tokens = [triple for triple in zip(
corpus[:-2], corpus[1:-1], corpus[2:])]
trigram_vocab = d2l.Vocab(trigram_tokens)
trigram_vocab.token_freqs[:10]
循环神经网络的从零开始实现
笔记来源:动手学深度学习