使用飞桨实现基于LSTM的情感分析模型

接下来让我们看看如何使用飞桨实现一个基于长短时记忆网络的情感分析模型。在飞桨中,不同深度学习模型的训练过程基本一致,流程如下:

  • 数据处理:选择需要使用的数据,并做好必要的预处理工作。

  • 网络定义:使用飞桨定义好网络结构,包括输入层,中间层,输出层,损失函数和优化算法。

  • 网络训练:将准备好的数据送入神经网络进行学习,并观察学习的过程是否正常,如损失函数值是否在降低,也可以打印一些中间步骤的结果出来等。

  • 网络评估:使用测试集合测试训练好的神经网络,看看训练效果如何。

在数据处理前,需要先加载飞桨平台(如果用户在本地使用,请确保已经安装飞桨)。

  1. #encoding=utf8
  2. # Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved.
  3. #
  4. # Licensed under the Apache License, Version 2.0 (the "License");
  5. # you may not use this file except in compliance with the License.
  6. # You may obtain a copy of the License at
  7. #
  8. # http://www.apache.org/licenses/LICENSE-2.0
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS,
  12. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. # See the License for the specific language governing permissions and
  14. # limitations under the License.
  15. import io
  16. import os
  17. import re
  18. import sys
  19. import six
  20. import requests
  21. import string
  22. import tarfile
  23. import hashlib
  24. from collections import OrderedDict
  25. import math
  26. import random
  27. import numpy as np
  28. import paddle
  29. import paddle.fluid as fluid
  30. from paddle.fluid.dygraph.nn import Embedding

数据处理

首先,需要下载语料用于模型训练和评估效果。我们使用的是IMDB的电影评论数据,这个数据集是一个开源的英文数据集,由训练数据和测试数据组成。每个数据都分别由若干小文件组成,每个小文件内部都是一段用户关于某个电影的真实评价,以及他/她对这个电影的情感倾向(是正向还是负向),数据集下载的代码如下:

  1. def download():
  2. #通过python的requests类,下载存储在
  3. #https://dataset.bj.bcebos.com/imdb%2FaclImdb_v1.tar.gz的文件
  4. corpus_url = "https://dataset.bj.bcebos.com/imdb%2FaclImdb_v1.tar.gz"
  5. web_request = requests.get(corpus_url)
  6. corpus = web_request.content
  7. #将下载的文件写在当前目录的aclImdb_v1.tar.gz文件内
  8. with open("./aclImdb_v1.tar.gz", "wb") as f:
  9. f.write(corpus)
  10. f.close()
  11. download()

接下来,将数据集加载到程序中,并打印一小部分数据观察一下数据集的特点,代码如下:

  1. def load_imdb(is_training):
  2. data_set = []
  3. #aclImdb_v1.tar.gz解压后是一个目录
  4. #我们可以使用python的rarfile库进行解压
  5. #训练数据和测试数据已经经过切分,其中训练数据的地址为:
  6. #./aclImdb/train/pos/ 和 ./aclImdb/train/neg/,分别存储着正向情感的数据和负向情感的数据
  7. #我们把数据依次读取出来,并放到data_set里
  8. #data_set中每个元素都是一个二元组,(句子,label),其中label=0表示负向情感,label=1表示正向情感
  9. for label in ["pos", "neg"]:
  10. with tarfile.open("./aclImdb_v1.tar.gz") as tarf:
  11. path_pattern = "aclImdb/train/" + label + "/.*\.txt$" if is_training \
  12. else "aclImdb/test/" + label + "/.*\.txt$"
  13. path_pattern = re.compile(path_pattern)
  14. tf = tarf.next()
  15. while tf != None:
  16. if bool(path_pattern.match(tf.name)):
  17. sentence = tarf.extractfile(tf).read().decode()
  18. sentence_label = 0 if label == 'neg' else 1
  19. data_set.append((sentence, sentence_label))
  20. tf = tarf.next()
  21. return data_set
  22. train_corpus = load_imdb(True)
  23. test_corpus = load_imdb(False)
  24. for i in range(5):
  25. print("sentence %d, %s" % (i, train_corpus[i][0]))
  26. print("sentence %d, label %d" % (i, train_corpus[i][1]))

一般来说,在自然语言处理中,需要先对语料进行切词,这里我们可以使用空格把每个句子切成若干词的序列,代码如下:

  1. def data_preprocess(corpus):
  2. data_set = []
  3. for sentence, sentence_label in corpus:
  4. #这里有一个小trick是把所有的句子转换为小写,从而减小词表的大小
  5. #一般来说这样的做法有助于效果提升
  6. sentence = sentence.strip().lower()
  7. sentence = sentence.split(" ")
  8. data_set.append((sentence, sentence_label))
  9. return data_set
  10. train_corpus = data_preprocess(train_corpus)
  11. test_corpus = data_preprocess(test_corpus)
  12. print(train_corpus[:5])

在经过切词后,需要构造一个词典,把每个词都转化成一个ID,以便于神经网络训练。代码如下:


注意:

在代码中我们使用了一个特殊的单词"[oov]"(out-of-vocabulary),用于表示词表中没有覆盖到的词。之所以使用"[oov]"这个符号,是为了处理某一些词,再测试数据中有,但训练数据没有的现象。


  1. #构造词典,统计每个词的频率,并根据频率将每个词转换为一个整数id
  2. def build_dict(corpus):
  3. word_freq_dict = dict()
  4. for sentence, _ in corpus:
  5. for word in sentence:
  6. if word not in word_freq_dict:
  7. word_freq_dict[word] = 0
  8. word_freq_dict[word] += 1
  9. word_freq_dict = sorted(word_freq_dict.items(), key = lambda x:x[1], reverse = True)
  10. word2id_dict = dict()
  11. word2id_freq = dict()
  12. #一般来说,我们把oov和pad放在词典前面,给他们一个比较小的id,这样比较方便记忆,并且易于后续扩展词表
  13. word2id_dict['[oov]'] = 0
  14. word2id_freq[0] = 1e10
  15. word2id_dict['[pad]'] = 1
  16. word2id_freq[1] = 1e10
  17. for word, freq in word_freq_dict:
  18. word2id_dict[word] = len(word2id_dict)
  19. word2id_freq[word2id_dict[word]] = freq
  20. return word2id_freq, word2id_dict
  21. word2id_freq, word2id_dict = build_dict(train_corpus)
  22. vocab_size = len(word2id_freq)
  23. print("there are totoally %d different words in the corpus" % vocab_size)
  24. for _, (word, word_id) in zip(range(50), word2id_dict.items()):
  25. print("word %s, its id %d, its word freq %d" % (word, word_id, word2id_freq[word_id]))

在完成word2id词典假设之后,我们还需要进一步处理原始语料,把语料中的所有句子都处理成ID序列,代码如下:

  1. #把语料转换为id序列
  2. def convert_corpus_to_id(corpus, word2id_dict):
  3. data_set = []
  4. for sentence, sentence_label in corpus:
  5. #将句子中的词逐个替换成id,如果句子中的词不在词表内,则替换成oov
  6. #这里需要注意,一般来说我们可能需要查看一下test-set中,句子oov的比例,
  7. #如果存在过多oov的情况,那就说明我们的训练数据不足或者切分存在巨大偏差,需要调整
  8. sentence = [word2id_dict[word] if word in word2id_dict \
  9. else word2id_dict['[oov]'] for word in sentence]
  10. data_set.append((sentence, sentence_label))
  11. return data_set
  12. train_corpus = convert_corpus_to_id(train_corpus, word2id_dict)
  13. test_corpus = convert_corpus_to_id(test_corpus, word2id_dict)
  14. print("%d tokens in the corpus" % len(train_corpus))
  15. print(train_corpus[:5])
  16. print(test_corpus[:5])

接下来,我们就可以开始把原始预料中的每个句子通过截断和填充,转换成一个固定长度的句子,并将所有数据整理成mini-batch,用于训练模型,代码如下:

  1. #编写一个迭代器,每次调用这个迭代器都会返回一个新的batch,用于训练或者预测
  2. def build_batch(word2id_dict, corpus, batch_size, epoch_num, max_seq_len, shuffle = True):
  3. #模型将会接受的两个输入:
  4. # 1. 一个形状为[batch_size, max_seq_len]的张量,sentence_batch,代表了一个mini-batch的句子。
  5. # 2. 一个形状为[batch_size, 1]的张量,sentence_label_batch,
  6. # 每个元素都是非0即1,代表了每个句子的情感类别(正向或者负向)
  7. sentence_batch = []
  8. sentence_label_batch = []
  9. for _ in range(epoch_num):
  10. #每个epcoh前都shuffle一下数据,有助于提高模型训练的效果
  11. #但是对于预测任务,不要做数据shuffle
  12. if shuffle:
  13. random.shuffle(corpus)
  14. for sentence, sentence_label in corpus:
  15. sentence_sample = sentence[:min(max_seq_len, len(sentence))]
  16. if len(sentence_sample) < max_seq_len:
  17. for _ in range(max_seq_len - len(sentence_sample)):
  18. sentence_sample.append(word2id_dict['[pad]'])
  19. #飞桨1.6.1要求输入数据必须是形状为[batch_size, max_seq_len,1]的张量
  20. #在飞桨的后续版本中不再存在类似的要求
  21. sentence_sample = [[word_id] for word_id in sentence_sample]
  22. sentence_batch.append(sentence_sample)
  23. sentence_label_batch.append([sentence_label])
  24. if len(sentence_batch) == batch_size:
  25. yield np.array(sentence_batch).astype("int64"), np.array(sentence_label_batch).astype("int64")
  26. sentence_batch = []
  27. sentence_label_batch = []
  28. if len(sentence_batch) == batch_size:
  29. yield np.array(sentence_batch).astype("int64"), np.array(sentence_label_batch).astype("int64")
  30. for _, batch in zip(range(10), build_batch(word2id_dict,
  31. train_corpus, batch_size=3, epoch_num=3, max_seq_len=30)):
  32. print(batch)

网络定义

1. 定义长短时记忆模型

使用飞桨定义一个长短时记忆模型,以便情感分析模型调用,代码如下:

  1. import paddle.fluid as fluid
  2. #使用飞桨实现一个长短时记忆模型
  3. class SimpleLSTMRNN(fluid.Layer):
  4. def __init__(self,
  5. hidden_size,
  6. num_steps,
  7. num_layers=1,
  8. init_scale=0.1,
  9. dropout=None):
  10. #这个模型有几个参数:
  11. #1. hidden_size,表示embedding-size,或者是记忆向量的维度
  12. #2. num_steps,表示这个长短时记忆网络,最多可以考虑多长的时间序列
  13. #3. num_layers,表示这个长短时记忆网络内部有多少层,我们知道,
  14. # 给定一个形状为[batch_size, seq_len, embedding_size]的输入,
  15. # 长短时记忆网络会输出一个同样为[batch_size, seq_len, embedding_size]的输出,
  16. # 我们可以把这个输出再链到一个新的长短时记忆网络上
  17. # 如此叠加多层长短时记忆网络,有助于学习更复杂的句子甚至是篇章。
  18. #4. init_scale,表示网络内部的参数的初始化范围,
  19. # 长短时记忆网络内部用了很多tanh,sigmoid等激活函数,这些函数对数值精度非常敏感,
  20. # 因此我们一般只使用比较小的初始化范围,以保证效果,
  21. super(SimpleLSTMRNN, self).__init__()
  22. self._hidden_size = hidden_size
  23. self._num_layers = num_layers
  24. self._init_scale = init_scale
  25. self._dropout = dropout
  26. self._input = None
  27. self._num_steps = num_steps
  28. self.cell_array = []
  29. self.hidden_array = []
  30. # weight_1_arr用于存储不同层的长短时记忆网络中,不同门的W参数
  31. self.weight_1_arr = []
  32. self.weight_2_arr = []
  33. # bias_arr用于存储不同层的长短时记忆网络中,不同门的b参数
  34. self.bias_arr = []
  35. self.mask_array = []
  36. # 通过使用create_parameter函数,创建不同长短时记忆网络层中的参数
  37. # 通过上面的公式,我们知道,我们总共需要8个形状为[_hidden_size, _hidden_size]的W向量
  38. # 和4个形状为[_hidden_size]的b向量,因此,我们在声明参数的时候,
  39. # 一次性声明一个大小为[self._hidden_size * 2, self._hidden_size * 4]的参数
  40. # 和一个 大小为[self._hidden_size * 4]的参数,这样做的好处是,
  41. # 可以使用一次矩阵计算,同时计算8个不同的矩阵乘法
  42. # 以便加快计算速度
  43. for i in range(self._num_layers):
  44. weight_1 = self.create_parameter(
  45. attr=fluid.ParamAttr(
  46. initializer=fluid.initializer.UniformInitializer(
  47. low=-self._init_scale, high=self._init_scale)),
  48. shape=[self._hidden_size * 2, self._hidden_size * 4],
  49. dtype="float32",
  50. default_initializer=fluid.initializer.UniformInitializer(
  51. low=-self._init_scale, high=self._init_scale))
  52. self.weight_1_arr.append(self.add_parameter('w_%d' % i, weight_1))
  53. bias_1 = self.create_parameter(
  54. attr=fluid.ParamAttr(
  55. initializer=fluid.initializer.UniformInitializer(
  56. low=-self._init_scale, high=self._init_scale)),
  57. shape=[self._hidden_size * 4],
  58. dtype="float32",
  59. default_initializer=fluid.initializer.Constant(0.0))
  60. self.bias_arr.append(self.add_parameter('b_%d' % i, bias_1))
  61. # 定义LSTM网络的前向计算逻辑,飞桨会自动根据前向计算结果,给出反向结果
  62. def forward(self, input_embedding, init_hidden=None, init_cell=None):
  63. self.cell_array = []
  64. self.hidden_array = []
  65. #输入有三个信号:
  66. # 1. input_embedding,这个就是输入句子的embedding表示,
  67. # 是一个形状为[batch_size, seq_len, embedding_size]的张量
  68. # 2. init_hidden,这个表示LSTM中每一层的初始h的值,有时候,
  69. # 我们需要显示地指定这个值,在不需要的时候,就可以把这个值设置为空
  70. # 3. init_cell,这个表示LSTM中每一层的初始c的值,有时候,
  71. # 我们需要显示地指定这个值,在不需要的时候,就可以把这个值设置为空
  72. # 我们需要通过slice操作,把每一层的初始hidden和cell值拿出来,
  73. # 并存储在cell_array和hidden_array中
  74. for i in range(self._num_layers):
  75. pre_hidden = fluid.layers.slice(
  76. init_hidden, axes=[0], starts=[i], ends=[i + 1])
  77. pre_cell = fluid.layers.slice(
  78. init_cell, axes=[0], starts=[i], ends=[i + 1])
  79. pre_hidden = fluid.layers.reshape(
  80. pre_hidden, shape=[-1, self._hidden_size])
  81. pre_cell = fluid.layers.reshape(
  82. pre_cell, shape=[-1, self._hidden_size])
  83. self.hidden_array.append(pre_hidden)
  84. self.cell_array.append(pre_cell)
  85. # res记录了LSTM中每一层的输出结果(hidden)
  86. res = []
  87. for index in range(self._num_steps):
  88. # 首先需要通过slice函数,拿到输入tensor input_embedding中当前位置的词的向量表示
  89. # 并把这个词的向量表示转换为一个大小为 [batch_size, embedding_size]的张量
  90. self._input = fluid.layers.slice(
  91. input_embedding, axes=[1], starts=[index], ends=[index + 1])
  92. self._input = fluid.layers.reshape(
  93. self._input, shape=[-1, self._hidden_size])
  94. # 计算每一层的结果,从下而上
  95. for k in range(self._num_layers):
  96. # 首先获取每一层LSTM对应上一个时间步的hidden,cell,以及当前层的W和b参数
  97. pre_hidden = self.hidden_array[k]
  98. pre_cell = self.cell_array[k]
  99. weight_1 = self.weight_1_arr[k]
  100. bias = self.bias_arr[k]
  101. # 我们把hidden和拿到的当前步的input拼接在一起,便于后续计算
  102. nn = fluid.layers.concat([self._input, pre_hidden], 1)
  103. # 将输入门,遗忘门,输出门等对应的W参数,和输入input和pre-hidden相乘
  104. # 我们通过一步计算,就同时完成了8个不同的矩阵运算,提高了运算效率
  105. gate_input = fluid.layers.matmul(x=nn, y=weight_1)
  106. # 将b参数也加入到前面的运算结果中
  107. gate_input = fluid.layers.elementwise_add(gate_input, bias)
  108. # 通过split函数,将每个门得到的结果拿出来
  109. i, j, f, o = fluid.layers.split(
  110. gate_input, num_or_sections=4, dim=-1)
  111. # 把输入门,遗忘门,输出门等对应的权重作用在当前输入input和pre-hidden上
  112. c = pre_cell * fluid.layers.sigmoid(f) + fluid.layers.sigmoid(
  113. i) * fluid.layers.tanh(j)
  114. m = fluid.layers.tanh(c) * fluid.layers.sigmoid(o)
  115. # 记录当前步骤的计算结果,
  116. # m是当前步骤需要输出的hidden
  117. # c是当前步骤需要输出的cell
  118. self.hidden_array[k] = m
  119. self.cell_array[k] = c
  120. self._input = m
  121. # 一般来说,我们有时候会在LSTM的结果结果内加入dropout操作
  122. # 这样会提高模型的训练鲁棒性
  123. if self._dropout is not None and self._dropout > 0.0:
  124. self._input = fluid.layers.dropout(
  125. self._input,
  126. dropout_prob=self._dropout,
  127. dropout_implementation='upscale_in_train')
  128. res.append(
  129. fluid.layers.reshape(
  130. self._input, shape=[1, -1, self._hidden_size]))
  131. # 计算长短时记忆网络的结果返回回来,包括:
  132. # 1. real_res:每个时间步上不同层的hidden结果
  133. # 2. last_hidden:最后一个时间步中,每一层的hidden的结果,
  134. # 形状为:[batch_size, num_layers, hidden_size]
  135. # 3. last_cell:最后一个时间步中,每一层的cell的结果,
  136. # 形状为:[batch_size, num_layers, hidden_size]
  137. real_res = fluid.layers.concat(res, 0)
  138. real_res = fluid.layers.transpose(x=real_res, perm=[1, 0, 2])
  139. last_hidden = fluid.layers.concat(self.hidden_array, 1)
  140. last_hidden = fluid.layers.reshape(
  141. last_hidden, shape=[-1, self._num_layers, self._hidden_size])
  142. last_hidden = fluid.layers.transpose(x=last_hidden, perm=[1, 0, 2])
  143. last_cell = fluid.layers.concat(self.cell_array, 1)
  144. last_cell = fluid.layers.reshape(
  145. last_cell, shape=[-1, self._num_layers, self._hidden_size])
  146. last_cell = fluid.layers.transpose(x=last_cell, perm=[1, 0, 2])
  147. return real_res, last_hidden, last_cell

2. 定义情感分析模型

长短时记忆模型定义完成后,我们便可以开始定义一个基于长短时记忆模型的情感分析模型,代码如下:

  1. import paddle.fluid as fluid
  2. # 定义一个可以用于情感分类的网络
  3. class SentimentClassifier(fluid.Layer):
  4. def __init__(self,
  5. hidden_size,
  6. vocab_size,
  7. class_num=2,
  8. num_layers=1,
  9. num_steps=128,
  10. init_scale=0.1,
  11. dropout=None):
  12. #这个模型的参数分别为:
  13. #1. hidden_size,表示embedding-size,hidden和cell向量的维度
  14. #2. vocab_size,模型可以考虑的词表大小
  15. #3. class_num,情感类型个数,可以是2分类,也可以是多分类
  16. #4. num_steps,表示这个情感分析模型最大可以考虑的句子长度
  17. #5. init_scale,表示网络内部的参数的初始化范围,
  18. # 长短时记忆网络内部用了很多tanh,sigmoid等激活函数,这些函数对数值精度非常敏感,
  19. # 因此我们一般只使用比较小的初始化范围,以保证效果
  20. super(SentimentClassifier, self).__init__()
  21. self.hidden_size = hidden_size
  22. self.vocab_size = vocab_size
  23. self.class_num = class_num
  24. self.init_scale = init_scale
  25. self.num_layers = num_layers
  26. self.num_steps = num_steps
  27. self.dropout = dropout
  28. # 声明一个LSTM模型,用来把一个句子抽象城一个向量
  29. self.simple_lstm_rnn = SimpleLSTMRNN(
  30. hidden_size,
  31. num_steps,
  32. num_layers=num_layers,
  33. init_scale=init_scale,
  34. dropout=dropout)
  35. # 声明一个embedding层,用来把句子中的每个词转换为向量
  36. self.embedding = Embedding(
  37. size=[vocab_size, hidden_size],
  38. dtype='float32',
  39. is_sparse=False,
  40. param_attr=fluid.ParamAttr(
  41. name='embedding_para',
  42. initializer=fluid.initializer.UniformInitializer(
  43. low=-init_scale, high=init_scale)))
  44. # 在得到一个句子的向量表示后,我们需要根据这个向量表示对这个句子进行分类
  45. # 一般来说,我们可以把这个句子的向量表示,
  46. # 乘以一个大小为[self.hidden_size, self.class_num]的W参数
  47. # 并加上一个大小为[self.class_num]的b参数
  48. # 通过这种手段达到把句子向量映射到分类结果的目标
  49. # 我们需要声明最终在使用句子向量映射到具体情感类别过程中所需要使用的参数
  50. # 这个参数的大小一般是[self.hidden_size, self.class_num]
  51. self.softmax_weight = self.create_parameter(
  52. attr=fluid.ParamAttr(),
  53. shape=[self.hidden_size, self.class_num],
  54. dtype="float32",
  55. default_initializer=fluid.initializer.UniformInitializer(
  56. low=-self.init_scale, high=self.init_scale))
  57. # 同样的,我们需要声明最终分类过程中的b参数
  58. # 这个参数的大小一般是[self.class_num]
  59. self.softmax_bias = self.create_parameter(
  60. attr=fluid.ParamAttr(),
  61. shape=[self.class_num],
  62. dtype="float32",
  63. default_initializer=fluid.initializer.UniformInitializer(
  64. low=-self.init_scale, high=self.init_scale))
  65. def forward(self, input, label):
  66. # 首先我们需要定义LSTM的初始hidden和cell,这里我们使用0来初始化这个序列的记忆
  67. init_hidden_data = np.zeros(
  68. (1, batch_size, embedding_size), dtype='float32')
  69. init_cell_data = np.zeros(
  70. (1, batch_size, embedding_size), dtype='float32')
  71. # 将这些初始记忆转换为飞桨可计算的向量
  72. # 并设置stop-gradient=True,避免这些向量被更新,从而影响训练效果
  73. init_hidden = fluid.dygraph.to_variable(init_hidden_data)
  74. init_hidden.stop_gradient = True
  75. init_cell = fluid.dygraph.to_variable(init_cell_data)
  76. init_cell.stop_gradient = True
  77. init_h = fluid.layers.reshape(
  78. init_hidden, shape=[self.num_layers, -1, self.hidden_size])
  79. init_c = fluid.layers.reshape(
  80. init_cell, shape=[self.num_layers, -1, self.hidden_size])
  81. # 将输入的句子的mini-batch input,转换为词向量表示
  82. x_emb = self.embedding(input)
  83. x_emb = fluid.layers.reshape(
  84. x_emb, shape=[-1, self.num_steps, self.hidden_size])
  85. if self.dropout is not None and self.dropout > 0.0:
  86. x_emb = fluid.layers.dropout(
  87. x_emb,
  88. dropout_prob=self.dropout,
  89. dropout_implementation='upscale_in_train')
  90. # 使用LSTM网络,把每个句子转换为向量表示
  91. rnn_out, last_hidden, last_cell = self.simple_lstm_rnn(x_emb, init_h,
  92. init_c)
  93. last_hidden = fluid.layers.reshape(
  94. last_hidden, shape=[-1, self.hidden_size])
  95. # 将每个句子的向量表示,通过矩阵计算,映射到具体的情感类别上
  96. projection = fluid.layers.matmul(last_hidden, self.softmax_weight)
  97. projection = fluid.layers.elementwise_add(projection, self.softmax_bias)
  98. projection = fluid.layers.reshape(
  99. projection, shape=[-1, self.class_num])
  100. pred = fluid.layers.softmax(projection, axis=-1)
  101. # 根据给定的标签信息,计算整个网络的损失函数,这里我们可以直接使用分类任务中常使用的交叉熵来训练网络
  102. loss = fluid.layers.softmax_with_cross_entropy(
  103. logits=projection, label=label, soft_label=False)
  104. loss = fluid.layers.reduce_mean(loss)
  105. # 最终返回预测结果pred,和网络的loss
  106. return pred, loss

模型训练

在完成模型定义之后,我们就可以开始训练模型了。当训练结束以后,我们可以使用测试集合评估一下当前模型的效果,代码如下:

  1. import paddle.fluid as fluid
  2. #开始训练
  3. batch_size = 128
  4. epoch_num = 5
  5. embedding_size = 256
  6. step = 0
  7. learning_rate = 0.01
  8. max_seq_len = 128
  9. use_gpu=False
  10. place = fluid.CUDAPlace(0) if use_gpu else fluid.CPUPlace()
  11. with fluid.dygraph.guard():
  12. # 创建一个用于情感分类的网络实例,sentiment_classifier
  13. sentiment_classifier = SentimentClassifier(
  14. embedding_size, vocab_size, num_steps=max_seq_len)
  15. # 创建优化器AdamOptimizer,用于更新这个网络的参数
  16. adam = fluid.optimizer.AdamOptimizer(learning_rate=learning_rate, parameter_list = sentiment_classifier.parameters())
  17. for sentences, labels in build_batch(
  18. word2id_dict, train_corpus, batch_size, epoch_num, max_seq_len):
  19. sentences_var = fluid.dygraph.to_variable(sentences)
  20. labels_var = fluid.dygraph.to_variable(labels)
  21. pred, loss = sentiment_classifier(sentences_var, labels_var)
  22. loss.backward()
  23. adam.minimize(loss)
  24. sentiment_classifier.clear_gradients()
  25. step += 1
  26. if step % 10 == 0:
  27. print("step %d, loss %.3f" % (step, loss.numpy()[0]))
  28. # 我们希望在网络训练结束以后评估一下训练好的网络的效果
  29. # 通过eval()函数,将网络设置为eval模式,在eval模式中,网络不会进行梯度更新
  30. sentiment_classifier.eval()
  31. # 这里我们需要记录模型预测结果的准确率
  32. # 对于二分类任务来说,准确率的计算公式为:
  33. # (true_positive + true_negative) /
  34. # (true_positive + true_negative + false_positive + false_negative)
  35. tp = 0.
  36. tn = 0.
  37. fp = 0.
  38. fn = 0.
  39. for sentences, labels in build_batch(
  40. word2id_dict, test_corpus, batch_size, 1, max_seq_len):
  41. sentences_var = fluid.dygraph.to_variable(sentences)
  42. labels_var = fluid.dygraph.to_variable(labels)
  43. # 获取模型对当前batch的输出结果
  44. pred, loss = sentiment_classifier(sentences_var, labels_var)
  45. # 把输出结果转换为numpy array的数据结构
  46. # 遍历这个数据结构,比较预测结果和对应label之间的关系,并更新tp,tn,fp和fn
  47. pred = pred.numpy()
  48. for i in range(len(pred)):
  49. if labels[i][0] == 1:
  50. if pred[i][1] > pred[i][0]:
  51. tp += 1
  52. else:
  53. fn += 1
  54. else:
  55. if pred[i][1] > pred[i][0]:
  56. fp += 1
  57. else:
  58. tn += 1
  59. # 输出最终评估的模型效果
  60. print("the acc in the test set is %.3f" % ((tp + tn) / (tp + tn + fp + fn)))