안녕하세요.

이번 글에서는 training 과정 visualization 해주는 패키지를 소개하려고 합니다.

 

기본적으로 pytorch에서는 tensorboard를 사용하여 loss, accuracy 등 다양한 metrics와 weight, gradient 값들을 histogram으로 볼 수 있도록 summarywriter라는 가능을 제공하고 있습니다.

 

torch.utils.tensorboard import SummaryWriter

 

(↓↓SummaryWriter 사용법↓)

https://pytorch.org/docs/stable/tensorboard.html

 

torch.utils.tensorboard — PyTorch 1.9.0 documentation

torch.utils.tensorboard Before going further, more details on TensorBoard can be found at https://www.tensorflow.org/tensorboard/ Once you’ve installed TensorBoard, these utilities let you log PyTorch models and metrics into a directory for visualization

pytorch.org

 

 

 

하지만, 이외에 많은 분들이 "Weight and Biases"에서 제공하는 wandb 패키지를 이용해 학습과정들을 visualization하기도 하는데요. 이번 글에서는 wandb 패키지를 이용해 어떻게 visualization 할 수 있는지 살펴보도록 하겠습니다.

 

 

※이전 글에 있는 코드에 wandb 패키지를 적용하는 법을 설명하도록 할테니, 중간중간 코드가 이해안되시는 분들은 꼭 이전 글들을 봐주세요!

 

 

[필자의 개발환경]

OS: Window

가상환경: 아나콘다

딥러닝 프레임워크: Pytorch

IDE: Visual Studio Code

 

 

1. 회원가입 및 wandb 연동하기

1-1. wandb 패키지 설치하기

먼저,  제 경우에는 아나콘다 가상환경VS Code interpreter에 연동시켜 사용하고 있기 때문에 아나콘다에 wandb 패키지설치하도록 하겠습니다.

 

(↓↓ 아나콘다 가상환경에 다양한 패키지 설치 및 VS code 연동 방법↓)

https://89douner.tistory.com/74

 

5. 아나콘다 가상환경으로 tensorflow, pytorch 설치하기 (with VS code IDE, pycharm 연동)

안녕하세요~ 이번시간에는 아나콘다를 통해 2개의 가상환경을 만들고 각각의 가상환경에서 pytorch, tensorflow를 설치하는법을 배워볼거에요~ Pytorch: Python 3.7버전/ CUDA 10.1 버전/ Pytorch=1.4버전 Tensorf..

89douner.tistory.com

 

 

우선 "anaconda wandb"라고 검색하니 아래 사이트가 나옵니다. 해당 사이트에 접속해보겠습니다.

https://anaconda.org/conda-forge/wandb

 

Wandb :: Anaconda.org

 

anaconda.org

 

 

접속하니 아래와 같은 화면이 뜹니다. 아나콘다wandb설치하는 명령어를 알려주네요. 

해당부분을 복사합니다.

그림1

 

 

 

우선 저는 대부분의 패키지(pytorch 등)아나콘다 base라는 가상환경에 설치되어 있기 때문에, base 가상환경복사한 명령어입력하여 설치진행해주겠습니다.

그림2

 

 

1-2. Wandb (Weight and Biases) 회원가입

wandb 패키지이용하려면 "Weight and Biases"회원가입이 되어 있어야 합니다.

그래야 "Weight and Biases" 웹 사이트와 연동하여 visualization 결과들을 살펴볼 수 있기 때문이죠.

 

그럼 "Weight and Biases"에 회원 가입을 해보도록하겠습니다.

 

우선 아래 "Weight and Biases" 사이트접속합니다.

https://wandb.ai/site

 

Weights & Biases – Developer tools for ML

A central dashboard to keep track of your hyperparameters, system metrics, and predictions so you can compare models live, and share your findings.

wandb.ai

 

 

 

접속을 하시면 아래화면이 처음 등장하게됩니다. 그리고 회원가입 버튼 "Sign up"을 클릭해줍니다.

그림3

 

 

그럼 아래와 같이 회원가입 창이 뜹니다. 제 경우에는 github과 연동해서 사용하려 하기 때문에, "Sign up with GitHub"을 클릭해서 사용하도록 하겠습니다.

 

그림4

 

 

 

그리고 "Authorize wandb" 을 클릭해줍니다.

 

그림5

 

 

마지막으로 아래와 같이 계정생성에 필요한 정보를 입력하시면 회원가입이 완료가 됩니다.

그림6

 

 

 

회원가입 후, 로그인을 하시면 아래와 같은 화면이 나타납니다.

그림7

 

 

우선 이 화면은 대기해 놓고 아나콘다 가상환경wandb연동하는 작업부터 하겠습니다.

 

 

 

 

 

 

2. 아나콘다 가상환경과 wandb 연동하기

앞서 언급했듯이 "아나콘다 가상환경을 VS Code interpreter에 연동시켜 사용하고 있고", Weight and Biases 웹 사이트와 연동하여 visualization 결과"를 볼 수 있기 때문에, 아나콘다 가상환경에서 Weight and Biases 웹 사이트와 연동시켜주어야 합니다.

 

연동 방식은 간단합니다.

먼저 아래와 같이 순서를 진행합니다.

  • base 가상환경 프롬프트 열기
  • wandb login 명령어 입력
  • 아래 빨간색 박스 사이트 복사

그림8

 

  • 위에서 복사한 사이트 접속시 아래 화면이 출력 해당 인증키 복사

그림9

 

  • 복사한 인증키 아래 빨간색 밑줄 부분에 붙여넣기하고 엔터 (참고로 저는 복붙이 잘 안돼서 메모장에 복붙한다음 하나씩 인증키를 입력했습니다;;;;)

그림10

 

 

 

연동완료 되었습니다!

그림11

 

 

 

 

3. Project 생성해주기

VS Code에서도 task 마다 별도의 project를 만들게 됩니다. 그렇다면 각각의 task마다 visualization 하려는 정보들도 모두 다르겠죠? 그래서, task 별로 wandb의 visualization project를 만들어 주는것이 좋습니다. (그래야 task 마다 visualization 기록들을 용이하게 관리 할 수 있어요.)

 

그림12

 

 

 

그럼 WandB(=W&B) project생성해보겠습니다. 

먼저 좌측 상단 "Home" 부분에 "Projects"→"Create new project"를 클릭 해주세요

그림7

 

 

 

 

아니면 아래 사이트(="wandb.ai/username")에 직접 접속해서 우측에 있는 "Create new project"를 클릭해주세요.

그림13

 

Project name설정하고 생성해줍니다.

우선 저는 개인용으로 사용할 거라 "private" 버전으로 만들었습니다.

그림14

 

 

 

 

Project생성되면 아래 화면이 출력됩니다.

이제 VS code에서 wandb 패키지로 visualization 관련 코드를 입력하고 실행시키면, 아래 화면에 visualization 결과들이 생성됩니다. 그럼 이제 VS code에서 관련 코드들을 입력해볼까요? 

그림15

 

 

 

3. VS code에 visualization을 위한 wandb 관련 코드 입력하기

제일 먼저 할 것은 wandb 패키지import 시켜주는 것입니다.

현재 "alb_train2.py"에 train 관련 코드가 들어가 있습니다.

 

("alb_train2.py" 파일은 VS code 상에서 UNet segmentation project에 속해있는데, 해당 project가 base 아나콘다 가상환경 interpreter에 연동되어 있습니다. 앞서 base 아나콘다에 wandb 패키지를 설치했기 때문에 에러없이 import wandb 를 수행할 수 있습니다.) 

그림16

 

 

그리고 train_model 함수 부분에 먼저 두 개코드 입력해줍니다.

  • wandb.init()
  • wandb.watch()
def train_model(net, fn_loss, optim, num_epoch):
    wandb.init(project='test', entity='douner89') #추가된 코드
    wandb.watch(net, fn_loss, log="all", log_freq=10) #추가된 코드
    since = time.time()

    best_model_wts = copy.deepcopy(net.state_dict())
    best_loss = 100

 

 

3-1. wandb.init()

먼저, wandb.init() 함수에 대해 설명해보도록 하겠습니다.

 

(↓↓↓ wandb.init() API ↓)

https://docs.wandb.ai/ref/python/init

 

wandb.init

 

docs.wandb.ai

 

 

 

먼저 위의 사이트를 접속한 후, 제일 먼저 눈에 보이는 문장은 아래와 같습니다.

 

"you could add wandb.init() to the beginning of your training script as well as your evaluation script"

 

위와 같은 설명을 토대로 train 함수 첫 번째 부분에 wandb.init() 함수를 구현해놨습니다.

 

wandb.init() 함수 인자들은 아래와 같이 설명이 나와있습니다.

 

그림17

 

 

이 중에서 우선 두 가지 argumetns 설명하도록 하겠습니다.

  • project: 앞서 생성한 project 명 (←"그림14" 참고)
  • entity: 앞서 계정 생성시 설정한 user name (←"그림14" 참고)

해당 project, entity 명을 제대로 입력 해주어야 연동된 weight and biases 사이트에 정보들이 전송이됩니다.

 

 

 

 

 

3-2. wandb.watch()

이번엔 wandb.watch() 함수에 대해서 알아보겠습니다.

 

(↓↓↓ wandb.watch() API ↓↓↓)

https://docs.wandb.ai/ref/python/watch

 

wandb.watch

 

docs.wandb.ai

 

 

해당 API reference를 살펴보면 아래와 같은 문구가 나옵니다.

 

"Hooks into the torch model to collect gradients and the topology."

 

즉, gradient, topology와 관련 정보visualization 해주기 위해 입력해주는 코드라고 하네요.

결국 이 코드로 인해 gradient, topology 값들을 visualization 해줄 수 있게 됩니다. 

 

아래 argument에서 3가지 정도만 설명하겠습니다.

  • models: 딥러닝 모델
  • criterions: loss function
  • log: all이라고 설정하면, gradient, parameters와 관련된 값들을 visualization 해서 볼 수 있습니다.

그림18

 

 

 

3-3. wandb.log()

train_model 함수"#추가된 코드3" 부분을 보면 "wandb.log"라는 코드를 볼 수 있으실 겁니다.

 

(↓↓ wandb.log() API ↓)

https://docs.wandb.ai/ref/python/log

 

wandb.log

 

docs.wandb.ai

 

 

위의 API에서 설명하듯이, wandb.log 함수에 내가 visualization 하고 싶은 argument를 넘겨줄 수 있습니다. 아래 코드에서는 epochtraining lossvisualization 해주기 위해 아래와 같이 입력했습니다.

 

wandb.log({'Epoch': epoch, 'loss': np.mean(loss_arr)})

 

(※wandb.log() API를 보면 알 수 있듯이 다양한 정보들을 visualization (ex: gradient 'histogram', image, etc...) 를 할 수 있으니 참고해주세요)

 

# TRAIN MODE
def train_model(net, fn_loss, optim, num_epoch):
    wandb.init(project='test', entity='douner89') #추가된 코드1
    wandb.watch(net, fn_loss, log="all", log_freq=10) #추가된 코드2
    since = time.time()

    best_model_wts = copy.deepcopy(net.state_dict())
    best_loss = 100

    for epoch in range(st_epoch + 1, num_epoch + 1):
        net.train()
        loss_arr = []
        batch_order=0

        for batch, data in enumerate(loader_train, 1):
            batch_order=batch_order+1
            data['label'] = data['label']/255.0

            input = data['input']
            label = data['label']

            # forward pass
            label = data['label'].to(device)
            input = data['input'].to(device)

            output = net(input)

            # backward pass
            optim.zero_grad()

            loss = fn_loss(output, label)
            loss.backward()

            optim.step()

            # 손실함수 계산
            loss_arr += [loss.item()]

            print("TRAIN: EPOCH %04d / %04d | BATCH %04d / %04d | Batch LOSS %.4f" %
                (epoch, num_epoch, batch, num_batch_train, np.mean(loss_arr)))
        
        print("#############################################################")
        print("TRAIN: EPOCH %04d | Epoch LOSS %.4f" %
                (epoch, np.mean(loss_arr)))
        print("#############################################################")
        wandb.log({'Epoch': epoch, 'loss': np.mean(loss_arr)}) #추가된 코드3



        with torch.no_grad():
            net.eval()
            loss_arr = []

            for batch, data in enumerate(loader_val, 1):
                data['label'] = data['label']/255.0

                # forward pass
                label = data['label'].to(device, dtype=torch.float32)
                input = data['input'].to(device, dtype=torch.float32)

                output = net(input)

                # 손실함수 계산하기
                loss = fn_loss(output, label)

                loss_arr += [loss.item()]

                print("VALID: EPOCH %04d / %04d | BATCH %04d / %04d | LOSS %.4f" %
                        (epoch, num_epoch, batch, num_batch_val, np.mean(loss_arr))) 

            epoch_loss = np.mean(loss_arr)

            # deep copy the model
            if epoch_loss < best_loss:
                best_loss = epoch_loss
                best_model_wts = copy.deepcopy(net.state_dict())

        print()
    
    time_elapsed = time.time() - since
    print('Training complete in {:.0f}m {:.0f}s'.format(
        time_elapsed // 60, time_elapsed % 60))
    print('Best val loss: {:4f}'.format(best_loss))
    net.load_state_dict(best_model_wts)
    return net

 

 

 

 

 

4. Visualization 결과보기

다시 weight and biases 사이트 화면으로 돌아오겠습니다.

그림15

 

4-1. wandb.log() 부분 visualization 하기

하지만, 코드를 실행시킨 후, charts 부분을 보면 아래와 같이 wandb.log()에 설정했던 log들이 기록됨을 알 수 있습니다. "wandb.log()"에 epoch과 loss를 설정했기 때문에 epoch, loss값visualization 되는 것을 볼 수 있습니다.

그림19

 

 

 

4-2. wandb.watch() 부분 visualization 하기 (Feat. gradient)

또한, 앞서 "wandb.watch()" 함수를 통해 gradient, parameters 값을 visualization 할 수 있다 언급한 바 있습니다.

 

먼저, gradient 값부터 확인해보겠습니다.

그림20

 

 

아래 그림에서 X, Y축의 의미하는 바는 다음과 같습니다.

  • X축: epoch (필자의 코드에서는 epoch=30으로 설정되어 있음)
  • Y축: gradient

그림21

 

 

 

 

앞서 구현한 model의 변수명을 기반으로 해당 위치에 있는 layer에 전파되는 gradient 값확인해 볼 수 있습니다.

그림22

 

 

 

 

5 epoch에 마우스 포인터를 올려놓으면 해당 epoch 단계에서 얻는 gradient 들의 분포 (histogram) 를 알 수 있습니다.

그림23

 

 

 

그리고 특정 epoch 부분들의 gradient 값디테일하게 보고 싶으면 해당 부분drag 하면 됩니다. 

그림24

 

 

 

또 다른 layer들의 gradient 값을 확인하고 싶다면, 아래 화면우측 하단 빨간색 부분클릭해주시면 됩니다.

그림25

 

 

 

이러한 gradient 값은 다양하게 이용될 수 있지만, 그 중에 가장 대표적인 것이 exploding gradient, vanishing gradient확인해보는 것입니다.

 

예를 들어, conv2 weightgradient 값도 대략 10^5로 굉장히 큰데, conv1 weightgradient 값이 대략 10^7 이면  exploding gradient를 의심해볼 수 있겠죠?

  • 3e+5 →  3*10^5 → 대략 10^5

그림26. 이미지출처:  https://wandb.ai/site/articles/debugging-neural-networks-with-pytorch-and-w-b-using-gradients-and-visualizations

 

(↓↓↓ W&B를 이용해 exploding gradient, vanishing gradient를 보여주는 사례 ↓↓↓)

https://wandb.ai/site/articles/debugging-neural-networks-with-pytorch-and-w-b-using-gradients-and-visualizations

 

Debugging Neural Networks with PyTorch and W&B Using Gradients and Visualizations on Weights & Biases

by Ayush Thakur — Debugging Neural Networks with PyTorch and W&B Using Gradients and Visualizations

wandb.ai

 

 

 

 

 

4-3. wandb.watch() 부분 visualization 하기 (Feat. parameters)

gradient 값 외에, conv filter 값들도 확인해 볼 수 있습니다.

이러한 Conv filter 값들을 통해 유의미한 통계분석도 해볼 수 있겠네요

그림27

 

 

 

 

5. ETC

위에서 설명한 것 외에 다양한 정보들을 visualization 해서 볼 수 있습니다.

 

먼저, 왼쪽 빨간색 박스 부분을 클릭하면 system 즉, hardward (CPU, Memory, GPU) 관련 정보들을 살펴 볼 수 있습니다. 

그림28

 

 

 

아래 빨간색 네모 박스 log 관련 정보를 보여주는 곳인데, 학습 시 vs code기록되는 log 들을 그대로 볼 수 있습니다.

그림29

 

 

 

6. 다른 결과들과 비교하기

실험을 하다보면 다양한 hyper-parameter 조합을 통해 결과를 내야하는 경우가 많습니다.

앞에서는 learning rate 부분을 1e-3으로 설정하고 실행했습니다.

그렇다면 이번에는 le-2로 설정하고 실행해보겠습니다.

 

그림30

 

 

왼쪽 빨간색 네모 부분새로운 process가 실행되는 것을 볼 수 있고, 이전 실험 결과(="solar-toterm-19")와 중첩으로 visualization해서 볼 수 있으니, 비교수월할 수 있겠네요.

그림31

 

 

 

하지만 위와 같은 경우 어떠한 hyper-parameter 조합으로 실험한 결과인지 모르기 때문에, 아래와 같이 해당 hyper-parameter 조합에 대한 정보process name으로 설정해주면 좋습니다.

그림34

 

 

위에서 설명한 방법 외에 다양한 visualization 기능들이 있습니다. 예를 들어, line plot, scatter plot 형태로도 보여 줄 수 있고, GAN 관련한 정보들을 visualization 해줄 수 도 있고, hyper-parameter 중에 중요한게 무엇인지도 알려주는 기능도 있습니다. 이와 관련된 부분은 추후 다루도록 하겠지만, 아래 영상을 보시면 상당 부분 혼자서 하실 수 있을거라 생각됩니다.

 

https://www.youtube.com/watch?v=91HhNtmb0B4 

 

 

 

그 외 참고하면 좋을 사이트를 아래 링크해두겠습니다.

https://theaisummer.com/weights-and-biases-tutorial/

 

A complete Weights and Biases tutorial | AI Summer

Learn about the Weights and Biases library with a hands-on tutorial on the different features and visualizations.

theaisummer.com

 

https://analyticsindiamag.com/hands-on-guide-to-weights-and-biases-wandb-with-python-implementation/

 

 

사실 wandb 패키지의 가장 강력한 기능은 다양한 hyper-parameter 조합을 자동으로 실행해주고 관련 결과들을 visualization 하여 어떤 parameter가 중요한지 보여주는 것입니다. 이러한 기능은 wandb의 sweep을 통해 구현할 수 있는데, 이 부분은 정리가 되는데로 업로드 하겠습니다.

 

감사합니다. 

안녕하세요.

이번 글에서는 pytorch를 이용해 UNet 모델을 구현한 code를 설명할 예정입니다.

 

다양한 딥러닝 기반 segmentation 모델이 있지만, UNet 모델이 가장 기본이 되기 때문에 다루었습니다.

 

소개해 드릴 UNet pytorch 코드는 아래 영상을 기반으로 리뷰했으니 아래 영상도 참고해주세요!

https://www.youtube.com/watch?v=sSxdQq9CCx0 

 

 

최종코드는 제일 아래에 있으니 참고해주세요!

※대부분 PPT 슬라이드에 설명한 내용을 이미지로 만들어 업로드했기 때문에 글씨가 잘 안보일 수 도 있습니다. 그래서 PPT파일을 따로 첨부하도록 하겠습니다.

 

 

Unet pytorch implementation.pptx
2.52MB

 

 

 

0. UNet() 함수 호출

 

Pytorch에서 UNet 모델을 불러오는 코드는 아래 한 줄로 가능합니다.

 

model = UNet().to(device)

 

위의 코드를 실행시키면 구현해 놓은 UNet class가 로드 됩니다.

 

그림1

 

그럼 구현해 놓은 UNet class를 살펴보도록 하겠습니다.

 

 

 

 

 

1. Contracting Path 구현하기

그림2

 

 

그림3

 

 

 

 

 

2. Expansive Path 구현하기

그림4

 

 

 

그림5

 

 

 

 

 

3. Concatenation 구현하기

그림6

 

 

 

그림7

 

 

 

 

 

 

 

4. 최종코드

class UNet(nn.Module):
    def __init__(self):
        super(UNet, self).__init__()

        def CBR2d(in_channels, out_channels, kernel_size=3, stride=1, padding=1, bias=True):
            layers = []
            layers += [nn.Conv2d(in_channels=in_channels, out_channels=out_channels,
                                 kernel_size=kernel_size, stride=stride, padding=padding,
                                 bias=bias)]
            layers += [nn.BatchNorm2d(num_features=out_channels)]
            layers += [nn.ReLU()]

            cbr = nn.Sequential(*layers)

            return cbr

        # Contracting path
        self.enc1_1 = CBR2d(in_channels=1, out_channels=64)
        self.enc1_2 = CBR2d(in_channels=64, out_channels=64)

        self.pool1 = nn.MaxPool2d(kernel_size=2)

        self.enc2_1 = CBR2d(in_channels=64, out_channels=128)
        self.enc2_2 = CBR2d(in_channels=128, out_channels=128)

        self.pool2 = nn.MaxPool2d(kernel_size=2)

        self.enc3_1 = CBR2d(in_channels=128, out_channels=256)
        self.enc3_2 = CBR2d(in_channels=256, out_channels=256)

        self.pool3 = nn.MaxPool2d(kernel_size=2)

        self.enc4_1 = CBR2d(in_channels=256, out_channels=512)
        self.enc4_2 = CBR2d(in_channels=512, out_channels=512)

        self.pool4 = nn.MaxPool2d(kernel_size=2)

        self.enc5_1 = CBR2d(in_channels=512, out_channels=1024)

        # Expansive path
        self.dec5_1 = CBR2d(in_channels=1024, out_channels=512)

        self.unpool4 = nn.ConvTranspose2d(in_channels=512, out_channels=512,
                                          kernel_size=2, stride=2, padding=0, bias=True)

        self.dec4_2 = CBR2d(in_channels=2 * 512, out_channels=512)
        self.dec4_1 = CBR2d(in_channels=512, out_channels=256)

        self.unpool3 = nn.ConvTranspose2d(in_channels=256, out_channels=256,
                                          kernel_size=2, stride=2, padding=0, bias=True)

        self.dec3_2 = CBR2d(in_channels=2 * 256, out_channels=256)
        self.dec3_1 = CBR2d(in_channels=256, out_channels=128)

        self.unpool2 = nn.ConvTranspose2d(in_channels=128, out_channels=128,
                                          kernel_size=2, stride=2, padding=0, bias=True)

        self.dec2_2 = CBR2d(in_channels=2 * 128, out_channels=128)
        self.dec2_1 = CBR2d(in_channels=128, out_channels=64)

        self.unpool1 = nn.ConvTranspose2d(in_channels=64, out_channels=64,
                                          kernel_size=2, stride=2, padding=0, bias=True)

        self.dec1_2 = CBR2d(in_channels=2 * 64, out_channels=64)
        self.dec1_1 = CBR2d(in_channels=64, out_channels=64)

        self.fc = nn.Conv2d(in_channels=64, out_channels=1, kernel_size=1, stride=1, padding=0, bias=True)

    def forward(self, x):
        enc1_1 = self.enc1_1(x)
        enc1_2 = self.enc1_2(enc1_1)
        pool1 = self.pool1(enc1_2)

        enc2_1 = self.enc2_1(pool1)
        enc2_2 = self.enc2_2(enc2_1)
        pool2 = self.pool2(enc2_2)

        enc3_1 = self.enc3_1(pool2)
        enc3_2 = self.enc3_2(enc3_1)
        pool3 = self.pool3(enc3_2)
        # print(pool3.size())
        enc4_1 = self.enc4_1(pool3)
        enc4_2 = self.enc4_2(enc4_1)
        pool4 = self.pool4(enc4_2)

        enc5_1 = self.enc5_1(pool4)

        dec5_1 = self.dec5_1(enc5_1)

        unpool4 = self.unpool4(dec5_1)
        cat4 = torch.cat((unpool4, enc4_2), dim=1)
        dec4_2 = self.dec4_2(cat4)
        dec4_1 = self.dec4_1(dec4_2)

        unpool3 = self.unpool3(dec4_1)
        cat3 = torch.cat((unpool3, enc3_2), dim=1)
        dec3_2 = self.dec3_2(cat3)
        dec3_1 = self.dec3_1(dec3_2)

        unpool2 = self.unpool2(dec3_1)
        cat2 = torch.cat((unpool2, enc2_2), dim=1)
        dec2_2 = self.dec2_2(cat2)
        dec2_1 = self.dec2_1(dec2_2)

        unpool1 = self.unpool1(dec2_1)
        cat1 = torch.cat((unpool1, enc1_2), dim=1)
        dec1_2 = self.dec1_2(cat1)
        dec1_1 = self.dec1_1(dec1_2)

        x = self.fc(dec1_1)

        return x

 

 

 

지금까지 UNet을 Pytorch로 구현한 code에 대해서 설명해봤습니다.

다음 글에서는 Pretrained model을 불러와 transfer learning을 적용시키는 코드에 대해 설명하도록 하겠습니다.

 

 

 

[Reference Site]

https://toitoitoi79.tistory.com/97

 

U-net: Convolutional Networks for Biomedical Image Segmentation Pytorch 구현

U-net은 바이오 기술에 사용되는 segmentation 논문입니다. sliding window 방식을 사용하는 CNN 구조와 달리 검증된 patch는 넘기기 때문에 보다 빠른 처리가 가능한 구조 입니다. 해당 포스팅은 구현에 포

toitoitoi79.tistory.com

 

안녕하세요.

이번 글에서는 Albumentations라는 패키지를 이용하여 데이터로드하는 방법에 대해서 설명하도록 하겠습니다.

 

https://github.com/albumentations-team/albumentations

 

GitHub - albumentations-team/albumentations: Fast image augmentation library and an easy-to-use wrapper around other libraries.

Fast image augmentation library and an easy-to-use wrapper around other libraries. Documentation: https://albumentations.ai/docs/ Paper about the library: https://www.mdpi.com/2078-2489/11/2/125 -...

github.com

 

https://albumentations.ai/

 

Albumentations

Albumentations: fast and flexible image augmentations

albumentations.ai

 

 

 

앞선 글에서는 pytorch에서 제공하는 torchvision.transforms를 이용하여 데이터 로드 하는 방식을 설명했습니다.

 

하지만, 이러한 방식으로 데이터 로드를 할 때, 두 가지 부분에서 불편한 부분이 생깁니다.

  1. input과 label 이미지에 동일한(일치한) augmentation이 적용되야하기 때문에 torch.manual_seed() 함수이용난수고정시켜주어야 합니다.
  2. torchvision.transforms에서 제공해주는 augmentation 종류한정적입니다.

 

위와 같은 문제를 해결하기 위해 많은 분들이 albumentations 패키지를 사용하고 있습니다.

그럼 지금부터 albumentations 패키지를 사용하여 데이터를 로드하는 방식에 대해서 설명해보도록 하겠습니다.

 

필자는 현재 Visual Studio Code IDE (=VS Code)를 이용해 코딩을 하고 있는데, VS Code의 interpreter아나콘다(anaconda) 가상환경에 연동되어 있기 때문에, Albumentations 패키지설치하기 위해서 anaconda 명령어를 이용하도록 하겠습니다.

 

(↓↓↓아나콘다 가상환경 설명↓↓↓)

https://89douner.tistory.com/73?category=878197 

 

4. 아나콘다 가상환경 구축하기

안녕하세요~ 제가 이전글에서 했던 질문을 다시 가져와 볼게요. "여러분이 진행하는 프로젝트에서 딥러닝과 관련된 프로그램을 3개(A,B,C) 정도 사용한다고 했을때 여러분의 PC는 하나라고 가정

89douner.tistory.com

 

(↓↓↓아나콘다 가상환경과 VS code interpreter 연동방법↓↓↓)

https://89douner.tistory.com/74

 

5. 아나콘다 가상환경으로 tensorflow, pytorch 설치하기 (with VS code IDE, pycharm 연동)

안녕하세요~ 이번시간에는 아나콘다를 통해 2개의 가상환경을 만들고 각각의 가상환경에서 pytorch, tensorflow를 설치하는법을 배워볼거에요~ Pytorch: Python 3.7버전/ CUDA 10.1 버전/ Pytorch=1.4버전 Tensorf..

89douner.tistory.com

 

 

 

 

 

 

1. Albumentations 패키지 설치하기

앞서 필자VS code interpereter아나콘다 가상환경연동해서 사용하기 때문에, anaconda 명령어를 통해 albumentations 패키지설치할 것이라고 언급했습니다. 

 

Anaconda 명령어를 이용해 albumentations 패키지를 설치하는 방식은 아래사이트에서 확인할 수 있습니다.

https://anaconda.org/conda-forge/albumentations

 

Albumentations :: Anaconda.org

Fast image augmentation library and easy to use wrapper around other libraries

anaconda.org

 

(↓↓↓ albumentations 설치 명령어 ↓↓↓)

conda install -c conda-forge albumentations

 

anaconda prompt열고 위와 같이 설치 명령어입력한 후 설치를 진행해줍니다.

그림1

 

 

 

설치가 완료되면 아래와 같이 자신이 코드를 작성하고 있는 디렉토리에서 albumentations 모듈을 import해 관련 attribute or function들을 사용하면 됩니다. 그럼 지금부터 albumentations 모듈을 사용하여 데이터 로드 하는 방식을 설명해보도록 하겠습니다.

 

그림2

 

 

2. Albumentation 데이터 로드 코드

지금부터 설명하는 내용은 대부분 이전글 ("1-1. Data Load (Feat. torchvision transform)기반으로 달라진 부분들에 대해서만 설명하도록 하겠습니다. 다시 말해, albumentation 을 이용하여 데이터 로드를 할 때 torchvision transform 기반으로 데이터 로드를 하는 코드들 중 어느 부분을 수정하면 되는지 말씀드리겠습니다. (그러므로 이전 글을 읽어보시는걸 추천합니다)

 

(↓↓↓ 이전 글: torchvision.transform 기반 데이터 로드 방식↓↓↓)

https://89douner.tistory.com/299?category=1001221 

 

1-1. Data Load (Feat. torchvision transform)

안녕하세요. 이번 글에서는 "UNet (딥러닝 segmentation모델)" 학습을 위해 해당 모델에 입력으로 들어갈 데이터들이 어떤 과정을 통해 load 되는지 알아보도록 하겠습니다. 코드는 아래 사이트를 기반

89douner.tistory.com

 

먼저, 필자는 "alb_data_load.py, alb_train2.py"와 같이 파이썬 파일을 만들었습니다.

이 파일의 코드는 이전 글에서 설명한 코드들을 복사 붙여넣기 하여, albumentation을 적용하기 위해 수정된 최종 코드입니다.

 

[alb_data_laod.py]

import os
import numpy as np
import glob

import torch
import torch.nn as nn


## 데이터 로더를 구현하기
class Dataset(torch.utils.data.Dataset):
    def __init__(self, data_dir, transform=None):
        self.data_dir = data_dir
        self.transform = transform

        self.data_dir_input = self.data_dir + '/input'
        self.data_dir_label = self.data_dir + '/label'

        lst_data_input = os.listdir(self.data_dir_input)
        lst_data_label = os.listdir(self.data_dir_label)

        self.lst_label = lst_data_label
        self.lst_input = lst_data_input

    def __len__(self):
        return len(self.lst_label)

    def __getitem__(self, index):
        label = np.load(os.path.join(self.data_dir_label, self.lst_label[index]))
        input = np.load(os.path.join(self.data_dir_input, self.lst_input[index]))

        if label.ndim == 2:
            label = label[:, :, np.newaxis]
        if input.ndim == 2:
            input = input[:, :, np.newaxis]

        #data = {'input': input, 'label': label}

        if self.transform:
            data = self.transform(image=input, mask=label)
            data_img = data["image"]
            data_lab = data["mask"]

            data = {'input': data_img, 'label': data_lab}
            

        return data

 

 

[alb_train2.py]

import os
from albumentations.pytorch import transforms
import numpy as np

import torch
from torch._C import dtype
import torch.nn as nn
from torch.utils.data import DataLoader

from model import UNet
from alb_data_load import Dataset

import time

#from torchvision import transforms
import albumentations as A


import copy
from torchvision.utils import save_image

data_dir = 'data'
batch_size= 2


transform_train = A.Compose([
    A.HorizontalFlip(),
    A.VerticalFlip(), 
    A.Normalize(mean=0.5, std=0.5),
    transforms.ToTensorV2(transpose_mask=True)
    ])


transform_val = A.Compose([
    A.HorizontalFlip(), 
    A.Normalize(mean=0.5, std=0.5),
    transforms.ToTensorV2(transpose_mask=True)
    ])


dataset_train = Dataset(data_dir=os.path.join(data_dir, 'train'), transform=transform_train)
loader_train = DataLoader(dataset_train, batch_size=batch_size, shuffle=False, num_workers=0)

dataset_val = Dataset(data_dir=os.path.join(data_dir, 'val'), transform=transform_val)
loader_val = DataLoader(dataset_val, batch_size=batch_size, shuffle=False, num_workers=0)


# 그밖에 부수적인 variables 설정하기
num_data_train = len(dataset_train)
num_data_val = len(dataset_val)

num_batch_train = np.ceil(num_data_train / batch_size)
num_batch_val = np.ceil(num_data_val / batch_size)

device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")

## 네트워크 생성하기
net = UNet().to(device)

## 손실함수 정의하기
fn_loss = nn.BCEWithLogitsLoss().to(device)

## Optimizer 설정하기
optim = torch.optim.Adam(net.parameters(), lr=1e-3)

## 네트워크 학습시키기
st_epoch = 0
num_epoch = 30
# TRAIN MODE

def train_model(net, fn_loss, optim, num_epoch):
    since = time.time()

    best_model_wts = copy.deepcopy(net.state_dict())
    best_loss = 100

    for epoch in range(st_epoch + 1, num_epoch + 1):
        net.train()
        loss_arr = []

        for batch, data in enumerate(loader_train, 1):
            data['label'] = data['label']/255.0

            input = data['input']
            label = data['label']

            # forward pass
            label = data['label'].to(device)
            input = data['input'].to(device)

            output = net(input)

            # backward pass
            optim.zero_grad()

            loss = fn_loss(output, label)
            loss.backward()

            optim.step()

            # 손실함수 계산
            loss_arr += [loss.item()]

            print("TRAIN: EPOCH %04d / %04d | BATCH %04d / %04d | LOSS %.4f" %
                (epoch, num_epoch, batch, num_batch_train, np.mean(loss_arr)))


        with torch.no_grad():
            net.eval()
            loss_arr = []

            for batch, data in enumerate(loader_val, 1):
                data['label'] = data['label']/255.0

                # forward pass
                label = data['label'].to(device, dtype=torch.float32)
                input = data['input'].to(device, dtype=torch.float32)

                output = net(input)

                # 손실함수 계산하기
                loss = fn_loss(output, label)

                loss_arr += [loss.item()]

                print("VALID: EPOCH %04d / %04d | BATCH %04d / %04d | LOSS %.4f" %
                        (epoch, num_epoch, batch, num_batch_val, np.mean(loss_arr))) 

            epoch_loss = np.mean(loss_arr)

            # deep copy the model
            if epoch_loss < best_loss:
                best_loss = epoch_loss
                best_model_wts = copy.deepcopy(net.state_dict())

        print()
    
    time_elapsed = time.time() - since
    print('Training complete in {:.0f}m {:.0f}s'.format(
        time_elapsed // 60, time_elapsed % 60))
    print('Best val loss: {:4f}'.format(best_loss))

    # load best model weights
    net.load_state_dict(best_model_wts)
    return net

model_ft = train_model(net, fn_loss, optim, num_epoch)
torch.save(model_ft.state_dict(), './model_log/model_weights.pth')

 

그럼 위의 코드와 이전 글의 코드를 비교하면서 설명을 해보도록 하겠습니다.

 

 

 

 

 

3. Import

먼저 이전 글에서 추가할 import 부분에 대해서 설명하겠습니다.

  1. alb_data_load.py 라는 새로운 데이터 로드 파일을 만들어 주었으므로 alb_data_load의 Dataset을 import 합니다.
    • from alb_data_load import Dataset
  2. 설치한 albumentaion 관련 모듈을 import 해줍니다.
    • import albumentations as A
    • from albumentations.pytorch import transfroms → 아래 "그림3"에서 transfor_train 부분을 보면 마지막에 ToTensorV2가 구현된 것을 볼 수 있습니다. ToTensorV2만 "albumentations.pytorch"로부터 import 한다는 걸 인지하세요!

그림3

 

 

 

 

4. alb_data_load.py 변경

4-1. albumentation.transform(image, mask)

먼저, 이전 글에서 설명한 torchvision.transform인자형태를 보도록 하겠습니다. (아래 "4그림")

transform(data['input']), transform(data['label']) 이렇게 transform에는 하나의 리스트 인자(argument)만 받을 수 있게 되어 있습니다 (이러한 부분 때문에 torch.manual_seed()를 사용했죠 ← 자세한 설명은 이전 글 참고!)

그림4

 

그렇다면, albumentation에서 제공해주는 transform을 이용하면 위의 부분(="그림4"의 빨간색 박스)이 어떻게 바뀔 수 있을까요?

 

albumentation에서 제공해주는 transform두 개의 리스트 인자를 받을 수 있게 되어 있습니다. (아래 "그림5")

그래서 training image, training label 동시에 넘겨줄 수 있기 때문에 따로 seed를 고정시켜줄 필요가 없습니다. (실제로 data_img, data_lab 데이터를 10번 정도 이미지화해서 살펴봐도 augmentation이 동일하게(일치하게) 적용되는 것을 확인할 수 있었습니다)

그림5

 

 

 

 

 

5. alb_train2.py 변경

5-1. transform.Compose → A.Compose

(이전 글에서 봤듯이) transform.Compose에 구현된 augmentation을 적용하기 위해 입력되는 데이터 형식은 numpy입니다. 

그림6

 

그림7

 

 

먼저, 이전 글에서 사용했던 torchvision.transform.Compose구현순서를 살펴보겠습니다. (아래 "그림8")

torchvision.transform.Compose에서 제공하는 augmentation (ex: RandomHorizontalFlip(), etc..) 을 적용하기 위해서는 PIL 타입의 데이터입력되어야 합니다. 그래서 아래와 같이 "transforms.ToPILImage()" 를 먼저 수행시켜주어야 합니다. 그리고, PIL 타입을 torch tensor 타입으로 변경시켜준 후 (by "ToTensor()"), Normalize() 작업을 진행해줍니다.

그림8

 

 

그렇다면, albumentation.transform.Compose에서는 어떤 순서로 구성되는지 알아볼까요? (아래 "그림9")

우선 numpy 형식으로 입력되는 데이터를 PIL 형식으로 변경해줄 필요가 없기 때문에 "ToPILImage()"를 사용할 필요 없습니다. 그리고, torch tensor 형태로 변경하기 전에 먼저 Normalize를 적용해주네요. 그리고, ToTensorV2를 적용해줍니다.

 

그림9

 

여기서 좀 더 보충해서 설명해야할 부분이 Normalize(), ToTensorV2() 입니다.

 

 

5-2. Normalize()

먼저, 아래 "그림10"처럼 breakpoint를 걸어주고 "alb_train2.py"를 실행시켜봅시다.

 

그림10

"그림10"처럼 디버깅을 하면 data_img, data_lab 값을 살펴볼 수 있습니다. 

그런데, data_img에는 Normalize가 적용이 안되어 있습니다.

 

이러한 사실로 볼때 albumentation.transform.Compose에 적용되는 augmentation 중에 Normalize()label(=mask)에 적용되지 않는 듯합니다.

 

 

 

5-3. ToTensorV2()

이전 글에서 torchvision.transform.ToTensor()는 아래와 같은 기능을 한다고 했습니다.

  • Converts a PIL Image or numpy.ndarray (H x W x C) in the range [0, 255] to a torch.FloatTensor of shape (C x H x W) in the range [0.0, 1.0] if the PIL Image belongs to one of the modes (L, LA, P, I, F, RGB, YCbCr, RGBA, CMYK, 1) or if the numpy.ndarray has dtype = np.uint8

 

그런데, albumentation.transform.ToTensorV2() 결과 label의 타입이 numpy (=H x W x C) 에서 torch tensor 타입 (=C x H x W)으로 바뀌었지만, range는 그대로 0~255인 것을 확인할 수 있습니다.  (아래 "그림11")

 

그림11

 

 

이유를 아래 "albumentations.pytorch.transforms.ToTensorV2()" API를 살펴본 후 알 수 있었습니다.

https://albumentations.ai/docs/api_reference/pytorch/transforms/

 

Albumentations Documentation - Transforms (pytorch.transforms)

Albumentations: fast and flexible image augmentations

albumentations.ai

 

 

쉽게 말해 albumentation 패키지 version 0.5.2 이후 부터는 255로 나누어주어 range0~1변경해주는 기능제거된다. 이러한 사실통해 살펴 볼때, 앞서 "data_img" 값들이 0~1로 범위가 변경된 이유는 Normalize()해당 기능(← 값의 범위를 0~1로 변경해주는 기능)이 들어있기 때문인듯 합니다. (앞서 albumentation.transforms.Normalize()는 label이 아닌 image에만 적용된다고 언급했습니다)

그림12

 

하지만, label(=mask)에 해당하는 값이 loss function(=crossentropy)의 인자 값으로 들어가기 위해서는 label이 0 or 1의 값을 갖아야 합니다. 즉, label 데이터 값의 255를 1로 변경해주어야 하는 것이죠 (or labeling smoothing을 적용하려면 label의 값의 범위가 0~1 사이로 변경되어야겠죠?)

 

 

이 부분은 간단하게 구현해줄 수 있습니다.

그냥 "alb_train2.py"에서 아래와 같이 data['label']을 255로 나누어주면 됩니다.

그림13

 

 

[주의사항]

아래와 같이 "ToTensorV2()"에 transpose_mask 부분을 명시해주지 않으면 False 값이 default가 됩니다.

 

그림14

 

 

위와 같이 코드를 실행 시키면 torch tensor 형식(=C x H x W)이 아닌  (H x W x C) 형식인걸 알 수 있습니다.

그림15

물론 (H x W x C) 구조를 permute()을 이용해 쉽게  (C x H x W) 구조로 변경 가능하지만, 그냥 ToTensorV2(transpose_mask=True)를 해주면 자동으로 구조변경이 된다는 점을 알아두시면 좋을 듯 합니다.

 

(↓↓permute() 사용법 ↓)

https://devbruce.github.io/machinelearning/ml-05-np_torch_summary/

 

[ML] Numpy & PyTorch Summary

 

devbruce.github.io

 

 

 

6. albumentation 응용

albumentation.transform.Compose 내부에 적용되는 augmetation 조합은 굉장히 다양하게 가져갈 수 있습니다.

방법은 아래 사이트의 "albumentations 응용 사례" 부분을 참고해주세요!

 

https://hoya012.github.io/blog/albumentation_tutorial/

 

albumentations - fast image augmentation library 소개 및 사용법 Tutorial

image augmentation library인 albumentations에 대한 소개와 사용 방법을 Tutorial로 정리해보았습니다.

hoya012.github.io

 

 

 

 

 

 

 

지금까지 albumentations 패키지를 이용한 segmentation 데이터 로드 코드를 알아보았습니다.

감사합니다.

안녕하세요.

이번 글에서는 "UNet (딥러닝 segmentation모델)" 학습을 위해 해당 모델입력으로 들어갈 데이터들이 어떤 과정을 통해 load 되는지 알아보도록 하겠습니다. 

 

코드는 아래 사이트를 기반으로 수정하였으니 아래 영상을 먼저 참고하시면 글을 이해하시는데 도움이 될 것으로 생각됩니다.

 

https://www.youtube.com/watch?v=1gMnChpUS9k 

 

 

위의 강의에서는 augmentation 부분을 직접구현해주었는데, 이번 글에서는 torchvision에서 augmentation을 위해 제공해주는 torchvision.transform 모듈을 적용하여 data load 하는 내용을 설명하려고 합니다.

 

 

 

[data_load.py]

import os
import numpy as np
import glob

import torch
import torch.nn as nn


## 데이터 로더를 구현하기
class Dataset(torch.utils.data.Dataset):
    def __init__(self, data_dir, transform=None, seed=None):
        self.data_dir = data_dir
        self.transform = transform
        self.seed = seed

        self.data_dir_input = self.data_dir + '/input'
        self.data_dir_label = self.data_dir + '/label'

        lst_data_input = os.listdir(self.data_dir_input)
        lst_data_label = os.listdir(self.data_dir_label)

        self.lst_label = lst_data_label
        self.lst_input = lst_data_input

    def __len__(self):
        return len(self.lst_label)

    def __getitem__(self, index):
        label = np.load(os.path.join(self.data_dir_label, self.lst_label[index]))
        input = np.load(os.path.join(self.data_dir_input, self.lst_input[index]))

        if label.ndim == 2:
            label = label[:, :, np.newaxis]
        if input.ndim == 2:
            input = input[:, :, np.newaxis]

        data = {'input': input, 'label': label}

        if self.transform: 
            torch.manual_seed(self.seed)
            data['input'] = self.transform(data['input'])
            
        if self.transform:   
            torch.manual_seed(self.seed)
            data['label'] = self.transform(data['label'])

        return data

 

 

[train.py]

import os
import numpy as np

import torch
import torch.nn as nn
from torch.utils.data import DataLoader

from model import UNet
from data_load import *

import time

from torchvision import transforms
import copy
from torchvision.utils import save_image

data_dir = 'data'
batch_size= 2
data_load_seed = 10

transform_train = transforms.Compose([
    transforms.ToPILImage(), 
    transforms.RandomHorizontalFlip(),
    transforms.RandomVerticalFlip(), 
    transforms.ToTensor(),
    transforms.Normalize(mean=0.5, std=0.5)])


transform_val = transforms.Compose([
    transforms.ToPILImage(), 
    transforms.RandomHorizontalFlip(), 
    transforms.ToTensor(),
    transforms.Normalize(mean=0.5, std=0.5)
    ])


dataset_train = Dataset(data_dir=os.path.join(data_dir, 'train'), transform=transform_train, seed=data_load_seed)
loader_train = DataLoader(dataset_train, batch_size=batch_size, shuffle=False, num_workers=0)

dataset_val = Dataset(data_dir=os.path.join(data_dir, 'val'), transform=transform_val, seed=data_load_seed)
loader_val = DataLoader(dataset_val, batch_size=batch_size, shuffle=False, num_workers=0)


# 그밖에 부수적인 variables 설정하기
num_data_train = len(dataset_train)
num_data_val = len(dataset_val)

num_batch_train = np.ceil(num_data_train / batch_size)
num_batch_val = np.ceil(num_data_val / batch_size)

device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")

## 네트워크 생성하기
net = UNet().to(device)

## 손실함수 정의하기
fn_loss = nn.BCEWithLogitsLoss().to(device)

## Optimizer 설정하기
optim = torch.optim.Adam(net.parameters(), lr=1e-3)

## 네트워크 학습시키기
st_epoch = 0
num_epoch = 30
# TRAIN MODE

def train_model(net, fn_loss, optim, num_epoch):
    since = time.time()

    best_model_wts = copy.deepcopy(net.state_dict())
    best_loss = 100

    for epoch in range(st_epoch + 1, num_epoch + 1):
        net.train()
        loss_arr = []

        for batch, data in enumerate(loader_train, 1):
            data['label'] = data['label']*0.5+0.5 #denormalization -> X*std+mean
            label = data['label']
            input = data['input']

            # first_batch_input = input[0]*0.5+0.5
            # save_image(first_batch_input, 'first_batch_input.jpg')

            # first_batch_label = label[0]
            # save_image(first_batch_label, 'first_batch_label.jpg')

            # forward pass
            label = data['label'].to(device)
            input = data['input'].to(device)

            output = net(input)

            # backward pass
            optim.zero_grad()

            loss = fn_loss(output, label)
            loss.backward()

            optim.step()

            # 손실함수 계산
            loss_arr += [loss.item()]

            print("TRAIN: EPOCH %04d / %04d | BATCH %04d / %04d | LOSS %.4f" %
                (epoch, num_epoch, batch, num_batch_train, np.mean(loss_arr)))


        with torch.no_grad():
            net.eval()
            loss_arr = []

            for batch, data in enumerate(loader_val, 1):
                data['label'] = data['label']*0.5+0.5 #denormalization -> X*std+mean

                # forward pass
                label = data['label'].to(device)
                input = data['input'].to(device)

                output = net(input)

                # 손실함수 계산하기
                loss = fn_loss(output, label)

                loss_arr += [loss.item()]

                print("VALID: EPOCH %04d / %04d | BATCH %04d / %04d | LOSS %.4f" %
                        (epoch, num_epoch, batch, num_batch_val, np.mean(loss_arr))) 

            epoch_loss = np.mean(loss_arr)

            # deep copy the model
            if epoch_loss < best_loss:
                best_loss = epoch_loss
                best_model_wts = copy.deepcopy(net.state_dict())

        print()
    
    time_elapsed = time.time() - since
    print('Training complete in {:.0f}m {:.0f}s'.format(
        time_elapsed // 60, time_elapsed % 60))
    print('Best val loss: {:4f}'.format(best_loss))

    # load best model weights
    net.load_state_dict(best_model_wts)
    return net

model_ft = train_model(net, fn_loss, optim, num_epoch)
torch.save(model_ft.state_dict(), './model_log/model_weights.pth')

 

에 있는 코드에서 핵심적인 코드 또는 수정한 코드에 대해서만 설명 하도록 하겠습니다 (좀 더 구체적인 설명을 듣고 싶으신 분은 에 링크를 걸어둔 유튜브 강의를 참고해주시면 감사하겠습니다)

 

 

 

 

0.Dataset 클래스

from torchvision import transforms

transform_train = transforms.Compose([
    transforms.ToPILImage(), 
    transforms.RandomHorizontalFlip(),
    transforms.RandomVerticalFlip(), 
    transforms.ToTensor(),
    transforms.Normalize(mean=0.5, std=0.5)])
    
dataset_train = Dataset(data_dir=os.path.join(data_dir, 'train'), transform=transform_train)
loader_train = DataLoader(dataset_train, batch_size=batch_size, shuffle=True, num_workers=0)

 

Dataset()에 넘겨주는 인자를 보면 두 가지 입니다.

  1. train 디렉토리
  2. 적용할 augmentation 기법들 → transform

그럼 Dataset에 구현되있는 부분 중에 train 디렉토리에 해당하는 부분들을 살펴보도록 하겠습니다.

 

 

 

 

 

 

 

1. train 디렉토리와 관련된 부분

## 데이터 로더를 구현하기
class Dataset(torch.utils.data.Dataset):
    def __init__(self, data_dir, transform=None):
        self.data_dir = data_dir
        self.transform = transform

        self.data_dir_input = self.data_dir + '/input'
        self.data_dir_label = self.data_dir + '/label'

        lst_data_input = os.listdir(self.data_dir_input)
        lst_data_label = os.listdir(self.data_dir_label)

        self.lst_label = lst_data_label
        self.lst_input = lst_data_input

    def __len__(self):
        return len(self.lst_label)

    def __getitem__(self, index):
        label = np.load(os.path.join(self.data_dir_label, self.lst_label[index]))
        input = np.load(os.path.join(self.data_dir_input, self.lst_input[index]))

        if label.ndim == 2:
            label = label[:, :, np.newaxis]
        if input.ndim == 2:
            input = input[:, :, np.newaxis]

        data = {'input': input, 'label': label}

 

위의 코드를 살펴보면 train 디렉토리 Dataset인자로 넘어가게 되면 아래 그림1에 있는 디렉토리numpy 데이터접근한다는 것을 확인할 수 있습니다.

 

그림1

 

 

실제로 breakpiont를 통해 살펴보면 input, label 이라는 변수에 모든 numpy 데이터들이 리스트 형태로 저장되는 것을 확인할 수 있습니다.

그림2

 

 

 

앞선 글에서 저장된 데이터의 형태가 (H,W) 임을 확인할 수 있었습니다.

또한, 딥러닝 모델학습을 위해서 (H,W)2차원 구조(H,W,C)3차원 구조변경되어야 한다고도 말씀드렸습니다.

 

(↓↓ 아래 글에서 "2-3) numpy 형태로 저장하는 이유" 부분을 참고해주세요↓)

https://89douner.tistory.com/298?category=1001221 

 

0.DataSet 마련하기 (Feat. ISBI 2012 EM segmentation)

안녕하세요. 이번 글에서는 Segmentation을 하기 위해 데이터셋을 어떻게 세팅해놓는지에 대해 설명하려고 합니다. 코드는 아래 사이트를 기반으로 수정하였으니 아래 영상을 먼저 참고하시면 글

89douner.tistory.com

 

(아래 "그림3"처럼 shape 부분을 살펴보면 label, input 변수에 저장된 데이터는 numpy형식의2차원 데이터입니다.)

그림3

 

 

위와 같은 2차원 구조 3차원으로 늘려주기 위해 아래 코드 실행됩니다.

 

[data_load.py]

        if label.ndim == 2:
            label = label[:, :, np.newaxis]
        if input.ndim == 2:
            input = input[:, :, np.newaxis]

 

 

 

 

 

 

 

 

2. torchvision.transform 

앞서 설명드린 내용아래 코드에서 'if self.transform' 직전까지 부분입니다.

## 데이터 로더를 구현하기
class Dataset(torch.utils.data.Dataset):
    def __init__(self, data_dir, transform=None):
        self.data_dir = data_dir
        self.transform = transform

        self.data_dir_input = self.data_dir + '/input'
        self.data_dir_label = self.data_dir + '/label'

        lst_data_input = os.listdir(self.data_dir_input)
        lst_data_label = os.listdir(self.data_dir_label)

        self.lst_label = lst_data_label
        self.lst_input = lst_data_input

    def __len__(self):
        return len(self.lst_label)

    def __getitem__(self, index):
        label = np.load(os.path.join(self.data_dir_label, self.lst_label[index]))
        input = np.load(os.path.join(self.data_dir_input, self.lst_input[index]))

        if label.ndim == 2:
            label = label[:, :, np.newaxis]
        if input.ndim == 2:
            input = input[:, :, np.newaxis]

        data = {'input': input, 'label': label}

        if self.transform:
            torch.manual_seed(10)
            data['input'] = self.transform(data['input'])
            
        if self.transform: 
            torch.manual_seed(10)
            data['label'] = self.transform(data['label'])

        return data

 

 

그렇다면, 지금부터는 self.transform관련내용에 대해서 설명하도록 하겠습니다.

self.transform 부분Dataset 클래스에서 두 번째로 살펴볼 인자transform과 관련이 있습니다.

 

(transformtorchvision에서 제공해주는 모듈로 사용하고 있습니다.)

from torchvision import transforms

transform_train = transforms.Compose([
    transforms.ToPILImage(), 
    transforms.RandomHorizontalFlip(),
    transforms.RandomVerticalFlip(), 
    transforms.ToTensor(),
    transforms.Normalize(mean=0.5, std=0.5)])
    
dataset_train = Dataset(data_dir=os.path.join(data_dir, 'train'), transform=transform_train)
loader_train = DataLoader(dataset_train, batch_size=batch_size, shuffle=True, num_workers=0)

 

 

2-1) transforms.ToPILImage()

우선 위의 코드에서 transforms.Compose 부분을 살펴보겠습니다.

 

현재 아래코드가 실행되기 직전data['input']에 들어 있는 데이터 구조3차원 (H,W,C) 형태로 변경된 numpy 데이터 입니다.

 

        if self.transform:
            torch.manual_seed(10)
            data['input'] = self.transform(data['input'])

 

그리고, self.transform(data['input'])실행되면 transform.Compose에 적힌 순서대로 데이터의 변화가 일어납니다.

 

먼저, RandomHorizontalFlip과 같이 torchvision에서 제공해주는 augmentation 기법을 사용하기 위해서는 현재 numpy 형식의 데이터가 PIL 이미지 형식의 데이터가 입력값으로 들어와야 합니다.

 

그렇기 때문에 transform.Compose 부분에서 제일 처음으로 "transforms.ToPILImage()"를 작성해줍니다. 

 

 

[주의사항1]

만약, ToPILImage() 부분을 작성하지 않으면 아래와 같은 에러가 발생합니다.

에러 메시지 내용은 아래와 같습니다.

 

"현재 입력 받은 이미지의 형식은 numpy.ndarray 이니까 (="Got <class 'numpy.ndarray'>),  PIL 형태로 바꿔주어야 합니다(="img should be PIL Image")"

 

그림4

 

 

 

 

2-2) transforms.ToTensor, transforms.Normalize 위치

Pytorch에서 제공해주는 transforms.Normalize()를 사용하려면 항상 transforms.ToTensor() 이후에 위치해야 합니다.  

 

이렇게 위치시켜야하는 이유는 Normalize()torch tensor 형식입력으로 받기 때문입니다.

 

아래 코드를 기반으로 설명하면, RandomVerticalFlip까지 적용된 데이터 형식은 PIL image 형식 (512x 512x 1) 인데, 이것을 Normalize가 적용되려면 (1x 512x 512) 형식으로 변경되어야 합니다 (pixel range0~1변경되어야 합니다).   

 

  • ToTensor(): Converts a PIL Image or numpy.ndarray (H x W x C) in the range [0, 255] to a torch.FloatTensor of shape (C x H x W) in the range [0.0, 1.0]
from torchvision import transforms

transform_train = transforms.Compose([
    transforms.ToPILImage(), 
    transforms.RandomHorizontalFlip(),
    transforms.RandomVerticalFlip(), 
    transforms.ToTensor(),
    transforms.Normalize(mean=0.5, std=0.5)])
    
dataset_train = Dataset(data_dir=os.path.join(data_dir, 'train'), transform=transform_train)
loader_train = DataLoader(dataset_train, batch_size=batch_size, shuffle=True, num_workers=0)

 

[주의사항2]

만약, 순서를 아래와 같이 위치시켰다면 에러 메시지가 출력됩니다.

  • transforms.Normalize
  • transforms.ToTensor

에러 메시지의 내용은 다음과 같습니다.

 

"현재 입력 받은 이미지의 형식은 PIL기반 tensor이니까 (="Got <class 'PIL.Image.Image'>),  PIL 형태로 바꿔주어야 합니다(="tensor should be a torch tensor")"

 

그림5

 

 

 

 

 

 

2-3. input, label에 적용되는 transform seed 고정해주기

아래 코드를 살펴보면 "torch.manual_seed(10)"이라는 것이 있습니다.

 

[data_load.py]

        if self.transform:  
            torch.manual_seed(10)
            data['input'] = self.transform(data['input'])
            
        if self.transform:
            torch.manual_seed(10)
            data['label'] = self.transform(data['label'])

 

 

 

"torch.manual_seed(10)" 코드를 추가하는 이유를 설명하기 위해, "torch.manual_seed(10)"가 없을 때 일어나는 일에 대해서 알아보도록 하겠습니다.

 

우선 "data_load.py" 부분에서 "torch.manual_seed(10)" 코드를 주석 처리해보겠습니다.

        if self.transform:  
            #torch.manual_seed(10)
            data['input'] = self.transform(data['input'])
            
        if self.transform:
            #torch.manual_seed(10)
            data['label'] = self.transform(data['label'])

 

 

 

그리고, "train.py" 부분에서 딥러닝 모델입력되기 직전이미지 상태를 알아보도록 하겠습니다.

 

이전 transforms.Compose에서 input, label에 모두 transforms.Normalize가 적용이 됐다는걸 알 수 있습니다.

 

그런데, 생각해보면 label에는 Normalize 적용이 돼서는 안되겠죠? (label에는 0 or 1 값만 들어 있어야 되는데 앞서 mean, std를 이용해 normalize를 하면 1 or -1 값을 갖게됩니다. 하지만, CrossEntropy loss function이 받는 label 값들은 0 or 1 이어야 하죠)

 

그러므로, 가장 먼저 수정해주어야 하는 부분이 normalize가 적용된 label 이미지 데이터들을 다시 denormalize 해주어야 한다는 점입니다. 그래서 아래 "data['label']*0.5+0.5" 부분이 추가가 되었습니다.

 

[train.py]

      for batch, data in enumerate(loader_train, 1):
            data['label'] = data['label']*0.5+0.5 #denormalization -> X*std+mean
            label = data['label']
            input = data['input']

 

 

다음으로는 딥러닝 모델에 들어가기 직전데이터들을 저장해서 보겠습니다.

먼저, 아래 부분(89번 line)에 breakpoint를 걸어주어 input 데이터shape을 살펴보면, (batch, Channel, Height, Width)와 같은 형태로 구성되어 있는걸 확인하실 수 있습니다. 

 

그림6

 

 

그럼 input, label의 각각 첫 번째 batch 이미지를 따로 저장시킬 코드를 추가하겠습니다.

 

우선 input 데이터에서도 normalize가 적용된 상태이기 때문에 denormalize를 해줍니다. (←"input[0]*0.5+0.5")

 

그리고 첫 번째 batch 이미지에 해당하는 데이터 값은 "input[0]*0.5+0.5", "label[0]"인데, 현재 데이터 형식은 torch tensor입니다.

 

torch tensor 형식에서 곧 바로 이미지저장하기 위해서는 "torchvision.utils"에서 제공하는 save_image()를 이용하면 됩니다.

      for batch, data in enumerate(loader_train, 1):
            data['label'] = data['label']*0.5+0.5 #denormalization -> X*std+mean
            label = data['label']
            input = data['input']
			
            #################코드가 추가된 부분###################
            first_batch_input = input[0]*0.5+0.5
            save_image(first_batch_input, 'first_batch_input.jpg')

            first_batch_label = label[0]
            save_image(first_batch_label, 'first_batch_label.jpg')
            #######################################################

 

 

코드를 실행하고 저장된 이미지를 살펴보겠습니다.

input 데이터의 이미지label 데이터의 이미지일치하지 않는게 보이시나요? 어느 한쪽이 flip이 안 됐다는 정도는 파악할 수 있을겁니다.

그림7

 

 

이렇게 나오는 이유를 찾기 위해 transforms.RandomHorizontalFlip 코드를 살펴보았습니다.

(↓↓↓transforms.RandomHorizontalFlip API ↓↓↓)

https://pytorch.org/vision/stable/_modules/torchvision/transforms/transforms.html#RandomHorizontalFlip

 

torchvision.transforms.transforms — Torchvision 0.10.0 documentation

Shortcuts

pytorch.org

 

해당 API를 차례대로 살펴보겠습니다.

그림8

 

앞서 input, label서로다른 augmenation이 적용된 이유torch.rand() 때문입니다. 왜 torch.rand() 때문이었는지 좀 더 자세히 설명해보도록 하겠습니다.

 

 

 torch.rand()이라는 함수에 대한 설명은 아래와 같습니다.

  • Returns a tensor filled with random numbers from a uniform distribution on the interval [0, 1)

(↓↓torch.rand↓)

https://pytorch.org/docs/stable/generated/torch.rand.html

 

torch.rand — PyTorch 1.9.0 documentation

Shortcuts

pytorch.org

 

 

 

torch.rand(1)은 데이터가 1차원 형태이며 0~1 사이 중 하나를 출력한다는 뜻입니다. 

만약, 4차원 형태를 나타내려면 아래와 같이 코딩해주면 됩니다.

그림9

 

위의 코드를 결과를 살펴보면 torch.rand실행시켜 줄 때 마다 난수발생하기 때문에 a, b에 들어가는 들이 전부다른걸 보실 수 있습니다.

 

 

위와 같은 사실기반으로 "data_load.py"에 구현되어 있는 아래 코드를 살펴보겠습니다.

우선 data['input']에 해당하는 데이터에 transform이 진행되는 과정을 살펴보겠습니다. → self.transform(data['input'])

        if self.transform:  
            #torch.manual_seed(10)
            data['input'] = self.transform(data['input'])

 

앞서 transforms.Compose에 구현된 것 중하나가 RandomHorizontalFlip()인데, 이 부분이 아래와 같은 코드를 기반으로 수행이 될 겁니다. 그런데 보면, torch.rand(1)를 통해 난수가 발생하는 걸 볼 수 있죠? 만약 여기서 0.3이라는 생성되면 "p=0.5" 기준에 의해 RandomHorizontalFlip() 방식의 augmentation이 진행되지 않을 것입니다.

그림8

 

 

이때 label 데이터에서도 RandomHorzontalFlip()적용이 되는데 (by "self.transform(data['label'])"),  torch.rand(1)에서 생성(=난수)가 0.6이면, label에는 RandomHorzontalFlip() 적용되게 됩니다.

        if self.transform:  
            #torch.manual_seed(10)
            data['input'] = self.transform(data['input'])
            
        if self.transform:
            #torch.manual_seed(10)
            data['label'] = self.transform(data['label'])

 

그래서 "그림7"과 같은 결과를 보이게 됩니다.

 

 

 

 

이러한 문제 해결하기 위해서는 "torch.rand(1)"를 통해 생성되는 난수고정시켜주어야 합니다.

난수를 고정시키는 방법은 간단합니다. 아래와 "그림10"처럼 난수가 생성되기 전에  seed 값고정시켜주면 됩니다. 그러면, torch.rand()를 통해 생겨나는 난수 값들이 고정됩니다.

그림10

 

 

위와 같은 방식을 통해 self.transform(data['input']), self.trasnform(data['label'])에서 augmentation 시, 발생되는 난수 값이 (by "torch.rand()") 동일해집니다.  즉, input, label 모두 동일한(일치한) augmentation을 제공해주게 됩니다.

        if self.transform:  
            torch.manual_seed(10)
            data['input'] = self.transform(data['input'])
            
        if self.transform:
            torch.manual_seed(10)
            data['label'] = self.transform(data['label'])

 

 

 

 

 

3. freeze_support() 에러

데이터 로드와 관련된 모든 준비가 완료되었습니다.

그럼 "train.py" 코드를 실행해보죠.

 

만약 앞서 제가 설명드린 코드가 아닌 유튜브 강의에서 설명한 코드행했을 때 리눅스에서 실행하셨다면 큰 문제없이 실행됐겠지만, 만약 윈도우에서 실행시키셨다면 아래와 같은 에러 메시지를 만나실 수 있습니다.

 

그림11

 

 

아래 코드를 한 번 살펴보겠습니다. 

DataLoader에 num_workers가 보이시나요?

from torchvision import transforms

transform_train = transforms.Compose([
    transforms.ToPILImage(), 
    transforms.RandomHorizontalFlip(),
    transforms.RandomVerticalFlip(), 
    transforms.ToTensor(),
    transforms.Normalize(mean=0.5, std=0.5)])
    
dataset_train = Dataset(data_dir=os.path.join(data_dir, 'train'), transform=transform_train)
loader_train = DataLoader(dataset_train, batch_size=batch_size, shuffle=True, num_workers=0)

 

num_workersGPU학습 이미지업로드하기 위해서 사용되는 CPUprocess 개수입니다. 

 

쉽게 말해 GPU 학습 이미지를 업로드 하기 위해서는 결국 CPU가 중간 다리 역할을 해줘야 하는데, 이때 num_workers를 크게 설정해주면 GPU에 학습 이미지를 업로드하는데 관여하는 process도 많아지겠죠. 이렇게 되면 결국 학습 이미지 업로드 속도도 빨라질겁니다.

 

(↓↓↓num_workers에 대해서 설명한 글↓↓↓)

https://89douner.tistory.com/287?category=994842 

 

1. Data Load (Feat. CUDA)

안녕하세요. 이번 글에서는 CNN 모델 학습을 위해 학습 데이터들을 로드하는 코드에 대해 설명드리려고 합니다. 아래 사이트의 코드를 기반으로 설명드리도록 하겠습니다. https://pytorch.org/tutorials

89douner.tistory.com

 

 

 

앞서 "그림11"에서 설명한 에러가 발생하는 이유는 아래의 사이트에서 설명하고 있습니다.

https://pytorch.org/docs/stable/notes/windows.html#usage-multiprocessing

 

Windows FAQ — PyTorch 1.9.0 documentation

Shortcuts

pytorch.org

 

요약해 설명하자면 "Dataload"를 수행시키기 위해서는 multi-process (by "num_workers")를 이용하는데, 이것이 리눅스가 아닌 window에서 사용하기 위해서는 아래와 같이 특정 작업을 해주어야 한다고 합니다.

그림12

 

 

3-1) 첫 번째 에러 수정 방법

첫 번째 방식은 매우 간단합니다.

그냥 num_workers 부분을 0으로 세팅해주면 됩니다.

그림13

 

 

3-1) 두 번째 에러 수정 방법

두 번째 에러 수정 방법은 "그림11"에서 제안한대로 코드를 변경시켜 주면 됩니다.

 

아래 그림14처럼 "train.py"에서 "def train()"함수 부분을 만들어주고 (←유튜브 강의에서는 train()함수를 따로 정의하진 않고 있습니다) training에 해당되는 코드옮겨줍니다. 그리고, 마지막 코드에 추가로 main관련 코드를 작성해줍니다. 

 

import os
import numpy as np

import torch
import torch.nn as nn
from torch.utils.data import DataLoader

from model import UNet
from data_load import *

import time

from torchvision import transforms
import albumentations as A

import copy
from torchvision.utils import save_image

data_dir = 'data'
batch_size= 2


transform_train = transforms.Compose([
    transforms.ToPILImage(), 
    transforms.RandomHorizontalFlip(),
    transforms.RandomVerticalFlip(), 
    transforms.ToTensor(),
    transforms.Normalize(mean=0.5, std=0.5)])


transform_val = transforms.Compose([
    transforms.ToPILImage(), 
    transforms.RandomHorizontalFlip(), 
    transforms.ToTensor(),
    transforms.Normalize(mean=0.5, std=0.5)
    ])


dataset_train = Dataset(data_dir=os.path.join(data_dir, 'train'), transform=transform_train)
loader_train = DataLoader(dataset_train, batch_size=batch_size, shuffle=True, num_workers=2)

dataset_val = Dataset(data_dir=os.path.join(data_dir, 'val'), transform=transform_val)
loader_val = DataLoader(dataset_val, batch_size=batch_size, shuffle=False, num_workers=2)


# 그밖에 부수적인 variables 설정하기
num_data_train = len(dataset_train)
num_data_val = len(dataset_val)

num_batch_train = np.ceil(num_data_train / batch_size)
num_batch_val = np.ceil(num_data_val / batch_size)

device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")

## 네트워크 생성하기
net = UNet().to(device)

## 손실함수 정의하기
fn_loss = nn.BCEWithLogitsLoss().to(device)

## Optimizer 설정하기
optim = torch.optim.Adam(net.parameters(), lr=1e-3)

## 네트워크 학습시키기
st_epoch = 0
num_epoch = 30
# TRAIN MODE

def train_model(net, fn_loss, optim, num_epoch):
    since = time.time()

    best_model_wts = copy.deepcopy(net.state_dict())
    best_loss = 100

    for epoch in range(st_epoch + 1, num_epoch + 1):
        net.train()
        loss_arr = []

        for batch, data in enumerate(loader_train, 1):
            data['label'] = data['label']*0.5+0.5 #denormalization -> X*std+mean
            label = data['label']
            input = data['input']

            # first_batch_input = input[0]*0.5+0.5
            # save_image(first_batch_input, 'first_batch_input.jpg')

            # first_batch_label = label[0]
            # save_image(first_batch_label, 'first_batch_label.jpg')

            # forward pass
            label = data['label'].to(device)
            input = data['input'].to(device)

            output = net(input)

            # backward pass
            optim.zero_grad()

            loss = fn_loss(output, label)
            loss.backward()

            optim.step()

            # 손실함수 계산
            loss_arr += [loss.item()]

            print("TRAIN: EPOCH %04d / %04d | BATCH %04d / %04d | LOSS %.4f" %
                (epoch, num_epoch, batch, num_batch_train, np.mean(loss_arr)))


        with torch.no_grad():
            net.eval()
            loss_arr = []

            for batch, data in enumerate(loader_val, 1):
                data['label'] = data['label']*0.5+0.5 #denormalization -> X*std+mean

                # forward pass
                label = data['label'].to(device)
                input = data['input'].to(device)

                output = net(input)

                # 손실함수 계산하기
                loss = fn_loss(output, label)

                loss_arr += [loss.item()]

                print("VALID: EPOCH %04d / %04d | BATCH %04d / %04d | LOSS %.4f" %
                        (epoch, num_epoch, batch, num_batch_val, np.mean(loss_arr))) 

            epoch_loss = np.mean(loss_arr)

            # deep copy the model
            if epoch_loss < best_loss:
                best_loss = epoch_loss
                best_model_wts = copy.deepcopy(net.state_dict())

        print()
    
    time_elapsed = time.time() - since
    print('Training complete in {:.0f}m {:.0f}s'.format(
        time_elapsed // 60, time_elapsed % 60))
    print('Best val loss: {:4f}'.format(best_loss))

    # load best model weights
    net.load_state_dict(best_model_wts)
    return net

if __name__ == '__main__':
    model_ft = train_model(net, fn_loss, optim, num_epoch)
    torch.save(model_ft.state_dict(), './model_log/model_weights.pth')

 

위와 같이 변경해주고 실행시켜주면 num_workers에 따라 subprocess가 발생하는걸 확인하실 수 있습니다.

그림14

 

 

그림15

 

 

 

 

 

지금까지 pytorch에서 제공해주는 transform을 이용하기 위해 변경시켜야 할 부분들을 설명했습니다.

감사합니다.

 

 

 

 

안녕하세요.

이번 글에서는 Segmentation을 하기 위해 데이터셋을 어떻게 세팅해놓는지에 대해 설명하려고 합니다.

 

코드아래 사이트기반으로 수정하였으니 아래 영상을 먼저 참고하시면 글을 이해하시는데 도움이 될 것으로 생각됩니다.

 

https://www.youtube.com/watch?v=fWmRYmjF-Xw 

 

 

 

 

 

1. 데이터 다운받기 (Feat. ISBI 2012 EM segmentation Challenge)

보통 segmentation을 하기 위한 public 데이터들은 Kaggle, MICCAI 같은 곳에서 열리는 segmentation challenge에서 구할 수 있습니다. 또는 과거에 진행되었거나 현재에 진행되는 다른 segmentation challenge에서도 구할 수 있습니다.

 

보통 유명한 데이터셋들 중에 크기가 작은 데이터들은 github에 올려놓는 경우도 있습니다.

 

이번 글에서는 "ISBI 2012 EM segmentation Challenge"에서 사용되었던 membrane 데이터셋github에서 다운받아 사용해보려고 합니다.

 

방법은 간단합니다.

먼저 "ISBI 2012 github", "ISBI 2012 segmentation" 등의 키워드구글검색하시면 다양한 github 사이트가 노출이 될 겁니다.

 

제가 찾은 사이트는 아래 사이트인데, 먼저 아래 사이트접속해보도록 하겠습니다.

https://github.com/alexklibisz/isbi-2012

 

GitHub - alexklibisz/isbi-2012: Image Segmentation Techniques on the ISBI 2012 dataset: http://brainiac2.mit.edu/isbi_challenge/

Image Segmentation Techniques on the ISBI 2012 dataset: http://brainiac2.mit.edu/isbi_challenge/ - GitHub - alexklibisz/isbi-2012: Image Segmentation Techniques on the ISBI 2012 dataset: http://bra...

github.com

 

 

 

해당 사이트에 접속하시면 data 폴더"ISBI 2012 EM" 데이터들이 들어 있다는걸 확인할 수 있습니다.

아래 "그림1"처럼 github repository다운 받습니다 ("우측 상단Code를 누른 후, Download ZIP을 클릭해 주세요") 

그림1

 

 

다운받은 데이터는 아래 그림과 같이 되어 있습니다.

그림2

 

 

 

해당 데이터에 데한 정보는 다음과 같습니다.

  • train-volume.tif : input image data for training
  • train-label.tif : label image data for training
  • test-volume.tif : test image data

그림3

 

위의 세 가지 데이터 모두 512×512×30으로 설정되어 있습니다.

여기서 "30"이 의미하는 것은 무엇일까요?

 

해당 이미지 속성을 보면 slices=30이라는 부분이 눈에 띕니다. 

 

그림4

 

 

즉, "train-volume.tif" 이미지 파일은 30개별도이미지를 포함하고 있다는 뜻인데, 이것을 그림으로 표현하면 아래와 같습니다.

그림5

 

 

 

 

2. 개별 이미지 추출하기

 

앞서 다운받은 이미지 형식이 무엇을 의미하는지 설명했습니다.

그럼 이제부터 "512×512×30" 형식의 이미지에서 개별 이미지들을 따로 추출해보겠습니다.

 

먼저, UNet_segmentation이라는 폴더를 만든 후, 이전에 다운 받은 data 폴더를 복사, 붙여놓기 합니다.

그리고, 개별 이미지들을 저장시킬 디렉터리 ('test', 'train', 'val') 폴더 만들어 줍니다.

 

 

그림6

 

추가적으로 각각의 폴더 ('train', 'val', 'test')input, label 데이터를 저장시킬 별도의 폴더를 아래와 같이 만들어 줍니다.

그림7

 

 

 

 

Visual Studio에서 위의 개별 이미지들을 나누는 코드작성해보도록 하겠습니다.

그림8

 

 

 

우선 전체 코드를 보여드리면 아래와 같습니다.

## 필요한 패키지 등록
import os
import numpy as np
from PIL import Image
import matplotlib.pyplot as plt

## 1) 데이터 불러오기
dir_data = './data'

name_label = 'train-labels.tif'
name_input = 'train-volume.tif'

img_label = Image.open(os.path.join(dir_data, name_label))
img_input = Image.open(os.path.join(dir_data, name_input))

ny, nx = img_label.size
nframe = img_label.n_frames

## 2) training, validation, test 이미지 데이터 개수 정해주기
nframe_train = 24
nframe_val = 3
nframe_test = 3

dir_save_train_input = os.path.join(dir_data, 'train/input')
dir_save_train_label = os.path.join(dir_data, 'train/label')
dir_save_val_input = os.path.join(dir_data, 'val/input')
dir_save_val_label = os.path.join(dir_data, 'val/label')

dir_save_test_input = os.path.join(dir_data, 'test/input')
dir_save_test_label = os.path.join(dir_data, 'test/label')

## 3) 512x512x30에서 선별할 frame shuffle해서 추출하기 
id_frame = np.arange(nframe)
np.random.shuffle(id_frame) #사실 shuffle은 굳이 안해줘도 됨

## 4) 24개의 training data (input, label)를 추출하기
offset_nframe = 0

for i in range(nframe_train):
    img_label.seek(id_frame[i + offset_nframe])
    img_input.seek(id_frame[i + offset_nframe])

    # img_label.save(os.path.join(dir_save_train, 'label_%03d.tif' % i))
    # img_input.save(os.path.join(dir_save_train, 'input_%03d.tif' % i))

    label_ = np.asarray(img_label)
    input_ = np.asarray(img_input)

    np.save(os.path.join(dir_save_train_label, 'label_%03d.npy' % i), label_)
    np.save(os.path.join(dir_save_train_input, 'input_%03d.npy' % i), input_)

## 5) 3개의 validation data (input, label)를 추출하기
offset_nframe = nframe_train

for i in range(nframe_val):
    img_label.seek(id_frame[i + offset_nframe])
    img_input.seek(id_frame[i + offset_nframe])

    # img_label.save(os.path.join(dir_save_val, 'label_%03d.tif' % i))
    # img_input.save(os.path.join(dir_save_val, 'input_%03d.tif' % i))

    label_ = np.asarray(img_label)
    input_ = np.asarray(img_input)

    np.save(os.path.join(dir_save_val_label, 'label_%03d.npy' % i), label_)
    np.save(os.path.join(dir_save_val_input, 'input_%03d.npy' % i), input_)

## 6) 3개의 test data (input, label)를 추출하기
offset_nframe = nframe_train + nframe_val

for i in range(nframe_test):
    img_label.seek(id_frame[i + offset_nframe])
    img_input.seek(id_frame[i + offset_nframe])

    # img_label.save(os.path.join(dir_save_test, 'label_%03d.tif' % i))
    # img_input.save(os.path.join(dir_save_test, 'input_%03d.tif' % i))

    label_ = np.asarray(img_label)
    input_ = np.asarray(img_input)

    np.save(os.path.join(dir_save_test_label, 'label_%03d.npy' % i), label_)
    np.save(os.path.join(dir_save_test_input, 'input_%03d.npy' % i), input_)

 

 

 

2-1) Image 모듈

사실 위에 있는 코드를 따로 설명한다기보다 Image 모듈attribute, function 들을 소개하는 것이 더 좋을 것 같아 아래 링크를 첨부하도록 하겠습니다.

 

https://89douner.tistory.com/310

 

2. Image 모듈 (Image.open(), Image.seek())

안녕하세요. 이번 글에서는 Pillow 패키지의 가장 기본이 되는 모듈인 Image 모듈에 대해서 설명하려고 합니다. 1. Image 모듈이란? Image 모듈은 기본적으로 이미지 파일을 로드하거나 새로운 이미지

89douner.tistory.com

https://89douner.tistory.com/309?category=1002521 

 

1.Pillow 패키지란 무엇인가요?

안녕하세요. 이번 글에서는 Pillow라는 파이썬 패키지에 대해서 소개해드리려고 합니다. 1. Pillow 패키지란? Pillow 패키지를 설명하기 전에 PIL 패키지에 대해 간단히 설명하겠습니다. PIL는 Python Imagi

89douner.tistory.com

 

 

2-2) training dataset

위의 코드에서 한 가지 부분만 말씀드리면 "train-volume.tif" 데이터에는 30개별도 이미지가 있는데, 이중에서 24개 training dataset, 3개validatin dataset, 3개test dataset으로 이용한다는 점입니다.

 

 

 

 

2-3) numpy 형태로 저장하는 이유

위의 코드에서 개별 이미지numpy 형태저장하는 것을 볼 수 있습니다.

numpy 형태로 저장하는 이유는 현재 이미지 shape(512, 512) 이기 때문입니다.

 

    label_ = np.asarray(img_label)
    input_ = np.asarray(img_input)

    np.save(os.path.join(dir_save_train, 'label_%03d.npy' % i), label_)
    np.save(os.path.join(dir_save_train, 'input_%03d.npy' % i), input_)

 

(아래와 같이 "a=label_shape" 코드를 추가한 후,  살펴 보면 (512,512) 형태임을 알 수 있습니다) 

 

 

위의 이미지 형태에서 빠진 것 중 하나가 channel 정보입니다.

Pytorch의 딥러닝 모델학습시키기 위해서는 입력 데이터의 가로, 세로 길이와 channel (1=gray, 3=rgb) 정보가 들어있어야 합니다.

 

하지만 위의 shape을 통해 확인한 결과 현재"(H,W)=(이미지 높이, 이미지 너비)" 구조입니다.

그래서 이러한 구조를  "(H,W,C) = (이미지 높이, 이미지 너비, 이미지 채널)" 만들어 주어야 합니다.

 

numpy 형식을 이용하면 현재 "(H,W)"형태의 구조에서 축 하나를 쉽게 늘려 "(H,W,C)" 형태만들 수 있게 됩니다.

 

 

위의 코드 중에 아래와 같이 주석처리된 부분이 있는데, 만약 본인이 numpy가 아닌 이미지 형식으로 저장시켜 보고 싶다면 아래 주석 부분을 제거해주시면 됩니다. (그럼 개별 이미지들이 tif 형식으로 저장되 직접 확인하실 수 있습니다)

    # img_label.save(os.path.join(dir_save_train, 'label_%03d.tif' % i))
    # img_input.save(os.path.join(dir_save_train, 'input_%03d.tif' % i))

 

 

 

※사실 R,G,B 이미지였다면 (512,512,3) 형태로 저장이 되었을 것이기 때문에, 따로 numpy로 저장할 필요가 없습니다. 하지만, 현재 다루고 있는 이미지가 Gray scale을 따른다면 numpy 형태로 저장할 필요가 있습니다. (사실, gray scale 이미지도 엄격하게 표현하려면 (512,512,1)로 표현되어야 하지만, 이미지에서는 channel에 해당하는 1 부분을 생략하고 (512,512)로 표현하는 경우가 있습니다.)

 

※이 글에서는 slice형태로 이미지가 묶여서 나오기 때문에 지금과 같이 별개의 이미지로 분리하는 작업을 거쳤지만, 만약 이미지 데이터들이 3차원의 별도의 이미지로 제공이 될 경우는 지금까지 설명했던 코드를 구현할 필요는 없습니다.

 

※하지만, 의료 영상에서 다루는 CXR, CT, MRI 이미지들은 대부분 Gray scale이 많기 때문에 medical 분야에서 인공지능을 하시는 분들은 위의 코드를 잘 숙지하고 있으시면 많은 도움이 될 거라 생각합니다.

 

 

감사합니다. 

 

안녕하세요.

이번 Pytorch 기반의 segmentation 코드 설명은 UNet을 기반으로 하려합니다.

 

아래 강의를 참고로 설명을 하니 먼저 아래 강의를 들으셔도 좋을 것 같습니다.

 

 

 

https://www.youtube.com/watch?v=fWmRYmjF-Xw 

 

 

https://github.com/hanyoseob/youtube-cnn-002-pytorch-unet

 

GitHub - hanyoseob/youtube-cnn-002-pytorch-unet: [CNN PROGRAMMING] 002 - UNET

[CNN PROGRAMMING] 002 - UNET. Contribute to hanyoseob/youtube-cnn-002-pytorch-unet development by creating an account on GitHub.

github.com

 

 

 

 

 

 

 

+ Recent posts