从零开始的深度学习项目(PyTorch识别人群行为)

PyTorch识别人群行为

系统环境介绍

环境版本
Python3.11.5
pandas2.0.3
numpy1.24.3
torch2.1.2+cu121

注意: 2.1.2+cu121 这样的版本号通常用于描述 TensorFlow 等深度学习框架的版本信息,其中:

  • 2.1.2 是 TensorFlow 的主要版本号,表示主要的功能和接口的变化。
  • cu121 表示该 TensorFlow 版本是专门针对 CUDA 12.1 进行了优化的版本,其中 cu 是 CUDA 的缩写,121 是 CUDA 的版本号。

因此,2.1.2+cu121 表示 TensorFlow 的版本是 2.1.2,并且该版本针对 CUDA 12.1 进行了优化,以提供更好的性能和兼容性。

目录

1、项目背景
2、加载数据集

3、创建一个CNN模型

4、可视化


项目背景: 随着社会的不断发展和城市的快速扩张,公共场所的管理和安全监控变得越来越重要。特定行为的识别与监测,如人群中的电话使用和吸烟行为,成为改善公共场所安全与管理的关键环节。

本项目旨在利用计算机视觉和深度学习技术,构建一个能够识别人群行为的智能系统,主要针对接听电话、吸烟以及无行为的分类。

# 导入所需的库
import torch
import torch.nn as nn
import numpy as np
import torch.optim as optim
from torchvision import datasets, transforms
from torch.utils.data import Subset, DataLoader
import os
os.environ['KMP_DUPLICATE_LIB_OK']='True'
# 解决遇到与MKL或库冲突有关的错误,防止jupyter notebook中断内核
import matplotlib.pyplot as plt
plt.rcParams['font.sans-serif'] = ['SimHei']  # 用来正常显示中文标签
plt.rcParams['axes.unicode_minus'] = False  # 用来正常显示负号

加载数据集

数据预处理

torchvision.transforms.Compose():

PyTorch 中 torchvision.transforms 模块的一个实用程序类,专门用于帮助构建一系列图像转换操作。此类允许链接或合成多个图像变换,然后可以将其顺序应用于输入图像或张量。
参数

  1. transforms.Resize:此转换将图像的大小调整为指定的大小。

  2. transforms.RandomCrop:将图像随机裁剪到指定的大小。用于数据扩充。

  3. transforms.ToTensor:将图像(PIL-image或numpy.ndarray)转换为 PyTorch 张量。

  4. transforms.Normalize:函数用于将图像数据标准化,其作用是对图像进行归一化处理,使其均值变为 0 0 0,标准差变为 1 1 1。对于每个通道执行以下操作:

input [ c h a n n e l ] = input [ c h a n n e l ] − mean [ c h a n n e l ] std [ c h a n n e l ] \text{input}[channel] = \frac{\text{input}[channel] - \text{mean}[channel]}{\text{std}[channel]} input[channel]=std[channel]input[channel]mean[channel]
例如:

from PIL import Image
img = Image.open("./识别人群行为_1.jpg")#原图片大小为(1080, 1068)
data1 = transforms.Resize((224, 224))(img)#将原图片大小调整为(224,244)
data2 = transforms.RandomCrop(512)(img)#在原图片中随机裁剪图片大小为(512,512)

结果:

# 创建图片矩阵

## 显示原图片
plt.subplot(1,3,1)
plt.imshow(img)
plt.title("img")

## 显示data1中的图片
plt.subplot(1,3,2)
plt.imshow(data1)
plt.title("data1")

## 显示data2中的图片
plt.subplot(1,3,3)
plt.imshow(data2)
plt.title("data2")

# 调整图片矩阵间距
plt.subplots_adjust(wspace=0.3)
plt.show()

在这里插入图片描述

加载数据

datasets.ImageFolder():

此类用于加载数据集,其中图像被组织在特定的文件夹结构中。

root/
├── dog/
│   ├── 1.png
│   ├── 2.png
│   └── 3.png
│
└── cat/
    ├── 1.png
    ├── 2.png
    └── 3.png

这里,root 是包含每个类(如dog 和cat )的子目录的根目录,每个类目录都包含与该类对应的映像。

这里我们的数据集名称为(smoking and calling image_datasets)其每个类为(calling_images,normal_images,smoking_images)

在使用PyTorch加载数据集。我们常用 ImageFolder 类通过指定根目录并在需要时对图像应用转换来轻松加载这种类型的数据集。它提供了一种访问图像及其相应标签的简单方式。
其中 datasets.ImageFolder 中的 transform=transform 参数是一种在实例化ImageFolder数据集时将一组图像转换传递给该数据集的方法。

划分数据集

torch.utils.data.random_split():

将数据集拆分为随机的非重叠子集。

创建数据加载器

DataLoader():

主要目的是有效地从PyTorch数据集对象加载和提供数据。它通过多处理处理处理各种方面,如批处理、混洗和并行数据加载,以优化训练过程。
参数:

  1. my_dataset:您的自定义数据集(MyCustomDataset)的实例,它提供对数据样本的访问。

  2. batch_size:DataLoader返回的每个批次中的样本数。

  3. shuffle:是否在每个epoch之前对数据进行shuffle(设置为True进行训练)。

  4. num_workers:用于数据加载的子进程数。它通过利用多个子流程进行数据提取,实现了更快的数据加载。

  5. DataLoader对数据样本的加载和迭代过程进行了抽象,使您能够通过在每次迭代中提供一批数据来高效地集中精力训练或评估模型。

# 数据预处理和加载
transform = transforms.Compose([
    transforms.Resize((224, 224)),
    transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2, hue=0.1),  # 随机颜色变换
    transforms.ToTensor(),
    transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))  # 归一化
])

# 加载完整的训练数据集
full_train_data = datasets.ImageFolder(r'.\smoking and calling image_datasets\smoking and calling image_datasets\train', transform=transform)

# 将数据按比例分成训练集和测试集
train_size = int(0.9 * len(full_train_data))
test_size = len(full_train_data) - train_size
train_data, test_data = torch.utils.data.random_split(full_train_data, [train_size, test_size])

# 创建训练数据加载器和测试数据加载器
train_loader = DataLoader(train_data, batch_size=8, shuffle=True, num_workers=2)
test_loader = DataLoader(test_data, batch_size=8, shuffle=False, num_workers=2)

创建一个模型(CNN)

用于处理具有类似网格结构数据的深度学习模型。CNN特别擅长处理图像数据,但也可用于其他类型的数据,如音频、文本等。
以下是CNN模型的一般结构和主要组成部分:
1. 卷积层(Convolutional Layer):

卷积是CNN的核心操作。卷积层通过一系列的卷积核(也称为过滤器)对输入数据进行滤波,从而提取特征。卷积操作有助于捕获数据中的空间关系。

2. 激活函数(Activation Function):

通常,在卷积层的输出之后会应用激活函数,以引入非线性性。常见的激活函数包括ReLU(Rectified Linear Unit)和其变体。

3. 池化层(Pooling Layer):

池化层用于减小特征图的空间尺寸,同时保留重要的特征。最大池化和平均池化是常见的池化操作。

4. 全连接层(Fully Connected Layer):

在提取了特征之后,通常将其输入到全连接层,这些层负责对高级抽象特征进行分类。在全连接层中,每个神经元与前一层的所有神经元相连接。

5. 批归一化层(Batch Normalization):

批归一化用于规范网络中间层的输入,有助于加速训练过程并提高模型的鲁棒性。

6. Dropout层:

Dropout是一种正则化技术,通过在训练过程中随机关闭一些神经元,防止过拟合。

7. 损失函数(Loss Function):

用于衡量模型输出与实际标签之间的差异。常见的损失函数包括交叉熵损失。

8. 优化器(Optimizer):

优化器用于调整模型参数,使损失函数最小化。Adam、SGD等是常用的优化算法。

卷积核的通道

意义:卷积核的通道数量影响了它所能学习到的特征种类和复杂性。下面我来介绍几种通道卷积核
1. 单通道卷积核:

在灰度图像中,通常使用单通道卷积核。单通道的卷积核与输入图像进行卷积操作,用于提取图像中的特征。

应用场景(图像边缘检测,特征提取,图像滤波)

2. RGB图像(三通道)对应的卷积核:

在RGB彩色图像中,每个卷积核需要对应于每个颜色通道。这意味着对于RGB图像,卷积核的通道数通常是3,每个通道分别对应红色、绿色和蓝色通道。

应用场景(颜色特征提取,纹理和颜色结合特征提取)

3. 多通道卷积核:

在更复杂的情况下,卷积核也可以具有多个通道。例如,如果输入图像具有更多的通道(如4或更多),那么相应的卷积核可以具有与输入通道数相匹配的多个通道,以处理这种多通道输入。

应用场景(多层次特征提取,视频处理和分析,多模态数据处理)

例如:

以单通道卷积为例,输入为(1,6,6),分别表示1个通道,宽为6,高为6。假设卷积核大小为3x3,padding=0,stride=1。
过程如下:

conv_input = torch.tensor([[0, 0, 0, 0, 0, 0, 0, 0],
                           [0, 0, 0, 0, 1, 0, 0, 0],
                           [0, 0, 0, 1, 1, 0, 0, 0],
                           [0, 0, 1, 0, 1, 0, 0, 0],
                           [0, 1, 1, 1, 1, 1, 1, 0],
                           [0, 0, 0, 0, 1, 0, 0, 0],
                           [0, 0, 0, 0, 1, 0, 0, 0],
                           [0, 0, 0, 0, 0, 0, 0, 0]])

# 定义单通道卷积核权重矩阵
conv_kernel = torch.tensor([[1, 1, 1],
                            [1, 1, 1],
                            [1, 1, 1]])

# 使用PyTorch的卷积函数进行卷积操作
conv = torch.nn.functional.conv2d(conv_input.unsqueeze(0).unsqueeze(0).float(),
                                  conv_kernel.unsqueeze(0).unsqueeze(0).float(),
                                  padding=0)

# 可视化权重矩阵
plt.subplot(1, 3, 1)
plt.imshow(conv_input, cmap='gray')
plt.title('Original Input')

plt.subplot(1, 3, 2)
plt.imshow(conv_kernel, cmap='gray')
plt.title('Convolution Kernel')

plt.subplot(1, 3, 3)
plt.imshow(conv.squeeze(), cmap='gray')
plt.title('Convolved')
plt.tight_layout()
plt.show()

在这里插入图片描述

这个卷积核的每个值都是1,因此它在卷积操作中执行的是平均池化,将输入图像中每个区域的像素值取平均。
输出结果:

print('conv_input:\n',np.array(conv_input))
print('conv_kernel:\n',np.array(conv_kernel))
print('conv:\n',np.array(conv))
conv_input:
 [[0 0 0 0 0 0 0 0]
 [0 0 0 0 1 0 0 0]
 [0 0 0 1 1 0 0 0]
 [0 0 1 0 1 0 0 0]
 [0 1 1 1 1 1 1 0]
 [0 0 0 0 1 0 0 0]
 [0 0 0 0 1 0 0 0]
 [0 0 0 0 0 0 0 0]]
conv_kernel:
 [[1 1 1]
 [1 1 1]
 [1 1 1]]
conv:
 [[[[0. 1. 3. 3. 2. 0.]
   [1. 2. 5. 4. 3. 0.]
   [3. 5. 7. 6. 5. 2.]
   [3. 4. 6. 5. 5. 2.]
   [2. 3. 5. 5. 5. 2.]
   [0. 0. 2. 2. 2. 0.]]]]

激活函数

意义:引入了非线性性质,使得神经网络能够学习更加复杂的映射关系。
1. Sigmoid 函数(Logistic 函数):

  • 映射到 (0, 1) 范围:

    Sigmoid函数的主要特性是将任意实数输入 $ x$ 映射到区间 (0, 1) 内。随着 $ x$ 的增加,$ \sigma(x)$ 接近1;随着 $ x$ 的减小, σ ( x ) \sigma(x) σ(x)接近0。

  • 平滑变化:

    Sigmoid函数具有平滑的S形曲线,这使得神经网络在训练中更容易调整权重,因为其导数可以在大部分输入范围内都不为零。

  • 用途:

    Sigmoid函数常用于二分类问题的输出层,将网络的输出转化为概率值。如果输出值大于阈值(通常为0.5),则模型预测为正类;否则,预测为负类。

  • 梯度消失问题:

    尽管Sigmoid函数具有平滑性,但它在接近极端值(0和1)时,导数趋近于零,这可能导致梯度消失问题,影响深度神经网络的训练效果。

Sigmoid 函数(Logistic 函数)公式如下:

σ ( x ) = 1 1 + e − x \sigma(x) = \frac{1}{1 + e^{-x}} σ(x)=1+ex1

可视化函数:

def sigmoid(x):
    return 1 / (1 + np.exp(-x))

# 生成一系列在指定范围内的数据
x_values = np.linspace(-7, 7, 200)

# 计算对应的Sigmoid值
y_values = sigmoid(x_values)

# 绘制Sigmoid函数的图表
plt.figure(figsize=(8, 6))
plt.plot(x_values, y_values, label='Sigmoid Function', color='blue')
plt.title('Sigmoid Function')
plt.xlabel('Input Value (x)')
plt.ylabel('Output Value (Sigmoid(x))')
plt.axhline(0.5, color='red', linestyle='--', linewidth=1, label='Threshold (0.5)')
plt.legend()
plt.grid(True)
plt.show()

在这里插入图片描述

应用范围:

  • 二分类问题: Sigmoid函数广泛用于解决二分类问题,如垃圾邮件检测、疾病诊断、文本情感分析等。它将网络的输出映射到 (0, 1) 范围,可被解释为样本属于某个类别的概率。

  • 输出层激活函数: 在神经网络的输出层,特别是用于二分类任务时,Sigmoid函数通常被用作激活函数。它可以将网络的原始输出转化为概率值,方便进行阈值分类。

  • 逻辑回归模型: Sigmoid函数常用于逻辑回归模型,用于建模二分类问题。在逻辑回归中,Sigmoid函数将线性组合的输入映射到概率。

  • 生成对抗网络(GANs): 在生成对抗网络中,Sigmoid函数通常作为判别器(discriminator)的最后一层激活函数,用于输出一个介于 0 和 1 之间的概率值,表示输入数据是真实样本的概率。
    2. ReLU 函数(Rectified Linear Unit):

  • 映射到非负数:

    ReLU函数的主要特性是将输入 $ x $ 映射到非负数。如果输入 $ x $ 大于零,则输出为 $ x $;如果输入 $ x $ 小于或等于零,则输出为零。

  • 非线性变换:

    ReLU引入了非线性性,这对于神经网络的学习能力至关重要。它允许网络学习复杂的特征和模式。

  • 计算效率:

    ReLU的计算相对简单,因为对于正数输入,输出即为输入,无需进行复杂的计算。

  • Dead ReLU 问题:

    尽管ReLU在很多情况下表现出色,但可能存在Dead ReLU问题,即某些神经元在训练过程中可能永远不会被激活。为了解决这个问题,Leaky ReLU等ReLU的变体被引入。

ReLU函数(Rectified Linear Unit)公式如下:

f ( x ) = max ⁡ ( 0 , x ) f(x) = \max(0, x) f(x)=max(0,x)

可视化函数:

def relu(x):
    return np.maximum(0, x)

# 生成一系列在指定范围内的数据
x_values = np.linspace(-3, 3, 200)

# 计算对应的ReLU值
y_values = relu(x_values)

# 绘制ReLU函数的图表
plt.figure(figsize=(8, 6))
plt.plot(x_values, y_values, label='ReLU Function', color='green')
plt.title('ReLU Function')
plt.xlabel('Input Value (x)')
plt.ylabel('Output Value (ReLU(x))')
plt.axhline(0, color='red', linestyle='--', linewidth=1, label='Zero Line')
plt.legend()
plt.grid(True)
plt.show()

在这里插入图片描述

应用范围:

  • 图像处理和计算机视觉: ReLU 在卷积神经网络(CNN)中常用于图像处理和计算机视觉任务,如图像分类、物体检测和语义分割。它能够有效地捕捉图像中的非线性特征。

  • 自然语言处理: 在自然语言处理任务中,如文本分类、命名实体识别和机器翻译,ReLU 作为激活函数被广泛使用。它有助于学习词嵌入和句子表示。

  • 游戏和强化学习: 在强化学习领域,特别是在处理游戏环境的深度强化学习中,ReLU 常被用于神经网络的隐藏层,以提高网络的学习能力。

  • 语音识别: 在语音处理领域,ReLU 被应用于深度学习模型,用于语音识别和语音生成任务。

  • 推荐系统: 在推荐系统中,ReLU 用于学习用户和物品之间的复杂关系,提高推荐的准确性。

  • 时间序列分析: 在处理时间序列数据的任务中,如股票预测、天气预测等,ReLU 被用于循环神经网络(RNN)和长短时记忆网络(LSTM)等结构。

3. Tanh 函数(双曲正切函数):

  • 映射到 (-1, 1) 范围:

    Tanh函数的主要特性是将任意实数输入 x x x 映射到区间 $(-1, 1) $内。随着 x x x 的增加, tanh ⁡ ( x ) \tanh(x) tanh(x)接近1;随着 x x x 的减小, tanh ⁡ ( x ) \tanh(x) tanh(x) 接近-1。

  • 零均值性质:

    Tanh函数具有零均值性质,即其输出的均值为零。这有助于神经网络的训练,尤其在一些优化算法中。

  • 平滑变化:

    Tanh函数同样具有平滑的S形曲线,类似于Sigmoid函数,这使得神经网络在训练中更容易调整权重。

  • 用途:

    Tanh函数常用于处理具有零均值的数据,如图像数据的预处理。在循环神经网络(RNN)等结构中,Tanh也被用于学习时间序列数据的表示。

  • 梯度消失问题:

    类似于Sigmoid函数,Tanh函数在接近极端值时导数趋近于零,可能导致梯度消失问题。在深度学习中,一些激活函数的变体被提出以缓解这个问题。

Tanh函数(双曲正切函数)公式如下:

tanh ⁡ ( x ) = e 2 x − 1 e 2 x + 1 \tanh(x) = \frac{e^{2x} - 1}{e^{2x} + 1} tanh(x)=e2x+1e2x1

可视化函数:

def tanh(x):
    return np.tanh(x)

# 生成一系列在指定范围内的数据
x_values = np.linspace(-3, 3, 200)

# 计算对应的Tanh值
y_values = tanh(x_values)

# 绘制Tanh函数的图表
plt.figure(figsize=(8, 6))
plt.plot(x_values, y_values, label='Tanh Function', color='orange')
plt.title('Tanh Function')
plt.xlabel('Input Value (x)')
plt.ylabel('Output Value (Tanh(x))')
plt.legend()
plt.grid(True)
plt.show()

在这里插入图片描述

应用范围:

  • 循环神经网络(RNN): Tanh函数常用于RNN的隐藏层,用于学习和表示时间序列数据。其零均值性质有助于缓解梯度消失问题,使得网络更容易训练。

  • 图像处理: Tanh函数可以用于图像数据的预处理,特别是在需要将像素值映射到 $(-1, 1) $范围时。这有助于神经网络更好地学习图像特征。

  • 生成对抗网络(GANs): 在生成对抗网络中,Tanh函数通常作为生成器(generator)的最后一层激活函数,用于输出范围在 $(-1, 1) $内的生成图像。

  • 序列到序列模型: 在序列到序列的模型中,如机器翻译任务,Tanh函数可以用于编码器(encoder)和解码器(decoder)的隐藏层,以处理输入和输出序列的表示。

  • 梯度消失问题: Tanh函数相对于Sigmoid函数在一些情况下更有用,因为它的输出范围更广,且零均值性质能够缓解梯度消失问题。

  • 神经网络的隐藏层: Tanh函数可以用作神经网络的隐藏层激活函数,特别是在需要处理均值为零的数据时。

4. Softmax 函数:

  • 概率分布:

    Softmax函数的主要特性是将输入向量的元素映射到 ( 0 , 1 ) (0, 1) (0,1) 范围,并且所有元素的和等于 1 1 1,形成一个概率分布。

  • 相对大小不变性:

    Softmax函数对输入向量的相对大小保持不变,即如果输入向量中的元素 x i x_i xi 大于 x j x_j xj,则Softmax输出中对应的概率 softmax ( x ) i \text{softmax}(\mathbf{x})_i softmax(x)i 也会大于 softmax ( x ) j \text{softmax}(\mathbf{x})_j softmax(x)j

  • 多类别分类问题:

    Softmax常用于多类别分类问题的输出层,将模型的原始输出转化为类别概率。在这种情况下,模型预测为每个类别的概率由Softmax函数输出。

  • 交叉熵损失:

    Softmax函数结合交叉熵损失函数常用于训练分类模型,帮助模型更好地拟合目标分布。

  • 梯度计算:

    Softmax函数的梯度计算相对简单,这有助于在反向传播中有效地更新模型参数。

Softmax 函数公式如下:

softmax ( x ) i = e x i ∑ j = 1 N e x j \text{softmax}(\mathbf{x})_i = \frac{e^{x_i}}{\sum_{j=1}^{N} e^{x_j}} softmax(x)i=j=1Nexjexi

可视化函数:

def softmax(x):
    exp_x = np.exp(x - np.max(x))  # 对输入进行归一化处理,防止数值溢出
    return exp_x / np.sum(exp_x, axis=0)

# 生成一系列在指定范围内的数据
x_values = np.linspace(-3, 3, 200)

# 计算对应的Softmax值
y_values = softmax(x_values)

# 绘制Softmax函数的图表
plt.figure(figsize=(8, 6))
plt.plot(x_values, y_values, label='Softmax Function', color='purple')
plt.title('Softmax Function')
plt.xlabel('Input Value (x)')
plt.ylabel('Output Value (Softmax(x))')
plt.legend()
plt.grid(True)
plt.show()

在这里插入图片描述

应用范围:

  • 多类别分类模型的输出层: Softmax函数常用于神经网络输出层,将原始分数转换为概率分布。在多类别分类问题中,Softmax将模型的输出映射到每个类别的概率,使得模型可以对输入进行分类。

  • 交叉熵损失函数: Softmax通常与交叉熵损失函数结合使用,用于训练分类模型。交叉熵损失衡量了模型输出的概率分布与真实标签的差异,Softmax将模型的输出转化为概率分布的形式,与交叉熵损失配合使用可以有效地进行模型训练。

  • 神经网络中的概率表示: 在某些任务中,需要模型输出样本属于每个类别的概率,而不仅仅是最终的预测类别。Softmax函数提供了这样的概率表示。

  • 生成对抗网络(GANs): 在生成对抗网络中,Softmax函数可用于生成器网络的输出层,将生成的样本映射到一个合法的概率分布。

  • 自然语言处理(NLP)中的标签预测: 在文本分类、命名实体识别等NLP任务中,Softmax函数常用于对每个标签的预测进行概率化。

  • 多任务学习: Softmax函数也可以用于多任务学习的场景,其中模型需要同时预测多个相关任务的输出。

池化层

意义:小特征图的空间尺寸,降低计算复杂度,同时保留重要的特征信息。

PyTorch中的一个二维最大池化(max pooling)操作。它用于减小输入数据的空间维度,通过在每个局部区域中选择最大值来提取重要特征。

具体参数如下:

kernel_size:

 池化窗口的大小,指定了在进行池化时在输入数据上滑动的窗口的形状。可以是单个整数,表示正方形窗口的边长,也可以是一个元组 (h, w),表示窗口的高度和宽度。

stride:

指定在输入数据上滑动池化窗口的步幅。可以是单个整数,表示高度和宽度上的相同步幅,也可以是一个元组 (h, w),分别表示高度和宽度上的步幅。默认值是 kernel_size。

padding:

在输入的每一侧添加零值,以控制池化操作的形状。可以是单个整数,表示在高度和宽度上的相同填充量,也可以是一个元组 (h, w),分别表示在高度和宽度上的填充量。默认值是0,表示不填充。

dilation:

控制窗口元素之间的间距,即在输入上采样时的步幅。可以是单个整数或元组。默认值是1,表示不使用空洞卷积。

return_indices:

如果设置为 True,则返回输出的最大值的索引,这对于后面的最大池化反向传播(如nn.MaxUnpool2d)很有用。默认值是 False。

ceil_mode:

如果设置为 True,则使用向上取整来计算输出大小。默认值是 False。

count_include_pad:

在计算平均池化时,控制是否包括填充值。如果设置为 True,则包括填充值,如果设置为 False,则不包括填充值。默认值是 True。

可视化过程:
该图片应用其他作者

该图片应用其他作者

例如:

import numpy as np

def max_pooling(input_data, pool_size=(2, 2), strides=(2, 2)):
    # 获取输入数据的尺寸
    input_height, input_width = input_data.shape
    
    # 获取池化窗口大小和步长
    pool_height, pool_width = pool_size
    stride_height, stride_width = strides
    
    # 计算池化后的输出尺寸
    output_height = (input_height - pool_height) // stride_height + 1
    output_width = (input_width - pool_width) // stride_width + 1
    
    # 初始化池化后的输出特征图
    output_data = np.zeros((output_height, output_width))
    
    # 在输入数据上滑动池化窗口并执行最大池化
    for i in range(output_height):
        for j in range(output_width):
            # 计算当前窗口在输入数据上的位置
            start_i = i * stride_height
            start_j = j * stride_width
            end_i = start_i + pool_height
            end_j = start_j + pool_width
            
            # 在当前窗口内找到最大值并保存到输出特征图中
            output_data[i, j] = np.max(input_data[start_i:end_i, start_j:end_j])
    
    return output_data

# 测试最大池化函数
input_data = np.array([[1, 2, 1, 0],
                       [0, 1, 2, 3],
                       [2, 3, 1, 2],
                       [0, 0, 1, 1]])

output_data = max_pooling(input_data, pool_size=(2, 2), strides=(2, 2))
print("Input Data:")
print(input_data)
print("\nOutput Data after Max Pooling:")
print(output_data)
Input Data:
[[1 2 1 0]
 [0 1 2 3]
 [2 3 1 2]
 [0 0 1 1]]

Output Data after Max Pooling:
[[2. 3.]
 [3. 2.]]

线性变换

线性变换通常由以下两个步骤组成:

  1. 权重矩阵乘法(线性映射):

    输入数据向量被乘以一个权重矩阵,这个矩阵定义了从输入特征到输出特征的映射关系。这个步骤对输入数据进行了一种线性转换,并将其映射到另一个向量空间。

  2. 偏置加法:

    在乘以权重矩阵后,通常会将一个偏置向量添加到结果中。这个偏置向量可以看作是一个对输入数据的平移操作,它允许模型在没有输入时也能够输出非零值。

在数学符号中,线性变换可以表示为以下形式:

y = W ⋅ x + b \mathbf{y} = \mathbf{W} \cdot \mathbf{x} + \mathbf{b} y=Wx+b

其中:

  • ( x x x ) 是输入数据向量。
  • ( W W W ) 是权重矩阵。
  • ( b b b ) 是偏置向量。
  • ( y y y ) 是输出数据向量。

在神经网络中,这种线性变换通常用于隐藏层和输出层之间的连接。隐藏层中的每个神经元通常都包含一个线性变换,其输出被传递给激活函数以引入非线性特性。

下面我们就来创建一个CNN模型

# 创建CNN模型并将其移动到GPU上
class CNNModel(nn.Module):
    def __init__(self):
        super(CNNModel, self).__init__()
        # 输入图像通道数为 3,输出通道数为 16,卷积核大小为 3x3,padding为 1(保持特征图大小不变)
        self.conv1 = nn.Conv2d(3, 16, kernel_size=3, padding=1)
        self.relu = nn.ReLU()  # ReLU激活函数
        self.pool = nn.MaxPool2d(2, 2)  # 最大池化层,2x2大小,步长为2
        # 输入通道数为 16,输出通道数为 32,卷积核大小为 3x3,padding为 1(保持特征图大小不变)
        self.conv2 = nn.Conv2d(16, 32, kernel_size=3, padding=1)
        # 全连接层,输入维度为 32 * 56 * 56(假设输入为 224x224,经过两次池化大小变为 56x56)
        # 输出维度为 128
        self.fc1 = nn.Linear(32 * 56 * 56, 128)
        # 最终输出层,输出维度为 3(假设有三个类别)
        self.fc2 = nn.Linear(128, 3)  # 3个类别

    def forward(self, x):
        # 前向传播过程
        x = self.pool(self.relu(self.conv1(x)))  # 第一个卷积层,ReLU,池化
        x = self.pool(self.relu(self.conv2(x)))  # 第二个卷积层,ReLU,池化
        x = x.view(-1, 32 * 56 * 56)  # 展平特征图
        x = self.relu(self.fc1(x))  # 全连接层,ReLU
        x = self.fc2(x)  # 输出层
        return x

# 实例化模型
model = CNNModel().to('cuda')
print(model)

输出结果

CNNModel(
  (conv1): Conv2d(3, 16, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
  (relu): ReLU()
  (pool): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
  (conv2): Conv2d(16, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
  (fc1): Linear(in_features=100352, out_features=128, bias=True)
  (fc2): Linear(in_features=128, out_features=3, bias=True)
)

交叉熵损失

在PyTorch中,nn.CrossEntropyLoss() 是一个用于计算交叉熵损失的损失函数。通常用于多分类问题,特别是当目标标签是以整数形式给出,并且类别之间是互斥的情况下。

交叉熵损失的计算公式如下:

loss = − 1 N ∑ i = 1 N ( y i ⋅ log ⁡ ( softmax ( x i ) ) ) \text{loss} = - \frac{1}{N} \sum_{i=1}^{N} \left( y_i \cdot \log(\text{softmax}(x_i)) \right) loss=N1i=1N(yilog(softmax(xi)))

其中:

  • ( N N N) 是样本数量。
  • ( y i y_i yi) 是第 ( i i i) 个样本的真实类别标签。
  • ( x i x_i xi) 是第 ( i i i ) 个样本的模型预测输出。
  • ( s o f t m a x ( x i ) {softmax}(x_i) softmax(xi)) 是对模型输出进行 softmax 归一化得到的概率分布。
  • ( l o g log log ) 是自然对数函数。

在使用 nn.CrossEntropyLoss() 时,我们不需要手动对模型输出进行 softmax 操作,因为该函数会在内部自动进行。它接受两个参数:

  • weight:各类别的损失权重,可选参数,默认为 None
  • reduction:指定损失的计算方式,可选参数,默认为 "mean",表示对每个样本的损失取平均值;也可以设置为 "sum",表示对每个样本的损失求和。

在训练神经网络时,通常会将 nn.CrossEntropyLoss() 作为模型的损失函数,将模型的输出和真实标签传递给该函数,然后通过反向传播来更新模型参数,以最小化损失函数。

优化器

optim.Adam() 是PyTorch中Adam优化器的构造函数,用于创建Adam优化器的实例。

  • params:模型的参数,可以通过 model.parameters() 方法获取,或者手动指定要优化的参数列表。
  • lr:学习率(learning rate),用于控制参数更新的步长。
  • betas:用于计算梯度的指数衰减因子,默认值为 (0.9, 0.999)。
  • eps:用于数值稳定性的小值,防止除零错误,默认值为 1e-8。
  • weight_decay:权重衰减(L2 正则化)的系数,默认值为 0,表示不应用权重衰减。
    现在我们就通过定义损失值和优化器来对CNN模型进行训练
# 定义损失函数和优化器
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)

# 训练模型
epochs = 20
train_losses = []
for epoch in range(epochs):
    model.train()
    running_loss = 0.0
    for i, data in enumerate(train_loader, 0):
        inputs, labels = data
        inputs, labels = inputs.to('cuda'), labels.to('cuda')
        optimizer.zero_grad()
        outputs = model(inputs)
        loss = criterion(outputs, labels)
        loss.backward()
        optimizer.step()
        running_loss += loss.item()
        if i % 100 == 99:  # 每100个batch打印一次损失
            print(f"Epoch [{epoch + 1}/{epochs}], "
                  f"Batch [{i + 1}/{len(train_loader)}], "
                  f"Loss: {running_loss / 100:.4f}")
            train_losses.append(running_loss / 100)
            running_loss = 0.0

# 在一部分测试数据上评估模型
model.eval()
correct = 0
total = 0

with torch.no_grad():
    for data in test_loader:
        images, labels = data
        images, labels = images.to('cuda'), labels.to('cuda')
        outputs = model(images)
        _, predicted = torch.max(outputs.data, 1)
        total += labels.size(0)
        correct += (predicted == labels).sum().item()

accuracy = correct / total
print(f"Accuracy on test set: {100 * accuracy:.2f}%")
Accuracy on test set: 71.00%

下面我们将损失值利用matplotlib可视化

可视化

预测可视化

import matplotlib.pyplot as plt
# 设置 Matplotlib 样式(可选)
plt.style.use('seaborn-darkgrid')

# 创建一个新的图表并设置尺寸
plt.figure(figsize=(8, 6))

# 绘制训练损失值
plt.plot(train_losses, label='Training Loss', color='blue', linestyle='-')
plt.xlabel('Iterations', fontsize=12)
plt.ylabel('Loss', fontsize=12)
plt.title('Training Loss over iterations', fontsize=14)
plt.legend()

# 添加网格线
plt.grid(True)

# 显示图表
plt.tight_layout()  # 调整布局,防止标签被裁剪
plt.show()

输出结果
在这里插入图片描述

损失值可视化

下面我们将预测可视化前8个样本并且在预测结果的可视化中显示预测的类别以及真实的类别,而不直接显示原始预测值(0、1 等)和输出标签值。

# 替换 class_names 为你数据集中对应的类别名称列表
class_names = ['calling', 'normal', 'smoking']

# 使用模型预测并可视化结果
with torch.no_grad():
    for i, data in enumerate(test_loader):
        images, labels = data
        images, labels = images.to('cuda'), labels.to('cuda')  # 如果使用GPU,请将数据移到GPU

        # 获取模型预测
        outputs = model(images)
        _, predicted = torch.max(outputs, 1)  # 获取输出张量中最大值的索引

        # 可视化预测结果
        # 可视化前 10 个样本
        if i == 0:
            plt.figure(figsize=(12, 6))
            for j in range(min(10, len(images))):  # 可视化前 10 个样本
                plt.subplot(2, 4, j + 1)
                img = images[j].cpu().numpy().transpose((1, 2, 0))  # 将图像转换为 NumPy 格式,并转置维度
                img = np.clip(img * 0.5 + 0.5, 0, 1)  # 恢复图像的范围
                plt.imshow(img)
                # 显示预测结果和真实结果的类别名称
                predicted_class = class_names[predicted[j].item()]
                true_class = class_names[labels[j].item()]
                plt.title(f'预测: {predicted_class}\n真实: {true_class}')
                plt.axis('off')
            plt.tight_layout()  # 自动调整子图之间的间距
            plt.show()
            break

输出结果
请添加图片描述
在输出结果中我们可以发现有两张图片CNN模型识别不了(smoking与calling)其中的原因可能为:

  • 特征相似性:吸烟者和打电话者可能在特征上相似,导致模型难以区分。
  • 训练不充分:模型可能没有足够的训练迭代次数或训练数据质量不足。

作者条件有限,有兴趣的小伙伴可以尝试提高训练迭代次数。对于有宝贵模型修改意见的小伙伴可以留言或者私信作者。

其中所用到的数据来自与阿里云数据集(Smoking and Calling Image Dataset 吸烟打电话行为图片数据集

感谢您的关注和支持!

如果您喜欢这个项目,希望各位分享给其他人!如果您发现了Bug或者有任何改进建议,请在评论中上提出修改建议,我们会尽快处理。

再次感谢您的支持!

特别鸣谢所有为项目做出贡献的人!

  • 21
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值