神经网络实践介绍:从头开始在Python中实现单个神经元
在过去的十年中,人工智能(AI)已经深入公众关注的焦点,很大程度上归功于机器学习(ML)和人工神经网络(ANN)的进步。
但是,现在这个领域的噪音已经非常大。这就是为什么我认为回到基础并使用Python 实际实现一个单独的神经元是有用的。
人工神经元
在我们进入之前,我只想快速谈论一个神经元是什么。早期的人工智能支持者注意到生物神经元能够从大量数据中进行概念化和学习,并假设在机器中对这个神经元进行建模可能会产生类似的能力。为此,神经元被抽象为模型的输入,输出和权重。
图1:简单的神经元模型
在机器学习术语中,神经元(x1,x2,... xn)的每个输入被称为特征,每个特征用一个数字加权以表示该输入(w1j,w2j,... wnj)的强度。输入的加权和(netj)然后通过激活函数, 其通用目的是通过根据公式将加权和转换成新数字来模拟生物神经元的“firing rate”。
如果这就是神经元的工作原理,我们来看看它是如何学习的。简单地说,训练神经元是指迭代地更新与每个输入相关的权重,以便在给定的数据集中逐步逼近底层关系。一旦经过适当的训练,一个神经元就可以被用来做正确的事情,比如,把猫和狗的图像完全归类到不同的类,就像人们一样。在机器学习术语中,这被称为分类。
训练
为了训练一个简单的分类器,我们使用公开可用的sklearn breast cancer数据集(http://scikit-learn.org/stable/modules/generated/sklearn.datasets.load_breast_cancer.html),它具有以下属性:
数据集中的每个样本都是乳房肿物的图像,已被翻译成30个数字(特征)。使用一部分样本来训练我们的神经元,我们将会看看它是否能将乳房肿块的不可见部分分类为恶性或良性。换句话说,我们需要执行监督式学习 任务,使用明确标记的数据点作为神经元的教师来学习相关模式。
要运行和修改下面的代码,请查看脚本:single-layer-perceptron.py(https://github.com/dguliani/neural-network-tutorials/blob/master/single-layer-perceptron.py)
首先,我们加载数据集,随机地将恶性和良性的样本混合在一起,同时保持每个样本的标签。这是因为我们不希望我们的神经元根据它所看到的样本的顺序来得出结论——只根据每个样本的特征。
# Using scikit-learn's breast cancer dataset: classifying cell features as malignant or benign
bc = datasets.load_breast_cancer()
x = bc['data'] # array of features with shape: (n_samples, n_features)
y = bc['target'] # array of binary values for the two classes: (n_samples)
shuffle_ind = np.random.permutation(len(x)) # shuffle data for training
x = np.array(x[shuffle_ind])
# convert y to a column to make it more consistent with x
y = np.array(y[shuffle_ind])[np.newaxis].T
x /= np.max(x) # linearly scaling the inputs to a range between 0 and 1
train_fraction = 0.75
train_idx = int(len(x)*train_fraction)
x_train = x[:train_idx]
y_train = y[:train_idx]
x_test = x[train_idx:]
y_test = y[train_idx:]
为了训练我们的神经元,我们基本上需要做三件事:
- 1.请神经元对样本进行分类。
- 2.根据预测的错误更新神经元的权重。
- 3.重复。
由于神经元本质上只是权重的集合,我们可以使用Python的矩阵操作包numpy来随机初始化权重向量。权重的数量对应于初始化功能(输入)神经元的数量,每个特性是加权求和。
# initialize single layer weights randomly with mean 0: single neuron in layer
W = 2*np.random.random((np.array(x).shape[1], 1)) - 1
Forward Pass
在训练的第一步,我们要求神经元对训练样本进行预测。这就是所谓的正向传递(Forward Pass),它包含了输入特性的加权和,并通过激活函数传递这个和。
# forward pass
l0 = x_train # layer 0 output
l1 = sigmoid(np.matmul(l0, W)) # perceptron output
上面代码片段中的l0是具有形状的特征矩阵(n_samples * n_features)。代表我们单个神经元的权重是形状的(n_features * 1)。因此,这两个矩阵之间的矩阵乘法将为每个样本提供所有特征的加权和。(如果你亲自尝试这个,你会发现它并不像听起来那么复杂。)
当通过激活函数时,这些加权和会有效地成为每个训练样本的类别预测。
Sigmoid函数
sigmoid函数是logistic函数的一个特殊情况,在这里选择它作为激活函数有以下几个原因:它是容易微分的,非线性的,有界的,具有以下形状和定义:
Figure 2: Sigmoid function shape.
图3:Sigmoid函数定义
该函数由一行实现,如下所示:
def sigmoid(x):
return 1/(1+np.exp(-x))
一个标准的logistic函数有一个轻松的导数计算形式
图4:Sigmoid函数的导数
其中f(x)表示logistic函数。正如我们将很快看到的,当试图最小化我们神经元预测中的错误时,这个属性非常有用。
sigmoid函数的导数实现如下:
def dsigmoid(y):
return y*(1-y)
梯度下降
现在来看看有趣的(也是棘手的)部分 - 实际上让我们的神经元学习数据集中的基础关系。现在我们已经对每个训练样本进行了有界预测,我们可以计算这些预测中的误差 / 损失,并更新与此误差成正比的神经元权重。
我们将为此权重更新使用梯度下降优化算法。为了使用这种算法,我们需要一个误差函数来表示我们的神经元预测和地面实况之间的差距。这个错误函数被定义为均方误差的缩放版本(缩放使得区分变得容易)。
图5:均方误差函数
在代码中,这个均方误差函数的实现如下:
def mse(y, pred):
return np.mean(np.square(y - pred))/2
在数学中,梯度是一个函数的导数向量,它依赖于多个变量。回想一下,向量既有大小又有方向。我们的神经元的误差依赖于输入的所有权重。梯度是误差的偏导对每个权值的偏导。
作为一个矢量,梯度点在函数增长速度最大的方向上。因此,向与梯度方向相反的方向移动,可以使函数最小化。如果我们能够计算出神经元的误差函数相对于每一个权重的梯度,我们就可以按比例更新权重以最小化误差。把误差函数想象成一个有山脊和山谷的曲面。通过与梯度相反的下降,我们进入了误差较低的山谷。
下面是使用链式法则的误差函数梯度的简单推导。
图6:对每个神经元权重的误差的偏导数
这里,E是误差函数,wij是一个特定权重,oj是神经元的输出,netj是神经元输入的加权和。指数i和j分别对应于权重和神经元。我们将分别计算偏导数的每个因子。
第一个因子很简单,是误差对神经元输出的导数:
图7:与神经元输出有关的输出误差的导数
第二因子也是简单,并且是图4中所述的sigmoid函数的导数
图8:相对于加权和的神经元输出的导数
第三个也是最后一个因子简化了等于特定神经元的输入
图9:相对于每个权重的输入的加权总和的偏导数
在图9中,oi是该神经元输入的向量,在我们的例子中是来自我们训练集的特征。
权重更新规则
将我们刚刚看到的偏导数与下降结合起来,给我们更新表示我们神经元的权重的规则:
图10:权重更新规则
图10显示每个权重将在梯度的负方向上更新,与附加项n成比例。比例因子n决定了更新神经元权重时我们采取的步骤有多大,从而有效地控制了神经元学习的速度。我们称之为学习率。
实现权重更新
下面是我们单个神经元的梯度计算和权重更新的实现。您可以根据注释查找权重更新规则所需的每一步导数。
lr = 0.5 # learning rate
epochs = 600
for iter in range(epochs):
# forward pass
l0 = x_train # layer 0 output
l1 = sigmoid(np.matmul(l0, W)) # layer 1 output
# backward pass
l1_error = l1 - y_train # output layer error, (dE/do)
l1_gradient = dsigmoid(l1) # (do/dnetx)
l1_delta = np.multiply(l1_error, l1_gradient) # (dE/do * do/dnetx = dE/dnetx)
l1_weight_delta = np.matmul(l0.T, l1_delta) # (dnetx/dwij * dE/dnetx = dE/dwij)
# update weights with a scaling factor of learning rate
W -= l1_weight_delta * lr
在训练神经网络的同时,相同的训练数据通过网络多次运行,每次全程都被称为epoch。在每个epoch,权重会进一步更新以尝试降低误差。对于我们这个简单的例子,通过反复试验来选择epoch的数量和学习率,观察它们的损失减少和收敛。
结果
Figure 11: Training results.
图11显示,在数百个epochs,训练数据集和测试数据集的损失减少,准确性增加。接下来,我们通过在相同数据集上训练10个不同的随机初始化神经元来检查这个训练过程是否可重复。在10次训练结束时,平均测试准确度为90.49%(s = 2.40%),平均总准确率为90.33%(s = 0.304%)。
如果我们发现训练和测试精度差异很大,或者如果测试损失增加而训练损失减少,我们就有理由相信神经元没有学习隐藏在数据集中的模式。虽然这种验证水平还不足以将此神经元放入生产环境,但迹象表明神经元已经在数据集中学习了一种模式。
结论
我们在这里查看了人造神经网络的最简单形式,即具有由梯度下降驱动的单个神经元的网络。网络可以由许多神经元或其他可训练的滤波器/单元组成,并根据其目的使用各种loss和激活功能。所有这些扩展允许ANN执行广泛的任务,如对象检测,语言翻译,时间序列预测等。