ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • pytorch - Conv2D 알아보기 ( CNN )
    AI-ML 2024. 3. 3. 12:15
    728x90
    반응형

    - 목차

     

    키워드.

    • - CNN
    • - Convolution
    • - conv2d

     

    들어가며.

    CNN 을 구성하기 위해서 사용되는 nn.Conv2d 의 구체적인 동작 방식에 대해서 알아보려고 합니다.

     

    함께 보면 좋은 글.

    https://westlife0615.tistory.com/765

     

    Cross Entropy 알아보기

    - 목차 키워드.- Probability- Surprise- Entropy- Cross Entropy 들어가며.이번 글에서는 Cross Entropy 에 대해서 알아보는 시간을 가지도록 하겠습니다. Cross Entropy 를 이해하기 위해서 몇가지 사전지식들이 요

    westlife0615.tistory.com

     

     

    Kernel or Filter.

    CNN 은 원본 이미지와 커널이라고 불리는 작은 크기의 Matrix 를 곱하여 새로운 Matrix 인 Feature Map 을 생성하게 됩니다.

    아래 두개의 이미지를 첨부했는데요.

    CNN 에서 원본 이미지와 커널 간의 연산을 잘 보여주는 이미지라서 첨부하게 되었습니다.

    출처 : https://learnopencv.com/understanding-convolutional-neural-networks-cnn/

     

     

    출처 : https://vitalflux.com/real-world-applications-of-convolutional-neural-networks/

     

    커널은 Kernel 또는 Filter 라고도 불리웁니다.

    이 커널은 원본 이미지의 각 픽셀을 순회하면서 element-wise 방식으로 곱셈 연산을 수행하게 되는데요.

    예를 들어, 아래와 같인 image_tensor 와 kernel_tensor 가 존재한다고 가정하겠습니다.

    image_tensor = torch.tensor([
        [1, 1, 1, 1, 1],
        [1, 1, 1, 1, 1],
        [1, 1, 1, 1, 1],
        [1, 1, 1, 1, 1],
        [1, 1, 1, 1, 1],
    ], dtype=torch.float32)
    # tensor([[1., 1., 1., 1., 1.],
    #         [1., 1., 1., 1., 1.],
    #         [1., 1., 1., 1., 1.],
    #         [1., 1., 1., 1., 1.],
    #         [1., 1., 1., 1., 1.]])
    
    kernel_tensor = torch.tensor([
        [1, 0, 0],
        [0, -1, 0],
        [0, 0, 1],
    ], dtype=torch.float32)
    
    # tensor([[ 1.,  0.,  0.],
    #         [ 0., -1.,  0.],
    #         [ 0.,  0.,  1.]])

     

    그리고 Conv2d 레이어를 생성합니다.

    아래 Conv2d 의 의미는 커널의 크기를 3x3 인 커널로 생성한다는 의미이구요.

    conv = nn.Conv2d(1, 1, 3, 1)

     

    생성된 Conv2d 의 weight 는 아래와 같습니다.

    이 weight 가 곧 커널에 해당하는 Matrix 를 의미합니다.

    conv.weight
    
    # Parameter containing:
    # tensor([[[[ 0.2041,  0.0130, -0.3247],
    #           [-0.0896,  0.3322,  0.2517],
    #           [-0.2145, -0.0163, -0.0718]]]], requires_grad=True)

     

    그리고 생성된 Conv2d 레이어의 weight 를 아래와 같이 변경한 후에 Convolution 을 적용합니다.

    참고로 콘볼루션에 사용되는 이미지 텐서와 커널 텐서는 4차원 또는 3차원의 Dimension 을 가져야합니다.

    conv.weight = nn.Parameter(kernel_tensor.view(1, 1, 3, 3))
    conv.bias = nn.Parameter(torch.tensor([0.0]))
    conv(image_tensor.view(1, 1, 5, 5))

     

    5x5 이미지와 3x3 인 커널이 콘볼루션된 후의 결과값은 아래와 같습니다.

    tensor([[[[1., 1., 1.],
              [1., 1., 1.],
              [1., 1., 1.]]]], grad_fn=<ConvolutionBackward0>)

     

    이러한 결론에 도달하게 된 과정의 연산을 풀어보면 아래와 같습니다.

    연산의 내용은 이미지 텐서와 커널 텐서의 각 자리의 Item 을 곱한 후 모두 더하게 됩니다.

    이 과정에서 element-wise 방식으로 곱셈과 뎃셈의 연산이 적용되구요.

    1x1 + 1x0 + 1x0 
    + 1x0 + 1x-1 + 1x0
    + 1x0 + 1x0 + 1x1
    = 1

     

    이해를 돕고자 관련된 이미지를 첨부합니다.

     

     

    출처 : https://www.researchgate.net/figure/in-a-convolutional-layer-element-wise-matrix-multiplication-and-summation-of-the_fig1_353130362

    Channel.

    Channel 이라는 개념도 중요합니다.

    위에서 설명하길 이미지 텐서와 커널 텐서는 4차원 또는 3차원의 Dimension 을 사용해야한다고 말씀드렸습니다.

    각 차원은 batch, channel, width, height 로 구성된 Dimension 이구요.

    batch 는 딥러닝 학습과정에서 DataLoader 에 의해서 산출되는 batch 를 의미합니다.

    즉, 이미지 텐서의 갯수, 배치 사이즈를 의미하죠.

    그리고 channel 은 RGB 와 같이 이미지의 조합을 의미하는데요.

    일반적인 이미지는 RGB 와 같이 3개의 이미지의 조합으로 이루어지죠 ?

    이미지를 구성하는 각 픽셀이 0 ~ 255 의 값으로 구성되는데,

    하나의 픽셀은 빨강의 정도, 파랑의 정도, 그리고 녹색의 정도를 수치적으로 표현합니다.

    그래서 하나의 이미지, 또는 하나의 픽셀은 3개의 값을 취합니다.

    만약 흑백 이미지라고 한다면 이는 1개의 Channel 을 가지게 되는 점도 이해하시면 좋을 것 같네요.

     

    커널 또한 Channel 을 가집니다.

    커널의 Channel 은 조금 다른 의미입니다.

    일반적으로 커널은 이미지에서 특정 패턴을 추출하기 위해서 사용됩니다.

    예를 들어, 이미지에서 굴곡진 선을 잘 추출한다던지 가로 또는 세로의 수직선을 추출한다던지 와 같은 역할을 수행하죠.

     

    아래의 이미지 텐서는 대각선으로 이뤄진 Tensor 이구요.

    대각선의 패턴을 추출하는 커널과 콘볼루션을 계산한 결과입니다.

    이미지는 아래와 같이 표현됩니다.

    아래의 결과처럼 이미지와 커널가 서로 상호작용하는 관계인 경우에 결과의 Tensor 의 강도가 더 커지게 됩니다.

    image_tensor = torch.tensor([
        [1, 0, 0, 0, 0],
        [0, 1, 0, 0, 0],
        [0, 0, 1, 0, 0],
        [0, 0, 0, 1, 0],
        [0, 0, 0, 0, 1],
    ], dtype=torch.float32)
    
    kernel_tensor = torch.tensor([
        [1, 0, 0],
        [0, 1, 0],
        [0, 0, 1],
    ], dtype=torch.float32)

     

     

     

     

    아래의 예시는 대각선의 이미지와 수평선의 패턴을 인식하는 커널의 조합입니다.

    이미지와 커널의 관계가 유사하지 않을 때에는 아래의 결과처럼 이미지의 패턴을 잘 인식하지 못하게 됩니다.

    image_tensor = torch.tensor([
        [1, 0, 0, 0, 0],
        [0, 1, 0, 0, 0],
        [0, 0, 1, 0, 0],
        [0, 0, 0, 1, 0],
        [0, 0, 0, 0, 1],
    ], dtype=torch.float32)
    
    kernel_tensor = torch.tensor([
        [0, 0, 0],
        [1, 1, 1],
        [0, 0, 0],
    ], dtype=torch.float32)

     

     

     

    그리고 아래의 예시는 원형의 이미지와 원형의 패턴을 인식하기 위한 커널의 조합입니다.

    image_tensor = torch.tensor([
        [0, 0.1, 0.1, 0.1, 0],
        [0.1, 0, 0, 0, 0.1],
        [0.1, 0, 0, 0, 0.1],
        [0.1, 0, 0, 0, 0.1],
        [0, 0.1, 0.1, 0.1, 0],
    ], dtype=torch.float32)
    
    kernel_tensor = torch.tensor([
        [0, 1, 0],
        [1, 0, 1],
        [0, 1, 0],
    ], dtype=torch.float32)

     

     

    이렇듯 CNN 의 커널은 이미지의 패턴을 인식하여 콘볼루션 결과로써 강도가 높은 이미지 텐서가 출력되는 방식입니다.

    그리고 인식하고자 하는 다양한 패턴의 갯수만큼 커널의 Channel 을 늘려주어야합니다.

     

     

    Conv2d 인자 알아보기.

    nn.Conv2d 클래스를 통해서 Conv2d Layer 를 생성하기 위해서 여러가지 인자들이 사용됩니다.

    각 인자들의 특징과 사용법에 대해서 설명합니다.

    padding.

    padding 인자는 Image Tensor 의 외과에 새로운 Item 들을 추가합니다.

    예를 들어, 4x4 Image Tensor 에 Padding 1 을 추가하게 되면, 6x6 인 Image Tensor 가 됩니다.

    아래와 같은 4x4 이미지 텐서는 padding 1 이 추가된다면, 0 으로 구성된 새로운 Item 들이 4x4 Image Tensor 를 둘러싸게 됩니다.

    data = torch.tensor([
        [1, 1, 1, 1],
        [1, 1, 1, 1],
        [1, 1, 1, 1],
        [1, 1, 1, 1]
    ], dtype=torch.float32)
    # tensor([[1., 1., 1., 1.],
    #         [1., 1., 1., 1.],
    #         [1., 1., 1., 1.],
    #         [1., 1., 1., 1.]])
    
    --> Padding 1 적용 이후.
    
    tensor([[0., 0., 0., 0., 0., 0.],
            [0., 1., 1., 1., 1., 0.],
            [0., 1., 1., 1., 1., 0.],
            [0., 1., 1., 1., 1., 0.],
            [0., 1., 1., 1., 1., 0.],
            [0., 0., 0., 0., 0., 0.]])

     

    이렇게 Padding 을 사용함으로써 얻을 수 있는 이점은 커널의 크기가 이미지 텐서의 크기보다 큰 경우에 Convolution 을 수행할 수 있다는 점입니다.

    만약 이미지 텐서보다 커널의 사이즈가 더 큰 경우에 아래와 같은 에러가 발생합니다.

    data = torch.tensor([
        [1, 1, 1, 1],
        [1, 1, 1, 1],
        [1, 1, 1, 1],
        [1, 1, 1, 1]
    ], dtype=torch.float32).view(1, 1, 4, 4)
    
    conv = nn.Conv2d(1, 1, 5, 1)
    conv(data)
    RuntimeError: Calculated padded input size per channel: (4 x 4). 
    Kernel size: (5 x 5). 
    Kernel size can't be greater than actual input size

     

     

    이러한 경우에 Padding 은 1 이상을 주어 이미지 텐서가 커널의 사이즈보다 크거나 같도록 변경할 수 있습니다.

    data = torch.tensor([
        [1, 1, 1, 1],
        [1, 1, 1, 1],
        [1, 1, 1, 1],
        [1, 1, 1, 1]
    ], dtype=torch.float32).view(1, 1, 4, 4)
    
    conv = nn.Conv2d(1, 1, 5, 1, padding=1, padding_mode='zeros', bias=False)
    conv(data)
    tensor([[[[ 0.1318, -0.3695],
              [ 0.1329, -0.1852]]]], grad_fn=<ConvolutionBackward0>)

     

    nn.Conv2d 사용해보기.

    nn.Conv2d 를 사용하여 간단한 CNN 예시를 작성해보겠습니다.

    사용할 이미지 텐서는 torch.tensor 함수를 사용하여 생성해보았구요.

    아래의 사진과 같은 형식의 Tensor 들로 구성됩니다.

    import torch
    import torch.nn as nn
    import torch.nn.functional as F
    import torch.optim as optim
    
    one_image_1 = torch.tensor([
        [1, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [1, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [1, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [1, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [1, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [1, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [1, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [1, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [1, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [1, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    ], dtype=torch.float32)
    one_image_2 = torch.tensor([
        [0, 1, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 1, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 1, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 1, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 1, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 1, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 1, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 1, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 1, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 1, 0, 0, 0, 0, 0, 0, 0, 0],
    ], dtype=torch.float32)
    one_image_3 = torch.tensor([
        [0, 0, 1, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 1, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 1, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 1, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 1, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 1, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 1, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 1, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 1, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 1, 0, 0, 0, 0, 0, 0, 0],
    ], dtype=torch.float32)
    one_image_4 = torch.tensor([
        [0, 0, 0, 1, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 1, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 1, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 1, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 1, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 1, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 1, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 1, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 1, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 1, 0, 0, 0, 0, 0, 0],
    ], dtype=torch.float32)
    two_image_1 = torch.tensor([
        [1, 1, 1, 1, 1, 1, 1, 1, 1, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 1, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 1, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 1, 0],
        [0, 1, 1, 1, 1, 1, 1, 1, 1, 0],
        [0, 1, 0, 0, 0, 0, 0, 0, 1, 0],
        [0, 1, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 1, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 1, 1, 1, 1, 1, 1, 1, 1, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    ], dtype=torch.float32)

     

    그리고 CNN 모델을 생성합니다.

    1개의 Conv2d 레이어와 ReLU Activation Function 그리고 Linear 레이어로 구성됩니다.

    커널의 갯수는 10개로 구성하였고, 커널의 사이즈를 5x5 입니다.

    그리고 Linear 레이어는 10 * 6 * 6 인 in_features 를 가지는데요.

    그 이유는 10개의 Channel 과 6x6 인 Feature Map 의 사이즈의 곱해야 하기 때문입니다.

    6x6 Feature Map 이 생성되는 이유는 5x5 커널과 10x10 이미지가 콘볼루션된 결과 6x6 인 Feature Map 이 생성됩니다.

     

    class CNN(nn.Module):
        def __init__(self):
            super(CNN, self).__init__()
            self.conv1 = nn.Conv2d(1, 10, 5, 1)
            self.linear = nn.Linear(10 * 6 * 6, 10)
    
        def forward(self, x):
            image_tensor = x.view(1, 1, 10, 10)
            output = F.relu(self.conv1(image_tensor))
            return self.linear(output.view(1, -1))
    
    model = CNN()
    optimizer = optim.Adam(model.parameters(), lr=0.01)
    loss_function = nn.CrossEntropyLoss()
    
    for epoch in range(100):
        
        for image in [one_image_1, one_image_2, one_image_3, one_image_4]:
            model.train()
            label_tensor = torch.tensor([0, 1, 0, 0, 0, 0, 0, 0, 0, 0], dtype=torch.float32).view(1, 10)
            output = model(image)
            optimizer.zero_grad()
            loss = loss_function(output, label_tensor)
            loss.backward()
            optimizer.step()
            print(loss.item())
            
        for image in [two_image_1]:
            model.train()
            label_tensor = torch.tensor([0, 0, 1, 0, 0, 0, 0, 0, 0, 0], dtype=torch.float32).view(1, 10)
            output = model(image)
            optimizer.zero_grad()
            loss = loss_function(output, label_tensor)
            loss.backward()
            optimizer.step()
            print(loss.item())
    
    print(f"one_image_1 : {torch.argmax(model(one_image_1))}")
    print(f"one_image_2 : {torch.argmax(model(one_image_2))}")
    print(f"one_image_3 : {torch.argmax(model(one_image_3))}")
    print(f"one_image_4 : {torch.argmax(model(one_image_4))}")
    print(f"two_image_1 : {torch.argmax(model(two_image_1))}")

     

    생성된 CNN 모델은 학습 데이터에 대해서 아래와 같은 추론 결과를 보여줍니다.

    one_image_1 : 1
    one_image_2 : 1
    one_image_3 : 1
    one_image_4 : 1
    two_image_1 : 2

     

     

     

     

    반응형
Designed by Tistory.