在Keras/Tensorflow自定义损失函数中使用额外的*可训练*变量

6
我知道如何在Keras中编写具有其他输入的自定义损失函数,而不是标准的y_truey_pred对。我的问题是使用一个可训练变量(其中一些是损失梯度的一部分并应进行更新)来输入损失函数。
我的解决方法是:
  • 输入一个大小为NXV的虚拟输入,其中N是观测值数量,V是附加变量数量
  • 添加 Dense()dummy_output,以便Keras会跟踪我的V“权重”
  • 在我的真实输出层中使用这个层的V权重作为自定义损失函数
  • 对于这个dummy_output层使用虚拟损失函数(仅返回0.0和/或权重为0.0),以便我的V“权重”只通过我的自定义损失函数进行更新
我的问题是:有没有更自然的Keras / TF方式来做到这一点?因为这种方法感觉很牵强,还容易出错。
以下是我的解决方法的示例代码:(是的,我知道这是一个非常愚蠢的自定义损失函数,在现实中情况要复杂得多)。
import numpy as np
from sklearn.model_selection import train_test_split
import matplotlib.pyplot as plt
from tensorflow.keras.layers import Dense
from tensorflow.keras.callbacks import EarlyStopping
import tensorflow.keras.backend as K
from tensorflow.keras.layers import Input
from tensorflow.keras import Model

n_col = 10
n_row = 1000
X = np.random.normal(size=(n_row, n_col))
beta = np.arange(10)
y = X @ beta

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

# my custom loss function accepting my dummy layer with 2 variables
def custom_loss_builder(dummy_layer):
    def custom_loss(y_true, y_pred):
        var1 = dummy_layer.trainable_weights[0][0]
        var2 = dummy_layer.trainable_weights[0][1]
        return var1 * K.mean(K.square(y_true-y_pred)) + var2 ** 2 # so var2 should get to zero, var1 should get to minus infinity?
    return custom_loss

# my dummy loss function
def dummy_loss(y_true, y_pred):
    return 0.0

# my dummy input, N X V, where V is 2 for 2 vars
dummy_x_train = np.random.normal(size=(X_train.shape[0], 2)) 

# model
inputs = Input(shape=(X_train.shape[1],))
dummy_input = Input(shape=(dummy_x_train.shape[1],))
hidden1 = Dense(10)(inputs) # here only 1 hidden layer in the "real" network, assume whatever network is built here
output = Dense(1)(hidden1)
dummy_output = Dense(1, use_bias=False)(dummy_input)
model = Model(inputs=[inputs, dummy_input], outputs=[output, dummy_output])

# compilation, notice zero loss for the dummy_output layer
model.compile(
  loss=[custom_loss_builder(model.layers[-1]), dummy_loss],
  loss_weights=[1.0, 0.0], optimizer= 'adam')

# run, notice y_train repeating for dummy_output layer, it will not be used, could have created dummy_y_train as well
history = model.fit([X_train, dummy_x_train], [y_train, y_train],
                    batch_size=32, epochs=100, validation_split=0.1, verbose=0,
                   callbacks=[EarlyStopping(monitor='val_loss', patience=5)])

看起来无论 var1var2(初始化 dummy_output 层)的起始值是什么,它似乎都能渐进地达到负无穷和 0:

(此图来自迭代运行该模型并保存这两个权重的结果,如下所示)

var1_list = []
var2_list = []
for i in range(100):
    if i % 10 == 0:
        print('step %d' % i)
    model.fit([X_train, dummy_x_train], [y_train, y_train],
              batch_size=32, epochs=1, validation_split=0.1, verbose=0)
    var1, var2 = model.layers[-1].get_weights()[0]
    var1_list.append(var1.item())
    var2_list.append(var2.item())

plt.plot(var1_list, label='var1')
plt.plot(var2_list, 'r', label='var2')
plt.legend()
plt.show()

enter image description here


“part of the loss gradient”到底是什么意思?由于损失梯度的净效应实质上是通过模型进行反向传播并改变可训练模型权重,这是否意味着您在自定义损失函数中的‘var1’和‘var2’实际上可以来自层权重(也许还包括偏置)? - Bill Huang
变量是参与创建观察数据的未知参数。因此,它们是复杂损失函数的一部分,模型试图找到X和y之间的联系,给定这些也需要估计的未知参数。因此,它们是损失函数的一部分,也是梯度的一部分,就像我愚蠢的例子中所看到的那样。真正的模型更有趣,也更有意义。 - Giora Simchoni
不错。无论如何,只要在训练后立即更新损失函数参数,那么keras回调仍然是最佳选择。如何计算损失函数参数超出了本问题的范围。只要您知道如何更新闭包变量,就可以像我的答案中演示的那样在keras回调中使其正常工作。 - Bill Huang
1个回答

5

我自问自答,经过多日的努力,我在不需要使用虚拟输入的情况下使它正常工作了,我认为这种方法更好,并且应该是“规范”的方式,直到Keras/TF简化这个过程。这就是Keras/TF文档中所做的这里

使用带有外部可训练变量的损失函数的关键是通过使用自定义损失/输出进行处理,该层在其call()实现中具有self.add_loss(...),像这样:

class MyLoss(Layer):
    def __init__(self, var1, var2):
        super(MyLoss, self).__init__()
        self.var1 = K.variable(var1) # or tf.Variable(var1) etc.
        self.var2 = K.variable(var2)
    
    def get_vars(self):
        return self.var1, self.var2
    
    def custom_loss(self, y_true, y_pred):
        return self.var1 * K.mean(K.square(y_true-y_pred)) + self.var2 ** 2
    
    def call(self, y_true, y_pred):
        self.add_loss(self.custom_loss(y_true, y_pred))
        return y_pred

现在请注意,MyLoss层需要两个输入:实际的y_true和截至该点的预测值y

inputs = Input(shape=(X_train.shape[1],))
y_input = Input(shape=(1,))
hidden1 = Dense(10)(inputs)
output = Dense(1)(hidden1)
my_loss = MyLoss(0.5, 0.5)(y_input, output) # here can also initialize those var1, var2
model = Model(inputs=[inputs, y_input], outputs=my_loss)

model.compile(optimizer= 'adam')

最后,正如TF文档所述,在这种情况下,您不必在fit()函数中指定lossy
history = model.fit([X_train, y_train], None,
                    batch_size=32, epochs=100, validation_split=0.1, verbose=0,
                    callbacks=[EarlyStopping(monitor='val_loss', patience=5)])

再次注意,y_train 作为输入之一传递到 fit() 中。

现在它可以正常工作:

var1_list = []
var2_list = []
for i in range(100):
    if i % 10 == 0:
        print('step %d' % i)
    model.fit([X_train, y_train], None,
              batch_size=32, epochs=1, validation_split=0.1, verbose=0)
    var1, var2 = model.layers[-1].get_vars()
    var1_list.append(var1.numpy())
    var2_list.append(var2.numpy())

plt.plot(var1_list, label='var1')
plt.plot(var2_list, 'r', label='var2')
plt.legend()
plt.show()

这里输入图片描述

(需要提醒的是,var1var2 的特定模式高度取决于它们的初始值。如果 var1 的初始值大于 1,则它实际上不会减少,直到减少至负无穷)


这看起来确实是规范的!如果我理解正确,从Layer继承将允许自动梯度反向传播,而这对于Callback是不可用的。因此,我删除了我的帖子,以免误导他人。 - Bill Huang
1
我其实很喜欢你的帖子。 - Giora Simchoni
非常高兴!我认为我们都从伟大的Keras基础设施中学到了更多。Callbacks用于前向和后向传播操作,因此不适合您当前的需求。我认为删除它是正确的,因为毕竟这是一个问答网站,而不是讨论论坛 :) - Bill Huang

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