文章目录
从问题入手
分类和回归
- 分类问题
分类问题在日常生活中非常普遍,我们人脑每天要处理成千上万次的分类问题。例如,我们根据记忆中的面貌特征来辨别同学。可以这样理解分类:给定一个未知类别的样本和历史经验(已知样本),利用未知样本的特征来判断其类别。分类问题的输出结果是一个有限的集合 { c l a s s 1 , c l a s s 2 , . . . c l a s s n } \{class_1, class_2, ...class_n\} {class1,class2,...classn},且有对错之分,例如误将同学A认成了同学B或同学C。 - 回归问题
回归处理的往往是“定量”问题。例如,根据路程和历史价格、时段来预估出租车车费。回归问题的输出空间不再是一个有限元素的集合,而是一个连续的实数域[a, b]。相对于分类问题来说,回归结果没有绝对的正确和错误,但有回归误差的概念。我们可以定义一个度量 d = F ( y p r e d , y t r u e ) d=F(y_{pred}, y_{true}) d=F(ypred,ytrue)来判断回归结果与真实值之间的误差。
如何根据坐标分布判断二维数据点的类别
- 已知样本
假设我们在二维空间内有两组组数据点,分别属于两个类别。
X1 = [[1.1, 4.2], [0.5, 2.5], [2.1, 6.9], [1.5, 5.5], [1.9, 7.0]]
X2 = [[5.1, 2.1], [4.9, 1.4], [3.9, 0.9], [4.3, 1.7], [6.0, 2.9]]
- 任务目标
我们需要根据已知样本,预测未知样本[3.5, 1.9]的类别 - 分析已知样本
一般来说,同一类的样本总是有相似之处的。我们要完成分类任务,就要想尽办法挖掘所谓的相似之处。在输入了样本点之后,我们使用matplotlib画出已知样本在坐标系中的分布图。
import matplotlib.pyplot as plt
#定义一个画点的函数
def draw(x1, x2):
plt.plot([i[0] for i in x1], [i[1] for i in x1], 'g.')
plt.plot([i[0] for i in x2], [i[1] for i in x2], 'r.')
draw(X1, X2)
可以看到输出:
可以发现,第一类(绿色)与第二类(红色)样本可以用一条直线分割,这时候称总体是线性可分的。这个直线在许多算法中被称作超平面。现在,我们的分类策略是:如果我们找到一个超平面,就可以根据样本出现在超平面的哪一侧来判断样本的类别。
感知机——神经网络的雏形
前向:利用超平面表示样本的类别
接下来,我们将设计一个感知机来完成我们的分类任务。感知机是一个典型的线性判别器。我们回想一下二维空间的超平面(直线)的表达:
y
=
a
∗
x
+
b
y = a*x +b
y=a∗x+b
如果我们把某样本的横坐标
x
0
x_0
x0带入上式,可以得到一个结果
y
^
\hat{y}
y^。注意到(
x
0
x_0
x0,
y
^
\hat{y}
y^)是超平面上的点,如果该样本的真实纵坐标
x
1
x_1
x1大于
y
^
\hat{y}
y^,说明样本在超平面的上侧,则我们就将该样本分类为为正类,否则就分类为负类:
z
(
x
0
,
x
1
)
=
x
1
−
a
∗
x
0
−
b
z(x_0, x_1) = x_1 - a * x_0 - b
z(x0,x1)=x1−a∗x0−b
f
(
z
)
=
{
1
,
z>0
−
1
,
otherwise
f(z)= \begin{cases} 1, & \text {z>0} \\ -1, & \text{otherwise} \end{cases}
f(z)={1,−1,z>0otherwise
为了方便在计算机上实现运算,我们还需要对z函数进行变形。注意到1、-a分别是
x
1
x_1
x1,
x
0
x_0
x0的系数,在等式两边同时乘以某个常数
w
1
w_1
w1,则:
w
1
∗
z
=
−
w
1
∗
a
∗
x
0
+
w
1
∗
x
1
−
w
1
∗
b
w_1 * z = - w_1*a*x_0 +w_1 * x_1 - w_1*b
w1∗z=−w1∗a∗x0+w1∗x1−w1∗b
同时,为了表示上的方便,将
−
w
1
∗
a
-w_1*a
−w1∗a表示为
w
0
w_0
w0,将
−
w
1
∗
b
-w_1*b
−w1∗b表示为
B
B
B。其正负性来仍然可以表示类别。则新的表达式为:
Z
=
w
0
∗
x
0
+
w
1
∗
x
1
+
B
Z = w_0 * x_0 + w_1 * x_1 + B
Z=w0∗x0+w1∗x1+B
f
(
Z
)
=
{
1
,
Z>0
−
1
,
otherwise
f(Z)= \begin{cases} 1, & \text {Z>0} \\ -1, & \text{otherwise} \end{cases}
f(Z)={1,−1,Z>0otherwise
其中,
x
0
x_0
x0 与
x
1
x_1
x1是样本的坐标,
w
0
w_0
w0,
w
1
w_1
w1,
B
B
B可以决定超平面的位置。
现在让我们使用tensorflow完成以上的计算。
import tensorflow as tf
# 创建w_0、w_1、B
w = tf.Variable(initial_value=tf.random.normal(shape=[3, 1], dtype='float32'),
trainable=True)
print('w: ', w)
X1_tensor = tf.convert_to_tensor(X1)
X1_tensor = tf.concat(values=[X1_tensor, tf.ones(shape=[X1_tensor.shape[0], 1])], axis=-1)
X2_tensor = tf.convert_to_tensor(X2)
X2_tensor = tf.concat(values=[X2_tensor, tf.ones(shape=[X2_tensor.shape[0], 1])], axis=-1)
X = tf.concat(values=[X1_tensor, X2_tensor], axis=0)
print('X.shape: ', X.shape)
Z = tf.matmul(X, w)
print(Z)
损失函数:衡量分类的误差
观察分类效果
我们在上一步定义的w,实际上是一个由 w 0 w_0 w0、 w 1 w_1 w1、 B B B构成的向量,我们通过这三个参数还原超平面的斜率与截距,使用matplotlib画出超平面:
import numpy as np
#定义一个根据w向量画直线的函数
def draw_line(w):
a = -w[0]/w[1]
b = -w[-1]/w[1]
x = np.linspace(start=0, stop=10, num=10)
y = a * x + b
plt.plot(x, y, '--')
#画出数据点和超平面
draw(X1, X2)
draw_line(w)
可以看到随机生成的w并不能很好地对样本进行分类:
现在面临的问题:如何才能衡量超平面位置的优劣?
将误分类点到平面的距离作为分类损失
我们的目的是让超平面将两类已知样本分开,所以可以将误分类的数据点离超平面的距离作为损失。如果超平面将样本正确分类,则我们不考虑其产生的损失。如果分类错误,就将该样本点离直线的距离作为损失,距离越远,损失越大,分类的错误样本点越少,损失越趋于少。在构建感知机的损失函数之前,我们回顾一下点到直线的距离公式:
d
=
∣
A
∗
x
0
+
B
∗
y
0
+
C
A
2
+
B
2
2
∣
d = |\frac{A*x_0 + B*y_0+C}{\sqrt[2]{A^2+B^2}}|
d=∣2A2+B2A∗x0+B∗y0+C∣
公式中的直线方程为Ax+By+C=0,其中的A、B、C就对应
w
0
w_0
w0、
w
1
w_1
w1、
B
B
B,点P的坐标为(x0,y0),对应我们的样本点
(
x
0
,
x
1
)
(x_0, x_1)
(x0,x1)。我们可以根据此构建感知机的损失L:
L
(
x
,
y
,
w
⃗
)
=
−
1
w
0
2
+
w
1
2
2
∗
∑
i
y
i
(
w
0
∗
x
0
+
w
1
∗
x
1
+
B
)
L(x, y, \vec{w}) = -\frac{1}{\sqrt[2]{w_0^2+w_1^2}}*\sum_i{y_i(w_0 * x_0 + w_1*x_1+B)}
L(x,y,w)=−2w02+w121∗i∑yi(w0∗x0+w1∗x1+B)
其中,
y
i
y_i
yi是指第i个误分类样本的真实分类,取值{-1, 1}。它作为一个乘数,保证了误分类样本的损失始终是一个正数。
我们还需要对L进一步精简。现在分析一下等式的右端,可以发现
w
0
2
+
w
1
2
2
\sqrt[2]{w_0^2+w_1^2}
2w02+w12始终是一个正数,它只影响损失的大小,不影响分类的结果,这里可以将其舍去以方便以后的求导(就算去除了该项,在梯度下降过程中,超平面仍然会朝着正确方向调整)。最终的损失L可以表示为:
L
(
x
,
y
,
w
⃗
)
=
−
∑
i
y
i
(
w
0
∗
x
0
+
w
1
∗
x
1
+
B
)
L(x, y, \vec{w}) = -\sum_i{y_i(w_0 * x_0 + w_1*x_1+B)}
L(x,y,w)=−i∑yi(w0∗x0+w1∗x1+B)
现在让我们为样本分配正确的标签:
# 为已知样本点分配标签{-1, 1}
y1 = tf.ones(shape=[X1_tensor.shape[0], 1], dtype='float32')
y2 = -tf.ones(shape=[X2_tensor.shape[0], 1], dtype='float32')
Y = tf.concat(values=[y1, y2], axis=0)
print(Y)
然后找出被超平面误分类的样本,并计算相应的损失:
#计算误分类样本的损失
#1.找出误分类点
L = tf.multiply(Y, Z)
wrong_index = tf.where(condition=L<0)
print(wrong_index)
#2.计算总误分类点的损失
L = tf.multiply(Y, Z)
wrong_index = tf.where(condition=L<0)
#print(L)
loss = -tf.reduce_sum(tf.gather_nd(params=L, indices=wrong_index))
print('loss: ', loss)
print('wrong index: ', wrong_index)
根据结果,我们发现总共有四个样本被错误分类:
反向传播:根据损失,不断调整超平面
构建了损失函数之后,感知机就可以使用梯度下降来更新w中的参数了。梯度下降采用以下的策略进行更新:
w
⃗
←
w
⃗
−
α
∗
∇
w
L
(
w
⃗
,
x
,
y
)
\vec{w} \gets \vec{w}-\alpha *\nabla_wL({\vec{w}, x, y)}
w←w−α∗∇wL(w,x,y)
∇
w
L
(
w
⃗
,
x
,
y
)
\nabla_wL({\vec{w}, x, y)}
∇wL(w,x,y)是由L对w中的各参数偏导组成,我们随机选取一个误分类点,具体的计算可表示为:
w
0
←
w
0
+
α
∗
y
i
∗
x
0
w_0 \gets w_0 + \alpha*y_i*x_0
w0←w0+α∗yi∗x0
w
1
←
w
0
+
α
∗
y
i
∗
x
1
w_1 \gets w_0 + \alpha*y_i*x_1
w1←w0+α∗yi∗x1
B
←
B
+
α
∗
y
i
B \gets B + \alpha*y_i
B←B+α∗yi
我们用python写出梯度更新:
#训练:更新参数
#定义学习率alpha
alpha = 1e-1
#1.获取所有误分类样本和标签
wrong_samples_x = tf.gather(params=X, indices=wrong_index[:,0])
#print('wrong samples x: ', wrong_samples_x)
wrong_samples_y = tf.gather(params=Y, indices=wrong_index[:,0])
print('wrong labels: ', wrong_samples_y)
#2.计算梯度
gradient_w = tf.multiply(wrong_samples_x, wrong_samples_y)
print('gradient_w: ', gradient_w)
gradient_mean = tf.reduce_mean(gradient_w, axis=0, keepdims=True)
gradient_mean = tf.transpose(gradient_mean, perm=[1, 0])
print('gradient_mean: ', gradient_mean)
#3.更新参数
w = w+alpha*gradient_mean
#观察更新之后的超平面
draw(X1, X2)
print('w: ', w)
draw_line(w)
如果调小步长,你会看到超平面的位置发生了更小的变动
对未知数据进行分类
让我们回归最初的问题:预测样本点[3.5, 1.9]。我们得到了超平面之后,可以根据公式
Z
=
w
0
∗
x
0
+
w
1
∗
x
1
+
B
Z = w_0 * x_0 + w_1 * x_1 + B
Z=w0∗x0+w1∗x1+B计算出Z,判断Z的正负号即可对样本进行分类。
我们看一下最终的分类图:
plt.plot([3.5], [1.9], '.b')
draw(X1, X2)
draw_line(w)
plt.show()
从单感知机到神经网络
更复杂的数据
上一节,我们搭建了一个二维数据的感知机并成功让它学习到了样本的分布。现在我们画出它的计算图:
如果我们的样本不是那么得简单,例如:
这时,我们无论如何都不能用一条直线正确分类所有的样本,但它仍然是线性可分的:
如果我们拟合出两个超平面,那么他们可以共同决定样本的类别。这时候,我们的感知机单元就显得不那么够用了,必须加以扩展:
现在可以直观地看到,我们的计算图更复杂了。理论上来说,中间那一层的Z足够多,那我们就能将任何分布的数据正确分类。这时候,众多的感知机就组成了一个有向计算网络。
神经网络的基本构成
实际问题越复杂,我们的神经网络也越复杂。无论多么复杂,我们构建一个神经网络一定要考虑以下的步骤:
- 完整的前向计算
这里需要设计从原始数据到最终输出的计算步骤。为了完成这一步骤,还需要:- 合理设计网络的大致层数
- 定义神经元内的计算
- 定义层与层之间的的计算
- 设计出准确、可导的损失函数
- 设计合理的训练策略。划分数据集,完成训练。
以上每一个步骤都可以展出许多内容,以后我们在搭建CNN与LSTM网络时再具体讨论。
封装我们的感知机(试着完成)
import tensorflow as tf
import matplotlib.pyplot as plt
import numpy as np
class Perceptron(tf.keras.Model):
def __init__(self, X1, X2):
super().__init__()
self.x1 = X1
self.x2 = X2
self.w = tf.Variable(initial_value=tf.random.uniform(shape=[3, 1], dtype='float32'),
trainable=True)
X1_tensor = tf.convert_to_tensor(X1)
X1_tensor = tf.concat(values=[X1_tensor, tf.ones(shape=[X1_tensor.shape[0], 1])], axis=-1)
X2_tensor = tf.convert_to_tensor(X2)
X2_tensor = tf.concat(values=[X2_tensor, tf.ones(shape=[X2_tensor.shape[0], 1])], axis=-1)
y1 = tf.ones(shape=[X1_tensor.shape[0], 1], dtype='float32')
y2 = -tf.ones(shape=[X2_tensor.shape[0], 1], dtype='float32')
self.Y = tf.concat(values=[y1, y2], axis=0)
self.X = tf.concat(values=[X1_tensor, X2_tensor], axis=0)
def draw(self, x1, x2, w):
plt.plot([i[0] for i in x1], [i[1] for i in x1], 'g.')
plt.plot([i[0] for i in x2], [i[1] for i in x2], 'r.')
a = -w[0]/w[1]
b = -w[-1]/w[1]
x = np.linspace(start=0, stop=10, num=10)
y = a * x + b
plt.plot(x, y, '--')
plt.show()
def forward(self, input_x, w):
Z = tf.matmul(input_x, w)
return Z
def train(self, lr = 5e-2):
while True:
Z = self.forward(input_x = self.X, w = self.w)
L = tf.multiply(self.Y, Z)
wrong_index = tf.where(condition=L<0)
#print(L)
loss = -tf.reduce_sum(tf.gather_nd(params=L, indices=wrong_index))
print('loss: ', loss)
wrong_samples_x = tf.gather(params=self.X, indices=wrong_index[:,0])
print('wrong index: ', wrong_index)
if wrong_index.shape[0] == 0:
self.draw(x1 = self.x1, x2 = self.x2, w = self.w)
break
wrong_samples_y = tf.gather(params=self.Y, indices=wrong_index[:,0])
#print('wrong labels: ', wrong_samples_y)
#计算梯度
gradient_w = tf.multiply(wrong_samples_x, wrong_samples_y)
#print('gradient_w: ', gradient_w)
gradient_mean = tf.reduce_mean(gradient_w, axis=0, keepdims=True)
gradient_mean = tf.transpose(gradient_mean, perm=[1, 0])
#print('gradient_mean: ', gradient_mean)
self.w = self.w + lr*gradient_mean
self.draw(x1 = self.x1, x2 = self.x2, w = self.w)
c1 = [[1.1, 4.2], [0.5, 2.5], [2.1, 6.9], [1.5, 5.5], [1.9, 7.0]]
c2 = [[5.1, 2.1], [4.9, 1.4], [3.9, 0.9], [4.3, 1.7], [6.0, 2.9]]
model = Perceptron(X1 = c1, X2 = c2)