Python 实战人工智能数学基础:神经网络

47 阅读11分钟

1.背景介绍

人工智能(Artificial Intelligence)简称AI,它是一个通过模拟智能行为人的能力,使得计算机能够像人类一样解决各种复杂的任务。近几年,基于机器学习的AI技术已经应用到各个领域,如图像识别、语音识别、自然语言理解等。本文将以神经网络(Neural Network)作为典型的例子来阐述如何运用数学理论和编程技术来实现AI。

神经网络由多个节点组成,每个节点接受上一层的输入信号并产生输出信号,输出信号会根据接收到的信息调整权重,并影响下一层节点的输入。因此,神经网络中的权重可以形象地理解为一个超大的连接矩阵,每一个权重对应于上一层某个节点输出和当前层某个节点的连接。在训练过程中,通过调整权重的值来优化网络的输出,使其达到预期的效果。

2.核心概念与联系

2.1 激活函数与导数

在神经网络中,激活函数(activation function)对神经元的输出进行非线性变换,目的是为了将网络的输入转换为可以用于后续计算的输出值。激活函数基本可以分为以下几种:

  1. Sigmoid函数: Sigmoid函数是S型曲线,当z值的绝对值超过阈值时,输出趋于1;而当z值较小时,输出趋向于0。在神经网络的激活函数中,Sigmoid函数通常用来将输入转换为概率值。它的公式为: f(z)=11+ezf(z)=\frac{1}{1+e^{-z}}

    • z: 神经元的输入总和或前一层节点输出的加权之和
    • f(z): 神经元的输出值
  2. tanh函数: tanh函数类似于Sigmoid函数,但是它的输出范围是-1到1之间,相比Sigmoid函数更加平滑。它的公式为: tanh(x)=exexex+ex=sinh(x)cosh(x)tanh(x)=\frac{e^x-e^{-x}}{e^x+e^{-x}}=\frac{\sinh(x)}{\cosh(x)}

  3. ReLU函数: Rectified Linear Unit(ReLU)函数是目前最流行的激活函数。它一般不参与反向传播,也不需要求导,因此训练速度快。ReLU函数的公式为: ReLU(x)=max(0,x)\text{ReLU}(x)=\max (0, x)

  4. Leaky ReLU函数: Leaky ReLU函数是在ReLU函数的基础上加入了一个斜率因子,以缓解负输入导致的死亡现象。它的公inary形式为: LeakyReLu(x)=max(αx,x)\text{LeakyReLu}(x)=\max (\alpha x, x)

    • alpha: 斜率因子,默认为0.01。

2.2 多层感知器(MLP)

多层感知器(Multi-Layer Perceptron, MLP)是一种非常简单的神经网络模型,它由多个隐藏层(Hidden Layer)构成。其中,输入层(Input Layer)接受原始输入数据,中间层(Intermediate Layer)由多个神经元组成,最后一层(Output Layer)输出结果。隐藏层的神经元数量越多,则网络的表示能力越强,就越适合处理复杂的问题。

假设有一个有m个输入特征的样本x=(x1,x2,...,xm),那么:

  1. 在输入层,每个输入特征都送入一个神经元。
  2. 在隐藏层,每个神经元都对所有输入特征做一次线性组合,再加上偏置项b,然后再通过激活函数计算输出a。
  3. 对输出层,每个神经元都对上一步的输出a做一次线性组合,再加上偏置项b,得到最终的输出y。

如下图所示:

3.核心算法原理和具体操作步骤以及数学模型公式详细讲解

3.1 感知机模型

感知机(Perceptron)是最早被提出的判别模型之一,其基本思想是从输入空间映射到输出空间,其特点是简单、易于实现和抽象化。我们假设输入空间X与输出空间Y存在一一对应的一个可行的函数f(x)。感知机的基本模型如下:

f(x) = sign(\sum_{j=1}^n w_jx_j + b) \tag{1}

其中,wjw_jbb为权重和偏置项,j=1nwjxj\sum_{j=1}^nw_jx_j表示输入信号的线性组合。符号函数sign(.)用来确定神经元的输出,如果输入信号的线性组合的结果大于等于0,则输出为1,否则输出为-1。

给定训练数据集T={(x_1,y_1),(x_2,y_2),...,(x_N,y_N)},其中,xi∈X为输入向量,yi∈Y为期望的输出标记(一般取-1或1),如果f(xi)!=yi,则称为“错误的分类”,可以通过梯度下降法或其他方式来更新权重参数,直到误分类的数据被分对,即

\min_{\theta} J(\theta)=-\frac{1}{N}\sum_{i=1}^Ny_i(wx_i+\theta_0)\tag{2}

其中,θ=[w,θ0]\theta=[w,\theta_0]为权重参数向量。J()为损失函数,也叫做对数似然损失或交叉熵损失,表示模型在给定的训练数据上的性能。这里注意,我们也可以把线性回归问题看作是一种特殊情况的感知机问题,因为线性回归问题中的输出空间为R。

另外,如果限制wjw_j的取值范围,比如±\pm\infty,或者wjw_j00(不参与训练),那么这个问题就退化为最大化最小化单个类的误差概率,即

\max_{\theta} P_{err}(\theta)=\prod_{i=1}^{N}[1-(f(x_i)+y_i)]\tag{3}

其中,Perr(θ)P_{err}(\theta)表示模型在给定参数θ\theta下的错误分类的概率,也就是模型在实际使用时的性能。

3.2 BP算法

BP算法是神经网络中最常用的训练算法之一。它的基本原理是误差逆传播法,就是通过迭代的方式不断修正权重参数,使得网络的输出误差最小化。BP算法包括输入层到隐藏层、隐藏层到输出层和隐藏层之间的权重更新三个阶段。

3.2.1 输入层到隐藏层

首先,输入层的输出信号a(0)a^{(0)}通过激活函数得到:

a^{(0)} = g(W^{0}x+b^{0})\tag{4}

其中,gg为激活函数。然后,a(0)a^{(0)}传递至隐藏层。

3.2.2 隐藏层到输出层

对于隐藏层的输出信号z(l)z^{(l)},我们需要通过权重WlW^{l}和偏置项blb^{l}计算出来:

z^{(l)} = W^{l}a^{(l-1)}+b^{l}\tag{5}

3.2.3 输出层到隐藏层

隐藏层的输出信号a(l)a^{(l)}通过激活函数得到:

a^{(l)} = g(z^{(l)})\tag{6}

3.2.4 更新权重

最后,通过代价函数(损失函数)计算出错误率,更新权重的参数。我们要让目标函数尽可能降低,所以我们希望误差逆传播法能使得每层单元的误差逐渐减少。这就可以通过以下步骤完成:

  1. 首先计算隐藏层输出信号z(l)z^{(l)},此时误差项δ(l)\delta^{(l)}未知,用暂态变量z~(l)\tilde{z}^{(l)}代替,此时隐藏层的输出信号仍然不用乘以激活函数:

    \tilde{z}^{(l)} := a^{(l-1)}\tag{7}

  2. 计算输出层的误差项δ(L)\delta^{(L)},此时输出层单元的输出已知,目标函数为:

    \delta^{(L)} = \nabla_{a^{(L)}} C(a^{(L)}, y)\tag{8}

    其中,C()为损失函数。

  3. 计算隐藏层的误差项δ(l)\delta^{(l)},此时隐藏层单元的输出为z(l)z^{(l)},误差项为:

    \delta^{(l)} = \left[(\delta^{(l+1)}W^{\prime l})^\top\circ\sigma'(z^{(l)})\right]\odot\delta^{(l+1)}\tag{9}

    σ(z(l))\sigma'(z^{(l)})表示sigmoid函数的导数。

    \odot表示按元素相乘。

  4. 使用链式法则计算权重参数的偏导数,并更新它们,如更新权重矩阵WlW^{l}

    W^{\prime}^{(l)} := W^{(l)}-\eta \delta^{(l+1)}a^{\prime (l-1)}\tag{10}

    a(l1)a^{\prime (l-1)}表示上一层的激活函数的导数。

    η\eta为学习速率。

  5. 使用同样的方法计算偏置项blb^{\prime l},更新它们:

    b^{\prime}^{(l)} := b^{l}-\eta \delta^{(l+1)}\tag{11}

    至此,一次迭代结束。

3.3 CNN卷积神经网络

卷积神经网络(Convolutional Neural Networks, CNN)是神经网络的一个重要研究方向,主要用于处理图像和视频等多维数据。CNN利用不同尺寸的卷积核(Convolution Kernel)对输入数据进行过滤,从而提取数据的局部特征。在CNN中,卷积核的大小往往是一个奇数,这样可以保证卷积后的尺寸与输入的尺寸一致,便于网络进行局部特征的学习与匹配。CNN的架构一般由卷积层、池化层、全连接层三部分组成,具体结构如下图所示:

3.3.1 卷积层

卷积层的主要功能是提取特征,由卷积操作和池化操作两部分组成。卷积操作即对卷积核与输入数据进行二次互相关运算,从而提取局部特征。池化操作则是对卷积后的结果进行整体Pooling,从而进一步减少模型的参数数量并防止过拟合。

具体来说,卷积操作由两个操作完成:

  1. 将卷积核与输入数据按大小卷积,得到输出特征图(Feature Map)。
  2. 应用激活函数(ReLU,Sigmoid等)将输出特征图非线性激活。

如下图所示:

3.3.2 池化层

池化层的主要作用是缩小图像的空间尺寸,从而避免过拟合。池化层的基本操作是选择一个区域,在该区域内取最大值,或者平均值,作为输出特征。池化层的大小可以设置为2×2,4×4,8×8等。

如下图所示:

3.3.3 CNN参数共享

CNN除了具有上面所说的卷积层、池化层、激活函数等基本组件之外,还引入了参数共享机制。这种机制允许不同的卷积核共享相同的参数,从而降低参数数量。具体来说,参数共享分两种:

  1. 空间参数共享:在相同尺寸的卷积核之间共享权重。
  2. 通道参数共享:在相同数量的通道(即输入通道数与输出通道数相同)的卷积核之间共享权重。

如下图所示:

3.3.4 卷积神经网络的训练过程

卷积神经网络的训练过程与BP算法非常相似,也是误差逆传播法的具体实现。

首先,输入层的输出信号a(0)a^{(0)}通过激活函数得到:

a^{(0)} = g(W^{0}x+b^{0})\tag{12}

其中,gg为激活函数。然后,a(0)a^{(0)}传递至卷积层。

对于卷积层,对输入数据进行卷积操作,得到输出特征图(Feature Map)。如下图所示:

对于池化层,对输出特征图进行整体Pooling,得到输出。如下图所示:

接着,通过全连接层,将池化后的特征连接到输出层,然后通过softmax激活函数将输出转换成概率分布。最后,通过代价函数(损失函数)计算出错误率,更新权重的参数,迭代训练。

4.具体代码实例和详细解释说明

4.1 线性回归

我们先来回顾一下线性回归的概念。线性回归模型描述的是两个变量间存在线性关系的连续型数据。它有如下几个特点:

  1. 有唯一的最优解:线性回归模型只涉及加法操作,没有除法操作,因此不存在除零等无意义的情况。同时,线性回归模型只有一个参数(weight),因此也无法同时刻画多个线性方程。所以,它只能找到一条最佳拟合线。

  2. 可解释性好:通过分析模型的系数,我们可以直观地理解模型的工作原理。通过这一特点,我们可以比较不同的模型间是否存在显著差异。

线性回归模型可以使用如下公式来表示:

\hat{y}=w_{0}+w_{1}x_{1}+\cdots+w_{p}x_{p}+\epsilon\tag{13}

其中,y^\hat{y}表示预测值,w0,w1,,wpw_{0},w_{1},\ldots,w_{p}分别表示权重,x1,,xpx_{1},\ldots,x_{p}表示输入数据。ϵ\epsilon表示误差项。

对于训练数据集T={(x_1,y_1),(x_2,y_2),...,(x_N,y_N)},其中,xi∈X为输入向量,yi∈Y为期望的输出标记,我们可以尝试用极大似然估计方法求解模型参数。

极大似然估计法表示如下:

L(\beta)=\prod_{i=1}^NL(y_i|x_i;\beta)\tag{14}

其中,L(yixi;β)L(y_i|x_i;\beta)为似然函数,表示在给定模型参数β\beta下,观测值yiy_i出现的概率。

要最大化似然函数,我们需要取它的对数,然后求导并令其为0,得到最优解。

\ln L(\beta)=\sum_{i=1}^N(y_i-w_{0}-w_{1}x_{i1}-\cdots-w_{px_ip})^2\tag{15}

求导并令其为0:

\frac{\partial}{\partial w_{j}}\ln L(\beta)=\sum_{i=1}^N(y_iw_{j}x_{ij}-1)x_{ij}\\[1ex]=-\sum_{i=1}^Nx_{ij}(y_iw_{j}x_{ij}-y_ix_{ij})\\[1ex]=0 \\[1ex] \implies w_{j}=\frac{\sum_{i=1}^N(y_ix_{ij}x_{ij}-y_ix_{ij})}{\sum_{i=1}^Nx_{ij}^2}\tag{16}

得到最优解:

w_{0}=\bar{y}-w_{1}\bar{x}_{1}-\cdots-w_{p}\bar{x}_{p}\tag{17}

其中,yˉ,xˉ1,,xˉp\bar{y},\bar{x}_{1},\ldots,\bar{x}_{p}为样本均值。

接下来,我们使用scikit-learn库来实现线性回归的代码。首先导入库:

from sklearn import linear_model
import numpy as np

然后准备数据:

x = [[1], [2], [3]]
y = [2, 4, 6]

定义模型对象:

reg = linear_model.LinearRegression()

拟合模型:

reg.fit(x, y)

预测新数据:

new_data = [[3], [5], [7]]
pred = reg.predict(new_data)
print(pred) # Output:[8.  11.  14.]

4.2 感知机算法

我们可以用Python来实现感知机算法。首先,导入必要的库:

import numpy as np

然后准备数据:

def load_data():
    '''load data'''
    X = np.array([[1, 0],
                  [1, 1],
                  [-1, -1],
                  [-1, 0]])
    
    Y = np.array([[-1],[1],[-1],[1]])
    
    return X, Y
    
X, Y = load_data()

定义感知机模型:

class PerceptronModel:
    def __init__(self):
        self.weights = None
        
    def fit(self, X, Y, learning_rate=0.1, epochs=1000):
        n_samples, n_features = X.shape
        
        if not self.weights:
            self.weights = np.zeros((1, n_features))
            
        for epoch in range(epochs):
            delta_weights = []
            
            for i in range(n_samples):
                result = np.dot(self.weights, X[i])
                
                if (result * Y[i]) <= 0:
                    delta_weights.append(-learning_rate * Y[i] * X[i])
                    
            self.weights += sum(delta_weights) / len(delta_weights)
        
model = PerceptronModel()

训练模型:

model.fit(X, Y, learning_rate=0.1, epochs=1000)

预测新数据:

new_data = np.array([[1, 0], [1, 1], [-1, -1], [-1, 0]])

preds = model.predict(new_data)
print("Preds:", preds) #[-1  1 -1  1]

4.3 BP算法

我们可以用Python来实现BP算法。首先,导入必要的库:

import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline

然后准备数据:

def create_dataset():
    """create dataset"""
    np.random.seed(1)
    X = np.sort(5 * np.random.rand(40, 1), axis=0)
    y = np.sin(X).ravel()
    y[::5] += 3 * (0.5 - np.random.rand(8))
    
    return X, y

X, y = create_dataset()
plt.scatter(X, y);

定义BP模型:

class BpModel:
    def __init__(self, n_inputs, hidden_size, n_outputs):
        self.n_inputs = n_inputs
        self.hidden_size = hidden_size
        self.n_outputs = n_outputs

        # initialize weights with random values between 0 and 1
        limit = 1 / np.sqrt(self.n_inputs + self.hidden_size)
        self.W1 = np.random.uniform(-limit, limit, size=(self.hidden_size, self.n_inputs))
        self.b1 = np.zeros((self.hidden_size, 1))
        self.W2 = np.random.uniform(-limit, limit, size=(self.n_outputs, self.hidden_size))
        self.b2 = np.zeros((self.n_outputs, 1))

    def sigmoid(self, Z):
        """sigmoid activation function"""
        A = 1/(1 + np.exp(-Z))
        cache = Z
        return A, cache

    def relu(self, Z):
        """relu activation function"""
        A = np.maximum(0, Z)
        cache = Z
        return A, cache

    def forward_propagation(self, X):
        """forward propagation through the network"""
        Z1 = np.dot(self.W1, X) + self.b1
        A1, cache1 = self.relu(Z1)
        Z2 = np.dot(self.W2, A1) + self.b2
        A2, cache2 = self.sigmoid(Z2)
        caches = (cache1, cache2)
        return A2, caches

    def backward_propagation(self, AL, Y, caches):
        """backward propagation through the network"""
        cache1, cache2 = caches
        dZ2 = AL - Y
        dW2 = (1./self.m) * np.dot(dZ2, cache1.T)
        db2 = (1./self.m) * np.sum(dZ2, axis=1, keepdims=True)
        dZ1 = np.dot(self.W2.T, dZ2) * (cache1 > 0)
        dW1 = (1./self.m) * np.dot(dZ1, X.T)
        db1 = (1./self.m) * np.sum(dZ1, axis=1, keepdims=True)
        gradients = {"dW1": dW1, "db1": db1, "dW2": dW2, "db2": db2}
        return gradients

    def update_parameters(self, parameters, gradients, learning_rate):
        """update parameters of the network using gradient descent"""
        self.W1 -= learning_rate * gradients["dW1"]
        self.b1 -= learning_rate * gradients["db1"]
        self.W2 -= learning_rate * gradients["dW2"]
        self.b2 -= learning_rate * gradients["db2"]

    def train(self, X, y, learning_rate=0.1, num_iterations=1000):
        """train the neural network"""
        costs = []
        self.m = X.shape[0]
        for i in range(num_iterations):
            # Forward propagation
            AL, caches = self.forward_propagation(X)

            # Compute cost and add to list of costs
            cost = self.compute_cost(AL, y)
            costs.append(cost)

            # Backward propagation
            grads = self.backward_propagation(AL, y, caches)

            # Update parameters
            self.update_parameters(self.params(), grads, learning_rate)

            # Print the cost every 100 iterations
            if i % 100 == 0:
                print ("Cost after iteration %i: %f" %(i, cost))

        plt.plot(np.squeeze(costs))
        plt.ylabel('cost')
        plt.xlabel('iteration (per hundreds)')
        plt.title("Learning rate =" + str(learning_rate))
        plt.show()

    def compute_cost(self, AL, Y):
        """compute cross entropy loss"""
        m = Y.shape[0]
        logprobs = np.multiply(np.log(AL), Y) + np.multiply((1 - Y), np.log(1 - AL))
        cost = -(1./m) * np.sum(logprobs)
        return np.squeeze(cost)

    def predict(self, X, threshold=0.5):
        """make predictions on new input data"""
        probabilities, _ = self.forward_propagation(X)
        predicted_classes = [1 if prob > threshold else 0 for prob in probabilities]
        return predicted_classes

    def params(self):
        """get network parameters"""
        params = {
            'W1': self.W1,
            'b1': self.b1,
            'W2': self.W2,
            'b2': self.b2
        }
        return params

训练模型:

model = BpModel(n_inputs=1, hidden_size=20, n_outputs=1)
model.train(X, y, learning_rate=0.1, num_iterations=1000)

预测新数据:

new_data = np.expand_dims([-0.2, 0.7, -0.4, 0.3, 0.8], axis=1)
predictions = model.predict(new_data)
print("Predictions:", predictions) #[False False True False True]