单样本学习(One shot learning)和孪生网络(Siamese Network)



  • 孪生网络与伪孪生网络

    Siamese network就是“连体的神经网络”,神经网络的“连体”是通过共享权值来实现的,如下图所示。共享权值意味着两边的网络权重矩阵一模一样,甚至可以是同一个网络。

    如果左右两边不共享权值,而是两个不同的神经网络,叫伪孪生网络(pseudo-siamese network,伪孪生神经网络),对于pseudo-siamese network,两边可以是不同的神经网络(如一个是lstm,一个是cnn),也可以是相同类型的神经网络。

    原理

    衡量两个输入的相似程度,输出是一个[0,1]的浮点数,表示二者的相似程度。孪生神经网络有两个输入(Input1 and Input2),将两个输入feed进入两个神经网络(Network1 and Network2),这两个神经网络分别将输入映射到新的空间,**形成输入在新的空间中的表示。**通过Loss的计算,评价两个输入的相似度。

    孪生神经网络和伪孪生神经网络分别适用的场景

    先上结论:孪生神经网络用于处理两个输入**"比较类似"的情况。伪孪生神经网络适用于处理两个输入"有一定差别"**的情况。比如,我们要计算两个句子或者词汇的语义相似度,使用siamese network比较适合;如果验证标题与正文的描述是否一致(标题和正文长度差别很大),或者文字是否描述了一幅图片(一个是图片,一个是文字),就应该使用pseudo-siamese network。也就是说,要根据具体的应用,判断应该使用哪一种结构,哪一种Loss。

    用途

    • 前面提到的词汇的语义相似度分析,QA中question和answer的匹配,签名/人脸验证。
    • 手写体识别也可以用siamese network,网上已有github代码。
    • 还有kaggle上Quora的question pair的比赛,即判断两个提问是不是同一问题,冠军队伍用的就是n多特征+Siamese network,知乎团队也可以拿这个模型去把玩一下。
    • 在图像上,基于Siamese网络的视觉跟踪算法也已经成为热点《Fully-convolutional siamese networks for object tracking》。
    • 单样本学习

    单样本学习

    定义问题

    我们的模型只获得了很少的标记的训练样本S,它有N个样本,每个相同维度的向量有一个对应的标签y

    <br/>S=(x1,y1),,(xN,yN)<br/><br /> S = {(x_1,y_1), …, (x_N,y_N)}<br />

    再给出一个待分类的测试样例 \hat{x} 。因为样本集中每个样本都有一个正确的类别,我们的目标是正确的预测 y\in S 中哪一个是 \hat{x} 的正确标签 \hat{y}

    一个单样本学习的baseline--1近邻

    最简单的分类方式是使用k近邻方法,但是因为每个类别只有一个样本,所以我们需要用1近邻。这很简单,只需要计算测试样本与训练集中每个样本的的欧式距离,然后选择最近的一个就可以了:

    $$
    C(\hat{x}) = \underset{c \in S}{\operatorname{argmin}} || \hat{x} - x_c ||
    $$

    根据Koch等人的论文,在omniglot数据集中的20类上,单样本分类,1-nn可以得到大约28%的精度,28%看起来很差,但是它已经是随机猜测(5%)的6倍精度了。这是一个单样本学习算法最好的baseline或者“合理性测试”了。

    网络架构

    Koch等人使用卷积孪生网络去分类成对的omniglot图像,所以这两个孪生网络都是卷积神经网络。这两个孪生网络每个的架构如下:64通道的10×10卷积核,relu->max pool->128通道的7×7卷积核,relu->max pool->128通道的4×4卷积核,relu->max pool->256通道的4×4卷积核。孪生网络把输入降低到越来越小的3d张量上,最终它们经过一个4096神经元的全连接层。两个向量的绝对差作为线性分类器的输入。这个网络一共有38,951,745个参数--96%的参数属于全连接层。这个参数量很大,所以网络有很高的过拟合风风险,但是成对的训练意味着数据集是很大的,所以过拟合问题不成出现。

    输出被归一化到[0,1]之间,使用sigmoid函数让它成为一个概率。当两个图像是相同类别的时候,我们使目标_t_=1,类别不相同的时候使_t_=0。它使用逻辑斯特回归来训练。这意味着损失函数应该是预测和目标之间的二分类交叉熵。损失函数中还有一个L2权重衰减项,以让网络可以学习更小的\更平滑的权重,从而提高泛化能力: L(x_1,x_2,t) = t \cdot log(p(x_1 \circ x_2)) + (1 - t) \cdot log(1-p(x_1 \circ x_2)) + \lambda \cdot ||w||_2

    当网络做单样本学习的时候,孪生网络简单的分类一下测试图像与训练集中的图像中哪个最相似就可以了: C(\hat{x},S) = \underset{c}{\operatorname{argmax}} P(\hat{x} \circ x_c), x_c \in S

    这里使用argmax而不是近邻方法中的argmin,因为类别越不同,L2度量的值越高,但是这个模型的输出 p(x_1 \circ x_2) ,所以我们要这个值最大。这个方法有一个明显的缺陷:对于训练集中的 x_{a},概率 x_1 \circ x_2 与训练集中每个样本都是独立的!这意味着概率值的和不为1。言归正传,测试图像与训练图像应该是相同类型的。。。

    观察一下:逐对训练的有效的数据集大小

    经过与以为UoA大学的博士讨论后发现,我认为这个是过分夸大的,或者就是错的。凭经验来说,我的实现没有过拟合,即使它没有在每个可能的成对图像上充分训练,这与该节是冲突的。在有错就说思想的指引下,我会保留这个问题。

    我注意到,采用逐对训练的话,将会有平方级别对的图像对来训练模型,这让模型很难过拟合,好酷。假设我们有_E_类,每类有_C_个样本。一共有 C \cdot E 张图片,总共可能的配方数量可以这样计算:

    N_{pairs} = {\binom {C \cdot E}{2}} = {\frac {(C \cdot E) !}{2! (C \cdot E-2) !}} 对于omniglot中的964类(每类20个样本),这会有185,849,560个可能的配对,这是巨大的!然而,孪生网络需要相同类的和不同类的配对都有。每类_E_个训练样本,所以每个类别有 {\binom {E}{2}} 对,这意味着这里有 N_{same} = {\binom {E}{2}} \cdot C 个相同类别的配对。--对于Omniglot有183,160对。及时183,160对已经很大了,但他只是所有可能配对的千分之一,因为相同类别的配对数量随着E平方级的增大,但是随着C是线性增加。这个问题非常重要,因为孪生网络训练的时候,同类别和不同类别的比例应该是1:1.--或许它表明逐对训练在那种每个类别有更多样本的数据集上更容易训练。

    代码

    如果你更喜欢用jupyter notebook?这里是传送门

    下面是模型定义,如果你见过keras,那很容易理解。我只用Sequential()来定义一次孪生网络,然后使用两个输入层来调用它,这样两个输入使用相同的参数。然后我们把它们使用绝对距离合并起来,添加一个输出层,使用二分类交叉熵损失来编译这个模型。

    from keras.layers import Input, Conv2D, Lambda, merge, Dense, Flatten,MaxPooling2D
    from keras.models import Model, Sequential
    from keras.regularizers import l2
    from keras import backend as K
    from keras.optimizers import SGD,Adam
    from keras.losses import binary_crossentropy
    import numpy.random as rng
    import numpy as np
    import os
    import dill as pickle
    import matplotlib.pyplot as plt
    from sklearn.utils import shuffle
    
    def W_init(shape,name=None):
        """Initialize weights as in paper"""
        values = rng.normal(loc=0,scale=1e-2,size=shape)
        return K.variable(values,name=name)
    #//TODO: figure out how to initialize layer biases in keras.
    def b_init(shape,name=None):
        """Initialize bias as in paper"""
        values=rng.normal(loc=0.5,scale=1e-2,size=shape)
        return K.variable(values,name=name)
    
    input_shape = (105, 105, 1)
    left_input = Input(input_shape)
    right_input = Input(input_shape)
    #build convnet to use in each siamese 'leg'
    convnet = Sequential()
    convnet.add(Conv2D(64,(10,10),activation='relu',input_shape=input_shape,
                       kernel_initializer=W_init,kernel_regularizer=l2(2e-4)))
    convnet.add(MaxPooling2D())
    convnet.add(Conv2D(128,(7,7),activation='relu',
                       kernel_regularizer=l2(2e-4),kernel_initializer=W_init,bias_initializer=b_init))
    convnet.add(MaxPooling2D())
    convnet.add(Conv2D(128,(4,4),activation='relu',kernel_initializer=W_init,kernel_regularizer=l2(2e-4),bias_initializer=b_init))
    convnet.add(MaxPooling2D())
    convnet.add(Conv2D(256,(4,4),activation='relu',kernel_initializer=W_init,kernel_regularizer=l2(2e-4),bias_initializer=b_init))
    convnet.add(Flatten())
    convnet.add(Dense(4096,activation="sigmoid",kernel_regularizer=l2(1e-3),kernel_initializer=W_init,bias_initializer=b_init))
    #encode each of the two inputs into a vector with the convnet
    encoded_l = convnet(left_input)
    encoded_r = convnet(right_input)
    #merge two encoded inputs with the l1 distance between them
    L1_distance = lambda x: K.abs(x[0]-x[1])
    both = merge([encoded_l,encoded_r], mode = L1_distance, output_shape=lambda x: x[0])
    prediction = Dense(1,activation='sigmoid',bias_initializer=b_init)(both)
    siamese_net = Model(input=[left_input,right_input],output=prediction)
    #optimizer = SGD(0.0004,momentum=0.6,nesterov=True,decay=0.0003)
    
    optimizer = Adam(0.00006)
    #//TODO: get layerwise learning rates and momentum annealing scheme described in paperworking
    siamese_net.compile(loss="binary_crossentropy",optimizer=optimizer)
    
    siamese_net.count_params()
    
    

    原论文中每个层的学习率和冲量都不相同--我跳过了这个步骤,因为使用keras来实现这个太麻烦了,并且超参数不是该论文的重点。Koch等人增加向训练集中增加失真的图像,使用150,000对样本训练模型。因为这个太大了,我的内存放不下,所以我决定使用随机采样的方法。载入图像对或许是这个模型最难实现的部分。因为这里每个类别有20个样本,我把数据重新调整为N_classes×20×105×105的数组,这样可以很方便的来索引。

    class Siamese_Loader:
        """For loading batches and testing tasks to a siamese net"""
        def __init__(self,Xtrain,Xval):
            self.Xval = Xval
            self.Xtrain = Xtrain
            self.n_classes,self.n_examples,self.w,self.h = Xtrain.shape
            self.n_val,self.n_ex_val,_,_ = Xval.shape
    
        def get_batch(self,n):
            """Create batch of n pairs, half same class, half different class"""
            categories = rng.choice(self.n_classes,size=(n,),replace=False)
            pairs=[np.zeros((n, self.h, self.w,1)) for i in range(2)]
            targets=np.zeros((n,))
            targets[n//2:] = 1
            for i in range(n):
                category = categories[i]
                idx_1 = rng.randint(0,self.n_examples)
                pairs[0][i,:,:,:] = self.Xtrain[category,idx_1].reshape(self.w,self.h,1)
                idx_2 = rng.randint(0,self.n_examples)
                #pick images of same class for 1st half, different for 2nd
                category_2 = category if i >= n//2 else (category + rng.randint(1,self.n_classes)) % self.n_classes
                pairs[1][i,:,:,:] = self.Xtrain[category_2,idx_2].reshape(self.w,self.h,1)
            return pairs, targets
    
        def make_oneshot_task(self,N):
            """Create pairs of test image, support set for testing N way one-shot learning. """
            categories = rng.choice(self.n_val,size=(N,),replace=False)
            indices = rng.randint(0,self.n_ex_val,size=(N,))
            true_category = categories[0]
            ex1, ex2 = rng.choice(self.n_examples,replace=False,size=(2,))
            test_image = np.asarray([self.Xval[true_category,ex1,:,:]]*N).reshape(N,self.w,self.h,1)
            support_set = self.Xval[categories,indices,:,:]
            support_set[0,:,:] = self.Xval[true_category,ex2]
            support_set = support_set.reshape(N,self.w,self.h,1)
            pairs = [test_image,support_set]
            targets = np.zeros((N,))
            targets[0] = 1
            return pairs, targets
    
        def test_oneshot(self,model,N,k,verbose=0):
            """Test average N way oneshot learning accuracy of a siamese neural net over k one-shot tasks"""
            pass
            n_correct = 0
            if verbose:
                print("Evaluating model on {} unique {} way one-shot learning tasks ...".format(k,N))
            for i in range(k):
                inputs, targets = self.make_oneshot_task(N)
                probs = model.predict(inputs)
                if np.argmax(probs) == 0:
                    n_correct+=1
            percent_correct = (100.0*n_correct / k)
            if verbose:
                print("Got an average of {}% {} way one-shot learning accuracy".format(percent_correct,N))
            return percent_correct
    
    

    下面是训练过程了。没什么特别的,除了我监测的是验证机精度来测试性能,而不是验证集上的损失。

    evaluate_every = 7000
    loss_every=300
    batch_size = 32
    N_way = 20
    n_val = 550
    siamese_net.load_weights("PATH")
    best = 76.0
    for i in range(900000):
        (inputs,targets)=loader.get_batch(batch_size)
        loss=siamese_net.train_on_batch(inputs,targets)
        if i % evaluate_every == 0:
            val_acc = loader.test_oneshot(siamese_net,N_way,n_val,verbose=True)
            if val_acc >= best:
                print("saving")
                siamese_net.save('PATH')
                best=val_acc
    
        if i % loss_every == 0:
            print("iteration {}, training loss: {:.2f},".format(i,loss))
    
    

    引用

    1. https://sorenbouma.github.io/blog/oneshot/
    2. https://zhuanlan.zhihu.com/p/29058453
    3. https://zhuanlan.zhihu.com/p/35040994


 

Copyright © 2018 bbs.dian.org.cn All rights reserved.

Looks like your connection to Dian was lost, please wait while we try to reconnect.