感知器算法及python实现

这边建议异步到第二版感知器算法及其python 实现 V2.0,训练速度更快,数据集更直观。
第一版写的还是太过粗糙。

感知器算法及python实现

通俗来讲,感知器算法可以完成如下这类线性可分的二分类分类任务。即找出一条超平面,将两类数据进行划分。
在输入特征为二维的数据集中,即一条直线。

理论依据

感知机二类分类的线性分类模型,根据输入特征,输出类别 1 和 0。(也有为 +1、-1)
下面搬运一个图片,需要注意将左下角的 x 0 x_0 x0 改成 x n x_n xn。(懒就不做图了。。。侵删)

为方便优化,我们也采用激励函数输出为 +1、-1 的形式。
整个算法留存即:
对输入特征加权求和:
∑ i = 0 n w i x i = z \displaystyle\sum_{i=0}^nwixi = z i=0nwixi=z
注意这里将偏置 b b b 转为了 w 0 w_0 w0 ,方便叙述表达。
对加权和 z z z 经过激励函数:
y ′ = { + 1 i f z ≥ 0 − 1 i f z < 0 y' = \begin{cases} +1 & if & z \geq 0 \\ -1 & if & z < 0\\ \end{cases} y={+11ififz0z<0

损失函数或者说目标函数

简单起见,这里不聊概率上的通过参数的似然函数来优化,而从最直观的评估来优化。

我们可以建立一个这样的 目标函数或者说 准确度函数,按照损失函数的管理,这里我们仍然取 L ( w , b ) L(w,b) L(w,b) ,但和损失函数不同的是,我们并不是希望他越小越好。下面我会做解释。 (这里和我在其他地方查到的资料不同,更多的资料是最小化误分类点到决策面的距离
l k ( w , b ) = y k ( ∑ i = 0 n w i x i k ) = y k z k l^k(w,b) = y^k(\displaystyle\sum_{i=0}^nw_ix_i^k) = y^kz^k lk(w,b)=yk(i=0nwixik)=ykzk
k 代表数据集中的第 k 个样本, y k y^k yk 代表期望的输出,即数据集中正确的输出,即为 +1或 -1。 若分类正确,则 l k ( w , b ) l^k(w,b) lk(w,b) 必定为正。( y k y^k yk z k z^k zk 同号,否则异号)。

将左右的样本的目标函数加和起来:
L ( w , b ) = ∑ k = 1 m l k ( w , b ) = ∑ k = 1 m y k z k L(w,b) = \displaystyle\sum_{k = 1}^m l^k(w,b) = \displaystyle\sum_{k = 1}^my^kz^k L(w,b)=k=1mlk(w,b)=k=1mykzk
故,输出的准确率越高, L ( w , b ) L(w,b) L(w,b) 越大。我们通过梯度上升不停使其越大越好即可。

对权重 w w w 的求导:
{ ▽ w L ( w , b ) = ∑ y i x i ▽ b L ( w , b ) = ∑ y i \begin {cases} \triangledown_wL(w,b) = \sum y_ix_i \\ \triangledown_bL(w,b) = \sum y_i\\ \end {cases} {wL(w,b)=yixibL(w,b)=yi

参数更新:
{ w k + 1 : = w k + ϵ ▽ w L ( w , b ) b k + 1 : = b k + ϵ ▽ b L ( w , b ) \begin {cases} w_{k+1} := w_k + \epsilon\triangledown_wL(w,b) \\ b_{k+1} := b_k + \epsilon\triangledown_bL(w,b) \\ \end {cases} {wk+1:=wk+ϵwL(w,b)bk+1:=bk+ϵbL(w,b)
上面 ϵ \epsilon ϵ 为学习率。

思考

可能会想,将分类正确的 l k ( w , b ) + l^k(w,b)_+ lk(w,b)+ 加如进 L ( w , b ) L(w,b) L(w,b) ,会不会导致训练使参数一味最求使得 l k ( w , b ) + l^k(w,b)_+ lk(w,b)+ 来优化,导致训练的目的发生变化?

查阅其他人的做法,有只将分类错误的 l k ( w , b ) − l^k(w,b)_- lk(w,b) 加和起来,通过梯度下降使其尽可能的小,似乎这更符合直观上的训练。

但我想了想, ∑ i = 0 n w i x i = z \displaystyle\sum_{i=0}^nwixi = z i=0nwixi=z 本身为一线性运算,激励函数也为一简单的阈值函数,模型训练一味追求 l k ( w , b ) + l^k(w,b)_+ lk(w,b)+ 更大,也不会影响模型训练的目的,因为对于 z ( w , b ) z(w,b) z(w,b) 为一线性的,使 l k ( w , b ) + l^k(w,b)_+ lk(w,b)+ 更大的参数,对应也会使 l k ( w , b ) − l^k(w,b)_- lk(w,b) 对应更大,使 l k ( w , b ) − l^k(w,b)_- lk(w,b) 超过阈值,从分类错误到分类正确,同样符合训练目标。

Python 实现

2021年11月16日:
这里我写了最新版本的代码,训练速度快很多,下面这部分代码若非兴趣,不用看了!

下面的代码,跑起来很慢,训练效果也不是很好。建议看最新的↑。


参考代码:
Rocky1ee/Perceptron-Model: 感知机模型

上面参考代码是直接使用的sklearn 中的自带的 Perceptron ,我这里没有使用,采用自己实现。但参考了它生成测试数据、画图的部分。

必要模块导入

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt

from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
#from sklearn.linear_model import Perceptron

  • make_classification 生成用于分类的数据,用以练习。
  • train_test_split 用于测试集、训练集分离。
  • Perceptron 感知器模型。
    我尝试只用前面两个模块,感知器模型尝试自己来写。

生成数据

#生成数据 (2,1000) 特征,(1,1000)输出
X,y = make_classification(n_samples=1000, n_features=2,n_redundant=0,n_informative=1,n_clusters_per_class=1,random_state= 2)

X_train,X_test,y_train,y_test = train_test_split(X,y,test_size = 0.2,random_state = 2)

make_classification 函数详解
sklearn.datasets.make_classification — scikit-learn 1.0.1 文档

datasets.make_classification(n_samples=100, 
							 n_features=20, 
							 n_informative=2, 
							 n_redundant=2, 
							 n_repeated=0, 
							 n_classes=2,
							 n_clusters_per_class=2, 
							 weights=None, 
							 flip_y=0.01, 
							 class_sep=1.0, 
							 hypercube=True, 
							 shift=0.0, 
							 scale=1.0, 
							 shuffle=True, 
							 random_state=None)

主要参数:

  • n_samples 样本个数
  • n_features 特征个数= n_informative + n_redundant + n_repeated + 噪声
  • n_informative:有效特征的个数
  • n_redundant:冗余信息,informative特征的随机线性组合
  • n_repeated :重复信息,有效特征和冗余特征的随机组合
  • n_classes:分类类别
  • n_clusters_per_class :某一个类别是由几个cluster构成的
  • shuffle : bool 默认为 True 是否打乱样本和特征
  • random_state : 用于可复现的随机数种子。

train_test_split 用于训练集和验证集的分离。

判断是否线性可分

#绘图显示
#将数据进行分类 f1,f2为样本的特征
positive_f1 = [X[i,0] for i in range(1000) if y[i] == 0]
positive_f2 = [X[i,1] for i in range(1000) if y[i] == 0]
negetive_f1 = [X[i,0] for i in range(1000) if y[i] == 1]
negetive_f2 = [X[i,1] for i in range(1000) if y[i] == 1]
 
plt.figure()
plt.scatter(positive_f1,positive_f2,c = 'red',alpha=0.1)
plt.scatter(negetive_f1,negetive_f2,c = 'blue',alpha=0.1)

可以看出,差不多线性可分,所以可以采用感知器算法。

# numpy to pandas
X_train = pd.DataFrame(X_train)
X_test = pd.DataFrame(X_test)
y_train = pd.DataFrame(y_train)
y_test = pd.DataFrame(y_test)

y_train[y_train[0] == 0] = -1
y_test[y_test[0] == 0] = -1

我更习惯在 pandas 的 DataFrame 上操作,所以这里将 numpy的数据类型转成了 DataFrame。 并把输出label 的 0 替换为了-1,方便之后优化。

实际上,这里其实用 numpy 更好, 我不应该转换的。 numpy 在矩阵运算上更方便。

类 Perceptron



class Perceptron(object):
    def __init__(self,w_dim,b0 = 0,l_rate = 0.01,epoch = 1000):
        self.w = np.ones(w_dim,dtype=np.float32) # w_dim = len(X_train.columns)
        self.b = b0
        self.l_rate = l_rate   # 先固定学习率
        self.epoch = epoch
        self.accuracy = []

    def sign(self,x,w,b) -> int: # 定义符号函数
        y = np.dot(x,w) + b
        y = float(y)
        if y >= 0:
            return 1
        else:
            return -1
    def Weighted_sum(self,x,w,b) -> float:
        y = np.dot(x,w) + b
        return y
    
    def fit(self,x_train,y_train):
        #
        self.accuracy.clear()
        accuracy_temp = 0
        for iter_ in range(self.epoch):
            for i in range(len(x_train)):
                xi = x_train.iloc[i]
                yi = y_train.iloc[i]
                accuracy_temp += float(yi) * float(self.Weighted_sum(xi, self.w, self.b))
                # SGD
                # if  self.sign(xi, self.w,self.b) < 0: // 我发现没有这个条件训练的更快,甚至效果更好
                self.w += np.dot(xi,float(yi)) * self.l_rate
                self.b += yi * self.l_rate
            self.accuracy.append(float(accuracy_temp))
            if iter_%10 == 0:
                print('time :',iter_,'accuracy: ',float(accuracy_temp))
                print('w = ',list(self.w),'b=',float(self.b))
                y_predict = perceptron.predict(X_test)
                print('score : ',perceptron.score(y_predict, y_test))
            accuracy_temp = 0
    def predict(self,x_test):
        y = []
        for i in range(len(x_test)):
            xi = x_test.iloc[i]
            y.append(self.sign(xi, self.w,self.b))
        return y.copy()

    def score(self,y,label):
        accuracy = 0
        for i in range(len(y)):
            if  y[i] == label.iloc[i][0]:
                accuracy += 1
        return accuracy / len(label)

    pass

主函数 main



if __name__ == '__main__':

    perceptron = Perceptron(len(X_train.columns),epoch= 10,l_rate= 0.01)
    perceptron.fit(X_train, y_train)
    y_predict = perceptron.predict(X_test)
    print('score : ',perceptron.score(y_predict, y_test))
    # 可视化
    #绘图显示
    #将数据进行分类 f1,f2为样本的特征
    positive_f1 = [X_test.iloc[i][0] for i in range(len(X_test)) if y_test.iloc[i][0] == 1]
    positive_f2 = [X_test.iloc[i][1] for i in range(len(X_test)) if y_test.iloc[i][0] == 1]
    negetive_f1 = [X_test.iloc[i][0] for i in range(len(X_test)) if y_test.iloc[i][0] == -1]
    negetive_f2 = [X_test.iloc[i][1] for i in range(len(X_test)) if y_test.iloc[i][0] == -1]

    # positive_f1_pre = [X_test.iloc[i][0] for i in range(len(X_test)) if y_predict[i] == 1]
    # positive_f2_pre = [X_test.iloc[i][1] for i in range(len(X_test)) if y_predict[i] == 1]
    # negetive_f1_pre = [X_test.iloc[i][0] for i in range(len(X_test)) if y_predict[i] == -1]
    # negetive_f2_pre = [X_test.iloc[i][1] for i in range(len(X_test)) if y_predict[i] == -1]

    mistake_f1_pre = [X_test.iloc[i][0] for i in range(len(X_test)) if y_predict[i] != y_test.iloc[i][0]]
    mistake_f2_pre = [X_test.iloc[i][1] for i in range(len(X_test)) if y_predict[i] != y_test.iloc[i][0]]


    fig = plt.figure(num=1,figsize=(14,6))
    ax1 = fig.add_subplot(121)

    ax1.scatter(positive_f1,positive_f2,c = 'red',alpha=0.5)
    ax1.scatter(negetive_f1,negetive_f2,c = 'blue',alpha=0.5)

    line_x = np.linspace(-0.5,0.5,100)  # 这里的范围,方便显示可以自己调

    line_w = -1*(perceptron.w[0]/perceptron.w[1])
    line_b = -1*(float(perceptron.b)/perceptron.w[1])

    line_y = list(map(lambda x : x*line_w + line_b ,line_x))

    ax1.plot(line_x,line_y,c = 'orange')

    ax2 = fig.add_subplot(122)

    # ax2.scatter(positive_f1,positive_f2,c = 'green',alpha=0.5)
    # ax2.scatter(negetive_f1,negetive_f2,c = 'orange',alpha=0.5)

    ax2.scatter(mistake_f1_pre,mistake_f2_pre,c = 'orange',alpha=0.5)
    ax2.plot(line_x,line_y,c = 'orange')


    print(line_w,line_b)

    plt.show()

    

   

输出:

实现结论

我用 sklearn.datasets.make_classification 生成了大小为 1000 的数据集,并在其中按照 8 : 2 8:2 8:2 的比例随机拆分为了训练集和验证集。

在训练过程中,发现可能是因为由 make_classification 生成的数据集太理想,在学习率固定为 0.01 ,通过随机梯度下降进行 1个 epoch 的训练,即可得到非常好的效果,事实上,在 epoch为1,l_rate 为 0.01 时得到准确率为 90% 已经是我多次测试得到最好的结果。

实际上,我发现改变数据集的随机数种子,生成其他的数据集,相同的感知器模型参数,最后得到的结果甚至有出现0.9以上的,如:

end ,若有哪里不对,请尽情指出,多谢😀

  • 8
    点赞
  • 85
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值