有状态 LSTM 和流预测

Stateful LSTM and stream predictions

我已经在多个批次的 7 个样本上训练了一个 LSTM 模型(用 Keras 和 TF 构建),每个样本有 3 个特征,形状如下图所示(下面的数字只是为了解释目的的占位符),每个批次都标记为 0 或 1:

数据:

[
   [[1,2,3],[1,2,3],[1,2,3],[1,2,3],[1,2,3],[1,2,3],[1,2,3]]
   [[1,2,3],[1,2,3],[1,2,3],[1,2,3],[1,2,3],[1,2,3],[1,2,3]]
   [[1,2,3],[1,2,3],[1,2,3],[1,2,3],[1,2,3],[1,2,3],[1,2,3]]
   ...
]

即:m 个序列的批次,每个序列的长度为 7,其元素是 3 维向量(因此批次的形状为 (m73))

目标:

[
   [1]
   [0]
   [1]
   ...
]

我的生产环境数据是具有 3 个特征的样本流 ([1,2,3],[1,2,3]...)。我想在每个样本到达我的模型时对其进行流式传输,并在不等待整个批次 (7) 的情况下获得中间概率 - 请参见下面的动画。

我的一个想法是用 0 填充缺失样本的批次, [[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[1,2,3]] 但这似乎效率低下。

如果能为我指明正确的方向,即以持久的方式保存 LSTM 中间状态,同时等待下一个样本并预测使用部分数据在特定批量大小上训练的模型,我将不胜感激。


更新,包括型号代码:

    opt = optimizers.Adam(lr=0.001, beta_1=0.9, beta_2=0.999, epsilon=10e-8, decay=0.001)
    model = Sequential()

    num_features = data.shape[2]
    num_samples = data.shape[1]

    first_lstm = LSTM(32, batch_input_shape=(None, num_samples, num_features), 
                      return_sequences=True, activation='tanh')
    model.add(first_lstm)
    model.add(LeakyReLU())
    model.add(Dropout(0.2))
    model.add(LSTM(16, return_sequences=True, activation='tanh'))
    model.add(Dropout(0.2))
    model.add(LeakyReLU())
    model.add(Flatten())
    model.add(Dense(1, activation='sigmoid'))

    model.compile(loss='binary_crossentropy', optimizer=opt,
                  metrics=['accuracy', keras_metrics.precision(), 
                           keras_metrics.recall(), f1])

模型总结:

_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
lstm_1 (LSTM)                (None, 100, 32)           6272      
_________________________________________________________________
leaky_re_lu_1 (LeakyReLU)    (None, 100, 32)           0         
_________________________________________________________________
dropout_1 (Dropout)          (None, 100, 32)           0         
_________________________________________________________________
lstm_2 (LSTM)                (None, 100, 16)           3136      
_________________________________________________________________
dropout_2 (Dropout)          (None, 100, 16)           0         
_________________________________________________________________
leaky_re_lu_2 (LeakyReLU)    (None, 100, 16)           0         
_________________________________________________________________
flatten_1 (Flatten)          (None, 1600)              0         
_________________________________________________________________
dense_1 (Dense)              (None, 1)                 1601      
=================================================================
Total params: 11,009
Trainable params: 11,009
Non-trainable params: 0
_________________________________________________________________

如果我没理解错的话,你有 m 个序列的批次,每个序列的长度为 7,其元素是 3 维向量(因此批次的形状为 (m*7*3))。 在任何 Keras RNN 中,您可以设置 return_sequences 标记为 True 成为中间状态,即对于每个批次,您将获得相应的 7 个输出,而不是最终预测,其中输出 i 表示阶段预测i 给定从 0 到 i 的所有输入。

但是最后你会一下子搞定。据我所知,Keras 不提供在处理批处理时检索吞吐量的直接接口。如果您使用任何 CUDNN 优化的变体,这可能会受到更多限制。您基本上可以做的是 将您的批次视为形状 (m*1*3) 的 7 个连续批次,并将它们逐步提供给您的 LSTM,记录每一步的隐藏状态和预测。为此,您可以将 return_state 设置为 True 并手动执行,或者您可以简单地将 stateful 设置为 True 并让对象跟踪它。


下面的 Python2+Keras 示例应该完全代表您想要的。具体来说:

  • 允许以持久的方式保存整个 LSTM 中间状态
  • 等待下一个样本时
  • 并预测在特定批量大小上训练的模型,该批量大小可能是任意且未知的。

为此,它包含一个示例 stateful=True 用于最简单的训练,以及 return_state=True 用于最精确的推理,因此您可以体验这两种方法。它还假定您获得了一个已经序列化并且您不太了解的模型。结构和Andrew Ng课程中的那个关系密切,在topic上绝对比我权威。由于您没有指定模型的训练方式,我假设采用多对一训练设置,但这很容易适应。

from __future__ import print_function
from keras.layers import Input, LSTM, Dense
from keras.models import Model, load_model
from keras.optimizers import Adam
import numpy as np

# globals
SEQ_LEN = 7
HID_DIMS = 32
OUTPUT_DIMS = 3 # outputs are assumed to be scalars


##############################################################################
# define the model to be trained on a fixed batch size:
# assume many-to-one training setup (otherwise set return_sequences=True)
TRAIN_BATCH_SIZE = 20

x_in = Input(batch_shape=[TRAIN_BATCH_SIZE, SEQ_LEN, 3])
lstm = LSTM(HID_DIMS, activation="tanh", return_sequences=False, stateful=True)
dense = Dense(OUTPUT_DIMS, activation='linear')
m_train = Model(inputs=x_in, outputs=dense(lstm(x_in)))
m_train.summary()

# a dummy batch of training data of shape (TRAIN_BATCH_SIZE, SEQ_LEN, 3), with targets of shape (TRAIN_BATCH_SIZE, 3):
batch123 = np.repeat([[1, 2, 3]], SEQ_LEN, axis=0).reshape(1, SEQ_LEN, 3).repeat(TRAIN_BATCH_SIZE, axis=0)
targets = np.repeat([[123,234,345]], TRAIN_BATCH_SIZE, axis=0) # dummy [[1,2,3],,,]-> [123,234,345] mapping to be learned


# train the model on a fixed batch size and save it
print(">> INFERECE BEFORE TRAINING MODEL:", m_train.predict(batch123, batch_size=TRAIN_BATCH_SIZE, verbose=0))
m_train.compile(optimizer=Adam(lr=0.5), loss='mean_squared_error', metrics=['mae'])
m_train.fit(batch123, targets, epochs=100, batch_size=TRAIN_BATCH_SIZE)
m_train.save("trained_lstm.h5")
print(">> INFERECE AFTER TRAINING MODEL:", m_train.predict(batch123, batch_size=TRAIN_BATCH_SIZE, verbose=0))


##############################################################################
# Now, although we aren't training anymore, we want to do step-wise predictions
# that do alter the inner state of the model, and keep track of that.


m_trained = load_model("trained_lstm.h5")
print(">> INFERECE AFTER RELOADING TRAINED MODEL:", m_trained.predict(batch123, batch_size=TRAIN_BATCH_SIZE, verbose=0))

# now define an analogous model that allows a flexible batch size for inference:
x_in = Input(shape=[SEQ_LEN, 3])
h_in = Input(shape=[HID_DIMS])
c_in = Input(shape=[HID_DIMS])
pred_lstm = LSTM(HID_DIMS, activation="tanh", return_sequences=False, return_state=True, name="lstm_infer")
h, cc, c = pred_lstm(x_in, initial_state=[h_in, c_in])
prediction = Dense(OUTPUT_DIMS, activation='linear', name="dense_infer")(h)
m_inference = Model(inputs=[x_in, h_in, c_in], outputs=[prediction, h,cc,c])

#  Let's confirm that this model is able to load the trained parameters:
# first, check that the performance from scratch is not good:
print(">> INFERENCE BEFORE SWAPPING MODEL:")
predictions, hs, zs, cs = m_inference.predict([batch123,
                                               np.zeros((TRAIN_BATCH_SIZE, HID_DIMS)),
                                               np.zeros((TRAIN_BATCH_SIZE, HID_DIMS))],
                                              batch_size=1)
print(predictions)


# import state from the trained model state and check that it works:
print(">> INFERENCE AFTER SWAPPING MODEL:")
for layer in m_trained.layers:
    if "lstm" in layer.name:
        m_inference.get_layer("lstm_infer").set_weights(layer.get_weights())
    elif "dense" in layer.name:
        m_inference.get_layer("dense_infer").set_weights(layer.get_weights())

predictions, _, _, _ = m_inference.predict([batch123,
                                            np.zeros((TRAIN_BATCH_SIZE, HID_DIMS)),
                                            np.zeros((TRAIN_BATCH_SIZE, HID_DIMS))],
                                           batch_size=1)
print(predictions)


# finally perform granular predictions while keeping the recurrent activations. Starting the sequence with zeros is a common practice, but depending on how you trained, you might have an <END_OF_SEQUENCE> character that you might want to propagate instead:
h, c = np.zeros((TRAIN_BATCH_SIZE, HID_DIMS)), np.zeros((TRAIN_BATCH_SIZE, HID_DIMS))
for i in range(len(batch123)):
    # about output shape: https://keras.io/layers/recurrent/#rnn
    # h,z,c hold the network's throughput: h is the proper LSTM output, c is the accumulator and cc is (probably) the candidate
    current_input = batch123[i:i+1] # the length of this feed is arbitrary, doesn't have to be 1
    pred, h, cc, c = m_inference.predict([current_input, h, c])
    print("input:", current_input)
    print("output:", pred)
    print(h.shape, cc.shape, c.shape)
    raw_input("do something with your prediction and hidden state and press any key to continue")

附加信息:

因为我们有两种形式的状态持久化:
1.模型的saved/trained个参数对于每个sequence都是一样的
2. ac 状态在整个序列中进化,可能是 "restarted"

看一下 LSTM 对象的内部结构很有趣。在我提供的 Python 示例中, ac 权重被显式处理,但训练参数没有被处理,并且它们的内部实现方式或内容可能并不明显他们的意思是。可以按如下方式检查它们:

for w in lstm.weights:
    print(w.name, w.shape)

在我们的例子中(32 个隐藏状态)returns 如下:

lstm_1/kernel:0 (3, 128)
lstm_1/recurrent_kernel:0 (32, 128)
lstm_1/bias:0 (128,)

我们观察到维度为 128。这是为什么? this link对Keras LSTM的实现描述如下:

The g is the recurrent activation, p is the activation, Ws are the kernels, Us are the recurrent kernels, h is the hidden variable which is the output too and the notation * is an element-wise multiplication.

这解释了 128=32*4 是发生在 4 个门中每个门内的仿射变换的参数,连接起来:

  • 形状为 (3, 128) 的矩阵(名为 kernel)处理给定序列元素的输入
  • 形状为 (32, 128) 的矩阵(名为 recurrent_kernel)处理最后一个循环状态的输入 h
  • 形状为 (128,) 的向量(名为 bias),与任何其他 NN 设置一样。

注意:此答案假设您的训练阶段模型不是有状态的。您必须了解什么是有状态 RNN 层,并确保训练数据具有相应的有状态属性。简而言之,这意味着序列之间存在依赖关系,即一个序列是另一个序列的后续,您要在模型中考虑这一点。如果您的模型和训练数据是有状态的,那么我认为其他涉及从一开始就为 RNN 层设置 stateful=True 的答案会更简单。

更新:无论训练模型是否有状态,您始终可以将其权重复制到推理模型并启用有状态。所以我认为基于设置 stateful=True 的解决方案比我的更短更好。它们唯一的缺点是这些解决方案中的批量大小必须固定。


请注意,LSTM 层在单个序列上的输出由其固定的权重矩阵及其内部状态决定,该内部状态取决于 先前处理的时间步长 。现在要获得长度为 m 的单个序列的 LSTM 层的输出,一种明显的方法是将整个序列一次输入到 LSTM 层。然而,正如我之前所说,由于它的内部状态取决于之前的时间步长,我们可以利用这一事实并通过在处理块结束时获取 LSTM 层的状态并将其传递给 LSTM 来逐块地提供单个序列块处理下一个块的层。为了更清楚,假设序列长度为 7(即它有 7 个固定长度特征向量的时间步长)。例如,可以像这样处理这个序列:

  1. 将时间步长 1 和 2 馈送到 LSTM 层;获得最终状态(称之为 C1)。
  2. 将时间步长3、4、5和状态C1作为初始状态馈送到LSTM层;获得最终状态(称之为 C2)。
  3. 将时间步长 6 和 7 以及状态 C2 作为初始状态馈送到 LSTM 层;得到最终的输出。

最后的输出相当于 LSTM 层产生的输出,如果我们一次输入整个 7 个时间步。

所以要在Keras中实现这一点,可以将LSTM层的return_state参数设置为True,这样就可以得到中间状态。此外,在定义输入层时不要指定固定的时间步长。相反,使用 None 能够为模型提供任意长度的序列,这使我们能够逐步处理每个序列(如果您在训练时的输入数据是固定长度的序列就没问题)。

由于您在推理时需要这种卡盘处理能力,我们需要定义一个新模型,该模型共享训练模型中使用的 LSTM 层,并且可以将初始状态作为输入,并将结果状态作为输出。下面是它可以做的一个大致的草图(注意LSTM层的返回状态在训练模型时不使用,我们只在测试时需要它):

# define training model
train_input = Input(shape=(None, n_feats))   # note that the number of timesteps is None
lstm_layer = LSTM(n_units, return_state=True)
lstm_output, _, _ =  lstm_layer(train_input) # note that we ignore the returned states
classifier = Dense(1, activation='sigmoid')
train_output = classifier(lstm_output)

train_model = Model(train_input, train_output)

# compile and fit the model on training data ...

# ==================================================

# define inference model
inf_input = Input(shape=(None, n_feats))
state_h_input = Input(shape=(n_units,))
state_c_input = Input(shape=(n_units,))

# we use the layers of previous model
lstm_output, state_h, state_c = lstm_layer(inf_input,
                                           initial_state=[state_h_input, state_c_input])
output = classifier(lstm_output)

inf_model = Model([inf_input, state_h_input, state_c_input],
                  [output, state_h, state_c])  # note that we return the states as output

现在您可以向 inf_model 提供序列的时间步长。但是,请注意,最初您必须为状态提供全零向量(这是状态的默认初始值)。例如,如果序列长度为 7,则当新数据流可用时会发生什么情况的示意图如下:

state_h = np.zeros((1, n_units,))
state_c = np.zeros((1, n_units))

# three new timesteps are available
outputs = inf_model.predict([timesteps, state_h, state_c])

out = output[0,0]  # you may ignore this output since the entire sequence has not been processed yet
state_h = outputs[0,1]
state_c = outputs[0,2]

# after some time another four new timesteps are available
outputs = inf_model.predict([timesteps, state_h, state_c])

# we have processed 7 timesteps, so the output is valid
out = output[0,0]  # store it, pass it to another thread or do whatever you want to do with it

# reinitialize the state to make them ready for the next sequence chunk
state_h = np.zeros((1, n_units))
state_c = np.zeros((1, n_units))

# to be continued...

当然,您需要在某种循环中执行此操作或实施控制流结构来处理数据流,但我认为您已经了解了总体思路。

最后,虽然你的具体例子不是sequence-to-sequence模型,但是我强烈推荐阅读official Keras seq2seq tutorial,我认为可以从中学到很多想法。

我认为可能有更简单的解决方案。

如果您的模型没有卷积层或作用于 length/steps 维度的任何其他层,您可以简单地将其标记为 stateful=True

警告:您的模型具有作用于长度尺寸的层!!

Flatten层将长度维度转换为特征维度。这将完全阻止您实现目标。如果 Flatten 层需要 7 个步骤,那么您将始终需要 7 个步骤。

因此,在应用下面的答案之前,请修复您的模型以不使用 Flatten 层。相反,它可以只删除 last LSTM 层的 return_sequences=True

以下代码解决了这个问题,还准备了一些要与以下答案一起使用的东西:

def createModel(forTraining):

    #model for training, stateful=False, any batch size   
    if forTraining == True:
        batchSize = None
        stateful = False

    #model for predicting, stateful=True, fixed batch size
    else:
        batchSize = 1
        stateful = True

    model = Sequential()

    first_lstm = LSTM(32, 
        batch_input_shape=(batchSize, num_samples, num_features), 
        return_sequences=True, activation='tanh', 
        stateful=stateful)   

    model.add(first_lstm)
    model.add(LeakyReLU())
    model.add(Dropout(0.2))

    #this is the last LSTM layer, use return_sequences=False
    model.add(LSTM(16, return_sequences=False, stateful=stateful,  activation='tanh'))

    model.add(Dropout(0.2))
    model.add(LeakyReLU())

    #don't add a Flatten!!!
    #model.add(Flatten())

    model.add(Dense(1, activation='sigmoid'))

    if forTraining == True:
        compileThisModel(model)

有了这个,你将能够用 7 步训练,一步预测。否则将不可能。

使用有状态模型解决您的问题

首先,再次训练这个新模型,因为它没有 Flatten 层:

trainingModel = createModel(forTraining=True)
trainThisModel(trainingModel)

现在,使用这个经过训练的模型,您可以简单地创建一个 新模型,与创建经过训练的模型完全相同,但在其所有 LSTM 中标记 stateful=True层。我们应该从训练模型中复制权重。

由于这些新层需要固定的批量大小(Keras 的规则),我假设它是 1(一个单一的流,而不是 m 个流)并将其添加到上面的模型创建中。

predictingModel = createModel(forTraining=False)
predictingModel.set_weights(trainingModel.get_weights())

瞧瞧。只需一步即可预测模型的输出:

pseudo for loop as samples arrive to your model:
    prob = predictingModel.predict_on_batch(sample)

    #where sample.shape == (1, 1, 3)

当您决定到达您认为的连续序列的末尾时,调用 predictingModel.reset_states() 这样您就可以安全地开始一个新序列,而模型不会认为它应该在前一个序列的末尾进行修复.


保存和加载状态

只需获取并设置它们,使用 h5py 保存:

def saveStates(model, saveName):

    f = h5py.File(saveName,'w')

    for l, lay in enumerate(model.layers):
        #if you have nested models, 
            #consider making this recurrent testing for layers in layers
        if isinstance(lay,RNN):
            for s, stat in enumerate(lay.states):
                f.create_dataset('states_' + str(l) + '_' + str(s),
                                 data=K.eval(stat), 
                                 dtype=K.dtype(stat))

    f.close()


def loadStates(model, saveName):

    f = h5py.File(saveName, 'r')
    allStates = list(f.keys())

    for stateKey in allStates:
        name, layer, state = stateKey.split('_')
        layer = int(layer)
        state = int(state)

        K.set_value(model.layers[layer].states[state], f.get(stateKey))

    f.close()

saving/loading 个州的工作测试

import h5py, numpy as np
from keras.layers import RNN, LSTM, Dense, Input
from keras.models import Model
import keras.backend as K




def createModel():
    inp = Input(batch_shape=(1,None,3))
    out = LSTM(5,return_sequences=True, stateful=True)(inp)
    out = LSTM(2, stateful=True)(out)
    out = Dense(1)(out)
    model = Model(inp,out)
    return model


def saveStates(model, saveName):

    f = h5py.File(saveName,'w')

    for l, lay in enumerate(model.layers):
        #if you have nested models, consider making this recurrent testing for layers in layers
        if isinstance(lay,RNN):
            for s, stat in enumerate(lay.states):
                f.create_dataset('states_' + str(l) + '_' + str(s), data=K.eval(stat), dtype=K.dtype(stat))

    f.close()


def loadStates(model, saveName):

    f = h5py.File(saveName, 'r')
    allStates = list(f.keys())

    for stateKey in allStates:
        name, layer, state = stateKey.split('_')
        layer = int(layer)
        state = int(state)

        K.set_value(model.layers[layer].states[state], f.get(stateKey))

    f.close()

def printStates(model):

    for l in model.layers:
        #if you have nested models, consider making this recurrent testing for layers in layers
        if isinstance(l,RNN):
            for s in l.states:
                print(K.eval(s))   

model1 = createModel()
model2 = createModel()
model1.predict_on_batch(np.ones((1,5,3))) #changes model 1 states

print('model1')
printStates(model1)
print('model2')
printStates(model2)

saveStates(model1,'testStates5')
loadStates(model2,'testStates5')

print('model1')
printStates(model1)
print('model2')
printStates(model2)

数据方面的思考

在你的第一个模型中(如果它是 stateful=False),它认为 m 中的每个序列都是独立的并且不与其他序列相连。它还认为每个批次都包含独特的序列。

如果不是这种情况,您可能想要训练有状态模型(考虑到每个序列实际上都连接到前一个序列)。然后你需要 m 批 1 个序列。 -> m x (1, 7 or None, 3).

据我所知,由于 Tensorflow 中的静态图,没有有效的方法来提供与训练输入长度不同的输入。

填充是解决该问题的官方方法,但效率较低且占用内存。我建议你研究一下 Pytorch,这对于解决你的问题来说是微不足道的。

用Pytorch构建lstm的great posts有很多,看了就会明白动态图的好处。