说起来,我们在前面12章里,学了传统的机器学习算法------线性回归、逻辑回归、决策树、随机森林、支持向量机、K-Means聚类,甚至还做了两个完整的实战项目。这些算法都是上世纪50年代到90年代发展起来的,虽然经典,但在处理复杂任务时,确实有点力不从心了。
现在,我们要进入深度学习的世界了。
深度学习不是什么玄学,它本质上就是神经网络,而且是多层神经网络。但为什么要搞多层?一层不够吗?这得从神经网络的鼻祖------感知机说起。
从生物神经元到人工神经元
1943年,心理学家Warren McCulloch和数学家Walter Pitts提出了第一个神经元模型,后来被称为M-P神经元模型。这个模型很简单:神经元接收多个输入,每个输入都有一个权重,神经元对所有加权输入求和,然后通过一个激活函数决定是否"兴奋"。
这个想法其实挺自然的,因为它模仿了人类大脑的工作方式。人脑里有大约860亿个神经元,每个神经元通过突触连接到成千上万个其他神经元。当外界刺激(光、声、触觉等)作用于我们的感官时,神经元会接收这些信号,如果信号强度超过某个阈值,神经元就会"兴奋",并向下游神经元发送电信号。
1957年,康奈尔大学的心理学家Frank Rosenblatt发明了感知机(Perceptron),这是第一个可以学习的人工神经网络模型。感知机就是一个简单的二分类器:输入几个特征,对每个特征加权求和,然后根据结果的正负来分类。
说真的,这个模型简单到让人怀疑它能不能解决问题。但事实是,在某些任务上,它确实能工作,而且工作得还不错。
感知机:最简单的神经网络
感知机的工作原理可以用一个数学公式来表达:
y = f ( ∑ i = 1 n w i x i + b ) y = f(\sum_{i=1}^{n} w_i x_i + b) y=f(i=1∑nwixi+b)
其中:
- x i x_i xi是输入特征
- w i w_i wi是对应的权重
- b b b是偏置
- f f f是激活函数
如果用代码来实现,一个感知机就是这样:
python
import numpy as np
class Perceptron:
def __init__(self, n_features):
# 初始化权重和偏置
self.weights = np.random.randn(n_features)
self.bias = np.random.randn()
def forward(self, X):
# 前向传播
z = np.dot(X, self.weights) + self.bias
return self._step_function(z)
def _step_function(self, z):
# 阶跃激活函数
return np.where(z >= 0, 1, 0)
def predict(self, X):
return self.forward(X)
这个感知机可以做简单的二分类任务。比如,我们可以用它来模拟"AND"逻辑:只有当两个输入都是1时,输出才为1。
python
# 模拟AND逻辑
perceptron = Perceptron(n_features=2)
# 训练数据(手动设置权重和偏置)
perceptron.weights = np.array([1, 1])
perceptron.bias = -1.5
# 测试
X = np.array([[0, 0], [0, 1], [1, 0], [1, 1]])
predictions = perceptron.predict(X)
print("AND逻辑测试:")
for x, y_pred in zip(X, predictions):
print(f"输入: {x}, 输出: {y_pred}")
这看起来很简单,对吧?但问题是,感知机有一个致命的缺陷:它只能解决线性可分问题。
感知机的致命缺陷:线性可分性
什么意思呢?如果我们在二维平面上画点,能用一条直线把两类点分开,这就是线性可分问题。感知机可以学习这条直线的位置,然后根据点在直线的哪一边来分类。
但如果不能用一条直线分开呢?比如"XOR"逻辑:当两个输入相同时输出0,不同时输出1。这是一个经典的非线性可分问题。
python
# 测试XOR逻辑
X = np.array([[0, 0], [0, 1], [1, 0], [1, 1]])
y = np.array([0, 1, 1, 0])
perceptron = Perceptron(n_features=2)
predictions = perceptron.predict(X)
print("XOR逻辑测试:")
print(f"真实标签: {y}")
print(f"预测结果: {predictions}")
你会发现,无论怎么调整权重和偏置,感知机都无法正确分类XOR问题。这是一个巨大的打击,因为在1969年,AI领域的泰斗Marvin Minsky和Seymour Papert在《Perceptrons》一书中,从数学上证明了单层感知机的局限性。
这本书的出版直接导致了AI的第一个寒冬。研究者们意识到,简单的感知机解决不了复杂问题,但当时的技术又无法构建更复杂的网络,所以很多人放弃了AI研究。
这一停,就是将近20年。
多层感知机:突破线性限制
20世纪80年代,AI研究者们找到了突破感知机局限性的方法:多层感知机(Multi-Layer Perceptron,MLP)。核心思想很简单,但很巧妙:既然一层感知机解决不了问题,那就用多层。
多层感知机包含三种类型的层:
- 输入层:接收原始数据
- 隐藏层:进行特征提取和变换
- 输出层:输出最终结果
每个神经元都与上一层的所有神经元连接,这种结构叫做全连接层。
但这里有个问题:如果所有的激活函数都是线性的,那么多层网络就等价于单层网络。因为线性变换的组合仍然是线性变换。比如,两层线性网络: y = W 2 ( W 1 x + b 1 ) + b 2 y = W_2(W_1 x + b_1) + b_2 y=W2(W1x+b1)+b2,可以简化为 y = ( W 2 W 1 ) x + ( W 2 b 1 + b 2 ) y = (W_2 W_1)x + (W_2 b_1 + b_2) y=(W2W1)x+(W2b1+b2),这还是个线性变换。
所以,我们需要非线性激活函数。
非线性激活函数:网络的灵魂
非线性激活函数是神经网络能够学习复杂模式的关键。没有它们,多层网络就失去了意义。
常用的非线性激活函数包括:
Sigmoid函数
Sigmoid函数将输入压缩到(0, 1)区间,历史上曾被广泛使用:
σ ( x ) = 1 1 + e − x \sigma(x) = \frac{1}{1 + e^{-x}} σ(x)=1+e−x1
python
def sigmoid(x):
return 1 / (1 + np.exp(-x))
Sigmoid函数的输出可以理解为概率,所以在早期的神经网络中被广泛使用。但它有一个致命缺陷:梯度消失问题。当输入非常大或非常小时,Sigmoid的导数趋近于0,导致梯度无法有效传播。
Tanh函数
Tanh函数将输入压缩到(-1, 1)区间:
tanh ( x ) = e x − e − x e x + e − x \tanh(x) = \frac{e^x - e^{-x}}{e^x + e^{-x}} tanh(x)=ex+e−xex−e−x
python
def tanh(x):
return np.tanh(x)
Tanh函数的输出是以0为中心的,这在某些情况下比Sigmoid更好。但它同样存在梯度消失问题。
ReLU函数
ReLU(Rectified Linear Unit)是目前最流行的激活函数,它非常简单:
R e L U ( x ) = max ( 0 , x ) ReLU(x) = \max(0, x) ReLU(x)=max(0,x)
python
def relu(x):
return np.maximum(0, x)
ReLU函数解决了梯度消失问题,因为当输入为正时,梯度恒为1。这使得深度网络能够被有效训练。这也是为什么深度学习在2010年后迅速发展的关键原因之一。
前向传播:数据如何在网络中流动
在神经网络中,数据从输入层流向输出层,这个过程叫做前向传播。我们用一个简单的例子来说明:
假设有一个三层网络:输入层有2个神经元,隐藏层有3个神经元,输出层有1个神经元。
python
class NeuralNetwork:
def __init__(self, input_size, hidden_size, output_size):
# 初始化权重和偏置
self.W1 = np.random.randn(input_size, hidden_size)
self.b1 = np.zeros((1, hidden_size))
self.W2 = np.random.randn(hidden_size, output_size)
self.b2 = np.zeros((1, output_size))
def sigmoid(self, x):
return 1 / (1 + np.exp(-x))
def forward(self, X):
# 输入层到隐藏层
self.z1 = np.dot(X, self.W1) + self.b1
self.a1 = self.sigmoid(self.z1)
# 隐藏层到输出层
self.z2 = np.dot(self.a1, self.W2) + self.b2
self.a2 = self.sigmoid(self.z2)
return self.a2
前向传播的过程就是:
- 输入X乘以权重W1,加上偏置b1,得到z1
- 对z1应用激活函数,得到a1
- a1乘以权重W2,加上偏置b2,得到z2
- 对z2应用激活函数,得到输出a2
这个过程可以重复任意次,形成任意深度的网络。
损失函数:如何衡量网络的表现
网络输出之后,我们需要评估它的表现。对于二分类问题,我们通常使用交叉熵损失函数:
L = − y log ( y ^ ) − ( 1 − y ) log ( 1 − y ^ ) L = -y \log(\hat{y}) - (1 - y) \log(1 - \hat{y}) L=−ylog(y^)−(1−y)log(1−y^)
其中y是真实标签(0或1), y ^ \hat{y} y^是网络输出的概率。
python
def binary_cross_entropy(y_true, y_pred):
epsilon = 1e-15 # 防止log(0)
y_pred = np.clip(y_pred, epsilon, 1 - epsilon)
return -np.mean(y_true * np.log(y_pred) + (1 - y_true) * np.log(1 - y_pred))
损失函数的值越小,说明网络的预测越准确。训练网络的目标就是最小化这个损失函数。
反向传播:网络如何学习
现在我们有了前向传播和损失函数,但问题来了:如何调整网络的权重和偏置,使得损失最小?
这就需要反向传播算法。反向传播是神经网络训练的核心,它基于链式法则,从输出层向输入层反向计算梯度。
假设我们的网络只有一层,损失函数 L L L关于权重 w w w的梯度是:
∂ L ∂ w = ∂ L ∂ y ^ ⋅ ∂ y ^ ∂ z ⋅ ∂ z ∂ w \frac{\partial L}{\partial w} = \frac{\partial L}{\partial \hat{y}} \cdot \frac{\partial \hat{y}}{\partial z} \cdot \frac{\partial z}{\partial w} ∂w∂L=∂y^∂L⋅∂z∂y^⋅∂w∂z
根据链式法则,我们把它分解成三个部分:
- 损失关于输出的导数
- 输出关于激活值的导数
- 激活值关于权重的导数
这个过程可以递归地应用到每一层,从输出层一直反向传播到输入层。
梯度下降:更新网络参数
有了梯度之后,我们就可以用梯度下降法来更新权重:
w n e w = w o l d − η ⋅ ∂ L ∂ w w_{new} = w_{old} - \eta \cdot \frac{\partial L}{\partial w} wnew=wold−η⋅∂w∂L
其中 η \eta η是学习率,控制每次更新的步长。
python
def update_weights(self, X, y, learning_rate):
# 前向传播
y_pred = self.forward(X)
# 计算损失
loss = binary_cross_entropy(y, y_pred)
# 反向传播
# 输出层梯度
m = X.shape[0]
dz2 = y_pred - y
dW2 = (1 / m) * np.dot(self.a1.T, dz2)
db2 = (1 / m) * np.sum(dz2, axis=0, keepdims=True)
# 隐藏层梯度
da1 = np.dot(dz2, self.W2.T)
dz1 = da1 * self.a1 * (1 - self.a1) # Sigmoid的导数
dW1 = (1 / m) * np.dot(X.T, dz1)
db1 = (1 / m) * np.sum(dz1, axis=0, keepdims=True)
# 更新权重
self.W1 -= learning_rate * dW1
self.b1 -= learning_rate * db1
self.W2 -= learning_rate * dW2
self.b2 -= learning_rate * db2
return loss
这个过程不断重复,直到损失收敛或者达到预设的迭代次数。
训练一个完整的神经网络
现在我们把所有部分组合起来,训练一个简单的神经网络来分类XOR问题:
python
# 训练数据
X = np.array([[0, 0], [0, 1], [1, 0], [1, 1]])
y = np.array([[0], [1], [1], [0]])
# 创建网络
nn = NeuralNetwork(input_size=2, hidden_size=4, output_size=1)
# 训练
losses = []
learning_rate = 0.1
epochs = 10000
for epoch in range(epochs):
loss = nn.update_weights(X, y, learning_rate)
losses.append(loss)
if (epoch + 1) % 1000 == 0:
print(f"Epoch {epoch + 1}/{epochs}, Loss: {loss:.6f}")
# 预测
predictions = nn.forward(X)
print(f"\n预测结果:")
print(f"真实: {y.flatten()}")
print(f"预测: {predictions.flatten().round()}")
你会发现,这个网络能够正确分类XOR问题!这是单层感知机做不到的。
从简单到复杂:网络的设计哲学
神经网络的强大之处在于它的灵活性。你可以随意调整网络的结构:增加层数、改变每层的神经元数量、选择不同的激活函数、设计不同的连接方式。
但这也带来了一个问题:如何设计一个"好"的网络?
这个问题没有标准答案,但有一些经验法则:
- 从简单开始:先尝试简单的网络,如果效果不好,再增加复杂度
- 经验法则:隐藏层的神经元数量通常在输入层和输出层之间
- 深度优先:增加层数通常比增加每层的神经元数量更有效
- 任务相关:不同的任务需要不同的网络结构
神经网络的"黑盒"问题
神经网络的一个主要批评是它的不可解释性。你知道它输入什么、输出什么,但很难知道中间发生了什么。每个神经元的权重都代表什么?隐藏层提取了什么特征?
这确实是个问题,但在实际应用中,我们并不总是需要知道网络内部的工作原理。只要它在测试集上表现良好,我们就可以信任它。
而且,研究者们也在发展各种可视化技术,试图理解网络学到了什么。
从理论到实践:为什么深度学习现在火了?
神经网络的概念并不新,多层感知机在1980年代就已经存在。但为什么深度学习直到2010年后才开始爆发?
我认为主要有三个原因:
- 计算能力的提升:GPU的出现使得并行计算成为可能,大大加速了神经网络的训练
- 大数据的可用性:互联网时代产生了海量数据,为训练深度网络提供了可能
- 算法的改进:ReLU激活函数、批量归一化、残差连接等技术的发明,使得训练深度网络变得更加容易
这三个因素共同推动了深度学习的发展。
本章小结
这一章我们学习了神经网络的基础知识,从最简单的感知机到多层感知机,从线性分类到非线性分类。核心概念包括:
- 感知机:最简单的神经网络,但只能解决线性可分问题
- 多层感知机:通过多层结构和非线性激活函数,突破线性限制
- 激活函数:网络的灵魂,引入非线性是关键
- 前向传播:数据从输入层流向输出层
- 损失函数:衡量网络表现的指标
- 反向传播:计算梯度、更新权重的核心算法
- 梯度下降:优化网络参数的方法
神经网络之所以强大,是因为它的通用性。理论上,只要网络足够大、训练足够久,它可以逼近任何连续函数。这就是所谓的"万能逼近定理"。
下一章,我们将深入学习各种激活函数的特性,以及为什么选择合适的激活函数对网络性能如此重要。