<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>미니감자</title>
    <link>https://smp0417.tistory.com/</link>
    <description>알찬 감자가 되기 위해 빡세게 공부하는 빡성의 블로그입니다.</description>
    <language>ko</language>
    <pubDate>Fri, 10 Apr 2026 19:41:37 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>빡성</managingEditor>
    <image>
      <title>미니감자</title>
      <url>https://tistory1.daumcdn.net/tistory/7154654/attach/ecea52ee1a494a69b75d47f12e5cab5f</url>
      <link>https://smp0417.tistory.com</link>
    </image>
    <item>
      <title>[UNLV]   Las Vegas에서 한 달 여행기 - 하편</title>
      <link>https://smp0417.tistory.com/79</link>
      <description>&lt;div id=&quot;travel-blog-root&quot;&gt;&lt;header class=&quot;blog-header&quot;&gt;&lt;span class=&quot;blog-tag&quot;&gt;Travel Journal &amp;middot; 2편&lt;/span&gt;
&lt;p class=&quot;blog-subtitle&quot; data-ke-size=&quot;size16&quot;&gt;그랜드캐니언부터 인천 귀국까지&lt;/p&gt;
&lt;div class=&quot;blog-meta&quot;&gt;&lt;span&gt;  2025.02.14 ~ 03.02&lt;/span&gt; &lt;span&gt;  South Outlet &amp;rarr; Valley of Fire &amp;rarr; Bryce/Antelope/Grand Canyon &amp;rarr; Incheon&lt;/span&gt;&lt;/div&gt;
&lt;/header&gt;&lt;nav class=&quot;toc-nav&quot;&gt;
&lt;h3 class=&quot;toc-title&quot; data-ke-size=&quot;size23&quot;&gt;Contents&lt;/h3&gt;
&lt;div class=&quot;toc-grid&quot;&gt;&lt;a class=&quot;toc-link toc-prev&quot; href=&quot;#footer-prev&quot;&gt;&amp;larr; 이전 편&lt;/a&gt; &lt;a class=&quot;toc-link&quot; href=&quot;#ch01&quot;&gt;01. South Outlet &amp;amp; LV Strip&lt;/a&gt; &lt;a class=&quot;toc-link&quot; href=&quot;#ch02&quot;&gt;02. Valley of Fire&lt;/a&gt; &lt;a class=&quot;toc-link&quot; href=&quot;#ch03&quot;&gt;03. Buddy Town Square&lt;/a&gt; &lt;a class=&quot;toc-link&quot; href=&quot;#ch04&quot;&gt;04. Bryce &amp;amp; Antelope &amp;amp; Grand Canyon ⭐&lt;/a&gt; &lt;a class=&quot;toc-link&quot; href=&quot;#ch05&quot;&gt;05. 호버댐 &amp;amp; 수료식&lt;/a&gt; &lt;a class=&quot;toc-link&quot; href=&quot;#ch06&quot;&gt;06. 귀국&lt;/a&gt;&lt;/div&gt;
&lt;/nav&gt;
&lt;section class=&quot;section&quot;&gt;&lt;!-- 01. 주말 South Outlet &amp; LV Strip --&gt;
&lt;article id=&quot;ch01&quot; class=&quot;chapter&quot;&gt;
&lt;div class=&quot;chapter-num&quot;&gt;01&lt;/div&gt;
&lt;h3 class=&quot;chapter-title&quot; data-ke-size=&quot;size23&quot;&gt;South Outlet &amp;amp; LV Strip &lt;span class=&quot;date-tag&quot;&gt;2/14 ~ 2/15&lt;/span&gt;&lt;/h3&gt;
&lt;!-- [사진: South Outlet] --&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;4284&quot; data-origin-height=&quot;5712&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dAUl7w/dJMcaaR21rI/neLYcjEEOrUgZOaHVO6wM1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dAUl7w/dJMcaaR21rI/neLYcjEEOrUgZOaHVO6wM1/img.jpg&quot; data-alt=&quot;South Outlet에 있는 한식집&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dAUl7w/dJMcaaR21rI/neLYcjEEOrUgZOaHVO6wM1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdAUl7w%2FdJMcaaR21rI%2FneLYcjEEOrUgZOaHVO6wM1%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;341&quot; height=&quot;455&quot; data-origin-width=&quot;4284&quot; data-origin-height=&quot;5712&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;South Outlet에 있는 한식집&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;2월 14일과 15일은 두번째 주말이었습니다. 14일에는 North Outlet에 이어 South Outlet을 갔습니다. 아울렛 쇼핑은 돈을 많이 써서 그런지 역시 재밌더라구요ㅎ 그리고 이 날 되게 유명하신 한국 여배우를 Levis매장에서 뵀는데 지인들과 있어서 말은 못걸었지만.. 실물이 장난 아니었습니다 &amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;그리고 점심으로 아울렛에 있는 한식집을 갔는데 사장님이 한국분이어서 주문도 편했고 음식도 고향의 맛을.. 맛보게 된 행복한 시간이었습니다.&lt;/p&gt;
&lt;figure class=&quot;photo-block&quot;&gt;
&lt;figcaption style=&quot;display: none;&quot;&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;!-- [사진: LV Strip 코카콜라/엠앤엠 스토어] --&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1212&quot; data-origin-height=&quot;1518&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bhdPVu/dJMcaadqUQg/ebRfzyzrTvWv3LkzJIWbN1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bhdPVu/dJMcaadqUQg/ebRfzyzrTvWv3LkzJIWbN1/img.png&quot; data-alt=&quot;Strip에 있는 코카콜라 스토어&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bhdPVu/dJMcaadqUQg/ebRfzyzrTvWv3LkzJIWbN1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbhdPVu%2FdJMcaadqUQg%2FebRfzyzrTvWv3LkzJIWbN1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;342&quot; height=&quot;428&quot; data-origin-width=&quot;1212&quot; data-origin-height=&quot;1518&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Strip에 있는 코카콜라 스토어&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;15일에는 LV Strip에 갔습니다. 코카콜라 스토어랑 M&amp;amp;M 스토어도 가고, 롤러코스터도 탔습니다!! 스트립에 있는 롤러코스터 진짜 재밌었어요.(2번 탄건 안비밀) 그리고 유명하다는 와더버거도 먹었는데 감튀랑 같이 찍어먹는 밀크쉐이크가 한국와서도 계속 생각 날 정도로 맛있었습니다...&lt;/p&gt;
&lt;figure class=&quot;photo-block&quot;&gt;
&lt;figcaption style=&quot;display: none;&quot;&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/article&gt;
&lt;!-- 02. Valley of Fire --&gt;
&lt;article id=&quot;ch02&quot; class=&quot;chapter&quot;&gt;
&lt;div class=&quot;chapter-num&quot;&gt;02&lt;/div&gt;
&lt;h3 class=&quot;chapter-title&quot; data-ke-size=&quot;size23&quot;&gt;Valley of Fire &lt;span class=&quot;date-tag&quot;&gt;2/16&lt;/span&gt;&lt;/h3&gt;
&lt;!-- [사진: Valley of Fire] --&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2999&quot; data-origin-height=&quot;2748&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dl9CsL/dJMcahQ8K76/MC1sAoDhweVUPZ96ddMKK0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dl9CsL/dJMcahQ8K76/MC1sAoDhweVUPZ96ddMKK0/img.jpg&quot; data-alt=&quot;valley of fire에서 찍은 사진ㅎ&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dl9CsL/dJMcahQ8K76/MC1sAoDhweVUPZ96ddMKK0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdl9CsL%2FdJMcahQ8K76%2FMC1sAoDhweVUPZ96ddMKK0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;352&quot; height=&quot;323&quot; data-origin-width=&quot;2999&quot; data-origin-height=&quot;2748&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;valley of fire에서 찍은 사진ㅎ&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;2월 16일에는 Presidents' day라서 쉬는 날이라 Valley of Fire에 갔습니다. 붉은 바위와 사막 풍경이 정말 웅장했습니다. 사진 찍기 좋은 곳이 많아서 여기저기 담았던 기억이 나네요. 배경들이 어딜 찍든 이쁘게 다 나와서 나중에 갈 캐니언 트립이 더 기대 되었습니다 ㅎ&lt;/p&gt;
&lt;figure class=&quot;photo-block&quot;&gt;
&lt;figcaption style=&quot;display: none;&quot;&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/article&gt;
&lt;!-- 03. Buddy Town Square --&gt;
&lt;article id=&quot;ch03&quot; class=&quot;chapter&quot;&gt;
&lt;div class=&quot;chapter-num&quot;&gt;03&lt;/div&gt;
&lt;h3 class=&quot;chapter-title&quot; data-ke-size=&quot;size23&quot;&gt;Buddy Program &amp;amp; Town Square &lt;span class=&quot;date-tag&quot;&gt;2/17 ~ 2/20&lt;/span&gt;&lt;/h3&gt;
&lt;!-- [사진: Town Square / Buddy 디너] --&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3023&quot; data-origin-height=&quot;3300&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ooryA/dJMcagdEqBu/r0zieIOojp7tJFlpWWr8NK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ooryA/dJMcagdEqBu/r0zieIOojp7tJFlpWWr8NK/img.jpg&quot; data-alt=&quot;또 다시 한식ㅎ&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ooryA/dJMcagdEqBu/r0zieIOojp7tJFlpWWr8NK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FooryA%2FdJMcagdEqBu%2Fr0zieIOojp7tJFlpWWr8NK%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;371&quot; height=&quot;405&quot; data-origin-width=&quot;3023&quot; data-origin-height=&quot;3300&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;또 다시 한식ㅎ&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;수업은 계속 이어졌고, 2월 18일에는 저녁에 Buddy Program으로 Town Square에 갔습니다. 외국인 친구와 같이 한식도 먹고 쇼핑도 했습니다. 현지 학생들이랑 어울리면서 대화하는 시간이 좋았어요. 이때 나름(?) 영어 회화실력도 향상된 것 같아서 좋았습니다! 이때는 정신이 없어서 먹는것 밖에 못찍었네요..&lt;/p&gt;
&lt;figure class=&quot;photo-block&quot;&gt;
&lt;figcaption style=&quot;display: none;&quot;&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/article&gt;
&lt;!-- 04. Bryce, Horseshoe Bend, Antelope, Grand Canyon --&gt;
&lt;article id=&quot;ch04&quot; class=&quot;chapter chapter-highlight&quot;&gt;
&lt;div class=&quot;chapter-num&quot;&gt;04&lt;/div&gt;
&lt;span class=&quot;chapter-badge&quot;&gt;⭐ 가장 기대되었던 활동!! ⭐️&lt;/span&gt;
&lt;h3 class=&quot;chapter-title&quot; data-ke-size=&quot;size23&quot;&gt;Bryce Canyon &amp;amp; Horseshoe Bend &amp;amp; Antelope Canyon &amp;amp; Grand Canyon&amp;nbsp; &lt;span class=&quot;date-tag&quot;&gt;2/21 ~ 2/22&lt;/span&gt;&lt;/h3&gt;
&lt;!-- [사진: Bryce Canyon - 양옆 배치] --&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;2월 21일에는 Bryce Canyon이랑 Horseshoe Bend를 갔습니다.&lt;/p&gt;
&lt;div class=&quot;photo-duo&quot;&gt;
&lt;div class=&quot;photo-duo-item&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;4284&quot; data-origin-height=&quot;5712&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bWhUsS/dJMcagdEq5C/KfkDKspXhfMXv7IT5mY9gk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bWhUsS/dJMcagdEq5C/KfkDKspXhfMXv7IT5mY9gk/img.jpg&quot; data-alt=&quot;Bryce Canyon 풍경(정상에서 찍은 사진)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bWhUsS/dJMcagdEq5C/KfkDKspXhfMXv7IT5mY9gk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbWhUsS%2FdJMcagdEq5C%2FKfkDKspXhfMXv7IT5mY9gk%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;270&quot; height=&quot;360&quot; data-origin-width=&quot;4284&quot; data-origin-height=&quot;5712&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Bryce Canyon 풍경(정상에서 찍은 사진)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/div&gt;
&lt;div class=&quot;photo-duo-item&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;4284&quot; data-origin-height=&quot;5712&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/U7ezC/dJMcabpS8so/z4EQnKa4keguNC1Qkw7RE1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/U7ezC/dJMcabpS8so/z4EQnKa4keguNC1Qkw7RE1/img.jpg&quot; data-alt=&quot;눈이 너무 많이 왔어요...&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/U7ezC/dJMcabpS8so/z4EQnKa4keguNC1Qkw7RE1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FU7ezC%2FdJMcabpS8so%2Fz4EQnKa4keguNC1Qkw7RE1%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;270&quot; height=&quot;360&quot; data-origin-width=&quot;4284&quot; data-origin-height=&quot;5712&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;눈이 너무 많이 왔어요...&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;Bryce Canyon은 오렌지색 기암괴석이 매우 인상 깊었습니다. 문제는.. 최근에 폭설이 왔다고 해서 눈이 많이 쌓여있어서 등산하는 과정이 매우매우 힘들었습니다. 미끄럽고 옷도 다 젖어서 힘들긴 했지만 주변을 둘러볼때마다 '우와' 소리가 절로 나올 정도로 이쁜 풍경이 펼쳐져있었습니다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;4284&quot; data-origin-height=&quot;5712&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/VqB93/dJMcafsfFHx/OOUILlcs33JvtuESEaYze1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/VqB93/dJMcafsfFHx/OOUILlcs33JvtuESEaYze1/img.jpg&quot; data-alt=&quot;멀리서 보면 말발굽 닮았습니다 정말&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/VqB93/dJMcafsfFHx/OOUILlcs33JvtuESEaYze1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FVqB93%2FdJMcafsfFHx%2FOOUILlcs33JvtuESEaYze1%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;307&quot; height=&quot;409&quot; data-origin-width=&quot;4284&quot; data-origin-height=&quot;5712&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;멀리서 보면 말발굽 닮았습니다 정말&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;Horseshoe Bend는 바로 앞까지 가서 구경해보니 말발굽 모양 강이 정말정말 인상적이었습니다. 근데 여기서 팬스가 없는 부분이 있는데 그런 부분에서 사진을 찍고 싶었지만 차마 무서워서 찍지 못해서 아쉬웠습니다ㅜ&lt;/p&gt;
&lt;figure class=&quot;photo-block&quot;&gt;
&lt;figcaption style=&quot;display: none;&quot;&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;!-- [사진: Antelope Canyon] --&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;2월 22일에는 앤텔로프 캐니언이랑 대망의 그랜드 캐니언을 갔습니다!!&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;4284&quot; data-origin-height=&quot;5712&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c1EGgh/dJMcahwRvgJ/3A2iTJa55OREB9Y5g1U8zk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c1EGgh/dJMcahwRvgJ/3A2iTJa55OREB9Y5g1U8zk/img.jpg&quot; data-alt=&quot;가이드분이 찍어주신 사진(독수리..? 닮았다고 하심)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c1EGgh/dJMcahwRvgJ/3A2iTJa55OREB9Y5g1U8zk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc1EGgh%2FdJMcahwRvgJ%2F3A2iTJa55OREB9Y5g1U8zk%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;278&quot; height=&quot;371&quot; data-origin-width=&quot;4284&quot; data-origin-height=&quot;5712&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;가이드분이 찍어주신 사진(독수리..? 닮았다고 하심)&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;앤텔로프 캐니언은 나바호 족에게 직접 가이드를 받으며 구경했는데 빛이 비치는 계곡 풍경이 너무 예뻤고, 중간중간 암석이 닮은 것들을 포인트로 설명해주고 사진도 찍어주셔서 정말 투어에 온 기분이 들었습니다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;5712&quot; data-origin-height=&quot;4284&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bcL02u/dJMcacbeGTq/BUkeWuVrNhYZzA88G4K5bK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bcL02u/dJMcacbeGTq/BUkeWuVrNhYZzA88G4K5bK/img.jpg&quot; data-alt=&quot;기대하고 기대하던 Grand Canyon&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bcL02u/dJMcacbeGTq/BUkeWuVrNhYZzA88G4K5bK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbcL02u%2FdJMcacbeGTq%2FBUkeWuVrNhYZzA88G4K5bK%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;427&quot; height=&quot;320&quot; data-origin-width=&quot;5712&quot; data-origin-height=&quot;4284&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;기대하고 기대하던 Grand Canyon&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;그리고 그랜드 캐니언은 역시 사진으로만 봤을 때랑 차원이 다르더라구요. 이게 정말로 사진으로 못담는게 한입니다.. 안가보신분들은 살면서 한번쯤은 꼭 봐야할 정도로 아름다운 경관이 펼쳐져있습니다! 제일 유명한 canyon답게 기념품 샵이나 이런것도 되게 잘되어있었고 박물관도 있었습니다.&lt;/p&gt;
&lt;figure class=&quot;photo-block&quot;&gt;
&lt;figcaption style=&quot;display: none;&quot;&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;!-- [사진: 그랜드 캐니언] --&gt;&lt;/article&gt;
&lt;!-- 05. 호버댐 &amp; 수료식 --&gt;
&lt;article id=&quot;ch05&quot; class=&quot;chapter&quot;&gt;
&lt;div class=&quot;chapter-num&quot;&gt;05&lt;/div&gt;
&lt;h3 class=&quot;chapter-title&quot; data-ke-size=&quot;size23&quot;&gt;후버댐 필드트립 &amp;amp; Goodbye Reception &lt;span class=&quot;date-tag&quot;&gt;2/27&lt;/span&gt;&lt;/h3&gt;
&lt;!-- [사진: 호버댐] --&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;4284&quot; data-origin-height=&quot;5712&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cskf5b/dJMcabi7wrp/LyMXd2WJPXC0dATExmeNTK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cskf5b/dJMcabi7wrp/LyMXd2WJPXC0dATExmeNTK/img.jpg&quot; data-alt=&quot;후버댐 안 사진&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cskf5b/dJMcabi7wrp/LyMXd2WJPXC0dATExmeNTK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcskf5b%2FdJMcabi7wrp%2FLyMXd2WJPXC0dATExmeNTK%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;319&quot; height=&quot;425&quot; data-origin-width=&quot;4284&quot; data-origin-height=&quot;5712&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;후버댐 안 사진&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;2월 27일 오전에는 후버댐 필드트립을 갔습니다. 댐이 엄청 크고 역사도 오래됐더라구요. 전망대에서 콜로라도 강이 보이는 풍경을 봤는데, 이 물이 라스베이거스에 전기랑 물을 공급한다고 하니까 신기했습니다. 근데 이때도 너무 높아서 무서워서 제대로 못찍었습니다..&lt;/p&gt;
&lt;figure class=&quot;photo-block&quot;&gt;
&lt;figcaption style=&quot;display: none;&quot;&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;!-- [사진: Goodbye Reception] --&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2028&quot; data-origin-height=&quot;1521&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/65rQ6/dJMcacoLMHq/QuCGpFwNgm5P7nZdIx9exk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/65rQ6/dJMcacoLMHq/QuCGpFwNgm5P7nZdIx9exk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/65rQ6/dJMcacoLMHq/QuCGpFwNgm5P7nZdIx9exk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F65rQ6%2FdJMcacoLMHq%2FQuCGpFwNgm5P7nZdIx9exk%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;574&quot; height=&quot;431&quot; data-origin-width=&quot;2028&quot; data-origin-height=&quot;1521&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;점심에는 Goodbye Reception이 있었어요. 고급 레스토랑에서 식사하면서 한 달 동안 같이한 Buddy, 교수님, 친구들이랑 마지막으로 인사하고 수료증도 받았습니다. &quot;내가 여기를 다시 올 수 있을까?&quot; 하는 생각에 좀 아쉬웠어요ㅜ 그래도 정말 좋은 시간이었습니다ㅎㅎ&lt;/p&gt;
&lt;figure class=&quot;photo-block&quot;&gt;
&lt;figcaption style=&quot;display: none;&quot;&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/article&gt;
&lt;!-- 06. 귀국 --&gt;
&lt;article id=&quot;ch06&quot; class=&quot;chapter chapter-closing&quot;&gt;
&lt;div class=&quot;chapter-num&quot;&gt;06&lt;/div&gt;
&lt;h3 class=&quot;chapter-title&quot; data-ke-size=&quot;size23&quot;&gt;귀국 &lt;span class=&quot;date-tag&quot;&gt;2/28 출발 ~ 3/2 도착&lt;/span&gt;&lt;/h3&gt;
&lt;!-- [사진: 발표 / 카쇼 / 공항] --&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;4032&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/7G1Ut/dJMcafeLujj/DAOvmQOMGlrpko4FPnX091/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/7G1Ut/dJMcafeLujj/DAOvmQOMGlrpko4FPnX091/img.jpg&quot; data-alt=&quot;카쇼(놀랍게도 관객석쪽 사진입니다)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/7G1Ut/dJMcafeLujj/DAOvmQOMGlrpko4FPnX091/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F7G1Ut%2FdJMcafeLujj%2FDAOvmQOMGlrpko4FPnX091%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;212&quot; height=&quot;283&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;4032&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;카쇼(놀랍게도 관객석쪽 사진입니다)&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;2월 28일은 라스베이거스에서의 마지막 날이었어요. 오전에는 우리 프로젝트에 대한 발표를 했고, 점심에는 카쇼(KA by Cirque du Soleil)를 보러 갔습니다. 라스베이거스 마지막으로 꼭 보고 싶었던 쇼였는데 진짜 비현실적으로 이뻐서 넋 놓고 구경했습니다!&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;4283&quot; data-origin-height=&quot;3674&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dHfxfV/dJMcaiJftB8/OJDF9Cm7gbOKAi7CeUSMW0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dHfxfV/dJMcaiJftB8/OJDF9Cm7gbOKAi7CeUSMW0/img.jpg&quot; data-alt=&quot;마지막 공항에서 굿바이 사진ㅜ&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dHfxfV/dJMcaiJftB8/OJDF9Cm7gbOKAi7CeUSMW0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdHfxfV%2FdJMcaiJftB8%2FOJDF9Cm7gbOKAi7CeUSMW0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;391&quot; height=&quot;335&quot; data-origin-width=&quot;4283&quot; data-origin-height=&quot;3674&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;마지막 공항에서 굿바이 사진ㅜ&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;저녁에 비행기를 타고 한국으로 출발했습니다. 2월 28일 출발해서 3월 2일에 인천에 도착했어요. 비행기 창밖으로 라스베이거스가 점점 멀어지는 걸 보면서 한 달이 스쳐 지나갔다는 게 실감 났어요. 라스베이거스는 단순한 관광지가 아니라, 한 달간 살아본 집 같은 느낌이었습니다.&lt;/p&gt;
&lt;figure class=&quot;photo-block&quot;&gt;
&lt;figcaption style=&quot;display: none;&quot;&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;div class=&quot;callout callout-ending&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;미국 여행 마무리!&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;&quot;인천에서 출발해 라스베이거스에서 한 달을 살아보고, Valley of Fire, Bryce, 앤텔로프, 그랜드 캐니언, 호버댐, 카쇼까지. 다시 기회가 된다면 진심으로 꼭 또 가보고 싶어요..&quot;&lt;/i&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/article&gt;
&lt;/section&gt;
&lt;footer id=&quot;footer-prev&quot; class=&quot;blog-footer&quot;&gt;&lt;a class=&quot;part-link prev-link&quot; href=&quot;https://smp0417.tistory.com/78&quot;&gt;&amp;larr; 이전: 상편 (1편) 읽기&lt;/a&gt;&lt;/footer&gt;&lt;/div&gt;
&lt;div&gt;
&lt;style&gt;
    #travel-blog-root { font-family: 'Pretendard', -apple-system, BlinkMacSystemFont, 'Malgun Gothic', sans-serif; max-width: 720px; margin: 0 auto; padding: 40px 24px 60px; color: #2c2c2c; line-height: 1.75; }
    .blog-header { text-align: center; margin-bottom: 32px; }
    .blog-tag { display: inline-block; font-size: 12px; font-weight: 600; letter-spacing: 0.1em; color: #c17f59; text-transform: uppercase; margin-bottom: 12px; }
    .blog-title { font-size: 28px; font-weight: 800; color: #1a1a1a; margin: 0 0 12px; line-height: 1.35; }
    .blog-subtitle { font-size: 15px; color: #6b6b6b; margin: 0 0 20px; }
    .blog-meta { font-size: 13px; color: #8a8a8a; display: flex; gap: 20px; justify-content: center; flex-wrap: wrap; }
    
    .photo-block { margin: 24px 0; }
    .photo-duo { display: flex; gap: 12px; margin: 20px 0; justify-content: center; align-items: flex-start; flex-wrap: wrap; clear: both; }
    .photo-duo .photo-duo-item { flex: 1; min-width: 200px; max-width: 50%; }
    .photo-duo .photo-duo-item img { width: 100%; height: auto; display: block; }
    
    .toc-nav { background: linear-gradient(135deg, #faf8f5 0%, #f5f0ea 100%); border: 1px solid #e8e2db; border-radius: 12px; padding: 20px 24px; margin-bottom: 40px; }
    .toc-title { font-size: 13px; font-weight: 700; color: #8a7a6a; margin: 0 0 14px; letter-spacing: 0.05em; }
    .toc-grid { display: flex; flex-wrap: wrap; gap: 10px; }
    .toc-link { display: inline-block; padding: 8px 14px; background: #fff; border: 1px solid #e0d9d0; border-radius: 8px; font-size: 14px; color: #4a4a4a; text-decoration: none; transition: all 0.2s; }
    .toc-link:hover { background: #c17f59; color: #fff; border-color: #c17f59; }
    .toc-prev { background: #f5f0ea !important; }
    .toc-next { background: #c17f59 !important; color: #fff !important; border-color: #c17f59 !important; }
    
    .section { margin-bottom: 48px; }
    .chapter { background: #fff; border: 1px solid #ebe6e0; border-radius: 12px; padding: 24px 28px; margin-bottom: 20px; }
    .chapter-highlight { border-color: #d4b896; box-shadow: 0 4px 16px rgba(193, 127, 89, 0.08); }
    .chapter-num { font-size: 32px; font-weight: 200; color: #e0d5cc; margin-bottom: -8px; }
    .chapter-badge { display: inline-block; font-size: 11px; background: #c17f59; color: #fff; padding: 3px 10px; border-radius: 20px; margin-left: 8px; }
    .chapter-title { font-size: 18px; font-weight: 700; color: #1a1a1a; margin: 0 0 14px; }
    .date-tag { font-size: 13px; font-weight: 500; color: #8a7a6a; }
    .para { margin: 0 0 16px; color: #3d3d3d; line-height: 1.85; }
    .para:last-of-type { margin-bottom: 0; }
    .callout { margin-top: 24px; padding: 20px 24px; border-radius: 10px; }
    .callout-teaser { background: linear-gradient(135deg, #faf5f0 0%, #f5ebe3 100%); border: 1px solid #e8d9cc; }
    .callout-ending { background: linear-gradient(135deg, #f0f5fa 0%, #e8eef5 100%); border: 1px solid #d0dce8; }
    .callout p { margin: 0 0 8px; font-size: 15px; }
    .callout p:last-child { margin-bottom: 0; }
    .teaser-next { font-weight: 600; color: #c17f59; margin-top: 12px !important; }
    
    .blog-footer { text-align: center; padding: 40px 0; border-top: 1px solid #e8e2db; }
    .part-link { display: inline-block; padding: 14px 28px; background: #c17f59; color: #fff !important; font-size: 16px; font-weight: 600; text-decoration: none; border-radius: 10px; transition: opacity 0.2s; }
    .part-link:hover { opacity: 0.9; text-decoration: none; }
    &lt;/style&gt;
&lt;/div&gt;</description>
      <category>UNLV</category>
      <author>빡성</author>
      <guid isPermaLink="true">https://smp0417.tistory.com/79</guid>
      <comments>https://smp0417.tistory.com/79#entry79comment</comments>
      <pubDate>Tue, 10 Mar 2026 16:49:39 +0900</pubDate>
    </item>
    <item>
      <title>[UNLV]   Las Vegas에서 한 달 여행기 - 상편</title>
      <link>https://smp0417.tistory.com/78</link>
      <description>&lt;div id=&quot;travel-blog-root&quot;&gt;&lt;header class=&quot;blog-header&quot;&gt;&lt;span class=&quot;blog-tag&quot;&gt;Travel Journal &amp;middot; 1편&lt;/span&gt;
&lt;p class=&quot;blog-subtitle&quot; data-ke-size=&quot;size16&quot;&gt;인천에서 라스베가스까지, 그리고 설레는 첫 2주&lt;/p&gt;
&lt;div class=&quot;blog-meta&quot;&gt;&lt;span&gt;  2025.02.01 ~ 02.13&lt;/span&gt; &lt;span&gt;  Incheon &amp;rarr; Las Vegas &amp;rarr; UNLV&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;blog-meta&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;/header&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #2c2c2c; text-align: start;&quot;&gt;글로벌 AI 인재 트랙 참여로 라스베가스에 가게 되었습니다!&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #2c2c2c; text-align: start;&quot;&gt;너무너무 재밌게 다녀와서 짧지만 한달간의 여정을 기록해보려 합니다ㅎㅎ&lt;/span&gt;&lt;/p&gt;
&lt;!-- 대표 이미지: 인천 공항 여권 3명 --&gt;&lt;nav class=&quot;toc-nav&quot;&gt;
&lt;h3 class=&quot;toc-title&quot; data-ke-size=&quot;size23&quot;&gt;Contents&lt;/h3&gt;
&lt;div class=&quot;toc-grid&quot;&gt;&lt;a class=&quot;toc-link&quot; href=&quot;#ch01&quot;&gt;01. 인천 &amp;rarr; 라스베가스&lt;/a&gt; &lt;a class=&quot;toc-link&quot; href=&quot;#ch02&quot;&gt;02. 호텔 &amp;amp; 첫날&lt;/a&gt; &lt;a class=&quot;toc-link&quot; href=&quot;#ch03&quot;&gt;03. UNLV 첫 등교&lt;/a&gt; &lt;a class=&quot;toc-link&quot; href=&quot;#ch04&quot;&gt;04. 분수쇼 &amp;amp; Fremont&lt;/a&gt; &lt;a class=&quot;toc-link&quot; href=&quot;#ch05&quot;&gt;05. 수업 &amp;amp; Buddy&lt;/a&gt; &lt;a class=&quot;toc-link&quot; href=&quot;#ch06&quot;&gt;06. 첫 주말&lt;/a&gt; &lt;a class=&quot;toc-link&quot; href=&quot;#ch07&quot;&gt;07. 농구 &amp;amp; MJ ONE&lt;/a&gt; &lt;a class=&quot;toc-link toc-next&quot; href=&quot;#footer-next&quot;&gt;다음 편 &amp;rarr;&lt;/a&gt;&lt;/div&gt;
&lt;/nav&gt;
&lt;section class=&quot;section&quot;&gt;&lt;!-- 01. 오프닝 + 출국 --&gt;
&lt;article id=&quot;ch01&quot; class=&quot;chapter&quot;&gt;
&lt;div class=&quot;chapter-num&quot;&gt;01&lt;/div&gt;
&lt;h3 class=&quot;chapter-title&quot; data-ke-size=&quot;size23&quot;&gt;인천 &amp;rarr; 라스베가스 출발 &lt;span class=&quot;date-tag&quot;&gt;2/1&lt;/span&gt;&lt;/h3&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;4032&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nnz73/dJMcadnDC0v/btiK7c6oZgle7mKQRCnD6k/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nnz73/dJMcadnDC0v/btiK7c6oZgle7mKQRCnD6k/img.jpg&quot; data-alt=&quot;인천 -&amp;amp;gt; 라스베가스 ✈️&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nnz73/dJMcadnDC0v/btiK7c6oZgle7mKQRCnD6k/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fnnz73%2FdJMcadnDC0v%2FbtiK7c6oZgle7mKQRCnD6k%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;243&quot; height=&quot;324&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;4032&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;인천 -&amp;gt; 라스베가스 ✈️&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;인천공항에 도착하니 이제서야 한국을 떠난다는게 실감이 났습니다.. 장시간 비행기를 타는것도 처음이라 걱정도 됐지만 설레는 마음을 안고 출발했습니다!&lt;/p&gt;
&lt;/article&gt;
&lt;!-- 02. 도착, 호텔 체크인 --&gt;
&lt;article id=&quot;ch02&quot; class=&quot;chapter&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div class=&quot;chapter-num&quot;&gt;02&lt;/div&gt;
&lt;h3 class=&quot;chapter-title&quot; data-ke-size=&quot;size23&quot;&gt;라스베이거스 도착 &amp;amp; 호텔 체크인 &lt;span class=&quot;date-tag&quot;&gt;2/1&lt;/span&gt;&lt;/h3&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3790&quot; data-origin-height=&quot;2885&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lFv35/dJMcajnOPiR/CnNO53799dgMiMTVZwzKL1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lFv35/dJMcajnOPiR/CnNO53799dgMiMTVZwzKL1/img.jpg&quot; data-alt=&quot;스피어 뷰 짱!!&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lFv35/dJMcajnOPiR/CnNO53799dgMiMTVZwzKL1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlFv35%2FdJMcajnOPiR%2FCnNO53799dgMiMTVZwzKL1%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;381&quot; data-origin-width=&quot;3790&quot; data-origin-height=&quot;2885&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;스피어 뷰 짱!!&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;비행기가 내려앉으면서 창밖에 사막과 네온이 보이기 시작했습니다. 밖에 풍경을 보면서 이게 미국이구나.. 싶은 생각이 들면서 호텔에 도착하여 짐을 버스에서 내리는 도중에 호텔 뒷편을 바라보았는데 스피어가 보이면서 풍경이 너무너무 이뻐서 여기서 한 달을 산다는 생각에 잠깐 멍해졌습니다.&lt;/p&gt;
&lt;figure class=&quot;photo-block&quot;&gt;
&lt;figcaption style=&quot;display: none;&quot;&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/article&gt;
&lt;!-- 03. UNLV 첫 등교, 캠퍼스, 월마트 --&gt;
&lt;article id=&quot;ch03&quot; class=&quot;chapter&quot;&gt;
&lt;div class=&quot;chapter-num&quot;&gt;03&lt;/div&gt;
&lt;h3 class=&quot;chapter-title&quot; data-ke-size=&quot;size23&quot;&gt;설레는 UNLV 첫 등교 &amp;amp; 캠퍼스 투어 &lt;span class=&quot;date-tag&quot;&gt;2/2&lt;/span&gt;&lt;/h3&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;4284&quot; data-origin-height=&quot;5712&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dRNRwL/dJMcabwCbnX/TtgUBcls3rLenGJJ69lw90/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dRNRwL/dJMcabwCbnX/TtgUBcls3rLenGJJ69lw90/img.jpg&quot; data-alt=&quot;UNLV캠퍼스 내 포토존(?)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dRNRwL/dJMcabwCbnX/TtgUBcls3rLenGJJ69lw90/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdRNRwL%2FdJMcabwCbnX%2FTtgUBcls3rLenGJJ69lw90%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;314&quot; height=&quot;419&quot; data-origin-width=&quot;4284&quot; data-origin-height=&quot;5712&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;UNLV캠퍼스 내 포토존(?)&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;다음 날, UNLV에 처음으로 등교해서 오리엔테이션과 캠퍼스 투어를 했는데, 미국 대학 캠퍼스는 처음이어서 모든 게 새로웠고 해외에서 공부한다는 설렘이 생겼습니다. 특히, 건물들 구조들이 신기하게 되어있었고 넓은 잔디들도 많았고 식당도 좋았습니다.&lt;/p&gt;
&lt;figure class=&quot;photo-block&quot;&gt;
&lt;figcaption style=&quot;display: none;&quot;&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;4284&quot; data-origin-height=&quot;5712&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/PkIAc/dJMcaaYMjWE/OxWzu8QhJk8OVR3gJktDzK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/PkIAc/dJMcaaYMjWE/OxWzu8QhJk8OVR3gJktDzK/img.jpg&quot; data-alt=&quot;월마트에서 첫 장보기&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/PkIAc/dJMcaaYMjWE/OxWzu8QhJk8OVR3gJktDzK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPkIAc%2FdJMcaaYMjWE%2FOxWzu8QhJk8OVR3gJktDzK%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;358&quot; height=&quot;477&quot; data-origin-width=&quot;4284&quot; data-origin-height=&quot;5712&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;월마트에서 첫 장보기&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;캠퍼스 투어가 끝나고 버스를 타고 이동한 후 월마트에 갔습니다. 미국에서 생활용품을 사려면 여기서 사야한다는 말이 많았는데 역시나 없는게 없었습니다(이때, 여기서 더 많은 생필품을 샀어야하는데...)&lt;/p&gt;
&lt;figure class=&quot;photo-block&quot;&gt;
&lt;figcaption style=&quot;display: none;&quot;&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/article&gt;
&lt;!-- 04. LV Strip &amp; Fremont --&gt;
&lt;article id=&quot;ch04&quot; class=&quot;chapter&quot;&gt;
&lt;div class=&quot;chapter-num&quot;&gt;04&lt;/div&gt;
&lt;h3 class=&quot;chapter-title&quot; data-ke-size=&quot;size23&quot;&gt;분수쇼 &amp;amp; Fremont Street &lt;span class=&quot;date-tag&quot;&gt;2/2 저녁&lt;/span&gt;&lt;/h3&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1151&quot; data-origin-height=&quot;2086&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Ol4j7/dJMcaa5yQll/qd24nhBQDcciM6SgP5JaF1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Ol4j7/dJMcaa5yQll/qd24nhBQDcciM6SgP5JaF1/img.jpg&quot; data-alt=&quot;분수쇼(사진으로 안담기는게 아숩..)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Ol4j7/dJMcaa5yQll/qd24nhBQDcciM6SgP5JaF1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOl4j7%2FdJMcaa5yQll%2Fqd24nhBQDcciM6SgP5JaF1%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;235&quot; height=&quot;426&quot; data-origin-width=&quot;1151&quot; data-origin-height=&quot;2086&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;분수쇼(사진으로 안담기는게 아숩..)&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;저녁을 먹고 나서 바로 분수쇼를 보러 갔습니다. 미국 오자마자 스케줄이 아주 빡세더군요..&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;물론 좋았습니다ㅎ 너무 아름다웠어요!! 봐도 또 보고싶은 느낌이었습니다.&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;음악에 맞춰 치솟는 물줄기, 주변 호텔과 네온, 영어로 말하는 사람들.. 등등 조화가 완벽했습니다.&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&quot;여행을 왔구나&quot;라는 느낌이 이때 확 온거같아요.&lt;/p&gt;
&lt;figure class=&quot;photo-block&quot;&gt;
&lt;figcaption style=&quot;display: none;&quot;&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;4032&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dMyKix/dJMcadnDI4P/RfbpmArePWmlFmwdKYFGak/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dMyKix/dJMcadnDI4P/RfbpmArePWmlFmwdKYFGak/img.jpg&quot; data-alt=&quot;Fremont Street&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dMyKix/dJMcadnDI4P/RfbpmArePWmlFmwdKYFGak/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdMyKix%2FdJMcadnDI4P%2FRfbpmArePWmlFmwdKYFGak%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;274&quot; height=&quot;365&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;4032&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Fremont Street&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;이어서 Fremont Street로 이동했는데요, 분명 대전에서 이런 느낌의 거리를 본 적이 있는거같은.. 생각이 들긴 했습니다. 위에는 짚라인 같은 걸 타는 사람들이 보였고 저희 학교 사람들도 몇명 타러갔지만 저는 타러가지않았습니다ㅜ 그래도 분위기는 제대로 즐긴거같아요! 이곳저곳 가게도 보고, 사람 구경도 하고, 공연같은것도 보면서 Vegas의 밤거리를 걸어다녔습니다.&lt;/p&gt;
&lt;figure class=&quot;photo-block&quot;&gt;
&lt;figcaption style=&quot;display: none;&quot;&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/article&gt;
&lt;!-- 05. 2/3~2/6 수업, Buddy 볼링, 헬스장, 농구 --&gt;
&lt;article id=&quot;ch05&quot; class=&quot;chapter&quot;&gt;
&lt;div class=&quot;chapter-num&quot;&gt;05&lt;/div&gt;
&lt;h3 class=&quot;chapter-title&quot; data-ke-size=&quot;size23&quot;&gt;본격 수업 &amp;amp; Buddy Program &lt;span class=&quot;date-tag&quot;&gt;2/3 ~ 2/6&lt;/span&gt;&lt;/h3&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;4032&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bcrtME/dJMcajuDhWc/B3R0OdJUSkvMdCg9B1BjU1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bcrtME/dJMcajuDhWc/B3R0OdJUSkvMdCg9B1BjU1/img.jpg&quot; data-alt=&quot;OT수업(하루 수업 4시간 개꿀)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bcrtME/dJMcajuDhWc/B3R0OdJUSkvMdCg9B1BjU1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbcrtME%2FdJMcajuDhWc%2FB3R0OdJUSkvMdCg9B1BjU1%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;301&quot; height=&quot;401&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;4032&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;OT수업(하루 수업 4시간 개꿀)&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;이제 본격적으로 학교에 가서 수업을 들었습니다. 정말 설렜어요! &lt;s&gt;처음에는 분명 수업이 설렜었죠...&amp;nbsp;&lt;/s&gt;&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;수업실에 앉아 강의를 듣는데 역시 영어로 수업듣는게 쉽지 않았습니다. 전문 용어들이 숨 돌릴 틈도 없이 나왔기 때문에 한국어로 번역하는데만 뇌 용량을 다 쓴 것 같았어요ㅜ&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;그래도 한국에서 배운 적 없는 임베디드 내용도 배울 수 있다는게 기대가 됐습니다!&lt;/p&gt;
&lt;figure class=&quot;photo-block&quot;&gt;
&lt;figcaption style=&quot;display: none;&quot;&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;4284&quot; data-origin-height=&quot;5712&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9rHab/dJMcaiJfqQE/w7tIS49PFTslv8bWiJMMgK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9rHab/dJMcaiJfqQE/w7tIS49PFTslv8bWiJMMgK/img.jpg&quot; data-alt=&quot;호텔에 있는 볼링장&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9rHab/dJMcaiJfqQE/w7tIS49PFTslv8bWiJMMgK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9rHab%2FdJMcaiJfqQE%2Fw7tIS49PFTslv8bWiJMMgK%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;323&quot; height=&quot;431&quot; data-origin-width=&quot;4284&quot; data-origin-height=&quot;5712&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;호텔에 있는 볼링장&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;수업 이후 저녁시간에 진행한 Buddy Program 덕분에 외국인 친구들과도 어울릴 수 있었습니다. 이때, 처음 안 사실이 이 프로그램을 같이 참여하는 외국인들이 처음엔 마냥 가이드인줄 알았는데, 모두 여기 UNLV에 다니는 대학생들이었습니다. (놀랍게도 우리보다 나이가 엄청 어렸다..) 볼링을 치러 갔을 때가 특히 기억이 나는데 이때를 기점으로 팀전으로 점수내기도 하면서 서로 웃고 떠들며 친해지게 되는 시간이었습니다.&lt;/p&gt;
&lt;figure class=&quot;photo-block&quot;&gt;
&lt;figcaption style=&quot;display: none;&quot;&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;5712&quot; data-origin-height=&quot;4284&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/7SZ63/dJMcabi7sUZ/URUW0qHfoZI6rmYM6UKDwK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/7SZ63/dJMcabi7sUZ/URUW0qHfoZI6rmYM6UKDwK/img.jpg&quot; data-alt=&quot;피지컬 쩌는 형님들과 농구&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/7SZ63/dJMcabi7sUZ/URUW0qHfoZI6rmYM6UKDwK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F7SZ63%2FdJMcabi7sUZ%2FURUW0qHfoZI6rmYM6UKDwK%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;485&quot; height=&quot;364&quot; data-origin-width=&quot;5712&quot; data-origin-height=&quot;4284&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;피지컬 쩌는 형님들과 농구&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;수업이 끝나면 거의 매일 헬스장에도 가고 농구도 했는데 헬스장 시설이 너무너무 좋았고, 오랜만에 농구도 하며 학업 스트레스를 덜 수 있는 시간이었습니다. 그리고 외국인 형님들(?)과 같이 농구 경기도 했습니다ㅎ&lt;/p&gt;
&lt;figure class=&quot;photo-block&quot;&gt;
&lt;figcaption style=&quot;display: none;&quot;&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;photo-block&quot;&gt;
&lt;figcaption style=&quot;display: none;&quot;&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/article&gt;
&lt;!-- 06. 2/7~2/8 첫 주말 - North Outlet, 수영장 --&gt;
&lt;article id=&quot;ch06&quot; class=&quot;chapter&quot;&gt;
&lt;div class=&quot;chapter-num&quot;&gt;06&lt;/div&gt;
&lt;h3 class=&quot;chapter-title&quot; data-ke-size=&quot;size23&quot;&gt;첫 주말: North Outlet &amp;amp; 호텔 수영장 &lt;span class=&quot;date-tag&quot;&gt;2/7 ~ 2/8&lt;/span&gt;&lt;/h3&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;4284&quot; data-origin-height=&quot;5712&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GI8Cm/dJMcaivIxKy/wxAkrKzV4WpNDNmOyRn9x1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GI8Cm/dJMcaivIxKy/wxAkrKzV4WpNDNmOyRn9x1/img.jpg&quot; data-alt=&quot;아울렛에서 한바탕 한 모습..&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GI8Cm/dJMcaivIxKy/wxAkrKzV4WpNDNmOyRn9x1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGI8Cm%2FdJMcaivIxKy%2FwxAkrKzV4WpNDNmOyRn9x1%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;285&quot; height=&quot;380&quot; data-origin-width=&quot;4284&quot; data-origin-height=&quot;5712&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;아울렛에서 한바탕 한 모습..&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;드디어 기다리고 기다리던 첫 주말!!!!! 아침 일찍부터 일어나서 옹기종기 모여 바로 North Outlet에 쇼핑하러 갔습니다. 브랜드도 많고 할인도 되고, 미국 아울렛이 처음이라 둘러보는 재미가 쏠쏠했습니다. 그중에서 맘에 드는 선글라스도 하나 구매하였는데 햇빛이 너무 쎄서 선글라스 없이는 견딜 수가 없더라고요.. 그래서인지 길을 다니다보면, 선글라스를 안끼고 다니는 미국인들이 거의 없을 정도였습니다.&lt;/p&gt;
&lt;figure class=&quot;photo-block&quot;&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;4284&quot; data-origin-height=&quot;5712&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/o4q9i/dJMcacCjz26/Xqw2NgV5O2qqsfT0bdKo3k/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/o4q9i/dJMcacCjz26/Xqw2NgV5O2qqsfT0bdKo3k/img.jpg&quot; data-alt=&quot;캬&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/o4q9i/dJMcacCjz26/Xqw2NgV5O2qqsfT0bdKo3k/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fo4q9i%2FdJMcacCjz26%2FXqw2NgV5O2qqsfT0bdKo3k%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;328&quot; height=&quot;437&quot; data-origin-width=&quot;4284&quot; data-origin-height=&quot;5712&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;캬&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;아울렛을 다녀온 뒤에는 호텔 수영장에서 햇살 좋은 날, 수영장에 누워 여유를 만끽했습니다. 평일엔 바쁘지만 주말에는 이렇게 쉬는 시간이 있어서 워라벨이 완벽했던 것 같아요ㅎㅎ&lt;/p&gt;
&lt;figure class=&quot;photo-block&quot;&gt;&lt;/figure&gt;
&lt;/article&gt;
&lt;!-- 07. 2/9~2/13 드론 수업, 농구 경기, MJ ONE --&gt;
&lt;article id=&quot;ch07&quot; class=&quot;chapter chapter-closing&quot;&gt;
&lt;div class=&quot;chapter-num&quot;&gt;07&lt;/div&gt;
&lt;h3 class=&quot;chapter-title&quot; data-ke-size=&quot;size23&quot;&gt;농구 경기, 그리고 Michael Jackson ONE &lt;span class=&quot;date-tag&quot;&gt;2/9 ~ 2/13&lt;/span&gt;&lt;/h3&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1992&quot; data-origin-height=&quot;1022&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/NfDTs/dJMcadunPQe/McCJ6G8tm4A4dzeZw9A2Xk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/NfDTs/dJMcadunPQe/McCJ6G8tm4A4dzeZw9A2Xk/img.png&quot; data-alt=&quot;드론으로 촬영한 내모습&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/NfDTs/dJMcadunPQe/McCJ6G8tm4A4dzeZw9A2Xk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNfDTs%2FdJMcadunPQe%2FMcCJ6G8tm4A4dzeZw9A2Xk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;543&quot; height=&quot;279&quot; data-origin-width=&quot;1992&quot; data-origin-height=&quot;1022&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;드론으로 촬영한 내모습&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;2월 9일, 수업이 다시 시작됐습니다.. 그래도 이번주에는 드론 수업을 했는데 드론 조종 원리부터 실제 비행까지, 처음 배우는 내용이어서 재밌었습니다. 특히, 매번 실습할때마다 느꼈던건데 장비들이 삐까뻔쩍해보여서 매우 좋았습니다.&lt;/p&gt;
&lt;figure class=&quot;photo-block&quot;&gt;
&lt;figcaption style=&quot;display: none;&quot;&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;4032&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/WdCke/dJMcabci2ZF/SxmuZz7O69gFIyVc5KRIVk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/WdCke/dJMcabci2ZF/SxmuZz7O69gFIyVc5KRIVk/img.jpg&quot; data-alt=&quot;UNLV 농구경기&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/WdCke/dJMcabci2ZF/SxmuZz7O69gFIyVc5KRIVk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FWdCke%2FdJMcabci2ZF%2FSxmuZz7O69gFIyVc5KRIVk%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;356&quot; height=&quot;475&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;4032&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;UNLV 농구경기&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;2월 10일에는 UNLV 농구 경기를 관람하러 갔습니다. 타대학과의 대학농구 경기였는데 경기장이 대학교 안에 있는 것임에도 불구하고 스타디움처럼 엄청나게 컸던것과 관중석에서 응원하는 사람들이 많았다는게 되게 신기했습니다. 그리고 덩크를 밥먹듯이 하는것을 보고 유전자의 차이도 느낄 수 있었습니다..&lt;/p&gt;
&lt;figure class=&quot;photo-block&quot;&gt;
&lt;figcaption style=&quot;display: none;&quot;&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;4032&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pAXDj/dJMcadunP4h/tsN7gpgVr3B4BJW0Gx2h3k/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pAXDj/dJMcadunP4h/tsN7gpgVr3B4BJW0Gx2h3k/img.jpg&quot; data-alt=&quot;Michael Jackson ONE공연&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pAXDj/dJMcadunP4h/tsN7gpgVr3B4BJW0Gx2h3k/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpAXDj%2FdJMcadunP4h%2FtsN7gpgVr3B4BJW0Gx2h3k%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;292&quot; height=&quot;389&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;4032&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Michael Jackson ONE공연&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;2월 13일. Michael Jackson ONE 공연을 봤습니다. 라스베가스가 유명한 쇼가 많다고 했는데 그 중 하나가 이거였나 생각하면서 봤던 것 같습니다. 무대 위에선 마이클 잭슨의 음악과 퍼포먼스가 살아 있었고, 공연장을 앞에만 쓰는 것이 아니라 관객석 쪽도 공연장으로 쓰는게 되게 좋았습니다. 그러나 기대를 많이해서 그런지 엄~청 재밌었다 까진 아니고 볼만 하다 정도였던 것 같습니다.&amp;nbsp;&lt;/p&gt;
&lt;figure class=&quot;photo-block&quot;&gt;
&lt;figcaption style=&quot;display: none;&quot;&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;div class=&quot;callout callout-teaser&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;상편 마무리&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;&quot;인천에서 출발해 라스베가스에 도착하고, UNLV 첫 등교부터 첫 주말, 농구 경기와 MJ ONE까지. 이제 워크숍이 벌써 절반이 지났네요. 후편에선 대망의 그랜드캐니언으로 뵙겠습니다.&quot;&lt;/i&gt;&lt;/p&gt;
&lt;p class=&quot;teaser-next&quot; data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 하편 예고: 그랜드캐니언, 호버댐, 수료식, 귀국&lt;/p&gt;
&lt;/div&gt;
&lt;/article&gt;
&lt;/section&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;footer id=&quot;footer-next&quot; class=&quot;blog-footer&quot;&gt;&lt;a class=&quot;part-link next-link&quot; href=&quot;https://smp0417.tistory.com/79&quot;&gt;다음: 하편 (2편) 읽기 &amp;rarr;&lt;/a&gt;&lt;/footer&gt;&lt;/div&gt;
&lt;div&gt;
&lt;style&gt;
#travel-blog-root { font-family: 'Pretendard', -apple-system, BlinkMacSystemFont, 'Malgun Gothic', sans-serif; max-width: 720px; margin: 0 auto; padding: 40px 24px 60px; color: #2c2c2c; line-height: 1.75; }
.blog-header { text-align: center; margin-bottom: 32px; }
.blog-tag { display: inline-block; font-size: 12px; font-weight: 600; letter-spacing: 0.1em; color: #c17f59; text-transform: uppercase; margin-bottom: 12px; }
.blog-title { font-size: 28px; font-weight: 800; color: #1a1a1a; margin: 0 0 12px; line-height: 1.35; }
.blog-subtitle { font-size: 15px; color: #6b6b6b; margin: 0 0 20px; }
.blog-meta { font-size: 13px; color: #8a8a8a; display: flex; gap: 20px; justify-content: center; flex-wrap: wrap; }

.photo-hero { margin: 0 0 40px; }
.photo-hero .photo-placeholder { aspect-ratio: 16/9; }
.photo-block { margin: 24px 0; }
.photo-placeholder { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 12px; min-height: 200px; background: linear-gradient(135deg, #f8f4f0 0%, #efe8e0 100%); border: 2px dashed #d4c4b0; border-radius: 12px; color: #8a7a6a; }
.photo-hero .photo-placeholder { min-height: 280px; }
.photo-icon { font-size: 48px; opacity: 0.6; }
.photo-label { font-size: 14px; font-weight: 500; text-align: center; padding: 0 16px; }
.photo-block figcaption { font-size: 13px; color: #6b6b6b; margin-top: 10px; padding: 0 4px; line-height: 1.6; }

.toc-nav { background: linear-gradient(135deg, #faf8f5 0%, #f5f0ea 100%); border: 1px solid #e8e2db; border-radius: 12px; padding: 20px 24px; margin-bottom: 40px; }
.toc-title { font-size: 13px; font-weight: 700; color: #8a7a6a; margin: 0 0 14px; letter-spacing: 0.05em; }
.toc-grid { display: flex; flex-wrap: wrap; gap: 10px; }
.toc-link { display: inline-block; padding: 8px 14px; background: #fff; border: 1px solid #e0d9d0; border-radius: 8px; font-size: 14px; color: #4a4a4a; text-decoration: none; transition: all 0.2s; }
.toc-link:hover { background: #c17f59; color: #fff; border-color: #c17f59; }
.toc-next { background: #c17f59 !important; color: #fff !important; border-color: #c17f59 !important; }

.section { margin-bottom: 48px; }
.chapter { background: #fff; border: 1px solid #ebe6e0; border-radius: 12px; padding: 24px 28px; margin-bottom: 20px; }
.chapter-num { font-size: 32px; font-weight: 200; color: #e0d5cc; margin-bottom: -8px; }
.chapter-title { font-size: 18px; font-weight: 700; color: #1a1a1a; margin: 0 0 14px; }
.date-tag { font-size: 13px; font-weight: 500; color: #8a7a6a; }
.para { margin: 0 0 16px; color: #3d3d3d; line-height: 1.85; }
.para:last-of-type { margin-bottom: 0; }
.callout { margin-top: 24px; padding: 20px 24px; border-radius: 10px; }
.callout-teaser { background: linear-gradient(135deg, #faf5f0 0%, #f5ebe3 100%); border: 1px solid #e8d9cc; }
.callout p { margin: 0 0 8px; font-size: 15px; }
.callout p:last-child { margin-bottom: 0; }
.teaser-next { font-weight: 600; color: #c17f59; margin-top: 12px !important; }

.blog-footer { text-align: center; padding: 40px 0; border-top: 1px solid #e8e2db; }
.footer-note { font-size: 12px; color: #8a8a8a; margin-bottom: 12px; }
.footer-note code { background: #f0ede8; padding: 2px 6px; border-radius: 4px; font-size: 11px; }
.part-link { display: inline-block; padding: 14px 28px; background: #c17f59; color: #fff !important; font-size: 16px; font-weight: 600; text-decoration: none; border-radius: 10px; transition: opacity 0.2s; }
.part-link:hover { opacity: 0.9; text-decoration: none; }
&lt;/style&gt;
&lt;/div&gt;</description>
      <category>UNLV</category>
      <author>빡성</author>
      <guid isPermaLink="true">https://smp0417.tistory.com/78</guid>
      <comments>https://smp0417.tistory.com/78#entry78comment</comments>
      <pubDate>Tue, 10 Mar 2026 16:47:27 +0900</pubDate>
    </item>
    <item>
      <title>[UNLV] - 온라인 머신러닝과 이상 탐지: 이론과 진동 센서 실습 (13일차)</title>
      <link>https://smp0417.tistory.com/77</link>
      <description>&lt;!-- ========================= --&gt;
&lt;div id=&quot;post-root&quot;&gt;
&lt;div class=&quot;toc&quot;&gt;
&lt;div style=&quot;font-weight: 800; margin-bottom: 8px;&quot;&gt;&amp;nbsp; &amp;nbsp; 목차&lt;/div&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#sec1&quot;&gt;수업 개요&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#sec2&quot;&gt;온라인 머신러닝: 배치 학습과의 비교&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#sec3&quot;&gt;이상 탐지의 정의와 전통적 방법&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#sec4&quot;&gt;딥러닝 기반 이상 탐지 아키텍처&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#sec5&quot;&gt;평가 지표, 요약 및 모델 선택&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#sec6&quot;&gt;실습: 진동 스트리밍&amp;middot;온라인 학습&amp;middot;이상 탐지&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#sec7&quot;&gt;정리&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;h2 id=&quot;sec1&quot; class=&quot;sec-title title-26&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;hl-yellow&quot;&gt;1. 수업 개요&lt;/span&gt;&lt;/h2&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;이번 수업은 &lt;b&gt;온라인 머신러닝(Online Machine Learning)&lt;/b&gt;의 철학(배치 학습과의 비교, 스트리밍 환경에서의 평가&amp;middot;윈도우 전략, SGD&amp;middot;Hoeffding Tree, 개념 드리프트&amp;middot;ADWIN&amp;middot;Half-Space Trees, 연합 온라인 학습)과 &lt;b&gt;이상 탐지(Anomaly Detection)&lt;/b&gt; 이론(전통적 방법&amp;middot;딥러닝 재구성 기반)을 다룬다. 실습에서는 라즈베리파이(또는 센서 보드)에서 진동&amp;middot;가속도 데이터를 PC로 스트리밍한 뒤, 호스트 PC에서 온라인 분류(Hoeffding Tree), 개념 드리프트 감지(ADWIN), 이상 탐지(Autoencoder 등), 필요 시 연합 학습까지 진행하는 구성을 다룬다.&lt;/p&gt;
&lt;h2 id=&quot;sec2&quot; class=&quot;sec-title title-26&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;hl-yellow&quot;&gt;2. 온라인 머신러닝: 배치 학습과의 비교&lt;/span&gt;&lt;/h2&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;온라인 머신러닝은 데이터가 생성되는 즉시 학습에 반영하는 &lt;b&gt;동적 학습 체계&lt;/b&gt;이다.&lt;/p&gt;
&lt;h3 class=&quot;sec-subtitle&quot; style=&quot;margin-top: 20px; margin-bottom: 8px; font-weight: bold; font-size: 18px;&quot; data-ke-size=&quot;size23&quot;&gt;배치 vs 온라인&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;전통적 머신러닝 (Batch Learning)&lt;/b&gt;: 고정된 데이터셋(CSV 등)을 한꺼번에 학습한다. 데이터가 바뀌면 전체를 다시 학습해야 하므로 자원 소모가 크다. 식으로는 Model = Train(X_all, Y_all)에 해당한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;온라인 머신러닝 (Incremental Learning)&lt;/b&gt;: 스트림에서 새 샘플이 도착할 때마다 모델을 즉시 갱신한다. Model_t = Update(Model_{t-1}, X_new, Y_new) 형태로, 한 번에 한 인스턴스(또는 소량의 미니배치)만 사용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;필요성&lt;/b&gt;: (1) &lt;b&gt;대용량&amp;middot;스트리밍&lt;/b&gt; &amp;mdash; 서버 로그처럼 RAM을 초과하는 데이터를 다룰 때 필수적이다. (2) &lt;b&gt;역동적 환경&lt;/b&gt; &amp;mdash; 사용자 패턴&amp;middot;센서 데이터가 실시간으로 변할 때 유리하다. (3) &lt;b&gt;엣지 컴퓨팅&lt;/b&gt; &amp;mdash; 라즈베리 파이 같은 저사양 기기에서 대용량 저장 없이 AI를 운영할 수 있다. (4) &lt;b&gt;콜드 스타트 완화&lt;/b&gt; &amp;mdash; 방대한 데이터가 쌓일 때까지 기다리지 않고 바로 가동할 수 있다.&lt;/p&gt;
&lt;h3 class=&quot;sec-subtitle&quot; style=&quot;margin-top: 20px; margin-bottom: 8px; font-weight: bold; font-size: 18px;&quot; data-ke-size=&quot;size23&quot;&gt;핵심 용어와 평가&lt;/h3&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;실시간 스트리밍에서는 기존의 학습/테스트 분리가 그대로 통하지 않는다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;인스턴스(Instance)&lt;/b&gt;: 시간 t에 도착하는 단일 데이터 포인트 (x, y)이다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Interleaved Test-Then-Train&lt;/b&gt;: 들어온 데이터로 먼저 예측을 수행하고, 이후 실제 정답이 확인되면 그 데이터를 바로 학습에 사용하는 표준 방식이다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;리그렛(Regret)&lt;/b&gt;: 온라인 모델과 (모든 데이터를 알고 있다고 가정한) 가상의 최적 정적 모델 간의 성능 차이이다. 이 값이 작을수록 온라인 모델이 잘 적응하고 있다는 의미이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 class=&quot;sec-subtitle&quot; style=&quot;margin-top: 20px; margin-bottom: 8px; font-weight: bold; font-size: 18px;&quot; data-ke-size=&quot;size23&quot;&gt;윈도우 전략 (Windowing)&lt;/h3&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;데이터의 &amp;lsquo;신선도&amp;rsquo;를 유지하기 위해 어떤 데이터를 기억하고 버릴지 결정하는 방법이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Sliding Window&lt;/b&gt;: 최근 N개의 샘플만 유지하는 가장 보편적인 방식(FIFO)이다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Damped Window&lt;/b&gt;: 최근 데이터에는 높은 가중치를, 오래된 데이터에는 지수적으로 감소하는 낮은 가중치를 부여한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 class=&quot;sec-subtitle&quot; style=&quot;margin-top: 20px; margin-bottom: 8px; font-weight: bold; font-size: 18px;&quot; data-ke-size=&quot;size23&quot;&gt;알고리즘: 선형 모델과 트리&lt;/h3&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;온라인 학습의 성능은 모델이 얼마나 빠르게 갱신되느냐에 좌우된다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;SGD (Stochastic Gradient Descent)&lt;/b&gt;: 온라인 학습의 기반이다. 오차가 발생할 때마다 가중치를 소량씩 조정하며, 회귀&amp;middot;SVM 등에 널리 쓰인다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Hoeffding Trees (VFDT)&lt;/b&gt;: 스트리밍용 의사결정 나무이다. 노드 분할 시 &lt;b&gt;Hoeffding Bound&lt;/b&gt;를 사용한다. 식은 &amp;epsilon; = &amp;radic;(R&amp;sup2; ln(1/&amp;delta;) / (2n)) 이며, n개 샘플이 쌓였을 때 현재 관찰된 최선의 분할이 진정한 최적임을 통계적으로 보장한다. 이 &amp;epsilon;이 일정 수준 이하로 떨어져야 비로소 노드를 분할해 모델이 정교해진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 class=&quot;sec-subtitle&quot; style=&quot;margin-top: 20px; margin-bottom: 8px; font-weight: bold; font-size: 18px;&quot; data-ke-size=&quot;size23&quot;&gt;개념 드리프트 (Concept Drift)와 ADWIN&lt;/h3&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;온라인 환경의 큰 도전은 &lt;b&gt;정답이 변하는 현상&lt;/b&gt;이다. 계절에 따라 온도의 &amp;lsquo;정상&amp;rsquo; 범위가 달라지듯, 데이터의 통계적 특성이 바뀌는 것을 개념 드리프트라 한다. &lt;b&gt;Sudden&lt;/b&gt;(갑작스러운 사고), &lt;b&gt;Gradual&lt;/b&gt;(센서 노후화), &lt;b&gt;Recurring&lt;/b&gt;(계절성) 유형이 있다. &lt;b&gt;ADWIN&lt;/b&gt;은 스트림의 변화를 감지해 윈도우 크기를 자동으로 조절하거나, 감지 시 윈도우를 리셋해 드리프트 이후 데이터만 반영하도록 한다.&lt;/p&gt;
&lt;h3 class=&quot;sec-subtitle&quot; style=&quot;margin-top: 20px; margin-bottom: 8px; font-weight: bold; font-size: 18px;&quot; data-ke-size=&quot;size23&quot;&gt;Half-Space Trees (HST)와 이상 탐지&lt;/h3&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;공간을 무작위로 분할해, 데이터가 &lt;b&gt;희소한(Sparse)&lt;/b&gt; 영역에 떨어지면 이를 이상치로 판단하는 비지도 학습 방식이다. 라벨 없이 스트리밍 이상 탐지에 활용할 수 있다.&lt;/p&gt;
&lt;h3 class=&quot;sec-subtitle&quot; style=&quot;margin-top: 20px; margin-bottom: 8px; font-weight: bold; font-size: 18px;&quot; data-ke-size=&quot;size23&quot;&gt;연합 온라인 학습 (Federated Online Learning)&lt;/h3&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;프라이버시와 실시간 성능을 동시에 만족시키는 방식이다. 라즈베리 파이 같은 엣지 기기가 로컬에서 데이터를 즉시 학습하고, &lt;b&gt;데이터 자체가 아닌 업데이트된 가중치(Weight)&lt;/b&gt;만 서버로 전송한다. 서버는 여러 기기의 가중치를 합산해 글로벌 모델을 만들고 다시 기기들에 배포한다. 데이터 유출 없이 대규모 협업 학습이 가능하다.&lt;/p&gt;
&lt;h2 id=&quot;sec3&quot; class=&quot;sec-title title-26&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;hl-yellow&quot;&gt;3. 이상 탐지의 정의와 전통적 방법&lt;/span&gt;&lt;/h2&gt;
&lt;h3 class=&quot;sec-subtitle&quot; style=&quot;margin-top: 20px; margin-bottom: 8px; font-weight: bold; font-size: 18px;&quot; data-ke-size=&quot;size23&quot;&gt;이상 탐지란?&lt;/h3&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;정의&lt;/b&gt;: 데이터셋의 일반적인 패턴이나 행동에서 크게 벗어나는 데이터 포인트, 이벤트, 또는 관측치를 식별하는 기술이다. 건초 더미 속의 바늘을 찾는 것과 같다. 실제 환경에서는 이상치가 매우 드물기 때문에(보통 전체의 1% 미만) 정확히 찾아내는 것이 어렵다.&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;세 가지 주요 유형&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;점 이상(Point Anomalies)&lt;/b&gt;: 개별 데이터 포인트 하나가 나머지와 멀리 떨어진 경우. 예: 신용카드 부정 사용.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;문맥적 이상(Contextual Anomalies)&lt;/b&gt;: 특정 상황에서만 이상으로 간주되는 경우. 예: 여름의 30&amp;deg;C는 정상이지만 겨울의 30&amp;deg;C는 이상.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;집단적 이상(Collective Anomalies)&lt;/b&gt;: 개별 데이터는 정상일 수 있으나, 그 시퀀스나 집합이 비정상인 경우. 예: 불규칙한 심장 박동.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 class=&quot;sec-subtitle&quot; style=&quot;margin-top: 20px; margin-bottom: 8px; font-weight: bold; font-size: 18px;&quot; data-ke-size=&quot;size23&quot;&gt;왜 딥러닝인가? (전통적 머신러닝과의 차이)&lt;/h3&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;전통적 방법의 한계&lt;/b&gt;: Isolation Forest나 One-Class SVM 같은 기존 방식은 이미지, 비디오 같은 고차원 데이터나 복잡한 비선형 관계를 다루기 어렵다.&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;딥러닝의 장점&lt;/b&gt;: (1) &lt;b&gt;특징 학습&lt;/b&gt; &amp;mdash; 수동으로 특징을 뽑지 않고 계층적 표현을 자동으로 학습한다. (2) &lt;b&gt;확장성&lt;/b&gt; &amp;mdash; 대규모 데이터를 효과적으로 처리할 수 있다. (3) &lt;b&gt;비정형 데이터&lt;/b&gt; &amp;mdash; 이미지, 오디오, 시계열 데이터 처리에 강하다.&lt;/p&gt;
&lt;h3 class=&quot;sec-subtitle&quot; style=&quot;margin-top: 20px; margin-bottom: 8px; font-weight: bold; font-size: 18px;&quot; data-ke-size=&quot;size23&quot;&gt;Isolation Forest: 전통적 방식의 대표&lt;/h3&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;데이터를 직접 모델링하기보다, 데이터를 &lt;b&gt;고립(Isolate)&lt;/b&gt;시키는 데 집중하는 비지도 학습 알고리즘이다. 무작위 분할로 트리를 만들 때, 이상치는 정상 데이터보다 적은 횟수의 분할로도 쉽게 고립된다. 따라서 &lt;b&gt;경로 길이(Path length)&lt;/b&gt;가 짧을수록 이상치일 확률이 높다고 판단한다. 표 형식 데이터나 저차원 특징에는 여전히 많이 쓰인다.&lt;/p&gt;
&lt;h2 id=&quot;sec4&quot; class=&quot;sec-title title-26&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;hl-yellow&quot;&gt;4. 딥러닝 기반 이상 탐지 아키텍처&lt;/span&gt;&lt;/h2&gt;
&lt;h3 class=&quot;sec-subtitle&quot; style=&quot;margin-top: 20px; margin-bottom: 8px; font-weight: bold; font-size: 18px;&quot; data-ke-size=&quot;size23&quot;&gt;재구성 기반 탐지 원리&lt;/h3&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;딥러닝 이상 탐지의 핵심 아이디어는 &lt;b&gt;재구성(Reconstruction)&lt;/b&gt;이다. &lt;b&gt;가설&lt;/b&gt;: 정상 데이터로만 학습한 모델은 정상 데이터는 잘 복원하지만, 이상치를 넣으면 복원에 실패한다. &lt;b&gt;지표&lt;/b&gt;: 입력과 출력의 차이인 &lt;b&gt;재구성 오차(Reconstruction Error, MSE 등)&lt;/b&gt;를 쓴다. 오차가 낮으면 정상, 높으면 이상으로 본다.&lt;/p&gt;
&lt;h3 class=&quot;sec-subtitle&quot; style=&quot;margin-top: 20px; margin-bottom: 8px; font-weight: bold; font-size: 18px;&quot; data-ke-size=&quot;size23&quot;&gt;주요 아키텍처&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Autoencoder (AE)&lt;/b&gt;: 입력을 잠재 공간(Latent space)으로 압축했다가 다시 복원하는 구조. 정상 샘플로만 학습해 재구성 손실을 최소화한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Variational Autoencoder (VAE)&lt;/b&gt;: 일반 AE는 입력을 고정된 점으로 인코딩하는 반면, VAE는 평균(&amp;mu;)과 분산(&amp;sigma;&amp;sup2;)을 가진 확률 분포로 인코딩한다. 잠재 공간이 더 부드러워지고 이상 점수를 확률적으로 계산하기 좋다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;LSTM Autoencoder&lt;/b&gt;: 로그&amp;middot;센서 같은 &lt;b&gt;시계열 데이터&lt;/b&gt;에 맞춘 구조. LSTM으로 시간적 의존성을 잡고, 재구성 오차로 이상을 판단한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;GAN (AnoGAN)&lt;/b&gt;: 생성자(G)는 정상 데이터를 생성하도록, 판별자(D)는 가짜를 걸러내도록 학습한다. 추론 시 특정 입력을 잘 생성하지 못하거나 판별자가 거부하면 이상으로 본다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;sec5&quot; class=&quot;sec-title title-26&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;hl-yellow&quot;&gt;5. 평가 지표, 요약 및 모델 선택&lt;/span&gt;&lt;/h2&gt;
&lt;h3 class=&quot;sec-subtitle&quot; style=&quot;margin-top: 20px; margin-bottom: 8px; font-weight: bold; font-size: 18px;&quot; data-ke-size=&quot;size23&quot;&gt;평가 지표와 임계값&lt;/h3&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;이상 탐지에서는 데이터 불균형이 심하므로 &lt;b&gt;정확도(Accuracy)&lt;/b&gt;만 믿으면 안 된다. &lt;b&gt;임계값(Threshold)&lt;/b&gt;은 고정값이나 통계적 방법(예: 평균 + 3&amp;times;표준편차)으로 정한다. &lt;b&gt;주요 지표&lt;/b&gt;: 정밀도(Precision), 재현율(Recall), F1-Score, AUROC로 성능을 종합 평가한다.&lt;/p&gt;
&lt;h3 class=&quot;sec-subtitle&quot; style=&quot;margin-top: 20px; margin-bottom: 8px; font-weight: bold; font-size: 18px;&quot; data-ke-size=&quot;size23&quot;&gt;요약 및 권장 사항&lt;/h3&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;데이터 준비&lt;/b&gt;: 신경망 학습 전에 데이터를 [0, 1] 범위로 정규화하는 것이 일반적이다.&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;데이터 유형별 모델 선택&lt;/b&gt;: 표 형식(Tabular) &amp;rarr; AE/VAE. 시계열(Time-Series) &amp;rarr; LSTM/Transformer. 이미지 &amp;rarr; Convolutional AE / GAN.&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;주의사항&lt;/b&gt;: 학습 데이터에 이상치가 섞여 들어가는 &lt;b&gt;오염(Contamination)&lt;/b&gt;을 피해야 한다. 이상치까지 정상으로 학습하면 탐지 성능이 떨어진다.&lt;/p&gt;
&lt;h2 id=&quot;sec6&quot; class=&quot;sec-title title-26&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;hl-yellow&quot;&gt;6. 실습: 진동 스트리밍&amp;middot;온라인 학습&amp;middot;이상 탐지&lt;/span&gt;&lt;/h2&gt;
&lt;h3 class=&quot;sec-subtitle&quot; style=&quot;margin-top: 20px; margin-bottom: 8px; font-weight: bold; font-size: 18px;&quot; data-ke-size=&quot;size23&quot;&gt;구성&lt;/h3&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;라즈베리파이(또는 M5Core 등)가 &lt;b&gt;가속도&amp;middot;진동 데이터&lt;/b&gt;를 수집해 TCP로 스트리밍하고, 호스트 PC가 이를 받아 &lt;b&gt;온라인 학습&lt;/b&gt;과 &lt;b&gt;이상 탐지&lt;/b&gt;를 수행한다. 센서로는 BMI160&amp;middot;MPU6050 같은 IMU(가속도계)를 사용해 X, Y, Z 축 가속도 값을 일정 주기(예: 20Hz)로 전송한다. 호스트는 수신한 패킷을 JSON으로 파싱한 뒤, 특징(ax, ay, az, 또는 윈도우 통계)을 추출해 온라인 분류기&amp;middot;이상 탐지기에 넣는다.&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;실습에서는 (1) &lt;b&gt;온라인 분류&lt;/b&gt;: 슬라이딩 윈도우로 윈도우별 통계(std_x, std_y, std_z, mean_x 등)를 만들고, River의 StandardScaler + HoeffdingTreeClassifier로 &amp;ldquo;정지(IDLE)&amp;rdquo; vs &amp;ldquo;진동(MOTION)&amp;rdquo;을 Test-Then-Train 방식으로 학습&amp;middot;예측한다. (2) &lt;b&gt;개념 드리프트&lt;/b&gt;: ADWIN으로 스트림의 평균 변화를 감지하는 데모(합성 스트림 또는 실데이터)를 통해 드리프트 시점을 확인한다. (3) &lt;b&gt;연합 학습&lt;/b&gt;: 엣지 클라이언트가 로컬에서 학습한 뒤 가중치만 서버(Flower 등)로 보내고, 서버가 FedAvg로 집계한 글로벌 모델을 다시 배포하는 흐름을 경험할 수 있다.&lt;/p&gt;
&lt;h3 class=&quot;sec-subtitle&quot; style=&quot;margin-top: 20px; margin-bottom: 8px; font-weight: bold; font-size: 18px;&quot; data-ke-size=&quot;size23&quot;&gt;두 단계: 캘리브레이션 &amp;rarr; 탐지&lt;/h3&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1단계(캘리브레이션)&lt;/b&gt;: 센서를 &lt;b&gt;정지 상태&lt;/b&gt;로 두고 일정 개수(예: 200~1000개)의 &amp;ldquo;정상&amp;rdquo; 샘플만 수집한다. 이 구간에서는 사용자가 진동을 주지 않는다. 수집이 끝나면 이 데이터만으로 모델을 학습(또는 통계 기준을 잡는다).&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2단계(탐지)&lt;/b&gt;: 학습이 끝나면 실시간으로 들어오는 샘플마다 이상 여부를 판단한다. 전통적 방법(Isolation Forest)이면 fit된 모델로 predict&amp;middot;decision_function을 쓰고, 통계적 방법(Z-Score)이면 최근 구간의 평균&amp;middot;표준편차를 기준으로 Z값을 계산해 임계값을 넘으면 이상으로 친다. 딥러닝 방법(Autoencoder)이면 정규화된 입력을 재구성한 뒤 MSE를 구하고, 학습 시 정해둔 임계값(예: 학습 재구성 오차의 평균+3&amp;times;표준편차, 또는 최대&amp;times;1.1)과 비교한다.&lt;/p&gt;
&lt;h3 class=&quot;sec-subtitle&quot; style=&quot;margin-top: 20px; margin-bottom: 8px; font-weight: bold; font-size: 18px;&quot; data-ke-size=&quot;size23&quot;&gt;세 가지 탐지 방식&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Z-Score 기반&lt;/b&gt;: 최근 N개 샘플의 진동 크기(또는 축별 값)에 대해 평균과 표준편차를 구하고, 현재 값의 Z-Score가 임계값(예: 3.5)을 넘으면 이상. 구현이 단순하고 별도 학습이 없지만, 구간 길이와 임계값 설정에 따라 민감도가 달라진다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Isolation Forest&lt;/b&gt;: 수집한 정상 데이터로 Isolation Forest를 학습한다. 추론 시 1=정상, -1=이상으로 예측하고, decision_function 점수(음수일수록 이상에 가깝다)를 함께 쓸 수 있다. contamination 파라미터로 민감도를 조절한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Autoencoder&lt;/b&gt;: 입력 차원(예: 3)을 잠재 차원(예: 2)으로 압축했다가 다시 3으로 복원하는 AE를 정상 데이터로만 학습한다. Min-Max 정규화 후 MSE 손실로 학습하고, 학습 데이터의 재구성 오차 분포를 보고 임계값을 정한다(평균+3&amp;times;표준편차 또는 최대 오차&amp;times;여유 계수). 실시간으로 들어오는 샘플의 재구성 오차가 임계값을 넘으면 이상으로 판단한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 class=&quot;sec-subtitle&quot; style=&quot;margin-top: 20px; margin-bottom: 8px; font-weight: bold; font-size: 18px;&quot; data-ke-size=&quot;size23&quot;&gt;테스트 방법&lt;/h3&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;먼저 센서를 움직이지 않고 정상 데이터만 모아 캘리브레이션&amp;middot;학습을 끝낸다. 그다음 Z축 방향으로 살짝 두드리거나 흔들어 &lt;b&gt;인위적인 진동(이상치)&lt;/b&gt;을 준다. 이상 탐지기에서는 재구성 오차가 임계값을 넘으면 &amp;ldquo;ANOMALY&amp;rdquo;로, 온라인 분류기에서는 &amp;ldquo;MOTION&amp;rdquo; 등으로 표시된다. 콘솔에 해당 메시지가 뜨면 동작이 확인된다. 정상일 때는 &amp;ldquo;NORMAL&amp;rdquo; 또는 &amp;ldquo;IDLE&amp;rdquo;로 표시되며, 재구성 오차나 점수가 임계값 아래로 유지되는지 보면 된다.&lt;/p&gt;
&lt;h2 id=&quot;sec7&quot; class=&quot;sec-title title-26&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;hl-yellow&quot;&gt;7. 정리&lt;/span&gt;&lt;/h2&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;이번 수업에서는 &lt;b&gt;온라인 머신러닝&lt;/b&gt;의 철학(배치 vs 스트리밍, 인스턴스&amp;middot;Test-Then-Train&amp;middot;리그렛, 슬라이딩&amp;middot;댐핑 윈도우, SGD&amp;middot;Hoeffding Tree, 개념 드리프트&amp;middot;ADWIN&amp;middot;Half-Space Trees, 연합 온라인 학습)과 &lt;b&gt;이상 탐지&lt;/b&gt;의 정의(점&amp;middot;문맥&amp;middot;집단 이상), 전통적 방법(Isolation Forest), 딥러닝 재구성 접근(AE, VAE, LSTM-AE, GAN), 평가 지표&amp;middot;실무 지침(정규화, 데이터 유형별 모델, Contamination 주의)을 정리했다. 실습에서는 라즈베리파이(또는 센서 보드)에서 진동&amp;middot;가속도 데이터를 스트리밍하고, 호스트 PC에서 온라인 분류(Hoeffding Tree), 개념 드리프트 감지(ADWIN), 이상 탐지(Z-Score, Isolation Forest, Autoencoder), 필요 시 연합 학습까지 진행했다. 센서를 멈춘 상태에서 정상 기준을 학습한 뒤, 진동을 주어 이상을 유도해 탐지&amp;middot;분류기가 반응하는지 확인하는 흐름으로 진행했다.&lt;/p&gt;
&lt;style&gt;
    #post-root a { color:#495057 !important; text-decoration:none !important; }
    #post-root a:hover { color:#212529 !important; border-bottom:1px solid #ced4da; }
    #post-root a:visited { color:#666 !important; }

    .sec-title { display:block; margin:28px 0 12px; font-weight:800; line-height:1.35; }
    .sec-title &gt; .hl-yellow {
      background:linear-gradient(transparent 65%, #fff3bf 65%);
      padding:0 .12em;
    }
    .title-26 { font-size:26px; }

    .para { margin:10px 0 14px; color:#343a40; line-height:1.9; }

    .toc {
      border:1px solid #e9ecef;
      border-radius:10px;
      padding:12px 14px;
      margin:18px 0 24px;
      background:#f8f9fa;
    }

    .codecard {
      background:#f8f9fa;
      border:1px solid #e9ecef;
      border-radius:8px;
      padding:16px;
      margin:16px 0;
      overflow-x:auto;
    }

    .codecard .head {
      font-weight:700;
      color:#212529;
      margin-bottom:10px;
      font-size:15px;
    }

    .codecard pre { margin:0; padding:0; background:transparent; border:none; }
    .codecard code {
      font-family:'Courier New', monospace;
      font-size:13px;
      line-height:1.6;
      color:#212529;
    }
    &lt;/style&gt;
&lt;/div&gt;</description>
      <category>UNLV</category>
      <author>빡성</author>
      <guid isPermaLink="true">https://smp0417.tistory.com/77</guid>
      <comments>https://smp0417.tistory.com/77#entry77comment</comments>
      <pubDate>Tue, 24 Feb 2026 07:38:25 +0900</pubDate>
    </item>
    <item>
      <title>[UNLV 프로젝트] - 프로젝트 설명과 내 역할, 진행 내용</title>
      <link>https://smp0417.tistory.com/76</link>
      <description>&lt;!-- ========================= --&gt;
&lt;div id=&quot;post-root&quot;&gt;
&lt;div class=&quot;toc&quot;&gt;
&lt;div style=&quot;font-weight: 800; margin-bottom: 8px;&quot;&gt;&amp;nbsp; &amp;nbsp; 목차&lt;/div&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#sec1&quot;&gt;프로젝트가 하는 일&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#sec2&quot;&gt;AoII와 시스템 구성&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#sec3&quot;&gt;데이터 흐름과 ML 모델&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#sec4&quot;&gt;내 역할과 맡은 부분&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#sec5&quot;&gt;지금까지 진행한 작업&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#sec6&quot;&gt;장애가 날 수 있는 지점&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#sec7&quot;&gt;앞으로 발전시킬 것들&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#sec8&quot;&gt;마치며&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;h2 id=&quot;sec1&quot; class=&quot;sec-title title-26&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;hl-yellow&quot;&gt;1. 프로젝트가 하는 일&lt;/span&gt;&lt;/h2&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;이 프로젝트는 &lt;b&gt;엣지 디바이스(ESP32)&lt;/b&gt;에서 온&amp;middot;습도 센서(AHT20/21) 값을 읽고, 작은 예측 모델(MLP, &lt;b&gt;Rolling Window 4단계&amp;middot;12-64-32-2 ReLU&lt;/b&gt;)로 &amp;ldquo;다음 온&amp;middot;습도&amp;rdquo;를 추정한다. 예측이 괜찮으면 전송하지 않고, &lt;b&gt;예측 오차가 임계값을 넘을 때만&lt;/b&gt; 433MHz LoRa로 한 번 보낸다. 그렇게 해서 통신 횟수&amp;middot;전력을 줄이면서도, 게이트웨이와 엣지 모두에서 &lt;b&gt;실제값을 받을 때마다 온라인 학습&lt;/b&gt;으로 같은 모델을 유지&amp;middot;동기화한다. 수집된 데이터는 MQTT를 한 번 발행한 뒤, 구독자가 MySQL&amp;middot;CSV에 각각 저장하고, Flask 대시보드&amp;middot;API&amp;middot;Prometheus 메트릭&amp;middot;Grafana로 모니터링한다. 실험 시간대는 라스베이거스(UTC-8) 기준으로, 엣지&amp;middot;게이트웨이 모두 &amp;ldquo;하루 중 시간 비율(time_n, 0~1)&amp;rdquo;을 입력으로 쓴다. 한 줄로 말하면, &lt;b&gt;&amp;ldquo;필요할 때만 보내고, 엣지와 게이트웨이가 같은 모델로 함께 학습하는 IoT + AoII 실험 시스템&amp;rdquo;&lt;/b&gt;이다.&lt;/p&gt;
&lt;h2 id=&quot;sec2&quot; class=&quot;sec-title title-26&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;hl-yellow&quot;&gt;2. AoII와 시스템 구성&lt;/span&gt;&lt;/h2&gt;
&lt;h3 class=&quot;sec-subtitle&quot; style=&quot;margin-top: 20px; margin-bottom: 8px; font-weight: bold; font-size: 18px;&quot; data-ke-size=&quot;size23&quot;&gt;AoII(Age of Incorrect Information)란?&lt;/h3&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;잘못된 정보가 얼마나 오래 유지되는가&amp;rdquo;를 재는 지표다. IoT&amp;middot;센서 네트워크에서는 데이터를 매번 보내지 않고도 원격지가 현재 상태를 잘 추정하게 만드는 연구와 연결된다. 이 프로젝트에서는 &lt;b&gt;온도 오차 &amp;beta;_temp(0.5&amp;deg;C) 또는 습도 오차 &amp;beta;_hum(3%) 이상&lt;/b&gt;일 때, 또는 &lt;b&gt;10분 주기 하트비트&lt;/b&gt;, &lt;b&gt;시간 미동기화&lt;/b&gt;일 때만 전송한다. 그 외에는 전송을 생략(SKIP)해 전력을 아끼고, 전송 횟수&amp;middot;오차&amp;middot;수신 데이터를 DB&amp;middot;CSV&amp;middot;메트릭으로 쌓아 AoII&amp;middot;전송 효율&amp;middot;모델 성능을 분석할 수 있게 했다.&lt;/p&gt;
&lt;h3 class=&quot;sec-subtitle&quot; style=&quot;margin-top: 20px; margin-bottom: 8px; font-weight: bold; font-size: 18px;&quot; data-ke-size=&quot;size23&quot;&gt;시스템 구성 상세&lt;/h3&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;559&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d0uLPH/dJMcab4jeBp/RJlkbbUoMpZCnsV1CMBNj0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d0uLPH/dJMcab4jeBp/RJlkbbUoMpZCnsV1CMBNj0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d0uLPH/dJMcab4jeBp/RJlkbbUoMpZCnsV1CMBNj0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd0uLPH%2FdJMcab4jeBp%2FRJlkbbUoMpZCnsV1CMBNj0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;697&quot; height=&quot;380&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;559&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;엣지&lt;/b&gt;: Heltec WiFi LoRa 32 v2, AHT20/21 온습도, 433MHz LoRa, OLED. 1분 주기로 light sleep 후 측정&amp;middot;예측&amp;middot;전송 조건 판단을 한다. 전송 조건을 만족하면 LoRa로 &amp;ldquo;온도,습도&amp;rdquo; 문자열을 보내고, 게이트웨이에서 시리얼로 넘겨 주는 Unix 시간을 받아 시간 동기화 후 엣지 MLP를 한 번 온라인 학습한다. 시간만 요청할 때는 Ping(0.0, 0.0) 같은 특수 패킷을 쓰고, 응답으로 시간만 받을 수 있다. USB 시리얼로 매 주기마다 SKIP/SEND/HEARTBEAT 등 한 줄 로그를 출력해, 별도 로거로 수집해 DB에 남길 수 있다.&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;게이트웨이(하드웨어)&lt;/b&gt;: ESP32 + LoRa. 엣지 패킷을 받으면 수신 내용을 그대로 USB 시리얼로 전달한다. ML 연산은 하지 않고, 게이트웨이 소프트웨어가 시리얼을 읽어 처리한다.&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;게이트웨이(소프트웨어)&lt;/b&gt;: 시리얼 포트는 설정 파일(.env)에서 지정한다(맥북 예: /dev/cu.usbserial-3, 라즈베리파이 예: /dev/ttyUSB0). 시리얼에서 &amp;ldquo;Received: 온도,습도&amp;rdquo; 형태의 문자열을 파싱해 실제 온&amp;middot;습도를 얻고, 엣지와 동일한 &lt;b&gt;12-64-32-2&lt;/b&gt; MLP로 현재 상태에서 예측한 뒤 실제값으로 온라인 학습(역전파)을 수행한다. 그다음 MQTT 브로커(설정에서 주소&amp;middot;포트 지정, 라즈베리파이에서 실행 시 보통 localhost, 맥북에서 실행 시 라즈베리파이 IP)로 특정 토픽(예: aoii/readings)에 JSON을 발행한다. 이벤트 종류는 두 가지다.&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;RX&lt;/b&gt;: 엣지에서 데이터를 수신했을 때(실제 온&amp;middot;습도, 예측값, 오차, 누적 전송 횟수 등 포함)&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;EST&lt;/b&gt;: 60초마다 게이트웨이만의 예측값을 보낼 때. RX가 발생하면 엣지에게 시리얼로 Unix 시간을 보내 ACK 역할을 한다.&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;메시지 브로커&lt;/b&gt;: Mosquitto를 라즈베리파이에서 실행한다(기본 포트 1883). 외부에서 접속하려면 listener를 0.0.0.0으로 두고, 필요 시 allow_anonymous 등 설정을 한다. 발행&amp;middot;구독은 하나의 토픽으로 중개된다.&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;저장&lt;/b&gt;: 구독자 두 개를 둘 수 있다.&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;(1) &lt;b&gt;DB 저장&lt;/b&gt;: event=RX인 메시지만 MySQL의 readings 테이블에 insert한다. 맥북에서 MySQL을 쓰고 이 구독자를 실행하면, created_at은 로컬 시간으로 저장해 대시보드&amp;middot;CSV와 맞춘다.&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;(2) &lt;b&gt;CSV 저장&lt;/b&gt;: RX&amp;middot;EST 모두 프로젝트 루트의 experiment_log_online.csv에 한 줄씩 append한다. 라즈베리파이에서 이 구독자만 실행하면 Pi에 CSV가 쌓여 실험 로그&amp;middot;재현용으로 쓴다.&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;(3) 선택적으로 엣지 USB 시리얼을 읽는 로거를 두어, SKIP/SEND/HEARTBEAT 구분으로 edge_log 테이블에 남길 수 있다.&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;모니터링&lt;/b&gt;: Flask 서버는 DB에서 통계(get_stats)&amp;middot;최근 데이터(get_recent)를 조회해 대시보드(차트&amp;middot;통계)와 API(/api/stats, /api/recent)를 제공한다. 동시에 Prometheus 형식의 /metrics 엔드포인트를 노출해, aoii_readings_total(총 수신 횟수), aoii_mae_temp(온도 평균 절대 오차) 등 메트릭을 준다. Prometheus는 설정(prometheus.yml)에 따라 이 /metrics를 주기적으로 스크래핑하고, Grafana는 Prometheus를 데이터 소스로 연결해 대시보드&amp;middot;알람을 만든다.&lt;/p&gt;
&lt;h2 id=&quot;sec3&quot; class=&quot;sec-title title-26&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;hl-yellow&quot;&gt;3. 데이터 흐름과 ML 모델&lt;/span&gt;&lt;/h2&gt;
&lt;h3 class=&quot;sec-subtitle&quot; style=&quot;margin-top: 20px; margin-bottom: 8px; font-weight: bold; font-size: 18px;&quot; data-ke-size=&quot;size23&quot;&gt;데이터 흐름 (단계별)&lt;/h3&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;559&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bd8VyV/dJMcagSaTx9/XXuBGurPvGSMIk5dsf8NIK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bd8VyV/dJMcagSaTx9/XXuBGurPvGSMIk5dsf8NIK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bd8VyV/dJMcagSaTx9/XXuBGurPvGSMIk5dsf8NIK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbd8VyV%2FdJMcagSaTx9%2FXXuBGurPvGSMIk5dsf8NIK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1024&quot; height=&quot;559&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;559&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1단계 &amp;mdash; 엣지&lt;/b&gt;: AHT로 온도&amp;middot;습도를 측정한다. 입력은 &lt;b&gt;과거 4시점의 (온도, 습도, time_n)을 펼친 12차원&lt;/b&gt;이다. &lt;b&gt;12-64-32-2&lt;/b&gt; MLP로 &amp;ldquo;다음 예측 온도&amp;middot;습도&amp;rdquo;를 낸 뒤, 실제 측정값과 비교해 err_t &amp;ge; &amp;beta;_temp(0.5&amp;deg;C) 또는 err_h &amp;ge; &amp;beta;_hum(3%)이면, 또는 10분 하트비트 구간이면, 또는 시간이 아직 동기화되지 않았으면 LoRa로 &amp;ldquo;온도,습도&amp;rdquo;를 전송한다. 전송한 경우에는 게이트웨이 ESP32가 시리얼로 넘겨 주는 Unix 시간을 LoRa로 받아 시간을 맞추고, 방금 보낸 실제값으로 엣지 MLP를 한 번 역전파(온라인 학습)한다.&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2단계 &amp;mdash; 게이트웨이 ESP32&lt;/b&gt;: LoRa로 엣지 패킷을 받으면 수신 문자열(예: &amp;ldquo;Received: 25.3,60.2&amp;rdquo;)을 USB 시리얼로 그대로 PC 또는 라즈베리파이에 전달한다. Ping 0.0,0.0이면 시간 요청/응답만 처리할 수 있다.&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3단계 &amp;mdash; 게이트웨이 소프트웨어&lt;/b&gt;: 시리얼에서 &amp;ldquo;Received: ...&amp;rdquo;를 파싱해 실제 온&amp;middot;습도를 얻는다. 동일한 &lt;b&gt;12-64-32-2&lt;/b&gt; MLP로 현재 상태에서 예측한 뒤, 이 실제값으로 온라인 학습(online_update)을 호출해 가중치를 갱신한다. MQTT로 aoii/readings 토픽에 JSON을 발행한다. event=RX(엣지 수신 시) 또는 60초마다 event=EST(게이트웨이만의 예측). 페이로드에는 timestamp, time_n, actual_t, actual_h, pred_t, pred_h, error_t, error_h, total_tx 등이 들어간다. RX일 때는 엣지에게 시리얼로 Unix 시간을 보내 ACK&amp;middot;시간 동기화를 한다.&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4단계 &amp;mdash; MQTT 구독자&lt;/b&gt;: DB 저장 구독자는 event=RX인 메시지만 MySQL readings 테이블에 insert한다(맥북에서 실행). CSV 저장 구독자는 RX&amp;middot;EST 모두 experiment_log_online.csv에 한 줄씩 append한다(라즈베리파이에서 실행 시 Pi에 파일 생성).&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;5단계 &amp;mdash; 모니터링&lt;/b&gt;: Flask는 DB에서 get_stats&amp;middot;get_recent를 조회해 대시보드&amp;middot;API를 제공하고, /metrics에서 Prometheus 메트릭을 노출한다. Prometheus가 이를 스크래핑하고, Grafana에서 시각화&amp;middot;알람을 구성한다.&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 class=&quot;sec-subtitle&quot; style=&quot;margin-top: 20px; margin-bottom: 8px; font-weight: bold; font-size: 18px;&quot; data-ke-size=&quot;size23&quot;&gt;ML 모델 (12-64-32-2 MLP, Rolling Window)&lt;/h3&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;구조&lt;/b&gt;: 입력 &lt;b&gt;12&lt;/b&gt;(과거 4시점의 온도&amp;middot;습도&amp;middot;time_n을 펼친 벡터) &amp;rarr; 은닉층 &lt;b&gt;64(ReLU)&lt;/b&gt; &amp;rarr; 은닉층 &lt;b&gt;32(ReLU)&lt;/b&gt; &amp;rarr; 출력 2(예측 온도, 예측 습도). 엣지와 게이트웨이에 동일한 구조&amp;middot;가중치를 둔다.&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;사전 학습&lt;/b&gt;: 사전에 수집한 데이터셋(타임스탬프, 온도, 습도)으로 &lt;b&gt;Rolling Window(크기 4)&lt;/b&gt;를 적용해 12차원 입력&amp;middot;2차원 출력 데이터를 만들고, &lt;b&gt;sklearn MLPRegressor 12-64-32-2(ReLU)&lt;/b&gt;를 학습한 뒤, 가중치와 스케일러(정규화 파라미터)를 추출해 엣지에는 C 배열로, 게이트웨이에는 Python으로 동일하게 반영한다.&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;온라인 학습&lt;/b&gt;: 전송이 일어날 때마다 엣지&amp;middot;게이트웨이 모두 실제값(방금 수신한 온&amp;middot;습도)으로 역전파를 한 번 수행한다. 학습률은 0.05 등으로 고정해 두고, 같은 데이터로 갱신하므로 양쪽 모델이 계속 동기화된다.&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;비교 실험&lt;/b&gt;: &amp;ldquo;온라인 학습 없이 고정 모델만 쓰는 경우&amp;rdquo;(오프라인 TinyML 비교군)는 게이트웨이에서 online_update 호출만 주석 처리하면 된다. CSV&amp;middot;DB에 event, total_tx, error_t, error_h가 쌓이므로, &amp;beta; 변경&amp;middot;학습 여부에 따른 전송 횟수&amp;middot;MAE 비교가 가능하다.&lt;/p&gt;
&lt;h2 id=&quot;sec4&quot; class=&quot;sec-title title-26&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;hl-yellow&quot;&gt;4. 내 역할과 맡은 부분&lt;/span&gt;&lt;/h2&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;이 프로젝트에서 나는 &lt;b&gt;데이터 파이프라인 설계&amp;middot;구현, 설정&amp;middot;저장&amp;middot;모니터링 정리, 그리고 장애 지점 파악&amp;middot;보완 과제 정리&lt;/b&gt;를 맡았다. 엣지 펌웨어나 LoRa 통신 프로토콜 자체보다는, 게이트웨이가 받은 패킷을 어디로 보낼지(한 번만 MQTT 발행), 누가 DB에 넣고 누가 CSV에 넣을지(구독자 분리), 시리얼 포트&amp;middot;MQTT 브로커 주소&amp;middot;DB 비밀번호는 어디에 두는지(.env 단일 소스), DB&amp;middot;CSV&amp;middot;대시보드의 시간을 어떻게 맞출지(로컬 시간 저장), 파이프라인 어디가 끊기면 어떤 영향인지(장애 지점 표), 장애가 생기면 무엇을 더 해야 하는지(QoS&amp;middot;DLQ&amp;middot;재시도&amp;middot;백업&amp;middot;런북 등)를 문서와 코드로 정리하는 쪽에 기여했다.&lt;/p&gt;
&lt;h2 id=&quot;sec5&quot; class=&quot;sec-title title-26&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;hl-yellow&quot;&gt;5. 지금까지 진행한 작업&lt;/span&gt;&lt;/h2&gt;
&lt;h3 class=&quot;sec-subtitle&quot; style=&quot;margin-top: 20px; margin-bottom: 8px; font-weight: bold; font-size: 18px;&quot; data-ke-size=&quot;size23&quot;&gt;이벤트 기반 파이프라인으로 전환&lt;/h3&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;454&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bxOxOh/dJMcajg0etX/wRydCJ27wkurGXnki1CLuK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bxOxOh/dJMcajg0etX/wRydCJ27wkurGXnki1CLuK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bxOxOh/dJMcajg0etX/wRydCJ27wkurGXnki1CLuK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbxOxOh%2FdJMcajg0etX%2FwRydCJ27wkurGXnki1CLuK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1024&quot; height=&quot;454&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;454&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #343a40; text-align: start;&quot;&gt;원래는 게이트웨이가 수신할 때 DB와 CSV에 직접 쓰는 구조였다. DB 연결 실패나 CSV 쓰기 실패가 나면 한 경로 장애가 전체 저장 실패로 이어질 수 있어서, 게이트웨이는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;수신 시 MQTT로 한 번만 발행&lt;/b&gt;&lt;span style=&quot;color: #343a40; text-align: start;&quot;&gt;하도록 바꿨다. DB 저장은 &amp;ldquo;RX만 MySQL readings 테이블에 insert하는 구독자&amp;rdquo;, CSV 저장은 &amp;ldquo;RX&amp;middot;EST 모두 experiment_log_online.csv에 append하는 구독자&amp;rdquo;가 각각 구독해 처리한다. 발행부와 저장부를 분리해 장애 격리가 되었고, 나중에 저장 채널을 추가하거나(예: 다른 DB, 다른 파일) 변경할 때도 구독자만 추가&amp;middot;수정하면 된다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 class=&quot;sec-subtitle&quot; style=&quot;margin-top: 20px; margin-bottom: 8px; font-weight: bold; font-size: 18px;&quot; data-ke-size=&quot;size23&quot;&gt;분산 환경 역할 분리&amp;middot;문서화&lt;/h3&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;라즈베리파이에는 MQTT 브로커(Mosquitto)와 CSV 저장 구독자만 실행하고 MySQL은 설치하지 않는다. 맥북에서는 게이트웨이 프로세스(시리얼 수신), MySQL, DB 저장 구독자를 실행한다. &amp;ldquo;어디서 무엇을 실행할지&amp;rdquo;, 실행 순서(예: 브로커 &amp;rarr; 게이트웨이 &amp;rarr; 구독자), 포트&amp;middot;토픽 이름(aoii/readings 등)을 MQTT&amp;middot;모니터링 문서에 정리해 두었다.&lt;/p&gt;
&lt;h3 class=&quot;sec-subtitle&quot; style=&quot;margin-top: 20px; margin-bottom: 8px; font-weight: bold; font-size: 18px;&quot; data-ke-size=&quot;size23&quot;&gt;설정&amp;middot;비밀정보 단일 소스&lt;/h3&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;여러 스크립트가 각자 설정 파일을 갖지 않고, 프로젝트 루트의 &lt;b&gt;.env 한 곳만&lt;/b&gt; 읽도록 통일했다. SERIAL_PORT, MQTT_BROKER, MQTT_PORT, MYSQL_HOST, MYSQL_USER, MYSQL_PASSWORD 등 필요한 키를 .env에 두고, .env는 git에서 제외한다. 팀원에게는 &amp;ldquo;필요한 키 목록 + 예시값&amp;rdquo;만 공유하고, 실제 비밀번호는 직접 채우도록 했다. mysql_example.env 같은 중복 예시 파일은 제거했다.&lt;/p&gt;
&lt;h3 class=&quot;sec-subtitle&quot; style=&quot;margin-top: 20px; margin-bottom: 8px; font-weight: bold; font-size: 18px;&quot; data-ke-size=&quot;size23&quot;&gt;데이터 정합성(시간)&lt;/h3&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;DB에만 UTC로 저장하고 CSV&amp;middot;모니터링은 로컬 시간을 쓰다 보니 8~9시간 차이가 났다. DB 저장 시 insert_reading&amp;middot;insert_edge_log에서 &lt;b&gt;created_at을 로컬 시간&lt;/b&gt;으로 기록하도록 수정해, 대시보드&amp;middot;CSV와 맞춰 두었다.&lt;/p&gt;
&lt;h3 class=&quot;sec-subtitle&quot; style=&quot;margin-top: 20px; margin-bottom: 8px; font-weight: bold; font-size: 18px;&quot; data-ke-size=&quot;size23&quot;&gt;모니터링&amp;middot;문서 정리&lt;/h3&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;Flask의 /metrics 노출 &amp;rarr; Prometheus가 prometheus.yml 설정대로 스크래핑 &amp;rarr; Grafana에서 Prometheus를 데이터 소스로 연결하는 절차와, aoii_readings_total, aoii_mae_temp 등 메트릭 목록을 MONITORING.md에 정리했다. MQTT.md, MONITORING.md를 &amp;ldquo;실행 순서&amp;middot;역할&amp;middot;설정&amp;rdquo; 중심으로 재구성해, 새로 합류한 사람이 어디서 무엇을 켜야 하는지 따라 할 수 있게 했다.&lt;/p&gt;
&lt;h3 class=&quot;sec-subtitle&quot; style=&quot;margin-top: 20px; margin-bottom: 8px; font-weight: bold; font-size: 18px;&quot; data-ke-size=&quot;size23&quot;&gt;코드&amp;middot;중복 정리&lt;/h3&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;루트에 있던 중복 게이트웨이 스크립트를 삭제하고, RX 이벤트가 두 번 로그에 찍히던 부분을 제거했다. 시리얼 포트&amp;middot;MQTT 브로커 주소 등은 모두 .env에서 읽도록 환경 변수화해, 설정 변경은 .env 한 곳에서만 하면 되도록 했다. Prometheus가 생성하는 data/ 폴더 용도도 문서에 적어 두었다.&lt;/p&gt;
&lt;h2 id=&quot;sec6&quot; class=&quot;sec-title title-26&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;hl-yellow&quot;&gt;6. 장애가 날 수 있는 지점&lt;/span&gt;&lt;/h2&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;파이프라인은 &amp;ldquo;엣지 &amp;rarr; LoRa &amp;rarr; 게이트웨이 ESP32 &amp;rarr; USB 시리얼 &amp;rarr; 게이트웨이 소프트웨어 &amp;rarr; MQTT 브로커(라즈베리파이) &amp;rarr; mqtt_to_csv / mqtt_to_mysql&amp;rdquo; 순이다. 구간별로 장애 가능 지점&amp;middot;영향&amp;middot;비고를 정리해 두었다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;시리얼&lt;/b&gt;: USB 케이블 분리, 잘못된 포트 지정, 다른 프로세스가 포트 점유(예: 아두이노 IDE 시리얼 모니터)하면 게이트웨이 소프트웨어가 데이터를 받지 못하고 &lt;b&gt;전체 파이프라인 중단&lt;/b&gt;이 된다. 맥북에서 게이트웨이를 돌릴 때 시리얼 포트 충돌이 자주 원인이 된다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;게이트웨이 프로세스&lt;/b&gt;: 크래시, 맥북 슬립/종료 시 수신&amp;middot;MQTT 발행이 끊긴다. 이후 구간(브로커&amp;middot;구독자)에는 데이터가 들어오지 않는다. 단일 프로세스라 재시작 전까지 복구되지 않는다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;맥북&amp;ndash;라즈베리파이 네트워크&lt;/b&gt;: Wi‑Fi 끊김, 라즈베리파이 전원 나감&amp;middot;재부팅 시 게이트웨이가 브로커에 접속하지 못하고, 맥북에서 돌리는 DB 저장 구독자도 브로커를 구독하지 못한다. 브로커가 라즈베리파이에 있으므로 네트워크 의존도가 크다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;MQTT 브로커(Mosquitto)&lt;/b&gt;: 프로세스 종료, Pi 디스크 풀, Pi 전원 장애 시 &lt;b&gt;모든 구독자에게 메시지 전달이 불가&lt;/b&gt;하다. 단일 브로커라 SPOF(단일 장애점)이다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;CSV 저장 구독자&lt;/b&gt;: 프로세스 종료, Pi 디스크 풀, 파일 권한 오류 시 CSV만 갱신되지 않는다. DB&amp;middot;MQTT 자체는 동작하므로 영향은 CSV 구간으로 한정된다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;DB 저장 구독자&amp;middot;MySQL&lt;/b&gt;: 구독자 프로세스 종료나 MySQL 다운&amp;middot;연결 한도 초과 시 DB에 insert가 되지 않는다. 현재 MQTT는 &lt;b&gt;QoS 0&lt;/b&gt;이라 브로커가 메시지를 보관하지 않는다. 따라서 구독자가 다운된 시점에 발행된 메시지는 &lt;b&gt;유실&lt;/b&gt;되고, 재시작 후에도 &amp;ldquo;그 사이&amp;rdquo; 메시지는 복구할 수 없다. MySQL 쪽에서 insert 실패 시 재시도가 없으면 해당 메시지 역시 유실된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;요약하면, &lt;b&gt;전체 중단&lt;/b&gt;은 시리얼 단절&amp;middot;게이트웨이 중단&amp;middot;브로커 중단&amp;middot;맥북&amp;ndash;Pi 네트워크 단절일 때이고, &lt;b&gt;부분 유실&lt;/b&gt;은 DB 저장 구독자&amp;middot;MySQL 장애 시 해당 구간 데이터 유실, 그리고 QoS 0으로 인한 &amp;ldquo;구독자 다운 시점 메시지 유실&amp;rdquo;이다.&lt;/p&gt;
&lt;h2 id=&quot;sec7&quot; class=&quot;sec-title title-26&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;hl-yellow&quot;&gt;7. 앞으로 발전시킬 것들&lt;/span&gt;&lt;/h2&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;한 번 들어온 데이터는 유실 없이 처리&amp;middot;보관하고, 장애 시 빠르게 알아내고 복구할 수 있게 하려면 아래 같은 보완이 필요하다. 우선순위와 리소스에 맞춰 단계적으로 적용할 수 있다.&lt;/p&gt;
&lt;h3 class=&quot;sec-subtitle&quot; style=&quot;margin-top: 20px; margin-bottom: 8px; font-weight: bold; font-size: 18px;&quot; data-ke-size=&quot;size23&quot;&gt;파이프라인&amp;middot;가용성&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;메시지 유실 방지&lt;/b&gt;: 현재 QoS 0은 브로커가 메시지를 저장하지 않아 구독자 다운 시 유실된다. QoS 1/2 또는 persistent session을 도입하면 구독자가 잠시 꺼져 있어도 브로커가 메시지를 보관&amp;middot;재전달할 수 있다. 채널 암호화&amp;middot;신뢰성 요구에 맞추려면 보통 보완이 필요하다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Dead Letter Queue(DLQ)&lt;/b&gt;: DB insert 실패 등 처리 실패 메시지를 별도 토픽&amp;middot;큐로 보내 두고, 원인(DB 다운, 스키마 오류 등)을 조치한 뒤 재처리(재생)할 수 있게 한다. 한 번은 반드시 처리하거나 보관하는 구조로 가져가는 것이 좋다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;브로커 이중화&lt;/b&gt;: 단일 Mosquitto는 SPOF이므로, 클러스터 또는 대기 브로커를 두어 장애 시 전환되도록 구성하면 가용성이 올라간다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;게이트웨이 자동 재기동&lt;/b&gt;: systemd&amp;middot;supervisor 등으로 게이트웨이 프로세스를 감시하고, 죽으면 자동 재시작하도록 한다. 가능하면 동일 역할을 하는 인스턴스를 여러 개 두는 것도 검토할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 class=&quot;sec-subtitle&quot; style=&quot;margin-top: 20px; margin-bottom: 8px; font-weight: bold; font-size: 18px;&quot; data-ke-size=&quot;size23&quot;&gt;장애 대응&amp;middot;모니터링&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;헬스체크&amp;middot;알람&lt;/b&gt;: 이미 &amp;ldquo;수신 횟수&amp;middot;MAE&amp;rdquo; 등 메트릭이 있으므로, Grafana에서 &amp;ldquo;N분 이상 수신 없음&amp;rdquo;일 때 알람 규칙을 강화한다. 게이트웨이&amp;middot;MQTT&amp;middot;MySQL 프로세스가 살아 있는지 주기적으로 확인하는 헬스체크를 추가하면, 어느 구간에서 끊겼는지 빠르게 파악할 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;장애 시나리오 런북&lt;/b&gt;: &amp;ldquo;시리얼이 안 뜬다 &amp;rarr; 포트 목록 확인, 다른 프로세스 점유 여부, 재연결&amp;rdquo; / &amp;ldquo;MQTT 연결 실패 &amp;rarr; 브로커 주소&amp;middot;포트&amp;middot;네트워크 확인&amp;rdquo; / &amp;ldquo;MySQL 연결 실패 &amp;rarr; 서비스 상태&amp;middot;연결 한도&amp;middot;비밀번호 확인&amp;rdquo; / &amp;ldquo;CSV가 안 쌓인다 &amp;rarr; 구독자 프로세스&amp;middot;디스크&amp;middot;권한 확인&amp;rdquo;처럼 증상별 원인&amp;middot;확인 절차&amp;middot;조치를 문서(MD)로 정리해 두면 운영 시 유용하다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;메트릭 보강&lt;/b&gt;: &amp;ldquo;마지막 MQTT publish 성공 시각&amp;rdquo;, &amp;ldquo;구독자별 마지막 처리 시각&amp;rdquo;, &amp;ldquo;insert 실패 횟수&amp;rdquo; 등 파이프라인 구간별 메트릭을 추가하면, 어디서 지연&amp;middot;실패가 나는지 시각화&amp;middot;알람으로 잡기 쉬워진다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;로그 표준화&lt;/b&gt;: 에러&amp;middot;경고 시 시간&amp;middot;구간&amp;middot;에러 코드를 포함한 구조화된 로그를 남기고, 필요 시 중앙 수집(파일&amp;middot;에이전트)으로 추적하기 쉽게 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 class=&quot;sec-subtitle&quot; style=&quot;margin-top: 20px; margin-bottom: 8px; font-weight: bold; font-size: 18px;&quot; data-ke-size=&quot;size23&quot;&gt;데이터&amp;middot;운영 안정성&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;재시도&amp;middot;백오프&lt;/b&gt;: MySQL insert 실패 시 제한된 횟수&amp;middot;지수 백오프로 재시도하고, 그래도 실패하면 DLQ로 보낸다. 무한 재시도는 부하를 키우므로 막는 것이 좋다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;멱등성&lt;/b&gt;: 같은 메시지가 중복 수신돼도 중복 insert가 나지 않도록, 메시지 ID나 (timestamp, device) 같은 유니크 키로 제약을 두거나 &amp;ldquo;이미 있으면 skip&amp;rdquo; 로직을 넣는다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;백업&amp;middot;복구&lt;/b&gt;: DB&amp;middot;CSV 정기 백업과 보관 주기&amp;middot;삭제 정책을 정하고, 복구 절차(RPO/RTO)를 정의해 두면 장애 시 데이터 복구가 수월하다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;리텐션&amp;middot;용량&lt;/b&gt;: CSV&amp;middot;DB 보관 기간과 디스크 사용량을 추정하고, 오래된 데이터는 아카이빙&amp;middot;삭제하는 정책을 두어 디스크 풀을 방지한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 class=&quot;sec-subtitle&quot; style=&quot;margin-top: 20px; margin-bottom: 8px; font-weight: bold; font-size: 18px;&quot; data-ke-size=&quot;size23&quot;&gt;보안&amp;middot;감사&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;MQTT 브로커&amp;middot;클라이언트 간 TLS 적용(채널 암호화 요구 대응), MySQL 연결 TLS&amp;middot;필요 시 민감 컬럼 암호화, 설정(.env)&amp;middot;중요 스크립트 배포 시 &amp;ldquo;누가/언제/무엇을&amp;rdquo; 변경했는지 이력 관리 절차를 검토한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 class=&quot;sec-subtitle&quot; style=&quot;margin-top: 20px; margin-bottom: 8px; font-weight: bold; font-size: 18px;&quot; data-ke-size=&quot;size23&quot;&gt;설계&amp;middot;문서&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;엣지&amp;ndash;게이트웨이&amp;ndash;MQTT&amp;ndash;구독자&amp;ndash;DB/CSV 구간과 장애 지점을 한 그림에 표시한 아키텍처 다이어그램을 두면, 신규 합류자&amp;middot;운영자가 구조를 이해하기 쉽다. Part 2의 장애 지점 표를 확장해 &amp;ldquo;장애 유형 &amp;rarr; 영향 구간 &amp;rarr; 복구 절차&amp;rdquo; 매트릭스로 정리해 두는 것도 좋다. 용어(Gateway, 브로커, 구독자, readings 등)를 문서&amp;middot;코드 주석에서 통일하면 유지보수에 도움이 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;sec8&quot; class=&quot;sec-title title-26&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;hl-yellow&quot;&gt;8. 마치며&lt;/span&gt;&lt;/h2&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;이 프로젝트는 엣지 IoT에서 &lt;b&gt;AoII를 고려한 &amp;ldquo;필요할 때만 전송&amp;rdquo;&lt;/b&gt;을 구현하고, 엣지와 게이트웨이에 같은 &lt;b&gt;12-64-32-2&lt;/b&gt; MLP를 두어 &lt;b&gt;전송 시마다 온라인 학습으로 동기화&lt;/b&gt;하며, MQTT로 발행&amp;middot;구독을 나눠 &lt;b&gt;수집&amp;middot;저장&amp;middot;모니터링을 분리&lt;/b&gt;한 구조다. 설정은 .env 단일 소스로 두고, 실행 순서와 역할은 MQTT&amp;middot;모니터링 문서를 참고하면 된다.&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;나는 그중에서 파이프라인 설계&amp;middot;이벤트 기반 전환(게이트웨이 &amp;rarr; MQTT 한 번 발행, 구독자 분리), 분산 환경(라즈베리파이&amp;middot;맥북 역할 분리)&amp;middot;문서화, .env 단일 소스&amp;middot;비밀정보 관리, DB 시간 정합성(로컬 시간 저장), 모니터링 절차&amp;middot;메트릭 목록 정리, 코드&amp;middot;중복 제거, 그리고 &amp;ldquo;어디가 깨질 수 있는지&amp;rdquo;를 문서로 정리하는 역할을 했다.&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;앞으로는 메시지 유실 방지(QoS 1/2&amp;middot;persistent session&amp;middot;DLQ), 재시도&amp;middot;백오프&amp;middot;멱등성&amp;middot;백업, 헬스체크&amp;middot;알람&amp;middot;장애 시나리오 런북, 메트릭 보강, 필요 시 브로커 이중화&amp;middot;TLS&amp;middot;감사 절차를 단계적으로 적용하면, 실험용을 넘어 운영&amp;middot;서비스 관점에서도 더 견고한 시스템으로 발전시킬 수 있다.&lt;/p&gt;
&lt;style&gt;
    #post-root a { color:#495057 !important; text-decoration:none !important; }
    #post-root a:hover { color:#212529 !important; border-bottom:1px solid #ced4da; }
    #post-root a:visited { color:#666 !important; }

    .sec-title { display:block; margin:28px 0 12px; font-weight:800; line-height:1.35; }
    .sec-title &gt; .hl-yellow {
      background:linear-gradient(transparent 65%, #fff3bf 65%);
      padding:0 .12em;
    }
    .title-26 { font-size:26px; }

    .para { margin:10px 0 14px; color:#343a40; line-height:1.9; }

    .toc {
      border:1px solid #e9ecef;
      border-radius:10px;
      padding:12px 14px;
      margin:18px 0 24px;
      background:#f8f9fa;
    }

    .codecard {
      background:#f8f9fa;
      border:1px solid #e9ecef;
      border-radius:8px;
      padding:16px;
      margin:16px 0;
      overflow-x:auto;
    }

    .codecard .head {
      font-weight:700;
      color:#212529;
      margin-bottom:10px;
      font-size:15px;
    }

    .codecard pre { margin:0; padding:0; background:transparent; border:none; }
    .codecard code {
      font-family:'Courier New', monospace;
      font-size:13px;
      line-height:1.6;
      color:#212529;
    }
    &lt;/style&gt;
&lt;/div&gt;</description>
      <category>UNLV</category>
      <author>빡성</author>
      <guid isPermaLink="true">https://smp0417.tistory.com/76</guid>
      <comments>https://smp0417.tistory.com/76#entry76comment</comments>
      <pubDate>Tue, 24 Feb 2026 03:51:18 +0900</pubDate>
    </item>
    <item>
      <title>[UNLV] - 이상 탐지: 이론과 진동 센서 실습(12일차)</title>
      <link>https://smp0417.tistory.com/75</link>
      <description>&lt;!-- ========================= --&gt;
&lt;div id=&quot;post-root&quot;&gt;
    &lt;div class=&quot;toc&quot;&gt;
    &lt;div style=&quot;font-weight: 800; margin-bottom: 8px;&quot;&gt;&amp;nbsp; &amp;nbsp; 목차&lt;/div&gt;
    &lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
    &lt;li&gt;&lt;a href=&quot;#sec1&quot;&gt;수업 개요&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href=&quot;#sec2&quot;&gt;이상 탐지의 정의와 전통적 방법&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href=&quot;#sec3&quot;&gt;딥러닝 기반 이상 탐지 아키텍처&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href=&quot;#sec4&quot;&gt;평가 지표, 요약 및 모델 선택&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href=&quot;#sec5&quot;&gt;실습: 진동 데이터 스트리밍과 이상 탐지&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href=&quot;#sec6&quot;&gt;정리&lt;/a&gt;&lt;/li&gt;
    &lt;/ol&gt;
    &lt;/div&gt;

    &lt;h2 id=&quot;sec1&quot; class=&quot;sec-title title-26&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;hl-yellow&quot;&gt;1. 수업 개요&lt;/span&gt;&lt;/h2&gt;
    &lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;이번 수업은 &lt;b&gt;이상 탐지(Anomaly Detection)&lt;/b&gt;의 정의, 전통적 방법(Isolation Forest), 딥러닝 방법(Autoencoder, VAE, LSTM-AE, GAN), 평가 지표를 이론으로 다루고, 실습에서는 라즈베리파이(또는 센서 보드)에서 가속도·진동 데이터를 PC로 스트리밍한 뒤, 호스트 PC에서 정상 데이터만으로 캘리브레이션·학습을 하고 실시간으로 이상을 탐지하는 구성을 진행했다.&lt;/p&gt;

    &lt;h2 id=&quot;sec2&quot; class=&quot;sec-title title-26&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;hl-yellow&quot;&gt;2. 이상 탐지의 정의와 전통적 방법&lt;/span&gt;&lt;/h2&gt;

    &lt;h3 class=&quot;sec-subtitle&quot; style=&quot;margin-top:20px; margin-bottom:8px; font-weight:700; font-size:18px;&quot;&gt;이상 탐지란?&lt;/h3&gt;
    &lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;정의&lt;/b&gt;: 데이터셋의 일반적인 패턴이나 행동에서 크게 벗어나는 데이터 포인트, 이벤트, 또는 관측치를 식별하는 기술이다. 건초 더미 속의 바늘을 찾는 것과 같다. 실제 환경에서는 이상치가 매우 드물기 때문에(보통 전체의 1% 미만) 정확히 찾아내는 것이 어렵다.&lt;/p&gt;
    &lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;세 가지 주요 유형&lt;/b&gt;&lt;/p&gt;
    &lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
    &lt;li&gt;&lt;b&gt;점 이상(Point Anomalies)&lt;/b&gt;: 개별 데이터 포인트 하나가 나머지와 멀리 떨어진 경우. 예: 신용카드 부정 사용.&lt;/li&gt;
    &lt;li&gt;&lt;b&gt;문맥적 이상(Contextual Anomalies)&lt;/b&gt;: 특정 상황에서만 이상으로 간주되는 경우. 예: 여름의 30°C는 정상이지만 겨울의 30°C는 이상.&lt;/li&gt;
    &lt;li&gt;&lt;b&gt;집단적 이상(Collective Anomalies)&lt;/b&gt;: 개별 데이터는 정상일 수 있으나, 그 시퀀스나 집합이 비정상인 경우. 예: 불규칙한 심장 박동.&lt;/li&gt;
    &lt;/ul&gt;

    &lt;h3 class=&quot;sec-subtitle&quot; style=&quot;margin-top:20px; margin-bottom:8px; font-weight:700; font-size:18px;&quot;&gt;왜 딥러닝인가? (전통적 머신러닝과의 차이)&lt;/h3&gt;
    &lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;전통적 방법의 한계&lt;/b&gt;: Isolation Forest나 One-Class SVM 같은 기존 방식은 이미지, 비디오 같은 고차원 데이터나 복잡한 비선형 관계를 다루기 어렵다.&lt;/p&gt;
    &lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;딥러닝의 장점&lt;/b&gt;: (1) &lt;b&gt;특징 학습&lt;/b&gt; — 수동으로 특징을 뽑지 않고 계층적 표현을 자동으로 학습한다. (2) &lt;b&gt;확장성&lt;/b&gt; — 대규모 데이터를 효과적으로 처리할 수 있다. (3) &lt;b&gt;비정형 데이터&lt;/b&gt; — 이미지, 오디오, 시계열 데이터 처리에 강하다.&lt;/p&gt;

    &lt;h3 class=&quot;sec-subtitle&quot; style=&quot;margin-top:20px; margin-bottom:8px; font-weight:700; font-size:18px;&quot;&gt;Isolation Forest: 전통적 방식의 대표&lt;/h3&gt;
    &lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;데이터를 직접 모델링하기보다, 데이터를 &lt;b&gt;고립(Isolate)&lt;/b&gt;시키는 데 집중하는 비지도 학습 알고리즘이다. 무작위 분할로 트리를 만들 때, 이상치는 정상 데이터보다 적은 횟수의 분할로도 쉽게 고립된다. 따라서 &lt;b&gt;경로 길이(Path length)&lt;/b&gt;가 짧을수록 이상치일 확률이 높다고 판단한다. 표 형식 데이터나 저차원 특징에는 여전히 많이 쓰인다.&lt;/p&gt;

    &lt;h2 id=&quot;sec3&quot; class=&quot;sec-title title-26&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;hl-yellow&quot;&gt;3. 딥러닝 기반 이상 탐지 아키텍처&lt;/span&gt;&lt;/h2&gt;

    &lt;h3 class=&quot;sec-subtitle&quot; style=&quot;margin-top:20px; margin-bottom:8px; font-weight:700; font-size:18px;&quot;&gt;재구성 기반 탐지 원리&lt;/h3&gt;
    &lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;딥러닝 이상 탐지의 핵심 아이디어는 &lt;b&gt;재구성(Reconstruction)&lt;/b&gt;이다. &lt;b&gt;가설&lt;/b&gt;: 정상 데이터로만 학습한 모델은 정상 데이터는 잘 복원하지만, 이상치를 넣으면 복원에 실패한다. &lt;b&gt;지표&lt;/b&gt;: 입력과 출력의 차이인 &lt;b&gt;재구성 오차(Reconstruction Error, MSE 등)&lt;/b&gt;를 쓴다. 오차가 낮으면 정상, 높으면 이상으로 본다.&lt;/p&gt;

    &lt;h3 class=&quot;sec-subtitle&quot; style=&quot;margin-top:20px; margin-bottom:8px; font-weight:700; font-size:18px;&quot;&gt;주요 아키텍처&lt;/h3&gt;
    &lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
    &lt;li&gt;&lt;b&gt;Autoencoder (AE)&lt;/b&gt;: 입력을 잠재 공간(Latent space)으로 압축했다가 다시 복원하는 구조. 정상 샘플로만 학습해 재구성 손실을 최소화한다.&lt;/li&gt;
    &lt;li&gt;&lt;b&gt;Variational Autoencoder (VAE)&lt;/b&gt;: 일반 AE는 입력을 고정된 점으로 인코딩하는 반면, VAE는 평균(μ)과 분산(σ²)을 가진 확률 분포로 인코딩한다. 잠재 공간이 더 부드러워지고 이상 점수를 확률적으로 계산하기 좋다.&lt;/li&gt;
    &lt;li&gt;&lt;b&gt;LSTM Autoencoder&lt;/b&gt;: 로그·센서 같은 &lt;b&gt;시계열 데이터&lt;/b&gt;에 맞춘 구조. LSTM으로 시간적 의존성을 잡고, 재구성 오차로 이상을 판단한다.&lt;/li&gt;
    &lt;li&gt;&lt;b&gt;GAN (AnoGAN)&lt;/b&gt;: 생성자(G)는 정상 데이터를 생성하도록, 판별자(D)는 가짜를 걸러내도록 학습한다. 추론 시 특정 입력을 잘 생성하지 못하거나 판별자가 거부하면 이상으로 본다.&lt;/li&gt;
    &lt;/ul&gt;

    &lt;h2 id=&quot;sec4&quot; class=&quot;sec-title title-26&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;hl-yellow&quot;&gt;4. 평가 지표, 요약 및 모델 선택&lt;/span&gt;&lt;/h2&gt;

    &lt;h3 class=&quot;sec-subtitle&quot; style=&quot;margin-top:20px; margin-bottom:8px; font-weight:700; font-size:18px;&quot;&gt;평가 지표와 임계값&lt;/h3&gt;
    &lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;이상 탐지에서는 데이터 불균형이 심하므로 &lt;b&gt;정확도(Accuracy)&lt;/b&gt;만 믿으면 안 된다. &lt;b&gt;임계값(Threshold)&lt;/b&gt;은 고정값이나 통계적 방법(예: 평균 + 3×표준편차)으로 정한다. &lt;b&gt;주요 지표&lt;/b&gt;: 정밀도(Precision), 재현율(Recall), F1-Score, AUROC로 성능을 종합 평가한다.&lt;/p&gt;

    &lt;h3 class=&quot;sec-subtitle&quot; style=&quot;margin-top:20px; margin-bottom:8px; font-weight:700; font-size:18px;&quot;&gt;요약 및 권장 사항&lt;/h3&gt;
    &lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;데이터 준비&lt;/b&gt;: 신경망 학습 전에 데이터를 [0, 1] 범위로 정규화하는 것이 일반적이다.&lt;/p&gt;
    &lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;데이터 유형별 모델 선택&lt;/b&gt;: 표 형식(Tabular) → AE/VAE. 시계열(Time-Series) → LSTM/Transformer. 이미지 → Convolutional AE / GAN.&lt;/p&gt;
    &lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;주의사항&lt;/b&gt;: 학습 데이터에 이상치가 섞여 들어가는 &lt;b&gt;오염(Contamination)&lt;/b&gt;을 피해야 한다. 이상치까지 정상으로 학습하면 탐지 성능이 떨어진다.&lt;/p&gt;

    &lt;h2 id=&quot;sec5&quot; class=&quot;sec-title title-26&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;hl-yellow&quot;&gt;5. 실습: 진동 데이터 스트리밍과 이상 탐지&lt;/span&gt;&lt;/h2&gt;

    &lt;h3 class=&quot;sec-subtitle&quot; style=&quot;margin-top:20px; margin-bottom:8px; font-weight:700; font-size:18px;&quot;&gt;구성&lt;/h3&gt;
    &lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;라즈베리파이(또는 M5Core 등)가 &lt;b&gt;가속도·진동 데이터&lt;/b&gt;를 수집해 TCP로 스트리밍하고, 호스트 PC가 이를 받아 이상 탐지를 수행한다. 센서로는 MPU6050 같은 IMU(가속도계)를 사용해 X, Y, Z 축 가속도 값을 일정 주기(예: 20Hz)로 전송한다. 호스트는 수신한 패킷을 JSON으로 파싱한 뒤, 특징(ax, ay, az 또는 진동 크기)을 추출해 탐지기에 넣는다.&lt;/p&gt;

    &lt;h3 class=&quot;sec-subtitle&quot; style=&quot;margin-top:20px; margin-bottom:8px; font-weight:700; font-size:18px;&quot;&gt;두 단계: 캘리브레이션 → 탐지&lt;/h3&gt;
    &lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1단계(캘리브레이션)&lt;/b&gt;: 센서를 &lt;b&gt;정지 상태&lt;/b&gt;로 두고 일정 개수(예: 200~1000개)의 “정상” 샘플만 수집한다. 이 구간에서는 사용자가 진동을 주지 않는다. 수집이 끝나면 이 데이터만으로 모델을 학습(또는 통계 기준을 잡는다).&lt;/p&gt;
    &lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2단계(탐지)&lt;/b&gt;: 학습이 끝나면 실시간으로 들어오는 샘플마다 이상 여부를 판단한다. 전통적 방법(Isolation Forest)이면 fit된 모델로 predict·decision_function을 쓰고, 통계적 방법(Z-Score)이면 최근 구간의 평균·표준편차를 기준으로 Z값을 계산해 임계값을 넘으면 이상으로 친다. 딥러닝 방법(Autoencoder)이면 정규화된 입력을 재구성한 뒤 MSE를 구하고, 학습 시 정해둔 임계값(예: 학습 재구성 오차의 평균+3×표준편차, 또는 최대×1.1)과 비교한다.&lt;/p&gt;

    &lt;h3 class=&quot;sec-subtitle&quot; style=&quot;margin-top:20px; margin-bottom:8px; font-weight:700; font-size:18px;&quot;&gt;세 가지 탐지 방식&lt;/h3&gt;
    &lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
    &lt;li&gt;&lt;b&gt;Z-Score 기반&lt;/b&gt;: 최근 N개 샘플의 진동 크기(또는 축별 값)에 대해 평균과 표준편차를 구하고, 현재 값의 Z-Score가 임계값(예: 3.5)을 넘으면 이상. 구현이 단순하고 별도 학습이 없지만, 구간 길이와 임계값 설정에 따라 민감도가 달라진다.&lt;/li&gt;
    &lt;li&gt;&lt;b&gt;Isolation Forest&lt;/b&gt;: 수집한 정상 데이터로 Isolation Forest를 학습한다. 추론 시 1=정상, -1=이상으로 예측하고, decision_function 점수(음수일수록 이상에 가깝다)를 함께 쓸 수 있다. contamination 파라미터로 민감도를 조절한다.&lt;/li&gt;
    &lt;li&gt;&lt;b&gt;Autoencoder&lt;/b&gt;: 입력 차원(예: 3)을 잠재 차원(예: 2)으로 압축했다가 다시 3으로 복원하는 AE를 정상 데이터로만 학습한다. Min-Max 정규화 후 MSE 손실로 학습하고, 학습 데이터의 재구성 오차 분포를 보고 임계값을 정한다(평균+3×표준편차 또는 최대 오차×여유 계수). 실시간으로 들어오는 샘플의 재구성 오차가 임계값을 넘으면 이상으로 판단한다.&lt;/li&gt;
    &lt;/ul&gt;

    &lt;h3 class=&quot;sec-subtitle&quot; style=&quot;margin-top:20px; margin-bottom:8px; font-weight:700; font-size:18px;&quot;&gt;테스트 방법&lt;/h3&gt;
    &lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;먼저 센서를 움직이지 않고 정상 데이터만 모아 캘리브레이션·학습을 끝낸다. 그다음 Z축 방향으로 살짝 두드리거나 흔들어 &lt;b&gt;인위적인 진동(이상치)&lt;/b&gt;을 준다. 탐지기가 이를 이상으로 잡아내고, 콘솔에 “ANOMALY” 또는 유사 메시지가 뜨면 동작이 확인된다. 정상일 때는 “NORMAL” 등으로 표시되며, 재구성 오차나 점수 바가 임계값 아래로 유지되는지 보면 된다.&lt;/p&gt;

    &lt;h2 id=&quot;sec6&quot; class=&quot;sec-title title-26&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;hl-yellow&quot;&gt;6. 정리&lt;/span&gt;&lt;/h2&gt;
    &lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;이번 수업에서는 &lt;b&gt;이상 탐지&lt;/b&gt;의 정의(점·문맥·집단 이상), 전통적 방법인 Isolation Forest의 원리, 딥러닝 기반 재구성 접근(AE, VAE, LSTM-AE, GAN)과 평가 지표·임계값·데이터 유형별 모델 선택을 정리했다. 실습에서는 라즈베리파이(또는 센서 보드)에서 가속도 데이터를 스트리밍하고, 호스트 PC에서 정상만 수집한 뒤 Z-Score, Isolation Forest, Autoencoder 세 가지 방식으로 실시간 이상 탐지를 수행했다. 센서를 멈춘 상태에서 정상 기준을 학습하고, 이후 진동을 주어 이상을 유도해 탐지기가 반응하는지 확인하는 흐름으로 진행했다.&lt;/p&gt;

    &lt;style&gt;
    #post-root a { color:#495057 !important; text-decoration:none !important; }
    #post-root a:hover { color:#212529 !important; border-bottom:1px solid #ced4da; }
    #post-root a:visited { color:#666 !important; }

    .sec-title { display:block; margin:28px 0 12px; font-weight:800; line-height:1.35; }
    .sec-title &gt; .hl-yellow {
      background:linear-gradient(transparent 65%, #fff3bf 65%);
      padding:0 .12em;
    }
    .title-26 { font-size:26px; }

    .para { margin:10px 0 14px; color:#343a40; line-height:1.9; }

    .toc {
      border:1px solid #e9ecef;
      border-radius:10px;
      padding:12px 14px;
      margin:18px 0 24px;
      background:#f8f9fa;
    }

    .codecard {
      background:#f8f9fa;
      border:1px solid #e9ecef;
      border-radius:8px;
      padding:16px;
      margin:16px 0;
      overflow-x:auto;
    }

    .codecard .head {
      font-weight:700;
      color:#212529;
      margin-bottom:10px;
      font-size:15px;
    }

    .codecard pre { margin:0; padding:0; background:transparent; border:none; }
    .codecard code {
      font-family:'Courier New', monospace;
      font-size:13px;
      line-height:1.6;
      color:#212529;
    }
    &lt;/style&gt;
&lt;/div&gt;</description>
      <category>UNLV</category>
      <author>빡성</author>
      <guid isPermaLink="true">https://smp0417.tistory.com/75</guid>
      <comments>https://smp0417.tistory.com/75#entry75comment</comments>
      <pubDate>Sat, 21 Feb 2026 03:47:29 +0900</pubDate>
    </item>
    <item>
      <title>[UNLV] - 오프라인 음성 인식으로 Tello 드론 제어(11일차)</title>
      <link>https://smp0417.tistory.com/74</link>
      <description>&lt;!-- ========================= --&gt;
&lt;div id=&quot;post-root&quot;&gt;
&lt;div class=&quot;toc&quot;&gt;
&lt;div style=&quot;font-weight: 800; margin-bottom: 8px;&quot;&gt;&amp;nbsp; &amp;nbsp; 목차&lt;/div&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#sec1&quot;&gt;수업 개요&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#sec2&quot;&gt;실시간 드론 제어와 오프라인 음성 인식&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#sec3&quot;&gt;음성 엔진 비교: Vosk, OpenWakeWord, PocketSphinx&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#sec4&quot;&gt;실습 1: Vosk + Tello (네이티브 오디오)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#sec5&quot;&gt;실습 2: OpenWakeWord + Vosk (웨이크워드 후 명령)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#sec6&quot;&gt;실습 3: PocketSphinx + Tello&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#sec7&quot;&gt;실습 4: 데이터셋&amp;middot;CNN 학습&amp;middot;임베디드 추론&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#sec8&quot;&gt;정리&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;h2 id=&quot;sec1&quot; class=&quot;sec-title title-26&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;hl-yellow&quot;&gt;1. 수업 개요&lt;/span&gt;&lt;/h2&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;이번 수업은 &lt;b&gt;오프라인 음성 인식&lt;/b&gt;으로 &lt;b&gt;Tello 드론&lt;/b&gt;을 실시간 제어하는 내용이다. 인터넷이 없어도 &amp;ldquo;Take off&amp;rdquo;, &amp;ldquo;Land&amp;rdquo;, &amp;ldquo;Stop&amp;rdquo; 같은 말을 인식해 드론을 조종할 수 있도록, PC에서 쓰기 좋은 엔진(Vosk, OpenWakeWord, PocketSphinx)과 마이크 라이브러리(SoundDevice)를 사용했다. 이론에서는 왜 실시간 드론 제어에 Vosk가 적합한지, Whisper와의 차이, 네이티브 오디오(SoundDevice)의 장점을 정리하고, 실습에서는 Vosk 단독 제어, OpenWakeWord(웨이크워드 &amp;ldquo;Hey Jarvis&amp;rdquo;) 후 Vosk 명령, PocketSphinx 제어, 그리고 Google Speech Commands 데이터셋&amp;middot;MFCC CNN 학습&amp;middot;TFLite&amp;middot;라즈베리파이&amp;middot;M5Core2 추론까지의 흐름을 다룬다.&lt;/p&gt;
&lt;h2 id=&quot;sec2&quot; class=&quot;sec-title title-26&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;hl-yellow&quot;&gt;2. 실시간 드론 제어와 오프라인 음성 인식&lt;/span&gt;&lt;/h2&gt;
&lt;h3 class=&quot;sec-subtitle&quot; style=&quot;margin-top: 20px; margin-bottom: 8px; font-weight: bold; font-size: 18px;&quot; data-ke-size=&quot;size23&quot;&gt;왜 Vosk인가? (Vosk vs Whisper)&lt;/h3&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;음성을 글자로 바꾸는 &lt;b&gt;STT(Speech-to-Text)&lt;/b&gt; 중에서 OpenAI의 &lt;b&gt;Whisper&lt;/b&gt;는 정확도가 높지만, &lt;b&gt;실시간 드론 제어&lt;/b&gt;에는 &lt;b&gt;Vosk&lt;/b&gt;가 더 적합하다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;지연 시간(Latency)&lt;/b&gt;: Vosk는 오디오를 &lt;b&gt;스트리밍&lt;/b&gt;으로 처리해 밀리초 단위 반응이 가능하다. Whisper는 보통 몇 초 단위 청크를 모아 처리하므로 지연이 크다. &amp;ldquo;Stop&amp;rdquo;이라고 했을 때 드론이 즉시 멈추려면 Vosk 같은 저지연 엔진이 필요하다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;경량성&lt;/b&gt;: Vosk는 일반 CPU만으로 동작하며, 무거운 GPU가 필요 없다. 노트북&amp;middot;RPi에서도 실행하기 쉽다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;설치&lt;/b&gt;: &lt;code&gt;pip install vosk&lt;/code&gt;로 설치하고, 작은 언어 모델(vosk-model-small-en-us-0.15 등)을 받아 폴더만 두면 된다. Whisper나 다른 엔진은 빌드&amp;middot;의존성 문제가 생기는 경우가 많다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 class=&quot;sec-subtitle&quot; style=&quot;margin-top: 20px; margin-bottom: 8px; font-weight: bold; font-size: 18px;&quot; data-ke-size=&quot;size23&quot;&gt;네이티브 오디오: SoundDevice&lt;/h3&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;Python 음성 인식에서 가장 까다로운 부분은 &lt;b&gt;마이크 라이브러리&lt;/b&gt;다. &lt;b&gt;PyAudio&lt;/b&gt;는 Windows&amp;middot;Mac에서 Visual Studio Build Tools, Homebrew, PortAudio 등 시스템 의존성을 요구해 설치가 번거롭다. 이 수업에서는 &lt;b&gt;SoundDevice&lt;/b&gt;를 쓴다. SoundDevice는 Windows&amp;middot;macOS용 &lt;b&gt;사전 빌드 바이너리(DLL/dylib)&lt;/b&gt;를 포함해, 별도 컴파일 없이 &lt;code&gt;pip install sounddevice&lt;/code&gt;만으로 마이크 입력을 사용할 수 있다. 따라서 &amp;ldquo;네이티브&amp;rdquo; 방식으로 Mac&amp;middot;Windows 모두에서 동일한 코드가 동작하도록 구성했다.&lt;/p&gt;
&lt;h2 id=&quot;sec3&quot; class=&quot;sec-title title-26&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;hl-yellow&quot;&gt;3. 음성 엔진 비교: Vosk, OpenWakeWord, PocketSphinx&lt;/span&gt;&lt;/h2&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Vosk&lt;/b&gt;: 오프라인 STT. 스트리밍 처리, 낮은 지연, pip&amp;middot;소형 모델로 설치 간단. 드론 명령어(&amp;ldquo;take off&amp;rdquo;, &amp;ldquo;land&amp;rdquo;, &amp;ldquo;stop&amp;rdquo; 등) 인식에 적합.&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;OpenWakeWord&lt;/b&gt;: &amp;ldquo;Hey Jarvis&amp;rdquo; 같은 &lt;b&gt;웨이크 워드&lt;/b&gt;만 감지하는 경량 모델. 항상 전체 어휘를 듣지 않고, 웨이크 워드가 나온 뒤에만 Vosk로 명령을 받으면 오인식과 배터리 소모를 줄일 수 있다.&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;PocketSphinx&lt;/b&gt;: 클래식한 오프라인 엔진. 설치 시 SWIG&amp;middot;C++ 빌드 도구가 필요해 Windows&amp;middot;Mac에서 설정이 다소 까다롭다. 짧은 단어(&amp;ldquo;up&amp;rdquo; 등)보다 &amp;ldquo;go up&amp;rdquo;, &amp;ldquo;go down&amp;rdquo;처럼 구문으로 인식시키면 정확도가 올라간다. SoundDevice로 녹음한 뒤 SpeechRecognition으로 PocketSphinx를 호출하는 방식으로 사용할 수 있다.&lt;/p&gt;
&lt;h2 id=&quot;sec4&quot; class=&quot;sec-title title-26&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;hl-yellow&quot;&gt;4. 실습 1: Vosk + Tello (네이티브 오디오)&lt;/span&gt;&lt;/h2&gt;
&lt;h3 class=&quot;sec-subtitle&quot; style=&quot;margin-top: 20px; margin-bottom: 8px; font-weight: bold; font-size: 18px;&quot; data-ke-size=&quot;size23&quot;&gt;4-1. 설치 및 모델 다운로드&lt;/h3&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;터미널( Mac) 또는 명령 프롬프트(Windows)에서:&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;pip install vosk sounddevice djitellopy numpy&lt;/code&gt;&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;Vosk는 소형 언어 모델 폴더가 필요하다. (1) &lt;b&gt;vosk-model-small-en-us-0.15&lt;/b&gt;(약 40MB) zip을 다운로드하고, (2) 압축을 푼 뒤 (3) 폴더 이름을 &lt;code&gt;model&lt;/code&gt; 또는 &lt;code&gt;vosk_model&lt;/code&gt;로 맞추고, (4) 프로젝트 폴더 안에 넣는다. 폴더 안에 &lt;code&gt;am&lt;/code&gt;, &lt;code&gt;conf&lt;/code&gt;, &lt;code&gt;graph&lt;/code&gt; 등이 있어야 한다.&lt;/p&gt;
&lt;h3 class=&quot;sec-subtitle&quot; style=&quot;margin-top: 20px; margin-bottom: 8px; font-weight: bold; font-size: 18px;&quot; data-ke-size=&quot;size23&quot;&gt;4-2. 제어 흐름&lt;/h3&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;SoundDevice로 마이크에서 16kHz&amp;middot;16bit&amp;middot;모노 오디오를 받아, 큐를 통해 Vosk에 넘긴다. 한 문장이 완성되면 인식 결과를 파싱해 &amp;ldquo;take off&amp;rdquo;, &amp;ldquo;land&amp;rdquo;, &amp;ldquo;up&amp;rdquo;, &amp;ldquo;down&amp;rdquo;, &amp;ldquo;left&amp;rdquo;, &amp;ldquo;right&amp;rdquo;, &amp;ldquo;forward&amp;rdquo;, &amp;ldquo;back&amp;rdquo;, &amp;ldquo;stop&amp;rdquo;, &amp;ldquo;flip&amp;rdquo; 등으로 Tello를 제어한다. Tello 연결 실패 시 시뮬레이션 모드로 동작하도록 예외 처리해 두면 편하다.&lt;/p&gt;
&lt;div class=&quot;codecard&quot;&gt;
&lt;div class=&quot;head&quot;&gt;Vosk + Tello 제어 &amp;mdash; 핵심 구조&lt;/div&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;import sys, json, queue
import sounddevice as sd
from vosk import Model, KaldiRecognizer
from djitellopy import Tello

SPEED = 30
MODEL_PATH = &quot;model&quot;   # 또는 &quot;vosk_model&quot;

tello = Tello()
try:
    tello.connect()
    tello.streamoff()
    print(f&quot;[SUCCESS] Battery: {tello.get_battery()}%&quot;)
except Exception as e:
    print(f&quot;[WARN] Tello connection failed: {e}&quot;)
    tello = None

q = queue.Queue()
def callback(indata, frames, time, status):
    if status: print(status, file=sys.stderr)
    q.put(bytes(indata))

def parse_and_execute(text):
    if not text: return
    print(f&quot; &amp;gt;&amp;gt; Heard: '{text}'&quot;)
    if not tello: return
    try:
        if &quot;take off&quot; in text: tello.takeoff()
        elif &quot;land&quot; in text: tello.land()
        elif &quot;up&quot; in text: tello.move_up(SPEED)
        elif &quot;down&quot; in text: tello.move_down(SPEED)
        elif &quot;left&quot; in text: tello.move_left(SPEED)
        elif &quot;right&quot; in text: tello.move_right(SPEED)
        elif &quot;forward&quot; in text: tello.move_forward(SPEED)
        elif &quot;back&quot; in text: tello.move_back(SPEED)
        elif &quot;stop&quot; in text: tello.send_rc_control(0,0,0,0)
        elif &quot;flip&quot; in text: tello.flip_forward()
    except Exception as e: print(f&quot;Command Error: {e}&quot;)

model = Model(MODEL_PATH)
rec = KaldiRecognizer(model, 16000)
with sd.RawInputStream(samplerate=16000, blocksize=8000, dtype='int16', channels=1, callback=callback):
    while True:
        data = q.get()
        if rec.AcceptWaveform(data):
            result = json.loads(rec.Result())
            parse_and_execute(result.get(&quot;text&quot;, &quot;&quot;))&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h2 id=&quot;sec5&quot; class=&quot;sec-title title-26&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;hl-yellow&quot;&gt;5. 실습 2: OpenWakeWord + Vosk (웨이크워드 후 명령)&lt;/span&gt;&lt;/h2&gt;
&lt;h3 class=&quot;sec-subtitle&quot; style=&quot;margin-top: 20px; margin-bottom: 8px; font-weight: bold; font-size: 18px;&quot; data-ke-size=&quot;size23&quot;&gt;5-1. 구성&lt;/h3&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;(1) &lt;b&gt;OpenWakeWord&lt;/b&gt;가 &amp;ldquo;Hey Jarvis&amp;rdquo;를 감지하면 (2) &lt;b&gt;명령 모드&lt;/b&gt;로 전환해 (3) &lt;b&gt;Vosk&lt;/b&gt;가 &amp;ldquo;Take off&amp;rdquo;, &amp;ldquo;Up&amp;rdquo; 등의 명령을 인식하고 Tello를 제어한다. SoundDevice로 16kHz PCM을 받아, 웨이크 워드 구간에는 OpenWakeWord에만 넘기고, 명령 모드일 때만 Vosk에 넘긴다.&lt;/p&gt;
&lt;h3 class=&quot;sec-subtitle&quot; style=&quot;margin-top: 20px; margin-bottom: 8px; font-weight: bold; font-size: 18px;&quot; data-ke-size=&quot;size23&quot;&gt;5-2. 설치 및 모델&lt;/h3&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;pip install sounddevice numpy openwakeword vosk djitellopy&lt;/code&gt;&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;Tello는 Wi‑Fi로 연결되므로, 연결 전에 모델을 미리 받아 두어야 한다. Vosk 모델은 위와 같이 폴더로 준비하고, OpenWakeWord 모델은 라이브러리에서 제공하는 다운로드 함수를 한 번 실행해 받아 둔다.&lt;/p&gt;
&lt;div class=&quot;codecard&quot;&gt;
&lt;div class=&quot;head&quot;&gt;OpenWakeWord 모델 다운로드&lt;/div&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;import openwakeword
openwakeword.utils.download_models()
print(&quot;OpenWakeWord models downloaded!&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h3 class=&quot;sec-subtitle&quot; style=&quot;margin-top: 20px; margin-bottom: 8px; font-weight: bold; font-size: 18px;&quot; data-ke-size=&quot;size23&quot;&gt;5-3. 실행 흐름&lt;/h3&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;웨이크 워드 대기&amp;rdquo;와 &amp;ldquo;명령 청취&amp;rdquo;를 플래그로 구분한다. OpenWakeWord 예측값이 임계값(예: 0.5)을 넘으면 명령 모드로 바꾸고 Vosk 버퍼를 리셋한 뒤, 이후 오디오는 Vosk로만 넘긴다. Vosk가 문장을 완성하면 해당 명령으로 드론을 제어하고, 다시 웨이크 워드 모드로 돌아간다.&lt;/p&gt;
&lt;div class=&quot;codecard&quot;&gt;
&lt;div class=&quot;head&quot;&gt;웨이크 워드 감지 후 Vosk 명령 처리&lt;/div&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;# 웨이크 워드 대기 중일 때: OpenWakeWord로 &quot;hey_jarvis&quot; 감지
# 명령 모드일 때: Vosk로 &quot;take off&quot;, &quot;up&quot; 등 인식 후 실행
if not is_command_mode:
    audio_np = np.frombuffer(data, dtype=np.int16)
    prediction = oww.predict(audio_np)
    if prediction[WAKE_WORD] &amp;gt; 0.5:
        print(&quot;[!] WAKE WORD DETECTED! Speak now...&quot;)
        is_command_mode = True
        v_rec.Reset()
else:
    if v_rec.AcceptWaveform(data):
        result = json.loads(v_rec.Result())
        cmd_text = result.get(&quot;text&quot;, &quot;&quot;)
        if cmd_text:
            execute_command(cmd_text, drone)
            is_command_mode = False
            oww.reset()&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;실행 시 PC를 Tello Wi‑Fi(TELLO-XXXXXX)에 연결한 뒤 프로그램을 켠다. &amp;ldquo;Hey Jarvis&amp;rdquo;라고 부르고, 이어서 &amp;ldquo;Take off&amp;rdquo; 등을 말하면 드론이 반응한다.&lt;/p&gt;
&lt;h2 id=&quot;sec6&quot; class=&quot;sec-title title-26&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;hl-yellow&quot;&gt;6. 실습 3: PocketSphinx + Tello&lt;/span&gt;&lt;/h2&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;PocketSphinx는 오프라인 인식이 가능한 클래식 엔진이다. 설치 시 Windows는 Visual C++ Build Tools, Mac은 Homebrew로 swig&amp;middot;portaudio 등을 설치해야 할 수 있다. SoundDevice로 일정 시간(예: 3.5초) 녹음한 뒤 SpeechRecognition 라이브러리로 PocketSphinx를 호출한다. 짧은 단어(&amp;ldquo;up&amp;rdquo; 등)보다 &amp;ldquo;go up&amp;rdquo;, &amp;ldquo;go down&amp;rdquo;, &amp;ldquo;take off&amp;rdquo;, &amp;ldquo;land&amp;rdquo;처럼 구문으로 매핑하면 인식률이 좋다.&lt;/p&gt;
&lt;div class=&quot;codecard&quot;&gt;
&lt;div class=&quot;head&quot;&gt;PocketSphinx + SoundDevice &amp;mdash; 녹음&amp;middot;인식 루프&lt;/div&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;DURATION = 3.5   # seconds
SAMPLE_RATE = 16000
r = sr.Recognizer()
while True:
    recording = sd.rec(int(DURATION * SAMPLE_RATE), samplerate=SAMPLE_RATE, channels=1, dtype='int16')
    sd.wait()
    audio_data = sr.AudioData(recording.tobytes(), SAMPLE_RATE, 2)
    try:
        command = r.recognize_sphinx(audio_data)
        execute_command(command)   # &quot;take off&quot;, &quot;go up&quot;, &quot;land&quot; 등 매핑
    except sr.UnknownValueError:
        pass&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h2 id=&quot;sec7&quot; class=&quot;sec-title title-26&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;hl-yellow&quot;&gt;7. 실습 4: 데이터셋&amp;middot;CNN 학습&amp;middot;임베디드 추론&lt;/span&gt;&lt;/h2&gt;
&lt;h3 class=&quot;sec-subtitle&quot; style=&quot;margin-top: 20px; margin-bottom: 8px; font-weight: bold; font-size: 18px;&quot; data-ke-size=&quot;size23&quot;&gt;7-1. 데이터셋 준비&lt;/h3&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;드론 명령어를 &lt;b&gt;키워드/클래스&lt;/b&gt;로 구분해 학습하려면, 클래스별 폴더에 1초 길이 WAV(16kHz 또는 8kHz, 모노)를 넣는다. 예: takeoff, land, up, down, left, right, 배경 소음용 폴더 등. 직접 녹음하거나, &lt;b&gt;Google Speech Commands v0.02&lt;/b&gt;를 받아 필요한 단어(on, up, down, left, right, off, 배경 소음 등)만 추출해 쓸 수 있다. UAV 명령용 데이터셋은 &lt;a href=&quot;https://drive.google.com/file/d/1_OWPkUtMUUn8OWK3O5omttjYejKZwstd/view?usp=drive_link&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Command Dataset 링크&lt;/a&gt;에서 받을 수 있다. &amp;ldquo;Tello&amp;rdquo;처럼 공개 데이터셋에 없는 단어는 gTTS&amp;middot;pydub로 1초 WAV를 만들어 넣을 수 있다.&lt;/p&gt;
&lt;h3 class=&quot;sec-subtitle&quot; style=&quot;margin-top: 20px; margin-bottom: 8px; font-weight: bold; font-size: 18px;&quot; data-ke-size=&quot;size23&quot;&gt;7-2. MFCC + 2D CNN 학습&lt;/h3&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;librosa로 오디오에서 &lt;b&gt;MFCC&lt;/b&gt;를 뽑고, 2D CNN(Conv2D + MaxPooling + Dense)으로 명령어를 분류한다. 샘플레이트(16kHz), 1초 길이, MFCC 개수(13) 등을 맞춰 두고, 클래스별 폴더를 돌며 로드한다. 배경 소음은 긴 파일을 1초 단위로 잘라 여러 샘플로 쓴다. 학습 후 모델을 저장하고, 정확도&amp;middot;F1&amp;middot;혼동 행렬&amp;middot;분류 리포트로 평가한다. 임베디드용으로 쓸 때는 8kHz&amp;middot;1초 raw 또는 MFCC로 입력을 맞춘 뒤 TFLite로 Full Integer 양자화해 내보내면 된다.&lt;/p&gt;
&lt;h3 class=&quot;sec-subtitle&quot; style=&quot;margin-top: 20px; margin-bottom: 8px; font-weight: bold; font-size: 18px;&quot; data-ke-size=&quot;size23&quot;&gt;7-3. TFLite&amp;middot;라즈베리파이&amp;middot;M5Core2 요약&lt;/h3&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;전체 흐름은 다음과 같다.&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;(1) &lt;b&gt;Python&lt;/b&gt;: 8kHz&amp;middot;1초 raw 오디오를 입력으로 하는 1D CNN을 학습하고, TFLite Full Integer 양자화로 모델을 내보낸다.&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;(2) &lt;b&gt;M5Core2(ESP32)&lt;/b&gt;: 내보낸 모델을 C 배열로 변환해 TFLite Micro로 올리면, 보드 마이크로 1초 녹음 후 추론할 수 있다.&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;(3) &lt;b&gt;라즈베리파이 Zero 2 W&lt;/b&gt;: TFLite 런타임과 sounddevice로 8kHz&amp;middot;1초 캡처 후, 모델이 Int8 입력이면 Float32를 Int8로 바꿔 추론하고, 인식된 라벨에 따라 Tello 제어를 붙일 수 있다. 라즈베리파이에서 마이크가 안 잡히면 사용 중인 오디오 장치 번호를 확인한 뒤, 해당 장치를 지정해 녹음하면 된다.&lt;/p&gt;
&lt;h2 id=&quot;sec8&quot; class=&quot;sec-title title-26&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;hl-yellow&quot;&gt;8. 정리&lt;/span&gt;&lt;/h2&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;이번 수업에서는 &lt;b&gt;실시간 드론 제어&lt;/b&gt;에 적합한 &lt;b&gt;오프라인 음성 인식&lt;/b&gt;을 다뤘다. Vosk는 지연이 적고 경량이라 &amp;ldquo;Stop&amp;rdquo; 같은 즉각 반응이 필요할 때 Whisper보다 유리하다. 마이크 입력은 PyAudio 대신 &lt;b&gt;SoundDevice&lt;/b&gt;를 쓰면 Mac&amp;middot;Windows에서 같은 방식으로 동작한다. 실습 1에서는 Vosk만으로 Tello를 제어하고, 실습 2에서는 OpenWakeWord(&amp;ldquo;Hey Jarvis&amp;rdquo;)를 먼저 듣고 나서 Vosk로 명령을 받는 방식, 실습 3에서는 PocketSphinx와 SoundDevice로 고정 길이 녹음 후 인식하는 방식을 사용했다. 실습 4에서는 Google Speech Commands에서 필요한 단어만 추출&amp;middot;준비하고, Tello 등 커스텀 단어는 음성 합성으로 보강한 뒤, MFCC와 2D CNN으로 학습하고, TFLite로 내보내서 라즈베리파이&amp;middot;M5Core2 같은 보드에서 추론하는 흐름까지 정리했다.&lt;/p&gt;
&lt;style&gt;
    #post-root a { color:#495057 !important; text-decoration:none !important; }
    #post-root a:hover { color:#212529 !important; border-bottom:1px solid #ced4da; }
    #post-root a:visited { color:#666 !important; }

    .sec-title { display:block; margin:28px 0 12px; font-weight:800; line-height:1.35; }
    .sec-title &gt; .hl-yellow {
      background:linear-gradient(transparent 65%, #fff3bf 65%);
      padding:0 .12em;
    }
    .title-26 { font-size:26px; }

    .para { margin:10px 0 14px; color:#343a40; line-height:1.9; }

    .toc {
      border:1px solid #e9ecef;
      border-radius:10px;
      padding:12px 14px;
      margin:18px 0 24px;
      background:#f8f9fa;
    }

    .codecard {
      background:#f8f9fa;
      border:1px solid #e9ecef;
      border-radius:8px;
      padding:16px;
      margin:16px 0;
      overflow-x:auto;
    }

    .codecard .head {
      font-weight:700;
      color:#212529;
      margin-bottom:10px;
      font-size:15px;
    }

    .codecard pre { margin:0; padding:0; background:transparent; border:none; }
    .codecard code {
      font-family:'Courier New', monospace;
      font-size:13px;
      line-height:1.6;
      color:#212529;
    }
    &lt;/style&gt;
&lt;/div&gt;</description>
      <category>UNLV</category>
      <author>빡성</author>
      <guid isPermaLink="true">https://smp0417.tistory.com/74</guid>
      <comments>https://smp0417.tistory.com/74#entry74comment</comments>
      <pubDate>Fri, 20 Feb 2026 03:18:26 +0900</pubDate>
    </item>
    <item>
      <title>[UNLV] - OpenCV와 YOLO: 컴퓨터 비전 실습(10일차)</title>
      <link>https://smp0417.tistory.com/73</link>
      <description>&lt;!-- ========================= --&gt;
&lt;div id=&quot;post-root&quot;&gt;
&lt;div class=&quot;toc&quot;&gt;
&lt;div style=&quot;font-weight: 800; margin-bottom: 8px;&quot;&gt;&amp;nbsp; &amp;nbsp; 목차&lt;/div&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#sec1&quot;&gt;수업 개요&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#sec2&quot;&gt;OpenCV 한눈에 보기&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#sec3&quot;&gt;디지털 이미지와 필터링&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#sec4&quot;&gt;엣지 검출부터 객체 인식까지&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#sec5&quot;&gt;움직임 감지, 캘리브레이션, YOLO&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#sec6&quot;&gt;실습 1: OpenCV 기초&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#sec7&quot;&gt;실습 2: RPi 카메라 &amp;rarr; PC 전송&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#sec8&quot;&gt;실습 3: 캡처 도구와 YOLO 분석&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#sec9&quot;&gt;실습 4: YOLO로 결과 다루기&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#sec10&quot;&gt;정리&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;h2 id=&quot;sec1&quot; class=&quot;sec-title title-26&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;hl-yellow&quot;&gt;1. 수업 개요&lt;/span&gt;&lt;/h2&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;이번 수업은 &lt;b&gt;OpenCV&lt;/b&gt;와 &lt;b&gt;YOLO&lt;/b&gt;로 컴퓨터 비전을 배우는 시간이었다. OpenCV는 카메라&amp;middot;이미지를 다루는 대표 라이브러리이고, YOLO는 영상 속에서 사물을 실시간으로 찾아 주는 AI 모델이다. 이론에서는 이미지가 어떻게 만들어지고, 노이즈를 줄이는 필터링, 경계선(엣지)을 뽑는 방법, 움직임 감지, YOLO의 특징까지 다뤘다. 실습에서는 Python으로 이미지 열기&amp;middot;색 보정&amp;middot;원 검출, 라즈베리파이 카메라에서 PC로 사진&amp;middot;영상 보내기, 웹캠으로 촬영한 뒤 YOLO로 검출&amp;middot;분류&amp;middot;세그멘테이션을 해 보며, 결과 데이터를 코드에서 어떻게 쓰는지까지 진행했다.&lt;/p&gt;
&lt;h2 id=&quot;sec2&quot; class=&quot;sec-title title-26&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;hl-yellow&quot;&gt;2. OpenCV 한눈에 보기&lt;/span&gt;&lt;/h2&gt;
&lt;h3 class=&quot;sec-subtitle&quot; style=&quot;margin-top: 20px; margin-bottom: 8px; font-weight: bold; font-size: 18px;&quot; data-ke-size=&quot;size23&quot;&gt;정의와 역사&lt;/h3&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;OpenCV&lt;/b&gt;는 오픈소스 &lt;b&gt;컴퓨터 비전&amp;middot;머신러닝 라이브러리&lt;/b&gt;다. 쉽게 말하면, 카메라&amp;middot;이미지를 읽고 처리하는 도구 모음이다. &lt;b&gt;실시간 컴퓨터 비전(real-time computer vision)&lt;/b&gt;에 맞춰 설계되어, 웹캠&amp;middot;드론&amp;middot;로봇처럼 &amp;ldquo;보는&amp;rdquo; 프로그램을 만들 때 표준으로 쓴다.&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;역사&lt;/b&gt;: 1999년 Intel에서 처음 개발했고, 2008년부터 개인용 로보틱스로 유명했던 &lt;b&gt;Willow Garage&lt;/b&gt;가 지원했다. 이후 전 세계 커뮤니티가 유지&amp;middot;보수하며 지금까지 이어지고 있다.&lt;/p&gt;
&lt;h3 class=&quot;sec-subtitle&quot; style=&quot;margin-top: 20px; margin-bottom: 8px; font-weight: bold; font-size: 18px;&quot; data-ke-size=&quot;size23&quot;&gt;플랫폼&amp;middot;라이선스&amp;middot;언어&lt;/h3&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;플랫폼&lt;/b&gt;: Windows, Linux, Android, MacOS, iOS 등 주요 OS를 모두 지원하는 &lt;b&gt;크로스 플랫폼&lt;/b&gt;이다. 한 번 배우면 여러 기기에서 쓸 수 있다.&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;라이선스&lt;/b&gt;: &lt;b&gt;BSD 라이선스&lt;/b&gt;로 무료 제공되며, 상업적 용도로도 자유롭게 사용할 수 있다. 회사 제품에 넣어도 라이선스 비용이 들지 않는다.&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;기반 언어&amp;middot;API&lt;/b&gt;: 핵심은 &lt;b&gt;C/C++&lt;/b&gt;로 작성되어 속도가 빠르다. 현재는 &lt;b&gt;Python&lt;/b&gt; 바인딩이 가장 많이 쓰이고, Java, MATLAB도 지원한다. GPU 가속이 필요하면 &lt;b&gt;NVIDIA CUDA&lt;/b&gt;, &lt;b&gt;OpenCL&lt;/b&gt; 인터페이스를 쓸 수 있다. 슬라이드 기준 최신 안정 버전은 4.13.0으로 소개된 바 있다(실제 버전은 배포 시점에 따라 다를 수 있음).&lt;/p&gt;
&lt;h3 class=&quot;sec-subtitle&quot; style=&quot;margin-top: 20px; margin-bottom: 8px; font-weight: bold; font-size: 18px;&quot; data-ke-size=&quot;size23&quot;&gt;알고리즘 개요&lt;/h3&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;OpenCV에는 &lt;b&gt;2,000개 이상&lt;/b&gt;의 알고리즘이 포함되어 있다. 주요 기능만 나열하면 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;일반 영상 처리, 이미지 피라미드(다중 해상도 표현)&lt;/li&gt;
&lt;li&gt;세그멘테이션(영역 분할), 기하학적 기술자(Geometric descriptors)&amp;middot;특징점(Features) 추출&lt;/li&gt;
&lt;li&gt;카메라 캘리브레이션, 스테레오&amp;middot;3D 비전&lt;/li&gt;
&lt;li&gt;머신러닝(얼굴 검출, 객체 인식), 트래킹, 행렬 연산&lt;/li&gt;
&lt;/ul&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;딥러닝과의 관계&lt;/b&gt;: OpenCV는 단독으로도 쓰이지만, TensorFlow, PyTorch, Caffe, Keras 같은 딥러닝 프레임워크에서 &lt;b&gt;이미지 전처리&amp;middot;로딩&lt;/b&gt;용으로 핵심적으로 사용된다. 즉, &amp;ldquo;영상 입력&amp;rdquo;을 다루는 단계에서 OpenCV가 자주 쓰인다.&lt;/p&gt;
&lt;h3 class=&quot;sec-subtitle&quot; style=&quot;margin-top: 20px; margin-bottom: 8px; font-weight: bold; font-size: 18px;&quot; data-ke-size=&quot;size23&quot;&gt;주요 활용 분야&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;IT&amp;middot;서비스&lt;/b&gt;: 구글 맵, 스트리트 뷰, 구글 어스 등에서 이미지 처리에 사용된다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;안전&amp;middot;보안&lt;/b&gt;: 댐, 광산, 수영장 등의 안전 모니터링, 보안 시스템.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;산업&amp;middot;연구&lt;/b&gt;: 학술&amp;middot;산업 연구, 공장 생산 라인의 머신 비전 검사 시스템.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;엔터테인먼트&amp;middot;로봇&lt;/b&gt;: 영화 모션 캡처(Structure from Motion), 로보틱스 등 사실상 &amp;ldquo;눈&amp;rdquo;이 필요한 모든 분야에 쓰인다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 class=&quot;sec-subtitle&quot; style=&quot;margin-top: 20px; margin-bottom: 8px; font-weight: bold; font-size: 18px;&quot; data-ke-size=&quot;size23&quot;&gt;공식 리소스와 참고 문헌&lt;/h3&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;공식 사이트&lt;/b&gt;: 문서&amp;middot;튜토리얼&amp;middot;예제는 &lt;b&gt;docs.opencv.org&lt;/b&gt;, Q&amp;amp;A 포럼은 &lt;b&gt;answers.opencv.org&lt;/b&gt;에서 이용할 수 있다. 공식 홈페이지 리소스 탭에서 책, 논문, 유용한 링크를 확인할 수 있다.&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;참고 문헌&lt;/b&gt;: 입문&amp;middot;실무용으로 OpenCV Essentials, OpenCV Starter, Learning Image Processing with OpenCV 등(Packt Publishing 위주)이 있다. 언어별로는 Python용 OpenCV Computer Vision with Python, Java용 OpenCV 3.0 Computer Vision with Java, .NET용 Emgu CV Essentials(C# 등)가 있고, 모바일&amp;middot;게임 쪽에서는 iOS/Android 전용 가이드, Raspberry Pi 활용, Unity 게임 엔진 연동을 다룬 서적도 있다.&lt;/p&gt;
&lt;h3 class=&quot;sec-subtitle&quot; style=&quot;margin-top: 20px; margin-bottom: 8px; font-weight: bold; font-size: 18px;&quot; data-ke-size=&quot;size23&quot;&gt;주요 모듈 구성 (OpenCV Modules)&lt;/h3&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;라이브러리는 기능별로 모듈이 나뉘어 있다. 필요한 기능만 골라 쓸 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Core&lt;/b&gt;: 행렬 연산 및 기본 데이터 구조. 이미지가 숫자 배열로 어떻게 저장되는지 다룬다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Imgproc&lt;/b&gt;: 이미지 필터링, 기하학적 변환 등 핵심 이미지 처리. 블러, 엣지, 형태학 연산 등이 여기 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Highgui&lt;/b&gt;: 이미지&amp;middot;동영상 읽기/쓰기, 간단한 창(UI) 생성. &lt;code&gt;imread&lt;/code&gt;, &lt;code&gt;imshow&lt;/code&gt; 같은 함수가 여기 속한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Features2D&lt;/b&gt;: 특징점 검출 및 매칭. 다른 이미지 간에 같은 점을 찾을 때 쓴다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Calib3d&lt;/b&gt;: 카메라 교정(캘리브레이션) 및 3D 재구성.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;ML&lt;/b&gt;: 통계적 머신러닝(클러스터링, 부스팅, SVM 등). 딥러닝이 아닌 전통적인 ML 도구다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Objdetect&lt;/b&gt;: 얼굴&amp;middot;물체 검출을 위한 미리 만들어진 검출기.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;sec3&quot; class=&quot;sec-title title-26&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;hl-yellow&quot;&gt;3. 디지털 이미지와 필터링&lt;/span&gt;&lt;/h2&gt;
&lt;h3 class=&quot;sec-subtitle&quot; style=&quot;margin-top: 20px; margin-bottom: 8px; font-weight: bold; font-size: 18px;&quot; data-ke-size=&quot;size23&quot;&gt;디지털 이미지 획득 (Digital Image Acquisition)&lt;/h3&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;디지털 이미지가 만들어지는 &lt;b&gt;물리적 과정&lt;/b&gt;을 단계별로 보면 다음과 같다. 카메라나 스캐너가 &amp;ldquo;보는&amp;rdquo; 방식을 이해하면, 왜 노이즈나 왜곡이 생기는지 파악하는 데 도움이 된다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;에너지원(Illumination source)&lt;/b&gt;: 빛과 같은 에너지원이 있어야 한다. 어두우면 신호가 약해져 노이즈가 심해진다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;장면 요소(Scene element)&lt;/b&gt;: 빛을 반사하거나 투과시키는 물체. 이게 우리가 찍는 &amp;ldquo;대상&amp;rdquo;이다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;이미징 시스템(Imaging system)&lt;/b&gt;: 렌즈와 센서가 빛을 모아 전기 신호로 바꾼다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;이미지 평면 투영(Projection)&lt;/b&gt;: 모인 빛이 카메라 내부의 이미지 평면에 맺힌다. 여기서 왜곡이 생길 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;디지털화(Digitized image)&lt;/b&gt;: 연속적인 아날로그 신호를 격자 형태의 픽셀 데이터로 샘플링해 최종 이미지를 만든다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 class=&quot;sec-subtitle&quot; style=&quot;margin-top: 20px; margin-bottom: 8px; font-weight: bold; font-size: 18px;&quot; data-ke-size=&quot;size23&quot;&gt;디지털 카메라의 주요 문제점 (Digital Camera Issues)&lt;/h3&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;위 과정에서 발생할 수 있는 &lt;b&gt;기술적 결함&lt;/b&gt;이다. 알고 있으면 &amp;ldquo;왜 화질이 나쁜가&amp;rdquo;를 원인별로 이해할 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;노이즈(Noise)&lt;/b&gt;: 저조도 환경에서 주로 발생한다. 감도(ISO)를 올리면 밝아지지만 노이즈도 함께 늘어난다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;색상 결함(Color)&lt;/b&gt;: 베이어 패턴(Bayer pattern) 센서에서 나오는 &lt;b&gt;색수차(Color fringing)&lt;/b&gt; &amp;mdash; 경계 부근에서 색이 번져 보이는 현상.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;블루밍(Blooming)&lt;/b&gt;: 한 픽셀에 전하가 너무 많이 쌓이면 인접 픽셀로 넘쳐서 밝은 부분이 퍼져 보인다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;카메라 내부 처리(In-camera processing)&lt;/b&gt;: 과도한 샤프닝(선명화)을 하면 물체 주변에 &lt;b&gt;후광(Halo)&lt;/b&gt;이 생길 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;압축(Compression)&lt;/b&gt;: JPEG 등으로 압축하면 &lt;b&gt;블록 현상(Blocking artifacts)&lt;/b&gt; &amp;mdash; 네모 칸이 보이는 현상이 나타날 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 class=&quot;sec-subtitle&quot; style=&quot;margin-top: 20px; margin-bottom: 8px; font-weight: bold; font-size: 18px;&quot; data-ke-size=&quot;size23&quot;&gt;이미지 필터링의 원리 (Image Filtering)&lt;/h3&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;공간 필터링(Spatial filtering)&lt;/b&gt;은 &amp;ldquo;각 픽셀 값을 주변 픽셀과 일정한 규칙으로 계산해 바꾸는 것&amp;rdquo;이다. 이때 쓰는 규칙을 &lt;b&gt;필터 마스크&lt;/b&gt;라고 부른다. 보통 &lt;b&gt;3&amp;times;3&lt;/b&gt; 크기 마스크를 많이 쓰며, 각 위치에 계수(가중치 w)를 두고 곱한 뒤 합산한다.&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;중간값 필터(Median Filter)&lt;/b&gt;: 마스크 안의 픽셀 값들을 &lt;b&gt;크기순으로 나열&lt;/b&gt;한 뒤, 그중 &lt;b&gt;중간값&lt;/b&gt;을 선택해 해당 픽셀의 새 값으로 쓴다. 점처럼 튀는 노이즈(솔트-앤-페퍼)를 잘 제거하면서도 선이나 모서리는 비교적 잘 보존한다.&lt;/p&gt;
&lt;h3 class=&quot;sec-subtitle&quot; style=&quot;margin-top: 20px; margin-bottom: 8px; font-weight: bold; font-size: 18px;&quot; data-ke-size=&quot;size23&quot;&gt;필터링 응용 예: 허블 망원경 이미지&lt;/h3&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;실제 활용 예로 허블 망원경의 성단 이미지를 들 수 있다. 먼저 &lt;b&gt;15&amp;times;15 평균 마스크(Averaging mask)&lt;/b&gt;를 적용해 이미지를 부드럽게 만들고, 그다음 &lt;b&gt;이진화(Thresholding)&lt;/b&gt; &amp;mdash; 정해진 임계값을 기준으로 밝기를 잘라서 &amp;mdash; 적용하면 주요 천체(객체)만 흰색으로 분리해 낼 수 있다.&lt;/p&gt;
&lt;h3 class=&quot;sec-subtitle&quot; style=&quot;margin-top: 20px; margin-bottom: 8px; font-weight: bold; font-size: 18px;&quot; data-ke-size=&quot;size23&quot;&gt;노이즈 제거 성능 비교 (Filtering for Denoising)&lt;/h3&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;솔트-앤-페퍼 노이즈를 제거할 때, &lt;b&gt;3&amp;times;3 평균 필터&lt;/b&gt;를 쓰면 노이즈가 흐려지긴 하지만 잔상이 남고 디테일이 뭉개질 수 있다. 반면 &lt;b&gt;3&amp;times;3 중간값 필터&lt;/b&gt;는 노이즈를 거의 깔끔히 제거하면서도 회로 기판 같은 디테일을 잘 보존한다. 그래서 점 노이즈 제거에는 중간값 필터가 더 적합하다고 배우면 된다.&lt;/p&gt;
&lt;h2 id=&quot;sec4&quot; class=&quot;sec-title title-26&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;hl-yellow&quot;&gt;4. 엣지 검출부터 객체 인식까지&lt;/span&gt;&lt;/h2&gt;
&lt;h3 class=&quot;sec-subtitle&quot; style=&quot;margin-top: 20px; margin-bottom: 8px; font-weight: bold; font-size: 18px;&quot; data-ke-size=&quot;size23&quot;&gt;엣지 검출 (Edge Detection)&lt;/h3&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;엣지 검출은 2D 이미지를 &lt;b&gt;곡선의 집합&lt;/b&gt;으로 바꾸는 과정이다. 장면의 핵심적인 특징(Salient features)만 추출하므로, 원본 픽셀 데이터보다 훨씬 &lt;b&gt;컴팩트한 정보&lt;/b&gt;로 압축된다. 즉, &amp;ldquo;경계선만 남긴 그림&amp;rdquo;을 만든다고 보면 된다.&lt;/p&gt;
&lt;h3 class=&quot;sec-subtitle&quot; style=&quot;margin-top: 20px; margin-bottom: 8px; font-weight: bold; font-size: 18px;&quot; data-ke-size=&quot;size23&quot;&gt;최적의 엣지 검출: Canny 알고리즘&lt;/h3&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;Canny가 제시한 &amp;ldquo;좋은 엣지 검출기&amp;rdquo;의 조건은 세 가지다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;좋은 검출(Good Detection)&lt;/b&gt;: 노이즈가 아닌 &lt;b&gt;실제 엣지&lt;/b&gt;에만 반응해야 한다. 거짓 양성을 줄이는 것.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;좋은 위치 선정(Good Localization)&lt;/b&gt;: 검출된 엣지가 실제 경계 위치와 매우 가깝게 나와야 한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;단일 응답(Single Response)&lt;/b&gt;: 하나의 엣지에는 선이 하나만 나와야 한다. 같은 경계가 두 줄로 나뉘어 나오면 안 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;참고로 스무딩(Smoothing)을 강하게 하면 노이즈는 줄어들어 검출이 좋아지지만, 경계 위치 정확도는 떨어지는 &lt;b&gt;트레이드오프&lt;/b&gt;가 있다. 그래서 Canny에서는 이 둘을 절충한 파라미터를 쓰게 된다.&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Canny 엣지 검출 단계&lt;/b&gt;: (1) 원본 이미지 &amp;rarr; (2) &lt;b&gt;그래디언트 노름(Norm of gradient)&lt;/b&gt;: 픽셀별 밝기 변화율을 계산 &amp;rarr; (3) &lt;b&gt;임계값 적용(Thresholding)&lt;/b&gt;: 유효한 엣지만 남기고 나머지 제거 &amp;rarr; (4) &lt;b&gt;가늘게 만들기(Thinning)&lt;/b&gt;: 엣지 두께를 1픽셀로 줄여 정교하게 만든다.&lt;/p&gt;
&lt;h3 class=&quot;sec-subtitle&quot; style=&quot;margin-top: 20px; margin-bottom: 8px; font-weight: bold; font-size: 18px;&quot; data-ke-size=&quot;size23&quot;&gt;윤곽선 추출 예시 (Contours)&lt;/h3&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;데이터 압축&lt;/b&gt; 측면에서 보면, 180,000개의 점으로 이루어진 이미지를 윤곽선 추출로 1,800개 미만으로 줄이고, 다시 &lt;b&gt;근사화(Approximation)&lt;/b&gt;를 하면 180개 미만으로 비약적으로 줄일 수 있다. 복잡한 장면에서도 640&amp;times;480 해상도 기준 약 &lt;b&gt;70 FPS&lt;/b&gt;의 빠른 속도로 동작할 수 있다는 뜻이다.&lt;/p&gt;
&lt;h3 class=&quot;sec-subtitle&quot; style=&quot;margin-top: 20px; margin-bottom: 8px; font-weight: bold; font-size: 18px;&quot; data-ke-size=&quot;size23&quot;&gt;객체 인식 (Object Recognition)&lt;/h3&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;추출된 &lt;b&gt;윤곽선(Contours)&lt;/b&gt;과 &lt;b&gt;특징점&lt;/b&gt; 데이터를 기하학적으로 분석하면, 전화기, 신발, 곰인형 같은 특정 객체를 식별&amp;middot;분류할 수 있다. 이게 전통적인 컴퓨터 비전에서의 &lt;b&gt;객체 인식&lt;/b&gt; 단계다. 최근에는 딥러닝(YOLO 등)으로 이 단계를 대체하는 경우가 많지만, 원리를 이해하려면 윤곽선&amp;middot;특징점이 어떻게 쓰이는지 알아두는 것이 좋다.&lt;/p&gt;
&lt;h2 id=&quot;sec5&quot; class=&quot;sec-title title-26&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;hl-yellow&quot;&gt;5. 움직임 감지, 캘리브레이션, YOLO&lt;/span&gt;&lt;/h2&gt;
&lt;h3 class=&quot;sec-subtitle&quot; style=&quot;margin-top: 20px; margin-bottom: 8px; font-weight: bold; font-size: 18px;&quot; data-ke-size=&quot;size23&quot;&gt;인간 포즈&amp;middot;수어 추정 (Human Pose / Sign Estimation)&lt;/h3&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;이미지로부터 사람의 &lt;b&gt;관절 위치&lt;/b&gt;나 &lt;b&gt;손동작(수어)&lt;/b&gt;을 파악하는 기술이다. OpenCV&amp;middot;관련 라이브러리에서는 보통 다음 세 가지 정보를 활용한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Depth(깊이)&lt;/b&gt;: 8UC1 형식의 깊이 데이터로 손&amp;middot;몸의 형태를 파악한다. 카메라로부터의 거리 정보가 있으면 2D보다 정확한 포즈 추정이 가능하다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Label(라벨링)&lt;/b&gt;: 손가락 마디, 손바닥 등 각 부분을 색상 코드로 구분해 &lt;b&gt;세그멘테이션&lt;/b&gt;한다. &amp;ldquo;어디가 손가락인지&amp;rdquo; 영역별로 표시하는 단계다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Model(모델링)&lt;/b&gt;: 추정된 데이터를 실제 3D 손&amp;middot;몸 모델과 매칭해 실시간 포즈를 재구성한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 class=&quot;sec-subtitle&quot; style=&quot;margin-top: 20px; margin-bottom: 8px; font-weight: bold; font-size: 18px;&quot; data-ke-size=&quot;size23&quot;&gt;움직임 감지 기법 (Motion Detection)&lt;/h3&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;영상 안에서 &amp;ldquo;움직이는 객체&amp;rdquo;를 찾아내는 대표적인 방법 세 가지다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;프레임 차이법(Frame Differencing)&lt;/b&gt;: 현재 프레임에서 이전 프레임을 빼는 방식이다. 수식으로는 |Frame_t &amp;minus; Frame_{t&amp;minus;1}|. 구현이 매우 빠르고 간단하지만, 노이즈에 취약하고 객체가 멈추면 차이가 0이 되어 감지하지 못한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;배경 차분법(Background Subtraction)&lt;/b&gt;: 시간에 따라 &amp;ldquo;움직이지 않는 배경&amp;rdquo; 모델을 학습해 두고, 현재 프레임에서 배경을 빼서 움직이는 &lt;b&gt;전경(Foreground)&lt;/b&gt;만 분리한다. 조명 변화나 그림자에 상대적으로 강하다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;광학 흐름(Optical Flow)&lt;/b&gt;: 영상의 각 픽셀에 대해 &amp;ldquo;속도와 방향 벡터&amp;rdquo;를 계산하는 고급 기법이다. 데이터는 풍부하지만 연산 비용이 크다. 흐름을 조금만 쓰고 싶을 때는 &lt;b&gt;희소(Sparse)&lt;/b&gt; 광학 흐름, 전체 픽셀을 쓰면 &lt;b&gt;밀집(Dense)&lt;/b&gt; 광학 흐름이라고 부른다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;광학 흐름의 원리&lt;/b&gt;: &amp;ldquo;같은 점의 밝기는 시간이 조금 지나도 변하지 않는다&amp;rdquo;는 가정(밝기 불변성)을 쓴다. 수식으로는 I(x+dx, y+dy, t+dt) = I(x, y, t). OpenCV에서는 희소 광학 흐름에 &lt;code&gt;calcOpticalFlowPyrLK()&lt;/code&gt;, 밀집 광학 흐름에 &lt;code&gt;calcOpticalFlowFarneback()&lt;/code&gt;를 주로 사용한다.&lt;/p&gt;
&lt;h3 class=&quot;sec-subtitle&quot; style=&quot;margin-top: 20px; margin-bottom: 8px; font-weight: bold; font-size: 18px;&quot; data-ke-size=&quot;size23&quot;&gt;단일 카메라 캘리브레이션 (Single Camera Calibration)&lt;/h3&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;렌즈의 &lt;b&gt;왜곡&lt;/b&gt;을 보정하고 카메라의 내부&amp;middot;외부 특성을 파악하는 과정이다. &lt;b&gt;체커보드(Checkerboard)&lt;/b&gt;를 카메라 앞에 몇 초간 들고 있는 것만으로 보정에 필요한 점들을 자동으로 찾을 수 있다. 결과로 카메라의 내부/외부 파라미터를 계산하고, 왜곡된 이미지를 평평하게 보정한 &lt;b&gt;Un-distorted image&lt;/b&gt;를 얻을 수 있으며, 이를 바탕으로 3D 뷰&amp;middot;측정을 할 수 있다.&lt;/p&gt;
&lt;h3 class=&quot;sec-subtitle&quot; style=&quot;margin-top: 20px; margin-bottom: 8px; font-weight: bold; font-size: 18px;&quot; data-ke-size=&quot;size23&quot;&gt;OpenCV 머신러닝 라이브러리 (MLL)&lt;/h3&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;OpenCV 내부에 들어 있는 &lt;b&gt;전통적인 머신러닝&lt;/b&gt; 도구들이다. 딥러닝이 아닌, 통계&amp;middot;기하 기반 ML이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;분류&amp;middot;회귀&lt;/b&gt;: FLANN, Random Trees, SVM, Naive Bayes, MLP(역전파), Boosting 등.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;클러스터링&lt;/b&gt;: K-Means, EM(Expectation-Maximization) 알고리즘.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;검증&lt;/b&gt;: 교차 검증(Cross validation), 부트스트래핑(Bootstrapping) 등 성능 평가&amp;middot;튜닝에 쓰인다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 class=&quot;sec-subtitle&quot; style=&quot;margin-top: 20px; margin-bottom: 8px; font-weight: bold; font-size: 18px;&quot; data-ke-size=&quot;size23&quot;&gt;YOLO: 정의&amp;middot;발전&amp;middot;장단점&amp;middot;활용&lt;/h3&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;YOLO&lt;/b&gt;는 &amp;ldquo;You Only Look Once&amp;rdquo;의 약자로, 현재 가장 널리 쓰이는 &lt;b&gt;실시간 객체 인식&lt;/b&gt; 시스템이다. 이미지를 여러 단계로 스캔하는 기존 방식과 달리, 객체 검출을 &lt;b&gt;단일 회귀 문제(Single regression problem)&lt;/b&gt;로 취급한다. 즉, 이미지를 &lt;b&gt;딱 한 번만&lt;/b&gt; 보고 &amp;ldquo;무엇이 있는지(Classification)&amp;rdquo;와 &amp;ldquo;어디에 있는지(Bounding Boxes)&amp;rdquo;를 동시에 예측한다. 추론 속도가 매우 빨라 실시간 영상 처리의 표준으로 자리 잡았다.&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;YOLO의 발전 과정 (Evolution of YOLO)&lt;/b&gt;: v1(2015)은 실시간 인식을 가능하게 한 돌파구였으나 작은 객체 인식에 취약했다. v2&amp;middot;v3에서는 &lt;b&gt;앵커 박스(Anchor Boxes)&lt;/b&gt;와 &lt;b&gt;다중 스케일 검출&lt;/b&gt;을 도입해 성능을 개선했다. v4&amp;middot;v5에서는 &lt;b&gt;모자이크 증강(Mosaic augmentation)&lt;/b&gt; 등 속도 최적화와 PyTorch 구현에 집중했다. v8(2023) 이후는 &lt;b&gt;앵커 프리(Anchor-Free)&lt;/b&gt; 방식과 포즈 추정, 세그멘테이션 기능을 공식 지원하며 현재의 표준이 되었다.&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;장점&lt;/b&gt;: 현대 GPU에서 140 FPS 이상의 속도, 이미지 전체를 한꺼번에 보기 때문에 배경을 객체로 오인하는 오류가 적고, 검출&amp;middot;세그멘테이션&amp;middot;포즈 등 다양한 API를 한 번에 지원한다. &lt;b&gt;단점&lt;/b&gt;: 새 떼처럼 작고 빽빽하게 모여 있는 객체를 구분할 때는 Faster R-CNN 같은 2단계 검출기보다 다소 약할 수 있다.&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;실생활 활용 사례 (YOLO Real-World Applications)&lt;/b&gt;: 자율 주행에서 보행자&amp;middot;신호등을 밀리초 단위로 감지하고, 보안&amp;middot;감시에서 침입자 감지&amp;middot;군중 밀집도 계산에 쓰인다. 리테일 자동 계산, 의료 영상의 종양 감지, 드론을 이용한 농작물 모니터링 등 광범위하게 활용된다.&lt;/p&gt;
&lt;h2 id=&quot;sec6&quot; class=&quot;sec-title title-26&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;hl-yellow&quot;&gt;6. 실습 1: OpenCV 기초&lt;/span&gt;&lt;/h2&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;여기서는 OpenCV로 이미지를 열고, 색을 보정해서 보여 주고, 원을 찾아 그리는 것까지 해 본다. 먼저 &lt;code&gt;pip install opencv-python matplotlib numpy&lt;/code&gt;로 라이브러리를 설치한 뒤 진행하면 된다.&lt;/p&gt;
&lt;h3 class=&quot;sec-subtitle&quot; style=&quot;margin-top: 20px; margin-bottom: 8px; font-weight: bold; font-size: 18px;&quot; data-ke-size=&quot;size23&quot;&gt;6-1. 이미지 로드와 검사&lt;/h3&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;OpenCV는 이미지를 &lt;b&gt;NumPy 배열&lt;/b&gt;로 읽는다. 그래서 &lt;code&gt;shape&lt;/code&gt;, &lt;code&gt;dtype&lt;/code&gt; 같은 배열 속성으로 크기&amp;middot;채널&amp;middot;픽셀 타입을 바로 확인할 수 있다. 컬러 이미지는 3차원(높이, 너비, 채널), 그레이스케일은 2차원이다. shapes.png가 있는 폴더에서 아래 스크립트를 실행해 보자.&lt;/p&gt;
&lt;div class=&quot;codecard&quot;&gt;
&lt;div class=&quot;head&quot;&gt;이미지 로드 및 검사 (inspect_image.py)&lt;/div&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;import cv2 as cv
image_path = &quot;shapes.png&quot;
img = cv.imread(image_path, cv.IMREAD_COLOR)
if img is None:
    print(f&quot;Error: Could not load image from {image_path}&quot;)
else:
    print(f&quot;Data Type: {type(img)}&quot;)       # &amp;lt;class 'numpy.ndarray'&amp;gt;
    print(f&quot;Dimensions: {img.ndim}&quot;)       # 3 for color images
    print(f&quot;Shape: {img.shape}&quot;)           # (Height, Width, Channels)
    print(f&quot;Pixel Data Type: {img.dtype}&quot;) # uint8
    img_gray = cv.imread(image_path, cv.IMREAD_GRAYSCALE)
    print(f&quot;Grayscale Shape: {img_gray.shape}&quot;)  # (Height, Width)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h3 class=&quot;sec-subtitle&quot; style=&quot;margin-top: 20px; margin-bottom: 8px; font-weight: bold; font-size: 18px;&quot; data-ke-size=&quot;size23&quot;&gt;6-2. 시각화와 BGR &amp;rarr; RGB 변환&lt;/h3&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;OpenCV는 이미지를 &lt;b&gt;BGR&lt;/b&gt;(파랑&amp;middot;초록&amp;middot;빨강) 순으로 읽는다. Matplotlib&amp;middot;화면은 보통 &lt;b&gt;RGB&lt;/b&gt;(빨강&amp;middot;초록&amp;middot;파랑)라서, 그대로 넣으면 색이 반대로 보인다. &lt;code&gt;cv.cvtColor(image, cv.COLOR_BGR2RGB)&lt;/code&gt;로 바꾼 뒤 표시하고, 그레이스케일은 &lt;code&gt;cmap='gray'&lt;/code&gt;로 보여 주면 된다.&lt;/p&gt;
&lt;div class=&quot;codecard&quot;&gt;
&lt;div class=&quot;head&quot;&gt;시각화 및 색공간 변환 (display_image.py)&lt;/div&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;import cv2 as cv
from matplotlib import pyplot as plt

def show_image(image, title=&quot;Image&quot;, is_bgr=True):
    if is_bgr:
        image = cv.cvtColor(image, cv.COLOR_BGR2RGB)
    plt.imshow(image)
    plt.title(title)
    plt.xticks([]), plt.yticks([])
    plt.show()

if __name__ == &quot;__main__&quot;:
    img = cv.imread(&quot;shapes.png&quot;, cv.IMREAD_COLOR)
    if img is not None:
        show_image(img, title=&quot;Corrected RGB Output&quot;)
        img_gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
        plt.imshow(img_gray, cmap='gray')
        plt.title(&quot;Grayscale Version&quot;)
        plt.xticks([]), plt.yticks([])
        plt.show()
    else:
        print(&quot;Image not found.&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h3 class=&quot;sec-subtitle&quot; style=&quot;margin-top: 20px; margin-bottom: 8px; font-weight: bold; font-size: 18px;&quot; data-ke-size=&quot;size23&quot;&gt;6-3. 원 검출 (Canny + Hough)&lt;/h3&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;도형을 찾는 흐름은 (1) 그레이스케일로 바꾸고, (2) Canny로 엣지를 뽑고, (3) Hough 변환으로 원을 찾는 것이다. Canny 50&amp;middot;150은 약한/강한 엣지 구분, HoughCircles의 minDist&amp;middot;minRadius&amp;middot;maxRadius로 원 크기&amp;middot;간격을 조절한다. smarties.png로 실행하면 검출된 원이 초록색으로 그려진다.&lt;/p&gt;
&lt;div class=&quot;codecard&quot;&gt;
&lt;div class=&quot;head&quot;&gt;원 검출 (detect_circles.py)&lt;/div&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;def detect_and_draw_circles(image_path):
    img = cv.imread(image_path)
    if img is None: return
    img_gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
    edges = cv.Canny(img_gray, 50, 150)
    circles = cv.HoughCircles(edges, cv.HOUGH_GRADIENT, dp=1, minDist=20,
                              param1=50, param2=30, minRadius=10, maxRadius=50)
    output_img = img.copy()
    if circles is not None:
        circles = np.uint16(np.around(circles))
        for i in circles[0, :]:
            cv.circle(output_img, (i[0], i[1]), i[2], (0, 255, 0), 5)
            cv.putText(output_img, &quot;Circle&quot;, (i[0]-30, i[1]), cv.FONT_HERSHEY_COMPLEX, 0.5, (0,0,0))
    output_img_rgb = cv.cvtColor(output_img, cv.COLOR_BGR2RGB)
    plt.figure(figsize=(10, 5))
    plt.imshow(output_img_rgb)
    plt.title(f&quot;Detected {len(circles[0])} Circles&quot;)
    plt.xticks([]), plt.yticks([])
    plt.show()
detect_and_draw_circles(&quot;smarties.png&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h2 id=&quot;sec7&quot; class=&quot;sec-title title-26&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;hl-yellow&quot;&gt;7. 실습 2: RPi 카메라 &amp;rarr; PC 전송&lt;/span&gt;&lt;/h2&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;라즈베리파이 Zero W2에 CSI 카메라를 붙이고, PC로 사진 한 장 보내기&amp;middot;실시간 영상 보내기를 해 본다. RPi와 PC는 같은 Wi-Fi에 있어야 하고, Bookworm OS에서는 예전 raspistill 대신 &lt;b&gt;rpicam-apps&lt;/b&gt;, &lt;b&gt;Picamera2&lt;/b&gt;를 쓴다.&lt;/p&gt;
&lt;h3 class=&quot;sec-subtitle&quot; style=&quot;margin-top: 20px; margin-bottom: 8px; font-weight: bold; font-size: 18px;&quot; data-ke-size=&quot;size23&quot;&gt;7-1. RPi 환경 설정 (스왑&amp;middot;의존성)&lt;/h3&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;설치 중 메모리 부족으로 멈추지 않도록, 먼저 &lt;b&gt;스왑&lt;/b&gt;을 2GB로 늘려 두자. &lt;code&gt;sudo nano /etc/dphys-swapfile&lt;/code&gt;에서 &lt;code&gt;CONF_SWAPSIZE=100&lt;/code&gt;을 &lt;code&gt;CONF_SWAPSIZE=2048&lt;/code&gt;로 바꾼 뒤 저장하고, &lt;code&gt;sudo dphys-swapfile setup&lt;/code&gt;, &lt;code&gt;sudo dphys-swapfile swapon&lt;/code&gt;으로 적용한다. RPi 쪽 패키지: &lt;code&gt;sudo apt update&lt;/code&gt;, &lt;code&gt;sudo apt install rpicam-apps python3-libcamera python3-kms++ libcamera-ipa python3-picamera2&lt;/code&gt;. PC에는 VLC와 &lt;code&gt;pip install opencv-python ultralytics numpy&lt;/code&gt;가 필요하다. Windows에서 netcat이 필요하면 Nmap 설치 시 Ncat을 선택해 두면 &lt;code&gt;ncat&lt;/code&gt; 명령을 쓸 수 있다.&lt;/p&gt;
&lt;h3 class=&quot;sec-subtitle&quot; style=&quot;margin-top: 20px; margin-bottom: 8px; font-weight: bold; font-size: 18px;&quot; data-ke-size=&quot;size23&quot;&gt;7-2. 단일 이미지 전송&lt;/h3&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;CLI 방식&lt;/b&gt;: PC에서 먼저 &lt;code&gt;nc -l 5000 &amp;gt; received_image.jpg&lt;/code&gt;(Mac)로 대기. RPi에서 &lt;code&gt;rpicam-jpeg -o - -t 2000 --width 1920 --height 1080 | nc PC_IP 5000&lt;/code&gt; 실행하면 한 장이 전송된다. &lt;b&gt;Python 방식&lt;/b&gt;: RPi에서 Picamera2로 JPEG 캡처 후 TCP로 보내고, PC에서 소켓으로 받아 파일로 저장한다. HOST_IP&amp;middot;PORT(65432)만 본인 환경에 맞게 바꾸면 된다.&lt;/p&gt;
&lt;div class=&quot;codecard&quot;&gt;
&lt;div class=&quot;head&quot;&gt;RPi 송신 (send_image.py)&lt;/div&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;import socket, time
from picamera2 import Picamera2
HOST_IP = '192.168.1.10'  # 호스트 PC IP로 변경
PORT = 65432
picam2 = Picamera2()
config = picam2.create_preview_configuration(main={&quot;size&quot;: (1920, 1080), &quot;format&quot;: &quot;XRGB8888&quot;})
picam2.configure(config)
picam2.start()
time.sleep(2)
jpg_data = picam2.capture_buffer(&quot;main&quot;, format=&quot;jpeg&quot;)
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    s.connect((HOST_IP, PORT))
    s.sendall(jpg_data)
    print(&quot;Image sent!&quot;)
picam2.stop()&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div class=&quot;codecard&quot;&gt;
&lt;div class=&quot;head&quot;&gt;호스트 수신 (recv_image.py)&lt;/div&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;import socket
PORT = 65432
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    s.bind(('0.0.0.0', PORT))
    s.listen()
    print(&quot;Waiting for image...&quot;)
    conn, addr = s.accept()
    with conn:
        with open('received_py.jpg', 'wb') as f:
            while True:
                data = conn.recv(4096)
                if not data: break
                f.write(data)
    print(&quot;Image saved as received_py.jpg&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h3 class=&quot;sec-subtitle&quot; style=&quot;margin-top: 20px; margin-bottom: 8px; font-weight: bold; font-size: 18px;&quot; data-ke-size=&quot;size23&quot;&gt;7-3. 영상 스트리밍&lt;/h3&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;VLC로 보기&lt;/b&gt;: RPi에서 &lt;code&gt;rpicam-vid -t 0 --inline --listen -o tcp://0.0.0.0:8554 --width 1280 --height 720 --framerate 30&lt;/code&gt; 실행. PC의 VLC에서 File &amp;rarr; Open Network, URL에 &lt;code&gt;tcp/h264://RPI_IP:8554&lt;/code&gt; 입력하면 실시간 영상이 보인다.&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Python으로 받아서 OpenCV&amp;middot;YOLO 쓰기&lt;/b&gt;: raw H.264는 OpenCV에서 까다로울 수 있어서, 프레임마다 JPEG로 압축해 보내는 &lt;b&gt;MJPEG 스타일&lt;/b&gt;이 편하다. 형식은 [4바이트 길이][JPEG 데이터]. RPi는 계속 캡처해서 길이+데이터를 보내고, PC는 길이만큼 읽어 &lt;code&gt;cv2.imdecode&lt;/code&gt;로 프레임 복원 후 화면에 띄우거나 YOLO에 넣으면 된다. &lt;b&gt;실행 순서&lt;/b&gt;: PC(수신)를 먼저 켜고, 그다음 RPi(송신)를 실행한다.&lt;/p&gt;
&lt;div class=&quot;codecard&quot;&gt;
&lt;div class=&quot;head&quot;&gt;RPi 스트리밍 송신 (rpi_stream_sender.py)&lt;/div&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;import socket, struct, io
from picamera2 import Picamera2
HOST_IP, PORT = '192.168.1.10', 8000
picam2 = Picamera2()
config = picam2.create_preview_configuration(main={&quot;size&quot;: (640, 480), &quot;format&quot;: &quot;XRGB8888&quot;})
picam2.configure(config)
picam2.start()
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client_socket.connect((HOST_IP, PORT))
connection = client_socket.makefile('wb')
try:
    while True:
        stream = io.BytesIO()
        picam2.capture_file(stream, format=&quot;jpeg&quot;)
        data = stream.getvalue()
        connection.write(struct.pack('&amp;lt;L', len(data)))
        connection.write(data)
except KeyboardInterrupt: pass
finally:
    connection.close(); client_socket.close(); picam2.stop()&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div class=&quot;codecard&quot;&gt;
&lt;div class=&quot;head&quot;&gt;호스트: OpenCV 그레이 표시 (host_stream_opencv_view.py)&lt;/div&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;import socket, struct, cv2, numpy as np, io
PORT = 8000
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.bind(('0.0.0.0', PORT))
server_socket.listen(0)
conn, addr = server_socket.accept()
connection = conn.makefile('rb')
while True:
    image_len_data = connection.read(4)
    if not image_len_data: break
    image_len = struct.unpack('&amp;lt;L', image_len_data)[0]
    image_stream = io.BytesIO()
    image_stream.write(connection.read(image_len))
    image_stream.seek(0)
    data = np.frombuffer(image_stream.getvalue(), dtype=np.uint8)
    frame = cv2.imdecode(data, cv2.IMREAD_COLOR)
    if frame is not None:
        gray_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
        cv2.imshow('RPi Stream (Gray)', gray_frame)
    if cv2.waitKey(1) &amp;amp; 0xFF == ord('q'): break
conn.close(); server_socket.close(); cv2.destroyAllWindows()&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div class=&quot;codecard&quot;&gt;
&lt;div class=&quot;head&quot;&gt;호스트: YOLO 검출 (host_stream_yolo_detect.py)&lt;/div&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;from ultralytics import YOLO
model = YOLO(&quot;yolov8n.pt&quot;)
# 위와 동일하게 image_len 읽기, frame = cv2.imdecode(...) 후
# results = model(frame, verbose=False)
# annotated_frame = results[0].plot()
# cv2.imshow('YOLOv8 Detection', annotated_frame)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h2 id=&quot;sec8&quot; class=&quot;sec-title title-26&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;hl-yellow&quot;&gt;8. 실습 3: 캡처 도구와 YOLO 분석&lt;/span&gt;&lt;/h2&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;웹캠으로 사진&amp;middot;영상을 찍고, 그걸 YOLO로 검출&amp;middot;분류&amp;middot;세그멘테이션해 보는 단계다. 먼저 캡처 도구로 데이터를 모은 뒤, 분석 스크립트에서 불러와서 쓴다.&lt;/p&gt;
&lt;h3 class=&quot;sec-subtitle&quot; style=&quot;margin-top: 20px; margin-bottom: 8px; font-weight: bold; font-size: 18px;&quot; data-ke-size=&quot;size23&quot;&gt;8-1. 캡처 도구 (01_capture_tool.py)&lt;/h3&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;웹캠을 켜고 키보드로 조작한다. &lt;b&gt;i&lt;/b&gt; 한 장 저장, &lt;b&gt;v&lt;/b&gt; 녹화 시작/중지, &lt;b&gt;q&lt;/b&gt; 종료. 파일명은 &lt;code&gt;capture_타임스탬프.jpg&lt;/code&gt;, &lt;code&gt;video_타임스탬프.mp4&lt;/code&gt;처럼 저장된다. 녹화 중일 때는 화면 왼쪽 위에 빨간 점이 보인다. i로 사진 몇 장, v로 짧은 영상 한 번씩 찍어 두면 다음 단계에서 쓸 수 있다.&lt;/p&gt;
&lt;div class=&quot;codecard&quot;&gt;
&lt;div class=&quot;head&quot;&gt;캡처 도구 (01_capture_tool.py) &amp;mdash; 키 처리&lt;/div&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;cap = cv2.VideoCapture(0)
writer = None
recording = False
# CONTROLS: 'i'=Image, 'v'=Video toggle, 'q'=Quit
while True:
    ret, frame = cap.read()
    if recording:
        cv2.circle(display_frame, (30, 30), 10, (0, 0, 255), -1)
        writer.write(frame)
    cv2.imshow(&quot;Webcam Capture Tool&quot;, display_frame)
    key = cv2.waitKey(1) &amp;amp; 0xFF
    if key == ord('i'):
        cv2.imwrite(f&quot;capture_{int(time.time())}.jpg&quot;, frame)
    elif key == ord('v'):
        if not recording:
            writer = cv2.VideoWriter(filename, cv2.VideoWriter_fourcc(*'mp4v'), 30, (width, height))
            recording = True
        else:
            writer.release()
            recording = False
    elif key == ord('q'):
        break&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h3 class=&quot;sec-subtitle&quot; style=&quot;margin-top: 20px; margin-bottom: 8px; font-weight: bold; font-size: 18px;&quot; data-ke-size=&quot;size23&quot;&gt;8-2. YOLO 분석 (02_analyze_media.py)&lt;/h3&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;SOURCE&lt;/b&gt;는 입력 소스다. 0이면 웹캠, 파일명(예: capture_12345.jpg, video_12345.mp4)을 넣으면 그 파일을 연다. &lt;b&gt;TASK&lt;/b&gt;를 'detect', 'classify', 'segment' 중 하나로 바꾸면 된다. detect &amp;rarr; yolov8n.pt, classify &amp;rarr; yolov8n-cls.pt, segment &amp;rarr; yolov8n-seg.pt가 자동 선택된다. 이미지면 한 번 추론 후 화면에 띄우고 끝나고, 비디오/웹캠이면 프레임마다 추론해서 보여 준다.&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;모드별로 보이는 것&lt;/b&gt;: (A) &lt;b&gt;detect&lt;/b&gt; &amp;mdash; 박스와 라벨(예: Person 0.85, Cell Phone). (B) &lt;b&gt;classify&lt;/b&gt; &amp;mdash; 박스 없이 상위 클래스만(예: coffee_mug 0.70). ImageNet 1000클래스 기준. (C) &lt;b&gt;segment&lt;/b&gt; &amp;mdash; 객체를 반투명 색으로 픽셀 단위로 칠해서 구분. 첫 실행 시 모델 파일이 자동 다운로드된다. Nano(yolov8n.pt)는 작고 빠르서 CPU에서도 돌리기 좋다.&lt;/p&gt;
&lt;div class=&quot;codecard&quot;&gt;
&lt;div class=&quot;head&quot;&gt;02_analyze_media.py &amp;mdash; TASK&amp;middot;모델 선택&lt;/div&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;SOURCE = 0   # or 'capture_12345.jpg' / 'video_12345.mp4'
TASK = 'segment'  # 'detect' | 'classify' | 'segment'
if TASK == 'detect':   model = YOLO('yolov8n.pt')
elif TASK == 'classify': model = YOLO('yolov8n-cls.pt')
elif TASK == 'segment': model = YOLO('yolov8n-seg.pt')
# 이미지면: results = model(SOURCE); res_plotted = results[0].plot(); cv2.imshow(...); cv2.waitKey(0)
# 비디오/웹캠이면: cap = cv2.VideoCapture(SOURCE); 루프에서 model(frame, verbose=False); results[0].plot()&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h2 id=&quot;sec9&quot; class=&quot;sec-title title-26&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;hl-yellow&quot;&gt;9. 실습 4: YOLO로 결과 다루기&lt;/span&gt;&lt;/h2&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;YOLO로 정적 이미지&amp;middot;웹캠 영상을 분석하고, 화면에 그리는 것 말고 &lt;b&gt;코드에서 결과를 어떻게 쓰는지&lt;/b&gt;까지 다룬다. &quot;사람이 보이면 드론 멈추기&quot; 같은 로직을 만들 때 필요하다.&lt;/p&gt;
&lt;h3 class=&quot;sec-subtitle&quot; style=&quot;margin-top: 20px; margin-bottom: 8px; font-weight: bold; font-size: 18px;&quot; data-ke-size=&quot;size23&quot;&gt;9-1. 정적 이미지 검출 (yolo_image.py)&lt;/h3&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;COCO 80클래스(사람, 자동차, 개, 휴대폰 등)로 학습된 모델을 불러와서, 로컬 파일이나 URL 이미지에 추론한다. &lt;code&gt;results&lt;/code&gt;는 리스트라서 &lt;code&gt;for result in results&lt;/code&gt;로 돌리며 &lt;code&gt;result.plot()&lt;/code&gt;으로 그린 뒤 창에 띄우고, 키 입력 시 종료하면 된다.&lt;/p&gt;
&lt;div class=&quot;codecard&quot;&gt;
&lt;div class=&quot;head&quot;&gt;yolo_image.py &amp;mdash; 정적 이미지 검출&lt;/div&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;from ultralytics import YOLO
import cv2
model = YOLO('yolov8n.pt')
image_source = 'https://ultralytics.com/images/zidane.jpg'  # or local path
results = model(image_source)
for result in results:
    annotated_frame = result.plot()
    cv2.imshow(&quot;YOLO Detection&quot;, annotated_frame)
    cv2.waitKey(0)
cv2.destroyAllWindows()&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h3 class=&quot;sec-subtitle&quot; style=&quot;margin-top: 20px; margin-bottom: 8px; font-weight: bold; font-size: 18px;&quot; data-ke-size=&quot;size23&quot;&gt;9-2. 웹캠 실시간 검출 (yolo_webcam.py)&lt;/h3&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;웹캠을 열고 매 프레임을 YOLO에 넣어서 박스&amp;middot;라벨을 그린 뒤 화면에 띄운다. &lt;code&gt;stream=True&lt;/code&gt;로 두면 비디오에서 메모리 관리가 수월하다. q를 누르면 종료. 드론&amp;middot;로봇에서 실시간 영상 검출할 때와 같은 패턴이다.&lt;/p&gt;
&lt;div class=&quot;codecard&quot;&gt;
&lt;div class=&quot;head&quot;&gt;yolo_webcam.py &amp;mdash; 웹캠 실시간 검출&lt;/div&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;cap = cv2.VideoCapture(0)
while True:
    success, frame = cap.read()
    if not success: break
    results = model(frame, stream=True)
    for result in results:
        annotated_frame = result.plot()
        cv2.imshow(&quot;YOLO Real-Time Inference&quot;, annotated_frame)
    if cv2.waitKey(1) &amp;amp; 0xFF == ord('q'): break
cap.release()
cv2.destroyAllWindows()&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h3 class=&quot;sec-subtitle&quot; style=&quot;margin-top: 20px; margin-bottom: 8px; font-weight: bold; font-size: 18px;&quot; data-ke-size=&quot;size23&quot;&gt;9-3. 결과 데이터 활용 (boxes, conf, cls, name)&lt;/h3&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&quot;사람이 감지되면 드론 정지&quot;처럼 &lt;b&gt;로직&lt;/b&gt;을 넣으려면, 그리기(plot)가 아니라 &lt;b&gt;숫자 데이터&lt;/b&gt;를 써야 한다. &lt;code&gt;result.boxes&lt;/code&gt;에서 각 박스마다 &lt;code&gt;xyxy[0]&lt;/code&gt;(x1,y1,x2,y2), &lt;code&gt;conf[0]&lt;/code&gt;(신뢰도 0~1), &lt;code&gt;cls[0]&lt;/code&gt;(클래스 번호), &lt;code&gt;model.names[cls]&lt;/code&gt;(클래스 이름)을 꺼낸다. 예: &lt;code&gt;name == 'person'&lt;/code&gt;이고 &lt;code&gt;conf &amp;gt; 0.5&lt;/code&gt;일 때만 동작하도록 조건을 걸 수 있다.&lt;/p&gt;
&lt;div class=&quot;codecard&quot;&gt;
&lt;div class=&quot;head&quot;&gt;결과에서 좌표&amp;middot;클래스&amp;middot;신뢰도 추출&lt;/div&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;for result in results:
    boxes = result.boxes
    for box in boxes:
        x1, y1, x2, y2 = box.xyxy[0]
        conf = box.conf[0]
        cls = int(box.cls[0])
        name = model.names[cls]
        if name == 'person' and conf &amp;gt; 0.5:
            print(f&quot;Person detected at [{x1}, {y1}] with {conf:.2f} confidence&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h2 id=&quot;sec10&quot; class=&quot;sec-title title-26&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;hl-yellow&quot;&gt;10. 정리&lt;/span&gt;&lt;/h2&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;이번 수업에서는 &lt;b&gt;OpenCV&lt;/b&gt;의 정의&amp;middot;역사&amp;middot;플랫폼&amp;middot;라이선스, 기반 언어&amp;middot;API&amp;middot;알고리즘 개요(2,000개 이상)&amp;middot;딥러닝과의 관계, 주요 활용 분야(IT&amp;middot;안전&amp;middot;산업&amp;middot;엔터테인먼트), 공식 리소스&amp;middot;참고 문헌, 주요 모듈(Core, Imgproc, Highgui, Features2D, Calib3d, ML, Objdetect)을 정리했다. 이어서 &lt;b&gt;디지털 이미지 획득&lt;/b&gt; 5단계, 디지털 카메라의 5가지 문제점(노이즈&amp;middot;색수차&amp;middot;블루밍&amp;middot;후광&amp;middot;압축), &lt;b&gt;이미지 필터링&lt;/b&gt; 원리(공간 필터링&amp;middot;마스크&amp;middot;중간값 필터)&amp;middot;허블 망원경 예&amp;middot;노이즈 제거 비교, &lt;b&gt;엣지 검출&lt;/b&gt;&amp;middot;Canny 세 조건&amp;middot;4단계&amp;middot;스무딩 트레이드오프, &lt;b&gt;윤곽선 추출&lt;/b&gt;(데이터 압축&amp;middot;70 FPS)&amp;middot;&lt;b&gt;객체 인식&lt;/b&gt;, &lt;b&gt;인간 포즈&amp;middot;수어 추정&lt;/b&gt;(Depth&amp;middot;Label&amp;middot;Model), &lt;b&gt;움직임 감지&lt;/b&gt; 세 가지(프레임 차이&amp;middot;배경 차분&amp;middot;광학 흐름)&amp;middot;밝기 불변성&amp;middot;calcOpticalFlowPyrLK/Farneback, &lt;b&gt;캘리브레이션&lt;/b&gt;(체커보드&amp;middot;Un-distorted image)&amp;middot;&lt;b&gt;OpenCV ML&lt;/b&gt;(분류&amp;middot;클러스터링&amp;middot;검증), &lt;b&gt;YOLO&lt;/b&gt; 정의&amp;middot;발전(v1~v8)&amp;middot;장단점&amp;middot;실생활 활용을 다뤘다.&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;실습은 네 묶음으로 진행했다. &lt;b&gt;실습 1&lt;/b&gt; OpenCV 기초(설치&amp;middot;이미지 로드&amp;middot;검사&amp;middot;BGR&amp;rarr;RGB&amp;middot;원 검출), &lt;b&gt;실습 2&lt;/b&gt; RPi 카메라에서 PC로(환경 설정&amp;middot;스왑&amp;middot;단일 이미지&amp;middot;스트리밍 VLC&amp;middot;Python MJPEG), &lt;b&gt;실습 3&lt;/b&gt; 캡처 도구와 YOLO 분석(01_capture_tool&amp;middot;02_analyze_media detect/classify/segment), &lt;b&gt;실습 4&lt;/b&gt; YOLO 정적 이미지&amp;middot;웹캠 실시간&amp;middot;결과 데이터(boxes, conf, cls, name) 활용이다. 드론&amp;middot;로봇&amp;middot;감시 등 실시간 비전 파이프라인과 데이터 활용의 기초가 되는 내용이다.&lt;/p&gt;
&lt;style&gt;
    #post-root a { color:#495057 !important; text-decoration:none !important; }
    #post-root a:hover { color:#212529 !important; border-bottom:1px solid #ced4da; }
    #post-root a:visited { color:#666 !important; }
    
    .sec-title { display:block; margin:28px 0 12px; font-weight:800; line-height:1.35; }
    .sec-title &gt; .hl-yellow {
      background:linear-gradient(transparent 65%, #fff3bf 65%);
      padding:0 .12em;
    }
    .title-26 { font-size:26px; }
    
    .para { margin:10px 0 14px; color:#343a40; line-height:1.9; }
    
    .toc {
      border:1px solid #e9ecef;
      border-radius:10px;
      padding:12px 14px;
      margin:18px 0 24px;
      background:#f8f9fa;
    }
    
    .codecard {
      background:#f8f9fa;
      border:1px solid #e9ecef;
      border-radius:8px;
      padding:16px;
      margin:16px 0;
      overflow-x:auto;
    }
    
    .codecard .head {
      font-weight:700;
      color:#212529;
      margin-bottom:10px;
      font-size:15px;
    }
    
    .codecard pre {
      margin:0;
      padding:0;
      background:transparent;
      border:none;
    }
    
    .codecard code {
      font-family:'Courier New', monospace;
      font-size:13px;
      line-height:1.6;
      color:#212529;
    }
    &lt;/style&gt;
&lt;/div&gt;</description>
      <category>UNLV</category>
      <author>빡성</author>
      <guid isPermaLink="true">https://smp0417.tistory.com/73</guid>
      <comments>https://smp0417.tistory.com/73#entry73comment</comments>
      <pubDate>Thu, 19 Feb 2026 08:13:12 +0900</pubDate>
    </item>
    <item>
      <title>[UNLV] - 모델 압축과 TinyML 배포(9일차)</title>
      <link>https://smp0417.tistory.com/72</link>
      <description>&lt;!-- ========================= --&gt;
&lt;div id=&quot;post-root&quot;&gt;
&lt;div class=&quot;toc&quot;&gt;
&lt;div style=&quot;font-weight: 800; margin-bottom: 8px;&quot;&gt;&amp;nbsp; &amp;nbsp; 목차&lt;/div&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#sec1&quot;&gt;수업 개요&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#sec2&quot;&gt;이식성 트레이드오프 (Portability Trade-offs)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#sec3&quot;&gt;ML 모델 진화: 정확도 vs 연산량&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#sec4&quot;&gt;대표 CNN 모델 비교&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#sec5&quot;&gt;모델 압축 기법의 위치&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#sec6&quot;&gt;Tiny 디바이스 최적화 3대 축&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#sec7&quot;&gt;Pruning (가지치기)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#sec8&quot;&gt;Quantization (양자화)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#sec9&quot;&gt;Knowledge Distillation (지식 증류)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#sec10&quot;&gt;TinyML과 M5Core2 배포 워크플로우&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#sec11&quot;&gt;실습 Task I: IMU 데이터 전송 방식 비교&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#sec12&quot;&gt;실습 Task II&amp;middot;III: CNN 압축과 보드 추론&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#sec13&quot;&gt;정리&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;!-- 1 --&gt;
&lt;h2 id=&quot;sec1&quot; class=&quot;sec-title title-26&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;hl-yellow&quot;&gt;1. 수업 개요&lt;/span&gt;&lt;/h2&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;이번 수업은 임베디드/하드웨어 환경에서의 &lt;b&gt;이식성(Portability)과 효율(Efficiency)의 트레이드오프&lt;/b&gt;, 그리고 &lt;b&gt;모델 압축(Quantization, Pruning, Knowledge Distillation)&lt;/b&gt;을 통해 Tiny 디바이스에 ML 모델을 배포하는 전 과정을 다뤘다.&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;큰 모델과 범용 코드의 &quot;이식성&quot;을 고집하면 비용&amp;middot;전력&amp;middot;엔지니어링 노력이 커질 수 있고, 반대로 HW 맞춤 최적화를 하면 이식성은 떨어지는 선택이 자주 필요하다. 임베디드에서는 정확도뿐 아니라 연산량&amp;middot;지연&amp;middot;전력&amp;middot;메모리를 함께 고려해야 한다.&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;실습에서는 M5Core2에서 PC로 IMU 데이터를 전송하는 방식(BLE, WiFi UDP, ESP-NOW)을 비교하고, CNN 모델에 프루닝과 양자화를 적용해 TFLite로 변환한 뒤 M5Core2에 배포하는 파이프라인을 경험했다.&lt;/p&gt;
&lt;!-- 2 --&gt;
&lt;h2 id=&quot;sec2&quot; class=&quot;sec-title title-26&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;hl-yellow&quot;&gt;2. 이식성 트레이드오프 (Portability Trade-offs)&lt;/span&gt;&lt;/h2&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;임베디드에서는 &lt;b&gt;&quot;이식성을 희생하고 효율을 얻는&quot;&lt;/b&gt; 선택이 자주 필요하다. 특정 HW(보드) 구현 관점에서 선택지는 크게 두 가지다.&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Option 1: 범용 코드 이식성/호환성(Universal Code Portability)&lt;/b&gt;을 최우선으로 할 때&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;장점: 여러 시스템에서 그대로 동작한다(이식성 ✓).&lt;/li&gt;
&lt;li&gt;단점: 비용(Cost), 전력(Power), 엔지니어링 노력(Engineering Effort)이 커질 수 있다(✗).&lt;/li&gt;
&lt;/ul&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Option 2: 이식성은 낮추고(Lower Code Portability) 최적화&lt;/b&gt;할 때&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;단점: 이식성이 떨어진다(✗).&lt;/li&gt;
&lt;li&gt;장점: 비용/전력/개발 노력 측면에서 유리하다(✓).&lt;/li&gt;
&lt;/ul&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;핵심은 &quot;어디서나 동일하게 동작&quot;을 고집하면 비용&amp;middot;전력&amp;middot;노력에서 불리해질 수 있고, HW 맞춤 최적화를 하면 이식성은 떨어질 수 있다는 트레이드오프를 인지하는 것이다.&lt;/p&gt;
&lt;!-- 3 --&gt;
&lt;h2 id=&quot;sec3&quot; class=&quot;sec-title title-26&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;hl-yellow&quot;&gt;3. ML 모델 진화: 정확도 vs 연산량&lt;/span&gt;&lt;/h2&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;모델이 발전할수록 정확도(Top-1 Accuracy)를 올리기 위해 연산량(GFLOPs)과 규모가 커지는 경향이 있다. 동시에 &lt;b&gt;모바일/엣지 지향 모델&lt;/b&gt;(예: MobileNet 계열)처럼 연산량 대비 효율을 노린 모델도 등장한다.&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;임베디드에서는 &quot;정확도만&quot;이 아니라 &lt;b&gt;연산량&amp;middot;지연&amp;middot;전력&amp;middot;메모리&lt;/b&gt;까지 함께 봐야 한다. 작은 모델은 더 빠르게 돌고, 메모리를 덜 쓰며, 전력을 덜 소비한다(Smaller models run faster, use less memory, and consume less power).&lt;/p&gt;
&lt;!-- 4 --&gt;
&lt;h2 id=&quot;sec4&quot; class=&quot;sec-title title-26&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;hl-yellow&quot;&gt;4. 대표 CNN 모델 비교&lt;/span&gt;&lt;/h2&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;대표 CNN들을 정확도&amp;middot;파라미터 규모&amp;middot;특징으로 비교하면 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;AlexNet (2012)&lt;/b&gt;: Top-1 약 63%(ImageNet), 약 60M parameters, 수백 MB 수준. 초기 딥 CNN의 대표로, 현재는 교육/학습용 성격.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;VGGNet (VGG16/19)&lt;/b&gt;: VGG16 Top-1 약 71.3%, 약 138M params, 약 528MB. 구조가 단순하고 균일하지만 크고 무거움(heavy compute).&lt;/li&gt;
&lt;li&gt;&lt;b&gt;ResNet (ResNet152V2 등)&lt;/b&gt;: Top-1 약 78.0%, 약 60.4M params. Residual 구조로 성능이 좋고, VGG보다 효율적인 경향.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;MobileNet (MobileNetV3 Large)&lt;/b&gt;: Top-1 약 75.2%, 약 5.48M params. 모바일/엣지용으로 크기와 지연(latency)에서 유리한 트레이드오프.&lt;/li&gt;
&lt;/ul&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;임베디드/엣지에서는 MobileNet처럼 &lt;b&gt;&quot;작고 효율적인 모델&quot;&lt;/b&gt;이 중요하다.&lt;/p&gt;
&lt;!-- 5 --&gt;
&lt;h2 id=&quot;sec5&quot; class=&quot;sec-title title-26&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;hl-yellow&quot;&gt;5. 모델 압축 기법의 위치&lt;/span&gt;&lt;/h2&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;ML 시스템을 (1) Models &amp;harr; (2) Runtimes &amp;harr; (3) Hardware 세 층으로 보면, 서로 영향을 주는 생태계다. &quot;하드웨어에 맞게 실제로 돌아가게&quot; 하려면 모델 자체뿐 아니라 런타임과 하드웨어까지 포함한 최적화가 필요하다.&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Model Compression Techniques&lt;/b&gt;로는 Pruning(가지치기), Quantization(양자화), Knowledge Distillation(지식 증류) 등이 있다. 이 기법들을 조합(Quantized &amp;amp; pruned, Distillation applied)하여 사용할 수 있다.&lt;/p&gt;
&lt;!-- 6 --&gt;
&lt;h2 id=&quot;sec6&quot; class=&quot;sec-title title-26&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;hl-yellow&quot;&gt;6. Tiny 디바이스 최적화 3대 축&lt;/span&gt;&lt;/h2&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;Tiny 디바이스에서 모델을 맞추기 위한 대표 기법 세 가지는 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Quantization(양자화)&lt;/b&gt;: float32 &amp;rarr; int8처럼 정밀도를 낮춰(정수화) 모델을 가볍게 한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Model Pruning(프루닝)&lt;/b&gt;: 중요하지 않은 뉴런/연결을 제거한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Knowledge Distillation(지식 증류)&lt;/b&gt;: 큰(teacher) 모델의 지식을 작은(student) 모델이 모방하도록 학습시킨다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;이를 묶어 &lt;b&gt;Model Compression(모델 압축)&lt;/b&gt;으로 사용하며, &quot;Quantized &amp;amp; pruned&quot;, &quot;Distillation applied&quot;처럼 조합 가능하다.&lt;/p&gt;
&lt;!-- 7 --&gt;
&lt;h2 id=&quot;sec7&quot; class=&quot;sec-title title-26&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;hl-yellow&quot;&gt;7. Pruning (가지치기)&lt;/span&gt;&lt;/h2&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;Pruning은 자원 제약 장치에서 실시간 추론에 적합하도록 모델을 최적화하는 압축 기법이다. 촘촘한 연결(가중치/시냅스) 중 일부를 제거하는 방식으로 이해할 수 있다.&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;Pruning은 두 관점으로 설명할 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Synapse(가중치 연결) 제거&lt;/b&gt;: 특정 가중치를 0으로 만들어 연결 자체를 줄인다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Neuron(노드) 제거&lt;/b&gt;: 뉴런 전체를 제거하여 연산과 파라미터를 줄인다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;tensorflow-model-optimization 패키지로 프루닝을 적용할 수 있고, 설치가 어려우면 수동(manual) 프루닝도 가능하다. 압축률(compression %)을 계산하여 보고하는 것이 실습 목표 중 하나다.&lt;/p&gt;
&lt;!-- 8 --&gt;
&lt;h2 id=&quot;sec8&quot; class=&quot;sec-title title-26&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;hl-yellow&quot;&gt;8. Quantization (양자화)&lt;/span&gt;&lt;/h2&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;Quantization은 모델 파라미터(기본 float32)를 정밀도가 낮은 표현으로 바꾸는 최적화다. 결과적으로 모델 크기 감소, 이식성 향상, 계산 속도 향상이 기대된다.&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;분류: PTQ vs QAT&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Post-training Quantization (PTQ)&lt;/b&gt;: 학습이 끝난 모델을 변환(conversion)해서 양자화한다. POT(Post-training Optimization Tool) 같은 도구가 빠르고 쉽다. 모델 사이즈 감소와 CPU/하드웨어 가속기 지연 개선 효과가 있으나, 정확도 저하가 조금 있을 수 있다(little degradation).&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Quantization-aware Training (QAT)&lt;/b&gt;: 학습 단계에서 추론 시 양자화를 에뮬레이션(emulates inference-time quantization)한다. 실제 양자화 모델을 만들 때 양자화 환경을 고려한 모델이 되도록 한다. 8-bit 같은 저정밀 사용이 배포 시 이점을 만든다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;왜 되는가?&lt;/b&gt; AlexNet 예시처럼 가중치 분포가 좁은 범위에 몰려 있는 경향이 있어, 큰 손실 없이 더 적은 비트로 표현 가능하다. 양자화 대상은 &lt;b&gt;Weights(가중치), Biases(바이어스), Activations(활성값)&lt;/b&gt;이다. 연속적인 분포(실수)를 이산적인 값(계단/막대 형태)으로 바꾸는 과정이다.&lt;/p&gt;
&lt;!-- 9 --&gt;
&lt;h2 id=&quot;sec9&quot; class=&quot;sec-title title-26&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;hl-yellow&quot;&gt;9. Knowledge Distillation (지식 증류)&lt;/span&gt;&lt;/h2&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;지식 증류는 &lt;b&gt;&quot;큰 모델의 지식을 작은 모델로 옮기는 과정&quot;&lt;/b&gt;이다. 큰 네트워크(teacher) &amp;rarr; 작은 네트워크(student) 구조로, 같은 입력 x가 teacher와 student에 동시에 들어간다.&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;Teacher 출력은 Softmax(T=t) 형태로 &lt;b&gt;soft labels(부드러운 확률 분포)&lt;/b&gt;를 생성하고, student도 Softmax(T=t)로 soft predictions를 만든다. teacher soft label과 student soft prediction 사이를 맞추는 &lt;b&gt;distillation loss&lt;/b&gt;를 계산한다. 동시에 student는 Softmax(T=1)에서 실제 정답 y(ground truth, hard label)와의 차이를 &lt;b&gt;student loss&lt;/b&gt;로 학습한다.&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;핵심은 &quot;정답만&quot; 따라가는 것이 아니라, teacher가 내는 &lt;b&gt;클래스 확률분포 전체(soft target)&lt;/b&gt;를 참고해 더 풍부한 정보를 학습한다는 점이다.&lt;/p&gt;
&lt;!-- 10 --&gt;
&lt;h2 id=&quot;sec10&quot; class=&quot;sec-title title-26&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;hl-yellow&quot;&gt;10. TinyML과 M5Core2 배포 워크플로우&lt;/span&gt;&lt;/h2&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;TinyML/Embedded ML은 wearable, camera, IoT sensor 같은 작고 저전력 장치에서 동작한다. MCU 예시로 메모리 16KB, 32-bit Arm Cortex-M4 같은 제한이 강조된다. 따라서 &lt;b&gt;&quot;Shrink &amp;amp; Fit Model into Small Memory&quot;&lt;/b&gt;가 핵심이고, Model Shrinking Concepts로 Quantization(32bits&amp;rarr;8bits), Model Pruning, Distillation/Knowledge Distillation, Model Compression 등이 정리된다.&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Audio ML Workflow: TFLite Model to Arduino (M5Core2)&lt;/b&gt; 흐름은 대략 다음과 같다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;PC에서 모델 학습&lt;/b&gt;: TensorFlow로 학습하고 model.tflite 출력.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;INT8로 양자화&lt;/b&gt;: 컨트롤러에서 돌아가도록 사이즈 축소.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;TFLite &amp;rarr; C 배열로 변환&lt;/b&gt;: xxd -i model_int8.tflite &amp;gt; model_data.cc 같은 형태로 C/C++ 배열화.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Arduino Sketch(M5Core2) 구성&lt;/b&gt;: model_data.cc 추가, 메모리(arena) 설정, TFLM(TensorFlow Lite Micro) 라이브러리, 특징추출(MFCC), 추론 루프(inference loop).&lt;/li&gt;
&lt;li&gt;&lt;b&gt;오디오 캡처 &amp;amp; 특징 추출&lt;/b&gt;: 마이크(i2s, 16kHz), log-mel / MFCC(40). 학습 때와 동일한 설정 유지가 중요하다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;TFLite Micro로 추론&lt;/b&gt;: 루프에서 실행, MFCC 특징 입력, 출력 점수/라벨 읽기.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;M5Core2에 최종 배포&lt;/b&gt;: C array 모델, INT8 양자화, 업로드 및 실행.&lt;/li&gt;
&lt;/ol&gt;
&lt;!-- 11 --&gt;
&lt;h2 id=&quot;sec11&quot; class=&quot;sec-title title-26&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;hl-yellow&quot;&gt;11. 실습 Task I: IMU 데이터 전송 방식 비교&lt;/span&gt;&lt;/h2&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;PC로 IMU 데이터를 캡처할 때 다음 세 가지 프로토콜을 각각 테스트했다. 어떤 것이 더 빠른지, 얼마나 빠른지(속도 측정), 각 프로토콜이 어떻게 동작하는지(작동 원리)를 비교하는 것이 목표다.&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;① BLE (Bluetooth Serial)&lt;/b&gt;&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;M5Core2에서 BluetoothSerial로 &quot;M5Core2_IMU&quot; 이름으로 BLE를 초기화하고, 50Hz(20ms 간격)로 가속도&amp;middot;자이로(ax, ay, az, gx, gy, gz)를 타임스탬프와 함께 한 줄씩 전송한다. PC에서는 페어링 후 해당 COM 포트를 열어 시리얼로 수신하고, CSV로 저장한 뒤 pandas로 시각화한다.&lt;/p&gt;
&lt;div class=&quot;codecard&quot;&gt;
&lt;div class=&quot;head&quot;&gt;M5Core2 BLE 전송 (M5Core2_BTSerial.ino)&lt;/div&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;SerialBT.begin(&quot;M5Core2_IMU&quot;);
// 50Hz: interval 20000 us
SerialBT.printf(&quot;%lu,%.3f,%.3f,%.3f,%.3f,%.3f,%.3f\n&quot;, 
                millis(), ax, ay, az, gx, gy, gz);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div class=&quot;codecard&quot;&gt;
&lt;div class=&quot;head&quot;&gt;Python BLE 수신 및 로깅 (M5Core2_BTSerial_capture.py)&lt;/div&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;ser = serial.Serial(port, BAUD, timeout=1)
while (time.time() - start_time) &amp;lt; RECORD_SECONDS:
    if ser.in_waiting &amp;gt; 0:
        line = ser.readline().decode('utf-8', errors='ignore').strip()
        parts = line.split(',')
        if len(parts) == 7:  # ms, ax, ay, az, gx, gy, gz
            data_list.append(parts)
df.to_csv(OUT_FILE, index=False)  # bt_imu_log.csv&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;② WiFi: UDP&lt;/b&gt;&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;M5Core2는 WiFi에 연결한 뒤, IMU에서 가속도&amp;middot;자이로를 읽고 온보드에서 쿼터니언을 계산(자이로 적분 + 가속도 보정)한다. 계산된 qw, qx, qy, qz를 UDP 패킷으로 PC IP와 포트(예: 12345)로 전송한다. PC에서는 UDP 소켓을 열고 해당 포트에서 수신한 패킷을 파싱해 quaternion_wifi_log.csv로 저장하고 시각화한다.&lt;/p&gt;
&lt;div class=&quot;codecard&quot;&gt;
&lt;div class=&quot;head&quot;&gt;M5Core2 UDP 전송 (M5Core2_UDP_TX.ino)&lt;/div&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;udp.beginPacket(host_ip, udp_port);
udp.printf(&quot;%lu,%.4f,%.4f,%.4f,%.4f&quot;, millis(), qw, qx, qy, qz);
udp.endPacket();
delay(10);  // ~100Hz&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div class=&quot;codecard&quot;&gt;
&lt;div class=&quot;head&quot;&gt;Python UDP 수신 (M5Core2_UDP_RX.py)&lt;/div&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind((UDP_IP, UDP_PORT))
data, addr = sock.recvfrom(1024)
# parts: ms, qw, qx, qy, qz &amp;rarr; quaternion_wifi_log.csv&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;③ ESP-NOW&lt;/b&gt;&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;ESP-NOW는 WiFi 기반이지만 AP/라우터 없이 디바이스 간 직접 통신하는 프로토콜이다. M5Core2(TX)에서 IMU로 쿼터니언을 계산한 뒤 struct_message(ms, qw, qx, qy, qz)로 묶어 esp_now_send로 브로드캐스트 주소에 전송한다. 수신 보드(RX)는 USB로 PC에 연결되어 있고, ESP-NOW로 받은 패킷을 Serial(921600)로 PC에 CSV 형식으로 출력한다. PC에서는 RX가 연결된 COM 포트를 열어 시리얼로 수신해 esp_now_quats.csv 등으로 저장한다. delay(5)로 약 200Hz 수준의 고속 스트림이 가능하다.&lt;/p&gt;
&lt;div class=&quot;codecard&quot;&gt;
&lt;div class=&quot;head&quot;&gt;M5Core2 ESP-NOW TX (M5Core2_espnow_TX.ino)&lt;/div&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;typedef struct struct_message { uint32_t ms; float qw, qx, qy, qz; } struct_message;
esp_now_send(broadcastAddress, (uint8_t *) &amp;amp;myData, sizeof(myData));
delay(5);  // ~200Hz&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div class=&quot;codecard&quot;&gt;
&lt;div class=&quot;head&quot;&gt;ESP-NOW RX &amp;rarr; Serial (M5Core2_espnow_RX.ino)&lt;/div&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;Serial.begin(921600);
void OnDataRecv(...) {
    Serial.printf(&quot;%d,%lu,%.4f,%.4f,%.4f,%.4f\n&quot;, id, ms, qw, qx, qy, qz);
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;실습을 통해 BLE는 페어링과 대역폭 제한, UDP는 WiFi 설정과 지연, ESP-NOW는 저지연&amp;middot;고속이지만 RX 보드가 필요하다는 차이를 체감할 수 있다.&lt;/p&gt;
&lt;!-- 12 --&gt;
&lt;h2 id=&quot;sec12&quot; class=&quot;sec-title title-26&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;hl-yellow&quot;&gt;12. 실습 Task II&amp;middot;III: CNN 압축과 보드 추론&lt;/span&gt;&lt;/h2&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Task II (Open Problem)&lt;/b&gt;에서는 &quot;action&quot; IMU 데이터를 PC로 수집한다. 위에서 성공한 프로토콜(BLE, UDP, ESP-NOW) 중 하나를 사용하거나, 이전 시간에 사용한 Serial(UART)로 캡처한 데이터를 사용할 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;모델 개발&lt;/b&gt;: CNN으로 모델을 개발하고 h5 포맷으로 저장한다. tensorflow-model-optimization 패키지를 사용해 CNN 개발 과정에서 pruning을 적용한다. 패키지 설치가 안 되면 수동(manual) pruning을 한다. model compression %(압축률)를 계산하여 보고한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;최적화/변환&lt;/b&gt;: TFLiteConverter로 float32 &amp;rarr; int8 변환(양자화)한다. tflite 모델로 export하고, 모델을 C++ header 파일로 변환해 임베디드에 올릴 수 있게 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Task III: 결과 비교/개선/보드 추론&lt;/b&gt;&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;비교 대상 모델은 (1) 기본 h5, (2) pruned h5, (3) quantized tflite 세 종류다. 정확도(accuracy)와 시간(timing)을 비교하고 결과를 표로 정리(tabulate)한다. tflite 결과가 나쁘면 개선(improve)하고, 개선된 tflite로 M5Core2에서 실제 추론을 수행한다.&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;이를 통해 &quot;이식성과 효율의 트레이드오프&quot;와 &quot;모델 압축 &amp;rarr; TFLite &amp;rarr; Tiny 디바이스 배포&quot; 전체 파이프라인을 한 번에 경험하게 된다.&lt;/p&gt;
&lt;!-- 13 --&gt;
&lt;h2 id=&quot;sec13&quot; class=&quot;sec-title title-26&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;hl-yellow&quot;&gt;13. 정리&lt;/span&gt;&lt;/h2&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;이번 수업에서는 임베디드 환경에서의 이식성&amp;middot;효율 트레이드오프, ML 모델의 정확도와 연산량 관계, 대표 CNN 비교, 그리고 모델 압축 기법(Pruning, Quantization, Knowledge Distillation)을 이론으로 다뤘다.&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;실습에서는 M5Core2에서 PC로 IMU 데이터를 전송하는 세 가지 방식(BLE, WiFi UDP, ESP-NOW)을 구현하고 비교했고, CNN 모델에 프루닝과 양자화를 적용해 TFLite로 변환한 뒤 정확도&amp;middot;시간을 비교하고 M5Core2에 배포하는 과정까지 맛봤다.&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;TinyML은 제한된 메모리와 전력에서 모델을 &quot;맞춰 넣는&quot; 문제다. 작은 모델은 더 빠르고, 메모리를 덜 쓰고, 전력을 덜 소비한다는 점을 직접 확인하는 시간이었다.&lt;/p&gt;
&lt;style&gt;
#post-root a { color:#495057 !important; text-decoration:none !important; }
#post-root a:hover { color:#212529 !important; border-bottom:1px solid #ced4da; }
#post-root a:visited { color:#666 !important; }

.sec-title { display:block; margin:28px 0 12px; font-weight:800; line-height:1.35; }
.sec-title &gt; .hl-yellow {
  background:linear-gradient(transparent 65%, #fff3bf 65%);
  padding:0 .12em;
}
.title-26 { font-size:26px; }

.para { margin:10px 0 14px; color:#343a40; line-height:1.9; }

.toc {
  border:1px solid #e9ecef;
  border-radius:10px;
  padding:12px 14px;
  margin:18px 0 24px;
  background:#f8f9fa;
}

.codecard {
  background:#f8f9fa;
  border:1px solid #e9ecef;
  border-radius:8px;
  padding:16px;
  margin:16px 0;
  overflow-x:auto;
}

.codecard .head {
  font-weight:700;
  color:#212529;
  margin-bottom:10px;
  font-size:15px;
}

.codecard pre {
  margin:0;
  padding:0;
  background:transparent;
  border:none;
}

.codecard code {
  font-family:'Courier New', monospace;
  font-size:13px;
  line-height:1.6;
  color:#212529;
}
&lt;/style&gt;
&lt;/div&gt;</description>
      <category>UNLV</category>
      <author>빡성</author>
      <guid isPermaLink="true">https://smp0417.tistory.com/72</guid>
      <comments>https://smp0417.tistory.com/72#entry72comment</comments>
      <pubDate>Thu, 19 Feb 2026 02:57:11 +0900</pubDate>
    </item>
    <item>
      <title>[UNLV] - 딥러닝과 합성곱 신경망(8일차)</title>
      <link>https://smp0417.tistory.com/71</link>
      <description>&lt;!-- ========================= --&gt;
&lt;div id=&quot;post-root&quot;&gt;
&lt;div class=&quot;toc&quot;&gt;
&lt;div style=&quot;font-weight: 800; margin-bottom: 8px;&quot;&gt;&amp;nbsp; &amp;nbsp; 목차&lt;/div&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#sec1&quot;&gt;수업 개요: 딥러닝의 기초&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#sec2&quot;&gt;인공 신경망(ANN)의 작동 원리&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#sec3&quot;&gt;ANN 아키텍처 설계 프로세스&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#sec4&quot;&gt;가중치 조정과 역전파&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#sec5&quot;&gt;TensorFlow &amp;amp; Keras&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#sec6&quot;&gt;심층 신경망 (DNN)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#sec7&quot;&gt;합성곱 신경망 (CNN)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#sec8&quot;&gt;실습: 간단한 신경망 예제&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#sec9&quot;&gt;실습: 오디오 분류를 위한 DNN&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#sec10&quot;&gt;실습: 오디오 분류를 위한 CNN&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#sec11&quot;&gt;실습: USB 마이크를 이용한 오디오 녹음&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#sec12&quot;&gt;실습: TFLite 모델 추론 시스템&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#sec13&quot;&gt;정리&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;!-- 1 --&gt;
&lt;h2 id=&quot;sec1&quot; class=&quot;sec-title title-26&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;hl-yellow&quot;&gt;1. 수업 개요: 딥러닝의 기초&lt;/span&gt;&lt;/h2&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;이번 수업은 &lt;b&gt;딥러닝(Deep Learning)&lt;/b&gt;의 기초 개념과 실습을 다뤘다. 인공 신경망(ANN)의 작동 원리부터 시작해, 심층 신경망(DNN)과 합성곱 신경망(CNN)까지 이론과 실습을 함께 진행했다.&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;특히 오디오 분류 문제를 통해 DNN과 CNN의 차이점을 직접 경험했다. DNN은 MFCC 특성의 평균값을 사용하고, CNN은 시간 정보를 보존한 MFCC 시퀀스를 사용한다는 점이 핵심이었다.&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;실습에서는 TensorFlow와 Keras를 사용하여 모델을 구축하고, TFLite로 변환하여 임베디드 시스템에서 실행 가능한 형태로 만들었다.&lt;/p&gt;
&lt;!-- 2 --&gt;
&lt;h2 id=&quot;sec2&quot; class=&quot;sec-title title-26&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;hl-yellow&quot;&gt;2. 인공 신경망(ANN)의 작동 원리&lt;/span&gt;&lt;/h2&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;인공 신경망은 생물학적 뉴런의 작동 방식을 모방한 계산 모델이다. &lt;b&gt;인공 뉴런&lt;/b&gt;은 여러 입력에 가중치를 적용하고, 편향과 함께 합산한 후 활성화 함수를 거쳐 출력을 생성한다.&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;뉴런 구조&lt;/b&gt;:&lt;/p&gt;
&lt;p class=&quot;para&quot; style=&quot;text-align: center; font-family: monospace; background: #f8f9fa; padding: 10px; border-radius: 5px;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;Sigma; = X₁w₁ + X₂w₂ + ... + Xₘwₘ + b = y&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;여기서:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;X₁, X₂, ..., Xₘ&lt;/b&gt;: 입력 값들&lt;/li&gt;
&lt;li&gt;&lt;b&gt;w₁, w₂, ..., wₘ&lt;/b&gt;: 각 입력에 대한 가중치&lt;/li&gt;
&lt;li&gt;&lt;b&gt;b&lt;/b&gt;: 편향(Bias)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&amp;Sigma;&lt;/b&gt;: 합산 유닛(Summation unit)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;y&lt;/b&gt;: 전달 함수(Transfer function)를 거친 최종 출력&lt;/li&gt;
&lt;/ul&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;활성화 함수&lt;/b&gt;: 가장 흔히 사용되는 것은 &lt;b&gt;시그모이드(Sigmoid)&lt;/b&gt; 함수다. 시그모이드는 출력을 0과 1 사이의 값으로 변환하여 비선형성을 추가한다. 최근에는 ReLU(Rectified Linear Unit) 함수도 널리 사용된다.&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;활성화 함수가 없으면 신경망은 선형 변환만 수행할 수 있어 복잡한 패턴을 학습할 수 없다. 비선형 활성화 함수가 있어야 신경망이 복잡한 관계를 모델링할 수 있다.&lt;/p&gt;
&lt;!-- 3 --&gt;
&lt;h2 id=&quot;sec3&quot; class=&quot;sec-title title-26&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;hl-yellow&quot;&gt;3. ANN 아키텍처 설계 프로세스&lt;/span&gt;&lt;/h2&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;신경망을 설계하고 훈련하는 과정은 다음과 같은 단계로 이루어진다:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;입력을 받음&lt;/b&gt;: 데이터를 신경망에 입력한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;편향(Bias) 추가&lt;/b&gt;: 필요시 편향을 추가한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;입력 특징에 무작위 가중치 할당&lt;/b&gt;: 초기 가중치는 보통 [-1, 1] 사이의 작은 무작위 값으로 설정한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;훈련 코드 실행&lt;/b&gt;: 모델을 학습시킨다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;예측 오류 확인&lt;/b&gt;: 모델의 예측과 실제 값 사이의 오차를 계산한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;경사 하강법으로 가중치 업데이트&lt;/b&gt;: 오차를 최소화하는 방향으로 가중치를 조정한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;업데이트된 가중치로 훈련 반복&lt;/b&gt;: 위 과정을 반복하여 모델 성능을 향상시킨다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;예측 수행&lt;/b&gt;: 학습이 완료된 모델로 새로운 데이터에 대한 예측을 수행한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;설계 이슈&lt;/b&gt;:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;초기 가중치 설정&lt;/b&gt;: 보통 [-1, 1] 사이의 작은 무작위 값으로 초기화한다. 잘못된 초기화는 학습을 방해할 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;활성화 함수 선택&lt;/b&gt;: 문제 유형에 따라 적절한 활성화 함수를 선택해야 한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;오차 추정 및 가중치 조정&lt;/b&gt;: 경사 하강법 최적화 알고리즘을 사용한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;뉴런의 수&lt;/b&gt;: 너무 적으면 학습 능력이 부족하고, 너무 많으면 과적합이 발생할 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;데이터 표현 방식&lt;/b&gt;: 입력 데이터를 어떻게 전처리하고 표현할지 결정한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;훈련 세트의 크기&lt;/b&gt;: 충분한 데이터가 있어야 모델이 일반화할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;!-- 4 --&gt;
&lt;h2 id=&quot;sec4&quot; class=&quot;sec-title title-26&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;hl-yellow&quot;&gt;4. 가중치 조정과 역전파&lt;/span&gt;&lt;/h2&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;신경망 학습의 핵심은 &lt;b&gt;가중치 조정&lt;/b&gt;이다. 목표는 반복할 때마다 오차(Error)를 최소화하도록 가중치를 조정하는 것이다.&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;역전파(Back propagation)&lt;/b&gt;: 가중치를 업데이트하는 방향을 결정하는 알고리즘이다. 출력층에서 계산된 오차를 입력층 방향으로 역전파하여 각 가중치의 기여도를 계산한다.&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;역전파는 연쇄 법칙(Chain Rule)을 사용하여 각 가중치의 기여도를 계산한다. 이 과정을 반복하면서 모델이 점진적으로 정확도를 향상시킨다.&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;학습률(Learning Rate)&lt;/b&gt;: 업데이트 시 한 번에 이동할 단계의 크기를 결정하는 튜닝 파라미터다. 학습률이 너무 크면 최적점을 지나쳐버리고, 너무 작으면 학습이 느리게 진행된다.&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;손실 함수(Loss Function)는 모델의 예측과 실제 값 사이의 차이를 측정한다. 경사 하강법을 통해 오차(Cost)를 최소화하는 지점으로 가중치가 업데이트된다.&lt;/p&gt;
&lt;!-- 5 --&gt;
&lt;h2 id=&quot;sec5&quot; class=&quot;sec-title title-26&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;hl-yellow&quot;&gt;5. TensorFlow &amp;amp; Keras&lt;/span&gt;&lt;/h2&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;TensorFlow&lt;/b&gt;: 머신러닝의 복잡한 수학 연산을 수행하는 엔진이다. 수치 계산을 &lt;b&gt;그래프(Graph)&lt;/b&gt;로 표현하는 것이 핵심 아이디어다.&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;TensorFlow에서 노드(Nodes)는 연산(Ops)을 나타내고, 엣지(Edges)는 데이터인 &lt;b&gt;텐서(Tensors)&lt;/b&gt;의 흐름을 나타낸다. 예를 들어, f(x, y, z) = (x + y) * z 같은 수식을 그래프로 구현하여 계산한다.&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Keras&lt;/b&gt;: TensorFlow 기반의 고수준 API로, 모델 구축과 빠른 프로토타이핑에 용이하다. 복잡한 TensorFlow 코드를 간단하게 작성할 수 있게 해준다.&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;TensorFlow Lite (TFLite)&lt;/b&gt;: 리소스가 제한된 장치(모바일, IoT, 임베디드 시스템)를 위한 경량 버전이다. 특징은 다음과 같다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모델 변환 및 최적화&lt;/li&gt;
&lt;li&gt;온디바이스 추론&lt;/li&gt;
&lt;li&gt;저지연&lt;/li&gt;
&lt;li&gt;작은 점유 공간&lt;/li&gt;
&lt;/ul&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;Keras 훈련 옵션:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;옵티마이저(Optimizer)&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;b&gt;sgd&lt;/b&gt;: 확률적 경사 하강법&lt;/li&gt;
&lt;li&gt;&lt;b&gt;adam&lt;/b&gt;: 적응형 학습률 알고리즘&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;손실 함수(Loss)&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;b&gt;binary_crossentropy&lt;/b&gt;: 이진 분류용&lt;/li&gt;
&lt;li&gt;&lt;b&gt;categorical_crossentropy&lt;/b&gt;: 다중 클래스 분류용&lt;/li&gt;
&lt;li&gt;&lt;b&gt;mean_squared_error&lt;/b&gt;: 회귀 문제용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;콜백(Callbacks)&lt;/b&gt;: EarlyStopping(조기 종료), ModelCheckpoint(최적 모델 저장) 등&lt;/li&gt;
&lt;/ul&gt;
&lt;!-- 6 --&gt;
&lt;h2 id=&quot;sec6&quot; class=&quot;sec-title title-26&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;hl-yellow&quot;&gt;6. 심층 신경망 (DNN)&lt;/span&gt;&lt;/h2&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;DNN 정의&lt;/b&gt;: 입력층과 출력층 사이에 &lt;b&gt;여러 개의 은닉층(Hidden layers)&lt;/b&gt;이 있는 신경망이다.&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;일반 NN vs DNN&lt;/b&gt;:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;일반 NN&lt;/b&gt;: 층이 적음 &amp;rarr; 단순한 패턴 학습&lt;/li&gt;
&lt;li&gt;&lt;b&gt;DNN&lt;/b&gt;: 층이 많음 &amp;rarr; 복잡하고 계층적인 패턴 학습&lt;/li&gt;
&lt;/ul&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;DNN은 층이 깊어질수록 더 추상적이고 복잡한 특징을 학습할 수 있다. 첫 번째 은닉층은 저수준 특징을, 마지막 은닉층은 고수준 특징을 학습한다.&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;하지만 층이 너무 많으면 과적합(Overfitting)이 발생할 수 있으므로, &lt;b&gt;드롭아웃(Dropout)&lt;/b&gt; 같은 규제 기법이 필요하다. DNN에서 드롭아웃 비율은 보통 0.3~0.5를 권장한다.&lt;/p&gt;
&lt;!-- 7 --&gt;
&lt;h2 id=&quot;sec7&quot; class=&quot;sec-title title-26&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;hl-yellow&quot;&gt;7. 합성곱 신경망 (CNN)&lt;/span&gt;&lt;/h2&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;CNN은 이미지나 시계열 데이터에서 공간적 또는 시간적 패턴을 학습하는 데 특화된 신경망이다.&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;완전 연결층의 문제&lt;/b&gt;: 이미지를 픽셀 단위로 일렬로 펼쳐 입력하면 파라미터가 폭증하고 &lt;b&gt;공간 정보(Spatial information)&lt;/b&gt;가 손실된다. 예를 들어, 28&amp;times;28 이미지를 784개의 입력으로 펼치면 인접 픽셀 간의 관계가 무너진다.&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;합성곱(Convolution)의 해결책&lt;/b&gt;: 이미지의 작은 패치(Patch)를 은닉층의 단일 뉴런에 연결하여 공간 구조를 보존한다. &lt;b&gt;필터(Filter/Kernel)&lt;/b&gt;가 이미지 위를 슬라이딩하며 연산하여 &lt;b&gt;특징 맵(Feature map)&lt;/b&gt;을 생성한다.&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;합성곱 파라미터&lt;/b&gt;:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;필터 크기(f)&lt;/b&gt;: 필터의 크기 (예: 3&amp;times;3, 5&amp;times;5)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;스트라이드(s)&lt;/b&gt;: 필터가 이동하는 간격&lt;/li&gt;
&lt;li&gt;&lt;b&gt;패딩(p)&lt;/b&gt;: 이미지 주변에 추가하는 영역&lt;/li&gt;
&lt;/ul&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;레이어 스택&lt;/b&gt;: 합성곱, ReLU, 풀링(Pooling) 레이어를 차례로 배치한다. 층이 깊어질수록 필터는 더 크고 복잡한 특징을 배우고, 특징 맵의 크기는 작아진다.&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;배치 정규화(BatchNorm)&lt;/b&gt;: 레이어의 출력을 정규화하여 훈련을 더 빠르고 안정적으로 만든다.&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;완전 연결층&lt;/b&gt;: 합성곱 층에서 추출된 고수준 특징들을 비선형적으로 조합하여 객체를 인식한다.&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;드롭아웃&lt;/b&gt;: CNN에서 드롭아웃 비율은 보통 0.1~0.3을 권장한다. DNN보다 낮은 이유는 합성곱 레이어 자체가 이미 규제 효과가 있기 때문이다.&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;소프트맥스(Softmax) 레이어&lt;/b&gt;: 출력층 직전에 위치하며, 입력 벡터를 확률 분포로 변환하여 반환한다. 모든 출력의 합은 1이며, 출력층과 동일한 수의 노드를 가져야 한다.&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;CNN 아키텍처 요약&lt;/b&gt;:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;특징 학습(Feature Learning)&lt;/b&gt;: INPUT &amp;rarr; CONVOLUTION + RELU &amp;rarr; POOLING 반복&lt;/li&gt;
&lt;li&gt;&lt;b&gt;분류(Classification)&lt;/b&gt;: FLATTEN &amp;rarr; FULLY CONNECTED &amp;rarr; SOFTMAX &amp;rarr; 최종 분류&lt;/li&gt;
&lt;/ol&gt;
&lt;!-- 8 --&gt;
&lt;h2 id=&quot;sec8&quot; class=&quot;sec-title title-26&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;hl-yellow&quot;&gt;8. 실습: 간단한 신경망 예제&lt;/span&gt;&lt;/h2&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;가장 간단한 신경망 예제로 X(-1, 0, 1, 2, 3, 4)와 Y(-3, -1, 1, 3, 5, 7) 사이의 관계를 찾는 문제를 풀었다. 이는 선형 관계 Y = 2X - 1을 학습하는 문제다.&lt;/p&gt;
&lt;div class=&quot;codecard&quot;&gt;
&lt;div class=&quot;head&quot;&gt;간단한 신경망 예제 (simple_NN_example.py)&lt;/div&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;import numpy as np
from tensorflow import keras

# 1. Define the model
model = keras.Sequential([keras.layers.Dense(units=1, input_shape=[1])])

# 2. Compile the model
model.compile(optimizer='sgd', loss='mean_squared_error')

# 3. Define the data
xs = np.array([-1.0, 0.0, 1.0, 2.0, 3.0, 4.0], dtype=float)
ys = np.array([-3.0, -1.0, 1.0, 3.0, 5.0, 7.0], dtype=float)

# 4. Train the model
model.fit(xs, ys, epochs=50)

# 5. Make a prediction
print(model.predict(np.array([10.0])))&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;이 예제는 단일 뉴런으로 구성된 가장 간단한 신경망이다. Dense 레이어 하나만 사용하여 선형 관계를 학습한다. 에포크가 진행될수록 손실(loss) 값이 감소하는 것을 확인할 수 있었다.&lt;/p&gt;
&lt;!-- 9 --&gt;
&lt;h2 id=&quot;sec9&quot; class=&quot;sec-title title-26&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;hl-yellow&quot;&gt;9. 실습: 오디오 분류를 위한 DNN&lt;/span&gt;&lt;/h2&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;오디오 분류 문제를 DNN으로 해결하는 실습을 진행했다. 5가지 클래스(Clap, Cough, Footsteps, Glassbreak, Knock)를 분류하는 모델을 구축했다.&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;DNN의 특징&lt;/b&gt;: MFCC 특성을 추출한 후 &lt;b&gt;시간에 대한 평균&lt;/b&gt;을 계산하여 1D 벡터로 만든다. 이렇게 하면 시간 정보가 손실되지만, 파라미터 수가 줄어들고 계산이 빠르다.&lt;/p&gt;
&lt;div class=&quot;codecard&quot;&gt;
&lt;div class=&quot;head&quot;&gt;DNN 오디오 분류 (DNN_audio_classification.py)&lt;/div&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;import librosa
import numpy as np
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense, Dropout

# MFCC 추출 후 평균 계산 (DNN용)
def extract_features_dnn(file_path, label):
    features = []
    labels = []
    
    y, sr = librosa.load(file_path, sr=22050)
    n_samples = int(0.5 * sr)  # 0.5초 세그먼트
    step = int(0.25 * sr)  # 50% 오버랩
    
    for start in range(0, len(y) - n_samples + 1, step):
        segment = y[start:start + n_samples]
        
        # MFCC 추출 (Shape: n_mfcc, time_steps)
        mfcc = librosa.feature.mfcc(y=segment, sr=sr, n_mfcc=40)
        
        # DNN: 시간에 대한 평균 (1D 벡터)
        mfcc_mean = np.mean(mfcc.T, axis=0)  # Shape: (40,)
        
        features.append(mfcc_mean)
        labels.append(label)
    
    return features, labels

# DNN 모델 구축
model = Sequential([
    Dense(256, activation='relu', input_shape=(40,)),
    Dropout(0.3),
    Dense(128, activation='relu'),
    Dropout(0.3),
    Dense(64, activation='relu'),
    Dense(5, activation='softmax')  # 5개 클래스
])

model.compile(optimizer='adam', 
              loss='categorical_crossentropy', 
              metrics=['accuracy'])

# 모델 학습
history = model.fit(X_train, y_train, 
                    epochs=50, 
                    batch_size=32, 
                    validation_data=(X_test, y_test))

# TFLite로 변환
converter = tf.lite.TFLiteConverter.from_keras_model(model)
tflite_model = converter.convert()
with open('audio_dnn_model.tflite', 'wb') as f:
    f.write(tflite_model)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;DNN 모델은 다음과 같은 구조를 가진다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;입력층: 40차원 MFCC 평균 벡터&lt;/li&gt;
&lt;li&gt;은닉층 1: 256 뉴런, ReLU 활성화, Dropout 0.3&lt;/li&gt;
&lt;li&gt;은닉층 2: 128 뉴런, ReLU 활성화, Dropout 0.3&lt;/li&gt;
&lt;li&gt;은닉층 3: 64 뉴런, ReLU 활성화&lt;/li&gt;
&lt;li&gt;출력층: 5 뉴런, Softmax 활성화&lt;/li&gt;
&lt;/ul&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;학습 결과를 평가하기 위해 정확도, 정밀도, 재현율, F1-Score를 계산하고, 혼동 행렬(Confusion Matrix)을 시각화했다.&lt;/p&gt;
&lt;!-- 10 --&gt;
&lt;h2 id=&quot;sec10&quot; class=&quot;sec-title title-26&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;hl-yellow&quot;&gt;10. 실습: 오디오 분류를 위한 CNN&lt;/span&gt;&lt;/h2&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;같은 오디오 분류 문제를 CNN으로 해결하는 실습을 진행했다. CNN은 시간 정보를 보존한 MFCC 시퀀스를 사용한다는 점이 DNN과 다르다.&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;CNN의 특징&lt;/b&gt;: MFCC 특성을 추출한 후 &lt;b&gt;전체 시간 시퀀스&lt;/b&gt;를 그대로 사용한다. 이렇게&amp;nbsp;하면&amp;nbsp;시간적&amp;nbsp;패턴을&amp;nbsp;학습할&amp;nbsp;수&amp;nbsp;있다.&amp;nbsp;합성곱&amp;nbsp;층은&amp;nbsp;parameter&amp;nbsp;sharing으로&amp;nbsp;파라미터&amp;nbsp;수는&amp;nbsp;FC에&amp;nbsp;비해&amp;nbsp;적게&amp;nbsp;유지되지만,&amp;nbsp;2D&amp;nbsp;시퀀스&amp;nbsp;전체에&amp;nbsp;대해&amp;nbsp;연산하므로&amp;nbsp;계산은&amp;nbsp;더&amp;nbsp;복잡해진다.&lt;/p&gt;
&lt;div class=&quot;codecard&quot;&gt;
&lt;div class=&quot;head&quot;&gt;CNN 오디오 분류 (CNN_audio_classification.py)&lt;/div&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;import librosa
import numpy as np
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Conv1D, MaxPooling1D, Flatten, Dense, Dropout

# MFCC 추출 후 시퀀스 유지 (CNN용)
def extract_features_cnn(file_path, label):
    features = []
    labels = []
    
    y, sr = librosa.load(file_path, sr=22050)
    n_samples = int(0.5 * sr)
    step = int(0.25 * sr)
    
    for start in range(0, len(y) - n_samples + 1, step):
        segment = y[start:start + n_samples]
        
        # MFCC 추출
        mfcc = librosa.feature.mfcc(y=segment, sr=sr, n_mfcc=40)
        
        # CNN: 시간 시퀀스 유지 (2D 배열)
        mfcc = mfcc.T  # Shape: (time_steps, 40)
        
        features.append(mfcc)
        labels.append(label)
    
    return features, labels

# 1D CNN 모델 구축
input_shape = (22, 40)  # (time_steps, n_mfcc)

model = Sequential([
    # 합성곱 레이어 1
    Conv1D(filters=32, kernel_size=3, activation='relu', input_shape=input_shape),
    MaxPooling1D(pool_size=2),
    Dropout(0.2),
    
    # 합성곱 레이어 2
    Conv1D(filters=64, kernel_size=3, activation='relu'),
    MaxPooling1D(pool_size=2),
    Dropout(0.2),
    
    # 완전 연결층
    Flatten(),
    Dense(64, activation='relu'),
    Dropout(0.3),
    Dense(5, activation='softmax')
])

model.compile(optimizer='adam', 
              loss='categorical_crossentropy', 
              metrics=['accuracy'])

# 모델 학습
history = model.fit(X_train, y_train, 
                    epochs=30, 
                    batch_size=32, 
                    validation_data=(X_test, y_test))

# TFLite로 변환
converter = tf.lite.TFLiteConverter.from_keras_model(model)
tflite_model = converter.convert()
with open('audio_cnn_model.tflite', 'wb') as f:
    f.write(tflite_model)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;CNN 모델은 다음과 같은 구조를 가진다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;입력층: (22, 40) 형태의 MFCC 시퀀스&lt;/li&gt;
&lt;li&gt;합성곱 레이어 1: 32개 필터, 커널 크기 3, MaxPooling, Dropout 0.2&lt;/li&gt;
&lt;li&gt;합성곱 레이어 2: 64개 필터, 커널 크기 3, MaxPooling, Dropout 0.2&lt;/li&gt;
&lt;li&gt;Flatten: 2D 특징 맵을 1D로 변환&lt;/li&gt;
&lt;li&gt;완전 연결층: 64 뉴런, ReLU 활성화, Dropout 0.3&lt;/li&gt;
&lt;li&gt;출력층: 5 뉴런, Softmax 활성화&lt;/li&gt;
&lt;/ul&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;데이터의 시간적&amp;middot;공간적 국소 패턴이 중요한 문제에서는 CNN이 일반화에 더 유리한 경향을 보인다. 모델 크기와 계산 비용은 네트워크 설계(층 수, 필터 수, 완전 연결층 규모 등)에 따라 달라지며, 많은 CNN에서도 파라미터의 상당 부분은 마지막 완전 연결층이 차지한다.&lt;/span&gt;&lt;/p&gt;
&lt;!-- 11 --&gt;
&lt;h2 id=&quot;sec11&quot; class=&quot;sec-title title-26&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;hl-yellow&quot;&gt;11. 실습: USB 마이크를 이용한 오디오 녹음&lt;/span&gt;&lt;/h2&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;Raspberry Pi Zero W2에 USB 마이크를 연결하여 오디오를 녹음하는 실습을 진행했다. 이는 오디오 분류 모델 학습을 위한 데이터 수집 과정이다.&lt;/p&gt;
&lt;div class=&quot;codecard&quot;&gt;
&lt;div class=&quot;head&quot;&gt;USB 마이크 녹음 (rpi_USB_mic.py)&lt;/div&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;import sounddevice as sd
from scipy.io.wavfile import write
import numpy as np

# 설정
DURATION = 2  # 녹음 시간 (초)
SAMPLE_RATE = 22050  # 샘플링 레이트
OUTPUT_FILENAME = &quot;rpi_mic_recording.wav&quot;

def list_input_devices():
    &quot;&quot;&quot;사용 가능한 입력 장치 목록 출력&quot;&quot;&quot;
    print(&quot;\n--- Available Audio Devices ---&quot;)
    devices = sd.query_devices()
    input_devices = []
    
    for i, device in enumerate(devices):
        if device['max_input_channels'] &amp;gt; 0:
            print(f&quot;ID {i}: {device['name']}&quot;)
            input_devices.append(i)
    return input_devices

def record_from_device(device_id):
    print(f&quot;\n녹음 준비: {DURATION}초, Device ID {device_id}...&quot;)
    
    try:
        # 녹음 시작
        audio_data = sd.rec(int(DURATION * SAMPLE_RATE),
                            samplerate=SAMPLE_RATE,
                            channels=1,
                            device=device_id,
                            dtype='int16')
        
        print(&quot;녹음 중... (지금 말하세요!)&quot;)
        sd.wait()  # 녹음 완료까지 대기
        print(&quot;완료.&quot;)
        
        # 파일로 저장
        write(OUTPUT_FILENAME, SAMPLE_RATE, audio_data)
        print(f&quot;저장 완료: '{OUTPUT_FILENAME}'&quot;)
        
    except Exception as e:
        print(f&quot;녹음 오류: {e}&quot;)

if __name__ == &quot;__main__&quot;:
    # 1. 장치 목록 출력
    valid_ids = list_input_devices()
    
    # 2. 사용자가 USB 마이크 ID 선택
    print(&quot;\n위 목록에서 'USB Audio Device' 또는 유사한 이름을 찾으세요.&quot;)
    try:
        selected_id = int(input(&quot;USB 마이크의 ID 번호를 입력하세요: &quot;))
        
        if selected_id in valid_ids:
            record_from_device(selected_id)
        else:
            record_from_device(selected_id)  # 폴백 체크
            
    except ValueError:
        print(&quot;잘못된 입력입니다. 숫자를 입력하세요.&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;이 스크립트는 다음과 같은 기능을 제공한다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용 가능한 오디오 입력 장치 목록 출력&lt;/li&gt;
&lt;li&gt;사용자가 USB 마이크를 선택&lt;/li&gt;
&lt;li&gt;지정된 시간 동안 오디오 녹음&lt;/li&gt;
&lt;li&gt;WAV 파일로 저장&lt;/li&gt;
&lt;/ul&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;녹음된 오디오는 이후 MFCC 특성 추출 및 모델 학습에 사용된다.&lt;/p&gt;
&lt;!-- 12 --&gt;
&lt;h2 id=&quot;sec12&quot; class=&quot;sec-title title-26&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;hl-yellow&quot;&gt;12. 실습: TFLite 모델 추론 시스템&lt;/span&gt;&lt;/h2&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;학습된 모델을 TFLite로 변환하고, 실제 오디오 파일에 대해 추론을 수행하는 시스템을 구축했다.&lt;/p&gt;
&lt;div class=&quot;codecard&quot;&gt;
&lt;div class=&quot;head&quot;&gt;TFLite 추론 시스템 (CNN_classfication_inference_sys.py)&lt;/div&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;import numpy as np
import librosa
import tensorflow.lite as tflite

# 설정
MODEL_TYPE = 'dnn'  # 또는 'cnn'
MODEL_PATH = f&quot;audio_{MODEL_TYPE}_model.tflite&quot;
TEST_AUDIO_FILE = &quot;test_audio.wav&quot;

SAMPLE_RATE = 22050
DURATION = 0.5
OVERLAP = 0.25
N_MFCC = 40
CLASSES = ['Clap', 'Cough', 'Footsteps', 'Glassbreak', 'Knock']

def preprocess_dnn(segment, sr):
    &quot;&quot;&quot;DNN 전처리: MFCC 평균 계산&quot;&quot;&quot;
    mfccs = librosa.feature.mfcc(y=segment, sr=sr, n_mfcc=N_MFCC)
    features = np.mean(mfccs.T, axis=0)
    return features.reshape(1, -1).astype(np.float32)

def preprocess_cnn(segment, sr, target_time_steps):
    &quot;&quot;&quot;CNN 전처리: MFCC 시퀀스 유지&quot;&quot;&quot;
    mfccs = librosa.feature.mfcc(y=segment, sr=sr, n_mfcc=N_MFCC)
    features = mfccs.T  # (time_steps, n_mfcc)
    
    # 고정 길이로 패딩 또는 자르기
    if features.shape[0] &amp;lt; target_time_steps:
        pad_width = target_time_steps - features.shape[0]
        features = np.pad(features, ((0, pad_width), (0, 0)), mode='constant')
    else:
        features = features[:target_time_steps, :]
    
    return np.expand_dims(features, axis=0).astype(np.float32)

def run_inference(audio_path, model_path, model_type):
    # 1. 모델 로드
    interpreter = tflite.Interpreter(model_path=model_path)
    interpreter.allocate_tensors()
    
    input_details = interpreter.get_input_details()
    output_details = interpreter.get_output_details()
    
    input_index = input_details[0]['index']
    output_index = output_details[0]['index']
    
    # 2. 오디오 로드
    y, sr = librosa.load(audio_path, sr=SAMPLE_RATE)
    
    # 3. 슬라이딩 윈도우 추론
    n_samples = int(DURATION * sr)
    step = int((DURATION - OVERLAP) * sr)
    
    predictions = []
    
    for i in range(0, len(y) - n_samples + 1, step):
        segment = y[i: i + n_samples]
        
        # 전처리
        if model_type == 'dnn':
            input_data = preprocess_dnn(segment, sr)
        elif model_type == 'cnn':
            target_time_steps = input_details[0]['shape'][1]
            input_data = preprocess_cnn(segment, sr, target_time_steps)
        
        # 추론
        interpreter.set_tensor(input_index, input_data)
        interpreter.invoke()
        output_data = interpreter.get_tensor(output_index)
        
        # 결과 디코딩
        pred_idx = np.argmax(output_data[0])
        confidence = output_data[0][pred_idx]
        label = CLASSES[pred_idx]
        
        start_time = i / sr
        end_time = (i + n_samples) / sr
        
        predictions.append(label)
        print(f&quot;Time {start_time:.2f}s - {end_time:.2f}s: {label:&amp;lt;10} (Conf: {confidence:.2f})&quot;)
    
    # 4. 최종 다수결 투표
    from collections import Counter
    most_common = Counter(predictions).most_common(1)
    print(f&quot;\nFINAL PREDICTION: {most_common[0][0]}&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;이 추론 시스템은 다음과 같은 기능을 제공한다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;TFLite 모델 로드 및 초기화&lt;/li&gt;
&lt;li&gt;오디오 파일을 세그먼트로 분할&lt;/li&gt;
&lt;li&gt;각 세그먼트에 대해 추론 수행&lt;/li&gt;
&lt;li&gt;다수결 투표로 최종 예측 결정&lt;/li&gt;
&lt;/ul&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;슬라이딩 윈도우 방식을 사용하여 긴 오디오 파일도 처리할 수 있다. 각 세그먼트의 예측 결과를 수집한 후, 가장 많이 예측된 클래스를 최종 결과로 선택한다.&lt;/p&gt;
&lt;!-- 13 --&gt;
&lt;h2 id=&quot;sec13&quot; class=&quot;sec-title title-26&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;hl-yellow&quot;&gt;13. 정리&lt;/span&gt;&lt;/h2&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;이번 수업을 통해 딥러닝의 기초 개념부터 실전 적용까지 전 과정을 경험했다. 인공 신경망의 작동 원리를 이해하고, DNN과 CNN의 차이점을 오디오 분류 문제를 통해 직접 확인했다.&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;특히 중요한 것은 &lt;b&gt;&quot;DNN은 시간 정보를 평균내어 손실하지만 빠르고, CNN은 시간 정보를 보존하여 더 정확하지만 계산 비용이 높다&quot;&lt;/b&gt;는 점이었다. 문제의 특성에 따라 적절한 모델을 선택하는 것이 중요하다.&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;TensorFlow와 Keras를 사용하여 모델을 구축하고, TFLite로 변환하여 임베디드 시스템에서 실행 가능한 형태로 만드는 과정을 배웠다. 이는 실제 IoT 프로젝트에서 매우 유용한 기술이다.&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;이 수업에서 다룬 내용은 앞으로 이미지 분류, 음성 인식, 센서 데이터 분석 등 다양한 딥러닝 프로젝트에 적용할 수 있다. 특히 CNN의 합성곱 연산과 풀링의 개념은 컴퓨터 비전 분야에서 필수적이다.&lt;/p&gt;
&lt;style&gt;
#post-root a { color:#495057 !important; text-decoration:none !important; }
#post-root a:hover { color:#212529 !important; border-bottom:1px solid #ced4da; }
#post-root a:visited { color:#666 !important; }

.sec-title { display:block; margin:28px 0 12px; font-weight:800; line-height:1.35; }
.sec-title &gt; .hl-yellow {
  background:linear-gradient(transparent 65%, #fff3bf 65%);
  padding:0 .12em;
}
.title-26 { font-size:26px; }

.para { margin:10px 0 14px; color:#343a40; line-height:1.9; }

.toc {
  border:1px solid #e9ecef;
  border-radius:10px;
  padding:12px 14px;
  margin:18px 0 24px;
  background:#f8f9fa;
}

.codecard {
  background:#f8f9fa;
  border:1px solid #e9ecef;
  border-radius:8px;
  padding:16px;
  margin:16px 0;
  overflow-x:auto;
}

.codecard .head {
  font-weight:700;
  color:#212529;
  margin-bottom:10px;
  font-size:15px;
}

.codecard pre {
  margin:0;
  padding:0;
  background:transparent;
  border:none;
}

.codecard code {
  font-family:'Courier New', monospace;
  font-size:13px;
  line-height:1.6;
  color:#212529;
}
&lt;/style&gt;
&lt;/div&gt;</description>
      <category>UNLV</category>
      <author>빡성</author>
      <guid isPermaLink="true">https://smp0417.tistory.com/71</guid>
      <comments>https://smp0417.tistory.com/71#entry71comment</comments>
      <pubDate>Sat, 14 Feb 2026 02:34:01 +0900</pubDate>
    </item>
    <item>
      <title>[UNLV] - 데이터 엔지니어링과 MQTT 스트리밍(7일차)</title>
      <link>https://smp0417.tistory.com/70</link>
      <description>&lt;!-- ========================= --&gt;
&lt;div id=&quot;post-root&quot;&gt;
&lt;div class=&quot;toc&quot;&gt;
&lt;div style=&quot;font-weight: 800; margin-bottom: 8px;&quot;&gt;&amp;nbsp; &amp;nbsp; 목차&lt;/div&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#sec1&quot;&gt;수업 개요: 데이터 엔지니어링과 스트리밍&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#sec2&quot;&gt;데이터 엔지니어링의 정의와 프로세스&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#sec3&quot;&gt;데이터 수집 (Data Collection)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#sec4&quot;&gt;MQTT 프로토콜과 Pub/Sub 모델&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#sec5&quot;&gt;데이터 전처리 (Data Pre-processing)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#sec6&quot;&gt;Data Drift와 Concept Drift&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#sec7&quot;&gt;실습: M5Core2 + AHT20 센서로 MQTT 발행&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#sec8&quot;&gt;실습: Python MQTT 구독자 및 데이터 로깅&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#sec9&quot;&gt;실습: 다중 디바이스 데이터 집계&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#sec10&quot;&gt;실습: RPi + PCT2075 센서로 MQTT 발행&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#sec11&quot;&gt;시뮬레이션: 합성 데이터 생성 및 전처리&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#sec12&quot;&gt;정리&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;!-- 1 --&gt;
&lt;h2 id=&quot;sec1&quot; class=&quot;sec-title title-26&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;hl-yellow&quot;&gt;1. 수업 개요: 데이터 엔지니어링과 스트리밍&lt;/span&gt;&lt;/h2&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;이번 수업은 &lt;b&gt;데이터 엔지니어링(Data Engineering)&lt;/b&gt;과 실시간 데이터 스트리밍에 대해 다뤘다. 데이터 엔지니어링은 분석 및 의사 결정을 위해 원시 데이터를 수집, 처리하고 구조화된 형식으로 변환하는 프로세스와 기술을 포함한다.&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;특히 IoT 환경에서 여러 센서로부터 실시간으로 데이터를 수집하고 처리하는 방법을 &lt;b&gt;MQTT(Message Queuing Telemetry Transport)&lt;/b&gt; 프로토콜을 통해 실습했다. MQTT는 경량 메시징 프로토콜로, IoT 디바이스 간 통신에 최적화되어 있다.&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;실습에서는 M5Stack Core2와 Raspberry Pi Zero W2를 사용하여 엔드 투 엔드 IoT 데이터 파이프라인을 구축했다. 센서 데이터를 MQTT로 발행하고, Python으로 구독하여 CSV 파일로 저장하고 시각화하는 전체 과정을 경험했다.&lt;/p&gt;
&lt;!-- 2 --&gt;
&lt;h2 id=&quot;sec2&quot; class=&quot;sec-title title-26&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;hl-yellow&quot;&gt;2. 데이터 엔지니어링의 정의와 프로세스&lt;/span&gt;&lt;/h2&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;데이터 엔지니어링은 &lt;b&gt;원시 데이터를 수집, 처리 및 구조화되고 사용 가능한 형식으로 변환하는 프로세스와 기술&lt;/b&gt;을 포함한다. 머신러닝 모델이 제대로 작동하려면 고품질의 데이터가 필요하며, 데이터 엔지니어링은 이를 보장하는 핵심 단계다.&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;데이터 엔지니어링의 주요 프로세스는 다음과 같다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;수집 (Gathering / Collection)&lt;/b&gt;: 다양한 소스로부터 데이터를 가져온다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;정제 (Refinement)&lt;/b&gt;: 데이터를 정제하고 변환한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;유지 관리 (Sustainment)&lt;/b&gt;: 데이터 파이프라인을 지속적으로 관리한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;처리 (Processing)&lt;/b&gt;: 데이터를 분석 가능한 형태로 가공한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;검증 (Validation)&lt;/b&gt;: 데이터 품질을 확인한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;저장소 (Storage)&lt;/b&gt;: 데이터를 적절한 저장소에 저장한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;보안 (Security)&lt;/b&gt;: 데이터 접근 권한과 프라이버시를 관리한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;증강 (Augmentation)&lt;/b&gt;: 데이터에 추가 정보를 결합한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;라벨링 (Labeling)&lt;/b&gt;: 지도 학습을 위한 레이블을 생성한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;버전 관리 (Versioning)&lt;/b&gt;: 데이터셋의 버전을 추적한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;데이터 엔지니어링에서 중요한 것은 &lt;b&gt;요구사항 정의&lt;/b&gt;다. 문제 정의, 권한 및 권리, 기계 및 인간이 사용 가능한 형식 등을 명확히 해야 한다.&lt;/p&gt;
&lt;!-- 3 --&gt;
&lt;h2 id=&quot;sec3&quot; class=&quot;sec-title title-26&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;hl-yellow&quot;&gt;3. 데이터 수집 (Data Collection)&lt;/span&gt;&lt;/h2&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;데이터 수집은 데이터 엔지니어링의 첫 단계다. 다양한 소스로부터 원시 데이터를 가져와야 한다.&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;데이터 소스 (Sources of Data)&lt;/b&gt;:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;센서&lt;/b&gt;: IoT, IMU, 환경, 의료, 산업용 센서&lt;/li&gt;
&lt;li&gt;&lt;b&gt;로그&lt;/b&gt;: 시스템, 애플리케이션, 클릭스트림, 텔레메트리&lt;/li&gt;
&lt;li&gt;&lt;b&gt;API&lt;/b&gt;: 금융, 날씨, 소셜 미디어 등 외부 API&lt;/li&gt;
&lt;li&gt;&lt;b&gt;데이터베이스&lt;/b&gt;: SQL/NoSQL 엑스포트&lt;/li&gt;
&lt;li&gt;&lt;b&gt;사용자 생성 데이터&lt;/b&gt;: 이미지, 텍스트, 라벨, 크라우드소싱&lt;/li&gt;
&lt;li&gt;&lt;b&gt;공공 데이터셋&lt;/b&gt;: UCI, Kaggle, OpenML&lt;/li&gt;
&lt;/ul&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;수집 방법 (Collection Methods)&lt;/b&gt;:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;배치 데이터 캡처&lt;/b&gt;: CSV/JSON 덤프 등 주기적 수집&lt;/li&gt;
&lt;li&gt;&lt;b&gt;스트리밍 데이터 캡처&lt;/b&gt;: Kafka, MQTT, RabbitMQ 등 실시간 수집&lt;/li&gt;
&lt;li&gt;&lt;b&gt;엣지/임베디드 수집&lt;/b&gt;: TinyML 장치, 모바일 앱&lt;/li&gt;
&lt;/ul&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;도전 과제 (Challenges)&lt;/b&gt;:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;데이터 라벨링 및 주석&lt;/b&gt;: 수동, 반자동, 능동 학습&lt;/li&gt;
&lt;li&gt;&lt;b&gt;데이터 프라이버시 및 윤리&lt;/b&gt;: 동의, 익명화&lt;/li&gt;
&lt;li&gt;&lt;b&gt;데이터 품질&lt;/b&gt;: 누락, 노이즈, 중복, 편향&lt;/li&gt;
&lt;/ul&gt;
&lt;!-- 4 --&gt;
&lt;h2 id=&quot;sec4&quot; class=&quot;sec-title title-26&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;hl-yellow&quot;&gt;4. MQTT 프로토콜과 Pub/Sub 모델&lt;/span&gt;&lt;/h2&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;MQTT는 IoT 환경에서 널리 사용되는 경량 메시징 프로토콜이다. &lt;b&gt;Publish/Subscribe (Pub/Sub) 모델&lt;/b&gt;을 사용하여 데이터 소스와 데이터 소비자를 분리(Decouple)한다.&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;전통적인 &quot;Polling&quot; 방식과 달리 MQTT는 &lt;b&gt;&quot;Push&quot; 모델&lt;/b&gt;을 사용한다. 장치는 데이터가 있을 때만 전송하고 서버는 즉시 수신한다. 이는 배터리 소비를 줄이고 실시간성을 향상시킨다.&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;MQTT 데이터 흐름 파이프라인&lt;/b&gt;:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;수집 (Publishers)&lt;/b&gt;: 센서(엔드 디바이스)가 깨어나 데이터를 읽고 특정 Topic(예: factory/machine_01/temp)으로 페이로드를 발행한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;전송 (Broker)&lt;/b&gt;: 중앙 MQTT 브로커(예: Mosquitto)가 메시지를 수신하고, 해당 토픽을 구독하는 대상에게 데이터를 필터링하여 라우팅한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;처리 (Subscribers)&lt;/b&gt;: 백엔드 시스템(DB, 대시보드, AI 모델)이 관심 토픽을 구독하여 저장 또는 분석을 위해 실시간으로 수신한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;MQTT의 장점은 다음과 같다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;경량 프로토콜로 저전력 디바이스에 적합&lt;/li&gt;
&lt;li&gt;Pub/Sub 모델로 확장성이 뛰어남&lt;/li&gt;
&lt;li&gt;Topic 기반 필터링으로 효율적인 메시지 라우팅&lt;/li&gt;
&lt;li&gt;QoS(Quality of Service) 레벨로 신뢰성 보장&lt;/li&gt;
&lt;/ul&gt;
&lt;!-- 5 --&gt;
&lt;h2 id=&quot;sec5&quot; class=&quot;sec-title title-26&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;hl-yellow&quot;&gt;5. 데이터 전처리 (Data Pre-processing)&lt;/span&gt;&lt;/h2&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;데이터 과학자는 시간의 50% 이상을 데이터 전처리에 소비한다. 데이터 수집은 두 번째로 많은 시간이 소요되는 구성 요소이며, 알고리즘 튜닝은 적은 부분을 차지한다.&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;시간 소요 비중:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터 전처리 (Data preprocessing): 60%&lt;/li&gt;
&lt;li&gt;데이터 수집 (Collecting Data): 19%&lt;/li&gt;
&lt;li&gt;데이터 패턴 인식 (Recognizing Data Patterns): 9%&lt;/li&gt;
&lt;li&gt;학습 데이터 구축 (Constructing Training Data): 4%&lt;/li&gt;
&lt;li&gt;알고리즘 튜닝 (Tuning Algorithm): 3%&lt;/li&gt;
&lt;li&gt;기타 (Others): 5%&lt;/li&gt;
&lt;/ul&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;왜 데이터 전처리를 하는가?&lt;/b&gt;&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;원시 데이터는 다음과 같은 문제를 포함할 수 있다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;불완전함 (Incomplete)&lt;/b&gt;: 속성이 부족하거나 결측값이 포함됨&lt;/li&gt;
&lt;li&gt;&lt;b&gt;노이즈 (Noisy)&lt;/b&gt;: 이상치와 같은 잘못된 레코드 포함&lt;/li&gt;
&lt;li&gt;&lt;b&gt;불일치 (Inconsistent)&lt;/b&gt;: 충돌하는 레코드나 불일치 발생&lt;/li&gt;
&lt;li&gt;&lt;b&gt;결측값 (Missing values)&lt;/b&gt;: 일부 속성이 비어있거나 NULL임&lt;/li&gt;
&lt;li&gt;&lt;b&gt;유효하지 않은 값 (Invalid Values)&lt;/b&gt;: 성별 등 속성에 잘못된 값 입력&lt;/li&gt;
&lt;li&gt;&lt;b&gt;고유성 (Uniqueness)&lt;/b&gt;: 동일 식별자의 반복된 값&lt;/li&gt;
&lt;li&gt;&lt;b&gt;오타 (Misspellings)&lt;/b&gt;: 잘못 적힌 값들&lt;/li&gt;
&lt;/ul&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;데이터 전처리 기법&lt;/b&gt;:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;결측값 (Missing Values)&lt;/b&gt;: 제거 또는 대체(평균, 중앙값, 보간법 등)로 처리&lt;/li&gt;
&lt;li&gt;&lt;b&gt;이상치 (Outliers)&lt;/b&gt;: 모델 왜곡을 방지하기 위해 극단값을 감지하고 처리&lt;/li&gt;
&lt;li&gt;&lt;b&gt;데이터 스케일링 (Data Scaling)&lt;/b&gt;: 모든 변수가 동등하게 기여하도록 정규화 또는 표준화
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;b&gt;정규화 (Normalization)&lt;/b&gt;: 데이터를 0-1 사이의 고정 범위로 재조정&lt;/li&gt;
&lt;li&gt;&lt;b&gt;표준화 (Standardization)&lt;/b&gt;: 평균 0, 분산 1로 변환 (z-score)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;다운샘플링 (Down-sampling)&lt;/b&gt;: 계산/저장 비용을 줄이기 위해 데이터 속도나 샘플 수 감소&lt;/li&gt;
&lt;li&gt;&lt;b&gt;업샘플링 (Up-sampling)&lt;/b&gt;: 데이터 복제 또는 합성을 통해 샘플 속도를 높이거나 클래스 불균형 해소&lt;/li&gt;
&lt;li&gt;&lt;b&gt;노이즈 필터링 (Noise Filtering)&lt;/b&gt;: 센서 데이터에서 원치 않는 고주파 또는 무작위 변동 제거&lt;/li&gt;
&lt;li&gt;&lt;b&gt;평활화 (Smoothing)&lt;/b&gt;: 이동 평균 등을 적용하여 단기 변동 감소&lt;/li&gt;
&lt;li&gt;&lt;b&gt;인코딩 (Encoding)&lt;/b&gt;: 범주형 데이터를 수치 표현(One-hot 등)으로 변환&lt;/li&gt;
&lt;/ul&gt;
&lt;!-- 6 --&gt;
&lt;h2 id=&quot;sec6&quot; class=&quot;sec-title title-26&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;hl-yellow&quot;&gt;6. Data Drift와 Concept Drift&lt;/span&gt;&lt;/h2&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;머신러닝 모델을 배포한 후에도 데이터는 계속 변화한다. 이러한 변화를 감지하고 대응하는 것이 중요하다.&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Data Drift&lt;/b&gt;: 작업(Task)은 변하지 않지만, 시간이 지남에 따라 입력 데이터의 통계적 분포가 변하는 현상이다. 예를 들어, 센서가 노화되거나 환경이 변하면 데이터 분포가 달라질 수 있다.&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Concept Drift&lt;/b&gt;: 시간이 지남에 따라 입력 데이터와 타겟 라벨 사이의 근본적인 관계가 변하는 현상이다. 예를 들어, 사용자 행동 패턴이 바뀌면 모델의 결정 경계가 더 이상 유효하지 않을 수 있다.&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;비교&lt;/b&gt;:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Data Drift&lt;/b&gt;: 입력 피처(x)의 분포 변화&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Concept Drift&lt;/b&gt;: 결정 경계(Decision boundary)의 변화, 피처-라벨 관계 변화&lt;/li&gt;
&lt;/ul&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;두 현상 모두 모델 성능 저하를 야기할 수 있으므로, 지속적인 모니터링과 재학습이 필요하다.&lt;/p&gt;
&lt;!-- 7 --&gt;
&lt;h2 id=&quot;sec7&quot; class=&quot;sec-title title-26&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;hl-yellow&quot;&gt;7. 실습: M5Core2 + AHT20 센서로 MQTT 발행&lt;/span&gt;&lt;/h2&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;실습에서는 M5Stack Core2에 AHT20 온습도 센서를 연결하고, 센서 데이터를 MQTT 브로커로 발행하는 시스템을 구축했다.&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;하드웨어 구성&lt;/b&gt;:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;M5Stack Core2 (Wi-Fi 지원)&lt;/li&gt;
&lt;li&gt;AHT20 온습도 센서 (I2C 통신)&lt;/li&gt;
&lt;li&gt;Raspberry Pi Zero W2 (MQTT 브로커)&lt;/li&gt;
&lt;/ul&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;배선 (AHT20 to M5Core2 Port A)&lt;/b&gt;:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;GND (Black) &amp;rarr; GND&lt;/li&gt;
&lt;li&gt;5V (Red) &amp;rarr; VIN/VCC&lt;/li&gt;
&lt;li&gt;G32 (White) &amp;rarr; SDA&lt;/li&gt;
&lt;li&gt;G33 (Yellow) &amp;rarr; SCL&lt;/li&gt;
&lt;/ul&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;필요한 라이브러리&lt;/b&gt;:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;M5Unified (M5Stack 통합 라이브러리)&lt;/li&gt;
&lt;li&gt;PubSubClient (MQTT 클라이언트)&lt;/li&gt;
&lt;li&gt;ArduinoJson (JSON 처리)&lt;/li&gt;
&lt;li&gt;Adafruit AHTX0 (AHT20 센서 드라이버)&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&quot;codecard&quot;&gt;
&lt;div class=&quot;head&quot;&gt;M5Core2 AHT20 MQTT Publisher (M5Core2_AHT20_MQTT.ino)&lt;/div&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;#include &amp;lt;M5Unified.h&amp;gt;
#include &amp;lt;WiFi.h&amp;gt;
#include &amp;lt;PubSubClient.h&amp;gt;
#include &amp;lt;ArduinoJson.h&amp;gt;
#include &amp;lt;Adafruit_AHTX0.h&amp;gt;

// --- USER CONFIGURATION ---
const char* ssid = &quot;YOUR_WIFI_SSID&quot;;
const char* password = &quot;YOUR_WIFI_PASSWORD&quot;;
const char* mqtt_server = &quot;192.168.1.100&quot;; // Replace with your RPi IP
const int mqtt_port = 1883;
const char* location = &quot;Lab_M5_Unit1&quot;; 
// --------------------------

WiFiClient espClient;
PubSubClient client(espClient);
Adafruit_AHTX0 aht;

void setup_wifi() {
  delay(10);
  M5.Display.print(&quot;Connecting to WiFi...&quot;);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    M5.Display.print(&quot;.&quot;);
  }
  M5.Display.println(&quot;\nWiFi connected&quot;);
}

void reconnect() {
  while (!client.connected()) {
    M5.Display.print(&quot;Attempting MQTT connection...&quot;);
    String clientId = &quot;M5Core2-&quot;;
    clientId += String((uint32_t)ESP.getEfuseMac(), HEX);
    
    if (client.connect(clientId.c_str())) {
      M5.Display.println(&quot;connected&quot;);
    } else {
      M5.Display.printf(&quot;failed, rc=%d try again in 5s\n&quot;, client.state());
      delay(5000);
    }
  }
}

void setup() {
  M5.begin();
  M5.Display.setTextSize(2);
  M5.Display.println(&quot;Init AHT20 &amp;amp; MQTT...&quot;);

  // Initialize I2C for Port A
  Wire.begin(32, 33); 

  if (!aht.begin(&amp;amp;Wire)) {
    M5.Display.println(&quot;Could not find AHT? Check wiring&quot;);
    while (1) delay(10);
  }
  M5.Display.println(&quot;AHT20 Found&quot;);

  setup_wifi();
  client.setServer(mqtt_server, mqtt_port);
}

void loop() {
  M5.update();
  
  if (!client.connected()) {
    reconnect();
  }
  client.loop();

  // 1. Get Sensor Data
  sensors_event_t humidity, temp;
  aht.getEvent(&amp;amp;humidity, &amp;amp;temp);

  float t_val = temp.temperature;
  float h_val = humidity.relative_humidity;

  // 2. Create JSON Payload
  StaticJsonDocument&amp;lt;256&amp;gt; doc;
  doc[&quot;loc&quot;] = location;
  doc[&quot;temp&quot;] = serialized(String(t_val, 2));
  doc[&quot;hum&quot;] = serialized(String(h_val, 2));

  char buffer[256];
  serializeJson(doc, buffer);

  // 3. Publish
  client.publish(&quot;lab/temphum&quot;, buffer);

  // 4. Visual Feedback
  M5.Display.fillScreen(TFT_BLACK);
  M5.Display.setCursor(10, 50);
  M5.Display.printf(&quot;Loc: %s\n&quot;, location);
  M5.Display.printf(&quot;Temp: %.2f C\n&quot;, t_val);
  M5.Display.printf(&quot;Hum:  %.2f %%&quot;, h_val);
  
  delay(5000); 
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;코드의 주요 기능:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Wi-Fi 연결 및 MQTT 브로커 연결&lt;/li&gt;
&lt;li&gt;AHT20 센서로부터 온도와 습도 데이터 읽기&lt;/li&gt;
&lt;li&gt;JSON 형식으로 데이터 포맷팅&lt;/li&gt;
&lt;li&gt;&quot;lab/temphum&quot; 토픽으로 데이터 발행&lt;/li&gt;
&lt;li&gt;M5Stack 디스플레이에 현재 값 표시&lt;/li&gt;
&lt;/ul&gt;
&lt;!-- 8 --&gt;
&lt;h2 id=&quot;sec8&quot; class=&quot;sec-title title-26&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;hl-yellow&quot;&gt;8. 실습: Python MQTT 구독자 및 데이터 로깅&lt;/span&gt;&lt;/h2&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;Raspberry Pi Zero W2에서 Mosquitto MQTT 브로커를 설정하고, Python 스크립트로 M5Core2에서 발행한 데이터를 구독하여 CSV 파일로 저장하는 실습을 진행했다.&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;MQTT Broker 설정 (Raspberry Pi)&lt;/b&gt;:&lt;/p&gt;
&lt;div class=&quot;codecard&quot;&gt;
&lt;div class=&quot;head&quot;&gt;Mosquitto 설치 및 설정&lt;/div&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;# 1. 시스템 업데이트
sudo apt update &amp;amp;&amp;amp; sudo apt upgrade -y

# 2. Mosquitto 설치
sudo apt install mosquitto mosquitto-clients -y

# 3. Mosquitto 서비스 시작 및 자동 시작 설정
sudo systemctl enable mosquitto
sudo systemctl start mosquitto

# 4. 상태 확인
systemctl status mosquitto

# 5. 원격 액세스 활성화
sudo nano /etc/mosquitto/mosquitto.conf
# 다음 내용 추가:
# listener 1883
# allow_anonymous true

# 6. Mosquitto 재시작
sudo systemctl restart mosquitto

# 7. 포트 확인
ss -tulpn | grep 1883

# 8. 브로커 테스트
# 터미널 1 (구독):
mosquitto_sub -h localhost -t lab/temphum

# 터미널 2 (발행):
mosquitto_pub -h localhost -t lab/temphum -m '{&quot;temp&quot;:25.5,&quot;hum&quot;:60.2}'&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Python MQTT 구독자&lt;/b&gt;:&lt;/p&gt;
&lt;div class=&quot;codecard&quot;&gt;
&lt;div class=&quot;head&quot;&gt;MQTT Consumer (mqtt_loc_consumer.py)&lt;/div&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;import json
import csv
from datetime import datetime
import paho.mqtt.client as mqtt

# Configuration
TOPIC = &quot;lab/temphum&quot;
CSV_FILE = &quot;temphum_log.csv&quot;
BROKER_ADDRESS = &quot;localhost&quot;

# Initialize CSV with header if file doesn't exist
try:
    with open(CSV_FILE, &quot;r&quot;) as f:
        pass
except FileNotFoundError:
    with open(CSV_FILE, &quot;w&quot;, newline=&quot;&quot;) as f:
        writer = csv.writer(f)
        writer.writerow([&quot;timestamp&quot;, &quot;location&quot;, &quot;temp&quot;, &quot;hum&quot;])
    print(f&quot;Created new log file: {CSV_FILE}&quot;)

def on_connect(client, userdata, flags, rc):
    print(f&quot;Connected with result code {rc}&quot;)
    client.subscribe(TOPIC)
    print(f&quot;Listening on topic: {TOPIC}&quot;)
    print(&quot;-&quot; * 65)
    print(f&quot;{'TIMESTAMP':&amp;lt;22} | {'LOCATION':&amp;lt;15} | {'TEMP':&amp;lt;8} | {'HUM':&amp;lt;8}&quot;)
    print(&quot;-&quot; * 65)

def on_message(client, userdata, msg):
    payload = msg.payload.decode()
    ts = datetime.now().strftime(&quot;%Y-%m-%d %H:%M:%S&quot;)

    try:
        data = json.loads(payload)
        loc = data.get(&quot;loc&quot;, &quot;Unknown&quot;)
        temp = float(data.get(&quot;temp&quot;, 0.0))
        hum = float(data.get(&quot;hum&quot;, 0.0))

        # Display on Terminal
        print(f&quot;{ts:&amp;lt;22} | {loc:&amp;lt;15} | {temp:&amp;lt;8.2f} | {hum:&amp;lt;8.2f}&quot;)

        # Save to CSV
        with open(CSV_FILE, &quot;a&quot;, newline=&quot;&quot;) as f:
            writer = csv.writer(f)
            writer.writerow([ts, loc, temp, hum])

    except json.JSONDecodeError:
        print(f&quot;Received non-JSON message: {payload}&quot;)
    except Exception as e:
        print(f&quot;Error processing message: {e}&quot;)

# Setup MQTT Client
client = mqtt.Client()
client.on_connect = on_connect
client.on_message = on_message

try:
    client.connect(BROKER_ADDRESS, 1883, 60)
    client.loop_forever()
except KeyboardInterrupt:
    print(&quot;\nDisconnecting...&quot;)
    client.disconnect()
except Exception as e:
    print(f&quot;Connection failed: {e}&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;이 스크립트는 MQTT 메시지를 수신하여 터미널에 표시하고, 동시에 CSV 파일로 저장한다. 실시간으로 데이터가 수집되는 것을 확인할 수 있었다.&lt;/p&gt;
&lt;!-- 9 --&gt;
&lt;h2 id=&quot;sec9&quot; class=&quot;sec-title title-26&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;hl-yellow&quot;&gt;9. 실습: 다중 디바이스 데이터 집계&lt;/span&gt;&lt;/h2&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;여러 M5Core2 디바이스로부터 받은 데이터의 평균을 계산하는 실습을 진행했다. 이는 분산 센서 네트워크에서 중앙 집중식 데이터 집계의 예시다.&lt;/p&gt;
&lt;div class=&quot;codecard&quot;&gt;
&lt;div class=&quot;head&quot;&gt;MQTT Consumer with Average Calculation (mqtt_consumer_avg.py)&lt;/div&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;import json
import csv
import sys
from datetime import datetime
import paho.mqtt.client as mqtt

TOPIC = &quot;lab/temphum&quot;
CSV_FILE = &quot;temphum_log.csv&quot;
BROKER_ADDRESS = &quot;localhost&quot;

# Global dictionary to store running stats per location
location_stats = {}

def on_connect(client, userdata, flags, rc):
    print(f&quot;Connected to MQTT Broker (Result: {rc})&quot;)
    client.subscribe(TOPIC)
    print(f&quot;Listening on topic: {TOPIC}&quot;)
    print(&quot;=&quot; * 90)
    print(f&quot;{'TIMESTAMP':&amp;lt;20} | {'LOCATION':&amp;lt;12} | {'TEMP (C)':&amp;lt;18} | {'HUMIDITY (%)':&amp;lt;18}&quot;)
    print(f&quot;{'':&amp;lt;20} | {'':&amp;lt;12} | {'Current (Avg)':&amp;lt;18} | {'Current (Avg)':&amp;lt;18}&quot;)
    print(&quot;=&quot; * 90)

def on_message(client, userdata, msg):
    try:
        payload = msg.payload.decode()
        data = json.loads(payload)

        loc = data.get(&quot;loc&quot;, &quot;Unknown&quot;)
        temp = float(data.get(&quot;temp&quot;, 0.0))
        hum = float(data.get(&quot;hum&quot;, 0.0))
        ts = datetime.now().strftime(&quot;%Y-%m-%d %H:%M:%S&quot;)

        # Update running averages
        if loc not in location_stats:
            location_stats[loc] = {&quot;temp_sum&quot;: 0.0, &quot;hum_sum&quot;: 0.0, &quot;count&quot;: 0}

        stats = location_stats[loc]
        stats[&quot;temp_sum&quot;] += temp
        stats[&quot;hum_sum&quot;] += hum
        stats[&quot;count&quot;] += 1

        # Calculate Averages
        avg_temp = stats[&quot;temp_sum&quot;] / stats[&quot;count&quot;]
        avg_hum = stats[&quot;hum_sum&quot;] / stats[&quot;count&quot;]

        # Display output: &quot;25.0 (24.8)&quot;
        temp_str = f&quot;{temp:.1f} ({avg_temp:.1f})&quot;
        hum_str = f&quot;{hum:.1f} ({avg_hum:.1f})&quot;

        print(f&quot;{ts:&amp;lt;20} | {loc:&amp;lt;12} | {temp_str:&amp;lt;18} | {hum_str:&amp;lt;18}&quot;)

        # Save to CSV
        with open(CSV_FILE, &quot;a&quot;, newline=&quot;&quot;) as f:
            writer = csv.writer(f)
            writer.writerow([ts, loc, temp, hum, round(avg_temp, 2), round(avg_hum, 2)])

    except json.JSONDecodeError:
        print(f&quot;Error: Received non-JSON payload: {msg.payload}&quot;)
    except Exception as e:
        print(f&quot;Error processing message: {e}&quot;)

# Setup MQTT Client
client = mqtt.Client()
client.on_connect = on_connect
client.on_message = on_message

try:
    client.connect(BROKER_ADDRESS, 1883, 60)
    client.loop_forever()
except KeyboardInterrupt:
    print(&quot;\nDisconnecting...&quot;)
    client.disconnect()
except Exception as e:
    print(f&quot;Connection failed: {e}&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;이 스크립트는 각 위치별로 누적 평균을 계산하여 표시한다. 현재 값과 평균 값을 함께 보여주어 데이터의 추세를 파악할 수 있다.&lt;/p&gt;
&lt;!-- 10 --&gt;
&lt;h2 id=&quot;sec10&quot; class=&quot;sec-title title-26&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;hl-yellow&quot;&gt;10. 실습: RPi + PCT2075 센서로 MQTT 발행&lt;/span&gt;&lt;/h2&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;Raspberry Pi Zero W2에 PCT2075 온도 센서를 연결하고, Python으로 센서 데이터를 읽어 MQTT 브로커로 발행하는 실습을 진행했다. 이는 이종 네트워크(Microcontrollers 및 SBC)로부터 환경 모니터링을 중앙 집중화하는 예시다.&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;하드웨어 구성&lt;/b&gt;:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Raspberry Pi Zero W2&lt;/li&gt;
&lt;li&gt;PCT2075 온도 센서 (I2C 통신)&lt;/li&gt;
&lt;/ul&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;배선 (RPi Zero W2 to PCT2075)&lt;/b&gt;:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Pin 1 (3.3V) &amp;rarr; VCC&lt;/li&gt;
&lt;li&gt;Pin 3 (SDA) &amp;rarr; SDA&lt;/li&gt;
&lt;li&gt;Pin 5 (SCL) &amp;rarr; SCL&lt;/li&gt;
&lt;li&gt;Pin 6 (GND) &amp;rarr; GND&lt;/li&gt;
&lt;/ul&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;필요한 설정&lt;/b&gt;:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;I2C 활성화: &lt;code&gt;sudo raspi-config&lt;/code&gt; &amp;rarr; Interface Options &amp;rarr; I2C&lt;/li&gt;
&lt;li&gt;라이브러리 설치: &lt;code&gt;pip3 install adafruit-circuitpython-pct2075&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&quot;codecard&quot;&gt;
&lt;div class=&quot;head&quot;&gt;RPi PCT2075 MQTT Publisher (rpi_publisher_pct2075.py)&lt;/div&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;import time
import json
import board
import adafruit_pct2075
import paho.mqtt.client as mqtt

# --- USER CONFIGURATION ---
MQTT_BROKER_IP = &quot;192.168.1.100&quot;
MQTT_PORT = 1883
MQTT_TOPIC = &quot;lab/temphum&quot;
LOCATION_NAME = &quot;RPI_Node_PCT_East&quot;
# --------------------------

# Initialize I2C bus
i2c = board.I2C()
sensor = adafruit_pct2075.PCT2075(i2c)

# MQTT Setup
def on_connect(client, userdata, flags, rc):
    if rc == 0:
        print(f&quot;Connected to Master Broker at {MQTT_BROKER_IP}&quot;)
    else:
        print(f&quot;Failed to connect, return code {rc}&quot;)

client = mqtt.Client()
client.on_connect = on_connect
client.connect(MQTT_BROKER_IP, MQTT_PORT, 60)
client.loop_start()

# Main Loop
try:
    while True:
        # Read Temperature
        current_temp = sensor.temperature

        # Prepare Payload (humidity is None for PCT2075)
        payload = {
            &quot;loc&quot;: LOCATION_NAME,
            &quot;temp&quot;: round(current_temp, 2),
            &quot;hum&quot;: None
        }
        json_payload = json.dumps(payload)

        # Publish
        print(f&quot;Sending: {json_payload}&quot;)
        client.publish(MQTT_TOPIC, json_payload)

        time.sleep(5)

except KeyboardInterrupt:
    print(&quot;\nStopping...&quot;)
    client.loop_stop()
    client.disconnect()&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;PCT2075는 온도 전용 센서이므로 습도 값은 None으로 처리한다. 이렇게 하면 M5Core2의 AHT20 데이터와 호환되는 JSON 구조를 유지할 수 있다.&lt;/p&gt;
&lt;!-- 11 --&gt;
&lt;h2 id=&quot;sec11&quot; class=&quot;sec-title title-26&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;hl-yellow&quot;&gt;11. 시뮬레이션: 합성 데이터 생성 및 전처리&lt;/span&gt;&lt;/h2&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;실제 센서 데이터의 특성을 모방한 합성 데이터를 생성하고, 다양한 전처리 기법을 적용하여 비교하는 시뮬레이션을 진행했다.&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;합성 데이터 생성&lt;/b&gt;:&lt;/p&gt;
&lt;div class=&quot;codecard&quot;&gt;
&lt;div class=&quot;head&quot;&gt;Synthetic Data Generation (generate_synthetic_data_mqtt.py)&lt;/div&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;import pandas as pd
import numpy as np
import matplotlib.pyplot as plt

def generate_and_visualize_data():
    # 7 days of data at 15-minute intervals
    dates = pd.date_range(start='2024-01-01', periods=24 * 4 * 7, freq='15min')
    n = len(dates)

    # Temperature: Daily cycle (sine wave) + random Gaussian noise
    temp_base = 20 + 10 * np.sin(np.linspace(0, 7 * 2 * np.pi, n))
    temp_noise = np.random.normal(0, 1.5, n)
    temperature = temp_base + temp_noise

    # Humidity: Inverse to temp + random noise
    humidity_base = 60 - 20 * np.sin(np.linspace(0, 7 * 2 * np.pi, n))
    humidity_noise = np.random.normal(0, 3, n)
    humidity = humidity_base + humidity_noise

    locations = np.random.choice(['Lab_A', 'Server_Room', 'Outdoor_Unit'], size=n)

    df = pd.DataFrame({
        'Timestamp': dates,
        'Location': locations,
        'Temperature': temperature,
        'Humidity': humidity
    })

    # Introduce Artifacts (Bad Data)
    # Missing Values (5% of data)
    nan_indices_t = np.random.choice(df.index, size=int(n * 0.05), replace=False)
    nan_indices_h = np.random.choice(df.index, size=int(n * 0.05), replace=False)
    df.loc[nan_indices_t, 'Temperature'] = np.nan
    df.loc[nan_indices_h, 'Humidity'] = np.nan

    # Outliers
    outlier_indices_t = np.random.choice(df.index, size=10, replace=False)
    df.loc[outlier_indices_t, 'Temperature'] = df.loc[outlier_indices_t, 'Temperature'] * 3

    outlier_indices_h = np.random.choice(df.index, size=5, replace=False)
    df.loc[outlier_indices_h, 'Humidity'] = -20

    # Save to CSV
    df.to_csv('synthetic_sensor_data.csv', index=False)
    print(f&quot;Dataset saved to: synthetic_sensor_data.csv&quot;)

    return df&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;데이터 전처리 및 비교&lt;/b&gt;:&lt;/p&gt;
&lt;div class=&quot;codecard&quot;&gt;
&lt;div class=&quot;head&quot;&gt;Data Processing Comparison (data_processing_comparison.py)&lt;/div&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from sklearn.preprocessing import MinMaxScaler, StandardScaler
from scipy import stats

def process_and_visualize():
    df = pd.read_csv('synthetic_sensor_data.csv')
    df['Timestamp'] = pd.to_datetime(df['Timestamp'])
    df_clean = df.copy()

    # 1. Handle Missing Values (Linear Interpolation)
    df_clean['Temperature'] = df_clean['Temperature'].interpolate(method='linear')
    df_clean['Humidity'] = df_clean['Humidity'].interpolate(method='linear')
    df_clean.dropna(inplace=True)

    # 2. Detect &amp;amp; Handle Outliers (Z-Score Method)
    for col in ['Temperature', 'Humidity']:
        z_scores = np.abs(stats.zscore(df_clean[col]))
        outliers = z_scores &amp;gt; 3
        df_clean.loc[outliers, col] = df_clean[col].median()

    # 3. Noise Filtering / Smoothing (Moving Average)
    df_clean['Temp_Smoothed'] = df_clean['Temperature'].rolling(window=4, center=True).mean().bfill().ffill()
    df_clean['Hum_Smoothed'] = df_clean['Humidity'].rolling(window=4, center=True).mean().bfill().ffill()

    # 4. Normalization (Min-Max Scaling)
    scaler_minmax = MinMaxScaler()
    df_clean[['Temp_Norm', 'Hum_Norm']] = scaler_minmax.fit_transform(df_clean[['Temp_Smoothed', 'Hum_Smoothed']])

    # 5. Standardization (Z-Score Scaling)
    scaler_std = StandardScaler()
    df_clean[['Temp_Std', 'Hum_Std']] = scaler_std.fit_transform(df_clean[['Temp_Smoothed', 'Hum_Smoothed']])

    # 6. Encoding Categorical Data (One-Hot Encoding)
    df_clean = pd.get_dummies(df_clean, columns=['Location'], prefix='Loc')

    # Save processed data
    df_clean.to_csv('processed_sensor_data.csv', index=False)
    print(&quot;Processed data saved to: processed_sensor_data.csv&quot;)

    return df_clean&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;이 시뮬레이션을 통해 다음을 확인했다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;원시 데이터에 노이즈, 이상치, 결측값이 포함된 상태&lt;/li&gt;
&lt;li&gt;전처리 후 데이터가 부드럽고 일관성 있게 변환된 상태&lt;/li&gt;
&lt;li&gt;정규화와 표준화의 차이점&lt;/li&gt;
&lt;li&gt;이동 평균 필터링의 효과&lt;/li&gt;
&lt;/ul&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Data Drift와 Concept Drift 시뮬레이션&lt;/b&gt;:&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;실제 환경에서 발생할 수 있는 Data Drift와 Concept Drift를 시뮬레이션하는 코드도 작성했다. 이 코드는 3단계로 구성된다:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;Baseline (정상 작동)&lt;/b&gt;: 온도와 습도 간의 안정적인 관계&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Data Drift (센서 보정 드리프트)&lt;/b&gt;: 온도 센서가 점진적으로 5도 상승하는 드리프트 발생. 관계는 유지되지만 센서 값이 변함&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Concept Drift (물리적 관계 변화)&lt;/b&gt;: 온도와 습도 간의 관계가 반대로 변함 (음의 상관관계 &amp;rarr; 양의 상관관계)&lt;/li&gt;
&lt;/ol&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;시뮬레이션에서는 KS-Test를 사용하여 Data Drift를 감지하고, 모델의 예측 오차(MAE)를 모니터링하여 Concept Drift를 감지한다. Drift가 감지되면 자동으로 스케일러를 업데이트하거나 모델을 재학습한다.&lt;/p&gt;
&lt;!-- 12 --&gt;
&lt;h2 id=&quot;sec12&quot; class=&quot;sec-title title-26&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;hl-yellow&quot;&gt;12. 정리&lt;/span&gt;&lt;/h2&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;이번 수업을 통해 데이터 엔지니어링의 전체 파이프라인을 경험했다. MQTT 프로토콜을 사용한 실시간 데이터 수집부터 전처리까지의 과정을 직접 구현해볼 수 있었다.&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;특히 중요한 것은 &lt;b&gt;&quot;데이터 과학자는 시간의 50% 이상을 데이터 전처리에 소비한다&quot;&lt;/b&gt;는 점이었다. 좋은 모델을 만들기 위해서는 고품질의 데이터가 필수이며, 이를 위해서는 체계적인 데이터 엔지니어링 프로세스가 필요하다.&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;MQTT의 Pub/Sub 모델은 IoT 환경에서 확장 가능한 데이터 파이프라인을 구축하는 데 매우 유용하다. 여러 디바이스로부터 데이터를 수집하고 중앙에서 집계하는 구조는 실제 산업 환경에서 널리 사용되는 패턴이다.&lt;/p&gt;
&lt;p class=&quot;para&quot; data-ke-size=&quot;size16&quot;&gt;이 수업에서 다룬 내용은 앞으로 IoT 프로젝트나 센서 데이터 분석을 할 때 매우 유용할 것이다. 특히 MQTT를 활용한 실시간 데이터 수집과 전처리 기법을 이해한 것은 실무에서 큰 도움이 될 것 같다.&lt;/p&gt;
&lt;style&gt;
#post-root a { color:#495057 !important; text-decoration:none !important; }
#post-root a:hover { color:#212529 !important; border-bottom:1px solid #ced4da; }
#post-root a:visited { color:#666 !important; }

.sec-title { display:block; margin:28px 0 12px; font-weight:800; line-height:1.35; }
.sec-title &gt; .hl-yellow {
  background:linear-gradient(transparent 65%, #fff3bf 65%);
  padding:0 .12em;
}
.title-26 { font-size:26px; }

.para { margin:10px 0 14px; color:#343a40; line-height:1.9; }

.toc {
  border:1px solid #e9ecef;
  border-radius:10px;
  padding:12px 14px;
  margin:18px 0 24px;
  background:#f8f9fa;
}

.codecard {
  background:#f8f9fa;
  border:1px solid #e9ecef;
  border-radius:8px;
  padding:16px;
  margin:16px 0;
  overflow-x:auto;
}

.codecard .head {
  font-weight:700;
  color:#212529;
  margin-bottom:10px;
  font-size:15px;
}

.codecard pre {
  margin:0;
  padding:0;
  background:transparent;
  border:none;
}

.codecard code {
  font-family:'Courier New', monospace;
  font-size:13px;
  line-height:1.6;
  color:#212529;
}
&lt;/style&gt;
&lt;/div&gt;</description>
      <category>UNLV</category>
      <author>빡성</author>
      <guid isPermaLink="true">https://smp0417.tistory.com/70</guid>
      <comments>https://smp0417.tistory.com/70#entry70comment</comments>
      <pubDate>Fri, 13 Feb 2026 02:20:26 +0900</pubDate>
    </item>
  </channel>
</rss>