【动手学深度学习----现代循环神经网络笔记】

GRU介绍

门控循环单元包含重置门和更新门两种,输入是由当前时间步的输入和前一时间步的隐状态给出。两个门的输出是由使用sigmoid激活函数的两个全连接层给出。
二者的特点:

  1. 重置门有助于捕获序列中的短期依赖关系。
  2. 更新门有助于捕获序列中的长期依赖关系。

【动手学深度学习----现代循环神经网络笔记】_第1张图片
两种门的数学表达式:
【动手学深度学习----现代循环神经网络笔记】_第2张图片
重置门Rt与常规隐状态更新机制集成后,可以得到时间步t的候选隐状态H_hat(n x h的大小):
在这里插入图片描述
得到候选隐状态H_hat后,与更新们Zt结合:
在这里插入图片描述

得到最终的Ht。
代码实现

def gru(inputs, state, params):
    W_xz, W_hz, b_z, W_xr, W_hr, b_r, W_xh, W_hh, b_h, W_hq, b_q = params
    H, = state
    outputs = []
    for X in inputs:
        Z = torch.sigmoid((X @ W_xz) + (H @ W_hz) + b_z)
        R = torch.sigmoid((X @ W_xr) + (H @ W_hr) + b_r)
        H_tilda = torch.tanh((X @ W_xh) + ((R * H) @ W_hh) + b_h)
        H = Z * H + (1 - Z) * H_tilda
        Y = H @ W_hq + b_q
        outputs.append(Y)
    return torch.cat(outputs, dim=0), (H,)

长短期记忆网络(LSTM)介绍

特点:可以缓解梯度消失和梯度爆炸
LSTM引入了记忆元,同时为了控制记忆元,又使用了输入门、输出门、遗忘门三个门,注意它们由三个具有sigmoid激活函数的全连接层处理。
【动手学深度学习----现代循环神经网络笔记】_第3张图片
三种门的数学表达式为
【动手学深度学习----现代循环神经网络笔记】_第4张图片
根据上一时刻的隐状态和当前时刻的输入,可以得到候选记忆元C~:
在这里插入图片描述
根据候选记忆单元和遗忘门,输入门以及上一时刻的记忆元可以得到当前时刻的记忆元:
在这里插入图片描述
最后需要计算一下隐状态:
在这里插入图片描述
代码实现

def lstm(inputs, state, params):
    [W_xi, W_hi, b_i, W_xf, W_hf, b_f, W_xo, W_ho, b_o, W_xc, W_hc, b_c,
     W_hq, b_q] = params
    (H, C) = state
    outputs = []
    for X in inputs:
        I = torch.sigmoid((X @ W_xi) + (H @ W_hi) + b_i)
        F = torch.sigmoid((X @ W_xf) + (H @ W_hf) + b_f)
        O = torch.sigmoid((X @ W_xo) + (H @ W_ho) + b_o)
        C_tilda = torch.tanh((X @ W_xc) + (H @ W_hc) + b_c)
        C = F * C + I * C_tilda
        H = O * torch.tanh(C)
        Y = (H @ W_hq) + b_q
        outputs.append(Y)
    return torch.cat(outputs, dim=0), (H, C)

深度循环神经网络

注意L是隐藏层数,T为时间步
【动手学深度学习----现代循环神经网络笔记】_第5张图片
其中隐藏层数目L和隐藏单元数目h都是超参数,其数学表达式为:
【动手学深度学习----现代循环神经网络笔记】_第6张图片
Ht的大小一直不变注意。

到目前为止介绍的模型都是进行预测下一个词元的

双向循环神经网络

这种模型可以实现根据上下文内容,对中间的某个词元进行预测。
具有单个隐藏层的双向循环神经网络的架构
【动手学深度学习----现代循环神经网络笔记】_第7张图片
数学表达式为:(注意一些矩阵大小的变化):
【动手学深度学习----现代循环神经网络笔记】_第8张图片
注意,双向循环神经网络对预测未来词元的能力,有很大的缺陷,即使困惑度看起来很合理。同时因为梯度链更长,所以双向循环神经网络的训练代价非常高。

机器翻译

第一步:准备数据集

  1. 下载Tatoeba项目的双语句子对 组成的“英-法”数据集。
  2. 预处理操作:1)用空格替换不间断的空格;2)用小写字母替换大写字母;3)在单词和标点符号之间插入空格。
#@save
def preprocess_nmt(text):
    """预处理“英语-法语”数据集"""
    def no_space(char, prev_char):
        return char in set(',.!?') and prev_char != ' '

    # 使用空格替换不间断空格
    # 使用小写字母替换大写字母
    text = text.replace('\u202f', ' ').replace('\xa0', ' ').lower()
    # 在单词和标点符号之间插入空格
    out = [' ' + char if i > 0 and no_space(char, text[i - 1]) else char
           for i, char in enumerate(text)]
    return ''.join(out)

第二步:词元化

这里进行的是单词级词元化。

#@save
def tokenize_nmt(text, num_examples=None): # num_examples用来控制对多少个文本序列处理
    """词元化“英语-法语”数据数据集"""
    source, target = [], []
    for i, line in enumerate(text.split('\n')):
        if num_examples and i > num_examples:
            break
        parts = line.split('\t')
        if len(parts) == 2:
            source.append(parts[0].split(' ')) # 存放英语:【单词,标点】
            target.append(parts[1].split(' ')) # 存放法语:【法语,标点】
    return source, target

第三步:创建词表

把出现次数少于2次的低频率词元视为相同的未知词元,并且指定了额外的特定词元,如用来填充到相同的填充词元,序列开始词元结束词元

src_vocab = d2l.Vocab(source, min_freq=2,
                      reserved_tokens=['', '', ''])
len(src_vocab)

第四步:加载数据集

通过截断(truncation)和 填充(padding)方式实现一次只处理一个小批量的文本序列。

#@save
def truncate_pad(line, num_steps, padding_token):
    """截断或填充文本序列"""
    if len(line) > num_steps:
        return line[:num_steps]  # 截断
    return line + [padding_token] * (num_steps - len(line))  # 填充

truncate_pad(src_vocab[source[0]], 10, src_vocab[''])

然后定义 文本序列转换为小批量数据集训练的函数,为每一行文本序列结尾都添加一个的结束词元。valid_len是每个文本序列的长度,他不包含填充词元:

#@save
def build_array_nmt(lines, vocab, num_steps):
    """将机器翻译的文本序列转换成小批量"""
    lines = [vocab[l] for l in lines]
    lines = [l + [vocab['']] for l in lines]
    array = torch.tensor([truncate_pad(
        l, num_steps, vocab['']) for l in lines])
    valid_len = (array != vocab['']).type(torch.int32).sum(1)
    return array, valid_len

第五步:整合上面功能生成数据迭代器

#@save
def load_data_nmt(batch_size, num_steps, num_examples=600):
    """返回翻译数据集的迭代器和词表"""
    text = preprocess_nmt(read_data_nmt())
    source, target = tokenize_nmt(text, num_examples)
    src_vocab = d2l.Vocab(source, min_freq=2,
                          reserved_tokens=['', '', ''])
    tgt_vocab = d2l.Vocab(target, min_freq=2,
                          reserved_tokens=['', '', ''])
    src_array, src_valid_len = build_array_nmt(source, src_vocab, num_steps)
    tgt_array, tgt_valid_len = build_array_nmt(target, tgt_vocab, num_steps)
    data_arrays = (src_array, src_valid_len, tgt_array, tgt_valid_len)
    data_iter = d2l.load_array(data_arrays, batch_size)
    return data_iter, src_vocab, tgt_vocab

第六步,构建编码器-解码器架构(seq2seq 序列到序列学习)

机器翻译是序列转换模型的一个核心问题, 其输入和输出都是长度可变的序列。 为了处理这种类型的输入和输出,第一个组件是一个编码器(encoder): 它接受一个长度可变的序列作为输入, 并将其转换为具有固定形状的编码状态。 第二个组件是解码器(decoder): 它将固定形状的编码状态映射到长度可变的序列。
【动手学深度学习----现代循环神经网络笔记】_第9张图片
循环神经网络效果:
【动手学深度学习----现代循环神经网络笔记】_第10张图片

6.1定义编码器

编码器将长度可变的输入序列转换成 形状固定的上下文变量 c , 并且将输入序列的信息在该上下文变量中进行编码,可以得到下面的数学公式:
【动手学深度学习----现代循环神经网络笔记】_第11张图片
实现循环神经网络编码器时,使用了嵌入层(embedding layer)来获得输入序列中每个词元的特征向量,他的大小为(输入词表的大小(vocab_size) X 特征向量的大小(embed_size)),对于任意输入词元的索引 i , 嵌入层获取权重矩阵的第 i 行(从 0 开始)以返回其特征向量。

#@save
class Seq2SeqEncoder(d2l.Encoder):
    """用于序列到序列学习的循环神经网络编码器"""
    def __init__(self, vocab_size, embed_size, num_hiddens, num_layers,
                 dropout=0, **kwargs):
        super(Seq2SeqEncoder, self).__init__(**kwargs)
        # 嵌入层
        self.embedding = nn.Embedding(vocab_size, embed_size)
        self.rnn = nn.GRU(embed_size, num_hiddens, num_layers,
                          dropout=dropout)

    def forward(self, X, *args):
        # 输出'X'的形状:(batch_size,num_steps,embed_size)
        X = self.embedding(X)# 假如输入为(4,7),经过嵌入层变为了(4,7,8)
        # 在循环神经网络模型中,第一个轴对应于时间步
        X = X.permute(1, 0, 2)# 第一维和第二维交换了下位置,(7,4,8)
        # 如果未提及状态,则默认为0
        output, state = self.rnn(X)# output为(7,4,16),state为(2,4,16)
        # output的形状:(num_steps,batch_size,num_hiddens)
        # state[0]的形状:(num_layers,batch_size,num_hiddens)
        return output, state

TIPS:嵌入层替换了one-hot的方法,可以节省很多的内存

6.2 定义解码器

解码器的数学公式如下:
【动手学深度学习----现代循环神经网络笔记】_第12张图片
代码实现

class Seq2SeqDecoder(d2l.Decoder):
    """用于序列到序列学习的循环神经网络解码器"""
    def __init__(self, vocab_size, embed_size, num_hiddens, num_layers,
                 dropout=0, **kwargs):
        super(Seq2SeqDecoder, self).__init__(**kwargs)
        self.embedding = nn.Embedding(vocab_size, embed_size)
        self.rnn = nn.GRU(embed_size + num_hiddens, num_hiddens, num_layers,
                          dropout=dropout)
        self.dense = nn.Linear(num_hiddens, vocab_size)

    def init_state(self, enc_outputs, *args):
        return enc_outputs[1]

    def forward(self, X, state):
        # 输出'X'的形状:(batch_size,num_steps,embed_size)
        X = self.embedding(X).permute(1, 0, 2) # (num_steps,batch_size,embed_size)
        # 广播context,使其具有与X相同的num_steps
        context = state[-1].repeat(X.shape[0], 1, 1) # 用state的最后一个词元应该是来填充,同时s大小为(X,shape[0],state[1],state[2])
        X_and_context = torch.cat((X, context), 2)# 
        output, state = self.rnn(X_and_context, state)
        output = self.dense(output).permute(1, 0, 2)
        # output的形状:(batch_size,num_steps,vocab_size)
        # state[0]的形状:(num_layers,batch_size,num_hiddens)
        return output, state

    """调试输出结果
X's size:torch.Size([7, 4, 8])
state's size:torch.Size([2, 4, 16])
context's size:torch.Size([7, 4, 16])
X_and_context's size:torch.Size([7, 4, 24])
output's size:torch.Size([4, 7, 10])
state's size:torch.Size([2, 4, 16])
"""

6.3 损失函数

类似于语言模型,可以使用softmax来获得分布, 并通过计算交叉熵损失函数来进行优化,并且因为我们给序列填充了特殊词元,所以我们应该把这些给排除在外,当计算loss时。以下代码通过0值化屏蔽不想关的项,来使后面任何不想干预测的计算都是与零的乘积,结果都等于零。

#@save
def sequence_mask(X, valid_len, value=0):
    """在序列中屏蔽不相关的项"""
    maxlen = X.size(1)
    mask = torch.arange((maxlen), dtype=torch.float32,
                        device=X.device)[None, :] < valid_len[:, None]
    # 这里的代码 [:,None]来增加维度,比如[0,1,2,3,4],则[:,None]的结果为[[0],[1],[2]],[None,:]的结果为[[0 1 2 3 4]]
    X[~mask] = value
    return X


6.4训练

特定的序列开始词元(“”)和 原始的输出序列(不包括序列结束词元“”) 拼接在一起作为解码器的输入。 这被称为强制教学(teacher forcing), 因为原始的输出序列(词元的标签)被送入解码器。 或者,将来自上一个时间步的预测得到的词元作为解码器的当前输入。

代码实现(部分)

    for epoch in range(num_epochs):
        timer = d2l.Timer()
        metric = d2l.Accumulator(2) # 训练损失总和,词元数量
        for batch in data_iter:
            optimizer.zero_grad()
            X,X_valid_len,Y,Y_valid_len = [x.to(device) for x in batch]
            bos = torch.tensor([tgt_vocab['']] * Y.shape[0],
                               device=device).reshape(-1,1)
            print("bos'size: " + str(bos.shape))
            print("Y'size: " +str(Y.shape))
            dec_input = torch.cat([bos,Y[:,:-1]],1) # 强制教学,这里Y[:,:-1]的shape为(64,9),就是不取最后一列
            print("dec_input'ssize: " + str(dec_input.shape))
            Y_hat,_ = net(X,dec_input,X_valid_len)
            l = loss(Y_hat,Y,Y_valid_len)
            l.sum().backward() # 损失函数的标量进行“反向传播”
            d2l.grad_clipping(net,1)
            num_tokens = Y_valid_len.sum()
            optimizer.step()
            with torch.no_grad():
                metric.add(l.sum(),num_tokens)
"""
bos'size: torch.Size([64, 1])
Y'size: torch.Size([64, 10])
dec_input'ssize: torch.Size([64, 10]) 编码器输入
"""

6.5预测

这个部分的代码我是在不是特别懂,尤其是那个for _ in range(num_steps)的循环。。过两天再研究吧。

#@save
def predict_seq2seq(net, src_sentence, src_vocab, tgt_vocab, num_steps,
                    device, save_attention_weights=False):
    """序列到序列模型的预测"""
    # 在预测时将net设置为评估模式
    net.eval()
    src_tokens = src_vocab[src_sentence.lower().split(' ')] + [
        src_vocab['']]
    enc_valid_len = torch.tensor([len(src_tokens)], device=device)
    src_tokens = d2l.truncate_pad(src_tokens, num_steps, src_vocab[''])
    # 添加批量轴
    enc_X = torch.unsqueeze(
        torch.tensor(src_tokens, dtype=torch.long, device=device), dim=0)
    enc_outputs = net.encoder(enc_X, enc_valid_len)
    dec_state = net.decoder.init_state(enc_outputs, enc_valid_len)
    # 添加批量轴
    dec_X = torch.unsqueeze(torch.tensor(
        [tgt_vocab['']], dtype=torch.long, device=device), dim=0)
    output_seq, attention_weight_seq = [], []
    for _ in range(num_steps):
        Y, dec_state = net.decoder(dec_X, dec_state)
        # 我们使用具有预测最高可能性的词元,作为解码器在下一时间步的输入
        dec_X = Y.argmax(dim=2)
        pred = dec_X.squeeze(dim=0).type(torch.int32).item()
        # 保存注意力权重(稍后讨论)
        if save_attention_weights:
            attention_weight_seq.append(net.decoder.attention_weights)
        # 一旦序列结束词元被预测,输出序列的生成就完成了
        if pred == tgt_vocab['']:
            break
        output_seq.append(pred)
    return ' '.join(tgt_vocab.to_tokens(output_seq)), attention_weight_seq

6.6预测序列的评估BLEU

BLEU的评估是这个n元语法是否出现在标签序列中,定义为:
【动手学深度学习----现代循环神经网络笔记】_第13张图片
当预测序列与标签序列完全相同时,BLEU为1.
代码的实现

def bleu(pred_seq, label_seq, k):  #@save
    """计算BLEU"""
    pred_tokens, label_tokens = pred_seq.split(' '), label_seq.split(' ')
    len_pred, len_label = len(pred_tokens), len(label_tokens)
    score = math.exp(min(0, 1 - len_label / len_pred))
    for n in range(1, k + 1):
        num_matches, label_subs = 0, collections.defaultdict(int)
        for i in range(len_label - n + 1):
            label_subs[' '.join(label_tokens[i: i + n])] += 1
        for i in range(len_pred - n + 1):
            if label_subs[' '.join(pred_tokens[i: i + n])] > 0:
                num_matches += 1
                label_subs[' '.join(pred_tokens[i: i + n])] -= 1
        score *= math.pow(num_matches / (len_pred - n + 1), math.pow(0.5, n))
    return score

TIPS

  1. 使用tanh函数的一个目的是把输出值变为(-1,1)。

你可能感兴趣的:(DL和ML笔记,深度学习,rnn,lstm)