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 ¶m,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(); }