云计算百科
云计算领域专业知识百科平台

pytorch【第二部分】

第三章

您可以在此处查看前二章的内容

数据管道

我们使用MNIST的数据集,这是标准的深度学习入门方式。可是如果我们希望构建特定功能的模型,比如不只是识别数字,而是识别花卉、识别行人。我们或许就要用到自己准备的训练集了,这个时候,训练集可能并不标准,或许包含空值或者没有没有意义的值,而这些值一旦交给模型训练,就会导致模型无法读取有效数据而造成最终的训练效果不尽人意的情况,这时候对于数据的处理就至关重要了

当您拿到训练集后,您一般需要做以下的预备工作

  • 将图片与标签关联起来【图片和标签的原本关系是什么样的呢?标签就藏在图片的名称当中?还是标签与图片的关系存储在一个文件当中?您需要处理各种不同的情况,并转换为上述的(data,labels)的标准格式训练】
  • 将图片转换为正确的格式(正确的大小,正确的格式,正确的数据结构),以保证训练器可以识别并根据这些数据训练模型
  • 您需要高效地批量加载所有数据,控制每次加载的大小【太小模型训练的速度会较慢(因为批次很多,模型会花时间做大量重复的工作),太大又会导致内存的占用过载(大量资源加载而导致空间不足)】
  • 我们可以将这些问题大致分为访问问题(数据加载器可合法访问,建立图片与标签的关联),质量问题(训练集的质量,使用正确统一的格式)和效率问题(高效运作,选择合适的批量加载大小)

    在本章节中,我们将使用oxFord花卉数据集进行学习

    图像下载地址:https://www.robots.ox.ac.uk/~vgg/data/flowers/102/102flowers.tgz

    标签下载地址:https://www.robots.ox.ac.uk/~vgg/data/flowers/102/imagelabels.mat

    解决访问问题

    资源下载

    在oxFord数据集中,标签存储在.mat文件中,所以我们需要手动构建这样的对应关系,确保模型可以正确的获取我们的图像——标签映射

    在此之前,我们需要下载数据集和标签,urllib库为我们提供了这样的支持

    import urllib
    # ……
    urllib.request.urlretrieve("download_url","target_path")

    我们使用urllib中request模块的urlretrieve方法从对应url下载文件到本地,第一个参数是下载路径,第二个参数是目标路径【本地】,格式为文件所在目录/文件名称,这里的文件名称我们可以直接获取下载文件的名称,而目录则需要我们自己指定,在课程当中,我们使用这样的目录【您也可以构建自己的目录规则】

    — flowers
    |— img
    |— **imgfiles【need download】**
    |— **labelsfile【need download】**

    我们使用flowers作为我们的数据集根目录,将我们的标签文件存储在根目录下,同时在数据库根目录下新建一个img目录,来存储下载的图像资源

    据此,我们可以构建图像文件的路径和Label文件的路径,os.path.join是一个处理路径拼接的方法,它能帮助我们优雅地解决这些问题

    image_url = "https://www.robots.ox.ac.uk/~vgg/data/flowers/102/102flowers.tgz"
    labels_url = "https://www.robots.ox.ac.uk/~vgg/data/flowers/102/imagelabels.mat"
    os.path.join("flowers","img",os.path.basename(image_url)) # 拼接图像资源目标路径
    os.path.join("flowers",os.path.basename(image_url)) # 拼接label资源目标路径

    我们有了资源下载路径和目标路径,接下来就可以使用urlretrieve进行下载了

    urllib.request.urlretrieve(image_url,os.path.join("flowers","img",os.path.basename(image_url)))
    urllib.request.urlretrieve(labels_url,os.path.join("flowers",os.path.basename(labels_url)))

    我们注意到image_url中文件并不是直接以图像文件的格式存在的,他是一个tgz压缩包,我们需要使用解压工具解压到图片资源所在路径,这样后续模型才能访问我们的图片

    在下载好文件之后,我们需要从目标路径创建文件流【使用tarfile.open】,然后对文件流管控的文件进行解压操作【您不用手动处理这些步骤,tg(your file stream).extractall(“your target path”)方法可以帮您解决解压缩的问题】

    import tarfile
    # ……
    with tarfile.open(os.path.join("flowers", "img", "102flowers.tgz")) as tg:
    tg.extractall(os.path.join("flowers", "img"))

    您可以用函数封装这些步骤,以在代码运行的时候,可以直接从url下载文件并运行您所做的这些预处理操作让它成为数据加载器可以直接访问的文件。

    def download_dataset() > None:
    image_url = "https://www.robots.ox.ac.uk/~vgg/data/flowers/102/102flowers.tgz"
    labels_url = "https://www.robots.ox.ac.uk/~vgg/data/flowers/102/imagelabels.mat"

    os.makedirs("flowers",exist_ok=True)
    # 防止重复下载
    if not os.path.exists(os.path.join("flowers","img","102flowers.tgz")):
    urllib.request.urlretrieve(image_url, os.path.join("flowers","img",os.path.basename(image_url)))
    with tarfile.open(os.path.join("flowers", "img", "102flowers.tgz")) as tg:
    tg.extractall(os.path.join("flowers", "img"))
    # 防止重复下载
    if not os.path.exists(os.path.join("flowers","imagelabels.mat")):
    urllib.request.urlretrieve(labels_url, os.path.join("flowers",os.path.basename(labels_url)))

    创建DataSet类

    我们知道数据集和标签存放的位置,可是模型该如何去寻找它呢,您还记得我们之前提到的SomeDataSet吗,他并不是一个真实的类,而是我们需要去实现的类,就像我们之前在分类MNIST数据集的时候,MNISI库本身实现了自己的DataSet

    train_dataset=torchvision.datasets.MNIST('./data',train=True,download=True,transform=transform)

    继承DataSet的类需要实现以下两个方法,以便后续DataLoader加载的时候能够正确获取信息

    __len__返回数据集的数量——模型一个epoch该获取多少资源

    __getitem__返回索引对应的资源(image,label)-> 模型通过索引从资源集合中获取单组(image,label)具体的资源

    _init_

    无论是返回资源数量还是返回具体资源,我们都需要在这些类中提供这些信息,因为上述两个方法只是获取的操作,我们可以在类的构造函数中__init__加载这些资源(为了对内存更友好,我们可以采用懒加载的形式),即类本身只保存这些资源的获取位置,等到模型真正调用__getitem__再去获取它

    def __init__(self, root, transform, target_transform=None):
    # 数据集根路径——外部调用的时候将传入flowers
    self.root = root
    self.img_dir = os.path.join(self.root,"img","jpg")
    self.transform = transform

    labels_mat = scipy.io.loadmat(os.path.join(self.root,"imagelabels.mat"))
    self.labels = labels_mat["labels"][0] 1

  • 您会发现上一步我们解压图像tgz压缩文件的时候,我们的img文件夹多出了一个jpg文件夹,当打开这个文件夹后,您会发现那里才是存储真实图片的地方,所以我们的img_dir会在拼接img文件夹的基础上再拼接一个jpg文件夹

  • 我们在保存loabels的时候并没有使用懒加载,因为这只是一个文件,对内存的压力微乎其微【通过查看发现它仅有502字节(可能经过了压缩算法)】,这个时候放在__getitem__频繁的加载反而会因为拼接的函数调用开销而导致训练速度变慢

  • transform我想等到讲完整个dataSet后再讲

  • _len_

    def __len__(self):
    return len(self.labels)

    返回数据集的大小,这里标签与图像一一对应,所以标签的大小就是整个数据集的大小

    _getitem_

    def __getitem__(self, index):
    img_name = f'image_{index+1:05d}.jpg'
    img_path = os.path.join(self.img_dir,img_name)

    img = Image.open(img_path)
    if self.transform is not None:
    img = self.transform(img)
    labels = self.labels[index]
    return img, labels

    在获取具体数据的时候,模型使用参数idx调用函数,这意味着您需要返回第idx个资源(第idx个图像数据以及第idx个图像数据所对应的标签),标签很容易寻找,labels数组当中的第idx个数据就是第idx图片所对应的标签,对于文件结构来说,它是一一对应的,但是我们如何从图片文件夹中获取第idx个图像呢?

    可以像寻找标签那样,保存所有文件夹的图片路径到一个数组,然后通过找idx位找到第idx个图像路径,然后根据图像路径访问吗?或许这是可行的,但是这样并不稳定,您得保证数组中的资源是有序的。标签文件是逐字节有序读取的,所以他能保证读取的文件和源文件数据一致,但是对于文件夹来说,他可能并没有一个规范的结构将文件与一个位置一一对应,就像您在保存文件的时候不会指定一号文件放在文件夹的第一个位置,我们很少听说文件夹有第一个位置这种说法,读取方式或许也是随机的,种种因素可能会导致数组中的文件并不是您想要的顺序结构【除非您在读取之后对数组按照名称进行排序】但是我们的oxford数据集的确是有结构的,这样的结构藏在文件命名中

    我们会发现,文件以image__00001.jpg,image_00002.jpg这样的方式命名,实际上这就是文件顺序

    00001对应第1个文件,00002对应第二个文件,以此类推f'image_{index+1:05d}.jpg'对应第index+1个文件

    05d代表5位整数,< 5为整数用0填充,为什么要用index + 1而不是index呢?

    对于图片来说,是从00001开始命名的,而模型传参从0开始,他代表获取第一张图片

    模型(index)获取资源
    0 00001
    1 00002
    index index+1:05d

    通过文件名与实际文件的对应,我们就能找到第idx+1张图片和第idx+1个标签并返回给模型

    if self.transform is not None:
    img = self.transform(img)

    这行代码再次出现了transform,语义是如果对象包含trannsform属性 同时 transform属性不是none就应用transform变换

    解决质量问题

    transforms的出现,正是为解决我们提出的质量问题。

    模型期望输入的数据是标准且规整的,即大小一致,色彩表示范围一致,数据结构一致,我们可以用transforms进行统一处理

    # 注意transform是您自定义的变量,transforms是模块
    transform = transforms.Compose([
    transforms.Resize(256),
    transforms.CenterCrop(224),
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
    ])

    这里我们的处理流程是这样的

  • 重新修改图片尺寸——这里把短边缩放为256像素,长边自动等比例缩放
  • o

    r

    S

    h

    o

    r

    t

    o

    r

    L

    e

    n

    g

    t

    h

      

    =
      

    a

    f

    S

    h

    o

    r

    t

    :

    256

    a

    f

    L

    e

    n

    g

    t

    h

    o

    r

    L

    e

    n

    g

    t

    h
      


      

    a

    f

    S

    h

    o

    r

    t

    :

    256
      

    =
      

    o

    r

    S

    h

    o

    r

    t
      


      

    a

    f

    L

    e

    n

    g

    t

    h

    a

    f

    L

    e

    n

    g

    t

    h
      

    =
      

    a

    f

    S

    h

    o

    r

    t

    :

    256
      


      

    o

    r

    L

    e

    n

    g

    t

    h

    o

    r

    S

    h

    o

    r

    t

    \\begin{align} &\\dfrac{orShort}{orLength}\\;=\\;\\dfrac{afShort:256}{afLength} \\\\ &orLength\\;*\\;afShort:256\\;=\\;orShort\\;*\\;afLength \\\\ &afLength\\;=\\; \\dfrac{afShort:256\\;*\\;orLength}{orShort} \\end{align}

    orLengthorShort=afLengthafShort:256orLengthafShort:256=orShortafLengthafLength=orShortafShort:256orLength

  • 取样有效区域——取中心区域长宽为224像素的尺寸,这样所有经过处理的单张图片就都统一标准化成了【3,224,224】
  • 为什么不在第一步就Resize(224)呢?

    第一步将短边缩放为224像素,再从中心截取,确实能够构建出【3,224,224】尺寸的图片,但是我们还考虑到了噪声的处理,多数时候,图片的一些边缘信息是不重要的,他和整体识别无关,或许是环境或许是水印,如果我们将带有这些印记的信息添加进去,不仅无效,甚至可能会干扰模型的训练,所以我们做了一个简单的噪声处理,默认边缘为无关信息,至少会在短边裁剪出左右各16像素的噪声

  • 为什么采用中心裁剪,而不是直接将图片拉伸为(3,224,224)

    拉伸后的图片变形,会让依赖于相对位置关系的特征被修改,产生错误的特征,从而干扰模型的训练

  • 转换为模型能够训练的数据类型——(RGB空间->张量)
  • 归一化(【0~255】/ 255 ->【0 ~ 1】),模型对于数值较小的数据,在训练的过程中更有优势,而且小数据可以防止数据过大而导致的溢出问题
  • 最后就可以通过构造函数传入transform,使得模型在调用__getitem__获取数据的时候自动应用transform

    transform = transforms.Compose([
    transforms.Resize(256),
    transforms.CenterCrop(224),
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
    ])
    data_prepare.download_dataset()
    dataset = OxfordFlowersDataSet('flowers',transform=transform)

    解决效率问题

    模型训练不仅包含训练的过程,还包含验证、测试,具体可见pyTorch构建模块化的方式

    什么是验证集

    您可以将它理解为训练阶段的测试集。这里的训练阶段会比模块化的我训练更广泛,只要您还准备调参优化或者调整代码,不打算正式部署,都可以被规划为训练阶段,您在训练几个epoch后,会用验证集进行验证,并根据结果调参优化,而测试集就是正式部署之前的最后测试,您可以用不同的测试集,但是结果不能再作为调参的依据,

    我们可以对整个数据集按大小随机划分,从而划分出三个不同的数据集,所有集合的大小一般是(训练集:0.7,测试集:0.15,验证集:0.15)

    为什么要随机划分

    如果数据集本身是有序的,那么很有可能会出现三个集合;落在某一个或者某几个类别上,从而无法覆盖大多数测试情况,对于训练集我们还需要在每次加载的时候进行随机洗牌,以避免在多次的epoch中学习到与批次相关的特征而非与图片本身,如果每次的epoch采用相同的关系,模型可能会这样认为,第一次是玫瑰、月季、牡丹……,第二次也是玫瑰、月季、牡丹、第三次也是……,第n次,然后它会认为特征应该就是如此,用户第一次喂给我的数据是玫瑰,第二次喂给我的数据是月季……,当我们DataLoader随机洗牌后,每个批次的数据,批次与批次的数据都变得不完全一致,这有助于防止模型过拟合,提高模型的泛化能力

    Knuth洗牌算法(拓展)

    洗牌算法有一个极其优雅的实现——他能保证公平的洗牌,在某个位置上,每个元素出现的概率都是 1 / n

    import secrets
    k_list = [1,2,3,4,5]
    for i in range(len(k_list) 1,1,1):
    rd = secrets.randbelow(i+1)
    k_list[i],k_list[rd] = k_list[rd],k_list[i]
    print(k_list)

    $$ \\begin{align}

    &index_{n-1}:\\dfrac{1}{n}\\ &index_{n-2}:\\dfrac{1}{n-1};;\\dfrac{n-1}{n};=;\\dfrac{1}{n}\\ &index_{n-3}:\\dfrac{1}{n-2};;\\dfrac{n-2}{n-1};;\\dfrac{n-1}{n};=;\\dfrac{1}{n}\\ &…\\ &index_{0}:\\dfrac{1}{\\cancel{1}};;\\dfrac{\\cancel{1}}{\\cancel{2}};;\\dfrac{\\cancel{2}}{\\cancel{3}};;…;;\\dfrac{\\cancel{n-2}}{\\cancel{n-1}};*;\\dfrac{\\cancel{n-1}}{n};=\\dfrac{1}{n} \\end{align} $$

    在第n个位置的时候,每个元素取到的概率相同【secrets.randbelow(i)是真随机生成[0,i)的随机整数】,概率

    p
      

    =
      

    1

    n

    {p\\;=\\;\\dfrac{1}{n}}

    p=n1,然后该位置已经是随机,不需要再纳入随机范围了,此时随机范围为[0,i-1)

    在第n-1个位置时,每个元素取到的概率是

    p
      

    =
      

    1

    n

    1

    p\\;=\\;{\\dfrac{1}{n-1}}

    p=n11,但您需要确保上一次的随机不会取到该元素,上一次取到p的概率是

    1

    n

    {\\dfrac{1}{n}}

    n1,对于两种情况的否命题【取和不取】来说,不取的概率是

    ¬

    P

    {\\neg{P_取}}

    ¬P,概率是

    1
      


      

    P

      

    =
      

    n

    1

    n

    {1\\;-\\;P_取\\;=\\;\\dfrac{n-1}{n}}

    1P=nn1,所以概率是在上一轮不会取到该元素的情况下这一轮取到该元素

    p

    =

    1

    n

    1

      


      

    n

    1

    n

      

    =
      

    1

    n

    {p = \\dfrac{1}{n-1}\\;*\\;\\dfrac{n-1}{n}\\;=\\;\\dfrac{1}{n}}

    p=n11nn1=n1,然后该位置已经是随机,不需要再纳入随机范围了,此时随机范围为[0,i-2)

    同理,在n-2个位置时,每个元素取到的概率是

    p
      

    =
      

    1

    n

    2

    p\\;=\\;{\\dfrac{1}{n-2}}

    p=n21,在前面两次随机都不取得情况下【第一次是

    1
      


      

    P

    1

      

    =
      

    n

    1

    n

    {1\\;-\\;P_{取1}\\;=\\;\\dfrac{n-1}{n}}

    1P1=nn1,第二次是

    1
      


      

    P

    2

      

    =
      

    1
      


      

    1

    n

    1

      

    =
      

    n

    2

    n

    1

    {1\\;-\\;P_{取2}\\;=\\;1\\;-\\;\\dfrac{1}{n-1}\\;=\\;\\dfrac{n-2}{n-1}}

    1P2=1n11=n1n2】,

    p
      

    =
      

    1

    n

    2

      


      

    n

    2

    n

    1

      


      

    n

    1

    n

      

    =
      

    1

    n

    {p\\;=\\;\\dfrac{1}{n-2}\\;*\\;\\dfrac{n-2}{n-1}\\;*\\;\\dfrac{n-1}{n}\\;=\\;\\dfrac{1}{n}}

    p=n21n1n2nn1=n1

    以此类推当n = 0时,p也等于

    1

    n

    {\\dfrac{1}{n}}

    n1

    batch设置

    让我们回到pytorch本身,对于DataLoader的batchSize,建议配置为32,如果您的内存 / 显存无法支持一次性加载这些资源,尝试将它缩减,等到可以存储的时候再放大,直到无法存储又缩减,然后可以又放大——这样循环往复后可以找到临界值(如果您了解二分思想的话,可以用二分来解决这个问题),如果您想调整比32更高的 加载数据 / batch,您也可以通过二分思想解决这个问题

    关于 batch 和 epoch 的区别

    您会在batch里面规定每一轮次加载多少张图片,而为什么epoch不要规定呢,事实上,您已经规定了,您还记得您编写的__len__方法吗,它会返回所有数据集的总量,事实上,这就是一个epoch,一个epoch覆盖了所有的图像【至少覆盖了整个数据集的训练集部分】,而batch就是为实现一个epoch所做的加载策略,每次取32个batch,您会在(一个epoch需要处理的总图像/ 32 + (一个epoch需要处理的总图像%32 > 0 ? 1 : 0) )个batch中完成一轮epoch的训练,您可以选择训练多个epoch,它会用您的训练集再次训练,不过由于shuffle,这次训练集的加载顺序与上一次的会不同

    code-clip

    train_data_size = int(len(dataset) * 0.7)
    valid_data_size = int(len(dataset) * 0.15)
    test_data_size = len(dataset) train_data_size valid_data_size

    train_data,valid_data,test_data = random_split(dataset,[train_data_size,valid_data_size,test_data_size])

    data_batch = DataLoader(dataset=train_data,batch_size=32,shuffle=True)
    valid_batch = DataLoader(dataset=valid_data,batch_size=32,shuffle=False)
    test_batch = DataLoader(dataset=test_data,batch_size=32,shuffle=False)

    print(f'Train Batch size: {len(train_data)}')
    print(f'Validation Batch size: {len(valid_data)}')
    print(f'Test Batch size: {len(test_data)}')
    for index,(images,labels) in enumerate(data_batch):
    if index > 175:
    print(f'the {index}th batch size is {images.shape}')

    防漏洞部署和数据增强

    数据增强

    如果您来自第四章——模块化架构,请点击此处返回,否则请忽略

    您是否有过这样的经历呢,当您在人脸识别的时候,不管是在明亮的地方还是在稍微昏暗的地方,设备总能精确识别到您的面部,从而为您开启权限,这在模型训练上的一个启发就是,模型能不能有一定的抗干扰因素,为了做到这一点,我们需要在模型训练的过程中,为训练集做一些数据增强(翻转,二值化……)的操作,期望模型在数据增强后依然能够识别图片

    transform = transforms.Compose([
    # 数据增强
    transforms.RandomHorizontalFlip(p=0.5), # 随机水平翻转
    transforms.RandomRotation(degrees=10), # 随机旋转
    transforms.ColorJitter(brightness=0.2), # 随机调整明暗度
    # 数据标准化
    transforms.Resize(256),
    transforms.CenterCrop(224),
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
    ])

    我们在应用数据变换的时候会随机对数据进行水平翻转、旋转、调整亮度等操作,以模拟同一个花卉在不同的环境和拍摄角度下的视觉呈现,就像人脸在明亮和昏暗的情况下,模型是否能正确识别这是同一人。

    然而我们只希望这样的变换应用在训练集中,而对测试集和验证集无效,因为如果对测试集和验证集应用相同的变换,就无法确定每一次epoch,模型的训练结果的变化仅仅是因为输入不同还是因为模型本身的性能得到改变。在我们现有的架构中,无法做到对不同的数据集使用不同的transform,因为我们的transform只在OxfordFlowersDataSet类被传入,随机分配产生的3个数据集,本身都是dataset的子集,应用和dataset一样的变换,可是如果我们在得到子集之后,再次应用不同的变换呢

    这是我们原来的架构

    #mermaid-svg-CcmAAKhOvDn78Qyo{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-CcmAAKhOvDn78Qyo .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-CcmAAKhOvDn78Qyo .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-CcmAAKhOvDn78Qyo .error-icon{fill:#552222;}#mermaid-svg-CcmAAKhOvDn78Qyo .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-CcmAAKhOvDn78Qyo .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-CcmAAKhOvDn78Qyo .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-CcmAAKhOvDn78Qyo .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-CcmAAKhOvDn78Qyo .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-CcmAAKhOvDn78Qyo .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-CcmAAKhOvDn78Qyo .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-CcmAAKhOvDn78Qyo .marker{fill:#333333;stroke:#333333;}#mermaid-svg-CcmAAKhOvDn78Qyo .marker.cross{stroke:#333333;}#mermaid-svg-CcmAAKhOvDn78Qyo svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-CcmAAKhOvDn78Qyo p{margin:0;}#mermaid-svg-CcmAAKhOvDn78Qyo .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-CcmAAKhOvDn78Qyo .cluster-label text{fill:#333;}#mermaid-svg-CcmAAKhOvDn78Qyo .cluster-label span{color:#333;}#mermaid-svg-CcmAAKhOvDn78Qyo .cluster-label span p{background-color:transparent;}#mermaid-svg-CcmAAKhOvDn78Qyo .label text,#mermaid-svg-CcmAAKhOvDn78Qyo span{fill:#333;color:#333;}#mermaid-svg-CcmAAKhOvDn78Qyo .node rect,#mermaid-svg-CcmAAKhOvDn78Qyo .node circle,#mermaid-svg-CcmAAKhOvDn78Qyo .node ellipse,#mermaid-svg-CcmAAKhOvDn78Qyo .node polygon,#mermaid-svg-CcmAAKhOvDn78Qyo .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-CcmAAKhOvDn78Qyo .rough-node .label text,#mermaid-svg-CcmAAKhOvDn78Qyo .node .label text,#mermaid-svg-CcmAAKhOvDn78Qyo .image-shape .label,#mermaid-svg-CcmAAKhOvDn78Qyo .icon-shape .label{text-anchor:middle;}#mermaid-svg-CcmAAKhOvDn78Qyo .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-CcmAAKhOvDn78Qyo .rough-node .label,#mermaid-svg-CcmAAKhOvDn78Qyo .node .label,#mermaid-svg-CcmAAKhOvDn78Qyo .image-shape .label,#mermaid-svg-CcmAAKhOvDn78Qyo .icon-shape .label{text-align:center;}#mermaid-svg-CcmAAKhOvDn78Qyo .node.clickable{cursor:pointer;}#mermaid-svg-CcmAAKhOvDn78Qyo .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-CcmAAKhOvDn78Qyo .arrowheadPath{fill:#333333;}#mermaid-svg-CcmAAKhOvDn78Qyo .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-CcmAAKhOvDn78Qyo .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-CcmAAKhOvDn78Qyo .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-CcmAAKhOvDn78Qyo .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-CcmAAKhOvDn78Qyo .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-CcmAAKhOvDn78Qyo .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-CcmAAKhOvDn78Qyo .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-CcmAAKhOvDn78Qyo .cluster text{fill:#333;}#mermaid-svg-CcmAAKhOvDn78Qyo .cluster span{color:#333;}#mermaid-svg-CcmAAKhOvDn78Qyo div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-CcmAAKhOvDn78Qyo .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-CcmAAKhOvDn78Qyo rect.text{fill:none;stroke-width:0;}#mermaid-svg-CcmAAKhOvDn78Qyo .icon-shape,#mermaid-svg-CcmAAKhOvDn78Qyo .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-CcmAAKhOvDn78Qyo .icon-shape p,#mermaid-svg-CcmAAKhOvDn78Qyo .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-CcmAAKhOvDn78Qyo .icon-shape .label rect,#mermaid-svg-CcmAAKhOvDn78Qyo .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-CcmAAKhOvDn78Qyo .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-CcmAAKhOvDn78Qyo .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-CcmAAKhOvDn78Qyo :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

    transform

    root

    OxfordFlowersDataSet

    train_set

    valid_set

    test_set

    这是我们可以尝试的新架构

    #mermaid-svg-aiDkG02SXvWH92YW{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-aiDkG02SXvWH92YW .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-aiDkG02SXvWH92YW .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-aiDkG02SXvWH92YW .error-icon{fill:#552222;}#mermaid-svg-aiDkG02SXvWH92YW .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-aiDkG02SXvWH92YW .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-aiDkG02SXvWH92YW .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-aiDkG02SXvWH92YW .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-aiDkG02SXvWH92YW .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-aiDkG02SXvWH92YW .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-aiDkG02SXvWH92YW .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-aiDkG02SXvWH92YW .marker{fill:#333333;stroke:#333333;}#mermaid-svg-aiDkG02SXvWH92YW .marker.cross{stroke:#333333;}#mermaid-svg-aiDkG02SXvWH92YW svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-aiDkG02SXvWH92YW p{margin:0;}#mermaid-svg-aiDkG02SXvWH92YW .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-aiDkG02SXvWH92YW .cluster-label text{fill:#333;}#mermaid-svg-aiDkG02SXvWH92YW .cluster-label span{color:#333;}#mermaid-svg-aiDkG02SXvWH92YW .cluster-label span p{background-color:transparent;}#mermaid-svg-aiDkG02SXvWH92YW .label text,#mermaid-svg-aiDkG02SXvWH92YW span{fill:#333;color:#333;}#mermaid-svg-aiDkG02SXvWH92YW .node rect,#mermaid-svg-aiDkG02SXvWH92YW .node circle,#mermaid-svg-aiDkG02SXvWH92YW .node ellipse,#mermaid-svg-aiDkG02SXvWH92YW .node polygon,#mermaid-svg-aiDkG02SXvWH92YW .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-aiDkG02SXvWH92YW .rough-node .label text,#mermaid-svg-aiDkG02SXvWH92YW .node .label text,#mermaid-svg-aiDkG02SXvWH92YW .image-shape .label,#mermaid-svg-aiDkG02SXvWH92YW .icon-shape .label{text-anchor:middle;}#mermaid-svg-aiDkG02SXvWH92YW .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-aiDkG02SXvWH92YW .rough-node .label,#mermaid-svg-aiDkG02SXvWH92YW .node .label,#mermaid-svg-aiDkG02SXvWH92YW .image-shape .label,#mermaid-svg-aiDkG02SXvWH92YW .icon-shape .label{text-align:center;}#mermaid-svg-aiDkG02SXvWH92YW .node.clickable{cursor:pointer;}#mermaid-svg-aiDkG02SXvWH92YW .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-aiDkG02SXvWH92YW .arrowheadPath{fill:#333333;}#mermaid-svg-aiDkG02SXvWH92YW .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-aiDkG02SXvWH92YW .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-aiDkG02SXvWH92YW .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-aiDkG02SXvWH92YW .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-aiDkG02SXvWH92YW .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-aiDkG02SXvWH92YW .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-aiDkG02SXvWH92YW .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-aiDkG02SXvWH92YW .cluster text{fill:#333;}#mermaid-svg-aiDkG02SXvWH92YW .cluster span{color:#333;}#mermaid-svg-aiDkG02SXvWH92YW div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-aiDkG02SXvWH92YW .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-aiDkG02SXvWH92YW rect.text{fill:none;stroke-width:0;}#mermaid-svg-aiDkG02SXvWH92YW .icon-shape,#mermaid-svg-aiDkG02SXvWH92YW .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-aiDkG02SXvWH92YW .icon-shape p,#mermaid-svg-aiDkG02SXvWH92YW .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-aiDkG02SXvWH92YW .icon-shape .label rect,#mermaid-svg-aiDkG02SXvWH92YW .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-aiDkG02SXvWH92YW .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-aiDkG02SXvWH92YW .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-aiDkG02SXvWH92YW :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

    transform_strengthen

    transform_base

    transform_base

    transform_strengthen

    transform_base

    transform_base

    root

    OxfordFlowersDataSet

    SubSet

    train_set

    valid_set

    test_set

    train_set

    valid_set

    test_set

    我们将获取与应用分离,OxfordFlowersDataSet类仅提供从原文件夹获取资源(传入root文件夹路径构造),而SubSet类专注于资源的加工(传入OxfordFlowersDataSet类分离出来的数据集作为加工对象,传入transform作为应用变换),这样就可以为三个数据集应用不同的transform

    让我们来试试

    class OxfordFlowersDataSet(Dataset):
    def __init__(self, root, target_transform=None):
    self.root = root
    self.img_dir = os.path.join(self.root,"img","jpg")

    labels_mat = scipy.io.loadmat(os.path.join(self.root,"imagelabels.mat"))

    self.labels = labels_mat["labels"][0] 1
    def __len__(self):
    return len(self.labels)
    def __getitem__(self, index):
    img_name = f'image_{index+1:05d}.jpg'
    img_path = os.path.join(self.img_dir,img_name)

    img = Image.open(img_path)
    labels = self.labels[index]

    return img, labels
    def info(self):
    print(len(self.labels))
    print(f"Max label: {max(self.labels)}")
    print(f"Min label: {min(self.labels)}")

    class SubSet(Dataset):
    def __init__(self, dataset,transform = None):
    self.dataset = dataset
    self.transform = transform

    def __len__(self):
    return len(self.dataset)

    def __getitem__(self, index):
    img,label = self.dataset[index]
    if self.transform is not None:
    img = self.transform(img)
    return img,label

    from sympy import false
    from torch.utils.data import DataLoader
    from torchvision import transforms
    import data_prepare
    from data_prepare import OxfordFlowersDataSet
    from data_prepare import SubSet
    from torch.utils.data import random_split

    transform_strengthen = transforms.Compose([
    transforms.RandomHorizontalFlip(p=0.5),
    transforms.RandomRotation(degrees=10),
    transforms.ColorJitter(brightness=0.2),

    transforms.Resize(256),
    transforms.CenterCrop(224),
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
    ])

    transform_base = transforms.Compose([
    transforms.Resize(256),
    transforms.CenterCrop(224),
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
    ])

    data_prepare.download_dataset()
    dataset = OxfordFlowersDataSet('flowers')

    train_data_size = int(len(dataset) * 0.7)
    valid_data_size = int(len(dataset) * 0.15)
    test_data_size = len(dataset) train_data_size valid_data_size

    train_data,valid_data,test_data = random_split(dataset,[train_data_size,valid_data_size,test_data_size])

    train_set = SubSet(train_data,transform_strengthen)
    valid_set = SubSet(valid_data,transform_base)
    test_set = SubSet(test_data,transform_base)

    data_batch = DataLoader(dataset=train_set,batch_size=32,shuffle=True)
    valid_batch = DataLoader(dataset=valid_set,batch_size=32,shuffle=False)
    test_batch = DataLoader(dataset=test_set,batch_size=32,shuffle=False)

    print(f'Train Batch size: {len(train_data)}')
    print(f'Validation Batch size: {len(valid_data)}')
    print(f'Test Batch size: {len(test_data)}')
    for index,(images,labels) in enumerate(data_batch):
    if index > 175:
    print(f'the {index}th batch size is {images.shape}')

    E:\\Users\\ASUS\\anaconda3\\envs\\aiAlgorithm\\python.exe E:\\pythonProject\\PythonProject\\data_handle.py
    Train Batch size: 5732
    Validation Batch size: 1228
    Test Batch size: 1229
    the 176th batch size is torch.Size([32, 3, 224, 224])
    the 177th batch size is torch.Size([32, 3, 224, 224])
    the 178th batch size is torch.Size([32, 3, 224, 224])
    the 179th batch size is torch.Size([4, 3, 224, 224])

    结果表示这确实可以被应用,但是问题时,我们仍然不知道transform.Compose([…])工作流内部到底发生了什么,他们真的被应用增强了吗?最好的方法是写一个可视化函数亲自看看图像被增强后的效果

    可视化图像验证

    我们将应用的图像转换成了张量,并进行了归一化,如果我们使用转换后的图像作为imshow的参数显示,这会造成色彩失真,因为imshow是根据值渲染图像的,我们之前提到,数值本身对于模型并没有意义,而是我们赋予的意义,他所做的工作是从数值的关系中找到特征,然后识别,imshow需要展示人可以辨认的色彩,这个时候值对于我们来说是有意义的【或者要给他缩放在一个人为规定的标准下】,所以我们需要对原本的图像反归一化

    您可以查阅我在第二章——数据摄取 & 数据准备对于归一化的解释,在这里将归一化的公式做一个逆向推导,就是我们将会使用到的反归一化

    n

    o

    r

    m

    a

    l

    i

    z

    e
      

    =
      

    o

    r

    i

    g

    i

    n
      


      

    m

    e

    a

    n

    s

    t

    d

    o

    r

    i

    g

    i

    n
      

    =
      

    (

    s

    t

    d
      


      

    n

    o

    r

    m

    a

    l

    i

    z

    e

    )
      

    +
      

    m

    e

    a

    n

    normalize\\;=\\;\\dfrac{origin\\;-\\;mean}{std}\\\\ origin\\;=\\;(std\\;*\\;normalize)\\;+\\;mean

    normalize=stdoriginmeanorigin=(stdnormalize)+mean 利用第二个逆推导,我们可以将归一化后的图像转换回原图像,但是在此之前,我们还需要做一些预处理

    对于mean和std来说,它们是列表类型无法与img:Tensor的类型做点乘,我们需要把它们的类型也变换为Tensor,tensor()提供了这样的转换,就像int(double)可以转换为double为int那样,我们可以尝试打印转换后的shape,它会显示torch.Size([3]),这仍然无法与torch,Size([3,224,224])大小的张量进行点乘 在这里插入图片描述

    然而我们可以将mean和std转换为[3,1,1]的大小,我们使用Tensor.view()方法,第一个参数为 -1 表示自动计算大小,第二个参数和第三个参数为1,表示转换后张量的第二层(行)和第三层(列)的大小各位1,3 = 1 * 1 * 3,经计算后,张量的一层大小为3(channel),这看来也无法进行点乘

    在这里插入图片描述

    但是您还记得张量的广播机制嘛,张量在进行运算的时候如果遇到行列不匹配的情况会尝试向行列进行广播,第二层为1会广播为img的第二层大小,第三层为1会广播为img的第三层的大小,具体可见张量的广播机制,所以在计算时,mean和std都会被广播成[3,224,224]大小 在这里插入图片描述

    from matplotlib import pyplot as plt
    import torch
    from torch import tensor

    from data_prepare import *
    from torch.utils.data import random_split
    from data_handle import transform_strengthen
    from torchvision import transforms

    def denormalize(img,mean,std):
    img = img.clone().detach().cpu()
    mean = tensor(mean).view(1,1,1)
    std = tensor(std).view(1,1,1)
    img = img*std + mean
    return img

    def vision_strengthen(img_set,strengthen_set,title,count=8):
    img_set = img_set

    fig,axes = plt.subplots(nrows=2,ncols=count,figsize=(12,6))
    axes = axes.flatten()

    for index in range(count):
    img,label = img_set[index]
    img_trans,label_trans = strengthen_set[index]
    img = transforms.ToTensor()(img)
    img_trans = denormalize(img_trans,mean=[0.485, 0.456, 0.406],std=[0.229, 0.224, 0.225])
    axes[index].imshow(img.permute(1,2,0))
    axes[count + index].imshow(img_trans.permute(1,2,0))

    axes[index].axis('off')
    axes[index].set_title(str(label))
    axes[count + index].axis('off')
    axes[count + index].set_title(str(label_trans))
    plt.suptitle(title)
    plt.tight_layout()
    plt.show()

    originSet = OxfordFlowersDataSet("flowers")

    trainSet_size = int(len(originSet) * 0.7)
    validSet_size = int(len(originSet) * 0.15)
    testSet_size = len(originSet) trainSet_size validSet_size

    originTrain,originValid,originTest = random_split(originSet,[trainSet_size,validSet_size,testSet_size])
    transformTrain = SubSet(originTrain,transform_strengthen)

    vision_strengthen(originTrain,transformTrain,"train-origin")

    从图像的结果来看,我们在训练集上的应用增强的确有效

    在这里插入图片描述

    防漏洞部署

    如果在模型训练的过程中发生图片损坏或者格式问题而引发的异常导致程序终止,这是令人崩溃的问题,模型训练是一个持久的过程,这意味修复错误,从头开始训练,防漏洞部署正是为解决这个问题而引出的方案,我们会想要如何排查错误呢?用if else将每个可能出错的情况列出来处理嘛,对于常见的错误这是有效的,但是如果错误实在非常隐蔽还是会因为没有考虑到所有情况而导致异常抛出,我们可以采用一种记录但不处理的策略

    try-except 异常捕捉机制能够帮助我们处理所有程序运行错误而导致的异常

    class OxfordFlowersDataSet(Dataset):
    def __init__(self, root, target_transform=None):
    self.root = root
    self.img_dir = os.path.join(self.root,"img","jpg")

    labels_mat = scipy.io.loadmat(os.path.join(self.root,"imagelabels.mat"))

    self.labels = labels_mat["labels"][0] 1
    def __len__(self):
    return len(self.labels)
    def __getitem__(self, index):
    try:
    img_name = f'image_{index+1:05d}.jpg'
    img_path = os.path.join(self.img_dir,img_name)

    img = Image.open(img_path)
    labels = self.labels[index]

    return img, labels
    except Exception as e:
    throw_error(e.__str__())
    def getImgDir(self,index):
    return os.path.join(self.img_dir,f'image_{index+1:05d}.jpg')
    def info(self):
    print(len(self.labels))
    print(f"Max label: {max(self.labels)}")
    print(f"Min label: {min(self.labels)}")

    class SubSet(Dataset):
    def __init__(self, dataset,transform = None):
    self.dataset = dataset
    self.transform = transform
    self.errLog = []

    def __len__(self):
    return len(self.dataset)

    def __getitem__(self, index):
    try:
    img,label = self.dataset[index]
    if self.transform is not None:
    img = self.transform(img)
    return img,label
    except Exception as e:
    self.errLog.append({
    'index':index,
    'error':str(e),
    'path':"unknow" if self.dataset.getImgDir(index) is None else self.dataset.getImgDir(index),
    })

    # 递归返回下一次数据
    return self.__getitem__( (index+1) % len(self))

    我们选择在发生异常的时候记录异常并递归调用下一次将要访问的数据,直到返回没有异常的数据,这不会再抛出异常了,而是会返回下一组将要访问数据的结果,这听起来很不错,但是它会造成一个问题,我们并没有事先通知模型我返回的是下一组数据,您不用再访问啦,模型会严格按照批次继续返回下一个数据,实际上这可能导致一组数据被多次访问。您可以考虑为SubSet加一个bool成员对象长度是dataset,记录每次是否被访问,如果被访问则通知模型继续下一次,如果没有则在return之前标记为访问并正常return

    if visted[index]:
    callTonext(model)
    return
    img,label = self.dataset[index]
    if self.transform is not None:
    img = self.transform(img)
    visted[index] = True
    return img,label

    事实上,我写的更像是伪代码,我不清楚模型是否能在不返回任何数据的情况下执行下一次训练而不抛出异常,如果我们这样做的话,是否会主动引发模型的异常呢,如果感兴趣的话您可以查阅更多资料探讨……但目前而言,这是防漏洞部署中比较稳妥的策略,即使可能带来多次访问,但是它可以随着本次异常的解决一起被解决,其实换种角度思考,异常都抛出了,我们已经让错误引发的代价降到很小了

    code

    # data_prepare
    import os
    import tarfile
    import urllib

    from PIL import Image
    from django.urls import path
    from numpy.f2py.auxfuncs import throw_error
    from sympy.codegen.ast import none
    from sympy.stats.sampling.sample_scipy import scipy
    import scipy.io
    from torch.utils.data import Dataset

    def download_dataset() > None:
    image_url = "https://www.robots.ox.ac.uk/~vgg/data/flowers/102/102flowers.tgz"
    labels_url = "https://www.robots.ox.ac.uk/~vgg/data/flowers/102/imagelabels.mat"

    os.makedirs("flowers",exist_ok=True)
    if not os.path.exists(os.path.join("flowers","img","102flowers.tgz")):
    urllib.request.urlretrieve(image_url, os.path.join("flowers","img",os.path.basename(image_url)))
    with tarfile.open(os.path.join("flowers", "img", "102flowers.tgz")) as tg:
    tg.extractall(os.path.join("flowers", "img"))
    if not os.path.exists(os.path.join("flowers","imagelabels.mat")):
    urllib.request.urlretrieve(labels_url, os.path.join("flowers",os.path.basename(labels_url)))

    class OxfordFlowersDataSet(Dataset):
    def __init__(self, root, target_transform=None):
    self.root = root
    self.img_dir = os.path.join(self.root,"img","jpg")

    labels_mat = scipy.io.loadmat(os.path.join(self.root,"imagelabels.mat"))

    self.labels = labels_mat["labels"][0] 1
    def __len__(self):
    return len(self.labels)
    def __getitem__(self, index):
    try:
    img_name = f'image_{index+1:05d}.jpg'
    img_path = os.path.join(self.img_dir,img_name)

    img = Image.open(img_path)
    labels = self.labels[index]

    return img, labels
    except Exception as e:
    throw_error(e.__str__())
    def getImgDir(self,index):
    return os.path.join(self.img_dir,f'image_{index+1:05d}.jpg')
    def info(self):
    print(len(self.labels))
    print(f"Max label: {max(self.labels)}")
    print(f"Min label: {min(self.labels)}")

    class SubSet(Dataset):
    def __init__(self, dataset,transform = None):
    self.dataset = dataset
    self.transform = transform
    self.errLog = []

    def __len__(self):
    return len(self.dataset)

    def __getitem__(self, index):
    try:
    img,label = self.dataset[index]
    if self.transform is not None:
    img = self.transform(img)
    return img,label
    except Exception as e:
    self.errLog.append({
    'index':index,
    'error':str(e),
    'path':"unknow" if self.dataset.getImgDir(index) is None else self.dataset.getImgDir(index),
    })

    return self.__getitem__(index+1 % len(self))

    # data_handler
    from sympy import false
    from torch.utils.data import DataLoader
    from torchvision import transforms
    import data_prepare
    from data_prepare import OxfordFlowersDataSet
    from data_prepare import SubSet
    from torch.utils.data import random_split

    transform_strengthen = transforms.Compose([
    transforms.RandomHorizontalFlip(p=0.5),
    transforms.RandomRotation(degrees=10),
    transforms.ColorJitter(brightness=0.2),

    transforms.Resize(256),
    transforms.CenterCrop(224),
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
    ])

    transform_base = transforms.Compose([
    transforms.Resize(256),
    transforms.CenterCrop(224),
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
    ])

    data_prepare.download_dataset()
    dataset = OxfordFlowersDataSet('flowers')

    train_data_size = int(len(dataset) * 0.7)
    valid_data_size = int(len(dataset) * 0.15)
    test_data_size = len(dataset) train_data_size valid_data_size

    train_data,valid_data,test_data = random_split(dataset,[train_data_size,valid_data_size,test_data_size])

    train_set = SubSet(train_data,transform_strengthen)
    valid_set = SubSet(valid_data,transform_base)
    test_set = SubSet(test_data,transform_base)

    data_batch = DataLoader(dataset=train_set,batch_size=32,shuffle=True)
    valid_batch = DataLoader(dataset=valid_set,batch_size=32,shuffle=False)
    test_batch = DataLoader(dataset=test_set,batch_size=32,shuffle=False)

    if __name__ == "__main__":
    print(f'Train Batch size: {len(train_data)}')
    print(f'Validation Batch size: {len(valid_data)}')
    print(f'Test Batch size: {len(test_data)}')
    for index,(images,labels) in enumerate(data_batch):
    if index > 175:
    print(f'the {index}th batch size is {images.shape}')

    Congratulations!!!

    通过第三章的学习,您掌握了

    • 模型训练的数据流程是怎样的
    • 我们需要解决数据流程中遇到的哪些问题
    • 如何解决在这些数据流程中遇到的问题

    第四章

    我们已经能够正确的处理图片数据,接下来,我们是时候把它交给模型,让训练器从图片提取特征并训练出可以识别图像的模型了。我们平常是怎么识别图像呢?这让我想起来了多年前在语文课本上学的一个民间故事并被《铁齿铜牙纪晓岚》演绎的情景

    《铁齿铜牙纪晓岚》中经典对话

    纪晓岚当时为侍郎一职。 和珅:这是何物啊,是狼是狗(侍郎是狗)?

    其中一个御史附和:侍郎,是狗

    纪晓岚:您堂堂一大学士,是狼是狗分不清,我教教您,

    “这尾巴下摆的狼,上竖的是狗,尚书是狗“

    御史:狼吃肉,狗吃粪,它吃肉,侍郎是狗,毫无疑问

    纪晓岚:狗和人不一样,它是遇肉吃肉,遇屎吃屎,御史吃屎。

    文章来自《铁齿铜牙纪晓岚》中经典对话 – 知乎

    这是一件小趣事,但是我们能从中看到各位官员是如何分辨狼和狗的,和珅等官员通过尾巴这一特征判断,纪晓岚则通过生活习性来判断,它们都是狼和狗身上的一些显著特征,人可以通过特征对一张照片进行分类,卷积神经网络(CNN)正是在这一生物学事实下启发,通过卷积核等工具进行特征扫描,再经过激活、池化等一系列步骤判断一张照片属于什么类别

    CNN卷积神经网络——卷积

    原理

    在卷积神经网络中,我们不再聚焦于分析一个个像素点,而是分析一个区域是否能和卷积核的特征有较高的相似度。就像我们关注图片时不会关注内部的一个像素点是什么,而是关注图片中有什么(特征),除非您在学习图片相关的工作原理或者对图片进行像素级的处理

    卷积核能够帮助我们在图像中扫描特征,它又被称为滤波器,就像我们会着重关注图像的某一个具体特征(我们的大脑并行处理能力极强,能够瞬间捕捉很多特征并综合分析),卷积核就像大脑中的一个分析器,对于一张猫的图片,他可能会有一个专门的卷积核分析猫的耳朵,或者分析猫的爪子。这样的分析工作是由一个特定的矩阵来完成的

    [

    1

    0

    1

    2

    0

    2

    1

    0

    1

    ]

    \\begin{bmatrix} -1 && 0 && 1\\\\ -2 && 0 && 2\\\\ -1 && 0 && 1 \\end{bmatrix}

    121000121

    这看起来只是一个矩阵,它该如何作用于图像呢,它的工作原理是这样的

  • 初始:将卷积核作用于图片的其实区域,左上角 3 * 3的像素网格【并不严格一致,有时会让卷积核中心位于最左上角的一个像素,并将其他超出图片区域的像素设置为一个固定的值,一般是0,这使得它可以和其他非特殊情况一样进行卷积工作】(起始边缘)
  • 对图像区域【截取和卷积核一样大小的一个矩阵】和卷积核点乘并将所有点乘结果相加
  • 让卷积核在图像上以固定的步长向右滑动继续进行第二步,直到达到图片右侧边缘后,让图片回到该操作的起始边缘,以固定步长下滑,更新起始边缘为当前图像区域与卷积核作用区域,继续进行第二步
  • 当图像的下一次固定步长的卷积核中心的行索引超过图像底部的行索引时,结束卷积操作 在这里插入图片描述 这看起来像是矩阵版的滑动窗口,不过我们为什么要这么做,或许我们可以模拟CNN的工作过程,看看究竟发生了什么
  • 我们尝试将一个卷积核应用在不同的图像截取矩阵呢,观察不同的图像的计算结果会有什么不同 $$ \\begin{bmatrix} 1 && 1 && 1\\ 1 && 1 && 1\\ 1 && 1 && 1 \\end{bmatrix} \\cdot \\begin{bmatrix} -1 && 0 && 1\\ -2 && 0 && 2\\ -1 && 0 && 1 \\end{bmatrix}

    \\begin{bmatrix} -1 && 0 && 1\\ -2 && 0 && 2\\ -1 && 0 && 1 \\end{bmatrix} ->0\\ \\ \\begin{bmatrix} -1 && 0 && 1\\ -2 && 0 && 2\\ -1 && 0 && 1 \\end{bmatrix} \\cdot \\begin{bmatrix} -1 && 0 && 1\\ -2 && 0 && 2\\ -1 && 0 && 1 \\end{bmatrix}

    \\begin{bmatrix} 1 && 0 && 1\\ 4 && 0 && 4\\ 1 && 0 && 1 \\end{bmatrix} ->12 \\ \\ \\begin{bmatrix} -1 && 0 && 1\\ -2 && 0 && 2\\ -1 && 0 && 1 \\end{bmatrix} \\cdot \\begin{bmatrix} 1 && 0 && -1\\ 2 && 0 && -2\\ 1 && 0 && -1 \\end{bmatrix}

    \\begin{bmatrix} -1 && 0 && -1\\ -4 && 0 && -4\\ -1 && 0 && -1 \\end{bmatrix} ->-12 $$

    如果我们通过数值比较矩阵点乘后相加的结果,我们可以看到常数矩阵的计算结果为0,而与卷积核相等的矩阵,他们的计算结果数值较高,而与卷积核在水平翻转后的矩阵,与它进行计算后的结果较低,如果我们激活与卷积核运算数值较高的矩阵【在这里表现为与卷积核相等】,那么当卷积核在原始图像上滑动时,它对于卷积核相等的响应会更激烈,从而使模型找到正确的特征(与卷积核高度相似),事实上,这种表示并不完全在体现在与卷积核数值全等的图像截取,我们可以让中间的矩阵再通用一些 $$ \\begin{bmatrix} -ax && 0 && dx\\ -by && 0 && ey\\ -cz && 0 && fz \\end{bmatrix} \\cdot \\begin{bmatrix} -x && 0 && x\\ -y && 0 && y\\ -z && 0 && z \\end{bmatrix}

    \\begin{bmatrix} ax^2 && 0 && dx^2\\ by^2 && 0 && ey^2\\ cz^2 && 0 && fz^2 \\end{bmatrix} ->(a+d)x2;+;(b+e)y2;+;(c+f)z^2\\ \\ a、b、c、d、e、f;均为正数 KaTeX parse error: Expected 'EOF', got '#' at position 85: …第一章——优化单神经元模型](#̲优化单神经元模型))激活 \\begin{bmatrix} ax && 0 && -dx\\ by && 0 && -ey\\ cz && 0 && -fz \\end{bmatrix} \\cdot \\begin{bmatrix} -x && 0 && x\\ -y && 0 && y\\ -z && 0 && z \\end{bmatrix}

    \\begin{bmatrix} -ax^2 && 0 && -dx^2\\ -by^2 && 0 && -ey^2\\ -cz^2 && 0 && -fz^2 \\end{bmatrix} ->-(a+d)x2;-;(b+e)y2;-;(c+f)z^2\\\\ a、b、c、d、e、f;均为正数 $$ 相反,当系统为正数、即图像截取方向与卷积核方向完全相反时,我们能得到最小的值,而且往反方向偏离的越多,值越小,它会被Relu激活函数设置为0。

    如果卷积核中数值的正负代表特征一致或者相反这种方向,那么数值就类似权重,如果在权重较大的地方,图片截取的某一像素犯了方向的错误,那么这对整体结果的惩罚程度会较大(更加偏离特征相似)如果像素在权重较大的地方方向对了,那么这对整体结果的奖励力度也会越大 $$ \\begin{bmatrix} -1 && 0 && 1\\ 2 && 0 && -2\\ -1 && 0 && 1 \\end{bmatrix} \\cdot \\begin{bmatrix} -1 && 0 && 1\\ -2 && 0 && 2\\ -1 && 0 && 1 \\end{bmatrix}

    \\begin{bmatrix} 1 && 0 && 1\\ -4 && 0 && -4\\ 1 && 0 && 1 \\end{bmatrix} ->-4

    对于这个例子,图像截取在矩阵点乘的许多方向与卷积核保持一致,但是在权重最大的两个方向相反,这导致原本较大的数值——

    12

    变成了负数

    对于这个例子,图像截取在矩阵点乘的许多方向与卷积核保持一致,但是在权重最大的两个方向相反,这导致原本较大的数值——12变成了负数

    对于这个例子,图像截取在矩阵点乘的许多方向与卷积核保持一致,但是在权重最大的两个方向相反,这导致原本较大的数值——12变成了负数 \\begin{bmatrix} 1 && 0 && -1\\ -2 && 0 && 2\\ 1 && 0 && -1 \\end{bmatrix} \\cdot \\begin{bmatrix} -1 && 0 && 1\\ -2 && 0 && 2\\ -1 && 0 && 1 \\end{bmatrix}

    \\begin{bmatrix} -1 && 0 && -1\\ 4 && 0 && 4\\ -1 && 0 && -1 \\end{bmatrix} ->4 $$ 对于这个例子,图像截取在矩阵点乘的许多方向与卷积核相反,但是在权重最大的两个方向一致,这导致原本较小的数值变得较大——-12变成了正数

    tips:在日常CNN训练中,我们一般采用小卷积核(3*3 | 5*5),因为在许多图片中,相邻像素之间的差距不会特别大,以此保持较为稳定的图像截取与卷积核相乘,避免特别大的权重的干扰

    pytorch对应

    您不用亲自为每一个特征涉及卷积核,pytorch在模型训练的过程中会通过一系列调整——矫正(通过改变参数和应用损失函数等),让我们看看如何在pytorch中做到这一点

    from torch import nn
    conv_layer = nn.Conv2d(
    in_channels=3,
    out_channels=16,
    kernel_size=3,
    stride=1,
    padding=1,
    )

    torch模块当中的nn模块的Conv2d方法可以帮助我们构建卷积层,其中的参数

    in_channels:这代表输入图片的通道数,对于我们RGB图像来说,正好是R、G、B三个通道

    out_channels:这代表输出图片的通道数,每一个通道存储一个卷积核卷积后的结果,总共会有out_channels个逻辑卷积核(【in_channels * 单通道卷积核】作为一个整体作用在一张图片上)

    kernel_size:卷积核大小,是kerner_size * kernersize大小的单通道卷积核

    stride:步长,卷积核每次在图像上向右和向下滑动的距离,对于步长来说,如果您设置的较大,则对应的卷积完成速度也会快,但是这会让模型错失一些像素的卷积计算,导致错过一些细节特征

    padding:内边距,当您想要让卷积核中心位于图像角落的时候,您需要设置,以避免矩阵行索引 + 矩阵列索引 < 矩阵中心的行索引 + 矩阵中心列索引时无法找到有效的像素值相乘,值一般表示需要多少层padding,一般是(kernel_size – 1) / 2

    CNN卷积神经网络——池化

    接下来让我们放松心情,来一场卷积之间的大比拼,胜者来自池化比拼的佼佼者,他们将进入下一轮的卷积

    哈哈,这听起来令人心潮澎湃,实际上,这便是池化所做的事情,他像是一个围栏,将卷积后的图像围城一个个决斗场,符合判赢规则的选手,进入下一次比赛,这是来自于训练模型的优中选优,您作为裁判,可以制定判赢规则,常见的规则是从里面选值最大的那一位像素选手,最仁慈的规则,当然是全部接收啦,这个时候,决斗圈(或者是团队)中的每个像素都贡献自己的一份力量,取平均值上交给训练模型。模型期望能够选出和特征最相近的,比赛是残酷的,最常见的模式就是“决斗场”模式,围栏取最大值,至于围栏的大小就是kernelsize * kernelsize的池化核大小了 在这里插入图片描述

    池化会减小图像面积,让图像重点关注特征相似的部分,从而达到减少噪声以及提升模型训练速度的效果

    CNN卷积神经网络——完整架构实现

    class SimpleCNN(nn.Module):
    def __init__(self):
    super().__init__()
    self.conv1 = nn.Conv2d(3, 32, kernel_size=3,padding=1)
    self.relu1 = nn.ReLU()
    self.pool1 = nn.MaxPool2d(kernel_size=2,stride=2)

    self.conv2 = nn.Conv2d(32, 64, kernel_size=3, padding=1)
    self.relu2 = nn.ReLU()
    self.pool2 = nn.MaxPool2d(kernel_size=2,stride=2)

    self.conv3 = nn.Conv2d(64, 128, kernel_size=3, padding=1)
    self.relu3 = nn.ReLU()
    self.pool3 = nn.MaxPool2d(kernel_size=2,stride=2)

    self.flatten = nn.Flatten()

    self.fc1 = nn.Linear(128 * 28 * 28, 512)
    self.relu4 = nn.ReLU()
    self.dropout = nn.Dropout(0.5)
    self.fc2 = nn.Linear(512, 102)

    我们定义一个类,让它继承自nn,Module,这意味着在正式训练的时候,它会被实例化为一个模型对象,关于完整的模型构建,您可以查看ML&模型(构建模型、训练),模型构建的步骤是相同的,都是先通过__init__构造函数定义模型层以及每一层所要做的工作,然后在forward函数中应用模型层进行训练,关于nn.MaxPool2d这个函数,它是用来构建池化层的,参数kernel_size代表池化核的大小,stride代表步长,他一般和kernel_size保持一致,它的作用和卷积的按步长滑动是一样的,当步长等于核的尺寸时,刚好往方向滑动一个核的大小(滑过自身),从而避免重叠【重叠可能导致一个特征值被多次包含】,最后的步骤就是将我们的多通道图像应用于全连接层,全连接层接收图像,并构建多项式来拟合模型特征

    在整个模型构建的过程中,我们需要额外关注一些参数设置

  • conv1的in_channel等于原始图像的channel,而每一次的out_channel作为下一次卷积的in_channel,我们在forward函数中一般按照顺序构建,第一层的卷积后的结果特征图在池化后会进入下一轮卷积,确保上一轮的卷积输出通道数与当前轮的卷积输入通道数保持一致【池化并不改变通道大小】

  • 全连接层的第一层输入节点数应该等于当前通道数 * 图形尺寸(height * width),每经过一层池化,图像尺寸都会变化为

    图像尺寸
      

    =
      

    h

    e

    i

    g

    h

    t

    :

    o

    r

    i

    g

    i

    n

    H

    e

    i

    g

    h

    t

    p

    o

    o

    l

    k

    e

    r

    n

    e

    l

      


      

    w

    i

    d

    t

    h

    :

    o

    r

    i

    g

    i

    n

    W

    i

    d

    t

    h

    p

    o

    o

    l

    k

    e

    r

    n

    e

    l

    {图像尺寸\\;=\\;height:\\dfrac{originHeight}{poolkernel}\\;*\\;width:\\dfrac{originWidth}{poolkernel}}

    图像尺寸=height:poolkerneloriginHeightwidth:poolkerneloriginWidth

  • 最后一层连接层为输出层,输出节点数应该等于分类数量【在分类任务中】

  • tips:激活函数是为了拟合非线性的多种情况而做的策略,当输出层输出节点后,我们并不需要再设置激活函数,此时函数不再拟合,而是作为一次训练结果输出

    tips:您需要将图像展平为一维数据,这是全连接节点可以接受的输入通道shape,展平后的shape虽然与原图像不一致,但是总像素数量是一致的,为当前通道数 * 图形尺寸(height * width)

    Dropout在模型构建中有什么作用?

    我们在看到描写夏天的诗句时,会听到“小荷才露尖尖角,早有蜻蜓立上头”,好像一想到荷花,脑海中就浮现出烈日炎炎的夏日,池塘里的那一片遮阳伞,可是,如果荷花不出现在池塘,而是出现在……冰箱呢😀

    我小时候喜欢吃荷叶饭,商贩们可能会从冰箱拿出自己包好的荷叶饭放到蒸笼里蒸熟,模型在看到荷叶饭的荷叶时会识别到这是荷叶嘛?这就涉及到模型的识别模式了,在训练过程中,如果荷叶总是出现在烈日炎炎的池塘里,模型就会认为有夏天的图片就是荷花,这种现象被称为协同适应现象,Dropout就是解决这一问题的,它会在训练的时候随机关闭一部分神经元,以避免其他神经元或者识别模式过分依赖某一单独的神经元判断,比如它会关闭阳光强度神经元,训练在不依赖这一神经元的情况下,模型是否能够准备识别荷花,以鼓励神经元不依赖其他神经元的结果【让单一神经元的权重占比不再过于巨大以产生决定性作用】

    from torch import nn
    class SimpleCNN(nn.Moudle):
    def __init(self):
    super.__init__()
    #……
    def forward(self, x):
    x = self.conv1(x)
    x = self.relu1(x)
    x = self.pool1(x)

    x = self.conv2(x)
    x = self.relu2(x)
    x = self.pool2(x)

    x = self.conv3(x)
    x = self.relu3(x)
    x = self.pool3(x)

    x = self.flatten(x)

    x = self.fc1(x)
    x = self.relu4(x)
    x = self.dropout(x)
    x = self.fc2(x)
    return x

    在forward中,我们就可以直接应用在__init__中构建的那些节点了

    您还记得我们在第二章提到的[交叉熵损失函数](#交叉熵损失函数(Cross Entropy loss))和Adam优化器嘛,定义好这些诊断——调整工具之后(我们就可以开始尝试训练一个真实可用的模型了)

    adam = optim.Adam(model.parameters(),lr=0.001)
    loss_func = nn.CrossEntropyLoss()

    code

    import torch
    from sympy import gamma
    from torch import nn, device
    from torch.utils.data import random_split, DataLoader
    from torchvision import transforms

    from cnn import *
    from torch import optim
    from data_prepare import *

    def model_init():
    # 数据准备
    download_dataset()
    all_dataset = OxfordFlowersDataSet('flowers')

    train_size = int(len(all_dataset) * 0.7)
    valid_size = int(len(all_dataset) * 0.15)
    test_size = len(all_dataset) train_size valid_size

    transform_simple = transforms.Compose([
    transforms.Resize(256),
    transforms.CenterCrop(224),
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485, 0.456, 0.406],std=[0.229, 0.224, 0.225])
    ])

    transform_strengthen = transforms.Compose([
    transforms.RandomHorizontalFlip(p=0.5),
    transforms.RandomRotation(degrees=10),
    transforms.ColorJitter(brightness=0.2),
    transforms.Resize(256),
    transforms.CenterCrop(224),
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
    ])

    train_origin_set,valid_origin_set,test_origin_set = random_split(all_dataset,[train_size,valid_size,test_size])

    train_use_set = SubSet(train_origin_set,transform_strengthen)
    valid_use_set = SubSet(valid_origin_set,transform_simple)
    test_use_set = SubSet(test_origin_set,transform_simple)

    train_loader = DataLoader(train_use_set,batch_size=32,shuffle=True)
    valid_loader = DataLoader(valid_use_set,batch_size=32,shuffle=False)
    test_loader = DataLoader(test_use_set,batch_size=32,shuffle=False)

    #定义训练所需的工具
    device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
    model = SimpleCNN().to(device)
    optimizer = optim.Adam(model.parameters(), lr=0.001,weight_decay=1e-4)
    loss_func = nn.CrossEntropyLoss()

    return train_loader,valid_loader,test_loader,model,device,optimizer,loss_func

    def train_epoch(train_loader,model,device,optimizer,loss_func):

    train_counter = 0
    running_loss = 0.0
    running_correct = 0.0
    record_print = 0
    for index,(img,label) in enumerate(train_loader):
    img = img.to(device)
    label = label.to(device)

    optimizer.zero_grad()
    output = model(img)
    loss = loss_func(output,label)
    loss.backward()
    optimizer.step()

    running_loss += loss.item()
    _,predict = output.max(1)
    running_correct += (predict == label).sum().item()
    train_counter += label.size(0)
    record_print += label.size(0)

    if train_counter % 800 == 0 or index == len(train_loader) 1:
    accuracy = (running_correct / record_print) * 100
    loss = running_loss / record_print

    print(f'the {train_counter} batch : accuracy is {accuracy:.3f}% and loss is {loss:.3f}')

    running_correct = 0
    running_loss = 0
    record_print = 0

    def valid_epoch(valid_loader,model,device):
    model.eval()
    correct = 0
    total = 0
    with torch.no_grad():
    for img,label in valid_loader:
    img = img.to(device)
    label = label.to(device)
    output = model(img)
    _,predict = output.max(1)
    correct += (predict == label).sum().item()
    total += label.size(0)
    accuracy = (correct / total) * 100
    return accuracy

    if __name__ == '__main__':
    train_loader,valid_loader,test_loader,model,device,optimizer,loss_function= model_init()
    epochs = 10
    for item in range(epochs):
    train_epoch(train_loader,model,device,optimizer,loss_function)
    print(f'the {item + 1} epoch\\'s accuracy is {valid_epoch(valid_loader,model,device)}% ')

    静态构建和动态计算图

    回顾我们所有训练的模型,不管是快递模型还是MNIST数据集识别模型甚至是我们在上一章编写的Oxford卷积神经网络模型,它们都有一个共同点,所有模型的训练路径是确定的,输入节点是什么,先经历哪些层,后经历哪些层,各个层的参数该怎么确定,它们看起来像一个精密的数学公式,这在训练模型中被称为静态图

    对于基于静态图的模型,我们也可以使用nn.Sequential()方法来定义

    nn.Sequential(
    nn.Conv2d(3,32,3)
    nn.ReLU(),
    nn.Conv2d(32,64,3),
    nn.ReLU(),
    nn.Flatten(),
    nn.Linear(64 * 26 * 26,10)
    )

    它的构建流程和我们现阶段在__init__函数中所构建的一致 $$

  • ;;nn.Conv2d()->out(N_i,C_{out_j});=;bias(C_{out_j});+;\\sum_{k=0}^{C_{in}-1}weight(C_{out_j},k);*;input(N_i,k)\\
  • ;;ReLU(x);=;(x)^+;=;max(0,x) $$
  • 公式一是卷积的数学公式,其中

    N

    i

    {N_i}

    Ni表示第i张图片,

    C

    o

    u

    t

    j

    {C_{out_{j}}}

    Coutj表示一个逻辑卷积核【channel * 单层二维卷积核】,在等式右边我们对所有的图像输入通道卷积,再将所有单层通道的卷积核相加,使多个通道的数值相加形成一个结果特征通道,这个行为在公式中表示为

    k

    =

    0

    C

    i

    n

    1

    (

    .

    .

    .

    )

    {\\sum_{k=0}^{C_{in}-1}(…)}

    k=0Cin1(),而求和当中的每一项就是逻辑卷积核的第k层和图像中的第k层通道逐元素相乘,这个公式是对于单像素的卷积单位运算,可以以行索引和列索引创建循环,使其运用于整个图像,bias(

    C

    o

    u

    t

    j

    {C_{out_j}}

    Coutj)是逻辑卷积核的偏置项,用于对整个卷积进行微调

  • 公式二是ReLU激活函数的数学公式,用于规定最小值为0【max(0,x)】,在逻辑上等价于将但神经元输出 <= 0的部分设置为0,使它不对整体结果产生影响(不激活),而 > 0的神经元输出会按照它原本的工作方式对整体结果产生影响
  • 展平和全连接模型也在数学上分别对应矩阵变换和构建多项式,对于这样一个由nn.Sequential构建的模型训练过程或者现阶段我们在继承nn.Module类中所构建的模型层都是基于这样一种顺序的,能够用确切数学公式表达的多层训练模型,如果将这样一个过程画成一个概念图,它大概会是这样子的

    #mermaid-svg-ATRKOz9tpoZZAzl4{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ATRKOz9tpoZZAzl4 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ATRKOz9tpoZZAzl4 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ATRKOz9tpoZZAzl4 .error-icon{fill:#552222;}#mermaid-svg-ATRKOz9tpoZZAzl4 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ATRKOz9tpoZZAzl4 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ATRKOz9tpoZZAzl4 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ATRKOz9tpoZZAzl4 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ATRKOz9tpoZZAzl4 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ATRKOz9tpoZZAzl4 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ATRKOz9tpoZZAzl4 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ATRKOz9tpoZZAzl4 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ATRKOz9tpoZZAzl4 .marker.cross{stroke:#333333;}#mermaid-svg-ATRKOz9tpoZZAzl4 svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ATRKOz9tpoZZAzl4 p{margin:0;}#mermaid-svg-ATRKOz9tpoZZAzl4 .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-ATRKOz9tpoZZAzl4 .cluster-label text{fill:#333;}#mermaid-svg-ATRKOz9tpoZZAzl4 .cluster-label span{color:#333;}#mermaid-svg-ATRKOz9tpoZZAzl4 .cluster-label span p{background-color:transparent;}#mermaid-svg-ATRKOz9tpoZZAzl4 .label text,#mermaid-svg-ATRKOz9tpoZZAzl4 span{fill:#333;color:#333;}#mermaid-svg-ATRKOz9tpoZZAzl4 .node rect,#mermaid-svg-ATRKOz9tpoZZAzl4 .node circle,#mermaid-svg-ATRKOz9tpoZZAzl4 .node ellipse,#mermaid-svg-ATRKOz9tpoZZAzl4 .node polygon,#mermaid-svg-ATRKOz9tpoZZAzl4 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ATRKOz9tpoZZAzl4 .rough-node .label text,#mermaid-svg-ATRKOz9tpoZZAzl4 .node .label text,#mermaid-svg-ATRKOz9tpoZZAzl4 .image-shape .label,#mermaid-svg-ATRKOz9tpoZZAzl4 .icon-shape .label{text-anchor:middle;}#mermaid-svg-ATRKOz9tpoZZAzl4 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-ATRKOz9tpoZZAzl4 .rough-node .label,#mermaid-svg-ATRKOz9tpoZZAzl4 .node .label,#mermaid-svg-ATRKOz9tpoZZAzl4 .image-shape .label,#mermaid-svg-ATRKOz9tpoZZAzl4 .icon-shape .label{text-align:center;}#mermaid-svg-ATRKOz9tpoZZAzl4 .node.clickable{cursor:pointer;}#mermaid-svg-ATRKOz9tpoZZAzl4 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-ATRKOz9tpoZZAzl4 .arrowheadPath{fill:#333333;}#mermaid-svg-ATRKOz9tpoZZAzl4 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ATRKOz9tpoZZAzl4 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ATRKOz9tpoZZAzl4 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ATRKOz9tpoZZAzl4 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ATRKOz9tpoZZAzl4 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ATRKOz9tpoZZAzl4 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-ATRKOz9tpoZZAzl4 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ATRKOz9tpoZZAzl4 .cluster text{fill:#333;}#mermaid-svg-ATRKOz9tpoZZAzl4 .cluster span{color:#333;}#mermaid-svg-ATRKOz9tpoZZAzl4 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ATRKOz9tpoZZAzl4 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ATRKOz9tpoZZAzl4 rect.text{fill:none;stroke-width:0;}#mermaid-svg-ATRKOz9tpoZZAzl4 .icon-shape,#mermaid-svg-ATRKOz9tpoZZAzl4 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ATRKOz9tpoZZAzl4 .icon-shape p,#mermaid-svg-ATRKOz9tpoZZAzl4 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-ATRKOz9tpoZZAzl4 .icon-shape .label rect,#mermaid-svg-ATRKOz9tpoZZAzl4 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ATRKOz9tpoZZAzl4 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ATRKOz9tpoZZAzl4 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ATRKOz9tpoZZAzl4 :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

    L2

    f * w = g

    g + b_g = h

    ….

    L1

    a * b = c

    c + d = e

    max(0,e) = f

    ->表示模型推理时的方向;—>表示模型根据结果反馈调整的方向

    图示的固定模式就是我们现阶段所做的工作,它遵循以下几点约束

  • 一切操作都按照固定流程进行
  • 不能打印中间值调试
  • 没有分支和循环根据条件灵活调整
  • 这样的工作方式非常适合在已知确切计算图 / 计算公式的时候精确控制参数和流程,以达到理论上的速度和内存效率,但代价是会放弃灵活性【因为以固定流程运行】

    有没有一种方式是能够让我们可以像写python程序那样构建我们的模型呢,比如根据条件返回

    def forward(self,x):
    if self.is_flower(x):
    return self.flower_layers(x)
    else:
    return self.butterfly_layers(x)

    我希望一个模型可以同时训练花卉和蝴蝶,或者当一个数据集同时包含花卉和蝴蝶的时候,模型依然可以准确的识别它们呢,我们会为不同的品种构建不同的训练层【比如花卉的图片形状是(3,224,224),而蝴蝶的图片形状是(3,256,256)】,pytorch的动态计算图可以帮我们做到这一点

    和静态构建一样,动态计算图也是一种行为模式,您无需进行任何额外设置,无需对现有的设计模式大动干戈,只需像写日常工具那样进行判断

    上述根据对象方法判断输入图像的类别从而调用不同的构建层并返回结果,这在pytorch的训练模式中是这样体现的

    #mermaid-svg-WZnUZd0vdRWLHx5W{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-WZnUZd0vdRWLHx5W .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-WZnUZd0vdRWLHx5W .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-WZnUZd0vdRWLHx5W .error-icon{fill:#552222;}#mermaid-svg-WZnUZd0vdRWLHx5W .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-WZnUZd0vdRWLHx5W .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-WZnUZd0vdRWLHx5W .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-WZnUZd0vdRWLHx5W .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-WZnUZd0vdRWLHx5W .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-WZnUZd0vdRWLHx5W .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-WZnUZd0vdRWLHx5W .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-WZnUZd0vdRWLHx5W .marker{fill:#333333;stroke:#333333;}#mermaid-svg-WZnUZd0vdRWLHx5W .marker.cross{stroke:#333333;}#mermaid-svg-WZnUZd0vdRWLHx5W svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-WZnUZd0vdRWLHx5W p{margin:0;}#mermaid-svg-WZnUZd0vdRWLHx5W .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-WZnUZd0vdRWLHx5W .cluster-label text{fill:#333;}#mermaid-svg-WZnUZd0vdRWLHx5W .cluster-label span{color:#333;}#mermaid-svg-WZnUZd0vdRWLHx5W .cluster-label span p{background-color:transparent;}#mermaid-svg-WZnUZd0vdRWLHx5W .label text,#mermaid-svg-WZnUZd0vdRWLHx5W span{fill:#333;color:#333;}#mermaid-svg-WZnUZd0vdRWLHx5W .node rect,#mermaid-svg-WZnUZd0vdRWLHx5W .node circle,#mermaid-svg-WZnUZd0vdRWLHx5W .node ellipse,#mermaid-svg-WZnUZd0vdRWLHx5W .node polygon,#mermaid-svg-WZnUZd0vdRWLHx5W .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-WZnUZd0vdRWLHx5W .rough-node .label text,#mermaid-svg-WZnUZd0vdRWLHx5W .node .label text,#mermaid-svg-WZnUZd0vdRWLHx5W .image-shape .label,#mermaid-svg-WZnUZd0vdRWLHx5W .icon-shape .label{text-anchor:middle;}#mermaid-svg-WZnUZd0vdRWLHx5W .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-WZnUZd0vdRWLHx5W .rough-node .label,#mermaid-svg-WZnUZd0vdRWLHx5W .node .label,#mermaid-svg-WZnUZd0vdRWLHx5W .image-shape .label,#mermaid-svg-WZnUZd0vdRWLHx5W .icon-shape .label{text-align:center;}#mermaid-svg-WZnUZd0vdRWLHx5W .node.clickable{cursor:pointer;}#mermaid-svg-WZnUZd0vdRWLHx5W .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-WZnUZd0vdRWLHx5W .arrowheadPath{fill:#333333;}#mermaid-svg-WZnUZd0vdRWLHx5W .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-WZnUZd0vdRWLHx5W .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-WZnUZd0vdRWLHx5W .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-WZnUZd0vdRWLHx5W .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-WZnUZd0vdRWLHx5W .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-WZnUZd0vdRWLHx5W .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-WZnUZd0vdRWLHx5W .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-WZnUZd0vdRWLHx5W .cluster text{fill:#333;}#mermaid-svg-WZnUZd0vdRWLHx5W .cluster span{color:#333;}#mermaid-svg-WZnUZd0vdRWLHx5W div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-WZnUZd0vdRWLHx5W .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-WZnUZd0vdRWLHx5W rect.text{fill:none;stroke-width:0;}#mermaid-svg-WZnUZd0vdRWLHx5W .icon-shape,#mermaid-svg-WZnUZd0vdRWLHx5W .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-WZnUZd0vdRWLHx5W .icon-shape p,#mermaid-svg-WZnUZd0vdRWLHx5W .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-WZnUZd0vdRWLHx5W .icon-shape .label rect,#mermaid-svg-WZnUZd0vdRWLHx5W .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-WZnUZd0vdRWLHx5W .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-WZnUZd0vdRWLHx5W .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-WZnUZd0vdRWLHx5W :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

    is_flower

    flower_layer

    exe_lay1

    exe_lay2

    model

    is_flower:true

    这个图与本章标题“动态计算图”完美呼应,不同于静态构建的把所有路径顺序连接成为静态图,动态计算图会根据条件实时计算,从而只构建与条件符合的训练层,当前训练结束模型根据各层反馈调整参数后,会抹除当前训练层,重新根据下一轮训练的新条件构建符合条件的训练层

    #mermaid-svg-q2hK2ZGXILZ4e1gS{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-q2hK2ZGXILZ4e1gS .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-q2hK2ZGXILZ4e1gS .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-q2hK2ZGXILZ4e1gS .error-icon{fill:#552222;}#mermaid-svg-q2hK2ZGXILZ4e1gS .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-q2hK2ZGXILZ4e1gS .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-q2hK2ZGXILZ4e1gS .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-q2hK2ZGXILZ4e1gS .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-q2hK2ZGXILZ4e1gS .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-q2hK2ZGXILZ4e1gS .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-q2hK2ZGXILZ4e1gS .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-q2hK2ZGXILZ4e1gS .marker{fill:#333333;stroke:#333333;}#mermaid-svg-q2hK2ZGXILZ4e1gS .marker.cross{stroke:#333333;}#mermaid-svg-q2hK2ZGXILZ4e1gS svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-q2hK2ZGXILZ4e1gS p{margin:0;}#mermaid-svg-q2hK2ZGXILZ4e1gS .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-q2hK2ZGXILZ4e1gS .cluster-label text{fill:#333;}#mermaid-svg-q2hK2ZGXILZ4e1gS .cluster-label span{color:#333;}#mermaid-svg-q2hK2ZGXILZ4e1gS .cluster-label span p{background-color:transparent;}#mermaid-svg-q2hK2ZGXILZ4e1gS .label text,#mermaid-svg-q2hK2ZGXILZ4e1gS span{fill:#333;color:#333;}#mermaid-svg-q2hK2ZGXILZ4e1gS .node rect,#mermaid-svg-q2hK2ZGXILZ4e1gS .node circle,#mermaid-svg-q2hK2ZGXILZ4e1gS .node ellipse,#mermaid-svg-q2hK2ZGXILZ4e1gS .node polygon,#mermaid-svg-q2hK2ZGXILZ4e1gS .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-q2hK2ZGXILZ4e1gS .rough-node .label text,#mermaid-svg-q2hK2ZGXILZ4e1gS .node .label text,#mermaid-svg-q2hK2ZGXILZ4e1gS .image-shape .label,#mermaid-svg-q2hK2ZGXILZ4e1gS .icon-shape .label{text-anchor:middle;}#mermaid-svg-q2hK2ZGXILZ4e1gS .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-q2hK2ZGXILZ4e1gS .rough-node .label,#mermaid-svg-q2hK2ZGXILZ4e1gS .node .label,#mermaid-svg-q2hK2ZGXILZ4e1gS .image-shape .label,#mermaid-svg-q2hK2ZGXILZ4e1gS .icon-shape .label{text-align:center;}#mermaid-svg-q2hK2ZGXILZ4e1gS .node.clickable{cursor:pointer;}#mermaid-svg-q2hK2ZGXILZ4e1gS .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-q2hK2ZGXILZ4e1gS .arrowheadPath{fill:#333333;}#mermaid-svg-q2hK2ZGXILZ4e1gS .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-q2hK2ZGXILZ4e1gS .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-q2hK2ZGXILZ4e1gS .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-q2hK2ZGXILZ4e1gS .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-q2hK2ZGXILZ4e1gS .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-q2hK2ZGXILZ4e1gS .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-q2hK2ZGXILZ4e1gS .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-q2hK2ZGXILZ4e1gS .cluster text{fill:#333;}#mermaid-svg-q2hK2ZGXILZ4e1gS .cluster span{color:#333;}#mermaid-svg-q2hK2ZGXILZ4e1gS div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-q2hK2ZGXILZ4e1gS .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-q2hK2ZGXILZ4e1gS rect.text{fill:none;stroke-width:0;}#mermaid-svg-q2hK2ZGXILZ4e1gS .icon-shape,#mermaid-svg-q2hK2ZGXILZ4e1gS .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-q2hK2ZGXILZ4e1gS .icon-shape p,#mermaid-svg-q2hK2ZGXILZ4e1gS .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-q2hK2ZGXILZ4e1gS .icon-shape .label rect,#mermaid-svg-q2hK2ZGXILZ4e1gS .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-q2hK2ZGXILZ4e1gS .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-q2hK2ZGXILZ4e1gS .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-q2hK2ZGXILZ4e1gS :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

    ! is_flower

    butterfly_layer

    exe_lay1

    exe_lay2

    model

    is_flower:false

    您还可以在动态模式中进行输出调试,以捕捉模型在训练阶段的异常表现

    def forward(self,x):
    x = self.conv1(x)
    if x.std() < 0.1
    print(f"Problem detected! Variance: {x.std()}")
    x = self.conv2(x)
    return x

    如果您需要使用动态计算图模式,您需要使用nn模块(继承nn.Module)来构建模型,nn.Sequential(…)更像是一个方法,您不会在python方法的参数列表里面写分支和循环等逻辑,但是在函数体def __forward__(self,x):中您可以这么做

    尽管动态图的灵活性能帮助您在训练过程中做许多您想做的事情,但它并非完美无瑕,它有性能上的开销,pytorch每次需要根据条件创建新的动态图,且输出会根据条件的不同而变化,难以做到像静态图那样精确优化,但是它提供的灵活性对于研究人员和开发人员来说意味着可以训练出更快的迭代、更容易调试和更具表现力的模型

    模块化架构

    如果您擅用复制粘贴操作,模型构建是一件能够快速完成的事情,可是如果我们有50层,100层甚至更多,我们需要把卷积->激活->池化这些训练层复制成百上千遍,然后一个一个的改参数嘛,这样效率并不高,更令人糟心的是,它会把一个好消息变成坏消息

    您的蝴蝶识别模型构建完成,交付给客户使用时,客户很满意

    您的模型准确率很不错,不过我们新引入了一类蝴蝶,它们看来和其他蝴蝶一样,但是在细节上有所区分,也许只是翅膀的斑点排布不同,也许只是翅膀稍微长了一些,您能帮我们改善模型,新增这类蝴蝶的识别嘛

    您想到了可以新增一个卷积神经网络让模型增强特征识别,于是您在卷积层的激活函数写下relu4 = nn.ReLU(),假设您有3层卷积层,一层全连接隐藏层,一层输出层,您马上意识到relu4被全连接层声明并使用了,于是您将名称改名为relu5,这很奇怪,明明它在第四层卷积神经网络,为什么要将它称为relu5……,这很容易令人产生误解

    我们可以运用nn.Sequential解决这个问题,将所有的流程写在一个nn.Sequential当中,然后在__forward__(self,x)调用这个流程,我们无需在流程中对每个训练层命名,而是对整个流程命名,这可以解决我们在上述表述中提到的单训练层命名冲突问题

    def __init__(self,x):
    super.__init__()

    block_1 = nn.Sequential(
    nn.Conv2d(......),
    nn.ReLU(),
    nn.MaxPool2d(......),

    nn.Conv2d(......),
    nn.ReLU(),
    nn.MaxPool2d(......),

    nn.Conv2d(......),
    nn.ReLU(),
    nn.MaxPool2d(......),

    nn.Conv2d(......),
    nn.ReLU(),
    nn.MaxPool2d(......),
    ......
    )

    block2 = nn.Sequential(
    nn.Flatten()
    nn.Linear(......)
    nn.ReLU(),
    nn.DropOut(......)
    nn.Linear(......)
    )

    def forward(self,x):
    x = block_1(x)
    x = block_2(x)
    return x

    我们解决了命名问题,但是效率问题依然存在,在上述代码中,我们仍然需要写冗长的模型定义,但是好在我们并不需要写冗长的模型调用

    能否将这些重复的nn.Conv2d(…),nn.ReLU(),nn.MaxPool2d(……)写成一个通用的形式呢?其实您在数据处理那一章节中已经做过类似的事情了

    您可以查阅第三章——数据增强

    为了给不同的数据集应用不同的变换,您写了一个包装类SubSet,它同样继承自DataSet类,实现了获取方式等方法,所以可以直接传入train_loader参数被调用

    与之相似的情况是下一层卷积神经网络层的输入就是上一层神经网络层的输出,更巧妙的是您依然可以用nn.Module规范【定义继承nn.Module】的类,它的调用方式和输出都是您所熟悉且在pytorch规范下的写法,您不必担心不适配的问题。或者我们可以这样子去做

    我们将nn.Conv2d(…),nn.ReLU(),nn.MaxPool2d(……)封装成一个黑盒子,不去管里面做了什么,只关注输入数据和输出数据,对于输入来说,它是上一轮的输出或者原始图像,对于输出来说,它会作用于下一轮的输入。

    class SimpleCNN(nn.Module):
    def __init__(self):
    super().__init__()
    block_1 = nn.Sequential(
    # chain1 [a black box]
    nn.Conv2d(......),
    nn.ReLU(),
    nn.MaxPool2d(......),

    # chain2 [a black box] (last output as the level input)
    nn.Conv2d(......),
    nn.ReLU(),
    nn.MaxPool2d(......),
    ......
    )
    ......

    这不正是我们的链式调用嘛,至于链式调用的细节【每一个训练层】,外部调用者并不关心,就像forward调用block1(x)就可以做好一切训练工作,封装类也继承了nn.Module,这意味着调用者只需要调用block1(x), block(x)就可以将x传入类内部构造对象进行调用,为什么可以这么做?其实nn.Conv2d也是继承nn.Module,现在您的类也继承自nn.Module了,您传递x和要求x返回结果的过程都是nn.Module这个类规定的,所以您的做法和nn.Conv2d一样是符合规范的,只不过您在函数内部的逻辑会与Conv2d这些函数不同,但是从传递的输入类型和输出类型来看,两者都是一样的;关于底层细节,如果您感兴趣可以查阅更多资料进行探索,模型内部需要定义流程细节作为调用逻辑,与此类似,我们的继承类也需要定义我们的单训练层作为调用逻辑

    让我们尝试这样的模块化构建,还记得我们训练oxford数据集的训练类嘛,我们可以用这种方式重构,让它更加精简

    from torch import nn

    class CNNLevel(nn.Module):
    def __init__(self,in_channel,out_channel):
    super().__init__()
    self.cnn_general_step = nn.Sequential(
    nn.Conv2d(in_channel, out_channel, kernel_size=3, padding=1),
    nn.ReLU(),
    nn.MaxPool2d(kernel_size=2, stride=2),
    )
    def forward(self,x):
    x = self.cnn_general_step(x)
    return x

    class SimpleCNN(nn.Module):
    def __init__(self):
    super().__init__()
    self.cnn_step = nn.Sequential(
    CNNLevel(in_channel=3, out_channel=32),
    CNNLevel(in_channel=32, out_channel=64),
    CNNLevel(in_channel=64, out_channel=128),
    )
    self.flatten = nn.Flatten()
    self.dropout1 = nn.Dropout(p=0.2)

    self.fc1 = nn.Linear(128 * 28 * 28, 256)
    self.relu4 = nn.ReLU()
    self.dropout2 = nn.Dropout(0.5)
    self.fc2 = nn.Linear(256, 102)
    def forward(self, x):
    x = self.cnn_step(x)

    x = self.flatten(x)
    x = self.dropout1(x)

    x = self.fc1(x)
    x = self.relu4(x)
    x = self.dropout2(x)
    x = self.fc2(x)
    return x

    模块化构建会减少冗余设计,这会极大的提升开发效率,特别是在训练层的数量偏多的时候

    既然模块化构建如此高效,那为什么我们一开始还需要从重复定义训练层开始学习呢,这仍是有必要的。对于刚接触pytorch的开发者来说,写出所有的重复的层能够帮助我们理解构建原理,知道哪些训练层是重复的,应该将它构建成一个单独的模块,哪些训练层的模式是单一的,不必将它构建成一个模块,就像我们并没有将后面的全连接层构建成一个模块,因为我们发现,两个全连接层所做的工作并不一样,第一个连接层作为隐藏层,使用ReLU()激活拟合复杂特征,同时应用dropout避免神经元依赖单一模式进行训练,但是第二次全连接层作为输出层,输出结果的节点数直接就是输出特征,不能忽略任何一个神经元【这里是输出特征】的输出,也不能关闭某一神经元,所以不能在输出层后面使用激活函数和dropout。这并不是一个固定的模式,您需要定义两个类或许在一个类里面同时做这两个事情,这相较于直接在训练模型中定义都看起来更加复杂,而且在一个类里面同时做这两个事情这种情况并不通用,除了模型在最后输出的时候,其他层都不会作为输出层,所以这并不通用。

    模型检查与调试

    构建模型的过程就像培育一株盆栽,训练层像是土壤,它们源源不断地为植物提供养分,但是土壤里面有什么,很大程度上取决于您为它提供什么,如果给它合适地水与无机盐,它大概率会成长,如果给它撒上饮料,它大概率会枯竭,在模型构建中,我们也会提供训练集和训练参数,这是模型的“养料”,如果它足够好,模型的学习效果可能就更好,如果它不能被模型接收【形状不匹配】,他甚至拒绝训练【报错】,在模型构建之前检查它或者在运行过程中调试,是确保模型能稳定运行的关键

    如果查看模型的信息呢,我们可能会想到直接打印,print(model),这是一个不错的方式,它会打印出我们在__init__中定义的训练层,您可以检查模型是否按照您预定的方式执行,结构是否正确

    from cnn import *
    model = SimpleCNN()
    print(model)

    E:\\Users\\ASUS\\anaconda3\\envs\\torch_env\\python.exe E:\\pythonProject\\PythonProject\\modelPrint.py
    SimpleCNN(
    (cnn_step): Sequential(
    (0): CNNLevel(
    (cnn_general_step): Sequential(
    (0): Conv2d(3, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (1): ReLU()
    (2): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
    )
    )
    (1): CNNLevel(
    (cnn_general_step): Sequential(
    (0): Conv2d(32, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (1): ReLU()
    (2): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
    )
    )
    (2): CNNLevel(
    (cnn_general_step): Sequential(
    (0): Conv2d(64, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (1): ReLU()
    (2): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
    )
    )
    )
    (flatten): Flatten(start_dim=1, end_dim=1)
    (dropout1): Dropout(p=0.2, inplace=False)
    (fc1): Linear(in_features=100352, out_features=256, bias=True)
    (relu4): ReLU()
    (dropout2): Dropout(p=0.5, inplace=False)
    (fc2): Linear(in_features=256, out_features=102, bias=True)
    )

    进程已结束,退出代码为 0

    但是,每一层有多少参数,张量的形状是什么,我们可以使用model.paraments()追踪,您是否熟悉它呢,他曾作为实参【以引用的形式】放入优化器当中,这意味着优化器可以追踪模型的输出并根据结果进行优化,当我们拿到它之后也可以追踪模型的输出,现在我们并不聚焦于输出结果,那是训练层面做的事情,在调试和检查的时候,我们主要观察张量大小和参数个数

    如果我们使用print(model.paraments()),并不会得到所有的参数,而是得到一个生成器

    <generator object Module.parameters at 0x00000231C1997290>

    它采取和DataLoader一样的策略,不会一次性将所有内容加载到内存当中,也是在您需要的时候(循环获取的时候)一次给您一个参数,这样对内存更友好

    #……
    for item in model.parameters():
    print(item.shape)

    torch.Size([32, 3, 3, 3])
    torch.Size([32])
    torch.Size([64, 32, 3, 3])
    torch.Size([64])
    torch.Size([128, 64, 3, 3])
    torch.Size([128])
    torch.Size([256, 100352])
    torch.Size([256])
    torch.Size([102, 256])
    torch.Size([102])

    这基本上就包含了每一层的张量大小,但是这些数据究竟代表什么含义,我们需要以类似键值的方式去罗列出来,model对象中有一个名为named_parameters的方法可以帮助我们做到这件事情

    for name,data in model.named_parameters():
    print(f'{name}: {data.shape}')

    cnn_step.0.cnn_general_step.0.weight: torch.Size([32, 3, 3, 3])
    cnn_step.0.cnn_general_step.0.bias: torch.Size([32])
    cnn_step.1.cnn_general_step.0.weight: torch.Size([64, 32, 3, 3])
    cnn_step.1.cnn_general_step.0.bias: torch.Size([64])
    cnn_step.2.cnn_general_step.0.weight: torch.Size([128, 64, 3, 3])
    cnn_step.2.cnn_general_step.0.bias: torch.Size([128])
    fc1.weight: torch.Size([256, 100352])
    fc1.bias: torch.Size([256])
    fc2.weight: torch.Size([102, 256])
    fc2.bias: torch.Size([102])

    我们发现,这些数据与我们在__init__函数填充的参数相似,但是呈现方式却有所不同,以卷积层为例,当我们写入参数的时,我们的参数填充方式是nn.Conv2d(输入通道数,输出通道数,卷积核大小,padding),但是在这里每一个卷积层的weight的第一层大小是输出通道数,第二层大小是输入通道数,它并不是以开发者的视角描述应该怎么做【先输入后输出】,而是以pytorch能够处理的方式定义了所有的卷积核,一个逻辑卷积核的大小就是输入通道 * (卷积核大小),而这样的逻辑卷积核有32个【对应输出通道32,以上述代码第一层为例】,对应大小[32,3,3,3],卷积层的bias则对每一个输出通道进行偏置处理,32个逻辑卷积核产生32个输出通道,它会对32个输出通道进行处理,所以它准备了大小为[32]的偏置项数据。

    fc1和fc2是我们定义的全连接层,按照和卷积层相似的分析,将weight看成模型需要准备的所有参数大小,根据公式

    y

    1

      

    =
      

    (

    a

    1

      


      

    x

    1

    )
      

    +
      

    (

    a

    2

      


      

    x

    2

    )
      

    +
      

    .

    .

    .

    .

    .

    .
      

    +
      

    (

    a

    100352

      


      

    x

    100352

    )
      

    +

    b

    1

    y

    2

      

    =
      

    (

    a

    100353

      


      

    x

    1

    )
      

    +
      

    (

    a

    100354

      


      

    x

    2

    )
      

    +
      

    .

    .

    .

    .

    .

    .
      

    +
      

    (

    a

    (

    2

    100352

    )

      


      

    x

    100352

    )
      

    +

    b

    2

    )

    .

    .

    .

    .

    .

    .

    y

    256

      

    =
      

    (

    a

    (

    255

    100353

    )

      


      

    x

    1

    )
      

    +
      

    (

    a

    (

    (

    255

    100353

    )

    +

    1

    )

      


      

    x

    2

    )
      

    +
      

    .

    .

    .

    .

    .

    .
      

    +
      

    (

    a

    256
      


      

    100352

      


      

    x

    100352

    )
      

    +
      

    b

    256

    \\begin{align} y_1\\;&=\\;(a_{1}\\;*\\;x_1)\\;+\\;(a_2\\;*\\;x_2)\\;+\\;……\\;+\\;(a_{100352}\\;*\\;x_{100352})\\;+b_1\\\\ y_2\\;&=\\;(a_{100353}\\;*\\;x_1)\\;+\\;(a_{100354}\\;*\\;x_2)\\;+\\;……\\;+\\;(a_{(2*100352)}\\;*\\;x_{100352})\\;+b_2)\\\\ &……\\\\ y_{256}\\;&=\\;(a_{(255*100353)}\\;*\\;x_1)\\;+\\;(a_{((255*100353)+1)}\\;*\\;x_2)\\;+\\;……\\;+\\;(a_{256\\;*\\;100352}\\;*\\;x_{100352})\\;+\\;b_{256} \\end{align}

    y1y2y256=(a1x1)+(a2x2)+……+(a100352x100352)+b1=(a100353x1)+(a100354x2)+……+(a(2100352)x100352)+b2)……=(a(255100353)x1)+(a((255100353)+1)x2)+……+(a256100352x100352)+b256 我们发现weight总共需要256 * 100352个参数,其参数大小为[256,100352],对于偏置项b来说,在256个输出节点中,总共有256个偏置项,所以输出是[256]

    知道了每一层的训练层参数之后,您可以所有训练层的参数个数,不过这并不需要您手动完成,仅需调用每一个item.numel()就可以计算每一层的元素个数【对应参数个数】,通过for循环和sum求和,您可以计算出所有的参数个数

    total_parament = sum(p.numel() for p in model.parameters())
    print(total_parament)

    25809830

    如果您需要模型输出更复杂的信息,或者将所有嵌套结构展示出来,您可以使用model.named_children()和model.named_modules(),它们将展示更详细的信息,

    for name,module in model.named_chidren():
    print(name,module)
    for name,module in model.named_modules():
    if name:
    print(name)

    对于named_children它只显示顶层的模块,如果模块中包含其他层,您需要使用named_modules获取全部信息

    以下代码的运行结果仅为模拟,它与上述代码无关,仅用于描述步骤

    知道了这些参数信息之后,我们可以做什么呢。假设您在处理训练集的时候不小心让一个不规整的图像乘虚而入,模型可能会报这样的错误,您定位到这个错误的来源是来自cnn_x

    RuntimeError: mat1 and mat2 shapes cannot be multiplied(32×2048 and 1024×512)

    您不确定是哪一组图片尺寸参数导致的错误,最直接的办法是打印出错报错训练层期望的参数大小,如果您已确保模型构建阶段参数设置准确无误

    print(model.cnn_x.weight.shape)

    for name,param in model.named_parameters():
    if 'cnn_x.weight' in name:
    print(f"{name}: {param.shape}")

    torch.Size([512,1024])

    您确定是32 x 2048这组参数带来的错误,它是训练集中的哪一组数据呢,是在训练过程中的哪一个阶段出错的,我们可以直接调试def forward(self,x)根据输出结果或者写一个条件判断过滤错误项来进行一次深入的排查

    def forward(self,x):
    print(f"Input: {x.shape}")
    x = self.features(x)
    print(f"After features: {x.shape}")
    x = x.flatten(1)
    print(f"Flattened: {x.shape}")
    x = self.classifier(x)
    return x

    Congratulations!!!

    通过第四章的学习,您掌握了

    • CNN神经网络的工作原理
    • 如何在pytorch中实现CNN神经网络
    • 了解动态计算图设计和静态构建的工作原理和具体实现
    • 使用模块化架构避免冗余设计,提升模型的可维护性和可扩展性
    • 了解检查模型参数和调试模型的方法

    Success

    您已经成功完成pytorch基础阶段的课程,在这一阶段,您构建了快递时间预测模型,MNIST手写数字识别模型,oxford花卉识别模型,掌握了pytorch的基本使用和构建原理,无论接下来您打算做什么,祝您顺利😀

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » pytorch【第二部分】
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!