使用序列到序列模型完成数字加法

作者: jm12138
日期: 2021.05
摘要: 本示例介绍如何使用飞桨完成一个数字加法任务,将会使用飞桨提供的LSTM,组建一个序列到序列模型,并在随机生成的数据集上完成数字加法任务的模型训练与预测。

一、环境配置

本教程基于Paddle 2.1 编写,如果你的环境不是本版本,请先参考官网安装 Paddle 2.1 。

  1. # 导入项目运行所需的包
  2. import paddle
  3. import paddle.nn as nn
  4. import random
  5. import numpy as np
  6. from visualdl import LogWriter
  7. # 打印Paddle版本
  8. print('paddle version: %s' % paddle.__version__)
  1. paddle version: 2.1.0

二、构建数据集

  • 随机生成数据,并使用生成的数据构造数据集

  • 通过继承 paddle.io.Dataset 来完成数据集的构造

  1. # 编码函数
  2. def encoder(text, LEN, label_dict):
  3. # 文本转ID
  4. ids = [label_dict[word] for word in text]
  5. # 对长度进行补齐
  6. ids += [label_dict[' ']]*(LEN-len(ids))
  7. return ids
  8. # 单个数据生成函数
  9. def make_data(inputs, labels, DIGITS, label_dict):
  10. MAXLEN = DIGITS + 1 + DIGITS
  11. # 对输入输出文本进行ID编码
  12. inputs = encoder(inputs, MAXLEN, label_dict)
  13. labels = encoder(labels, DIGITS + 1, label_dict)
  14. return inputs, labels
  15. # 批量数据生成函数
  16. def gen_datas(DATA_NUM, MAX_NUM, DIGITS, label_dict):
  17. datas = []
  18. while len(datas)<DATA_NUM:
  19. # 随机取两个数
  20. a = random.randint(0,MAX_NUM)
  21. b = random.randint(0,MAX_NUM)
  22. # 生成输入文本
  23. inputs = '%d+%d' % (a, b)
  24. # 生成输出文本
  25. labels = str(eval(inputs))
  26. # 生成单个数据
  27. inputs, labels = [np.array(_).astype('int64') for _ in make_data(inputs, labels, DIGITS, label_dict)]
  28. datas.append([inputs, labels])
  29. return datas
  30. # 继承paddle.io.Dataset来构造数据集
  31. class Addition_Dataset(paddle.io.Dataset):
  32. # 重写数据集初始化函数
  33. def __init__(self, datas):
  34. super(Addition_Dataset, self).__init__()
  35. self.datas = datas
  36. # 重写生成样本的函数
  37. def __getitem__(self, index):
  38. data, label = [paddle.to_tensor(_) for _ in self.datas[index]]
  39. return data, label
  40. # 重写返回数据集大小的函数
  41. def __len__(self):
  42. return len(self.datas)
  43. print('generating datas..')
  44. # 定义字符表
  45. label_dict = {
  46. '0': 0, '1': 1, '2': 2, '3': 3,
  47. '4': 4, '5': 5, '6': 6, '7': 7,
  48. '8': 8, '9': 9, '+': 10, ' ': 11
  49. }
  50. # 输入数字最大位数
  51. DIGITS = 2
  52. # 数据数量
  53. train_num = 5000
  54. dev_num = 500
  55. # 数据批大小
  56. batch_size = 32
  57. # 读取线程数
  58. num_workers = 8
  59. # 定义一些所需变量
  60. MAXLEN = DIGITS + 1 + DIGITS
  61. MAX_NUM = 10**(DIGITS)-1
  62. # 生成数据
  63. train_datas = gen_datas(
  64. train_num,
  65. MAX_NUM,
  66. DIGITS,
  67. label_dict
  68. )
  69. dev_datas = gen_datas(
  70. dev_num,
  71. MAX_NUM,
  72. DIGITS,
  73. label_dict
  74. )
  75. # 实例化数据集
  76. train_dataset = Addition_Dataset(train_datas)
  77. dev_dataset = Addition_Dataset(dev_datas)
  78. print('making the dataset...')
  79. # 实例化数据读取器
  80. train_reader = paddle.io.DataLoader(
  81. train_dataset,
  82. batch_size=batch_size,
  83. shuffle=True,
  84. drop_last=True
  85. )
  86. dev_reader = paddle.io.DataLoader(
  87. dev_dataset,
  88. batch_size=batch_size,
  89. shuffle=False,
  90. drop_last=True
  91. )
  92. print('finish')
  1. generating datas..
  2. making the dataset...
  3. finish

三、模型组网

  • 通过继承 paddle.nn.Layer 类来搭建模型

  • 本次介绍的模型是一个简单的基于 LSTMSeq2Seq 模型

  • 一共有如下四个主要的网络层:

    1. 嵌入层(Embedding):将输入的文本序列转为嵌入向量

    2. 编码层(LSTM):将嵌入向量进行编码

    3. 解码层(LSTM):将编码向量进行解码

    4. 全连接层(Linear):对解码完成的向量进行线性映射

  • 损失函数为交叉熵损失函数

  1. # 继承paddle.nn.Layer类
  2. class Addition_Model(nn.Layer):
  3. # 重写初始化函数
  4. # 参数:字符表长度、嵌入层大小、隐藏层大小、解码器层数、处理数字的最大位数
  5. def __init__(self, char_len=12, embedding_size=128, hidden_size=128, num_layers=1, DIGITS=2):
  6. super(Addition_Model, self).__init__()
  7. # 初始化变量
  8. self.DIGITS = DIGITS
  9. self.MAXLEN = DIGITS + 1 + DIGITS
  10. self.hidden_size = hidden_size
  11. self.char_len = char_len
  12. # 嵌入层
  13. self.emb = nn.Embedding(
  14. char_len,
  15. embedding_size
  16. )
  17. # 编码器
  18. self.encoder = nn.LSTM(
  19. input_size=embedding_size,
  20. hidden_size=hidden_size,
  21. num_layers=1
  22. )
  23. # 解码器
  24. self.decoder = nn.LSTM(
  25. input_size=hidden_size,
  26. hidden_size=hidden_size,
  27. num_layers=num_layers
  28. )
  29. # 全连接层
  30. self.fc = nn.Linear(
  31. hidden_size,
  32. char_len
  33. )
  34. # 重写模型前向计算函数
  35. # 参数:输入[None, MAXLEN]、标签[None, DIGITS + 1]
  36. def forward(self, inputs, labels=None):
  37. # 嵌入层
  38. out = self.emb(inputs)
  39. # 编码器
  40. out, (_, _) = self.encoder(out)
  41. # 按时间步切分编码器输出
  42. out = paddle.split(out, self.MAXLEN, axis=1)
  43. # 取最后一个时间步的输出并复制 DIGITS + 1 次
  44. out = paddle.expand(out[-1], [out[-1].shape[0], self.DIGITS + 1, self.hidden_size])
  45. # 解码器
  46. out, (_, _) = self.decoder(out)
  47. # 全连接
  48. out = self.fc(out)
  49. # 如果标签存在,则计算其损失和准确率
  50. if labels is not None:
  51. # 转置解码器输出
  52. tmp = paddle.transpose(out, [0, 2, 1])
  53. # 计算交叉熵损失
  54. loss = nn.functional.cross_entropy(tmp, labels, axis=1)
  55. # 计算准确率
  56. acc = paddle.metric.accuracy(paddle.reshape(out, [-1, self.char_len]), paddle.reshape(labels, [-1, 1]))
  57. # 返回损失和准确率
  58. return loss, acc
  59. # 返回输出
  60. return out

四、模型训练与评估

  • 使用 Adam 作为优化器进行模型训练

  • 以模型准确率作为评价指标

  • 使用 VisualDL 对训练数据进行可视化

  • 训练过程中会同时进行模型评估和最佳模型的保存

  1. # 初始化log写入器
  2. log_writer = LogWriter(logdir="./log")
  3. # 模型参数设置
  4. embedding_size = 128
  5. hidden_size=128
  6. num_layers=1
  7. # 训练参数设置
  8. epoch_num = 50
  9. learning_rate = 0.001
  10. log_iter = 2000
  11. eval_iter = 500
  12. # 定义一些所需变量
  13. global_step = 0
  14. log_step = 0
  15. max_acc = 0
  16. # 实例化模型
  17. model = Addition_Model(
  18. char_len=len(label_dict),
  19. embedding_size=embedding_size,
  20. hidden_size=hidden_size,
  21. num_layers=num_layers,
  22. DIGITS=DIGITS)
  23. # 将模型设置为训练模式
  24. model.train()
  25. # 设置优化器,学习率,并且把模型参数给优化器
  26. opt = paddle.optimizer.Adam(
  27. learning_rate=learning_rate,
  28. parameters=model.parameters()
  29. )
  30. # 启动训练,循环epoch_num个轮次
  31. for epoch in range(epoch_num):
  32. # 遍历数据集读取数据
  33. for batch_id, data in enumerate(train_reader()):
  34. # 读取数据
  35. inputs, labels = data
  36. # 模型前向计算
  37. loss, acc = model(inputs, labels=labels)
  38. # 打印训练数据
  39. if global_step%log_iter==0:
  40. print('train epoch:%d step: %d loss:%f acc:%f' % (epoch, global_step, loss.numpy(), acc.numpy()))
  41. log_writer.add_scalar(tag="train/loss", step=log_step, value=loss.numpy())
  42. log_writer.add_scalar(tag="train/acc", step=log_step, value=acc.numpy())
  43. log_step+=1
  44. # 模型验证
  45. if global_step%eval_iter==0:
  46. model.eval()
  47. losses = []
  48. accs = []
  49. for data in dev_reader():
  50. loss_eval, acc_eval = model(inputs, labels=labels)
  51. losses.append(loss_eval.numpy())
  52. accs.append(acc_eval.numpy())
  53. avg_loss = np.concatenate(losses).mean()
  54. avg_acc = np.concatenate(accs).mean()
  55. print('eval epoch:%d step: %d loss:%f acc:%f' % (epoch, global_step, avg_loss, avg_acc))
  56. log_writer.add_scalar(tag="dev/loss", step=log_step, value=avg_loss)
  57. log_writer.add_scalar(tag="dev/acc", step=log_step, value=avg_acc)
  58. # 保存最佳模型
  59. if avg_acc>max_acc:
  60. max_acc = avg_acc
  61. print('saving the best_model...')
  62. paddle.save(model.state_dict(), 'best_model')
  63. model.train()
  64. # 反向传播
  65. loss.backward()
  66. # 使用优化器进行参数优化
  67. opt.step()
  68. # 清除梯度
  69. opt.clear_grad()
  70. # 全局步数加一
  71. global_step += 1
  72. # 保存最终模型
  73. paddle.save(model.state_dict(),'final_model')
  1. train epoch:0 step: 0 loss:2.485989 acc:0.041667
  2. eval epoch:0 step: 0 loss:2.485989 acc:0.041667
  3. saving the best_model...
  4. eval epoch:3 step: 500 loss:1.168023 acc:0.583333
  5. saving the best_model...
  6. eval epoch:6 step: 1000 loss:1.080799 acc:0.583333
  7. eval epoch:9 step: 1500 loss:0.930121 acc:0.645833
  8. saving the best_model...
  9. train epoch:12 step: 2000 loss:0.723319 acc:0.750000
  10. eval epoch:12 step: 2000 loss:0.723319 acc:0.750000
  11. saving the best_model...
  12. eval epoch:16 step: 2500 loss:0.385135 acc:0.875000
  13. saving the best_model...
  14. eval epoch:19 step: 3000 loss:0.200507 acc:0.968750
  15. saving the best_model...

五、模型测试

  • 使用保存的最佳模型进行测试
  1. # 反转字符表
  2. label_dict_adv = {v: k for k, v in label_dict.items()}
  3. # 输入计算题目
  4. input_text = '12+40'
  5. # 编码输入为ID
  6. inputs = encoder(input_text, MAXLEN, label_dict)
  7. # 转换输入为向量形式
  8. inputs = np.array(inputs).reshape(-1, MAXLEN)
  9. inputs = paddle.to_tensor(inputs)
  10. # 加载模型
  11. params_dict= paddle.load('best_model')
  12. model.set_dict(params_dict)
  13. # 设置为评估模式
  14. model.eval()
  15. # 模型推理
  16. out = model(inputs)
  17. # 结果转换
  18. result = ''.join([label_dict_adv[_] for _ in np.argmax(out.numpy(), -1).reshape(-1)])
  19. # 打印结果
  20. print('the model answer: %s=%s' % (input_text, result))
  21. print('the true answer: %s=%s' % (input_text, eval(input_text)))
  1. the model answer: 12+40=52
  2. the true answer: 12+40=52

六、总结

  • 你还可以通过变换网络结构,调整数据集,尝试不同的参数的方式来进一步提升本示例当中的数字加法的效果

  • 同时,也可以尝试在其他的类似的任务中用飞桨来完成实际的实践