特征缩放(feature scaling)
其中,特征缩放(feature scaling)大致的思路是这样的:梯度下降算法中,在有多个特征的情况下,如果你能确保这些不同的特征都处在一个相近的范围,这样梯度下降法就能更快地收敛。
举个例子来说明:
x1=size(0−2000)feet2
x2=卧室的数量(1−5)
假如你有一个具有两个特征的问题,其中 x1 是房屋面积大小,它的取值在0到2000之间; x2 是卧室的数量,可能这个值的取值范围在1到5之间。其代价函数 J(θ) 是一个关于参数 θ0,θ1和θ2 的函数。但这里我们暂时不考虑 θ0 并假想一个函数的变量只有 θ1和θ2 。
如果 x1 的取值范围远远大于 x2 的取值范围的话,那么最终画出来的代价函数 J(θ) 的轮廓图就会呈现出这样一种非常偏斜并且椭圆的形状:
如果你用这个代价函数来运行梯度下降的话,你要得到梯度值最终可能需要花很长一段时间,并且可能会来回波动,然后会经过很长时间最终才收敛到全局最小值。
事实上如果这些轮廓再被放大一些的话,如果你画的再夸张一些把它画的更细更长,那么可能情况会更糟糕,梯度下降的过程可能更加缓慢,需要花更长的时间反复来回振荡,最终才找到一条正确通往全局最小值的路。
在这样的情况下一种有效的方法是进行特征缩放(feature scaling)。
具体来说把特征 x 定义为:
x2=卧室的数量5
通过这样的变化,表示代价函数 J(θ) 的轮廓图的形状就会变得偏移没那么严重,可能看起来更圆一些了。
如果你用这样的代价函数来执行梯度下降的话,那么可以从数学上来证明梯度下降算法将会找到一条更捷径的路径通向全局最小,而不是像刚才那样 沿着一条让人摸不着头脑的路径,来找到全局最小值。
因此在这个例子中,通过特征缩放,我们最终得到的两个特征 x1 和 x2 都在0和1之间,这样你得到的梯度下降算法就会更快地收敛。
更一般地,我们执行特征缩放时,我们通常的目的是将特征的取值约束到−1到+1的范围内。其中,特征 x0 总是等于1,因此这已经是在这个范围内了,但对于其他的特征,你可能需要通过除以不同的数来让它们处于同一范围内。
−1和+1这两个数字并不是太重要,所以如果你有一个特征 x1 它的取值在0 ~ 3之间,这没问题 如果你有另外一个特征取值在-2 ~ +0.5之间,这也没什么关系,因为这也非常接近-1 ~ +1的范围。
但如果你有另一个特征x3,假如它的范围在-100 ~ +100之间,那么这个范围跟-1 ~ +1就有很大不同了。所以这可能是一个不那么好的特征。类似地,如果你的特征在一个非常非常小的范围内,比如另外一个特征x4,它的范围在-0.0001 ~ +0.0001之间,那么这同样是一个比-1 ~ +1小得多的范围,因此我同样会认为这个特征也不太好。所以可能你认可的范围,也许可以大于或者小于-1 ~ +1,但是也别太大或太小,只要与-1 ~ +1范围偏差不多就可以接受。
因此,总的来说不用过于担心你的特征是否在完全相同的范围或区间内,但是只要它们足够接近的话,梯度下降法就会正常地工作。
均值归一化(mean normalization)
除了在特征缩放中将特征除以最大值以外,有时候我们也会进行一个称为均值归一化(mean normalization)的工作。
具体做法就是:如果你有一个特征 xi 你就用 xi−μi 来替换。这样做的目的是为了让你的特征值具有为0的平均值。很明显 我们不需要把这一步应用到 x0中,因为x0 总是等于1的,所以它不可能有为0的的平均值。
但是对其他的特征来说,比如房子的大小取值介于0 ~ 2000,并且假如房子面积的平均值是等于1000的,那么你可以用这个公式
x1=size−10002000
类似地,如果你的房子有五间卧室,并且平均一套房子有两间卧室,那么你可以使用这个公式来归一化你的第二个特征x2:
x2=卧室数−25
在这两种情况下你可以算出新的特征 x1和x2 ,它们的范围可以在-0.5 ~ +0.5之间,当然这肯定不对,x2的值实际上肯定会大于0.5。更一般的规律是用:
xn−μnSn , , μn为平均值 , , Sn为最大值−最小值
来替换原来的特征 xn 。其中定义μn的意思是在训练集中特征 xn 的平均值。而 Sn 是该特征值的范围(最大值减去最小值)。
最后直的一提的是:特征缩放其实并不需要太精确,其目的只是为了让梯度下降能够运行得更快一点,让梯度下降收敛所需的循环次数更少一些而已。
均值归一化过程
#include<bits/stdc++.h>
using namespace std;
#define eps 1e-10
double x[100][100]={{1,1,4},{1,2,5},{1,5,1},{1,4,2}};
double y[100]={19,26,19,20};
double theta[100]={0,0};
int m=4;
int n=2;
double learn_rate=0.1;
double error=100.0;
void p(){
for(int i=1;i<=n;i++){ //lie
double averagex=0.0;
double Maxx2=x[0][i],Minx2=x[0][i];
for(int j=0;j<m;j++){
averagex+=x[j][i];
Maxx2=max(Maxx2,x[j][i]);
Minx2=min(Minx2,x[j][i]);
}
averagex/=m;
//cout<<Maxx2<<" "<<Minx2<<" "<<averagex<<endl;
for(int j=0;j<m;j++){
x[j][i]=(x[j][i]-averagex)/(Maxx2-Minx2);
}
}
}
double get_hx(int index){
double ans=0.0;
for(int i=0;i<=n;i++) ans+=x[index][i]*theta[i];
return ans;
}
int main(){
double error1=0.0,error2=0.0;
// cout<<endl<<endl;
// cout<<"x0"<<" "<<"x1"<<" "<<"x2"<<" "<<"y"<<endl;
//
// for(int i=0;i<m;i++){
// for(int j=0;j<=n;j++){
// cout<<x[i][j]<<" ";
// }
// cout<<y[i];
// cout<<endl;
// }
// cout<<endl<<endl;
p();
// cout<<"x0"<<" "<<"x1"<<" "<<"x2"<<" "<<"y"<<endl;
// for(int i=0;i<m;i++){
// for(int j=0;j<=n;j++){
// printf("%.2f ",x[i][j]);
// }
// cout<<y[i];
// cout<<endl;
// }
// cout<<endl<<endl;
//
//
// return 0;
while(true){
for(int i=0;i<m;i++){
double h_x=get_hx(i);
for(int j=0;j<=n;j++){
theta[j]+=(y[i]-h_x)*learn_rate*x[i][j]/m;
}
}
for(int i=0;i<=n;i++) cout<<theta[i]<<" ";
cout<<endl;
//找出最小的值,即与上一次的结果相差小于eps
error1=0.0;
for(int i=0;i<m;i++){
error1+=((y[i]-get_hx(i))/m)*(y[i]-get_hx(i))/2;
}
if(abs(error1-error2)<eps) break;
else error2=error1;
}
return 0;
}
数据均值话之前和均值化之后:
注意:y不需要处理
数据均值话之前和均值化之后的拟合结果:
处理前:
处理后:
可以分别得到拟合函数:
处理前:
y1=0+3x1+4x2
处理后:
y2=21+12x1+16x2
拟合数据的目的是为了得到预测数据,数据均值处理前后拟合函数不一样,又怎样得到拟合函数呢?
其实两条函数是等价的:
y2=21+12x1−μn1Sn1+16x2−μn2Sn2==>y1
因此,新的预测数据也要均值处理,再运用拟合函数,能得到相同结果:
例如(6,6),求Y
y1=0+3∗6+4∗6=42
y2=21+126−34+166−34=42
两式子是等效的。但是,数据均值处理后的处理能更快的拟合出结果,运算更快。