Повторение pytorch для глубокого обучения — модельные статьи — LeNet

PyTorch

title: LeNet

mathjax: true

date: 2020-09-08 22:26:45

теги: [pytorch, глубокое обучение, LeNet, CNN]

Если у вас есть какие-либо вопросы, пожалуйста, свяжитесь с блогом


Внедрение LeNet и воспроизведения pytorch

Введение модели

Мы часто говорим, что LeNet должен относиться к LeNet-5 в статье, опубликованной LeCun в 1998. Это новаторская работа сверточной нейронной сети CNN, С тех пор сверточная нейронная сеть расцвела повсюду, и различные оригинальные сети, основанные на сверточная нейронная сеть.Структуры постоянно создаются с хорошими результатами.

lenet.jpg

входить

Вход представляет собой черно-белое рукописное цифровое изображение размером 32*32 пикселя и размером 1*32*32. Примечание: рукописная карта цифр взята из набора данных mnist.Максимальный размер исходного набора данных составляет 28 * 28. Цель этого состоит в том, чтобы надеяться, что потенциально очевидные функции, такие как прерывистые штрихи, углы и т. д., могут появиться в рецептивном поле ядра свертки высшего уровня центр.

скрытый слой

C1 — сверточный слой, kernel_size = 5*5, шаг = 1, padding = 0, kernel_num = 6. Поскольку заполнение отсутствует, вывод равен 6*28*28.

S2 — это слой пула, kernel_size = 2*2, шаг = 2, padding = 0, kernel_num = 6. Обратите внимание, что этот слой объединения отличается от максимального и среднего объединения, которые мы обычно видим, он получается путем сложения четырех точек и их умножения на вес и смещение. То есть этот слой пула обучаем, а тот пул, который мы обычно видим, не поддается обучению. Выход 6*14*14. Код pytorch слоя понижающей дискретизации LeNet выглядит следующим образом:

class Downsampling2d(nn.Module):  # lenet中的降采样层非普通的池化层,lenet中的降采样层具有可学习的参数
    
    def __init__(self, in_channel, kernel_size = (2,2)):
        super(Downsampling2d, self).__init__()
        # lenet中的降采样是对卷积对应的四个点加和再乘以一个权重,再加上偏置
        # 可以用平均池化代替加和,尽管平均池化有除以4的过程,但因为设置有权重而线性抵消
        self.avg_pool2d = nn.AvgPool2d(kernel_size)
        self.in_channel = in_channel
        self.weights = nn.Parameter(torch.randn(in_channel), requires_grad=True)
        self.bias = nn.Parameter(torch.randn(in_channel), requires_grad=True)


    def forward(self, x):
        # input.shape = (n, in_channel, h, w)   
        x = self.avg_pool2d(x)
        outs = []
        for i in range(self.in_channel):
            out = x[:,i] * self.weights[i] + self.bias[i]
            outs.append(out.unsqueeze(1))

        return torch.cat(outs, 1)

C3 — это сверточный слой, kernel_size = 5*5, шаг = 1, padding = 0, kernel_num = 16. Слой C3 особенный в LeNet.Каждая из его Карт характеристик не связана со всеми Картами характеристик предыдущего слоя, а намеренно выбирает часть Карты характеристик предыдущего слоя для связи с ней.Искусственно разработанный вкус. При этом, с одной стороны, уменьшаются параметры слоя С3, а с другой стороны, ядро ​​свертки слоя С3 вынуждено принимать разные входные данные, тем самым вынуждая его обучаться другим функциям, что очень умный маленький дизайн. Выход 16*10*10. Код сверточного слоя C3:

class DropoutConv2d(nn.Module):
    def __init__(self, in_channels, out_channels, kernel_size = 5):
        super(DropoutConv2d, self).__init__()

        mapping = [[1, 0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 1, 1],
                   [1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 1],
                   [1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 1, 0, 1, 1, 1],
                   [0, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 0, 1, 1],
                   [0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 1, 1, 0, 1],
                   [0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 1, 1, 1]]
        self.in_channel = in_channels
        self.out_channel = out_channels
        mapping = torch.tensor(mapping, dtype=torch.long)

        self.register_buffer('mapping', mapping)
        self.convs = {}   # 用列表或者字典等装载nn的各种方法后,需要挨个注册到模块中
        for i in range(mapping.size(1)):
            conv = nn.Conv2d(mapping[:,i].sum().item(),1 ,kernel_size)
            module_name = 'conv{}'.format(i)
            self.convs[module_name] = conv
            # 通过 add_module 将 conv 中的参数注册到当前模块中
            # 若不注册则不会作为权重放入GPU中更新参数
            self.add_module(module_name, conv)

    def forward(self, x):
        out = []
        for i in range(self.mapping.size(1)):
            # .nonzero 返回矩阵中非零元素的索引的张量
            # squeeze,去掉维数为1的维度
            # in_channels是mapping中1的index
            index_channels = self.mapping[:, i].nonzero().squeeze()
            in_tensors = x.index_select(1, index_channels)
            conv_out = self.convs['conv{}'.format(i)](in_tensors)
            out.append(conv_out)

        return torch.cat(out, 1)

S4 — это уровень пула, аналогичный S2. kernel_size = 2*2, шаг = 2, отступ = 0, kernel_num = 16. Выход 6*5*5.

C5 — сверточный слой, kernel_size = 5*5, kernel_num = 120. Размер каждого ядра свертки составляет 16 * 5 * 5, а входные данные представляют собой карту признаков 16 * 5 * 5, тогда выход после свертки составляет 120 * 1 * 1. (Эффект сначала сглаживания, а затем полного соединения одинаков, какой бы метод ни использовался)

F6 — полносвязный слой, выход 84*1*1, формула:

yi=jwijxj+biy_i = \sum_j{w_{ij} \cdot x_j +b_{i}}

Что касается того, почему он предназначен для 84 вместо 74 или 94, он фактически кодирует некоторые часто используемые символы с растровым изображением 7 * 12, -1 означает белый, а 1 означает черный. После обучения модели выход слоя F6 соответствует кодировке входного числа, и длина этой кодировки равна 84. Вот кодировка числа 0:

_zero = [-1, +1, +1, +1, +1, +1, -1] + \
        [-1, -1, -1, -1, -1, -1, -1] + \
        [-1, -1, +1, +1, +1, -1, -1] + \
        [-1, +1, +1, -1, +1, +1, -1] + \
        [+1, +1, -1, -1, -1, +1, +1] + \
        [+1, +1, -1, -1, -1, +1, +1] + \
        [+1, +1, -1, -1, -1, +1, +1] + \
        [+1, +1, -1, -1, -1, +1, +1] + \
        [-1, +1, +1, -1, +1, +1, -1] + \
        [-1, -1, +1, +1, +1, -1, -1] + \
        [-1, -1, -1, -1, -1, -1, -1] + \
        [-1, -1, -1, -1, -1, -1, -1]

Функция активации, используемая в слоях C1~F6, представляет собой тангенсную функцию, которую можно преобразовать в сигмовидную функцию путем линейного преобразования. Диапазон выходных значений функции тангенса составляет (-1~1).

tanh(x)=exexex+extanh(x)=\frac{e^x-e^{-x}}{e^x+e^{-x}}
sigmoid(x)=11+exsigmoid(x) = \frac{1}{1+e^{-x}}

вывод

Выходной слой принимает метод соединения полносвязного слоя, но метод расчета выходных данных отличается от метода расчета выходных данных полносвязного слоя F6.Используется радиальная базисная функция (RBF), а выходные данные 10 * 1 * 1. Формула:

yi=j(xjwij)2y_i = \sum_j{(xj - w_{ij})^2}

Параметры выходного слоя Wij разрабатываются и фиксируются вручную, так как же они разрабатываются? Затем вернемся к таблице кодирования слоя F6.Если мы введем в сеть образ числа 0, предполагая, что сеть к этому времени обучена, слой F6 выдаст строку кодов длиной 84: "1 -1 1 1 1 -1 1 -1·····", то мы возьмем составленную ранее таблицу кодирования числа 0 и обнаружим, что эти два кода одинаковы или очень близки. Если вы этого не понимаете, перейдите к введению слоя F6. Вот искусственное кодирование числа 0.

Мы располагаем 10 выходных ячеек в столбце, представляющем числа 0-9 сверху вниз. Сначала мы вручную устанавливаем веса для первой выходной единицы 0. Поскольку она полностью подключена, каждая выходная единица соответствует 84 входам, поэтому имеется 84 веса Wij. Мы устанавливаем эти 84 веса Wij в соответствии с таблицей кодирования символа 0. Это равно 1 или -1, а затем вычисляется по формуле RBF, будет обнаружено, что если входные 84 единицы равны 84 весам, результирующий y равен 0. Тогда это означает, что входной картинкой является число 0. Аналогично, если значение i-го узла равно 0, это означает, что результатом сетевой идентификации является число i.

Вот код выходного слоя (RBF):

class RBF(nn.Module):
    def __init__(self, in_features, out_features, init_weight=None):
        super(RBF, self).__init__()
        ## register_buffer 在内存中定义一个常量,不会被optimizer更新
        if init_weight is not None:
            self.register_buffer('weight', torch.tensor(init_weight))
        else:
            self.register_buffer('weight', torch.rand(in_features, out_features))
        
    def forward(self, x):
        x = x.unsqueeze(-1)
        x = (x - self.weight).pow(2).sum(-2)
        return x

Полное воспроизведение кода (pytorch)

Воспроизведение LeNet в этой статье отличается от упрощенной версии в других блогах.Например, слой C3 принимает отсев или полностью отказывается от отсева, слой пула использует необучаемое среднее или максимальное объединение, а выходной слой использует softmax и т. д. строго следует бумаге Модель репродукции.

import torch
from torch import nn


class Downsampling2d(nn.Module):  # lenet中的降采样层非普通的池化层,lenet中的降采样层具有可学习的参数
    
    def __init__(self, in_channel, kernel_size = (2,2)):
        super(Downsampling2d, self).__init__()
        # lenet中的降采样是对卷积对应的四个点加和再乘以一个权重,再加上偏置
        # 可以用平均池化代替加和,尽管平均池化有除以4的过程,但因为设置有权重而线性抵消
        self.avg_pool2d = nn.AvgPool2d(kernel_size)
        self.in_channel = in_channel
        self.weights = nn.Parameter(torch.randn(in_channel), requires_grad=True)
        self.bias = nn.Parameter(torch.randn(in_channel), requires_grad=True)


    def forward(self, x):
        # input.shape = (n, in_channel, h, w)   
        x = self.avg_pool2d(x)
        outs = []
        for i in range(self.in_channel):
            out = x[:,i] * self.weights[i] + self.bias[i]
            outs.append(out.unsqueeze(1))

        return torch.cat(outs, 1)


class DropoutConv2d(nn.Module):
    def __init__(self, in_channels, out_channels, kernel_size = 5):
        super(DropoutConv2d, self).__init__()

        mapping = [[1, 0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 1, 1],
                   [1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 1],
                   [1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 1, 0, 1, 1, 1],
                   [0, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 0, 1, 1],
                   [0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 1, 1, 0, 1],
                   [0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 1, 1, 1]]
        self.in_channel = in_channels
        self.out_channel = out_channels
        mapping = torch.tensor(mapping, dtype=torch.long)

        self.register_buffer('mapping', mapping)
        self.convs = {}   # 用列表或者字典等装载nn的各种方法后,需要挨个注册到模块中
        for i in range(mapping.size(1)):
            conv = nn.Conv2d(mapping[:,i].sum().item(),1 ,kernel_size)
            module_name = 'conv{}'.format(i)
            self.convs[module_name] = conv
            # 通过 add_module 将 conv 中的参数注册到当前模块中
            # 若不注册则不会作为权重放入GPU中更新参数
            self.add_module(module_name, conv)

    def forward(self, x):
        out = []
        for i in range(self.mapping.size(1)):
            # .nonzero 返回矩阵中非零元素的索引的张量
            # squeeze,去掉维数为1的维度
            # in_channels是mapping中1的index
            index_channels = self.mapping[:, i].nonzero().squeeze()
            in_tensors = x.index_select(1, index_channels)
            conv_out = self.convs['conv{}'.format(i)](in_tensors)
            out.append(conv_out)

        return torch.cat(out, 1)


class RBF(nn.Module):
    def __init__(self, in_features, out_features, init_weight=None):
        super(RBF, self).__init__()
        ## register_buffer 在内存中定义一个常量,不会被optimizer更新
        if init_weight is not None:
            self.register_buffer('weight', torch.tensor(init_weight))
        else:
            self.register_buffer('weight', torch.rand(in_features, out_features))
        
    def forward(self, x):
        x = x.unsqueeze(-1)
        x = (x - self.weight).pow(2).sum(-2)
        return x

import numpy as np

_zero = [-1, +1, +1, +1, +1, +1, -1] + \
        [-1, -1, -1, -1, -1, -1, -1] + \
        [-1, -1, +1, +1, +1, -1, -1] + \
        [-1, +1, +1, -1, +1, +1, -1] + \
        [+1, +1, -1, -1, -1, +1, +1] + \
        [+1, +1, -1, -1, -1, +1, +1] + \
        [+1, +1, -1, -1, -1, +1, +1] + \
        [+1, +1, -1, -1, -1, +1, +1] + \
        [-1, +1, +1, -1, +1, +1, -1] + \
        [-1, -1, +1, +1, +1, -1, -1] + \
        [-1, -1, -1, -1, -1, -1, -1] + \
        [-1, -1, -1, -1, -1, -1, -1]

_one = [-1, -1, -1, +1, +1, -1, -1] + \
       [-1, -1, +1, +1, +1, -1, -1] + \
       [-1, +1, +1, +1, +1, -1, -1] + \
       [-1, -1, -1, +1, +1, -1, -1] + \
       [-1, -1, -1, +1, +1, -1, -1] + \
       [-1, -1, -1, +1, +1, -1, -1] + \
       [-1, -1, -1, +1, +1, -1, -1] + \
       [-1, -1, -1, +1, +1, -1, -1] + \
       [-1, -1, -1, +1, +1, -1, -1] + \
       [-1, +1, +1, +1, +1, +1, +1] + \
       [-1, -1, -1, -1, -1, -1, -1] + \
       [-1, -1, -1, -1, -1, -1, -1]

_two = [-1, +1, +1, +1, +1, +1, -1] + \
       [-1, -1, -1, -1, -1, -1, -1] + \
       [-1, +1, +1, +1, +1, +1, -1] + \
       [+1, +1, -1, -1, -1, +1, +1] + \
       [+1, -1, -1, -1, -1, +1, +1] + \
       [-1, -1, -1, -1, +1, +1, -1] + \
       [-1, -1, +1, +1, +1, -1, -1] + \
       [-1, +1, +1, -1, -1, -1, -1] + \
       [+1, +1, -1, -1, -1, -1, -1] + \
       [+1, +1, +1, +1, +1, +1, +1] + \
       [-1, -1, -1, -1, -1, -1, -1] + \
       [-1, -1, -1, -1, -1, -1, -1]

_three = [+1, +1, +1, +1, +1, +1, +1] + \
         [-1, -1, -1, -1, -1, +1, +1] + \
         [-1, -1, -1, -1, +1, +1, -1] + \
         [-1, -1, -1, +1, +1, -1, -1] + \
         [-1, -1, +1, +1, +1, +1, -1] + \
         [-1, -1, -1, -1, -1, +1, +1] + \
         [-1, -1, -1, -1, -1, +1, +1] + \
         [-1, -1, -1, -1, -1, +1, +1] + \
         [+1, +1, -1, -1, -1, +1, +1] + \
         [-1, +1, +1, +1, +1, +1, -1] + \
         [-1, -1, -1, -1, -1, -1, -1] + \
         [-1, -1, -1, -1, -1, -1, -1]

_four = [-1, +1, +1, +1, +1, +1, -1] + \
        [-1, -1, -1, -1, -1, -1, -1] + \
        [-1, -1, -1, -1, -1, -1, -1] + \
        [-1, +1, +1, -1, -1, +1, +1] + \
        [-1, +1, +1, -1, -1, +1, +1] + \
        [+1, +1, +1, -1, -1, +1, +1] + \
        [+1, +1, -1, -1, -1, +1, +1] + \
        [+1, +1, -1, -1, -1, +1, +1] + \
        [+1, +1, -1, -1, +1, +1, +1] + \
        [-1, +1, +1, +1, +1, +1, +1] + \
        [-1, -1, -1, -1, -1, +1, +1] + \
        [-1, -1, -1, -1, -1, +1, +1]

_five = [-1, +1, +1, +1, +1, +1, -1] + \
        [-1, -1, -1, -1, -1, -1, -1] + \
        [+1, +1, +1, +1, +1, +1, +1] + \
        [+1, +1, -1, -1, -1, -1, -1] + \
        [+1, +1, -1, -1, -1, -1, -1] + \
        [-1, +1, +1, +1, +1, -1, -1] + \
        [-1, -1, +1, +1, +1, +1, -1] + \
        [-1, -1, -1, -1, -1, +1, +1] + \
        [+1, +1, -1, -1, -1, +1, +1] + \
        [-1, +1, +1, +1, +1, +1, -1] + \
        [-1, -1, -1, -1, -1, -1, -1] + \
        [-1, -1, -1, -1, -1, -1, -1]

_six = [-1, -1, +1, +1, +1, +1, -1] + \
       [-1, +1, +1, -1, -1, -1, -1] + \
       [+1, +1, -1, -1, -1, -1, -1] + \
       [+1, +1, -1, -1, -1, -1, -1] + \
       [+1, +1, +1, +1, +1, +1, -1] + \
       [+1, +1, +1, -1, -1, +1, +1] + \
       [+1, +1, -1, -1, -1, +1, +1] + \
       [+1, +1, -1, -1, -1, +1, +1] + \
       [+1, +1, +1, -1, -1, +1, +1] + \
       [-1, +1, +1, +1, +1, +1, -1] + \
       [-1, -1, -1, -1, -1, -1, -1] + \
       [-1, -1, -1, -1, -1, -1, -1]

_seven = [+1, +1, +1, +1, +1, +1, +1] + \
         [-1, -1, -1, -1, -1, +1, +1] + \
         [-1, -1, -1, -1, -1, +1, +1] + \
         [-1, -1, -1, -1, +1, +1, -1] + \
         [-1, -1, -1, +1, +1, -1, -1] + \
         [-1, -1, -1, +1, +1, -1, -1] + \
         [-1, -1, +1, +1, -1, -1, -1] + \
         [-1, -1, +1, +1, -1, -1, -1] + \
         [-1, -1, +1, +1, -1, -1, -1] + \
         [-1, -1, +1, +1, -1, -1, -1] + \
         [-1, -1, -1, -1, -1, -1, -1] + \
         [-1, -1, -1, -1, -1, -1, -1]

_eight = [-1, +1, +1, +1, +1, +1, -1] + \
         [+1, +1, -1, -1, -1, +1, +1] + \
         [+1, +1, -1, -1, -1, +1, +1] + \
         [+1, +1, -1, -1, -1, +1, +1] + \
         [-1, +1, +1, +1, +1, +1, -1] + \
         [+1, +1, -1, -1, -1, +1, +1] + \
         [+1, +1, -1, -1, -1, +1, +1] + \
         [+1, +1, -1, -1, -1, +1, +1] + \
         [+1, +1, -1, -1, -1, +1, +1] + \
         [-1, +1, +1, +1, +1, +1, -1] + \
         [-1, -1, -1, -1, -1, -1, -1] + \
         [-1, -1, -1, -1, -1, -1, -1]

_nine = [-1, +1, +1, +1, +1, +1, -1] + \
        [+1, +1, -1, -1, +1, +1, +1] + \
        [+1, +1, -1, -1, -1, +1, +1] + \
        [+1, +1, -1, -1, -1, +1, +1] + \
        [+1, +1, -1, -1, +1, +1, +1] + \
        [-1, +1, +1, +1, +1, +1, +1] + \
        [-1, -1, -1, -1, -1, +1, +1] + \
        [-1, -1, -1, -1, -1, +1, +1] + \
        [-1, -1, -1, -1, +1, +1, -1] + \
        [-1, +1, +1, +1, +1, -1, -1] + \
        [-1, -1, -1, -1, -1, -1, -1] + \
        [-1, -1, -1, -1, -1, -1, -1]

# 84 x 10
RBF_WEIGHT = np.array([_zero, _one, _two, _three, _four, _five, _six, _seven, _eight, _nine]).transpose()


class LeNet(nn.Module):

    def __init__(self):
        super(LeNet, self).__init__()
        self.conv1 = nn.Conv2d(1, 6, 5)
        self.samp2 = Downsampling2d(6, (2, 2))
        self.conv3 = DropoutConv2d(6, 16, 5)
        self.samp4 = Downsampling2d(16, (2, 2))
        self.conv5 = nn.Conv2d(16, 120, 5)
        self.fc6   = nn.Linear(120, 84)
        self.output= RBF(84, 10,init_weight= RBF_WEIGHT)
        self.active= nn.Tanh()
    
    def forward(self, x):
        x = self.active(self.conv1(x))
        x = self.active(self.samp2(x))
        x = self.active(self.conv3(x))
        x = self.active(self.samp4(x))
        x = self.active(self.conv5(x))
        x = torch.squeeze(x)
        x = self.active(self.fc6(x))
        x = self.output(x)
        return x

from torchsummary import summary
net = LeNet().cuda()
summary(net, (1,32,32))

#############################输出#################################
----------------------------------------------------------------
#         Layer (type)               Output Shape         Param #
# ================================================================
#             Conv2d-1            [-1, 6, 28, 28]             156
#               Tanh-2            [-1, 6, 28, 28]               0
#          AvgPool2d-3            [-1, 6, 14, 14]               0
#     Downsampling2d-4            [-1, 6, 14, 14]               6
#               Tanh-5            [-1, 6, 14, 14]               0
#             Conv2d-6            [-1, 1, 10, 10]              76
#             Conv2d-7            [-1, 1, 10, 10]              76
#             Conv2d-8            [-1, 1, 10, 10]              76
#             Conv2d-9            [-1, 1, 10, 10]              76
#            Conv2d-10            [-1, 1, 10, 10]              76
#            Conv2d-11            [-1, 1, 10, 10]              76
#            Conv2d-12            [-1, 1, 10, 10]             101
#            Conv2d-13            [-1, 1, 10, 10]             101
#            Conv2d-14            [-1, 1, 10, 10]             101
#            Conv2d-15            [-1, 1, 10, 10]             101
#            Conv2d-16            [-1, 1, 10, 10]             101
#            Conv2d-17            [-1, 1, 10, 10]             101
#            Conv2d-18            [-1, 1, 10, 10]             101
#            Conv2d-19            [-1, 1, 10, 10]             101
#            Conv2d-20            [-1, 1, 10, 10]             101
#            Conv2d-21            [-1, 1, 10, 10]             151
#     DropoutConv2d-22           [-1, 16, 10, 10]               0
#              Tanh-23           [-1, 16, 10, 10]               0
#         AvgPool2d-24             [-1, 16, 5, 5]               0
#    Downsampling2d-25             [-1, 16, 5, 5]              16
#              Tanh-26             [-1, 16, 5, 5]               0
#            Conv2d-27            [-1, 120, 1, 1]          48,120
#              Tanh-28            [-1, 120, 1, 1]               0
#            Linear-29                   [-1, 84]          10,164
#              Tanh-30                   [-1, 84]               0
#               RBF-31                   [-1, 10]             840
# ================================================================
# Total params: 60,818
# Trainable params: 59,956
# Non-trainable params: 862
# ----------------------------------------------------------------
# Input size (MB): 0.00
# Forward/backward pass size (MB): 0.15
# Params size (MB): 0.23
# Estimated Total Size (MB): 0.38
# ----------------------------------------------------------------