이번에 프로젝트로 위성 사진을 시멘틱 분할하는 어플리케이션을 준비하게 되었다. 베이스라인 모델로 U-Net 같은 것들을 사용하면 된다고 하더라. 일단 이전에 시멘틱 분할 모델인 FCN이나 U-Net 같은 모델 논문을 잠깐 간단히 보기는 했지만 그렇게 잘 와닿지는 않았다.
아무튼 지금나는 시멘틱 세그먼테이션을 하기 위해서 저런 모델을 사용한다는 사실만 알고, 데이터셋을 어떻게 준비하는 지 모델을 만들어서 준비한 데이터셋으로 학습하고 돌리는지 까지는 잘 모르는 상태였다. 그래서 잠깐 구글링으로 U-Net으로 별도의 데이터셋을 훈련하는 방법, 프로젝트에 관해서 검색 해보았는데 자세히 보지는 못했지만 Palliwi라는 분이 잘 정리해서 미디움에다가 올려놓으셨더라.
주의 깊게 본다면 이해할 수 있을것 같긴한데 내용 앞부분을 보면서 잠시 FCN, U-Net 모델에 대해서 조금 더 깊이 이해할 필요성을 느꼈다. 이전에 여러번 초록 정도는 보고, 이해하려고 뭔가 속시원하게 파악하지는 못하다보니 이번에 다시 FCN 부터 다양한 국내 블로그 정리 글을 찾아보았는데 가장 먼저 라온 피플에서 작성한 글을 보았다. 보면서 나도 이렇게 잘 좀 정리하고 싶은데 왜 이렇게 힘들까 ㅠㅜ. 지난번에 ResNet도 깔끔하게 정리해봐야지 해놓고 계속 미뤄두네 ;;
이 글에서는 우선 FCN이 언제 나오고 얼마나 열광 받고 있는지 부터 시작하고 있다. 그리고 분류, 검출, 시멘틱 세그먼테이션과 같이 컴퓨터 비전 분야의 문제들을 소개하면서 각각 문제 모델의 특징들을 알려주는데, 이런 내용은 여러번 보았고 알곤 있었지만 뭔가 허전함? 깔끔하게 정리가 잘 안됬는데 이런 내용들 처럼 잘 정리하고싶다 ㅜㅜ
아무튼 FCN의 Fully Convollution layer/ 1 x 1 Convolution의 장점에 대해서 소개하는데 이 방법이 나는 FCN에서 처음으로 사용된 것인줄 알았지만 이전에 잠깐 있다고만 들었던 OverFeat 모델에서 사용되었다고 하더라. 하지만 OverFeat에서는 분류, 검출에서만 사용했다 하고, FCN이 시멘틱 세그먼테이션 용도로 1 x 1 conv가 사용된 경우라고 한다.
여기서 말하는 1 x 1 Convolution의 장점으로는 기존의 분류 모델이나 검출 모델에서는 뒤에 고정 크기의 벡터를 만드는 완전 연결 레이어가 존재하여 입력 크기의 제한을 받는다고 한다. 아마 입력 크기가 클수록 고정 크기의 입력 벡터로 정리하려고 많은 정보들을 정리할 수 없고, 완전 연결 계층에서 연산을 하기 위해 Flatten 할 필요가 없어지니 기존의 공간적 정보가 사라지는 것을 방지 한다는 점에서 영상 제한이 사라진다고 하는것 처럼 보인다.
이 다음으로는 디컨볼루션을 이용한 업샘플링에 대해서 설명해주고 있다. 업샘플링을 하는 이유는 여러 레이어들을 거쳐가면서 피쳐맵의 공간적 크기가 줄어드는데 이를 기존의 입력 크기와 동일하게 맞춰주기 위함인데, 간단하게 업샘플링을 한다면 opencv의 resize함수처럼 양선형 보간법을 사용하면 되겠지만, 여기서는 end-to-end 학습을 통해 구하며 backward convolution(deconvolution)을 사용한다고 말한다.
하지만 마지막 레이어에서 너무 작아진 피쳐맵만 가지고 deconv로 크기를 키우면 중간에 존재하던 디테일한 정보들이 사라지기 때문에 중간 레이어로부터 스킵커넥션을 통해 보강시킨다고 한다. 이걸 tf로 어떻게 표현하는지는 잘 모르겠지만 skip layer를 추가시킬수록 더 정교한 예측을 만드는 것으로 이 글이 마쳤다. 라온 피플에서 FCN 모델에 대해 추가적인 설명을 하니 다음 글도 봐야겠다.
A1: b, a. A2: continuous. A3: continuous. A4: u, y, l, t. A5: g, p, gg. A6: c, d, cc, i, j, k, m, r, q, w, x, e, aa, ff. A7: v, h, bb, j, n, z, dd, ff, o. A8: continuous. A9: t, f. A10: t, f. A11: continuous. A12: t, f. A13: g, p, s. A14: continuous. A15: continuous. A16: +,- (class attribute)
A1속성만으로 판단하는 경우
- A1 속성 값이 a인 경우 98개가 긍정이고, 112개가 부정
- A1 속성 값이 b인 경우 206개가 긍정, 262개가 부정
- A1 속성 값이 알수없음 ?인 경우 3개가 긍정, 9개가 부정
=> 가능한 모든 경우의 총 긍정 307, 총 부정 383
=> A1 속성만으로는 긍정과 부정 여부를 잘 분류하지 못한다고 볼수 있다. 거의 50:50 비율로 판별하므로
A9 속성 만으로 판단하는 경우
- A9 속성 값이 t인 경우 284개가 긍정, 77개가 부정
- A9 속성 값이 f인 경우 23개가 긍정, 306개가 부정
=> A9 속성이 t일때 긍정을 많이 찾고, f일때 부정을 많이 찾는다. => A1 속성보다는 잘 분류해낸다.
입력 : 대응쌍 집합 X, 반복 회수, 인라이어 판단 임계치 t, 인라이어 집합 크기 d, 적합오차 e
출력 : 기하 번환 행렬 T
Q = []
for i in range(k):
X에서 임의의 대응쌍 3개 선택.
세 대응쌍으로 최소제곱법으로 T_j 추정
세 대응쌍은 인라이어로 판단
m = X에서 세 대응쌍을 제외한 대응쌍 집합)
for p in m:
# T_j로 첫번째 대응쌍 요소를 변환시킨 p.1'와 p.2의 오차가 t보다 작은경우
# 인라이어로 판단
if (p.2 - T_j*p.1) < e:
p를 인라이어에 추가
# 인라이어의 개수가 d이상이 되면
if |inlier| >= d:
인라이어 요소(대응쌍)들로 새 T_j 계산
# 구한 T_j의 적합 오차가 e보다 작은경우 Q에 추가
if T_j 적합 오차 < e:
T_j를 Q에 추가
Q에 있는 것들 중에서 가장 좋은 변환행렬 T를 선정
m = len(keypoints1)
n = len(keypoints2)
mlist = []
for i in range(m):
shortest = inf
idx_lst = []
for j in range(n):
dist = get_dist(keypoints1[i], keypoints2[j])
if dist < smallest:
shortest = dist
if shortest < T:
ids_lst.append(j)
match = [i, min(idx_lst)]
mlist.append(match)
하지만 이 방법은 가능한 모든 특징 쌍들에 대해 계산하므로 너무 오랜 시간이 걸리게 된다.
이 외 방법으로 이진 검색 트리를 개조한 kd 트리나 해싱을 이용할수 있겠다.
이진 검색 트리 binary search tree
이진 검색 트리는 특정 노드를 기준으로 왼쪽은 작은 값 오른쪽은 큰 값을 가지는 트리를 말한다.
아래의 그림을 보면 값이 8인 루트 노드를 기준으로 왼쪽으로는 8보다 작고, 우측으로는 8보다 큰 값을 노드들이 갖는다.
검색 키를 4라고 지정하면,
1. 8보다 4가 작으므로 왼쪽 노드, 노드 값이 다르므로 한번 더
2. 3보다 4가 크므로 우측 노드, 노드 값이 다르므로 한번 더
3. 6보다 4가 작으므로 좌측 노드, 탐색 완료
이진 검색 트리를 이용해 3번만에 4를 찾을수 있었다.
이를 슈도 코드로 나타내면 아래와 같이 정리할수 있겟다.
트리의 깊이는 log(n)이므로 빅오 표기법으로 O(log(n)) 시간에 검색 마침
t = binary_search(T, v):
if (t = Null):
v가 존재하지 않음
else:
키가 v인 노드 t를 반환
def binary_search(t, v):
if t == Null:
return Null
if t.key == v:
return t
else:
if (v < t.key): # 탐색 값, 키가 현재 노드의키보다 작으면 왼쪽 노드 탐색
return binary_search(t.leftchild, v)
else: # 아니면 우측 자식 노드 탐색
return binary_search(t.rightchild, v)
빠른 특징 기술자 탐색을 위한 자료구조 - kd 트리
이진 트리 탐색 방법을 생각해보면
특징 벡터를 기반한 트리를 만들어 탐색함으로써
모든 특징벡터의 거리를 구해서 비교하는 기존 방법보다 빠르게 탐색할수 있겠다.
하지만 특징 벡터는 이진 트리처럼 1개의 값을 가지는게 아니라
SIFT 기술자는 128차원을 가지니 이에 맞는 트리 생성 알고리즘이 필요하다.
1975년 벤트리는 kd 트리를 제안함.
k dimension을 다루므로 kd 트리라 부르는것 같다.
kd 트리 분할 기준 2가지
1. 분할 축 선택
각 차원 분산 중 최대 분산을 가지는 차원 k를 분할 축으로 선정
2. 분할 기준 선정하기
차원 k 기준 정렬, 중앙값을 분할 기준으로 사용
X = [x_1, x_2, ...]
T = get_kdtree(X)
def get_kdtree(X):
if X == Null:
return Null
else if X == 1: #단일 노드이면
node.vector = X.m
node.leftchild, node.rightchilde = Null
return node
else:
k = 주어진 특징 벡터의 차원들 중에서 최대 분산을 갖는 차원
X_sorted = sort(X, k)
X_left, X_median, X_right = get_divided(X_sorted)
node.dim = k
node.vector = X_median
node.leftchild = get_kdtree(X_left)
node.rightchild = get_kdtree(X_right)
return node
kd 트리를 이용한 최근접 이웃 탐색
새 특징 벡터 x가 주어질 때, 최근접 이웃을 탐색해보자
1. 루트 노드의 분할기준 차윈 k을 확인하다.
2. 입력 벡터의 k차원 값과 루트노드의 기준값을 비교하여 좌측 혹은 우측 자식 노드의 입력으로 보낸다.
위 1과 2를 단일 노드에 도착할때까지 판복한다.
이렇게 탐색한 단일 노드는 항상 최근접이라는 보장은 없음.
다른 트리 가지에 찾아낸 노드 보다 가까운 벡터가 존재할수 있기 때문
개선된 최근접 이웃 탐색
위 방법에서는 반대편 노드를 확인하지 못해, 분명하게 최근접인지 확인할수 없음.
현재까지 탐색한 노드들을 스택에 저장 -> 깊이 방향 탐색 수행
리프 노드 도착시 백트래킹 수행 -> 입력 벡터 x와 스택에서 꺼낸 노드 t의 특정 차원 오차가 최근접 거리보다 작은경우
=> 노드 t의 반대편에 더 가까운 노드가 존재할 가능성이 있음. 재귀 탐색 수행
이 방법은 노드가 많더라도 log(n)으로 빠른 탐색이 가능,
하지만 특징 벡터의 차원이 10차원 이상일 경우 모든 특징벡터를 수행하는것과 비슷해져 성능이 떨어짐.
#지나온 노드를 저장할 스택 생성
stack s = null;
# 현재 최근접 거리
current_best = inf
# 현재 최근접 노드
nearest = Null
search_kdtree(T, x)
def search_kdtree(T, x):
t = T
# 리프 노드가 아니면
while is_leaf(t) == False:
# 현재 차원기준, x가 현재노드보드 작다
# 오른쪽 자식 노드는 스택에 대입
# 왼쪽 자식노드는 t에 대입
if x[t.dim] < t.vector[t.dim]
s.push(t, "right")
t = t.leftchild
else
s.push(t, "left")
t=t.rightchild
# 현재 노드 t와 x 거리 계산
d = dist(x, t.vector)
if d < current_best:
current_best = d
nearest = t
#거쳐온 노드에 대해 최근접 가능성 여부 확인
while s != null:
t1, other_side = s.pop()
#입력 벡터 x와 거쳐온 벡터 t1의 거리 계산
d = dist(x, t1.vector)
#거쳐왔던 벡터 t1이 최적이라면, 현재 최근접 거리와 벡터 변경
if d < current_best
current_best = d
nearest = t1
# (벡터 x의 어느 차원의 값 - t1의 어느 차원 값) < current_best
# t1의 반대편 t_other에 보다 가까운 점이 존재할 가능성이 있음.
if (x, t1.vector)의 분할 평면까지 거리 < current_best:
t_other = t1의 other_side 자식 노드
search_kdtree(t_other, x)
kd 트리를 이용한 근사 최근접 이웃 탐색
최근접 이웃이거나 가까운 이웃(근사 최근접 이웃 approximate nearest neighbor)을 찾는 방법
위 방법에서 스택 대신 우선 순위 큐인 힙을 사용
x로부터 거리가 우선순위로 이용됨.
-> 거쳐온 순서대로 백트래킹을 수행한 스택과 달리, 가장 거리가 가까운 노드부터 수행
추가로 try_allowed로 조사 횟수제한
#지나온 노드를 저장할 힙 생성
heap s = null;
# 현재 최근접 거리
current_best = inf
# 현재 최근접 노드
nearest = Null
# 비교 횟수
try = 0
search_kdtree(T, x)
def search_kdtree(T, x):
t = T
# 리프 노드가 아니면
while is_leaf(t) == False:
# 현재 차원기준, x가 현재노드보드 작다
# 오른쪽 자식 노드는 스택에 대입
# 왼쪽 자식노드는 t에 대입
if x[t.dim] < t.vector[t.dim]
s.push(t, "right")
t = t.leftchild
else
s.push(t, "left")
t=t.rightchild
# 현재 노드 t와 x 거리 계산
d = dist(x, t.vector)
if d < current_best:
current_best = d
nearest = t
try++
#지정한 횟수만큼 우선순위 노드를 백트랙킹으로 탐색 후에는 종류
if try > try_allowed:
탐색 종료
#거쳐온 노드에 대해 최근접 가능성 여부 확인
while s != null:
t1, other_side = s.pop()
#입력 벡터 x와 거쳐온 벡터 t1의 거리 계산
d = dist(x, t1.vector)
#거쳐왔던 벡터 t1이 최적이라면, 현재 최근접 거리와 벡터 변경
if d < current_best
current_best = d
nearest = t1
# (벡터 x의 어느 차원의 값 - t1의 어느 차원 값) < current_best
# t1의 반대편 t_other에 보다 가까운 점이 존재할 가능성이 있음.
if (x, t1.vector)의 분할 평면까지 거리 < current_best:
t_other = t1의 other_side 자식 노드
search_kdtree(t_other, x)