使用Keras在滑动窗口中评估一个函数

16
我正在尝试将一个匹配算法扩展到整个序列上。我的匹配项长度为20个单位,并且每个时间点有4个通道。我已经建立了一个模型来封装匹配过程,但我无法想出如何在滑动窗口中使用它,以便在更长的序列中找到匹配项。
我有两个(20,4)的输入张量(query和target),我将它们连接,相加,展平,然后应用一个简单的密集层。在这个阶段,我有100,000个查询目标对的数据用于训练。
def sum_seqs(seqs):
    return K.sum(seqs, axis=3)

def pad_dims(seq):
    return K.expand_dims(seq, axis=3)

def pad_outshape(in_shape):
    return (in_shape[0], in_shape[1], in_shape[2], 1)


query = Input((20, 4))
query_pad = Lambda(pad_dims, output_shape=pad_outshape, name='gpad')(query)

target = Input((20,4))
target_pad = Lambda(pad_dims, output_shape=pad_outshape)(target)

matching = Concatenate(axis = 3)([query_pad, target_pad])
matching = Lambda(sum_seqs)(matching)

matching = Flatten()(matching)
matching = Dropout(0.1)(matching)
matching = Dense(1, activation = 'sigmoid')(matching)

match_model = Model([query, target], matching)

这个功能完美无误。现在我想使用这个预训练模型来搜索一个更长的目标序列和不同的查询序列。

看起来应该是这样:

long_target = Input((100, 4))

short_target = Input((20, 4))
choose_query = Input((20, 4))

spec_match = match_model([choose_query, short_target])

mdl = TimeDistributed(spec_match)(long_target)

但是 TimeDistributed 需要一个 Layer 而不是一个 Tensor 。我是否缺少包装器?我这样做的方式是否有误?我需要以某种方式重新构造为卷积问题吗?

继续尝试:

经过一天的思考,很明显TimeDistributedbackend.rnn都只允许您将模型/层应用于单个时间切片的数据。似乎没有办法做到这一点。看起来唯一可以“跨越”时间维度的多个片段的是Conv1D

所以,我把我的问题重新构想成了一个卷积问题,但效果也不好。我能构建一个Conv1D滤波器,使其匹配特定的query。这运行得相当不错,它确实允许我扫描更长的序列并获得匹配。但是,每个过滤器都是唯一的,对于每个query张量都需要独立训练一个新的Conv1D层以获得相应的过滤器权重。由于我的目标是查找与大多数目标匹配的新query,这没有什么帮助。

由于我的“匹配”需要目标和查询在每个窗口的交互,因此似乎我无法通过Conv1D在100长度的target张量上的每个窗口上获取20长度query张量的交互。

是否有任何方法在Keras / tensorflow中进行这种滑动窗口类型的评估?它看起来很简单,但是距离很远。是否有我找不到的方法可以做到这一点?

响应和进一步尝试。

@today和@nuric的解决方案有效,但它们最终会以平铺方式复制输入 target 数据。因此,对于长度为m的查询,在图中将有略少于m个副本的输入数据。我希望找到一种实际上可以在target上“滑动”评估而不重复的解决方案。

下面是我想出的Conv1D几乎解决方案的版本。

query_weights = []

for query, (targets, scores) in query_target_gen():
    single_query_model = Sequential()
    single_query_model.add(Conv1D(1, 20, input_shape = (20, 4)))
    single_query_model.add(Flatten())

    single_query_model.fit(targets, scores)

    query_weights.append(single_query_model.layers[0].get_weights())

multi_query_model_long_targets = Sequential()
multi_query_model_long_targets.add(Conv1D(len(query_weights), 20, input_shape = (100, 4)))

multi_query_model_long_targets.layers[0].set_weights(combine_weights(query_weights))

multi_query_model_long_targets.summary()

combine_weights函数只是对过滤器进行了一些解包和矩阵重新排列,以按照Conv1D的要求堆叠过滤器。

这个解决方案解决了数据复制的问题,但它在其他方面给我带来了麻烦。其中一个问题是基于数据的......我的数据包含许多querytarget对,但往往是相同的target与许多query配对,因为按照这种方式生成真实世界的数据更容易。因此,这样做使训练变得困难。其次,这假定每个query都是独立工作的,而实际上,我知道querytarget匹配才是真正重要的。因此,使用可以查看多个示例对的模型而不是个体对有意义。

有没有办法将两种方法结合起来?有没有办法使Conv1D同时获取长的target张量并将其与恒定的query组合在一起,当它沿着序列移动时?


为了确保我理解您的问题:假设您有一个长度为100的目标,您想要找出target[0:20]target[1:21]target[2,22],...,target[-20:]是否与长度为20的query匹配,并使用您训练好的模型?也许每个目标的长度可能是k,其中k不一定是100? - today
@today。是的,这是正确的...虽然它将是target[0:20, :]target[1:21, :],...因为匹配需要在评估中使用所有4个通道。我通常假设每个批次的k都是相同的。最终,我将为每个目标取最大匹配分数到下一层。因此,不同的目标长度不会影响下游层。 - JudoWill
你尝试过使用 tf.extract_image_patches() 吗?这基本上就是你要找的东西。如果你无法使用它,请告诉我。 - today
@今天可能需要一些调整,虽然看起来似乎需要一些调整。tf.extract_image_patches()需要一个4D张量 [batch, in_rows, in_cols, depth],而我的是2D的。而且不清楚张量是如何输出的(我AFK,所以无法测试)。如果您能写一个带有一些基本代码的答案,我今晚很乐意测试并授予奖励。 - JudoWill
现在我考虑到你的一般问题(即查找任意长度的目标序列与固定长度查询的匹配分数),我认为它可以重新表述,以便您不需要显式滑动窗口操作。但是,我无法理解您在使用 Conv1D 层时提到的问题?您能否也把您卷积模型的代码贴出来吗? - today
显示剩余4条评论
2个回答

13

以下是使用Keras后端函数提供的另一种解决方案。

您还可以使用K.arangeK.map_fn生成滑动窗口:

def sliding_windows(inputs):
    target, query = inputs
    target_length = K.shape(target)[1]  # variable-length sequence, shape is a TF tensor
    query_length = K.int_shape(query)[1]
    num_windows = target_length - query_length + 1  # number of windows is also variable

    # slice the target into consecutive windows
    start_indices = K.arange(num_windows)
    windows = K.map_fn(lambda t: target[:, t:(t + query_length), :],
                       start_indices,
                       dtype=K.floatx())

    # `windows` is a tensor of shape (num_windows, batch_size, query_length, ...)
    # so we need to change the batch axis back to axis 0
    windows = K.permute_dimensions(windows, (1, 0, 2, 3))

    # repeat query for `num_windows` times so that it could be merged with `windows` later
    query = K.expand_dims(query, 1)
    query = K.tile(query, [1, num_windows, 1, 1])

    # just a hack to force the dimensions 2 to be known (required by Flatten layer)
    windows = K.reshape(windows, shape=K.shape(query))
    return [windows, query]

如何使用它:

long_target = Input((None, 4))
choose_query = Input((20, 4))
windows, query = Lambda(sliding_windows)([long_target, choose_query])

根据您的预训练match_model,使用TimeDistributed的问题在于它无法包装具有多个输入的Keras Model

然而,由于逻辑匹配targetquery是在Concatenate之后的层中实现的,因此您可以将这些层收集到一个Model中,并对其应用TimeDistributed

submodel_input = Input((20, 4, 2))
x = submodel_input
for layer in match_model.layers[-4:]:  # the `Lambda(sum_seqs)` layer
    x = layer(x)
submodel = Model(submodel_input, x)

现在您只需要像在“match_model”中那样处理和合并“sliding_windows”的输出即可。
long_target = Input((None, 4))
choose_query = Input((20, 4))
windows, query = Lambda(sliding_windows)([long_target, choose_query])

windows_pad = Lambda(lambda x: K.expand_dims(x))(windows)
query_pad = Lambda(lambda x: K.expand_dims(x))(query)
merged = Concatenate()([windows_pad, query_pad])

match_scores = TimeDistributed(submodel)(merged)
max_score = GlobalMaxPooling1D()(match_scores)
model = Model([long_target, choose_query], max_score)
< p > model可以被用于端到端的方式来匹配长目标。

您还可以通过将match_model应用于滑动窗口来验证model的输出确实是匹配分数的最大值:

target_arr = np.random.rand(32, 100, 4)
query_arr = np.random.rand(32, 20, 4)

match_model_scores = np.array([
    match_model.predict([target_arr[:, t:t + 20, :], query_arr])
    for t in range(81)
])
scores = model.predict([target_arr, query_arr])

print(np.allclose(scores, match_model_scores.max(axis=0)))
True

太好了!这是一个纯粹的tensorflow/Keras解决方案!@JudoWill如果你问我的意见,你应该接受这个答案并授予它赏金,因为它比我的更好、更完整(尽管,正如你在这个解决方案中看到的,正如我之前提到的,没有绕过数据复制的方法;相信我,它做的事情比伤害多得多!) - today

10

注意:请看@Yu-Yang的解决方案,它更好。


嗯,正如我在评论中提到的,你可以使用tf.extract_image_patches()(如果文档似乎有点含糊,请阅读SO上的此答案)来提取补丁(编辑:我刚刚添加了两个变量win_lenfeat_len,并将100更改为None,将81更改为-1以使其适用于任意长度的目标序列):

import tensorflow as tf
from keras import layers, models
import keras.backend as K

win_len = 20   # window length
feat_len = 4   # features length

def extract_patches(data):
    data = K.expand_dims(data, axis=3)
    patches = tf.extract_image_patches(data, ksizes=[1, win_len, feat_len, 1], strides=[1, 1, 1, 1], rates=[1, 1, 1, 1], padding='VALID')
    return patches

target = layers.Input((None, feat_len))
patches = layers.Lambda(extract_patches)(target)
patches = layers.Reshape((-1, win_len, feat_len))(patches)

model = models.Model([target], [patches])
model.summary()
Layer (type)                 Output Shape              Param #   
=================================================================
input_2 (InputLayer)         (None, None, 4)           0         
_________________________________________________________________
lambda_2 (Lambda)            (None, None, None, 80)    0         
_________________________________________________________________
reshape_2 (Reshape)          (None, None, 20, 4)       0         
=================================================================
Total params: 0
Trainable params: 0
Non-trainable params: 0
_________________________________________________________________
例如,如果输入目标具有形状(100, 4),则输出的形状为(81, 20, 4)
这是一个测试:
import numpy as np

# an array consisting of numbers 0 to 399 with shape (100, 4)
target = np.arange(1*100*4*1).reshape(1, 100, 4)
print(model.predict(a))

以下是输出结果:

[[[[  0.   1.   2.   3.]
   [  4.   5.   6.   7.]
   [  8.   9.  10.  11.]
   ...
   [ 68.  69.  70.  71.]
   [ 72.  73.  74.  75.]
   [ 76.  77.  78.  79.]]

  [[  4.   5.   6.   7.]
   [  8.   9.  10.  11.]
   [ 12.  13.  14.  15.]
   ...
   [ 72.  73.  74.  75.]
   [ 76.  77.  78.  79.]
   [ 80.  81.  82.  83.]]

  [[  8.   9.  10.  11.]
   [ 12.  13.  14.  15.]
   [ 16.  17.  18.  19.]
   ...
   [ 76.  77.  78.  79.]
   [ 80.  81.  82.  83.]
   [ 84.  85.  86.  87.]]

  ...

  [[312. 313. 314. 315.]
   [316. 317. 318. 319.]
   [320. 321. 322. 323.]
   ...
   [380. 381. 382. 383.]
   [384. 385. 386. 387.]
   [388. 389. 390. 391.]]

  [[316. 317. 318. 319.]
   [320. 321. 322. 323.]
   [324. 325. 326. 327.]
   ...
   [384. 385. 386. 387.]
   [388. 389. 390. 391.]
   [392. 393. 394. 395.]]

  [[320. 321. 322. 323.]
   [324. 325. 326. 327.]
   [328. 329. 330. 331.]
   ...
   [388. 389. 390. 391.]
   [392. 393. 394. 395.]
   [396. 397. 398. 399.]]]]

根据这些形状,这正是我要找的。我今晚会试一下看看它是否有效! - JudoWill
@JudoWill "我希望找到一个能够在不重复的情况下真正“滑动”评估目标的解决方案。" 我有点困惑,除了您的卷积解决方案可能不适合之外,您正在一个列表中累加 Conv 层的权重,如果目标和查询对的数量很高,这将耗费大量内存。而且,您说:“我希望找到一个能够在不重复的情况下真正“滑动”评估目标的解决方案。”:请注意,这意味着会损害模型的性能。 >>>>>>> - today
1
数据复制的好处在于它可以利用并行性。甚至大多数主要深度学习库中的卷积操作都是通过提取数据中的所有补丁,然后同时应用内核于所有补丁(例如在GPU中)来实现的。(实际上,所有补丁都存储在矩阵中)。 - today
我同意,我发布的Conv1D解决方案很糟糕,但那是我能想到的唯一方法,我认为可能有一种方法可以修改它以适应我的目的。我不知道Conv1D的底层实现是平铺的,我以为它是某种渐进式滚动方法。我想我只能接受重复,并修改其他东西来解决它。感谢所有的帮助! - JudoWill
如果你调整得当,它会非常准确(尽管这取决于你的数据、使用的架构、超参数等),而且速度也非常快。如果你有兴趣讨论这种方法,请告诉我。 - today
显示剩余5条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接