Pytorch基础二十、(RNN)

技术
     通过之前的八次分享,比较详细的介绍了CNN以及基于CNN的一些经典网络,想了解Pytorch在CNN方面的模型实战,可以参考下面列表文章:  

Pytorch基础十三、(CNN)

Pytorch基础十四、(LENET)

Pytorch基础十五、(ALEXNET)

Pytorch基础十六、(VGG)

Pytorch基础十七、(GoogleNET)

Pytorch基础十八、(RESNET)

Pytorch基础十九、(DENSENET)

    从本次分享开始介绍NLP领域的网络结构,比如RNN、LSTM和GRU,在介绍这些结构之前,我们首先简单的了解一下语言模型和n元语法的概念。  

1、语言模型

    在自然语言处理中最常见的数据就是文本数据,语言模型就是计算一个序列出现的概率,是自然语言处理非常重要的技术,有很多应用常见,比如拼写纠错、提升语言识别、机器翻译等等。比如英文“you go first”,那么可以翻译成中文“你先走”或者“你走先”,如果通过语言模型计算他们的概率,很显然“你先走”的概率要大一些。  

语言模型的计算方式如下:

例如,一段含有4个词的⽂本序列的概率

picture.image

    从例子中可以很清晰的看到计算概率的逻辑是比较简单的,但是如果一个文本序列包括很长的词序列的话,最后几项的概率很可能为0,导致整个文本序列的概率为0,这显然不合理。我们可以通过马尔科夫假设把计算序列的方式进行简化,这就需要介绍n元语法(n-gram)  

2、n元语法

    ⻢尔可夫假设是指⼀个词的出现只与前⾯ n个词相关,即 n阶⻢尔可夫链。比如1阶马尔科夫(一元语法)认为每个词都是条件独立的,2阶马尔科夫(二元语法)认为每个词只与它的前一个词有关系,以此类推:

⼀元语法、二元语法和三元语法中的概率计算公式分别为:

picture.image

    当 n较⼩时, n元语法往往并不准确。例如,在⼀元语法中,由三个词组成的句子“你走先”和“你先走”的概率是⼀样的。然而,当 n较大时, n元语法需要计算并存储⼤量的词频和多词相邻频率。n是用来权衡计算复杂度和准确率的,在实际应用中,通常取二元语法或者三元语法。


    那么,有没有⽅法在语言模型中更好地平衡以上这两点呢?接下来我们介绍的RNN就是一个好方法。

3、RNN

关于RNN的介绍,有很多资料,读者可以自行查阅,这里主要以介绍在Pytorch的coding,下面我简单说明一下RNN的工作原理:

picture.image

   上图是按照时间步展开的结果,其实每个cell的模型参数都是共享的,也就是说每个时间步的参数是共享的。  

4、下面使用RNN来构建一个语言模型

picture.image

    如上图所示,如果预测“要”,那么需要依赖当前输入“想”,如果预测“有”,那么需要依赖当前输入“要”和上次输入“想”,以此类推。下面我们使用周杰伦歌词作为预料来搭建模型,包括从零开始和高效实现。  

一、从零开始

1.1、读取数据


          
import torch
          
import random
          
import zipfile
          

          
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
          
print(torch.__version__)
          
print(device)
          

          

          
with zipfile.ZipFile('jaychou_lyrics.txt.zip') as zin:
          
    with zin.open('jaychou_lyrics.txt') as f:
          
        corpus_chars = f.read().decode('utf-8')
          
corpus_chars[:40]
      

'想要有直升机\n想要和你飞到宇宙去\n想要和你融化在一起\n融化在宇宙里\n我每天每天每'

1.2、数据预处理

替换换行符为空格,并且取前1万条数据作为训练集


          
corpus_chars = corpus_chars.replace('\n', ' ').replace('\r', ' ')
          
corpus_chars = corpus_chars[0:10000]
      

1.3、建立预料中词的索引


          
idx_to_char = list(set(corpus_chars))
          
char_to_idx = dict([(char, i) for i, char in enumerate(idx_to_char)])
          
vocab_size = len(char_to_idx)
          
corpus_indices = [char_to_idx[char] for char in corpus_chars]
      

1.4、定义时序数据采样方式

1.4.1、随机采样


          
def data_iter_random(corpus_indices, batch_size, num_steps, device=None):
          
    # 减1是因为输出的索引x是相应输入的索引y加1
          
    num_examples = (len(corpus_indices) - 1) // num_steps
          
    epoch_size = num_examples // batch_size
          
    example_indices = list(range(num_examples))
          
    random.shuffle(example_indices)
          

          
    # 返回从pos开始的长为num_steps的序列
          
    def _data(pos):
          
        return corpus_indices[pos: pos + num_steps]
          
    if device is None:
          
        device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
          
    
          
    for i in range(epoch_size):
          
        # 每次读取batch_size个随机样本
          
        i = i * batch_size
          
        batch_indices = example_indices[i: i + batch_size]
          
        X = [_data(j * num_steps) for j in batch_indices]
          
        Y = [_data(j * num_steps + 1) for j in batch_indices]
          
        yield torch.tensor(X, dtype=torch.float32, device=device), torch.tensor(Y, dtype=torch.float32, device=device)
      

1.4.2、相邻采样


          
def data_iter_consecutive(corpus_indices, batch_size, num_steps, device=None):
          
    if device is None:
          
        device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
          
    corpus_indices = torch.tensor(corpus_indices, dtype=torch.float32, device=device)
          
    data_len = len(corpus_indices)
          
    batch_len = data_len // batch_size
          
    indices = corpus_indices[0: batch_size*batch_len].view(batch_size, batch_len)
          
    epoch_size = (batch_len - 1) // num_steps
          
    for i in range(epoch_size):
          
        i = i * num_steps
          
        X = indices[:, i: i + num_steps]
          
        Y = indices[:, i + 1: i + num_steps + 1]
          
        yield X, Y
      

一、从零开始

1.1、定义one-hot函数


          
def one_hot(x, n_class, dtype=torch.float32): 
          
    # X shape: (batch), output shape: (batch, n_class)
          
    x = x.long()
          
    res = torch.zeros(x.shape[0], n_class, dtype=dtype, device=x.device)
          
    res.scatter_(1, x.view(-1, 1), 1)
          
    return res
          
    
          
x = torch.tensor([0, 2])
          
one_hot(x, vocab_size)
      

1.2、定义转换one-hot函数

    我们每次采样的⼩批量的形状是(批量大小, 时间步数)。下面的函数将这样的⼩批量变换成数个可以输⼊进⽹络的形状为(批量大小, 词典⼤小)的矩阵,矩阵个数等于时间步数。

          
def to_onehot(X, n_class):  
          
    # X shape: (batch, seq_len), output: seq_len elements of (batch, n_class)
          
    return [one_hot(X[:, i], n_class) for i in range(X.shape[1])]
          

          
X = torch.arange(10).view(2, 5)
          
inputs = to_onehot(X, vocab_size)
          
print(len(inputs), inputs[0].shape)
      

1.3、初始化模型参数


          
num_inputs, num_hiddens, num_outputs = vocab_size, 256, vocab_size
          
print('will use', device)
          

          
def get_params():
          
    def _one(shape):
          
        ts = torch.tensor(np.random.normal(0, 0.01, size=shape), device=device, dtype=torch.float32)
          
        return torch.nn.Parameter(ts, requires_grad=True)
          

          
    # 隐藏层参数
          
    W_xh = _one((num_inputs, num_hiddens))
          
    W_hh = _one((num_hiddens, num_hiddens))
          
    b_h = torch.nn.Parameter(torch.zeros(num_hiddens, device=device, requires_grad=True))
          
    # 输出层参数
          
    W_hq = _one((num_hiddens, num_outputs))
          
    b_q = torch.nn.Parameter(torch.zeros(num_outputs, device=device, requires_grad=True))
          
    return nn.ParameterList([W_xh, W_hh, b_h, W_hq, b_q])
      

1.4、定义模型

1.4.1、定义 init_rnn_state 函数来返回初始化的隐藏状态。


          
def init_rnn_state(batch_size, num_hiddens, device):
          
    return (torch.zeros((batch_size, num_hiddens), device=device), )
      

1.4.2、 rnn 函数定义了在⼀个时间步⾥如何计算隐藏状态和输出


          
def rnn(inputs, state, params):
          
    # inputs和outputs皆为num_steps个形状为(batch_size, vocab_size)的矩阵
          
    W_xh, W_hh, b_h, W_hq, b_q = params
          
    H, = state
          
    outputs = []
          
    for X in inputs:
          
        H = torch.tanh(torch.matmul(X, W_xh) + torch.matmul(H, W_hh) + b_h)
          
        Y = torch.matmul(H, W_hq) + b_q
          
        outputs.append(Y)
          
    return outputs, (H,)
      

测试一下功能


          
state = init_rnn_state(X.shape[0], num_hiddens, device)
          
inputs = to_onehot(X.to(device), vocab_size)
          
params = get_params()
          
outputs, state_new = rnn(inputs, state, params)
          
print(len(outputs), outputs[0].shape, state_new[0].shape)
      

5 torch.Size([2, 1027]) torch.Size([2, 256])

1.5、定义预测函数


          
def predict_rnn(prefix, num_chars, rnn, params, init_rnn_state,
          
                num_hiddens, vocab_size, device, idx_to_char, char_to_idx):
          
    state = init_rnn_state(1, num_hiddens, device)
          
    output = [char_to_idx[prefix[0]]]
          
    for t in range(num_chars + len(prefix) - 1):
          
        # 将上一时间步的输出作为当前时间步的输入
          
        X = to_onehot(torch.tensor([[output[-1]]], device=device), vocab_size)
          
        # 计算输出和更新隐藏状态
          
        (Y, state) = rnn(X, state, params)
          
        # 下一个时间步的输入是prefix里的字符或者当前的最佳预测字符
          
        if t < len(prefix) - 1:
          
            output.append(char_to_idx[prefix[t + 1]])
          
        else:
          
            output.append(int(Y[0].argmax(dim=1).item()))
          
    return ''.join([idx_to_char[i] for i in output])
      

简单测试一下


          
predict_rnn('分开', 10, rnn, params, init_rnn_state, num_hiddens, vocab_size,
          
            device, idx_to_char, char_to_idx)
      

'分开西圈绪升王凝瓜必客映'

1.6、定义梯度裁剪

为了控制梯度的范围,从而保证模型的训练效率和收敛情况。


          
def grad_clipping(params, theta, device):
          
    norm = torch.tensor([0.0], device=device)
          
    for param in params:
          
        norm += (param.grad.data ** 2).sum()
          
    norm = norm.sqrt().item()
          
    if norm > theta:
          
        for param in params:
          
            param.grad.data *= (theta / norm)
      

1.7、定义训练模型函数

在定义训练模型之前,首先来介绍一个概念-困惑度,困惑度是用来平均语言模型的一个指标,

最佳情况下,模型总是把标签类别的概率预测为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 = data_iter_random
          
    else:
          
        data_iter_fn = data_iter_consecutive
          
    params = get_params()
          
    loss = nn.CrossEntropyLoss()
          

          
    for epoch in range(num_epochs):
          
        if not is_random_iter:  # 如使用相邻采样,在epoch开始时初始化隐藏状态
          
            state = init_rnn_state(batch_size, num_hiddens, device)
          
        l_sum, n, start = 0.0, 0, time.time()
          
        data_iter = data_iter_fn(corpus_indices, batch_size, num_steps, device)
          
        for X, Y in data_iter:
          
            if is_random_iter:  # 如使用随机采样,在每个小批量更新前初始化隐藏状态
          
                state = init_rnn_state(batch_size, num_hiddens, device)
          
            else:  # 否则需要使用detach函数从计算图分离隐藏状态
          
                for s in state:
          
                    s.detach_()
          
            
          
            inputs = to_onehot(X, vocab_size)
          
            # outputs有num_steps个形状为(batch_size, vocab_size)的矩阵
          
            (outputs, state) = rnn(inputs, state, params)
          
            # 拼接之后形状为(num_steps * batch_size, vocab_size)
          
            outputs = torch.cat(outputs, dim=0)
          
            # Y的形状是(batch_size, num_steps),转置后再变成长度为
          
            # batch * num_steps 的向量,这样跟输出的行一一对应
          
            y = torch.transpose(Y, 0, 1).contiguous().view(-1)
          
            # 使用交叉熵损失计算平均分类误差
          
            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))
      

1.8、开始训练

    我们将根据前缀“分开”和“不分开”分别创作⻓度为 50个字符(不考虑前缀⻓度)的一段歌词。

          
num_epochs, num_steps, batch_size, lr, clipping_theta = 250, 35, 32, 1e2, 1e-2
          
pred_period, pred_len, prefixes = 50, 50, ['分开', '不分开']
      

以相邻采样模式进行训练模型


          
train_and_predict_rnn(rnn, get_params, init_rnn_state, num_hiddens,
          
                      vocab_size, device, corpus_indices, idx_to_char,
          
                      char_to_idx, False, num_epochs, num_steps, lr,
          
                      clipping_theta, batch_size, pred_period, pred_len,
          
                      prefixes)
      

epoch 50, perplexity 59.514416, time 0.11 sec

  • 分开 我想要这 我想了空 我想了空 我想了空 我想了空 我想了空 我想了空 我想了空 我想了空 我想了空
  • 不分开 我不要这 全使了双 我想了这 我想了空 我想了空 我想了空 我想了空 我想了空 我想了空 我想了空
    epoch 100, perplexity 6.801417, time 0.11 sec
  • 分开 我说的这样笑 想你都 不着我 我想就这样牵 你你的回不笑多难的 它在云实 有一条事 全你了空
  • 不分开觉 你已经离开我 不知不觉 我跟好这节活 我该好好生活 不知不觉 你跟了离开我 不知不觉 我跟好这节
    epoch 150, perplexity 2.063730, time 0.16 sec
  • 分开 我有到这样牵着你的手不放开 爱可不可以简简单单没有伤 古有你烦 我有多烦恼向 你知带悄 回我的外
  • 不分开觉 你已经很个我 不知不觉 我跟了这节奏 后知后觉 又过了一个秋 后哼哈兮 快使用双截棍 哼哼哈兮
    epoch 200, perplexity 1.300031, time 0.11 sec
  • 分开 我想要这样牵着你的手不放开 爱能不能够永远单甜没有伤害 你 靠着我的肩膀 你 在我胸口睡著 像这样
  • 不分开觉 你已经离开我 不知不觉 我跟了这节奏 后知后觉 又过了一个秋 后知后觉 我该好好生活 我该好好生
    epoch 250, perplexity 1.164455, time 0.11 sec
  • 分开 我有一这样布 对你依依不舍 连隔壁邻居都猜到我现在的感受 河边的风 在吹着头发飘动 牵着你的手 一
  • 不分开觉 你已经离开我 不知不觉 我跟了这节奏 后知后觉 又过了一个秋 后知后觉 我该好好生活 我该好好生

二、高效实现

2.1、定义模型


          
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)
          
        self.state = None
          

          
    def forward(self, inputs, state): # inputs: (batch, seq_len)
          
        # 获取one-hot向量表示
          
        X = d2l.to_onehot(inputs, vocab_size) # X是个list
          
        Y, self.state = self.rnn(torch.stack(X), state)
          
        # 全连接层会首先将Y的形状变成(num_steps * batch_size, num_hiddens),它的输出
          
        # 形状为(num_steps * batch_size, vocab_size)
          
        output = self.dense(Y.view(-1, Y.shape[-1]))
          
        return output, self.state
      

2.2、定义训练函数


          
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)
          
    state = None
          
    for epoch in range(num_epochs):
          
        l_sum, n, start = 0.0, 0, time.time()
          
        data_iter = data_iter_consecutive(corpus_indices, batch_size, num_steps, device) # 相邻采样
          
        for X, Y in data_iter:
          
            if state is not None:
          
                # 使用detach函数从计算图分离隐藏状态, 这是为了
          
                # 使模型参数的梯度计算只依赖一次迭代读取的小批量序列(防止梯度计算开销太大)
          
                if isinstance (state, tuple): # LSTM, state:(h, c)  
          
                    state = (state[0].detach(), state[1].detach())
          
                else:   
          
                    state = state.detach()
          
    
          
            (output, state) = model(X, state) # output: 形状为(num_steps * batch_size, vocab_size)
          
            
          
            # Y的形状是(batch_size, num_steps),转置后再变成长度为
          
            # batch * num_steps 的向量,这样跟输出的行一一对应
          
            y = torch.transpose(Y, 0, 1).contiguous().view(-1)
          
            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]
          
        
          
        try:
          
            perplexity = math.exp(l_sum / n)
          
        except OverflowError:
          
            perplexity = float('inf')
          
        if (epoch + 1) % pred_period == 0:
          
            print('epoch %d, perplexity %f, time %.2f sec' % (
          
                epoch + 1, perplexity, time.time() - start))
          
            for prefix in prefixes:
          
                print(' -', predict_rnn_pytorch(
          
                    prefix, pred_len, model, vocab_size, device, idx_to_char,
          
                    char_to_idx))
      

2.3、开始训练模型


          
num_epochs, batch_size, lr, clipping_theta = 250, 32, 1e-3, 1e-2 # 注意这里的学习率设置
          
pred_period, pred_len, prefixes = 50, 50, ['分开', '不分开']
          
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)
      

epoch 50, perplexity 10.658418, time 0.05 sec

  • 分开始我妈 想要你 我不多 让我心到的 我妈妈 我不能再想 我不多再想 我不要再想 我不多再想 我不要
  • 不分开 我想要你不你 我 你不要 让我心到的 我妈人 可爱女人 坏坏的让我疯狂的可爱女人 坏坏的让我疯狂的
    epoch 100, perplexity 1.308539, time 0.05 sec
  • 分开不会痛 不要 你在黑色幽默 开始了美丽全脸的梦滴 闪烁成回忆 伤人的美丽 你的完美主义 太彻底 让我
  • 不分开不是我不要再想你 我不能这样牵着你的手不放开 爱可不可以简简单单没有伤害 你 靠着我的肩膀 你 在我
    epoch 150, perplexity 1.070370, time 0.05 sec
  • 分开不能去河南嵩山 学少林跟武当 快使用双截棍 哼哼哈兮 快使用双截棍 哼哼哈兮 习武之人切记 仁者无敌
  • 不分开 在我会想通 是谁开没有全有开始 他心今天 一切人看 我 一口令秋软语的姑娘缓缓走过外滩 消失的 旧
    epoch 200, perplexity 1.034663, time 0.05 sec
  • 分开不能去吗周杰伦 才离 没要你在一场悲剧 我的完美主义 太彻底 分手的话像语言暴力 我已无能为力再提起
  • 不分开 让我面到你 爱情来的太快就像龙卷风 离不开暴风圈来不及逃 我不能再想 我不能再想 我不 我不 我不
    epoch 250, perplexity 1.021437, time 0.05 sec
  • 分开 我我外的家边 你知道这 我爱不看的太 我想一个又重来不以 迷已文一只剩下回忆 让我叫带你 你你的
  • 不分开 我我想想和 是你听没不 我不能不想 不知不觉 你已经离开我 不知不觉 我跟了这节奏 后知后觉
0
0
0
0
评论
未登录
看完啦,登录分享一下感受吧~
暂无评论