65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 Python 实现 LSTM 网络(使用 TensorFlow 和 Keras)的两种方法

starIconstarIconstarIconstarIconstarIcon

5.00/5 (5投票s)

2018年4月2日

CPOL

8分钟阅读

viewsIcon

15610

在前一篇文章中,我们讨论了强大的循环神经网络——长短期记忆(LSTM)网络的工作原理。现在,我们将学习如何实现这种网络。

在前一篇文章中,我们讨论了强大的循环神经网络——长短期记忆(LSTM)网络的工作原理。它们不仅仅是将输出信息传递到下一个时间步,还会存储和传递所谓的 LSTM 单元的状态。这个单元内部包含四个神经网络——门(gates),用于决定哪些信息会被存储在单元状态中并被传递到输出。因此,网络在某个时间步的输出不仅依赖于前一个时间步,还依赖于n个前时间步。

好的,理论部分就到这里,为实际实现这类网络做好准备。在本文中,我们将考虑两个类似的语言建模问题,并使用两种不同的 API 来解决它们。首先,我们将创建一个能够根据提供的文本片段预测单词的网络,为此我们将使用 TensorFlow。在第二个实现中,我们将使用 Keras 对 IMDB 数据集中的评论进行分类。

在我们深入探讨要解决的问题和代码本身之前,请确保已设置好您的环境。与本系列所有之前的文章一样,我将使用 Python 3.6。此外,我使用的是 Anaconda 和 Spyder,但您可以使用任何您偏好的 IDE。然而,重要的是要安装 TensorFlow 和 Keras。

TensorFlow 实现的问题

假设我们想训练一个 LSTM,通过示例文本来预测下一个单词。我们示例中的简单文本将是我非常喜欢的马可·奥勒留《沉思录》中的一段:

从某种意义上说,人是我们真正的关注对象。我们的职责是善待他们,容忍他们。但当他们阻碍我们正常的任务时,他们就变得与我们无关了——就像太阳、风、动物一样。我们的行动可能会被他们阻碍,但我们的意图或决心却无法被阻碍。因为我们可以适应和调整。心灵会适应并将其应对行动的障碍转化为自身的用途。阻碍行动的因素促进了行动。挡在路上的东西就是道路。

请注意,这段文本被稍微修改过。每个标点符号都被空格包围,因此每个单词和每个标点符号都被视为一个符号。为了演示 LSTM 网络的工作原理,我们将使用简化的过程。我们将输入三个符号的序列作为输入,输出一个符号。通过重复这个过程,网络将学会根据前面的三个单词来预测下一个单词。

TensorFlow 实现

所以,我们的计划是:将三个符号的序列和对应的输出输入 LSTM 网络,并让它学会预测该输出。然而,由于我们使用的是数学模型,我们需要做的第一件事是准备这些数据(文本),以便进行任何类型的操作。这些网络理解数字,而不是我们文本中的strings,所以我们需要将这些符号转换为数字。我们将通过根据出现频率为每个符号分配唯一的整数来实现这一点。

为此,我们将使用DataHandler类。此类有两个目的:从文件中加载数据,以及为每个符号分配一个数字。代码如下:

import numpy as np
import collections

class DataHandler:
    def read_data(self, fname):
        with open(fname) as f:
            content = f.readlines()
        content = [x.strip() for x in content]
        content = [content[i].split() for i in range(len(content))]
        content = np.array(content)
        content = np.reshape(content, [-1, ])
        return content
    
    def build_datasets(self, words):
        count = collections.Counter(words).most_common()
        dictionary = dict()
        for word, _ in count:
            dictionary[word] = len(dictionary)
        reverse_dictionary = dict(zip(dictionary.values(), dictionary.keys()))
        return dictionary, reverse_dictionary
此类中的第一个方法read_data用于从指定文件中读取文本并创建符号数组。调用示例文本上的这个方法后,它看起来是这样的:

第二个方法build_datasets用于创建两个字典。第一个字典标记为dictionary,包含符号作为键,其对应的数字作为值。第二个字典reverse_dictionary包含相同的信息,只是键是数字,值是符号本身。使用我们在此示例中使用的示例文本创建它们时,它们将是这样的:

太棒了!我们的数据已准备就绪。现在开始有趣的部分,创建模型。为此,我们将创建一个新类,该类能够基于传入的参数生成 LSTM 网络。看看代码:

import tensorflow as tf
from tensorflow.contrib import rnn

class RNNGenerator:
    def create_LSTM(self, inputs, weights, biases, seq_size, num_units):
        # Reshape input to [1, sequence_size] and split it into sequences
        inputs = tf.reshape(inputs, [-1, seq_size])
        inputs = tf.split(inputs, seq_size, 1)
    
        # LSTM with 2 layers
        rnn_model = rnn.MultiRNNCell([rnn.BasicLSTMCell(num_units),rnn.BasicLSTMCell(num_units)])
    
        # Generate prediction
        outputs, states = rnn.static_rnn(rnn_model, inputs, dtype=tf.float32)
    
        return tf.matmul(outputs[-1], weights['out']) + biases['out']
我们在这里导入了一些重要的类:TensorFlow本身以及tensorflow.contrib中的rnn类。由于我们的 LSTM 网络是 RNN 的子类型,我们将使用它来创建我们的模型。首先,我们重塑了输入,然后将其拆分为三个符号的序列。然后我们创建了模型本身。

我们使用BasicLSTMCell方法创建了两个 LSTM 层。这些层中的每一层都有由参数num_units定义的单元数。除此之外,我们使用MultiRNNCell将这两层组合成一个网络。然后我们使用static_rnn方法来构建网络并生成预测。

最后,我们将使用SessionRunner类。此类包含我们的模型将被运行和评估的环境。代码看起来是这样的:

import tensorflow as tf
import random
import numpy as np

class SessionRunner():
    training_iters = 50000
            
    def __init__(self, optimizer, accuracy, cost, lstm, initilizer, writer):
        self.optimizer = optimizer
        self.accuracy = accuracy
        self.cost = cost
        self.lstm = lstm
        self.initilizer = initilizer
        self.writer = writer
    
    def run_session(self, x, y, n_input, dictionary, reverse_dictionary, training_data):
        
        with tf.Session() as session:
            session.run(self.initilizer)
            step = 0
            offset = random.randint(0, n_input + 1)
            acc_total = 0
        
            self.writer.add_graph(session.graph)
        
            while step < self.training_iters:
                if offset > (len(training_data) - n_input - 1):
                    offset = random.randint(0, n_input+1)
        
                sym_in_keys = [ [dictionary[ str(training_data[i])]] 
                                 for i in range(offset, offset+n_input) ]
                sym_in_keys = np.reshape(np.array(sym_in_keys), [-1, n_input, 1])
        
                sym_out_onehot = np.zeros([len(dictionary)], dtype=float)
                sym_out_onehot[dictionary[str(training_data[offset+n_input])]] = 1.0
                sym_out_onehot = np.reshape(sym_out_onehot,[1,-1])
        
                _, acc, loss, onehot_pred = session.run([self.optimizer, self.accuracy, 
                   self.cost, self.lstm], feed_dict={x: sym_in_keys, y: sym_out_onehot})
                acc_total += acc
                
                if (step + 1) % 1000 == 0:
                    print("Iteration = " + str(step + 1) + ", Average Accuracy= " + 
                                           "{:.2f}%".format(100*acc_total/1000))
                    acc_total = 0
                step += 1
                offset += (n_input+1)

我们的模型正在运行 50000 次迭代。我们将模型、优化器、损失函数等注入构造函数,因此该类拥有这些信息。当然,我们需要做的第一件事是将提供的数据切片,并制作编码的输出(分别为sym_in_keyssym_out_onehot)。此外,我们正在向模型注入随机序列以避免过拟合。这由offset变量处理。最后,我们将运行会话并获得准确率。不要被代码中的最终 if 语句混淆。它仅用于显示目的(每 1000 次迭代显示平均准确率)。

我们的主脚本将所有这些结合在一起,代码如下:

import tensorflow as tf
from DataHandler import DataHandler
from RNN_generator import RNNGenerator
from session_runner import SessionRunner

log_path = '/output/tensorflow/'
writer = tf.summary.FileWriter(log_path)

# Load and prepare data
data_handler = DataHandler()

training_data =  data_handler.read_data('meditations.txt')

dictionary, reverse_dictionary = data_handler.build_datasets(training_data)

# TensorFlow Graph input
n_input = 3
n_units = 512

x = tf.placeholder("float", [None, n_input, 1])
y = tf.placeholder("float", [None, len(dictionary)])

# RNN output weights and biases
weights = {
    'out': tf.Variable(tf.random_normal([n_units, len(dictionary)]))
}
biases = {
    'out': tf.Variable(tf.random_normal([len(dictionary)]))
}

rnn_generator = RNNGenerator()
lstm = rnn_generator.create_LSTM(x, weights, biases, n_input, n_units)

# Loss and optimizer
cost = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits=lstm, labels=y))
optimizer = tf.train.RMSPropOptimizer(learning_rate=0.001).minimize(cost)

# Model evaluation
correct_pred = tf.equal(tf.argmax(lstm,1), tf.argmax(y,1))
accuracy = tf.reduce_mean(tf.cast(correct_pred, tf.float32))

# Initializing the variables
initilizer = tf.global_variables_initializer()

session_runner = SessionRunner(optimizer, accuracy, cost, lstm, initilizer, writer)
session_runner.run_session(x, y, n_input, dictionary, reverse_dictionary, training_data)
一旦我们运行此代码,我们将获得 97.10% 的准确率。

Keras 实现的问题

这个TensorFlow示例相当直接且简单。我们使用了少量数据,网络能够很快地学习。如果我们有一个更复杂的问题呢?例如,假设我们要对某个网站上每部电影评论的情感进行分类。幸运的是,有一个专门为此问题设计的数据集——大型电影评论数据集(通常称为 IMDB 数据集)。

这个数据集由斯坦福大学的研究人员于 2011 年收集。它包含 25000 条用于训练的电影评论(好评或差评),以及相同数量的评论用于测试。我们的目标是创建一个网络,能够确定这些评论中哪些是积极的,哪些是消极的。我们将使用 Keras API,它内置了这个数据集

Keras 实现

Keras 的强大之处在于它抽象了我们在使用TensorFlow时需要处理的许多事情。但是,它也提供了较少的灵活性。当然,一切都是权衡。所以,让我们开始这个实现,导入必要的类和库。

from keras.preprocessing import sequence 
from keras.models import Sequential 
from keras.layers import Dense, Dropout, Embedding, LSTM 
from keras.datasets import imdb 
正如您所看到的,与我们实现标准 ANN 或卷积神经网络的示例相比,导入的库略有不同。我们导入了SequentialDenseDropout。但我们仍然可以看到一些新的导入。我们使用了Embedding以及keras.layers中的LSTM。正如您所能想象的,LSTM用于在网络中创建 LSTM 层。另一方面,Embedding用于提供单词的密集表示。

这是一个很棒的技术,可以将每条电影评论映射到一个实数向量域。单词被编码为高维空间中的实值向量,其中单词在含义上的相似性转化为向量空间中的接近度。我们可以注意到,我们导入了keras.datasets中提供的imdb数据集。但是,要从该数据集加载数据,我们需要这样做:

num_words = 1000 
(X_train, y_train), (X_test, y_test) = imdb.load_data(num_words=num_words) 
我们正在加载前 1000 个单词的数据集。在此之后,我们需要划分数据集并创建和填充序列。这是通过使用keras.preprocessing中的sequence来完成的,如下所示:
X_train = sequence.pad_sequences(X_train, maxlen=200) 
X_test = sequence.pad_sequences(X_test, maxlen=200)

在填充时,我们使用了数字200,这意味着我们的序列长度将为200个单词。训练输入数据在执行此操作后看起来是这样的:

现在我们可以定义、编译和拟合 LSTM 模型了:

# Define network architecture and compile 
model = Sequential() 
model.add(Embedding(num_words, 50, input_length=200)) 
model.add(Dropout(0.2)) 
model.add(LSTM(100, dropout=0.2, recurrent_dropout=0.2)) 
model.add(Dense(250, activation='relu')) 
model.add(Dropout(0.2)) 
model.add(Dense(1, activation='sigmoid'))
model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy']) 

正如我们在之前的文章中学到的,Sequential用于模型组合。添加到其中的第一个层是Embedding,我们在上一章中解释了它的用途。在单词嵌入完成后,我们添加了一个 LSTM 层。最后,由于这是一个分类问题,我们试图弄清楚评论是好是坏,因此添加了一个带有 sigmoid 函数的Dense层。最后,模型被编译,并使用了binary_crossentorpyAdam优化器。我们的模型看起来是这样的:

让我们将数据拟合到模型并获得准确率:

.gist table { margin-bottom: 0; }
 

我们获得了 85.12% 的准确率。

结论

我们看到了创建 LSTM 网络的两种方法。这两种方法都处理了简单的问题,并且每种方法都使用了不同的 API。这样,人们就可以看出TensorFlow更详细、更灵活,然而,与使用 Keras 相比,您需要处理更多事情。Keras 更简单、更直接,但它不像纯TensorFlow那样提供灵活性和可能性。这两种示例都提供了不错的结果,但它们可以做得更好。特别是第二个示例,我们通常会结合使用 CNN 和 RNN 来获得更高的准确率,但这将是另一篇文章的主题。

感谢阅读!

© . All rights reserved.