<?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>Global Dev Blog Blog</title>
        <link>https://globaldevlog.com/ko/blog</link>
        <description>Global Dev Blog Blog</description>
        <lastBuildDate>Sun, 18 Oct 2026 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[45개 npm 취약점 경고와 무위험의 진실: 정적 사이트에서 npm audit fix --force를 맹신하면 안 되는 이유]]></title>
            <link>https://globaldevlog.com/ko/blog/static-site-security-npm-audit-truth</link>
            <guid>https://globaldevlog.com/ko/blog/static-site-security-npm-audit-truth</guid>
            <pubDate>Sun, 18 Oct 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[정적 사이트(SSG) 환경에서 번들러 도구의 45개 취약점 경고가 왜 런타임에 무해한지 분석하고, npm audit fix --force가 사이트를 망가뜨리는 원인과 4단계 실전 보안 하드닝 방안을 공유합니다.]]></description>
            <content:encoded><![CDATA[<p>프론트엔드나 정적 블로그를 운영하는 개발자라면 누구나 한 번쯤 등골이 서늘해지는 순간을 겪어보셨을 것입니다. 평화롭게 <code>npm audit</code>을 입력했을 때, 터미널 가득 시뻘건 경고 문구가 쏟아져 내리는 장면입니다:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">45 vulnerabilities (15 moderate, 15 high, 15 critical)</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">To address all issues (including breaking changes), run:</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  npm audit fix --force</span><br></div></code></pre></div></div>
<p><strong>Critical</strong>, <strong>Remote Code Execution (원격 코드 실행)</strong>, <strong>Prototype Pollution (프로토타입 오염)</strong> 같은 무시무시한 단어들을 보면, 당장 빨간 불을 끄기 위해 무의식적으로 <code>npm audit fix --force</code>를 타이핑하고 싶어집니다.</p>
<p>하지만 Docusaurus, Next.js, Hugo처럼 **정적 사이트 생성기(SSG)**를 통해 빌드된 후 GitHub Pages나 AWS S3에 배포되는 아키텍처에서는, 이러한 무지성 다운그레이드가 오히려 멀쩡한 프로덕션 빌드를 깨뜨리는 최악의 악수가 되곤 합니다.</p>
<p>본 포스팅에서는 **정적 웹 사이트의 위협 모델(Threat Model)**을 명확히 정의하고, 빌드 도구의 취약점과 런타임 취약점의 본질적인 차이를 파헤치며, 정적 블로그에서 진짜로 지켜야 할 <strong>4단계 실전 보안 하드닝(Security Hardening) 청사진</strong>을 공유합니다.</p>
<!-- -->
<hr>
<p><img decoding="async" loading="lazy" alt="위협 모델 비교: 동적 웹 서버 vs 정적 사이트" src="https://globaldevlog.com/ko/assets/images/01_threat_model-4770df6c58430b3bba205124a37d91ab.png" width="2000" height="1000" class="img_ev3q"></p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="1-3줄-핵심-요약-엔지니어링-판단-기준">1. 3줄 핵심 요약: 엔지니어링 판단 기준<a href="https://globaldevlog.com/ko/blog/static-site-security-npm-audit-truth#1-3%EC%A4%84-%ED%95%B5%EC%8B%AC-%EC%9A%94%EC%95%BD-%EC%97%94%EC%A7%80%EB%8B%88%EC%96%B4%EB%A7%81-%ED%8C%90%EB%8B%A8-%EA%B8%B0%EC%A4%80" class="hash-link" aria-label="1. 3줄 핵심 요약: 엔지니어링 판단 기준에 대한 직접 링크" title="1. 3줄 핵심 요약: 엔지니어링 판단 기준에 대한 직접 링크" translate="no">​</a></h2>
<ol>
<li class=""><strong>빌드 도구 != 프로덕션 런타임</strong>: <code>npm audit</code>에 걸린 패키지(<code>tinypool</code>, <code>serialize-javascript</code>, <code>postcss-selector-parser</code> 등)는 개발자의 로컬 PC에서 <code>npm run build</code>를 수행할 때만 실행되는 번들러 워커입니다.</li>
<li class=""><strong>CDN에는 살아있는 서버가 없다</strong>: 빌드가 끝나면 GitHub Pages에는 순수 HTML, CSS, 번들 JS 파일만 남습니다. <strong>실행 중인 Node.js 데몬도 없고, 열린 백엔드 포트도 없으며, 데이터베이스 커넥션도 없습니다.</strong></li>
<li class=""><strong><code>--force</code> 옵션의 치명적 함정</strong>: <code>npm audit fix --force</code>를 실행하면 취약점을 없앤답시고 프레임워크(<code>@docusaurus/core</code>)를 구버전(3.7.0)으로 강제 다운그레이드하여 React 19 호환성과 플러그인을 완전히 망가뜨립니다.</li>
<li class=""><strong>정적 사이트의 진짜 위협</strong>: 정적 웹의 핵심 공격 벡터는 서버 침투가 아니라, <strong>Git 커밋 내 API 키 유출</strong>, <strong>데이터셋 내 개인정보(PII) 노출</strong>, <strong>클라이언트 리퍼러 경로 유출</strong>입니다.</li>
</ol>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="2-45개-취약점의-실체-분석-빌드타임-vs-런타임">2. 45개 취약점의 실체 분석: 빌드타임 vs 런타임<a href="https://globaldevlog.com/ko/blog/static-site-security-npm-audit-truth#2-45%EA%B0%9C-%EC%B7%A8%EC%95%BD%EC%A0%90%EC%9D%98-%EC%8B%A4%EC%B2%B4-%EB%B6%84%EC%84%9D-%EB%B9%8C%EB%93%9C%ED%83%80%EC%9E%84-vs-%EB%9F%B0%ED%83%80%EC%9E%84" class="hash-link" aria-label="2. 45개 취약점의 실체 분석: 빌드타임 vs 런타임에 대한 직접 링크" title="2. 45개 취약점의 실체 분석: 빌드타임 vs 런타임에 대한 직접 링크" translate="no">​</a></h2>
<p>이번 보안 점검에서 검출된 주요 CVE 항목들을 구체적으로 뜯어보겠습니다:</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="a-tinypool-프로토타입-오염-ghsa-5gmw-xhrv-c9v3---critical">A. Tinypool 프로토타입 오염 (<code>GHSA-5gmw-xhrv-c9v3</code> - Critical)<a href="https://globaldevlog.com/ko/blog/static-site-security-npm-audit-truth#a-tinypool-%ED%94%84%EB%A1%9C%ED%86%A0%ED%83%80%EC%9E%85-%EC%98%A4%EC%97%BC-ghsa-5gmw-xhrv-c9v3---critical" class="hash-link" aria-label="a-tinypool-프로토타입-오염-ghsa-5gmw-xhrv-c9v3---critical에 대한 직접 링크" title="a-tinypool-프로토타입-오염-ghsa-5gmw-xhrv-c9v3---critical에 대한 직접 링크" translate="no">​</a></h3>
<ul>
<li class=""><strong>취약점 내용</strong>: 신뢰할 수 없는 외부 입력값이 워커 초기화 옵션에 주입될 경우 프로토타입 오염 발생 가능.</li>
<li class=""><strong>SSG 환경에서의 진실</strong>: <code>tinypool</code>은 빌드 속도를 높이기 위해 로컬 CPU 코어에 번들링 작업을 분산시키는 라이브러리입니다. 입력값은 오직 내 컴퓨터의 <code>docusaurus.config.ts</code> 설정 파일뿐입니다. 외부 방문자가 배포된 웹사이트를 통해 내 빌드 워커에 악의적인 페이로드를 보낼 수 있는 통로는 존재하지 않습니다.</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="b-serialize-javascript-rce-ghsa-5c6j-r48x-rmvq---high">B. Serialize-JavaScript RCE (<code>GHSA-5c6j-r48x-rmvq</code> - High)<a href="https://globaldevlog.com/ko/blog/static-site-security-npm-audit-truth#b-serialize-javascript-rce-ghsa-5c6j-r48x-rmvq---high" class="hash-link" aria-label="b-serialize-javascript-rce-ghsa-5c6j-r48x-rmvq---high에 대한 직접 링크" title="b-serialize-javascript-rce-ghsa-5c6j-r48x-rmvq---high에 대한 직접 링크" translate="no">​</a></h3>
<ul>
<li class=""><strong>취약점 내용</strong>: 정규식 플래그나 날짜 객체를 역직렬화할 때 원격 코드 실행(RCE) 가능성.</li>
<li class=""><strong>SSG 환경에서의 진실</strong>: 웹팩(Webpack)이 컴파일 결과물을 정적 청크로 묶을 때 내부적으로 사용합니다. <code>build/</code> 폴더로 출력된 정적 JS 파일은 HTTPS로 서빙되는 정적 텍스트일 뿐, 서버에서 역직렬화 코드가 실행되지 않습니다.</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="c-postcss-selector-parser-cpu-과부하-ghsa-rj75-hqrm-r3gf---moderate">C. PostCSS Selector Parser CPU 과부하 (<code>GHSA-rj75-hqrm-r3gf</code> - Moderate)<a href="https://globaldevlog.com/ko/blog/static-site-security-npm-audit-truth#c-postcss-selector-parser-cpu-%EA%B3%BC%EB%B6%80%ED%95%98-ghsa-rj75-hqrm-r3gf---moderate" class="hash-link" aria-label="c-postcss-selector-parser-cpu-과부하-ghsa-rj75-hqrm-r3gf---moderate에 대한 직접 링크" title="c-postcss-selector-parser-cpu-과부하-ghsa-rj75-hqrm-r3gf---moderate에 대한 직접 링크" translate="no">​</a></h3>
<ul>
<li class=""><strong>취약점 내용</strong>: 플랫 셀렉터 파싱 시 2차 복잡도로 인한 CPU 고갈(DoS) 가능성.</li>
<li class=""><strong>SSG 환경에서의 진실</strong>: 파싱 대상 CSS는 개발자가 작성한 테마 스타일시트(<code>custom.css</code>)입니다. 공격자가 깃허브 저장소 쓰기 권한을 탈취하지 않는 한, 신뢰할 수 없는 CSS가 빌드 파이프라인에 주입될 수 없습니다.</li>
</ul>
<p>결론적으로, 이 취약점들을 잡겠다고 <code>npm audit fix --force</code>를 돌리면 <strong>발생할 수 없는 가상의 위협을 막기 위해 실제로 작동하는 최신 기능(React 19, Docusaurus v4 플래그)을 부수는 꼴</strong>이 됩니다.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="3-정적-블로그에서-진짜-지켜야-할-것-4단계-실전-보안-청사진">3. 정적 블로그에서 진짜 지켜야 할 것: 4단계 실전 보안 청사진<a href="https://globaldevlog.com/ko/blog/static-site-security-npm-audit-truth#3-%EC%A0%95%EC%A0%81-%EB%B8%94%EB%A1%9C%EA%B7%B8%EC%97%90%EC%84%9C-%EC%A7%84%EC%A7%9C-%EC%A7%80%EC%BC%9C%EC%95%BC-%ED%95%A0-%EA%B2%83-4%EB%8B%A8%EA%B3%84-%EC%8B%A4%EC%A0%84-%EB%B3%B4%EC%95%88-%EC%B2%AD%EC%82%AC%EC%A7%84" class="hash-link" aria-label="3. 정적 블로그에서 진짜 지켜야 할 것: 4단계 실전 보안 청사진에 대한 직접 링크" title="3. 정적 블로그에서 진짜 지켜야 할 것: 4단계 실전 보안 청사진에 대한 직접 링크" translate="no">​</a></h2>
<p>빌드 도구의 오탐(False Positive)에 휘둘리지 않는다면, 정적 블로그 엔지니어는 어디에 보안 노력을 집중해야 할까요? 이번에 본 블로그에 적용한 4단계 방어 체계를 소개합니다.</p>
<p><img decoding="async" loading="lazy" alt="4단계 정적 블로그 보안 하드닝 파이프라인" src="https://globaldevlog.com/ko/assets/images/02_security_pipeline-120240a37282a9b3b578a5dc4f6231c0.png" width="2000" height="900" class="img_ev3q"></p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="1단계-git-프리커밋-시크릿-스캐닝-자동화">1단계: Git 프리커밋 시크릿 스캐닝 자동화<a href="https://globaldevlog.com/ko/blog/static-site-security-npm-audit-truth#1%EB%8B%A8%EA%B3%84-git-%ED%94%84%EB%A6%AC%EC%BB%A4%EB%B0%8B-%EC%8B%9C%ED%81%AC%EB%A6%BF-%EC%8A%A4%EC%BA%90%EB%8B%9D-%EC%9E%90%EB%8F%99%ED%99%94" class="hash-link" aria-label="1단계: Git 프리커밋 시크릿 스캐닝 자동화에 대한 직접 링크" title="1단계: Git 프리커밋 시크릿 스캐닝 자동화에 대한 직접 링크" translate="no">​</a></h3>
<p>공개 기술 블로그에서 발생할 수 있는 가장 치명적인 재앙은, 코드 예제나 트러블슈팅 글을 작성하다가 실수로 OpenAI API 키(<code>sk-...</code>), GitHub 토큰(<code>ghp_...</code>), AWS 인증키(<code>AKIA...</code>)를 커밋하는 일입니다.</p>
<p>이를 방지하기 위해 Git 추적 파일 전체를 정규식 패턴으로 자동 검사하는 보안 감사 스크립트(<a href="file:///D:/ai/antigravity/global_blog/tools/audit_security.py" target="_blank" rel="noopener noreferrer" class=""><code>tools/audit_security.py</code></a>)를 제작하여 배포 전 전수 검사를 실행했습니다:</p>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">PATTERNS </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"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">r'(?i)(api[_-]?key|secret|token|password|auth|bearer)\s*[:=]\s*["\']([^"\']{8,})["\']'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"Potential Key Assignment"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">r'ghp_[a-zA-Z0-9]{36}'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"GitHub Personal Access Token"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">r'sk-[a-zA-Z0-9]{32,}'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"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></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">r'AIzaSy[a-zA-Z0-9_-]{33}'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"Google 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></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">r'AKIA[0-9A-Z]{16}'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"AWS Access Key"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">r'-----BEGIN\s+(RSA\s+)?PRIVATE\s+KEY-----'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"Private Key Certificate"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">]</span><br></div></code></pre></div></div>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="2단계-개인정보pii-및-임상-데이터-필터링">2단계: 개인정보(PII) 및 임상 데이터 필터링<a href="https://globaldevlog.com/ko/blog/static-site-security-npm-audit-truth#2%EB%8B%A8%EA%B3%84-%EA%B0%9C%EC%9D%B8%EC%A0%95%EB%B3%B4pii-%EB%B0%8F-%EC%9E%84%EC%83%81-%EB%8D%B0%EC%9D%B4%ED%84%B0-%ED%95%84%ED%84%B0%EB%A7%81" class="hash-link" aria-label="2단계: 개인정보(PII) 및 임상 데이터 필터링에 대한 직접 링크" title="2단계: 개인정보(PII) 및 임상 데이터 필터링에 대한 직접 링크" translate="no">​</a></h3>
<p>인공지능 연구나 비전 AI 프로젝트를 포스팅할 때는 데이터셋의 개인정보 보호가 필수적입니다:</p>
<ul>
<li class=""><strong>마음지기 23편 연재</strong>: 실제 상담 기록이 아닌, HIPAA 및 임상 윤리를 준수하기 위해 대규모 언어 모델로 생성한 **합성 대화 데이터(<code>synth_gen</code>)**를 학습에 사용하여 개인정보 유출을 원천 차단했습니다.</li>
<li class=""><strong>정규식 PII 검사</strong>: 주민등록번호(<code>\d{6}-[1-4]\d{6}</code>)나 휴대폰 번호(<code>010-\d{4}-\d{4}</code>)가 본문에 포함되지 않았는지 자동 검증했습니다.</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="3단계-gitignore-보안-하드닝">3단계: <code>.gitignore</code> 보안 하드닝<a href="https://globaldevlog.com/ko/blog/static-site-security-npm-audit-truth#3%EB%8B%A8%EA%B3%84-gitignore-%EB%B3%B4%EC%95%88-%ED%95%98%EB%93%9C%EB%8B%9D" class="hash-link" aria-label="3단계-gitignore-보안-하드닝에 대한 직접 링크" title="3단계-gitignore-보안-하드닝에 대한 직접 링크" translate="no">​</a></h3>
<p>기본 템플릿의 <code>.gitignore</code>는 <code>.env.local</code>만 제외하고 정작 <code>.env</code>는 누락하는 경우가 많습니다. 환경 변수, 인증서, 통계 CSV 덤프가 저장소에 절대 올라가지 않도록 규칙을 철저히 보강했습니다:</p>
<div class="language-gitignore codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-gitignore codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain"># 환경 변수 및 시크릿 보호</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">.env</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">.env.*</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">*.pem</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">*.key</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">*.pfx</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">*.cert</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">*credential*</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">*.secret</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"># 분석 데이터 및 CSV 덤프 보호</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">*.csv</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"># 아카이브된 템플릿 파일</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">_old/</span><br></div></code></pre></div></div>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="4단계-클라이언트-프라이버시--댓글-iframe-샌드박싱">4단계: 클라이언트 프라이버시 &amp; 댓글 iframe 샌드박싱<a href="https://globaldevlog.com/ko/blog/static-site-security-npm-audit-truth#4%EB%8B%A8%EA%B3%84-%ED%81%B4%EB%9D%BC%EC%9D%B4%EC%96%B8%ED%8A%B8-%ED%94%84%EB%9D%BC%EC%9D%B4%EB%B2%84%EC%8B%9C--%EB%8C%93%EA%B8%80-iframe-%EC%83%8C%EB%93%9C%EB%B0%95%EC%8B%B1" class="hash-link" aria-label="4단계: 클라이언트 프라이버시 &amp; 댓글 iframe 샌드박싱에 대한 직접 링크" title="4단계: 클라이언트 프라이버시 &amp; 댓글 iframe 샌드박싱에 대한 직접 링크" translate="no">​</a></h3>
<p>백엔드 서버가 없더라도, 사이트를 방문하는 독자의 개인정보를 보호해야 합니다:</p>
<ol>
<li class="">
<p><strong>엄격한 리퍼러 정책 (<code>strict-origin-when-cross-origin</code>)</strong>:<br>
<!-- -->방문자가 블로그 본문 내의 외부 링크를 클릭했을 때, 블로그의 상세 URL 쿼리나 방문 경로가 외부 서버로 전달되지 않도록 도메인 단위로 차단합니다.</p>
<div class="language-typescript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-typescript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic">// docusaurus.config.ts</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">themeConfig</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"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  metadata</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"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" 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 string" style="color:#e3116c">'referrer'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> content</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'strict-origin-when-cross-origin'</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></div><div 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></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></div></code></pre></div></div>
</li>
<li class="">
<p><strong>Giscus 댓글창 보안 설정</strong>:<br>
<!-- -->GitHub Discussions 기반 댓글 시스템을 운영할 때는 <code>BlogPostItem/index.tsx</code>에서:</p>
<ul>
<li class=""><code>emitMetadata="0"</code>: iframe 내부의 토론 메타데이터가 외부 스크립트로 브로드캐스트되지 않도록 차단.</li>
<li class=""><code>loading="lazy"</code>: 글 맨 아래로 스크롤하기 전까지 불필요한 iframe 리소스를 로드하지 않음.</li>
</ul>
</li>
</ol>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="4-엔지니어를-위한-최종-제언">4. 엔지니어를 위한 최종 제언<a href="https://globaldevlog.com/ko/blog/static-site-security-npm-audit-truth#4-%EC%97%94%EC%A7%80%EB%8B%88%EC%96%B4%EB%A5%BC-%EC%9C%84%ED%95%9C-%EC%B5%9C%EC%A2%85-%EC%A0%9C%EC%96%B8" class="hash-link" aria-label="4. 엔지니어를 위한 최종 제언에 대한 직접 링크" title="4. 엔지니어를 위한 최종 제언에 대한 직접 링크" translate="no">​</a></h2>
<ul>
<li class=""><strong><code>npm audit</code>의 빨간 글씨에 당황하지 마세요</strong>: 취약점이 보고되었을 때 해당 패키지가 <strong>내 로컬 빌드 도구</strong>인지, <strong>사용자 요청을 직접 받는 프로덕션 서버</strong>인지 먼저 구분하는 것이 진정한 보안 엔지니어링의 시작입니다.</li>
<li class=""><strong><code>--force</code>를 맹신하지 마세요</strong>: 발생하지도 않을 가상의 위협을 막기 위해 프레임워크를 강제 다운그레이드하여 시스템을 깨뜨리는 것은 보안이 아닙니다.</li>
<li class=""><strong>진짜 중요한 곳에 집중하세요</strong>: Git 시크릿 스캐닝 자동화, 개인정보 배제, 리퍼러 정책 및 올바른 컴플라이언스 준수가 여러분의 기술 블로그를 진정으로 안전하고 신뢰받게 만듭니다.</li>
</ul>]]></content:encoded>
            <category>웹보안</category>
            <category>npm-audit</category>
            <category>Docusaurus</category>
            <category>데브옵스</category>
            <category>공급망보안</category>
        </item>
        <item>
            <title><![CDATA[[Vision AI] 한국형 번호판 OCR 최적화, 정규식 검증 및 시간 축 Majority Voting 앙상블 (v01~v11 완결편) (#5)]]></title>
            <link>https://globaldevlog.com/ko/blog/2026-06-29-vision-anpr-5</link>
            <guid>https://globaldevlog.com/ko/blog/2026-06-29-vision-anpr-5</guid>
            <pubDate>Mon, 29 Jun 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[한국 자동차 번호판 정규표현식 검증, EasyOCR 오인식 보정, 그리고 외계어 노이즈에서 64가1511 확정까지의 시간 축 Majority Voting 엔진과 v01~v11 11단계 진화 결산.]]></description>
            <content:encoded><![CDATA[<p><img decoding="async" loading="lazy" alt="시간 축 Majority Voting 및 최종 확정 화면" src="https://globaldevlog.com/ko/assets/images/01_voting_evolution-02da50ebc7e87755c32fbc132dc9b992.png" width="2131" height="1599" class="img_ev3q"></p>
<blockquote>
<p>📌 <strong>Key Takeaways (핵심 요약)</strong></p>
<ul>
<li class=""><strong>문제점</strong>: 단일 프레임 OCR은 차량 이동 시 그림자나 각도 변화에 따라 <code>64아1511</code>, <code>64가7151</code>, <code>24가1511</code> 등 한두 글자의 일시적 오인식이 불가피하게 발생합니다.</li>
<li class=""><strong>해결책</strong>: 한국 번호판 규격 정규식(<code>^\d{2,3}[가-힣]\d{4}$</code>) 필터링과, 최근 7프레임 슬라이딩 윈도우에서 3회 이상 일치 시 번호를 확정하는 시간 축 Majority Voting 앙상블 엔진을 구현했습니다.</li>
<li class=""><strong>결과</strong>: 6월 5일 초기 외계어 노이즈(<code>ACCIIXOY-1Z01...</code>)부터 6월 7일 완벽한 <code>64가1511</code> 확정에 이르기까지 11단계 리팩토링(v01~v11)을 거쳐 99.4%의 최종 판독 신뢰도를 달성했습니다.</li>
</ul>
</blockquote>
<!-- -->
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="1-한국형-자동차-번호판-규격과-easyocr-전처리">1. 한국형 자동차 번호판 규격과 EasyOCR 전처리<a href="https://globaldevlog.com/ko/blog/2026-06-29-vision-anpr-5#1-%ED%95%9C%EA%B5%AD%ED%98%95-%EC%9E%90%EB%8F%99%EC%B0%A8-%EB%B2%88%ED%98%B8%ED%8C%90-%EA%B7%9C%EA%B2%A9%EA%B3%BC-easyocr-%EC%A0%84%EC%B2%98%EB%A6%AC" class="hash-link" aria-label="1. 한국형 자동차 번호판 규격과 EasyOCR 전처리에 대한 직접 링크" title="1. 한국형 자동차 번호판 규격과 EasyOCR 전처리에 대한 직접 링크" translate="no">​</a></h2>
<p>한국 자동차 번호판은 엄격한 법적 구성을 따릅니다:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">규격 구성: [2자리 또는 3자리 차종 숫자] + [1자리 한글 용도 기호] + [4자리 일련번호]</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">예시: 64가1511, 123가4567</span><br></div></code></pre></div></div>
<p>EasyOCR 모델에 초해상화된 번호판 이미지를 입력할 때, 우리는 불필요한 바운딩 박스 출력을 끄고 텍스트 전처리 정규식을 결합했습니다:</p>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">import</span><span class="token plain"> easyocr</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">import</span><span class="token plain"> re</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">from</span><span class="token plain"> collections </span><span class="token keyword" style="color:#00009f">import</span><span class="token plain"> deque</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> Counter</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">reader </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> easyocr</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Reader</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">[</span><span class="token string" style="color:#e3116c">'ko'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'en'</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> gpu</span><span class="token operator" style="color:#393A34">=</span><span class="token boolean" style="color:#36acaa">True</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div 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">parse_korean_plate</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">plate_img</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    results </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> reader</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">readtext</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">plate_img</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> detail</span><span class="token operator" 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></div><div class="token-line" style="color:#393A34"><span class="token plain">    raw_text </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">""</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">join</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">results</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">replace</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 plain"> </span><span class="token string" style="color:#e3116c">""</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token comment" style="color:#999988;font-style:italic"># 한국 번호판 정규표현식 매칭</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    pattern </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">r'(\d{2,3}[가-힣]\d{4})'</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">match</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> re</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">search</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">pattern</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> raw_text</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div 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">match</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div 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><span class="token keyword" style="color:#00009f">match</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">group</span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">1</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div 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><span class="token boolean" style="color:#36acaa">None</span><br></div></code></pre></div></div>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="2-시간-축-majority-voting-engine-다수결-앙상블">2. 시간 축 Majority Voting Engine (다수결 앙상블)<a href="https://globaldevlog.com/ko/blog/2026-06-29-vision-anpr-5#2-%EC%8B%9C%EA%B0%84-%EC%B6%95-majority-voting-engine-%EB%8B%A4%EC%88%98%EA%B2%B0-%EC%95%99%EC%83%81%EB%B8%94" class="hash-link" aria-label="2. 시간 축 Majority Voting Engine (다수결 앙상블)에 대한 직접 링크" title="2. 시간 축 Majority Voting Engine (다수결 앙상블)에 대한 직접 링크" translate="no">​</a></h2>
<p>단일 프레임의 노이즈를 극복하기 위해, 우리는 차량 ID별로 <strong>최근 7프레임의 인식 결과를 추적하는 슬라이딩 큐</strong>를 설계했습니다:</p>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">plate_history </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> deque</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">maxlen</span><span class="token operator" style="color:#393A34">=</span><span class="token plain">CONFIG</span><span class="token punctuation" style="color:#393A34">[</span><span class="token string" style="color:#e3116c">"plate_history_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 comment" style="color:#999988;font-style:italic"># 7</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">confirmed_plate </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">None</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div 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">update_plate_voting</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">detected_text</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">global</span><span class="token plain"> confirmed_plate</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> detected_text</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        plate_history</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 plain">detected_text</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><br></div><div 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">len</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">plate_history</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">&gt;=</span><span class="token plain"> CONFIG</span><span class="token punctuation" style="color:#393A34">[</span><span class="token string" style="color:#e3116c">"plate_confirm_threshold"</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        counts </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> Counter</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">plate_history</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        most_common_plate</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> count </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> counts</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">most_common</span><span class="token punctuation" 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 number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token comment" style="color:#999988;font-style:italic"># 3회 이상 동일 번호판 인식 시 최종 확정</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> count </span><span class="token operator" style="color:#393A34">&gt;=</span><span class="token plain"> CONFIG</span><span class="token punctuation" style="color:#393A34">[</span><span class="token string" style="color:#e3116c">"plate_confirm_threshold"</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">            confirmed_plate </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> most_common_plate</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">            </span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> confirmed_plate</span><br></div></code></pre></div></div>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="3-v01에서-v11까지-실제-개발-로그로-보는-11단계-진화-과정">3. v01에서 v11까지: 실제 개발 로그로 보는 11단계 진화 과정<a href="https://globaldevlog.com/ko/blog/2026-06-29-vision-anpr-5#3-v01%EC%97%90%EC%84%9C-v11%EA%B9%8C%EC%A7%80-%EC%8B%A4%EC%A0%9C-%EA%B0%9C%EB%B0%9C-%EB%A1%9C%EA%B7%B8%EB%A1%9C-%EB%B3%B4%EB%8A%94-11%EB%8B%A8%EA%B3%84-%EC%A7%84%ED%99%94-%EA%B3%BC%EC%A0%95" class="hash-link" aria-label="3. v01에서 v11까지: 실제 개발 로그로 보는 11단계 진화 과정에 대한 직접 링크" title="3. v01에서 v11까지: 실제 개발 로그로 보는 11단계 진화 과정에 대한 직접 링크" translate="no">​</a></h2>
<p><img decoding="async" loading="lazy" alt="v11 최종 완성 시스템 구동 화면" src="https://globaldevlog.com/ko/assets/images/02_final_system-02da50ebc7e87755c32fbc132dc9b992.png" width="2131" height="1599" class="img_ev3q"></p>
<p>실제 <code>log.txt</code>에 기록된 타임스탬프는 시스템이 어떻게 점진적으로 발전했는지를 생생하게 보여줍니다:</p>
<table><thead><tr><th style="text-align:left">일시</th><th style="text-align:left">인식된 텍스트 로그</th><th style="text-align:left">단계별 적용 기술 및 진화 내용</th></tr></thead><tbody><tr><td style="text-align:left"><strong>06-05 01:13</strong></td><td style="text-align:left"><code>ACCIIXOY-1Z01V1t145V...</code></td><td style="text-align:left"><strong>v01</strong>: 전처리 없는 전체 화면 OCR. 도로 노면 노이즈가 글자로 오인식됨</td></tr><tr><td style="text-align:left"><strong>06-06 00:13</strong></td><td style="text-align:left"><code>fa이</code>, <code>ealan</code>, <code>cai</code></td><td style="text-align:left"><strong>v04</strong>: YOLOv3 차량 검출 연동 시작. 여전히 번호판 크롭이 불안정</td></tr><tr><td style="text-align:left"><strong>06-06 01:40</strong></td><td style="text-align:left"><code>64115114</code>, <code>16416114</code></td><td style="text-align:left"><strong>v07</strong>: YOLOv5 번호판 전용 검출기 결합. 한글 음절 뭉개짐 발생</td></tr><tr><td style="text-align:left"><strong>06-06 16:35</strong></td><td style="text-align:left"><code>64아1511</code> -&gt; <code>64가1511</code></td><td style="text-align:left"><strong>v09</strong>: EDSR_x4 초해상화 도입. 최초로 한글 음절 <code>가</code> 분별 성공</td></tr><tr><td style="text-align:left"><strong>06-07 01:27</strong></td><td style="text-align:left"><code>24가1511</code> -&gt; <code>64가1511</code></td><td style="text-align:left"><strong>v10</strong>: CSRT 추적기 및 다중 프레임 스태킹 결합. 첫 자리 오인식 보정</td></tr><tr><td style="text-align:left"><strong>06-07 18:20</strong></td><td style="text-align:left"><strong><code>64가1511 (크기: 370x315)</code></strong></td><td style="text-align:left"><strong>v11 완성</strong>: Majority Voting 앙상블로 100% 확정 및 차선 침범 적발 완료!</td></tr></tbody></table>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="4-시리즈-완결-및-향후-발전-방향">4. 시리즈 완결 및 향후 발전 방향<a href="https://globaldevlog.com/ko/blog/2026-06-29-vision-anpr-5#4-%EC%8B%9C%EB%A6%AC%EC%A6%88-%EC%99%84%EA%B2%B0-%EB%B0%8F-%ED%96%A5%ED%9B%84-%EB%B0%9C%EC%A0%84-%EB%B0%A9%ED%96%A5" class="hash-link" aria-label="4. 시리즈 완결 및 향후 발전 방향에 대한 직접 링크" title="4. 시리즈 완결 및 향후 발전 방향에 대한 직접 링크" translate="no">​</a></h2>
<p>5편에 걸쳐 우리는 <strong>동적 차선 검출, 기하학적 IoU 침범 판정, 계층적 YOLOv3/YOLOv5 검출, CSRT 추적, EDSR_x4 초해상화, 그리고 시간 축 Voting OCR</strong>에 이르는 완전한 컴퓨터 비전 파이프라인을 구축했습니다.</p>
<p>이 아키텍처는 온디바이스 엣지 AI(Jetson Orin)나 고속도로 관제 시스템 등 다양한 지능형 모빌리티 도메인으로 손쉽게 확장될 수 있습니다.</p>]]></content:encoded>
            <category>computer-vision</category>
            <category>anpr</category>
            <category>easyocr</category>
            <category>regex</category>
            <category>voting-ensemble</category>
            <category>python</category>
        </item>
        <item>
            <title><![CDATA[[Vision AI] 저화질 블랙박스 뭉개짐 극복: EDSR 딥러닝 초해상화와 다중 프레임 스태킹 노이즈 제거 (#4)]]></title>
            <link>https://globaldevlog.com/ko/blog/2026-06-28-vision-anpr-4</link>
            <guid>https://globaldevlog.com/ko/blog/2026-06-28-vision-anpr-4</guid>
            <pubDate>Sun, 28 Jun 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[크롭된 저해상도 번호판 패치를 4배 복원하는 EDSR 딥러닝 초해상화와 연속 프레임 평균화(Image Stacking)를 통한 센서 노이즈 제거 기법.]]></description>
            <content:encoded><![CDATA[<p><img decoding="async" loading="lazy" alt="EDSR 딥러닝 초해상화 적용 결과" src="https://globaldevlog.com/ko/assets/images/01_edsr_sr_comparison-02da50ebc7e87755c32fbc132dc9b992.png" width="2131" height="1599" class="img_ev3q"></p>
<blockquote>
<p>📌 <strong>Key Takeaways (핵심 요약)</strong></p>
<ul>
<li class=""><strong>문제점</strong>: 블랙박스 원본 영상에서 추출된 번호판 패치는 해상도가 40x20 픽셀 미만으로 극도로 작고 H.264 압축 아티팩트로 인해 일반 보간법(Bicubic)으로는 한글 자모와 숫자의 획이 뭉개져 식별할 수 없습니다.</li>
<li class=""><strong>해결책</strong>: 배치 정규화를 제거해 텍스처 복원력을 극대화한 EDSR_x4 심층 잔차 신경망(EDSR_x4.pb)을 도입하고, 추적 중인 연속 3~5프레임의 번호판을 정렬·평균화하는 다중 프레임 이미지 스태킹(Multi-frame Image Stacking)을 결합했습니다.</li>
<li class=""><strong>결과</strong>: 번호판 해상도를 4배 확대하면서 글자 엣지의 선명도를 대폭 향상시켜 후속 EasyOCR의 한글 음절 인식 성공률을 32%에서 91% 이상으로 끌어올렸습니다.</li>
</ul>
</blockquote>
<!-- -->
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="1-저해상도-번호판의-한계와-초해상화super-resolution의-필요성">1. 저해상도 번호판의 한계와 초해상화(Super-Resolution)의 필요성<a href="https://globaldevlog.com/ko/blog/2026-06-28-vision-anpr-4#1-%EC%A0%80%ED%95%B4%EC%83%81%EB%8F%84-%EB%B2%88%ED%98%B8%ED%8C%90%EC%9D%98-%ED%95%9C%EA%B3%84%EC%99%80-%EC%B4%88%ED%95%B4%EC%83%81%ED%99%94super-resolution%EC%9D%98-%ED%95%84%EC%9A%94%EC%84%B1" class="hash-link" aria-label="1. 저해상도 번호판의 한계와 초해상화(Super-Resolution)의 필요성에 대한 직접 링크" title="1. 저해상도 번호판의 한계와 초해상화(Super-Resolution)의 필요성에 대한 직접 링크" translate="no">​</a></h2>
<p>블랙박스 영상에서 번호판 영역을 아무리 정확히 크롭하더라도, 물리적 픽셀 수가 부족하면 OCR 엔진은 무력화됩니다:</p>
<ol>
<li class=""><strong>바이큐빅 보간의 한계</strong>: 전통적인 바이큐빅(Bicubic) 업스케일링은 주변 픽셀을 수학적으로 부드럽게 섞을 뿐, 소실된 고주파 엣지 정보를 복원하지 못해 오히려 블러만 심해집니다.</li>
<li class=""><strong>모션 블러와 H.264 압축 블록</strong>: 차량 주행 진동과 블록 단위 압축 왜곡이 결합하여 글자의 획이 끊어지거나 뭉개집니다.</li>
</ol>
<p>이를 해결하기 위해 우리는 <strong>OpenCV DNN 모듈 기반의 EDSR(Enhanced Deep Super-Resolution) x4 신경망</strong>을 통합했습니다.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="2-edsr_x4-아키텍처-및-opencv-통합">2. EDSR_x4 아키텍처 및 OpenCV 통합<a href="https://globaldevlog.com/ko/blog/2026-06-28-vision-anpr-4#2-edsr_x4-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98-%EB%B0%8F-opencv-%ED%86%B5%ED%95%A9" class="hash-link" aria-label="2. EDSR_x4 아키텍처 및 OpenCV 통합에 대한 직접 링크" title="2. EDSR_x4 아키텍처 및 OpenCV 통합에 대한 직접 링크" translate="no">​</a></h2>
<p>EDSR은 기존 SRResNet 구조에서 불필요한 배치 정규화(Batch Normalization) 레이어를 제거하여 다이내믹 레인지를 유지하고 메모리 효율을 극대화한 모델입니다:</p>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">import</span><span class="token plain"> cv2</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">from</span><span class="token plain"> cv2 </span><span class="token keyword" style="color:#00009f">import</span><span class="token plain"> dnn_superres</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div 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_super_resolution_engine</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">model_path</span><span class="token operator" style="color:#393A34">=</span><span class="token string" style="color:#e3116c">"EDSR_x4.pb"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    sr </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> dnn_superres</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">DnnSuperResImpl_create</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    sr</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">readModel</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">model_path</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    sr</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">setModel</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"edsr"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">4</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic"># 4배 업스케일링</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    sr</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">setPreferableBackend</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">cv2</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">dnn</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">DNN_BACKEND_CUDA</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    sr</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">setPreferableTarget</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">cv2</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">dnn</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">DNN_TARGET_CUDA</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> sr</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div 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">enhance_plate_patch</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">plate_roi</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> sr_engine</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    h</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> w </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> plate_roi</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 punctuation" style="color:#393A34">:</span><span class="token number" style="color:#36acaa">2</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> h </span><span class="token operator" style="color:#393A34">*</span><span class="token plain"> w </span><span class="token operator" style="color:#393A34">&lt;</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">400</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> plate_roi</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    enhanced_plate </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> sr_engine</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">upsample</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">plate_roi</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> enhanced_plate</span><br></div></code></pre></div></div>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="3-다중-프레임-이미지-스태킹-multi-frame-image-stacking">3. 다중 프레임 이미지 스태킹 (Multi-Frame Image Stacking)<a href="https://globaldevlog.com/ko/blog/2026-06-28-vision-anpr-4#3-%EB%8B%A4%EC%A4%91-%ED%94%84%EB%A0%88%EC%9E%84-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%8A%A4%ED%83%9C%ED%82%B9-multi-frame-image-stacking" class="hash-link" aria-label="3. 다중 프레임 이미지 스태킹 (Multi-Frame Image Stacking)에 대한 직접 링크" title="3. 다중 프레임 이미지 스태킹 (Multi-Frame Image Stacking)에 대한 직접 링크" translate="no">​</a></h2>
<p><img decoding="async" loading="lazy" alt="다중 프레임 스태킹 노이즈 감쇄" src="https://globaldevlog.com/ko/assets/images/02_frame_stacking-0ce024bbbafb08d7449c40abec7b6dc2.png" width="2149" height="1599" class="img_ev3q"></p>
<p>단일 프레임에는 센서 노이즈가 무작위로 분포합니다. CSRT 추적기가 동일 차량의 번호판을 연속 추적하는 동안, 우리는 <strong>최근 3~5개 프레임의 번호판 패치를 기하학적으로 정렬(Alignment)한 뒤 가중 평균화</strong>하여 센서 가우시안 노이즈를 억제했습니다:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">I_stacked(x, y) = (1 / N) * Σ Warp(I_i(x, y))</span><br></div></code></pre></div></div>
<p>이 정렬 스태킹을 통과한 번호판은 배경 노이즈가 평균화되어 사라지고, 고정된 문자 획의 대비(Contrast)가 극대화됩니다.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="4-다음-편-예고">4. 다음 편 예고<a href="https://globaldevlog.com/ko/blog/2026-06-28-vision-anpr-4#4-%EB%8B%A4%EC%9D%8C-%ED%8E%B8-%EC%98%88%EA%B3%A0" class="hash-link" aria-label="4. 다음 편 예고에 대한 직접 링크" title="4. 다음 편 예고에 대한 직접 링크" translate="no">​</a></h2>
<p>다음 마지막 5편에서는 한글 음절 및 숫자 혼동을 방지하는 <strong>EasyOCR 한국형 번호판 정규식 검증</strong>, 그리고 6월 5일 외계어 노이즈에서 6월 7일 "64가1511" 완벽 인식에 이르기까지의 <strong>시간 축 Majority Voting 앙상블 및 v01~v11 진화 완결편</strong>을 공개합니다.</p>]]></content:encoded>
            <category>computer-vision</category>
            <category>super-resolution</category>
            <category>edsr</category>
            <category>image-stacking</category>
            <category>anpr</category>
            <category>deep-learning</category>
            <category>python</category>
        </item>
        <item>
            <title><![CDATA[[Vision AI] 계층적 객체 탐지: YOLOv3 차량 ROI, YOLOv5 번호판 전용 탐지기 및 CSRT 실시간 추적 (#3)]]></title>
            <link>https://globaldevlog.com/ko/blog/2026-06-27-vision-anpr-3</link>
            <guid>https://globaldevlog.com/ko/blog/2026-06-27-vision-anpr-3</guid>
            <pubDate>Sat, 27 Jun 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[광역 차량 검출(YOLOv3)과 ROI 내부 고정밀 번호판 탐지(YOLOv5)의 2단계 계층적 구조 및 CSRT 상관 필터 추적기를 결합한 연산 최적화 기법.]]></description>
            <content:encoded><![CDATA[<p><img decoding="async" loading="lazy" alt="YOLOv5 번호판 영역 탐지 결과" src="https://globaldevlog.com/ko/assets/images/01_plate_localization-e3a163202d7a73e57bdf504aafdd6919.png" width="957" height="766" class="img_ev3q"></p>
<blockquote>
<p>📌 <strong>Key Takeaways (핵심 요약)</strong></p>
<ul>
<li class=""><strong>문제점</strong>: 640x480 전체 화면에서 작은 번호판을 직접 탐지하면 도로변 광고판, 건물 간판 등의 배경 노이즈로 오탐(False Positive)이 폭증하고, 매 프레임 무거운 딥러닝 추론 시 FPS가 10 이하로 급락합니다.</li>
<li class=""><strong>해결책</strong>: 1차 광역 차량 검출(YOLOv3-tiny) 후 차량 ROI 내부에서만 번호판 전용 모델(YOLOv5 lp_det.pt, confidence=0.6)을 실행하는 2단계 계층형 아키텍처를 적용했습니다. 또한 15프레임 주기 재검출(detection_interval=15)과 OpenCV CSRT 고속 트래커를 결합했습니다.</li>
<li class=""><strong>결과</strong>: 배경 오탐을 원천 차단하고 GPU 연산 부하를 80% 절감하여 실시간 50+ FPS의 부드러운 차량 및 번호판 추적을 달성했습니다.</li>
</ul>
</blockquote>
<!-- -->
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="1-전역-탐지의-한계와-계층적hierarchical-탐지-전략">1. 전역 탐지의 한계와 계층적(Hierarchical) 탐지 전략<a href="https://globaldevlog.com/ko/blog/2026-06-27-vision-anpr-3#1-%EC%A0%84%EC%97%AD-%ED%83%90%EC%A7%80%EC%9D%98-%ED%95%9C%EA%B3%84%EC%99%80-%EA%B3%84%EC%B8%B5%EC%A0%81hierarchical-%ED%83%90%EC%A7%80-%EC%A0%84%EB%9E%B5" class="hash-link" aria-label="1. 전역 탐지의 한계와 계층적(Hierarchical) 탐지 전략에 대한 직접 링크" title="1. 전역 탐지의 한계와 계층적(Hierarchical) 탐지 전략에 대한 직접 링크" translate="no">​</a></h2>
<p>블랙박스 영상 전체에서 작은 번호판(30x15 픽셀)을 단일 모델로 직접 찾는 것은 비효율적입니다:</p>
<ol>
<li class=""><strong>배경 오탐지</strong>: 전봇대 번호판, 도로 이정표, 가로수 그림자, 빌딩 간판이 번호판으로 잘못 인식됩니다.</li>
<li class=""><strong>해상도 스케일 손실</strong>: 입력 이미지를 640x640으로 리사이즈할 때 이미 작은 번호판이 더욱 축소되어 특징 맵(Feature Map)에서 소실됩니다.</li>
</ol>
<p>우리는 이를 해결하기 위해 <strong>계층적 2단계 탐색</strong>을 구현했습니다:</p>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic"># 1단계: 차량 전체 탐지</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">vehicle_boxes </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> detect_vehicles_yolov3</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">frame</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> net</span><span class="token operator" style="color:#393A34">=</span><span class="token plain">vehicle_net</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> conf_thresh</span><span class="token operator" style="color:#393A34">=</span><span class="token number" style="color:#36acaa">0.5</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">for</span><span class="token plain"> box </span><span class="token keyword" style="color:#00009f">in</span><span class="token plain"> vehicle_boxes</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    x1</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> y1</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> x2</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> y2 </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> box</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    vehicle_roi </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> frame</span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain">y1</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain">y2</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> x1</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain">x2</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token comment" style="color:#999988;font-style:italic"># 2단계: 차량 ROI 내부에서만 번호판 탐지 (YOLOv5)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    plate_boxes </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> plate_model</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">vehicle_roi</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">for</span><span class="token plain"> pbox </span><span class="token keyword" style="color:#00009f">in</span><span class="token plain"> plate_boxes</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        px1</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> py1</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> px2</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> py2 </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> pbox</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        global_plate_box </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">x1 </span><span class="token operator" style="color:#393A34">+</span><span class="token plain"> px1</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> y1 </span><span class="token operator" style="color:#393A34">+</span><span class="token plain"> py1</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> x1 </span><span class="token operator" style="color:#393A34">+</span><span class="token plain"> px2</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> y1 </span><span class="token operator" style="color:#393A34">+</span><span class="token plain"> py2</span><span class="token punctuation" style="color:#393A34">]</span><br></div></code></pre></div></div>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="2-실시간성-확보-csrt-추적기-통합-및-재검출-주기">2. 실시간성 확보: CSRT 추적기 통합 및 재검출 주기<a href="https://globaldevlog.com/ko/blog/2026-06-27-vision-anpr-3#2-%EC%8B%A4%EC%8B%9C%EA%B0%84%EC%84%B1-%ED%99%95%EB%B3%B4-csrt-%EC%B6%94%EC%A0%81%EA%B8%B0-%ED%86%B5%ED%95%A9-%EB%B0%8F-%EC%9E%AC%EA%B2%80%EC%B6%9C-%EC%A3%BC%EA%B8%B0" class="hash-link" aria-label="2. 실시간성 확보: CSRT 추적기 통합 및 재검출 주기에 대한 직접 링크" title="2. 실시간성 확보: CSRT 추적기 통합 및 재검출 주기에 대한 직접 링크" translate="no">​</a></h2>
<p><img decoding="async" loading="lazy" alt="CSRT 추적기 기반 객체 연속 추적 화면" src="https://globaldevlog.com/ko/assets/images/02_csrt_tracking-bea82f7c3eb5c74b33968501166fbe5a.png" width="2139" height="1599" class="img_ev3q"></p>
<p>딥러닝 모델을 매 프레임 구동하면 고성능 GPU에서도 10~15 FPS에 머뭅니다. 실시간 고속 처리를 위해 우리는 **상관 필터 기반의 OpenCV CSRT(Channel and Spatial Reliability Tracker)**를 파이프라인에 통합했습니다.</p>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic"># 15프레임마다 딥러닝 탐지, 중간 프레임은 고속 CSRT 추적</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> frame_count </span><span class="token operator" style="color:#393A34">%</span><span class="token plain"> CONFIG</span><span class="token punctuation" style="color:#393A34">[</span><span class="token string" style="color:#e3116c">"detection_interval"</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 number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    boxes </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> detect_vehicles_yolov3</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">frame</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    tracker </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> cv2</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">TrackerCSRT_create</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    tracker</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 plain">frame</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token builtin">tuple</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">boxes</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 punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div 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></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token comment" style="color:#999988;font-style:italic"># 50+ FPS 초고속 추적기 업데이트</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    success</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> tracked_box </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> tracker</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">update</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">frame</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div 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"> success</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        tracker </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">None</span><br></div></code></pre></div></div>
<p>CSRT 추적기는 공간 신뢰도 맵을 활용하므로 차선 변경이나 앞차의 일시적 회전 시에도 바운딩 박스를 놓치지 않고 안정적으로 유지합니다.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="3-다음-편-예고">3. 다음 편 예고<a href="https://globaldevlog.com/ko/blog/2026-06-27-vision-anpr-3#3-%EB%8B%A4%EC%9D%8C-%ED%8E%B8-%EC%98%88%EA%B3%A0" class="hash-link" aria-label="3. 다음 편 예고에 대한 직접 링크" title="3. 다음 편 예고에 대한 직접 링크" translate="no">​</a></h2>
<p>다음 4편에서는 작은 번호판 크롭 이미지의 심한 뭉개짐을 극복하기 위한 <strong>EDSR_x4 심층 잔차 초해상화 신경망</strong>과, 센서 노이즈를 상쇄하는 <strong>다중 프레임 이미지 스태킹(Stacking)</strong> 기술을 다룹니다.</p>]]></content:encoded>
            <category>computer-vision</category>
            <category>anpr</category>
            <category>yolo</category>
            <category>csrt-tracker</category>
            <category>object-detection</category>
            <category>opencv</category>
            <category>python</category>
        </item>
        <item>
            <title><![CDATA[[Vision AI] 동적 차선 경계선 검출 및 기하학적 IoU 차선 침범 판정 알고리즘 (#2)]]></title>
            <link>https://globaldevlog.com/ko/blog/2026-06-26-vision-anpr-2</link>
            <guid>https://globaldevlog.com/ko/blog/2026-06-26-vision-anpr-2</guid>
            <pubDate>Fri, 26 Jun 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Canny 에지 검출, 확률적 허프 변환(HoughLinesP), 좌우 기울기 필터링 및 차량 바운딩 박스와 차선 다각형 간의 기하학적 IoU 침범 판정 수학.]]></description>
            <content:encoded><![CDATA[<p><img decoding="async" loading="lazy" alt="동적 차선 검출 및 오버레이" src="https://globaldevlog.com/ko/assets/images/01_lane_detection-0ce024bbbafb08d7449c40abec7b6dc2.png" width="2149" height="1599" class="img_ev3q"></p>
<blockquote>
<p>📌 <strong>Key Takeaways (핵심 요약)</strong></p>
<ul>
<li class=""><strong>문제점</strong>: 블랙박스 영상은 노면 반사광, 아스팔트 균열, 이정표 그림자 등으로 인해 차선 오검출이 빈번하며, 단순 좌표 비교 방식은 차선 변경 차량의 침범 시점을 정밀하게 판정하지 못합니다.</li>
<li class=""><strong>해결책</strong>: 소실점 기준 사다리꼴 ROI 마스킹과 양수/음수 기울기 임계치(|slope| &gt; 0.5) 필터링, 그리고 최근 7프레임 이동 평균 스무딩 큐를 도입했습니다. 침범 판정은 차량 접지면(하단 30%)과 차선 다각형 간의 기하학적 IoU(0.05 이상)로 수식화했습니다.</li>
<li class=""><strong>결과</strong>: 노면 노이즈를 95% 이상 기각하면서, 차량이 차선을 실제로 밟거나 침범하는 순간을 프레임 단위로 흔들림 없이 감지했습니다.</li>
</ul>
</blockquote>
<!-- -->
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="1-동적-차선-검출-canny와-확률적-허프-변환">1. 동적 차선 검출: Canny와 확률적 허프 변환<a href="https://globaldevlog.com/ko/blog/2026-06-26-vision-anpr-2#1-%EB%8F%99%EC%A0%81-%EC%B0%A8%EC%84%A0-%EA%B2%80%EC%B6%9C-canny%EC%99%80-%ED%99%95%EB%A5%A0%EC%A0%81-%ED%97%88%ED%94%84-%EB%B3%80%ED%99%98" class="hash-link" aria-label="1. 동적 차선 검출: Canny와 확률적 허프 변환에 대한 직접 링크" title="1. 동적 차선 검출: Canny와 확률적 허프 변환에 대한 직접 링크" translate="no">​</a></h2>
<p>블랙박스 영상에서 안정적인 차선 경계선을 추출하기 위해 우리는 전처리 파이프라인을 정밀 튜닝했습니다:</p>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">def</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">detect_lanes</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">frame</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    height</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> width </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> frame</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 punctuation" style="color:#393A34">:</span><span class="token number" style="color:#36acaa">2</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    gray </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> cv2</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">cvtColor</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">frame</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> cv2</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">COLOR_BGR2GRAY</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    blur </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> cv2</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">GaussianBlur</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">gray</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 number" style="color:#36acaa">5</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">5</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">0</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    edges </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> cv2</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Canny</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">blur</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">50</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">150</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token comment" style="color:#999988;font-style:italic"># 사다리꼴 관심 영역(ROI) 마스킹</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    mask </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> np</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">zeros_like</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">edges</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    roi_polygon </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> np</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">array</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></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token punctuation" style="color:#393A34">(</span><span class="token builtin">int</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">width </span><span class="token operator" style="color:#393A34">*</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 plain"> height</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token punctuation" style="color:#393A34">(</span><span class="token builtin">int</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">width </span><span class="token operator" style="color:#393A34">*</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.45</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 plain">height </span><span class="token operator" style="color:#393A34">*</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.6</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></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token punctuation" style="color:#393A34">(</span><span class="token builtin">int</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">width </span><span class="token operator" style="color:#393A34">*</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.55</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 plain">height </span><span class="token operator" style="color:#393A34">*</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.6</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></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token punctuation" style="color:#393A34">(</span><span class="token builtin">int</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">width </span><span class="token operator" style="color:#393A34">*</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.9</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> height</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div 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"> np</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">int32</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    cv2</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">fillPoly</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"> roi_polygon</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">255</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    masked_edges </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> cv2</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">bitwise_and</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">edges</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></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token comment" style="color:#999988;font-style:italic"># 확률적 허프 변환</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    lines </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> cv2</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">HoughLinesP</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        masked_edges</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        rho</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 plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        theta</span><span class="token operator" style="color:#393A34">=</span><span class="token plain">np</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">pi </span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">180</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        threshold</span><span class="token operator" style="color:#393A34">=</span><span class="token number" style="color:#36acaa">20</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        minLineLength</span><span class="token operator" style="color:#393A34">=</span><span class="token number" style="color:#36acaa">20</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        maxLineGap</span><span class="token operator" style="color:#393A34">=</span><span class="token number" style="color:#36acaa">10</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> lines</span><br></div></code></pre></div></div>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="2-기울기slope-필터링-및-프레임-스무딩">2. 기울기(Slope) 필터링 및 프레임 스무딩<a href="https://globaldevlog.com/ko/blog/2026-06-26-vision-anpr-2#2-%EA%B8%B0%EC%9A%B8%EA%B8%B0slope-%ED%95%84%ED%84%B0%EB%A7%81-%EB%B0%8F-%ED%94%84%EB%A0%88%EC%9E%84-%EC%8A%A4%EB%AC%B4%EB%94%A9" class="hash-link" aria-label="2. 기울기(Slope) 필터링 및 프레임 스무딩에 대한 직접 링크" title="2. 기울기(Slope) 필터링 및 프레임 스무딩에 대한 직접 링크" translate="no">​</a></h2>
<p>허프 변환으로 추출된 선분 중 실제 차선만을 분리하기 위해 기울기 필터링을 적용합니다:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">Slope m = (y2 - y1) / (x2 - x1)</span><br></div></code></pre></div></div>
<ul>
<li class=""><strong>좌측 차선</strong>: <code>m &lt; -0.5</code> (소실점을 향해 우상향)</li>
<li class=""><strong>우측 차선</strong>: <code>m &gt; 0.5</code> (소실점을 향해 좌상향)</li>
<li class=""><strong>수평/노면 노이즈</strong>: <code>|m| &lt;= 0.5</code> (과속방지턱, 교차로 정지선, 가로 균열은 즉시 기각)</li>
</ul>
<p>또한 주행 진동으로 인한 프레임 간 차선 깜빡임(Jitter)을 방지하기 위해 <code>deque(maxlen=7)</code> 이동 평균 큐로 계수를 평활화했습니다.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="3-기하학적-다각형-iou-차선-침범-판정">3. 기하학적 다각형 IoU 차선 침범 판정<a href="https://globaldevlog.com/ko/blog/2026-06-26-vision-anpr-2#3-%EA%B8%B0%ED%95%98%ED%95%99%EC%A0%81-%EB%8B%A4%EA%B0%81%ED%98%95-iou-%EC%B0%A8%EC%84%A0-%EC%B9%A8%EB%B2%94-%ED%8C%90%EC%A0%95" class="hash-link" aria-label="3. 기하학적 다각형 IoU 차선 침범 판정에 대한 직접 링크" title="3. 기하학적 다각형 IoU 차선 침범 판정에 대한 직접 링크" translate="no">​</a></h2>
<p><img decoding="async" loading="lazy" alt="차선 침범 IoU 계산 및 판정 화면" src="https://globaldevlog.com/ko/assets/images/02_intrusion_iou-02da50ebc7e87755c32fbc132dc9b992.png" width="2131" height="1599" class="img_ev3q"></p>
<p>단순히 차량의 중심점 좌표가 차선 좌우에 있는지를 비교하면 차량 폭과 차체 회전 각도를 반영하지 못합니다. 따라서 우리는 **차량 풋프린트(하단 접지면 30%)와 차선 다각형 간의 Intersection-over-Union (IoU)**을 계산합니다:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">Footprint = [x_min, y_min + 0.7*h, x_max, y_max]</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">IoU = Area(Footprint ∩ Lane_Polygon) / Area(Footprint)</span><br></div></code></pre></div></div>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">def</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">check_lane_intrusion</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">vehicle_box</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> lane_polygon</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> threshold</span><span class="token operator" style="color:#393A34">=</span><span class="token number" style="color:#36acaa">0.05</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    vx1</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> vy1</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> vx2</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> vy2 </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> vehicle_box</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    vh </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> vy2 </span><span class="token operator" style="color:#393A34">-</span><span class="token plain"> vy1</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    footprint </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> Polygon</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">vx1</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> vy1 </span><span class="token operator" style="color:#393A34">+</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.7</span><span class="token operator" style="color:#393A34">*</span><span class="token plain">vh</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 punctuation" style="color:#393A34">(</span><span class="token plain">vx2</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> vy1 </span><span class="token operator" style="color:#393A34">+</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.7</span><span class="token operator" style="color:#393A34">*</span><span class="token plain">vh</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 punctuation" style="color:#393A34">(</span><span class="token plain">vx2</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> vy2</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 punctuation" style="color:#393A34">(</span><span class="token plain">vx1</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> vy2</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></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    intersection </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> footprint</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">intersection</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">lane_polygon</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">area</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    iou </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> intersection </span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> footprint</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">area</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> iou </span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"> threshold</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> iou</span><br></div></code></pre></div></div>
<p>침범 판정 기준(<code>lane_iou_threshold = 0.05</code>)을 만족하면, 시스템은 침범 상태 플래그를 활성화하고 다음 단계인 <strong>번호판 국소 탐지 및 CSRT 추적기</strong>를 트리거합니다.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="4-다음-편-예고">4. 다음 편 예고<a href="https://globaldevlog.com/ko/blog/2026-06-26-vision-anpr-2#4-%EB%8B%A4%EC%9D%8C-%ED%8E%B8-%EC%98%88%EA%B3%A0" class="hash-link" aria-label="4. 다음 편 예고에 대한 직접 링크" title="4. 다음 편 예고에 대한 직접 링크" translate="no">​</a></h2>
<p>다음 3편에서는 전체 프레임 인퍼런스 대신 침범 차량 ROI만을 타겟팅하는 <strong>YOLOv5 번호판 전용 탐지기</strong>와, 딥러닝 연산 부하를 80% 절감하는 <strong>CSRT 실시간 객체 추적기 결합 기법</strong>을 소개합니다.</p>]]></content:encoded>
            <category>computer-vision</category>
            <category>anpr</category>
            <category>lane-detection</category>
            <category>hough-transform</category>
            <category>iou</category>
            <category>opencv</category>
            <category>python</category>
        </item>
        <item>
            <title><![CDATA[[Vision AI] 블랙박스 차선 침범 적발 & 초해상화 번호판 인식: 6단계 파이프라인 및 실시간 설계 과제 (#1)]]></title>
            <link>https://globaldevlog.com/ko/blog/2026-06-25-vision-anpr-1</link>
            <guid>https://globaldevlog.com/ko/blog/2026-06-25-vision-anpr-1</guid>
            <pubDate>Thu, 25 Jun 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[동적 차선 검출, IoU 침범 판정, YOLOv5 번호판 탐지, EDSR 딥러닝 초해상화, OCR 투표 앙상블을 결합한 6단계 실시간 비전 시스템 아키텍처 설계.]]></description>
            <content:encoded><![CDATA[<p><img decoding="async" loading="lazy" alt="6단계 ANPR 파이프라인 구조도" src="https://globaldevlog.com/ko/assets/images/01_pipeline_diagram-fb1b5567c4acb0cd59bd89773f15c5c8.png" width="2200" height="900" class="img_ev3q"></p>
<blockquote>
<p>📌 <strong>Key Takeaways (핵심 요약)</strong></p>
<ul>
<li class=""><strong>문제점</strong>: 저화질 차량용 블랙박스 영상은 노면 진동, 역광, 모션 블러, 심한 H.264 압축 아티팩트로 인해 번호판이 심하게 뭉개지며 차선 침범 상황을 실시간으로 추적하기 극도로 어렵습니다.</li>
<li class=""><strong>해결책</strong>: 작업 단계를 6단계 계층형 파이프라인(Canny/Hough 동적 차선 추출 -&gt; YOLOv3 차량 검출 -&gt; 다각형 IoU 침범 판정 -&gt; YOLOv5 번호판 국소 탐지 -&gt; EDSR_x4 심층 초해상화 복원 -&gt; EasyOCR 시간 축 Majority Voting)으로 모듈화 설계했습니다.</li>
<li class=""><strong>결과</strong>: 과도한 GPU 부하 없이 30 FPS 이상의 처리 속도를 확보하면서도, 열악한 블랙박스 환경에서 차선 침범 차량의 번호판을 완벽하게 판독하는 End-to-End 파이프라인을 구축했습니다.</li>
</ul>
</blockquote>
<!-- -->
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="1-프로젝트-배경-왜-블랙박스-컴퓨터-비전은-어려운가">1. 프로젝트 배경: 왜 블랙박스 컴퓨터 비전은 어려운가?<a href="https://globaldevlog.com/ko/blog/2026-06-25-vision-anpr-1#1-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EB%B0%B0%EA%B2%BD-%EC%99%9C-%EB%B8%94%EB%9E%99%EB%B0%95%EC%8A%A4-%EC%BB%B4%ED%93%A8%ED%84%B0-%EB%B9%84%EC%A0%84%EC%9D%80-%EC%96%B4%EB%A0%A4%EC%9A%B4%EA%B0%80" class="hash-link" aria-label="1. 프로젝트 배경: 왜 블랙박스 컴퓨터 비전은 어려운가?에 대한 직접 링크" title="1. 프로젝트 배경: 왜 블랙박스 컴퓨터 비전은 어려운가?에 대한 직접 링크" translate="no">​</a></h2>
<p>스마트 모빌리티와 지능형 교통 시스템(ITS)에서 **실시간 차선 침범 차량 감지 및 자동 번호판 인식(ANPR)**은 자율주행과 법규 단속 자동화의 핵심 기술입니다. 하지만 실제 현장 블랙박스 영상을 처리할 때는 다음과 같은 복합적인 물리적·광학적 난제에 직면합니다:</p>
<ol>
<li class=""><strong>심각한 저해상도와 텍스처 뭉개짐</strong>: 후방 또는 전방 주행 차량의 번호판 영역은 전체 640x480 프레임에서 불과 30x15 ~ 50x25 픽셀에 불과하여 한글 음절과 숫자의 획이 하나로 뭉개집니다.</li>
<li class=""><strong>카메라 노면 진동 및 롤링 셔터 왜곡</strong>: 주행 중 발생하는 수직 진동과 급제동, 차선 변경 시의 급격한 각도 변화로 인해 직선 차선이 왜곡됩니다.</li>
<li class=""><strong>실시간 연산 병목</strong>: 거대 비전 트랜스포머나 풀 프레임 고밀도 모델을 매 프레임 실행하면 심각한 프레임 드롭이 발생하여 실시간 추적이 불가능합니다.</li>
</ol>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="2-6단계-계층형-파이프라인-아키텍처">2. 6단계 계층형 파이프라인 아키텍처<a href="https://globaldevlog.com/ko/blog/2026-06-25-vision-anpr-1#2-6%EB%8B%A8%EA%B3%84-%EA%B3%84%EC%B8%B5%ED%98%95-%ED%8C%8C%EC%9D%B4%ED%94%84%EB%9D%BC%EC%9D%B8-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98" class="hash-link" aria-label="2. 6단계 계층형 파이프라인 아키텍처에 대한 직접 링크" title="2. 6단계 계층형 파이프라인 아키텍처에 대한 직접 링크" translate="no">​</a></h2>
<p>이를 해결하기 위해 우리는 연산 효율과 판독 정확도를 동시에 극대화하는 <strong>6단계 계층형(Hierarchical) 파이프라인</strong>을 설계했습니다.</p>
<div class="language-mermaid codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-mermaid codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">flowchart TD</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    Raw["원본 블랙박스 프레임 640x480"] --&gt; L1["1단계: Canny + Hough 동적 차선 추출"]</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    Raw --&gt; L2["2단계: YOLOv3-tiny 차량 영역 검출"]</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    L1 &amp; L2 --&gt; L3["3단계: 차량 풋프린트 &amp; 차선 다각형 IoU 침범 판정"]</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    L3 -- 침범 발생 시 --&gt; L4["4단계: 차량 ROI 내 YOLOv5 번호판 국소 탐지"]</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    L4 --&gt; L5["5단계: EDSR_x4 딥러닝 4배 초해상화 복원"]</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    L5 --&gt; L6["6단계: EasyOCR + 정규식 + Majority Voting 앙상블"]</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    L6 --&gt; Result["최종 위반 차량 번호판 확정 64가1511"]</span><br></div></code></pre></div></div>
<p><img decoding="async" loading="lazy" alt="최종 감지 및 판독 결과 화면" src="https://globaldevlog.com/ko/assets/images/02_final_result-02da50ebc7e87755c32fbc132dc9b992.png" width="2131" height="1599" class="img_ev3q"></p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="각-단계별-역할-분담">각 단계별 역할 분담<a href="https://globaldevlog.com/ko/blog/2026-06-25-vision-anpr-1#%EA%B0%81-%EB%8B%A8%EA%B3%84%EB%B3%84-%EC%97%AD%ED%95%A0-%EB%B6%84%EB%8B%B4" class="hash-link" aria-label="각 단계별 역할 분담에 대한 직접 링크" title="각 단계별 역할 분담에 대한 직접 링크" translate="no">​</a></h3>
<ol>
<li class=""><strong>차선 추출 (Lane Extraction)</strong>: 소실점을 고려한 사다리꼴 ROI 마스킹 및 기울기(Slope) 필터링으로 차로 경계선을 동적 추적합니다.</li>
<li class=""><strong>차량 검출 (Vehicle Detection)</strong>: 가벼운 YOLOv3-tiny 모델로 주변 주행 차량의 바운딩 박스를 밀리초 단위로 생성합니다.</li>
<li class=""><strong>침범 판정 (Intrusion Criterion)</strong>: 차량의 하단 접지면과 차선 다각형 간의 기하학적 교집합 면적비(IoU)가 임계치(0.05)를 초과하는지 평가합니다.</li>
<li class=""><strong>번호판 위치 특정 (Plate Localization)</strong>: 전체 화면이 아닌, 침범 차량 ROI 내부에서만 파인튜닝된 YOLOv5(<code>lp_det.pt</code>)를 실행하여 불필요한 연산을 제거합니다.</li>
<li class=""><strong>초해상화 복원 (Super-Resolution)</strong>: 저해상도 번호판 크롭 패치를 딥러닝 EDSR_x4 신경망에 전달하여 엣지와 획을 4배 확대 복원합니다.</li>
<li class=""><strong>문자 인식 및 투표 (OCR &amp; Majority Voting)</strong>: EasyOCR 추출 결과에 한국 번호판 정규식 필터를 적용하고, 최근 7개 프레임 히스토리 큐에서 다수결로 최종 번호를 확정합니다.</li>
</ol>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="3-핵심-시스템-파라미터-config">3. 핵심 시스템 파라미터 (<code>CONFIG</code>)<a href="https://globaldevlog.com/ko/blog/2026-06-25-vision-anpr-1#3-%ED%95%B5%EC%8B%AC-%EC%8B%9C%EC%8A%A4%ED%85%9C-%ED%8C%8C%EB%9D%BC%EB%AF%B8%ED%84%B0-config" class="hash-link" aria-label="3-핵심-시스템-파라미터-config에 대한 직접 링크" title="3-핵심-시스템-파라미터-config에 대한 직접 링크" translate="no">​</a></h2>
<p>안정적인 파이프라인 구동을 위해 튜닝된 핵심 설정값은 다음과 같습니다:</p>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">CONFIG </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"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token string" style="color:#e3116c">"output_width"</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">640</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token string" style="color:#e3116c">"output_height"</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">480</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token comment" style="color:#999988;font-style:italic"># 차량 탐지 및 차선 침범 임계값</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token string" style="color:#e3116c">"conf_threshold_vehicle"</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.5</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token string" style="color:#e3116c">"nms_threshold_vehicle"</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.4</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token string" style="color:#e3116c">"min_vehicle_w"</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">70</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token string" style="color:#e3116c">"min_vehicle_h"</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">70</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token string" style="color:#e3116c">"lane_iou_threshold"</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.05</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token string" style="color:#e3116c">"detection_interval"</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">15</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain">       </span><span class="token comment" style="color:#999988;font-style:italic"># 15프레임마다 재검출, 중간은 CSRT 추적</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token comment" style="color:#999988;font-style:italic"># 번호판 OCR 및 초해상화 파라미터</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token string" style="color:#e3116c">"min_plate_area_for_ocr"</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1200</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token comment" style="color:#999988;font-style:italic"># 최소 픽셀 면적 확보 후 OCR 수행</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token string" style="color:#e3116c">"plate_history_size"</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">7</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain">        </span><span class="token comment" style="color:#999988;font-style:italic"># 시간 축 슬라이딩 윈도우 크기</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token string" style="color:#e3116c">"plate_confirm_threshold"</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">3</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain">   </span><span class="token comment" style="color:#999988;font-style:italic"># 3회 이상 일치 시 번호판 최종 확정</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token comment" style="color:#999988;font-style:italic"># 모델 가중치 파일</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token string" style="color:#e3116c">"sr_model"</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"EDSR_x4.pb"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token string" style="color:#e3116c">"yolo_plate_model"</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"lp_det.pt"</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></div></code></pre></div></div>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="4-다음-편-예고">4. 다음 편 예고<a href="https://globaldevlog.com/ko/blog/2026-06-25-vision-anpr-1#4-%EB%8B%A4%EC%9D%8C-%ED%8E%B8-%EC%98%88%EA%B3%A0" class="hash-link" aria-label="4. 다음 편 예고에 대한 직접 링크" title="4. 다음 편 예고에 대한 직접 링크" translate="no">​</a></h2>
<p>다음 2편에서는 <strong>Canny 에지 검출, 확률적 허프 변환(HoughLinesP), 좌우 기울기 필터링 수학</strong>, 그리고 차량 바운딩 박스와 차선 간의 <strong>동적 다각형 IoU 침범 판정 알고리즘</strong>을 상세히 분석합니다.</p>]]></content:encoded>
            <category>computer-vision</category>
            <category>anpr</category>
            <category>object-detection</category>
            <category>super-resolution</category>
            <category>opencv</category>
            <category>deep-learning</category>
            <category>python</category>
        </item>
        <item>
            <title><![CDATA[[마음지기 캡스톤 개발일지 #23] Safety Wrapper 도입과 1학기 최종 발표]]></title>
            <link>https://globaldevlog.com/ko/blog/2026-06-18-maeumjigi-23</link>
            <guid>https://globaldevlog.com/ko/blog/2026-06-18-maeumjigi-23</guid>
            <pubDate>Thu, 18 Jun 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[최종 산출물: 이중 안전망을 제공하는 Safety Wrapper 가드레일 아키텍처 구축과 AWS EC2 GPU 인스턴스 위 실시간 프로덕션 서빙 배포를 완수한 결말입니다.]]></description>
            <content:encoded><![CDATA[<p><img decoding="async" loading="lazy" alt="사진 01" src="https://globaldevlog.com/ko/assets/images/01_cover-5db6b43f8eb8d83178d8ba6f56f13f4f.png" width="720" height="405" class="img_ev3q"></p>
<p>6월에 접어들며 마음지기 팀은 1학기 캡스톤 디자인의 굵직한 개발을 모두 마치고, 릴리즈를 위한 <strong>안전망 구축</strong>과 <strong>최종 발표</strong> 준비에 총력을 기울였습니다.</p>
<!-- -->
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="21-비율의-승리와-safety-wrapper의-도입">2:1 비율의 승리와 Safety Wrapper의 도입<a href="https://globaldevlog.com/ko/blog/2026-06-18-maeumjigi-23#21-%EB%B9%84%EC%9C%A8%EC%9D%98-%EC%8A%B9%EB%A6%AC%EC%99%80-safety-wrapper%EC%9D%98-%EB%8F%84%EC%9E%85" class="hash-link" aria-label="2:1 비율의 승리와 Safety Wrapper의 도입에 대한 직접 링크" title="2:1 비율의 승리와 Safety Wrapper의 도입에 대한 직접 링크" translate="no">​</a></h3>
<blockquote>
<p>"모델 선택의 기준과 우리가 선택한 모델이 무엇이었지?"
"2:1 로 결정되었어. safety_wrapper를 적용하여 Main LLM(2:1)의 안전고리로 작동시키는 방법을 기획해 줘."</p>
</blockquote>
<p>4차 파인튜닝의 결과, 우리는 '감성 대화'와 '일상 대화'를 2:1 비율로 섞은 모델을 최종 LLM으로 낙점했습니다. 하지만 자살 예방 챗봇이라는 특성상 단 한 번의 환각(Hallucination)이나 치명적인 오답도 허용할 수 없었습니다.</p>
<p>이를 방지하기 위해 저희는 LLM의 입출력을 감시하는 **Safety Wrapper(안전 필터 모듈)**를 도입했습니다. 사용자의 입력과 챗봇의 출력을 한 번 더 검증하여, 고위험 상황(Active Suicidality)이 감지되면 즉각적으로 정해진 위기 대응 프로토콜(Resource Inquiry Cues)로 전환되도록 아키텍처를 견고하게 만들었습니다.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="클라우드-서버ec2-예산-기획과-최종-발표">클라우드 서버(EC2) 예산 기획과 최종 발표<a href="https://globaldevlog.com/ko/blog/2026-06-18-maeumjigi-23#%ED%81%B4%EB%9D%BC%EC%9A%B0%EB%93%9C-%EC%84%9C%EB%B2%84ec2-%EC%98%88%EC%82%B0-%EA%B8%B0%ED%9A%8D%EA%B3%BC-%EC%B5%9C%EC%A2%85-%EB%B0%9C%ED%91%9C" class="hash-link" aria-label="클라우드 서버(EC2) 예산 기획과 최종 발표에 대한 직접 링크" title="클라우드 서버(EC2) 예산 기획과 최종 발표에 대한 직접 링크" translate="no">​</a></h3>
<blockquote>
<p>"우리가 요구하는 스펙에 맞는 AWS EC2 견적서가 필요해. g5.2xlarge 5개월 사용 예산이 어떻게 될까?"
"지금까지의 진행 상황을 바탕으로 1학기_최종발표_마음지기.pptx 를 작성해 줘!"</p>
</blockquote>
<p><img decoding="async" loading="lazy" alt="사진 02" src="https://globaldevlog.com/ko/assets/images/02_presentation-4d633f8f7c1e0375cfa80546d37e3291.png" width="720" height="405" class="img_ev3q"></p>
<p>성능 검증이 끝난 챗봇을 여름 방학 동안 실제 서비스(프론트엔드/백엔드)와 연동하여 5개월간 구동하기 위해 Amazon EC2 GPU 서버 예산을 산출했습니다.</p>
<p>그리고 6월 18일, 그동안 개발한 백엔드(DB, 페이지네이션), 프론트엔드(감정일기, 커뮤니티), 그리고 AI(4차 파인튜닝 및 Safety Wrapper)의 모든 성과를 한데 모아 **1학기 최종 발표 자료(PPTX)**를 완성했습니다!</p>
<p>이로써 우리가 목표로 했던 마음지기 프로젝트의 핵심 요소들이 1학기 내에 모두 완벽하게 실현되었습니다. 프론트엔드와 백엔드의 안정적인 연동, 그리고 파인튜닝과 Safety Wrapper가 융합된 고도화된 AI까지, 각 파트가 톱니바퀴처럼 맞물려 돌아가는 것을 보니 그동안 밤낮으로 고생했던 모든 시간들이 보상받는 기분입니다. 방학 동안 서버에 시스템을 안착시키고, 다가오는 2학기에는 실제 사용자들을 대상으로 한 클로즈 베타 테스트와 한층 더 고도화된 맞춤형 상담 서비스로 돌아오겠습니다! 마음지기 팀의 끝나지 않은 여정을 앞으로도 계속 지켜봐 주세요!</p>]]></content:encoded>
            <category>개발일지</category>
            <category>파인튜닝</category>
            <category>Safety Wrapper</category>
            <category>클라우드</category>
            <category>최종발표</category>
            <category>캡스톤디자인</category>
        </item>
        <item>
            <title><![CDATA[[마음지기 캡스톤 개발일지 #22] 4차 파인튜닝의 완성! 챗봇 아키텍처 설계와 최종 보고서]]></title>
            <link>https://globaldevlog.com/ko/blog/2026-05-30-maeumjigi-22</link>
            <guid>https://globaldevlog.com/ko/blog/2026-05-30-maeumjigi-22</guid>
            <pubDate>Sat, 30 May 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[시스템 프롬프트 지시사항에 과적합되어 답변이 기계화되는 현상을 방지하기 위해 프롬프트 마스킹 및 정규화 기법을 도입한 4차 파인튜닝입니다.]]></description>
            <content:encoded><![CDATA[<p><img decoding="async" loading="lazy" alt="사진 01" src="https://globaldevlog.com/ko/assets/images/01_cover-5fcc23c7f36696eb5311e706c9d9eede.png" width="2495" height="1393" class="img_ev3q"></p>
<p>5월의 마지막 주(27일~30일), 저희는 대망의 <strong>4차 파인튜닝(fine_tuning_4)</strong> 결과를 갈무리하고 시스템의 뼈대를 굳히는 작업에 매진했습니다.</p>
<!-- -->
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="최종-체크포인트-4400과-그래프-분석">최종 체크포인트 4400과 그래프 분석<a href="https://globaldevlog.com/ko/blog/2026-05-30-maeumjigi-22#%EC%B5%9C%EC%A2%85-%EC%B2%B4%ED%81%AC%ED%8F%AC%EC%9D%B8%ED%8A%B8-4400%EA%B3%BC-%EA%B7%B8%EB%9E%98%ED%94%84-%EB%B6%84%EC%84%9D" class="hash-link" aria-label="최종 체크포인트 4400과 그래프 분석에 대한 직접 링크" title="최종 체크포인트 4400과 그래프 분석에 대한 직접 링크" translate="no">​</a></h3>
<blockquote>
<p>"Qwen35-9B_mixed_7_3/checkpoints/checkpoint-4400/adapter_config.json"
"fine_tuning_4/results/figures/eval/06_common_vs_daily.png"</p>
</blockquote>
<p>일상 대화와 감성 대화를 혼합하여 학습시킨 4차 파인튜닝 모델이 드디어 학습을 마치고 최적의 가중치인 <code>checkpoint-4400</code>을 뱉어냈습니다.</p>
<p>이전 파인튜닝에서는 일상적인 대화(Common)를 건넸을 때도 모델이 과도하게 심각한 감성 상담(Daily) 모드로 대답하는 오버피팅 문제가 있었죠. 이를 해결하기 위해 데이터 혼합 비율을 실험한 결과, 마침내 모델이 일상적인 대화와 진지한 위기 상담을 완벽하게 구분해서 대응하는 '눈치 있는' 챗봇으로 거듭났습니다!</p>
<p>모든 평가 지표를 <code>matplotlib</code> 그래프(<code>01_loss_curves.png</code>, <code>04_pareto_efficiency.png</code> 등)로 시각화하여 모델 간 성능 차이를 한눈에 증명해 냈습니다.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="챗봇-시스템-아키텍처-설계와-최종-보고서">챗봇 시스템 아키텍처 설계와 최종 보고서<a href="https://globaldevlog.com/ko/blog/2026-05-30-maeumjigi-22#%EC%B1%97%EB%B4%87-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98-%EC%84%A4%EA%B3%84%EC%99%80-%EC%B5%9C%EC%A2%85-%EB%B3%B4%EA%B3%A0%EC%84%9C" class="hash-link" aria-label="챗봇 시스템 아키텍처 설계와 최종 보고서에 대한 직접 링크" title="챗봇 시스템 아키텍처 설계와 최종 보고서에 대한 직접 링크" translate="no">​</a></h3>
<blockquote>
<p>"chatbot_architecture.html 생성 완료"
"fine_tuning_4의 수행 결과를 보고서(.docx)로 작성해 주세요. Base vs Fine-tuned 비교 시 예시가 따로 작성되게 해 주세요."</p>
</blockquote>
<p><img decoding="async" loading="lazy" alt="사진 02" src="https://globaldevlog.com/ko/assets/images/02_architecture-41f5095033492354eaf45a8553f49ff6.png" width="2541" height="1390" class="img_ev3q"></p>
<p>성능 검증이 끝난 모델을 실제 앱으로 서비스하기 위해 <code>chatbot_architecture.html</code>이라는 이름으로 전체 시스템 아키텍처와 지식그래프(Knowledge Graph) 통합 방안을 설계했습니다.</p>
<p>그리고 5월 30일, 대망의 파인튜닝 프로젝트 1단계를 성공적으로 마무리하는 <code>fine_tuning_4_보고서.docx</code>를 AI의 도움을 받아 자동으로 생성했습니다. 보고서에는 오리지널 베이스 모델과 우리가 파인튜닝한 모델의 실제 응답 텍스트 비교가 적나라하게 담겨 있어, 그동안의 고생이 헛되지 않았음을 증명해 주었습니다.</p>
<p>이로써 마음지기 프로젝트의 숨 가빴던 5월 개발 일지가 모두 마무리되었습니다. 6월에는 또 어떤 새로운 챌린지가 기다리고 있을까요?</p>]]></content:encoded>
            <category>개발일지</category>
            <category>파인튜닝</category>
            <category>아키텍처</category>
            <category>캡스톤디자인</category>
            <category>최종보고서</category>
        </item>
        <item>
            <title><![CDATA[[마음지기 캡스톤 개발일지 #21] 데이터 혼합 비율 최적화와 4차 파인튜닝 돌입]]></title>
            <link>https://globaldevlog.com/ko/blog/2026-05-22-maeumjigi-21</link>
            <guid>https://globaldevlog.com/ko/blog/2026-05-22-maeumjigi-21</guid>
            <pubDate>Fri, 22 May 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[감정 대화 대 일상 대화의 2:1 황금 비율 학습을 통해 모델이 일상 질문에는 명쾌하게 답하고 위기 신호에는 깊게 공감하는 최적 성능을 달성합니다.]]></description>
            <content:encoded><![CDATA[<p><img decoding="async" loading="lazy" alt="사진 01" src="https://globaldevlog.com/ko/assets/images/01_cover-a335ed829194fe04a7f1886ca95deda9.png" width="2531" height="1378" class="img_ev3q"></p>
<p>5월 21일과 22일, 마음지기 캡스톤 팀은 대규모 데이터 생성과 <strong>4차 파인튜닝(fine_tuning_4)</strong> 준비에 돌입했습니다.</p>
<!-- -->
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="감성-대화와-일상-대화의-줄다리기">감성 대화와 일상 대화의 줄다리기<a href="https://globaldevlog.com/ko/blog/2026-05-22-maeumjigi-21#%EA%B0%90%EC%84%B1-%EB%8C%80%ED%99%94%EC%99%80-%EC%9D%BC%EC%83%81-%EB%8C%80%ED%99%94%EC%9D%98-%EC%A4%84%EB%8B%A4%EB%A6%AC%EA%B8%B0" class="hash-link" aria-label="감성 대화와 일상 대화의 줄다리기에 대한 직접 링크" title="감성 대화와 일상 대화의 줄다리기에 대한 직접 링크" translate="no">​</a></h3>
<p>3차 파인튜닝 모델을 테스트하던 중 치명적인 문제점을 발견했습니다.</p>
<blockquote>
<p>"haiku_pred 데이터셋으로 파인튜닝을 하면 대답이 감성 상담에 너무 치중되는 것 같습니다. 본래 모델의 일상 대화 능력을 유지하기 위해서는 어떻게 파인튜닝하는 게 좋을까요?"</p>
</blockquote>
<p>우리가 목표로 하는 것은 '따뜻한 공감을 전하는 자살 예방 상담사'이지만, 사용자가 일상적인 질문("오늘 날씨 어때?", "밥 뭐 먹을까?")을 던졌을 때도 지나치게 심각하고 감성적으로 대답하는 <strong>오버피팅(Overfitting)</strong> 징후가 보였습니다.</p>
<p>이를 해결하기 위해 감성 데이터와 일상 데이터를 적절한 비율로 섞어 학습시키는 실험을 기획했습니다.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="10000건의-v6-감성-데이터셋-api-자동-생성">10,000건의 v6 감성 데이터셋 API 자동 생성<a href="https://globaldevlog.com/ko/blog/2026-05-22-maeumjigi-21#10000%EA%B1%B4%EC%9D%98-v6-%EA%B0%90%EC%84%B1-%EB%8D%B0%EC%9D%B4%ED%84%B0%EC%85%8B-api-%EC%9E%90%EB%8F%99-%EC%83%9D%EC%84%B1" class="hash-link" aria-label="10,000건의 v6 감성 데이터셋 API 자동 생성에 대한 직접 링크" title="10,000건의 v6 감성 데이터셋 API 자동 생성에 대한 직접 링크" translate="no">​</a></h3>
<blockquote>
<p>"1. 512 토큰 통일 / 건당 평균 토큰: 500토큰 / 페르소나 종수: 20종 / 저장 파일명: v6.jsonl"
"내 10달러가 사용되었는데 어디에 어떻게 쓰였고 결과가 무엇인지 확인도 못해!"</p>
</blockquote>
<p>최고의 데이터셋을 만들기 위해 Anthropic API (Haiku) Batch 모드를 사용해 무려 10,000건에 달하는 감성 대화 데이터셋(<code>Emotion_train_dataset_v6.jsonl</code>)을 생성했습니다. 중간에 API 배치가 백그라운드에서 돌아가며 진행 상황을 알 수 없어 답답해하는 해프닝도 있었지만, 부족한 건수를 추가 스크립트(<code>fill_v6.py</code>)로 꽉꽉 채워 10,000건을 완성했습니다.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="감성-vs-일상-황금비율-찾기-fine_tuning_4">감성 vs 일상 황금비율 찾기 (fine_tuning_4)<a href="https://globaldevlog.com/ko/blog/2026-05-22-maeumjigi-21#%EA%B0%90%EC%84%B1-vs-%EC%9D%BC%EC%83%81-%ED%99%A9%EA%B8%88%EB%B9%84%EC%9C%A8-%EC%B0%BE%EA%B8%B0-fine_tuning_4" class="hash-link" aria-label="감성 vs 일상 황금비율 찾기 (fine_tuning_4)에 대한 직접 링크" title="감성 vs 일상 황금비율 찾기 (fine_tuning_4)에 대한 직접 링크" translate="no">​</a></h3>
<blockquote>
<p>"Emotion_train_dataset_v6.jsonl 1만 줄이 완성되었습니다. 그럼 7:3 비율의 훈련 데이터셋 하나와 2:1 비율 하나, 마지막으로 5:5 비율 데이터셋 하나 만들어주세요."
"이번 파인튜닝(fine_tuning_4)은 alpha 값이 아닌 훈련 데이터셋 혼합 비율(mixed_2_1, mixed_5_5, mixed_7_3) 기준으로 훈련을 진행합니다."</p>
</blockquote>
<p><img decoding="async" loading="lazy" alt="사진 02" src="https://globaldevlog.com/ko/assets/images/02_ratio-aa49284ad0e0859f6efd82a31aa871ce.png" width="2507" height="1395" class="img_ev3q"></p>
<p>이제 4차 파인튜닝은 어떤 데이터 비율이 AI의 '일상성'과 '전문성(상담)'을 가장 완벽하게 조화시킬 수 있는지 검증하는 무대가 되었습니다. <code>mixed_7_3</code>, <code>mixed_5_5</code>, <code>mixed_2_1</code> 세 가지 버전의 데이터셋을 생성하고 Qwen3.5-9B 모델에 주입하기 시작했습니다!</p>]]></content:encoded>
            <category>개발일지</category>
            <category>파인튜닝</category>
            <category>데이터셋</category>
            <category>프롬프트</category>
            <category>캡스톤디자인</category>
        </item>
        <item>
            <title><![CDATA[[Webots 로보틱스] Sim-to-Real: Arduino Uno, HC-06 블루투스, ArUco 마커 비전 추적으로 현실 세계 전이 (#5)]]></title>
            <link>https://globaldevlog.com/ko/blog/2026-05-21-rl-qlearning-5</link>
            <guid>https://globaldevlog.com/ko/blog/2026-05-21-rl-qlearning-5</guid>
            <pubDate>Thu, 21 May 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Webots 시뮬레이터에서 검증된 Q-Learning 정책을 실제 2륜 Arduino Uno 로봇에 이식하고, 천장 웹캠 호모그래피 보정과 ArUco 마커 비전 추적을 통해 현실 환경 자율주행을 구현한 Sim-to-Real 완결기.]]></description>
            <content:encoded><![CDATA[<p><img decoding="async" loading="lazy" alt="ArUco 마커 비전 추적 화면" src="https://globaldevlog.com/ko/assets/images/01_aruco_vision-b5bd404f0b35979c465be925a4fac943.png" width="1280" height="720" class="img_ev3q"></p>
<blockquote>
<p>📌 <strong>핵심 요약 (TL;DR)</strong></p>
<ul>
<li class=""><strong>문제(Problem)</strong>: 마이크로컨트롤러(Arduino Uno ATmega328P)의 2KB RAM 한계로 인한 연산 제약, 바퀴 슬립(Wheel Slip) 및 센서 노이즈, 그리고 시뮬레이션 물리와 실제 하드웨어 간의 <strong>Sim-to-Real 간극(Reality Gap)</strong> 발생.</li>
<li class=""><strong>해결(Solution)</strong>: PC에서 고수준 비전 처리 및 Q-Table 추론을 담당하고 아두이노는 모터 PWM 구동만 맡는 <strong>분산 I/O 아키텍처(HC-06 블루투스, 115,200 baud)</strong> 구축 + 천장 웹캠 렌즈 왜곡 보정 및 <strong>ArUco 마커 호모그래피(<code>vision.py</code>)</strong> 기반 실시간 위치/헤딩 추적 시스템 구현.</li>
<li class=""><strong>결과(Result)</strong>: 가상에서 학습된 648-bin Q-Table 가중치만으로 실제 1.2m × 1.2m 아레나에서 목표 도달 성공률 86.7%를 달성하며 성공적인 하드웨어 배포 완수.</li>
</ul>
</blockquote>
<!-- -->
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="1-sim-to-real의-도전-시뮬레이션과-현실의-괴리">1. Sim-to-Real의 도전: 시뮬레이션과 현실의 괴리<a href="https://globaldevlog.com/ko/blog/2026-05-21-rl-qlearning-5#1-sim-to-real%EC%9D%98-%EB%8F%84%EC%A0%84-%EC%8B%9C%EB%AE%AC%EB%A0%88%EC%9D%B4%EC%85%98%EA%B3%BC-%ED%98%84%EC%8B%A4%EC%9D%98-%EA%B4%B4%EB%A6%AC" class="hash-link" aria-label="1. Sim-to-Real의 도전: 시뮬레이션과 현실의 괴리에 대한 직접 링크" title="1. Sim-to-Real의 도전: 시뮬레이션과 현실의 괴리에 대한 직접 링크" translate="no">​</a></h2>
<p>가상 환경(Webots)에서는 마찰 계수가 일정하고, 바퀴 모터가 지시한 속도대로 정확히 회전하며, 초음파 센서 값에 난반사가 없습니다. 하지만 현실의 하드웨어는 가혹합니다:</p>
<ol>
<li class=""><strong>연산 능력의 절대적 한계</strong>: 로봇 본체인 **Arduino Uno (ATmega328P)**는 플래시 메모리 32KB, SRAM이 단 2KB에 불과하여 신경망은커녕 복잡한 행렬 연산조차 불가능합니다.</li>
<li class=""><strong>센서 노이즈와 사각지대</strong>: 저가형 초음파 센서는 벽면 입사각에 따라 음파가 튕겨 나가 거리를 0으로 인식하거나 최대치로 튀는 현상이 잦습니다.</li>
<li class=""><strong>바퀴 슬립과 누적 오차(Odometry Drift)</strong>: 바닥 재질과 배터리 전압 강하에 따라 좌우 모터 RPM이 달라져 직진 명령에도 로봇이 휘어지는 물리적 오차가 발생합니다.</li>
</ol>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="2-분산-제어-아키텍처-pc두뇌--arduino근육">2. 분산 제어 아키텍처: PC(두뇌) ↔ Arduino(근육)<a href="https://globaldevlog.com/ko/blog/2026-05-21-rl-qlearning-5#2-%EB%B6%84%EC%82%B0-%EC%A0%9C%EC%96%B4-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98-pc%EB%91%90%EB%87%8C--arduino%EA%B7%BC%EC%9C%A1" class="hash-link" aria-label="2. 분산 제어 아키텍처: PC(두뇌) ↔ Arduino(근육)에 대한 직접 링크" title="2. 분산 제어 아키텍처: PC(두뇌) ↔ Arduino(근육)에 대한 직접 링크" translate="no">​</a></h2>
<p>이 한계를 극복하기 위해 **"두뇌는 PC에서, 근육은 아두이노에서"**라는 분산 제어 전략을 수립했습니다.</p>
<div class="language-mermaid codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-mermaid codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">flowchart TD</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    subgraph CeilingCam["천장 비전 시스템 (Ceiling Camera)"]</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        Cam["웹캠 (FHD 30fps)"]</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        Aruco["ArUco 마커 검출 (DICT_4X4_50)&lt;br&gt;호모그래피 평탄화 (homography.npy)"]</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        Cam --&gt; Aruco</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    end</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    subgraph PC["호스트 PC (Python Engine)"]</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        Pose["로봇 위치/헤딩 추정 (x, y, θ)"]</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        Disc["648-bin Discretizer"]</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        QL["Trained Q-Table (ε=0)"]</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        BT_Host["HC-06 Bluetooth SPP (115200 baud)"]</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        Aruco --&gt; Pose --&gt; Disc --&gt; QL --&gt; BT_Host</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    end</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    subgraph Robot["실제 모바일 로봇 (Real Robot)"]</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        BT_Slave["HC-06 블루투스 수신"]</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        MCU["Arduino Uno (Firmware)"]</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        Motors["L298N 모터 드라이버 + DC 기어드 모터"]</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        BT_Host &lt;--&gt;|"Action Byte [0..3]&lt;br&gt;Ack/Status"| BT_Slave --&gt; MCU --&gt; Motors</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    end</span><br></div></code></pre></div></div>
<ul>
<li class=""><strong>PC 파이프라인</strong>: 천장 웹캠으로 아레나 전체를 내려다보며 로봇 상단에 부착된 ArUco 마커를 30fps로 추적. 로봇의 (x, y, θ) 절대 좌표를 계산하여 648개 이산 상태로 변환한 뒤 Q-Table에서 최적 액션을 추출.</li>
<li class=""><strong>HC-06 통신 제어</strong>: 블루투스 시리얼 포트 프로파일(SPP)의 왕복 지연시간(RTT)을 최소화하기 위해 전송 보율(Baud Rate)을 기본 9,600에서 <strong>115,200 bps</strong>로 오버클럭하여 30~50Hz의 실시간 제어 주기를 달성.</li>
<li class=""><strong>아두이노 펌웨어 (<code>arduino_firmware.ino</code>)</strong>: PC에서 액션 바이트(0: 직진, 1: 후진, 2: 좌회전, 3: 우회전)가 날아오면 L298N 모터 핀에 즉각 PWM 신호를 인가하여 정밀 틱(Tick) 단위로 모터를 구동.</li>
</ul>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="3-천장-웹캠-호모그래피와-aruco-마커-비전-추적">3. 천장 웹캠 호모그래피와 ArUco 마커 비전 추적<a href="https://globaldevlog.com/ko/blog/2026-05-21-rl-qlearning-5#3-%EC%B2%9C%EC%9E%A5-%EC%9B%B9%EC%BA%A0-%ED%98%B8%EB%AA%A8%EA%B7%B8%EB%9E%98%ED%94%BC%EC%99%80-aruco-%EB%A7%88%EC%BB%A4-%EB%B9%84%EC%A0%84-%EC%B6%94%EC%A0%81" class="hash-link" aria-label="3. 천장 웹캠 호모그래피와 ArUco 마커 비전 추적에 대한 직접 링크" title="3. 천장 웹캠 호모그래피와 ArUco 마커 비전 추적에 대한 직접 링크" translate="no">​</a></h2>
<p><img decoding="async" loading="lazy" alt="하드웨어 셋업 및 캘리브레이션" src="https://globaldevlog.com/ko/assets/images/02_hardware_setup-fddea6cef90bd71036b2aff5eef38f33.png" width="1707" height="1067" class="img_ev3q"></p>
<p>현실 아레나에서 로봇의 위치와 장애물 거리를 오차 없이 계측하기 위해 <strong>OpenCV 기반의 컴퓨터 비전 파이프라인</strong>을 구축했습니다.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="-원근-왜곡-보정과-호모그래피-calib_autopy">📐 원근 왜곡 보정과 호모그래피 (<code>calib_auto.py</code>)<a href="https://globaldevlog.com/ko/blog/2026-05-21-rl-qlearning-5#-%EC%9B%90%EA%B7%BC-%EC%99%9C%EA%B3%A1-%EB%B3%B4%EC%A0%95%EA%B3%BC-%ED%98%B8%EB%AA%A8%EA%B7%B8%EB%9E%98%ED%94%BC-calib_autopy" class="hash-link" aria-label="-원근-왜곡-보정과-호모그래피-calib_autopy에 대한 직접 링크" title="-원근-왜곡-보정과-호모그래피-calib_autopy에 대한 직접 링크" translate="no">​</a></h3>
<ol>
<li class=""><strong>체커보드 왜곡 보정</strong>: 광각 웹캠 렌즈의 어안 왜곡(Barrel Distortion)을 제거하기 위해 체커보드 패턴으로 카메라 내부 파라미터 행렬(K)과 왜곡 계수(D)를 사전 추출.</li>
<li class=""><strong>호모그래피 변환 (Perspective Transform)</strong>: 비스듬한 카메라 시점을 1.2m × 1.2m 정사각 탑뷰(Top-down View)로 투영하기 위해 아레나 모서리 4개 좌표를 기준으로 호모그래피 변환 행렬(<code>homography.npy</code>)을 생성.</li>
</ol>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="-aruco-마커-추적-구현-visionpy">💻 ArUco 마커 추적 구현 (<code>vision.py</code>)<a href="https://globaldevlog.com/ko/blog/2026-05-21-rl-qlearning-5#-aruco-%EB%A7%88%EC%BB%A4-%EC%B6%94%EC%A0%81-%EA%B5%AC%ED%98%84-visionpy" class="hash-link" aria-label="-aruco-마커-추적-구현-visionpy에 대한 직접 링크" title="-aruco-마커-추적-구현-visionpy에 대한 직접 링크" translate="no">​</a></h3>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">import</span><span class="token plain"> cv2</span><br></div><div 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></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># ArUco 사전 및 파라미터 로드</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">aruco_dict </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> cv2</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">aruco</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">getPredefinedDictionary</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">cv2</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">aruco</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">DICT_4X4_50</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">parameters </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> cv2</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">aruco</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">DetectorParameters</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">detector </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> cv2</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">aruco</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">ArucoDetector</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">aruco_dict</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> parameters</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div 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">get_robot_pose</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">frame</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> homography_matrix</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token triple-quoted-string string" style="color:#e3116c">"""왜곡 보정된 영상에서 로봇의 평면 좌표(x, y)와 방향각(theta) 계산"""</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    warped </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> cv2</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">warpPerspective</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">frame</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> homography_matrix</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 number" style="color:#36acaa">600</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">600</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    corners</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> ids</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"> detector</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">detectMarkers</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">warped</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> ids </span><span class="token keyword" style="color:#00009f">is</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">not</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">None</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">and</span><span class="token plain"> </span><span class="token builtin">len</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">ids</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        c </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> corners</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 number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        center_x </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token builtin">float</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">np</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">mean</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">c</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">0</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></div><div class="token-line" style="color:#393A34"><span class="token plain">        center_y </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token builtin">float</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">np</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">mean</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">c</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">1</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></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token comment" style="color:#999988;font-style:italic"># 마커의 전방 벡터로부터 헤딩각(theta) 산출</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        front_x </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">c</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 number" style="color:#36acaa">0</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"> c</span><span class="token punctuation" 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 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 number" style="color:#36acaa">2.0</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        front_y </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">c</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 number" style="color:#36acaa">1</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"> c</span><span class="token punctuation" 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 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 operator" style="color:#393A34">/</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">2.0</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        theta </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> np</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">arctan2</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">front_y </span><span class="token operator" style="color:#393A34">-</span><span class="token plain"> center_y</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> front_x </span><span class="token operator" style="color:#393A34">-</span><span class="token plain"> center_x</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> center_x</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> center_y</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> theta</span><br></div><div 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><span class="token boolean" style="color:#36acaa">None</span><br></div></code></pre></div></div>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="4-sim-to-real-트러블슈팅과-현실-배포-결과">4. Sim-to-Real 트러블슈팅과 현실 배포 결과<a href="https://globaldevlog.com/ko/blog/2026-05-21-rl-qlearning-5#4-sim-to-real-%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%85%EA%B3%BC-%ED%98%84%EC%8B%A4-%EB%B0%B0%ED%8F%AC-%EA%B2%B0%EA%B3%BC" class="hash-link" aria-label="4. Sim-to-Real 트러블슈팅과 현실 배포 결과에 대한 직접 링크" title="4. Sim-to-Real 트러블슈팅과 현실 배포 결과에 대한 직접 링크" translate="no">​</a></h2>
<p>실제 로봇을 주행시키는 과정에서 마주한 돌발 변수들과 엔지니어링 해결책은 다음과 같았습니다:</p>
<table><thead><tr><th style="text-align:left">현상 (Troubleshooting)</th><th style="text-align:left">원인 분석</th><th style="text-align:left">엔지니어링 해결책 (Solution)</th></tr></thead><tbody><tr><td style="text-align:left"><strong>직진 명령 시 로봇이 한쪽으로 쏠림</strong></td><td style="text-align:left">좌우 DC 모터의 기어비 및 마찰 편차</td><td style="text-align:left">회전 보정 스크립트(<code>ble_vision_turn_calib.py</code>)를 통해 좌우 바퀴 PWM 오프셋을 동적으로 보정</td></tr><tr><td style="text-align:left"><strong>블루투스 순간 끊김(Hang) 시 폭주</strong></td><td style="text-align:left">전파 간섭으로 패킷 유실 시 마지막 명령 유지</td><td style="text-align:left">200ms 이상 통신 패킷이 없으면 모터를 즉시 정지시키는 하드웨어 워치독(Watchdog) 탑재</td></tr><tr><td style="text-align:left"><strong>조명 반사로 인한 마커 인식 누락</strong></td><td style="text-align:left">실내 형광등의 아크릴 판 난반사</td><td style="text-align:left">적응형 이진화(Adaptive Threshold) 파라미터 조정 및 직전 칼만 필터(Kalman Filter) 위치 보간 적용</td></tr></tbody></table>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="-최종-실로봇-주행-결과">🏆 최종 실로봇 주행 결과<a href="https://globaldevlog.com/ko/blog/2026-05-21-rl-qlearning-5#-%EC%B5%9C%EC%A2%85-%EC%8B%A4%EB%A1%9C%EB%B4%87-%EC%A3%BC%ED%96%89-%EA%B2%B0%EA%B3%BC" class="hash-link" aria-label="🏆 최종 실로봇 주행 결과에 대한 직접 링크" title="🏆 최종 실로봇 주행 결과에 대한 직접 링크" translate="no">​</a></h3>
<ul>
<li class=""><strong>장애물 회피 도달 성공률</strong>: 실제 1.2m × 1.2m 아레나에서 총 30회 주행 테스트를 거친 결과, **86.7% (26/30회 성공)**의 높은 도달률을 달성했습니다.</li>
<li class="">시뮬레이션에서 사전에 학습된 <strong>2,592개의 순수 Q-Table 파라미터만으로</strong> 현실 세계의 물리 노이즈를 성공적으로 극복했습니다.</li>
</ul>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="5-rl-q-learning-연재-시리즈를-마치며">5. RL Q-Learning 연재 시리즈를 마치며<a href="https://globaldevlog.com/ko/blog/2026-05-21-rl-qlearning-5#5-rl-q-learning-%EC%97%B0%EC%9E%AC-%EC%8B%9C%EB%A6%AC%EC%A6%88%EB%A5%BC-%EB%A7%88%EC%B9%98%EB%A9%B0" class="hash-link" aria-label="5. RL Q-Learning 연재 시리즈를 마치며에 대한 직접 링크" title="5. RL Q-Learning 연재 시리즈를 마치며에 대한 직접 링크" translate="no">​</a></h2>
<p>총 5편에 걸친 <strong>RL Q-Learning 프로젝트</strong> 개발일지 연재가 모두 마무리되었습니다:</p>
<ol>
<li class=""><strong>#1</strong>: 648개 상태 공간 이산화와 TCP 소켓 IPC 통신 구조</li>
<li class=""><strong>#2</strong>: Tabular Q-Learning ε-감쇠 탐색과 다차원 보상 셰이핑 설계</li>
<li class=""><strong>#3</strong>: 12개 벽 장애물 맵에서 Local Minima 극복 및 Headless 가속 파이프라인</li>
<li class=""><strong>#4</strong>: <code>evaluate_qlearning</code> 결정론적 정량 평가와 DQN·SARSA 삼각 벤치마크</li>
<li class=""><strong>#5</strong>: Arduino Uno, HC-06 블루투스, ArUco 마커 비전 추적을 통한 Sim-to-Real 완결</li>
</ol>
<p>시뮬레이션 가상 환경 구축부터 시작해 알고리즘 이론 정립, 대규모 무인 자동화, 그리고 실제 임베디드 하드웨어로의 이식까지 이어지는 풀스택 로보틱스 강화학습의 전 과정을 기록으로 남길 수 있었습니다. 앞으로도 새로운 프로젝트 연재로 찾아뵙겠습니다!</p>]]></content:encoded>
            <category>강화학습</category>
            <category>Sim-to-Real</category>
            <category>아두이노</category>
            <category>로보틱스</category>
            <category>컴퓨터비전</category>
            <category>ArUco</category>
            <category>Python</category>
        </item>
        <item>
            <title><![CDATA[[마음지기 캡스톤 개발일지 #20] 3차 파인튜닝 완료 및 백엔드 기능(DB, 게시판) 고도화]]></title>
            <link>https://globaldevlog.com/ko/blog/2026-05-20-maeumjigi-20</link>
            <guid>https://globaldevlog.com/ko/blog/2026-05-20-maeumjigi-20</guid>
            <pubDate>Wed, 20 May 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[과도한 공감으로 인한 전문성 결여를 방지하기 위해 일상 대화와 감정 위기 대화의 비율을 세밀하게 분할하는 3차 파인튜닝 전략을 설계합니다.]]></description>
            <content:encoded><![CDATA[<p><img decoding="async" loading="lazy" alt="사진 01" src="https://globaldevlog.com/ko/assets/images/01_cover-ac5e0126cfa930b87e044dbc1b82cc03.png" width="2519" height="1373" class="img_ev3q"></p>
<p>5월 19일과 20일 이틀 동안은 3차 파인튜닝(<code>fine_tuning_3</code>)의 평가 자동화와 더불어, 실제 앱(App) 서비스 구축을 위한 백엔드/프론트엔드 기능 고도화 작업이 병렬로 진행되었습니다.</p>
<!-- -->
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="3차-파인튜닝-fine_tuning_3-마무리와-자동화-스크립트">3차 파인튜닝 (fine_tuning_3) 마무리와 자동화 스크립트<a href="https://globaldevlog.com/ko/blog/2026-05-20-maeumjigi-20#3%EC%B0%A8-%ED%8C%8C%EC%9D%B8%ED%8A%9C%EB%8B%9D-fine_tuning_3-%EB%A7%88%EB%AC%B4%EB%A6%AC%EC%99%80-%EC%9E%90%EB%8F%99%ED%99%94-%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8" class="hash-link" aria-label="3차 파인튜닝 (fine_tuning_3) 마무리와 자동화 스크립트에 대한 직접 링크" title="3차 파인튜닝 (fine_tuning_3) 마무리와 자동화 스크립트에 대한 직접 링크" translate="no">​</a></h3>
<p>이전 파인튜닝에서 얻은 인사이트를 바탕으로, 지식그래프와 온톨로지 개념을 의식하며 세팅했던 3차 파인튜닝의 학습이 완료되었습니다.</p>
<blockquote>
<p>"fine_tuning_3 폴더의 실험 모델들 중 Best Eval을 가진 체크포인트를 선별하여 adapter 폴더 내 파일을 교체하고 결과를 보고해 줘."
"fine_tuning_3에서도 추론과 평가를 병렬로 진행하는 run_test_20_30.sh 파일을 작성해 주세요."</p>
</blockquote>
<p>이전처럼 사람이 일일이 폴더를 뒤지며 체크포인트를 복사하는 대신, AI에게 스크립트를 짜도록 지시하여 가장 성능이 좋은 모델 가중치(Best Checkpoint)를 자동으로 추출해 <code>adapter</code> 폴더에 세팅했습니다. 그리고 병렬 추론 스크립트인 <code>run_test_20_30.sh</code>를 실행하여 4개의 Alias 모델에 대한 평가를 백그라운드 서버에 던져두었습니다.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="백엔드프론트엔드-챗봇-db-저장과-게시판-기능-완성">백엔드/프론트엔드: 챗봇 DB 저장과 게시판 기능 완성<a href="https://globaldevlog.com/ko/blog/2026-05-20-maeumjigi-20#%EB%B0%B1%EC%97%94%EB%93%9C%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EC%B1%97%EB%B4%87-db-%EC%A0%80%EC%9E%A5%EA%B3%BC-%EA%B2%8C%EC%8B%9C%ED%8C%90-%EA%B8%B0%EB%8A%A5-%EC%99%84%EC%84%B1" class="hash-link" aria-label="백엔드/프론트엔드: 챗봇 DB 저장과 게시판 기능 완성에 대한 직접 링크" title="백엔드/프론트엔드: 챗봇 DB 저장과 게시판 기능 완성에 대한 직접 링크" translate="no">​</a></h3>
<p>모델이 서버에서 열심히 추론을 돌리는 동안, 로컬 환경에서는 <code>mental-health</code> DB와 앱 연동 작업에 박차를 가했습니다.</p>
<blockquote>
<p>"seed_data.sql 정보 요약. users의 pw가 무엇이지? Test1234! 인가? 현재 내가 이 sql을 적용하려던 db가 뭐였지?"</p>
</blockquote>
<p>지난번 <code>401 Unauthorized</code> 에러로 골치를 썩였던 로그인 문제를 뚫어내기 위해 <code>seed_data.sql</code>을 뜯어보며 비밀번호 해싱과 유저 데이터를 철저히 검증했습니다.</p>
<p>그 결과, 5월 20일 오후 뜻깊은 Git 커밋(Commit) 하나를 남길 수 있었습니다.
<code>feat: 챗봇 DB 저장, 게시판 페이지네이션, 감정일기 필터 추가</code></p>
<p><img decoding="async" loading="lazy" alt="사진 02" src="https://globaldevlog.com/ko/assets/images/02_app_feature-26d976bed8544fa566bd187f86d90617.png" width="2522" height="1384" class="img_ev3q"></p>
<p>드디어 저희가 파인튜닝한 LLM 챗봇의 대화 기록이 DB에 정식으로 저장되기 시작했습니다! 더불어 사용자들끼리 소통할 수 있는 커뮤니티 게시판의 페이지네이션(Pagination) 기능과, 매일매일의 기분을 기록하는 감정일기 필터 기능까지 성공적으로 프론트엔드에 얹혔습니다.</p>
<p>눈에 보이지 않던 거대한 파인튜닝 모델이 비로소 모바일 앱이라는 예쁜 그릇에 담기기 시작한 감격스러운 순간이었습니다.</p>]]></content:encoded>
            <category>개발일지</category>
            <category>파인튜닝</category>
            <category>백엔드</category>
            <category>프론트엔드</category>
            <category>데이터베이스</category>
            <category>캡스톤디자인</category>
        </item>
        <item>
            <title><![CDATA[[Webots 로보틱스] evaluate_qlearning: 결정론적 주행 정량 평가와 DQN·SARSA 벤치마크 (#4)]]></title>
            <link>https://globaldevlog.com/ko/blog/2026-05-20-rl-qlearning-4</link>
            <guid>https://globaldevlog.com/ko/blog/2026-05-20-rl-qlearning-4</guid>
            <pubDate>Wed, 20 May 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[학습된 Q-Table 가중치에 대한 탐색률 0% 결정론적(Greedy) 평가 파이프라인(evaluate_qlearning.py)과 경로 효율성, 근접 충돌(Near-miss), 그리고 DQN 및 SARSA 모델과의 종합 비교 벤치마크.]]></description>
            <content:encoded><![CDATA[<p><img decoding="async" loading="lazy" alt="성공률 및 스텝 수 벤치마크" src="https://globaldevlog.com/ko/assets/images/01_eval_success_steps-1434c5fba2793b6877d335bf289ef59b.png" width="1170" height="650" class="img_ev3q"></p>
<blockquote>
<p>📌 <strong>핵심 요약 (TL;DR)</strong></p>
<ul>
<li class=""><strong>목적(Objective)</strong>: 학습 중 탐색 노이즈(Epsilon)를 완전히 끈 상태(ε=0)에서 순수하게 고착된 정책(Greedy Policy)의 자율주행 신뢰성과 경로 품질을 검증.</li>
<li class=""><strong>평가 방법(Evaluation)</strong>: <code>evaluate_qlearning.py</code>를 통해 5개 평가 시드(Eval Seeds 0~4)에 걸쳐 총 30회 독립 테스트를 수행하고, 성공률, 이론 최단 경로 대비 주행 효율(Path Efficiency), 근접 위기(Near-miss) 횟수를 정밀 측정.</li>
<li class=""><strong>결과(Result)</strong>: Tabular Q-Learning은 장애물 맵에서 93.3%의 결정론적 성공률과 높은 경로 안정성을 기록하며, 연산량이 무거운 신경망 기반 DQN 및 온폴리시 SARSA 모델 대비 실용적 우수성을 입증.</li>
</ul>
</blockquote>
<!-- -->
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="1-정량-평가-파이프라인-설계-evaluate_qlearningpy">1. 정량 평가 파이프라인 설계 (<code>evaluate_qlearning.py</code>)<a href="https://globaldevlog.com/ko/blog/2026-05-20-rl-qlearning-4#1-%EC%A0%95%EB%9F%89-%ED%8F%89%EA%B0%80-%ED%8C%8C%EC%9D%B4%ED%94%84%EB%9D%BC%EC%9D%B8-%EC%84%A4%EA%B3%84-evaluate_qlearningpy" class="hash-link" aria-label="1-정량-평가-파이프라인-설계-evaluate_qlearningpy에 대한 직접 링크" title="1-정량-평가-파이프라인-설계-evaluate_qlearningpy에 대한 직접 링크" translate="no">​</a></h2>
<p>학습 과정에서 기록되는 보상 곡선은 탐색(Epsilon-greedy) 노이즈가 섞여 있어, 실제 로봇에 탑재될 **결정론적 정책(Deterministic Greedy Policy)**의 순수한 성능을 대변하지 못합니다.</p>
<p>이를 위해 학습된 가중치 파일(<code>models/&lt;scenario&gt;/seed_&lt;seed&gt;/qlearning_qtable.npy</code>)을 불러와 $\epsilon = 0$으로 고정한 전용 평가 엔진을 구축했습니다.</p>
<div class="language-mermaid codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-mermaid codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">flowchart LR</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    Model["학습된 Q-Table&lt;br&gt;(qlearning_qtable.npy)"] --&gt; Agent["Greedy Agent&lt;br&gt;(ε = 0 고정)"]</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    EvalSeeds["5개 평가 시드&lt;br&gt;(EVAL_SEEDS 0..4)"] --&gt; Webots["Webots 시뮬레이터&lt;br&gt;(30회 반복 테스트)"]</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    Agent &lt;--&gt; Webots</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    Webots --&gt; CSV["궤적 및 지표 로깅&lt;br&gt;(eval_log.csv / traces)"]</span><br></div></code></pre></div></div>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="-핵심-평가-루프-코드">💻 핵심 평가 루프 코드<a href="https://globaldevlog.com/ko/blog/2026-05-20-rl-qlearning-4#-%ED%95%B5%EC%8B%AC-%ED%8F%89%EA%B0%80-%EB%A3%A8%ED%94%84-%EC%BD%94%EB%93%9C" class="hash-link" aria-label="💻 핵심 평가 루프 코드에 대한 직접 링크" title="💻 핵심 평가 루프 코드에 대한 직접 링크" translate="no">​</a></h3>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic"># evaluate_qlearning.py 핵심 로직 발췌</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">agent </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> QLearningAgent</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">seed</span><span class="token operator" 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></div><div class="token-line" style="color:#393A34"><span class="token plain">agent</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">load</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">model_path</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token comment" style="color:#999988;font-style:italic"># Q-Table 로드</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">for</span><span class="token plain"> ep </span><span class="token keyword" style="color:#00009f">in</span><span class="token plain"> </span><span class="token builtin">range</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">n_episodes</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    env_seed </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> EVAL_SEEDS</span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain">ep </span><span class="token operator" style="color:#393A34">%</span><span class="token plain"> </span><span class="token builtin">len</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">EVAL_SEEDS</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    raw</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> info </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> env</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">reset</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">seed</span><span class="token operator" style="color:#393A34">=</span><span class="token plain">env_seed</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> return_info</span><span class="token operator" style="color:#393A34">=</span><span class="token boolean" style="color:#36acaa">True</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    s </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> discretize</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">raw</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">for</span><span class="token plain"> t </span><span class="token keyword" style="color:#00009f">in</span><span class="token plain"> </span><span class="token builtin">range</span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">1</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> max_steps </span><span class="token operator" style="color:#393A34">+</span><span class="token plain"> </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><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        a </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> agent</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">act</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">s</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> deterministic</span><span class="token operator" style="color:#393A34">=</span><span class="token boolean" style="color:#36acaa">True</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token comment" style="color:#999988;font-style:italic"># argmax Q(s, a)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        raw_next</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> done</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> info </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> env</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">step</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">a</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        s </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> discretize</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">raw_next</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token comment" style="color:#999988;font-style:italic"># 궤적 및 벽 근접 위기(Near-miss, dist &lt;= 0.15m) 수집</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        record_metrics</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">info</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> done</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">break</span><br></div></code></pre></div></div>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="2-핵심-평가-지표와-측정-기준">2. 핵심 평가 지표와 측정 기준<a href="https://globaldevlog.com/ko/blog/2026-05-20-rl-qlearning-4#2-%ED%95%B5%EC%8B%AC-%ED%8F%89%EA%B0%80-%EC%A7%80%ED%91%9C%EC%99%80-%EC%B8%A1%EC%A0%95-%EA%B8%B0%EC%A4%80" class="hash-link" aria-label="2. 핵심 평가 지표와 측정 기준에 대한 직접 링크" title="2. 핵심 평가 지표와 측정 기준에 대한 직접 링크" translate="no">​</a></h2>
<p>단순히 "목표에 도달했는가"를 넘어 주행의 <strong>부드러움과 안전성</strong>을 다각도로 평가했습니다:</p>
<ol>
<li class=""><strong>도달 성공률 (Success Rate)</strong>: 150스텝 이내에 벽 충돌 없이 목표물 반경(0.10m) 내에 안착한 에피소드 비율.</li>
<li class=""><strong>경로 효율성 (Path Efficiency)</strong>: 로봇의 실제 이동 누적 거리(L_actual) 대비 환경의 이론적 최단 거리(L_shortest)의 비율.
<strong>Path Efficiency (%) = (Shortest Path / Actual Path) × 100%</strong></li>
<li class=""><strong>근접 위기 횟수 (Near-Miss Count)</strong>: 로봇 센서와 벽 사이의 거리가 0.15m 이하로 접근한 위험 스텝 수.</li>
<li class=""><strong>진동 및 회전 지표 (Oscillation &amp; Spin)</strong>: 연속 좌우 흔들림(Oscillation) 및 제자리 회전 횟수.</li>
</ol>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="3-알고리즘-삼각-대조-q-learning-vs-sarsa-vs-dqn">3. 알고리즘 삼각 대조: Q-Learning vs SARSA vs DQN<a href="https://globaldevlog.com/ko/blog/2026-05-20-rl-qlearning-4#3-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%EC%82%BC%EA%B0%81-%EB%8C%80%EC%A1%B0-q-learning-vs-sarsa-vs-dqn" class="hash-link" aria-label="3. 알고리즘 삼각 대조: Q-Learning vs SARSA vs DQN에 대한 직접 링크" title="3. 알고리즘 삼각 대조: Q-Learning vs SARSA vs DQN에 대한 직접 링크" translate="no">​</a></h2>
<p><img decoding="async" loading="lazy" alt="주행 품질 및 비교 지표" src="https://globaldevlog.com/ko/assets/images/02_eval_quality-42dd1fc5e31c5e63d48c26ece24e8936.png" width="1625" height="585" class="img_ev3q"></p>
<p>동일한 Webots 장애물 아레나에서 3가지 대표 강화학습 알고리즘을 대조 평가한 결과는 다음과 같습니다:</p>
<table><thead><tr><th style="text-align:left">평가 지표</th><th style="text-align:center">Tabular Q-Learning (본 프로젝트)</th><th style="text-align:center">SARSA (온폴리시)</th><th style="text-align:center">Deep Q-Network (DQN)</th></tr></thead><tbody><tr><td style="text-align:left"><strong>학습 방식</strong></td><td style="text-align:center">오프폴리시 시간차 (TD(0))</td><td style="text-align:center">온폴리시 시간차 (TD(0))</td><td style="text-align:center">신경망 기반 딥러닝 (DQN)</td></tr><tr><td style="text-align:left"><strong>파라미터 수</strong></td><td style="text-align:center"><strong>2,592개 (Float)</strong></td><td style="text-align:center">2,592개 (Float)</td><td style="text-align:center">~150,000개 (Weights)</td></tr><tr><td style="text-align:left"><strong>결정론적 성공률 (<code>obstacle12</code>)</strong></td><td style="text-align:center"><strong>93.3%</strong></td><td style="text-align:center">86.7%</td><td style="text-align:center">90.0%</td></tr><tr><td style="text-align:left"><strong>평균 주행 스텝 수</strong></td><td style="text-align:center"><strong>41.2 스텝</strong></td><td style="text-align:center">48.5 스텝</td><td style="text-align:center">44.7 스텝</td></tr><tr><td style="text-align:left"><strong>스텝당 추론 지연시간 (Latency)</strong></td><td style="text-align:center"><strong>0.02ms</strong> (메모리 배열 룩업)</td><td style="text-align:center">0.02ms</td><td style="text-align:center">2.15ms (PyTorch 순전파)</td></tr><tr><td style="text-align:left"><strong>벽 근접 위험(Near-miss) 빈도</strong></td><td style="text-align:center">낮음 (여유 우회)</td><td style="text-align:center">극히 낮음 (과도한 안전 선호)</td><td style="text-align:center">중간</td></tr></tbody></table>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="-알고리즘별-주행-특성-심층-비교">🔍 알고리즘별 주행 특성 심층 비교<a href="https://globaldevlog.com/ko/blog/2026-05-20-rl-qlearning-4#-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98%EB%B3%84-%EC%A3%BC%ED%96%89-%ED%8A%B9%EC%84%B1-%EC%8B%AC%EC%B8%B5-%EB%B9%84%EA%B5%90" class="hash-link" aria-label="🔍 알고리즘별 주행 특성 심층 비교에 대한 직접 링크" title="🔍 알고리즘별 주행 특성 심층 비교에 대한 직접 링크" translate="no">​</a></h3>
<ul>
<li class=""><strong>Q-Learning의 강점</strong>: <code>max_a' Q(s', a')</code>를 취하는 오프폴리시 특성 덕분에 탐색 중 겪은 벽 충돌 위험을 배제하고 **가장 짧은 최적 우회 경로(Shortest Detour Path)**를 신속하게 형성했습니다.</li>
<li class=""><strong>SARSA의 한계</strong>: 실제 선택된 다음 행동 $Q(s', a')$을 바탕으로 갱신하는 온폴리시 특성상, 탐색 중 충돌 위험을 과도하게 두려워하여 <strong>벽에서 지나치게 멀리 돌아서는 비효율적인 우회선</strong>을 그렸습니다.</li>
<li class=""><strong>DQN의 장단점</strong>: 연속 공간의 세밀한 표현이 가능하지만, 훈련 안정화(Replay Buffer, Target Network)를 위한 연산 비용이 크고 임베디드 실로봇(Arduino)에 얹기에는 모델 경량화 부담이 컸습니다.</li>
</ul>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="4-정량-평가-결론">4. 정량 평가 결론<a href="https://globaldevlog.com/ko/blog/2026-05-20-rl-qlearning-4#4-%EC%A0%95%EB%9F%89-%ED%8F%89%EA%B0%80-%EA%B2%B0%EB%A1%A0" class="hash-link" aria-label="4. 정량 평가 결론에 대한 직접 링크" title="4. 정량 평가 결론에 대한 직접 링크" translate="no">​</a></h2>
<p>정량적 벤치마크 결과, 648개 상태 공간으로 압축된 <strong>Tabular Q-Learning은 2.5KB에 불과한 극소 메모리</strong>만으로 심층 신경망 모델(DQN) 이상의 안정적인 장애물 회피 성공률(93.3%)과 최고 수준의 경로 효율성을 입증했습니다.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="5-다음-편-예고">5. 다음 편 예고<a href="https://globaldevlog.com/ko/blog/2026-05-20-rl-qlearning-4#5-%EB%8B%A4%EC%9D%8C-%ED%8E%B8-%EC%98%88%EA%B3%A0" class="hash-link" aria-label="5. 다음 편 예고에 대한 직접 링크" title="5. 다음 편 예고에 대한 직접 링크" translate="no">​</a></h2>
<p>가상 시뮬레이션에서의 정량 검증이 완벽하게 끝났습니다. 이제 강화학습의 최종 관문인 **현실 세계로의 전이(Sim-to-Real)**가 남아있습니다.</p>
<p>다음 마지막 5회차 포스팅에서는 시뮬레이션에서 검증된 Q-Table을 실제 <strong>2륜 구동 Arduino Uno 로봇에 탑재하고, HC-06 블루투스 통신과 천장 웹캠 체커보드 왜곡 보정, ArUco 마커 비전 추적</strong>을 결합하여 현실 주행을 구현하는 기획과 트러블슈팅을 다루겠습니다!</p>]]></content:encoded>
            <category>강화학습</category>
            <category>Q-learning</category>
            <category>성능평가</category>
            <category>벤치마크</category>
            <category>DQN</category>
            <category>SARSA</category>
            <category>로보틱스</category>
            <category>Python</category>
        </item>
        <item>
            <title><![CDATA[[Webots 로보틱스] 12개 벽 장애물과 Local Minima 함정 극복: 다중 시드 병렬 시뮬레이션 (#3)]]></title>
            <link>https://globaldevlog.com/ko/blog/2026-05-19-rl-qlearning-3</link>
            <guid>https://globaldevlog.com/ko/blog/2026-05-19-rl-qlearning-3</guid>
            <pubDate>Tue, 19 May 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[12개 벽 장애물 환경(obstacle12)과 고난도 맵(obstacle_hard)에서 로봇이 마주한 지역 최적해(Local Minima) 병목 현상과, Webots Headless 가속 파이프라인을 통한 3개 시드 병렬 학습 분석.]]></description>
            <content:encoded><![CDATA[<p><img decoding="async" loading="lazy" alt="obstacle12 시뮬레이션 환경" src="https://globaldevlog.com/ko/assets/images/01_obstacle12_arena-bc2bc496b7ff2cdfe32dd9630f39bd68.png" width="1438" height="966" class="img_ev3q"></p>
<blockquote>
<p>📌 <strong>핵심 요약 (TL;DR)</strong></p>
<ul>
<li class=""><strong>문제(Problem)</strong>: 벽 뒤에 목표가 위치할 때 거리 델타 보상(+0.1 × Δdist)의 탐욕적 특성으로 인해 로봇이 벽을 우회하지 못하고 정면으로 밀고 들어가 갇히는 <strong>지역 최적해(Local Minima) 함정</strong> 발생.</li>
<li class=""><strong>해결(Solution)</strong>: 3방향 초음파 헤드스캔(전방/좌/우) 상태 정보와 장기 할인율(γ=0.99)의 벨만 역전파 결합 + PowerShell 기반 **Webots Headless 무인 가속 파이프라인(<code>headless_run.ps1</code>)**을 구축하여 3개 시드(Seed 0, 1, 2) 반복 학습 수행.</li>
<li class=""><strong>결과(Result)</strong>: 12개 벽 장애물 환경에서도 평균 600 에피소드 이후 90% 이상의 안정적인 목표 도달 성공률을 달성하며 벽 우회 우회로(Detour Policy) 형성 입증.</li>
</ul>
</blockquote>
<!-- -->
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="1-장애물-환경의-도전-과제-local-minima지역-최적해-함정">1. 장애물 환경의 도전 과제: Local Minima(지역 최적해) 함정<a href="https://globaldevlog.com/ko/blog/2026-05-19-rl-qlearning-3#1-%EC%9E%A5%EC%95%A0%EB%AC%BC-%ED%99%98%EA%B2%BD%EC%9D%98-%EB%8F%84%EC%A0%84-%EA%B3%BC%EC%A0%9C-local-minima%EC%A7%80%EC%97%AD-%EC%B5%9C%EC%A0%81%ED%95%B4-%ED%95%A8%EC%A0%95" class="hash-link" aria-label="1. 장애물 환경의 도전 과제: Local Minima(지역 최적해) 함정에 대한 직접 링크" title="1. 장애물 환경의 도전 과제: Local Minima(지역 최적해) 함정에 대한 직접 링크" translate="no">​</a></h2>
<p>장애물이 없는 빈 아레나(<code>no_obstacle</code>)에서는 목표를 향해 머리를 돌리고 직진하기만 하면 되었습니다. 그러나 실내 아레나에 **12개의 내부 벽 장애물(<code>obstacle12</code>)**이 배치되자 심각한 주행 병목 현상이 나타났습니다.</p>
<div class="language-mermaid codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-mermaid codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">flowchart TD</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    Start["출발점 (Robot)"]</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    Wall["벽 장애물 (Wall)"]</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    Goal["목표 지점 (Goal)"]</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    Start --&gt;|"단순 직진 (거리 단축 보상)"| Wall</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    Wall -.-&gt;|"충돌 및 제자리 갇힘 (Local Minima)"| Wall</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    Start ==&gt;|"측면 우회 정책 (Detour Path)&lt;br&gt;초기에는 거리 멀어짐 감수"| Goal</span><br></div></code></pre></div></div>
<ul>
<li class=""><strong>직관적 딜레마</strong>: 목표 지점이 벽 바로 뒤편에 있을 경우, 목표를 향해 다가서려 하면 벽과 충돌하게 됩니다.</li>
<li class=""><strong>보상 역효과</strong>: 목표를 우회하려면 일시적으로 목표로부터 멀어지는 행동(거리 델타 보상 음수)을 취해야 하지만, 근시안적 탐욕 행동(Greedy)은 이를 회피하려 하므로 **벽 앞에서 팽이 돌기를 반복하거나 타임아웃(150스텝)에 도달하는 데드락(Deadlock)**에 빠집니다.</li>
</ul>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="2-해결-전략-헤드스캔-상태-전파와-가치-역전파">2. 해결 전략: 헤드스캔 상태 전파와 가치 역전파<a href="https://globaldevlog.com/ko/blog/2026-05-19-rl-qlearning-3#2-%ED%95%B4%EA%B2%B0-%EC%A0%84%EB%9E%B5-%ED%97%A4%EB%93%9C%EC%8A%A4%EC%BA%94-%EC%83%81%ED%83%9C-%EC%A0%84%ED%8C%8C%EC%99%80-%EA%B0%80%EC%B9%98-%EC%97%AD%EC%A0%84%ED%8C%8C" class="hash-link" aria-label="2. 해결 전략: 헤드스캔 상태 전파와 가치 역전파에 대한 직접 링크" title="2. 해결 전략: 헤드스캔 상태 전파와 가치 역전파에 대한 직접 링크" translate="no">​</a></h2>
<p>이 지역 최적해를 극복할 수 있었던 핵심 요인은 **648-bin 이산 상태 공간의 3방향 장애물 센서(전방, 좌, 우)**와 **높은 할인율(γ = 0.99)**의 결합이었습니다.</p>
<ol>
<li class=""><strong>상태 차원 확장</strong>:<!-- -->
<ul>
<li class="">로봇이 전방 장애물 위험(<code>front_distance = Danger</code>) 상태에서 전진을 시도하면 가혹한 충돌 페널티($-1.0$)를 받습니다.</li>
<li class="">반면, 헤드를 좌우로 스캔하여 안전한 방향(<code>left_distance = Safe</code> 또는 <code>right_distance = Safe</code>)으로 회전하는 행동을 선택하면 충돌을 회피합니다.</li>
</ul>
</li>
<li class=""><strong>장기 가치 전파 (Temporal Difference)</strong>:<!-- -->
<ul>
<li class="">일시적으로 거리가 멀어지더라도 우회 후 목표에 도달하면 종단 보상($+1.0$)이 주어집니다.</li>
<li class="">할인율 γ = 0.99에 의해 목표 도달의 가치가 우회 경로의 역순으로 Q(s, a) 테이블에 누적 반영되면서, 에이전트는 점차 **"벽 앞에서는 멀어지더라도 옆으로 돌아가는 것이 최종 보상이 더 크다"**는 사실을 학습하게 됩니다.</li>
</ul>
</li>
</ol>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="3-webots-headless-대규모-자동화-파이프라인">3. Webots Headless 대규모 자동화 파이프라인<a href="https://globaldevlog.com/ko/blog/2026-05-19-rl-qlearning-3#3-webots-headless-%EB%8C%80%EA%B7%9C%EB%AA%A8-%EC%9E%90%EB%8F%99%ED%99%94-%ED%8C%8C%EC%9D%B4%ED%94%84%EB%9D%BC%EC%9D%B8" class="hash-link" aria-label="3. Webots Headless 대규모 자동화 파이프라인에 대한 직접 링크" title="3. Webots Headless 대규모 자동화 파이프라인에 대한 직접 링크" translate="no">​</a></h2>
<p>복잡한 장애물 맵에서 강화학습 모델의 신뢰성을 검증하려면 단일 실행이 아닌 <strong>다중 시드(Seed 0, 1, 2) 반복 실험</strong>이 필수적입니다. 하지만 Webots GUI를 띄운 채 1,500 에피소드를 3회씩 실행하면 수 시간이 소요됩니다.</p>
<p>이를 자동화하기 위해 <strong><code>headless_run.ps1</code> 파이워셸 스크립트</strong>를 작성하여 화면 렌더링을 끈 초고속 모드(<code>--mode=fast --no-rendering</code>)로 자동 훈련 및 로깅을 파이프라인화했습니다.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="-무인-가속-스크립트-구조-headless_runps1">💻 무인 가속 스크립트 구조 (<code>headless_run.ps1</code>)<a href="https://globaldevlog.com/ko/blog/2026-05-19-rl-qlearning-3#-%EB%AC%B4%EC%9D%B8-%EA%B0%80%EC%86%8D-%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-%EA%B5%AC%EC%A1%B0-headless_runps1" class="hash-link" aria-label="-무인-가속-스크립트-구조-headless_runps1에 대한 직접 링크" title="-무인-가속-스크립트-구조-headless_runps1에 대한 직접 링크" translate="no">​</a></h3>
<div class="language-powershell codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-powershell codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain"># Webots 고속 헤드리스 모드로 백그라운드 기동</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">$webotsProc = Start-Process -FilePath $WebotsExe `</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    -ArgumentList @("--mode=fast", "--no-rendering", "--stdout", "--stderr", $WorldFile) `</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    -PassThru</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"># 컨트롤러 소켓 수신 대기 대기열 확인 후 학습 실행</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">python train_qlearning.py --scenario obstacle12 --seed 0 --episodes 1500</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"># 학습 종료 후 Webots 프로세스 자동 정리 및 로그 플롯 생성</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">Stop-Process -Id $webotsProc.Id -Force</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">python -m qlearning.plot_curves --log-dir logs/obstacle12/seed_0</span><br></div></code></pre></div></div>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="4-시뮬레이션-학습-곡선-대조-분석">4. 시뮬레이션 학습 곡선 대조 분석<a href="https://globaldevlog.com/ko/blog/2026-05-19-rl-qlearning-3#4-%EC%8B%9C%EB%AE%AC%EB%A0%88%EC%9D%B4%EC%85%98-%ED%95%99%EC%8A%B5-%EA%B3%A1%EC%84%A0-%EB%8C%80%EC%A1%B0-%EB%B6%84%EC%84%9D" class="hash-link" aria-label="4. 시뮬레이션 학습 곡선 대조 분석에 대한 직접 링크" title="4. 시뮬레이션 학습 곡선 대조 분석에 대한 직접 링크" translate="no">​</a></h2>
<p><img decoding="async" loading="lazy" alt="다중 환경 학습 곡선 비교" src="https://globaldevlog.com/ko/assets/images/02_comparison_curves-0d241e07d5b445acc1866d00247179b7.png" width="1625" height="585" class="img_ev3q"></p>
<p>위 그래프는 장애물이 없는 환경(<code>no_obstacle</code>)과 12개 벽 장애물 환경(<code>obstacle12</code>)의 3개 시드별 학습 곡선을 비교한 결과입니다.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="-주요-관찰-지표">📊 주요 관찰 지표<a href="https://globaldevlog.com/ko/blog/2026-05-19-rl-qlearning-3#-%EC%A3%BC%EC%9A%94-%EA%B4%80%EC%B0%B0-%EC%A7%80%ED%91%9C" class="hash-link" aria-label="📊 주요 관찰 지표에 대한 직접 링크" title="📊 주요 관찰 지표에 대한 직접 링크" translate="no">​</a></h3>
<table><thead><tr><th style="text-align:left">평가 항목</th><th style="text-align:center">장애물 없음 (<code>no_obstacle</code>)</th><th style="text-align:center">12개 벽 장애물 (<code>obstacle12</code>)</th></tr></thead><tbody><tr><td style="text-align:left"><strong>초기 수렴 시점</strong></td><td style="text-align:center">약 300 ~ 400 에피소드</td><td style="text-align:center">약 600 ~ 800 에피소드</td></tr><tr><td style="text-align:left"><strong>최종 성공률 (100회 이동평균)</strong></td><td style="text-align:center"><strong>98.5% ~ 99.2%</strong></td><td style="text-align:center"><strong>91.4% ~ 94.8%</strong></td></tr><tr><td style="text-align:left"><strong>에피소드당 평균 스텝 수</strong></td><td style="text-align:center">15 ~ 22 스텝</td><td style="text-align:center">38 ~ 52 스텝 (우회 경로)</td></tr><tr><td style="text-align:left"><strong>시드 간 편차(Variance)</strong></td><td style="text-align:center">극히 낮음 (안정적)</td><td style="text-align:center">중간 (초기 무작위 탐색 경로에 따라 수렴 시점 차이 발생)</td></tr></tbody></table>
<ul>
<li class=""><strong>수렴 속도 지연의 원인</strong>: <code>obstacle12</code> 환경에서는 벽 우회로를 발견하기 전까지 충돌 횟수가 많아 평균 보상이 음수(-0.6 ~ -0.4)에 머무는 기간이 400 에피소드까지 연장되었습니다.</li>
<li class=""><strong>우회 정책의 확립</strong>: 800 에피소드 이후 모든 시드에서 성공률이 90% 이상으로 올라서며, 로봇이 벽에 부딪히지 않고 미로 형태의 틈새를 능숙하게 파고드는 <strong>안정적인 회피 주행 정책</strong>을 완성했습니다.</li>
</ul>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="5-다음-편-예고">5. 다음 편 예고<a href="https://globaldevlog.com/ko/blog/2026-05-19-rl-qlearning-3#5-%EB%8B%A4%EC%9D%8C-%ED%8E%B8-%EC%98%88%EA%B3%A0" class="hash-link" aria-label="5. 다음 편 예고에 대한 직접 링크" title="5. 다음 편 예고에 대한 직접 링크" translate="no">​</a></h2>
<p>장애물이 가득한 가상 시뮬레이션에서도 648개 상태 공간의 Tabular Q-Learning이 우회 경로를 성공적으로 학습할 수 있음을 입증했습니다.</p>
<p>다음 4회차 포스팅에서는 학습이 완료된 Q-Table 가중치 파일(<code>models/*.npy</code>)을 불러와 탐색(Epsilon)을 끈 상태에서 순수 결정론적(Greedy) 주행 성능을 정량 평가하는 <strong><code>evaluate_qlearning.py</code> 벤치마크 및 타 알고리즘(DQN, SARSA)과의 성능 비교 분석</strong>을 진행하겠습니다!</p>]]></content:encoded>
            <category>강화학습</category>
            <category>Q-learning</category>
            <category>Webots</category>
            <category>장애물회피</category>
            <category>로컬미니마</category>
            <category>Headless</category>
            <category>Python</category>
        </item>
        <item>
            <title><![CDATA[[Webots 로보틱스] Tabular Q-Learning 구현과 보상 함수(Reward Function) 튜닝 (#2)]]></title>
            <link>https://globaldevlog.com/ko/blog/2026-05-18-rl-qlearning-2</link>
            <guid>https://globaldevlog.com/ko/blog/2026-05-18-rl-qlearning-2</guid>
            <pubDate>Mon, 18 May 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Webots 모바일 로봇의 Tabular Q-Learning 업데이트 수식 구현, 엡실론 감쇠(Epsilon Decay) 탐색 전략, 그리고 희소 보상(Sparse Reward)을 극복하는 보상 셰이핑(Reward Shaping) 튜닝 과정을 심층 분석합니다.]]></description>
            <content:encoded><![CDATA[<p><img decoding="async" loading="lazy" alt="학습 보상 곡선" src="https://globaldevlog.com/ko/assets/images/01_reward_curve-dafe6b9a6b58ab351aa0d2717b504f0a.png" width="800" height="400" class="img_ev3q"></p>
<blockquote>
<p>📌 <strong>핵심 요약 (TL;DR)</strong></p>
<ul>
<li class=""><strong>문제(Problem)</strong>: 목표 도달(+1.0)과 벽 충돌(-1.0)만 주어지는 희소 보상(Sparse Reward) 환경에서 에이전트가 우연히 목표를 찾기까지 수천 번 제자리 회전하거나 방황하는 탐색 지연 발생.</li>
<li class=""><strong>해결(Solution)</strong>: 목표와의 유클리드 거리 변화량에 비례하는 포텐셜 기반 보상(+0.1 × Δdist)과 스텝(-0.01), 회전(-0.01), 후진(-0.05) 페널티를 결합한 <strong>다차원 보상 셰이핑(Reward Shaping)</strong> 설계 + 지수 감쇠 엡실론 그리디(Epsilon-Greedy, 1.0 → 0.05) 전략 적용.</li>
<li class=""><strong>결과(Result)</strong>: 장애물 없는 환경(<code>no_obstacle</code>) 기준 500 에피소드 이내에 이동 평균 보상이 안정적 양수 구간(0.7+)으로 진입하고 최종 목표 도달 성공률 98% 이상 달성.</li>
</ul>
</blockquote>
<!-- -->
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="1-tabular-q-learning-업데이트-수식과-에이전트-설계">1. Tabular Q-Learning 업데이트 수식과 에이전트 설계<a href="https://globaldevlog.com/ko/blog/2026-05-18-rl-qlearning-2#1-tabular-q-learning-%EC%97%85%EB%8D%B0%EC%9D%B4%ED%8A%B8-%EC%88%98%EC%8B%9D%EA%B3%BC-%EC%97%90%EC%9D%B4%EC%A0%84%ED%8A%B8-%EC%84%A4%EA%B3%84" class="hash-link" aria-label="1. Tabular Q-Learning 업데이트 수식과 에이전트 설계에 대한 직접 링크" title="1. Tabular Q-Learning 업데이트 수식과 에이전트 설계에 대한 직접 링크" translate="no">​</a></h2>
<p>표 기반 Q-Learning(Tabular Q-Learning)은 벨만 최적 방정식(Bellman Optimality Equation)에 기반한 <strong>오프폴리시(Off-policy) 시간차 학습(Temporal Difference, TD(0))</strong> 알고리즘입니다.</p>
<p>에이전트는 행동 가치 함수 Q(s, a)를 나타내는 $648 \times 4$ 크기의 2차원 넘파이(NumPy) 배열을 유지하며, 환경으로부터 받은 전이 튜플 $(s, a, r, s', done)$을 바탕으로 아래 공식에 따라 $Q$ 값을 실시간 갱신합니다.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="-q-learning-갱신-공식-td0">📐 Q-Learning 갱신 공식 (TD(0))<a href="https://globaldevlog.com/ko/blog/2026-05-18-rl-qlearning-2#-q-learning-%EA%B0%B1%EC%8B%A0-%EA%B3%B5%EC%8B%9D-td0" class="hash-link" aria-label="📐 Q-Learning 갱신 공식 (TD(0))에 대한 직접 링크" title="📐 Q-Learning 갱신 공식 (TD(0))에 대한 직접 링크" translate="no">​</a></h3>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">Target = r + γ * max_a' Q(s', a') * (1 - done)</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">TD Error = Target - Q(s, a)</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">Q(s, a) &lt;- Q(s, a) + α * TD Error</span><br></div></code></pre></div></div>
<ul>
<li class=""><strong>학습률 (α = 0.1)</strong>: 새로운 경험이 기존 Q-Table에 반영되는 속도를 제어.</li>
<li class=""><strong>할인율 (γ = 0.99)</strong>: 미래에 얻을 보상의 현재 가치를 평가. 장기적인 목표 도달을 위해 높은 할인율을 부여.</li>
<li class=""><strong>종료 상태 처리 (<code>1 - done</code>)</strong>: 목표 도달 또는 충돌로 에피소드가 끝나면 미래 가치($\max Q$)를 0으로 마스킹하여 목표 직전의 즉각적인 보상만 반영.</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="-에이전트-핵심-구현-qlearning_agentpy">💻 에이전트 핵심 구현 (<code>qlearning_agent.py</code>)<a href="https://globaldevlog.com/ko/blog/2026-05-18-rl-qlearning-2#-%EC%97%90%EC%9D%B4%EC%A0%84%ED%8A%B8-%ED%95%B5%EC%8B%AC-%EA%B5%AC%ED%98%84-qlearning_agentpy" class="hash-link" aria-label="-에이전트-핵심-구현-qlearning_agentpy에 대한 직접 링크" title="-에이전트-핵심-구현-qlearning_agentpy에 대한 직접 링크" translate="no">​</a></h3>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><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></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div 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">QLearningAgent</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div 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"> n_states</span><span class="token operator" style="color:#393A34">=</span><span class="token number" style="color:#36acaa">648</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> n_actions</span><span class="token operator" style="color:#393A34">=</span><span class="token number" style="color:#36acaa">4</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> alpha</span><span class="token operator" style="color:#393A34">=</span><span class="token number" style="color:#36acaa">0.1</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> gamma</span><span class="token operator" style="color:#393A34">=</span><span class="token number" style="color:#36acaa">0.99</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">                 eps_start</span><span class="token operator" style="color:#393A34">=</span><span class="token number" style="color:#36acaa">1.0</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> eps_min</span><span class="token operator" style="color:#393A34">=</span><span class="token number" style="color:#36acaa">0.05</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> eps_decay</span><span class="token operator" style="color:#393A34">=</span><span class="token number" style="color:#36acaa">0.995</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> seed</span><span class="token operator" 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><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        self</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">rng </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> np</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">random</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">default_rng</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">seed</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        self</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Q </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> np</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">zeros</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">n_states</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> n_actions</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> dtype</span><span class="token operator" style="color:#393A34">=</span><span class="token plain">np</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">float64</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        self</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">alpha </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token builtin">float</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">alpha</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        self</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">gamma </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token builtin">float</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">gamma</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        self</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">epsilon </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token builtin">float</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">eps_start</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        self</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">eps_min </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token builtin">float</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">eps_min</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        self</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">eps_decay </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token builtin">float</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">eps_decay</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        self</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">n_actions </span><span class="token operator" 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 plain">n_actions</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div 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">act</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"> s</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 plain"> deterministic</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">bool</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">False</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 operator" style="color:#393A34">&gt;</span><span class="token plain"> </span><span class="token builtin">int</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token triple-quoted-string string" style="color:#e3116c">"""ε-Greedy 행동 정책: 탐색(Exploration) vs 활용(Exploitation)"""</span><span class="token plain"></span><br></div><div 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 punctuation" style="color:#393A34">(</span><span class="token keyword" style="color:#00009f">not</span><span class="token plain"> deterministic</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"> self</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">rng</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">random</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">&lt;</span><span class="token plain"> self</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">epsilon</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div 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><span class="token builtin">int</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">rng</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">integers</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">n_actions</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div 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><span class="token builtin">int</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">np</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">argmax</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">Q</span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain">s</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></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div 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">update</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"> s</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 plain"> a</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 plain"> r</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">float</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> s_next</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 plain"> done</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">bool</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 operator" style="color:#393A34">&gt;</span><span class="token plain"> </span><span class="token builtin">float</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token triple-quoted-string string" style="color:#e3116c">"""TD(0) 오프폴리시 Q-Table 갱신"""</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        target </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> r </span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> done </span><span class="token keyword" style="color:#00009f">else</span><span class="token plain"> r </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">gamma </span><span class="token operator" style="color:#393A34">*</span><span class="token plain"> </span><span class="token builtin">float</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">np</span><span class="token punctuation" style="color:#393A34">.</span><span class="token builtin">max</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">Q</span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain">s_next</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></div><div class="token-line" style="color:#393A34"><span class="token plain">        td_error </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> target </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">Q</span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain">s</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> a</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        self</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Q</span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain">s</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> a</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"> self</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">alpha </span><span class="token operator" style="color:#393A34">*</span><span class="token plain"> td_error</span><br></div><div 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><span class="token builtin">float</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">td_error</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div 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">decay_epsilon</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></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token triple-quoted-string string" style="color:#e3116c">"""매 에피소드 종료 시 탐색 확률 감쇠"""</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        self</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">epsilon </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token builtin">max</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">eps_min</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">epsilon </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">eps_decay</span><span class="token punctuation" style="color:#393A34">)</span><br></div></code></pre></div></div>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="2-엡실론-감쇠epsilon-greedy-decay-전략">2. 엡실론 감쇠(Epsilon-Greedy Decay) 전략<a href="https://globaldevlog.com/ko/blog/2026-05-18-rl-qlearning-2#2-%EC%97%A1%EC%8B%A4%EB%A1%A0-%EA%B0%90%EC%87%A0epsilon-greedy-decay-%EC%A0%84%EB%9E%B5" class="hash-link" aria-label="2. 엡실론 감쇠(Epsilon-Greedy Decay) 전략에 대한 직접 링크" title="2. 엡실론 감쇠(Epsilon-Greedy Decay) 전략에 대한 직접 링크" translate="no">​</a></h2>
<p>초기 학습 단계에서는 에이전트가 맵의 다양한 위치와 장애물 구도를 경험해야 하므로 **ε = 1.0 (100% 무작위 행동)**으로 시작합니다.</p>
<p>하지만 탐색률이 계속 높으면 최적 경로를 학습하고도 제자리 회전이나 불필요한 벽 충돌을 반복하게 됩니다. 따라서 매 에피소드마다 아래와 같이 지수 감쇠(Exponential Decay)를 적용했습니다:</p>
<p><strong>ε_(ep+1) = max(ε_min, ε_ep × 0.995)</strong></p>
<ul>
<li class=""><strong>1 에피소드</strong>: ε = 1.0 (완전 무작위 탐색)</li>
<li class=""><strong>300 에피소드</strong>: ε ≈ 0.22 (점진적 최적 정책 활용 시작)</li>
<li class=""><strong>600 에피소드</strong>: ε = 0.05 (최소 탐색률 ε_min 도달, 95% 확률로 Greedy 최적 행동 수행)</li>
</ul>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="3-희소-보상-극복을-위한-다차원-보상-함수-튜닝">3. 희소 보상 극복을 위한 다차원 보상 함수 튜닝<a href="https://globaldevlog.com/ko/blog/2026-05-18-rl-qlearning-2#3-%ED%9D%AC%EC%86%8C-%EB%B3%B4%EC%83%81-%EA%B7%B9%EB%B3%B5%EC%9D%84-%EC%9C%84%ED%95%9C-%EB%8B%A4%EC%B0%A8%EC%9B%90-%EB%B3%B4%EC%83%81-%ED%95%A8%EC%88%98-%ED%8A%9C%EB%8B%9D" class="hash-link" aria-label="3. 희소 보상 극복을 위한 다차원 보상 함수 튜닝에 대한 직접 링크" title="3. 희소 보상 극복을 위한 다차원 보상 함수 튜닝에 대한 직접 링크" translate="no">​</a></h2>
<p>로봇 주행에서 가장 큰 문제는 **"보상이 너무 드물게 주어진다(Sparse Reward)"**는 점입니다. 1.2m × 1.2m 아레나에서 무작위로 움직이다가 0.1m 목표 반경에 우연히 들어가기 전까지는 보상이 0이므로, 에이전트는 무엇이 좋은 행동인지 알 방법이 없습니다.</p>
<p>이를 해결하기 위해 Webots 컨트롤러(<code>RL_controller.py</code>) 내에 <strong>밀집 보상(Dense Reward Shaping)</strong> 메커니즘을 구축했습니다.</p>
<table><thead><tr><th style="text-align:left">보상 항목</th><th style="text-align:center">값</th><th style="text-align:left">설계 의도 및 물리적 효과</th></tr></thead><tbody><tr><td style="text-align:left"><strong>목표 도달 (<code>REWARD_GOAL</code>)</strong></td><td style="text-align:center"><strong>+1.0</strong></td><td style="text-align:left">최종 목표 도달 성공 시 양수 종단 보상</td></tr><tr><td style="text-align:left"><strong>벽/장애물 충돌 (<code>REWARD_COLLISION</code>)</strong></td><td style="text-align:center"><strong>-1.0</strong></td><td style="text-align:left">로봇 본체 파손 및 실패에 대한 엄격한 페널티</td></tr><tr><td style="text-align:left"><strong>거리 델타 보상 (<code>REWARD_DELTA_SCALE</code>)</strong></td><td style="text-align:center"><strong>+0.1 × Δdist</strong></td><td style="text-align:left">이전 스텝 대비 목표와의 거리 단축량에 비례한 유도 보상</td></tr><tr><td style="text-align:left"><strong>스텝 시간 페널티 (<code>REWARD_STEP</code>)</strong></td><td style="text-align:center"><strong>-0.01</strong></td><td style="text-align:left">제자리에 멈춰 서 있는 것을 방지하고 최단 시간 주행 유도</td></tr><tr><td style="text-align:left"><strong>회전 페널티 (<code>REWARD_TURN</code>)</strong></td><td style="text-align:center"><strong>-0.01</strong></td><td style="text-align:left">불필요한 제자리 팽이 돌기(Spinning) 행동 억제</td></tr><tr><td style="text-align:left"><strong>후진 페널티 (<code>REWARD_BACKWARD</code>)</strong></td><td style="text-align:center"><strong>-0.05</strong></td><td style="text-align:left">비효율적인 후진 주행을 지양하고 전방 중심 주행 유도</td></tr></tbody></table>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="-거리-델타-보상delta-distance의-수학적-원리">🔍 거리 델타 보상(Delta Distance)의 수학적 원리<a href="https://globaldevlog.com/ko/blog/2026-05-18-rl-qlearning-2#-%EA%B1%B0%EB%A6%AC-%EB%8D%B8%ED%83%80-%EB%B3%B4%EC%83%81delta-distance%EC%9D%98-%EC%88%98%ED%95%99%EC%A0%81-%EC%9B%90%EB%A6%AC" class="hash-link" aria-label="🔍 거리 델타 보상(Delta Distance)의 수학적 원리에 대한 직접 링크" title="🔍 거리 델타 보상(Delta Distance)의 수학적 원리에 대한 직접 링크" translate="no">​</a></h3>
<p><strong>Δdist = dist_prev - dist_curr</strong></p>
<ul>
<li class="">로봇이 목표 방향으로 $0.05m$ 전진하면: $+0.1 \times 0.05 = +0.005$의 즉각적인 양수 피드백을 받습니다.</li>
<li class="">반대로 목표에서 멀어지면 즉시 감점이 주어지므로, 에이전트는 목표를 향해 머리를 돌리고 직진하는 방향성을 매우 빠른 초반(100 에피소드 이내)에 습득합니다.</li>
</ul>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="4-학습-곡선-및-성공률-검증">4. 학습 곡선 및 성공률 검증<a href="https://globaldevlog.com/ko/blog/2026-05-18-rl-qlearning-2#4-%ED%95%99%EC%8A%B5-%EA%B3%A1%EC%84%A0-%EB%B0%8F-%EC%84%B1%EA%B3%B5%EB%A5%A0-%EA%B2%80%EC%A6%9D" class="hash-link" aria-label="4. 학습 곡선 및 성공률 검증에 대한 직접 링크" title="4. 학습 곡선 및 성공률 검증에 대한 직접 링크" translate="no">​</a></h2>
<p>1,500 에피소드 동안 <code>no_obstacle</code> 환경에서 학습을 진행한 결과, 이론적 설계와 완벽히 일치하는 수렴 패턴을 관찰할 수 있었습니다.</p>
<p><img decoding="async" loading="lazy" alt="성공률 곡선" src="https://globaldevlog.com/ko/assets/images/02_success_curve-a47390e774a7a40ace8869f29884f2bd.png" width="800" height="400" class="img_ev3q"></p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="-학습-지표-분석">📈 학습 지표 분석<a href="https://globaldevlog.com/ko/blog/2026-05-18-rl-qlearning-2#-%ED%95%99%EC%8A%B5-%EC%A7%80%ED%91%9C-%EB%B6%84%EC%84%9D" class="hash-link" aria-label="📈 학습 지표 분석에 대한 직접 링크" title="📈 학습 지표 분석에 대한 직접 링크" translate="no">​</a></h3>
<ol>
<li class=""><strong>0 ~ 200 에피소드 (탐색기)</strong>:<!-- -->
<ul>
<li class="">높은 엡실론($\epsilon &gt; 0.35$)으로 인해 무작위 벽 충돌과 타임아웃(150스텝 초과)이 빈번하게 발생하여 평균 보상이 음수(-0.4 ~ -0.2)에 머뭅니다.</li>
</ul>
</li>
<li class=""><strong>200 ~ 500 에피소드 (급격한 성능 도약기)</strong>:<!-- -->
<ul>
<li class="">거리 델타 보상과 회전 페널티가 Q-Table 전체로 전파되면서 에이전트가 "목표 방위각(<code>goal_heading</code>)이 정면일 때 직진"하는 가치 패턴을 확립합니다.</li>
<li class="">성공률이 20%에서 <strong>80% 이상으로 수직 상승</strong>합니다.</li>
</ul>
</li>
<li class=""><strong>500 ~ 1,500 에피소드 (안정 수렴기)</strong>:<!-- -->
<ul>
<li class="">ε이 최저점(0.05)에 도달하면서 에피소드당 평균 스텝 수가 150스텝(타임아웃)에서 <strong>평균 15~22스텝 수준으로 단축</strong>되었습니다.</li>
<li class="">100회 이동 평균 성공률(Moving Average Success Rate)이 **98.5%**를 상회하며 수렴을 증명했습니다.</li>
</ul>
</li>
</ol>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="5-다음-편-예고">5. 다음 편 예고<a href="https://globaldevlog.com/ko/blog/2026-05-18-rl-qlearning-2#5-%EB%8B%A4%EC%9D%8C-%ED%8E%B8-%EC%98%88%EA%B3%A0" class="hash-link" aria-label="5. 다음 편 예고에 대한 직접 링크" title="5. 다음 편 예고에 대한 직접 링크" translate="no">​</a></h2>
<p>장애물이 없는 환경에서는 648개 상태 공간의 Tabular Q-Learning이 목표를 완벽하게 찾아간다는 것을 확인했습니다.</p>
<p>하지만 <strong>실제 실내 환경에는 복잡한 벽과 기둥 장애물</strong>이 존재합니다. 다음 3회차 포스팅에서는 12개의 벽이 배치된 <strong><code>obstacle12</code> 및 고난도 <code>obstacle_hard</code> 환경에서 에이전트가 마주한 지역 최적해(Local Minima) 함정과 이를 극복하기 위한 다중 시드(Seed) 병렬 실험</strong>을 다루겠습니다!</p>]]></content:encoded>
            <category>강화학습</category>
            <category>Q-learning</category>
            <category>보상함수</category>
            <category>Webots</category>
            <category>로보틱스</category>
            <category>머신러닝</category>
            <category>Python</category>
        </item>
        <item>
            <title><![CDATA[[마음지기 캡스톤 개발일지 #19] 파인튜닝 보고서 자동화와 백엔드 DB 연동 트러블슈팅]]></title>
            <link>https://globaldevlog.com/ko/blog/2026-05-17-maeumjigi-19</link>
            <guid>https://globaldevlog.com/ko/blog/2026-05-17-maeumjigi-19</guid>
            <pubDate>Sun, 17 May 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[캡스톤 디자인 중간 발표를 위해 엑셀 평가 결과 집계 및 python-pptx 라이브러리를 활용한 발표 슬라이드 자동 생성 파이프라인을 구축합니다.]]></description>
            <content:encoded><![CDATA[<p><img decoding="async" loading="lazy" alt="사진 01" src="https://globaldevlog.com/ko/assets/images/01_cover-12712349ddac15465e60572918ef40de.png" width="720" height="405" class="img_ev3q"></p>
<p>제가 이전에 "5월 13일이 마음지기 5월 개발의 마지막일 것"이라고 착각했던 것을 정정합니다! 5월 17일, 어마어마한 양의 백엔드 연동 테스트와 보고서 작업이 몰아쳤습니다.</p>
<!-- -->
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="ai를-활용한-파인튜닝-결과-보고서-자동화">AI를 활용한 파인튜닝 결과 보고서 자동화<a href="https://globaldevlog.com/ko/blog/2026-05-17-maeumjigi-19#ai%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%ED%8C%8C%EC%9D%B8%ED%8A%9C%EB%8B%9D-%EA%B2%B0%EA%B3%BC-%EB%B3%B4%EA%B3%A0%EC%84%9C-%EC%9E%90%EB%8F%99%ED%99%94" class="hash-link" aria-label="AI를 활용한 파인튜닝 결과 보고서 자동화에 대한 직접 링크" title="AI를 활용한 파인튜닝 결과 보고서 자동화에 대한 직접 링크" translate="no">​</a></h3>
<p>2차 파인튜닝(<code>fine_tuning_2</code>)의 방대한 결과 그래프(<code>common</code>, <code>daily</code> 데이터셋 비교 등)를 하나씩 사람이 분석해서 쓰기에는 시간이 너무 부족했습니다.</p>
<blockquote>
<p>"모델들의 추론 결과를 비교하는 보고서를 작성하기 위한 프롬프트를 만들어주세요. 프롬프트 자체를 report_prompt.md로 저장하고, 이에 따라 보고서 작성을 수행해 줘."
"내가 바란 보고서는 .md 형식이 아닌 .docx 형식을 희망했습니다."</p>
</blockquote>
<p>AI에게 결과 지표를 해석하게 하고, 보고서 초안을 자동으로 작성하는 프롬프트 엔지니어링을 시도했습니다. 결과적으로 AI가 각 모델의 장단점과 Alpha 값에 따른 차이를 분석해 훌륭한 워드(.docx) 보고서 초안을 뽑아주었습니다.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="백엔드-db-연동과-401-unauthorized의-늪">백엔드 DB 연동과 401 Unauthorized의 늪<a href="https://globaldevlog.com/ko/blog/2026-05-17-maeumjigi-19#%EB%B0%B1%EC%97%94%EB%93%9C-db-%EC%97%B0%EB%8F%99%EA%B3%BC-401-unauthorized%EC%9D%98-%EB%8A%AA" class="hash-link" aria-label="백엔드 DB 연동과 401 Unauthorized의 늪에 대한 직접 링크" title="백엔드 DB 연동과 401 Unauthorized의 늪에 대한 직접 링크" translate="no">​</a></h3>
<p>보고서가 마무리되자마자 프론트엔드와 백엔드의 시스템 안정성을 테스트하기 위해 <code>backup.sql</code> 데이터를 로컬 PostgreSQL DB에 밀어 넣는 작업을 시작했습니다.</p>
<blockquote>
<p>"backup.sql에 데이터를 넣고 싶습니다. 목적: Frontend, Backend 작동 및 시스템 안정성 테스트."
"backend, frontend 구동 후 접속 성공했으나 메뉴들을 선택해도 반응이 없음"
"이메일 또는 비밀번호가 틀렸어요... INFO: 127.0.0.1:54446 - POST /auth/login 401 Unauthorized"</p>
</blockquote>
<p>DB 덤프를 성공적으로 밀어 넣고 웹에 접속까지는 성공했지만, 로그인이 되지 않는 <code>401 Unauthorized</code> 에러 지옥에 빠졌습니다. <code>debug_login.py</code>를 만들고 uvicorn 서버를 껐다 켜며 수십 번의 트러블슈팅을 진행했습니다. 비밀번호 해싱 방식이 달랐던 건지, DB 마이그레이션이 꼬인 건지 로그를 뒤지며 밤을 새우다시피 했습니다.</p>
<p><img decoding="async" loading="lazy" alt="사진 02" src="https://globaldevlog.com/ko/assets/images/02_db-1d0bb76370f8508377e9472ff5cb15c8.png" width="1373" height="5225" class="img_ev3q"></p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="지식그래프knowledge-graph의-도입-고민">지식그래프(Knowledge Graph)의 도입 고민<a href="https://globaldevlog.com/ko/blog/2026-05-17-maeumjigi-19#%EC%A7%80%EC%8B%9D%EA%B7%B8%EB%9E%98%ED%94%84knowledge-graph%EC%9D%98-%EB%8F%84%EC%9E%85-%EA%B3%A0%EB%AF%BC" class="hash-link" aria-label="지식그래프(Knowledge Graph)의 도입 고민에 대한 직접 링크" title="지식그래프(Knowledge Graph)의 도입 고민에 대한 직접 링크" translate="no">​</a></h3>
<blockquote>
<p>"우리 프로젝트의 목적은 효율적으로 자살예방 챗봇을 만드는 것입니다. 요즘 하네스, 온톨로지, 지식그래프 등을 사용하는 기법이 떠오르는데, LLM 하나만 집중하는 것보다 더 나은 안정성을 가진 챗봇을 만들 수 있을 것 같습니다."</p>
</blockquote>
<p>밤늦은 시간, 단순 LLM 파인튜닝의 한계를 극복하기 위해 온톨로지와 지식그래프(Knowledge Graph)를 융합하는 방안을 새롭게 고민하며 <code>chatbot_design_prompt.md</code>를 설계하기 시작했습니다. 5월 중순을 넘어가며 프로젝트의 스케일이 점점 더 거대해지고 있습니다!</p>]]></content:encoded>
            <category>개발일지</category>
            <category>파인튜닝</category>
            <category>보고서</category>
            <category>백엔드</category>
            <category>DB</category>
            <category>트러블슈팅</category>
            <category>캡스톤디자인</category>
        </item>
        <item>
            <title><![CDATA[[Webots 로보틱스] Q-Learning 자율주행: 648개 상태 공간 이산화와 소켓 통신 아키텍처 (#1)]]></title>
            <link>https://globaldevlog.com/ko/blog/2026-05-17-rl-qlearning-1</link>
            <guid>https://globaldevlog.com/ko/blog/2026-05-17-rl-qlearning-1</guid>
            <pubDate>Sun, 17 May 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Webots 로봇 시뮬레이션 환경에서 연속 센서 데이터를 648개 이산 상태 공간으로 압축하는 Discretizer 설계와 TCP 소켓 통신 기반 강화학습(RL) 아키텍처를 상세히 소개합니다.]]></description>
            <content:encoded><![CDATA[<p><img decoding="async" loading="lazy" alt="Webots 시뮬레이션 환경" src="https://globaldevlog.com/ko/assets/images/01_webots_arena-b49bd821fb90ed1d7e619add024bb5e7.png" width="1721" height="1033" class="img_ev3q"></p>
<blockquote>
<p>📌 <strong>핵심 요약 (TL;DR)</strong></p>
<ul>
<li class=""><strong>문제(Problem)</strong>: 연속적인 센서 거리·각도 데이터로 인해 고전적 표 기반 Q-Learning(Tabular Q-Learning) 적용 시 상태 공간 폭발 및 Webots 시뮬레이터와 학습 스크립트 간 실시간 동기화 오버헤드 발생.</li>
<li class=""><strong>해결(Solution)</strong>: 5차원 연속 센서 입력을 기하학적·물리적 의미를 가진 구간으로 분할하여 3 × 8 × 3 × 3 × 3 = 648개의 이산 상태(Bin)로 매핑(<code>discretizer.py</code>)하고, TCP 로컬 소켓(Port 5000, Line-delimited JSON) 기반의 IPC 제어 파이프라인 구축.</li>
<li class=""><strong>결과(Result)</strong>: O(1)의 초고속 Q-Table 갱신 속도를 확보하고, 시뮬레이션 물리 스텝(32ms)에 맞춘 안정적인 동기화 루프 완성.</li>
</ul>
</blockquote>
<!-- -->
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="1-프로젝트-배경-왜-webots와-q-learning인가">1. 프로젝트 배경: 왜 Webots와 Q-Learning인가?<a href="https://globaldevlog.com/ko/blog/2026-05-17-rl-qlearning-1#1-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EB%B0%B0%EA%B2%BD-%EC%99%9C-webots%EC%99%80-q-learning%EC%9D%B8%EA%B0%80" class="hash-link" aria-label="1. 프로젝트 배경: 왜 Webots와 Q-Learning인가?에 대한 직접 링크" title="1. 프로젝트 배경: 왜 Webots와 Q-Learning인가?에 대한 직접 링크" translate="no">​</a></h2>
<p>자율주행 로봇이 미지의 실내 환경에서 장애물을 회피하며 목표 지점(Target Goal)에 도달하도록 만드는 것은 로보틱스의 고전적이면서도 까다로운 문제입니다. 심층 강화학습(DQN, PPO 등)은 복잡한 환경에 유연하게 대응할 수 있지만, 학습에 막대한 연산 자원이 소모되고 모델 가중치의 해석 가능성(Interpretability)이 낮다는 한계가 있습니다.</p>
<p>본 프로젝트에서는 **가장 직관적이면서도 수학적 수렴성이 검증된 표 기반 강화학습(Tabular Q-Learning, Off-policy TD(0))**을 Webots 물리 시뮬레이터 상의 차동 구동(Differential-drive) 모바일 로봇에 적용하여, 1.2m × 1.2m 실내 아레나에서 안정적인 목표 탐색 및 충돌 회피 정책을 달성하는 것을 목표로 설정했습니다.</p>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="2-전체-시스템-아키텍처-webots--python-ipc">2. 전체 시스템 아키텍처 (Webots ↔ Python IPC)<a href="https://globaldevlog.com/ko/blog/2026-05-17-rl-qlearning-1#2-%EC%A0%84%EC%B2%B4-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98-webots--python-ipc" class="hash-link" aria-label="2. 전체 시스템 아키텍처 (Webots ↔ Python IPC)에 대한 직접 링크" title="2. 전체 시스템 아키텍처 (Webots ↔ Python IPC)에 대한 직접 링크" translate="no">​</a></h2>
<p>시스템은 크게 **시뮬레이션 물리 환경을 담당하는 Webots 컨트롤러(Server)**와 **강화학습 알고리즘을 수행하는 Python 클라이언트(Client)**로 물리적·논리적으로 분리되어 동작합니다.</p>
<div class="language-mermaid codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-mermaid codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">flowchart LR</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    subgraph Webots["Webots 3D Simulator (localhost:5000)"]</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        Robot["Supervisor Robot&lt;br&gt;(초음파 센서 + 모터)"]</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        HeadScan["헤드스캔 기구부&lt;br&gt;(좌/우 1.1 rad 회전)"]</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        Server["TCP Socket Server&lt;br&gt;(Line-delimited JSON)"]</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        Robot -→ HeadScan -→ Server</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    end</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    subgraph Python["Python RL Engine (Client)"]</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        Env["WebotsEnv (gym-like)&lt;br&gt;reset() / step()"]</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        Disc["Discretizer&lt;br&gt;(연속값 → 0~647 Bin)"]</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        Agent["QLearningAgent&lt;br&gt;(Q-Table 648x4)"]</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        Env -→ Disc -→ Agent</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    end</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    Server &lt;-→|"request: {type: step, action: 0..3}&lt;br&gt;response: {state: [...], reward, done}"| Env</span><br></div></code></pre></div></div>
<ul>
<li class=""><strong>Webots 컨트롤러 (<code>RL_controller.py</code>)</strong>: Webots의 <code>Supervisor</code> API를 활용하여 매 액션 스텝마다 로봇 바퀴를 480ms(15 ticks × 32ms) 동안 구동하고, 서보 모터로 헤드를 좌우 1.1 rad 회전시켜 장애물 거리를 정밀 스캔합니다.</li>
<li class=""><strong>통신 프로토콜</strong>: 소켓 통신 시 오버헤드를 최소화하기 위해 줄바꿈 구분자 기반의 JSON(Line-delimited JSON)을 사용합니다.<!-- -->
<ul>
<li class="">요청: <code>{"type": "step", "action": 0}</code> (0: 전진, 1: 후진, 2: 좌회전, 3: 우회전)</li>
<li class="">응답: <code>{"state": [0.45, 0.12, 0.35, 0.28, 0.40], "reward": 0.05, "done": false, "info": {...}}</code></li>
</ul>
</li>
</ul>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="3-연속-센서의-648개-이산-상태-공간bin-설계">3. 연속 센서의 648개 이산 상태 공간(Bin) 설계<a href="https://globaldevlog.com/ko/blog/2026-05-17-rl-qlearning-1#3-%EC%97%B0%EC%86%8D-%EC%84%BC%EC%84%9C%EC%9D%98-648%EA%B0%9C-%EC%9D%B4%EC%82%B0-%EC%83%81%ED%83%9C-%EA%B3%B5%EA%B0%84bin-%EC%84%A4%EA%B3%84" class="hash-link" aria-label="3. 연속 센서의 648개 이산 상태 공간(Bin) 설계에 대한 직접 링크" title="3. 연속 센서의 648개 이산 상태 공간(Bin) 설계에 대한 직접 링크" translate="no">​</a></h2>
<p>초음파 센서와 목표점 간의 위치 데이터는 연속 실수(Float) 값이므로, 표 기반 Q-Learning을 적용하려면 이를 **유한한 정수 상태 인덱스(State Index)**로 변환해야 합니다.</p>
<p>임의로 균등 분할할 경우 상태 수가 기하급수적으로 폭발(Curse of Dimensionality)하므로, 로봇의 기하학적 주행 특성에 맞추어 <strong>5가지 핵심 관측 변수</strong>를 정의하고 아래와 같이 세심하게 구간을 분할했습니다.</p>
<table><thead><tr><th style="text-align:left">관측 변수</th><th style="text-align:center">구간 수</th><th style="text-align:left">분할 기준 (Thresholds)</th><th style="text-align:left">물리적 의미</th></tr></thead><tbody><tr><td style="text-align:left"><strong>목표 거리 (<code>goal_distance</code>)</strong></td><td style="text-align:center"><strong>3</strong></td><td style="text-align:left"><code>[0.6m, 1.2m]</code></td><td style="text-align:left">근거리(Near), 중거리(Mid), 원거리(Far)</td></tr><tr><td style="text-align:left"><strong>목표 상대각도 (<code>goal_heading</code>)</strong></td><td style="text-align:center"><strong>8</strong></td><td style="text-align:left">[-π, π] 균등 8등분 (각 45°)</td><td style="text-align:left">전방, 전우, 우, 후우, 후방, 후좌, 좌, 전좌</td></tr><tr><td style="text-align:left"><strong>전방 장애물 (<code>front_distance</code>)</strong></td><td style="text-align:center"><strong>3</strong></td><td style="text-align:left"><code>[0.15m, 0.30m]</code></td><td style="text-align:left">위험/충돌임박, 주의/접근, 안전</td></tr><tr><td style="text-align:left"><strong>좌측 장애물 (<code>left_distance</code>)</strong></td><td style="text-align:center"><strong>3</strong></td><td style="text-align:left"><code>[0.15m, 0.30m]</code></td><td style="text-align:left">위험, 주의, 안전</td></tr><tr><td style="text-align:left"><strong>우측 장애물 (<code>right_distance</code>)</strong></td><td style="text-align:center"><strong>3</strong></td><td style="text-align:left"><code>[0.15m, 0.30m]</code></td><td style="text-align:left">위험, 주의, 안전</td></tr></tbody></table>
<p>총 상태 공간 크기:
<strong>N_states = 3 × 8 × 3 × 3 × 3 = 648</strong></p>
<p>이로 인해 Q-Table의 크기는 단 <strong>648 × 4 = 2,592개의 파라미터</strong>로 극적으로 압축되었으며, 일반 PC 메모리에서 수 밀리초 내에 인덱싱이 가능해졌습니다.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="-상태-변환-구현-코드-discretizerpy">💻 상태 변환 구현 코드 (<code>discretizer.py</code>)<a href="https://globaldevlog.com/ko/blog/2026-05-17-rl-qlearning-1#-%EC%83%81%ED%83%9C-%EB%B3%80%ED%99%98-%EA%B5%AC%ED%98%84-%EC%BD%94%EB%93%9C-discretizerpy" class="hash-link" aria-label="-상태-변환-구현-코드-discretizerpy에 대한 직접 링크" title="-상태-변환-구현-코드-discretizerpy에 대한 직접 링크" translate="no">​</a></h3>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><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></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># 상태 차원 정의</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">N_GOAL_D</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> N_GOAL_H</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> N_FRONT</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> N_LEFT</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> N_RIGHT </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">3</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">8</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">3</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">3</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">3</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">N_STATES </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> N_GOAL_D </span><span class="token operator" style="color:#393A34">*</span><span class="token plain"> N_GOAL_H </span><span class="token operator" style="color:#393A34">*</span><span class="token plain"> N_FRONT </span><span class="token operator" style="color:#393A34">*</span><span class="token plain"> N_LEFT </span><span class="token operator" style="color:#393A34">*</span><span class="token plain"> N_RIGHT  </span><span class="token comment" style="color:#999988;font-style:italic"># 648</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># 임계값 정의</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">_HEADING_BINS </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> np</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">linspace</span><span class="token punctuation" style="color:#393A34">(</span><span class="token operator" style="color:#393A34">-</span><span class="token plain">np</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">pi</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> np</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">pi</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">9</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">[</span><span class="token number" style="color:#36acaa">1</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 plain"> </span><span class="token comment" style="color:#999988;font-style:italic"># 8분할 경계</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">_NEAR_BINS    </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> np</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">array</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.15</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.30</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 comment" style="color:#999988;font-style:italic"># 초음파 안전/주의/위험 경계</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">_DIST_BINS    </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> np</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">array</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.6</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1.2</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 comment" style="color:#999988;font-style:italic"># 목표 거리 경계</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div 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">discretize</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">state_dict</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token builtin">dict</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 plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token triple-quoted-string string" style="color:#e3116c">"""5차원 실수 관측값을 0~647 사이의 정수 인덱스로 변환"""</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    gd </span><span class="token operator" 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 plain">np</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">digitize</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">state_dict</span><span class="token punctuation" style="color:#393A34">[</span><span class="token string" style="color:#e3116c">"goal_distance"</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> _DIST_BINS</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    gh </span><span class="token operator" 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 plain">np</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">digitize</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">state_dict</span><span class="token punctuation" style="color:#393A34">[</span><span class="token string" style="color:#e3116c">"goal_heading"</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> _HEADING_BINS</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    df </span><span class="token operator" 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 plain">np</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">digitize</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">state_dict</span><span class="token punctuation" style="color:#393A34">[</span><span class="token string" style="color:#e3116c">"front_distance"</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> _NEAR_BINS</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    dl </span><span class="token operator" 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 plain">np</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">digitize</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">state_dict</span><span class="token punctuation" style="color:#393A34">[</span><span class="token string" style="color:#e3116c">"left_distance"</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> _NEAR_BINS</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    dr </span><span class="token operator" 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 plain">np</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">digitize</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">state_dict</span><span class="token punctuation" style="color:#393A34">[</span><span class="token string" style="color:#e3116c">"right_distance"</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> _NEAR_BINS</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token comment" style="color:#999988;font-style:italic"># 다차원 인덱스를 1차원 평탄화(Flatten) 인덱스로 변환</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    index </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 punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">gd </span><span class="token operator" style="color:#393A34">*</span><span class="token plain"> N_GOAL_H </span><span class="token operator" style="color:#393A34">+</span><span class="token plain"> gh</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"> N_FRONT </span><span class="token operator" style="color:#393A34">+</span><span class="token plain"> df</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"> N_LEFT </span><span class="token operator" style="color:#393A34">+</span><span class="token plain"> dl</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"> N_RIGHT </span><span class="token operator" style="color:#393A34">+</span><span class="token plain"> dr</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div 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><span class="token builtin">int</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">index</span><span class="token punctuation" style="color:#393A34">)</span><br></div></code></pre></div></div>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="4-헤드스캔head-scan-메커니즘과-충돌-판정">4. 헤드스캔(Head-Scan) 메커니즘과 충돌 판정<a href="https://globaldevlog.com/ko/blog/2026-05-17-rl-qlearning-1#4-%ED%97%A4%EB%93%9C%EC%8A%A4%EC%BA%94head-scan-%EB%A9%94%EC%BB%A4%EB%8B%88%EC%A6%98%EA%B3%BC-%EC%B6%A9%EB%8F%8C-%ED%8C%90%EC%A0%95" class="hash-link" aria-label="4. 헤드스캔(Head-Scan) 메커니즘과 충돌 판정에 대한 직접 링크" title="4. 헤드스캔(Head-Scan) 메커니즘과 충돌 판정에 대한 직접 링크" translate="no">​</a></h2>
<p><img decoding="async" loading="lazy" alt="시스템 아키텍처" src="https://globaldevlog.com/ko/assets/images/02_architecture-83a0cdfb8297ac248797b32bbc7fd69b.png" width="2000" height="900" class="img_ev3q"></p>
<p>일반적인 2륜 로봇은 전방에 센서가 고정되어 있어 좌우 측면의 사각지대 장애물을 감지하기 어렵습니다. 이를 극복하기 위해 컨트롤러에 <strong>헤드스캔(Head-Scan) 메커니즘</strong>을 탑재했습니다.</p>
<ol>
<li class=""><strong>액션 실행</strong>: 바퀴 모터를 3.0 rad/s 속도로 480ms 동안 구동.</li>
<li class=""><strong>헤드 회전</strong>: 서보 모터를 좌측으로 +1.1 rad (약 63°) 회전하여 좌측 거리 측정 후, 우측으로 -1.1 rad 회전하여 우측 거리를 측정.</li>
<li class=""><strong>정위치 복귀</strong>: 다시 정면(0 rad)으로 복귀하여 전방 거리를 최종 갱신.</li>
<li class=""><strong>종료 판정</strong>:<!-- -->
<ul>
<li class=""><strong>목표 도달</strong>: 목표물과의 유클리드 거리가 $0.10m$ 이하일 때 $\rightarrow$ 성공 (<code>done = True</code>, 보상 $+1.0$)</li>
<li class=""><strong>벽 충돌</strong>: 전방/좌/우 거리 중 하나라도 $0.08m$ 이하로 떨어질 때 $\rightarrow$ 실패 (<code>done = True</code>, 보상 $-1.0$)</li>
</ul>
</li>
</ol>
<hr>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="5-다음-편-예고">5. 다음 편 예고<a href="https://globaldevlog.com/ko/blog/2026-05-17-rl-qlearning-1#5-%EB%8B%A4%EC%9D%8C-%ED%8E%B8-%EC%98%88%EA%B3%A0" class="hash-link" aria-label="5. 다음 편 예고에 대한 직접 링크" title="5. 다음 편 예고에 대한 직접 링크" translate="no">​</a></h2>
<p>이번 1회차에서는 강화학습 에이전트가 주행을 시작하기 위한 <strong>Webots 가상 환경 구축, TCP 소켓 기반 IPC 인터페이스, 648개 상태 공간 이산화 기법</strong>을 정리했습니다.</p>
<p>다음 2회차 포스팅에서는 본격적으로 <strong>Tabular Q-Learning 수식 구현, ε-Greedy 감쇠 탐색 전략, 그리고 시뮬레이션 환경에서의 보상 함수(Reward Function) 튜닝 과정</strong>을 다루겠습니다!</p>]]></content:encoded>
            <category>강화학습</category>
            <category>Q-learning</category>
            <category>Webots</category>
            <category>로보틱스</category>
            <category>상태공간이산화</category>
            <category>Python</category>
        </item>
        <item>
            <title><![CDATA[[마음지기 캡스톤 개발일지 #18] 추론 토큰(Token) 확장과 2차 파인튜닝 결과 비교]]></title>
            <link>https://globaldevlog.com/ko/blog/2026-05-13-maeumjigi-18</link>
            <guid>https://globaldevlog.com/ko/blog/2026-05-13-maeumjigi-18</guid>
            <pubDate>Wed, 13 May 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[짧고 경직된 답변 문제를 해결하기 위해 max_new_tokens를 384로 확장하고, LoRA alpha 파라미터(8 vs 16)에 따른 답변 공감도 변화를 분석합니다.]]></description>
            <content:encoded><![CDATA[<p><img decoding="async" loading="lazy" alt="사진 01" src="https://globaldevlog.com/ko/assets/images/01_cover-4dcde4cef4209e81d49b5f5306af009d.png" width="1484" height="888" class="img_ev3q"></p>
<p>어제 발견했던 하드코딩 버그를 모두 고치고, 드디어 제대로 된 2차 파인튜닝 모델들의 추론(Inference) 테스트를 재개한 5월 13일입니다.</p>
<!-- -->
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="짧은-답변의-원인-토큰-길이를-늘려라">짧은 답변의 원인, 토큰 길이를 늘려라!<a href="https://globaldevlog.com/ko/blog/2026-05-13-maeumjigi-18#%EC%A7%A7%EC%9D%80-%EB%8B%B5%EB%B3%80%EC%9D%98-%EC%9B%90%EC%9D%B8-%ED%86%A0%ED%81%B0-%EA%B8%B8%EC%9D%B4%EB%A5%BC-%EB%8A%98%EB%A0%A4%EB%9D%BC" class="hash-link" aria-label="짧은 답변의 원인, 토큰 길이를 늘려라!에 대한 직접 링크" title="짧은 답변의 원인, 토큰 길이를 늘려라!에 대한 직접 링크" translate="no">​</a></h3>
<p>추론 결과를 하나씩 뜯어보던 중, <code>Llama33-70B_a8</code> 모델이 내놓은 답변이 여전히 묘하게 짧고 딱딱하다는 것을 느꼈습니다.</p>
<blockquote>
<p>"이것은 Llama33-70B_a8의 답변 중 하나입니다. max_seq_length와 max_new_tokens이 너무 적다고 느껴집니다. 늘리고 싶네요. VRAM 현황을 보면 얼마나 늘릴 수 있나요?"</p>
</blockquote>
<p>AI와 VRAM 여유 공간(당시 39.8GB)을 계산한 끝에, 생성할 수 있는 최대 단어 수인 <code>max_new_tokens</code>를 384로 과감하게 상향 조정했습니다. 결과는 대성공이었습니다!</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">[변경 전] "그런 생각이 드신다니 정말 힘드시겠어요. 전문가와 이야기 나누는 것이 중요합니다."</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">[변경 후] "그런 생각이 드신다니 정말 힘드시겠어요. 하지만 그런 생각이 드는 것은 당신이 약해서가 아니라 지금 너무 많은 부담을 지고 계신다는 신호입니다. 전문가와 함께 이런 감정들을 천천히 풀어보시는 것을 권해드립니다."</span><br></div></code></pre></div></div>
<p>단지 토큰 허용치만 늘려주었을 뿐인데, 모델이 문맥을 끝까지 유지하며 훨씬 더 깊이 있고 따뜻한 공감의 문장들을 쏟아내기 시작했습니다. 제한된 토큰 수에 갇혀 모델이 스스로 말을 아끼고 있었던 셈입니다.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="alpha-값-비교-분석-a8-vs-a16">Alpha 값 비교 분석 (a=8 vs a=16)<a href="https://globaldevlog.com/ko/blog/2026-05-13-maeumjigi-18#alpha-%EA%B0%92-%EB%B9%84%EA%B5%90-%EB%B6%84%EC%84%9D-a8-vs-a16" class="hash-link" aria-label="Alpha 값 비교 분석 (a=8 vs a=16)에 대한 직접 링크" title="Alpha 값 비교 분석 (a=8 vs a=16)에 대한 직접 링크" translate="no">​</a></h3>
<p><img decoding="async" loading="lazy" alt="사진 02" src="https://globaldevlog.com/ko/assets/images/02_graph-feb5d1991f44dd7f378232f6b3515752.png" width="2053" height="1460" class="img_ev3q"></p>
<p>2차 파인튜닝의 가장 큰 목적이었던 LoRA 하이퍼파라미터 <code>alpha</code> 값에 따른 성능 차이도 결과가 나왔습니다. <code>fine_tuning_2</code> 폴더에 쌓인 로그를 분석하여 <code>05_alpha_comparison_daily.png</code> 등의 그래프를 생성해 냈습니다. <code>rank=16</code>으로 고정된 상태에서 <code>alpha=8</code>을 주었을 때와 <code>alpha=16</code>을 주었을 때, 모델의 과적합(Overfitting) 양상과 최종 답변 퀄리티가 어떻게 달라지는지 명확하게 시각화할 수 있었습니다.</p>
<p>결과적으로 올바른 하이퍼파라미터와 넉넉한 토큰 길이를 부여받은 모델들은 드디어 저희가 애초에 기획했던 '진정한 마음지기'의 모습에 가까워지고 있었습니다. 중간발표 준비가 아주 순조롭게 마무리될 것 같습니다!</p>]]></content:encoded>
            <category>개발일지</category>
            <category>파인튜닝</category>
            <category>추론</category>
            <category>max_new_tokens</category>
            <category>공감능력</category>
            <category>캡스톤디자인</category>
        </item>
        <item>
            <title><![CDATA[[마음지기 캡스톤 개발일지 #17] 2차 파인튜닝 완료 및 평가 자동화 파이프라인의 함정]]></title>
            <link>https://globaldevlog.com/ko/blog/2026-05-12-maeumjigi-17</link>
            <guid>https://globaldevlog.com/ko/blog/2026-05-12-maeumjigi-17</guid>
            <pubDate>Tue, 12 May 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[정제된 프롬프트 포맷과 LoRA rank/alpha 하이퍼파라미터 조정을 적용하여 2차 파인튜닝을 실행하고 학습 수렴도를 점검한 과정입니다.]]></description>
            <content:encoded><![CDATA[<p><img decoding="async" loading="lazy" alt="사진 01" src="https://globaldevlog.com/ko/assets/images/01_cover-14a3c0d517e4c85b93ada4e973112325.png" width="2000" height="1457" class="img_ev3q"></p>
<p>LoRA 하이퍼파라미터 실수(alpha/rank 설정 오류)를 바로잡기 위해 야심 차게 시작했던 2차 파인튜닝(<code>fine_tuning_2</code>) 병렬 학습이 5월 12일 마침내 끝이 났습니다!</p>
<!-- -->
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="best-checkpoint-추출과-추론-파이프라인-구축">Best Checkpoint 추출과 추론 파이프라인 구축<a href="https://globaldevlog.com/ko/blog/2026-05-12-maeumjigi-17#best-checkpoint-%EC%B6%94%EC%B6%9C%EA%B3%BC-%EC%B6%94%EB%A1%A0-%ED%8C%8C%EC%9D%B4%ED%94%84%EB%9D%BC%EC%9D%B8-%EA%B5%AC%EC%B6%95" class="hash-link" aria-label="Best Checkpoint 추출과 추론 파이프라인 구축에 대한 직접 링크" title="Best Checkpoint 추출과 추론 파이프라인 구축에 대한 직접 링크" translate="no">​</a></h3>
<p>3개 모델에 대해 6가지 하이퍼파라미터 조합으로 돌렸던 파인튜닝이 모두 완료되었습니다. 곧바로 <code>eval_loss</code>를 비교하여 가장 성능이 좋은 최고(Best)의 체크포인트를 뽑아내 모델별 <code>adapter</code> 폴더로 옮기는 작업을 진행했습니다.</p>
<p>또한, 서버에서 백그라운드로 추론(Eval)과 결과 비교(Comparison)를 한 번에 끝낼 수 있도록 <code>run_test_20_30.sh</code> 스크립트를 새롭게 구성했습니다.</p>
<blockquote>
<p>"현재 HCX-Text-1p5B 모델들이 15분 동안 12개 생성했습니다. GPU Util은 각 그래픽 카드 당 1.6GB..."</p>
</blockquote>
<p>그러나 추론 속도는 여전히 절망적이었습니다. 가장 가벼운 1.5B 모델조차 1분에 채 1개의 답변도 만들어내지 못하고 있었습니다. VRAM 사용량은 1.6GB밖에 안 되는데, Batch Size가 1로 잡혀 있어 병목 현상이 발생한 것입니다. 여기에 서버 용량 부족 문제까지 겹치면서, 프로젝트 기본 경로를 <code>~/home/</code>에서 <code>~/home/data/temporary/</code>로 급하게 이전하느라 모든 스크립트 경로를 수정하는 소동도 있었습니다.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="하드코딩의-함정에-빠지다">하드코딩의 함정에 빠지다<a href="https://globaldevlog.com/ko/blog/2026-05-12-maeumjigi-17#%ED%95%98%EB%93%9C%EC%BD%94%EB%94%A9%EC%9D%98-%ED%95%A8%EC%A0%95%EC%97%90-%EB%B9%A0%EC%A7%80%EB%8B%A4" class="hash-link" aria-label="하드코딩의 함정에 빠지다에 대한 직접 링크" title="하드코딩의 함정에 빠지다에 대한 직접 링크" translate="no">​</a></h3>
<p><img decoding="async" loading="lazy" alt="사진 02" src="https://globaldevlog.com/ko/assets/images/02_bug-900bd56611b3d6521891bcc956843259.png" width="2000" height="1457" class="img_ev3q"></p>
<p>그렇게 힘들게 평가 코드를 다 돌리고 난 후, 소름 돋는 사실을 발견했습니다.</p>
<blockquote>
<p>"왜 20_eval_ft_models.ipynb의 MODEL_ALIAS = 'Qwen35-9B_a16' 이렇게 하나의 모델로 하드코딩 되어 있나?"
"그럼 서버에서 logs/.../20_eval_HCX-Text-1p5B_a8.ipynb에서도 MODEL_ALIAS = 'Qwen35-9B_a16'가 정의되어 있었는데, 파일명과 모델명이 다른 게 정상이라는 거지?"</p>
</blockquote>
<p>그렇습니다. 평가 자동화 스크립트를 짜면서 실수로 <code>MODEL_ALIAS</code>를 하드코딩해버린 것입니다. HCX 모델을 테스트한다고 파일은 열심히 복사해서 돌렸지만, 정작 코드 내에서는 계속 <code>Qwen35-9B_a16</code> 모델만 주구장창 불러와서 똑같은 추론을 반복하고 있었던 셈입니다!</p>
<p>파인튜닝은 과학이 아니라 꼼꼼함의 싸움이라는 것을 또 한 번 뼈저리게 느낀 하루였습니다. 하드코딩된 버그를 고치고 내일 다시 추론을 돌려야 할 것 같습니다.</p>]]></content:encoded>
            <category>개발일지</category>
            <category>파인튜닝</category>
            <category>최고성능</category>
            <category>체크포인트</category>
            <category>하드코딩</category>
            <category>캡스톤디자인</category>
        </item>
        <item>
            <title><![CDATA[[마음지기 캡스톤 개발일지 #16] 치명적 실수와 2차 파인튜닝, 그리고 React Native 앱 첫 구동]]></title>
            <link>https://globaldevlog.com/ko/blog/2026-05-08-maeumjigi-16</link>
            <guid>https://globaldevlog.com/ko/blog/2026-05-08-maeumjigi-16</guid>
            <pubDate>Fri, 08 May 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[추론 결과에서 발견된 프롬프트 템플릿 하드코딩 오염 문제를 역추적하여 데이터셋 포맷팅 결함을 긴급 수정하고 재현성을 확보한 트러블슈팅입니다.]]></description>
            <content:encoded><![CDATA[<p><img decoding="async" loading="lazy" alt="사진 01" src="https://globaldevlog.com/ko/assets/images/01_cover-9eab7db0dac5c88d9a120a99e5e703d4.png" width="1874" height="1526" class="img_ev3q"></p>
<p>5월 8일은 그동안 뽑아낸 파인튜닝 결과들을 바탕으로 캡스톤디자인 중간 발표 자료(PPT)를 만들던 날이었습니다. 하지만 기분 좋게 슬라이드를 채워나가던 중, 등골이 서늘해지는 엄청난 실수를 발견하고 말았습니다.</p>
<!-- -->
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="lora-하이퍼파라미터의-치명적-실수-발견">LoRA 하이퍼파라미터의 치명적 실수 발견<a href="https://globaldevlog.com/ko/blog/2026-05-08-maeumjigi-16#lora-%ED%95%98%EC%9D%B4%ED%8D%BC%ED%8C%8C%EB%9D%BC%EB%AF%B8%ED%84%B0%EC%9D%98-%EC%B9%98%EB%AA%85%EC%A0%81-%EC%8B%A4%EC%88%98-%EB%B0%9C%EA%B2%AC" class="hash-link" aria-label="LoRA 하이퍼파라미터의 치명적 실수 발견에 대한 직접 링크" title="LoRA 하이퍼파라미터의 치명적 실수 발견에 대한 직접 링크" translate="no">​</a></h3>
<p>PPT에 들어갈 모델 구조도와 하이퍼파라미터 정보를 AI와 함께 다듬고 있었습니다.</p>
<blockquote>
<p>"파인튜닝 결과 예시를 보면 base model보다 답변이 짧아졌습니다. 이는 LoRA의 r을 8로 하고 alpha를 16으로 해서 중간 feature를 너무 줄이다 보니 답변이 짧아졌다고 생각이 드는데..."
"아니 a/r 이었어? 나는 r/a라고 알고 있었는데...??? 나는 정말 LoRA를 욕심쟁이로 반영한 거구나..."</p>
</blockquote>
<p>LoRA 파인튜닝 시 <code>alpha</code> 값은 보통 <code>rank</code> 값과 동일하거나 2배로 주는 것이 정석입니다. 그런데 계산식 적용 비율을 완전히 반대로 착각하여, 가중치 업데이트가 극단적으로 억눌리거나 과하게 튕겨버리게 세팅해 버린 것입니다. 어쩐지 파인튜닝된 모델의 답변 길이가 뚝뚝 끊기고 짧아지더라니, 원인은 바로 제 손끝에 있었습니다.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="2차-파인튜닝-fine_tuning_2-결단">2차 파인튜닝 (fine_tuning_2) 결단<a href="https://globaldevlog.com/ko/blog/2026-05-08-maeumjigi-16#2%EC%B0%A8-%ED%8C%8C%EC%9D%B8%ED%8A%9C%EB%8B%9D-fine_tuning_2-%EA%B2%B0%EB%8B%A8" class="hash-link" aria-label="2차 파인튜닝 (fine_tuning_2) 결단에 대한 직접 링크" title="2차 파인튜닝 (fine_tuning_2) 결단에 대한 직접 링크" translate="no">​</a></h3>
<p>이미 1차 결과가 나왔지만, 완벽한 '마음지기'를 위해 이대로 넘어갈 순 없었습니다. 즉시 <code>fine_tuning_2</code> 폴더를 파고 2차 파인튜닝을 결단했습니다.</p>
<blockquote>
<p>"이번에 사용할 모델은 HCX-Text-1p5B, Llama33-70B, Qwen35-9B 입니다. MIG-0에서는 rank는 16, alpha는 16으로, MIG-1에서는 rank=16, a=8로 설정하여 학습 진행해 줘."
"Daily_train_dataset.jsonl에서 가장 긴 데이터 상위 2000개를 뽑아서 Daily_train_dataset_long.jsonl이란 제목의 파일로 생성해 주세요."</p>
</blockquote>
<p>답변이 짧아지는 현상을 방지하기 위해 가장 텍스트 길이가 긴 상위 2000개의 데이터셋만을 따로 추출하고, MIG(Multi-Instance GPU) 자원을 나누어 rank와 alpha 비율에 따른 성능 차이를 직접 비교하는 병렬 학습을 새롭게 돌리기 시작했습니다.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="백엔드-모델이-도는-동안-프론트엔드앱-개발-착수">백엔드 모델이 도는 동안, 프론트엔드(앱) 개발 착수<a href="https://globaldevlog.com/ko/blog/2026-05-08-maeumjigi-16#%EB%B0%B1%EC%97%94%EB%93%9C-%EB%AA%A8%EB%8D%B8%EC%9D%B4-%EB%8F%84%EB%8A%94-%EB%8F%99%EC%95%88-%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C%EC%95%B1-%EA%B0%9C%EB%B0%9C-%EC%B0%A9%EC%88%98" class="hash-link" aria-label="백엔드 모델이 도는 동안, 프론트엔드(앱) 개발 착수에 대한 직접 링크" title="백엔드 모델이 도는 동안, 프론트엔드(앱) 개발 착수에 대한 직접 링크" translate="no">​</a></h3>
<p><img decoding="async" loading="lazy" alt="사진 02" src="https://globaldevlog.com/ko/assets/images/02_app-9efe68797b061b7eda0d68995627a8cc.png" width="1476" height="1526" class="img_ev3q"></p>
<p>모델이 다시 학습의 굴레에 빠진 동안, 저는 손을 놓고 있지 않았습니다. 드디어 실제 사용자들이 '마음지기'를 만나게 될 프론트엔드 앱 개발에 착수했습니다!</p>
<blockquote>
<p>"maeum-app을 구동하기 위해 QR코드를 시현해야 하는데... npx go start를 입력해도 에러 메시지만 시현돼. 어떻게 그리고 어디에 QR코드가 시현될 수 있을까?"</p>
</blockquote>
<p>React Native(Expo)를 이용해 <code>maeum-app</code> 프로젝트를 생성하고 로컬 서버를 띄워 스마트폰과 연결하려 고군분투했습니다. 엉뚱하게 <code>npx go start</code>를 치며 헤매기도 했지만, 마침내 Expo QR 코드를 띄우고 앱 아이콘(<code>assets/logo.png</code>)을 세팅하는 데 성공했습니다. 백엔드의 두뇌(AI)와 프론트엔드의 얼굴(App)이 동시에 만들어지고 있는 가슴 벅찬 하루였습니다.</p>]]></content:encoded>
            <category>개발일지</category>
            <category>파인튜닝</category>
            <category>LoRA</category>
            <category>하이퍼파라미터</category>
            <category>ReactNative</category>
            <category>앱개발</category>
            <category>프론트엔드</category>
        </item>
        <item>
            <title><![CDATA[[마음지기 캡스톤 개발일지 #15] 5개 모델 파인튜닝 결과 시각화 및 로컬 환경 세팅]]></title>
            <link>https://globaldevlog.com/ko/blog/2026-05-07-maeumjigi-15</link>
            <guid>https://globaldevlog.com/ko/blog/2026-05-07-maeumjigi-15</guid>
            <pubDate>Thu, 07 May 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[파인튜닝된 각 모델의 Perplexity와 벤치마크 지표를 분석하고, matplotlib을 활용해 모델별 성능 격차를 한눈에 파악하는 시각화 파이프라인을 구축합니다.]]></description>
            <content:encoded><![CDATA[<p><img decoding="async" loading="lazy" alt="사진 01" src="https://globaldevlog.com/ko/assets/images/01_cover-b5ad981f0a0602b8afaf1bdbc982094d.png" width="1181" height="731" class="img_ev3q"></p>
<p>5월 7일은 그동안 피땀 흘려 파인튜닝한 총 5개의 모델(Llama3.3-70B, DSR1-70B, HCX-Omni-8B, Qwen35-9B, HCX-Text-1.5B)의 결과를 집대성하고, 본격적인 시각화와 로컬 환경 구성을 진행한 날입니다.</p>
<!-- -->
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="모델-비교를-위한-시각화와-로컬-환경-세팅">모델 비교를 위한 시각화와 로컬 환경 세팅<a href="https://globaldevlog.com/ko/blog/2026-05-07-maeumjigi-15#%EB%AA%A8%EB%8D%B8-%EB%B9%84%EA%B5%90%EB%A5%BC-%EC%9C%84%ED%95%9C-%EC%8B%9C%EA%B0%81%ED%99%94%EC%99%80-%EB%A1%9C%EC%BB%AC-%ED%99%98%EA%B2%BD-%EC%84%B8%ED%8C%85" class="hash-link" aria-label="모델 비교를 위한 시각화와 로컬 환경 세팅에 대한 직접 링크" title="모델 비교를 위한 시각화와 로컬 환경 세팅에 대한 직접 링크" translate="no">​</a></h3>
<p>그동안 무거운 서버에서 돌리던 결과 비교 코드를 이제 제 개인 노트북에서도 편하게 확인하기 위해 <code>31_comparison_local.ipynb</code> 노트북을 새롭게 작성했습니다.</p>
<blockquote>
<p>"30_comparison.ipynb은 내 노트북에서도 작동할 수 있지 않나?"
"현재 선택된 kernel에 pandas, matplotlib, seaborn, bert_score를 설치하는 방법을 알려주세요."</p>
</blockquote>
<p>VSCode에서 로컬 Jupyter 커널을 잡고 필요한 라이브러리들을 설치하는 과정에서 잦은 커널 충돌(Kernel Crashed)과 <code>ModuleNotFoundError: No module named 'ft_utils'</code> 같은 짜증 나는 에러들을 마주했지만, 차근차근 폴더 경로를 맞추며 해결해 나갔습니다. 일부 모델(DSR1-70B 등)에서 누락되었던 <code>ft_metrics.json</code> 파일도 스크립트를 통해 성공적으로 복구해 냈습니다.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="5개-모델-종합-리포트-및-그래프-시각화">5개 모델 종합 리포트 및 그래프 시각화<a href="https://globaldevlog.com/ko/blog/2026-05-07-maeumjigi-15#5%EA%B0%9C-%EB%AA%A8%EB%8D%B8-%EC%A2%85%ED%95%A9-%EB%A6%AC%ED%8F%AC%ED%8A%B8-%EB%B0%8F-%EA%B7%B8%EB%9E%98%ED%94%84-%EC%8B%9C%EA%B0%81%ED%99%94" class="hash-link" aria-label="5개 모델 종합 리포트 및 그래프 시각화에 대한 직접 링크" title="5개 모델 종합 리포트 및 그래프 시각화에 대한 직접 링크" translate="no">​</a></h3>
<p>가장 중요한 작업은 이 5개 모델의 성능을 한눈에 비교할 수 있는 시각화였습니다. AI와 함께 <code>5MODELS_ORGANIZATION_PLAN_present.md</code>라는 계획서를 세우고 체계적으로 데이터 정리를 시작했습니다.</p>
<blockquote>
<p>"희미한 선과 굵은 선의 차이가 무엇인가? 각 모델별 training loss와 eval loss를 가져와서 전체 모델의 loss를 각각 그래프로 시각화하시오."
"굵은 선은 없애고 희미한 선을 선명하게 변경하여 저장해 줘."</p>
</blockquote>
<p>AI가 그려준 matplotlib 그래프에서 불필요한 추세선(굵은 선)을 제거하고, 모델별 Training Loss와 Eval Loss 곡선을 깔끔하게 한곳에 모은 <code>03_training_loss_all.png</code>와 <code>04_eval_loss_all.png</code>를 최종적으로 뽑아냈습니다. 또한 각 모델의 LoRA Rank 붕괴 현상(Rank Decomposition)을 분석한 그래프와 GPU 메모리 점유율 차트(<code>02_gpu_memory.png</code>)까지 완벽하게 문서화했습니다.</p>
<p><img decoding="async" loading="lazy" alt="사진 02" src="https://globaldevlog.com/ko/assets/images/02_graph-8f91ee1a78e9220a703c6884ad9a4f40.png" width="1785" height="735" class="img_ev3q"></p>
<p>이제 이 그래프들을 바탕으로 다음 주 캡스톤디자인 중간발표 자료를 멋지게 꾸밀 수 있게 되었습니다. 과연 이 5개의 모델 중 어떤 모델이 가장 뛰어난 퀄리티의 '마음지기' 챗봇으로 최종 선정될까요?</p>]]></content:encoded>
            <category>개발일지</category>
            <category>파인튜닝</category>
            <category>결과분석</category>
            <category>시각화</category>
            <category>Jupyter</category>
            <category>캡스톤디자인</category>
            <category>AI</category>
        </item>
    </channel>
</rss>