第66课:感知机与多层感知机(MLP)
学习目标
完成本课学习后,你将能够:
- 理解感知机的基本原理及其局限性。
- 阐明多层感知机(MLP) 是如何克服感知机的局限性的。
- 掌握MLP的核心组件,如输入层、隐藏层、输出层、激活函数。
- 使用PyTorch框架搭建并训练一个基础的多层感知机模型。
- 识别并理解一个简单神经网络的前向传播过程。
核心概念
1. 从感知机说起:一个简单的决策者
想象你要决定今天是否去跑步。你考虑三个因素:天气(是否晴朗)、温度(是否适中)、是否有空。你的大脑可能会给这些因素分配不同的重要程度(权重),然后做一个综合判断。
感知机(Perceptron) 就是模仿这个最简单的决策过程。
- 输入:多个特征(如
x1,x2,x3)。 - 权重:每个特征对应一个权重(
w1,w2,w3),表示其重要性。 - 阈值(阈值逻辑):计算所有
输入 * 权重的总和,如果总和超过某个阈值,就输出“1”(例如,去跑步),否则输出“0”(不去)。
数学形式:
输出 = 1 if (w1*x1 + w2*x2 + ... + b) > 0 else 0
这里的 b 是偏置,可以看作调整阈值的参数。
核心局限:单个感知机只能解决线性可分的问题。什么是线性可分?就像用一根直线(或高维空间中的一个超平面)就能把两类数据完全分开。著名的异或(XOR)问题就是线性不可分的:两个0或两个1输入得到0,一个0一个1输入得到1,你无法用一条直线在二维平面上把(0,0)(1,1)和(0,1)(1,0)分开。
2. 多层感知机(MLP):组合的力量
为了解决非线性问题,我们引入多层感知机(Multilayer Perceptron, MLP),它是神经网络最基础的形态。 核心思想:将多个感知机(现在我们叫它们神经元)组织成层(Layer),并层层相连。
一个典型的MLP包含以下结构:
- 输入层(Input Layer):接收原始数据,神经元数量由数据特征维度决定。
- 隐藏层(Hidden Layer):位于输入和输出层之间。可以有一层或多层。隐藏层负责从数据中学习复杂的特征和表示。隐藏层的引入是MLP能解决非线性问题的关键。
- 输出层(Output Layer):产生最终的预测结果,神经元数量由任务决定(如二分类为1个,十分类为10个)。
关键改进:激活函数(Activation Function) 如果只有线性变换(加权求和),无论多少层,整个网络仍然是线性的,无法拟合非线性关系。因此,每个神经元在加权求和后,会通过一个非线性激活函数(如 Sigmoid, Tanh, ReLU)进行转换。这就像给神经元一个“开关”,让它能够表达更复杂的逻辑。
前向传播(Forward Propagation):
数据从输入层开始,逐层计算,直到输出层产生结果。
输入层 -> [线性变换 -> 激活函数] -> 隐藏层1 -> ... -> [线性变换] -> 输出层
代码示例
示例1:一个简单的感知机(演示其局限性)
import numpy as np
class Perceptron:
def __init__(self, input_size, lr=0.1):
self.weights = np.zeros(input_size + 1) # +1 for bias
self.lr = lr
def predict(self, x):
# 加入偏置项
x_with_bias = np.insert(x, 0, 1)
# 计算加权和
weighted_sum = np.dot(self.weights, x_with_bias)
# 应用阈值函数
return 1 if weighted_sum > 0 else 0
def train(self, X, y, epochs):
for _ in range(epochs):
for inputs, label in zip(X, y):
prediction = self.predict(inputs)
# 如果预测错误,更新权重
if prediction != label:
error = label - prediction
# 更新权重和偏置 (输入特征也需要加上偏置项)
inputs_with_bias = np.insert(inputs, 0, 1)
self.weights += self.lr * error * inputs_with_bias
# 测试线性可分数据 (AND 逻辑)
X_and = np.array([[0, 0], [0, 1], [1, 0], [1, 1]])
y_and = np.array([0, 0, 0, 1]) # 只有(1,1)是1
model = Perceptron(2)
model.train(X_and, y_and, epochs=100)
print("AND 感知机预测:")
for x in X_and:
print(f"{x} -> {model.predict(x)}")
# 测试线性不可分数据 (XOR 逻辑)
X_xor = X_and.copy()
y_xor = np.array([0, 1, 1, 0]) # 异或结果
model_xor = Perceptron(2)
model_xor.train(X_xor, y_xor, epochs=100)
print("\nXOR 感知机预测(会失败):")
for x in X_xor:
print(f"{x} -> {model_xor.predict(x)}")
示例2:使用PyTorch搭建一个简单的MLP
import torch
import torch.nn as nn
import torch.optim as optim
# 定义网络结构
class SimpleMLP(nn.Module):
def __init__(self, input_size, hidden_size, output_size):
super(SimpleMLP, self).__init__()
# 定义层
self.fc1 = nn.Linear(input_size, hidden_size) # 第一个全连接层(输入层 -> 隐藏层)
self.relu = nn.ReLU() # 激活函数
self.fc2 = nn.Linear(hidden_size, output_size) # 第二个全连接层(隐藏层 -> 输出层)
def forward(self, x):
# 定义前向传播路径
out = self.fc1(x)
out = self.relu(out)
out = self.fc2(out)
return out
# 创建模型实例
input_dim = 2 # 输入特征维度
hidden_dim = 10 # 隐藏层神经元数量
output_dim = 1 # 输出维度(二分类)
model = SimpleMLP(input_dim, hidden_dim, output_dim)
print(model)
# 模拟一些数据
X = torch.tensor([[0.0, 0.0], [0.0, 1.0], [1.0, 0.0], [1.0, 1.0]])
y = torch.tensor([[0.0], [1.0], [1.0], [0.0]]) # XOR数据
# 定义损失函数和优化器
criterion = nn.MSELoss() # 均方误差损失(用于回归或简单演示)
optimizer = optim.SGD(model.parameters(), lr=0.1)
# 训练循环
epochs = 1000
for epoch in range(epochs):
# 1. 前向传播
outputs = model(X)
# 2. 计算损失
loss = criterion(outputs, y)
# 3. 反向传播和优化
optimizer.zero_grad() # 清空上一轮的梯度
loss.backward() # 计算梯度
optimizer.step() # 更新权重
if (epoch + 1) % 200 == 0:
print(f'Epoch [{epoch+1}/{epochs}], Loss: {loss.item():.4f}')
# 测试
model.eval()
with torch.no_grad():
test_output = model(X)
predictions = (test_output > 0.5).float() # 简单二值化
print("\nMLP 对 XOR 的预测:")
for i in range(len(X)):
print(f"输入: {X[i].numpy()}, 预测: {predictions[i].item():.0f}, 实际: {y[i].item():.0f}")
实践练习
练习1:感知机参数探索
在示例1的感知机类基础上,尝试修改学习率 lr 和训练轮数 epochs,观察它们如何影响模型收敛的速度和最终在AND门上的表现。记录下你发现的最佳参数组合。
练习2:构建更复杂的MLP
在示例2的PyTorch代码基础上,完成以下任务:
- 增加一个额外的隐藏层。
- 将输出层激活函数改为
nn.Sigmoid()(更适用于二分类)。 - 将损失函数改为更合适的
nn.BCELoss()(二元交叉熵损失)。 - 用新的模型重新训练XOR数据,并报告最终的损失值和预测准确率。
练习3:挑战二分类问题
创建一个数据集,包含100个二维数据点,分为两个类别(例如,使用sklearn.datasets.make_moons生成月牙形数据,或者手动创建以原点为圆心的内圈和外圈数据)。使用MLP进行分类:
- 搭建一个包含一个隐藏层的MLP。
- 进行训练,并在测试集上评估准确率。
- (附加) 尝试使用不同的激活函数(ReLU vs Tanh)和隐藏层大小,观察分类效果的变化。
常见错误
- 忽略激活函数:在隐藏层后忘记添加非线性激活函数,导致整个网络退化为线性模型,无法学习复杂模式。
- 输出层激活函数选择错误:
- 二分类问题,输出层通常用
Sigmoid激活函数。 - 多分类问题,输出层用
Softmax激活函数。 - 回归问题,输出层通常不使用激活函数(线性输出)。
- 二分类问题,输出层通常用
- 输入数据未标准化/归一化:神经网络对输入数据的尺度敏感。特征尺度差异过大(如一个特征在0-1,另一个在0-1000)会导致训练困难、收敛缓慢。通常需要将数据标准化(Z-score)或归一化到[0,1]或[-1,1]区间。
- 权重初始化不当:将所有权重初始化为0会导致对称性问题,所有神经元学到相同的东西。应使用随机初始化(如PyTorch默认的Xavier/Kaiming初始化)。
- 学习率设置不合理:学习率过大,模型可能无法收敛甚至发散;学习率过小,收敛速度极慢。
小结
本节课我们学习了神经网络的基础架构:
- 感知机是最简单的神经网络单元,但只能处理线性可分问题。
- 为了解决非线性问题,我们引入了多层感知机(MLP),通过堆叠多个层并引入非线性激活函数(如ReLU, Sigmoid)来增强模型的表达能力。
- MLP的标准结构包括输入层、隐藏层和输出层。数据在各层之间前向传播。
- 使用PyTorch等深度学习框架,我们可以方便地通过定义
nn.Module类来搭建和训练MLP。 - 理解前向传播、损失函数、优化器是训练神经网络的基本流程。
通过本课,你从理论和代码上理解了从单一神经元到多层网络的演进,为深入学习更复杂的网络架构和核心算法(如反向传播)打下了坚实的基础。