Vision Transformer (vit)-Prinzipanalyse und Funktionsvisualisierung

Inhaltsverzeichnis

Einführung in Vit

Strukturdiagramm des Vit-Modells

Vit-Eingabeverarbeitung

Bildkacheln

Hinzufügung von Klassentoken und Position

Merkmalsextraktion

Vit-Code


Einführung in Vit

Vision Transformer (ViT) ist ein Deep-Learning-Modell , das auf der Transformer- Architektur für Bilderkennungs- und Computer-Vision-Aufgaben basiert. Im Gegensatz zu herkömmlichen Faltungs-Neuronalen Netzen (CNN) betrachtet ViT das Bild direkt als serialisierte Eingabe und verwendet den Selbstaufmerksamkeitsmechanismus, um die Pixelbeziehung im Bild zu verarbeiten.

ViT unterteilt das Bild in eine Reihe von Kacheln (Patches) und wandelt jede Kachel in eine Vektordarstellung als Eingabesequenz um . Diese Vektoren werden dann durch einen mehrschichtigen Transformer-Encoder verarbeitet, der aus Selbstaufmerksamkeits- und Feed-Forward-Neuronalen Netzwerkschichten besteht . Dadurch werden Kontextabhängigkeiten an verschiedenen Stellen im Bild erfasst . Schließlich können bestimmte Bildverarbeitungsaufgaben durch Klassifizierung oder Regression der Transformer-Encoder-Ausgabe erfüllt werden.

vit-Code-Referenz: Kurzaufzeichnung zum Lernen neuronaler Netze 67 – Detaillierte Erläuterung der Wiederholung der Pytorch-Version des Vision Transformer (VIT) model_vit recurrence_Bubbliiings Blog-CSDN-Blog

Warum können Transformatoren nicht direkt auf die Bildverarbeitung angewendet werden? Dies liegt daran, dass der Transformator selbst zur Verarbeitung von Sequenzaufgaben (z. B. NLP) verwendet wird, das Bild jedoch zweidimensional oder dreidimensional ist und eine bestimmte strukturelle Beziehung zwischen den Pixeln besteht. Wenn der Transformator einfach auf das Bild angewendet wird, Pixel und Pixel Da zwischen ihnen eine gewisse Korrelation bestehen muss, ist der Rechenaufwand recht groß. So wurde Vit geboren.


Strukturdiagramm des Vit-Modells

Die Modellstruktur von Vit ist in der folgenden Abbildung dargestellt. vit besteht darin, Bildblöcke auf den Transformator anzuwenden. CNN basiert auf der Idee des Schiebefensters und verwendet den Faltungskern, um das Bild zu falten und eine Feature-Map zu erhalten. Damit das Bild die Eingabesequenz von NLP imitiert, können wir das Bild zunächst in Patches unterteilen , diese Bildpatches dann kacheln und in das Netzwerk eingeben ( auf diese Weise wird es zu einer Bildsequenz ) und dann eine Merkmalsextraktion durchführen durch den Transformator und klassifizieren Sie diese Merkmale schließlich durch MLP [tatsächlich kann es als Ersetzen des Backbones durch den Transformator in früheren CNN-Klassifizierungsaufgaben verstanden werden].

Abbildung 1: Modellübersicht.  Wir teilen ein Bild in Patches fester Größe auf, betten jeden von ihnen linear ein, fügen Positionseinbettungen hinzu und geben die resultierende Vektorfolge an einen Standard-Transformer-Encoder weiter.  Um eine Klassifizierung durchzuführen, verwenden wir den Standardansatz, der Sequenz ein zusätzliches lernbares „Klassifizierungs-Token“ hinzuzufügen.  Die Illustration des Transformer-Encoders wurde von Vaswani et al. inspiriert.  (2017).

Vit-Eingabeverarbeitung

Bildkacheln

Die Bildblockierung ist der Patch im obigen Vit-Diagramm , und die Positionseinbettung ist die Positionseinbettung (die Positionsinformationen des Bildblocks können abgerufen werden) . Wie kann man also Bilder blockieren ? Das einfachste kann durch Faltung erreicht werden . Wir können die Auflösung des Bildblocks und die Anzahl der Blöcke, in die er unterteilt wird, steuern, indem wir die Größe des Faltungskerns und die Schrittgröße festlegen.

Wie wird es im Code implementiert? Sie können den Code unten sehen.

class PatchEmbed(nn.Module):
    def __init__(self, input_shape = [224,224], patch_size = 16, in_channels = 3, num_features = 768, norm_layer = None, flatten = True):
        super().__init__()
        self.num_patch = (input_shape[0] // patch_size) * (input_shape[1] // patch_size)  # 14*14的patch = 196
        self.flatten = flatten

        self.proj = nn.Conv2d(in_channels, num_features, kernel_size=patch_size, stride=patch_size)
        self.norm = norm_layer(num_features) if norm_layer else nn.Identity()

    def forward(self, x):
        x = self.proj(x)  # 先进行卷积 [1,3,224,224] ->[1,768,14,14]
        if self.flatten:
            x = x.flatten(2).transpose(1, 2)  # x.flatten.transpose(1, 2) shape:[1,768,196]
        x = self.norm(x)
        return x

Im obigen Code gibt num_patch an, wie viele Bildblöcke unterteilt werden können. proj besteht darin, Faltungsblöcke für das Eingabebild und eine Merkmalszuordnung durchzuführen. Angenommen, die Eingabegröße beträgt 1, 3, 224, 224. Nach der Faltungsoperation werden 1, 768, 14, 14 erhalten [was darauf hinweist, dass 768 Auflösungen gebildet werden]. Durch Faltung beträgt die Bildblockgröße 14×14].

Jeder Bildblock wird einmal extrahiert. Sie können einen der Bildblöcke visualisieren:

Eingabebild
Eingabebild
einer der Bildblöcke

Führen Sie dann den Abflachungsvorgang durch. Es wird zu [1, 768, 196] und durchläuft schließlich eine Layernorm-Ebene, um die endgültige Ausgabe zu erhalten.

Visualisieren Sie die Eingabesequenz

Hinzufügung von Klassentoken und Position

Durch die obigen Operationen können wir die gekachelte Merkmalssequenz erhalten (Form ist 1.768.196) . Anschließend wird der Sequenz ein Klassen-Token hinzugefügt, und dieses Token wird zusammen mit der vorherigen Feature-Sequenz zur Feature-Extraktion an das Netzwerk gesendet. Das Klassentoken ist im Bild 0*, sodass die ursprüngliche Sequenz mit einer Länge von 196 zu einer Sequenz mit einer Länge von 197 wird.

Anschließend wird die Positionseinbettung hinzugefügt , mit der allen Feature-Sequenzen Positionsinformationen hinzugefügt werden können . Durch Generieren einer [197.768]-Matrix und Hinzufügen dieser zur ursprünglichen Merkmalssequenz. Zu diesem Zeitpunkt ist die Vorverarbeitung der Patch- und Positionseinbettung der Netzwerkeingabe abgeschlossen.

# class token的定义
self.cls_token      = nn.Parameter(torch.zeros(1, 1, num_features))

# position embedding定义
self.pos_embed = nn.Parameter(torch.zeros(1, num_patches + 1, num_features))

Code:

class VisionTransformer(nn.Module):
    def __init__(
            self, input_shape=[224, 224], patch_size=16, in_chans=3, num_classes=1000, num_features=768,
            depth=12, num_heads=12, mlp_ratio=4., qkv_bias=True, drop_rate=0.1, attn_drop_rate=0.1, drop_path_rate=0.1,
            norm_layer=partial(nn.LayerNorm, eps=1e-6), act_layer=GELU
        ):
        """
        输入大小为224,以16*16的卷积核分块14*14
        :param input_shape: 网络输入大小
        :param patch_size:  分块大小
        :param in_chans:  输入通道
        :param num_classes:  类别数量
        :param num_features: 特征图维度
        :param num_heads:  多头注意力机制head的数量
        :param mlp_ratio: MLP ratio
        :param qkv_bias: qkv的bias
        :param drop_rate: dropout rate
        :param norm_layer: layernorm
        :param act_layer: 激活函数
        """
        super().__init__()
        #-----------------------------------------------#
        #   224, 224, 3 -> 196, 768
        #-----------------------------------------------#
        self.patch_embed    = PatchEmbed(input_shape=input_shape, patch_size=patch_size, in_channels=in_chans, num_features=num_features)
        num_patches         = (224 // patch_size) * (224 // patch_size)
        self.num_features   = num_features
        self.new_feature_shape = [int(input_shape[0] // patch_size), int(input_shape[1] // patch_size)]
        self.old_feature_shape = [int(224 // patch_size), int(224 // patch_size)]

        self.cls_token = nn.Parameter(torch.zeros(1, 1, num_features))  # shape [1,1,768]
        self.pos_embed = nn.Parameter(torch.zeros(1, num_patches + 1, num_features))  # shape [1, 197, 768]

    def forward_features(self, x):
        x = self.patch_embed(x)  # 先分块 [1,196, 768]
        cls_token = self.cls_token.expand(x.shape[0], -1, -1)  # [1,1,768]
        x = torch.cat((cls_token, x), dim=1)  # [1,197,768]
        cls_token_pe = self.pos_embed[:, 0:1, :]  # 获取class token pos_embed 【类位置信息】
        img_token_pe = self.pos_embed[:, 1:, :]  # 后196维度是图像特征的位置信息

        img_token_pe = img_token_pe.view(1, *self.old_feature_shape, -1).permute(0, 3, 1, 2)  # [1,768,14,14]
        img_token_pe = F.interpolate(img_token_pe, size=self.new_feature_shape, mode='bicubic', align_corners=False)
        img_token_pe = img_token_pe.permute(0, 2, 3, 1).flatten(1, 2)  # [1,196,768]
        pos_embed = torch.cat([cls_token_pe, img_token_pe], dim=1)  # [1,197,768]

        x = self.pos_drop(x + pos_embed)

Merkmalsextraktion

Wie beim CNN-Netzwerk ist auch für die Merkmalsextraktion ein Backbone erforderlich. Tatsächlich wird der Transformator-Encoder zur Merkmalsextraktion verwendet.

Unsere Eingabe ist eine Sequenz von [197,768], wobei 197 das Klassentoken [lernbar], die Bildsequenz und pos_embed [lernbar] umfasst. Diese Sequenz wird zur Merkmalsextraktion in unseren Encoder eingegeben, und die wichtige Komponente der Merkmalsextraktion im Transformator ist die Aufmerksamkeit mehrerer Köpfe.

Im Bild oben können Sie sehen, dass das Eingabebild zunächst die Normebene durchläuft und dann in drei Teile geteilt wird. Diese drei Teile sind q, k, v und werden dann gleichzeitig in den Multi-Head-Aufmerksamkeitsmechanismus eingegeben Zeit, die der Selbstaufmerksamkeitsmechanismus ist. Fügen Sie dann die Eingabe zur Restseite hinzu und geben Sie sie dann über Norm und MLP aus.

q ist unsere Abfragesequenz. Die Multiplikation von q und k dient dazu, die Korrelation oder Wichtigkeit zwischen jedem Abfragevektor in q und dem Merkmalsvektor in k zu erhalten. Dann multiplizieren wir es mit dem ursprünglichen Eingabevektor v, um den Beitrag jeder Sequenz zu erhalten [eigentlich ähnlich dem Kanalaufmerksamkeitsmechanismus].

Extrahieren Sie Funktionen, indem Sie viele Selbstaufmerksamkeiten aufbauen. Im Vergleich zu CNN ist die Grundkomponenteneinheit des Transformators die Selbstaufmerksamkeit und die Grundkomponenteneinheit von CNN der Faltungskern.

Code des Selbstaufmerksamkeitsmechanismus:

qkv im Code:

# Geometrische Bedeutung: q, k, v sind in num_heads-Köpfen verteilt (jeder Kopf hat qkv) und es gibt 197 * 64 Merkmalssequenzen auf jedem Kopf.

class Attention(nn.Module):
    def __init__(self, dim, num_heads=8, qkv_bias=False, attn_drop=0., proj_drop=0.):
        super().__init__()
        self.num_heads  = num_heads
        self.scale      = (dim // num_heads) ** -0.5

        self.qkv        = nn.Linear(dim, dim * 3, bias=qkv_bias)
        self.attn_drop  = nn.Dropout(attn_drop)
        self.proj       = nn.Linear(dim, dim)
        self.proj_drop  = nn.Dropout(proj_drop)

    def forward(self, x):
        B, N, C     = x.shape
        qkv         = self.qkv(x).reshape(B, N, 3, self.num_heads, C // self.num_heads).permute(2, 0, 3, 1, 4)
        q, k, v     = qkv[0], qkv[1], qkv[2]

        attn = (q @ k.transpose(-2, -1)) * self.scale
        attn = attn.softmax(dim=-1)
        attn = self.attn_drop(attn)

        x = (attn @ v).transpose(1, 2).reshape(B, N, C)
        x = self.proj(x)
        x = self.proj_drop(x)
        return x

Auf die gleiche Weise können wir die Merkmale von qkv visualisieren, um zu sehen, was in q, k und v enthalten ist. Die Formen unserer q, k und v sind gleich und die Form ist [batch_size, num_heads, 197, 768//num_heads]. Wir visualisieren die Eingabe des ersten Kopfes von q (in diesem Bild gibt es 12 Köpfe, Auf jedem Kopf werden 64 Merkmale extrahiert.

Der q-Merkmalsvektor auf dem ersten Kopf

Schauen wir uns dann die Eigenschaften von k im ersten Kopf an.

Charakteristischer Vektor von k auf dem ersten Kopf

Dann wird das Aufmerksamkeitsgewicht durch Multiplikation der q- und k-Matrizen erhalten

ps: Warum ist das Ergebnis von q und k im Code q @ k.transpose(-2,-1)? Warum nicht kT? Dies liegt daran, dass wir bei zwei weiteren Matrixberechnungen nur die Berechnungen für die letzten beiden Dimensionen durchführen müssen. Die Skalarproduktoperation zwischen q und k jedes Kopfes muss für die letzten beiden Dimensionen durchgeführt werden. Tauschen Sie also einfach die letzten beiden Dimensionen aus.

Die durch Matrixmultiplikation von q und k erhaltene Aufmerksamkeitsmerkmalskarte lautet wie folgt. Wir visualisieren immer noch nur den ersten Kopf [es gibt insgesamt 12 Köpfe und die Aufmerksamkeitsmerkmalskarte jedes Kopfes ist unterschiedlich]:

Verwenden Sie dann sofmax, um die Aufmerksamkeitswerte für alle Köpfe zu berechnen

Der Aufmerksamkeitswert des ersten Kopfes beträgt:

Tensor([[9.9350e-01, 2.5650e-05, 2.6444e-05, ..., 3.7445e-05, 3.3614e-05,
         2.7365e-05],
        [3.7948e-01, 2.3743e-01, 8.7877e-02, ..., 2.2976e-05, 1.2177e-04,
         6.6991e-04],
        [3.7756e-01, 1.2583e-01, 1.4249e-01, ..., 1.0860e-05, 3.4743e-05,
         1.1384e-04],
        ...,
        [4.1151e-01, 3.6945e-05, 9.8513e-06, ..., 1.5886e-01, 1.1042e-01,
         4.4855e-02] ,
        [4.0967e-01, 1.7754e-04, 2.8480e-05, ..., 1.0884e-01, 1.4333e-01,
         1.2111e-01],
        [4.1888e-01, 6.8779e-04, 6.7465e -05, ..., 3.5659e-02, 9.4098e-02,
         2.2174e-01]], device='cuda:0')

Der erhaltene Aufmerksamkeitswert wird dann mit v multipliziert, um den Beitrag jedes Kanals zu erhalten. 

Fügen Sie dann die MLP-Ebene hinzu und schließlich können Sie unseren Transformer Block erhalten. 

class Mlp(nn.Module):
    """ MLP as used in Vision Transformer, MLP-Mixer and related networks
    """

    def __init__(self, in_features, hidden_features=None, out_features=None, act_layer=GELU, drop=0.):
        super().__init__()
        out_features = out_features or in_features
        hidden_features = hidden_features or in_features
        drop_probs = (drop, drop)

        self.fc1 = nn.Linear(in_features, hidden_features)
        self.act = act_layer()
        self.drop1 = nn.Dropout(drop_probs[0])
        self.fc2 = nn.Linear(hidden_features, out_features)
        self.drop2 = nn.Dropout(drop_probs[1])

    def forward(self, x):
        x = self.fc1(x)
        x = self.act(x)
        x = self.drop1(x)
        x = self.fc2(x)
        x = self.drop2(x)
        return x


class Block(nn.Module):
    def __init__(self, dim, num_heads, mlp_ratio=4., qkv_bias=False, drop=0., attn_drop=0.,
                 drop_path=0., act_layer=GELU, norm_layer=nn.LayerNorm):
        super().__init__()
        self.norm1 = norm_layer(dim)
        self.attn = Attention(dim, num_heads=num_heads, qkv_bias=qkv_bias, attn_drop=attn_drop, proj_drop=drop)
        self.norm2 = norm_layer(dim)
        self.mlp = Mlp(in_features=dim, hidden_features=int(dim * mlp_ratio), act_layer=act_layer, drop=drop)
        self.drop_path = DropPath(drop_path) if drop_path > 0. else nn.Identity()

    def forward(self, x):
    def forward(self, x):
        '''
        :param x: 输入序列
        x --> layer_norm --> mulit head attention --> + --> x --> layer_norm --> mlp --> +-->x
        |____________________________________________|     |_____________________________|

        '''
        x = x + self.drop_path(self.attn(self.norm1(x)))
        x = x + self.drop_path(self.mlp(self.norm2(x)))
        return x
        x = x + self.drop_path(self.attn(self.norm1(x)))
        x = x + self.drop_path(self.mlp(self.norm2(x)))
        return x

Vit-Code

GitHub - YINYIPENG-EN/vit_classification_pytorch: Verwenden Sie vit, um die Bildklassifizierung zu implementieren . Tragen Sie zur Entwicklung von YINYIPENG-EN/vit_classification_pytorch bei, indem Sie ein Konto auf GitHub erstellen. icon-default.png?t=N7T8https://github.com/YINYIPENG-EN/vit_classification_pytorch.git

class VisionTransformer(nn.Module):
    def __init__(
            self, input_shape=[224, 224], patch_size=16, in_chans=3, num_classes=1000, num_features=768,
            depth=12, num_heads=12, mlp_ratio=4., qkv_bias=True, drop_rate=0.1, attn_drop_rate=0.1, drop_path_rate=0.1,
            norm_layer=partial(nn.LayerNorm, eps=1e-6), act_layer=GELU
        ):
        """
        输入大小为224,以16*16的卷积核分块14*14
        :param input_shape: 网络输入大小
        :param patch_size:  分块大小
        :param in_chans:  输入通道
        :param num_classes:  类别数量
        :param num_features: 特征图维度
        :param num_heads:  多头注意力机制head的数量
        :param mlp_ratio: MLP ratio
        :param qkv_bias: qkv的bias
        :param drop_rate: dropout rate
        :param norm_layer: layernorm
        :param act_layer: 激活函数
        """
        super().__init__()
        #-----------------------------------------------#
        #   224, 224, 3 -> 196, 768
        #-----------------------------------------------#
        self.patch_embed    = PatchEmbed(input_shape=input_shape, patch_size=patch_size, in_channels=in_chans, num_features=num_features)
        num_patches         = (224 // patch_size) * (224 // patch_size)
        self.num_features   = num_features
        self.new_feature_shape = [int(input_shape[0] // patch_size), int(input_shape[1] // patch_size)]
        self.old_feature_shape = [int(224 // patch_size), int(224 // patch_size)]

        self.cls_token = nn.Parameter(torch.zeros(1, 1, num_features))  # shape [1,1,768]
        self.pos_embed = nn.Parameter(torch.zeros(1, num_patches + 1, num_features))  # shape [1, 197, 768]

        # -----------------------------------------------#
        #   197, 768 -> 197, 768  12次
        # -----------------------------------------------#
        dpr = [x.item() for x in torch.linspace(0, drop_path_rate, depth)]
        self.blocks = nn.Sequential(
            *[
                Block(
                    dim=num_features,
                    num_heads=num_heads,
                    mlp_ratio=mlp_ratio,
                    qkv_bias=qkv_bias,
                    drop=drop_rate,
                    attn_drop=attn_drop_rate,
                    drop_path=dpr[i],
                    norm_layer=norm_layer,
                    act_layer=act_layer
                ) for i in range(depth)
            ]
        )
        self.norm = norm_layer(num_features)
        self.head = nn.Linear(num_features, num_classes) if num_classes > 0 else nn.Identity()


    def forward_features(self, x):
        x = self.patch_embed(x)  # 先分块 [1,196, 768]
        cls_token = self.cls_token.expand(x.shape[0], -1, -1)  # [1,1,768]
        x = torch.cat((cls_token, x), dim=1)  # [1,197,768]
        cls_token_pe = self.pos_embed[:, 0:1, :]  # 获取class token pos_embed 【类位置信息】
        img_token_pe = self.pos_embed[:, 1:, :]  # 后196维度是图像特征的位置信息

        img_token_pe = img_token_pe.view(1, *self.old_feature_shape, -1).permute(0, 3, 1, 2)  # [1,768,14,14]
        img_token_pe = F.interpolate(img_token_pe, size=self.new_feature_shape, mode='bicubic', align_corners=False)
        img_token_pe = img_token_pe.permute(0, 2, 3, 1).flatten(1, 2)  # [1,196,768]
        pos_embed = torch.cat([cls_token_pe, img_token_pe], dim=1)  # [1,197,768] 获得最终的位置信息

        x = self.pos_drop(x + pos_embed)  # 将位置信息和图像序列相加

        x = self.blocks(x)  # 特征提取
        x = self.norm(x)
        return x[:, 0]

    def forward(self, x):
        x = self.forward_features(x)
        x = self.head(x)
        return x

    def freeze_backbone(self):
        backbone = [self.patch_embed, self.cls_token, self.pos_embed, self.pos_drop, self.blocks[:8]]
        for module in backbone:
            try:
                for param in module.parameters():
                    param.requires_grad = False
            except:
                module.requires_grad = False

    def Unfreeze_backbone(self):
        backbone = [self.patch_embed, self.cls_token, self.pos_embed, self.pos_drop, self.blocks[:8]]
        for module in backbone:
            try:
                for param in module.parameters():
                    param.requires_grad = True
            except:
                module.requires_grad = True

Supongo que te gusta

Origin blog.csdn.net/z240626191s/article/details/132504292
Recomendado
Clasificación