重要说明:本篇博客大多数参考Dodo大牛的博客,这位大牛写的很具体,通俗易懂,代码几乎每行都有注释,本系列博客跟随大牛的脚步,在其基础上记录小白学习统计学方法的过程,心得及理解。大牛的博客链接为(http://www.pkudodo.com),隆重推荐! 同时也参考了另外一篇博客,出处是(https://www.cnblogs.com/huangyc/p/9706575.html)在Markdown中输入数学公式(MathJax),参考博客是(https://www.jianshu.com/p/a0aa94ef8ab2)若文章有错误,希望各位不吝赐教!我会及时修正!
目录
感知机的定义
感知机是机器学习应用中分类的最简单的一种算法。如下图所示:感知机划分超平面
感知机是二分类的线性模型,输入是实例的特征向量,输出是实例的类别;假设训练的数据集是线性可分的,感知机的目标就是求一个能够将训练集的正负样本完全正确分离开的超平面(也就是上图中所示的那些将蓝、黄数据点完全分离开的直线)。但是如果这些数据是非线性可分的,这个超平面是无法获取的。上图的坐标轴,横坐标为
X
1
X_1
X1,纵坐标为
X
2
X_2
X2。图中的每一个点都由
(
X
1
,
X
2
)
(X_1,X_2)
(X1,X2)所决定。举个实例:有一批零件,判断零件是否合格有两个重要点,长度和重量。
X
1
X_1
X1表示长度,
X
2
X_2
X2表示重量,上图的两条黑线表示零件的长度均值和重量均值。只有当长度和重量都满足一定条件,该零件才为合格品。都不满足一定条件,视为不可修复的劣质品,直接丢弃。那么机器学习如何学习到这个规则呢?我们在代码实现的时候,拿到手的是所有样本的信息
(
X
1
,
X
2
)
(X_1,X_2)
(X1,X2)和标签
(
0
或
1
)
(0或1)
(0或1),标签里面0表示不合格品,1表示合格品。简单的说就是图片上黄色和蓝色的点。根据我们手上的这些点,我们需要找到一条直线将上面的点完美的分开。这样的直线我们可以找到很多条,那么哪一条才是最好的呢?实际上,感知机只是一个二分类的问题,无法找到一条最佳的直线,只需要能把所有的点都分开就好。我们设定损失函数为所有分错的点和直线的距离求和,然后训练,使这段求和的数值最小(最优的情况是0,因为0代表完全分开了),那么这条直线就满足我们的条件,就是我们所找的。
感知机的数学原理
首先,点
P
(
x
0
,
y
0
)
P(x_0,y_0)
P(x0,y0) 到直线
A
x
+
B
y
+
C
=
0
Ax+By+C=0
Ax+By+C=0 的距离为:
类似的:设超平面公式为
h
=
w
x
+
b
h=wx+b
h=wx+b,其中
w
=
(
w
0
,
w
1
,
w
2
,
.
.
.
w
n
)
,
x
=
(
x
0
,
x
1
,
x
2
,
.
.
.
x
n
)
w=(w_0,w_1,w_2,...w_n),x=(x_0,x_1,x_2,...x_n)
w=(w0,w1,w2,...wn),x=(x0,x1,x2,...xn),其中样本点
x
"
x^"
x"到超平面的距离为:
那么这个超平面为什么设置为
w
x
+
b
wx+b
wx+b 呢?它和我们常见的
a
x
+
b
ax+b
ax+b 有什么区别呢?
本质没啥区别,
a
x
+
b
ax+b
ax+b是二维中的,
w
x
+
b
wx+b
wx+b是高维中的。就看你的理解啦,简单的来说,wx+b是一个n维空间中的超平面S,其中w是超平面的法向量,b是超平面的截距,这个超平面将特征空间划分成两部分,位于两部分的点分别被分为正负两类,所以,超平面S称为分离超平面。其中
w
=
(
w
0
,
w
1
,
w
2
,
.
.
.
w
n
)
,
x
=
(
x
0
,
x
1
,
x
2
,
.
.
.
x
n
)
w=(w_0,w_1,w_2,...w_n),x=(x_0,x_1,x_2,...x_n)
w=(w0,w1,w2,...wn),x=(x0,x1,x2,...xn)
细节:
w是超平面的法向量:对于一个平面来说w就是这么定义的。数学上就这么定义的。很简单的哦!
b是超平面的截距:可以按照二维中的ax+b理解
特征空间:也就是整个n维空间,样本的每个属性都叫一个特征,特征空间的意思就是在这个空间中可以找到样本所有的属性组合。
感知机的模型
感知机的模型:输入空间—>输出空间:
sign函数很简单,当x大于等于0,sign输出1,否则输出-1。那么往前想一下,
w
x
+
b
wx+b
wx+b 如果大于等于0,
f
(
x
)
f(x)
f(x)就等于1,反之
f
(
x
)
f(x)
f(x)等于-1。
感知机的损失函数
我们定义样本
(
x
i
,
y
i
)
(x_i,y_i)
(xi,yi),如果上面的距离
d
>
0
d>0
d>0,则
y
i
=
1
y_i=1
yi=1;如果
d
<
0
d<0
d<0,则
y
i
=
−
1
y_i=-1
yi=−1,这样取
y
y
y 有一个好处,就是方便定义损失函数。优化的目标:期望使误分类的所有样本,到超平面的距离之和最小。
所以定义损失函数为:
其中M集合就是误分类点的集合。
不考虑前面系数,感知机模型的损失函数为:
那么问题来了,为啥可以不考虑那么“归一化”的前缀呢?
下面贴出一个解释,个人觉得很贴切:
感知机学习算法
感知机学习算法是对于上述损失函数进行极小化,求得
w
和
b
w和b
w和b。这里使用随机梯度下降法(SGD),因为误分类的M集合里面的样本才能参加损失函数的优化。
目标函数如下:
感知机学习算法有两种,一种是原始形式,一种是对偶形式,下面分别介绍:
原始形式算法
对偶形式算法
原始形式与对偶形式的选择
1.在向量维数(特征数)过高时,计算内积非常耗时,应选择对偶形式算法加速。
2.在向量个数(样本数)过多时,每次计算累计和就没有必要,应选择原始算法。
代码块阅读
代码块:这一块我还是隆重推荐Dodo大牛,理由嘛!很简单,因为我的代码都是跟着他后面一个一个码的!稍微改改!特此感谢大牛!大牛的博客链接为(http://www.pkudodo.com)!
# coding=utf-8
# Author:Dodo
# Date:2018-11-15
# Email:lvtengchao@pku.edu.cn
'''
数据集:Mnist
训练集数量:60000
测试集数量:10000
------------------------------
运行结果:
正确率:81.72%(二分类)
运行时长:68.1s
'''
import numpy as np
import time
def loadData(fileName): # 这边加载数据
'''
加载Mnist数据集
:param fileName:要加载的数据集路径
:return: list形式的数据集及标记
'''
print('start to read data')
# 存放数据及标记的list
dataArr = []
labelArr = []
# 打开文件
fr = open(fileName, 'r')
# 将文件按行读取
for line in fr.readlines():
# 对每一行数据按切割福','进行切割,返回字段列表
curLine = line.strip().split(',')
# Mnsit有0-9是个标记,由于是二分类任务,所以将>=5的作为1,<5为-1
if int(curLine[0]) >= 5:
labelArr.append(1) # 标签的列表
else:
labelArr.append(-1)
# 存放标记
# [int(num) for num in curLine[1:]] -> 遍历每一行中除了以第一哥元素(标记)外将所有元素转换成int类型
# [int(num)/255 for num in curLine[1:]] -> 将所有数据除255归一化(非必须步骤,可以不归一化)
dataArr.append([int(num)/255 for num in curLine[1:]]) # 将里面的数据进行归一化,1:1到最后
# 返回data和label
return dataArr, labelArr # 返回归一化的数据 以及对应的标签
def perceptron(dataArr, labelArr, iter=50):
'''
感知器训练过程
:param dataArr:训练集的数据 (list)
:param labelArr: 训练集的标签(list)
:param iter: 迭代次数,默认50
:return: 训练好的w和b
'''
print('start to trans')
# 将数据转换成矩阵形式(在机器学习中因为通常都是向量的运算,转换称矩阵形式方便运算)
# 转换后的数据中每一个样本的向量都是横向的
dataMat = np.mat(dataArr) # (60000, 784)
# 将标签转换成矩阵,之后转置(.T为转置)。
# 转置是因为在运算中需要单独取label中的某一个元素,如果是1xN的矩阵的话,无法用label[i]的方式读取
# 对于只有1xN的label可以不转换成矩阵,直接label[i]即可,这里转换是为了格式上的统一
labelMat = np.mat(labelArr).T
# 获取数据矩阵的大小,为m*n
m, n = np.shape(dataMat) # m:6000 n:784
# 创建初始权重w,初始值全为0。
# np.shape(dataMat)的返回值为m,n -> np.shape(dataMat)[1])的值即为n,与
# 样本长度保持一致
w = np.zeros((1, np.shape(dataMat)[1])) # (1,784)
# 初始化偏置b为0
b = 0
# 初始化步长,也就是梯度下降过程中的n,控制梯度下降速率
h = 0.0001
# 进行iter次迭代计算
for k in range(iter):
# 对于每一个样本进行梯度下降
# 李航书中在2.3.1开头部分使用的梯度下降,是全部样本都算一遍以后,统一进行一次梯度下降
# 在2.3.1的后半部分可以看到(例如公式2.6 2.7),求和符号没有了,此时用的是随机梯度下降,即计算一个样本
# 就针对该样本进行一次梯度下降。
# 两者的差异各有千秋,但较为常用的是随机梯度下降。
for i in range(m):
# 获取当前样本的向量
xi = dataMat[i]
# 获取当前样本所对应的标签
yi = labelMat[i]
# 判断是否是误分类样本
# 误分类样本特诊为: -yi(w*xi+b)>=0,详细可参考书中2.2.2小节
# 在书的公式中写的是>0,实际上如果=0,说明改点在超平面上,也是不正确的
if -1 * yi * (w * xi.T + b) >= 0:
# 对于误分类样本,进行梯度下降,更新w和b
w = w + h * yi * xi # 求导的梯度
b = b + h * yi
# 打印训练进度
print('Round %d:%d training' % (k, iter))
# 返回训练完的w、b
return w, b
def test(dataArr, labelArr, w, b):
'''
测试准确率
:param dataArr:测试集
:param labelArr: 测试集标签
:param w: 训练获得的权重w
:param b: 训练获得的偏置b
:return: 正确率
'''
print('start to test')
# 将数据集转换为矩阵形式方便运算
dataMat = np.mat(dataArr)
# 将label转换为矩阵并转置,详细信息参考上文perceptron中
# 对于这部分的解说
labelMat = np.mat(labelArr).T
# 获取测试数据集矩阵的大小
m, n = np.shape(dataMat)
# 错误样本数计数
errorCnt = 0
# 遍历所有测试样本
for i in range(m):
# 获得单个样本向量
xi = dataMat[i]
# 获得该样本标记
yi = labelMat[i]
# 获得运算结果
result = -1 * yi * (w * xi.T + b)
# 如果-yi(w*xi+b)>=0,说明该样本被误分类,错误样本数加一
if result >= 0:
errorCnt += 1
# 正确率 = 1 - (样本分类错误数 / 样本总数)
accruRate = 1 - (errorCnt / m)
# 返回正确率
return accruRate
if __name__ == '__main__':
# 获取当前时间
# 在文末同样获取当前时间,两时间差即为程序运行时间
start = time.time()
# 获取训练集及标签
trainData, trainLabel = loadData('../Mnist/mnist_train.csv')
# 获取测试集及标签
testData, testLabel = loadData('../Mnist/mnist_test.csv')
# 训练获得权重
w, b = perceptron(trainData, trainLabel, iter=30)
# 进行测试,获得正确率
accruRate = test(testData, testLabel, w, b)
# 获取当前时间,作为结束时间
end = time.time()
# 显示正确率
print('accuracy rate is:', accruRate)
# 显示用时时长
print('time span:', end - start)