Logistic回歸


Part I: 線性回歸

  線性回歸很常見,給你一堆點,作出一條直線,盡可能去擬合這些點。對於多維的數據,設特征為xi,設函數$h(\theta )=\theta+\theta_{1}x_{1}+\theta_{2}x_{2}+....\theta_{n}x_{n}$為擬合的線性函數,其實就是內積,實際上就是$y=w^{T}x+b$

那么如何確定這些θ參數(parament)才能保證擬合比較好呢?

1.1 使用最小二乘法的目標函數

我們易有這樣的二次目標函數:$J=min\sum_{i=1}^{m}\frac{1}{2}(h(\theta )-y^{i})^{2}$ ,m即所有數據條數。

該目標函數的意義在於:誤差最小化。這就是最小二乘法。

h(θ中的h即是hypothesis(假設的意思),h函數是我們的算出結果,yi是我們的實際結果,平方的作用有兩個,一是是代替ABS,二方便求導。

想要實現目標函數,然后求出θ,最傳統的是矩陣方法(最小二乘法擬合最常用的數學方法),$\theta=(X^{T}X)^{-1}X^{T}Y$。

該式子來自於二維最小二乘法(一次擬合、忽視偏置=就一個方程):$\theta\sum_{i=1}^{N}x_{i}^{2}=\sum_{i=1}^{N}x_{i}y_{i}$

如果你是使用MATLAB之類的話,那么就是幾行代碼的事。如果是C/JAVA,還是乖乖使用下面的梯度下降算法(Gradient Descent)。

值得注意的事,矩陣方法要計算矩陣的逆(慢、而且可能不存在、且編程麻煩),復雜度O(n^3),在數據量較大的時候,效率會被梯度下降算法爆掉。

1.2  梯度下降算法

計算機是很笨的。如果想要確定某個值,那么有一種很笨的方法:迭代法。

設定一個邊界初值(最大or最小),然后一小步一小步的去變化這個值並驗證,知道最后逼近最優值,停止迭代。

由於目標函數是最小化,我們有這個古老的算法。

 $\theta_{i}^{new}=\theta_{i}^{old}-\alpha\frac{\partial }{\partial \theta}J(\theta)$

至於原理,參照wiki的解釋,這樣調整θ,會使得目標函數不停的收斂,同時偏導的梯度控制了收斂的速度。

首先把各個θ設為零,讓其從一個平衡狀態,在迭代過程中緩慢修改。

減小的量由alpha(學習率,由數據大小確定,既要防止調整過大,也要防止調整過小)*目標函數J的偏導數構成。

經驗規則表明,對於Tanh/Logistic激活函數,初始學習率在0.1是個不錯的選擇,如果訓練過程中迅速過擬合,或出現NaN,表明學習率過大。

降一個量級或除以2試試。對於線性激活函數,如果誤差波動較大,可以適當的降低量級。

調整各個θ的同時,目標函數J的計算值也在減小,向局部最小值移動。

同時梯度的下降速度由疾變緩,在最后的迭代過程中,梯度將會趨於0,從而能夠保證較為精確的逼近目標值。這樣,既保證了目標函數的最小化,又求出的參數θ。

1.3 偏導數的處理

假設只有一條數據。目標函數J的偏導數你可以化簡一下。鏈式法則求導。

 

最后的求導結果是一個非常神奇的式子:$(h(\theta)-y)*x_{i}$

如果是多條數據呢?累加偏導結果即可。

 $\theta_{i}^{new}=\theta_{i}^{old}-\sum_{j=1}^{m}\alpha( h_{\theta}(x^{j})-y^{j})*x_{i}^{j}$

由此誕生了三種常用訓練梯度算法。

1.4 三種梯度訓練梯度方法

一、批梯度算法(BGD,Batch Gradient Descent)

正如上面的式子,先掃每個維度,然后掃全部數據累計$\theta$的變化值

for(i:維度)

  for(j:數據條數)

      $\theta_{i}^{new}=\theta_{i}^{old}-\sum_{j=1}^{m}\alpha*(h(\theta)-y^{j})*x_{i}^{j}$

每次迭代復雜度O(n^3), 下場是:慢。

二、隨機梯度算法(SGD,Stochastic Gradient Descent)

它的改良就是,調整循環順序,每次只取一條數據作為誤差,將h(θ)預處理,去求梯度,而不是放在兩層循環下變成三層循環去求個總梯度。

for(j:數據條數)

   $calc\quad h(\theta )$

   for(i: 維度)

     $\theta_{i}^{new}=\theta_{i}^{old}-\alpha*(h(\theta)-y^{j})*x_{i}^{j}$

每次迭代復雜度O(n^2)

PS. 關於常量b的求法,即對b的偏導*alpha,$b=\alpha*(h(\theta)-y^{j})$

這種算法偷工減了一層for,並且帶來了更少的迭代次數(實測,批:50次,隨:35次)下場就是,最后的近似值沒有批梯度精確,不過夠用了。

三、迷你批梯度算法(mBGD,Mini-Batch Gradient Descent

深度學習中使用。每次取一小段數據做完全梯度,而不是只用一個。在速度和精度之間做了一個均衡。

SGD是BatchSize=1的特例,即每次更新只對一個樣本誤差取梯度。BGD是BatchSize=ExampleSize的特例,每次更新對全部樣本取梯度。

為了平衡計算,mini-batch里把跑完全樣本梯度一次稱為epoch,跑一個batch稱為iter。

訓練方法具體參考:Theano深度學習分析

1.5 迭代問題

如何確定迭代的次數?我們大可以設個500,讓它一直算下去,然而還是有一些停止技巧。

①目標函數收斂判斷法(淺層學習)

迭代停止條件目前分為兩個。設精度是0.001

一、近似達到目標函數的局部最小值,即變化小於0.001

二、各個參數Θi的變化小於0.001(可選)

一般達到這兩個條件,迭代就可以考慮停止了。

②交叉驗證&Early-Stopping(深度學習)

在Deep Learning里,單純看目標函數是不靠譜的,我們只能根據目標函數下降,來判斷學習的有效進行。

但是我們並不能知道學習情況。尤其是加入L1/L2懲罰后,目標函數的比例會出現問題。

更嚴重的是,過擬合情況很難從目標函數中發現。有時候目標函數還在下降,其實已經過擬合很嚴重了。

這時候就需要Early-Stopping,所以引入交叉驗證手段(需要對訓練樣本划分驗證集),具體參考:Theano深度學習分析

其中,每次epoch,測一次驗證集,根據驗證集錯誤率下降情況,判斷過擬合,這是訓練深度神經網絡的基本功。

下圖是訓練一個人臉檢測的CNN,加了L2懲罰,似然函數比往常大了一個量級,交叉驗證直觀反應了訓練情況

1.6 優化:局部加權回歸

擬合時,我們希望離預測點比較遠的點分配比較小的權重,而離的比較近的點分配比較大的權重。

這樣,擬合時,擬合的出的線將不是一條單調的直線(容易欠擬合,不能很好適應數據特征),而是一條隨數據擺動的曲線。

這樣就帶來新的問題:過擬合,即隨着數據不停擺動(可以理解成把所有數據點用折線連起來),容易受到噪聲數據影響,導致擬合效果奇差。

因此,局部加權回歸比較吃數據,也比較難調整,用不好基本就完蛋,但是某些大師很喜歡用(來自Andrew NG的吐槽)。

根據距離遠近分配權重由高斯核函數(又名徑向基核函數)完成。

$f=e^{\frac{{\begin{vmatrix}x^{i}-x\end{vmatrix}}^{2}}{-2k^{2}}}$

距離采用的是歐拉距離(同KNN),k值是這個核函數唯一需要調整的地方。

通常取值有0.001,0.01,0.1,1,10,k越小,離預測點越近的點越容易分配到比較大的權重,就越容易過擬合。

加權方法:

for(每一個預測點)

  for(每條樣本數據)

     計算該條數據的加權$x_{i}^{new}=f*x_{i}^{old}$

然后利用加權過后的$x_{i}^{new}$進行擬合

 

Part Ⅱ  Logistic回歸

Logistic回歸中文又叫邏輯回歸。它和線性回歸的最大區別在於:它將線性回歸結果,通過Logistic函數生成概率,從而進行0/1分類。

盡管Logistic函數是非線性的(S形,平滑),然而它的功能只是生成概率,而不是非線性划分數據。

所以Logistic回歸只對線性可分的數據的效果比較好,對於線性不可分數據,需要使用含有將數據映射到高層空間的,BP神經網絡網絡或SVM神經網絡。

2.1 概率論與目標函數設計的原理

已有$x_{i}^{j}$,若求出$\theta$,則$y^{j}$即可回歸算出。這是一個事實。

那么假如用$P(y^{j}|x_{i}^{j};\theta)$表示擬合出$y^{j}$的概率,當然這個概率不是說高就高,說低就低的,它近似服從正態(高斯)分布。

我們的任務是設計很科學的$\theta$,使得利用樣本數據造成的概率分布盡可能接近正態分布,也就是說估計出$\theta$的近似值。

這樣的模型稱之為判別模型(Discriminate Model),判別模型可以用於分類/回歸。

假設$P(y^{j}|x_{i}^{j};\theta)$服從正態分布,那么就有了概率密度函數,利用概率密度函數,反過來設計一個對$\theta$的似然估計函數$L(\theta)$

於是最終任務便變成:基於假設下,利用最大似然估計方法求出$\theta$。

由於最大化$L(\theta)$式子與最小二乘法的式子很接近,於是近似認為最大似然方法等效於最小二乘方法,這便是最小二乘法的概率論角度的解釋。

2.2 誤差服從二項分布的目標函數

由於Logistic回歸用於二類分類時,y的取值非0,即1。上述的正態分布,則變成二項分布。

由二項分布,有如下的概率分布。

$P(y=1|x;\theta)=h(\theta)$

$P(y=0|x;\theta)=1-h(\theta)$

合並這兩個分布為一個式子$P(y|x;\theta)$,即概率分布函數,這里由於y取值的特殊性,所以有以下優美的指數式子(方便取對數似然函數)

於是有關於y的概率密度函數,取對參數$\theta$的似然估計,就是估計出最准確的$\theta$,累乘概率密度函數。

 

老樣子,log取對數,化累乘為累加

 

求偏導數。

 

 

然后你就會發現這個偏導結果怎么那么耳熟?怎么和那個回歸的最小二乘法J函數的偏導結果差不多。

真是個優美的式子。於是你又可以用梯度下降算法了。

不同的是,這次你得讓似然函數取最大值,也就是說,參數θ初始設為0,然后慢慢增加,直到似然函數取得最大值。這其實是梯度上升算法

2.3  概率映射Logistic函數——化回歸為分類,Logistic回歸之本質

關於分類與回歸,不同在於y的取值類型。連續型叫回歸,離散型叫分類。當然一般分類指的是二類分類問題。回歸的方法固然可以拿來回歸分類問題。

一個簡單的方法就是對於最后回歸出的連續型y,加上階躍函數(負0正1),轉化為離散型y。這就是60年代盛行的線性神經網絡,即Rosenblatt感知器。

Logistic回歸在線性回歸+階躍函數的基礎做了改良,對線性回歸結果使用了Logistic-Sigmoid函數,這樣h(Θ)=Sigmoid(內積)。

 

 

Logistic函數值域[0,1],非線性雙端飽和,平滑,目前廣泛用於概率生成函數 。可以參考:限制Boltzmann機

有效定義域范圍[-3,3],所以要對輸入進行進行處理,盡量縮放至[-1,1]。

有一個非常容易混淆的地方,就是神經網絡的Sigmoid隱層激活函數和這里概率生成函數作用是不同的。

Sigmoid除了有良好的概率生成能力之外,還有非線性加強輸入的能力(中央區域強化信號,兩側區域弱化信號)

在神經網絡中會用來做激活函數。可以參考:ReLu激活函數

2.4 迭代問題

迭代終止條件最重要的是保證似然函數的近似值最大且收斂。

 

也就是說,每次迭代時,都要算一算這個似然目標函數的值,不出意外,應該是逐步增長,速度由快到慢,最后收斂於一個值的。注意這里h(Θ)要經過sigmoid函數處理,不然log會爆的。

2.5  C++代碼

#include "cstdio"
#include "iostream"
#include "fstream"
#include "vector"
#include "sstream"
#include "string"
#include "math.h"
using namespace std;
#define N 500
#define delta 0.0001
#define alpha 0.1
#define cin fin
struct Data
{
    vector<int> feature;
    int y;
    Data(vector<int> feature,int y):feature(feature),y(y) {}
};
struct Parament
{
    vector<double> w;
    double b;
    Parament() {}
    Parament(vector<double> w,double b):w(w),b(b) {}
};
vector<Data> dataSet;
Parament parament;
void read()
{
    ifstream fin("traindata.txt");
    int id,fea,cls,cnt=0;
    string line;
    while(getline(cin,line))
    {
        stringstream sin(line);
        vector<int> feature;
        sin>>id;
        while(sin>>fea) feature.push_back(fea);
        cls=feature.back();feature.pop_back();
        dataSet.push_back(Data(feature,cls));
    }
    parament=Parament(vector<double>(dataSet[0].feature.size(),0.0),0.0);
}
double calcInner(Parament param,Data data)
{
    double ret=0.0;
    for(int i=0;i<data.feature.size();i++) ret+=(param.w[i]*data.feature[i]);
    return ret+param.b;
}
double sigmoid(Parament param,Data data)
{
    double inner=calcInner(param,data);
    return exp(inner)/(1+exp(inner));
}
double calcLW(Parament param)
{
    double ret=0.0;
    for(int i=0;i<dataSet.size();i++)
    {
        double h=sigmoid(param,dataSet[i]);
        ret+=(dataSet[i].y*log(h))+(1-dataSet[i].y)*log(1-h);
    }
    return ret;
}
void gradient(Parament &param,int iter)
{
    /*batch
    for(int i=0;i<param.w.size();i++)
    {
        double ret=0.0;
        for(int j=0;j<dataSet.size();j++)
        {
            double ALPHA=(double)0.1/(iter+j+1)+0.1;
            ret+=ALPHA*(dataSet[j].y-sigmoid(param,dataSet[j]))*dataSet[j].feature[i];
        }
        param.w[i]+=ret;
    }
    for(int i=0;i<dataSet.size();i++) ret+=alpha*(dataSet[i].y-sigmoid(param,dataSet[i]));
    */
    //random
    for(int j=0;j<dataSet.size();j++)
    {
        double ret=0.0,h=sigmoid(param,dataSet[j]);
        double ALPHA=(double)0.1/(iter+j+1)+0.1;
        for(int i=0;i<param.w.size();i++)
           param.w[i]+=ALPHA*(dataSet[j].y-h)*dataSet[j].feature[i];
        param.b+=alpha*(dataSet[j].y-h);
    }
}
bool samewb(Parament p1,Parament p2)
{
    for(int i=0;i<p1.w.size();i++)
         if(fabs(p2.w[i]-p1.w[i])>delta) return false;
    if(fabs(p2.b-p1.b)>delta) return false;
    return true;
}
void classify()
{
    ifstream fin("testdata.txt");
    int id,fea,cls;
    string line;
    while(getline(cin,line))
    {
        stringstream sin(line);
        vector<int> feature;
        sin>>id;
        while(sin>>fea) feature.push_back(fea);
        cls=feature.back();feature.pop_back();
        double p1=sigmoid(parament,Data(feature,cls)),p0=1-p1;
        cout<<"id:"<<id<<"  origin:"<<cls<<" classify:";
        if(p1>=p0) cout<<" 1"<<endl;
        else cout<<" 0"<<endl;
    }
}
void mainProcess()
{
    double objLW=calcLW(parament),newLW;
    Parament old=parament;
    int iter=0;
    gradient(parament,iter);
    newLW=calcLW(parament);
    while(fabs(newLW-objLW)>delta||!samewb(old,parament))
    {
        objLW=newLW;
        old=parament;
        gradient(parament,iter);
        newLW=calcLW(parament);
        iter++;
        if(iter%5==0) cout<<"iter: "<<iter<<"  target value: "<<newLW<<endl;
    }
    cout<<endl<<endl;
}
int main()
{
    read();
    mainProcess();
    classify();
}
View Code

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM