<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>AS IS -&amp;gt; TO BE</title>
    <link>https://doobeom-coding.tistory.com/</link>
    <description>서비스 사용자와 함께 하는 팀원들에게 모두 선한 영향력을 주는 기획자를 꿈꿉니다.</description>
    <language>ko</language>
    <pubDate>Tue, 11 Aug 2026 08:54:55 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>kdb1248</managingEditor>
    <image>
      <title>AS IS -&amp;gt; TO BE</title>
      <url>https://tistory1.daumcdn.net/tistory/4832914/attach/b728e522db364b2ebabbc4907b9f43c8</url>
      <link>https://doobeom-coding.tistory.com</link>
    </image>
    <item>
      <title>18. 상품 핫딜 헌팅 에이전트 (2) - Tool call 자율 에이전트와 에이전트 시스템 완성</title>
      <link>https://doobeom-coding.tistory.com/95</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;목차&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;0. 들어가며 &amp;mdash; 가격은 맞히게 됐다, 이제 무엇의 가격을 맞힐지 정할 차례&lt;br /&gt;1.&amp;nbsp;상품&amp;nbsp;딜은&amp;nbsp;어디서&amp;nbsp;찾나&amp;nbsp;-&amp;nbsp;RSS&amp;nbsp;피드를&amp;nbsp;긁어오는&amp;nbsp;코드&lt;br /&gt;2.&amp;nbsp;비정형&amp;nbsp;데이터를&amp;nbsp;정형으로&amp;nbsp;-&amp;nbsp;Structured&amp;nbsp;Outputs와&amp;nbsp;제약&amp;nbsp;디코딩&lt;br /&gt;3.&amp;nbsp;네&amp;nbsp;번째&amp;nbsp;에이전트&amp;nbsp;-&amp;nbsp;30건&amp;nbsp;딜&amp;nbsp;중&amp;nbsp;5건을&amp;nbsp;골라내는&amp;nbsp;ScannerAgent&lt;br /&gt;4.&amp;nbsp;다섯&amp;nbsp;번째&amp;nbsp;에이전트&amp;nbsp;-&amp;nbsp;딜&amp;nbsp;알림을&amp;nbsp;보내주는&amp;nbsp;메세징&amp;nbsp;에이전트&lt;br /&gt;5.&amp;nbsp;먼저&amp;nbsp;규칙으로&amp;nbsp;조율하기&amp;nbsp;-&amp;nbsp;PlanningAgent&lt;br /&gt;6.&amp;nbsp;이제&amp;nbsp;실행&amp;nbsp;순서를&amp;nbsp;LLM에게&amp;nbsp;맡긴다&amp;nbsp;-&amp;nbsp;툴&amp;nbsp;콜과&amp;nbsp;자율&amp;nbsp;에이전트&lt;br /&gt;7.&amp;nbsp;피날레&amp;nbsp;-&amp;nbsp;프레임워크와&amp;nbsp;Gradio&amp;nbsp;이용&amp;nbsp;UI,&amp;nbsp;그리고&amp;nbsp;실제로&amp;nbsp;찾아낸&amp;nbsp;딜&lt;br /&gt;마무리&amp;nbsp;&amp;mdash;&amp;nbsp;에이전틱&amp;nbsp;AI,&amp;nbsp;그리고&amp;nbsp;8주의&amp;nbsp;회고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;0. 들어가며 &amp;mdash; 가격은 맞히게 됐다, 이제 무엇의 가격을 맞힐지 정할 차례&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지난 글에서 Week 8의 절반을 정리했다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;1) 파인튜닝한 Llama 3.2를 Modal에 올려 항상 켜져 있는 서비스로 만들고(SpecialistAgent)&lt;br /&gt;2) 상품 80만 개를 Chroma DB(벡터스토어)에 넣어 GPT-5.1에게 유사 상품을 RAG로 쥐여주고(FrontierAgent)&lt;br /&gt;3) Week 6의 신경망도 가격 예측에 쓰고 (neural network Agent)&lt;br /&gt;4) 세 개를 섞어 앙상블 에이전트 을 만들었다(EnsembleAgent)&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 지금 가능한 것은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;agent.price(&quot;Shure MV7+ Podcast Microphone&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; (출력) 261.21&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;꽤 쓸 만해 보이지만, 여기엔 빠진 게 하나 있다. 저 따옴표 안에 무엇을 넣을지는 여전히 내가 정한다는 것.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 만든 가격 예측 에이전트는 &quot;이 상품 얼마야?&quot;라고 질문을 넣으면 거기에 답하는 도구일 뿐이고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;질문 자체는 사람이 만들어서 넣어주고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글(Week 8의 남은 영역) 에서 채우는 게 정확히 그 부분이다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;1) 인터넷에서 상품 딜을 긁어오고&lt;br /&gt;2) 그중 살펴볼 만한 것을 스스로 고르고&lt;br /&gt;3) 값을 매기고&lt;br /&gt;4) 진짜 상품의 본래 가치 대비 싸다 싶으면 나한테 알림을 보내는 것까지&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사람이 하는 일은 프로그램을 켜두는 것뿐이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Week 8의 남은 일정은 이렇다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Day 3: 딜을 모아오고 고르는 에이전트와 알림을 보내는 에이전트를 만듬&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Day 4: 이 에이전트들을 조율하는 플래너를 두 가지 방식으로 만들어봄. 하나는 순서를 코드에 박아둔 방식, 다른 하나는 순서를 LLM에게 맡기는 방식.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Day 5: Day 1~4에서 만든 에이전트 시스템을 전부 묶어서 화면(UI)을 붙이는 피날레&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 이 글이 Ed donner의 LLM Engineering 강의 정리의 마지막 글이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;글 마무리에는 Week 8 결론과 함께 8주 코스 전체를 돌아보는 내용을 담았다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 상품 딜은 어디서 찾나 - RSS 피드를 긁어오는 코드&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에이전트가 상품의 가격 예측을 해보려면 먼저 &quot;어떤 상품이 지금 얼마에 팔리고 있는지&quot;를 알아야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(해당 상품이 정말 모델의 가격 예측 대비 싸게 나온지 판단하기 위함)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 원천 데이터를 가져오는 코드가 agents/deals.py다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기엔 LLM이 한 줄도 안 들어간다. 그냥 웹에서 긁어오는 코드다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;딜은 dealnews의 RSS 피드에서 온다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강의는 dealnews.com이라는 미국 핫딜 사이트의 RSS 피드 3개를 쓴다.&lt;/p&gt;
&lt;pre class=&quot;makefile&quot;&gt;&lt;code&gt;feeds = [
    &quot;https://www.dealnews.com/c142/Electronics/?rss=1&quot;,
    &quot;https://www.dealnews.com/c39/Computers/?rss=1&quot;,
    &quot;https://www.dealnews.com/f1912/Smart-Home/?rss=1&quot;,
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각각 전자제품 / 컴퓨터 / 스마트홈 카테고리다. 주석에는 자동차, 홈&amp;amp;가든 피드도 적혀 있는데 원하면 늘리면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RSS를 읽는 건 feedparser라는 오픈소스 라이브러리가 다 해준다.&lt;/p&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;@classmethod
def fetch(cls, show_progress: bool = False) -&amp;gt; List[Self]:
    deals = []
    feed_iter = tqdm(feeds) if show_progress else feeds
    for feed_url in feed_iter:
        feed = feedparser.parse(feed_url)
        for entry in feed.entries[:10]:   # 피드당 최신 10건만
            deals.append(cls(entry))
            time.sleep(0.05)              # 사이트에 부담 주지 않도록 잠깐 쉼
    return deals
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;피드 3개 &amp;times; 10건 = 30건. 이게 한 사이클에서 다루는 후보 딜의 양이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RSS 요약만으로는 부족하다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 좀 의외였던 부분.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;feedparser가 준 항목에는 제목과 짧은 요약만 들어 있는데, 이걸로는 가격을 판단하기에 정보가 부족하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 ScrapedDeal은 생성될 때 딜의 상세 페이지까지 한 번 더 직접 방문한다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;def __init__(self, entry: Dict[str, str]):
    self.title = entry[&quot;title&quot;]
    self.summary = extract(entry[&quot;summary&quot;])
    self.url = entry[&quot;links&quot;][0][&quot;href&quot;]
    stuff = requests.get(self.url).content              # 상세 페이지를 직접 가져옴
    soup = BeautifulSoup(stuff, &quot;html.parser&quot;)
    content = soup.find(&quot;div&quot;, class_=&quot;content-section&quot;).get_text()
    content = content.replace(&quot;\nmore&quot;, &quot;&quot;).replace(&quot;\n&quot;, &quot; &quot;)
    if &quot;Features&quot; in content:
        self.details, self.features = content.split(&quot;Features&quot;, 1)   # 설명 / 스펙 분리
    else:
        self.details = content
        self.features = &quot;&quot;
    self.truncate()
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BeautifulSoup으로 content-section div를 찾아 텍스트만 뽑고, &quot;Features&quot;라는 단어를 기준으로 앞뒤를 잘라 설명(details)과 스펙(features)으로 나눈다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;=&amp;gt; dealnews 페이지 구조에 딱 맞춰 짠 코드다. 사이트가 개편되면 바로 깨진다. 이 점이 2장에서 다룰 이야기의 복선이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모델에 보내기 전에 길이를 자른다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;__init__ 마지막 줄의 truncate()가 하는 일은 단순하다.&lt;/p&gt;
&lt;pre class=&quot;ruby&quot;&gt;&lt;code&gt;def truncate(self):
    self.title = self.title[:100]
    self.details = self.details[:500]
    self.features = self.features[:500]
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이유는 주석에 그대로 적혀 있다.&lt;/p&gt;
&lt;pre class=&quot;livecodeserver&quot;&gt;&lt;code&gt;Limit the fields to a sensible length to avoid sending too much info to the model
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; 모델에 너무 많은 정보를 보내지 않도록 각 필드 길이를 적당히 제한한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;30건을 통째로 프롬프트에 밀어 넣을 거라서, 한 건이 길어지면 그대로 토큰 비용과 지연으로 돌아온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 모델에게 보낼 때는 describe()가 한 건을 한 덩어리 텍스트로 만들어준다.&lt;/p&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;def describe(self):
    return f&quot;Title: {self.title}\nDetails: {self.details.strip()}\nFeatures: {self.features.strip()}\nURL: {self.url}&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 긁어보면&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;deals = ScrapedDeal.fetch(show_progress=True)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; (출력) 100%|██████████| 3/3 [00:54&amp;lt;00:00, 18.15s/it]&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;len(deals)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; (출력) 30&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;30건 모으는 데 54초가 걸렸다. RSS 자체는 금방인데, 30번의 상세 페이지 방문이 시간을 다 잡아먹는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 건을 열어보면 이렇게 생겼다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;deals[10].describe()
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;Title: Logitech Gaming Deals at Woot: Up to 54% off + extra $5 off + free shipping w/ Prime
Details: Woot is discounting a range of Logitech gaming gear, from racing wheels and pedals to headsets,
keyboards, mice, and speakers. Using promo code &quot;LOGITECHWOOT&quot; takes an extra $5 off, and new customers
get an additional $10 off. The lineup spans steep markdowns, like the Logitech G Pro Mouse League of
Legends Edition at $59.99, down 54% off its regular price, alongside refurbished options such as the
G435 LIGHTSPEED headset at $24.99 for shoppers who don't need new condition. Coupon ends July 18.
Features: 
URL: https://www.dealnews.com/Logitech-Gaming-Deals-at-Woot-Up-to-54-off-extra-5-off-free-shipping-w-Prime/21897103.html?iref=rss-c39
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 문제가 보인다. 이 딜은 상품 하나가 아니라 여러 제품 묶음이고, 가격도 &quot;최대 54% 할인&quot;, &quot;추가 $5 off&quot;, &quot;$59.99&quot;, &quot;$24.99&quot;가 뒤섞여 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;=&amp;gt; 이 텍스트를 그대로 가격 예측 에이전트에 넣을 수는 없다. 사람이 읽어도 &quot;그래서 뭘 얼마에 파는 건데?&quot;가 바로 안 나온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 지금 손에 들어온 건 30개의 자유 형식 텍스트 덩어리이고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 단계에 필요한 건 상품 설명 한 문단과 숫자 하나(가격)로 정리된 데이터다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비정형 데이터를 정형으로 바꾸는 작업이 필요하고, 그게 2장의 주제다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;느낀점&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이번장은 AI 기술보다는 데이터를 AI에게 어떻게 전달할것인가와 이를 위한 파이프라인에 대한 고민임&lt;/li&gt;
&lt;li&gt;실무에서도 에이전트를 만들어가면 갈수록, 결국은 에이전트에 컨텍스트로 넣어줄 데이터/지식 성격을 어떻게 적재하고, 또 이를 지속적으로 업데이트해가며 적재할 수 있는 파이프라인에 대한 고민이 늘어나는 것 같다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;그런점에서 이번 장은 실제 상품 딜의 원천 소스를 정하고, 부족할 수 있는 정보를 상세페이지까지 들어가서 한번 더 긁고, llm 에게 넣는 토큰비용/지연시간 고려해서 데이터를 자르는 등 실제 어찌보면 가장 중요할 수 있는 데이터 정제과정에서의 관점을 다뤄볼 수 있다는 게 의미가 있었다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 비정형 데이터를 정형으로 - Structured Outputs와 제약 디코딩&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1장 끝에서 손에 쥔 건 30개의 자유 형식 텍스트였다. 이걸 다음 단계로 넘기려면 이런 형태가 되어야 한다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;상품 자체를 설명하는 문단 하나 (할인 조건을 제외한 본질적인 상품 설명)&lt;/li&gt;
&lt;li&gt;실제 판매가 숫자 하나&lt;/li&gt;
&lt;li&gt;원본 URL&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Day 3의 주제가 바로 이 변환이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강의 노트에 적어둔 한 줄로 요약하면 &quot;비정형 데이터를 파싱해서 정형화된 형태로 변환&quot;하는 것.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 대목에서 강사가 짚는 맥락이 재미있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 작업은 몇 년 전까지만 해도 잘 풀리지 않는 문제였다. 도메인마다 파서를 따로 만들었는데, 문제는 형식이 조금만 달라지면 바로 깨진다는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1장에서 본 soup.find(&quot;div&quot;, class_=&quot;content-section&quot;) 같은 코드가 딱 그렇다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;dealnews가 클래스 이름 하나만 바꿔도 끝이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(실제로 전 회사에서 AI B2B 사업을 하는 과정에서 파서를 납품할때도 고객사, 파싱하려는 대상에 따라 결국 다 수작업과 튜닝이 맞춤형 개발이 들어갈 수 밖에 없다고 많이 이야기했던게 기억이 난다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LLM이 이 문제를 바꿔줬다고 강사는 말한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강사는 이걸 &quot;지능형 파싱(intelligent parsing)&quot;이라고 부른다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;형식을 규칙으로 기술하는 게 아니라, 모델이 읽고 이해해서 원하는 구조로 뽑아주는 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이력서 파싱처럼 예전에는 정규식과 규칙으로 힘들게 하던 일들이 LLM으로 훨씬 쉬워진 게 같은 맥락이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;=&amp;gt; 즉 Structured Outputs는 단순한 편의 기능이 아니라, 그동안 안 풀리던 문제 하나를 통째로 대체하는 도구다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 원하는 형태를 파이썬 클래스로 정의한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Structured Outputs를 쓰는 방식은 이렇다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pydantic의 BaseModel을 상속한 클래스를 정의하면, 모델이 그 클래스의 인스턴스를 돌려준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;agents/deals.py에 정의된 클래스가 이것이다.&lt;/p&gt;
&lt;pre class=&quot;smalltalk&quot;&gt;&lt;code&gt;class Deal(BaseModel):
    product_description: str = Field(
        description=&quot;Your clearly expressed summary of the product in 3-4 sentences. Details of the item are much more important than why it's a good deal. Avoid mentioning discounts and coupons; focus on the item itself.&quot;
    )
    price: float = Field(
        description=&quot;The actual price of this product, as advertised in the deal. Be sure to give the actual price; for example, if a deal is described as $100 off the usual $300 price, you should respond with $200&quot;
    )
    url: str = Field(description=&quot;The URL of the deal, as provided in the input&quot;)


class DealSelection(BaseModel):
    deals: List[Deal] = Field(
        description=&quot;Your selection of the 5 deals that have the most detailed, high quality description and the most clear price.&quot;
    )
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 눈여겨볼 게 Field(description=...)다. 그냥 개발자용 주석처럼 보이지만 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 price 필드의 설명을 보면 거의 프롬프트다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;if a deal is described as $100 off the usual $300 price, you should respond with $200
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; &quot;$300짜리를 $100 할인&quot;이라고 쓰여 있으면 $200이라고 답해라&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1장의 Logitech 딜에서 봤던 &quot;최대 54% 할인&quot;, &quot;추가 $5 off&quot; 같은 함정을 여기서 미리 막고 있는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;=&amp;gt; description은 사람을 위한 문서이자 동시에 LLM에게 주는 힌트다. 두 역할을 겸한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 내가 착각하고 있던 게 하나 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 막연히 &quot;AI가 JSON을 잘 배워서 특정 형식으로 잘 뱉어주는구나&quot; 정도로 생각했는데, 실제 동작은 그게 아니었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 흐름은 이렇다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;① 개발자가 pydantic 클래스를 정의
        &amp;darr;
② pydantic이 JSON Schema를 자동 생성
        &amp;darr;
③ OpenAI SDK가 그 스키마를 시스템 프롬프트에 삽입
   &quot;You must output JSON matching this schema.&quot;
        &amp;darr;
④ 모델은 JSON '텍스트'를 생성
        &amp;darr;
⑤ SDK가 그 JSON을 파싱
        &amp;darr;
⑥ pydantic이 파이썬 객체로 변환
        &amp;darr;
⑦ 개발자는 deal.price 처럼 그냥 꺼내 쓴다
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 ④다. 모델이 파이썬 객체를 만드는 게 아니라, 스키마를 만족하는 JSON 텍스트를 만든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체로 바꿔주는 건 SDK와 pydantic이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 진짜 트릭은 제약 디코딩이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지만 보면 &quot;그래서 결국 프롬프트로 부탁하는 거 아냐?&quot;라는 의문이 남는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 초기 Function Calling 시절에는 그랬다. &quot;JSON으로 답해줘&quot;라고 프롬프트를 주면 대체로 잘 되지만, 가끔 앞에 Sure!를 붙이거나 마지막에 쉼표를 잘못 넣어서 파싱이 깨졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금은 다르다. 강의에서 언급되는 게 inference time constrained decoding(추론 시점 제약 디코딩)이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LLM은 한 번에 문장을 만드는 게 아니라 토큰을 하나씩 뽑는다. 매 순간 &quot;다음 토큰이 무엇일지&quot;에 대한 확률 분포를 계산한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 { 까지 생성한 상태라면 다음 후보가 이럴 수 있다.&lt;/p&gt;
&lt;pre class=&quot;erlang-repl&quot;&gt;&lt;code&gt;&quot;name&quot;   40%
&quot;age&quot;    20%
&quot;hello&quot;  15%
&quot;}&quot;      10%
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원래대로면 hello가 나올 가능성도 있다. 그런데 스키마에 hello라는 필드가 없다면, OpenAI는 그 토큰의 확률을 아예 0으로 만들어버린다. 선택 후보에서 제거하는 것이다.&lt;/p&gt;
&lt;pre class=&quot;clean&quot;&gt;&lt;code&gt;일반 생성:   모든 토큰 -&amp;gt; 확률 높은 것 선택
제약 디코딩: 모든 토큰 -&amp;gt; 스키마 위반 토큰 제거 -&amp;gt; 남은 것 중에서 선택
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;타입도 마찬가지다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;age가 integer로 정의돼 있으면 &quot;thirty&quot;라는 토큰은 확률이 높든 낮든 나올 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;=&amp;gt; 예전은 &quot;JSON으로 답해줘&quot;는 부탁이었다면, 이 방식은 강제다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모델이 더 똑똑해져서 형식을 잘 지키는 게 아니라, 형식을 어기는 선택지 자체를 출력 단계에서 없애버리는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;inference time 테크닉&quot;이라고 부르는 이유도 여기서 나온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;학습으로 모델 가중치를 바꾸는 게 아니라, 학습이 끝난 모델이 답을 생성하는 그 순간에만 제약을 건다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 호출해보면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;호출 자체는 허무할 정도로 간단하다. response_format에 아까 만든 클래스를 그대로 넘기면 끝이다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;response = openai.chat.completions.parse(
    model=MODEL,                    # gpt-5-mini
    messages=messages,
    response_format=DealSelection,  # 여기에 pydantic 클래스를 그대로
    reasoning_effort=&quot;minimal&quot;,
)
results = response.choices[0].message.parsed
results
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;30건의 딜 텍스트를 통째로 넣고 12.5초 만에 결과가 왔다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;DealSelection(deals=[Deal(product_description='Samsung M70H is a 55&quot; Mini LED 4K smart TV that uses
a dense mini-LED backlight array to improve contrast and brightness compared with standard LED
panels. ...', price=350.0, url='https://www.dealnews.com/products/Samsung/...'), Deal(...
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;results.deals를 돌면서 꺼내 보면 이렇게 나온다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-08-08 오후 3.10.22.png&quot; data-origin-width=&quot;2246&quot; data-origin-height=&quot;1316&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bIPB4C/dJMcafnmE2S/qQEwalYoiTQHxYsIMRP440/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bIPB4C/dJMcafnmE2S/qQEwalYoiTQHxYsIMRP440/img.png&quot; data-alt=&quot;30건의 자유 형식 텍스트가 상품 설명 + 가격 + URL 구조로 정리되어 돌아왔다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bIPB4C/dJMcafnmE2S/qQEwalYoiTQHxYsIMRP440/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbIPB4C%2FdJMcafnmE2S%2FqQEwalYoiTQHxYsIMRP440%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;2246&quot; height=&quot;1316&quot; data-filename=&quot;스크린샷 2026-08-08 오후 3.10.22.png&quot; data-origin-width=&quot;2246&quot; data-origin-height=&quot;1316&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;30건의 자유 형식 텍스트가 상품 설명 + 가격 + URL 구조로 정리되어 돌아왔다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1장에서 봤던 그 뒤죽박죽한 텍스트가, 상품 설명 한 문단과 숫자 하나로 정리되어 나왔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;게다가 price는 float 형식이라 바로 계산에 쓸 수 있고, 파싱 실패를 대비한 try/except도 필요 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;느낀점&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;나는 Structured Output을 &quot;LLM의 출력 형식 맞춰주는 옵션&quot; 정도로만 알고 썼고 실제 LLM, SDK, Pydantic 각각의 역할이 뭔지 몰랐는데 이를 단계적으로 나눠서 이해해볼 수 있어서 좋았다. 특히 트랜스포머 기반 모델이 다음 토큰의 확률들을 예측 하는 단계에서, 이런 스키마를 통한 제약을 줌으로써 특정 토큰은 아예 다음 토큰 출력에서 배제 시킨다는 설명을 듣고서 왜 이게 잘 작동할 수 있는지 납득이 됐다.&lt;/li&gt;
&lt;li&gt;실무에서 LLM 출력 형식 제약을 둘때 참고가 될 수 있는 영역이었다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;Field(description=...)이 문서이자 프롬프트라는 점도 인상적이었다. 스키마 설계가 곧 프롬프트 엔지니어링이 되는 셈이라, 어떤 필드를 어떤 이름과 설명으로 둘지가 결과 품질에 직접 영향을 준다. 특히 실무에서 에이전트를 만들 때 이런 description 영역은 llm 한테 자동으로 짜달라고 하는 경우도 많지만, 작게 변경했는데도 결과 품질에 영향을 줄 수 있는 부분이고 기획자도 변경에 관여할 수 있는 영역이기에 해당 부분도 에이전트 기획/개발 시 참고해야겠다는 생각을 했다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 네 번째 에이전트 - 30건 딜 중 5건을 골라내는 ScannerAgent&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2장에서 한 건 노트북에서 직접 호출해본 것이었다. 이걸 시스템의 부품으로 쓰려면 클래스로 감싸야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇게 만든 게 agents/scanner_agent.py의 ScannerAgent, 이 시스템의 네 번째 에이전트다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;글 17에서 만든 세 개(SpecialistAgent, FrontierAgent, EnsembleAgent)가 &quot;이 상품 얼마?&quot;에 답하는 쪽이었다면, ScannerAgent는 그 질문 자체를 만들어오는 역할이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 에이전트는 같은 부모를 갖는다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;글 17 1장에서 봤던 Agent 추상 클래스를 여기서도 상속한다.&lt;/p&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;class ScannerAgent(Agent):
    MODEL = &quot;gpt-5-mini&quot;
    name = &quot;Scanner Agent&quot;
    color = Agent.CYAN
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;name과 color만 지정하면 부모의 log()가 알아서 이름표와 색을 붙여준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에이전트가 여러 개 동시에 돌아가면 로그가 뒤섞이는데, 색으로 구분하면 누가 무슨 일을 하고 있는지 바로 보인다. 이 색 코드가 나중에 7장에서 Gradio 화면의 로그 패널로 그대로 이어진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프롬프트에 가드가 여러 겹 들어간다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2장에서 Field(description=...)이 프롬프트 역할을 한다고 했는데, 그것만으로는 부족해서 시스템 프롬프트에도 같은 취지의 경고를 반복한다.&lt;/p&gt;
&lt;pre class=&quot;smalltalk&quot;&gt;&lt;code&gt;SYSTEM_PROMPT = &quot;&quot;&quot;You identify and summarize the 5 most detailed deals from a list, by selecting deals that have the most detailed, high quality description and the most clear price.
Respond strictly in JSON with no explanation, using this format. You should provide the price as a number derived from the description. If the price of a deal isn't clear, do not include that deal in your response.
Most important is that you respond with the 5 deals that have the most detailed product description with price. It's not important to mention the terms of the deal; most important is a thorough description of the product.
Be careful with products that are described as &quot;$XXX off&quot; or &quot;reduced by $XXX&quot; - this isn't the actual price of the product. Only respond with products when you are highly confident about the price.
&quot;&quot;&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심 문장 두 개만 옮기면 이렇다.&lt;/p&gt;
&lt;pre class=&quot;sml&quot;&gt;&lt;code&gt;If the price of a deal isn't clear, do not include that deal in your response.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; 가격이 명확하지 않은 딜은 아예 응답에 넣지 마라&lt;/p&gt;
&lt;pre class=&quot;vbnet&quot;&gt;&lt;code&gt;Be careful with products that are described as &quot;$XXX off&quot; - this isn't the actual price of the product.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; &quot;$XXX 할인&quot;이라고 적힌 상품을 조심해라. 그건 실제 가격이 아니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1장의 Logitech 딜 같은 걸 걸러내라는 지시다. 그리고 유저 프롬프트 앞뒤로도 같은 말을 한 번 더 붙인다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;USER_PROMPT_SUFFIX = &quot;\n\nInclude exactly 5 deals, no more.&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;=&amp;gt; 같은 지시를 스키마 설명, 시스템 프롬프트, 유저 프롬프트 세 군데에 나눠 넣었다. 중복처럼 보이지만 &quot;가격을 잘못 읽으면 시스템 전체가 헛일을 한다&quot;는 걸 생각하면 납득이 간다. 가격은 이 시스템의 입력값이고, 여기가 틀리면 뒤의 가격 예측도 알림도 전부 무의미해진다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;이미 본 딜은 다시 보지 않는다&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;scan() 전에 호출되는 fetch_deals()가 하는 일이 하나 더 있다.&lt;/p&gt;
&lt;pre class=&quot;ruby&quot;&gt;&lt;code&gt;def fetch_deals(self, memory) -&amp;gt; List[ScrapedDeal]:
    self.log(&quot;Scanner Agent is about to fetch deals from RSS feed&quot;)
    urls = [opp.deal.url for opp in memory]              # 과거에 다뤘던 딜의 URL
    scraped = ScrapedDeal.fetch()
    result = [scrape for scrape in scraped if scrape.url not in urls]   # 중복 제거
    self.log(f&quot;Scanner Agent received {len(result)} deals not already scraped&quot;)
    return result
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;memory는 과거에 이미 처리한 기회(Opportunity)들의 목록이다. 거기 있는 URL은 후보에서 빼버린다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작아 보이는 코드지만 의미가 있다. 이게 없으면 5분마다 돌 때마다 같은 딜을 계속 다시 평가하고, 같은 알림을 계속 보내게 된다. 에이전트가 &quot;이전에 무엇을 했는지&quot;를 기억한다는 것 자체가 나중에 다룰 에이전틱 AI의 특징 중 하나다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;스캔의 본체&lt;/h4&gt;
&lt;pre class=&quot;ruby&quot;&gt;&lt;code&gt;def scan(self, memory: List[str] = []) -&amp;gt; Optional[DealSelection]:
    scraped = self.fetch_deals(memory)
    if scraped:
        user_prompt = self.make_user_prompt(scraped)
        self.log(&quot;Scanner Agent is calling OpenAI using Structured Outputs&quot;)
        result = self.openai.chat.completions.parse(
            model=self.MODEL,
            messages=[
                {&quot;role&quot;: &quot;system&quot;, &quot;content&quot;: self.SYSTEM_PROMPT},
                {&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: user_prompt},
            ],
            response_format=DealSelection,
            reasoning_effort=&quot;minimal&quot;,
        )
        result = result.choices[0].message.parsed
        result.deals = [deal for deal in result.deals if deal.price &amp;gt; 0]   # 가격 0 이하는 제외
        self.log(f&quot;Scanner Agent received {len(result.deals)} selected deals with price&amp;gt;0 from OpenAI&quot;)
        return result
    return None
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2장에서 본 호출이 그대로 들어 있고, 앞에 수집과 중복 제거, 뒤에 price &amp;gt; 0 필터가 붙었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프롬프트로 아무리 당부해도 가격이 0으로 오는 경우가 있으니 코드로 한 번 더 막는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;=&amp;gt; LLM에게 맡긴 부분과 코드로 보장하는 부분이 나뉘어 있다. 판단은 모델이, 최소한의 안전장치는 코드가.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;=&amp;gt; 항상 에이전트를 만들 때 설계해야하는 중요한 원칙 중에 하나인 거 같다. (프롬프트 성격의 것들에도 강제를 해두지만, 백엔드 코드 상으로도 강제를 해둬야 안전하게 돌아갈 수 있다는 것)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 돌려보면&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;agent = ScannerAgent()
result = agent.scan()
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;45.8초가 걸렸다. 이 중 대부분은 1장에서 본 RSS 수집 시간이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-08-08 오후 3.25.09.png&quot; data-origin-width=&quot;2246&quot; data-origin-height=&quot;614&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0LCcC/dJMcab6g8W9/kFPfGyy9X4RBD6U0d0C7rk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0LCcC/dJMcab6g8W9/kFPfGyy9X4RBD6U0d0C7rk/img.png&quot; data-alt=&quot;수집 30건 -&amp;amp;gt; Structured Outputs 호출 -&amp;amp;gt; 가격이 있는 5건 선별까지 로그에 그대로 찍힌다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0LCcC/dJMcab6g8W9/kFPfGyy9X4RBD6U0d0C7rk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0LCcC%2FdJMcab6g8W9%2FkFPfGyy9X4RBD6U0d0C7rk%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;2246&quot; height=&quot;614&quot; data-filename=&quot;스크린샷 2026-08-08 오후 3.25.09.png&quot; data-origin-width=&quot;2246&quot; data-origin-height=&quot;614&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;수집 30건 -&amp;gt; Structured Outputs 호출 -&amp;gt; 가격이 있는 5건 선별까지 로그에 그대로 찍힌다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에 뽑힌 5건은 이렇다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Apple AirPods Pro 3 (2025) 리퍼 - $150&lt;/li&gt;
&lt;li&gt;Samsung M70H 55인치 4K Mini LED TV - $350&lt;/li&gt;
&lt;li&gt;Acer 15.6인치 포터블 모니터 PM161Q 리퍼 - $37&lt;/li&gt;
&lt;li&gt;Shark Matrix Plus 로봇청소기 - $249.99&lt;/li&gt;
&lt;li&gt;Apple MacBook Air M1 13.3인치 (2020) 리퍼 - $399&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2장에서 뽑힌 목록과 다르다. 같은 코드지만 RSS를 다시 긁어오기 때문에 그사이 피드가 바뀌었고, 모델의 선택도 매번 완전히 같지는 않다. 이 시스템은 실행할 때마다 다른 걸 본다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트용 딜도 준비되어 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ScannerAgent에는 test_scan()이라는 메서드가 하나 더 있다. 하드코딩된 딜 4건을 그냥 DealSelection 객체로 만들어 돌려준다.&lt;/p&gt;
&lt;pre class=&quot;elixir&quot;&gt;&lt;code&gt;def test_scan(self, memory: List[str] = []) -&amp;gt; Optional[DealSelection]:
    results = { &quot;deals&quot;: [ ... ] }   # Hisense TV, Poly Studio P21, Lenovo IdeaPad, Dell G15
    return DealSelection(**results)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RSS를 긁고 모델을 부르는 데 45초씩 걸리니, 뒷단을 개발할 때는 이 고정 데이터로 돌리라는 것이다. 6장에서 실제로 이걸 쓴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;느낀점&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;프롬프트에 같은 경고를 세 군데 반복해서 넣은 게 처음엔 지저분해 보였는데, 생각해보면 이 시스템에서 긁어온 상품 딜의 가격은 사람이 검증하지 않고 그대로 다음 단계로 흘러가는 값이다. 검증 없이 흘러가는 값일수록 앞단에서 겹겹이 막아둘 수밖에 없다는 게 이해가 됐다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;LLM에게 맡길 부분과 코드로 보장할 부분을 나눈 방식도 참고가 됐다. 어떤 딜이 좋은지 고르는 판단은 모델에게 주고, &quot;가격은 0보다 커야 한다&quot; 같은 명확한 규칙은 코드로 처리한다. 실무에서도 이 경계선을 어디에 그을지가 결국 설계의 핵심인 것 같다. 각 에이전트의 시나리오별로, 그리고 실수하면 안되는 영역인지 등의 중요성등을 고려해서 이런 백엔드/모델 간의 역할 경계를 잘 만들어주는게 에이전트 개발에서의 중요한 지점이라고 생각이 든다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;딜 중복 서치를 방지하기 위해 memory를 참조한다는 게 인상적이었다. 꼭 복잡한 메모리 시스템이 아니더라도, 비즈니스 로직이 돌아가기 위해 필수적이라면 이런 메모리 시스템을 만드는게 필요하다는 걸 느꼈다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 다섯 번째 에이전트 - 딜 알림을 보내주는 메세징 에이전트&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;딜을 모으고 5개만 골라내는 에이전트를 위에 스캐너 에이전트로 설명했다.&amp;nbsp;&lt;/p&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;p data-ke-size=&quot;size16&quot;&gt;그게 다섯 번째 에이전트인 MessagingAgent다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Pushover로 알림을 받는다&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강의는 Pushover라는 서비스를 쓴다. 계정을 만들고 애플리케이션을 하나 등록하면 키 두 개가 나오는데, 그걸 .env에 넣어두면 된다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;PUSHOVER_USER=u로 시작하는 사용자 키
PUSHOVER_TOKEN=a로 시작하는 애플리케이션 토큰
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원래 강의는 휴대폰에 Pushover 앱을 설치해서 받는 방식인데, 나는 앱 대신 브라우저 웹 푸시로 대체했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Pushover가 Chrome 웹 푸시를 지원해서 앱 없이도 알림을 받을 수 있다.&lt;/p&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;p data-ke-size=&quot;size16&quot;&gt;보내는 것 자체는 그냥 HTTP POST 한 번이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;알림 발송 코드는 놀랄 만큼 단순하다.&lt;/p&gt;
&lt;pre class=&quot;ruby&quot;&gt;&lt;code&gt;def push(self, text):
    self.log(&quot;Messaging Agent is sending a push notification&quot;)
    payload = {
        &quot;user&quot;: self.pushover_user,
        &quot;token&quot;: self.pushover_token,
        &quot;message&quot;: text,
        &quot;sound&quot;: &quot;cashregister&quot;,   # 금전등록기 소리
    }
    requests.post(pushover_url, data=payload)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;sound=&quot;cashregister&quot;는 강사의 취향이다. 돈 버는 소식이니 금전등록기 소리로 울린다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;노트북에서 바로 테스트해보면 이렇게 된다.&lt;/p&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;agent = MessagingAgent()
agent.push(&quot;SUCH A MASSIVE DEAL!!&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보낸 그대로 알림이 온다. 여기까지는 그냥 알림 API 연동이고 AI와는 상관이 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;알림을 보내는 방법이 두 가지다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MessagingAgent에는 알림을 보내는 메서드가 두 개 있고, 성격이 완전히 다르다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째는 alert().&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전달해야하는 주요 값(예측 가격, 할인 가격, 상품 설명 등) 을 사전에 정규화해서 정해놓고 코드에서 내려오는 값을 문자열로 이어 붙인다.&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;def alert(self, opportunity: Opportunity):
    text = f&quot;Deal Alert! Price=${opportunity.deal.price:.2f}, &quot;
    text += f&quot;Estimate=${opportunity.estimate:.2f}, &quot;
    text += f&quot;Discount=${opportunity.discount:.2f} :&quot;
    text += opportunity.deal.product_description[:10] + &quot;... &quot;
    text += opportunity.deal.url
    self.push(text)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정확하지만 사람이 읽기엔 좀 딱딱하다. 상품 설명은 앞 10글자만 들어간다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 번째는 notify(). 여기서 Claude가 등장한다.&lt;/p&gt;
&lt;pre class=&quot;ruby&quot;&gt;&lt;code&gt;MODEL = &quot;claude-sonnet-4-5&quot;

def craft_message(self, description: str, deal_price: float, estimated_true_value: float) -&amp;gt; str:
    user_prompt = &quot;Please summarize this great deal in 2-3 sentences to be sent as an exciting push notification alerting the user about this deal.\n&quot;
    user_prompt += f&quot;Item Description: {description}\nOffered Price: {deal_price}\nEstimated true value: {estimated_true_value}&quot;
    user_prompt += &quot;\n\nRespond only with the 2-3 sentence message which will be used to alert &amp;amp; excite the user about this deal&quot;
    response = completion(
        model=self.MODEL,
        messages=[{&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: user_prompt}],
    )
    return response.choices[0].message.content


def notify(self, description: str, deal_price: float, estimated_true_value: float, url: str):
    self.log(&quot;Messaging Agent is using Claude to craft the message&quot;)
    text = self.craft_message(description, deal_price, estimated_true_value)
    self.push(text[:200] + &quot;... &quot; + url)   # 알림 길이 제한 때문에 200자 컷
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상품 설명, 딜가, 추정가 세 개를 주고 &quot;사용자가 솔깃할 만한 2~3문장으로 써달라&quot;고 요청한다. 그리고 그 결과를 200자로 자른 뒤 URL을 붙여 발송한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 쓰는 completion()은 litellm이다. 지금까지 OpenAI는 openai 라이브러리로 불렀는데, Claude는 litellm으로 부른다. 여러 회사의 모델을 같은 인터페이스로 호출할 수 있게 해주는 라이브러리다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;=&amp;gt; 이 시스템이 특정 회사 모델에 묶여 있지 않다는 게 여기서 드러난다. 딜 선별은 gpt-5-mini, 가격 추정은 파인튜닝 Llama와 gpt-5.1,딥러닝 신경망, 알림 문구는 claude-sonnet-4-5. 각 자리에 맞는 모델을 그냥 갖다 쓴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 Claude에게 써달라고 해보면 다음과 같이 나온다다.&lt;/p&gt;
&lt;pre class=&quot;lsl&quot;&gt;&lt;code&gt;agent.notify(&quot;A special deal on Sumsung 60 inch LED TV going at a great bargain&quot;, 300, 1000, &quot;www.samsung.com&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입력은 오타(Sumsung)까지 섞인 짧은 문장 하나와 숫자 두 개뿐이다. 그런데 알림으로 도착한 문구는 이랬다.&lt;/p&gt;
&lt;pre class=&quot;fortran&quot;&gt;&lt;code&gt;  INCREDIBLE DEAL ALERT! Get a Samsung 60&quot; LED TV for just $300 - that's 70% OFF the $1,000
value! This massive screen at an unbeatable price won't last long - grab it now!... www.samsung.com
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; 삼성 60인치 LED TV를 단돈 $300에. $1,000 가치 대비 70% 할인이다. 이 가격은 오래 안 간다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시키지 않았는데 오타를 고쳤고, $300과 $1,000을 받아서 70%라는 할인율을 직접 계산해 넣었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;950&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0Md5Z/dJMcagGAyKH/5GjE0MR3FtwpfJ2Crrntm0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0Md5Z/dJMcagGAyKH/5GjE0MR3FtwpfJ2Crrntm0/img.png&quot; data-alt=&quot;아래에서부터 MASSIVE DEAL 테스트, SUCH A MASSIVE DEAL 테스트, 그리고 Claude가 쓴 삼성 TV 알림. 맨 위 로보락 알림은 6장에서 실제로 받은 것이다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0Md5Z/dJMcagGAyKH/5GjE0MR3FtwpfJ2Crrntm0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0Md5Z%2FdJMcagGAyKH%2F5GjE0MR3FtwpfJ2Crrntm0%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;2048&quot; height=&quot;950&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;950&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;아래에서부터 MASSIVE DEAL 테스트, SUCH A MASSIVE DEAL 테스트, 그리고 Claude가 쓴 삼성 TV 알림. 맨 위 로보락 알림은 6장에서 실제로 받은 것이다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&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;pre class=&quot;routeros&quot;&gt;&lt;code&gt;alert()  -&amp;gt; Deal Alert! Price=$300.00, Estimate=$1000.00, Discount=$700.00 :A special d... www.samsung.com
notify() -&amp;gt;   INCREDIBLE DEAL ALERT! Get a Samsung 60&quot; LED TV for just $300 - that's 70% OFF ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘 중 어느 쪽을 쓰는지는 5장과 6장에서 갈린다. 규칙 기반 플래너는 alert()를, 자율 플래너는 notify()를 쓴다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;느낀점&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;LLM을 &quot;판단&quot;에만 쓴다고 생각하기 쉬운데, 여기서는 마지막 문장을 다듬는 용도로 쓴다. 상품 설명과 숫자 두 개를 사람이 읽고 싶은 문장으로 바꾸는 일. 정확도가 크게 중요하지 않고 표현력이 중요한 지점이라 오히려 잘 어울린다.&lt;/li&gt;
&lt;li&gt;특히나 이 부분을 alert 처럼 백엔드에서 내려오는 값으로만 구성하는 방법도 있지만, 그럼 챗봇처럼 딱 정형화된 답변이 나올 수도 있다. 기획적인 의도에 따라, 정해진 형식대로 나오게 하고 싶은지 좀 더 표현력있는 AI 처럼 나오게 하고 싶은지에 따라서 선택이 달라질 수 있을 거 같다. 에이전트 응답 UX를 만들 떄 고려할 부분이다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;각 기능마다 다른 회사의 모델을 쓴다는 게 인상적이었다. 용도에 따라 모델을 다르게 가져가는 부분도 인상적인 포인트 중 하나였다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 먼저 규칙으로 조율하기 - PlanningAgent&lt;/h2&gt;
&lt;p 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;SpecialistAgent - Modal에 올린 파인튜닝 Llama로 가격 예측 (글 17)&lt;/li&gt;
&lt;li&gt;FrontierAgent - RAG로 유사 상품을 찾아 gpt-5.1전달해준뒤 가격 예측 시킴(글 17)&lt;/li&gt;
&lt;li&gt;NeuralNetworkAgent - Week 6의 신경망으로 가격 예측 (글 17)&lt;/li&gt;
&lt;li&gt;ScannerAgent - RSS에서 딜을 긁어 5건 선별 (3장)&lt;/li&gt;
&lt;li&gt;MessagingAgent - 알림 발송 (4장)&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 1~3번을 묶은 EnsembleAgent가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부품은 다 나왔는데 아직 아무도 이들을 순서대로 부르지 않는다. 그 역할을 하는 게 플래닝 에이전트다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 순서를 코드에 박아둔 버전을 본다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;세 에이전트를 하나로 묶는다&lt;/h4&gt;
&lt;pre class=&quot;ruby&quot;&gt;&lt;code&gt;class PlanningAgent(Agent):
    name = &quot;Planning Agent&quot;
    color = Agent.GREEN
    DEAL_THRESHOLD = 50

    def __init__(self, collection):
        self.log(&quot;Planning Agent is initializing&quot;)
        self.scanner = ScannerAgent()
        self.ensemble = EnsembleAgent(collection)
        self.messenger = MessagingAgent()
        self.log(&quot;Planning Agent is ready&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성자에서 세 에이전트를 전부 만들어 들고 있는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;collection은 글 17에서 만든 Chroma 벡터스토어이고, EnsembleAgent가 RAG를 돌리는 데 쓴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DEAL_THRESHOLD = 50은 알림 기준선이다. 추정가와 실제 딜가의 차이가 $50를 넘어야 알림을 보낸다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;딜 하나를 기회로 바꾸는 계산&lt;/h4&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;def run(self, deal: Deal) -&amp;gt; Opportunity:
    self.log(&quot;Planning Agent is pricing up a potential deal&quot;)
    estimate = self.ensemble.price(deal.product_description)
    discount = estimate - deal.price
    self.log(f&quot;Planning Agent has processed a deal with discount ${discount:.2f}&quot;)
    return Opportunity(deal=deal, estimate=estimate, discount=discount)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;계산 자체는 뺄셈 한 줄이다. 앙상블이 추정한 가격에서 실제 판매가를 빼면 그게 할인폭이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 Opportunity는 agents/deals.py에 정의된 또 하나의 pydantic 클래스다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;class Opportunity(BaseModel):
    &quot;&quot;&quot;
    A class to represent a possible opportunity: a Deal where we estimate
    it should cost more than it's being offered
    &quot;&quot;&quot;
    deal: Deal
    estimate: float
    discount: float
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주석을 옮기면 &quot;우리가 추정한 가격이 실제 판매가보다 높은 딜&quot; = 기회다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;=&amp;gt; Deal(딜)과 Opportunity(기회)를 나눠둔 게 깔끔하다. 딜은 인터넷에서 긁어온 사실이고, 기회는 우리 모델이 판단을 얹은 결과다. 사실과 판단을 다른 타입으로 분리해둔 셈이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;전체 순서&lt;/h4&gt;
&lt;pre class=&quot;monkey&quot;&gt;&lt;code&gt;def plan(self, memory: List[str] = []) -&amp;gt; Optional[Opportunity]:
    self.log(&quot;Planning Agent is kicking off a run&quot;)
    selection = self.scanner.scan(memory=memory)                      # 1. 딜 수집&amp;middot;선별
    if selection:
        opportunities = [self.run(deal) for deal in selection.deals[:5]]   # 2. 5건 전부 가격 추정
        opportunities.sort(key=lambda opp: opp.discount, reverse=True)     # 3. 할인폭 내림차순 정렬
        best = opportunities[0]
        self.log(f&quot;Planning Agent has identified the best deal has discount ${best.discount:.2f}&quot;)
        if best.discount &amp;gt; self.DEAL_THRESHOLD:                            # 4. 기준 넘으면 알림
            self.messenger.alert(best)
        self.log(&quot;Planning Agent has completed a run&quot;)
        return best if best.discount &amp;gt; self.DEAL_THRESHOLD else None
    return None
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;딜을 스캔해 5건의 딜을 가져오고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5건을 다 가격 추정을 돌리고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;할인폭을 계산한뒤 할인폭이 큰 순서대로 정렬하고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정해둔 할인폭 기준을 넘는 딜이 있으면 opportunity 로 알린다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 알림은 4장에서 본 두 방법 중 alert(), 즉 기계적으로 값을 이어 붙이는 쪽이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식의 장점은 분명하다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드는 예측 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;딜 5건이면 앙상블 호출이 정확히 5번이고, 스캔은 1번이고, 알림은 최대 1번이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비용도 시간도 미리 계산할 수 있다. 뭔가 잘못되면 어느 줄에서 틀렸는지 바로 찾을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;글 17 1장에서 인용한 강사의 경고와도 일맥상통한다. 문제를 만나자마자 에이전트 아키텍처로 뛰어들지 말고, 단순한 것부터 시작하라는 이야기.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 한 가지가 코드에 고정되어 있다. 무엇을 언제 할지에 대한 순서다.&lt;/p&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;scan -&amp;gt; 5건 전부 가격 추정 -&amp;gt; 할인폭 정렬 -&amp;gt; 1등이 할인폭 $50 넘으면 알림&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 순서를 바꾸려면 코드를 고치는 방법 밖에 없다. 딜 5건 중 명백히 가망 없는 게 있어도 전부 가격추정 하고, 알림은 무조건 1등 1건이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;=&amp;gt; 6장에서는 이 순서를 코드에서 빼내 LLM에게 넘긴다. 같은 부품, 같은 목표인데 조율하는 주체만 바뀐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;느낀점&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 장은 새로운 개념이 없는데도 넣어둔 이유가 있다고 느꼈다. 다음 장의 자율 에이전트를 이해하려면 &quot;순서를 코드가 정하는 상태&quot;를 먼저 눈으로 봐야 대비가 되기 때문이다. 강의 구성이 친절하다고 느낀 대목이었다.&lt;/li&gt;
&lt;li&gt;실무에서 에이전트를 만들 때도 결국 이 질문으로 돌아오는 것 같다. 이 순서를 사람이 정할 것인가, 모델이 정하게 둘 것인가. 정답이 하나로 정해져 있지 않고, 예측 가능성이 중요한 기능이냐 유연성이 중요한 기능이냐에 따라 달라진다. 실제 최근에도 실무에서 에이전트를 기획하는 과정에서 이 시나리오를 사람이 정해둔 순서대로 동작하게 할것이냐, 모델이 자율적으로 정하게 할것이냐에 대한 고민들을 계속 하게 되는 거 같다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;Deal과 Opportunity를 타입으로 분리한 게 작지만 좋았다. 긁어온 사실과 우리가 얹은 판단을 섞어두면 나중에 &quot;이 숫자는 어디서 나온 거지&quot;를 추적하기 어려워진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 이제 실행 순서를 LLM에게 맡긴다 - 툴 콜과 자율 에이전트&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5장의 플래너는 실행 순서가 코드에 박혀 있었다. 이번 장에서는 그 순서를 빼낸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부품은 그대로 두고, 무엇을 언제 부를지만 LLM이 정하게 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강의는 여기서 에이전틱 AI가 무엇인지 정의하고 넘어간다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에이전틱 AI의 다섯 가지 특징&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강사가 제시하는 hallmarks of an agentic AI solution(에이전틱 AI 솔루션의 특징) 다섯 가지다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;breaking a larger problem into smaller steps carried out by individual processes/models &lt;br /&gt;-&amp;gt; 큰 문제를 작은 단계로 나누고, 각 단계를 개별 프로세스나 모델이 처리한다&lt;/li&gt;
&lt;li&gt;using tools / function calling / structured output &lt;br /&gt;-&amp;gt; 툴, 함수 호출, 구조화된 출력을 쓴다&lt;/li&gt;
&lt;li&gt;an agent environment in which agents can collaborate &lt;br /&gt;-&amp;gt; 에이전트들이 협업할 수 있는 환경이 있다&lt;/li&gt;
&lt;li&gt;a planning agent that coordinates activities &lt;br /&gt;-&amp;gt; 활동을 조율하는 플래닝 에이전트가 있다&lt;/li&gt;
&lt;li&gt;autonomy &amp;amp; memory - existing beyond a chat with a human &lt;br /&gt;-&amp;gt; 자율성과 메모리. 사람과의 대화 한 번을 넘어서 존재한다&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 만든 것으로 보면 1번은 에이전트 여섯 개로, 2번은 2장의 Structured Outputs로, 3번은 Agent 부모 클래스와 로깅으로, 4번은 5장의 플래너로 이미 채웠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;남은 게 5번, 그리고 2번의 &quot;툴&quot;이다. 이번 장이 그 두 개를 채운다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 가짜 함수로 원리를 확인한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Day 4 노트북은 곧바로 진짜 에이전트를 붙이지 않는다. 3장에서 본 test_scan()의 하드코딩 딜을 쓰고, 함수 세 개도 가짜로 만든다.&lt;/p&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;test_results = ScannerAgent().test_scan()

def scan_the_internet_for_bargains() -&amp;gt; str:
    print(&quot;Fake function to scan the internet - this returns a hardcoded set of deals&quot;)
    return test_results.model_dump_json()

def estimate_true_value(description: str) -&amp;gt; str:
    print(f&quot;Fake function to estimating true value of {description[:20]}... - this always returns $300&quot;)
    return f&quot;Product {description} has an estimated true value of $300&quot;

def notify_user_of_deal(description: str, deal_price: float, estimated_true_value: float, url: str) -&amp;gt; str:
    print(f&quot;Fake function to notify user of {description} which costs {deal_price} and estimate is {estimated_true_value}&quot;)
    return &quot;notification sent ok&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추정가는 무조건 $300을 돌려주고, 알림은 실제로 보내지 않고 print만 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;45초짜리 RSS 수집도, 돈 드는 모델 호출도 없이 툴 콜 흐름만 확인하려는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;함수를 LLM에게 설명하는 방법&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LLM은 파이썬 함수를 직접 볼 수 없다. 그래서 함수마다 &quot;이런 게 있고 이런 인자를 받는다&quot;를 JSON으로 설명해줘야 한다.&lt;/p&gt;
&lt;pre class=&quot;smalltalk&quot;&gt;&lt;code&gt;estimate_function = {
    &quot;name&quot;: &quot;estimate_true_value&quot;,
    &quot;description&quot;: &quot;Given the description of an item, estimate how much it is actually worth&quot;,
    &quot;parameters&quot;: {
        &quot;type&quot;: &quot;object&quot;,
        &quot;properties&quot;: {
            &quot;description&quot;: {
                &quot;type&quot;: &quot;string&quot;,
                &quot;description&quot;: &quot;The description of the item to be estimated&quot;
            },
        },
        &quot;required&quot;: [&quot;description&quot;],
        &quot;additionalProperties&quot;: False
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2장의 pydantic 스키마와 형태가 거의 같다. 다른 점은 방향이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2장은 &quot;이 형식으로 답을 내놔라&quot;였고, 여기는 &quot;이런 함수를 부를 수 있다&quot;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;알림 함수의 설명에는 이런 문장이 들어간다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Send the user a push notification about the single most compelling deal; only call this one time
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; 가장 매력적인 딜 하나에 대해서만 푸시 알림을 보내라. 이 함수는 한 번만 호출해라&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;함수 설명란에 &quot;한 번만 불러라&quot;라는 행동 지침이 들어가 있다. 여기도 description이 곧 프롬프트다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세 개를 리스트로 묶으면 준비 끝이다.&lt;/p&gt;
&lt;pre class=&quot;makefile&quot;&gt;&lt;code&gt;tools = [
    {&quot;type&quot;: &quot;function&quot;, &quot;function&quot;: scan_function},
    {&quot;type&quot;: &quot;function&quot;, &quot;function&quot;: estimate_function},
    {&quot;type&quot;: &quot;function&quot;, &quot;function&quot;: notify_function},
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 while 루프 하나다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;툴 콜의 동작 원리는 생각보다 단순하다.&lt;/p&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;done = False
while not done:
    response = openai.chat.completions.create(model=MODEL, messages=messages, tools=tools)
    if response.choices[0].finish_reason == &quot;tool_calls&quot;:     # 모델이 함수를 부르고 싶어함
        message = response.choices[0].message
        results = handle_tool_call(message)                   # 우리가 대신 실행
        messages.append(message)                              # 요청과
        messages.extend(results)                              # 결과를 대화에 추가
    else:
        done = True                                           # 더 부를 게 없으면 종료
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모델이 함수를 부르고 싶으면 finish_reason이 &quot;tool_calls&quot;로 온다. 그러면 우리가 실제로 그 함수를 실행하고, 요청과 결과를 대화 기록에 붙여서 다시 모델에게 넘긴다. 모델이 더 이상 부를 게 없다고 판단하면 그냥 텍스트로 답하고 루프가 끝난다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;handle_tool_call이 하는 일은 이름으로 함수를 찾아 인자를 풀어 실행하는 것뿐이다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;def handle_tool_call(message):
    results = []
    for tool_call in message.tool_calls:
        tool_name = tool_call.function.name
        arguments = json.loads(tool_call.function.arguments)   # 모델이 채운 인자
        tool = globals().get(tool_name)
        result = tool(**arguments) if tool else {}
        results.append({&quot;role&quot;: &quot;tool&quot;, &quot;content&quot;: json.dumps(result), &quot;tool_call_id&quot;: tool_call.id})
    return results
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;=&amp;gt; 모델이 함수를 실행하는 게 아니다. 모델은 &quot;이 함수를 이 인자로 불러줘&quot;라고 말할 뿐이고, 실행은 우리 코드가 한다. 결과를 다시 넣어주면 모델이 그걸 보고 다음 판단을 한다. 이 왕복이 반복되는 게 툴 콜 루프다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 지시는 이게 전부다&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;system_message = &quot;You find great deals on bargain products using your tools, and notify the user of the best bargain.&quot;

user_message = &quot;&quot;&quot;
First, use your tool to scan the internet for bargain deals. Then for each deal, use your tool to estimate its true value.
Then pick the single most compelling deal where the price is much lower than the estimated true value, and use your tool to notify the user.
Then just reply OK to indicate success.
&quot;&quot;&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; 먼저 툴로 인터넷에서 딜을 스캔해라. 그다음 각 딜마다 툴로 실제 가치를 추정해라. 그리고 가격이 추정 가치보다 훨씬 낮은 가장 매력적인 딜 하나를 골라 툴로 사용자에게 알려라. 그다음 OK라고만 답해라&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5장의 plan() 메서드와 내용은 비슷한데, 이건 파이썬이 아니라 영어 문장이다. 그리고 &quot;5건&quot;이나 &quot;$50&quot; 같은 구체적인 숫자가 없다. 몇 번 부를지, 어떤 걸 고를지는 모델이 정한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;가짜를 진짜로 바꾼다&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원리를 확인했으니 agents/autonomous_planning_agent.py에서 가짜 함수를 실제 에이전트 호출로 교체한다.&lt;/p&gt;
&lt;pre class=&quot;ruby&quot;&gt;&lt;code&gt;class AutonomousPlanningAgent(Agent):
    name = &quot;Autonomous Planning Agent&quot;
    color = Agent.GREEN
    MODEL = &quot;gpt-5.1&quot;

    def scan_the_internet_for_bargains(self) -&amp;gt; str:
        self.log(&quot;Autonomous Planning agent is calling scanner&quot;)
        results = self.scanner.scan(memory=self.memory)
        return results.model_dump_json() if results else &quot;No deals found&quot;

    def estimate_true_value(self, description: str) -&amp;gt; str:
        self.log(&quot;Autonomous Planning agent is estimating value via Ensemble Agent&quot;)
        estimate = self.ensemble.price(description)
        return f&quot;The estimated true value of {description} is {estimate}&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;test_scan() 자리에 진짜 ScannerAgent가, 무조건 $300 돌려주던 자리에 EnsembleAgent가 들어갔다. 툴 스키마와 while 루프는 그대로다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;알림 쪽에는 가드가 하나 더 들어간다.&lt;/p&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;def notify_user_of_deal(self, description, deal_price, estimated_true_value, url) -&amp;gt; Dict:
    if self.opportunity:
        self.log(&quot;Autonomous Planning agent is trying to notify the user a 2nd time; ignoring&quot;)
    else:
        self.log(&quot;Autonomous Planning agent is notifying user&quot;)
        self.messenger.notify(description, deal_price, estimated_true_value, url)
        ...
    return &quot;Notification sent ok&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미 알림을 보냈으면 두 번째 호출은 그냥 무시한다. 툴 설명에 &quot;한 번만 불러라&quot;라고 써뒀는데도 코드로 한 번 더 막는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;=&amp;gt; 3장에서 본 것과 같은 패턴이다. 프롬프트로 부탁하고, 코드로 보장한다. 특히 자율성을 준 상황에서는 이 가드가 더 중요해진다. 순서를 모델이 정한다는 건 예상 못 한 순서도 나올 수 있다는 뜻이기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 여기서 쓰는 알림은 notify(), 즉 4장에서 본 Claude가 문구를 쓰는 쪽이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 돌려보면&lt;/p&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;agent = AutonomousPlanningAgent(collection)
agent.plan()
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2분 26.4초가 걸렸다. 로그를 따라가 보면 모델이 이렇게 움직였다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;950&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjAQDp/dJMcahlb6CL/bXKDp8WwG9wqGVlsch7gbk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjAQDp/dJMcahlb6CL/bXKDp8WwG9wqGVlsch7gbk/img.png&quot; data-alt=&quot;초록색 자율 플래너가 하늘색 스캐너를 부르고, 그다음 노란색 앙상블 안에서 빨강&amp;amp;middot;보라&amp;amp;middot;자주색 에이전트가 차례로 값을 내놓는다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjAQDp/dJMcahlb6CL/bXKDp8WwG9wqGVlsch7gbk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbjAQDp%2FdJMcahlb6CL%2FbXKDp8WwG9wqGVlsch7gbk%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;2048&quot; height=&quot;950&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;950&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;초록색 자율 플래너가 하늘색 스캐너를 부르고, 그다음 노란색 앙상블 안에서 빨강&amp;middot;보라&amp;middot;자주색 에이전트가 차례로 값을 내놓는다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;색깔로 누가 말하고 있는지가 바로 보인다. 초록은 자율 플래너, 하늘은 스캐너, 노랑은 앙상블, 그 안에서 빨강(Specialist), 보라(Frontier), 자주(Neural Network)가 각자 값을 낸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;딜 5건에 대한 앙상블 결과는 이렇게 나왔다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1건: Specialist $299.00 / Frontier $289.00 / NN  $81.68 -&amp;gt; Ensemble $269.27
2건: Specialist $220.00 / Frontier $179.00 / NN $252.94 -&amp;gt; Ensemble $190.49
3건: Specialist $299.00 / Frontier $649.99 / NN $148.03 -&amp;gt; Ensemble $564.69
4건: Specialist $220.00 / Frontier $849.99 / NN $318.34 -&amp;gt; Ensemble $733.83
5건: Specialist $299.00 / Frontier $749.99 / NN $152.52 -&amp;gt; Ensemble $645.14
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앙상블 가중치는 Frontier 0.8, Specialist 0.1, Neural Network 0.1이다. 3건을 넣어 검산해보면 649.99 &amp;times; 0.8 + 299 &amp;times; 0.1 + 148.03 &amp;times; 0.1 = 564.69로 맞는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 눈에 띄는 건 신경망의 편차다. $81.68이나 $148.03처럼 다른 둘과 크게 벌어지는 값을 낸다. 글 17에서 앙상블이 RAG 단독보다 겨우 $0.27 나았던 이유가 이 로그에서 좀 더 실감이 났다. 가중치를 0.1로 낮게 준 데는 이유가 있었던 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 로그의 마지막 부분은 이렇다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막 줄의 OK가 유저 메시지에서 요청했던 그 OK다. 모델이 할 일을 다 마쳤다고 판단하고 텍스트로 답한 것이고, 그 순간 while 루프가 종료됐다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;919&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/caR2t0/dJMcagmaqvW/96D1f7se5tHAotOZUvpQXk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/caR2t0/dJMcagmaqvW/96D1f7se5tHAotOZUvpQXk/img.png&quot; data-alt=&quot;다섯 번째 평가가 끝나자 플래너가 알림을 결정하고, Claude가 문구를 만들어 발송한 뒤 OK로 마무리한다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/caR2t0/dJMcagmaqvW/96D1f7se5tHAotOZUvpQXk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcaR2t0%2FdJMcagmaqvW%2F96D1f7se5tHAotOZUvpQXk%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;2048&quot; height=&quot;919&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;919&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;다섯 번째 평가가 끝나자 플래너가 알림을 결정하고, Claude가 문구를 만들어 발송한 뒤 OK로 마무리한다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모델이 고른 건 3번째 딜, 로보락 Qrevo였다. 딜가 $400에 앙상블 추정 $564.69, 차이는 약 $164.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 4장에서 봤던 그 알림이 도착했다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;  Amazing Deal Alert! Save $164 on the Roborock Qrevo robot vacuum &amp;amp; mop combo with powerful
8,000Pa suction, self-emptying station, and automatic mop washing&amp;mdash;now just $400! This smart
cleaning powerh... https://www.dealnews.com/Roborock-Qrevo-8-000-Pa-Robot-Vacuum-and-Mop-for-400-free-shipping/...
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반환값은 Opportunity 객체다.&lt;/p&gt;
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;Opportunity(deal=Deal(product_description='Roborock Qrevo is a self-emptying robot vacuum and mop
that delivers 8,000Pa HyperForce suction and includes an all-in-one docking station that washes
and self-empties. ...
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;한 사이클에 모델이 몇 번 호출되나&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강의 노트에 적어둔 숫자가 있다. 한 사이클에 모델 호출이 34번, 그중 LLM이 29번이고 신경망이 5번이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세어보면 이렇다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;딜 5건 각각에 대해 전처리(llama3.2) 1번, Specialist(파인튜닝 Llama) 1번, Frontier의 임베딩 1번과 gpt-5.1 1번, 신경망 1번. 여기에 스캐너의 gpt-5-mini 1번, 알림 문구의 claude-sonnet-4-5 1번, 그리고 자율 플래너 자신이 툴 콜 루프를 도는 gpt-5.1 호출들이 더해진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 번 돌 때 다섯 회사의 서로 다른 모델이 서른 번 넘게 불려 나간다는 이야기다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;느낀점&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;툴 콜의 원리가 while 루프 하나라는 게 의외로 허무했다. finish_reason을 보고 함수를 대신 실행해 결과를 다시 넣어주는 것뿐인데, 그게 반복되면서 &quot;스스로 일하는 것처럼&quot; 보인다. 복잡한 프레임워크 없이도 에이전트가 성립한다는 강사의 말이 여기서 이해가 됐다.&lt;/li&gt;
&lt;li&gt;가짜 함수로 흐름만 먼저 확인하고 나중에 진짜로 갈아 끼우는 방식이 좋았다. 실무에서 에이전트를 만들 때도 툴 하나하나가 느리고 비싸서 전체를 돌려보며 디버깅하기가 부담스러운데, 인터페이스만 맞춰두고 껍데기로 먼저 돌려보는 접근은 그대로 쓸 만하다.&lt;/li&gt;
&lt;li&gt;모델에게 자율성을 준다는 게 결국 &quot;어디까지 맡기고 어디부터 막을 것인가&quot;의 문제라는 걸 다시 확인했다. 알림 중복 방지처럼 사용자에게 직접 노출되는 부분은 프롬프트만 믿지 않고 코드로 잠가둔다. 자율성이 커질수록 오히려 가드는 더 촘촘해져야 한다는 게 인상적이었다.&lt;/li&gt;
&lt;li&gt;다만 여기서 한 가지 짚고 싶은 게 있다. 5장의 규칙 기반 플래너는 딜 5건이면 앙상블 5번으로 끝이 정해져 있는데, 자율 플래너는 모델이 몇 번 부를지 모른다. 실행할 때마다 비용과 시간이 달라진다는 뜻이다. 자율성을 얻는 대신 예측 가능성을 내놓는 거래인 셈이다.&lt;/li&gt;
&lt;li&gt;짧은 프롬프트 지시만으로 모델이 툴 호출을 자율적으로 해나가며, 의도했던 결과를 내는게 인상적이었다. 생각보다 툴 정의가 잘 되어있다면, 이런 플로우는 모델에게 자율적으로 맡겨도 동작할 수 있다는 게 인상적이었다. 실무에서 에이전트 기획할 떄도 모든 시나리오에서 에이전트의 실행순서를 강제하기 보다, 어느정도 자율적으로 동작할 수 있는 시나리오를 찾는것도 오히려 모델의 능력을 덜 제한하는 길이 아닐까 라는 생각이 들었다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 피날레 - 프레임워크와 Gradio 이용 UI, 그리고 실제로 찾아낸 딜&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에이전트 여섯 개와 플래너 두 종류까지 다 만들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;남은 건 이걸 계속 돌아가는 하나의 프로그램으로 묶고 화면을 붙이는 일이다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;기억을 파일에 남긴다&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;deal_agent_framework.py의 DealAgentFramework가 전체를 감싸는 껍데기다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;class DealAgentFramework:
    DB = &quot;products_vectorstore&quot;
    MEMORY_FILENAME = &quot;memory.json&quot;

    def __init__(self):
        init_logging()
        client = chromadb.PersistentClient(path=self.DB)
        self.memory = self.read_memory()
        self.collection = client.get_or_create_collection(&quot;products&quot;)
        self.planner = None
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Chroma 벡터스토어를 열고, memory.json을 읽어들인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3장에서 ScannerAgent가 중복 제거에 쓰던 그 memory가 여기서 파일로 관리된다.&lt;/p&gt;
&lt;pre class=&quot;ruby&quot;&gt;&lt;code&gt;def run(self) -&amp;gt; List[Opportunity]:
    self.init_agents_as_needed()
    logging.info(&quot;Kicking off Planning Agent&quot;)
    result = self.planner.plan(memory=self.memory)
    logging.info(f&quot;Planning Agent has completed and returned: {result}&quot;)
    if result:
        self.memory.append(result)
        self.write_memory()      # 파일에 저장
    return self.memory
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 사이클이 끝나고 기회를 찾았으면 memory에 추가하고 파일에 쓴다. 프로그램을 껐다 켜도 이전에 뭘 찾았는지가 남는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;=&amp;gt; 6장에서 본 다섯 번째 특징, &quot;autonomy &amp;amp; memory - existing beyond a chat with a human&quot;이 이 JSON 파일 하나로 구현된다. 거창한 게 아니라 그냥 파일이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;init_agents_as_needed()도 눈에 띈다. 플래너를 미리 만들지 않고 처음 필요할 때 만든다. 앙상블 에이전트를 만드는 순간 신경망 가중치를 읽고 Chroma를 붙잡아야 하니 무겁기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 하나 짚어둘 게 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프레임워크가 임포트하는 플래너는 이것이다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;from agents.planning_agent import PlanningAgent
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;6장에서 만든 AutonomousPlanningAgent가 아니라 5장의 규칙 기반 PlanningAgent다. 실제 실행 로그를 봐도 초록색 라벨이 [Planning Agent]로 찍히고, 알림도 &quot;using Claude to craft the message&quot; 없이 바로 발송된다. alert() 쪽이라는 뜻이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 자율 플래너는 Day 4에서 원리를 확인하는 용도로 만들어졌고, 최종 앱은 예측 가능한 규칙 기반 플래너로 돌아간다. 갈아 끼우려면 이 import 한 줄만 바꾸면 되긴 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5장 끝에서 &lt;b&gt;&quot;예측 가능성이 중요한 기능이냐 유연성이 중요한 기능이냐&quot;&lt;/b&gt;라고 썼는데, 5분마다 자동으로 도는 프로그램에서는 강사도 예측 가능한 쪽을 골랐다는 게 재미있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;터미널 색깔을 화면 색깔로 바꾼다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3장에서 각 에이전트에 색을 지정했던 게 여기서 쓰인다. 로깅에 들어 있는 건 터미널용 ANSI 색 코드라서 브라우저에서는 깨진다. 그래서 log_utils.py가 매핑을 들고 있다.&lt;/p&gt;
&lt;pre class=&quot;sas&quot;&gt;&lt;code&gt;mapper = {
    BG_BLACK+RED: &quot;#dd0000&quot;,
    BG_BLACK+GREEN: &quot;#00dd00&quot;,
    BG_BLACK+YELLOW: &quot;#dddd00&quot;,
    BG_BLACK+BLUE: &quot;#0000ee&quot;,
    BG_BLACK+MAGENTA: &quot;#aa00dd&quot;,
    BG_BLACK+CYAN: &quot;#00dddd&quot;,
    BG_BLACK+WHITE: &quot;#87CEEB&quot;,
    BG_BLUE+WHITE: &quot;#ff7800&quot;,
}

def reformat(message):
    for key, value in mapper.items():
        message = message.replace(key, f'&amp;lt;span style=&quot;color: {value}&quot;&amp;gt;')
    message = message.replace(RESET, '&amp;lt;/span&amp;gt;')
    return message
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ANSI 코드를 &amp;lt;span style=&quot;color: ...&quot;&amp;gt;로 치환하는 것뿐이다. 단순한데 화면에서 보면 효과가 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그를 실시간으로 흘려보낸다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;price_is_right.py에서 제일 재미있었던 부분이다. 에이전트가 도는 데 2분 넘게 걸리는데, 그동안 화면이 멈춰 있으면 곤란하다.&lt;/p&gt;
&lt;pre class=&quot;ruby&quot;&gt;&lt;code&gt;class QueueHandler(logging.Handler):
    def __init__(self, log_queue):
        super().__init__()
        self.log_queue = log_queue

    def emit(self, record):
        self.log_queue.put(self.format(record))
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로깅 핸들러를 하나 만들어서 로그를 큐에 넣는다. 그리고 에이전트는 별도 스레드에서 돌린다.&lt;/p&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;def worker():
    result = do_run()
    result_queue.put(result)

thread = threading.Thread(target=worker)
thread.start()
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메인 쪽은 큐를 계속 확인하면서 새 로그가 있으면 화면에 뿌리는 제너레이터다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;while True:
    try:
        message = log_queue.get_nowait()
        log_data.append(reformat(message))
        yield log_data, html_for(log_data), final_result or initial_result
    except queue.Empty:
        try:
            final_result = result_queue.get_nowait()
            ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;=&amp;gt; 로깅을 UI 스트리밍 채널로 쓴다는 발상이 좋았다. 에이전트 코드는 그냥 self.log(...)를 부를 뿐인데, 그게 터미널에도 찍히고 브라우저 패널에도 실시간으로 흐른다. 화면을 위해 에이전트 코드를 고칠 필요가 없다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;5분마다 알아서 돈다&lt;/h4&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;timer = gr.Timer(value=300, active=True)
timer.tick(run_with_logging, inputs=[log_data], outputs=[log_data, logs, opportunities_dataframe])
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;gr.Timer에 300초를 주면 5분마다 전체 사이클이 다시 돈다. 사람이 버튼을 누를 필요가 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;딜 목록의 행을 클릭하면 그 딜의 알림을 다시 보내는 기능도 있다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;def do_select(selected_index: gr.SelectData):
    opportunities = self.get_agent_framework().memory
    row = selected_index.index[0]
    opportunity = opportunities[row]
    self.get_agent_framework().planner.messenger.alert(opportunity)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오른쪽에는 벡터 시각화가 들어간다. 글 17에서 만든 80만 개 임베딩 중 800개를 뽑아 t-SNE로 3차원으로 줄이고, 카테고리별로 색을 입힌 3D 산점도다.&lt;/p&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;@classmethod
def get_plot_data(cls, max_datapoints=2000):
    result = collection.get(include=[&quot;embeddings&quot;, &quot;documents&quot;, &quot;metadatas&quot;], limit=max_datapoints)
    vectors = np.array(result[&quot;embeddings&quot;])
    categories = [metadata[&quot;category&quot;] for metadata in result[&quot;metadatas&quot;]]
    colors = [COLORS[CATEGORIES.index(c)] for c in categories]
    tsne = TSNE(n_components=3, random_state=42, n_jobs=-1)
    reduced_vectors = tsne.fit_transform(vectors)
    return documents, reduced_vectors, colors
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기능적으로 꼭 필요하진 않은데, 이 시스템이 뒤에서 뭘 깔고 있는지 보여주는 역할을 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Day 5 노트북은 화면을 세 단계로 쌓는다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바로 완성본을 보여주지 않고 Gradio를 조금씩 늘려간다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;제목과 설명 줄만 있는 빈 화면&lt;/li&gt;
&lt;li&gt;딜 테이블(gr.Dataframe)을 붙이고 예시 데이터 한 줄을 넣어본 화면&lt;/li&gt;
&lt;li&gt;DealAgentFramework를 연결해서 실제 memory를 테이블에 뿌리고, 행 클릭 시 알림이 가게 한 화면&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 마지막 셀에서 완성본을 실행한다.&lt;/p&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;!uv run price_is_right.py
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행하면 이런 화면이 뜬다&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;865&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjCcxc/dJMcaft2TIK/Mx67vdAq9gAEkWY8PiAf30/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjCcxc/dJMcaft2TIK/Mx67vdAq9gAEkWY8PiAf30/img.png&quot; data-alt=&quot;위쪽은 지금까지 발굴한 딜 테이블, 왼쪽 아래는 에이전트별로 색이 입혀진 실시간 로그, 오른쪽 아래는 상품 임베딩 3D 시각화&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjCcxc/dJMcaft2TIK/Mx67vdAq9gAEkWY8PiAf30/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbjCcxc%2FdJMcaft2TIK%2FMx67vdAq9gAEkWY8PiAf30%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;2048&quot; height=&quot;865&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;865&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;위쪽은 지금까지 발굴한 딜 테이블, 왼쪽 아래는 에이전트별로 색이 입혀진 실시간 로그, 오른쪽 아래는 상품 임베딩 3D 시각화&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왼쪽 아래 로그 패널을 보면 3장에서 지정한 색이 그대로 살아 있다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;그리고 실제로 찾아낸 딜&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테이블에 세 건이 올라와 있는데, 위의 두 건(삼성 갤럭시 워치 울트라, 아이폰 14 프로 맥스)은 reset_memory()가 남겨두는 기본값이다. 세 번째가 내가 돌려서 실제로 찾은 딜이다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Roborock Qrevo (8,000Pa 로봇청소기 겸 물걸레)
딜가       $400.00
추정가     $769.86
할인폭     $369.86
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;memory.json에도 그대로 저장돼 있다.&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
  &quot;deal&quot;: {
    &quot;product_description&quot;: &quot;Roborock Qrevo is a self-emptying robot vacuum and mop with 8,000Pa HyperForce suction ...&quot;,
    &quot;price&quot;: 400.0,
    &quot;url&quot;: &quot;https://www.dealnews.com/Roborock-Qrevo-8-000-Pa-Robot-Vacuum-and-Mop-for-400-free-shipping/21905563.html?iref=rss-f1912&quot;
  },
  &quot;estimate&quot;: 769.8555787963868,
  &quot;discount&quot;: 369.8555787963868
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 하나 짚고 갈 게 있다. 6장의 자율 플래너 실행에서도 같은 로보락 Qrevo가 걸렸는데, 그때 추정가는 $564.69였고 여기서는 $769.86이다. 딜가는 똑같이 $400인데 추정치가 $200 넘게 차이 난다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 번은 다른 실행이다. 전처리 단계에서 llama3.2가 문장을 매번 조금씩 다르게 다시 쓰고, 그 문장으로 RAG 검색과 gpt-5.1 호출이 다시 이뤄지니 결과가 달라진다. 글 17에서 확인한 앙상블의 평균 오차가 $30.70이었던 걸 생각하면, 같은 상품에 $200 차이는 결코 작은 편차가 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;=&amp;gt; 화면에 숫자가 딱 떠 있으면 확정된 값처럼 보이지만, 실제로는 실행할 때마다 흔들리는 값이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;느낀점&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;로깅을 UI 스트리밍 채널로 재활용한 구조가 인상적이었다. 에이전트는 자기 일만 하고 로그만 남기는데, 그게 관측 수단이자 사용자 화면이 된다. 에이전트 시스템에서 &quot;지금 뭐 하고 있는지 보여주기&quot;는 생각보다 중요한 UX인데, 별도 진행률 API 같은 걸 만들지 않고 이미 있는 로그로 해결했다.&lt;/li&gt;
&lt;li&gt;자율 플래너를 만들어놓고 최종 앱에는 규칙 기반을 쓴 선택이 오히려 현실적으로 느껴졌다. 뭐가 더 좋은지는 돌릴려는 시나리오 상황에 따라 다른 것. 예측 가능성이 중요한 기능이냐 유연성이 중요한 기능이냐 등.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마무리 &amp;mdash; 에이전틱 AI, 그리고 8주의 회고&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Day 5 노트의 마지막에 강사는 처음 이야기로 돌아간다. 에이전트의 정의 세 가지다.&lt;/p&gt;
&lt;pre class=&quot;livecodeserver&quot;&gt;&lt;code&gt;ai systems that can do work for you independently
ai systems where an llm controls the workflow
an agent runs tools in a loop to achieve a goal
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; 독립적으로 당신을 대신해 일을 하는 AI 시스템&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; LLM이 워크플로를 통제하는 AI 시스템&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; 목표를 달성하기 위해 툴을 루프로 돌리는 것&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;글 17 1장에서 이 세 문장을 처음 봤을 때는 그냥 정의 나열로 읽혔는데, 시스템을 다 만들고 나서 다시 보니 각각이 코드의 어느 부분인지가 보인다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;독립적으로 일한다 -&amp;gt; gr.Timer(value=300)로 5분마다 스스로 돌고, memory.json으로 이전 실행을 기억한다&lt;/li&gt;
&lt;li&gt;LLM이 워크플로를 통제한다 -&amp;gt; 6장의 자율 플래너. 스캔할지 평가할지 알릴지를 gpt-5.1이 정한다&lt;/li&gt;
&lt;li&gt;툴을 루프로 돌린다 -&amp;gt; finish_reason == &quot;tool_calls&quot;를 보는 while 루프 하나&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;거창한 에이전트 프레임워크는 하나도 안 썼다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CrewAI도 LangGraph도 없이, pydantic 클래스 몇 개와 while 루프 하나와 JSON 파일 하나로 위 세 가지가 다 성립했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;완성된 시스템을 다시 보면&lt;/p&gt;
&lt;p 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;dealnews RSS 3개에서 딜 30건을 긁는다 (1장)&lt;/li&gt;
&lt;li&gt;gpt-5-mini가 Structured Outputs로 5건을 골라 상품 설명과 가격으로 정리한다 (2&amp;middot;3장)&lt;/li&gt;
&lt;li&gt;각 딜마다 llama3.2가 문장을 전처리하고, Modal의 파인튜닝 Llama와 RAG를 태운 gpt-5.1과 신경망이 각자 가격을 추정해 앙상블로 통합된 가격 추정값을 전한다 (글 17)&lt;/li&gt;
&lt;li&gt;추정가에서 딜가를 뺀 값이 가장 큰 딜을 고르고, 기준을 넘으면 알린다 (5장)&lt;/li&gt;
&lt;li&gt;claude-sonnet-4-5가 알림 문구를 쓰고 Pushover로 발송한다 (4장)&lt;/li&gt;
&lt;li&gt;결과를 memory.json에 남긴다. 그 상품 URL은 다음 사이클에서 제외된다 (7장)&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모델 호출 서른 번 남짓, 다섯 회사의 모델이 각자 자기 자리에서 한 번씩 불려 나온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 실제로 로보락 청소기 하나를 찾아냈다. 딜가 $400에 추정가 $769.86.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지가 Week 8이다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;8주를 돌아보며&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글이 이 강의 정리의 18번째, 마지막 글이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에 웹사이트 하나 요약하는 코드로 시작해서 자율 에이전트 시스템까지 왔으니 꽤 먼 길이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주차별로 무엇을 했고 그때 뭘 느꼈는지 되짚어본다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Week 1&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GPT API와 Ollama를 활용한 로컬모델로 웹사이트를 요약했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;별거 아닌 기능이었는데, 그때 남긴 말이 &quot;이전까지는 막연하게 프론티어 모델과 오픈소스 모델 성능 차이가 난다고 머리로만 알고 있었다면, 처음 오픈소스 모델도 직접 써보면서 어떤 부분에서 차이가 있는지를 느꼈다&quot;였다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Week 2&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Gradio로 챗봇을 만들고 tool calling과 멀티모달을 붙였다. 회사에서 말로만 듣던 tool calling을 처음 코드로 만져본 주차다. 그때 익힌 것이 이번 6장의 자율 에이전트로 그대로 이어졌다. 그때는 함수 하나를 부르는 수준이었는데, 이번엔 그 원리로 에이전트 전체가 돌아갔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Week 3&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Hugging Face, 토크나이저와 chat_template, 양자화, 그리고 print(model)로 트랜스포머 구조를 뜯어봤다. 모델 내부를 처음 들여다본 주차이고, 여기서 익힌 양자화 개념이 Week 7의 QLoRA로 연결됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Week 4&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LLM 평가였다. 벤치마크와 리더보드를 정리하면서 남긴 결론이 이거였다. &quot;벤치마크 성능이 더 좋은 모델이라 하더라도, 실제 특정 task에서 잘 먹히냐는 결국 돌려봐야 안다.&quot; 그리고 모델 지표는 최소 기준을 통과시키는 용도로 쓰고 그 뒤는 비즈니스 지표로 판단해야 한다는 이야기. 이후 주차에서 실제 서비스들을 만들어보며 이 문장이 더 생각났던 거 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Week 5&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RAG였다. 구현하고, RAG 평가 시스템을 만들고, Advanced RAG까지 갔다. &quot;단순하게 RAG를 구축하는 것 자체는 쉽다. 하지만 실제 돌려봤을 때 세부 케이스들에서 잘 안 되는 걸 보니&quot; 기업들이 왜 PoC와 BMT를 요구하는지 이해됐다고 썼다. 그리고 완전 자동화는 어렵고 결국 사람 손을 탄다는 것도.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Week 6&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 큐레이션과 프론티어 모델 파인튜닝이었다. &quot;데이터를 모은다가 아니라 분포를 설계한다&quot;가 큐레이션의 본질이었고, 결과는 예상 밖이었다. 학습을 전혀 안 시킨 GPT가 자체 학습시킨 모델을 이겼고, 파인튜닝은 오히려 성능을 떨어뜨렸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Week 7&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;QLoRA로 3B짜리 Llama 3.2를 파인튜닝했다. 여기서 Week 6의 실패가 설명됐다. 파인튜닝은 만능 성능 향상기가 아니라 작은 전문가를 만드는 도구라는 것. 그리고 &quot;학습과 모델 개발은 계산이 아니라 관찰과 판단&quot;이라는 게 eval loss 곡선을 들여다보며 남은 문장이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Week 8&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞의 7주에서 만든 조각을 전부 꺼내 하나로 조립했다. Week 7의 파인튜닝 모델은 Modal에 올려 SpecialistAgent가 됐고, Week 5의 RAG는 FrontierAgent가 됐고, Week 6의 신경망은 NeuralNetworkAgent가 됐다. Week 2의 tool calling은 자율 플래너가 됐고, Week 3의 양자화 지식은 그 모델들을 GPU에 올리는 기반이 됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;전체 느낀점&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 8주의 코스를 하기 전에 나는 여기저기서 각종 용어들은 들어봤지만, 그게 정말로 코드단에서 어떻게 동작하냐에 대한 감은 없는 상황이었다. 실제 RAG, Tool calling, agent, fine tuning, gradio, ollama 등등 실제 코드로 작게나마 LLM 엔지니어링 일련의 과정을 돌려보며 AI 서비스가 만들어지는데 있어 뒤에서 돌아가는 작업들에 대한 이해도를 많이 키울 수 있었다. 이 부분이 현재 회사에서 에이전트를 기획해나가는 데 있어서도 많은 이해도를 높여줬다고 생각한다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;8주 내내 코드를 직접 돌려본 게 컸다. 글 1에서 썼던 &quot;머리로만 알고 있던 것과 직접 해보는 것은 다르다&quot;가 마지막 글까지 그대로 유효했다. 읽기만 했으면 남지 않았을 것들이 손으로 돌려보니 남았다.&lt;/li&gt;
&lt;li&gt;또한, 이렇게 블로그로 남기는 과정도 나에게 큰 의미가 있었던 거 같다. 단순히 한번 강의를 듣고 실행하는 것만으로는 내 것이 되는 느낌이 안들었는데 블로그를 작성하면서 좀 더 지식이 체화가 되는 느낌이었다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;그리고 이렇게 직접 코드 단에서 작업을 해나가면서 AI, 엔지니어링에 더 많은 관심을 가지게 되는 계기가 된 거 같다. 이 부분은 현재 회사에서 일하면서도 많은 긍정적인 영향을 주고 있는 부분이라고 생각한다. (실제로 회사에서도 이전보다 엔지니어 분들과 기술적인 소통도 많이 하게 된 거 같고, 실제 간단한 시스템이나 PoC 는 직접 만들기도 하게 됐다)&amp;nbsp;&lt;/li&gt;
&lt;li&gt;그렇기 때문에 앞으로도 이런 기술에 대한 공부, AI에 대한 공부는 계속 이어나가야 겠다는 생각이다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지가 Ed Donner의 LLM Engineering 8주 과정 정리다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 Ed Donner의 LLM Engineering 8주 과정을 18편의 글로 정리했었습니다.&amp;nbsp;&lt;/p&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;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;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;p data-ke-size=&quot;size16&quot;&gt;그래서 다음글 부터는 AI 공부글과, 제 커리어/실무 관련된 블로그 글을 좀 섞어서 올리게 될 거 같습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아주 확정은 아니지만, 올해 초 회사를 옮긴 뒤 작년에 다녔던 회사에서 배웠던 것들을 한번 정리해서 올려야지 하고 있었는데&lt;/p&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;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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;Profile:&lt;/span&gt;&lt;br /&gt;&lt;a style=&quot;color: #0070d1; text-align: start;&quot; href=&quot;http://www.linkedin.com/in/doobeom-kim-%EA%B9%80%EB%91%90%EB%B2%94-2b9649242&quot;&gt;&lt;span&gt;&lt;span style=&quot;color: #0070d1;&quot;&gt;Linkedin&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;</description>
      <category>AI 공부</category>
      <category>Agent</category>
      <category>ai</category>
      <category>ai 에이전트</category>
      <category>claude</category>
      <category>GPT</category>
      <category>llm</category>
      <category>Rag</category>
      <category>생성형 AI</category>
      <category>에이전트</category>
      <category>오픈소스 모델</category>
      <author>kdb1248</author>
      <guid isPermaLink="true">https://doobeom-coding.tistory.com/95</guid>
      <comments>https://doobeom-coding.tistory.com/95#entry95comment</comments>
      <pubDate>Sat, 8 Aug 2026 17:13:50 +0900</pubDate>
    </item>
    <item>
      <title>17. 상품 핫딜 헌팅 에이전트 (1) - 파인튜닝 모델 서비스화 및 가격 예측 에이전트 개발</title>
      <link>https://doobeom-coding.tistory.com/94</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;목차&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;0. 들어가며&lt;br /&gt;1.&amp;nbsp;7개&amp;nbsp;에이전트(agent)의&amp;nbsp;협업&amp;nbsp;-&amp;nbsp;Week&amp;nbsp;8에서&amp;nbsp;만들려는&amp;nbsp;에이전트&amp;nbsp;시스템&lt;br /&gt;2.&amp;nbsp;파인튜닝&amp;nbsp;모델을&amp;nbsp;클라우드&amp;nbsp;GPU에서&amp;nbsp;실행&amp;nbsp;-&amp;nbsp;Modal&amp;nbsp;이용&lt;br /&gt;3.&amp;nbsp;잠깐&amp;nbsp;떴다&amp;nbsp;사라지는&amp;nbsp;앱에서&amp;nbsp;배포된&amp;nbsp;서비스로&amp;nbsp;-&amp;nbsp;파인튜닝&amp;nbsp;모델&amp;nbsp;서비스화&lt;br /&gt;4.&amp;nbsp;첫&amp;nbsp;번째&amp;nbsp;에이전트&amp;nbsp;-&amp;nbsp;SpecialistAgent,&amp;nbsp;그리고&amp;nbsp;전처리가&amp;nbsp;필요한&amp;nbsp;이유&lt;br /&gt;5.&amp;nbsp;학습이&amp;nbsp;아니라&amp;nbsp;추론(inference)에서&amp;nbsp;승부&amp;nbsp;-&amp;nbsp;상품&amp;nbsp;80만&amp;nbsp;개를&amp;nbsp;벡터&amp;nbsp;DB로&lt;br /&gt;6.&amp;nbsp;두&amp;nbsp;번째&amp;nbsp;에이전트&amp;nbsp;-&amp;nbsp;GPT-5.1에게&amp;nbsp;RAG를&amp;nbsp;통해&amp;nbsp;비슷한&amp;nbsp;상품&amp;nbsp;5개를&amp;nbsp;쥐여주기&lt;br /&gt;7.&amp;nbsp;세&amp;nbsp;번째&amp;nbsp;에이전트와&amp;nbsp;앙상블&amp;nbsp;에이전트&amp;nbsp;-&amp;nbsp;셋을&amp;nbsp;섞으면&amp;nbsp;정말&amp;nbsp;더&amp;nbsp;좋아질까&lt;br /&gt;마무리&amp;nbsp;-&amp;nbsp;성적표가&amp;nbsp;또&amp;nbsp;뒤집혔다&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;0. 들어가며&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지난 글(&lt;a href=&quot;https://doobeom-coding.tistory.com/93&quot;&gt;글 16&lt;/a&gt;)에서 나온 가격 예측 모델 성능 비교는 꽤 놀라웠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3B(30억) 파라미터짜리 오픈소스 모델인 Llama 3.2를 QLoRA로 파인튜닝했더니&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오차 $39.85로 제로샷 프론티어 모델을 전부 제치고 1등을 했으니까.&lt;/p&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;p data-ke-size=&quot;size16&quot;&gt;마지막 주차인 Week8은 해당 모델을 활용해서, 풀고 싶은 문제를 푸는 자율화된 에이전트 워크플로우를 만든다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Week8에서 하는 일을 간단히 요약하면 다음과 같다.&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1) 인터넷에서 할인 상품 정보를 스스로 긁어와&lt;br /&gt;2) 그 상품의 진짜 가치가 얼마인지 추정하고 &lt;br /&gt;3) 파는 가격이 충분히 싸면 내 휴대폰으로 알림을 보내는 Agent 시스템을 만든다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 &quot;스스로&quot;라는 말과, &lt;b&gt;에이전트(Agent)&lt;/b&gt; 라는 말이 핵심이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강사는 에이전트에 대해 3가지 정의를 안내해줬다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;: AI systems that can do wokr for you independently&lt;br /&gt;-&amp;gt; 사용자를 대신해 독립적으로 업무를 수행할 수 있는 AI 시스템 (원래 &quot;오퍼레이터&quot;라 부르던 것을 에이전트라 부르기 시작)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Anthropic / HuggingFace&lt;/b&gt;: AI systems where an llm controls the workflow &lt;br /&gt;-&amp;gt; LLM이 워크플로를 통제하는 AI 시스템&lt;/li&gt;
&lt;li&gt;&lt;b&gt;2025년 후반의 통용 정의&lt;/b&gt;: An agent runs tools in a loop to acheive a goal &lt;br /&gt;-&amp;gt; 목표를 달성하기 위해 도구(tool)를 루프로 돌리는 것&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3가지 정의가 조금씩 다르지만 공통점은 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사람이 매번 다음 단계를 지시하지 않아도 된다는 것.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 주에 만들 시스템은 이 3가지 정의를 전부 만족하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Week 8의 블로그 글은&amp;nbsp;Week 7과 마찬가지로 두 편으로 나눠 쓰려 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 이번 글(Day 1~2)은 &lt;b&gt;&quot;가격을 맞히는 쪽&quot;&lt;/b&gt; 이다. 상품 설명을 던지면 가격을 추정해주는 전문가 Agent 3종을 만들고, 그 3개를 하나로 묶어 가격 예측하는 시스템을 만든다.&lt;br /&gt;- 다음 글(Day 3~5)은&amp;nbsp;&lt;b&gt;&quot;가격 예측을 제외한 나머지 영역&quot;. &lt;/b&gt;상품딜을 찾아오고, 판단하고, 알림을 보내는 부분이다.&lt;/p&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;p data-ke-size=&quot;size16&quot;&gt;참고로 이번 주부터는 Colab을 떠나 내 맥북에서 직접 돌렸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GPU가 필요한 부분(만든 파인튜닝 모델을 돌리는 것)은 Modal이라는 서비스에 떼어 맡기는 구조라 가능했는데,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 이야기를 2장에서 먼저 하려고 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bWULyl/dJMcaf8vJ8u/FzMGK7V6iOTw79cyWkCX1K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bWULyl/dJMcaf8vJ8u/FzMGK7V6iOTw79cyWkCX1K/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;2402&quot; data-origin-height=&quot;642&quot; data-filename=&quot;스크린샷 2026-08-01 오후 3.40.33.png&quot; style=&quot;width: 60.5318%; margin-right: 10px;&quot; data-widthpercent=&quot;61.24&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bWULyl/dJMcaf8vJ8u/FzMGK7V6iOTw79cyWkCX1K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbWULyl%2FdJMcaf8vJ8u%2FFzMGK7V6iOTw79cyWkCX1K%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;2402&quot; height=&quot;642&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bKmk8E/dJMcagflIZv/v5UyXJoqBEKI6elKIuZhuK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bKmk8E/dJMcagflIZv/v5UyXJoqBEKI6elKIuZhuK/img.png&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;865&quot; data-is-animation=&quot;false&quot; style=&quot;width: 38.3054%;&quot; data-widthpercent=&quot;38.76&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bKmk8E/dJMcagflIZv/v5UyXJoqBEKI6elKIuZhuK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbKmk8E%2FdJMcagflIZv%2Fv5UyXJoqBEKI6elKIuZhuK%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;2048&quot; height=&quot;865&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;최종적으로 완성되게 될 deal 찾기 에이전트(좌측이 실제 딜 알림이 온것, 우측이 이 알림을 주기 위해 뒷단에서 돌아가는 에이전트). 이번글에서는 이 에이전트 시스템 중 가격 추정하는 부분을 만든다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 7개 에이전트(agent)의 협업 -&amp;nbsp; Week 8에서 만들려는 에이전트 시스템&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;에이전트 시스템&quot;이라고 하면 뭔가 거창해 보이는데, 이번 주에 만들 것을 뜯어보면 결국 역할이 다른 파이썬 클래스 7개다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;계층으로 그리면 이렇다.&lt;/p&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;UI (Gradio)
 └─ 에이전트 프레임워크  ... 메모리(어떤 딜을 이미 봤는지)와 로깅 담당
     └─ 플래닝 에이전트   ... 전체 흐름 조율
         ├─ 스캐너 에이전트   ... RSS에서 딜 긁어와 유망한 딜 선별
         ├─ 앙상블 에이전트   ... 가격 추정 (내부에 3개를 더 갖고 있음)
         │    ├─ 스페셜리스트 에이전트  ... 파인튜닝한 Llama 3.2 (Modal에서 원격 실행)
         │    ├─ 프론티어 에이전트      ... RAG + GPT-5.1
         │    └─ 뉴럴 네트워크 에이전트  ... Week 6에서 만든 딥러닝 모델
         └─ 메시징 에이전트   ... 휴대폰으로 푸시 알림 (나는 웹 푸시로 대체)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 이번 글이 다루는 범위는 가운데에 있는 앙상블 에이전트와 그 안의 3개 하위 가격 예측 에이전트다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 7개 에이전트를 만드는 데 시중의 에이전트 프레임워크는 하나도 쓰지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CrewAI, OpenAI Agents SDK, Google ADK, LangGraph 같은 에이전트 프레임워크가 시중에 나와있지만, 강의에서는 전부 순수한 파이썬 클래스와 LLM 호출만으로 만든다.&amp;nbsp;&lt;/p&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;p data-ke-size=&quot;size16&quot;&gt;문제 풀려고 할때 너무 빨리 에이전트 아키텍처로 넘길려는 사람이 있는데,&amp;nbsp; 비즈니스 목표에 집중, 프롬프트 테스트, 평가 하는 게 더 중요하다고 강사는 말한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 에이전트 아키텍처에 뛰어드는 게 아닌 비즈니스 문제에 집중해야하며&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LLM 호출도, 에이전트 개수도 단순하게 시작해서 그 뒤에 정말 필요하다고 느낄때 넓히는게 중요하다고 말한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;모든 에이전트의 부모 클래스&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;7개가 공유하는 것은 딱 하나, Agent라는 추상 클래스다. 내용도 짧다.&lt;/p&gt;
&lt;pre class=&quot;ruby&quot;&gt;&lt;code&gt;class Agent:
    # 에이전트별 로그 색상
    RED = '\033[31m'
    GREEN = '\033[32m'
    YELLOW = '\033[33m'
    BLUE = '\033[34m'
    MAGENTA = '\033[35m'
    CYAN = '\033[36m'
    WHITE = '\033[37m'
    BG_BLACK = '\033[40m'
    RESET = '\033[0m'

    name: str = &quot;&quot;
    color: str = '\033[37m'

    def log(self, message):
        # [에이전트 이름] 메시지 형태로, 자기 색깔을 입혀서 로그를 남긴다
        color_code = self.BG_BLACK + self.color
        message = f&quot;[{self.name}] {message}&quot;
        logging.info(color_code + message + self.RESET)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하는 일은 로그 찍기가 전부다. 그런데 왜 굳이 부모 클래스까지 만들어서 색을 입힐까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;=&amp;gt; 실제로 돌려보면 이유가 바로 납득된다. 가격 하나를 추정하는 동안 에이전트 4~5개가 동시에 로그를 쏟아내기 때문에, 누가 지금 무슨 말을 하는지 색으로 구분되지 않으면 읽을 수가 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 이 색깔은 나중에 Day 5에서 그대로 재활용된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;터미널 색상 코드를 HTML &amp;lt;span style=&quot;color: ...&quot;&amp;gt; 로 바꿔주는 매퍼가 있어서, Gradio 화면의 로그 패널에 똑같은 색으로 흘러나온다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1404&quot; data-origin-height=&quot;642&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/XRzUp/dJMcai5eFYr/ufR1qNAJRWcyy7ErRor2YK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/XRzUp/dJMcai5eFYr/ufR1qNAJRWcyy7ErRor2YK/img.png&quot; data-alt=&quot;이런식으로 각 에이전트의 로그를 gradio UI에서 볼 수 있게된다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/XRzUp/dJMcai5eFYr/ufR1qNAJRWcyy7ErRor2YK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FXRzUp%2FdJMcai5eFYr%2FufR1qNAJRWcyy7ErRor2YK%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;1404&quot; height=&quot;642&quot; data-origin-width=&quot;1404&quot; data-origin-height=&quot;642&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;이런식으로 각 에이전트의 로그를 gradio UI에서 볼 수 있게된다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&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;h3 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;강사가 이 장에서 한 경고가 인상 깊었다. &quot;문제를 풀 때 너무 빨리 에이전트 아키텍처로 넘어가려는 사람이 많다&quot;는 것. 중요한 건 비즈니스 목표에 집중하고, 프롬프트를 테스트하고, 평가하는 쪽이지 아키텍처가 아니라고 했다. LLM 1개로 시작해서, 정말 필요하다고 느껴질 때 넓혀가라는 조언.&lt;/li&gt;
&lt;li&gt;특히 처음 실무에서 에이전트를 만들어갈 떄도 이런 부분이 생기는 거 같다. 에이전트 아키텍처적인 고민을 먼저 하고(ex. 이걸 어떤 서브에이전트로 나눌까, 어떻게 스킬화/tool을 나눠서 이용할까 등) 근데 이렇게 가다보면, 결국에 아키텍처 구조에 문제를 끼워맞추게 되는 상황이 생긴다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;이번 Week 8의 시스템도 처음부터 7개의 에이전트로 강사가 나눴던 건 아니라고 한다. &quot;가격을 잘 추정한다&quot;는 하나의 목표가 먼저 있었고, 그걸 잘하기 위해 역할을 쪼갠 결과가 에이전트 7개였다. 결국에 중요한 건 비즈니스 문제를 푸는 것이라는 점이 인상적이었다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 파인튜닝 모델을 클라우드 GPU에서 실행 - Modal 이용&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Week 7에서 만든 파인튜닝 모델은 GPU가 있어야 돌아간다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 내 맥북에는 해당 파인튜닝 모델을 지속적으로 돌리기엔 GPU가 빡셀 뿐더러 계속 맥북을 켜놓을 순 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 그렇다고 Google Colab 노트북을 24시간 켜둘 수도 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;딜을 발견했을 때만 잠깐 가격을 물어보면 되는 시스템인데, GPU 서버를 계속 띄워두는 것도 낭비다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;=&amp;gt; 필요할 때만 몇 초 빌려 쓰는 GPU가 필요하다. 그게 이번에 쓰는 Modal이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Modal은 서버리스 컴퓨팅 플랫폼이다. 파이썬 함수 위에 데코레이터 한 줄만 붙이면 그 함수가 클라우드에서 실행된다. 서버를 띄우거나 Docker 파일을 쓰거나 배포 파이프라인을 만들 필요가 없고, 실행에 쓴 시간만큼만 과금된다. 가입하면 매달 30달러어치 크레딧이 무료로 주어져서 이번 주 실습은 이걸로 충분했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;640&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oG4ad/dJMcacqyPzm/0Jq69CaOzqsHx5COcY6ETK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oG4ad/dJMcacqyPzm/0Jq69CaOzqsHx5COcY6ETK/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oG4ad/dJMcacqyPzm/0Jq69CaOzqsHx5COcY6ETK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoG4ad%2FdJMcacqyPzm%2F0Jq69CaOzqsHx5COcY6ETK%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;610&quot; height=&quot;305&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;640&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시작은 토큰 등록이다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;uv run modal token set --token-id ak-... --token-secret as-...
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;혹은 .env에 MODAL_TOKEN_ID, MODAL_TOKEN_SECRET 두 줄을 직접 넣어도 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인프라를 코드로 정의한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 단순한 예제인 hello.py를 보면 Modal의 사고방식이 그대로 드러난다.&lt;/p&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;import modal
from modal import Image

app = modal.App(&quot;hello&quot;)

# 실행 환경(컨테이너 이미지)도 파이썬 코드로 정의한다
# 데비안 슬림 이미지에 requests 하나만 설치
image = Image.debian_slim().pip_install(&quot;requests&quot;)


@app.function(image=image)
def hello() -&amp;gt; str:
    import requests
    # 지금 이 코드가 어느 지역에서 돌고 있는지 확인
    response = requests.get(&quot;https://ipinfo.io/json&quot;)
    data = response.json()
    city, region, country = data[&quot;city&quot;], data[&quot;region&quot;], data[&quot;country&quot;]
    return f&quot;Hello from {city}, {region}, {country}!!&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p 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;컨테이너 이미지를 Dockerfile이 아니라 파이썬 한 줄(Image.debian_slim().pip_install(&quot;requests&quot;))로 정의한다.&lt;/li&gt;
&lt;li&gt;import requests가 함수 바깥이 아니라 안쪽에 있다. 이 함수는 내 맥북이 아니라 클라우드 컨테이너에서 실행되므로, 필요한 import도 그 안에서 일어나야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;local과 remote, 한 글자 차이&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 함수를 두 가지 방식으로 부를 수 있다.&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;with app.run():
    reply = hello.local()   # 내 컴퓨터에서 실행
reply
&lt;/code&gt;&lt;/pre&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;gt; 'Hello from Seongnam-si, Gyeonggi-do, KR!!'&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;with app.run():
    reply = hello.remote()  # Modal 클라우드에서 실행
reply
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; 'Hello&amp;nbsp;from&amp;nbsp;Ashburn,&amp;nbsp;Virginia,&amp;nbsp;US!!'&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드는 똑같은데 결과가 성남에서 미국 버지니아 애쉬번으로 바뀐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;.local()과 .remote(), 딱 이 차이 하나로 실행 위치가 바뀐다는 게 Modal의 핵심 아이디어다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지역을 지정할 수도 있다. 데코레이터에 region만 추가하면 된다.&lt;/p&gt;
&lt;pre class=&quot;livescript&quot;&gt;&lt;code&gt;@app.function(image=image, region=&quot;eu&quot;)
def hello_europe() -&amp;gt; str:
    ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; 'Hello&amp;nbsp;from&amp;nbsp;Brussels,&amp;nbsp;Brussels&amp;nbsp;Capital,&amp;nbsp;BE!!'&lt;/p&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;p data-ke-size=&quot;size16&quot;&gt;여기까지는 CPU 함수였고, 이제 진짜 목적인 GPU를 붙일 차례다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;llama.py에서는 데코레이터에 gpu 한 줄이 추가된다.&lt;/p&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;app = modal.App(&quot;llama&quot;)
image = Image.debian_slim().pip_install(&quot;torch&quot;, &quot;transformers&quot;, &quot;accelerate&quot;)

# Modal 대시보드에 등록해둔 시크릿을 이름으로 가져온다
secrets = [modal.Secret.from_name(&quot;huggingface-secret&quot;)]

GPU = &quot;T4&quot;
MODEL_NAME = &quot;meta-llama/Llama-3.2-3B&quot;


@app.function(image=image, secrets=secrets, gpu=GPU, timeout=1800)
def generate(prompt: str) -&amp;gt; str:
    from transformers import AutoTokenizer, AutoModelForCausalLM, set_seed

    tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
    tokenizer.pad_token = tokenizer.eos_token
    tokenizer.padding_side = &quot;right&quot;
    model = AutoModelForCausalLM.from_pretrained(MODEL_NAME, device_map=&quot;auto&quot;)

    set_seed(42)
    inputs = tokenizer.encode(prompt, return_tensors=&quot;pt&quot;).to(&quot;cuda&quot;)
    outputs = model.generate(inputs, max_new_tokens=5)
    return tokenizer.decode(outputs[0])
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Llama 3.2는 HuggingFace에서 토큰이 있어야 내려받을 수 있는 모델이라 HF_TOKEN이 필요한데,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이걸 코드에 적지 않고 Modal 대시보드에 huggingface-secret이라는 이름으로 등록해두고 이름으로만 참조한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;=&amp;gt; 내 노트북에는 토큰이 없어도 되고, 클라우드 컨테이너 안에서만 환경변수로 풀린다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 결과는 이렇다.&lt;/p&gt;
&lt;pre class=&quot;verilog&quot;&gt;&lt;code&gt;with modal.enable_output():
    with app.run():
        result = generate.remote(&quot;Never gonna give you up, never gonna&quot;)
result
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;vbnet&quot;&gt;&lt;code&gt;'&amp;lt;|begin_of_text|&amp;gt;Never gonna give you up, never gonna let you down, never'
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;modal.enable_output()으로 감싸면 클라우드 쪽 진행 상황이 그대로 흘러나오는데, 첫 실행에서는 이런 로그가 한참 찍힌다.&lt;/p&gt;
&lt;pre class=&quot;excel&quot;&gt;&lt;code&gt;=&amp;gt; Step 0: FROM base
=&amp;gt; Step 1: RUN python -m pip install accelerate torch transformers
Collecting accelerate ...
Collecting torch ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내 컴퓨터에서 pip install을 하는 게 아니라, 클라우드에서 컨테이너 이미지를 처음 굽고 있는 것이다. 한 번 구워두면 다음부터는 이 단계가 사라진다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;느낀 점&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모델을 만드는 것을 넘어 모델을 서비스로 굴리기 위해 필요한 작업을 해본게 의미가 있었다. 이런 Modal을 이용해서, 오픈소스 모델이나 파인튜닝한 모델도 서비스화 할 수 있는 지점이 신기했다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;특히 &quot;안 쓸 때는 0원&quot;이 될 수 있는 부분이 Modal이 가지는 큰 장점이라고 느꼈다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 잠깐 떴다 사라지는 앱에서 배포된 서비스로 - 파인튜닝 모델 서비스화&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞 장의 hello와 llama는 with app.run(): 블록 안에서만 살아 있는 앱이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;블록을 빠져나오면 앱도 사라진다. Modal은 이걸 ephemeral app(임시 앱)이라고 부른다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실험할 때는 편하지만, 에이전트 시스템이 아무 때나 가격을 물어보려면 늘 그 자리에 있는 서비스여야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 장은 임시 앱 &amp;rarr; 배포된 앱으로 넘어가는 과정이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 임시 앱 버전.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pricer_ephemeral.py는 Week 7의 파인튜닝 모델을 그대로 GPU에 올려 가격을 뱉는 함수다.&lt;/p&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;GPU = &quot;T4&quot;
BASE_MODEL = &quot;meta-llama/Llama-3.2-3B&quot;
HF_USER = &quot;ed-donner&quot;
RUN_NAME = &quot;2025-11-28_18.47.07&quot;
FINETUNED_MODEL = f&quot;{HF_USER}/price-{RUN_NAME}&quot;
REVISION = &quot;b19c8bfea3b6ff62237fbb0a8da9779fc12cefbd&quot;


@app.function(image=image, secrets=secrets, gpu=GPU, timeout=1800)
def price(description: str) -&amp;gt; float:
    ...
    # Week 7과 똑같은 4bit 양자화 설정
    quant_config = BitsAndBytesConfig(
        load_in_4bit=True,
        bnb_4bit_use_double_quant=True,
        bnb_4bit_compute_dtype=torch.float16,
        bnb_4bit_quant_type=&quot;nf4&quot;,
    )

    # 베이스 모델을 올리고 그 위에 LoRA 어댑터를 붙인다
    base_model = AutoModelForCausalLM.from_pretrained(
        BASE_MODEL, quantization_config=quant_config, device_map=&quot;auto&quot;)
    fine_tuned_model = PeftModel.from_pretrained(base_model, FINETUNED_MODEL, revision=REVISION)

    # Week 7에서 학습할 때 쓴 프롬프트 형식을 그대로 재현
    prompt = f&quot;{QUESTION}\n\n{description}\n\n{PREFIX}&quot;   # ... 마지막이 &quot;Price is $&quot;
    outputs = fine_tuned_model.generate(inputs, max_new_tokens=5)
    result = tokenizer.decode(outputs[0])
    contents = result.split(&quot;Price is $&quot;)[1]              # $ 뒤 숫자만 잘라낸다
    ...
&lt;/code&gt;&lt;/pre&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;p data-ke-size=&quot;size16&quot;&gt;호출은 앞 장과 똑같다.&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;with modal.enable_output():
    with app.run():
        result = price.remote(&quot;Quadcast HyperX condenser mic, connects via usb-c to your computer for crystal clear audio&quot;)
result
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; (출력 결과) 90.0&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;배포된 앱으로 바꾸기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;터미널에서 deploy 한 번이면 임시 앱이 상주 서비스가 된다.&lt;/p&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;!uv run modal deploy -m pricer_service
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;✓ App deployed in 1.761s!  
View Deployment: https://modal.com/apps/kdb1248/main/deployed/pricer-service
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 with app.run(): 없이, 이름만 알면 어디서든 부를 수 있다.&lt;/p&gt;
&lt;pre class=&quot;php&quot;&gt;&lt;code&gt;pricer = modal.Function.from_name(&quot;pricer-service&quot;, &quot;price&quot;)
pricer.remote(text)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; 130.0&lt;/p&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;p data-ke-size=&quot;size16&quot;&gt;노트북이든 다른 스크립트든, 나중에 만들 SpecialistAgent든 전부 이 이름으로 접근한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실상 REST API를 만든 것과 같은 효과인데 엔드포인트 코드를 한 줄도 쓰지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;느린 이유와 그 해결 - @modal.enter()&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 이 방식에는 큰 낭비가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;price()가 호출될 때마다 함수 안에서 3B 모델을 새로 내려받고 4bit로 양자화하고 어댑터를 붙인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가격 하나 물어보는 데 그걸 매번 다시 한다는 뜻이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Modal 대시보드에서 실제 실행 시간을 보면 체감이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;1012&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cwIXIL/dJMcabZuJC2/J0AeS0EytnkcUIeEmRcsPK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cwIXIL/dJMcabZuJC2/J0AeS0EytnkcUIeEmRcsPK/img.png&quot; data-alt=&quot;함수 방식. 호출 한 번에 1분 22초. 대부분이 모델을 올리는데 드는 시간이다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cwIXIL/dJMcabZuJC2/J0AeS0EytnkcUIeEmRcsPK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcwIXIL%2FdJMcabZuJC2%2FJ0AeS0EytnkcUIeEmRcsPK%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;2048&quot; height=&quot;1012&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;1012&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;함수 방식. 호출 한 번에 1분 22초. 대부분이 모델을 올리는데 드는 시간이다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해결책이 pricer_service2.py다. 함수 대신 클래스로 바꾼다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@app.cls(
    image=image.env({&quot;HF_HUB_CACHE&quot;: CACHE_DIR}),
    secrets=secrets,
    gpu=GPU,
    timeout=1800,
    min_containers=MIN_CONTAINERS,
    volumes={CACHE_DIR: hf_cache_volume},   # 모델 가중치를 캐시해두는 저장소
)
class Pricer:
    @modal.enter()
    def setup(self):
        # 컨테이너가 살아날 때 딱 한 번 실행된다
        # 모델 로드/양자화/어댑터 부착을 여기서 끝내둔다
        ...
        self.fine_tuned_model = PeftModel.from_pretrained(self.base_model, FINETUNED_MODEL, revision=REVISION)

    @modal.method()
    def price(self, description: str) -&amp;gt; float:
        # 이제 이 메서드는 추론만 한다
        ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@modal.enter()가 붙은 메서드는 컨테이너가 뜰 때 한 번만 실행된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;무거운 준비는 전부 setup()에 몰아넣고, price()는 이미 메모리에 올라간 모델로 추론만 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;거기에 Volume(hf-hub-cache)을 붙여서 모델 가중치 자체도 매번 새로 받지 않게 캐시한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;호출 방식만 살짝 바뀐다. Function 대신 Cls다.&lt;/p&gt;
&lt;pre class=&quot;makefile&quot;&gt;&lt;code&gt;Pricer = modal.Cls.from_name(&quot;pricer-service&quot;, &quot;Pricer&quot;)
pricer = Pricer()
reply = pricer.price.remote(text)
print(reply)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; 130.0&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 결과인데 걸린 시간이 다르다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;1012&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zWwz6/dJMcajpvj93/dFVZDJYbt8B9NXUYEVl4Nk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zWwz6/dJMcajpvj93/dFVZDJYbt8B9NXUYEVl4Nk/img.png&quot; data-alt=&quot;클래스 방식. 컨테이너가 이미 떠 있으면 1.08초. 1분 22초에서 1초대로 줄었다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zWwz6/dJMcajpvj93/dFVZDJYbt8B9NXUYEVl4Nk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzWwz6%2FdJMcajpvj93%2FdFVZDJYbt8B9NXUYEVl4Nk%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;2048&quot; height=&quot;1012&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;1012&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;클래스 방식. 컨테이너가 이미 떠 있으면 1.08초. 1분 22초에서 1초대로 줄었다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&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;h4 data-ke-size=&quot;size20&quot;&gt;콜드 스타트를 어떻게 다룰까&lt;/h4&gt;
&lt;p 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;맨 처음 이미지를 굽는 데는 최대 10분&lt;/li&gt;
&lt;li&gt;그 뒤로는 컨테이너가 잠들어 있다가 깨어나는 데 30초 정도&lt;/li&gt;
&lt;li&gt;컨테이너가 살아 있으면 2초&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이걸 조절하는 방식이 두 개 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1) 하나는 MIN_CONTAINERS = 1로 두어 컨테이너를 항상 살려두는 것.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;- 다만 그만큼 크레딧이 계속 나간다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2) 다른 하나는 컨테이너가 잠들기까지의 대기 시간을 늘리는 것.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;pricer = modal.Cls.from_name(&quot;pricer-service&quot;, &quot;Pricer&quot;)()
pricer.update_autoscaler(scaledown_window=1200)   # 20분간 깨어 있기 (기본값은 120초)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;=&amp;gt; 실습하는 동안만 20분으로 늘려두고, 끝나면 다시 120초로 돌려놓는 식으로 쓰면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;느낀 점&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&quot;모델을 올리는 시간&quot;과 &quot;추론하는 시간&quot;을 분리하는 게 서빙의 핵심이라는 걸 숫자로 보니 확 와닿았다. 1분 22초 중 실제 추론은 1초 남짓이고 나머지는 전부 준비 과정이었다. (실제 모델을 서비스 화 하는 과정에서 신경 써야 할 포인트를 배울 수 있었다. 모델 인프라 담당하시는 엔지니어들은 이런 부분에 대한 고민들을 엄청 해나가는 사람들이겠지?)&lt;/li&gt;
&lt;li&gt;결국 서빙 설계는 콜드 스타트와 비용 사이의 트레이드오프를 어디에 둘 것인가의 문제인 것 같다. 5분에 한 번 도는 딜 알림 시스템이라면 30초 콜드 스타트를 감수하는 쪽이 맞고, 사용자가 화면 앞에서 기다리는 서비스라면 컨테이너를 살려두는 비용을 내야 한다. 엔지니어링 세계에서의 최적화에 대한 관점을 작게나마 느껴볼 수 있던 순간이었다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 첫 번째 에이전트 - SpecialistAgent, 그리고 전처리가 필요한 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 Modal에 올린 서비스를 에이전트로 감쌀 차례다. 아래는 첫 번째 에이전트인 SpecialistAgent 이다.&lt;/p&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;class SpecialistAgent(Agent):
    &quot;&quot;&quot;
    Modal에서 원격으로 돌고 있는 파인튜닝 모델을 호출하는 에이전트
    &quot;&quot;&quot;
    name = &quot;Specialist Agent&quot;
    color = Agent.RED

    def __init__(self):
        self.log(&quot;Specialist Agent is initializing - connecting to modal&quot;)
        Pricer = modal.Cls.from_name(&quot;pricer-service&quot;, &quot;Pricer&quot;)
        self.pricer = Pricer()

    def price(self, description: str) -&amp;gt; float:
        self.log(&quot;Specialist Agent is calling remote fine-tuned model&quot;)
        result = self.pricer.price.remote(description)
        self.log(f&quot;Specialist Agent completed - predicting ${result:.2f}&quot;)
        return result
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞 장에서 만든 배포 서비스를 이름으로 붙잡아서(modal.Cls.from_name), price()로 감싸고, 앞뒤로 로그를 찍는 게 전부다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;무거운 모델 로딩, GPU, 양자화는 전부 Modal 쪽에 있다.&lt;/p&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;agent = SpecialistAgent()
agent.price(&quot;iPhone 10&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;INFO:root:[Specialist Agent] Specialist Agent is initializing - connecting to modal
INFO:root:[Specialist Agent] Specialist Agent is calling remote fine-tuned model
INFO:root:[Specialist Agent] Specialist Agent completed - predicting $350.00
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; 350.0&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;iPhone 10&quot; 네 글자만 던졌는데 350달러가 나온다. 잘 되는 것처럼 보인다. 그런데 여기서 강사가 제동을 건다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;학습할 때의 입력과 추론(Inference)할 때의 입력이 다르다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Week 7에서 이 모델을 학습(파인튜닝)시킬 때 넣었던 데이터는 이런 모양이었다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;Title: Excess V2 Distortion/Modulation Pedal
Category: Music Pedals
Brand: Old Blood Noise
Description: A versatile pedal offering distortion and three modulation modes...
Details: Features include separate gain, tone, and volume controls...
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제목, 카테고리, 브랜드, 설명, 상세가 정해진 순서로 들어간 정형화된 텍스트다. 그런데 실제 서비스에서 들어오는 입력은 &quot;iPhone 10&quot; 같은 단어이거나, RSS에서 긁어온 홍보 문구다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;=&amp;gt; 모델은 학습 때 본 형식으로 물어볼 때 가장 잘한다. 답이 나오니까 되는 것 같아 보이지만, 이상적인 상태는 아니라는 것.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 모델에 보내기 전에 입력을 학습 때의 형식으로 다시 써주는 단계를 하나 둔다. 그게 Preprocessor다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전처리기는 작은 LLM이 맡는다.&lt;/p&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;SYSTEM_PROMPT = &quot;&quot;&quot;Create a concise description of a product. Respond only in this format. Do not include part numbers.
Title: Rewritten short precise title
Category: eg Electronics
Brand: Brand name
Description: 1 sentence description
Details: 1 sentence on features&quot;&quot;&quot;

# 기본값은 내 맥에서 도는 로컬 Llama 3.2
DEFAULT_MODEL_NAME = os.getenv(&quot;PRICER_PREPROCESSOR_MODEL&quot;, &quot;ollama/llama3.2&quot;)


class Preprocessor:
    def preprocess(self, text: str) -&amp;gt; str:
        response = completion(
            messages=self.messages_for(text),
            model=self.model_name,
            api_base=self.base_url,      # ollama라면 http://localhost:11434
        )
        return response.choices[0].message.content
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;재밌는 건 이 전처리를 굳이 좋은 모델에 맡기지 않는다는 점이다. 기본값은 내 맥에서 Ollama로 도는 Llama 3.2. 형식만 맞춰 다시 쓰는 일이라 작은 모델로 충분하고, 무료이고, 데이터도 밖으로 나가지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 돌려보면 이렇다.&lt;/p&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;preprocessor = Preprocessor()
text = preprocessor.preprocess(&quot;Quadcast HyperX condenser mic, connects via usb-c to your computer for crystal clear audio&quot;)
print(text)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;gams&quot;&gt;&lt;code&gt;### System:
**Title:** High-Quality Microphone
**Category:** Electronics
**Brand:** HyperX
**Description:** The Quadcast HyperX condenser mic delivers exceptional sound quality and connectivity.
**Details:** Equipped with USB-C, this microphone features plug-and-play functionality.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;형식은 맞췄는데 아쉬운 구석이 보인다. ### System: 같은 군더더기가 붙었고, 제목이 &quot;High-Quality Microphone&quot;으로 뭉개져서 정작 중요한 제품명이 사라졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모델을 바꿔보면 결과가 달라진다. .env에 한 줄만 추가하면 된다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;PRICER_PREPROCESSOR_MODEL=groq/openai/gpt-oss-20b
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;preprocessor = Preprocessor(model_name=&quot;groq/openai/gpt-oss-20b&quot;)
text = preprocessor.preprocess(&quot;Quadcast HyperX condenser mic, connects via usb-c to your computer for crystal clear audio&quot;)
print(text)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;avrasm&quot;&gt;&lt;code&gt;Title: HyperX Quadcast Mic
Category: Audio Equipment
Brand: HyperX
Description: Crystal-clear USB-C condenser mic.
Details: Built-in cardioid pickup, tap-to-mute button, four-channel switching, USB-C powered.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제품명이 살아 있고 군더더기도 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;전처리가 실제로 예측을 바꾸는가&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 상품을 두 가지 입력으로 물어봤다. 원문 그대로 던졌을 때는 90달러, 전처리를 거친 텍스트로 던졌을 때는 130달러가 나왔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 마이크(HyperX QuadCast)의 실제 가격은 140달러대다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;=&amp;gt; 모델도, 가중치도, 프롬프트 템플릿도 전부 같은데 입력 형식 하나만 바꿔서 90 &amp;rarr; 130으로 바뀌었다.&lt;/p&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;h4 data-ke-size=&quot;size20&quot;&gt;느낀 점&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;학습 때의 입력 분포와 서비스에서 들어오는 입력 분포가 다르면 성능이 조용히 새어나간다는 걸 눈으로 봤다. 모델을 더 잘 학습시키는 것보다, inference 과정에서 들어오는 입력을 학습 때 형식으로 맞춰주는 게 훨씬 싸게 먹히는 개선일 수 있다는 것도.&lt;/li&gt;
&lt;li&gt;이를 통해 inference 과정에서의 전처리 테크닉 등, 인퍼런스 테크닉의 중요성을 많이 느꼈던 거 같다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;그리고 전처리 같은 &quot;형식 맞추기&quot; 작업에 굳이 비싼 모델을 쓸 필요가 없다는 점도 실용적이었다. 공짜 로컬 모델로도 어느정도 작업이 가능하다는 점이 인상적이었다 . 태스크 난이도에 따라 모델 체급을 나눠 쓰는 게 시스템 설계의 기본이 되어가는 느낌이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 학습이 아니라 추론(inference)에서 승부 - 상품 80만 개를 벡터 DB로&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Week 6~7에서 성능을 올리는 방법은 전부 학습(training) 쪽이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터를 모으고, 모델을 고르고, 파인튜닝을 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Day 2에서 시도하는 건 정반대다. 학습은 전혀 하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대신 AI에게 질문을 던지는 순간에 참고 자료를 함께 넣어준다. 추론 시점 기법(inference-time technique), 즉 RAG다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;재밌는 건 재료가 새로 필요하지 않다는 점이다. Week 6에서 학습 데이터로 썼던 아마존 상품 80만 개를, 이번에는 검색용 자료로 다시 쓴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;=&amp;gt; 같은 데이터를 학습에 한 번, 추론에 또 한 번 쓰는 셈이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;상품 설명 문장을 384개의 숫자로&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 텍스트를 벡터로 바꿔줄 임베딩 모델이 필요하다. 여기서는 HuggingFace에 있는 오픈소스 모델인 all-MiniLM-L6-v2를 쓴다.&lt;/p&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;encoder = SentenceTransformer('sentence-transformers/all-MiniLM-L6-v2')

vector = encoder.encode([&quot;A proficient AI engineer who has almost reached the finale of AI Engineering Core Track!&quot;])[0]
print(vector.shape)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; (384,)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문장 하나가 384개의 실수로 바뀐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OpenAI의 임베딩 API를 써도 되지만 이 모델을 쓰는 이유는 명확하다. 무료이고, 빠르고, 내 컴퓨터에서 돌기 때문에 데이터가 밖으로 나가지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;벡터 데이터베이스에 80만 개 상품 정보 밀어 넣기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;벡터 DB는 오픈소스인 Chroma를 쓴다. 로컬 폴더에 파일로 저장되는 방식이다.&lt;/p&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;client = chromadb.PersistentClient(path=&quot;products_vectorstore&quot;)
collection = client.create_collection(&quot;products&quot;)

for i in tqdm(range(0, len(train), 1000)):
    documents = [item.summary for item in train[i: i+1000]]       # 상품 설명 텍스트
    vectors = encoder.encode(documents).astype(float).tolist()     # 384차원 벡터로 변환
    metadatas = [{&quot;category&quot;: item.category, &quot;price&quot;: item.price}  # 정답 가격을 메타데이터로 함께 저장
                 for item in train[i: i+1000]]
    ids = [f&quot;doc_{j}&quot; for j in range(i, i+1000)]
    collection.add(ids=ids, documents=documents, embeddings=vectors, metadatas=metadatas)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 핵심은 metadatas에 price를 같이 넣는다는 점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나중에 비슷한 상품을 찾았을 때 그 상품들의 실제 가격을 바로 꺼내 쓰기 위해서다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 시간이다. 강사 GPU로도 30분이 걸린다고 했고, 내 맥에서는 더 걸렸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러다 중간에 한 번 끊겨서, 이미 들어간 개수를 확인하고 이어서 채우는 코드를 따로 짜야 했다.&lt;/p&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;# 중단된 지점부터 이어서 채우기
START = collection.count()   # 이미 들어간 개수 = 다음에 채울 시작 인덱스
print(f&quot;Resuming from doc_{START}, {len(train) - START:,} items remaining&quot;)

for i in tqdm(range(START, len(train), 1000)):
    ...
print(f&quot;Done. Total count: {collection.count():,}&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Resuming from doc_289000, 511,000 items remaining
Done. Total count: 800,000
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;28만 9천 개까지 들어가 있었고, 남은 51만 1천 개를 마저 채워서 80만 개를 완성했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;임베딩이 의미를 담고 있다는 시각적 증거&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;80만 개 상품정보를 다 넣고 나면 궁금해진다. 이 384차원 숫자 뭉치가 정말 의미를 담고 있을까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확인하는 방법이 t-SNE다. 384차원을 사람이 볼 수 있는 2차원이나 3차원으로 눌러서 그려보는 기법이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전부 그리면 컴퓨터가 버티지 못해서 1만 개만 뽑고, 8개 카테고리를 색으로 구분했다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;MAXIMUM_DATAPOINTS = 10_000

CATEGORIES = ['Appliances', 'Automotive', 'Cell_Phones_and_Accessories', 'Electronics',
              'Musical_Instruments', 'Office_Products', 'Tools_and_Home_Improvement', 'Toys_and_Games']
COLORS = ['cyan', 'blue', 'brown', 'orange', 'yellow', 'green', 'purple', 'red']

# 384차원 -&amp;gt; 2차원
tsne = TSNE(n_components=2, random_state=42)
reduced_vectors = tsne.fit_transform(vectors)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1996&quot; data-origin-height=&quot;1260&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cdRlKS/dJMcabdXWZo/EzbbclroHFOitddkpJYvs0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cdRlKS/dJMcabdXWZo/EzbbclroHFOitddkpJYvs0/img.png&quot; data-alt=&quot;384차원을 2차원으로 눌러본 결과. 카테고리를 알려준 적이 없는데도 색깔끼리 뭉친다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cdRlKS/dJMcabdXWZo/EzbbclroHFOitddkpJYvs0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcdRlKS%2FdJMcabdXWZo%2FEzbbclroHFOitddkpJYvs0%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;1996&quot; height=&quot;1260&quot; data-origin-width=&quot;1996&quot; data-origin-height=&quot;1260&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;384차원을 2차원으로 눌러본 결과. 카테고리를 알려준 적이 없는데도 색깔끼리 뭉친다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주목할 점은 임베딩 모델에게 카테고리를 알려준 적이 없다는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그냥 상품 설명 텍스트만 넣었는데, 자동차 용품은 자동차 용품끼리, 악기는 악기끼리 모였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;=&amp;gt; 벡터 사이의 거리가 곧 의미의 유사도라는 걸 눈으로 확인하는 순간이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;n_components=3으로 바꾸면 3차원으로도 볼 수 있고, 이 3D 플롯은 나중에 Day 5의 Gradio 화면 오른쪽에 그대로 박히게 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1996&quot; data-origin-height=&quot;1060&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c8h1cT/dJMcaa7mwJv/43ADpqHUArIkVau0Wjxg4k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c8h1cT/dJMcaa7mwJv/43ADpqHUArIkVau0Wjxg4k/img.png&quot; data-alt=&quot;3차원 이미지&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c8h1cT/dJMcaa7mwJv/43ADpqHUArIkVau0Wjxg4k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc8h1cT%2FdJMcaa7mwJv%2F43ADpqHUArIkVau0Wjxg4k%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;1996&quot; height=&quot;1060&quot; data-origin-width=&quot;1996&quot; data-origin-height=&quot;1060&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;3차원 이미지&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;느낀 점&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;같은 데이터를 학습용과 검색용으로 두 번 쓴다는 발상이 좋았다. 이를 통해 inference-time technique으로 성능을 높일 수 있다는 점이 인상적이었다.&lt;/li&gt;
&lt;li&gt;벡터 DB를 채우는 일이 생각보다 지루하고 잘 끊긴다는 것도 실감했다. 80만 건짜리 배치 작업에서 실패를 했을 때, 재시작 지점을 어떻게 잡을지 고민해보는 것도 좋은 경험이었다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 두 번째 에이전트 - GPT-5.1에게 RAG를 통해 비슷한 상품 5개를 쥐여주기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;벡터 DB가 준비됐으니 이제 진짜 RAG를 해볼 차례다. 흐름은 세 단계다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;가격을 모르는 상품에 대한 설명이 들어온다&lt;/li&gt;
&lt;li&gt;벡터 DB에서 그와 비슷한 상품 5개와 그 실제 가격을 찾는다&lt;/li&gt;
&lt;li&gt;&quot;이런 비슷한 상품들이 이 가격이더라&quot;를 프롬프트에 붙여서 GPT-5.1에게 물어본다&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트에 쓸 상품은 이거다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;test[0]
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; &amp;lt;Old Blood Noise Excess V2 Distortion Chorus/Delay Pedal = $219.0&amp;gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기타 이펙터 페달이고 정답은 219달러다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;비슷한 상품 5개 찾기&lt;/h4&gt;
&lt;pre class=&quot;ruby&quot;&gt;&lt;code&gt;def vector(item):
    return encoder.encode(item.summary)

def find_similars(item):
    vec = vector(item)
    # 벡터 DB에서 가장 가까운 5개를 꺼낸다
    results = collection.query(query_embeddings=vec.astype(float).tolist(), n_results=5)
    documents = results['documents'][0][:]
    prices = [m['price'] for m in results['metadatas'][0][:]]   # 아까 넣어둔 정답 가격
    return documents, prices
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 돌려보면 이런 게 잡힌다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;Old Blood Noise Endeavors Procession Reverb          $209.00
Boss MD-2 Mega Distortion Modulation Pedal           $109.99
Old Blood Noise Endeavors Mondegreen Delay Pedal     $209.00
Aural Dream Bold Distortion Pedal                     $24.49
Bit Commander V2 Monophonic Analog Guitar Synth      $199.00
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 브랜드(Old Blood Noise)의 다른 페달 2개가 상위에 올라온 게 눈에 띈다. 브랜드명으로 검색한 게 아니라 설명 문장의 의미로만 찾은 결과다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가격대도 흥미롭다. 209, 109.99, 209, 24.49, 199. 비슷해 보이는 페달인데 24달러짜리부터 209달러짜리까지 섞여 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;=&amp;gt; 그래서 단순 평균을 내는 게 아니라, 어떤 게 우리 상품과 더 가까운지 판단하는 일을 LLM에게 맡기는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;컨텍스트를 프롬프트에 붙이기&lt;/h4&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;def make_context(similars, prices):
    message = &quot;For context, here are some other items that might be similar to the item you need to estimate.\n\n&quot;
    for similar, price in zip(similars, prices):
        message += f&quot;Potentially related product:\n{similar}\nPrice is ${price:.2f}\n\n&quot;
    return message

def messages_for(item, similars, prices):
    message = f&quot;Estimate the price of this product. Respond with the price, no explanation\n\n{item.summary}\n\n&quot;
    message += make_context(similars, prices)
    return [{&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: message}]
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;완성된 프롬프트는 이런 모양이 된다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;Estimate the price of this product. Respond with the price, no explanation

Title: Excess V2 Distortion/Modulation Pedal
Category: Music Pedals
Brand: Old Blood Noise
...

For context, here are some other items that might be similar to the item you need to estimate.

Potentially related product:
Title: Old Blood Noise Endeavors Procession Reverb
...
Price is $209.00

Potentially related product:
Title: Boss MD-2 Mega Distortion Modulation Multi-Effects Pedal
...
Price is $109.99

(이하 3개 더)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;호출 부분은 짧다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;def gpt_5__1_rag(item):
    documents, prices = find_similars(item)
    response = completion(
        model=&quot;gpt-5.1&quot;,
        messages=messages_for(item, documents, prices),
        reasoning_effort=&quot;none&quot;,   # 추론 모드를 끄고 빠르게
        seed=42,                   # 같은 입력이면 같은 답이 나오도록
    )
    return response.choices[0].message.content
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;gpt_5__1_rag(test[0])
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; '$229.00'&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정답 219달러에 예측 229달러. 10달러 차이다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;200건으로 평가해보면&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 건 잘 맞힌 건 우연일 수 있으니 테스트셋 200건으로 돌려봤다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;evaluate(gpt_5__1_rag, test)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내 실행 결과는 평균 오차 30.97달러였다. (MSE 2,978, r&amp;sup2; 86.4%)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1682&quot; data-origin-height=&quot;1238&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bG2qQA/dJMcahSNtMY/PAuK4vTgFcuzQiU1OBkkMK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bG2qQA/dJMcahSNtMY/PAuK4vTgFcuzQiU1OBkkMK/img.png&quot; data-alt=&quot;200건 평균 오차 $30.97. 지금까지 중 최고 기록이다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bG2qQA/dJMcahSNtMY/PAuK4vTgFcuzQiU1OBkkMK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbG2qQA%2FdJMcahSNtMY%2FPAuK4vTgFcuzQiU1OBkkMK%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;1682&quot; height=&quot;1238&quot; data-origin-width=&quot;1682&quot; data-origin-height=&quot;1238&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;200건 평균 오차 $30.97. 지금까지 중 최고 기록이다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p 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;GPT-5.1 제로샷 (Week 6): $44.74&lt;/li&gt;
&lt;li&gt;Llama 3.2 3B 파인튜닝 Full (Week 7): $39.85&lt;/li&gt;
&lt;li&gt;GPT-5.1 + RAG (지금): $30.97&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지난 글에서 몇 시간 넘게 파인튜닝을 통해 만든 1등 기록을, 학습을 한 번도 하지 않은 방법이 넘어섰다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바뀐 것은 프롬프트에 비슷한 상품 5개를 붙여준 것뿐이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;에이전트로 감싸기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 로직을 FrontierAgent 클래스로 옮긴다. 구조는 SpecialistAgent와 똑같다.&lt;/p&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;class FrontierAgent(Agent):
    name = &quot;Frontier Agent&quot;
    color = Agent.BLUE

    def __init__(self, collection):
        self.log(&quot;Initializing Frontier Agent&quot;)
        self.client = OpenAI()
        self.MODEL = &quot;gpt-5.1&quot;
        self.collection = collection                                   # 벡터 DB
        self.model = SentenceTransformer(&quot;sentence-transformers/all-MiniLM-L6-v2&quot;)
        self.log(&quot;Frontier Agent is ready&quot;)

    def get_price(self, s) -&amp;gt; float:
        # &quot;$229.00&quot; 같은 문자열에서 숫자만 뽑아낸다
        s = s.replace(&quot;$&quot;, &quot;&quot;).replace(&quot;,&quot;, &quot;&quot;)
        match = re.search(r&quot;[-+]?\d*\.\d+|\d+&quot;, s)
        return float(match.group()) if match else 0.0

    def price(self, description: str) -&amp;gt; float:
        documents, prices = self.find_similars(description)
        ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;돌려보면 이렇다.&lt;/p&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;agent = FrontierAgent(collection)
agent.price(&quot;Quadcast HyperX condenser mic, connects via usb-c to your computer for crystal clear audio&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;INFO:root:[Frontier Agent] Frontier Agent is performing a RAG search of the Chroma datastore to find 5 similar products
INFO:root:[Frontier Agent] Frontier Agent has found similar products
INFO:root:[Frontier Agent] Frontier Agent is about to call gpt-5.1 with context including 5 similar products
INFO:root:[Frontier Agent] Frontier Agent completed - predicting $135.00
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; 135.0 (실제 가격 140달러대)&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;agent.price(&quot;Shure MV7+ professional podcaster microphone with usb-c and XLR outputs&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; 279.0 (실제 가격 299달러)&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;느낀 점&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;파인튜닝없이 프론티어 모델에 RAG만을 붙였는데 성능이 꽤 높아지는게 인상적이었다.&lt;/li&gt;
&lt;li&gt;에이전트 개발하는 과정에서도 적절한 지식을 에이전트에 주는 것만으로도 원하는 성능을 꽤 끌어올릴 수 있다는 반증을 보여주는 예시라고 생각한다.&lt;/li&gt;
&lt;li&gt;다만 파인튜닝 모델은 한번 특화되어서 만들어놓으면 작은 모델을 API 비용없이 쓸 수 있다는 점에서 의미자체가 없는 건 아니라고 생각한다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;RAG의 또하나의 장점은 정보의 갱신이라고도 생각한다. 새 상품이 나오면 벡터 DB에 한 건 추가해서 inference에서 참고하게 하면 되지만, 파인튜닝 모델은 다시 학습해야 한다. 가격처럼 계속 변하는 정보를 다룰수록 이 차이가 커질 것 같다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 세 번째 에이전트와 앙상블 에이전트 - 셋을 섞으면 정말 더 좋아질까&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가격을 추정하는 전문가가 지금 둘이다. 파인튜닝 모델(SpecialistAgent)과 RAG(FrontierAgent).&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기에 하나를 더 붙인다. Week 6에서 만들었던 가격예측용 딥러닝 모델이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Week 6에서 학습해둔 가중치 파일(deep_neural_network.pth)을 내려받아서 그대로 불러온다.&lt;/p&gt;
&lt;pre class=&quot;ruby&quot;&gt;&lt;code&gt;class NeuralNetworkAgent(Agent):
    name = &quot;Neural Network Agent&quot;
    color = Agent.MAGENTA

    def __init__(self):
        self.log(&quot;Neural Network Agent is initializing&quot;)
        self.neural_network = DeepNeuralNetworkInference()
        self.neural_network.setup()
        self.neural_network.load(&quot;deep_neural_network.pth&quot;)   # Week 6에서 학습한 가중치
        self.log(&quot;Neural Network Agent is ready and weights are loaded&quot;)

    def price(self, description: str) -&amp;gt; float:
        self.log(&quot;Neural Network Agent is starting a prediction&quot;)
        result = self.neural_network.inference(description)
        self.log(f&quot;Neural Network Agent completed - predicting ${result:.2f}&quot;)
        return result
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;agent = NeuralNetworkAgent()
agent.price(&quot;Shure MV7+ professional podcaster microphone with usb-c and XLR outputs&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; 162.3922576904297 (실제 가격 299달러)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;혼자서는 좀 아쉬운 성적이다(Week 6 기록으로 평균 오차 $46.49). 그런데 앙상블에서는 이런 모델도 쓸모가 있다는 게 이 장의 이야기다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;앙상블이 왜 통하는가&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강사가 든 예시가 직관적이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 상품의 실제 가격이 100달러라고 하자. 모델 A는 항상 10달러씩 빗나간다. 110이나 90을 낸다. 평균 오차는 10.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모델 B도 마찬가지로 평균 오차가 10이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p 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;최악의 경우: A와 B가 둘 다 +10으로 틀린다 -&amp;gt; 평균도 110, 오차 그대로 10&lt;/li&gt;
&lt;li&gt;하지만 A는 +10, B는 -10으로 틀리는 경우가 생기면 -&amp;gt; 평균은 100, 오차 0&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;=&amp;gt; 두 모델이 서로 다른 방향으로 틀리는 만큼, 평균 오차는 10보다 작아진다. 서로의 노이즈를 상쇄시키는 것이 앙상블의 원리다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이건 새로운 아이디어가 아니다. 넷플릭스 프라이즈 우승팀도 대규모 앙상블로 이겼고, Week 6에서 썼던 랜덤 포레스트 자체가 결정 트리 여러 개를 묶은 앙상블 모델이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;그럼 3개 에이전트를 어떤 비율로 섞을까&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강사가 원래 소개한 방식은 검증 데이터에 선형 회귀를 돌려서 최적 가중치를 찾는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Week 6에서 여러 모델을 묶을 때 썼던 그 방법이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 이번 코드에서 실제로 들어간 건 고정 가중치다.&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;def ensemble(item):
    price1 = get_price(gpt_5__1_rag(item))   # RAG
    price2 = specialist(item)                 # 파인튜닝 모델
    price3 = deep_neural_network(item)        # 딥러닝 모델
    return price1 * 0.8 + price2 * 0.1 + price3 * 0.1
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RAG에 0.8, 나머지 둘에 0.1씩. 성능이 가장 좋았던 모델에 몰아주고 나머지를 가볍게만 얹은 구성이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;결과는 어땠나&lt;/h4&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;evaluate(ensemble, test)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내 실행 결과는 평균 오차 30.70달러. (MSE 3,011, r&amp;sup2; 86.3%)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1682&quot; data-origin-height=&quot;1238&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/blnh9u/dJMcaijXBZi/v3WxCfFTaFFjspvRKoufEk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/blnh9u/dJMcaijXBZi/v3WxCfFTaFFjspvRKoufEk/img.png&quot; data-alt=&quot;앙상블 $30.70. RAG 단독 $30.97에서 0.27달러 내려갔다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/blnh9u/dJMcaijXBZi/v3WxCfFTaFFjspvRKoufEk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fblnh9u%2FdJMcaijXBZi%2Fv3WxCfFTaFFjspvRKoufEk%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;1682&quot; height=&quot;1238&quot; data-origin-width=&quot;1682&quot; data-origin-height=&quot;1238&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;앙상블 $30.70. RAG 단독 $30.97에서 0.27달러 내려갔다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;솔직히 말하면 기대보다 훨씬 작은 개선이다. RAG 단독이 30.97이었으니 0.27달러 좋아진 셈이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강사의 실행에서도 30.19에서 29.90으로, 0.29달러 내려갔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 이것밖에 안 올랐을까 생각해보면 이유는 가중치에 있는 것 같다. RAG가 30.97인데 파인튜닝 모델은 39.85, 딥러닝 모델은 46.49다. 나머지 둘의 오차가 확실히 크니 0.1씩밖에 실을 수 없고, 그러면 상쇄 효과도 그만큼 작아진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;=&amp;gt; 앙상블은 성능이 비슷한 모델들이 서로 다른 실수를 할 때 효과가 크다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;앙상블 에이전트로 묶기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 세 에이전트를 EnsembleAgent 하나로 묶는다. 앞에서 만든 Preprocessor도 여기에 들어간다.&lt;/p&gt;
&lt;pre class=&quot;ruby&quot;&gt;&lt;code&gt;class EnsembleAgent(Agent):
    name = &quot;Ensemble Agent&quot;
    color = Agent.YELLOW

    def __init__(self, collection):
        self.log(&quot;Initializing Ensemble Agent&quot;)
        self.specialist = SpecialistAgent()
        self.frontier = FrontierAgent(collection)
        self.neural_network = NeuralNetworkAgent()
        self.preprocessor = Preprocessor()
        self.log(&quot;Ensemble Agent is ready&quot;)

    def price(self, description: str) -&amp;gt; float:
        # 1) 들어온 텍스트를 학습 때의 형식으로 다시 쓴다
        rewrite = self.preprocessor.preprocess(description)
        # 2) 세 에이전트에게 각각 물어본다
        specialist = self.specialist.price(rewrite)
        frontier = self.frontier.price(rewrite)
        neural_network = self.neural_network.price(rewrite)
        # 3) 가중 평균
        combined = frontier * 0.8 + specialist * 0.1 + neural_network * 0.1
        self.log(f&quot;Ensemble Agent complete - returning ${combined:.2f}&quot;)
        return combined
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 돌려보면 1장에서 이야기했던 색깔 로그의 쓸모가 드러난다.&lt;/p&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;agent = EnsembleAgent(collection)
agent.price(&quot;Shure MV7+ professional podcaster microphone with usb-c and XLR outputs&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1682&quot; data-origin-height=&quot;880&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bMOtJZ/dJMb99UPfDm/K5HRpDwrV1k0yONA68zwE0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bMOtJZ/dJMb99UPfDm/K5HRpDwrV1k0yONA68zwE0/img.png&quot; data-alt=&quot;가격 하나를 추정하는 데 에이전트 4개가 순서대로 움직인다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bMOtJZ/dJMb99UPfDm/K5HRpDwrV1k0yONA68zwE0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbMOtJZ%2FdJMb99UPfDm%2FK5HRpDwrV1k0yONA68zwE0%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;1682&quot; height=&quot;880&quot; data-origin-width=&quot;1682&quot; data-origin-height=&quot;880&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;가격 하나를 추정하는 데 에이전트 4개가 순서대로 움직인다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; 261.21&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 한 건에서는 파인튜닝 모델이 299달러로 정답을 정확히 맞혔는데, 앙상블은 다른 둘에 끌려 내려가 261.21달러가 됐다. 평균적으로는 앙상블이 낫지만, 개별 건에서는 잘 맞힌 모델의 답을 깎아먹기도 한다는 걸 보여주는 사례다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 비용도 같이 봐야 한다. 가격 하나를 답하는 데 LLM 호출이 4번(전처리 llama3.2, 파인튜닝 모델, 임베딩, GPT-5.1), 신경망 추론이 1번 일어난다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;=&amp;gt; 0.27달러의 정확도 개선을 위해 호출 수가 몇 배로 늘어난 구조다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;느낀 점&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;앙상블이 왜 좋은가에 대해 강사의 예시를 통해서 이해해볼 수 있어서 의미 있었다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;또한, 앙상블이 항상 좋지는 않을 수 있고 조건이 붙는다는 걸 숫자로 봤다. 모델들의 성능이 비슷하고 서로 다른 실수를 할 때만 상쇄가 일어난다. 한 모델이 압도적이면 나머지는 노이즈만 더할 수도 있다.&lt;/li&gt;
&lt;li&gt;정확도 0.27달러 개선에 호출 4번을 쓰는 게 맞는 선택인지도 생각해볼 지점이었다. 실무였다면 앙상블을 빼고 RAG 단독으로 가는 쪽이 더 합리적일 수 있다. 성능표의 1등이 곧 시스템의 정답은 아니라는 것. 실무에선 여러번 호출로 인해 발생하는 레이턴시와 비용을 고려했을 때에도, 정말로 이 여러번 호출을 하는게 성능상의 유의미한 차이를 내는지가 중요했을 거 같다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;그럼에도 이 구조를 만들어본 의미는 있다고 느꼈다. 서로 다른 성질의 모델(파인튜닝 / 검색 기반 / 딥러닝)을 한 인터페이스(price(description) -&amp;gt; float)로 통일해두면, 나중에 모델을 갈아끼우거나 가중치를 바꾸는 게 쉬워진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마무리 - 성적표가 또 뒤집혔다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Week 6부터 이어온 가격 예측 성적표에 이번 글의 결과 두 줄이 추가됐다. 평균 절대 오차이고, 낮을수록 좋다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Base Llama 3.2 (4bit)         $185.52   &amp;larr; 파인튜닝 전 (내 실행 기준)
Constant (평균값)              $106.18
Linear Regression             $101.56
Human (강사 Ed 본인)            $87.62
NLP + Linear Regression        $76.81
GPT-4.1 Nano (파인튜닝)         $75.91
Random Forest                  $72.28
XGBoost                        $68.23
Fine-tuned Lite                $65.40
Neural Network                 $63.97
GPT-4.1 Nano (제로샷)           $62.51
Grok 4.1 Fast                  $57.62
Gemini 3 Pro                   $50.54
Claude 4.5 Sonnet              $47.10
Deep Neural Network            $46.49
GPT-5.1 (제로샷)                $44.74
Fine-tuned Full                $39.85   &amp;larr; Week 7의 1등
GPT-5.1 + RAG                  $30.97   &amp;larr; 이번 글 (내 실행)
Ensemble                       $30.70   &amp;larr; 이번 글 (내 실행), 최종 1위
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지난 글의 결말은 &quot;작은 오픈소스 모델을 파인튜닝했더니 프론티어 모델을 이겼다&quot;였다. 이번 글의 결말은 그 위에 하나가 더 얹힌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;=&amp;gt; 그 파인튜닝 모델을, 학습을 한 번도 하지 않은 방법이 다시 넘어섰다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p 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;RAG(추론 시점)는 모델은 그대로 두고 필요한 지식을 그때그때 넣어준다. 갱신이 쉽고, 학습 비용이 0이다.&lt;/li&gt;
&lt;li&gt;둘은 대체재가 아니라 다른 축이고, 이번 시스템처럼 같이 쓸 수도 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 앙상블에서 배운 게 하나 더 있다. 성적표 1등이 곧 시스템의 정답은 아니라는 것.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Ensemble이 최종 1위지만 RAG 단독보다 0.27달러 나은 대가로 호출 수가 몇 배가 됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 서비스를 만든다면 나는 RAG 단독을 골랐을 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지가 &quot;가격을 맞히는 쪽&quot;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Day 1에서 파인튜닝 모델을 Modal에 올려 서비스로 만들었고, Day 2에서 벡터 DB를 채우고 RAG 에이전트와 앙상블 에이전트를 만들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 상품 설명을 던지면 가격이 나온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;다음 글에서 다룰 것&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 아직 빠진 게 있다. 지금은 내가 &quot;이 상품 가격 얼마야?&quot;라고 물어봐야만 시스템이 움직인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;무엇의 가격을 물어볼지는 여전히 사람이 정하고 있다는 뜻이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 글(Day 3~5)에서는 그 부분을 채운다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인터넷에서 딜을 긁어와 유망한 것만 골라내는 스캐너 에이전트 (그리고 Structured Outputs)&lt;/li&gt;
&lt;li&gt;알림 문구를 작성해 휴대폰으로 보내는 메시징 에이전트&lt;/li&gt;
&lt;li&gt;어떤 도구를 언제 부를지 LLM이 스스로 정하는 자율 플래닝 에이전트 (툴 콜)&lt;/li&gt;
&lt;li&gt;전부를 묶는 에이전트 프레임워크(메모리 등)와 Gradio UI&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 글은 이 LLM 엔지니어링 강의의 마지막 파트로,&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Day 3~5를 통해 최종적인 에이전트를 구축한 내용을 소개하고,&amp;nbsp;&lt;/p&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;해당 내용으로 다시 찾아오겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;(꾸준히 공부하고&lt;span&gt;&amp;nbsp;적을테니&amp;nbsp;&lt;/span&gt;많은 관심 부탁드립니다.)&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;Profile:&lt;/span&gt;&lt;br /&gt;&lt;a style=&quot;color: #0070d1; text-align: start;&quot; href=&quot;http://www.linkedin.com/in/doobeom-kim-%EA%B9%80%EB%91%90%EB%B2%94-2b9649242&quot;&gt;&lt;span&gt;&lt;span style=&quot;color: #0070d1;&quot;&gt;Linkedin&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>AI 공부</category>
      <category>Agent</category>
      <category>ai</category>
      <category>AI 공부</category>
      <category>ai 에이전트</category>
      <category>inference</category>
      <category>llm</category>
      <category>Modal.com</category>
      <category>생성형 AI</category>
      <category>에이전트</category>
      <category>파인튜닝</category>
      <author>kdb1248</author>
      <guid isPermaLink="true">https://doobeom-coding.tistory.com/94</guid>
      <comments>https://doobeom-coding.tistory.com/94#entry94comment</comments>
      <pubDate>Sun, 2 Aug 2026 14:55:04 +0900</pubDate>
    </item>
    <item>
      <title>16. 3B 오픈소스 모델(Llama 3.2)로 GPT-5.1 이겨보기 &amp;mdash; QLoRA 파인튜닝 실전</title>
      <link>https://doobeom-coding.tistory.com/93</link>
      <description>&lt;h3 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;/li&gt;
&lt;li&gt;1장. 학습을 좌우하는 5개 하이퍼파라미터&lt;/li&gt;
&lt;li&gt;2장. 잠깐 복습 &amp;mdash; QLoRA 5개 하이퍼파라미터&lt;/li&gt;
&lt;li&gt;3장. 학습은 실제로 어떻게 일어나는가 &amp;mdash; 학습 4단계와 backpropagation&lt;/li&gt;
&lt;li&gt;4장. SFTTrainer로 실제 학습 돌리기 &amp;mdash; 그리고 wandb 곡선 읽기&lt;/li&gt;
&lt;li&gt;5장. 오버피팅 관찰기 &amp;mdash; train loss가 좋아 보일 때가 더 위험하다&lt;/li&gt;
&lt;li&gt;6장. LLM은 &quot;숫자를 빼지 않는다&quot; &amp;mdash; cross-entropy와 128k 확률분포&lt;/li&gt;
&lt;li&gt;7장. 최종 평가와 전체 성적표 &amp;mdash; 작은 모델이 프론티어를 이긴다&lt;/li&gt;
&lt;li&gt;마무리&lt;span&gt; &amp;mdash; &lt;/span&gt;파인튜닝은&lt;span&gt; &lt;/span&gt;만능인가&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;들어가며&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지난 글(&lt;a href=&quot;https://doobeom-coding.tistory.com/92&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;글 15&lt;/a&gt;)에서는 실제 모델 학습을 거의 하지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대신 모델 학습을 시작하기 전에 알아야 할 것들을 파악해보는 시간을 가졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 왜 3B(30억) 파라미터를 그냥 학습할 수 없는지&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- LoRA와 양자화(Quantization)가 각각 무엇을 해결하는지&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 그리고 파인튜닝 전 base 모델(라마 3.2)이 가격 예측을 얼마나 못하는지&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&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;Day 3~4에서 Llama 3.2 3B를 내 Google Colab에서 QLoRA로 미세조정하고, Day 5에서 그 결과를 평가한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지난 글 15 도입부에서 스포일러를 하나 풀어놨었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;Week 6에서 잘 동작하지 않던 파인튜닝이(GPT 4.1 nano를 OpenAI API를 써서 파인튜닝 했었음)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번엔 프론티어 모델을 전부 제치고 1등을 한다&quot;고.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글이 그 회수다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3B(30억 파라미터)짜리 오픈소스 모델이, 그것도 4비트 양자화(Quantization)으로 짓눌린 채로,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GPT-5.1까지 제치고 가격 예측 task에서 성능 1위를 찍는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 결과 뿐 아니라, 나는 그 과정에서 지금껏 미지의 세계에 가까웠던 모델 학습에 관해 궁금한 게 훨씬 많았다.&lt;br /&gt;예를 들면,&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;학습을 &quot;돌린다&quot;는 게 코드로는 사실상&amp;nbsp;train()&amp;nbsp;한 줄인데, 그 안에서 대체 무슨 일이 일어나는가?&lt;/li&gt;
&lt;li&gt;학습률(Learning rate)&amp;middot;배치(batch size)&amp;middot;에폭(epoch) 같은 학습을 위한 하이퍼 파라미터들 어떤 기준으로 정하는가? 아무 값이나 넣으면 안 되나?&lt;/li&gt;
&lt;li&gt;LLM은 &quot;89달러가 정답인데 99달러라고 했으니 10만큼 틀렸다&quot;는 식으로 학습하는 걸까?&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;=&amp;gt; 이번 글은 이 질문들을 하나씩 풀면서, 마지막에 모델간의 성능 비교로 마무리한다. &lt;br /&gt;코드는 짧지만 그 밑에 깔린 모델학습에 대한 개념이 이번 주의 진짜 핵심이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 학습을 좌우하는 5개 하이퍼파라미터&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;학습 코드를 열면 맨 위에 상수들이 잔뜩 선언돼 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그중 &quot;학습이 어떻게 진행될지&quot;를 정하는 5개의 핵심 하이퍼파라미터가 있다.&lt;/p&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;(loss라는 골짜기 바닥을 향해 산을 내려가는 게 학습이다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 학습 노트북의 상수는 이렇게 생겼다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1783749356083&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;EPOCHS = 1 if LITE_MODE else 3

BATCH_SIZE = 32 if LITE_MODE else 256

GRADIENT_ACCUMULATION_STEPS = 1



LEARNING_RATE = 1e-4

LR_SCHEDULER_TYPE = 'cosine'

WARMUP_RATIO = 0.01

OPTIMIZER = &quot;paged_adamw_32bit&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LITE_MODE는 내가 무료 T4 GPU에서 가볍게 돌릴지(lite, 데이터 2만 개), 유료 A100에서 제대로 돌릴지(full, 80만 개)를 선택할 수 있게 해준다. 나는 lite로 돌렸다. 위 값들을 하나씩 풀어보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. epochs &amp;mdash; 데이터를 몇 바퀴 돌릴지&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;전체 데이터를 몇 번 반복 학습할지다. lite는 1바퀴, full은 3바퀴.&lt;/li&gt;
&lt;li&gt;처음엔 이게 이상했다. &lt;b&gt;같은 데이터를 왜 학습을 위해 또 넣지?&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; &lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;답을 외운 학생한테 같은 문제집을 또 풀리는 것 아닌가?&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; 강사의 설명은 이랬다. 매 epoch마다 데이터를 섞어(shuffle) 배치를 새로 구성하기 때문에, 완전히 같은 입력이 아니다&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;(강사의 비유) 같은 학생 1000명을 다시 한 번 전부 만나긴 하는데, 줄 세우는 순서와 조 편성이 바뀐다.&lt;/li&gt;
&lt;li&gt;셔플의 예시를 좀 더 설명하자면 아래와 같다
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;epoch 1에서는 데이터 순서가 A, B, C, D&lt;/li&gt;
&lt;li&gt;epoch 2에서는 C, A, D, B이런 식으로 순서가 바뀔 수 있음. 그리고 배치로 나누기 떄문에 첫 epoch의 첫 배치와 다음 epoch의 첫 배치 구성이 달라질 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;=&amp;gt; 그리고 애초에 한 바퀴로는 파라미터가 조금씩만 움직여서 충분히 안 배운다. 다만 너무 많이 돌리면 문제집을 통째로 외워버리는 과적합이 온다(이건 5장에서 실제로 관찰한다). 산 비유로는 &quot;산을 몇 번 반복해서 답사할지&quot;다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. batch size &amp;mdash; 한 번에 몇 개를 보고 판단할지&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;한 스텝에 데이터를 몇 개 묶어서 볼지다. lite는 32, full은 256.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예를 들어 데이터 1,000개가 있고 batch size가 10이면:&lt;/li&gt;
&lt;li&gt;한 번에 10개씩 넣어서 학습&lt;/li&gt;
&lt;li&gt;총 100번에 나눠서 1 epoch를 끝냄&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;데이터 전체를 한 번에 넣으면 좋을 것 같지만, 보통은 GPU 메모리가 부족해서 그렇게 못한다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;그래서 현실적으로는&amp;nbsp;&lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;몇 개씩 묶어서 넣고 -&amp;gt; loss 계산하고 -&amp;gt; gradient 계산하고 -&amp;gt; 업데이트하고 -&amp;gt; 다시 다음 묶음을 넣는다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Batch Size를 크게 잡으면 여러 샘플의 평균으로 방향을 정하니 안정적이지만, 한 번 한 번의 업데이트가 너무 &amp;ldquo;평균적&amp;rdquo;이어서 거칠게 탐색을 덜 할 수 있음.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;작게 잡으면 메모리를 덜 먹고 작은 GPU에서도 학습이 가능하며 때로는 일반화에 유리하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;=&amp;gt; (강사의 비유) 작은 batch는 여러 사람의 불완전한 의견을 들으며 움직이는 것, 큰 batch는 아주 많은 사람의 평균 의견으로 움직이는 것. 평균 의견은 안정적이지만 가끔 너무 보수적일 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;=&amp;gt; 배치 크기를 보통 2의 거듭제곱(32, 256&amp;hellip;)으로 잡는 건, GPU가 딱 떨어지는 묶음 단위를 좋아하기 때문이다.&lt;br /&gt;요즘은 엄격한 규칙은 아니고 관습에 가깝다. 산 비유로는 &quot;한 번에 주변을 얼마나 넓게 보고 판단할지&quot;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. learning rate &amp;mdash; 한 걸음의 보폭&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;모델은 loss를 줄이는 방향으로 움직이는데,learning rate는 그 이동 폭을 정함.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;learning rate가 크다 &amp;rarr; 큰 걸음&lt;/li&gt;
&lt;li&gt;learning rate가 작다 &amp;rarr; 작은 걸음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;산 내려가기 비유로 한다면, 한 스텝에서 파라미터를 얼마나 크게 움직일지.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&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;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;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span data-token-index=&quot;0&quot;&gt;즉, 좋은 지점 근처에서도 너무 크게 움직여서 안정적으로 못 멈출 수 있음&lt;/span&gt;&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span data-token-index=&quot;0&quot;&gt;반대로 발걸음이 너무 작다면&lt;/span&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;/ul&gt;
&lt;/li&gt;
&lt;li&gt;정리를 하면&amp;nbsp;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;높은 learning rate는 때때로 좁은 지역 해(local minimum)에서 벗어나게 도와줄 수 있음&lt;/li&gt;
&lt;li&gt;하지만 너무 크면 아예 좋은 곳에 정착을 못 함&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;보통 1e-4(0.0001)에서 시작해 10배 단위로 탐색한다.&lt;/li&gt;
&lt;/ul&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;gt; 이렇게 발걸음(learning rate) 크기에 따라 trade off 가 있다&amp;nbsp;&lt;b&gt;작으면 안정적이지만 답답하고, 크면 빠르지만 위험하다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;=&amp;gt; 그래서 학습률(learning rate)을 처음부터 끝까지 고정하지 않고 시간에 따라 바꾸는 &lt;b&gt;scheduler&lt;/b&gt;를 쓴다. 코드의 'cosine'이 그거다. 초반엔 크게 시작해서 빠르게 배우고, 뒤로 갈수록 작게 줄여서 섬세하게 미세 조정한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4. gradient accumulation &amp;mdash; 메모리 우회용&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이건 이름 때문에 오해하기 쉽다. 나도 처음엔 &quot;속도 높이는 기법인가?&quot; 했는데, &lt;b&gt;속도가 아니라 메모리 우회&lt;/b&gt;가 목적이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오히려 더 느려질 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 학습은 아래 4단계를 거친다.&amp;nbsp;&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;forward pass&lt;/b&gt;: 입력 넣고 예측&lt;/li&gt;
&lt;li&gt;&lt;b&gt;loss calculation&lt;/b&gt;: 얼마나 틀렸는지 계산&lt;/li&gt;
&lt;li&gt;&lt;b&gt;backward pass&lt;/b&gt;: 어느 방향으로 고쳐야 할지 gradient 계산&lt;/li&gt;
&lt;li&gt;&lt;b&gt;take a step&lt;/b&gt;: optimizer가 실제로 가중치 업데이트&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;gradient accumulation은 GPU 메모리가 부족해서, &lt;b&gt;큰 batch size를 한 번에 못 넣을 때&lt;/b&gt; 쓰는 우회 방법이라 할 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 원래는 batch 32로 학습하고 싶은데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내 GPU 메모리가 batch 8밖에 못 버틴다고 해보자.&lt;/p&gt;
&lt;p 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;batch 8 넣고 gradient 계산&lt;/li&gt;
&lt;li&gt;또 batch 8 넣고 gradient 계산을 누적&lt;/li&gt;
&lt;li&gt;또 batch 8&lt;/li&gt;
&lt;li&gt;또 batch 8&lt;/li&gt;
&lt;li&gt;그다음에 한 번만 step 수행&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 &lt;b&gt;실질적으로 batch 32처럼 동작&lt;/b&gt;하게 만들 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쉽게 말하면, 작은 배치를 여러 번 처리하면서 gradient를 모아뒀다가, 한 번에 업데이트한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 GPU 메모리는 작은 배치만큼만 쓰면서, 효과는 큰 배치처럼 낼수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;micro batch size = GPU에 실제로 한 번 넣는 크기&lt;/li&gt;
&lt;li&gt;gradient accumulation steps = 몇 번 모을지&lt;/li&gt;
&lt;li&gt;effective batch size = 둘을 곱한 값&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;effective batch size = micro batch size &amp;times; gradient accumulation steps&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;# 예: 8 &amp;times; 4 = 32&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 코드에선 GRADIENT_ACCUMULATION_STEPS = 1이라 실제로는 쓰지 않았지만(배치 32/256이 메모리에 들어갔으니까),&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;메모리가 부족할 때 큰 배치를 흉내 내는 카드&quot;로 알아두면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;산 비유로는&lt;b&gt; &quot;좁은 시야로 여러 번 본 뒤 종합해서 한 걸음 떼기&quot;.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;5. optimizer &amp;mdash; 걸음걸이&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;gradient가 방향을, learning rate가 보폭을 정한다면, optimizer는 &lt;b&gt;어떤 걸음걸이로 움직일지&lt;/b&gt;를 정한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드의 paged_adamw_32bit(AdamW 계열)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;gradient를 받아서&lt;/p&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;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;backward pass가 &amp;ldquo;어느 방향으로 가야 할지&amp;rdquo; 알려주고&lt;/li&gt;
&lt;li&gt;optimizer가 &amp;ldquo;그 방향으로 실제로 어떻게 움직일지&amp;rdquo; 결정한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;optimizer의 종류인 SGD와 ADAM 간 차이를 비교하면 아래와 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SGD는 가장 단순한 방식으로&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;loss 줄이는 방향으로&lt;/li&gt;
&lt;li&gt;learning rate만큼&lt;/li&gt;
&lt;li&gt;한 걸음 이동&lt;/li&gt;
&lt;/ul&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;p data-ke-size=&quot;size16&quot;&gt;Adam은 SGD보다 똑똑하게 움직이려는 optimizer다&lt;/p&gt;
&lt;p 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;최근 gradient의 평균도 보고&lt;/li&gt;
&lt;li&gt;변화량의 크기도 고려해서&lt;/li&gt;
&lt;li&gt;파라미터마다 업데이트를 다르게 조절한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p 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;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;딥러닝, 특히 LLM fine-tuning에서는&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SGD보다 &lt;b&gt;Adam/AdamW&lt;/b&gt;를 훨씬 자주 볼수 있다고 한다&lt;/p&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;gt; (강사의 비유) SGD는 지금 발밑 기울기만 보고 걷는 사람. &quot;지금 오른쪽 아래네? 그쪽으로 가자.&quot; &lt;br /&gt;Adam은 최근 몇 걸음의 경향도 보고, 이 길이 얼마나 울퉁불퉁했는지도 보면서 걷는 사람. &quot;최근 계속 이쪽이 맞았고, 여기선 흔들림이 컸으니 조금 조심해서 가자.&quot;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;=&amp;gt; Adam/AdamW은 파라미터마다 보폭을 적응적으로 조절해서 LLM 파인튜닝의 사실상 표준이다. (노트북엔 &quot;Adam은 이전 gradient들의 이동평균을 저장해서 잘 수렴하지만, 그만큼 모델 파라미터 수만큼의 메모리를 더 쓴다&quot;는 강사 메모도 붙어 있다.)&lt;/p&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;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;기타. loss 와 gradient의 차이&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;loss 와 gradient에 대해 조금 헷갈렸어서 추가적으로 공부한 내용은 아래와 같다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. loss 계산&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;loss는&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;/ul&gt;
&lt;p 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;정답: &amp;ldquo;고양이&amp;rdquo;&lt;/li&gt;
&lt;li&gt;모델 예측: &amp;ldquo;개&amp;rdquo;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 틀렸으니까 loss가 큼.&lt;/p&gt;
&lt;p 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;loss가 작아짐.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 loss는&lt;/p&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. gradient 계산&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;gradient는&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;loss를 줄이려면 각 파라미터를 어느 방향으로 얼마나 바꿔야 하는지 알려주는 값&lt;/b&gt;&lt;/p&gt;
&lt;p 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;loss: &amp;ldquo;지금 틀린 정도&amp;rdquo;&lt;/li&gt;
&lt;li&gt;gradient: &amp;ldquo;그래서 어디로 얼마나 고치면 되는지&amp;rdquo;&lt;/li&gt;
&lt;/ul&gt;
&lt;p 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;loss = 시험 점수표에서 &amp;ldquo;지금 60점&amp;rdquo;&lt;/li&gt;
&lt;li&gt;gradient = &amp;ldquo;어느 문제 유형을 어떻게 고치면 점수가 오를지&amp;rdquo;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. loss와 gradient의 차이&lt;/b&gt;&lt;/p&gt;
&lt;p 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;loss&lt;/b&gt;: 결과 평가&lt;/li&gt;
&lt;li&gt;&lt;b&gt;gradient&lt;/b&gt;: 수정 방향 정보&lt;/li&gt;
&lt;/ul&gt;
&lt;p 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;/li&gt;
&lt;li&gt;예측 나옴&lt;/li&gt;
&lt;li&gt;loss 계산&lt;/li&gt;
&lt;li&gt;그 loss를 기준으로 gradient 계산&lt;/li&gt;
&lt;li&gt;optimizer가 gradient를 이용해 파라미터 수정&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 gradient는 그냥 갑자기 생기는 게 아니라,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;loss를 미분해서 얻는 값&lt;/b&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 설명한 5개의 학습 관련 하이퍼파라미터를 한 표로 정리하면 이렇다.&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;하이퍼파라미터&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;쉽게&lt;/b&gt;&lt;span&gt;&lt;b&gt; &lt;/b&gt;&lt;/span&gt;&lt;b&gt;말하면&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;&lt;b&gt;이번&lt;/b&gt;&lt;/span&gt;&lt;b&gt; &lt;/b&gt;&lt;span&gt;&lt;b&gt;코드&lt;/b&gt;&lt;/span&gt;&lt;b&gt; &lt;/b&gt;&lt;span&gt;&lt;b&gt;값&lt;/b&gt;&lt;/span&gt;&lt;b&gt;(lite / full)&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;epochs&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;몇&lt;span&gt; &lt;/span&gt;바퀴&lt;span&gt; &lt;/span&gt;학습할지&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;1 / 3&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;batch size&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;한&lt;span&gt; &lt;/span&gt;번에&lt;span&gt; &lt;/span&gt;먹는&lt;span&gt; &lt;/span&gt;양&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;32 / 256&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;learning rate&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;발걸음&lt;span&gt; &lt;/span&gt;크기&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;0.0001 (+ cosine scheduler)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;gradient accumulation&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;메모리&lt;span&gt; &lt;/span&gt;부족&lt;span&gt; &lt;/span&gt;우회&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;1 (&lt;span&gt;미사용&lt;/span&gt;)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;optimizer&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;업데이트&lt;span&gt; &lt;/span&gt;방식&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;paged_adamw_32bit&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;느낀 점&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 이전 회사에서 모델 부서와 협업할떄, 노코드 llmops 툴로 모델 학습 쪽 화면을 볼때, 혹은 모델 부서의 자료들을 볼 떄 저런 다양한 모델학습의 하이퍼 파라미터들이 완전히 이해가진 않았었다 (그때그때 용어들을 AI에게 물어보긴 했어도)&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 이번에 이렇게 하나하나 더 깊게 뜯어보면서 좀 더 관련 이해를 높일 수 있던게 의미가 있었고, 왜 이런 하이퍼파라미터들을 모델 학습과정에서 조정하는지를 알게된게 흥미로웠다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 잠깐 복습 &amp;mdash; QLoRA 5개 하이퍼파라미터&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞의 5개(epochs&amp;middot;batch size&amp;middot;learning rate&amp;middot;gradient accumulation&amp;middot;optimizer)가 &lt;b&gt;&quot;어떤 모델을 학습하든 공통으로 쓰는&quot;&lt;/b&gt; 하이퍼파라미터라면, 이번 장의 5개는 &lt;b&gt;QLoRA라는 학습 방식 자체를 정의하는&lt;/b&gt; 하이퍼파라미터다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 5개는 글 15에서 이미 원리를 다뤘다(LoRA&amp;middot;양자화 편).&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 여기선 개념을 처음부터 다시 세우기보다, 실제 학습 코드의 LoraConfig가 이 값들을 어떻게 받는지 연결하면서 짧게 복습하고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대신 지난 글에서 얕게 넘어갔던 &lt;b&gt;dropout&lt;/b&gt;만 좀 더 깊게 파본다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 코드는 이렇게 생겼다.&lt;/p&gt;
&lt;pre id=&quot;code_1783751461433&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;LORA_R = 32 if LITE_MODE else 256

LORA_ALPHA = LORA_R * 2                       # 항상 r의 2배

ATTENTION_LAYERS = [&quot;q_proj&quot;, &quot;v_proj&quot;, &quot;k_proj&quot;, &quot;o_proj&quot;]

MLP_LAYERS = [&quot;gate_proj&quot;, &quot;up_proj&quot;, &quot;down_proj&quot;]

TARGET_MODULES = ATTENTION_LAYERS if LITE_MODE else ATTENTION_LAYERS + MLP_LAYERS

LORA_DROPOUT = 0.1

QUANT_4_BIT = True



lora_parameters = LoraConfig(

    lora_alpha=LORA_ALPHA,

    lora_dropout=LORA_DROPOUT,

    r=LORA_R,

    bias=&quot;none&quot;,

    task_type=&quot;CAUSAL_LM&quot;,

    target_modules=TARGET_MODULES,

)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 LoraConfig 한 덩어리에 5개 중 4개(target modules, r, alpha, dropout)가 다 들어 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나머지 하나(quantization)는 글 15에서 본 BitsAndBytesConfig 쪽에서 따로 처리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나씩 짚어보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. target modules &amp;mdash; 어느 레이어에 어댑터를 붙일지&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;신경망의 어느 층을 LoRA로 건드릴지다.&lt;/p&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;코드를 보면 lite는 어텐션 4개(q_proj, v_proj, k_proj, o_proj)만, full은 거기에 MLP 3개(gate_proj, up_proj, down_proj)까지 붙인다. 즉 full이 훨씬 넓게, 많은 층에 어댑터를 붙여 더 공격적으로 학습한다는 뜻이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;=&amp;gt; 이게 나중에 lite와 full의 성능 차이로 이어진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. r (rank) &amp;mdash; 어댑터 행렬의 차원&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LoRA 행렬의 rank, 즉 차원 수다. lite는 32, full은 256.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;r이 클수록 어댑터의 표현력이 커지지만(더 많이 배울 수 있지만), 그만큼 파라미터가 늘고 과적합 위험도 커진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;lite와 full이 8배(32&amp;rarr;256) 차이 나는 게, full이 기본 base 모델에 &quot;더 큰 보정 장치&quot;(로라 어댑터) 를 다는 것과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. alpha &amp;mdash; 어댑터를 얼마나 세게 반영할지&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LoRA가 만든 변화(BA)를 원본 모델에 더할 때 얼마나 스케일업할지다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드에선 LORA_ALPHA = LORA_R * 2, 즉 &lt;b&gt;항상 r의 2배&lt;/b&gt;로 잡는다(lite=64, full=512).&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강사도 &quot;대부분 2배가 일반적&quot;이라고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4. quantization &amp;mdash; 정밀도를 낮춰 메모리 확보&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;베이스 모델의 가중치 정밀도를 낮춰(여기선 4비트) 메모리를 줄이는 것. 코드의 QUANT_4_BIT = True가 이걸 켠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이게 QLoRA의 &quot;Q&quot;이고, 글 15에서 6.4GB (16비트)&amp;rarr; 2.2GB(4비트 양자화)로 줄어드는 걸 실측했던 그 부분이다. &lt;br /&gt;(핵심은 &lt;b&gt;베이스 모델만 양자화하고, LoRA 어댑터 행렬은 그대로 둔다&lt;/b&gt;는 것.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;5. dropout &amp;mdash; LoRA가 데이터에 너무 딱 붙지 않게 막는 브레이크&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기가 이번 장에서 새로 판 부분이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강사 메모엔 그냥 &quot;10% 주로 사용, 일반화 가능성&amp;uarr;, 훈련시간&amp;uarr;&quot;으로만 적혀 있었는데, 이게 정확히 무슨 뜻인지 헷갈려서 더 파봤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1) dropout이 어디에 걸리나?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LoRA는 원본 출력 Wx에 보정 BAx를 더한다(Wx + BAx).&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;dropout은 여기서 &lt;b&gt;LoRA가 보는 입력 일부를 학습 중 랜덤하게 꺼버린다&lt;/b&gt;(Wx + BA(dropout(x)) 느낌).&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중요한 건 원본 모델 본체를 끄는 게 아니라 &lt;b&gt;LoRA 쪽 보정 경로에만&lt;/b&gt; 건다는 점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;=&amp;gt; 그래서 기본 모델의 원래 능력은 그대로 유지되고, LoRA가 특정 feature에만 과하게 맞춰지는 것만 막는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2) 왜 일반화가 좋아지나?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;dropout이 없으면 LoRA가 학습 데이터를 아주 빠르게 &quot;외워버린다&quot;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 데이터가 적거나 비슷한 패턴만 많으면 &quot;이 상황엔 무조건 이 표현&quot;처럼 좁게 적응한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;dropout을 넣으면 매번 일부 정보 없이도 버텨야 하니, 특정 신호 하나에 의존하지 못하고 더 분산된 방식으로 배운다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(강사의 비유) dropout 없음 = 시험 문제를 통째로 외워버리기 쉬움. dropout 있음 = 개념을 더 넓게 익히도록 강제.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3) &quot;훈련시간이 높아진다&quot;는 말의 진짜 의미.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 처음에 &quot;연산량이 늘어서 느려지나?&quot; 했는데 그게 아니었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;dropout 때문에 학습 신호가 조금 더 noisy해져서, 같은 성능에 도달하려면 &lt;b&gt;더 많은 step이 필요&lt;/b&gt;하고 loss가 더 천천히 안정된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;=&amp;gt; 한 스텝이 무거워지는 게 아니라, &lt;b&gt;학습이 덜 직진적이라 수렴이 느려지는 것&lt;/b&gt;에 가깝다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4) 왜 하필 0.1인가, 그리고 안 쓰기도 하나?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;0.1은 무난한 기본값이다(0.0은 빠르지만 과적합 위험, 0.2 이상은 데이터가 아주 작을 때나).&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흥미로운 건 &lt;b&gt;LoRA/QLoRA에서 dropout을 아예 0으로 두는 경우도 꽤 많다&lt;/b&gt;는 것.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;항상 필수가 아니라 &quot;정규화가 필요할 때 켜는 조절 장치&quot;에 가깝다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터가 많고 다양하면 0, 데이터가 적거나 형식이 반복적이면 0.05~0.1을 고려한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;=&amp;gt; 그래서 강사는 r&amp;middot;alpha&amp;middot;dropout을 &quot;기본 모델 위에 덧붙이는 작은 보정 장치&quot;로 묶어 이렇게 정리한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;r: 보정 장치의 크기&lt;/li&gt;
&lt;li&gt;alpha: 보정 장치의 증폭 정도&lt;/li&gt;
&lt;li&gt;dropout: 보정 장치가 너무 설치지 않게 일부러 랜덤 브레이크 거는 것&lt;/li&gt;
&lt;/ul&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;- QLoRA의 dropout은 &quot;LoRA가 학습 데이터에 너무 딱 붙지 않도록, 학습 중 일부 신호를 랜덤하게 끄는 정규화 장치&quot;다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지의 QLoRA 5개를 표로 묶으면 이렇다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;&lt;b&gt;QLoRA &lt;/b&gt;&lt;/span&gt;&lt;b&gt;하이퍼파라미터&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;무슨&lt;/b&gt;&lt;span&gt;&lt;b&gt; &lt;/b&gt;&lt;/span&gt;&lt;b&gt;뜻&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;&lt;b&gt;이번&lt;/b&gt;&lt;/span&gt;&lt;b&gt; &lt;/b&gt;&lt;span&gt;&lt;b&gt;코드&lt;/b&gt;&lt;/span&gt;&lt;b&gt; &lt;/b&gt;&lt;span&gt;&lt;b&gt;값&lt;/b&gt;&lt;/span&gt;&lt;b&gt;(lite / full)&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;target modules&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;어느&lt;span&gt; &lt;/span&gt;층에&lt;span&gt; &lt;/span&gt;어댑터를&lt;span&gt; &lt;/span&gt;붙일지&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;어텐션만&lt;span&gt; / &lt;/span&gt;어텐션&lt;span&gt;+MLP&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;r (rank)&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;어댑터&lt;span&gt; &lt;/span&gt;행렬의&lt;span&gt; &lt;/span&gt;차원&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;32 / 256&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;alpha&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;어댑터를&lt;span&gt; &lt;/span&gt;얼마나&lt;span&gt; &lt;/span&gt;세게&lt;span&gt; &lt;/span&gt;반영할지&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;64 / 512 (= r&amp;times;2)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;quantization&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;정밀도를&lt;span&gt; &lt;/span&gt;낮춰&lt;span&gt; &lt;/span&gt;메모리&lt;span&gt; &lt;/span&gt;확보&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;4&lt;/span&gt;비트&lt;span&gt; (&lt;/span&gt;동일&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;dropout&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;과적합&lt;span&gt; &lt;/span&gt;막는&lt;span&gt; &lt;/span&gt;랜덤&lt;span&gt; &lt;/span&gt;브레이크&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;0.1 (&lt;span&gt;동일&lt;/span&gt;)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;느낀 점&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;lite와 full의 차이가 결국 이 표에 다 들어 있다는 게 인상적이었다.&amp;nbsp;&lt;b&gt;더 넓은 층(target)에, 더 큰 어댑터(r)를, 더 세게(alpha)&lt;/b&gt;&amp;nbsp;&amp;mdash; full은 이 세 개를 전부 키운 설정이다. &quot;모델 크기가 아니라 어댑터 설정으로 학습 규모를 조절한다&quot;는 QLoRA의 감각이 여기서 잡혔다.&lt;/li&gt;
&lt;li&gt;&lt;span&gt;dropout&lt;/span&gt;이&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;데이터가&lt;span&gt; &lt;/span&gt;적을&lt;span&gt; &lt;/span&gt;때&lt;span&gt; &lt;/span&gt;켜는&lt;span&gt; &lt;/span&gt;안전장치&lt;span&gt;&quot;&lt;/span&gt;라는&lt;span&gt; &lt;/span&gt;것&lt;span&gt;, &lt;/span&gt;그리고&lt;span&gt; &lt;/span&gt;실무에선&lt;span&gt; 0&lt;/span&gt;으로도&lt;span&gt; &lt;/span&gt;자주&lt;span&gt; &lt;/span&gt;둔다는&lt;span&gt; &lt;/span&gt;게&lt;span&gt; &lt;/span&gt;의외였다&lt;span&gt;. 데이터 양에 따라 조절을 해나가는게 인상적이었다&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;dropout의 개념은 꼭 모델학습이 아니라 에이전트 품질 개선 작업에서도 쓸 수 있는 기법이라고 생각한다. 개선해야하는 테스트 케이스들이 있을 때 과적합 막기 위해 일부는 Dropout 해버리고 적용하는 방식도 써먹을 수 있지 않을까 라는 생각을 하게 됐다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 학습은 실제로 어떻게 일어나는가 &amp;mdash; 학습 4단계와 backpropagation&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞의 두 장은 &quot;학습과정에서 세부적으로 어떤 세팅값을 넣을지&quot;(하이퍼파라미터)를 봤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 장은 그 값들을 넣고 train()을 눌렀을 때 &lt;b&gt;모델 안에서 실제로 무슨 일이 일어나는지에 대해 소개하려 한다&lt;/b&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강사는 학습을 이렇게 한 문장으로 정의한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;tweaking the parameters of a model based on training data in a way that should generalize to unseen data&lt;/p&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;①파라미터를 조정한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;②처음 보는 데이터에도 잘하게 만든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 학습은 &quot;훈련 데이터 정답 맞히기 게임&quot;이 아니라 일반화를 만드는 것&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 앞 장에서 소개한 학습파라미터인 dropout&amp;middot;epochs 같은 게 다 이 &quot;일반화&quot;를 지키려는 장치였던 거다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;먼저 짚고 갈 것 &amp;mdash; 파라미터 vs 하이퍼파라미터&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4단계로 들어가기 전에 이 구분을 깔끔히 하면 전체가 쉬워진다.&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;파라미터(parameter)&lt;/b&gt;: 모델이 학습 중에&amp;nbsp;&lt;b&gt;직접 바꾸는 값&lt;/b&gt;. 신경망 가중치, bias, 그리고 QLoRA에선 특히&amp;nbsp;&lt;b&gt;LoRA A/B 행렬&lt;/b&gt;. &lt;br /&gt;=&amp;gt; 학습의&amp;nbsp;대상.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;하이퍼파라미터(hyperparameter)&lt;/b&gt;: 학습 전에&amp;nbsp;&lt;b&gt;사람이 미리 정해두는 설정값&lt;/b&gt;. learning rate, batch size, epochs, r, alpha, dropout&amp;hellip; =&amp;gt; 파라미터를&amp;nbsp;어떻게&lt;i&gt; &lt;/i&gt;바꿀지&lt;i&gt; &lt;/i&gt;정하는&lt;i&gt; &lt;/i&gt;규칙.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 줄로:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;파라미터 = 모델이 배우는 값, 하이퍼파라미터 = 사람이 학습 방식을 정하는 값.&lt;/b&gt; (앞의 1&amp;middot;2장에서 다룬 10개가 전부 하이퍼파라미터였다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;학습의 4단계 &amp;mdash; &quot;답안지 고치기&quot;로 보면&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강사는 학습 한 스텝을 4단계로 쪼개고, &lt;b&gt;시험 답안지 고치는 과정&lt;/b&gt;에 빗대준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1) forward pass &amp;mdash; 답안지 제출&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 파라미터 상태로 입력을 넣고 예측을 만든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입력에서 첫 레이어 &amp;rarr; 다음 레이어 &amp;rarr; &amp;hellip; &amp;rarr; 출력으로 정보가 앞&lt;i&gt; &lt;/i&gt;방향으로 흐른다고 해서 &quot;forward&quot;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;=&amp;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;QLoRA에선 출력이&amp;nbsp;Wx + &amp;Delta;Wx로 만들어진다(&amp;Delta;W = (&amp;alpha;/r)BA). 즉 베이스 모델의 원래 계산에, LoRA가 만든 보정이 alpha 스케일을 거쳐 더해진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2) loss calculation &amp;mdash; 채점&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예측이 정답과 얼마나 다른지 숫자로 계산한다. 거의 맞으면 loss 가 작고, 많이 틀리면 loss 가크다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;=&amp;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;중요한 건 loss가 단순 정오판정(맞음/틀림)이 아니라는 것.&amp;nbsp;&lt;b&gt;얼마나 자신 있게 틀렸는지&lt;/b&gt;까지 반영하는 연속적인 점수다. &lt;br /&gt;&quot;애매하게 틀림&quot;과 &quot;확신하며 틀림&quot;은 loss가 다르다. (이 감각이 6장 cross-entropy로 이어진다.)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3) backward pass &amp;mdash; 빨간펜으로 원인 분석&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;loss를 줄이려면 각 파라미터를 &lt;b&gt;어느 방향으로 얼마나&lt;/b&gt; 바꿔야 하는지(gradient)를 계산한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;forward가 입력&amp;rarr;출력이었다면, backward는 반대로 &lt;b&gt;출력의 오차(loss)에서 시작해 거꾸로&lt;/b&gt; 앞 레이어로 영향을 전파한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;=&amp;gt; 채점이 끝난 답안지에 &lt;b&gt;&quot;여길 이렇게 바꾸면 점수가 오른다&quot;를 빨간펜으로 분석하는 단계.&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;QLoRA에선 베이스 모델은 frozen이고&amp;nbsp;&lt;b&gt;LoRA 행렬 A&amp;middot;B에 대한 gradient만&lt;/b&gt;&amp;nbsp;계산&amp;middot;저장한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4) optimization &amp;mdash; 연필로 실제 수정&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;backward에서 구한 gradient로 파라미터를 실제로 조금 수정한다. 드디어 모델이 바뀐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;=&amp;gt; 분석대로 &lt;b&gt;연필로 답안지를 조금 고치는 단계.&lt;/b&gt; 여기서 1장의 optimizer(걸음걸이)와 learning rate(보폭)가 등장한다. &lt;br /&gt;SGD로 쓰면 이 한 줄이다.&lt;/p&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;theta; = &amp;theta; &amp;minus; &amp;eta;&amp;middot;&amp;nabla;L&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;# &amp;theta;: 현재 파라미터, &amp;nabla;L: gradient(방향), &amp;eta;: learning rate(보폭)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 네 단계를 실제 예로 이어보면:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입력&lt;span&gt;&amp;nbsp; &lt;/span&gt;&quot;한국의 수도는&quot; &lt;span&gt;&amp;nbsp; &lt;/span&gt;/&lt;span&gt;&amp;nbsp; &lt;/span&gt;정답 &quot;서울&quot;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1) forward &lt;span&gt;&amp;nbsp; &lt;/span&gt;: 모델이 &quot;부산&quot;이라고 예측&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2) loss&lt;span&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;/span&gt;: 정답 &quot;서울&quot;과 차이 &amp;rarr; loss 큼&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3) backward&lt;span&gt;&amp;nbsp; &lt;/span&gt;: 왜 틀렸는지 거꾸로 추적 &amp;rarr; (LoRA 행렬의) gradient 계산&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4) optimize&lt;span&gt;&amp;nbsp; &lt;/span&gt;: optimizer가 LoRA 행렬을 아주 조금 수정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;/span&gt;&amp;rarr; 다음번엔 &quot;서울&quot; 확률이 조금 더 높아지도록&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이걸 수천&amp;middot;수만 번 반복하는 게 학습이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;QLoRA에서도 이 흐름은 똑같고, 단지 업데이트 대상이 베이스 모델 전체가 아니라 LoRA 행렬이라는 점만 다르다.&lt;/b&gt;&lt;/p&gt;
&lt;p 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;backward pass &amp;ne; 업데이트. backward는 gradient&amp;nbsp;계산, optimization이 실제&amp;nbsp;적용. (계산과 적용은 다르다.)&lt;/li&gt;
&lt;li&gt;loss &amp;ne; gradient. loss는 &quot;지금 틀린 정도&quot;, gradient는 &quot;그래서 어디로 얼마나 고칠지&quot;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;backward pass를 한 겹 더 &amp;mdash; gradient는 &quot;책임 점수&quot;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3단계(backward)가 가장 헷갈려서 여기만 더 팠다. gradient가 대체 뭔지, 왜 파라미터마다 크기가 다른지.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;gradient = &quot;이 파라미터를 조금 바꾸면 loss가 얼마나, 어느 방향으로 변하나&quot;&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;부호가&amp;nbsp;&lt;b&gt;양수&lt;/b&gt;면: 그 값을 키우면 loss가 커지니 &amp;rarr; 줄이는 게 낫다&lt;/li&gt;
&lt;li&gt;부호가&amp;nbsp;&lt;b&gt;음수&lt;/b&gt;면: 그 값을 키우면 loss가 줄어드니 &amp;rarr; 늘리는 게 낫다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;절댓값이 크면&lt;/b&gt;&amp;nbsp;loss에 영향이 큼, 작으면 영향이 작음&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아주 단순한 예(ŷ = wx, L = (wx&amp;minus;y)&amp;sup2;)로 손으로 계산해보면 감이 온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;x=2, y=10, w=3이면 예측 ŷ=6, loss (6&amp;minus;10)&amp;sup2;=16. 이걸 w로 미분하면:&lt;/p&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;part;L/&amp;part;w = 2(wx &amp;minus; y)&amp;middot;x = 2(6 &amp;minus; 10)&amp;middot;2 = &amp;minus;16&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;gradient가 &amp;minus;16이라는 건 =&amp;gt; &quot;w를 키우면 loss가 줄어든다(음수), 그것도 꽤 크게(절댓값 16)&quot;라는 뜻.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 예측 6을 정답 10에 가깝게 만들려면 w를 키워야 하니 맞다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 실제 모델은 층이 여러 개라 loss가 파라미터에 직접 안 붙어 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 &lt;b&gt;연쇄법칙(chain rule)&lt;/b&gt; 으로 쪼개 계산한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;loss가 중간값 a를 거쳐 w에 연결돼 있다면:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;dL/dw = (dL/da) &amp;times; (da/dw)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;# &quot;loss가 중간값에 얼마나 민감한지&quot; &amp;times; &quot;중간값이 파라미터에 얼마나 민감한지&quot;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;딥러닝 프레임워크가 이걸 출력층부터 입력층까지 층층이 자동으로 해준다. 이게 backpropagation이다.&lt;/p&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(강사의 비유) 공장에서 최종 제품이 불량이 났다고 하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;forward pass = 원재료 투입 &amp;rarr; 공정1 &amp;rarr; 공정2 &amp;rarr; 공정3 &amp;rarr; 최종 제품.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;backpropagation = 최종 제품이 왜 불량인지 보고, 공정3의 책임은 얼마인지, 공정2는, 공정1은&amp;hellip; 이렇게 &lt;b&gt;거꾸로 책임을 배분&lt;/b&gt;하는 것.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 공정 담당자가 다음번엔 얼마나 수정해야 할지를 정하는 셈.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;=&amp;gt; 그래서 gradient를 아주 직관적으로 말하면 &lt;b&gt;&quot;너를 조금 바꾸면 loss가 얼마나 달라질까?&quot;에 대한 책임 점수&lt;/b&gt;다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;책임이 크면 gradient가 크고, 작으면 작다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파라미터마다 gradient 크기가 다른 이유도 결국 &quot;책임&quot;으로 설명된다(강사가 든 이유들 중 핵심만):&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;출력에 더 직접적으로 큰 영향을 준 파라미터일수록 gradient가 크다&lt;/li&gt;
&lt;li&gt;이번 입력 샘플에 많이 관여한 파라미터일수록 크다 (gradient는 현재 배치 데이터에 대한&amp;nbsp;책임도를 반영)&lt;/li&gt;
&lt;li&gt;이미 잘 맞고 있는 부분은 굳이 고칠 필요가 없으니 gradient가 작다&lt;/li&gt;
&lt;li&gt;위 단순식에서 봤듯 gradient는 오차뿐 아니라&amp;nbsp;&lt;b&gt;입력 활성값 x에도 비례&lt;/b&gt;한다 (2(wx&amp;minus;y)&amp;middot;x) &amp;mdash; 같은 오차여도 입력이 크면 gradient가 크다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;QLoRA에서도 원리는 똑같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 토큰 관계를 더 잘 잡아야 하는 샘플에선 그쪽 LoRA 파라미터의 gradient가 크게 나오고, 아닌 경우 작다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;다만 업데이트 대상이 LoRA 쪽으로 제한돼 있을 뿐.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 줄 압축: &lt;b&gt;forward는 답 제출, loss는 채점, backpropagation은 오답 원인 추적, optimization은 실제 수정.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;느낀 점&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&quot;gradient = 책임 점수&quot;라는 프레임이 나한테 제일 크게 남았다. 그동안 backprop을 많이 들어보기만 하고 원리는 잘 몰랐는데, 미분 chain rule을 이용해서 거꾸로 전파하는 프레임을 보며 이해가 갔던 거 같다. &quot;이번 오답에 네가 얼마나 책임 있냐&quot;로 바꿔 생각하니 이해가 조금 더 됐던 거 같다.&lt;/li&gt;
&lt;li&gt;학생 시절 배웠던 미분의 개념이 실제로 이런식으로 쓰이게 되는 구나를 배운 느낌이었달까&amp;nbsp;&lt;/li&gt;
&lt;li&gt;학습 단계에 대해서도 좀 더 깊게 배워볼 수 있어 의미있었다&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. SFTTrainer로 실제 학습 돌리기 &amp;mdash; 그리고 wandb 곡선 읽기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개념은 여기까지. 이제 이 모든 걸 코드로 돌린다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좋은 소식은, 앞에서 그렇게 길게 뜯어본 forward &amp;rarr; loss &amp;rarr; backward &amp;rarr; optimize의 반복이 실제 코드에선 &lt;b&gt;SFTTrainer가 다 대신해준다&lt;/b&gt;는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 할 일은&lt;b&gt; &quot;무엇을&amp;middot;어떻게 학습할지&quot;&lt;/b&gt;만 설정 객체 두 개로 넘겨주는 거다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 흐름은 이렇다.&lt;/p&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;rarr; 4비트 양자화로 베이스 모델 로드 &amp;rarr; LoRA 어댑터 부착&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; SFTTrainer로 학습 &amp;rarr; 매 SAVE_STEPS마다 HuggingFace Hub에 업로드&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; wandb로 학습 곡선 추적&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4비트로 양자화한 모델을 메모리에 올린다&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;글 15에서 본 그 양자화 설정 그대로, 베이스 모델을 4비트로 로드한다.&lt;/p&gt;
&lt;pre id=&quot;code_1783754838052&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;base_model = AutoModelForCausalLM.from_pretrained(

    BASE_MODEL, quantization_config=quant_config, device_map=&quot;auto&quot;,

)

print(f&quot;Memory footprint: {base_model.get_memory_footprint() / 1e6:.1f} MB&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Memory footprint: 2197.6 MB &lt;span&gt;&amp;nbsp; &lt;/span&gt;# 약 2.2GB &amp;mdash; 30억 파라미터 모델이 이 크기로&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;30억 파라미터 모델이 2.2GB로 줄어 colab에서 제공하는 T4 무료 GPU에도 올라간다. &lt;br /&gt;(참고로 내 실행은 T4라 use_bf16=False로 잡혀서 계산은 fp16으로 돌아갔다. A100이면 더 안정적인 bf16을 쓴다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;설정 객체 ①: LoraConfig &amp;mdash; &quot;어떤 어댑터를&quot;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2장에서 본 그 LoraConfig다. 여기에 눈여겨볼 인자 두 개만 더 짚으면:&lt;/p&gt;
&lt;pre id=&quot;code_1783754937187&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;lora_parameters = LoraConfig(

    lora_alpha=LORA_ALPHA, lora_dropout=LORA_DROPOUT, r=LORA_R,

    bias=&quot;none&quot;,            # bias 파라미터는 학습 안 함

    task_type=&quot;CAUSAL_LM&quot;,  # 인과적 언어모델 = 다음 토큰 예측

    target_modules=TARGET_MODULES,

)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;task_type=&quot;CAUSAL_LM&quot;이 이 학습이 &quot;다음 토큰 예측&quot;임을 알려주고, bias=&quot;none&quot;으로 bias는 건드리지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;설정 객체 ②: SFTConfig &amp;mdash; &quot;어떻게&quot;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기가 실제 학습 옵션이 다 모인 곳이다. 인자가 많은데, 그냥 나열하면 안 와닿아서 &lt;b&gt;&quot;왜 넣는지&quot;&lt;/b&gt; 위주로 핵심만 추렸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1783754982618&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;train_parameters = SFTConfig(

    num_train_epochs=EPOCHS,

    per_device_train_batch_size=BATCH_SIZE,

    per_device_eval_batch_size=1,      # 평가 배치는 1 (메모리 절약)

    learning_rate=LEARNING_RATE,

    lr_scheduler_type=LR_SCHEDULER_TYPE,  # cosine

    warmup_ratio=WARMUP_RATIO,         # 초반 1% 동안 lr을 0&amp;rarr;1e-4로 서서히

    optim=OPTIMIZER,                   # paged_adamw_32bit

    max_grad_norm=0.3,                 # 그래디언트 클리핑 &amp;mdash; gradient 폭주 방지

    group_by_length=True,              # 비슷한 길이끼리 배치 &amp;rarr; 패딩 낭비 &amp;darr;

    max_length=MAX_SEQUENCE_LENGTH,    # 128토큰으로 자름

    fp16=not use_bf16, bf16=use_bf16,  # GPU에 따라 정밀도 자동 선택

    eval_strategy=&quot;steps&quot;, eval_steps=SAVE_STEPS,  # N스텝마다 평가

    save_steps=SAVE_STEPS, save_total_limit=10,    # 체크포인트 주기/최대 개수

    push_to_hub=True, hub_strategy=&quot;every_save&quot;,   # 체크포인트마다 Hub 업로드

    hub_model_id=HUB_MODEL_NAME, hub_private_repo=True,

)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 중에서 내가 &quot;아, 이래서 넣는구나&quot; 했던 것들:&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;max_grad_norm=0.3&amp;nbsp;(그래디언트 클리핑)&lt;/b&gt;: 3장에서 gradient가 &quot;움직이는 방향&quot;이라 했는데, 그럼 한 스텝에 파라미터가 확 튀어(=산에서 반대편으로 튀는 것) 학습이 망가진다. 그래서 gradient 크기에 상한을 씌운다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;group_by_length=True&lt;/b&gt;: 길이가 비슷한 샘플끼리 배치로 묶으면 패딩(빈칸 채우기) 토큰이 줄어 학습이 빨라진다. 짧은 문장과 긴 문장을 한 배치에 넣으면 짧은 쪽을 긴 쪽 길이에 맞춰 낭비하게 되니까.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;push_to_hub=True&amp;nbsp;+&amp;nbsp;hub_strategy=&quot;every_save&quot;&lt;/b&gt;: 이게 은근히 중요했다. Colab 무료 세션은 언제든 끊긴다. 매 저장 시점마다 어댑터를 HuggingFace Hub에 올려두면, 끊겨도 마지막 체크포인트에서&amp;nbsp;&lt;b&gt;재개&lt;/b&gt;할 수 있다. (강사도 &quot;재개하려면 모델을&amp;nbsp;is_trainable=True로 불러오는 게 트릭&quot;이라고 안내한다.)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;optim=&quot;paged_adamw_32bit&quot;&lt;/b&gt;: 1장에서 본 AdamW인데, optimizer 상태(모멘텀 등)를 GPU&amp;harr;CPU로 페이징해 메모리 초과(OOM)를 피한다. T4 같은 작은 GPU에선 사실상 필수.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;설정 객체 ③: SFTTrainer &amp;mdash; 두 개를 합쳐 학습 시작&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 이 두 설정과 데이터를 SFTTrainer에 넘긴다.&lt;/p&gt;
&lt;pre id=&quot;code_1783755117802&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fine_tuning = SFTTrainer(

    model=base_model,            # 4비트 양자화된 베이스 모델

    train_dataset=train, eval_dataset=val,

    peft_config=lora_parameters, # ★ 이걸 주면 자동으로 LoRA 어댑터 부착

    args=train_parameters,

)

fine_tuning.train()&lt;/code&gt;&lt;/pre&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;p data-ke-size=&quot;size16&quot;&gt;여기서 마법은 peft_config=lora_parameters 한 줄이다. 이걸 넘기면 SFTTrainer가 알아서:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;베이스 모델을&amp;nbsp;&lt;b&gt;얼리고(freeze)&lt;/b&gt;,&lt;/li&gt;
&lt;li&gt;지정한 모듈(q_proj&amp;nbsp;등)에&amp;nbsp;&lt;b&gt;LoRA 어댑터를 끼워넣고&lt;/b&gt;,&lt;/li&gt;
&lt;li&gt;&lt;b&gt;어댑터만 학습&lt;/b&gt;시킨다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3장에서 손으로 뜯어본 4단계(forward/loss/backward/optimize)를, .train() 한 줄이 매 배치마다 알아서 돌리는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;=&amp;gt; 개념을 알고 나니 이 한 줄이 &quot;블랙박스&quot;가 아니라 &quot;그 반복을 돌리는 버튼&quot;으로 보인다.&lt;/p&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;mdash; &lt;b&gt;&quot;4비트로 얼린 모델을 어떻게 학습하지?&quot;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;답은 베이스&lt;i&gt; &lt;/i&gt;가중치는&lt;i&gt; &lt;/i&gt;학습&lt;i&gt; &lt;/i&gt;안&lt;i&gt; &lt;/i&gt;한다는 것.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;베이스 W는 4비트로 얼려두고 forward 때만 잠깐 fp16/bf16으로 풀어 계산하며, &lt;b&gt;학습 가능한 건 LoRA 어댑터 A&amp;middot;B뿐&lt;/b&gt;(이건 풀 정밀도로 따로 보관).&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 gradient도 어댑터에만 흐른다. 강사 말대로 이게 &quot;LLM을 작은 메모리의 GPU에서도 학습시킬 수 있는 QLoRA의 묘미&quot;다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;wandb 학습 곡선 읽기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;학습이 도는 동안 wandb가 곡선을 그려준다(모델 자체는 안 올리고 loss&amp;middot;lr&amp;middot;평가 지표만: WANDB_LOG_MODEL=&quot;false&quot;, WANDB_WATCH=&quot;false&quot;).&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내 lite 실행 기준 &amp;mdash; &lt;b&gt;데이터 2만 개, 배치 32 &amp;rarr; 한 에폭에 625 스텝&lt;/b&gt;(20000 &amp;divide; 32 &amp;asymp; 625)이었고, 약 1시간 13분 걸렸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;학습이 진행되며 찍힌 값은 이랬다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Step&lt;span&gt;&amp;nbsp; &lt;/span&gt;Training Loss&lt;span&gt;&amp;nbsp; &lt;/span&gt;Validation Loss&lt;span&gt;&amp;nbsp; &lt;/span&gt;Mean Token Accuracy&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;100&lt;span&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;/span&gt;1.303 &lt;span&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;/span&gt;1.2918&lt;span&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;/span&gt;0.757&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;200&lt;span&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;/span&gt;1.265 &lt;span&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;/span&gt;1.2722&lt;span&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;/span&gt;0.760&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;600&lt;span&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;/span&gt;1.265 &lt;span&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;/span&gt;1.2483&lt;span&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;/span&gt;(&amp;mdash;)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;1039&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/RKgP8/dJMcadvPOxQ/mSLMeIX72KZyTkWqSi4enK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/RKgP8/dJMcadvPOxQ/mSLMeIX72KZyTkWqSi4enK/img.png&quot; data-alt=&quot;625스텝(배치 32 &amp;amp;times; 데이터 2만)을 도는 동안 train loss는 완만히 내려가고, learning rate는 0.0001 부근을 유지한다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/RKgP8/dJMcadvPOxQ/mSLMeIX72KZyTkWqSi4enK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRKgP8%2FdJMcadvPOxQ%2FmSLMeIX72KZyTkWqSi4enK%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;2048&quot; height=&quot;1039&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;1039&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;625스텝(배치 32 &amp;times; 데이터 2만)을 도는 동안 train loss는 완만히 내려가고, learning rate는 0.0001 부근을 유지한다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&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;p 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;train loss&lt;/b&gt;가 1.303 &amp;rarr; 1.236 근처로 내려간다(노이즈는 있지만 추세는 하락). 모델이 실제로 배우고 있다는 뜻.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;validation loss&lt;/b&gt;도 1.2918 &amp;rarr; 1.2483로 꾸준히 내려간다. &lt;br /&gt;=&amp;gt; 훈련 데이터만 외운 게 아니라 처음 보는 데이터(val)에도 잘하고 있다는 신호. 아직 과적합 조짐이 없다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;mean token accuracy&lt;/b&gt;가 0.757 &amp;rarr; 0.763으로 오른다.&lt;/li&gt;
&lt;li&gt;learning rate는 cosine 스케줄이라 초반 워밍업 뒤 0.0001 부근을 유지하며 서서히 내려간다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;학습이 끝나면 어댑터가 Hub에 올라간다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;adapter_model.safetensors: 100%&lt;span&gt;&amp;nbsp; &lt;/span&gt;73.4MB / 73.4MB&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Saved to the hub: price-2026-05-01_...-lite&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-07-11 오후 4.36.46.png&quot; data-origin-width=&quot;2756&quot; data-origin-height=&quot;1276&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lBYMI/dJMcahZm3sO/o8eKhcbTCruun5lWPvm9E0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lBYMI/dJMcahZm3sO/o8eKhcbTCruun5lWPvm9E0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lBYMI/dJMcahZm3sO/o8eKhcbTCruun5lWPvm9E0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlBYMI%2FdJMcahZm3sO%2Fo8eKhcbTCruun5lWPvm9E0%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;2756&quot; height=&quot;1276&quot; data-filename=&quot;스크린샷 2026-07-11 오후 4.36.46.png&quot; data-origin-width=&quot;2756&quot; data-origin-height=&quot;1276&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 &lt;b&gt;73.4MB&lt;/b&gt;가 인상적이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;30억 파라미터 모델을 파인튜닝했는데, 결과물로 저장되는 건 어댑터 73MB짜리 파일 하나다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;베이스 모델(라마 3.2)은 건드리지 않았으니 저장할 필요도 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;=&amp;gt; &quot;학습 결과 = 작은 어댑터 파일 하나&quot;라는 게 LoRA에서 일어나는 일이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;느낀 점&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;그렇게 개념을 길게 팠는데 정작 학습 코드는 SFTTrainer(...) + .train()이 전부였다. 라이브러리가 4단계 반복을 대신 돌려주니, 내가 신경 쓸 건 &quot;무엇을&amp;middot;어떻게(설정 두 개)&quot;뿐이라는 것. =&amp;gt; 전직장에서 있던 서비스인 노코드 LLMOps 툴의 학습 세팅 화면이 왜 결국 &quot;데이터 + 하이퍼파라미터 몇 개&quot; 입력폼으로 수렴하는지 이제 이해가 됐다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;val loss&lt;/span&gt;가&lt;span&gt; &lt;/span&gt;같이&lt;span&gt; &lt;/span&gt;내려가는&lt;span&gt; &lt;/span&gt;걸&lt;span&gt; &lt;/span&gt;눈으로&lt;span&gt; &lt;/span&gt;확인하니&lt;span&gt;, &lt;/span&gt;앞&lt;span&gt; &lt;/span&gt;장에서&lt;span&gt; &lt;/span&gt;배운&lt;span&gt; &quot;&lt;/span&gt;일반화&lt;span&gt;&quot;&lt;/span&gt;가&lt;span&gt; &lt;/span&gt;추상적&lt;span&gt; &lt;/span&gt;개념이&lt;span&gt; &lt;/span&gt;아니라&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;실제로&lt;/b&gt;&lt;span&gt;&lt;b&gt; &lt;/b&gt;&lt;/span&gt;&lt;b&gt;지켜보며&lt;/b&gt;&lt;span&gt;&lt;b&gt; &lt;/b&gt;&lt;/span&gt;&lt;b&gt;판단하는&lt;/b&gt;&lt;span&gt;&lt;b&gt; &lt;/b&gt;&lt;/span&gt;&lt;b&gt;지표&lt;/b&gt;라는&lt;span&gt; &lt;/span&gt;게&lt;span&gt; &lt;/span&gt;와닿았다&lt;span&gt;. &lt;/span&gt;그리고&lt;span&gt; &lt;/span&gt;이&lt;span&gt; val loss&lt;/span&gt;가&lt;span&gt; &lt;/span&gt;다음&lt;span&gt; &lt;/span&gt;장의&lt;span&gt; &lt;/span&gt;주인공이다&lt;span&gt; =&amp;gt;&amp;nbsp; lite 모드로 1epoch를 돌릴때가 아니라, full &lt;/span&gt;모드로&lt;span&gt; 3epoch&lt;/span&gt;을&lt;span&gt; &lt;/span&gt;돌리면&lt;span&gt; &lt;/span&gt;이게&lt;span&gt; &lt;/span&gt;어느&lt;span&gt; &lt;/span&gt;순간&lt;span&gt; &lt;/span&gt;다시&lt;span&gt; &lt;/span&gt;올라가기&lt;span&gt; &lt;/span&gt;시작하니까&lt;span&gt;.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 오버피팅 관찰기 &amp;mdash; train loss가 좋아 보일 때가 더 위험하다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞 장 끝에서 예고한 그 이야기다. 내가 돌린 lite는 &lt;b&gt;1에폭&lt;/b&gt;이라 val loss가 내려가기만 하고 끝났다 -&amp;gt; 과적합을 볼 기회 자체가 없었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 이 장은 강사가 &lt;b&gt;full 모드로 3에폭&lt;/b&gt; 돌린 곡선을 기준으로 본다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(같은 코드에서 LITE_MODE=False로 두면 데이터 80만&amp;middot;배치 256&amp;middot;3에폭이 된다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(나도 colab에서 A100 이용권을 사서 Full 모드로 돌리려 했으나, 메모리가 초과되어서 실패했다..ㅎ)&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;먼저: train loss가 초반에 급락한다&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;full 곡선을 보면 train loss가 시작하자마자 뚝 떨어진다. 이건 좋은 신호라기보다 &lt;b&gt;태스크가 쉬워서&lt;/b&gt;다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 학습의 정답은 &quot;219.00&quot; 같은 짧은 숫자 하나.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모델이 &quot;숫자를 예측한 다음 끝(끝 토큰)을 내면 된다&quot;는 형식을 금방 익히기 때문에, 형식만 맞춰도 loss가 확 준다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1492&quot; data-origin-height=&quot;742&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b4hCWL/dJMcagM2fNZ/R7Xp6eHdlARnWpEozkKv5k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b4hCWL/dJMcagM2fNZ/R7Xp6eHdlARnWpEozkKv5k/img.png&quot; data-alt=&quot;train loss는 초반에 급락한다 &amp;amp;mdash; 모델이 '숫자 하나 + 끝' 형식을 금방 익히기 때문&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b4hCWL/dJMcagM2fNZ/R7Xp6eHdlARnWpEozkKv5k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb4hCWL%2FdJMcagM2fNZ%2FR7Xp6eHdlARnWpEozkKv5k%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;1492&quot; height=&quot;742&quot; data-origin-width=&quot;1492&quot; data-origin-height=&quot;742&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;train loss는 초반에 급락한다 &amp;mdash; 모델이 '숫자 하나 + 끝' 형식을 금방 익히기 때문&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;853&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dK9ca3/dJMcaiqslZG/KLMgfgJV11M2kFDEiO3EkK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dK9ca3/dJMcaiqslZG/KLMgfgJV11M2kFDEiO3EkK/img.png&quot; data-alt=&quot;노이즈를 걷어내고 스무딩하면 추세가 또렷하다. wandb의 스무딩 슬라이더로 조절할 수 있다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dK9ca3/dJMcaiqslZG/KLMgfgJV11M2kFDEiO3EkK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdK9ca3%2FdJMcaiqslZG%2FKLMgfgJV11M2kFDEiO3EkK%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;2048&quot; height=&quot;853&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;853&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;노이즈를 걷어내고 스무딩하면 추세가 또렷하다. wandb의 스무딩 슬라이더로 조절할 수 있다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로 &lt;b&gt;full(80만 개) 곡선이 lite보다 더 매끄러워 보이는 건 배치가 더 크기 때문&lt;/b&gt;이다(256 vs 32). 1장에서 본 그 얘기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;큰 배치는 여러 샘플의 평균으로 움직이니 한 스텝 한 스텝이 덜 튄다. =&amp;gt; 곡선의 매끈함 정도에도 batch size가 그대로 드러난다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;진짜 봐야 할 건 train loss가 아니라 eval loss&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 3에폭째다. train loss가 2에폭 &amp;rarr; 3에폭 구간에서 &lt;b&gt;너무 크게&lt;/b&gt; 떨어진다. 왜 하필 여기서 급락할까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 &lt;b&gt;에폭이 바뀌어도 데이터는 같은 데이터&lt;/b&gt;라는 점이다(1장에서 본 것 &amp;mdash; 순서&amp;middot;조 편성만 셔플될 뿐).&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러니 두 번, 세 번 본 데이터에 대해 모델이 예측을 잘하게 되는 건 당연하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;=&amp;gt; 즉 3에폭째 train loss 급락은 &quot;더 똑똑해졌다&quot;가 아니라 &lt;b&gt;본 문제집을 외우기 시작했다&lt;/b&gt;는 신호에 가깝다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이게 문제인 이유: 훈련 데이터를 외우면 정작 &lt;b&gt;처음 보는 데이터(unseen)에선 잘 못할 수 있다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 train loss만 봐선 안 되고, 학습 중 따로 떼둔 검증셋으로 재는 &lt;b&gt;eval loss&lt;/b&gt;를 봐야 한다(이 코드에선 일정 스텝마다 평가한다).&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 eval loss 곡선에서 결정적인 장면이 나온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;eval loss가 저점 &lt;b&gt;1.123&lt;/b&gt;까지 내려갔다가, 어느 순간 &lt;b&gt;1.283&lt;/b&gt;까지 확 올라가는 구간이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바로 이 구간이 오버핏이 일어나는 지점이고, &lt;b&gt;세 번째 에폭에서 (unseen 데이터에 대한) 성능이 오히려 줄어든다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;train loss는 계속 내려가는데 eval loss는 다시 올라간다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;=&amp;gt; 이 &quot;엇갈림&quot;이 과적합의 교과서적 신호다. 훈련 데이터엔 점점 잘 맞는데 새 데이터엔 오히려 나빠지는 것.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;그래서 결론: &quot;현재 상황에선 2에폭이 생산적&quot;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강사의 결론은 담백하다. &lt;b&gt;생산적인 건 2에폭까지&lt;/b&gt;고, 3에폭째는 오히려 성능을 깎아먹는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;완화하려면 앞서 2장에서 본 &lt;b&gt;dropout을 올려보는 것&lt;/b&gt;도 방법이다(LoRA가 데이터에 너무 딱 붙는 걸 막으니까).&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 3에폭째 과적합이 좀 줄어들 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;=&amp;gt; 여기서 1&amp;middot;2장이 다시 연결된다. epochs를 몇으로 둘지, dropout을 얼마로 둘지가 추상적인 &quot;설정값&quot;이 아니라, &lt;b&gt;이 eval loss 곡선을 보고 조정하는 실제 조정장치&lt;/b&gt;였던 거다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;느낀 점&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&quot;train loss가 좋아지는데 안심하면 안 된다&quot;는 게 이 장의 교훈이었다. 오히려 train loss만 예쁘게 떨어질 때가 가장 위험한 순간(외우는 중)일 수 있다는 것. =&amp;gt;&amp;nbsp; &quot;학습 데이터에서의 성적&quot;과 &quot;처음 보는 데이터에서의 성적&quot;은 다르고, 후자를 봐야 한다.&lt;/li&gt;
&lt;li&gt;그리고&lt;span&gt; &lt;/span&gt;이&lt;span&gt; &lt;/span&gt;판단을&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;눈으로&lt;/b&gt;&lt;span&gt;&lt;b&gt; &lt;/b&gt;&lt;/span&gt;&lt;b&gt;곡선&lt;/b&gt;&lt;span&gt;&lt;b&gt; &lt;/b&gt;&lt;/span&gt;&lt;b&gt;보며&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;한다는&lt;span&gt; &lt;/span&gt;게&lt;span&gt; &lt;/span&gt;인상적이었다&lt;span&gt;. &quot;&lt;/span&gt;몇&lt;span&gt; &lt;/span&gt;에폭이&lt;span&gt; &lt;/span&gt;정답&lt;span&gt;&quot;&lt;/span&gt;이라는&lt;span&gt; &lt;/span&gt;공식이&lt;span&gt; &lt;/span&gt;있는&lt;span&gt; &lt;/span&gt;게&lt;span&gt; &lt;/span&gt;아니라&lt;span&gt;, eval loss&lt;/span&gt;가&lt;span&gt; &lt;/span&gt;반등하는&lt;span&gt; &lt;/span&gt;지점을&lt;span&gt; &lt;/span&gt;관찰해서&lt;span&gt; &quot;&lt;/span&gt;여기까지&lt;span&gt;&quot;&lt;/span&gt;를&lt;span&gt; &lt;/span&gt;정하는&lt;span&gt; &lt;/span&gt;것&lt;span&gt;. &lt;/span&gt;결국&lt;span&gt; &lt;/span&gt;하이퍼파라미터&lt;span&gt; &lt;/span&gt;튜닝과, 모델 학습은&lt;span&gt; &lt;/span&gt;계산이&lt;span&gt; &lt;/span&gt;아니라&lt;span&gt; &lt;/span&gt;관찰과&lt;span&gt; &lt;/span&gt;판단, 실험의&lt;span&gt; &lt;/span&gt;영역이라는&lt;span&gt; &lt;/span&gt;감각이&lt;span&gt; &lt;/span&gt;잡혔다&lt;span&gt;.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. LLM은 &quot;숫자를 빼지 않는다&quot; &amp;mdash; cross-entropy와 128k 확률분포&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글에서 개인적으로 제일 인상 깊었던 장이다. 글 초반에 던진 세 번째 질문 &amp;mdash; &lt;i&gt;&quot;LLM&lt;/i&gt;은&lt;i&gt; 89&lt;/i&gt;달러가&lt;i&gt; &lt;/i&gt;정답인데&lt;i&gt; 99&lt;/i&gt;달러라고&lt;i&gt; &lt;/i&gt;했으니&lt;i&gt; 10&lt;/i&gt;만큼&lt;i&gt; &lt;/i&gt;틀렸다는&lt;i&gt; &lt;/i&gt;식으로&lt;i&gt; &lt;/i&gt;학습하는&lt;i&gt; &lt;/i&gt;걸까&lt;i&gt;?&quot;&lt;/i&gt; &amp;mdash; 의 답이 여기 있다. 결론부터 말하면 &lt;b&gt;아니다.&lt;/b&gt; LLM은 숫자를 빼지 않는다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;다시, 가격 예측을 forward pass부터&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3장에서 본 학습의 4단계를 이번엔 가격 예측 태스크에 그대로 얹어보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입력 프롬프트:&lt;span&gt;&amp;nbsp; &lt;/span&gt;&quot;...(상품 설명)... Price is $&quot;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 토큰 id로 변환 &amp;rarr; Llama 모델에 입력&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 마지막에 나오는 게 predicted next token &amp;rarr; &quot;99&quot; &lt;span&gt;&amp;nbsp; &lt;/span&gt;(모델이 99달러라고 예측)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 코드도 딱 이 흐름이다(글 15에서 본 그 추론 함수, 파인튜닝 모델로 바뀐 것만 다르다).&lt;/p&gt;
&lt;pre id=&quot;code_1783756706070&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;def model_predict(item):

    inputs = tokenizer(item[&quot;prompt&quot;], return_tensors=&quot;pt&quot;).to(&quot;cuda&quot;)

    with torch.no_grad():

        output_ids = fine_tuned_model.generate(**inputs, max_new_tokens=8)

    prompt_len = inputs[&quot;input_ids&quot;].shape[1]

    generated_ids = output_ids[0, prompt_len:]   # 새로 생성된 토큰만

    return tokenizer.decode(generated_ids)&lt;/code&gt;&lt;/pre&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;p data-ke-size=&quot;size16&quot;&gt;자, 모델이 &quot;99&quot;라고 했는데 실제 정답이 89달러였다고 하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 loss는 어떻게 계산될까? &quot;99 &amp;minus; 89 = 10, 10만큼 틀렸다&quot;일까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;=&amp;gt; 여기서부터가 핵심이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;forward pass의 진짜 출력은 &quot;토큰 하나&quot;가 아니다&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;깊이 들어가면, 모델은 다음 토큰을 출력하는 과정에서 ₩다음 토큰 하나를 딱 집어내는 게 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모델은 해당 모델의 vocab 사전에 있는 가능한 &lt;b&gt;128,000개 토큰 각각에 대한 확률 분포&lt;/b&gt;를 출력한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3장에서 잠깐 봤던 신경망의 마지막 층 lm_head를 기억하면 이 층의 출력이 &lt;b&gt;128,000차원짜리 벡터&lt;/b&gt;이고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 원소가 &quot;각 토큰이 다음에 올 확률&quot;이다. 그리고 이 확률을 &lt;b&gt;다 합치면 1&lt;/b&gt;이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(쉽게 설명하면 모델이 내장하고 있는 128000개 토큰 사전 각각에 대해 다음 토큰에 해당 토큰이 나올 확률을 계산한다고 보면 된다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리가 보는 &quot;99&quot;라는 예측은, 이 128,000개 확률 중 가장 높은 걸 고른 결과일 뿐이다(temperature 0이면 최고 확률 토큰 선택).&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;=&amp;gt; 즉 모델은 매 순간 &quot;다음은 무조건 99&quot;라고 단정하는 게 아니라, &lt;b&gt;&quot;89일 확률 몇 %, 99일 확률 몇 %, cat일 확률 몇 %&amp;hellip;&quot;&lt;/b&gt; 하는 분포를 통째로 내놓고 있는 거다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;그럼 loss는? &amp;mdash; 정답 토큰에 준 확률을 본다&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정답이 89달러라면, loss 계산에서 묻는 건 이거다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot;모델이 실제 정답인 89 토큰에 어떤 확률을 부여했나?&quot;&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;89에 100% 확률을 줬다면 &amp;rarr; 완벽한 예측 &amp;rarr; loss는 0이어야 한다.&lt;/li&gt;
&lt;li&gt;89에 아주 작은 확률만 줬다면 &amp;rarr; 많이 틀린 것 &amp;rarr; loss는 커야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 &quot;확률이 1이면 loss 0, 확률이 작으면 loss 큼&quot;을 만족하는 공식이 바로 &lt;b&gt;음의 로그(negative log)&lt;/b&gt; 다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;loss = -log(정답 토큰에 부여한 확률)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-log(1)&lt;span&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;/span&gt;= 0&lt;span&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;/span&gt;# 확률 100% &amp;rarr; loss 0 (완벽)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-log(작은 수) &lt;span&gt;&amp;nbsp; &lt;/span&gt;= 큰 값 &lt;span&gt;&amp;nbsp; &amp;nbsp; &lt;/span&gt;# 확률 작음 &amp;rarr; loss 큼&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확률이 작을수록 -log는 커진다. 이게 바로 &lt;b&gt;cross-entropy loss(교차 엔트로피 손실)&lt;/b&gt; 다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 학습(backward &amp;rarr; optimize)은 이 값을 줄이는 방향으로, 즉 &lt;b&gt;정답 토큰에 부여하는 확률을 조금씩 높이는 방향으로&lt;/b&gt; LoRA 어댑터를 조정한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;=&amp;gt; 여기서 3장의 &quot;loss는 얼마나 자신 있게 틀렸는지까지 반영한다&quot;가 정확히 맞아떨어진다. 89가 정답인데 89에 5%밖에 안 줬으면 loss가 크고, 40%쯤 줬으면 loss가 작다 &amp;mdash; &lt;b&gt;틀린 정도가 아니라 &quot;정답에 얼마나 확신을 줬나&quot;로 재는 것&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대체 자신있게 틀렸는지가 뭔말인가 했는데 이런 의미였다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;그래서 &quot;숫자를 빼지 않는다&quot;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심 통찰이 여기 있다. 모델은 99 &amp;minus; 89 = 10 같은 &lt;b&gt;산수를 하는 게 아니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정답 토큰 &quot;89&quot;에 부여한 확률을 보고, 그 확률의 음의 로그로 loss를 매길 뿐이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강사 표현 그대로 옮기면 &lt;i&gt;&quot;&lt;/i&gt;숫자&lt;i&gt; &lt;/i&gt;차이를&lt;i&gt; &lt;/i&gt;하는&lt;i&gt; &lt;/i&gt;게&lt;i&gt; &lt;/i&gt;아니라&lt;i&gt; &lt;/i&gt;이런&lt;i&gt; &lt;/i&gt;식으로&lt;i&gt; &lt;/i&gt;확률에&lt;i&gt; &lt;/i&gt;대해&lt;i&gt;&quot;&lt;/i&gt; 학습하며,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;LLM은 (애초에 산수를 하도록) 이런 식으로 설계되지 않았기 때문&quot;*이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 이게 왜 중요하냐면 &lt;b&gt;cross-entropy는 가격 예측에만 쓰는 게 아니라 어디에나 적용 가능&lt;/b&gt;하다는 것.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강사가 든 예가 인상적이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추천을 제시하고 &amp;rarr; 그 추천에 모델이 어떤 확률을 부여했는지 알아내고 &amp;rarr; 그거에 대해 cross-entropy loss를 매긴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 &quot;정답이 있고, 모델이 그 정답에 부여한 확률을 본다&quot;는 틀만 있으면, 가격이든 추천이든 분류든 전부 같은 방식으로 학습시킬 수 있다는 얘기다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;느낀 점&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&quot;LLM은 숫자를 빼지 않는다&quot;는 한 문장이 그동안의 오해를 정리해줬다. 나는 은연중에 가격 예측 모델이 &quot;얼마 차이 났나&quot;를 최소화하는 회귀 문제처럼 학습된다고 생각했는데(loss 계산도), 실제로는 &lt;b&gt;&quot;89라는 토큰에 확률을 얼마나 몰아줬나&quot;&lt;/b&gt; 를 최적화하는 방식으로 작동됐다. 같은 &quot;가격 예측&quot;이라도 그 밑에서 도는 손실 함수는 전혀 다른 것.&lt;/li&gt;
&lt;li&gt;특히&lt;span&gt; &quot;&lt;/span&gt;추천에도&lt;span&gt; cross-entropy&lt;/span&gt;를&lt;span&gt; &lt;/span&gt;그대로&lt;span&gt; &lt;/span&gt;쓸&lt;span&gt; &lt;/span&gt;수&lt;span&gt; &lt;/span&gt;있다&lt;span&gt;&quot;&lt;/span&gt;는&lt;span&gt; &lt;/span&gt;대목이&lt;span&gt; &lt;/span&gt;광고&lt;span&gt;&amp;nbsp;도메인&amp;nbsp;&lt;/span&gt;실무자로서&lt;span&gt; &lt;/span&gt;확&lt;span&gt; &lt;/span&gt;와닿았다&lt;span&gt;. &lt;/span&gt;추천&lt;span&gt; &lt;/span&gt;클릭&lt;span&gt; &lt;/span&gt;예측이&lt;span&gt; &lt;/span&gt;결국&lt;span&gt; &quot;&lt;/span&gt;정답&lt;span&gt;(&lt;/span&gt;클릭한&lt;span&gt; &lt;/span&gt;아이템&lt;span&gt;)&lt;/span&gt;에&lt;span&gt; &lt;/span&gt;모델이&lt;span&gt; &lt;/span&gt;부여한&lt;span&gt; &lt;/span&gt;확률&lt;span&gt;&quot;&lt;/span&gt;을&lt;span&gt; &lt;/span&gt;높이는&lt;span&gt; &lt;/span&gt;문제라는&lt;span&gt; &lt;/span&gt;걸&lt;span&gt; &lt;/span&gt;생각하면&lt;span&gt;, LLM &lt;/span&gt;학습과&lt;span&gt; &lt;/span&gt;추천&lt;span&gt; &lt;/span&gt;학습이&lt;span&gt; &lt;/span&gt;손실&lt;span&gt; &lt;/span&gt;함수&lt;span&gt; &lt;/span&gt;레벨에선&lt;span&gt; &lt;/span&gt;형제였던&lt;span&gt; &lt;/span&gt;셈이다&lt;span&gt;. &lt;/span&gt;도메인이&lt;span&gt; &lt;/span&gt;달라&lt;span&gt; &lt;/span&gt;보여도&lt;span&gt; &quot;&lt;/span&gt;정답&lt;span&gt; &lt;/span&gt;확률의&lt;span&gt; &lt;/span&gt;음의&lt;span&gt; &lt;/span&gt;로그를&lt;span&gt; &lt;/span&gt;줄인다&lt;span&gt;&quot;&lt;/span&gt;는&lt;span&gt; &lt;/span&gt;뼈대는&lt;span&gt; &lt;/span&gt;똑같을 수 있다는게&lt;span&gt;&amp;nbsp;&lt;/span&gt;이번&lt;span&gt; &lt;/span&gt;장에서 배워간 것이었다&amp;nbsp;&lt;/li&gt;
&lt;li&gt;앞의 주차에서 배웠던 트랜스포머 모델 구조 (다음 토큰의 확률을 예측하는) 가 이런 식으로 모델 학습에서도 이어지는 것을 볼 수 있어서 공부가 이어지는 느낌을 낼 수 있었다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7. 최종 평가와 전체 성적표 &amp;mdash; 작은 모델이 프론티어를 이긴다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 진짜 결승선이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Day 5는 파인튜닝한 모델을 test set으로 평가하고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 이 프로젝트(The Price Is Right)에 등장한 &lt;b&gt;모든 모델을 한 판에 세워&lt;/b&gt; 성적을 비교한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;어떻게 평가하나&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;평가 코드는 단순하다. 4비트 베이스 모델을 로드한 뒤, 학습해서 Hub에 올려둔 어댑터를 붙이면 끝이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1783757309904&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fine_tuned_model = PeftModel.from_pretrained(base_model, HUB_MODEL_NAME, revision=REVISION)

print(f&quot;Memory footprint: {fine_tuned_model.get_memory_footprint() / 1e6:.1f} MB&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Memory footprint: 3753.7 MB &lt;span&gt;&amp;nbsp; &lt;/span&gt;# full 모델(r=256, 어텐션+MLP 전부에 어댑터) 기준&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;full 모델은 어댑터를 더 넓게(어텐션+MLP 전부, r=256) 붙여서 약 3.7GB다(lite는 훨씬 작다).&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예측 함수는 6장에서 본 model_predict 그대로고, 평가는 이 한 줄이다.&lt;/p&gt;
&lt;pre id=&quot;code_1783757345799&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;set_seed(42)          # 매번 같은 test 샘플을 뽑도록 고정 (공정 비교)

evaluate(model_predict, test)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;evaluate는 Week 6부터 쭉 쓴 util.py의 그 함수다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;test 샘플마다 예측가와 실제가의 &lt;b&gt;오차(Error) = |예측 &amp;minus; 정답|&lt;/b&gt; 를 재서 평균 낸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예측이 실제가와 가까우면 초록, 어중간하면 주황, 많이 빗나가면 빨강으로 산점도에 찍힌다(오차 $40 미만 또는 20% 이내면 초록, $80/40% 선이 주황).&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;=&amp;gt; 그래서 산점도에서 &lt;b&gt;초록 점이 대각선(y=x)에 몰릴수록 좋은 모델&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p 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;Human(Ed 본인) 수준 $87.62를 이기기&lt;/li&gt;
&lt;li&gt;가능하면 GPT-4.1-nano(파인튜닝 없는 기본)의 $62.51 근처까지&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;전체 성적표&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 이 프로젝트의 모든 모델을 오차 순으로 세운 최종 성적표다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;낮을수록(오차가 작을수록) 좋다.&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1370&quot; data-origin-height=&quot;1106&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/byHy2i/dJMcafAxioA/7UOTYpdpsvc7GV1LsiZsS0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/byHy2i/dJMcafAxioA/7UOTYpdpsvc7GV1LsiZsS0/img.png&quot; data-alt=&quot;The Price Is Right 전 모델 성적표 &amp;amp;mdash; 오차(Error, $). 파인튜닝한 오픈소스 모델(빨강)이 프론티어 제로샷(보라)을 제쳤다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/byHy2i/dJMcafAxioA/7UOTYpdpsvc7GV1LsiZsS0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbyHy2i%2FdJMcafAxioA%2F7UOTYpdpsvc7GV1LsiZsS0%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;1370&quot; height=&quot;1106&quot; data-origin-width=&quot;1370&quot; data-origin-height=&quot;1106&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;The Price Is Right 전 모델 성적표 &amp;mdash; 오차(Error, $). 파인튜닝한 오픈소스 모델(빨강)이 프론티어 제로샷(보라)을 제쳤다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;모델&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;&lt;b&gt;오차&lt;/b&gt;&lt;/span&gt;&lt;b&gt;($)&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;분류&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;Base Llama 3.2 4bit&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;185.52&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&amp;larr; &lt;span&gt;파인튜닝&lt;/span&gt; &lt;span&gt;전&lt;/span&gt;, &lt;span&gt;꼴찌&lt;/span&gt; (&lt;span&gt;내&lt;/span&gt; Colab &lt;span&gt;실측&lt;/span&gt;)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;Constant (&lt;span&gt;평균값&lt;/span&gt; &lt;span&gt;찍기&lt;/span&gt;)&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;106.18&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;전통&lt;span&gt; &lt;/span&gt;베이스라인&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;Linear Regression&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;101.56&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;전통&lt;/span&gt; ML&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;Human (Ed)&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;87.62&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;사람&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;NLP + LR&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;76.81&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;전통&lt;/span&gt; ML&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;GPT-4.1-nano (Fine-tuned)&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;75.91&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&amp;larr; Week 6 &lt;span&gt;결과&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;Random Forest&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;72.28&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;전통&lt;/span&gt; ML&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;XGBoost&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;68.23&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;전통&lt;/span&gt; ML&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;Fine-tuned Lite&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;65.40&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&amp;larr; &lt;span&gt;우리&lt;/span&gt; lite, GPT-4.1-nano&lt;span&gt;급&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;Neural Network&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;63.97&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;신경망&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;GPT-4.1-nano&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;62.51&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;프론티어&lt;span&gt; &lt;/span&gt;제로샷&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;Grok 4.1 Fast&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;57.62&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;프론티어&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;Gemini 3 Pro&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;50.54&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;프론티어&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;Claude 4.5 Sonnet&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;47.10&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;프론티어&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;Deep Neural Network&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;46.49&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;신경망&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;GPT-5.1&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;44.74&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;프론티어&lt;span&gt; &lt;/span&gt;최강&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;Fine-tuned Full&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;39.85&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&amp;larr; &lt;span&gt;우리&lt;/span&gt; full, &lt;span&gt;전체&lt;/span&gt; 1&lt;span&gt;위&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;(Base Llama &lt;/i&gt;는&lt;i&gt; &lt;/i&gt;내가&lt;i&gt; &lt;/i&gt;직접&lt;i&gt; Colab&lt;/i&gt;에서&lt;i&gt; &lt;/i&gt;돌린&lt;i&gt; &lt;/i&gt;실측치다&lt;i&gt;. &lt;/i&gt;나머지&lt;i&gt; &lt;/i&gt;수치는&lt;i&gt; &lt;/i&gt;강사가 돌렸을 때의&lt;i&gt; results &lt;/i&gt;기준&lt;i&gt;.)&lt;/i&gt;&lt;/p&gt;
&lt;p 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;출발점 &amp;mdash; Base Llama, 꼴찌.&lt;/b&gt;&amp;nbsp;파인튜닝 전 베이스 Llama 3.2(4bit)는 오차 $185.52로 성적표 맨 밑이다. Constant(그냥 평균값만 찍는 베이스라인 $106)보다도 못하다. =&amp;gt; 글 15에서 &quot;베이스 모델은 형편없다&quot;고 했던 그 지점.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;한 걸음 &amp;mdash; Lite, GPT-4.1-nano급.&lt;/b&gt;&amp;nbsp;내가 T4에서 1에폭만 가볍게 돌린 lite가 $65.40. 프론티어인 GPT-4.1-nano 제로샷($62.51)과 거의 어깨를 나란히 한다.&amp;nbsp;&lt;b&gt;꼴찌였던 모델이 어댑터 73MB 붙이고 프론티어급으로 올라온 것.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;결승선 &amp;mdash; Full, 전체 1위.&lt;/b&gt;&amp;nbsp;full로 제대로 학습한 모델은&amp;nbsp;&lt;b&gt;$39.85&lt;/b&gt;로 성적표&amp;nbsp;&lt;b&gt;전체 1등&lt;/b&gt;이다. GPT-5.1($44.74), Claude 4.5 Sonnet($47.10)은 물론, 사람(강사, $87.62)도 한참 앞선다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3B(30억 파라미터)짜리 오픈소스 모델이, 4비트 양자화로 짓눌린 채, 수조 파라미터급 프론티어 모델들을 전부 제친 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;글 초반에 풀어둔 스포일러가 여기서 회수된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 이 1위가 &quot;모델이 시세를 완벽히 안다&quot;는 뜻은 아니다.&lt;/p&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;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;느낀 점&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Week 6에서 파인튜닝(GPT-4.1-nano)이 오히려 성능을 깎아먹는 걸 보고 &quot;파인튜닝으로 성능개선하는게 쉽지 않구나&quot; 했는데, 이번엔 정반대 결과가 나왔다. &lt;b&gt;같은 &quot;파인튜닝&quot;인데 왜 결과가 뒤집혔을까&lt;/b&gt; -&amp;gt; 이건 마무리에서 정리하려 한다.&lt;/li&gt;
&lt;li&gt;실무 감각으로 제일 크게 남은 건 &quot;&lt;b&gt;좁은 태스크 하나엔, 작은 전문가 모델이 거대 제너럴리스트를 이길 수 있다&lt;/b&gt;&quot;는 실증이었다. 프론티어 API를 그냥 부르는 것보다, 내 도메인 데이터로 작은 오픈소스 모델을 파인튜닝하는 게 성능&amp;middot;비용&amp;middot;소유권 모든 면에서 나을 수 있다는 것. 실제 에이전트를 만드는 태스크에서도 의도분류라든가, 일부 좁게 정의된 태스크에선 이렇게 로컬 llm 을 파인튜닝해서 쓰는 방식도 가능하겠다라는 걸 이번 결과를 통해서 본거 같다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;오히려 학습 자체보다도 학습을 위한 데이터를 만드는 일, 그리고 여러 실험을 통해서 최적의 하이퍼 파라미터들을 지정하고, 최적의 체크포인트를 찾아가는 그 과정이 중요하다는 걸 실제로 파인튜닝을 돌려보고 느꼈던 거 같다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;파인튜닝.. 실무를 하면서는 모델을 직접 만드는 부서에서나 가능한 먼 이야기 처럼만 들리고 관련된 용어들도 너무 낯설었는데 이번에 직접 돌려보며 조금 더 모델 단의 지식까지 알아볼 수 있었던 게 의미가 있던 거 같다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;마무리 &amp;mdash; 파인튜닝은 만능인가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 편에 걸친 Week 7이 끝났다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;글 15에서 QLoRA로 30억 파라미터를 작은 GPU에 욱여넣는 준비를 했고, 이 글에서 실제로 학습시켜 프론티어 모델을 이겼다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 처음의 질문 하나가 남는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Week 6에선 그렇게 헛돌던 파인튜닝이, 왜 이번엔 1등을 했을까?&lt;/b&gt; 같은 &quot;파인튜닝&quot;인데.&lt;/p&gt;
&lt;p 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;Week 6&lt;/b&gt;은 이미 방대한 지식을 가진&amp;nbsp;&lt;b&gt;프론티어 모델(GPT-4.1-nano)&lt;/b&gt;&amp;nbsp;을,&amp;nbsp;&lt;b&gt;적은 데이터(200개 정도) &lt;/b&gt;로 파인튜닝했다. 원래 잘하는 모델에 좁은 데이터를 덧입히니 오히려 기존 능력이 흔들려 성능이 떨어졌다. &lt;br /&gt;=&amp;gt; 그래서 그때 결론이 &quot;파인튜닝은 성능 향상이 아니라 스타일&amp;middot;형식 맞추기용&quot;이었다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Week 7&lt;/b&gt;은 그 태스크를 아직 못 하는&amp;nbsp;&lt;b&gt;작은 base 모델&lt;/b&gt;을,&amp;nbsp;&lt;b&gt;그 태스크와 관련된 수많은 데이터로(Full 기준 80만건, Epoch 3회)&lt;/b&gt;&amp;nbsp;파인튜닝했다. 백지에 가까운 모델에 &quot;이 일 하나만 잘하도록&quot; 집중적으로 가르치니 성능이 확 올랐다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;=&amp;gt; 즉 파인튜닝이 성능을 올리느냐 마느냐는 &lt;b&gt;&quot;어떤 모델을, 어떤 데이터로, 무엇을 위해&quot;&lt;/b&gt; 에 달렸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미 잘하는 큰 모델을 미세하게 다듬는 것과, 못 하는 작은 모델을 특정 태스크의 전문가로 키우는 것은 같은 &quot;파인튜닝&quot;이라도 전혀 다른 작업이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 파인튜닝은 만능이 아니라, &lt;b&gt;&quot;작은 전문가를 만드는 도구&quot;&lt;/b&gt; 에 가깝다는 게 두 주의 결론이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터가 있고 태스크가 명확하다면, 거대 모델을 부르는 대신 작은 모델을 내 손으로 키우는 선택지&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그게 이번 주에 QLoRA로 직접 확인한 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지가&lt;span&gt; Ed Donner LLM Engineering &lt;/span&gt;강의의&lt;span&gt; Week 7 &lt;/span&gt;정리였다&lt;span&gt;. &lt;/span&gt;개념&lt;span&gt;(&lt;/span&gt;글&lt;span&gt; 15)&lt;/span&gt;부터&lt;span&gt; &lt;/span&gt;학습&lt;span&gt;&amp;middot;&lt;/span&gt;평가&lt;span&gt;(&lt;/span&gt;글&lt;span&gt; 16)&lt;/span&gt;까지&lt;span&gt;, &lt;/span&gt;오픈소스&lt;span&gt; &lt;/span&gt;모델을&lt;span&gt; &lt;/span&gt;내&lt;span&gt; &lt;/span&gt;손으로&lt;span&gt; &lt;/span&gt;파인튜닝하는&lt;span&gt; &lt;/span&gt;한&lt;span&gt; &lt;/span&gt;사이클을&lt;span&gt; &lt;/span&gt;처음&lt;span&gt; &lt;/span&gt;끝까지&lt;span&gt; &lt;/span&gt;돌려본&lt;span&gt; &lt;/span&gt;셈이다&lt;span&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt; &lt;/span&gt;다음&lt;span&gt; Week8은 강의의 마지막 주차로, 지금까지 공부한 것의 총 집합체이다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;지금껏 파인튜닝한 가격 예측 모델을 기반으로, 전 주차들에서 배운 RAG등도 활용하여 하나의 에이전트 시스템을 만들어보는 주차다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;해당 내용으로 다시 찾아오겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;(꾸준히 공부하고&lt;span&gt;&amp;nbsp;적을테니&amp;nbsp;&lt;/span&gt;많은 관심 부탁드립니다.)&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;Profile:&lt;/span&gt;&lt;br /&gt;&lt;a style=&quot;color: #0070d1; text-align: start;&quot; href=&quot;http://www.linkedin.com/in/doobeom-kim-%EA%B9%80%EB%91%90%EB%B2%94-2b9649242&quot;&gt;&lt;span&gt;&lt;span style=&quot;color: #0070d1;&quot;&gt;Linkedin&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>AI 공부</category>
      <category>ai</category>
      <category>AI 공부</category>
      <category>llm</category>
      <category>lora</category>
      <category>QLORA</category>
      <category>라마</category>
      <category>에이전트</category>
      <category>오픈소스 모델</category>
      <category>파인튜닝</category>
      <category>허깅페이스</category>
      <author>kdb1248</author>
      <guid isPermaLink="true">https://doobeom-coding.tistory.com/93</guid>
      <comments>https://doobeom-coding.tistory.com/93#entry93comment</comments>
      <pubDate>Sat, 11 Jul 2026 17:35:57 +0900</pubDate>
    </item>
    <item>
      <title>15. LoRA&amp;middot;QLoRA 원리 파악 &amp;mdash; Llama 3.2 파인튜닝 준비하기</title>
      <link>https://doobeom-coding.tistory.com/92</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;목차&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;들어가며&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 이번엔 오프소스 모델을 직접 파인튜닝한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 3B(30억) 파라미터 모델을 그냥 학습 못하는 이유&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. LoRA - 원본은 얼리고(freeze), 작은 어댑터만 학습&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. QLoRA의 Q - 양자화로 4비트까지 줄이기&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5. LoRA 어댑터는 실제로 얼마나 작을까&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;6. 학습데이터 만들기 - 프롬프트 설계&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;7. 파인튜닝 전, 베이스 모델은 얼마나 못할까&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마무리&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;들어가며&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지난 글 2편 &lt;br /&gt;&lt;a href=&quot;https://doobeom-coding.tistory.com/90&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;- &lt;/a&gt;&lt;a href=&quot;https://doobeom-coding.tistory.com/90&quot;&gt;https://doobeom-coding.tistory.com/90&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;a href=&quot;https://doobeom-coding.tistory.com/91&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://doobeom-coding.tistory.com/91&lt;/a&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에서 나는 GPT-4.1-nano를 데이터로 파인튜닝을 해보는 작업을 진행하고, 이를 통해 상품 설명을 기반으로 가격을 예측해보는 작업을 진행했다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과는 좀 허무했다. 아무 튜닝 없이 그냥 호출을 통해 물어본 프론티어 LLM의 정확도가 제일 높았고,&lt;/p&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;p data-ke-size=&quot;size16&quot;&gt;그런데 Week 7에서 강사는 같은 가격 예측 문제(The Price Is Right)를 다시 들고 와서 이렇게 말한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;이번엔 오픈소스 모델을, 그것도 노트북에 달릴 법한 GPU 한 장에서 직접 파인튜닝해보자.&quot;&lt;/p&gt;
&lt;p 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;첫째, Week 6에선 OpenAI 서버가 알아서 파인튜닝해줬는데,&amp;nbsp;&lt;b&gt;모델을 내 손으로 직접 학습하는 건 어떤식으로 이뤄질까&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;둘째, 우리가 쓸 모델은 Llama 3.2의&amp;nbsp;&lt;b&gt;30억(3B) 파라미터&lt;/b&gt;짜리다. Week 6에서 만든 신경망(딥러닝 모델)의 파라미터가 2억 개였는데, 그보다 15배 큰 모델을 &lt;b&gt;단일 GPU에서 학습할 수 있을까?&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 두 번째 질문에 대한 답이 이번 주의 핵심 키워드, &lt;b&gt;QLoRA&lt;/b&gt;다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 스포일러를 하나 풀자면 Week 6에서 그렇게 헛돌던 파인튜닝이, 이번엔 제로샷 프론티어 모델을 전부 제치고 1등을 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 &quot;파인튜닝&quot;인데 왜 결과가 정반대일까? &lt;b&gt;그 이야기는 다음 글(글 16)에서 제대로 풀고&lt;/b&gt;,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 우선 QLoRA가 도대체 무슨 마법이길래 30억 파라미터를 작은 GPU에 욱여넣는지부터 차근차근 뜯어보려 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글은 모델을 거의 &quot;학습&quot;시키지 않는다.&lt;/p&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;1) 왜 큰 모델은 그냥 학습할 수 없는지&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2) LoRA와 양자화가 각각 무엇을 해결하는지,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3) 그리고 파인튜닝 전 베이스 모델이 얼마나 못하는지(= 비교 기준선) 을 다룬다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Week 6 때도 느꼈지만, &lt;b&gt;평가의 신뢰도는 데이터와 베이스라인 단계에서 이미 결정된다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 이번에도 모델을 돌리기 전 준비 과정에 글의 절반을 쓴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 이번엔 오픈소스 모델을 직접 파인튜닝한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리가 푸는 문제는 Week 6과 똑같다. &lt;b&gt;상품 설명을 보고 가격을 맞히는 것&lt;/b&gt;(The Price Is Right)&lt;/p&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;p data-ke-size=&quot;size16&quot;&gt;Week 6과 무엇이 다른지 표로 정리하면 이렇다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 111px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 35px;&quot;&gt;
&lt;td style=&quot;height: 35px;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 35px;&quot;&gt;&lt;span&gt;&lt;b&gt;Week 6 (&lt;/b&gt;&lt;span&gt;&lt;b&gt;글&lt;/b&gt;&lt;/span&gt;&lt;b&gt; 13&amp;middot;14)&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 35px;&quot;&gt;&lt;span&gt;&lt;b&gt;Week 7 (&lt;/b&gt;&lt;span&gt;&lt;b&gt;이번&lt;/b&gt;&lt;/span&gt;&lt;b&gt;)&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;span&gt;&lt;b&gt;모델&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;span&gt;GPT-4.1-nano (OpenAI &lt;span&gt;클라우드&lt;/span&gt;)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;span&gt;Llama 3.2&amp;nbsp;&lt;b&gt;3B&lt;/b&gt;&amp;nbsp;(&lt;span&gt;오픈소스&lt;/span&gt;)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;span&gt;&lt;b&gt;학습&lt;/b&gt;&lt;span&gt;&lt;b&gt; &lt;/b&gt;&lt;/span&gt;&lt;b&gt;위치&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;span&gt;OpenAI &lt;span&gt;서버&lt;/span&gt; (&lt;span&gt;블랙박스&lt;/span&gt;)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;b&gt;내&lt;/b&gt;&lt;/span&gt;&lt;b&gt; GPU &lt;/b&gt;&lt;span&gt;&lt;b&gt;한&lt;/b&gt;&lt;/span&gt;&lt;b&gt; &lt;/b&gt;&lt;span&gt;&lt;b&gt;장&lt;/b&gt;&lt;/span&gt;&amp;nbsp;(Colab)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;span&gt;&lt;b&gt;가중치&lt;/b&gt;&lt;span&gt;&lt;b&gt; &lt;/b&gt;&lt;/span&gt;&lt;b&gt;접근&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;span&gt;&lt;span&gt;불가&lt;/span&gt; (API&lt;span&gt;로만&lt;/span&gt;)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;span&gt;&lt;b&gt;전부&lt;/b&gt;&lt;span&gt;&lt;b&gt; &lt;/b&gt;&lt;/span&gt;&lt;b&gt;열려&lt;/b&gt;&lt;span&gt;&lt;b&gt; &lt;/b&gt;&lt;/span&gt;&lt;b&gt;있음&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;span&gt;&lt;b&gt;방법&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;span&gt;파일&lt;span&gt; &lt;/span&gt;업로드&lt;span&gt; &amp;rarr; &lt;/span&gt;서버가&lt;span&gt; &lt;/span&gt;알아서&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;span&gt;&lt;b&gt;QLoRA&lt;/b&gt;&lt;span&gt;&lt;b&gt;로&lt;/b&gt;&lt;/span&gt;&lt;b&gt; &lt;/b&gt;&lt;span&gt;&lt;b&gt;직접&lt;/b&gt;&lt;/span&gt;&lt;b&gt; &lt;/b&gt;&lt;span&gt;&lt;b&gt;학습&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한마디로 Week 6이 &lt;b&gt;남의 주방에 재료를 맡기는 것&lt;/b&gt;이었다면, Week 7은 &lt;b&gt;내 주방에서 직접 칼을 잡는 것&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가중치를 직접 만질 수 있으니 &quot;어디를, 어떻게, 얼마나&quot; 학습할지 내가 정한다. 대신 그만큼 알아야 할 것도 많아진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 한 가지 더 짚고 갈 게 있다. 우리가 고른 모델은 겨우 30억 파라미터짜리다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Llama 3.2 &lt;b&gt;3B&lt;/b&gt; (30억, 게다가 4비트로 양자화) &lt;b&gt;vs&lt;/b&gt; GPT-5 (수조 단위 파라미터)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;체급이 말이 안 되게 차이 난다. 강사가 굳이 이 작은 모델을 고른 건, &lt;b&gt;&quot;좁은 태스크 하나에선 작은 전문가가 거대한 제너럴리스트를 이길 수 있다&quot;&lt;/b&gt; 는 가설을 검증하려는 거다. 우리가 시키는 일은 &quot;온 세상 질문에 답하기&quot;가 아니라 &lt;b&gt;&quot;상품 설명 보고 가격 맞히기&quot; 딱 하나&lt;/b&gt;니까.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p 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;Day 1&lt;/b&gt;&amp;nbsp;&amp;mdash; QLoRA가 뭔지 (이 글의 2~5장)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Day 2&lt;/b&gt;&amp;nbsp;&amp;mdash; 데이터 만들기 + 베이스 모델 측정 (이 글의 6~7장)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Day 3~4&lt;/b&gt;&amp;nbsp;&amp;mdash; 실제로 학습 돌리기 (다음 글)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Day 5&lt;/b&gt;&amp;nbsp;&amp;mdash; 최종 평가 (다음 글)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 3B(30억) 파라미터 모델을 그냥 학습 못 하는 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;QLoRA를 이해하려면, 먼저 &lt;b&gt;&quot;왜 그냥은 안 되는가&quot;&lt;/b&gt; 부터 봐야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리가 쓸 Llama 3.2 3B는 이름 그대로 약 &lt;b&gt;30억 개의 파라미터&lt;/b&gt;를 가진 모델이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Week 6에서 직접 만든 딥러닝 신경망이 2억 개 정도였으니, 그보다 15배쯤 크다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 모델을 &quot;학습한다&quot;는 건 단순히 가중치를 GPU에 올려 추론(inference)에 쓰는 것과는 차원이 다른 일이다.&lt;/p&gt;
&lt;p 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;모델 파라미터(weights)를 GPU 메모리에 올린다&lt;/li&gt;
&lt;li&gt;입력을 넣고&amp;nbsp;&lt;b&gt;forward pass&lt;/b&gt;(순전파)를 수행한다&lt;/li&gt;
&lt;li&gt;정답과 비교해&amp;nbsp;&lt;b&gt;loss&lt;/b&gt;(얼마나 틀렸는지)를 계산한다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;backward pass&lt;/b&gt;(역전파)로 각 파라미터의 gradient(기울기)를 구한다&lt;/li&gt;
&lt;li&gt;optimizer가 그 gradient를 받아 파라미터를 업데이트한다&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 이 과정에서 GPU 메모리에 올라가 있어야 하는 게 모델 원본 가중치 하나가 아니라는 점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모델 파라미터 30억 개를 전부 학습하려면 최소한 이만큼이 동시에 필요하다.&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;gradient (각 파라미터마다 하나씩)&lt;/li&gt;
&lt;li&gt;optimizer state (Adam류는 파라미터당 추가로 상태값을 들고 있다)&lt;/li&gt;
&lt;li&gt;중간 activation (forward pass에서 나온 중간 결과들)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 강의 노트에 이렇게 적어뒀다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot;모델을 올리는 것&quot;보다 &quot;학습하는 것&quot;이 훨씬 더 비싸다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;감을 잡아보자면, Llama 3.2 3B는 가중치만 올려도 대략 &lt;b&gt;13GB&lt;/b&gt; 정도를 차지한다. (3bilion * 32 bits(4byte) = 13gb)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 이건 어디까지나 &lt;b&gt;학습을 시작하기도 전&lt;/b&gt;의 숫자다. 위의 gradient&amp;middot;optimizer state&amp;middot;activation까지 얹히면 실제 학습에 드는 메모리는 그보다 훨씬 불어난다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 노트북에 흔히 달려 있는 GPU 한 장(16GB 안팎)으로는 30억 파라미터를 통째로 학습하는 건 사실상 불가능하다는 얘기다.&amp;nbsp;&lt;br /&gt;(일반적인 인프라로는 모델 파라미터 전체를 건드리는 풀 파인튜닝은 쉽지 않다는 의미)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1596&quot; data-origin-height=&quot;1356&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bzVcfy/dJMcabktD2G/aMGPitzDSlkDGW3eQykwTk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bzVcfy/dJMcabktD2G/aMGPitzDSlkDGW3eQykwTk/img.png&quot; data-alt=&quot;추론용(양자화 없는 디폴트 값 BF16(16Bit)) 올리기만 해도 6.4GB. 여기에 학습용 메모리가 더 얹힌다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bzVcfy/dJMcabktD2G/aMGPitzDSlkDGW3eQykwTk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbzVcfy%2FdJMcabktD2G%2FaMGPitzDSlkDGW3eQykwTk%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;1596&quot; height=&quot;1356&quot; data-origin-width=&quot;1596&quot; data-origin-height=&quot;1356&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;추론용(양자화 없는 디폴트 값 BF16(16Bit)) 올리기만 해도 6.4GB. 여기에 학습용 메모리가 더 얹힌다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 방법은 두 갈래다.&lt;/p&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;b&gt; ② 파라미터 하나하나가 차지하는 메모리를 줄이거나.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;QLoRA는 이 둘을 동시에 쓴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞쪽이 &lt;b&gt;LoRA&lt;/b&gt;(파라미터 수 줄이기),&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;뒤쪽이 &lt;b&gt;Q = Quantization&lt;/b&gt;(파라미터당 메모리 줄이기)다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 장에서 이 둘을 차례로 뜯어본다.&lt;/p&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;h3 data-ke-size=&quot;size23&quot;&gt;3. LoRA &amp;mdash; 원본은 얼리고(freeze), 작은 어댑터만 학습&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞 장에서 &quot;학습할 파라미터 수를 줄이는 쪽&quot;이 LoRA라고 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LoRA(Low-Rank Adaptation)의 발상은 한 문장으로 요약된다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;원본 Llama는 그대로 두고, 그 위에 작은 보정치만 학습해서 원하는 동작을 만들자.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이걸 풀면 핵심이 세 개다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;① 원본은 전부 &lt;b&gt;얼리고&lt;/b&gt;(freeze),&lt;/p&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;b&gt;작은 행렬 두 개&lt;/b&gt;를 붙여 그것만 학습한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 ①.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;full fine-tuning이 원본 모델의 모든 가중치(파라미터)를 업데이트하는 것이라면, LoRA는 &lt;b&gt;원본 30억 개 파라미터를 전부 고정&lt;/b&gt;한다. gradient 기반 최적화의 대상에서 빼버리는 것이다. 원본 가중치는 한 톨도 바뀌지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 학습 대상이 줄어드니 파라미터 외에 gradient&amp;middot;optimizer state&amp;middot;activation을 들고 있어야 할 양도 같이 줄어든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p 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;full fine-tuning&lt;/b&gt;: 자동차 엔진을 통째로 뜯어 전부 개조&lt;/li&gt;
&lt;li&gt;&lt;b&gt;LoRA&lt;/b&gt;: 엔진은 그대로 두고, 성능에 영향을 주는&amp;nbsp;&lt;b&gt;작은 조절 장치만 추가&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3-1. 어디를 건드릴까 &amp;mdash; target modules&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 어디에 그 &quot;작은 조절 장치&quot;를 붙일까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모델의 모든 부분을 다 건드리는 게 아니라, &lt;b&gt;영향력이 큰 특정 선형층(linear layer)&lt;/b&gt; 만 골라서 거기에만 LoRA를 붙인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 고른 층들을 &lt;b&gt;target modules&lt;/b&gt;라고 부른다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p 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;Attention안의 projection Layer 들&lt;/b&gt;:&amp;nbsp;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;q_proj&lt;/li&gt;
&lt;li&gt;k_proj&lt;/li&gt;
&lt;li&gt;v_proj&lt;/li&gt;
&lt;li&gt;o_proj&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;혹은 MLP 안의&lt;/b&gt;:&amp;nbsp;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;gate_proj&lt;/li&gt;
&lt;li&gt;up_proj&lt;/li&gt;
&lt;li&gt;down_proj&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 선형 층들이 모델이 정보를 &quot;표현하는 방식&quot;을 크게 좌우하기 때문에, LoRA는 여기를 노린다. 발상은 이렇다.&lt;/p&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;참고로 이번 실험에선 &quot;가벼운 설정(lite)&quot;은 attention의 4개 층만, &quot;본격 설정(full)&quot;은 MLP까지 합쳐 7개 층을 target으로 잡는다. 어디까지 건드릴지가 곧 학습량을 정하는 셈인데, 이 둘이 실제로 파라미터 수에서 얼마나 차이 나는지는 5장에서 직접 세본다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3-2. 왜 LoRA에서 행렬(rank)이 둘이고, alpha는 무엇인가&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원래 선형층에서의 계산은 이렇다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1782626243819&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;y = Wx&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;W&amp;nbsp;: 원본 모델의 큰 가중치 행렬&lt;/li&gt;
&lt;li&gt;x&amp;nbsp;: 입력,&amp;nbsp;y&amp;nbsp;: 출력&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;full fine-tuning이면 이 W 자체를 바꾼다. 하지만 LoRA는 W를 건드리지 않고, 대신 아래처럼 생각한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1782626292213&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;y = (W + &amp;Delta;W) x&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 &amp;Delta;W만 새로 학습하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 원래 가중치 W는 그대로 두고,&lt;b&gt;작은 보정 행렬 &amp;Delta;W&lt;/b&gt; 를 더해서 동작을 바꾸는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 문제가 하나 있다. W와 더하려면 &amp;Delta;W도 W와 &lt;b&gt;같은 크기&lt;/b&gt;여야 한다. (행렬 연산 상)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;근데 큰 크기 그대로 &amp;Delta;W를 만들어 학습하면, 결국 파라미터가 원본만큼 많아져서 &lt;b&gt;LoRA를 쓰는 의미가 사라진다. &lt;/b&gt;(학습해야 할 파라미터 수 자체를 줄여 메모리를 덜 차지)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 LoRA는 &amp;Delta;W를 직접 만들지 않고, &lt;b&gt;두 개의 작은 행렬 곱&lt;/b&gt;으로 표현한다. 이게 LoRA에서의 행렬(rank)이 둘인 이유다.&lt;/p&gt;
&lt;pre id=&quot;code_1782626415982&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;Delta;W = B A&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;A &lt;span&gt;&amp;isin;&lt;/span&gt; R^(r &amp;times; d_in)&amp;nbsp;&amp;mdash; 입력 차원을 작은&amp;nbsp;r로 줄인다&lt;/li&gt;
&lt;li&gt;B &lt;span&gt;&amp;isin;&lt;/span&gt; R^(r &amp;times; d_out)&amp;nbsp;방향으로 &amp;mdash; 다시 출력 차원으로 늘린다&lt;/li&gt;
&lt;li&gt;r&amp;nbsp;은 아주 작은 값 (보통 8, 16, 32, 64)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 행렬을 곱하면 B A 의 크기는 다시 (d_out &amp;times; d_in) 이 되어 원본 W와 딱 맞는다.&lt;/p&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직관적으로는, 큰 수정 행렬 하나를 통째로 배우는 대신 &lt;b&gt;아주 낮은 차원(r)의 병목(bottleneck)을 거쳐서 근사&lt;/b&gt;하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 이름이 &lt;b&gt;Low-Rank Adaptation&lt;/b&gt;(낮은 랭크의 적응)이다. 모델이 변할 수 있는 자유도를 일부러 작게 묶어두는 대신, 훨씬 적은 파라미터로도 꽤 효과적인 미세조정을 해낸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 노트북에서 LoRA를 붙인 모델 구조를 찍어보면, 원래 한 줄이던 q_proj 가 이렇게 바뀌어 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1782626689616&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;(q_proj): lora.Linear4bit(

  (base_layer): Linear4bit(in=3072, out=3072)   &amp;larr; 원본 W (고정, 4bit)

  (lora_A): Linear(in=3072, out=32)             &amp;larr; A: 3072 &amp;rarr; 32 (r=32)

  (lora_B): Linear(in=32,   out=3072)           &amp;larr; B: 32 &amp;rarr; 3072

  (lora_dropout): Dropout(p=0.1)

)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원본 base_layer는 그대로 얼어 있고, 그 옆에 lora_A(3072&amp;rarr;32)와 lora_B(32&amp;rarr;3072) 두 행렬만 새로 붙은 게 눈에 보인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;학습 중 gradient가 실제로 반영되는 건 &lt;b&gt;이 A&amp;middot;B 두 행렬뿐&lt;/b&gt;이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-06-28 오후 3.04.03.png&quot; data-origin-width=&quot;1532&quot; data-origin-height=&quot;1290&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HUosG/dJMb997XKMy/RUpfAi4N5gKTq2KGUWDX4K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HUosG/dJMb997XKMy/RUpfAi4N5gKTq2KGUWDX4K/img.png&quot; data-alt=&quot;원본 q_proj는 얼어 있고, 옆에 lora_A&amp;amp;middot;lora_B 두 행렬만 새로 붙는다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HUosG/dJMb997XKMy/RUpfAi4N5gKTq2KGUWDX4K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHUosG%2FdJMb997XKMy%2FRUpfAi4N5gKTq2KGUWDX4K%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;1532&quot; height=&quot;1290&quot; data-filename=&quot;스크린샷 2026-06-28 오후 3.04.03.png&quot; data-origin-width=&quot;1532&quot; data-origin-height=&quot;1290&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;원본 q_proj는 얼어 있고, 옆에 lora_A&amp;middot;lora_B 두 행렬만 새로 붙는다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 &lt;b&gt;alpha 가 왜 필요한지를 설명하겠다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;행렬 A와 B를 곱해 &amp;Delta;W = BA를 만들었는데, 이로 인한 모델의 보정값이 너무 약하거나 너무 셀 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 alpha를 곱해 LoRA의 개입 세기를 조절할 수 있는 것이다.&lt;/p&gt;
&lt;pre id=&quot;code_1782626807216&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;y = W x + (&amp;alpha; / r) &amp;middot; B A x&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;alpha가 크면 &amp;rarr; LoRA가 원본에&amp;nbsp;&lt;b&gt;더 강하게&lt;/b&gt;&amp;nbsp;개입&lt;/li&gt;
&lt;li&gt;alpha가 작으면 &amp;rarr;&amp;nbsp;&lt;b&gt;더 약하게&lt;/b&gt;&amp;nbsp;개입&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 alpha는 &lt;b&gt;&quot;이 LoRA를 통한 보정을 원본에 얼마나 크게 반영할까&quot;&lt;/b&gt; 를 정하는 볼륨 손잡이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;경험적으로는 보통 alpha = 2 &amp;times; r 정도로 잡는다(이번 실험도 그렇게 설정한다).&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면 LoRA는&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1) 원본 모델 가중치는 얼리고(freeze),&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2) 중요한 선형층만 골라(target modules), 거&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3) 기에 작은 두 행렬 A&amp;middot;B를 붙여(low-rank) &lt;b&gt;&lt;span&gt;&amp;nbsp;그&amp;nbsp;&lt;/span&gt;작은 행렬만 학습하고,&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4) alpha로 세기를 조절해 보정을 얼마나 원본에 크게 반영할지를 정하는 작업이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이걸로 &quot;학습할 파라미터 수&quot;는 확 줄었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 &lt;b&gt;얼려둔 원본 파라미터 30억 개가 메모리에 떡하니 올라가 있는 문제&lt;/b&gt;는 아직 그대로다.&lt;/p&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;rarr; 그걸 해결하는 게 다음 장의 &lt;b&gt;Q, 양자화다.&lt;/b&gt;&lt;/p&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;h3 data-ke-size=&quot;size23&quot;&gt;4. QLoRA의 Q &amp;mdash; 양자화로 4비트까지 줄이기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LoRA로 &quot;&lt;b&gt;① 학습해야 할 파라미터 수 자체&lt;/b&gt;&quot;는 줄였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 3장 끝에서 말했듯, &lt;b&gt;얼려둔 원본 30억 개 파라미터&lt;/b&gt;는 여전히 메모리에 통째로 올라가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;학습은 안 하더라도 forward pass에서 계속 쓰이니까 어딘가엔 있어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 덩치를 줄이는 게 QLoRA의 &lt;b&gt;Q, 양자화(Quantization)&lt;/b&gt; 다. (&lt;b&gt;② 파라미터 하나하나가 차지하는 메모리를 줄이기)&lt;/b&gt;&lt;/p&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;mdash; 파라미터 하나하나가 기본적으로 &lt;b&gt;32비트 부동소수점&lt;/b&gt;으로 저장되기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;계산해보면:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;30억 개 &amp;times; 32비트(= 4바이트) &amp;asymp; 13GB &lt;span&gt;&amp;nbsp; &lt;/span&gt;(강사의 말)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3B &quot;작은&quot; 모델인데도 32비트로 저장하면 가중치만 13GB다. 그럼 이렇게 생각해볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot;정확도를 유지한 채로 저장하지 말고, 각 가중치의 정밀도(precision)를 좀 낮추면 어떨까?&quot;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 공짜는 아니다. 정밀도를 거칠게 깎는 거라, 사전학습에서 배운 것을 일부 버리는 셈이긴 하다.&lt;/p&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;실제로 노트북에서 같은 모델을 정밀도만 바꿔가며 올려보고 get_memory_footprint()로 재면 이렇게 줄어든다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;정밀도&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;&lt;b&gt;메모리&lt;/b&gt;&lt;/span&gt;&lt;b&gt; footprint&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;기본&lt;/span&gt; &lt;span&gt;로드&lt;/span&gt; (bf16, 16&lt;span&gt;비트&lt;/span&gt;)&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;6.4 GB&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;8&lt;/span&gt;비트&lt;span&gt; &lt;/span&gt;양자화&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;3.6 GB&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;4&lt;span&gt;비트&lt;/span&gt; &lt;span&gt;양자화&lt;/span&gt; (NF4)&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;2.20 GB&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&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;p data-ke-size=&quot;size16&quot;&gt;잠깐, 앞에서 &quot;32비트면 13GB&quot;라고 했는데 표의 첫 줄은 왜 6.4GB일까? &amp;rarr; &lt;b&gt;Llama 3.2가 기본으로 bf16(16비트)으로 로드되기 때문이다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;30억 &amp;times; 2Byte(16bit) &amp;asymp; 6.4GB. 즉 13GB는 &quot;각 가중치가 32비트(4바이트)였다면&quot;의 이론값이고, 노트북이 아무 양자화 없이 라마 3.2 에서 기본 지원하는 bf16으로 올린 실측치가 6.4GB다.&lt;/p&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;여기서 한 발 더 나아가 8비트(3.6GB) &amp;rarr; 4비트(2.2GB)로 더 짓누르는 것이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bFYsCq/dJMcajbBfdE/18LyqeCTRyNex6cxWkZDBK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bFYsCq/dJMcajbBfdE/18LyqeCTRyNex6cxWkZDBK/img.png&quot; data-origin-width=&quot;1596&quot; data-origin-height=&quot;1356&quot; data-is-animation=&quot;false&quot; style=&quot;width: 32.5581%; margin-right: 10px;&quot; data-widthpercent=&quot;33.33&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bFYsCq/dJMcajbBfdE/18LyqeCTRyNex6cxWkZDBK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbFYsCq%2FdJMcajbBfdE%2F18LyqeCTRyNex6cxWkZDBK%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;1596&quot; height=&quot;1356&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dnKRTL/dJMcadWPuoF/rqGvU0jukakCckt680QyYk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dnKRTL/dJMcadWPuoF/rqGvU0jukakCckt680QyYk/img.png&quot; data-origin-width=&quot;1596&quot; data-origin-height=&quot;1356&quot; data-is-animation=&quot;false&quot; style=&quot;width: 32.5581%; margin-right: 10px;&quot; data-widthpercent=&quot;33.33&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dnKRTL/dJMcadWPuoF/rqGvU0jukakCckt680QyYk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdnKRTL%2FdJMcadWPuoF%2FrqGvU0jukakCckt680QyYk%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;1596&quot; height=&quot;1356&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/YgH6y/dJMcajvZWmS/tJe6QC9t6B8ik7fRpKPTZ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/YgH6y/dJMcajvZWmS/tJe6QC9t6B8ik7fRpKPTZ1/img.png&quot; data-origin-width=&quot;1596&quot; data-origin-height=&quot;1356&quot; data-is-animation=&quot;false&quot; style=&quot;width: 32.5581%;&quot; data-widthpercent=&quot;33.34&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/YgH6y/dJMcajvZWmS/tJe6QC9t6B8ik7fRpKPTZ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYgH6y%2FdJMcajvZWmS%2FtJe6QC9t6B8ik7fRpKPTZ1%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;1596&quot; height=&quot;1356&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;같은 모델인데 4비트로 올리면 2.2GB까지 내려간다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1782628625288&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;quant_config = BitsAndBytesConfig(

    load_in_4bit=True,

    bnb_4bit_use_double_quant=True,

    bnb_4bit_compute_dtype=torch.bfloat16,

    bnb_4bit_quant_type=&quot;nf4&quot;,

)&lt;/code&gt;&lt;/pre&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;p 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;load_in_4bit=True&lt;/b&gt;&amp;nbsp;&amp;mdash; 모델 가중치를 4비트로 저장한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;bnb_4bit_quant_type=&quot;nf4&quot;&lt;/b&gt;&amp;nbsp;&amp;mdash; 핵심. 4비트는 가질 수 있는 값이&amp;nbsp;&lt;b&gt;16개&lt;/b&gt;뿐인데, 그냥 균등하게 16칸으로 나누는 게 아니라&amp;nbsp;&lt;b&gt;정규분포에 맞춰&lt;/b&gt;&amp;nbsp;16개의 &quot;거친 위치&quot;를 배치한다. 모델 가중치 값들이 대체로 0 근처에 정규분포처럼 몰려 있으니, 그 분포를 반영한 NF4(Normal Float 4)가 일반 int4보다 정보를 덜 잃는다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;bnb_4bit_use_double_quant=True&lt;/b&gt;&amp;nbsp;&amp;mdash; 양자화하면서 생기는 보조 상수들까지&amp;nbsp;&lt;b&gt;한 번 더 압축&lt;/b&gt;해 메모리를 조금 더 아낀다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;bnb_4bit_compute_dtype=torch.bfloat16&lt;/b&gt;&amp;nbsp;&amp;mdash; 저장은 4비트로 하되,&amp;nbsp;&lt;b&gt;실제 계산은 bfloat16으로&lt;/b&gt;&amp;nbsp;한다. 정확도와 속도의 균형을 맞추는 부분이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 헷갈리기 쉬운 포인트가 하나 있다. &lt;b&gt;양자화하는 건 어디까지나 &quot;원본 base 모델&quot;뿐이고, 우리가 새로 붙여 학습할 LoRA 행렬(A&amp;middot;B)은 32비트 그대로 둔다.&lt;/b&gt; 무거운 원본은 4비트로 짓눌러 메모리를 아끼고, 정작 학습이 일어나는 작은 어댑터는 정밀도를 온전히 유지하는 것이다. &amp;rarr; 이래서 이름이 &lt;b&gt;Q + LoRA = QLoRA&lt;/b&gt;다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3장의 그림 위에 이 장을 얹으면 이렇게 정리된다.&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;LoRA&lt;/b&gt;&amp;nbsp;&amp;rarr; 학습할 파라미터 &quot;&lt;b&gt;개수&lt;/b&gt;&quot;를 줄인다 (원본은 얼리고 작은 A&amp;middot;B만)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Q (양자화)&lt;/b&gt;&amp;nbsp;&amp;rarr; 얼려둔 원본 파라미터 &quot;&lt;b&gt;하나당 메모리&lt;/b&gt;&quot;를 줄인다 (16/32비트 &amp;rarr; 4비트)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두&lt;span&gt; &lt;/span&gt;트릭을&lt;span&gt; &lt;/span&gt;합치니&lt;span&gt;, 13GB&lt;/span&gt;짜리&lt;span&gt; &lt;/span&gt;모델을&lt;span&gt; &lt;/span&gt;학습용으로도&lt;span&gt; &lt;/span&gt;단일&lt;span&gt; GPU&lt;/span&gt;에&lt;span&gt; &lt;/span&gt;욱여넣을&lt;span&gt; &lt;/span&gt;수&lt;span&gt; &lt;/span&gt;있게&lt;span&gt; &lt;/span&gt;된다&lt;span&gt;. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이게&lt;/b&gt;&lt;span&gt;&lt;b&gt; &quot;30&lt;/b&gt;&lt;/span&gt;&lt;b&gt;억&lt;/b&gt;&lt;span&gt;&lt;b&gt; &lt;/b&gt;&lt;/span&gt;&lt;b&gt;파라미터를&lt;/b&gt;&lt;span&gt;&lt;b&gt; &lt;/b&gt;&lt;/span&gt;&lt;b&gt;노트북&lt;/b&gt;&lt;span&gt;&lt;b&gt; GPU&lt;/b&gt;&lt;/span&gt;&lt;b&gt;에서&lt;/b&gt;&lt;span&gt;&lt;b&gt; &lt;/b&gt;&lt;/span&gt;&lt;b&gt;학습&lt;/b&gt;&lt;span&gt;&lt;b&gt;&quot;&lt;/b&gt;&lt;/span&gt;&lt;b&gt;이&lt;/b&gt;&lt;span&gt;&lt;b&gt; &lt;/b&gt;&lt;/span&gt;&lt;b&gt;가능한&lt;/b&gt;&lt;span&gt;&lt;b&gt; &lt;/b&gt;&lt;/span&gt;&lt;b&gt;진짜&lt;/b&gt;&lt;span&gt;&lt;b&gt; &lt;/b&gt;&lt;/span&gt;&lt;b&gt;이유다&lt;/b&gt;&lt;span&gt;&lt;b&gt;.&lt;/b&gt;&lt;/span&gt;&lt;/p&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;h3 data-ke-size=&quot;size23&quot;&gt;5. LoRA 어댑터는 실제로 얼마나 작을까&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LoRA가 &quot;작은 행렬만 학습한다&quot;고 계속 말했는데, 그래서 &lt;b&gt;얼마나&lt;/b&gt; 작은 걸까? 말로만 &quot;작다&quot;가 아니라 직접 측정해본다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어차피 LoRA 어댑터 A&amp;middot;B는 차원이 다 정해져 있으니, 행렬 원소 개수를 더하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 우리 모델(Llama 3.2 3B)의 선형층(&lt;b&gt;linear layer)&lt;/b&gt; 차원부터 정리하면 이렇다. (3장에서 본 q/k/v/o, MLP 층들의 실제 크기)&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;층&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;입력&lt;/b&gt;&lt;span&gt;&lt;b&gt; &amp;rarr; &lt;/b&gt;&lt;/span&gt;&lt;b&gt;출력&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;q_proj&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;3072 &amp;rarr; 3072&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;k_proj&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;3072 &amp;rarr; 1024&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;v_proj&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;3072 &amp;rarr; 1024&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;o_proj&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;3072 &amp;rarr; 3072&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;gate_proj&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;3072 &amp;rarr; 8192&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;up_proj&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;3072 &amp;rarr; 8192&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;down_proj&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;8192 &amp;rarr; 3072&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 층에 붙는 LoRA는 A(입력&amp;rarr;r)와 B(r&amp;rarr;출력) 두 행렬이다. 그러니 한 층의 LoRA 파라미터 수는 (입력 &amp;times; r) + (r &amp;times; 출력) 으로 떨어진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;① 가벼운 설정 (lite): r = 32, attention 4개 층만 LoRA 학습&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1782629129968&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;r = 32



# attention 층만 (각 층 = 입력&amp;times;r + r&amp;times;출력)

lora_q_proj = 3072*r + 3072*r

lora_k_proj = 3072*r + 1024*r

lora_v_proj = 3072*r + 1024*r

lora_o_proj = 3072*r + 3072*r



lora_layer = lora_q_proj + lora_k_proj + lora_v_proj + lora_o_proj

params = lora_layer * 28          # decoder layer가 28개

size = (params * 4) / 1_000_000   # 32비트(4바이트)로 저장



print(f&quot;Total params: {params:,}, size {size:,.1f}MB&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&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;b&gt;Total params: 18,350,080, size 73.4MB&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 30억 개짜리 파라미터를 가진 모델에 붙는 학습 대상이 &lt;b&gt;고작 1,835만 개, 용량으로 치면 약 73MB&lt;/b&gt;다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원본의 0.6%쯤밖에 안 된다. 학습이 끝나면 이 73MB짜리 어댑터만 따로 저장하면 되니,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모델 하나를 통째로 복제해 들고 다닐 필요가 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;② 본격 설정 (full): r = 256, attention + MLP 7개 층 전부 LoRA 학습&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 계산을 r을 256으로 키우고, MLP 3개 층(gate/up/down)까지 target에 넣어서 돌리면:&lt;/p&gt;
&lt;pre id=&quot;code_1782629225910&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;r = 256

# attention 4개 + MLP 3개 (gate/up/down: 3072*r + 8192*r ...)

# ... 같은 방식으로 7개 층을 28번&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출력:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Total params: 389,021,696, size 1,556.1MB&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 이쪽은 &lt;b&gt;약 3.9억 개, 1.5GB&lt;/b&gt;짜리 어댑터다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;lite보다 20배 넘게 크다. 강사도 이건 &lt;b&gt;&quot;꽤 극단적인(aggressive) LoRA 설정&quot;&lt;/b&gt; 이라고 부른다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;학습 데이터가 워낙 많아서(full 데이터셋) &quot;그만큼 많이 학습시켜보겠다&quot;는 의도로 일부러 r과 target을 키운 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 설정을 나란히 놓으면 3장에서 깔아둔 &quot;linear layer에서 어디까지 건드릴지가 학습량을 정한다&quot;는 말이 숫자로 와닿는다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;설정&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;r&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;target &lt;/b&gt;&lt;span&gt;&lt;b&gt;층&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;학습&lt;/b&gt;&lt;span&gt;&lt;b&gt; &lt;/b&gt;&lt;/span&gt;&lt;b&gt;파라미터&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;어댑터&lt;/b&gt;&lt;span&gt;&lt;b&gt; &lt;/b&gt;&lt;/span&gt;&lt;b&gt;용량&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;lite&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;32&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;attention 4&lt;span&gt;개&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;약&lt;/span&gt; 1,835&lt;span&gt;만&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;73 MB&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;full&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;256&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;attention + MLP 7&lt;span&gt;개&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;약&lt;/span&gt; 3.9&lt;span&gt;억&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;1,556 MB&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&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;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/26WPe/dJMcaa6V4zs/Pa7OJePTYktbeXlOMLCFI0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/26WPe/dJMcaa6V4zs/Pa7OJePTYktbeXlOMLCFI0/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;1532&quot; data-origin-height=&quot;798&quot; data-filename=&quot;스크린샷 2026-06-28 오후 3.50.09.png&quot; style=&quot;width: 52.7671%; margin-right: 10px;&quot; data-widthpercent=&quot;53.39&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/26WPe/dJMcaa6V4zs/Pa7OJePTYktbeXlOMLCFI0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F26WPe%2FdJMcaa6V4zs%2FPa7OJePTYktbeXlOMLCFI0%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;1532&quot; height=&quot;798&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bxFqEB/dJMcahdTMH0/rMU5rhmlQhlOwZ3JdjvRIk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bxFqEB/dJMcahdTMH0/rMU5rhmlQhlOwZ3JdjvRIk/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;1532&quot; data-origin-height=&quot;914&quot; data-filename=&quot;스크린샷 2026-06-28 오후 3.50.26.png&quot; data-widthpercent=&quot;46.61&quot; style=&quot;width: 46.0701%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bxFqEB/dJMcahdTMH0/rMU5rhmlQhlOwZ3JdjvRIk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbxFqEB%2FdJMcahdTMH0%2FrMU5rhmlQhlOwZ3JdjvRIk%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;1532&quot; height=&quot;914&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;어디까지&amp;middot;얼마나(r) 건드리느냐에 따라 어댑터 크기가 73MB ~ 1.5GB로 달라진다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어느 쪽이든 핵심은 같다. &lt;b&gt;30억 개짜리 원본은 손도 안 대고, 그 옆에 붙인 작은 어댑터(73MB ~ 1.5GB)만 학습한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 &quot;학습 대상&quot;을 잘게 줄여놓은 덕분에, 4장의 양자화와 합쳐 단일 GPU 학습이 현실이 되는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음&lt;span&gt; &lt;/span&gt;장부터는&lt;span&gt; &lt;/span&gt;이론을&lt;span&gt; &lt;/span&gt;떠나&lt;span&gt;, &lt;/span&gt;이&lt;span&gt; &lt;/span&gt;모델에게&lt;span&gt; &lt;/span&gt;먹일&lt;span&gt; &lt;/span&gt;&lt;b&gt;학습&lt;/b&gt;&lt;span&gt;&lt;b&gt; &lt;/b&gt;&lt;/span&gt;&lt;b&gt;데이터를&lt;/b&gt;&lt;span&gt;&lt;b&gt; &lt;/b&gt;&lt;/span&gt;&lt;b&gt;직접&lt;/b&gt;&lt;span&gt;&lt;b&gt; &lt;/b&gt;&lt;/span&gt;&lt;b&gt;만드는&lt;/b&gt;&lt;span&gt;&lt;b&gt; &lt;/b&gt;&lt;/span&gt;&lt;b&gt;이야기&lt;/b&gt;로&lt;span&gt; &lt;/span&gt;넘어간다&lt;span&gt;.&lt;/span&gt;&lt;/p&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;h3 data-ke-size=&quot;size23&quot;&gt;6. 학습 데이터 만들기 &amp;mdash; 프롬프트 설계&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지가 Day 1, &quot;어떻게 작은 GPU에 욱여넣나(QLoRA)&quot;였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Day 2부터는 모델에게 먹일 &lt;b&gt;데이터&lt;/b&gt;를 만진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;학습 코드를 돌리기 전에, 모델이 보게 될 입력과 정답을 어떤 모양으로 빚을지부터 정해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터셋은 강사가 미리 만들어 HuggingFace에 올려둔 걸 쓴다. 이번 글은 가벼운 쪽(lite)으로 진행한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ed-donner/items_prompts_lite&amp;nbsp;&amp;mdash; train 20,000 / val 1,000 / test 1,000&lt;/li&gt;
&lt;li&gt;(full 버전은 train 80만 / val&amp;middot;test 1만 &amp;mdash; 다음 글의 본격 학습용)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 샘플은 이렇게 생겼다. 1장에서 봤던 그 형태다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1782629830260&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{

 'prompt': 'What does this cost to the nearest dollar?\n\n'

           'Title: Schlage F59 ... Interior Knob ...\n\n'

           'Price is $',

 'completion': '64.00'

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모델 입장에서 할 일은 단순하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Price is $ 로 끝나는 prompt를 받아, 그 뒤에 올 completion(64.00)을 이어서 생성하는 것.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 prompt/completion 한 쌍을 만드는 게 make_prompts 메서드다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1782629989512&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;def make_prompts(self, tokenizer, max_tokens, do_round):

    tokens = tokenizer.encode(self.summary, add_special_tokens=False)

    if len(tokens) &amp;gt; max_tokens:

        summary = tokenizer.decode(tokens[:max_tokens]).rstrip()

    else:

        summary = self.summary

    self.prompt = f&quot;{QUESTION}\n\n{summary}\n\n{PREFIX}&quot;

    self.completion = f&quot;{round(self.price)}.00&quot; if do_round else str(self.price)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 설계 결정이 두 개 숨어 있다. &lt;b&gt;(1) summary를 110 토큰에서 자른다&lt;/b&gt;,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 &lt;b&gt;(2) 정답을 반올림할지 말지를 train/test에서 다르게 둔다.&lt;/b&gt; 하나씩 보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;① 프롬프트 구조 &amp;mdash; 왜 Price is $ 까지 미리 넣을까&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프롬프트는 세 조각을 이어 붙인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;What does this cost to the nearest dollar? &lt;span&gt;&amp;nbsp; &lt;/span&gt;&amp;larr; QUESTION&lt;/p&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;lt;상품 summary&amp;gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Price is $&lt;span&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;/span&gt;&amp;larr; PREFIX&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 포인트는 마지막 PREFIX = &quot;Price is $&quot; 에 &lt;b&gt;$ 기호까지 미리 박아둔다&lt;/b&gt;는 점이다. 모델은 base 모델(다음 토큰을 이어 쓰는 모델)이라, 프롬프트를 $ 로 끝내두면 그 뒤에 자연스럽게 &lt;b&gt;숫자만&lt;/b&gt; 이어 쓰게 유도된다. &quot;가격이 얼마인가요?&quot;라고 열어두는 것보다 출력이 훨씬 깔끔해진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;② CUTOFF = 110 &amp;mdash; summary를 잘라내는 이유&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;make_prompts의 앞부분은 summary를 토큰화해서 &lt;b&gt;110개를 넘으면 앞 110개만 남기고 자른다&lt;/b&gt;(tokens[:max_tokens]). 왜 굳이 길이 상한을 둘까? 노트에 네 가지로 정리해뒀다.&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;&amp;nbsp;&amp;mdash; 트랜스포머의 attention은 시퀀스 길이에 대해&amp;nbsp;&lt;b&gt;O(n&amp;sup2;)&lt;/b&gt;&amp;nbsp;다. summary가 길수록 메모리&amp;middot;시간이 급격히 는다. &lt;br /&gt;110으로 자르면 batch를 키울 수 있고 한 epoch이 훨씬 빨라진다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;패딩 낭비 줄이기&lt;/b&gt;&amp;nbsp;&amp;mdash; 배치 학습은 시퀀스를 같은 길이로 맞춰 패딩한다. 일부 샘플만 200~500 토큰이면 나머지도 거기 맞춰 패딩돼, 계산량 대부분이 의미 없는&amp;nbsp;[PAD]&amp;nbsp;토큰에 쓰인다. 상한을 두면 이 낭비가 준다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;정답 토큰 보호&lt;/b&gt;&amp;nbsp;&amp;mdash; 이게 제일 중요하다. 프롬프트는&amp;nbsp;... summary ... Price is $123.00&amp;nbsp;순서인데, summary가 너무 길면 모델 max length에 걸려&amp;nbsp;&lt;b&gt;맨 뒤의&amp;nbsp;123.00(= loss를 계산하는 정답)이 잘려나간다.&lt;/b&gt;&amp;nbsp;summary를 미리 잘라두면 정답 토큰은 항상 살아남는다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;노이즈 차단&lt;/b&gt;&amp;nbsp;&amp;mdash; 긴 상품 설명엔 스펙 나열&amp;middot;마케팅 문구 같은 군더더기가 많다. 핵심 정보가 앞쪽 110 토큰에 몰려 있다고 보고 잘라내,&amp;nbsp;&lt;b&gt;신호 대 잡음비&lt;/b&gt;를 높인다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 왜 하필 110일까? 셀 위쪽에서 전체 데이터의 &lt;b&gt;토큰 수 히스토그램&lt;/b&gt;을 그려보고, &lt;b&gt;잘려나가는 샘플 비율이 아주 작게(몇 %) 떨어지는 지점&lt;/b&gt;으로 잡은 값이다. 대부분 데이터는 손실 없이 두고, 소수의 outlier만 truncate되도록 한 것이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;934&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c3FFL1/dJMcaiKyUtK/hXX2BdelRiqkLwY3XDUCP1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c3FFL1/dJMcaiKyUtK/hXX2BdelRiqkLwY3XDUCP1/img.png&quot; data-alt=&quot;분포를 보고, 잘려나가는 샘플이 몇 %뿐인 지점으로 CUTOFF=110을 잡았다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c3FFL1/dJMcaiKyUtK/hXX2BdelRiqkLwY3XDUCP1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc3FFL1%2FdJMcaiKyUtK%2FhXX2BdelRiqkLwY3XDUCP1%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;2048&quot; height=&quot;934&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;934&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;분포를 보고, 잘려나가는 샘플이 몇 %뿐인 지점으로 CUTOFF=110을 잡았다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;곁다리 하나: Llama 토크나이저는 &lt;b&gt;0~999를 토큰 하나로&lt;/b&gt; 처리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가격 대부분이 이 범위에 들어가니, 정답 가격이 토큰 1개로 깔끔하게 떨어진다 &amp;rarr; 이 가격 예측 태스크에 Llama가 잘 맞는 이유 중 하나다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;③ do_round &amp;mdash; train은 반올림, test는 원본 가격&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막 줄에서 정답(completion)을 만들 때 do_round 로 분기한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;do_round=True&amp;nbsp;&amp;rarr;&amp;nbsp;f&quot;{round(price)}.00&quot;&amp;nbsp;(예:&amp;nbsp;123.00)&lt;/li&gt;
&lt;li&gt;do_round=False&amp;nbsp;&amp;rarr;&amp;nbsp;str(price)&amp;nbsp;(예:&amp;nbsp;123.45)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 데이터를 두 루프로 나눠, &lt;b&gt;train/val에는 True, test에는 False&lt;/b&gt; 를 준다.&lt;/p&gt;
&lt;pre id=&quot;code_1782630360993&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;for item in train + val:

    item.make_prompts(tokenizer, CUTOFF, True)   # 정답을 반올림 &amp;rarr; &quot;123.00&quot;

for item in test:

    item.make_prompts(tokenizer, CUTOFF, False)  # 정답을 원본 그대로 &amp;rarr; &quot;123.45&quot;&lt;/code&gt;&lt;/pre&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;p 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;train/val을 반올림하는 이유&lt;/b&gt;: 정답을 정수로 만들면(.00&amp;nbsp;고정) 모델이 배울 패턴이 단순해진다. 사실상 &quot;정수 분류&quot;에 가까워져 출력 공간이 줄고, 토큰도 일관되며, 수렴이 빨라진다. 1센트 단위까지 맞히려 들면 학습이 어렵고 노이즈만 는다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;test를 원본으로 두는 이유&lt;/b&gt;: 평가는&amp;nbsp;&lt;b&gt;진짜 정답&lt;/b&gt;으로 해야 공정하다. 학습은 반올림 값으로 했어도, 최종 채점은 실제 가격($123.45)과 모델 예측을 비교해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 루프를 돌고 나면 모든 샘플에 prompt(... Price is $로 끝남)와 completion(정답 숫자)이 채워지고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;{&quot;prompt&quot;: ..., &quot;completion&quot;: ...} 형태로 HuggingFace Hub에 올라가 Day 3의 학습에 그대로 쓰인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터&lt;span&gt; &lt;/span&gt;준비가&lt;span&gt; &lt;/span&gt;끝났으니&lt;span&gt;, &lt;/span&gt;이제&lt;span&gt; &lt;/span&gt;마지막으로&lt;span&gt; &amp;mdash; &lt;/span&gt;학습을&lt;span&gt; &lt;/span&gt;시작하기&lt;span&gt; &lt;/span&gt;&lt;b&gt;전에&lt;/b&gt;&lt;span&gt; &lt;/span&gt;베이스&lt;span&gt; &lt;/span&gt;모델(라마 3.2)이&lt;span&gt; &lt;/span&gt;이&lt;span&gt; &lt;/span&gt;문제를&lt;span&gt; &lt;/span&gt;얼마나&lt;span&gt; &lt;/span&gt;못&lt;span&gt; &lt;/span&gt;푸는지&lt;span&gt;(= &lt;/span&gt;우리의&lt;span&gt; &lt;/span&gt;비교&lt;span&gt; &lt;/span&gt;기준선&lt;span&gt;)&lt;/span&gt;부터&lt;span&gt; &lt;/span&gt;재본다&lt;span&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7. 파인튜닝 전, 베이스 모델은 얼마나 못할까&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 글의 마지막 단계다. &lt;b&gt;학습을 시작하기 전에&lt;/b&gt;, 베이스 모델 Llama 3.2 3B가 가격 예측을 얼마나 못 하는지부터 재둔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 숫자가 곧 다음 글에서 &quot;파인튜닝이 얼마나 끌어올렸나&quot;를 가늠할 &lt;b&gt;비교 기준선(baseline)&lt;/b&gt; 이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(이전 글(Week 6)에서도 강조했지만, 기준선 없는 성능 숫자는 의미가 없다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;왜 base 모델인가 &amp;mdash; instruct(챗) 모델이 아니라&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 짚고 갈 게 하나 있다. 우리가 쓰는 건 Llama-3.2-3B, 즉 &lt;b&gt;base 모델&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 우리가 챗으로 쓰는 모델은 그 위에 다시 학습을 얹은 &lt;b&gt;instruct(챗) variant&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;base 모델&lt;/b&gt;&amp;nbsp;&amp;mdash; 인풋 토큰이 들어오면&amp;nbsp;&lt;b&gt;그다음에 올 가장 가능성 높은 토큰을 예측&lt;/b&gt;하도록만 학습된 모델. (순수한 &quot;이어 쓰기&quot; 기계)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;instruct 모델&lt;/b&gt;&amp;nbsp;&amp;mdash; 거기에 &quot;지시를 따르고 대화하도록&quot; 추가 학습을 얹은 모델.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 6장에서 system/user 프롬프트 같은 챗 형식을 안 쓰고 그냥 ... Price is $ 로 끝나는 평문을 던진 것이다. 우리가 하려는 건 대화가 아니라 &lt;b&gt;&quot;가격 예측&quot;이라는 새 기술 하나를 가르치는 것&lt;/b&gt;이고, 이렇게 단일 기술을 가르칠 땐 잡다한 지침이 안 붙은 &lt;b&gt;base 모델이 오히려 더 깔끔한 출발점&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;모델 로드 &amp;mdash; pad_token 트릭&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;베이스 모델은 4장의 그 4비트 설정(NF4)으로 올린다. 토크나이저 쪽에 작은 트릭이 하나 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1782630648332&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;tokenizer.pad_token = tokenizer.eos_token   # Llama엔 pad 토큰이 따로 없어서

tokenizer.padding_side = &quot;right&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Llama 계열은 패딩 토큰이 따로 없어서, 문장 끝 토큰(eos)을 패딩용으로 빌려 쓰는 게 관례다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예측 함수 &amp;mdash; model_predict&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;베이스 모델에게 한 상품을 물어보는 함수는 이렇게 생겼다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1782630661592&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;def model_predict(item):

    inputs = tokenizer(item[&quot;prompt&quot;], return_tensors=&quot;pt&quot;).to(&quot;cuda&quot;)

    with torch.no_grad():

        output_ids = base_model.generate(**inputs, max_new_tokens=8)

    prompt_len = inputs[&quot;input_ids&quot;].shape[1]

    generated_ids = output_ids[0, prompt_len:]

    return tokenizer.decode(generated_ids)&lt;/code&gt;&lt;/pre&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;p 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;max_new_tokens=8&lt;/b&gt;&amp;nbsp;&amp;mdash; 새로 만들 토큰을 8개로 제한한다. 우리가 원하는 건&amp;nbsp;249.99&amp;nbsp;같은 짧은 숫자뿐이라, 8개면 충분하고 길게 풀면 모델이 횡설수설하며 시간만 쓴다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;output_ids[0, prompt_len:]&lt;/b&gt;&amp;nbsp;&amp;mdash;&amp;nbsp;generate()는&amp;nbsp;&lt;b&gt;입력 프롬프트 + 새 토큰&lt;/b&gt;을 합쳐서 돌려준다. 그래서 입력 길이(prompt_len)만큼 잘라내고&amp;nbsp;&lt;b&gt;새로 생성된 부분만&lt;/b&gt;&amp;nbsp;디코딩한다. 안 그러면 프롬프트가 통째로 따라 나와 후처리가 번거롭다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;실제로 돌려보면 &amp;mdash; 처참하다&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;test 첫 샘플(&lt;b&gt;정답 $219&lt;/b&gt;)을 던지면, 베이스 모델은 이렇게 답한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정답: &lt;span&gt;&amp;nbsp; &lt;/span&gt;$219&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모델 출력:&lt;span&gt;&amp;nbsp; &lt;/span&gt;'179.00. The pedal is currently'&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가격을 $179로 빗나간 것도 문제지만, 더 눈에 띄는 건 &lt;b&gt;숫자만 뱉고 멈추질 못하고 &quot;The pedal is currently...&quot; 하고 문장을 이어 쓰려 한다&lt;/b&gt;는 점이다. 그야 그럴 게, 이 모델은 아직 &quot;가격만 답하는 법&quot;을 배운 적이 없다. 그냥 &quot;그럴듯한 다음 토큰&quot;을 이어 쓰는 base 모델일 뿐이니까.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 식으로 test 1,000건 전체를 evaluate(model_predict, test)로 돌려 평균 오차를 내면 결과는 이렇다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Base Llama 3.2 (4bit) &amp;mdash; 평균 오차 $185.52&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1960&quot; data-origin-height=&quot;1302&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cFxFyx/dJMcahLHAt2/GsGJYw2Zk6JEg79XFEXQ2K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cFxFyx/dJMcahLHAt2/GsGJYw2Zk6JEg79XFEXQ2K/img.png&quot; data-alt=&quot;파인튜닝 전 베이스 모델의 평균 오차 $185.52 &amp;amp;mdash; 지금은 전 모델 중 꼴찌다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cFxFyx/dJMcahLHAt2/GsGJYw2Zk6JEg79XFEXQ2K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcFxFyx%2FdJMcahLHAt2%2FGsGJYw2Zk6JEg79XFEXQ2K%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;1960&quot; height=&quot;1302&quot; data-origin-width=&quot;1960&quot; data-origin-height=&quot;1302&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;파인튜닝 전 베이스 모델의 평균 오차 $185.52 &amp;mdash; 지금은 전 모델 중 꼴찌다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 $185.52가 어느 정도냐면, 전체 성적표에서 &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;사람(강사 Ed)이 직접 맞힌 오차:&amp;nbsp;&lt;b&gt;$87.62&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;GPT-4.1-nano 제로샷:&amp;nbsp;&lt;b&gt;$62.51&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금의 베이스 모델은 사람은커녕 작은 프론티어 모델에도 한참 못 미친다. &amp;rarr; 바로 이 &lt;b&gt;$185.52&lt;/b&gt;가 다음 글의 출발선이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;마무리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 모델을 한 번도 &quot;학습&quot;시키지 않았다. 대신 학습을 시작하기 위한 판을 깔았다.&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;&amp;nbsp;30억 파라미터를 그냥 학습할 수 없는지 (메모리 벽)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;LoRA&lt;/b&gt;&amp;nbsp;&amp;mdash; 원본은 얼리고 작은 어댑터 A&amp;middot;B만 학습 (학습 파라미터 수 &amp;darr;)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Q(양자화)&lt;/b&gt;&amp;nbsp;&amp;mdash; 얼려둔 원본을 4비트로 (파라미터당 메모리 &amp;darr;)&lt;/li&gt;
&lt;li&gt;어댑터가 실제로 얼마나 작은지 (73MB ~ 1.5GB)&lt;/li&gt;
&lt;li&gt;데이터/프롬프트 설계 (CUTOFF=110, do_round)&lt;/li&gt;
&lt;li&gt;그리고 베이스라인 측정 &amp;mdash;&amp;nbsp;&lt;b&gt;$185.52, 현재 꼴찌&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;느낀점&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;-&amp;nbsp;&lt;/b&gt;말로만 LoRA, QLoRA 이야기를 많이 들었었는데 실제로 모델의 행렬계산 단에서 어떻게 작동하는 것인지&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떻게 적은 GPU로 파인튜닝이 가능했었던 것인지 실제 LoRA의 원리를 뜯어보며 이해할 수 있던 게 의미 있었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 그리고 실제로 코드로 찍어보며 모델의 크기가 양자화로 줄어드는 과정, LoRA 어댑터는 어느정도 사이즈인지 등을 재볼 수 있던 것 자체가 의미가 있었다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 추가로 base 모델과 instruct 모델의 차이를 보는 것도 의미가 있었는데, 단순 다음 토큰을 예측하도록만 작동이 되는 base 모델이 오히려 이런 가격 예측 task와 같은 한정적인 상황에서는 파인튜닝할 때 더 적합한 모델이라는 점이 인상적이었다. (잡다한 대화 지침등이 안붙어있는)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Llama 토크나이저는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;0~999를 토큰 하나로&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;처리하는 것을 파악해, &quot;가격 대부분이 이 범위에 들어가니, 정답 가격이 토큰 1개로 깔끔하게 떨어진다 &amp;rarr; 이 가격 예측 태스크에 Llama가 잘 맞을 거다&quot; 를 생각해내는 강사가 대단하다고 느껴졌다. 결국은 해결하려는 비즈니스에 맞는 파인튜닝 모델 선정도 중요하다고 느꼈다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- train/val 부분과 test 부분에서 가격 예측을 측정할떄 한쪽은 반올림을 진행하고, 한쪽은 없이 진행하는 것도 인상적이었다. (모델이 배울 패턴을 단순화 시켜서 학습 효율을 높이고, 대신 실제 측정은 공정성을 위해 반올림 없이 진행하는 등)&lt;/p&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;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;p data-ke-size=&quot;size16&quot;&gt;다음 글(글 16)에서는 드디어 이 판 위에서 &lt;b&gt;실제로 학습을 돌린다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1) 학습을 좌우하는 하이퍼파라미터들(epochs&amp;middot;batch&amp;middot;learning rate 등),&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2) 학습이 forward &amp;rarr; loss &amp;rarr; backward &amp;rarr; 업데이트로 도는 4단계에 대한 정리&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3)&amp;nbsp;그리고&lt;span&gt; 글 초반에&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;흘린&lt;span&gt; &lt;/span&gt;스포일러에 대한 회수와 (라마 3.2 모델이 어떻게 GPT 5.1까지 이기게 되는지)&lt;span&gt;&amp;nbsp;&lt;/span&gt;같은&lt;span&gt; &quot;&lt;/span&gt;파인튜닝&lt;span&gt;&quot;&lt;/span&gt;인데&lt;span&gt; Week 6&lt;/span&gt;과&lt;span&gt; &lt;/span&gt;왜&lt;span&gt; &lt;/span&gt;결과가&lt;span&gt; &lt;/span&gt;정반대인지도&lt;span&gt; &lt;/span&gt;그때&lt;span&gt; &lt;/span&gt;정리하겠다&lt;span&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;다음 글에서 오픈소스 모델을 파인튜닝한 결과로 찾아오겠습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;(꾸준히 공부하고&lt;span&gt;&amp;nbsp;적을테니&amp;nbsp;&lt;/span&gt;많은 관심 부탁드립니다.)&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;Profile:&lt;/span&gt;&lt;br /&gt;&lt;a style=&quot;color: #0070d1; text-align: start;&quot; href=&quot;http://www.linkedin.com/in/doobeom-kim-%EA%B9%80%EB%91%90%EB%B2%94-2b9649242&quot;&gt;&lt;span&gt;&lt;span style=&quot;color: #0070d1;&quot;&gt;Linkedin&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>AI 공부</category>
      <category>ai</category>
      <category>AI 공부</category>
      <category>GPT</category>
      <category>GPU</category>
      <category>Llama</category>
      <category>llm</category>
      <category>lora</category>
      <category>생성형 AI</category>
      <category>오픈소스 모델</category>
      <category>파인튜닝</category>
      <author>kdb1248</author>
      <guid isPermaLink="true">https://doobeom-coding.tistory.com/92</guid>
      <comments>https://doobeom-coding.tistory.com/92#entry92comment</comments>
      <pubDate>Sun, 28 Jun 2026 16:45:33 +0900</pubDate>
    </item>
    <item>
      <title>14. 내 데이터로 GPT를 파인튜닝하면 더 똑똑해질까 - 가격 예측 모델 실험기</title>
      <link>https://doobeom-coding.tistory.com/91</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;목차&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;0. 들어가며&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1.&amp;nbsp;모델&amp;nbsp;간&amp;nbsp;성능&amp;nbsp;비교를&amp;nbsp;위한&amp;nbsp;기준&amp;nbsp;설정&lt;br /&gt;2.&amp;nbsp;베이스라인부터&amp;nbsp;전통&amp;nbsp;머신러닝까지&lt;br /&gt;3.&amp;nbsp;딥러닝&amp;nbsp;-&amp;nbsp;신경망으로&amp;nbsp;한&amp;nbsp;단계&amp;nbsp;더&lt;br /&gt;4.&amp;nbsp;학습&amp;nbsp;없이&amp;nbsp;이긴다&amp;nbsp;-&amp;nbsp;제로샷&amp;nbsp;프론티어&amp;nbsp;모델&lt;br /&gt;5.&amp;nbsp;프론티어&amp;nbsp;모델을&amp;nbsp;직접&amp;nbsp;파인튜닝해보다&lt;br /&gt;6.&amp;nbsp;의외의&amp;nbsp;결과,&amp;nbsp;그리고&amp;nbsp;그게&amp;nbsp;알려주는&amp;nbsp;것&lt;br /&gt;7.&amp;nbsp;마무리&amp;nbsp;-&amp;nbsp;전체&amp;nbsp;성적표와&amp;nbsp;다음&amp;nbsp;이야기&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;0. 들어가며&lt;/h3&gt;
&lt;p style=&quot;background-color: #fdfdfc; color: #0a0a0a; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;지난 글에서는 Week 6 프로젝트 - 상품 설명 기반 가격 예측 (THE PRICE IS RIGHT)의 앞부분 데이터 전처리를 다뤘다.&lt;/p&gt;
&lt;p style=&quot;background-color: #fdfdfc; color: #0a0a0a; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;아마존 상품 데이터를 긁어와 정제하고, 학습 80만 / 검증 1만 / 테스트 1만 건의 데이터셋을 만들어 허깅페이스 허브에 올리는 데까지가 지난 글의 내용이었다.&lt;/p&gt;
&lt;p style=&quot;background-color: #fdfdfc; color: #0a0a0a; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이번 글은 정제한 데이터를 기반으로&lt;/p&gt;
&lt;p style=&quot;background-color: #fdfdfc; color: #0a0a0a; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;(1) 전통 머신러닝부터&lt;/p&gt;
&lt;p style=&quot;background-color: #fdfdfc; color: #0a0a0a; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;(2) 딥러닝과 제로샷 프론티어 모델&lt;/p&gt;
&lt;p style=&quot;background-color: #fdfdfc; color: #0a0a0a; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;(3) 프론티어 모델을 직접 파인튜닝하는 해&lt;/p&gt;
&lt;p style=&quot;background-color: #fdfdfc; color: #0a0a0a; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;차례로 가격 예측을 돌려본다.&lt;/p&gt;
&lt;p style=&quot;background-color: #fdfdfc; color: #0a0a0a; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #fdfdfc; color: #0a0a0a; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;강의 Week 6의 Day 3-5에 해당한다.&lt;/p&gt;
&lt;p style=&quot;background-color: #fdfdfc; color: #0a0a0a; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;미리 말하면, 이 글에는 작은 반전이 하나 있다.&lt;/p&gt;
&lt;p style=&quot;background-color: #fdfdfc; color: #0a0a0a; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&quot;프론티어 모델을 내 데이터로 파인튜닝하면 더 좋아지겠지&quot;라는 기대가 보기 좋게 빗나간다.&lt;/p&gt;
&lt;p style=&quot;background-color: #fdfdfc; color: #0a0a0a; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그리고 그 빗나감이 다음 Week 7(오픈소스 모델 QLoRA 파인튜닝)으로 넘어가는 가장 중요한 동기가 된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 style=&quot;background-color: #fdfdfc; color: #0a0a0a; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;1. 모델 간 성능 비교를 위한 기준 설정&lt;/h3&gt;
&lt;p style=&quot;background-color: #fdfdfc; color: #0a0a0a; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;여러 모델을 비교하려면, &quot;무엇으로 성능을 비교할 것인가&quot;부터 정해야 한다.&lt;/p&gt;
&lt;p style=&quot;background-color: #fdfdfc; color: #0a0a0a; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Day 3은 모델을 돌리는 코드 보다 평가 체계(evaluation) 부터 시작한다.&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;background-color: #fdfdfc; color: #0a0a0a; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;1-1) 세 가지 지표 - Error, MSE, R&amp;sup2;&lt;/h4&gt;
&lt;p style=&quot;background-color: #fdfdfc; color: #0a0a0a; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 프로젝트는 가격을 맞히는 회귀 문제라, 세 가지 지표를 같이 본다.&lt;/p&gt;
&lt;p style=&quot;background-color: #fdfdfc; color: #0a0a0a; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;먼저&lt;/p&gt;
&lt;p style=&quot;background-color: #fdfdfc; color: #0a0a0a; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. Error&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #fdfdfc; color: #0a0a0a; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;곧 평균 절대 오차(MAE)다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;average_error = sum(errors) / size &lt;br /&gt;# errors = abs(예측값 - 실제값)&lt;/blockquote&gt;
&lt;p style=&quot;background-color: #fdfdfc; color: #0a0a0a; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #fdfdfc; color: #0a0a0a; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&quot;평균적으로 얼마나 틀렸나&quot;를 그대로 보여주는, 가장 직관적인 지표다.&lt;/p&gt;
&lt;p style=&quot;background-color: #fdfdfc; color: #0a0a0a; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;측정 단위가 달러($)라&lt;span&gt;&amp;nbsp;&lt;/span&gt;Error: $42.50이면 &quot;평균적으로 실제 가격과 42.50달러 차이로 예측했다&quot;는 뜻이고, 낮을수록 좋다.&lt;/p&gt;
&lt;p style=&quot;background-color: #fdfdfc; color: #0a0a0a; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 글의 모든 모델은 이 MAE 하나로 줄 세운다.&lt;/p&gt;
&lt;p style=&quot;background-color: #fdfdfc; color: #0a0a0a; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #fdfdfc; color: #0a0a0a; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. MSE(평균 제곱 오차)&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #fdfdfc; color: #0a0a0a; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;mse = mean_squared_error(truths, guesses)&lt;br /&gt;# = 평균((예측값 - 실제값)^2)&lt;/blockquote&gt;
&lt;div style=&quot;background-color: #fdfdfc; color: #0a0a0a; text-align: start;&quot;&gt;
&lt;div data-code-text=&quot;mse = mean_squared_error(truths, guesses)
# = 평균((예측값 - 실제값)^2)&quot;&gt;
&lt;div style=&quot;background-color: #fdfdfc;&quot; data-gutter=&quot;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p style=&quot;background-color: #fdfdfc; color: #0a0a0a; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;오차를 제곱해서 평균낸 값이라, 크게 틀린 예측에 훨씬 큰 패널티를 준다(오차 100달러면 10,000점).&lt;/p&gt;
&lt;p style=&quot;background-color: #fdfdfc; color: #0a0a0a; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;단위가 달러 제곱이라 직관적이진 않지만, 모델 사이의 미세한 차이를 비교할 때 쓸모가 있다.&lt;/p&gt;
&lt;p style=&quot;background-color: #fdfdfc; color: #0a0a0a; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #fdfdfc; color: #0a0a0a; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. R&amp;sup2;(결정계수)&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #fdfdfc; color: #0a0a0a; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;한마디로 설명하면 &quot;이 모델이 그냥 평균값만 답하는 것보다 얼마나 더 나은가&quot;다.&lt;/p&gt;
&lt;p style=&quot;background-color: #fdfdfc; color: #0a0a0a; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;(대학교 때 배웠던 기초 통계학 부분을 다시 공부하는 느낌이었다)&lt;/p&gt;
&lt;div style=&quot;background-color: #fdfdfc; color: #0a0a0a; text-align: start;&quot;&gt;
&lt;div data-code-text=&quot;R&amp;sup2; = 1 - (내 모델의 오차 합) / (평균만 쓸 때의 오차 합)&quot;&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;R&amp;sup2; = 1 - (내 모델의 오차 합) / (평균만 쓸 때의 오차 합)&lt;/blockquote&gt;
&lt;div style=&quot;background-color: #fdfdfc;&quot; data-gutter=&quot;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #fdfdfc; color: #0a0a0a; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;100%: 완벽한 예측&lt;/li&gt;
&lt;li&gt;0%: 그냥 전체 평균을 답하는 것과 똑같음&lt;/li&gt;
&lt;li&gt;음수: 평균을 답하는 것보다도 못함&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #fdfdfc; color: #0a0a0a; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;가격 예측에서 가장 게으른 모델은 &quot;모든 상품 = 전체 평균 가격&quot;이라고 답하는 것인데,&lt;/p&gt;
&lt;p style=&quot;background-color: #fdfdfc; color: #0a0a0a; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;R&amp;sup2;는 그 게으른 기준선 대비 내 모델이 얼마나 더 설명력이 있는지를 0-100%로 환산해준다.&lt;/p&gt;
&lt;p style=&quot;background-color: #fdfdfc; color: #0a0a0a; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #fdfdfc; color: #0a0a0a; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #fdfdfc; color: #0a0a0a; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;한 가지 주의할 점도 있다.&lt;/p&gt;
&lt;p style=&quot;background-color: #fdfdfc; color: #0a0a0a; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;R&amp;sup2;가 높아도 Error(달러)가 크면 실제로는 별로일 수 있다.&lt;/p&gt;
&lt;p style=&quot;background-color: #fdfdfc; color: #0a0a0a; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;평균 가격이 500달러인 데이터에서 R&amp;sup2; 80%인데 Error가 150달러라면, 설명력은 그럴듯해도 실용적으로는 여전히 크게 틀리는 것이다. &lt;b&gt;그래서 Error(&quot;얼마나 틀리냐&quot;)와 R&amp;sup2;(&quot;얼마나 이해하냐&quot;)를 같이 봐야 한다.&lt;/b&gt;&lt;/p&gt;
&lt;h4 style=&quot;background-color: #fdfdfc; color: #0a0a0a; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;1-2) 색상으로 한눈에 - 평가 하베스트&lt;/h4&gt;
&lt;p style=&quot;background-color: #fdfdfc; color: #0a0a0a; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;강의에서 제공한&lt;span&gt;&amp;nbsp;&lt;/span&gt;Tester(평가 하베스트)는 예측 하나하나를 색으로 칠해 보여준다. 기준은 이렇다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #fdfdfc; color: #0a0a0a; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;초록: 오차 40달러 미만 또는 상대 오차 20% 미만&lt;/li&gt;
&lt;li&gt;주황: 오차 80달러 미만 또는 상대 오차 40% 미만&lt;/li&gt;
&lt;li&gt;빨강: 그 이상&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #fdfdfc; color: #0a0a0a; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;절대 오차와 상대 오차를 OR로 묶은 게 포인트다.&lt;/p&gt;
&lt;p style=&quot;background-color: #fdfdfc; color: #0a0a0a; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;1,000달러짜리 상품을 950달러로 맞히면 절대 오차는 50달러라 초록은 아니지만 상대 오차 5%라 초록이 된다.&lt;/p&gt;
&lt;p style=&quot;background-color: #fdfdfc; color: #0a0a0a; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;비싼 상품과 싼 상품을 같은 잣대로 보지 않으려는 장치다.&lt;/p&gt;
&lt;p style=&quot;background-color: #fdfdfc; color: #0a0a0a; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #fdfdfc; color: #0a0a0a; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그리고 결과를 산점도로 그린다. x축은 실제 가격, y축은 예측 가격이고, 파란 대각선(y=x)에 점이 가까울수록 정확한 예측이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1664&quot; data-origin-height=&quot;1338&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HrwC2/dJMcageHNgf/etAAG8MakkkMImKcBUCr60/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HrwC2/dJMcageHNgf/etAAG8MakkkMImKcBUCr60/img.png&quot; data-alt=&quot;점들이 파란 대각선에 가까울수록 예측이 정확(위 그래프는 random pricer 로 돌린 결과)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HrwC2/dJMcageHNgf/etAAG8MakkkMImKcBUCr60/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHrwC2%2FdJMcageHNgf%2FetAAG8MakkkMImKcBUCr60%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;1664&quot; height=&quot;1338&quot; data-origin-width=&quot;1664&quot; data-origin-height=&quot;1338&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;점들이 파란 대각선에 가까울수록 예측이 정확(위 그래프는 random pricer 로 돌린 결과)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #fdfdfc; color: #0a0a0a; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;background-color: #fdfdfc; color: #0a0a0a; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;느낀 점&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #fdfdfc; color: #0a0a0a; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;각 모델을 돌리기전 &quot;성능 비교를 위한 기준&quot;부터 코드로 고정해두니, 이후 모든 모델이 같은 무대 위에서 비교됐다. 에이전트 개발, ai 기능 개발과저에서도 결국 뭐 하나를 바꿨을 때 이런 정량 성능이 어떻게 변했냐를 근거로 변경을 가져가는게 정말 중요하다고 생각한다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;지표 하나만 보면 속는다는 게 인상적이었다. R&amp;sup2;(설명력)와 Error(실제 오차 금액)를 함께 봐야 한다는 건, 데이터 특성에 따라 한가지 지표가 아니라 여러 지표를 보조해서 봐야할 수 있다는 점을 느끼게 해줬다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 베이스라인부터 전통 머신러닝까지&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;본격적인 모델 돌리기는 전통 머신러닝부터 시작한다. (대학교 때 처음 머신러닝 공부할때 돌렸던 모델들이 많이 나와 반가웠다 ㅎ)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강의에서 짚은 머신러닝의 핵심 개념이 하나 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;목표는 학습 데이터를 잘 외우는 게 아니라 &lt;b&gt;일반화(generalize)&lt;/b&gt;, 곧 처음 보는 데이터에서도 잘 맞히는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 반대편에 있는 게 &lt;b&gt;과적합(overfitting)&lt;/b&gt;으로, 학습 데이터에만 딱 맞춰져 새 데이터에서는 엉망이 되는 상태다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 일반화-과적합 구도는 뒤에 나올 딥러닝과 LLM 모델을 돌릴때도 똑같이 적용된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2-1) 가장 게으른 출발점 - Random과 Constant&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제일 먼저 만드는 건 &quot;이것보다 못하면 안 되는&quot; 바닥 기준(베이스라인)이다.&lt;/p&gt;
&lt;pre class=&quot;ruby&quot;&gt;&lt;code&gt;def random_pricer(item):
    return random.randrange(1, 1000)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1달러부터 999달러 사이를 그냥 무작위로 던지는 모델이다. 당연히 평균 오차가 크다(약 382달러).&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-06-06 오후 3.44.28.png&quot; data-origin-width=&quot;1664&quot; data-origin-height=&quot;1324&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/emkojr/dJMcaiczlBP/VMBlwhGvOhuKRKMKGGFbAk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/emkojr/dJMcaiczlBP/VMBlwhGvOhuKRKMKGGFbAk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/emkojr/dJMcaiczlBP/VMBlwhGvOhuKRKMKGGFbAk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Femkojr%2FdJMcaiczlBP%2FVMBlwhGvOhuKRKMKGGFbAk%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;1664&quot; height=&quot;1324&quot; data-filename=&quot;스크린샷 2026-06-06 오후 3.44.28.png&quot; data-origin-width=&quot;1664&quot; data-origin-height=&quot;1324&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조금 나은 게 Constant pricer로, 모든 상품에 학습 데이터의 평균 가격 하나만 답한다.&lt;/p&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;training_prices = [item.price for item in train]
training_average = sum(training_prices) / len(training_prices)  # 140.57

def constant_pricer(item):
    return training_average&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;학습 데이터 평균은 140.57달러였다. 어떤 상품이 와도 140.57달러라고만 답하는 모델인데,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이게 의외로 무시 못 할 기준선이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞으로 만들 모델이 이 게으른 상수 모델(전체 가격 평균만 외치는 모델) 조차 못 이기면 의미가 없다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-06-06 오후 3.46.22.png&quot; data-origin-width=&quot;1664&quot; data-origin-height=&quot;1324&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2HIhD/dJMcagFOVpQ/pqqTB5irVivEoxiJepg1KK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2HIhD/dJMcagFOVpQ/pqqTB5irVivEoxiJepg1KK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2HIhD/dJMcagFOVpQ/pqqTB5irVivEoxiJepg1KK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2HIhD%2FdJMcagFOVpQ%2FpqqTB5irVivEoxiJepg1KK%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;1664&quot; height=&quot;1324&quot; data-filename=&quot;스크린샷 2026-06-06 오후 3.46.22.png&quot; data-origin-width=&quot;1664&quot; data-origin-height=&quot;1324&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-2) 선형 회귀(Linear Regression) - 단순 피처 3개로는 어림없다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 본격적으로 머신러닝 학습 부터 시켜본다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상품마다 피처(입력 정보) 3개를 뽑았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;무게(weight), 무게 정보가 없는지 여부(weight_unknown), 설명 텍스트 길이(text_length)다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(무게가 0이면 정보 없음으로 간주해 별도 플래그 weight_unknown 을 둠)&lt;/p&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;np.random.seed(42)

# Separate features and target
feature_columns = ['weight', 'weight_unknown', 'text_length']

X_train = train_df[feature_columns]
y_train = train_df['price']
X_test = test_df[feature_columns]
y_test = test_df['price']

# Train a Linear Regression
model = LinearRegression()
model.fit(X_train, y_train)

for feature, coef in zip(feature_columns, model.coef_):
    print(f&quot;{feature}: {coef}&quot;)
print(f&quot;Intercept: {model.intercept_}&quot;)

# Predict the test set and evaluate
y_pred = model.predict(X_test)
mse = mean_squared_error(y_test, y_pred)
r2 = r2_score(y_test, y_pred)

print(f&quot;Mean Squared Error: {mse}&quot;)
print(f&quot;R-squared Score: {r2}&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;선형 회귀는 가격 = (weight &amp;times; a) + (weight_unknown &amp;times; b) + (text_length &amp;times; c) + 절편 형태의 수식에서 a, b, c를 데이터로부터 찾아낸다. 결과는 이랬다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출력:&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;weight: 0.449          # 무게 1단위 오르면 +$0.45
weight_unknown: -6.63  # 무게 정보 없으면 -$6.63
text_length: 0.247     # 설명 1글자 늘면 +$0.25
Intercept: 51.11
Mean Squared Error: 25615.84
R-squared Score: -0.0595
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;R&amp;sup2;가 음수다. 앞에서 말한 그 &quot;평균만 답하는 것(컨스턴트 프라이서)보다도 못한 모델&quot;이라는 뜻이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이유는 단순하다. 텍스트의 길이는 알아도 내용을 모른다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;TV&quot;인지 &quot;볼펜&quot;인지 구분하지 못하고, 무게가 같아도 금붙이와 플라스틱 장난감을 똑같이 본다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 3개 숫자는 가격과 거의 상관이 없었던 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2-3) NLP를 끼얹다 - 단어를 피처로&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 설명문 텍스트의 내용 자체를 피처로 넣는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CountVectorizer로 상품 설명을 단어 빈도 벡터로 바꾸는, 이른바 Bag of Words 방식이다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;vectorizer = CountVectorizer(max_features=2000, stop_words='english')
X = vectorizer.fit_transform(documents)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;the&quot;, &quot;a&quot; 같은 의미 없는 단어(stop words)는 빼고, 가장 자주 등장하는 단어 2000개만 골라 각 상품을 &quot;이 2000개 단어가 각각 몇 번 나왔나&quot;라는 2000칸짜리 숫자 배열(벡터)로 만든다. 그리고 똑같은 선형 회귀를 돌린다.&lt;/p&gt;
&lt;pre class=&quot;makefile&quot;&gt;&lt;code&gt;regressor = LinearRegression()
regressor.fit(X, prices)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모델 종류는 2-2와 똑같은 선형 회귀인데, 피처를 &quot;숫자 3개&quot;에서 &quot;각 단어가 몇번 나왔는지 2000차원 벡터&quot;로 바꿨을 뿐이다. 그&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;런데 성능이 확 올라간다. &quot;leather&quot;, &quot;luxury&quot;, &quot;USB&quot; 같은 단어가 가격과 훨씬 관련이 깊기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 머신러닝 알고리즘(선형 회귀)이라도 무엇을 먹이느냐가 성능을 가른다는 걸 가장 선명하게 보여준 대목이었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-06-06 오후 3.54.09.png&quot; data-origin-width=&quot;1664&quot; data-origin-height=&quot;1324&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bFvkNh/dJMcacDpIYv/xHYsUfavzWVkpgDWq4egsk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bFvkNh/dJMcacDpIYv/xHYsUfavzWVkpgDWq4egsk/img.png&quot; data-alt=&quot;이미지 - NLP + 선형 회귀 결과. 숫자 3개짜리 선형 회귀보다 눈에 띄게 나아졌다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bFvkNh/dJMcacDpIYv/xHYsUfavzWVkpgDWq4egsk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbFvkNh%2FdJMcacDpIYv%2FxHYsUfavzWVkpgDWq4egsk%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;1664&quot; height=&quot;1324&quot; data-filename=&quot;스크린샷 2026-06-06 오후 3.54.09.png&quot; data-origin-width=&quot;1664&quot; data-origin-height=&quot;1324&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;이미지 - NLP + 선형 회귀 결과. 숫자 3개짜리 선형 회귀보다 눈에 띄게 나아졌다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&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;h4 data-ke-size=&quot;size20&quot;&gt;2-4) 앙상블 - Random Forest와 XGBoost&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막은 결정 트리 여러 개를 묶는 앙상블 두 가지다. 둘 다 트리를 쓰지만 만드는 방식이 정반대다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Random Forest는 결정 트리 100개를 &lt;b&gt;동시에&lt;/b&gt; 만들고, 각 트리가 데이터와 단어를 무작위로 다르게 골라 학습한 뒤 100개 예측의 &lt;b&gt;평균&lt;/b&gt;을 낸다. 여럿이 따로 공부하고 투표하는 셈이라, 트리 하나일 때의 과적합을 줄여준다.&lt;/p&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;rf_model = RandomForestRegressor(n_estimators=100, random_state=42, n_jobs=4)
rf_model.fit(X[:15000], prices[:15000])   # 속도 때문에 1.5만개만

def random_forest(item):
    x = vectorizer.transform([item.summary])
    return max(0, rf_model.predict(x)[0])&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-06-06 오후 3.56.45.png&quot; data-origin-width=&quot;1664&quot; data-origin-height=&quot;1324&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zIgM3/dJMcab5w0DB/3eQUdquCbd9W4mmmg4AAsK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zIgM3/dJMcab5w0DB/3eQUdquCbd9W4mmmg4AAsK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zIgM3/dJMcab5w0DB/3eQUdquCbd9W4mmmg4AAsK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzIgM3%2FdJMcab5w0DB%2F3eQUdquCbd9W4mmmg4AAsK%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;1664&quot; height=&quot;1324&quot; data-filename=&quot;스크린샷 2026-06-06 오후 3.56.45.png&quot; data-origin-width=&quot;1664&quot; data-origin-height=&quot;1324&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;XGBoost는 트리를 &lt;b&gt;순서대로&lt;/b&gt; 만든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새 트리가 앞선 트리들이 틀린 오차를 집중적으로 보정하는 부스팅(boosting) 방식이고, 이 보정을 경사 하강법(gradient descent)으로 한 걸음씩 내려간다. (랜덤 포레스트 보다 빠르고 일반화도 잘됨)&lt;/p&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;xgb_model = xgb.XGBRegressor(n_estimators=1000, learning_rate=0.1, n_jobs=4, random_state=42)
xgb_model.fit(X, prices)   # 전체 80만개 사용

def xg_boost(item):
    x = vectorizer.transform([item.summary])
    return max(0, xgb_model.predict(x)[0])&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;learning_rate=0.1은 한 트리가 오차를 한 번에 10%씩만 보정한다는 뜻이고, 그래서 트리가 1000개나 필요하다. 조금씩 여러 번 고쳐 수렴하는 구조다. 결과적으로 XGBoost가 전통 머신러닝 중에서는 가장 좋은 성능을 냈다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-06-06 오후 3.59.28.png&quot; data-origin-width=&quot;1664&quot; data-origin-height=&quot;1324&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/beM3OJ/dJMcaaS6iqR/8gjmdkiJttqek3W9CRsmS1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/beM3OJ/dJMcaaS6iqR/8gjmdkiJttqek3W9CRsmS1/img.png&quot; data-alt=&quot;(이미지 - XGBoost 결과. 전통 ML 중 최고 성적)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/beM3OJ/dJMcaaS6iqR/8gjmdkiJttqek3W9CRsmS1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbeM3OJ%2FdJMcaaS6iqR%2F8gjmdkiJttqek3W9CRsmS1%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;1664&quot; height=&quot;1324&quot; data-filename=&quot;스크린샷 2026-06-06 오후 3.59.28.png&quot; data-origin-width=&quot;1664&quot; data-origin-height=&quot;1324&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;(이미지 - XGBoost 결과. 전통 ML 중 최고 성적)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면 전통 ML 안에서도 선형 회귀(숫자 3개) &amp;rarr; 선형 회귀(NLP) &amp;rarr; Random Forest &amp;rarr; XGBoost 순으로 또렷하게 좋아졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 &quot;어떤 알고리즘이냐&quot;만큼이나 &quot;어떤 피처를 주느냐&quot;가 성능을 좌우한다는 점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강사는 전통 ML이 옛날 기술이 아니라 특징이 명확한 문제에선 지금도 산업에서 활발히 쓰인다는 메세지를 했다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 광고 쪽에서도 여러 추천 모델을 비롯한 다양한 모델들을 전통 ML로 아직까지 많이 하고 있으니까&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특징이 명확한 문제에선 ML 도 여전히 좋은 선택이라고 생각한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p 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;가장 게으른 Constant 모델을 기준선으로 깔고 시작하는 구조가 좋았다. 새 기능을 붙일 때도 &quot;아무것도 안 한 상태&quot; 혹은 &quot;지금 운영 중인 단순 로직&quot; 대비 얼마나 나아졌는지를 항상 기준선으로 두고 비교해야 과장 없이 가치를 판단할 수 있다고 느꼈다.&lt;/li&gt;
&lt;li&gt;같은 선형 회귀 알고리즘인데 피처만 바꿔 성능이 뒤집힌 게 인상적이었다. 모델을 더 똑똑한 걸로 갈아끼우기 전에, 지금 모델에 &quot;무엇을 입력으로 주고 있나&quot;를 먼저 의심하는 게 더 빠른 길일 수 있다는 교훈으로 읽혔다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 딥러닝 - 신경망으로 한 단계 더&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전통 ML 다음은 딥러닝을 돌려봤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;신경망을 어떻게 학습시키는지 먼저 짚고, 직접 PyTorch로 작은 신경망을 하나 만든다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3-1) 신경망 학습의 4단계&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파라미터를 가진 모델이 학습한다는 건, 결국 이 네 단계를 데이터를 보며 계속 반복하는 일이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;forward pass: 입력을 넣어 예측값(ŷ)을 낸다.&lt;/li&gt;
&lt;li&gt;loss 계산: 예측값과 정답(y)이 얼마나 다른지 손실을 구한다.&lt;/li&gt;
&lt;li&gt;backward pass: 신경망을 거슬러 올라가며 &quot;파라미터를 어느 쪽으로 조금씩 바꾸면 손실이 줄어드는지&quot;(기울기, gradient)를 계산한다.&lt;/li&gt;
&lt;li&gt;optimization: 그 방향으로 파라미터를 아주 조금씩 움직인다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 두 가지 하이퍼파라미터가 나온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;epoch은 전체 데이터를 몇 번 훑을지(한 번만 보는 게 아니라 여러 번 반복 학습),&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;learning rate는 4번 단계에서 한 걸음을 얼마나 크게 내디딜지다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 forward &amp;rarr; loss &amp;rarr; backward &amp;rarr; optimize 루프는 작은 신경망이든 거대한 LLM이든 본질적으로 똑같다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3-2) 8층 바닐라 신경망&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상품 설명을 신경망에 넣으려면 먼저 숫자로 바꿔야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서는 HashingVectorizer로 설명문을 5000차원 벡터로 만든다(각 단어가 등장했는지 여부로 0/1로 표시).&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;vectorizer = HashingVectorizer(n_features=5000, binary=True, stop_words='english')
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 위에 8개 층을 쌓은 단순한(바닐라) 신경망을 올린다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5000차원 입력을 128 &amp;rarr; 64로 좁히고, 점점 좁아지는 8개 층(layer)를 거친 뒤 마지막에 1개(가격)로 뽑아낸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;층 사이에는 ReLU(비선형성을 주는 활성화 함수)를 끼운다.&lt;/p&gt;
&lt;pre class=&quot;gml&quot;&gt;&lt;code&gt;class NeuralNetwork(nn.Module):
    def __init__(self, input_size):
        super().__init__()
        self.layer1 = nn.Linear(input_size, 128)
        self.layer2 = nn.Linear(128, 64)
        self.layer3 = nn.Linear(64, 64)
        self.layer4 = nn.Linear(64, 64)
        self.layer5 = nn.Linear(64, 64)
        self.layer6 = nn.Linear(64, 64)
        self.layer7 = nn.Linear(64, 64)
        self.layer8 = nn.Linear(64, 1)
        self.relu = nn.ReLU()

    def forward(self, x):
        x = self.relu(self.layer1(x))
        x = self.relu(self.layer2(x))
        x = self.relu(self.layer3(x))
        x = self.relu(self.layer4(x))
        x = self.relu(self.layer5(x))
        x = self.relu(self.layer6(x))
        x = self.relu(self.layer7(x))
        return self.layer8(x)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;손실 함수는 MSE, 옵티마이저는 Adam(learning rate 0.001)을 쓰고, 전체 데이터를 2 epoch 돌렸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 신경망의 학습 가능한 파라미터는 669,249개다. (프론티어 LLM 모델은 수십억개 이상)&lt;/p&gt;
&lt;pre id=&quot;code_1780731419044&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;loss_function = nn.MSELoss()                    # 오차 = (예측-정답)&amp;sup2;
optimizer = optim.Adam(model.parameters(), lr=0.001)
EPOCHS = 2

for epoch in range(EPOCHS):
    model.train()
    for batch_X, batch_y in tqdm(train_loader):
        optimizer.zero_grad()
        outputs = model(batch_X)          # ① 순전파(예측)
        loss = loss_function(outputs, batch_y)  # ② 손실 계산
        loss.backward()                   # ③ 역전파(기울기)
        optimizer.step()                  # ④ 가중치 업데이트
    
    model.eval()
    with torch.no_grad():
        val_outputs = model(X_val)
        val_loss = loss_function(val_outputs, y_val)

    print(f'Epoch [{epoch+1}/{EPOCHS}], Train Loss: {loss.item():.3f}, Val Loss: {val_loss.item():.3f}')&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-06-06 오후 4.38.04.png&quot; data-origin-width=&quot;1664&quot; data-origin-height=&quot;274&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mxYzq/dJMcageHOGc/v6kUBaORQ3T7utoWaOMs4k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mxYzq/dJMcageHOGc/v6kUBaORQ3T7utoWaOMs4k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mxYzq/dJMcageHOGc/v6kUBaORQ3T7utoWaOMs4k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmxYzq%2FdJMcageHOGc%2Fv6kUBaORQ3T7utoWaOMs4k%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;1664&quot; height=&quot;274&quot; data-filename=&quot;스크린샷 2026-06-06 오후 4.38.04.png&quot; data-origin-width=&quot;1664&quot; data-origin-height=&quot;274&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;신경망 학습의 핵심 4단계(foward pass -&amp;gt; loss 계산 -&amp;gt; backward pass -&amp;gt; optimization)&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 데이터 2번(epoch =2) 훑음. Train loss 줄어드는 거 볼 수 있음&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;model = NeuralNetwork(input_size=5000)
criterion = nn.MSELoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과는 평균 오차 63.33달러. 전통 ML 최고 성적이었던 XGBoost(68.23)보다 조금 더 나았다. 단어 등장 여부만 보는 단순한 신경망인데도 앙상블을 넘어선 것이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1478&quot; data-origin-height=&quot;1174&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vnp7y/dJMcafGTGME/Mw5dHl2hkhQupq7az1clH0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vnp7y/dJMcafGTGME/Mw5dHl2hkhQupq7az1clH0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vnp7y/dJMcafGTGME/Mw5dHl2hkhQupq7az1clH0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fvnp7y%2FdJMcafGTGME%2FMw5dHl2hkhQupq7az1clH0%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;1478&quot; height=&quot;1174&quot; data-origin-width=&quot;1478&quot; data-origin-height=&quot;1174&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;느낀 점&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&quot;거대한 LLM도 결국 이 forward &amp;rarr; loss &amp;rarr; backward &amp;rarr; optimize 루프의 반복&quot;이라는 설명이 딥러닝을 막연한 블랙박스에서 한 덩어리 메커니즘으로 보이게 해줬다. 모델 학습에서의 기본 골격을 볼 수 있어서 좋았다&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 학습 없이 이긴다 - 제로샷 프론티어 모델&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서부터가 이 글에서 가장 인상적이었던 구간이다. 전통 ML과 신경망은 모두 아마존 상품 데이터로 &quot;학습&quot;을 시켰다. 그런데 프론티어 모델(GPT, Claude, Gemini 등)은 데이터로 따로 단 한 줄도 학습시키지 않고, 그냥 상품 설명을 프롬프트로 던지기만 한다(제로샷).&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그것만으로 어느정도 성능까지 가는지 보는 게 핵심이다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4-1) 먼저 사람 기준선부터&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비교를 위해 사람의 실력도 재둔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트셋 100개를 CSV로 내보내, 강사(Ed)가 직접 가격을 눈대중으로 적은 뒤 그 결과를 다시 읽어 평가했다.&lt;/p&gt;
&lt;pre class=&quot;ruby&quot;&gt;&lt;code&gt;def human_pricer(item):
    idx = test.index(item)
    return human_predictions[idx]
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사람의 평균 오차는 87.62달러였다. 앞서 본 XGBoost(68.23)나 신경망(63.33)이 이미 사람보다 나은 셈이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-06-06 오후 4.43.02.png&quot; data-origin-width=&quot;1664&quot; data-origin-height=&quot;1334&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/OBFYL/dJMcada8ptV/KV5hPXUheUVjk4OZnaG6X1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/OBFYL/dJMcada8ptV/KV5hPXUheUVjk4OZnaG6X1/img.png&quot; data-alt=&quot;(이미지 - 사람(강사) 직접 예측 결과)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/OBFYL/dJMcada8ptV/KV5hPXUheUVjk4OZnaG6X1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOBFYL%2FdJMcada8ptV%2FKV5hPXUheUVjk4OZnaG6X1%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;1664&quot; height=&quot;1334&quot; data-filename=&quot;스크린샷 2026-06-06 오후 4.43.02.png&quot; data-origin-width=&quot;1664&quot; data-origin-height=&quot;1334&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;(이미지 - 사람(강사) 직접 예측 결과)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4-2) 프롬프트 한 줄로 묻기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프론티어 모델에 보내는 건 정말 단순하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시스템 메시지도 없이, 사용자 메시지 하나로 &quot;이 상품 가격을 추정해라, 설명 없이 가격만 답해라&quot;라고만 한다.&lt;/p&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;def messages_for(item):
    message = f&quot;Estimate the price of this product. Respond with the price, no explanation\n\n{item.summary}&quot;
    return [{&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: message}]
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;호출은 litellm의 completion 하나로 여러 회사 모델을 같은 방식으로 돌린다. 예를 들어 GPT-5.1은 이렇게 부른다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;def gpt_5__1(item):
    response = completion(model=&quot;gpt-5.1&quot;, messages=messages_for(item),
                          reasoning_effort='high', seed=42)
    return response.choices[0].message.content
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 패턴으로 GPT-4.1-nano, Claude Opus 4.5, Gemini 3 Pro, 그리고 가벼운 Gemini 2.5 Flash Lite까지 돌려봤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/V2NXR/dJMcaf7TlBn/vAHMl60tfG8huOYDF82oV1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/V2NXR/dJMcaf7TlBn/vAHMl60tfG8huOYDF82oV1/img.png&quot; data-origin-width=&quot;1478&quot; data-origin-height=&quot;1174&quot; data-is-animation=&quot;false&quot; style=&quot;width: 32.5581%; margin-right: 10px;&quot; data-widthpercent=&quot;33.33&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/V2NXR/dJMcaf7TlBn/vAHMl60tfG8huOYDF82oV1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FV2NXR%2FdJMcaf7TlBn%2FvAHMl60tfG8huOYDF82oV1%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;1478&quot; height=&quot;1174&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mHp2E/dJMcaf7TlBA/00gAN27hVKpxeN6pKIkt2k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mHp2E/dJMcaf7TlBA/00gAN27hVKpxeN6pKIkt2k/img.png&quot; data-origin-width=&quot;1478&quot; data-origin-height=&quot;1174&quot; data-is-animation=&quot;false&quot; style=&quot;width: 32.5581%; margin-right: 10px;&quot; data-widthpercent=&quot;33.33&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mHp2E/dJMcaf7TlBA/00gAN27hVKpxeN6pKIkt2k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmHp2E%2FdJMcaf7TlBA%2F00gAN27hVKpxeN6pKIkt2k%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;1478&quot; height=&quot;1174&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/diE4yw/dJMcadCaT8c/dboipUKatQblOfFFBV03k1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/diE4yw/dJMcadCaT8c/dboipUKatQblOfFFBV03k1/img.png&quot; data-origin-width=&quot;1478&quot; data-origin-height=&quot;1174&quot; data-is-animation=&quot;false&quot; style=&quot;width: 32.5581%;&quot; data-widthpercent=&quot;33.34&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/diE4yw/dJMcadCaT8c/dboipUKatQblOfFFBV03k1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdiE4yw%2FdJMcadCaT8c%2FdboipUKatQblOfFFBV03k1%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;1478&quot; height=&quot;1174&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/M51d2/dJMcaaldUau/Iv6WMOdOSBpZVwdRAqbjpk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/M51d2/dJMcaaldUau/Iv6WMOdOSBpZVwdRAqbjpk/img.png&quot; data-origin-width=&quot;1478&quot; data-origin-height=&quot;1174&quot; data-is-animation=&quot;false&quot; style=&quot;width: 49.6467%; margin-right: 10px;&quot; data-widthpercent=&quot;50.23&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/M51d2/dJMcaaldUau/Iv6WMOdOSBpZVwdRAqbjpk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FM51d2%2FdJMcaaldUau%2FIv6WMOdOSBpZVwdRAqbjpk%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;1478&quot; height=&quot;1174&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/m4o1J/dJMcac4p2ZA/lRyWeoIiKMGjCgbBdxpZpK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/m4o1J/dJMcac4p2ZA/lRyWeoIiKMGjCgbBdxpZpK/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;1664&quot; data-origin-height=&quot;1334&quot; data-filename=&quot;스크린샷 2026-06-06 오후 4.54.08.png&quot; style=&quot;width: 49.1905%;&quot; data-widthpercent=&quot;49.77&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/m4o1J/dJMcac4p2ZA/lRyWeoIiKMGjCgbBdxpZpK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fm4o1J%2FdJMcac4p2ZA%2FlRyWeoIiKMGjCgbBdxpZpK%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;1664&quot; height=&quot;1334&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&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;h4 data-ke-size=&quot;size20&quot;&gt;4-3) 결과&amp;nbsp;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;평균 오차(달러, 낮을수록 좋음)는 이랬다.&lt;/p&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;GPT-5.1            45.57   &amp;lt;- 1위
Claude Opus 4.5    45.73
Gemini 3 Pro       51.21
Gemini 2.5 Flash.  53.66
GPT-4.1-nano       85.71
-------------------------- (참고: 우리가 학습시킨 모델들)
8층 신경망          63.33
XGBoost            68.23
사람(Ed)           87.62&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리 데이터로 한참을 학습시킨 신경망(63.97)과 XGBoost(68.23)를, 학습이라곤 전혀 안 한 프론티어 모델들이 제로샷으로 대부분 앞섰다. (작은 모델인 gpt 4.1 nano 정도를 제외하면)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LLM 모델이 세상에 대해 이미 알고 있는 방대한 지식(사전 학습에 투여된 세상의 지식)이,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 작은 가격 예측 문제에서도 그대로 힘을 발휘한 것이다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;느낀 점&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&quot;학습을 안 시킨 LLM 모델이 자체 데이터로 학습시킨 ML/DL 모델을 이긴다&quot;는 결과가 꽤나 충격적이었다. 그래도 전통 ML/DL의 영역이라고 느꼈던 가격 예측 문제 조차 이 결과가 나왔다는 게 신기했다(물론 ML/DL 성능 낼라면 더 제대로된 피처엔지니어링 필요한건 맞겠지만)&lt;/li&gt;
&lt;li&gt;기능을 만들 때 처음부터 자체 모델을 학습시키려 들기보다 잘 만들어진 프론티어 모델에 프롬프트로 일을 시켜보는 게 합리적 출발점이라는 걸 수치로 보여줬다. 많은 문제에서 제로샷이 이미 강력한 베이스라인이다.&lt;/li&gt;
&lt;li&gt;같은 messages_for 한 함수로 회사가 다른 모델 대여섯 개를 똑같이 호출해 비교한 구조가 기획 관점에서 와닿았다. 모델을 갈아끼울 수 있게 추상화해두면, 새 모델이 나올 때마다 같은 평가셋으로 즉시 재보고 교체 결정을 내릴 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 프론티어 모델을 직접 파인튜닝해보다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막 단계는 프론티어 모델을 내 데이터로 직접 파인튜닝하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;base 모델로는 가장 작은 축인 GPT-4.1-nano를 골랐다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;5-1) 파인튜닝이란 - 그리고 LoRA&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프론티어 모델은 우리가 가중치 전체를 직접 만질 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대신 OpenAI에 요청하면 내부적으로 LoRA(저랭크 어댑터) 방식으로 소수의 파라미터 사본만 학습시켜준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모델 전체를 다시 만드는 게 아니라, 기존 모델을 원하는 방향으로 살짝 &quot;넛지&quot;하는 셈이다. 그 결과로 나만의 GPT 변형 모델이 하나 생긴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;5-2) 파인튜닝의 세 갈래 - SFT, DPO, RFT&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강의에서 Open AI 플랫폼에서 지원하는 파인튜닝 종류를 정리해줬는데, 이게 뒤의 결과를 이해하는 열쇠가 된다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SFT(지도 파인튜닝): 입력과 정답 출력 예시를 그대로 보여주는 방식. 분류, 특정 포맷 생성, 지시 따르기 교정 등에 쓴다. 이번에 쓴 방식이다.&lt;/li&gt;
&lt;li&gt;DPO: 좋은 예와 나쁜 예를 함께 줘서 &quot;사람이 어느 쪽을 선호하는지&quot;를 학습시킨다. 좋아요/싫어요 같은 주관적 선호를 반영할 때 쓴다.&lt;/li&gt;
&lt;li&gt;RFT: 정답이 딱 정해져 있지 않고, 응답에 점수를 매기는 채점 프로그램(이 채점기가 LLM일 수도 있다)으로 학습한다. 리즈닝 모델에서만 쓸 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 강사가 짚은 중요한 전제가 하나 있다. 프론티어 모델 파인튜닝의 진짜 용도는 스타일이나 톤 맞추기, 출력 신뢰성 높이기, 복잡한 프롬프트에서의 실패 수정, 엣지케이스 처리, 새로운 스킬 부여라는 점이다. 이 복선을 6번에서 회수한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;5-3) 3단계 파이프라인&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파인튜닝 과정은 3 단계다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;JSONL 학습셋을 만들어 OpenAI에 업로드&lt;/li&gt;
&lt;li&gt;학습 실행 - train/validation loss가 줄어드는지 모니터링&lt;/li&gt;
&lt;li&gt;결과 평가 후 튜닝하고 반복&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 학습 데이터를 JSONL로 만든다. 제로샷 때와 달리 assistant 메시지에 정답 가격을 넣어준다. 곧 &quot;이 설명에는 이 가격으로 답해라&quot;라는 예시를 보여주는 것이다.&lt;/p&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;def messages_for(item):
    message = f&quot;Estimate the price of this product. Respond with the price, no explanation\n\n{item.summary}&quot;
    return [
        {&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: message},
        {&quot;role&quot;: &quot;assistant&quot;, &quot;content&quot;: f&quot;${item.price:.2f}&quot;}
    ]
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OpenAI가 권장하는 대로 적은 수의 예시(학습 100개, 검증 50개)만 골라 파일로 쓴다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;fine_tune_train = train[:100]
fine_tune_validation = val[:50]
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 파일을 업로드하고 파인튜닝 작업을 건다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;with open(&quot;jsonl/fine_tune_train.jsonl&quot;, &quot;rb&quot;) as f:
    train_file = openai.files.create(file=f, purpose=&quot;fine-tune&quot;)

openai.fine_tuning.jobs.create(
    training_file=train_file.id,
    validation_file=validation_file.id,
    model=&quot;gpt-4.1-nano-2025-04-14&quot;,
    seed=42,
    hyperparameters={&quot;n_epochs&quot;: 1, &quot;batch_size&quot;: 1},
    suffix=&quot;pricer&quot;
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작업을 걸면 OpenAI 대시보드에서 업로드한 JSONL, loss 곡선, 시간별 loss 변화를 실시간으로 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OpenAI 작업 큐에 들어가 한참 대기하는 시간이 좀 길긴 했다.(오히려 실제 학습이 돌아가는 시간보다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;1036&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bcGqPn/dJMb997H578/4VXoS7alJTtldsJOMqRwXK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bcGqPn/dJMb997H578/4VXoS7alJTtldsJOMqRwXK/img.png&quot; data-alt=&quot;(이미지 - 업로드한 JSONL 학습 데이터)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bcGqPn/dJMb997H578/4VXoS7alJTtldsJOMqRwXK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbcGqPn%2FdJMb997H578%2F4VXoS7alJTtldsJOMqRwXK%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;2048&quot; height=&quot;1036&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;1036&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;(이미지 - 업로드한 JSONL 학습 데이터)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FAQpE/dJMcaaevQoR/2D2xvtzmdA3pk1KeFLRKC1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FAQpE/dJMcaaevQoR/2D2xvtzmdA3pk1KeFLRKC1/img.png&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;1036&quot; data-is-animation=&quot;false&quot; style=&quot;width: 32.5581%; margin-right: 10px;&quot; data-widthpercent=&quot;33.33&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FAQpE/dJMcaaevQoR/2D2xvtzmdA3pk1KeFLRKC1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFAQpE%2FdJMcaaevQoR%2F2D2xvtzmdA3pk1KeFLRKC1%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;2048&quot; height=&quot;1036&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mwj9z/dJMcabxFy7f/7AGHwszMeZF7OvedM2uorK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mwj9z/dJMcabxFy7f/7AGHwszMeZF7OvedM2uorK/img.png&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;1036&quot; data-is-animation=&quot;false&quot; style=&quot;width: 32.5581%; margin-right: 10px;&quot; data-widthpercent=&quot;33.33&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mwj9z/dJMcabxFy7f/7AGHwszMeZF7OvedM2uorK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fmwj9z%2FdJMcabxFy7f%2F7AGHwszMeZF7OvedM2uorK%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;2048&quot; height=&quot;1036&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bkuveh/dJMcah5NddX/4EntUvEy3TLxSbpmViaEDk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bkuveh/dJMcah5NddX/4EntUvEy3TLxSbpmViaEDk/img.png&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;1036&quot; data-is-animation=&quot;false&quot; style=&quot;width: 32.5581%;&quot; data-widthpercent=&quot;33.34&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bkuveh/dJMcah5NddX/4EntUvEy3TLxSbpmViaEDk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbkuveh%2FdJMcah5NddX%2F4EntUvEy3TLxSbpmViaEDk%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;2048&quot; height=&quot;1036&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;파인튜닝 과정에서의 loss 변화&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;1036&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kDD5m/dJMcabqWRhA/kCS5awBEqtPTj5KnKe52iK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kDD5m/dJMcabqWRhA/kCS5awBEqtPTj5KnKe52iK/img.png&quot; data-alt=&quot;파인튜닝과정에서의 로그&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kDD5m/dJMcabqWRhA/kCS5awBEqtPTj5KnKe52iK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkDD5m%2FdJMcabqWRhA%2FkCS5awBEqtPTj5KnKe52iK%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;2048&quot; height=&quot;1036&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;1036&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;파인튜닝과정에서의 로그&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;학습이 끝나면 ft:gpt-4.1-nano-...:pricer:... 형태의 내 전용 모델 이름이 나오고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이걸로 추론(inference) 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정답을 뺀 프롬프트만 넣고, 가격만 짧게 받으면 되니 토큰도 7개로 제한한다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;def gpt_4__1_nano_fine_tuned(item):
    response = openai.chat.completions.create(
        model=fine_tuned_model_name,
        messages=test_messages_for(item),
        max_tokens=7
    )
    return response.choices[0].message.content
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;느낀 점&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;프론티어 모델을 파인튜닝 시키는 일이 생각보다 평범한 API 호출 몇 줄로 끝난다는 게 인상적이었다. 데이터를 JSONL로 정리하고, 올리고, 작업을 걸고, loss를 지켜보는 흐름은 그 자체로 한 번쯤 손에 익혀둘 가치가 있었다.&lt;/li&gt;
&lt;li&gt;오히려 오픈 AI 작업 큐에 들어가는 대기시간이 꽤 걸렸다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;파인튜닝을 시작하기 전에 &quot;이건 어떤 종류(SFT/DPO/RFT)이고, 애초에 무엇을 바꾸기 위해 파인튜닝 하고 있나&quot;를 먼저 구분하는 틀이 중요하다고 느꼈다&lt;/li&gt;
&lt;li&gt;실제 파인튜닝 과정에서 Loss 가 줄어드는 걸 그래프로 시각화 해서 볼 수 있던 것도 좋았던 거 같다&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. 의외의 결과, 그리고 그게 알려주는 것&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내심 기대가 있었다. 데이터로 직접 학습까지 시켰으니, 적어도 같은 gpt 4.1 nano의 제로샷 프롬프트로 테스트 했을 때 보다는&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과가 더 나아야 하지 않을까. 결과는 이랬다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출력:&lt;/p&gt;
&lt;pre class=&quot;subunit&quot;&gt;&lt;code&gt;GPT 4.1 Nano Fine Tuned results
Error: $90.78   MSE: 24,383   r&amp;sup2;: -10.9%
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1786&quot; data-origin-height=&quot;1276&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b1GAKy/dJMcabxFzaq/cXpK1MPRfQiFFTtPD4St01/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b1GAKy/dJMcabxFzaq/cXpK1MPRfQiFFTtPD4St01/img.png&quot; data-alt=&quot;(이미지 - 파인튜닝한 GPT-4.1-nano 예측 산점도. 점들이 대각선에서 꽤 흩어져 있다)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b1GAKy/dJMcabxFzaq/cXpK1MPRfQiFFTtPD4St01/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb1GAKy%2FdJMcabxFzaq%2FcXpK1MPRfQiFFTtPD4St01%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;1786&quot; height=&quot;1276&quot; data-origin-width=&quot;1786&quot; data-origin-height=&quot;1276&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;(이미지 - 파인튜닝한 GPT-4.1-nano 예측 산점도. 점들이 대각선에서 꽤 흩어져 있다)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&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;p data-ke-size=&quot;size16&quot;&gt;파인튜닝한 모델은 평균 오차 90.78달러로, 같은 모델의 제로샷(85.71달러)보다 오히려 조금 더 나빠졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기대와는 반대 방향이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 5-2에서 강사가 깔아둔 복선을 떠올리면, 이건 사실 예상할 수 있는 결과였다.&lt;/p&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프론티어 모델은 이미 세상에 대한 방대한 지식을 사전 학습으로 갖고 있고, 가격 감각도 그 안에 들어 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;거기에 우리가 고른 상품 100개의 가격 예시를 더한다고 해서 그 거대한 지식이 더 좋아질 이유가 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오히려 적은 예시에 과하게 끌려가며 흔들릴 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;곧 결과가 나빠진 건 실패라기보다 프론티어 모델 파인튜닝이라는 도구를 용도와 안 맞게 쓴 것에 가깝다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 이 가격 예측 문제의 진짜 정답 방향은 무엇일까.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;힌트는 강의 후반에 잠깐 등장한 또 다른 신경망에 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 만든 8층짜리 작은 신경망을 크게 키운, 약 2.8억 개 파라미터의 깊은 신경망이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 모델을 우리가 직접 모은 80만 건 데이터로 처음부터 학습시키자, 평균 오차가 제로샷 1위였던 GPT-5.1에 거의 필적하는 수준까지 내려갔다. (46대로 45대의 gpt 5.1, 클로드 오퍼스 4.5 와 거의 동일)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 일은 못 하지만 가격 예측 하나만큼은 프론티어급이 된 것이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1206&quot; data-origin-height=&quot;242&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/stvxQ/dJMcadCaUNI/sDKvoQXnOh2wyZdz1b45vk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/stvxQ/dJMcadCaUNI/sDKvoQXnOh2wyZdz1b45vk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/stvxQ/dJMcadCaUNI/sDKvoQXnOh2wyZdz1b45vk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FstvxQ%2FdJMcadCaUNI%2FsDKvoQXnOh2wyZdz1b45vk%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;1206&quot; height=&quot;242&quot; data-origin-width=&quot;1206&quot; data-origin-height=&quot;242&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1432&quot; data-origin-height=&quot;1116&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/r6IVD/dJMcahxVwVj/s4GSeKvgfGJ1kShMeqvK10/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/r6IVD/dJMcahxVwVj/s4GSeKvgfGJ1kShMeqvK10/img.png&quot; data-alt=&quot;(이미지 - 2.8억 파라미터 깊은 신경망 예측 결과. GPT-5.1과 거의 비슷한 성적)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/r6IVD/dJMcahxVwVj/s4GSeKvgfGJ1kShMeqvK10/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fr6IVD%2FdJMcahxVwVj%2Fs4GSeKvgfGJ1kShMeqvK10%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;1432&quot; height=&quot;1116&quot; data-origin-width=&quot;1432&quot; data-origin-height=&quot;1116&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;(이미지 - 2.8억 파라미터 깊은 신경망 예측 결과. GPT-5.1과 거의 비슷한 성적)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&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;p data-ke-size=&quot;size16&quot;&gt;핵심은 이거다. 이미 사전 지식이 많은 거대한 모델을 작은 예시로 살짝 미는 것보다,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지식이 별로 없는 작은 모델을 우리 데이터로 제대로 학습시키는 쪽이 이 문제에는 맞았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;느낀 점&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&quot;파인튜닝하면 무조건 좋아진다&quot;는 막연한 기대가 깨진 게 오히려 큰 수확이었다. 어떤 기법이든 그게 해결하도록 설계된 문제 유형이 따로 있고, 내 문제가 거기에 해당하는지를 먼저 따져야 한다는 걸 직접 돌려보며 체감했다.&lt;/li&gt;
&lt;li&gt;프론티어 모델 파인튜닝의 용도를 좀 더 파악해볼 수 있던 게 의미가 있었다&lt;/li&gt;
&lt;li&gt;또한 실제 프론티어 모델도 파인튜닝을 할 수 있다는 것 자체가 몰랐던 사실이었는데 이렇게 프론티어 모델 플랫폼 단에서 파인튜닝을 지원해주는 거 자체가 신기했다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;실제 말로만 듣던 파인튜닝을 직접 해본 것 자체가 의미가 있었고, 용도만 잘 맞출 수 있다면 실 서비스 gpt 모델등으로 만드는 과정에서도 파인튜닝을 시도해볼 수 있겠다는 생각을 했다&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7. 마무리 - 전체 성적표와 다음 이야기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Day 3부터 5까지 돌린 모델들을 한 줄로 세우면 이렇다(평균 오차, 달러, 낮을수록 좋음).&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;GPT-5.1                   45.57   제로샷 프론티어
Claude Opus 4.5           45.73   제로샷 프론티어
Gemini 3 Pro              51.21   제로샷 프론티어
Gemini 2.5 Flash Lite     53.66   제로샷 프론티어
8층 신경망                 63.33   딥러닝
XGBoost                   68.23   전통 ML
Random Forest             72.28   전통 ML
NLP + 선형회귀             76.81   전통 ML
GPT-4.1-nano (제로샷)      85.71   제로샷 프론티어
사람(Ed)                  87.62   사람
GPT-4.1-nano (파인튜닝)    90.78   파인튜닝
선형회귀(피처 3개)         101.56   베이스라인
Constant                 106.18   베이스라인
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2065&quot; data-origin-height=&quot;1101&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wAul2/dJMb990ZeuO/aQZogNqj3NjQPpMtdU9i80/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wAul2/dJMb990ZeuO/aQZogNqj3NjQPpMtdU9i80/img.png&quot; data-alt=&quot;전체 모델 성능 막대그래프.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wAul2/dJMb990ZeuO/aQZogNqj3NjQPpMtdU9i80/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwAul2%2FdJMb990ZeuO%2FaQZogNqj3NjQPpMtdU9i80%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;2065&quot; height=&quot;1101&quot; data-origin-width=&quot;2065&quot; data-origin-height=&quot;1101&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;전체 모델 성능 막대그래프.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&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;p data-ke-size=&quot;size16&quot;&gt;정리하면 이번 캡스톤의 결론은 세 가지다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫째, 학습을 전혀 안 한 제로샷 프론티어 모델이 우리가 공들여 데이터 바탕으로 학습시킨 ML/DL 모델들을 대부분 앞섰다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘째, 그렇다고 전통 ML과 딥러닝이 죽은 것도 아니어서(2.8억 신경망 성능 등), 특징이 명확한 문제에서는 여전히 경쟁력이 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;셋째, 프론티어 모델 파인튜닝은 적어도 이 가격 예측 문제에는 맞는 도구가 아니었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 마지막 셋째가 다음 이야기의 출발점이 된다. &quot;거대한 프론티어 모델을 미세조정&quot;하는 게 답이 아니라면,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;6번 끝에서 본 깊은 신경망처럼 &quot;우리 데이터로 모델을 제대로 학습&quot;시키는 길이 남는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 Week 7에서는 바로 그 방향으로, 오픈소스 모델을 QLoRA 기법으로 우리 데이터에 파인튜닝하는 작업을 다룬다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 &quot;파인튜닝&quot;이지만 프론티어 모델에 했던 것과는 의미가 사뭇 다른 시도다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;느낀 점&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;한 문제를 두고 베이스라인부터 전통 ML, 딥러닝, 프론티어, 파인튜닝까지 같은 잣대로 줄 세워본 경험 자체가 값졌다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;가장 좋은 결과가 가장 화려한 기법에서 나오지 않았다는 점이 오래 남는다. 제로샷 한 줄이 최고였고 정교한 파인튜닝은 헛돌았다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;다음 글에서 Week7 오픈소스 모델 파인튜닝으로 찾아오겠습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;(꾸준히 공부하고&lt;span&gt;&amp;nbsp;적을테니&amp;nbsp;&lt;/span&gt;많은 관심 부탁드립니다.)&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;Profile:&lt;/span&gt;&lt;br /&gt;&lt;a style=&quot;color: #0070d1; text-align: start;&quot; href=&quot;http://www.linkedin.com/in/doobeom-kim-%EA%B9%80%EB%91%90%EB%B2%94-2b9649242&quot;&gt;&lt;span&gt;&lt;span style=&quot;color: #0070d1;&quot;&gt;Linkedin&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/p&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;</description>
      <category>AI 공부</category>
      <category>ai</category>
      <category>AI 공부</category>
      <category>GPT</category>
      <category>llm</category>
      <category>sft</category>
      <category>가격 예측</category>
      <category>딥러닝</category>
      <category>모델</category>
      <category>생성형 AI</category>
      <category>파인튜닝</category>
      <author>kdb1248</author>
      <guid isPermaLink="true">https://doobeom-coding.tistory.com/91</guid>
      <comments>https://doobeom-coding.tistory.com/91#entry91comment</comments>
      <pubDate>Sat, 6 Jun 2026 17:56:50 +0900</pubDate>
    </item>
    <item>
      <title>13. RAG를 넘어 파인튜닝으로 - 그 첫단계, 데이터 큐레이션 및 전처리</title>
      <link>https://doobeom-coding.tistory.com/90</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;목차&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;들어가며&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal; background-color: #fdfdfc; color: #0a0a0a; text-align: start;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;어떤 프로젝트인가&lt;/li&gt;
&lt;li&gt;Day 1 - 데이터 큐레이션&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #fdfdfc; color: #0a0a0a; text-align: start;&quot;&gt;Day 2 - 데이터 전처리 (LLM으로 재전처리)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #fdfdfc; color: #0a0a0a; text-align: start;&quot;&gt;마무리&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;background-color: #99cefa;&quot;&gt;들어가며&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지난 3개의 글에서는 RAG를 다뤘다. 모델은 그대로 두고, 외부 지식을 검색해서 물려주는 방식이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Week 6~7은 결이 다르다. 이번엔 모델 자체를 우리 문제에 맞게 똑똑하게 만드는 파인튜닝(fine-tuning)을 말로만 듣다 드디어 해본다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LLM을 똑똑하게 쓰는 방법은 크게 두 갈랜데,&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;추론 시점(inference-time)에 손대는 법: 멀티샷 프롬프팅, 툴/펑션 콜링, RAG 등 -&amp;gt; 모델은 그대로 두고 입력을 잘 차려주는 쪽&lt;/li&gt;
&lt;li&gt;학습(training)으로 모델을 바꾸는 법: 파인튜닝 -&amp;gt; 모델의 파라미터(가중치) 자체를 우리 데이터에 맞게 다시 조정&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RAG를 다뤘던 Week 5(지난 3개의 글)까지가 전자였다면, Week 6~7은 후자로 넘어간다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Week 6~7 은 캡스톤 프로젝트 형식으로 진행하는데 상품의 설명을 기반으로 가격을 맞추는 &quot;THE PRICE IS RIGHT&quot;라는 프로젝트를 진행한다.&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Week 6에서는 초반부에 관련 데이터를 모으고 전처리 한뒤, 여러 모델로 가격을 맞춰본 뒤 프론티어 모델 (GPT)를 파인튜닝&amp;nbsp;&lt;/li&gt;
&lt;li&gt;Week 7은 오픈소스 모델(라마)를 파인튜닝해서 성능을 보는 작업을 진행&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글에서는 Week 6에서의 앞 이틀, 데이터를 만들고 전처리하는 과정(Day 1-2)을 다룬다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모델을 돌리고 파인튜닝하는 Day 3-5는 다음 글에서 이어 간다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;background-color: #99cefa;&quot;&gt;1. 어떤 프로젝트인가&lt;/span&gt;&lt;/h3&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;입력은 &quot;스테인리스 텀블러 600ml, 보온 6시간, 브랜드 X&quot; 같은 상품 설명이고, 출력은 &quot;$24.90&quot; 같은 값이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 자연스러운 의문이 든다. 가격 예측은 전통적인 회귀 모델(선형회귀 같은)이 원래 잘하는 영역인데, 왜 굳이 LLM을 쓰나?&lt;/p&gt;
&lt;p 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;평가가 명확하다(clear and tangible eval):&lt;/b&gt; 정답 가격이 있으니 &quot;예측가와 실제가의 차이&quot;처럼 숫자로 딱 떨어지게 성능을 잴 수 있다. &quot;대화 잘했네&quot; 같은 주관 평가가 아니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;언어 뉘앙스/세부 맥락을 LLM은 잘 파악한다:&lt;/b&gt; 상품 설명은 정돈된 표가 아니라 텍스트 뭉치다. 같은 &quot;60인치 TV&quot;라도 그냥 TV인지, 스마트 TV인지, 럭셔리 포지셔닝인지에 따라 가격이 갈린다. LLM은 이런 뉘앙스(속성 추출 + 의미 해석)를 잘한다 -&amp;gt; 가격 예측 성능이 잘 나올 수 있음&lt;/li&gt;
&lt;li&gt;&lt;b&gt;프론티어 모델이 원래 이걸 잘한다:&lt;/b&gt; 최신 모델은 무수히 많은 인터넷 학습 데이터에서 얻은 감 덕에 &quot;대충 말이 되는&quot; 가격을 꽤 잘 부른다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;6주 차 전체 로드맵은 이렇게 흘러간다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Day 1: 데이터 큐레이션 (데이터 수집&amp;middot;정제)&lt;/li&gt;
&lt;li&gt;Day 2: 데이터 전처리 (표준 포맷으로 재작성)&lt;/li&gt;
&lt;li&gt;Day 3: 평가&amp;middot;베이스라인&amp;middot;전통 ML&lt;/li&gt;
&lt;li&gt;Day 4: 딥러닝과 LLM&lt;/li&gt;
&lt;li&gt;Day 5: 프론티어 모델 파인튜닝&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;느낀 점&lt;/h4&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;RAG와 파인튜닝을 &quot;추론 시점에 손대기 vs 학습으로 모델을 바꾸기&quot;로 가르는 프레임이 깔끔했다. 당장은 서비스를 만드는 과정에서 파인튜닝까지 고려해야 할 상황이 많지는 않겠지만, 기획자 입장에서 지금 상황이 파인튜닝까지 고려해야 할 상황일까? 를 고민하는 데 있어 하나의 기준점을 알려준 기분이었다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;단순 회귀, 전통 ML이 잘 맞을 거 같은 영역으로 보이지만 LLM 만의 장점이 있을 수 있다는 걸 알려준 점이 인상적이었다. (언어의 뉘앙스 캐치 등)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;background-color: #99cefa;&quot;&gt;2. Day 1 - 데이터 큐레이션&lt;/span&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2-1) 좋은 데이터는 어디서 오나&lt;/h4&gt;
&lt;p 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;Kaggle&lt;/li&gt;
&lt;li&gt;허깅페이스 데이터셋&lt;/li&gt;
&lt;li&gt;합성 데이터(synthetic data)&lt;/li&gt;
&lt;li&gt;scale.com 같은 데이터 전문 업체&lt;/li&gt;
&lt;/ul&gt;
&lt;p 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;학습 데이터(train): 모델의 내부 파라미터를 조정하는 데 쓴다.&lt;/li&gt;
&lt;li&gt;검증 데이터(validation): 학습에 포함되지 않은 데이터. 훈련 중에 반복적으로 들여다보며 &quot;더 나아지고 있나, 나빠지고 있나&quot;를 확인한다.&lt;/li&gt;
&lt;li&gt;테스트 데이터(test): 마지막에 딱 한 번, 일반화가 잘 되는지(과적합이 되진 않았는지) 최종 판정에 쓴다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전통 ML에서는 보통 80/10/10으로 나누는 경향이 많다고 설명해줬고, LLM에서는 학습에 훨씬 많은 데이터를 쓴다고 강의에서 말해줬다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(워낙 다루는 데이터의 양이 크다 보니까, 학습 데이터에서 최대한 손실이 나는 걸 방지하고 싶은 것일까 싶기도 하고)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p 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;모델 중심(model-centric): loss, LLM에서는 cross-entropy loss. 모델이 결과를 얼마나 잘못 예측했는지를 본다.&lt;/li&gt;
&lt;li&gt;비즈니스 중심(business-centric): 실제 가격 차이, MSE(예측 가격과 실제 가격 차이를 제곱한 값) 같은 것. 이번 프로젝트의 진짜 목표 지표다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;=&amp;gt; 실제 모델 쪽 같은 경우는 이런 모델 중심(기술 지표)과 프로젝트 목적에 맞는 비즈니스 지표를 복합적으로 보는 게 중요하다 느꼈다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2-2) 데이터셋 불러오기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에 쓸 데이터는 McAuley-Lab의 Amazon-Reviews-2023이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아마존 상품 메타데이터가 카테고리별로 정리돼 있다. 먼저 가전(Appliances) 카테고리부터 열어 본다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;from datasets import load_dataset

dataset = load_dataset(
    &quot;McAuley-Lab/Amazon-Reviews-2023&quot;,
    &quot;raw_meta_Appliances&quot;,
    split=&quot;full&quot;,
    trust_remote_code=True
)
print(f&quot;Number of Appliances: {len(dataset):,}&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출력:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Number of Appliances: 94,327
&lt;/code&gt;&lt;/pre&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 한 건을 까 보면, title(상품명), features(특징 리스트), description, price, 그리고 details(브랜드&amp;middot;무게&amp;middot;모델번호 등이 들어 있는 JSON 문자열) 같은 필드가 보인다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-05-31 오후 1.40.03.png&quot; data-origin-width=&quot;1954&quot; data-origin-height=&quot;1476&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FC9qG/dJMcag6Lgtc/3HObGRBk5PJASI9QgU2sFK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FC9qG/dJMcag6Lgtc/3HObGRBk5PJASI9QgU2sFK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FC9qG/dJMcag6Lgtc/3HObGRBk5PJASI9QgU2sFK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFC9qG%2FdJMcag6Lgtc%2F3HObGRBk5PJASI9QgU2sFK%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;1954&quot; height=&quot;1476&quot; data-filename=&quot;스크린샷 2026-05-31 오후 1.40.03.png&quot; data-origin-width=&quot;1954&quot; data-origin-height=&quot;1476&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;재미 삼아 가장 비싼 가전을 찾아봤다.&lt;/p&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;max_price = 0
max_item = None
for datapoint in tqdm(dataset):
    try:
        price = float(datapoint[&quot;price&quot;])
        if price &amp;gt; max_price:
            max_item, max_price = datapoint, price
    except ValueError:
        pass
print(f&quot;The most expensive is {max_item['title']} at {max_price:,.2f}&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출력:&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;The most expensive item is TurboChef BULLET Rapid Cook Electric Microwave Convection Oven and it costs 21,095.62
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2만 달러짜리 업소용 오븐이다. 이런 극단값은 뒤에서 가격 범위 필터로 걸러진다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-05-31 오후 1.42.43.png&quot; data-origin-width=&quot;2848&quot; data-origin-height=&quot;1476&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cYIsSo/dJMcahR6K9G/Rzhm8YSTebStbDA7ZDy5SK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cYIsSo/dJMcahR6K9G/Rzhm8YSTebStbDA7ZDy5SK/img.png&quot; data-alt=&quot;유사 제품 아마존에서 실제로 찾아본것&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cYIsSo/dJMcahR6K9G/Rzhm8YSTebStbDA7ZDy5SK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcYIsSo%2FdJMcahR6K9G%2FRzhm8YSTebStbDA7ZDy5SK%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;550&quot; height=&quot;285&quot; data-filename=&quot;스크린샷 2026-05-31 오후 1.42.43.png&quot; data-origin-width=&quot;2848&quot; data-origin-height=&quot;1476&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;유사 제품 아마존에서 실제로 찾아본것&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&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;h4 data-ke-size=&quot;size20&quot;&gt;2-3) Item 클래스로 정제하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원본 데이터셋을 그대로 쓸 수는 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1) 가격이 적당한 범위인지,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2) 설명이 충분히 긴지 걸러야 한다.&amp;nbsp;&lt;br /&gt;(너무 과도하게 비싸거나, 너무 과도하게 설명이 짧은 건 적당히 데이터를 거르는 게 모델 학습엔 좋음)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 로직은 pricer/parser.py의 parse()와 pricer/items.py의 Item 클래스에 들어 있다. 핵심 규칙은 이렇다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;가격이 숫자가 아니면 None 반환&lt;/li&gt;
&lt;li&gt;가격이 $0.50 - $999.49 범위인 것만 통과(2만 달러 오븐은 여기서 탈락)&lt;/li&gt;
&lt;li&gt;title/description/features/details를 scrub()으로 합치고 정제&amp;nbsp; - 불필요한 항목(Part Number, Best Sellers Rank 등) 제거, 공백 정리, 정규식으로 제품번호 같은 영숫자 코드 제거&amp;nbsp;&lt;/li&gt;
&lt;li&gt;정제된 텍스트가 최소 600자 이상(MIN_CHARS) 일 때만 Item 생성&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;from pricer.parser import parse

items = [parse(datapoint, &quot;Appliances&quot;) for datapoint in tqdm(dataset)]
items = [item for item in items if item is not None]
print(f&quot;There are {len(items):,} items from {len(dataset):,} datapoints&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출력:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;There are 35,307 items from 94,327 datapoints
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;9만 4천여 건에서 3만 5천여 건만 남았다. 절반 넘게 버린 셈이다.&lt;/p&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;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;2-4) 분포 살펴보기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;남은 데이터의 텍스트 길이(설명 길이)와 가격 분포를 그려 본다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;prices = [item.price for item in items]
lengths = [len(item.full) for item in items]

plt.hist(lengths, bins=range(0, 6000, 100), color=&quot;lightblue&quot;)
plt.hist(prices, bins=range(0, 1000, 10), color=&quot;orange&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/biordF/dJMcaayKbOT/CWpweg07FypomR70EPQin0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/biordF/dJMcaayKbOT/CWpweg07FypomR70EPQin0/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;1836&quot; data-origin-height=&quot;854&quot; data-filename=&quot;스크린샷 2026-05-31 오후 1.53.55.png&quot; style=&quot;width: 49.4186%; margin-right: 10px;&quot; data-widthpercent=&quot;50&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/biordF/dJMcaayKbOT/CWpweg07FypomR70EPQin0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbiordF%2FdJMcaayKbOT%2FCWpweg07FypomR70EPQin0%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;1836&quot; height=&quot;854&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cTWXqb/dJMcadvmv63/ky0KwJJXs1CRvRk5PE06j1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cTWXqb/dJMcadvmv63/ky0KwJJXs1CRvRk5PE06j1/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;1836&quot; data-origin-height=&quot;854&quot; data-filename=&quot;스크린샷 2026-05-31 오후 1.54.56.png&quot; style=&quot;width: 49.4186%;&quot; data-widthpercent=&quot;50&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cTWXqb/dJMcadvmv63/ky0KwJJXs1CRvRk5PE06j1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcTWXqb%2FdJMcadvmv63%2Fky0KwJJXs1CRvRk5PE06j1%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;1836&quot; height=&quot;854&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;(좌) 텍스트 길이 분포 히스토그램)/(우) 가격 분포 히스토그램&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&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;gt; 예전에 ML 짧게 공부해 봤을 때도 그렇지만 이렇게 학습하려는 대상의 데이터의 분포, 특성들을 보면서 전처리를 추가로 하거나 어떻게 학습을 할지 정리해 나가는 작업이 정말 중요한 거 같다. 학습 쪽에서는&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2-5) 8개 카테고리로 확장하고 중복 제거&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에선 가전 하나로 데이터 처리해 봤는데, 가전 카테고리만으로 진행하기엔 부족하다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 ItemLoader로 8개 카테고리를 한꺼번에 불러와서 처리했다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 로더는 내부적으로 병렬 처리로 빠르게 정제까지 끝낸다.&lt;/p&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;from pricer.loaders import ItemLoader

dataset_names = [
    &quot;Automotive&quot;, &quot;Electronics&quot;, &quot;Office_Products&quot;,
    &quot;Tools_and_Home_Improvement&quot;, &quot;Cell_Phones_and_Accessories&quot;,
    &quot;Toys_and_Games&quot;, &quot;Appliances&quot;, &quot;Musical_Instruments&quot;,
]
items = []
for dataset_name in dataset_names:
    loader = ItemLoader(dataset_name)
    items.extend(loader.load())&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 카테고리를 순회하며 로드해서 items 하나로 합침 (extend)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-05-31 오후 2.03.59.png&quot; data-origin-width=&quot;1228&quot; data-origin-height=&quot;210&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xKf84/dJMcagTfyRq/TzuJULnjuL3aKTDypPDcm0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xKf84/dJMcagTfyRq/TzuJULnjuL3aKTDypPDcm0/img.png&quot; data-alt=&quot;총 290만여개 아이템이 모임&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xKf84/dJMcagTfyRq/TzuJULnjuL3aKTDypPDcm0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxKf84%2FdJMcagTfyRq%2FTzuJULnjuL3aKTDypPDcm0%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;1228&quot; height=&quot;210&quot; data-filename=&quot;스크린샷 2026-05-31 오후 2.03.59.png&quot; data-origin-width=&quot;1228&quot; data-origin-height=&quot;210&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;총 290만여개 아이템이 모임&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&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;middot;검증 데이터가 오염되기 때문이다.&lt;/p&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;random.seed(42)
random.shuffle(items)

seen = set()
items = [x for x in tqdm(items) if not (x.title in seen or seen.add(x.title))]

seen = set()
items = [x for x in tqdm(items) if not (x.full in seen or seen.add(x.full))]

del seen
print(f&quot;After deduplication, we have {len(items):,} items&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;seed(42) + shuffle -&amp;gt; 카테고리 순서로 정렬된 걸 섞음 (재현 가능하게)&amp;nbsp;&lt;/li&gt;
&lt;li&gt;제목 중복 제거 후 full 텍스트 중복제거&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 텍스트 길이(설명), 가격 분포는 아래와 같다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/KSj35/dJMcaiQ1WOF/euU8txt9XHCHWSwgH6VD41/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/KSj35/dJMcaiQ1WOF/euU8txt9XHCHWSwgH6VD41/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;1838&quot; data-origin-height=&quot;836&quot; data-filename=&quot;스크린샷 2026-05-31 오후 2.09.14.png&quot; style=&quot;width: 49.4186%; margin-right: 10px;&quot; data-widthpercent=&quot;50&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/KSj35/dJMcaiQ1WOF/euU8txt9XHCHWSwgH6VD41/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FKSj35%2FdJMcaiQ1WOF%2FeuU8txt9XHCHWSwgH6VD41%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;1838&quot; height=&quot;836&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/5sIrE/dJMcabRSQgo/2nyzqcz4DlOF9d1i44jD3k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/5sIrE/dJMcabRSQgo/2nyzqcz4DlOF9d1i44jD3k/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;1838&quot; data-origin-height=&quot;836&quot; data-filename=&quot;스크린샷 2026-05-31 오후 2.09.29.png&quot; data-widthpercent=&quot;50&quot; style=&quot;width: 49.4186%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/5sIrE/dJMcabRSQgo/2nyzqcz4DlOF9d1i44jD3k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F5sIrE%2FdJMcabRSQgo%2F2nyzqcz4DlOF9d1i44jD3k%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;1838&quot; height=&quot;836&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;(좌) 텍스트 길이 분포 그래프/ (우) 가격 분포 그래프&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&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;p data-ke-size=&quot;size16&quot;&gt;또한, 이 단계에서 카테고리별 개수를 막대/파이 차트로 보면, 8개 카테고리 중 Automotive가 압도적으로 많은 걸 알 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-05-31 오후 2.10.42.png&quot; data-origin-width=&quot;1838&quot; data-origin-height=&quot;972&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IloTq/dJMb99T6lIg/kjpWX0pDwMOoPo3HYak74k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IloTq/dJMb99T6lIg/kjpWX0pDwMOoPo3HYak74k/img.png&quot; data-alt=&quot;(이미지 - 카테고리별 상품 개수 막대 차트)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IloTq/dJMb99T6lIg/kjpWX0pDwMOoPo3HYak74k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIloTq%2FdJMb99T6lIg%2FkjpWX0pDwMOoPo3HYak74k%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;1838&quot; height=&quot;972&quot; data-filename=&quot;스크린샷 2026-05-31 오후 2.10.42.png&quot; data-origin-width=&quot;1838&quot; data-origin-height=&quot;972&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;(이미지 - 카테고리별 상품 개수 막대 차트)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2-6) 가중 샘플링으로 균형 맞추기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기가 큐레이션의 핵심이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 정리한 전체 데이터를 그냥 다 쓰면 두 가지 문제가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫째, 싼 물건이 너무 많아(위 보라그래프 참조) 모델이 &quot;대충 싸게 부르면 맞네&quot; 쪽으로 치우친다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘째, Automotive 카테고리가 데이터를 지배한다. (데이터 카테고리가 한 카테고리에 치중되어 있어)&lt;/p&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;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;b&gt;과대표집된 카테고리는 가중치를 깎아서 820,000개를 샘플링한다.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;livecodeserver&quot;&gt;&lt;code&gt;np.random.seed(42)
SIZE = 820_000

prices = np.array([it.price for it in items], dtype=float)
categories = np.array([it.category for it in items])
p = (prices - prices.min()) / (prices.max() - prices.min() + 1e-9)

w = p**2
w[categories == &quot;Tools_and_Home_Improvement&quot;] *= 0.5
w[categories == &quot;Automotive&quot;] *= 0.05

w = w / w.sum()
idx = np.random.choice(len(items), size=SIZE, replace=False, p=w)
sample = [items[i] for i in idx]
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;가격을 0~1로 정규화한뒤 제곱(p**2) -&amp;gt; 비싼 상품이 더 잘 뽑히게 가중치 (저가에만 쏠리지 않도록 가격 분포를 퍼줌)&amp;nbsp;&lt;/li&gt;
&lt;li&gt;과대표 카테고리는 가중치를 깎음 (Tools(2등 카테고리) x 0.5, Automotive(1등 카테고리) x 0.05)&amp;nbsp;&lt;/li&gt;
&lt;li&gt;이 가중치 w를 확률로 만들어 np.random.choice로 중복 없이 (replace=False) 82만 건 추출 (총 290만여 건 중)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;목적: 가격이 더 고르게 분포하고, 카테고리 균형이 잡힌 학습용 데이터셋 만들기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;샘플링 후 가격 분포를 다시 그리면 싼 쪽으로 쏠려 있던 게 한결 평평해진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;카테고리도 Automotive가 여전히 선두지만 훨씬 완만해졌다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bPhZUd/dJMcaarYBDr/oOPcLkn0nWvrBqj6psTk10/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bPhZUd/dJMcaarYBDr/oOPcLkn0nWvrBqj6psTk10/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;1838&quot; data-origin-height=&quot;844&quot; data-filename=&quot;스크린샷 2026-05-31 오후 2.17.50.png&quot; style=&quot;width: 39.731%; margin-right: 10px;&quot; data-widthpercent=&quot;40.68&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bPhZUd/dJMcaarYBDr/oOPcLkn0nWvrBqj6psTk10/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbPhZUd%2FdJMcaarYBDr%2FoOPcLkn0nWvrBqj6psTk10%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;1838&quot; height=&quot;844&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bh43HV/dJMcahLnKMD/gch3ykSktHGg6SGBWKI7B1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bh43HV/dJMcahLnKMD/gch3ykSktHGg6SGBWKI7B1/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;1838&quot; data-origin-height=&quot;964&quot; data-filename=&quot;스크린샷 2026-05-31 오후 2.18.29.png&quot; style=&quot;width: 34.7853%; margin-right: 10px;&quot; data-widthpercent=&quot;35.61&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bh43HV/dJMcahLnKMD/gch3ykSktHGg6SGBWKI7B1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbh43HV%2FdJMcahLnKMD%2Fgch3ykSktHGg6SGBWKI7B1%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;1838&quot; height=&quot;964&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Cizwz/dJMcaiKeH2r/Pk3oj1aUKyPgQb0bGOWEik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Cizwz/dJMcaiKeH2r/Pk3oj1aUKyPgQb0bGOWEik/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;1838&quot; data-origin-height=&quot;1448&quot; data-filename=&quot;스크린샷 2026-05-31 오후 2.18.52.png&quot; style=&quot;width: 23.1581%;&quot; data-widthpercent=&quot;23.71&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Cizwz/dJMcaiKeH2r/Pk3oj1aUKyPgQb0bGOWEik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCizwz%2FdJMcaiKeH2r%2FPk3oj1aUKyPgQb0bGOWEik%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;1838&quot; height=&quot;1448&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;(좌) 가격 분포 (가운데, 우) 카테고리 분포&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 텍스트 길이와 무게가 가격과 단순 상관이 있는지 산점도로 확인한다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;plt.scatter([len(i.full) for i in sample], [i.price for i in sample], s=0.2)
plt.scatter([i.weight for i in sample], [i.price for i in sample], s=0.2)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vsp7z/dJMcagr92iV/DjBKSaBP2Br2HtdPpkCDEK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vsp7z/dJMcagr92iV/DjBKSaBP2Br2HtdPpkCDEK/img.png&quot; data-origin-width=&quot;1246&quot; data-origin-height=&quot;701&quot; data-is-animation=&quot;false&quot; style=&quot;width: 49.1425%; margin-right: 10px;&quot; data-widthpercent=&quot;49.72&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vsp7z/dJMcagr92iV/DjBKSaBP2Br2HtdPpkCDEK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fvsp7z%2FdJMcagr92iV%2FDjBKSaBP2Br2HtdPpkCDEK%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;1246&quot; height=&quot;701&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/suO1B/dJMcagr92jZ/FHhYCoqTTG4rHcKbPc09w1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/suO1B/dJMcagr92jZ/FHhYCoqTTG4rHcKbPc09w1/img.png&quot; data-origin-width=&quot;1260&quot; data-origin-height=&quot;701&quot; data-is-animation=&quot;false&quot; style=&quot;width: 49.6947%;&quot; data-widthpercent=&quot;50.28&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/suO1B/dJMcagr92jZ/FHhYCoqTTG4rHcKbPc09w1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsuO1B%2FdJMcagr92jZ%2FFHhYCoqTTG4rHcKbPc09w1%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;1260&quot; height=&quot;701&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;(좌) 텍스트길이 - 가격 산점도 / (우) 텍스트길이 - 무게 산점도&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과는 &quot;뚜렷한 단순 상관은 없다&quot;였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설명 길이가 길다고 비싸지도, 물건 무게가 무겁다고 비싸지도 않다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 가격은 글자 수나 무게 같은 단순 피처가 아니라 내용의 의미에서 나온다는 뜻이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 글 서두에 &quot;왜 LLM인가&quot;의 두 번째 이유와 정확히 연결되는 지점이다.&lt;/p&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;h4 data-ke-size=&quot;size20&quot;&gt;2-7) 허깅페이스 허브에 업로드&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;완성된 데이터셋을 허브에 올린다. 전체 버전(full)과, 빠르게 실습할 수 있는 경량 버전(lite)을 함께 만든다.&lt;/p&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;username = &quot;{유저네임}&quot;
train = sample[:800_000]
val   = sample[800_000:810_000]
test  = sample[810_000:]
Item.push_to_hub(f&quot;{username}/items_raw_full&quot;, train, val, test)

Item.push_to_hub(f&quot;{username}/items_raw_lite&quot;,
                 train[:20_000], val[:1_000], test[:1_000])&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;items_raw_full: 학습 80만 / 검증 1만 / 테스트 1만&lt;/li&gt;
&lt;li&gt;items_raw_lite: 학습 2만 / 검증 1천 / 테스트 1천&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;느낀 점&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&quot;데이터를 모은다&quot;가 아니라 &quot;분포를 설계한다&quot;가 큐레이션의 본질이었다. 가격을 제곱해 가중치를 주고 특정 카테고리를 0.05배로 깎는 한 줄 한 줄이, 결국 모델이 무엇을 학습할지를 정한다. 모델보다 이 샘플링(데이터) 코드가 성능을 더 좌우한다는 말이 와닿았다.&lt;/li&gt;
&lt;li&gt;ML 공부할 때도 모델링 쪽보다 이 데이터 전처리 쪽에 시간을 많이 들였던 게 생각나면서 이 데이터 전처리의 중요성을 많이 느꼈던 거 같다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;길이&amp;middot;무게와 가격이 단순 상관이 없다는 산점도 한 장이, &quot;왜 LLM을 쓰는지&quot;를 데이터로 증명해 줬다. 기획할 때도 &quot;이 피처로 충분한가&quot;를 먼저 그려 보면 모델 선택의 근거가 생긴다고 생각했다. =&amp;gt; 데이터를 통해 지속적으로 가설을 검증해 나가는 것의 중요성을 여기서도 느꼈다.&lt;/li&gt;
&lt;li&gt;train/val/test를 나누고 중복을 제거하는 건 지루하지만, 이게 빠지면 나중 평가 숫자를 통째로 못 믿게 된다. 평가의 신뢰도는 데이터 단계에서 이미 결정된다. =&amp;gt; 꼭 모델 학습이 아니라 에이전트 기획/개발 과정에서도 에이전트 응답 평가를 이런 식의 3단계 구조로 해야겠다는 생각을 했다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;background-color: #99cefa;&quot;&gt;3. Day 2 - 데이터 전처리 (LLM으로 재 전처리)&lt;/span&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3-1) 왜 또 손을 대나&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Day 1(위 부분)에서 정제까지 했는데 왜 전처리를 또 하나?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Day 1이 &quot;쓸 데이터를 거르는&quot; 작업이었다면, Day 2는 &quot;남은 데이터를 일정한 형식으로 다시 쓰는&quot; 작업이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아마존 데이터 원본 상품 설명은 제각각이다. 어떤 건 마케팅 문구로 도배돼 있고, 어떤 건 표 쪼가리만 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이걸 LLM으로 깔끔한 표준 포맷(제목/카테고리/브랜드/설명/특징)으로 재작성한다.&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3-2) 데이터 불러오고 시스템 프롬프트 잡기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;경량 데이터 버전(lite)을 허브에서 내려받는다. 학습&amp;middot;검증&amp;middot;테스트를 합쳐 22,000건이다.&lt;/p&gt;
&lt;pre class=&quot;makefile&quot;&gt;&lt;code&gt;LITE_MODE = True
dataset = f&quot;ed-donner/items_raw_lite&quot; if LITE_MODE else f&quot;ed-donner/items_raw_full&quot;
train, val, test = Item.from_hub(dataset)
items = train + val + test
print(f&quot;Loaded {len(items):,} items&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출력:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Loaded 22,000 items
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배치 결과를 나중에 매칭하려면 각 아이템에 id가 필요하다.&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;for index, item in enumerate(items):
    item.id = index
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;재작성 규칙은 시스템 프롬프트에 박아 둔다. 출력 형식을 강제하는 게 포인트다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;SYSTEM_PROMPT = &quot;&quot;&quot;Create a concise description of a product. Respond only in this format. Do not include part numbers.
Title: Rewritten short precise title
Category: eg Electronics
Brand: Brand name
Description: 1 sentence description
Details: 1 sentence on features&quot;&quot;&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3-3) 모델로 한 건 재작성해 보기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서는 Groq에 등재되어 있는 오픈소스 모델 openai/gpt-oss-20b를 쓴다. 도어락 상품 하나를 넣어 본다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;from litellm import completion

messages = [
    {&quot;role&quot;: &quot;system&quot;, &quot;content&quot;: SYSTEM_PROMPT},
    {&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: items[0].full},
]
response = completion(messages=messages, model=&quot;groq/openai/gpt-oss-20b&quot;, reasoning_effort=&quot;low&quot;)
print(response.choices[0].message.content)
print(f&quot;Cost: {response._hidden_params['response_cost']*100:.3f} cents&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출력:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Title: Schlage F59 &amp;amp; 613 Interior Knob with Deadbolt - Oil Rubbed Bronze (Interior Half Only)
Category: Hardware
Brand: Schlage
Description: A durable oil-rubbed bronze interior knob paired with a deadbolt, designed for easy installation.
Details: Features a solid metal construction, 1.5-lb weight, and a lifetime mechanical and finish warranty.

Cost: 0.007 cents
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지저분하던 원본이 다섯 줄짜리 표준 포맷으로 정리됐다. 한 건에 0.007센트. 참고로&amp;nbsp; OSS 계열 모델은 추론 토큰(reasoning token)도 출력 토큰에 합산돼서 출력 토큰 수가 좀 크게 잡힌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 프롬프트를 로컬 ollama/llama3.2로도 돌려서 비교해 봤는데, 무료 로컬 모델로도 그럭저럭 비슷한 포맷이 나왔다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lEdjN/dJMcahdyq5O/bfFfSw1lWa37kmOVNR4I5k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lEdjN/dJMcahdyq5O/bfFfSw1lWa37kmOVNR4I5k/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;1942&quot; data-origin-height=&quot;818&quot; data-filename=&quot;스크린샷 2026-05-31 오후 2.33.54.png&quot; style=&quot;width: 49.4186%; margin-right: 10px;&quot; data-widthpercent=&quot;50&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lEdjN/dJMcahdyq5O/bfFfSw1lWa37kmOVNR4I5k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlEdjN%2FdJMcahdyq5O%2FbfFfSw1lWa37kmOVNR4I5k%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;1942&quot; height=&quot;818&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/SDSwT/dJMcacDk6v1/mAGKTKDAgafmB3FVyjrnc0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/SDSwT/dJMcacDk6v1/mAGKTKDAgafmB3FVyjrnc0/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;1942&quot; data-origin-height=&quot;818&quot; data-filename=&quot;스크린샷 2026-05-31 오후 2.34.11.png&quot; style=&quot;width: 49.4186%;&quot; data-widthpercent=&quot;50&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/SDSwT/dJMcacDk6v1/mAGKTKDAgafmB3FVyjrnc0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FSDSwT%2FdJMcacDk6v1%2FmAGKTKDAgafmB3FVyjrnc0%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;1942&quot; height=&quot;818&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;(좌) OSS 20b (우) 로컬 라마 3.2&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3-4) 배치 API - 그리고 막힌 지점&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;22,000건을 한 건씩 호출하면 느리고 번거롭다. 그래서 배치 API를 쓴다. 각 요청을 JSONL 한 줄로 만들어 파일로 모은 뒤, 통째로 제출하는 방식이다.&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;def make_jsonl(item):
    body = {
        &quot;model&quot;: &quot;openai/gpt-oss-20b&quot;,
        &quot;messages&quot;: [
            {&quot;role&quot;: &quot;system&quot;, &quot;content&quot;: SYSTEM_PROMPT},
            {&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: item.full},
        ],
        &quot;reasoning_effort&quot;: &quot;low&quot;,
    }
    line = {&quot;custom_id&quot;: str(item.id), &quot;method&quot;: &quot;POST&quot;,
            &quot;url&quot;: &quot;/v1/chat/completions&quot;, &quot;body&quot;: body}
    return json.dumps(line)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;custom_id에 아이템 id를 넣어 둬서, 결과가 뒤섞여 와도 나중에 정확히 되짚을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1,000건 단위로 파일을 만들어 업로드를 시도했는데, 여기서 막혔다.&lt;/p&gt;
&lt;pre class=&quot;livecodeserver&quot;&gt;&lt;code&gt;with open(&quot;jsonl/0_1000.jsonl&quot;, &quot;rb&quot;) as f:
    response = groq.files.create(file=f, purpose=&quot;batch&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출력:&lt;/p&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;PermissionDeniedError: Error code: 403 - {'error': {'message': 'Not available for your plan', 'type': 'permissions_error', 'code': 'not_available_for_plan'}}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Groq 무료 플랜에서는 배치 API를 못 쓴다는 것이다. 강의에서도 이건 선택 사항으로 두고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직접 돌리고 싶으면 유료 플랜이 필요하고, 그게 아니면 강사가 이미 전처리해 둔 데이터셋을 허브에서 그대로 내려받으면 된다. 비용은 강사 기준으로 lite 데이터가 1달러 미만, 전체 데이터가 30달러 정도였다고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3-5) 배치 클래스와 마무리&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강의에서는 위 로직(1,000건씩 쪼개기 &amp;rarr; 배치 제출 &amp;rarr; 완료되면 결과 수집)을 Batch 클래스로 깔끔하게 감싸 뒀다.&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;Batch.create(items, LITE_MODE)   # JSONL 파일 생성
Batch.run()                      # 배치 제출
Batch.fetch()                    # 결과 받아서 item.summary에 채우기
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과가 채워지면, 허브에 올리기 전에 이제 필요 없는 원본 필드를 비운다.&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;for item in items:
    item.full = None
    item.id = None
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 재작성이 끝난 최종 데이터셋을 허브에 올린다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;if LITE_MODE:
    Item.push_to_hub(&quot;ed-donner/items_lite&quot;,
                     items[:20_000], items[20_000:21_000], items[21_000:])
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 해서 items_lite / items_full이 완성됐다. 다음 글에서 모델을 직접 돌려 가격 예측을 테스트해볼 때 바로 이 데이터를 쓰게 된다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;느낀 점&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;꼭 이런 모델 학습용 데이터 처리 할 때뿐만 아니라 정성데이터 (Ex. cs 문의) 등을 llm에 분석 요청할 때도 이런 공통의 형식으로 한번 정제시킨 후에 분석시키면 훨씬 좋은 성능이 나오지 않을까라는 생각을 하게 되는 순간이었다.&lt;/li&gt;
&lt;li&gt;&quot;거르기(Day 1)&quot;와 &quot;LLM 이용 다시 쓰기(Day 2)&quot;를 분리한 설계가 좋았다. 데이터 정제를 한 덩어리로 생각하기 쉬운데, 버릴 걸 버리는 단계와 남은 걸 표준화하는 단계는 목적도 도구도 다르게 할 수 있는 게 신기했다.&lt;/li&gt;
&lt;li&gt;LLM을 이용해 데이터를 LLM 학습용으로 가공한다는 게 흥미로웠다.&lt;/li&gt;
&lt;li&gt;Groq 같은 시스템에 배치 처리 등이 있기 때문에 사람들이 모델을 특정 솔루션사 홈페이지에서 바로 api key 받아서 쓰는 게 아니라, groq 이용해서 쓰는구나 느꼈던 순간이었다. (이런 Inference 과정에서의 배치 처리를 잘 지원해 주니까) 특히나 production 환경에서 다양한 유저의 발화를 실시간으로 응대하기 위해 이런 배치 처리를 잘해나가는 것도 중요하겠다. 지금은 아니지만, 나중엔 프론티어 랩들의 api key 사용할 때도 그냥 사용하는 것과 이런 배치 처리까지 고려해서 쓰는 것간에 고민이 많이 필요하겠다는 생각을 했다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;background-color: #99cefa;&quot;&gt;4. 마무리&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 편에서는 모델을 한 번도 돌리지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대신 아마존 원본 상품 데이터 9만여 건을&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1) 상품 가격&amp;middot;설명 텍스트 길이로 거르고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2) 8개 상품 카테고리 간의 중복을 없애고&lt;br /&gt;3) 분포를 보며 가중치 이용해 데이터 샘플링을 진행하고&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4) 걸러진 데이터 들에 대해 LLM으로 표준 포맷까지 재작성해서 깨끗한 학습 데이터셋을 만들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강사가 왜 5일짜리 프로젝트에서 데이터 전처리에 이틀을 쓰는지 알 것 같다.&lt;/p&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;p data-ke-size=&quot;size16&quot;&gt;다음 글에서는 드디어 이 데이터로 각 상품의 가격을 맞혀 본다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상수 베이스라인부터 전통 ML(선형회귀&amp;middot;랜덤포레스트&amp;middot;XGBoost), 딥러닝, 그리고 프론티어 LLM까지 줄세워 비교하고, 마지막엔 프론티어 LLM을 직접 파인튜닝해 본다. 그 결과가 꽤 의외인데, 그건 다음 편에서 풀겠다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-05-31 오후 2.45.55.png&quot; data-origin-width=&quot;1942&quot; data-origin-height=&quot;1334&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bbsG1H/dJMcaiQ1XHj/hFyPlbMiIWnQIjaOuod7H0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bbsG1H/dJMcaiQ1XHj/hFyPlbMiIWnQIjaOuod7H0/img.png&quot; data-alt=&quot;과연 모델별로 어떤 결과가 나왔을까&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bbsG1H/dJMcaiQ1XHj/hFyPlbMiIWnQIjaOuod7H0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbbsG1H%2FdJMcaiQ1XHj%2FhFyPlbMiIWnQIjaOuod7H0%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;1942&quot; height=&quot;1334&quot; data-filename=&quot;스크린샷 2026-05-31 오후 2.45.55.png&quot; data-origin-width=&quot;1942&quot; data-origin-height=&quot;1334&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;과연 모델별로 어떤 결과가 나왔을까&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;(꾸준히 공부하고&lt;span&gt;&amp;nbsp;적을테니&amp;nbsp;&lt;/span&gt;많은 관심 부탁드립니다.)&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;Profile:&lt;/span&gt;&lt;br /&gt;&lt;a style=&quot;color: #0070d1; text-align: start;&quot; href=&quot;http://www.linkedin.com/in/doobeom-kim-%EA%B9%80%EB%91%90%EB%B2%94-2b9649242&quot;&gt;&lt;span&gt;&lt;span style=&quot;color: #0070d1;&quot;&gt;Linkedin&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/p&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;</description>
      <category>AI 공부</category>
      <author>kdb1248</author>
      <guid isPermaLink="true">https://doobeom-coding.tistory.com/90</guid>
      <comments>https://doobeom-coding.tistory.com/90#entry90comment</comments>
      <pubDate>Sun, 31 May 2026 15:02:29 +0900</pubDate>
    </item>
    <item>
      <title>12. RAG의 한계를 어떻게 깰까 &amp;mdash; Advanced RAG (Semantic Chunking&amp;middot;Reranking&amp;middot;Query Rewriting 등)</title>
      <link>https://doobeom-coding.tistory.com/89</link>
      <description>&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;목차&lt;/h3&gt;
&lt;p style=&quot;color: #000000;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;0. 들어가며&lt;br /&gt;1. Advanced RAG로 - 10가지 개선 기법&lt;br /&gt;2. 개선 ① - LLM 기반 지능형 청킹&lt;br /&gt;3. 개선 ② - Reranking&lt;br /&gt;4. 개선 ③ - Query Rewriting&lt;br /&gt;5. 최종 파이프라인 - 모두 합치기&lt;br /&gt;6.&amp;nbsp;마무리&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;background-color: #99cefa;&quot;&gt;0. 들어가며&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000;&quot; data-ke-size=&quot;size16&quot;&gt;지난&amp;nbsp;글에서&amp;nbsp;RAG&amp;nbsp;평가&amp;nbsp;시스템을&amp;nbsp;만들고,&amp;nbsp;기본&amp;nbsp;RAG의&amp;nbsp;한계를&amp;nbsp;점수로&amp;nbsp;정량화했다.&lt;br /&gt;&lt;br /&gt;요약하면&amp;nbsp;이렇다.&lt;br /&gt;&lt;br /&gt;-&amp;nbsp;검색&amp;nbsp;메트릭(MRR&amp;nbsp;/&amp;nbsp;nDCG&amp;nbsp;/&amp;nbsp;Keyword&amp;nbsp;Coverage)에서&amp;nbsp;전반적으로&amp;nbsp;점수가&amp;nbsp;낮고,&amp;nbsp;특히&amp;nbsp;spanning&amp;middot;holistic&amp;nbsp;카테고리가&amp;nbsp;약했다.&lt;br /&gt;-&amp;nbsp;답변&amp;nbsp;메트릭에서&amp;nbsp;풀네임&amp;nbsp;누락&amp;nbsp;같은&amp;nbsp;completeness&amp;nbsp;문제가&amp;nbsp;잡혔다.&lt;br /&gt;-&amp;nbsp;청크&amp;nbsp;크기&amp;middot;임베딩&amp;nbsp;모델만&amp;nbsp;바꾸는&amp;nbsp;단순&amp;nbsp;튜닝으로는&amp;nbsp;이&amp;nbsp;한계를&amp;nbsp;못&amp;nbsp;깬다는&amp;nbsp;결론이었다.&lt;br /&gt;&lt;br /&gt;이번&amp;nbsp;글에서는&amp;nbsp;이&amp;nbsp;한계를&amp;nbsp;깨기&amp;nbsp;위한&amp;nbsp;본질적&amp;nbsp;개선,&amp;nbsp;즉&amp;nbsp;&lt;b&gt;Advanced&amp;nbsp;RAG&amp;nbsp;기법&lt;/b&gt;으로&amp;nbsp;들어간다.&lt;br /&gt;&lt;br /&gt;*&amp;nbsp;청크를&amp;nbsp;더&amp;nbsp;똑똑하게&amp;nbsp;만들고&amp;nbsp;(시맨틱&amp;nbsp;청킹&amp;nbsp;+&amp;nbsp;문서&amp;nbsp;전처리&amp;nbsp;결합)&lt;br /&gt;*&amp;nbsp;검색&amp;nbsp;결과를&amp;nbsp;한&amp;nbsp;번&amp;nbsp;더&amp;nbsp;거르고&amp;nbsp;(Reranking)&lt;br /&gt;*&amp;nbsp;사용자&amp;nbsp;질문을&amp;nbsp;검색&amp;nbsp;친화적으로&amp;nbsp;다듬는다&amp;nbsp;(Query&amp;nbsp;Rewriting)&lt;br /&gt;&lt;br /&gt;이 세 가지를 직접 구현하고, 마지막에 기본 RAG와 점수를 비교해본다.&lt;/p&gt;
&lt;p style=&quot;color: #000000;&quot; data-ke-size=&quot;size16&quot;&gt;그 전에 RAG라는 기술 자체가 여전히 유효한지 잠깐 짚고 시작한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;background-color: #99cefa;&quot;&gt;1. Advanced RAG로- 10가지 개선 기법&lt;/span&gt;&amp;nbsp;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;평가 결과를 통해 확인된 한계를 어떻게 극복할지 본격적으로 보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 전에 먼저 RAG가 여전히 유효한 기술인지부터 짚고 넘어간다.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;1) 잠깐, RAG는 죽었나?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요즘 LLM 트렌드를 보면 RAG가 좀 위태로워 보이는 변화가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫째, 컨텍스트 창이 엄청나게 커졌다. Gemini나 GPT 계열 최신 모델은 100만 토큰까지 컨텍스트 윈도우로 받는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;그럼 그냥 문서 다 때려넣으면 되는 거 아냐?&quot; 라는 의견이 나올 만하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘째, 에이전틱 RAG가 등장했다. 에이전틱 RAG는 LLM이 매 턴마다 &quot;벡터 검색을 할까, SQL 쿼리를 날릴까, 아니면 외부 API를 호출할까&quot;를 직접 결정하고 도구를 골라 쓰는 구조다. 단순한 벡터 검색에서 한참 진화한 모습이다. &quot;이런 게 있는데 일반 RAG는 구식 아니야?&quot; 라는 주장도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;근데 둘 다 &quot;RAG가 죽었다&quot;는 결론을 내릴 만한 근거는 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;긴 컨텍스트를 그냥 다 넣는 방식은,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #fdfdfc; color: #000000; text-align: start;&quot;&gt;KB(Knowledge Base, RAG가 참조하는 문서 저장소)가&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;GB 단위로 커지면 비용&amp;middot;속도가 감당이 안 된다. 매번 full로 넣으면 비싸고 느리다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 핵심 정보가 긴 컨텍스트 어디에 박혀 있느냐에 따라 LLM이 정보를 놓치는 &quot;lost in the middle&quot; 현상도 있어, 컨텍스트가 크다고 무조건 좋은 답을 보장하는 것도 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에이전틱 RAG도 결국 도구로 &quot;벡터 검색&quot;을 부르는 경우가 대부분이다. 즉 에이전틱 RAG도 RAG의 한 형태이지 RAG를 대체하는 게 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론은 RAG는 안 죽었고, 오히려 그 안에서 더 정교한 형태로 진화하고 있다는 거다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 섹션에서 보는 10가지 기법이 그 진화중 일부라고 볼 수 있다.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;2) 10가지 기법 한눈에&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강의에서는 RAG 개선 기법을 10가지로 정리한다. 표로 먼저 한 번 훑고, 아래에 하나씩 풀어쓴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;background-color: #9b9b9b; color: #ffffff;&quot;&gt;#&lt;/td&gt;
&lt;td style=&quot;background-color: #9b9b9b; color: #ffffff;&quot;&gt;기법&lt;/td&gt;
&lt;td style=&quot;background-color: #9b9b9b; color: #ffffff;&quot;&gt;한 줄 설명&lt;/td&gt;
&lt;td style=&quot;background-color: #9b9b9b; color: #ffffff;&quot;&gt;이번 글에서&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;background-color: #efefef;&quot;&gt;1&lt;/td&gt;
&lt;td&gt;시맨틱 청킹&lt;/td&gt;
&lt;td&gt;의미 단위로 분할&lt;/td&gt;
&lt;td&gt;직접 구현&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;background-color: #efefef;&quot;&gt;2&lt;/td&gt;
&lt;td style=&quot;background-color: #f9f9f9;&quot;&gt;인코더 선택&lt;/td&gt;
&lt;td style=&quot;background-color: #f9f9f9;&quot;&gt;임베딩 모델 / 멀티모달&lt;/td&gt;
&lt;td style=&quot;background-color: #f9f9f9;&quot;&gt;컨셉만&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;background-color: #efefef;&quot;&gt;3&lt;/td&gt;
&lt;td&gt;프롬프트 개선&lt;/td&gt;
&lt;td&gt;날짜&amp;middot;이력&amp;middot;컨텍스트 구조화&lt;/td&gt;
&lt;td&gt;컨셉만&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;background-color: #efefef;&quot;&gt;4&lt;/td&gt;
&lt;td style=&quot;background-color: #f9f9f9;&quot;&gt;문서 전처리&lt;/td&gt;
&lt;td style=&quot;background-color: #f9f9f9;&quot;&gt;LLM이 청크 재작성&lt;/td&gt;
&lt;td style=&quot;background-color: #f9f9f9;&quot;&gt;직접 구현 (1과 결합)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;background-color: #efefef;&quot;&gt;5&lt;/td&gt;
&lt;td&gt;Query Rewriting&lt;/td&gt;
&lt;td&gt;사용자 질문 &amp;rarr; KB 친화 쿼리&lt;/td&gt;
&lt;td&gt;직접 구현&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;background-color: #efefef;&quot;&gt;6&lt;/td&gt;
&lt;td style=&quot;background-color: #f9f9f9;&quot;&gt;Query Expansion&lt;/td&gt;
&lt;td style=&quot;background-color: #f9f9f9;&quot;&gt;한 쿼리 &amp;rarr; 여러 쿼리&lt;/td&gt;
&lt;td style=&quot;background-color: #f9f9f9;&quot;&gt;컨셉만&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;background-color: #efefef;&quot;&gt;7&lt;/td&gt;
&lt;td&gt;Reranking&lt;/td&gt;
&lt;td&gt;검색 후 LLM이 재정렬&lt;/td&gt;
&lt;td&gt;직접 구현&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;background-color: #efefef;&quot;&gt;8&lt;/td&gt;
&lt;td style=&quot;background-color: #f9f9f9;&quot;&gt;Hierarchical RAG&lt;/td&gt;
&lt;td style=&quot;background-color: #f9f9f9;&quot;&gt;요약 검색 &amp;rarr; 드릴다운&lt;/td&gt;
&lt;td style=&quot;background-color: #f9f9f9;&quot;&gt;컨셉만&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;background-color: #efefef;&quot;&gt;9&lt;/td&gt;
&lt;td&gt;Graph RAG&lt;/td&gt;
&lt;td&gt;청크 관계 메타데이터 활용&lt;/td&gt;
&lt;td&gt;컨셉만&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;background-color: #efefef;&quot;&gt;10&lt;/td&gt;
&lt;td style=&quot;background-color: #f9f9f9;&quot;&gt;Agentic RAG&lt;/td&gt;
&lt;td style=&quot;background-color: #f9f9f9;&quot;&gt;LLM이 도구 선택&lt;/td&gt;
&lt;td style=&quot;background-color: #f9f9f9;&quot;&gt;컨셉만&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 기법을 좀 풀어보면 이렇다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. 시맨틱 청킹&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 단순히 &quot;문자 1000자마다 자른다&quot; 같은 길이 기반 청킹이 아니라, 텍스트의 의미 경계를 인식해서 자르는 방식&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 문단&amp;middot;주제가 바뀌는 지점에서 청크를 끊는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 청크 안에 의미가 끊기지 않은 한 덩어리가 들어가서 검색 품질이 올라간다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 이번 글에서는 LLM에게 직접 청킹을 시키는 방식으로 구현한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 인코더(임베딩) 선택&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 도메인이나 언어에 잘 맞는 임베딩 모델을 고르는 거다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 한국어 데이터가 많은 KB라면 한국어 데이터로 학습된 모델이 유리하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 멀티모달 RAG의 경우 이미지를 직접 임베딩하기보단, LLM이 캡션을 만들어 텍스트로 임베딩하는 쪽이 잘 작동하는 경우가 많다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 텍스트 임베딩이 보통 더 잘 학습되어 있기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. 프롬프트 개선&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 시스템 프롬프트에 일반 컨텐츠, 현재 날짜, 검색된 컨텍스트, 대화 이력을 명시적으로 구조화해서 넣는 거다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 예를 들어 &quot;최신 가격은?&quot; 같은 시간 의존 질문은 모델에게 &quot;오늘 날짜가 X다&quot;라고 알려주지 않으면 정확히 답할 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 단순한 디테일이지만 답변 품질에 영향이 크다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4. 문서 전처리&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- LLM에게 문서나 청크를 KB 검색에 더 친화적인 형태로 다시 작성하게 시키는 거다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 예를 들어 항공권 가격 테이블을 그냥 두면 자연어 질문과 잘 매칭이 안 되는데, LLM에게 &quot;이 표를 자연어로 풀어서 다시 써줘&quot;라고 하면 검색에 잘 잡히는 형태가 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 1번(시맨틱 청킹)과 결합해서 &quot;의미 단위로 자르면서 동시에 검색 친화적으로 재작성&quot;하는 한 단계로 묶는 게 강력하다. 이번 글에서 그 결합 형태로 구현한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;5. Query Rewriting&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 사용자가 자연어로 던진 질문을 LLM이 KB 검색에 최적화된 짧은 쿼리로 다시 쓰는 거다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 사용자 질문은 보통 &quot;그 사람은 어떤 부서에서 일했지?&quot; 같이 모호하거나 후속 질문 형태인데, 그대로 검색하면 답을 못 찾는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 이전 대화 맥락을 결합해서 &quot;Maxine Thompson 부서&quot; 같은 명확한 검색 쿼리로 바꾸면 검색 적중률이 올라간다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 이번 글에서 직접 구현한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;6. Query Expansion&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 한 질문을 LLM이 여러 변형 쿼리로 만들어 다중 검색을 한 뒤, 결과를 통합하는 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- recall(자료가 컨텍스트에 들어왔는지)을 끌어올리는 데 효과적이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 예를 들어 &quot;보험 청구 절차&quot;라는 질문을 &quot;claim filing process&quot;, &quot;insurance reimbursement steps&quot;, &quot;policy claim workflow&quot; 같은 여러 쿼리로 확장해 검색한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;7. Reranking&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 1차 벡터 검색 결과를 LLM이 다시 관련성 순으로 재정렬하는 단계다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 1차 검색은 recall 우선으로 top-K를 크게(10-20개) 가져오고, 그걸 LLM이 보면서 정말 관련 있는 순서로 재배열한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- recall과 precision을 두 단계로 나눠서 각각 최적화하는 셈이다. 이번 글에서 직접 구현한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;8. Hierarchical RAG&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- KB에 미리 요약을 만들어두고, 질문이 오면 요약을 먼저 검색한 뒤 거기서 더 세부 청크로 드릴다운하는 2단계 구조다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 다중 문서에 걸친 holistic 질문 (예: &quot;회사의 전체 보상 정책은?&quot;) 처리에 유리하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 직원별 임금 정보가 흩어져 있다면, 직원별 임금 요약 페이지를 먼저 만든 다음 그걸 KB에 같이 넣어두는 식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 평가 결과로 본 한계에서 spanning/holistic 카테고리가 약했는데, 거기에 직접 들어맞는 기법이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;*&lt;span style=&quot;background-color: #fdfdfc; color: #6e6c68; text-align: start;&quot;&gt;헷갈리기 쉬운 지점 하나. 청크에 summary가 들어간다고 해서 이게 표의 8번 &quot;Hierarchical RAG&quot;는 아니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #fdfdfc; color: #6e6c68; text-align: start;&quot;&gt;Hierarchical은 요약 인덱스와 청크 인덱스를 따로 두고 두 단계로 검색하는 구조인데, 여기서는 headline&amp;middot;summary&amp;middot;원문을&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;한 덩어리로 합쳐 하나의 인덱스에 저장&lt;/b&gt;&lt;span style=&quot;background-color: #fdfdfc; color: #6e6c68; text-align: start;&quot;&gt;한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #fdfdfc; color: #6e6c68; text-align: start;&quot;&gt;단일 인덱스, 단일 검색이다. 표의 4번 &quot;문서 전처리&quot;에 가까운 형태로 보면 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;9. Graph RAG&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 청크에 메타데이터로 관계 정보(예: &quot;이 청크는 X 부서와 관련됨&quot;, &quot;이 청크는 Y 정책과 연결됨&quot;)를 부여하고, 그래프 DB에 저장한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 특정 청크를 조회할 때 관계로 연결된 청크도 함께 컨텍스트에 포함시킬 수 있다. &quot;X와 협업했던 사람의 부서는?&quot; 같이 관계 추적이 필요한 질문에 강하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;10. Agentic RAG&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- LLM이 매 턴마다 &quot;벡터 검색을 할까, SQL을 쓸까, API를 호출할까&quot;를 스스로 결정하고 도구를 골라 쓰는 구조다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 위 1~9를 도구로 갖되, 그중 무엇을 언제 쓸지를 LLM이 판단한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 멀티턴으로 계속 작동하면서 답을 정제해나갈 수도 있다.&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;3) 이번 글의 집중 포인트&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;10가지 중에 이번 글(강의)에서는 4개를 직접 구현한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;1번(시맨틱 청킹) + 4번(문서 전처리) &amp;rarr; 한 단계로 결합&lt;/li&gt;
&lt;li&gt;7번(Reranking)&lt;/li&gt;
&lt;li&gt;5번(Query Rewriting)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 네 가지를 고른 이유는, 평가 결과로 본 한계(검색 품질 부족, spanning 카테고리 약함, 풀네임 누락 같은 completeness 문제)를 구조적으로 풀어줄 수 있는 가장 효과적인 조합이기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나머지(인코더 선택, 프롬프트 구조화, Query Expansion, Hierarchical, Graph, Agentic)는 위 표 풀이 정도로 컨셉만 잡고 넘어간다.&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;4) LangChain을 뺀다&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 글에서는 LangChain의 DirectoryLoader, RecursiveCharacterTextSplitter, Chroma 헬퍼 등을 적극적으로 썼다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;근데 Advanced RAG로 들어가면서는 LangChain을 빼고 native 코드로 다시 짠다.&lt;/p&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;middot;검색&amp;middot;생성 로직 하나하나에 손을 대야 진짜 개선이 가능한데,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LangChain 추상화 레이어 위에서 그걸 하기엔 제약이 많다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;청크 구조를 바꾸려면 LangChain Document를 그대로 쓰기 어렵고, 검색 후 reranking을 끼워넣으려면 Chain을 분해해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 추상화를 우회해서 native 코드를 써야 하는데, 그럴 거면 처음부터 native가 깔끔하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 처음부터 native로 가는 건 추천하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LangChain으로 한 번 RAG의 기본 흐름을 따라가본 다음, 한계가 보일 때 native로 내려가는 순서가 효율적이다.&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;5) 느낀 점&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;빠른 프로토타이핑 단계에서 LangChain은 정말 강력하지만, 프로덕션에서 RAG 품질을 더 높이려면 결국 추상화 레이어를 벗기게 된다는 점이 인상적이었다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;&quot;RAG가 죽었다&quot;는 말이 자꾸 들리는데, 실제로는 RAG의 정의가 넓어지고 있다는 게 더 정확한 표현 같다. 단순 벡터 검색은 한계가 있지만, 그 위에 reranking, query rewriting, agentic 같은 레이어가 쌓이면서 RAG는 오히려 더 풍성해지는 중인 거 같다&lt;/li&gt;
&lt;li&gt;10가지 기법을 한 번에 적용하려고 하면 길을 잃기 쉽다. 평가 결과를 보고 약한 부분에 맞는 기법부터 하나씩 적용하는 순서가 중요해 보인다. 적용 우선순위를 &quot;평가 점수가 가리키는 약점&quot;에 맞춰서 정하는 게 중요하겠다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;background-color: #99cefa;&quot;&gt;2. 개선 ① - LLM 기반 지능형 청킹&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;표의 1번(시맨틱 청킹)과 4번(문서 전처리)을 한 단계로 묶어 구현한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LLM이 의미 단위로 청크를 자르면서, 동시에 각 청크에 검색용 메타텍스트(headline, summary)를 만들어 붙이는 구조다.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;1) 단순 길이 기반 청킹의 한계&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 글의 RAG 파이프라인에서는 LangChain의 RecursiveCharacterTextSplitter로 청크를 만들었다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;문자 1000자마다 자르고, 200자씩 오버랩.&lt;/li&gt;
&lt;li&gt;문단 경계를 어느 정도 봐주긴 하지만, 결국 길이 기준이 우선이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p 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;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해결의 핵심은 LLM에게 청킹을 맡기는 거다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1) 길이가 아니라 &quot;이 문장과 다음 문장이 같은 주제인가?&quot;를 LLM이 판단하면서 자르고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2) 각 청크에 검색을 도와줄 메타텍스트를 같이 만들어 붙인다.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;2) Chunk Pydantic 모델 - 3-필드 구조&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 청크 하나의 형태를 Pydantic으로 정의한다.&lt;/p&gt;
&lt;pre style=&quot;background-color: #f8f8f8; color: #383a42;&quot;&gt;&lt;code&gt;class Chunk(BaseModel):
    headline: str = Field(description=&quot;A brief heading for this chunk, typically a few words, that is most likely to be surfaced in a query&quot;)
    summary: str = Field(description=&quot;A few sentences summarizing the content of this chunk to answer common questions&quot;)
    original_text: str = Field(description=&quot;The original text of this chunk from the provided document, exactly as is, not changed in any way&quot;)

    def as_result(self, document):
        metadata = {&quot;source&quot;: document[&quot;source&quot;], &quot;type&quot;: document[&quot;type&quot;]}
        return Result(
            page_content=self.headline + &quot;\n\n&quot; + self.summary + &quot;\n\n&quot; + self.original_text,
            metadata=metadata
        )


class Chunks(BaseModel):
    chunks: list[Chunk]
&lt;/code&gt;&lt;/pre&gt;
&lt;p 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;headline : 청크의 짧은 제목. 검색 키 역할.&lt;/li&gt;
&lt;li&gt;summary : 몇 문장 요약. 검색 키이자 컨텍스트 보강 역할.&lt;/li&gt;
&lt;li&gt;original_text : 원문 그대로, 수정 X. 답변 근거 역할.&lt;/li&gt;
&lt;/ul&gt;
&lt;p 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;headline&amp;middot;summary는 LLM이 가공한 텍스트라 자연어 질문과의 매칭은 좋지만, 그 자체는 원문이 아니라 환각이 섞일 가능성이 있다.&lt;/li&gt;
&lt;li&gt;original_text는 손대지 말고 그대로 둬야 환각 위험이 없고 출처 인용&amp;middot;추적이 가능하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 셋을 어떻게 쓰는지가 as_result 메서드에 들어 있다. 세 필드를 한 덩어리로 합쳐서 page_content를 만들고, 이걸 통째로 임베딩한다.&lt;/p&gt;
&lt;pre style=&quot;background-color: #f8f8f8; color: #383a42;&quot;&gt;&lt;code&gt;page_content = headline + &quot;\n\n&quot; + summary + &quot;\n\n&quot; + original_text
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;검색 시점에는 headline&amp;middot;summary 덕분에 자연어 질문과 매칭이 잘 되고, LLM이 답변할 때는 같은 청크 안에 original_text가 들어 있어 정확한 근거로 인용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(앞 섹션에서도 짚었지만, 이건 Hierarchical RAG가 아니다. 인덱스도 한 개, 검색도 한 번이다. 표의 4번 &quot;문서 전처리&quot;에 가까운 형태다.)&lt;/p&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;3) 청킹 프롬프트&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LLM에게 줄 프롬프트는 다음과 같다.&lt;/p&gt;
&lt;pre style=&quot;background-color: #f8f8f8; color: #383a42;&quot;&gt;&lt;code&gt;AVERAGE_CHUNK_SIZE = 500

def make_prompt(document):
    how_many = (len(document[&quot;text&quot;]) // AVERAGE_CHUNK_SIZE) + 1
    return f&quot;&quot;&quot;
You take a document and you split the document into overlapping chunks for a KnowledgeBase.

The document is from the shared drive of a company called Insurellm.
The document is of type: {document[&quot;type&quot;]}
The document has been retrieved from: {document[&quot;source&quot;]}

A chatbot will use these chunks to answer questions about the company.
You should divide up the document as you see fit, being sure that the entire document is returned in the chunks - don't leave anything out.
This document should probably be split into {how_many} chunks, but you can have more or less as appropriate.
There should be overlap between the chunks as appropriate; typically about 25% overlap or about 50 words, so you have the same text in multiple chunks for best retrieval results.

For each chunk, you should provide a headline, a summary, and the original text of the chunk.
Together your chunks should represent the entire document with overlap.

Here is the document:

{document[&quot;text&quot;]}

Respond with the chunks.
&quot;&quot;&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p 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;how_many : 문서 길이를 평균 청크 크기(500자)로 나눠서 적정 청크 수를 동적으로 결정한다. 짧은 문서는 1~2개, 긴 문서는 그에 맞게.&lt;/li&gt;
&lt;li&gt;오버랩 25% (약 50단어): 인접 청크 사이에 겹치는 텍스트가 있으면, 청크 경계에 걸친 정보도 양쪽에서 검색이 잡힌다.&lt;/li&gt;
&lt;li&gt;&quot;전체 문서가 빠짐없이 반환되어야 한다&quot; 명시 : LLM이 임의로 일부를 누락하지 않도록.&lt;/li&gt;
&lt;li&gt;type, source 정보 제공 : LLM이 도메인 컨텍스트를 알고 청킹&amp;middot;요약하도록.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 프롬프트와 함께 LLM을 호출하는 코드는 다음과 같다.&lt;/p&gt;
&lt;pre style=&quot;background-color: #f8f8f8; color: #383a42;&quot;&gt;&lt;code&gt;def make_messages(document):
    return [
        {&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: make_prompt(document)},
    ]


def process_document(document):
    messages = make_messages(document)
    response = completion(model=MODEL, messages=messages, response_format=Chunks)
    reply = response.choices[0].message.content
    doc_as_chunks = Chunks.model_validate_json(reply).chunks
    return [chunk.as_result(document) for chunk in doc_as_chunks]


def create_chunks(documents):
    chunks = []
    for doc in tqdm(documents):
        chunks.extend(process_document(doc))
    return chunks
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서도 structured output이 핵심이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;response_format=Chunks : LLM이 자유 텍스트가 아니라 Chunks 스키마(chunks 배열, 각 원소가 Chunk 객체)에 맞춘 JSON으로 응답하게 강제.&lt;/li&gt;
&lt;li&gt;Chunks.model_validate_json(...) : 받은 JSON을 Pydantic으로 검증&amp;middot;파싱.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 LLM 청킹의 결과물을 안전하게 받아낼 수 있다. 자유 텍스트로 받았다면 &quot;어디부터 어디까지가 한 청크지?&quot; 같은 파싱 문제로 골치를 앓았을 거다.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;4) 자체 문서 로더 - LangChain DirectoryLoader 빼기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 글에서는 LangChain의 DirectoryLoader가 폴더에서 .md 파일을 알아서 긁어왔는데, 이번엔 그것도 직접 짠다.&lt;/p&gt;
&lt;pre style=&quot;background-color: #f8f8f8; color: #383a42;&quot;&gt;&lt;code&gt;KNOWLEDGE_BASE_PATH = Path(&quot;knowledge-base&quot;)

def fetch_documents():
    &quot;&quot;&quot;A homemade version of the LangChain DirectoryLoader&quot;&quot;&quot;
    documents = []
    for folder in KNOWLEDGE_BASE_PATH.iterdir():
        doc_type = folder.name
        for file in folder.rglob(&quot;*.md&quot;):
            with open(file, &quot;r&quot;, encoding=&quot;utf-8&quot;) as f:
                documents.append({
                    &quot;type&quot;: doc_type,
                    &quot;source&quot;: file.as_posix(),
                    &quot;text&quot;: f.read()
                })
    print(f&quot;Loaded {len(documents)} documents&quot;)
    return documents
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pathlib의 rglob으로 재귀적으로 .md 파일을 찾고, 폴더명을 type으로 쓴다 (products, employees, contracts, company)&lt;/p&gt;
&lt;pre style=&quot;background-color: #f8f8f8; color: #383a42;&quot;&gt;&lt;code&gt;class Result(BaseModel):
    page_content: str
    metadata: dict
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이건 LangChain의 Document를 모방한 클래스다. as_result에서 봤듯이 page_content에 합쳐진 텍스트를 담고, metadata에 source/type 정보를 담는다.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;5) ChromaDB에 저장&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;청크가 만들어졌으면 임베딩해서 벡터 DB에 넣어야 한다.&lt;/p&gt;
&lt;pre style=&quot;background-color: #f8f8f8; color: #383a42;&quot;&gt;&lt;code&gt;DB_NAME = &quot;preprocessed_db&quot;
collection_name = &quot;docs&quot;
embedding_model = &quot;text-embedding-3-large&quot;

def create_embeddings(chunks):
    chroma = PersistentClient(path=DB_NAME)
    if collection_name in [c.name for c in chroma.list_collections()]:
        chroma.delete_collection(collection_name)

    texts = [chunk.page_content for chunk in chunks]
    emb = openai.embeddings.create(model=embedding_model, input=texts).data
    vectors = [e.embedding for e in emb]

    collection = chroma.get_or_create_collection(collection_name)

    ids = [str(i) for i in range(len(chunks))]
    metas = [chunk.metadata for chunk in chunks]

    collection.add(ids=ids, embeddings=vectors, documents=texts, metadatas=metas)
    print(f&quot;Vectorstore created with {collection.count()} documents&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p 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;ChromaDB의 PersistentClient를 띄우고, 같은 이름의 컬렉션이 있으면 삭제 (재생성 보장).&lt;/li&gt;
&lt;li&gt;청크의 page_content를 한 번에 OpenAI 임베딩 모델로 보내서 벡터로 변환.&lt;/li&gt;
&lt;li&gt;ids, embeddings, documents, metadatas를 같이 add.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서도 LangChain의 Chroma 헬퍼를 쓰지 않고 ChromaDB 클라이언트를 직접 쓴다. 추상화 한 겹이 빠지면서 &quot;내가 무엇을 저장하고 있는지&quot; (page_content는 합쳐진 텍스트, metadata는 source/type)가 명확해진다.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;6) t-SNE로 시각화 (재등장)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;벡터 DB가 잘 쌓였는지 확인하는 단골 도구가 t-SNE 시각화다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 글에서도 한 번 봤는데, 여기서는 청크 메타텍스트가 추가됨에 따라 클러스터링이 어떻게 변하는지 비교해볼 수 있다.&lt;/p&gt;
&lt;pre style=&quot;background-color: #f8f8f8; color: #383a42;&quot;&gt;&lt;code&gt;chroma = PersistentClient(path=DB_NAME)
collection = chroma.get_or_create_collection(collection_name)
result = collection.get(include=['embeddings', 'documents', 'metadatas'])
vectors = np.array(result['embeddings'])
metadatas = result['metadatas']
doc_types = [metadata['type'] for metadata in metadatas]
colors = [['blue', 'green', 'red', 'orange'][['products', 'employees', 'contracts', 'company'].index(t)] for t in doc_types]

tsne = TSNE(n_components=2, random_state=42)
reduced_vectors = tsne.fit_transform(vectors)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;products(파랑), employees(초록), contracts(빨강), company(주황) 별로 색을 다르게 칠해 plot한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 글의 단순 청킹 결과와 비교했을 때, 청크 안에 headline과 summary가 들어가면서 같은 type끼리 더 뚜렷하게 뭉쳐 보이는 경향을 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2yQp1/dJMcadhE3Zv/AHuHewCTA19dfsezXkzUtk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2yQp1/dJMcadhE3Zv/AHuHewCTA19dfsezXkzUtk/img.png&quot; style=&quot;width: 50.8178%; margin-right: 10px;&quot; data-widthpercent=&quot;51.42&quot; data-filename=&quot;스크린샷 2026-05-09 오후 8.20.11.png&quot; data-origin-height=&quot;1194&quot; data-origin-width=&quot;1598&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2yQp1/dJMcadhE3Zv/AHuHewCTA19dfsezXkzUtk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2yQp1%2FdJMcadhE3Zv%2FAHuHewCTA19dfsezXkzUtk%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;1598&quot; height=&quot;1194&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/k9xLO/dJMcacwi25r/gOO0qiRcFahrISU7LnekGk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/k9xLO/dJMcacwi25r/gOO0qiRcFahrISU7LnekGk/img.png&quot; style=&quot;width: 48.0194%;&quot; data-widthpercent=&quot;48.58&quot; data-filename=&quot;스크린샷 2026-05-09 오후 8.21.19.png&quot; data-origin-height=&quot;1194&quot; data-origin-width=&quot;1510&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/k9xLO/dJMcacwi25r/gOO0qiRcFahrISU7LnekGk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fk9xLO%2FdJMcacwi25r%2FgOO0qiRcFahrISU7LnekGk%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;1510&quot; height=&quot;1194&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;t-SNE 2D/3D 시각화 결과&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;7) 느낀 점&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&quot;청크 = 검색 단위&quot;라는 정의를 &quot;청크 = 검색용 메타 + 답변용 원문&quot;으로 확장한 게 이번 기법의 핵심 같다. headline&amp;middot;summary로 검색 매칭률을 올리고, original_text로 답변 신뢰성을 지키는 역할 분리. 회사 RAG 기반 에이전트에도, 청크에 부가 정보를 LLM이 만들어 붙여 검색 품질을 끌어올리는 형태로 시도해볼 만한 패턴이다.&lt;/li&gt;
&lt;li&gt;&quot;시맨틱 청킹&quot;이라는 단어는 회사에서도 자주 들었는데, 직접 작게나마 구현해보니까 &quot;왜 길이 기반 청킹보다 좋은지&quot;가 감이 잡혔다. 길이로 자르면 검색 시 어떤 청크가 잡혀도 의미가 끊겨 있지 않은지 보장이 안 되는데, LLM이 자르면 적어도 한 청크 안에서는 의미가 덜 끊기도록 만들 수 있는게 장점인거 같다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;LLM 청킹은 비용이 들고 느리다는 단점이 있다. 큰 KB를 한 번에 청킹하기엔 부담이 크다. 근데 임베딩이나 청킹은 한 번 만들어두면 계속 쓰는 자산이라(물론 업뎃은 필요), 청킹 단계의 비용&amp;middot;시간보다는 검색 품질이 더 중요한 경우가 많다. 회사 RAG에서도 운영 트래픽이 아닌 &quot;초기 인덱싱&quot; 단계의 비용이라 감수할 만한 트레이드오프로 보인다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;background-color: #99cefa;&quot;&gt;3. 개선 ② - Reranking&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LLM 청킹으로 청크 품질을 올렸으니, 이번엔&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;검색 단계 자체에 손을 댄다.&lt;/b&gt;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;1) 왜 reranking이 필요한가&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;벡터 검색의 작동 원리는 단순하다. 질문을 임베딩하고, KB 안의 청크 임베딩과 코사인 유사도를 계산해서 가까운 순서대로 가져온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 &quot;임베딩 공간에서 가까운 것&quot;과 &quot;질문에 진짜 답이 되는 것&quot;이 항상 일치하지는 않는다는 점이다.&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;같은 키워드가 들어갔다는 이유로 무관한 청크가 끌려올 수도 있다&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해결의 방향은 검색을&lt;span&gt;&amp;nbsp;&lt;/span&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;1단계 - 1차 검색 : recall 우선. top-K를 넉넉히(10-20개) 가져와서 정답 후보가 컨텍스트에 들어올 가능성을 최대한 높인다.&lt;/li&gt;
&lt;li&gt;2단계 - reranking : precision 우선. 1차 결과를 LLM이 보면서 질문에 가장 잘 맞는 순서로 다시 정렬한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;벡터 검색은 의미 유사도를 잘 잡아주지만 질문의 의도까지 정밀하게 읽지는 못하고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LLM은 질문 의도를 잘 읽지만 KB 전체를 훑기엔 너무 비싸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 단계로 쪼개면 각자 잘 하는 걸 시킬 수 있다.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;2) RankOrder Pydantic 모델과 rerank 코드&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 LLM이 반환할 형식을 Pydantic으로 정의한다.&lt;/p&gt;
&lt;pre style=&quot;background-color: #f8f8f8; color: #383a42;&quot;&gt;&lt;code&gt;class RankOrder(BaseModel):
    order: list[int] = Field(
        description=&quot;The order of relevance of chunks, from most relevant to least relevant, by chunk id number&quot;
    )
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LLM이 청크를 다시 정렬한 결과를 &quot;ID 리스트&quot;로 받는다. 청크 본문을 다시 출력하게 하면 토큰을 너무 많이 쓰니까 ID만 받는 거다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;rerank 함수는 다음과 같다.&lt;/p&gt;
&lt;pre style=&quot;background-color: #f8f8f8; color: #383a42;&quot;&gt;&lt;code&gt;def rerank(question, chunks):
    system_prompt = &quot;&quot;&quot;
You are a document re-ranker.
You are provided with a question and a list of relevant chunks of text from a query of a knowledge base.
The chunks are provided in the order they were retrieved; this should be approximately ordered by relevance, but you may be able to improve on that.
You must rank order the provided chunks by relevance to the question, with the most relevant chunk first.
Reply only with the list of ranked chunk ids, nothing else. Include all the chunk ids you are provided with, reranked.
&quot;&quot;&quot;
    user_prompt = f&quot;The user has asked the following question:\n\n{question}\n\nOrder all the chunks of text by relevance to the question, from most relevant to least relevant. Include all the chunk ids you are provided with, reranked.\n\n&quot;
    user_prompt += &quot;Here are the chunks:\n\n&quot;
    for index, chunk in enumerate(chunks):
        user_prompt += f&quot;# CHUNK ID: {index + 1}:\n\n{chunk.page_content}\n\n&quot;
    user_prompt += &quot;Reply only with the list of ranked chunk ids, nothing else.&quot;
    messages = [
        {&quot;role&quot;: &quot;system&quot;, &quot;content&quot;: system_prompt},
        {&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: user_prompt},
    ]
    response = completion(model=MODEL, messages=messages, response_format=RankOrder)
    reply = response.choices[0].message.content
    order = RankOrder.model_validate_json(reply).order
    return [chunks[i - 1] for i in order]
&lt;/code&gt;&lt;/pre&gt;
&lt;p 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;system 프롬프트로 &quot;넌 reranker야, ID 리스트만 반환해라&quot; 역할 부여.&lt;/li&gt;
&lt;li&gt;user 프롬프트에 질문과 청크들을 ID(1, 2, 3...)와 함께 나열.&lt;/li&gt;
&lt;li&gt;LLM이 RankOrder 스키마로 ID 리스트 반환.&lt;/li&gt;
&lt;li&gt;그 ID 순서대로 chunks를 재배열해서 반환.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서도 structured output(response_format=RankOrder)이 핵심이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LLM이 자유 텍스트로 &quot;1번이 제일 좋고, 그 다음은 5번이고...&quot; 식으로 답하면 파싱이 골치 아픈데, ID 리스트로 강제하면 한 줄에 처리된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;벡터 검색에서 1차로 청크를 가져오는 함수도 짚고 가자.&lt;/p&gt;
&lt;pre style=&quot;background-color: #f8f8f8; color: #383a42;&quot;&gt;&lt;code&gt;RETRIEVAL_K = 10

def fetch_context_unranked(question):
    query = openai.embeddings.create(model=embedding_model, input=[question]).data[0].embedding
    results = collection.query(query_embeddings=[query], n_results=RETRIEVAL_K)
    chunks = []
    for result in zip(results[&quot;documents&quot;][0], results[&quot;metadatas&quot;][0]):
        chunks.append(Result(page_content=result[0], metadata=result[1]))
    return chunks
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서도 LangChain의 retriever 추상화를 쓰지 않고, ChromaDB 클라이언트를 직접 호출한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RETRIEVAL_K=10이 기본값인데, reranking과 함께 쓸 때는 더 크게(20개 등) 가져와도 좋다.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;3) 효과 검증 - Manchester University 사례&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;reranking이 정말 효과가 있는지 확인하기 위해 의도적으로 까다로운 질문 하나를 던져본다.&lt;/p&gt;
&lt;pre style=&quot;background-color: #f8f8f8; color: #383a42;&quot;&gt;&lt;code&gt;question = &quot;Who went to Manchester University?&quot;
RETRIEVAL_K = 20
chunks = fetch_context_unranked(question)
for index, c in enumerate(chunks):
    if &quot;manchester&quot; in c.page_content.lower():
        print(index)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출력:&lt;/p&gt;
&lt;pre style=&quot;background-color: #f8f8f8; color: #383a42;&quot;&gt;&lt;code&gt;6
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 1차 검색 결과 20개 중에서 &quot;manchester&quot;라는 단어가 들어 있는 청크가 7번째(인덱스 6)에 있다. 즉 질문의 답에 직접 닿는 청크가 위쪽이 아니라 중간쯤에 있는 거다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이걸 reranking에 통과시키면 어떻게 되는지 보자.&lt;/p&gt;
&lt;pre style=&quot;background-color: #f8f8f8; color: #383a42;&quot;&gt;&lt;code&gt;reranked = rerank(question, chunks)
for index, c in enumerate(reranked):
    if &quot;manchester&quot; in c.page_content.lower():
        print(index)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출력:&lt;/p&gt;
&lt;pre style=&quot;background-color: #f8f8f8; color: #383a42;&quot;&gt;&lt;code&gt;0
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 0번.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;manchester가 들어 있는 청크가 가장 위로 올라왔다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LLM이 청크들을 보면서 &quot;아, 이 질문은 manchester university를 물어본 거니까 manchester가 들어 있는 청크가 1번이지&quot; 라고 판단해서 재정렬해준 거다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;벡터 유사도만으로는 못 잡았던 의도를, LLM은 청크 내용을 직접 읽고 잡아낸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;4) 1차 검색 + Reranking 한 함수로 합치기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 1차 검색과 reranking을 한 흐름으로 묶는다.&lt;/p&gt;
&lt;pre style=&quot;background-color: #f8f8f8; color: #383a42;&quot;&gt;&lt;code&gt;def fetch_context(question):
    chunks = fetch_context_unranked(question)
    return rerank(question, chunks)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 RAG 파이프라인에서는 그냥 fetch_context만 호출하면 1차 검색 + reranking이 한 번에 처리된다.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;5) 느낀 점&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;1차 검색의 top-K를 작게(3-5개) 두면 reranking 효과가 거의 없다. 1차 결과 안에 정답 후보가 충분히 들어와야 LLM이 그걸 위로 올릴 수 있는데, top-K가 작으면 후보 자체가 적어서 재정렬할 게 없다. 일부러 크게(10-20개) 가져오고 LLM이 거르는 구조가 정석이다. recall과 precision의 역할 분담이 명확해지는 지점이다.&lt;/li&gt;
&lt;li&gt;비용은 추가되지만 응답 정확도 체감 차이가 크다. 특히 이름&amp;middot;고유명사&amp;middot;동일 키워드가 여러 곳에 등장하는 KB에서 reranking이 강력하다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;reranking 자체도 LLM 호출이라 latency가 추가된다. 실시간 챗봇이라면 이 latency가 서비스 응답시간에 영향을 줄 수 있어, &quot;어떤 질문에 reranking을 적용할지&quot;를 게이트웨이로 결정하는 등의 최적화가 추가로 필요해 보인다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;background-color: #99cefa;&quot;&gt;4. 개선 ③ - Query Rewriting&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;검색 품질의 시작점은 &quot;좋은 검색 쿼리&quot;다. 사용자가 던진 질문을 그대로 검색에 쓰는 게 늘 최선은 아니다.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;1) 왜 필요한가&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자 질문은 보통 두 가지 형태로 검색에 부적합하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫째,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;자연어 질문은 검색에 최적화되어 있지 않다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;사람은 &quot;근데 그 IIOTY라는 상은 누가 받았어?&quot; 같이 말하는데,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;임베딩 검색에는 &quot;IIOTY award winner&quot; 같은 짧고 명확한 키워드 형태가 더 잘 맞는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘째,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;후속 질문은 단독으로 의미가 안 통한다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;멀티턴 대화에서 사용자가 &quot;그 사람은 어떤 부서에서 일해?&quot; 라고 던지면, &quot;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 사람&quot;이 누구인지 이전 대화 맥락을 봐야 안다. 후속 질문을 그대로 검색하면 답을 못 찾는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해결책은 LLM에게&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;질문을 검색 쿼리로 다시 쓰게 시키는 것이다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대화 히스토리를 같이 던져 주면, LLM이 맥락을 보고 짧고 구체적인 검색 쿼리로 변환한다.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;2) rewrite_query 코드&lt;/h4&gt;
&lt;pre style=&quot;background-color: #f8f8f8; color: #383a42;&quot;&gt;&lt;code&gt;def rewrite_query(question, history=[]):
    &quot;&quot;&quot;Rewrite the user's question to be a more specific question that is more likely to surface relevant content in the Knowledge Base.&quot;&quot;&quot;
    message = f&quot;&quot;&quot;
You are in a conversation with a user, answering questions about the company Insurellm.
You are about to look up information in a Knowledge Base to answer the user's question.

This is the history of your conversation so far with the user:
{history}

And this is the user's current question:
{question}

Respond only with a single, refined question that you will use to search the Knowledge Base.
It should be a VERY short specific question most likely to surface content. Focus on the question details.
Don't mention the company name unless it's a general question about the company.
IMPORTANT: Respond ONLY with the knowledgebase query, nothing else.
&quot;&quot;&quot;
    response = completion(model=MODEL, messages=[{&quot;role&quot;: &quot;system&quot;, &quot;content&quot;: message}])
    return response.choices[0].message.content
&lt;/code&gt;&lt;/pre&gt;
&lt;p 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;대화 히스토리(history)를 같이 넣어 후속 질문을 처리할 수 있게 한다.&lt;/li&gt;
&lt;li&gt;&quot;VERY short specific question&quot; 명시: 짧을수록 검색에 잘 잡힌다.&lt;/li&gt;
&lt;li&gt;&quot;Don't mention the company name&quot; : 모든 청크가 같은 회사(Insurellm) 관련이라 회사명을 매번 넣으면 의미 없는 토큰만 늘어난다.&lt;/li&gt;
&lt;li&gt;&quot;Respond ONLY with the knowledgebase query&quot; : LLM이 &quot;여기 쿼리입니다: ...&quot; 같은 부연 설명을 못 붙이게 강제.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;3) 동작 예시&lt;/h4&gt;
&lt;pre style=&quot;background-color: #f8f8f8; color: #383a42;&quot;&gt;&lt;code&gt;rewrite_query(&quot;Who won the IIOTY award?&quot;, [])
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 히스토리가 없는 단독 질문도 검색에 더 친화적인 짧은 형태로 다듬어진다.&lt;/p&gt;
&lt;p 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;User: &quot;Who won the IIOTY award in 2023?&quot;&lt;/li&gt;
&lt;li&gt;Bot: &quot;Maxine Thompson won the IIOTY award in 2023.&quot;&lt;/li&gt;
&lt;li&gt;User: &quot;What department does she work in?&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막 질문 &quot;What department does she work in?&quot;을 그대로 검색하면 &quot;she&quot;가 누구인지 알 수 없으니 답을 못 찾는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 rewrite_query가 히스토리를 보면, 직전 답변에 &quot;Maxine Thompson&quot;이 등장했다는 걸 인식해서 검색 쿼리를&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;Maxine Thompson department&quot; 같은 형태로 다시 써준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 변환된 쿼리로 검색하면 정확한 청크가 잡힌다.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;4) 느낀 점&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;검색 품질의 시작은 &quot;좋은 쿼리&quot;. 사용자 입력을 LLM이 한 번 다듬는 단계가 작지만 큰 차이를 만든다.&lt;br /&gt;RAG의 다른 모든 개선이 좋아져도 검색 쿼리가 부실하면 효과가 반감된다.&lt;/li&gt;
&lt;li&gt;회사에서 멀티턴 대화 에이전트를 만들 때, 단순히 &quot;이전 대화를 메모리로 저장&quot;만 신경 썼었다. 근데 그 메모리를 검색 쿼리로 어떻게 압축&amp;middot;재구성할지(즉 query rewriting 단계)까지 같이 설계해야 멀티턴 RAG가 자연스러워진다는 걸 느꼈다. 메모리는 저장이고, query rewriting은 그 메모리를 검색용으로 가공하는 단계다.&lt;/li&gt;
&lt;li&gt;rewrite_query 자체도 LLM 호출이라 latency가 한 단계 추가된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;background-color: #99cefa;&quot;&gt;5. 최종 파이프라인 - 모두 합치기&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 구현한 LLM 청킹, Reranking, Query Rewriting을 하나의 RAG 파이프라인으로 묶는다.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;1) answer_question - 전체 흐름&lt;/h4&gt;
&lt;pre style=&quot;background-color: #f8f8f8; color: #383a42;&quot;&gt;&lt;code&gt;def answer_question(question: str, history: list[dict] = []) -&amp;gt; tuple[str, list]:
    &quot;&quot;&quot;
    Answer a question using RAG and return the answer and the retrieved context
    &quot;&quot;&quot;
    query = rewrite_query(question, history)
    chunks = fetch_context(query)
    messages = make_rag_messages(question, history, chunks)
    response = completion(model=MODEL, messages=messages)
    return response.choices[0].message.content, chunks
&lt;/code&gt;&lt;/pre&gt;
&lt;p 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;rewrite_query&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;: 사용자 질문 + 히스토리 &amp;rarr; KB 검색용으로 다듬은 쿼리.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;fetch_context&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;: 1차 벡터 검색(top-10) + LLM reranking &amp;rarr; 정렬된 청크 리스트.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;make_rag_messages&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;: 시스템 프롬프트에 청크들을 컨텍스트로 주입한 뒤 message 리스트 생성.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;completion&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;: LLM이 컨텍스트를 보고 최종 답변 생성.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 글의 LangChain RAG 체인이 한 줄(chain.invoke)로 끝났던 것에 비하면 단계가 늘어 보이지만,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 단계가 명확히 분리되어 있어서 어디서 문제가 나는지 추적하기가 쉽다.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;2) 시스템 프롬프트 설계&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LLM이 답변을 만들 때 쓰는 시스템 프롬프트는 다음과 같다.&lt;/p&gt;
&lt;pre style=&quot;background-color: #f8f8f8; color: #383a42;&quot;&gt;&lt;code&gt;SYSTEM_PROMPT = &quot;&quot;&quot;
You are a knowledgeable, friendly assistant representing the company Insurellm.
You are chatting with a user about Insurellm.
Your answer will be evaluated for accuracy, relevance and completeness, so make sure it only answers the question and fully answers it.
If you don't know the answer, say so.
For context, here are specific extracts from the Knowledge Base that might be directly relevant to the user's question:
{context}

With this context, please answer the user's question. Be accurate, relevant and complete.
&quot;&quot;&quot;


def make_rag_messages(question, history, chunks):
    context = &quot;\n\n&quot;.join(f&quot;Extract from {chunk.metadata['source']}:\n{chunk.page_content}&quot; for chunk in chunks)
    system_prompt = SYSTEM_PROMPT.format(context=context)
    return [{&quot;role&quot;: &quot;system&quot;, &quot;content&quot;: system_prompt}] + history + [{&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: question}]
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 가지 디자인 포인트가 인상적이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫째,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;답변이 어떻게 평가될지를 모델에게 미리 알려준다.&lt;/b&gt;&lt;/p&gt;
&lt;blockquote style=&quot;color: #666666;&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p style=&quot;color: #666666;&quot; data-ke-size=&quot;size16&quot;&gt;&quot;Your answer will be evaluated for accuracy, relevance and completeness.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이게 Day 4에서 LLM-as-a-Judge가 사용한 평가 차원과 정확히 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LLM에게 &quot;어떤 기준으로 채점될지&quot;를 미리 알려주면 그 기준에 맞춰 답하려고 노력하게 된다. 메타 의식을 부여하는 셈이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘째,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;각 청크 앞에 출처(source)를 명시한다.&lt;/b&gt;&lt;/p&gt;
&lt;pre style=&quot;background-color: #f8f8f8; color: #383a42;&quot;&gt;&lt;code&gt;Extract from knowledge-base/employees/Maxine_Thompson.md:
[청크 본문]
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 출처를 같이 넣으면 LLM이 답변할 때 &quot;어느 문서에서 봤는지&quot;를 인지하고 인용 정확도가 올라간다.&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;3) before / after 비교&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 테스트셋을 기본 RAG (이전 글의 LangChain RAG)와 Advanced RAG (지금까지 구현한 파이프라인)로 각각 돌려서 점수를 비교한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(이미지 - 기본 버전 점수)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(이미지 - Advanced 버전 점수)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oC5l3/dJMcaiiXa2z/VrVP9YjmAXm2tHsW3VMck1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oC5l3/dJMcaiiXa2z/VrVP9YjmAXm2tHsW3VMck1/img.png&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;1179&quot; data-is-animation=&quot;false&quot; style=&quot;width: 50.1192%; margin-right: 10px;&quot; data-widthpercent=&quot;50.71&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oC5l3/dJMcaiiXa2z/VrVP9YjmAXm2tHsW3VMck1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoC5l3%2FdJMcaiiXa2z%2FVrVP9YjmAXm2tHsW3VMck1%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;2048&quot; height=&quot;1179&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Mvnlt/dJMcacXldAp/bkbbemcqVNLXWxsMlBNtjK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Mvnlt/dJMcacXldAp/bkbbemcqVNLXWxsMlBNtjK/img.png&quot; data-origin-width=&quot;2526&quot; data-origin-height=&quot;1496&quot; data-is-animation=&quot;false&quot; style=&quot;width: 48.718%;&quot; data-widthpercent=&quot;49.29&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Mvnlt/dJMcacXldAp/bkbbemcqVNLXWxsMlBNtjK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMvnlt%2FdJMcacXldAp%2FbkbbemcqVNLXWxsMlBNtjK%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;2526&quot; height=&quot;1496&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p 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;검색 메트릭이 전반적으로 올랐다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;MRR 0.76 &amp;rarr; 0.88, nDCG 0.77 &amp;rarr; 0.85. 특히 약점이었던 카테고리들에서 큰 개선이 나왔다 (spanning 0.45 &amp;rarr; 0.75, comparative 0.67 &amp;rarr; 0.97).&lt;/li&gt;
&lt;li&gt;LLM 청킹으로 청크에 headline&amp;middot;summary 메타텍스트가 붙으면서 다중 청크 질문도 검색 매칭이 잘 되는 것으로 해석할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;답변 메트릭은 결과가 살짝 섞여 있다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;평균은 다 올랐고(Accuracy 4.15 &amp;rarr; 4.40, Completeness 4.03 &amp;rarr; 4.19),&lt;/li&gt;
&lt;li&gt;특히 holistic 카테고리 정확도가 2.6 &amp;rarr; 3.3으로 의미 있게 올라갔다.&lt;/li&gt;
&lt;li&gt;풀네임 누락 같은 completeness 문제가 메타텍스트 덕에 완화된 결과로 보인다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;흥미로운 발견 하나.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;spanning은 검색 MRR이 가장 크게 올랐는데(+0.30), 답변 Accuracy는 오히려 떨어졌다(4.25 &amp;rarr; 3.6).&lt;/li&gt;
&lt;li&gt;검색이 정답 자료는 잘 잡아오기 시작했지만, 여러 청크의 정보를 LLM이 통합해 답을 합성하는 단계에서 여전히 약하다는 뜻이다.&lt;/li&gt;
&lt;li&gt;검색 개선만으로 안 풀리는 약점은 hierarchical RAG나 단계적 추론 프롬프트 같은 추가 기법이 필요해 보인다 &amp;mdash; 평가 시스템이 있어야 보이는 디테일이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;4) 더 가볼 수 있는 길&lt;/h4&gt;
&lt;p 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;점수가 일정 수준 이상 나올 때까지 자동 재시도&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;: 답변 평가 점수가 임계치 이하면 다른 청크 조합 / 다른 프롬프트로 다시 시도하는 루프.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;외부 데이터 소스 연결&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;: Google Docs, Confluence, Notion 같은 협업 도구를 KB로 연결하면 회사 RAG로 확장 가능.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;에이전틱 RAG로 진화&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;: 매 질문마다 &quot;벡터 검색을 할까, SQL 쿼리를 날릴까, API를 호출할까&quot;를 LLM이 결정하는 구조. (표의 10번)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;5) 느낀 점&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;세 가지 개선이 곱셈 효과로 작용한다는 점이 인상적이다. 하나만 적용하면 효과가 작은데 셋을 다 적용하면 체감 차이가 크다. 측정 &amp;rarr; 개선 &amp;rarr; 재측정의 루프를 한 사이클 돌고 나니, &quot;RAG는 한 번에 완성하는 게 아니라 평가 점수에 따라 반복적으로 다듬는 시스템이구나&quot; 하는 감이 잡혔다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;background-color: #99cefa;&quot;&gt;6. 마무리&lt;/span&gt;&lt;/h3&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;1) 이번 글 요약&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번&amp;nbsp;글에서는&amp;nbsp;지난&amp;nbsp;평가편에서&amp;nbsp;드러난&amp;nbsp;한계를&amp;nbsp;Advanced&amp;nbsp;RAG&amp;nbsp;기법으로&amp;nbsp;풀어봤다.&lt;br /&gt;&lt;br /&gt;- LLM 기반 지능형 청킹 (시맨틱 청킹 + 문서 전처리 결합) : 청크에 headline / summary / original_text 필드를 부여해 검색 매칭과 답변 신뢰성을 동시에 잡았다.&lt;br /&gt;-&amp;nbsp;Reranking&amp;nbsp;:&amp;nbsp;1차&amp;nbsp;벡터&amp;nbsp;검색&amp;nbsp;결과를&amp;nbsp;LLM이&amp;nbsp;다시&amp;nbsp;정렬해서&amp;nbsp;정답&amp;nbsp;후보를&amp;nbsp;위로&amp;nbsp;올렸다.&lt;br /&gt;-&amp;nbsp;Query&amp;nbsp;Rewriting&amp;nbsp;:&amp;nbsp;사용자&amp;nbsp;질문을&amp;nbsp;KB&amp;nbsp;검색&amp;nbsp;친화적인&amp;nbsp;짧은&amp;nbsp;쿼리로&amp;nbsp;변환했다.&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;세 가지 개선을 바탕으로, 평가편에서 약했던 spanning 카테고리와 completeness 점수가 의미 있게 올라가는 걸 확인할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-size: 1.25em; letter-spacing: -1px; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif;&quot;&gt;2) Week 5 시리즈 전체 회고&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5주 동안의 흐름을 돌아보면 이렇다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Day 1~3 : 단순 키워드 매칭 &amp;rarr; 벡터 임베딩 &amp;rarr; LangChain 기반 RAG 구축.&lt;/li&gt;
&lt;li&gt;Day 4 : 평가 메트릭 도입. RAG의 한계를 정량화.&lt;/li&gt;
&lt;li&gt;Day 5 : 평가 결과를 보고 Advanced RAG 기법 적용.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5주차의 키워드를 한 단어로 줄이면 &quot;측정&quot;이다. &quot;그럴듯한 RAG&quot;에서 &quot;측정 가능한 RAG&quot;로 옮겨가는 과정이었다고 정리할 수 있다.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;3) 다음 시리즈 예고&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 주차부터는 잠시 RAG에서 벗어나,&lt;span&gt;&amp;nbsp;&lt;/span&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;Week 6 : 프론티어 모델 파인튜닝&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash; &quot;THE PRICE IS RIGHT&quot; 캡스톤 프로젝트. 데이터 큐레이션, 전처리, 전통 ML 베이스라인을 거쳐 프론티어 모델 fine-tuning까지.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Week 7 : 오픈소스 모델 파인튜닝&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash; QLoRA로 직접 fine-tuning, prompt data 구성, train/eval.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;4) 마치며&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5주차는 개인적으로 가장 실무에 직접 와닿는 내용이었고,&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;계속 이론적으로 알고는 있었던 RAG를 실제 코드 단에서 구현해보며 더 깊게 파고들어가볼 수 있던게 좋았던 거 같다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히나 RAG는 한번에 완성되는 게 아니고 trial &amp;amp; error가 필요하다고 느꼈다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;뿐만 아니라, RAG 평가 프레임이나, RAG성능을 올리기 위한 Advanced RAG 기법 등이 회사에서 에이전트를 만드는 과정에서도&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고할 만한 것들이 꽤 있었던 거 같아 의미 있었다.&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;(꾸준히 공부하고&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span data-grammar-focus=&quot;false&quot; data-grammar-id=&quot;grammar92&quot; data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;적을테니&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;적을 테니&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;space&amp;quot;}&quot;&gt;적을테니&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;많은 관심 부탁드립니다.)&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;Profile:&lt;/span&gt;&lt;br /&gt;&lt;a style=&quot;color: #0070d1; text-align: start;&quot; href=&quot;http://www.linkedin.com/in/doobeom-kim-%EA%B9%80%EB%91%90%EB%B2%94-2b9649242&quot;&gt;&lt;span&gt;&lt;span style=&quot;color: #0070d1;&quot;&gt;Linkedin&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>AI 공부</category>
      <category>advanced rag</category>
      <category>ai</category>
      <category>AI 공부</category>
      <category>llm</category>
      <category>Query Rewriting</category>
      <category>Rag</category>
      <category>semantic chunking</category>
      <category>벡터 db</category>
      <category>생성형 AI</category>
      <category>임베딩</category>
      <author>kdb1248</author>
      <guid isPermaLink="true">https://doobeom-coding.tistory.com/89</guid>
      <comments>https://doobeom-coding.tistory.com/89#entry89comment</comments>
      <pubDate>Sat, 9 May 2026 21:32:05 +0900</pubDate>
    </item>
    <item>
      <title>11. RAG, 잘 만들었는지 어떻게 알지? &amp;mdash; RAG 평가 시스템 구축 (테스트셋 구축 및 검색/답변 평가 진행)</title>
      <link>https://doobeom-coding.tistory.com/88</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;목차&lt;br /&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;0. 들어가며&lt;br /&gt;1. 평가가 전부다 (Evaluations are Everything)&lt;br /&gt;2. 골든 테스트셋 만들기&lt;br /&gt;3. 검색 평가 메트릭 (Retrieval Evaluation)&lt;br /&gt;4. 답변 평가 메트릭 (LLM-as-a-Judge)&lt;br /&gt;5. 평가 결과로 본 한계&lt;br /&gt;6.&amp;nbsp;1편&amp;nbsp;마치며&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;background-color: #99cefa;&quot;&gt;0. 들어가며&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://doobeom-coding.tistory.com/87&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;지난 글&lt;/a&gt;에서 가상의 보험회사의 사내문서를 기반으로 답변을 할 수 있는 Rag챗봇을&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순 키워드 매칭 방식부터 LangChain 기반 RAG까지 만들어봤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 과정에서 마지막에 한계를 볼 수 있었다. &lt;br /&gt;(복합질문에 답변이 어려운 것, 각 청크에 해당 인물의 풀네임이 적혀있진 않아서 답변에서도 풀네임을 갖고 오지 못한 것 등)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글은 이런 RAG의 한계를 극복하기 위한 방법을 고민해나가는 글이고 1가지 질문에서 출발한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;근데 RAG가 잘 만들어졌는지 어떻게 알지?&quot;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 위해 아래 2가지 내용을 중심으로 작성하려고 한다.&lt;br /&gt;(1) RAG 구성이 잘 됐는지 측정할 수 있는 RAG 평가 시스템&lt;br /&gt;(2) 평가 결과를 본 뒤, 기존 단순 RAG의 한계를 극복하기 위한 Advanced RAG 활용 및 성능 향상&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #fdfdfc; color: #6e6c68; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;분량이 길어져 (1) 평가편과 (2) Advanced RAG편으로 글을 나눠서 다루려고 한다. 이번 글은 첫 번째인 평가편이다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;background-color: #99cefa;&quot;&gt;1. 평가가 전부다&amp;nbsp;&lt;/span&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1) 왜 평가 없이는 개선이 안 되는가&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RAG를 어떻게 좋게 만들어볼까? 라는 질문에 사람들이 흔히 떠올리는 답은 이런 식이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;chunk size를 좀 줄여볼까?&lt;/li&gt;
&lt;li&gt;임베딩 모델을 더 좋은 걸로 바꿔볼까?&lt;/li&gt;
&lt;li&gt;top-k를 늘려볼까?&lt;/li&gt;
&lt;li&gt;reranker를 붙여볼까?&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 시도들이 의미 없는 건 아니다. 진짜 문제는, 이렇게 바꿨을 때 좋아졌는지 나빠졌는지를 객관적으로 모른다는 점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트셋이 없으면 &quot;어 이번엔 좀 잘 답하는 것 같은데?&quot; 같은 느낌으로 판단하게 되고, RAG 튜닝이 감으로 흘러간다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강의에서 가장 강하게 강조한 메시지가 이거였다. &quot;Evaluations are everything.&quot; (평가가 전부다)&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2) 3단계 평가 프레임&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;평가는 막연히 &quot;잘 답하는지 본다&quot;가 아니라 다음 3단계로 분리해서 봐야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 1단계 - 테스트셋 큐레이션 : 평가의 기준이 될 골든 데이터셋을 만든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 2단계 - 검색 평가 (Retrieval Evaluation) : 질문이 들어왔을 때 RAG가 관련 있는 청크를 잘 찾아오는가?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 3단계 - 답변 평가 (Answer Evaluation) : 가져온 청크들로 LLM이 실제로 좋은 답변을 만들어내는가?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이걸 한 덩어리로 보면 안 되는 이유가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RAG가 답을 못할 때 원인이 두 군데에서 올 수 있기 때문이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;검색이 못 잡았다 &amp;rarr; 아무리 LLM이 좋아도 답할 수가 없다.&lt;/li&gt;
&lt;li&gt;검색은 잘 됐는데 LLM이 답을 못 만들었다 &amp;rarr; 프롬프트나 생성 쪽 문제다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;평가를 분리해야 어디를 손봐야 할지 알 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;검색이 70% 정확한데 답변이 50%라면 검색이 아니라 프롬프트나 컨텍스트 구성 쪽을 봐야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 검색이 30%인데 답변 점수도 30%면 검색부터 고쳐야 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;991&quot; data-origin-height=&quot;555&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9pVp0/dJMcahxBGdM/SHKwy0wD743282TKXY6VH0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9pVp0/dJMcahxBGdM/SHKwy0wD743282TKXY6VH0/img.png&quot; data-alt=&quot;강의의 3단계 평가 프레임 도식&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9pVp0/dJMcahxBGdM/SHKwy0wD743282TKXY6VH0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9pVp0%2FdJMcahxBGdM%2FSHKwy0wD743282TKXY6VH0%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;500&quot; height=&quot;280&quot; data-origin-width=&quot;991&quot; data-origin-height=&quot;555&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;강의의 3단계 평가 프레임 도식&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-size: 1.25em; letter-spacing: -1px; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif;&quot;&gt;3) 느낀 점&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;단순히 청크사이즈를 바꾸고, 임베딩 모델을 바꾸고 등의 시도를 하는 것이 아니라 평가 체계를 잘 만들어야 한다는 점이 인상적이었다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;그 과정에서 테스트셋과 메트릭이 먼저 만들어져야 한다는 점&lt;/li&gt;
&lt;li&gt;회사에서도 RAG 기반 에이전트를 만들고 있는데, 이번 강의를 보면서 결국 평가 데이터셋과 측정 메트릭을 먼저 잡고, 코드나 아키텍처상 변경점이 있을 때마다 때마다 점수로 비교하면서 지속적으로 버저닝하는 게 중요하다는 걸 다시 느꼈다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;background-color: #99cefa;&quot;&gt;2. 골든 테스트셋 만들기&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;평가의 출발점은 테스트셋이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RAG에게 물어볼 질문과, 그 질문에 대한 정답이 무엇인지를 미리 정의해둔 데이터셋이다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1) 무엇을 담는가&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 강의에서는 한 테스트 케이스에 다음 4가지를 담는다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;question : RAG에게 물어볼 질문&lt;/li&gt;
&lt;li&gt;keywords : RAG가 검색해 온 청크들 안에 반드시 등장해야 할 단어들. 검색이 정답 자료를 잘 잡아왔는지 자동 판정하기 위한 정답 키 (예: &quot;Maxine&quot;, &quot;Thompson&quot;, &quot;IIOTY&quot;)&lt;/li&gt;
&lt;li&gt;reference_answer : 사람이 작성한 모범 답안 (LLM 심판이 답변 평가할 때 비교 기준으로 사용)&lt;/li&gt;
&lt;li&gt;category : 질문의 유형 (direct_fact, spanning, holistic 등)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 흥미로운 게 keywords와 category 필드다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;background-color: #fdfdfc; text-align: start;&quot;&gt;keywords는&amp;nbsp;&lt;/span&gt;&lt;b&gt;검색 평가에만&lt;/b&gt;&lt;span style=&quot;background-color: #fdfdfc; text-align: start;&quot;&gt;&amp;nbsp;쓰인다. RAG가 검색해 온 청크(retrieved context)의 본문에 이 키워드들이 등장하는지를 검사하면, 검색이 정답에 필요한 자료를 잘 잡아왔는지를 자동으로 판정할 수 있다. 코드에서는&amp;nbsp;&lt;/span&gt;doc.page_content.lower()&lt;span style=&quot;background-color: #fdfdfc; text-align: start;&quot;&gt;에&amp;nbsp;&lt;/span&gt;keyword.lower()&lt;span style=&quot;background-color: #fdfdfc; text-align: start;&quot;&gt;가 포함되는지로 판정한다(대소문자 무시).&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;category는&lt;b&gt; &quot;어떤 유형에서 약한지&quot;&lt;/b&gt;를 진단하는 데 쓰인다. direct_fact 점수는 높은데 spanning(여러 청크에 걸친 질문) 점수가 낮으면, 검색 시스템이 &quot;한 청크 안에 답이 있는 질문&quot;엔 강하지만 &quot;여러 청크를 통합해야 하는 질문&quot;엔 약하다는 뜻이다. 그러면 청킹 전략을 바꾸거나 hierarchical RAG 같은 기법을 검토해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 셋 구축시 또 하나 중요한 팁은, 가능하면 실제 사용자 질문 데이터를 활용해 만드는 것이다. CS 데이터나 챗봇 로그가 있으면 거기서 추리는 게 가장 좋다. 없으면 합성 데이터로 시작하되, 한 번에 완벽하게 만들려고 하지 말고 운영하면서 점진적으로 확장하는 방식을 권장한다. 한 가지 주의할 점은, 테스트셋에 너무 맞춰서 튜닝하면 모델이 그 테스트셋에만 잘 작동하게 될 수 있다는 거다(과적합).&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2) jsonl 포맷&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 강의에서는 테스트셋을 jsonl 포맷으로 저장한다. JSON과 살짝 다르다. 일반적인 JSON은 전체가 하나의 큰 객체나 배열이지만, jsonl은 한 줄에 하나씩 독립된 JSON 객체가 들어간다.&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{&quot;question&quot;: &quot;Who won the IIOTY award in 2023?&quot;, &quot;keywords&quot;: [&quot;Maxine&quot;, &quot;Thompson&quot;, &quot;IIOTY&quot;], &quot;reference_answer&quot;: &quot;Maxine Thompson won...&quot;, &quot;category&quot;: &quot;direct_fact&quot;}
{&quot;question&quot;: &quot;When was Insurellm founded?&quot;, &quot;keywords&quot;: [&quot;2015&quot;], &quot;reference_answer&quot;: &quot;Insurellm was founded in 2015.&quot;, &quot;category&quot;: &quot;temporal&quot;}
&lt;/code&gt;&lt;/pre&gt;
&lt;p 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;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OpenAI Batch API 같은 LLM 배치 호출 인터페이스도 jsonl을 표준으로 쓰는데, 이런 이유 때문이다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3) 코드 - test.py&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 케이스를 코드에서 다루기 위해서 Pydantic 모델을 하나 정의한다.&lt;/p&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;import json
from pathlib import Path
from pydantic import BaseModel, Field

TEST_FILE = str(Path(__file__).parent / &quot;tests.jsonl&quot;)


class TestQuestion(BaseModel):
    &quot;&quot;&quot;A test question with expected keywords and reference answer.&quot;&quot;&quot;

    question: str = Field(description=&quot;The question to ask the RAG system&quot;)
    keywords: list[str] = Field(description=&quot;Keywords that must appear in retrieved context&quot;)
    reference_answer: str = Field(description=&quot;The reference answer for this question&quot;)
    category: str = Field(description=&quot;Question category (e.g., direct_fact, spanning, temporal)&quot;)


def load_tests() -&amp;gt; list[TestQuestion]:
    &quot;&quot;&quot;Load test questions from JSONL file.&quot;&quot;&quot;
    tests = []
    with open(TEST_FILE, &quot;r&quot;, encoding=&quot;utf-8&quot;) as f:
        for line in f:
            data = json.loads(line.strip())
            tests.append(TestQuestion(**data))
    return tests
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 처음 보는 분들을 위해 Pydantic을 잠깐 짚고 넘어가겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Pydantic은 BaseModel을 상속받아서 데이터 모델을 만드는 라이브러리다. 위 코드에서 TestQuestion이 그 예다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반 dict와 비교했을 때 장점이 세 가지 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;타입 검증 : question은 str이어야 하고 keywords는 list[str]이어야 한다는 식의 타입을 강제한다. 잘못된 타입이 들어오면 즉시 에러가 난다.&lt;/li&gt;
&lt;li&gt;데이터 파싱 : TestQuestion(**data)처럼 dict를 그대로 넘기면, Pydantic이 검증하면서 객체로 만들어준다.&lt;/li&gt;
&lt;li&gt;스키마 정보 : Field(description=&quot;...&quot;)가 단순 주석이 아니라 자동 문서화&amp;middot;OpenAI structured output 같은 데에서 LLM 가이드로도 쓰인다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 강의 전반에 걸쳐 Pydantic이 굉장히 자주 등장하는데, 그 이유는 뒤에 답변 평가에서 자세히 다룬다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;load_tests() 함수의 흐름은 단순하다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;tests.jsonl을 한 줄씩 읽는다.&lt;/li&gt;
&lt;li&gt;json.loads로 dict로 만든다.&lt;/li&gt;
&lt;li&gt;TestQuestion(**data)로 Pydantic 검증&amp;middot;객체화한다.&lt;/li&gt;
&lt;li&gt;리스트로 반환한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 만들어진 TestQuestion 리스트가 이후 검색 평가, 답변 평가의 입력이 된다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4) 실제 데이터 살펴보기&lt;/h4&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;from evaluation import test
tests = test.load_tests()
len(tests)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출력:&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;150&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; 테스트셋은 총 150개가 들어 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;example = tests[0]
print(example.question)
print(example.category)
print(example.reference_answer)
print(example.keywords)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출력:&lt;/p&gt;
&lt;pre class=&quot;arcade&quot;&gt;&lt;code&gt;Who won the prestigious IIOTY award in 2023?
direct_fact
Maxine Thompson won the prestigious Insurellm Innovator of the Year (IIOTY) award in 2023.
['Maxine', 'Thompson', 'IIOTY']
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;질문, 카테고리, 정답, 정답에 등장해야 할 키워드 3개가 한 케이스로 묶여 있는 게 보인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;카테고리가 어떻게 분포되어 있는지도 확인해본다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;from collections import Counter
count = Counter([t.category for t in tests])
count
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출력:&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;Counter({
  'direct_fact': 70,
  'temporal': 20,
  'spanning': 20,
  'comparative': 10,
  'numerical': 10,
  'relationship': 10,
  'holistic': 10
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p 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;direct_fact (70개): 한 청크 안에 답이 들어 있는 사실형 질문&lt;/li&gt;
&lt;li&gt;temporal (20개): 시점&amp;middot;기간 관련 질문 (&quot;언제&quot;, &quot;얼마나 오래&quot;)&lt;/li&gt;
&lt;li&gt;spanning (20개): 여러 문서&amp;middot;청크에 걸쳐 답이 흩어져 있는 질문&lt;/li&gt;
&lt;li&gt;comparative (10개): 비교 질문 (&quot;A와 B 중 어느 쪽이 더...&quot;)&lt;/li&gt;
&lt;li&gt;numerical (10개): 숫자 계산이 필요한 질문&lt;/li&gt;
&lt;li&gt;relationship (10개): 관계 기반 질문 (&quot;X와 일한 사람은 누구인가&quot;)&lt;/li&gt;
&lt;li&gt;holistic (10개): 문서 전체를 이해해야 답할 수 있는 질문&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;direct_fact가 절반 가까이 차지하지만, 일부러 다양한 유형이 섞여 있다. 그래야 RAG의 약점을 다각도로 잡아낼 수 있기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;5) 느낀 점&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&quot;정답 발화 예시&quot;만 있으면 된다고 생각했는데 그게 아니었다. 검색 청크에 포함되어야 할 키워드, 그리고 질문 카테고리(유형) 라벨링까지 같이 있어야 메트릭으로 채점이 자동화되고, 취약 카테고리 파악이 가능하다. RAG 기반으로 지식 데이터를 참조하는 에이전트를 만들 때 그대로 차용할 만한 테스트셋 생성 패턴이었다.&lt;/li&gt;
&lt;li&gt;카테고리를 다양하게 둔 이유는 나중에 &quot;어떤 유형에서 약한지&quot; 진단하기 위해서다. 회사에서 만약 CS 에이전트를 만든다고 한다면 &quot;지식 데이터만 참조해서 해결되는 단순 FAQ형&quot;과, &quot;지식데이터 단일 매핑이 아니라 여러 데이터를 참조해야하는 복합 FAQ형&quot;, &quot;내부 시스템&amp;middot;계정 정보 조회가 필요한 액션형&quot; 같은 식으로 카테고리를 분류해서 골고루 분포시키는 설계가 필요해 보인다. 그래야 어떤 유형의 질문에서 약한지를 파악하고, 거기에 맞는 개선책(추가 데이터? 다른 도구 연결? 프롬프트 보강?)을 찾을 수 있다.&lt;/li&gt;
&lt;li&gt;jsonl + Pydantic 조합은 데이터셋의 형식을 코드 레벨에서 강제함으로써, 데이터셋 품질을 지켜주는 점이 인상적이었다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;background-color: #99cefa;&quot;&gt;3. 검색 평가 메트릭 (Retrieval Evaluation)&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트셋이 만들어졌으니 이제 메트릭으로 채점할 차례다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;검색 평가는 다음 질문에 답하기 위한 거다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &quot;RAG가 가져온 청크들 안에 정답에 필요한 자료가 충분히 있는가?&quot;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 보는 각도가 여러 개라서 메트릭도 여러 개를 같이 본다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1) MRR (Mean Reciprocal Rank) - &quot;정답이 검색해온 청크 리스트 중 몇 번째에 처음 나왔나?&quot;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;키워드가 처음 등장한 청크의 순위(rank)를 1/rank로 환산해서 평균낸 값이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;정답이 1등에 잡히면 1.0&lt;/li&gt;
&lt;li&gt;2등이면 0.5&lt;/li&gt;
&lt;li&gt;3등이면 0.33&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;첫 번째 결과의 품질&quot;에 민감하기 때문에, 답이 한 청크 안에 들어 있는 QA형 질문에서 특히 중요하게 본다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;챗봇이 첫 검색 결과만 보고 답하는 구조라면 MRR이 사실상 시스템 정확도를 결정한다고 봐도 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해석은 직관적이다. MRR이 0.5면 평균적으로 정답이 2번째에 잡힌다는 뜻이고, 0.1 이하면 검색이 거의 실패하고 있다는 신호다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2) nDCG (Normalized Discounted Cumulative Gain) - &quot;관련 있는 청크들이 위쪽에 잘 정렬됐나?&quot;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DCG는 각 자리(rank)의 relevance 값에 1/log2(rank+1) 가중치를 붙여 합산한 값이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위쪽 자리일수록 가중치가 크다. nDCG는 이걸 &quot;이상적인 정렬&quot;의 DCG로 나눠서 0~1로 정규화한 값이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드에서는 relevance를 이진값으로 잡았다. 키워드가 청크에 포함되면 1, 아니면 0.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MRR이 &quot;첫 번째에만&quot; 집중한다면 nDCG는 &quot;전체 순위가 얼마나 이상적인가&quot;를 본다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 정답에 필요한 자료가 여러 청크에 흩어져 있을 때, 또는 reranking을 적용한 후 효과를 측정할 때 적합하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해석: 1.0에 가까울수록 이상적 정렬. 낮으면 정답 후보가 뒤쪽으로 흩어져 있다는 뜻.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3) Recall@K / Precision@K&lt;/h4&gt;
&lt;p 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;Recall@K : 상위 K개 청크 안에 정답에 필요한 자료가 한 번이라도 잡혔나? (있다/없다 비율)&lt;/li&gt;
&lt;li&gt;Precision@K : 상위 K개 청크 중 실제 관련 있는 청크의 비율&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RAG에서는 Recall@K가 Precision@K보다 더 중요하게 다뤄진다. 이유는 단순하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LLM이 답을 만들 마지막 기회를 주려면 적어도 자료는 컨텍스트에 들어가 있어야 하기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자료 자체가 안 들어가면 LLM은 답할 도리가 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 K를 무한정 늘리면 Precision이 떨어지고 컨텍스트 윈도우만 잡아먹는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 Recall과 Precision은 trade-off 관계로, K를 어떻게 잡을지가 RAG 튜닝의 한 축이 된다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4) Keyword Coverage - &quot;전체 키워드 중 몇 %를 top-K에 담아냈나&quot;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Recall의 변형이라고 봐도 된다. 한 질문에 정답 키워드가 여러 개일 때 (예: &quot;Maxine&quot;, &quot;Thompson&quot;, &quot;IIOTY&quot;) 그중 몇 개가 top-K 안에 한 번이라도 등장했는지를 비율로 계산한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;100%면 모든 키워드가 잡혔다는 뜻이고, 66%면 3개 중 2개만 잡혔다는 뜻이다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;5) 코드 - eval.py&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 메트릭들을 실제로 계산하는 코드는 evaluation/eval.py에 들어 있다. 핵심만 떼어 놓으면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;def calculate_mrr(keyword: str, retrieved_docs: list) -&amp;gt; float:
    &quot;&quot;&quot;Calculate reciprocal rank for a single keyword (case-insensitive).&quot;&quot;&quot;
    keyword_lower = keyword.lower()
    for rank, doc in enumerate(retrieved_docs, start=1):
        if keyword_lower in doc.page_content.lower():
            return 1.0 / rank
    return 0.0


def calculate_dcg(relevances: list[int], k: int) -&amp;gt; float:
    &quot;&quot;&quot;Calculate Discounted Cumulative Gain.&quot;&quot;&quot;
    dcg = 0.0
    for i in range(min(k, len(relevances))):
        dcg += relevances[i] / math.log2(i + 2)  # i+2 because rank starts at 1
    return dcg


def calculate_ndcg(keyword: str, retrieved_docs: list, k: int = 10) -&amp;gt; float:
    &quot;&quot;&quot;Calculate nDCG for a single keyword (binary relevance, case-insensitive).&quot;&quot;&quot;
    keyword_lower = keyword.lower()

    relevances = [
        1 if keyword_lower in doc.page_content.lower() else 0 for doc in retrieved_docs[:k]
    ]

    dcg = calculate_dcg(relevances, k)

    ideal_relevances = sorted(relevances, reverse=True)
    idcg = calculate_dcg(ideal_relevances, k)

    return dcg / idcg if idcg &amp;gt; 0 else 0.0
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- calculate_mrr은 키워드별로 rank를 찾아 1/rank를 반환한다. 키워드가 어떤 청크에도 없으면 0.0.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- calculate_dcg는 i+2 인덱스를 쓰는 게 살짝 헷갈릴 수 있는데, rank가 1부터 시작하기 때문에 i=0에 대해 log2(2)=1, i=1에 대해 log2(3) ... 이런 식으로 가중치가 떨어지는 구조다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- calculate_ndcg는 위 두 함수를 활용해서 실제 DCG를 이상적 DCG로 나눠 정규화한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 키워드별로 점수를 낸 뒤, evaluate_retrieval에서는 한 질문 안의 키워드 점수들을 평균내서 RetrievalEval이라는 결과 객체로 묶어 반환한다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;class RetrievalEval(BaseModel):
    &quot;&quot;&quot;Evaluation metrics for retrieval performance.&quot;&quot;&quot;

    mrr: float = Field(description=&quot;Mean Reciprocal Rank - average across all keywords&quot;)
    ndcg: float = Field(description=&quot;Normalized Discounted Cumulative Gain (binary relevance)&quot;)
    keywords_found: int = Field(description=&quot;Number of keywords found in top-k results&quot;)
    total_keywords: int = Field(description=&quot;Total number of keywords to find&quot;)
    keyword_coverage: float = Field(description=&quot;Percentage of keywords found&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 등장하는 RetrievalEval도 Pydantic 모델이지만, 앞에서 본 TestQuestion과는 역할이 살짝 다르다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TestQuestion은 외부 jsonl 데이터를 검증하는 입력 스키마였다면, RetrievalEval은 코드가 직접 계산한 결과를 담는 그릇(DTO) 역할이다. LLM과는 무관하다. 이 차이는 다음 섹션의 답변 평가에서 LLM의 출력을 강제하는 용도로 쓰일 때 더 분명해진다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;6) 실제로 돌려보기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 케이스로 돌려보면 이런 결과가 나온다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;from evaluation.eval import evaluate_retrieval, evaluate_answer
evaluate_retrieval(example)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출력:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;RetrievalEval(mrr=0.16666666666666666, ndcg=0.28711770538226206, keywords_found=2, total_keywords=3, keyword_coverage=66.66666666666666)
&lt;/code&gt;&lt;/pre&gt;
&lt;p 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;mrr=0.166 &amp;rarr; 키워드들의 평균 reciprocal rank. 키워드별로 보면 어떤 건 6번째에 처음 잡혔고, 어떤 건 아예 못 잡혔을 가능성이 크다.&lt;/li&gt;
&lt;li&gt;ndcg=0.287 &amp;rarr; 정답 키워드 청크가 위쪽에 잘 정렬되어 있다고 보기 어렵다.&lt;/li&gt;
&lt;li&gt;keyword_coverage=66.6% &amp;rarr; 전체 키워드 3개 중 2개만 top-K에 잡혔다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 RAG가 IIOTY 같은 단순해 보이는 질문에서도 검색 품질이 그렇게 좋지 않다는 게 숫자로 드러난다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;7) 느낀 점&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메트릭이 여러 개인 이유는 보는 각도가 다르기 때문이다. 첫 결과 품질(MRR), 정렬 품질(nDCG), 들어왔는지 여부(Recall), 노이즈 비율(Precision). 한두 개만 봐서 결정 내리면 위험하다.&lt;/li&gt;
&lt;li&gt;&quot;RAG에서는 recall이 가장 중요하다&quot;는 말이 와닿았다. 검색이 자료를 안 잡아오면 LLM은 답할 도리가 없다. 컨텍스트에 정답 자료가 들어가는 것이 우선이고, 정렬&amp;middot;노이즈 문제는 다음 단계.&lt;/li&gt;
&lt;li&gt;메트릭은 단순한 지표가 아니라 &quot;어떤 부분이 약한지&quot;를 가리키는 진단 도구다. MRR은 좋은데 nDCG가 나쁘다면 1등은 잘 잡지만 2~3등 정렬이 엉망이라는 뜻이고, MRR/nDCG 다 나쁜데 Coverage만 높다면 자료가 어딘가 들어와 있긴 하지만 위쪽에 안 잡혀 있다는 뜻이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;background-color: #99cefa;&quot;&gt;4. 답변 평가 메트릭 (LLM-as-a-Judge)&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;검색이 잘 됐다고 답이 잘 만들어진다는 보장은 없다. 청크들을 받아 LLM이 실제로 좋은 답을 만들었는지를 따로 평가해야 한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1) 왜 LLM-as-a-Judge 인가&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전통적인 자동 평가 방식으로는 BLEU나 ROUGE 같은 단어 겹침 기반 메트릭이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 RAG 답변 평가에는 한계가 명확하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 정답이 &quot;Maxine Thompson&quot;이고 RAG가 &quot;Maxine&quot;이라고 답했다고 해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의미상 거의 같은 말인데 단어 겹침으로는 50%만 맞은 걸로 처리된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 정답과 단어가 많이 겹쳐도 핵심 사실이 틀려 있을 수도 있다. 단어 단위 비교로는 의미를 못 잡는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 강의에서는 LLM 자체를 평가자로 쓴다. 참고 답안과 RAG 통해 LLM이 만든답을 같이 별도의 평가자용 LLM에 던져 주고, &quot;비교해서 점수 매겨봐&quot;라고 시킨다. LLM이라는 도구가 의미 단위로 비교를 잘 해주는 데다, 사람이 일일이 채점할 수 없는 규모로도 자동화할 수 있다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2) 3가지 차원&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;평가자 LLM에게 다음 3가지 차원으로 점수를 매기게 한다 (1~5).&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Accuracy (정확성) : 사실 정확도. 핵심 사실이 틀렸으면 무조건 1점.&lt;/li&gt;
&lt;li&gt;Completeness (완전성) : 참고 답안의 모든 정보가 빠짐없이 포함됐는지.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;Relevance (관련성) : 질문에 직접 답하고 군더더기 없는지. 장황한 부연 설명도 감점.&lt;/li&gt;
&lt;/ul&gt;
&lt;p 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;Accuracy 낮음 &amp;rarr; LLM이 환각하거나 잘못된 청크를 골라 답한 상태.&lt;/li&gt;
&lt;li&gt;Completeness 낮음 &amp;rarr; 검색이 부분적으로만 잡아왔거나 LLM이 정보를 누락한 상태.&lt;/li&gt;
&lt;li&gt;Relevance 낮음 &amp;rarr; 프롬프트가 LLM을 자유롭게 풀어놔서 군더더기를 붙인 상태.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3) 코드 - AnswerEval과 structured output&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 LLM 평가자가 출력해야 할 형식을 Pydantic으로 정의한다.&lt;/p&gt;
&lt;pre class=&quot;smalltalk&quot;&gt;&lt;code&gt;class AnswerEval(BaseModel):
    &quot;&quot;&quot;LLM-as-a-judge evaluation of answer quality.&quot;&quot;&quot;

    feedback: str = Field(
        description=&quot;Concise feedback on the answer quality, comparing it to the reference answer and evaluating based on the retrieved context&quot;
    )
    accuracy: float = Field(
        description=&quot;How factually correct is the answer compared to the reference answer? 1 (wrong. any wrong answer must score 1) to 5 (ideal - perfectly accurate). An acceptable answer would score 3.&quot;
    )
    completeness: float = Field(
        description=&quot;How complete is the answer in addressing all aspects of the question? 1 (very poor - missing key information) to 5 (ideal - all the information from the reference answer is provided completely). Only answer 5 if ALL information from the reference answer is included.&quot;
    )
    relevance: float = Field(
        description=&quot;How relevant is the answer to the specific question asked? 1 (very poor - off-topic) to 5 (ideal - directly addresses question and gives no additional information). Only answer 5 if the answer is completely relevant to the question and gives no additional information.&quot;
    )
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기가 이 강의에서 말하는 structured output이 빛을 발하는 지점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;structured output이란 LLM이 자유 텍스트가 아니라 미리 정의해둔 스키마(필드와 타입)에 맞춘 JSON으로 응답하게 강제하는 기능이다. OpenAI나 LiteLLM 같은 라이브러리가 이걸 지원한다. 장점이 명확하다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;파싱 실패가 거의 없다. (LLM 응답에서 JSON 추출하다가 깨지는 일이 사라진다)&lt;/li&gt;
&lt;li&gt;누락 필드 검증이 자동으로 된다. (Pydantic이 검증)&lt;/li&gt;
&lt;li&gt;후속 처리가 안정적이다. (점수 집계, DB 저장 등)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 흥미로운 게 한 가지 있다. 지금까지 본 세 Pydantic 모델의 역할이 모두 다르다는 점이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;TestQuestion (test.py) : 외부 jsonl 데이터가 코드 안으로 들어올 때 검증하는 &lt;b&gt;입력 스키마&lt;/b&gt;.&lt;/li&gt;
&lt;li&gt;RetrievalEval (eval.py) : 코드가 직접 계산한 결과를 담는 &lt;b&gt;결과 그릇&lt;/b&gt;.&lt;/li&gt;
&lt;li&gt;AnswerEval (eval.py) : LLM의 출력을 미리 정해진 형식으로 강제하는 &lt;b&gt;출력 스키마&lt;/b&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 BaseModel을 상속받아 만들지만, 어디에 어떻게 쓰이는지가 모두 다르다. Pydantic이 이 강의에서 자주 등장하는 이유가 여기에 있다. 데이터&amp;middot;코드&amp;middot;LLM 사이의 인터페이스를 모두 같은 도구로 정의할 수 있기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 AnswerEval을 LLM 호출에 결합하는 코드는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;def evaluate_answer(test: TestQuestion) -&amp;gt; tuple[AnswerEval, str, list]:
    # Get RAG response using shared answer module
    generated_answer, retrieved_docs = answer_question(test.question)

    # LLM judge prompt
    judge_messages = [
        {
            &quot;role&quot;: &quot;system&quot;,
            &quot;content&quot;: &quot;You are an expert evaluator assessing the quality of answers. Evaluate the generated answer by comparing it to the reference answer. Only give 5/5 scores for perfect answers.&quot;,
        },
        {
            &quot;role&quot;: &quot;user&quot;,
            &quot;content&quot;: f&quot;&quot;&quot;Question:
{test.question}

Generated Answer:
{generated_answer}

Reference Answer:
{test.reference_answer}

Please evaluate the generated answer on three dimensions:
1. Accuracy: How factually correct is it compared to the reference answer? Only give 5/5 scores for perfect answers.
2. Completeness: How thoroughly does it address all aspects of the question, covering all the information from the reference answer?
3. Relevance: How well does it directly answer the specific question asked, giving no additional information?

Provide detailed feedback and scores from 1 (very poor) to 5 (ideal) for each dimension. If the answer is wrong, then the accuracy score must be 1.&quot;&quot;&quot;,
        },
    ]

    # Call LLM judge with structured outputs
    judge_response = completion(model=MODEL, messages=judge_messages, response_format=AnswerEval)

    answer_eval = AnswerEval.model_validate_json(judge_response.choices[0].message.content)

    return answer_eval, generated_answer, retrieved_docs
&lt;/code&gt;&lt;/pre&gt;
&lt;p 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;completion(..., response_format=AnswerEval) : LLM에게 &quot;이 형식으로 응답해라&quot;라고 강제.&lt;/li&gt;
&lt;li&gt;AnswerEval.model_validate_json(...) : LLM이 준 JSON을 Pydantic으로 검증&amp;middot;파싱.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프롬프트에는 &quot;Only give 5/5 scores for perfect answers&quot; 같은 제약을 명시해서 점수가 인플레이션되지 않게 한다. LLM 심판은 친절하게도 점수를 후하게 주는 경향이 있어, 이런 명시적 제약이 없으면 모든 답변이 4~5점에 몰린다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4) 실제로 돌려보기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IIOTY 케이스로 돌려보면 이런 결과가 나온다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;eval, answer, chunks = evaluate_answer(example)
print(eval.feedback)
print(eval.accuracy)
print(eval.completeness)
print(eval.relevance)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출력:&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;The answer correctly identifies Maxine as the winner and mentions the IIOTY award in 2023, but it omits Thompson's full name, which is present in the reference. This affects completeness. The relevance is high, as it directly addresses the question about the award winner.
5.0
4.0
5.0
&lt;/code&gt;&lt;/pre&gt;
&lt;p 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;Accuracy 5.0 &amp;rarr; 사실 자체는 정확하다.&lt;/li&gt;
&lt;li&gt;Completeness 4.0 &amp;rarr; &quot;Thompson&quot;이 빠진 것에 대한 감점. (이건 지난 글에서 발견한 한계와 정확히 일치한다. 청크에 풀네임이 있지 않아 답에 풀네임이 안 나오는 문제.)&lt;/li&gt;
&lt;li&gt;Relevance 5.0 &amp;rarr; 군더더기 없이 질문에만 답했다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흥미로운 건 Day 1~3 글에서 &quot;왠지 풀네임이 안 나오네?&quot; 라고 막연히 느꼈던 한계가, 이번에는 Completeness 4점이라는 객관적 숫자로 잡혔다는 점이다. 평가 시스템이 있으니 한계가 정량화되고, 정량화되니 개선 후 비교가 가능해진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;5) 보너스 - 끝단 사용자 피드백&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LLM 심판 외에도 실제 서비스에서는 엔드유저의 좋아요/싫어요 신호를 평가에 활용할 수 있다. 골든 데이터셋이 정답 기준이라면, 사용자 피드백은 실제 사용 환경의 정답 기준이 된다. 두 신호를 같이 보는 게 가장 강력한 평가 체계다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;6) 느낀 점&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예전에 llm as a judge를 들었을 땐 단순히 모델의 성능에만 의존하는 걸까? 라고 생각했었는데, &quot;참고 답안 + 명확한 채점 기준 + structured output&quot; 등의 구조를 잘 정의 하는게 중요하다는 걸 느꼈었다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;답변 평가가 자동화 가능하다는 점이 진짜 매력 포인트다. 테스트셋이 1,000개, 10,000개로 커지면 사람이 채점할 수 없는데, LLM 평가자는 그대로 스케일업된다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;background-color: #99cefa;&quot;&gt;5. 평가 결과로 본 한계&amp;nbsp;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 메트릭이 갖춰졌으니, 한 케이스가 아니라 테스트셋 전체를 돌려보고 RAG의 약점을 찾아낼 차례다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1) 150개 한 번에 돌리기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;evaluation/eval.py에는 한 케이스가 아니라 전체 테스트셋을 순회하면서 평가 결과를 yield 하는 제너레이터 함수도 들어 있다.&lt;/p&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;def evaluate_all_retrieval():
    &quot;&quot;&quot;Evaluate all retrieval tests.&quot;&quot;&quot;
    tests = load_tests()
    total_tests = len(tests)
    for index, test in enumerate(tests):
        result = evaluate_retrieval(test)
        progress = (index + 1) / total_tests
        yield test, result, progress


def evaluate_all_answers():
    &quot;&quot;&quot;Evaluate all answers to tests using batched async execution.&quot;&quot;&quot;
    tests = load_tests()
    total_tests = len(tests)
    for index, test in enumerate(tests):
        result = evaluate_answer(test)[0]
        progress = (index + 1) / total_tests
        yield test, result, progress
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제너레이터로 만들어놓은 이유는 진행률(progress)을 받아 UI에서 프로그레스 바로 보여주기 위해서다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;150개 케이스를 돌리는 데 시간이 좀 걸리기 때문에 한 케이스 끝날 때마다 결과를 흘려보내는 구조가 편하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 위에 별도 진입점이 있어서, 터미널에서 다음과 같이 실행하면 된다.&lt;/p&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;cd week5
uv run evaluator.py
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;evaluator.py는 위 두 제너레이터를 돌면서 검색 평가 점수와 답변 평가 점수를 모두 모은 뒤, 카테고리별로 평균을 내서 한 화면에 표로 보여주는 역할을 한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2) 청킹&amp;middot;임베딩 조합 실험&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 한 번에 점수를 뽑아낼 수 있게 되니까,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 먼저 해볼 만한 게 RAG 튜닝의 양대 축인 &lt;b&gt;청크 크기&lt;/b&gt;와 &lt;b&gt;임베딩 모델&lt;/b&gt;을 바꿔가면서 점수가 어떻게 변하는지 보는 일이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 OpenAI text-embedding-3-large 임베딩을 고정해두고, 청크 크기와 top-k를 셋으로 바꿔봤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;A. chunk 1667 / topk 3 - 큰 청크를 적은 개수로&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;큰 청크에 정보가 많이 들어가지만, top-k가 작아서 다양한 자료를 끌어오기 어렵다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 청크 안에 답이 다 들어 있는 direct_fact엔 강하지만 spanning에는 불리하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;675&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2SEX1/dJMcaiJZsrl/lLjWiWZiHUMKHoD71X9eU1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2SEX1/dJMcaiJZsrl/lLjWiWZiHUMKHoD71X9eU1/img.png&quot; data-alt=&quot;(이미지 - chunk 1667 / topk 3 평가 결과)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2SEX1/dJMcaiJZsrl/lLjWiWZiHUMKHoD71X9eU1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2SEX1%2FdJMcaiJZsrl%2FlLjWiWZiHUMKHoD71X9eU1%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;2048&quot; height=&quot;675&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;675&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;(이미지 - chunk 1667 / topk 3 평가 결과)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;B. chunk 1000 / topk 5 - 중간 크기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;청크가 적당히 작아지고 top-k도 늘어났다. 다양성과 집중도의 균형을 맞춘 셋업.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;675&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dONxro/dJMb997ozb4/72pZixEgyhZsxs1vpGFkrk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dONxro/dJMb997ozb4/72pZixEgyhZsxs1vpGFkrk/img.png&quot; data-alt=&quot;(이미지 - chunk 1000 / topk 5 평가 결과)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dONxro/dJMb997ozb4/72pZixEgyhZsxs1vpGFkrk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdONxro%2FdJMb997ozb4%2F72pZixEgyhZsxs1vpGFkrk%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;2048&quot; height=&quot;675&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;675&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;(이미지 - chunk 1000 / topk 5 평가 결과)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&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;p data-ke-size=&quot;size16&quot;&gt;C. chunk 500 / topk 10 - 작은 청크를 많은 개수로&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;청크는 작지만 많이 가져와서 다양한 자료를 컨텍스트에 넣을 수 있다. 다만 청크 하나하나가 너무 짧으면 답에 필요한 맥락이 끊긴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;1196&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GEyfY/dJMcaiDfLDA/Ia7n6agqNUtwjTXCgqBaJ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GEyfY/dJMcaiDfLDA/Ia7n6agqNUtwjTXCgqBaJ1/img.png&quot; data-alt=&quot;(이미지 - chunk 500 / topk 10 평가 결과)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GEyfY/dJMcaiDfLDA/Ia7n6agqNUtwjTXCgqBaJ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGEyfY%2FdJMcaiDfLDA%2FIa7n6agqNUtwjTXCgqBaJ1%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;2048&quot; height=&quot;1196&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;1196&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;(이미지 - chunk 500 / topk 10 평가 결과)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;D. HuggingFace 임베딩 + chunk 500 / topk 10&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;청크 크기와 top-k는 같지만 임베딩 모델만 OpenAI에서 HuggingFace 오픈소스 모델로 바꿨다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모델만 갈아끼웠을 때 점수가 어떻게 변하는지를 보기 위한 비교다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;1218&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bgdj7n/dJMcaffv3wP/eOhTVgzFlyFn1caz9sUAU0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bgdj7n/dJMcaffv3wP/eOhTVgzFlyFn1caz9sUAU0/img.png&quot; data-alt=&quot;(이미지 - HuggingFace 임베딩 평가 결과)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bgdj7n/dJMcaffv3wP/eOhTVgzFlyFn1caz9sUAU0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbgdj7n%2FdJMcaffv3wP%2FeOhTVgzFlyFn1caz9sUAU0%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;2048&quot; height=&quot;1218&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;1218&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;(이미지 - HuggingFace 임베딩 평가 결과)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&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;p 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;direct_fact는 어떤 셋업에서도 비교적 잘 답한다. 한 청크 안에 답이 있기 때문에 검색이 별로 안 어렵다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;spanning, holistic 카테고리는 모든 셋업에서 점수가 낮다.&lt;/b&gt; 청크 크기를 1667로 키워도, 500으로 줄이고 top-k를 10까지 늘려도 답이 여러 청크에 흩어져 있는 질문 자체엔 약하다.&lt;/li&gt;
&lt;li&gt;임베딩 모델만 바꿔도 결과가 출렁인다. 같은 청크 크기&amp;middot;top-k인데 OpenAI text-embedding-3-large와 HuggingFace 모델이 잡아오는 청크가 달라서 점수가 다르게 찍힌다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3) 진단&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 중요한 결론이 두 가지 나온다.&lt;/p&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;- spanning/holistic 같은 카테고리는 본질적으로 &quot;여러 청크를 통합해야 답할 수 있는 질문&quot;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 청크를 작게 쪼개도 그걸 다 끌어올 방법이 없으면 LLM이 답을 만들 수 없고, 청크를 크게 합쳐도 한 청크에 다 담기 어려우면 똑같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 즉, 청크 크기는 trade-off의 위치만 바꿀 뿐 한계 자체를 깨지는 못한다.&lt;/p&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;- 같은 RAG 구조에서 임베딩 모델만 바꿨는데 카테고리별 점수가 꽤 다르게 나온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 이건 RAG 튜닝이 한 가지 요소가 아니라 여러 요소의 조합이라는 걸 보여준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;종합하면 이렇다.&lt;/p&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;b&gt;RAG 파이프라인 자체에 새로운 구조를 도입해야 spanning/holistic 같은 카테고리에서의 한계를 깰 수 있다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이게 다음 섹션에서 다룰 Advanced RAG의 출발점이다. 청크를 더 똑똑하게 만들고(시맨틱 청킹), 검색 결과를 한 번 더 거르고(reranking), 사용자 질문을 검색 친화적으로 다듬는(query rewriting) 등의 본질적 개선이 필요하다는 결론이다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4) 느낀 점&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;청크 크기, top k 같은 하이퍼파라미터는 평가 파이프라인을 돌리지 않곤 최적의 값을 절대 못 정한다는 게 와닿았다. 직관으로는 &quot;당연히 작은 청크가 정확하지 않을까?&quot; 같은 생각이 드는데, 카테고리별로 보면 큰 청크가 강한 영역과 작은 청크가 강한 영역이 다르다. 측정해서 비교하지 않으면 잘못된 직관에 끌려간다. (또 문서의 특성에 따라서도 다를 수도 있다)&lt;/li&gt;
&lt;li&gt;임베딩 모델만 바꿔도 결과가 출렁이는 걸 보면서, 회사에서도 RAG 기반 에이전트를 운영할 때 임베딩 모델 변경이 결코 가벼운 결정이 아니란 걸 다시 느꼈다. 임베딩 모델 업데이트, 청킹 전략 변경, 프롬프트 수정 등 어떤 하나라도 바꾸면 그 즉시 평가 셋업으로 점수를 다시 뽑아 비교하는 프로세스가 있어야 안전하다.&lt;/li&gt;
&lt;li&gt;무엇보다 인상적이었던 건 &quot;어디가 약한지를 카테고리 단위로 알 수 있다&quot;는 점이다. 평균 점수만 보면 &quot;그냥 좀 떨어졌다&quot; 정도로 흘려넘겼을 차이가, 카테고리로 쪼개면 spanning에서 약하다는 명확한 진단으로 바뀐다. 이게 곧 &quot;어떤 개선책을 시도해야 하는지&quot;의 단서가 된다. (spanning에 약하다 &amp;rarr; hierarchical RAG나 시맨틱 청킹을 검토). 테스트 셋을 만들 때 카테고리를 다양화 해야하는 이유를 느낄 수 있었다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;background-color: #99cefa;&quot;&gt;6. 마치며&lt;/span&gt;&lt;/h3&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;글에서는&amp;nbsp;RAG&amp;nbsp;평가의&amp;nbsp;두&amp;nbsp;축을&amp;nbsp;정리했다.&lt;br /&gt;&lt;br /&gt;- 골든 테스트셋으로 채점 기준을 만들고&lt;br /&gt;- 검색 메트릭(MRR / nDCG / Recall@K / Precision@K / Keyword Coverage)과 답변 메트릭(LLM-as-a-Judge + structured output)으로 기존 단순 RAG의 한계를 정량화했다.&lt;br /&gt;&lt;br /&gt;평가&amp;nbsp;결과로&amp;nbsp;두&amp;nbsp;가지&amp;nbsp;한계가&amp;nbsp;보였다.&lt;br /&gt;&lt;br /&gt;-&amp;nbsp;spanning,&amp;nbsp;holistic&amp;nbsp;카테고리에서&amp;nbsp;점수가&amp;nbsp;일관되게&amp;nbsp;낮다&amp;nbsp;(단순&amp;nbsp;청크&amp;nbsp;크기&amp;nbsp;튜닝으로&amp;nbsp;안&amp;nbsp;풀린다).&lt;br /&gt;-&amp;nbsp;completeness&amp;nbsp;점수에서&amp;nbsp;풀네임&amp;nbsp;누락&amp;nbsp;같은&amp;nbsp;디테일이&amp;nbsp;잡힌다&amp;nbsp;(이전&amp;nbsp;글에서&amp;nbsp;막연히&amp;nbsp;느꼈던&amp;nbsp;한계가&amp;nbsp;객관적&amp;nbsp;숫자로&amp;nbsp;드러났다).&lt;br /&gt;&lt;br /&gt;다음&amp;nbsp;글에서는&amp;nbsp;이&amp;nbsp;한계를&amp;nbsp;어떻게&amp;nbsp;깰지를&amp;nbsp;다룬다.&amp;nbsp;시맨틱&amp;nbsp;청킹&amp;nbsp;+&amp;nbsp;문서&amp;nbsp;전처리&amp;nbsp;결합,&amp;nbsp;Reranking,&amp;nbsp;Query&amp;nbsp;Rewriting&amp;nbsp;같은&amp;nbsp;Advanced&amp;nbsp;RAG&amp;nbsp;기법을&amp;nbsp;직접&amp;nbsp;구현해보고,&amp;nbsp;적용&amp;nbsp;전후&amp;nbsp;점수를&amp;nbsp;비교해본다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;(꾸준히&amp;nbsp;공부하고&amp;nbsp;적을테니&amp;nbsp;많은&amp;nbsp;관심&amp;nbsp;부탁드립니다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;Profile:&lt;/span&gt;&lt;br /&gt;&lt;a style=&quot;color: #0070d1; text-align: start;&quot; href=&quot;http://www.linkedin.com/in/doobeom-kim-%EA%B9%80%EB%91%90%EB%B2%94-2b9649242&quot;&gt;&lt;span&gt;&lt;span style=&quot;color: #0070d1;&quot;&gt;Linkedin&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;</description>
      <category>AI 공부</category>
      <category>ai</category>
      <category>AI 공부</category>
      <category>Evaluation</category>
      <category>llm</category>
      <category>Rag</category>
      <category>RAG평가</category>
      <category>retrieval</category>
      <category>벡터 db</category>
      <category>생성형 AI</category>
      <category>청킹</category>
      <author>kdb1248</author>
      <guid isPermaLink="true">https://doobeom-coding.tistory.com/88</guid>
      <comments>https://doobeom-coding.tistory.com/88#entry88comment</comments>
      <pubDate>Sat, 9 May 2026 21:31:36 +0900</pubDate>
    </item>
    <item>
      <title>10. LLM이 우리 회사 문서를 읽고 답하게 하려면? &amp;mdash; 키워드 매칭에서 임베딩&amp;middot;벡터 DB&amp;middot;LangChain으로 확장해본 RAG 구현기</title>
      <link>https://doobeom-coding.tistory.com/87</link>
      <description>&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;목차&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;1. RAG는 왜 필요한가&lt;br /&gt;2. 단순 RAG ( 벡터 DB 없이 문자열 매칭을 통한 구현)&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;3. 벡터 임베딩(Vector Embedding) 이해하기&lt;br /&gt;4. 문서를 벡터 DB에 넣기&lt;br /&gt;5. LangChain으로 기본 RAG 파이프라인 완성&lt;br /&gt;6. RAG의 한계 &amp;mdash; 개발한 챗봇을 돌려보니 드러난 문제들&lt;br /&gt;7. 마무리&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;
&lt;p style=&quot;color: #666666;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;ldquo;LLM이 우리 회사 내부 문서를 기반으로 답변하게 만들 수 있을까?&amp;rdquo;&lt;/b&gt;&lt;br /&gt;이번 글에서는 이 질문을 출발점으로 삼아, 키워드 매칭 기반 검색에서 시작해 벡터 임베딩 검색과 LangChain 파이프라인까지 RAG의 기본 흐름을 직접&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span data-grammar-focus=&quot;false&quot; data-grammar-id=&quot;grammar1&quot; data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;구현해봤다.&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;구현해 봤다.&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;space&amp;quot;}&quot;&gt;구현해봤다.&lt;/span&gt;&lt;br /&gt;직접 만들어보고 돌려보면서, RAG 작동방식에 대한 이해와, 또 실제 적용 시 어디서 한계가 드러나는지도 함께 살펴봤다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;background-color: #99cefa;&quot;&gt;1. RAG는 왜 필요한가&lt;/span&gt;&lt;/h2&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;1) LLM 프롬프트를&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span data-grammar-focus=&quot;false&quot; data-grammar-id=&quot;grammar2&quot; data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;향상시키는&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;향상하는&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;spell&amp;quot;}&quot;&gt;향상시키는&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;방법들&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;LLM이 더 똑똑한 답변을 하게 만드는 방법은 크게 세 가지&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;Multi-shot 프롬프팅&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash; 예시 여러 개를 미리 보여주기&lt;/li&gt;
&lt;li&gt;&lt;b&gt;툴 사용&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash; 외부 API, 계산기, 검색 엔진 연결&lt;/li&gt;
&lt;li&gt;&lt;b&gt;추가&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span data-grammar-focus=&quot;false&quot; data-grammar-id=&quot;grammar3&quot; data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;컨텍스트&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;콘텍스트&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;spell&amp;quot;}&quot;&gt;컨텍스트&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;주입&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash; 질문과 관련된 배경 정보를 프롬프트에 끼워 넣기&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;1)-1. RAG의 자리&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;RAG(Retrieval Augmented&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span data-grammar-focus=&quot;false&quot; data-grammar-id=&quot;grammar4&quot; data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;Generation) 는&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;Generation)는&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;space&amp;quot;}&quot;&gt;Generation)&lt;/span&gt;&lt;/b&gt;&lt;span data-grammar-focus=&quot;false&quot; data-grammar-id=&quot;grammar4&quot; data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;Generation) 는&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;Generation)는&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;space&amp;quot;}&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;는&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;세 번째 방법인 &quot;추가&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span data-grammar-focus=&quot;false&quot; data-grammar-id=&quot;grammar5&quot; data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;컨텍스트&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;콘텍스트&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;spell&amp;quot;}&quot;&gt;컨텍스트&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;주입&quot;을&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;자동화&amp;middot;지능화&lt;/b&gt;한 기법이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;사람이 매번 관련 문서를 찾아서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span data-grammar-focus=&quot;false&quot; data-grammar-id=&quot;grammar6&quot; data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;붙여넣는&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;붙여 넣는&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;space&amp;quot;}&quot;&gt;붙여넣는&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;게 아니라, 질문이 들어오면 시스템이 알아서 관련 문서를 검색해 프롬프트에 끼워 넣는 방식&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;2) RAG의 핵심 3단계&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;Knowledge Base 구축&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash; 전문 지식이 담긴 데이터베이스 만들기&lt;/li&gt;
&lt;li&gt;&lt;b&gt;검색(Retrieval)&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash; 유저가 질문하면 DB에서 관련 내용 쿼리&lt;/li&gt;
&lt;li&gt;&lt;b&gt;쿼리 내용을 프롬프트에 주입&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash; &quot;여기 추가 정보가 있고, 질문과 관련 있을 수 있어. 참고해서 답변해&quot;라고 LLM에게 전달&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;LLM이 마치 그 분야 전문가처럼 답변하게 되는 것&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1230&quot; data-origin-height=&quot;610&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bUyito/dJMcaf7l1Oo/4xjugnYxt8WJd0LeIKklRK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bUyito/dJMcaf7l1Oo/4xjugnYxt8WJd0LeIKklRK/img.png&quot; data-alt=&quot;rag 간단 작동방식&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bUyito/dJMcaf7l1Oo/4xjugnYxt8WJd0LeIKklRK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbUyito%2FdJMcaf7l1Oo%2F4xjugnYxt8WJd0LeIKklRK%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;669&quot; height=&quot;332&quot; data-origin-width=&quot;1230&quot; data-origin-height=&quot;610&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;rag 간단 작동방식&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;3) 실습 요약 &amp;mdash; Insurellm 사내 RAG구축&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- 가상의 보험 테크 스타트업&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Insurellm&lt;/b&gt;.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- 회사의 공유 드라이브(직원 정보, 제품 정보, 계약서 등)를 Knowledge Base로 삼아&lt;br /&gt;&lt;b&gt;전문적인 질의응답이 가능한 AI Knowledge Worker&lt;/b&gt;를 만드는 게 목표&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;background-color: #99cefa;&quot;&gt;2. 단순 RAG ( 벡터 DB 없이 문자열 매칭을 통한 구현)&lt;/span&gt;&lt;/h2&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;1) 설계 개요&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- 가장 단순한 방식으로 시작&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- 벡터 DB도, 임베딩 모델도 없이&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Python dict + 문자열 매칭&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;만으로 RAG의 본질 체험 진행&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- Insurelllm의 직원, 제품 정보를&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span data-grammar-focus=&quot;false&quot; data-grammar-id=&quot;grammar7&quot; data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;답변해주는&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;답변해 주는&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;space&amp;quot;}&quot;&gt;답변해주는&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;전문가 rag 챗봇 구축&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&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;knowledge-base/employees/ &amp;mdash; 직원 정보 마크다운 파일&lt;/li&gt;
&lt;li&gt;knowledge-base/products/ &amp;mdash; 제품 정보 마크다운 파일&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;2) 세팅 코드&lt;/h3&gt;
&lt;pre class=&quot;xl&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;import os
import glob
from dotenv import load_dotenv
from pathlib import Path
import gradio as gr
from openai import OpenAI

load_dotenv(override=True)
openai_api_key = os.getenv('OPENAI_API_KEY')

MODEL = &quot;gpt-4.1-nano&quot;
openai = OpenAI()
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;3) Knowledge Base를 dict로 로드&lt;/h3&gt;
&lt;pre class=&quot;tcl&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;knowledge = {}

filenames = glob.glob(&quot;knowledge-base/employees/*&quot;)

for filename in filenames:
    name = Path(filename).stem.split(' ')[-1]  # 파일명에서 성(last name) 추출
    with open(filename, &quot;r&quot;, encoding=&quot;utf-8&quot;) as f:
        knowledge[name.lower()] = f.read()

# products도 동일하게 추가
filenames = glob.glob(&quot;knowledge-base/products/*&quot;)
for filename in filenames:
    name = Path(filename).stem
    with open(filename, &quot;r&quot;, encoding=&quot;utf-8&quot;) as f:
        knowledge[name.lower()] = f.read()
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;key는 직원의 성 or 제품명, value는 문서 전체 내용인 dict가 만들어짐&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-04-19 오후 3.03.15.png&quot; data-origin-width=&quot;2262&quot; data-origin-height=&quot;1052&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/NEqDZ/dJMcagFeUmf/lO0YVw8L6nTVb6qDfrYrS0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/NEqDZ/dJMcagFeUmf/lO0YVw8L6nTVb6qDfrYrS0/img.png&quot; data-alt=&quot;예시&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/NEqDZ/dJMcagFeUmf/lO0YVw8L6nTVb6qDfrYrS0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNEqDZ%2FdJMcagFeUmf%2FlO0YVw8L6nTVb6qDfrYrS0%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;2262&quot; height=&quot;1052&quot; data-filename=&quot;스크린샷 2026-04-19 오후 3.03.15.png&quot; data-origin-width=&quot;2262&quot; data-origin-height=&quot;1052&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;예시&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-04-19 오후 3.04.18.png&quot; data-origin-width=&quot;2262&quot; data-origin-height=&quot;262&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b3l4Sr/dJMcaaSx4O5/QK135k1zuSZ1uxex2onvKK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b3l4Sr/dJMcaaSx4O5/QK135k1zuSZ1uxex2onvKK/img.png&quot; data-alt=&quot;직원의 성이 key 값이 되어 직원문서 내용 검색 가능&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b3l4Sr/dJMcaaSx4O5/QK135k1zuSZ1uxex2onvKK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb3l4Sr%2FdJMcaaSx4O5%2FQK135k1zuSZ1uxex2onvKK%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;2262&quot; height=&quot;262&quot; data-filename=&quot;스크린샷 2026-04-19 오후 3.04.18.png&quot; data-origin-width=&quot;2262&quot; data-origin-height=&quot;262&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;직원의 성이 key 값이 되어 직원문서 내용 검색 가능&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;4) 핵심: 관련&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span data-grammar-focus=&quot;false&quot; data-grammar-id=&quot;grammar8&quot; data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;컨텍스트&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;콘텍스트&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;spell&amp;quot;}&quot;&gt;컨텍스트&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;찾기&lt;/h3&gt;
&lt;pre class=&quot;livecodeserver&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;def get_relevant_context(message):
    text = ''.join(ch for ch in message if ch.isalpha() or ch.isspace())
    words = text.lower().split()
    return [knowledge[word] for word in words if word in knowledge]
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;유저 질문을 단어 단위로 쪼개고, knowledge dict의 key와 일치하는 단어 있으면 해당 문서 가져옴.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;단순 매칭&lt;/b&gt;임.&amp;nbsp;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;(유저 질문 단어단위로 쪼갤 때&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span data-grammar-focus=&quot;false&quot; data-grammar-id=&quot;grammar9&quot; data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;알파벳,공백만&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;알파벳, 공백만&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;space&amp;quot;}&quot;&gt;알파벳,공백만&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;남기고 소문자로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span data-grammar-focus=&quot;false&quot; data-grammar-id=&quot;grammar10&quot; data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;변환한뒤&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;변환한 뒤&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;space&amp;quot;}&quot;&gt;변환한뒤&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;단어 분리)&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;5)&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span data-grammar-focus=&quot;false&quot; data-grammar-id=&quot;grammar11&quot; data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;컨텍스트를&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;콘텍스트를&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;spell&amp;quot;}&quot;&gt;컨텍스트를&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;프롬프트에 주입&lt;/h3&gt;
&lt;pre class=&quot;python&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;SYSTEM_PREFIX = &quot;&quot;&quot;
You represent Insurellm, the Insurance Tech company.
You are an expert in answering questions about Insurellm; its employees and its products.
You are provided with additional context that might be relevant to the user's question.
Give brief, accurate answers. If you don't know the answer, say so.

Relevant context:
&quot;&quot;&quot;

def additional_context(message):
    relevant_context = get_relevant_context(message)
    if not relevant_context:
        result = &quot;There is no additional context relevant to the user's question.&quot;
    else:
        result = &quot;The following additional context might be relevant in answering the user's question:\n\n&quot;
        result += &quot;\n\n&quot;.join(relevant_context)
    return result
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;관련 문서를 찾으면 프롬프트에 붙이고, 못 찾으면 &quot;관련 정보 없음&quot;을&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span data-grammar-focus=&quot;false&quot; data-grammar-id=&quot;grammar12&quot; data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;명시해준다.&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;명시해 준다.&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;space&amp;quot;}&quot;&gt;명시해준다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-04-19 오후 3.14.26.png&quot; data-origin-width=&quot;2262&quot; data-origin-height=&quot;1032&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cLdjNO/dJMcagyp6AM/69ap2kklHrrQ3VJjD8LFi1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cLdjNO/dJMcagyp6AM/69ap2kklHrrQ3VJjD8LFi1/img.png&quot; data-alt=&quot;예시&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cLdjNO/dJMcagyp6AM/69ap2kklHrrQ3VJjD8LFi1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcLdjNO%2FdJMcagyp6AM%2F69ap2kklHrrQ3VJjD8LFi1%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;2262&quot; height=&quot;1032&quot; data-filename=&quot;스크린샷 2026-04-19 오후 3.14.26.png&quot; data-origin-width=&quot;2262&quot; data-origin-height=&quot;1032&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;예시&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;6) 챗 함수 + Gradio&lt;/h3&gt;
&lt;pre class=&quot;vim&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;def chat(message, history):
    system_message = SYSTEM_PREFIX + additional_context(message)
    messages = [{&quot;role&quot;: &quot;system&quot;, &quot;content&quot;: system_message}] + history + [{&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: message}]
    response = openai.chat.completions.create(model=MODEL, messages=messages)
    return response.choices[0].message.content

gr.ChatInterface(chat, type=&quot;messages&quot;).launch(inbrowser=True)
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- &quot;SYSTEM_PREFIX+ 검색된&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span data-grammar-focus=&quot;false&quot; data-grammar-id=&quot;grammar13&quot; data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;컨텍스트\&amp;quot; 를&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;콘텍스트\&amp;quot;를&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;space_spell&amp;quot;}&quot;&gt;컨텍스트&quot; 를&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;시스템 메시지로 구성&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- 기존 대화 히스토리 + 유저이 새 발화 합쳐서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span data-grammar-focus=&quot;false&quot; data-grammar-id=&quot;grammar14&quot; data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;messages 로&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;messages로&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;space&amp;quot;}&quot;&gt;messages 로&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;만든 뒤 OPENAI_API 호출&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- Gradio ChatInterface로&amp;nbsp; 챗봇&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span data-grammar-focus=&quot;false&quot; data-grammar-id=&quot;grammar15&quot; data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;UI 로&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;UI로&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;space&amp;quot;}&quot;&gt;UI 로&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;테스트 진행&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;7) 실제로 돌려보니 발견한 사항들&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bftDC5/dJMcaiQwvaj/evZkSsPVgpcyjChmuiLbXk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bftDC5/dJMcaiQwvaj/evZkSsPVgpcyjChmuiLbXk/img.png&quot; style=&quot;width: 37.93813982612749%;&quot; data-widthpercent=&quot;38.38&quot; data-is-animation=&quot;false&quot; data-origin-height=&quot;984&quot; data-origin-width=&quot;2048&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bftDC5/dJMcaiQwvaj/evZkSsPVgpcyjChmuiLbXk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbftDC5%2FdJMcaiQwvaj%2FevZkSsPVgpcyjChmuiLbXk%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;2048&quot; height=&quot;984&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ybHEo/dJMcaiQwvar/prvLUK0eGCNE1LUnKbOHPk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ybHEo/dJMcaiQwvar/prvLUK0eGCNE1LUnKbOHPk/img.png&quot; style=&quot;width: 60.89906947619811%;&quot; data-widthpercent=&quot;61.62&quot; data-is-animation=&quot;false&quot; data-origin-height=&quot;613&quot; data-origin-width=&quot;2048&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ybHEo/dJMcaiQwvar/prvLUK0eGCNE1LUnKbOHPk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FybHEo%2FdJMcaiQwvar%2FprvLUK0eGCNE1LUnKbOHPk%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;2048&quot; height=&quot;613&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;앞 세션에선 avery가 누구냐고 질문했을 때 답변이 되는데, 신규 세션에선 불가능하다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;질문:&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;&quot;Who is Avery?&quot;&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;(성이 아닌 이름만)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;knowledge dict에는 성(last name)이 key로 들어 있음 (Avery&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span data-grammar-focus=&quot;false&quot; data-grammar-id=&quot;grammar16&quot; data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;langcaster 의&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;langcaster의&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;space&amp;quot;}&quot;&gt;langcaster 의&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;문서에선&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span data-grammar-focus=&quot;false&quot; data-grammar-id=&quot;grammar17&quot; data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;\&amp;quot;langcaster\&amp;quot; 가&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;\&amp;quot;langcaster\&amp;quot;가&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;space&amp;quot;}&quot;&gt;&quot;langcaster&quot; 가&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;dict의 key값)&amp;nbsp;&lt;br /&gt;&amp;rarr; get_relevant_context가 빈 리스트 반환&lt;/li&gt;
&lt;li&gt;2번째 신규세션에는 답변이 그래서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span data-grammar-focus=&quot;false&quot; data-grammar-id=&quot;grammar18&quot; data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;안나왔지만,&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;안 나왔지만,&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;space&amp;quot;}&quot;&gt;안나왔지만,&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span data-grammar-focus=&quot;false&quot; data-grammar-id=&quot;grammar19&quot; data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;첫번째&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;첫 번째&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;space&amp;quot;}&quot;&gt;첫번째&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;세션 대화에선&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;답변이 나옴?!&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;원인은 코드에 있음.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;chat 함수는 검색&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span data-grammar-focus=&quot;false&quot; data-grammar-id=&quot;grammar20&quot; data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;컨텍스트(유저&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;콘텍스트(유저&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;spell&amp;quot;}&quot;&gt;컨텍스트(유저&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;질문에서 단어 추출해 해당 값과 일치하는 dict key 값 검색) 뿐 아니라&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;지금까지의 대화&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span data-grammar-focus=&quot;false&quot; data-grammar-id=&quot;grammar21&quot; data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;기록(history) 을&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;기록(history)을&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;space&amp;quot;}&quot;&gt;기록(history) 을&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;전부 LLM에 전달&lt;/b&gt;하고 있음&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이전 대화에서 &quot;Avery Lancaster&quot;가 언급된 적이 있다면, LLM이 그 히스토리를 보고 답하는 것&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;rarr;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;신규 세션에서는 동일 질문에 답 못함&lt;/b&gt;. 검색 로직이 동작하지 않는다는 게 드러납니다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;8) 이 방식의 한계&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;직원에 대해 성(dict 값의 key)이 아니라 이름으로 물으면 실패&lt;/li&gt;
&lt;li&gt;오타가 있으면 실패 (langcaster 가 아니라 rangcaster 물으면 실패)&lt;/li&gt;
&lt;li&gt;동의어/유사어 처리 불가&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;느낀 점&lt;/b&gt;:&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 단계에서 &quot;왜 벡터 검색이 필요한가&quot;가 온몸으로 이해됨.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;글자가 똑같아야 매칭되는 게 아니라,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;의미가 비슷한 걸 찾아야&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;실사용 수준의 RAG가 될 수 있다는 걸 파악하게 됨.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;단순히 문자열 매칭이 아니라 성이 아닌 이름을 사용해도 작동되거나, 성에&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span data-grammar-focus=&quot;false&quot; data-grammar-id=&quot;grammar22&quot; data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;오타있게&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;오타 있게&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;space&amp;quot;}&quot;&gt;오타있게&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;입력했어도 이거 바로잡고 매칭되면 좋을 것&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;background-color: #99cefa;&quot;&gt;3. 벡터 임베딩(Vector Embedding) 이해하기&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;벡터 검색으로 가기 전에, 임베딩이 뭔지 개념을 정리&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;1) LLM에는 두 종류가 있다&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;① Auto-regressive LLM&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span data-grammar-focus=&quot;false&quot; data-grammar-id=&quot;grammar23&quot; data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;(자기회귀)&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;(자기 회귀)&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;space&amp;quot;}&quot;&gt;(자기회귀)&lt;/span&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;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;다음 토큰을 예측&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;한 번에 한 토큰씩, 계속 반복&lt;/li&gt;
&lt;li&gt;학습 데이터: 입력 시퀀스와 다음 토큰 쌍이 수없이 많음&lt;/li&gt;
&lt;li&gt;예: GPT 계열&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;② Auto-encoding LLM (인코더 / 임베딩 모델/ 벡터 임베딩 모델)&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;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;1개의 출력&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;생성 (full input base)&lt;/li&gt;
&lt;li&gt;용도:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;분류(Classification)&lt;/li&gt;
&lt;li&gt;Sentiment Analysis&lt;/li&gt;
&lt;li&gt;&lt;b&gt;벡터 임베딩 계산&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 입력을 가장 잘 나타내는 숫자 집합 만들기 (입력의 의미&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span data-grammar-focus=&quot;false&quot; data-grammar-id=&quot;grammar24&quot; data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;어떤식으로든&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;어떤 식으로든&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;space&amp;quot;}&quot;&gt;어떤식으로든&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;반영하려 하는 거)&lt;/li&gt;
&lt;li&gt;입력토큰을 받아 벡터로 매핑&lt;/li&gt;
&lt;li&gt;예: BERT, OpenAI Embeddings&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;2) 토큰 vs 벡터&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;텍스트를 토큰에 매핑, 그 후 벡터는 llm 이 이러한 토큰 가져온 후 반대편에서 나오는 거&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;table style=&quot;color: #333333; text-align: start; border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;background-color: #9b9b9b; color: #ffffff;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;background-color: #9b9b9b; color: #ffffff;&quot;&gt;토큰&lt;/td&gt;
&lt;td style=&quot;background-color: #9b9b9b; color: #ffffff;&quot;&gt;벡터&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;background-color: #efefef;&quot;&gt;역할&lt;/td&gt;
&lt;td&gt;입력&lt;/td&gt;
&lt;td&gt;출력&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;background-color: #efefef;&quot;&gt;성격&lt;/td&gt;
&lt;td style=&quot;background-color: #f9f9f9;&quot;&gt;텍스트를 숫자로 표현한 단순한 방법&lt;/td&gt;
&lt;td style=&quot;background-color: #f9f9f9;&quot;&gt;모델 내부 변환을 거친&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;의미 표현&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;흐름: 텍스트 &amp;rarr; 토큰(모델 입력) &amp;rarr; 모델 내부 변환 &amp;rarr; 벡터(의미를 담은 숫자 집합)&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;3)&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span data-grammar-focus=&quot;false&quot; data-grammar-id=&quot;grammar25&quot; data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;\&amp;quot;벡터(숫자) 가&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;\&amp;quot;벡터(숫자)가&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;space&amp;quot;}&quot;&gt;&quot;벡터(숫자) 가&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;의미를 나타낸다&quot;는 게 뭔 말?&lt;/h3&gt;
&lt;blockquote style=&quot;background-color: #000000; color: #333333; text-align: center;&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p style=&quot;color: #666666;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Vectors mathematically represent the 'meaning' of an input&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;특징&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;br /&gt;(예: Insurellm 문서에서 직원들의 경력도 벡터화 가능)&lt;/li&gt;
&lt;li&gt;보통&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;수백~수천 차원&lt;br /&gt;&lt;/b&gt;(예: 텍스트를 1000차원의 점으로 나타내)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;인풋에 대한&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span data-grammar-focus=&quot;false&quot; data-grammar-id=&quot;grammar26&quot; data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;&amp;lsquo;understanding&amp;rsquo; 을&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;&amp;lsquo;understanding&amp;rsquo;을&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;space&amp;quot;}&quot;&gt;&amp;lsquo;understanding&amp;rsquo; 을&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;나타내, 비슷한 인풋은 서로 close to each other 임&lt;br /&gt;&lt;/b&gt;예시:&lt;br /&gt;- &quot;뉴욕&amp;rarr;런던 항공권 가격&quot;과 &quot;JFK&amp;rarr;히스로 항공권 가격&quot; &amp;rarr;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span data-grammar-focus=&quot;false&quot; data-grammar-id=&quot;grammar27&quot; data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;두개&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;두 개&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;space&amp;quot;}&quot;&gt;두개&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span data-grammar-focus=&quot;false&quot; data-grammar-id=&quot;grammar28&quot; data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;점(벡터)는&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;점(벡터)은&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;spell&amp;quot;}&quot;&gt;점(벡터)는&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;가까울 것&lt;br /&gt;- 모델이 이런 식으로 수치를 만들어내도록 학습됨&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;4)&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span data-grammar-focus=&quot;false&quot; data-grammar-id=&quot;grammar29&quot; data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;Word2vec과&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;Word2 vec과&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;space&amp;quot;}&quot;&gt;Word2vec과&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;벡터 수학&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;2013년&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span data-grammar-focus=&quot;false&quot; data-grammar-id=&quot;grammar30&quot; data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;Word2vec이&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;Word2 vec이&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;space&amp;quot;}&quot;&gt;Word2vec이&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;유명해진 건 이런 마법 같은 연산 때문&lt;/p&gt;
&lt;pre class=&quot;inform7&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;king - man + woman &amp;asymp; Queen
Paris - France + England &amp;asymp; London
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;의미 단위의 산수&lt;/b&gt;가 가능해진 것&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;벡터는 단순한 좌표가 아니라 &quot;의미를 빼고 더할 수 있는 공간 위의 점&quot;이라고 이해하면 편함&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;요즘 인코더 모델은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;단어뿐 아니라 문단 전체&lt;/b&gt;에 대해서도 동일한 방식으로 벡터 공간에 매핑&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;텍스트 단락 전체에 대해 동일한 방식으로 작동해. 텍스트 단락을 벡터에 매핑한 다음 일부 의미를 제거하고 다른 의미를 추가할 수 있음&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;5) Fuzzy Lookup &amp;mdash; RAG의 핵심 트릭&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;시나리오&lt;/b&gt;: 유저가 &quot;히드로(런던의 공항)행 티켓&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span data-grammar-focus=&quot;false&quot; data-grammar-id=&quot;grammar31&quot; data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;가격은?\&amp;quot; 이라고&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;가격은?\&amp;quot;이라고&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;space&amp;quot;}&quot;&gt;가격은?&quot; 이라고&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;묻는데, 우리 DB에는 &quot;런던행&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span data-grammar-focus=&quot;false&quot; data-grammar-id=&quot;grammar32&quot; data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;가격\&amp;quot; 만&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;가격\&amp;quot;만&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;space&amp;quot;}&quot;&gt;가격&quot; 만&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&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;&quot;히드로&quot;의 벡터와 &quot;런던&quot;의 벡터는 공간적으로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;매우 가까움&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;질문 벡터 주변의 DB 벡터를 찾으면 &quot;런던&quot; 항목이&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span data-grammar-focus=&quot;false&quot; data-grammar-id=&quot;grammar33&quot; data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;걸려나옴&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;걸려 나옴&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;space&amp;quot;}&quot;&gt;걸려나옴&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이걸&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Fuzzy Lookup(의미 기반&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span data-grammar-focus=&quot;false&quot; data-grammar-id=&quot;grammar34&quot; data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;조회) 이라고&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;조회)이라고&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;space&amp;quot;}&quot;&gt;조회)&lt;/span&gt;&lt;/b&gt;&lt;span data-grammar-focus=&quot;false&quot; data-grammar-id=&quot;grammar34&quot; data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;조회) 이라고&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;조회)이라고&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;space&amp;quot;}&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;이라고&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;부릅니다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;6) RAG 파이프라인 정리&lt;/h3&gt;
&lt;pre class=&quot;cs&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;유저 질문
   &amp;darr;
[인코딩 LLM] 질문을 벡터로 변환
   &amp;darr;
[벡터 DB] 가장 가까운 벡터 찾기
   &amp;darr;
벡터에 연결된 &quot;원문 텍스트&quot; 꺼내기
   &amp;darr;
[답변 LLM] 자연어 프롬프트로 주입 &amp;rarr; 최종 답변
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;중요&lt;/b&gt;:&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- 인코딩 LLM과 답변 LLM은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;별개&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- 답변 LLM에게 가는 건 벡터가 아니라&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;자연어&lt;/b&gt;여야 함&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- 인코더의 역할은 질문과 DB 데이터를 벡터로 바꾸는 것까지입니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;background-color: #99cefa;&quot;&gt;4. 문서를 벡터 DB에 넣기&amp;nbsp;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이제 진짜 벡터 기반 RAG를 만들기.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;LangChain&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span data-grammar-focus=&quot;false&quot; data-grammar-id=&quot;grammar35&quot; data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;1.0 을&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;1.0을&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;space&amp;quot;}&quot;&gt;1.0&lt;/span&gt;&lt;/b&gt;&lt;span data-grammar-focus=&quot;false&quot; data-grammar-id=&quot;grammar35&quot; data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;1.0 을&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;1.0을&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;space&amp;quot;}&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;을&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;사용&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;0) 작업 flow 요약&amp;nbsp;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- 문서들을 청크단위로 쪼갬 &amp;rarr; 청크 벡터화&amp;rarr; 벡터 db에 저장 -&amp;gt; 시각화&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;1) LangChain 소개 및 장단점&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;LangChain?&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 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;pros &amp;amp; cons&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;처음등장했을 때는 각 모델별로 엔드포인트 형식이 달랐음 (이때 LangChain에선 추상화해서 호출 가능)&lt;/li&gt;
&lt;li&gt;지금은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span data-grammar-focus=&quot;false&quot; data-grammar-id=&quot;grammar36&quot; data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;오픈ai&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;오픈 ai&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;space&amp;quot;}&quot;&gt;오픈ai&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;compatible 엔드포인트를 제공, python 클라이언트 라이브러리 사용해 다른&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span data-grammar-focus=&quot;false&quot; data-grammar-id=&quot;grammar37&quot; data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;모델간&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;모델 간&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;space&amp;quot;}&quot;&gt;모델간&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;간단히 전환 가능&lt;br /&gt;(베이스 url 만 변경하면)&lt;/li&gt;
&lt;li&gt;툴, 프롬프트 템플릿&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span data-grammar-focus=&quot;false&quot; data-grammar-id=&quot;grammar38&quot; data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;이런것도&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;이런 것도&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;space&amp;quot;}&quot;&gt;이런것도&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;많이 통합됐음&lt;/li&gt;
&lt;li&gt;상당히 무거운 추상화 계층 임&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span data-grammar-focus=&quot;false&quot; data-grammar-id=&quot;grammar39&quot; data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;(litellm 과&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;(litellm과&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;space&amp;quot;}&quot;&gt;(litellm 과&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;달리)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;2) 문서를 왜 청크(Chunk)로 쪼개야 하나&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;문서의 벡터스토어 저장을 위해&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span data-grammar-focus=&quot;false&quot; data-grammar-id=&quot;grammar40&quot; data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;아래 처럼&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;아래처럼&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;space&amp;quot;}&quot;&gt;아래 처럼&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&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;/li&gt;
&lt;li&gt;이걸 벡터로 전환&lt;/li&gt;
&lt;li&gt;크로마(오픈소스 벡터&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span data-grammar-focus=&quot;false&quot; data-grammar-id=&quot;grammar41&quot; data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;db) 에&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;db)에&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;space&amp;quot;}&quot;&gt;db) 에&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;넣음&lt;/li&gt;
&lt;li&gt;시각화&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;color: #333333; text-align: start;&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;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;여러 주제&lt;/b&gt;가 섞여 있음 (예: 직원 인사 기록에는 이름, 직책, 급여, 평가 등 다양한 정보)&lt;/li&gt;
&lt;li&gt;유저 질문은 보통 문서의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;특정 한 부분&lt;/b&gt;에만 관련됨&lt;/li&gt;
&lt;li&gt;전체문서와 연관된 하나의 벡터를 만들면 질문이 전체 문서와 직접적으로 일치할 가능성이&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span data-grammar-focus=&quot;false&quot; data-grammar-id=&quot;grammar42&quot; data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;줄어듬&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;줄어듦&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;spell&amp;quot;}&quot;&gt;줄어듬&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;그래서 각 문서를 어떻게 분할해서 하나의 특정조각이&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span data-grammar-focus=&quot;false&quot; data-grammar-id=&quot;grammar43&quot; data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;몇가지&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;몇 가지&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;space&amp;quot;}&quot;&gt;몇가지&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;다른 질문을 해결할 수 있는 가장 좋은 기회를 가질 수 있는지, 그리고 그렇게 할 수 있는 적절한 세분성을 확보할 수 있는지 고민하는 것이 합리적&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그래서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;적절한&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span data-grammar-focus=&quot;false&quot; data-grammar-id=&quot;grammar44&quot; data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;세분성(granularity) 으로&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;세분성(granularity)으로&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;space&amp;quot;}&quot;&gt;세분성(granularity)&lt;/span&gt;&lt;/b&gt;&lt;span data-grammar-focus=&quot;false&quot; data-grammar-id=&quot;grammar44&quot; data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;세분성(granularity) 으로&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;세분성(granularity)으로&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;space&amp;quot;}&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;으로&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;청크를 나눠야 함&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;문서를 질문에 매핑될 가능성 높은 청크로 나누는데 집중&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;하나의 청크가 하나의 질문 유형에 답할 수 있을 만큼 짧고 명확해야 함&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;3) 임포트&lt;/h3&gt;
&lt;pre class=&quot;elm&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;import os
import glob
import tiktoken
import numpy as np
from dotenv import load_dotenv
from langchain_openai import OpenAIEmbeddings
from langchain_chroma import Chroma
from langchain_huggingface import HuggingFaceEmbeddings
from langchain_community.document_loaders import DirectoryLoader, TextLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from sklearn.manifold import TSNE
import plotly.graph_objects as go
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;4) 지식 창고 전체 크기 확인&lt;/h3&gt;
&lt;pre class=&quot;makefile&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;MODEL = &quot;gpt-4.1-nano&quot;
db_name = &quot;vector_db&quot;

files = glob.glob(&quot;knowledge-base/**/*.md&quot;, recursive=True)
entire_knowledge_base = &quot;&quot;
for file_path in files:
    with open(file_path, 'r', encoding='utf-8') as f:
        entire_knowledge_base += f.read() + &quot;\n\n&quot;

# 토큰 수 측정 (비용 가늠용)
encoding = tiktoken.encoding_for_model(MODEL)
tokens = encoding.encode(entire_knowledge_base)
print(f&quot;Total tokens: {len(tokens):,}&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;임베딩 비용을 미리 가늠할 때 유용&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;76개 파일, 303,434 character, 63,555 토큰 수&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-04-19 오후 3.51.45.png&quot; data-origin-width=&quot;2262&quot; data-origin-height=&quot;126&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ccJLNP/dJMcaffg7lf/idjoV27yaXurcQjCaIvxlk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ccJLNP/dJMcaffg7lf/idjoV27yaXurcQjCaIvxlk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ccJLNP/dJMcaffg7lf/idjoV27yaXurcQjCaIvxlk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FccJLNP%2FdJMcaffg7lf%2FidjoV27yaXurcQjCaIvxlk%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;2262&quot; height=&quot;126&quot; data-filename=&quot;스크린샷 2026-04-19 오후 3.51.45.png&quot; data-origin-width=&quot;2262&quot; data-origin-height=&quot;126&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-04-19 오후 3.52.07.png&quot; data-origin-width=&quot;2262&quot; data-origin-height=&quot;126&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HLLhD/dJMcafl0IIa/EJFZggyP20EkdA2v90YJzK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HLLhD/dJMcafl0IIa/EJFZggyP20EkdA2v90YJzK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HLLhD/dJMcafl0IIa/EJFZggyP20EkdA2v90YJzK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHLLhD%2FdJMcafl0IIa%2FEJFZggyP20EkdA2v90YJzK%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;2262&quot; height=&quot;126&quot; data-filename=&quot;스크린샷 2026-04-19 오후 3.52.07.png&quot; data-origin-width=&quot;2262&quot; data-origin-height=&quot;126&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;5) LangChain으로 문서 로드&lt;/h3&gt;
&lt;pre class=&quot;vim&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;folders = glob.glob(&quot;knowledge-base/*&quot;)

documents = []
for folder in folders:
    doc_type = os.path.basename(folder)  # 'employees', 'products' 등
    loader = DirectoryLoader(
        folder,
        glob=&quot;**/*.md&quot;,
        loader_cls=TextLoader,
        loader_kwargs={'encoding': 'utf-8'}
    )
    folder_docs = loader.load()
    for doc in folder_docs:
        doc.metadata[&quot;doc_type&quot;] = doc_type  # 메타데이터 주입
        documents.append(doc)
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- knowledge base/ 하위 각 폴더 (products, employees) 순회&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- 폴더명을 doc_type 메타데이터로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span data-grammar-focus=&quot;false&quot; data-grammar-id=&quot;grammar45&quot; data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;저장해두면,&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;저장해 두면,&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;space&amp;quot;}&quot;&gt;저장해두면,&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;나중에 시각화할 때 색깔로 구분하기 좋음&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;(product 문서인지, employee 문서 유형인지를 각 문서의 타입정보로 활용)&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- 76개 문서 로드&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;6) RecursiveCharacterTextSplitter로 청크 분할&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=1000,
    chunk_overlap=200
)
chunks = text_splitter.split_documents(documents)

print(f&quot;Divided into {len(chunks)} chunks&quot;)
print(f&quot;First chunk:\n\n{chunks[0]}&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;chunk_size=1000: 각 청크 최대 1000자&lt;/li&gt;
&lt;li&gt;chunk_overlap=200: 청크 경계에서 문맥이 끊기지 않도록 200자씩 겹치게&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;413개 청크로 전체 문서를 나눈 것을 볼 수 있음&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-04-19 오후 3.57.42.png&quot; data-origin-width=&quot;2334&quot; data-origin-height=&quot;472&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/I6dDE/dJMcaipu7zQ/P8Y3XJPcqoZ5EX8mB8DerK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/I6dDE/dJMcaipu7zQ/P8Y3XJPcqoZ5EX8mB8DerK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/I6dDE/dJMcaipu7zQ/P8Y3XJPcqoZ5EX8mB8DerK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FI6dDE%2FdJMcaipu7zQ%2FP8Y3XJPcqoZ5EX8mB8DerK%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;2334&quot; height=&quot;472&quot; data-filename=&quot;스크린샷 2026-04-19 오후 3.57.42.png&quot; data-origin-width=&quot;2334&quot; data-origin-height=&quot;472&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;7) 임베딩 모델 선택&amp;nbsp;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;텍스트를 인코더를 사용해 벡터로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span data-grammar-focus=&quot;false&quot; data-grammar-id=&quot;grammar46&quot; data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;변환해야함&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;변환해야 함&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;space&amp;quot;}&quot;&gt;변환해야함&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;embeddings = HuggingFaceEmbeddings(model_name=&quot;all-MiniLM-L6-v2&quot;)
# embeddings = OpenAIEmbeddings(model=&quot;text-embedding-3-large&quot;)  # 유료 대안
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;선택지 비교&lt;/b&gt;&lt;/p&gt;
&lt;table style=&quot;color: #333333; text-align: start; border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;background-color: #9b9b9b; color: #ffffff;&quot;&gt;모델명&lt;/td&gt;
&lt;td style=&quot;background-color: #9b9b9b; color: #ffffff;&quot;&gt;차원&lt;/td&gt;
&lt;td style=&quot;background-color: #9b9b9b; color: #ffffff;&quot;&gt;비용&lt;/td&gt;
&lt;td style=&quot;background-color: #9b9b9b; color: #ffffff;&quot;&gt;비고&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;background-color: #efefef;&quot;&gt;HuggingFace all-MiniLM-L6-v2&lt;/td&gt;
&lt;td&gt;384&lt;/td&gt;
&lt;td&gt;무료(로컬)&lt;/td&gt;
&lt;td&gt;가벼움, 실습용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;background-color: #efefef;&quot;&gt;OpenAI text-embedding-3-small&lt;/td&gt;
&lt;td style=&quot;background-color: #f9f9f9;&quot;&gt;1,500&lt;/td&gt;
&lt;td style=&quot;background-color: #f9f9f9;&quot;&gt;저렴&lt;/td&gt;
&lt;td style=&quot;background-color: #f9f9f9;&quot;&gt;API 호출&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;background-color: #efefef;&quot;&gt;OpenAI text-embedding-3-large&lt;/td&gt;
&lt;td&gt;3,000&lt;/td&gt;
&lt;td&gt;중간&lt;/td&gt;
&lt;td&gt;고성능&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 외에도 google의 gemini-embedding-001 등 다양&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;8) Chroma db에 저장&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;if os.path.exists(db_name):
    Chroma(persist_directory=db_name, embedding_function=embeddings).delete_collection()

vectorstore = Chroma.from_documents(
    documents=chunks,
    embedding=embeddings,
    persist_directory=db_name
)
print(f&quot;Vectorstore created with {vectorstore._collection.count()} documents&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;기존 DB가 있으면 지우고 새로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span data-grammar-focus=&quot;false&quot; data-grammar-id=&quot;grammar47&quot; data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;만듬.&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;만듦.&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;spell&amp;quot;}&quot;&gt;만듬.&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;persist_directory에 저장하면 다음 세션에서 재사용 가능.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;문서 임베딩 후 chroma 벡터스토어 생성/저장&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-04-19 오후 4.03.21.png&quot; data-origin-width=&quot;1216&quot; data-origin-height=&quot;68&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ccP0z7/dJMcad2JDjP/t05Vpnefoo30OrKqxKlL1k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ccP0z7/dJMcad2JDjP/t05Vpnefoo30OrKqxKlL1k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ccP0z7/dJMcad2JDjP/t05Vpnefoo30OrKqxKlL1k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FccP0z7%2FdJMcad2JDjP%2Ft05Vpnefoo30OrKqxKlL1k%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;1216&quot; height=&quot;68&quot; data-filename=&quot;스크린샷 2026-04-19 오후 4.03.21.png&quot; data-origin-width=&quot;1216&quot; data-origin-height=&quot;68&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;9) 벡터 DB 현황 확인&lt;/h3&gt;
&lt;pre class=&quot;markdown&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;collection = vectorstore._collection
count = collection.count()
sample_embedding = collection.get(limit=1, include=[&quot;embeddings&quot;])[&quot;embeddings&quot;][0]
dimensions = len(sample_embedding)
print(f&quot;{count:,} vectors with {dimensions:,} dimensions&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-04-19 오후 4.05.46.png&quot; data-origin-width=&quot;1216&quot; data-origin-height=&quot;68&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bCsML3/dJMcaa54Hqu/fTG453ce6M2Ibe102IwLH0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bCsML3/dJMcaa54Hqu/fTG453ce6M2Ibe102IwLH0/img.png&quot; data-alt=&quot;벡터 개수와 각 벡터의 차원수&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bCsML3/dJMcaa54Hqu/fTG453ce6M2Ibe102IwLH0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbCsML3%2FdJMcaa54Hqu%2FfTG453ce6M2Ibe102IwLH0%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;1216&quot; height=&quot;68&quot; data-filename=&quot;스크린샷 2026-04-19 오후 4.05.46.png&quot; data-origin-width=&quot;1216&quot; data-origin-height=&quot;68&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;벡터 개수와 각 벡터의 차원수&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;10) 벡터 스토어 선택지 비교&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;color: #333333; text-align: start; border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;background-color: #9b9b9b; color: #ffffff;&quot;&gt;&lt;b&gt;이름&amp;nbsp;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;background-color: #9b9b9b; color: #ffffff;&quot;&gt;성격&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;background-color: #efefef;&quot;&gt;&lt;b&gt;Chroma&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;오픈소스 벡터 DB (실습 적합)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;background-color: #efefef;&quot;&gt;&lt;b&gt;FAISS&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;background-color: #f9f9f9;&quot;&gt;DB는 아니고&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;in-memory 벡터 서치용 라이브러리&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;background-color: #efefef;&quot;&gt;&lt;b&gt;Elastic&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;벡터 검색 + 필터링 동시 지원&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;background-color: #efefef;&quot;&gt;Pinecone / Weaviate&lt;/td&gt;
&lt;td style=&quot;background-color: #f9f9f9;&quot;&gt;엔터프라이즈용 유료&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;background-color: #efefef;&quot;&gt;Postgres pgvector&lt;/td&gt;
&lt;td&gt;주류 DB에 벡터 기능 얹힌 형태&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;핵심 포인트&lt;/b&gt;:&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;대부분의 경우&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;진짜 중요한 건 임베딩 모델&lt;/b&gt;. DB 자체는 비용/성능/확장성/복원력/백업 등의 인프라 선택 문제에 가깝습니다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;11) t-SNE로 2D 시각화&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;벡터는 수백 차원이라 눈으로 볼 수 없으니,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;t-SNE&lt;/b&gt;(t-distributed stochastic neighbor embedding)로 2D/3D로 축소해서 시각화&lt;/p&gt;
&lt;pre class=&quot;routeros&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;result = collection.get(include=['embeddings', 'documents', 'metadatas'])
vectors = np.array(result['embeddings'])
documents = result['documents']
metadatas = result['metadatas']
doc_types = [metadata['doc_type'] for metadata in metadatas]
colors = [['blue', 'green', 'red', 'orange']
          [['products', 'employees', 'contracts', 'company'].index(t)]
          for t in doc_types]

tsne = TSNE(n_components=2, random_state=42)
reduced_vectors = tsne.fit_transform(vectors)

fig = go.Figure(data=[go.Scatter(
    x=reduced_vectors[:, 0],
    y=reduced_vectors[:, 1],
    mode='markers',
    marker=dict(size=5, color=colors, opacity=0.8),
    text=[f&quot;Type: {t}&amp;lt;br&amp;gt;Text: {d[:100]}...&quot; for t, d in zip(doc_types, documents)],
    hoverinfo='text'
)])
fig.update_layout(title='2D Chroma Vector Store Visualization', width=800, height=600)
fig.show()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bLxkQp/dJMcaadWi6J/nUQVmKrDLvUSGTvHoRvly1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bLxkQp/dJMcaadWi6J/nUQVmKrDLvUSGTvHoRvly1/img.png&quot; style=&quot;width: 33.00399924170889%;&quot; data-widthpercent=&quot;33.79&quot; data-filename=&quot;스크린샷 2026-04-19 오후 4.08.51.png&quot; data-origin-height=&quot;1000&quot; data-origin-width=&quot;1326&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bLxkQp/dJMcaadWi6J/nUQVmKrDLvUSGTvHoRvly1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbLxkQp%2FdJMcaadWi6J%2FnUQVmKrDLvUSGTvHoRvly1%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;1326&quot; height=&quot;1000&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/PDW5N/dJMcadPb2FR/I9q8aa5bZKI2se7zNVAjqk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/PDW5N/dJMcadPb2FR/I9q8aa5bZKI2se7zNVAjqk/img.png&quot; style=&quot;width: 32.08462878771898%;&quot; data-widthpercent=&quot;32.85&quot; data-is-animation=&quot;false&quot; data-origin-height=&quot;1024&quot; data-origin-width=&quot;1320&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/PDW5N/dJMcadPb2FR/I9q8aa5bZKI2se7zNVAjqk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPDW5N%2FdJMcadPb2FR%2FI9q8aa5bZKI2se7zNVAjqk%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;1320&quot; height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kJctW/dJMcadVYPkr/IUb1bzTjPwOh0n99KHikNk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kJctW/dJMcadVYPkr/IUb1bzTjPwOh0n99KHikNk/img.png&quot; style=&quot;width: 32.58579057522329%;&quot; data-widthpercent=&quot;33.36&quot; data-is-animation=&quot;false&quot; data-origin-height=&quot;1022&quot; data-origin-width=&quot;1338&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kJctW/dJMcadVYPkr/IUb1bzTjPwOh0n99KHikNk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkJctW%2FdJMcadVYPkr%2FIUb1bzTjPwOh0n99KHikNk%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;1338&quot; height=&quot;1022&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;왼쪽부터, 허깅페이스 임베딩 /오픈 ai 스몰/라지&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;doc_type에 따라 색상이 다름 (products(파랑),&amp;nbsp; employees(초록), contracts(빨강), company(주황) )&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;동일 문서 유형끼리 벡터들이 모여있는 걸 볼 수 있음&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;12) 3D 버전&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;tsne = TSNE(n_components=3, random_state=42)
reduced_vectors = tsne.fit_transform(vectors)

fig = go.Figure(data=[go.Scatter3d(
    x=reduced_vectors[:, 0],
    y=reduced_vectors[:, 1],
    z=reduced_vectors[:, 2],
    mode='markers',
    marker=dict(size=5, color=colors, opacity=0.8),
    text=[f&quot;Type: {t}&amp;lt;br&amp;gt;Text: {d[:100]}...&quot; for t, d in zip(doc_types, documents)],
    hoverinfo='text'
)])
fig.show()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/TGgSo/dJMcabcNTyk/tMONIPNklOivtUCgGq3AxK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/TGgSo/dJMcabcNTyk/tMONIPNklOivtUCgGq3AxK/img.png&quot; style=&quot;width: 49.10897306936132%;&quot; data-widthpercent=&quot;49.69&quot; data-filename=&quot;스크린샷 2026-04-19 오후 4.11.14.png&quot; data-origin-height=&quot;1154&quot; data-origin-width=&quot;1492&quot; data-is-animation=&quot;false&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/TGgSo/dJMcabcNTyk/tMONIPNklOivtUCgGq3AxK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FTGgSo%2FdJMcabcNTyk%2FtMONIPNklOivtUCgGq3AxK%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;1492&quot; height=&quot;1154&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HZV3W/dJMcaduSYSx/6LynhBXLWWeCKyVVyUxpR1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HZV3W/dJMcaduSYSx/6LynhBXLWWeCKyVVyUxpR1/img.png&quot; style=&quot;width: 49.728236232964264%;&quot; data-widthpercent=&quot;50.31&quot; data-is-animation=&quot;false&quot; data-origin-height=&quot;1022&quot; data-origin-width=&quot;1338&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HZV3W/dJMcaduSYSx/6LynhBXLWWeCKyVVyUxpR1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHZV3W%2FdJMcaduSYSx%2F6LynhBXLWWeCKyVVyUxpR1%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;1338&quot; height=&quot;1022&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;왼쪽 부터 허깅페이스모델/오픈AI large&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;doc_type에 따라 색상이 다름 (products(파랑),&amp;nbsp; employees(초록), contracts(빨강), company(주황) )&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;동일 문서 유형끼리 벡터들이 모여있는 걸 볼 수 있음&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;13) 시각화로 관찰한 것&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;(employees),&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;파란색&lt;/b&gt;(products),&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span data-grammar-focus=&quot;false&quot; data-grammar-id=&quot;grammar48&quot; data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;노랑색(contracts)이&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;노란색(contracts)이&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;spell&amp;quot;}&quot;&gt;&lt;b&gt;노랑색&lt;/b&gt;(contracts)이&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;각자 뚜렷한 클러스터를 형성&lt;/li&gt;
&lt;li&gt;같은 doc_type 문서들이&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;실제로 벡터 공간에서 뭉쳐 있음&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;rarr; 임베딩이 의미를 잘 포착한다는 시각적 증거&lt;/li&gt;
&lt;li&gt;&lt;b&gt;임베딩&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span data-grammar-focus=&quot;false&quot; data-grammar-id=&quot;grammar49&quot; data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;모델을변경해&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;모델을 변경해&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;space&amp;quot;}&quot;&gt;모델을변경해&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;차원이 커질수록&lt;/b&gt;(384 &amp;rarr; 1,500 &amp;rarr; 3,000) 클러스터가&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;더 깔끔하게 분리&lt;/b&gt;됨&lt;/li&gt;
&lt;li&gt;단,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;contracts와 products 클러스터가 일부 겹치는 것&lt;/b&gt;이 관찰됨 &amp;rarr; 계약서에 제품 정보가 섞여 있기 때문&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;14) 큰 임베딩 모델이 항상 더 좋을까?&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;더 많은 차원 &amp;rarr; 더 많은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;자유도&lt;/b&gt;로 의미 표현 가능 &amp;rarr; 잠재적으로 더 정교&lt;/li&gt;
&lt;li&gt;하지만&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;항상 좋은 건 아님&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;텍스트의 의미를 더 잘 포착할 수 있는 더 고급 트랜스포머 모델이란 걸 의미하는&lt;/li&gt;
&lt;li&gt;많은 차원은 표현력의 &quot;여지&quot;를 줄 뿐&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;느낀 점&lt;/b&gt;:&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;시각화해서 보니 '같은 종류 문서는 정말 뭉쳐있다'가 눈으로 보인게 신기 했고,&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;더 고급 모델을 씀에 따라&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span data-grammar-focus=&quot;false&quot; data-grammar-id=&quot;grammar50&quot; data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;뭉침정도가&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;뭉침 정도가&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;space&amp;quot;}&quot;&gt;뭉침정도가&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;더 깔끔하게&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span data-grammar-focus=&quot;false&quot; data-grammar-id=&quot;grammar51&quot; data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;되는게&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;되는 게&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;space&amp;quot;}&quot;&gt;되는게&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span data-grammar-focus=&quot;false&quot; data-grammar-id=&quot;grammar52&quot; data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;보이는게&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;보이는 게&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;space&amp;quot;}&quot;&gt;보이는게&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;신기했다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;실제 문서 업로드 -&amp;gt; 청킹 -&amp;gt; 임베딩 모델을 사용한 벡터화 -&amp;gt; 벡터 db에 저장 -&amp;gt; 저장된 결과물을 시각화해서 보는 일련의 과정을&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span data-grammar-focus=&quot;false&quot; data-grammar-id=&quot;grammar53&quot; data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;진행해볼&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;진행해 볼&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;space&amp;quot;}&quot;&gt;진행해볼&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;수 있던 게&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span data-grammar-focus=&quot;false&quot; data-grammar-id=&quot;grammar54&quot; data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;의미있었다.&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;의미 있었다.&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;space&amp;quot;}&quot;&gt;의미있었다.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;background-color: #99cefa;&quot;&gt;5. LangChain으로 기본 RAG 파이프라인 완성&amp;nbsp;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;앞에&lt;b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&quot;4. 문서를 벡터 DB에&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span data-grammar-focus=&quot;false&quot; data-grammar-id=&quot;grammar55&quot; data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;넣기\&amp;quot; 에서&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;넣기\&amp;quot;에서&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;space&amp;quot;}&quot;&gt;넣기&quot;&lt;/span&gt;&lt;/b&gt;&lt;span data-grammar-focus=&quot;false&quot; data-grammar-id=&quot;grammar55&quot; data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;넣기\&amp;quot; 에서&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;넣기\&amp;quot;에서&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;space&amp;quot;}&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;에서&lt;/span&gt;&amp;nbsp;만든 vector_db를 불러와서 실제 챗봇을 완성합니다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;1) 전체 흐름&lt;/h3&gt;
&lt;pre class=&quot;excel&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;유저 질문
   &amp;darr;
임베딩 모델이 질문을 벡터로 변환
   &amp;darr;
벡터 DB에서 유사 벡터 검색
   &amp;darr;
연결된 원문 텍스트 꺼내기
   &amp;darr;
프롬프트에 주입 &amp;rarr; LLM 답변 생성
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;2) 임포트와 세팅&lt;/h3&gt;
&lt;pre class=&quot;clean&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from langchain_chroma import Chroma
from langchain_core.messages import SystemMessage, HumanMessage
from langchain_huggingface import HuggingFaceEmbeddings
import gradio as gr

MODEL = &quot;gpt-4.1-nano&quot;
DB_NAME = &quot;vector_db&quot;
load_dotenv(override=True)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;3) Chroma db연결&lt;/h3&gt;
&lt;pre class=&quot;reasonml&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;embeddings = HuggingFaceEmbeddings(model_name=&quot;all-MiniLM-L6-v2&quot;)
vectorstore = Chroma(persist_directory=DB_NAME, embedding_function=embeddings)
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;4. 문서를 벡터 DB에&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span data-grammar-focus=&quot;false&quot; data-grammar-id=&quot;grammar56&quot; data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;넣기\&amp;quot;&amp;nbsp;에서&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;넣기\&amp;quot;에서&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;space&amp;quot;}&quot;&gt;넣기&quot;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;span data-grammar-focus=&quot;false&quot; data-grammar-id=&quot;grammar56&quot; data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;넣기\&amp;quot;&amp;nbsp;에서&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;넣기\&amp;quot;에서&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;space&amp;quot;}&quot;&gt;에서&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;저장한 DB를 그대로 재사용&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;중요&lt;/b&gt;: 저장할 때 썼던 것과&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;동일한 임베딩&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span data-grammar-focus=&quot;false&quot; data-grammar-id=&quot;grammar57&quot; data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;모델 을&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;모델을&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;space&amp;quot;}&quot;&gt;모델&lt;/span&gt;&lt;/b&gt;&lt;span data-grammar-focus=&quot;false&quot; data-grammar-id=&quot;grammar57&quot; data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;모델 을&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;모델을&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;space&amp;quot;}&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;을&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;사용 (안 그러면 차원도 안 맞고 의미 공간도 달라서 엉뚱한 결과가 나옴)&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span data-grammar-focus=&quot;false&quot; data-grammar-id=&quot;grammar58&quot; data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;저장시와&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;저장 시와&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;space&amp;quot;}&quot;&gt;저장시와&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span data-grammar-focus=&quot;false&quot; data-grammar-id=&quot;grammar59&quot; data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;검색시&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;검색 시&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;space&amp;quot;}&quot;&gt;검색시&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;벡터공간이&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span data-grammar-focus=&quot;false&quot; data-grammar-id=&quot;grammar60&quot; data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;일치해야하기&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;일치해야 하기&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;space&amp;quot;}&quot;&gt;일치해야하기&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;때문&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;4) LangChain의 핵심 추상화 2가지&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;랭체인 (없이도 rag 짤 수 있는데 좀 더 빠르게 짤 수 있음)&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;① llm&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash; 모델 교체 자유&lt;/p&gt;
&lt;pre class=&quot;routeros&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;llm = ChatOpenAI(temperature=0, model_name=MODEL)
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;랭체인 이용해서 모델을&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span data-grammar-focus=&quot;false&quot; data-grammar-id=&quot;grammar61&quot; data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;여러가지&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;여러 가지&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;space&amp;quot;}&quot;&gt;여러가지&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;교체해서 사용가능(추상화 이용해)&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;② retriever&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash; 벡터 DB + 임베딩 + 검색 로직을 하나로 감싼 추상화&lt;/p&gt;
&lt;pre class=&quot;ini&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;retriever = vectorstore.as_retriever()
&lt;/code&gt;&lt;/pre&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;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span data-grammar-focus=&quot;false&quot; data-grammar-id=&quot;grammar62&quot; data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;리트리버에&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;레트리버에&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;spell&amp;quot;}&quot;&gt;리트리버에&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;요청가능&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;두 객체&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span data-grammar-focus=&quot;false&quot; data-grammar-id=&quot;grammar63&quot; data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;모두 .invoke()&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;모두. invoke()&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;space&amp;quot;}&quot;&gt;모두 .invoke()&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;메서드로 호출하는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;통일된 인터페이스&lt;/b&gt;를 가짐&lt;/p&gt;
&lt;pre class=&quot;less&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;retriever.invoke(&quot;Who is Avery?&quot;)  # 관련 문서 청크 리스트 반환
llm.invoke(&quot;Who is Avery?&quot;)         # 그냥 LLM에 질문 (컨텍스트 없이)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-04-19 오후 4.22.46.png&quot; data-origin-width=&quot;2348&quot; data-origin-height=&quot;692&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/czX4cg/dJMcahYpIK2/KK7p13ihakAQFIVlbvil3K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/czX4cg/dJMcahYpIK2/KK7p13ihakAQFIVlbvil3K/img.png&quot; data-alt=&quot;retriever.invoke() 시, 유사 청크 4개 반환이 기본값&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/czX4cg/dJMcahYpIK2/KK7p13ihakAQFIVlbvil3K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FczX4cg%2FdJMcahYpIK2%2FKK7p13ihakAQFIVlbvil3K%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;2348&quot; height=&quot;692&quot; data-filename=&quot;스크린샷 2026-04-19 오후 4.22.46.png&quot; data-origin-width=&quot;2348&quot; data-origin-height=&quot;692&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;retriever.invoke() 시, 유사 청크 4개 반환이 기본값&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;5) Temperature 짧게 짚고 가기&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;llm = ChatOpenAI(temperature=0, model_name=MODEL)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;temperature =&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;출력 다양성 제어&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;temperature=0: 항상 최고 확률 토큰 선택 (예측 가능)&lt;/li&gt;
&lt;li&gt;temperature=1: 확률대로 샘플링 (다양성)&lt;/li&gt;
&lt;li&gt;흔히 &quot;창의성&quot;이라고 표현하지만 정확히는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;토큰 선택 방식&lt;/b&gt;일 뿐&lt;/li&gt;
&lt;li&gt;&lt;b&gt;창의성이 필요하면 System Prompt로 지시하는 게 맞음&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;주의: temperature=0이라도 완전히 재현 가능한 건 아님 (random seed도 필요)&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;6) 답변 함수 &amp;mdash; RAG의 핵심&lt;/h3&gt;
&lt;pre class=&quot;python&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;SYSTEM_PROMPT_TEMPLATE = &quot;&quot;&quot;
You are a knowledgeable, friendly assistant representing the company Insurellm.
You are chatting with a user about Insurellm.
If relevant, use the given context to answer any question.
If you don't know the answer, say so.
Context:
{context}
&quot;&quot;&quot;

def answer_question(question: str, history):
    docs = retriever.invoke(question)                            # 1) 관련 청크 검색
    context = &quot;\n\n&quot;.join(doc.page_content for doc in docs)      # 2) 텍스트 합치기
    system_prompt = SYSTEM_PROMPT_TEMPLATE.format(context=context)  # 3) 프롬프트 포맷
    response = llm.invoke([                                      # 4) LLM 호출
        SystemMessage(content=system_prompt),
        HumanMessage(content=question)
    ])
    return response.content
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이게 전부&lt;/b&gt;. RAG 파이프라인이 실질적으로 5줄짜리 함수로 끝남.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;RAG 핵심 3단계&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. Retrieval&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;retriever.invoke(question)&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;-&amp;gt; 유저 질문과&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span data-grammar-focus=&quot;false&quot; data-grammar-id=&quot;grammar64&quot; data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;관련있는&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;관련 있는&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;space&amp;quot;}&quot;&gt;관련있는&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;(벡터상 유사한) 청크 4개 검색&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. Augmentation&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;검색된 청크들을 \n\n으로 연결해서 시스템 프롬프트의 {context}에 주입&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. Generation&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span data-grammar-focus=&quot;false&quot; data-grammar-id=&quot;grammar65&quot; data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;컨텍스트가&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;콘텍스트가&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;spell&amp;quot;}&quot;&gt;컨텍스트가&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;주입된 시스템 프롬프트 + 유저 질의를&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span data-grammar-focus=&quot;false&quot; data-grammar-id=&quot;grammar66&quot; data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;llm 에&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;llm에&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;space&amp;quot;}&quot;&gt;llm 에&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;전달&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;-&amp;gt; 답변 생성&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;7) Gradio 챗봇&lt;/h3&gt;
&lt;pre class=&quot;css&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;gr.ChatInterface(answer_question).launch()
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;한 줄로 챗봇 UI 완성.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;8) 모듈 구조로 정리 (실전 프로젝트 구조)&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;실습에서는 Jupyter로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span data-grammar-focus=&quot;false&quot; data-grammar-id=&quot;grammar67&quot; data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;한번에&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;한 번에&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;space&amp;quot;}&quot;&gt;한번에&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;다 돌렸지만, 실제 운영 코드로 만들 때는 이렇게 나눔&lt;/p&gt;
&lt;pre class=&quot;clean&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;implementation/
├── ingest.py    # 문서 로드 &amp;rarr; 청킹 &amp;rarr; 벡터화 &amp;rarr; Chroma 저장
├── answer.py    # 질문이 주어지면 retriever 통해 컨텍스트 가져옴 + llm 구성 &amp;rarr; answer_question()
app.py           # Gradio 앱 (answer.py import)&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;실행:&lt;/p&gt;
&lt;pre class=&quot;dockerfile&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;# implementation 디렉토리에서 (최초 1회)
uv run ingest.py

# week5 디렉토리에서
uv run app.py
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;ingest.py는 DB 만들 때 한 번만 돌리고, 앱은 app.py만&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span data-grammar-focus=&quot;false&quot; data-grammar-id=&quot;grammar68&quot; data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;재실행하면됨&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;재실행하면 됨&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;space&amp;quot;}&quot;&gt;재실행하면됨&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;9) 이전 &quot;2. 단순 RAG ( 벡터 DB 없이 문자열 매칭을 통한 단순한 구현)&quot; 대비 나아진 점&amp;nbsp;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. 문서 dict의 key 인 &quot;성&quot; 이 아닌 &quot;이름 (first&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span data-grammar-focus=&quot;false&quot; data-grammar-id=&quot;grammar69&quot; data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;name)\&amp;quot; 으로&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;name)\&amp;quot;으로&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;space&amp;quot;}&quot;&gt;name)&quot; 으로&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;검색이 가능&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Avery lancaster에 대해, who is lancaster가 아니라 who is&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span data-grammar-focus=&quot;false&quot; data-grammar-id=&quot;grammar70&quot; data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;Avery 라고&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;Avery라고&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;space&amp;quot;}&quot;&gt;Avery 라고&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;해도 답변이 나옴&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-04-19 오후 4.37.59.png&quot; data-origin-width=&quot;2526&quot; data-origin-height=&quot;1074&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cIXGsl/dJMcacQledd/hqkGKxbUmjtE0sOoHhUKA1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cIXGsl/dJMcacQledd/hqkGKxbUmjtE0sOoHhUKA1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cIXGsl/dJMcacQledd/hqkGKxbUmjtE0sOoHhUKA1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcIXGsl%2FdJMcacQledd%2FhqkGKxbUmjtE0sOoHhUKA1%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;2526&quot; height=&quot;1074&quot; data-filename=&quot;스크린샷 2026-04-19 오후 4.37.59.png&quot; data-origin-width=&quot;2526&quot; data-origin-height=&quot;1074&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 오타가 있어도 의미 기반 검색을 통해 검색이 가능&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;who is Aver&quot;i&quot; Lancaster?&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;-&amp;gt; 오타가 있었음에도 벡터 유사도 기반 의미기반 검색이라 Avery Lancaster 정보 찾아냄&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-04-19 오후 4.39.44.png&quot; data-origin-width=&quot;2526&quot; data-origin-height=&quot;1074&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dGExd1/dJMcac3Q1xs/qJV6E1LwTEI4KUorlo2GN0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dGExd1/dJMcac3Q1xs/qJV6E1LwTEI4KUorlo2GN0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dGExd1/dJMcac3Q1xs/qJV6E1LwTEI4KUorlo2GN0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdGExd1%2FdJMcac3Q1xs%2FqJV6E1LwTEI4KUorlo2GN0%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;2526&quot; height=&quot;1074&quot; data-filename=&quot;스크린샷 2026-04-19 오후 4.39.44.png&quot; data-origin-width=&quot;2526&quot; data-origin-height=&quot;1074&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. 연속 질문 대응 가능&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span data-grammar-focus=&quot;false&quot; data-grammar-id=&quot;grammar71&quot; data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;avery 에&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;avery에&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;space&amp;quot;}&quot;&gt;avery 에&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;대해 물은 뒤 그녀의 salary에 대해 물었는데 답변 성공&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;1110&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wA3Ri/dJMcacJBw6c/mhFitfSfmviE9zO09BRRk1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wA3Ri/dJMcacJBw6c/mhFitfSfmviE9zO09BRRk1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wA3Ri/dJMcacJBw6c/mhFitfSfmviE9zO09BRRk1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwA3Ri%2FdJMcacJBw6c%2FmhFitfSfmviE9zO09BRRk1%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;2048&quot; height=&quot;1110&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;1110&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;10) LangChain을 꼭 써야 할까?&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;Pros&lt;/b&gt;: 파이프라인을 몇 줄로 끝낼 수 있음. 엔터프라이즈 환경에서 많이 사용&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Cons&lt;/b&gt;: 추상화 계층이 상당히 무거움 (LiteLLM 같은 경량 대안에 비해)&lt;/li&gt;
&lt;li&gt;요즘은 대부분 모델이&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;OpenAI compatible 엔드포인트&lt;/b&gt;를 제공해서 base_url만 바꿔도 모델 교체 가능 &amp;rarr; LangChain 없이도 RAG 구축은 충분히 가능&lt;/li&gt;
&lt;li&gt;선택 기준:&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;툴, 프롬프트 템플릿, 체인 기능을 적극 쓸지&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;여부&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;background-color: #99cefa;&quot;&gt;6. RAG의 한계 &amp;mdash; 개발한 챗봇을 돌려보니 드러난 문제들&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;위에선 기존 키워드 기반 검색 챗봇 대비 나아진 점 (오타 대응, 이름으로도 검색 가능)을 이야기했지만&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;실제 만든 rag챗봇을 이용해&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span data-grammar-focus=&quot;false&quot; data-grammar-id=&quot;grammar72&quot; data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;보다보니&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;보다 보니&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;space&amp;quot;}&quot;&gt;보다보니&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span data-grammar-focus=&quot;false&quot; data-grammar-id=&quot;grammar73&quot; data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;몇가지&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;몇 가지&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;space&amp;quot;}&quot;&gt;몇가지&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;한계를 파악할 수 있었음&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;1) 문제 ① &amp;mdash; 청크 경계 문제&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-04-19 오후 4.46.51.png&quot; data-origin-width=&quot;2526&quot; data-origin-height=&quot;1074&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9WuVH/dJMcaiJKGbn/i4mUB9cTI9c2sf13Qqilh1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9WuVH/dJMcaiJKGbn/i4mUB9cTI9c2sf13Qqilh1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9WuVH/dJMcaiJKGbn/i4mUB9cTI9c2sf13Qqilh1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9WuVH%2FdJMcaiJKGbn%2Fi4mUB9cTI9c2sf13Qqilh1%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;2526&quot; height=&quot;1074&quot; data-filename=&quot;스크린샷 2026-04-19 오후 4.46.51.png&quot; data-origin-width=&quot;2526&quot; data-origin-height=&quot;1074&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;해당 청크에 풀네임이 없어서, 풀네임을 가져오지 못함.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;청킹 크기와 오버랩 설정, 원문의 구조에 따라 답변 가능 여부가 갈립니다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;3) 문제&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;②&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash; 복합 질문의 검색 품질 저하&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;1110&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dA6eI9/dJMcadaAotd/HbV5zRPOh1D9xkPbaRaeMk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dA6eI9/dJMcadaAotd/HbV5zRPOh1D9xkPbaRaeMk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dA6eI9/dJMcadaAotd/HbV5zRPOh1D9xkPbaRaeMk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdA6eI9%2FdJMcadaAotd%2FHbV5zRPOh1D9xkPbaRaeMk%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;2048&quot; height=&quot;1110&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;1110&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;시나리오&lt;/b&gt;: 한 채팅에&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Avery와 IIOTY award 가&lt;/b&gt;&amp;nbsp;동시에 언급되면?&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span data-grammar-focus=&quot;false&quot; data-grammar-id=&quot;grammar74&quot; data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;리트리버가&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;레트리버가&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;spell&amp;quot;}&quot;&gt;리트리버가&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;두 주제(avery, IIOTY)&amp;nbsp; 모두에 대한 유사 문서를 찾으려 하면서&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;rarr;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;검색 품질이 떨어짐&lt;/b&gt;. 데이터 저장소에서 엉뚱하게 많은 걸 끌어옵니다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;4) 핵심 교훈&lt;/h3&gt;
&lt;blockquote style=&quot;background-color: #000000; color: #333333; text-align: center;&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p style=&quot;color: #666666;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;RAG는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span data-grammar-focus=&quot;false&quot; data-grammar-id=&quot;grammar76&quot; data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;\&amp;quot;한번에&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;\&amp;quot;한 번에&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;space&amp;quot;}&quot;&gt;&quot;한번에&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;완성되는 해법&quot;이 아니라 지속적으로 개선해야 하는 실험적 파이프라인&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;각 실패 케이스를&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;개별 문제&lt;/b&gt;로 보고 하나씩 해결해야 함&lt;/li&gt;
&lt;li&gt;한 문제 고치면 다른 문제가 생기기도 함 &amp;rarr; 이게&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;정상&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;청킹 전략, 임베딩 모델, 검색 파라미터(top-k), 쿼리 재작성(query rewriting), 하이브리드 검색 등&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;고도화할 요소가 많음&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;느낀 점&lt;/b&gt;:&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- 단순하게 rag를 구축하는 것 자체는 쉽다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- 하지만, 실제 돌려봤을 때 이런 세부 case들에서 잘&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span data-grammar-focus=&quot;false&quot; data-grammar-id=&quot;grammar77&quot; data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;안되는&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;안 되는&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;space&amp;quot;}&quot;&gt;안되는&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;걸 보니 왜 실제 b2b si 등에서 rag 챗봇 구축할 때 poc나 업체간 BMT 들을 많이 요구하는지 느꼈다. (실제 쓸 땐 생각보다 성능이&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span data-grammar-focus=&quot;false&quot; data-grammar-id=&quot;grammar78&quot; data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;안나올&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;안 나올&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;space&amp;quot;}&quot;&gt;안나올&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;수 있으니까)&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- 또 이런 에러 케이스들을&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span data-grammar-focus=&quot;false&quot; data-grammar-id=&quot;grammar79&quot; data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;한땀한땀&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;한 땀 한 땀&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;space&amp;quot;}&quot;&gt;한땀한땀&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;맞춤형으로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span data-grammar-focus=&quot;false&quot; data-grammar-id=&quot;grammar80&quot; data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;수정해나갈&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;수정해 나갈&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;space&amp;quot;}&quot;&gt;수정해나갈&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span data-grammar-focus=&quot;false&quot; data-grammar-id=&quot;grammar81&quot; data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;수 밖에&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;수밖에&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;space&amp;quot;}&quot;&gt;수 밖에&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;없기에, 결국에 rag 구성하는데 완전 자동화는 쉽지 않고 사람 손을 다&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span data-grammar-focus=&quot;false&quot; data-grammar-id=&quot;grammar82&quot; data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;타야한다 라고&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;타야 한다라고&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;space&amp;quot;}&quot;&gt;타야한다 라고&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;하는지에 대한 이유를 좀 느낄 수 있었던 거 같다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- Advanced&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span data-grammar-focus=&quot;false&quot; data-grammar-id=&quot;grammar83&quot; data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;rag 라고&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;rag라고&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;space&amp;quot;}&quot;&gt;rag 라고&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;하면서 다양한 기법들이 나오게 된 이유도 조금은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span data-grammar-focus=&quot;false&quot; data-grammar-id=&quot;grammar84&quot; data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;알것&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;알 것&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;space&amp;quot;}&quot;&gt;알것&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;같기도 하고&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;마무리&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이번&amp;nbsp;파트에서는&amp;nbsp;RAG의&amp;nbsp;가장&amp;nbsp;기본적인&amp;nbsp;형태를&amp;nbsp;처음부터&amp;nbsp;끝까지&amp;nbsp;직접&amp;nbsp;&lt;span data-grammar-focus=&quot;false&quot; data-grammar-id=&quot;grammar85&quot; data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;구성해보면서,&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;구성해 보면서,&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;space&amp;quot;}&quot;&gt;구성해보면서,&lt;/span&gt;&amp;nbsp;&amp;nbsp;&lt;br /&gt;&amp;ldquo;LLM이&amp;nbsp;내부&amp;nbsp;문서를&amp;nbsp;참고해서&amp;nbsp;답하게&amp;nbsp;만든다&amp;rdquo;는&amp;nbsp;말이&amp;nbsp;실제로&amp;nbsp;어떤&amp;nbsp;구조로&amp;nbsp;구현되는지&amp;nbsp;감을&amp;nbsp;잡을&amp;nbsp;수&amp;nbsp;있었다.&lt;br /&gt;&lt;br /&gt;처음에는&amp;nbsp;단순&amp;nbsp;문자열&amp;nbsp;매칭&amp;nbsp;기반으로&amp;nbsp;시작했지만,&amp;nbsp;&amp;nbsp;&lt;br /&gt;이름으로는&amp;nbsp;검색이&amp;nbsp;안&amp;nbsp;되거나,&amp;nbsp;오타에&amp;nbsp;취약하거나,&amp;nbsp;표현이&amp;nbsp;조금만&amp;nbsp;달라져도&amp;nbsp;실패하는&amp;nbsp;모습을&amp;nbsp;보면서&amp;nbsp;&amp;nbsp;&lt;br /&gt;왜&amp;nbsp;의미&amp;nbsp;기반&amp;nbsp;검색이&amp;nbsp;필요한지&amp;nbsp;자연스럽게&amp;nbsp;이해하게&amp;nbsp;됐다.&amp;nbsp;&amp;nbsp;&lt;br /&gt;&lt;span data-grammar-focus=&quot;false&quot; data-grammar-id=&quot;grammar86&quot; data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;그&amp;nbsp;다음&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;그다음&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;space&amp;quot;}&quot;&gt;그&amp;nbsp;다음&lt;/span&gt;&amp;nbsp;단계로&amp;nbsp;문서를&amp;nbsp;&lt;span data-grammar-focus=&quot;false&quot; data-grammar-id=&quot;grammar87&quot; data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;청킹하고,&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;청킹 하고,&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;space&amp;quot;}&quot;&gt;청킹하고,&lt;/span&gt;&amp;nbsp;임베딩하고,&amp;nbsp;벡터&amp;nbsp;DB에&amp;nbsp;저장하고,&amp;nbsp;&amp;nbsp;&lt;br /&gt;검색&amp;nbsp;결과를&amp;nbsp;프롬프트에&amp;nbsp;주입하는&amp;nbsp;흐름까지&amp;nbsp;&lt;span data-grammar-focus=&quot;false&quot; data-grammar-id=&quot;grammar88&quot; data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;구현해보니&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;구현해 보니&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;space&amp;quot;}&quot;&gt;구현해보니&lt;/span&gt;&amp;nbsp;RAG의&amp;nbsp;기본&amp;nbsp;구조가&amp;nbsp;훨씬&amp;nbsp;또렷해졌다.&lt;br /&gt;&lt;br /&gt;다만&amp;nbsp;동시에,&amp;nbsp;RAG는&amp;nbsp;생각보다&amp;nbsp;쉽게&amp;nbsp;&lt;span data-grammar-focus=&quot;false&quot; data-grammar-id=&quot;grammar89&quot; data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;&amp;ldquo;완성됐다&amp;rdquo;고&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;&amp;ldquo;완성됐다&amp;rdquo;라고&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;spell&amp;quot;}&quot;&gt;&amp;ldquo;완성됐다&amp;rdquo;고&lt;/span&gt;&amp;nbsp;말하기&amp;nbsp;어려운&amp;nbsp;시스템이라는&amp;nbsp;점도&amp;nbsp;느꼈다.&amp;nbsp;&amp;nbsp;&lt;br /&gt;실제로&amp;nbsp;돌려보면&amp;nbsp;청크&amp;nbsp;경계,&amp;nbsp;복합&amp;nbsp;질문,&amp;nbsp;검색&amp;nbsp;품질&amp;nbsp;저하&amp;nbsp;같은&amp;nbsp;문제가&amp;nbsp;계속&amp;nbsp;나타나고,&amp;nbsp;&amp;nbsp;&lt;br /&gt;이런 부분을 하나씩 다듬어가며 개선해야 비로소 실사용에&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span data-grammar-focus=&quot;false&quot; data-grammar-id=&quot;grammar90&quot; data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;가까워질수 밖에&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;가까워질 수밖에&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;space&amp;quot;}&quot;&gt;가까워질수 밖에&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;없다는 것을 알게 됐다.&lt;br /&gt;&lt;br /&gt;다음&amp;nbsp;글에서는&amp;nbsp;이런&amp;nbsp;한계를&amp;nbsp;어떻게&amp;nbsp;보완할&amp;nbsp;수&amp;nbsp;있는지,&amp;nbsp;&amp;nbsp;&lt;br /&gt;즉 Advanced RAG, RAG평가 등 다양한 RAG 개선 기법들을 중심으로 이어서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span data-grammar-focus=&quot;false&quot; data-grammar-id=&quot;grammar91&quot; data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;정리해볼&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;정리해 볼&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;space&amp;quot;}&quot;&gt;정리해볼&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;예정이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;(꾸준히 공부하고&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span data-grammar-focus=&quot;false&quot; data-grammar-id=&quot;grammar92&quot; data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;적을테니&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;적을 테니&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;space&amp;quot;}&quot;&gt;적을테니&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;많은 관심 부탁드립니다.)&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;Profile:&lt;/span&gt;&lt;br /&gt;&lt;a style=&quot;color: #0070d1; text-align: start;&quot; href=&quot;http://www.linkedin.com/in/doobeom-kim-%EA%B9%80%EB%91%90%EB%B2%94-2b9649242&quot;&gt;&lt;span&gt;&lt;span style=&quot;color: #0070d1;&quot;&gt;Linkedin&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>AI 공부</category>
      <category>Agent</category>
      <category>ai</category>
      <category>AI 공부</category>
      <category>LangChain</category>
      <category>llm</category>
      <category>Rag</category>
      <category>RAG 챗봇</category>
      <category>랭체인</category>
      <category>벡터 db</category>
      <category>임베딩</category>
      <author>kdb1248</author>
      <guid isPermaLink="true">https://doobeom-coding.tistory.com/87</guid>
      <comments>https://doobeom-coding.tistory.com/87#entry87comment</comments>
      <pubDate>Sun, 19 Apr 2026 17:20:55 +0900</pubDate>
    </item>
    <item>
      <title>9. LLM 평가(Evaluation) - 벤치마크&amp;middot;리더보드 정리부터 9개 모델 Python -&amp;gt; C++ 포팅 실험까지</title>
      <link>https://doobeom-coding.tistory.com/86</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;background-color: #99cefa;&quot;&gt;목차&amp;nbsp;&lt;/span&gt;&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;1. 주요 벤치마크와 벤치마크의 한계점&amp;nbsp;&lt;br&gt;2. 주요 리더보드 사이트&lt;br&gt;3. 프론티어 모델별 코드 생성능력 (Python to C++ 포팅 성능 비교)&lt;br&gt;4. 오픈소스 모델 포함 생성능력 비교 ( Python to C++ 포팅 성능 비교, w/ Gradio UI)&lt;br&gt;5. 생성형 AI 솔루션 성능 평가를 어떻게 할 것인가?&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글은 모델 평가(Evaluation) 관련 챕터를 공부했던 글로&amp;nbsp;&lt;br&gt;1) 주요 벤치마크들과 리더보드 사이트에 대해 공부해 본 내용&amp;nbsp;&lt;br&gt;2) 실제로 다양한 closed 모델, open source 모델들의 성능을 코드 생성능력 task를 직접 시켜보며 비교해 본 내용&amp;nbsp;&lt;br&gt;을 작성해보고자 한다.&amp;nbsp;&lt;/p&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 주요 벤치마크와 벤치마크의 한계점&amp;nbsp;&lt;/h2&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;1) 주요 벤치마크&lt;/h3&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;903&quot; data-origin-height=&quot;529&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/r7ehK/dJMcabDO982/8q2xRg5NTlIx3SLl3TjtIK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/r7ehK/dJMcabDO982/8q2xRg5NTlIx3SLl3TjtIK/img.png&quot; data-alt=&quot;주요 6개 벤치마크&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/r7ehK/dJMcabDO982/8q2xRg5NTlIx3SLl3TjtIK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fr7ehK%2FdJMcabDO982%2F8q2xRg5NTlIx3SLl3TjtIK%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;903&quot; height=&quot;529&quot; data-origin-width=&quot;903&quot; data-origin-height=&quot;529&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;주요 6개 벤치마크&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1)- 1. 6개 벤치마크 등장 배경&amp;nbsp;&lt;/h4&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
 &lt;li&gt;MMLU, GSM8K, HumanEval 같은 기존 벤치마크&lt;br&gt;→ &lt;b&gt;GPT-4급 모델들이 거의 만점&lt;/b&gt;&lt;/li&gt; 
 &lt;li&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;/li&gt; 
&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;=&amp;gt; 그에따라 더 어렵고, 더 인간 상위권 수준의 벤치마크 지표가 개발됨&lt;br&gt;이런 벤치마크를 맹신해서는 안되지만 특정 작업에 맞는 모델을 고르는 데 있어 starting point로 적절&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;1)- 2. 6개 벤치마크별 상세 설명&amp;nbsp;&lt;/h4&gt;&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt; 
 &lt;tbody&gt; 
  &lt;tr&gt; 
   &lt;td style=&quot;width: 20%;&quot;&gt;&amp;nbsp;&lt;/td&gt; 
   &lt;td style=&quot;width: 20%;&quot;&gt;1) 평가항목&lt;/td&gt; 
   &lt;td style=&quot;width: 20%;&quot;&gt;어떤 벤치마크?&lt;/td&gt; 
   &lt;td style=&quot;width: 20%;&quot;&gt;해석 포인트&lt;/td&gt; 
  &lt;/tr&gt; 
  &lt;tr&gt; 
   &lt;td style=&quot;width: 20%;&quot;&gt;1. GPQA &lt;br&gt;(google proof q&amp;amp;A)&lt;br&gt;&lt;br&gt;-&amp;gt; PhD급 과학 전문성&lt;/td&gt; 
   &lt;td style=&quot;width: 20%;&quot;&gt;PhD Science Expertise&lt;/td&gt; 
   &lt;td style=&quot;width: 20%;&quot;&gt; 
    &lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
     &lt;li&gt;생물·화학·물리 등&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;박사급 과학 질문 448개&lt;/b&gt;&lt;/li&gt; 
     &lt;li&gt;구글링 가능해도: 
      &lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
       &lt;li&gt;&lt;b&gt;비전공 인간 평균 정답률: 34%&lt;/b&gt;&lt;/li&gt; 
       &lt;li&gt;&lt;b&gt;65%가 박사급&lt;/b&gt;&lt;/li&gt; 
       &lt;li&gt;gpt 4o 가 39% 정도&lt;/li&gt; 
      &lt;/ul&gt; &lt;/li&gt; 
     &lt;li&gt;단순 검색 ❌&lt;/li&gt; 
     &lt;li&gt;&lt;b&gt;전문 지식 + 개념 연결 능력&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;필요&lt;/li&gt; 
    &lt;/ul&gt; &lt;/td&gt; 
   &lt;td style=&quot;width: 20%;&quot;&gt; 
    &lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
     &lt;li&gt;&lt;span&gt;GPQA 점수 높다 =&lt;/span&gt;&lt;br&gt;&lt;span&gt;  &lt;b&gt;“이 모델은 전문가 역할을 어느 정도 대체 가능”&lt;/b&gt;&lt;/span&gt;&lt;/li&gt; 
    &lt;/ul&gt; &lt;/td&gt; 
  &lt;/tr&gt; 
  &lt;tr&gt; 
   &lt;td style=&quot;width: 20%;&quot;&gt;2. MMLU-PRO&lt;br&gt;&lt;br&gt;-&amp;gt; 언어 이해력&lt;/td&gt; 
   &lt;td style=&quot;width: 20%;&quot;&gt;&lt;b&gt; &lt;span data-token-index=&quot;0&quot;&gt;Language Understanding&lt;/span&gt; &lt;/b&gt;&lt;/td&gt; 
   &lt;td style=&quot;width: 20%;&quot;&gt;&lt;b&gt;기존 MMLU와의 차이&amp;nbsp;&lt;/b&gt;&lt;br&gt; 
    &lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
     &lt;li&gt;4지선다-&amp;gt; 10지 선다&lt;/li&gt; 
     &lt;li&gt;노이즈 많음 -&amp;gt; 정제된 문제&lt;/li&gt; 
     &lt;li&gt;패턴 추측 가능 -&amp;gt; 추측 불가&lt;/li&gt; 
    &lt;/ul&gt; &lt;/td&gt; 
   &lt;td style=&quot;width: 20%;&quot;&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;b&gt;의미 기반 이해 / 문맥 유지 능력&lt;/b&gt; 측정&lt;br&gt; 
      &lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
       &lt;li&gt;챗봇, 문서 요약, 정책 분석 등&lt;br&gt;→ &lt;b&gt;언어 정확도가 중요한 서비스용 모델 평가에 적합&lt;/b&gt;&lt;/li&gt; 
      &lt;/ul&gt; &lt;/li&gt; 
    &lt;/ul&gt; &lt;/td&gt; 
  &lt;/tr&gt; 
  &lt;tr&gt; 
   &lt;td style=&quot;width: 20%;&quot;&gt;3. AIME &lt;br&gt;&lt;br&gt;-&amp;gt; 상위 1% 수학 사고력&lt;/td&gt; 
   &lt;td style=&quot;width: 20%;&quot;&gt;&lt;span data-token-index=&quot;0&quot;&gt;Math (Reasoning 중심)&lt;/span&gt;&lt;/td&gt; 
   &lt;td style=&quot;width: 20%;&quot;&gt; 
    &lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
     &lt;li&gt;미국 &lt;b&gt;AIME 수학 경시대회&lt;/b&gt;&lt;/li&gt; 
     &lt;li&gt;초엘리트 고등학생 대상&lt;/li&gt; 
     &lt;li&gt;계산 ❌&lt;/li&gt; 
     &lt;li&gt;&lt;b&gt;&lt;b&gt;수학적 사고 + 증명 아이디어&lt;br&gt;&lt;/b&gt;&lt;/b&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;/li&gt; 
    &lt;/ul&gt; &lt;/td&gt; 
   &lt;td style=&quot;width: 20%;&quot;&gt; 
    &lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
     &lt;li&gt;기존 주요 벤치마크였던 GSM8K는 “초중등 수학”&lt;/li&gt; 
     &lt;li&gt;AIME는 &lt;b&gt;&lt;b&gt;“수학적 사고력 그 자체”&lt;br&gt;&lt;/b&gt;&lt;/b&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;/ul&gt; &lt;/li&gt; 
    &lt;/ul&gt; &lt;/td&gt; 
  &lt;/tr&gt; 
  &lt;tr&gt; 
   &lt;td style=&quot;width: 20%;&quot;&gt;4. LiveCodeBench &lt;br&gt;&lt;br&gt;-&amp;gt; 코딩 실력&lt;/td&gt; 
   &lt;td style=&quot;width: 20%;&quot;&gt;&lt;span data-token-index=&quot;0&quot;&gt;Coding (실전형)&lt;/span&gt;&lt;/td&gt; 
   &lt;td style=&quot;width: 20%;&quot;&gt; 
    &lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
     &lt;li&gt;LeetCode / AtCoder / Codeforces 문제 기반&lt;/li&gt; 
     &lt;li&gt;단순 함수 구현 ❌&lt;/li&gt; 
     &lt;li&gt;&lt;b&gt;&lt;b&gt;시간 복잡도, 엣지 케이스 고려&lt;br&gt;&lt;/b&gt;&lt;/b&gt; 
      &lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
       &lt;li&gt;HumanEval은 “toy problem”&lt;/li&gt; 
       &lt;li&gt;LiveCodeBench는 &lt;b&gt;대회급 문제&lt;/b&gt;&lt;/li&gt; 
      &lt;/ul&gt; &lt;/li&gt; 
    &lt;/ul&gt; &lt;/td&gt; 
   &lt;td style=&quot;width: 20%;&quot;&gt; 
    &lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
     &lt;li&gt;코드 생성 AI의: 
      &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;br&gt;&lt;br&gt;&lt;/li&gt; 
      &lt;/ul&gt; &lt;/li&gt; 
    &lt;/ul&gt; &lt;/td&gt; 
  &lt;/tr&gt; 
  &lt;tr&gt; 
   &lt;td style=&quot;width: 20%;&quot;&gt;5. MuSR&lt;br&gt;&lt;br&gt;-&amp;gt; 추론 성능&lt;/td&gt; 
   &lt;td style=&quot;width: 20%;&quot;&gt;&lt;span data-token-index=&quot;0&quot;&gt;Multi-Step Reasoning&lt;/span&gt;&lt;/td&gt; 
   &lt;td style=&quot;width: 20%;&quot;&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;/ul&gt; &lt;br&gt;&lt;br&gt;예시문제&amp;nbsp;&lt;br&gt; 
    &lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
     &lt;li&gt;&lt;span&gt;1,000단어짜리 &lt;b&gt;추리소설&lt;/b&gt;&lt;/span&gt;&lt;/li&gt; 
     &lt;li&gt;&lt;span&gt;질문: &lt;br&gt;“누가 수단(means), 동기(motive), 기회(opportunity)를 가졌는가?”&lt;/span&gt;&lt;/li&gt; 
    &lt;/ul&gt; &lt;/td&gt; 
   &lt;td style=&quot;width: 20%;&quot;&gt; 
    &lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
     &lt;li&gt;Chain-of-Thought의 &lt;b&gt;질적 수준&lt;/b&gt; 평가&lt;/li&gt; 
     &lt;li&gt;단순 reasoning 흉내 ❌&lt;/li&gt; 
    &lt;/ul&gt; &lt;/td&gt; 
  &lt;/tr&gt; 
  &lt;tr&gt; 
   &lt;td style=&quot;width: 20%;&quot;&gt;6. HLE&lt;br&gt;&lt;br&gt;-&amp;gt; 초인적 지능 평가&lt;/td&gt; 
   &lt;td style=&quot;width: 20%;&quot;&gt;&lt;span data-token-index=&quot;0&quot;&gt; &lt;span data-token-index=&quot;0&quot;&gt;Super-human Intelligence&lt;/span&gt; &lt;/span&gt;&lt;/td&gt; 
   &lt;td style=&quot;width: 20%;&quot;&gt; 
    &lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
     &lt;li&gt;2,500문제&lt;/li&gt; 
     &lt;li&gt;멀티모달 (텍스트+이미지 등)&lt;/li&gt; 
     &lt;li&gt;출제 의도: “AI를 위한 마지막 학술 시험”&lt;br&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;b&gt;사고 과정 자체&lt;/b&gt;가 관건&lt;/li&gt; 
      &lt;/ul&gt; &lt;/li&gt; 
    &lt;/ul&gt; &lt;/td&gt; 
   &lt;td style=&quot;width: 20%;&quot;&gt; 
    &lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
     &lt;li&gt;이건 실무용보다는: 
      &lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
       &lt;li&gt;AGI 근접성&lt;/li&gt; 
       &lt;li&gt;연구용 벤치마크&lt;br&gt;-&amp;gt; “이 모델이 인간 지능을 넘었는가?”를 묻는 실험장&lt;br&gt;-&amp;gt; 2024에는 모델들이 2~3%&lt;/li&gt; 
      &lt;/ul&gt; &lt;/li&gt; 
    &lt;/ul&gt; &lt;/td&gt; 
  &lt;/tr&gt; 
 &lt;/tbody&gt; 
&lt;/table&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;2) 벤치마크의 한계점&amp;nbsp;&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;1. Training data contamination (훈련데이터 오염)&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;/ul&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;2. Not consistently applied&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;/ul&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;3. Too narrow in scope&lt;/p&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;&lt;li&gt;벤치마크가 너무 세부적인 영역(ex. 화학) 집중하는 케이스도 있음&lt;/li&gt;&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;4. Hard to measure nuanced reasoning&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 data-ke-size=&quot;size16&quot;&gt;5. Satuartion (포화도)&amp;nbsp;&lt;/p&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;&lt;li&gt;이미 99점 이상 맞는 벤치마크도 많음&lt;/li&gt;&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;6. Overfitting&amp;nbsp;&amp;nbsp;&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;/ul&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;7. (증명 안됨) frontier llm은 그들이 평가되고 있다는 걸 인지해서 답변을 왜곡할 수도 있음&amp;nbsp;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;3) 느낀 점&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 ai 트렌드 관련한 기사들을 보다 보면 새로 나온 이 모델의 벤치마크 성능이 어떻네?라는 식의 내용들을 많이 접하게 된다.&amp;nbsp;&lt;br&gt;요새는 모델들의 벤치마크 성능이 워낙 상향 평준화 됐기도 하고, 단순 모델 성능보다 에이전트를 위한 하네스를 어떻게 설계하냐 그로 인한 에이전트 쪽 실행능력이 얼마나 뛰어나냐가 더 중요해지고 있긴 하다. &lt;br&gt;(실제로 관련된 벤치마크들도 계속 새로 나오기도 하고)&lt;br&gt;&amp;nbsp;&lt;br&gt;그래도 일반적인 모델 성능을 측정할 때 주로 쓰는 벤치마크들에 대해 한번 정리해 볼 수 있는 지점이었다.&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 주요 리더보드 사이트&amp;nbsp;&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 소개한 벤치마크들을 포함해서 다양한 지표로 모델들을 비교하는&amp;nbsp;&lt;br&gt;주요 리더보드 사이트 5곳에 대해서 알아봤다.&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;1) 주요 리더 보드 사이트&amp;nbsp;&lt;/h3&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;1)-1. Artificial Analysis&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;기사에 자주 등장하고, 최근에 독파모 사업 관련해서도 &quot;특정회사의 모델이 Artificial Analysis 순위 몇 등에 들었다&quot; 식으로 기사에 많이 보도되기도 한다.&amp;nbsp;&lt;br&gt;&lt;a href=&quot;https://artificialanalysis.ai/&quot; target=&quot;_blank&quot;&gt;&lt;span&gt;https://artificialanalysis.ai/&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;&lt;figure data-ke-type=&quot;opengraph&quot; data-og-title=&quot;AI Model &amp;amp; API Providers Analysis | Artificial Analysis&quot; data-ke-align=&quot;alignCenter&quot; data-og-description=&quot;Comparison and analysis of AI models and API hosting providers. Independent benchmarks across key performance metrics including quality, price, output speed &amp;amp; latency.&quot; data-og-host=&quot;artificialanalysis.ai&quot; data-og-source-url=&quot;https://artificialanalysis.ai/&quot; data-og-image=&quot;https://blog.kakaocdn.net/dna/bgHyhR/dJMb8YpWzL1/AAAAAAAAAAAAAAAAAAAAAIvIIC88qhFnG7L_bf5QCObDNhxU2ii7bkUPCsWMLwq9/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1777561199&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=IDV79iJKdBn05zFgz6yBGigaggM%3D&quot; data-og-url=&quot;https://artificialanalysis.ai/&quot;&gt;&lt;a href=&quot;https://artificialanalysis.ai/&quot; target=&quot;_blank&quot; data-source-url=&quot;https://artificialanalysis.ai/&quot;&gt;&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://blog.kakaocdn.net/dna/bgHyhR/dJMb8YpWzL1/AAAAAAAAAAAAAAAAAAAAAIvIIC88qhFnG7L_bf5QCObDNhxU2ii7bkUPCsWMLwq9/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1777561199&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=IDV79iJKdBn05zFgz6yBGigaggM%3D')&quot;&gt; &lt;/div&gt;&lt;div class=&quot;og-text&quot;&gt;&lt;p class=&quot;og-title&quot;&gt;AI Model &amp;amp; API Providers Analysis | Artificial Analysis&lt;/p&gt;&lt;p class=&quot;og-desc&quot;&gt;Comparison and analysis of AI models and API hosting providers. Independent benchmarks across key performance metrics including quality, price, output speed &amp;amp; latency.&lt;/p&gt;&lt;p class=&quot;og-host&quot;&gt;artificialanalysis.ai&lt;/p&gt;&lt;/div&gt;&lt;/a&gt;&lt;/figure&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;아래의 10개의 벤치마크 지표를 기준으로 최근엔 순위가 매겨진다.&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;몇 달 전까지만 해도 10개의 벤치마크 지표에 위에서 서술한 MMLU-Pro, Live code bench, AIME 등도 포함되어 있었으나&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;해당 지표들도 이미 너무 상향 평준화가 되었는지 해당 지표들이 빠지고 다른 지표들로 대체가 되었다.&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;- 10 evaluations: &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;GDPval-AA,  ²-Bench Telecom, Terminal-Bench Hard, SciCode, AA-LCR, AA-Omniscience, IFBench, Humanity's Last Exam, GPQA Diamond, CritPt &lt;/span&gt;&lt;/span&gt;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1466&quot; data-origin-height=&quot;775&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cOJgl8/dJMcadVTsLO/cYkorEkh3ZDGimNg2JlRXk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cOJgl8/dJMcadVTsLO/cYkorEkh3ZDGimNg2JlRXk/img.png&quot; data-alt=&quot;2026.04.12 기준 순위&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cOJgl8/dJMcadVTsLO/cYkorEkh3ZDGimNg2JlRXk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcOJgl8%2FdJMcadVTsLO%2FcYkorEkh3ZDGimNg2JlRXk%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;1466&quot; height=&quot;775&quot; data-origin-width=&quot;1466&quot; data-origin-height=&quot;775&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;2026.04.12 기준 순위&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;span style=&quot;color: #000000;&quot;&gt;순위 외에 &lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Artificial intelligence index를 돌리기 위해 각 모델별로 필요한 cost와 각 모델별 점수를 비교해 놓은 그래프도 볼 수 있다.&lt;/span&gt;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1477&quot; data-origin-height=&quot;780&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3K6f3/dJMcaadQ3Uh/vOI3SosC9aQNpPHOd7ECR1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3K6f3/dJMcaadQ3Uh/vOI3SosC9aQNpPHOd7ECR1/img.png&quot; data-alt=&quot;(2026.04.12 기준) Artificial intelligence index 를 돌리기 위해 각 모델별로 필요한 cost와 각 모델별 점수를 비교해놓은 그래프&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3K6f3/dJMcaadQ3Uh/vOI3SosC9aQNpPHOd7ECR1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3K6f3%2FdJMcaadQ3Uh%2FvOI3SosC9aQNpPHOd7ECR1%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;1477&quot; height=&quot;780&quot; data-origin-width=&quot;1477&quot; data-origin-height=&quot;780&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;(2026.04.12 기준) Artificial intelligence index 를 돌리기 위해 각 모델별로 필요한 cost와 각 모델별 점수를 비교해놓은 그래프&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1)-2. Vellum&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;Vellum의 경우 AA와 유사하게 리더보드를 볼 수 있고 모델별 비교를 하기 좋은 사이트이다.&lt;br&gt;&lt;a href=&quot;https://www.vellum.ai/llm-leaderboard?utm_source=google&amp;amp;utm_medium=organic&quot; target=&quot;_blank&quot;&gt;&lt;span&gt;https://www.vellum.ai/llm-leaderboard?utm_source=google&amp;amp;utm_medium=organic&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;&lt;figure data-ke-type=&quot;opengraph&quot; data-og-title=&quot;LLM Leaderboard 2025&quot; data-ke-align=&quot;alignCenter&quot; data-og-description=&quot;This AI leaderboard shows comparison of capabilities, price and context window for leading commercial and open-source LLMs, based on the benchmark data provided in technical reports in 2025.&quot; data-og-host=&quot;www.vellum.ai&quot; data-og-source-url=&quot;https://www.vellum.ai/llm-leaderboard?utm_source=google&amp;amp;utm_medium=organic&quot; data-og-image=&quot;https://blog.kakaocdn.net/dna/bpEPwr/dJMb83kt7hm/AAAAAAAAAAAAAAAAAAAAALyj9QAD7ejjzG0QhIqUz9KKovvwnYRSa67gxQ9dJBss/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1777561199&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=ml4pZCYheyIBySFRtYxUnNcBpu8%3D&quot; data-og-url=&quot;https://www.vellum.ai/llm-leaderboard&quot;&gt;&lt;a href=&quot;https://www.vellum.ai/llm-leaderboard&quot; target=&quot;_blank&quot; data-source-url=&quot;https://www.vellum.ai/llm-leaderboard?utm_source=google&amp;amp;utm_medium=organic&quot;&gt;&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://blog.kakaocdn.net/dna/bpEPwr/dJMb83kt7hm/AAAAAAAAAAAAAAAAAAAAALyj9QAD7ejjzG0QhIqUz9KKovvwnYRSa67gxQ9dJBss/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1777561199&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=ml4pZCYheyIBySFRtYxUnNcBpu8%3D')&quot;&gt; &lt;/div&gt;&lt;div class=&quot;og-text&quot;&gt;&lt;p class=&quot;og-title&quot;&gt;LLM Leaderboard 2025&lt;/p&gt;&lt;p class=&quot;og-desc&quot;&gt;This AI leaderboard shows comparison of capabilities, price and context window for leading commercial and open-source LLMs, based on the benchmark data provided in technical reports in 2025.&lt;/p&gt;&lt;p class=&quot;og-host&quot;&gt;www.vellum.ai&lt;/p&gt;&lt;/div&gt;&lt;/a&gt;&lt;/figure&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;아래 이미지와 같이 모델 2개 간 벤치마크를 서로 비교해 볼 수 있고,&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1474&quot; data-origin-height=&quot;750&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/7w2vk/dJMb99TwLSc/I5QFxJR0bKdioHxDqeHP00/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/7w2vk/dJMb99TwLSc/I5QFxJR0bKdioHxDqeHP00/img.png&quot; data-alt=&quot;(2026.04.12 기준) 클로드 모델간 비교&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/7w2vk/dJMb99TwLSc/I5QFxJR0bKdioHxDqeHP00/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F7w2vk%2FdJMb99TwLSc%2FI5QFxJR0bKdioHxDqeHP00%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;1474&quot; height=&quot;750&quot; data-origin-width=&quot;1474&quot; data-origin-height=&quot;750&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;(2026.04.12 기준) 클로드 모델간 비교&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;특히나 모델별 context window와 비용을 표로 바로 비교해볼 수 있어 어떤 모델을 쓸지 선택하는 데 있어 참고하기 유용하다&lt;br&gt;(물론 openrouter나, Groq 등의 플랫폼을 써서 모델을 사용하는 경우에도 이를 바로 볼 수 있겠지만&amp;nbsp;&lt;br&gt;특정 모델사에서 바로 API를 써야 할 땐 이 표를 보고 참고하는 것도 괜찮을 듯하다)&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1177&quot; data-origin-height=&quot;754&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b9OyDc/dJMcaaZctQX/zi91spK8qPddpBlim8SCa0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b9OyDc/dJMcaaZctQX/zi91spK8qPddpBlim8SCa0/img.png&quot; data-alt=&quot;(2026.04.12 기준) 모델별 Context window와 cost&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b9OyDc/dJMcaaZctQX/zi91spK8qPddpBlim8SCa0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb9OyDc%2FdJMcaaZctQX%2Fzi91spK8qPddpBlim8SCa0%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;1177&quot; height=&quot;754&quot; data-origin-width=&quot;1177&quot; data-origin-height=&quot;754&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;(2026.04.12 기준) 모델별 Context window와 cost&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1)-3. Seal&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;다양한 전문 분야에 대해 구체적으로 테스트 가능&amp;nbsp;&lt;br&gt;매우 전문적인 작업하는 경우 자신에게 해당하는 Seal 리더보드 있는지 확인할 때 유용&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;a href=&quot;https://labs.scale.com/leaderboard&quot; target=&quot;_blank&quot;&gt;&lt;span&gt;https://labs.scale.com/leaderboard&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;&lt;figure data-ke-type=&quot;opengraph&quot; data-og-title=&quot;AI Model Leaderboards &amp;amp; Benchmarks&quot; data-ke-align=&quot;alignCenter&quot; data-og-description=&quot;Explore leaderboards with expert-driven LLM benchmarks and updated AI model rankings across coding, reasoning and more.&quot; data-og-host=&quot;labs.scale.com&quot; data-og-source-url=&quot;https://labs.scale.com/leaderboard&quot; data-og-url=&quot;https://labs.scale.com/leaderboard&quot;&gt;&lt;a href=&quot;https://labs.scale.com/leaderboard&quot; target=&quot;_blank&quot; data-source-url=&quot;https://labs.scale.com/leaderboard&quot;&gt;&lt;div class=&quot;og-image&quot;&gt;&lt;/div&gt;&lt;div class=&quot;og-text&quot;&gt;&lt;p class=&quot;og-title&quot;&gt;AI Model Leaderboards &amp;amp; Benchmarks&lt;/p&gt;&lt;p class=&quot;og-desc&quot;&gt;Explore leaderboards with expert-driven LLM benchmarks and updated AI model rankings across coding, reasoning and more.&lt;/p&gt;&lt;p class=&quot;og-host&quot;&gt;labs.scale.com&lt;/p&gt;&lt;/div&gt;&lt;/a&gt;&lt;/figure&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;HLE 벤치마크에 대한 자세한 정보를 볼 수도 있고&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1552&quot; data-origin-height=&quot;730&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/QLfQP/dJMcagZpqCD/HwNXRCb3pJwkvBHGfpWdmk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/QLfQP/dJMcagZpqCD/HwNXRCb3pJwkvBHGfpWdmk/img.png&quot; data-alt=&quot;HLE 벤치마크에 대한 자세한 정보&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/QLfQP/dJMcagZpqCD/HwNXRCb3pJwkvBHGfpWdmk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQLfQP%2FdJMcagZpqCD%2FHwNXRCb3pJwkvBHGfpWdmk%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;1552&quot; height=&quot;730&quot; data-origin-width=&quot;1552&quot; data-origin-height=&quot;730&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;HLE 벤치마크에 대한 자세한 정보&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래처럼 특수 TASK별 지표를 볼 수도 있다.&lt;/p&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;&lt;li&gt;MCP ATLAS: mcp 활용한 툴 호출 성능&lt;/li&gt;&lt;li&gt;Fortress: 보안 관련 지표&amp;nbsp;&lt;/li&gt;&lt;li&gt;MASK: 모델이 거짓말하는지를 측정하는 지표&amp;nbsp;&lt;/li&gt;&lt;li&gt;TutorBench: 잘 가르치는 AI인지 평가&lt;/li&gt;&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bh7p9b/dJMcadImdmM/BAKfoCKVTPM3i4yIUKg5Hk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bh7p9b/dJMcadImdmM/BAKfoCKVTPM3i4yIUKg5Hk/img.png&quot; data-origin-width=&quot;1518&quot; data-origin-height=&quot;751&quot; style=&quot;width: 47.6619%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bh7p9b/dJMcadImdmM/BAKfoCKVTPM3i4yIUKg5Hk/img.png&quot; alt=&quot;&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbh7p9b%2FdJMcadImdmM%2FBAKfoCKVTPM3i4yIUKg5Hk%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;1518&quot; height=&quot;751&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bEGTGX/dJMcabw13Dp/XEw1t9NKTqnoYvPQCaQfo1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bEGTGX/dJMcabw13Dp/XEw1t9NKTqnoYvPQCaQfo1/img.png&quot; data-origin-width=&quot;1491&quot; data-origin-height=&quot;687&quot; style=&quot;width: 51.1753%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bEGTGX/dJMcabw13Dp/XEw1t9NKTqnoYvPQCaQfo1/img.png&quot; alt=&quot;&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbEGTGX%2FdJMcabw13Dp%2FXEw1t9NKTqnoYvPQCaQfo1%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;1491&quot; height=&quot;687&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;MCP atlas, fortress&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b9wTP1/dJMcaiJFqTP/QSICU4kYr6EToJCKbjk4ok/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b9wTP1/dJMcaiJFqTP/QSICU4kYr6EToJCKbjk4ok/img.png&quot; data-origin-width=&quot;1519&quot; data-origin-height=&quot;716&quot; style=&quot;width: 51.0577%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b9wTP1/dJMcaiJFqTP/QSICU4kYr6EToJCKbjk4ok/img.png&quot; alt=&quot;&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb9wTP1%2FdJMcaiJFqTP%2FQSICU4kYr6EToJCKbjk4ok%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;1519&quot; height=&quot;716&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bK24iz/dJMcadhkkE9/OjLPTDJ4BwDMWC8XO5ENQ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bK24iz/dJMcadhkkE9/OjLPTDJ4BwDMWC8XO5ENQ1/img.png&quot; data-origin-width=&quot;1485&quot; data-origin-height=&quot;748&quot; style=&quot;width: 47.7795%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bK24iz/dJMcadhkkE9/OjLPTDJ4BwDMWC8XO5ENQ1/img.png&quot; alt=&quot;&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbK24iz%2FdJMcadhkkE9%2FOjLPTDJ4BwDMWC8XO5ENQ1%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;1485&quot; height=&quot;748&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;mask, tutorbench&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1)-4. LiveBench&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;데이터셋 유출 문제가 발생하지 않도록 장치를 해둔 사이트인&amp;nbsp;&lt;br&gt;LiveBench 도 있다.&lt;br&gt;&lt;a href=&quot;https://livebench.ai/#/?highunseenbias=true&quot; target=&quot;_blank&quot;&gt;&lt;span&gt;https://livebench.ai/#/?highunseenbias=true&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;&lt;figure data-ke-type=&quot;opengraph&quot; data-og-title=&quot;LiveBench&quot; data-ke-align=&quot;alignCenter&quot; data-og-host=&quot;livebench.ai&quot; data-og-source-url=&quot;https://livebench.ai/#/?highunseenbias=true&quot; data-og-url=&quot;https://livebench.ai/#/?highunseenbias=true&quot;&gt;&lt;a href=&quot;https://livebench.ai/#/?highunseenbias=true&quot; target=&quot;_blank&quot; data-source-url=&quot;https://livebench.ai/#/?highunseenbias=true&quot;&gt;&lt;div class=&quot;og-image&quot;&gt;&lt;/div&gt;&lt;div class=&quot;og-text&quot;&gt;&lt;p class=&quot;og-title&quot;&gt;LiveBench&lt;/p&gt;&lt;p class=&quot;og-host&quot;&gt;livebench.ai&lt;/p&gt;&lt;/div&gt;&lt;/a&gt;&lt;/figure&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;1)-5. LM Arena&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 LM Arena는 blind human test 즉 blind 상태로 2개의 모델과 채팅해 볼 수 있는 사이트이다.&lt;br&gt;&lt;a href=&quot;https://arena.ai/&quot; target=&quot;_blank&quot;&gt;&lt;span&gt;https://arena.ai/&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;&lt;figure data-ke-type=&quot;opengraph&quot; data-og-title=&quot;Arena | Benchmark &amp;amp; Compare the Best AI Models&quot; data-ke-align=&quot;alignCenter&quot; data-og-description=&quot;Chat with multiple AI models side-by-side. Compare ChatGPT, Claude, Gemini, and other top LLMs. Crowdsourced benchmarks and leaderboards.&quot; data-og-host=&quot;arena.ai&quot; data-og-source-url=&quot;https://arena.ai/&quot; data-og-image=&quot;https://blog.kakaocdn.net/dna/iDiKV/dJMb9bv3tVz/AAAAAAAAAAAAAAAAAAAAAE8HYolauMnlrkdfL66V6sxovFy8Y0SilU-OLdKJk1sy/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1777561199&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=BvCdRIy3%2Br5WsC72HO8oy6BIpBk%3D&quot; data-og-url=&quot;https://arena.ai&quot;&gt;&lt;a href=&quot;https://arena.ai&quot; target=&quot;_blank&quot; data-source-url=&quot;https://arena.ai/&quot;&gt;&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://blog.kakaocdn.net/dna/iDiKV/dJMb9bv3tVz/AAAAAAAAAAAAAAAAAAAAAE8HYolauMnlrkdfL66V6sxovFy8Y0SilU-OLdKJk1sy/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1777561199&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=BvCdRIy3%2Br5WsC72HO8oy6BIpBk%3D')&quot;&gt; &lt;/div&gt;&lt;div class=&quot;og-text&quot;&gt;&lt;p class=&quot;og-title&quot;&gt;Arena | Benchmark &amp;amp; Compare the Best AI Models&lt;/p&gt;&lt;p class=&quot;og-desc&quot;&gt;Chat with multiple AI models side-by-side. Compare ChatGPT, Claude, Gemini, and other top LLMs. Crowdsourced benchmarks and leaderboards.&lt;/p&gt;&lt;p class=&quot;og-host&quot;&gt;arena.ai&lt;/p&gt;&lt;/div&gt;&lt;/a&gt;&lt;/figure&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;2) 느낀 점&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;벤치마크와 마찬가지로 기사에서 많이 봤던 리더보드 사이트들이 실제로 어떻게 생겼고, 각 사이트별로 어떤 기능들을 담고 있는지를 볼 수 있던 게 의미가 있던 거 같다. 특히나 각 세부지표별로 최근엔 어떤 모델들이 강점을 가지고 있나 지켜보는 것도 의미가 있던 거 같다.&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 프론티어 모델별 코드 생성능력 (Python to C++ 포팅 성능 비교)&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;Python 코드를 LLM에게 C++로 변환시킨 뒤, 실제로 컴파일·실행해서&lt;br&gt;3개 모델 (GPT-5, Claude-Sonnet 4.5, gemini 2.5 pro) 중 어떤 모델이 가장 빠른 C++을 생성하는지 비교하는 실험 진행&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;1) 실행과정&amp;nbsp;&lt;/h3&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;1) -1. 시스템 정보 수집&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;현재 컴퓨터의 OS, CPU, 컴파일러 정보를 자동으로 수집&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1786&quot; data-origin-height=&quot;734&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lkK78/dJMcah47f6K/fjjjXgvlME8W76DqTDySYK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lkK78/dJMcah47f6K/fjjjXgvlME8W76DqTDySYK/img.png&quot; data-alt=&quot;현재 컴퓨터 OS, CPU, 컴파일러 정보 수집&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lkK78/dJMcah47f6K/fjjjXgvlME8W76DqTDySYK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlkK78%2FdJMcah47f6K%2FfjjjXgvlME8W76DqTDySYK%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;1786&quot; height=&quot;734&quot; data-origin-width=&quot;1786&quot; data-origin-height=&quot;734&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;현재 컴퓨터 OS, CPU, 컴파일러 정보 수집&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;GPT 5에게 컴파일 방법 질문&lt;br&gt;: 내 시스템에서 C++를 어떻게 컴파일하면 되냐&quot;라고 물어봄&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1786&quot; data-origin-height=&quot;824&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Fxvpe/dJMcaaEV5S0/GuW27U4mNdsl4tAArpE62k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Fxvpe/dJMcaaEV5S0/GuW27U4mNdsl4tAArpE62k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Fxvpe/dJMcaaEV5S0/GuW27U4mNdsl4tAArpE62k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFxvpe%2FdJMcaaEV5S0%2FGuW27U4mNdsl4tAArpE62k%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;1786&quot; height=&quot;824&quot; data-origin-width=&quot;1786&quot; data-origin-height=&quot;824&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;GPT 응답 참고해 컴파일 명령어 설정한 뒤 프롬프트 구성&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1786&quot; data-origin-height=&quot;620&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/blBfJq/dJMcaf7gSs9/7pebuZpT6tJkcaqTHbnEN0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/blBfJq/dJMcaf7gSs9/7pebuZpT6tJkcaqTHbnEN0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/blBfJq/dJMcaf7gSs9/7pebuZpT6tJkcaqTHbnEN0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FblBfJq%2FdJMcaf7gSs9%2F7pebuZpT6tJkcaqTHbnEN0%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;1786&quot; height=&quot;620&quot; data-origin-width=&quot;1786&quot; data-origin-height=&quot;620&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;1) 생성된 c++ 코드를 main.cpp 파일로 저장하는 코드와&amp;nbsp;&lt;br&gt;2) port 함수 정의 (LLM에 c++ 변환요청 -&amp;gt; 응답 텍스트 추출 -&amp;gt; 마크다운 코드블록 제거 -&amp;gt; main.cpp 로 저장)&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1786&quot; data-origin-height=&quot;620&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cAmJjT/dJMcaffbQd1/bO7G36sqdcbAacTX6KMOv0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cAmJjT/dJMcaffbQd1/bO7G36sqdcbAacTX6KMOv0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cAmJjT/dJMcaffbQd1/bO7G36sqdcbAacTX6KMOv0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcAmJjT%2FdJMcaffbQd1%2FbO7G36sqdcbAacTX6KMOv0%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;1786&quot; height=&quot;620&quot; data-origin-width=&quot;1786&quot; data-origin-height=&quot;620&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;1) -2. 테스트용 파이썬 코드 준비&amp;nbsp;&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;라이프니츠 급수를 활용한 원주율(π) 계산 코드를 사용. 2억 번 반복하는 연산 집약적인 코드&lt;br&gt;python 기준으로 10초 정도가 걸렸음&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1786&quot; data-origin-height=&quot;580&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bgE2ix/dJMcacW0QoJ/OKmhC38rgjO4SSpKGWxVK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bgE2ix/dJMcacW0QoJ/OKmhC38rgjO4SSpKGWxVK0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bgE2ix/dJMcacW0QoJ/OKmhC38rgjO4SSpKGWxVK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbgE2ix%2FdJMcacW0QoJ%2FOKmhC38rgjO4SSpKGWxVK0%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;1786&quot; height=&quot;580&quot; data-origin-width=&quot;1786&quot; data-origin-height=&quot;580&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1786&quot; data-origin-height=&quot;344&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cUjSey/dJMcagrAhc1/OqW6GDgpCmwe3LOkjCbOC0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cUjSey/dJMcagrAhc1/OqW6GDgpCmwe3LOkjCbOC0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cUjSey/dJMcagrAhc1/OqW6GDgpCmwe3LOkjCbOC0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcUjSey%2FdJMcagrAhc1%2FOqW6GDgpCmwe3LOkjCbOC0%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;1786&quot; height=&quot;344&quot; data-origin-width=&quot;1786&quot; data-origin-height=&quot;344&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;1) -3. LLM 에 C++ 변환 요청 &amp;amp; 컴파일 &amp;amp; 실행&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;nbsp; GPT-5, Claude Sonnet 4.5, Grok, Gemini 2.5 Pro 각각에게 동일한 Python 코드를 넘기고 &quot;가장 빠른 C++로 변환해 달라&quot;라고 요청&lt;br&gt;-&amp;nbsp; OpenAI는 직접 API를 사용하고, 나머지 2개 모델은 OpenRouter를 통해 하나의 API 키로 통합 호출&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1786&quot; data-origin-height=&quot;100&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IQ2tJ/dJMcacvYY4e/9pczaVpxjqXHgowjr7c2Z0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IQ2tJ/dJMcacvYY4e/9pczaVpxjqXHgowjr7c2Z0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IQ2tJ/dJMcacvYY4e/9pczaVpxjqXHgowjr7c2Z0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIQ2tJ%2FdJMcacvYY4e%2F9pczaVpxjqXHgowjr7c2Z0%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;1786&quot; height=&quot;100&quot; data-origin-width=&quot;1786&quot; data-origin-height=&quot;100&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;GPT 5 기준 3번 돌렸을 때 중앙값 기준으로 50배 정도 빨라짐&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1786&quot; data-origin-height=&quot;770&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BRxY2/dJMcagLUKr1/pmZJIlOVsYMl8MRS4TZtz0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BRxY2/dJMcagLUKr1/pmZJIlOVsYMl8MRS4TZtz0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BRxY2/dJMcagLUKr1/pmZJIlOVsYMl8MRS4TZtz0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBRxY2%2FdJMcagLUKr1%2FpmZJIlOVsYMl8MRS4TZtz0%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;1786&quot; height=&quot;770&quot; data-origin-width=&quot;1786&quot; data-origin-height=&quot;770&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;1) -4. 결과 비교&amp;nbsp;&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;Python 실행 시간 대비 C++ 실행 시간의 속도 향상 배수(speedup)를 모델별로 비교&amp;nbsp;&lt;br&gt;Gemini 2.5 pro&amp;gt; claude sonnet 4.5 &amp;gt; GPT-5 순으로 *성능이 좋은 것을 확인할 수 있었다.&amp;nbsp;&lt;br&gt;(*실행할 때마다의 환경에 따라서도 속도가 달라졌기 때문에 절대적인 순위에 대한 맹신은 x)&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1786&quot; data-origin-height=&quot;498&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c4Y5sX/dJMcadO6MzZ/Cgz6GrkDuacNzrE9cGnQGk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c4Y5sX/dJMcadO6MzZ/Cgz6GrkDuacNzrE9cGnQGk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c4Y5sX/dJMcadO6MzZ/Cgz6GrkDuacNzrE9cGnQGk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc4Y5sX%2FdJMcadO6MzZ%2FCgz6GrkDuacNzrE9cGnQGk%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;1786&quot; height=&quot;498&quot; data-origin-width=&quot;1786&quot; data-origin-height=&quot;498&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 오픈소스 모델 포함 코드 생성능력 비교 ( Python to C++ 포팅 성능 비교, w/ Gradio UI)&lt;/h2&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;1) 실행과정&amp;nbsp;&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;이번엔 위에서 언급한 3개의 프론티어 모델 외에도 오픈소스 모델 몇 개를 추가하여&lt;br&gt;총 9개의 모델을 가지고 동일한 python코드에 대한 c++ 포팅 시 성능을 비교해 봤다.&amp;nbsp;&lt;br&gt;아래 이미지처럼 Gradion UI를 통해 실제 변환된 C++ 코드도 바로 볼 수 있게 진행되었다.&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;실험해 본 9개 모델 리스트는 아래와 같다.&amp;nbsp;&lt;br&gt;오픈소스 모델인 qwen 2.5 coder, deepseek coder, gpt oss 20b 등은 ollama를 통해 로컬에서 돌렸고&amp;nbsp;&lt;br&gt;오픈소스 모델이지만 규모가 큰 gpt oss 120 b는 openrouter를 통해 돌렸다.&amp;nbsp;&lt;br&gt;LGAI의 K EXAONE은 friendli AI를 통해 돌렸다.&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;grok4는 openrouter로, gpt 5, claude sonnet 4.5, gemini 2.5 pro는 각각의 api로 돌렸다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;models = [&quot;gpt-5&quot;, &quot;claude-sonnet-4-5-20250929&quot;, &quot;x-ai/grok-4&quot;, &quot;gemini-2.5-pro&quot;, &quot;qwen2.5-coder&quot;, &quot;deepseek-coder-v2&quot;, &quot;gpt-oss:20b&quot;, &quot;qwen/qwen3-coder-30b-a3b-instruct&quot;, &quot;openai/gpt-oss-120b&quot;, &quot;LGAI-EXAONE/K-EXAONE-236B-A23B&quot;]

clients = {&quot;gpt-5&quot;: openai, &quot;claude-sonnet-4-5-20250929&quot;: anthropic, &quot;x-ai/grok-4&quot;: openrouter, &quot;gemini-2.5-pro&quot;: gemini, &quot;openai/gpt-oss-120b&quot;:&amp;nbsp;&amp;nbsp;openrouter, &quot;qwen2.5-coder&quot;: ollama, &quot;deepseek-coder-v2&quot;: ollama, &quot;gpt-oss:20b&quot;: ollama, &quot;qwen/qwen3-coder-30b-a3b-instruct&quot;: openrouter, &quot;LGAI-EXAONE/K-EXAONE-236B-A23B&quot;: friendli}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c0Qeir/dJMcaadQ5Iz/1CkJvfRpjAEdFQSMKBg1R1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c0Qeir/dJMcaadQ5Iz/1CkJvfRpjAEdFQSMKBg1R1/img.png&quot; data-origin-width=&quot;2578&quot; data-origin-height=&quot;1186&quot; style=&quot;width: 45.3117%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c0Qeir/dJMcaadQ5Iz/1CkJvfRpjAEdFQSMKBg1R1/img.png&quot; alt=&quot;&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc0Qeir%2FdJMcaadQ5Iz%2F1CkJvfRpjAEdFQSMKBg1R1%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;2578&quot; height=&quot;1186&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qdYVO/dJMcadImeUg/kukqO5wJkFplO23ycolTrk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qdYVO/dJMcadImeUg/kukqO5wJkFplO23ycolTrk/img.png&quot; data-origin-width=&quot;2578&quot; data-origin-height=&quot;1004&quot; style=&quot;width: 53.5255%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qdYVO/dJMcadImeUg/kukqO5wJkFplO23ycolTrk/img.png&quot; alt=&quot;&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqdYVO%2FdJMcadImeUg%2FkukqO5wJkFplO23ycolTrk%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;2578&quot; height=&quot;1004&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;c++ 포팅을 실행해볼 수 있는 Gradio UI 구성함&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;실제 실행결과 gemini 2.5 pro가 압도적으로 성능이 좋게 나와버렸는데, 이전에 프론티어 모델끼리 테스트했을 때에 비해서도&amp;nbsp;&lt;br&gt;성능이 확 좋게 나온 건 이번엔 openrouter로 gemini를 돌린 게 아니라 직접 gemini api를 써서인가 싶다.&amp;nbsp;&lt;br&gt;아쉽게도 exaone과 , deepseek coder v2, qwen 2.5 coder가 변환한 c++코드는 내가 돌렸을 땐 코드에서 에러가 발생하며 fail로 실패한걸 볼수 있었다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1720&quot; data-origin-height=&quot;978&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wAwIK/dJMcahRyttA/lUKkVuThTa58dt4kk9szi1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wAwIK/dJMcahRyttA/lUKkVuThTa58dt4kk9szi1/img.png&quot; data-alt=&quot;그래프 하단 3개 모델은 c++ 코드 실행과정에서 에러가 발생했다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wAwIK/dJMcahRyttA/lUKkVuThTa58dt4kk9szi1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwAwIK%2FdJMcahRyttA%2FlUKkVuThTa58dt4kk9szi1%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;1720&quot; height=&quot;978&quot; data-origin-width=&quot;1720&quot; data-origin-height=&quot;978&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;그래프 하단 3개 모델은 c++ 코드 실행과정에서 에러가 발생했다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 유데미 강사가 돌렸던 결과(아마도 작년 11~12월쯤?)인데 큰 경향성은 유사한 걸 볼 수 있다.&amp;nbsp;&lt;br&gt;(뭔가 이 task는 제미나이가 잘하는듯하다)&lt;br&gt;하지만 실제 내가 돌렸을 땐 fail 인 것도 때로는 성공하는 걸 보면 그때그때의 상황+ 각자의 하드웨어에 따라서도 결과가 조금씩 다르게 나오는 듯하다.&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1720&quot; data-origin-height=&quot;322&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cZYGyi/dJMcaf0uIhg/1oEskhVy1M3KH9eCN0tYy1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cZYGyi/dJMcaf0uIhg/1oEskhVy1M3KH9eCN0tYy1/img.png&quot; data-alt=&quot;강사가 진행했던 결과&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cZYGyi/dJMcaf0uIhg/1oEskhVy1M3KH9eCN0tYy1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcZYGyi%2FdJMcaf0uIhg%2F1oEskhVy1M3KH9eCN0tYy1%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;1720&quot; height=&quot;322&quot; data-origin-width=&quot;1720&quot; data-origin-height=&quot;322&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;강사가 진행했던 결과&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;2) 느낀 점&amp;nbsp;&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;벤치마크 성능이 더 좋은 모델이라 하더라도, 실제 특정 task에서 해당 모델이 잘 먹히냐 안 먹히냐는 결국 돌려봐야 안다는 것을 좀 몸소 느꼈던 거 같다. 또한 때로는 특정 task에선 비싸게 프론티어 모델 쓰는 것보다 오픈소스 모델 잘 가져다 쓰는 게 괜찮을 수도 있겠다는 걸&amp;nbsp;&lt;br&gt;위 테스트에서 오픈소스 모델들의 선전을 통해 볼 수 있었던 거 같다.&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 생성형 AI 솔루션 성능 평가를 어떻게 할 것인가?&amp;nbsp;&lt;/h2&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;1) 느낀 점&lt;/h3&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1003&quot; data-origin-height=&quot;558&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yfukd/dJMcabRlfAB/HI6NRyXBP4qGBa1KsNwXNk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yfukd/dJMcabRlfAB/HI6NRyXBP4qGBa1KsNwXNk/img.png&quot; data-alt=&quot;모델 센트릭과, 비즈니스 센트릭 지표&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yfukd/dJMcabRlfAB/HI6NRyXBP4qGBa1KsNwXNk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fyfukd%2FdJMcabRlfAB%2FHI6NRyXBP4qGBa1KsNwXNk%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;1003&quot; height=&quot;558&quot; data-origin-width=&quot;1003&quot; data-origin-height=&quot;558&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;모델 센트릭과, 비즈니스 센트릭 지표&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;강사는 모델 평가 관련된 마지막으로 모델 센트릭한 지표와, 비즈니스 센트릭한 지표를 예로 들어주며&lt;br&gt;모델 centric 지표만 좋다고 되는 게 아니라 결국엔 비즈니스 지표적으로 좋은 게 생성형 AI 서비스에선 중요하단걸 마지막으로 언급했다.&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;굉장히 많이 공감을 했던 내용으로 개발적으로 나오는 기술적 수치와 또 실제 엔드유저가 느끼는 체감은 클 수밖에 없다.&amp;nbsp;&lt;br&gt;그렇기 때문에 위 강사의 말처럼 기본적으로 모델 centric 지표는 어떠한 최소 기준을 통과시키는 용도로 사용하고,&amp;nbsp;&lt;br&gt;비즈니스 지표를 그 후에 보며 실제 성공/실패 여부를 판단하는 게 필요하지 않을까 라는 생각을 하게 됐다.&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글은 강의의 모델 evaluation 관련 전체 챕터를 한 글에 담아보았다.&amp;nbsp;&lt;br&gt;기사에서 많이 봤던 벤치마크나 리더보드에 대한 공부도 해볼 수 있었고&amp;nbsp;&lt;br&gt;실제 코드 생성 능력을 프론티어모델부터 오픈 소스 모델까지 직접 써보며 수치를 가지고 비교해 볼 수 있던 부분이 인상적이었던 거 같다.&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;다음 챕터는 RAG로 아마 한편에 다 쓰긴 어려울 거 같고,&amp;nbsp;&lt;br&gt;조금 나눠서 글을 쓸 생각이다.&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;(꾸준히 공부하고, 적을 테니 많은 관심 부탁드립니다.)&lt;br&gt;&amp;nbsp;&lt;br&gt;Profile:&lt;br&gt;&lt;a href=&quot;http://www.linkedin.com/in/doobeom-kim-%EA%B9%80%EB%91%90%EB%B2%94-2b9649242&quot; target=&quot;_self&quot;&gt;&lt;span&gt;&lt;span style=&quot;color: #0070d1;&quot;&gt;Linkedin&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;&lt;br&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>AI 공부</category>
      <category>AAII</category>
      <category>Agent</category>
      <category>ai</category>
      <category>AI 공부</category>
      <category>Artificial Analysis</category>
      <category>Evaluation</category>
      <category>llm</category>
      <category>모델 evaluation</category>
      <category>모델 평가</category>
      <category>벤치마크</category>
      <author>kdb1248</author>
      <guid isPermaLink="true">https://doobeom-coding.tistory.com/86</guid>
      <comments>https://doobeom-coding.tistory.com/86#entry86comment</comments>
      <pubDate>Sun, 12 Apr 2026 17:37:04 +0900</pubDate>
    </item>
  </channel>
</rss>