큰 용량의 데이터셋을 크롤러(crawler)로 다운받거나 아니면 어떠한 모델을 학습할때 즉시 로그를 확인할 필요는 없지만 학습기간이 오래걸릴때 등등 경우에는 터미널(terminal)을 계속 열어두면 많이 불편하다.

 

특히 ssh(secure shell)로 다른곳에서 열어두고 학습하다가 갑자기 인터넷이라도 끊기면 다시 시작해야하는 번거로움도 발생한다.

 

이러한 경우 터미널이 종료되어도 백그라운드 작업을 nohup로 진행할 수 있다. nohup은 posix 커맨드로 HUP(signal hang up) 신호를 무시한다[1].

 

기본적인 사용은 다음과 같다[2].

$ nohup test.py &

그리고 많은 경우가 로그를 보기를 원하는데 위 같은 간단한 명령은 실행했던 디렉토리에 nohup.out 이라는 로그파일을 남기게 된다. 그러나 이는 모두가 원하는 경우는 아니다.

 

그래서 다음과 같이 사용하면 원하는 디렉토리에 원하는 로그파일을 남길 수 있다.

$ nohup python -u ./test.py > ./test.log &

python의 -u 플래그는 --help 에서도 확인이 가능한데, 강제로 stdout 과 strerr 스트림 버퍼링을 해제하는 옵션이다.(단 stdin에는 적용안됨) 다시 요약하면 실시간으로 로그를 남기고 싶다면 -u를 아니면 -u를 빼도 된다[3].

 

종료는 해당 프로세서가 종료가 되면 자동으로 종료가 되나 강제로 종료를 하고싶다면 실행한 프로세서의 pid를 확인한후 kill 명령어로 프로세서를 강제로 종료하는 방법이 있다.

참고문헌

1. "nohup", May 2020, en.wikipedia.org/wiki/Nohup

2. "리눅스 nohup 사용법", Mar 2019, zetawiki.com/wiki/%EB%A6%AC%EB%88%85%EC%8A%A4_nohup_%EC%82%AC%EC%9A%A9%EB%B2%95

3. "Nohup is not writing log to output file", Oct 2012, stackoverflow.com/questions/12919980/nohup-is-not-writing-log-to-output-file

일반적인 분류(classification)문제를 다루다보면 항상 마지막에 linear layer가 들어가게 된다.

이는 우리가 생각할수 있는 가장 간단한 방식이고, 이는 말이 된다.

 

그런데 만약 입력 텐서의 크기가 고정되지 않다면 출력 텐서의 크기도 마찬가지로 다르게 나온다, 여기서 문제가 보통 사용하는 linear layer는 입력 크기가 고정되기에 입력 사이즈가 변화되면 에러가 발생한다.

 

그래서 이를 위해서 입력에 관계없이 출력을 고정하도록 설계된 Adaptive Pooling을 사용할 수 있다.[1, 2]

 

단순히 출력을 지정한 튜플값으로 맞춰주는 pooling이라고 생각하면 된다.

 

예로 다음과 같은 텐서(b, c, h, w)를 만들어서 Max pooling을 했다고 가정한다.

>>> a = torch.randn(1, 2, 5, 5)
>>> b = F.max_pool2d(a, stride=1, kernel_size=2)
>>> b.size()
torch.Size([1, 2, 4, 4])
>>> b
tensor([[[[ 1.4379,  1.4379,  1.0259,  1.0259],
          [ 1.8803,  0.7414,  0.0418,  0.8152],
          [ 1.8803,  0.7414,  0.0418, -0.0604],
          [ 1.3686,  0.2596,  1.2160,  1.2160]],

         [[ 1.5899,  0.6822,  1.4687,  1.4687],
          [ 1.5899,  0.6822,  1.4687,  1.4687],
          [-0.1199,  0.7847,  0.7847,  0.7320],
          [-0.2405,  0.7847,  0.7847,  0.6991]]]])

그리고 위와 같은 텐서는 Adaptive Max Pooling을 사용하면 다음과 같이 표현할수 있다.

>>> c = F.adaptive_max_pool2d(a, (4, 4))
>>> c.size()
torch.Size(1, 2, 4, 4)
>>> c
tensor([[[[ 1.4379,  1.4379,  1.0259,  1.0259],
          [ 1.8803,  0.7414,  0.0418,  0.8152],
          [ 1.8803,  0.7414,  0.0418, -0.0604],
          [ 1.3686,  0.2596,  1.2160,  1.2160]],

         [[ 1.5899,  0.6822,  1.4687,  1.4687],
          [ 1.5899,  0.6822,  1.4687,  1.4687],
          [-0.1199,  0.7847,  0.7847,  0.7320],
          [-0.2405,  0.7847,  0.7847,  0.6991]]]])

Adaptive Max Pooling의 매개변수로 위 코드처럼 튜플값이 들어가는데 이는 2d Max pooling을 적용하고 나올 결과의 (h, w)를 의미한다. 위 코드 경우에는 (b, c, 4, 4)로 나오기를 원해서 (4, 4)라는 튜플을 넘겨준 것 이다.

 

*참고로 torchvision의 resnet은 Adaptive Avg Pool2d가 적용되었다. pre-trained weight은 이미지넷 224사이즈로 학습이 된걸로 보인다.[3]

>>> import torchvision.models as models
>>> models.resnet18()
ResNet(
  (conv1): Conv2d(3, 64, kernel_size=(7, 7), stride=(2, 2), padding=(3, 3), bias=False)
  (bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
  (relu): ReLU(inplace=True)
  (maxpool): MaxPool2d(kernel_size=3, stride=2, padding=1, dilation=1, ceil_mode=False)
  (layer1): Sequential(
    (0): BasicBlock(
      (conv1): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (relu): ReLU(inplace=True)
      (conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    )
    (1): BasicBlock(
      (conv1): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (relu): ReLU(inplace=True)
      (conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    )
  )
  (layer2): Sequential(
    (0): BasicBlock(
      (conv1): Conv2d(64, 128, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
      (bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (relu): ReLU(inplace=True)
      (conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (downsample): Sequential(
        (0): Conv2d(64, 128, kernel_size=(1, 1), stride=(2, 2), bias=False)
        (1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      )
    )
    (1): BasicBlock(
      (conv1): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (relu): ReLU(inplace=True)
      (conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    )
  )
  (layer3): Sequential(
    (0): BasicBlock(
      (conv1): Conv2d(128, 256, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
      (bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (relu): ReLU(inplace=True)
      (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (downsample): Sequential(
        (0): Conv2d(128, 256, kernel_size=(1, 1), stride=(2, 2), bias=False)
        (1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      )
    )
    (1): BasicBlock(
      (conv1): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (relu): ReLU(inplace=True)
      (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    )
  )
  (layer4): Sequential(
    (0): BasicBlock(
      (conv1): Conv2d(256, 512, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
      (bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (relu): ReLU(inplace=True)
      (conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (downsample): Sequential(
        (0): Conv2d(256, 512, kernel_size=(1, 1), stride=(2, 2), bias=False)
        (1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      )
    )
    (1): BasicBlock(
      (conv1): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (relu): ReLU(inplace=True)
      (conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    )
  )
  (avgpool): AdaptiveAvgPool2d(output_size=(1, 1))
  (fc): Linear(in_features=512, out_features=1000, bias=True)
)

references

1. "Contiguous vs non-contiguous tensor", Nov 2018, discuss.pytorch.org/t/contigious-vs-non-contigious-tensor/30107

2. "Adaptive_avg_pool2d vs avg_pool2d", Oct 2018, discuss.pytorch.org/t/adaptive-avg-pool2d-vs-avg-pool2d/27011

3. "TORCHVISION.MODELS", pytorch.org/docs/stable/torchvision/models.html

pytorch를 사용하다보면 코드속에 .contiguous()가 가끔 보일때가 있다. 이는 연속적인 메모리 텐서를 반환하는 메서드로 만약 어떤 연산을 사용할때 이를 사용하지 않으면 에러가 발생하는 경우가 생긴다.

 

예로 아래와 같이 a라는 텐서를 만들고 텐서의 형태(shape)와 보폭(stride)을 살펴보면 다음과 같다.[1]

>>> a = torch.randn(2, 3, 4)
>>> a.size()
torch.Size([2, 3, 4])
>>> a.stride()
(12, 4, 1)

여기서 보폭은 해당하는 차원의 원소에 접근할때 건너 뛰어야 할 원소들의 수(보폭)를 의미한다. 위의 예로 0차원 에서 건너 다음 원소를 가져올때는 12개의 원소를 뛰어 넘어야 한다는 의미이다.

이는 말이 된다. 하지만 메모리에 연속적으로 값이 들어있지 않는 경우에는 말이좀 달라진다.

 

예로 아래와 같이 a텐서의 차원 0과 1을 바꾸어 보았다. 그리고 텐서의 형태가 바뀐 a와 같은 형태의 b를 만들어서 비교 해보면 다음과 같다.[1]

>>> a = a.transpose(0, 1)
>>> a.size()
torch.Size([3, 2, 4])
>>> a.stride()
(4, 12, 1)
>>> a.is_contiguous()
False
>>> b = torch.randn(3, 2, 4)
>>> b.stride()
(8, 4, 1)
>>> b.is_contiguous()
True

여기서 눈여겨 볼점은 a와 b가 같은 형태로(3, 2, 4) 유지되나 실제 원소에 접근하기 위한 보폭을 살펴보면 값이 다르다는 점(a: [4, 12, 1], b: [8, 4, 1])을 확인할 수 있다.

이런 이유는 텐서 a의 형태가 변화될때 실제로 해당 원소에 접근할때는(메모리에서는 원소들의) 위치가 변화되지 않았고 접근 인덱스만 변화되었기 때문이다. 이는 pytorch에서 일부 텐서 연산을 수행함에 있어서 성능향상을 위한것으로 생각된다.(만약 텐서의 형태가 바뀔때마다 메모리에 있는 그래도 배치하려면 재 할당을 해주어야 하는데 이러한 연산이 잦으면 오히려 성능을 떨어뜨리는 원인이 될수있다.)  그래서 형태가 바뀐 a의 보폭을 해석해보면 차원 0 에서 다음 원소에 접근할때 8개가 아닌 4개의 원소만 건너면 b와 같은 텐서처럼 사용을 할수가 있다는 의미로, 실제 메모리에는 a와 b가 다른 순서로 배열이 되어있으나 메모리 재할당을 할필요 없이 접근 인덱스만 바꾸면 b처럼 사용을 할수가 있다는 의미가 된다.

 

그리고 .contiguous() 메서드는 다음과 같이 이러한 a와 같은 비연속적인 텐서를 연속적으로 만들어주는 역할을 한다.

>>> a = a.contiguous()
>>> a.stride()
(8, 4, 1)

이제 텐서 b랑 같아졌다.

 

아래의 예는 contiguous하지 않은 텐서를 펴서 사용할 때 접할 수 있는 에러이다.

>>> a = a.transpose(0, 1)
>>> a = a.view(-1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
RuntimeError: view size is not compatible with input tensor`s size and stride (at least one dimension spans across two contiguous subspaces). Use .reshape(...) instead.

 

이를 해결하려면 텐서를 연속적으로 만들어주면 된다.

>>> a = a.contiguous()
>>> a = a.view(-1)
>>> a.size()
torch.Size([24])

 

또는 .contiguous() 없이 아래와 같이 사용이 가능하다.

>>> a = torch.randn(3, 2, 4)
>>> a = a.transpose(0, 1)
>>> a = a.reshape(-1) # view() 대신 reshape() 사용
>>> a.size()
torch.Size([24])

위의 예 처럼 연속을 고려하는 메서드(reshape)와 고려하지 않는 텐서변환 메서드(view)가 존재하니 알맞게 사용하면 에러를 예방할수 있다.

참고문헌

1. "Contiguous vs non-contiguous tensor", Nov 2018, discuss.pytorch.org/t/contigious-vs-non-contigious-tensor/30107

+ Recent posts