이전부터 뭐든 실제로 서비스를 만들어보는 것이 중요하다고 생각하여 아이펠을 진행하면서도 틈틈이 프로젝트를 위한 공부를 따로 하고 있었다. 그러다가 GAN을 이용한 노드를 진행했을 때, '이거다!' 하고 느낌이 와서 실제로 프로젝트로 진행해보기로 했다.

우선 기존에 아이펠 노드에서 진행한 모델은 내가 잘못 한건지, 노드가 잘못된건지 학습은 되는데 어딘가 이상한 결과물이 나와서 텐서플로 공식 튜토리얼을 보고 다시 모델을 만들어보았다.

실패한 학습 결과

데이터셋은 kaggle의 Sketch2Pokemon 데이터셋을 학습시켰다. train 이미지가 830장 밖에 되질 않았고, test 이미지도 train 이미지에 있는 중복 이미지여서 데이터가 부족하긴 했지만, 일단 서비스로 배포해보는 것에 의의를 두기로 했다.

프로젝트 진행은 다음과 같이 이루어졌다.

  • 2021-03-28 : 프로젝트 시작
  • 2021-03-28 ~ 2021-03-29 : 첫 번째 모델 학습 및 결과 확인
  • 2021-03-29 ~ 2021-03-30 : 두 번째 모델 학습 및 결과 확인
  • 2021-03-30 ~ 2021-04-01 : 모델 서비스화 진행 중
  • 2021-04-02 : Ainize로 배포 완료

기존과의 차이점이라고 한다면 이미지를 Normalize 해주는 시점이 조금 다르다는 것이다. 아마 이 부분에서 문제가 생겨서 위와 같은 이미지가 학습된 것으로 보인다. 또한, 이미지 데이터를 다룰 때에는 항상 Normalize, Denormalize에 신경써야 하는 것 같다. 어떤 Activation Function을 사용하느냐에 따라 Normalize 범위를 [-1, 1]로 할지, [0, 1]로 할지가 결정되기 떄문이다. 이번에 사용된 모델에서는 tanh를 Activation Function으로 사용했기 때문에 [-1, 1]의 범위에서 Normalize 해주었다.

우선 시작부터 1000 epoch 정도를 돌려보고 시작하기로 했다.

  • 학습 과정을 시각화 해본 결과, 400 epoch 정도 부터 큰 변화가 없는걸로 보아 local minima에 빠졌거나 학습이 종료된 것 같다.
  • 750 epoch 의 checkpoint를 불러와서 기울기를 1/10으로 줄여서 50 epoch 정도 더 학습을 시켜보았으나 큰 변화는 없는 것 같다.
  • 750 epoch 의 checkpoint를 불러와서 기울기를 10배로 늘려서 50 epoch 정도 더 학습을 시켰더니 오히려 원하지 않는 방향으로 학습이 진행되었다.

750 epoch에서의 learning rate 테스트는 자원이 남아서 학습이 종료됐을 때 learning rate를 조절하여 다시 학습시키면 어떻게 될까 궁금해서 진행해봤다.

좌 : Dicriminator loss / 우 : Generator loss
Train set에 있는 이미지 학습 결과

모델을 학습해본 결과, 두 Loss를 비교해봤을 때, disc_loss는 감소하고 gen_loss는 증가하는 것으로 보아 generator가 discriminator를 속이기 위한 이미지를 잘 생성하지 못하는 것으로 판단된다. 실제로 train과 test에 없는 전혀 새로운 이미지를 입력했을 때, 다음과 같이 잘 생성하지 못하는 결과를 보여줬다.

뒤틀린 황천의 피카츄

아무래도 기존에 있는 이미지는 원본이랑 비슷한 정도로 생성하는 것으로 보아 train set에 overfitting 된 것 같다. 추가로 다른 데이터셋을 더 활용하거나, 모델의 구조를 바꾸어서 다시 학습시켜봐야겠다.

 

프로젝트를 진행하면서 많은 문제점과 어려움을 겪었다. 우선 웹페이지에 이미지를 하나 띄우는 것 까진 쉬웠지만 생성된 이미지를 다시 화면에 출력하는 것이 이렇게 어려운 작업이 될 줄은 몰랐다. 프론트엔드 개발자들 대단해...

겪었던 문제점들을 정리해보면 다음과 같다.

  • 프론트엔드/백엔드 지식이 전혀 없어서 HTML부터 공부해서 페이지를 제작함
  • Inference 결과 tensor object로 반환되어 해당 object를 바로 웹에서 rendering 가능한 방법을 찾다가 결국 이미지로 저장하여 rendering 하는 방식으로 진행
  • 페이지는 정상적으로 만들어졌는데 계속해서 기존의 이미지를 가져옴 -> 브라우저 쿠키 문제
  • model의 크기가 100MB가 넘어가서 git lfs를 이용하여 git에 업로드 하였으나 Ainize에서 불러오지 못하는 문제가 발생
  • heroku에서 배포를 시도하였으나 메모리 용량 부족(500MB)으로 배포 실패
  • 기본 git lfs 용량을 모두 사용하여 wget으로 바로 다운로드 가능한 무료 웹서버를 찾아보았지만 당장 쓸 수 있는 게 없어서 개인 웹서버 이용하여 모델 다운로드 후 Ainize로 배포

모델을 만들어서 학습시키고 실행 가능한 python 파일로 정리하는 것 까진 어렵지 않았는데 웹페이지를 구축하는 게 가장 어려웠다. 단순 텍스트 데이터가 아닌 이미지 데이터를 웹에서 띄우는 방법을 몰라 html을 공부해서 겨우 띄울 수 있게 되었는데 브라우저 캐시 때문에 정상작동 하는 걸 오작동으로 인식하여 다른 방법을 찾았다. 결국 프론트엔드를 공부하고 있는 sunhpark에게 도움을 요청하여 js로 웹페이지를 좀 더 깔끔하게 만드는 데 성공했다.

 

로컬에서 테스트 할 땐 잘 되었는데 막상 배포하려고 하니 적절한 서비스가 없었다. 우선 git에 모델 용량이 100MB를 초과해서 git lfs를 이용하여 Ainize로 배포를 시도하였으나 디버깅 하다가 git lfs 무료 사용량을 전부 사용하여 다른 서비스를 찾아보았다. heroku로 배포를 시도했을 땐 메모리 용량 부족으로 실패하였다.

 

마지막 남은 EC2는 1년만 무료여서 최후의 보루로 남겨두고 Ainize로 배포해보기 위하여 무료 파일 서버 서비스를 찾아보았지만 마땅한 게 없어서 결국 임시로 웹서버를 만들어서 Ainize에서 Dockerfile을 빌드할 때 모델을 받아올 수 있도록 하였고, 결국 성공하였다.

 

구글 드라이브나 네이버 클라우드 같은 곳에 파일을 업로드해서 wget으로 다운로드 받는 방법도 생각했었다. 그러나, wget으로 직접 다운로드 가능한 링크들이 아니어서 다른 방법을 찾았던건데, 나중에 찾아보니 구글 드라이브에 올린 파일도 wget으로 받을 수 있는 방법이 있었다. 당시엔 배포에 정신이 팔려 있어서 검색해볼 생각도 못 했나보다.

 

이번 프로젝트를 진행해보면서 왜 직접 해보는 것이 중요한지 확실하게 깨달았다. 아주 작은 토이 프로젝트였지만 어떻게 보면 프론트엔드와 백엔드 두 부분 모두 조금이나마 경험해볼 수 있었고, 직접 모델 설계부터 서비스까지 배포한 것은 MLOps라고 볼 수도 있지 않을까?

 

다음번엔 서버에 데이터 저장도 하고, 실제로 아마존 EC2나 GCP, Azure 같은 서비스를 이용하여 배포도 해보고, 좀 더 재미있는 아이디어를 가지고 팀원도 모아서 프로젝트를 진행해보도록 해야겠다.

 

해당 서비스는 다음 링크에서 테스트해볼 수 있다.


Run on Ainize

'AI' 카테고리의 다른 글

Inference time은 CPU와 GPU 중 뭐가 더 빠를까?  (0) 2021.05.13
auto-painter 진행 상황  (0) 2021.05.11
[Monthly Dacon 14] 첫 in 10등!  (1) 2021.04.12
Heroku로 딥러닝 모델 배포하는 방법  (0) 2021.04.02
VGGNet 구현해보기  (0) 2021.03.27

문제 링크 : leetcode.com/problems/longest-substring-without-repeating-characters/

 

Longest Substring Without Repeating Characters - LeetCode

Level up your coding skills and quickly land a job. This is the best place to expand your knowledge and get prepared for your next interview.

leetcode.com

이 문제는 주어진 문자열에서 각 문자가 중복되지 않는 연속된 가장 긴 문자열의 길이를 반환하는 문제이다.

 

풀이방법

 

from collections import deque

class Solution:
    def lengthOfLongestSubstring(self, s: str) -> int:
        dq = deque()
        ans = ""
        for char in s:
            if char not in dq:
                dq.append(char)
                if len(ans) < len(dq):
                      ans = "".join(dq)
            else:
                if len(ans) < len(dq):
                      ans = "".join(dq)
                while True:
                    if dq.popleft() == char:
                        break
                dq.append(char)
        return len(ans)

이 문제는 스택을 이용하여 해결하였다. 스택에 문자를 하나씩 넣어두고 길이를 계산하여 최대 길이일 때만 정답을 갱신하고, 만약 중복된 문자가 나오면 스택에서 해당 문자가 나올 때 까지 이전 문자들을 제거하여, 스택에는 문자가 중복되지 않게 하였다. 문자열을 다 순회했을 때, 가장 길었던 정답의 길이를 반환한다.

'Algorithm > Leetcode' 카테고리의 다른 글

[LeetCode] 771. Jewels and Stones  (0) 2021.02.14
[LeetCode] 23. Merge k Sorted Lists  (0) 2021.02.14
[LeetCode] 937. Reorder Data in Log Files  (0) 2021.01.27
[LeetCode] 344. Reverse String  (0) 2021.01.27
[LeetCode] 125. Valid Palindrome  (0) 2021.01.27

문제 링크 : leetcode.com/problems/jewels-and-stones/

 

Jewels and Stones - LeetCode

Level up your coding skills and quickly land a job. This is the best place to expand your knowledge and get prepared for your next interview.

leetcode.com

이 문제는 jewels에 있는 문자가 stones에 몇 개가 들어있는지 갯수를 반환하는 문제이다.

 

풀이방법

이 문제는 key, value 쌍 구조인 딕셔너리를 이용하여 해결하는 가장 기본적인 문제이다.
딕셔너리의 jewels를 dict에 입력해두고 stones에서 jewels에 있으면 count를 +1 하여 해결하였다.

 

class Solution:
    def numJewelsInStones(self, jewels: str, stones: str) -> int:
        dicts = {}
        for i in jewels:
            if i not in dicts:
                dicts[i] = 0
        for j in stones:
            if j in dicts:
                dicts[i] += 1
        return sum(dicts.values())

 

문제 링크 : leetcode.com/problems/merge-k-sorted-lists/

 

Merge k Sorted Lists - LeetCode

Level up your coding skills and quickly land a job. This is the best place to expand your knowledge and get prepared for your next interview.

leetcode.com

이 문제는 주어진 정렬된 linked list를 merge하여 하나의 linked list로 만드는 문제이다.

 

풀이방법

처음에는 n개의 linked list를 순회하여 가장 작은 값을 이전 값의 next로 연결하는 방식으로 구현하려고 했었는데 잘 되지 않아서 stack을 이용하는 방법으로 문제를 해결하였다. 처음엔 stack에 linked list의 node도 넣을 수 있나 싶었는데 역시 파이썬. 안되는 게 없다.

lists를 전부 순회하면서 각 linked list에 들어있는 요소들을 모두 stack에 저장한 후, lambda식을 이용하여 stack의 value를 기준으로 정렬한 후 각각의 node들을 연결시켜주었다.

class Solution:
    def mergeKLists(self, lists: List[ListNode]) -> ListNode:
        head = ListNode()
        temp = head
        stack = []
        for i in lists:
            while i:
                stack.append(i)
                i = i.next
        stack = sorted(stack, key=lambda x:x.val, reverse=True)
        while stack:
            temp.next = stack.pop()
            temp = temp.next
        temp.next = None
        return head.next

문제 링크 : leetcode.com/problems/reorder-data-in-log-files/

 

Reorder Data in Log Files - LeetCode

Level up your coding skills and quickly land a job. This is the best place to expand your knowledge and get prepared for your next interview.

leetcode.com

이 문제는 주어진 logs 라는 배열을 주어진 조건에 맞게 정렬하는 문제이다.

주어진 조건은 다음과 같다.

1. 각 단어 뒤의 식별자는 소문자로만 구성된다. 또는,

2. 각 단어 뒤의 식별자는 숫자로만 구성된다.

 

정렬해야 하는 순서는

1. 식별자 뒤에 문자열이 오면(letter-logs) 사전순으로 정렬하고, 사전순이 같으면 식별자 숫자로 정렬한다.

2. 식별자 뒤에 숫자가 오면(digit-logs) Input되어있는 순서로 정렬되어야 한다.

3. 그 후, letter-logs 뒤에 digit-logs를 이어준다.

 

풀이방법

처음에는 위 조건대로 우선 letter-logs랑 digit-logs를 분리한 후, letter-logs를 lambda식을 이용하여 정렬하고, join으로 다시 하나의 문자열로 만들어 준 후에 letter-logs와 digit-logs를 합쳐주었다.

class Solution:
    def reorderLogFiles(self, logs: List[str]) -> List[str]:
        split_list = []
        log_list = []
        dig_list = []
        ret = []
        for str in logs:
            s = str.split()
            if s[1].isdecimal():
                dig_list.append(s)
            else:
                log_list.append(s)
        sorted_log = sorted(log_list, key=lambda x: (x[1:], x[0]))
        for s in sorted_log:
            ret.append(" ".join(s))
        for s in dig_list:
            ret.append(" ".join(s))
        return ret

다른 풀이방법을 찾아보니 확실히 파이썬 답게 풀었구나 싶었다. split에 maxsplit parameter로 split할 수 있는 범위를 제한할 수 있는 것도 처음 알았고, return에서 조건문을 이렇게도 사용할 수 있구나 싶었다.

class Solution:
    def reorderLogFiles(self, logs: List[str]) -> List[str]:

        def get_key(log):
            _id, rest = log.split(" ", maxsplit=1)
            return (0, rest, _id) if rest[0].isalpha() else (1, )

        return sorted(logs, key=get_key)

'Algorithm > Leetcode' 카테고리의 다른 글

[LeetCode] 771. Jewels and Stones  (0) 2021.02.14
[LeetCode] 23. Merge k Sorted Lists  (0) 2021.02.14
[LeetCode] 344. Reverse String  (0) 2021.01.27
[LeetCode] 125. Valid Palindrome  (0) 2021.01.27
[LeetCode] 92. Reverse Linked List II  (0) 2021.01.24

문제 링크 : leetcode.com/problems/reverse-string/

 

Reverse String - LeetCode

Level up your coding skills and quickly land a job. This is the best place to expand your knowledge and get prepared for your next interview.

leetcode.com

이 문제는 문자열을 Reverse 시키는 문제이다. 단, 주어진 문자열을 수정하여 공간복잡도가 O(1)이 되게 해야 한다.

 

풀이방법

파이썬에는 reverser() 라는 내장 메소드가 있다. list를 reverser 시켜주는 메소드이다.

class Solution:
    def reverseString(self, s: List[str]) -> None:
        s.reverse()

 

문제 링크 : leetcode.com/problems/valid-palindrome/

 

Valid Palindrome - LeetCode

Level up your coding skills and quickly land a job. This is the best place to expand your knowledge and get prepared for your next interview.

leetcode.com

이 문제는 문자열의 알파벳과 숫자의 배열이 Palindrome(문자열을 역순으로 해도 같은 문자열)인지 확인하면 되는 문제이다.

 

풀이방법

1. isalnum() 함수를 이용하여 특수문자를 제외해주고

2. 대소문자를 구문하지 않으므로 lower() 함수를 이용하여 소문자로 만들어서

3. Palindrome인지 확인한다.

 

Palindrome인지 확인하는 방법에는 여러가지가 있는데

1. start와 end를 이동하며 비교하기

2. pop(0) == pop()으로 비교하기

3. 중간부터 왼쪽과 오른쪽으로 가면서 비교하기

등이 있으나 파이썬에서는 list slicing을 이용해서 아주 쉽게 Palindrome을 확인할 수 있다.

class Solution:
    def isPalindrome(self, s: str) -> bool:
        s = [i.lower() for i in s if i.isalnum()]
        return s == s[::-1]

아직 파이썬을 파이썬 답게 쓰지 못해서 s == s[::=1]같은게 되는 줄 몰랐다.

맨날 앞뒤 인덱스 비교해가면서 풀었는데 알아두면 참 좋은 테크닉인 것 같다.

문제 링크 : leetcode.com/problems/reverse-linked-list-ii/

 

Reverse Linked List II - LeetCode

Level up your coding skills and quickly land a job. This is the best place to expand your knowledge and get prepared for your next interview.

leetcode.com

이 문제는 m, n이 주어졌을 때, linked list의 m번째 노드부터 n번째 노드까지 역순으로 정렬하는 문제이다.

 

풀이방법

이번 문제는 주어진 범위의 list만 reverse하는 문제이다. 처음에는 주어진 범위의 list만 reverse하여 m-1 노드의 next를 rev 노드의 head에 연결하고, rev 노드의 tail 의 next를 n+1 노드에 연결하려 했지만 잘 되지 않아서 다른 방법을 찾았다. 이 방법은 이동하면서 start의 next를 end의 next로, end의 next를 end의 next.next로 연결하는 방법이다.

 

# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, val=0, next=None):
#         self.val = val
#         self.next = next
class Solution:
    def reverseBetween(self, head: ListNode, m: int, n: int) -> ListNode:
        if not head or m == n:
            return head
        root = ListNode()
        root.next = head
        start = root
        for _ in range(m - 1):
            start = start.next
        end = start.next
        for _ in range(n - m):
            temp = start.next
            start.next = end.next
            end.next = end.next.next
            start.next.next = temp
        return root.next

'Algorithm > Leetcode' 카테고리의 다른 글

[LeetCode] 344. Reverse String  (0) 2021.01.27
[LeetCode] 125. Valid Palindrome  (0) 2021.01.27
[LeetCode] 328. Odd Even Linked List  (0) 2021.01.24
[LeetCode] 24. Swap Nodes in Pairs  (0) 2021.01.24
[LeetCode] 2. Add Two Numbers  (0) 2021.01.23

문제 링크 : leetcode.com/problems/odd-even-linked-list/

 

Odd Even Linked List - LeetCode

Level up your coding skills and quickly land a job. This is the best place to expand your knowledge and get prepared for your next interview.

leetcode.com

풀이방법

이 문제는 Linked list를 홀수번과 짝수번을 나눠서 정렬시키는 문제이다.
처음에는 노드를 swap해야 하나 싶었는데 다시 생각해보니 홀수, 짝수번째의 Node만 이어서 홀수번째 마지막을 짝수번째 시작 노드에 연결해주면 되는 문제였다.

# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, val=0, next=None):
#         self.val = val
#         self.next = next
class Solution:
    def oddEvenList(self, head: ListNode) -> ListNode:
        # Linked list가 존재하지 않을 때 예외처리
        if not head:
            return None
        odd = head
        even_head = head.next
        even = even_head
        # even의 next가 없으면 list의 끝
        while even and even.next:
            odd.next = odd.next.next
            odd = odd.next
            even.next = even.next.next
            even = even.next
        # odd의 다음은 even의 시작
        odd.next = even_head
        return head

 

'Algorithm > Leetcode' 카테고리의 다른 글

[LeetCode] 125. Valid Palindrome  (0) 2021.01.27
[LeetCode] 92. Reverse Linked List II  (0) 2021.01.24
[LeetCode] 24. Swap Nodes in Pairs  (0) 2021.01.24
[LeetCode] 2. Add Two Numbers  (0) 2021.01.23
[LeetCode] 206. Reverse Linked List  (0) 2021.01.23

문제 링크 : leetcode.com/problems/swap-nodes-in-pairs/

 

Swap Nodes in Pairs - LeetCode

Level up your coding skills and quickly land a job. This is the best place to expand your knowledge and get prepared for your next interview.

leetcode.com

이 문제는 linked list의 노드를 두 개씩 짝을 지어 swap한 후 head를 반환하는 문제이다.

 

풀이방법

swap은 짝수번째 마다 일어난다.

swap 후에는 홀수번째가 되므로 다음 노드로 이동한다.

head, mid, tail로 노드의 조건을 나뉘어서 swap하였다.

# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, val=0, next=None):
#         self.val = val
#         self.next = next
class Solution:
    def swapPairs(self, head: ListNode) -> ListNode:
        curr = prev = head
        while curr and curr.next:
            prec = curr.next
            # head
            if curr == head:
                head = curr.next
                curr.next = head.next
                head.next = curr
            # tail
            elif not curr.next.next:
                prev.next = prec
                curr.next = prec.next
                prec.next = curr
            # mid
            else:
                prev.next = curr.next
                curr.next = prec.next
                prec.next = curr
            prev = curr
            curr = curr.next
        return head

다른 풀이를 보니 head 앞에 root 노드를 만들어서 모든 노드에서 동일한 방식으로 swap이 가능하게 푼 방법도 있었다.

알고리즘 문제를 많이 풀어보질 않아서 이런 알고리즘 테크닉들을 익혀두면 알고리즘 문제들을 조금 더 general하게 풀 수 있을 것 같다.

 

class Solution:
    def swapPairs(self, head: ListNode) -> ListNode:
        root = prev = ListNode()
        prev.next = head
        while head and head.next:
            b = head.next
            head.next = b.next
            b.next = head
            
            prev.next = b
            
            prev = head
            head = head.next
            
        return root.next

'Algorithm > Leetcode' 카테고리의 다른 글

[LeetCode] 92. Reverse Linked List II  (0) 2021.01.24
[LeetCode] 328. Odd Even Linked List  (0) 2021.01.24
[LeetCode] 2. Add Two Numbers  (0) 2021.01.23
[LeetCode] 206. Reverse Linked List  (0) 2021.01.23
[LeetCode] 21. Merge Two Sorted Lists  (0) 2021.01.23

+ Recent posts