在本次实验专注于两种前馈神经网络的深入研究:多层感知器和卷积神经网络。
多层感知器作为简单感知器的进阶版本,通过将多个感知器集成在一个层内,并层层叠加,形成了更为复杂的网络结构。在“多层感知器在姓氏分类中的应用”章节中,我们将展示这种网络如何在复杂的多级分类任务中发挥作用。
卷积神经网络(CNN)则是另一种前馈神经网络,它借鉴了数字信号处理中的窗口滤波技术,能够捕捉输入数据中的局部特征。这一能力不仅使CNN在计算机视觉领域成为核心工具,同时也使其成为识别序列数据中的模式(例如单词和句子)的理想选择。在“CNN在姓氏分类中的应用”章节中,我们将具体演示如何利用CNN进行分类任务。
一、实验原理
1.1 多层感知机
1.1.1 多层感知机介绍
多层感知机(MLP,Multilayer Perceptron)也叫人工神经网络(ANN,Artificial Neural Network),除了输入输出层,它中间可以有多个隐层,最简单的MLP只含一个隐层,即三层的结构。
多层感知器(multilayer Perceptron,MLP)是指可以是感知器的人工神经元组成的多个层次。MPL的层次结构是一个有向无环图。通常,每一层都全连接到下一层,某一层上的每个人工神经元的输出成为下一层若干人工神经元的输入。MLP至少有三层人工神经元。分别为输入层、隐藏层、输出层。它中间可以有多个隐层,最简单的MLP只含一个隐层,即三层的结构。下图为MLP的网络结构。
输入层(input layer)由简单的输入人工神经元构成。每个输入神经元至少连接一个隐藏层(hidden layer)的人工神经元。隐藏层表示潜在的变量;层的输入和输出都不会出现在训练集中。隐藏层后面连接的是输出层(output layer)。
1.1.2 多层感知机工作机制
多层感知器(MLP)的输入层接收要处理的输入信号,输出层执行预测和分类等所需任务,位于输入和输出层之间的隐藏层则是MLP的真正计算引擎。数据正向沿着输入层流向输出层,MLP中的神经元通过反向传播进行训练学习算法。在本部分,我们将详细介绍多层感知机的工作机制,包括前向传播、反向传播和权重更新的过程。
1、前向传播(Forward Propagation)
在前向传播过程中,输入数据从输入层经过一系列的隐藏层传递到输出层。每个神经元都接收上一层所有神经元的输出,并计算加权和,并应用激活函数来产生输出。
假设我们有一个多层感知机模型,包含一个输入层、两个隐藏层和一个输出层。输入层有3个神经元,隐藏层1有4个神经元,隐藏层2有2个神经元,输出层有1个神经元。每个神经元与上一层的所有神经元相连。
下图展示了这个多层感知机的结构:
输入层 隐藏层1 隐藏层2 输出层
o1 -------------- o1 -------------- o1 -------------- o1
o2 -------------- o2 -------------- o2 -------------- o2
o3 -------------- o3 -------------- o3 -------------- o3
o4 -------------- o4
o5
假设输入数据为 [1, 2, 3],每个连接都有一个权重,我们用 w 来表示。每个神经元还有一个偏置项 b。我们使用 ReLU(Rectified Linear Unit)作为激活函数。
首先,我们将输入数据乘以权重并加上偏置项,然后将结果传递给激活函数。这一过程可以表示为:
隐藏层1的输入 = (1 * w1) + (2 * w2) + (3 * w3) + b1
隐藏层1的输出 = ReLU(隐藏层1的输入)
隐藏层2的输入 = (隐藏层1的输出 * w4) + (隐藏层1的输出 * w5) + (隐藏层1的输出 * w6) + b2
隐藏层2的输出 = ReLU(隐藏层2的输入)
输出层的输入 = (隐藏层2的输出 * w7) + (隐藏层2的输出 * w8) + b3
输出层的输出 = ReLU(输出层的输入)
这样,我们就完成了前向传播过程,得到了最终的输出。
2、反向传播(Backpropagation)
在反向传播过程中,我们计算输出层的误差,并将误差反向传播回隐藏层,以更新权重,从而最小化预测输出与实际输出之间的差距。
首先,我们计算输出层的误差。假设我们的目标输出为 y,输出层的输出为 y_hat,则输出层的误差可以使用均方误差(Mean Squared Error)来计算:
输出层的误差 = (y - y_hat)^2
然后,我们将输出层的误差反向传播到隐藏层2,并计算隐藏层2的误差:
隐藏层2的误差 = 输出层的误差 * 隐藏层2的梯度
接下来,我们将隐藏层2的误差反向传播到隐藏层1,并计算隐藏层1的误差:
隐藏层1的误差 = 隐藏层2的误差 * 隐藏层1的梯度
在反向传播过程中,我们需要计算每个神经元的梯度,以便更新权重。梯度表示了误差相对于权重的变化率。
3、权重更新
在权重更新阶段,我们使用梯度下降法来调整权重,以最小化损失函数。我们根据反向传播计算得到的梯度,使用优化算法(如随机梯度下降)来更新网络中的权重。
权重的更新可以使用以下公式表示:
新权重 = 旧权重 - 学习率 * 梯度
学习率是一个超参数,控制每次更新的步长。通过迭代训练样本并反复进行前向传播和反向传播,不断调整权重,直到达到收敛条件或达到预定的训练轮数。
通过前向传播和反向传播的过程,多层感知机可以学习输入数据的特征,并进行预测和分类任务。
1.2 卷积神经网络
1.2.1 卷积神经网络介绍
卷积神经网络(Convolutional Neural Network,简称CNN),是一种前馈神经网络,人工神经元可以响应周围单元,可以进行大型图像处理。卷积神经网络利用输入是图片的特点,把神经元设计成三个维度 : width, height, depth。比如输入的图片大小是 32 × 32 × 3 (rgb),那么输入神经元就也具有 32×32×3 的维度。下图为CNN的网络结构。可以看到,一个卷积神经网络由很多层组成,它们的输入是三维的,输出也是三维的,有的层有参数,有的层不需要参数。当CNN网络工作时,这些卷积会伴随着卷积过程进行不断转换。
图1 CNN网络结构
CNN网络一共有5个层级结构:输入层、卷积层、激活层、池化层、全连接FC层。
1.2.2 输入层
输入层接收原始图像数据。图像通常由三个颜色通道(红、绿、蓝)组成,形成一个二维矩阵,表示像素的强度值。
1.2.3 卷积层
卷积层将输入图像与卷积核进行卷积操作。它并不是一下子整张图同时识别,而是对于图片中的每一个特征首先局部感知,然后在更高层次对局部进行综合操作,从而得到的全局信息。
图2 局部感知示意图
1.2.4 激活层
对卷积层的输出结果做一次非线性映射。使网络能够学习复杂的特征。常见的激活函数有:Sigmoid函数、Tanh函数、ReLU、Leaky ReLU。
Sigmoid:
Tanh:
ReLU:
Leaky ReLU:
1.2.5 池化层
通过减小特征图的大小来减少计算复杂性。它通过选择池化窗口内的最大值或平均值来实现。这有助于提取最重要的特征。
图 最大池化
1.2.6 全连接层
全连接层将提取的特征映射转化为网络的最终输出。这可以是一个分类标签、回归值或其他任务的结果。整个模型训练完毕。
所有过程可以用下图来表示:
二、实验介绍
2.1 实验目的
- 通过“带有多层感知器的姓氏分类”,掌握多层感知器在多层分类中的应用
- 掌握每种类型的神经网络层对它所计算的数据张量的大小和形状的影响
2.2 实验环境
- Python 3.6.7
2.3 实验流程图
因为代码比较多和繁琐,做了个比较粗糙的流程图供观赏~
数据预处理主要步骤
MLP训练和可视化主要步骤
MLP和CNN在姓氏分类预测的主要步骤
三、具体代码实现
3.1 数据预处理
我们先导入预先给定的数据(surnames.csv
)和必要的库
import collections
import numpy as np # 导入numpy库,用于进行数值计算和数组操作
import pandas as pd # 导入pandas库,用于数据分析和处理
import re
from argparse import Namespace # 导入argparse模块中的Namespace类,用于创建命名空间对象
使用Namespace
创建一个名为args
的对象存储数据集路径、数据集分割比例、输出路径和随机数种子等参数。
# 定义一个Namespace对象,用于存储参数
args = Namespace(
# 指定原始数据集的CSV文件路径
raw_dataset_csv="data/surnames/surnames.csv",
# 指定训练集占总数据集的比例
train_proportion=0.7,
# 指定验证集占总数据集的比例
val_proportion=0.15,
# 指定测试集占总数据集的比例
test_proportion=0.15,
# 指定处理后的数据集的CSV文件输出路径
output_munged_csv="data/surnames/surnames_with_splits.csv",
# 指定随机数生成器的种子,用于确保数据集分割的可重复性
seed=1337
)
读取原始数据,使用head
函数查看数据集的前几行后,使用collections.defaultdict
创建一个按国籍分组的字典,并将数据集拆分构建最终数据列表
# Read raw data
surnames = pd.read_csv(args.raw_dataset_csv, header=0)# 读取CSV文件
surnames.head() # 返回DataFrame的前5行数据
# Unique classes
set(surnames.nationality) # 返回一个包含所有不同国籍的集合
# Splitting train by nationality
# Create dict
# 根据国籍将训练数据集拆分
# 创建一个字典,用于根据国籍分组存储数据
by_nationality = collections.defaultdict(list)
for _, row in surnames.iterrows():
by_nationality[row.nationality].append(row.to_dict())
# 创建用于划分数据的数据列表。
final_list = []
# 设置随机数生成器的种子
np.random.seed(args.seed)
# 遍历按国籍排序的字典项。
for _, item_list in sorted(by_nationality.items()):
# 打乱列表顺序,以进行随机划分。
np.random.shuffle(item_list)
# 计算列表中的数据点总数
n = len(item_list)
# 根据给定比例计算训练集、验证集和测试集的大小。
n_train = int(args.train_proportion * n)
n_val = int(args.val_proportion * n)
n_test = int(args.test_proportion * n)
# 给数据点添加一个'split'属性,表示它们属于哪个数据集。
for item in item_list[:n_train]: # 前n_train个为训练集
item['split'] = 'train'
for item in item_list[n_train:n_train+n_val]: # 接下来的n_val个为验证集
item['split'] = 'val'
for item in item_list[n_train+n_val:]: # 剩余的为测试集
item['split'] = 'test'
# 将当前国籍的数据点添加到最终列表中。
final_list.extend(item_list)
将final_list
转换为pandas
的DataFrame
对象,将处理后的数据写入到指定的CSV文件中,使用to_csv
函数,并设置index=False
以避免写入索引。
# Write split data to file
# 最终的数据列表转换为pandas的DataFrame对象
final_surnames = pd.DataFrame(final_list)
# Write munged data to CSV
# 将处理后的数据写入到CSV文件中
final_surnames.to_csv(args.output_munged_csv, index=False)
我们对数据处理前后进行结果展示
查看处理前的数据集
查看和验证处理后的数据集
# 统计DataFrame中'split'列的各个唯一值出现的次数
final_surnames.split.value_counts()
# 返回前5行数据
final_surnames.head()
可以看见,数据排列更加整齐了~
3.2 多层感知器的训练和可视化
在应用之前,我们先通过可视化结果观察一下多层感知机的训练过程^-^。
老规矩,导入必要的库。并定义个全局变量标签集合LABELS
和数据点的中心位置CENTERS。
import torch # 导入torch库,用于构建和训练神经网络模型
import torch.nn as nn # 导入torch.nn模块,提供了构建神经网络模型所需的类和函数
import torch.nn.functional as F # 导入torch.nn.functional模块,提供了一些非线性函数和损失函数
import torch.optim as optim # 导入torch.optim模块,提供了优化算法
import numpy as np # 导入numpy库,用于进行数值计算和数组操作
import matplotlib.pyplot as plt
%matplotlib inline
LABELS = [0, 0, 1, 1]
CENTERS = [(-3, -3), (3, 3), (3, -3), (-3, 3)]
定义多层感知器类(MultilayerPerceptron
),用于构建多层感知器模型
class MultilayerPerceptron(nn.Module):
"""
初始化多层感知机的权重。
"""
def __init__(self, input_size, hidden_size=2, output_size=3,
num_hidden_layers=1, hidden_activation=nn.Sigmoid):
"""
初始化多层感知机的构造函数。
参数:
input_size (int): 输入层的大小
hidden_size (int): 隐藏层的大小
output_size (int): 输出层的大小
num_hidden_layers (int): 隐藏层的数量
hidden_activation (torch.nn.*): 隐藏层使用的激活函数类
"""
super(MultilayerPerceptron, self).__init__() # 调用基类的构造函数
self.module_list = nn.ModuleList() # 创建一个模块列表
interim_input_size = input_size # 临时输入大小初始化为输入层大小
interim_output_size = hidden_size # 临时输出大小初始化为隐藏层大小
# 为每个隐藏层创建一个线性层和激活函数层,并添加到模块列表中
for _ in range(num_hidden_layers):
self.module_list.append(nn.Linear(interim_input_size, interim_output_size))
self.module_list.append(hidden_activation())
interim_input_size = interim_output_size # 更新临时输入大小为下一层的输出大小
# 创建最终的全连接层,将最后一个隐藏层的输出映射到输出层
self.fc_final = nn.Linear(interim_input_size, output_size)
# 用于存储前向传播过程中的中间数据的列表
self.last_forward_cache = []
def forward(self, x, apply_softmax=False):
"""MLP的前向传播
参数:
x (torch.Tensor): 输入数据张量
apply_softmax (bool): 是否应用softmax激活函数
如果与交叉熵损失一起使用,则应为False
返回:
结果张量,张量形状应为(batch, output_dim)
"""
self.last_forward_cache = []
# 将输入数据存储为NumPy数组,并转换为CPU张量
self.last_forward_cache.append(x.to("cpu").numpy())
# 遍历模块列表,应用每个层的计算
for module in self.module_list:
x = module(x)
# 将每层的输出存储为NumPy数组
self.last_forward_cache.append(x.to("cpu").data.numpy())
# 通过最终的全连接层
output = self.fc_final(x)
# 存储最终输出
self.last_forward_cache.append(output.to("cpu").data.numpy())
# 如果需要,应用softmax激活函数
if apply_softmax:
output = F.softmax(output, dim=1)
# 返回模型的输出
return output
定义数据生成函数(get_toy_data
),用于训练和测试多层感知器。
def get_toy_data(batch_size):
"""
生成一个简单的数据集。
参数:
batch_size (int): 要生成的数据批次的大小
返回:
x_data (torch.Tensor): 输入数据张量,包含模拟的特征
y_targets (torch.Tensor): 目标张量,包含每个输入数据点的标签
"""
assert len(CENTERS) == len(LABELS), 'centers should have equal number labels'
# 初始化存储输入数据的列表
x_data = []
# 初始化目标标签数组,大小为batch_size,初始值为0
y_targets = np.zeros(batch_size)
# 获取中心点的数量
n_centers = len(CENTERS)
# 遍历批次中的每个样本
for batch_i in range(batch_size):
# 随机选择一个中心点的索引
center_idx = np.random.randint(0, n_centers)
# 从以中心点为中心的正态分布中生成一个数据点,并添加到x_data列表
x_data.append(np.random.normal(loc=CENTERS[center_idx]))
# 将当前样本的标签设置为对应的中心点标签
y_targets[batch_i] = LABELS[center_idx]
# 将输入数据列表转换为PyTorch张量,数据类型为float32
x_data = torch.tensor(x_data, dtype=torch.float32)
# 将目标标签数组转换为PyTorch张量,数据类型为int64
y_targets = torch.tensor(y_targets, dtype=torch.int64)
# 返回生成的数据和标签张量
return x_data, y_targets
定义结果可视化函数(visualize_results
),用于可视化训练后的模型分类结果和决策边界。
def visualize_results(perceptron, x_data, y_truth, n_samples=1000, ax=None, epoch=None,
title='', levels=[0.3, 0.4, 0.5], linestyles=['--', '-', '--']):
"""
可视化多层感知机的分类结果。
参数:
perceptron : 训练好的多层感知机模型
x_data: 输入特征的数据张量
y_truth: 实际标签的数据张量
n_samples: 用于绘制的样本数量
ax: 绘图的轴对象,如果为None则创建新的
epoch: 当前训练世代,如果提供则显示在图上
title: 图的标题
levels: 用于绘制分类边界的阈值
linestyles: 用于分类边界的线条样式
"""
# 前向传播模型并获取预测的最大概率和对应的标签
_, y_pred = perceptron(x_data, apply_softmax=True).max(dim=1)
y_pred = y_pred.data.numpy() # 将预测的标签转换为NumPy数组
x_data = x_data.data.numpy() # 将输入特征转换为NumPy数组
y_truth = y_truth.data.numpy() # 将实际标签转换为NumPy数组
n_classes = len(set(LABELS)) # 获取类别数量
# 初始化每个类别的x数据和颜色列表
all_x = [[] for _ in range(n_classes)]
all_colors = [[] for _ in range(n_classes)]
# 设置颜色和标记
colors = ['black', 'white']
markers = ['o', '*']
# 遍历数据点,根据预测和真实标签填充颜色和x数据列表
for x_i, y_pred_i, y_true_i in zip(x_data, y_pred, y_truth):
all_x[y_true_i].append(x_i)
# 如果预测标签与真实标签一致,则使用白色,否则使用黑色
if y_pred_i == y_true_i:
all_colors[y_true_i].append("white")
else:
all_colors[y_true_i].append("black")
#all_colors[y_true_i].append(colors[y_pred_i])
# 将列表转换为NumPy数组
all_x = [np.stack(x_list) for x_list in all_x]
# 如果没有提供ax,则创建一个新的绘图轴
if ax is None:
_, ax = plt.subplots(1, 1, figsize=(10,10))
# 为每个类别绘制散点图
for x_list, color_list, marker in zip(all_x, all_colors, markers):
ax.scatter(x_list[:, 0], x_list[:, 1], edgecolor="black", marker=marker,
facecolor=color_list, s=100)
# 设置x和y轴的限制
xlim = (min([x_list[:,0].min() for x_list in all_x]),
max([x_list[:,0].max() for x_list in all_x]))
ylim = (min([x_list[:,1].min() for x_list in all_x]),
max([x_list[:,1].max