如何基于单个类构建共享权重的孪生卷积网络(Siamese Net)
嘿,我来帮你拆解这个孪生网络权重共享的问题,咱们一步步来:
你现在的代码核心问题是一开始就强制开启了变量复用:每个variable_scope都加了reuse=True,但第一次创建net1时,layer1/conv2d/kernel这类变量还完全没被定义出来呢!
TensorFlow里reuse=True的语义是“我要复用已经存在的变量”,你第一次调用模型时根本没有可复用的变量,自然就抛出了找不到变量的ValueError。而且你用的tf.layers.conv2d这类高层API,内部默认用tf.get_variable创建变量,这种“先复用再创建”的逻辑完全矛盾,肯定跑不通。
有两种简单靠谱的方式,适配你的代码场景:
方式一:用统一作用域+tf.AUTO_REUSE自动判断
把孪生网络的特征提取逻辑封装成一个函数,用一个全局的变量作用域包裹,并且设置reuse=tf.AUTO_REUSE——它会自动帮你判断:变量不存在就创建,存在就复用,完美解决第一次创建、第二次复用的问题。
重构后的代码示例:
def build_siamese_backbone(inputs): with tf.variable_scope('siamese_backbone', reuse=tf.AUTO_REUSE): # 所有卷积、全连接层都放在这个统一作用域下 layer1 = tf.layers.conv2d(inputs=inputs, filters=8, kernel_size=[1,1], padding='same', activation=tf.nn.relu) batch_layer1 = tf.layers.batch_normalization(inputs=layer1, axis=-1) dropout_layer1 = tf.layers.dropout(inputs=batch_layer1, rate=0.2) layer2 = tf.layers.conv2d(inputs=dropout_layer1, filters=8, kernel_size=[4,4], padding='same', activation=tf.nn.relu) batch_layer2 = tf.layers.batch_normalization(inputs=layer2, axis=-1) dropout_layer2 = tf.layers.dropout(inputs=batch_layer2, rate=0.2) layer3 = tf.layers.conv2d(inputs=dropout_layer2, filters=16, kernel_size=[4,4], padding='same', activation=tf.nn.relu) batch_layer3 = tf.layers.batch_normalization(inputs=layer3, axis=-1) dropout_layer3 = tf.layers.dropout(inputs=batch_layer3, rate=0.2) flatten_layer3 = tf.layers.flatten(dropout_layer3) dense_layer4 = tf.layers.dense(inputs=flatten_layer3, units=1000, activation=tf.nn.relu) logits = tf.layers.dense(inputs=dense_layer4, units=500) return logits
使用时直接两次调用这个函数即可:
with tf.Session() as sess: net1 = build_siamese_backbone(x1) # 第一次调用:创建所有变量 net2 = build_siamese_backbone(x2) # 第二次调用:复用已创建的变量 loss = Loss(2) optimiser = tf.train.AdamOptimizer(learning_rate=0.01).minimize(loss.contrastive_loss(1, net1, net2)) # 别忘了初始化变量!你之前的代码漏掉了这一步,也会报错 sess.run(tf.global_variables_initializer()) for i in range(4): l = sess.run(loss.contrastive_loss(1, net1, net2)) print(l)
方式二:复用同一个网络实例
如果你更倾向于用类的写法,可以让类的model方法接收输入参数,而不是在初始化时绑定输入。这样只需要创建一个SiameseNet实例,两次调用它的model方法分别传入不同输入,配合变量作用域就能实现共享:
class SiameseNet(): def __init__(self): # 初始化时不绑定输入 pass def model(self, inputs): with tf.variable_scope('siamese_backbone', reuse=tf.AUTO_REUSE): # 这里的层定义和上面的函数版本完全一致 layer1 = tf.layers.conv2d(inputs=inputs, filters=8, kernel_size=[1,1], padding='same', activation=tf.nn.relu) batch_layer1 = tf.layers.batch_normalization(inputs=layer1, axis=-1) dropout_layer1 = tf.layers.dropout(inputs=batch_layer1, rate=0.2) layer2 = tf.layers.conv2d(inputs=dropout_layer1, filters=8, kernel_size=[4,4], padding='same', activation=tf.nn.relu) batch_layer2 = tf.layers.batch_normalization(inputs=layer2, axis=-1) dropout_layer2 = tf.layers.dropout(inputs=batch_layer2, rate=0.2) layer3 = tf.layers.conv2d(inputs=dropout_layer2, filters=16, kernel_size=[4,4], padding='same', activation=tf.nn.relu) batch_layer3 = tf.layers.batch_normalization(inputs=layer3, axis=-1) dropout_layer3 = tf.layers.dropout(inputs=batch_layer3, rate=0.2) flatten_layer3 = tf.layers.flatten(dropout_layer3) dense_layer4 = tf.layers.dense(inputs=flatten_layer3, units=1000, activation=tf.nn.relu) logits = tf.layers.dense(inputs=dense_layer4, units=500) return logits
使用示例:
with tf.Session() as sess: siamese_net = SiameseNet() net1 = siamese_net.model(x1) net2 = siamese_net.model(x2) # 后续损失、优化器和训练逻辑和之前一致 loss = Loss(2) optimiser = tf.train.AdamOptimizer(learning_rate=0.01).minimize(loss.contrastive_loss(1, net1, net2)) sess.run(tf.global_variables_initializer()) for i in range(4): l = sess.run(loss.contrastive_loss(1, net1, net2)) print(l)
如果完全不用变量作用域,创建两个SiameseNet实例时,TensorFlow会给每个实例的卷积核、偏置等变量自动分配不同的名称(比如第一个实例的卷积核叫conv2d/kernel,第二个叫conv2d_1/kernel),这意味着:
- 两个网络的权重完全独立,根本达不到孪生网络“共享特征提取能力”的核心要求;
- 模型参数数量直接翻倍,训练需要的计算资源、数据量也会翻倍,效率极低;
- 孪生网络的核心逻辑(让两个网络学习相同的特征表示,从而有效对比输入相似度)完全失效,训练出来的模型根本无法完成相似性匹配任务。
内容的提问来源于stack exchange,提问作者dodo

