这里写目录标题
- 全连接神经网络vs前馈神经网络
- 基于全连接神经网络的手写数字识别
- 使用Pytorch实现
- 纯Python实现
- 全连接神经网络的局限
端到端学习
深度学习有时也称为端到端机器学习(end-to-end machine learning)。这里所说的端到端是指从一端到另一端的意思,也就是从原始数据(输入)中获得目标结果(输出)的意思。
数据驱动
神经网络的性能依赖于大量的数据,通过训练数据来学习模式和特征。
大模型解决运筹优化问题中,一种端到端的方式是,输入问题描述,直接生成模型。而不需要经过中间的参数提取,结构化建模信息
全连接神经网络vs前馈神经网络
前馈神经网络是一类广义的神经网络架构,其中信号从输入层经过一系列隐藏层最终到达输出层,且信号只向前传播,不存在反馈连接。全连接神经网络是前馈神经网络的一种特殊形式。
全连接神经网络是一种特殊的前馈神经网络,其中每一层的每一个神经元都与下一层的每一个神经元相连。这意味着每个节点都接收来自前一层所有节点的输入,并对这些输入进行加权求和,然后通过激活函数输出。
z ( l ) = X W ( l ) ( ) z^{(l)} = XW^{(l)}() z(l)=XW(l)()
基于全连接神经网络的手写数字识别
mini-batch学习(小批量学习)
MNIST数据集的训练数据有60000个,如果以全部数据为对象求损失函数的和,则计算过程需要花费较长的时间。再者,如果遇到大数据,数据量会有几百万、几千万之多,这种情况下以全部数据为对象计算损失函数是不现实的。因此,我们从全部数据中选出一部分,作为全部数据的“近似”。神经网络的学习也是从训练数据中选出一批数据(称为mini-batch,小批量),然后对每个mini-batch进行学习。比如,从60000个训练数据中随机选择100笔,再用这100笔数据进行学习。这种学习方式称为mini-batch学习。
计算电视收视率时,并不会统计所有家庭的电视机,而是仅以那些被选中的家庭为统计对象。比如,通过从关东地区随机选择 1000个家庭计算收视率,可以近似地求得关东地区整体的收视率。这 1000个家庭的收视率,虽然严格上不等于整体的收视率,但可以作为整体的一个近似值。和收视率一样,mini-batch的损失函数也是利用一部分样本数据来近似地计算整体。也就是说,用随机选择的小批量数据(mini-batch)作为全体训练数据的近似值。
损失函数
交叉熵损失(Cross-Entropy Loss)在分类任务中非常常用,尤其是多分类问题。交叉熵损失的公式如下:
L ( Y , Y ^ ) = − 1 m ∑ i = 1 m ∑ j = 1 C Y i j log ( Y ^ i j ) L(Y, \hat{Y})= -\frac{1}{m} \sum_{i=1}^{m} \sum_{j=1}^{C} Y_{ij} \log(\hat Y_{ij}) L(Y,Y^)=−m1i=1∑mj=1∑CYijlog(Y^ij)
其中:
- Y Y Y 是真实标签(one-hot编码形式), Y ^ \hat{Y} Y^ 是模型预测的概率分布。
- m m m 是样本数量。
- C C C 是类别数量。
- Y i j Y_{ij} Yij是第 i i i 个样本在第 j j j 类的真实标签(1或0)。
- Y ^ i j \hat{Y}_{ij} Y^ij是第 i i i 个样本在第 j j j 类的预测概率。
使用Pytorch实现
步骤
- 数据预处理:
- 使用 transforms.ToTensor() 将图像转换为张量。
- 使用 transforms.Normalize() 进行归一化处理。
- 加载数据集:
- 使用 torchvision.datasets.MNIST 加载MNIST数据集。
- 使用 torch.utils.data.DataLoader 创建数据加载器。
- 定义神经网络模型:
- 使用 nn.Module 定义一个简单的全连接神经网络(FCNN)。
- 包含三个全连接层和ReLU激活函数。
- 定义损失函数和优化器:
- 使用 nn.CrossEntropyLoss 作为损失函数。使用 optim.Adam 作为优化器。
- 训练模型:进行前向传播、计算损失、反向传播和参数更新。
- 测试模型。在测试数据集上评估模型性能,计算准确率。
完整代码
# 下载数据集
from torchvision import datasets, transforms
import torchvision
import torch
import torch.nn as nn # torch.nn 模块包含了构建和训练神经网络所需的各种类和函数
import torch.optim as optim # torch.optim 模块包含了各种优化算法import matplotlib.pyplot as pltbatch_size = 256 # 一次处理图片的数量
# Data set
train_dataset = torchvision.datasets.MNIST(root="./data", # 数据集下载位置train=True, # 是否作为训练集download=True, # =True为从网络下载数据集,若已经下载,则不会再次下载transform=transforms.ToTensor(), # 一般来说,下载的是numpy格式,要转为tensor
)test_dataset = torchvision.datasets.MNIST(root="./data", train=False, transform=transforms.ToTensor()
)# Data loader
train_loader = torch.utils.data.DataLoader(dataset=train_dataset, batch_size=batch_size, shuffle=True
)test_loader = torch.utils.data.DataLoader(dataset=test_dataset, batch_size=batch_size, shuffle=False
)
print(train_dataset)
print(test_dataset)
print(train_loader, type(train_loader), len(train_loader))
print(test_loader)# train_loader 是一个 DataLoader 对象,它实现了迭代器协议,因此可以在 for 循环中使用。每次迭代时,train_loader 会返回一个批次的数据和对应的标签。fig, axes = plt.subplots(16, 16)
for batch_idx, (data, target) in enumerate(train_loader):print(f"batch index : {batch_idx}")print(f"data shape: {data.shape}")print(f"target shape :{target.shape}")for batch_index in range(16):for j in range(16):index = batch_index * 16 + jimage = data[index].numpy().squeeze()axes[batch_index][j].imshow(image, cmap="gray")axes[batch_index][j].set_title(f"{target[index]}", fontsize=12)axes[batch_index][j].axis("off") # 隐藏每个子图的坐标轴,移除子图的坐标轴线、刻度和刻度标签fig.tight_layout(pad=0.2, w_pad=0, h_pad=0)plt.show()break# 定义全连接神经网络模型
class FCNN(nn.Module):# 这里定义了一个名为 FCNN 的类,继承自 nn.Module。nn.Module 是所有神经网络模块的基类。"""这个网络是一个简单的三层全连接神经网络,用于处理MNIST手写数字识别任务。MNIST数据集中的每张图像是28x28像素的灰度图像,表示0到9的手写数字。"""def __init__(self):super(FCNN, self).__init__() # 调用父类 nn.Module 的初始化方法。self.fc1 = nn.Linear(28 * 28, 512) # 定义第一个全连接层(线性层),输入大小为28x28(784个像素),输出大小为512个神经元。self.fc2 = nn.Linear(512, 256) # 定义第二个全连接层,输入大小为512个神经元,输出大小为256个神经元。self.fc3 = nn.Linear(256, 10) # 定义第三个全连接层,输入大小为256个神经元,输出大小为10个神经元(对应0到9的10个类别)。def forward(self, x):x = x.view(-1, 28 * 28) # 将输入展平x = torch.relu(self.fc1(x))x = torch.relu(self.fc2(x))x = self.fc3(x)return xmodel = FCNN()# 定义损失函数和优化器
criterion = nn.CrossEntropyLoss() # PyTorch 中用于多分类任务的损失函数
"""
optim.Adam 是 PyTorch 中的 Adam 优化器,它是一种自适应学习率优化算法,结合了 AdaGrad 和 RMSProp 的优点。
model.parameters() 返回模型中所有需要优化的参数(即权重和偏置)
lr=0.001 设置了优化器的学习率,学习率决定了每次参数更新的步长
"""
optimizer = optim.Adam(model.parameters(), lr=0.001)# 训练模型
num_epochs = 5
loss_list = []
for epoch in range(num_epochs):running_loss = 0.0for batch_index, data in enumerate(train_loader, 0):inputs, labels = dataoptimizer.zero_grad() # 清除梯度缓存outputs = model(inputs) # 前向传播loss = criterion(outputs, labels) # 计算损失loss.backward() # 反向传播optimizer.step() # 更新模型参数running_loss += loss.item() # 累积损失if batch_index % 100 == 99: # 每100个批次打印一次损失avg_loss = running_loss / 100loss_list.append(avg_loss)print(f"Epoch {epoch + 1}, Batch {batch_index + 1}, Loss: {running_loss / 100:.3f}")running_loss = 0.0print("Finished Training")# 测试模型
correct = 0
total = 0with torch.no_grad():for data in test_loader:images, labels = dataoutputs = model(images)_, predicted = torch.max(outputs.data, 1)total += labels.size(0)correct += (predicted == labels).sum().item()print(f"Accuracy of the network on the 10000 test images: {100 * correct / total:.2f}%")
纯Python实现
import torch
import torchvision
import torchvision.transforms as transforms
import numpy as np# 定义数据转换
transform = transforms.Compose([transforms.ToTensor(),transforms.Normalize((0.5,), (0.5,))
])# 加载训练和测试数据集
trainset = torchvision.datasets.MNIST(root='./data', train=True, download=True, transform=transform)
trainloader = torch.utils.data.DataLoader(trainset, batch_size=64, shuffle=True)testset = torchvision.datasets.MNIST(root='./data', train=False, download=True, transform=transform)
testloader = torch.utils.data.DataLoader(testset, batch_size=64, shuffle=False)# 定义神经网络模型
class SimpleNN:def __init__(self, input_size, hidden_size, output_size):# 初始化权重和偏置"""使用标准正态分布生成随机权重是一种常见的初始化方法,原因如下:用标准正态分布(均值为0,标准差为1)生成随机权重,可以帮助保持每层的激活值的方差稳定。具体来说:如果权重初始化得太大,激活值可能会爆炸(变得非常大),导致梯度爆炸问题。如果权重初始化得太小,激活值可能会消失(变得非常小),导致梯度消失问题。通过使用标准正态分布生成随机权重,可以在一定程度上避免这些问题,使得激活值在合理的范围内变化,从而有助于稳定训练过程。"""self.W1 = np.random.randn(input_size, hidden_size) * 0.01 # 这样做的目的是将权重初始化为较小的值,避免初始权重过大导致的梯度爆炸问题。self.b1 = np.zeros((1, hidden_size))self.W2 = np.random.randn(hidden_size, output_size) * 0.01self.b2 = np.zeros((1, output_size))def relu(self, Z):return np.maximum(0, Z)def relu_derivative(self, Z):return Z > 0def softmax(self, Z):expZ = np.exp(Z - np.max(Z, axis=1, keepdims=True))return expZ / np.sum(expZ, axis=1, keepdims=True)def forward(self, X):# 前向传播self.Z1 = np.dot(X, self.W1) + self.b1self.A1 = self.relu(self.Z1)self.Z2 = np.dot(self.A1, self.W2) + self.b2self.A2 = self.softmax(self.Z2)return self.A2def compute_loss(self, Y, Y_hat):# 计算交叉熵损失m = Y.shape[0]log_likelihood = -np.log(Y_hat[range(m), Y])loss = np.sum(log_likelihood) / mreturn lossdef backward(self, X, Y, Y_hat):# 反向传播m = X.shape[0]dZ2 = Y_hatdZ2[range(m), Y] -= 1dZ2 /= mself.dW2 = np.dot(self.A1.T, dZ2)self.db2 = np.sum(dZ2, axis=0, keepdims=True)dA1 = np.dot(dZ2, self.W2.T)dZ1 = dA1 * self.relu_derivative(self.Z1)self.dW1 = np.dot(X.T, dZ1)self.db1 = np.sum(dZ1, axis=0, keepdims=True)def update_parameters(self, learning_rate):# 更新参数self.W1 -= learning_rate * self.dW1self.b1 -= learning_rate * self.db1self.W2 -= learning_rate * self.dW2self.b2 -= learning_rate * self.db2# 训练模型
def train(model, trainloader, epochs, learning_rate):for epoch in range(epochs):running_loss = 0.0for i, data in enumerate(trainloader, 0):print("batch index", i)inputs, labels = datainputs = inputs.view(inputs.size(0), -1).numpy() # 展平输入labels = labels.numpy()# 前向传播Y_hat = model.forward(inputs)# 计算损失loss = model.compute_loss(labels, Y_hat)# 反向传播model.backward(inputs, labels, Y_hat)# 更新参数model.update_parameters(learning_rate)running_loss += lossif i % 100 == 99: # 每100个批次打印一次损失avg_loss = running_loss / 100print(f'Epoch {epoch + 1}, Batch {i + 1}, Loss: {avg_loss:.3f}')running_loss = 0.0# 初始化模型
"""
input_size 在全连接神经网络中,输入层的神经元数量应与输入数据的特征数量相同
hidden_size 藏层的神经元数量是一个超参数,需要通过实验进行调整。隐藏层的神经元数量决定了网络的容量和复杂度。更多的神经元可以捕捉到更复杂的特征,但也可能导致过拟合和计算开销增加。128个神经元是一个合理的起点,可以在实际应用中根据需要进行调整。
输出层(output_size = 10):与分类任务的类别数量相同,MNIST数据集有10个类别,因此输出层的神经元数量为10。
"""
input_size = 784 # 28x28
hidden_size = 128
output_size = 10
model = SimpleNN(input_size, hidden_size, output_size)# 训练模型
epochs = 5
learning_rate = 0.1
train(model, trainloader, epochs, learning_rate)# 测试模型
def predict(model, testloader):correct = 0total = 0for data in testloader:inputs, labels = datainputs = inputs.view(inputs.size(0), -1).numpy() # 展平输入labels = labels.numpy()Y_hat = model.forward(inputs)predictions = np.argmax(Y_hat, axis=1)total += labels.sizecorrect += (predictions == labels).sum()accuracy = correct / totalprint(f'Accuracy: {accuracy * 100:.2f}%')predict(model, testloader)
全连接神经网络的局限
全连接的神经网络中使用了全连接层(Affine层)。在全连接层中,相邻层的神经元全部连接在一起,输出的数量可以任意决定。这会有一个问题:那就是数据的形状被忽视了。
比如,输入数据是图像时,图像通常是高、长、通道方向上的3维形状。但是,向全连接层输入时,需要将3维数据拉平为1维数据。实际上,前面提到的使用了MNIST数据集的例子中,输入图像就是1通道、高28像素、长28像素的(1, 28, 28)形状,但却被排成1列,以784个数据的形式输入到最开始的Affine层。
图像是3维形状,这个形状中应该含有重要的空间信息。比如,空间上邻近的像素为相似的值、RGB的各个通道之间分别有密切的关联性、相距较远的像素之间没有什么关联等,3维形状中可能隐藏有值得提取的本质模式。但是,因为全连接层会忽视形状,将全部的输入数据作为相同的神经元(同一维度的神经元)处理,所以无法利用与形状相关的信息。
而卷积层可以保持形状不变。当输入数据是图像时,卷积层会以3维数据的形式接收输入数据,并同样以3维数据的形式输出至下一层。因此,在CNN中,可以(有可能)正确理解图像等具有形状的数据。