<?xml version="1.0" encoding="utf-8"?><?xml-stylesheet type="text/xsl" href="rss.xsl"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <channel>
        <title>Silver Archive Blog</title>
        <link>https://watanka.github.io/</link>
        <description>Silver Archive Blog</description>
        <lastBuildDate>Sun, 13 Jul 2025 00:00:00 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <language>ko</language>
        <item>
            <title><![CDATA[The New Code]]></title>
            <link>https://watanka.github.io/the-new-code</link>
            <guid>https://watanka.github.io/the-new-code</guid>
            <pubDate>Sun, 13 Jul 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[OpenAI의 Sean Grove의 스피치 The New Code를 듣고 느낀 내용을 정리했습니다. Agent 시대에 새로운 코드의 모습이 어떨지 상상해보는 시간이였습니다.]]></description>
            <content:encoded><![CDATA[<p>최근 OpenAI의 Sean Grove가 진행한 “The New Code”라는 스피치를 듣고 많은 걸 느꼈습니다. LLM의 발전에 따라 변화할 소프트웨어의 미래와 그 변화의 양상을 소프트웨어의 본질에 근거해 상상하고 구체화해볼 수 있었습니다.<br>
<!-- -->※ 이 글은 해당 영상을 기반으로 한 요약이며, 일부 해석은 제 개인적인 의견이 반영되어 있을 수 있습니다.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="-소프트웨어의-본질-코드는-전부가-아닙니다">🔍 소프트웨어의 본질: 코드는 전부가 아닙니다<a href="https://watanka.github.io/the-new-code#-%EC%86%8C%ED%94%84%ED%8A%B8%EC%9B%A8%EC%96%B4%EC%9D%98-%EB%B3%B8%EC%A7%88-%EC%BD%94%EB%93%9C%EB%8A%94-%EC%A0%84%EB%B6%80%EA%B0%80-%EC%95%84%EB%8B%99%EB%8B%88%EB%8B%A4" class="hash-link" aria-label="🔍 소프트웨어의 본질: 코드는 전부가 아닙니다에 대한 직접 링크" title="🔍 소프트웨어의 본질: 코드는 전부가 아닙니다에 대한 직접 링크">​</a></h2>
<p>우리는 흔히 소프트웨어의 중심이 ‘코드’라고 생각하지만, 실제로 코드가 만들어지기까지의 전 과정을 들여다보면 진짜 핵심은 '커뮤니케이션' 에 있음을 알 수 있습니다.<br>
<!-- -->무엇을 만들지, 왜 만들지, 어떻게 만들지, 그리고 만들어진 것이 의도한 대로 작동하는지까지. 이 모든 것은 사람 사이 또는 사람과 인공지능 사이에 구조화된 커뮤니케이션을 통해 이루어집니다.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="-llm-시대의-전환점-컴파일러에서-모델로">🤖 LLM 시대의 전환점: 컴파일러에서 모델로<a href="https://watanka.github.io/the-new-code#-llm-%EC%8B%9C%EB%8C%80%EC%9D%98-%EC%A0%84%ED%99%98%EC%A0%90-%EC%BB%B4%ED%8C%8C%EC%9D%BC%EB%9F%AC%EC%97%90%EC%84%9C-%EB%AA%A8%EB%8D%B8%EB%A1%9C" class="hash-link" aria-label="🤖 LLM 시대의 전환점: 컴파일러에서 모델로에 대한 직접 링크" title="🤖 LLM 시대의 전환점: 컴파일러에서 모델로에 대한 직접 링크">​</a></h2>
<p>LLM이 본격적으로 등장하면서, 프로그래머가 코드를 직접 작성하지 않고 모델에게 자연어로 설명하는 방식(바이브 코딩)이 점차 늘어나고 있습니다.<br>
<!-- -->LLM이라는 매개체를 통해 사람의 의도를 컴퓨터에게 입력하는 것은 사실 새로운 컨셉이 아닙니다. 우리는 항상 코드를 통해 우리의 의도를 컴퓨터에게 전달하고 있었습니다. 개발자가 의도를 담아 코드를 작성하면, 컴파일러가 이를 번역해서 컴퓨터가 수행할 수 있는 기계어로 전환합니다.   LLM은 단지 코드를 사람의 언어(자연어)로 확장했을 뿐이라고 볼 수 있습니다.<br>
<!-- -->그렇다면, 여기서 한 가지 고민해볼 점은 지금까지처럼 코드를 보존하고 관리하는 것이 여전히 적절한 방식인지, 아니면 프롬프트나 명세와 같은 인간의 의도를 담은 표현을 중심으로 보존해야 하는지에 대해서입니다.</p>
<div class="codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">**사람이 작성한 코드** - [컴파일러] -&gt; 기계어</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">**사람이 작성한 프롬프트와 명세** - [LLM] -&gt; 코드</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="클립보드에 코드 복사" title="복사" class="clean-btn"><span class="copyButtonIcons_eSgA" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_y97N"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_LjdS"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>이런 논리라면, 컴파일러가 작성한 기계어가 아니라, <strong>의도가 좀 더 명확한 프롬프트와 명세(specification)</strong> 를 보존하는 것이 더 타당하지 않을까요?</p>
<p>Sean Grove는 이 프롬프트와 명세를 <strong>'모델 스펙'</strong> 이라고 정의합니다.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="-모델-스펙-ai가-이해하는-인간의-언어">📄 모델 스펙: AI가 이해하는 인간의 언어<a href="https://watanka.github.io/the-new-code#-%EB%AA%A8%EB%8D%B8-%EC%8A%A4%ED%8E%99-ai%EA%B0%80-%EC%9D%B4%ED%95%B4%ED%95%98%EB%8A%94-%EC%9D%B8%EA%B0%84%EC%9D%98-%EC%96%B8%EC%96%B4" class="hash-link" aria-label="📄 모델 스펙: AI가 이해하는 인간의 언어에 대한 직접 링크" title="📄 모델 스펙: AI가 이해하는 인간의 언어에 대한 직접 링크">​</a></h2>
<p>Sean Grove는 앞으로의 소프트웨어 개발에서 가장 중요한 자산은 코드가 아니라 명확한 명세(model spec) 가 될 것이라고 말합니다.</p>
<p>모델 스펙은 다음과 같은 특징을 가집니다:<br>
<!-- -->• <strong>Specs compose</strong>: 조합 가능하며 모듈화될 수 있습니다.<br>
<!-- -->• <strong>Specs are executable</strong>: 실행 가능한 명령 구조를 가집니다.<br>
<!-- -->• <strong>Specs are testable</strong>: 테스트가 가능하며 검증 기준을 포함할 수 있습니다.<br>
<!-- -->• <strong>Specs have interfaces</strong>: 외부와 소통할 수 있는 인터페이스를 가집니다.</p>
<p>이러한 스펙은 단순한 설명을 넘어서, 사람의 의도를 일관되게 전달할 수 있는 추상화된 형식입니다. 말 그대로 자연어가 새로운 코드(The New Code)인 셈입니다.
LLM 시대에는 이 명세가 자연어로 작성되기 때문에, 기존 코드보다도 명확한 의사소통의 도구로 기능할 수 있습니다.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="️-법�조-시스템과-스펙-기반-개발의-유사성">⚖️ 법조 시스템과 스펙 기반 개발의 유사성<a href="https://watanka.github.io/the-new-code#%EF%B8%8F-%EB%B2%95%EC%A1%B0-%EC%8B%9C%EC%8A%A4%ED%85%9C%EA%B3%BC-%EC%8A%A4%ED%8E%99-%EA%B8%B0%EB%B0%98-%EA%B0%9C%EB%B0%9C%EC%9D%98-%EC%9C%A0%EC%82%AC%EC%84%B1" class="hash-link" aria-label="⚖️ 법조 시스템과 스펙 기반 개발의 유사성에 대한 직접 링크" title="⚖️ 법조 시스템과 스펙 기반 개발의 유사성에 대한 직접 링크">​</a></h2>
<p>모델 스펙의 개념은 법조 시스템과도 매우 유사합니다.<br>
<!-- -->사회가 사람의 행동을 정렬하기 위해 헌법과 법률을 만들듯, AI 모델 역시 명확한 스펙을 통해 행동을 정렬 하게 됩니다.</p>
<p>• <strong>헌법 · 법률 (written text)</strong>
→ written specification
모델이 따라야 할 기준이 되는 문서. 명확하고 모호하지 않아야 함.</p>
<p>• <strong>개정 조항 (amendments)</strong>
→ pull request / version bump
명세를 업데이트하는 절차. 코드의 버전 관리와 유사.</p>
<p>• <strong>판례 (case law)</strong>
→ regression tests
기존 동작을 유지하는 테스트. 과거의 기준을 반복해서 검증함.</p>
<p>• <strong>사법적 검토 (judicial review)</strong>
→ grader model
모델의 결과물이 명세에 부합하는지 자동 평가.</p>
<p>• <strong>권한 계층 (supremacy clause / hierarchy)</strong>
→ spec hierarchy
명세 간 우선순위를 설정하는 구조. 충돌 방지를 위함.</p>
<p>• <strong>집행 기관 (enforcement by executive)</strong>
→ reinforcement loop
명세에 맞는 행동을 하도록 학습을 유도하는 강화 루프.</p>
<p>이러한 비교는 AI 엔지니어의 역할이 단순한 기술 구현자 를 넘어, AI 시스템의 ‘입법자’ 혹은 ‘정책 설계자’ 로 진화하고 있음을 보여줍니다.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="-직업의-본질은-정렬입니다">🧭 직업의 본질은 ‘정렬’입니다<a href="https://watanka.github.io/the-new-code#-%EC%A7%81%EC%97%85%EC%9D%98-%EB%B3%B8%EC%A7%88%EC%9D%80-%EC%A0%95%EB%A0%AC%EC%9E%85%EB%8B%88%EB%8B%A4" class="hash-link" aria-label="🧭 직업의 본질은 ‘정렬’입니다에 대한 직접 링크" title="🧭 직업의 본질은 ‘정렬’입니다에 대한 직접 링크">​</a></h2>
<p>Sean Grove의 스피치는 <strong>정렬(alignment)</strong> 에 대한 통찰로 마무리됩니다.<br>
<!-- -->우리의 직업은 각기 다르지만, 본질적으로는 ‘무언가를 정렬시키는 일’을 수행 하고 있습니다. 정렬이란 단순히 "일치시키는 것"이 아닙니다. <strong>의도를 명확히 정의하고 최소 법칙 아래에서 구성원들(컴포넌트들)이 자유롭지만 의도한대로 동작할 수 있도록 설계</strong> 하는 일 입니다. 아마 이 과정을 얼마나 명확히 할 수 있는지가 미래의 핵심 역량 이 될 것이라고 조심스럽게 예측해봅니다.</p>
<p>역할별 정렬 대상을 정리해본다면 다음과 같습니다.</p>
<p><strong>프로그래머</strong></p>
<ul>
<li>정렬 대상: 컴퓨터(실리콘)</li>
<li>정렬 수단: 코드 명세 (code spec)</li>
</ul>
<p><strong>제품 매니저(PM)</strong></p>
<ul>
<li>정렬 대상: 팀</li>
<li>정렬 수단: 제품 명세 (product spec)</li>
</ul>
<p><strong>법률가</strong></p>
<ul>
<li>정렬 대상: 사회 구성원</li>
<li>정렬 수단: 법률 명세 (legal spec)</li>
</ul>
<p><strong>AI 엔지니어</strong></p>
<ul>
<li>정렬 대상: 인공지능 모델</li>
<li>정렬 수단: 모델 명세 (model spec)</li>
</ul>
<p>20분짜리 짧은 영상이였지만, 많은 내용이 담겨 있어 미처 담지 못한 내용들도 있습니다. 좋은 영상인 것 같아 직접 영상을 보시는 것도 추천드립니다.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[MCP가 뭐길래🤔]]></title>
            <link>https://watanka.github.io/mcp</link>
            <guid>https://watanka.github.io/mcp</guid>
            <pubDate>Sun, 30 Mar 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[LLM에게 날개를 달아줄 툴들의 프로토콜, mcp에 대해 알아봅니다.]]></description>
            <content:encoded><![CDATA[<p>최근 AI 개발 커뮤니티에서 **MCP(Model Context Protocol)**가 큰 주목을 받고 있습니다. 2024년 11월 <strong>Anthropic</strong>이 오픈소스로 공개한 이 프로토콜은 처음에는 큰 반응을 얻지 못했지만, 2025년 초부터 관심이 급격히 증가하고 있습니다. MCP는 LLM에게 부족한 파일 시스템, 데이터베이스, API 등 기존 데이터 소스를 컨텍스트로써 통신할 수 있는 표준을 제공합니다. HTTP 통신이 처음 세상에 나왔을 때처럼, LLM 엔진에 붙일 수 있는 도구를 정의하는 규격을 제공함으로써, LLM이 가진 가능성을 무한대로 확장시켜줍니다.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="mcp란-무엇인가"><strong>MCP란 무엇인가?</strong><a href="https://watanka.github.io/mcp#mcp%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80" class="hash-link" aria-label="mcp란-무엇인가에 대한 직접 링크" title="mcp란-무엇인가에 대한 직접 링크">​</a></h2>
<p>MCP는 <strong>대규모 언어 모델(LLM)이 외부 데이터와 시스템을 효과적으로 활용할 수 있도록 설계된 개방형 표준 프로토콜</strong>입니다. <strong>Anthropic</strong>은 MCP를 <strong>USB-C 포트</strong>에 비유하는데, USB-C가 다양한 기기와 주변 장치를 표준화된 방식으로 연결하듯이, MCP는 AI 모델이 다양한 데이터 소스와 도구에 표준화된 방식으로 연결될 수 있도록 합니다.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="mcp의-주요-특징"><strong>MCP의 주요 특징</strong><a href="https://watanka.github.io/mcp#mcp%EC%9D%98-%EC%A3%BC%EC%9A%94-%ED%8A%B9%EC%A7%95" class="hash-link" aria-label="mcp의-주요-특징에 대한 직접 링크" title="mcp의-주요-특징에 대한 직접 링크">​</a></h3>
<ol>
<li><strong>개방형 표준</strong>: 누구나 자유롭게 사용하고 개선할 수 있습니다.</li>
<li><strong>양방향 연결</strong>: AI 모델과 데이터 소스 간의 지속적인 통신을 지원합니다.</li>
<li><strong>범용성과 표준화</strong>: 다양한 데이터 소스와 도구를 하나의 프로토콜로 연결합니다.</li>
<li><strong>보안 및 신뢰성</strong>: 안전하고 신뢰할 수 있는 연결을 제공합니다.</li>
</ol>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="mcp의-동작-방식"><strong>MCP의 동작 방식</strong><a href="https://watanka.github.io/mcp#mcp%EC%9D%98-%EB%8F%99%EC%9E%91-%EB%B0%A9%EC%8B%9D" class="hash-link" aria-label="mcp의-동작-방식에 대한 직접 링크" title="mcp의-동작-방식에 대한 직접 링크">​</a></h2>
<p>MCP는 다음과 같은 프로세스를 거칩니다.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="1-연결-설정"><strong>1. 연결 설정</strong><a href="https://watanka.github.io/mcp#1-%EC%97%B0%EA%B2%B0-%EC%84%A4%EC%A0%95" class="hash-link" aria-label="1-연결-설정에 대한 직접 링크" title="1-연결-설정에 대한 직접 링크">​</a></h3>
<ul>
<li>호스트 애플리케이션(예: <strong>Claude Desktop, Cursor</strong>)이 필요한 기능을 가진 <strong>MCP 서버에 대한 클라이언트를 생성</strong>합니다.</li>
</ul>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="2-초기화-프로세스"><strong>2. 초기화 프로세스</strong><a href="https://watanka.github.io/mcp#2-%EC%B4%88%EA%B8%B0%ED%99%94-%ED%94%84%EB%A1%9C%EC%84%B8%EC%8A%A4" class="hash-link" aria-label="2-초기화-프로세스에 대한 직접 링크" title="2-초기화-프로세스에 대한 직접 링크">​</a></h3>
<ul>
<li>클라이언트가 서버와 연결되면 <strong>JSON-RPC 기반 메시지를 통해 프로토콜 버전과 지원 가능한 기능을 확인</strong>합니다.</li>
<li>이 과정에서 <strong>서버의 능력과 제약 사항이 결정</strong>됩니다.</li>
</ul>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="3-기능-탐색"><strong>3. 기능 탐색</strong><a href="https://watanka.github.io/mcp#3-%EA%B8%B0%EB%8A%A5-%ED%83%90%EC%83%89" class="hash-link" aria-label="3-기능-탐색에 대한 직접 링크" title="3-기능-탐색에 대한 직접 링크">​</a></h3>
<ul>
<li>클라이언트는 서버에 <code>tools/list</code> 등의 요청을 보내 <strong>사용 가능한 도구, 리소스, 프롬프트 목록을 수집</strong>합니다.</li>
<li>이 정보는 호스트를 통해 <strong>LLM에게 전달</strong>됩니다.</li>
</ul>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="4-요청-처리-과정"><strong>4. 요청 처리 과정</strong><a href="https://watanka.github.io/mcp#4-%EC%9A%94%EC%B2%AD-%EC%B2%98%EB%A6%AC-%EA%B3%BC%EC%A0%95" class="hash-link" aria-label="4-요청-처리-과정에 대한 직접 링크" title="4-요청-처리-과정에 대한 직접 링크">​</a></h3>
<ol>
<li>사용자가 호스트에 질문을 입력하면 LLM이 이를 분석합니다.</li>
<li>LLM은 적절한 서버와 도구를 선택해 <strong>구조화된 요청</strong>을 생성합니다.</li>
<li>호스트는 해당 요청을 관련 클라이언트에 전달합니다.</li>
<li>클라이언트는 요청을 서버가 이해할 수 있는 형식으로 변환하여 전송합니다.</li>
<li>서버는 요청된 작업을 수행하고 결과를 반환합니다.</li>
<li>결과는 클라이언트를 통해 호스트로 전달되며, 최종적으로 LLM에게 제공됩니다.</li>
</ol>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="5-통신-형식"><strong>5. 통신 형식</strong><a href="https://watanka.github.io/mcp#5-%ED%86%B5%EC%8B%A0-%ED%98%95%EC%8B%9D" class="hash-link" aria-label="5-통신-형식에 대한 직접 링크" title="5-통신-형식에 대한 직접 링크">​</a></h3>
<ul>
<li><strong>JSON-RPC 2.0 프로토콜</strong>을 기반으로 데이터 교환이 이루어집니다.</li>
<li><strong>텍스트 데이터는 UTF-8 인코딩</strong>, 이미지 및 바이너리 데이터는 <strong>Base64 인코딩</strong>을 사용하여 전송됩니다.</li>
</ul>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="mcp-구조"><strong>MCP 구조</strong><a href="https://watanka.github.io/mcp#mcp-%EA%B5%AC%EC%A1%B0" class="hash-link" aria-label="mcp-구조에 대한 직접 링크" title="mcp-구조에 대한 직접 링크">​</a></h2>
<p><img decoding="async" loading="lazy" alt="mcp구조" src="https://watanka.github.io/assets/images/mcp%EA%B5%AC%EC%A1%B0-9d10124f7eda4f256a35def6557930af.png" width="2132" height="1374" class="img_ev3q"></p>
<p>MCP는 호스트(Host), 클라이언트(Client), 서버(Server) 세 개의 컴포넌트로 이루어져 있습니다.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="1-호스트host"><strong>1. 호스트(Host)</strong><a href="https://watanka.github.io/mcp#1-%ED%98%B8%EC%8A%A4%ED%8A%B8host" class="hash-link" aria-label="1-호스트host에 대한 직접 링크" title="1-호스트host에 대한 직접 링크">​</a></h3>
<p>호스트는 AI 애플리케이션의 컨테이너이자 조정자 역할을 합니다.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="호스트의-주요-역할"><strong>호스트의 주요 역할</strong><a href="https://watanka.github.io/mcp#%ED%98%B8%EC%8A%A4%ED%8A%B8%EC%9D%98-%EC%A3%BC%EC%9A%94-%EC%97%AD%ED%95%A0" class="hash-link" aria-label="호스트의-주요-역할에 대한 직접 링크" title="호스트의-주요-역할에 대한 직접 링크">​</a></h3>
<ul>
<li>여러 클라이언트 인스턴스를 생성하고 관리</li>
<li>클라이언트 연결 권한과 생명 주기 제어</li>
<li>보안 정책과 동의 요구사항 시행</li>
<li>AI/LLM 통합 및 샘플링 조정</li>
<li>대화 컨텍스트 관리 및 클라이언트 간 컨텍스트 집계</li>
</ul>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="2-클라이언트client"><strong>2. 클라이언트(Client)</strong><a href="https://watanka.github.io/mcp#2-%ED%81%B4%EB%9D%BC%EC%9D%B4%EC%96%B8%ED%8A%B8client" class="hash-link" aria-label="2-클라이언트client에 대한 직접 링크" title="2-클라이언트client에 대한 직접 링크">​</a></h3>
<p>클라이언트는 <strong>호스트에 의해 생성되며 서버와의 독립적인 1:1 연결을 유지</strong>합니다.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="클라이언트의-주요-역할"><strong>클라이언트의 주요 역할</strong><a href="https://watanka.github.io/mcp#%ED%81%B4%EB%9D%BC%EC%9D%B4%EC%96%B8%ED%8A%B8%EC%9D%98-%EC%A3%BC%EC%9A%94-%EC%97%AD%ED%95%A0" class="hash-link" aria-label="클라이언트의-주요-역할에 대한 직접 링크" title="클라이언트의-주요-역할에 대한 직접 링크">​</a></h3>
<ul>
<li>서버당 하나의 상태 유지 세션 설정</li>
<li>프로토콜 협상 및 기능 교환 처리</li>
<li>양방향으로 프로토콜 메시지 라우팅</li>
<li>구독 및 알림 관리</li>
<li>서버 간 보안 경계 유지</li>
</ul>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="3-서버server"><strong>3. 서버(Server)</strong><a href="https://watanka.github.io/mcp#3-%EC%84%9C%EB%B2%84server" class="hash-link" aria-label="3-서버server에 대한 직접 링크" title="3-서버server에 대한 직접 링크">​</a></h3>
<p>서버는 특정 <strong>컨텍스트와 기능을 제공하는 독립적인 프로그램</strong>입니다.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="서버의-주요-역할"><strong>서버의 주요 역할</strong><a href="https://watanka.github.io/mcp#%EC%84%9C%EB%B2%84%EC%9D%98-%EC%A3%BC%EC%9A%94-%EC%97%AD%ED%95%A0" class="hash-link" aria-label="서버의-주요-역할에 대한 직접 링크" title="서버의-주요-역할에 대한 직접 링크">​</a></h3>
<ul>
<li>리소스, 도구, 프롬프트 등의 기능 노출</li>
<li>독립적으로 작동하며 특정 책임 수행</li>
<li>클라이언트 인터페이스를 통해 샘플링 요청 처리</li>
<li>보안 제약 준수</li>
<li>로컬 프로세스 또는 원격 서비스로 구현 가능</li>
</ul>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="클라이언트가-서버를-호출하는-방식"><strong>클라이언트가 서버를 호출하는 방식</strong><a href="https://watanka.github.io/mcp#%ED%81%B4%EB%9D%BC%EC%9D%B4%EC%96%B8%ED%8A%B8%EA%B0%80-%EC%84%9C%EB%B2%84%EB%A5%BC-%ED%98%B8%EC%B6%9C%ED%95%98%EB%8A%94-%EB%B0%A9%EC%8B%9D" class="hash-link" aria-label="클라이언트가-서버를-호출하는-방식에 대한 직접 링크" title="클라이언트가-서버를-호출하는-방식에 대한 직접 링크">​</a></h2>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="1-stdio-방식-표준-입출력"><strong>1. stdio 방식 (표준 입출력)</strong><a href="https://watanka.github.io/mcp#1-stdio-%EB%B0%A9%EC%8B%9D-%ED%91%9C%EC%A4%80-%EC%9E%85%EC%B6%9C%EB%A0%A5" class="hash-link" aria-label="1-stdio-방식-표준-입출력에 대한 직접 링크" title="1-stdio-방식-표준-입출력에 대한 직접 링크">​</a></h3>
<ul>
<li>클라이언트가 서버를 <strong>하위 프로세스로 실행</strong>합니다.</li>
<li>서버는 표준 입력(<code>stdin</code>)으로 명령을 받고 표준 출력(<code>stdout</code>)으로 응답합니다.</li>
<li><strong>JSON-RPC 2.0 형식</strong>으로 메시지를 주고받습니다.</li>
<li><strong>로컬 환경에서 설정이 간단</strong>하고 <strong>빠르게 실행 가능</strong>합니다.</li>
</ul>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="2-httpsse-방식-server-sent-events"><strong>2. HTTP+SSE 방식 (Server-Sent Events)</strong><a href="https://watanka.github.io/mcp#2-httpsse-%EB%B0%A9%EC%8B%9D-server-sent-events" class="hash-link" aria-label="2-httpsse-방식-server-sent-events에 대한 직접 링크" title="2-httpsse-방식-server-sent-events에 대한 직접 링크">​</a></h3>
<ul>
<li>클라이언트는 HTTP <code>POST</code> 요청으로 명령을 서버에 보냅니다.</li>
<li>서버는 <code>SSE(Server-Sent Events)</code>를 사용하여 <strong>클라이언트에게 응답이나 이벤트를 스트리밍</strong>합니다.</li>
<li><strong>웹 애플리케이션 및 분산 시스템</strong>에서 유용합니다.</li>
</ul>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="마무리">마무리<a href="https://watanka.github.io/mcp#%EB%A7%88%EB%AC%B4%EB%A6%AC" class="hash-link" aria-label="마무리에 대한 직접 링크" title="마무리에 대한 직접 링크">​</a></h2>
<p>MCP는 <strong>AI 모델이 외부 데이터와 도구에 접근할 수 있는 표준화된 방법을 제공</strong>함으로써 AI 애플리케이션의 가능성을 크게 확장시키고 있습니다. <strong>호스트, 클라이언트, 서버</strong>로 구성된 아키텍처는 복잡한 시스템을 효율적으로 관리할 수 있도록 돕습니다.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[안쓰는 노트북으로 개인 서버 만들기💻-배포관리 편]]></title>
            <link>https://watanka.github.io/personal_server2</link>
            <guid>https://watanka.github.io/personal_server2</guid>
            <pubDate>Sat, 01 Mar 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[안 쓰는 노트북으로 개인 서버를 구축하는 두 번째 이야기입니다. 리눅스 설치부터 SSH 설정, 도커 레지스트리, GitHub Actions Runner, 쿠버네티스 환경 구성까지 홈 서버 배포 환경 구축 과정을 소개합니다.]]></description>
            <content:encoded><![CDATA[<p><a href="https://watanka.github.io/personal_server" target="_blank" rel="noopener noreferrer">이전 글</a>에서는 홈서버의 네트워크를 셋팅했다. 이번 글에서는 본격적으로 홈서버에서 사이드 프로젝트를 배포하고 관리하기 위해서 필요한 설정들을 하려고 한다.
글의 구성은 다음과 같다. 필요한 부분만 찾아서 확인해보셔도 좋겠다.</p>
<ul>
<li>리눅스 설치</li>
<li>ssh 설치</li>
<li>서버용 설정</li>
<li>docker-registry 설정</li>
<li>github-action runner 설정</li>
<li>쿠버네티스 환경 구성</li>
</ul>
<br>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="리눅스-설치">리눅스 설치<a href="https://watanka.github.io/personal_server2#%EB%A6%AC%EB%88%85%EC%8A%A4-%EC%84%A4%EC%B9%98" class="hash-link" aria-label="리눅스 설치에 대한 직접 링크" title="리눅스 설치에 대한 직접 링크">​</a></h3>
<p>macos의 버젼이 오래 돼서 도커 데스크탑 설치가 안 된다. os를 업데이트하려고 했지만, 왠지 모르는 이유로 소프트웨어 업데이트 버튼 클릭 시, 무한로딩에 걸려버려서 리눅스를 설치했다.</p>
<p><img decoding="async" loading="lazy" alt="리눅스를 설치한 맥북" src="https://watanka.github.io/assets/images/%EB%85%B8%ED%8A%B8%EB%B6%81-%EB%A6%AC%EB%88%85%EC%8A%A4%EC%84%A4%EC%B9%98-272cf272cdb90a7c05dd2d49ba6c8330.jpg" width="1080" height="1440" class="img_ev3q"></p>
<br>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="ssh-설치">ssh 설치<a href="https://watanka.github.io/personal_server2#ssh-%EC%84%A4%EC%B9%98" class="hash-link" aria-label="ssh 설치에 대한 직접 링크" title="ssh 설치에 대한 직접 링크">​</a></h3>
<p>다음은 외부에서 홈서버의 콘솔에 접속할 수 있도록 ssh를 설치한다. ssh는 컴퓨터 간 암호화되어 안전한 통신을 가능하게 해주는 프로토콜이다. ssh에 대해 처음 들어본다면, 검색해보자. 자세히 잘 설명해준 글들이 많다.</p>
<p><img decoding="async" loading="lazy" alt="비밀키와 공개키를 통해 컴퓨터간 �통신을 하는 ssh" src="https://watanka.github.io/assets/images/ssh%EC%84%A4%EB%AA%85-52afdb87488693af4bb0a5c6042f710b.png" width="1404" height="724" class="img_ev3q">
id_rsa를 발급받고 id_rsa.pub을 서버에 심는다. 그리고, 공유기 관리자 페이지에서 포트 포워딩을 해준다.<br>
<!-- -->이제 외부에서 로컬 서버로 접속이 가능하다.<br>
<img decoding="async" loading="lazy" alt="ssh 접속 확인!" src="https://watanka.github.io/assets/images/ssh%EC%A0%91%EC%86%8D-2dfb49aa450721f14cf57cc0824b0589.png" width="764" height="488" class="img_ev3q">
현재는 내부 네트워크에서 접속하는 거지만, 외부에서 접속하고 싶다면, 마찬가지로 ssh key를 발급받고, 서버에 심은 다음, 이전 글에서 설정한 dDNS 주소로 접속하면 된다.</p>
<br>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="서버용으로-노트북-설정-변경하기">서버용으로 노트북 설정 변경하기<a href="https://watanka.github.io/personal_server2#%EC%84%9C%EB%B2%84%EC%9A%A9%EC%9C%BC%EB%A1%9C-%EB%85%B8%ED%8A%B8%EB%B6%81-%EC%84%A4%EC%A0%95-%EB%B3%80%EA%B2%BD%ED%95%98%EA%B8%B0" class="hash-link" aria-label="서버용으로 노트북 설정 변경하기에 대한 직접 링크" title="서버용으로 노트북 설정 변경하기에 대한 직접 링크">​</a></h3>
<p>이 노트북은 24/7 돌아가기에 적합한 서버용은 아니지만, 필요한 설정들을 해주자. 절전 모드를 비활성화하고, 덮개가 닫혀도 돌아갈 수 있도록 설정한다.</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">sudo systemctl mask sleep.target suspend.target hibernate.target hybrid-sleep.target</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="클립보드에 코드 복사" title="복사" class="clean-btn"><span class="copyButtonIcons_eSgA" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_y97N"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_LjdS"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p><img decoding="async" loading="lazy" alt="노트북의 절전 모드를 끈다" src="https://watanka.github.io/assets/images/laptop-setting-fc306894f446a612fab8ed633e771372.png" width="1223" height="194" class="img_ev3q">
이제 이 서버(노트북이였던)는 더 이상 절전모드에 들어가지 않는다. 그리고 이어서 노트북 덮개를 덮어도 꺼지지 않도록 설정한다. etc 디렉토리의 <code>logind.conf</code> 에 들어가서 그림 아래 두 설정들을 ignore로 바꿔준다.</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">sudo nano /etc/systemd/logind.conf</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="클립보드에 코드 복사" title="복사" class="clean-btn"><span class="copyButtonIcons_eSgA" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_y97N"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_LjdS"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p><img decoding="async" loading="lazy" alt="노트북이 덮여도 꺼지지 않도록 구성한다." src="https://watanka.github.io/assets/images/%EB%85%B8%ED%8A%B8%EB%B6%81%EB%8D%AE%EA%B0%9C%EC%84%A4%EC%A0%95-41edc7ece8b699928754c08dfb690927.png" width="1509" height="920" class="img_ev3q"></p>
<p>노트북이 덮여도 꺼지지 않도록 구성한다.</p>
<p>그리고 서버가 재부팅되었을 때, 시작할 프로그램들을 설정해준다.</p>
<p>systemd 서비스에 직접 등록하거나, <code>systemctl enable [your-service]</code>로 설정할 수 있다. 나는 docker registry와 github action runner가 자동으로 시작되도록 설정했다.</p>
<br>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="docker-registry-구성">docker-registry 구성<a href="https://watanka.github.io/personal_server2#docker-registry-%EA%B5%AC%EC%84%B1" class="hash-link" aria-label="docker-registry 구성에 대한 직접 링크" title="docker-registry 구성에 대한 직접 링크">​</a></h3>
<p>다음은 docker-registry 구성했다. docker-registry는 도커 이미지 저장소이다. docker-registry를 처음 들어봤다면, AWS의 ECR이나 dockerhub를 생각하면 된다. 도커 컨테이너로 docker-registry를 띄우고, 컨테이너가 내려가도 이미지들은 그대로 저장되어 있을 수 있도록 volume을 지정해두었다.</p>
<br>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="github-action-runner-구성">github action runner 구성<a href="https://watanka.github.io/personal_server2#github-action-runner-%EA%B5%AC%EC%84%B1" class="hash-link" aria-label="github action runner 구성에 대한 직접 링크" title="github action runner 구성에 대한 직접 링크">​</a></h3>
<p>그리고 github action runner를 local-hosted runner로 구성했다. github action은 CI/CD를 무료로 제공하는 편리한 서비스다. github action은 기본적으로 github에서 제공하는 runner를 사용하는데, 이 runner는 무료인만큼 속도가 비교적 느리다는 단점과 따로 캐시를 설정하지 않는다면, 매번 CI/CD에 설정된 빌드 과정을 처음부터  실행하여 시간이 오래 걸린다는 단점이 있다. 이 또한, 설정하는 방법은 github action 페이지의 local-hosted runner 섹션에 <a href="https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners/about-self-hosted-runners" target="_blank" rel="noopener noreferrer">친절히 설명</a>되어 있으므로 자세한 과정은 생략한다.</p>
<p>github action 페이지에 local-hosted runner를  프로젝트별로 설정해야하길래, 나는 Organization을 만들어 사이드 프로젝트들을 다 옮기고, 이 organization을 관리하는 local-hosted runner로 등록했다.</p>
<br>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="쿠버네티스-환경-구축하기">쿠버네티스 환경 구축하기<a href="https://watanka.github.io/personal_server2#%EC%BF%A0%EB%B2%84%EB%84%A4%ED%8B%B0%EC%8A%A4-%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%B6%95%ED%95%98%EA%B8%B0" class="hash-link" aria-label="쿠버네티스 환경 구축하기에 대한 직접 링크" title="쿠버네티스 환경 구축하기에 대한 직접 링크">​</a></h3>
<h4 class="anchor anchorWithStickyNavbar_LWe7" id="쿠버네티스가-과연-필요한가">쿠버네티스가 과연 필요한가?<a href="https://watanka.github.io/personal_server2#%EC%BF%A0%EB%B2%84%EB%84%A4%ED%8B%B0%EC%8A%A4%EA%B0%80-%EA%B3%BC%EC%97%B0-%ED%95%84%EC%9A%94%ED%95%9C%EA%B0%80" class="hash-link" aria-label="쿠버네티스가 과연 필요한가?에 대한 직접 링크" title="쿠버네티스가 과연 필요한가?에 대한 직접 링크">​</a></h4>
<p>마지막은 대망의 쿠버네티스 환경 구축이다. 사실 이 부분은 환경적으로 정말 필요해서라기보다는 쿠버네티스 운영을 맛보고 싶어서다.</p>
<p>보통 쿠버네티스 환경 구성은 최소 3개의 마스터 노드와 여러 개의 워커 노드들로 이루어져있다. 나는 단지 맥북 하나 있을 뿐이고, 이 환경이 얼마만큼의 부하를 견딜 수 있을지도 모른다.</p>
<p>그리고 쿠버네티스의 기능은 컨테이너 오케스트레이션, 즉 여러 개의 컨테이너들을 한 번에 관리하는 것이다.이 여러 개의 컨테이너가 필요한 이유는 서버가 받는 트래픽을 분산시키기 위해서(또는 아키텍쳐 구성상 예를 들면 MSA)인데, 내가 진행하고 있는 사이드 프로젝트들 중에서 서버량이 여러 개의 인스턴스가 필요할 정도의 트래픽은 있는 건 아니다.</p>
<p>정리하자면 노드도 한 개이고 서비스 규모도 크지 않아서 쿠버네티스의 필요성은 의문이다. 하지만, 뭐 이렇게 시간이 남아돌 때 해봐야지 언제 해보겠나. 간단한 서비스를 띄워보고, 실제 트래픽이 많은 서비스 구성과는 어떻게 다를지 가늠하는 식으로 진행해보자.</p>
<h4 class="anchor anchorWithStickyNavbar_LWe7" id="쿠버네티스를-어떻게-구성하면-좋을까">쿠버네티스를 어떻게 구성하면 좋을까?<a href="https://watanka.github.io/personal_server2#%EC%BF%A0%EB%B2%84%EB%84%A4%ED%8B%B0%EC%8A%A4%EB%A5%BC-%EC%96%B4%EB%96%BB%EA%B2%8C-%EA%B5%AC%EC%84%B1%ED%95%98%EB%A9%B4-%EC%A2%8B%EC%9D%84%EA%B9%8C" class="hash-link" aria-label="쿠버네티스를 어떻게 구성하면 좋을까?에 대한 직접 링크" title="쿠버네티스를 어떻게 구성하면 좋을까?에 대한 직접 링크">​</a></h4>
<p>쿠버네티스에는 여러 종류의 배포판이 있다. 단일 노드 환경에 적합한 경량화된 쿠버네티스 배포판에는 minikube, k3 , kind 등이 있는데, 그 중에 k3를 선택했다. k3를 선택한 이유는 상태와 구성 정보를 저장하는 키-값 저장소인 etcd 대신 sqlite3를 사용하여 리소스를 절약할 수 있기 때문이다. minikube와 kind는 etcd를 사용한다. 우리는 단일 노드를 사용하기 때문에 etcd의 분산 시스템이 필요가 없다.</p>
<p>내 서버는 i5-7360U, cpu 4코어, 8gb RAM, 228gb 디스크를 가지고 있다. 이 노드 안에서 구성을 어떻게 할지 고민됐다.</p>
<p>서버 안에 가상 머신으로 노드들을 몇 개 구성하면, 좀 더 실제와 가까운 쿠버네티스 셋팅을 맛볼 수 있을 것 같다는 생각이 들었다. 예를 들어, taint나 tolerate 같은 노드 라벨링, 노드 간의 네트워크 통신 설정, 드레이닝이나 코든 같은 노드 유지보수 과정들을 실습해볼 수 있을 것 같다.</p>
<p>하지만 결과적으로는 가상 머신으로 분리하지 않고, 단일 노드에서 마스터 노드가 워커 노드 역할을 겸하는 구성으로 가기로 했다. 왜냐하면, 첫번째로 가상 머신을 여럿 띄울만큼의 리소스가 없다는 점. 그리고 두번째로 마스터 노드와 워커 노드의 분리 목적이 애초에 클러스터의 안정성을 위해서라는 점에서, 가상머신을 띄운 맥북이 다운되면 노드 전부가 꺼져버릴 게 당연하기 때문이다.</p>
<h4 class="anchor anchorWithStickyNavbar_LWe7" id="앱을-띄워보자">앱을 띄워보자<a href="https://watanka.github.io/personal_server2#%EC%95%B1%EC%9D%84-%EB%9D%84%EC%9B%8C%EB%B3%B4%EC%9E%90" class="hash-link" aria-label="앱을 띄워보자에 대한 직접 링크" title="앱을 띄워보자에 대한 직접 링크">​</a></h4>
<p>이 단일 노드에 쿠버네티스에 띄울 앱과 상태를 적은 쿠버네티스 manifest를 작성했다. 최대한 간단하게 앱을 구성했다. fastapi, react, mysql, 그리고 nginx로 돌아가는 앱이다. 이름을 등록하면, '이름 hello world'를 리턴하는 간단한 앱이다.(코드는 <a href="https://github.com/silver-s-side-projects/k8s-hello-world-app" target="_blank" rel="noopener noreferrer">여기</a>)</p>
<p><img decoding="async" loading="lazy" alt="이 앱은 이름도 프린트해준다!" src="https://watanka.github.io/assets/images/app-demo-437a91ae55bbd6c4c7174a3979458f1b.png" width="929" height="1007" class="img_ev3q"></p>
<p>이 앱은 이름도 프린트해준다!</p>
<details class="details_lb9f alert alert--info details_b_Ee" data-collapsed="true"><summary>Details</summary><div><div class="collapsibleContent_i85q"><p></p><summary>소소한 트러블 슈팅</summary>
docker registry에 이미지를 push할 때, github action이 계속 https로 요청을 보내서 오류가 난다. 우선은 docker daemon config에 insecure-registries 목록에 해당 registry 주소를 추가하여 (우회)해결했다. 근본적으로는 registry에도 https로 통신하는 게 맞기 때문에 후에 추가로 설정을 바꿔봐야겠다.<p></p></div></div></details>
<p>그리고 k8s에 올리기 위해 필요한 manifest들을 작성한다. 앱의 라벨링을 담당할 namespace, 각 요소(backend, frontend, nginx)의 deployment와 서비스, 그리고 각종 config들을 담을 configmap과, secret, 그리고 요청의 라우팅을 처리해줄 ingress(이쪽은 손을 더 봐야할듯)를 작성한다. 이후에는 self-hosted runner에서 돌아갈 workflow를 작성해준다. (몇 문장으로 정리가 가능하지만, 이 과정에 거의 세시간 정도 소요된 것 같다…) 드디어 이제, 로컬 서버 쿠버네티스에서 실행되는 앱을 확인할 수 있다….!</p>
<p><img decoding="async" loading="lazy" alt="쿠버네티스에 올라간 앱 동작 확인" src="https://watanka.github.io/assets/images/%EC%BF%A0%EB%B2%84%EB%84%A4%ED%8B%B0%EC%8A%A4%EB%8F%99%EC%9E%91%ED%99%95%EC%9D%B8-%EC%82%AC%EC%8B%A4%EC%95%84%EB%8B%98-d82f3dab2730cc329d3ca107217cd396.jpg" width="1080" height="1440" class="img_ev3q"></p>
<p>이번 시간에는 홈 서버에 작성한 앱을 배포하기 위한 배포 환경을 구성했다.</p>
<p>양이 생각보다 길어져서 다음 편으로 이어서 쓰겠다. 다음 편에는 이 앱을 가지고, 여러 설정을 추가(ingress 설정, 파드 증설 등)하고 실험을 해보면서 모니터링을 해보려고 한다.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[안쓰는 노트북으로 개인 서버 만들기💻-네트워크편]]></title>
            <link>https://watanka.github.io/personal_server</link>
            <guid>https://watanka.github.io/personal_server</guid>
            <pubDate>Sun, 16 Feb 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[안 쓰는 노트북을 활용해 개인 서버를 구축하는 과정을 소개합니다. 홈 네트워크 구성 파악부터 외부 IP 설정, DHCP 서버 설정, 포트 포워딩, 동적 DNS 등록까지 네트워크 설정의 모든 과정을 다룹니다.]]></description>
            <content:encoded><![CDATA[<p>얼마전 의도치 않은 AWS 요금 폭탄을 맞고(미리 요금 모니터링을 안 한 내 잘못이 맞ㄷㅏ…), 개인용 pc로 배포 서버를 구성하기로 마음 먹었다. 마침 중고로 팔려고 언젠가부터 창고 한켠에 쳐박아두었던 맥북 프로가 생각났다. 오랫동안 쓰지 않은 터라, 부팅 속도가 느리고, 프로그램 하나만 실행해도 팬 돌아가는 소리가 시끄럽긴 했지만, 꽤 잘 동작한다.</p>
<p><img decoding="async" loading="lazy" alt="macbook.png" src="https://watanka.github.io/assets/images/macbook-b947156d1a6f782d34496f2f61398c25.png" width="478" height="533" class="img_ev3q"></p>
<p>이 맥북을 서버로 사용하기 위해 필요한 작업을 크게 세가지로 정리했다.</p>
<ol>
<li>서버 셋팅</li>
<li>외부에서 접속 가능하도록 네트워크 설정</li>
<li>앱 배포</li>
</ol>
<br>
<p>서버 셋팅과 앱 배포 부분은 우선 네트워크를 구성해놓은 후 천천히 진행해도 될 것 같아서, 이번 글에서는 2번 네트워크 설정한 후기에 대해서 적어보려고 한다.</p>
<br>
<p>진행 방식은 다음과 같다.</p>
<ol>
<li>홈 네트워크 구성 파악하기</li>
<li>외부 IP와 내부 IP 파악하기</li>
<li>DHCP 서버에서 고정 IP 할당받기</li>
<li>포트 포워딩하기</li>
<li>dDNS 등록하기</li>
</ol>
<br>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="홈-네트워크-구성-파악하기">홈 네트워크 구성 파악하기<a href="https://watanka.github.io/personal_server#%ED%99%88-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-%EA%B5%AC%EC%84%B1-%ED%8C%8C%EC%95%85%ED%95%98%EA%B8%B0" class="hash-link" aria-label="홈 네트워크 구성 파악하기에 대한 직접 링크" title="홈 네트워크 구성 파악하기에 대한 직접 링크">​</a></h3>
<p>우선 맥북이 연결된 네트워크의 구성을 파악할 필요가 있었다.</p>
<p>네트워크 구성을 살펴보면 이렇게 그림으로 정리될 수 있다.</p>
<p><img decoding="async" loading="lazy" alt="network_composition.png" src="https://watanka.github.io/assets/images/network_composition-46f30924d6af8dd8b4892d795317d397.png" width="681" height="310" class="img_ev3q"></p>
<details class="details_lb9f alert alert--info details_b_Ee" data-collapsed="true"><summary>네트워크 구성 트러블 슈팅</summary><div><div class="collapsibleContent_i85q"><p>우리집 네트워크는 회선이 두 개다. 하나는 내 방에 랜선으로 데스크탑에 연결되어있고, 다른 하나는 거실에 WiFi를 연결할 수 있는 공유기로 연결되어 있다. 회선을 2개를 사용(아니 애시당초 왜 2개 회선인 거지…?)하고 있기 때문에 당연히 나는 구성이 데스크탑과 WIFI가 서로 별개의 네트워크 구성이라고 생각했다. 그림으로 그려보자면,</p><p><img decoding="async" loading="lazy" alt="wrong_network_composition.png" src="https://watanka.github.io/assets/images/wrong_network_composition-a4f9c43ce0403d4a83c88b829535829e.png" width="404" height="318" class="img_ev3q"></p><p>이런 식으로 말이다.</p><p>결론적으로 알게 된 구성의 형태는</p><p><img decoding="async" loading="lazy" alt="network_composition.png" src="https://watanka.github.io/assets/images/network_composition-46f30924d6af8dd8b4892d795317d397.png" width="681" height="310" class="img_ev3q"></p><p>이런 식이였다.</p><p>35.1 공유기의 포트 포워딩 설정 후에도 외부에서 접속이 불가한 이슈가 있었는데, 한참을 헤매다가 <code>traceroute 8.8.8.8</code> 커맨드로 35.1 공유기를 지나친 패킷이 55.1을 거쳐간다는 걸 확인하고 35.1이 55.1에 속해있다는 걸 파악할 수 있었다.</p><p><img decoding="async" loading="lazy" alt="traceroute.png" src="https://watanka.github.io/assets/images/traceroute-0267b56ab3b2fad68e9c546af24bb767.png" width="882" height="122" class="img_ev3q"></p><p>그래서 이 35.1 공유기를 bridge모드로 설정하여 서브넷팅 없이 패킷만 전송하도록 설정하니 외부에서 접속이 가능해졌다.</p><p>아무튼 내 소소한 삽질이였고, 본인의 네트워크 구성도 skbroad밴드에서 회선을 두 개 사용하는 분이라면, 위에 저 그림만 참고하면 좋을 것 같다.</p><p>그런데 인터넷 회선은 결국 랜선에 연결된 55.1 하나인 것 같은데, 도대체 왜 회선 2개만큼의 비용을 지불해야하는지 의문이다. 아시는 분…?</p></div></div></details>
<br>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="외부-ip와-맥북-ip-파악하기">외부 IP와 맥북 IP 파악하기<a href="https://watanka.github.io/personal_server#%EC%99%B8%EB%B6%80-ip%EC%99%80-%EB%A7%A5%EB%B6%81-ip-%ED%8C%8C%EC%95%85%ED%95%98%EA%B8%B0" class="hash-link" aria-label="외부 IP와 맥북 IP 파악하기에 대한 직접 링크" title="외부 IP와 맥북 IP 파악하기에 대한 직접 링크">​</a></h3>
<p>외부에서 바라보는 내 네트워크(맥북이 연결된)의 공인 IP를 우선 파악한다.</p>
<p>이렇게 되면</p>
<p><a href="http://ip.pe.kr/" target="_blank" rel="noopener noreferrer">ip.pe.kr</a>에 접속하면 내 공인 IP를 확인할 수 있다.</p>
<p><img decoding="async" loading="lazy" alt="public_ip.png" src="https://watanka.github.io/assets/images/public_ip-7cd91d5f56377d722722edd1fa68800a.png" width="584" height="335" class="img_ev3q"></p>
<p>그리고 이제 맥북의 IP를 파악해야한다. 공유기 관리자 페이지(?)에서 확인할 수도 있고, <code>ifconfig</code> 를 통해 확인할 수도 있다.</p>
<p><img decoding="async" loading="lazy" alt="ifconfig.png" src="https://watanka.github.io/assets/images/ifconfig-c9d11b943c262af7ff991acf1d235f11.png" width="1218" height="154" class="img_ev3q"></p>
<br>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="dhcp-서버에서-고정-ip-할당받기">DHCP 서버에서 고정 IP 할당받기<a href="https://watanka.github.io/personal_server#dhcp-%EC%84%9C%EB%B2%84%EC%97%90%EC%84%9C-%EA%B3%A0%EC%A0%95-ip-%ED%95%A0%EB%8B%B9%EB%B0%9B%EA%B8%B0" class="hash-link" aria-label="DHCP 서버에서 고정 IP 할당받기에 대한 직접 링크" title="DHCP 서버에서 고정 IP 할당받기에 대한 직접 링크">​</a></h3>
<p>지금 맥북 주소는 공유기의 DHCP 서버로부터 할당받은 동적 IP이기 때문에, 공유기 관리자 페이지에서 DHCP 서버가 이 맥북에게 고정 IP를 제공할 수 있도록 설정해줘야 한다.</p>
<p><img decoding="async" loading="lazy" alt="dhcp_static_ip.png" src="https://watanka.github.io/assets/images/dhcp_static_ip-bc67a159869e32690a19b0fbf4d852c4.png" width="610" height="216" class="img_ev3q"></p>
<br>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="포트-포워딩하기">포트 포워딩하기<a href="https://watanka.github.io/personal_server#%ED%8F%AC%ED%8A%B8-%ED%8F%AC%EC%9B%8C%EB%94%A9%ED%95%98%EA%B8%B0" class="hash-link" aria-label="포트 포워딩하기에 대한 직접 링크" title="포트 포워딩하기에 대한 직접 링크">​</a></h3>
<p><img decoding="async" loading="lazy" alt="port_forwarding.png" src="https://watanka.github.io/assets/images/port_forwarding-2a4e2cb8d4896a2b93f5f967c4a24fed.png" width="661" height="157" class="img_ev3q"></p>
<p>맥북에서 실행되는 앱을 외부에서 접근할 수 있도록 하려면, 포트 포워딩이 필요하다.</p>
<p>실제로 앱이 접속가능한지 테스트해보기 위해 간단한 fastapi 앱을 작성해준다.</p>
<p><img decoding="async" loading="lazy" alt="simple_app.png" src="https://watanka.github.io/assets/images/simple_app-b47b026a709d9217f520b01823cf921e.png" width="1226" height="708" class="img_ev3q"></p>
<p>그리고, 공유기 관리자 페이지에서 외부에서 해당 포트로 접속했을 경우 맥북 IP의 로컬 포트로 연결받을 수 있도록 설정을 추가해준다.</p>
<p><img decoding="async" loading="lazy" alt="port_forwarding_page.png" src="data:image/png;base64,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" width="769" height="112" class="img_ev3q"></p>
<p>그럼 이제 외부 IP의 8000번 포트로 접속이 성사되는 걸 확인할 수 있다.</p>
<p><img decoding="async" loading="lazy" alt="ip_sample_success.png" src="https://watanka.github.io/assets/images/ip_sample_success-a1ad123d0d066187597505d976480ef7.png" width="596" height="1170" class="img_ev3q"></p>
<br>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="ddns-등록하기">dDNS 등록하기<a href="https://watanka.github.io/personal_server#ddns-%EB%93%B1%EB%A1%9D%ED%95%98%EA%B8%B0" class="hash-link" aria-label="dDNS 등록하기에 대한 직접 링크" title="dDNS 등록하기에 대한 직접 링크">​</a></h3>
<p>마지막으로 한가지 문제가 남았다.</p>
<p>맥북은 와이파이를 통해 연결이 되고, 가정용 인터넷 서비스는 대부분의 경우 동적 IP를 할당한다.(고정 IP를 할당받기 위해서는 추가적인 비용이 필요하다.) 왜 동적 IP를 할당하냐면, 인터넷 서비스 업체들(ISP)이 수익을 최대화하기 위해서라고 할 수 있다. 접속을 끊은 사용자들의 IP는 거둬들이고, 접속한 사용자들에게 다시 할당하는 방식으로 한정된 IP주소들을 최대한 활용하는 방법이다.</p>
<p>외부에서 계속해서 바뀌는 동적 IP 주소를 접근하도록 하기 위해서는 동적 DNS가 필요하다. DNS(Domain Name System)는 서버의 이름과 IP를 매칭해서 관리하는 시스템이라고 보면된다. 서버의 이름을 설정해놓고, 서버에 DNS가 제공하는 소프트웨어를 설치해놓으면 지속적으로 변경되는 IP를 DNS에서 추적해서 업데이트해준다. (무료로 DNS를 활용할 수 있는 <a href="https://freedns.afraid.org/" target="_blank" rel="noopener noreferrer">사이트</a>들이 있다.)</p>
<p>나는 noip.com이라는 사이트를 이용했다. 내 공인 IP와 hostname을 매칭해서 설정해준다.</p>
<p><img decoding="async" loading="lazy" alt="noip_ddns.png" src="https://watanka.github.io/assets/images/noip_ddns-f932d95f9faf84dcbaae49792777a8d4.png" width="3305" height="219" class="img_ev3q"></p>
<p>이제 dDNS에서 할당받은 hostname으로 맥북에 실행시켜둔 앱이 정상적으로 동작하는 걸 확인할 수 있다.</p>
<p><img decoding="async" loading="lazy" alt="ddns_sample_success.png" src="https://watanka.github.io/assets/images/ddns_sample_success-17b8363e2be59f837c53db13021c9319.png" width="585" height="1191" class="img_ev3q"></p>
<br>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="마무리">마무리<a href="https://watanka.github.io/personal_server#%EB%A7%88%EB%AC%B4%EB%A6%AC" class="hash-link" aria-label="마무리에 대한 직접 링크" title="마무리에 대한 직접 링크">​</a></h3>
<p>이렇게 외부에서 접근가능한 개인서버의 네트워크 설정이 완료되었다. 이 맥북 프로를 팔았을 때 중고가격(서버로 운용한다면 사용가능한 수명이 확 떨어지지 않을까…), 24시간 운용했을 때 발생하는 전기비 등 클라우드 서비스 비용과 비교해봤을 때 개인 서버 운용이 과연 경제적인가 하는 질문에 대해서는 좀 더 지켜봐야겠지만, 서버를 구성하면서 나름 네트워크 공부도 하고 나만의 서버를 구성한다는 것 자체가 즐거웠다.</p>
<p>정상적으로 동작하긴 하지만, 아직 추가할 부분들이 많다. 도메인 구매, SSL설정, 도커 레지스트리 구성, CI/CD 설정 그리고 쿠버네티스 환경 구성, 모니터링, 백업 설정 등 앱 올려보면서 진행해보려고 한다. 다음 글은 <a href="https://watanka.github/io/personal_server2" target="_blank" rel="noopener noreferrer">여기</a>에서 확인 가능하다!</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[메이플랜드의 시세는 어떻게 결정되는 걸까🍁]]></title>
            <link>https://watanka.github.io/maple_price</link>
            <guid>https://watanka.github.io/maple_price</guid>
            <pubDate>Sun, 02 Feb 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[이번 글은 호기심을 데이터로 구현하는 월간 데이터노트 1회차를 참여하면서 작성한 내용을 담았다.]]></description>
            <content:encoded><![CDATA[<p><strong>이번 글은 호기심을 데이터로 구현하는 <strong>월간 데이터노트</strong> 1회차를 참여하면서 작성한 내용을 담았다.</strong></p>
<br>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="내가-월간-데이터노트에-참여하게-된-계기">내가 월간 데이터노트에 참여하게 된 계기<a href="https://watanka.github.io/maple_price#%EB%82%B4%EA%B0%80-%EC%9B%94%EA%B0%84-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%85%B8%ED%8A%B8%EC%97%90-%EC%B0%B8%EC%97%AC%ED%95%98%EA%B2%8C-%EB%90%9C-%EA%B3%84%EA%B8%B0" class="hash-link" aria-label="내가 월간 데이터노트에 참여하게 된 계기에 대한 직접 링크" title="내가 월간 데이터노트에 참여하게 된 계기에 대한 직접 링크">​</a></h3>
<p>참여중인 개발자 글쓰기 모임(글또)에서 <a href="https://github.com/MonthlyDataNote/MonthlyReport" target="_blank" rel="noopener noreferrer">월간 데이터노트</a> 홍보글을 봤다.</p>
<p>월간 데이터노트는 호기심을 데이터로 구현하여 1-2장의 결과물을 기록하는 모임이다. 예를 들어, 계절별 과일 가격은 추석이나 설날 같은 명절에 어떻게 변동하는지 추적하거나, 한강의 노벨문학상 수상을 통해 종이책 구매량은 어떻게 변했는지와 같은 일상 주제들을 데이터로써 분석해볼 수 있다.</p>
<p>예전부터 자주 이런 일상 주제들에 대해서 머릿속으로 나름의 가설을 세우고, 증명하기 위해서는 어떤 데이터들이 필요할지 상상해보곤 했다. 예를 들어, 퇴근시간과 같이 교통량이 몰리는 시기에 신호 패턴이 어떤 식으로 바뀌는지, 그에 따른 영향은 어떤 식으로 수치화할 수 있는지와 같은 상상.. 그래서 이런 상상을 직접 데이터를 수집해보고 결과물을 뽑아보는 실천으로 옮겨보면 재밌을 거라는 생각에 참여하게 되었다.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="분석-주제-및-목적">분석 주제 및 목적<a href="https://watanka.github.io/maple_price#%EB%B6%84%EC%84%9D-%EC%A3%BC%EC%A0%9C-%EF%BF%BD%EF%BF%BD%EB%B0%8F-%EB%AA%A9%EC%A0%81" class="hash-link" aria-label="분석 주제 및 목적에 대한 직접 링크" title="분석 주제 및 목적에 대한 직접 링크">​</a></h3>
<ul>
<li>
<p>초등학교 시절 자주 하던 메이플스토리. 그 시절 메이플스토리가 최근에 다시 ‘메이플랜드’ 라는 이름으로 런칭되었다. 반가운 마음에 유튜브에 뜬 메이플스토리 플레이 영상을 보다보면 아련한 옛 추억에 잠기곤 한다.</p>
</li>
<li>
<p>찾아보니 게임머니를 현금처럼 사용하고, 아이템을 구매하기 위해 적지 않은 금액을 기꺼이 지불하는 걸 알게 되었다.</p>
</li>
<li>
<p>아이템 매니아(25.01.09) 기준 현재 100만메소당 5000-9000원 정도의 가격에 거래되는 것을 확인할 수 있다. 정말 비싼 아이템은 몇백만원에 거래되기도 한다.</p>
</li>
<li>
<p>mapleland.gg같은 사이트에 팝니다, 삽니다 정보를 알아보기 쉽게 정리되어있다.</p>
<p><img decoding="async" loading="lazy" alt="레드크리븐.png" src="https://watanka.github.io/assets/images/%EB%A0%88%EB%93%9C%ED%81%AC%EB%A6%AC%EB%B8%90-c720619aac4e5027d58be842b4cd6967.png" width="408" height="482" class="img_ev3q"></p>
</li>
</ul>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="분석-결과">분석 결과<a href="https://watanka.github.io/maple_price#%EB%B6%84%EC%84%9D-%EA%B2%B0%EA%B3%BC" class="hash-link" aria-label="분석 결과에 대한 직접 링크" title="분석 결과에 대한 직접 링크">​</a></h3>
<p><strong>“아이템 시세에 영향을 미치는 요소에는 뭐가 있을까?”</strong></p>
<ul>
<li>
<p>시세를 어떻게 측정할 수 있을까?</p>
<p>매수희망 가격을 내림차순, 매도희망 가격을 오름차순으로 정렬함.</p>
<p><strong>파는 사람은 최대한 비싸게, 사는 사람은 최대한 싸게</strong></p>
<p>순서대로 매수/매도 금액을 매칭하여 거래가 성사될 수 있는 건들에 대한 평균값을 시세로 측정.</p>
<p><img decoding="async" loading="lazy" alt="노작기준_매수_매도가격_비교.png" src="https://watanka.github.io/assets/images/%EB%85%B8%EC%9E%91%EA%B8%B0%EC%A4%80_%EB%A7%A4%EC%88%98_%EB%A7%A4%EF%BF%BD%EB%8F%84%EA%B0%80%EA%B2%A9_%EB%B9%84%EA%B5%90-2ae764c9419e95215af8bbac63d8067a.png" width="1189" height="589" class="img_ev3q"></p>
<p>업그레이드 없는 순정 레드 크리븐을 기준으로 <strong>시세는 2억 4889만 메소</strong>로 형성 되어있음.</p>
<p>*발표 후 질문 중에 ‘실제 성사된 거래 내역을 확인해볼 수도 있을까요?’라는 질문에 깨닫게 되었는데, 시세를 확인하기 위해 먼저 고려해볼 부분은 실제 거래가 성사된 내역이라는 걸 간과하고 있었다.</p>
</li>
<li>
<p>어떤 아이템의 시세를 살펴보면 좋을까?</p>
<p>현시점(2025-01-25) 직업군 중 가장 쎈 도적의 엔드 아이템(가장 좋은 아이템) <strong>레드 크리븐</strong></p>
<p>옵션이 좋은 매물을 기준으로 5억 메소에 매수가가 형성되어 있음. 5억 메소는 자그마치 <strong>현금 450만원</strong>(100만 메소=9000원 가정)</p>
<p><img decoding="async" loading="lazy" alt="레드크리븐.png" src="https://watanka.github.io/assets/images/%EB%A0%88%EB%93%9C%ED%81%AC%EB%A6%AC%EB%B8%90-c720619aac4e5027d58be842b4cd6967.png" width="408" height="482" class="img_ev3q"></p>
</li>
<li>
<p>아이템 스탯별 시세의 상승률을 시각화해보자</p>
<p><img decoding="async" loading="lazy" alt="주문서_종류.png" src="https://watanka.github.io/assets/images/%EC%A3%BC%EB%AC%B8%EC%84%9C_%EC%A2%85%EB%A5%98-b9e902f53c582385721a611cdd783317.png" width="1260" height="397" class="img_ev3q"></p>
<ul>
<li>
<p>메이플랜드에는 “주문서” 시스템이 있음. 주문서를 사용해서 아이템의 스탯을 업그레이드할 수 있는 시스템임</p>
</li>
<li>
<p>아이템에는 보통 업그레이드횟수가 7번 정도 주어짐.</p>
</li>
<li>
<p>주문서는 성공 확률에 따라 10%, 60%, 100%로 나뉘는데, 성공확률이 낮을수록 업그레이드할 수 있는 스탯 비중이 높아짐.</p>
</li>
<li>
<p>공격력별 성공확률 시각화</p>
<p><img decoding="async" loading="lazy" alt="주문서_성공확률.png" src="https://watanka.github.io/assets/images/%EC%A3%BC%EB%AC%B8%EC%84%9C_%EC%84%B1%EA%B3%B5%ED%99%95%EB%A5%A0-0f32d92c5d1a73b414540c028fb477e2.png" width="1489" height="790" class="img_ev3q"></p>
</li>
<li>
<p>공격력별로 매수/매도 데이터를 그룹화한 후, 시세를 구한다.</p>
</li>
</ul>
<p><img decoding="async" loading="lazy" alt="공격력별_시세비교.png" src="https://watanka.github.io/assets/images/%EA%B3%B5%EA%B2%A9%EB%A0%A5%EB%B3%84_%EC%8B%9C%EC%84%B8%EB%B9%84%EA%B5%90-b42bbfaf8f8fc8c19412e5b707d57b74.png" width="1489" height="789" class="img_ev3q"></p>
<div class="language-markdown codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-markdown codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">=== 시세 분석 ===</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">공격력과 시세의 상관계수: 0.776</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">매도 있는 최대 공격력: 12</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">매수 있는 최대 공격력: 18</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">=== 공격력 구간별 가격 상승률 ===</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">공격력 3 → 4: 0.0%</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">공격력 4 → 5: 122.2%</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">공격력 5 → 6: 80.0%</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">공격력 6 → 7: 144.4%</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">공격력 7 → 8: 11.4%</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">공격력 8 → 90: 69.4%</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">공격력 9 → 10: 137.3%</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">공격력 10 → 11: 39.1%</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">공격력 11 → 12: 9.5%</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">공격력 12 → 13: 216.7%</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">공격력 13 → 14: 47.4%</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">공격력 14 → 15: 42.9%</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">공격력 15 → 16: 6.1%</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">공격력 16 → 17: 88.6%</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">공격력 17 → 18: 75.0%</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="클립보드에 코드 복사" title="복사" class="clean-btn"><span class="copyButtonIcons_eSgA" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_y97N"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_LjdS"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
</li>
</ul>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="진행하면서-배우게-된-것-더-보충할-수-있을-것-같은-부분">진행하면서 배우게 된 것/ 더 보충할 수 있을 것 같은 부분<a href="https://watanka.github.io/maple_price#%EC%A7%84%ED%96%89%ED%95%98%EB%A9%B4%EC%84%9C-%EB%B0%B0%EC%9A%B0%EA%B2%8C-%EB%90%9C-%EA%B2%83-%EB%8D%94-%EB%B3%B4%EC%B6%A9%ED%95%A0-%EC%88%98-%EC%9E%88%EC%9D%84-%EA%B2%83-%EA%B0%99%EC%9D%80-%EB%B6%80%EB%B6%84" class="hash-link" aria-label="진행하면서 배우게 된 것/ 더 보충할 수 있을 것 같은 부분에 대한 직접 링크" title="진행하면서 배우게 된 것/ 더 보충할 수 있을 것 같은 부분에 대한 직접 링크">​</a></h3>
<ul>
<li>분석면에 있어서 한 문장으로 정의내릴 수 있는 명확한 결론은 나지 않은 것 같다.</li>
<li>시세를 어떻게 정의내릴지에 대해 알아본 것은 유용했다.</li>
<li>업그레이드 수준에 따른 시세 변화 같은 경우에 결론은 결국 ‘업그레이드가 잘 될 수록 시세는 비싸진다’ 정도에서 그친 것 같아서 좀 아쉽다. 업그레이드 횟수와 성공확률에 따라 ‘기회비용’이 얼마정도 되는지(과연 추가로 주문서 작을 하는 게 나을지에 대한 판단을 위한 수치)와 같은 분석을 추가로 진행하면 좋을 것 같다는 생각이 들었다.</li>
<li>한 아이템에 대해서만 분석을 진행했는데, 크롤링 범위를 전체 아이템 또는 API 형식으로 확장하고 범용적으로 분석이 가능한 대쉬보드를 작성한다면 좀 더 유용한 결과물이 될 수 있을 것 같다는 생각이 들었다.</li>
</ul>
<br>
<p><strong>데이터출처 &amp; 코드</strong></p>
<ul>
<li><a href="http://mapleland.gg/" target="_blank" rel="noopener noreferrer">mapleland.gg</a> - 셀레니움으로 크롤링(코드는 chatGPT로 작성)</li>
</ul>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Django Signal 쉽게 이해하기⚡️]]></title>
            <link>https://watanka.github.io/django-signal-쉽게-이해하기</link>
            <guid>https://watanka.github.io/django-signal-쉽게-이해하기</guid>
            <pubDate>Sun, 05 Jan 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[Intro]]></description>
            <content:encoded><![CDATA[<h3 class="anchor anchorWithStickyNavbar_LWe7" id="intro">Intro<a href="https://watanka.github.io/django-signal-%EC%89%BD%EA%B2%8C-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0#intro" class="hash-link" aria-label="Intro에 대한 직접 링크" title="Intro에 대한 직접 링크">​</a></h3>
<p>django를 학습 중이다. django에서는 웹 서비스 개발에 필요한 편리한 도구들을 제공한다. 그 중 Signal은 이벤트 기반 아키텍쳐에서 강력한 도구로 활용될 수 있다.
이번 글에서는 <strong>Django Signal이란 무엇인지</strong>, <strong>어떻게 동작하는지</strong>, 그리고 <strong>어떤 상황에서 Signal을 사용하는 것이 적절한지</strong>에 대해 심층적으로 알아보려고 한다.</p>
<br>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="signal이-뭔가요">Signal이 뭔가요?<a href="https://watanka.github.io/django-signal-%EC%89%BD%EA%B2%8C-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0#signal%EC%9D%B4-%EB%AD%94%EA%B0%80%EC%9A%94" class="hash-link" aria-label="Signal이 뭔가요?에 대한 직접 링크" title="Signal이 뭔가요?에 대한 직접 링크">​</a></h3>
<p>signal은 django 시스템 실행 중 이벤트가 발생했을 때 실행되는 콜백 메커니즘이다. 이 시그널은 이벤트를 알리는 publisher와 알림을 받는 subscriber로 이루어진 pub/sub 구조를 통해 동작한다. publisher가 이벤트의 특정 동작 시점을 알리면, 대기하고 있던 subscriber는 그에 맞춰서 필요한 동작들을 수행한다. 이 pub/sub 구조의 장점은 <strong>느슨한 결합을 유지</strong>할 수 있다는 점인데, 예를 통해 어떤 의미인지 살펴보자.</p>
<br>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="pubsub-구조">Pub/Sub 구조<a href="https://watanka.github.io/django-signal-%EC%89%BD%EA%B2%8C-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0#pubsub-%EA%B5%AC%EC%A1%B0" class="hash-link" aria-label="Pub/Sub 구조에 대한 직접 링크" title="Pub/Sub 구조에 대한 직접 링크">​</a></h3>
<p><img decoding="async" loading="lazy" alt="police" src="https://watanka.github.io/assets/images/police-1e823ea40cd203f42a58bd3df59417be.png" width="512" height="512" class="img_ev3q"></p>
<p>신호등이 없는 나라에 새로 부임받은 교통경찰이 있다. 이 교통경찰은 차량들이 지체없이 통행할 수 있도록 해야한다. 이 교통경찰은 지나가는 차량들을 일일이 붙잡고 멈춰야할 시점과 출발해야하는 시점을 알려준다. 열심히 뛰어다녔지만 혼자서는 버겁다. 그리고 간혹 말이 통하지 않는 외국인이 있는 경우에는 정보 전달을 위해 더 많은 시간이 걸렸다.  차량 퇴근시간이 되어 차량이 많아지자, 교통경찰 혼자서 차량들을 통제하는 일은 점점 더 버거워졌고, 결국은 3중 추돌 사고가 나고 말았다.</p>
<p>이렇게는 안 되겠다 싶었던 경찰은 신호등을 설치하기로 마음먹는다. 빨강, 노랑, 초록 싸인에 대한 설명 글귀를 큼지막하게 적어 운전자들이 확인할 수 있도록 하고, 차량들이 신호등의 신호에 따를 수 있도록 통제했다. 색깔 사인은 언어가 달라도 이해할 수 있었기 때문에 외국인도 별 무리 없이 운전이 가능했다. 만약 신호등 통제를 따르지 않는다면, 벌금을 부과하도록 단속했다. 이제 모든 차량들이 신호등의 신호에 집중해서 악셀을 밟을지 브레이크를 밟을지 결정했고, 교통 경찰은 더 이상 차량들을 하나하나 통제하지 않아도 됐다.</p>
<p><img decoding="async" loading="lazy" alt="pubsub" src="https://watanka.github.io/assets/images/pubsub-503c853e4a7f441774349c819be269d3.png" width="1091" height="354" class="img_ev3q"></p>
<p>이 스토리에서 교통경찰이 일일이 차량을 통제하는 건 강한 결합, 그리고 신호등을 통해 통제하는 건 느슨한 결합이라고 볼 수 있다. SRP 원칙에 따라 한 앱(모델 또는 앱)은 자신이 맡은 역할만 수행하는 게 기본적이지만, 로직상 다른 앱의 처리과정과 연관되어 있을 경우도 자주 발생한다. (예를 들어 온라인 쇼핑몰에서 상품 결제 처리 시 재고 처리, 결제 처리, 알림 처리 등이 동작해야한다.)</p>
<p>pub/sub 구조는 로직을 같은 컨텍스트 안에 작성하여 강하게 결합된 코드를 만드는 대신, ‘분리되어야하는 동작을 발동시키는 로직’을 ‘이벤트’로 정의하고 큐에 넣어서 처리한다. 이렇게 하면, 모델은 본 컨텍스트에 관련된 로직만 처리하고, 신호등과 같은 역할인 큐에서 컨슈머들(차량)이 알아서 가져가서 처리할 수 있도록 할 수 있다. 퍼블리셔는 자신의 행동 외에 컨슈머가 어떤 행동을 할지에 대해서 모르고, 컨슈머도 발생한 이벤트에 맞춰 자신이 어떤 행동을 해야할지만 알 뿐이다.</p>
<br>
<p>정리하자면, signal을 사용함으로써 얻을 수 있는 이점은</p>
<p><em>“독립적인 컴포넌트들이 독립적으로 구분되어 서로를 모르고 동작할 수 있도록 한다.”</em></p>
<br>
<p>한 가지 주의할 점은 일반적인 pub/sub 구조는 publisher와 subscriber 가운데에 queue 또는 broker를 두어 느슨한 결합을 이루지만, django signal 같은 경우 <strong>따로 queue없이 publisher, 즉 signal 자체가 브로커 역할을 한다는 점</strong>이다.</p>
<p>django에서 signal을 사용하는 대표적인 예는 유저 정보를 처리할 때다. django에서 제공하는 User 모델로는 충분한 정보를 담지 못 하기 때문에 일대일 대응되는 UserProfile 같은 모델로 추가 정보를 담는 경우가 일반적인데, User 생성시에 UserProfile도 함께 생성될 수 있도록, User 생성 이벤트에 UserProfile이 생성되는 시그널을 만들어 처리한다.</p>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token decorator annotation punctuation" style="color:#393A34">@receiver</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">post_save</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> sender</span><span class="token operator" style="color:#393A34">=</span><span class="token plain">User</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">def</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">create_user_profile</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">sender</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> instance</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> created</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">**</span><span class="token plain">kwargs</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">	</span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> created</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">		UserProfile</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">objects</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">create</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">user</span><span class="token operator" style="color:#393A34">=</span><span class="token plain">instance</span><span class="token punctuation" style="color:#393A34">)</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="클립보드에 코드 복사" title="복사" class="clean-btn"><span class="copyButtonIcons_eSgA" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_y97N"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_LjdS"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>receiver 데코레이터는 아래 함수가 이벤트 콜백 함수임을 명시하고, 어떤 이벤트에 동작하는지 정의한다. create_user_profile이 <code>User</code> 모델의 <code>post_save</code> 시에 동작하는 콜백함수인 것을 확인할 수 있다.</p>
<br>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="signal-종류">signal 종류<a href="https://watanka.github.io/django-signal-%EC%89%BD%EA%B2%8C-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0#signal-%EC%A2%85%EB%A5%98" class="hash-link" aria-label="signal 종류에 대한 직접 링크" title="signal 종류에 대한 직접 링크">​</a></h3>
<p>이벤트 종류에 따라 시그널 종류가 나뉜다.</p>
<p>크게 ORM관련, Request와 Response 사이클 관련, Authentication 관련, 그리고 데이터베이스 연결, 테스트, 매니지먼트(manage.py 실행 관련), Admin관련, 미들웨어 관련 등으로 나눌 수 있다. 다양한 종류들이 존재하지만, ORM, Request/Response, 그리고 Authentication 관련 시그널이 많이 쓰인다고 볼 수 있다. pre, post와 같이 세부적으로 이벤트 시점 조정이 가능하고, 필요하다면 Custom Signal을 만들어 특정 이벤트에 동작할 수 있도록 구성할 수도 있다.</p>
<p>아래 표를 통해 어떤 signal 종류가 있는지 알아보자.</p>
<br>
<p><strong>ORM 관련</strong></p>
<table><thead><tr><th><strong>Signal</strong></th><th><strong>설명</strong></th></tr></thead><tbody><tr><td><strong><code>pre_init</code></strong></td><td>모델 인스턴스가 초기화되기 전에 발생</td></tr><tr><td><strong><code>post_init</code></strong></td><td>모델 인스턴스가 초기화된 후에 발생</td></tr><tr><td><strong><code>pre_save</code></strong></td><td>모델 인스턴스가 저장되기 전에 발생</td></tr><tr><td><strong><code>post_save</code></strong></td><td>모델 인스턴스가 저장된 후에 발생</td></tr><tr><td><strong><code>pre_delete</code></strong></td><td>모델 인스턴스가 삭제되기 전에 발생</td></tr><tr><td><strong><code>post_delete</code></strong></td><td>모델 인스턴스가 삭제된 후에 발생</td></tr><tr><td><strong><code>m2m_changed</code></strong></td><td><code>ManyToManyField</code>관계가 변경될 때 발생</td></tr><tr><td><strong><code>pre_migrate</code></strong></td><td>마이그레이션이 실행되기 전에 발생</td></tr><tr><td><strong><code>post_migrate</code></strong></td><td>마이그레이션이 실행된 후에 발생</td></tr></tbody></table>
<p><strong>Request/Response 관련</strong></p>
<table><thead><tr><th><strong>Signal</strong></th><th><strong>설명</strong></th></tr></thead><tbody><tr><td><strong><code>request_started</code></strong></td><td>HTTP 요청이 시작될 때 발생</td></tr><tr><td><strong><code>request_finished</code></strong></td><td>HTTP 요청이 끝날 때 발생합</td></tr><tr><td><strong><code>got_request_exception</code></strong></td><td>예외가 발생했을 때 발생</td></tr><tr><td><strong><code>setting_changed</code></strong></td><td>Django 설정(<code>settings.py</code>)이 변경될 때 발생</td></tr></tbody></table>
<p><strong>Authentication 관련</strong></p>
<table><thead><tr><th><strong>Signal</strong></th><th><strong>설명</strong></th></tr></thead><tbody><tr><td><strong><code>user_logged_in</code></strong></td><td>사용자가 로그인할 때 발생합니다.</td></tr><tr><td><strong><code>user_logged_out</code></strong></td><td>사용자가 로그아웃할 때 발생합니다.</td></tr><tr><td><strong><code>user_login_failed</code></strong></td><td>로그인에 실패했을 때 발생합니다.</td></tr><tr><td><strong><code>password_changed</code></strong></td><td>사용자의 비밀번호가 변경될 때 발생합니다.</td></tr><tr><td><strong><code>password_reset</code></strong></td><td>사용자의 비밀번호가 재설정될 때 발생합니다.</td></tr></tbody></table>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="어떤-식으로-사용할-수-있나요">어떤 식으로 사용할 수 있나요?<a href="https://watanka.github.io/django-signal-%EC%89%BD%EA%B2%8C-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0#%EC%96%B4%EB%96%A4-%EC%8B%9D%EC%9C%BC%EB%A1%9C-%EC%82%AC%EC%9A%A9%ED%95%A0-%EC%88%98-%EC%9E%88%EB%82%98%EC%9A%94" class="hash-link" aria-label="어떤 식으로 사용할 수 있나요?에 대한 직접 링크" title="어떤 식으로 사용할 수 있나요?에 대한 직접 링크">​</a></h3>
<ol>
<li><strong>Decorator 방식 (@receiver 사용)</strong></li>
</ol>
<p>가장 일반적이고 권장되는 방식이다.</p>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic"># signals.py</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">from</span><span class="token plain"> django</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">db</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">models</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">signals </span><span class="token keyword" style="color:#00009f">import</span><span class="token plain"> post_save</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">from</span><span class="token plain"> django</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">dispatch </span><span class="token keyword" style="color:#00009f">import</span><span class="token plain"> receiver</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">from</span><span class="token plain"> django</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">contrib</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">auth</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">models </span><span class="token keyword" style="color:#00009f">import</span><span class="token plain"> User</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token decorator annotation punctuation" style="color:#393A34">@receiver</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">post_save</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> sender</span><span class="token operator" style="color:#393A34">=</span><span class="token plain">User</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">def</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">user_created_signal</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">sender</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> instance</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> created</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">**</span><span class="token plain">kwargs</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> created</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">print</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string-interpolation string" style="color:#e3116c">f'New user created: </span><span class="token string-interpolation interpolation punctuation" style="color:#393A34">{</span><span class="token string-interpolation interpolation">instance</span><span class="token string-interpolation interpolation punctuation" style="color:#393A34">.</span><span class="token string-interpolation interpolation">username</span><span class="token string-interpolation interpolation punctuation" style="color:#393A34">}</span><span class="token string-interpolation string" style="color:#e3116c">'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="클립보드에 코드 복사" title="복사" class="clean-btn"><span class="copyButtonIcons_eSgA" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_y97N"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_LjdS"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<ol>
<li><strong><code>connect</code> 메서드 사용하기</strong></li>
</ol>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">python</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># signals.py</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">from</span><span class="token plain"> django</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">db</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">models</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">signals </span><span class="token keyword" style="color:#00009f">import</span><span class="token plain"> post_save</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">from</span><span class="token plain"> django</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">contrib</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">auth</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">models </span><span class="token keyword" style="color:#00009f">import</span><span class="token plain"> User</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">def</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">user_created_signal</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">sender</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> instance</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> created</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">**</span><span class="token plain">kwargs</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> created</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">print</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string-interpolation string" style="color:#e3116c">f'New user created: </span><span class="token string-interpolation interpolation punctuation" style="color:#393A34">{</span><span class="token string-interpolation interpolation">instance</span><span class="token string-interpolation interpolation punctuation" style="color:#393A34">.</span><span class="token string-interpolation interpolation">username</span><span class="token string-interpolation interpolation punctuation" style="color:#393A34">}</span><span class="token string-interpolation string" style="color:#e3116c">'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">post_save</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">connect</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">user_created_signal</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> sender</span><span class="token operator" style="color:#393A34">=</span><span class="token plain">User</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="클립보드에 코드 복사" title="복사" class="clean-btn"><span class="copyButtonIcons_eSgA" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_y97N"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_LjdS"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p><strong>차이점:</strong></p>
<ul>
<li><code>connect</code>는 동적으로 Signal을 연결할 수 있다.</li>
</ul>
<p><strong>c. Custom Signal 만들기</strong></p>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">python</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">코드 복사</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># signals.py</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">from</span><span class="token plain"> django</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">dispatch </span><span class="token keyword" style="color:#00009f">import</span><span class="token plain"> Signal</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># Signal 정의</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">order_completed </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> Signal</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># Signal 수신기</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">def</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">notify_order_completed</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">sender</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">**</span><span class="token plain">kwargs</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">print</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string-interpolation string" style="color:#e3116c">f"Order completed for: </span><span class="token string-interpolation interpolation punctuation" style="color:#393A34">{</span><span class="token string-interpolation interpolation">kwargs</span><span class="token string-interpolation interpolation punctuation" style="color:#393A34">[</span><span class="token string-interpolation interpolation string" style="color:#e3116c">'user'</span><span class="token string-interpolation interpolation punctuation" style="color:#393A34">]</span><span class="token string-interpolation interpolation punctuation" style="color:#393A34">}</span><span class="token string-interpolation string" style="color:#e3116c">"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># Signal 연결</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">order_completed</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">connect</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">notify_order_completed</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># Signal 발행</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">order_completed</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">send</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">sender</span><span class="token operator" style="color:#393A34">=</span><span class="token boolean" style="color:#36acaa">None</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> user</span><span class="token operator" style="color:#393A34">=</span><span class="token string" style="color:#e3116c">'John Doe'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="클립보드에 코드 복사" title="복사" class="clean-btn"><span class="copyButtonIcons_eSgA" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_y97N"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_LjdS"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>그럼 signal은 디렉터리 구조 중 어디에 위치하는 게 좋을까?</p>
<p>일반적으로 다음과 같은 구조를 사용한다.</p>
<div class="language-lua codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-lua codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">myapp/</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">├── __init__.py</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">├── models.py</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">├── views.py</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">├── signals.py   &lt;-- Signal 정의</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">├── apps.py      &lt;-- Signal 등록</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">└── admin.py</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="클립보드에 코드 복사" title="복사" class="clean-btn"><span class="copyButtonIcons_eSgA" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_y97N"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_LjdS"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>(apps.py에 시그널을 등록해야만 활성화된다는 점도 잊지말자)</p>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic"># apps.py</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">from</span><span class="token plain"> django</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">apps </span><span class="token keyword" style="color:#00009f">import</span><span class="token plain"> AppConfig</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">class</span><span class="token plain"> </span><span class="token class-name">MyAppConfig</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">AppConfig</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    default_auto_field </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'django.db.models.BigAutoField'</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    name </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'myapp'</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">def</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">ready</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">self</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">import</span><span class="token plain"> myapp</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">signals  </span><span class="token comment" style="color:#999988;font-style:italic"># Signal import</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="클립보드에 코드 복사" title="복사" class="clean-btn"><span class="copyButtonIcons_eSgA" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_y97N"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_LjdS"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<br>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="어떻게-동작하나요">어떻게 동작하나요?<a href="https://watanka.github.io/django-signal-%EC%89%BD%EA%B2%8C-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0#%EC%96%B4%EB%96%BB%EA%B2%8C-%EB%8F%99%EC%9E%91%ED%95%98%EB%82%98%EC%9A%94" class="hash-link" aria-label="어떻게 동작하나요?에 대한 직접 링크" title="어떻게 동작하나요?에 대한 직접 링크">​</a></h3>
<p>signal의 동작 방식은 생각보다 단순하다. <code>django.dispatch.dispatcher.py</code>의 Signal의 <code>connect</code> 함수를 통해 확인 할 수 있다.</p>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">with</span><span class="token plain"> self</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">lock</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    self</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">_clear_dead_receivers</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">not</span><span class="token plain"> </span><span class="token builtin">any</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">r_key </span><span class="token operator" style="color:#393A34">==</span><span class="token plain"> lookup_key </span><span class="token keyword" style="color:#00009f">for</span><span class="token plain"> r_key</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> _</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> _ </span><span class="token keyword" style="color:#00009f">in</span><span class="token plain"> self</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">receivers</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        self</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">receivers</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">append</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">lookup_key</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> receiver</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> is_async</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    self</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">sender_receivers_cache</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">clear</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="클립보드에 코드 복사" title="복사" class="clean-btn"><span class="copyButtonIcons_eSgA" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_y97N"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_LjdS"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>lock이 걸린 상태에서 <code>connect</code>함수가 호출될 경우, Signal 인스턴스에 정의된 receivers를 순회하며 해당 signal에 대해 어떻게 반응해야할지 찾는다. 그리고 기본값으로 설정되어 있는 <code>weak=True</code>는 receiver가 호출 후, garbage collector에 의해 수거되어 메모리를 낭비하지 않아도 될 수 있도록 약한 참조를 할 수 있게 돕는다.</p>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> weak</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    ref </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> weakref</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">ref</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    receiver_object </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> receiver</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token comment" style="color:#999988;font-style:italic"># Check for bound methods</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> </span><span class="token builtin">hasattr</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">receiver</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"__self__"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">and</span><span class="token plain"> </span><span class="token builtin">hasattr</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">receiver</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"__func__"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        ref </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> weakref</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">WeakMethod</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        receiver_object </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> receiver</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">__self__</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    receiver </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> ref</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">receiver</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    weakref</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">finalize</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">receiver_object</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> self</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">_remove_receiver</span><span class="token punctuation" style="color:#393A34">)</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="클립보드에 코드 복사" title="복사" class="clean-btn"><span class="copyButtonIcons_eSgA" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_y97N"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_LjdS"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<br>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="어떨-때-사용하면-좋을까요">어떨 때 사용하면 좋을까요?<a href="https://watanka.github.io/django-signal-%EC%89%BD%EA%B2%8C-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0#%EC%96%B4%EB%96%A8-%EB%95%8C-%EC%82%AC%EC%9A%A9%ED%95%98%EB%A9%B4-%EC%A2%8B%EC%9D%84%EA%B9%8C%EC%9A%94" class="hash-link" aria-label="어떨 때 사용하면 좋을까요?에 대한 직접 링크" title="어떨 때 사용하면 좋을까요?에 대한 직접 링크">​</a></h3>
<p>장점과 단점을 알고 상황에 맞게 사용하는 것이 중요하다. 위에서 언급한 것처럼 signal을 사용하면서 얻을 수 있는 장점은 느슨한 결합이다. 책임이 분리되어있기 때문에 코드가 깔끔해지고, 유지보수가 쉬워진다는 장점이 있다. 하지만 물론 단점도 있다. 코드 흐름을 알기 어렵다. two scoops of django 책에는 “그러므로 시그널을 받는 리시버가 어떤 것인지조차 알 필요가 없는 경우라면 그 때 시그널을 활용하라”라는 조언이 있다. 그리고, signal은 “<strong>동기화되고 블로킹</strong>을 일으키는 무거운 프로세스를 호출한다. 확장과 성능 면에서 어떤 장점도 찾아볼 수 없다”라는 단점이 있기도 하다. 그러므로 signal을 과도하게 사용하기보다는 정말 필요한 경우에만 사용하도록 하고, 무거운 작업일 경우, celery를 사용하는 등 비동기 처리로 옮기는 게 더 바람직하다.</p>
<br>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="outro">Outro<a href="https://watanka.github.io/django-signal-%EC%89%BD%EA%B2%8C-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0#outro" class="hash-link" aria-label="Outro에 대한 직접 링크" title="Outro에 대한 직접 링크">​</a></h3>
<p>이번 글에서는 django Signal에 대해 알아보았다. signal이 어떤 장점을 가지고 있는지, 어를 통해 어떤 기능을 구현할 수 있는지, 그리고 어떻게 동작하는지와 언제 사용하면 적합한지에 대해 살펴보았다. 모쪼록 django를 공부하는 분들께 도움이 되었으면 좋겠다.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[오픈소스 첫 발자국 떼기🌱]]></title>
            <link>https://watanka.github.io/open_source_first_step</link>
            <guid>https://watanka.github.io/open_source_first_step</guid>
            <pubDate>Sun, 22 Dec 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[Intro]]></description>
            <content:encoded><![CDATA[<h3 class="anchor anchorWithStickyNavbar_LWe7" id="intro">Intro<a href="https://watanka.github.io/open_source_first_step#intro" class="hash-link" aria-label="Intro에 대한 직접 링크" title="Intro에 대한 직접 링크">​</a></h3>
<p>2024년이 끝나간다. 연초에 이루고 싶은 여러 목표들을 적어두었었는데, 그 중 하나는 '오픈소스 기여하기'였다. 미루고 미루다가 24년이 얼마 안 남은 시점에 그 목표를 시도해보려고 한다. 전에 해본 적이 없으니 어디서부터 시작해야할지 막연한 마음이 컸고, 이런 오픈소스 기여같은 작업은 뭔가 대단한 작업이라 내가 할 수 있을까 하는 생각 때문에 미루게 된 것 같은데, 우선 해보는 게 중요하지 않을까. 이번 글은 그래서 PR을 제출하는 과정에 있어서 어떤 생각의 흐름을 가지고 진행했는지에 대해서 써보았다.</p>
<br>
<br>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="오픈소스란">오픈소스란?<a href="https://watanka.github.io/open_source_first_step#%EC%98%A4%ED%94%88%EC%86%8C%EC%8A%A4%EB%9E%80" class="hash-link" aria-label="오픈소스란?에 대한 직접 링크" title="오픈소스란?에 대한 직접 링크">​</a></h3>
<p>소스 코드가 공개되어 있어&nbsp;누구나 자유롭게 사용, 복사, 수정, 배포할 수 있는 소프트웨어를 말한다. 오픈소스의 장점은 모두에게 수정할 기회가 열려있다보니, 코드에 대해서 활발하게 의견 교류가 이뤄지고, 더 좋은 코드로 발전할 가능성이 높아진다는 점이다. 오픈소스 코드 작성에 기여하면서 얻을 수 있는 점은 다른 개발자들의 코드를 읽고 배우면서 개발 실력을 기를 수 있다는 점이다.</p>
<br>
<br>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="오픈소스-기여-팁">오픈소스 기여 팁<a href="https://watanka.github.io/open_source_first_step#%EC%98%A4%ED%94%88%EC%86%8C%EC%8A%A4-%EA%B8%B0%EC%97%AC-%ED%8C%81" class="hash-link" aria-label="오픈소스 기여 팁에 대한 직접 링크" title="오픈소스 기여 팁에 대�한 직접 링크">​</a></h3>
<p>본격적으로 시작하기 전, 오픈소스 기여 관련 다양한 자료를 얻을 수 있었다. 다음과 같은 조언을 얻었다.</p>
<ul>
<li><strong>어떤 오픈소스를 고를 것인가?</strong>
=&gt; 대단한 게 아니여도 된다. 비교적 코드가 복잡하지 않은 리포지토리부터 시작해보자</li>
<li><strong>시작을 어떻게 하면 좋을까?</strong>
=&gt; 자주 사용하는 라이브러리 또는 프레임워크<br>
<!-- -->=&gt; 리포지토리의 이슈 항목을 살펴보자. 보통 오픈소스는 컨트리뷰트를 어디서부터 시작하면 좋을지 안내하는 good first issue 라벨이 있다.<br>
<!-- -->=&gt; 전체 코드를 한 번에 이해하려 하지 말고, 관심있는 컴포넌트를 하나 선택해서 거기서부터 시작하는 게 좋다.</li>
<li><strong>어떻게 코드를 읽어야할지?</strong>
=&gt; 우선 README부터 읽기<br>
<!-- -->=&gt; 시작 포인트를 찾아서 거기서부터 타고들어갈 것<br>
<!-- -->=&gt; 한줄 한줄 다 읽어볼 것<br>
<!-- -->=&gt; 리포지토리의 TODO 부분을 확인하고 기여할 수 있는 부분을 확인해볼 것<br>
<!-- -->=&gt; 테스트 코드 활용하기<br>
<!-- -->=&gt; 코드를 읽다가 이해가 안되는 부분이 있다면, 관련 PR을 찾아보는 것도 방법<br>
<!-- -->(<a href="https://www.youtube.com/watch?v=gBjWgCXL4mA" target="_blank" rel="noopener noreferrer">배두식님의 PyCon KR 2023 오픈소스와 함께 성장하기</a>를 참고했다.)</li>
</ul>
<br>
<br>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="오픈소스-선정하기">오픈소스 선정하기<a href="https://watanka.github.io/open_source_first_step#%EC%98%A4%ED%94%88%EC%86%8C%EC%8A%A4-%EC%84%A0%EC%A0%95%ED%95%98%EA%B8%B0" class="hash-link" aria-label="오픈소스 선정하기에 대한 직접 링크" title="오픈소스 선정하기에 대한 직접 링크">​</a></h3>
<p>위 팁들을 기반으로 요새 사용중인 웹 프레임워크인 django의 라이브러리 중 <code>django-faker</code>라는 라이브러리에 기여해보기로 마음먹었다.<br>
<code>django-faker</code>는 django model의 값들을 랜덤으로 생성해주는 역할을 한다.<br>
<!-- -->로직이 자료형에 알맞는 랜덤값만 생성하면 된다는 점에서 코드 파악을 위한 피로감이 다른 리포지토리보다 덜 하지 않을까하는 생각이 들었다. 한 가지 문제는 마지막 update가 2016년... 무려 8년전... 거의 업데이트가 더 이상 없는 라이브러리라는 점이였다. 하지만, 목표가 '오픈소스 기여하기'인만큼 이 또한 좋은 경험이 될 거라는 생각이 들었다. PR에 대한 피드백이 늦을 수 있다는 점만 감안하면 될 듯 했다.<br>
<img decoding="async" loading="lazy" alt="django-faker의 커밋 히스토리" src="https://watanka.github.io/assets/images/django-faker-commit-history-531ce23a14a06957c33a7b4999af98aa.png" width="2606" height="1098" class="img_ev3q"></p>
<br>
<br>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="코드-파악하기">코드 파악하기<a href="https://watanka.github.io/open_source_first_step#%EC%BD%94%EB%93%9C-%ED%8C%8C%EC%95%85%ED%95%98%EA%B8%B0" class="hash-link" aria-label="코드 파악하기에 대한 직접 링크" title="코드 파악하기에 대한 직접 링크">​</a></h3>
<p>README.md를 우선 한 번 훑었다. README에는 어떻게 모듈을 사용할지에 대한 예제가 있어서 이해가 조금 더 수월해졌다. 그리고 테스트 코드를 보면서 어떻게 동작하는지 파악할 수 있었다. <code>django-faker</code>는 django Model의  field 타입을 예측하는 <code>Guessor</code>, 타입에 맞춰 값을 랜덤하게 생성하는 <code>Generator</code>, 생성한 값의 포맷팅을 담당하는 <code>Formatter</code>, 모델에 생성한 랜덤값을 주입하는 <code>Populator</code>로 이루어져 있다. 이에 맞춰서 테스트 코드도 각 부분들을 테스트하는 것을 확인할 수 있었다.
<img decoding="async" loading="lazy" alt="django-faker의 동작방식" src="https://watanka.github.io/assets/images/faker%EB%8F%99%EC%9E%91%EB%B0%A9%EC%8B%9D-2e9cee9d11a003343340a1715aab2330.png" width="922" height="1556" class="img_ev3q"></p>
<br>
<br>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="기여-목표-선정하기">기여 목표 선정하기<a href="https://watanka.github.io/open_source_first_step#%EA%B8%B0%EC%97%AC-%EB%AA%A9%ED%91%9C-%EC%84%A0%EC%A0%95%ED%95%98%EA%B8%B0" class="hash-link" aria-label="기여 목표 선정하기에 대한 직접 링크" title="기여 목표 선정하기에 대한 직접 링크">​</a></h3>
<p><code>django-faker</code>는 업데이트된지 꽤 오래되었다. 하지만 해당 모듈이 의존하는 <code>django</code>와 <code>faker</code>는 계속 업데이트가 이뤄지고 있었다. 그래서 django-faker가 현재 버젼이 커버하지 못하는 새로운 django의 model field들이 있을 거라고 가정을 세우고, 이 부분에 코드를 추가해보기로 마음먹었다.
그리고 다음과 같이 목표를 세웠다.</p>
<ul>
<li><code>django-faker</code>에서 정해놓은 PR checks를 우선적으로 통과할 것.</li>
<li>메인테이너 또는 오너가 내가 올린 PR을 merge시켜주는 것. (워낙 오래된 라이브러리라 리액션이 늦게 돌아올 수도 있지만, 우선은 해보자)</li>
</ul>
<br>
<br>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="기여할-부분-찾기">기여할 부분 찾기<a href="https://watanka.github.io/open_source_first_step#%EA%B8%B0%EC%97%AC%ED%95%A0-%EB%B6%80%EB%B6%84-%EC%B0%BE%EA%B8%B0" class="hash-link" aria-label="기여할 부분 찾기에 대한 직접 링크" title="기여할 부분 찾기에 대한 직접 링크">​</a></h3>
<p>우선 보통 기여할 부분을 찾는 방법은 이슈를 살펴보거나, 모듈 사용중에 불편함을 겪은 부분에 대해 개선방법을 제시하는 식으로 진행한다는 점을 기억하자. (우리는 전략적으로 django-faker가 업데이트가 오랫동안 이뤄지지 않았다는 점을 이용하는 거다.)
<code>django-faker</code>의 마지막 업데이트 일자인 2015년을 기점으로 <code>django</code>에는 어떠한 모델 관련 업데이트가 있었는지 확인해보았다.(<a href="https://docs.djangoproject.com/en/5.1/releases/" target="_blank" rel="noopener noreferrer">https://docs.djangoproject.com/en/5.1/releases/</a>)
여러 변경사항들중에 2.0을 기준으로 시간 간격을 저장하는 <code>DurationField</code> 필드가 추가된 것을 확인할 수 있었다!
우선 내 계정으로 리포지토리를 fork하고 로컬에 clone했다.<br>
<img decoding="async" loading="lazy" alt="repo_fork" src="https://watanka.github.io/assets/images/repo_fork-f79c4825715cca244bca5d9b2719e512.png" width="904" height="100" class="img_ev3q"><br>
<!-- -->실제로 <code>django-faker</code>가 <code>DurationField</code>를 처리하지 못하는지 확인할 필요가 있었기 때문에, <code>DurationField</code>를 사용하는 모델을 하나 만들고, 테스트 코드를 작성해준다.</p>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">class</span><span class="token plain"> </span><span class="token class-name">GamePlayTime</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">models</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Model</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">	game </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> models</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">ForeignKey</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">Game</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> on_delete</span><span class="token operator" style="color:#393A34">=</span><span class="token plain">models</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">CASCADE</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">	player </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> models</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">ForeignKey</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">Player</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> on_delete</span><span class="token operator" style="color:#393A34">=</span><span class="token plain">models</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">CASCADE</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">	duration </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> models</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">DurationField</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="클립보드에 코드 복사" title="복사" class="clean-btn"><span class="copyButtonIcons_eSgA" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_y97N"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_LjdS"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">class</span><span class="token plain"> </span><span class="token class-name">TestDurationField</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">unittest</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">TestCase</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">	</span><span class="token keyword" style="color:#00009f">def</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">testGamePlayTime</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">self</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">		generator </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> fake</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">		populator </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> Populator</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">generator</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">		populator</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">addEntity</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">Game</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">10</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">		populator</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">addEntity</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">Player</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">10</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">		</span><span class="token string" style="color:#e3116c">'game'</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">lambda</span><span class="token plain"> x</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> Game</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">objects</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">order_by</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'?'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">first</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">		</span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">		populator</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">addEntity</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">GamePlayTime</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">10</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">		</span><span class="token string" style="color:#e3116c">'game'</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">lambda</span><span class="token plain"> x</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> Game</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">objects</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">order_by</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'?'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">first</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">		</span><span class="token string" style="color:#e3116c">'player'</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">lambda</span><span class="token plain"> x</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> Player</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">objects</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">order_by</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'?'</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">first</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">		</span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">		insertedPks </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> populator</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">execute</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">		self</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">assertEqual</span><span class="token punctuation" style="color:#393A34">(</span><span class="token builtin">len</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">insertedPks</span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain">GamePlayTime</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">10</span><span class="token punctuation" style="color:#393A34">)</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="클립보드에 코드 복사" title="복사" class="clean-btn"><span class="copyButtonIcons_eSgA" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_y97N"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_LjdS"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<br>
<p>테스트가 <code>django-faker</code>에서 발생하는 AttributeError로 테스트를 통과하지 못 하는 걸 확인했다.</p>
<br>
<p><img decoding="async" loading="lazy" alt="DurationField에러" src="https://watanka.github.io/assets/images/DurationField%EC%97%90%EB%9F%AC-68d34ccf29c3307e7ec279ecddd8e794.png" width="1624" height="392" class="img_ev3q"></p>
<p>새로운 코드를 반영하기 위한 브랜치를 생성한다. 브랜치 이름은 나름 비장하게 <code>eature/enhance-django-support</code> 그리고, 테스트 코드를 통과하기 위해 <code>FieldTypeGuesser</code>에 <code>DurationField</code>를 처리할 수 있도록 코드를 추가해주었다. (django와 faker의 버젼에 맞춰 코드를 업데이트해줄 필요가 있었는데, 이 부분에서 의외로 시간을 많이 사용했다.)</p>
<br>
<p><img decoding="async" loading="lazy" alt="DurationHandling코드추가" src="https://watanka.github.io/assets/images/DurationHandling%EC%BD%94%EB%93%9C%EC%B6%94%EA%B0%80-9d53862d8b5f8adb60aad4531028e9a0.png" width="1668" height="1452" class="img_ev3q">
이렇게 작성한 코드를 바탕으로 테스트를 다시 실행시켜준다.<br>
<!-- -->테스트를 잘 통과한다..!<br>
<!-- -->테스트를 내가 임의로 바꾸는 건 좋지 않은 방법이지만, 오랫동안 관리되지 않은 부분에 대한 업데이트를 적용하기 위한 테스트이기 때문에 우선 적용하기로 했다.</p>
<br>
<br>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="pull-request-작성하기">Pull Request 작성하기<a href="https://watanka.github.io/open_source_first_step#pull-request-%EC%9E%91%EC%84%B1%ED%95%98%EA%B8%B0" class="hash-link" aria-label="Pull Request 작성하기에 대한 직접 링크" title="Pull Request 작성하기에 대한 직접 링크">​</a></h3>
<p>테스트도 무사히 잘 통과하니, 이제 마지막으로 메인 리포지토리에 풀리퀘스트를 날려준다!! PR에는 어떤 문제를 찾았고, 그 문제를 해결하기 위해 어떤 변경사항을 적용했는지 코드를 보지 않더라도 의도와 변경사항이 명확히 전달될 수 있도록 작성하는 게 중요하다. 작성한 <a href="https://github.com/joke2k/django-faker/pull/35" target="_blank" rel="noopener noreferrer">PR</a>은 여기에서 확인 가능하다.
<img decoding="async" loading="lazy" alt="PR작성" src="https://watanka.github.io/assets/images/PR_screenshot-3fc3a8bdeb3574965173aba51f354dd3.png" width="2042" height="880" class="img_ev3q"></p>
<br>
<br>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="outro">Outro<a href="https://watanka.github.io/open_source_first_step#outro" class="hash-link" aria-label="Outro에 대한 직접 링크" title="Outro에 대한 직접 링크">​</a></h3>
<p>이렇게 첫 오픈소스 기여하기를 진행해보았다. 나름 단순한 코드 리포지토리였지만, 소스코드를 살펴보는 과정, 업데이트 릴리즈 노트를 확인하는 과정, 라이브러리 호환성을 맞추는 과정, PR을 잘 작성하는 과정 등 생각보다 신경쓸 것이 많았다. 하지만, 오픈소스 기여라는 것이 내가 엄두조차 못 낼 정도로 대단한 건 아니라는 걸 알게 되었다. 다음에는 버그를 찾고, 픽스해보는 PR을 날려보자!
2024년도 며칠 안 남았다. 연초에 다짐했던 목표 중 하나를 이렇게 글을 쓰면서 달성할 수 있어서 다행이다!</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[AI해커톤 2024 후기]]></title>
            <link>https://watanka.github.io/ai_hackerthon_2024</link>
            <guid>https://watanka.github.io/ai_hackerthon_2024</guid>
            <pubDate>Sun, 24 Nov 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[[해커톤 리포지토리 link]]]></description>
            <content:encoded><![CDATA[<p><a href="https://github.com/HIRO-works/HIRO-AI/tree/master" target="_blank" rel="noopener noreferrer">[해커톤 리포지토리 link]</a><br>
<!-- -->항해 AI 해커톤에 참여했다. 몇 팀 참여하지 않긴 했지만, 기분 좋게 우승도 했다. 이번 글에서는 회고겸, 어떤 기능들을 구현했는지, 만약 시간이 더 주어진다면 어떤 부분들을 더 손볼 수 있을지 적어보려고 한다.</p>
<p>주제는 'AI를 통해 삶과 일의 균형을 찾아라'였다. 금요일 저녁 7시부터 다음날 오후 2시까지 진행되었다. 우리팀은 AI 개발자 1명, 백엔드 3명, 그리고 프론트 2명으로 구성되었다. 다들 의견 제시와 역할 분담에 적극적이여서 초반부터 끝날 때까지 신나게 진행할 수 있었다.</p>
<p>우리 팀이 정한 서비스 주제는 '채용담당관 입장에서 수많은 이력서를 검토하면서 느낄 수 있는 피로감을 줄이기 위해 이력서 정보를 LLM으로 요약정리해주는 서비스'였다.
<img decoding="async" loading="lazy" alt="시연영상.gif" src="https://watanka.github.io/assets/images/%EC%8B%9C%EC%97%B0%EC%98%81%EC%83%81-7af0491ab893b5651de7d8436ede25a2.gif" width="1280" height="616" class="img_ev3q"></p>
<p>메인 기능은 요약하자면 다음과 같다.</p>
<ol>
<li><strong>이력서 정보 추출 및 벡터DB에 저장</strong>(input: PDF file, output: 벡터DB저장 정보)</li>
<li><strong>엠베딩 기반 질문 생성 기능</strong>(input: 이력서 ID, output: 이력서 관련 생성 질문)</li>
<li><strong>자연어 기반 벡터DB 쿼리 기능</strong>(input: 프롬프트, output: list[이력서ID])</li>
</ol>
<br>
<p>좀 더 자세히 설명하자면,</p>
<ol>
<li><strong>이력서 정보 추출 및 벡터DB에 저장</strong></li>
</ol>
<ul>
<li>PDF 이력서 업로드 시, 텍스트를 추출하여 요약정리 후 벡터DB에 저장한다. 이력서에서 뽑을 수 있는 공통된 정보들을 정형화하여(지원자 이름, 직군[백엔드, AI, 프론트, 풀스택], 연차, 사용 언어) 벡터DB의 메타정보에 저장해두었다. Pydantic으로 만든 스키마가 있다면, 이에 맞춰 정보를 뽑아주는 langchain의 <code>with_structured_output</code> 함수를 사용했다.</li>
</ul>
<ol start="2">
<li><strong>엠베딩 기반 질문 생성 기능</strong></li>
</ol>
<ul>
<li>미리 저장해둔 이력서의 엠베딩 정보를 기반으로 질문을 생성한다. 질문의 종류는 직군별, 컬쳐핏, 경험, 프로젝트 질문으로 나뉜다. 예를 들자면, 이력서에 OCR 프로젝트에 대한 내용이 있다면, 해당 프로젝트에 대해 어떤 기여를 했는지, 어떤 어려움이 있었는지에 대한 질문을 생성해준다.</li>
</ul>
<ol start="3">
<li><strong>자연어 기반 벡터DB 쿼리 기능</strong></li>
</ol>
<ul>
<li>채용담당관이 적은 요구사항(프롬프트)을 기반으로 매칭되는 이력서ID들을 반환한다. 요구사항에 있는 메타정보로 우선적으로 필터링한 후, similarity search를 진행한다. (langchain에서는 이 과정을 <code>query-construction chain</code>이라고 명칭되어있다.)</li>
</ul>
<br>
<p>생각보다 밤을 새면서 개발하는 과정은 쉽지 않았지만, 팀원들이 다 열심히 해준 덕분에 해가 뜰 무렵, 가시적인 결과물인 나오기 시작했다. 미리 MockAPI를 작성해둔 덕분에 각 도메인(프론트, 백엔드, AI)에서 따로 작업을 하긴 했지만, API 연동도 나름 순탄하게 진행되었다. 11월달에 들어 개발하면서 가장 뿌듯한 순간이였다.
그리고 해커톤 일주일이 지난 시점, 만약 시간이 더 주어진다면 어떤 부분을 더 개선해볼 수 있을까 고민했다. 구현한 기능은 LLM을 활용한다는 점에서 특수할 수는 있으나, 결국 데이터를 저장하고, 잘 읽어오는 과정이다. Store와 Retriever 객체로 추상화하여 틀을 만들어 변경사항에 유연하게 대처할 수 있도록 한 다음에 LLM을 활용해서 할 수 있는 여러 가지 시도들을 해볼 수 있을 거라는 생각이 들었다.</p>
<p>여러 가지 시도들을 하기 전에 가장 먼저 해야할 일들은 평가 지표를 설정하는 것이다. 시도들에 대한 객관적인 지표가 있어야만 서비스에 알맞는 선택을 할 수 있기 때문이다.</p>
<h4 class="anchor anchorWithStickyNavbar_LWe7" id="평가-프로세스-구축">평가 프로세스 구축<a href="https://watanka.github.io/ai_hackerthon_2024#%ED%8F%89%EA%B0%80-%ED%94%84%EB%A1%9C%EC%84%B8%EC%8A%A4-%EA%B5%AC%EC%B6%95" class="hash-link" aria-label="평가 프로세스 구축에 대한 직접 링크" title="평가 프로세스 구축에 대한 직접 링크">​</a></h4>
<p>현재 시스템의 성능을 객관적으로 측정할 수 있는 평가 체계가 필요하다. 현재 프로세스는 이력서를 요약하고 저장하고 쿼리하여 읽어오는 방식이다.
요약, 저장, 쿼리 각각의 과정에 대해 평가 지표를 구성할 수 있다. 그리고, 이 과정에서 사용자의 피드백을 반영할 수 있도록 한다면 더욱 좋을 거라고 생각한다.</p>
<ul>
<li>이력서 요약의 정확성 평가</li>
<li>생성된 질문의 품질 평가</li>
<li>검색 결과의 정확도 및 관련성 평가</li>
<li>사용자 피드백 시스템 구축</li>
</ul>
<p>이 밖에도 찾아보니 LLM관련 개선할 부분들이 굉장히 많았다. 시간은 좀 걸리겠지만, 천천히 이 부분들에 대해서도 알아보려고 한다.</p>
<h4 class="anchor anchorWithStickyNavbar_LWe7" id="임베딩-최적화">임베딩 최적화<a href="https://watanka.github.io/ai_hackerthon_2024#%EC%9E%84%EB%B2%A0%EB%94%A9-%EC%B5%9C%EC%A0%81%ED%99%94" class="hash-link" aria-label="임베딩 최적화에 대한 직접 링크" title="임베딩 최적화에 대한 직접 링크">​</a></h4>
<p>현재는 이력서 내용을 요약한 후 임베딩을 수행하고 있는데, 이는 중요한 정보의 손실을 초래할 수 있다.</p>
<ul>
<li>전체 이력서 내용에 대한 섹션별 임베딩 수행</li>
<li>주요 키워드와 문맥 정보를 보존하는 청크 단위 임베딩</li>
<li>다중 임베딩 모델의 앙상블 적용</li>
</ul>
<h4 class="anchor anchorWithStickyNavbar_LWe7" id="검색-성능-향상">검색 성능 향상<a href="https://watanka.github.io/ai_hackerthon_2024#%EA%B2%80%EC%83%89-%EC%84%B1%EB%8A%A5-%ED%96%A5%EC%83%81" class="hash-link" aria-label="검색 성능 향상에 대한 직접 링크" title="검색 성능 향상에 대한 직접 링크">​</a></h4>
<p>프롬프트 기반 검색의 정확도를 높이기 위한 방안:</p>
<ul>
<li>하이브리드 검색 구현 (키워드 + 시맨틱 검색)</li>
<li>검색 결과 재순위화(Reranking) 도입</li>
<li>사용자 피드백을 반영한 검색 결과 개선</li>
</ul>
<h4 class="anchor anchorWithStickyNavbar_LWe7" id="데이터-저장소-최적화">데이터 저장소 최적화<a href="https://watanka.github.io/ai_hackerthon_2024#%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%A0%80%EC%9E%A5%EC%86%8C-%EC%B5%9C%EC%A0%81%ED%99%94" class="hash-link" aria-label="데이터 저장소 최적화에 대한 직접 링크" title="데이터 저장소 최적화에 대한 직접 링크">​</a></h4>
<p>메타데이터 저장 및 관리 전략:</p>
<ul>
<li>RDB와 벡터DB의 하이브리드 구조 검토(어디까지 메타정보로 저장해도 될까? 어디서부터 RDB를 활용하는 게 좋을까?)</li>
<li>메타데이터의 인덱싱 전략</li>
<li>캐싱 레이어 도입 검토</li>
</ul>
<h4 class="anchor anchorWithStickyNavbar_LWe7" id="데이터-검증-및-예외처리">데이터 검증 및 예외처리<a href="https://watanka.github.io/ai_hackerthon_2024#%EB%8D%B0%EC%9D%B4%ED%84%B0-%EA%B2%80%EC%A6%9D-%EB%B0%8F-%EC%98%88%EC%99%B8%EC%B2%98%EB%A6%AC" class="hash-link" aria-label="데이터 검증 및 예외처리에 대한 직접 링크" title="데이터 검증 및 예외처리에 대한 직접 링크">​</a></h4>
<p>안정적인 서비스 운영을 위한 개선사항:</p>
<ul>
<li>입력 데이터 유효성 검사 강화</li>
<li>PDF 파싱 예외상황 대응</li>
<li>정형화된 데이터 스키마 검증 로직 추가</li>
<li>에러 로깅 및 모니터링 시스템 구축</li>
</ul>
<p>이번 글에서는 11월 15일부터 16일까지 무박 2일로 참여한 항해 AI 해커톤 회고 글을 작성해보았다. LLM을 활용한 실제 서비스를 구현해보면서, LLM의 활용 가능성에 대해 다시 한 번 감탄하지 않을 수 없었다. 그리고, 실제 사용자들이 사용하는 서비스를 만들기 위해서 많은 부분들을 개선할 수 있을 거라는 생각이 들었다.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[외부 API 다루는 방법(feat. Rate Limit)]]></title>
            <link>https://watanka.github.io/muze-api</link>
            <guid>https://watanka.github.io/muze-api</guid>
            <pubDate>Sat, 09 Nov 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[TLDR]]></description>
            <content:encoded><![CDATA[<p><strong>TLDR</strong></p>
<ul>
<li>Muze라는 음악 SNS를 개발하고 있다.</li>
<li>노래 정보 검색 기능이 필요한데, 데이터가 부족한 상황이라 검색 결과가 좋지 않다.</li>
<li>필요한 데이터를 바로 검색 후 수집할 수 있도록 외부 API를 활용한다.</li>
<li>이 외부 API에 문제가 생기면, 앱에도 문제가 생긴다. 그리고, API에는 Rate Limit이 걸려있어서 요청 수가 제한된다.</li>
<li>외부 API의 의존성을 최소화하기 위해 fallback으로 사용할 API를 여러 개 두고, Token Bucket 알고리즘을 사용하여 요청을 분산시킨다.</li>
</ul>
<p>서비스를 개발하면서 A-Z까지 전부 다 구현하기란 어렵다. 구현의 난이도 뿐만 아니라 효율면에서 부적절하다. 그래서 우리는 외부 API를 호출한다. 원하는 기능을 편리하게 사용이 가능하다. 한 가지 문제점이라면, 외부의 의존성이 생김으로써 내부의 문제가 아닌 외부의 문제로 서비스의 결함이 발생할 수도 있다는 점이다.  예를 들어, 쇼핑몰 서비스에서 외부 결제 시스템의 문제로 상품 결제가 이루어지지 않는다면, 실질적인 손해로 이어질 수 있다.</p>
<p>진행중인 사이드 프로젝트 Muze는 음악 취향을 공유하는 웹서비스이다. 자신이 좋아하는 노래를 게시하고 공유하는 기능이 메인 서비스다. 이 서비스를 위해서는 노래 정보들을 데이터베이스에 저장하고 검색하는 기능이 필요한데, 프로젝트의 규모상 '검색이 가능한 노래'의 범위를 정하기가 애매했다. 물론 적당히 구할 수 있는 노래들을 읽어와서 저장하는 방법도 있겠지만, 실제 사용자들이 사용하기에 매끄러운 서비스를 만들고 싶었다.</p>
<p>그래서 생각한 방법은 사용자가 검색 시에 음악 플랫폼(Spotify, Shazam 등)의 검색 API를 활용해서 결과를 리턴해준다. 음악 정보가 서버 내에 당장 없더라도, 정보를 반환해줄 수 있게 되는 것이다. 하지만 이런 처리는 외부 API에 대한 서버의 의존성이 생기게 된다. 우리 서버가 정상적으로 동작한다고 해도, Spotify의 서버에 문제가 생기면 고스란히 그 문제가 우리 서버의 문제로 돌아온다.</p>
<p>외부 API를 활용하는 것은 이미 결정된 사항이기 때문에 이 상황에서 내릴 수 있는 차선의 선택을 하는 것이 중요하다. Spotify 서버에 문제가 생긴다면, Shazam 서버를 활용하는 건 어떨까? 두 플랫폼 모두 문제가 생길 일은 극히 드물 것이다. 인풋 형식과 아웃풋 형식을 맞춰서 추상화된 <code>RequestHandler</code> 클래스를 작성한다.</p>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">from</span><span class="token plain"> abc </span><span class="token keyword" style="color:#00009f">import</span><span class="token plain"> ABC</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> abstractmethod</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">class</span><span class="token plain"> </span><span class="token class-name">RequestHandler</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">ABC</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">	</span><span class="token keyword" style="color:#00009f">def</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">__init__</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">self</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> api_client</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">		self</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">api_client </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> api_client</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">	</span><span class="token decorator annotation punctuation" style="color:#393A34">@abstractmethod</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">	</span><span class="token keyword" style="color:#00009f">def</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">search</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">self</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> keyword</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> category</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">		</span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> 노래정보</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">class</span><span class="token plain"> </span><span class="token class-name">SpotifyAPIRequestHandler</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">RequestHandler</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">	</span><span class="token punctuation" style="color:#393A34">.</span><span class="token punctuation" style="color:#393A34">.</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">class</span><span class="token plain"> </span><span class="token class-name">ShazamAPIRequestHandler</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">RequestHandler</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">	</span><span class="token punctuation" style="color:#393A34">.</span><span class="token punctuation" style="color:#393A34">.</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">class</span><span class="token plain"> </span><span class="token class-name">YoutubeAPIRequestHandler</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">RequestHandler</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">	</span><span class="token punctuation" style="color:#393A34">.</span><span class="token punctuation" style="color:#393A34">.</span><span class="token punctuation" style="color:#393A34">.</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="클립보드에 코드 복사" title="복사" class="clean-btn"><span class="copyButtonIcons_eSgA" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_y97N"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_LjdS"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p><img decoding="async" loading="lazy" alt="노래 검색을 위해 활용할 API가 세 개나 생겼다." src="https://watanka.github.io/assets/images/muze-api-ef6f266ad217ea954700d6b8dd1131b1.png" width="947" height="506" class="img_ev3q">
노래 검색을 위해 활용할 API가 세 개나 생겼다.
그럼 이제 들어오는 요청들을 이 API들에게 어떻게 분배해줄지 설정해주어야한다. 요구사항은</p>
<ol>
<li>결과가 나오지 않을 경우(response.status_code !=200), 다른 API로 요청을 넘긴다.</li>
<li>각각의 API의 Rate Limit을 초과하지 않는다.</li>
<li>사용자 경험을 고려하여 최대한 빠른 속도로 결과를 리턴해준다.
로 정리가 가능하다.</li>
</ol>
<p>이 요구사항을 충족시키는 방법은 Token Bucket알고리즘을 활용하여 Rate Limiting을 적용하는 방법이다.
<img decoding="async" loading="lazy" alt="TokenBucketAlgorithm.png" src="https://watanka.github.io/assets/images/TokenBucketAlgorithm-763c7d44cec073279b9b62fb1f80ab3f.png" width="1746" height="873" class="img_ev3q"></p>
<p>토큰 버킷 알고리즘은 토큰을 기록하여 사용량을 조절한다. 토큰은 시간에 따라 일정량씩 채워지고, 등록된 함수의 호출 시점에 토큰을 사용한다. 만약 시간당 채워지는 토큰보다 더 많은 호출이 이뤄지면 잠시동안 함수 호출이 멈추는 원리이다.</p>
<p>TokenBucket 클래스와 이를 관리하는 APIManager 클래스를 만들어 토큰 버킷 알고리즘을 구현해준다. 각 API당 할당된 Rate Limit 정보를 기반으로 <code>rate</code>(초당 버킷에 채워지는 토큰 수)과 <code>capacity</code>(버킷이 담을 수 있는 토큰의 최대수)을 설정한다.</p>
<p>그리고, 서버 접속 사용자 수에 따라 프로세스가 추가된다는 점을 상기해야한다. 프로세스 간의 <code>APIManager</code> 인스턴스는 독립적인데, API 호출은 공유하기 때문에 이를 인스턴스 간 정보들을 공유할 수 있도록 설정해주어야한다. 인메모리의 Redis를 사용해서 rate, capacity, token 정보를 공유하도록 설정한다.</p>
<p>rate, capacity 설정을 위해서 API 문서에서 Rate Limit에 대한 정보들을 살펴보았다. Spotify같은 경우에는 Rate Limit을 30초동안 쌓인 요청수를 기준으로 잡는다는 이야기만 있고 최대 요청량은 적혀있지 않았다(내가 못 찾았을수도...). 그래서 <code>concurrent.future</code> 모듈을 활용하여 30초동안 보낼 요청 수를 조정하여 실험한 후 최대 요청량을 가늠하였다.</p>
<div class="codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain"># num_worker=1,2,3,4,5, ...,9,10</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">[{'num_requests': 158, 'num_success': 158, 'num_failures': 0}, {'num_requests': 302, 'num_success': 301, 'num_failures': 1}, {'num_requests': 533, 'num_success': 307, 'num_failures': 226}, {'num_requests': 774, 'num_success': 311, 'num_failures': 463}, {'num_requests': 992, 'num_success': 342, 'num_failures': 650}, {'num_requests': 1208, 'num_success': 356, 'num_failures': 852}, {'num_requests': 1474, 'num_success': 414, 'num_failures': 1060}, {'num_requests': 1659, 'num_success': 390, 'num_failures': 1269}, {'num_requests': 1973, 'num_success': 405, 'num_failures': 1568}, {'num_requests': 2243, 'num_success': 402, 'num_failures': 1841}]</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="클립보드에 코드 복사" title="복사" class="clean-btn"><span class="copyButtonIcons_eSgA" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_y97N"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_LjdS"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>스포티파이 API는 30초동안 대략 300 - 400개 정도의 요청을 받아준다. 대체 API인 Shazam과 Youtube도 비슷한 수량의 요청을 받아준다고 할 때, 대략 우리 서버는 30초동안 1000개의 검색 요청을 처리할 수 있다는 이야기이다.
이 얘기는 Muze 앱 운영 중에 1001명의 유저가 검색을 한 번씩 하는 경우가 30초의 시간동안 한 번이라도 발생할 경우, 다운타임이 발생할 수도 있다는 이야기이다. (물론 300명의 유저가 생길지 안 생길지조차 모르는 상황이긴 하지만, 30초동안 검색 한 번은 너무 짜다. 보통 한 유저당 한번 이상의 검색을 한다.)</p>
<p>이런 문제는 검색이 호출될 때 백그라운드로 해당 노래 정보를 데이터베이스에 저장하는 식으로 구성한다면, 시간이 지남에 따라 이런 문제들은 해결될 수 있을 거라고 생각했다. 한 번 이상 검색된 정보는 데이터베이스에서 호출되는 방식이기 때문에, 시간이 지날수록 외부 API에 대한 의존도가 낮아질 것이라고 기대했다. 여기에다가 추가적으로 백그라운드 스케쥴러로 최신곡에 대한 크롤링을 주기적으로 수행한다면, 외부 검색 API의 의존성을 최소화하고, 괜찮은 사용자 경험을 제공해줄 수 있게 된다.</p>
<p>이번 글에서는 Rate Limit이 설정되어있는 API를 다루기 위해 고려해야할 부분들과 안정적으로 처리할 수 있는 Rate Limit 기법에 대해서 알아보았다. 검색 기능은 Muze에서 꽤 중요한 기능이다. 이런 중요한 기능을 외부 API에 의존해야한다는 점이 고민되긴 했지만, 부족한 데이터를 다 커버할 수 없는 초기 개발 상황에서 차선의 선택이 필요했다. 외부 API에 의존하되, 운영 시 생길 수 있는 의존성을 최소한으로 줄이고, 데이터가 쌓임에 따라 그 의존성을 아예 없앨 수 있다면, 꽤 괜찮은 시나리오라고 생각한다.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[GIL이 뭐길래]]></title>
            <link>https://watanka.github.io/GIL이_뭐길래</link>
            <guid>https://watanka.github.io/GIL이_뭐길래</guid>
            <pubDate>Mon, 21 Oct 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[Introduction]]></description>
            <content:encoded><![CDATA[<h3 class="anchor anchorWithStickyNavbar_LWe7" id="introduction">Introduction<a href="https://watanka.github.io/GIL%EC%9D%B4_%EB%AD%90%EA%B8%B8%EB%9E%98#introduction" class="hash-link" aria-label="Introduction에 대한 직접 링크" title="Introduction에 대한 직접 링크">​</a></h3>
<p>지난 10월 7일, 파이썬 3.13이 릴리즈되었습니다. 변경사항 중 가장 주목할만한 내용은 GIL을 키고 끌 수 있도록 변경한 부분이였습니다. 정식으로 GIL이 완전 제거 결정을 내리기까지는(제거하지 않기로 결정할 수도 있음) 약 5년정도의 시간이 걸린다고 하지만, 멀티쓰레딩이 어려웠던 기존의 파이썬을 생각하면 엄청난 변화라고 할 수 있을 것 같습니다. 이번 글에서는 GIL이 무엇인지, 왜 등장했고 왜 제거하기로 한 건지에 대해 알아보겠습니다.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="gil이-무엇일까요">GIL이 무엇일까요?<a href="https://watanka.github.io/GIL%EC%9D%B4_%EB%AD%90%EA%B8%B8%EB%9E%98#gil%EC%9D%B4-%EB%AC%B4%EC%97%87%EC%9D%BC%EA%B9%8C%EC%9A%94" class="hash-link" aria-label="GIL이 무엇일까요?에 대한 직접 링크" title="GIL이 무엇일까요?에 대한 직접 링크">​</a></h3>
<p>GIL이 무엇인지 이해하기 위해서 우선 우리가 작성한 파이썬 코드가 어떤 식으로 돌아가는지 알아야합니다.
컴퓨터의 기본 동작 방식은 다음과 같습니다. 보조기억장치(하드 드라이브)에 저장된 코드를 읽어 메모리 공간(RAM)에 프로세스로써 올리고, CPU는 레지스터의 도움을 받아 RAM에 있는 명령어들을 처리합니다.
<img decoding="async" loading="lazy" src="https://watanka.github.io/assets/images/nogil-%EC%BB%B4%ED%93%A8%ED%84%B0%EB%8F%99%EC%9E%91%EB%B0%A9%EC%8B%9D-033f4b8de7c7adbaed68e1fe09b36f30.png" width="1216" height="574" class="img_ev3q">
RAM에 적재된 코드는 컴퓨터가 이해할 수 있는 언어로 번역(소스코드 -&gt;바이트코드-&gt;기계어)되어야합니다. 언어마다 다르지만, 파이썬은 작성한 코드를 메모리에 올릴 때마다 인터프리터가 코드 한줄 한줄 이 번역을 수행해주는 <code>인터프리터 언어</code>입니다.
<img decoding="async" loading="lazy" src="https://watanka.github.io/assets/images/nogil-%ED%8C%8C%EC%9D%B4%EC%8D%AC%EC%9D%B8%ED%84%B0%ED%94%84%EB%A6%AC%ED%84%B0-2ead99fd755923c7917cb7d0db7069ec.png" width="1356" height="1020" class="img_ev3q">
인터프리터는 사람이 작성한 코드를 CPU에서 수행할 수 있게 번역해주는 역할을 한다고 보면 됩니다. 그리고, GIL(Global Interpreter Lock)은 프로세스당 하나의 쓰레드만이 이 인터프리터의 제어권을 갖고 명령을 수행할 수 있게 하는 락(Mutex)입니다. 동일한 프로세스라면, 여러 쓰레드가 존재해도, 이 락을 소유하기 전까지는 명령을 수행할 수 없습니다.
GIL은 멀티코어로 구성되어있는 최근 CPU들을(2024년 기준 인텔 i7은 최대 20개의 코어, 애플 M3는 최대 40코어 보유) 멀티 쓰레딩으로 활용할 수 있는 이점을 제약합니다.
<img decoding="async" loading="lazy" src="https://watanka.github.io/assets/images/nogil-%EC%93%B0%EB%A0%88%EB%93%9C-4c478a4ac5de7f6ae1aadb3948ff58f4.png" width="866" height="540" class="img_ev3q">
<img decoding="async" loading="lazy" src="https://watanka.github.io/assets/images/nogil-gil%EC%9D%B4%ED%95%B4-df4ccc5c2754d8d08dc482c2935c5bf0.png" width="1324" height="642" class="img_ev3q"></p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="gil은-왜-등장한-것일까요">GIL은 왜 등장한 것일까요?<a href="https://watanka.github.io/GIL%EC%9D%B4_%EB%AD%90%EA%B8%B8%EB%9E%98#gil%EC%9D%80-%EC%99%9C-%EB%93%B1%EC%9E%A5%ED%95%9C-%EA%B2%83%EC%9D%BC%EA%B9%8C%EC%9A%94" class="hash-link" aria-label="GIL은 왜 등장한 것일까요?에 대한 직접 링크" title="GIL은 왜 등장한 것일까요?에 대한 직접 링크">​</a></h3>
<p>그렇다면 왜 파이썬에 GIL이 등장한 걸까요? 정답은 다수의 쓰레드를 사용하면서 생길 수 있는 Race Condition을 예방하기 위해서입니다. 더 자세히는 파이썬의 메모리 관리 방법과 연관되어있습니다.
파이썬에서는 Garbage Collection에 (Heap영역에서 더이상 사용되지 않는 객체들을 삭제) Reference Counting을 사용합니다. 어떤 객체의 Reference Count가 0이상이라면, Garbage Collection 대상에서 제외하고, 0에 도달한다면, 더 이상 사용하지 않는다고 판단하고 제거하는 식입니다. 그런데 이런 Reference Counting 방식은 같은 메모리 공간을 공유하는 쓰레드들끼리 같은 자원에 접근하는 Race Condition이 발생했을 때 문제가 생길 수 있습니다. Reference Counting에 대한 동시 접근으로 인해 삭제되어야할 객체가 삭제되지 않거나 유지되어야할 객체가 삭제되는 경우가 발생할 수 있습니다. 이런 Race Condition을 방지하기 위해 GIL이 탄생한 것입니다. (Java같은 경우, 특정 시점을 기준으로 garbage collection을 수행합니다.)</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="파이썬이-멀티코어를-활용하는-방법">파이썬이 멀티코어를 활용하는 방법<a href="https://watanka.github.io/GIL%EC%9D%B4_%EB%AD%90%EA%B8%B8%EB%9E%98#%ED%8C%8C%EC%9D%B4%EC%8D%AC%EC%9D%B4-%EB%A9%80%ED%8B%B0%EC%BD%94%EC%96%B4%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95" class="hash-link" aria-label="파이썬이 멀티코어를 활용하는 방법에 대한 직접 링크" title="파이썬이 멀티코어를 활용하는 방법에 대한 직접 링크">​</a></h3>
<p>GIL이 있다고 해서 파이썬이 멀티코어를 아예 활용하지 못 하는 건 아닙니다. 파이썬은 웹 프레임워크나 머신러닝 등 다양한 분야에서 활용되는 언어입니다. 파이썬이 멀티코어를 활용하는 방법은 크게 세 가지로 나뉠 수 있습니다.</p>
<ol>
<li>멀티 프로세싱</li>
<li>C나 C++로 작성한 코드</li>
<li>CPython이 아닌 다른 인터프리터(PyPy, Jython과 같은) 사용
(추가로 GIL때문에 멀티쓰레드가 아예 의미가 없는 건 아닙니다. <code>I/O작업시에 GIL은 자동으로 해제</code>되기 때문에 I/O bounded 작업에서는 멀티 쓰레드가 동작한다고 볼 수 있습니다.)</li>
</ol>
<h4 class="anchor anchorWithStickyNavbar_LWe7" id="파이썬-멀티코어-활용법1-멀티프로세스">파이썬 멀티코어 활용법1. 멀티프로세스<a href="https://watanka.github.io/GIL%EC%9D%B4_%EB%AD%90%EA%B8%B8%EB%9E%98#%ED%8C%8C%EC%9D%B4%EC%8D%AC-%EB%A9%80%ED%8B%B0%EC%BD%94%EC%96%B4-%ED%99%9C%EC%9A%A9%EB%B2%951-%EB%A9%80%ED%8B%B0%ED%94%84%EB%A1%9C%EC%84%B8%EC%8A%A4" class="hash-link" aria-label="파이썬 멀티코어 활용법1. 멀티프로세스에 대한 직접 링크" title="파이썬 멀티코어 활용법1. 멀티프로세스에 대한 직접 링크">​</a></h4>
<p>멀티프로세싱을 활용하면 멀티 코어를 온전히 사용할 수 있습니다. 그림으로 그리면 이런 식입니다. GIL에 의해 제약받지 않을 수 있도록 각 프로세스마다 별도의 인터프리터를 둡니다.
<img decoding="async" loading="lazy" src="https://watanka.github.io/assets/images/nogil-%EB%A9%80%ED%8B%B0%ED%94%84%EB%A1%9C%EC%84%B8%EC%8B%B1-4683bdd06de389a205e0069e45fee219.png" width="1104" height="868" class="img_ev3q">
웹 게이트웨이 인터페이스인 <code>gunicorn</code>은 일반적으로 위에 보는 것과 같이 멀티 프로세싱 방식으로 동작합니다. 여기서 한 가지 알아야할 점은 프로세스는 개별적인 메모리 공간을 요구하기 때문에 멀티쓰레드에 비해서 메모리 공간을 더 차지합니다. 그리고 이 메모리 공간 때문에 메모리를 공유하는 쓰레드에 비해 컨텍스트 스위칭 비용이 발생합니다.</p>
<h4 class="anchor anchorWithStickyNavbar_LWe7" id="파이썬-멀티코어-활용법2-cc-작성-코드">파이썬 멀티코어 활용법2. C,C++ 작성 코드<a href="https://watanka.github.io/GIL%EC%9D%B4_%EB%AD%90%EA%B8%B8%EB%9E%98#%ED%8C%8C%EC%9D%B4%EC%8D%AC-%EB%A9%80%ED%8B%B0%EC%BD%94%EC%96%B4-%ED%99%9C%EC%9A%A9%EB%B2%952-cc-%EC%9E%91%EC%84%B1-%EC%BD%94%EB%93%9C" class="hash-link" aria-label="파이썬 멀티코어 활용법2. C,C++ 작성 코드에 대한 직접 링크" title="파이썬 멀티코어 활용법2. C,C++ 작성 코드에 대한 직접 링크">​</a></h4>
<p>파이썬에서는 C나 C++로 작성된 코드를 수행할 수 있도록 C API를 제공합니다. 파이썬 인터프리터가 C 코드를 확인하면 제어권을 넘기는 방식입니다. 이렇게 되면 GIL 상관없이 C나 C++ 코드에서 사용하는 것처럼 멀티쓰레딩을 사용할 수 있습니다. 대표적으로 numpy나 pandas와 같은 데이터과학 모듈이 이 방법을 활용합니다. C 작성 코드의 빠름과 유연함을 활용할 수 있다는 점이 장점이지만, C, C++ 데이터 구조에서 Python 구조체로 변경하기 위해 어느 정도의 오버헤드는 발생할 것입니다.</p>
<h4 class="anchor anchorWithStickyNavbar_LWe7" id="파이썬-멀티코어-활용법3-cpython이-아닌-다른-인터프리터-사용하기">파이썬 멀티코어 활용법3. CPython이 아닌 다른 인터프리터 사용하기<a href="https://watanka.github.io/GIL%EC%9D%B4_%EB%AD%90%EA%B8%B8%EB%9E%98#%ED%8C%8C%EC%9D%B4%EC%8D%AC-%EB%A9%80%ED%8B%B0%EC%BD%94%EC%96%B4-%ED%99%9C%EC%9A%A9%EB%B2%953-cpython%EC%9D%B4-%EC%95%84%EB%8B%8C-%EB%8B%A4%EB%A5%B8-%EC%9D%B8%ED%84%B0%ED%94%84%EB%A6%AC%ED%84%B0-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0" class="hash-link" aria-label="파이썬 멀티코어 활용법3. CPython이 아닌 다른 인터프리터 사용하기에 대한 직접 링크" title="파이썬 멀티코어 활용법3. CPython이 아닌 다른 인터프리터 사용하기에 대한 직접 링크">​</a></h4>
<p>CPython이 표준 인터프리터이긴 하지만, PyPy나 Jython, IronPython 등 다른 인터프리터들도 존재합니다. GIL은 CPython 안에 존재하는 락으로, 만약 다른 인터프리터를 사용한다면 쓰레드 제약에 대해 더 이상 신경쓰지 않아도 됩니다. (물론, 다중 쓰레드 사용에 따른 공유 자원의 Race Condition을 직접 신경써줘야하겠죠) CPython이 아닌 다른 인터프리터를 직접 사용해본 적은 없지만, 필요하다면 인터프리터의 장단점을 확인하여 상황에 맞게 적절히 사용해볼 수 있을 것 같습니다.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="313은-gil-없이-어떻게-동작하는-걸까">3.13은 GIL 없이 어떻게 동작하는 걸까?<a href="https://watanka.github.io/GIL%EC%9D%B4_%EB%AD%90%EA%B8%B8%EB%9E%98#313%EC%9D%80-gil-%EC%97%86%EC%9D%B4-%EC%96%B4%EB%96%BB%EA%B2%8C-%EB%8F%99%EC%9E%91%ED%95%98%EB%8A%94-%EA%B1%B8%EA%B9%8C" class="hash-link" aria-label="3.13은 GIL 없이 어떻게 동작하는 걸까?에 대한 직접 링크" title="3.13은 GIL 없이 어떻게 동작하는 걸까?에 대한 직접 링크">​</a></h3>
<p>그럼 여기서 떠오르는 궁금증은 이미 잘 돌아가고 있는데, 왜 이제서야 바꾸는 걸까 입니다.
2007년에 파이썬의 창시자 귀도 반 로썸이 게시한 글에는 멀티 코어가 등장하면서, GIL을 제거하는 방안을 고민해보았지만, 딱히 성능적으로 이점이 없었다는 얘기가 있습니다. GIL을 제거하면 CPU위주의 태스크를 수행하는 멀티 쓰레딩 프로그램에서는 확실히 이점이 있겠지만, 기존의 싱글쓰레드 프로그램에서 성능적으로 손실이 있었기 때문에 정식으로 반영되지 않았다고 합니다. 실제로 GIL을 제거하려는 여러 프로젝트(대표적으로 <a href="https://github.com/larryhastings/gilectomy/tree/master" target="_blank" rel="noopener noreferrer">Giletomy</a>)들이 있었지만, 3.13까지는 정식적으로 반영될 정도로 효과적이진 않았던 것 같습니다.
하지만 GIL 없이 멀티 쓰레딩을 사용가능하다면, 확실히 개선할 수 있는 포인트들이 있을 것입니다. 쓰레드 간 메모리를 공유한다는 점을 활용하면 메모리 공간을 더 효율적으로 사용할 수 있고, 멀티프로세스 사용시 프로세스 간에 자원을 공유해야하는 부담도 덜 수 있습니다.
이 GIL 제거는 기존의 reference counting을 <code>biased referencing</code>, <code>immortialization</code>을 통해 가능해졌다고 합니다. 자세한 사항은 글이 길어져 생략하지만, 락을 걸지 않고, 어떻게 쓰레드 간의 Race Condition을 해결할지에 초점을 두고 키워드들을 살펴보시면 좋을 것 같습니다.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="outro">Outro<a href="https://watanka.github.io/GIL%EC%9D%B4_%EB%AD%90%EA%B8%B8%EB%9E%98#outro" class="hash-link" aria-label="Outro에 대한 직접 링크" title="Outro에 대한 직접 링크">​</a></h3>
<p>지금까지 GIL이 무엇인지, 왜 파이썬에 GIL이 생긴 건지, 그리고 파이썬에서는 이런 제약을 어떻게 해결해왔는지에 대하여 알아보았습니다.
기존의 파이썬 생태계에서 어느정도의 최적화가 이루어진 상황이지만, no gil 도입을 통해 어떤 식으로 더 개선될 수 있을지 기대됩니다.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[DEVIEW 2023 "ML/AI 개발자를 위한 단계별 Python 최적화 가이드라인"을 보고]]></title>
            <link>https://watanka.github.io/deview2023-ml-ai-개발자를-위한-단계별-python-최적화-가이드라인을-보고</link>
            <guid>https://watanka.github.io/deview2023-ml-ai-개발자를-위한-단계별-python-최적화-가이드라인을-보고</guid>
            <pubDate>Sun, 04 Jun 2023 00:00:00 GMT</pubDate>
            <description><![CDATA[영상 링크 : ML/AI 개발자를 위한 단계별 Python 최적화 가이드라인]]></description>
            <content:encoded><![CDATA[<p>영상 링크 :&nbsp;<a href="https://www.youtube.com/watch?v=b0kFuRho27U" target="_blank" rel="noopener noreferrer">ML/AI 개발자를 위한 단계별 Python 최적화 가이드라인</a></p>
<p>유튜브 추천 동영상에 흥미로운 영상이 올라왔다.</p>
<br>
<p><em><strong>"ML/AI 개발자를 위한 단계별 Python 최적화 가이드라인"</strong></em></p>
<p><strong>네이버 파파고 OCR팀 문주혁 님의 DEVIEW 2023 영상이다.</strong></p>
<p>OCR, 이미지에서 텍스트를 탐지하는 단계에서, 텍스트 박스 갯수가 많아지면 속도가 느려지는 이슈를 어떻게 해결했는지에 대해 이야기한다. 전에 일했던 회사에서 똑같이 겪었던 이슈였기 때문에 집중해서 봤다. 영상을 보고 내 나름대로 문제 해결을 위해 발표자분이 어떤 식으로 접근했는지 정리해보았다. 기술적인 부분보다는&nbsp;<strong>문제를 해결하기 위한 자세에 초점</strong>을 두고 글을 작성했다.</p>
<p><img decoding="async" loading="lazy" src="https://velog.velcdn.com/images/silvercity/post/5146f5e8-fe68-461a-ab63-8fd7025d6c64/image.png" alt="https://velog.velcdn.com/images/silvercity/post/5146f5e8-fe68-461a-ab63-8fd7025d6c64/image.png" class="img_ev3q"></p>
<ol>
<li>문제 배경</li>
<li>문제 해결 방법</li>
<li>내가 시도해볼 것(Takeaway)</li>
</ol>
<br>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="문제-배경">문제 배경<a href="https://watanka.github.io/deview2023-ml-ai-%EA%B0%9C%EB%B0%9C%EC%9E%90%EB%A5%BC-%EC%9C%84%ED%95%9C-%EB%8B%A8%EA%B3%84%EB%B3%84-python-%EC%B5%9C%EC%A0%81%ED%99%94-%EA%B0%80%EC%9D%B4%EB%93%9C%EB%9D%BC%EC%9D%B8%EC%9D%84-%EB%B3%B4%EA%B3%A0#%EB%AC%B8%EC%A0%9C-%EB%B0%B0%EA%B2%BD" class="hash-link" aria-label="문제 배경에 대한 직접 링크" title="문제 배경에 대한 직접 링크">​</a></h3>
<hr>
<p>파이썬은 딥러닝 모델을 개발하기 위한 생태계가 잘 구성되어있다. GPU를 활용하여 딥러닝 모델을 학습/평가/서빙할 수 있는 pytorch, tensorflow 등 여러 프레임워크들과 고차원의 행렬 데이터들을 처리할 수 있는 pandas나 numpy와 같은 라이브러리들이 있다.</p>
<p>문제는 파이썬이 다른 언어들보다 느리다는 점이다. 모델 개발이 끝난 후, 딥러닝 모델의 앞단과 끝단에서 데이터를 처리하는 전처리나 후처리 또는 다른 부분들에서 속도가 느린 걸 발견할 수도 있다.</p>
<p>데이터 사이언티스트들은 보통 CS 백그라운드가 다른 개발자들보다 강한 편은 아니다. (물론 케바케겠지만) 내 첫번째 컴퓨터 언어는 파이썬이고, 비교적 최근에야 C나 C++ 같은 언어에 관심을 갖게 되었지만, 개인적으로 생각하기에 코드를 보고 '이런 부분을 고치면 조금 더 속도가 개선되겠는걸? 아 여기서 병목현상이 발생하는구나' 같은&nbsp;<strong>컴퓨터 내부 동작 원리를 건드리는</strong>(<strong>또는 컴퓨터 내부 동작 원리를 알아야만 가능하다고 생각한</strong>) 생각은 많이 해보지 못한 것 같다.</p>
<p>그래도 완성된 딥러닝 알고리즘이 작동하는 방식을 제일 잘 이해하는 개발자는 모델을 담당한 데이터 사이언티스트 본인일 것이고, 속도가 개선되어야만 한다면 그건 데이터 사이언티스트의 임무일 것이다.</p>
<p>영상에서는 요새 핫한 모델 최적화 기법, 상당히 높은 지식 수준을 요구하는 고난이도 기술 대신, 우리가 시도해볼 수 있는 것 그리고 실질적인 결과물을 낼 수 있는 방법에 대해서 이야기한다.</p>
<br>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="문제-해결방법">문제 해결방법<a href="https://watanka.github.io/deview2023-ml-ai-%EA%B0%9C%EB%B0%9C%EC%9E%90%EB%A5%BC-%EC%9C%84%ED%95%9C-%EB%8B%A8%EA%B3%84%EB%B3%84-python-%EC%B5%9C%EC%A0%81%ED%99%94-%EA%B0%80%EC%9D%B4%EB%93%9C%EB%9D%BC%EC%9D%B8%EC%9D%84-%EB%B3%B4%EA%B3%A0#%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0%EB%B0%A9%EB%B2%95" class="hash-link" aria-label="문제 해결방법에 대한 직접 링크" title="문제 해결방법에 대한 직접 링크">​</a></h3>
<hr>
<div class="codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">측정 =&gt; {문제 개선방법 시도 =&gt; 측정 =&gt; 결과 분석} x repeat =&gt; 문제 개선</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="클립보드에 코드 복사" title="복사" class="clean-btn"><span class="copyButtonIcons_eSgA" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_y97N"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_LjdS"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<br>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="측정">측정<a href="https://watanka.github.io/deview2023-ml-ai-%EA%B0%9C%EB%B0%9C%EC%9E%90%EB%A5%BC-%EC%9C%84%ED%95%9C-%EB%8B%A8%EA%B3%84%EB%B3%84-python-%EC%B5%9C%EC%A0%81%ED%99%94-%EA%B0%80%EC%9D%B4%EB%93%9C%EB%9D%BC%EC%9D%B8%EC%9D%84-%EB%B3%B4%EA%B3%A0#%EC%B8%A1%EC%A0%95" class="hash-link" aria-label="측정에 대한 직접 링크" title="측정에 대한 직접 링크">​</a></h3>
<p>문제를 해결하기 위해서는 우선 문제가 있다는 사실부터 인정해야한다는 말이 있다.</p>
<p>'속도가 느리다'라는 이슈를 해결하기 위해, 우선 속도 프로파일링을 진행한다.&nbsp;<em><strong>문제 해결을 위한 가장 첫번째 단계는 문제 개선 여부를 측정할 수 있는 지표 설정</strong></em>이다.</p>
<p>파이썬에서 제공하는 timeit함수도 있지만, 코드 곳곳에 일일히 입력해야한다는 단점이 있다. timeit 대신&nbsp;<code>line_profiler</code>라는 라이브러리를 사용했다고 한다. 전체 코드에 대해서 줄줄이 얼마만큼의 시간이 걸렸고, 몇 퍼센트의 비중을 차지하는지 알 수 있다고 한다.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="문제-개선방법-시도">문제 개선방법 시도<a href="https://watanka.github.io/deview2023-ml-ai-%EA%B0%9C%EB%B0%9C%EC%9E%90%EB%A5%BC-%EC%9C%84%ED%95%9C-%EB%8B%A8%EA%B3%84%EB%B3%84-python-%EC%B5%9C%EC%A0%81%ED%99%94-%EA%B0%80%EC%9D%B4%EB%93%9C%EB%9D%BC%EC%9D%B8%EC%9D%84-%EB%B3%B4%EA%B3%A0#%EB%AC%B8%EC%A0%9C-%EA%B0%9C%EC%84%A0%EB%B0%A9%EB%B2%95-%EC%8B%9C%EB%8F%84" class="hash-link" aria-label="문제 개선방법 시도에 대한 직접 링크" title="문제 개선방법 시도에 대한 직접 링크">​</a></h3>
<p>해결해야하는 문제가 '속도가 느리다'에서 '우리 코드 중 어느 부분이 특히 느리다'로&nbsp;<em><strong>문제의 범위가 좁아졌다</strong></em>. 이제는 문제를 개선할 수 있는 방법들을 나열한다. 무작정 이 방법, 저 방법 해보자 보다는&nbsp;<em><strong>여러 측면/레벨에서 시도해볼 수 있는 것들을 체계화하는 것이 중요</strong></em>하다. 영상에서는 속도 개선을 위해 python level, semi-c level, c/c++ level 세 가지로 나누어 접근했다.</p>
<p><img decoding="async" loading="lazy" src="https://velog.velcdn.com/images/silvercity/post/51584687-ccf1-401b-80a3-e973c2ac4d33/image.png" alt="https://velog.velcdn.com/images/silvercity/post/51584687-ccf1-401b-80a3-e973c2ac4d33/image.png" class="img_ev3q"></p>
<p><strong>문제 개선방법 - python Level</strong></p>
<p><em><strong>문제 해결 방법들을 나열했다면, 가장 시도해보기 쉬운 것부터 시도</strong></em>해본다. 실패했을 때 비용이 적고, 가볍게 직접 시도해보면서 해결하고자 하는 문제가 조금 더 구체화된다는 장점이 있다. 문제 정의 시에 알지 못했던 부분에 대해 새롭게 알게 된다거나, 새로운 해결방법들을 추가해볼 수도 있다.</p>
<p>numpy나 opencv는 c와 c++로 작성된 라이브러리로 파이썬에서 작동할 수 있는 최적 속도를 어느정도 보장받는다. 그렇다고 해서 해당 라이브러리로 작성된 코드가 작성할 수 있는 최적의 속도라는 의미는 아니다. 개선할 수 있는 부분이 있을지도 모른다.</p>
<p>다시 한번, 문제 해결 방법은 여러가지가 있겠지만, 가장 시도해보기 쉬운 것부터 시도하는 것이 중요하다. numpy나 opencv에서 개선할 수 있는 부분을 찾으라고 해서, 안에 구동되는 동작원리를 파악하고, c++ 코드를 재작성하라는 의미가 아니다. 가장 쉽게 시도해볼 수 있는 방법은 '같은 라이브러리 내에서 코드 문맥에 조금 더 적합한 함수가 있을 수 있다. 더 알맞은 함수로 변경해보자'이다.</p>
<p><strong>문제 개선방법 - Semi-c Level</strong></p>
<p>파이썬 속도 개선을 위해 검색을 하다보면 항상 나오는 키워드가 있다. cython이나 numba. 결론만 말하자면, "pure python, 즉 라이브러리를 사용하지 않은 파이썬 자체 코드일 경우 눈에 띄는 성능 개선을 확인할 수 있지만, 다른 언어로 작성되어 이미 빠른 라이브러리를 사용할 경우, 그다지 좋은 성능 개선은 기대하기 힘들다"고 한다. 여기서 내가 생각하기에 중요한 부분은&nbsp;<strong>'좋다고 해서 써봤는데?"라는 생각으로 시도한 방법에 대한 대응 방식</strong>이다.</p>
<p><img decoding="async" loading="lazy" src="https://velog.velcdn.com/images/silvercity/post/ee84945e-32af-4c3e-b1ae-e3230ec5e4ba/image.png" alt="https://velog.velcdn.com/images/silvercity/post/ee84945e-32af-4c3e-b1ae-e3230ec5e4ba/image.png" class="img_ev3q"></p>
<p>시도한 방법은 생각보다 결과가 좋을 수도 있고, 좋지 않을 수도 있다. 하지만, 결과의 퀄리티를 떠나, 결과를 분석하는 단계는 항상 있어야한다. cython이나 numba를 시도하는 방법에 있어서, '파이썬 자체 코드 =&gt; 성능 개선 우수, 라이브러리 코드 =&gt; 성능 개선 미미'라는 결론에 다다르기까지는 여러번의 실험과 분석이 있었을 것이다(추측이긴 하지만). 한 번에 이런 결론에 다다르면 정말 좋겠지만, 배경지식이 없는 상태에서 실험 후에 명료한 결론을 내리는 것은 생각보다 쉬운 일이 아니라고 생각한다.&nbsp;<em><strong>여러번 걸릴 수도 있겠지만, 각 시도마다 결과를 분석하고, 나름의 결론과 경험치를 쌓아서 실용적인 결론에 다다르는 것이 중요</strong></em>하다.</p>
<p>('시도해봤는데 결과가 잘 안나왔다. 이 방법은 구리다' 같은 결론이 아니라, 영상에서처럼 '이 방법은 이럴 때는 잘 작동하지만, 저럴 때는 잘 작동하지 않을 수도 있으니 참고하세요.' 같은 실용적인 결론이다.)</p>
<br>
<p><strong>문제 개선방법 - C/C++ Level</strong></p>
<p><img decoding="async" loading="lazy" src="https://velog.velcdn.com/images/silvercity/post/89a1d1bb-c2e1-4118-ba69-20834eb4f066/image.png" alt="https://velog.velcdn.com/images/silvercity/post/89a1d1bb-c2e1-4118-ba69-20834eb4f066/image.png" class="img_ev3q"></p>
<p><img decoding="async" loading="lazy" src="https://velog.velcdn.com/images/silvercity/post/ef040d8e-7c41-40f9-a410-d42e1c5bf283/image.png" alt="https://velog.velcdn.com/images/silvercity/post/ef040d8e-7c41-40f9-a410-d42e1c5bf283/image.png" class="img_ev3q"></p>
<p>만약 C/C++ implementation을 적용하기로 결정했다면, '시도해보기 쉬운 것부터 시도' 원칙이 다시 한번 적용된다. 전체 코드를 바꾸는 대신, 시간이 오래 걸리는 병목 함수를 찾고 해당 부분만 C/C++ 구현을 적용한다. 여기서 내가 생각하기에 중요한 부분은 두 가지였다.</p>
<p>첫째는&nbsp;<em><strong>새로운 코드 적용이 항상 공식 문서에 제공된 example과 같이 딱 들어맞지는 않을 것이기 때문에, 내 상황에 맞게 활용하는 유연성이 필요하다</strong></em>는 점이다. 영상에서는 numpy의 ndarray를 C에서 지원하는&nbsp;<code>cv::Mat</code>&nbsp;형식으로 변환하기 위해 Numpy C api에서 제공하는&nbsp;<code>PyArray_FromAny</code>와 c++에서 제공하는&nbsp;<code>PyArrayObject</code>를 거친다. '어? 내가 원하는 기능을 지원 안 하네?'하고 바로 포기해버리기보다는 조금 돌아가더라도 어떻게 해결할 수 있을지 고민해볼 필요가 있다.</p>
<p>두번째는&nbsp;<em><strong>직접 해봐야 안다</strong></em>는 점이다. 위에서 언급했다시피, numpy와 opencv 같은 라이브러리들은 C/C++로 구현되어 있기 때문에 어느정도 최적의 속도를 보장한다. 그렇다면, 해당 라이브러리로 작성한 코드 속도가 무조건 C/C++로 구현한 속도와 동일할까? 아니다. 영상에서는 라이브러리로 작성한 파이썬 코드를 동일한 로직의 C/C++로 변환했을 때, 50%에 가까운 속도 향상을 얻었다고 한다. 개발을 하다보면 이론과는 다른 결과가 발생하기도 한다. 이론을 맹신하지말고, 직접 시도해보는 과정이 필요하다.</p>
<p>(문제 해결을 위해 돌아서 풀어가는 과정(workaround)에 길을 잃지 않기 위해서는 기초지식과 다루고 있는 문제의 본질에 대한 이해가 필요하다. 또, 이론이 전부가 아니다. 직접 시도해보는 과정이 필요하다.)</p>
<br>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="내가-시도해볼-것들takeaway">내가 시도해볼 것들(Takeaway)<a href="https://watanka.github.io/deview2023-ml-ai-%EA%B0%9C%EB%B0%9C%EC%9E%90%EB%A5%BC-%EC%9C%84%ED%95%9C-%EB%8B%A8%EA%B3%84%EB%B3%84-python-%EC%B5%9C%EC%A0%81%ED%99%94-%EA%B0%80%EC%9D%B4%EB%93%9C%EB%9D%BC%EC%9D%B8%EC%9D%84-%EB%B3%B4%EA%B3%A0#%EB%82%B4%EA%B0%80-%EC%8B%9C%EB%8F%84%ED%95%B4%EB%B3%BC-%EA%B2%83%EB%93%A4takeaway" class="hash-link" aria-label="내가 시도해볼 것들(Takeaway)에 대한 직접 링크" title="내가 시도해볼 것들(Takeaway)에 대한 직접 링크">​</a></h3>
<p>영상을 보고 배운 점을 정리하자면 다음과 같다.</p>
<ul>
<li>최적화를 위해 컴퓨터 내부 동작 원리를 알아야만 가능한 것은 아니다.</li>
<li>문제 해결은 <code>측정 -&gt; 해결 방법 시도 -&gt; 측정 -&gt; 결과 분석</code> 의 반복으로 이루어진다.</li>
<li>문제 개선 여부를 측정할 수 있는 지표 설정하기</li>
<li>문제 범위를 좁히기</li>
<li>여러 측면/레벨에서 시도해볼 수 있는 것들을 체계화하기</li>
<li>각 시도마다 결과를 분석하고, 나름의 결론과 경험치를 쌓아서 실용적인 결론 내리기</li>
<li>새로운 방법을 내 상황에 맞게 활용하는 유연성 기르기<!-- -->
<ul>
<li>기초지식 갈고닦기</li>
<li>해결하고자하는 문제를 이해하기</li>
</ul>
</li>
<li>이론을 맹신하지 말고, 직접 시도해보기</li>
</ul>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[LLMSummarizer 프로젝트(2) CI/CD 구성]]></title>
            <link>https://watanka.github.io/llmsummarizer-프로젝트2-ci-cd-구성</link>
            <guid>https://watanka.github.io/llmsummarizer-프로젝트2-ci-cd-구성</guid>
            <pubDate>Sun, 15 Jan 2023 00:00:00 GMT</pubDate>
            <description><![CDATA[만족스럽진 않지만, 우선은 동작하는 어플리케이션을 배포했다.]]></description>
            <content:encoded><![CDATA[<p>만족스럽진 않지만, 우선은 동작하는 어플리케이션을 배포했다.</p>
<p>(어플리케이션은 <a href="http://3.39.105.35:8090/" target="_blank" rel="noopener noreferrer">http://3.39.105.35:8090/</a>에서 확인할 수 있다.)</p>
<p>아직 추가해야할 내용도 많고, 개선해야할 내용도 많지만, 한 번에 하기보다는 <strong>하나씩 작업하는 게 중요하다</strong>. 개선과정에 있어서 중요한 것은 자동화할 수 있는 부분은 최대한 자동화해서 개발 싸이클을 개선하는 것이다. 작업하는 내용에만 집중하고, 코드를 관리하기 위해서 Github Issue에 작업할 내용을 기록하고, 브랜치를 파서 작업했다.</p>
<p>현재 AWS LightSail에다가 어플리케이션을 배포하였는데, 이 배포 과정을 Github Action을 사용해서 자동화하려고 한다.</p>
<p>서칭해보니 AWS의 다른 서비스들에서는 배포를 위해 CodeDeploy라는 배포 서비스를 제공하는데, AWS-LightSail은 해당사항이 없다고 한다. 그럼 LightSail은 배포를 자동화를 할 수 없는 걸까?</p>
<p>AWS-LightSail Instance가 EC2와 비슷하다면, AWS-LightSail Container Service는 ECS(container service)와 비슷하다.</p>
<p>aws-cli에서 lightsail로 이미지를 푸쉬하고 배포하는 기능을 제공한다.</p>
<p>EC2를 사용하면 S3에 빌드한 소스를 넘기고, S3에서 CodeDeploy가 변경내용을 가져와서 인스턴스에 적용하는 방식이였는데, LightSail Container Service는 바로 이미지를 푸쉬하고 배포하면 되니 더 간편한 것 같다. (왜 CodeDeploy가 S3를 통해야만 하는지는 아직 잘 모르겠다.)</p>
<p>다시 본론으로 돌아와서,</p>
<p>LightSail Container Service에 배포하기 위해서는 Github Action이 다음과 같은 과정을 거쳐야한다. 여기서 action runner는 깃헙 액션에서 워크플로를 실행하기 위해 제공하는 임시 서버이다.</p>
<ol>
<li>깃헙 체크아웃 - 마스터 브랜치의 변경사항을 action runner에 반영한다.</li>
<li>aws-cli 중 lightsailctl을 사용할 것이므로, action runner에 aws-cli lightsail 플러그인을 설치한다.</li>
<li>도커 이미지를 빌드한다.</li>
<li>빌드한 이미지를 컨테이너 서비스로 푸쉬한다.</li>
<li>푸쉬한 서버를 배포한다.</li>
</ol>
<div class="language-jsx codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-jsx codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token literal-property property" style="color:#36acaa">name</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> lightsail</span><span class="token operator" style="color:#393A34">-</span><span class="token plain">deploy</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token literal-property property" style="color:#36acaa">on</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token literal-property property" style="color:#36acaa">push</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">branches</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token string" style="color:#e3116c">'master'</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token literal-property property" style="color:#36acaa">pull_request</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">branches</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token string" style="color:#e3116c">'master'</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token literal-property property" style="color:#36acaa">permissions</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">contents</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> read</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token literal-property property" style="color:#36acaa">env</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token constant" style="color:#36acaa">LIGHTSAIL_SSH_KEY</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> $</span><span class="token punctuation" style="color:#393A34">{</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> secrets</span><span class="token punctuation" style="color:#393A34">.</span><span class="token constant" style="color:#36acaa">LIGHTSAIL_SSH_KEY</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token constant" style="color:#36acaa">LIGHTSAIL_HOST</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> $</span><span class="token punctuation" style="color:#393A34">{</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> secrets</span><span class="token punctuation" style="color:#393A34">.</span><span class="token constant" style="color:#36acaa">LIGHTSAIL_HOST</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token constant" style="color:#36acaa">OPENAI_API_KEY</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> $</span><span class="token punctuation" style="color:#393A34">{</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> secrets</span><span class="token punctuation" style="color:#393A34">.</span><span class="token constant" style="color:#36acaa">OPENAI_API_KEY</span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token constant" style="color:#36acaa">LIGHTSAIL_USERNAME</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> ubuntu</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token constant" style="color:#36acaa">LIGHTSAIL_SERVICE_NAME</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> llmsummarizer</span><span class="token operator" style="color:#393A34">-</span><span class="token plain">container</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token constant" style="color:#36acaa">AWS_REGION</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> ap</span><span class="token operator" style="color:#393A34">-</span><span class="token plain">northeast</span><span class="token operator" style="color:#393A34">-</span><span class="token number" style="color:#36acaa">2</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token literal-property property" style="color:#36acaa">jobs</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token literal-property property" style="color:#36acaa">buildfastapi</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">name</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token maybe-class-name">Building</span><span class="token plain"> </span><span class="token maybe-class-name">FastAPI</span><span class="token plain"> app</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    runs</span><span class="token operator" style="color:#393A34">-</span><span class="token plain">on</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> ubuntu</span><span class="token operator" style="color:#393A34">-</span><span class="token plain">latest</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">steps</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">			# </span><span class="token number" style="color:#36acaa">1.</span><span class="token plain"> 깃헙 체크아웃 </span><span class="token operator" style="color:#393A34">-</span><span class="token plain"> 마스터 브랜치의 변경사항을 action runner에 반영한다</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain"> </span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token operator" style="color:#393A34">-</span><span class="token plain"> name</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token maybe-class-name">Getting</span><span class="token plain"> </span><span class="token maybe-class-name">Code</span><span class="token plain"> </span><span class="token keyword module" style="color:#00009f">from</span><span class="token plain"> </span><span class="token maybe-class-name">Github</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token literal-property property" style="color:#36acaa">uses</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> actions</span><span class="token operator" style="color:#393A34">/</span><span class="token plain">checkout@v4</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token operator" style="color:#393A34">-</span><span class="token plain"> name</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token maybe-class-name">Updating</span><span class="token plain"> to the latest versions</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token literal-property property" style="color:#36acaa">run</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">|</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">          sudo apt</span><span class="token operator" style="color:#393A34">-</span><span class="token keyword" style="color:#00009f">get</span><span class="token plain"> update</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">          sudo apt</span><span class="token operator" style="color:#393A34">-</span><span class="token keyword" style="color:#00009f">get</span><span class="token plain"> install </span><span class="token operator" style="color:#393A34">-</span><span class="token plain">y jq unzip</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">			# </span><span class="token number" style="color:#36acaa">2.</span><span class="token plain"> aws</span><span class="token operator" style="color:#393A34">-</span><span class="token plain">cli 중 lightsailctl을 사용할 것이므로</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> action runner에 aws</span><span class="token operator" style="color:#393A34">-</span><span class="token plain">cli lightsail 플러그인을 설치한다</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token operator" style="color:#393A34">-</span><span class="token plain"> name</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token maybe-class-name">Install</span><span class="token plain"> </span><span class="token maybe-class-name">Amazon</span><span class="token plain"> </span><span class="token maybe-class-name">Client</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token literal-property property" style="color:#36acaa">run</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">|</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">          curl </span><span class="token string" style="color:#e3116c">"https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip"</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">-</span><span class="token plain">o </span><span class="token string" style="color:#e3116c">"awscliv2.zip"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">          unzip awscliv2</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">zip</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">          sudo </span><span class="token punctuation" style="color:#393A34">.</span><span class="token operator" style="color:#393A34">/</span><span class="token plain">aws</span><span class="token operator" style="color:#393A34">/</span><span class="token plain">install </span><span class="token operator" style="color:#393A34">||</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">true</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">          aws </span><span class="token operator" style="color:#393A34">--</span><span class="token plain">version</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">          curl </span><span class="token string" style="color:#e3116c">"https://s3.us-west-2.amazonaws.com/lightsailctl/latest/linux-amd64/lightsailctl"</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">-</span><span class="token plain">o </span><span class="token string" style="color:#e3116c">"lightsailctl"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">          sudo mv </span><span class="token string" style="color:#e3116c">"lightsailctl"</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"/usr/local/bin/lightsailctl"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">          sudo chmod </span><span class="token operator" style="color:#393A34">+</span><span class="token plain">x </span><span class="token operator" style="color:#393A34">/</span><span class="token plain">usr</span><span class="token operator" style="color:#393A34">/</span><span class="token plain">local</span><span class="token operator" style="color:#393A34">/</span><span class="token plain">bin</span><span class="token operator" style="color:#393A34">/</span><span class="token plain">lightsailctl</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      </span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">			# </span><span class="token number" style="color:#36acaa">3.</span><span class="token plain"> 도커 이미지를 빌드한다</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token operator" style="color:#393A34">-</span><span class="token plain"> name</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token maybe-class-name">Build</span><span class="token plain"> a </span><span class="token maybe-class-name">Docker</span><span class="token plain"> </span><span class="token maybe-class-name">Container</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token literal-property property" style="color:#36acaa">run</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> docker build </span><span class="token operator" style="color:#393A34">-</span><span class="token plain">t llmsummarizer</span><span class="token operator" style="color:#393A34">:</span><span class="token plain">latest </span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      </span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">			#</span><span class="token number" style="color:#36acaa">4.</span><span class="token plain">빌드한 이미지를 컨테이너 서비스로 푸쉬한다</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token operator" style="color:#393A34">-</span><span class="token plain"> name</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> upload image to </span><span class="token maybe-class-name">Lightsail</span><span class="token plain"> container service</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token literal-property property" style="color:#36acaa">run</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">|</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">          service_name</span><span class="token operator" style="color:#393A34">=</span><span class="token plain">$</span><span class="token punctuation" style="color:#393A34">{</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> env</span><span class="token punctuation" style="color:#393A34">.</span><span class="token constant" style="color:#36acaa">LIGHTSAIL_SERVICE_NAME</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">          aws lightsail push</span><span class="token operator" style="color:#393A34">-</span><span class="token plain">container</span><span class="token operator" style="color:#393A34">-</span><span class="token plain">image \</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token operator" style="color:#393A34">--</span><span class="token plain">region $</span><span class="token punctuation" style="color:#393A34">{</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> env</span><span class="token punctuation" style="color:#393A34">.</span><span class="token constant" style="color:#36acaa">AWS_REGION</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"> \</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token operator" style="color:#393A34">--</span><span class="token plain">service</span><span class="token operator" style="color:#393A34">-</span><span class="token plain">name $</span><span class="token punctuation" style="color:#393A34">{</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> env</span><span class="token punctuation" style="color:#393A34">.</span><span class="token constant" style="color:#36acaa">LIGHTSAIL_SERVICE_NAME</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"> \</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token operator" style="color:#393A34">--</span><span class="token plain">label llmsummarizer \</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token operator" style="color:#393A34">--</span><span class="token plain">image llmsummarizer</span><span class="token operator" style="color:#393A34">:</span><span class="token plain">latest</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      </span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token operator" style="color:#393A34">-</span><span class="token plain"> name</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token constant" style="color:#36acaa">AWS</span><span class="token plain"> authentication</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token literal-property property" style="color:#36acaa">uses</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> aws</span><span class="token operator" style="color:#393A34">-</span><span class="token plain">actions</span><span class="token operator" style="color:#393A34">/</span><span class="token plain">configure</span><span class="token operator" style="color:#393A34">-</span><span class="token plain">aws</span><span class="token operator" style="color:#393A34">-</span><span class="token plain">credentials@v1</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">with</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">          aws</span><span class="token operator" style="color:#393A34">-</span><span class="token plain">region</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> $</span><span class="token punctuation" style="color:#393A34">{</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> env</span><span class="token punctuation" style="color:#393A34">.</span><span class="token constant" style="color:#36acaa">AWS_REGION</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">          aws</span><span class="token operator" style="color:#393A34">-</span><span class="token plain">access</span><span class="token operator" style="color:#393A34">-</span><span class="token plain">key</span><span class="token operator" style="color:#393A34">-</span><span class="token plain">id</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> $</span><span class="token punctuation" style="color:#393A34">{</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> secrets</span><span class="token punctuation" style="color:#393A34">.</span><span class="token constant" style="color:#36acaa">AWS_ACCESS_KEY_ID</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">          aws</span><span class="token operator" style="color:#393A34">-</span><span class="token plain">secret</span><span class="token operator" style="color:#393A34">-</span><span class="token plain">access</span><span class="token operator" style="color:#393A34">-</span><span class="token plain">key</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> $</span><span class="token punctuation" style="color:#393A34">{</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain">secrets</span><span class="token punctuation" style="color:#393A34">.</span><span class="token constant" style="color:#36acaa">AWS_SECRET_ACCESS_KEY</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      </span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token operator" style="color:#393A34">-</span><span class="token plain"> name</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token constant" style="color:#36acaa">AWS</span><span class="token plain"> </span><span class="token maybe-class-name">Lightsail</span><span class="token plain"> 연결 확인</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token literal-property property" style="color:#36acaa">run</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> aws configure list</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      #</span><span class="token number" style="color:#36acaa">5.</span><span class="token plain">푸쉬한 서버를 배포한다</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token operator" style="color:#393A34">-</span><span class="token plain"> name</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token maybe-class-name">Launching</span><span class="token plain"> the </span><span class="token maybe-class-name">Containers</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token literal-property property" style="color:#36acaa">run</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">|</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">          aws lightsail create</span><span class="token operator" style="color:#393A34">-</span><span class="token plain">container</span><span class="token operator" style="color:#393A34">-</span><span class="token plain">service</span><span class="token operator" style="color:#393A34">-</span><span class="token plain">deployment </span><span class="token operator" style="color:#393A34">--</span><span class="token plain">service</span><span class="token operator" style="color:#393A34">-</span><span class="token plain">name $</span><span class="token punctuation" style="color:#393A34">{</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> env</span><span class="token punctuation" style="color:#393A34">.</span><span class="token constant" style="color:#36acaa">LIGHTSAIL_SERVICE_NAME</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"> \</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">          </span><span class="token operator" style="color:#393A34">--</span><span class="token plain">containers file</span><span class="token operator" style="color:#393A34">:</span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain">aws</span><span class="token operator" style="color:#393A34">-</span><span class="token plain">lightsail</span><span class="token operator" style="color:#393A34">/</span><span class="token plain">deploymentconfig</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">json</span><span class="token plain"> \</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">          </span><span class="token operator" style="color:#393A34">--</span><span class="token keyword" style="color:#00009f">public</span><span class="token operator" style="color:#393A34">-</span><span class="token plain">endpoint file</span><span class="token operator" style="color:#393A34">:</span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">/</span><span class="token plain">aws</span><span class="token operator" style="color:#393A34">-</span><span class="token plain">lightsail</span><span class="token operator" style="color:#393A34">/</span><span class="token plain">publicendpoint</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">json1</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="클립보드에 코드 복사" title="복사" class="clean-btn"><span class="copyButtonIcons_eSgA" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_y97N"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_LjdS"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>물론 액션이 한 번에 성공하지는 못했다. 예기치 못한 실패를 몇십번 거치고 나서야 액션이 잘 실행된 걸 확인할 수 있었다.</p>
<p><img decoding="async" loading="lazy" alt="Untitled" src="data:image/png;base64,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/kBDg4hqYhdW69A6WiC2ZAsk96rZRSW/SCiDg+FBRNTqtL4AISKiNoEBQkREWhggRESkhQFCRERaGCBERKSFAUJERFoYIEREpIUBQkREWpwSIN9fKkeH2z3UHBFR05F1jaxzqOU5JUDOFBbCz+8uNUdE1HRkXSPrHGp5TgmQT7MPoUevILi6tlMlRETOJ+sYWdfIOodanpNaIH9H7skTGBAWzhAhoiYh6xZZx8i6RtY51PJcXX502ytq+qb8/fSX8PG5EwPDhwA3gCtXruDaP/+plhIR6ZFjHt27ByB0UBgK/56PXZlb1RJqaU75c+62uvv/C+4PHSSe/XG7h/w/tImI9MkBcznmIbut2PJoXZweIERE9MPA74EQEZEWBggREWlhgBARkRYGCBERaWGAEBGRFgYIERFpYYAQEZEWBggREWlhgBARkRYGCBERaWGAEBGRFgYIERFpYYAQEZEWBggREWlhgBARkRYGCBERaWGAEBGRFgYIERFpYYAQEZEWBggREWlhgBARkRYGCBERaWGAEBGRFgYIERFpYYAQEZEWBggREWlhgBARkRYGCBERaWGAEBGRFgYIERFpYYAQEZEWBggREWlhgBARkQbg/wHLe7Cvu4718gAAAABJRU5ErkJggg==" width="400" height="202" class="img_ev3q"></p>
<p>그렇다면 이번엔 라이트세일 페이지에서 배포가 성공적으로 되는 걸 확인한다.</p>
<p>![Untitled](Untitled 1.png)</p>
<p>서버도 무사히 잘 동작하는 걸 확인했다.</p>
<p>배포를 자동화하는 개발환경을 셋팅했으니, 개발속도와 피드백 주기가 조금 더 빨라질 것을 기대해본다. 그럼 이제 추가 기능과 성능을 개선해보자.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[LLMSummarizer 프로젝트(1) - 왜 나는 같은 프로젝트를 다시 진행하게 되었을까?]]></title>
            <link>https://watanka.github.io/llmsummarizer-프로젝트1-왜-나는-같은-프로젝트를-다시-진행하게-되었을까</link>
            <guid>https://watanka.github.io/llmsummarizer-프로젝트1-왜-나는-같은-프로젝트를-다시-진행하게-되었을까</guid>
            <pubDate>Tue, 10 Jan 2023 00:00:00 GMT</pubDate>
            <description><![CDATA[LLMSummarizer는 이미 한 번 진행한 적이 있는 프로젝트이다. langchain-youtube-video-summarizer라는 이름으로 작년 말에 진행하다가 그만두었다. 프로젝트를 제대로 완성시키는 경험을 하고 싶기도 하고, 도대체 뭐가 문제였길래 하는 생각에 같은 주제로 다시 프로젝트를 진행하기로 마음먹었다.]]></description>
            <content:encoded><![CDATA[<p>LLMSummarizer는 이미 한 번 진행한 적이 있는 프로젝트이다. <a href="https://github.com/watanka/langchain-youtube-video-summarizer" target="_blank" rel="noopener noreferrer">langchain-youtube-video-summarizer</a>라는 이름으로 작년 말에 진행하다가 그만두었다. 프로젝트를 제대로 완성시키는 경험을 하고 싶기도 하고, 도대체 뭐가 문제였길래 하는 생각에 같은 주제로 다시 프로젝트를 진행하기로 마음먹었다.</p>
<p>같은 프로젝트를 다시 진행하기 전에 우선 왜 그만두었는지 이유를 곰곰히 고민해보고,  똑같은 실수를 반복하지 않도록 하고싶다.</p>
<p>이전 프로젝트를 그만두게 된 원인을 몇가지 꼽아보자면 다음과 같다.</p>
<ol>
<li>
<p>코드 구조를 미리 설계하고 시작했다.</p>
<p>프로젝트를 진행하기 위해서 미리 설계하고 시작하는 것은 중요하다. 코드 구조가 잡혔는데 새로운 기능들을 추가하다보면, 코드가 금방 지저분해질 것 같다는 생각이 들었다. 그래서 디렉토리 구조를 미리 나눠놓고, 거기에 코드들을 끼워맞췄다. 틀을 정해놨으니, 그 안에 맞추면 된다는 생각이였다. 하지만, 이상하게도 코드 라인이 많아지면 많아질수록, 관리하기 힘들어졌고, 뭐 하나 쉽사리 변경하기 어려워졌다.</p>
<p><strong>코드 구조는 정해져있는 절대적인 답이 있는 것이 아니라, 그 쓰임새에 맞게 합리적으로 조금씩 변화해가는 과정을 통해 결정된다.</strong> 구조에 대한 고민은 프로젝트 초기에 한 번 하고 마는 것이 아니라, 코드를 작성하면서 끊임없이 고민해야된다는 것이였다. 코드가 돌아간다고 괜찮다 생각하지말고, 나중에 다시 봤을 때, 혹은 전체 구조에서 알맞는 코드인지 가독성이 좋은지 주기적으로 검토하는 것이 중요하다. 마틴 파울러의 ‘클린 아키텍쳐’는 확장성이 좋고, 유지 보수성이 좋은 코드를 작성하기 위한 해법들을 잘 알려준다. 그리고 테스트 코드를 작성하면, 변경사항에도 안전성을 확보할 수 있다고 한다. 이번 프로젝트에서는 테스트 코드를 작성하고, 클린 아키텍쳐에서 말하는 내용을 잘 숙지하면서 진행해보려고 한다.</p>
</li>
<li>
<p>프로젝트에서 샛길로…</p>
<p>진행하면서 초기단계에서는 미처 생각하지 못 했던 사항들이 발생했다. 예를 들면, 음성파일을 텍스트파일로 변환하는 whisper 모델 같은 경우에는 직접 서빙을 목표로 했었는데, 서빙을 위한 클라우드 비용을 알아보니, 운영이 도저히 힘든 가격이였다. 이럴 때는 조금 찝찝하더라도, <strong>애초에 계획한 목표에 도달하기 위해 우선 가장 빠르고 쉬운 방법을 택하는 게 맞지 않을까</strong> 라는 생각을 지금에서라도 해본다. 그 당시에는 어떻게든 이 문제를 해결해야만 다음 단계로 넘어갈 수 있을 거라는 생각이였다. 결과적으로 GPU이던 CPU이던 whisper를 로컬로 서빙할 수 있는 방법은 찾지 못하였고, 계획한 목표에도 달성하지 못했다. 결과만 봤을 때는 시간만 낭비한 셈이다. 우선은 openai에서 제공하는 api로 목표한 요약 기능을 구현하고, 추가 개선을 하는 상황에서 다시 접했다면 어땠을까 하는 생각이 든다. 심리적으로도 결과물이 이미 있으니, 조금 덜 부담을 가지고 문제를 해결할 수 있지 않았을까 아쉬운 마음이다. 기술부채가 나쁜 것만은 아니다. 상황에 맞게 현명한 판단을 하는 게 중요하다. 이번 프로젝트에서는 미흡하더라도 우선은 완성을 하고, 조금씩 개선해나가는 것이 목표이다.</p>
</li>
</ol>
<p>그래서 우선 돌아가는 어플리케이션을 작성했다. 이미 이전 프로젝트에서 작성해놓은 코드들을 가지고와서 확장성을 고려하여 작성했다. 인풋에 대한 전처리와 요약 과정이 혹시 변경될 수 있을지 모르니, Inputhandler와 mapreducer를 추상화하여 변경 사항에 대처할 수 있도록 했다. 어플리케이션 배포는 AWS의 lightsail 인스턴스에다가 했다. 3개월 무료라는 점과 lighsail이 제공하는 간편성 그리고 서버 규모가 커지면 EC2로 손쉽게 옮길 수 있다는 점에서 채택했다.</p>
<p>어플리케이션은 <a href="http://3.39.105.35:8090/" target="_blank" rel="noopener noreferrer">http://3.39.105.35:8090/</a> 여기에서 확인할 수 있다.</p>
<p>물론 고칠 점이 많다. 하나하나씩 개선해나갈 예정이다.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[실험을 잘한다는 것]]></title>
            <link>https://watanka.github.io/실험을-잘한다는-것</link>
            <guid>https://watanka.github.io/실험을-잘한다는-것</guid>
            <pubDate>Sun, 11 Dec 2022 00:00:00 GMT</pubDate>
            <description><![CDATA[if(kakao)dev2022 발표영상]]></description>
            <content:encoded><![CDATA[<p><a href="https://if.kakao.com/2022/session/4" target="_blank" rel="noopener noreferrer">if(kakao)dev2022 발표영상</a></p>
<p>카카오 개발자 컨퍼런스 if(kakao) dev 2022가 12월 7일부터 9일까지 열렸다. 이번에도 역시 흥미로운 주제들을 다룬 세션들이 많았는데(얼마전에 있었던 카카오 서버 다운 사건에 대해서도), 그 중 내 관심을 가장 끈 주제는 ‘실험을 잘한다는 것은 무엇일까’. 카카오 추천팀의 개발자 분께서 발표한 세션이였다.</p>
<p>업무에 딥러닝을 활용하는 개발자로써(딥러닝 개발자, 딥러닝 엔지니어, 딥러닝 리서치 사이언티스트 등등 다양한 텀이 있지만, 정확히 내가 어떤 범주에 속하고, 그리고 또 속하고싶은지는 아직 잘 모르겠다.) 성장하기 위해서 어떤 역량들이 필요한지 항상 고민하곤 한다.</p>
<p>빠른 구현을 위한 프로그래밍 능력, 당면한 문제 해결을 위해 딥러닝을 적절히 활용하는 창의성, 매번 업데이트되는 최신 트렌드를 잘 반영하여 코드와 모델의 성능을 기록하고 정리하는 능력 등등 정말 다양한 분야의 역량이 필요한 딥러닝이지만, 그 중 실무에서 가장 중요한 덕목은 아무래도 ‘실험을 잘하는 것’이라는 생각을 한 적이 있다. 그래서 많은 세션들 중 단연 ‘실험을 잘한다는 것은 무엇일까’라는 타이틀은 더욱 눈에 띄었다.</p>
<p>불확실성을 전제로 하는 머신러닝 특성상 100%의 정확도, 100%의 효율은 없다. 다만, 전세계에서 부리나케 연구되는 학문이라 빠른 페이스로 트렌드가 바뀌고 새로운 기술들이 세상에 나온다. 기존 솔루션이 조금은 부족했던 부분 플러스 ‘더 좋은 게 나왔다던데?’라는 소식을 들으면, 이제 개발자는 실험 모드에 들어가게 된다. 새로운 모델(여기선 모델이라고 뭉뚱그렸지만, 학습 방법, 데이터 처리 등 머신러닝에 관련된 다양한 기술을 말한다)이 ‘기존 모델보다 실제로 나은지’를 실험으로 입증해야하는 것이다.</p>
<p>물론 실험 결과가 기존 솔루션보다 낫다라는 결론이 나면 해피엔딩이다. 문제는 새로운 모델의 결과가 애매할 때이다. (원래 있던 문제는 해결하는데, 없던 문제가 생겼다거나. 분명 정확도는 좋은데, 모델 연산이 너무 무거워 해결하고자하는 문제에는 투머치라거나. 정말 다양한 방면에서 애매할 수 있다.) 그럴 때는 두 가지 옵션으로 정리가 가능하다고 한다. 솔루션을 포기하거나 아니면 보완하거나.</p>
<br>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="옵션1-솔루션-포기">옵션1. 솔루션 포기<a href="https://watanka.github.io/%EC%8B%A4%ED%97%98%EC%9D%84-%EC%9E%98%ED%95%9C%EB%8B%A4%EB%8A%94-%EA%B2%83#%EC%98%B5%EC%85%981-%EC%86%94%EB%A3%A8%EC%85%98-%ED%8F%AC%EA%B8%B0" class="hash-link" aria-label="옵션1. 솔루션 포기에 대한 직접 링크" title="옵션1. 솔루션 포기에 대한 직접 링크">​</a></h3>
<p>솔루션을 포기하기로 결정했다는 것은 그동안 해당 솔루션을 위해 투자한 시간을 (어떤 의미에서는) 확실한 성과가 없이 버려야하는 것과 마찬가지다. 확실한 성과가 없으면 실험자 본인이 지치기는 물론이고, 실적을 내야하는 회사에서 살아남기는 힘들다. 그렇다고 해서 솔루션 포기가 그동안 했던 것들이 아예 티끌로 사라지는 것은 아니다. 진행한 실험 결과로부터 어떤 교훈을 얻을 수 있을지, 즉 ‘어떻게 다음 실험에도 도움이 될 수 있을까‘를 남기는 것이 중요하다고 한다. 실험을 반복할수록 경험이 쌓이고, ‘이 방향은 아니야’, ‘이 방향은 더 파볼만 한데?’하는 소위 말해 짬(?)이 생긴다. 이런 경험들을 어떻게 본인의 발전 뿐만 아니라 팀원들에게 도움이 될 수 있는 방향으로 기록하여 ‘팀의 발전’으로 기여할 수 있는지가 ‘실험을 잘 하는 기준’의 하나가 될 수 있지 않을까 싶다.</p>
<p>또한, 성능을 개선하기 위한 다른 솔루션을 찾아야한다. 다른 솔루션을 찾아야한다는 것은 지금 투자한만큼의 시간이 다시 필요할 수도 있음을 의미하고, 더욱 최악인 것은 다음 솔루션이 이번 솔루션처럼 ‘포기’로 돌아갈 수도 있음을 인지해야한다는 것이다. 또, 해당 솔루션을 애초에 선택한 이유는 ‘성능이 좋다더라’하는 이유에서였을텐데, 이렇게 두세번 찾은 솔루션의 성능이 애매한 것을 확인하게 되면, 어느 기준으로 새로운 다른 솔루션을 찾아야하는지 막막해지기도 한다. (나도 논문을 볼 때, ‘이 퍼블릭 데이터셋에 대해서 이만큼 엄청난 성능을 보였다!’ 하는 부분에 대해서는 연차가 쌓일수록 무덤덤해지는 것 같다. 이렇게 연구결과를 믿기 힘든 부분은 머신러닝의 특성상 아마도 실험 셋팅이 회사, 기관, 연구소마다 다른 탓과, 실무에서 사용되는 데이터와 학회에서 사용되는 데이터 간의 격차가 커서일 듯 싶다.)</p>
<p>포기한 솔루션은 내가 해결하지 못하였다고 접는 것이 아니라, 남들에게도 매력적인 솔루션으로 느껴진다면 계속해서 시도될 것이라고 한다. 마치 원탁의 기사에서 나오는 ‘엑스칼리버’처럼. (효율이 중요하고 영업이익을 내야하는 회사이지만, 구성원들이 매력적인 솔루션들을 충분히 시도해볼 수 있도록 하고, 실패하더라도 커뮤니케이션을 통해 팀 전체의 성장을 위해 교훈을 남기는 회사가 정말 좋은 회사가 아닐까 싶다.)</p>
<br>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="옵션2-솔루션-보완">옵션2. 솔루션 보완<a href="https://watanka.github.io/%EC%8B%A4%ED%97%98%EC%9D%84-%EC%9E%98%ED%95%9C%EB%8B%A4%EB%8A%94-%EA%B2%83#%EC%98%B5%EC%85%982-%EC%86%94%EB%A3%A8%EC%85%98-%EB%B3%B4%EC%99%84" class="hash-link" aria-label="옵션2. 솔루션 보완에 대한 직접 링크" title="옵션2. 솔루션 보완에 대한 직접 링크">​</a></h3>
<p>솔루션을 보완하기로 마음먹었다면, 어떤 부분들을 보완할지 리스트업하게 될 것이다. 때때로 주어진 시간에 비해 보완해야할 부분이 너무 많아 어디서부터 시작해야할지조차 모를 때가 있는데, 그럴 때일수록 우선 순위를 설정하고 그에 맞게 하나하나 차근차근 고쳐나가는 것이 중요하다고 한다. 그렇게 우선순위를 설정하면, 보완하고 실험하고 실패하고를 반복하며 솔루션을 보완해나간다. 한 번만에 원하는 결과가 나올수도 있고, 수십번해도 안 될 수도 있다. 뚝심있게 실험을 풀어나가는 것도 중요하지만, 실험이 반복될수록 ‘매몰 비용이 발생’한다는 것을 인지해야한다고 한다. 어떻게 될 것 같은데… 될 것 같은데… 싶은 마음은 알겠지만, 물러서야할 때를 알아야한다는 것이다. 이 물러서야할 때를 알기 위해, 실험 전 ‘마무리 조건’을 설정해놓으라고 한다. (이 ‘마무리 조건’을 어떻게 정해야할지는 아직 잘 모르겠다. 사실 실험이 원하는 방향으로 진행이 안되면 찜찜하고 될 때까지 해내고 싶은데, 어떻게 마무리 조건을 설정해야 이만큼 했으면 할만큼 했다 싶은 생각이 들지 잘 모르겠다.)</p>
<p>결론적으로 실험을 하면서, 아래와 같은 질문들에 대한 대답이 물흐르듯이 나온다면 아마 실험을 잘 진행하고 있는 걸 거라고 한다.</p>
<p>Q1. 실험 결과를 어떻게 남겨야 다음 실험에 도움이 될 수 있을까?</p>
<p>Q2. 보완 실험은 어디서부터 진행하는 게 좋을까? (보완하면 좋을 점이 너무 많이 보인다.)</p>
<p>Q3. 마무리 조건을 어떻게 세우는 게 좋을까?</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="실험을-잘한다는-것">실험을 잘한다는 것<a href="https://watanka.github.io/%EC%8B%A4%ED%97%98%EC%9D%84-%EC%9E%98%ED%95%9C%EB%8B%A4%EB%8A%94-%EA%B2%83#%EC%8B%A4%ED%97%98%EC%9D%84-%EC%9E%98%ED%95%9C%EB%8B%A4%EB%8A%94-%EA%B2%83" class="hash-link" aria-label="실험을 잘한다는 것에 대한 직접 링크" title="실험을 잘한다는 것에 대한 직접 링크">​</a></h3>
<p>실험을 잘 한다는 것은 ‘승부수’가 있다는 것이라고 한다. 그리고 이 승부수는 이 실험이 성공할 거라고 생각하는 이유라고 한다. 단지 ‘이번에 나온 논문의 성능이 좋다던데?’라는 이유보다는 ‘학습용 데이터가 부족한 우리 문제에 소량의 데이터만으로도 이 정도 성능을 뽑는 논문이 나왔대’라는 이유라면, 실험이 성공할 확률이 높다는 것이다. 좀 더 일반화하자면, ‘문제 환경(Environment)과 문제 해결의 핵심이 되는 특징(Key Feature)이 들어맞을 때, 솔루션이 적합하다.’라고 한다.</p>
<p>승부수에 따른 실험 결과 분석</p>
<ol>
<li>key feature 구현을 제대로 했는지 확인</li>
<li>key feature가 실제로 문제 해결에 도움이 되는지 확인</li>
</ol>
<p>승부수가 있으면, 실험 결과 분석이 쉬워진다고 한다. ‘승부수가 되는 key feature 구현이 제대로 되었는지 확인한다’와 ‘key feature가 실제로 문제 해결에 도움이 되는지 확인한다.’라는 두 가지 가이드라인이 생겼기 때문이다. 이 다음 스텝은 두 가지 가이드라인으로부터 차근차근 밟아나가면 된다. 만약 key feature 구현은 제대로 되었는데, 문제 해결에 도움이 되지 않는다면, 내 가설과 실제 상황이 다르다는 것을 의미함으로, 어떻게 다른지 비교 분석해나가다보면, 다른 실마리가 보일 것이라고 한다. 승부수는 실험을 위해 어느 부분에 집중해야할지 알기 때문에, 실험 사이클도 짧게 가져갈 수도 있다고 한다. 또 승부수는 위의 세가지 질문들에 대한 답변도 될 수 있다고한다. 예를 들어, 마무리 조건을 어떻게 설정해야할지 모를 때는 ‘key feature’를 올바르게 구현하고, ‘key feature’가 실제 문제 해결에 도움이 되는지 확인하는 것으로 마무리 조건을 설정하면 된다고 한다.</p>
<p>Q1. 실험 결과를 어떻게 남겨야 다음 실험에 도움이 될 수 있을까?</p>
<p>Q2. 보완 실험은 어디서부터 진행하는 게 좋을까? (보완하면 좋을 점이 너무 많이 보인다.)</p>
<p>Q3. 마무리 조건을 어떻게 세우는 게 좋을까?</p>
<p>⇒ Key Feature에 기반해서 판단하면 된다!</p>
<br>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="승부수를-잘-세우려면">승부수를 잘 세우려면?<a href="https://watanka.github.io/%EC%8B%A4%ED%97%98%EC%9D%84-%EC%9E%98%ED%95%9C%EB%8B%A4%EB%8A%94-%EA%B2%83#%EC%8A%B9%EB%B6%80%EC%88%98%EB%A5%BC-%EC%9E%98-%EC%84%B8%EC%9A%B0%EB%A0%A4%EB%A9%B4" class="hash-link" aria-label="승부수를 잘 세우려면?에 대한 직접 링크" title="승부수를 잘 세우려면?에 대한 직접 링크">​</a></h3>
<ol>
<li>내가 풀려는 문제(+환경)를 이해해야한다.</li>
<li>기존 솔루션에 대해 이해해야한다.</li>
<li>내가 구현하려는 솔루션에 대해서도 깊이 있게 이해해야한다.</li>
</ol>
<p>가장 중요한 것은 내가 풀려고 하는 문제와 환경에 대해서 잘 이해하는 것이다. 아무리 솔루션이 좋아도, 내 문제에 맞지 않다면, 틀린 솔루션이다. 이 좋은 솔루션을 내 문제에 맞게 활용하는 것 또한 문제와 환경에 대한 이해가 필요하다. 두번째는 기존 솔루션에 대해 이해해야한다고 한다. 기존 솔루션을 모르면, 내가 구현한 새로운 솔루션이 아무리 좋다고 해도, 어떻게 기존 솔루션과 다른지, 그래서 어떻게 새로운 솔루션이 더 나은 건지 모르게 된다. 세번째는 내가 구현하려는 솔루션에 대해서도 깊이 있게 이해해야한다는 것이다. 무얼 중점적으로 구현해야하는지, 어떠한 부분이 내 문제 해결에 적합한지 알아야 실험의 진행 방향도 알 수 있다.</p>
<br>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="추가-조언">추가 조언<a href="https://watanka.github.io/%EC%8B%A4%ED%97%98%EC%9D%84-%EC%9E%98%ED%95%9C%EB%8B%A4%EB%8A%94-%EA%B2%83#%EC%B6%94%EA%B0%80-%EC%A1%B0%EC%96%B8" class="hash-link" aria-label="추가 조언에 대한 직�접 링크" title="추가 조언에 대한 직접 링크">​</a></h3>
<p>번외로 실험에는 파이프라인 결함과 같은 엔지니어링적인 요소가 성능에 큰 영향을 미친다고 한다. 이럴 때는 단순하게 시작해서 점점 하나씩 추가해가는 식으로 개발을 하면, 예기치 않은 결함을 피할 수 있다고 한다.</p>
<p>또, key feature가 잘 작동하지 않을 때에는 솔루션 자체가 아닌 환경이 원인일 때도 있다고 한다. 실험이 실패했을 때, key feature가 잘 동작할 수 있는 환경이었는지도 검토해보라고 한다.</p>
<p>마지막으로, 실험 과정에서 많은 실패를 경험하게 될 텐데, 그저 실패에서 끝내기보단 하나하나 교훈을 얻어가면서 실험을 진행하다보면 결국에는 좋은 결과가 나올 수 있을 것이라고 한다.</p>
<br>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="마치며">마치며<a href="https://watanka.github.io/%EC%8B%A4%ED%97%98%EC%9D%84-%EC%9E%98%ED%95%9C%EB%8B%A4%EB%8A%94-%EA%B2%83#%EB%A7%88%EC%B9%98%EB%A9%B0" class="hash-link" aria-label="마치며에 대한 직접 링크" title="마치며에 대한 직접 링크">​</a></h3>
<p>좋은 딥러닝 개발자(또는 딥러닝 리서치 사이언티스트, 딥러닝 연구원)로써 내가 길러야할 역량은 무엇일까 하는 고민을 자주 하곤 했다. 딥러닝 문제를 근본적으로 잘 해결할 수 있는 수학, 프로그래밍 능력, 최신 트렌드를 따라가는 부지런함, 당면한 문제 해결을 위해 딥러닝 기술을 잘 활용하는 창의성 등등 많은 역량들이 중요하겠지만, 그 중 실험을 잘 하는 것이 실무 문제 해결을 위해 정말 중요하다는 생각을 했다. 카카오 if 세션 ‘실험을 잘한다는 것’에서 어떻게 하면 실험이 잘 진행되지 않아도 실패로부터 교훈을 남길 수 있는지, 솔루션을 보완하기로 했다면 어디서부터 보완해나갈지, 그리고 얼마만큼 했을 때 멈춰야할지에 대해서 배웠다. 무엇보다 실험을 할 때에는 찾은 솔루션이 어떻게 당면한 문제에 적합한지 아는 승부수가 필요하다고 한다. 실험 결과가 안 나올 때면, 다음 스텝은 어디로 가야하지? 그냥 포기해야하나? 더 잡고 있어야하나? 하고 고민할 때가 많았는데, 그동안 있었던 고민들에 정말 중요한 단서가 된 세션이였다. 역시 개발자 컨퍼런스는 시간을 내서 봐야되는 듯 싶다. 그럼 이번 글도 좋은 자양분으로 삼아 정진하자.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Software2.0 by Andrew Karpathy(2017)]]></title>
            <link>https://watanka.github.io/software2-by-andrew-karpathy-2017</link>
            <guid>https://watanka.github.io/software2-by-andrew-karpathy-2017</guid>
            <pubDate>Sun, 18 Sep 2022 00:00:00 GMT</pubDate>
            <description><![CDATA[이 글은 테슬라의 CTO였던 Andrew Karpathy가 2017년에 쓴 Software 2.0이라는 글이다. 2017년 작성 당시, Andrew Karpathy가 뉴럴 네트워크에 대해서 어떤 생각을 가지고 있는지, 5년이 지난 지금 2022년에 뉴럴 네트워크는 어떻게 변화되었는지, 그리고 앞으로는 또 어떻게 변화할지 고민하면서 글을 번역해보려고 한다.]]></description>
            <content:encoded><![CDATA[<p>이 글은 테슬라의 CTO였던 Andrew Karpathy가 2017년에 쓴 Software 2.0이라는 글이다. 2017년 작성 당시, Andrew Karpathy가 뉴럴 네트워크에 대해서 어떤 생각을 가지고 있는지, 5년이 지난 지금 2022년에 뉴럴 네트워크는 어떻게 변화되었는지, 그리고 앞으로는 또 어떻게 변화할지 고민하면서 글을 번역해보려고 한다.</p>
<br>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="소프트웨어-20">소프트웨어 2.0<a href="https://watanka.github.io/software2-by-andrew-karpathy-2017#%EC%86%8C%ED%94%84%ED%8A%B8%EC%9B%A8%EC%96%B4-20" class="hash-link" aria-label="소프트웨어 2.0에 대한 직접 링크" title="소프트웨어 2.0에 대한 직접 링크">​</a></h2>
<p>나는 사람들이 뉴럴넷을 ‘머신러닝 툴박스 중 하나’라고 소개하는 것을 가끔씩 보곤한다. 뉴럴넷은 그만의 장점과 단점이 존재하고, 여기 저기에 쓰이며, 캐글 컴피티션 우승을 위해 사용되기도 한다. (지금은 캐글 컴피티션에 뉴럴넷을 사용하는 것이 보편적인 일이지만, 2017년 당시에는 그다지 보편적이진 않았나보다.)</p>
<p>불행히도, 그들의 이해는 나무만 보고 숲은 보지 못하는 것과 같다. 뉴럴넷은 그저 또 다른 분류기가 아니라 우리가 소프트웨어를 개발하는데 있어서 근본적인 변환을 일으킬 시작점이다. 뉴럴넷은 소프트웨어 2.0이다.</p>
<p>소프트웨어 1.0의 전통적인 “기술 스택”은 모두가 잘 알고 있다. 파이썬, C++과 같은 언어로 작성되어있으며, 프로그래머에 의해서 직접적인 명시로 이루어져있다. 코드의 라인들을 한줄 한줄 작성하면서 프로그래머는 자신의 의도를 프로그램 스페이스에 명시해야한다.</p>
<p><img decoding="async" loading="lazy" src="https://watanka.github.io/assets/images/Untitled-6986a05ff9fa16486c8cc63bc56f95a8.png" width="700" height="161" class="img_ev3q"></p>
<p>반면에, 소프트웨어 2.0은 뉴럴넷의 가중치들과 같이 훨씬 추상적이고 인간에게 친화적이지 않은(human-unfriendly) 언어로 작성된다. 뉴럴넷은 너무나 많은 가중치로 이루어져 있기 때문에 (보통 수백만 개의 가중치로 구성된다) 어떤 인간도 이런 코드를 작성하는 것은 불가능하다.(필자가 시도해봄)</p>
<p>![](Untitled 1.png)</p>
<p>대신에, 우리는 의도한 프로그램의 행동에 어떠한 목표를 상세해주고(e.g. ‘인풋 아웃풋예시를 충족하는’, 또는 ‘바둑 게임을 이기는’ 등의), 서칭할 프로그램 스페이스의 부분집합(subset)을 명시하는 코드 골격(rough skeleton)을 작성하며(i.e. 뉴럴넷 아키텍쳐와 같은), 또 이 프로그램 스페이스를 찾는데 연산 리소스들을 사용하여 프로그래밍 목표를 달성한다. 뉴럴넷의 경우, 이 서칭 과정을 역전파와 SGD(stochastic gradient descent)와 함께 효과적으로 이루어질 수 있는 프로그램 스페이스의 연속적인 부분집합을 찾는 것으로 제한한다.</p>
<p>![](Untitled 2.png)</p>
<p>이 비교를 더 구체적으로 하기 위해서, 사람이 직접 소스코드를 엔지니어링하는 소프트웨어 1.0(e.g. .cpp 파일들)에서는 바이너리 파일로 컴파일된다. 소프트웨어 2.0에서는 소스코드는 1)목표하는 행동 양상을 정의하는 데이터셋들과 2)대략적인(많은 디테일이 채워져야하는) 코드 골격을 제공하는 뉴럴넷 아키텍쳐로 이루어져있다. 최종적인 뉴럴넷을 컴파일하기 위해서는 데이터셋을 학습시켜 뉴럴넷에 녹여내야한다. 오늘날 가장 실용적인 적용사례에서, 뉴럴넷 아키텍쳐들과 학습 시스템들은 **원자재(commodity)**로써 입지를 굳혀가고, 대부분의 액티브한 ‘소프트웨어 개발’은 라벨링되있는 데이터들을 선별하고 가공 정제하는데(curating, growing, massaging and cleaning) 집중되어 있다. 그리고 이것은 우리의 소프트웨어에 반복해 적용하는 프로그래밍 패러다임을 본질적으로 전환시키고 있다. 개발 팀은 이에 따라 데이터셋을 수정하고 키우는 2.0 프로그래머들(데이터 라벨러들)과, 모델 학습코드 인프라, 분석, 시각화, 라벨링 인터페이스들을 감싸고 있는 코드들을 유지보수하고 반복하는 소수의 1.0 프로그래머들로 나뉜다.</p>
<p>파고들어보니, 대부분의 현실 문제들에서 직접 프로그램을 짜는 것보다 데이터를 수집하는 것(또는 더 일반적으로 말하자면, 목표하는 행동을 정의하는 것)이 더 쉬웠다. 데이터 수집이 직접 코딩보다 쉽다는 이유와 아래에서 설명할 소프트웨어 2.0 프로그램들의 장점들 때문에, 우리는 산업들이 1.0 코드에서 2.0 코드로 전환되는 것을 목격하고 있다. <strong>소프트웨어 (1.0)은 세상을 먹어치웠고, 이제 AI(소프트웨어 2.0)은 소프트웨어를 먹어치우고 있다.</strong></p>
<br>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="지금-일어나고-있는-전환ongoing-transition">지금 일어나고 있는 전환(Ongoing Transition)<a href="https://watanka.github.io/software2-by-andrew-karpathy-2017#%EC%A7%80%EA%B8%88-%EC%9D%BC%EC%96%B4%EB%82%98%EA%B3%A0-%EC%9E%88%EB%8A%94-%EC%A0%84%ED%99%98ongoing-transition" class="hash-link" aria-label="지금 일어나고 있는 전환(Ongoing Transition)에 대한 직접 링크" title="지금 일어나고 있는 전환(Ongoing Transition)에 대한 직접 링크">​</a></h3>
<p>지금 일어나고 있는 전환들의 몇몇 예들을 간단하게 살펴보자. 각 예들은 직접 코드로 작성하기에는 너무 복잡해 포기하고 있다가 2.0으로 스택 전환이 되면서 근 몇 년 사이에 발전을 보인 분야들이다.</p>
<p>**Visual Recognition(시각 인식)**은 ****머신러닝(e.g. SVM)이 살짝 가미된(sprinkled on top at the end) 엔지니어된 피쳐들로 구성되어있었다. 그리고 우리는 큰 데이터셋(e.g. ImageNet과 같은)과 CNN 구조들의 스페이스 서칭을 통해 좀 더 강력한 visual feature들을 찾았다. 최근에는 해당 뉴럴넷 구조를 직접 서칭하지 않고, 서칭 자체를 뉴럴넷에 맡기게 되었다.</p>
<p>**Speech Recogntion(음성 인식)**은 ****수많은 전처리와 gaussian mixture 모델들과 hidden markov 모델들로 수행되었지만, 오늘날에는 완전히 뉴럴넷으로 수행된다. Fred Jelinek이 1985년에 쓴 인용에 따르면, ‘언어학자를 자를 때마다, 우리의 음성인식 시스템은 더 좋아질 것이다.’</p>
<p>Speech Synthesis(음성 합성)는 다양한 봉합 메커니즘(stitching mechanism)에 의해 시도되었으나, 오늘날에 최신 모델들은 오디오 시그널 아웃풋 그 자체를 생성하는 큰 ConvNet(e.g. WaveNet)들로 대체되었다.</p>
<p>**Machine Translation(기계번역)**은 어절 기반의 통계 기술들(phrase-based statistical techniques)로 시도되었었지만, 이것 또한 뉴럴넷으로 대체되었다. 필자가 좋아하는 구조는 weakly supervised 또는 아예 unsupervised한 환경에서 소스 언어에서 학습된 단일 모델이 다른 타겟 언어로 번역되는 다국어 셋팅이다.</p>
<p><strong>Games.</strong> 룰 기반의 바둑 프로그램은 예전에 개발되었었지만, 어떤 프로그램도 AlphaGo Zero를 이기진 못한다. 필자는 DOTA2나 Starcraft 또한 그러리라 믿는다.</p>
<p><strong>Databases.</strong> AI 밖의 전통적인 시스템들은 이미 서서히 전환을 겪고 있었다. ‘The Case for Learned Index Structures’는 기존의 cache-optimized B-Trees 알고리즘을 메모리에서는 10배가량(order-of-magnitude) 절약하고 속도 측면에서는 70% 이상 앞지르는 뉴럴넷으로 데이터 관리 시스템의 코어 부분을 대체하였다.</p>
<br>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="소프트웨어-20의-이점들">소프트웨어 2.0의 이점들<a href="https://watanka.github.io/software2-by-andrew-karpathy-2017#%EC%86%8C%ED%94%84%ED%8A%B8%EC%9B%A8%EC%96%B4-20%EC%9D%98-%EC%9D%B4%EC%A0%90%EB%93%A4" class="hash-link" aria-label="소프트웨어 2.0의 이점들에 대한 직접 링크" title="소프트웨어 2.0의 이점들에 대한 직접 링크">​</a></h3>
<p>왜 우리는 소프트웨어 2.0으로 복잡한 프로그램들을 이동시켜야할까? 가장 쉬운 답은 실전에서 더 잘 작동하기 때문이다. 하지만, 이 스택을 선호하는데에는 다른 많은 편한 이유들이 있다. 소프트웨어 2.0(예시 ConvNet)과 소프트웨어 1.0(예시 제품 레벨의 C++ 코드 베이스)을 비교하여 소프트웨어 2.0의 이점들을 몇 개 살펴보자.</p>
<p><strong>계산적으로 동질적이다(Computationally homongeneous)</strong></p>
<p>표준적인 뉴럴넷은 본질적으로 행렬곱과 ReLU와 활성화 함수(thresholding at zero), 이 두 가지 연산으로 이루어진다. 훨씬 더 이질적이고(heterogeneous) 복잡한 전통적인 소프트웨어의 요소들(instruction set)과 비교해본다면?</p>
<p>적은 수의 코어 연산 단위(core computational primitives. e.g. 행렬곱)를 위해 소프트웨어 1.0만 적용하면 되기 때문에, 소프트웨어 2.0이 훨씬 정확도/퍼포먼스 개런티 측면에서 훨씬 쉽다.</p>
<p><strong>연산에 담아내기 수월하다(Simple to bake into silicon)</strong></p>
<p>결과적으로 뉴럴넷을 돌리기위한 기초 셋팅(명령어 집합; instruction set)이 비교적 간단하기 때문에, 이 뉴럴넷들을 실리콘(연산장치)에 담기는 훨씬 수월하다. e.g) 커스텀 ASICs, <a href="https://spectrum.ieee.org/semiconductors/design/neuromorphic-chips-are-destined-for-deep-learningor-obscurity" target="_blank" rel="noopener noreferrer">뉴로모픽 칩</a> (인간의 두뇌와 유사한 컴퓨터 칩) 등등. 세상은 저전력으로도 돌아가는 지능이 보편화될 때 바뀔 것이다. e.g) 기학습된 ConvNet, 음성 인식기, WaveNet 음성 합성 네트워크 등을 전부 다 합친 작은 인공지능(protobrain)을 담을 수 있는 작고 저렴한 칩이 나올 수도 있다.</p>
<p><strong>지속적인 러닝 타임(Constant running time)</strong></p>
<p>뉴럴넷 전파의 모든 반복은 FLOPS의 정도와 동일하다. C++ 코드 베이스를 사용했을 때, 생길 수 있는 다양성(variability)은 0이다. 물론, 다이내믹 연산 그래프(dynamic compute graphs)를 가질 수는 있겠지만, 그럼에도 실행 플로우(execution flow)는 보통 굉장히 제한적이다. 그런 관점에서 소프트웨어2.0에서 우리는 의도치 않은 무한 루프에 빠질 일은 거의 없다고 보면 된다.</p>
<p><strong>지속적인 메모리 사용(Constant memory use)</strong></p>
<p>위 특징들과 비슷한 맥락으로 다이내믹하게 할당되는 메모리는 없기 때문에 디스크가 스와핑될 확률은 거의 없고, 코드를 뒤지면서 메모리 누수를 확인할 필요가 없다.</p>
<p><strong>휴대하기 편하다(It is highly portable)</strong></p>
<p>매트릭 연산들로 이루어진 이 시퀀스는 클래식한 바이너리 코드들이나 스크립트들과 비교했을 때 어떤 연산 구성이든 실행하기 훨씬 쉽다.</p>
<p><strong>애자일하다(It is very agile)</strong></p>
<p>만약 당신이 C++코드가 있고, 누군가 그 코드를 두배 더 빠르게 돌릴 수 있길 원한다면(퍼포먼스 손실을 감안하더라도), 새로운 스펙에 시스템을 튜닝하는 일은 결코 쉬운 일이 아니다. 하지만, 소프트웨어 2.0에서 우리는 뉴럴넷에 채널의 반을 삭제하거나 재학습해서 조금의 퍼포먼스 손실으로만 스피드를 두 배 이상 빠르게 만들 수 있다. 이건 마술이다. 거꾸로, 데이터나 컴퓨팅 파워가 더 생겼다고하면, 뉴럴넷의 사이즈를 더 키우거나 재학습함으로써 당신의 프로그램을 개선할 수 있다.</p>
<p><strong>모듈들이 이상적인 전체로 녹아들 수 있다(Modules can meld into an optimal whole)</strong></p>
<p>우리 소프트웨어는 보통 공공 함수들, API들, 엔드포인트들을 통해 통신하는 모듈들로 쪼갤 수 있다. 하지만, 따로 학습된 두 개의 소프트웨어 2.0 모듈들이 상호작용한다면, 우리는 쉽게 전체(whole)에 대해 역전파할 수 있다. (한 개 이상의 태스크가 다른 뉴럴넷들도 역전파를 통해 가중치 업데이트를 공유할 수 있다는 뜻) 만약 당신의 웹브라우저가 자동으로 low-level 시스템 명령어들(low level system instructions)을 10 스택 다운 재 디자인해서 웹페이지들을 로딩하는데 훨씬 더 높은 효율을 냈다고 생각해보라. 얼마나 멋진 일인가. 아니면 컴퓨터 비전 라이브러리(e.g. OpenCV)에서 너의 특정한 데이터를 자동으로 튜닝해준다면? 소프트웨어 2.0에서 이런 건 기본이다.</p>
<p><strong>당신보다 낫다(It is better than you)</strong></p>
<p>그리고 마지막으로 가장 중요한 것은 뉴럴넷은 사람들이 만들어낸 어떠한 코드들보다(지금도 이미지/비디오와 소리/음성에서 사람의 코드가 차지하는 비율은 극히 적다) 좋은 코드이다.</p>
<br>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="소프트웨어-20의-한계">소프트웨어 2.0의 한계<a href="https://watanka.github.io/software2-by-andrew-karpathy-2017#%EC%86%8C%ED%94%84%ED%8A%B8%EC%9B%A8%EC%96%B4-20%EC%9D%98-%ED%95%9C%EA%B3%84" class="hash-link" aria-label="소프트웨어 2.0의 한계에 대한 직접 링크" title="소프트웨어 2.0의 한계에 대한 직접 링크">​</a></h3>
<p>2.0 스택은 단점들도 있다. 최적화 후에 결과물로 나온 거대한 네트워크들은 분명 잘 작동하긴 하지만, 어떻게 작동하는지에 대해서는 알지 못한다. 많은 응용 분야들에서 우리는 이해하지(설명하지) 못한 99% 정확도의 모델 또는 이해 가능한(설명 가능한) 90% 정확도의 모델을 선택할지 결정해야한다.</p>
<p>2.0 스택은 비직관적이고 당황스러운 방법으로 실패할 수도 있다. 아니면 더 쵝악은 ‘조용히 실패'하는 것이다. e.g) 학습 데이터 중 bias(편향, 여기서는 학습목표와 다른 데이터 특징이라고 이해하면 될 듯하다.)를 조용히 반영하는데, 이는 대부분의 경우 데이터가 수십만 개에 다다르기 때문에  정확히 분석하거나 평가하기 힘들다.</p>
<p>마지막으로, 우리는 이 스택의 이상한 특징들에 대해서 아직 알아가는 중이다. 예를 들어, <a href="https://openai.com/blog/adversarial-example-research/" target="_blank" rel="noopener noreferrer">adversarial examples</a>와 <a href="https://github.com/yenchenlin/awesome-adversarial-machine-learning" target="_blank" rel="noopener noreferrer">adversarial attacks</a>는 이 스택의 비직관적이 성향을 나타낸다. (여기서 adversarial이란 의도적으로 뉴럴넷에 학습목표와 다른 데이터를 학습에 적용하여 학습목표로부터 벗어나게하는 것을 말한다. 더 알아보고 싶다면 링크 참조)</p>
<br>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="20-스택에서의-프로그래밍">2.0 스택에서의 프로그래밍<a href="https://watanka.github.io/software2-by-andrew-karpathy-2017#20-%EC%8A%A4%ED%83%9D%EC%97%90%EC%84%9C%EC%9D%98-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D" class="hash-link" aria-label="2.0 스택에서의 프로그래밍에 대한 직접 링크" title="2.0 스택에서의 프로그래밍에 대한 직접 링크">​</a></h3>
<p>소프트웨어 1.0은 우리가 쓰는 코드다. 소프트웨어 2.0은 평가 기준(’학습 데이터를 올바르게 분류해줘’와 같은)에 근거한 최적화에 의해 쓰여진 코드다. 최적화는 사람이 작성한 코드들보다 훨씬 더 나은 코드를 찾을 수 있기 때문에, 어떤 셋팅에서던 프로그램 자체는 분명하진 않지만, 반복해서 퍼포먼스를 평가할(e.g. 이미지 분류 잘했어? 바둑 게임 이겼어?) 수 있는지가 이번 전환(Transition)에서 중요한 주제가 될 것이다.</p>
<p>![](Untitled 3.png)</p>
<p>트렌드를 살펴보는 관점(lens)은 중요하다. <em>만약 당신이 소프트웨어 2.0을 단순히 ‘분류를 꽤 잘하는 뉴럴넷 또는 머신러닝 기술 중 하나'로 인식하기보다, 새롭고 떠오르는 프로그래밍 패러다임으로 인식한다면, 더 많은 예측들(extrapolations)이 뚜렷해질 것이고 더 할 수 있는 것들이 보일 것이다</em>.</p>
<p>1.0 코드를 적기 위해 사람을 도울 어마어마한 양의 툴들을 만들어냈다.(IDE 구문 강조 기능, 디버거, 프로파일러, 함수 서칭, 깃 통합 등등) 2.0 스택은 데이터셋들을 축적하고, 마사징하고 정제하는 프로그래밍으로 이루어질 것이다. 예를 들어, 뉴럴넷이 몇몇의 어렵거나 흔하지 않은 케이스들에서 실패한다면, 우리는 코드를 적기보단 더 많은 라벨링 샘플을 적용하여 문제를 해결할 것이다. 데이터셋들을 축적하고, 시각화하고, 정제하고, 라벨링하고, 소싱하는 워크플로우를 돕는 소프트웨어 2.0 IDE를 누가 만들 것인가? <em>IDE가 데이터 예시 단위 손실함수에 근거하여 잘못 라벨링된 이미지들을 띄워주거나, 예측을 통해 라벨링(프리라벨링)하는 것을 돕거나, 아니면 뉴럴넷 예측의 불확실성에 근거하여 라벨링할만한 데이터 예시를 제시해줄지도 모른다.</em></p>
<p>같은 맥락에서, 깃허브도 소프트웨어 1.0 코드를 위한 아주 성공적인 플랫폼(home)이다. 소프트웨어 2.0에도 깃허브와 같은 공간이 있을까? 2.0의 경우 저장소는 데이터셋이 되고 커밋들은 라벨들의 추가 및 수정이 되겠다.</p>
<p>전통적인 패키지 매니져들과 pip, conda, docker 등과 같은 서빙 인프라는 우리가 바이너리를 더 잘 배포하고 구성할 수 있도록 돕는다. 소프트웨어 2.0 바이너리를 효과적으로 배포하고, 공유하고, 임포트하고, 더 잘 작동하게 하기 위해서 우리는 어떻게 해야할까? 뉴럴넷에서도 conda와 비슷한 게 있을까?</p>
<p>짧게 말해서, 반복적인 평가가 가능하고 저렴하며, 알고리즘 자체가 명시적으로 디자인하기 어렵지 않다면, 어떤 도메인이던 소프트웨어는 점차 널리 퍼질 것이다. 소프트웨어 개발 생태계와 이 새로운 프로그래밍 패러다임이 어떻게 적응할지. 많은 기회들이 널려있다. 그리고 장기적으로 봤을 때, 이 패러다임의 미래는 우리가 AGI를 개발할 때, 소프트웨어 2.0으로 작성될 것이 점점 명확해지고 있기 때문에 밝다.</p>
<br>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="takeaway">Takeaway<a href="https://watanka.github.io/software2-by-andrew-karpathy-2017#takeaway" class="hash-link" aria-label="Takeaway에 대한 직접 링크" title="Takeaway에 대한 직접 링크">​</a></h3>
<p>Andrew Karpathy가 이 글을 낸 2017년은 구글 딥마인드에서 이세돌을 이겼던 알파고의 바둑실력을 초월하는 알파고 제로가 출시된 년도이다. 그리고 뉴럴넷의 획기적인 성능향상을 일구어낸, 이제는 자연어 처리부터 이미지, 음성 인식, 강화학습 등 거의 모든 분야에서 사용되는 Transformer가 세상에 알려진 년도기도 하다. 2022년 현재 돌이켜봤을 때, 위에 적어놓은 Andrew Karpathy의 모든 말들은 어쩌면 너무나 당연한 말일지도 모르겠다. 그의 말 중 많은 부분이 현실로 이뤄졌고 이뤄지는 중이다. 마치 높은 언덕에 올라가 저 멀리를 내다보는 선구자처럼. 가끔은 기술만 파고들다가 시야가 좁아지는 경우가 있다. 그럴 때에는 한걸음 물러서서 시대적 흐름으로써 ‘<strong>내가 하고 있는 일이 가지는 의미와 내가 사회에 기여할 수 있는 바는 어떤 것이 있는지</strong>’ 한 번쯤 생각해볼 여유가 필요하다.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[unet]]></title>
            <link>https://watanka.github.io/unet</link>
            <guid>https://watanka.github.io/unet</guid>
            <pubDate>Sat, 11 Jun 2022 00:00:00 GMT</pubDate>
            <description><![CDATA[Intro]]></description>
            <content:encoded><![CDATA[<h2 class="anchor anchorWithStickyNavbar_LWe7" id="intro">Intro<a href="https://watanka.github.io/unet#intro" class="hash-link" aria-label="Intro에 대한 직접 링크" title="Intro에 대한 직접 링크">​</a></h2>
<ul>
<li>Image Segmentation Model(이미지 분할 모델)은 픽셀 단위의 클래스 분류를 통해 이미지 내 물체를 탐지하는 모델이다.</li>
</ul>
<p><img decoding="async" loading="lazy" alt="Image Segmentation Model 중 하나인 DeepLabV3. 픽셀별 값을 예측해 물체를 탐지한다." src="https://watanka.github.io/assets/images/Untitled-38fe0cac2fa9aeb98cd96dfbec7cb4b6.png" width="2801" height="1422" class="img_ev3q"></p>
<p>Image Segmentation Model 중 하나인 DeepLabV3. 픽셀별 값을 예측해 물체를 탐지한다.</p>
<ul>
<li>
<p>segmentation model의 대표적인 모델 UNet. 2015년에 생명공학 분야에서 MRI나 전자현미경 이미지의 탐지를 위해 처음 나온 논문이다. (<a href="https://arxiv.org/pdf/1505.04597.pdf" target="_blank" rel="noopener noreferrer">UNet: Convolutional Networks for Biological Image Segmentation</a>) 벌써 7년이나 된 모델이고, 현재는 segmentation 태스크에서도 적용되기 transformer와 같은 다른 우수한 성능을 내는 모델들이 많지만, UNet은 여전히 segmentation 태스크를 가장 직관적인 방법으로 설계한 구조라고 할 수 있다.</p>
</li>
<li>
<p>이 UNet에 대해서 이렇게 글을 쓰게 된 계기는 현재 참여하고 있는 <a href="https://www.kaggle.com/competitions/uw-madison-gi-tract-image-segmentation/data" target="_blank" rel="noopener noreferrer">kaggle competition</a>(UW-Madison GI Tract Image Segmentation)에서 UNet을 다시 공부하게 되었기 때문이다. 정확히는 2차원 이미지를 처리하는 vanilla UNet에서 변형된 <strong>3차원 MRI 이미지를 처리하는 2.5D UNet</strong>이다.</p>
<p>참고 : UNet으로부터 파생된 UNet 구조들</p>
<ul>
<li>Eff-UNET</li>
<li>UNET+, ++, 3+ (NestedUNet)</li>
<li>3D UNet</li>
<li>2.5D UNet</li>
</ul>
</li>
</ul>
<p><img decoding="async" loading="lazy" alt="포켓몬 이브이는 품고 있는 돌의 속성에 따라 다양한 종류로 진화하게 된다. UNet이 이브이라면, 2.5D UNet과 다른 UNet 변형 모델들은 그 진화형 쯤 되지 않을까 싶다." src="https://watanka.github.io/assets/images/Untitled1-ada46cc842fd202c70501dd593558dfa.png" width="739" height="829" class="img_ev3q"></p>
<p>포켓몬 이브이는 품고 있는 돌의 속성에 따라 다양한 종류로 진화하게 된다. UNet이 이브이라면, 2.5D UNet과 다른 UNet 변형 모델들은 그 진화형 쯤 되지 않을까 싶다.</p>
<ul>
<li>이번 글은 UNet에 대해서 간단하게 설명하고, UNet의 변형 모델인 2.5D UNet에 대해서 설명하고자 한다.</li>
</ul>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="unet">UNET<a href="https://watanka.github.io/unet#unet" class="hash-link" aria-label="UNET에 대한 직접 링크" title="UNET에 대한 직접 링크">​</a></h2>
<hr>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="모델-구조">모델 구조<a href="https://watanka.github.io/unet#%EB%AA%A8%EB%8D%B8-%EA%B5%AC%EC%A1%B0" class="hash-link" aria-label="모델 구조에 대한 직접 링크" title="모델 구조에 대한 직접 링크">​</a></h3>
<p><img decoding="async" loading="lazy" alt="Untitled" src="https://watanka.github.io/assets/images/Untitled2-038afb7a67db80e9e311e42e653c3a34.png" width="1555" height="1036" class="img_ev3q"></p>
<p>이름 그대로 인풋 이미지가 왼쪽에서 오른쪽으로 U자형 곡선을 타고 내려갔다가(contracting path;downstream;downsampling;encoder 등 다양한 이름으로 불린다) 올라가는 것(expanding path; upstream;upsampling;decoder)을 확인할 수 있다.</p>
<p>높이로 봤을 때 총 5개의 층으로 구분되는데, 층이 한 칸씩 낮아질 때마다 featuremap의 hxw 사이즈는 반으로 줄어든다.(갈색 화살표;2x2 max pool). 반대로 층이 한 칸씩 높아질 때에는 featuremap의 hxw 사이즈는 두 배가 된다(초록색 화살표;2x2 up-conv).</p>
<p>각 층에서 featuremap은 두 번의 3x3 conv과 ReLU를 거치는데, 각 연산마다 hxw 사이즈는 2씩 줄어들고(패딩이 없기 때문에 커널 사이즈(3x3)에서 하나 작은 2만큼의 사이즈가 줄어든다), 채널 수는 64부터 2배씩 증가한다.</p>
<blockquote>
<p>e.g) 첫번째 레이어 피쳐맵 사이즈
<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mo stretchy="false">(</mo><msub><mi>L</mi><mrow><mi>f</mi><mi>e</mi><mi>a</mi><mi>t</mi><mi>u</mi><mi>r</mi><mi>e</mi><mi>m</mi><mi>a</mi><mi>p</mi></mrow></msub><mo>−</mo><msub><mi>L</mi><mrow><mi>k</mi><mi>e</mi><mi>r</mi><mi>n</mi><mi>e</mi><mi>l</mi><mi>s</mi><mi>i</mi><mi>z</mi><mi>e</mi></mrow></msub><mo>+</mo><mn>2</mn><msub><mi>L</mi><mrow><mi>p</mi><mi>a</mi><mi>d</mi><mi>d</mi><mi>i</mi><mi>n</mi><mi>g</mi></mrow></msub><mo stretchy="false">)</mo><mi mathvariant="normal">/</mi><msub><mi>L</mi><mrow><mi>s</mi><mi>t</mi><mi>r</mi><mi>i</mi><mi>d</mi><mi>e</mi></mrow></msub><mo>+</mo><mn>1</mn><mo>=</mo><mo stretchy="false">(</mo><mn>572</mn><mo>−</mo><mn>3</mn><mo>+</mo><mn>2</mn><mo>∗</mo><mn>0</mn><mo stretchy="false">)</mo><mi mathvariant="normal">/</mi><mn>1</mn><mo>+</mo><mn>1</mn><mo>=</mo><mn>570</mn></mrow><annotation encoding="application/x-tex">(L_{featuremap} - L_{kernelsize}+2L_{padding}) / L_{stride} + 1 = (572 - 3 + 2*0)/1 + 1 = 570</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1.0361em;vertical-align:-0.2861em"></span><span class="mopen">(</span><span class="mord"><span class="mord mathnormal">L</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3361em"><span style="top:-2.55em;margin-left:0em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight" style="margin-right:0.10764em">f</span><span class="mord mathnormal mtight">e</span><span class="mord mathnormal mtight">a</span><span class="mord mathnormal mtight">t</span><span class="mord mathnormal mtight">u</span><span class="mord mathnormal mtight">re</span><span class="mord mathnormal mtight">ma</span><span class="mord mathnormal mtight">p</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.2861em"><span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.2222em"></span><span class="mbin">−</span><span class="mspace" style="margin-right:0.2222em"></span></span><span class="base"><span class="strut" style="height:0.8333em;vertical-align:-0.15em"></span><span class="mord"><span class="mord mathnormal">L</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3361em"><span style="top:-2.55em;margin-left:0em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight" style="margin-right:0.03148em">k</span><span class="mord mathnormal mtight" style="margin-right:0.02778em">er</span><span class="mord mathnormal mtight">n</span><span class="mord mathnormal mtight">e</span><span class="mord mathnormal mtight" style="margin-right:0.01968em">l</span><span class="mord mathnormal mtight">s</span><span class="mord mathnormal mtight">i</span><span class="mord mathnormal mtight">ze</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em"><span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.2222em"></span><span class="mbin">+</span><span class="mspace" style="margin-right:0.2222em"></span></span><span class="base"><span class="strut" style="height:1.0361em;vertical-align:-0.2861em"></span><span class="mord">2</span><span class="mord"><span class="mord mathnormal">L</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3361em"><span style="top:-2.55em;margin-left:0em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight">p</span><span class="mord mathnormal mtight">a</span><span class="mord mathnormal mtight">dd</span><span class="mord mathnormal mtight">in</span><span class="mord mathnormal mtight" style="margin-right:0.03588em">g</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.2861em"><span></span></span></span></span></span></span><span class="mclose">)</span><span class="mord">/</span><span class="mord"><span class="mord mathnormal">L</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3361em"><span style="top:-2.55em;margin-left:0em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight">s</span><span class="mord mathnormal mtight">t</span><span class="mord mathnormal mtight" style="margin-right:0.02778em">r</span><span class="mord mathnormal mtight">i</span><span class="mord mathnormal mtight">d</span><span class="mord mathnormal mtight">e</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em"><span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.2222em"></span><span class="mbin">+</span><span class="mspace" style="margin-right:0.2222em"></span></span><span class="base"><span class="strut" style="height:0.6444em"></span><span class="mord">1</span><span class="mspace" style="margin-right:0.2778em"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em"></span><span class="mopen">(</span><span class="mord">572</span><span class="mspace" style="margin-right:0.2222em"></span><span class="mbin">−</span><span class="mspace" style="margin-right:0.2222em"></span></span><span class="base"><span class="strut" style="height:0.7278em;vertical-align:-0.0833em"></span><span class="mord">3</span><span class="mspace" style="margin-right:0.2222em"></span><span class="mbin">+</span><span class="mspace" style="margin-right:0.2222em"></span></span><span class="base"><span class="strut" style="height:0.6444em"></span><span class="mord">2</span><span class="mspace" style="margin-right:0.2222em"></span><span class="mbin">∗</span><span class="mspace" style="margin-right:0.2222em"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em"></span><span class="mord">0</span><span class="mclose">)</span><span class="mord">/1</span><span class="mspace" style="margin-right:0.2222em"></span><span class="mbin">+</span><span class="mspace" style="margin-right:0.2222em"></span></span><span class="base"><span class="strut" style="height:0.6444em"></span><span class="mord">1</span><span class="mspace" style="margin-right:0.2778em"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em"></span></span><span class="base"><span class="strut" style="height:0.6444em"></span><span class="mord">570</span></span></span></span></p>
</blockquote>
<p>그리고 한가지 주목할 점은 내려갈 때(downstream) 각 층의 마지막 피쳐맵이 보존되었다가, U자 모양에서 마주보고 있는 올라갈 때(upstream)의 featuremap과 결합(skip architecture)된다는 점이다. 예를 들어서, 첫번째 층의 마지막 피쳐맵(hxwxc = 568x568x64)은 동일한 높이의 맨 마지막 층의 피쳐맵(hxwxc=392x392x128)과 합쳐진다. 피쳐맵의 크기가 서로 다르기 때문에 더 큰 왼쪽의 피쳐맵 사이즈를 오른쪽의 피쳐맵 사이즈와 맞게 잘라준다. (e.g 568 ⇒ 392) 이렇게 서로 먼 위치의 피쳐맵을 더해주는 것은 초기 연산 단계(얕은 층, low-level)의 피쳐맵이 후기 연산 단계(깊은 층, high level)에까지 골고루 영향을 미치도록 설계한 구조이다. ResNet에서 residual layer(skip connection)로 가중치 소실 문제를 해결한 것과 비슷한 맥락이라고 생각하면 된다.</p>
<p><img decoding="async" loading="lazy" alt="residual layer; layer를 거치면서 소실될 수도 있는 가중치를 identity layer로 뒷단에 다시 한번 더함으로써 gradient vanishing 문제를 어느정도 해소한다." src="https://watanka.github.io/assets/images/%EC%8A%A4%ED%81%AC%EB%A6%B0%EC%83%B7_2022-06-11_%EC%98%A4%EC%A0%84_13553-1d22f8100815df226473cf1deab23113.png" width="1152" height="644" class="img_ev3q"></p>
<p>residual layer; layer를 거치면서 소실될 수도 있는 가중치를 identity layer로 뒷단에 다시 한번 더함으로써 gradient vanishing 문제를 어느정도 해소한다.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="손실함수loss-function">손실함수(loss function)<a href="https://watanka.github.io/unet#%EC%86%90%EC%8B%A4%ED%95%A8%EC%88%98loss-function" class="hash-link" aria-label="손실함수(loss function)에 대한 직접 링크" title="손실함수(loss function)에 대한 직접 링크">​</a></h3>
<p>손실함수로는 cross-entropy를 사용한다. 분류 모델이 인풋으로 들어온 이미지 한 장에 대해서 softmax값을 도출한다면, segmentation 모델인 UNet의 경우, 이미지를 이루는 각 픽셀에 대해서 softmax값을 갖는다.</p>
<p><img decoding="async" loading="lazy" alt="이미지 분류 모델 기본적인 형태. softmax값(각 클래스에 대한 예측확률을 담은 아웃풋)을 도출한다." src="https://watanka.github.io/assets/images/Untitled3-a509956c38ff9284e0fe67d4ad1156ac.png" width="973" height="466" class="img_ev3q"></p>
<p>이미지 분류 모델 기본적인 형태. softmax값(각 클래스에 대한 예측확률을 담은 아웃풋)을 도출한다.</p>
<p><img decoding="async" loading="lazy" alt="이미지를 이루는 픽셀들 각각에 대해서 softmax값을 도출한다. prediction의 색깔은 클래스를 의미한다.
e.g)갈색 : 소| 분홍색 : 나무|초록색 : 잔디| 파랑색 : 하늘" src="https://watanka.github.io/assets/images/Untitled4-9f3e7b40d3390585985e86e258ce6a22.png" width="405" height="124" class="img_ev3q"></p>
<p>이미지를 이루는 픽셀들 각각에 대해서 softmax값을 도출한다. prediction의 색깔은 클래스를 의미한다.
e.g)갈색 : 소| 분홍색 : 나무|초록색 : 잔디| 파랑색 : 하늘</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="25d-unet">2.5D UNet<a href="https://watanka.github.io/unet#25d-unet" class="hash-link" aria-label="2.5D UNet에 대한 직접 링크" title="2.5D UNet에 대한 직접 링크">​</a></h2>
<hr>
<ul>
<li>의학 데이터에는 3D 데이터(volumetric)가 많이 존재한다. MRI는 대표적인 3D 데이터이다. 3D 데이터를 2D로 자르지 않고, 3D 자체로 사용해야하는 이유는 2D로 자르면 중복되는 정보가 많고, 3D 데이터로 구성되어 있을 때여야만 의미있는 데이터일 경우가 많기 때문이다. 예를 들어서, MRI로 촬영한 복부 사진의 단면 이미지들보다 온전한 3D 이미지 한 장일 때 더 유용한 정보인 것과 같다.</li>
</ul>
<p><img decoding="async" loading="lazy" alt="MRA(Magnetic Resonance Angiography)로 스캔한 동맥 3D 이미지." src="https://watanka.github.io/assets/images/%EC%8A%A4%ED%81%AC%EB%A6%B0%EC%83%B7_2022-06-12_%EC%98%A4%EC%A0%84_11056-096c2ed764df94a2d4d01d831036de91.png" width="1500" height="422" class="img_ev3q"></p>
<p>MRA(Magnetic Resonance Angiography)로 스캔한 동맥 3D 이미지.</p>
<ul>
<li>3D 이미지의 세번째 차원값은 volume과 pixel을 합친 voxel이라고 부른다.</li>
<li>모델을 거친 3D 이미지 인풋의 voxel은 해당 위치에 타겟이 있을 확률값이 된다. 2D 이미지에서 UNet의 픽셀 결괏값이 해당 클래스에 대한 확률인 것과 같은 맥락이다.</li>
<li>이 3D 이미지를 핸들하기 위해서 2D에 차원을 하나 더 붙인 3D 컨볼루션을 사용할 수 있지만, 3D 컨볼루션은 연산 비용이 굉장히 큰 편이다.</li>
<li>그래서 2.5D UNet에서는 3D 컨볼루션을 사용하지 않고 효율적으로 3D 데이터를 핸들하고, 3D 차원에서 타겟값을 예측하는 3D image segmentation을 소개한다.</li>
</ul>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="방법">방법<a href="https://watanka.github.io/unet#%EB%B0%A9%EB%B2%95" class="hash-link" aria-label="방법에 대한 직접 링크" title="방법에 대한 직접 링크">​</a></h3>
<ol>
<li>3D 이미지를 다양한 방향에서 투영시켜 이미지 시퀀스로 만든다. 투영시키는 방법은 보통 Maximum Intensity Projection이나 Radon Transform을 사용한다.</li>
<li>이미지 시퀀스로 만든 이 프로젝션 이미지들에 2D 컨볼루션을 다시 적용하고, 학습가능한 3D 재구축 알고리즘을 적용하여 3D 이미지로 다시 만든다.</li>
<li>이미지 시퀀스를 타겟 오브젝트에 다시 투사해서 복원하는 과정에서 그림자와 같은 얼룩이 발생하는데, 이를 학습가능한 filtration(여과 연산)으로 개선한다.</li>
<li>학습가능한 재구축(learnable reconstruction algorithm) 알고리즘을 통해 3D 이미지로 아웃풋을 도출한다. 다양한 방향에서 선형 backprojection을 적용한다.</li>
</ol>
<p><img decoding="async" loading="lazy" alt="이미지 재구축 연산자 " src="https://watanka.github.io/assets/images/%EC%8A%A4%ED%81%AC%EB%A6%B0%EC%83%B7_2022-06-12_%EC%98%A4%ED%9B%84_22547-46efdb42deda49e9dffd097eaec64cf4.png" width="736" height="574" class="img_ev3q"></p>
<p>이미지 재구축 연산자</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="모델-구조-1">모델 구조<a href="https://watanka.github.io/unet#%EB%AA%A8%EB%8D%B8-%EA%B5%AC%EC%A1%B0-1" class="hash-link" aria-label="모델 구조에 대한 직접 링크" title="모델 구조에 대한 직접 링크">​</a></h3>
<ul>
<li>M : MIP(maximum Intesity Projection)</li>
<li>U : 2D Unet</li>
<li>F : learnable filtration</li>
<li>R : reconstruction operator using p linear backprojections for directions</li>
<li>T : fine-tuning operator (average pooling followed by a learnable normalization followed by the sigmoid activation)</li>
</ul>
<p><img decoding="async" loading="lazy" alt="모델 구조" src="https://watanka.github.io/assets/images/%EC%8A%A4%ED%81%AC%EB%A6%B0%EC%83%B7_2022-06-12_%EC%98%A4%ED%9B%84_82100-23d482471d814e0279966df2c8cc87aa.png" width="1250" height="1048" class="img_ev3q"></p>
<p>모델 구조</p>
<p>논문에서는 위의 3D 이미지 학습 방법론에, m개의 projection 이미지들에 대해서 전부 학습시키기엔 메모리 소모가 크니, 두가지 path를 사용한 random 2.5D 기법을 추가한다. projection 이미지 전체가 아닌 랜덤으로 일부를 뽑아 학습 효율을 극대화하는 방식이다.</p>
<p><strong>path1. <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msub><mover accent="true"><mi>y</mi><mo>^</mo></mover><mrow><mi>a</mi><mi>u</mi><mi>x</mi></mrow></msub></mrow><annotation encoding="application/x-tex">\hat{y}_{aux}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8889em;vertical-align:-0.1944em"></span><span class="mord"><span class="mord accent"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.6944em"><span style="top:-3em"><span class="pstrut" style="height:3em"></span><span class="mord mathnormal" style="margin-right:0.03588em">y</span></span><span style="top:-3em"><span class="pstrut" style="height:3em"></span><span class="accent-body" style="left:-0.1944em"><span class="mord">^</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.1944em"><span></span></span></span></span></span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.1514em"><span style="top:-2.55em;margin-left:-0.0359em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight">a</span><span class="mord mathnormal mtight">ux</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em"><span></span></span></span></span></span></span></span></span></span></strong></p>
<p>path1에서는 projection 이미지들의 각도를 <strong>랜덤으로 일부분 뽑아</strong> UNet에 태우고, learnable filtration과 reconstruction 과정을 거쳐 3D 아웃풋을 뽑는다. UNet, learnable filtration, reconstruction 과정 모두 학습된다.</p>
<p><strong>path2. <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mover accent="true"><mi>y</mi><mo>^</mo></mover></mrow><annotation encoding="application/x-tex">\hat{y}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8889em;vertical-align:-0.1944em"></span><span class="mord accent"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.6944em"><span style="top:-3em"><span class="pstrut" style="height:3em"></span><span class="mord mathnormal" style="margin-right:0.03588em">y</span></span><span style="top:-3em"><span class="pstrut" style="height:3em"></span><span class="accent-body" style="left:-0.1944em"><span class="mord">^</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.1944em"><span></span></span></span></span></span></span></span></span></strong></p>
<p>path2에서는 3D 인풋 x에서 나온 MIP 이미지들의 <strong>모든 프로젝션 방향을 생성</strong>한다. 그리고 UNet을 태운 후, m개의 프로젝션 이미지에 적용한다. 여기서 UNet은 학습되지 않고 고정된(frozen) 상태이다. m개의 UNet 아웃풋을 가지고, filtration F와 파인튜닝 T를 학습할 수 있게 된다.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="손실함수">손실함수<a href="https://watanka.github.io/unet#%EC%86%90%EC%8B%A4%ED%95%A8%EC%88%98" class="hash-link" aria-label="손실함수에 대한 직접 링크" title="손실함수에 대한 직접 링크">​</a></h3>
<p>이렇게 아웃풋으로 나온 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msub><mover accent="true"><mi>y</mi><mo>^</mo></mover><mrow><mi>a</mi><mi>u</mi><mi>x</mi></mrow></msub></mrow><annotation encoding="application/x-tex">\hat{y}_{aux}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8889em;vertical-align:-0.1944em"></span><span class="mord"><span class="mord accent"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.6944em"><span style="top:-3em"><span class="pstrut" style="height:3em"></span><span class="mord mathnormal" style="margin-right:0.03588em">y</span></span><span style="top:-3em"><span class="pstrut" style="height:3em"></span><span class="accent-body" style="left:-0.1944em"><span class="mord">^</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.1944em"><span></span></span></span></span></span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.1514em"><span style="top:-2.55em;margin-left:-0.0359em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight">a</span><span class="mord mathnormal mtight">ux</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em"><span></span></span></span></span></span></span></span></span></span> 와 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mover accent="true"><mi>y</mi><mo>^</mo></mover></mrow><annotation encoding="application/x-tex">\hat{y}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8889em;vertical-align:-0.1944em"></span><span class="mord accent"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.6944em"><span style="top:-3em"><span class="pstrut" style="height:3em"></span><span class="mord mathnormal" style="margin-right:0.03588em">y</span></span><span style="top:-3em"><span class="pstrut" style="height:3em"></span><span class="accent-body" style="left:-0.1944em"><span class="mord">^</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.1944em"><span></span></span></span></span></span></span></span></span> 는 정답지 y와 dice-loss로 학습에 반영된다.</p>
<p><img decoding="async" loading="lazy" alt="스크린샷 2022-06-12 오후 8.35.41.png" src="https://watanka.github.io/assets/images/%EC%8A%A4%ED%81%AC%EB%A6%B0%EC%83%B7_2022-06-12_%EC%98%A4%ED%9B%84_83541-b59be4649b2e47e43b0284db0f271abc.png" width="890" height="100" class="img_ev3q"></p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="마무리">마무리<a href="https://watanka.github.io/unet#%EB%A7%88%EB%AC%B4%EB%A6%AC" class="hash-link" aria-label="마무리에 대한 직접 링크" title="마무리에 대한 직접 링크">​</a></h2>
<hr>
<ul>
<li>복부를 찍은 MRI 영상 이미지에서 대장, 소장, 위장을 찾는 캐글 컴피티션에 참여하게되었다. 이 MRI 데이터는 복부의 여러 단면(slice)들로 이루어진 3D 이미지들이다. 이 3D 이미지로부터 물체를 탐지하는 태스크는 3D Image Segmentation이라고 한다.</li>
<li>3D Image Segmenetation 문제를 해결하기 위해 kaggle Discussion을 살펴보다, 2.5D UNet이라는 모델을 찾게 되었다.</li>
<li>2.5D UNet은 3D 컨볼루션을 사용하지 않고, 다양한 각도에서 타겟을 2D로 투사해(MIP;Maximum Intesity Projection) 이미지 시퀀스를 만들어 UNet에 태운 후, 다시 3D로 복원해서 3차원 내의 타겟 위치를 탐지하는 방식을 사용한다.</li>
<li>3D 컨볼루션을 사용하지 않기 때문에 연산량 부담이 적고, 여기에 논문은 다양한 각도를 랜덤 샘플링하여 UNet과 3D 재구축 웨이트들을 학습시키는 path1과 모든 프로젝션 방향에 대해 프리즈된 UNet과 3D 재구축 웨이트를 학습시키는 path2, 이 두 개의 방법으로 모델을 학습시킨다.</li>
<li>MIP나 reconstruction operator, filtration operator와 같은 방법에 대한 자세한 설명이 없어서 조금 더 서칭 후, 업데이트하도록 하겠다.</li>
</ul>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Multimodal Semi-Supervised Learning for Text Recognition]]></title>
            <link>https://watanka.github.io/multimodal-supervised-learning-for-text-recognition</link>
            <guid>https://watanka.github.io/multimodal-supervised-learning-for-text-recognition</guid>
            <pubDate>Fri, 03 Jun 2022 00:00:00 GMT</pubDate>
            <description><![CDATA[이번에 리뷰할 페이퍼는 Multimodal Semi-Supervised Learning for Text Recognition이다. 2022년 AWS AI Lab에서 나온 ‘Multimodal’과 ‘Semi-Supervised’가 키워드인 텍스트 인식 모델이다. 우선 ‘Multimodal’부터 살펴보자.]]></description>
            <content:encoded><![CDATA[<p>이번에 리뷰할 페이퍼는 Multimodal Semi-Supervised Learning for Text Recognition이다. 2022년 AWS AI Lab에서 나온 ‘Multimodal’과 ‘Semi-Supervised’가 키워드인 텍스트 인식 모델이다. 우선 ‘Multimodal’부터 살펴보자.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="multimodal">MultiModal<a href="https://watanka.github.io/multimodal-supervised-learning-for-text-recognition#multimodal" class="hash-link" aria-label="MultiModal에 대한 직접 링크" title="MultiModal에 대한 직접 링크">​</a></h2>
<p>불과 5년 전만 해도 컴퓨터가 글을 읽는 일은 무리였다. 물론 단어 몇 개나 미리 지정해둔 트리거 신호를 사용해서 글자를 인식하는 경우는 있었지만, 한 이미지에 몇 백개가 넘는 글자를 오탈자 하나 없이 다 맞추는 것은 불가능에 가까웠다. 게다가 몇 백개의 글자를 포함한 이미지가 몇 만장이라면? 정확도는 현업에서 사용할 수 있는 수준이 아니였다. 하지만, 딥러닝의 발전과 함께 글자를 읽는 <a href="https://www.notion.so/OCR-d61d9146f5f749d6bcd482e722913a49?pvs=21" target="_blank" rel="noopener noreferrer">OCR</a>(Optical Character Recognition) 특성 모델들이 발전하면서 이제 AI는 <strong>글자를 꽤 잘 본다.</strong> 꽤 높은 정확도와 사람이 따라올 수 없는 속도로 문서 이미지를 보고 그 안에 포함된 글자들을 내뱉는다.</p>
<p><img decoding="async" loading="lazy" alt="네이버 OCR 결과 예시" src="https://watanka.github.io/assets/images/clova_result-0e8a2ccabd7f4e58b2aa40ffd6125522.png" width="670" height="456" class="img_ev3q"></p>
<p>네이버 OCR 결과 예시</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="보고-읽는다는-것">보고 읽는다는 것<a href="https://watanka.github.io/multimodal-supervised-learning-for-text-recognition#%EB%B3%B4%EA%B3%A0-%EC%9D%BD%EB%8A%94%EB%8B%A4%EB%8A%94-%EA%B2%83" class="hash-link" aria-label="보고 읽는다는 것에 대한 직접 링크" title="보고 읽는다는 것에 대한 직접 링크">​</a></h3>
<p>하지만, 한 가지 아쉬운 점은 ‘**읽다’**는 것은 ‘<strong>본다</strong>’와는 다르다는 점이다. ‘읽다’라는 행위는 문장 성분 하나하나를 독립적으로 보는 것 뿐만 아니라 문맥(context)을 함께 이해하는 것이다. 대상이 단어라면, 단어를 구성하는 글자들을 유기적으로 이해하는 과정이 필요하고, 대상이 문장이라면, 문장을 구성하는 단어들 각각과 그 단어들이 서로 맺는 관계를 이해하여 문장을 인식하는 과정이 필요하다. (그런 의미에서 책을 읽는 건 우리가 생각한 것보다 훨씬 어려운 일일지도 모른다) 아래 그림을 한 번 살펴보자.</p>
<p><img decoding="async" loading="lazy" alt="본다 ≠ 읽는다" src="https://watanka.github.io/assets/images/ocr_example-a05d4a5ce4d4121b9423aab6558259a6.png" width="500" height="500" class="img_ev3q"></p>
<p><em>본다 ≠ 읽는다</em></p>
<p>사진의 간판에 뭐라고 적혀있는걸까? 이 사진을 ‘보기'만 했을 때, 추출해낼 수 있는 단어는 ‘보’, ‘설’, ‘렁'이다. 나무에 어렴풋이 가려진 글자가 두 개정도 더 있는 것 같지만, 해당 이미지를 기존의 OCR프로세스에 돌렸을 때, 나올 수 있는 결과는 ‘보', ‘설', ‘렁' 뿐일 것이다. 하지만, 사람은 이 이미지를 보고 나무에 가려져 보이지 않는 ‘탕'까지 유추해낼 수 있다. 보이는 글자 ‘설렁'을 보고 ‘탕’까지 읽어낸 것이다. 이것이  문맥을 이해했을 때 나올 수 있는 결과이다. 이처럼 사람은 ‘<strong>보고 읽는</strong>' 반면에, 현재(어쩌면 이미 과거의) OCR 프로세스는 ‘<strong>보는</strong>' 행위에 그친다는 단점이 있다.</p>
<p>이런 단점을 해결하고자 다른 데이터 도메인을 함께 활용하는(e.g: text + vision, vision + speech) multi-modal  연구들이 진행되고 있다. multi-modal 모델의 예로는 문장이 말한대로 그려주는 OpenAI의 Dall-E나 LG에서 개발중인 엑사원 등이 있다.(아래는 직접 뽑아본 Dall-E 이미지 아웃풋들이다.)</p>
<p><img decoding="async" loading="lazy" alt="white horse face with carrot on the forehead, oil painting" src="https://watanka.github.io/assets/images/GAN_example1-f6e6f1a0dc5139241921e5b4f08a5b6a.png" width="1024" height="1024" class="img_ev3q"></p>
<p>white horse face with carrot on the forehead, oil painting</p>
<p><img decoding="async" loading="lazy" alt="van gogh style armored cat" src="https://watanka.github.io/assets/images/GAN_example2-024b235484d8dfd6f39b918e0c464db6.png" width="1024" height="1024" class="img_ev3q"></p>
<p>van gogh style armored cat</p>
<p><img decoding="async" loading="lazy" alt="armored maltese in battlefield" src="https://watanka.github.io/assets/images/GAN_example3-82f997117e5cd4c9951e5bc28e42493e.png" width="1024" height="1024" class="img_ev3q"></p>
<p>armored maltese in battlefield</p>
<p>한 이미지에는 픽셀뿐만 아니라, 텍스트로써 이해할 수 있는 문맥들이 존재한다. 컴퓨터 비전과 자연어 처리를 융합한 이번 페이퍼의 모델 SemiMTR(Semi-Supervised Multimodal Text Recognition)은 이미지로 한 번, 그리고 텍스트로 다시 한 번 이미지를 인식한다.</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="semi-supervised">Semi-Supervised<a href="https://watanka.github.io/multimodal-supervised-learning-for-text-recognition#semi-supervised" class="hash-link" aria-label="Semi-Supervised에 대한 직접 링크" title="Semi-Supervised에 대한 직접 링크">​</a></h2>
<p>딥러닝과 빅데이터 시대가 도래하면서 무엇보다 데이터의 중요성이 강조되었고, 그만큼 방대한 양의 데이터가 공개되었다. CNN과 같은 지도학습 모델은 데이터와 상응하는 답안지(라벨링)를 필요로 한다. 문제는 데이터의 양이 커질수록, 자연스럽게 컴퓨터가 답안지로 사용할 라벨링 비용 또한 커진다는 것이다. 쉴새없이 쏟아지는 데이터의 양에 비해 라벨링은 턱없이 부족하기만 하다. 그럼 어떻게 해야할까? SemiMTR모델은 1)가지고 있는 데이터를 더 효율적으로 활용하여 모델을 학습시키는 방법과, 2)모델을 사용해서 라벨링을 대체하는 방법을 활용한다.</p>
<ol>
<li>가지고 있는 데이터를 더 효율적으로 활용하여 모델을 학습시키는 방법</li>
</ol>
<p>데이터를 더 효율적으로 활용하기 위해서 컴퓨터만 이해할 수 있는 공간에다가 가지고 있는 데이터들을 나열시킨다. 데이터를 나열시키면서 데이터 간의 관계(거리)를 하나씩 학습해나간다. 예를 들어, 학습이 제대로 이뤄졌다면, 단어 ‘king’과 ‘queen’ 간의 관계(거리+방향)는 ‘man’과 ‘woman’의 관계와 비슷하다는 걸 알게 된다. 독립적인 데이터 포인트 하나에 대해서 하나의 라벨링으로 학습하는 것이 아니라, 두 개 이상의 데이터 간의 관계를 학습해나가는 것인데, 이를 <strong>contrastive learning</strong> 기법이라고도 한다.</p>
<p><img decoding="async" loading="lazy" alt="Untitled" src="https://watanka.github.io/assets/images/contrastive_learning-9f39dacdbb152a6f651750beda1cccad.png" width="614" height="606" class="img_ev3q"></p>
<ol>
<li>모델을 사용해서 라벨링을 대체하는 방법</li>
</ol>
<p>모델이 일정 이상의 성능을 확보했다고 가정하자. 99.999% 맞출 거라는 확신이 있는 데이터들만 따로 모아놓는다. 그리고 그 데이터들을 모델이 인식하지 못하지만 사람은 인식할 수 있는 수준의 변형(augment)시킨다. 그렇게 되면, 결과물로 모델이 생성한 99.999% 확신할 수 있는 라벨링과 모델이 아직 학습하지 못한(사람은 인식하지만) 패턴을 가진 데이터 먹거리가 생긴다. 라벨링이 없어도 모델이 자급자족할 수 있는 수단이 생긴 것이다.</p>
<p><img decoding="async" loading="lazy" alt="Untitled" src="https://watanka.github.io/assets/images/semi-supervised-learning-2ed82f0178e7faf4ab7907a41679edd6.png" width="531" height="538" class="img_ev3q"></p>
<p>아래는 논문을 읽고 요약한 내용이다.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="abstract">Abstract<a href="https://watanka.github.io/multimodal-supervised-learning-for-text-recognition#abstract" class="hash-link" aria-label="Abstract에 대한 직접 링크" title="Abstract에 대한 직접 링크">​</a></h3>
<ul>
<li>최근까지 텍스트 인식기를 학습시키기 위한 실제 텍스트 이미지 데이터셋이 부족했었음</li>
<li>학습을 위해 합성 데이터를 사용한 지도 학습에 초점을 뒀었음</li>
<li>라벨링 없는 실제 텍스트 이미지들이 대량 방출됨</li>
<li>이 리소스들을 활용하기 위해, semi-supervised 방법들이 나오기 시작함</li>
<li>아주 소수의 방법들만 vision/language를 활용한 multi-modal 구조를 가지고 있음.</li>
<li>이 간극을 메꾸기 위해, multimodal 방식의 텍스트 인식기를 소개함 (SemiMTR)</li>
<li>해당 방법은 추가적인 학습 절차를 삼가하고, 현 3단계 multi-modal 학습 방식을 유지함</li>
<li>우선 vision 모델을 사전학습 시켜 self-supervised 학습과 supervised-학습을 합침<!-- -->
<ul>
<li>더 자세히는, 기존의 visual representation learning algorithm을 확장시키고 글자인식을 위한 contrastive-based 방식을 소개함.</li>
</ul>
</li>
<li>텍스트 뭉치들(corpus)를 사용해서 language 모델을 학습한 후에는 전체 네트워크를 약(weakly)과 강(strongly) 증강(augmented)된 텍스트 이미지들 간의 시퀀셜, 글자레벨, consistency regularization을 활용하여 파인튜닝함</li>
<li>새로운 셋업에서, consistency는 각 modality에서 각각 시행됨</li>
<li>추가 실험에서는 우리의 방법이 현 학습 스키마들의 정확도를 넘어서고, 텍스트 인식 벤치마크에서 최신 성능을 보여주는 것을 확인할 수 있었음</li>
</ul>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="introduction">Introduction<a href="https://watanka.github.io/multimodal-supervised-learning-for-text-recognition#introduction" class="hash-link" aria-label="Introduction에 대한 직접 링크" title="Introduction에 대한 직접 링크">​</a></h3>
<ul>
<li>ABINet이라는 모델 사용<!-- -->
<ul>
<li>세 단계로 이루어져있는데, 이 단계들로 라벨링이 없는 실제 데이터들을 학습에 활용할 수 있음<!-- -->
<ul>
<li>supervised vision model pretraining</li>
<li>bidrectional language representation learning of the language model</li>
<li>supervised fine-tuning of the fusion model and entire</li>
</ul>
</li>
<li>vision model을 학습시키기 위해서 label과 unlabel 데이터들을 contrastive learning으로 활용하는데,<!-- -->
<ul>
<li>contrastive learning의 핵심 아이디어는 같은 이미지이지만 다르게 증강된 이미지들의 representation 간의 동의율과 다른 이미지들 간의 분리율을 최대화하는 것</li>
</ul>
</li>
<li>더 정확히는 SeqCLR 기법을 적용함.<!-- -->
<ul>
<li>SeqCLR은 seq2seq contrastive learning 기법임 (원래는 손글씨 인식용으로 나옴)</li>
<li>이 기법 적용을 위해, 더 로버스트한 transformer-based 백본을 채택했고, 더 강한 색깔-텍스쳐 증강기법을 적용했다.</li>
</ul>
</li>
<li>파인튜닝 단계에서는<!-- -->
<ul>
<li>약증강과 강증강된 텍스트 이미지로부터 증강된 실제 텍스트 이미지 간 시퀀셜, 글자-레벨, consistency regularization을 진행한다.</li>
<li>약증강 이미지로부터 인공 가짜-라벨시퀀스를 생성하고, 같은 이미지의 강증강 버젼을 전체 모듈을 학습시킬 때 라벨 시퀀스로 사용한다.</li>
</ul>
</li>
<li>실험을 통해, 각 모덜리티가 스스로 자급자족 학습(teacher of itself)하고, 각각의 학습을 위한 psuedo-label을 생성할 때, 가장 학습이 잘 진행된 것을 확인했다.</li>
</ul>
</li>
</ul>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="related-work">Related Work<a href="https://watanka.github.io/multimodal-supervised-learning-for-text-recognition#related-work" class="hash-link" aria-label="Related Work에 대한 직접 링크" title="Related Work에 대한 직접 링크">​</a></h3>
<ul>
<li>Vision-language multimodal text recognizer<!-- -->
<ul>
<li>SCATTER - custom decoders for vision and language</li>
<li>SRN - global semantic reasoning module</li>
<li>SEED - offered a semantics enhanced encoder-decoder framework</li>
<li>VisionLAN -  provide vision model w/ language capability</li>
<li>ABINET - vision-language multimodal architecture that possesss an explicit language model which can be pretrained on text corpus</li>
</ul>
</li>
<li>Semi-supervised learning for text recognition<!-- -->
<ul>
<li>case1. seq2seq domain adaptation techniques between labeled and unlabeld datasets</li>
<li>case2. seq2seq contrastive learning for visual representations learning</li>
<li>psuedo-labeling methods to utilize unlabeled images</li>
<li>confidence-based criterion for filtering noisy pseudo-labels</li>
</ul>
</li>
<li>Consistency Regularization<!-- -->
<ul>
<li>self-supervised learning에 널리 쓰이는 기술</li>
<li>core idea : model predictions should remain the same under each semantic-preserving perturbations of the same image</li>
<li>case1. enforces the network to be agnostic to some transformations and disturbances</li>
<li>case2. proposes a consistency regularization in semi-supervised settings by using noise injections and augmentations on the unlabeled examples</li>
<li>FixMatch : classification task에서 unlabeled 데이터에 대해 psuedo-labeling을 한 consistency regularization함.<!-- -->
<ul>
<li>strongly augmented version으로부터 나온 predictions와, 같은 이미지지만 weakly augmented version으로부터 나온 pseudo-label과 매칭하도록  학습한다.</li>
</ul>
</li>
</ul>
</li>
</ul>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="architectural-background">Architectural Background<a href="https://watanka.github.io/multimodal-supervised-learning-for-text-recognition#architectural-background" class="hash-link" aria-label="Architectural Background에 대한 직접 링크" title="Architectural Background에 대한 직접 링크">​</a></h3>
<ol>
<li>pretrain vision model</li>
<li>pretrain language model via a version of masked language modeling<!-- -->
<ul>
<li>mask the attention maps instead of input characters</li>
</ul>
</li>
<li>end2end fine tuning stage via supervised cross-entropy losses on the vision, language, and fusion predictions</li>
</ol>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="semimtr-multimodal-semi-supervised-learning">SemiMTR: Multimodal Semi-Supervised Learning<a href="https://watanka.github.io/multimodal-supervised-learning-for-text-recognition#semimtr-multimodal-semi-supervised-learning" class="hash-link" aria-label="SemiMTR: Multimodal Semi-Supervised Learning에 대한 직접 링크" title="SemiMTR: Multimodal Semi-Supervised Learning에 대한 직접 링크">​</a></h3>
<p><img decoding="async" loading="lazy" alt="Untitled" src="https://watanka.github.io/assets/images/semi-mtr-6273ebab84e6724917f2f515b512a361.png" width="708" height="323" class="img_ev3q"></p>
<p>vision model ⇒ vision features ⇒ vision prediction ⇒ (gradient stop) ⇒ language model ⇒ contextual features ⇒ combine visual + contextual features ⇒ fusion prediction</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="vision-model-pretraining">Vision Model Pretraining<a href="https://watanka.github.io/multimodal-supervised-learning-for-text-recognition#vision-model-pretraining" class="hash-link" aria-label="Vision Model Pretraining에 대한 직접 링크" title="Vision Model Pretraining에 대한 직접 링크">​</a></h3>
<ul>
<li>사전학습에 unlabeled real data 포함</li>
<li>마지막 학습 phase에 unlabeled data만 사용하는 competitive semi-supervised methods가 vision model의 성능을 개선하였다</li>
<li>SeqCLR + Transformer-based backbone</li>
</ul>
<p><img decoding="async" loading="lazy" alt="Untitled" src="https://watanka.github.io/assets/images/vision_model_pretraining-0b5f36a09777b904f227bad4957911be.png" width="685" height="363" class="img_ev3q"></p>
<ul>
<li>각 이미지를 두 번 augment ⇒ 각 visual backbone 과 projection head에 feed</li>
<li>instance-mapping 함수 적용 ⇒ 각 증강된 이미지의 representation 시퀀스를 만들어 sub-word 단위의 contrastive learning이 가능하게 함</li>
<li>각 평행한 브랜치에서 라벨링 데이터의 visual prediction의 supervised loss 계산</li>
</ul>
<p>사전학습은 6개의 빌딩 블럭으로 이루어져있음.</p>
<p><strong>stochastic data augmentation</strong></p>
<ul>
<li>인풋 이미지에서 두 개의 augmented view 생성</li>
</ul>
<p><img decoding="async" loading="lazy" alt="Untitled" src="https://watanka.github.io/assets/images/augmented_view-c78e7569ba4f8c07109d7f90f0d83a21.png" width="487" height="275" class="img_ev3q"></p>
<p><strong>Visual backbone</strong></p>
<ul>
<li>ResNet + Transformer units</li>
</ul>
<p><strong>Projection head</strong></p>
<ul>
<li>optional auxiliary network</li>
<li>transforms the visual backbone features into a lower dimensional space</li>
</ul>
<p><strong>Instance-mapping function</strong></p>
<ul>
<li>unique block for seq2seq predictions divides each feature map into a sequence of separate representations over which the contrastive loss is computed</li>
<li>기존 contrastive 방법과 달리, 전체 이미지들이 아닌 시퀀스를 이루는 개별 성분들 간의 contrasting이 가능하게 합니다.</li>
<li>window-to-instance mapping 방식을 사용<!-- -->
<ul>
<li>
<p>feature map들의 배치가 NxFxHxW ⇒ NxH*WxF가 되도록 펴고,</p>
</li>
<li>
<p>adpative average pooling 적용해서</p>
</li>
<li>
<p>flatten된 각 featuremap으로부터 T개의 독립된 representation 추출하여 Z 세트로 모음</p>
<p><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>Z</mi><mo>=</mo><mi>A</mi><mi>d</mi><mi>a</mi><mi>p</mi><mi>t</mi><mi>i</mi><mi>v</mi><mi>e</mi><mi>A</mi><mi>v</mi><mi>g</mi><mi>P</mi><mi>o</mi><mi>o</mi><mi>l</mi><mn>2</mn><mi>d</mi><mo stretchy="false">(</mo><mi>F</mi><mi>l</mi><mi>a</mi><mi>t</mi><mi>t</mi><mi>e</mi><mi>n</mi><mo stretchy="false">(</mo><mi>P</mi><mo stretchy="false">)</mo><mo stretchy="false">)</mo></mrow><annotation encoding="application/x-tex">Z=AdaptiveAvgPool2d(Flatten(P))</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6833em"></span><span class="mord mathnormal" style="margin-right:0.07153em">Z</span><span class="mspace" style="margin-right:0.2778em"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em"></span><span class="mord mathnormal">A</span><span class="mord mathnormal">d</span><span class="mord mathnormal">a</span><span class="mord mathnormal">pt</span><span class="mord mathnormal">i</span><span class="mord mathnormal" style="margin-right:0.03588em">v</span><span class="mord mathnormal">e</span><span class="mord mathnormal">A</span><span class="mord mathnormal" style="margin-right:0.03588em">vg</span><span class="mord mathnormal" style="margin-right:0.13889em">P</span><span class="mord mathnormal">oo</span><span class="mord mathnormal" style="margin-right:0.01968em">l</span><span class="mord">2</span><span class="mord mathnormal">d</span><span class="mopen">(</span><span class="mord mathnormal" style="margin-right:0.01968em">Fl</span><span class="mord mathnormal">a</span><span class="mord mathnormal">tt</span><span class="mord mathnormal">e</span><span class="mord mathnormal">n</span><span class="mopen">(</span><span class="mord mathnormal" style="margin-right:0.13889em">P</span><span class="mclose">))</span></span></span></span></p>
</li>
<li>
<p>이 함수는 두번 호출되어 각 augmented view로 적용</p>
</li>
</ul>
</li>
</ul>
<p><strong>Contrastive Loss</strong></p>
<ul>
<li>같은 인풋은 augmentation과 상관없이 더 가깝게, 다른 인풋은 더 멀게 학습하기 위한 손실함수</li>
<li>Noise Contrastive Estimation(NCE) loss function</li>
</ul>
<p><img decoding="async" loading="lazy" alt="Untitled" src="https://watanka.github.io/assets/images/nce-dd2d01d05d3fd752e85c56b4335f8d99.png" width="494" height="75" class="img_ev3q"></p>
<p><img decoding="async" loading="lazy" alt="cosine distance" src="data:image/png;base64,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" width="241" height="38" class="img_ev3q"></p>
<p>cosine distance</p>
<p><img decoding="async" loading="lazy" alt="Untitled" src="https://watanka.github.io/assets/images/consistency_regularization-2ed82f0178e7faf4ab7907a41679edd6.png" width="531" height="538" class="img_ev3q"></p>
<p><strong>Vision decoder and supervised loss</strong></p>
<ul>
<li>SeqCLR 과 contrastive learning methods에서 supervised training phase는 self-supervised stage가 끝나고 진행됩니다. 하지만, semiMTR은 학습 스테이지들이 늘어나는 걸 막고, 통합된 semi-supervised training scheme을 진행합니다.</li>
</ul>
<p><img decoding="async" loading="lazy" alt="Untitled" src="data:image/png;base64,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" width="330" height="62" class="img_ev3q"></p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="consistency-regularization-for-fusion-model-training">Consistency Regularization for Fusion Model Training<a href="https://watanka.github.io/multimodal-supervised-learning-for-text-recognition#consistency-regularization-for-fusion-model-training" class="hash-link" aria-label="Consistency Regularization for Fusion Model Training에 대한 직접 링크" title="Consistency Regularization for Fusion Model Training에 대한 직접 링크">​</a></h3>
<ul>
<li>pseudo labeling을 사용하여 추가 재학습 과정이 필요한 다른 semi-supervised learning 방법과는 다르게</li>
<li>self-supervised learning을 위해서, 파인튜닝에 더 적합한 sequential, character-level, consistency regulaization을 진행</li>
<li>weakly-augmented version에서 라벨 추출</li>
<li>strongly-augmented version에다가 추출한 라벨 적용해서 학습</li>
</ul>
<p><strong>Stochastic strong and weak augmentations</strong></p>
<ul>
<li>여러 color-texture augmentation method를 실험함</li>
<li>그중 가장 효과있는 방법 채택</li>
</ul>
<p><strong>Sequential consistency regularization loss</strong></p>
<ul>
<li>첫번째 padding token location (denoted by <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msup><mi>N</mi><mrow><mi>w</mi><mi>e</mi><mi>a</mi><mi>k</mi></mrow></msup></mrow><annotation encoding="application/x-tex">N^{weak}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8491em"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.10903em">N</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.8491em"><span style="top:-3.063em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight" style="margin-right:0.02691em">w</span><span class="mord mathnormal mtight">e</span><span class="mord mathnormal mtight" style="margin-right:0.03148em">ak</span></span></span></span></span></span></span></span></span></span></span></span>)에서 먼저 teacher’s sequential prediction을 prune하고,</li>
<li>각 글자들에 독립적으로 consistency regularization 적용</li>
</ul>
<p><img decoding="async" loading="lazy" alt="Untitled" src="https://watanka.github.io/assets/images/loss_consist-9c26dd95df6dc2fd9c462e7c24309ed5.png" width="647" height="65" class="img_ev3q"></p>
<ul>
<li>1(*&gt;t) = threshold operator ⇒ teacher label의 신뢰도가 일정이상일 때만 학습에 반영</li>
<li>L은 cross-entropy 또는 KL-divergence</li>
<li>6.2에선 threshold, label값 종류(prob vector or one-hot)의 영향에 대해서 실험</li>
</ul>
<p><strong>Teacher and Student Modalities</strong></p>
<ul>
<li>보통 teacher student decoder에서 consistency regularization을 계산하는 방법은 전체 네트워크의 단일 디코더를 통해서이지만,</li>
<li>multimodal text recognition scheme에서는 각 modality가 각각의 decoder를 가지고 있으므로, 각각이 teacher 또는 student가 될 수 있음.</li>
<li>6.2에서는 각 configuration을 실험해 각 modality가 pseudo label을 생성하는 teacher가 되어야하는지 유무를 체크함</li>
</ul>
<p><img decoding="async" loading="lazy" alt="Untitled" src="https://watanka.github.io/assets/images/teacher_student_modality-e1cd5109e55018ad6689c1a25c7a68b1.png" width="719" height="68" class="img_ev3q"></p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="experiments">Experiments<a href="https://watanka.github.io/multimodal-supervised-learning-for-text-recognition#experiments" class="hash-link" aria-label="Experiments에 대한 직접 링크" title="Experiments에 대한 직접 링크">​</a></h3>
<ol>
<li>analyze capability of current synthetic and real-world datasets</li>
<li>compare our method with leading semi-supervised methods</li>
</ol>
<p><em>Datasets</em></p>
<p><img decoding="async" loading="lazy" alt="Untitled" src="https://watanka.github.io/assets/images/dataset-c2099867fcd591ba80b4a3d2f45791ea.png" width="288" height="590" class="img_ev3q"></p>
<p><em>Comparison to State-of-the-Art Methods</em></p>
<p><img decoding="async" loading="lazy" alt="Untitled" src="https://watanka.github.io/assets/images/comparison_table-79251035173337f42bb7ad6e93f4fe17.png" width="740" height="286" class="img_ev3q"></p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="ablation-studies">Ablation Studies<a href="https://watanka.github.io/multimodal-supervised-learning-for-text-recognition#ablation-studies" class="hash-link" aria-label="Ablation Studies에 대한 직접 링크" title="Ablation Studies에 대한 직접 링크">​</a></h3>
<p><strong>6.1 Vision Model Pretraining</strong></p>
<p><em>Two-stage vs unified training</em></p>
<ul>
<li>기존 contrastive learning ⇒ contrastive-based pretraining &amp; fully-supervised fine-tuning 2 stages 였음</li>
<li>contrastive learning objective와 supervised cross-entropy loss를 합치는 unified training stage로 진행</li>
</ul>
<p><strong>6.2 Consistency Regularization</strong></p>
<p><em>Sequnetial consistency regularization loss</em></p>
<ul>
<li>cross-entropy vs KLDivergence</li>
<li>soft label vs one hot label</li>
<li>stopping the teach gradients and using a threshold</li>
</ul>
<p><em>Teacher-student modalities</em></p>
<p><img decoding="async" loading="lazy" alt="Untitled" src="https://watanka.github.io/assets/images/teacher_student_modality_comparison_table-32ce72761d797bdc48a6642065f3f4f8.png" width="592" height="245" class="img_ev3q"></p>
<ul>
<li>각 modality가 teacher 또는 student가 될 수 있는데, 각 modal끼리 teacher-student 관계가 가장 높은 성능을 보였다.</li>
</ul>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="conclusions-and-future-work">Conclusions and Future Work<a href="https://watanka.github.io/multimodal-supervised-learning-for-text-recognition#conclusions-and-future-work" class="hash-link" aria-label="Conclusions and Future Work에 대한 직접 링크" title="Conclusions and Future Work에 대한 직접 링크">​</a></h3>
<ul>
<li>SemiMTR은 첫번째  multimodal이자 semi-supervised learning 알고리즘을 사용한 text recognition 모델이다</li>
<li>contrastive-based visual representation learning과 sequential, character-level consistency regularization을 적용했다.</li>
<li>label 데이터 뿐만 아니라 unlabel 데이터도 활용하지만, 기존 모델의 3 스테이지를 그대로 유지했다.</li>
<li>다른 모델들보다 높은 성능을 보여준다</li>
<li>다른 리서처들에게 unlabel 데이터를 어떻게 활용할지에 대한 생각을 열어줬다.</li>
</ul>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[카카오 OCR을 따라해보자]]></title>
            <link>https://watanka.github.io/카카오_ocr을_따라해보자</link>
            <guid>https://watanka.github.io/카카오_ocr을_따라해보자</guid>
            <pubDate>Mon, 03 Jan 2022 00:00:00 GMT</pubDate>
            <description><![CDATA[11월 16일부터 3일간 카카오 if 2021이 열렸다. 개발자들에게 흥미로운 컨텐츠들로 가득했는데, 그 중 엔터프라이즈 팀에서 진행한 'OCR 모델 진행 개편기'가 눈에 들어왔다. 카카오는 OCR을 어떻게 구성하고 있을까? (OCR이 무엇인지 궁금하다면, 이 글을 읽어보길 권한다.)]]></description>
            <content:encoded><![CDATA[<p>11월 16일부터 3일간 카카오 if 2021이 열렸다. 개발자들에게 흥미로운 컨텐츠들로 가득했는데, 그 중 엔터프라이즈 팀에서 진행한 'OCR 모델 진행 개편기'가 눈에 들어왔다. 카카오는 OCR을 어떻게 구성하고 있을까? (OCR이 무엇인지 궁금하다면, <a href="https://watanka.github.io/ocr%EC%9D%B4%EB%9E%80">이 글</a>을 읽어보길 권한다.)</p>
<br>
<p>OCR에 대한 설명과 카카오에서 구성하고 있는 OCR파이프라인을 소개했다. <code>Text Detection</code>과 <code>Text Recognition</code> 사이에 <code>Script identification</code> 을 두어 인식하는 이미지가 어떤 언어인지 구별한다고 한다. 이렇게 언어를 구별하는 단계를 두면, 언어별 특화된 모델 웨이트를 사용해서 더 광범위한 데이터 범위와 언어별 더 정확한 예측이 가능해진다.</p>
<br>
<p>그 후에, 인식한 단어들을 기반으로 해당 단어가 어떤 개체(entity; 사람, 조직, 시간 등 일반화할 수 있는 범주)에 속하는지 인식하는 <code>NER(Named Entity Recognition)</code> 과 텍스트 간 알맞게 묶어주는 <code>Text Clustering</code> , 그리고 등록된 템플릿에 단어들을 매칭하는 <code>Template Matching</code> 으로 후처리를 했다고 한다.</p>
<p><img decoding="async" loading="lazy" alt="kakao OCR 구성도" src="https://watanka.github.io/assets/images/Untitled-1831eb1d7bfe0e9bdfcf1ce6c06f26e1.png" width="1438" height="580" class="img_ev3q"></p>
<p>이 글은 어떻게 OCR 파이프라인을 AS-IS(현재 상태)에서 TO-BE로 개편하기 위해 어떤 점들을 고쳤는지에 대해 주목하고, 개편 전과 후의 모델 구조를 살펴보려고 한다.</p>
<p>(개인적으로 조금(보통은 1~2% 이내의) 나은 성능 향상을 위해 이미 성능이 어느정도 나오는 파이프라인을 과감히 놓아주고 새로운 체크포인트에서 출발을 하는 것은 결코 쉬운 일이 아니었을 거라고 생각한다. )</p>
<br>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="ocr-개편-목표">OCR 개편 목표<a href="https://watanka.github.io/%EC%B9%B4%EC%B9%B4%EC%98%A4_ocr%EC%9D%84_%EB%94%B0%EB%9D%BC%ED%95%B4%EB%B3%B4%EC%9E%90#ocr-%EA%B0%9C%ED%8E%B8-%EB%AA%A9%ED%91%9C" class="hash-link" aria-label="OCR 개편 목표에 대한 직접 링크" title="OCR 개편 목표에 대한 직접 링크">​</a></h2>
<p>![](Untitled 1.png)</p>
<p>새로운 시도를 위해 목표를 확실하게 정의하는 과정은 필수다. 텍스트 탐지 모델의 목표는 높은 정확도, 글자 레벨 박스, 모델을 통한 텍스트 클러스터링, 텍스트 방향 예측, 그리고 간단한 후처리였고, 텍스트 인식 모델의 목표는 마찬가지로 높은 정확도, 병렬 예측(더 빠른 연산을 위해서), 더 적은 리소스, 그리고 간단한 후처리였다고 한다. 그럼 텍스트 탐지 모델부터 어떤 식으로 개편했는지 알아보자.</p>
<br>
<h1>텍스트 탐지 모델(STD)</h1>
<p>![](Untitled 2.png)</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="as-is">AS-IS<a href="https://watanka.github.io/%EC%B9%B4%EC%B9%B4%EC%98%A4_ocr%EC%9D%84_%EB%94%B0%EB%9D%BC%ED%95%B4%EB%B3%B4%EC%9E%90#as-is" class="hash-link" aria-label="AS-IS에 대한 직접 링크" title="AS-IS에 대한 직접 링크">​</a></h3>
<ul>
<li>나열한 as-is 조건 중에 부합하는 모델 중 가장 유명한 모델은 아무래도 <a href="https://github.com/clovaai/CRAFT-pytorch" target="_blank" rel="noopener noreferrer">CRAFT</a>인듯하다.</li>
</ul>
<p>Object Detection</p>
<ul>
<li>이미지 내에 물체의 영역을 탐지하는 물체 탐지 모델은 박스 좌표를 추출하는 방식에 따라, Segmentation-based와 Regression-based로 나뉠 수 있다.</li>
</ul>
<ol>
<li>Segmentation-based</li>
</ol>
<ul>
<li>Segmentation-based는 원본이미지에 대응되는 Featuremap을 뽑아서 pixel 단위 처리로 물체 영역을 뽑는 방식이다.</li>
<li>Segmentation-based Text Detction의 대표적인 모델, CRAFT는 적절한 파라미터 조절로 word-level과 character-level 텍스트 탐지가 가능하다.</li>
<li>아래 그림처럼 모델을 거친 각 글자들은 heatmap(아보카도처럼 생긴 동그라미)으로 표현된다. 정 가운데 빨강이 1에 가까운 값, 가운데에 멀어질수록 0에 가까운 파랑색 값을 갖게 된다. 이는 글자가 실제로 위치할 확률(probability)로도 해석이 가능하다.</li>
</ul>
<p><img decoding="async" loading="lazy" alt="[출처] : CRAFT: Character-Region Awareness For Text detection" src="https://watanka.github.io/assets/images/craft_example-9f69274551a53f1d2727a0e974dbf7c2.gif" width="1172" height="295" class="img_ev3q"></p>
<p>[출처] : <strong>CRAFT: Character-Region Awareness For Text detection</strong></p>
<ul>
<li>뽑은 heatmap 위에 일정값 이상을 갖는 픽셀들끼리 합치고, 그 합친 영역을 구분하는 박스를 그리면 텍스트 영역 탐지가 완성된다.</li>
<li>regression-based 방식처럼 박스 형태에 얽매이지 않고, pixel 단위로 텍스트 영역을 탐지하기 때문에 다양한 물체 형태를 예측 가능하다는 장점이 있다.</li>
<li>모델에서 나온 결과로부터 물체 영역을 탐지하는 후처리가 시간이 오래 걸린다는 단점이 있다.</li>
<li>한 이미지에 텍스트가 오밀조밀 모여 있을 가능성이 높은 문자 탐지 태스크의 특성상, segmentation-based 기법을 사용할 경우, 한 텍스트 그룹이 다른 텍스트 그룹과 겹치게 되는 경우가 생길 수 있다. 아래 그림처럼 'PLEASE', 'TURN', ... 등의 텍스트들은 개별의 텍스트 박스로 잡혀야 마땅하지만, heatmap 영역 간격이 가까우면 하나의 박스 형태로 잡히는 단점이 있다.</li>
</ul>
<p>![](Untitled 3.png)</p>
<ol>
<li>
<p>Regression-based</p>
</li>
</ol>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="to-be">TO-BE<a href="https://watanka.github.io/%EC%B9%B4%EC%B9%B4%EC%98%A4_ocr%EC%9D%84_%EB%94%B0%EB%9D%BC%ED%95%B4%EB%B3%B4%EC%9E%90#to-be" class="hash-link" aria-label="TO-BE에 대한 직접 링크" title="TO-BE에 대한 직접 링크">​</a></h3>
<ul>
<li>그렇다면 to-be는?</li>
<li>조건별로 나열해서 모델을 추측해보자.</li>
<li>TO-BE 조건들<!-- -->
<ul>
<li>
<p><strong>one-shot anchor-free model</strong></p>
<ul>
<li>anchor free는 카카오에서 어떤 모델을 사용하는지 추측하는 데에 가장 큰 단서였다.</li>
<li>물체 탐지(object detection) 모델은 크게 regression-based와 segmentation-based로 나뉜다.</li>
<li>regression-based는 다시 anchor-based와 anchor-free로 나뉜다. 그 중 anchor-based는 이미지 인풋을 격자 셀로 나누고, 각 격자마다 미리 정의해둔 k개의 앵커 박스들을 할당해서 물체의 박스 좌표를 표현한다.<!-- -->
<ul>
<li>
<p>아래 그림을 예로 들면, 4x4 격자 셀에 2개의 앵커박스가 존재한다.</p>
</li>
<li>
<p>박스 좌표는 보통 4개로 표현하기 때문에 아웃풋 형식은 4x4x(4*2)의 행렬이 된다.</p>
</li>
<li>
<p>격자 셀 각각에 대해서 2개씩의 앵커박스, 그리고 각 앵커박스는 4개의 좌표로 구성</p>
</li>
<li>
<p>한 격자 셀에 같은 클래스가 두 개 이상 존재하는 경우와 같은 앵커박스 형태가 두 개 이상 존재하는 경우는 이 anchor box구조가 커버하지 못하는 예외케이스이긴 하지만, 격자 셀 개수가 많아질수록 언급한 경우들을 드물다.</p>
</li>
<li>
<p>학습을 위한 라벨링의 경우, 각 격자셀에 대해서 정답 박스 영역(gt box)과 앵커박스별 IoU를 구해서 positive 또는 negative, 아예 겹치지 않는 경우 background로 설정한다.</p>
</li>
</ul>
</li>
</ul>
<p>![](Untitled 4.png)</p>
<ul>
<li><a href="https://nuggy875.tistory.com/33" target="_blank" rel="noopener noreferrer">anchor-based detection</a>는 사전정의된 앵커박스의 정보에 따라 성능이 바뀌는 문제와, 정확한 박스영역 탐지를 위해서는 더 많은 앵커박스가 필요하다는 문제, 그리고 각 그리드셀의 앵커박스 수만큼 (예: 50x50 grid x 80 anchor boxes=200k) gt를 포함하는지 안 하는지(positive인지 negative인지) IoU를 일일히 계산해야한다는 연산적 부담이 있다.</li>
<li>이를 보완하기 위해 나온 것이 anchor-free model. anchor free의 대표적인 모델 FCOS(Fully Convolutional One Stage Object Detection) 모델을 살펴보자.</li>
</ul>
<p>FCOS</p>
<table><thead><tr><th></th><th>anchor-based</th><th>anchor-free</th></tr></thead><tbody><tr><td>격자 셀 라벨링</td><td>정답 박스와의 IoU 계산해서 positive 또는 negative 설정</td><td>정답 박스 안에 속하면 positive, 정답 박스가 여럿일 경우 ambiguous</td></tr><tr><td>regression loss</td><td>L1 Loss</td><td>IoU loss</td></tr><tr><td>classification loss</td><td>Binary Cross Entropy Loss</td><td>Focal Loss</td></tr><tr><td>박스 좌표 regression 방법</td><td>anchor box와의 중심으로부터 박스값 보정</td><td>gt box 경계값으로부터의 거리</td></tr></tbody></table>
<ul>
<li>Centerness라는 개념 도입. heatmap과 유사</li>
<li>anchor based는 anchor box와의 offset을 계산하지만, FCOS는 중심점으로부터 거리를 계산한다.</li>
<li>L_cls는 focal loss, L_reg는 IoU loss</li>
<li>anchor box scale 대신 FCOS는 bounding box regression의 범위를 제한</li>
</ul>
</li>
<li>
<p><strong>Direct Regression</strong></p>
<ul>
<li>Indirect Regression은 앵커박스로부터 박스좌표를 보정하는 과정</li>
<li>Direct Regression은 중점으로부터 박스좌표를 바로 보정하는 과정</li>
<li>Segmentation-based에서 Direct Regression. 이는 박스를 구한 후, offset으로 박스 영역을 텍스트 영역에 좀 더 fit하게 깎는 과정이 있을 것 같다.</li>
</ul>
</li>
<li>
<p><strong>No NMS</strong></p>
<ul>
<li>
<p>Non-Maximum-Suppression을 빼고도 정확한 박스 후보군을 뽑을 수 있는 방법?</p>
</li>
<li>
<p>centerness라는 개념을 사용하면, 텍스트 영역 내의 모든 픽셀값이 다 같은 값을 갖는 것이 아니라, 중앙에 가까울수록 더 높은 값, 멀수록 더 낮은 값을 갖게 된다. min(l*, r*)와 max(l*, r*) 간 격차가 적다는 것은 중앙에 가깝다는 의미이다.</p>
<p>![](Untitled 5.png)</p>
</li>
<li>
<p><a href="https://www.notion.so/MT-Multi-Perspective-Feature-Learning-Network-a6914981053f4d6b86e0cb07cf50fea2?pvs=21" target="_blank" rel="noopener noreferrer">MT</a>는 NMS 제거</p>
</li>
</ul>
</li>
<li>
<p><strong>Both Word &amp; Character prediction</strong></p>
<p>![](Untitled 6.png)</p>
<ul>
<li>사실 이 과정이 가장 궁금했다. CRAFT도 나온 heatmap으로부터 heatmap이나 affinity threshold를 조정하면 단어에서 글자 단위 탐색이 가능하지만, 정확하다고 하기엔 조금 무리가 있다.</li>
<li>Direct Regression이 character 단위로 결과를 뽑으면, offset으로 텍스트 영역을 깎는 과정에서 character들을 word로 합치는 과정이 있을 것 같다..?</li>
<li></li>
</ul>
</li>
</ul>
</li>
</ul>
<p>![](Untitled 7.png)</p>
<ul>
<li>centerness</li>
<li>offset</li>
<li>regression</li>
</ul>
<br>
<h1>텍스트 인식 모델(STR)</h1>
<p>![](Untitled 8.png)</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="as-is-1">AS-IS<a href="https://watanka.github.io/%EC%B9%B4%EC%B9%B4%EC%98%A4_ocr%EC%9D%84_%EB%94%B0%EB%9D%BC%ED%95%B4%EB%B3%B4%EC%9E%90#as-is-1" class="hash-link" aria-label="AS-IS에 대한 직접 링크" title="AS-IS에 대한 직접 링크">​</a></h3>
<ul>
<li>CNN + Self-Attention + CTC</li>
<li>NO TPS</li>
<li>Variable Length Input</li>
<li>Full Floating Point Precision</li>
</ul>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="to-be-1">TO-BE<a href="https://watanka.github.io/%EC%B9%B4%EC%B9%B4%EC%98%A4_ocr%EC%9D%84_%EB%94%B0%EB%9D%BC%ED%95%B4%EB%B3%B4%EC%9E%90#to-be-1" class="hash-link" aria-label="TO-BE에 대한 직접 링크" title="TO-BE에 대한 직접 링크">​</a></h3>
<p>![](Untitled 9.png)</p>
<ul>
<li>이 구조는 사실 <a href="https://github.com/roatienza/deep-text-recognition-benchmark" target="_blank" rel="noopener noreferrer">ViTSTR</a>과 동일한 구조다.</li>
<li>한 가지 인상적이였던 것은 이미지 패치를 16x16이 아닌 텍스트 방향에 맞춰서 (보통 왼쪽에서 오른쪽으로 예상) column-wise하게 잘랐다는 점이다.</li>
<li>STD 단계에서 orientation 방향을 구하는데, 이 값을 STR 단계에서 어떤 식으로 활용하는지 궁금하다.<!-- -->
<ul>
<li>딱 떠오르는 방법은 orientation 방향에 맞게 왼쪽에서 오른쪽으로 회전.</li>
</ul>
</li>
<li>듣고 보니 당연한 얘기인 것 같다.(훌륭한 논문의 개념을 다 읽고 나면, 마치 너무나도 당연해서 여태껏의 다른 방법들은 틀린 듯 느껴지는 것처럼)</li>
</ul>
<br>
<h1>학습 방법</h1>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="std">STD<a href="https://watanka.github.io/%EC%B9%B4%EC%B9%B4%EC%98%A4_ocr%EC%9D%84_%EB%94%B0%EB%9D%BC%ED%95%B4%EB%B3%B4%EC%9E%90#std" class="hash-link" aria-label="STD에 대한 직접 링크" title="STD에 대한 직접 링크">​</a></h2>
<p>![](Untitled 10.png)</p>
<ul>
<li>
<p>L1 Loss(예측값과 GT 차이를 절대값으로 계산) : 아마 STD에 들어가는 값 중, [Regression(박스 좌표), Offset] 에 사용될 듯</p>
</li>
<li>
<p>BCE(Binary Cross Entropy Loss)  : orientation과 centerness</p>
</li>
<li>
<p>OHEM : Online Hard Example Mining의 약자. 학습 이미지의 픽셀 loss값 기준 임계값보다 큰 픽셀들을 hard sample, loss 값이 작은 픽셀들을 easy sample로 구별한다. negative pixel(easy sample) 양이 positive pixel의 양이 3배(조절 가능한 값)보다 많을 경우, negative pixel의 값은 전체가 아닌 top n개만큼(n &lt; #negative pixel)만 반영한다.</p>
<p><strong>hard sample는 학습에 상대적으로 많이 반영하고, easy sample은 적게 반영한다는 컨셉.</strong></p>
<ul>
<li>
<p>OHEM이 사용된 예 (CRAFT)</p>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic"># 해당 코드는 [CRAFT-Reimplementation](https://github.com/backtime92/CRAFT-Reimplementation/blob/craft/loss/mseloss.py)에서 가져온 코드</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">import</span><span class="token plain"> numpy </span><span class="token keyword" style="color:#00009f">as</span><span class="token plain"> np</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">import</span><span class="token plain"> torch</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">import</span><span class="token plain"> torch</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">nn </span><span class="token keyword" style="color:#00009f">as</span><span class="token plain"> nn</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">class</span><span class="token plain"> </span><span class="token class-name">Maploss</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">nn</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Module</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">def</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">__init__</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">self</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> use_gpu </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">True</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token builtin">super</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">Maploss</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain">self</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">__init__</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">def</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">single_image_loss</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">self</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> pre_loss</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> loss_label</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        batch_size </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> pre_loss</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">shape</span><span class="token punctuation" style="color:#393A34">[</span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token comment" style="color:#999988;font-style:italic"># sum_loss = torch.mean(pre_loss.view(-1))*0</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token comment" style="color:#999988;font-style:italic"># pre_loss = pre_loss.view(batch_size, -1)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token comment" style="color:#999988;font-style:italic"># loss_label = loss_label.view(batch_size, -1)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        positive_pixel </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">loss_label </span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.1</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">.</span><span class="token builtin">float</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        positive_pixel_number </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> torch</span><span class="token punctuation" style="color:#393A34">.</span><span class="token builtin">sum</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">positive_pixel</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        positive_loss_region </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> pre_loss </span><span class="token operator" style="color:#393A34">*</span><span class="token plain"> positive_pixel</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        positive_loss </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> torch</span><span class="token punctuation" style="color:#393A34">.</span><span class="token builtin">sum</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">positive_loss_region</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> positive_pixel_number</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        negative_pixel </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">loss_label </span><span class="token operator" style="color:#393A34">&lt;=</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.1</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">.</span><span class="token builtin">float</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        negative_pixel_number </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> torch</span><span class="token punctuation" style="color:#393A34">.</span><span class="token builtin">sum</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">negative_pixel</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> negative_pixel_number </span><span class="token operator" style="color:#393A34">&lt;</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">3</span><span class="token operator" style="color:#393A34">*</span><span class="token plain">positive_pixel_number</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            negative_loss_region </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> pre_loss </span><span class="token operator" style="color:#393A34">*</span><span class="token plain"> negative_pixel</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            negative_loss </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> torch</span><span class="token punctuation" style="color:#393A34">.</span><span class="token builtin">sum</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">negative_loss_region</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> negative_pixel_number</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">else</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            negative_loss_region </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> pre_loss </span><span class="token operator" style="color:#393A34">*</span><span class="token plain"> negative_pixel</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">            negative_loss </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> torch</span><span class="token punctuation" style="color:#393A34">.</span><span class="token builtin">sum</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">torch</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">topk</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">negative_loss_region</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">view</span><span class="token punctuation" style="color:#393A34">(</span><span class="token operator" style="color:#393A34">-</span><span class="token number" style="color:#36acaa">1</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token builtin">int</span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">3</span><span class="token operator" style="color:#393A34">*</span><span class="token plain">positive_pixel_number</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">[</span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">positive_pixel_number</span><span class="token operator" style="color:#393A34">*</span><span class="token number" style="color:#36acaa">3</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token comment" style="color:#999988;font-style:italic"># negative_loss_region = pre_loss * negative_pixel</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token comment" style="color:#999988;font-style:italic"># negative_loss = torch.sum(negative_loss_region) / negative_pixel_number</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        total_loss </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> positive_loss </span><span class="token operator" style="color:#393A34">+</span><span class="token plain"> negative_loss</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> total_loss</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">def</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">forward</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">self</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> region_scores_label</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> affinity_socres_label</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> region_scores_pre</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> affinity_scores_pre</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> mask</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        loss_fn </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> torch</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">nn</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">MSELoss</span><span class="token punctuation" style="color:#393A34">(</span><span class="token builtin">reduce</span><span class="token operator" style="color:#393A34">=</span><span class="token boolean" style="color:#36acaa">False</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> size_average</span><span class="token operator" style="color:#393A34">=</span><span class="token boolean" style="color:#36acaa">False</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">assert</span><span class="token plain"> region_scores_label</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">size</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">==</span><span class="token plain"> region_scores_pre</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">size</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">and</span><span class="token plain"> affinity_socres_label</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">size</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">==</span><span class="token plain"> affinity_scores_pre</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">size</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        loss1 </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> loss_fn</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">region_scores_pre</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> region_scores_label</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        loss2 </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> loss_fn</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">affinity_scores_pre</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> affinity_socres_label</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        loss_region </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> torch</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">mul</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">loss1</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> mask</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        loss_affinity </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> torch</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">mul</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">loss2</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> mask</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        char_loss </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> self</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">single_image_loss</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">loss_region</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> region_scores_label</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        affi_loss </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> self</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">single_image_loss</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">loss_affinity</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> affinity_socres_label</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> char_loss </span><span class="token operator" style="color:#393A34">+</span><span class="token plain"> affi_loss</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="클립보드에 코드 복사" title="복사" class="clean-btn"><span class="copyButtonIcons_eSgA" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_y97N"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_LjdS"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
</li>
</ul>
</li>
</ul>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="str">STR<a href="https://watanka.github.io/%EC%B9%B4%EC%B9%B4%EC%98%A4_ocr%EC%9D%84_%EB%94%B0%EB%9D%BC%ED%95%B4%EB%B3%B4%EC%9E%90#str" class="hash-link" aria-label="STR에 대한 직접 링크" title="STR에 대한 직접 링크">​</a></h2>
<p>![](Untitled 11.png)</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="seqsimclr">SeqSimCLR<a href="https://watanka.github.io/%EC%B9%B4%EC%B9%B4%EC%98%A4_ocr%EC%9D%84_%EB%94%B0%EB%9D%BC%ED%95%B4%EB%B3%B4%EC%9E%90#seqsimclr" class="hash-link" aria-label="SeqSimCLR에 대한 직접 링크" title="SeqSimCLR에 대한 직접 링크">​</a></h3>
<p>실제 라벨된 데이터로부터 Data Augmentation을 진행하면서 SeqSimCLR 기법을 사용했다고 한다.</p>
<p>SimCLR의 개념은 이렇다.</p>
<ol>
<li>인풋 이미지에 서로 다른 데이터 증강 기법을 적용한다. (예를 들어, 하나는 색 변환, 하나는 rotation)</li>
<li>두 이미지를 네트워크에 태워서 Representation으로 표현한다.</li>
<li>다시 조그만 네트워크에 태워서 Projections로 뽑은 후, Contrastive Loss로 유사도 손실함수로 계산한다.</li>
</ol>
<p>![[출처] : <a href="https://zablo.net/blog/post/understanding-implementing-simclr-guide-eli5-pytorch/" target="_blank" rel="noopener noreferrer">https://zablo.net/blog/post/understanding-implementing-simclr-guide-eli5-pytorch/</a>](Untitled 12.png)</p>
<p><a href="https://deep-learning-study.tistory.com/731" target="_blank" rel="noopener noreferrer">https://deep-learning-study.tistory.com/731</a></p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="seqfixmatch">seqfixmatch<a href="https://watanka.github.io/%EC%B9%B4%EC%B9%B4%EC%98%A4_ocr%EC%9D%84_%EB%94%B0%EB%9D%BC%ED%95%B4%EB%B3%B4%EC%9E%90#seqfixmatch" class="hash-link" aria-label="seqfixmatch에 대한 직접 링크" title="seqfixmatch에 대한 직접 링크">​</a></h3>
<p><a href="https://github.com/google-research/fixmatch" target="_blank" rel="noopener noreferrer">https://github.com/google-research/fixmatch</a></p>
<p><a href="https://ainote.tistory.com/6" target="_blank" rel="noopener noreferrer">https://ainote.tistory.com/6</a></p>
<h1>Reference</h1>
<p><a href="https://if.kakao.com/session/73" target="_blank" rel="noopener noreferrer">if(kakao) 2021 : OCR 모델 개편 진행기</a></p>
<p><a href="https://arxiv.org/pdf/2012.10873v1.pdf" target="_blank" rel="noopener noreferrer">Sequence-to-Sequence Contrastive Learning for Text Recognition</a></p>
<p><a href="https://www.ecva.net/papers/eccv_2020/papers_ECCV/papers/123600154.pdf" target="_blank" rel="noopener noreferrer">PlugNet: Degradation Aware Scene Text Recognition Supervised by a Pluggable Super-Resolution Unit</a></p>
<p><a href="https://arxiv.org/pdf/2008.00714.pdf" target="_blank" rel="noopener noreferrer">AE TextSpotter: Learning Visual and Linguistic Representation for Ambiguous Text Spotting</a></p>
<p><a href="https://arxiv.org/pdf/2105.05455.pdf" target="_blank" rel="noopener noreferrer">MT: Multi-Perspective Feature Learning Network for Scene Text Detection</a></p>
<p><a href="https://arxiv.org/pdf/2108.01809.pdf" target="_blank" rel="noopener noreferrer">What's wrong with the Bottom-up Methods in Arbitrary-shape Scene Text Detection?</a></p>
<p><a href="https://neverabandon.tistory.com/m/60" target="_blank" rel="noopener noreferrer">https://neverabandon.tistory.com/m/60</a></p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[PAN++]]></title>
            <link>https://watanka.github.io/pan_plus_plus</link>
            <guid>https://watanka.github.io/pan_plus_plus</guid>
            <pubDate>Mon, 27 Dec 2021 00:00:00 GMT</pubDate>
            <description><![CDATA[이번 글은 2021년 8월에 나온 PAN++: Towards Efficient and Accurate End-to-End Spotting of Arbitrarily-Shaped Text의 리뷰이다. 텍스트 탐지(STD)와 텍스트 인식(STR)이 한 번에 이루어지는 End2End Text Spotting 모델이고, 기존의 End2End 모델들이나 STD(only) 모델들과 비교했을 때, 더 빠른 속도와 정확도를 갖는다고 한다. End2End Spotting 모델이긴 하지만, 이 글에서는 텍스트 탐지 과정이 어떻게 이루어지는지에 대해서만 중점적으로 다룰 예정이다.]]></description>
            <content:encoded><![CDATA[<p>이번 글은 2021년 8월에 나온 <a href="https://arxiv.org/pdf/2105.00405.pdf" target="_blank" rel="noopener noreferrer">PAN++: Towards Efficient and Accurate End-to-End Spotting of Arbitrarily-Shaped Text</a>의 리뷰이다. <strong>텍스트 탐지(STD)와 텍스트 인식(STR)이 한 번에 이루어지는 End2End Text Spotting</strong> 모델이고, 기존의 End2End 모델들이나 STD(only) 모델들과 비교했을 때, 더 빠른 속도와 정확도를 갖는다고 한다. End2End Spotting 모델이긴 하지만, 이 글에서는 텍스트 탐지 과정이 어떻게 이루어지는지에 대해서만 중점적으로 다룰 예정이다.</p>
<p><img decoding="async" loading="lazy" alt="PAN++와 기존 STD 모델 간의 Precision, Recall, F1 Measure 비교" src="https://watanka.github.io/assets/images/%EC%8A%A4%ED%81%AC%EB%A6%B0%EC%83%B7_2021-12-26_%EC%98%A4%ED%9B%84_12.19.51-d8178feea9bbc952a2abe3df4d1029ed.png" width="2264" height="1024" class="img_ev3q"></p>
<p>PAN++와 기존 STD 모델 간의 Precision, Recall, F1 Measure 비교</p>
<p><img decoding="async" loading="lazy" alt="PAN++와 기존 End2End text spotting 모델 간의 F-measure와 Inference Speed 비교" src="https://watanka.github.io/assets/images/%EC%8A%A4%ED%81%AC%EB%A6%B0%EC%83%B7_2021-12-26_%EC%98%A4%ED%9B%84_12.22.09-12db0e29bf6fb0ea5f74f2a0f313a58b.png" width="1146" height="988" class="img_ev3q"></p>
<p>PAN++와 기존 End2End text spotting 모델 간의 F-measure와 Inference Speed 비교</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="pan-모델의-탄생-배경">PAN++ 모델의 탄생 배경<a href="https://watanka.github.io/pan_plus_plus#pan-%EB%AA%A8%EB%8D%B8%EC%9D%98-%ED%83%84%EC%83%9D-%EB%B0%B0%EA%B2%BD" class="hash-link" aria-label="PAN++ 모델의 탄생 배경에 대한 직접 링크" title="PAN++ 모델의 탄생 배경에 대한 직접 링크">​</a></h3>
<p>텍스트 탐지가 어려운 이유는 탐지해야하는 텍스트의 형태가 매번 다르기 때문이다. 빳빳한 A4용지 위에 또렷하게 프린트된 텍스트는 탐지가 쉬운 반면, 종이가 구겨져있다거나, 잉크가 흐릿하다거나, 아니면 글자가 일직선이 아닌 다른 형태(폴리곤)로 적혀져있거나 하는 예외케이스들이 존재하기 때문이다.</p>
<p>텍스트 탐지 방법은 크게 1)예측값을 텍스트 탐지 박스 좌표로 잡는 regression-based와 2)텍스트 탐지 영역을 픽셀단위로 예측하는 segmentation-based로 나뉜다. 텍스트 형태가 일정한 사각형이 아닌 폴리곤의 형태일 때, 아래 그림 (b)처럼 탐지 박스의 네 좌표를 결괏값으로 갖는 regression based는 폴리곤 영역을 제대로 잡기가 힘들다. 좀 더 다양한 텍스트 형태를 탐지하기 위해서 고안된 <strong>segmentation based</strong> 방법은 아래 그림 (c)처럼 형태 자체는 잘 잡지만, 구분되어야할 텍스트 라인이 합쳐져서 잡힌다는 문제점이 있다. ‘we won’t go back,’ 과 ‘we will fight back!’ 텍스트는 각각 다른 텍스트 인스턴스로 분리되어 잡혀야 맞지만, <strong>텍스트 간의 간격이 가깝다보니, 여러 개의 텍스트 인스턴스들을 하나의 텍스트 박스로 인식(conglutination)하는 문제점</strong>이 발생한다.</p>
<p>PAN++(선행연구 PSENet, PAN)는 위 문제점을 해결하기 위해 고안된 모델로, 서로 다른 텍스트 인스턴스를 구분짓기 위해서 텍스트 영역 라벨링을 <strong>텍스트 영역,</strong> <strong>텍스트 커널</strong>, 그리고 <strong>텍스트 인스턴스,</strong> 총 세 가지로 구분하여 모델학습을 진행하고, <strong>Pixel Aggregation 기법</strong>을 사용해서 텍스트 인스턴스들을 구분한다.</p>
<p>** Regression-based와 Segmentation-based가 궁금하다면 이 <a href="https://www.notion.so/STD-Regression-vs-Segmentation-9d9ae48d53b94942b7cf640e48b20ae3?pvs=21" target="_blank" rel="noopener noreferrer">링크</a> 참조</p>
<p><img decoding="async" loading="lazy" alt="(a) 원본 이미지 (b) regression based 탐지 결과 (c) segmentation based 탐지 결과 (d) PAN++(segmentation based) 탐지 결과" src="https://watanka.github.io/assets/images/%EC%8A%A4%ED%81%AC%EB%A6%B0%EC%83%B7_2021-12-27_%EC%98%A4%ED%9B%84_1.36.01-bf981f5aeb620ea928800764f0629281.png" width="1206" height="1208" class="img_ev3q"></p>
<p>(a) 원본 이미지 (b) regression based 탐지 결과 (c) segmentation based 탐지 결과 (d) PAN++(segmentation based) 탐지 결과</p>
<h1>Model Architecture</h1>
<p>PAN++ 모델의 모델 구조는 크게 backbone, neck, head 세 단계로 이루어져있다.</p>
<p><img decoding="async" loading="lazy" alt="PAN++ 모델 구조" src="https://watanka.github.io/assets/images/%EC%8A%A4%ED%81%AC%EB%A6%B0%EC%83%B7_2021-12-26_%EC%98%A4%ED%9B%84_3.21.04-11b3c31f083c8c1b6b09a228f0b24683.png" width="1824" height="712" class="img_ev3q"></p>
<p>PAN++ 모델 구조</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="1-backbone">1. backbone<a href="https://watanka.github.io/pan_plus_plus#1-backbone" class="hash-link" aria-label="1. backbone에 대한 직접 링크" title="1. backbone에 대한 직접 링크">​</a></h3>
<p>인풋 이미지로부터 featuremap을 추출하는 백본망으로는 resnet18, 50, 101을 제공한다.</p>
<p><strong>백본망의 결과값으로는 인풋 이미지에 상응하는 featuremap이 총 4판</strong>이 나오는데, 각각 인풋 이미지 해상도의 1/4, 1/8, 1/16, 1/32를 갖는다. (다운샘플링하는 컨볼루션(kernel_size = 1, stride = 2)를 여러번 거칠수록, 해상도는 줄어들고, feature level은 높아진다. 해상도 1/32 featuremap은 해상도 1/4 featuremap보다 해상도는 낮지만, 더 high-level feature를 갖는다.)</p>
<p>이렇게 <strong>4가지의 해상도로 나누어서 featuremap을 추출하는 이유는 다양한 feature-level의 표현력(representation)을 학습에 잘 반영하기 위함</strong>이다.</p>
<p><img decoding="async" loading="lazy" alt="백본 인풋 (a)과 아웃풋 (b). 결괏값은 왼쪽부터 오른쪽으로 인풋이미지의 1/4, 1/8, 1/16, 1/32 해상도를 갖는다. " src="https://watanka.github.io/assets/images/%EC%8A%A4%ED%81%AC%EB%A6%B0%EC%83%B7_2021-12-26_%EC%98%A4%ED%9B%84_2.24.05-c89688a58140d865ad34337447744149.png" width="994" height="428" class="img_ev3q"></p>
<p>백본 인풋 (a)과 아웃풋 (b). 결괏값은 왼쪽부터 오른쪽으로 인풋이미지의 1/4, 1/8, 1/16, 1/32 해상도를 갖는다.</p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="2-neck">2. neck<a href="https://watanka.github.io/pan_plus_plus#2-neck" class="hash-link" aria-label="2. neck에 대한 직접 링크" title="2. neck에 대한 직접 링크">​</a></h3>
<p>4개의 featuremap은 컨볼루션 레이어를 거쳐 채널수를 128개로 고정되고(그림의 Reducing Channel 단계), N_stk개의 <strong>FPEM 모듈을 거쳐 인풋의 1/4 해상도와 512개의 채널을 갖는 아웃풋으로 변환</strong>된다. FPEM(Feature Pyramid Enhancement Module)은 <strong>마찬가지로 다양한 feature-level의 표현력을 학습에 반영하고, 신경망 층이 깊어짐에 따라 학습되어야할 값들이 희미해지는(gradient vanishing) 문제를 보완하기 위한 모듈</strong>이다.</p>
<p>** FPEM에 대해서 좀 더 궁금하다면 이 <a href="https://www.notion.so/FPEM-Feature-Pyramid-Enhancement-Module-ba91b72ae2184555a456ff26f3172a72?pvs=21" target="_blank" rel="noopener noreferrer">링크</a>를 참조</p>
<p><img decoding="async" loading="lazy" alt="스크린샷 2021-12-26 오후 2.47.44.png" src="https://watanka.github.io/assets/images/%EC%8A%A4%ED%81%AC%EB%A6%B0%EC%83%B7_2021-12-26_%EC%98%A4%ED%9B%84_2.47.44-2dcc3e093aa8f805fedcfc1ac00e0d21.png" width="912" height="576" class="img_ev3q"></p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="3-head">3. head<a href="https://watanka.github.io/pan_plus_plus#3-head" class="hash-link" aria-label="3. head에 대한 직접 링크" title="3. head에 대한 직접 링크">​</a></h3>
<p><img decoding="async" loading="lazy" alt="스크린샷 2021-12-26 오후 3.24.06.png" src="https://watanka.github.io/assets/images/%EC%8A%A4%ED%81%AC%EB%A6%B0%EC%83%B7_2021-12-26_%EC%98%A4%ED%9B%84_3.24.06-8ab95f5ce2ba21831004330edeff836d.png" width="1100" height="692" class="img_ev3q"></p>
<p>neck에서 받은 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>H</mi><mi mathvariant="normal">/</mi><mn>4</mn><mo>∗</mo><mi>W</mi><mi mathvariant="normal">/</mi><mn>4</mn><mo>∗</mo><mn>512</mn></mrow><annotation encoding="application/x-tex">H/4*W/4*512</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em"></span><span class="mord mathnormal" style="margin-right:0.08125em">H</span><span class="mord">/4</span><span class="mspace" style="margin-right:0.2222em"></span><span class="mbin">∗</span><span class="mspace" style="margin-right:0.2222em"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em"></span><span class="mord mathnormal" style="margin-right:0.13889em">W</span><span class="mord">/4</span><span class="mspace" style="margin-right:0.2222em"></span><span class="mbin">∗</span><span class="mspace" style="margin-right:0.2222em"></span></span><span class="base"><span class="strut" style="height:0.6444em"></span><span class="mord">512</span></span></span></span> 형태의 featuremap(위 그림 (e)  <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msub><mi>F</mi><mi>f</mi></msub></mrow><annotation encoding="application/x-tex">F_f</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.9694em;vertical-align:-0.2861em"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.13889em">F</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3361em"><span style="top:-2.55em;margin-left:-0.1389em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight" style="margin-right:0.10764em">f</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.2861em"><span></span></span></span></span></span></span></span></span></span> )을 활용해서 Text Region, Text Kernel, Instance Vector에 대한 세 가지 아웃풋을 예측한다. 세 가지 아웃풋을 종합해서 Pixel Aggregation 기법으로 최종 아웃풋에 해당하는 Text Masks를 도출한다. 그리고 Text Masks로 얻은 텍스트 영역(Masked RoI; <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msub><mi>F</mi><mi>f</mi></msub></mrow><annotation encoding="application/x-tex">F_f</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.9694em;vertical-align:-0.2861em"></span><span class="mord"><span class="mord mathnormal" style="margin-right:0.13889em">F</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3361em"><span style="top:-2.55em;margin-left:-0.1389em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight" style="margin-right:0.10764em">f</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.2861em"><span></span></span></span></span></span></span></span></span></span> 위 Text Masks 영역을 투영(reflect)시킴)에서 텍스트가 어떤 문자인지 예측하는 텍스트 인식(STR) 단계를 수행한다. 이 중 우리는 Text Region, Text Kernel, Instance Vector가 무엇인지 그리고 어떻게 Pixel Aggregation을 통해 최종 아웃풋을 도출하는지 집중해서 살펴볼 것이다.</p>
<h1>PAN++의 아웃풋 형식</h1>
<p>텍스트 이미지를 라벨링(또는 어노테이션)하는 방법은 여느 이미지 라벨링과 비슷하다. 텍스트 인스턴스의 단위를 어떻게 설정할지나 라벨링에 [UNK]와 같은 특수한 태그를 추가해서 데이터 처리에 활용하는 등 세부적인 사항에서 그 편차가 있을 수 있겠지만, 텍스트 영역에 4점 이상의 박스를 그리고, 텍스트 내용으로 라벨링하는 것이 일반적이다. 원하는 결과를 도출하기 위해서 이 보편적인 라벨링을 PAN++에서는 어떤 식으로 활용하는지 주목하자. (라벨링 형태 자체를 변환하기 위해서는 많은 비용이 들기 때문에, 딥러닝 모델들은 기존의 라벨링을 잘 활용해서 독창적인 기법을 만들어내곤 한다.)</p>
<p><img decoding="async" loading="lazy" alt="보편적인 텍스트 라벨링" src="https://watanka.github.io/assets/images/%EC%8A%A4%ED%81%AC%EB%A6%B0%EC%83%B7_2021-12-26_%EC%98%A4%ED%9B%84_9.02.56-d13f78011a5b1ab47507bb0fd1ee04ac.png" width="1018" height="508" class="img_ev3q"></p>
<p>PAN++는 이 보편적인 라벨링을 Text Region, Text Kernel, 그리고 Instance Vector, 총 세 가지의 값을 활용한다.</p>
<ul>
<li>Text Region : 라벨링된 텍스트 박스와 동일하다. Segmentation map에 <code>cv2.drawContours</code> 함수로 마킹한다. 형태는 원본 이미지 기준 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>H</mi><mi mathvariant="normal">/</mi><mn>4</mn><mo>∗</mo><mi>W</mi><mi mathvariant="normal">/</mi><mn>4</mn><mo>∗</mo><mn>1</mn></mrow><annotation encoding="application/x-tex">H/4*W/4*1</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em"></span><span class="mord mathnormal" style="margin-right:0.08125em">H</span><span class="mord">/4</span><span class="mspace" style="margin-right:0.2222em"></span><span class="mbin">∗</span><span class="mspace" style="margin-right:0.2222em"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em"></span><span class="mord mathnormal" style="margin-right:0.13889em">W</span><span class="mord">/4</span><span class="mspace" style="margin-right:0.2222em"></span><span class="mbin">∗</span><span class="mspace" style="margin-right:0.2222em"></span></span><span class="base"><span class="strut" style="height:0.6444em"></span><span class="mord">1</span></span></span></span> 형태다.</li>
<li>Text Kernel : 서로 다른 텍스트 인스턴스들이 가까워서 하나의 인스턴스로 잡히는 문제(conglutination)를 해결하기 위해서 고안되었다. 아래의 그림처럼 Text Region을 텍스트 박스 중심으로 응축(shrink)시킨다. shrink 구현은 <code>pyclipper</code> 모듈을 활용한다. 형태는 원본 이미지 기준 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>H</mi><mi mathvariant="normal">/</mi><mn>4</mn><mo>∗</mo><mi>W</mi><mi mathvariant="normal">/</mi><mn>4</mn><mo>∗</mo><mn>1</mn></mrow><annotation encoding="application/x-tex">H/4*W/4*1</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em"></span><span class="mord mathnormal" style="margin-right:0.08125em">H</span><span class="mord">/4</span><span class="mspace" style="margin-right:0.2222em"></span><span class="mbin">∗</span><span class="mspace" style="margin-right:0.2222em"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em"></span><span class="mord mathnormal" style="margin-right:0.13889em">W</span><span class="mord">/4</span><span class="mspace" style="margin-right:0.2222em"></span><span class="mbin">∗</span><span class="mspace" style="margin-right:0.2222em"></span></span><span class="base"><span class="strut" style="height:0.6444em"></span><span class="mord">1</span></span></span></span> 형태다.</li>
<li>Text Instance : Text Kernel과 마찬가지로 위 conglutination 문제를 해결하기 위해서 고안되었다. 각 텍스트 박스는 고유한 Text Instance이다. Segmentation map에 <code>cv2.drawContours</code> 함수, for loop와 enumerate 함수로 서로 다른 픽셀값으로 색칠하여 고유한 인스턴스를 마킹한다. (영역이 겹칠 경우, 뒤에 오는 인스턴스 넘버로 마킹?) 형태는 원본 이미지 기준 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>H</mi><mi mathvariant="normal">/</mi><mn>4</mn><mo>∗</mo><mi>W</mi><mi mathvariant="normal">/</mi><mn>4</mn><mo>∗</mo><mi>D</mi></mrow><annotation encoding="application/x-tex">H/4*W/4*D</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em"></span><span class="mord mathnormal" style="margin-right:0.08125em">H</span><span class="mord">/4</span><span class="mspace" style="margin-right:0.2222em"></span><span class="mbin">∗</span><span class="mspace" style="margin-right:0.2222em"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em"></span><span class="mord mathnormal" style="margin-right:0.13889em">W</span><span class="mord">/4</span><span class="mspace" style="margin-right:0.2222em"></span><span class="mbin">∗</span><span class="mspace" style="margin-right:0.2222em"></span></span><span class="base"><span class="strut" style="height:0.6833em"></span><span class="mord mathnormal" style="margin-right:0.02778em">D</span></span></span></span> 형태다. (코드 상에는 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>D</mi><mo>=</mo><mn>4</mn><mo stretchy="false">)</mo></mrow><annotation encoding="application/x-tex">D=4)</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6833em"></span><span class="mord mathnormal" style="margin-right:0.02778em">D</span><span class="mspace" style="margin-right:0.2778em"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em"></span><span class="mord">4</span><span class="mclose">)</span></span></span></span></li>
</ul>
<p><img decoding="async" loading="lazy" alt="텍스트 라벨링에서 text kernel을 도출하는 방법" src="https://watanka.github.io/assets/images/%EC%8A%A4%ED%81%AC%EB%A6%B0%EC%83%B7_2021-12-26_%EC%98%A4%ED%9B%84_8.42.38-403d60aea3b2fd02867546524f3e6276.png" width="1420" height="450" class="img_ev3q"></p>
<p>텍스트 라벨링에서 text kernel을 도출하는 방법</p>
<p><img decoding="async" loading="lazy" alt="스크린샷 2021-12-27 오전 9.53.05.png" src="https://watanka.github.io/assets/images/%EC%8A%A4%ED%81%AC%EB%A6%B0%EC%83%B7_2021-12-27_%EC%98%A4%EC%A0%84_9.53.05-11b72930724fc6ce5d62e7036b7fc0ff.png" width="866" height="238" class="img_ev3q"></p>
<p>아래는 PAN++의 아웃풋 예시이다.</p>
<p><img decoding="async" loading="lazy" alt="원본 이미지" src="https://watanka.github.io/assets/images/%EC%8A%A4%ED%81%AC%EB%A6%B0%EC%83%B7_2021-12-27_%EC%98%A4%ED%9B%84_1.35.00-ed07b50f47c3cd09d97e9c478af88b89.png" width="1206" height="1200" class="img_ev3q"></p>
<p>원본 이미지</p>
<p><img decoding="async" loading="lazy" alt="Text Region" src="https://watanka.github.io/assets/images/%EC%8A%A4%ED%81%AC%EB%A6%B0%EC%83%B7_2021-12-27_%EC%98%A4%ED%9B%84_1.35.32-f5baddd28097ea0b6a377abc72a1eb61.png" width="1202" height="1206" class="img_ev3q"></p>
<p>Text Region</p>
<p><img decoding="async" loading="lazy" alt="Text Kernel(Text Region보다 중심으로 응축된 영역)" src="https://watanka.github.io/assets/images/%EC%8A%A4%ED%81%AC%EB%A6%B0%EC%83%B7_2021-12-27_%EC%98%A4%ED%9B%84_1.35.44-2bc34a936ce88c69642c5a8b934fb86b.png" width="1208" height="1202" class="img_ev3q"></p>
<p>Text Kernel(Text Region보다 중심으로 응축된 영역)</p>
<p><img decoding="async" loading="lazy" alt="Text Instance (고유한 텍스트 박스 마스킹)" src="https://watanka.github.io/assets/images/%EC%8A%A4%ED%81%AC%EB%A6%B0%EC%83%B7_2021-12-27_%EC%98%A4%ED%9B%84_1.36.01-bf981f5aeb620ea928800764f0629281.png" width="1206" height="1208" class="img_ev3q"></p>
<p>Text Instance (고유한 텍스트 박스 마스킹)</p>
<h1>손실 함수</h1>
<p>이제 우리에게 Text Region, Text kernel, 그리고 Text Instance에 대한 정보가 생겼다. 이 정보를 <strong>Train 단계에서</strong> 어떻게 활용해야 <strong>가까운 두 개의 텍스트가 겹치지 않고 서로 다른 텍스트 인스턴스로 구분되면서 텍스트 영역을 충분히 커버</strong>할 수 있을까?</p>
<p><img decoding="async" loading="lazy" alt="text kernel i(ki; 파랑)는 text region만큼 확장하지만, 다른 커널(주황색)과 겹치지 않아야한다." src="https://watanka.github.io/assets/images/%EC%8A%A4%ED%81%AC%EB%A6%B0%EC%83%B7_2021-12-26_%EC%98%A4%ED%9B%84_10.00.19-9cd36f6cb91c5c0cd541c53edcd5db48.png" width="856" height="352" class="img_ev3q"></p>
<p>text kernel i(ki; 파랑)는 text region만큼 확장하지만, 다른 커널(주황색)과 겹치지 않아야한다.</p>
<p>탐지 손실함수는 text loss, kernel loss, 그리고 emb loss 총 세 종류의 <strong>loss의 합</strong>으로 이루어진다.</p>
<p><img decoding="async" loading="lazy" alt="스크린샷 2021-12-26 오후 10.57.10.png" src="https://watanka.github.io/assets/images/%EC%8A%A4%ED%81%AC%EB%A6%B0%EC%83%B7_2021-12-26_%EC%98%A4%ED%9B%84_10.57.10-97fa3a7133493bce09ef4f36b4b01d20.png" width="324" height="134" class="img_ev3q"></p>
<ul>
<li>
<p><strong>text loss</strong> : 텍스트 영역에 대해서 dice loss 적용. dice loss는 예측값과 정답값 간 겹치는 영역을 2로 곱한 후, 예측값과 정답값을 더한 값을 나누는 손실함수로 segmentation based 모델에서 자주 쓰인다.</p>
</li>
<li>
<p><strong>kernel loss</strong> : 텍스트 커널에 대해서도 마찬가지로 dice loss를 적용한다.</p>
</li>
<li>
<p><strong>emb loss</strong> : Text kernel은 Text Region이 응축된 영역이기 때문에, 다른 인스턴스와 겹칠 일은 거의 없다. 이 텍스트 커널에서 픽셀 영역을 조금씩 확장해나가면 어느 지점에서는 Text Region을 충분히 커버하지만, 거기서 더 나아가게되면 다른 Text Instance와 겹치게 된다. 모델의 학습방향은 Text Kernel이 1) 다른 인스턴스 영역과는 구분(<strong>discrimination loss</strong>)되면서, 2) Text Region만큼은 확장(<strong>aggregation loss</strong>)하는 것이다.</p>
<p>aggregation loss</p>
<p><img decoding="async" loading="lazy" alt="F(p) = instance vector, g(K) = instance vector of text kernel. D1은 instance vector와 instance kernel 간의 거리를 나타낸다" src="https://watanka.github.io/assets/images/%EC%8A%A4%ED%81%AC%EB%A6%B0%EC%83%B7_2021-12-27_%EC%98%A4%ED%9B%84_2.31.58-837ab88564e430c6920bae3071e441af.png" width="706" height="182" class="img_ev3q"></p>
<p>F(p) = instance vector, g(K) = instance vector of text kernel. D1은 instance vector와 instance kernel 간의 거리를 나타낸다</p>
<p>discrimination loss</p>
<p><img decoding="async" loading="lazy" alt="background와 인스턴스 간의 거리와 서로 다른 인스턴스 간의 거리를 나타낸다. 서로 다른 인스턴스 간의 거리를 계산하기 위해서 torch.repeat 과 torch.eye 를 활용한 것이 주목할만 하다. " src="https://watanka.github.io/assets/images/%EC%8A%A4%ED%81%AC%EB%A6%B0%EC%83%B7_2021-12-27_%EC%98%A4%ED%9B%84_2.31.31-e8c97551abef74f6fb7bf99048508835.png" width="804" height="342" class="img_ev3q"></p>
<p>background와 인스턴스 간의 거리와 서로 다른 인스턴스 간의 거리를 나타낸다. 서로 다른 인스턴스 간의 거리를 계산하기 위해서 <code>torch.repeat</code> 과 <code>torch.eye</code> 를 활용한 것이 주목할만 하다.</p>
</li>
</ul>
<h1>Pixel Aggregation</h1>
<p>위 손실함수를 잘 반영해서 학습하면, 꽤 정교한 아웃풋(text region, kernel, instance)을 얻을 수 있다. 더 정교한 결과를 위해서 PAN++는 <strong>inference단계에서는</strong> pixel aggregation 기법을 사용한다.</p>
<p><img decoding="async" loading="lazy" alt="스크린샷 2021-12-27 오후 3.10.40.png" src="https://watanka.github.io/assets/images/%EC%8A%A4%ED%81%AC%EB%A6%B0%EC%83%B7_2021-12-27_%EC%98%A4%ED%9B%84_3.10.40-a883d60ccc8b04cd5e72bf6f2edb9860.png" width="1040" height="366" class="img_ev3q"></p>
<ol>
<li>결괏값(코드상 kernels, emb)에 대해서 connected component 기법을 적용한다. (connected component 기법은 같은 값을 갖는 인접한 픽셀들을 그룹화해주는 기법이다) 그룹화된 pixel들은 text kernel과 같은 의미를 갖는다.</li>
<li>각각의 kernel은 4방향으로 픽셀을 확장해나간다(BFS와 같은 방식). 탐색하는 픽셀의 instance vector 간의 유클리디언 거리가 d보다 작을 경우에만 확장하여, 다른 instance들과 겹치는 일이 없도록 한다</li>
<li>근접 픽셀이 없을 때까지 두번째 스텝을 반복한다.</li>
</ol>
<p>해당 pixel aggregation 코드는 Cython으로 짜여있다. 파이썬에서 <code>cv2</code>나 <code>numpy</code> 라이브러리를 활용해도 충분히 구현이 가능하지만, 속도가 느린 편인데, 해당 코드는 정말 빠르다. Cython으로 다른 python code들과 함께 구동가능하다는 점도 주목할만하다.</p>
<h1>Outro</h1>
<p>End2End Text Spotting 모델 PAN++의 텍스트 탐지(STD) 부분에 대해서 살펴보았다. PAN++는 실제로 다른 우수한 텍스트 탐지 모델들(CRAFT)과 비교했을 때, 굉장히 성능도 잘 나오고 빠른 편이다. 모델 구조, 라벨링을 활용하는 방법, 커스텀한 손실함수, 그리고 inference 시 후처리까지 아주 꼼꼼하게 설계되어 있기 때문에 가능한 성능이지 않나 싶다.</p>
<p>해당 논문과 모델 코드까지 천천히 뜯어보면서 논문 하나(모델 하나)를 만들기 위해서 저자들이 얼마나 많은 실험과 머리를 쥐어짜는 고심을 했을지 생각해보았다. 모델을 구성하는 단계마다 최신/최선의 기법들을 적용해서 0.001%의 정확도라도 더 끌어올리려고한 노고가 느껴졌다. 이 글에 논문의 최대한 많은 내용을 담으려고 노력했지만, 모델의 STR 부분, 모듈 구성 시의 builder 패턴, 학습 시의 ohem 기법, pixel aggregation의 cython 익스텐딩 등 커버하지 못한 부분도 많다. (누락된 부분에 대해서는 차차 살펴보면서 머릿속에 익히려고 한다)</p>
<p>한편으로 코드를 천천히 뜯어보고 정리하면서 배운 점 또한 많다.</p>
<ul>
<li>모델을 구성하는 모듈들은 어떻게 정리하는 게 깔끔한지 알게 되었다. (<a href="https://github.com/watanka/CRAFTS-implementation" target="_blank" rel="noopener noreferrer">CRAFTS 코드 구현</a>할 때, 가장 힘들었던 부분이 코드가 지저분한데, 방대한 양을 어떻게 정리해야할지 모르겠었다는 것)</li>
<li>가지고 있는 데이터를 활용해서 원하는 아웃풋을 만들어내는 것도 모델 성능을 높이기 위한 하나의 방법이라는 것을 알게 되었다. (높은 정확도까지 보장된다면 새로운 논문이 나올지도..?)</li>
<li>그 외에 자잘한 함수들(<code>torch.Conv2d</code>  의 <code>groups</code> 인자, <code>torch.repeat</code> 과 <code>torch.eye</code> 로 두 개의 for문 처리 등등..)</li>
</ul>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[OCR이란?]]></title>
            <link>https://watanka.github.io/ocr이란</link>
            <guid>https://watanka.github.io/ocr이란</guid>
            <pubDate>Sat, 20 Nov 2021 00:00:00 GMT</pubDate>
            <description><![CDATA[Intro]]></description>
            <content:encoded><![CDATA[<h2 class="anchor anchorWithStickyNavbar_LWe7" id="intro">Intro<a href="https://watanka.github.io/ocr%EC%9D%B4%EB%9E%80#intro" class="hash-link" aria-label="Intro에 대한 직접 링크" title="Intro에 대한 직접 링크">​</a></h2>
<p>OCR(Optical Character Recognition)은 이미지 속의 글자를 읽는 기술이다. 조금 생소할 수도 있는 이름인 OCR은 생각보다 <a href="https://medium.com/naver-cloud-platform/%EC%9D%B4%EB%A0%87%EA%B2%8C-%EC%82%AC%EC%9A%A9%ED%95%98%EC%84%B8%EC%9A%94-ocr%EC%9D%84-%EC%96%B4%EB%96%BB%EA%B2%8C-%ED%99%9C%EC%9A%A9%ED%95%A0%EA%B9%8C%EC%9A%94-ada22fb3c3ca#:~:text=%EB%AC%B8%EC%84%9C%EB%A5%BC%20%EC%9D%B8%EC%8B%9D%ED%95%98%EA%B3%A0%20%EC%82%AC%EC%9A%A9%EC%9E%90,%EB%8B%A4%EC%96%91%ED%95%98%EA%B2%8C%20%ED%99%9C%EC%9A%A9%EB%90%98%EA%B3%A0%20%EC%9E%88%EC%8A%B5%EB%8B%88%EB%8B%A4.&amp;text=%EB%84%A4%EC%9D%B4%EB%B2%84%20%ED%81%B4%EB%A1%9C%EB%B0%94%EC%9D%98%20OCR%20%EA%B8%B0%EC%88%A0,%ED%95%98%EC%97%AC%20%EC%A0%95%ED%99%95%EB%8F%84%EB%A5%BC%20%EB%86%92%EC%9D%B4%EA%B3%A0%20%EC%9E%88%EC%8A%B5%EB%8B%88%EB%8B%A4." target="_blank" rel="noopener noreferrer">우리의 일상 속 깊숙이 자리하고 있다</a>. OCR은 신용카드, 사업자등록증이나 주민등록증, 또는 영수증 등 필요한 서류 정보를 촬영만 하면 전자 정보로 변환 가능하게 해준다. 또, 차량번호판 정보를 추출해서 불법 주차나 속도 위반 차량을 파악하기도 한다. 그리고 아직 <a href="https://www.joongang.co.kr/article/23961049#home" target="_blank" rel="noopener noreferrer">종이 문서에서 전자 문서로의 전환</a>이 진행중인 기업들이 한 장씩 문서 내용을 직접 체크해야하는 부담을 줄여 업무 자동화를 가속화해준다. 유입되는 데이터의 크기가 커지고 있는 빅데이터 시대에 사는 우리에게 '사람의 눈을 일일히 거치지 않고 이미지에서 필요한 텍스트 정보만을 추출할 수 있게 되었다'라는 것은 <strong>빅데이터를 우리의 편의에 맞게 더 잘 활용할 수 있는 옵션이 생겼음을 의미</strong>한다.</p>
<p><img decoding="async" loading="lazy" alt="OCR이 실생활에 사용되는 예" src="https://watanka.github.io/assets/images/ocr-samples-8d04f8312784b4540ced17b2588ffbda.png" width="1548" height="434" class="img_ev3q"></p>
<p>2019년, AI 자동차수리비 산출시스템 프로젝트를 진행중이였을 때였다. 파손된 차량의 사진을 휴대폰 촬영해서 앱에 업로드하면 수리 금액이 얼마가 나올지 예측하는 이미지 딥러닝 모델을 개발 중이였다. 여느때처럼 퇴근하고 집에 돌아가는 길, 같은 지하철을 타는 팀장님에게 이런 질문을 했었다.</p>
<p>'저희 프로젝트처럼 사물 자체가 아닌 사물의 손상 심도까지 파악할 정도로 이미지 모델의 성능이 좋다면, 글자들을 인식하는 것도 상품화가 가능하지 않을까요?'</p>
<p>그 때 팀장님의 답변은 아직도 기억에 남는다.</p>
<p>"이미지 모델의 형태가 점점 발전하고 있기는 하지만, 결국 이미지 안의 물체의 영역을 탐지하고, 해당 물체가 미리 정의해놓은 클래스의 범주 내에서 분류되는 이미지 모델의 틀은 쉽게 변하기 힘들어요. 한글로 나올 수 있는 한 글자짜리 조합만 생각해도 대략 6, 7천개(<a href="https://m.blog.naver.com/khsamuel/221442007316" target="_blank" rel="noopener noreferrer">링크</a>)쯤은 될 거고, ****거기다 종이 한장에 보통 글자가 못해도 300개 400개 쯤은 될 거예요. 그럼 그 문서 한 장에 나오는 글자들을 모두 올바르게 맞출 확률은 얼마나 될까요? 그리고 이 글자 읽는 태스크를 컴퓨터로 대체하려면 적어도 100장 이상, 아니 10,000장 정도는 거뜬히 처리할 줄 알아야 현장에 사용이 가능할 텐데, 그렇게 된다면 정확도는 얼마 정도가 보장될까요? 그런데 또 모르죠. 나중에는 그 정확도를 확 올릴 수 있는 기술들이 나올수도..."</p>
<p>자고 일어나면 최신 기술들이 뒤바뀔만큼 미친 속도로 새로운 기술들이 쏟아져 나왔고, 왜 문서를 AI 이미지 모델로 처리해서 정확도를 뽑는 게 힘든지 설명을 해주셨던 팀장님과 우리 팀은 2년째 OCR 제품을 개발중이다.</p>
<br>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="ocr-컴퓨터가-사람-대신-글을-읽어주는-일">OCR. 컴퓨터가 사람 대신 글을 읽어주는 일.<a href="https://watanka.github.io/ocr%EC%9D%B4%EB%9E%80#ocr-%EC%BB%B4%ED%93%A8%ED%84%B0%EA%B0%80-%EC%82%AC%EB%9E%8C-%EB%8C%80%EC%8B%A0-%EA%B8%80%EC%9D%84-%EC%9D%BD%EC%96%B4%EC%A3%BC%EB%8A%94-%EC%9D%BC" class="hash-link" aria-label="OCR. 컴퓨터가 사람 대신 글을 읽어주는 일.에 대한 직접 링크" title="OCR. 컴퓨터가 사람 대신 글을 읽어주는 일.에 대한 직접 링크">​</a></h2>
<p><img decoding="async" loading="lazy" alt="OCR. 이미지를 보고 컴퓨터가 인식할 수 있는 문자로 변환하는 일." src="https://watanka.github.io/assets/images/ocr-abstraction-8b50b4a270c0e5f110409f44843f5f3e.png" width="596" height="220" class="img_ev3q"></p>
<p>OCR. 이미지를 보고 컴퓨터가 인식할 수 있는 문자로 변환하는 일.</p>
<p>OCR은 어떻게 이뤄질까?</p>
<p>회사마다 어떤 모델을 쓰고, 어떤 단계에 기준을 두고 모델을 설계하는지는 조금씩 다르겠지만, 보통 OCR은 2단계 또는 3단계로 이루어진다. 2단계는 이미지 중 텍스트 영역을 탐지하는 1)문자 영역 탐지(STD)단계와, 탐지한 영역의 텍스트를 인식하는 2)문자 인식(STR)단계, 그리고 문서 이미지의 경우 인식한 텍스트 정보를 분류하고 구조화하는 3)문서 이해(Document Understanding) 단계까지 3단계로 구성된다.</p>
<br>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="1-문자-영역-탐지-scene-text-detectionstd">1. <strong>문자 영역 탐지 (Scene Text Detection;STD)</strong><a href="https://watanka.github.io/ocr%EC%9D%B4%EB%9E%80#1-%EB%AC%B8%EC%9E%90-%EC%98%81%EC%97%AD-%ED%83%90%EC%A7%80-scene-text-detectionstd" class="hash-link" aria-label="1-문자-영역-탐지-scene-text-detectionstd에 대한 직접 링크" title="1-문자-영역-탐지-scene-text-detectionstd에 대한 직접 링크">​</a></h3>
<p>이미지에는 한 개 이상의 문자 영역이 존재할 수 있다. 정확한 문자 인식을 위해 문자가 속하는 영역을 탐지한다. 잘못 잘린 문자 영역은 문자 인식의 성능을 떨어트리지만, 반대로 알맞게 잘린 문자 영역은 문서 이해(Document Understanding) 단계의 알맞은 단위로 작용한다.</p>
<p>Scene Text Detection 관련 최신 모델들의 정보는 **<a href="https://paperswithcode.com/task/scene-text-detection" target="_blank" rel="noopener noreferrer">여기</a>**에서 찾을 수 있다.</p>
<p><img decoding="async" loading="lazy" src="https://watanka.github.io/assets/images/std-process-860f9cf999ee3f74adb39ff9817a6e3d.png" width="1704" height="523" class="img_ev3q"></p>
<br>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="2-문자-인식-scene-text-recognitionstr">2. <strong>문자 인식 (Scene Text Recognition;STR)</strong><a href="https://watanka.github.io/ocr%EC%9D%B4%EB%9E%80#2-%EB%AC%B8%EC%9E%90-%EC%9D%B8%EC%8B%9D-scene-text-recognitionstr" class="hash-link" aria-label="2-문자-인식-scene-text-recognitionstr에 대한 직접 링크" title="2-문자-인식-scene-text-recognitionstr에 대한 직접 링크">​</a></h3>
<p>STD(문자 영역 탐지)단계에서 잘라낸 문자 이미지로부터 컴퓨터 문자로 인식하는 단계이다. 보통 글자 하나 하나를 개별로 인식하는 것이 아니라, 1개 이상의 글자로 이루어진 단어 단위로 인식한다. 글자가 1개 이상이다 보니, 글자와 글자 간의 순차적 종속성(sequential dependency)이 존재한다. 예를 들어, 앞에 두 글자가 '아이스크'였다면 그 다음에 오는 글자는 '림'일 확률이 높다. 이런 순차적인(sequential) 특성을 활용하기 위해서, Bi-LSTM, Attention, 그리고 최근에는 Transformer 모델을 활용한다. (<a href="https://www.notion.so/e181266b3b624c959e832c86343d435e?pvs=21" target="_blank" rel="noopener noreferrer">이 글</a>에서 Sequential Data를 처리하기 위한 모델들에 대해서 읽을 수 있다.)</p>
<p>STR 관련 최신 모델들의 정보는 <a href="https://paperswithcode.com/task/scene-text-recognition" target="_blank" rel="noopener noreferrer"><strong>여기</strong></a>에서 찾을 수 있다.</p>
<p><img decoding="async" loading="lazy" src="https://watanka.github.io/assets/images/str-process-da005d155888b65c7e572a5cc93075ed.png" width="1134" height="302" class="img_ev3q"></p>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="3-문서-이해-document-understanding">3. <strong>문서 이해 (Document Understanding)</strong><a href="https://watanka.github.io/ocr%EC%9D%B4%EB%9E%80#3-%EB%AC%B8%EC%84%9C-%EC%9D%B4%ED%95%B4-document-understanding" class="hash-link" aria-label="3-문서-이해-document-understanding에 대한 직접 링크" title="3-문서-이해-document-understanding에 대한 직접 링크">​</a></h3>
<p>STD와 STR 단계를 무사히 거쳐 원하는 컴퓨터 문자를 추출해냈다고 하자. 다음엔 뭐가 필요할까? 추출해낸 문자들을 컴퓨터가 이해할 수 있는 방식으로 정리하는 작업이 필요하다.</p>
<p>아래 표 이미지를 중앙에 위치한 글자들로 추출해냈다고 하자. 사람은 맨 윗쪽 행이 각 열들을 나타내는 열이름(column name)이라는 것, 그리고 맨 왼쪽의 열은 행이름(row name), 그리고 옆의 행들은 각 행들의 정보를 나타낸다는 사실을 쉽게 알아차릴 수 있다. (Matt의 수학 점수는 60점, 물리학 점수는 65점) 하지만, 컴퓨터는 그렇지 않다. 중앙 글자들(정보)로부터 구조화하는 과정이 필요하다. 올바른 문서 이해 과정을 거친다면, 아래 그림의 세번째 표를 뽑아낼 수 있을 것이다. 참고로, 테이블화한다는 것은 [('Name' : 'Matt', 'Math' : 80, 'Physics' : 65, 'History' : 80, 'English' : 74), ...]와 같이 json, xml 등과 같은 포맷으로 구조화할 수 있음을 의미한다. 여기서 중요한 점은 글자에 대한 정보 뿐만 아니라, 글자의 위치, 그리고 그 글자 주변의 표 성분, 즉 사람이 문맥에 맞춰 글자를 읽기 위해 시각적으로 사용하는 정보들이 문서 이해 과정에서 고려되어야 한다는 것이다.</p>
<p>문서 이해 관련 최신 모델들의 논문 정보는 **<a href="https://paperswithcode.com/search?q_meta=&amp;q_type=&amp;q=document+understanding" target="_blank" rel="noopener noreferrer">여기</a>**에서 찾을 수 있다.</p>
<p><img decoding="async" loading="lazy" src="https://watanka.github.io/assets/images/understanding-process-4170824c1fdf4429b92e4b3951a58e4b.png" width="1856" height="391" class="img_ev3q"></p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="ocr을-하면서-생길-수-있는-문제들">OCR을 하면서 생길 수 있는 문제들<a href="https://watanka.github.io/ocr%EC%9D%B4%EB%9E%80#ocr%EC%9D%84-%ED%95%98%EB%A9%B4%EC%84%9C-%EC%83%9D%EA%B8%B8-%EC%88%98-%EC%9E%88%EB%8A%94-%EB%AC%B8%EC%A0%9C%EB%93%A4" class="hash-link" aria-label="OCR을 하면서 생길 수 있는 문제들에 대한 직접 링크" title="OCR을 하면서 생길 수 있는 문제들에 대한 직접 링크">​</a></h2>
<p>물론 위 3단계를 문제없이 진행하면 이상적이겠지만, AI 모델에 99.9999%의 정확도는 있더라도 100% 정확도는 없다. 각 단계별 인식 측면에서 다양한 문제들이 발생한다.</p>
<br>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="1-문자-영역-탐지std-단계">1. 문자 영역 탐지(STD) 단계<a href="https://watanka.github.io/ocr%EC%9D%B4%EB%9E%80#1-%EB%AC%B8%EC%9E%90-%EC%98%81%EC%97%AD-%ED%83%90%EC%A7%80std-%EB%8B%A8%EA%B3%84" class="hash-link" aria-label="1. 문자 영역 탐지(STD) 단계에 대한 직접 링크" title="1. 문자 영역 탐지(STD) 단계에 대한 직접 링크">​</a></h3>
<p><strong>목적 : 인식 대상 텍스트를 이미지로부터 캡쳐</strong></p>
<ol>
<li>
<p>글자 미탐지/오탐지</p>
<ol>
<li>
<p>오탈자 발생 ⇒ 문서 이해 단계에서 정보가 빠지는 경우 발생</p>
</li>
<li>
<p>오탐지(글자가 아닌 오점을 문자로 인식한 경우)</p>
<p>⇒ 인식 단계에서 잘못된 글자로 인식</p>
</li>
</ol>
</li>
<li>
<p>탐지 박스 단위</p>
<ol>
<li>원래 하나의 박스를 두 개 이상의 박스로 잡는 경우</li>
<li>반대로 두 개 이상의 박스를 하나로 잡는 경우</li>
</ol>
<p>![<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mn>10</mn><mo separator="true">,</mo><mn>000</mn><mtext>를</mtext><msup><mo stretchy="false">[</mo><mo mathvariant="normal" lspace="0em" rspace="0em">′</mo></msup></mrow><annotation encoding="application/x-tex">10,000를 ['</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1.0019em;vertical-align:-0.25em"></span><span class="mord">10</span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.1667em"></span><span class="mord">000</span><span class="mord hangul_fallback">를</span><span class="mopen"><span class="mopen">[</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.7519em"><span style="top:-3.063em;margin-right:0.05em"><span class="pstrut" style="height:2.7em"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mtight">′</span></span></span></span></span></span></span></span></span></span></span></span>10', '000']으로 탐지했다. 상금 만 달러가 10달러로 줄어든다면 아무도 좋아하지 않을 거다.](misdetection-example.png)</p>
</li>
</ol>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="2-문자-인식str-단계">2. 문자 인식(STR) 단계<a href="https://watanka.github.io/ocr%EC%9D%B4%EB%9E%80#2-%EB%AC%B8%EC%9E%90-%EC%9D%B8%EC%8B%9Dstr-%EB%8B%A8%EA%B3%84" class="hash-link" aria-label="2. 문자 인식(STR) 단계에 대한 직접 링크" title="2. 문자 인식(STR) 단계에 대한 직접 링크">​</a></h3>
<p><strong>목적 : STD단계에서 캡쳐한 텍스트 이미지를 전자 문자로 알맞게 변환</strong></p>
<ol>
<li>글자 방향에 따른 오인식<!-- -->
<ul>
<li>
<p>왼쪽에서 오른쪽으로 나열된 단어들만 학습하다가 다른 방향으로 나열(수직이나 오른쪽에서 왼쪽 나열)된 단어 이미지가 들어오는 경우</p>
<p><img decoding="async" loading="lazy" alt="직사각형의 글자 이미지가 아니라 폴리곤(polygon) 형태의 이미지가 들어온다면?" src="https://watanka.github.io/assets/images/exceptional-cases-003db6e42f0875bd6558cb9e3a58c6cd.png" width="647" height="382" class="img_ev3q"></p>
<p><img decoding="async" loading="lazy" src="https://watanka.github.io/assets/images/vertical-alignment-example-7b22aa6630da3f267600126ffab63b58.png" width="1600" height="1200" class="img_ev3q"></p>
</li>
</ul>
</li>
<li>학습 데이터에 기반한 인식 오류<!-- -->
<ul>
<li>
<p>학습한 폰트와 아예 다른(인식이 불가능할 정도의) 폰트를 사용한 텍스트인 경우</p>
</li>
<li>
<p>숫자 '0'과 자음 'ㅇ', 숫자 '1'과 모음 'ㅣ' 처럼 문맥을 고려하지 않으면 구분하기 힘든 단일 문자들</p>
<p><img decoding="async" loading="lazy" alt="손글씨 폰트체다. 과연 고딕체의 직각 글자 이미지를 학습한 모델이 손글씨를 인식할 수 있을까?" src="https://watanka.github.io/assets/images/handwritten-75abb983c3549d06c36bf1e4a1c0875f.png" width="1600" height="1200" class="img_ev3q"></p>
</li>
</ul>
</li>
</ol>
<h3 class="anchor anchorWithStickyNavbar_LWe7" id="3-문서-이해-단계-rule-based일-경우">3. 문서 이해 단계 (Rule-Based일 경우)<a href="https://watanka.github.io/ocr%EC%9D%B4%EB%9E%80#3-%EB%AC%B8%EC%84%9C-%EC%9D%B4%ED%95%B4-%EB%8B%A8%EA%B3%84-rule-based%EC%9D%BC-%EA%B2%BD%EC%9A%B0" class="hash-link" aria-label="3. 문서 이해 단계 (Rule-Based일 경우)에 대한 직접 링크" title="3. 문서 이해 단계 (Rule-Based일 경우)에 대한 직접 링크">​</a></h3>
<p><strong>목적 : 이미지에서 추출한 정보들을 구조화</strong></p>
<ol>
<li>기존 유형과 다른 문서 구조에 대한 인식 오류<!-- -->
<ol>
<li>같은 문서 유형이지만 문서 구성이 다른 경우</li>
</ol>
</li>
<li>앞에 STD와 STR에서 오류가 발생한 경우</li>
</ol>
<br>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="outro">Outro<a href="https://watanka.github.io/ocr%EC%9D%B4%EB%9E%80#outro" class="hash-link" aria-label="Outro에 대한 직접 링크" title="Outro에 대한 직접 링크">​</a></h2>
<p>OCR 진행 시 위에 적어놓은 문제 외에도 많은 문제들이 존재한다. 그 중, 현재 OCR 모델 구조 상 해결이 불가능한 문제들이 생기면 머리가 아파진다. 예를 들어, STD모델의 학습 데이터 탐지 단위가 '단어'였는데, '글자' 단위의 탐지가 필요한 경우라거나  Wild-Scene 이미지 인식 시에 사용중인 STR모델의 아웃풋에는 없는 '글자 방향'이 필요한 경우. 그럴 때면, '현재 모델 구조를 유지한 채로 성능을 고도화한다' 첫번째 옵션과 '새로운 모델을 사용한다' 두번째 옵션 중에 갈팡질팡하게 되는데, 이는 결코 쉬운 과정이 아니다.</p>
<p>뭐가 맞을지는 아무도 모른다. 기간 안에 에러를 감안하고도 도달할 수 있는 최고 성능을 뽑아내는 것 외에 정해진 건 없다. 몇 년 전만 해도 내가 OCR제품을 개발하고 있으리라곤 상상도 못 했던 것처럼. 다만, 한 가지 모델만 오래 사용하다보면, 그 모델이 이미지를 처리하는 방식과 뽑는 아웃풋에 사고 구조가 굳어지는 경우가 있는데, 사고가 굳지 않도록 계속해서 새로운 모델 구조를 공부해나가는 과정이 필요하다.</p>]]></content:encoded>
        </item>
    </channel>
</rss>