논문: Going Deeper with convolutions
게재: 2015년
학회: CVPR

GoogLeNet이란?

GoogLeNet은 한 마디로 네트워크의 depth와 width를 늘리면서도 내부적으로 inception 모듈을 활용해 computational efficiency를 확보한 모델이다. 이전에 나온 VGGNet이 깊은 네트워크로 AlexNet보다 높은 성능을 얻었지만, 반면 파라미터 측면에서 효율적이지 못하다는점을 보완하기 위해 만들어졌다.

 

CNN 아키텍처와 GoogLeNet

2010년 초/중반 CNN 아키텍처를 활용해 높은 이미지 인식 능력을 선보인 모델은 AlexNet (2012) → VGGNet (2014) → GoogLeNet (2014) → ResNet (2015) → ... 순으로 계보가 이어진다. 이 모델들은 네트워크의 depth와 width를 늘리면서 성능 향상이 이루어지는 것을 보며 점점 depth와 width를 늘려갔다. 하지만 이렇게 네트워크가 커지면서 두 가지 문제점이 발생했다. 첫 번째는 많은 파라미터가 있지만 이를 학습시킬 충분한 데이터가 없다면 오버피팅이 발생하는 것이고, 두 번째는 computational resource는 늘 제한적이기에 비용이 많이 발생하거나 낭비될 수 있다는 점이다. GoogLeNet은 이러한 문제점을 해결하기 위해 fully-connected layer의 neuron 수를 줄이거나 convolution layer의 내부 또한 줄여 상대적으로 sparse한 network를 만들고자 만들어졌다. 결과적으로 GoogLeNet은 가장 초반에 나온 AlexNet보다도 파라미터수가 12배 더 적지만 더 높은 성능을 보이는 것이 특징이다. 결과적으로 GoogLeNet은 ILSVRC 2014에서 classification과 detection 트랙에서 SOTA를 달성했다.

 

Inception: GoogLeNet 성능의 핵심

파라미터 효율적이면서도 높은 성능을 낼 수 있는 GoogLeNet 아키텍처의 핵심은 내부 Inception 모듈의 영향이다. Inception 모듈을 보기전 먼저 GoogLeNet 아키텍처를 살펴보면 다음과 같다.


왼쪽(아래)에서 출발하여 오른쪽(위)으로 점점 깊어진다. Inception 모듈은 위 GoogLeNet 네트워크에서 사용되는 부분 네트워크에 해당한다. 그 부분 구조는 아래와 같다. 두 가지 버전이 있다.

두 버전 모두 이미지 feature를 효율적으로 추출하기 위해 공통적으로 1x1, 3x3, 5x5 필터 크기를 갖는 convolution 연산을 수행한다. 이렇게 이미지의 다양한 영역을 볼 수 있도록 서로 다른 크기의 필터를 사용함으로써 앙상블하는 효과를 가져와 모델의 성능이 높아지게 된다. 참고로 필터 크기를 1x1, 3x3, 5x5로 지정한 것은 필요성에 의한 것이기보다 편의성을 기반으로 했다. 만약 짝수 크기의 필터 크기를 갖게 된다면 patch-alignment 문제라고하여 필터 사이즈가 짝수일 때 patch(필터)의 중간을 어디로 두어야 할지 결정해야하기에 번거로워지기 때문이다.

두 버전의 Inception 모듈을 비교해보자면, 먼저 좌측 naive Inception 모듈의 경우 1x1, 3x3, 5x5 convolution 연산과 3x3 max pooling 연산을 병렬 수행한 뒤 concatenation을 통해 결과를 합쳐주는 구조로 되어 있다. 하지만 결과적으로 이는 잘 동작하지 않았다. 그 이유는 convolution 결과와 max pooling 결과를 concatenation하면 기존 보다 차원이 늘어나는 문제점이 발생했기 때문이다. 정확히는 max pooling 연산의 경우 channel 수가 그대로 보존되지만 convolution 연산에 의해 더해진 channel로 인해 computation 부하가 늘어나게 된다. 이 과정을 도식화 한 것은 아래 그림과 같다. (출처: 정의석님 블로그)


위 그림과 같이 output channel의 크기가 증가하여 computation 부하가 커지는 문제점을 해결하기 위해 우측 두 번째 Inception 모듈이 사용된다. 두 번째 Inception 모듈을 통해 output channel을 줄이는 즉 차원을 축소하는 효과를 가져온다. 실제로도 이러한 효과로 인한 computation efficiency 때문에 GoogLeNet 아키텍처에서도 첫 번째 naive Inception 모듈이 아닌 두 번째 dimension reduction Inception 모듈을 사용한다.

그렇다면 두 번째 Inception module에서의 차원축소는 어떻게 이루어질까? 이에 대한 핵심은 1x1 convolution 연산에 있다. 1x1 convolution 연산은 Network in Network (Lin et al) 라는 연구에서 도입된 방법이다. 이름 그대로 1x1 필터를 가지는 convolution 연산을 하는 것이다. 1x1 convolution 연산은 아래 GIF와 같이 동작한다. 그 결과 feature map 크기가 동일하게 유지된다. 하지만 중요한 것은 1x1 filter 개수를 몇 개로 해주느냐에 따라 feature map의 차원의 크기(채널)를 조절할 수 있게 되는 것이다. 따라서 만약 input dimension 보다 1x1 filter 개수를 작게 해준다면 차원축소가 일어나게 되며, 그 결과 핵심적인 정보만 추출할 수 있게 된다.

 

Global Average Pooling (GAP)

GoogLeNet 아키텍처에선 Global Average Pooling 개념을 도입하여 사용한다. 이는 마찬가지로 Network in Network (Lin et al) 에서 제안된 것으로, 기존 CNN + fully connected layer 구조에서 fully-connected layer를 대체하기 위한 목적이다. 그 이유는 convolution 연산의 결과로 만들어진 feature map을 fully-connected layer에 넣고 최종적으로 분류하는 과정에서 공간 정보가 손실되기 때문이다. 반면 GAP은 feature map의 값의 평균을 구해 직접 softmax 레이어에 입력하는 방식이다. 이렇게 직접 입력하게 되면 공간 정보가 손실되지 않을 뿐만 아니라 fully-connected layer에 의해 파라미터를 최적화 하는 과정이 없기 때문에 효율적이게 된다.

Auxiliary classification

먼저 Auxiliary란 보조를 뜻하는 것으로 Auxiliary classification은 최종 classification 결과를 보조하기 위해 네트워크 중간 중간에 보조적인(Auxiliary) classifier를 두어 중간 결과 값을 추출하는 것이다. 아래 그림은 GoogLeNet 아키텍처를 부분 확대한 것이다. Auxiliary Classification 과정은 이 중 노란 박스 열에 해당한다.


이렇게 Auxiliary classifier를 중간 중간에 두는 이유는 네트워크가 깊어지면서 발생하는 gradient vanishing 문제를 해결하기 위함이다. 즉 네트워크가 깊어지면 loss에 대한 back-propagation이 영향력이 줄어드는 문제를 해결하기 위해 도입된 것이다. 이렇게 함으로써 중간에 있는 Inception layer들도 적절한 가중치 업데이트를 받을 수 있게 된다. 하지만 Inception 모듈 버전이 늘어날수록 Auxiliary classifier를 점점 줄였고 inception v4에서는 완전히 제외했다. 그 이유는 중간 중간에 있는 layer들은 back-propagation을 통해 학습이 적절히 이루어 졌지만 최종 classification layer에서는 학습이 잘 이루어지지 못해 최적의 feature가 뽑히지 않았기 때문이다.

파이토치로 GoogLeNet 구현하기

GoogLeNet 모델의 핵심은 Inception 모듈을 구현하는 것이다.
1. Inception 모듈 구현에 앞서 Inception 모듈에서 반복적으로 사용될 convolution layer를 생성하기 위해 ConvBlock이란 클래스로 다음과 같이 구현해준다. convolution layer 뒤에 batch normalization layer와 relu가 뒤따르는 구조이다.

import torch
import torch.nn as nn
from torch import Tensor

class ConvBlock(nn.Module):
    def __init__(self, in_channels, out_channels, **kwargs) -> None:
        super(ConvBlock, self).__init__()
        self.conv = nn.Conv2d(in_channels, out_channels, **kwargs)
        self.batchnorm = nn.BatchNorm2d(out_channels)
        self.relu = nn.ReLU()

    def forward(self, x: Tensor) -> Tensor:
        x = self.conv(x)
        x = self.batchnorm(x)
        x = self.relu(x)
        return x


2. ConvBlock 클래스를 활용하여 Inception module을 구현하기 위한 Inception 클래스를 다음과 같이 구현해준다.

import torch
import torch.nn as nn
from torch import Tensor

class Inception(nn.Module):
    def __init__(self, in_channels, n1x1, n3x3_reduce, n3x3, n5x5_reduce, n5x5, pool_proj) -> None:
        super(Inception, self).__init__()
        self.branch1 = ConvBlock(in_channels, n1x1, kernel_size=1, stride=1, padding=0)

        self.branch2 = nn.Sequential(
            ConvBlock(in_channels, n3x3_reduce, kernel_size=1, stride=1, padding=0),
            ConvBlock(n3x3_reduce, n3x3, kernel_size=3, stride=1, padding=1))
        
        self.branch3 = nn.Sequential(
            ConvBlock(in_channels, n5x5_reduce, kernel_size=1, stride=1, padding=0),
            ConvBlock(n5x5_reduce, n5x5, kernel_size=5, stride=1, padding=2))

        self.branch4 = nn.Sequential(
            nn.MaxPool2d(kernel_size=3, stride=1, padding=1),
            ConvBlock(in_channels, pool_proj, kernel_size=1, stride=1, padding=0))
        
    def forward(self, x: Tensor) -> Tensor:
        x1 = self.branch1(x)
        x2 = self.branch2(x)
        x3 = self.branch3(x)
        x4 = self.branch4(x)
        return torch.cat([x1, x2, x3, x4], dim=1)


Inception 클래스를 확인하면 내부적으로 4개의 branch 변수를 만든다. 이 4개의 branch라는 변수는 각각 아래 Inception module에서 네 갈래로 분기되는 것을 구현해준 것이다.


Inception 모듈에 들어갈 인자는 다음과 같다.
1x1 convolution의 경우 kernel_size=1, stride= 1, padding=0
3x3 convolution의 경우 kernel_size=3, stride= 1, padding=1
5x5 convolution의 경우 kernel_size=5, stride= 1, pading=2
max-pooling의 경우 kernel_size=3, stride=1, padding=1

kernel_size는 아키텍처에 보이는 그대로와 같고, GoogLeNet에서는 stride를 공통적으로 1로 사용했다. 각각의 padding은 추후 네 개의 branch가 합쳐졌을 때의 크기를 고려하여 맞춰준다.

3. Auxiliary Classifier를 구현하기 위해 아래와 같이 InceptionAux라는 클래스로 구현해주었다. 1x1 convolution의 output channel의 개수는 논문에 기술된 대로 128을 적용해주었으며 dropout rate 또한 논문에 기술된 대로 0.7을 적용해주었다.

import torch
import torch.nn as nn
from torch import Tensor

class InceptionAux(nn.Module):
    def __init__(self, in_channels, num_classes) -> None:
        super(InceptionAux, self).__init__()
        self.avgpool = nn.AvgPool2d(kernel_size=5, stride=3)
        self.conv = ConvBlock(in_channels, 128, kernel_size=1, stride=1, padding=0)
        self.fc1 = nn.Linear(2048, 1024)
        self.fc2 = nn.Linear(1024, num_classes)
        self.dropout = nn.Dropout(p=0.7)
        self.relu = nn.ReLU()

    def forward(self, x: Tensor) -> Tensor:
        x = self.avgpool(x)
        x = self.conv(x)
        x = x.view(x.size()[0], -1)
        x = self.fc1(x)
        x = self.relu(x)
        x = self.dropout(x)
        x = self.fc2(x)
        return x


InceptionAux 클래스는 아래와 그림과 같이 Auxiliary Classification 과정을 구현해 준 것이다.



4. ConvBlock, Inception, InceptionAux를 활용하여 GoogLeNet을 다음과 같이 구현할 수 있다.

import torch
import torch.nn as nn
from torch import Tensor

class GoogLeNet(nn.Module):
    def __init__(self, aux_logits=True, num_classes=1000) -> None:
        super(GoogLeNet, self).__init__()
        assert aux_logits == True or aux_logits == False
        self.aux_logits = aux_logits

        self.conv1 = ConvBlock(in_channels=3, out_channels=64, kernel_size=7, stride=2, padding=3)
        self.maxpool1 = nn.MaxPool2d(kernel_size=3, stride=2, padding=1, ceil_mode=True)
        self.conv2 = ConvBlock(in_channels=64, out_channels=64, kernel_size=1, stride=1, padding=0)
        self.conv3 = ConvBlock(in_channels=64, out_channels=192, kernel_size=3, stride=1, padding=1)
        self.maxpool2 = nn.MaxPool2d(kernel_size=3, stride=2, ceil_mode=True)

        self.a3 = Inception(192, 64, 96, 128, 16, 32, 32)
        self.b3 = Inception(256, 128, 128, 192, 32, 96, 64)
        self.maxpool3 = nn.MaxPool2d(kernel_size=3, stride=2, padding=1, ceil_mode=True)
        self.a4 = Inception(480, 192, 96, 208, 16, 48, 64)
        self.b4 = Inception(512, 160, 112, 224, 24, 64, 64)
        self.c4 = Inception(512, 128, 128, 256, 24, 64, 64)
        self.d4 = Inception(512, 112, 144, 288, 32, 64, 64)
        self.e4 = Inception(528, 256, 160, 320, 32, 128, 128)
        self.maxpool4 = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
        self.a5 = Inception(832, 256, 160, 320, 32, 128, 128)
        self.b5 = Inception(832, 384, 192, 384, 48, 128, 128)
        self.avgpool = nn.AvgPool2d(kernel_size=8, stride=1)
        self.dropout = nn.Dropout(p=0.4)
        self.linear = nn.Linear(1024, num_classes)

        if self.aux_logits:
            self.aux1 = InceptionAux(512, num_classes)
            self.aux2 = InceptionAux(528, num_classes)
        else:
            self.aux1 = None
            self.aux2 = None

    def transform_input(self, x: Tensor) -> Tensor:
        x_R = torch.unsqueeze(x[:, 0], 1) * (0.229 / 0.5) + (0.485 - 0.5) / 0.5
        x_G = torch.unsqueeze(x[:, 1], 1) * (0.224 / 0.5) + (0.456 - 0.5) / 0.5
        x_B = torch.unsqueeze(x[:, 2], 1) * (0.225 / 0.5) + (0.406 - 0.5) / 0.5
        x = torch.cat([x_R, x_G, x_B], 1)
        return x
        
    def forward(self, x: Tensor) -> Tensor:
        x = self.transform_input(x)

        x = self.conv1(x)
        x = self.maxpool1(x)
        x = self.conv2(x)
        x = self.conv3(x)
        x = self.maxpool2(x)
        x = self.a3(x)
        x = self.b3(x)
        x = self.maxpool3(x)
        x = self.a4(x)
        aux1: Optional[Tensor] = None
        if self.aux_logits and self.training:
            aux1 = self.aux1(x)

        x = self.b4(x)
        x = self.c4(x)
        x = self.d4(x)
        aux2: Optional[Tensor] = None
        if self.aux_logits and self.training:
            aux2 = self.aux2(x)

        x = self.e4(x)
        x = self.maxpool4(x)
        x = self.a5(x)
        x = self.b5(x)
        x = self.avgpool(x)
        x = x.view(x.shape[0], -1) # x = x.reshape(x.shape[0], -1)
        x = self.linear(x)
        x = self.dropout(x)

        if self.aux_logits and self.training:
            return aux1, aux2
        else:
            return x


Inception 모듈에 들어갈 모델 구성은 Table 1의 GoogLeNet 아키텍처를 참고하였다.


전체 코드는 다음과 같이 구현할 수 있다.

import torch
import torch.nn as nn
from torch import Tensor
from typing import Optional

class Inception(nn.Module):
    def __init__(self, in_channels, n1x1, n3x3_reduce, n3x3, n5x5_reduce, n5x5, pool_proj) -> None:
        super(Inception, self).__init__()
        self.branch1 = ConvBlock(in_channels, n1x1, kernel_size=1, stride=1, padding=0)

        self.branch2 = nn.Sequential(
            ConvBlock(in_channels, n3x3_reduce, kernel_size=1, stride=1, padding=0),
            ConvBlock(n3x3_reduce, n3x3, kernel_size=3, stride=1, padding=1))
        
        self.branch3 = nn.Sequential(
            ConvBlock(in_channels, n5x5_reduce, kernel_size=1, stride=1, padding=0),
            ConvBlock(n5x5_reduce, n5x5, kernel_size=5, stride=1, padding=2))

        self.branch4 = nn.Sequential(
            nn.MaxPool2d(kernel_size=3, stride=1, padding=1),
            ConvBlock(in_channels, pool_proj, kernel_size=1, stride=1, padding=0))
        
    def forward(self, x: Tensor) -> Tensor:
        x1 = self.branch1(x)
        x2 = self.branch2(x)
        x3 = self.branch3(x)
        x4 = self.branch4(x)
        return torch.cat([x1, x2, x3, x4], dim=1)


class GoogLeNet(nn.Module):
    def __init__(self, aux_logits=True, num_classes=1000) -> None:
        super(GoogLeNet, self).__init__()
        assert aux_logits == True or aux_logits == False
        self.aux_logits = aux_logits

        self.conv1 = ConvBlock(in_channels=3, out_channels=64, kernel_size=7, stride=2, padding=3)
        self.maxpool1 = nn.MaxPool2d(kernel_size=3, stride=2, padding=1, ceil_mode=True)
        self.conv2 = ConvBlock(in_channels=64, out_channels=64, kernel_size=1, stride=1, padding=0)
        self.conv3 = ConvBlock(in_channels=64, out_channels=192, kernel_size=3, stride=1, padding=1)
        self.maxpool2 = nn.MaxPool2d(kernel_size=3, stride=2, ceil_mode=True)

        self.a3 = Inception(192, 64, 96, 128, 16, 32, 32)
        self.b3 = Inception(256, 128, 128, 192, 32, 96, 64)
        self.maxpool3 = nn.MaxPool2d(kernel_size=3, stride=2, padding=1, ceil_mode=True)
        self.a4 = Inception(480, 192, 96, 208, 16, 48, 64)
        self.b4 = Inception(512, 160, 112, 224, 24, 64, 64)
        self.c4 = Inception(512, 128, 128, 256, 24, 64, 64)
        self.d4 = Inception(512, 112, 144, 288, 32, 64, 64)
        self.e4 = Inception(528, 256, 160, 320, 32, 128, 128)
        self.maxpool4 = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
        self.a5 = Inception(832, 256, 160, 320, 32, 128, 128)
        self.b5 = Inception(832, 384, 192, 384, 48, 128, 128)
        self.avgpool = nn.AdaptiveAvgPool2d((1, 1))
        self.dropout = nn.Dropout(p=0.4)
        self.linear = nn.Linear(1024, num_classes)

        if self.aux_logits:
            self.aux1 = InceptionAux(512, num_classes)
            self.aux2 = InceptionAux(528, num_classes)
        else:
            self.aux1 = None
            self.aux2 = None

    def transform_input(self, x: Tensor) -> Tensor:
        x_R = torch.unsqueeze(x[:, 0], 1) * (0.229 / 0.5) + (0.485 - 0.5) / 0.5
        x_G = torch.unsqueeze(x[:, 1], 1) * (0.224 / 0.5) + (0.456 - 0.5) / 0.5
        x_B = torch.unsqueeze(x[:, 2], 1) * (0.225 / 0.5) + (0.406 - 0.5) / 0.5
        x = torch.cat([x_R, x_G, x_B], 1)
        return x
        
    def forward(self, x: Tensor) -> Tensor:
        x = self.transform_input(x)

        x = self.conv1(x)
        x = self.maxpool1(x)
        x = self.conv2(x)
        x = self.conv3(x)
        x = self.maxpool2(x)
        x = self.a3(x)
        x = self.b3(x)
        x = self.maxpool3(x)
        x = self.a4(x)
        aux1: Optional[Tensor] = None
        if self.aux_logits and self.training:
            aux1 = self.aux1(x)

        x = self.b4(x)
        x = self.c4(x)
        x = self.d4(x)
        aux2: Optional[Tensor] = None
        if self.aux_logits and self.training:
            aux2 = self.aux2(x)

        x = self.e4(x)
        x = self.maxpool4(x)
        x = self.a5(x)
        x = self.b5(x)
        x = self.avgpool(x)
        x = x.view(x.shape[0], -1) # x = x.reshape(x.shape[0], -1)
        x = self.linear(x)
        x = self.dropout(x)

        if self.aux_logits and self.training:
            return aux1, aux2
        else:
            return x


class ConvBlock(nn.Module):
    def __init__(self, in_channels, out_channels, **kwargs) -> None:
        super(ConvBlock, self).__init__()
        self.conv = nn.Conv2d(in_channels, out_channels, **kwargs)
        self.batchnorm = nn.BatchNorm2d(out_channels)
        self.relu = nn.ReLU()

    def forward(self, x: Tensor) -> Tensor:
        x = self.conv(x)
        x = self.batchnorm(x)
        x = self.relu(x)
        return x


class InceptionAux(nn.Module):
    def __init__(self, in_channels, num_classes) -> None:
        super(InceptionAux, self).__init__()
        self.avgpool = nn.AvgPool2d(kernel_size=5, stride=3)
        self.conv = ConvBlock(in_channels, 128, kernel_size=1, stride=1, padding=0)
        self.fc1 = nn.Linear(2048, 1024)
        self.fc2 = nn.Linear(1024, num_classes)
        self.dropout = nn.Dropout(p=0.7)
        self.relu = nn.ReLU()

    def forward(self, x: Tensor) -> Tensor:
        x = self.avgpool(x)
        x = self.conv(x)
        x = x.view(x.shape[0], -1)
        x = self.fc1(x)
        x = self.relu(x)
        x = self.dropout(x)
        x = self.fc2(x)
        return x
        
if __name__ == "__main__":
    x = torch.randn(3, 3, 224, 224)
    model = GoogLeNet(aux_logits=True, num_classes=1000)
    print (model(x)[1].shape)


위 코드에서 Auxiliary Classifier의 입력 차원 같은 경우는 논문에서 기술된 대로 512, 528을 적용해주었고, fully-connected layer 또한 1024개의 unit으로 설정해주었다.

만약 GoogLeNet으로 간단한 학습을 진행해보고자 할 때 다음과 같이 코드 구현이 가능하다. 다만 CIFAR10 데이터셋을 예시로 사용하므로 위 코드에서 num_classes=10로 바꿔줘야 한다.

import os
import numpy as np 
import torch
import torchvision.datasets as datasets
import torchvision.transforms as transforms
from torch.utils.data import DataLoader
import matplotlib.pyplot as plt
import argparse
from googlenet import GoogLeNet

def load_dataset():
    # preprocess
    transform = transforms.Compose([    
    transforms.ToTensor(),
    transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])

    # load train, test data
    train = datasets.CIFAR10(root="../data", train=True, transform=transform, download=True)
    test = datasets.CIFAR10(root="../data", train=False, transform=transform, download=True)
    train_loader = DataLoader(train, batch_size=args.batch_size, shuffle=True)
    test_loader = DataLoader(test, batch_size=args.batch_size, shuffle=False)
    return train_loader, test_loader


if __name__ == "__main__":
    # set hyperparameter
    parser = argparse.ArgumentParser()
    parser.add_argument('--batch_size', action='store', type=int, default=100)
    parser.add_argument('--learning_rate', action='store', type=float, default='0.0002')
    parser.add_argument('--n_epochs', action='store', type=int, default=100)
    parser.add_argument('--plot', action='store', type=bool, default=True)
    args = parser.parse_args()
    
    np.random.seed(1)
    seed = torch.manual_seed(1)

    # load dataset
    train_loader, test_loader = load_dataset()

    # model, loss, optimizer
    losses = []
    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
    model = GoogLeNet(aux_logits=False, num_classes=10).to(device)
    criterion = torch.nn.CrossEntropyLoss()
    optimizer = torch.optim.Adam(model.parameters(), lr=args.learning_rate)

    # train
    for epoch in range(args.n_epochs):
        model.train()
        train_loss = 0
        correct, count = 0, 0
        for batch_idx, (images, labels) in enumerate(train_loader, start=1):
            images, labels = images.to(device), labels.to(device)
            optimizer.zero_grad()
            output = model.forward(images)
            loss = criterion(output, labels)
            loss.backward()
            optimizer.step()

            train_loss += loss.item()
            _, preds = torch.max(output, 1)
            count += labels.size(0)
            correct += preds.eq(labels).sum().item() # torch.sum(preds == labels)

            if batch_idx % 100 == 0:
                print (f"[*] Epoch: {epoch} \tStep: {batch_idx}/{len(train_loader)}\tTrain accuracy: {round((correct/count), 4)} \tTrain Loss: {round((train_loss/count)*100, 4)}")
        
        # valid
        model.eval()
        correct, count = 0, 0
        valid_loss = 0
        with torch.no_grad():
            for batch_idx, (images, labels) in enumerate(test_loader, start=1):
                images, labels = images.to(device), labels.to(device)
                output = model.forward(images)
                loss = criterion(output, labels)
                valid_loss += loss.item()
                _, preds = torch.max(output, 1)
                count += labels.size(0)
                correct += preds.eq(labels).sum().item() # torch.sum(preds == labels)
                if batch_idx % 100 == 0:
                    print (f"[*] Step: {batch_idx}/{len(test_loader)}\tValid accuracy: {round((correct/count), 4)} \tValid Loss: {round((valid_loss/count)*100, 4)}")

        # if epoch % 10 == 0:
        #     if not os.path.isdir('../checkpoint'):
        #         os.makedirs('../checkpoint', exists_ok=True)
        #     checkpoint_path = os.path.join(f"../checkpoint/googLeNet{epoch}.pth")
        #     state = {
        #         'epoch': epoch,
        #         'model': model.state_dict(),
        #         'optimizer': optimizer.state_dict(),
        #         'seed': seed,
        #     }
        #     torch.save(state, checkpoint_path)


전체 코드: https://github.com/roytravel/paper-implementation

Reference
[1] https://velog.io/@euisuk-chung/파이토치-파이토치로-CNN-모델을-구현해보자-GoogleNet
[2] https://wikidocs.net/137251
[3] https://github.com/aladdinpersson/Machine-Learning-Collection/blob/master/ML/Pytorch/CNN_architectures/pytorch_inceptionet.py
[4] https://github.com/Lornatang/GoogLeNet-PyTorch/blob/main/model.py
[5] https://github.com/weiaicunzai/pytorch-cifar100/blob/master/models/googlenet.py
[6] https://github.com/soapisnotfat/pytorch-cifar10/blob/master/models/GoogleNet.py
[7] https://github.com/Mayurji/Image-Classification-PyTorch/blob/main/GoogLeNet.py
[8] https://github.com/cxy1997/MNIST-baselines/blob/master/models/googlenet.py

[논문 정보]

제목: Very Deep Convolutional Networks For Large-Scale Image Recognition

게재: 2015년

학회: ICLR (International Conference on Learning Representations)

 

이 글에서는 크게 두 개의 파트로, 첫 번째론 VGGNet 논문의 연구를 설명하고, 두 번째로 VGGNet 모델을 구현한다.

 

개요

VGGNet은 2014년 ILSVRC에서 localisation과 classification 트랙에서 각각 1위와 2위를 달성한 모델이다. 이 모델이 가지는 의미는 2012년 혁신적이라 평가받던 AlexNet보다도 높은 성능을 갖는다는 점에서 의미가 있다. 실제로 아래 벤치마크 결과를 확인해보면 맨 아래에 있는 Krizhevsky et al. (Alexnet) 보다도 성능이 월등히 좋은 것을 확인할 수 있다.

 

 

2010년 초/중반에는 위 성능 비교표가 나타내는 것처럼 Convolutional network를 사용한 모델의 성능이 점점 높아졌다. 이러한 성능 증가의 배경에는 ImageNet과 같은 large scale 데이터를 활용가능 했기 때문이고 high performance를 보여주는 GPU를 사용했다는 점이다. 

 

2012년 AlexNet이 나온 이후에는 AlexNet을 개선하려는 시도들이 많았다. 더 작은 stride를 사용하거나 receptive window size를 사용하는 등 다양한 시도들이 있었다. VGGNet은 그 중에서 네트워크의 깊이 관점으로 성능을 올린 결과물이라 할 수 있다. 

 

VGGNet을 만들기 위해 사용했던 아키텍처를 살펴보면 아래와 같다. 모든 모델 공통적으로 convolutional layer이후 fully-connected layer가 뒤 따르는 구조이다. 모델 이름은 A, A-LRN, B, C, D, E로 지정했으며 이 중 VGG라 불리는 것은 D와 E이다. 참고로 간결성을 위해 ReLU 함수는 아래 표에 추가하지 않았다. 

 

표 1. Convolutional Network Configuration

 

이 논문에선 실험을 위해 네트워크의 깊이를 늘려가면서도 동시에 receptive field를 3x3과 1x1로 설정했다. receptive field는 컨볼루션 필터가 한 번에 보는 영역의 크기를 의미한다. AlexNet은 receptive field가 11x11의 크기지만 VGGNet은 3x3으로 설정함으로써 파라미터 수를 줄이는 효과를 가져왔고 그 결과 성능이 증가했다. 실제로 3x3이 두 개가 있으면 5x5가 되고 3개를 쌓으면 7x7이 된다. 이 때 5x5 1개를 쓰는 것 보다 3x3 2개를 써서 layer를 더 깊이할 수록 비선형성이 증가하기에 더 유용한 feature를 추출할 수 있기에 위 아키텍처에서는 receptive field가 작은 convolution layer를 여러 개를 쌓았다. 참고로 receptive field를 1x1로 설정함으로써 컨볼루션 연산 후에도 이미지 공간 정보를 보존하는 효과를 가져왔다고 한다.

 

위 표 1에서 확인할 수 있는 A-LRN 모델을 통해서 보인 것은 AlexNet에서 도입한 LRN(Local Response Network)이 사실상 성능 증가에 도움되지 않고 오히려 메모리 점유율과 계산 복잡도만 더해진다는 것이다. 아래 표를 통해 LRN의 무용성을 확인할 수 있다. 또한 표 1에서 네트워크 깊이가 가장 깊은 모델 E (VGGNet)가 가장 좋은 성능을 보이는 것을 확인할 수 있다. 

 

표 3. single test scale에서의 ConvNet 성능 비교

 

참고로, S, Q는 각각 train과 test에 사용한 이미지 사이즈 크기를 의미한다. 이를 기술한 이유는 scale jittering이라는 이미지 어그멘테이션 기법을 적용했을 때 성능이 더 높아진다는 것을 보인 것이다. 이 논문에선 scale jittering에 따라 달라지는 성능을 비교하기 위해 single-scale training과 multi-scale training으로 나누어 진행했다. single-scale training이란 이미지의 크기를 고정하는 것이다. 가령 train 때 256 또는 384로 진행하면 test때도 각각 256 또는 384로 추론을 진행하는 것이다. 반면 multi-scale training의 경우 S를 256 ~ 512에서 랜덤으로 크기가 정해지는 것이다. 이미지 크기의 다양성 때문에 모델이 조금더 robustness해지며 정확도가 높아지는 효과가 있다. 아래 표 4는 multi-scale training을 적용했을 때의 성능 결과이다.

 

표 4. multi test scale에서의 ConvNet 성능 비교

 

single-scale training에 비해 성능이 더욱 증가한 것을 알 수 있다. 추가적으로 S를 고정 크기의 이미지로 사용했을 경우보다 S를 256 ~ 512사이의 랜덤 크기의 이미지를 사용했을 때 더욱 성능이 높아지는 것을 확인할 수 있다.

 

또한 이 논문에서는 모델의 성능을 높이기 위해 evaluation technique으로 multi-crop을 진행했다. dense와 multi-crop을 동시에 사용하는 것이 더 높은 성능을 보이는 것을 확인할 수 있다.

 

표 5. ConvNet evaluation 테크닉 비교

 

마지막으로 여지껏 개별 ConvNet 모델에 대해 성능을 평가했다면 앙상블을 통해 여러 개의 모델의 출력을 결합하고 평균을 냈다. 결론적으로 D, E 두 모델의 앙상블을 사용해 오류율을 6.8%까지 현저히 줄이는 것을 보였다.

 

표 6. Multiple ConvNet fusion 결과

 

여기까지가 VGGNet 논문에 대한 내용이다. 다음은 VGGNet 논문 구현에 관한 것이다. 이 논문을 읽으며 구현해야 할 목록을 정리해본 것은 다음과 같다. 

 

1. Model configuration.

Convolution Filter

  • 3x3 receptive field
    • 좌/우/상/하를 모두를 포착할 수 있는 가장 작은 사이즈
  • 1x1 convolution filter 사용
    • spacial information 보존을 위함

Convolution Stride

  • 1로 고정

Max-Pooling Layer

  • kernel size: 2x2
  • stride: 2
  • 5개의 max-pooling 레이어에 의해 수행됨. 대부분 max-pooling은 conv layer와 함께하지만 그렇지 않은 것도 있음

Fully-Connected Layer

  • convolutional layer 스택 뒤에 3개의 FC layer가 따라 옴
  • 앞의 2개는 4096 채널을 각각 가지고 마지막은 1000개를 가지면서 소프트맥스 레이어가 사용됨

Local Response Normalization

  • Local Response Normalization 사용 X: LRN Layer가 성능 개선X이면서 메모리 점유율과 계산 복잡도만 높아짐

 

2. Initialization

잘못된 초기화는 deep network에서 gradient 불안정성으로 학습 지연시킬 수 있기에 중요함

  • weight sampling: 평균 0, 분산이 $10^{-2}$ variance인 정규분포 사용
  • bias: 0로 초기화

 

3. Augmentation

  • random crop:고정된 224x224 입력 이미지를 얻기 위함
  • horizontal flip
  • random RGB color shift
  • 고정 사이즈 224x224 RGB image를 사용했고, 각 픽셀에 대해 RGB value의 mean을 빼주는 것이 유일한 전처리(?)

 

4. Hyper-parameter

  • optimizer: SGD
  • momentum: 0.9
  • weight decay: L2 $5\cdot 10^{-4} = 0.0005$
  • batch size: 256
  • learning rate: 0.1
    • validation set 정확도가 증가하지 않을 때 10을 나눔
    • 학습은 370K iteration (74 epochs)에서 멈춤
  • dropout: 0.5
    • 1, 2번째 FC layer에 추가

 

이를 바탕으로 VGGNet에 대한 코드를 구현하면 다음과 같다. 크게 1. 모델 레이어 구성 2. 가중치 초기화 3. 하이퍼파라미터 설정 4. 데이터 로딩 5. 전처리 6. 학습으로 구성된다 볼 수 있다. 

 

import time
import torch
import torch.nn as nn
import torchvision.datasets as datasets
import torchvision.transforms as transforms
from torch.utils import data

CONFIGURES = {
    "VGG11": [64, 'M', 128, 'M', 256, 256, 'M', 512, 512, 'M', 512, 512, 'M'],
    "VGG13": [64, 64, 'M', 128, 128, 'M', 256, 256, 'M', 512, 512, 'M', 512, 512, 'M'],
    "VGG16": [64, 64, 'M', 128, 128, 'M', 256, 256, 256, 'M', 512, 512, 512, 'M', 512, 512, 512, 'M'],
    "VGG19": [64, 64, 'M', 128, 128, 'M', 256, 256, 256, 256, 'M', 512, 512, 512, 512, 'M', 512, 512, 512, 512, 'M'],
}

class VGGNet(nn.Module):
    def __init__(self, num_classes: int = 1000, init_weights: bool = True, vgg_name: str = "VGG19") -> None:
        super(VGGNet, self).__init__()
        self.num_classes = num_classes
        self.features = self._make_layers(CONFIGURES[vgg_name], batch_norm=False)
        self.avgpool = nn.AdaptiveAvgPool2d(output_size=(7, 7))
        self.classifier = nn.Sequential(
            nn.Linear(in_features=512 * 7 * 7, out_features=4096),
            nn.ReLU(inplace=True),
            nn.Dropout(p=0.5),
            nn.Linear(in_features=4096, out_features=4096),
            nn.ReLU(inplace=True),
            nn.Dropout(p=0.5),
            nn.Linear(in_features=4096, out_features=num_classes),
        )
        if init_weights:
            self._init_weight()


    def _init_weight(self) -> None:
        for m in self.modules():
            if isinstance(m, nn.Conv2d):
                nn.init.kaiming_normal_(m.weight, mode="fan_out", nonlinearity="relu") # fan out: neurons in output layer
                if m.bias is not None:
                    nn.init.constant_(m.bias, val=0)
            elif isinstance(m, nn.BatchNorm2d):
                nn.init.constant_(m.weight, val=1)
                nn.init.constant_(m.bias, val=0)
            elif isinstance(m, nn.Linear):
                nn.init.normal_(m.weight, mean=0, std=0.01)
                nn.init.constant_(m.bias, val=0)


    def forward(self, x: torch.Tensor) -> torch.Tensor:
        x = self.features(x)
        x = self.avgpool(x)
        # print (x.size()) # torch.Size([2, 512, 7, 7])
        x = x.view(x.size(0), -1) # return torch.Size([2, 1000])
        x = self.classifier(x)
        return x


    def _make_layers(self, CONFIGURES:list, batch_norm: bool = False) -> nn.Sequential:
        layers: list = []
        in_channels = 3
        for value in CONFIGURES:
            if value == "M":
                layers += [nn.MaxPool2d(kernel_size=2, stride=2)]
            else:
                conv2d = nn.Conv2d(in_channels=in_channels, out_channels=value, kernel_size=3, padding=1)
                if batch_norm:
                    layers += [conv2d, nn.BatchNorm2d(value), nn.ReLU(inplace=True)]
                else:
                    layers += [conv2d, nn.ReLU(inplace=True)]

                in_channels = value
        return nn.Sequential(*layers)


if __name__ == "__main__":
    # for simple test
    # x = torch.randn(1, 3, 256, 256)
    # y = vggnet(x)
    # print (y.size(), torch.argmax(y))

    # set hyper-parameter
    seed = torch.initial_seed()
    BATCH_SIZE= 256
    NUM_EPOCHS = 100
    LEARNING_RATE = 0.1 # 0.001
    CHECKPOINT_PATH = "./checkpoint/"
    device = "cuda" if torch.cuda.is_available() else "cpu"

    vggnet = VGGNet(num_classes=10, init_weights=True, vgg_name="VGG19")

    # preprocess = transforms.Compose([
    #     transforms.RandomResizedCrop(size=224),
    #     transforms.RandomHorizontalFlip(),
    #     transforms.ColorJitter(),
    #     transforms.ToTensor(),
    #     transforms.Normalize(mean=(0.48235, 0.45882, 0.40784), std=(1.0/255.0, 1.0/255.0, 1.0/255.0))
    # ])
    preprocess = transforms.Compose([
        transforms.Resize(224),
        # transforms.RandomCrop(224),
        transforms.ToTensor(),
        #transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)),
    ])

    train_dataset = datasets.STL10(root='./data', download=True, split='train', transform=preprocess)
    train_dataloader = data.DataLoader(train_dataset, batch_size=BATCH_SIZE, shuffle=True)

    test_dataset = datasets.STL10(root='./data', download=True, split='test', transform=preprocess)
    test_dataloader = data.DataLoader(test_dataset, batch_size=BATCH_SIZE, shuffle=False)
    
    criterion = nn.CrossEntropyLoss().cuda()
    optimizer = torch.optim.SGD(lr=LEARNING_RATE, weight_decay=5e-3, params=vggnet.parameters(), momentum=0.9)
    scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, factor=0.1, patience=3, verbose=True)
    vggnet = torch.nn.parallel.DataParallel(vggnet, device_ids=[0, ])

    start_time = time.time()
    """ labels = ['airplane', 'bird', 'car', 'cat', 'deer', 'dog', 'horse', 'monkey', 'ship', 'truck']"""
    for epoch in range(NUM_EPOCHS):
        # print("lr: ", optimizer.param_groups[0]['lr'])
        for idx, _data in enumerate(train_dataloader, start=0):
            images, labels = _data
            images, labels = images.to(device), labels.to(device)
            optimizer.zero_grad()
            output = vggnet(images)
            loss = criterion(output, labels)
            loss.backward()
            optimizer.step()

            if idx % 10 == 0:
                with torch.no_grad():
                    _, preds = torch.max(output, 1)
                    accuracy = torch.sum(preds == labels)
                    print ('Epoch: {} \tStep: {}\tLoss: {:.4f} \tAccuracy: {}'.format(epoch+1, idx, loss.item(), accuracy.item() / BATCH_SIZE))
                    scheduler.step(loss)
                    

        #checkpoint_path = os.path.join(CHECKPOINT_PATH)
        state = {
            'epoch': epoch,
            'optimizer': optimizer.state_dict(),
            'model': vggnet.state_dict(),
            'seed': seed,
        }
        if epoch % 10 == 0:
            torch.save(state, CHECKPOINT_PATH+'model_{}.pth'.format(epoch))

 

transforms.Compose 메서드에서 Normalize에 들어갈 mean과 std를 구하는 과정은 아래 링크를 참조할 수 있다.

https://github.com/Seonghoon-Yu/AI_Paper_Review/blob/master/Classification/VGGnet(2014).ipynb 

 

Code: https://github.com/roytravel/paper-implementation/tree/master/vggnet

 

 

Reference

[1] https://github.com/minar09/VGG16-PyTorch/blob/master/vgg.py

[2] https://github.com/pytorch/vision/blob/main/torchvision/models/vgg.py

[3] https://github.com/kuangliu/pytorch-cifar/blob/master/models/vgg.py

[논문 정보]

제목: ImageNet Classification with Deep Convolutional Neural Networks

게재: 2012년

학회: NIPS (Neural Information Processing Systems)

 

1. 개요

AlexNet은 이미지 분류 대회인 *ILSVRC에서 우승을 차지한 모델로 제프리 힌튼 교수 그룹이 만들었다. AlexNet이 가지는 의미는 2012년 ILSVRC에서 이미지 인식 능력이 크게 향상 되고 오류율이 크게 줄게 됐다는 것이다. 

*ILSVRC: ImageNet Large Scale Visual Recognition Competition

 

 

2. 아키텍처

AlexNet은 DCNN 구조를 가지는 모델로 5개의 convolution layer와 3개의 fully-connected layer로 구성되어 있다. 

 

 

  • 위 아키텍처 삽화에서 2가지 오탈자가 있다. 첫 번째론 입력 레이어의 입력 이미지 크기가 224로 되어 있지만 227이 되어야 한다. 두 번째론 두 번째 convolution layer에서 kernel size가 3x3이지만 5x5가 되어야 한다. 
  • 전체 아키텍처에서 top/bottom으로 두 그룹으로 나뉘어 있는데 이는 GPU 2개를 병렬로 사용했기 때문이다. 
  • 레이어 각각의 Input/output과 파라미터를 계산하면 다음과 같다.

 

Index Convolution Max Pooling Normalization Fully Connected
1 input: 227x227x3
output: 55x55x96
96 kernels of size 11x11x3, stride=4, padding=0
input: 55x55x96
output: 27x27x96
3x3 kernel, stride=2
input: 27x27x96
output: 27x27x96
none
2 input: 27x27x96
output: 13x13x256
256 kernels of size 5x5, stride=1, padding=2
input: 27x27x256
output: 13x13x256
3x3 kernel, stride= 2
input: 13x13x256
output: 13x13x256
none
3 input: 13x13x256
output: 13x13x384
384 kernels of size 3x3, stride=1, padding=1
none none none
4 input: 13x13x384
output: 13x13x256
384 kernels of size 3xq3, stride=1, padding=1
none none none
5 input: 13x13x384
output: 13x13x256
256 kernels of size 3x3, stride=1, padding=1
none none none
6 none none none input: 6x6x256
output: 4096
parameter: 4096 neurons
7 none none none input: 4096
output: 4096
8 none none none input: 4096
output: 1000 softmax classes

 

 

 

3. 구현 목록 정리

3.1 레이어 구성 및 종류

  • 5 convolution layers, max-pooling layers, 3 fully-connected layers 
    • overfitting 해결 위해 5개 convoutiona layer,  3개 fully-connected layer를 사용했다함
  • Dropout
    • overfitting 방지 위해 fully-connected layer에 적용
    • 레이어 추가 위치는 1,2 번째 fully-connected layer에 적용
    • dropout rate = 0.5
  • Local Response Normalization
    • $k$ = 2, $n$ = 5, $\alpha = 10^{-4}$, $\beta = 0.75$
    • 레이어 추가 위치는 1,2 번째 convolution layer 뒤에 적용
    • 적용 배경은 모델의 일반화를 돕는 것을 확인 (top-1, top-2 error율을 각각 1.4%, 1.2% 감소)
  • Activation Function
    • ReLU를 모든 convolution layer와 fully-connected에 적용
    • 적용 배경은 아래 그래프처럼 실선인 ReLU가 점선인 tanH보다 빠르게 학습했음

 

2. 하이퍼 파라미터

  • optimizer: SGD
  • momentum: 0.9
  • weight decay: 5e-4
  • batch size: 128
  • learning rate: 0.01
  • adjust learning rate: validation error가 현재 lr로 더 이상 개선 안되면 lr을 10으로 나눠줌. 0.01을 lr 초기 값으로 총 3번 줄어듦
  • epoch: 90

그리고 별도로 레이어에 가중치 초기화를 진행 해줌

  • 편차를 0.01로 하는 zero-mean 가우시안 정규 분포를 모든 레이어의 weight를 초기화
  • neuron bias: 2, 4, 5번째 convolution 레이어와 fully-connected 레이어에 상수 1로 적용하고 이외 레이어는 0을 적용.
def _init_bias(self):
    for layer in self.layers:
        if isinstance(layer, nn.Conv2d):
            nn.init.normal_(layer.weight, mean=0, std=0.01)
            nn.init.constant_(layer.bias, 0)

    nn.init.constant_(self.layers[4].bias, 1)
    nn.init.constant_(self.layers[10].bias, 1)
    nn.init.constant_(self.layers[12].bias, 1)
    nn.init.constant_(self.classifier[1].bias, 1)
    nn.init.constant_(self.classifier[4].bias, 1)
    nn.init.constant_(self.classifier[6].bias, 1)

참고로 헷갈렸던 것은 위 nn.init.constant_(layer.bias, 0)에서의 0은 bool로 편향 존재 여부를 나타내는 것이지 bias를 0으로 설정하는 것이 아니다. 

 

3. 이미지 전처리

  • 고화질 이미지를 256x256 사이즈로 다운 샘플링후 이미지의 center에서 cropped out
  • 각 픽셀에서 training set에 대한 평균 값을 빼줌

 

이를 바탕으로 전체 코드를 구현하면 아래와 같다. 크게 나누어 보자면 5가지 정도가 될 수 있다.

1. 레이어 구성 2. 가중치 초기화 3. 하이퍼파라미터 설정 4. 이미지 전처리 5. 학습 로직 작성이다. 참고로 이미지 전처리에 사용하는 transform 메서드에서 사용되는 상수 값은 별도로 논문에 기재되어 있지 않기에 pytorch 공식 documentation에서 기본 값을 가져와서 사용했다.

import os
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim
import torchvision.datasets as datasets
import torchvision.transforms as transforms
from torch.utils import data
import logging
from glob import glob

logging.basicConfig(level=logging.INFO)
class AlexNet(nn.Module):
    def __init__(self, num_classes=1000):
        super().__init__()
        self.num_classes = num_classes
        print (f"[*] Num of Classes: {self.num_classes}")
        self.layers = nn.Sequential(
            nn.Conv2d(kernel_size=11, in_channels=3, out_channels=96, stride=4, padding=0),
            nn.ReLU(), # inplace=True mean it will modify input. effect of this action is reducing memory usage. but it removes input.
            nn.LocalResponseNorm(alpha=1e-3, beta=0.75, k=2, size=5),
            nn.MaxPool2d(kernel_size=3, stride=2),
            nn.Conv2d(kernel_size=5, in_channels=96, out_channels=256, padding=2, stride=1),
            nn.ReLU(),
            nn.LocalResponseNorm(alpha=1e-3, beta=0.75, k=2, size=5),
            nn.MaxPool2d(kernel_size=3, stride=2),
            nn.Conv2d(kernel_size=3, in_channels=256, out_channels=384, padding=1, stride=1),
            nn.ReLU(),
            nn.Conv2d(kernel_size=3, in_channels=384, out_channels=384, stride=1, padding=1),
            nn.ReLU(),
            nn.Conv2d(kernel_size=3, in_channels=384, out_channels=256, stride=1, padding=1),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=3, stride=2))
        self.avgpool = nn.AvgPool2d((6, 6))
        self.classifier = nn.Sequential(
            nn.Dropout(p=0.5),
            nn.Linear(in_features=256, out_features=4096),
            nn.ReLU(),
            nn.Dropout(p=0.5),
            nn.Linear(in_features=4096, out_features=4096),
            nn.ReLU(),
            nn.Linear(in_features=4096, out_features=self.num_classes))

        self._init_bias()


    def _init_bias(self):
        for layer in self.layers:
            if isinstance(layer, nn.Conv2d):
                nn.init.normal_(layer.weight, mean=0, std=0.01)
                nn.init.constant_(layer.bias, 0)

        nn.init.constant_(self.layers[4].bias, 1)
        nn.init.constant_(self.layers[10].bias, 1)
        nn.init.constant_(self.layers[12].bias, 1)
        nn.init.constant_(self.classifier[1].bias, 1)
        nn.init.constant_(self.classifier[4].bias, 1)
        nn.init.constant_(self.classifier[6].bias, 1)


    def forward(self, x: torch.Tensor) -> torch.Tensor:
        x = self.layers(x)
        x = self.avgpool(x)
        x = torch.flatten(x, 1) # 1차원화
        x = self.classifier(x)
        return x


if __name__ == "__main__":
    seed = torch.initial_seed()
    print (f'[*] Seed : {seed}')
    NUM_EPOCHS = 1000 # 90
    BATCH_SIZE = 128
    NUM_CLASSES = 1000
    LEARNING_RATE = 0.01
    IMAGE_SIZE = 227
    TRAIN_IMG_DIR = "C:/github/paper-implementation/data/ILSVRC2012_img_train/"
    #VALID_IMG_DIR = "<INPUT VALID IMAGE DIR>"
    CHECKPOINT_PATH = "./checkpoint/"

    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
    print (f'[*] Device : {device}')

    alexnet = AlexNet(num_classes=NUM_CLASSES).cuda()
    checkpoints = glob(CHECKPOINT_PATH+'*.pth') # Is there a checkpoint file?
    if checkpoints:
        checkpoint = torch.load(checkpoints[-1])
        alexnet.load_state_dict(checkpoint['model'])
    #alexnet = torch.nn.parallel.DataParallel(alexnet, device_ids=[0,]) # for distributed training using multi-gpu

    transform = transforms.Compose(
        [transforms.CenterCrop(IMAGE_SIZE),
         transforms.RandomHorizontalFlip(),
         transforms.ToTensor(),
         transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
        ])
        
    train_dataset = datasets.ImageFolder(TRAIN_IMG_DIR, transform=transform)
    print ('[*] Dataset Created')

    train_dataloader = data.DataLoader(
        train_dataset,
        shuffle=True,
        pin_memory=False, # more training speed but more memory
        num_workers=8,
        drop_last=True,
        batch_size=BATCH_SIZE
    )
    print ('[*] DataLoader Created')

    optimizer = torch.optim.SGD(momentum=0.9, weight_decay=5e-4, params=alexnet.parameters(), lr=LEARNING_RATE) # SGD used in original paper
    print ('[*] Optimizer Created')

    lr_scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer=optimizer, factor=0.1, verbose=True, patience=4) # used if valid error doesn't improve.
    print ('[*] Learning Scheduler Created')

    steps = 1
    for epoch in range(50, NUM_EPOCHS):
        logging.info(f" training on epoch {epoch}...")        
        for batch_idx, (images, classes) in enumerate(train_dataloader):
            images, classes = images.cuda(), classes.cuda()
            output = alexnet(images)
            loss = F.cross_entropy(input=output, target=classes)
            optimizer.zero_grad()
            loss.backward()
            optimizer.step()
            if steps % 50 == 0:
                with torch.no_grad():
                    _, preds = torch.max(output, 1)
                    accuracy = torch.sum(preds == classes)
                    print ('[*] Epoch: {} \tStep: {}\tLoss: {:.4f} \tAccuracy: {}'.format(epoch+1, steps, loss.item(), accuracy.item() / BATCH_SIZE))
            steps = steps + 1

        lr_scheduler.step(metrics=loss)

        if epoch % 5 == 0:
            checkpoint_path = os.path.join(CHECKPOINT_PATH, "model_{}.pth".format(epoch))
            state = {
                'epoch': epoch,
                'optimizer': optimizer.state_dict(),
                'model': alexnet.state_dict(),
                'seed': seed
            }
            torch.save(state, checkpoint_path)

 

AlexNet의 경우 비교적 구현이 어렵지 않은 편이기에 논문 구현을 연습하기에 좋다고 느낀다.

 

Code: https://github.com/roytravel/paper-implementation

 

Reference

[1] https://github.com/YOUSIKI/PyTorch-AlexNet/blob/de241e90d3cb6bd3f8c94f88cf4430cdaf1e0b55/main.py

[2] https://github.com/Ti-Oluwanimi/Neural-Network-Classification-Algorithms/blob/main/AlexNet.ipynb

[3] https://github.com/daeunni/CNN_PyTorch-codes/blob/main/AlexNet(2012).ipynb

[4] https://deep-learning-study.tistory.com/376

 

링크드 리스트란 데이터와 포인터를 담은 노드를 연결시킨, 연결 자료구조다. 다음 노드만 가리키는 포인터만 있다면 단순 연결 리스트(Single Linked List)라 하고, 다음 노드와 이전 노드를 가리키는 포인터 두 개가 있다면 이중 연결 리스트(Doubly Linked List)라고 한다. 만약 두 링크드 리스트에서 head와 tail이 서로 앞 뒤로 연결되어 있다면 환형 연결 리스트(Circular Linked List)이다. 

 

링크드리스트는 순차 자료구조인 배열과 달리 포인터를 사용하기에 삽입과 삭제가 용이하다. 또한 배열과 달리 크기를 동적으로 조절할 수 있는 것도 장점이다. 반면 링크드 리스트는 특정 노드를 바로 접근할 수 없는 것이 단점이다. 링크드 리스트를 전부를 탐색해야 할 수 있도 있다. 따라서 링크드 리스트와 배열은 경우에 따라 나누어 사용해야 한다. 데이터가 빈번히 추가되거나 삭제될 경우 링크드리스트를 쓰는 것이 적합하고, 데이터 탐색과 정렬이 위주라면 배열을 쓰는 것이 적합하다. 참고로 윈도우 작업 관리자가 대표적인 링크드 리스트를 사용한 구현물이다. 하나의 프로세스는 앞 뒤로 다른 프로세스들과 연결되어 있는 Circular Linked List 구조로 되어 있다. 첫 번째 프로세스부터 다음 포인터를 계속 탐색하면 모든 프로세스 리스트를 가져올 수 있다.

 

단순 연결 리스트의 경우 기능은 크게 삽입/삭제/조회/탐색으로 구성된다. 이를 구현하기 위해 노드에 정보를 담을 Node 클래스와 기능을 구현할 SinglyLinkedList 클래스가 필요하다. 구현물은 다음과 같다.

 

class Node:
    def __init__(self, data, next = None) -> None:
        self.data = data
        self.next = next

class SinglyLinkedList:
    def __init__(self) -> None:
        self.head = None
        self.size = 0

    def size(self):
        return self.size

    def is_empty(self):
        return self.size == 0

    def insert_front(self, data):
        if self.is_empty():
            self.head = Node(data, None)
        else:
            self.head = Node(data, self.head)
        self.size += 1

    def insert_back(self, data, current):
        current.next = Node(data, current.next)
        self.size += 1
        
    def delete_front(self):
        if self.is_empty():
            raise LookupError("There is no node in the linked list")
        self.head = self.head.next
        self.size -= 1

    def delete_back(self, current):
        if self.is_empty():
            raise LookupError("There is no node in the linked list")
        temp = current.next
        current.next = temp.next
        self.size -= 1

    def traverse(self):
        array = []
        current = self.head
        while current:
            array.append(current.data)
            current = current.next
        return array

    def search(self, target):
        current = self.head
        for i in range(self.size):
            if current.data == target:
                return i
            current = current.next
        return None

    def print_list(self):
        current = self.head
        while current:
            if current.next != None:
                print (current.data, '→ ', end='')
            else:
                print (current.data)
            
            current = current.next

if __name__ == "__main__":
    S = SinglyLinkedList()

    # Insert
    S.insert_front('apple')
    S.insert_front('bear')
    S.insert_front('cat')
    S.insert_back('dog', S.head.next)
    S.insert_front('egg')
    S.insert_front('fish')

    # Search
    S.print_list() # fish → egg → cat → bear → dog → apple
    print(f"Index: {S.search('dog')}") # Index: 4
    
    # Delete
    S.delete_front() 
    S.print_list() # egg → cat → bear → dog → apple

    S.delete_back(S.head)
    S.print_list() # egg → bear → dog → apple

    # Traverse
    print (S.traverse()) # ['egg', 'bear', 'dog', 'apple']

 

더블 링크드 리스트 또한 구현을 위해 노드에 정보를 담을 Node 클래스와 기능을 구현할 SinglyLinkedList 클래스가 필요하다. 구현물은 다음과 같다.

 

class Node:
    def __init__(self, data=None, prev=None, next=None):
        self.data = data
        self.prev = prev
        self.next = next

class DoublyLinkedList:
    def __init__(self):
        self.head = Node()
        self.tail = Node(prev=self.head)
        self.head.next = self.tail
        self.size = 0

    def size(self):
        return self.size

    def is_empty(self):
        return self.size == 0

    def insert_front(self, current, data):
        temp = current.prev
        next = Node(data, temp, current)
        current.prev = next
        temp.next = next 
        self.size += 1 

    def insert_back(self, current, data):
        temp = current.next
        next = Node(data, current, temp)
        temp.prev = next
        current.next = next
        self.size += 1

    def delete(self, current):
        front = current.prev
        back = current.next
        front.next = back
        front.prev = back
        self.size -= 1
        return current.data

    def print_list(self):
        if self.is_empty():
            raise LookupError('There is no node in the linked list')
        else:
            current = self.head.next
            while current != self.tail:
                if current.next != self.tail:
                    print(current.data, '←→ ', end='')
                else:
                    print(current.data)
                current = current.next

D = DoublyLinkedList()
D.insert_back(D.head, 'apple') 
D.insert_front(D.tail, 'bear')
D.insert_front(D.tail, 'cat')
D.insert_back(D.head.next, 'dog')
D.print_list() # apple ←→ dog ←→ bear ←→ cat

D.delete(D.tail.prev)
D.print_list() # apple ←→ dog ←→ bear

D.insert_front(D.tail.prev, 'fish') # apple ←→ dog ←→ bear ←→ fish ←→ cat
D.print_list()

D.delete(D.head.next) # dog ←→ bear ←→ fish ←→ cat
D.print_list()

D.delete(D.tail.prev) # dog ←→ bear ←→ fish
D.print_list()

 

 

Reference

[1] https://reakwon.tistory.com/25

[2] https://daimhada.tistory.com/72

[3] https://it-garden.tistory.com/318

[4] https://it-garden.tistory.com/326 

연기론이란?

연기론이란 싯다르타가 설파하고자 했던 핵심 수행법이자 가르침이다. 먼저 연기론에서의 연기란 한 마디로 상호의존성이다. 이것이 있기에 저것이 있고 저것이 있기에 이것이 있으므로 상호의존한다는 의미한다. 즉 세상의 존재라고 부르는 것은 독립적으로 존재할 수 없고 상호의존적으로 존재하는 것이다. 다르게 말하면 원인이 있기에 결과가 있고 결과가 있기에 원인이 있다. 그리고 원인과 결과는 둘이 아닌 하나이다 라고도 표현하며, 인식자가 있기에 인식대상이 있고 인식대상이 있기에 인식자가 있다고도 표현한다. 결국 존재의 상호의존성을 말한다. 사물들은 결코 본래 홀로 존재한 것이 아니라 다른 것들에 의해 생겨났다. 이를 연기된 존재라고 말하며 실재가 아닌 환영과 같은 존재이다. 존재하지만 존재하지 않는 것이다. 이렇듯 존재의 실상이 무엇인지 깨달을 수 있도록 한 내용이 연기론이다.

 

깨달음이란?

깨달음이란 나와 나를 둘러싼 세상이란 존재가 무엇인지를 깨닫는 것이다. 즉 존재의 실체가 무엇인지 아는 것이다. 이를 깨닫게 되면 반야(般若)의 지혜를 얻게 된다. 또 깨달음이란 스스로 주인임을 아는 것이다. 그러므로 그 무엇도 숭배대상이 되지 않는다. 만약 숭배하게 되면 깨달음을 향한 수행은 어려워진다. 숭배는 필연적으로 숭배 대상의 존재 하중을 느끼게 되며 종속적인 위치에 놓이고 또 굴종을 낳기 때문이다. 

 

깨달음을 얻기 위한 두 가지 수행 방법

깨달음 즉, 반야의 지혜를 얻기 위해서 크게 두 가지 수행 방법이 있다. 첫 번째는 연기법을 통한 수행 방법이 있고 두 번째는 선불교에서 행하는 명상과 같은 심법이 있다. 첫 번째로 연기법은 개체의 생멸 과정을 보는 사유 방식이다. 이러한 생멸 과정을 통해 모든 존재가 무아(無我)임을 깨닫는 것이 연기론 수행의 목표다. 여기서 무아란 존재 없음, 내가 없음으로 오인되기도 한다. 하지만 무아란 정확히는 모든 존재에는 그 존재라고 불릴만한 성질, 자성이 없음을 말한다. 나라는 존재에 나일 수 있도록 하는 그 무엇도 없다는 것이다. 즉 연기법이란 이 세상의 모든 존재에 존재성이 없음을 보는 사유 방식이다.

 

가령 자동차를 볼 때 그 자동차는 자동차일 수 있도록 하는 것이 없다. 바퀴가 없다면 자동차라 할 수 있는가? 페달이 없다면 자동차라 할 수 있는가? 엔진이 없다면 자동차라할 수 있는가? 자동차란 바퀴, 페달, 엔진 등의 여러 수 많은 요소들이 의존적으로 모여 자동차로 불리우지 자동차는 본디 자동차라 할 수 있는 그 어떤 것도 없는 것이다. 이러한 연기론적 사유 방식을 통해 거듭 세상을 바라보다 보면 세상의 공(空)성을, 즉 모든 존재가 무아임을 깨달을 수 있다. 반야 지혜를 얻게 되는 것이다. 반야 지혜 얻어야만 색 그대로 공, 공 그대로 색을 깨닫고 번뇌 그대로 보리 보리 그대로 번뇌임을 깨닫고 살아갈 수 있다.

 

즉, 있는 그대로 볼 수 있게 되는 것으로 존재가 어떻게 생겨나고 소멸되는지 전면적으로 관찰할 수 있게 된다. 이렇게 되면 한 알의 모래에서도 우주를 볼 수 있게 되는 것이다. 일진법계(一眞法界)를 깨닫는 것이다. 이렇게 존재의 생멸을 관하게 되면 모든 것은 끊임없이 원인과 조건으로 말미암아 생겼다 사라지는 환영과 같음을 알게 된다. 싯다르타는 이러한 연기법적 사유를 통해 깨달음을 얻었다. 연기법적 사유는 지극히 이성적이며 누구나 납득할 수 있는 방식으로 시작하면서도 이성적인 사고의 틀을 넘어서게 한다.

 

두 번째로 선 불교의 수행 방식인 심법이 있다. 심법을 통한 목표는 모든 존재가 진아(眞我)임을 깨닫는 것이다. 진아란 생각을 일으키는 주체라 할 수 있다. 사실 궁극적으로 깨달아야 할 것은 연기법을 통해 깨닫는 무아와, 심법을 통해 깨닫는 진아가 둘이 아닌 하나임을 아는 것이다. 심법은 연기법과 달리 문자를 통하지 않고 진아를 직접적으로 드러내 단 번에 깨닫는 방식으로 위빠사나나 사마띠와 같은 명상을 통해 수행하는 방식이다.

 

이러한 심법의 배경에는 불교 교리인 불립문자가 있다. 문자가 가지고 있는 형식과 틀에 집착하거나 빠지지 않도록 경계하는 것을 말한다. 하지만 문자를 활용하지 않는다고 오해하여 쉽게 깨달을 수 있다고 생각하는 자도 있다. 하지만 불립문자는 충분한 문자 공부가 선행됨을 전제한다. 그렇지 않고 단번에 깨달을 수 있다는 말에 현혹되면 평생 고생만 하게 된다. 또한 충분한 문자 공부가 선행되지 않으면 깨달음 이후 삶에서의 지적 활동이 단절될 수 있다. 싯다르타는 선 수행을 통한 심법만으로 깨달으려 노력했지만 포기했다. 선 수행을 통한 선정 상태에서는 아무런 문제가 없지만 현재의 의식으로 돌아왔을 때 여전히 오온이 취착했기에 수행 상태와의 괴리감이 느껴졌기 때문이다. 

 

연기법과 심법

연기법은 주로 현상적 존재에 맞춘 가르침을 펼칠 때 설하는 방식이며 심법은 주로 현상적 존재의 근원에 대한 가르침을 펼칠 때 사용하는 방식이다. 현상적 존재들 상호 간의 상즉상입으로 보는 방식이 연기법이라면 현상적 존재의 근원을 밝혀서, 현상적 존재는 근원이 형상화된 것이라 보는 방식이 심법이다. 쉽게 말해 연기법은 생각하는 방법으로, 심법은 생각을 배재한 직관으로 깨달음을 얻는 방식이다. 존재가 어떻게 생겨났는가를 관하면 연기법을 깨치게 되고, 존재의 근원이 무엇인가를 관하면 마음을 깨치게 된다. 연기법은 주로 사유형 인간에게 적합하며 심법은 직관형 인간에게 적합하다. 사람마다 성향에 맞는 수행법을 선택하면 된다. 심법을 통해 진아를 깨닫는 것은 연기법을 통해 무아를 깨달아야만 보다 선명해진다. 즉 중요한 것은 두 방법 모두 함께해야 효과적이란 것이다. 연기법을 통해 존재가 무아임을 알고 이해수준에 머물 것이 아니라 심법을 통해 이해수준에서 증득수준으로 넘어가야 한다.

 

Reference

[1] 『이것이 깨달음이다

'Interest > 종교' 카테고리의 다른 글

[종교/불교] 불교란 무엇인가?  (0) 2021.11.01

 

 

삶의 통찰을 배울 수 있는 책이다. 진리나 삶에 대한 사상을 간결하고 날카롭게 표현한 경구나 명언을 엮은 형식으로 구성되어 있다. 이 책은 책을 좋아하기 이전 스무살쯤 샀었다. 당시엔 읽을 땐 아무런 감흥이 없었으나 5~6년 이후인 지금은 이해하고 절실히 느끼는 바가 전과는 달라짐을 느낀다. 아마 몇 년이 지난 이후 다시 읽으면 또 다르게 다가올 것 같다. 매일 일기를 적으며 하루 있던 일로부터 삶의 통찰을 도출해오고 있다. 그러나 홀로 도출해내는 통찰의 양이 한계가 있고, 일반화 가능 여부가 모호 했고, 다각도적인 측면을 고려하기 어렵다고 느꼈다. 하지만 이 책을 읽음으로써 이러한 부족한 부분을 보완할 수 있었다. 곧 바로 이런 책을 읽어야 겠단 생각과 결단을 하지 못했던 것을 반성한다. 

 

책 속에는 많은 삶의 통찰이 있었다. 가치관의 존속을 위해 모든 것을 받아들일 수 없기에 내가 가진 가치의 벡터와 비슷한 방향을 가진 경구를 밑줄 그으며 보았다. 많은 부분은 나의 가치와 다른 부분이 있었고, 철학적인 사색의 결과물이지만 실용성 측면에서는 큰 의미를 갖지 못하는 것도 있었다. 아래 경구 모음은 밑줄 그었던 문장 중에서도 앞으로도 되새기고 싶은 가치이다. 

 

1. 인간이 지향해야 할 최고의 가치들!

소년기의 이상주의는 진리를 인식하는 것이며, 그것은 이 세상 어떤 것도 대신할 수 없는 부를 지니고 있다. - 슈바이처

우리의 지혜가 깊으면 깊을수록 우리는 더욱 관대해진다. - 스탈 부인

사람이 거짓말을 하고 난 뒤에는 뛰어난 기억력이 필요하다. - 코르네유

정직한 사람은 타인에게 모욕을 주는 결과를 초래하더라도 진실을 말하며, 잘난 척 하는 사람은 모욕을 주기 위해서 진실을 말한다. - 헤즐릿

악은 필요하다. 만약 악이 존재하지 않는다면 선 역시 존재하지 않는다. 악이야말로 선의 유일한 존재이다. - 아나톨 프랑스

복수할 때 인간은 그 원수와 같은 수준이 된다. 그러나 용서할 때는 그 원수의 위에 서 있다. - 베이컨

사람의 선과 악은 그 사람의 마음 안에 있다. - 에픽테토스

모든 사람을 좋게 말하는 인간을 신뢰하지 말라. - 콜린스

위대한 것은 단순하게 말해야 효과가 있고, 강조를 하면 망치고 만다. 그러나 사소한 것은 표현과 어조를 고상하게 해야 한다. - 라 브뤼에르

우리는 현명해지기 위해서 먼저 어리석은 사람이 되어야 한다. 스스로를 이끌기 위해서는 먼저 장님이 되어야 한다. - 몽테뉴

긴 세월 동안 인간성에 대해 연구를 해본 결과, 우수한 사람과 평범한 사람의 차이는, 하나의 특징 유무로 결정된다는 사실을 확인하였다. 그것은 '호기심'이었다. 우수한 사람들은 대부분 많은 호기심을 가지고 있었으나 평범한 사람은 이것이 거의 없었다. - 찰스 부토

자기를 높이 평가해주는 사람을 거스를 수 있는 사람은 거의 없다. - 워싱턴

격렬한 말은 그 의미가 빈약하다는 것을 증명하는 것이다. - 위고

받은 상처는 모래에 기록하라. 받은 은혜는 대리석에 새겨라 - 프랭클린

오래 산 사람은 나이를 많이 먹은 사람이 아니고 많은 경험을 한 사람이다. - 루소

다른 사람을 믿지 못하는 사람은 그 자신이 신용을 못 받는다는 것을 알고 있다는 증거이다. - 아웨르바흐

 

2. 성공과 실패의 모든 것!

사람의 처세법에 있어서 가장 중요한 것은, 정(情)과 이치에도 쏠리지 말아야 하며, 동시에 두 가지를 모두 억제할 줄 알아야 한다. - 나폴레옹

우리가 성공하기 위해서는 겉으로는 어리석은 것처럼 보이면서 속으로 영리해야 한다. -몽테스키외

최상의 성공은 실망 뒤에 온다. - F. 비처

자신감은 최고의 성공 비결이다. - 토마스 에디슨

자신의 마음을 감추지 못하는 사람은 어떠한 일에도 성공하지 못한다. - 칼라일

돈을 빌리러 가는 것은 자유를 팔러 가는 것이다. - 프랭클린

지혜를 얻기 전에 돈을 쥐게 된 사람은 돈주인 노릇을 잠깐밖에 하지 못한다. - T. 플러

못난 사람도 돈만 있으면 잘나 보인다. - 서양 속담

절약은 돈지갑의 밑바닥이 드러났을 때는 이미 늦다. - 세네카

'이 일이 정말 필요한 일인가'하고 의심하는 마음이 없을 때 비로소 기쁨을 느낄 수 있다. - 톨스토이

쉽게 허락한 것은 반드시 신뢰성이 의심스럽고, 쉽게 일을 시작하면 반드시 어려움이 따른다. - 노자

인간은 늘 자신이 종사하고 있는 업무 속에서 세계관의 기초를 구축해야만 한다. - 페스탈로치

세상에 천한 직업은 없으며 다만 천한 사람이 있을 뿐이다. - 링컨

세상에 천한 직업은 없다. 다만 그것을 천하다고 여기는 천한 사람이 있을 뿐이다. - 나

남의 은혜를 망각한다면 벌써 인간으로서의 약점을 지니고 있다는 증거이다. 따라서 유능한 사람이 남의 은혜를 잊었다는 예는 어디에도 없다. - 괴테

남에게 예리하게 상처를 주고 싶거든, 그의 이기심을 겨누어서 치면 된다. - L. 윌리스

얼마나 많은 사람이 명성에 의해 칭송을 받은 후에 망각 속에 묻혀버렸던가? 그리고 타인의 명성을 찬양했던 사람들도 결국은 죽었다. - 아우렐리우스

적이 나보다 약하다고 해서 결코 동정해서는 안 된다. - 사드

우리의 진정한 적은 언제나 침묵하고 있다. - 발레리

가난하더라도 깨끗이 집안을 청소하고, 깨끗이 머릴르 손질하면 자연히 기품이 나타나게 마련이다. - 채근담

빈곤을 수치스럽다고 여기는 것은 부끄러운 일이다. 그러나 자신이 극복하기 위해 노력하지 않는 것은 더 부끄러운 일이다. - 투키디데스

이름이 무슨 소용인가, 장미꽃은 다른 이름으로 불려도 같은 향기가 나는걸. - 셰익스피어

강한 인간이 되고 싶다면 물과 같아야 한다. - 노자

정말로 바쁜 사람은 자기의 몸무게가 얼마나 되는지 모른다. - 하우

 

3. 시간

인간은 항상 시간이 모자란다고 불평을 하면서, 실제로는 마치 시간이 무한정 있는 것 처럼 행동한다. - 세네카

시간 엄수는 군주의 예절이다. - 루이 18세

내가 헛되이 보낸 오늘 하루는 어제 죽어간 이들이 그토록 바라던 하루이다. - 소포 클레스

승자는 시간을 관리하며 살고, 패자는 시간에 끌려 산다. - J. 하비스

시간을 지키고 안 지킴에 따라 사람의 품위가 결정된다. - 브하그완

현재는 과거의 제자다. - 프랭클린

청년이 청년을 인도하는 것은 맹인이 맹인을 인도하는 것과 같다. 그들은 머지않아 도랑에 같이 빠질 것이다. - 체스터필드

우리는 젊었을 때 배우고, 나이 먹어서 이해한다. - 에센바흐 

세상에서 젊음처럼 귀중한 것은 없다. 젊은은 마치 돈과 같다. 돈과 젊음은 모든 것을 가능하게 한다. - 고리키

노년의 결핍을 보충할 수 있는 것을 젊을 때 익혀둬라. 만약 노년의 식량이 지혜란 것을 이해한다면 영양실조에 걸리지 않도록 젊을 때 공부해라. - 다빈치

 

 

4. 인간 정신의 최고의 자양분, 예술!

시의 한 가지 장점을 부정할 사람은 없을 것이다. 즉 그것은 산문보다 적은 말로써 더 많은 것을 표현한다는 것이다. - 볼테르

책을 백 번 읽으면 그 뜻이 저절로 통한다. - 위략

대화할 때는 그 얼굴이나 용맹함이나 조상이나 문벌을 가지고 이야기할 것이 아니다. 독서한 내용을 가지고 이야기해야 한다. - 공자

누구에게나 정신적으로 하나의 기원(元)을 만들어주는 책이 있다. - 파브르

독서는 다만 지식의 재료를 공급할 뿐이며, 그것을 자기 것이 되게 하는 것은 사색의 힘이다. - 존 로크

어떤 책은 맛만 봐도 되고, 어떤 책은 통째로 삼켜야 하며, 또 어떤 책은 씹어서 소화시켜야 할 것이 있다. - 베이컨

생각하지 않고 책을 읽는 것은 제대로 씹지 않고 음식물을 삼키는 것과 같다. - 바이크

보기 드문 지식인을 만났을 때는 그가 무슨 책을 읽는가를 물어보아야 한다. - 에머슨

독서에 소비한 만큼의 시간을 생각하는 데 소비하라. - 베넷

아무리 어려운 글이라도 일백 번 정도 되풀이하여 읽으면 그 참뜻을 스스로 깨우쳐 알게 된다. - 주차훈학육기

우선 제1급의 책을 읽어라. 그러지 않으면 그것을 읽을 기회를 전혀 갖지 못하게 될지도 모른다. - 도로

사귀는 벗을 보면 그 사람을 알 수 있듯이 읽는 책을 보면 그 사람의 품격을 알 수 있다. - 스마일스

큰 도서관은 인류의 일기장과 같다. - G. 도슨

만 권의 책을 읽으면 신의 경지에 이른다. - 소식

내가 인생을 알게 된 것은 사람과 접촉해서가 아니라 책과 접촉하였기 때문이다. - A. 프랜스

잡서의 난독은 일시적으로는 다소 이익을 가져다줄지 모르지만, 궁극적으로는 시간과 정력의 낭비로 돌아간다. - E.S. 마틴

독서는 천천히 해야 하는 것이 첫번째 법칙이다. 이것은 모든 독서에 해당한다. 이것이야말로 독서의 기술이다. - E. 파게

자기 스스로 사물에 대해 생각하지 않는 자는 결국 다른 사람의 사상에 예속된다. (생략) 그러므로 그대 자신의 머리로 생각하라. - 톨스토이

항상 깊이 생각하라. 그리고 무엇보다도 당신의 사상을 풍부히하라. (생략) 현실이란 곧 사상의 그림자에 불과하다. - 칼라일

모든 책은 우리에게 지식의 자료를 줄 뿐이며, 진정 나 자신의 것은 나의 생각과 실천의 힘뿐이다. - 로크

현명한 사람은 어리석은 자가 현명한 사람에게 배우는 것보다 어리석은 자에게서 더 많이 배운다. - 카토

언어의 고전적 순수성이라는 문제를 너무 중시할 필요는 없다. 믿음직한 천재는 그 시대의 언어, 이미지, 사상 가운데 똑바로 뛰어들어서 빈틈없는 제빵공처럼 그것을 반죽해야 한다. - 로맹 롤랑

 

5. 인생에 주어진 진정한 보성, 우정

제 아무리 친한 친구라 할지라도 자신의 생각을 전부 말로 해버리면 평생토록 적이 될 수 있다. - 샤를 뒤클로

우정을 위한 최대의 노력은 벗에게 그의 결점을 스스로 깨닫게 하는 일이다. - 라 로셰호크

옳은 일을 권하는 것이 친구의 도리이다. - 맹자

진정한 친구란 서로의 약점을 포용해주어야 한다. - 셰익스피어

벗을 사귈 때는 그 사람의 장점만을 취하고 단점은 취하지 말라! 이렇게 하면 오래도록 사귈 수 있다. - 공자

우정에 있어서 최상의 노력은 친구가 자신의 결점을 우리에게 보여주게끔 만드는 일이다. - 라 로슈푸코

친구란 모든 것을 알고 있으면서도 사랑해주는 사람을 말한다. - 앨버트 하버드

우정은 사랑을 받는 데에 그 의미가 있는 것이 아니라 사랑을 주는 데에 있다. - 루소

어떠한 충언을 하건 말이 길어서는 안 된다. - 호라티우스

 

6. 우리의 일상을 지배하는 달콤한 환상!

우리들이 연애에 대해서 이야기를 하게 되면 곧 한가지 문제에 부딪힌다. 즉 사람은 무엇을 사랑하느냐 하는 것이다. 이에 대한 유일한 답은, '사람은 사랑할 가치가 있는 것을 사랑한다'는 것이다 - 키에르케고르

사랑을 받는 것, 그것은 행복이 아니다. 사랑하는 것, 그것이야 말로 진정한 행복이다 - 헤세

사랑의 본질은 개인을 보편화하는 것이다. - 콩트

사랑은 연령과 상관이 없다. 사랑은 어느 때든지 할 수 있다. - 파스칼

인간은 사랑에 의해 살아가고 있다. 그것은 자기 자신의 시작이다. 신과 인류에 대한 사랑은 삶의 시작이다. - 톨스토이

 

 

7. 결혼의 실체 & 고달픈 인생의 안식처, 가정!

타인이 좋아할 여자를 아내로 맞지 말고 자신의 취향에 맞는 여성을 아내로 맞아라. - 니체

결혼을 하십시오. 그러면 당신을 후회하게 될 것입니다. 결혼을 하지 마십시오. 그래도 당신을 후회하게 될 것입니다. - 키에르케고르

성공적인 결혼은 적당한 짝을 찾는 데 있기보다는 적당한 짝이 되어주는 데 있다. - 텐드우드

결혼은 발열로 시작해서 오한으로 끝난다. - 리히텐베르크

인간적인 사랑의 최고의 목적은 종교적인 사랑의 경우와 마찬가지로 사랑하는 사람과 하나가 되는 일이다. - 보부아르

서둘러서 한 결혼이 순조로운 경우는 매우 드물다. - 셰익스피어

결혼은 지혜로운 사람이나 어리석은 사람이나 모두 한 번씩 동경과 후회를 경험하는 기본 코스이다. - 서양 속담

교양이 있는 사람일수록 타인을 한 번보고 자신의 취향에 맞는지 구분하는 것은 쉽지 않음을 알 수 있다. - 체스터필드

결혼하기 전에 상대와의 결합을 열 번, 백 번도 더 생각해보는 것이 좋다. 단순한 성적 교섭으로 자신의 인생과 타인의 전 인생을 결합한다는 것은 매우 어리석은 일이기 때문이다. - 톨스토이

남자는 결혼해서 여자의 지혜로움을 알고, 여자는 결혼해서 남자의 어리석음을 안다. - 히세가와 조세칸

국가의 기본은 한 가정에 있다. 모든 가정이 제 역할을 잘 하면 국가는 바로 설 수 있다. - 대학

부모 앞에서는 결코 늙었다는 말을 해서는 안 된다. - 소학

자녀에게 침묵하는 것을 가르쳐라. 말하는 것은 너무나 쉽게 배울 수 있다. - B. 프랭클린

어진 남편은 그 아내를 귀하게 만들고, 악한 남편은 그 아내를 천하게 만든다. - 명심보감

 

8. 우리의 삶을 가장 인간답게 만드는 것

깍듯한 예절로 크게 이겨라. 최후의 승자는 친절한 사람의 것이다. 힘없는 사람, 용기 없는 사람은 다만 친절을 가장할 뿐이다. - 중국 격언

겸손도 정도를 넘으면 교만이 된다. - 영국 격언

남에게 친절함으로써 그 사람에게 준 유쾌함은 곧 나에게 돌아온다. 뿐만 아니라 때로는 이자를 가져오기도 한다. - 스미스

누구든지 자기를 높이는 자는 낮아지고, 자기를 낮추는 자는 높아지리라. - 신약성서

인간은 타인을 칭찬함으로써 자기가 낮아지는 것이 아니다. 그렇게 함으로써 오히려 자기를 상대방과 같은 위치에 올려놓는 것이다. - 괴테

겸손은 가장 얻기 어려운 미덕이다. 반면에 자기 자신을 높이 평가하는 것보다 더 어리석은 것은 없다. - T. S. 엘리엇

대화는 항상 겸손하고 부드럽게 하라. 그리고 부탁하건대 말수를 줄여라. 그러나 말을 할 때는 요령 있게 하라. - W. 존슨

우쭐대거나 뽐내지 않는 사람은 자기 자신이 믿고 있는 것보다 훨씬 큰 인물이다. - 괴테

말하는 사람은 씨를 뿌리고, 침묵하는 사람은 거두어들인다. - J. 레이

가장 무서운 자는 침묵을 지키는 자이다. - 호라티우스

사람의 인격은 항상 그 사람의 언어에서 드러난다. - 메난드로스

우리가 말을 할 때는 말하는 이유가 필요하지만 침묵을 지킬 때는 침묵해야 할 이유가 필요 없다. - P. 니콜

바른 말은 듣기에는 좋지 않은 법이다. - 한비자

훌륭한 사람일수록 말수가 적은 법이다. - 헤세

후회하지 않는 방법, 그것은 바로 침묵을 지키는 것이다. - 중국 격언

사람은 침묵하고 있어서는 안 될 경우에만 말해야 한다. 그리고 자신이 극복해온 일에 대해서만 말을 해야 한다. 그 이외의 것은 모두 쓸데없는 것들이다. - 니체

주의 깊게 듣고 현명하게 질문하고 조용히 대답하고, 그리고 더 이상 말이 필요 없을 때에 입을 열지 않는 사람은 인생에서 가장 필요한 의의를 깨달은 사람이다. - 라하테르

타인을 헐뜯거나 비방하고 싶은 마음이 들거든 차라리 침묵을 지켜라, 절대 타인의 욕설을 하지 말라. - 톨스토이

자신이 하는 말을 상대방이 듣게만 할 것이 아니라 이해를 시켜야 한다. 즉 말에는 기억력과 지성과 상상력이 자연스럽게 조화를 이루어야 한다. - 주베르

좋은 웅변은 필요한 것을 전부 말하지 않고, 필요하지 않은 것은 절대로 말하지 않는 데 있다. - 라 로슈푸코

마땅히 말해야 할 때에 말을 하지 못하는 사람은 전진할 수 없는 사람이다. 그 대신 침묵해야 할 때 그것을 참지 못하는 사람은 처세의 비결을 모르는 사람이다. - 스마일스

마음에 없는 이야기를 하기보다는 말을 하지 않는 것이 오히려 사교성에 도움을 준다. - 지눌

말이 많은 사람의 말 중에는 어리석은 말도 많이 섞여 있다. - 코르네유

친구와 교제하면서 자신의 이야기를 하는 편이 혼자서 자기 자신의 정신을 연구하는 것보다 더 많은 것을 정신에서 끄집어낼 수 있다. - 몽테뉴 

필요 이상으로 말을 하지 말라. - R. B. 세리틴

인간의 마음속에는 언제나 선과 악이 대립된 형태로 갈등한다. 이때 현자의 선은 악보다 강하고 우자의 선은 악보다 약하다. 여기에서 사람의 차이가 나타난다.

시기와 질투는 항상 타인을 쏘려다가 자신을 쏜다. - 맹자

타인의 지난날의 행동과 말을 가지고 그의 일평생을 꺾어서 단정하기는 어려운 일이다. - 명심보감

내일을 위해서 오늘 분수를 지키는 것이 지혜로운 사람의 도리이다. - 세르반테스 

 

9. 인간 내면의 다양한 색채들!

삶의 즐거움은 자기보다 어려운 사람들과 함께 어울려 사는 것이다. - 대커리

네가 가는 길의 마지막에는 만족이 있다. 그러나 처음부터 만족하는 사람은 멀리 가지 못한다. - 류카르 

어떤 일에 있어서든 가장 오래 기다리는 사람이 반드시 승리한다. - 잭슨

모든 일에 있어서 성공을 결정짓는 첫번째이자 유일한 조건은 인내이다. - 톨스토이

야심은 하늘을 나는 동시에 땅을 길 줄도 안다. - 에드먼드 버크

행복의 원칙은 첫째 어떤 일을 할 것, 둘째 어떤 사람을 사랑할 것, 셋째 어떤 일에 희망을 가질 것. - 칸트

 

🚀 배틀그라운드의 두 날개: 불신과 확신

 

이기문. 이 책의 저자이다. 기업 경영에 관심이 있는 나로서, 전 세계 10억 유저가 열광한 배틀그라운드가 있기까지 10년 간 크래프톤이 겪은 질곡과 인고의 시간을 한 권의 책으로 볼 수 있게 해준 저자의 노고에 매우 감사한 마음이 들었다.

기업 경영을 하게 되면 실제로 발생할 문제 상황과 고통들을 여실히 목도할 수 있는 책이다. 이 책을 읽기 전엔 뭣 모르고 시작해도 뭐라도 하니까 어떻게 되겠지 하는 생각이 있었다. 하지만 기실은 절대 그렇지 않다는 것을 느끼게 해주었다. 경영진의 번민과, 사무침과, 방향성에 대한 끝없는 천착과, 이로 말미암은 수 많은 갈등의 배태를 보며 결코 사업에 대한 가벼운 희구나 간절함으론 사업을 영위할 수 없겠다는 것을 느꼈다.

책을 읽는 내내 경영진의 더 나은 의사결정을 위한 끝 없는 사유와 원활한 커뮤니케이션 능력을 보면서 경외심도 느꼈다. 동시에, 더 이상 좋을 수 없어 보이는 커뮤니케이션 능력으로도 상호 갈등이 좁혀지지 않는 것을 보면서, 원하는 바 쟁취를 위해 기치를 내세우고 투쟁하며 썩은 고통의 구덩이에서 몸부림 쳐야한다는 것도 느꼈다. 눈 앞에서 당장 백 보 후퇴할 것 같아도 기꺼이 한 보 전진이 가능하다면 으스러진 다리를 끌고 나아가는 모습을 보며 비전 성취에 대한 의심과 믿음, 그리고 기개와 집념을 느낄 수 있었다. 과연 나는 이를 견딜 수 있겠는가 하는 생각도 함께 들었다.

만물에는 양면성이 깃들어 있다고 하였던가, 전 세계 10억이 열광하는 배틀그라운드를 만들기 위해 있었던 10년간의 믿음, 노력, 헌신, 갈구, 배려 뿐만 아니라 불신, 집착, 유린, 힐난, 결기와 같은 이 모든것이 있었기에 만들어질 수 있었다고 본다. 결국 나는 서로 양립 불가능한 가치들의 공존에서 위대한 창조가 일어난다고 느꼈다. 원하는 미래를 만들 수 있다는 믿음에 대한 의심을 하면서도 끝없는 간절함과 확신으로 버텼던 크래프톤 처럼 말이다. 나는 책을 두 번 세 번 읽지 않는 편인데, 이 책은 향후 다시 한 번 읽을 수 밖에 없겠다는 생각이 들었다. 내가 겪었던 것 처럼 체화하고 싶을 뿐만 아니라 경영진의 대화에서 드러나는 높은 사고력과 책 속에 담긴 풍부한 어휘 또한 마음에 들었기 때문이다.

🍎 마음에 남는 대목
“펍지 초기, 김창한이 ‘바람이 부는데, 그 끝이 어딘지 모르겠습니다’라는 표현을 한 적이 있다. 인생을 살아가며 바람을 느끼고 인식하는 순간은 정말 드물다. 바람이 불어도 대부분 바람인지 모른다. 바람이라 인식해도 평소처럼 살아가는 경우도 많다.”

이진 탐색 알고리즘은 탐색 알고리즘 종류 중 하나로, 이름 그대로 절반씩 나누어 원하는 값을 알고리즘이다. 이진 탐색을 위한 전제 조건으로 배열이 오름차순 또는 내림차순으로 정렬되어 있어야 한다. 동작 방식은 간단하다. 배열의 중앙을 기준으로 원하는 값이 작은지 큰지 비교하여 한 쪽을 배제하고 나머지 부분을 반복해서 탐색하는 방식이다. 

 

가령 예를 들어 아래와 같이 찾고자 하는 값이 5일 경우, 배열의 가운데인 4를 기준으로 삼은 다음 오른쪽에 있다는 것을 알 수 있다. 이후 왼쪽 배열 0~4를 배제하고 오른쪽 배열 5~9 부분을 탐색한다. 찾고자 하는 값이 5~9 부분의 중앙인 7을 기준으로 왼쪽에 있으므로 다시 오른쪽 배열을 배제하고 왼쪽 배열 5~7을 탐색한다. 6을 기준으로 왼쪽에 있으므로 오른쪽 7을 배제하고 왼쪽 5를 찾으며 탐색이 완료되는 방식으로 동작한다. 

 

원하는 값을 찾기 위해 배열의 모든 원소를 탐색하는 순차 탐색의 경우 시간복잡도가 $O(n)$이다. 하지만 이진 탐색의 경우 시간복잡도가 이보다 작은 $O(logN)$이 된다는 것이 특징이다. 조금 덧붙이자면 시간복잡도 $O(logN)$는 배열이 정렬되고 고정된 상태에서 가능하다. 만약 배열에 삽입, 제거, 탐색과 같은 연산이 함께 이뤄진다면 시간복잡도가 $O(n)$까지 떨어질 수 있다. 때문에 삽입, 제거, 탐색과 같은 연산이 함께 이뤄지는 경우 이진탐색 트리를 자료구조로서 사용해야 한다. 이진탐색트리의 경우 삽입, 제거, 탐색을 해도 시간복잡도가 $O(logN)$이 보장되는 특징이 있기 때문이다. 

 

이진 탐색 알고리즘 구현은 크게 두 가지 방법이 있다. 첫 번째는 재귀를 사용하지 않는 방식이고, 두 번째는 재귀를 사용하는 방식이다. 두 구현 방법 모두 공통적으로 세 종류의 변수들이 필요하다. 

 

1. 정렬된 배열 (array)

2. 찾고자 하는 값 (target)

3. 배열의 인덱스 표시 (start, end, mid)

 

이진 탐색 비재귀적 구현

array = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
start, end = 0, len(array)-1
target = 5

while start <= end:
    mid = (start + end) // 2
    
    if array[mid] == target:
    	return mid
        
    elif array[mid] < target:
    	start = mid + 1
        
    elif array[mid] > target:
    	end = mid - 1

 

이진 탐색 재귀적 구현

def binary_search(array, start, end, target):
    if start > end:
        return None

    mid = (start + end) // 2

    if array[mid] == target:
        return mid

    elif array[mid] < target:
        start = mid + 1
    
    elif array[mid] > target:
        end = mid - 1

    return binary_search(array, start, end, target)


if __name__ == "__main__":
    target = 5
    array = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
    start, end = 0, len(array)-1
    index = binary_search(array, start, end, target)
    print (index)

 

만약 결과가 출력되지 않을 경우 주어진 배열안에 target이 없는 것임.

 

Reference

[1] GIF of binary search: https://stackoverflow.com/questions/70131709/python-binary-search-mid-calculation

[2] https://velog.io/@he1256/작성요망-DS-탐색트리-이진탐색트리-AVL트리

Abstract

이 논문에서 문제점으로 삼고 있는 것은 dialouge management(이하 DM)을 scalable하고 robust하게 만드는 것이 어렵다는 것임. 그리고 이 논문의 저자가 말하고자 하는 것은 DM을 디자인 관점에서 연구자들에게 향후 연구 방향을 알려주고자 함.

 

1. Introduction

Conversational System은 크게 두 분류로 나뉨 1. task-oriented system 2. non-task-oriented system 여기서 non-task-oriented는 단순 대화 같이 별도 행위가 필요하지 않은 시스템이고 task-oriented는 예매와 같은 구체적인 태스크를 수행하는 시스템임. 이 논문에선 task-oriented 시스템을 다룸. DM에는 다양한 접근법이 있고, 각각 장단점이 있음.

 

2. Problem fundamentals and dimensions

 

 

 

A. Dialouge Management

위 그림은 DM의 전체적인 플로우임. DM은 크게 위 아키텍처를 벗어나지 않고 거의 정석으로 사용됨. NLU가 가장 처음으로 유저 발화 정보를 추출함. 크게 3가지로 Intent, entity, dialouge act를 가져옴. 즉 어떤 의도로 말했고, 어떤 것을 말했고, 어떤 행위를 요구하는지를 파악하기 위함임.

  1. intent는 goal/task와 거의 동의어로 사용됨
  2. entity는 slot과 동의어로 봐도 됨. 위 그림 1을 예로 들자면 departure_city나 destination_city가 entity가 됨.
  3. dialouge act는 유저가 단순 서술인지, 질문인지, 요청인지 파악하기 위해 사용됨

그리고 이 모든 정보는 Dialouge State Tracking(이하 DST)에게 전달함. 그리고 DST는 NLU에서 받은 구조적 representation을 저장해둠. 어떻게 대답할지 결정하기 위해. 다른 말로 표현하면 Dialouge State(이하 DS) 유지를 위해 명시적이거나 암시적인 정보까지 전부 저장해둠

 

그리고 DS에 기반해서 다음 행동은 Dialogue Policy(이하 DP)가 결정함. DP는 일종의 함수로 봐도 됨. DS에 기반해서 행동(Dialouge Act)을 매핑시켜주기 때문에. (일종의 if else와 같이 룰 베이스 형식인듯. NLU에서 처리한 Dialouge Act를 DP에서 받아서 실행시켜주는 방식인듯). 예를 들면 flight-booking이라는 domain에선 action이 크게 4가지로 제한됨. Request, Confirm, Query, Execute. 만약 Request라면 출발 시간과 도착 시간을 물어봐야하고, Confirm이라면 도착 위치가 여기가 맞는지 물어봐야하고, Query라면 항공편 리스트를 가져와야하고, Execute라면 예약 신청해야 함. 이러한 DP 동작의 결과를 NLG가 적절한 응답으로 생성함.

 

DM 시스템은 이렇게 동작하지만 산학계에서 제안 된 다양한 아키텍처들이 있음. 제시한건 NLU, DST, DP, NLG지만 하나로 합쳐진 것도 있고 그럼.

 

B. Analysis Dimension

크게 4가지 차원에서 구체적으로 설명함

  1. DS: 어떻게 DS가 representation되고, 어떤 정보가 포함되고, 수작업인지 데이터로 학습하는지 설명
  2. DST: DS가 결정되는 과정을 설명
  3. DP: 다음 action이 어떻게 결정되는지 설명
  4. Context support: 유저 의도를 파악하고 적절한 답을 주기 위해, 크게 3가지를 활용함 Converstional history, User profile, External knowledge

 

3. DM 접근법

 

DM 방식에는 크게 3가지가 존재함 1. 수작업 2. data-driven 3. 혼합

 

3.1 Hand-crafted

Rule-based

Rule-based 방식은 가장 초기에 나온 방식으로 한 번에 NLU, DM, NLG 태스크를 수행하는 방식임. AIML(AI Markup Language)라는 개념이 있음. 이건 크게 두 개념으로 구성되는데 1. 카테고리 2. 주제로 구성됨. 명확히 이해는 안되지만 이 논문에선 챗봇으로 예를 들었는데 사용자가 Hi robot이라 입력해야 대화가 시작되고 Hi robot에 대한 답변이 이미 정해져 있어 Hi human이란 답을 하는 방식이라고 함. 아무튼 이 방식의 특징은 당연히 DST와 DP의 역할이 거의? 없다는 것. 바로 NLU 한 다음 NLG를 함. 결론적으로 수 많은 패턴이 있어야 하기 때문에 간단한 태스크 조차 쉽지 않고, 패턴간 중복이나 충돌이 발생해서 이걸 처리하는 비용이 들기 때문에 비효율적이다. (고비용 & 스케일업 어려움)

 

이런 비효율적인 Rule-based를 보완하기 위해서 DS와 DP가 필요했음. 왜냐면 rule-based는 보통 바로 이전 발화만 저장했는데, 정교하게 만들기 위해 이전 발화 모두를 저장해서 활용할 필요가 생겼고 또 사용자 맞춤을 위해 user-profile 정보가 필요했음. 이런 발화 정보와 유저 프로필과 같은 대화에 도움을 주는 것을 context support라 부름.

 

Finite State-based

Final State Method는 규칙 기반으로 동일함. 하지만 룰베이스에서 조금 더 확장된 것이라 볼 수 있음. 사용자가 원하는 행위가 충분히 polynomial한 질문 수와 절차로 해결하는 방식이기 때문임. 비행기 예약과 위 그림 3 시나리오로를 보자면 프로그래밍을 통해 시퀀셜하게 이미 정해진 절차가 있어서 반드시 이걸 따라야 함. “다음 주 월요일에 리옹에서 시드니로 가는 편도 항공편을 예약하고 싶다"고 말 못함 왜냐면 “출발지 → 목적지 → 날짜 → 편도/왕복”의 일련의 절차 대로 말해야 하기에.

 

Frame-based

domain ontology기반 방식임. 예를 들자면 음악 틀어달라는 태스크라면 사전정의된 frame은 domain으로 Music을 가질 것이고 intent는 Play music일 것임. 그리고 slot이 될 수 있는 장르와 뮤지션을 요구하거나 수집할 수 있음. frame-based는 사전 정의된 frame을 보고 어느 슬롯이 채워졌고 안채워졌는지 확인해서 DS를 파악할 수 있음. rule-based와 activity-based 기반과 비교해 다른 점은 조금 더 유연하단 것이 장점임.

 

앞서 기술했듯 어떤 slot이 채워졌는지 안채워졌는지 파악해서 추가 요구하기 때문임. 반면 단점으론 복잡한 DP 알고리즘이 필요하고, 모든 조건에서 추가적으로 정보를 요청하는지 안하는지 테스팅이 필요함. 그럼에도 불구하고 frame-based는 현재에도 사용되는 방식임. 애플 시리, 아마존 알렉사, 구글 어시스턴트가 여기에 해당함. 그래도 이런 어시스턴트 들에서 조금 더 추가되는 부분은 NLU에서 머신러닝 방식이 사용되고, 프레임외에 control model을 추가한다고 함. Control model이 정확히 무엇인지 모르겠지만 봇 개발자가 복잡한 DP 지정을 방지할 수 있다고 함.

 

3.2 Data-driven

Data-driven 방식은 위 Hand-craft 방식과 달리 DS와 DP를 학습해서 사용함. 이를 위해 Supervised Learning(이하 SL)과 Reinforce Learning(이하 RL)이 사용됨. SL로는 라벨 있는 데이터로 corpus를 학습하고, RL로는 tiral-and-error 과정을 학습함 

 

1) Supervised approach

Data-driven 방식의 최초 접근은 SL로, CRF나 최대 엔트로피 모델을 사용했었음. 하지만 이는 대개 DS 정의를 위해 NLU의 출력에 의존하는 단점이 있었음. 하지만 DL이 사용되면서 DST에 대한 성능이 높아짐. 성능 증가의 배경에는 DS를 NLU에 종속시키는 것이 아니라 NLU와 DS를 단일 모델로 병합하는 연구가 대부분 이뤄지면서 가능했음. 이런 연구의 한 예시로 아래 그림과 같은 Neural Belief Trakcer (NBT)가 있음.

 

NBT 모델은 시스템 출력과 유저 발화와 slot-value pair를 입력으로 넣고 중간에 Context Modeling과 Semantic Decoding을 거쳐 최종적으로 이진 분류하고 최종적으로 slot-value에 들어갈 값을 결정하는  구조를 가짐. 

 

더욱 최근 DL 모델은 GNN 모델과 attnetion 메커니즘을 사용하기도 함. 최근 DL 모델도 결국 두 가지로 나뉨. pre-defined ontology-based(사전 정의된 온톨로지 기반)과 open-vocabulary candidate generation(개방형 어휘 후보 생성)으로 나뉨. 전자의 경우 도메인과 그에 해당하는 슬롯과 값이 제공된다고 가정함. 하지만 알다시피 정의된 것은 정확도가 높지만 동적인 환경에서는 여전히 취약함. 동적인 환경 예시는 영화 이름이나 사람 이름, 날짜, 위치 등이 있음 반면 후자의 경우 미리 정의된 것 없이 대화 기록이나 NLU 출력 값에서 슬롯 후보를 추정하는 방식임. 새로운 의도나 슬롯, 도메인을 추가할 때 데이터 수집이나 재학습과 같은 비용이 발생하지 않도록 제로샷으로 DST를 일반화 시키는 것이 현재 가장 메인이 되는 연구임. 

 

Dialouge Policy

DP에 지도학습을 적용시키는 두 가지 접근이 있음. 1. DP 모델을 파이프 라인 모델로 두어 DST와 NLU 모듈과 독립적으로 학습시키는 방법임. 이 방법은 DST의 DS로부터 input을 가져와서 넣거나 NLU 결과를 직접 넣어서 다음 action을 출력함. 그 과정은 아래 그림의 (a)와 같음 

DP network안에는 Act와 Slot을 예측하는 두 개의 Softmax가 있음. Act는 request, offer, confirm, select, bye 중에 하나가 될 수 있고 Slot은 price-range, area 같은 것들이 될 수 있음. (b)의 모델은 3개의 메모리에 의존함 slot-value memory, external memory, control memory인데 read/write function과 augmented attention 메커니즘을 공유함.

 

두 번째 접근은 end-to-end 모델임. 이 방식은 user utterance를 읽어서 곧 바로 모델에 넣고 다음 system action을 생성하는 방식임. 구현을 위해선 seq2seq 계열 알고리즘이 사용됨. sqe2seq 알고리즘에 사용되는 encoder는 user utterance와 dialouge history이며 decoder는 API 호출, 데이터베이스 쿼리가 될 수 있음. 알다시피 RNN, LSTM 사용했지만 최근엔 어텐션 메커니즘 사용됨. end-to-end 방식의 괄목할만한 대표적인 DP 모델은 아래 논문에서 확인할 수 있다고 함. 

 

" Z. Zhang, M. Huang, Z. Zhao, F. Ji, H. Chen, and X. Zhu, “Memory augmented dialogue management for task-oriented dialogue systems,” ACM Transactions on Information Systems (TOIS), 2019."

 

여하튼 end-to-end 방식은 앞전 pipelined policy과 달리 dialouge state를 추론할 수 있다는 것이 장점. 다만 데이터가 많은 데이터가 필요할 뿐. 참고로 pipelined policy 방법도 동일하게 지도학습함. 

 

Context support

conversation history를 앞 전 하나만 사용하냐 모두 사용하냐도 나뉨. 크게 3가지 방법으로 conversation history 모델링이 이뤄짐. 첫 번째 방법은 모든 발화를 concatenation하는 것임. 근데 단점이 계산 복잡도가 증가하니까 두 번째 방법인 user/system dialouge act나 dialouge state의 특정한 feature들만 캡처하는 방식임. 세 번째 방식은 최근에 나온것으로 그래graph structrural information를 활용하는 거임. 이건 Spacy에 있는 dependency relation과 관련이 있다고 함.

 

 

2) Reinforcement learning

RL 접근은 DM을 최적화 문제로 봄. 유저의 경험과 시간이 지나면서 성능이 점진적으로 개선하는. 전형적으로 Markov Decision process가 사용됐었음. (RL 파트 생략)

 

 

결론부터 요약해두자면 이 논문에서 말하고자 하는 DM 접근법은 크게 3가지가 있고 장단점은 다음과 같다.

1. Handcrafted 방식은 구현 쉽다. 데이터 필요 없다. dialouge traceability 충족시킬 수 있다. 단점은 유지 비용 높고 스케일업과 모델의 robustness 측면에서 한계가 있다. 또 사용자 피드백 고려하지 않는다. 

 

2. Data-driven 방식은 크게 지도학습 방식, 강화학습 방식으로 나뉨. (1) 지도학습 장점은 자연어의 variation을 다룰 수 있고 새로운 domain에 대한 적은 노력으로 적응 가능함. 반면 단점은 양질의 데이터 불충분과 DP에 적응하기 위한 유저 피드백 고려 못함. (2) 강화학습의 장점은 Robust하고 user utterance의 모호함을 다룰 수 있음. 단점은 작은 스케일의 대화 도메인에 한계가 있다..?, 양질의 데이터 필요하다. 

 

3. Hybrid 방식은 위 둘을 섞은 것임. 장점은 학습 복잡도와 학습 데이터 줄어듦. 단점은 충분한 양질의 데이터와 개발 노력 필요

 

아래 표는 DM (Dialouge Manager)에 사용되는 3가지 피처임. 

1. Conversation history 사용되고

2. User profile 사용되고

3. External knowledge 사용됨. 

 

 

읽고 나서 궁금해진 것

- GNN (Graph Neural Network)

- Graph Attention Network

- Knowledge Graph

- End-to-end Dialouge Policy Modeling

- Reinforcement Learning

 

읽어 볼 논문

[1] Z. Zhang, M. Huang, Z. Zhao, F. Ji, H. Chen, and X. Zhu, “Memory augmented dialogue management for task-oriented dialogue systems,” ACM Transactions on Information Systems (TOIS), 2019.

[2] Y. Murase, Y. Koichiro, and S. Nakamura, “Associative knowledge feature vector inferred on external knowledge base for dialog state tracking,” Computer Speech & Language, vol. 54, pp. 1–16, 2019.

 

Abstract

  • 의도 분류는 Spoken Language Understanding(SLU)의 하위 태스크에 해당하며, 의도 분류는 SLU의 또 다른 서브 태스크인 Semantic Slot Filling 태스크로 곧 바로 연계되기 때문에 그 중요성을 가짐.
  • ML 기반으론 유저 발화 이해가 어렵다. 때문에 이 논문에선 DL 기반의 의도 분류 연구가 최근까지 어떻게 이뤄져왔는지 분석, 비교, 요약한하고, 나아가 다중 의도 분류 태스크가 어떻게 딥러닝에 적용되는지 기술함.

내가 추출한 키워드: intent detection, mulit-intent detection, spoken language understanding, semantic slot filling.

 

Introduction

Dialouge System은 크게 5개 파트로 나뉨: ASR, SLU, DM, DG, TTS.

  • 유저가 발화 하면 ASR을 통해 유저의 발화를 생성하고 SLU 과정으로 넘어가 말하고자 하는 1. 주제 파악 2. 의도 파악 3. Semantic slot filling을 과정을 거침. 이후 Dialouge Management를 거치고 답변 생성 후 TTS로 유저에게 전달.
  • 과거엔 SLU에서 Domain recognition이 없었다. 왜냐면 dialouge system이 specific domain에 국한됐기 때문임. 하지만 최근엔 넓은 범위의 domain을 다루고자 하는 필요성이 있기 때문에 추가 됨.
  • Intent detection을 다른말로 Intent classification이라고도 부름.
  • 도메인 별, 의도 별 사전 정의된 카테고리를 이용해 유저 발화 분류함. 만약 사전 정의돼 있지 않다면 의도를 잘못 분류해서 잘못된 대답을 하게 됨.

 

  • 의도 분류에서 어려운 것은 다중 Domain이 들어왔을 때 어떤 Domain에 속하는지 명확히 해야함. 그렇지 않으면 Domain보다 더 세분화되어 있는 Intent category으로 접근하기 어려움. 위의 예시로 들면 맥락상 기차 말고 비행기 탈꺼니까 기차 환불하고 가장 빠른 비행기 시간 확인해 달라는 것임. 근데 환불인지 시간 확인인지 Domain 수준에서 파악이 안되면 그 다음 단계인 Intent detection과 semantic slot filling을 못함.

 

2. Intent Detection의 어려움

2.1 데이터 부족

부족하다.

 

2.2 화자 표현 광범위, 모호함.

  • 일반적으로 구어체 사용하고, 짧은 문장과 넓은 컨텐츠를 다루기 때문에 의도 파악이 어려움. 가령 예를 들어 “I want to look for a dinner place”라고 말하면 저녁 식사 장소 찾고 싶단건데 domain이 명확하지 않음. (사실 이 정도면 충분하다 생각하는데 불확실한가 봄)
  • 다른 예시 들자면 Hanting이라는 호텔이 있지만 Hanting Hotel이라고 구체적으로 이야기하지 않으면 이해하기 어려움. 또는 티켓 예약 하고 싶다고만 말하면 비행기 티켓인지 기차 티켓인지 콘서트 티켓인지 알 수 없는 것처럼 화자의 표현이 광범위하거나 모호한 경우가 발생해서 machine이 적절한 답변을 주기 어려움.

 

2.3 암시적인 의도 분류

의도는 명시적인 것(Explicit)과 암시적인 것(Implicit)으로 나눌 수 있음. 유저가 암시적으로 말하면 진짜 유저 의도가 뭔지 추론할 필요가 생김. 예를 들어 요새 건강에 관심이 있다고 말하면 아 오래살고 싶구나 하고 이해할 수 있어야 하는데 그게 어려움. (내가 만든 예시)

 

2.4 multiple intents detection

multi-label classification과 비슷하지만 다름. 그 차이점은 multi-label classification은 긴 문장 multiple intnets detection은 짧은 문장을 다룸. 짧은 문장으로 다중 의도 분류 해야해서 어려움. 짧은 발화안에 다양한 의도 분류를 해야하고 그 의도 수를 결정해야 하는게 쉽지 않음.

 

3. Main methods of intent detection

3.1 전통적인 Intent detection 방법론

  • 최근에는 intent detection을 Semantic Utterance Classification (SUC)로 여긴다.
  • 1993년엔 rule-based가 제안된 적 있고, 2002-2014까진 통계적으로 피처를 뽑아서 분류했다. 가령 예를 들면 나이브 베이즈나 SVM이나, Adaboost, 로지스틱 회귀를 썼었다. 근데 룰베이스의 경우 정확도는 높지만 새로운 카테고리가 추가될 때마다 수정해야 하는 번거로움이 있고, 통계적인 방식은 피처의 정확도나 데이터 부족문제가 있다. 근데 지금도 화자의 real intent detection은 여전히 어려운 연구 주제다.

 

3.2 현재 주류 방법론

워드 임베딩, CNN, RNN, LSTM, GRU, Attention, Capsule Network 등이 있다. 전통적인 방법과 비교하면 성능이 크게 좋아졌다.

 

3.2.1 워드 임베딩 기반 의도 분류

 

3.2.2 CNN 기반 의도 분류

  • CNN 기반으로 해서 피처 엔지니어링 과정을 많이 줄이고 피처 표현력도 좋아졌다. 근데 여전히 CNN으론representation 한계 있다.

 

3.2.3 RNN 기반 의도 분류

  • CNN과 달리 워드 시퀀스를 표현할 수 있음. 2013년에 context 정보 이용해서 Intent detection의 error rate를 줄인 연구가 있음. RNN은 기울기 소실과 기울기 폭발 문제가 있고 이 때문에 long-term depdendence 문제를 초래함.
  • 그래서 이 문제 해결하려고 LSTM이 나옴. LSTM가지고 ATIS 데이터셋 (Air Travel Information System)에서 RNN보다 에러율 1.48%을 줄임.
  • GRU는 LSTM 개선한 모델임. ATIS와 Cortana 데이터셋에서 성능이 둘다 같았지만 GRU가 파라미터가 적었음.
  • 2018년엔 짧은 텍스트로 인해 발생하는 data sparse 문제를 해결하기 위해 Biterm Topic Model(BTM)과 Bidirectional Gated Recurrent Unit(BGRU) 기반 멀티턴 대화 의도 분류 모델이 제시됨.
  • 위 두 모델을 합친 모델은 의료 의도 분류에서 좋은 성능을 냈음.

 

. . .

3.2.6 캡슐 네트워크 모델 기반 의도 분류

캡슐 개념은 CNN의 표현 한계 문제를 해결하기 위해 2011년에 힌튼에 의해 제시됐었음. 캡슐은 vector representation을 가짐. 이후 2017년에 CNN scalar output feature detector를 vector representation capsule로 대체하고 max pooling을 프로토콜 라우팅으로 대체하는 캡슐 네트워크가 제안됨. CNN과 비교하자면 캡슐 네트워크는 entity의 정확한 위치 정보를 유지함.

결론을 말하자면 capsule network는 텍스트 분류 태스크 잘 수행하고, multi-label 텍스트 분류에도 잘 동작함.

. . .

 

4. 의도 분류 평가 방법

현재 Intent Detection은 Semantic Discourse Classification 문제로 여겨짐. 결론은 F1-score 사용함.

 

5. 성능 비교

아래는 다른 연구 논문에서 가져온 성능 비교 결과임. 데이터셋은 SNIPS와 CVA(Commercial Voice Assistant)를 사용했음. 참고로 SNIPS는 영어 데이터셋이고 CVA는 중국어 데이터셋임.

Intent Capsnet이 가장 성능이 좋더라.

 

Conclusion

  • 머신러닝 기반의 의도 분류 태스크는 깊이 이해 못한다. 그래서 딥러닝 기반 의도 분류 태스크가 성능이 좋다. capsule network model이 의도 분류 태스크에서 좋은 성능을 내고, multi-label classification도 잘한다. self-attention 모델은 의도 분류 과정에서 문장의 다양한 semantic feature들을 추출할 수 있다.
  • 의도 분류는 e-commerce, travel consumption, medical treatment, chat에도 적용되고 있으며, 침입 탐지 시스템과 같은 네트워크 보안 분야에서도 적용된다.
  • 전통적인 dialouge system은 주로 single intent detection만 가능하다. 하지만 다양한 의도는 셀 수 없이 많으므로 multi intent detection이 가능하도록 연구가 필요하다.

 

 

궁금해진 것

  1. Semantic slot filling의 과정은 구체적으로 어떻게 이뤄지는가?
  2. Dialouge Management란 무엇이고 어떻게 동작하는가?
  3. Dialouge Generation 과정은 구체적으로 어떻게 이뤄지는가?
  4. task-oriented vertical domain이 무엇인가? vertical이 있으면 horizontal domain도 있을 것인데 각각은 무엇인가?

알게 된 것

  1. 도중에 fine-grained라는 말이 나옴. 이건 잘게 쪼갠 것을 의미함. 반면 coarse-grained는 덩어리째를 의미함.

 

한 줄 평

  • 큰 틀에서 연구가 어떻게 이뤄지는지 대략적으론 알 수 있어서 좋았지만 Dialougue System의 구체적으로 어떻게 이뤄지는지 나와있지 않고 데이터셋을 잘 정리해서 소개하지 않아서 아쉬움.

+ Recent posts