文章作者是我的一位大神学长陈雨学长~,征得学长同意后将文章转载到了我的blog上,特在此感谢学长~
代码在学长的github上: https://github.com/unnamed2/MDL 欢迎猛击
自己实现的机器学习1 1 机器学习学的是什么 10 featureslabels和parameters11 误差函数 LossFunctionLossFunction 12 学习率 2 简单的例子 20 实现21 尝试 3 说明对于给定的一组数据,我们通过人类的智慧,可以得到一组数据观测值:
给定一个(28x28)的图片,我们可以用双眼看出来这个图片上面写的数字是几:
给定这16组,每组28x28=784个数值,我们可以很容易的看出来每组数据所代表的图片上刻画的数字最接近哪个数字; 但是这对于一个机器来讲,就不那么简单了,他看到的仅仅是一堆数字而已(0.0,0.1,0.02,0.5…);
如果现在 让我们手工编写一个以一个包含784个数字的数组作为输入,以这个数组描述的图片所描述的数字(0 - 9)作为输出的函数 get_picture_number,这将是非常困难的(欢迎勇者尝试) ,就算是人类的智力,对于一些手写数字(0和6 4和9,3和5等等手写出来比较潦草的情况下)也会是难以分辨的.
不过,也正是这些难度很高的函数对人类来说难以编写,所以才有了机器学习: 人类只需要给计算机编写一个学习的算法,让机器从数据里面学习. 之后的实验证明,一个难度很大的函数等于一个简单的学习算法+好的训练数据.
现在训练数据已经有了(MINST数据集),就差了一个简单的学习算法而已.
人类之所以智力上强于其他生物,正是因为人类懂得学习; 让人类变得更强大的是,人类吧学习方法告诉了忠于人类的计算机: 某些智慧之人发明了一种方法–设计一个数学的函数去拟合这个结果:
N(features,parameters)=labels
其中 features 是要输入的一组数据(比如是一张图片), labels 是这个数据的观测值(比如这个图片上面是9,并不是任何其他的数字),剩下的 parameters 则正是机器要学习的内容—一些参数. 我们人类为机器设计一个函数 N ,使得在features,parameters作为输入的条件下,输出是 labels ..比如 parameters 可以是一个[784x10]的矩阵,让 parameters 和 features 相乘,得到一个10个数字的向量 z ,取其中最大的值({z0,z1...z9}) 的下标(0 - 9)作为分类结果:即这个图片属于0 - 9 10种类别的哪一类.然而,我们并不知道 parameters 应该取哪7840个数字才能达到这个效果: 这时候就可以让机器去寻找着7840个数字了 : 让机器学习这些权重,使得这个结果能达到我们需要的结果 . 实际上 想要达到100%的准确度几乎是不可能完成的,对于那样一个简单的矩阵乘法来说,到达90%已经是很可以的结果了.同时在训练的时候,我们同样设计一个误差函数 ,使得输出的和我们的观测值越接近,该误差函数的输出越小,训练的过程就是对 Loss(N(features,parameters),labels) 中的parameters求取合适的值,使得函数取得最小值 过程.
在给定一组 features 和 labels 的情况下,通过我们的函数用 features 作为输入计算出的结果可能并不准确,相对于给定的 labels 存在一定的误差,而且这个误差总应该是正数(不存在比没有误差还好的结果).我们需要设计一个误差函数 C ,来告诉我们的网络他究竟错在了哪里:
对于这种总是输出正数的函数 很容易想到一些:
二次函数C(X)=X2
绝对值函数 C(X)=|X|
一些稍微复杂一些的函数: C(X)=X−log2(X)
指数函数 C(X)=eX
等等 .
我们将使用二次误差函数作为起点,慢慢尝试着其他误差函数.使用二次函数的好处就是偏导数非常容易求得.
对于其他的初等函数,他们的偏导数不难求得,简单的高数知识足够用.
既然我们让我们的参数沿着梯度去下降,那么一次性下降多少?
我们可以使用一个固定的步长,每次进行梯度下降 下降一个固定的值;
也可与动态变化 根据当前误差 和迭代次数 决定学习率等等;
总之 学习率是一个表述下降步长的值 我们将再之后的实验中慢慢探索这个值的意义:smile_cat:_
我们先尝试着用一个数字作为输入,一个数字作为 labels 的情况:
我选择了一个3次多项式去拟合一个函数:该函数以 sin(X) 作为基础,并加入一定的噪声:
float target_func(float x) { return sinf(x) + ((float)rand() / RAND_MAX) *0.1f - 0.05f;// }我们并不会告诉我们的多项式这是一个什么样的函数, 让多项式去自己学习这些参数去拟合这条曲线, 也可以不断改变这个函数看看多项式的参数会发生什么样的变化.
我们选择的多项式是这样的一个函数:
P(x,a0,a1,a2)=∑i=02aixi 其中 a0 到 a3 就是我们要学习的 parameters .显然, P 对每一个parameter的偏导数:
∂P∂ai=xi(i=0,1,2) 关于误差函数,我们选用一个最简单的二次误差函数: C(X)=12X2 综述我们的误差模型是:err=P(x,parameters)−labels;
loss=C(err)
我们要做的就是利用梯度下降算法不断调整 parameters 使得函数的结果 loss 最小.
这里误差对每个 parameters 的偏导数的计算方法:
∂C∂ai=∂C∂Err∂Err∂ai=∂C∂Err∂P∂ai=err˙xi 首先 声明一个保存参数的数组: float parameters[3];//a0 - a2 for (auto& b : parameters) b = ((float)rand() / RAND_MAX);//initialize parameters学习率我们选择0.9:
const float learning_rate = 0.9f;先随机的采样: 选择一个样本训练数据:
float x = ((float)rand() / RAND_MAX) - 0.5f; float labels = target_func(x);计算 xi 和多项式输出,把结果保存在exps和sum中:
float exps[3]; exps[0] = 1.0f;//x^0 float sum = parameters[0];//parameters[0] * exps[0] for (int j = 1; j < 3; j++) { exps[j] = exps[j - 1] * x; sum += parameters[j] * exps[j]; }计算误差:
float err = sum - labels;梯度下降:
for (int j = 0; j < 3; j++) { parameters[j] -= err * exps[j] * learning_rate; }这样就完成了一次迭代:
在经过1000次迭代之后,我得到了这样的结果:
parameters = {0.000f,0.977f,-0.002f};//Tylor:{0.0f,1.0f,0.0f}这个和泰勒公式告诉我们的结果相差很小.
如果我们试着把sin(x)变成cos(x)或者其他函数,一样可以得到一个比较不错的结果.
但是如果我们把多项式的次数调的很大,会发现误差实在是有点大 ,这是因为在梯度下降的时候,因为 x 的取值范围是[-1,1],x10次幂实在是太小: 整个数字下降的非常缓慢,导致需要很多次迭代才能给高次项得到一个很好的值,这属于一个典型的消失的梯度现象.
同样如果我们把学习率调的很小的情况下;会发现算法学习的速度非常缓慢,以至于也需要很多迭代才能得到比较好的值;不过我们把学习率调的很大,会发现最后结果都变成了NaN: 因为误差和下降的距离形成了正反馈,即误差越大,下降的距离太大,全微分方程里面的 ε 不在接近于0,导致误差变得更大.
上面的这个例子虽然没有什么卵用,但是至少演示了梯度下降的方法和一般机器学习的总步骤.
在这之后,就会看到真正的有点用的机器学习算法: 对minst数据集里面手写数字的识别;