前言
随着17年阿尔法狗(AlphaGo)击败人类职业围棋选手、战胜围棋世界冠军,AI、人工智能等词汇也成为了时下人们追求的一个潮流,各种相关产业和人工智能为主题的创业公司也如雨后春笋般相继涌现,因此人工智能也成为了2017年的关键词。关于人工智能的概念从计算机诞生之初就已经有了,1936年艾伦·图灵(Alan Turing)提出了著名的 “图灵机”(Turing Machine)的设想,在十多年后,其为了明确机器是否具备智能更是提出了著名的图灵测试。因此在过去的半个多世纪,人工智能其实不算什么新鲜的话题,甚至许多好莱坞大片都不吝以此概念作为噱头,比如黑客帝国、人工智能、机械姬等(PS:绝没有推荐电影的意思)。但是处在21世纪的我们听到这些概念时仍然会不自觉的感到兴奋,因为这是第一次科技的力量让我们感受到科幻与现实离我们是如此的接近,也正是在这个信息高度聚合与高速传播的时代,使我们大部分人都能够参与到其中,去实现每个人心中的科幻梦。
注意事项
技能要求
接下来的知识不会涉及到高深的数学知识,对编程的要求也是极低,并且所有涉及到的知识点都会用通俗的语言进行解释,所以不要担心数学不够好,不会编程等问题。当然,便于理解本文,如果你数学基础足够好那是那是有好处的。同时关键部分将会给出某些重要信息的解释或是数学名词的解释链接,在阅读本文过程中适当的去理解其中的某些概念是有必要的,必要的时候还请停下阅读进度确定自己已经理解。最后希望在看完这篇文章后能够让你产生对数学和机器学习的浓厚兴趣。所以Come on!请花15分钟认真看完!请花15分钟认真看完!请花15分钟认真看完!
内容概述
接下来我们将从机器学习中最基础的模型感知机(Perceptron)出发去探索机器学习的奥秘,在这儿我们可以不用理解它是什么意思,也不用去查各种资料使自己迷失在知识的海洋里,花15分钟时间仔细读完本文,你将了解人工智能现在真正的样子,同时你也将正式开始入门人工智能这个领域。人类在统计与概率的基础上建立起了人工智能这么一套体系,在今天这么个日新月异的世界,也是越加的蓬勃发展。虽然人工智能的内容涵盖很广,但是其主线脉络却是明确的,从发现问题,分析问题再到解决问题,在发展出的那一套基础框架下,人工智能在众多的领域遍地开花,产生了许许多多应用于不同领域的新奇想法。因此我们在追逐学科前沿,面多众多令人眼花缭乱的模型或算法时,夯实自己的基础,提升自己发现问题并解决问题的能力显得更加总要。
机器学习的核心
机器学习已经成为一门完整的学科,在学科建设的基础上,已经出现了针对机器学习领域标准的研究方法和技巧。下图展示了机器学习的三要素,也是其核心内容。
从分类问题开始
分类在我们日常生活中很常见,商品分类,垃圾分类,食物分类……这些分类的场景在我们的生活中都是司空见惯的。正因为司空见惯,所以就让人感觉分类是理所当然的事,其过程也没有任何难度。但这简单的事儿对机器而言却并不是那么简单。如果要让机器来完成各种分类它能不能完成呢?答案是能,只要我们为特定的机器设定特定的规则使它能循环运转起来就行,而这儿那个规则就是分类的关键。比如将商场卖的橘子分成两类,一类长得好看的为橘子君A,一类长得难看的为橘子君B,这儿有一个规则Rule,能够判断一个橘子是好看还是不好看,那么将Rule告诉这个机器岂不是完美?以后再也不用担心吃不到好看的橘子了。
好吧跑偏了,通过上面举的的例子其实就能够看到人工智能研究中的一个重要步骤——选择模型。橘子分类中的Rule就是需要选择的模型,有了模型,有了Rule,机器才能知道它拿到一个橘子应该怎么做。那么现在有了模型还要干什么?如果那模型是我们从一个有限的样本空间中通过严格的推导得到的,并且我们也只将该模型应用于该有限样本空间,那么就没有什么事儿需要做了,但实际上这在现实中是不可能的,因为我既然从那一堆橘子推导出了一个模型,那那堆橘子就肯定已经被我们全都分好类了,那这个模型拿来还有什么意义?虽然从上面的角度我们似乎做了无用功,但统计学的知识却给了我们启发。那就是,我们推导出来的模型除了应用于我们用于推导模型所用的那堆橘子外,很大概率还能应用于那些还没有被分类的橘子。从概率论与统计学的观点上看,这是正确的。因为我们以橘子的美丑对橘子进行分类,而美和丑是那些橘子所共有的一些特征。就好比人,每个人都长着五官,在一小群人中我们以一个标准区分其中的美丑,那么将这个标准应用于全体人当中也是可行的,但也可能出现我们的标准无法区分的情况发生。
好了,到这儿有没有想到些什么?由概率论的知识我们能够想到,从部分数据中寻找其中我们关注的共性特征对其进行识别分类,那么这些共性特征也能够帮助我们识别分类那些未知的数据,这就是基于概率与统计学习的机器学习的核心原理与思想。看吧,其实很多感觉高深的知识就是我们身边随处可见的问题。有了这个思想有没有对机器学习的概念有一个初步的了解呢?机器从我们给的数据学习一个能够正确解决问题的方法,我们再用那个方法去应用到我们的实际问题中去,而那个方法就是上面多次提到的模型。
在二维平面中怎么分类?
读到这儿的朋友应该对机器学习还有较深的困惑,但这不要紧,现在需要你发挥一定的想象能力。同时如果手边有笔和纸也请拿起笔跟我一起画一个二维坐标系,这个坐标系是一个二维空间,在这个空间中分布着无数点它们都是这个二维世界的原住民,现在你是这个世界的上帝,你随意在这个空间中画了一条看不见直线,里面的人被你分成了两类A和B,分别位于直线两侧。下图展示了在平面上的一条直线
有一天,这个二维世界中穿越来了一个从三维世界来的人C,小C遇见了很多这个世界的人,他发现了这个世界上的人被分成了A,B两类。并且这些被分成两类的人类别存在较明显的位置特征,于是小C想了一个办法来描述这个世界上被分成两类的人,即假设这个世界的人可以用一条直线来划分。小C想到的办法如下:
通过以上计算方法,能够将所有二维世界中的原住民分成两类,也就是以每个原住民的坐标为参数,通过模型计算结果为1的为一类,为-1的为另一类,So easy有木有!到目前为止小C离成功猜到你是怎么给这个二维世界的原住民分类的又近了一步。小C还需要什么?它需要知道
- 模型运算结果与实际结果相符,不做任何额外操作,继续输入新的数据
- 模型运算结果与实际结果不符(误分类),调整
w,b 的值,使得其朝真相的方向逐步靠近
到这儿我想大家应该知道需要做什么了,那就是找到一个方法来调整
在这里不加证明地给出这个公式,如果有兴趣自己推导的同学可参考 点到平面距离公式的七种推导方法探讨,这里需要解释的一点是因为我们的模型有两个变量
有了计算距离的方式,下面我们来看看损失函数究竟怎么定义。由于对于模型来说,在分类错误的情况下,若
如此得到最终的损失函数为:
正如上面所示,
根据梯度下降所描述的方法,我们只需要在每次出现误分类时按如下方法更新
以上更新方法就是每次出现误分类时
到这儿是不是一切变得豁然开朗?小C在你所创造的二维世界中已经找到了方法来得到你对其中的二维原住民分类的方式,只要小C在那个世界发现足够多的原住民,每当找到一个原住民就用他的模型对其分类,只要分类结果与实际不符时,就用上面的方法更新模型,那么小C将得到一个无限接近你对二维世界原住民分类的模型。
让模型运转起来
上面我们已经确定了给二维世界原住民分类的方案,并且知道了怎么来使得随着数据的输入让模型变得越来越接近真实情况。而上面描述的模型还有一个高大上的名字叫感知机模型。是不是格调瞬间就上来了?那么我们来看看怎么用编程语言实现这个计算过程。
Python实现
Python具有很方便的数值计算库和简单的语法,因此我们用Python实现感知机模型试试看.
from random import randint
import numpy as np
import matplotlib.pyplot as plt
class TrainDataLoader:
def __init__(self):
pass
def GenerateRandomData(self, count, gradient, offset):
x1 = np.linspace(1, 5, count)
x2 = gradient*x1 + np.random.randint(-10,10,*x1.shape)+offset
dataset = []
y = []
for i in range(*x1.shape):
dataset.append([x1[i], x2[i]])
real_value = gradient*x1[i]+offset
if real_value > x2[i]:
y.append(-1)
else:
y.append(1)
return x1,x2,np.mat(y),np.mat(dataset)
class SimplePerceptron:
def __init__(self, train_data = [], real_result = [], eta = 1):
self.w = np.zeros([1, len(train_data.T)], int)
self.b = 0
self.eta = eta
self.train_data = train_data
self.real_result = real_result
def nomalize(self, x):
if x > 0 :
return 1
else :
return -1
def model(self, x):
# Here are matrix dot multiply get one value
y = np.dot(x, self.w.T) + self.b
# Use sign to nomalize the result
predict_v = self.nomalize(y)
return predict_v, y
def update(self, x, y):
# w = w + n*y_i*x_i
self.w = self.w + self.eta*y*x
# b = b + n*y_i
self.b = self.b + self.eta*y
def loss(slef, fx, y):
return fx.astype(int)*y
def train(self, count):
update_count = 0
while count > 0:
# count--
count = count - 1
if len(self.train_data) <= 0:
print("exception exit")
break
# random select one train data
index = randint(0,len(self.train_data)-1)
x = self.train_data[index]
y = self.real_result.T[index]
# wx+b
predict_v, linear_y_v = self.model(x)
# y_i*(wx+b) > 0, the classify is correct, else it's error
if self.loss(y, linear_y_v) > 0:
continue
update_count = update_count + 1
self.update(x, y)
print("update count: ", update_count)
pass
def verify(self, verify_data, verify_result):
size = len(verify_data)
failed_count = 0
if size <= 0:
pass
for i in range(size):
x = verify_data[i]
y = verify_result.T[i]
if self.loss(y, self.model(x)[1]) > 0:
continue
failed_count = failed_count + 1
success_rate = (1.0 - (float(failed_count)/size))*100
print("Success Rate: ", success_rate, "%")
print("All input: ", size, " failed_count: ", failed_count)
def predict(self, predict_data):
size = len(predict_data)
result = []
if size <= 0:
pass
for i in range(size):
x = verify_data[i]
y = verify_result.T[i]
result.append(self.model(x)[0])
return result
if __name__ == "__main__":
# Init some parameters
gradient = 2
offset = 10
point_num = 1000
train_num = 50000
loader = TrainDataLoader()
x, y, result, train_data = loader.GenerateRandomData(point_num, gradient, offset)
x_t, y_t, test_real_result, test_data = loader.GenerateRandomData(100, gradient, offset)
# First training
perceptron = SimplePerceptron(train_data, result)
perceptron.train(train_num)
perceptron.verify(test_data, test_real_result)
print("T1: w:", perceptron.w," b:", perceptron.b)
# Draw the figure
# 1. draw the (x,y) points
plt.plot(x, y, "*", color='gray')
plt.plot(x_t, y_t, "+")
# 2. draw y=gradient*x+offset line
plt.plot(x,x.dot(gradient)+offset, color="red")
# 3. draw the line w_1*x_1 + w_2*x_2 + b = 0
plt.plot(x, -(x.dot(float(perceptron.w.T[0]))+float(perceptron.b))/float(perceptron.w.T[1])
, color='green')
plt.show()
如下是由以上代码实现的模型分类结果图,其中红色直线为实际的分类模型,绿色直线为通过训练数据训练后得到的模型,灰色’*’符号组成的点集为训练数据集,蓝色的’+’号组成的点集为验证数据集:
感知机就这样?
看了以上的内容很多人可能感觉机器学习也不过如此!那么首先恭喜你,有这感觉证明对于机器学习你开始入门了,但是还有更多的东西在等着你。这儿有几个疑问步骤你有没有想过:
- 模型是怎么确定的,为什么就能想到用感知机这样的模型呢?
- 损失函数就只能靠那种思路得到吗?还有没有更好的方式?
- 损失函数都是求最小值吗,有没有求最大值的情形,最小/最大值真的能求出来吗?
- 求最小/最大值的方法还有什么?我们能不能换其它方法来替换随机梯度下降/上升?
我相信,读完整篇文章这些疑问应该是自然产生的,其中的这些问题希望大家自己能够随着学习的深入找到答案。
感知机的对偶形式变形
“对偶”一词听着挺奇怪的,但可以将其理解为形式不一样但结果相等的意思。如下就是感知机模型的一种对偶形式:
从上式可以看出与前面的模型相比仅替换了
由此我们假设总共有N个点被错误分类,
那么自然的,我们令
经过如上变换后,每次训练迭代更新就需要更新
同时观察模型可以发现
因此我们可以提前计算出Gram矩阵用于后面直接通过查询Gram矩阵知道
感知机对偶形式实现
# Init the parameter
from random import randint
import numpy as np
import matplotlib.pyplot as plt
class TrainDataLoader:
def __init__(self):
pass
def GenerateRandomData(self, count, gradient, offset):
x1 = np.linspace(1, 5, count)
x2 = gradient*x1 + np.random.randint(-10,10,*x1.shape)+offset
dataset = []
y = []
for i in range(*x1.shape):
dataset.append([x1[i], x2[i]])
real_value = gradient*x1[i]+offset
if real_value > x2[i]:
y.append(-1)
else:
y.append(1)
return x1,x2,np.mat(y),np.mat(dataset)
class SimplePerceptron:
def __init__(self, train_data = [], real_result = [], eta = 1):
self.alpha = np.zeros([train_data.shape[0], 1], int)
self.w = np.zeros([1, train_data.shape[1]], int)
self.b = 0
self.eta = eta
self.train_data = train_data
self.real_result = real_result
self.gram = np.matmul(train_data[0:train_data.shape[0]], train_data[0:train_data.shape[0]].T)
def nomalize(self, x):
if x > 0 :
return 1
else :
return -1
def train_model(self, index):
temp = 0
y = self.real_result.T
# Here are matrix dot multiply get one value
for i in range(len(self.alpha)):
alpha = self.alpha[i]
if alpha == 0:
continue
gram_value = self.gram[index].T[i]
temp = temp + alpha*y[i]*gram_value
y = temp + self.b
# Use sign to nomalize the result
predict_v = self.nomalize(y)
return predict_v, y
def verify_model(self, x):
# Here are matrix dot multiply get one value
y = np.dot(x, self.w.T) + self.b
# Use sign to nomalize the result
predict_v = self.nomalize(y)
return predict_v, y
def update(self, index, x, y):
# alpha = alpha + 1
self.alpha[index] = self.alpha[index] + 1
# b = b + n*y_i
self.b = self.b + self.eta*y
def loss(slef, fx, y):
return fx.astype(int)*y
def train(self, count):
update_count = 0
train_data_num = self.train_data.shape[0]
print("train_data:", self.train_data)
print("Gram:",self.gram)
while count > 0:
# count--
count = count - 1
if train_data_num <= 0:
print("exception exit")
break
# random select one train data
index = randint(0, train_data_num-1)
if index >= train_data_num:
print("exceptrion get the index")
break;
x = self.train_data[index]
y = self.real_result.T[index]
# w = \sum_{i=1}^{N}\alpha_iy_iGram[i]
# wx+b
predict_v, linear_y_v = self.train_model(index)
# y_i*(wx+b) > 0, the classify is correct, else it's error
if self.loss(y, linear_y_v) > 0:
continue
update_count = update_count + 1
self.update(index, x, y)
for i in range(len(self.alpha)):
x = self.train_data[i]
y = self.real_result.T[i]
self.w = self.w + float(self.alpha[i])*x*float(y)
print("update count: ", update_count)
pass
def verify(self, verify_data, verify_result):
size = len(verify_data)
failed_count = 0
if size <= 0:
pass
for i in range(size-1):
x = verify_data[i]
y = verify_result.T[i]
if self.loss(y, self.verify_model(x)[1]) > 0:
continue
failed_count = failed_count + 1
success_rate = (1.0 - (float(failed_count)/size))*100
print("Success Rate: ", success_rate, "%")
print("All input: ", size, " failed_count: ", failed_count)
def predict(self, predict_data):
size = len(predict_data)
result = []
if size <= 0:
pass
for i in range(size):
x = verify_data[i]
y = verify_result.T[i]
result.append(self.model(x)[0])
return result
if __name__ == "__main__":
# Init some parameters
gradient = 2
offset = 10
point_num = 1000
train_num = 1000
loader = TrainDataLoader()
x, y, result, train_data = loader.GenerateRandomData(point_num, gradient, offset)
x_t, y_t, test_real_result, test_data = loader.GenerateRandomData(100, gradient, offset)
# train_data = np.mat([[3,3],[4,3],[1,1]])
# First training
perceptron = SimplePerceptron(train_data, result)
perceptron.train(train_num)
perceptron.verify(test_data, test_real_result)
print("T1: w:", perceptron.w," b:", perceptron.b)
# Draw the figure
# 1. draw the (x,y) points
plt.plot(x, y, "*", color='gray')
plt.plot(x_t, y_t, "+")
# 2. draw y=gradient*x+offset line
plt.plot(x,x.dot(gradient)+offset, color="red")
# 3. draw the line w_1*x_1 + w_2*x_2 + b = 0
plt.plot(x, -(x.dot(float(perceptron.w.T[0]))+float(perceptron.b))/float(perceptron.w.T[1])
, color='green')
plt.show()
下图为以1000组数据训练,100组数据做验证的结果图,绿色直线为训练得到的模型。
感知机的限制与推广
感知机是什么?就是如上面所讲述的那种模型定义,而感知机有一个非常明显的特征——它是线性的。这儿先来各出一个结论:线性模型不可分类异或问题。到这儿很多人可能会糊涂了,异或问题是什么鬼?不能把话讲明白吗?这儿给出一个直观的例子,还是以前面描述的二维世界为例。我们知道二维世界的每个人都具有一个标签,就像身份证一样,那就是它们的坐标。假设你给它们分类的时候不是直接在里面画一条直线,直线一侧的是一类,另一侧的是另一类;而是以它们的
现在我们知道用感知机来解决分类问题是有限制的,也就是不能解决线性不可分问题,因此在应用感知机模型之前需要判断该问题是否是线性不可分的,至于应该如何具体的去判断?这个将留到之后的章节去讨论。读完前面我们已经掌握了怎么用感知机讲一个东西分成两类了,但是在现实当中讲一个东西分成两类的场景太少也太简单了。真正有需求的是将事物分成多类,如果感知机模型具有这样的功能它在现实中才具有更多的价值,我们也才有学习它的意义。因此我们需要放飞我们的思维,来直观上看看怎么将事物分成N类.
多维空间的多分类问题
在这我们来进行一次逻辑推导.以感知机为例,假设
即
那么我们可以得到一个关于平面的函数
到这儿大家我想大家就能够想像在三维空间中的点集分类了,三维空间中的一个线性点集表示为
一直到四维空间,五维空间甚至N维空间。从上面我们发现,每多一维,
上面的公式的表达是不是感觉很复杂很乱?我们来对其进行整理,由向量的点积性质
看到没,化简后就得到了我们在文章开头所见到的那种形式,这也是获得感知机模型的一个自然推导的过程,而在这儿
说到这儿我们了解了分类在高维空间中的表现形式,但是却还没有提到多分类的事儿,不知道读到这里的朋友有没有自己想到多分类应该怎么做呢?道理很简单,我们看到以上的所有分类例子都是用
像上面那样,岂不就实现了将点分成三类?分类从线性代数的概念来理解其实是一个映射的问题,前面我们提到的二分类问题的映射表示如下:
因此多分类的一般表示就应该如下:
其实经过前面将
进行如上扩展后我们就能够将N维特征向量空间直接映射到M维空间,通俗点讲就是将
好了根据这种矩阵表示我们应该也能够得到分类的一种直观表示,如图:
上图中有四类点,我们要将其各自分开就直接化多条直线就行。到这儿推导先告一段落,我们接下来看看多分类具体是怎么实现与应用的。
感知机如何应用在实际中
介绍与推导
相信到这你已经对感知机了解了,是不是产生了世界尽在我手的感觉?但也可能有可能会打击你,对你说:”你给我用感知机做个MNIST图片分类看看?”如果你真的是一个刚入门的人,经过短暂思考后可能会陷入迷茫,貌似感知机学是学了,但完全没法用来做事啊!别急,接下来让我们一起来分析MNIST分类究竟该怎么用感知机模型实现。
MNIST是什么很简单,网上资料也一大堆,总的来说它就是一堆由人手写的阿拉伯数字的图片。所谓的MNIST分类就是识别出某张图片上写的数字是几。但是怎么做呢?在这儿有一个关于MNIST的图片的信息,那就是其中的每一张图片的大小都是
回到正题,图片的特征值有784个,且我们需要将图片分成10类,则我们的模型应该是这样的:
好了模型确定了,还需要做什么?回顾前面对二维世界的原住民分类问题,接下来应该是思考怎么来判断模型的好坏。由上式可知,在785维空间中,超平面
以上结果是根据之前感知机的推导过程自然推导得到,如果有不理解的可以再返回去看看感知机的推导过程,这儿就不在赘述了,其中唯一的区别就是这儿使用矩阵直接将更新方式表示了出来,同时涉及的消绝对值符号的地方是构造了一个辅助函数sign(i,x),其意义同前面的sign(x),只是分别将其应用与每个超平面上。到这一步,我们就可以以此写出代码了。
实现
讨论
上面我们看到了,运用感知机再稍微进行一点原始且直观的扩展就能实现对图片的分类,那还可以怎么做呢?对于分类的问题在机器学习领域已经很成熟,因此也产生了许多应用于各种场景,用于解决各种不同分类问题的有效算法。而那些算法有人也直接将其称作分类器。因此,在这儿对MNIST进行分类只是进行了简单粗暴的运用了感知机的思想,真正工程中所有考虑的问题将更加复杂,比如还得考虑过拟合问题,算法是否能有效收敛,损失函数的惩值是否合理等等。因此在这留下几个问题:
- 特征空间应该怎么获得与选取?
- 损失函数的确定还需要注意什么?
- 如何用机器学习算法分类手写字母?
- 如何用机器学习算法分类音频?
结束语
通过感知机模型我们看到了机器学习的过程,当然感知机是非常非常非常简单的机器学习模型,它能够处理的问题也是非常有限。但这并不能妨碍我们了解机器学习是怎样一门学科,它应该去怎么学。在半个多世纪的发展中,机器学习也产生了很多分支,同时出现了无数经典的模型,但那些都是构建在机器学习基本理论框架下所产生的变化。因此立即机器学习的本质,对于去理解哪些种类繁多的算法将变得更加容易。同时这儿也要阐述一个事实就是,机器学习的核心不是那些各种算法,而是整个机器学习这门学科处理问题的基本思路和流程,我们通过时间的积累掌握可以应用于更多不同场景的算法,这可以帮助我们更快更好的处理问题,但是永远不要忘记我们使用那些工具的能力。
最后非常感谢您能读完本文!由于本人知识有限,其中不可避免有不当错漏之处,还请批评指出。同时也非常感谢李航老师所著《统计学习方法》,读完受益匪浅!由于篇幅有限很多严格的逻辑推导和基本概念在本文没能讲到,因此读完本文的朋友可以结合该书相互印证,相信你又能有不一样的理解。
任何意见或建议随时联系:
Gmail: yxhlfx@gmail.com
qq : 1137924614
2018.1