• 使用困惑度评价模型。
  • 在迭代模型参数前裁剪梯度。
  • 对时序数据采用不同采样方法将导致隐藏状态初始化的不同

 

# 本函数已保存在d2lzh_pytorch包中方便以后使用
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()
    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))
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, True, num_epochs, num_steps, lr,
                      clipping_theta, batch_size, pred_period, pred_len,
                      prefixes)
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)

这段代码实现了一个完整的训练和预测流程,用于训练一个基于 RNN 的语言模型,并在每个 epoch 结束时进行预测。下面我将对代码进行详细解析,并指出一些关键点和优化建议。

### 关键点解析

1. **数据迭代器选择**:
   • 根据 `is_random_iter` 的值选择使用随机采样或相邻采样的数据迭代器。

2. **参数初始化**:
   • `params = get_params()`:获取模型参数。
   • `loss = nn.CrossEntropyLoss()`:定义交叉熵损失函数。

3. **训练循环**:
   • 对于每个 epoch:
     ◦ 初始化隐藏状态(如果使用相邻采样)。
     ◦ 初始化数据迭代器。
     ◦ 遍历每个批次的数据:
       ◦ 初始化隐藏状态(如果使用随机采样)。
       ◦ 分离隐藏状态(如果使用相邻采样),防止梯度累积。
       ◦ 将输入数据转换为 one-hot 编码。
       ◦ 前向传播计算输出和更新隐藏状态。
       ◦ 计算损失并进行反向传播。
       ◦ 梯度裁剪,防止梯度爆炸。
       ◦ 更新模型参数。
       ◦ 累加损失值和样本数。

4. **预测**:
   • 每隔 `pred_period` 个 epoch 进行一次预测,并打印预测结果。

### 代码优化建议

1. **使用 `nn.Module`**:
   将训练和预测逻辑封装到一个 `nn.Module` 子类中,可以更好地管理模型参数和状态。

   ```python
   class RNNModel(nn.Module):
       def __init__(self, ...):
           ...
       def forward(self, inputs, state):
           ...
   ```

2. **使用内置 RNN 层**:
   使用 `torch.nn.RNN` 或 `torch.nn.LSTM` 可以简化代码并提高性能。

3. **批量处理**:
   如果需要生成多个序列,可以考虑批量处理以提高效率。

4. **学习率调度**:
   使用学习率调度器(如 `torch.optim.lr_scheduler`)来动态调整学习率,提高训练效果。

5. **日志记录**:
   使用日志库(如 `logging`)来记录训练过程中的信息,而不是直接打印。

6. **代码结构优化**:
   将训练和预测逻辑分离到不同的函数中,提高代码的可读性和可维护性。

### 示例优化后的代码结构

```



### 示例优化后的代码结构

```python
class RNNModel(nn.Module):
    def __init__(self, ...):
        ...
    def forward(self, inputs, state):
        ...

def train(model, data_iter, loss_fn, optimizer, clipping_theta):
    ...
def predict(model, prefix, num_chars, ...):
    ...

# 主函数
def main():
    model = RNNModel(...)
    optimizer = torch.optim.SGD(model.parameters(), lr=...)
    loss_fn = nn.CrossEntropyLoss()
    
    for epoch in range(num_epochs):
        train(model, data_iter, loss_fn, optimizer, clipping_theta)
        if (epoch + 1) % pred_period == 0:
            for prefix in prefixes:
                predict(model, prefix, pred_len, ...)

if __name__ == "__main__":
    main()



```

通过这些优化,代码将更加模块化、可读性更强,并且更易于维护和扩展。

这段代码实现了一个完整的训练和预测流程,用于训练一个基于 RNN 的语言模型,并在每个 epoch 结束时进行预测。下面我将对代码进行详细解析:

### 代码解析

1. **参数设置**:
   • `rnn`:预训练的 RNN 模型。
   • `get_params`:获取模型参数的函数。
   • `init_rnn_state`:初始化 RNN 状态的函数。
   • `num_hiddens`:隐藏层的大小。
   • `vocab_size`:词汇表的大小。
   • `device`:计算设备(CPU 或 GPU)。
   • `corpus_indices`:预处理后的语料库索引。
   • `idx_to_char`:索引到字符的映射。
   • `char_to_idx`:字符到索引的映射。
   • `is_random_iter`:是否使用随机采样。
   • `num_epochs`:训练的 epoch 数。
   • `num_steps`:每个样本的时间步长。
   • `lr`:学习率。
   • `clipping_theta`:梯度裁剪的阈值。
   • `batch_size`:每个批次的数据量。
   • `pred_period`:每隔多少个 epoch 进行一次预测。
   • `pred_len`:每次预测生成的字符数。
   • `prefixes`:用于预测的前缀列表。

2. **数据迭代器选择**:
   ```python
   if is_random_iter:
       data_iter_fn = d2l.data_iter_random
   else:
       data_iter_fn = d2l.data_iter_consecutive
   ```
   根据 `is_random_iter` 的值选择使用随机采样或相邻采样的数据迭代器。

3. **初始化参数和损失函数**:
   ```python
   params = get_params()
   loss = nn.CrossEntropyLoss()
   ```

4. **训练循环**:
   ```python
   for epoch in range(num_epochs):
       ...
   ```
   对于每个 epoch:
   • 初始化隐藏状态(如果使用相邻采样)。
   • 初始化数据迭代器。
   • 遍历每个批次的数据:
     ◦ 初始化隐藏状态(如果使用随机采样)。
     ◦ 分离隐藏状态(如果使用相邻采样)。
     ◦ 将输入数据转换为 one-hot 编码。
     ◦ 前向传播计算输出和更新隐藏状态。
     ◦ 计算损失并进行反向传播。
     ◦ 梯度裁剪。
     ◦ 更新模型参数。
     ◦ 累加损失值和样本数。

5. **预测**:
   ```python
   if (epoch + 1) % pred_period == 0:
       ...
   ```
   每隔 `pred_period` 个 epoch 进行一次预测,并打印预测结果。

### 关键点

• **梯度裁剪**:防止梯度爆炸问题。
• **分离隐藏状态**:确保梯度计算只依赖一次迭代读取的小批量序列,防止梯度计算开销太大。
• **one-hot 编码**:将输入数据转换为 one-hot 编码,以便进行前向传播。

### 优化建议

1. **使用 `nn.Module`**:
   将训练和预测逻辑封装到一个 `nn.Module` 子类中,可以更好地管理模型参数和状态。

2. **使用内置 RNN 层**:
   使用 `torch.nn.RNN` 或 `torch.nn.LSTM` 可以简化代码并提高性能。

3. **批量处理**:
   如果需要生成多个序列,可以考虑批量处理以提高效率。

4. **学习率调度**:
   使用学习率调度器(如 `torch.optim.lr_scheduler`)来动态调整学习率,提高训练效果。

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐