DataParallel (DP)

DP은 데이터 병렬화 기술 중, 싱글노드에서만 사용할 수 있는 병렬화 기술입니다.

Forward and Backward passes with torch.nn.DataParallel. src: https://medium.com/huggingface/training-larger-batches-practical-tips-on-1-gpu-multi-gpu-distributed-setups-ec88c3e51255

위의 그림과 같이 포워딩(첫 행)에서는 배치를 GPU 개수만큼 쪼개 보내고, 모델도 복제(replicas)하고 각 장치에서 포워딩후에 그 결과값을 가져오는 형식입니다. 백워드에서는 [o1,o2,o3,o4]을 GPU-1에서 받은 것을 label과의 차이를 계산하여 각 손실 [loss1, loss2, loss3, loss4] 을 계산하여 각 gradient을 계산합니다. 이렇게 계산한 각 gradient은 다시 각자의 장치로보내 백워드를 하고, 다시 집계합니다.

def data_parallel(module, input, device_ids, output_device=None):
    if not device_ids:
        return module(input)

    if output_device is None:
        output_device = device_ids[0]

    replicas = nn.parallel.replicate(module, device_ids)
    inputs = nn.parallel.scatter(input, device_ids)
    replicas = replicas[:len(inputs)]
    outputs = nn.parallel.parallel_apply(replicas, inputs)
    return nn.parallel.gather(outputs, output_device)
  • `nn.parallel.replicate`: pytorch model을 각 장치에 복사합니다.
  • `nn.parallel.scatter`: pytorch 장치를 첫차원(=배치)에 대해서 복제합니다.
  • `nn.parallel.parallel_apply`: 복제된 모델에 입력값을 포워딩합니다.
  • `nn.parallel.gather`: output결과값을 `output_device`에 모아와 concat합니다.

 

아래와 같이 DP은 `torch.DataParallel(model)`의 원라인만 추가하면 사용할 수 있던 기술입니다 [1, 2]. 이 클레스는 데이터 병렬(DP)은 모듈레벨(torch.nn.module) 에서 손 쉽게 사용할 수 있도록 구현한 것입니다. 일단 데이터를 작은 배치사이즈로 나눈 후(split), 데이모델을 각 장치(GPU)에 복제한 후, 데이터를 fowarding합니다. 이후, 각 복제된 모델로부터 백워드(backpropagation)을 진행하고, 원본(original module, 첫 번째 장치에 있던)로 모듈로 가져와 집계합니다[3]. 

CLASS torch.nn.DataParallel(module, device_ids=None, output_device=None, dim=0)
  • module: pytorch model을 의미
  • device_ids:cuda devices (디폴트가 all devices)
  • output_devices: 복제한 모델로부터 집계할 장치를 의미합니다. (디폴트: device_ids=[0])

그렇기에, GPU수보다 배치사이즈가 커야합니다. 

import torch

model = Model(input_size, output_size)
if torch.cuda.device_count() > 1:
  print("Let's use", torch.cuda.device_count(), "GPUs!")
  # dim = 0 [30, xxx] -> [10, ...], [10, ...], [10, ...] on 3 GPUs
  model = nn.DataParallel(model)

model.to(device)

 

하지만, 이 데이터클레는 파이토치에서 권장하지 않는 클레스가 되었습니다. Single node multiple GPU인 경우에도 권장하지 않습니다. 대신. `DistributedDataParallel`을 권장합니다[4]. 이유는 명확히 나와있지 않지만,

  1. 데이터 핸들링의 요구사항이 정확히 충족되지않으면 병렬처리(multiprocessing)을 이용하는 CUDA model에서 경고가 있을 수 있거나, 부정확할 수 있어서 권장하지 않는 듯
  2. DataParalell은 Multithreading을 사용하기에 GIL이 걸리기 때문입니다.

그렇기에`DistributedDataParallel` 을 권장합니다. 이 클레스 내부적으로 멀티프로세싱을 GPU마다 하기때문입니다. 

또한, DataParallel을 사용하는 경우, 원본 모듈(pytorch model)의 key-value 형식으로 저장된 state_dict(=파라미터)의 key값이 달라지기 때문에, 주의를 요할 필요가 있습니다[5]. 위와 같이 `model = nn.DataParallel(model)`로 엮는 경우, 모델의 key값이 달라지기 때문입니다. 원래는 { 'conv1.weight': tensor(...), 'conv1.bias': tensor(...), 'fc.weight': tensor(...), 'fc.bias': tensor(...) }로 저장되던 { 'module.model1.conv1.weight': tensor(...), 'module.model1.conv1.bias': tensor(...),  로 저장되게됩니다.

그렇기에, 나중에 불러올 모델과 DP로 저장한 모델의 파라미터가 맞지않다는 github issue도 종종 볼 수 있습니다.

 

Distributed DataParallel (DDP)

DDP은 데이터 병렬화를 모듈레벨(모델레벨)에서 쉽게 구현한 것이고, 노드수(=머신수)가 여럿일 때도 사용할 수 있습니다. DPP은 병렬처리시에, multiprocessing으로 프로세스를 spawn(부모프로세스의 메모리를 복제하지 않는, 새롭게 프로세스를 실행시키는) 방식으로 진행합니다(spawn vs fork 설명)

  병렬처리방식 노드수 속도
DP Multithread (GIL발생) 싱글 노드 느림
DDP Multiprocessing 싱글 / 멀티 노드 빠름

 

파이토치 세팅은 내부통신이 여러방법이 가능한데, 별도의 설치는 필요없고 파이토치 패키지 내에 들어가있습니다(`torch.distributed`). 가장기본적으로 아래와 같이 할 수 있습니다.

싱글머신에서는 아래와 같이 진행합니다.

"""run.py:"""
#!/usr/bin/env python
import os
import torch
import torch.distributed as dist
import torch.multiprocessing as mp

def run(rank, size):
    """ Distributed function to be implemented later. """
    pass

def init_process(rank, size, fn, backend='gloo'):
    """ Initialize the distributed environment. """
    os.environ['MASTER_ADDR'] = '127.0.0.1'
    os.environ['MASTER_PORT'] = '29500'
    dist.init_process_group(backend, rank=rank, world_size=size)
    fn(rank, size)


if __name__ == "__main__":
    size = 2
    processes = []
    mp.set_start_method("spawn")
    for rank in range(size):
        p = mp.Process(target=init_process, args=(rank, size, run))
        p.start()
        processes.append(p)

    for p in processes:
        p.join()

 

  • `init_process`: 마스터노드와 통신하기위해서 마스터노드의 호스트IP와, PORT을 지정합니다. backend은 어떻게 통신할 것인가에 대한 백엔드를 의미하는 것이며 `gloo`, `NCCL`, `MPI`, `Filesystem`, TCP`와 같은 백앤드가 가능합니다. 위 예시에서는 싱글 노드의 예시이므로 자기 자신 `127.0.0.1`로 통신하도록 되어있습니다.
  • `dist.init_process_group`: 프로세스 그룹을 초기화합니다. 인자 중 `rank`은 프로세스의 순위를 나타내며, 스폰되는 프로세스의 수만큼 0부터 N까지 랭크를 갖습니다. `world_size`은 스폰되는 전체 프로세스의 총 수를 의미합니다. 아래의 그림은 멀티프로세스 4개를 띄운 상태며, 0-3을 포함한 각각의 랭크를 지닌 프로세스입니다.

 

두 개의 프로세스를 띄워서 DDP로 병렬처리하는 코드는 아래와 같습니다.

import torch
import torch.distributed as dist
import torch.multiprocessing as mp
import torch.nn as nn
import torch.optim as optim
import os
from torch.nn.parallel import DistributedDataParallel as DDP


def example(rank, world_size):
    # create default process group
    dist.init_process_group("gloo", rank=rank, world_size=world_size)
    # create local model
    model = nn.Linear(10, 10).to(rank)
    # construct DDP model
    ddp_model = DDP(model, device_ids=[rank])
    # define loss function and optimizer
    loss_fn = nn.MSELoss()
    optimizer = optim.SGD(ddp_model.parameters(), lr=0.001)

    # forward pass
    outputs = ddp_model(torch.randn(20, 10).to(rank))
    labels = torch.randn(20, 10).to(rank)
    # backward pass
    loss_fn(outputs, labels).backward()
    # update parameters
    optimizer.step()

def main():
    world_size = 2
    mp.spawn(example,
        args=(world_size,),
        nprocs=world_size,
        join=True)

if __name__=="__main__":
    # Environment variables which need to be
    # set when using c10d's default "env"
    # initialization mode.
    os.environ["MASTER_ADDR"] = "localhost"
    os.environ["MASTER_PORT"] = "29500"
    main()

 

Distributed Data-Parallel with multi-mode

멀티 노드로 분산처리하기위해서는 파이토치 유틸리치 중하나인 `torchrun`을 이용해야합니다.

  • torchrun을 이용하면 `run`, `world_size`을 명시적으로 전달할 필요도 없고 환경변수도 알아서 세팅해줍니다.
  • DDP을 이용할 때 사용하는 `torch.multiprocessing.spawn`도 사용할 필요없습니다.

 

각 머신에서 돌려야하는 소스코드는 아래와 같다고 생각해보겠습니다.

import torch
import torch.nn.functional as F
from torch.utils.data import Dataset, DataLoader
from datautils import MyTrainDataset

import torch.multiprocessing as mp
from torch.utils.data.distributed import DistributedSampler
from torch.nn.parallel import DistributedDataParallel as DDP
from torch.distributed import init_process_group, destroy_process_group
import os


def ddp_setup():
    init_process_group(backend="nccl")
    torch.cuda.set_device(int(os.environ["LOCAL_RANK"]))

class Trainer:
    def __init__(
        self,
        model: torch.nn.Module,
        train_data: DataLoader,
        optimizer: torch.optim.Optimizer,
        save_every: int,
        snapshot_path: str,
    ) -> None:
        self.local_rank = int(os.environ["LOCAL_RANK"])  # 머신 내 프로세스들의 랭크
        self.global_rank = int(os.environ["RANK"])       # 전체 머신에서의 각 프로세스의 랭크
        self.model = model.to(self.local_rank)
        self.train_data = train_data
        self.optimizer = optimizer
        self.save_every = save_every
        self.epochs_run = 0
        self.snapshot_path = snapshot_path
        if os.path.exists(snapshot_path):
            print("Loading snapshot")
            self._load_snapshot(snapshot_path)

        self.model = DDP(self.model, device_ids=[self.local_rank])  # DDP로 감싸줍니다.

    def _load_snapshot(self, snapshot_path):
        loc = f"cuda:{self.local_rank}"
        snapshot = torch.load(snapshot_path, map_location=loc)
        self.model.load_state_dict(snapshot["MODEL_STATE"])
        self.epochs_run = snapshot["EPOCHS_RUN"]
        print(f"Resuming training from snapshot at Epoch {self.epochs_run}")

    def _run_batch(self, source, targets):
        self.optimizer.zero_grad()
        output = self.model(source)
        loss = F.cross_entropy(output, targets)
        loss.backward()
        self.optimizer.step()

    def _run_epoch(self, epoch):
        b_sz = len(next(iter(self.train_data))[0])
        print(f"[GPU{self.global_rank}] Epoch {epoch} | Batchsize: {b_sz} | Steps: {len(self.train_data)}")
        self.train_data.sampler.set_epoch(epoch)
        for source, targets in self.train_data:
            source = source.to(self.local_rank)
            targets = targets.to(self.local_rank)
            self._run_batch(source, targets)

    def _save_snapshot(self, epoch):
        snapshot = {
            "MODEL_STATE": self.model.module.state_dict(),
            "EPOCHS_RUN": epoch,
        }
        torch.save(snapshot, self.snapshot_path)
        print(f"Epoch {epoch} | Training snapshot saved at {self.snapshot_path}")

    def train(self, max_epochs: int):
        for epoch in range(self.epochs_run, max_epochs):
            self._run_epoch(epoch)
            if self.local_rank == 0 and epoch % self.save_every == 0:
                self._save_snapshot(epoch)


def load_train_objs():
    train_set = MyTrainDataset(2048)  # load your dataset
    model = torch.nn.Linear(20, 1)  # load your model
    optimizer = torch.optim.SGD(model.parameters(), lr=1e-3)
    return train_set, model, optimizer


def prepare_dataloader(dataset: Dataset, batch_size: int):
    return DataLoader(
        dataset,
        batch_size=batch_size,
        pin_memory=True,
        shuffle=False,
        sampler=DistributedSampler(dataset)
    )


def main(save_every: int, total_epochs: int, batch_size: int, snapshot_path: str = "snapshot.pt"):
    ddp_setup()
    dataset, model, optimizer = load_train_objs()
    train_data = prepare_dataloader(dataset, batch_size)
    trainer = Trainer(model, train_data, optimizer, save_every, snapshot_path)
    trainer.train(total_epochs)
    destroy_process_group()


if __name__ == "__main__":
    import argparse
    parser = argparse.ArgumentParser(description='simple distributed training job')
    parser.add_argument('total_epochs', type=int, help='Total epochs to train the model')
    parser.add_argument('save_every', type=int, help='How often to save a snapshot')
    parser.add_argument('--batch_size', default=32, type=int, help='Input batch size on each device (default: 32)')
    args = parser.parse_args()
    
    main(args.save_every, args.total_epochs, args.batch_size)

 

multinode training을 할 경우에는 training job을 직접 SLURM이라는 스케쥴러를 이용해서 돌리거나, torchrun을 이용해서 각자 머신에 실행시키거나 해야합니다. 위의 코드는 `torchrun`으로 같은 rendezvous 인자를 전달해서 돌리는 예시입니다.

// node 1
$ torchrun \
--nproc_per_node=1 \
--nnodes=2 \
--node_rank=0 \
--rdzv_id=456 \
--rdzv_backend=c10d \
--rdzv_endpoint=127.0.0.1:29603 \
multinode_torchrun.py 50 10

// node 2
$ torchrun \
--nproc_per_node=1 \
--nnodes=2 \
--node_rank=1 \
--rdzv_id=456 \
--rdzv_backend=c10d \
--rdzv_endpoint=[node 1의 IP]:29603 \
multinode_torchrun.py 50 10

 

아래와 같이 올바르게 학슴됨을 확인할 수 있습니다.

 

 

Load map

노드 수, GPU, 스케쥴러 지원에 따라 데이터병렬화 방법

노드 수 GPU 수 데이터페러렐 방법 스케쥴러지원
Single-machine single GPU DistributedDataParallel NA
Single-machine multiple GPU DistributedDataParallel NA
Multi-machine multiple GPU torchrun   + rendezvous  X
Multi-machine multiple GPU SLURM 이용 SLURM

 

Trouble shooting 1): The IPv6 network addresses of (서버명, 55039) cannot be retrieved (gai error: -3 - Temporary failure in name resolution

- 도메인을 찾지 못하는 문제입니다. `DNS`쪽 문제일 수 있습니다. DNS 세팅을 다시하거나 서버명에 해당하는 내용을 `/etc/hosts`에 추가합니다.

저의 경우 아래와 같이, `etc/hosts`을 추가해서 해결했습니다.

heon@gpusvr04:~/repositories/misc$ cat /etc/hosts
127.0.0.1 localhost
127.0.1.1 gpusvr04
***.***.***.*** gpusvr03

 

반응형

 

요약


Multi-task learning은 연관된 복수의 테스크를 하나의 모델에 학습시키면서, 추가적으로 성능이 올라갈 수 있어 종종 사용됩니다. 이 논문에서는 Multi-task learning시에 복수의 테스크들의 밸런스를 어떻게 주어야하는지, 불확실성(Uncertainity)을 기반으로 방법론을 제시합니다. 이 논문에서는 불확실성을 타나낼 수 있는 학습가능한 파라미터를 제시해서 손실함수에 함께 사용합니다.

 

Multi-task deep learning

아래의 이미지는 한 이미지로부터 서로 다른 3가지의 테스크을 수행하고, 3테스크의 손실함수를 합하여 최적화하는 일반적인 방법론입니다. 멀티테스크러닝(MTL)은 이런 유사한 테스크를 함께 사용하는 경우 하나의 테스크만 사용하는 것보다 더 높은 성능을 보일 수 있습니다.

 

문제는 이 테스크들의 각각의 손실함수 1), 2), 3)이 있을텐데, 이 테스크를 어떻게 가중치를 주어야하는지에 대한 고민입니다. 1:1:1이 최적일까요? 아닐 수 있습니다. 보통 이 가중치(비율)을 휴리스틱하게 여러 번의 실험을 하면서 실험적으로 구하기에, 매우 비용이 많이듭니다. 본 논문은 MTL시에 가중치를 불확실성기반으로 최적화하는 방법을 제안합니다.

 

Methods

MTL은 테스크가 N개면 N개의 손실함수를 보통 갖습니다. 이 논문에서는 2개로 가정합니다. 아래의 (7)에서 표기들은 각각 아래와 같습니다.

  • $\mathcal{L}(W,\sigma_{1},\sigma_{2})$: 최적화해야할 손실함수값입니다. $W,\sigma_{1},\sigma_{2} $각각의 인자에 따라서 값이 변화할 수 있음을 의미합니다.
  • $\mathbf{W}$: 모델의 trainable parameter 입니다
  • $\sigma_{1},\sigma_{2}$: 테스크 1, 테스크2에 대한 불확실성을 나타내는 trainable parameter입니다.
  • $f^{ \mathbf {W}}(x)$: 모델의 output입니다. 

(7)번식의 마지막 식을보면, 불확실성을 타나내는 파라미터가 각 테스크의 가중치로 있고, 마지막에 $log{\sigma_{1}\sigma_{2}}$에도 있습니다. 이는 불확실성($\sigma_{i}$)가 큰 경우 해당, 테스크의 손실함수를 별로 반영하지않기 위함입니다.

예를 들어보겠습니다. 1번 테스크의 불확실성이 큰 경우: $\sigma_{1}$이 커집니다. 따라서,$ \frac{1}{2\sigma_{1}^{2}}\mathcal{L}_{1}( \mathbf {W}) $은 작아집니다. 즉, 1의 불확실성이 큰 경우, 해당 테스크의 손실함수를 작게 반영합니다.

뒷항 $log{\sigma_{1}\sigma_{2}}$은 $ \sigma_{i}$가 무한히 커져 task1, task2의 손실함수($\mathcal{L}_{1}, \mathcal{L}_{2}$의 합이 무한이 작아짐을 방지하기위해서, log를 취하여 무한히 작아짐을 방지합니다.

 

Implementation

Pytorch Implementation이 있어, 이 깃헙의 주요 내용만 살펴보겠습니다.

  • 16번: $\sigma$을 trainiable parameter로 만들기위해서, 몇개의 sigma을 만들지를 인자로 받습니다.
  • 18번: 인자로 전달된 개수만큼 시그마를 1로 초기화합니다(=모든 테스크에 대해서 1로 초기화). gradient True로 최적화가 가능한 학습 가능한 변수로 합니다.
  • 23~23번: 각 손실함수값에 대해서 loss와 trainable parameters을 이용해서 위의 (7)식을 계산합니다.

 

 

Results

손실함수의 값을 어떻게 조정하냐에 따라서, 아래와 같은 Table1의 표를 보여줍니다. unweighted sum of losss에 비해서 모든 테스크의 지표들이 향상됨을 확인할 수 있습니다.

반응형

 

Data-Distortion Guided Self-Distillation for Deep Neural Networks , AAAI 2019

 

배경: Knowledge distillation 

큰 모델의 model confidence (softmax output)은 이 큰 모델이 학습한 지식을 담고있지 않을까라는 가설로 시작됩니다. 큰 모델의 결과물(증류된 정보)을 이용해서 작은 모델을 간단히 학습할 수 있지 않을까라는 생각입니다 [1]. 즉 큰 모델의지식(knowledge)이 model confidences 분포이겠구나라는 것입니다. 

아래의 그림과 같이 Teacher model이 훨씬 큰 딥러닝모델이고, Student모델이 더 작은 모델입니다. 큰 모델의 예측결과(model confidences)자체가 큰 모델의 지식이라는 것입니다. 예를 들어, 고양이가 6번인덱스의 클레스였다면, 0번은 낮고, 1번도 낮고..., 6번이 제일 큰데 0.4정도라는 이 분포자체가 Teacher모델이 갖고있는 큰 파라미터의 지식이 됩니다. 이를 답습하자는것이 Student모델의 학습목표입니다.

Knowledge distillation 방법론

흔히 지식증류(knowledge distillation)방법들은 학습방법에 따라, 3가지로 나뉩니다. 

  1. teacher-to-student: 큰 모델의 지식(logits, feature map,attention map등)을 작은 모델이 더 잘학습시키기 위한 방법론. 서로 다른 모델이 2개가 있어야하며, Teacher model은 학습되어있다고 가정합니다.
  2. student-to-student: 서로 다른 작은 다른 모델(student)을 서로 다른 초기값을 가지게하고, 학습을 동시에 해가면서 지식의 차이를 점점 줄여가나는 방식입니다. teacher-to-student와는 다르게 이미 사전학습된 모델이 필요없는 On-line training 방법입니다.
  3. self-distillation (self-teaching): 하나의 네트워크를 이용하며, 이미지 1개를 데이터 증강(random, rotation)등을 통해서 2개로 만들고, 서로 다른 지식을 일치시키는 방법을 사용합니다. 즉, 하나의 네트워크를 사용하며, 서로 다른이미지를 추론했음에도 분포가 같아야하는 모델 일반화가 더 잘되는 방법으로 학습을 유도합니다.

 

방법: Self-distillation

Self-distillation은 크게 4단계로 이어집니다.

1. Data distortion operation: data augmentation 방법과도 같습니다. 같은 원본데이터로, 서로 다른 두 이미지를 생성합니다.

2. Feature extraction layers: 하나의 모델에 두 증강된 이미지를 통과시켜 특징값(feature)을 얻습니다. 여기서는 두 특징값(벡터)의 차이를 계산합니다. 

MMD은 Maximum Mean Discrepancy을 의미하며, 두 증강된 이미지를 특징으로 만든 벡터의 차이를 의미합니다. 수식으로는 아래와 같습니다.

$MMD_{emp}=||\frac{1}{n}\sum_{i=1}^{n}\mathbf{h}(x_{ai})- \frac{1}{n}\sum_{i=1}^{n}\mathbf{h}(x_{bi})||_{2}^{2}$

$ \mathbf{h} $은 네트워크를 의미합니다. 즉 $ \mathbf{h} (x_{ai}), \mathbf{h} (x_{bi})$ 은 서로 다른 이미지를 하나의 네트워크에 태워 얻은 벡터를 의미합니다. 이 벡터의 차이를 계산하는 것이 MMD입니다. 

3. Classifier: classifier레이어에 통과시킵니다.

4. Predictor: 사후확률($\mathbf{p}(y|\mathbf{x}_{i})$)을 이용해서 2가지를 계산합니다. 하나는 분포의차이(KL divergence), 또 하나는 classification loss (cross-entropy loss)입니다.

  • KL divergence은 방향이 없기에 양방향으로 $KL(\mathbf{p}_{a}, \mathbf{p}_{b}), KL(\mathbf{p}_{b}, \mathbf{p}_{a})$을 둘 다 계산합니다. 
  • Classification loss은 두 이미지에 대해서 라벨을 이용해서 classification을 계산합니다.

KL diveregnce loss 2개, classification loss2개, MMD loss을 합산, 총 5개의 손실함수를 합산하여 모델을 학습시킵니다.

Results

첫 번째 결과는 CIFAR-100에서의 분류성능이고 동일한 파라미터수에서는 가장 낮은 테스트에러를 보였습니다.

두 번째, 결과는 모델의 크기에따라, self-distillation을 할때 어느정도 큰 차이가 나는지를 확인해봤습니다. 작은모델에서 distillation할때 성능이 크게 향상되는 것을 볼 수 있습니다. 즉, 이 방법론을 사용할거면 작은모델을 사용하는 접근이 유리해보입니다.

세 번째, 결과는 다양한 네트워크를 이용한 종합적인 성능인데, 모두 SD(Self-distilation)을 이용하는 경우 더 좋은 성능을 보였습니다.

네 번째 결과로, 데이터가 매우 큰 경우에서도 잘 동작하는지를 봤는데, 데이터가 매우 큰 경우에는 성능향상이 있었는데, 그렇게 큰 차이는 아닌 것 같내요.

Self-supervised learning vs self-distillation

두 방법론은 1)augmetentation이 둘 다 사용되고, 2) 하나의 네트워크를 학습한다는 것에 공통점이 있습니다.

하지만, SSL(Self-supervised learning)은 라벨이 필요없는 데이터로 학습한다는 것에 큰 차이가 있습니다.

SD(Self-distillation)은 총 5가지의 손실함수를 사용하는것중에 2개의 손실함수는 classification loss가 사용되니 라벨이 반드시 필요한 방법론이 됩니다.

 

References

[1] https://arxiv.org/abs/1503.02531

반응형

본 포스팅은 naver d2 이활석님의 발표자로를 글과 이해가 쉽도록 만든 Figure들로 재구성했음을 알립니다.

 

요약


  1. MSE로 최적화하는 것과 MLE로 최적화 하는 것은 동치이다. 
  2. sigmoid을 출력값으로 하는 경우 MSE로 최적화하는 것보다 CE로 하는 경우, 학습이 더 빨리 된다. 이는 activation function(sigmoid)의 도함수를 한 번 계산하지 않아도 되기 때문이다.

1. Backpropgation (역전파) 과정에 대한 가정(Assumption)


역전파알고리즘을 이용하기 위해서는 2가지 가정이 일단 필요하다 [1].

  1. Average $E = 1/n \sum_{x}E_{x}$: $n$개의 training 데이터에 해당하는 각각의 $x$에 대한 손실함수의 합의 평균은 손실함수 전체와 같다. 이는 각 훈련데이터를 각각 구한것의 그레디언트를 전체 오차함수에 대해서 일반화을 위하기 때문이다.
  2. 신경망의 출력값($\hat{y}$)에 대해서 손실함수를 표현할 수 있어야 한다는 것이다.

 

직관적으로 생각해보면 아래와 같다.

첫 번째로, MSE로 종류의 $E$(error)을 설정했다고 하자. 그리고, 이 오차함수는 예측값($y'$)과 실측값($y$)의 이를 구하게 된다. 그러면 다음과 같이 표기할 수 있다. 가령, $E(y, y')= (y-y')^{2}$가 라고 하자. 이는 일단 두번째 조건을 만족한다. 왜냐하면 $y'$은 신경망의 두번째 출력값으로 표현되었기 떄문이다. 그리고, 이러한 $y'$을 출력하기위한 입력값 $x$가 있을 것이다. 이 $x$가 $n$개가 있다고하면 각각의 $n$개의 오차의 합은 전체 $E$가 될 수 있다는 것이다. 즉, 다음과 같은 식으로 표현할 수 있다. $E=1/n \sum_{x} (y-y')^{2}$. 이렇게되면 1번 가정도 만족한다. 

 

위의 가정을 만족할 수 있으면, 이제 손실함수를 최소화시키는 최적의 파라미터($\theta$)을 찾으면된다. 보통 우리는 아래와 같이 표현한다.

$\theta^{*} = argmin_{\theta \in \Theta} L(f_{\theta}(x), y)$

위 식을 친절하게 해석해보면 다음과 같다.

  1. $\theta^{*}$: 우리가 원하는 최적의 파라미터를 의미한다. 
  2. $L(f_{\theta}(x), y)$: $\theta$가 어떤 것이 지정되냐에 따르는 손실함수($y$와 예측치 $f_{\theta}(x)$의 차이)를 의미한다.
  3. $argmin_{\theta \in \Theta} L$ : $\theta$가 될수 있는 모든 수 중에 $L$을 가장 작게만드는 $\theta$을 의미한다.

 

 

2. Gradient descent (경사하강법): 경사하강법으로 최적의 파라미터를 찾아간다.


위의 $\theta^{*} = argmin_{\theta \in \Theta} L(f_{\theta}(x), y)$식의 최적의 해를 찾는 방법으로 경사하강법(Gradient descent)가 이용된다. 이는 Iteration으로, 같은 로직을 여러번 반복하는 것을 의미한다. 식이 회귀방정식이라면 close form solution으로 직접 전개해서 풀면되지만, 활성화 함수랑 이것저것 엮여있는 DNN은 open form solution이어서 직접 추정해서 구해야한다.

 

경사하강법으로 최적의 파라미터($\theta$)을 구할 때, 고려사항과 각각의 방법은 아래의 표처럼 정리할 수 있다.

고려사항 방법
어느정도만큼 파라미터를 업데이트 할 것인가?
(= $\theta -> \theta + \Delta\theta$)
새로운 $\theta + \Delta\theta$로 손실을 측정할 때가 더 작을 때
(= $L(\theta + \Delta\theta) < L(\theta)$)
언제 멈출 것인가? 더 이상 업데이트가 변화하지 않을 때
$L(\theta + \Delta\theta) < L(\theta)$
어떻게 업데이트할 만큼($\Delta\theta$)을 찾을 것인가? $\Delta\theta = -\mu \nabla\theta$ where $\mu > 0 $

 

그림1. 이미지 출처: https://builtin.com/data-science/gradient-descent

 

위의 전략대로 아래와 같이, 일단 새로운 파라미터에 대해서 손실함수를 구하고자한다. 헷갈리지 말아야할 것은 $x$에 대해서 구하는게 아니라, 최적의 $\theta$을 찾는 것이다. 위의 그림과 같이 x1, x2축에 해당하는 w, b에따라서 손실함수의 크기 $J(w, b)$가 달라지는 것이다. 즉, $\theta$가 우리의 목표이다.

전체 데이터 포인트 X가 있으면, 아래와 같이 풀이가 가능하다.

  • $L(\theta_k, X) = \sum_{i}{L(\theta_{k}, x_{i}})$: 각 데이터포인트($x_{i}$)의 합은 전체와 같다.
  • $\nabla L(\theta_k, X) = \sum_{i}{\nabla L(\theta_{k}, x_{i}})$: 위의 식을 양변에 $\theta$에 대해서 미분한다. 
  • $\nabla L(\theta_k, X) \triangleq \sum_{i}{\nabla L(\theta_{k}, x_{i}})/ N$: 최적화만 구하면되니까, 이 미분값이 N으로 나눠도 된다. 
  • $\nabla L(\theta_k, X) \triangleq \sum_{j}{\nabla L(\theta_{k}, x_{j}})/M, ~ where ~M< N$: 전체 훈련데이터에 해당하는 데이터포인트를 다넣어서 N개의 연산을 해도되지만,너무 연산이 많이드니, 적당히 작은 M으로 나눠서 해를 찾을 수 있다. 이 때 랜덤으로 M개의 데이터 j개를 뽑는다.

즉, 결국에 X개 계산한다음에 업데이트하고싶지만, 계산이 많이 드니 M개만큼 쪼개서 업데이해볼 수 있다는 것이다.수식으로는 $\nabla L(\theta_k, X)$ 만큼 움직이는 것 대신에 $\sum_{j}{\nabla L(\theta_{k}, x_{j}})/M$ 만큼 움직여서 업데이트할 수 있다는 것이다.

그리고, 아래와 같이 다음(k+1)의 파라미터틀 업데이트한다. 우리는 이 k를 iteration, M개를 넣는 작업을 step이라고한다. tensorflow에는 iteration은 epoch, M개의 미니배치로 계산하는 것은 step이라는 표현으로 사용한다.

$\theta_{k+1} = \theta_{k} - \mu \nabla L(\theta_{k},X)$

 

 

 

3. Backpropgation에 따라서 각 레이어의 파라미터를 업데이트


위에서 구한 것과 같이 손실함수(loss function)은 $L(\theta_{k},X)$라고 했다. 헷갈리지 말아야할 것이, 파라미터($\theta$)에 해당하는 것은 각각의 레이어의 $w, b$을 의미한다. 2개의 표현식을 통틀어 크게는 $\theta$라고 표현한다. 

파라미터 업데이트는 $\theta_{k+1} = \theta_{k}-\mu\nabla L(\theta_{k}, X)$ 가 목표다. 이를 각각 w와 b에 대해서 풀어쓰면 다음과 같다.

  • $w_{k+1}^{l} = w_{k}^{l} - \mu\nabla L_{w_{k}^l}(\theta_{k}, X)$ : $k$번째 업데이트 할 때, $l$번째 레이어의 $w$에 대해서 미분이 필요하다
  • $b_{k+1}^{l} = w_{k}^{l} - \mu\nabla L_{b_{k}^l}(\theta_{k}, X)$: $k$번째 업데이트 할 때, $l$번째 레이어의 $b$에 대해서 미분이 필요하다

위의 두 식을 보면 각 레이어 $l$에 대해서 매 업데이트시(k)마다 미분이 필요하다. 이건 너무 계산이 많이들어 딥러닝이 못했던 허들이기도 하다.

 

그렇기에 이를 해결하고자 했던, 역전파로 해결한다 [2]. 이 계산을 알기위해서는 forward방향에서의 오차를 먼저 알아야한다. 그림4처럼 L번쨰 레이어의 i번째에 원래 뉴런이 하는 작업을 $\sigma$ (활성화함수아님)라 하자. 그러면, $z_{j}^{l}$이 입력이 오던걸, $\nabla z_{j}^{l}$만큼 변경하면, 출력값은 $\sigma(z_{j}^{l} + \nabla z_{j}^{l})$로 바뀐다. 그러면 최종적으로 전체 손실함수값은 $\frac{\partial C}{\partial z^l_j} \Delta z^l_j$만큼 바뀐다고 알려져있다.

그림 4.

이제 역전파로 바꿔생각해보자. 이를 구하기위한 각 notation은 아래 와 같다. 그리고, 레이어의 개수가 $l$개라고하자(=$l$ 레이어가 마지막레이어).

  • $\delta_{j}^{l}$: $j$번째 뉴런(유닛)의 $l$번째의 레이어의 변화량. 쉽게 표현하면, j번째 뉴런이 조금변경되면, 오차가 얼마나 변화하는지를 의미한다. 상세히는 w, b로 표현하면 $\partial C / \partial w^l_{jk}$, $\partial C / \partial b^l_j$도 된다. $\delta_{j}^{l}$을 활성화 함수가 포함된 합성함수라고 생각하면 다음과 같이 미분된다 [3]. $\delta^L_j = \frac{\partial C}{\partial a^L_j} \sigma'(z^L_j).$.
  • $a_{j}^{L}$: $l$레이어에 존재하는  $j$번재 아웃풋
  • $C$: cost function을 의미. 예를 들어 $C=1/2 \sum _{j}(y_{i}-a_{j}^{L})^{2}$
  • $z^{L}$: L번째 레이어에서 출력한 결과

 

다음과 같이 역전파를 진행한다.

순서 수식 의미
1 $\delta^L = \nabla_a C \odot \sigma'(z^L)$ l번째 레이어의 출력값이 변하면, 손실함수가 얼마나 변할까?
그리고 그 계수에 해당하는 activation()만큼 변하겠지?

$\nabla_a C$은 예측값과 출값의 차이니까 이를 대입한다. (단순히 MSE라고 생각)
가령, $\delta^L = (a^L-y) \odot \sigma'(z^L).$ 을 구할 수 있다.
2 $\delta^l = ((w^{l+1})^T \delta^{l+1}) \odot \sigma'(z^l)$ $l$번째 레이어와 $l+1$번째 레이어에서의 관계식을 의미한다. 

일단 다음 레이어인 $l+1$번째의 오차를 먼저알기 때문에, 이때 계산에 쓰였던 파라미터 $w^{l+1}$을 전치한다음에 곱하고, 활성화함수만 곱해주면 이전 연산의 에러를 구할 수 있다는 것이다.

3 $\frac{\partial C}{\partial b^l_j} = \delta^l_j.$ 1,2을 이용하여 각 b에 대해서 편미분을 구할 수 있다.
4 $\frac{\partial C}{\partial w^l_{jk}} = a^{l-1}_k \delta^l_j.$ 1,2을 이용하여 각 w에 대해서 편미분을 구할 수 있다.

 

 

 

 

MSE와 CrossEntorpy 비교: 왜 굳이 이진분류를 할 때 Xentopry을 사용할까?


 

TLTR: CrossEntropy을 이용할 수 있으면, gradient vanishing에서 조금이나마 MSE보다는 이득이기 떄문이다.

 

MSE와 CE을 이용하는 각각의 모델에서, 우리 모델의 마지막 레이어가 sigmoid로 활성화 함수를 썼다고 가정하자. MSE은 출력레이어에서의 오차를 $\delta^L = \nabla_a C \odot \sigma'(z^L)$처럼 계산한다고 했다. 자세히보면 오차를 계산할 때, 오차를 계산하자마자 $\sigma'(z^L)$을 한 번 한다. C은 간단히 y-a(마지막 아웃풋과의 차이)이라고하자. 그러면 MSE을 계산하려면 $\delta^L = (a-y) \sigma'(z^L)$.가 된다.

반대로 CE을 이용하자고하자 CE의 loss은 $C=-[ylna+(1-y)ln(1-a)]$이다. 이걸 a에 대해서 미분하면 다음과 같다.

  1. $\nabla_{a}C = \frac {1-y} {(1-a)a}$.
  2. $\sigma'(z^L)$도 미분하면, $\sigma'(x)=\frac{d}{dx}\sigma(x)=\sigma(x)(1-\sigma(x))$을 따르므로, $a(1-a)$가 된다.
  3. $\delta^L = \nabla_a C \odot \sigma'(z^L)$ 라고 했기 때문에,
  4. $\delta^L = \frac {1-y} {(1-a)a} a(1-a) = (a-y)$ 이므로,  $\sigma'(z^L)$가 상쇄된다. 

정리하면, MSE을 이용할경우 backpropagation에서 gradient decay가 한 번 발생하고 역전파하기 때문에, 학습이 늦게되는 반면, CE을 이용한 경우 gradient decay가 한번 없이 역전파해서 상대적으로 학습이 빠르게 된다

그림3. sigmoid function의 원함수와 미분된 함수의 값

 

 

 

 

MSE의 최적화와 Likelihood을 이용한 최적화의 비교


MLE(Maximum likelihood estimation)은 관측한 데이터(x)을 가장 잘 설명하도록 파라미터($\theta$)을 찾는 과정이라고 생각하면 쉽다. 조금 바꾸어 말하면, 모델(f)내에 파라미터가 출력(y)을 가장 잘 설하도록 모델의 파라미터($\theta$)을 찾는 과정이랑 같다. 우리가 모델에서부터 얻는 예측값($f_{\theta}(x))$( =$\hat(y)$ 이기에...)가 정규분포를 따른다면, 평균적으로 실측값(y)을 얻게끔 파라미터를 얻는 과정이다. 따라서 아래와 같이 수식으로 표현할 수 있다.

$\theta^{*} = argmin_{\theta}[-log(p(y|f_{\theta}(x))]$

위의 식이 (-)Negative와 log가 붙은 negative log likelihood 이다. 의미는 모델의 반환값이 어떤 분포를 따른다면, 따른다면, 이 분포의 파라미터를 잘조절에서 y가 잘 나오게해주는 $\theta$을 찾아주세요. 라는 의미이다. 

 

위를 풀기위해 함수를 최적화하기위해 IID 을 가정한다

  • Independent : $p(y|f_{\theta}(x) = \Pi_{i}P_{D_{i}}(y|f_{\theta}(x_{i}))$: 트레이닝 데이터 DB ($D_{i}$)을 최적화하는 것에 대한 모든 곱은 전체와 같다. (독립사건이기에 서로 곱할 수 있다)
  • Identical distribution: $p(y|f_{\theta}(x) = \Pi_{i}p(y|f_{\theta}(x_{i}))$: training데이터는 매번 다른 분포를 같는 것이 아니라, $x1$이 정규분포라면 $x_{2},...x_{i}$가 모두 정규분포이다.

이를 이용하여 다음과 같이 negative loglikelhood을 정리할 수 있다

=> $-log(p(y|f_{\theta}(x))) = -\sum_{i}log(p(y_{i}|f_{\theta}(x_{i})))$: (IID을 이용). 각각의 확률값의 negative log likelhood을 다 더하면 전체의 training DB의 negative loglikelihood와 같다

위의 분포가 정규분포를 따른다면, 정규분포의 Likelihood을 이용해서 파라미터를 찾을 수 있다. 정규분포의 log-likelihood값은 위의 그림2와 같다. x은 관찰값 데이터포인트, $\mu$은 우리가 구하고자하는 파라미터 중에 평균에 해당한다. 이 식을 우리의 문제로 바꿔보자. x가 관찰값이었다면, 우리문제에서는 모델의 매번반환하는 $f(x)$의 평균값이 $\mu$가 되고자하는 것이다. 즉, $f_{\theta}(x_{i}) =\mu_{i}, ~\sigma_{i}=1$ 인 것을 원한다.

그림 2. log-likelihood function .&nbsp;https://www.statlect.com/fundamentals-of-statistics/normal-distribution-maximum-likelihood

$= -\frac{n}{2}ln(2\pi)-\frac{n}{2}(\sigma^{2}) - \frac{1}{2\sigma^{2}}\sum_{j=1}(x_{j}-\mu)^{2}$ 을 

$= -\frac{n}{2}ln(2\pi)-\frac{n}{2}(\sigma^{2}) - \frac{1}{2\sigma^{2}}\sum_{j=1}(f_{\theta}(x) - y_{i})^{2}$: 이렇게 바꿀 수 있다.

위의 식을보면 $y_{i}$와 $f_{\theta}(x_{i})$의 차이의 제곱. 즉 MSE와 같다.

 

반면의 베르누이 분포(=확률(p)에 따라, 결과가 0,1로 나오는 분포)를 따른다면 아래와 같다.

$p(y_{i}|p_{i})=p_{i}^{y_{i}}(1-p_{i})^{1-y_{i}}$

<=> $log(p(y_{i}|p_{i})) = y_{i}logp_{i} + (1-y_{i})log(1-p_{i})$
<=> $-log(p(y_{i}|p_{i})) = -[y_{i}logp_{i} + (1-y_{i})log(1-p_{i})]$

 

 

Reference


[1]  Nielsen (2015), "[W]hat assumptions do we need to make about our cost function ... in order that backpropagation can be applied? The first assumption we need is that the cost function can be written as an average ... over cost functions ... for individual training examples ... The second assumption we make about the cost is that it can be written as a function of the outputs from the neural network ..."

[2] http://neuralnetworksanddeeplearning.com/chap2.html

 

Neural networks and deep learning

In the last chapter we saw how neural networks can learn their weights and biases using the gradient descent algorithm. There was, however, a gap in our explanation: we didn't discuss how to compute the gradient of the cost function. That's quite a gap! In

neuralnetworksanddeeplearning.com

[3] https://j1w2k3.tistory.com/1100

반응형

 

요약


요약: Supervised learning의 일반적인 개념으로, Supervsed learning에서는 분류하고자할 instance가 정확히 True/False 등의 라벨이 필요한데,Multiple instance learning에서는 Instance까지는 True/False 까지는 안되어있어도, Instance의 묶음(Bag) 내에 True가 있을까? False일까를 맞추는문제. 주로 탐색해야할 영역들이 넓은 Pathological image, Document classification에 사용된다. 

 

Fig 1. MIL과 Single instance learning 과의 차이(이미지: Multiple instance learning for histopathological breast cancer image classification)

 

 

 

 

입/출력에 대한 상세: 입력값의 형태, 출력값의 형태, 문제는 어떤형식인가?


구체적으로는 각각의 instance까지의 라벨은 안되어도, instance을 포함하는 bag들의 라벨만 있으면 되는 형식이다 [1]. 보통 이걸 Weakly labling되었다고 한다

수식으로 보면 아래와 같다 [2].

  • $B_{n}$: Whole Slide Image(WSI)을 하나의 Bag이라고 가정한다 
  • $B_{n} = (x_{1}, x_{2}, x_{3}, x_{4},...,x_{n_{i}}), \omega_{n}$ : n번째 WSI을 하나의 Bag이라고 생각하면, 이 WSI내에는 각각의 패치를 $x_{n}$이라고 할 수 있다. 이 패치는 WSI이미지 마다 개수가 다를 수 있으므로 $x_{n_{i}}$ 라고 서브스크립트를 달아 놓았다.
  • $\omega_{n}$: 이러한 n번째 WSI는 각각의 라벨이 존재한다. 예) 악성종양/양성종양 
  • $p(\omega|B_{n})$: 전체 WSI내에 여러 이미지를 하나의 bag으로 주었을 때, 라벨을 맞추는 문제.
  • $p(\omega|B_{n})= E[p(\omega|x_{i})] \forall x_{i} \in B_{n}$: 어떤 인스턴스(x)가 들어와도 라벨의 잘 맞출 확률
  • $\omega = max_{k}\left \{ y_{n_{i}} \right \}$: bag내에 $n_{i}$개 중, 모든라벨이 0이어야 bag의 라벨이 0, 하나라도 1일경우에는 1을 의미한다.

VGG을 이용한 MIL모델의 예시. 이미지 소스: MULTIPLE INSTANCE LEARNING OF DEEP CONVOLUTIONAL NEURAL NETWORKS FOR BREAST HISTOPATHOLOGY WHOLE SLIDE CLASSIFICATION

 

실제 구현 예시


tensorflow로 구현

 

 

 

언제쓰면 적절한가?


  1. 각 인스턴스의 라벨을 직접 다는 것이 너무 어렵지만, bag의 라벨은 달 수 있을 때(Weakly labled)
  2. 큰 이미지를 다룰 때,
  3. 다운샘플링(Donwsampling)이 불가능할 때

 

 

MIL 사례 연구들 


- 사례 1: Document에서의 사례 [1]


- 문제: 문서가 이미지로 들어올 때, 관련 문서인지 파악하는 문제
- Supervised로 한다면?: 문서에는 로고, 시그니처, 그림, 테이블 등이 포함되어서 global descriptor로 풀기가어려움.
따라서, 이미지를 각각의 여러 zone으로나눈후, 각각의 zone에 대한 분류로 나누고자함. 분류기가 Document = [zone1, zone,2 zone3, ... zone N]라고 생각해서 각각의 zone 중에 한 영역만 True여도 관련분야다라고
학습시키려고 한다는 것이다. 그럴려면 사실 각 zone에 대해서 많은 label이 필요한데 비용이 많이든다.

 

 

- 사례 2: Histopahtological Image (조직병리학적 이미지) 슬라이드에서의 암예측

Camelyon7 Challenge (breast ca): 병리이미지를보고 환자가 전이성암이 될 것인가 말것인가를 푸는문제, PANDA challenge (prostate ca): prostate ca의 점수를 계량할 수 있는 점수(Gleason score)을 예측하는 것

 

 

 

- 사례3: Classifying and segmenting microscopy images with deep multiple-instance learning [3, 4]

instance-based MIL형태에 속하는 연구이다. 각 instance에 대해서 분류기를 통과시키고, 이를 다시 aggregation하는 형태로 동작함. aggregation하는 함수를 global pooling layer라고하는데, 이 연구에서는 Noisy-and pooling function이라는 것을 만들었다. 가정은 Bag이 Positive이면, Bag내의 인스턴스가 Positive인 것들이 일정 threshold을 넘길것이라는 가정을 둔다. 

  1. $p(t_{i}=1 | x_{j})$: Bag내에 인스턴스 $x_{j}$을 인자로 받았을 때, $t_{i}$클레스일 확률을 구하는 분류기
  2. $g(\cdot)$ (global pooling function): noisty-OR 이라고 함. 집계한 확률을 내어주는 함수. $p(t_{i}=1 | x_{1},...x_{N})$. 좀 다르게 표현하면, 각 인스턴스의 확률을 집계하여, Bag이 i라벨에 속할 확률을 구해주는것. $P_{i} = g(p_{i1}, p_{i2}, ...P_{iN}):
  3. Noisy-AND: $P_{i}=g_{i}(p_{ij}) = \frac{\sigma(a_{p_{ij}-b_{i}}) - \sigma(-ab_{i})}{\sigma(a(1-b_{i})) - \sigma(-ab_{i})}$. 이 식의 $a$은 sigmoid function전에 입력값을 multiple할 때 쓰고, $b_{i}$은 적어도 몇개가 있어야하는지에 대한 threshold로 역할을 한다.

 

$g(\cdot)$(global pooling function)을 어떻게 하냐에 따라 종류가 많은데, 대체적으로 아래와 같이 알려져있음.

https://www.ncbi.nlm.nih.gov/pmc/articles/PMC4908336/pdf/btw252.pdf

 

실험에서는 MNIST dataset은 concat시켜서 만들어, MIL의 분류기를 만드는 시험을 했다. 라벨이 2라면 2가 적어도 b개만큼 있어야한다는 Noisy-AND을 이용하여 학습한다.

 

 

 

Reference



[1] : https://www.academia.edu/download/78345586/Kumar_ICDAR_2011.pdf

[2]: MULTIPLE INSTANCE LEARNING OF DEEP CONVOLUTIONAL NEURAL NETWORKS FOR BREAST HISTOPATHOLOGY WHOLE SLIDE CLASSIFICATION

[3] https://github.com/dancsalo/TensorFlow-MIL

[4] https://www.ncbi.nlm.nih.gov/pmc/articles/PMC4908336/pdf/btw252.pdf

반응형

요약


확률 보정(Probability calibration)은 실제 데이터로 사건이 발생할(=분류할) 이벤트가 확률값처럼나오도록 하는 과정을 의미한다. 가령, 기상청에서 쓰는 강수확률모델이 강수확률이 80%이면, 기상조건이 같은 날짜만 모아서 예측한 경우, 80%만 비가왔어야한다. 이렇듯, 모델이 반환하는 확률값이 (정확히는 확률은 아니지만, 일종의 신뢰도 역할을 한다), 신뢰도 역할을 잘 잘 할 수 있도록 하는 작업을 확률 보정(Probability calibration)이라고 한다.

서론


Sklearn 공식 도큐먼트를보면 1.16에 Probability calibartion에 대해 소개가 되어있다 [1]. ML모델로 얻은 확률(.predict_proba로 얻은 확률)은 우리가 실제로 확률처럼 쓸수 있냐면? 그건 아니다. 이진분류라면, 모델이 반환한 확률값이 0.8이었다면(1=positive case), 실제 positive class중에 80%정도만 모델이 맞추었어야한다. 모델이 반환한 확률이 1이라면? 모델이 1이라고 찍은 데이터들은 100% positive case여야한다.

 

 

켈리브리에이션 커브(Calibation curves): 얼마나 확률이 신뢰도를 잘 대변하고 있는지 파악할 수 있는 다이어그램


아래의 그림을 Calibration curves (=reliabiltiy diagrmas)이라고 한다. 이는 확률 값이 실제로 이진분류상에서 얼마나 신뢰도를 대변할 수 있는지를 보여주는지를 알려주는 그림이다. X축은 모델이 각 평균적으로 내뱉는 확률값(구간화한값인듯), y축은 그 중에 실제 positive가 얼마나 있었는지를 의미한다. 확률이 잘 보정되었다면, 실제 positive case중에 모델이 예측한 케이스의 비율과 동일할 것이다. 즉 이 선은 기울기가 1인 점선처럼 나와야 한다.

각 4가지 모델에 대한 probabiltiy calibaration을 파악하기 위한 비교.

 

위의 그래프를보면, logistic 모델의 경우(녹색) 잘 보정이 되어있고, 다른 방법론들은 그러하지 못하다. GaussianNB은 대다수가 0, 또는 1에 가까운 확률을 내뱉는다. Random Forest의 경우에는 히스토그램이 0.2, 0.9에서 피크치고, Calibration curve에서도 S형을 보인다. 이에 대해서 한 연구자(Niculescu-Mizil and Caruana)가 다음과 같은 설명을 했다. Bagging 또는 random forest의 경우는 확률이 0 이나 1이 나오는 경우는, 유일하게 한가지로, 모든 random forest의 tree(앙상블방법들의 weak learner)가 다 0의 확률을 예측해야 한다. 사실상 조금 어려운 경우이고, 우리가 일부 RF에 노이즈를 주더라도 0에서 멀어질 수밖에 없다[2].

 

 

어떻게 확률를 조정(calibartion)할 수있나? 매핑함수를 하나 더 씌워본다


수식이 직관적이어서 바로 서두에 작성한다.
$p(y_{i}=1|f_{i})$ 을 해줄 후처리 함수하나만 만들면 된다.
국룰이 없다. 단, 모델을 훈련할 때 사용했던, 데이터로는 사용하지 말아야한다. 왜냐하면, 훈련데이터로하면 성능이 워낙 좋기때문에, test 데이터로 찍은 확률과 차이가 있을 수 있기 때문이다. 즉, 본인이 테스트할 데이터를 가지고 예측기에, 각 확률만큼 positive 케이스를 맞추도록 보정하여야한다. 이 보정은 어떤 함수여도 상관이 없다. Sklearn에서는 CalibratiedClassifierCV라는 툴을 제공해서 모델의 확률이 calibration되도록 만들어준다. 대표적으로 쓰이는 방법은 2가지인데, isotonic과 sigmoid 방법이다.


Isotonic: $\sum_{i=1}^{n}(y_{i}-\hat{f_{i})^{2}}$ where $ \hat{f_{i}^{2}} = m(f_{i}) + e_{i} $
- Isotonic 방법은 non-parametric 방법(학습할 파라미터가 없이)으로 확률값을 보정하는 방법이다. $y_{i}$은 $i$번째 샘플의 실제 라벨을 의미하고, $\hat{f_{i})^{2}}$은 calibration된 분류기의 출력을 의미한다. 즉, calibration 시키기위해서 단조증가 함수를 하나 씌워서 만드는 것이다.[3]. 여기에 해당하는 $m$을 하나 학습해야하는데, 이거는 iteration돌면서 윗 식을 최소화할 수 있는 m을 찾는다. 전반적으로 Isotonic이 아래의 Platt calbiration보다 성능이 좋다고 알려져있다.

Sigmoid (=Platt calibration) : $ P(y=1|f) = \frac{1}{1+exp(Af+b)}$
- 확률값(f)을 다시 A, B을 학습시켜서 학습할 수 있도록 하는 방법이다. 이 방법에서는 A, B을 찾기위해서 Gradient descent을 사용한다고 한다.


예제코드


아래와 같이 Calibration plot으로 실제 예측치 중에 몇의 positive case가 있는지 파악할 수 있다. prob_pred은 각 확률의 구간들을 평균낸 것이다. 예를들어 n_bins에 따라서, 하위구간의 확률을 가진 샘플 10개가 평균적으로 얼마의 확률을 가졌는지를 의미한다. 이에 해당하는 실제 positive data point의 수가 prob_true이다.

 

그렇게해서 아래와 같이 CalibrationDisplay함수를 이용해서 plot으로 확률이 어느정도 신뢰도를 대변할 수 있는지 파악할 수 있다 [3].

import sklearn
import numpy as np

from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
from sklearn.svm import SVC

x, y = make_classification(
    n_samples=10000, n_features=20, n_informative=2, n_redundant=10, random_state=42
)

x_train, x_test, y_train, y_test = train_test_split(x, y)

base_clf = SVC(probability=True)
base_clf.fit(x_train, y_train)


# Calibriation plot 그리기
from sklearn.calibration import calibration_curve, CalibrationDisplay

y_prob = base_clf.predict_proba(x_test)[:, 1]
prob_true, prob_pred = calibration_curve(y_test, y_prob, n_bins=10)

disp = CalibrationDisplay(prob_true, prob_pred, y_prob)
disp.plot()

 

Calibration은 아래와 같이 진행한다.

from sklearn.calibration import CalibratedClassifierCV
calibriated_clf = CalibratedClassifierCV(base_estimator=base_clf, method="isotonic", cv=2)
calibriated_clf.fit(x_test, y_test)

pred_y = calibriated_clf.predict_proba(x_test)[:, 1]
prob_true, prob_pred = calibration_curve(y_test, pred_y, n_bins=10)

disp = CalibrationDisplay(prob_true, prob_pred, pred_y)
disp.plot()

 

[1] https://scikit-learn.org/stable/modules/calibration.html#probability-calibration
[2] Predicting Good Probabilities with Supervised Learning, A. Niculescu-Mizil & R. Caruana, ICML 2005
https://www.cs.cornell.edu/~alexn/papers/calibration.icml05.crc.rev3.pdf

[3] https://scikit-learn.org/stable/modules/generated/sklearn.calibration.CalibrationDisplay.html#sklearn.calibration.CalibrationDisplay.from_estimator

반응형

역전파알고리즘은 1986년 데이비드 등이 개발한 알고리즘이다. 핵심은 "은닉 유닛(hidden unit)이 무엇을 해야하는지는 모르지만, 은닉층의 활성도를 바꿀때 얼마나 빨리 오차가 변하는지는 계산할 수 있다".

 

그림1. Simple neural network

 

반환되는 Outcome $\hat{y}$가 변화할 때, 오차율(E)가 얼마나 변화할 것인지를 다음과 같이 미분식을 이용해보자. 아랫첨자 j은 데이터포인트이다. 

$\frac{\delta E}{\delta W_{1}} = \frac{\delta E}{\delta o_{1}} \frac{\delta o_{1}}{\delta z_{1}} \frac{\delta z_{1}}{\delta_{W_{1}}}$

 

첫번째 곱인 $\frac{\delta E}{\delta o_{1}}$ 은 MSE와 구하는 공식이 같아서 구할 수 있고, 

두번쨰 곱인 시그모이드(sigmoid)함수의 미분은 $\sigma'(\cdot) = \sigma(\cdot)(1-\sigma(\cdot))$ 으로 구할 수 있다.

세번째 곱은 W와 H1의 곱이 Z1이기 때문에,  $\frac{\delta z_{1}}{\delta_{W_{1}}} = h_{1}$

 

이를 모두 곱해주면된다. 곱한 모든 식의 아래와 같이 업데이트하는 과정을 이어나가면 끝이다.

$W_{1}^{new} = W_{1} -\alpha \frac{\delta E}{\delta W_{1}}$

 

 

위와 같은 과정을 $\hat{y}$ 을 이전레이어의 출력값으로 변경해서 층마다 업데이트하면된다.

 

 

예제를 위해서, 아래의 간단한 MLP(Multi-layer perceptron)을 생성해보았다.

import tensorflow as tf
model = tf.keras.models.Sequential()
model.add(tf.keras.layers.InputLayer(input_shape=(100)))
model.add(tf.keras.layers.Dense(10))
model.add(tf.keras.layers.Dense(1, activation='sigmoid'))

model.compile(loss='binary_crossentropy')

model.summary()

 

그리고 랜덤으로 xs, ys을 만들어주고, 학습시켰다.

xs = tf.constant(tf.random.uniform(shape=(100, 10)))
ys = tf.round(tf.random.uniform(shape=(100, 1)))

model.fit(xs, ys)

 

다행히 tensorflow2.x 은 역전파알고리즘을 tf.Gradient 객체로 한번에 구할 수 있게 만들어주어, 이를 사용하면된다.

with tf.GradientTape(persistent=True) as tape:
    tape.watch(xs)
    w = model.trainable_weights
    ys = model(xs)
    
    
dE_dW = tape.gradient(ys, w)

 

아래의 gradient descent방법과 같이 update을 해주면된다.

lr = 0.005
Ws = model.trainable_weights

for W, grad in zip(Ws, dE_dW):
    W = W - (lr * grad)

 

 

추가적으로 matrix derivation을 직접해보는경우 아래와같이 matrix shape이 안맞는 경우가 발생한다. 필자도 이 떄문에 한 일주일을 낭비했다..

 

마찬가지로, 우리가 구하고싶은것은 dE/dW 이므로, 아래와 같다.

 

 

https://medium.com/jun-devpblog/dl-3-backpropagation-98206058d72e

 

[DL] 3. Backpropagation

1. Error Backpropagation

medium.com

https://souryadey.github.io/teaching/material/Matrix_Calculus.pdf

반응형

Machine learning 관련 페이퍼를 보다보면, supervise, semi-supervise, reinforcment, 등의 용어는 흔히 들어봐서 이해하지만, inductive, transductive, self-supervised 등은 약간 낯설기도하다. 비슷한 개념인것같은데 분류방식에 의한 방법으로 생각되어 다음과 같이 정리해보았다.

 

1. 문제의 종류 따른 분류

- Supervised,

- Unsupervised

- Reinforment.

 

2. Statistical inference: inference은 학습한 기계학습모델이 주어진 관찰하지 않은 데이터(X, unseen data)을 예측하는 작업을 의미한다. 아래의 3가지 작업에 "Learning", 과 "Inference"을 꼭 구분하여 읽어야한다. "inductive", "deductive"에만 꽂혀서는 의미를 알 수 없다.

- Inductive learning (귀납적 학습): 근거를 이용한 추론방법. 여태까지 주어진 데이터를 이용하여(근거,from specific historical examples), 학습하는 것을 의미한다.. 대부분의 머신러닝들이 위의 학습방법에 해당된다. 주어진데이터에 따라 머신러닝모델이 데이터를 어떻게 표현할지 파라미터를 알고있으면, 이 과정이 일반화되는 과정일 수 있다. 데이터를 기반으로 모델을학습하는 과정(데이터: 구체적인 사례, 머신러닝의 파라미터: 일반화)

- deductive inference (연역적 추론): inductive learning 의 반대다. 학습한 머신러닝의 모델을 각 개별 사례에 적용하는 것을 의미한다. 예측모델로 흔히 하고자하는 궁극적인 일이 이에 해당된다. 모델의 학습이 Induction(귀납)이라면, 모델의 사용은 연역이다.

- Transductive learning: 특정한 사례를 이용하여, 또다른 특정한 사례를 예측하는 것을 의미한다. (specific case to specific case)

 

[reference] https://machinelearningmastery.com/types-of-learning-in-machine-learning/

반응형

Few-shot learning: 훈련데이터의 수가 매우 제한적인 상황에서 AI모델을 개발하려는 기술 또는 알고리즘들을 의미함. 이 Few-shot learning은 상대적으로 적은 훈련데이터에서도 새로 입력값으로 주어지는 데이터(unseen data)을 잘 인식하고 분류하고자하는 노력하는 것임. 전통적인 AI의 트렌드와같이 매우 많은 데이터로, ML 모델을 구축하는것이 목표가아닌, 적은데이터에서 ML 모델을 구축하자는것이 핵심.

Few-shot 접근법

데이터 레벨 접근법

파라미터 레벨 접근법

- 메타러닝

- 메타러닝의 아류 

메트릭 러닝

 

 

Reference: www.unite.ai/few-shot-learning/

반응형

Driver/library version mismatch:


2.1 CUDA가 사용한 GPU인지 파악

$ lspci | grep -i nvidia


2.2 시스템정보파악

$ uname -m && cat /etc/*release

x84_64 인지 아닌지 알아야하고, OS버전이 무엇인지 확인한다. 필자는 CentOS7, x84_64 bit이다.


2.3 시스템이 gccc가 설치되어있는지 확인.

gcc: CDUA Toolkit을 사용하기위한 컴파일러

$ gcc --version

gcc (GCC) 4.8.5 20150623 (Red Hat 4.8.5-39)

Copyright (C) 2015 Free Software Foundation, Inc.

This is free software; see the source for copying conditions.  There is NO

warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.


2.4 커널 헤더 와 패키지 버전 확인

$ uname -r

3.10.0-1127.13.1.el7.x86_64 


2.5 선택방법: 배보판특정패키지distribution-specific packages (PRM, Deb 패키지)와 배포판독립(Distribution-independent package(runfile))이 둘다사용하다. 배보판 특정 패키지는 패키지 관리자인 yum, apt 등에 종속되서 딸려있는 패키지들을 이용하는거라 손쉬웁게 설치가가능하고, 배보판독립은 리눅스 배보판에든지 설치가 가능하다. Nvidia에서는 배보판 특정 패키지의 사용을 권장한다.


반응형

+ Recent posts