什么是感知器算法

感知器的产生源于对单个生物神经元的行为进行建模的愿望。该算法是最简单和最古老的神经网络类型,其根源可以追溯到 1940 年代。它旨在用于分类问题。

感知器算法的基础是由 Warren McCulloch 和 Walter Pitts 在他们 1943 年的论文“神经活动中内在的思想的逻辑演算”中建立的。他们的工作构思了使用二进制阈值激活函数根据收到的输入做出逻辑决策的想法。弗兰克·罗森布拉特(Frank Rosenblatt)扩展了这些想法,开发了我们今天所认识的第一个感知器。他的工作发表在1962年的《神经动力学原理》一书中,极大地普及了这种算法。

马文·明斯基(Marvin Minsky)和西摩·帕珀特(Seymour Papert)在他们1969年出版的《感知器:计算几何导论》(Perceptrons: An Introduction to Computational Geometry)一书中,说明了感知器实际作用的局限性。这些包括对线性可分离性的依赖,以及这些算法无法学习由环绕函数转换的模式。

多层神经网络的最终发展,其中各种隐藏层用于自动学习解决了 Minsky 和 Paper 提出的问题。这一成就开启了神经网络从 1980 年代到现在的快速发展。

假设和注意事项

在考虑此算法时要记住以下几点:

  • 感知器高度依赖于输入到其中的特征。最重要的是,数据中的类需要是线性可分离的。因此,在训练模型之前,必须注意特征工程。
  • 感知器旨在处理监督分类问题。
  • 该算法相对简单,易于实现。因此,训练进行得很快,并且该算法可以很好地扩展到非常大的数据集

感知器算法

为了简单起见,我只考虑标签的二元分类问题python中的感知器算法_二进制阈值激活函数.我们可以通过一个简单的可视化来了解这个算法的结构:

python中的感知器算法_感知器_02

图 1:由 3 个输入特征组成的感知器图示。

python中的感知器算法_模型学习_03

                                                                                偏置项python中的感知器算法_二进制阈值激活函数_04也存在

python中的感知器算法_感知器_05

在 Python 中构建和测试感知器

现在,我们将尝试实现上一节中描述的算法。完成此操作后,我们将在 2 个玩具数据集上测试我们的自定义实现。此外,我们还将把我们的实现性能与scikit-learn提供的感知器进行比较。

首先,让我们导入实现和测试所需的所有包:

# imports
import numpy as np
from typing import Dict, List
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from sklearn.datasets import make_blobs, make_circles
from sklearn.metrics import accuracy_score, \
                            precision_score, \
                            recall_score, \
                            f1_score


从头开始实现感知器

我们的 Perceptron 实现将封装在单个 Python 类中。此实现的完整内容如下:

class Perceptron(object):
    """
    Class to encapsulate the Perceptron algorithm
    """
    
    def __init__(self, lr: float=1e-2, max_epochs: int=100, tol: float=1e-3, verbose: bool=False) -> None:
        """
        Initialiser function for a class instance
        
        Inputs:
            lr         -> learning rate; factor to determine how much weights are updated at each pass
            max_epochs -> maximum number of epochs the algorithm is allowed to run before stopping
            tol        -> tolerance to stop training procedure
            verbose    -> boolean to determine if extra information is printed out
        """
        if lr <= 0:
            raise ValueError(f'Input argument lr must be a positive number, got: {lr}')
        if max_epochs <= 0:
            raise ValueError(f'Input argument max_epochs must be a positive integer, got: {max_epochs}')
        if tol <= 0:
            raise ValueError(f'Input argument tol must be a positive number, got: {tol}')
        self.lr            = lr
        self.epochs        = max_epochs
        self.tol           = tol
        self.w             = np.array([])
        self.verbose       = verbose
        self.training_loss = []
        if self.verbose:
            print(f'Creating instance with parameters: lr = {self.lr}, max_epochs = {self.epochs}, and tol = {self.tol}')
        
    def __del__(self) -> None:
        """
        Destructor function for class instance
        """
        del self.lr
        del self.epochs
        del self.tol
        del self.w
        del self.verbose
        del self.training_loss
        
    def fit(self, X : np.array, y : np.array) -> None:
        """
        Training function for the class. Runs the training algorithm for the number of epochs set in the initialiser 
        
        Inputs:
            X -> numpy array of input features of assumed shape [number_samples, number_features]
            y -> numpy array of binary labels of assumed shape [number_samples]
        """    
        # add column for bias
        X = np.insert(X, 0, 1, axis=1)
        # initialise the weights
        self.w = np.zeros(X.shape[1])
        # initialise loss
        loss = 1e8
        
        # loop through the training procedure for the specified number of epochs
        for e in range(self.epochs):
            
            epoch_loss = 0
            
            # loop through each training sample
            for x_i,y_i in zip(X,y):
    
                # compute activation 
                z  = np.dot(x_i,self.w)
                yp = np.round(z >= 0)
                # update weights
                self.w += self.lr*(y_i-yp)*x_i
                # record the loss
                epoch_loss += np.abs(y_i-yp)
                
            # check for early stopping
            new_loss = (1/X.shape[0])*epoch_loss
            if (loss < (new_loss - self.tol)) or (new_loss < self.tol):
                if self.verbose:
                    print(f'Early stopping training procedure at Epoch: {e}, Loss: {new_loss}')
                break
            else:
                loss = new_loss
                self.training_loss.append(loss)
                if self.verbose:
                    print(f'Epoch: {e}, Loss: {loss}')
            
    def predict(self, X : np.array) -> np.array:
        """
        Predict function for the class. Assigns class label based on learned weights and the binary threshold 
        activation
        
        Input:
            X -> numpy array of input features of assumed shape [number_samples, number_features]
        Output:
            numpy array indicating class assignment per sample with shape: [number_samples,]
        """
        if self.w.size == 0:
            raise Exception("It doesn't look like you have trained this model yet!")
        # add column for bias
        X = np.insert(X, 0, 1, axis=1)
        # compute dot products for all training samples
        z = np.einsum('ij,j->i', X, self.w)
        # pass through activation function, and return
        yp = np.round(z >= 0)
        return yp
    
    def get_weights(self) -> np.array:
        """
        Function to return model weights
        
        Output:
            numpy array containing the model weights
        """
        return self.w
    
    def get_training_loss(self) -> List:
        """
        Function to return training loss 
        
        Output:
            list containing the training loss, one entry per epoch
        """
        return self.training_loss
    
    def get_params(self, deep : bool = False) -> Dict:
        """
        Public function to return model parameters
        Inputs:
            deep -> boolean input parameter
        Outputs:
            Dict -> dictionary of stored class input parameters
        """
        return {'lr':self.lr,
                'max_epochs':self.epochs,
                'tol':self.tol,
                'verbose':self.verbose}
  • __init__(self, lr, max_epochs, tol, verbose) :这是每个类实例的初始化器函数。学习率 (lr) 定义了每次训练迭代对权重的更新量。重复训练过程的次数(即纪元)由参数 max_epochs 定义。训练过程的提前停止由 tol 控制,它为我们的损失函数设置了一个阈值。最后,详细控制是否在运行时打印出额外信息。所有内部类范围的变量都在此函数中初始化。
  • __del__(self) :每个类实例的析构函数。删除实例后释放分配的资源。
  • fit(self, X, y):用于执行本文上一节中概述的训练过程的公共函数,用于训练数据 X 和 y。需要注意的三件事:
  1. 我正在使用二进制阈值激活函数。
    1. 我假设输入特征 X 尚未针对偏差进行处理。因此,在此实现中,我向此矩阵中添加了一列 1。
    • predict(self, X) :用于从经过训练的模型实例生成预测的公共函数。预测基于输入特征样本 X 的数组。与拟合函数一样,这里我假设需要向 X 添加一列 1 以考虑偏差。
    • get_weights(self) :用于返回模型权重的公共函数。
    • get_training_loss(self) :用于返回训练损失的公共函数。
    • get_params(self, deep) :用于返回模型超参数的公共函数。如果我们想在类实例中使用任何 scikit-learn 功能,则此函数是必需的。

    数据集示例 #1

    让我们从创建第一个测试数据集开始,使用 scikit-learn 的make_blobs函数:

    # make a dataset
    X,y = make_blobs(n_samples=1000, centers=2, n_features=2, cluster_std=0.5, random_state=0)
    
    # visualise the data
    fig, ax = plt.subplots(figsize=(10,8))
    sc      = ax.scatter(X[:,0],X[:,1],c=y)
    plt.xlabel('x1')
    plt.ylabel('x2')
    plt.title('Distribution of Data with True Class Labels')
    ax.legend(*sc.legend_elements(), title='classes')
    plt.show()

    python中的感知器算法_二进制阈值激活函数_06

    图 2:测试数据集 #1。

    我们生成了一个由 1000 个样本、两个预测变量特征和 2 个类标签组成的数据集。从图 2 可以看出,这两个类是线性可分的。

    现在,让我们对数据进行训练测试拆分,声明感知器的实例,并对其进行训练:

    # do train test split
    X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
    
    # declare an instance of our Perceptron class
    model1 = Perceptron(lr=0.1, verbose=True)


    Creating instance with parameters: lr = 0.1, max_epochs = 100, and tol = 0.001
    # fit the model on our training data
    model1.fit(X_train,y_train)


    Epoch: 0, Loss: 0.01375
    Epoch: 1, Loss: 0.01375
    Epoch: 2, Loss: 0.00625
    Epoch: 3, Loss: 0.00625
    Epoch: 4, Loss: 0.00625
    Epoch: 5, Loss: 0.00375
    Epoch: 6, Loss: 0.0025
    Early stopping training procedure at Epoch: 7, Loss: 0.0

    20%的数据已被保留以供测试。我已将感知器的学习率设置为 0.1,并且还请求打开详细模式。让模型在详细模式下运行意味着训练损失被打印出来,我们可以看到在第 7 纪元达到提前停止,损失为 0.0。

    现在,让我们使用经过训练的模型在测试集上生成预测。接下来,我们可以使用准确性、精确度、召回率和 F1 分数来查看分类器的性能:

    # generate predictions on the test set
    y_pred = model1.predict(X_test)
    
    # how accurate are the predictions?
    acc = accuracy_score(y_test,y_pred)
    pre = precision_score(y_test,y_pred,average='weighted')
    rec = recall_score(y_test,y_pred,average='weighted')
    f1  = f1_score(y_test,y_pred,average='weighted')
    print(f'Accuracy score: {acc:.4f}')
    print(f'Precision score: {pre:.4f}')
    print(f'Recall score: {rec:.4f}')
    print(f'F1 score: {f1:.4f}')


    Accuracy score: 1.0000
    Precision score: 1.0000
    Recall score: 1.0000
    F1 score: 1.0000

    我们有一个满分!这不应该太令人惊讶:我们已经知道数据集是线性可分离的。此外,感知器学会了完美地区分训练集中的两个类。

    现在让我们利用 scikit-learn 提供的感知器重复这个分析:

    # check out what we get from the scikit-learn Perceptron?
    from sklearn.linear_model import Perceptron as sklearn_Perceptron
    
    model2 = sklearn_Perceptron(random_state=42)
    model2.fit(X_train,y_train)
    y_pred = model2.predict(X_test)
    
    acc = accuracy_score(y_test,y_pred)
    pre = precision_score(y_test,y_pred,average='weighted')
    rec = recall_score(y_test,y_pred,average='weighted')
    f1  = f1_score(y_test,y_pred,average='weighted')
    print(f'Accuracy score: {acc:.4f}')
    print(f'Precision score: {pre:.4f}')
    print(f'Recall score: {rec:.4f}')
    print(f'F1 score: {f1:.4f}')


    Accuracy score: 1.0000
    Precision score: 1.0000
    Recall score: 1.0000
    F1 score: 1.0000

    我们再次获得了满分,这同样不足为奇。到目前为止,我们的自定义实现的行为与 scikit-learn 模型的行为相匹配。

    数据集示例 #2

    现在,我们可以创建第二个测试数据集。但这次我们将利用 scikit-learn 的 make_circles 函数:

    # make a dataset
    X,y = make_circles(n_samples=1000, noise=0.03, random_state=0)
    
    # visualise the data
    fig, ax = plt.subplots(figsize=(10,8))
    sc      = ax.scatter(X[:,0],X[:,1],c=y)
    plt.xlabel('x1')
    plt.ylabel('x2')
    plt.title('Distribution of Data with True Class Labels')
    ax.legend(*sc.legend_elements(), title='classes')
    plt.show()


    python中的感知器算法_感知器_07

    图 3:测试数据集 #2。

    与前面的数据集不同,这里的两个类位于同心环中。因此,这些类不是线性可分的。现在我们的感知器表现如何?

    # do train test split
    X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
    
    # fit the model on our training data
    model1.fit(X_train,y_train)


    Epoch: 0, Loss: 0.48875
    Early stopping training procedure at Epoch: 1, Loss: 0.5025000000000001
    # generate predictions on the test set
    y_pred = model1.predict(X_test)
    
    # how accurate are the predictions?
    acc = accuracy_score(y_test,y_pred)
    pre = precision_score(y_test,y_pred,average='weighted')
    rec = recall_score(y_test,y_pred,average='weighted')
    f1  = f1_score(y_test,y_pred,average='weighted')
    print(f'Accuracy score: {acc:.4f}')
    print(f'Precision score: {pre:.4f}')
    print(f'Recall score: {rec:.4f}')
    print(f'F1 score: {f1:.4f}')


    Accuracy score: 0.4700
    Precision score: 0.2209
    Recall score: 0.4700
    F1 score: 0.3005

    很明显,分类器在这些数据上没有很好地发挥作用。我们的误差指标突出显示了经过训练的感知器的糟糕性能。此外,训练过程提前停止,因为损失在 1 个 epoch 后增加