안녕하세요.

 

 

이번에는 다트 게임이라고 하는 문제를 풀어보려고 합니다.

 

 

저는 문제에 나온 그대로 구현을 해봤는데, 다른 답안을 보니 정규표현식을 이용해서 푼 답안이 있어 이를 같이 소개해드리고자 합니다.

 

 

정규식은 다양한 문자열 관련 문제에 활용될 수 있는 만큼, 이번 기회에 잘 알아두면 도움이 많이 될 것 같습니다.

 

 

https://school.programmers.co.kr/learn/courses/30/lessons/17682

 

프로그래머스

코드 중심의 개발자 채용. 스택 기반의 포지션 매칭. 프로그래머스의 개발자 맞춤형 프로필을 등록하고, 나와 기술 궁합이 잘 맞는 기업들을 매칭 받으세요.

programmers.co.kr

 

 

 

 

 

 

제가 푼 풀이:

def solution(dartResult):
    answer = 0
    
    num = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
    s_ = [] # score 저장
    b_ = [] # 보너스 저장
    o_ = [] # 옵션 저장
    
    # 문자열이 존재하는 경우 while문
    while dartResult:
        # 숫자 관련
        if len(dartResult) >= 2 and str(dartResult[:2]) == '10':
            s_.append(10); dartResult = dartResult[2:] # 첫 번째 숫자 10으로 빼고 문자열 밀기
        elif int(dartResult[0]) in num:
            s_.append(int(dartResult[0])); dartResult = dartResult[1:]
            
        # 보너스 관련
        if str(dartResult[0]) == 'S':
            b_.append('S')
        elif str(dartResult[0]) == 'D':
            b_.append('D')
        elif str(dartResult[0]) == 'T':
            b_.append('T')
        dartResult = dartResult[1:] # 얘는 공통이라
        
        # 옵션 관련
        if len(dartResult) >= 1 and str(dartResult[0]) == '*':
            o_.append('*')
            dartResult = dartResult[1:]
        elif len(dartResult) >= 1 and str(dartResult[0]) == '#':
            o_.append('#')
            dartResult = dartResult[1:]
        else:
            o_.append(" ")
    
    sol_ = []
    
    for i in range(3):
        # 제곱 작업 우선 진행
        if b_[i] == 'S':
            sol_.append(s_[i] ** 1)
        elif b_[i] == 'D':
            sol_.append(s_[i] ** 2)
        elif b_[i] == 'T':
            sol_.append(s_[i] ** 3)
    
    
    for i in range(3):
        # 아무것도 없는 경우, 아무 작업 안함
        if o_[i] == " ":
            continue
        
        # 첫 번째 나오고 스타상인 경우
        if o_[i] == '*' and i == 0:
            sol_[i] = sol_[i] * 2
        elif o_[i] == '*' and i != 0:
            sol_[i-1] = sol_[i-1] * 2 # 전에 얻은 점수 2배
            sol_[i] = sol_[i] * 2 # 해당 점수 2배
        
        # 아차상
        if o_[i] == '#':
            sol_[i] = sol_[i] * -1    
    
    return sum(sol_)

딱 봐도 문제 풀이 자체가 매우 긴 것을 확인하실 수 있습니다.

 

정직하게 문제에 나온 그대로를 코드로 구현한 것이라고 볼 수 있는데요.

 

score와 보너스, 옵션을 저장할 list를 별도로 지정하고 문자열에 대해서 while문을 돌면서 이 문자열을 score와 보너스, 옵션으로 분해하는 작업을 우선적으로 진행합니다.

 

먼저 숫자 관련 부분을 보시면, 

 

 while dartResult:
        # 숫자 관련
        if len(dartResult) >= 2 and str(dartResult[:2]) == '10':
            s_.append(10); dartResult = dartResult[2:] # 첫 번째 숫자 10으로 빼고 문자열 밀기
        elif int(dartResult[0]) in num:
            s_.append(int(dartResult[0])); dartResult = dartResult[1:]

 

문자열의 길이가 2 이상이고(해당 조건 없으면 dartResult [:2]에서 index 에러가 발생합니다), 앞에 두 글자가 10인 경우는 score 부분을 10으로 append 하고, 문자열을 밀어줍니다.

 

10이 아닌 0부터 9인 경우는 in num으로 조건을 걸어서 숫자를 빼는 식으로 진행했습니다.

 

 

# 보너스 관련
        if str(dartResult[0]) == 'S':
            b_.append('S')
        elif str(dartResult[0]) == 'D':
            b_.append('D')
        elif str(dartResult[0]) == 'T':
            b_.append('T')
        dartResult = dartResult[1:] # 얘는 공통이라

 

보너스 관련해서는 그다음 문자열이 S D T 중 하나이면 이를 보너스 관련 list에 추가해 주고, 문자열을 하나 밀었습니다.

 

S D T 중 뭐든 간에 1칸을 밀어주니 if 문에 넣지 않고 if문 바깥쪽에 미는 코드를 위치했습니다.

 

# 옵션 관련
        if len(dartResult) >= 1 and str(dartResult[0]) == '*':
            o_.append('*')
            dartResult = dartResult[1:]
        elif len(dartResult) >= 1 and str(dartResult[0]) == '#':
            o_.append('#')
            dartResult = dartResult[1:]
        else:
            o_.append(" ")

 

옵션 관련해서는 * 이거나 #인 경우 이를 옵션 관련 list에 추가하도록 했습니다.

 

len을 점검하는 코드가 있는 이유는, 옵션은 선택사항이라 옵션 자체가 아예 없는 경우가 있습니다. 이럴 때는 dartResult에 문자열 자체가 안 남아버려서 dartResult [0]로 접근이 안되고 index 에러가 발생합니다. 따라서 이를 방지하고자 문자열이 1개 이상 남은 경우에만 뽑아내도록 했습니다.

 

sol_ = []
    
    for i in range(3):
        # 제곱 작업 우선 진행
        if b_[i] == 'S':
            sol_.append(s_[i] ** 1)
        elif b_[i] == 'D':
            sol_.append(s_[i] ** 2)
        elif b_[i] == 'T':
            sol_.append(s_[i] ** 3)

 

그다음 부분은 먼저 제곱 작업을 우선적으로 진행해서, 값들을 sol_에 추가했습니다.

 

 

for i in range(3):
        # 아무것도 없는 경우, 아무 작업 안함
        if o_[i] == " ":
            continue
        
        # 첫 번째 나오고 스타상인 경우
        if o_[i] == '*' and i == 0:
            sol_[i] = sol_[i] * 2
        elif o_[i] == '*' and i != 0:
            sol_[i-1] = sol_[i-1] * 2 # 전에 얻은 점수 2배
            sol_[i] = sol_[i] * 2 # 해당 점수 2배
        
        # 아차상
        if o_[i] == '#':
            sol_[i] = sol_[i] * -1    
    
    return sum(sol_)

 

옵션에 별도로 없는 경우는 아무 작업 안 하고 continue로 넘어가도록 하였고,

 

 

*이 첫 번째 나오는 경우는 자기 자신의 값만 2배, *이 첫 번째가 아닌 곳에 나오는 경우는 전에 얻은 점수와 지금 점수를 모두 2배 하도록 했습니다.

 

그리고 #이 나오는 경우는 -1을 곱해주도록 했고, 마지막에는 sum 해서 결과가 나오도록 했습니다.

 

어떻게 보면 문제에 나온 내용을 말 그대로 충실히 진행해서 나온 코드라고 보시면 될 것 같습니다.

 

하지만 코드 수가 길고, 효율적이지는 못해서 더 효율적인 코드가 필요합니다.

 

 

 

 

개선된 코드:

import re

def solution(dartResult):
    bonus = {'S' : 1, 'D' : 2, 'T' : 3}
    option = {'' : 1, '*' : 2, '#' : -1}
    p = re.compile('([0-9]+)([SDT])([*#]?)')
    dart = p.findall(dartResult)
    for i in range(len(dart)):
        if dart[i][2] == '*' and i > 0: # i가 0보다 클때는 전에꺼 2배 해줌
            dart[i-1] *= 2
        dart[i] = int(dart[i][0]) ** bonus[dart[i][1]] * option[dart[i][2]] # 보너스로 제곱해주고 option에 맞게 곱해줌

    answer = sum(dart)
    return answer

 

개선된 코드는 정규표현식을 활용한 코드입니다.

 

이는 p = re.compile() 코드를 보시면 알 수 있습니다.

 

정규표현식을 compile하는 re.compile은 ( )를 통해 구분이 되는데요. 즉, 해당 코드에서는 크게 ([0-9]+) 부분과 ([SDT]) 부분, ([*#]?) 부분으로 총 3 부분으로 나눠집니다.

 

[0-9]는 0부터 9까지 숫자 중에 일치되는 경우를 찾는 정규표현식입니다. 여기서 +는 1개 이상을 뜻하므로, ([0-9]+)는 숫자이면서 1개 이상인 경우를 찾게 됩니다.

 

+가 들어가서 0~9인 숫자 뿐만 아니라 10인 경우도 찾을 수 있게 됩니다.

 

다음으로 ([SDT])는 S D T 중에 일치되는 경우를 찾게 되는 정규표현식입니다.

 

마지막으로 ([*#]?)는 두 번째 정규표현식 파트와 비슷하니 대충 유추하실 수 있겠지만, *이나 #인 경우를 찾습니다.

 

단, ? 는 나오지 않을 수도 있고, 1번 나올 수도 있습니다.

 

문제에서 옵션의 경우 *이거나 #이고, 혹은 아예 없을 수 있다고 했기 때문에, 이를 구현하기 위해 ?를 사용했다고 볼 수 있습니다.

 

1개 이상일 때는 +를, 0개 혹은 1개 일 때는 ?를 사용한다고 기억하면 좋겠습니다.

 

 

그러고 나서 p.findall()를 활용해 dartResult에서 compile 한 정규표현식 규칙을 만족하는 경우를 모두 찾은 것을 dart에 저장합니다.

 

 dart를 print 해보면 다음과 같이 나오는 것을 확인할 수 있습니다.

 

 

보시면 전체적으로는 list이고, 안에는 3개의 element로 이루어진 튜플인 것을 알 수 있습니다.

 

이에 대해서 for문을 수행하게 됩니다.

 

다음으로 if dart[i][2] == '*' and i > 0: 부분인데요. 이는 i가 0보다 클 때는 *이 적용되었을 때 이전 score에도 2배를 해줘야 하기 때문에 dart[i-1] *= 2를 해준다고 보시면 됩니다.

 

그리고 dart[i] = int(dart[i][0]) ** bonus[dart[i][1]] * option[dart[i][2]]로 구현을 해서 최종 마지막 점수를 내는 것을 확인할 수 있습니다.

 

score 값에 bonus에 해당하는 dictionary에서 찾아 이를 제곱으로 곱해주고, 마지막으로 option에 해당하는 *이나 #인 경우는 2배 혹은 -1배를 적용해 주는 코드입니다.

 

마지막으로는 sum을 해서 결과를 내주면 됩니다.

 

 

 

확실히 정규표현식을 활용하면 조금 더 손쉽게 score와 보너스, 옵션을 찾을 수 있었고, 이를 dictionary를 활용해서 간편하게 최종 점수를 계산할 수 있었습니다.

 

 

이번 문제는 여기까지 하도록 하겠습니다.

 

 

 

 

 

 

안녕하세요. 오늘은 Level 1 문제인 실패율 문제를 풀어보려고 합니다.

 

 

이번 문제는 어떻게 접근하느냐에 따라서 시간 복잡도 관점에서 차이가 많이 있어 이 부분을 중점적으로 보려고 합니다.

 

 

https://school.programmers.co.kr/learn/courses/30/lessons/42889#

 

프로그래머스

코드 중심의 개발자 채용. 스택 기반의 포지션 매칭. 프로그래머스의 개발자 맞춤형 프로필을 등록하고, 나와 기술 궁합이 잘 맞는 기업들을 매칭 받으세요.

programmers.co.kr

 

 

 

 

 

 

 

 

 

제가 푼 풀이:

def solution(N, stages):
    fail = {}
    
    for i in range(1, N+1):
        pass_cnt = 0
        succ_cnt = 0
        for s in stages:
            # stage가 i보다 크면 지나왔고, 해당 스테이지도 성공한 것
            if s > i:
                pass_cnt += 1
                succ_cnt += 1
            # 같기만 하면 지나오기만 한 것
            elif s == i:
                pass_cnt += 1
        
        # 한번도 안 지나간 stage는 0으로 처리해야 하므로 별도로 if문으로 뺀다
        if pass_cnt != 0:
            fail[i] = (pass_cnt - succ_cnt) / pass_cnt
        else:
            fail[i] = 0
    
    fail_rate = sorted(fail, key=lambda x: fail[x], reverse = True)
    
    return fail_rate

스테이지가 N개 있으므로, N개에 대해서 for문을 진행해야 하는 문제입니다.

 

반복문을 진행할 때, pass를 몇 명 했는지, 스테이지를 깬 사람은 몇 명인지 저장하기 위해서 변수를 설정해 줍니다.

 

그리고 결과에 대한 list인 stages에 대해서 반복문을 적용하면서, 각각이 i보다 크면 해당 stage를 지나왔고 성공한 것이므로 각각을 1개씩 올려주며, 같은 경우는 지나오기만 했으므로 pass_cnt를 하나 올려주는 방식으로 접근합니다.

 

그리고 실패율을 저장하는 dictionary인 fail에 대해서 값들을 저장해 줍니다.

 

만약 pass_cnt가 0인 경우는 해당 stage에 도달한 사람이 아무도 없으므로, 실패율을 0으로 저장합니다.

 

마지막으로 fail은 dictionary인데, 이를 for문으로 print 하면 key값이 나오게 됩니다.

 

그런데 fail의 키 값은 1, 2, 3... 등의 각 stage의 이름이므로, fail의 각 stage를 key로 잡아서 sort를 하면 문제에서 얘기하는 대로 실패율이 높은 순으로 배열을 return 할 수 있습니다.

 

하지만 위 풀이는 시간 복잡도 관점에서 간당간당하게 풀어진 케이스라, 조금 더 시간 복잡도 관점에서 생각해 볼 필요가 있어서 다양한 코드들을 참고해서 더욱 효과적인 풀이를 만들어냈습니다.

 

 

 

새로 만든 풀이:

from collections import Counter
def solution(N, stages):
    # stage 사람 수 저장
    cnt = len(stages)
    stage_dict = Counter(stages)
    failure = {}
    for i in range(1, N + 1):
        if cnt != 0: 
            # 딱 i인 경우라면 도전은 했지만 못 깬 경우니까 실패율의 분자가 됨.
            failure[i] = stage_dict[i] / cnt
            # stage i에 있는 인원은 그만큼 차감해줌(이미 해당 stage에서 count 했으니까)
            cnt -= stage_dict[i]
        else:
            failure[i] = 0
    
    # dictionary를 그냥 뽑으면 key가 나오게 된다.
    # 그래서 failure[i]를 하면 value 기준으로 sort 되고, 나오는 것은 key가 된다.
    return sorted(failure, key=lambda i: failure[i], reverse=True)

collections 패키지에 있는 Counter라는 함수는 중복된 데이터가 저장된 list에 사용하면 각 원소가 몇 번씩 나오는지 저장된 객체를 만들어 냅니다.

 

여기서 stages의 경우 중복된 값이 존재하는 경우가 많을 수 있고, 이럴 때는 굳이 모든 요소들을 다 for문으로 살펴볼게 아니라 Counter를 활용하게 되면 말 그대로 stages에 존재하는 요소의 가짓수 정도만 for문을 돌리면 되니까, 최소한 전체를 for문으로 돌리는 것보다는 시간 복잡도를 줄일 수 있게 됩니다.

 

 

그리고 풀이 아이디어 관점에서도 변경이 있었습니다.

 

먼저 stages에 있는 개수를 cnt로 저장하고, stages를 Counter 함수를 이용해서 dictionary 형태의 객체를 만들어냅니다.

 

그리고 동일하게 N개의 stage에 대해서 for문을 진행합니다.

 

cnt가 0인 경우는 해당 stage에 도달한 케이스가 0개 이므로, 이는 실패율을 0으로 저장해 줍니다.

 

그렇지 않은 경우는, stages에서 i에 해당하는 값의 개수를 실패율의 분자로 사용하고, cnt를 분모로 사용합니다.

 

저도 처음에 이 내용이 이해가 안 갔는데, 예시를 들어서 생각해 보겠습니다. (첫 번째 Test case)

 

for문에서 i가 1인 경우 (stage 1인 경우), 현재 stages에서 1인 경우가 1개 있으므로 stage_dict[1]은 1이 될 것이고, cnt는 8이 됩니다. 따라서 이 경우에는 실패율이 1/8로 계산됩니다.

 

for문에서 i가 2인 경우 (stage 2인 경우), 현재 stages에서 2인 경우가 3개 있으므로 stage_dict[2]은 3이 될 것이고, cnt는 stage 1에서 한 명 있었으니 이를 제외하고 7이 될 것입니다. 따라서 이 경우에는 실패율이 3/7로 계산됩니다.

 

여기서 stages에서 i에 해당하는 값의 개수를 실패율의 분자로 사용하는 이유는, stages에서 각 자연수는 사용자가 현재 도전 중인 스테이지의 번호를 나타내기 때문입니다.

 

즉, 해당 스테이지를 도전하고 있는 사람의 수 이기 때문에, 이를 정의된 실패율의 분자인 스테이지에 도달했으나 아직 클리어하지 못한 플레이어의 수와 동일한 것이죠.

 

그리고 여기서 카운팅 된 만큼은 전체 인원수에서 빼줘야 하므로, cnt에서 그만큼을 빼주면 됩니다.

 

 

마지막으로 실패율이 저장된 dictionary를 value 기준으로 내림차순으로 저장해 주면 정답이 됩니다.

 

 

처음 제가 풀었던 답안의 경우 시간 복잡도가 가장 높은 테스트 22에서 8116.53ms를 받았고, 개선한 답안의 경우 테스트 22에서 12.09ms를 받았으니, 거의 600배 이상 시간 복잡도 차이를 보였습니다.

 

Counter라고 하는 함수를 잘 활용하면, 여러모로 다른 문제에서도 시간 복잡도를 줄일 수 있을 것 같습니다.

 

 

오늘 문제는 여기까지 하겠습니다.

 

 

 

 

 

 

안녕하세요.

 

 

이번에 다뤄볼 문제는 K번째수 라는 문제입니다.

 

 

사실 문제 자체가 어렵지는 않아서, 풀이가 어렵지는 않은데요.

 

 

참고한 답안이 워낙 깔끔해서, 이를 한번 짚고 넘어가 보기 위해 글로 정리하게 되었습니다.

 

 

문제는 다음과 같습니다.

 

 

https://school.programmers.co.kr/learn/courses/30/lessons/42748

 

프로그래머스

코드 중심의 개발자 채용. 스택 기반의 포지션 매칭. 프로그래머스의 개발자 맞춤형 프로필을 등록하고, 나와 기술 궁합이 잘 맞는 기업들을 매칭 받으세요.

programmers.co.kr

 

 

문제를 한번 보시고, 어떻게 풀면 좋을지 고민해 보신 후 봐주시면 좋을 것 같습니다.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

제가 푼 풀이:

 

def solution(array, commands):
    answer = []
    
    for c in commands:
        answer.append(sorted(array[c[0]-1:c[1]])[c[2]-1])
    
    return answer

 

 

문제에서 commands가 주어졌을 때, commands의 모든 원소에 대해서 연산을 적용해야 한다고 했으므로 여기에 대해서 for문을 돌린다고 생각했습니다. 각 요소를 c로 정의했고요.

 

array에서 c[0]번째 숫자부터 c[1]번째 숫자까지 잘라야 하므로, 이를 array[c[0]-1:c[1]]로 구현했습니다.

 

우리가 n번째라고 말하지만, python에서는 index가 0부터 시작이므로 -1을 해주어야 합니다.

 

그리고 이를 정렬한 후, c[2]번째 있는 수를 빼내야 하므로 sorted(array[c[0]-1:c[1]])[c[2]-1]로 구현하였고, answer에 append해서 답을 냈습니다.

 

 

 

 

 

참고한 답안:

def solution(array, commands):
    return list(map(lambda x:sorted(array[x[0]-1:x[1]])[x[2]-1], commands))

 

한 줄의 코드로 표현한 답안입니다.

 

사실 제가 푼 것과 동일하지만, 이를 map과 lambda를 통해서 구현한 코드라고 보시면 될 것 같습니다.

 

일단 lambda 함수에 대해서 설명해 보자면, def를 활용해서 어떤 이름을 주고 함수를 만들 필요가 없는 경우 사용을 하게 됩니다. 즉, 결국 함수에 해당한다는 것이죠.

 

이 코드에서 lambda x:sorted(array[x[0]-1:x[1]])[x[2]-1]에 해당하는 것은 x라는 입력이 들어왔을 때, 출력을 sorted(array[x[0]-1:x[1]])[x[2]-1]로 내겠다는 의미입니다.

 

그리고 map의 경우는, map(함수, list) 형식으로 사용하게 되는데 list에 있는 각 요소들에 대해서 함수를 일괄 적용하겠다는 의미입니다.

 

즉, 위 코드는 lambda라는 함수를 commands라는 list 안에 있는 요소들에게 모두 적용하겠다. 그리고 이것을 list로 만들어서 return 하겠다는 코드로 보시면 될 것 같습니다.

 

 

 

이번 문제는 여기서 마무리하도록 하겠습니다.

 

 

감사합니다.

 

 

 

 

 

 

 

안녕하세요.

 

 

최근에 코딩테스트 준비를 조금씩 해보고 있는데요.

 

 

하다 보니 제가 모르는 파이썬의 기능들도 많이 있고, 또 나름대로 생각하고 구현하는 시간을 가질 수 있다는 점에서 좋은 것 같습니다.

 

 

또 얼마나 시간을 투자할 수 있을지 모르겠지만, 앞으로 제가 문제 풀면서 알게 된 점이랑 공유할 점들을 한번 정리해서 올려볼까 합니다.

 

 

첫 번째로 다뤄볼 문제는 '문자열 내 마음대로 정렬하기'라는 문제입니다.

 

 

https://school.programmers.co.kr/learn/courses/30/lessons/12915

 

프로그래머스

코드 중심의 개발자 채용. 스택 기반의 포지션 매칭. 프로그래머스의 개발자 맞춤형 프로필을 등록하고, 나와 기술 궁합이 잘 맞는 기업들을 매칭 받으세요.

programmers.co.kr

 

 

문제를 한번 보시고, 어떻게 풀면 좋을지 고민해보신 후 봐주시면 좋을 것 같습니다.

 

 

스포 방지를 위해 풀이는 접은글로 처리하고 있으니, 눌러서 확인해보시면 됩니다.

 

 

제가 푼 풀이:

더보기
def solution(strings, n):
    answer = []

    # 1. n번째 요소를 먼저 모은다.
    element = []
    for s in strings:
        element.append(s[n])
    
    # 2. 요소들을 set으로 묶은 다음에 sort한다.
    ele_lst = sorted(list(set(element)))
    
    # 3. 정렬된 요소 list에서 하나씩 빼면서, strings을 돌며 해당하는 요소랑 같은 애들을 빼고
    # 뺀 애들을 오름차순으로 정렬한 다음 answer에 다시 insert한다.
    for ele_l in ele_lst:
        sample_part = []
        for s in strings:
            if s[n] == ele_l:
                sample_part.append(s)
        answer.extend(sorted(sample_part))
    
    return answer

 

처음에는 단순히 n번째 요소만 가지고 sort하는 방식을 생각했는데, 문제에서 '인덱스 n의 문자가 같은 문자열이 여럿 일 경우, 사전순으로 앞선 문자열이 앞쪽에 위치합니다.'라는 조건이 있었습니다.

 

그래서 단순히 n번째 요소만 가지고 sort를 하게 되면 문제가 발생합니다.

 

 이를 해결하기 위해서 단계적으로 접근하는 방법을 생각해 보았는데요.

 

먼저 n번째 요소를 list에 담고, 이를 set으로 묶은 다음 list로 만들고 오름차순으로 만들어줍니다.

 

그런 다음, 이 정렬된 요소 list에서 하나씩 빼면서, strings에 대해 for문을 돌면서 n번째 요소가 정렬된 요소 list에서 나온 요소와 같은 경우만 append 한 다음, 이를 sort 해서 answer에 extend해주는 방식을 생각했습니다.

 

list에서 extend를 하는 경우는 list 안에 있는 element들을 그대로 list 안에 넣어줄 때 사용합니다. (많이 사용하는 append와의 차이점)

 

순차적으로 봤을 땐 나쁘지 않은 풀이인 것 같으나, 아무래도 조금 더 간단한 풀이가 있으니 간단하게 풀 수 있는 방법도 괜찮을 것 같습니다.

 

 

참고한 답안:

더보기
def solution(strings, n):
    answer = sorted(strings, key = lambda x: (x[n], x))
    
    return answer

이 문제를 그냥 한 줄로 풀 수 있는 코드인데요.

 

저도 처음에 sorted에서 lambda를 사용하는 방법은 생각을 했으나, 단순히 n번째 요소만 가지고 정렬을 해보았더니 안되어서 이 방법 말고 위에서 언급한 방법으로 갔었습니다.

 

sorted에서 key로 정렬해 줄 때, 조건을 여러 개 걸 수 있다는 점을 알고 가면 좋은 답안이라고 생각합니다.

 

lambda x: (x[n], x) 이런 형식으로 괄호로 묶어주면, 다중 조건 정렬이 가능합니다.

 

 

 

이번 글은 여기서 마무리하도록 하겠습니다.

 

 

감사합니다.

 

안녕하세요. 오랜만에 블로그 포스팅 올려보네요.

 

 

일하다가 torch.cdist라는 함수와 마주치게 되었는데, 마침 구글링을 해보니 한국어 자료가 0개여서 누군가에게 도움이 되고자 이렇게 포스팅을 만들게 되었습니다.

 

 

그럼 시작해보도록 하겠습니다.

 

 

torch.cdist의 공식 document의 주소는 다음과 같습니다.

 

 

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

 

torch.cdist — PyTorch 1.12 documentation

Shortcuts

pytorch.org

 

해당 함수에 대해서 설명하는 내용을 읽어보면, 'Computes batched the p-norm distance between each pair of the two collections of row vectors'라고 적혀 있습니다.

 

 

쉽게 말씀드리자면, 두 개의 벡터에서 각 행의 pair끼리의 p-norm distance를 구한 것이라는 설명입니다.

 

 

물론 이렇게 말씀드리면 이해하기가 어려우실 수 있으니, 이번에도 예시를 통해서 설명드리도록 하겠습니다.

 

 

p-norm에 대한 설명은 다른 블로그들이나 자료들을 참고해서 이해해주시고 본 포스팅에서는 별도로 설명드리지 않습니다.

 

 

import torch
import numpy as np

 

먼저 예시를 만들기 위한 library 두개를 import 해줍니다. pytorch와 numpy가 필요합니다.

 

 

a = torch.tensor(np.arange(1, 7).reshape(1, 3, 2), dtype=torch.float64)
print(a)

b = torch.tensor(np.arange(0.1, 0.9, 0.1).reshape(1, 4, 2))
print(b)

예시를 위해서 1부터 6까지 구성되는 numpy array를 만들고, (1, 3, 2)로 reshape 한 뒤에 torch tensor로 변환합니다.

 

또, 0.1부터 0.8까지 구성되는 numpy array를 만들고, (1, 4, 2)로 reshape 한 뒤에 torch tensor로 변환합니다.

 

참고로 torch.cdist는 column의 size가 동일한 vector끼리만 연산이 가능합니다. 따라서, 두 vector는 2차원으로 만들어두었습니다.

 

위 코드를 실행하면 다음과 같은 결과를 얻을 수 있습니다.

 

 

 

print(torch.cdist(a, b))

 

 

앞에서 선언한 a와 b를 torch.cdist를 이용해서 연산해줍니다.

 

 

이를 이용하면 다음과 같은 결과를 볼 수 있습니다.

 

 

 

 

값들이 꽤나 복잡하죠? 이에 대한 설명을 드리도록 하겠습니다.

 

 

 

앞에서 정의했던 a와 b는 다음과 같은 vector입니다. shape는 각각 Bx3x2와 Bx4x2로 정의하였습니다. B는 Batch size이며, 현재 예시에서는 1로 적용하였습니다. 

 

 

torch.cdist에서는 a 벡터의 row와 b 벡터의 row를 각각 pair로 만들고, 이에 대한 distance를 계산하게 됩니다.

 

 

a 벡터의 첫 번째 row와 b 벡터의 첫 번째 row, a 벡터의 첫 번째 row와 b 벡터의 두 번째 row.... 이런식으로 해서 a 벡터의 세 번째 row와 b 벡터의 네 번째 row까지 모두 pair를 만들면 총 몇 개의 pair가 만들어질까요?

 

 

3x4=12개의 pair가 만들어집니다.

 

 

그래서 위에서 보여드린 document에서는 다음과 같은 내용을 담고 있습니다.

 

 

 

만약 x1이 P 개의 row를 가지고 있고, x2가 R 개의 row를 가지고 있으면, 총 P x R 개의 pair가 만들어지므로, 결과값의 shape가 P x R 이라는 얘기입니다.

 

 

다시 위 예시로 돌아가서, 직접 한 개씩 연산을 진행해보겠습니다.

 

 

 

먼저 a 벡터의 첫 번째 row와 b 벡터의 첫 번째 row를 pair로 만들고, 이 둘 간의 distance를 계산해줍니다.

 

 

이를 output 벡터의 (1, 1) 위치에 적어줍니다.

 

 

 

다음으로는 a 벡터의 첫 번째 row와 b 벡터의 두 번째 row를 pair로 만들고, 이 둘 간의 distance를 계산해줍니다.

 

 

이를 output 벡터의 (1, 2) 위치에 적어줍니다.

 

 

이러한 방식으로 진행하기 때문에, output 벡터의 column 개수는 b 벡터의 row와 같아지게 됩니다. 

 

 

왜냐하면 b 벡터의 row 개수 만큼 계산을 진행해서, 이를 가로로 펴는 방식이기 때문이지요.

 

 

다음으로 a 벡터의 두 번째 row와 pair를 지어볼까요?

 

 

 

 

아무래도 수식이 커지다보니 그림이 작아졌네요.

 

 

a 벡터의 두 번째 row와 b 벡터의 첫 번째 row가 pair가 되어, 계산 결과가 output의 (2, 1) 위치에 오게 됩니다.

 

 

즉, 이러한 방식으로 진행되므로 output 벡터의 row 개수는 a 벡터의 row 개수가 같아지게 됩니다.

 

 

앞에서 설명한 방식을 전부 실행하면 다음과 같은 결과를 볼 수 있습니다.

 

 

 

 

 

 

이번 포스팅에서는 간단하게 torch.cdist 함수가 어떻게 작동하는지 예시를 통해서 준비해보았습니다.

 

 

감사합니다.

 

 

 

 

 

cv2.error:OpenCV(3.4.2) /tmp/build/80754af9/opencv-suite_1535558553474/work/modules/core/src/arithm.cpp:223:error:(-209:sizes of input arguments do not match) The operation is neither 'array op array' (where arrays have the same size and type), nor 'array op scalar' nor 'scalar op array' in function 'binary_op'

 

 

Python의 OpenCV(cv2)에서 cv2.bitwise_and()를 사용할 때 발생했던 에러입니다.

 

 

해당 오류가 발생하는 이유는 굉장히 다양해서, 해당 에러에 대한 해결방안을 적은 블로그 글에서 조차 다양한 해결방안을 제시하고 있습니다.

 

 

해당 에러에서의 핵심은 'where arrays have the same size and type'이라고 보여지며, 말 그대로 opencv 연산을 하고자 하는 array의 size와 type이 같아야 한다는 점입니다.

 

 

제 케이스에서는 size가 같았으나 에러가 나서, 변수의 .dtype을 찍어보니 하나는 float32였고, 다른 하나는 float64이였습니다.

 

 

이에 대응하고자 float64인 이미지를 .astype(np.float32)로 처리하여 float32로 맞춰주었더니 해결되었습니다.

 

 

 

 

안녕하세요.

 

오늘은 기존의 Convolutional Neural Network의 구조를 조금 더 효율적인 작동 형태로 변화하는 것을 시도한 논문인 MobileNet V1 논문을 살펴보려고 합니다.

 

논문의 주소는 다음과 같습니다.

 

https://arxiv.org/abs/1704.04861

 

MobileNets: Efficient Convolutional Neural Networks for Mobile Vision Applications

We present a class of efficient models called MobileNets for mobile and embedded vision applications. MobileNets are based on a streamlined architecture that uses depth-wise separable convolutions to build light weight deep neural networks. We introduce tw

arxiv.org

 

그럼 시작해보겠습니다.

 

 

Abstract

 

 

저자들은 본 논문에서 mobile과 embedding vision applications에서 사용할 수 있는 효율적 모델인 MobileNets를 제시합니다.

 

MobileNets는 가벼운 deep neural network를 만들기 위해 depth-wise separable convolution을 사용하여 간소화된 architecture를 기반으로 하고 있습니다.

 

저자들은 latency와 accuracy 사이의 trade-off를 조절하는 두 개의 간단한 global hyper-parameter를 도입합니다.

 

이러한 hyper-parameters는 model을 만드는 사람이 문제의 제약에 기반해서 적절한 사이즈의 모델을 선택할 수 있도록 도와줍니다.

 

저자들은 resource와 accuracy 사이의 tradeoff에 대한 광범위한 실험을 제시하고 있으며, ImageNet classification에서 유명한 다른 모델들과 비교했을 때 강력한 성능을 낸다는 사실을 보여줍니다.

 

그러고 나서 object detection, finegrain classification, face attribute, large scale geo-localization을 포함한 다양한 use case에서 MobileNet의 효과성을 검증합니다.

 

 

1. Introduction

 

 

AlexNet이 ImageNet Challenge인 ILSVRC 2012에서 승리한 이후로, computer vision 분야에서 Convolutional neural network는 아주 흔해졌습니다.

 

일반적인 경향은 더 높은 accuracy를 달성하기 위해서 더욱 deep 하고 복잡한 모델을 만드는 것이었습니다.

 

하지만, 이러한 accuracy에서의 진보는 네트워크를 size와 speed 측면에서 더욱 효율적이게 만들지 않습니다.

 

로보틱스나 자율주행차, 증강현실과 같은 현실 세계의 application에서는, recognition task가 연산적으로 한계가 있는 플랫폼에서 timely fashion으로 이루어질 필요가 있습니다. 

 

 

본 논문에서는 mobile과 embedded vision application에서의 design requirement에 쉽게 매칭 될 수 있는 매우 작고, low latency를 가지는 모델을 만들고자 효율적인 네트워크 아키텍처와 두 개의 hyper-parameter를 제시합니다.

 

Section 2에서는 작은 모델을 만드는 것에 대한 선행 연구들을 리뷰하고, Section 3에서는 MobileNets을 더욱 작고 더욱 효율적으로 정의할 수 있는 width multiplier와 resolution multiplier와 MobileNet architecture를 제시합니다.

 

Section 4에서는 ImageNet에 대한 실험과 더불어 다양한 application과 use case에서의 실험을 제시합니다.

 

Section 5에서는 summary 및 결론으로 마무리합니다.

 

 

2. Prior Work

 

 

 

생략하고 넘어갑니다.

 

 

 

 

3. MobileNet Architecture

 

 

이번 section에서, 저자들은 첫 번째로 MobileNet을 만드는데 가장 중요한 layer인 depthwise separable filter에 대해서 설명합니다.

 

그러고 나서 MobileNet network structure를 설명하고, 두 개의 모델 shrinking hyper-parameter인 width multiplier와 resolution multiplier에 대해서 설명하고 마무리합니다.

 

 

 

3.1 Depthwise Separable Convolution

 

 

MobileNet model은 분리된 convolution의 한 형태인 depthwise separable convolution을 기반으로 하고 있으며, 이는 standard convolution을 depthwise convolutionpointwise convolution이라고 불리는 1x1 convolution으로 분리합니다.

 

MobileNets에서 depthwise convolution은 하나의 filter를 각 input channel에 적용하게 됩니다.

 

Pointwise convolution은 그다음으로 depthwise convolution으로부터 나온 output을 결합하고자 1x1 convolution을 적용합니다.

 

Standard convolution은 filter를 적용하고, input을 새로운 일련의 output으로 결합하는 것을 하나의 step으로 적용합니다.

 

Depthwise separable convolution은 이를 filtering을 수행하는 별개의 layer와 combining을 수행하는 별개의 layer, 총 2개의 layer로 분리합니다.

 

이러한 분리는 computation과 model size를 과감하게 줄이는 효과를 가집니다.

 

Figure 2는 어떻게 standard convolution 2(a)이 depthwise convolution 2(b)과 1x1 pointwise convolution 2(c)으로 분리될 수 있는지를 보여줍니다.

 

 

Standard convolutional layer는 $D_F \times D_F \times M$의 사이즈를 가지는 feature map을 input으로 받고 $D_G \times D_G \times N$의 사이즈를 가지는 feature map을 output으로 만들어내게 됩니다.

 

여기서 $D_F$는 square input feature map의 spatial width, height이며, $M$은 input depth이고, $D_G$는 square output feature map의 spatial width, height, $N$은 output depth를 나타냅니다.

 

Standard convolutional layer는 $D_K \times D_K \times M \times N$의 사이즈를 가지는 convolution kernel $K$에 의해서 parameterized 될 수 있으며, $D_K$는 kernel의 spatial dimension이며 $M$은 input channel, $N$는 output channel을 나타냅니다.

 

 

Standard convolution의 computational cost는 다음과 같이 정의될 수 있습니다.

 

 

이를 통해, computational ocst는 input channel $M$, output channel $N$, kernel size $D_k \times D_k$, feature map size $D_F \times D_F$에 비례한다는 사실을 알 수 있습니다.

 

MobileNet은 depthwise separable convolution을 사용해서 output channel의 수와 kernel의 사이즈 간의 interaction을 끊는 작업을 진행하게 됩니다.

 

 

Depthwise separable convolution은 depthwise convolution과 pointwise convolution의 두 layer로 구성됩니다.

 

Depthwise convolution은 input channel 각각에 대해서 single filter를 적용하게 됩니다. (기존 standard convolution은 input channel 사이즈랑 동일한 사이즈의 filter를 적용했었죠.)

 

Pointwise convolution은 depthwise layer의 output의 linear combination을 만들기 위해서 단순한 1x1 convolution을 사용하게 됩니다.

 

MobileNet도 batchnorm과 ReLU는 동일하게 사용됩니다.

 

 

먼저 Depthwise convolution의 computational cost부터 살펴보겠습니다.

 

 

Input channel의 각각에 대해서만 filter를 적용하면 되기 때문에, 기존에 있던 $N$은 사라지게 됩니다.

 

따라서 kernel size인 $D_K$와 inputer feature size인 $D_F$, 그리고 input channel의 수인 $M$에 비례하게 됩니다.

 

 

다음으로는 Depthwise convolution을 통해 얻어지는 output을 1x1 convolution을 통해 linear combination 해야 하므로, 다음과 같은 연산량을 갖게 됩니다.

 

 

1x1xM 사이즈의 kernel을 $D_F \times D_F$만큼 돌고, 이것이 output의 1개의 channel만 만들어내게 되므로, $N$ channel의 output을 만들기 위해서는 이러한 filter가 $N$개가 존재해야 합니다.

 

따라서, 위와 같은 computational cost가 발생하게 됩니다.

 

 

Depthwise separable convolution을 기존 standard convolution과 비교하면 다음과 같습니다.

 

 

MobileNet이 3x3 depthwise separable convolution을 사용하므로, computation 관점에서 8~9배 정도 줄어든다는 사실을 확인할 수 있습니다.

 

연산량은 이렇게 줄어들지만, accuracy에서는 작은 감소만 있음을 Section 4에서 검증합니다.

 

 

 

지금까지는 논문에 나온 내용을 토대로 설명을 드렸는데, 혹시나 이 글을 읽고 이해가 되시지 않는 분들을 위해서 조금 더 간단한 예시로 부가설명을 추가해보겠습니다.

 

 

예를 들어서, 6x6x5 짜리 input이 존재하고, 이를 convolution 연산을 통해서 얻게 되는 output이 4x4x7이라고 가정해보겠습니다. (kernel size는 3x3, stride = 1로 고정합니다.)

 

먼저 standard convolution의 경우 kernel size는 3x3x5가 됩니다. 그리고 이러한 작업을 가로로 4번, 세로로 4번 진행하게 됩니다. 그리고 output의 channel이 7 이므로 kernel을 7개 가지고 있어야 합니다. 따라서 전체 연산량은 (3x3x5)[kernel size]x4[가로 이동]x4[세로 이동]x7[kernel 수]이 됩니다. 총 5040입니다.

 

 

이를 Depthwise separable convolution으로 푼다면 다음과 같습니다.

 

먼저 kernel size는 3x3x1이 됩니다. 그리고 이러한 kernel이 input channel의 수 만큼 존재하므로 5개가 존재하게 됩니다. 이를 가지고 sliding window 작업을 진행하면 가로로 4번, 세로로 4번 진행하게 되고 이를 통해 얻게 되는 output은 4x4x5가 됩니다.  이때 연산량은 (3x3x1)[kernel size]x4[가로 이동]x4[세로 이동]x5[kernel 수]가 됩니다. 총 720입니다.

 

이를 가지고 4x4x7짜리의 output을 만들어야 하므로, 1x1x5짜리 kernel을 가로 4, 세로 4만큼 이동하면서 4x4 짜리를 얻고 이를 7개의 kernel를 가지고 진행하여 4x4x7짜리를 얻게 됩니다. 이때 연산량은 (1x1x5)[kernel size]x4[가로 이동]x4[세로 이동]x7[kernel 수]가 됩니다. 총 560입니다.

 

두 단계를 더해주면 되므로, 총 1280의 연산량을 갖게 됩니다.

 

기존 방법론에 비해서 약 4배 정도의 연산량 감소가 있음을 확인할 수 있습니다.

 

제가 예시로 든 경우는 사이즈가 작기 때문에 연산량 감소가 적었지만, 사이즈가 커지면 커질수록 연산량 감소의 효과는 더 커지게 됩니다.

 

 

 

3.2 Network Structure and Training

 

 

MobileNet structure는 첫 번째 layer를 제외하고는 앞에서 언급된 depthwise separable convolution을 이용해서 구성됩니다.

 

MobileNet architecture는 Table 1에서 확인할 수 있습니다.

 

 

그리고 Figure 3에서 regular convolution, batchnorm, ReLU를 가지는 layer와 depthwise convolution, 1x1 pointwise convolution, bachnorm, ReLU를 가지는 layer를 비교합니다.

 

 

 

Downsampling은 depthwise convolution에 있는 strided convolution을 이용하여 처리되고요.

 

마지막 average pooling은 spatial resolution을 1로 줄이고, 이를 fully connected layer와 연결합니다.

 

 

MobileNet은 RMSprop을 사용해서 학습되었고, large model을 학습할 때와는 다르게 regularization과 data augmentation technique은 적게 사용하였습니다.

 

이는 small model이 overfitting을 겪을 가능성이 더 낮기 때문입니다.

 

추가적으로, depthwise filter에 매우 작은, 혹은 no weight decay (l2 regularization)을 적용하는 것이 중요하다는 사실을 확인하였고, 이는 여기에 작은 수의 parameter가 포함되어 있기 때문입니다.

 

 

 

3.3 Width Multiplier: Thinner Models

 

 

비록 base MobileNet architecture가 이미 작고 낮은 latency를 가지지만, 어떤 경우에는 모델이 더 작고 더 빠른 것을 요구하는 경우가 존재할 수 있습니다.

 

더 작고 연산량이 더 적은 모델을 만들기 위해, 저자들은 width multiplier라고 불리는 아주 작은 parameter인 $\alpha$를 도입하였습니다.

 

Width multiplier $\alpha$의 역할은 각 layer를 균일하게 더 얇게 만드는 것입니다.

 

Width multiplier $\alpha$가 주어졌을 때, input channel의 수 $M$는 $\alpha M$이 되고, output channel의 수 $N$는 $\alpha N$이 됩니다. 

 

 

width multiplier $\alpha$를 도입한 경우 depthwise separable convolution의 computational cost는 다음과 같습니다.

 

 

 

$\alpha \in (0,1]$이고, $\alpha$는 1, 0.75, 0.5, 0.25로 setting 하게 됩니다.

 

Width multiplier는 computational cost와 number of parameter를 대략 $\alpha^2$만큼 감소시키는 효과를 가집니다.

 

 

 

3.4 Resolution Multiplier: Reduced Representation

 

 

neural network의 computational cost를 감소시키는 두 번째 hyper-parameter는 resolution multiplier $\rho$입니다.

 

저자들은 이를 input image에 적용시켰으며, 이를 통해 모든 layer의 internal representation이 감소되는 효과를 얻을 수 있었습니다.

 

width multiplier $\alpha$와 resolution multiplier $\rho$를 사용하였을 때 depthwise separable convolution의 computational cost는 다음과 같습니다.

 

 

$\rho \in (0, 1]$는 input image가 224, 192, 160, 128이 되도록 설정합니다. 

 

다음 Table 3은 standard convolution과 depthwise separable convolution, 그리고 width multiplier, resolution multiplier를 적용했을 때를 비교한 표입니다.

 

 

standard conv과 depthwise separable conv 사이에도 큰 차이가 있지만, multiplier를 이용하면 연산량이 굉장히 감소하는 것을 확인할 수 있습니다.

 

 

 

4. Experiment

 

 

이번 section에서는 depthwise convolution의 효과와 network의 수를 줄이는 것이 아니라 network의 width를 줄이는 shrinking의 효과에 대해서 먼저 알아봅니다.

 

그다음으로는 width multiplier와 resolution multiplier의 기반해서 network를 줄이는 것에 대한 trade off를 보고, 여러 가지 유명한 모델과 성능을 비교합니다.

 

그러고 나서 MobileNet을 다양한 application에 적용할 수 있음을 알아봅니다.

 

 

4.1 Model Choices

 

 

첫 번째로 저자들은 full convolution을 사용한 모델과 depthwise separable convolution을 사용한 MobileNet을 비교한 결과를 제시합니다.

 

 

Table 4를 보면, depthwise separable convolution을 사용한 것이 Mult-adds와 parameters에서는 엄청나게 감소하였으나, accuracy에서는 1%만의 성능 감소가 있음을 확인할 수 있습니다.

 

 

다음으로는 width multiplier를 사용한 thinner model과 더 적은 layer를 사용하는 shallower 모델을 비교합니다.

 

MobileNet을 더 얕게 만들기 위해서, feature size 14x14x512를 사용하는 5개의 separable filter를 제거하였습니다.

 

 

 

 

Table 5는 유사한 정도의 computation과 parameter의 수를 가지고 있지만 얇은 MobileNet이 얕은 MobileNet보다 3% 정도 더 좋다는 것을 보여줍니다.

 

 

 

4.2 Model Shrinking Hyperparameters

 

 

 

Table 6은 width multiplier $\alpha$를 사용해서 MobileNet architecture를 shrinking 했을 때의 accuracy, computation, size trade off를 보여줍니다. 

 

아키텍처를 너무 작게 만드는 $\alpha = 0.25$가 될 때까지 accuracy는 smoothly 하게 감소하는 것을 확인할 수 있습니다.

 

 

 

Table 7은 reduced input resolution을 사용했을 때 MobileNet을 학습하는 경우를 보여줍니다. resolution이 떨어질수록 accuracy가 감소하는 것을 확인할 수 있습니다.

 

 

 

Figure 4는 ImageNet Accuracy와 width multiplier $\alpha \in \left\{1, 0.75, 0.5, 0.25\right\}$, 그리고 resolution {224, 192, 160, 128}의 cross product를 통해 얻어지는 16개 모델에 대한 computation의 trade off를 보여줍니다. log linear 한 경향을 보여주고 있습니다.

 

 

 

Figure 5는 ImageNet Accuracy와 width multiplier $\alpha \in \left\{1, 0.75, 0.5, 0.25\right\}$, 그리고 resolution {224, 192, 160, 128}의 cross product를 통해 얻어지는 16개 모델에 대한 parameter의 수 간의 trade off를 보여줍니다.

 

 

 

Table 8은 original GoogleNet과 VGG16과 full MobileNet 간의 비교를 보여줍니다.

 

MobileNet은 VGG16만큼 정확하지만, 32배 더 작고 27배 더 적은 컴퓨터 연산량을 가지고 있습니다.

 

이는 GoogleNet보다는 더 정확하지만 모델이 더 작고 computation도 2.5배 더 작습니다.

 

 

 

Table 9은 width multiplier $\alpha = 0.5$, reduced resolution 160x160을 사용했을 때의 MobileNet과의 비교를 나타냅니다.

 

Reduced MobileNet은 AlexNet보다 4% 더 좋은 성능을 내지만 45배 더 작고 9.4배 더 적은 연산량을 가집니다.

 

이는 Squeezenet보다 4% 더 좋지만 유사한 사이즈를 가지고 있고 22배 더 적은 연산량을 가집니다.

 

 

 

 

4.3 - 4.7는 다양한 분야에서 MobileNet을 사용해본 예시로, 생략하겠습니다.

 

 

 

5. Conclusion

 

 

저자들은 depthwise separable convolution을 기반으로 하는 새로운 모델 아키텍처인 MobileNet을 제안합니다.

 

저자들은 중요한 design decision이 효율적인 모델을 만들어냄을 증명하였습니다.

 

저자들은 그다음으로 size와 latency를 줄이기 위해 합리적인 정도의 accuracy를 희생함으로써 width multiplier와 resolution multiplier를 사용하여 어떻게 더 작고 빠른 MobileNet을 만들어낼 수 있는지를 증명하였습니다.

 

저자들은 그 다음으로 잘 알려진 모델과 다른 MobileNet과의 비교를 통해서 size, speed, accuracy에서 우월하다는 것을 증명하였습니다. 

 

마지막으로 MobileNet의 유효성을 다양한 task에 적용해봄으로써 검증하였습니다.

 

 

 

 

여기까지 MobileNetV1 논문을 정리해보았습니다.

 

크게 어려운 내용이 없이, 기존 conv를 새로운 방식으로 만들어 효율적인 구조를 만든 것이 주요한 내용이라고 보시면 될 것 같습니다.

 

다음 포스팅에서는 MobileNetV1의 code review를 진행해보겠습니다.

 

감사합니다.

 

안녕하세요.

 

오늘은 저번 글에서 paper review를 진행한 PaDiM의 code review를 진행해보려고 합니다.

 

Pretrained CNN model을 사용하기 때문에 별도로 학습을 진행할 필요가 없으므로, 일반적으로 학습을 요구하는 model에 비해서는 코드가 간결한 편이라고 생각합니다.

 

본 글에서 사용되는 모든 코드는 제 Github에서 확인하실 수 있습니다.

 

https://github.com/PeterKim1/paper_code_review/tree/master/11.%20PaDiM

 

PeterKim1/paper_code_review

paper review with codes. Contribute to PeterKim1/paper_code_review development by creating an account on GitHub.

github.com

 

그럼 시작해보겠습니다.

 

 

 

Training dataset modeling

 

 

먼저, Training dataset을 가지고 Multivariate Gaussian distribution의 parameter를 추정하는 작업을 진행합니다.

 

 

import random
from random import sample
import argparse
import numpy as np
import os
import pickle
from tqdm import tqdm
from collections import OrderedDict
from sklearn.metrics import roc_auc_score
from sklearn.metrics import roc_curve
from sklearn.metrics import precision_recall_curve
from scipy.spatial.distance import mahalanobis
from scipy.ndimage import gaussian_filter
from skimage import morphology
from skimage.segmentation import mark_boundaries
import matplotlib.pyplot as plt
import matplotlib

import torch
import torch.nn.functional as F
from torch.utils.data import DataLoader
from torchvision.models import wide_resnet50_2, resnet18
import datasets.mvtec as mvtec

 

코드를 실행시키는 데 있어서 필요한 library들을 모두 불러옵니다.

 

# device setup
use_cuda = torch.cuda.is_available()
device = torch.device('cuda' if use_cuda else 'cpu')


def parse_args():
    parser = argparse.ArgumentParser('PaDiM')
    parser.add_argument('--data_path', type=str, default='/content/drive/MyDrive/MVTec')
    parser.add_argument('--save_path', type=str, default='./mvtec_result')
    parser.add_argument('--arch', type=str, choices=['resnet18', 'wide_resnet50_2'], default='wide_resnet50_2')
    return parser.parse_args()

 

GPU 연산이 가능한지를 use_cuda로 저장하고, 사용 가능한 장치를 device에 저장합니다.

 

그리고 해당 코드는 .py 파일로 구성되어 있어, argparse를 사용하고 있습니다.

 

--data_path는 MVTec AD의 경로를 지정해주고, save_path는 결과를 저장할 경로를 지정해줍니다.

 

--arch는 어떤 model을 사용할 것인지를 결정합니다. 

 

def main():

    args = parse_args()

    # load model
    if args.arch == 'resnet18':
        model = resnet18(pretrained=True, progress=True)
        t_d = 448
        d = 100
    elif args.arch == 'wide_resnet50_2':
        model = wide_resnet50_2(pretrained=True, progress=True)
        t_d = 1792
        d = 550
    model.to(device)
    model.eval()
    random.seed(1024)
    torch.manual_seed(1024)
    if use_cuda:
        torch.cuda.manual_seed_all(1024)

    idx = torch.tensor(sample(range(0, t_d), d))

 

이제 main 함수의 시작 부분입니다.

 

먼저 아까 정의했던 argparse를 args로 저장합니다.

 

--arch로 선택한 모델의 구조에 따라서, model을 불러옵니다.

 

t_d는 해당 모델의 Layer 1, Layer 2, Layer 3의 embedding vector를 concatenate 했을 때의 차원의 수를 나타내고, d는 그중에서 random dimensionality reduction을 진행할 random dimension의 수를 나타냅니다.

 

즉, ResNet18을 사용하면, embedding vector를 448차원으로 얻게 되고, 그중에서 100차원을 랜덤으로 뽑게 된다고 이해해주시면 될 것 같습니다.

 

idx는 t_d 차원에서 임의로 d차원을 뽑아서 저장해 torch tensor로 저장합니다.

 

 

 # set model's intermediate outputs
    outputs = []

    def hook(module, input, output):
        outputs.append(output)

    model.layer1[-1].register_forward_hook(hook)
    model.layer2[-1].register_forward_hook(hook)
    model.layer3[-1].register_forward_hook(hook)

 

다음은 forward hook을 이용해서 모델의 Layer 1, Layer 2, Layer 3의 output을 outputs라는 변수에 저장할 수 있도록 해줍니다.

 

 for class_name in mvtec.CLASS_NAMES:

        train_dataset = mvtec.MVTecDataset(args.data_path, class_name=class_name, is_train=True)
        train_dataloader = DataLoader(train_dataset, batch_size=32, pin_memory=True)
        test_dataset = mvtec.MVTecDataset(args.data_path, class_name=class_name, is_train=False)
        test_dataloader = DataLoader(test_dataset, batch_size=32, pin_memory=True)

        train_outputs = OrderedDict([('layer1', []), ('layer2', []), ('layer3', [])])
        test_outputs = OrderedDict([('layer1', []), ('layer2', []), ('layer3', [])])

 

mvtec ad의 class 각각에 대해서 for문을 작동시키고, 각 class의 train dataset과 test dataset을 만들어줍니다.

 

그리고 train dataset의 embedding vector와 test dataset의 embedding vector를 저장하기 위해서 OrderedDict을 이용해 Layer 1, Layer 2, Layer 3에 해당하는 embedding vector를 저장할 장소를 train_outputs과 test_outputs로 지정해둡니다.

 

 

# extract train set features
        train_feature_filepath = os.path.join(args.save_path, 'temp_%s' % args.arch, 'train_%s.pkl' % class_name)
        if not os.path.exists(train_feature_filepath):
            for (x, _, _) in tqdm(train_dataloader, '| feature extraction | train | %s |' % class_name):
   
                # model prediction
                with torch.no_grad():
                    _ = model(x.to(device))

                # get intermediate layer outputs
                for k, v in zip(train_outputs.keys(), outputs):
                    train_outputs[k].append(v.cpu().detach())

                # initialize hook outputs
                outputs = []


            for k, v in train_outputs.items():
                train_outputs[k] = torch.cat(v, 0)

 

train_feature_filepath는 각 class에 대해서 저장한 Gaussian distribution의 parameter를 pickle 파일로 저장할 저장 경로를 저장하는 변수입니다.

 

다음으로는 for문을 돌게 되는데, 여기서 x는 train dataset의 이미지를 나타냅니다.

 

shape를 찍어보면 (32, 3, 224, 224) 임을 확인할 수 있습니다. (위에서 train_dataloader와 test_dataloader의 batch size가 32이기 때문에 32로 시작됩니다.)

 

그리고 _ = model(x.to(device))는 원래 model의 결과가 별도로 저장되어야 하는데, 본 모델에서는 모델의 최종 output을 필요로 하는 것이 아니라 중간 output을 필요로 하기 때문에 _를 사용하여 별도로 변수에 저장되지 않도록 처리한 것입니다.

 

이 코드 라인이 실행되면, 앞에서 지정한 forward hook에 의해서 Layer 1, 2, 3에서 얻어지는 중간 output 결과들을 outputs라는 변수에 저장이 됩니다.

 

for k, v in zip(train_outputs.keys(), outputs): 코드라인은 outputs에 저장된 각각의 요소들을 OrderedDict인 train_outputs의 각 Key에 append 하는 코드입니다.

 

forward hook이 Layer 1, 2, 3에 각각 작동하기 때문에 outputs에는 총 3개의 데이터가 append 되어 있는 상태입니다.

 

따라서 for문을 통해 각각을 train_outputs의 Key에 저장할 수 있게 됩니다.

 

그리고 outputs = []를 통해 중간 output을 저장하는 리스트를 다시 비워서 다음 class에 대해서 동일한 코드가 작동할 때 새롭게 저장될 수 있도록 만들어줍니다.

 

for k, v in train_outputs.items(): 코드라인은 앞에서 batch 단위로 저장되어 있는 데이터들을 합쳐서 전체로 만들어주는 코드입니다.

 

예를 들어서, bottle class는 209장의 train 이미지가 있는데, batch는 32이므로 32짜리로 여러 개의 데이터가 저장되어 있을 것입니다.

 

이를 concat 시켜서 209장 짜리의 embedding vector로 만들어주는 작업이라고 보시면 되겠습니다.

 

 

 # Embedding concat
 embedding_vectors = train_outputs['layer1'] # torch.Tensor, (209, 64, 56, 56)

 for layer_name in ['layer2', 'layer3']: 
     embedding_vectors = embedding_concat(embedding_vectors, train_outputs[layer_name])

 

다음으로는 Layer 1과 Layer 2, Layer 3에 저장된 embedding vector를 concat 하는 코드입니다.

 

사실 코드상으로는 이렇게 짧게 되어있지만, embeddding_concat이라는 함수는 생각보다 복잡한 형태로 구성이 되어 있습니다.

 

def embedding_concat(x, y):
    B, C1, H1, W1 = x.size()
    _, C2, H2, W2 = y.size()
    s = int(H1 / H2)
    x = F.unfold(x, kernel_size=s, dilation=1, stride=s)
    x = x.view(B, C1, -1, H2, W2)
    z = torch.zeros(B, C1 + C2, x.size(2), H2, W2)
    for i in range(x.size(2)):
        z[:, :, i, :, :] = torch.cat((x[:, :, i, :, :], y), 1)
    z = z.view(B, -1, H2 * W2)
    z = F.fold(z, kernel_size=s, output_size=(H1, W1), stride=s)

    return z

 

사실 이 함수가 이렇게 복잡하게 된 이유는, 각 Layer 마다의 embedding vector의 사이즈가 전부 다르기 때문입니다.

 

model을 ResNet18을 사용한다고 가정하였을 때,

 

Layer 1의 embedding vector는 (Training dataset size, 64, 56, 56)이며

 

Layer 2의 embedding vector는 (Training dataset size, 128, 28, 28)이고

 

Layer 3의 embedding vector는 (Training dataset size, 256, 14, 14)입니다. 

 

즉 가로와 세로가 절반씩 줄어드는 구조이기 때문에 단순히 Channel 방향으로 concat 시키는 것이 불가능하기 때문입니다.

 

일일이 shape를 다 확인하는 것도 방법이긴 하지만 대략 어떤식으로 embedding vector concat이 이루어지는지만 알고 있으면 충분할 것이라고 생각이 됩니다. (물론 코드를 하나하나 자세히 shape 찍어보시는 것도 좋지만 이 부분이 워낙 복잡하여 전체적인 컨셉만 짚고 넘어가려고 합니다.)

 

(N, 1, 4, 4) 크기의 embedding vector(A)와 (N, 2, 2, 2) 크기의 embedding vector(B)가 있다고 가정해보겠습니다.

 

N은 training dataset size이나, 4차원은 그림으로 표현할 수 없으므로 생략하고 3차원에 대해서만 그림으로 설명해보겠습니다.

 

embedding vector A와 B는 다음과 같이 그림으로 표현할 수 있습니다.

 

이를 위 함수를 이용해서 concat 하면 다음과 같은 방식으로 concat이 진행됩니다.

 

 

먼저 높이와 폭 기준으로 더 넓은 쪽인 (N, 1, 4, 4)가 앞쪽에 원래대로 위치하고, 높이와 폭이 1/2인 (N, 2, 2, 2)짜리는 각 위치에서 2x2 사이즈만큼 복사되어서 높이, 폭의 2배 사이즈 차이를 메우게 됩니다.

 

이게 말로 하려니 설명이 살짝 어려운데, 첫 번째 사진과 두 번째 사진을 보시면서 값들이 어떻게 배치되었는지 확인해주시면 이해가 크게 어렵지는 않을 것이라고 생각합니다.

 

이러한 방식을 이용하게 되면, 높이와 폭 사이즈는 그대로 유지되면서, 합쳐지는 두 embedding vector의 차원 수만 합쳐지는 결과를 가지고 오게 됩니다.

 

이런 방식으로 Layer 1, Layer 2, Layer 3에서 나오는 embedding vector를 합칠 수 있게 되고, ResNet18 기준으로 얻어지는 (Training dataset size, 64, 56, 56), (Training dataset size, 128, 28, 28), (Training dataset size, 256, 14, 14)을 concat 하게 되면 (Training dataset size, 64+128+256, 56, 56) 사이즈의 embedding vector를 얻을 수 있게 됩니다.

 

 

# randomly select d dimension
embedding_vectors = torch.index_select(embedding_vectors, 1, idx)

 

위 과정을 통해서 얻어진 embedding vector에서 d 차원만큼을 랜덤으로 선택합니다. 

 

PaDiM 논문에서는 embedding vector 전체를 사용하는 것에 비해서 random 하게 차원을 줄였을 때 시간 복잡도와 공간 복잡도가 줄어들지만 anoamly localization 성능에서는 큰 폭의 차이가 없음을 보였습니다.

 

 # calculate multivariate Gaussian distribution
 B, C, H, W = embedding_vectors.size()
 embedding_vectors = embedding_vectors.view(B, C, H * W) # (209, 100, 56*56)
 mean = torch.mean(embedding_vectors, dim=0).numpy() # (100, 56*56), 샘플 간 평균
 cov = torch.zeros(C, C, H * W).numpy() # (100, 100, 56*56) 100차원 간 cov 계산
 I = np.identity(C)
 for i in range(H * W):
     cov[:, :, i] = np.cov(embedding_vectors[:, :, i].numpy(), rowvar=False) + 0.01 * I
 # save learned distribution
 train_outputs = [mean, cov]
 with open(train_feature_filepath, 'wb') as f:
     pickle.dump(train_outputs, f)

 

다음은 얻어진 embedding vector를 가지고 multivariate Gaussian distribution의 parameter를 얻는 코드입니다.

 

먼저 embedding_vector를 (B, C, H*W)의 차원으로 바꿔줍니다. 

 

ResNet18 기준이라면, (N, 100, 56*56)이 됩니다. (최종적으로 얻어진 embedding vector는 (N, 448, 56, 56)이지만 448차원 중 100차원을 랜덤으로 선택하였으므로 (N, 100, 56, 56)을 얻게 되고, 이를 (N, 100, 56*56)로 reshape 한 것입니다.)

 

그리고 0차원(데이터 셋 크기) 기준으로 평균을 구합니다. 이는 (100, 56*56)의 size를 가집니다.

 

의미적으로 본다면, 이미지에서 각 패치의 embedding 평균을 구한 것이 되겠죠? 

 

그다음으로는 공분산을 계산해야 하는데, 먼저 torch.zeros로 0으로 채워진 (100, 100, 56*56) 짜리 tensor를 만들어줍니다.

 

그리고 for문을 돌면서, cov[:, :, i] = np.cov(embedding_vectors[:, :, i].numpy(), rowvar=False) + 0.01 * I를 실행합니다.

 

embedding vectors[:, :, i]라면 patch의 각 위치에서의 데이터가 되고, 크기는 (N, 100)이 될 것입니다.

 

np.cov에 rowvar라는 인자가 있는데, 이는 row를 기준으로 분산을 구할 것인지를 물어보는 것입니다.

 

rowvar = False이므로, 우리는 row를 기준으로 분산을 구하지 않고 column을 기준으로 분산을 구하게 됩니다.

 

이렇게 하면 column인 100(embedding vector의 차원 수)을 기준으로 분산을 구할 수 있게 됩니다.

 

이를 cov[:, :, i]에 저장하게 되죠. cov가 (100, 100, 56*56) 였음을 감안한다면, cov[:, :, i]는 각 patch 위치에서의 (100, 100) 크기의 matrix가 되는 것을 알 수 있습니다.

 

0.01 * I는 논문에서도 언급하였듯이 regularization term이고, 공분산 matrix의 invertible이 보장되도록 하는 역할을 수행합니다.

 

마지막으로 우리가 구한 mean과 cov를 pickle.dump을 통해서 pickle 파일로 저장해줍니다.

 

 

else:
    print('load train set feature from: %s' % train_feature_filepath)
    with open(train_feature_filepath, 'rb') as f:
        train_outputs = pickle.load(f)

 

만약 별도로 이미 저장된 pickle 파일이 있다면, 이를 읽어오기만 하면 됩니다.

 

아마 이미 계산된 mean과 cov가 있는 경우는 별도로 연산하지 않고 바로 불러올 수 있게끔 코드를 짜 놓은 것 같습니다.

 

 

 

Inference 

 

 

gt_list = [] # label을 담는 list
gt_mask_list = [] # mask를 담는 list
test_imgs = [] # 이미지를 담는 list

 

다음으로는 test image에 대한 Inference를 진행하겠습니다.

 

먼저 label(0인지 1인지, 0이면 정상, 1이면 이상입니다.), segmentation mask(정답 mask), 정답 이미지를 담는 list를 먼저 지정합니다.

 

 

for (x, y, mask) in tqdm(test_dataloader, '| feature extraction | test | %s |' % class_name):
  test_imgs.extend(x.cpu().detach().numpy())
  gt_list.extend(y.cpu().detach().numpy())
  gt_mask_list.extend(mask.cpu().detach().numpy())
  
  # model prediction
  with torch.no_grad():
      _ = model(x.to(device))
      
  # get intermediate layer outputs
  for k, v in zip(test_outputs.keys(), outputs):
       test_outputs[k].append(v.cpu().detach())
       
  # initialize hook outputs
  outputs = []
for k, v in test_outputs.items():
    test_outputs[k] = torch.cat(v, 0)
    
    
# Embedding concat
embedding_vectors = test_outputs['layer1']
for layer_name in ['layer2', 'layer3']:
    embedding_vectors = embedding_concat(embedding_vectors, test_outputs[layer_name])

# randomly select d dimension
embedding_vectors = torch.index_select(embedding_vectors, 1, idx)

test_dataloader에서 나오는 이미지 데이터(x)와 라벨(y), segmentation mask(mask)를 각각 앞에서 미리 지정된 list에 extend로 추가합니다.

 

그다음 코드들은 제가 앞에서 설명한 것과 동일하죠??

 

이미지를 주고 예측을 하게 만든 다음, 최종 output은 저장하지 않고 forward hook을 이용해서 Layer 1, 2, 3에서 나온 결과들이 저장되도록 만들고 test_outputs에 저장해두는 과정을 거칩니다.

 

그리고 얻어진 embedding vector를 concat 하고, 이 중에서 랜덤으로 d 차원만 골라줍니다.

 

 

 # calculate distance matrix
 B, C, H, W = embedding_vectors.size() # (N, 100, 56, 56)
 embedding_vectors = embedding_vectors.view(B, C, H * W).numpy() # (N, 100, 56*56)
 dist_list = []
 for i in range(H * W):
     mean = train_outputs[0][:, i]
     conv_inv = np.linalg.inv(train_outputs[1][:, :, i])
     dist = [mahalanobis(sample[:, i], mean, conv_inv) for sample in embedding_vectors]
     dist_list.append(dist)

 

다음으로는 Test image의 embedding_vector에 대해서 Mahalanobis distance를 계산하는 코드입니다.

 

patch의 각 위치에 대해서 평균과 공분산을 가져오고, distance 계산을 위해 공분산을 역행렬로 바꿔줍니다.

 

그리고 평균과 공분산을 가지고서 각 test image의 embedding vector에 대해서 distance 계산을 수행합니다.

 

 

dist_list = np.array(dist_list).transpose(1, 0).reshape(B, H, W) # (N, 56, 56)

# upsample
dist_list = torch.tensor(dist_list)

score_map = F.interpolate(dist_list.unsqueeze(1), size=x.size(2), mode='bilinear',
                          align_corners=False).squeeze().numpy() # (N, 224, 224)

 

계산해둔 distance를 저장한 dist_list는 patch의 각 위치에 대해서 테스트 이미지 개수만큼의 거리 계산을 수행하므로, shape가 (56*56, N)이 됩니다.

 

이를 transpose 해서 (N, 56*56)로 만들어주고, reshape 해서 (N, 56, 56) 형태로 만들어줍니다.

 

이렇게 하면 Test image 한 장당 56 x 56 짜리의 anomaly map을 얻을 수 있는 것이죠.

 

이를 bilinear interpolation을 이용하여 원래 이미지 사이즈인 224x224 사이즈로 늘려줍니다.

 

 

# apply gaussian smoothing on the score map
for i in range(score_map.shape[0]):
    score_map[i] = gaussian_filter(score_map[i], sigma=4)
    
# Normalization
max_score = score_map.max()
min_score = score_map.min() 
scores = (score_map - min_score) / (max_score - min_score)
#print("score shape; ", scores.shape) # (N, 224, 224)

img_scores = scores.reshape(scores.shape[0], -1).max(axis=1)

 

구해놓은 score_map에 논문에서 언급된 대로 gaussian filter를 적용해주고, anomaly map을 normalization 해줍니다.

 

이 작업을 통해서 모든 anomaly map이 0 ~ 1 사이의 값을 가지도록 만들어줄 수 있습니다.

 

마지막으로 이미지의 anomaly score는 해당 anomaly map의 최댓값을 이용해서 계산해주게 됩니다.

 

 

 

나머지 코드들은 대부분 구해진 값을 이용하여 시각화를 진행하는 코드들이라 해당 코드 리뷰에서는 제외하였습니다.

 

실제 코드를 구동하는 것에 관심이 있으신 분들은 Github에 올려진 코드를 이용하셔서 직접 실행해보시면 좋을 것 같습니다.

 

 

 

구해진 Embedding vector를 합치는 코드가 조금 까다로워서 보는데 시간이 걸렸지만, 그 외에 코드들은 크게 어려운 코드가 없어 보시면서 이해하는데 어렵지 않을 것이라고 생각합니다.

 

이번 code review는 여기까지 진행하도록 하겠습니다.

 

감사합니다.

+ Recent posts