使用 Keras 在滑动 window 中评估函数

Evaluate a function in a sliding window with Keras

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

我有 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)

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

好像应该是这样的:

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 都只允许您将 model/layer 应用于数据的单个时间片。似乎没有办法做到这一点。看起来唯一可以 "walk" 跨越时间维度的多个切片的是 Conv1D.

因此,我将我的问题重新定义为卷积,但效果也不佳。我能够构建一个 Conv1D 过滤器来匹配特定的 query。这工作得相当好,它确实允许我扫描更长的序列并获得匹配项。但是每个过滤器对于每个 query 张量都是唯一的,并且似乎没有办法在不训练全新的 Conv1D 层的情况下从新颖的 query 到适当的过滤器权重。因为我的目标是找到匹配最多目标的新 querys,所以这没什么帮助。

因为我的 "matching" 需要目标和查询在每个 window 处的交互,所以似乎没有办法获得 20 长度的交互 query 张量在每个 window 跨越 100 长度 target 张量到 Conv1D

有什么方法可以在 Keras/tensorflow 中执行这种滑动 window 类型的评估吗?这似乎是一件如此简单却又如此遥远的事情。有没有我找不到的方法可以做到这一点?

回应和进一步的实验。

来自@today 和@nuric 的解决方案有效,但它们最终以平铺类型方式复制输入 target 数据。因此,对于长度为 m 的查询,图表中的输入数据副本将少于 m 个。我希望找到一个解决方案,实际上 "slide" 整个 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 相结合?

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


好吧,正如我在评论中提到的,您可以在 SO 上使用 tf.exctract_image_patches() (if the documentation seems a bit vague read 来提取补丁(编辑:我刚刚添加了两个变量 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.]]]]

只是为了提供一个使用 Keras 后端函数的替代解决方案。

您还可以使用 K.arangeK.map_fn:

生成滑动 windows
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 应用于滑动 windows:

来验证 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