Python-构建感知器

1 Python-构建感知器

1.1 人工神经元

为了设计人工智能,人们尝试模仿生物神经元,神经元是大脑中连接起来参与化学和电信号处理与传输的神经细胞,麦库洛和皮兹(MCP)把神经细胞描述为带有二进制输出的简单逻辑门,多个信号到达树突,然后整合到细胞体,并当信号累计到一定的阈值时,输出信号将通过轴突。

弗兰克·罗森布拉特首先提出了基于MCP神经元模型的感知器学习规则概念。

基于此,可以将人工神经元逻辑放于二元分类场景,将两类分为\(1(正类)\)\(-1(负类)\)用以化简。定义决策函数\((\phi(z))\)输入值设为\(x\)权重设为\(w\),设净输入为\(z=w_1 x_1+\cdots+w_m x_m:\)

\[\boldsymbol{w}=\left[\begin{array}{c} w_1 \\ \vdots \\ w_m \end{array}\right], \boldsymbol{x}=\left[\begin{array}{c} x_1 \\ \vdots \\ x_m \end{array}\right] \]

设阈值为\(\theta\),因为输入的样本\(x\)不止一个,我将第\(i\)个样本的\(x\)设为\(x^{(i)}\),如果某个样本的\(x^{(i)}\)得到的净输入的输出比定义的阈值\(\theta\)大,则预测结果为\(1\),否则为\(-1\)

在感知算法中,决策函数\(\phi(\cdot)\)单位阶跃函数的变体:

\[\phi(z)=\left\{\begin{array}{rc} 1, & \text { if } z \geqslant \theta \\ -1, & \text { otherwise } \end{array}\right. \]

单位阶跃函数

\[u(t)= \begin{cases}0 & \text { for } t<0 \\ 1 & \text { for } t>0\end{cases} \]

它可以用来描述一个系统的输入和输出之间的关系。

为了简化,将阈值\(\theta\)放到等式左边,权重零重新定义为\(w_0=-\theta, \quad x_0=1\),这样就可以用更紧凑的方式表示\(z\)

\[z=w_0 x_0+w_1 x_1+\cdots+w_m x_m=w^{\mathrm{T}} \boldsymbol{x} \]

\(\phi(z)\)将变为:

\[\phi(z)=\left\{\begin{array}{rc} 1, & \text { if } z \geqslant 0 \\ -1, & \text { otherwise } \end{array}\right. \]

在机器学习文献中,通常把负的阈值和权重\(w_0=-\theta\)称为偏置

1.2 感知器学习规则

感知器模型背后的逻辑是用还原论的方法来模拟大脑神经元的工作情况:要么触发要么不触发。

还原论:也称为还原主义,是一种哲学思想,认为复杂的系统、事物、现象可以将其化解为各部分之组合来加以理解和描述。

感知器的规则总结为以下几步:

1.把权重初始化为0或小的随机数

2.对每个训练样本\(x^{(i)}:\)

a.计算输出值\(\hat{y}\)

b.更新权重。

输出值为单位阶跃函数预测的预先定义好的类标签,比如\(x^{(i)}\)输入运算后,输出\(\hat{y}^{(i)}\)

根据上述感知器规则,接下来需要更新权重,在感知器中,权重的更新表达式为:

\[w_j:=w_j+\Delta w_j \]

\(\Delta w_j\)用来更新\(w_j\)的值,\(\Delta w_j\)根据感知器的学习规则计算:

\[\Delta w_j=\eta\left(y^{(i)}-\hat{y}^{(i)}\right) x_j^{(i)} \]

\(\eta\)是预先规定好的学习率,\(y^{(i)}\)是该项样本正确分类的类别\(\hat{y}^{(i)}\)是感知器预测出的类别,在此需要注意,感知器模型中权重是和特征的维数相匹配的有多少个\(x_j\)就有多少个\(w_j\),而所有的\(w_j\)是同时更新的,因此在所有\(w_j\)更新完之前,不会重新计算\(\hat{y}^{(i)}\)。具体来说,在一个只有两个特征的数据(即二维数据)中以下列情况进行更新:

\[\begin{gathered} \Delta w_0=\eta\left(y^{(i)}-\text { output }^{(i)}\right) \\ \Delta w_1=\eta\left(y^{(i)}-\text { output }{ }^{(i)}\right) x_1^{(i)} \\ \Delta w_2=\eta\left(y^{(i)}-\text { output } ^{(i)}\right) x_2^{(i)} \end{gathered} \]

\[\begin{gathered} \Delta w_0=\eta\left(y^{(i)}-\text { output }^{(i)}\right) \\ \Delta w_1=\eta\left(y^{(i)}-\text { output }{ }^{(i)}\right) x_1^{(i)} \\ \Delta w_2=\eta\left(y^{(i)}-\text { output } ^{(i)}\right) x_2^{(i)} \end{gathered} \]

这里的更新规则和《统计学习方法》中描述的感知机学习策略不同,在《统计学习方法》中,感知机学习算法是求参数\(w,b\),使其为以下损失函数极小化问题的解

\[\min _{w, b} L(w, b)=-\sum_{x_i \in M} y_i\left(w * x_i+b\right) \]

其中\(M\)为误分类点的集合。

实际上,这两种方法都没问题,在感知机的原论文中使用的是

\[\begin{gathered} w_{i j_{new}}= w_{i j_{old}}+C\left(t_j-x_j\right) a_i \\ C=\text { learning rate } \end{gathered} \]

作为感知机的更新规则,也就是本文中的规则。

在《统计学习方法》中使用上述方法进行计算,在出现误分类时才对\(w,b\)进行更新:

\[\begin{gathered} w \leftarrow w+\eta y_i x_i \\ b \leftarrow b+\eta y_i \end{gathered} \]

而原论文中在正确分类时:

\[\begin{aligned} & \Delta w_j=\eta\left(y^{(i)}-\hat{y}^{(i)}\right) x_j^{(i)} \\ & y^{(i)}-\hat{y}^{(i)}=0 \end{aligned} \]

也就不会更新权重\(w,b\)。而在误分类时:

\[\begin{gathered} w_j:=w_j+\Delta w_j \\ \Delta \boldsymbol{W}_j=\pm 2 \eta \boldsymbol{X} \\ w_j=w_j \pm 2 \eta \boldsymbol{X} \end{gathered} \]

对比《统计学习方法》中的规则来说只有一个常数\(2\)的差别。
出现这样的差别可能是《统计学习方法》为了在后面章节中将感知机和\(SVM\)串联起来,因此做出这样的改变。

下面根据实例来理解感知器的更新规则。一共有两种分类正确的情况,分别是正类分为正类,负类分为负类,此时:

\[\begin{gathered} \Delta w_j=\eta(-1-(-1)) x_j^{(i)}=0 \\ \Delta w_j=\eta(1 - 1) x_j^{(i)}=0 \end{gathered} \]

当分类错误时,有:

\[\begin{gathered} \Delta w_j=\eta(1-(-1)) x^{(0)}=\eta(2) x_j^{(i)} \\ \Delta w_j=\eta(-1-1) x_j^{(i)}=\eta(-2) x_j^{(i)} \end{gathered} \]

简化为:

\[\Delta \boldsymbol{W}_j=\pm 2 \eta \boldsymbol{X} \]

帮助理解为什么要乘\(x_j^{(i)}\),我们有下面的例子,其中:

\[\hat{y}^{(i)}=-1, y^{(i)}=+1, \eta=1 \]

假设\(x_j^{(i)} = 0.5\),当模型误判为-1时,根据规则,\(\Delta w_j = (2)x_j^{(i)}\),我们的权重\(w_j:=w_j+\Delta w_j\),最终增加1,这样在下次迭代的时候,再遇到该样本时净输入\(x_j^{(i)} * w_j\)就会呈现为正数。这样极有可能超过单位阶跃函数的阈值,将该样本判断为+1:

\[\Delta w_j=(1-(-1)) 0.5=(2) 0.5=1 \]

\(x_j^{i}\)有多种解释,这里给出线性最小二乘的解释:

已知散点集合\(\mathrm{X}=\left\{\mathrm{x}_1, \mathrm{x}_2, \cdots, \mathrm{x}_{\mathrm{n}}\right\}, \mathrm{Y}=\left\{\mathrm{y}_1, \mathrm{y}_2, \cdots, \mathrm{y}_{\mathrm{n}}\right\}\),求参数方程\(y = ax + b\)

其中\(\mathrm{X}, \mathrm{Y},b\)已知,\(a\)未知。对于这种问题,我们希望方程\(y = ax + b\)能最大程度的贴近数据集\(\mathrm{X}, \mathrm{Y}\),我们定义代价函数为:

\[\mathrm{F}(\mathrm{a} )=\sum_{\mathrm{i}=1}^{\mathrm{n}}\left[\mathrm{y}_{\mathrm{i}}-\left(\mathrm{a} \mathrm{x}_{\mathrm{i}}+\mathrm{b}\right)\right]^2 \]

我们要求的是使得\(\mathrm{F}(\mathrm{a})\)最小的权重\(a\),所以对\(a\)进行求导,最后得到式子:

\[2(y_i - (ax_i + b))*x_i \]

在感知机中,我们将偏置\(-\theta\)放在了权重\(w_0\)\(x_0\)相乘的位置,所以最后感知机的更新规则为:

\[\Delta \boldsymbol{W}_j=\pm 2 \eta \boldsymbol{X} \]

权重更新与\(x_j^{(i)}\)成正比。假设另外有一个样本\(x_j^{(i)} = 2\)被错误的分类为-1,可以将决策边界推到更大,以确保下一次分类正确:

\[\Delta w_j=(1--1) 2=(2) 2=4 \]

重要的是要注意,只有两类线性可分,并且学习速率足够小,这样感知器的收敛性才能得到保证。如果两类不能用线性决策边界分离,可以为训练集设置最大通过数(迭代次数)及容忍错误的阈值,否则分类感知器将永远不会停止更新权重。

总流程如图所示:

image

1.3 代码实现

import numpy as np
#将感知器接口定义为一个类,允许初始化新的Perceptron对象,对象通过fit方法从数据中学习,通过predict方法进行预测,
#作为约定我们在对象初始化时未创建但通过对象的其他方法创建的属性的后面添加下划线(_),例如self.w_。
class Perceptron(object):
    """
    参数详解:
    w_:1维的数组,是模型训练后的权重
    errors_:列表,每次迭代错误分类的数量
    X:数组,形状=[n个样本,k个特征]
    y:数组,形状=[n个样本]是模型计算的目标
    """
    #对象初始化,初始化需要的学习率,迭代轮数,随机种子
    def __init__(self, eta = 0.01, n_iter = 50, random_state = 1):
        #学习率为0.01,迭代50次,随机种子为1
        self.eta = eta
        self.n_iter = n_iter
        self.random_state = random_state
    #训练模型
    def fit(self, X, y):
        rgen = np.random.RandomState(self.random_state)
        #通过正态分布随机初始化模型权重
        self.w_ = rgen.normal(loc=0.0, scale=0.01, size= 1 + X.shape[1])
        #self.error_保存每次迭代中分类的错误,可以在后期分析训练阶段感知器的表现
        self.errors_ = []
        #开始迭代
        for _ in range(self.n_iter):
            #记录这次迭代预测错误的次数
            errors = 0
            #zip返回两个可迭代序列中元素元组组成的迭代器
            # 如seq1 = [1, 2], seq_2 = [3, 4], 那么迭代zip(seq1, seq2)等价于迭代[(1, 3), (2, 4)]
            for xi, target in zip(X, y):
                update = self.eta * (target - self.predict(xi))
                #self.w_[0]是添加上去的一个值
                self.w_[1:] += update * xi
                self.w_[0] += update
                errors += int(update != 0.0)
            #记录该次迭代错误分类有几个
            self.errors_.append(errors)
        return self
    #计算向量点积
    def net_input(self, X):
        return np.dot(X, self.w_[1:]) + self.w_[0]
    #预测 
    def predict(self, X):
        return np.where(self.net_input(X) >= 0.0, 1, -1)
    

模型训练:

import pandas as pd
#读取数据
url = 'https://www.gairuo.com/file/data/dataset/iris.data'
df = pd.read_csv(url)
df.tail()
sepal_length sepal_width petal_length petal_width species
145 6.7 3.0 5.2 2.3 virginica
146 6.3 2.5 5.0 1.9 virginica
147 6.5 3.0 5.2 2.0 virginica
148 6.2 3.4 5.4 2.3 virginica
149 5.9 3.0 5.1 1.8 virginica
import matplotlib.pyplot as plt
y = df.iloc[0:100, 4].values#0-99行,第四列的数据
y = np.where(y == 'setosa', -1, 1)#将setosa设置为-1,其余为1

因为np.dot是向量点积所以X = df.df.iloc[0:100, [0,2]].values,不然会报错

X = df.iloc[0:100, [0,2]].values #取0-99行的第0列和第2列
#绘制散点图
plt.scatter(X[:50,0],     X[:50, 1],    color = 'red',  marker = 'o', label = 'setosa')
plt.scatter(X[50:100, 0], X[50:100, 1], color = 'blue', marker = 'x', label = 'versicolor')
plt.xlabel('sepal length [cm]')
plt.ylabel('petal length [cm]')
#设置图例所在位置
plt.legend(loc='upper left')
plt.show()

image

这里需要解释一下,为什么要将X分为X[:50,0]X[50:100, 0],观察数据集可知,在鸢尾花数据集中,数据的排列是有序的,每一种花都是排列在一起的且数量为50,而setosa为前50个,versicolor为后50个。

#实例化模型
ppn = Perceptron(eta=0.1, n_iter=10)
#训练
ppn.fit(X, y)
##图中点的横坐标为1~len(pnn.errors_),y坐标为errors_,形状为'o'
plt.plot(range(1, len(ppn.errors_) + 1),
            ppn.errors_, marker='o')
plt.xlabel('Epochs')
plt.ylabel('Number of updates')
plt.show()


image

from matplotlib.colors import ListedColormap


def plot_decision_regions(X, y, classifier, resolution=0.02):
    markers = ('s', 'x', 'o', '^', 'v')
    colors = ('red', 'blue', 'lightgreen', 'gray', 'cyan')
    #创建色度图,每类一种颜色
    cmap = ListedColormap(colors[:len(np.unique(y))])
    #设置网格图中的每个点xx1为x坐标,xx2为y坐标
    x1_min, x1_max = X[:, 0].min() - 1, X[:, 0].max() + 1
    x2_min, x2_max = X[:, 1].min() - 1, X[:, 1].max() + 1
    xx1, xx2 = np.meshgrid(np.arange(x1_min, x1_max, resolution),
                           np.arange(x2_min, x2_max, resolution))
    #通过训练好的模型对图中每个点进行分类
    Z = classifier.predict(np.array([xx1.ravel(), xx2.ravel()]).T)
    #xx1.shape为(305, 235)就是网图中点的个数
    Z = Z.reshape(xx1.shape)
    #图中颜色会随z值变化,alpha是透明度
    plt.contourf(xx1, xx2, Z, alpha=0.3, cmap=cmap)
    #设置x轴和y轴的范围
    plt.xlim(xx1.min(), xx1.max())
    plt.ylim(xx2.min(), xx2.max())
    #遍历每个y类(实际就两类)idx表示在数组np.unique(y)中的坐标,cl表示数组np.unique(y)中的值
    for idx, cl in enumerate(np.unique(y)):
        plt.scatter(x=X[y == cl, 0], 
                    y=X[y == cl, 1],
                    alpha=0.8, 
                    c=colors[idx],
                    marker=markers[idx], 
                    label=cl, 
                    edgecolor='black')
#画出决策边界
plot_decision_regions(X, y, classifier=ppn)
plt.xlabel('sepal length [cm]')
plt.ylabel('petal length [cm]')
plt.legend(loc='uper left')
plt.show()

image

相关博客

下一章:Python-构建自适应线性神经元

参考

  • [1] 李航. 统计学习方法(第2版)[M]. 清华大学出版社, 2019.
  • [2] Sebastian Raschka. Python机器学习(第2版)[M]. 机械工业出版社, 2017.
posted @ 2023-01-14 23:29  TTS-S  阅读(48)  评论(0编辑  收藏  举报