一、简介
- 项目使用torchvision.models自带模型:Mobilenet_v2;在约3400张手语手势图片上进行训练,准确率达到98%。
- 训练轮数约80轮。测试集中图片100%准确率,自己拍摄的手势标准的图片准确率达到80%以上。
- 模型轻量,2G显存可以轻松训练,推理速度很快。
- 具备完整代码,注释完备,思路讲解
二、数据集
手语识别数据集,每个字母有一种手势表达,共26个类别。每张图片主体清晰,质量很高。
训练集:26个字母每个字母约130张图片
测试集:26个字母每个字母约100张图片
数据集源地址:Synthetic ASL Alphabet (kaggle.com)
注:源数据集的训练集每个字母有900多张图片
三、模型效果
四、代码
(一)数据加载(loaddata.py)
1、思路
首先,我们需要导入一些必要的库:
import torch
import torchvision
from torchvision import transforms
这里,torch
是PyTorch的核心库,torchvision
是PyTorch的一个视觉库,它提供了很多视觉任务相关的工具,比如数据集加载器、模型和变换(transforms)。
接下来,我们定义一个函数
data_load
def data_load(data_dir, test_data_dir, img_height, img_width, batch_size):
data_dir
:训练数据集的路径。test_data_dir
:验证数据集的路径。img_height
:图像的高度。img_width
:图像的宽度。batch_size
:每个批次加载的图像数量。
定义一个随机灰度化的概率:
random_grayscale_p = 0.2
这意味着在训练过程中,有20%的概率将图像转换为灰度图,这可以帮助我们的模型更好地泛化。
接下来,我们定义训练数据加载器所需的变换:
transform_train = transforms.Compose([
transforms.Resize((img_height, img_width)), # 调整图像大小
transforms.RandomGrayscale(p=random_grayscale_p), # 随机灰度化
transforms.ToTensor(), # 转换为Tensor
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)) # 标准化
])
Resize
:将图像调整到指定的高度和宽度。RandomGrayscale
:以一定的概率将图像转换为灰度图。ToTensor
:将图像数据转换为PyTorch的Tensor格式,这是神经网络处理数据所需的格式。Normalize
:对图像进行标准化处理,这里我们用到了均值和标准差(0.5, 0.5, 0.5)来使数据分布在[-1, 1]之间,这有助于模型的训练。
对于验证数据加载器,我们不需要随机灰度化,所以变换稍微简单一些:
transform_val = transforms.Compose([
transforms.Resize((img_height, img_width)), # 调整图像大小
transforms.ToTensor(), # 转换为Tensor
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)) # 标准化
])
然后,我们使用
torchvision.datasets.ImageFolder
来加载图像数据集。这个类会自动读取指定目录下的图像,并将它们分为不同的类别(所以目录结构很重要,见 “五、注意事项(一)目录结构”)
train_dataset = torchvision.datasets.ImageFolder(root=data_dir, transform=transform_train)
val_dataset = torchvision.datasets.ImageFolder(root=test_data_dir, transform=transform_val)
接着,我们使用
torch.utils.data.DataLoader
来创建数据加载器。这个类会在训练时帮助我们有效地批量加载数据:
train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
val_loader = torch.utils.data.DataLoader(val_dataset, batch_size=batch_size, shuffle=False)
shuffle=True
表示在训练过程中每个epoch都会打乱数据顺序,而验证数据不需要打乱。
最后,我们从训练数据集中获取类别名称,并将训练数据加载器、验证数据加载器和类别名称返回
class_names = train_dataset.classes
return train_loader, val_loader, class_names
这样,我们的数据加载函数就讲解完毕了。通过这个函数,我们可以方便地加载数据,为后续的模型训练做好准备。
2、完整代码
import torch
import torchvision
from torchvision import transforms
def data_load(data_dir, test_data_dir, img_height, img_width, batch_size):
# 定义随机灰度化的概率
random_grayscale_p = 0.2
# 定义训练数据加载器
transform_train = transforms.Compose([
transforms.Resize((img_height, img_width)), # 调整图像大小
transforms.RandomGrayscale(p=random_grayscale_p), # 随机灰度化
transforms.ToTensor(), # 转换为Tensor
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)) # 标准化
])
# 定义验证数据加载器
transform_val = transforms.Compose([
transforms.Resize((img_height, img_width)), # 调整图像大小
transforms.ToTensor(), # 转换为Tensor
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)) # 标准化
])
train_dataset = torchvision.datasets.ImageFolder(root=data_dir, transform=transform_train)
val_dataset = torchvision.datasets.ImageFolder(root=test_data_dir, transform=transform_val)
train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
val_loader = torch.utils.data.DataLoader(val_dataset, batch_size=batch_size, shuffle=False)
# 获取训练数据集的类别
class_names = train_dataset.classes
# 返回训练数据加载器、验证数据加载器和类别
return train_loader, val_loader, class_names
(二)模型训练(train.py)
1、思路
我们导入了几个关键库:
import os
import torch
import torch.nn as nn
import torchvision
import time
import loaddata
os
:用于文件和目录操作,如检查文件是否存在。torch
和torch.nn
:PyTorch的核心库,用于构建和训练神经网络。torchvision
:提供了许多预训练模型和数据集处理工具。loaddata
:自定义模块,用于加载和预处理数据集。
定义模型加载函数
model_load
def model_load(class_num=26):
# 加载预训练的mobilenet_v2模型
mobilenet = torchvision.models.mobilenet_v2(weights=torchvision.models.MobileNet_V2_Weights.DEFAULT)
# 修改模型的最后一层全连接层,使其输出的类别数为class_num
mobilenet.classifier[1] = nn.Linear(in_features=mobilenet.classifier[1].in_features, out_features=class_num)
# 返回修改后的模型
return mobilenet
- 加载预训练的MobileNetV2模型。
- 修改模型的最后一层(分类器),以适应我们特定的任务,即分类
class_num
个数
定义训练函数train
def train(epochs):
# 加载数据集
train_loader, val_loader, class_names = loaddata.data_load("../dataset2/train", "../dataset2/test", 224, 224, 16)
# 打印类别名称
print("类别名称:", class_names)
# 加载模型
model = model_load(class_num=len(class_names))
# 定义损失函数
criterion = nn.CrossEntropyLoss()
# 添加权重衰减
optimizer = torch.optim.Adam(model.parameters(), lr=0.001, weight_decay=1e-5)
# 判断是否有可用的GPU
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
# 将模型移动到GPU上
model.to(device)
# 如果存在已有模型,则加载已有模型继续训练
if os.path.exists("mobilenet_fv.pth"):
model.load_state_dict(torch.load("mobilenet_fv.pth"))
print("加载已有模型继续训练")
best_val_accuracy = 0.0
train_losses = []
val_losses = []
train_accuracies = []
val_accuracies = []
# 设置早停的耐心值
patience = 5
# 初始化耐心计数器
patience_counter = 0
# 遍历所有epoch
for epoch in range(epochs):
print("Epoch {}/{}".format(epoch + 1, epochs))
running_loss = 0.0
correct = 0
total = 0
# 遍历训练集
for i, data in enumerate(train_loader, 0):
inputs, labels = data[0].to(device), data[1].to(device)
optimizer.zero_grad()
outputs = model(inputs)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
running_loss += loss.item()
_, predicted = torch.max(outputs.data, 1)
total += labels.size(0)
correct += (predicted == labels).sum().item()
train_losses.append(running_loss / len(train_loader))
train_accuracies.append(correct / total)
print("训练损失:{:.4f},准确率:{:.4f}".format(train_losses[-1], train_accuracies[-1]))
# 初始化验证损失、正确预测数量和总数量
running_loss = 0.0
correct = 0
total = 0
# 不计算梯度
with torch.no_grad():
# 遍历验证集
for i, data in enumerate(val_loader, 0):
# 获取输入和标签
inputs, labels = data[0].to(device), data[1].to(device)
# 前向传播
outputs = model(inputs)
# 计算损失
loss = criterion(outputs, labels)
# 累加损失
running_loss += loss.item()
# 获取预测结果
_, predicted = torch.max(outputs.data, 1)
# 累加总数量
total += labels.size(0)
# 累加正确预测数量
correct += (predicted == labels).sum().item()
# 计算验证损失和准确率
val_losses.append(running_loss / len(val_loader))
val_accuracies.append(correct / total)
# 打印验证损失和准确率
print("验证损失:{:.4f},准确率:{:.4f}".format(val_losses[-1], val_accuracies[-1]))
# 如果当前准确率大于最佳准确率
if val_accuracies[-1] > best_val_accuracy:
# 更新最佳准确率
best_val_accuracy = val_accuracies[-1]
# 保存模型
torch.save(model.state_dict(), "mobilenet_fv.pth")
print("模型已保存")
# 重置耐心计数器
patience_counter = 0
else:
# 累加耐心计数器
patience_counter += 1
# 如果耐心计数器超过耐心值
if patience_counter >= patience:
# 打印早停法触发信息
print("早停法触发,停止训练")
# 跳出循环
break
在训练函数中,我们做了以下事情:
- 加载训练和验证数据集。
- 设置损失函数和优化器。
- 检查是否有GPU可用,将模型移至GPU上运行。
- 如果有预训练模型(mobilenet_fv.pth)存在,加载并继续训练。
- 循环遍历每个epoch,执行训练和验证过程。
- 在每个epoch结束时,检查验证集上的准确率是否提高,如果是,则保存模型状态。
主函数调用
if __name__ == '__main__':
train(epochs=30)
最后,我们在主函数中调用train
函数,传入训练的epoch(轮数),开始整个训练过程。
2、完整代码
import os
import torch
import torch.nn as nn
import torchvision
import loaddata
# 定义一个函数,用于加载模型,参数class_num表示分类的类别数
def model_load(class_num=26):
# 加载预训练的mobilenet_v2模型
mobilenet = torchvision.models.mobilenet_v2(weights=torchvision.models.MobileNet_V2_Weights.DEFAULT)
# 修改模型的最后一层全连接层,使其输出的类别数为class_num
mobilenet.classifier[1] = nn.Linear(in_features=mobilenet.classifier[1].in_features, out_features=class_num)
# 返回修改后的模型
return mobilenet
# 训练过程
def train(epochs):
# 加载数据集
train_loader, val_loader, class_names = loaddata.data_load("../dataset2/train", "../dataset2/test", 224, 224, 16)
# 打印类别名称
print("类别名称:", class_names)
# 加载模型
model = model_load(class_num=len(class_names))
# 定义损失函数
criterion = nn.CrossEntropyLoss()
# 添加权重衰减
optimizer = torch.optim.Adam(model.parameters(), lr=0.001, weight_decay=1e-5)
# 判断是否有可用的GPU
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
# 将模型移动到GPU上
model.to(device)
# 如果存在已有模型,则加载已有模型继续训练
if os.path.exists("mobilenet_fv.pth"):
model.load_state_dict(torch.load("mobilenet_fv.pth"))
print("加载已有模型继续训练")
best_val_accuracy = 0.0
train_losses = []
val_losses = []
train_accuracies = []
val_accuracies = []
# 设置早停的耐心值
patience = 5
# 初始化耐心计数器
patience_counter = 0
# 遍历所有epoch
for epoch in range(epochs):
print("Epoch {}/{}".format(epoch + 1, epochs))
running_loss = 0.0
correct = 0
total = 0
# 遍历训练集
for i, data in enumerate(train_loader, 0):
inputs, labels = data[0].to(device), data[1].to(device)
optimizer.zero_grad()
outputs = model(inputs)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
running_loss += loss.item()
_, predicted = torch.max(outputs.data, 1)
total += labels.size(0)
correct += (predicted == labels).sum().item()
train_losses.append(running_loss / len(train_loader))
train_accuracies.append(correct / total)
print("训练损失:{:.4f},准确率:{:.4f}".format(train_losses[-1], train_accuracies[-1]))
# 初始化验证损失、正确预测数量和总数量
running_loss = 0.0
correct = 0
total = 0
# 不计算梯度
with torch.no_grad():
# 遍历验证集
for i, data in enumerate(val_loader, 0):
# 获取输入和标签
inputs, labels = data[0].to(device), data[1].to(device)
# 前向传播
outputs = model(inputs)
# 计算损失
loss = criterion(outputs, labels)
# 累加损失
running_loss += loss.item()
# 获取预测结果
_, predicted = torch.max(outputs.data, 1)
# 累加总数量
total += labels.size(0)
# 累加正确预测数量
correct += (predicted == labels).sum().item()
# 计算验证损失和准确率
val_losses.append(running_loss / len(val_loader))
val_accuracies.append(correct / total)
# 打印验证损失和准确率
print("验证损失:{:.4f},准确率:{:.4f}".format(val_losses[-1], val_accuracies[-1]))
# 如果当前准确率大于最佳准确率
if val_accuracies[-1] > best_val_accuracy:
# 更新最佳准确率
best_val_accuracy = val_accuracies[-1]
# 保存模型
torch.save(model.state_dict(), "mobilenet_fv.pth")
print("模型已保存")
# 重置耐心计数器
patience_counter = 0
else:
# 累加耐心计数器
patience_counter += 1
# 如果耐心计数器超过耐心值
if patience_counter >= patience:
# 打印早停法触发信息
print("早停法触发,停止训练")
# 跳出循环
break
if __name__ == '__main__':
train(epochs=30)
(三)模型预测(classmodel.py)
1、思路
导入必要的库
import os
import torch
import torch.nn as nn
import torchvision
import time
import loaddata
json
:用于读取JSON格式的标签文件。mobilenet_v2
:从torchvision.models
导入预训练的MobileNetV2模型。torch
和torch.nn.functional
:PyTorch库和函数。Resize
,Compose
,ToTensor
,Normalize
:用于图像预处理的torchvision.transforms
中的变换。PIL.Image
:Python Imaging Library,用于读取和操作图像。
定义
SignLanguageRecognizer
类初始化函数
__init__
def __init__(self, module_file='./mobilenet_fv.pth', labels_file='./ab.json'):
# 初始化成员变量
self.module_file = module_file
self.CUDA = torch.cuda.is_available()
self.net = mobilenet_v2(num_classes=26)
if self.CUDA:
self.net.cuda()
self.net.load_state_dict(torch.load(self.module_file, map_location='cuda' if self.CUDA else 'cpu'))
self.net.eval()
self.labels = self.load_labels(labels_file)
- 设置模型文件路径和标签文件路径。
- 检测是否有可用的GPU。
- 加载一个带有26个输出类别的MobileNetV2模型。
- 将模型移到GPU上(如果可用)。
- 加载模型的参数。
- 将模型设置为评估模式(eval),这是为了关闭可能影响推理速度和精度的训练相关功能,如dropout。
- 加载标签。
加载标签函数
load_labels
def load_labels(self, file_path):
with open(file_path, 'r', encoding='utf-8') as file:
return json.load(file)
这个函数用于打开并读取JSON文件,返回其中的内容作为字典。
图像预处理函数
preprocess_image
@torch.no_grad()
def preprocess_image(self, image_stream):
img = Image.open(image_stream)
transform = Compose([
Resize((224, 224)),
ToTensor(),
Normalize(mean=[0.56719673, 0.5293289, 0.48351972], std=[0.20874391, 0.21455203, 0.22451781]),
])
img = transform(img)
img = torch.unsqueeze(img, 0)
if self.CUDA:
img = img.cuda()
return img
- 调整图像大小到224x224像素。
- 将图像转换为张量。
- 对张量进行归一化。
- 增加一个维度,以便适合模型输入。
- 如果使用GPU,将图像张量移动到GPU上。
图像识别函数
recognize
@torch.no_grad()
def recognize(self, image_stream):
img = self.preprocess_image(image_stream)
y = self.net(img)
y = F.softmax(y, dim=1)
p, cls_idx = torch.max(y, dim=1)
return y.cpu(), cls_idx.cpu()
这个函数使用预处理后的图像作为输入,经过模型预测后,返回经过softmax处理的概率分布和预测类别的索引。
获取预测结果函数
get_prediction
def get_prediction(self, image_stream):
probs, cls = self.recognize(image_stream)
_, cls = torch.max(probs, 1)
p = probs[0][cls.item()]
cls_index = str(cls.numpy()[0])
label_name = self.labels.get(cls_index, "未知标签")
return label_name, p.item()
这个函数调用recognize
函数获取预测结果,然后从标签字典中查找对应的标签名称,并返回标签名称和最大概率。
创建实例并进行预测
recongize = SignLanguageRecognizer()
ans = recongize.get_prediction('../dataset2/test/A/fe0f9990-a4cb-4351-96df-10c2b25c63d1.rgb_0000.png')
print(ans)
这里创建了一个SignLanguageRecognizer
实例,并使用给定的测试图像进行预测,最后打印出预测的标签和概率。
2、完整代码
import json
from torchvision.models import mobilenet_v2
import torch
import torch.nn.functional as F
from torchvision.transforms import Resize, Compose, ToTensor, Normalize
from PIL import Image
class SignLanguageRecognizer:
# 初始化函数,传入模型文件和标签文件
def __init__(self, module_file='./mobilenet_fv.pth', labels_file='./ab.json'):
self.module_file = module_file
# 判断是否有可用的GPU
self.CUDA = torch.cuda.is_available()
# 加载模型
self.net = mobilenet_v2(num_classes=26)
# 如果有可用的GPU,则将模型加载到GPU上
if self.CUDA:
self.net.cuda()
# 加载模型参数
self.net.load_state_dict(torch.load(self.module_file, map_location='cuda' if self.CUDA else 'cpu'))
# 将模型设置为评估模式
self.net.eval()
# 加载标签
self.labels = self.load_labels(labels_file)
# 加载标签
def load_labels(self, file_path):
# 打开文件
with open(file_path, 'r', encoding='utf-8') as file:
# 返回json格式的文件内容
return json.load(file)
@torch.no_grad() # 不计算梯度
def preprocess_image(self, image_stream):
# 打开图像流
img = Image.open(image_stream)
# 定义图像预处理操作
transform = Compose([
Resize((224, 224)), # 调整图像大小为224x224
ToTensor(), # 将图像转换为张量
Normalize(mean=[0.56719673, 0.5293289, 0.48351972], std=[0.20874391, 0.21455203, 0.22451781]), # 归一化
])
# 对图像进行预处理
img = transform(img)
# 在第0维增加一个维度
img = torch.unsqueeze(img, 0)
# 如果使用CUDA,则将图像移动到GPU
if self.CUDA:
img = img.cuda()
# 返回预处理后的图像
return img
@torch.no_grad() # 在此函数中不计算梯度
def recognize(self, image_stream):
# 对输入的图像进行预处理
img = self.preprocess_image(image_stream)
# 将预处理后的图像输入到神经网络中
y = self.net(img)
# 对神经网络的输出进行softmax处理
y = F.softmax(y, dim=1)
# 找到softmax处理后的最大值和对应的索引
p, cls_idx = torch.max(y, dim=1)
# 返回softmax处理后的输出和对应的索引
return y.cpu(), cls_idx.cpu()
def get_prediction(self, image_stream):
# 调用recognize方法,获取图像的预测概率和类别
probs, cls = self.recognize(image_stream)
# 获取最大概率的类别
_, cls = torch.max(probs, 1)
# 获取最大概率
p = probs[0][cls.item()]
# 将类别转换为字符串
cls_index = str(cls.numpy()[0])
# 获取对应的标签名称
label_name = self.labels.get(cls_index, "未知标签")
# 返回标签名称和最大概率
return label_name, p.item()
recongize=SignLanguageRecognizer()
ans=recongize.get_prediction('../dataset2/test/A/fe0f9990-a4cb-4351-96df-10c2b25c63d1.rgb_0000.png')
print(ans)
(四)标签文件(ab.json)
{
"0": "A",
"1": "B",
"2": "C",
"3": "D",
"4": "E",
"5": "F",
"6": "G",
"7": "H",
"8": "I",
"9": "J",
"10": "K",
"11": "L",
"12": "M",
"13": "N",
"14": "O",
"15": "P",
"16": "Q",
"17": "R",
"18": "S",
"19": "T",
"20": "U",
"21": "V",
"22": "W",
"23": "X",
"24": "Y",
"25": "Z"
}
五、注意事项
(一)目录结构
1、AI文件夹 下存放:数据加载、模型训练、模型预测、标签文件
2、dataset2文件夹 下存放:test(测试集)和 train(训练集)两个目录,test和train两个目录下存放26个字母文件夹
3、AI文件夹和dataset2文件夹同级
注:1、其他文件夹不用管
2、图中 test 只有A、B是因为之前进行测试,正式训练时应该是A到Z
(二)自己拍识别图片
1、建议横屏(因为图像会变换成224*224,有可能被裁减)
2、背景可以乱,但不要和肤色融为一体
3、手势做标准,建议找个手指长但不一样长的手拍
4、最好网上找......