之前我们讲了K-NN算法,通过将未知数据点和训练集中的数据点做比较,用样本点之间的距离作为度量,然后从训练集中k个最相似的样本中,通过“选举”的方式,获得最高投票的类别获胜,作为最后分类的结果。
虽然简单直观,但是也有很多缺点:
- 实际上没有"学习"任何东西,如果算法出错,那么它将无法在接下来的分类中自动改正。
- 如果不指定数据结构,k-NN算法占用的空间将随着数据集中样本点数量的增大而增大。当使用数据集维度巨大的的时候,使得其不论在实际还是理论上去使用都具有很大的挑战性。
显然,如果我们每次做分类预测的时候都保存着一份几百GB甚至TB的训练集,这显然是不太合理的。我们需要一种学习模型能够在训练时从输入数据学习模式(需要更多的时间来训练模型),但是通过少量参数来定义模型的好处是可以轻松的表示模式,而不需要管训练集的大小。这种机器学习的方式称为:参数化学习。
“A learning model that summarizes data with a set of parameters of fifixed size
(independent of the number of training examples) is called a parametric model. No
matter how much data you throw at the parametric model, it won’t change its mind
about how many parameters it needs.” – Russell and Norvig (2009) [73]
总的来说,参数化学习最大的作用就是可以脱离数据集,通过定义参数的方式来表示模型。
本文将讨论参数化学习的概念,并实现一个简单的线性分类器。
什么是参数化学习?
简单来说,参数化学习就是为给定的模型定义必要参数的过程。
对于机器学习来说,参数话学习设计定义模型四个组成成分:
- Data
- scoring function
- Loss function
- Weights and Biases
Data:我们将要学习的,输入的数据。包括样本点和与样本点所联系的标签。通常我们将输入的数据表示为一个多维的矩阵,矩阵中的每一行代表一个样本点,每一列代表一个特征。伴随与此的还有一个标签向量y,其中yi表示数据集中第i个样本点的标签。
Scoring function:Scoring function将我们输入的Data映射为类别标签。如果我们将其data定义为input_data,Scoring function定义为func( ),类别标签定义为output_label,那么有:
f
u
n
c
(
i
n
p
u
t
_
d
a
t
a
)
=
o
u
t
p
u
t
_
l
a
b
e
l
func(input\_data) = output\_label
func(input_data)=output_label
Loss function:对我们的预测结果与真实值的符合程度进行定量,预测结果越符合真实值,loss越低,精度越高(最起码在训练集上是这样),我们训练机器学习模型的目标就是使Loss function最小,以此来提升预测的精度。
Weights & Biases:我们实际上要优化的参数,基于Scoring function和loss function的输出,我们会调整weights和Biases的值来提升准确性。
接下来让我们看看这些部分如何组成一个线性分类器吧。
假设数据集表示为Xi,其中每一个样本点有着对应的标签yi,假设共有N个样本点,K个标签类别。通过这些标量我们可以定义我们的scoring function为
f
(
x
i
,
W
,
b
)
=
W
x
i
+
b
f(x_i,W,b) = Wx_i+b
f(xi,W,b)=Wxi+b
参数化学习和线性分类的优势
-
一旦完成模型的训练,就不再需要训练集,只要保留矩阵W和向量b即可
-
对于新的测试数据分类速度极快。只需要和矩阵W进行一次点乘再加上向量b即可。
接下来让我们用Python实现一个简单的线性分类器。
需要学习的几个方法:
np.random.seed()
指定伪随机数生成器的“种子”。
个人理解,所谓种子就是指标志。我们在使用随机数生成器生成随机数时,并非是每次真的随机生成一个数组,而是从一个随机数序列中按照次序返回给我们若干个随机数。而这些随机数队列不可能只有一个,每个随机数队列的标志则称为种子(非官方解释,自己的理解)。
举个例子,在随机数生成器中有3个队列,里面存放着大量固定顺序的随机数,如下:
[0.1231,0.3456,0.7739,0.1435,0.7564,0.7653,0.9876,0.4444]
[0.7656,0.3546,0.1236,0.4265,0.1235,0.9999,0.2246,0.6548]
[0.5564,0.1134,0.4378,0.9806,0.5643,0.8789,0.2234,0.2145]
当我们指定np.random.seed(1)的时候,就会从1所代表的队列中按照顺序返回随机数。
当我们指定np.random.seed(2)的时候,就会从2所代表的队列中按照顺序返回随机数。
np.random.randn()
返回一个或一组服从标准正态分布的随机样本值。
当不传入参数时,返回一个浮点数。
当传入一个参数时,返回一个秩为1的数组。
当传入两个参数及以上时,返回对应维度的矩阵,可以表示对应的向量或者矩阵。
flatten()
将一个矩阵折叠成一个一维的数组,作用对象只能是NumPy数组或者Mat。
cv2.putText(image, content,startPoint, Font, size, color, thickness)
作用:在图片上写文字
参数:
image
:要写在哪幅图上
content
:字符串,要写的文字内容
startPoint
:坐标二元组(x,y),表示字符串在图片上的起点位置
font
:要使用的字体
size
:文字的大小
color
:颜色三元组(b,g,r),表示文字的颜色
thickness
:文字的粗细
新建一个Linear_classifier.py文件写入如下代码:
import numpy as np
import cv2
labels = ["squirrel", "dog", "cat"]
np.random.seed(1)
W = np.random.randn(3, 3072)
b = np.random.randn(3)
origin = cv2.imread("./animal.png")
image= cv2.resize(origin, (32, 32)).flatten()
scores = W.dot(image) + b
for(label, score) in zip(labels, scores):
print("[INFO]{}:{:.2f}".format(label, score))
cv2.putText(origin, "Label:{}".format(labels[np.argmax(scores)]),
(10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.9, (0, 0, 255), 2)
cv2.imshow("result", origin)
cv2.waitKey(0)
着这个例子并没有从头到尾演示如何训练模型,而是简单的展示了一下我们如何初始化weight矩阵W和bias向量b,并用这些参数通过点乘来分类一副图像。
我们的目标是正确的对下面的松鼠图像进行分类:
第4行初始化了目标类别标签,一共有三个标签:“squirrel", “dog”, “cat”
第6行设置了一个伪随机数的生成器,确保我们的实验结果可以重现
第8行初始化了我们的weight矩阵W,通过在[0,1]之间的标准正态分布中进行采样
第9行初始化了bias向量b,共3行,与分类标签数相对应
第11行从我们的电脑上读入一张图片,定义为矩阵"origin"
第12行将我们的矩阵“origin”拉伸为一个3072维度的向量image
第14行对我们的向量image执行scoring function
第16-17行将scoring function 输出结果按照类别打印到控制台
第19和20行将最终预测的类别结果标记在原来的标签上,并展示出来
如果我们是从头开始对这个线性分类器进行训练,我们需要通过一步步优化对W和b进行学习,但由于我们目前只是简单的演示一下过程,所以我用1来初始化了伪随机数数字生成器,来确保随机值能够给我们提供一个正确的分类结果(在实验之前提前测试过生成的随机数结果)。现在,我们仅需要吧矩阵W和向量b当作是经过某种神奇优化的“黑箱数组”,后面会学习其中的实现细节。
下来让我们执行linear_classifier.py文件
控制台输出以下结果:
[INFO]squirrel:7153.79
[INFO]dog:2259.43
[INFO]cat:1749.35
注意到”squirrel“标签获得了scoring function的最大值,这意味着分类器选择“squirrel”类作为最终的预测结果,我们可以看到,程序将分类结果“squirrel”写在了图片上。
但是我们要记住,我们这里给出的W和b都是特定好的,在实际中,如果我们不通过学习来得到W和b,我们将无法让分类器预测出正确的结果,本文中只是为了简化分类器预测的过程,所以仅仅将W和b进行了初始化。
Loss function
参数化学习的过程其实就是通过一系列参数并不断优化他们,来学习一种方法,这种方法可以将输入的数据映射为最终输出的结果。
但是为了真正意义上的“学习”这种映射,我们还需要探讨两个概念:
- Loss functions
- Optmization
什么是Loss Function?
实际上,损失函数就是用来衡量我们所给定的预测器在对数据集中的样本点进行分类时的“好”“坏”程度。
为了提高我们的分类精度,我们需要去调节weight矩阵W和biases向量b的参数。在理想条件下,我们的损失(loss)会随着我们调整模型参数的次数(时间)而下降。
多类别的支持向量机损失
多分类向量损失的灵感来自于支持向量机。支持向量机的scoring function表示为:
f
(
x
i
,
W
,
b
)
=
W
x
i
+
b
f(x_i,W,b) = Wx_i+b
f(xi,W,b)=Wxi+b
现在我们有了socring function,我们需要来确定这个函数在做预测时候的表现是好是坏,所以我们需要一个loss function.
为了简化,将我们的scoring function用**s**来表:
s
=
f
(
x
i
,
W
)
s = f(x_i,W)
s=f(xi,W)
第 i 个样本点 被预测为第 j 类的scoring function的输出为:
s
j
=
f
(
x
i
,
W
)
j
s_j = f(x_i,W)_j
sj=f(xi,W)j
通过这样的表示,我们可以将他们放在一起,获得 铰链损失函数(hinge loss function)
L
i
=
∑
j
≠
y
i
m
a
x
(
0
,
s
j
−
s
y
i
+
1
)
L_i = \sum_{j≠y_i}max(0,s_j-s_{y_i}+1)
Li=j=yi∑max(0,sj−syi+1)
铰链损失函数把所有不正确的类别(i ≠ j )加起来,并比较我们为第j类标签(不正确的类)和第yi类(正确的类)返回的评分函数s的输出,用最大值函数来把值钳制在0出,这对与确保我们不会加上负数很重要。
当损失Li = 0 的时候,一个给定的样本点xi就被正确分类了。TODO(因为说明其他类别的预测值小于正确类别的预测值,sj - syi + 1 <= 0 => sj - syi <= 1 )
为了推导出整个训练集的损失,我们简单的取每个Li之和的平均数:
L
=
1
N
∑
i
=
1
N
L
i
L=\frac{1}{N}\sum^{N}_{i=1}L_i
L=N1i=1∑NLi
另一种你可能会遇到的相关的损失函数:平方铰链损失函数:
L
i
=
∑
j
≠
y
i
m
a
x
(
0
,
s
j
−
s
y
i
+
1
)
2
L_i =\sum_{j≠y_i}max(0,s_j-s_{y_i}+1)^2
Li=j=yi∑max(0,sj−syi+1)2
平方项会通过将输出平方来加重处罚损失,这会导致不正确预测的损失以4次方增长。
具体选择那种损失函数依据情况而定,一般来说标准的铰链损失函数更加常用,但是在某些数据集上平方铰链损失函数表现更好,毕竟这是需要我们去调整的超参数(hyperparameter)
·
交叉熵损失和Softmax分类器
和铰链损失最大的区别: 铰链损失呈现的是间隔,而Softmax分类器呈现的是每一个类别标签的概率,更便于人们理解。
Softmax分类器是二值化形式的逻辑回归的一般化,如同铰链损失和平方铰链损失一样,我们的函数 f 接收一组输入数据xi , 并通过和矩阵W点乘运算,再加上向量b,将它们映射为输出数据标签
f
(
x
i
,
W
,
b
)
=
W
x
i
+
b
f(x_i,W,b)=Wx_i+b
f(xi,W,b)=Wxi+b
但是,和铰链损失不同的是,我们可以将输出的值解释为每个类别标签的非标准的对数概率,我们用交叉熵损失来代替铰链损失:
L
i
=
−
l
o
g
P
(
e
s
y
i
∑
j
e
s
j
)
L_i=-logP(\frac{e^{s_{y_i}}}{\sum_{j}e^{s_j}})
Li=−logP(∑jesjesyi)
让我们来推理一下这个公式。首先,我们的损失函数应该最小化正确类别的 概率,也就是最大化正确类别的对数概率,也就是最小化正确类别的负对数概率
L
i
=
−
l
o
g
P
(
Y
=
y
i
∣
x
=
x
i
)
L_i=-logP(Y=y_i|x=x_i)
Li=−logP(Y=yi∣x=xi)
上式中概率的部分可以表示为:
P
(
Y
=
k
∣
x
=
x
i
)
=
e
s
y
i
/
∑
j
e
s
j
P(Y=k|x=x_i)=e^{s_{y_i}}/\sum_j e^{s_j}
P(Y=k∣x=xi)=esyi/j∑esj
其中:
s
=
f
(
x
i
,
W
,
b
)
s=f(x_i,W,b)
s=f(xi,W,b)
综上所述,最终对于单个样本点的损失函数就变成了上面所描述的:
L
i
=
−
l
o
g
P
(
e
s
y
i
∑
j
e
s
j
)
L_i=-logP(\frac{e^{s_{y_i}}}{\sum_{j}e^{s_j}})
Li=−logP(∑jesjesyi)
最后通过求平均数来计算整个数据集上所有样本点的交叉熵损失:
1
N
∑
i
=
1
N
L
i
\frac{1}{N} \sum^{N}_{i=1}L_i
N1i=1∑NLi
参考书籍:Deep Learning For Computer Vision With Python