如何在Keras中强制回归模型输出单调递增/递减?

4

我目前正在解决一个问题,我提供一个神经网络的输入变量a,以及另一个输入x,它是一个单调递增的序列,包含N个数字。

所以我的神经网络基本上看起来像这样:

a_input = Input(shape=[1], name='a')
x_input = Input(shape=[N], name='x')
nn = concatenate([a_input, x_input])
nn = Dense(100, activation='relu')(nn)
nn = Dense(N, activation='relu')(nn)
model = Model(inputs=[a_input, x_input], outputs=[nn])
model.compile(loss='mean_squared_error', optimizer="adam")

我在输入空间上进行回归(其中对于每个 a,序列 x 是唯一的),我希望网络能够为每组输入 ax 输出一个单调递增的(非负)N 个数字的序列。
现在,我注意到到目前为止我的输出不严格单调递增,但如果你“放大”它们,它们看起来有点像。我的意思是,对于给定的 ax 的选择,如果我希望我的输出数组像这样:
[0, 0.5, 0.51, 0.7, 0.75, 0.9, 1.], 

我可能会选择获取:

[0.001, 0.5, 0.48, 0.7, 0.75, 0.9, 1.].

因此,我想知道是否有标准的方法或特定的工具可用于Keras,以将模型限制为仅输出单调递增的序列?
1个回答

4
为了保证输出为非负值,请在输出层使用ReLU或sigmoid等非负激活函数。 我不知道是否有神经方法来实现输出的单调性,但我认为一个明智的方法是将输出表示形式更改为使网络预测两个连续元素之间的差异。例如,您可以将输出数组:
a=[0, 0.5, 0.51, 0.7, 0.75, 0.9, 1.]
转换为:
b=[0, 0.5, 0.01, 0.19, 0.05, 0.15, 0.1]
其中b[0] = a[0], b[i] = a[i]-a[i-1](i>0)。 在这种情况下,最好使用递归层作为输出层,因为每个输出单元现在都依赖于前面的单元。 您的原始表示可以轻松恢复为a[0] = b[0]和a[i]=b[i]+a[i-1] (i>0),因为每个输出b[i]都是非负的,所以结果序列将单调递增。
更新1:LSTM应该返回完整序列。您可以尝试按以下方式构建模型:
a_input = Input(shape=[1], name='a')
x_input = Input(shape=[N], name='x')
nn = concatenate([a_input, x_input])
nn = Dense(100, activation='relu')(nn)
nn = Dense(N, activation='relu')(nn)
nn = Lambda(lambda x: x[..., None])(nn)  # Output shape=(batch_size, nb_timesteps=N, input_dim=1)
nn = LSTM(1, return_sequences=True, activation='relu')(nn)  # Output shape=(batch_size, nb_timesteps=N, output_dim=1)
nn = Lambda(lambda x: keras.backend.squeeze(x, axis=-1))(nn)  # Output shape=(batch_size, N)
model = Model(inputs=[a_input, x_input], outputs=[nn])
model.compile(loss='mean_squared_error', optimizer="adam")

更新2。只有一个隐藏单元的LSTM可能不够强大。我不确定这是否有帮助,但您可以尝试在最后一个 LSTM 之前添加另一个具有更多单元(即10个)的 LSTM 层:

...
nn = Lambda(lambda x: x[..., None])(nn)  # Output shape=(batch_size, nb_timesteps=N, input_dim=1)
nn = LSTM(10, return_sequences=True)(nn)  # Output shape=(batch_size, nb_timesteps=N, output_dim=10)
nn = LSTM(1, return_sequences=True, activation='relu')(nn)  # Output shape=(batch_size, nb_timesteps=N, output_dim=1)
nn = Lambda(lambda x: keras.backend.squeeze(x, axis=-1))(nn)  # Output shape=(batch_size, N)
...

我喜欢你的想法,认为它对我有意义,但要注意我很少使用RNN。据我所知,您建议对于每个训练样本,x_train = {a, [x0, x1,..., xN]},我将我的y_train转换为y_train' = {y_train[0],y_train[1]-y_train[0],...}?在构建模型时,是否只需将原始帖子中的最后一个Dense(N,activation ='relu')层更改为LSTM(N,activation ='relu / sigmoid')即可?因此,例如,在keras中使用train_on_batch时,我的输入和输出形状也将与我最初的尝试相同? - diddleridoo
没错,这正是我建议的!我更新了我的答案,展示了如何使用LSTM来保留输入和输出形状。 - rvinas
非常感谢这个极为方便的解决方案。我现在正在尝试使用它,并且注意到单调性已被严格执行! nn输出不再准确地适配所需的曲线,但我认为我已经找到了原因——我的y_train并不总是严格地从一个元素(这里是从左到右)增加到另一个元素,而是有时看起来像这样:[0, 0.5, 0.5, 0.51, 0.7, 0.7, 0.7,0.75, 0.9, 1.]。您认为增加回溯窗口(现在仅为一个元素)会有所帮助吗?还是在这种情况下可以使用任何其他基于LSTM的技巧? - diddleridoo
不客气!我不确定为什么输出结果不再准确地符合曲线(我猜测您正在撤销输出表示的转换)。只是为了明确,LSTM的第一个参数并不表示回顾的窗口,而是隐藏状态的维度。我已经更新了我的答案,并提出了建议。 - rvinas
是的,它肯定会撤销转换。目前来看,链式LSTM层对于这个问题并不起作用,事实上,如果我增加所需序列中的元素数量,输出也会开始变成nan(无论是否使用多个LSTM层)。然而,正如我上面提到的,我的数据中有许多情况下序列并不严格单调递增(即y[i]可能等于y[i+1]),这也可能是一个问题。 - diddleridoo
我不明白为什么会出现这个问题,因为网络应该可以轻松地在y[i] == y[i+1]时输出0,因为输出激活函数是ReLU。我不确定发生了什么事情。很抱歉我无法提供更多帮助。 - rvinas

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