系统环境

  • Ubuntu 18.04 LTS
  • Python 3.7.6
  • PyTorch 1.4.0
  • CUDA 10.1
  • cuDNN 7.6.5

Windows下有相应环境也可以,我电脑Linux下风扇驱动不理想,跑GPU太烫,训练的代码在Windows上也可以跑。

制作数据集

数据集在“素材”目录下,以下是num目录结构说明。

  1. .
  2. └── num
  3. ├── 0-original 负样本原图
  4. └── 0
  5. ├── 0-processed 负样本处理图
  6. └── 0
  7. ├── num1-original 数字1原图
  8. └── num1
  9. ├── num1-processed 数字1处理图
  10. └── num1
  11. ├── num2-original 数字2原图
  12. └── num2
  13. ├── num2-processed 数字2处理图
  14. └── num2
  15. ├── num3-original 数字3原图
  16. └── num3
  17. ├── num3-processed 数字3处理图
  18. └── num3
  19. ├── num4-original 数字4原图
  20. └── num4
  21. ├── num4-processed 数字4处理图
  22. └── num4
  23. ├── num5-original 数字5原图
  24. └── num5
  25. ├── num5-processed 数字5处理图
  26. └── num5
  27. ├── sentinel-original 哨兵标记原图
  28. └── sentinel
  29. └── sentinel-processed 哨兵标记处理图
  30. └── sentinel

根据目录结构,制作数据集,在CSV文件中打印图片路径和对应分类标签,

  1. """
  2. @author starrysky
  3. @date 2020/08/16
  4. @details 制作数据集标签
  5. """
  6. import pandas as pd
  7. import os
  8. df = pd.DataFrame({
  9. "image_path": [],
  10. "label": [],
  11. })
  12. # 素材文件目录路径
  13. src_dir = r"./素材/num/0-processed/0/"
  14. files = os.listdir(src_dir)
  15. for i in files:
  16. # print(src_dir + i)
  17. df.loc[df.shape[0] + 1] = {
  18. "image_path": src_dir + i,
  19. "label": 0,
  20. }
  21. dir_type_list = ["close", "closeh", "far", "farh"]
  22. label_type_list = ["num1", "num2", "num3", "num4", "num5", "sentinel"]
  23. for num in range(0, 6):
  24. for dir_name in dir_type_list:
  25. # 素材文件目录路径
  26. src_dir = r"./素材/num/" + label_type_list[num] + "-processed/" + label_type_list[num] + "/" + dir_name + "/"
  27. files = os.listdir(src_dir)
  28. for i in files:
  29. # print(src_dir + i)
  30. df.loc[df.shape[0] + 1] = {
  31. "image_path": src_dir + i,
  32. "label": num + 1,
  33. }
  34. # print(df)
  35. df.to_csv("./素材/num/label.csv")


在PyTorch中自定义数据集

在PyTrich种制作数据集主要是要将样本转换成Tensor格式,首先要继承torch.utils.data.Dataset这个父类,然后根据PyTorch的要求实现一些魔法方法,如initgetitemlen

  1. trans = transforms.ToTensor()
  2. def default_loader(path):
  3. """
  4. 定义读取图片的格式为28*28的单通道灰度图
  5. :param path: 图片路径
  6. :return: 图片
  7. """
  8. return Image.open(path).convert('L').resize((28, 28))
  9. class MyDataset(Dataset):
  10. """
  11. 制作数据集
  12. """
  13. def __init__(self, csv_path, transform=None, loader=default_loader):
  14. """
  15. :param csv_path: 文件路径
  16. :param transform: 转后后的Tensor格式
  17. :param loader: 图片加载方式
  18. """
  19. super(MyDataset, self).__init__()
  20. df = pd.read_csv(csv_path, engine="python", encoding="utf-8")
  21. self.df = df
  22. self.transform = transform
  23. self.loader = loader
  24. def __getitem__(self, index):
  25. """
  26. 按照索引从数据集提取对应样本的信息
  27. Args:
  28. index: 索引值
  29. Returns:
  30. 特征和标签
  31. """
  32. fn = self.df.iloc[index][1]
  33. label = self.df.iloc[index][2]
  34. img = self.loader(fn)
  35. # 按照路径读取图片
  36. if self.transform is not None:
  37. # 数据标签转换为Tensor
  38. img = self.transform(img)
  39. return img, label
  40. def __len__(self):
  41. """
  42. 样本数量
  43. Returns:
  44. 样本数量
  45. """
  46. return len(self.df)

定义好之后就可以创建数据集对象,打印着看一下。

  1. # 数据集元数据文件的路径
  2. metadata_path = r"./素材/num/label.csv"
  3. # 批次规模
  4. batch_size = 64
  5. # 线程数
  6. if sys.platform == "win32":
  7. num_workers = 0
  8. else:
  9. num_workers = 12
  10. # 训练集占比
  11. train_rate = 0.8
  12. # 创建数据集
  13. src_data = MyDataset(csv_path=metadata_path, transform=trans)
  14. print('num_of_trainData:', len(src_data))
  15. # K折交叉验证
  16. train_size = int(train_rate * len(src_data))
  17. test_size = len(src_data) - train_size
  18. train_set, test_set = torch.utils.data.random_split(src_data, [train_size, test_size])
  19. train_iter = DataLoader(dataset=train_set, batch_size=batch_size, shuffle=True, num_workers=num_workers)
  20. test_iter = DataLoader(dataset=test_set, batch_size=batch_size, shuffle=True, num_workers=num_workers)
  21. # 打印数据集
  22. for i, j in train_iter:
  23. print(i, j)
  24. break

运行结果如下,展示了一个批次共64个样本。

  1. num_of_trainData: 8120
  2. tensor([[[[0.0039, 0.0000, 0.0000, ..., 0.0000, 0.0000, 0.0000],
  3. [0.0000, 0.0157, 0.0000, ..., 0.0000, 0.0000, 0.0000],
  4. [0.0039, 0.0000, 0.0078, ..., 0.0000, 0.0000, 0.0000],
  5. ...,
  6. [0.0000, 0.0000, 0.0000, ..., 0.0000, 0.0000, 0.0000],
  7. [0.0000, 0.0000, 0.0000, ..., 0.0000, 0.0000, 0.0000],
  8. [0.0000, 0.0000, 0.0000, ..., 0.0000, 0.0000, 0.0000]]],
  9. [[[0.9922, 1.0000, 0.0000, ..., 0.0039, 0.0000, 0.0039],
  10. [1.0000, 0.9725, 0.0000, ..., 0.0078, 0.0000, 0.0235],
  11. [0.0000, 0.0000, 0.0039, ..., 0.0000, 0.0000, 0.0000],
  12. ...,
  13. [0.0000, 0.0000, 0.0000, ..., 0.0000, 0.0196, 0.0078],
  14. [0.0000, 0.0000, 0.0000, ..., 0.0235, 0.0078, 0.0000],
  15. [0.0000, 0.0000, 0.0000, ..., 0.0039, 0.0000, 1.0000]]],
  16. [[[0.0000, 0.0000, 0.0000, ..., 0.0078, 0.0000, 0.0000],
  17. [0.0000, 0.0000, 0.0000, ..., 0.0000, 0.0000, 0.0039],
  18. [0.0000, 0.0000, 0.0000, ..., 0.0157, 0.0039, 0.0000],
  19. ...,
  20. [0.0000, 0.0000, 0.0157, ..., 0.0000, 0.0000, 0.0078],
  21. [0.0039, 0.0118, 0.0000, ..., 0.0549, 0.0000, 0.0000],
  22. [0.0000, 0.0039, 0.0000, ..., 0.0000, 0.0118, 0.0235]]],
  23. ...,
  24. [[[0.9373, 0.0118, 0.0000, ..., 0.0000, 0.0000, 0.0000],
  25. [0.1529, 0.1255, 0.1059, ..., 0.0000, 0.0000, 0.0000],
  26. [0.9961, 1.0000, 0.7412, ..., 0.0000, 0.0000, 0.0000],
  27. ...,
  28. [0.0275, 0.5765, 0.5804, ..., 0.0000, 0.0000, 0.0000],
  29. [0.0078, 0.0000, 0.0039, ..., 0.0000, 0.0000, 0.0000],
  30. [0.0000, 0.0000, 0.0000, ..., 0.0000, 0.0000, 0.0000]]],
  31. [[[1.0000, 0.9961, 1.0000, ..., 1.0000, 1.0000, 1.0000],
  32. [0.9961, 1.0000, 0.9804, ..., 1.0000, 1.0000, 1.0000],
  33. [0.9961, 1.0000, 0.9961, ..., 0.9961, 0.9961, 0.9961],
  34. ...,
  35. [0.9961, 1.0000, 1.0000, ..., 1.0000, 1.0000, 1.0000],
  36. [1.0000, 0.9843, 1.0000, ..., 1.0000, 1.0000, 1.0000],
  37. [0.0000, 1.0000, 1.0000, ..., 0.0000, 0.0000, 0.0000]]],
  38. [[[0.0000, 0.0000, 0.0078, ..., 0.9922, 1.0000, 1.0000],
  39. [0.0000, 0.0039, 0.0078, ..., 1.0000, 0.9961, 1.0000],
  40. [0.0000, 0.0000, 0.2980, ..., 0.9961, 0.9843, 0.9922],
  41. ...,
  42. [0.0039, 0.0039, 0.0078, ..., 1.0000, 1.0000, 1.0000],
  43. [0.0000, 0.0000, 0.0000, ..., 1.0000, 1.0000, 0.9647],
  44. [0.0078, 0.0039, 0.0157, ..., 1.0000, 0.9843, 1.0000]]]]) tensor([4., 6., 0., 6., 6., 4., 6., 2., 4., 6., 4., 6., 2., 6., 2., 1., 5., 4.,
  45. 2., 2., 4., 3., 2., 1., 3., 3., 2., 6., 4., 3., 5., 4., 6., 2., 2., 5.,
  46. 1., 2., 1., 1., 6., 5., 6., 4., 3., 2., 2., 3., 4., 1., 5., 5., 2., 6.,
  47. 2., 2., 5., 3., 3., 5., 2., 3., 6., 6.])

定义模型

之前做MNIST问题的时候使用LeNet也可以取得比较不错的效果,这里再考虑到小电脑的计算能力,因为实际应用对于实时性的要求也非常高,暂时没有使用一些深度卷积神经网络的经典模型。
整个模型结构就是基本使用了LeNet的结构,我只修改了输出层参数为7。

  1. class LeNet(nn.Module):
  2. """
  3. 定义模型, 这里使用LeNet
  4. """
  5. def __init__(self):
  6. super(LeNet, self).__init__()
  7. # 卷积层
  8. self.conv = nn.Sequential(
  9. # 输入通道数, 输出通道数, kernel_size
  10. nn.Conv2d(1, 6, 5),
  11. nn.Sigmoid(),
  12. # 最大池化
  13. nn.MaxPool2d(2, 2),
  14. nn.Conv2d(6, 16, 5),
  15. nn.Sigmoid(),
  16. nn.MaxPool2d(2, 2)
  17. )
  18. # 全连接层
  19. self.fc = nn.Sequential(
  20. nn.Linear(16 * 4 * 4, 120),
  21. nn.Sigmoid(),
  22. nn.Linear(120, 84),
  23. nn.Sigmoid(),
  24. nn.Linear(84, 7)
  25. )
  26. def forward(self, img):
  27. feature = self.conv(img)
  28. output = self.fc(feature.view(img.shape[0], -1))
  29. return output
  30. net = LeNet()
  31. print(net)

模型结构展示。

  1. LeNet(
  2. (conv): Sequential(
  3. (0): Conv2d(1, 6, kernel_size=(5, 5), stride=(1, 1))
  4. (1): Sigmoid()
  5. (2): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
  6. (3): Conv2d(6, 16, kernel_size=(5, 5), stride=(1, 1))
  7. (4): Sigmoid()
  8. (5): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
  9. )
  10. (fc): Sequential(
  11. (0): Linear(in_features=256, out_features=120, bias=True)
  12. (1): Sigmoid()
  13. (2): Linear(in_features=120, out_features=84, bias=True)
  14. (3): Sigmoid()
  15. (4): Linear(in_features=84, out_features=7, bias=True)
  16. )
  17. )


评估模型

计算模型的分类正确率。

  1. # %% 模型评估
  2. def evaluate_accuracy(data_iter, net, device=None):
  3. """
  4. 评估模型, GPU加速运算
  5. :param data_iter: 测试集迭代器
  6. :param net: 待评估模型
  7. :param device: 训练设备
  8. :return: 正确率
  9. """
  10. # 未指定训练设备的话就使用 net 的 device
  11. if device is None and isinstance(net, nn.Module):
  12. device = list(net.parameters())[0].device
  13. acc_sum, n = 0.0, 0
  14. with torch.no_grad():
  15. for x, y in data_iter:
  16. if isinstance(net, nn.Module):
  17. # 评估模式, 关闭dropout(丢弃法)
  18. net.eval()
  19. acc_sum += (net(x.to(device)).argmax(dim=1) == y.to(device)).float().sum().cpu().item()
  20. # 改回训练模式
  21. net.train()
  22. n += y.shape[0]
  23. return acc_sum / n


训练模型

参考之前跟着Dive-into-DL-PyTorch学习时候的一套训练流程。

  1. def train_model(net, train_iter, test_iter, loss_func, optimizer, device, num_epochs):
  2. """
  3. 训练模型
  4. :param net: 原始网络
  5. :param train_iter: 训练集
  6. :param test_iter: 测试集
  7. :param loss_func: 损失函数
  8. :param optimizer: 优化器
  9. :param device: 训练设备
  10. :param num_epochs: 训练周期
  11. :return: 无
  12. """
  13. net = net.to(device)
  14. print("训练设备={0}".format(device))
  15. for i in range(num_epochs):
  16. # 总误差, 准确率
  17. train_lose_sum, train_acc_sum = 0.0, 0.0
  18. # 样本数量, 批次数量
  19. sample_count, batch_count = 0, 0
  20. # 训练时间
  21. start = time.time()
  22. for x, y in train_iter:
  23. # x, y = j
  24. x = x.to(device)
  25. y = y.long().to(device)
  26. y_output = net(x)
  27. lose = loss_func(y_output, y)
  28. optimizer.zero_grad()
  29. lose.backward()
  30. optimizer.step()
  31. train_lose_sum += lose.cpu().item()
  32. train_acc_sum += (y_output.argmax(dim=1) == y).sum().cpu().item()
  33. sample_count += y.shape[0]
  34. batch_count += 1
  35. test_acc = evaluate_accuracy(test_iter, net)
  36. print("第{0}个周期, lose={1:.3f}, train_acc={2:.3f}, test_acc={3:.3f}, time={4:.1f}".format(
  37. i, train_lose_sum / batch_count, train_acc_sum / sample_count, test_acc, time.time() - start
  38. ))


训练过程

配置超参数,训练模型。由于数据集也不是很大,采用K折交叉验证来重用数据。

  1. if __name__ == '__main__':
  2. # %% 设置工作路径
  3. # print(os.getcwd())
  4. # os.chdir(os.getcwd() + "\learn")
  5. # 获取当前文件路径
  6. print(os.getcwd())
  7. # %% 超参数配置
  8. # 数据集元数据文件的路径
  9. metadata_path = r"./素材/num/label.csv"
  10. # 训练设备
  11. device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
  12. # 批次规模
  13. batch_size = 64
  14. # 线程数
  15. if sys.platform == "win32":
  16. num_workers = 0
  17. else:
  18. num_workers = 12
  19. # 训练集占比
  20. train_rate = 0.8
  21. # 创建数据集
  22. # src_data = MyDataset(csv_path=metadata_path, transform=trans)
  23. src_data = MyDataset(csv_path=metadata_path, transform=trans)
  24. print('num_of_trainData:', len(src_data))
  25. # K折交叉验证
  26. train_size = int(train_rate * len(src_data))
  27. test_size = len(src_data) - train_size
  28. train_set, test_set = torch.utils.data.random_split(src_data, [train_size, test_size])
  29. train_iter = DataLoader(dataset=train_set, batch_size=batch_size, shuffle=True, num_workers=num_workers)
  30. test_iter = DataLoader(dataset=test_set, batch_size=batch_size, shuffle=True, num_workers=num_workers)
  31. # 打印数据集
  32. for i, j in train_iter:
  33. print(i, j)
  34. break
  35. net = LeNet()
  36. print(net)
  37. # 训练次数
  38. num_epochs = 5
  39. # 优化算法
  40. optimizer = torch.optim.Adam(net.parameters(), lr=0.002)
  41. # 交叉熵损失函数
  42. loss_func = nn.CrossEntropyLoss()
  43. for i in range(10):
  44. # K折交叉验证
  45. train_size = int(train_rate * len(src_data))
  46. test_size = len(src_data) - train_size
  47. train_set, test_set = torch.utils.data.random_split(src_data, [train_size, test_size])
  48. train_iter = DataLoader(dataset=train_set, batch_size=batch_size, shuffle=True, num_workers=num_workers)
  49. test_iter = DataLoader(dataset=test_set, batch_size=batch_size, shuffle=True, num_workers=num_workers)
  50. train_model(net, train_iter, test_iter, loss_func, optimizer, device, num_epochs)


保存模型

保存训练好的参数,以便后续使用。

  1. # 保存训练后的模型数据
  2. torch.save(net.state_dict(), "./model_param/state_dict.pt")


测试模型

测试在整个数据集上的正确率,并导出供LibTorch使用的模型,整体速度还是非常快。

  1. """
  2. @author starrysky
  3. @date 2020/08/16
  4. @details 加载训练好的模型参数, 测试模型, 导出完整的模型供C++项目部署使用
  5. """
  6. import sys
  7. sys.path.append("./")
  8. import time
  9. import torch
  10. from learn import mymodel
  11. import os
  12. import pandas as pd
  13. # %%
  14. # print(os.getcwd())
  15. # os.chdir(os.getcwd() + r"\test")
  16. print(os.getcwd())
  17. device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
  18. # %%
  19. model = mymodel.LeNet()
  20. model.load_state_dict(torch.load("./model_param/state_dict.pt"))
  21. model.eval()
  22. print(model)
  23. # 加载样本和标签
  24. df = pd.read_csv("./素材/num/label.csv", index_col=0)
  25. df["predict"] = None
  26. df["is_correct"] = None
  27. print(df.columns)
  28. # %%
  29. ans = 0
  30. start_time = time.time()
  31. for i in range(df.shape[0]):
  32. image_path = df.iloc[i, 0]
  33. image = mymodel.default_loader(image_path)
  34. label = df.iloc[i, 1]
  35. x = mymodel.trans(image)
  36. x_ = x.view(1, 1, 28, 28)
  37. y_predict = model(x_).argmax(dim=1).item()
  38. # print(i, y_predict)
  39. df.iloc[i, 2] = y_predict
  40. df.iloc[i, 3] = y_predict == label
  41. if y_predict == label:
  42. ans += 1
  43. print("正确样本数:{0}, 正确率={1:.4f}".format(ans, ans / df.shape[0]))
  44. print("测试时间:{0:.4f}".format(time.time() - start_time))
  45. # %%
  46. df.to_csv("result.csv", index=False)
  47. image_path = df.iloc[0, 0]
  48. image = mymodel.default_loader(image_path)
  49. x = mymodel.trans(image)
  50. x_ = x.view(1, 1, 28, 28)
  51. traced_script_module = torch.jit.trace(model, x_)
  52. traced_script_module.save("./libtorch_model/model.pt")

小结

后续在C++和OpenCV环境下的部署。在C++环境下和小电脑上的运行时间还是未知数,但是鉴于当前LeNet的运行速度还是比较快的,后续可以考虑使用深度卷积神经网络的模型。