深度学习 2 / CNN 神经网络

正则化

  • 在设计机器学习算法时希望在新样本上的泛化能力强。许多机器学习算法都采用相关的策略来减小测试误差,这些策略被统称为正则化

  • 神经网络强大的表示能力经常遇到过拟合,所以需要使用不同形式的正则化策略

  • 目前在深度学习中使用较多的策略有范数惩罚,DropOut,特殊的网络层等,接下来我们对其进行详细的介绍

  • 防止模型过拟合(训练集效果好,测试集效果差),提高模型泛化能力

  • 一种防止过拟合,提高模型泛化能力的策略

  • L1 正则:需要通过手动写代码实现

  • L2 正则:SGD(weight_decay=)

  • dropout

  • BN

dropout

在训练深层神经网络时,由于模型参数较多,在数据量不足的情况下,很容易过拟合。Dropout(中文翻译成随机失活)是一个简单有效的正则化方法。

  • 在训练过程中,Dropout的实现是让神经元以超参数p(丢弃概率)的概率停止工作或者激活被置为0,未被置为0的进行缩放,缩放比例为1/(1-p)。训练过程可以认为是对完整的神经网络的一些子集进行训练,每次基于输入数据只更新子网络的参数
  • 在实际应用中,Dropout参数p的概率通常取值在0.2到0.5之间
    • 对于较小的模型或较复杂的任务,丢弃率可以选择0.3或更小
    • 对于非常深的网络,较大的丢弃率(如0.5或0.6)可能会有效防止过拟合
    • 实际应用中,通常会在全连接层(激活函数后)之后添加Dropout层
  • 在测试过程中,随机失活不起作用
    • 在测试阶段,使用所有的神经元进行预测,以获得更稳定的结果
    • 直接使用训练好的模型进行测试,由于所有的神经元都参与计算,输出的期望值会比训练阶段高。测试阶段的期望输出是 Ex_test = x
    • 测试/推理模式:==model.eval()==
  • 缩放的必要性
    • 在训练阶段,将参与计算的神经元的输出除以(1-p)
    • 经过Dropout后的期望输出变为 Ex_dropout = (1-p) \* x / (1-p) = x,与测试阶段的期望输出一致
    • 训练模型:==model.train()==
py 复制代码
import torch
from torch import nn

# TODO:1-创建隐藏层输出结果
# float() 转换为浮点类型的张量
t1 = torch.randint(0, 10, (1, 4)).float()
print(t1)

# TODO:2.进行下一步加权求和计算
# 创建一个有 4个输入神经元、4个输出神经元的全连接层
linear1 = nn.Linear(4, 4)
l1 = linear1(t1)
print(t1)

# TODO:3.进行激活函数计算
output = torch.relu(l1)
print(output)

# TODO:4.对激活函数进行 dropout 处理 训练阶段
# p:失火概率
dropout = nn.Dropout(p=0.4)
d1 = dropout(output)
print(d1)

每次执行结果,都不一样,因此可以防止过拟合。

总结

  • 让神经元以 P 概率随机死亡,每批次样本训练模型时,死亡的神经元都是随机的,防止预测结果受某个神经元影响
  • P 概率 -> 0.2, 0.5,简单模型概率低,复杂模型概率高
  • 不失活的神经元计算结果除以(1 - p),让训练时输出结果和测试时(dropout不生效)结果一致
    • 训练模型:model.train()
    • 测试模型:model.eval()

批量归一化

在神经网络的训练过程中,流经网络的数据都是一个batch,每个batch之间的数据分布变化非常剧烈,==这就使得网络参数频繁的进行大的调整以适应流经网络的不同分布的数据==,给模型训练带来非常大的不稳定性,使得模型难以收敛。如果我们对每一个batch的数据进行标准化之后,数据分布就变得稳定,参数的梯度变化也变得稳定,有助于加快模型的收敛。

通过标准化每一层的输入,使其均值接近0,方差接近1,从而加速训练并提高泛化能力。

先对数据标准化,再对数据重构(缩放+平移),写成公式如下所示:

λ和β是可学习的参数,它相当于对标准化后的值做了一个线性变换λ为系数,β为偏置;

eps 通常指为 1e-5,避免分母为 0;

E(x) 表示变量的均值;

Var(x) 表示变量的方差;

批量归一化的步骤如下:

  1. 计算均值和方差 :对于每个神经元(即每一层的输入特征),计算该特征在一个小批量(batch)上的均值 μB μ_B μB 和方差 σB2 \sigma_B^2 σB2,它们的计算公式如下:

    μB=1m ∑i=1m xi μ_B=\frac{1}{m} \sum_{i=1}^{m} x_i μB=m1∑i=1mxi

    σB2=1m ∑i=1m (xi−μB)2 σ_B^2=\frac{1}{m} \sum_{i=1}^{m} (x_i - \mu_B)^2 σB2=m1∑i=1m(xi−μB)2

    其中 xi x_i xi 表示小批量中的第 ii i 个样本, mm m 是小批量的样本数量。

  2. 标准化:然后,对每个样本的输入进行标准化,得到归一化的输出:

    x^i = xi−μB σB2+ϵ \hat{x}_i = \frac{x_i - \mu_B}{\sqrt{\sigma_B^2 + \epsilon}} x^i=σB2+ϵ xi−μB

    其中, ϵϵ ϵ 是一个小常数,用来避免除以零的情况。

  3. 缩放和平移 :为了让网络能够恢复其学习能力,BN 层引入了两个可训练的参数 γγ γ 和 ββ β,分别用于缩放平移

    yi=γ x^i +β y_i = \gamma \hat{x}_i + β yi=γx^i+β

    其中, γγ γ 和 ββ β 是可学习的参数,通过 γ 和 β,BN 层不再是简单的将每一层输入强行变为标准正态分布,而是允许网络学习更适合于该层的输入分布;规范化操作会丢失原始输入的一些信息,而 γγ γ 和 ββ β 可以弥补这种信息损失。

批量归一化的作用:

  • **减少内部协方差偏移:**通过对每层的输入进行标准化,减少了输入数据分布的变化,从而加速了训练过程,并使得网络在训练过程中更加稳定。

  • 加速训练:

    • 在没有批量归一化的情况下,神经网络的训练通常会很慢,尤其是深度网络。因为在每层的训练过程中,输入数据的分布(特别是前几层)会不断变化,这会导致网络学习速度缓慢。
    • 批量归一化通过确保每层的输入数据在训练时分布稳定,有效减少了这种变化,从而加速了训练过程。
  • **起到正则化作用:**批量归一化可以视作一种正则化方法,因为它引入了对训练样本的噪声(不同批次的统计信息不同,批次较小的均值和方差估计会更加不准确),使得模型不容易依赖特定的输入特征,从而起到一定的正则化效果,减少了对其他正则化技术(如Dropout)的需求。

  • **提升泛化能力:**由于其正则化效果,批量归一化能帮助网络在测试集上取得更好的性能。

==批量归一化层在计算机视觉领域使用较多==

Batch Normalization 的使用步骤:

  1. 在网络层后添加 BN 层:
    • 通常,BN 层会添加在卷积层 (Conv2d) 或全连接层 (Linear) 之后,激活函数之前
    • 例如:Conv2d -> BN -> ReLU 或者 Linear -> BN -> ReLU。
  2. 训练时: ==model.train()==
    • BN 层会计算当前批次的均值 μμ μ 和方差 σ2σ² σ2。
    • 然后,利用这两个统计量对当前批次的数据进行规范化。
    • 规范化后的数据会被缩放 γγ γ 和平移 ββ β。
    • 同时,BN 层还会维护一个全局均值全局方差的移动平均值,用于推理阶段。
  3. 推理时: ==model.eval()==
    • 推理时,不会再使用当前批次的均值和方差,而是使用训练阶段计算的全局均值全局方差
    • 同样,规范化后的数据会被缩放 γγ γ 和平移 ββ β。

总结

  1. 计算每个 batch 样本的均值和标准差,利用均值和标准差计算出标准化的值
  2. 每个 batch 的均值和标准差都不一样,会引入噪声样本数据,降低训练模型的效果(防止过拟合)。
  3. 引入两个自学习的 Y 和 β参数,让每层的样本分布不一样(每层的激活函数可以不一样)
  4. 加速模型训练效果,数据分布越均匀,加权求和结果落入到合理区间(导数最大)
  5. 训练时进行标准化, 测试时不进行标准化

代码

py 复制代码
# 正则化:没批样本的均值和方差不一样,引入噪声样本
# 加快模型收;样本标准化后,落入激活函数的合理区间,导数尽可能最大

import torch
import torch.nn as nn

# nn.BatchNorm1d() 处理一维样本,没批样本数最少是 2 个,否则无法计算均值和标准差
# nn.BatchNorm2d() 处理二维样本,图像(每个通道由二维矩阵组成),计算二维矩阵每列均值和标准差
# nn.BatchNorm3d() 处理三维样本,视频

# TODO:1.创建图像样本数据集 2 个通道,每个通道 3 * 4列特征图,卷积层处理的特征图样本
# 数据集只有一张图像,图像是由 2 个通道组成,每个通道由 3 * 4 像素矩阵
input_2d = torch.randn(size=(1,2,3,4))
print('input_2d ->', input_2d)
# TODO:2.创建BN层,标准化 -> 一定是在激活函数前进行标准化
# num_features:输入样本的通道数
# eps:小常数,避免除 0
# momentum:指数移动加权平均值
# affine:默认True,引入可学习的伽马(γ)和贝塔(β)参数
bn2d = nn.BatchNorm2d(num_features=2, eps=1e-5, momentum=0.1, affine=True)
output_2d = bn2d(input_2d)
print('output_2d', output_2d)

代码输出结果

text 复制代码
input_2d -> tensor([[[[-0.1370,  0.2362,  0.4925, -0.6562],
          [-0.2663, -1.6501,  0.1457, -1.7754],
          [-1.0152, -1.3381,  1.1064,  0.7739]],

         [[ 3.0217,  0.8352,  0.9794,  0.8549],
          [ 1.1417, -0.8693,  0.9716,  1.3616],
          [ 1.8545, -0.1974,  0.8419,  0.4935]]]])
output_2d tensor([[[[ 0.2231,  0.6324,  0.9137, -0.3466],
          [ 0.0812, -1.4370,  0.5332, -1.5745],
          [-0.7404, -1.0947,  1.5872,  1.2224]],

         [[ 2.2587, -0.1145,  0.0419, -0.0933],
          [ 0.2181, -1.9647,  0.0335,  0.4567],
          [ 0.9918, -1.2354, -0.1073, -0.4855]]]],
       grad_fn=<NativeBatchNormBackward0>)

案例:手机分类

模型构建

py 复制代码
import numpy as np
import pandas as pd
import torch
from sklearn.model_selection import train_test_split
from torch import nn, optim
from torch.utils.data import TensorDataset, DataLoader
from torchsummary import summary

# TODO:1.构建数据集
data = pd.read_csv(
    r'E:\BaiduNetdiskDownload\Heima\25年8月完结 持续更新版本\源码课件软件\05 正课 深度学习-能源之星回归-V6-25年7月版本-10天-AI版本\day09\03-代码\data\手机价格预测.csv')
# print(data.head())
# print(data.shape)
# TODO:2.获取X特征数据集和 y 目标列数据集
x = data.drop(columns=['price_range'])
# 将特征列转换为浮点类型
x = x.astype(np.float32)

y = data.price_range
# print(x.head())
# print(y.head())

# TODO:3.数据集分隔 8:2
x_train, x_test, y_train, y_test = train_test_split(x, y, train_size=0.8, random_state=42)
# TODO:4.数据集转换成张量数据集
# x_train y_train 类型是 df 的, 不能直接转换张量数据集
# 需要先获取为 numpy再转换张量数据集
# 训练数据集
train_dataset = TensorDataset(torch.tensor(x_train.values), torch.tensor(y_train.values))
# 测试数据集
test_dataset = TensorDataset(torch.tensor(x_test.values), torch.tensor(y_test.values))

# 特征数,类别数
feature_num = x.shape[1]
class_num = len(np.unique(y))


# TODO:5.构建神经网络分类模型
class PhonePriceModel(nn.Module):
    # 构建神经网络
    def __init__(self, input_dim, output_dim):
        super().__init__()
        # 第一层隐藏层
        self.linear1 = nn.Linear(input_dim, 128)
        # 第二层隐藏层
        self.linear2 = nn.Linear(128, 256)
        # 输出层
        self.output = nn.Linear(256, output_dim)

    # 前向传播方法 forward()
    def forward(self, x):
        # 第一层隐藏层计算
        x = torch.relu(self.linear1(x))
        # 第二层隐藏层计算
        x = torch.relu(self.linear2(x))
        # 输出层计算
        # 没有进行 softmax 激活计算,后续创建损失函数时,CrossEntropyLoss = softmax + 损失计算。
        output = self.output(x)
        return output


# 创建模型对象--测试
# model = PhonePriceModel(input_dim=feature_num, output_dim=class_num)
# 计算模型参数,input_size = 输入层的样本
# summary(model, input_size=(16, feature_num))


# ----------------------------------------------------------------
#         Layer (type)               Output Shape         Param #
# ================================================================
#             Linear-1              [-1, 16, 128]           2,688
#             Linear-2              [-1, 16, 256]          33,024
#             Linear-3                [-1, 16, 4]           1,028
# ================================================================
# Total params: 36,740(共三万多参数)
# Trainable params: 36,740
# Non-trainable params: 0
# ----------------------------------------------------------------
# Input size (MB): 0.00
# Forward/backward pass size (MB): 0.05
# Params size (MB): 0.14
# Estimated Total Size (MB): 0.19
# ----------------------------------------------------------------

# TODO:6.模型训练
def train():
    # 创建数据加载器,批量训练
    loader = DataLoader(dataset=train_dataset, batch_size=8, shuffle=True)
    # 创建神经网络分类模型对象
    model = PhonePriceModel(input_dim=feature_num, output_dim=class_num)
    # 创建损失函数对象,多分类交叉熵损失函数 = softmax+损失计算
    criterion = nn.CrossEntropyLoss()
    # 创建优化器对象 SGD
    optimizer = optim.SGD(params=model.parameters(), lr=1e-3)
    # 模型训练 min-batch 随机梯度下降
    num_epoch = 50
    for epoch in range(num_epoch):
        # 定义变量统计每次训练的损失值,训练batch数
        total_loss = 0.0
        batch_num = 0
        # 批次训练
        for x, y in loader:
            # 切换模型形式
            model.train()
            # 模型预测 y 预测值
            y_pred = model(x)
            # 计算损失值
            loss = criterion(y_pred, y)
            # 梯度清零
            optimizer.zero_grad()
            # 计算梯度
            loss.backward()
            # 更新模型参数 梯度下降法
            optimizer.step()
            # 统计每次训练的所有 batch 的平均损失值和batch数
            total_loss += loss.item() # 获取标量张量
            batch_num += 1

        avg_loss = total_loss / batch_num
        print(f"epoch:{epoch + 1}, loss:{avg_loss}")
    # 模型保存,将模型参数保存到字典,再将字典保存到文件
    torch.save(model.state_dict(), './phone_price_model.pth')

train()

模型评估

py 复制代码
# TODO:7.模型评估
# TODO:8.创建神经网络分类模型对象
model = PhonePriceModel(input_dim=feature_num, output_dim=class_num)
# TODO:9.加载训练模型的参数字典
model.load_state_dict(torch.load(f='./phone_price_model.pth'))
# TODO:10.创建测试集数据加载器
# shuffle 不打乱,预测,不是训练
test_loader = DataLoader(dataset=test_dataset, batch_size=8, shuffle=False)
# TODO:11.定义变量,初始值为 0,统计预测正确的样本个数
correct = 0
# TODO:12.按 batch 进行预测
for x, y in test_loader:
    print(y)
    # 切换模型模式为预测模式
    model.eval()
    # 模型预测 y 预测值 -> 输出层的加权求和值
    output = model(x)
    print(output)
    # 根据加权求和值得到类别,argmax() 获取最大值对应的下标就是类别 y -> 0,1,2,3
    # dim=1:一行一行处理,一个样本一个类别
    y_pred = output.argmax(dim=1)
    print(y_pred)
    # 统计预测正确的样本个数
    print(y_pred == y)
    # 对布尔值进行求和,True == 1
    correct += (y_pred == y).sum()
# 计算准确率
acc = correct / len(test_dataset)
print(f'Acc:{acc}')

网络性能优化

  • 输入层数据进行标准化
  • 神经网络层数增加, 神经元个数增加
  • 梯度下降优化方法由SGD调整为Aam
  • 学习率由1e-3调整为1e-4
  • 正则化
  • 增加批量归一化层
  • 增加训练轮数
  • ...

进行下如下调整:

  1. 优化方法由 SGD 调整为 Adam
  2. 学习率由 1e-3 调整为 1e-4
  3. 对数据进行标准化
  4. 增加网络深度, 即: 增加网络参数量
python 复制代码
import torch
import torch.nn as nn
import pandas as pd
from sklearn.model_selection import train_test_split
from torch.utils.data import TensorDataset
from torch.utils.data import DataLoader
import torch.optim as optim
import numpy as np
import time
from sklearn.preprocessing import StandardScaler


# 构建数据集
def create_dataset():
	# 使用pandas读取数据
	data = pd.read_csv('../data/手机价格预测.csv')
	# 特征值和目标值
	x, y = data.iloc[:, :-1], data.iloc[:, -1]
	# 类型转换:特征值,目标值
	x = x.astype(np.float32)
	y = y.astype(np.int64)
	# 数据集划分
	x_train, x_valid, y_train, y_valid = train_test_split(x, y, train_size=0.8, random_state=88, stratify=y)
	# 优化①:数据标准化
	transfer = StandardScaler()
	x_train = transfer.fit_transform(x_train)
	x_valid = transfer.transform(x_valid)
	# 构建数据集,转换为pytorch的形式
	train_dataset = TensorDataset(torch.from_numpy(x_train), torch.tensor(y_train.values))
	valid_dataset = TensorDataset(torch.from_numpy(x_valid), torch.tensor(y_valid.values))
	# 返回结果
	return train_dataset, valid_dataset, x_train.shape[1], len(np.unique(y))


# 构建网络模型
class PhonePriceModel(nn.Module):

	def __init__(self, input_dim, output_dim):
		super(PhonePriceModel, self).__init__()
		# 优化②:增加网络深度
		# 1. 第一层: 输入为维度为 20, 输出维度为: 128
		self.linear1 = nn.Linear(input_dim, 128)
		# 2. 第二层: 输入为维度为 128, 输出维度为: 256
		self.linear2 = nn.Linear(128, 256)
		# 3. 第三层: 输入为维度为 256, 输出维度为: 512
		self.linear3 = nn.Linear(256, 512)
		# 4. 第三层: 输入为维度为 512, 输出维度为: 128
		self.linear4 = nn.Linear(512, 128)
		# 5. 第三层: 输入为维度为 128, 输出维度为: 4
		self.linear5 = nn.Linear(128, output_dim)

	def forward(self, x):
		# 前向传播过程
		x = torch.relu(self.linear1(x))
		x = torch.relu(self.linear2(x))
		x = torch.relu(self.linear3(x))
		x = torch.relu(self.linear4(x))
		# 后续CrossEntropyLoss损失函数中包含softmax过程, 所以当前步骤不进行softmax操作
		output = self.linear5(x)
		# 获取数据结果
		return output


# 编写训练函数
def train(train_dataset, input_dim, class_num):
	# 固定随机数种子
	torch.manual_seed(0)
	# 初始化数据加载器
	dataloader = DataLoader(train_dataset, shuffle=True, batch_size=8)
	# 初始化模型
	model = PhonePriceModel(input_dim, class_num)
	# 损失函数 CrossEntropyLoss = softmax + 损失计算
	criterion = nn.CrossEntropyLoss()
	# 优化③:使用Adam优化方法, 优化④:学习率变为1e-4
	optimizer = optim.Adam(model.parameters(), lr=1e-4)
	# 遍历每个轮次的数据
	num_epoch = 50
	for epoch_idx in range(num_epoch):
		# 训练时间
		start = time.time()
		# 计算损失
		total_loss = 0.0
		total_num = 0
		# 遍历每个batch数据进行处理
		for x, y in dataloader:
			model.train()
			output = model(x)
			# 计算损失
			loss = criterion(output, y)
			# 梯度清零
			optimizer.zero_grad()
			# 反向传播
			loss.backward()
			# 参数更新
			optimizer.step()
			# 损失计算
			total_num += len(y)
			total_loss += loss.item() * len(y)
		# 打印损失变换结果
		print('epoch: %4s loss: %.2f, time: %.2fs' %
			  (epoch_idx + 1, total_loss / total_num, time.time() - start))
	# 模型保存
	torch.save(model.state_dict(), '../model/phone-price-model2.pth')


def test(valid_dataset, input_dim, class_num):
	# 加载模型和训练好的网络参数
	model = PhonePriceModel(input_dim, class_num)
	# load_state_dict:将加载的参数字典应用到模型上
	# load:加载用来保存模型参数的文件
	model.load_state_dict(torch.load('../model/phone-price-model2.pth'))
	# 构建加载器
	dataloader = DataLoader(valid_dataset, batch_size=8, shuffle=False)
	# 评估测试集
	correct = 0
	# 遍历测试集中的数据
	for x, y in dataloader:
		# 将其送入网络中
		# model.eval()
		output = model(x)
		# 获取预测类别结果
		y_pred = torch.argmax(output, dim=1)
		# 获取预测正确的个数
		correct += (y_pred == y).sum()
	# 求预测精度
	print('Acc: %.5f' % (correct / len(valid_dataset)))


if __name__ == '__main__':
	train_dataset, valid_dataset, input_dim, class_num = create_dataset()
	train(train_dataset, input_dim, class_num)
	test(valid_dataset, input_dim, class_num)

卷积神经网络CNN

图像基础知识

图像基本概念

图像是人类视觉的基础,是自然景物的客观反映,是人类认识世界和人类本身 的重要源泉。图是物体反射或透射光的分布,像是人的视觉系统所接受的图在人脑中所形成的印象或认识照片、绘画、剪贴画、地图、书法作品、手写汉字、传真、卫星云图、影视画面、X光片、脑电图、心电图等都是图像。

在计算机中,按照颜色和灰度的多少可以将图像分为四种基本类型。

  • 二值图像

    一幅二值图像的二维矩阵仅由0、1两个值构成,"0"代表黑色,"1"代白色。由于每一像素(矩阵中每一元素)取值仅有0、1两种可能,所以计算机中二值图像的数据类型通常为1个二进制位。二值图像通常用于文字、线条图的扫描识别(OCR)和掩膜图像的存储。

  • 灰度图像

    灰度图像矩阵元素的取值范围通常为0,255 。因此其数据类型一般为8位无符号整数的(int8) ,这就是人们经常提到的256灰度图像。0 表示纯黑色,255表示纯白色,中间的数字从小到大表示由黑到白的过渡色。二值图像可以看成是灰度图像的一个特例。

  • 索引图像

    索引图像的文件结构比较复杂,除了存放图像的二维矩阵 外,还包括一个称之为颜色索引矩阵MAP的二维数组 。MAP的大小由存放图像的矩阵元素值域决定,如矩阵元素值域为0,255,则MAP矩阵的大小为256Ⅹ3,用MAP=RGB表示MAP中每一行的三个元素分别指定该行对应颜色的红、绿、蓝单色值,MAP中每一行对应图像矩阵像素的一个灰度值,如某一像素的灰度值为64,则该像素就与MAP中的第64行建立了映射关系,该像素在屏幕上的实际颜色由第64行的RGB组合决定。也就是说,图像在屏幕上显示时,每一像素的颜色由存放在矩阵中该像素的灰度值作为索引通过检索颜色索引矩阵MAP得到。

  • 真彩色RGB图像

    RGB图像与索引图像一样都可以用来表示彩色图像。与索引图像一样,它分别用红(R)、绿(G)、蓝(B)三原色的组合来表示每个像素的颜色。但与索引图像不同的是,RGB图像每一个像素的颜色值(由RGB三原色表示)直接存放在图像矩阵中 ,由于每一像素的颜色需由R、G、B三个分量来表示,M、N分别表示图像的行列数,三个M x N的二维矩阵分别表示各个像素的R、G、B三个颜色分量 。RGB图像的数据类型一般为8位无符号整形。注意:通道的顺序是 BGR 而不是 RGB

图像类型 通道数 像素值范围 主要特点 常见用途
二值图像 1通道 0 或 1 每个像素只有黑与白两种值 形态学操作、二值化、轮廓检测
灰度图像 1通道 0 到 255 每个像素表示灰度(亮度) 图像预处理、物体检测、人脸识别
索引图像 1通道 0 到 255(索引) 像素值为颜色表的索引,颜色表决定实际颜色 存储压缩、较少颜色的图像表示
RGB图像 3通道(R、G、B) 0 到 255 每个像素由红、绿、蓝三个通道组成 普通彩色图像显示、图像处理与分析

简单的讲:图像是由像素点组成的,每个像素点的取值范围为: 0, 255 。像素值越接近于0,颜色越暗,接近于黑色;像素值越接近于255,颜色越亮,接近于白色。

在深度学习中,我们使用的图像大多是彩色图,彩色图由RGB3个通道组成,如下图所示:

总结

计算机中图像分类表示

  • 二值图像 1通道(1个二维矩阵) 像素值:0或1
  • 灰度图像 1通道 像素值:0-255
  • 索引图像 1通道 索引值->RGB二维矩阵行下标 彩色图像 像素值:0-255
  • RGB真彩色图像(最常用) 3通道(3个二维矩阵) R G B三个二维矩阵 像素值:0-255

图像加载和生成案例

py 复制代码
import matplotlib.pyplot as plt
import numpy as np
import torch

# 创建一个全黑图像
# H W C:200,200,3
# 高 宽 通道
img1 = np.zeros(shape=(200, 200, 3))
print(img1)
print(img1.shape)

plt.imshow(img1)
plt.show()

# 全白图片
img2 = torch.full(size=(200, 200, 3), fill_value=255)
print(img2)
print(img2.shape)

plt.imshow(img2)
plt.show()

# 加载一张图片
img3 = plt.imread('3.png')
print(img3)
print(img3.shape)

# 保存图片
plt.imsave('ve.png', img3)

plt.imshow(img3)
plt.show()

卷积神经网络(CNN)概述

什么是卷积神经网络

卷积神经网络是深度学习在计算机视觉领域的突破性成果,专门用于处理图像、视频、语音等数据的神经网络

在计算机视觉领域, 往往我们输入的图像都很大,使用全连接网络的话,计算的代价较高。另外图像也很难保留原有的特征,导致图像处理的准确率不高。

卷积神经网络(Convolutional Neural Network)是含有卷积层的神经网络 。卷积层的作用就是用来自动学习、提取图像的特征

CNN网络主要由三部分构成:卷积层、池化层和全连接层构成:

(1)卷积层负责提取图像中的局部特征

(2)池化层用来大幅降低参数量级(降维)

(3)全连接层类似人工神经网络的部分,用来输出想要的结果

上图中CNN要做的事情是:给定一张图片,是车还是马未知,是什么车也未知,现在需要模型判断这张图片里具体是一个什么东西,总之输出一个结果:如果是车 那是什么车

  • 最左边是
    • 数据输入层:对数据做一些处理,比如去均值(各维度都减对应维度的均值,使得输入数据各个维度都中心化为0,避免数据过多偏差,影响训练效果)、归一化(把所有的数据都归一到同样的范围)、PCA等等。CNN只对训练集做"去均值"这一步。
  • 中间是
    • 卷积层(CONV):线性乘积求和,提取图像中的局部特征
    • 激励层(RELU):ReLU激活函数,输入数据转换成输出数据
    • 池化层(POOL):取区域平均值或最大值,大幅降低参数量级(降维)
  • 最右边是
    • 全连接层(FC):接收二维数据集,输出CNN模型预测结果

卷积神经网络应用

图像分类:最常见的应用,例如识别图片中的物体类别

目标检测:检测图像中物体的位置和类别

图像分割:将图像分成多个区域,用于语义分割

人脸识别:识别图像中的人脸

医学图像分析:用于检测医学图像中的异常(如癌症检测、骨折检测等)

自动驾驶:用于识别交通标志、车辆、行人

CNN中的经典算法/网络架构

LeNet-5:作为最早的CNN架构之一,证明了CNN在图像识别任务上的有效性,为后续的CNN发展奠定了基础

  • 卷积层: 提取图像的边缘、角点等基本特征
  • 池化层 (子采样层): 降低特征图的维度,减少计算量,并提高模型对输入图像微小变化的鲁棒性
  • 全连接层: 将卷积层和池化层提取的特征进行组合,用于最终的分类

**AlexNet:**显著提升了ImageNet图像分类的准确率,证明了深度学习在计算机视觉领域的潜力,并推动了深度学习的快速发展

  • 卷积层: 使用更大的卷积核和更多的卷积核,提取更丰富的图像特征
  • ReLU激活函数: 加速训练过程,并提高模型的性能
  • 最大池化层: 降低特征图的维度
  • Dropout层: 防止过拟合
  • 全连接层: 用于最终的分类

VGGNet:探索了网络深度对性能的影响,证明了更深的网络可以提取更抽象和更具表达力的特征

  • 卷积层: 使用更小的卷积核 (3x3),并堆叠多个卷积层,增加了网络的深度,提取更复杂的特征
  • 最大池化层: 降低特征图的维度
  • 全连接层: 用于最终的分类

GoogLeNet (Inception):提出了 Inception 模块,在提高性能的同时减少了计算量,为后续的网络架构设计提供了新的思路

  • Inception 模块: 并行使用不同大小的卷积核和池化操作,然后将它们的输出连接起来,增加了网络的宽度,提高了网络的效率

ResNet:解决了深度网络训练困难的问题,使得可以训练更深的网络,从而显著提高了模型的性能

  • 残差块 (Residual Block): 引入跳跃连接 (Shortcut Connection),允许梯度直接反向传播到浅层,解决了深度网络的梯度消失问题,使得训练非常深的网络成为可能

DenseNet

  • 密集块 (Dense Block): 将每一层都与之前的所有层连接,特征重用更加充分,进一步提高了网络的性能和参数效率
  • DenseNet通过密集连接(Dense Connectivity)在网络中各层之间建立了直接的连接,即每一层都接收前面所有层的输出作为输入。这种设计增强了特征传递和梯度流动,避免了梯度消失问题,并提高了信息的利用率

总结

什么是CNN

  • 包含卷积层,池化层以及全连接层的神经网络计算模型
  • 组成
    • 卷积层: 提取图像特征图
    • 池化层: 降维, 减少特征图的特征值, 减少模型参数
    • 全连接层: 进行预测, 只能接受二维数据集, 1个样本就是1维向量
      • 将池化层的特征图(1张图像)转换成一维 200*200*3->120000个特征值

CNN应用场景

  • 图像分类
  • 目标检测
  • 面部解锁
  • 自动驾驶
  • ...

卷积层

卷积层(Convolutional Layer)通过卷积操作提取输入数据中的特征(例如图像中的边缘、纹理、形状等)。

卷积层利用卷积核(滤波器)对输入进行处理,从而生成特征图(feature map),并且每个卷积层能够提取不同层次的特征,从低级特征(如边缘)到高级特征(如物体的形状)。

卷积层的主要作用如下:

  • 特征提取:卷积层的主要作用是从输入图像中提取低级特征(如边缘、角点、纹理等)。通过多个卷积层的堆叠,网络能够逐渐从低级特征到高级特征(如物体的形状、区域等)进行学习。

  • 权重共享:在卷积层中,同一个卷积核在整个输入图像上共享权重,这使得卷积层的参数数量大大减少,减少了计算量并提高了训练效率。

  • 局部连接 :卷积层中的每个神经元仅与输入图像的一个小局部区域相连,这称为局部感受野,这种局部连接方式更符合图像的空间结构,有助于捕捉图像中的局部特征。

  • 空间不变性 :由于卷积操作是局部的并且采用权重共享,卷积层在处理图像时具有平移不变性。也就是说,不论物体出现在图像的哪个位置,卷积层都能有效地检测到这些物体的特征。

卷积计算

  1. input 表示输入的图像

  2. filter 表示卷积核, 也叫做滤波器(滤波矩阵)

    • 一组固定的权重,因为每个神经元的多个权重固定,所以又可以看做一个恒定的滤波器filter
    • 非严格意义上来讲,下图中红框框起来的部分便可以理解为一个滤波器,即带着一组固定权重的神经元。多个滤波器叠加便成了卷积层
    • 一个卷积核就是一个神经元
  1. input 经过 filter 得到输出为最右侧的图像,该图叫做特征图

那么, 它是如何进行计算的呢?卷积运算本质上就是在滤波器和输入数据的局部区域间做点积。

左上角的点计算方法:

按照上面的计算方法可以得到最终的特征图为:

图像上的卷积:

在下图对应的计算过程中,输入是一定区域大小(width*height)的数据,和滤波器filter(带着一组固定权重的神经元)做内积后得到新的二维数据。

具体来说,左边是图像输入,中间部分就是滤波器filter(带着一组固定权重的神经元),不同的滤波器filter会得到不同的输出数据,比如颜色深浅、轮廓。相当于如果想提取图像的不同特征,则用不同的滤波器filter,提取想要的关于图像的特定信息:颜色深浅或轮廓。

小结

卷积计算

卷积计算等同于线性层加权求和计算

  • 通过带有权重的卷积核和图像的特征值进行点乘运算, 得到新特征图上的一个特征值

  • 卷积核/滤波器 -> 带有权重参数的神经元

  • w1x1 + w2x2 + .... + b

    • w1->卷积核一个权重参数
    • x1->特征图的一个特征值(像素点)

padding (填充)操作

通过上面的卷积计算过程,最终的特征图比原始图像小很多,如果想要保持经过卷积后的图像大小不变, 可以在原图周围添加 Padding 来实现。

Padding(填充)操作是一种用于==在输入特征图的边界周围添加额外像素(通常是零)==。

Padding的主要作用:

保持空间维度:如果不使用 padding,每次卷积操作后,特征图的尺寸都会缩小。多次卷积后,特征图会变得非常小,可能会丢失重要的边缘信息。Padding可以帮助维持输出特征图的尺寸与输入相同或接近相同。

保留边缘信息:图像边缘的像素在卷积过程中参与的计算次数较少,这意味着边缘信息在特征提取过程中容易丢失。Padding通过在边缘添加额外的像素,增加了边缘像素的参与度,从而更好地保留了边缘信息。

提高性能:Padding有助于避免由于特征图尺寸快速缩小而导致的信息丢失,从而提高模型的性能,尤其是在处理较小的图像或需要进行多层卷积时。

Padding的类型:

  • Valid Padding (No Padding): 不进行任何填充。卷积核只在输入图像的有效区域内滑动。输出尺寸会缩小。
  • Same Padding: 添加足够的填充,使得输出特征图的尺寸与输入相同。
  • Full Padding: 尽可能多地添加填充,使得卷积核的每个元素都至少在输入图像上滑动一次。输出尺寸会增大。

Padding的选择:取决于具体的应用场景和网络架构

  • Valid Padding: 适用于不需要保持输出尺寸的场景,或者输入图像足够大,边缘信息丢失不重要的情况。
  • Same Padding: 广泛应用于各种CNN架构中,因为它可以保持特征图的尺寸,方便网络设计和计算。
  • Full Padding: 较少使用,因为它会增加计算量,并且可能会在边缘引入一些伪影。

小结

Padding(填充)

  • 在原图像特征图周围补充特征值(默认补0)
  • 作用
    • 使新特征图和原特征图形状保持一致
    • 减少边缘特征值信息丢失问题
      • 未padding, 边缘特征值只参与一次计算, 经过padding后, 边缘特征值参与多次计算
  • 实现方式
    • 不进行padding处理: 新特征图比原图像特征图小
    • same padding: 原图像特征图形状和新特征图形状一致
    • full padding: 新特征图形状比原图像特征图大, 新增特征

Stride(步长) 操作

Stride(步长)指的是卷积核在图像上滑动时的步伐大小,即每次卷积时卷积核在图像中向右(或向下)移动的像素数。步长直接影响卷积操作后输出特征图的尺寸,以及计算量和模型的特征提取能力。

Stride的作用:

降低计算复杂度 :更大的步长意味着卷积核移动的次数更少,从而减少了计算量,并加快了训练和推理速度。 减小特征图尺寸 :步长越大,生成的特征图尺寸越小。这类似于池化的降维效果。 增大感受野:虽然更大的步长会减小特征图的尺寸,但它同时也会增大每个神经元在输入数据上的感受野。这意味着每个神经元能够捕捉到更大范围的输入信息。

Stride的选择:取决于具体的应用场景和网络架构

  • Stride = 1: 这是最常见的设置,尤其是在网络的早期层。它允许保留更多的空间细节。
  • Stride > 1: 通常用于减小特征图的尺寸和增大感受野,例如在网络的后期层或需要进行快速降维时。 常见的设置包括 stride=2 或 stride=4。

按照步长为1来移动卷积核,计算特征图如下所示:

如果把Stride增大为2,也是可以提取特征图的,如下图所示:

小结

  • stride指卷积核(神经元)在特征图上滑动的步伐 默认1
  • 作用:
    • 减少计算量
    • 减少特征, 新特征图特征值减少(降维)
  • 一般默认1, 可以设置2或4
  • 原图像特征图5*5, stride=1->新特征图3*3, stride=2->新特征图2*2

多通道卷积计算

实际中的图像都是多个通道组成的,我们怎么计算卷积呢?

计算方法如下:

  1. 当输入有多个通道(Channel), 例如 RGB 三个通道, 此时要求卷积核需要拥有相同的通道数(图像有多少通道,每个卷积核就有多少通道).
  2. 每个卷积核通道与对应的输入图像的各个通道进行卷积.
  3. 将每个通道的卷积结果按位相加得到最终的特征图.

如下图所示:

多卷积核卷积计算

上面的例子里我们只使用一个卷积核进行特征提取, 实际对图像进行特征提取时, 我们需要使用多个卷积核进行特征提取. 这个多个卷积核可以理解为从不同到的视角、不同的角度对图像特征进行提取.

那么, 当使用多个卷积核时, 应该怎么进行特征提取呢?

通过以下例子查看多卷积核卷积计算流程:

可以看到:

  • 两个神经元,意味着有两个滤波器
  • 数据窗口每次移动两个步长取3*3的局部数据,即stride=2
  • zero-padding=1。输入数据由5*5*3变为7*7*3
  • 左边是输入(7*7*3中,7*7代表图像的像素/长宽,3代表R、G、B 三个颜色通道)
  • 中间部分是两个不同的滤波器Filter w0、Filter w1
  • 最右边则是两个不同的输出

小结
多通道卷积计算

  • RGB彩色图像是由3个通道组成 -> 3个二维矩阵, 每个矩阵分别代表R/G/B
  • 卷积核通道数和原图像通道数一致
  • 卷积计算 -> 对应通道二维矩阵进行卷积计算, 将每个通道卷积计算的结果加到一起, 得到新特征图的一个特征值
  • 新特征图是1个二维矩阵, 不是3个二维矩阵

多卷积核卷积计算

  • 有多少个卷积核就是有多少个神经元, 就会提取到多少个二维的特征图

特征图大小

输出特征图的大小与以下参数息息相关:

  1. size: 卷积核/过滤器大小,一般会选择为奇数,比如有 1*1, 3*35*5
  2. Padding: 零填充的方式
  3. Stride: 步长

那计算方法如下图所示:

  1. 输入图像大小: W x W
  2. 卷积核大小: F x F
  3. Stride: S
  4. Padding: P
  5. 输出图像大小: N x N

以下图为例:

  1. 图像大小: 5 x 5
  2. 卷积核大小: 3 x 3
  3. Stride: 1
  4. Padding: 1
  5. (5 - 3 + 2) / 1 + 1 = 5(如果除不尽向下取整), 即得到的特征图大小为: 5 x 5

小结

  • N = (W-F+2P)/S + 1
  • N: 新特征图高或宽
  • W: 原特征图高或宽
  • F: 卷积核高或宽
  • P: padding值
  • S: stride值
  • N如果为小数, 向下取整, 内部封装floor函数

卷积层API实战

py 复制代码
import torch
from matplotlib import pyplot as plt
from torch import nn

"""
in_channels:原图像的通道数,RGB彩色图像是3
out_channels:卷积核/神经元个数,输出的新的图像是有n个通道的二维矩阵组成
kernel_size:卷积核形状(3,3)(3,5)
stride:步长默认为1
padding:填充圈数 默认为 0 1 same -> stride = 1
nn.Conv2d(in_channels=,out_channels=,kernel_size=,stride=,padding=)
"""

# TODO:1.加载RGB彩色图像(H,W,C)
img = plt.imread(fname=r'.\data\img.jpg')
print(img)
print(img.shape)
# TODO:2.将图像的形状(H,W,C)转换为(C,H,W)permute() 方法
img2 = torch.tensor(data=img, dtype=torch.float32).permute(dims=(2, 0, 1))
print(img2)
print(img2.shape)
# TODO:3.将这张图像保存到数据集中(batch_size,C,H,W) unsqueeze()方法
img3 = img2.unsqueeze(dim=0)
print(img3)
print(img3.shape)
# TODO:4.创建卷积层对象,提取特征图
conv = nn.Conv2d(in_channels=3, out_channels=4, kernel_size=(3, 3), stride=2, padding=0)
conv_img = conv(img3)
print('conv_img', conv_img)
print(conv_img.shape)

# 查看提取到的4个特征图
# 获取数据集中的第一张图像
img4 = conv_img[0]
print(img4)
# 转换形状
img5 = img4.permute(dims=(1, 2, 0))
print(img5)

# img5 -> (H,W,C)
# img5[:,:,0] 第一个通道的额二维矩阵特征图,还有3个通道
feature1 = img5[:, :, 0].detach().numpy()
plt.imshow(feature1)
plt.show()

池化层

池化层(Pooling Layer)是用于降低输入数据的空间维度(例如图像的高度和宽度),从而减少计算量、减少内存消耗,并提高模型的鲁棒性。

池化层通常位于卷积层之后,它通过对卷积层输出的特征图进行下采样,保留最重要的特征信息,同时丢弃一些不重要的细节。

池化层的主要作用如下:

  • 降维和计算量减少:池化层通过减少特征图的尺寸,从而降低了计算量,特别是在多层网络中,随着层数的增加,池化能够显著减少计算资源的消耗。

  • 提高鲁棒性:池化操作可以使得特征对小的变换、平移和旋转变得更加不敏感。这样,模型在面对噪声或图像的轻微变化时,依然能够稳定工作。

  • 防止过拟合:通过池化减少了特征图的大小,减少了模型的复杂度,从而有助于防止过拟合,尤其是在较小的数据集上。

  • 抽象特征:通过池化层的操作,可以提取更为抽象和高层次的特征,使得网络能够学习到更具泛化能力的表示。

池化层计算

  • 最大池化(Max Pooling) :通过池化窗口进行最大池化,取窗口中的最大值作为输出
  • 平均池化(Avg Pooling) :取窗口内的所有值的均值作为输出

Padding(填充)

Stride(步长)

多通道池化计算

在处理多通道输入数据时,池化层对每个输入通道分别池化,而不是像卷积层那样将各个通道的输入相加。这意味着==池化层的输出和输入的通道数是相等。==

池化只在宽高维度上池化在通道上是不发生池化(池化前后,多少个通道还是多少个通道)

小结

池化层

池化层没有神经元参与, 只是实现降维, 不进行特征提取

池化计算

  • 卷积层提取到的特征图进行降维操作
  • 最大池化 -> 二维矩阵中最大的特征作为输出特征
  • 平均池化 -> 二维矩阵中平均特征值作为输出特征

多通道池化计算

  • 池化只在高和宽维度计算, 通道维度不参与池化
  • 卷积层提取到的特征图像有多少通道, 经过池化后还是多少通道

api

py 复制代码
import torch
from torch import nn

# 单通道卷积层特征图池化
# 创建 1通道的 3*3二维矩阵,一张特征图
inputs = torch.tensor([[[0, 1, 2], [3, 4, 5], [6, 7, 8]]], dtype=torch.float)
print(inputs)
print(inputs.shape)

# 创建池化层
# kernel_size:窗口的形状大小
pool1 = nn.MaxPool2d(kernel_size=(2, 2), stride=1, padding=0)
outputs = pool1(inputs)
print(outputs)
print(outputs.shape)

pool2 = nn.AvgPool2d(kernel_size=(2, 2), stride=1, padding=0)
outputs = pool2(inputs)
print(outputs)
print(outputs.shape)

# 多通道卷积层特征图池化
inputs = torch.tensor([[[0, 1, 2], [3, 4, 5], [6, 7, 8]],
                       [[10, 20, 30], [40, 50, 60], [70, 80, 90]],
                       [[11, 22, 33], [44, 55, 66], [77, 88, 99]]], dtype=torch.float)
# 创建池化层
# kernel_size:窗口的形状大小
pool1 = nn.MaxPool2d(kernel_size=(2, 2), stride=1, padding=0)
outputs = pool1(inputs)
print(outputs)
print(outputs.shape)

pool2 = nn.AvgPool2d(kernel_size=(2, 2), stride=1, padding=0)
outputs = pool2(inputs)
print(outputs)
print(outputs.shape)
相关推荐
weixin_440730501 小时前
队列QUEUE介绍+类型示例(先进先出、后进先出、优先级高先出、双端进入、生产者+消费者线程)
python·线程·queue
这是程序猿1 小时前
Java线程池深度剖析:源码原理、核心参数、任务调度与生产调优实战
java·开发语言·python
rannn_1111 小时前
【Java面试八股】Java基础篇|数据类型、面向对象、关键字、反射、注解、异常、新特性、序列化、设计模式、IO
java·后端·面试
Web3&Basketball1 小时前
ChatGPT 路由自证:跨客户端实测对照
python·大模型·agent·推理·推理优化
Patrick在香港1 小时前
Claude 工作提醒自动化:香港天文台四个接口三个「更新时间」,警告到期了却还在生效
python·api·claude·数据抓取·香港
用户8356290780511 小时前
Python 实现 Word 文档域的插入与管理
后端·python
quantdash_cc1 小时前
从数据采集到策略消费,股票日内行情管道应该怎么设计?
开发语言·python·数据分析·量化交易·股票数据·quantdash
MacroZheng1 小时前
阿里又开源了一个神级 Skill 项目!
java·人工智能·后端
程序员杰哥2 小时前
UI自动化测试:Jenkins配置
自动化测试·软件测试·python·测试工具·职场和发展·jenkins·测试用例