携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第8天,点击查看活动详情
基于DNN的假钞识别
项目目录
一个项目需要一个清晰的目录来保证各个模块功能的正确放置,下面是经过学习得到的一个目录结构:
data目录 | 存放原始数据与预处理后切分为训练集、验证机、测试集的数据 |
---|---|
log目录 | 训练过程中使用tensorboardX保存的指标数值,如损失、精确度等 |
model_save目录 | 存放不同训练阶段的模型,最后找出个最优的用于测试集 |
config.py | 保存超参数 |
dataset_banknote.py | Banknote数据类,用于训练时获取数据 |
inference.py | 挑选模型在测试集上运行 |
model.py | 算法模型 |
preprocess.py | 对原始数据进行预处理,划分为训练集、验证集、测试集 |
trainer.py | 模型训练代码 |
data目录 | 存放数据集 |
utils.py | 工具类文件 |
项目配置
项目参数配置分三种:
- 数据配置(
Data
):配置相关的路径,维度,随机种子等。 - 模型配置(
Model
):配置不同层的模型大小,或者相关模型参数。 - 实验配置(
Experiment
):配置一些训练过程中的超参数。
配置文件可以选择 yaml
格式,这里会推荐选用 python
脚本,后续使用方便简洁,不需要折腾yaml
文件的读取等过程。
# banknote classification config
# 超参配置
# yaml
class Hyperparameter:
# ################################################################
# Data
# ################################################################
device = 'cpu' # cuda
data_dir = './data/' # 目录
data_path = './data/data_banknote_authentication.txt' # 原始数据集的路径
trainset_path = './data/train.txt'
devset_path = './data/dev.txt'
testset_path = './data/test.txt'
in_features = 4 # input feature dim 输入维度
out_dim = 2 # output feature dim (classes number) 输出维度
seed = 1234 # random seed
# ################################################################
# Model Structure
# ################################################################
layer_list = [in_features, 64, 128, 64, out_dim]
# ################################################################
# Experiment
# ################################################################
batch_size = 64 # 一次取64条数据
init_lr = 1e-3 # 学习率
epochs = 100 # 训练轮数
verbose_step = 10 #每隔的步数,进行损失值记录。(开发数据集)
save_step = 200 # 每隔的步数,对模型进行记录
HP = Hyperparameter()
复制代码
数据处理
数据处理这里涉及到数据清洗,数据校验等过程,这次实战使用的数据集是 Banknote Dataset
数据集。
Banknote Dataset
数据集:从纸币鉴别过程中的图像里提取的数据,用来预测钞票的真伪的数据集。每个样本由5个数值型变量构成,4个输入变量和1个输出变量。部分样本如下:
每一行的5个(列)变量含义如下:
第一列:图像经小波变换后的方差(variance)(连续值);
第二列:图像经小波变换后的偏态(skewness)(连续值);
第三列:图像经小波变换后的峰度(curtosis)(连续值);
第四列:图像的熵(entropy)(连续值);
第五列:钞票所属的类别(整数,0或1)。
下载地址:archive.ics.uci.edu/ml/datasets…
因为该数据集已经处理完成了,并且不需要进行数据清洗过程,所以这里直接开始其他流程。
我们拿到了假钞的数据集,会使用交叉验证方法,该数据集分成根据 0.7,0.2,0.1 比例分成三种数据集:训练数据集,开发数据集,测试数据集。在这个过程中需要对数据集进行打乱,保证取数据随机。将三种数据集分成三个文件:train.txt
,dev.txt
,test.txt
。
import numpy as np
from config import HP
import os
# 训练集
trainset_ratio = 0.7
# 测试集
devset_ratio = 0.2
# 验证集
testset_ratio = 0.1
# 随机种子
np.random.seed(HP.seed)
# 读取文件内容
dataset = np.loadtxt(HP.data_path, delimiter=',')
print(dataset)
# 对文件内容进行洗牌
np.random.shuffle(dataset)
# 获取数据行数
n_items = dataset.shape[0]
# 获取训练集行数
trainset_num = int(trainset_ratio * n_items)
devset_num = int(devset_ratio * n_items)
testset_num = n_items - trainset_num - devset_num
# 将数据保存在不同的文件里面
np.savetxt(os.path.join(HP.data_dir, 'train.txt'),
dataset[:trainset_num], delimiter=',')
np.savetxt(os.path.join(HP.data_dir, 'dev.txt'),
dataset[trainset_num:devset_num + trainset_num], delimiter=',')
np.savetxt(os.path.join(HP.data_dir, 'test.txt'),
dataset[devset_num + trainset_num:], delimiter=',')
复制代码
数据读取
构建数据集类,方便训练模型进行使用,一般需要重写的方法有
__init__
:从文件中加载数据集。__getitem__
:设置数据读取的格式以及获取格式。__len__
:获取数据长度
一般常用的是这三种,其余根据具体要求而定。
import torch
from config import HP
import numpy as np
class BanknoteDataset(torch.utils.data.Dataset):
def __init__(self, data_path):
# 从文件读取数据
self.dataset = np.loadtxt(data_path, delimiter=',')
def __getitem__(self, idx):
# 获取其中一个样本
item = self.dataset[idx]
# 将样本分开:参数与结果
x, y = item[:HP.in_features], item[HP.in_features:]
return torch.Tensor(x).float().to(HP.device), torch.Tensor(y).squeeze().long().to(HP.device)
def __len__(self):
return self.dataset.shape[0]
复制代码
模型设置
对模型进行设置,根据题目进行分析这是一个二分类问题,我们选择的模型是MLP
模型(多层感知机),层数不多一共5层,三层隐藏层分别是64,128,64。激活函数使用 relu
函数。
from torch import nn
from torch.nn import functional as F
from config import HP
# 设置模型
class BanknoteClassificationModel(nn.Module):
def __init__(self):
super(BanknoteClassificationModel, self).__init__()
# 定义模型 列表
self.linear_layer = nn.ModuleList(
[nn.Linear(in_features=in_dim, out_features=out_dim) for in_dim, out_dim in zip(HP.layer_list[:-1], HP.layer_list[1:])])
def forward(self, input_x):
"""前向计算"""
for layer in self.linear_layer:
# 先进行线性计算,然后增加激活函数
input_x = layer(input_x)
input_x = F.relu(input_x)
return input_x
复制代码
模型训练
模型训练过程:
- 准备:获取模型,获取损失函数,加载数据集,设置损失函数,定义优化器
# 定义模型
model = BanknoteClassificationModel()
model = model.to(HP.device)
# 定义损失函数:交叉熵
criterion = nn.CrossEntropyLoss()
# 定义优化器
opt = optim.Adam(model.parameters(), lr=HP.init_lr)
# train dataloader数据获取
trainset = BanknoteDataset(HP.trainset_path)
# 设置数据加载器:数据集 设置每次取数据行数,是否打乱,多余部分丢弃
train_loader = DataLoader(
trainset, batch_size=HP.batch_size, shuffle=True, drop_last=True)
# dev dataloader数据获取
devset = BanknoteDataset(HP.devset_path)
# 设置数据加载器:数据集 设置每次取数据行数,是否打乱,(不涉及训练选择不丢弃)
dev_loader = DataLoader(
devset, batch_size=HP.batch_size, shuffle=True, drop_last=False)
复制代码
- 开始训练:取每一批次的数据,训练流程如下:
- 梯度置0。
- 获取预测值与实际值
- 计算损失值
- 损失反向求导
- 更新模型
for epoch in range(start_epoch, HP.epochs):
print("Start Epoch:%d,Steps: %d" %
(epoch, len(train_loader) / HP.batch_size))
# 取每一批次的数据
for batch in train_loader:
x, y = batch
opt.zero_grad() # graninit clean
pred = model(x) # forward process
loss = criterion(pred, y) # loss calc
loss.backward() # 反向求导
opt.step() # 更新模型
# 记录每次训练的损失值
logger.add_scalar('loss/Train', loss, step)
复制代码
- 额外配置:在特定的点记录模型,记录训练损失值和开发损失值。
# 测试时候,每verbose_step次记录损失值
if not step % HP.verbose_step:
eval_loss = evaluate(model, dev_loader, criterion)
logger.add_scalar('Loss/Dev', eval_loss, step)
# 在 save_step 次进行存档
if not step % HP.save_step:
model_path = 'model_%d_%d.pth' % (epoch, step)
save_checkpoint(model, epoch, opt, os.path.join(
'model_save', model_path))
step += 1
logger.flush()
print('Epoch:[%d/%d],step:%d Train Loss:%.5f.Dev Loss:%0.5f' %
(epoch, HP.epochs, step, loss.item(), eval_loss))
复制代码
模型评估和选择
在相关文件下命令行下输入 tensorboard --logdir=./
就可以得到一个网站,可以查看相关的模型生成。
由上图可以得知:在600步时候,模型已经是比较好的模型了,200步处于欠拟合,1.4K步会存在过拟合,所以后面测试时候,应该选用600步存储的模型。
测试
根据选择训练好的模型,进行对模型加载相关参数。加载测试数据集,开始进行测试记录正确数据个数,得到预测准确率。
步骤:
- 加载合适的模型。
- 获取测试集
- 开始进行测试与验证,得出结果。
import torch
from config import HP
from dataset_banknote import BanknoteDataset
from model import BanknoteClassificationModel
from torch.utils.data import DataLoader
# 加载模型
model = BanknoteClassificationModel()
# 选择优化后的模型开始进行测试
checkpoint = torch.load('./model_save/model_40_600.pth')
model.load_state_dict(checkpoint['model_state_dict'])
# 获取测试集
testset = BanknoteDataset(HP.testset_path)
test_loader = DataLoader(
testset, batch_size=HP.batch_size, shuffle=True, drop_last=False)
model.eval()
total_cnt = 0 # 总共数据个数
correct_cnt = 0 # 正确数据个数
with torch.no_grad():
for batch in test_loader:
x, y = batch
pred = model(x) # 开始测试
total_cnt += pred.size(0)
correct_cnt += (torch.argmax(pred, 1) == y).sum()
print('Acc:%.3f' % (correct_cnt / total_cnt))
# Acc:1.000
复制代码