神经网络的基础——感知机

从问题入手

分类和回归

  • 分类问题
    分类问题在日常生活中非常普遍,我们人脑每天要处理成千上万次的分类问题。例如,我们根据记忆中的面貌特征来辨别同学。可以这样理解分类:给定一个未知类别的样本和历史经验(已知样本),利用未知样本的特征来判断其类别。分类问题的输出结果是一个有限的集合 { 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=ax+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)=x1ax0b
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 w1z=w1ax0+w1x1w1b
同时,为了表示上的方便,将 − w 1 ∗ a -w_1*a w1a表示为 w 0 w_0 w0,将 − w 1 ∗ b -w_1*b w1b表示为 B B B。其正负性来仍然可以表示类别。则新的表达式为:
Z = w 0 ∗ x 0 + w 1 ∗ x 1 + B Z = w_0 * x_0 + w_1 * x_1 + B Z=w0x0+w1x1+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+B2 Ax0+By0+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+w12 1iyi(w0x0+w1x1+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 )=iyi(w0x0+w1x1+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 w0w0+αyix0
w 1 ← w 0 + α ∗ y i ∗ x 1 w_1 \gets w_0 + \alpha*y_i*x_1 w1w0+αyix1
B ← B + α ∗ y i B \gets B + \alpha*y_i BB+α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=w0x0+w1x1+B计算出Z,判断Z的正负号即可对样本进行分类。
我们看一下最终的分类图:

plt.plot([3.5], [1.9], '.b')
draw(X1, X2)
draw_line(w)
plt.show()

在这里插入图片描述

从单感知机到神经网络

更复杂的数据

上一节,我们搭建了一个二维数据的感知机并成功让它学习到了样本的分布。现在我们画出它的计算图:
在这里插入图片描述

如果我们的样本不是那么得简单,例如:
在这里插入图片描述

这时,我们无论如何都不能用一条直线正确分类所有的样本,但它仍然是线性可分的:
在这里插入图片描述

如果我们拟合出两个超平面,那么他们可以共同决定样本的类别。这时候,我们的感知机单元就显得不那么够用了,必须加以扩展:
在这里插入图片描述

现在可以直观地看到,我们的计算图更复杂了。理论上来说,中间那一层的Z足够多,那我们就能将任何分布的数据正确分类。这时候,众多的感知机就组成了一个有向计算网络。

神经网络的基本构成

实际问题越复杂,我们的神经网络也越复杂。无论多么复杂,我们构建一个神经网络一定要考虑以下的步骤:

  • 完整的前向计算
    这里需要设计从原始数据到最终输出的计算步骤。为了完成这一步骤,还需要:
    1. 合理设计网络的大致层数
    2. 定义神经元内的计算
    3. 定义层与层之间的的计算
  • 设计出准确、可导的损失函数
  • 设计合理的训练策略。划分数据集,完成训练。
    以上每一个步骤都可以展出许多内容,以后我们在搭建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)
    
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值