본문 바로가기
A. Development/for Machine Learning

Pytorch 개발 팁

by IMCOMKING 2020. 1. 16.

Pytorch 설치하기

Pytorch를 설치하는 가장 간편한 방법은 conda를 이용하는 것이다. conda의 설치 및 사용 방법은 다음을 참조하길 바란다.
conda가 이미 있다면 아래의 명령을 실행하면 최신 버전의 pytorch가 설치된다. 이때 -c 옵션을 주는 것이 중요하다. 이는 pytorch채널에서 해당 라이브러리를 탐색하여 설치한다는 의미이다. 다른 pytorch 버전을 설치하고자하면, 공식 홈페이지를 참조하자.
 
conda install pytorch -c pytorch
 
 

Multi-GPU 환경에서 특정 GPU만 default로 사용하기

bash에서 다음을 실행

export CUDA_VISIBLE_DEVICES=4

 

혹은 python에서 다음을 실행

os.environ["CUDA_VISIBLE_DEVICES"] = 4

또는

torch.cuda.set_device(4)

 
 

Tensorboard로 graph그리기

TF의 tensorboard를 pytorch에서도 그대로 쓸 수 있다. SummaryWriter에 필요한 정보를 add하여 쉽게 그래프를 그릴 수 있다.
 

Tensorboard 실행하기

tensorboard --logdir /tmp/tensorboard/exp1 --port 9000 --bind_all
 
* 이때 --bind_all을 해주어야 ip주소를 통해 외부 네트워크에서 접속이 가능하다.
또한 logdir을 절대경로로 설정해 주는 것이 여러모로 용이하다.
그리고 tensorboard에서는 logdir 아래 생성된 각 directory를 하나의 실험으로 간주해서 보여준다. 따라서 이 경로 아래에 개별 실험을 폴더 단위로 저장하면 쉽게 실험 별 성능 비교를 할 수 있다.
 
 

Tensorboard 설치 문제 해결

원래는 pip install tensorboard만 하면 잘 설치가 되어야한다. 그런데 nvidia-tensorboard 나 tensorboardX등이 뭔가 같이 설치되어 있는 경우, tensorboard가 정상 실행되지 않는 문제가 있다. 이러한 경우 일단 tensorboard와 관련된 설치를 모두 pip uninstall하고, 아래의 명령을 실행해서 깔끔하게 재설치한다.
 
conda install -c conda-forge tensorboard-plugin-wit
conda install -c conda-forge tensorboard
 
 

완전히 Deterministic하게 실험 재현하기

random.seed(seed)
os.environ['PYTHONHASHSEED'] = str(seed)
np.random.seed(seed)
torch.manual_seed(seed)
torch.cuda.manual_seed(seed)
torch.cuda.manual_seed_all(seed) # if you are using multi-GPU.
torch.backends.cudnn.benchmark = False
torch.backends.cudnn.deterministic = True
 
 

AttributeError: cannot assign module before Module.__init__() call 에러 해결

이 에러는 nn.Module을 상속받은 class가 __init__에서 먼저 super의 __init__을 호출하지 않아서 발생하는 문제이다.

class BaseNetwork(nn.Module):
    def __init__(self):
        super().__init__()

위와 같이 호출하면 된다.

 

 

https://discuss.pytorch.org/t/attributeerror-cannot-assign-module-before-module---init---call/1446

 

 

 

특정 value의 first occurance index 찾기

eos_detection = (pred_text == EOS_token).float()
index_tensor = torch.arange(eos_detection.shape[-1], 0, -1)
eos_index = eos_detection * index_tensor
valid_length = torch.argmax(eos_index, dim=-1) + 1

원리는 torch.arange를 이용해서 0또는 1로 구성된 detection랑 곱해서 index로 바꾸고 argmax를 이용하는 것.

 

https://stackoverflow.com/questions/56088189/pytorch-how-can-i-find-indices-of-first-nonzero-element-in-each-row-of-a-2d-ten

 

 

 

두 Tensor가 서로 동일한 값을 갖는지 체크하기

torch.equal(a, b)

 

Tensor 주소값 가져오기

tensor.data_ptr() -> 첫번쨰 element의 pointer를 return해서 tensor의 id값으로 쓸 수 있음.

https://pytorch.org/docs/stable/generated/torch.Tensor.data_ptr.html

 

detach() vs clone()

detach()는 원래의 tensor에서 computation graph만 끊어서 gradient가 전파되지 않도록 만드는 것이다. 다만 원래 tensor와 메모리를 공유하므로, 여기에 변경을 가하면 원본 tensor도 그 값이 변한다.
clone()은 원래의 tensor를 computation graph까지 그대로 복사해서 새로 만들어내는 tensor이다. 새로운 메모리에 할당하므로 원본 tensor가 바뀌어도 상관이 없다.

 

 

 

Conv에서 padding이 계산되는 순서

nn.conv에서 padding 파라미터의 경우, 먼저 convolution을 하기 전에 input data에 대해 padding을 적용하는 옵션이다. 이때 지정한 padding 사이즈가 n이라면, 이 n만큼의 0값을 좌우에 각각 더한다. 따라서 padding 1을 입력했다면 왼쪽에 1칸, 오른쪽에 1칸을 붙여서 총 2칸을 더 붙이게 되는 것이다.

 

https://discuss.pytorch.org/t/padding-for-convolutions/5881

 

 

Conv1d와 ConvTranspose1d 의 output shape 계산하기

def conv1d_output_shape(data_len, kernel_size=1, stride=1, padding=0, dilation=1, **kwargs):
    """
    https://discuss.pytorch.org/t/utility-function-for-calculating-the-shape-of-a-conv-output/11173/5
    Utility function for computing output of convolutions
    """
    return (data_len + (2 * padding) - (dilation * (kernel_size - 1)) - 1)// stride + 1

def convtrans1d_output_shape(data_len, kernel_size=1, stride=1, padding=0, output_padding=0, dilation=1, **kwargs):
    """
    https://discuss.pytorch.org/t/utility-function-for-calculating-the-shape-of-a-conv-output/11173/5
    Utility function for computing output of transposed convolutions
    """
    return (data_len - 1) * stride - 2 * padding + dilation * (kernel_size-1) + output_padding + 1

 

 

ConvTranspose의 파라미터 이해하기

- output_padding을 제외한 모든 파라미터의 의미가 convolution의 역함수적인 의미를 갖는다. 즉 padding을 넣으면 output shape의 크기가 양쪽으로 그만큼 줄어든다.
- output_padding은 순수한 의미로 final zero_padding이다.

 

https://medium.com/@santi.pdp/how-pytorch-transposed-convs1d-work-a7adac63c4a5

 

 

nn.Softmax 와 nn.CrossEntropyLoss의 차이

nn.Softmax는 학습에 사용되는 loss와는 전혀 무관하다. Attention등에서 prob을 구하거나, RL의 policy에서 action sampling을하기 위해 사용되는 등, 즉 모델 아키텍쳐 상에 softmax prob 계산이 필요할 때에만 사용해야한다. 이와 동치인 구현으로는 모델이 내놓은 last activation(=logit)에 대해 nn.functional.F.softmax를 사용해도 된다. 중요한 점은 nn.Softmax가 내놓은 prob에 대해서는 바로 NLLloss 적용이 불가능하다는 것이다. 이론 상으로는 이 둘을 결합하면 CrossEntropyLoss가 되지만, 이런 식의 구현은 numerically 매우 불안정하기 때문에 Nan값이 쉽게 발생한다. 

 

따라서 권장되는 구현은 classification task 학습에는 nn.Softmax의 호출없이 model의 output에 nn.CrossEntropyLoss만 사용해야하고, softmax prob계산이 필요한 경우에 한해서 nn.Softmax를 쓰거나 F.softmax를 사용해야한다. 

 

그밖에 두번 째 방법으로는 nn.LogSoftmax와 nn.NLLloss를 결합해서 CrossEntropyLoss로 사용하는 것은 가능하고, 이 경우 model이 내놓은 output에 대해 torch.exp()를 취해주어야 한다.

https://discuss.pytorch.org/t/trouble-getting-probability-from-softmax/26764/6

 

 

* F.log_softmax는 N번 반복해서 사용해도 1개의 F.log_softmax와 동치이다.

log_softmax에서 취해지는 log는 자연상수 e를 밑으로 하는 자연로그 이다. 따라서 softmax에서 생기는 지수 e^x는 자연로그에 의해 상쇄되어, log_softmax(log_softmax(x)) = log_softmax(x) 이 된다.

이는 sigmoid에서도 동일하다. 즉, log_sigmoid(log_sigmoid(x)) = log_sigmoid(x) 이다.

 

즉 F.log_softmax(x)를 한 다음, nn.CrossEntorpyLoss = F.nll_loss(F.log_softmax(x))를 취할 경우, 아래와 같은 수식이 된다.

F.nll_loss(F.log_softmax(F.log_softmax(x))) = F.nll_loss(F.log_softmax(x)) 이므로, 결국 이 F.log_softmax를 사용해도 nn.CrossEntorpyLoss를 쓴것과 동일한 Loss가 계산된다.

다만 주의할 점은, F.log_softmax()를 사용한 output임을 인지하고서, 확률 계산시 exp()만 취하여 softmax prob을 계산해야한다는 점이 있다.

또한 이렇게 구현하더라도 실제 loss가 같은지 다른 사람들이 모를 수 있기 때문에, 가능하면 CrossEntropy보다는 직관적인 NLLloss를 쓰는게 좋다.

마지막으로 F.log_softmax + CE가 수학적으로는 그냥 CE와 동치일 수 있지만 softmax에 의해서 numerical error가 발생할 수 있기 때문에 가능하면 중복 연산은 피하는 게 좋다.

 

 

 

기본 nn.module의 RNN들에 구현 된 Dropout은 사용하면 안된다.

왜냐하면 RNN에 dropout을 적용하려면 time step이 흘러도 dropout mask가 바뀌지 않는 방식으로 구현되어야만 올바르게 동작한다.

그런데 GRU나 LSTM에 구현된 dropout은 mask가 매 time step마다 바뀌게 구현되어 있다고 한다.

 

따라서 직접 mask를 fix시켜서 구현해주거나, LockedDropout과 같은 모듈을 사용해야한다.

 

https://towardsdatascience.com/learning-note-dropout-in-recurrent-networks-part-2-f209222481f8

https://discuss.pytorch.org/t/dropout-in-lstm/7784

 

 

# nn.RNN모듈에 구현된 일반 Dropout

이미지 출처: https://nmhkahn.github.io/RNN-Regularizations

 

 

# LockedDropout == Variational Dropout == RNN Dropout

이미지 출처: https://nmhkahn.github.io/RNN-Regularizations

 

 

Network 파라미터 중 key가 똑같은 일부만 로딩하기

원리는 간단하다. save한 state_dict와 현재 network의 state_dict를 비교해서, key값이 똑같은 value만 현재의 state_dict에 업데이트하고, load_state_dict로 현재의 state_dict를 다시 불러오는 것이다.
saved_state = state["conf.network"]
current_state = conf.network.state_dict()

for k, v in saved_state.items():
    if k in current_state.keys():
        current_state.update({k:v})
conf.network.load_state_dict(current_state)

 

 

 

 

nn.init

layer의 weight initialization을 하려면 다음과 같이 할 수 있다.
conv1 = nn.Conv1d(1, out_channels=32, kernel_size=60, stride=3, dilation=2)  # groups
torch.nn.init.normal_(conv1.weight, mean=0, std=1/math.sqrt(conv1.weight.shape[0]))

 

https://stackoverflow.com/questions/49433936/how-to-initialize-weights-in-pytorch

 

 

nn.functional.pad 

 
이 함수는 1d, 2d, 3d tensor의 다양한 padding을 하는 데 사용된다. 사용법은 간단하다. 아래와 같이 입력하면, 1d tensor에 왼쪽에 1칸, 오른쪽에 5칸 만큼 zero-padding을 집어넣게 된다.
F.pad(target_1d_tensor, (1, 5), "constant", 0) 
 

Padding mode에는 constant, reflect, replicate, circular 등이 있고, 많은 경우 0 value, constant mode 즉 zero-padding을 가장 많이 사용한다.

Reflection padding은 아래와 같이 가장 가까운 픽셀로부터 거울 상(원점을 중심으로 점대칭)으로 값을 복사해오는 것이다. 이러면 결과적으로 동일한 이미지가 한번 더 그려지게 된다

 

 

 

 

Replication padding은 아래와 같이 가장 가까운 픽셀의 값을 복사해오는 방식이다. 단, padding하는 위치가 멀어지면 그만큼 복사해오는 대상 픽셀의 위치도 멀어진다. 이렇게 하면 결과적으로 반전된 이미지가 새로 그려지게 된다.

 

 

 

 

data_loader에 pin_memory 사용

data_loader를 생성할 때, pin_memory=True를 인자로주면, 매번 get_item을 통해 CPU 메모리에서 GPU 메모리로 올라가는 과정을 가속화할 수 있다.

 

 

 

 

Data_loader의 get_item에서 heavy한 CPU load를 caching하기

def __init__(self):
self.data_list = defaultdict((lambda: (None, None)))

def __len__(self):
return int(len(self.wave_chunk_list) * self.conf.data_usage)

def __getitem__(self, index):
xy_pair = self.data_list[index]

if xy_pair[0] is not None and self.conf.data_caching:
x, y = xy_pair
else:
x = slow_cpu_load(x)

y = slow_cpu_load(y)

self

.data_list[index] = x

,

y

return

x

,

y

 

 

 

Jit을 이용해서 Pytorch 모델을 다른 언어에서 serving하기

Jit으로 컴파일한 결과물은 zip으로 되어있고, 모델의 파라미터와 아키텍쳐가 컴팩트하게 저장되어 있다. 이를 java나 등등 원하는 플랫폼에서 불러와서 모델 서빙이 가능하다.
Jit의 목적은 모델의 inference 속도를 빠르게 하기 위해서라기보다는, python dependency없이 모델을 서빙하기위함이다.
그런데 JIT으로 컴파일된 아키텍쳐를 보통 torch C++로 inference를 하기 때문에, 체감 속도가 더 빨라진다.

Script

일반적으로 쓸 수 있음

Trace

좀더 단순한 경우에 사용
 

 

 

 

Automatic Mixed Precision(AMP)

pytorch 1.6 버전부터 지원되는 기능으로, float 16(half precision)과 일반적인 float 32를 적절히 섞어서 자동으로 GPU 최적화된 학습 및 inference를 가능하게 하는 기능이다.
 

이 실험에 따르면 일반적인 학습 속도가 2배 정도 빨라진다고 보고되었다.

 

pytorch 공식 AMP 예제를 참고하여 간단한 형태의 구현을 해본다.

 

 

from torch.cuda.amp import autocast, GradScaler

self.grad_scaler = GradScaler(self.conf.autocast)

with autocast(self.conf.autocast):
    loss, exp_result = self.do_one_batch(cnt_i, batch_dict, e_step, run_mode)

self.grad_scaler.scale(loss).backward()

 

음 그런데 실험해보니, with autocast(): 안에서 forward만 사용하는 inference상황에서는 오히려 속도가 더 느려지는 현상이 있었다. 

이는 아마도 fp16과 fp32를 변환하는 과정에서 속도저하가 있는 것으로 보인다. 이런 경우에는 model.half()로 모델 자체를 FP16으로 변환시켜놓고, 데이터만 매번 data = data.half()로 변환해서 넣어주면 느려지는 문제를 해결할 수 있다.(약간의 속도 개선이 된다.)

 

Mixed precision에서 속도가 느려지는 정확한 현상

K80모델은 mixed precision을 지원하지 않아서 FP16으로하면 속도가 더 느려진다. Mixed precision을 사용하려면 텐서코어가 있어야하고, 이는 볼타 아키텍쳐부터 도입되었다.

 

 

Multi-GPU

용어

rank: 글로벌 process id
local rank: 해당 node에서의 process id
workd size: 모든 글로벌 process의 합(multi-gpu에서는 process가 gpu에 해당한다.)
node size: 독립된 머신의 수
num_gpu: 각 머신당 사용할 gpu 개수
ex) node size * num_gpu = world_size
 

참고 예제

아래의 예제가 가장 정확히 pytorch DDP가 구현해진 예제이다. 핵심은 멀티프로세스를 띄운다는 점과, 수동으로 직접 몇번 gpu 모델과 데이터를 올릴지 직접 지정해줘서 module class문제가 발생하지 않는다.
 

DataParallel

모델만 .cuda()로 올려주고, 모델만 DataParallel로 감쌓은다음, 데이터를 넣어주면 알아서 분산함.
즉 데이터를 to()로 건드리지 않고 그냥 올려야함.
 
m = MyModule().cuda()
dp_m = nn.DataParallel(m)
output = dp_m(batch_data)
 
 
 
 

Multi-GPU를 사용하게되면 N개의 gpu로 동일한 모델이 카피되고, 데이터셋은 배치 dim을 따라서 N등분되어 들어가진다.

 
 

Batch-normalization에 의한 속도 저하

분산 GPU에서 sync batch norm을 자주쓰면 학습 속도가 매우 느려진다. async batch norm을 쓰면 속도는 빨라지나 성능이 불안정해진다.
validation을 할 때는 반드시 sync batch norm을 쓰거나, single gpu에서 수행해야 정확한 성능측정이 가능하다.
 
또 다른 해결방법으로는 batch norm 대신 layer norm을 쓰는 것이다.
 
 

DDP vs DP

 

pytorch 이미지넷 예제

근데 이게 좀 특이함, 멀티프로세싱이 섞여있어서
 
 
 
 
DataParallelCriterion
DataParallelModel
 
 
이건 .to(device)로 강제 지정하고있는데, 잘못 된것같음...
 
 
 

Data Parallel의 버그

아래 이슈와 같이 nn.module을 상속받은 클래스가 forward가 아닌 다른 함수로 멤버 변수 네트워크를을 호출할 경우 gpu parallel이 정상작동하지 않는다.
testmodule testmodule2가 같은 기능을 하는 클래스이긴 하나 testmodule 처럼 하면 파라메터를 여러 지피유에 replica하는게 안된다. 즉 self.operation_function 같은 함수의 파라메터를 캐치를 못한다.
 
 
 

 

Pytorch CUDA Stream

stream = torch.cuda.Stream(device=GlobalVar.Device)
with torch.cuda.stream(stream):
t = t.to("cpu", non_blocking=True).data.numpy()

 

 

torch.cuda.Stream을 사용하면 GPU <-> CPU 로 데이터를 전송할 때 non_blocking으로 데이터를 주고 받을 수 있다. 그래서 latency를 10~20% 정도 줄일 수 있는데, 다만 간헐적으로 아무런 값도 전달받지 못하고 그냥 0.0의 값만 전달받는 경우가 존재한다. 따라서 이에 대한 예외처리가 반드시 필요하다.

 

 

 

 

 

'A. Development > for Machine Learning' 카테고리의 다른 글

Numpy Optimization and Parallelization  (0) 2020.01.17
Pandas  (0) 2018.06.29
Numpy 문법, API, 환경설정 / HDF5를 위한 H5PY API  (0) 2015.12.01

댓글