前言
硬件环境:
- 笔记本电脑 win11 23h2
- cpu: amd r7 6800H
- gpu: rtx3060 6g laptop
- 32g内存
软件环境:
yolov5 6.0的源码请到官方github下载整个Source code压缩包 点我跳转。
python环境搭建
python环境这里我使用conda进行虚拟环境的搭建,conda的具体使用教程请参考文章。
我们打开powershell或者Anaconda Prompt,输入代码创建python3.8的虚拟环境
1
2
|
# 创建py3.8虚拟环境
conda create -n yolov5_60 python==3.8
|
结束之后,我们切换到上述环境,并验证python环境和python库情况
1
2
3
4
5
6
7
8
|
# 切换环境
conda activate yolov5_60
# 进入python,查看是否为py3.8
python
# 退出python
exit()
# 查看当前第三方库,是否干净,有无串库情况(如果以前没下过py3.8的第三方库,不会出现这种情况)
pip list
|
安装完新的py环境后,我们进入到yolov5的文件夹,安装指定的py库
1
2
|
# 在yolov5文件夹中运行
pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
|
注意这里的pytorch默认安装的是cpu版本,如果需要gpu的请自行下载。
同时pytorch版本可能会过高导致一些bug,具体bug请看下面注意事项
这里还有个问题 默认下载的numpy 版本太高,有些东西被弃用,在后面运行代码的时候会出错,所以我们安装旧版本的
1
2
|
pip uninstall numpy
pip install numpy==1.22
|
当全部安装完成,我们的py环境配置到此结束
运行案例
我们可以运行detect.py来检验一下运行效果
1
2
|
# 在yolov5文件夹中运行
python detect.py --source data/images/bus.jpg --weights yolov5s.pt --img 640
|
在刚开始的情况下我们会下载yolov5s,但如果你的网速不佳的话,可以手动下载,去前面下载代码的地方下载模型。
当全部运行结束,它会提示我们result保存在了run/detect下,我们打开里面的图片,就是推测结果。
模型训练
这里我采用的是摔倒检测的训练集,点我跳转下载地址。
下载完数据集后,我们将数据集放在yolov5文件夹下的dataset当中,具体位置如下所示:

防止好训练集后,我们要对训练集进行清洗处理和划分处理
清洗数据集
大致浏览数据集,有img、txt、xml三个文件夹,我们主要用到img和txt两个文件夹。
下载地址上并没有告知我们有多少类别,我们可以写个脚本,自行检索获得,新建一个scripts/nc_nums.py:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
|
import os
from tqdm import tqdm
# 标注文件所在的目录
label_dir = 'xxx'
# 用于存储所有出现的类别索引
class_indices = set()
# 用于存储每个类别出现的次数
class_count = {}
# 获取所有以 .txt 结尾的文件
txt_files = [filename for filename in os.listdir(label_dir) if filename.endswith('.txt')]
# 使用 tqdm 创建进度条
for filename in tqdm(txt_files, desc="Processing annotation files", unit="file"):
file_path = os.path.join(label_dir, filename)
try:
with open(file_path, 'r') as f:
lines = f.readlines()
for line in lines:
# 分割每行数据,获取第一个数字(类别索引)
parts = line.strip().split()
if parts:
class_index = int(parts[0])
class_indices.add(class_index)
# 统计每个类别出现的次数
if class_index in class_count:
class_count[class_index] += 1
else:
class_count[class_index] = 1
except Exception as e:
print(f"读取文件 {file_path} 时出错: {e}")
# 类别数量为最大索引加 1
num_classes = max(class_indices) + 1 if class_indices else 0
# 输出类别数量
print(f"类别数量: {num_classes}")
# 输出每个类别出现的次数
print("每个类别出现的次数:")
for class_index, count in sorted(class_count.items()):
print(f"类别 {class_index}: {count} 次")
|
填入自己的数据集位置,然后进行检索,我们可以得知该数据集总共有2个类别,但是这还是有问题的,实际上只有一个类别。
因为txt中只有标签1,并没有标签0,所以识别成2个类别,我们正常训练的数据集都是从类别0开始的,所以这个数据有点问题,但我们也可以写一个脚本,将里面的所有类别1转化成类别0,新建一个scripts/change1to1.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
import os
# 标注文件所在的目录
label_dir = 'xxxx'
# 遍历标注文件目录
for filename in os.listdir(label_dir):
if filename.endswith('.txt'):
file_path = os.path.join(label_dir, filename)
new_lines = []
try:
# 读取文件内容
with open(file_path, 'r') as f:
lines = f.readlines()
for line in lines:
parts = line.strip().split()
if parts:
class_index = int(parts[0])
if class_index == 1:
# 将类别标签 1 修改为 0
parts[0] = '0'
new_lines.append(' '.join(parts) + '\n')
# 写入修改后的内容
with open(file_path, 'w') as f:
f.writelines(new_lines)
print(f"文件 {file_path} 已修改。")
except Exception as e:
print(f"处理文件 {file_path} 时出错: {e}")
|
填入数据集位置,运行脚本,将所有的类别1转为类别0,运行结束,我们再重新运行前面的检测脚本,就会发现只有一个类别了。
划分训练集
我们将这些训练集按照8:1:1来划分为训练集、验证集和测试集,我们新建脚本scripts/split.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
|
import os
import random
import shutil
from tqdm import tqdm
import logging
# 配置日志记录
logging.basicConfig(filename='data_split.log', level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s')
# 数据集路径
data_dir = 'xxx\\yolov5-6.0\\dataset\\FallDataset'
img_dir = os.path.join(data_dir, 'img')
txt_dir = os.path.join(data_dir, 'txt')
# 输出路径
output_dir = 'xxx\\yolov5-6.0\\dataset\\FallDataset_split'
train_img_dir = os.path.join(output_dir, 'images', 'train')
val_img_dir = os.path.join(output_dir, 'images', 'val')
test_img_dir = os.path.join(output_dir, 'images', 'test')
train_label_dir = os.path.join(output_dir, 'labels', 'train')
val_label_dir = os.path.join(output_dir, 'labels', 'val')
test_label_dir = os.path.join(output_dir, 'labels', 'test')
# 创建输出文件夹
os.makedirs(train_img_dir, exist_ok=True)
os.makedirs(val_img_dir, exist_ok=True)
os.makedirs(test_img_dir, exist_ok=True)
os.makedirs(train_label_dir, exist_ok=True)
os.makedirs(val_label_dir, exist_ok=True)
os.makedirs(test_label_dir, exist_ok=True)
# 获取所有图像文件
img_files = os.listdir(img_dir)
random.shuffle(img_files)
# 划分数据集
train_size = int(len(img_files) * 0.8)
val_size = int(len(img_files) * 0.1)
test_size = len(img_files) - train_size - val_size
train_files = img_files[:train_size]
val_files = img_files[train_size:train_size + val_size]
test_files = img_files[train_size + val_size:]
# 复制图像和标注文件到相应的文件夹
def copy_files(files, img_dest, label_dest):
for file in tqdm(files, desc=f"Copying files to {img_dest}", unit="file"):
img_src = os.path.join(img_dir, file)
label_name = os.path.splitext(file)[0] + '.txt'
label_src = os.path.join(txt_dir, label_name)
try:
# 检查标注文件是否存在
if os.path.exists(label_src):
shutil.copy(img_src, img_dest)
shutil.copy(label_src, label_dest)
else:
logging.warning(f"No corresponding label file found for {img_src}")
print(f"Warning: No corresponding label file found for {img_src}")
except Exception as e:
logging.error(f"Error copying {img_src} or {label_src}: {e}")
print(f"Error copying {img_src} or {label_src}: {e}")
copy_files(train_files, train_img_dir, train_label_dir)
copy_files(val_files, val_img_dir, val_label_dir)
copy_files(test_files, test_img_dir, test_label_dir)
|
填入数据集位置和输出位置,然后运行脚本,我们就得到了划分好的数据集。
开始训练
搞定数据集后,我们开始训练,首先需要编写一个data/falling.yaml来确定训练集位置和类别标签
1
2
3
4
5
6
7
|
train: xxx/yolov5-6.0/dataset/Falldataset_split/images/train
val: xxx/yolov5-6.0/dataset/Falldataset_split/images/val
test: xxx/yolov5-6.0/dataset/Falldataset_split/images/test
nc: 1 # 类别数量,只有一个类别(人体摔倒)
names: ['falling']
|
编写好后,我们来开始训练,在命令行输入如下命令:
1
|
python train.py --img 640 --batch 8 --epochs 50 --data data/falling.yaml --cfg models/yolov5s.yaml --weights weights/yolov5s.pt
|
我这里的yolov5s.pt是手动下载,然后放到weights下的,默认下载是在yolov5的根目录下。
参数具体解释:
- –img 640
- 含义:指定训练和验证过程中输入图像的尺寸大小。在目标检测任务里,通常需要将不同尺寸的输入图像统一调整为固定大小,这样便于模型处理。这里将图像的长边或短边统一调整为 640 像素,图像的宽高比会保持不变,不足的部分会进行填充。
- 作用:统一图像尺寸可以保证模型输入的一致性,提高训练效率和稳定性。不同的图像尺寸会影响模型的计算量和检测精度,一般来说,较大的图像尺寸能提供更多的细节信息,但会增加计算量和内存占用。
- –batch 8
- 含义:指定每个训练批次(batch)中包含的图像数量。一个批次的图像会一起输入到模型中进行前向传播和反向传播,更新模型的参数。
- 作用:批量大小会影响训练的速度和模型的泛化能力。较大的批量大小可以更充分地利用 GPU 的并行计算能力,加快训练速度,但可能会导致内存不足;较小的批量大小则可以增加模型参数更新的频率,有助于模型更好地收敛,但训练速度会相对较慢。
- –epochs 50
- 含义:指定训练的轮数(epoch)。一个轮次表示将整个训练数据集完整地过一遍,模型会在每个轮次中对数据集中的所有图像进行训练,并更新模型的参数。
- 作用:训练轮数会影响模型的训练效果。如果轮数太少,模型可能无法充分学习到数据集中的特征,导致欠拟合;如果轮数太多,模型可能会过度学习训练数据中的噪声,导致过拟合。
- –data data/falling.yaml
- 含义:指定数据集的配置文件路径。data/falling.yaml 是一个 YAML 格式的文件,其中包含了训练集、验证集和测试集的路径,以及数据集的类别数量和类别名称等信息。
- 作用:YOLOv5 通过这个配置文件来加载和处理数据集,确保模型能够正确地读取和使用训练数据。
- –cfg models/yolov5s.yaml
- 含义:指定模型的配置文件路径。models/yolov5s.yaml 是一个 YAML 格式的文件,其中定义了 YOLOv5 模型的网络结构,包括卷积层、池化层、激活函数等的参数和连接方式。
- 作用:YOLOv5 根据这个配置文件来构建模型的网络结构,不同的配置文件可以创建不同规模和复杂度的模型,例如 yolov5s.yaml 对应较小的 YOLOv5s 模型,适合在资源有限的设备上使用。
- –weights weights/yolov5s.pt
- 含义:指定预训练模型的权重文件路径。weights/yolov5s.pt 是一个 PyTorch 模型的权重文件,其中包含了预训练模型的参数。
- 作用:使用预训练模型的权重可以加速模型的训练过程,因为预训练模型已经在大规模数据集上学习到了一些通用的特征。在训练新的数据集时,可以将预训练模型的权重作为初始值,然后在新数据集上进行微调,这样可以更快地收敛到较好的性能。
完成之后我们来运行train.py,刚开始它会检索数据集,将训练集和验证集搬到gpu上,这个过程上有很多的warning,不用去管它,这是因为数据集中有很多不好的图片,它会自动忽略,装载完数据集后,就开始一轮轮训练了,我们只要等待训练结束即可。
模型使用
如何使用我们训练完成之后的模型呢,与前面一样,只需要将模型的位置换成刚刚训练出来的即可:
1
|
python detect.py --weights runs/train/exp/weights/best.pt --source data/images
|
注意事项
在训练的时候如果报错:
1
|
RuntimeError: result type Float can‘t be cast to the desired output type __int64
|
这是因为pytorch的版本过高,不支持float直接转换为long int 型数据类型。
两种解决方案:
- 安装旧版本的pytorch(但具体要自己试)
- 手动转换数据类型
这里介绍手动转化数据类型的方法
打开yolov5文件夹下的utils/loss.py,然后找到
1
2
3
4
|
# 大概在173行,也可借助vscode的ctrl+h进行查找
...
gain = torch.ones(7, device=targets.device) # normalized to gridspace gain
...
|
在后面加一个long(),将其替换成:
1
|
gain = torch.ones(7, device=targets.device).long() # normalized to gridspace gain
|
这样就不会报错了。
参考资料
https://blog.csdn.net/weixin_54662911/article/details/143951389