建立“根据图片进行多分类”的感知机模型,并将组成此模型的三大重要部分进行分门别类,方便后续其他网络使用
同时预留足够的接口(如可调节的输入图像尺寸)和调试信息,提升泛用性
首先,将整个模型分为三部分:
①本地数据集获取
②感知机神经网络建立
③进行模型的训练/测试
下为三部分具体的代码实现和逻辑梳理
————————————————————————
逻辑梳理
首先继承torch中的Dataset类,此类包含了很多数据集的操作,直接继承方便后续使用
接着在初始化函数中初始化此父类
再对本地数据集的路径(储存了图片、文件夹名就是标签)进行遍历
最后,将路径中的图片(路径形式)和标签(文件夹名)放入"列表[元组()]"结构中,防止被更改
def __init__(self, root, is_train):
super(Datasets_V1, self).__init__()
# 定义数据集
self.dataset = []
# 读取本地数据集的路径(图片+标签)
self.path = root + "/" + ("TRAIN" if is_train else "TEST")
# 抓取本地数据集的文件列表
for i in os.listdir(self.path):
for j in os.listdir(self.path + "/" + i):
# 以(路径,标签)形式,填充数据集(以元祖填充,避免被修改)
self.dataset.append((self.path + "/" + i + "/" + j, i))
–为外部访问数据留下接口(迭代)–
以元祖和类的形式存放的数据不便于外部直接调用
所以,我们对魔术方法__getitem__进行设置,使其能够**“返回归一化的图片数据+独热(one_hot)编码化的标签”**
注意点1:
img.reshape(-1)
参数只写-1,会只保留第一维度(0维)的形状、余下维度全部整合为1个维度
最终的效果即为“转换NCWH”
注意点2:
return np.float32(img), np.float32(one_hot)
返回数据时,要把两个数据都改为np的float32(32位浮点型),才能被后续的网络正常计算
# 为外部(迭代)时留下接口
def __getitem__(self, item):
# 左侧小圆圈指:重写了父类的方法
# 创建可被迭代的对象
data = self.dataset[item]
# 对数据集的数据进行归一化
# 获取“数据”的路径,存放到L中
# imread(路径,几通道?[0:单、1:三])
img = cv2.imread(data[0], 0)
# 改为单通道
img = img.reshape(-1) # 只写-1,会只保留第一维度(0维)的形状、余下维度全部整合为1个维度
# 归一化
img = img / 255
# 对数据集的标签进行一位有效编码(one_hot)
# 创建全为0的数组
one_hot = np.zeros(10)
# 使对应位置变为1
one_hot[int(data[1])] = np.float(1)
# 返回真正需要的对象(注意:需要为np.float32)
return np.float32(img), np.float32(one_hot)
代码总览:
from torch.utils.data import Dataset
import os # 引用os,操作本地数据集
import cv2 # 引用cv2操作图像
import numpy as np
class Datasets_V1(Dataset):
def __init__(self, root, is_train):
super(Datasets_V1, self).__init__()
# 定义数据集
self.dataset = []
# 读取本地数据集的路径(图片+标签)
self.path = root + "/" + ("TRAIN" if is_train else "TEST")
# 抓取本地数据集的文件列表
for i in os.listdir(self.path):
for j in os.listdir(self.path + "/" + i):
# 以(路径,标签)形式,填充数据集(以元祖填充,避免被修改)
self.dataset.append((self.path + "/" + i + "/" + j, i))
# 为外部获取(数据集长度)时留下接口
def __len__(self):
return len(self.dataset)
# 为外部(迭代)时留下接口
def __getitem__(self, item):
# 左侧小圆圈指:重写了父类的方法
# 创建可被迭代的对象
data = self.dataset[item]
# 对数据集的数据进行归一化
# 获取“数据”的路径,存放到L中
# imread(路径,几通道?[0:单、1:三])
img = cv2.imread(data[0], 0)
# 改为单通道
img = img.reshape(-1) # 只写-1,会只保留第一维度(0维)的形状、余下维度全部整合为1个维度
# 归一化
img = img / 255
# 对数据集的标签进行一位有效编码(one_hot)
# 创建全为0的数组
one_hot = np.zeros(10)
# 使对应位置变为1
one_hot[int(data[1])] = np.float(1)
# 返回真正需要的对象(注意:需要为np.float32)
return np.float32(img), np.float32(one_hot)
————————————————————————
逻辑梳理
引用nn类来快捷进行感知机神经网络的建立
为了后续兼容复杂的网络,此网络的深度较深
代码总览:
from torch import nn
class Deep_Net_V1(nn.Module):
# 在初始化时,设置两个传入参数,分别定义网络的传入值尺寸和输出值尺寸
def __init__(self, input_size, output_size):
# 调用父类的初始化,真正初始化一个神经网络对象
super(Deep_Net_V1, self).__init__()
self.layer = nn.Sequential(
# 要求:十几层
nn.Linear(input_size, 1024),
# 注意:实际上,每一个数据层都可以加上一个激活函数(效果是影响下一层的输入),也确实可以提高效率
# 在层级之间加上”合适“的激活函数可以让降低层级的线性度,降低取值范围
nn.ReLU(),
nn.Linear(1024, 2048),
nn.ReLU(),
nn.Linear(2048, 1024),
nn.ReLU(),
nn.Linear(1024, 2048),
nn.ReLU(),
nn.Linear(2048, 1024),
nn.Linear(1024, 2048), # 注意:Linear的参数必须是(上层的感知机数量,这层感知机数)。一定要对应!
nn.Linear(2048, 1024),
nn.Linear(1024, 2048),
nn.Linear(2048, 1024),
nn.Linear(1024, 2048),
nn.ReLU(),
nn.Linear(2048, 1024),
nn.ReLU(),
nn.Linear(1024, 2048),
nn.ReLU(),
nn.Linear(2048, 1024),
nn.ReLU(),
nn.Linear(1024, 2048),
nn.Linear(2048, 1024),
nn.Linear(1024, 512),
nn.Linear(512, 128),
nn.Linear(128, 64),
nn.Linear(64, 32),
nn.Linear(32, 16),
nn.Linear(16, output_size),
# 传入的是NV结构(NHWC结构),使用Softmax激活其中的V(体积),也就是第1维度
nn.Softmax(dim=1) # dim=1代表”操作的维度为1“,即会输出第一维度的概率结果
# 与之对应的是第0维度,这时操作的数值是“批次”(N)。无实际意义
# nn.Sigmoid() # 也可以使用sigmoid激活
)
# 定义前向传输的过程
def forward(self, x):
# 直接调用layer(实际上调用的是Sequential),使它处理传入的X图像数据
return self.layer(x)
————————————————————————
逻辑梳理
加载之前写好的两部分模组、以及"数据加载器类DataLoader"——解决数据集太大加载缓慢的问题
注意点1:
建立神经网络时,可以直接通过加载器中,数据的shape(形状)来确定输入值/输出值的大小
# 获取输入数据(图像)
# 获取训练集_加载器中,第1号数据的第1个数据。再分析他的形状,即为输入(图片)大小
img_size = self.train_loader.dataset[0][0].shape[0]
# 获取训练集_加载器中,第1号数据的第2个数据。再分析他的形状,即为输出(标签)大小
label_size = self.train_loader.dataset[0][1].shape[0]
注意点2:
选择驱动时,根据当前环境CUDA的安装情况来决定使用的驱动类型
# 确定使用的驱动(是CUDA还是CPU)
self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
# 选择使用的驱动
self.net.to(self.device)
注意点3:
开始训练/测试时,一次如下的循环的具体执行次数为:
一个数据池中的数据数 / 总数据数
如:一个由10000张图片组成的数据集,一次选取200张放入数据池
那么他在一次for……enumerate……中的循环次数为:200 / 10000 = 50(次)
for i, (img, label) in enumerate(self.train_loader):
注意点3:
反向传播三定式:清空梯度/反向传播/步进更新参数
# 反向传播三定式:清空梯度/反向传播/步进更新参数
self.opt.zero_grad()
loss.backward()
self.opt.step()
代码总览:
# 训练器
import torch.cuda
from torch.utils.data import DataLoader # 引用数据集加载器类(避免数据集太大加载缓慢的问题)
from torch import optim # 优化参数、反向传播模块
import os
from nn_class import Deep_Net_V1 # 加载自定义神经网络类
from datasets_class import Datasets_V1 # 加载自定义数据集类
class Train_MNIST_Local:
def __init__(self, sample_img_url="MNIST_IMG"):
# 先定义数据集
self.train_dataset = Datasets_V1(sample_img_url, is_train=True)
self.train_loader = DataLoader(self.train_dataset, batch_size=256, shuffle=True)
self.test_dataset = Datasets_V1(sample_img_url, is_train=False)
self.test_loader = DataLoader(self.test_dataset, batch_size=256, shuffle=True)
# 获取输入数据(图像)
# 获取训练集_加载器中,第1号数据的第1个数据。再分析他的形状,即为输入(图片)大小
img_size = self.train_loader.dataset[0][0].shape[0]
# 获取训练集_加载器中,第1号数据的第2个数据。再分析他的形状,即为输出(标签)大小
label_size = self.train_loader.dataset[0][1].shape[0]
# 再定义网络
self.net = Deep_Net_V1(img_size, label_size)
# 确定使用的驱动(是CUDA还是CPU)
self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
# 选择使用的驱动
self.net.to(self.device)
# 优化器
# 优化器不止Adam,还有SGD等等
self.opt = optim.Adam(self.net.parameters())
# 魔法方法,开始训练
def __call__(self, *args, **kwargs):
# 创建存放参数表文件的文件夹
os.makedirs("param", exist_ok=True) # 创建存放参数的文件夹
# 进训练循环100 * 60000
for epoch in range(100):
print("第" + str(epoch + 1) + "次训练")
sum_loss = 0
for i, (img, label) in enumerate(self.train_loader):
# 进训练模式(可省略
self.net.train()
# 把数据放入驱动中(cuda、cpu)
img, label = img.to(self.device), label.to(self.device)
# 进行一次前向传播、得到结果out
out = self.net(img)
# 计算损失(使用自定义的均方差)
loss = torch.mean((out - label) ** 2)
sum_loss += loss.item() # !!!!item()
# 反向传播三定式:清空梯度/反向传播/步进更新参数
self.opt.zero_grad()
loss.backward()
self.opt.step()
# 保存网络参数表(后缀无所谓)
torch.save(self.net.state_dict(), "param/" + str(epoch + 1) + ".pt")
# 计算平均损失
avg_loss = sum_loss / len(self.train_loader)
print("第" + str(epoch + 1) + "次训练的平均损失:" + str(avg_loss))
def test_net(self):
for epoch in range(10):
print("第" + str(epoch + 1) + "次测试")
sum_loss = 0
for i, (img, label) in enumerate(self.test_loader):
# 进测试模式
self.net.eval()
img, label = img.to(self.device), label.to(self.device)
# 读取参数文件,读取最后一个
file_list = os.listdir("param")
file_list.sort(key=lambda x: int(x[:-3])) # 倒着数第三位开始(.pt往后),按数字的从小到大排序
last_param = file_list[len(file_list) - 1] # 最后的参数文件名(含后缀)
self.net.load_state_dict(torch.load("param/" + last_param))
out = self.net(img)
loss = torch.mean((out - label) ** 2)
sum_loss += loss.item()
avg_loss = sum_loss / len(self.test_loader)
print("第" + str(epoch + 1) + "次测试的平均损失:" + str(avg_loss))
if __name__ == '__main__':
train = Train_MNIST_Local()
train()
train.test_net()