Мягкий запас в линейной машине поддержки векторов с использованием python

Я изучаю машину векторной поддержки и пытаюсь придумать простую реализацию python (я знаю о пакете sklearn, просто чтобы лучше понять концепции), что делает простую линейную классификацию. Это является основным материалом, на который я ссылаюсь.

Я пытаюсь решить SVM из первичного, минимизируя это:

введите описание изображения здесь

Производная от J wrt w (согласно приведенной выше ссылке):

введите описание изображения здесь

Таким образом, это использование потери "шарнира", а C - параметр штрафа. Если я правильно понимаю, установка большего значения C заставит SVM иметь более высокий запас.

Ниже мой код:

import numpy
from scipy import optimize

class SVM2C(object):
    def __init__(self,xdata,ydata,c=200.,learning_rate=0.01,
            n_iter=5000,method='GD'):

        self.values=numpy.unique(ydata)
        self.xdata=xdata
        self.ydata=numpy.where(ydata==self.values[-1],1,-1)
        self.c=c
        self.lr=learning_rate
        self.n_iter=n_iter
        self.method=method

        self.m=len(xdata)
        self.theta=numpy.random.random(xdata.shape[1])-0.5

    def costFunc(self,theta,x,y):
        zs=numpy.dot(x,theta)
        j=numpy.maximum(0.,1.-y*zs).mean()*self.c+0.5*numpy.sum(theta**2)
        return j

    def jac(self,theta,x,y):
        '''Derivative of cost function'''
        zs=numpy.dot(x,theta)
        ee=numpy.where(y*zs>=1.,0.,-y)[:,None]
        # multiply rows by ee
        dj=(ee*x).mean(axis=0)*self.c+theta
        return dj

    def train(self):

        #----------Optimize using scipy.optimize----------
        if self.method=='optimize':
            opt=optimize.minimize(self.costFunc,self.theta,args=(self.xdata,self.ydata),\
                    jac=self.jac,method='BFGS')
            self.theta=opt.x

        #---------Optimize using Gradient descent---------
        elif self.method=='GD':
            costs=[]
            lr=self.lr

            for ii in range(self.n_iter):
                dj=self.jac(self.theta,self.xdata,self.ydata)
                self.theta=self.theta-lr*dj
                cii=self.costFunc(self.theta,self.xdata,self.ydata)
                costs.append(cii)

            self.costs=numpy.array(costs)

        return self


    def predict(self,xdata):

        yhats=[]
        for ii in range(len(xdata)):
            xii=xdata[ii]
            yhatii=xii.dot(self.theta)
            yhatii=1 if yhatii>=0 else 0
            yhats.append(yhatii)
        yhats=numpy.array(yhats)

        return yhats



#-------------Main---------------------------------
if __name__=='__main__':

    xdata = numpy.array([[-1, -1], [-2, -1], [1, 1], [2, 1]])
    ydata = numpy.array([1, 1, 2, 2])

    mysvm=SVM2C(xdata,ydata,method='GD')
    mysvm.train()

    from sklearn import svm
    clf=svm.SVC(C=50,kernel='linear')
    clf.fit(xdata,ydata)

    print mysvm.theta
    print clf.coef_

    #-------------------Plot------------------------
    import matplotlib.pyplot as plt
    figure=plt.figure(figsize=(12,10),dpi=100)
    ax=figure.add_subplot(111)

    cmap=plt.cm.jet
    nclasses=numpy.unique(ydata).tolist()
    colors=[cmap(float(ii)/len(nclasses)) for ii in nclasses]

    #----------------Plot training data----------------
    for ii in range(len(ydata)):
        xii=xdata[ii][0]
        yii=xdata[ii][1]
        colorii=colors[nclasses.index(ydata[ii])]
        ax.plot(xii,yii,color=colorii,marker='o')

    plt.show(block=False)

Итак, это действительно пример игрушки, где есть только 4 линейно разделяемых образца обучения, и я сбросил термин смещения b, а ожидаемый результат w - [0.5, 0.5] (результат skimage), тогда как моя реализация будет иметь тенденцию давать нечто большее 0,5 (например, [1.4650, 1.4650]), используя градиентный спуск или scipy.optimize. И это происходит только при установке параметра C в > 1, когда C==1, это дает мне [0,5, 0,5]. Также, когда C > 1, сбой scipy.optimize (я пробовал несколько разных методов, например Newton-CG, BFGS), хотя конечный результат близок к результату спуска градиента.

Я немного смущен, почему вектор w перестает сокращаться. Я думал, что когда все данные будут правильно классифицированы, штрафные штрафы перестанут вносить вклад в общую функцию затрат, поэтому он будет оптимизировать J, уменьшив размер w. Я неправильно понял производные?

Я знаю, что это может быть вопрос новичков, и я вставляю какой-то грязный код, это несколько озадачило меня, и у меня нет никого, кто мог бы помочь, поэтому любая поддержка будет высоко оценена!

UPDATE:

Спасибо за помощь. Я обновляю код, чтобы иметь дело с немного более сложным образцом. На этот раз я включил термин смещения и использовал следующее для его обновления:

введите описание изображения здесь

И после полученных мной отзывов я попробовал Nelder-Mead для scipy.optimize и попробовал 2 адаптивных метода спуска градиента. Код ниже:

import numpy
from scipy import optimize

class SVM2C(object):
    def __init__(self,xdata,ydata,c=9000,learning_rate=0.001,
            n_iter=600,method='GD'):

        self.values=numpy.unique(ydata)
        # Add 1 dimension for bias
        self.xdata=numpy.hstack([xdata,numpy.ones([xdata.shape[0],1])])
        self.ydata=numpy.where(ydata==self.values[-1],1,-1)
        self.c=c
        self.lr=learning_rate
        self.n_iter=n_iter
        self.method=method

        self.m=len(xdata)
        self.theta=numpy.random.random(self.xdata.shape[1])-0.5

    def costFunc(self,theta,x,y):
        zs=numpy.dot(x,theta)
        j=numpy.maximum(0.,1.-y*zs).mean()*self.c+0.5*numpy.sum(theta[:-1]**2)
        return j

    def jac(self,theta,x,y):
        '''Derivative of cost function'''
        zs=numpy.dot(x,theta)
        ee=numpy.where(y*zs>=1.,0.,-y)[:,None]
        dj=numpy.zeros(self.theta.shape)
        dj[:-1]=(ee*x[:,:-1]).mean(axis=0)*self.c+theta[:-1] # weights
        dj[-1]=(ee*self.c).mean(axis=0)                      # bias

        return dj

    def train(self):

        #----------Optimize using scipy.optimize----------
        if self.method=='optimize':
            opt=optimize.minimize(self.costFunc,self.theta,args=(self.xdata,self.ydata),\
                    jac=self.jac,method='Nelder-Mead')
            self.theta=opt.x

        #---------Optimize using Gradient descent---------
        elif self.method=='GD':

            costs=[]
            lr=self.lr
            # ADAM parameteres
            beta1=0.9
            beta2=0.999
            epsilon=1e-8

            mt_1=0
            vt_1=0
            for ii in range(self.n_iter):
                t=ii+1
                dj=self.jac(self.theta,self.xdata,self.ydata)
                '''
                mt=beta1*mt_1+(1-beta1)*dj
                vt=beta2*vt_1+(1-beta2)*dj**2
                mt=mt/(1-beta1**t)
                vt=vt/(1-beta2**t)
                self.theta=self.theta-lr*mt/(numpy.sqrt(vt)+epsilon)
                mt_1=mt
                vt_1=vt

                cii=self.costFunc(self.theta,self.xdata,self.ydata)
                '''
                old_theta=self.theta
                self.theta=self.theta-lr*dj
                if ii>0 and cii>costs[-1]:
                    lr=lr*0.9
                    self.theta=old_theta


                costs.append(cii)
            self.costs=numpy.array(costs)

        self.b=self.theta[-1]
        self.theta=self.theta[:-1]

        return self


    def predict(self,xdata):

        yhats=[]
        for ii in range(len(xdata)):
            xii=xdata[ii]
            yhatii=numpy.sign(xii.dot(self.theta)+self.b)
            yhatii=xii.dot(self.theta)+self.b
            yhatii=self.values[-1] if yhatii>=0 else self.values[0]
            yhats.append(yhatii)
        yhats=numpy.array(yhats)

        return yhats

#-------------Main---------------------------------
if __name__=='__main__':

    #------------------Sample case 1------------------
    #xdata = numpy.array([[-1, -1], [-2, -1], [1, 1], [2, 1]])
    #ydata = numpy.array([1, 1, 2, 2])

    #------------------Sample case 2------------------
    from sklearn import datasets
    iris=datasets.load_iris()
    xdata=iris.data[20:,:2]
    ydata=numpy.where(iris.target[20:]>0,1,0)

    #----------------------Train----------------------
    mysvm=SVM2C(xdata,ydata,method='GD')
    mysvm.train()

    ntest=20
    xtest=2*(numpy.random.random([ntest,2])-0.5)+xdata.mean(axis=0)

    from sklearn import svm
    clf=svm.SVC(C=50,kernel='linear')
    clf.fit(xdata,ydata)

    yhats=mysvm.predict(xtest)
    yhats2=clf.predict(xtest)

    print 'mysvm weights:', mysvm.theta, 'intercept:', mysvm.b
    print 'sklearn weights:', clf.coef_, 'intercept:', clf.intercept_
    print 'mysvm predict:',yhats
    print 'sklearn predict:',yhats2

    #-------------------Plot------------------------
    import matplotlib.pyplot as plt
    figure=plt.figure(figsize=(12,10),dpi=100)
    ax=figure.add_subplot(111)

    cmap=plt.cm.jet
    nclasses=numpy.unique(ydata).tolist()
    colors=[cmap(float(ii)/len(nclasses)) for ii in nclasses]

    #----------------Plot training data----------------
    for ii in range(len(ydata)):
        xii=xdata[ii][0]
        yii=xdata[ii][1]
        colorii=colors[nclasses.index(ydata[ii])]
        ax.plot(xii,yii,color=colorii,marker='o',markersize=15)

    #------------------Plot test data------------------
    for ii in range(ntest):
        colorii=colors[nclasses.index(yhats2[ii])]
        ax.plot(xtest[ii][0],xtest[ii][1],color=colorii,marker='^',markersize=5)

    #--------------------Plot line--------------------
    x1=xdata[:,0].min()
    x2=xdata[:,0].max()

    y1=(-clf.intercept_-clf.coef_[0][0]*x1)/clf.coef_[0][1]
    y2=(-clf.intercept_-clf.coef_[0][0]*x2)/clf.coef_[0][1]

    y3=(-mysvm.b-mysvm.theta[0]*x1)/mysvm.theta[1]
    y4=(-mysvm.b-mysvm.theta[0]*x2)/mysvm.theta[1]

    ax.plot([x1,x2],[y1,y2],'-k',label='sklearn line')
    ax.plot([x1,x2],[y3,y4],':k',label='mysvm line')
    ax.legend(loc=0)
    plt.show(block=False)

Новые проблемы, которые я получил:

  • он нестабилен, в зависимости от исходных случайных параметров, результаты могут быть совершенно разными. И примерно в половине случаев, это будет mis-classifiy 1 образец в наборе обучения, даже если я установил C довольно большое значение. Это происходит как с scipy.optimize, так и с GD.
  • Подход ADAM имеет тенденцию давать inf для vt, так как для больших C, vt растет очень быстро. Я неправильно понял градиенты?

Тонны благодарности заранее!

Ответ 1

Что касается scipy.optimize, вы неправильно используете его методы оптимизации. Как Newton-CG, так и BFGS предполагают, что ваша функция затрат гладкая, что не так. Если вы используете надежный метод без градиента, например, Nelder-Mead, вы в большинстве случаев сходите в нужную точку (я попробовал).

Ваша проблема может быть теоретически решена с помощью градиентного спуска, но только если вы адаптируете ее к негладкой функции. В настоящее время ваш алгоритм приближается к оптимальному быстрому, но начинает прыгать вокруг вместо сближения из-за большой скорости обучения в сочетании с резким изменением градиента, где максимум в функции стоимости изменяется от 0 до положительного:/p >

введите описание изображения здесь

Вы можете успокоить эти колебания, уменьшая скорость обучения каждый раз, когда ваши затраты не уменьшаются относительно предыдущей итерации

def train(self):

    #----------Optimize using scipy.optimize----------
    if self.method=='optimize':
        opt=optimize.minimize(self.costFunc,self.theta,args=(self.xdata,self.ydata),\
                jac=self.jac,method='BFGS')
        self.theta=opt.x

    #---------Optimize using Gradient descent---------
    elif self.method=='GD':
        costs=[]
        lr=self.lr

        for ii in range(self.n_iter):
            dj=self.jac(self.theta,self.xdata,self.ydata)
            old_theta = self.theta.copy()
            self.theta=self.theta-lr*dj
            cii=self.costFunc(self.theta,self.xdata,self.ydata)

            # if cost goes up, decrease learning rate and restore theta
            if len(costs) > 0 and cii > costs[-1]:
                lr *= 0.9
                self.theta = old_theta
            costs.append(cii)

        self.costs=numpy.array(costs)

    return self

Эта небольшая поправка к вашему коду приводит к значительно лучшей конвергенции:

введите описание изображения здесь

и в результирующих параметрах, которые близки к оптимальным - например, [0.50110433 0.50076661] или [0.50092616 0.5007394 ].

В современных приложениях (например, нейронных сетях) эта адаптация скорости обучения реализуется в рамках расширенных алгоритмов спуска градиента, таких как ADAM, которые постоянно отслеживают изменения в среднем и дисперсии градиента.

Обновление. Эта вторая часть ответа касается версии кода secont.

Об ADAM. Вы получили взрыв vt из-за строки vt=vt/(1-beta2**t). Вы должны нормализовать только значение vt, используемое для вычисления шага градиента, а не значение, которое идет на следующую итерацию. Как здесь:

...
mt=beta1*mt_1+(1-beta1)*dj
vt=beta2*vt_1+(1-beta2)*dj**2
mt_temp=mt/(1-beta1**t)
vt_temp=vt/(1-beta2**t)
old_theta=self.theta
self.theta=self.theta-lr*mt_temp/(numpy.sqrt(vt_temp)+epsilon)
mt_1=mt
vt_1=vt
...

О нестабильности. Как метод Нельдера-Мида, так и градиентный спуск зависят от начального значения параметров, что печальная правда. Вы можете попытаться улучшить конвергенцию, сделав больше итераций GD и затухающей скорости обучения более разумным способом или уменьшив параметры оптимизации, такие как xatol и fatol для метода Nelder-Mead.

Однако, даже если вы достигнете идеальной конвергенции (значения параметра, такие как [ 1.81818459 -1.81817712 -4.09093887] в вашем случае), у вас есть проблемы. Конвергенцию можно примерно проверить следующим кодом:

print(mysvm.costFunc(numpy.concatenate([mysvm.theta, [mysvm.b]]), mysvm.xdata, mysvm.ydata))
print(mysvm.costFunc(numpy.concatenate([mysvm.theta, [mysvm.b+1e-3]]), mysvm.xdata, mysvm.ydata))
print(mysvm.costFunc(numpy.concatenate([mysvm.theta, [mysvm.b-1e-3]]), mysvm.xdata, mysvm.ydata))
print(mysvm.costFunc(numpy.concatenate([mysvm.theta-1e-3, [mysvm.b]]), mysvm.xdata, mysvm.ydata))
print(mysvm.costFunc(numpy.concatenate([mysvm.theta+1e-3, [mysvm.b]]), mysvm.xdata, mysvm.ydata))

что приводит к

6.7323592305075515
6.7335116664996
6.733895813394582
6.745819882839341
6.741974212439457

Ваша стоимость увеличивается, если вы меняете theta или перехват в любом направлении - таким образом, решение является оптимальным. Но тогда решение sklearn не является оптимальным (с точки зрения mysvm), поскольку код

print(mysvm.costFunc(numpy.concatenate([clf.coef_[0], clf.intercept_]), mysvm.xdata, mysvm.ydata))

печатает 40.31527145374271! Это означает, что вы достигли локального минимума, но SVM sklearn минимизировал что-то другое.

И если вы читаете документацию sklearn, вы можете найти, что не так: они минимизируют sum(errors) * C + 0.5 * penalty, и вы минимизируете mean(errors) * C + 0.5 * penalty!!! Это наиболее вероятная причина расхождения.