안경잡이개발자

728x90
반응형

  Convolution 연산CNN을 포함한 다양한 딥러닝 네트워크에서 활용되는 중요한 연산이다. PyTorch와 같은 딥러닝 프레임워크에서는 Convolution 연산을 기본적으로 제공하고 있다. 이러한 Convolution 연산에 대해 더욱 자세히 이해하기 위하여 본 포스팅에서는 Convolution 연산을 Python NumPy만을 이용해 구현해보도록 하겠다.

 

  예를 들어 입력 차원이 (배치 크기, 채널 크기, 높이, 너비) = (2, 3, 5, 5)인 예시를 확인해 보자. 이때 3 X 3짜리 커널을 3개 사용하여 stride = 2, padding = 0 설정으로 Convolution 연산을 수행하면 그 결과는 다음과 같다. 수식으로 표현하자면 출력 차원은 (배치 크기, 커널 개수, out_h, out_w) = (2, 3, 2, 2)가 된다.

 

 

※ 단순 반복문을 이용한 구현 ※

 

  이러한 Convolution 연산을 어떻게 구현할 수 있을까? 가장 기본적인 방법은 바로 반복문을 이용하는 것이다. 기본적으로 출력 높이(out_h)와 출력 너비(out_w)는 다음의 공식을 이용해 구현할 수 있다.

out_h = (h + 2 * padding - filter_h) / stride + 1
out_w = (w + 2 * padding - filter_w) / stride + 1

 

  또한, 일반적으로 패딩(padding)은 입력 데이터의 높이와 너비 차원에 대해서만 수행한다. 이를 위해 np.pad() 메서드를 사용할 수 있다. 아래 코드를 보면 높이와 너비 차원에 대해서만 패딩을 넣는 것을 알 수 있다.

 

  결과적으로 출력 텐서(out)를 만든 뒤에 각각의 원소마다 Convolution 연산을 수행한 결과 값을 채워 넣는 것을 알 수 있다. NumPy에서 단순히 두 행렬에 대하여 곱셈 연산을 수행하면, 원소 단위(element-wise)로 곱셈 연산이 수행된다. 그래서 Convolution 연산을 다음과 같이 구현할 수 있다.

 

import numpy as np


def conv(X, filters, stride=1, pad=0):
    n, c, h, w = X.shape
    n_f, _, filter_h, filter_w = filters.shape

    out_h = (h + 2 * pad - filter_h) // stride + 1
    out_w = (w + 2 * pad - filter_w) // stride + 1

    # add padding to height and width.
    in_X = np.pad(X, [(0, 0), (0, 0), (pad, pad), (pad, pad)], 'constant')
    out = np.zeros((n, n_f, out_h, out_w))

    for i in range(n): # for each image.
        for c in range(n_f): # for each channel.
            for h in range(out_h): # slide the filter vertically.
                h_start = h * stride
                h_end = h_start + filter_h
                for w in range(out_w): # slide the filter horizontally.
                    w_start = w * stride
                    w_end = w_start + filter_w
                    # Element-wise multiplication.
                    out[i, c, h, w] = np.sum(in_X[i, :, h_start:h_end, w_start:w_end] * filters[c])

    return out

 

  이제 앞서 다루었던 그림을 그대로 코드로 옮겨 결과를 확인해 보자.

 

X = np.asarray([
# image 1
[
    [[1, 2, 9, 2, 7],
    [5, 0, 3, 1, 8],
    [4, 1, 3, 0, 6],
    [2, 5, 2, 9, 5],
    [6, 5, 1, 3, 2]],

    [[4, 5, 7, 0, 8],
    [5, 8, 5, 3, 5],
    [4, 2, 1, 6, 5],
    [7, 3, 2, 1, 0],
    [6, 1, 2, 2, 6]],

    [[3, 7, 4, 5, 0],
    [5, 4, 6, 8, 9],
    [6, 1, 9, 1, 6],
    [9, 3, 0, 2, 4],
    [1, 2, 5, 5, 2]]
],
# image 2
[
    [[7, 2, 1, 4, 2],
    [5, 4, 6, 5, 0],
    [1, 2, 4, 2, 8],
    [5, 9, 0, 5, 1],
    [7, 6, 2, 4, 6]],

    [[5, 4, 2, 5, 7],
    [6, 1, 4, 0, 5],
    [8, 9, 4, 7, 6],
    [4, 5, 5, 6, 7],
    [1, 2, 7, 4, 1]],

    [[7, 4, 8, 9, 7],
    [5, 5, 8, 1, 4],
    [3, 2, 2, 5, 2],
    [1, 0, 3, 7, 6],
    [4, 5, 4, 5, 5]]
]
])
print('Images:', X.shape)

filters = np.asarray([
# kernel 1
[
    [[1, 0, 1],
    [0, 1, 0],
    [1, 0, 1]],

    [[3, 1, 3],
    [1, 3, 1],
    [3, 1, 3]],

    [[1, 2, 1],
    [2, 2, 2],
    [1, 2, 1]]
],
# kernel 2
[
    [[5, 1, 5],
    [2, 1, 2],
    [5, 1, 5]],

    [[1, 1, 1],
    [1, 1, 1],
    [1, 1, 1]],

    [[2, 0, 2],
    [0, 2, 0],
    [2, 0, 2]],
],
# kernel 3
[
    [[5, 1, 5],
    [2, 1, 2],
    [5, 1, 5]],

    [[1, 1, 1],
    [1, 1, 1],
    [1, 1, 1]],

    [[2, 0, 2],
    [0, 2, 0],
    [2, 0, 2]],
]
])
print('Filters:', filters.shape)

out = conv(X, filters, stride=2, pad=0)
print('Output:', out.shape)
print(out)

 

  실행 결과는 다음과 같다. 그림에서의 결과와 동일한 것을 확인할 수 있다.

 

Images: (2, 3, 5, 5)
Filters: (3, 3, 3, 3)
Output: (2, 3, 2, 2)
[[[[174. 191.]
   [130. 122.]]

  [[197. 244.]
   [165. 159.]]

  [[197. 244.]
   [165. 159.]]]


 [[[168. 171.]
   [153. 185.]]

  [[188. 178.]
   [168. 200.]]

  [[188. 178.]
   [168. 200.]]]]

 

※ im2col을 이용한 구현 ※

 

  앞서 단순 반복문을 이용해 Convolution 연산을 구현해 보았다. Convolution 연산을 수행할 때 단순히 반복문(for loops)을 이용하는 경우, 행렬 곱을 제대로 활용하지 못한다는 점에서 속도가 느리다. 따라서 메모리를 조금 더 많이 사용하여, 속도를 비약적으로 개선할 수 있는 방법으로 im2col 연산을 활용하는 방법이 있다.

 

  다음과 같이 한 배치에 두 장의 이미지가 존재하여 마찬가지로 입력 차원이 (배치 크기, 채널 크기, 높이, 너비) = (2, 3, 5, 5)인 예시를 확인해 보자. 이러한 입력이 들어왔을 때, 이것을 행렬(matrix)로 형태를 변형하는 것이 im2col 연산이다. im2col 연산을 수행한 결과를 확인해 보자. (8, 27) 차원을 갖는 행렬이 생성되었다. 정확히는 (이미지 개수 X out_h X out_w, 입력 채널 개수 X kernel_h X kernel_w)의 차원을 갖는 행렬이 된다.

 

 

  im2col 연산은 다음과 같이 구현할 수 있다.

 

import numpy as np


def im2col(X, filters, stride=1, pad=0):
    n, c, h, w = X.shape
    n_f, _, filter_h, filter_w = filters.shape

    out_h = (h + 2 * pad - filter_h) // stride + 1
    out_w = (w + 2 * pad - filter_w) // stride + 1

    # add padding to height and width.
    in_X = np.pad(X, [(0, 0), (0, 0), (pad, pad), (pad, pad)], 'constant')
    out = np.zeros((n, c, filter_h, filter_w, out_h, out_w))

    for h in range(filter_h):
        h_end = h + stride * out_h
        for w in range(filter_w):
            w_end = w + stride * out_w
            out[:, :, h, w, :, :] = in_X[:, :, h:h_end:stride, w:w_end:stride]

    out = out.transpose(0, 4, 5, 1, 2, 3).reshape(n * out_h * out_w, -1)
    return out


X = np.asarray([
# image 1
[
    [[1, 2, 9, 2, 7],
    [5, 0, 3, 1, 8],
    [4, 1, 3, 0, 6],
    [2, 5, 2, 9, 5],
    [6, 5, 1, 3, 2]],

    [[4, 5, 7, 0, 8],
    [5, 8, 5, 3, 5],
    [4, 2, 1, 6, 5],
    [7, 3, 2, 1, 0],
    [6, 1, 2, 2, 6]],

    [[3, 7, 4, 5, 0],
    [5, 4, 6, 8, 9],
    [6, 1, 9, 1, 6],
    [9, 3, 0, 2, 4],
    [1, 2, 5, 5, 2]]
],
# image 2
[
    [[7, 2, 1, 4, 2],
    [5, 4, 6, 5, 0],
    [1, 2, 4, 2, 8],
    [5, 9, 0, 5, 1],
    [7, 6, 2, 4, 6]],

    [[5, 4, 2, 5, 7],
    [6, 1, 4, 0, 5],
    [8, 9, 4, 7, 6],
    [4, 5, 5, 6, 7],
    [1, 2, 7, 4, 1]],

    [[7, 4, 8, 9, 7],
    [5, 5, 8, 1, 4],
    [3, 2, 2, 5, 2],
    [1, 0, 3, 7, 6],
    [4, 5, 4, 5, 5]]
]
])
print('Images:', X.shape)

filters = np.asarray([
# kernel 1
[
    [[1, 0, 1],
    [0, 1, 0],
    [1, 0, 1]],

    [[3, 1, 3],
    [1, 3, 1],
    [3, 1, 3]],

    [[1, 2, 1],
    [2, 2, 2],
    [1, 2, 1]]
],
# kernel 2
[
    [[5, 1, 5],
    [2, 1, 2],
    [5, 1, 5]],

    [[1, 1, 1],
    [1, 1, 1],
    [1, 1, 1]],

    [[2, 0, 2],
    [0, 2, 0],
    [2, 0, 2]],
],
# kernel 3
[
    [[5, 1, 5],
    [2, 1, 2],
    [5, 1, 5]],

    [[1, 1, 1],
    [1, 1, 1],
    [1, 1, 1]],

    [[2, 0, 2],
    [0, 2, 0],
    [2, 0, 2]],
]
])
print('Filters:', filters.shape)

out = im2col(X, filters, stride=2, pad=0)
print('Output:', out.shape)
print(out)

 

  im2col 연산 예제 실행 결과는 다음과 같다.

 

Images: (2, 3, 5, 5)
Filters: (3, 3, 3, 3)
Output: (8, 27)
[[1. 2. 9. 5. 0. 3. 4. 1. 3. 4. 5. 7. 5. 8. 5. 4. 2. 1. 3. 7. 4. 5. 4. 6.
  6. 1. 9.]
 [9. 2. 7. 3. 1. 8. 3. 0. 6. 7. 0. 8. 5. 3. 5. 1. 6. 5. 4. 5. 0. 6. 8. 9.
  9. 1. 6.]
 [4. 1. 3. 2. 5. 2. 6. 5. 1. 4. 2. 1. 7. 3. 2. 6. 1. 2. 6. 1. 9. 9. 3. 0.
  1. 2. 5.]
 [3. 0. 6. 2. 9. 5. 1. 3. 2. 1. 6. 5. 2. 1. 0. 2. 2. 6. 9. 1. 6. 0. 2. 4.
  5. 5. 2.]
 [7. 2. 1. 5. 4. 6. 1. 2. 4. 5. 4. 2. 6. 1. 4. 8. 9. 4. 7. 4. 8. 5. 5. 8.
  3. 2. 2.]
 [1. 4. 2. 6. 5. 0. 4. 2. 8. 2. 5. 7. 4. 0. 5. 4. 7. 6. 8. 9. 7. 8. 1. 4.
  2. 5. 2.]
 [1. 2. 4. 5. 9. 0. 7. 6. 2. 8. 9. 4. 4. 5. 5. 1. 2. 7. 3. 2. 2. 1. 0. 3.
  4. 5. 4.]
 [4. 2. 8. 0. 5. 1. 2. 4. 6. 4. 7. 6. 5. 6. 7. 7. 4. 1. 2. 5. 2. 3. 7. 6.
  4. 5. 5.]]

 

  이렇게 구해진 im2col 결과 행렬(matrix)과 연산하기 위해 다음과 같이 커널(kernel) 또한 flatten을 진행하면 된다. 결과적으로 두 행렬(matrix)에 대하여 행렬 곱(matrix multiplication)을 수행하여 결과를 구할 수 있다.

 

 

  따라서 최종적인 코드는 다음과 같다.

 

import numpy as np


def im2col(X, filters, stride=1, pad=0):
    n, c, h, w = X.shape
    n_f, _, filter_h, filter_w = filters.shape

    out_h = (h + 2 * pad - filter_h) // stride + 1
    out_w = (w + 2 * pad - filter_w) // stride + 1

    # add padding to height and width.
    in_X = np.pad(X, [(0, 0), (0, 0), (pad, pad), (pad, pad)], 'constant')
    out = np.zeros((n, c, filter_h, filter_w, out_h, out_w))

    for h in range(filter_h):
        h_end = h + stride * out_h
        for w in range(filter_w):
            w_end = w + stride * out_w
            out[:, :, h, w, :, :] = in_X[:, :, h:h_end:stride, w:w_end:stride]

    out = out.transpose(0, 4, 5, 1, 2, 3).reshape(n * out_h * out_w, -1)
    return out


X = np.asarray([
# image 1
[
    [[1, 2, 9, 2, 7],
    [5, 0, 3, 1, 8],
    [4, 1, 3, 0, 6],
    [2, 5, 2, 9, 5],
    [6, 5, 1, 3, 2]],

    [[4, 5, 7, 0, 8],
    [5, 8, 5, 3, 5],
    [4, 2, 1, 6, 5],
    [7, 3, 2, 1, 0],
    [6, 1, 2, 2, 6]],

    [[3, 7, 4, 5, 0],
    [5, 4, 6, 8, 9],
    [6, 1, 9, 1, 6],
    [9, 3, 0, 2, 4],
    [1, 2, 5, 5, 2]]
],
# image 2
[
    [[7, 2, 1, 4, 2],
    [5, 4, 6, 5, 0],
    [1, 2, 4, 2, 8],
    [5, 9, 0, 5, 1],
    [7, 6, 2, 4, 6]],

    [[5, 4, 2, 5, 7],
    [6, 1, 4, 0, 5],
    [8, 9, 4, 7, 6],
    [4, 5, 5, 6, 7],
    [1, 2, 7, 4, 1]],

    [[7, 4, 8, 9, 7],
    [5, 5, 8, 1, 4],
    [3, 2, 2, 5, 2],
    [1, 0, 3, 7, 6],
    [4, 5, 4, 5, 5]]
]
])
print('Images:', X.shape)

filters = np.asarray([
# kernel 1
[
    [[1, 0, 1],
    [0, 1, 0],
    [1, 0, 1]],

    [[3, 1, 3],
    [1, 3, 1],
    [3, 1, 3]],

    [[1, 2, 1],
    [2, 2, 2],
    [1, 2, 1]]
],
# kernel 2
[
    [[5, 1, 5],
    [2, 1, 2],
    [5, 1, 5]],

    [[1, 1, 1],
    [1, 1, 1],
    [1, 1, 1]],

    [[2, 0, 2],
    [0, 2, 0],
    [2, 0, 2]],
],
# kernel 3
[
    [[5, 1, 5],
    [2, 1, 2],
    [5, 1, 5]],

    [[1, 1, 1],
    [1, 1, 1],
    [1, 1, 1]],

    [[2, 0, 2],
    [0, 2, 0],
    [2, 0, 2]],
]
])
print('Filters:', filters.shape)

stride = 2
pad = 0
X_col = im2col(X, filters, stride=stride, pad=pad)

n, c, h, w = X.shape
n_f, _, filter_h, filter_w = filters.shape

out_h = (h + 2 * pad - filter_h) // stride + 1
out_w = (w + 2 * pad - filter_w) // stride + 1

out = np.matmul(X_col, filters.reshape(n_f, -1).T)
out = out.reshape(n, out_h, out_w, n_f)
out = out.transpose(0, 3, 1, 2)

print('Output:', out.shape)
print(out)

 

  실행 결과는 다음과 같으며, 앞서 반복문을 이용한 구현 결과와 동일하다.

 

Images: (2, 3, 5, 5)
Filters: (3, 3, 3, 3)
Output: (2, 3, 2, 2)
[[[[174. 191.]
   [130. 122.]]

  [[197. 244.]
   [165. 159.]]

  [[197. 244.]
   [165. 159.]]]


 [[[168. 171.]
   [153. 185.]]

  [[188. 178.]
   [168. 200.]]

  [[188. 178.]
   [168. 200.]]]]
728x90
반응형

728x90
반응형

  구글 크롬(Chrome) 브라우저는 시크릿 모드를 제공합니다. 필자는 공용 PC를 사용하거나, 집에서 컴퓨터를 할 때에도 PC에 방문 기록을 남기지 않고자 할 때는 시크릿 모드를 사용합니다. 시크릿 모드는 크롬(Chrome) 브라우저를 실행한 뒤에 [새 시크릿 창] 버튼을 클릭하거나  [Ctrl + Shift + N]을 눌러서 간단하게 열 수 있습니다.

 

 

  시크릿 모드가 실행되면 다음과 같은 화면이 등장합니다. 스크릿 모드를 이용하면 방문 기록, 쿠키 및 사이트 데이터, 양식에 입력한 정보가 저장되지 않습니다. 다시 말해 이러한 정보가 현재 사용하고 있는 기기(컴퓨터)에 저장되지 않습니다. 그래서 나중에 내 컴퓨터를 다른 사람이 사용하더라도, 내가 어디에 접속했는지 알 수 없는 것입니다. (참고로 일반적인 경우에 자기 자신도 자기가 어디에 접속했었는지 알 수 없습니다. 머릿속으로 기억하고 있으면 좋지만, 기록상으로는 남기 때문에, 가끔 기억이 안 나서 낭패를 보는 일이 생깁니다.)

 

  아무튼 시크릿 모드는 굉장히 유용합니다. 예를 들어 필자가 누나의 생일 선물을 사주려고 인터넷에 검색할 때 시크릿 모드를 사용하게 되면, 같은 컴퓨터를 사용하는 누나가 검색 기록을 알지 못하게 되는 것입니다. 다만, 시크릿 모드를 사용하더라도 파일을 다운로드하거나 북마크를 추가했다면 이러한 정보는 저장되므로, 시크릿 모드일 때 다운로드한 파일이 기록된다는 점을 기억하세요.

 

 

※ 참고사항 ※

 

  참고로 시크릿 모드는 [닫은 탭 다시 열기] 기능을 제공하지 않습니다. 따라서 실수로 탭을 닫았을 때 혹은 이전에 방문했던 웹 페이지에 다시 방문하고 싶을 때, 기록이 남아 있지 않아서 낭패를 보는 경우가 발생할 수 있습니다. 그래서 시크릿 모드로 웹 페이지를 방문하다가, 좋은 정보가 포함된 웹 페이지를 찾았다면 그 링크는 별도로 기록할 필요가 있습니다.

 

  또한 내 컴퓨터에 정보가 남지 않는 것이지, 실제로 웹 사이트에 접속했을 때, 웹 사이트의 관리자는 여러분의 방문 기록을 알 수 있습니다. 예를 들어 우리가 네이버(Naver)에 접속해 특정한 웹 페이지에 접속했다고 가정해 봅시다. 그러면 여러분의 컴퓨터에서 보내는 패킷은 네이버 서버에 도달하게 됩니다. 당연히 네이버 입장에서는 여러분의 IP나 패킷 정보를 처리하게 될 것입니다. 그렇기에 웹 사이트 관리자, 인터넷 서비스 제공업체(ISP), 네트워크 관리자의 입장에서는 방문 기록을 확인할 가능성이 있다고 보시면 됩니다.

 

  그래서 여러분의 IP 정보를 포함해 완전히 여러분의 신원을 숨기고 싶다면, 시크릿 모드를 넘어서 VPN을 이용해 보시면 좋습니다. 물론 VPN을 이용한다고 하더라도, 여러분이 특정 웹 사이트에 로그인을 하는 등으로 정보를 노출한다면, 이 또한 활동 내역이 남게 될 여지가 있다는 점에 유의하세요. 실제로 웹 사이트 관리자 입장에서는 접속자가 로그인을 했을 때에만 중요한 정보를 노출하도록 하여, 어떤 사람이 어떤 기능을 이용했는지 처리하는 경우가 많습니다.

728x90
반응형