요약


Gradient Accumulation은 GPU 메모리 한계를 극복하면서 더 큰 배치(batch) 크기로 학습한 것과 동일한 효과를 내기 위해 사용하는 기법입니다.

 

문제: GPU 메모리 한계로 큰 배치를 한번에 학습할 수 없는 경우 발생

해결방법

  1. 작은 배치에서 계산된 loss을 gradient을 누적합니다. (=.backward()만 호출하면 gradient값이 더해집니다.)
  2. 정해진 횟수(gradient accumulation step)만큼 누적되었다면 optimizer로 가중키를 업데이트 합니다.
  3. 최종기울기가 큰 배치에서 한번에 처리한 결과와 동일하도록 각 loss을 gradient accumulation step만큼 나눠주어 스케일링합니다.

 

코드 스니펫


아래와 같은 코드스니펫으로 사용될 수 있습니다. 주요 코드 설명은 아래와 같습니다.

  • loss = loss / accumulation_steps : loss을 누적하여 이후에 backward을 할거기에 미리 accumulation_steps수로 나눠줍니다.
  • loss.backward(): backward을 호출하면 gradient가 이전의 gradient값과 내부적으로 더해(add)집니다. 여기서는 step()과 zero_grad()을 호출하지 않앗기때문에 backward을 호출할때마다 gradient값이 누적됩니다.
  • (step+1) % accumlation_steps == 0: 조건식을 만족하면 누적되어 평균된 gradient값에 대해서 모델의 가중치를 업데이트(step)하고, 이후 step에 반영되지 않도록 0으로 초기화해줍니다(zero_grad())
accumulation_steps = 4

model.train()
optimizer.zero_grad()

for step, (input_data, target_data) in enumerate(dataloader):
    outputs = model(input_data)
    loss = criterion(outputs, target_data)
    loss = loss / accumulation_steps
    loss.backward()

    if (step + 1) % accumulation_steps == 0:
        optimizer.step()
        optimizer.zero_grad()

 

수치적인 예시


예를 들어, 큰 배치(8)을 작은 배치(n=8)로 나누는 예시를 생각해볼 수 있습니다. 이 때, accumulation_steps은 2가 됩니다.

첫 번재 미니배치에서는 4개의 샘플이 들어옵니다

$loss_{1} = \frac{L_{1} + L_{2} + L_{3} + L_{4}}{4}$: 첫 번째 미니배치의 손실값입니다. pytorch에서 reduce을 미리 해주기에 평균값으로 자동으로 계산됩니다. 하지만, 최종적으로 8개 샘플에 대한 평균을 맞춰야 하므로, accumulation_steps = 2로 설정했다면, 아래와 같습니다.

$loss'_{1}  = \frac{1}{2} \times \frac{L_{1} + L_{2} + L_{3} + L_{4}}{4}$ 

이 때, loss_prime_1.backward()을 수행하면 $\nabla (loss_{1})/2$가 됩니다.

두 번째, 미니배치에서도 4개의 샘플이 들어옵니다.

$loss_{2} = \frac{L_{5} + L_{6} + L_{7} + L_{8}}{4}$: 두 번째 미니배치의 손실값입니다. 여기서도 accumulation_steps = 2로 설정햇기에 아래와 같습니다.

$loss'_{2}  = \frac{1}{2} \times \frac{L_{5} + L_{6} + L_{7} + L_{8}}{4}$ 

여기서도 loss_prime_2.backward()을 수행하면 $\nabla (loss_{2})/2$ 가 더해집니다.

 

최종적인 손실함수은 아래와 같습니다.

$\frac{1}{2} \times \frac{L_{1} + L_{2} + L_{3} + L_{4}}{4} + \frac{1}{2} \times \frac{L_{5} + L_{6} + L_{7} + L_{8}}{4} = \frac{L_{1} + ... + L_{8}}{8}$ 

 

그리고 최종적인 gradient은 아래와 같습니다. 즉, loss을 계산한후에 accumulation_step으로 나눠주어야, 원하는 큰 배치사이즈에 맞춰서 같은 스케일로 gradient을 업데이트할 수 있습니다.

$\frac{1}{2} \times \frac{\nabla (L_{1} + L_{2} + L_{3} + L_{4})}{4} + \frac{1}{2} \times \frac{\nabla (L_{5} + L_{6} + L_{7} + L_{8})}{4}  = \frac{ \nabla  (L_{1} + ... + L_{8})}{8} $

반응형

요약


  DP DDP
모델 복제 오버해드 매 반복마다 각 GPU에 모델 복제 초기 한번만으로 프로세스에 모델 복제
데이터 분산 및 수집 Scatter-Gather방식으로 통신비용발생 각 프로세스가 독립적으로 작업
(통신비용 적음)
GIL GIL로인해 multi-thread 성능제한 GIL문제없음
통신비용 GPU간 동기화없음 GPU 간 All-redeuce 통신비용발생
적합한 환경 단일 머신 멀티노

 

DataParallel (DP)

DP은 데이터 병렬화 기술 중, 싱글노드에서만 사용할 수 있는 병렬화 기술입니다. DP은 한 프로세스에서만 돌아가기에 "Single process, multi-threaded"입니다. 하나의 프로세스에서 여러 GPU을사용하는 방식입니다. 즉 하나의 프로세스이기 때문에, 모델과 데이터를 한 번만 메모리에 로드하고, 공유하는 방식입니다. 레핑코드인 torch.nn.DataParallel은 다음과 같은 방법을 따릅니다.

  1. Scatter mini-batch inputs to GPUs: DataLoader에서 병렬처리를하든 일단 하나의 GPU로 데이터를 모은 후, 이를 서로다른 GPU에 퍼뜨립니다. (여기서 불필요하게 오버해드가 발생합니다. 큰 데이터를 로드하는 경우, 데이터를 복사하는데 병목이 될 수 있습니다)
  2. Replicate model on GPUs: DP은 매 반복(step, iteration)마다 모델의 복제본(model replicas)을 만들어 각 GPU에 뿌려줍니다. 이유는 각 GPU가 독립적으로 연산을 수행할 수 있도록 동리한 모델을 GPU별로 만들기 위함입니다.(여기서 불필요하게 오버해드 발생)
  3. Parallel forward: 이 단계에서는 미니배치가 N이라면, N/4씩의 미니배치씩 처리하여 출력을 생성합니다. (이 과정은 병렬로 처리되기 때문에, 데이터가 미리 복제가되지않았거나, 모델이 복제되지 않았다면 연산시작까지 대기해야해서 오버해드가 발생합니다)
  4. Gather : 모든 출력을 하나의 GPU1에 가져옵니다. 이 과정에서는 GPU to GPU 통신이 발생합니다. (GPU1의 데이터를 모으는 과정에서 병목이 발생할 수 있음)
  5. Compute gradient: O1~O4까지 얻었으니 하나로 합쳐서 loss을 계산합니다. 이 합친 텐서에서는 .grad_fn 속성에 GatherBackward가 붙는데 여러 GPU에서 왔음을 알 수 있는 흔적입니다.
  6. Backword process: 하나의 모델에 대하여 백워드 프로세스가 진행됩니다.
  7. Parameter update: 모델의 파라미터를 한 GPU에서 업데이트 합니다.
import torch
from torchvision.models import resnet50

model = resnet50()
dp_model = torch.nn.DataParallel(model).to("cuda") # 이 단계까지는 GPU:0에만 옮겨집니다.
o = dp_model(torch.rand(8, 3, 256, 256)) # forward하는 순간 모델이 복제되고 배치가 나뉘어집니다.
o.device
tensor([[-0.4658, -0.4177,  1.4429,  ..., -0.5929,  0.6103, -0.0062],
        [-0.2560, -0.3838,  1.4179,  ..., -0.4299,  0.6211,  0.0942],
        [-0.3859, -0.4096,  1.4012,  ..., -0.5815,  0.6894, -0.0200],
        ...,
        [-0.4031, -0.4946,  1.3107,  ..., -0.4936,  0.5158,  0.0206],
        [-0.3646, -0.3747,  1.4674,  ..., -0.5438,  0.6902, -0.0674],
        [-0.3301, -0.4867,  1.3945,  ..., -0.4488,  0.6343,  0.0417]],
       device='cuda:0', grad_fn=<GatherBackward>)
model = nn.DataParallel(model)

# 옵티마이저 초기화
optimizer.zero_grad()

# 순전파
outputs = model(inputs) # 이 단계에서 모델복제, 데이터 scatter, forward, gather가 이뤄짐

# 손실 계산
loss = criterion(outputs, labels)  # 손실은 하나의 GPU에서만

# 역전파
loss.backward() # 역전파도 하나의 GPU에서만

# 파라미터 갱신
optimizer.step() # 파라미터 갱신

 

실제로 pytoch.nn.DataParallel에서도 다음과 같이 foward가 정의되어있습니다. 모아서 output만 반환해주는 것이죠.

class DataParallel(Module, Generic[T]):

    def forward(self, *inputs: Any, **kwargs: Any) -> Any:
    	inputs, module_kwargs = self.scatter(inputs, kwargs, self.device_ids)
        replicas = self.replicate(self.module, self.device_ids[: len(inputs)])
        outputs = self.parallel_apply(replicas, inputs, module_kwargs)
        return self.gather(outputs, self.output_device)
        
# 사용시
CLASS torch.nn.DataParallel(module, device_ids=None, output_device=None, dim=0)

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

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

어찌되었든 하나의 텐서를 모아서 backward을 진행하기 때문에, gather을 수행하는 GPU입장에서는 VRAM이 더 요구되는 사항입니다. 그래서 다른 개발자들은 Gradent까지 다 계산한다음에 GPU1로 가져오는 방법도 제안합니다.

 

 

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

반응형

+ Recent posts