这边建议异步到第二版感知器算法及其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=0∑nwixi=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′={+1−1ififz≥0z<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=0∑nwixik)=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=1∑mlk(w,b)=k=1∑mykzk
故,输出的准确率越高,
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)=∑yixi▽bL(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=0∑nwixi=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以上的,如: