Visualization of Image Exceeding Limitation

  • 연산 결과를 그림으로 표현하다보면, 표현 가능한 범위를 넘어설 때가 있습니다.
  • 이럴 때 어쩔 수 없이 표현 가능한 범위로 우겨넣어 표현하지만, 오해를 유발합니다.
  • 최대한 오해를 방지하려는 노력으로 이해해 주시면 좋겠습니다.

Pega Devlog: tf.keras BatchNormalization

1. 지난 글에 이어서

  • 지난 글에 이런 부분이 있습니다.


  • 상황을 다시 설명하면 이렇습니다.

    1. 데이터의 범위가 01에서 -22 정도로 바뀝니다.
    2. 이미지로는 0~1 밖을 표현할 수 없습니다.
    3. 그래서 min-max scaling으로 0~1로 강제합니다.
    4. 결과적으로 채도가 낮아집니다.
  • 결과만 보면 BN은 데이터 값의 범위를 좁히는 것이라고 오해하기 너무 좋습니다.

  • 아쉬운 마음에 밀도함수도 같이 그려봤지만 x 범위가 달라 오해를 키웁니다.

  • 이번 글에서는 앞 글에서 유발한 오해를 주워담아보겠습니다.

2. 시각화 설계

matplotlib tutorial: Customizing Figure Layouts Using Gridspec and Other Functions

  • 위 그림은 사실 matplotlib으로 저장한 파일들을 파워포인트에서 조합한 것입니다.
  • 파워포인트의 효율성이 좋아서 종종 쓰는 방법이지만 제대로 다시 그려봅니다.


  • matplotlib으로 이런 그림을 그릴 때는 gridspec이 좋습니다.

    1. 합쳐질 그림들을 포함해서 3행 6열의 자리를 만듭니다.
    2. 밀도함수가 들어올 자리 6개는 자리 하나당 subplot 하나를 만듭니다.
    3. 이미지가 들어올 자리 3개는 자리 4개당 subplot 하나를 만듭니다.
  • 코드로 표현하면 이렇습니다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    import tensorflow as tf
    from tensorflow.keras import layers

    import numpy as np
    import matplotlib.pyplot as plt
    import seaborn as sns

    fig = plt.figure(figsize=(12, 6))

    # 3행 6열 자리 만들기
    gridsize = (3, 6)

    # kdeplot이 들어올 자리: 1 x 1
    for i in range(6):
    ax_kde = plt.subplot2grid(gridsize, (0, i))

    # image가 들어올 자리: 2 x 2
    for i in range(3):
    ax_img = plt.subplot2grid(gridsize, (1, 2*i), rowspan=2, colspan=2)


3. batch normalization 수행

  • 자리가 생겼으니, 이제 자리를 채울 그림들을 다듬어봅니다.
  • 같은 그림들을 읽어서 batch normalization을 수행합니다.
  • 이전 글과 정확히 동일한 코드입니다.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    from tensorflow.keras.models import Sequential

    # 원본 파일 읽기
    img0 = plt.imread("./4-bn_0.jpg")
    img1 = plt.imread("./4-bn_1.jpg")
    img2 = plt.imread("./4-bn_2.jpg")

    # 그림 높이 저장
    img_shape = img0.shape

    # vectorization
    imgs = np.zeros((3, ) + img_shape)
    imgs[0] = img0/255.0
    imgs[1] = img1/255.0
    imgs[2] = img2/255.0

    # 이미지를 텐서로 변환
    imgs_tensor = tf.convert_to_tensor(imgs, dtype="float32")

    # BN 적용을 위한 모델 구축
    model = Sequential([
    layers.BatchNormalization()
    ])
    model.compile()

    # BN 적용
    bn1 = model(imgs_tensor, training=True)

    # numpy 형태로 데이터 변환
    bn1_numpy = bn1.numpy()

4. 데이터 분포 시각화

  • 시각화 코드를 준비합니다.
  • 지난번엔 밀도함수와 이미지를 따로 그렸지만 이번엔 한번에 출력되도록 하겠습니다.
  • axes도 함께 출력해서 출력된 그림을 수정하기 좋게 합니다.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    def plot(imgs, xlim=None, suptitle=None):
    fig, axes = plt.subplots(ncols=3, nrows=2, figsize=(12, 8))
    axs = axes.ravel()

    # image visualization
    for ax, img in zip(axs[:3], imgs):
    ax.imshow(img)

    # kdeplot visualization
    xlim_ = 0 # x 범위 설정을 위한 준비
    for ax, img in zip(axs[3:], imgs):
    colors = ["r", "g", "b"]
    [sns.kdeplot(img[:,:,ch].flatten(), color=c, fill=True, cut=0, ax=ax) for (ch, c) in zip(range(3), colors)]
    xlim_ax = ax.get_xlim()
    xlim_ = max(xlim_, max(-xlim_ax[0], xlim_ax[1]))

    if xlim == None: # x 범위를 대칭으로 만들기
    axs[3].set_xlim(-xlim_, xlim_)
    axs[4].set_xlim(-xlim_, xlim_)
    axs[5].set_xlim(-xlim_, xlim_)
    else: # 외부 입력 범위 사용
    axs[3].set_xlim(-xlim, xlim)
    axs[4].set_xlim(-xlim, xlim)
    axs[5].set_xlim(-xlim, xlim)

    # 제목 삽입
    if suptitle != None:
    fig.suptitle(suptitle, fontsize=24)

    fig.tight_layout()
    return fig, axes

4.1. 입력 데이터

  • 입력 이미지와 채널별 데이터 분포를 그려봅니다.

  • batch normalization 결과와 비교하기 위해 밀도함수 범위를 넓게 잡습니다.

    1
    fig, axes = plot(imgs, 3.5, "original images")


  • 데이터를 255로 나누어 넣었기 때문에 데이터가 0~1 사이에 분포하고 있습니다.

  • matplotlib은 이미지 데이터로 int를 받으면 0255를 기준으로 그림을 표시하고, float를 받으면 01 기준으로 표시합니다.

  • x 범위를 넓게 잡았기 때문에 분포 범위가 매우 좁게 그려졌습니다.

4.2. Batch Normalization 데이터

  • batch normalization 결과를 그려봅니다.
  • 0~1 외의 범위는 그림으로 표현되지 않기 때문에, 밀도함수를 살짝 가려서 그림으로 안보이는 부분이라는 표시를 했습니다.
    1
    2
    3
    4
    5
    6
    7
    8
    fig, axes = plot(bn1_numpy, 3.5, "batch normalized images")
    for ax in axes[1, :]:
    # 음영 표시
    ax.axvspan(-3.5, 0, facecolor="k", alpha=0.5)
    ax.axvspan(1, 3.5, facecolor="k", alpha=0.5)
    # text 출력
    ax.text(0.05, 0.8, "unseen\n(black)", transform=ax.transAxes, fontsize=16)
    ax.text(0.7, 0.8, "unseen\n(white)", transform=ax.transAxes, fontsize=16, color="w")


  • 일부 데이터가 표현되지 않는다는 경고 메시지가 뜹니다.
  • 이전 글과는 다르게 눈이 쨍한 그림이 나옵니다.
    min-max scaling을 하지 않았기 때문입니다.
  • 밀도함수의 범위가 확 넓어진 것이 눈에 뜨입니다.
  • 이미지에 시커먼 부분과 새하얀 부분이 있는데, 시각화 범위를 벗어난 곳입니다.

4.3. min-max scaling

  • 이미지에서 놓치는 부분 없이 표현하기 위해 min-max scaling을 합니다.
  • 여기서 얻은 밀도함수는 원본과 정성비교만 가능합니다.
    1
    2
    3
    4
    5
    6
    bn_min, bn_max = bn1_numpy.min(), bn1_numpy.max()

    # min-max scaling
    bn_mm = (bn1_numpy - bn_min)/(bn_max - bn_min)

    fig, axes = plot(bn_mm, 3.5, "batch normalized + min-max scaled images")

  • 이미지의 채도가 떨어졌습니다.
  • min-max scaling 전에는 범위가 넓어 드러나지 않았지만, 데이터 분포가 중앙으로 몰리면서 R, G, B 값들이 비슷해져 회색조에 가까워진 것으로 추정됩니다.

4.4. 밀도함수 정성비교

seaborn.kdeplot

  • 원본과 batch normalization을 거친 이미지를 나란히 놓고 비교합니다.
  • 앞에서 만든 시각화 함수로 커버되지 않는 범위이므로 함수를 새로 만듭니다.
  • 밀도함수만 그리면 어떤 이미지에서 나왔는지 알 수 없기 때문에, 이미지를 작게 삽입합니다.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    from matplotlib import patches

    fig, axes = plt.subplots(ncols=3, nrows=2, figsize=(12, 8), sharex=True, sharey=True,
    gridspec_kw={"hspace":0.3})

    # RGB 세 채널을 각기 빨강, 녹색, 파랑으로 표기
    colors = ["r", "g", "b"]

    # 원본 이미지 채널별 밀도함수
    for ax, img in zip(axes[0], imgs):
    # list comprehension 사용 RGB 채널별 밀도함수 그리기
    [sns.kdeplot(img[:,:,ch].flatten(), color=c, fill=True, cut=0, ax=ax) for (ch, c) in zip(range(3), colors)]
    # 원본 그림 삽입
    ax_inset = ax.inset_axes([0.02, 0.69, 0.3, 0.3])
    ax_inset.imshow(img)
    ax_inset.set_xticks([])
    ax_inset.set_yticks([])

    # batch normalization 후 이미지 채널별 밀도함수
    for ax, b in zip(axes[1], bn_mm):
    [sns.kdeplot(b[:,:,ch].flatten(), color=c, fill=True, cut=0, ax=ax) for (ch, c) in zip(range(3), colors)]
    ax_inset = ax.inset_axes([0.02, 0.69, 0.3, 0.3])
    ax_inset.imshow(b)
    ax_inset.set_xticks([])
    ax_inset.set_yticks([])

    # 제목 붙이기
    axes[0, 1].set_title("before batch normalization", fontsize=24, pad=12)
    axes[1, 1].set_title("after batch normalization", fontsize=24, pad=12)

    # 범례 그리기
    for i, c in enumerate(colors):
    x = 0.7
    y = 0.85 - 0.1*i
    Rectangle = patches.Rectangle((x, y), 0.12, 0.05, color=c, alpha=0.5, transform=axes[0, 2].transAxes)
    axes[0, 2].add_patch(Rectangle)
    axes[0, 2].text(x+0.2, y, c.upper(), fontdict={"fontsize":16, "color":"k"}, ha="center", transform=axes[0, 2].transAxes)

  • 데이터의 존재 범위가 채널별로 독립적으로 변화되었습니다.
  • 다른 이미지의 데이터 분포에 의해 서로 영향을 받았습니다.
  • 예를 들어, 맨 오른쪽 이미지에서 B 채널과 G 채널이 가운데로 심하게 쏠렸는데 앞 두 이미지에서 두 색상이 쌍봉을 그리며 양 극단에 위치한 영향으로 보입니다.
  • seaborn의 kdeplot은 hue인자 외에는 범례를 생성해주지 않습니다.

  • 객체를 받아 ax.legend()명령으로 넣으려고 하면 Legend does not support <AxesSubplot:ylabel='Density'> instances. 경고가 발생합니다.

  • 부족한 기능을 수동으로 메웠습니다: 네모 상자와 텍스트를 넣어 보완했습니다.

  • 만족스러운 그림을 얻었습니다.

  • 최종 그림에 이 분포들을 삽입하기로 하고, 이제 이미지로 넘어갑니다.

5. 이미지 시각화

numpy.concatenate

  • 원본 이미지 하나를 그려봅니다.
    1
    plt.imshow(imgs[1])

  • 원본 이미지에서 왼쪽 반을, batch normalization된 이미지에서 오른쪽 반을 가져옵니다.
  • 의도적으로 min-max scaling되지 않은 이미지를 사용하겠습니다.
  • 이미지의 왼쪽은 컬러 이미지 기준 3D numpy array의 두 번째 차원이 절반 이하라는 의미입니다.
  • 마찬가지로 이미지의 오른쪽은 두 번째 차원이 절반 이상이라는 의미입니다.
    1
    2
    3
    4
    5
    6
    7
    img_half = img_shape[0]//2              # 이미지를 자를 좌표.
    img1_org = imgs[1][:, :img_half, :] # 왼쪽 절반 원본 이미지
    img1_bn = bn1_numpy[1][:, img_half:, :] # 오른쪽 절반 batch normalization된 이미지

    fig, axs = plt.subplots(ncols=2)
    axs[0].imshow(img1_org)
    axs[1].imshow(img1_bn)

  • 이 둘을 합치겠습니다.
  • numpy array를 붙이는 명령은 numpy.concatenate()입니다.
  • 경계선에 점선도 하나 그어줍니다.
    1
    2
    3
    4
    5
    img1_c = np.concatenate((img1_org, img1_bn), axis=1)

    fig, ax = plt.subplots()
    ax.imshow(img1_c)
    ax.axvline(128, ls="--", c="k")

6. 집대성

  • 위에서 그린 부분 기술들을 한데 모았습니다.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    fig = plt.figure(figsize=(12, 6))

    # 3행 6열 자리 준비
    gridsize = (3, 6)

    # 윗줄 : 밀도함수
    for i in range(6):
    # 한 자리에 그림 하나
    ax_kde = plt.subplot2grid(gridsize, (0, i))

    # 홀수 자리 : 원본 이미지 데이터 분포
    if i%2 == 0: # original image kdeplot
    colors = ["r", "g", "b"]
    [sns.kdeplot(imgs[i//2, :,:,ch].flatten(), color=c, fill=True, cut=0, ax=ax_kde) for (ch, c) in zip(range(3), colors)]

    # 제목 붙이기
    ax_kde.set_title("original", fontsize=20, pad=12)

    # 짝수 자리 : batch normalization된 이미지 데이터 분포
    else: # batch normalized image kdeplot
    colors = ["r", "g", "b"]
    [sns.kdeplot(bn1_numpy[i//2, :,:,ch].flatten(), color=c, fill=True, cut=0, ax=ax_kde) for (ch, c) in zip(range(3), colors)]

    # 시각화 불가능 영역을 음영으로 표시
    ax_kde.axvspan(-3.5, 0, facecolor="k", alpha=0.2)
    ax_kde.axvspan(1, 3.5, facecolor="k", alpha=0.2)

    # 제목 붙이기
    ax_kde.set_title("batch norm.", fontsize=20, pad=12)

    # x축 범위 설정
    ax_kde.set_xlim(-3.5, 3.5)

    # 맨 왼쪽에만 y축에 "Density" 붙이기
    if i == 0:
    ax_kde.set_ylabel("Density", fontsize=20, labelpad=12)
    else:
    ax_kde.set_ylabel("")

    # 원본과 batch normalization된 그림을 붙이는 함수
    def img_c(img0, img1):
    img0_ = img0[:, :img_half, :]
    img1_ = img1[:, img_half:, :]
    return np.concatenate((img0_, img1_), axis=1)

    # 이미지 부분 출력
    for i in range(3):
    # 4 자리에 그림 하나
    ax_img = plt.subplot2grid(gridsize, (1, 2*i), rowspan=2, colspan=2)
    # 그림 출력
    ax_img.imshow(img_c(imgs[i], bn1_numpy[i]))
    # 그림 사이 경계선 삽입
    ax_img.axvline(128, ls="--", c="k")

    fig.tight_layout()


도움이 되셨나요? 카페인을 투입하시면 다음 포스팅으로 변환됩니다

Share