使用 Keras 评估滑动窗口中的函数

Posted

技术标签:

【中文标题】使用 Keras 评估滑动窗口中的函数【英文标题】:Evaluate a function in a sliding window with Keras 【发布时间】:2018-12-01 11:16:24 【问题描述】:

我正在尝试跨序列扩展匹配匹配算法。我的比赛有 20 个单位长,每个时间点有 4 个频道。我已经建立了一个封装匹配的模型,我只是不知道如何在滑动窗口中使用它来将它应用到更长的序列中以找到序列中的匹配。

我有 2 个 (20, 4) 输入张量(querytarget),我将它们连接、添加、展平,然后应用一个简单的密集层。在这个阶段我有数据可以用 100K 查询、目标对进行训练。

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)

这非常有效。现在我想使用这个预训练模型来搜索更长的target 序列和不同的query 序列。

看起来应该是这样的:

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 张量都是唯一的,并且似乎没有办法从新颖的 query 到适当的过滤器权重而不训练全新的 Conv1D 层。由于我的目标是找到与大多数目标匹配的新 querys,这并没有多大帮助。

由于我的“匹配”需要目标和每个窗口中的查询的交互,因此似乎没有一种方法可以在每个窗口中获得 20 长度 query 张量的交互,跨越 100-长度target张量到Conv1D

有没有办法在 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 许多 querys,因为在该方向上生成真实世界的数据更容易。因此,这样做会使培训变得困难。其次,这假设每个query 都以独立的方式工作,而实际上,我知道querytarget 配对才是真正重要的。因此,使用一个可以查看许多配对示例而不是个人示例的模型是有意义的。

有没有办法将这两种方法结合起来?有没有办法让 Conv1D 在沿着序列前进时将长 target 张量与常量 query 结合起来?

【问题讨论】:

为了确保我理解你的问题:假设你有一个长度为 100 的目标,你想找出每个 target[0:20]target[1:21]target[2,22]、...、@ 987654365@ 与长度为 20 的 query 匹配,使用您训练的模型?也许每个目标的长度可能是k,其中k不一定是100? @今天。这是正确的......虽然它会是target[0:20, :]target[1:21, :],......因为匹配需要评估中的所有4个通道。我通常假设每个批次的k 都是相同的。最终,我会将每个目标的最大匹配分数带入下一层。所以不同的目标长度不会影响下游层。 您是否尝试过使用tf.extract_image_patches()?它基本上就是你要找的东西。如果您不能使用它,请告诉我。 @today 也许虽然看起来需要一些技巧。 tf.extract_image_patches() 需要一个 4D 张量 [batch, in_rows, in_cols, depth] 我的是 2D。并且不清楚张量是如何产生的(我是 AFK,所以无法测试)。如果您可以使用一些基本代码将其编写为答案,我很乐意今晚对其进行测试并奖励赏金。 好的。我会写一个答案。还有一个问题:如果目标从形状(batch_size, 100, 4) 转换为(batch_size, 81, 20, 4) 是否可以,其中 81 正是滑动窗口(即补丁)的数量?你能处理它还是你想要的形状是(batch_size*81, 20, 4) 【参考方案1】:

只是为了提供使用 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_modelTimeDistributed 的问题在于它无法用多个输入包装 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)

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 如果你问我的意见,你应该接受这个答案并奖励它,因为它比我的更好并且更完整(尽管,正如你在这个解决方案中看到的,正如我之前提到的,没有办法绕过数据复制;相信我,它利大于弊!)【参考方案2】:

注意:看@Yu-Yang 的解决方案。好多了。


好吧,正如我在评论中提到的,您可以使用 tf.exctract_image_patches()(如果文档看起来有点模糊,请阅读 SO 上的 this answer)来提取补丁(编辑:我刚刚添加了两个变量win_lenfeat_len 并将100 更改为None81 更改为-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 我希望它能工作......但在第二次阅读你的问题后,我怀疑你可以轻松使用上述模型的输出而无需任何修改/后处理;因为据我了解,您正在寻找一个 Keras/tensorflow 解决方案,该解决方案可以打包为 一个单一的 Keras 模型,这样对于给定的目标序列和给定的查询,发现使用您的预训练模型(即match_model),目标的每个子序列与给定查询的匹配分数。无论如何,测试这个解决方案,如果它不起作用,请随时让我知道讨论替代方案。 和@nuric。您的两个答案都适用于我想要做的事情,但需要复制target 数据m 次以获取m 的查询长度。 target[20,:] 处的值被复制 20 倍(从第一个窗口到第 20 个窗口)。我一直在寻找能够真正与窗口一起“行走”而无需复制数据的东西。我正在添加我的 hackish Conv1D 解决方案,也许这会激发关于如何通过数据复制来做到这一点的想法。 @Judo我会看到。那么,您希望解决方案完全在 Keras/tensorflow 中实现,正如我所理解的那样?或者如果它使用 numpy 或 python 方法可以吗?显然,一种解决方案是一种 python 方法,它将目标和查询作为输入并在循环中滑过目标并在其上应用您的预训练模型?这样它就不会复制数据。顺便问一下,由于您关心数据复制,是否有任何内存限制? >>>>> 数据复制的好处在于它可以利用并行性。甚至大多数主要深度学习库中的卷积操作都是通过提取数据中的所有补丁然后将内核同时应用于所有补丁来实现的(例如在 GPU 中)。 (Actually all the patches are stored in a matrix)

以上是关于使用 Keras 评估滑动窗口中的函数的主要内容,如果未能解决你的问题,请参考以下文章

滑动窗口

SQL滑动窗口聚合(不使用窗口函数)

R中的滑动窗口

在 KDB/Q 中按时滑动窗口

pandas计算滑动窗口中的最小值实战(Rolling Minimum in a Pandas Column):计算单数据列滑动窗口中的最小值计算多数据列滑动窗口中的最小值

滑动窗口算法思路