Keras 中共享 LSTM 层中的状态持久性



我正在尝试在 Keras 模型中使用具有状态的共享 LSTM 层,但似乎每次并行使用都会修改内部状态。这就提出了两个问题:

  1. 当使用共享 LSTM 层训练模型并使用stateful=True时,并行使用是否也在训练期间更新相同的状态?
  2. 如果我的观察是有效的,有没有办法使用权重共享 LSTM,以便为每个并行用途独立存储状态?

下面的代码举例说明了共享 LSTM 的三个序列的问题。将完整输入的预测与将预测输入分成两半并连续馈送到网络中的结果进行比较。

可以观察到的是,a1aFull的前半部分相同,这意味着LSTM的使用在第一次预测时确实与独立状态并行。 即,z1不受并行调用的影响,从而产生z2z3。但a2aFull的后半部分不同,因此并行使用的状态之间存在一些相互作用。

我希望的是a1a2两部分的串联与使用较长输入序列调用预测的结果相同,但事实似乎并非如此。另一个问题是,当这种交互发生在预测中时,它是否也在训练期间发生。

import keras
import keras.backend as K
import numpy as np
nOut = 3
xShape = (3, 50, 4)
inShape = (xShape[0], None, xShape[2])   
batchInShape = (1, ) + inShape
x = np.random.randn(*xShape)
# construct network
xIn = keras.layers.Input(shape=inShape, batch_shape=batchInShape)
# shared LSTM layer
sharedLSTM = keras.layers.LSTM(units=nOut, stateful=True, return_sequences=True, return_state=False)
# split the input on the first axis
x1 = keras.layers.Lambda(lambda x: x[:,0,:,:])(xIn)
x2 = keras.layers.Lambda(lambda x: x[:,1,:,:])(xIn)
x3 = keras.layers.Lambda(lambda x: x[:,2,:,:])(xIn)
# pass each input through the LSTM
z1 = sharedLSTM(x1)
z2 = sharedLSTM(x2)
z3 = sharedLSTM(x3)
# add a singleton dimension
y1 = keras.layers.Lambda(lambda x: K.expand_dims(x, axis=1))(z1)
y2 = keras.layers.Lambda(lambda x: K.expand_dims(x, axis=1))(z2)
y3 = keras.layers.Lambda(lambda x: K.expand_dims(x, axis=1))(z3)
# combine the outputs
y = keras.layers.Concatenate(axis=1)([y1, y2, y3])
model = keras.models.Model(inputs=xIn, outputs=y)
model.compile(loss='mse', optimizer='adam')
model.summary()
# no need to train, since we're interested only what is happening mechanically
# reset to a known state and predict for full input
model.reset_states()
aFull = model.predict(x[np.newaxis,:,:,:])
# reset to a known state and predict for the same input, but in two pieces
model.reset_states()
a1 = model.predict(x[np.newaxis,:,:xShape[1]//2,:])
a2 = model.predict(x[np.newaxis,:,xShape[1]//2:,:])
# combine the pieces
aSplit = np.concatenate((a1, a2), axis=2)
print('full diff: {}, first half diff: {}, second half diff: {}'.format(str(np.sum(np.abs(aFull - aSplit))), str(np.sum(np.abs(aFull[:,:,:xShape[1]//2,:] - aSplit[:,:,:xShape[1]//2,:]))), str(np.sum(np.abs(aFull[:,:,xShape[1]//2:,:] - aSplit[:,:,xShape[1]//2:,:])))))

更新:上述行为是在使用 Tensorflow 1.14 和 1.15 作为后端的 Keras 中观察到的。使用 tf2.0 运行相同的代码(使用调整后的导入(会更改结果,以便a1不再与aFull的前半部分相同。这仍然可以通过在层实例化中设置stateful=False来实现。

这对我来说表明,我尝试使用具有共享参数的递归层的方式,但将自己的状态用于并行使用,实际上不可能像这样。

更新2:似乎之前的其他功能也错过了相同的功能:Keras的github上已关闭,未回答的问题。

为了进行比较,这里是pytorch中的一个涂鸦(我第一次尝试使用它(实现了一个简单的网络,其中N个并行LSTM共享权重,但具有独立的状态。在这种情况下,状态显式存储在列表中,并手动提供给 LSTM 单元。

import torch
import numpy as np
class sharedLSTM(torch.nn.Module):
def __init__(self, batchSz, nBands, nDims, outDim):
super(sharedLSTM, self).__init__()
self.internalLSTM = torch.nn.LSTM(input_size=nDims, hidden_size=outDim, num_layers=1, bias=True, batch_first=True)
allStates = list()
for bandIdx in range(nBands):
h_0 = torch.zeros(1, batchSz, outDim)
c_0 = torch.zeros(1, batchSz, outDim)
allStates.append((h_0, c_0))
self.allStates = allStates            
self.nBands = nBands
def forward(self, x):
allOut = list()
for dimIdx in range(self.nBands):
thisSlice = x[:,dimIdx,:,:] # (batchSz, nSteps, nFeats)
thisState = self.allStates[dimIdx]
thisY, thisState = self.internalLSTM(thisSlice, thisState) 
self.allStates[dimIdx] = thisState
allOut.append(thisY[:,None,:,:]) # => (batchSz, 1, nSteps, nFeats)
y = torch.cat(allOut, dim=1) # => (batchSz, nDims, nSteps, nFeats)
return y
def resetStates(self):
for bandIdx in range(nBands):
self.allStates[bandIdx][0][:] = 0.0
self.allStates[bandIdx][1][:] = 0.0

batchSz = 5
nBands = 3
nFeats = 4
nOutDims = 2
net = sharedLSTM(batchSz, nBands, nFeats, nOutDims)
net = net.float()
print(net)
N = 20
x = torch.from_numpy(np.random.rand(batchSz, nBands, N, nFeats)).float()
x1 = x[:, :, :N//2, :]
x2 = x[:, :, N//2:, :]
aa = net.forward(x)
net.resetStates()
a1 = net.forward(x1)
a2 = net.forward(x2)
print('(with reset) first half abs diff: {}'.format(str(torch.sum(torch.abs(a1 - aa[:,:,:N//2,:])).detach().numpy())))
print('(with reset) second half abs diff: {}'.format(str(torch.sum(torch.abs(a2 - aa[:,:,N//2:,:])).detach().numpy())))

结果:无论我们是一次性还是分段进行预测,输出都是相同的。

我试图使用子类化在 Keras 中复制它,但没有成功:

import keras
import numpy as np
class sharedLSTM(keras.Model):
def __init__(self, batchSz, nBands, nDims, outDim):
super(sharedLSTM, self).__init__()
self.internalLSTM = keras.layers.LSTM(units=outDim, stateful=True, return_sequences=True, return_state=True)
self.internalLSTM.build((batchSz, None, nDims))
self.internalLSTM.reset_states()
allStates = list()
allSlicers = list()
for bandIdx in range(nBands):
allStates.append(None)
allSlicers.append(keras.layers.Lambda(lambda x, b: x[:, :, b, :], arguments = {'b' : bandIdx}))
self.allStates = allStates            
self.allSlicers = allSlicers
self.Concat = keras.layers.Lambda(lambda x: keras.backend.concatenate(x, axis=2))
self.nBands = nBands
def call(self, x):
allOut = list()
for bandIdx in range(self.nBands):
thisSlice = self.allSlicers[bandIdx]( x )
thisState = self.allStates[bandIdx]
thisY, *thisState = self.internalLSTM(thisSlice, initial_state=thisState) 
self.allStates[bandIdx] = thisState.copy()
allOut.append(thisY[:,:,None,:]) 
y = self.Concat( allOut )
return y
batchSz = 1
nBands = 3
nFeats = 4
nOutDims = 2
N = 20
model = sharedLSTM(batchSz, nBands, nFeats, nOutDims)
model.compile(optimizer='SGD', loss='mae')
x = np.random.rand(batchSz, N, nBands, nFeats)
x1 = x[:, :N//2, :, :]
x2 = x[:, N//2:, :, :]
aa = model.predict(x)
model.reset_states()
a1 = model.predict(x1)
a2 = model.predict(x2)
print('(with reset) first half abs diff: {}'.format(str(np.sum(np.abs(a1 - aa[:,:N//2,:,:])))))
print('(with reset) second half abs diff: {}'.format(str(np.sum(np.abs(a2 - aa[:,N//2:,:,:])))))

如果你现在问"你为什么不使用火炬闭嘴?",答案是周围的实验框架已经建立在假设Keras的情况下,改变它将是一个不可忽视的工作量。

根据我目前对 Keras 中 LSTM(和其他 RNN(行为的理解,在stateful=True模式下使用共享 LSTM 层并不像人们预期的那样工作,只有一个状态变量通过所有并行使用进行更新。因此,问题的答案似乎是:

  1. 是的,他们是。处理在多个并行序列之一上运行,将状态存储在末尾,并将其用作第二个并行序列的初始状态,依此类推。
  2. 是的,但这需要一些工作。有关详细信息,请参阅下文。

我设法以两种方式完成了处理状态。首先是从 Keras 的 LSTM 和 LSTMCell 派生子类,并通过拆分输入以及存储和恢复每个并行流的状态来重载 LSTMCell.call(( 来处理并行数据流。这里的缺点是RNN的输入形状固定为3D,这意味着并行输入需要与真实特征一起重塑为特征维度。

第二种方法是创建一个与问题中的共享 LSTM 模型不完全不同的包装层,包含将输入切片到并行流,为每个流调用具有正确状态的内部 LSTM,并存储返回的状态。列表中的状态存储更新通过插入到 call(( 末尾的 add_update(( 调用来工作。这个add_update((不能(似乎(与模型一起使用,因此是层。但是,当使用 Keras <2.3 运行时,不会跟踪或更新嵌套层的权重,因此需要 Keras 2.3+ 或 TF2。

最新更新