- 2024-06-05
-
回复了主题帖:
#AI挑战营终点站# rv1106数字识别模型部署与问题求助
ccccccc@ 发表于 2024-6-4 10:20
这个要看到具体代码才能知晓问题的具体原因,至于contiguous()不能使用,他的作用是将非连续的张量转换为连 ...
感谢回复,我回去尝试一下修改后代码是否可以运行。
下面这些是我当时的报错信息以及我定位到的报错位置:
参考模型结构,我定位到了如下位置:
该模块在代码中的位置应该是这里,但是我后来又查看了最新的RKNNToolKit2_OP_Support-v2.0.0-beta0.md,我发现这段代码的每一个函数好像都是支持的,你知道这是为什么吗? 这是最新的工具包支持的算子的链接:
rknn-toolkit2/doc/RKNNToolKit2_OP_Support-v2.0.0-beta0.md at master · airockchip/rknn-toolkit2 (github.com)
或者说是我找错了位置吗?
- 2024-06-01
-
发表了主题帖:
#AI挑战营终点站# rv1106数字识别模型部署与问题求助
## AI训练营最终站
哎,最后yolov5还是没有搞出来,这个帖子就是复现了前面knv大佬的最终结果来完成这次的AI训练营任务,还有就是求助各位大佬来解决一下我yolov5模型的部署过程中的问题。
## 部署效果
因为是复现的别人的代码,所以我这里就不讲如何搭建环境了,因为大佬们的帖子里已经写的很详细了,我自己写的过程记录的都是关于yolov5的,就不放上来了,这里放一下我部署在开发板上的识别视频。
[localvideo]ab903093771fa6c587bc7a4c93f51bd8[/localvideo]
我一开始也是用CNN实现的,但是我看CNN实现的时候对环境要求比较大,因为MNIST数据集的背景都是纯色,没有噪声干扰,所以对于实际识别时会有较大的误差,比如光线角度等等都会影响到识别的结果,所以我打算使用自己的数据集训练一个yolov5模型来替换掉CNN模型,这样我还可以实现同时识别多个数字,这样还可以进行下一步的扩展,但是在不断的炼模型改代码的路上,最终还是没能成功,所以想在这里请教一下有没有懂得大佬,来解答一下我还没有解决的几个问题,这些问题解决了应该就能把yolov5模型部署到开发板上了。
## yolo部署问题
因为是刚学AI,所以对网络训练都不熟悉,再加上最近一堆课设和作业,导致这边开始的有点慢。原本打算用yolov5来做一个多目标识别然后进行一些拓展功能的,过程记录都写了一百多行了,结果自己没跑成功,也是真正的认识到了AI学习的不容易。
模型部署的过程中一直遇到各种问题,我这些问题在rknn的交流群里面也有人问,但是没有人去解答,网上也都不说这个问题,好像默认大家都会这个。
### 模型的输出维度不对
我查看了rknn_model_zoo-2.0.0下yolov5例程里面的模型yolov5s_relu.onnx,我发现他的输出是:
但是我的模型输出是:
这是我第一次在网上找了一个教程训练的yolov5 v5.0的模型输出,他的维度是五维,我看网上说把第二维和第五维相乘合并后得到的四维就和上面的那个模型维度相同了,但是最后模型输出是错的:
这是我的第二个模型,我训练的yolov5 v7.0的模型,他只有3维,网上是说把三个输出合并在一起了,也就是3*(80*80+40*40+20*20),都和在一起我尝试了很久也没有把他的结果解析出来
做到这里的时候我发现我做的好像不太对,我去例程下又找了一下,我发现他官方例程用的不是纯yolov5的代码他自己又修改了一下:
然后我按照他的官方网站又去看了一下,按照教程重新训练了一个模型,这个模型的输出就变成了:
这还是与前面例程给的维度不一样,没法用例程部署到rv1106上。
### 算子不支持
后来我打算先忽略这个问题,将模型导出为rknn模型在板子上跑一下,然后就是发现每个模型都有不支持的算子,我把能改的算子改了之后遇到一个他说不支持contiguous()这个函数,但是我在网上没有找到能替代这个函数的代码,有没有大佬可以解答一下疑惑了。
我试了很多模型,基本每个都有算子不支持的。我用他rknn_model_zoo推荐的代码去训练也有算子不支持,而且输出维度好像还是不对,是因为版本更新了吗,我可以如何去将yolov5部署到我的开发板上去?
- 2024-05-27
-
加入了学习《Follow me 第4期任务总结视频》,观看 任务总结视频
- 2024-05-09
-
回复了主题帖:
【AI挑战营第二站】算法工程化部署打包成SDK
ONNX模型其实就是我们训练出来的模型的一个权重文件保存形式,我们第一站训练出来的手写识别模型一开始导出的模型为.pth结尾,那我们为什么要将其转换为.ONNX模型呢?
这是为了方便模型之间的转化,设立了一个中间模型ONNX模型,它其实就相当于.pth改了个后缀而已,但是它可以转化成其他很多模型,比如ncnn、TensorRT、TVM以及我们这里说的RKNN模型。
他充当了一个中转站的功能,类似于我们计算机网络中的交换机,充当一个转发的功能,这样我们就不用为任意两个模型都编写一个转换函数了,以后出现一个新的模型只需要编写一个将其转化为ONNX模型的函数就可以很方便的将其应用在其他不同的地方了。比如我们有3个模型,如果两两之间需要转化每个模型至少需要两个转换函数,如果有30个模型,那每个模型需要适配29个转换函数,这样是非常不合理的,而有了ONNX模型,只需要适配两个函数,一个转化为ONNX模型和一个ONNX转化为自己的函数,这样对于后期模型开发和维护也更加方便。
RKNN模型跟其他的模型也差不多,大致是权重、结构和配置,只不过它是是Rockchip NPU平台专用的模型文件格式,可以被RKNN推理引擎加载和运行。RKNN模型文件通常以.rknn后缀结尾。
RKNN模型文件通常由Rockchip NPU平台上的模型转换工具生成,也可以由用户自行编写代码生成。
Rockchip 提供了完整了模型转换 Python 工具,方便用户将自主研发的算法模型转换成 RKNN 模型,同时 Rockchip 也提供了C/C++和Python API 接口。
#AI挑战营第二站#从零开始教你使用RKNN-Toolkit进行模型转换:
https://bbs.eeworld.com.cn/thread-1281291-1-1.html
-
发表了主题帖:
#AI挑战营第二站#从零开始教你使用RKNN-Toolkit进行模型转换
本帖最后由 Alex_Jun 于 2024-5-9 20:54 编辑
# RKNN-Toolkit环境部署与使用
RKNN-Toolkit是一款软件开发套件,可在PC和瑞芯微NPU平台(RK1808/RK1806/RK3399Pro/RV1109/RV1126)上为用户提供模型转换、推理和性能评估。
想要做好一件事,我们首先得要了解我们所需要的东西,正所谓工欲善其事必先利其器。
## 一、 下载RKNN-Toolkit
```
RKNN-Toolkit的github地址为:
https://github.com/rockchip-linux/rknn-toolkit
```
我们所要部署的开发板为幸狐RV1106开发板,
因此我们应该参考
因此我们点击链接跳转到rknn-toolkit2的链接去
```
https://github.com/rockchip-linux/rknn-toolkit2
```
但是我们发现它以及不再维护和更新了,不过给了我们新的地址,我们点击跳转到新的地址去:
```
https://github.com/airockchip/rknn-toolkit2/
```
我用网页给大家翻译了一下,方便我们查看:
具体的东西我们可以不用很了解,但是我们可以看到这句话:
这不就是我们需要的功能吗,所以我们需要下载这个仓库。
但是要注意,如果你下载了RKNN-Toolkit可能会出问题,因为它说两个不相容。
## 二、寻找教程
如果你不想寻找教程可以直接跳过这一步,我会将教程里面用到的东西贴出来,可以直接跳转**3.使用教程**,但是如果你想要了解这个工具,那么你一定要找到教程。
那么,在这个页面翻一翻,瞧我们发现了什么,更多的例程,有了例程不就可以来帮助我们理解和使用工具了吗?
我们点击链接进去:
```
https://github.com/airockchip/rknn_model_zoo
```
在这个页面也翻一翻,找到了什么,**Quick Start**
我们点进这个链接看一看:
```
https://github.com/airockchip/rknn-toolkit2/tree/master/doc
```
我相信你一定看到了很多版本的教程文件,我们应该选择RV1106对应的且后缀为CN的.pdf教程,如果你想学英语也可以选择EN结尾的。
下载文件打开,先看封面再看目录,好的确实是我们要找的教程了,没错了:
## 三、使用教程
教程找到了,我们又没有开发板,所以我们直接从**3 准备开发环境**开始寻找有用的东西。
好的,看完了,已经学会了,我们可以开始了:
### 1.下载仓库
这一步直接照着他的做可以,文件夹的名字可以修改,我这里使用的文件名是**code**,这个不影响后续操作。
### 2.安装 RKNN-Toolkit2 环境
```
conda -V
```
查看是否安装,虽然这一步我安装了也是这个结果,但是看一看,一般的新的虚拟机都没有安装,我们直接下一步就行。
下载安装包链接,直接复制到命令行运行就行,这个会自己下载的:
```
wget -c https://mirrors.bfsu.edu.cn/anaconda/miniconda/Miniconda3-latest-Linux-x86_64.sh
```
安装conda,也是照做就行,反正都是新手教程。
```
chmod 777 Miniconda3-latest-Linux-x86_64.sh
bash Miniconda3-latest-Linux-x86_64.sh
```
**使用 Conda 创建 Python 环境**
在命令行输入:
```
source ~/miniconda3/bin/activate
```
通过以下命令创建名称为 toolkit2 的 Python 3.8 环境:
```
conda create -n toolkit2 python=3.8
```
我已经创建过了就不再创建了,接下来我们激活 toolkit2 环境, 后续将在此环境中安装 RKNN-Toolkit2:
```
conda activate toolkit2
```
**安装依赖库和 RKNN-Toolkit2**
我们根据自己的路径来,只要到packages的上一级目录就行:
接下来就是安装依赖了,我们上面创建的python环境是3.8的,教程上说根据python版本选择依赖文件,因此我们选择**requirements_cp38-2.0.0b0.txt**文件。
*为什么我这里只有38和39?因为不想占内存所有,我把其他的都删了,剩个39懒得删了*。
所以我们输入的命令为:
```
pip install -r packages/requirements_cp38-2.0.0b0.txt
```
接下来就是等它下载...
如果出现**Read Time OUT**错误怎么办?
再执行一遍就行,我第一遍也超时了,多试几次就行,或者可以上网百度conda pip 换源,但是这里还挺快,所以我没换了。
然后来安装工具了,我们还是根据python版本选择38的那个:
```
pip install packages/rknn_toolkit2-2.0.0b09bab5682-cp38-cp38-linux_x86_64.whl
```
查看python是否安装成功
输入以下命令,没报错就是对的:
```
# 进入 Python 交互模式
python
# 导入 RKNN 类
from rknn.api import RKNN
```
那么,到这里我们的开发环境就搭建的差不多了,至少转换模型所需要的环境已经够了,后面的等板子到了再说吧。
### 3.准备转换
在运行之前我们需要进行一点改变,因为我们使用的不是yolo模型,而是MNIST训练的Pytorch模型,与网上的yolov5模型不同,这里我给一个网站:
```
https://netron.app/
```
这是别人yolov5模型的输入:
我相信你肯定发现不同了,我们这个输入是**input.1**我现在也不知都这个是为什么,但是跟yolo肯定是有点区别的。也会对后续我们的操作造成一点不同。
我们需要修改
```
rknn_model_zoo-2.0.0/examples/yolov5/python/convert.py
```
中的四十四行:
```python
# rknn.config(mean_values=[[0, 0, 0]], std_values=[
# [255, 255, 255]], target_platform=platform)
#修改为
rknn.config(target_platform=platform)
```
接下来就是模型转换了,我们输入这里的命令应该是
```
#注意my_model为我们模型的名字,将其放到model路径下
python convert.py ../model/my_model.onnx rv1106 i8 ../model/my_model.rknn
```
执行完命令就会生成.rknn模型了。
---
### 4.代码解读
这是我们使用到的官方的代码,我来浅浅的解读一下。
```python
import sys
from rknn.api import RKNN
DATASET_PATH = '../../../datasets/MNIST/mnist_subset_20.txt' # 这个就是我们测试数据的路径,用来量化模型
DEFAULT_RKNN_PATH = '../model/yolov5.rknn' # 这是默认的模型输出路径和名字,我们命令行里面输入流参数就不会使用这个了
DEFAULT_QUANT = True # 这个就是量化,RKNN库会根据数据集文件(就是我们上面那个路径)中的数据,自动调整量化结果以获得最佳性能。
# 这就是对参数进行一个处理,咱不管他
def parse_arg():
if len(sys.argv) < 3:
print("Usage: python3 {} onnx_model_path [platform] [dtype(optional)] [output_rknn_path(optional)]".format(sys.argv[0]))
print(" platform choose from [rk3562,rk3566,rk3568,rk3588,rk1808,rv1109,rv1126]")
print(" dtype choose from [i8, fp] for [rk3562,rk3566,rk3568,rk3588]")
print(" dtype choose from [u8, fp] for [rk1808,rv1109,rv1126]")
exit(1)
model_path = sys.argv[1]
platform = sys.argv[2]
do_quant = DEFAULT_QUANT
if len(sys.argv) > 3:
model_type = sys.argv[3]
if model_type not in ['i8', 'u8', 'fp']:
print("ERROR: Invalid model type: {}".format(model_type))
exit(1)
elif model_type in ['i8', 'u8']:
do_quant = True
else:
do_quant = False
if len(sys.argv) > 4:
output_path = sys.argv[4]
else:
output_path = DEFAULT_RKNN_PATH
return model_path, platform, do_quant, output_path
if __name__ == '__main__':
model_path, platform, do_quant, output_path = parse_arg()
# 创建一个rknn模型,verbose=False为不打印输出信息
rknn = RKNN(verbose=False)
# 这就是我们的预处理配置,设置目标平台,参数从命令行得到
print('--> Config model')
rknn.config(target_platform=platform)
print('done')
# 加载我们的onnx模型
print('--> Loading model')
ret = rknn.load_onnx(model=model_path)
if ret != 0:
print('Load model failed!')
exit(ret)
print('done')
# 根据onnx模型转化为rknn模型
print('--> Building model')
ret = rknn.build(do_quantization=do_quant, dataset=DATASET_PATH)
if ret != 0:
print('Build model failed!')
exit(ret)
print('done')
# 将训练好的模型导出到文件中
print('--> Export rknn model')
ret = rknn.export_rknn(output_path)
if ret != 0:
print('Export rknn model failed!')
exit(ret)
print('done')
# 释放资源
rknn.release()
```
看完代码我们发现有用的就那么几行,所以我们这一站也算是比较简单,但是前面环境配置比较费时间,网上的教程我找了一些后来还是打算自己弄,弄完发现也不是很难对吧。
## 总结
那么我们这一站的任务就完成了,但是目前板子还没到无法验证我们转化后的模型性能,所以后续打算将模型部署到 RV1106 Linux 开发板上再来决定是否需要更换模型。
让我们一起期待板子的到来,来进行后续的测试吧!
最后贴上转化前后的模型吧:
-
回复了主题帖:
入围名单公布:嵌入式工程师AI挑战营(初阶),获RV1106 Linux 板+摄像头的名单
个人信息已确认,领取板卡,可继续完成&分享挑战营第二站和第三站任务
- 2024-05-08
-
回复了主题帖:
入围名单公布:嵌入式工程师AI挑战营(初阶),获RV1106 Linux 板+摄像头的名单
本帖最后由 Alex_Jun 于 2024-5-9 15:40 编辑
个人信息已确认,领取板卡,可继续完成&分享挑战营第二站和第三站任务
- 2024-04-25
-
回复了主题帖:
免费申请:幸狐 RV1106 Linux 开发板(带摄像头),助力AI挑战营应用落地
【新提醒】#AI挑战营第一站#手把手教你训练一个基于pytorch的手写数字识别模型 - 编程基础 - 电子工程世界-论坛 (eeworld.com.cn)
预期应用:通过幸狐 RV1106 Linux 开发板上部署一个带有运算符识别的手写数字识别的模型去进行数学公式的识别和计算,用于辅助小学生进行简单的加减乘除计算,并配备语言模块进行结果播报。
-
回复了主题帖:
【AI挑战营第一站】模型训练:在PC上完成手写数字模型训练,免费申请RV1106开发板
1、跟帖回复:用自己的语言描述,模型训练的本质是什么,训练最终结果是什么
要了解模型训练的本质,我们首先需要知道模型是什么,它是做什么的?对于目前的人工智能领域来说,据我了解,目前的模型都是大数据模型,即通过投喂大量数据来让模型进行训练,然后用来解决特定问题,就像我们训练的警犬或者宠物狗一样,他们往往不能进行推理,即我们说的举一反三等等,这也是我们当前AI发展所面临的困境。
因此模型训练其实就是利用大量数据去训练一个权重网络,而网络本质就是一系列函数组合而成,而世界大部分的规律和事实皆可用函数来进行描述,只不过我们可能无法知道他们的具体表达式而已。模型的训练的本质就是一个函数拟合的过程,就像我们给定一个x网络就会给出一个y一样,在手写模型网络中我们输入一个图片,网络会给出一个预测结果。因此训练的最终结果就是拟合效果最好的网络其中各个函数的权重分布文件,下次我们需要使用时就从这个文件中读取对应神经元的权重,然后对输入进行计算,得到一个结果,我们所说的参数就是这些权重,越大的模型往往参数量越大。
2、跟帖回复:PyTorch是什么?目前都支持哪些系统和计算平台?
pytorch其实就是将一堆模型网络所需要的基础函数整合起来成为的一个库,可以用它来快速搭建一个属于自己的网络,而不需要对于每个基础的函数都自己重新敲一遍,内置的函数和类让我们编写代码时都很方便。目前基本支持主流的所有电脑操作系统如Linux、Windows、macos等。
3、动手实践:基于PyTorch,在PC上完成MNIST手写数字识别模型训练,发帖链接为:【新提醒】#AI挑战营第一站#手把手教你训练一个基于pytorch的手写数字识别模型 - 编程基础 - 电子工程世界-论坛 (eeworld.com.cn)
-
发表了主题帖:
#AI挑战营第一站#手把手教你训练一个基于pytorch的手写数字识别模型
手把手教你训练一个基于pytorch的手写数字识别模型
首先我们可以使用网上流传最广的MNIST数据集,这里有两种下载方式,两种下载方式效果一样,可以自由选择,如果程序内下载太慢就可以直接拷贝别人下载好的或者自己去官网下载一个数据包:
一、下载数据集:
1、官网下载
找到图中框选的四个下载链接,如果打开失败就多打开几次,可能是访问的人太多了导致服务器卡顿。
其中前两个文件时训练的图像和标签,后两个文件时测试集的图像和标签,都下载等会训练模型要用的。
2、程序内部自动下载(推荐)
程序内部下载就比较省心,只需要程序内的添加一个获取数据集的函数即可:
这个函数需要依赖import torchvision.datasets包,因此别忘了导入。
import torchvision.datasets
def get_data_loader(dat_path, bat_size, trans, to_train=False):
"""
获取MNIST数据集的数据加载器。
参数:
- dat_path: 数据集的路径。
- bat_size: 批量大小。
- trans: 数据转换器,用于数据预处理。
- to_train: 是否为训练模式,默认为False,即为测试模式。
返回值:
- dat_set: MNIST数据集。
- dat_loader: 数据加载器,用于遍历数据集。
"""
# 加载MNIST数据集,根据to_train参数决定是训练集还是测试集
dat_set = torchvision.datasets.MNIST(root=dat_path, train=to_train, transform=trans, download=True)
# 根据是否为训练模式,配置不同的数据加载器参数
if to_train is True:
dat_loader = torch.utils.data.DataLoader(dat_set, batch_size=bat_size, shuffle=True)
else:
dat_loader = torch.utils.data.DataLoader(dat_set, batch_size=bat_size)
return dat_set, dat_loader
如果你只想训练一个模型不想了解代码的细节的话可以跳过这一部分,直接复制上面代码即可。
也有人可能会问,这里没有下载链接啊,哪里看出来有下载的功能了,这就是我们起那么导入那个包的目的了,注意到我们有这么一行代码,我们设置download=true后他就会默认下载。
# 加载MNIST数据集,根据to_train参数决定是训练集还是测试集
dat_set = torchvision.datasets.MNIST(root=dat_path, train=to_train, transform=trans, download=True)
在pycharm中,我们鼠标点击MNIST,然后再按下Ctrl+B,即可跳转到它的初始化代码:
我们可以看到他有一个自带的下载函数:
再按Ctrl+B跳转到他的下载函数内,可以看到,它提示如果没有就会下载数据集:
我们看他下面下载文件的地方,点击self.mirrors然后跳转:
我们可以看到,resources里面的四个文件名和我们前面官网看到的一模一样,只是前面的镜像源不一样,所以不用担心下载的数据集不同,我个人喜欢直接程序里面下载,这样他下载好了后就会直接运行,也不需要我再去调整路径什么的,比较方便。
二、设置模型参数
下载好数据集后我们就应该设置模型参数了,学过神经网络的都知道,一般的神经网络看起来比较简单,但是如果自己写可能代码比较多,不过我们使用pytorch即可节省大量代码并且结构清晰也容易理解。
下面就是我们的普通的一个CNN,包括三个中间层一个全连接层,其中前两个中间层我们都是采用卷积加池化的配合中间加一个激活函数,这是默认搭配,经典的CNN网络都是卷积后面加池化层,卷积的功能可以理解为提取出目标中的特征,池化层的功能可以理解为将这些特征组合起来。我们的输入图像是28*28,卷积核采用3*3即可,也可以改成5*5或者7*7,想试试都可以试试,但是太大可能会导致模型效果不好,而且如果卷积核太大会导致卷积出来的结果图片像素过低无法进行下一次卷积操作。再定义一个前向传播的函数将这些层连接起来即可:
class CNN(nn.Module):
"""
CNN类:定义了一个基于卷积神经网络的模型。
"""
def __init__(self):
"""
初始化CNN模型,包括三个卷积层和一个全连接层。
"""
super(CNN, self).__init__()
# 第一个中间层:1个输入通道,16个输出通道,卷积核大小为3x3
self.conv1 = nn.Sequential(
nn.Conv2d(in_channels=1, out_channels=16, kernel_size=3, stride=1, padding=1),
nn.ReLU(), # 使用ReLU激活函数
nn.MaxPool2d(kernel_size=2, stride=2), # 使用2x2的最大池化层
)
# 第二个中间层:16个输入通道,32个输出通道,卷积核大小为3x3
self.conv2 = nn.Sequential(
nn.Conv2d(in_channels=16, out_channels=32, kernel_size=3, stride=1, padding=1),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2, stride=2),
)
# 第三个中间层:32个输入通道,64个输出通道,卷积核大小为3x3
self.conv3 = nn.Sequential(
nn.Conv2d(in_channels=32, out_channels=64, kernel_size=3, stride=1, padding=1),
nn.ReLU(),
)
# 全连接层:首先将卷积层输出展平,然后通过两个线性层降低维度,最后输出10个类别的概率
self.fullyConnected = nn.Sequential(
nn.Flatten(),
nn.Linear(in_features=7 * 7 * 64, out_features=128),
nn.ReLU(),
nn.Linear(in_features=128, out_features=10),
)
def forward(self, img):
"""
定义CNN的前向传播路径。
参数:
img -- 输入图像张量
返回值:
output -- 经过CNN模型处理后的输出张量
"""
output = self.conv1(img) # 应用第一个卷积层
output = self.conv2(output) # 应用第二个卷积层
output = self.conv3(output) # 应用第三个卷积层
output = self.fullyConnected(output) # 应用全连接层
return output
三、获取当前设备
了解过机器学习的都知道cuda吧,这是Nvidia公司为他们的显卡研究出来的专门用来加速类似于AI训练的东西,有他在我们的训练时间会大大降低,但是也不乏有部分人的电脑是没有Nvidia显卡甚至没有显卡(跟我一样可怜……),因此为了兼顾有显卡和没有显卡的用户,代码里面一般都会考虑到这个问题,所以我们会检测是否有cuda,有就用,没有就用cpu:
def get_device():
if torch.cuda.is_available():
train_device = torch.device('cuda')
else:
train_device = torch.device('cpu')
return train_device
四、编写训练函数
万事俱备就应该开始炼丹了家人们,定义一个训练的函数是为了方便我们后面的调用,将我们要用到的东西放进去即可,然后按照标准的网络训练过程编写代码即可,这些照抄就行了,中间输出一些信息来让我们看到训练的过程,防止程序死机了我们还发现不了。
def train(network, dat_loader, device, epos, loss_function, optimizer):
"""
对给定的网络进行训练。
参数:
- network: 待训练的网络模型。
- dat_loader: 数据加载器,用于批量加载训练数据。
- device: 指定训练过程使用的设备(如CPU或GPU)。
- epos: 训练的总轮数。
- loss_function: 指定的损失函数。
- optimizer: 优化器,用于更新网络参数。
返回值:
- 训练后的网络模型。
"""
for epoch in range(1, epos + 1):
network.train(mode=True) # 将网络设置为训练模式
for idx, (train_img, train_label) in enumerate(dat_loader):
train_img = train_img.to(device) # 将训练图像数据移动到指定设备
train_label = train_label.to(device) # 将训练标签移动到指定设备
outputs = network(train_img) # 通过网络计算输出
optimizer.zero_grad() # 清除之前的梯度
loss = loss_function(outputs, train_label) # 计算损失
loss.backward() # 反向传播计算梯度
optimizer.step() # 使用优化器更新网络参数
# 每隔100个批次打印一次训练状态
if idx % 100 == 0:
cnt = idx * len(train_img) + (epoch - 1) * len(dat_loader.dataset)
print('epoch: {}, [{}/{}({:.0f}%)], loss: {:.6f}'.format(epoch,
idx * len(train_img),
len(dat_loader.dataset),
(100 * cnt) / (
len(dat_loader.dataset) * epos),
loss.item()))
print('------------------------------------------------')
print('Training ended.')
return network
五、编写测试函数
跟训练函数类似,只不过这个是用来评估咱们刚练出来的模型效果如何的,也是套模板即可:
def test(network, dat_loader, device, loss_function):
"""
测试给定的网络模型。
参数:
- network: 训练好的神经网络模型。
- dat_loader: 测试数据集的数据加载器。
- device: 指定运行设备,如"cpu"或"cuda:0"。
- loss_function: 用于计算损失的函数。
返回值:
- 无返回值,但会打印测试集的损失、正确率等信息。
"""
test_loss_avg, correct, total = 0, 0, 0
test_loss = []
network.train(mode=False) # 将网络设置为评估模式
with torch.no_grad(): # 禁止计算梯度,以减少内存消耗
for idx, (test_img, test_label) in enumerate(dat_loader):
test_img = test_img.to(device) # 将测试图像数据移动到指定设备
test_label = test_label.to(device) # 将测试标签移动到指定设备
total += test_label.size(0) # 统计测试样本总数
outputs = network(test_img) # 通过网络获得输出结果
loss = loss_function(outputs, test_label) # 计算损失
test_loss.append(loss.item()) # 记录当前样本的损失
predictions = torch.argmax(outputs, dim=1) # 获取预测标签
correct += torch.sum(predictions == test_label) # 统计正确预测的数量
test_loss_avg = np.average(test_loss) # 计算测试损失的平均值
# 打印测试结果: 总数、正确数、准确率和平均损失
print('Total: {}, Correct: {}, Accuracy: {:.2f}%, AverageLoss: {:.6f}'.format(total, correct,
六、编写显示代码
我们第一步下载了数据集,应该都发现了数据集怎么是ubyte结尾,也没法查看啊,那是因为他们采用的 .idx1-ubyte和.idx3-ubyte 格式的文件。这是一种IDX数据格式,我们人类一般没法看懂这个文件,但是可以通过程序来显示,从网上找一个别人写好的显示这个格式的代码然后改改就能用:
def show_part_of_image(dat_loader, row, col):
"""
展示数据加载器中部分图像的网格视图。
参数:
- dat_loader: 数据加载器,应包含图像和对应的标签。
- row: 每行展示的图像数量。
- col: 每列展示的图像数量。
返回值:
- 无
"""
# 从数据加载器中获取第一个样本
iteration = enumerate(dat_loader)
idx, (exam_img, exam_label) = next(iteration)
# 创建一个图形窗口
fig = plt.figure(num=1)
# 在图形窗口中布局子图
for i in range(row * col):
plt.subplot(row, col, i + 1)
plt.tight_layout()
# 显示图像
plt.imshow(exam_img[i][0], cmap='gray', interpolation='none')
plt.title('Number: {}'.format(exam_label[i])) # 设置图像标题为对应的标签
plt.xticks([]) # 移除x轴刻度
plt.yticks([]) # 移除y轴刻度
plt.show() # 显示图像
既然都写了一个显示数据集的函数了,那我们结果当然也要显示出来看一下了:
def show_part_of_test_result(network, dat_loader, row, col):
"""
展示测试结果的一部分。
参数:
- network: 训练好的神经网络模型。
- dat_loader: 测试数据集的加载器。
- row: 展示图片的行数。
- col: 展示图片的列数。
返回值:
- 无。
"""
# 初始化测试数据的迭代器
iteration = enumerate(dat_loader)
# 获取第一批测试数据
idx, (exam_img, exam_label) = next(iteration)
# 在不计算梯度的情况下通过模型预测输出
with torch.no_grad():
outputs = network(exam_img)
# 创建一个图形用于展示图片
fig = plt.figure()
# 遍历要展示的图片并展示在图形上
for i in range(row * col):
plt.subplot(row, col, i + 1) # 创建子图
plt.tight_layout() # 调整子图间的布局以避免重叠
plt.imshow(exam_img[i][0], cmap='gray', interpolation='none') # 展示图片
plt.title('Number: {}, Prediction: {}'.format(
exam_label[i], outputs.data.max(1, keepdim=True)[1][i].item() # 标题包括真实标签和预测标签
))
plt.xticks([]) # 移除x轴标签
plt.yticks([]) # 移除y轴标签
plt.show() # 展示图形
七、所有代码展示
上面已经把代码都介绍一遍了,我相信大家应该也对模型的训练有个大致的思路了,接下来就是将所有代码整合到一起然后编写一个主函数去进行训练测试了,这里就不多说了,大家直接看代码然后就可以开始炼丹了:
import torch
import torch.nn as nn
import torchvision.datasets
import torchvision.transforms as transforms
import matplotlib.pyplot as plt
import numpy as np
from PIL import Image
class CNN(nn.Module):
"""
CNN类:定义了一个基于卷积神经网络的模型。
"""
def __init__(self):
"""
初始化CNN模型,包括三个卷积层和一个全连接层。
"""
super(CNN, self).__init__()
# 第一个中间层:1个输入通道,16个输出通道,卷积核大小为3x3
self.conv1 = nn.Sequential(
nn.Conv2d(in_channels=1, out_channels=16, kernel_size=3, stride=1, padding=1),
nn.ReLU(), # 使用ReLU激活函数
nn.MaxPool2d(kernel_size=2, stride=2), # 使用2x2的最大池化层
)
# 第二个中间层:16个输入通道,32个输出通道,卷积核大小为3x3
self.conv2 = nn.Sequential(
nn.Conv2d(in_channels=16, out_channels=32, kernel_size=3, stride=1, padding=1),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2, stride=2),
)
# 第三个中间层:32个输入通道,64个输出通道,卷积核大小为3x3
self.conv3 = nn.Sequential(
nn.Conv2d(in_channels=32, out_channels=64, kernel_size=3, stride=1, padding=1),
nn.ReLU(),
)
# 全连接层:首先将卷积层输出展平,然后通过两个线性层降低维度,最后输出10个类别的概率
self.fullyConnected = nn.Sequential(
nn.Flatten(),
nn.Linear(in_features=7 * 7 * 64, out_features=128),
nn.ReLU(),
nn.Linear(in_features=128, out_features=10),
)
def forward(self, img):
"""
定义CNN的前向传播路径。
参数:
img -- 输入图像张量
返回值:
output -- 经过CNN模型处理后的输出张量
"""
output = self.conv1(img) # 应用第一个卷积层
output = self.conv2(output) # 应用第二个卷积层
output = self.conv3(output) # 应用第三个卷积层
output = self.fullyConnected(output) # 应用全连接层
return output
def get_device():
if torch.cuda.is_available():
train_device = torch.device('cuda')
else:
train_device = torch.device('cpu')
return train_device
def get_data_loader(dat_path, bat_size, trans, to_train=False):
"""
获取MNIST数据集的数据加载器。
参数:
- dat_path: 数据集的路径。
- bat_size: 批量大小。
- trans: 数据转换器,用于数据预处理。
- to_train: 是否为训练模式,默认为False,即为测试模式。
返回值:
- dat_set: MNIST数据集。
- dat_loader: 数据加载器,用于遍历数据集。
"""
# 加载MNIST数据集,根据to_train参数决定是训练集还是测试集
dat_set = torchvision.datasets.MNIST(root=dat_path, train=to_train, transform=trans, download=True)
# 根据是否为训练模式,配置不同的数据加载器参数
if to_train is True:
dat_loader = torch.utils.data.DataLoader(dat_set, batch_size=bat_size, shuffle=True)
else:
dat_loader = torch.utils.data.DataLoader(dat_set, batch_size=bat_size)
return dat_set, dat_loader
def show_part_of_image(dat_loader, row, col):
"""
展示数据加载器中部分图像的网格视图。
参数:
- dat_loader: 数据加载器,应包含图像和对应的标签。
- row: 每行展示的图像数量。
- col: 每列展示的图像数量。
返回值:
- 无
"""
# 从数据加载器中获取第一个样本
iteration = enumerate(dat_loader)
idx, (exam_img, exam_label) = next(iteration)
# 创建一个图形窗口
fig = plt.figure(num=1)
# 在图形窗口中布局子图
for i in range(row * col):
plt.subplot(row, col, i + 1)
plt.tight_layout()
# 显示图像
plt.imshow(exam_img[i][0], cmap='gray', interpolation='none')
plt.title('Number: {}'.format(exam_label[i])) # 设置图像标题为对应的标签
plt.xticks([]) # 移除x轴刻度
plt.yticks([]) # 移除y轴刻度
plt.show() # 显示图像
def train(network, dat_loader, device, epos, loss_function, optimizer):
"""
对给定的网络进行训练。
参数:
- network: 待训练的网络模型。
- dat_loader: 数据加载器,用于批量加载训练数据。
- device: 指定训练过程使用的设备(如CPU或GPU)。
- epos: 训练的总轮数。
- loss_function: 指定的损失函数。
- optimizer: 优化器,用于更新网络参数。
返回值:
- 训练后的网络模型。
"""
for epoch in range(1, epos + 1):
network.train(mode=True) # 将网络设置为训练模式
for idx, (train_img, train_label) in enumerate(dat_loader):
train_img = train_img.to(device) # 将训练图像数据移动到指定设备
train_label = train_label.to(device) # 将训练标签移动到指定设备
outputs = network(train_img) # 通过网络计算输出
optimizer.zero_grad() # 清除之前的梯度
loss = loss_function(outputs, train_label) # 计算损失
loss.backward() # 反向传播计算梯度
optimizer.step() # 使用优化器更新网络参数
# 每隔100个批次打印一次训练状态
if idx % 100 == 0:
cnt = idx * len(train_img) + (epoch - 1) * len(dat_loader.dataset)
print('epoch: {}, [{}/{}({:.0f}%)], loss: {:.6f}'.format(epoch,
idx * len(train_img),
len(dat_loader.dataset),
(100 * cnt) / (
len(dat_loader.dataset) * epos),
loss.item()))
print('------------------------------------------------')
print('Training ended.')
return network
def test(network, dat_loader, device, loss_function):
"""
测试给定的网络模型。
参数:
- network: 训练好的神经网络模型。
- dat_loader: 测试数据集的数据加载器。
- device: 指定运行设备,如"cpu"或"cuda:0"。
- loss_function: 用于计算损失的函数。
返回值:
- 无返回值,但会打印测试集的损失、正确率等信息。
"""
test_loss_avg, correct, total = 0, 0, 0
test_loss = []
network.train(mode=False) # 将网络设置为评估模式
with torch.no_grad(): # 禁止计算梯度,以减少内存消耗
for idx, (test_img, test_label) in enumerate(dat_loader):
test_img = test_img.to(device) # 将测试图像数据移动到指定设备
test_label = test_label.to(device) # 将测试标签移动到指定设备
total += test_label.size(0) # 统计测试样本总数
outputs = network(test_img) # 通过网络获得输出结果
loss = loss_function(outputs, test_label) # 计算损失
test_loss.append(loss.item()) # 记录当前样本的损失
predictions = torch.argmax(outputs, dim=1) # 获取预测标签
correct += torch.sum(predictions == test_label) # 统计正确预测的数量
test_loss_avg = np.average(test_loss) # 计算测试损失的平均值
# 打印测试结果: 总数、正确数、准确率和平均损失
print('Total: {}, Correct: {}, Accuracy: {:.2f}%, AverageLoss: {:.6f}'.format(total, correct,
correct / total * 100,
test_loss_avg))
def show_part_of_test_result(network, dat_loader, row, col):
"""
展示测试结果的一部分。
参数:
- network: 训练好的神经网络模型。
- dat_loader: 测试数据集的加载器。
- row: 展示图片的行数。
- col: 展示图片的列数。
返回值:
- 无。
"""
# 初始化测试数据的迭代器
iteration = enumerate(dat_loader)
# 获取第一批测试数据
idx, (exam_img, exam_label) = next(iteration)
# 在不计算梯度的情况下通过模型预测输出
with torch.no_grad():
outputs = network(exam_img)
# 创建一个图形用于展示图片
fig = plt.figure()
# 遍历要展示的图片并展示在图形上
for i in range(row * col):
plt.subplot(row, col, i + 1) # 创建子图
plt.tight_layout() # 调整子图间的布局以避免重叠
plt.imshow(exam_img[i][0], cmap='gray', interpolation='none') # 展示图片
plt.title('Number: {}, Prediction: {}'.format(
exam_label[i], outputs.data.max(1, keepdim=True)[1][i].item() # 标题包括真实标签和预测标签
))
plt.xticks([]) # 移除x轴标签
plt.yticks([]) # 移除y轴标签
plt.show() # 展示图形
if __name__ == "__main__":
# 设置批次大小和训练轮数
batch_size, epochs = 64, 12
# 定义数据转换器,包括转换为张量和标准化
transform = transforms.Compose([transforms.ToTensor(), transforms.Normalize(mean=[0.1307], std=[0.3081])])
# 获取训练使用的设备(如GPU或CPU)
my_device = get_device()
# 数据集路径
path = './data'
# 加载训练数据集和数据加载器
_, train_data_loader = get_data_loader(path, batch_size, transform, True)
print('Training data loaded.')
# 展示训练数据集中的一部分图像
show_part_of_image(train_data_loader, 3, 3)
# 加载测试数据集和数据加载器
_, test_data_loader = get_data_loader(path, batch_size, transform)
print('Testing data loaded.')
# 定义卷积神经网络
cnn = CNN()
# 定义损失函数和优化器
loss_func = nn.CrossEntropyLoss()
optim = torch.optim.Adam(cnn.parameters(), lr=0.01)
# 训练CNN模型
cnn = train(cnn, train_data_loader, my_device, epochs, loss_func, optim)
# 在测试数据集上测试训练好的模型
test(cnn, test_data_loader, my_device, loss_func)
# 展示测试结果的一部分
show_part_of_test_result(cnn, test_data_loader, 5, 2)
# 保存训练好的CNN模型
torch.save(cnn, './cnn2.pth')
# 转化为onnx模型保存
torch.onnx.export(cnn, torch.randn(1, 1, 28, 28), "./cnn2.onnx")
八、开始炼丹
我们直接运行程序就可以开始炼丹了,注意最后两步我们将模型保存为了*.pth以及转换的ONNX模型。
训练开始时我们可以看到右边出现了我们数据集的照片,还有他们的标签,可以说图片还是有些抽象的,因为这是外国人的数据集,所以我们看起来还是有点不适应的,因此我们检测的时候也需要尽量写的跟他们差不多才能达到较高的准确率。
我们可以修改batch_size, epochs来修改网络的训练时间,选个适当的值就行了,batch_size太大会爆内存epochs太大会训练很久而且可能过拟合,所以差不多十几次就够了:
等待训练结束就可以看到我们的模型准确率了,96.83emm,应该是有点过拟合了,之前训练10次的时候还有97的准确率,所以参数的设置也是训练的一个重点哦:
最终我们得到的模型就是下面这两个东西啦:
可以看到这两个文件的大小只有1.6MB,属于是非常小的模型了,这是因为我们网络创建的时候比较小,所以需要的权重参数也比较少,更适合部署在嵌入式这种存储空间少且性能较弱的移动设备上。
九、测试模型
模型都出来了,不用用咋知道好坏呢,随便写个模型加载和测试的代码:
import torch
import numpy as np
from PIL import Image
from torchvision import transforms
import torch.nn as nn
import matplotlib.pyplot as plt
import cv2
class CNN(nn.Module):
def __init__(self):
super(CNN, self).__init__()
self.conv1 = nn.Sequential(
nn.Conv2d(in_channels=1, out_channels=16, kernel_size=3, stride=1, padding=1),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2, stride=2),
)
self.conv2 = nn.Sequential(
nn.Conv2d(in_channels=16, out_channels=32, kernel_size=3, stride=1, padding=1),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2, stride=2),
)
self.conv3 = nn.Sequential(
nn.Conv2d(in_channels=32, out_channels=64, kernel_size=3, stride=1, padding=1),
nn.ReLU(),
)
self.fullyConnected = nn.Sequential(
nn.Flatten(),
nn.Linear(in_features=7 * 7 * 64, out_features=128),
nn.ReLU(),
nn.Linear(in_features=128, out_features=10),
)
def forward(self, input):
output = self.conv1(input)
output = self.conv2(output)
output = self.conv3(output)
output = self.fullyConnected(output)
return output
def preprocess_and_highlight_text(img_pil):
# 将PIL Image转换为OpenCV格式(numpy.ndarray)
img_cv = np.array(img_pil.convert('RGB'))
# 预处理(可选)
img_cv = cv2.fastNlMeansDenoising(img_cv, None, 10, 7, 21)
img_cv = cv2.medianBlur(img_cv, 3)
# 二值化
_, img_bin = cv2.threshold(cv2.cvtColor(img_cv, cv2.COLOR_BGR2GRAY), 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
# 反转颜色
img_highlighted = cv2.bitwise_not(img_bin)
# 将处理后的OpenCV图像转换回PIL Image格式
img_highlighted_pil = Image.fromarray(img_highlighted, mode='L')
return img_highlighted_pil
model = torch.load('./cnn2.pth')
model.eval()
transform = transforms.Compose([transforms.ToTensor(), transforms.Normalize(mean=[0.1307], std=[0.3081])])
unloader = transforms.ToPILImage()
for k in range(10):
infile = './data/raw/' + '{}.jpg'.format(k)
img = Image.open(infile)
# 突出文字效果
img_highlighted = preprocess_and_highlight_text(img)
# 将图片设置为28*28大小
img = img.resize((28, 28))
img = img.convert('L')
img_array = np.array(img)
# 像素反转
for i in range(28):
for j in range(28):
img_array[i, j] = 255 - img_array[i, j]
# print(img_array)
img = Image.fromarray(img_array)
# img.show()
img = transform(img)
img = torch.unsqueeze(img, 0)
output = model(img)
pred = torch.argmax(output, dim=1)
image = torch.squeeze(img, 0)
image = unloader(image)
plt.subplot(5, 2, k + 1)
plt.tight_layout()
plt.imshow(image, cmap='gray', interpolation='none')
plt.title("Number: {}, Prediction: {}".format(k, pred.item()))
plt.xticks([])
plt.yticks([])
plt.show()
preprocess_and_highlight_text这个函数是用来对图像进行预处理的,不然我们的图像一般都噪声比较多还有其他因素干扰,我的纸偏黄,所以给他进行二值化编程黑白的后再进行反转就跟数据集差不多的,就可以交给模型去识别了,不然你就会得到一堆你都看不懂的数字图片跟别说让模型去看了,测试结果:
效果也还不错了,只有9错了,也还可以。
希望大家都能练出属于自己的丹,给自己折腾小玩意用。
最后偷偷的放一下没有预处理的图片是什么后果:
这告诉我们什么,对于测试数也要进行预处理,还要进行正确的预处理,不然就那阴影一块一块的图片别说模型认不出了,我都不一定认得出来。我一开始还在想模型怎么准确率这么低,看了一眼才发现输入的都是什么妖魔鬼怪QAQ……