三、计算机视觉_03LeNet5及手势识别案例

1 LeNet-5基本介绍

LeNet-5是一种经典的卷积神经网络(CNN)架构,由Yann LeCun在1998年提出,用于手写数字识别,LeNet-5是卷积神经网络的开创性工作之一,它引入了卷积层、池化层和全连接层的组合,为现代深度学习模型奠定了基础

LeNet-5网络结构包括以下几个关键层:

  1. 卷积层(Convolutional Layer):使用多个卷积核提取图像特征
  2. 池化层(Pooling Layer):通常使用最大池化(Max Pooling)或平均池化(Average Pooling)来降低特征图的空间维度
  3. 全连接层(Fully Connected Layer):将特征展平铺并连接到输出层进行分类

LeNet-5的具体网络结构如下:

  • 输入层:32x32像素的灰度图像
  • 卷积层C1:使用6个5x5的卷积核,输出6个28x28的特征图
  • 池化层S2:2x2的最大池化,输出6个14x14的特征图
  • 卷积层C3:使用16个5x5的卷积核,输出16个10x10的特征图
  • 池化层S4:2x2的最大池化,输出16个5x5的特征图
  • 全连接层C5:将16个5x5的特征图展平,连接到120维向量
  • 全连接层F6:120维全连接到84维向量
  • 输出层:84维全连接到10维,代表10个类别

全连接的图像展平处理:

  • 常规图像:[N, C, H, W]
  • 图像展平:[N, C * H * W]
  • 本质是改变shape,不会改变像素值,也不会改变数据本身的信息;只是为了对口型、为了科学计算、为了矩阵相乘

2 LeNet-5网络搭建

(1)复杂一点的写法(易于理解)

复制代码
# 引入pytorch和nn神经网络
import torch
import torch.nn as nn

class LeNet5(nn.Module):
    """
        自定义一个LeNet5神经网络
    """
    def __init__(self, in_channels=1, ):
        """
            初始化
        """
        # super(LeNet5, self).__init__()这行代码的作用是调用Model类的父类(即nn.Module)的__init__()方法
        super(LeNet5, self).__init__()
        # 第一层转换(卷积)
        self.conv1 = nn.Conv2d(
            in_channels=in_channels,
            out_channels=6,
            kernel_size=5, # stride=1, w-k+1=28, w=32-->k=5
            stride=1,
            padding=0
        )
        # 第二层转换(亚采样)
        self.mp1 = nn.MaxPool2d(
            kernel_size=2, # stride=2, 28/k=w, w=14-->k=2
            stride=2,
            padding=0
        )
        # 第三层转换(卷积)
        self.conv2 = nn.Conv2d(
            in_channels=6,
            out_channels=16,
            kernel_size=5, # stride=1, w-k+1=10, w=14-->k=5
            stride=1,
            padding=0
        )
        # 第四层转换(亚采样)
        self.mp2 = nn.MaxPool2d(
            kernel_size=2, # stride=2, 10/k=w, w=5-->k=2
            stride=2,
            padding=0
        )
        # 第五层转换(全连接)
        # 1、展平(常规图像:[N, C, H, W],展平:[N, C * H * W])
        # 数据处理的金标准:按维度理解和处理数据,画图和打印是行不通的,高纬度和大量的数据画图和打印会令人自闭!
        # 展平只是改变shape,不会改变像素值,不会改变数据本身的信息!
        # 之所以改变,是为了对口型,为了科学计算,为了矩阵相乘!
        self.flatten = nn.Flatten(
            start_dim=1,    # 第一个维度开始
            end_dim=-1    # 最后一个维度结束
        )
        # 2、全连接
        self.fc1 = nn.Linear(
            in_features=400,    # 上一个图的c*h*w=16*5*5=400
            out_features=120    # 输出是图中标注的120
        )
        self.fc2 = nn.Linear(
            in_features=120,
            out_features=84
        )
        self.fc3 = nn.Linear(
            in_features=84,
            out_features=n_classes
        )

    def forward(self, X):
        """
            前向传播
        """
        print(X.shape)
        X = self.conv1(X)
        print(X.shape)
        X = self.mp1(X)
        print(X.shape)
        X = self.conv2(X)
        print(X.shape)
        X = self.mp2(X)
        print(X.shape)
        X = self.flatten(X)
        print(X.shape)
        X = self.fc1(X)
        print(X.shape)
        X = self.fc2(X)
        print(X.shape)
        X = self.fc3(X)
        print(X.shape)

        return X

(2)简化后的写法

复制代码
# 引入pytorch和nn神经网络
import torch
import torch.nn as nn

# 简化的写法
class LeNet5(nn.Module):
    """
        自定义一个神经网络
    """
    def __init__(self, in_channels=1, n_classes=10):
        """
            初始化
        """
        super(LeNet5, self).__init__()

        # 1. 特征抽取
        self.feature_extractor = nn.Sequential(
            nn.Conv2d(in_channels=in_channels, 
                               out_channels=6, 
                               kernel_size=5,
                               stride=1,
                               padding=0),
            nn.MaxPool2d(kernel_size=2, 
                                stride=2,
                                padding=0),
            nn.Conv2d(in_channels=6,
                              out_channels=16,
                              kernel_size=5,
                              stride=1,
                              padding=0),
            nn.MaxPool2d(kernel_size=2, 
                               stride=2,
                               padding=0)
        )

        # 2. 分类输出
        self.classifier = nn.Sequential(
            nn.Flatten(start_dim=1, end_dim=-1),
            nn.Linear(in_features=400, out_features=120),
            nn.Linear(in_features=120, out_features=84),
            nn.Linear(in_features=84, out_features=n_classes)
        )

    def forward(self, x):
        """
            前向传播
        """
        # 1. 先做特征抽取
        x = self.feature_extractor(x)
        # 2. 再做分类回归
        x = self.classifier(x)
        return x

(3)效果测试

复制代码
# 实例化对象
m1 = LeNet5(in_channels=1)
# 创建一个随机X对象
X = torch.randn(2, 1, 32, 32)
# 获得预测结果
y_pred = m1(X)
# 查看预测结果的形状
y_pred.shape
# 查看预测结果的内容
y_pred

3 基于LeNet-5的手势识别案例

3.1 原始数据读取

人工智能的数据集很可能很大,要一次性把所有数据都读进内存是不现实的,因此,在本案例中,并不是把所有图像全部读进内存,而是先把所有图像的路径类别归纳和梳理出来,然后分批次读取

  • img_path

  • img_label

    """
    尝试读取 train
    """
    import os
    train_root = os.path.join("gesture", "train")

    图片路径的列表

    train_paths = []

    图片对应的数字列表

    train_labels = []

    for label in os.listdir(train_root):
    label_root = os.path.join(train_root, label)
    for file in os.listdir(label_root):
    file_path = os.path.join(label_root, file)
    print(file_path)
    print(label)
    train_paths.append(file_path)
    train_labels.append(label)

    """
    尝试读取 test (同理)
    """
    import os
    test_root = os.path.join("gesture", "test")
    test_paths = []
    test_labels = []

    for label in os.listdir(test_root):
    label_root = os.path.join(test_root, label)
    for file in os.listdir(label_root):
    file_path = os.path.join(label_root, file)
    test_paths.append(file_path)
    test_labels.append(label)

读取数据之后,我们可以构建标签字典

复制代码
# 构建 标签字典 label dict
labels = ["zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine"]
label2idx = {label: idx for idx, label in enumerate(labels)}
idx2label = {idx: label for label, idx in label2idx.items()}

# 打印输出
print(label2idx)
print(idx2label)

3.2 数据的批量化打包

批量化打包数据主要包括以下两个步骤:

  • 继承 Dataset,自定义一个数据集

  • 实例化 DataLoader

    Dataset 类是一个抽象类,它定义了数据集的接口,如果要自定义一个数据集类,则首先需要继承Dataset

    from torch.utils.data import Dataset

    DataLoader 类是一个迭代器,它封装了 Dataset 对象,并提供了批量加载数据、打乱数据、多进程加载等功能

    from torch.utils.data import DataLoader

    from PIL import Image

    torchvision 是一个与 PyTorch 深度学习框架配套使用的库,专门用于处理图像和视频数据。它提供了许多工具和预构建的模块,旨在简化计算机视觉任务中的数据处理、模型构建和训练流程

    transforms 是 torchvision 中的一个模块,它提供了许多实用的图像转换函数(与深度学习模型架构Transformer不是一个东西,不要弄混了)

    from torchvision import transforms
    import torch

    class GestureDataset(Dataset):
    """
    自定义手势识别数据集
    """
    def init(self, X, y):
    """
    初始化
    """
    self.X = X
    self.y = y

    复制代码
      def __getitem__(self, idx):
          """
              实现:按下标来索引一个样本
              __getitem__ 是 python 中的一个魔术方法(也称为特殊方法或双下划线方法),它在对象被用于下标操作时自动调用
              这个方法允许对象模拟序列(如列表、元组)或映射(如字典)的行为,使得对象可以通过索引或键来访问其元素
              这个方法在 Dataset 类中有一个特定的约定,它只接受一个参数,即索引参数
          """
          # 获取图像路径
          img_path = self.X[idx]
          # 读取图像
          img = Image.open(fp=img_path)
          # 统一大小
          img = img.resize((32, 32))
          # 转张量 [C, H, W]------>归一化
          # transforms.ToTensor() 是一个预处理操作,它将 PIL 图像或 NumPy 数组转换为 torch.Tensor 对象
          # 这个转换包括将图像的像素值从范围 [0, 255] 缩放到 [0.0, 1.0]
          img = transforms.ToTensor()(img)
          # [0, 1]减去0.5,除以0.5,得到[-1, 1]------>标准化
          img = transforms.Normalize(mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5])(img)
    
          # 读取标签
          img_label = self.y[idx]
          # 标签转 id
          img_idx = label2idx.get(img_label)
          # 转张量
          label = torch.tensor(data=img_idx, dtype=torch.long)
    
          return img, label
    
      def __len__(self):
          """
              返回该数据集的样本个数
          """
          return len(self.X)

    训练集加载器

    获取训练数据集

    train_dataset = GestureDataset(X=train_paths, y=train_labels)

    shuffle=True表示每个epoch开始时都随机打乱样本,batch_size=16表示每个批次将包含16个样本

    train_dataloader = DataLoader(dataset=train_dataset, shuffle=True, batch_size=16)

    测试集加载器(同上)

    test_dataset = GestureDataset(X=test_paths, y=test_labels)
    test_dataloader = DataLoader(dataset=test_dataset, shuffle=False, batch_size=32)

    打印看看测试数据集的效果

    for X, y in test_dataloader:
    print(X.shape)
    print(y.shape)
    break

3.3 模型搭建

复制代码
# 引入pytorch和nn神经网络
import torch
import torch.nn as nn

# 简化的写法
class LeNet5(nn.Module):
    """
        自定义一个神经网络
    """
    def __init__(self, in_channels=1, n_classes=10):
        """
            初始化
        """
        super(LeNet5, self).__init__()

        # 1. 特征抽取
        self.feature_extractor = nn.Sequential(
            nn.Conv2d(in_channels=in_channels, 
                               out_channels=6, 
                               kernel_size=5,
                               stride=1,
                               padding=0),
            nn.MaxPool2d(kernel_size=2, 
                                stride=2,
                                padding=0),
            nn.Conv2d(in_channels=6,
                              out_channels=16,
                              kernel_size=5,
                              stride=1,
                              padding=0),
            nn.MaxPool2d(kernel_size=2, 
                               stride=2,
                               padding=0)
        )

        # 2. 分类输出
        self.classifier = nn.Sequential(
            nn.Flatten(start_dim=1, end_dim=-1),
            nn.Linear(in_features=400, out_features=120),
            nn.Linear(in_features=120, out_features=84),
            nn.Linear(in_features=84, out_features=n_classes)
        )

    def forward(self, x):
        """
            前向传播
        """
        # 1. 先做特征抽取
        x = self.feature_extractor(x)
        # 2. 再做分类回归
        x = self.classifier(x)
        return x

3.4 模型训练

复制代码
# 设置训练轮次
epochs = 50
# 设备
device = "cuda" if torch.cuda.is_available() else "cpu"
# 实例化模型
model = Model()
model.to(device=device)
# 优化器
# 创建一个 Adam 优化器实例,Adam 是一种自适应学习率优化算法,它结合了 RMSprop 和 Momentum 两种优化算法的优点。它通常表现良好,并且对许多不同的问题都适用
# params=model.parameters() 用于指定了优化器将要优化的参数,参数值model.parameters() 是一个生成器,它返回模型中所有需要梯度更新的参数(即权重和偏置)
# lr=1e-3:设置优化器的学习率,即每次参数更新的步长为 0.001
optimizer = torch.optim.Adam(params=model.parameters(), lr=1e-3)
# 损失函数
# 创建一个交叉熵损失函数的实例,交叉熵损失函数是分类问题中最常用的损失函数之一,特别是当输出是互斥类别的概率分布时
# 实例化nn中交叉熵损失的计算类,创建损失函数对象
loss_fn = nn.CrossEntropyLoss()

# 准确率计算
def get_acc(data_loader):
    accs = []
    # model.eval() 用于将模型设置为评估(evaluation)模式。这个方法在模型的训练和评估过程中扮演着重要的角色,尤其是在使用某些特定层(如 Dropout 和 Batch Normalization)时
    model.eval()
    with torch.no_grad():
        for X, y in data_loader:
            X = X.to(device=device)
            y = y.to(device=device)
            y_pred = model(X)
            # 获取预测结果中概率最高的类别,argmax(dim=-1) 返回最后一个维度(即概率分布)中最大值的索引
            y_pred = y_pred.argmax(dim=-1)
            acc = (y_pred == y).to(torch.float32).mean().item()
            accs.append(acc)
    # ndigits=5表示保留5位小数,round表示四舍五入
    final_acc = round(number=sum(accs) / len(accs), ndigits=5)
    return final_acc

import time

# 训练过程
def train():

    # 训练集准确率列表
    train_accs = []
    # 测试集准确率列表
    test_accs = []
    # 当前准确率
    cur_test_acc = 0

    # 初始准确率
    train_acc = get_acc(data_loader=train_dataloader)
    test_acc = get_acc(data_loader=test_dataloader)
    train_accs.append(train_acc)
    test_accs.append(test_acc)

    print(f"训练之前的准确率:train_acc: {train_acc},test_acc: {test_acc}")

    # 每一轮次
    for epoch in range(epochs):
        # 模型设置为 train 模式
        model.train()
        # 每一轮的开始时间
        start_train = time.time()
        for X, y in train_dataloader:
            # 0, 数据搬家
            X = X.to(device=device)
            y = y.to(device=device)
            # 1,正向传播
            y_pred = model(X)
            # 2,计算损失
            loss = loss_fn(y_pred, y)
            # 3,反向传播
            loss.backward()
            # 4,优化一步
            optimizer.step()
            # 5,清空梯度
            optimizer.zero_grad() 
        # 每一轮的结束时间
        stop_train = time.time()

        # 本轮处理之后的准确率
        train_acc = get_acc(data_loader=train_dataloader)
        test_acc = get_acc(data_loader=test_dataloader)
        train_accs.append(train_acc)
        test_accs.append(test_acc)

        # 保存模型
        if cur_test_acc < test_acc:
            cur_test_acc = test_acc
            # 保存更好的模型
            torch.save(obj=model.state_dict(), f="lenet_best.pt")
        # 保存最好的模型
        torch.save(obj=model.state_dict(), f="lenet_last.pt") 

        # 日志监控
        print(f"""当前为第 {epoch + 1} 轮:""")
        print(f"""训练准确率 (train_acc)\t测试准确率 (test_acc)\t运行时间 (elapsed_time)""")
        # 在python的字符串格式化中,:<width用于指定字符串的最小宽度
        print(f"""{train_acc:<18}\t{test_acc:<17}\t{round(number=stop_train - start_train, ndigits=3)}秒""")

    return train_accs, test_accs

调用训练方法,获得优化后的最佳模型(lenet_last.pt)

复制代码
train_accs, test_accs = train()

3.5 Streamlit页面实现

Step1: 将【3.3 模型搭建】中的代码,复制粘贴到一个名为models.py的python文件中进行保存

Step2: 将【3.4 模型训练中获得的】lenet_last.pt文件,与models.py放到同级目录中

Step3: 新建一个modelApp.py的文件,与上面两个文件处于同级目录,复制粘贴以下内容

复制代码
import streamlit as st
import torch
import os
from PIL import Image
from models import LeNet5
from torchvision import transforms


def predict(img_path, model, device):
    # 判断图像是否存在
    if not os.path.exists(img_path):
        raise FileNotFoundError("文件已丢失")
    else:
        # 读取图像
        img = Image.open(fp=img_path)

        # 预处理及转张量
        img = img.resize((32, 32))
        img = transforms.ToTensor()(img)
        img = transforms.Normalize(mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5])(img)

        # 添加维度
        # 上面的维度是[height, width, channels],需要增加一个维度,使其变成[batch_size, channels, height, width]
        # 填0,即代表从第0个位置,给这个张量添加一个维度(1),也就是batch_size=1
        img = img.unsqueeze(0)

        # 数据搬家
        img = img.to(device=device)
        # 将模型设为评估模式
        model.eval()

        # 无梯度环境
        with torch.no_grad():
            # 正向传播
            y_pred = model(img)
            # 解析结果
            y_pred = y_pred.argmax(dim=-1).item()
            # 返回结果
            return y_pred

if __name__ == "__main__":
    # 显示当前设备是GPU设备还是CPU
    device = "cuda" if torch.cuda.is_available() else "cpu"
    st.write(f"当前设备是:{device}")

    # 加载模型
    m1 = LeNet5()
    m1.to(device=device)
    # 加载之前训练好的权重
    m1.load_state_dict(state_dict=torch.load(f="lenet_best.pt", map_location=device), strict=False)

    # 上传一张图片
    uploaded_img = st.file_uploader("请上传一张图片", type=["png", "jpg", "jpeg"])
    # 将上传的图像文件保存到临时文件
    if uploaded_img is not None:
        with open(file="temp_img.jpg", mode="wb") as f:
            f.write(uploaded_img.getvalue())
        img_path = "temp_img.jpg"
        if img_path:
            # 加载训练好的lenet_best.pt模型
            pred = predict(img_path=img_path, model=m1, device=device)
            st.write(f"经预测,此图片中对应的手势是:{pred}")
            # 显示上传好的图片
            img = Image.open(fp=img_path)
            st.image(image=img, caption="上传的图片", use_column_width=True)

Step4: 使用以下命令运行modelApp.py,通过streamlit页面进行使用

复制代码
streamlit run modelApp.py

效果示例:

相关推荐
杨浦老苏4 分钟前
本地优先的AI个人助手Moltis
人工智能·docker·ai·群晖
OBS插件网10 分钟前
OBS直播如何给人脸加口罩特效?OBS口罩特效插件下载安装教程
人工智能·数码相机·语音识别·产品经理
LitchiCheng15 分钟前
Mujoco 如何添加 Apriltag 并获得相机视野进行识别
人工智能·python·开源
想用offer打牌23 分钟前
一站式了解Agent Skills
人工智能·后端·ai编程
一切尽在,你来26 分钟前
LangGraph快速入门
人工智能·python·langchain·ai编程
阿杰学AI1 小时前
AI核心知识110—大语言模型之 AI Collaboration Manager(简洁且通俗易懂版)
人工智能·ai·语言模型·自然语言处理·aigc·人机交互·ai协作管理员
SCLchuck1 小时前
人工智能-概率密度估计
人工智能·python·概率论·概率密度估计
王解1 小时前
AI Agent记忆模块进化史:从临时缓存到认知架构的设计范式
人工智能·缓存·架构
琅琊榜首20202 小时前
AI+编程实战:小说高效改编短剧全指南
人工智能
新缸中之脑2 小时前
Graphlit: AI代理的上下文图层
大数据·人工智能