目录
介绍
机器学习需要一定的数学基础,包括基础数学知识,和微积分、线性代数和概率论等相关知识。本文将尝试横跨这些领域,详细地讲解机器学习的数学基础。
需要了解的计算
四则运算
加减乘除,如果不知道这个的话还是翻翻小学课本吧。另外,乘号也可以用·表示,或在两个字母之间省略不写。除法一般写成分数的形式。
乘方
乘方就是将某个数自己乘自己多少次,记作,如:
等号后面共有b个a相乘
求和
假设有一个数据,共有N个样本,第i个样本可以表示为,则这些样本之和可以表示为:
具体可以参考这张图片:
函数
函数的定义很简单,这里给出它在初中的定义,够用即可。
一般地,在一个变化的过程中,如果有两个变量x与y,满足对于x的每一个取值,y都有唯一确定的值与之对应,则称x为自变量,y是x的函数。
注意这里边的“唯一确定”指的是与一个x对应的y只有一个。比如,如果当x=-2和x=2时y=1是可以的,但是如果x=2时y=1或2是不行的。
另外,x的取值范围称为定义域,y的取值范围称为值域。定义域和值域可以用区间表示:
- (a, b)表示a<x<b
- [a, b)表示a≤x<b
- (a, b]表示a<x≤b
- [a, b]表示a≤x≤b
函数可以通过解析式和图象表示:
- 解析式:形如y=f(x)的代数式就是函数的解析式,其中,f(x)表示对应关系,比如f(x)=2x.
- 图象:对于一个函数,如果把每个自变量和对应的函数分别设为点的横、纵坐标,则组成的图形成为函数的图象
图为一个函数的图象,可以看到它的定义域为一切实数,值域为y≥1
图为函数的图象,可以看到,它在直线y=1以下没有取值,因此,它的定义域为一切实数,值域为y≥1
通过函数图象,我们可以直观的看出函数的一些性质。
函数的单调性
如果对于在函数定义域内的一个区间内的任意两个数和,其中,都满足,则称函数单调递增,是增函数;如果都满足,则称函数单调递减,是减函数:
增函数
减函数
微积分
微积分,是建立在函数上的一个数学工具,微积分分为微分和积分,但是在介绍微分和积分之前,需要先介绍函数的极限。
函数的极限
我们现在换一个函数y=2x,它的图象如下:
我们假设让x无限接近于2,y就无限接近于4。因此我们说,当x趋于2时,y=2x的极限为4,记作:
或
在这里给出函数的极限的定义:
对于一个函数y=f(x),当x无限接近于m时,y无限接近于n,则称当x趋于m时,y的极限为n.记作:
或
有些人可能会说,这有什么用呢?我求极限直接让x等于m求y的值不就行了?其实,这里边最大的意义就是给出了“无限接近”、“趋近”的概念以及“lim”运算符。
无穷大
这里引用高中必修一的定义:
实数集R可以用区间表示为(-∞, +∞),“∞”读作“无穷大”,“-∞”读作负无穷大,“+∞”读作正无穷大
这就是说,对于任意实数r,都满足-∞<r<+∞。
另外∞既可以表示正无穷大,也可以表示负无穷大。
无穷小量
无穷小量是一个变量或函数,如果:
则称y是当x趋于m时的无穷小量。
注意,无穷小量必须有x趋于m的条件。
这里以举例:
可以发现,当x无限接近于∞时,y无限接近于0,因此:
即,当x趋于∞时,y为无穷小量。
阶的比较
很容易发现,不同的无穷小量趋近于0的速度有快有慢。因此两个无穷小量之间又分为高阶无穷小、低阶无穷小和同阶无穷小。
高阶无穷小和低阶无穷小
对于两个当x趋于x0时的无穷小量a和b,如果:
则称a是b的高阶无穷小,b是a的低阶无穷小。记作:
意味着a趋于0的速度更快。
同阶无穷小
对于两个当x趋于x0时的无穷小量a和b,如果:
则称a和b是x趋于x0的同阶无穷小。
特别的,如果c=1,则称a和b是x趋于x0的等价无穷小,记作:
意味着a与b趋于0的速度相同
微分
有了以上的概念,就可以研究微分了。考虑这个函数
我们让x增加(又叫x取得增量),计算一下y的改变量:
可以发现,y的改变量Δy为:
其中我们可以发现,Δx的平方是Δx的高阶无穷小,即:
由于其趋于0的速度更快,因此,当改变量非常微小时,它可以被忽略不计,所以函数y的改变量可以近似为
如果我们让式中的x=m,则 是函数y在点m处对应于自变量的改变量的微分。
微分的定义
接下来给出微分的定义:
对于定义在一个区间上的函数,且和在这个区间上,如果函数的增量可以表示为 ,其中A是与 无关的常数(可能与有关),则称函数在点处可微,且称 为函数y在点处对应于自变量的改变量的微分,记作dy。
因此我们可以得出:
且当很小时:
因此我们的例子可以表示为:
或者说:
另外,也可以看成是函数y=x的微分,且函数的增加量就等于自变量的增加量,因此我们可以说:
因此,我们对微分的定义式可以写成:
微分的运算法则
微分有以下运算法则:
(1)
(2)
(3)特别地,
(4)
基本初等函数的微分公式
由于我们没有讲对数等运算,这里边只介绍两个微分公式,其他的微分公式可以上网查到:
(1),其中C为常数
(2)
举例
我们计算一下前面提到的的微分:
导数
导数,又叫微商,是函数的微分与自变量的微分的商,即,因此又叫微商。
函数y=f(x)的导数可以记作、、或
导数可以表示函数的变化快慢,比如 的导数就是:
我们发现,导数是一个自变量为x的函数,因此,导数的全称是“导函数”。
我们把它的导数和函数画出来:
这是它的导数
这是函数图象
我们可以看到,导数在x=-1时穿过x轴,并变为负数;而函数在x=-1时从减函数变为增函数。另外,导数在不断变大,而函数在x=-1前减少的速度越来越慢,在x=-1后的增长的速度越来越快。因此,导数可以表示函数的变化快慢。
极值和极值点
对于一个函数y=f(x),如果在x=m附近有确定的值,且f(m)为x=m附近的最大值(或最小值),则这个最大值(或最小值)就是函数的极大值(或极小值),m为极大值点(或极小值点)。极大值和极小值统称极值,极大值点和极小值点统称极值点。
极大值
极小值
函数的极值一定在导数为0的点或不存在导数的点。但是,导数为0的点或不存在导数的点不一定是函数的极值:
函数的极值为0,但不可导
函数在x=0时导数为0,但不是极值
通过导数判断函数单调性
如果导数小于0,则函数单调递减;如果函数大于0,则函数单调递增。
链式求导
链式求导的公式非常简单:
具体来说,如果要求:
可以这样求:
积分
积分是微分的逆运算,可以根据微分的结果还原函数。 但是,由于对常数的微分恒等于0,因此,还原出来的函数可以加上任意常数。
如果函数y=f(x)的微分是A(A与x的取值有关),则称A的积分是函数y=f(x),记作:
一般地,因为还原出来的函数可以加上任意常数,所以:
其中C是任意常数
积分的运算法则
积分的运算法则与微分一样,只是要在结果后面加一个常数。
基本积分表
由于我们没有讲对数等运算,这里边只介绍两个积分公式,其他的微分公式可以上网查到:
(1)
(2)注意当a=-1时是存在积分的,但不是这么算的。
举例
计算y=6xdx+6dx的积分:
当C取4时,与我们前面例子的原函数相同。因此可以看出,积分是微分的逆运算。
积分的结果又叫积分曲线。
多元函数
有多个自变量的函数叫多元函数,多元函数可以表示为:
每多一个自变量,多元函数的图象就要多一条坐标轴与所有原坐标轴垂直:
图为f(x, y)=x^2+y^2的图象
多元函数的微分和导数必须要规定对谁求微分或导数,因此,多元函数的微分和导数叫偏微分和偏导数。多元函数求导是,可以把其他自变量看成常数。下面的运算过程展示了如何求对x的偏微分和偏导数:
它的偏导数图象如下:
可以看到,当x相同时,y的取值对函数值z得取值没有影响。因此,对某一个自变量求导,就说明其他自变量的取值对导函数没有影响。
这里附上画二元函数的python代码,注意需要使用python的数学表达式定义函数:
import numpy as np
import matplotlib.pyplot as plt
s = input('请输入二元函数:f(x, y)=')
# 定义二元函数
def f(x, y):
return eval(s)
# 生成x和y的取值范围
x = np.linspace(-10, 10, 400)
y = np.linspace(-10, 10, 400)
X, Y = np.meshgrid(x, y)
# 计算函数值
Z = f(X, Y)
# 绘制三维曲面图
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
ax.plot_surface(X, Y, Z, cmap='viridis')
ax.set_xlabel('X')
ax.set_ylabel('Y')
ax.set_zlabel('Z')
ax.set_title('Function Surface')
plt.show()
向量
向量,是既有大小又有方向的量,因此,可以使用带箭头的线段表示向量。向量没有箭头的一端称为起点,有箭头的一端称为终点:
向量的加减法遵循三角形法则和平行四边形法则:
向量与一个数的乘积就是将向量延长多少倍,称为向量的数乘:
当然,机器学习中讲的向量,通常要放在一个直角坐标系里:
图中向量a就是向量(x, y)
一般,向量的运算遵循以下规则:
另外,向量可以有多个数在括号里,如:
梯度
接下来可以介绍一些机器学习中的概念了,先来介绍以下梯度。
考虑一个多元函数:
它对每一个自变量都有一个偏导,如果把这些偏导写成向量:
那么就说,这个向量是函数r在点处的梯度。(当然,既然我们可以说点,那就也可以把多元函数的自变量看成一个向量。)
因此,梯度就是一个函数对所有自变量的导数组成的向量。
我们来求一下函数在点x处的梯度:
因此,原函数的梯度就是(2x)
梯度下降
梯度下降是一种找极值的办法:
观察函数:
可以看到,它的最小值就是它的极值。我们来看一下如何找到最小值。
首先可以发现,从(2, 4)点看,越靠近极值点,它的增长速度越慢,即导数越小。因此,我们只需要找出一种办法,使它的导数最小。梯度下降的原理就是这样。梯度下降的过程为:
- 在函数上取任意一点,并求出它的梯度
- 对梯度乘上学习率(学习率是我们定义的一个参数),使对应自变量减去得到的结果,并更新自变量。
- 循环执行上述操作,直到梯度的每个偏导数的绝对值都小于指定的值,或干脆重复执行指定次数。
我们以上述函数为例,取x=2,学习率为0.1:
- 求出x=2处的梯度:
- 对梯度乘上学习率: ,使对应的自变量减去得到的结果(注意这里把自变量看成了一个向量):,并更新自变量:令
- 求出x=1.6处的梯度:
- 对梯度乘上学习率: ,使对应的自变量减去得到的结果:,并更新自变量:令
- 求出x=1.28处的梯度...
我们可以利用这个方法,求出一个二次函数(形如的函数,其中只有x是自变量,y是函数,其他都是参数)或类似的函数的最值。当然,机器学习中一般都是求最小值。
以下代码展示了这个过程:
#include <stdio.h>
#include <math.h>
#define EPSILON 0.000001
#define MAX_ITER 10000
double f(double x) {
return x * x; // 定义二次函数
}
double df(double x) {
return 2 * x; // 定义二次函数的导数
}
void gradient_descent(double x) {
double alpha = 0.1; // 定义学习率
int iter = 0; // 定义迭代次数
double delta = EPSILON + 1; // 初始梯度值大于epsilon,使得可以进入循环
while (iter < MAX_ITER && delta > EPSILON) {
double dx = df(x); // 计算梯度值
double x_new = x - alpha * dx; // 更新x值
delta = fabs(x - x_new); // 计算梯度下降的幅度
x = x_new; // 更新x值
iter++; // 增加迭代次数
}
printf("函数极值附近的x值为:%f\n", x);
printf("函数极值为:%f\n", f(x));
}
int main() {
double x = 1.0; // 从x=1处开始梯度下降法
gradient_descent(x);
return 0;
}
输出结果:
函数极值附近的x值为:0.000004
函数极值为:0.000000
梯度下降的问题
有些时候,使用梯度下降法可能会导致NaN出现。我们稍加改动上述代码,将二次函数从改成:
double f(double x) {
return 100 * x * x; // 定义二次函数
}
double df(double x) {
return 200 * x; // 定义二次函数的导数
}
输出:
函数极值附近的x值为:-nan
函数极值为:-nan
可以发现,程序输出了nan。
遇到这种情况,只需要调低学习率即可。将学习率从0.1改成0.001,可以看到,输出正常:
函数极值附近的x值为:0.000004
函数极值为:0.000000
目标函数
传统的机器学习程序实现的效果是:给定一个输入,计算并返回输出。这个计算过程需要一个函数,这个函数就被称为目标函数。
我们可以考虑这样一个问题:一辆汽车以60公里每小时的速度行驶,t小时后它行驶了多少公里。在这个问题中,t是我们给定的输入(可以为任意数),计算机需要将t带入一个函数(目标函数),最终将函数值输出给我们,这样我们就知道了问题的答案。很容易发现,这个问题中,目标函数就是:
我们向机器输入t=3,那么它就会计算:
然后输出:
这样,机器就利用目标函数,完成了数据的计算。
但是,在大部分的问题中,程序员并不知道具体的目标函数,这就需要先设定目标函数的形式,然后对参数进行梯度下降,这个过程就称为训练:
比如我们定义目标函数的形式如下(k, b都是参数,不是自变量,需要在使用前定义)
但是,我们需要一个函数评估当前参数是否与实际匹配,这就需要引入损失函数
损失函数
假设我们有一堆数据,以及这些数据的预测值,我们就可以定义一个损失函数,评估预测值的准确性。一般,损失函数值越小,数据的损失越小,也就越准确。
最常用的损失函数是均方误差函数:
其中,N表示样本的个数,表示第i个样本的预测值(其实就是第i个样本作为自变量的目标函数值),表示第i个样本的真正的值。这个函数非常好理解,对于目标函数的任意参数a,易求出它的偏导(可以利用链式求导法快速求出它的偏导):
可以发现,式中的其实就是目标函数对它的任意参数a的偏导。也就是说,均方误差损失函数的梯度就是对目标函数梯度做了一些运算。设自变量的值为第i个样本时,目标函数的梯度为:
如果我们还是使用原来的目标函数:
将它的梯度求出:
设第1个样本为,则损失函数的梯度为:
然后我们随机设置参数的初始值,就可以对损失函数进行梯度下降了。
举个例子
假设我给出一个数据:
时间(t) 自变量 | 距离(y) 因变量 |
---|---|
0 | 4 |
1 | 64 |
2 | 124 |
3 | 184 |
然后我们来简单地写个程序,确定目标函数:
#include <iostream>
#include <vector>
#include <cmath>
using namespace std;
class LinearModel{
private:
int count; // 迭代次数
double w, b; // y=wx+b
double alpha; // 学习率
public:
LinearModel(int c=10000, double a=0.001):count(c),alpha(a){
w = b = 1;
}
void train(vector<double> X, vector<double> Y){
// 梯度下降
for(int i=0; i < count; i++){
double kw = 0; // 损失函数在点(w, b)处对w的偏导数
double kb = 0; // 损失函数在点(w, b)处对b的偏导数
int n = X.size();
// 计算两个偏导数
for(int i=0; i<n; i++){
kw += (predict(X[i])-Y[i])*X[i];
kb += (predict(X[i])-Y[i]);
}
kw*=2;
kb*=2;
// 根据梯度进行迭代
w = w - alpha * kw;
b = b - alpha * kb;
}
return;
}
double predict(double x){
return w*x+b;
}
vector<double> get_coefficients(void){
return vector<double>{w, b};
}
};
int main(void){
vector<double> X{0, 1, 2, 3};
vector<double> Y{4, 64, 124, 184};
LinearModel model(1000, 0.001);
model.train(X, Y);
cout << model.predict(4) << endl;
vector<double> coe = model.get_coefficients();
cout << "w = " << coe[0] << "\tb = " << coe[1] << endl;
return 0;
}
以下是程序的流程:
-
初始化:
- 创建一个
LinearModel
对象,设置默认的迭代次数count
为10000,学习率alpha
为0.001。初始化权重w
和偏置b
都为1。
- 创建一个
-
数据准备:
- 在
main
函数中,定义了输入特征X
和目标值Y
,分别包含四个数据点。
- 在
-
训练模型:
- 调用
train
函数,传入特征X
和目标值Y
进行训练。 - 在
train
函数中,使用梯度下降法进行迭代更新。- 初始化两个偏导数变量
kw
和kb
为0。 - 对于每个样本点,计算偏导数
- 根据偏导数更新权重和偏置
- 初始化两个偏导数变量
- 调用
-
预测:
- 在训练完成后,使用训练得到的权重和偏置进行预测。
-
输出结果:
- 输出预测的值为4时的结果。
- 输出权重和偏置的值。
-
结束:返回0表示程序正常结束。
输出:
242.371
w = 59.1263 b = 5.86537
可以看到,它较好的完成了任务