2023.9.17周报

目录

摘要

ABSTRACT

一、文献阅读

1、题目

2、ABSTRACT

3、网络架构

4、文献解读

1、Introduce

2、创新点

3、实验过程

4、Discussion

5、结论

二、Alexnet代码复现

1、alexnet模型

2、数据集划分

3、训练

4、测试

5、预测结果

三、深入学习CNN

一、神经网络基本结构

二、前向传播与反向传播数学推导

三、为什么要使用卷积神经网络?

四、卷积神经网络是什么?

一、三个基本层

二、不同通道输入得到不同通道的输出

四、CNN手写数字识别

一、导入相应的库

二、准备数据集

三、构建网络

四、模型训练

五、模型测试

​总结

摘要

本周,我通读了论文《ImageNet Classification with Deep Convolutional Neural Networks》。该文献的主要贡献是构建了一个深层神经网络架构,该架构具有几点创新之处。第一,通过减少参数量来加速训练;第二,提出了几种避免过拟合的措施;第三,使用ReLU激活函数取代了tanh和softmax。另外,我还深入学习了CNN的原理。CNN通过卷积和池化等操作,逐步减小图像尺寸,从而大大减少了参数量。总体而言,本周对CNN进行了深入剖析,这个过程让我受益匪浅。

ABSTRACT

This week, I thoroughly read the paper "ImageNet Classification with Deep Convolutional Neural Networks". The main contribution of this paper is constructing a deep neural network architecture that has several innovations. First, it reduces the number of parameters to accelerate training. Second, it proposes several measures to prevent overfitting. Third, it uses the ReLU activation function instead of tanh and softmax. In addition, I studied the principles of CNN in depth. CNN gradually reduces image size through operations like convolution and pooling, thus greatly reducing the number of parameters. Overall, this week I gained deep insights into CNN through in-depth analysis, which benefited me tremendously.

一、文献阅读

1、题目

ImageNet Classification with Deep Convolutional Neural Networks

期刊:Communications of the ACM

论文链接:https://dl.acm.org/doi/10.1145/3065386

2、ABSTRACT

This article utilized a deep convolutional neural network for image classification and achieved excellent results. The deep convolutional network consists of 60 million parameters, 650,000 neurons, five convolutional layers, and three fully connected layers. GPU acceleration was employed to speed up the training process. Dropout, a regularization technique, was used to reduce overfitting.

这篇文章利用了一个深度卷积神经网络来进行图片分类,取得了一个非常好的效果。深度卷积网络由60million个参数,65w个神经元,以及五个卷积层和三个全连接层组成。为了加快训练,用到了GPU加速实现。用了dropout这个正则化方法来减少过拟合。

3、网络架构

网络架构分为八个层,其中有五个卷积层和三个全连接层。

分析:

输入为224×224×3的图像

卷积层1的卷积核为11×11×3,strde=4,每个GPU内输出55×55×48,响应规范化,池化

卷积层2:256个卷积核,大小为5×5×48;响应规范化,池化

卷积层3,4:384个卷积核,大小为3×3×256;无池化

卷积层5:256个卷积核,大小为3×3×192

全连接层6,7:每个GPU内有2048个神经元,共4096个

全连接层8:输出与1000个softmax相连

输出:关于1000个类的分布
注:整个架构被分为2路来进行,分别为GPU1和GPU2。两者的架构层次是完全相同的。

4、文献解读

1、Introduce

文章主要任务就是利用深度卷积神经网络进行分类任务,作者首先确定使用CNN的架构,这样做减少了神经层之间的连接以及参数量,便于训练,且性能相比标准的具有相似尺寸大小的网络层的前馈神经网络只是轻微的下降。之后作者在框架中引入了几个设计,如ReLU的使用,数据集的扩增等等,提高了CNN网络的性能,还有就是GPU的使用硬性保证了大规模网络能够train起来。

2、创新点

1、使用ReLU函数替代softmax和tanh函数

在本文中,作者用非饱和非线性函数f(x)=max(0,x)代替饱和非线性函数,如f(x)=tanh(x)f(x)=\frac{1}{1+e^{-x}}

实线为用ReLU函数的误差下降率,虚线为tanh函数的误差下降率,从图中可以看出,用ReLU函数误差下降会比tanh函数快好多倍。

2、在多个GPU上训练

从网络框架图中我们可以看出,整个网络被分为两个来进行训练,然后把切完之后的网络用两个GPU来训练。

3、局部响应归一化

在ReLU层之前我们应用了normalization得到了一个更好的效果,文章中说,使用局部响应归一化比没有使用局部响应归一化的错误率要低%2左右。

局部响应归一化的好处:它的作用是对每个神经元的输出进行归一化,使得较大的响应值相对较小,较小的响应值相对较大。这样做的好处是可以抑制较大的响应,使得其他神经元的响应更加突出,从而增强网络对不同特征的敏感性。

4、重叠池化

一般来说两个pooling是不重叠的,但是这里采用了一种对传统的pooling改进的方式。文章中通过重叠池化,得出top-1和top-5错误率分别降低0.4%和0.3%。

3、实验过程

一、数据集

本文使用的数据集是ILSVRC-2010,LSVRC使用ImageNet的一个子集,1000个类别中的每个类别大约有1000个图像。总共大约有120万个训练图像、50000个验证图像和150000个测试图像。

由于实验数据中的图像是由可变分辨率的图像组成,所以我们在输入数据之前要先固定分辨率,所以我们就把所有的图像裁剪成256*256的面片。具体裁剪方法:先对原始图片进行缩放,将短边变成256的大小,另一个长边在这一步操作中也会根据长宽比进行调整,然后第二步从图片中心对长边进行两侧的裁剪,得到256*256的尺寸大小。

二、评估指标

ImageNet通常用两个指标来代表错误率:top-1和top-5。top-5表示正确标签不在网络模型认为的最有可能的五个标签内的比例。

三、实验数据

1、ILSVRC-2010测试集的结果比较。

从图中可以看出,本文所用的模型CNN的表现结果要比之前最优的模型的表现结果还要好。

2、ILSVRC-2012验证集和测试集的错误率比较。

文章中作者做了很多组相似网络的实验 ,因为测试集不公开,但测试集与验证集数据接近,所以1 CNNs和 1CNN*用验证集的数据来表示 

1 CNN是本文描述的模型,5 CNNs是五个相似CNN的预测平均值,1 CNNs是在1CNN基础上,在最后一个池化层后接了一个新的卷积层(第六层卷积层),用整个ImageNet Fall 2011 release训练(15M images 22K categories)然后在ILSVRC-2012上进行"fine-tuning"的结果,7 CNNs* 两个在Fall 2011上与训练的模型和5CNNs的预测平均值。

四、超参数的设定 

我们使用随机梯度下降训练我们的模型,批量大小为128个示例,动量为0.9,权重衰减为0.0005。

初始化参数:用均值为0 ,方差为0.01的高斯随机变量去初始化了权重参数

学习率:我们在所有层上使用相同的学习率,设为0.01。但验证误差不降的时候我们就手动的乘以0.1,也就是降低十倍。也有自动的方法,例如Resnet,训练120轮epoch,初始学习率也是设为0.01,每30轮降低十倍,本文是训练了90个epoch,每一次是120w张图片。

4、Discussion

1、如何防止过拟合?

神经网络架构有6千万个参数,但这被证明不足以在没有相当大的过拟合的情况下学习如此多的参数,那么文章中是如何做到防止过拟合的呢?

1.1、数据增强

减少图像数据过度拟合的最简单也是最常见的方法是使用保留标签的变换人为地放大数据集。这里用了两种方式:

1、通过从256×256图像中随机提取224×224的图像,并在这些提取的图像上训练我们的网络来实现这一点。这将使我们的培训集的规模增加了2048倍。但是有个问题也不能说就是2048倍,因为很多图片都是相似的。
2、采用PCA的方式对RGB图像的channel进行了一些改变,使图像发生了一些变化,从而扩大了数据集。

1.2、Dropout

随机的将隐藏层的输出以50%的概率设为0,相当于一个L2的正则化,只不过用了这种方式实现了L2正则化的功能。

5、结论

我们的研究结果表明,一个大型的深度CNN能够使用纯粹的监督学习在一个具有高度挑战性的数据集上实现破纪录的结果。值得注意的是,如果删除单个卷积层,我们的网络性能会下降。例如,移除任何中间层导致网络的前1个性能损失约2%。因此,深度对于实现我们的结果非常重要。

二、Alexnet代码复现

1、alexnet模型

import torch
from torch import nn

#N = (input_size − conv_size + 2*Padding )/Stride+1
class AlexNet(nn.Module):
    def __init__(self, num_classes=1000):
        super(AlexNet, self).__init__()
        #nn.Sequential()是一个PyTorch中用于封装序列的函数,通常用于将多个层串联起来形成一个神经网络。
        self.net = nn.Sequential(
            # 为了和论文的图像输入尺寸保持一致以及下一层的55对应,这里对图像进行了padding
            #输入通道3,输出通道96,卷积核大小11*11,步长为4,padding设置为2
            nn.Conv2d(in_channels=3, out_channels=96, kernel_size=11, stride=4, padding=2),
            #ReLU
            nn.ReLU(),
            #局部响应归一化,size:用于归一化的相邻通道的数量;alpha:乘法因子,默认值0.0001;beta:指数,默认值0.75;k:附加因子,默认为1
            nn.LocalResponseNorm(size=5, alpha=10e-4, beta=0.75, k=2),
            #最大池化,卷积核大小为3*#,步长设置为2
            nn.MaxPool2d(kernel_size=3, stride=2),
            nn.Conv2d(in_channels=96, out_channels=256, kernel_size=5, padding=2),
            nn.ReLU(),
            nn.LocalResponseNorm(size=5, alpha=10e-4, beta=0.75, k=2),
            nn.MaxPool2d(kernel_size=3, stride=2),
            nn.Conv2d(in_channels=256, out_channels=384, kernel_size=3, padding=1),
            nn.ReLU(),
            nn.Conv2d(in_channels=384, out_channels=384, kernel_size=3, padding=1),
            nn.ReLU(),
            nn.Conv2d(in_channels=384, out_channels=256, kernel_size=3, padding=1),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=3, stride=2),
            #它用来将输入张量展平为一维张量,它可以被用来将二维卷积层的输出偏扁为一维传到全连接层里
            nn.Flatten(),
            #在 training 模式下,基于伯努利分布抽样,以概率 p 对张量 input 的值随机置0;
            nn.Dropout(p=0.5),
            #nn.Linear()是PyTorch中的一个线性变换层,常用来在神经网络中构建一个线性分类器
            nn.Linear(in_features=256 * 6 * 6, out_features=4096),
            nn.ReLU(),
            nn.Dropout(p=0.5),
            nn.Linear(in_features=4096, out_features=4096),
            nn.ReLU(),
            nn.Linear(in_features=4096, out_features=num_classes)
        )
        self.init_weights()

    def init_weights(self):
        for layer in self.net:
            # 先一致初始化
            if isinstance(layer, nn.Conv2d):
                nn.init.kaiming_normal_(layer.weight, mode='fan_out', nonlinearity='relu')
                # nn.init.normal_(layer.weight, mean=0, std=0.01) # 论文权重初始化策略
                nn.init.constant_(layer.bias, 0)
            elif isinstance(layer, nn.Linear):
                nn.init.normal_(layer.weight, mean=0, std=0.01)
                nn.init.constant_(layer.bias, 1)
            # 单独对论文网络中的2、4、5卷积层的偏置进行初始化
            nn.init.constant_(self.net[4].bias, 1)
            nn.init.constant_(self.net[10].bias, 1)
            nn.init.constant_(self.net[12].bias, 1)

    def forward(self, x):
        return self.net(x)

    def test_output_shape(self):
        test_img = torch.rand(size=(1, 3, 227, 227), dtype=torch.float32)
        for layer in self.net:
            test_img = layer(test_img)
            print(layer.__class__.__name__, 'output shape: \t', test_img.shape)

2、数据集划分

import os
from shutil import copy, rmtree
import random


def make_dir(file_path):
    if os.path.exists(file_path):
        # 如果文件夹存在,则先删除原文件夹再创建
        rmtree(file_path)
    os.makedirs(file_path)


def split_data(input_file_path, output_file_path, split_rate, seed='random'):
    if seed == 'fixed':
        random.seed(0)
    else:
        random.seed()
    # 获取当前文件路径
    cwd = os.getcwd()
    input_dataset_path = os.path.join(cwd, input_file_path)
    output_dataset_path = os.path.join(cwd, output_file_path)
    assert os.path.exists(input_dataset_path), f"path '{input_dataset_path}' does not exist."
    # ===================#######################################
    # os.listdir() 方法用于返回指定的文件夹包含的文件或文件夹的名字的列表
    # os.path.isdir() 方法用于判断某一路径是否为目录
    # 先遍历dataset_path获得文件\文件夹名称列表,再判断名称是否为目录
    ############################################################
    dataset_classes = [dataset_class for dataset_class in os.listdir(input_dataset_path) if
                       os.path.isdir(os.path.join(input_dataset_path, dataset_class))]
    # 训练集
    train_path = os.path.join(output_dataset_path, 'train')
    make_dir(train_path)
    for dataset_class in dataset_classes:
        make_dir(os.path.join(train_path, dataset_class))
    # 验证集
    val_path = os.path.join(output_dataset_path, 'val')
    make_dir(val_path)
    for dataset_class in dataset_classes:
        make_dir(os.path.join(val_path, dataset_class))

    for dataset_class in dataset_classes:
        input_dataset_class_path = os.path.join(input_dataset_path, dataset_class)
        images = os.listdir(input_dataset_class_path)
        images_num = len(images)
        # 随机选取验证集
        val_images = random.sample(images, k=int(images_num * split_rate))
        for index, image in enumerate(images):
            # 获取图像路径
            image_path = os.path.join(input_dataset_class_path, image)
            if image in val_images:
                # 将图像文件copy到验证集对应路径
                copy(image_path, os.path.join(val_path, dataset_class))
            else:
                copy(image_path, os.path.join(train_path, dataset_class))
            print(f'[{dataset_class}] is processing: {index + 1}/{images_num}')
    print('process finished.')



if __name__ == '__main__':
    original_data_file_path = r'E:\研究生学习\project\alexnet\flower_photos\flower_photos'
    spilit_data_file_path = 'data'
    split_rate = 0.1
    split_data(original_data_file_path, spilit_data_file_path, split_rate)

3、训练

import os
import json
import torch
from torch import nn
from torch import optim
from torch.utils.data import DataLoader
from torchvision import transforms, datasets
from tqdm import tqdm

from alexnet import AlexNet

BATCH_SIZE = 64  # 论文128
LR = 0.0001  # 论文 0.01
WEIGHT_DECAY = 0.0005
MOMENTUM = 0.9
EPOCHS = 10  # 论文90

DATASET_PATH = 'data'
MODEL = 'AlexNet.pth'


def train_device(device='cpu'):
    # 只考虑单卡训练
    if device == 'gpu':
        cuda_num = torch.cuda.device_count()
        if cuda_num >= 1:
            print('device:gpu')
            return torch.device(f'cuda:{0}')
    else:
        print('device:cpu')
        return torch.device('cpu')


def dataset_loader(dataset_path):
    dataset_path = os.path.join(os.getcwd(), dataset_path)
    assert os.path.exists(dataset_path), f'[{dataset_path}] does not exist.'
    train_dataset_path = os.path.join(dataset_path, 'train')
    val_dataset_path = os.path.join(dataset_path, 'val')
    # 训练集图片随机裁剪224x224区域,以0.5的概率水平翻转
    # 由于torchvision没有封装PCA jitter,所以用Corlor jitter模拟RGB通道强度的变化(不够严谨...)
    # alexnet中训练样本分布为零均值分布,这里采用了常用的均值为0方差为1的标准正态分布
    data_transform = {
        'train': transforms.Compose([transforms.RandomResizedCrop(size=224),
                                     transforms.RandomHorizontalFlip(p=0.5),
                                     transforms.ColorJitter(brightness=0.5, contrast=0.5, saturation=0.5, hue=0.5),
                                     transforms.ToTensor(),
                                     transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))]),
        'val': transforms.Compose([transforms.Resize((224, 224)),
                                   transforms.ToTensor(),
                                   transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])}
    train_dataset = datasets.ImageFolder(root=train_dataset_path, transform=data_transform['train'])
    val_dataset = datasets.ImageFolder(root=val_dataset_path, transform=data_transform['val'])
    return train_dataset, val_dataset


def idx2class_json(train_dataset):
    class2idx_dic = train_dataset.class_to_idx
    idx2class_dic = dict((val, key) for key, val in class2idx_dic.items())
    # json.dumps()把python对象转换成json格式的字符串
    json_str = json.dumps(idx2class_dic)
    with open('class_idx.json', 'w') as json_file:
        json_file.write(json_str)
    print('write class_idx.json complete.')


def evaluate_val_accuracy(net, val_dataset_loader, val_dataset_num, device=torch.device('cpu')):
   
    if isinstance(net, nn.Module):
        net.eval()
    val_correct_num = 0
    for i, (val_img, val_label) in enumerate(val_dataset_loader):
        val_img, val_label = val_img.to(device), val_label.to(device)
        output = net(val_img)
        _, idx = torch.max(output.data, dim=1)
        val_correct_num += torch.sum(idx == val_label)
    val_correct_rate = val_correct_num / val_dataset_num
    return val_correct_rate


def train(net, train_dataset, val_dataset, device=torch.device('cpu')):
    train_dataset_loader = DataLoader(dataset=train_dataset, batch_size=BATCH_SIZE, shuffle=True)
    val_dataset_loader = DataLoader(dataset=val_dataset, batch_size=BATCH_SIZE)
    print(f'[{len(train_dataset)}] images for training, [{len(val_dataset)}] images for validation.')
    net.to(device)
    loss_function = nn.CrossEntropyLoss()
    optimizer = optim.SGD(params=net.parameters(), lr=LR, momentum=MOMENTUM, weight_decay=WEIGHT_DECAY)  # 论文使用的优化器
   
    lr_scheduler = optim.lr_scheduler.ReduceLROnPlateau(optimizer=optimizer, mode='max', factor=0.1, patience=1,
                                                        min_lr=0.00000001)
    # 在训练的过程中会根据验证集的最佳准确率保存模型
    best_val_correct_rate = 0.0
    for epoch in range(EPOCHS):
        net.train()
        # 可视化训练进度条
        train_bar = tqdm(train_dataset_loader)
        # 计算每个epoch的loss总和
        loss_sum = 0.0
        for i, (train_img, train_label) in enumerate(train_bar):
            optimizer.zero_grad()
            train_img, train_label = train_img.to(device), train_label.to(device)
            output = net(train_img)
            loss = loss_function(output, train_label)
            loss.backward()
            optimizer.step()

            loss_sum += loss.item()
            train_bar.desc = f'train epoch:[{epoch + 1}/{EPOCHS}], loss:{loss:.5f}'
        # 测试验证集准确率
        val_correct_rate = evaluate_val_accuracy(net, val_dataset_loader, len(val_dataset), device)
        # 根据验证集准确率更新学习率
        lr_scheduler.step(val_correct_rate)
        print(
            f'epoch:{epoch + 1}, '
            f'train loss:{(loss_sum / len(train_dataset_loader)):.5f}, '
            f'val correct rate:{val_correct_rate:.5f}')
        if val_correct_rate > best_val_correct_rate:
            best_val_correct_rate = val_correct_rate
            # 保存模型
            torch.save(net.state_dict(), MODEL)
    print('train finished.')


if __name__ == '__main__':
    # 这里数据集只有5类
    alexnet = AlexNet(num_classes=5)
    device = train_device('gpu')
    train_dataset, val_dataset = dataset_loader(DATASET_PATH)
    # 保存类别对应索引的json文件,预测用
    idx2class_json(train_dataset)
    train(alexnet, train_dataset, val_dataset, device)

4、测试

从网上下载一张花草的图片,然后丢给模型,并得出结果。

import os
import json
import torch
from PIL import Image
from torchvision import transforms
import matplotlib.pyplot as plt

from alexnet import AlexNet

IMG_PATH = r'E:\研究生学习\project\alexnet\04.png'
JSON_PATH = 'class_idx.json'
WEIGHT_PATH = 'AlexNet.pth'


def predict(net, img, json_label):
    data_transform = transforms.Compose(
        [transforms.Resize((224, 224)),
         transforms.ToTensor(),
         transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])
    original_img=img
    img = data_transform(img)  # 3,224,224
    img = torch.unsqueeze(img, dim=0)  # 1,3,224,224
    assert os.path.exists(WEIGHT_PATH), f'file {WEIGHT_PATH} does not exist.'
    net.load_state_dict(torch.load(WEIGHT_PATH))
    net.eval()
    with torch.no_grad():
        output = torch.squeeze(net(img))  # net(img)的size为1,5,经过squeeze后变为5
        predict = torch.softmax(output, dim=0)
        predict_label_idx=int(torch.argmax(predict))
        predict_label=json_label[str(predict_label_idx)]
        predict_probability=predict[predict_label_idx]
    predict_result=f'class:{predict_label}, probability:{predict_probability:.3f}'
    plt.imshow(original_img)
    plt.title(predict_result)
    print(predict_result)
    plt.show()


def read_json(json_path):
    assert os.path.exists(json_path), f'{json_path} does not exist.'
    with open(json_path, 'r') as json_file:
        idx2class = json.load(json_file)
        return idx2class


if __name__ == '__main__':
    net = AlexNet(num_classes=5)
    img = Image.open(IMG_PATH)
    idx2class = read_json(JSON_PATH)
    predict(net, img, idx2class)

5、预测结果

三、深入学习CNN

一、神经网络基本结构

一个最简单的神经网络由输入层、隐含层以及输出层组成,其中每一层都包含多个神经元,上一层的神经元经过激活函数映射到下一层神经元,每个神经元之间都有相应的权值,输出的就是我们的分类类别。

二、前向传播与反向传播数学推导

如果我们有一个这样的网络层:

其中第一层是输入层,包括两个神经元i1和i2以及截距项b1;第二层是隐含层h1,h2以及截距项b2,第三层是输出o1,o2,每条线上标的wi是层与层之间连接的权重,激活函数我们默认为sigmoid函数。

如果我们对上述图像中的每一个未知参数赋值:

输入数据  i1=0.05,i2=0.10;

     输出数据 o1=0.01,o2=0.99;

     初始权重  w1=0.15,w2=0.20,w3=0.25,w4=0.30;

           w5=0.40,w6=0.45,w7=0.50,w8=0.55

目标:给出输入数据i1,i2(0.05和0.10),使输出尽可能与原始输出o1,o2(0.01和0.99)接近。

step1:前向传播

 

经过前向传播后,我们得出o1和o2的值分别为1.10590、0.77292,但是这与实际值0.01以及0.99还相差很远。所以我们要对误差进行反向传播,更新权重,重新计算输出。

Step2 反向传播

以上就是方向传播的具体步骤,经过反向传播,可以有效地使误差变小,并且可以使得预测值与实际值越来越接近。

三、为什么要使用卷积神经网络?

首先在图像领域,使用传统的神经网络并不合适,图像是由一个个像素点构成,每个像素点有三个通道,分别代表RGB颜色,比如说一个图像的尺寸是(1,28,28),它的含义是通道数channel为1,长宽分别为28的图像,也就是黑白图像。如果使用全连接神经网络的话,网络中的神经元与相邻层上的每个神经元均连接,那就意味着我们的网络有28*28=784个神经元,隐含层如果用15个神经元的话,我么我们需要的未知参数w和b的数量就要有:784*15+15*10+2=117,752个参数,这个参数太多了,随便进行一次反向传播计算量都是巨大的,从计算资源和调参的角度都不建议用传统的神经网络。

四、卷积神经网络是什么?

一、三个基本层

一、卷积层(convolutional layer)

传统的三层神经网络需要大量的参数,原因就是每个神经元都和相邻层的神经元相连接,但其实这种连接方式并不是必须的,举个例子来说,比如我们看一张猫的照片,可能我们只看到猫的嘴巴或者猫的眼睛我们就知道这是一只猫,就不需要看整张图片。所以说如果我们可以用某种方式对一张图片的某个典型特征识别,那么这张图片的类别我们也就知道了。而卷积就是干这个事情的。举个例子,现在有一个4*4的图像,我们设计两个卷积核,看看运用卷积核后图片会变成什么样。

原始图片是一张黑白图片,每个位置都是一个像素值,0表示白色,1表示黑色。而对于这个4*4的图像,我们采用两个2*2的卷积核来计算,步长stride默认为1,即每次向右或向下平移一个单位。计算过程就是卷积核和在原图像中对应区域做内积(注意不是做矩阵乘法)。

注:如何从原图像尺寸与卷积核大小直接计算出feature map的大小?

feature_map尺寸=【(原图像大小-卷积核大小)/步长】+1

以上就是卷积的基本过程,从计算中我们可以看到,同一层的神经元可以共享卷积核,那么对于高位数据的处理就会变得很简单,并且使用卷积核后图片的尺寸变小,这样能够方便后续的计算,且我们不需要手动去选取特征,只用设计好卷积核的尺寸,数量和滑动的步长就可以让它自己去训练。

但是为什么卷积核是有效的呢?

我们已经知道了卷积核是如何计算的,但是为什么使用卷积核计算后分类效果要优于普通的神经网络呢?通过观察上图中的计算结果,通过第一个卷积核计算后的feature map是一个三维数据,而且在第三列的绝对值最大,这说明在原始图片上,对应的地方有一条垂直方向的特征,也就是说像素值变化较大;而通过第二个卷积核计算后,第三列的数值是0,第二行的数值最大,说明原始图片上对应的地方有一条水平方向的特征。

我们设计的两个卷积核都能够分别提取图片的特征,其实我们可以把卷积核理解为特征提取器。所以我们只需要把数据输入进去,设计好卷积核的尺寸、数量以及步长就可以自动提取出图片的某些特征,从而达到分类的效果。

二、池化层

我们的图片经过上一层2*2的卷积核操作后,我们将原始图像由4*4的尺寸变为了3*3的新图片,池化层的主要目的是通过降采样的方式,在不影响图片质量的前提下,压缩图片,减少参数,比如说我们用MaxPooling,大小为2*2,步长为1,取每个窗口最大的数值更新,那么图片的尺寸就会由3*3变为2*2。

通常来说,池化方法一般有一下两种:

  • MaxPooling:取滑动窗口里最大的值
  • AveragePooling:取滑动窗口内所有值的平均值

上图中就是采用MaxPooling的方式,把每一个2*2的窗口中最大值取出来,组成新的feature_map。

为什么采用MaxPooling?

从计算的方式来看,MaxPooling是非常简单的,只需要取每一个窗口的最大值即可,但是这也引发了一个思考,为什么要采用MaxPooling呢?采用MaxPooling之后不会丢失一些重要的特征吗?不会丢掉一些重要的信息吗?

从卷积的效果来看,每一个卷积核都可以看作是一个特征提取器,不同的卷积核负责提取不同的特征,比如说上述的例子,第一个卷积核,我们能提取出“垂直”方向的特征,第二个卷积核,我们能提取出“水平”方向的特征,那么我们对其进行Max Pooling之后,提取出的是真正能够识别特征的数值,被舍弃的数值,其实可以被保留的数值所代替,经过Max Pooling之后,见笑了feature map的尺寸,减小了参数,也减小了计算量。

Padding

什么是padding?

padding 指的是将输入数据的边界进行扩充,使其符合卷积运算的要求。

为什么要使用padding?

1. 保持空间维度不变,卷积运算会减小输入数据的空间维度,而padding可以在输入数据边缘补0,保持输入和输出的空间维度一致。

2. 通过在边界补0,可以增加卷积核的感受野,获得更大范围的上下文信息。

3. padding可以避免卷积时边界信息的损失,保证不同位置的输入可以被平等对待。

4. 控制欠拟合和过拟合,合适的padding可以在一定程度上控制模型的复杂度,避免欠拟合或过拟合。

正如上述例子,我们的图片由4*4,通过卷积层变为3*3,再通过池化层变为2*2,如果我们继续卷积以及池化,那么图片岂不是会越来越小?这时,我们可以通过padding(补0)的方式来解决,它可以帮助我们保证每次经过卷积或池化输出后图片的大小不变,如:

有些时候,我们希望图片做完卷积操作后保持图片大小不变,所以我们一般会选择尺寸为3*3的卷积核以及1的padding。加入padding之后feature map尺寸=(width+2*padding_size-filter_size)/stride+1

三、全连接层

到这里,一个完整的"卷积部分"就算完成了,如果想要叠加层数,一般也是叠加“Conv-Max Pooling”,通过不断的设计卷积核的尺寸,数量,提取更多的特征,最后识别不同类别的物体。做完Max Pooling后,我们就会把这些数据丢给Flatten层,然后把Flatten层的输出丢给full connected层,采用激活函数对其分类。

二、不同通道输入得到不同通道的输出

一、单通道输入卷积操作

输入尺寸(1,5,5)卷积核尺寸(3,3),输出尺寸(5-3)/1+1=3。

二、3通道输入单通道输出卷积操作

这种方式就是,先分别用3个输入乘对应的卷积核,然后得到3个feature map,再把feature map中对应位置的数据进行相加。

三、N个通道的输入M个通道的输出

如何由n个通道的输入图片得到m个通道的输出图片呢?首先,n个通道的输入图片,卷积一次,需要与一个n个通道的卷积核进行卷积,并得到一个单通道的feature map,那么想要得到具有m个通道的输出图片,就需要由m个具有n个channel的卷积核,进行卷积m次,就可以拼接成一个具有m个通道的输出图片。

四、卷积层

通过上述把n个通道的输入变成m个通道的输出,我们可以得到一个4维张量的卷积层,其中第一个参数是输出图片的通道数,第二个参数是输入图片的通道数,第三个以及第四个参数是卷积核的大小。

四、CNN手写数字识别

一、导入相应的库

torchvision的作用就是,对于我们需要数据集的dataloader,它可以让我们使用一种方便的方式来加载MINIST数据集。

import torch
import torchvision
from torch.utils.data import DataLoader
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
import matplotlib.pyplot as plt

二、准备数据集

在准备数据集之前,我们要先定义超参数,其中epoch的大小就是我们循环训练数据集的次数,learning rate和momentum是我们使用优化器时的超参数。训练以及测试使用的batch_size分别为64和1000。

n_epochs = 3
batch_size_train = 64
batch_size_test = 1000
learning_rate = 0.01
momentum = 0.5
log_interval = 10
random_seed = 1
torch.manual_seed(random_seed)

对于可重复的实验,我们必须为任何使用随机数产生的东西设置随机种子--如numpy和random,因为多次运行同一个实验代码,由于随机数的不同,每次的结果也会不同。设置随机种子可以保证每次运行都使用相同的随机数,使实验结果可复现。

train_loader = torch.utils.data.DataLoader(
    torchvision.datasets.MNIST('./data/', train=True, download=True,
                               transform=torchvision.transforms.Compose([
                                   torchvision.transforms.ToTensor(),
                                   torchvision.transforms.Normalize(
                                       (0.1307,), (0.3081,))
                               ])),
    batch_size=batch_size_train, shuffle=True)
test_loader = torch.utils.data.DataLoader(
    torchvision.datasets.MNIST('./data/', train=False, download=True,
                               transform=torchvision.transforms.Compose([
                                   torchvision.transforms.ToTensor(),
                                   torchvision.transforms.Normalize(
                                       (0.1307,), (0.3081,))
                               ])),
    batch_size=batch_size_test, shuffle=True)

注:这段代码的主要功能是:

1、构建MINIST训练集和测试集的DataLoader

2、从torchvision中加载MNIST数据集,并指定训练集为train=Ture,测试集train=False

3、将训练集和测试集的数据进行转换:

-ToTensor():将图像数据从PIL类型转换为Tensor

-Normalize():将图像进行标准化

4、对训练集的数据进行随机打乱shuffle=True

5、指定训练集和测试集的batch_size,用于模型的训练和测试。

三、构建网络

在构建网络中,我们使用两个2d卷积层,然后是两个全连接层,激活函数使用ReLU,作为正则化的手段,我们使用两个dropout层。其中conv1输入通道数为1,输出通道数为10,卷积核大小为5*5;conv2输入通道数为10,输出通道数为20,卷积核大小为5*5,conv2层还是用了torch.nn中的Dropout2d类,全连接层fc1输入大小为320,输出大小为50.全连接层fc2输入大小为50,输出大小为10。forward()传递定义了使用给定的层和函数计算输出的方式。为了便于调试,在前向传递中打印出张量是完全可以的。

class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        self.conv1 = nn.Conv2d(1, 10, kernel_size=5)
        self.conv2 = nn.Conv2d(10, 20, kernel_size=5)
        self.conv2_drop = nn.Dropout2d()
        self.fc1 = nn.Linear(320, 50)
        self.fc2 = nn.Linear(50, 10)
    def forward(self, x):
        x = F.relu(F.max_pool2d(self.conv1(x), 2))
        x = F.relu(F.max_pool2d(self.conv2_drop(self.conv2(x)), 2))
        x = x.view(-1, 320)
        x = F.relu(self.fc1(x))
        x = F.dropout(x, training=self.training)
        x = self.fc2(x)
        return F.log_softmax(x)

初始化网络网络和优化器

network = Net()
optimizer = optim.SGD(network.parameters(), lr=learning_rate, momentum=momentum)

四、模型训练

首先,我们要确保我们的网络处于训练模式,然后,每个epoch对所有训练数据进行一次迭代,加载单独批次由DataLoader处理。

首先,我们需要使用optimizer.zero_grad()手动将梯度设置为零,因为pytorch在默认情况下会累计梯度,然后,我们生成网络的输出(前向传递),并计算输出与真实值标签之间的差值,计算损失。

然后,我们收集一组新的梯度,并使用optimizer.step()将其传播回每个网络参数。

def train(epoch):
    network.train()
    for batch_idx, (data, target) in enumerate(train_loader):
        optimizer.zero_grad()
        output = network(data)
        loss = F.nll_loss(output, target)
        loss.backward()
        optimizer.step()
        if batch_idx % log_interval == 0:
            print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format(epoch, batch_idx * len(data),
                                                                           len(train_loader.dataset),
                                                                           100. * batch_idx / len(train_loader),
                                                                           loss.item()))
            train_losses.append(loss.item())
            train_counter.append((batch_idx * 64) + ((epoch - 1) * len(train_loader.dataset)))
            torch.save(network.state_dict(), './model.pth')
            torch.save(optimizer.state_dict(), './optimizer.pth')

五、模型测试

def test():
  network.eval()
  test_loss = 0
  correct = 0
  with torch.no_grad():
    for data, target in test_loader:
      output = network(data)
      test_loss += F.nll_loss(output, target, size_average=False).item()
      pred = output.data.max(1, keepdim=True)[1]
      correct += pred.eq(target.data.view_as(pred)).sum()
  test_loss /= len(test_loader.dataset)
  test_losses.append(test_loss)
  print('\nTest set: Avg. loss: {:.4f}, Accuracy: {}/{} ({:.0f}%)\n'.format(
    test_loss, correct, len(test_loader.dataset),
    100. * correct / len(test_loader.dataset)))
    
test()
for epoch in range(1, n_epochs + 1):
    train(epoch)
    test()

运行结果:

总结

通过CNN手写数字识别这个项目,让我更深刻地理解了CNN的训练过程,也学习了torch.nn中的一些常用函数,以及初步认识了张量这个名词。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值