<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>플오그래밍</title>
    <link>https://som-ethi-ng.tistory.com/</link>
    <description>제가 작성하는 모든 글은 절대 상업적인 이용이 아니며, 그저 개인적인 공부 용도로만 사용하는 것임을 밝힙니다.</description>
    <language>ko</language>
    <pubDate>Sun, 5 Apr 2026 20:36:49 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>수민 '-'</managingEditor>
    <image>
      <title>플오그래밍</title>
      <url>https://tistory1.daumcdn.net/tistory/2878229/attach/ccb3eee9fefd47f6869007c40962ae06</url>
      <link>https://som-ethi-ng.tistory.com</link>
    </image>
    <item>
      <title>LangGraph Reflection - 자기평가로 답변을 개선하는 루프 설계</title>
      <link>https://som-ethi-ng.tistory.com/627</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Reflection은 에이전트가 스스로 결과를 평가&amp;middot;비판한 뒤 피드백을 상태(state)에 기록하고, 필요하면 수정 루프로 되돌아가 답을 개선하는 설계 패턴이다. 보통 &amp;ldquo;작성 노드(답 생성) &amp;rarr; 리플렉션 노드(자기평가) &amp;rarr; 라우팅(조건부 엣지)&amp;rdquo; 구조로 만들고, 반복 횟수 제한(max_iters)을 둬 무한 루프를 막는다.&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) Reflection 개념&lt;br /&gt;2) 랭체인 + OpenAI로 &amp;ldquo;가사 생성/평가/수정&amp;rdquo;&lt;br /&gt;3) LangGraph로 Reflection 루프 구현&lt;br /&gt;4) Reflexion(논문 아이디어) + 웹검색 툴 결합&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;까지 한 번에 정리한다.&lt;br /&gt;원문 흐름은 &lt;a href=&quot;https://ryuzyproject.tistory.com/195&quot;&gt;랭그래프 Reflection&lt;/a&gt; 기반으로 재구성했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Reflection&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Reflection의 핵심은 간단하다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;초안 생성&lt;/li&gt;
&lt;li&gt;자기 평가(잘된 점/아쉬운 점/수정 지시)&lt;/li&gt;
&lt;li&gt;피드백 반영해서 수정&lt;/li&gt;
&lt;li&gt;기준 미달이면 다시 반복&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, &amp;ldquo;한 번 생성하고 끝&amp;rdquo;이 아니라 &amp;ldquo;품질을 스스로 끌어올리는 루프&amp;rdquo;다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdna%2Fet5eoq%2FbtsQtfTBoz7%2FAAAAAAAAAAAAAAAAAAAAAM6C44vgBF-5pX8AStDskIMKM2c-gg9io9wsDgxh0NT0%2Fimg.png%3Fcredential%3DyqXZFxpELC7KVnFOS48ylbz2pIh7yKj8%26expires%3D1774969199%26allow_ip%3D%26allow_referer%3D%26signature%3D5r2AoFcs9RsXoictxR7M800sVgA%253D&quot; /&gt;&lt;/p&gt;
&lt;pre class=&quot;swift&quot;&gt;&lt;code&gt;import getpass
import os


def _set_env(var: str):
    if not os.environ.get(var):
        os.environ[var] = getpass.getpass(f&quot;{var}: &quot;)


_set_env(&quot;OPENAI_API_KEY&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;cmake&quot;&gt;&lt;code&gt;!pip install langchain_openai&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 랭체인 + OpenAI 모델을 이용한 가사 쓰기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서는 Reflection을 가장 직관적으로 확인하기 위해 &amp;ldquo;작사 도우미&amp;rdquo; 예제로 진행한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;가사 생성기: 사용자 요청을 받아 5단락 가사 생성&lt;/li&gt;
&lt;li&gt;가사 평가기: 잘된 점/아쉬운 점/수정 지시사항 생성&lt;/li&gt;
&lt;li&gt;가사 수정기: 피드백을 반영해 개선본 생성&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-1. 생성 프롬프트&lt;/h3&gt;
&lt;pre class=&quot;verilog&quot;&gt;&lt;code&gt;from langchain_core.messages import AIMessage, HumanMessage
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_openai import ChatOpenAI

prompt = ChatPromptTemplate.from_messages(
    [
        (
            &quot;system&quot;,
            &quot;당신은 5단락 노래 가사를 작성하는 작사 도우미입니다. &quot;
            &quot;사용자의 요청에 맞게 주제, 분위기, 감정을 반영한 가사를 작성하세요. &quot;
            &quot;좋은 가사는 감정이 자연스럽게 이어지고, 표현이 구체적이며, 전체 흐름이 일관되어야 합니다. &quot;
            &quot;사용자가 피드백을 제공하면, 이전 결과를 그대로 반복하지 말고 &quot;
            &quot;지적된 부분이 눈에 띄게 개선된 수정본을 작성하세요. &quot;
            &quot;이때 잘된 표현과 핵심 주제는 유지하고, 감정 표현, 문장 자연스러움, 이미지 표현을 더 좋게 다듬으세요. &quot;
            &quot;반드시 5단락으로 작성하고, 각 단락은 명확히 구분하세요.&quot;
        ),
        MessagesPlaceholder(variable_name=&quot;messages&quot;),
    ]
)

llm = ChatOpenAI(model=&quot;gpt-5.4-2026-03-05&quot;)
generate = prompt | llm&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-2. 평가 프롬프트&lt;/h3&gt;
&lt;pre class=&quot;makefile&quot;&gt;&lt;code&gt;reflection_prompt = ChatPromptTemplate.from_messages(
    [
        (
            &quot;system&quot;,
            &quot;당신은 노래 가사를 평가하고 개선 방향을 제안하는 작사 코치입니다. &quot;
            &quot;사용자가 제출한 가사를 읽고 주제의 일관성, 감정의 깊이, 표현의 구체성, &quot;
            &quot;문체의 자연스러움, 단락 간 흐름을 기준으로 평가하세요. &quot;
            &quot;응답은 반드시 다음 세 부분으로 작성하세요. &quot;
            &quot;1. 잘된 점 &quot;
            &quot;2. 아쉬운 점 &quot;
            &quot;3. 수정 지시사항 &quot;
            &quot;수정 지시사항은 다음 작사 단계에서 바로 반영할 수 있을 만큼 구체적으로 작성하세요.&quot;
        ),
        MessagesPlaceholder(variable_name=&quot;messages&quot;),
    ]
)

reflect = reflection_prompt | llm&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-3. 초안 &amp;rarr; 피드백 &amp;rarr; 수정본&lt;/h3&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;request = HumanMessage(content=&quot;이별에 대한 가사를 작성해줘.&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;verilog&quot;&gt;&lt;code&gt;draft_lyrics = &quot;&quot;
for chunk in generate.stream({&quot;messages&quot;: [request]}):
    if chunk.content:
        print(chunk.content, end=&quot;&quot;)
        draft_lyrics += chunk.content&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;reflection_feedback = &quot;&quot;
for chunk in reflect.stream({&quot;messages&quot;: [request, AIMessage(content=draft_lyrics)]}):
    if chunk.content:
        print(chunk.content, end=&quot;&quot;)
        reflection_feedback += chunk.content&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;revised_lyrics = &quot;&quot;
revision_request = HumanMessage(
    content=(
        &quot;아래 피드백을 반드시 반영하여 이전 가사를 수정해주세요. &quot;
        &quot;원문을 그대로 반복하지 말고, 표현과 감정을 더 풍부하게 개선해주세요.\n\n&quot;
        f&quot;{reflection_feedback}&quot;
    )
)

for chunk in generate.stream(
    {&quot;messages&quot;: [request, AIMessage(content=draft_lyrics), revision_request]}
):
    if chunk.content:
        print(chunk.content, end=&quot;&quot;)
        revised_lyrics += chunk.content&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. Graph로 Reflection 구현&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 과정을 그래프로 옮기면, 루프와 종료 조건을 더 깔끔하게 제어할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdna%2FKZUhB%2FbtsQxetHmFz%2FAAAAAAAAAAAAAAAAAAAAAP0EuRkgL4J0CjrSQh0IKSN45ducYKse-WsdmcfUNwRn%2Fimg.png%3Fcredential%3DyqXZFxpELC7KVnFOS48ylbz2pIh7yKj8%26expires%3D1774969199%26allow_ip%3D%26allow_referer%3D%26signature%3D3cXJM7xKKk5DHhdc95Azms6C9N4%253D&quot; /&gt;&lt;/p&gt;
&lt;pre class=&quot;cmake&quot;&gt;&lt;code&gt;!pip install langgraph&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;from typing import Annotated, Literal
from typing_extensions import TypedDict

from langgraph.graph import END, START, StateGraph
from langgraph.graph.message import add_messages
from langgraph.checkpoint.memory import MemorySaver&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;class State(TypedDict):
    messages: Annotated[list, add_messages]


def generation_node(state: State) -&amp;gt; State:
    return {&quot;messages&quot;: [generate.invoke(state[&quot;messages&quot;])]}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;def reflection_node(state: State) -&amp;gt; State:
    cls_map = {&quot;ai&quot;: AIMessage, &quot;human&quot;: HumanMessage}

    translated = [state[&quot;messages&quot;][0]] + [
        cls_map[msg.type](content=msg.content) for msg in state[&quot;messages&quot;][1:]
    ]
    res = reflect.invoke(translated)
    return {&quot;messages&quot;: [HumanMessage(content=res.content)]}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;def should_continue(state: State) -&amp;gt; Literal[&quot;reflect&quot;, END]:
    if len(state[&quot;messages&quot;]) &amp;gt; 6:
        return END
    return &quot;reflect&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;graph_builder = StateGraph(State)
graph_builder.add_node(&quot;generate&quot;, generation_node)
graph_builder.add_node(&quot;reflect&quot;, reflection_node)
graph_builder.add_edge(START, &quot;generate&quot;)
graph_builder.add_conditional_edges(&quot;generate&quot;, should_continue)
graph_builder.add_edge(&quot;reflect&quot;, &quot;generate&quot;)

memory = MemorySaver()
graph = graph_builder.compile(checkpointer=memory)&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;config = {&quot;configurable&quot;: {&quot;thread_id&quot;: &quot;1&quot;}}

for event in graph.stream(
    {&quot;messages&quot;: [HumanMessage(content=&quot;이별에 대한 가사를 작성해주세요.&quot;)]},
    config,
):
    print(event)
    print(&quot;-&quot; * 50)&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. Reflexion 구현&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Reflexion은 Reflection 아이디어를 더 체계화한 프레임워크다.&lt;br /&gt;핵심은 &amp;ldquo;반성문(언어 피드백)을 메모리에 저장하고 다음 시도에 반영&amp;rdquo;한다는 점이다.&lt;br /&gt;논문: &lt;a href=&quot;https://arxiv.org/abs/2303.11366&quot;&gt;Reflexion: Language Agents with Verbal Reinforcement Learning&lt;/a&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-1. 구성 요소&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Actor (LM): 실제 행동(답안 작성, 코드 생성 등) 수행&lt;/li&gt;
&lt;li&gt;Evaluator (LM): 결과 품질 평가&lt;/li&gt;
&lt;li&gt;Self-reflection (LM): 개선 피드백 생성&lt;/li&gt;
&lt;li&gt;Trajectory (short-term memory): 현재 시도의 행동/관찰 기록&lt;/li&gt;
&lt;li&gt;Experience (long-term memory): 누적된 반성문 저장&lt;/li&gt;
&lt;li&gt;Environment: 외부 보상/피드백(테스트, 툴 응답 등)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdna%2Fdzon1Q%2FbtsQxdO7zsM%2FAAAAAAAAAAAAAAAAAAAAAIv8GnJo2Z7lQar4RlJWoaap2PH03wadiph8ot4leqkE%2Fimg.png%3Fcredential%3DyqXZFxpELC7KVnFOS48ylbz2pIh7yKj8%26expires%3D1774969199%26allow_ip%3D%26allow_referer%3D%26signature%3DYLvIQazd8nfTlqgPYHLcJHzHcpY%253D&quot; /&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-2. Tavily 검색 도구 준비&lt;/h3&gt;
&lt;pre class=&quot;scala&quot;&gt;&lt;code&gt;def _set_env(var: str):
    if not os.environ.get(var):
        os.environ[var] = getpass.getpass(f&quot;{var}: &quot;)


_set_env(&quot;TAVILY_API_KEY&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;cmake&quot;&gt;&lt;code&gt;!pip install langchain_community langchain-tavily&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;from langchain_tavily import TavilySearch

tavily_tool = TavilySearch(max_results=5)&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-3. 데이터 클래스 정의&lt;/h3&gt;
&lt;pre class=&quot;monkey&quot;&gt;&lt;code&gt;from pydantic import BaseModel, Field


class Reflection(BaseModel):
    missing: str = Field(description=&quot;누락되거나 부족한 부분에 대한 비평&quot;)
    superfluous: str = Field(description=&quot;불필요한 부분에 대한 비평&quot;)


class AnswerQuestion(BaseModel):
    answer: str = Field(description=&quot;질문에 대한 10문장 이내의 자세한 답변&quot;)
    search_queries: list[str] = Field(
        description=&quot;현재 답변 비평을 해결하기 위한 1~3개의 검색 쿼리&quot;
    )
    reflection: Reflection = Field(description=&quot;답변에 대한 자기반성 내용&quot;)


class Responder:
    def __init__(self, runnable):
        self.runnable = runnable

    def respond(self, state: dict):
        response = self.runnable.invoke({&quot;messages&quot;: state[&quot;messages&quot;]})
        return {&quot;messages&quot;: response}&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-4. 초기 답변기 / 수정 답변기&lt;/h3&gt;
&lt;pre class=&quot;ceylon&quot;&gt;&lt;code&gt;from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder

actor_prompt_template = ChatPromptTemplate.from_messages(
    [
        (
            &quot;system&quot;,
            &quot;&quot;&quot;당신은 전문 연구자입니다.
1. {first_instruction}
2. &amp;lt;Reflect&amp;gt; 생성한 답변을 다시 되돌아보고 개선할 수 있도록 비판하세요.
3. &amp;lt;Recommend search queries&amp;gt; 답변 질을 높이기 위해 추가 조사할 웹 검색 쿼리를 추천하세요.
&quot;&quot;&quot;,
        ),
        MessagesPlaceholder(variable_name=&quot;messages&quot;),
        (&quot;user&quot;, &quot;\n\n&amp;lt;Reflect&amp;gt; 사용자 원래 질문과 지금까지의 행동을 되돌아보세요.&quot;),
    ]
)&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;initial_answer_chain = actor_prompt_template.partial(
    first_instruction=&quot;질문에 대한 10문장 이내의 자세한 답변을 제공해주세요.&quot;
) | llm.bind_tools(tools=[AnswerQuestion], tool_choice=&quot;any&quot;)

first_responder = Responder(runnable=initial_answer_chain)&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;class ReviseAnswer(AnswerQuestion):
    references: list[str] = Field(description=&quot;업데이트된 답변에 사용된 인용 출처&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;revise_instructions = &quot;&quot;&quot;이전 답변을 새로운 정보를 바탕으로 수정하세요.
- 이전 비평 내용을 활용해 중요한 정보를 추가하세요.
- 수정 답변에는 숫자 인용 표기를 포함하세요.
- 답변 하단에 참고문헌 섹션을 추가하세요.
- 불필요한 내용은 제거하고 최종 답변은 200자를 넘지 않게 작성하세요.
&quot;&quot;&quot;

revision_chain = actor_prompt_template.partial(
    first_instruction=revise_instructions
) | llm.bind_tools(tools=[ReviseAnswer], tool_choice=&quot;any&quot;)

revisor = Responder(runnable=revision_chain)&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-5. 웹 검색 ToolNode 생성&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;from langchain_core.tools import StructuredTool
from langgraph.prebuilt import ToolNode&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;def run_queries(search_queries: list[str], **kwargs):
    &quot;&quot;&quot;Run the generated queries.&quot;&quot;&quot;
    return tavily_tool.batch([{&quot;query&quot;: query} for query in search_queries])&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;tool_node = ToolNode(
    [
        StructuredTool.from_function(run_queries, name=AnswerQuestion.__name__),
        StructuredTool.from_function(run_queries, name=ReviseAnswer.__name__),
    ]
)&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-6. 그래프 구성 + 반복 제어&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;from typing_extensions import TypedDict
from typing import Annotated
from langgraph.graph import END, START, StateGraph
from langgraph.graph.message import add_messages


class State(TypedDict):
    messages: Annotated[list, add_messages]&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;MAX_ITERATIONS = 5

graph_builder = StateGraph(State)
graph_builder.add_node(&quot;draft&quot;, first_responder.respond)
graph_builder.add_node(&quot;execute_tools&quot;, tool_node)
graph_builder.add_node(&quot;revise&quot;, revisor.respond)

graph_builder.add_edge(&quot;draft&quot;, &quot;execute_tools&quot;)
graph_builder.add_edge(&quot;execute_tools&quot;, &quot;revise&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;ruby&quot;&gt;&lt;code&gt;def _get_num_iterations(messages: list):
    i = 0
    for m in messages[::-1]:
        if m.type not in {&quot;tool&quot;, &quot;ai&quot;}:
            break
        i += 1
    return i


def event_loop(state: State):
    num_iterations = _get_num_iterations(state[&quot;messages&quot;])
    if num_iterations &amp;gt; MAX_ITERATIONS:
        return END
    return &quot;execute_tools&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;graph_builder.add_conditional_edges(&quot;revise&quot;, event_loop, [&quot;execute_tools&quot;, END])
graph_builder.add_edge(START, &quot;draft&quot;)
graph = graph_builder.compile()&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;events = graph.stream(
    {&quot;messages&quot;: [HumanMessage(content=&quot;AI Agent가 무엇인가요?&quot;)]},
    stream_mode=&quot;values&quot;,
)

for i, step in enumerate(events):
    print(f&quot;Step {i}&quot;)
    step[&quot;messages&quot;][-1].pretty_print()&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdna%2FnMKU1%2FbtsQwRziUZl%2FAAAAAAAAAAAAAAAAAAAAAASlhE2QFrMfwp2mVpfbJqW1PV9_JrnKxfrPSLouN6QH%2Fimg.png%3Fcredential%3DyqXZFxpELC7KVnFOS48ylbz2pIh7yKj8%26expires%3D1774969199%26allow_ip%3D%26allow_referer%3D%26signature%3DTy2ZJklv8PUca0y%252FmQR9PTDcOP4%253D&quot; /&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마치며&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Reflection은 에이전트 품질을 높이는 가장 실용적인 패턴 중 하나다.&lt;/li&gt;
&lt;li&gt;초안 생성 &amp;rarr; 평가 &amp;rarr; 수정 루프만 잘 구성해도 결과 일관성이 크게 올라간다.&lt;/li&gt;
&lt;li&gt;LangGraph로 옮기면 반복/종료 조건, 체크포인트, 상태 추적이 명확해져 운영이 쉬워진다.&lt;/li&gt;
&lt;li&gt;Reflexion 방식까지 확장하면, 반성문과 검색 결과를 누적해 다음 시도를 더 똑똑하게 만들 수 있다.&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://ryuzyproject.tistory.com/195&quot;&gt;랭그래프 Reflection&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://arxiv.org/abs/2303.11366&quot;&gt;Reflexion 논문&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>현재/AI Agent</category>
      <author>수민 '-'</author>
      <guid isPermaLink="true">https://som-ethi-ng.tistory.com/627</guid>
      <comments>https://som-ethi-ng.tistory.com/627#entry627comment</comments>
      <pubDate>Tue, 24 Mar 2026 21:00:39 +0900</pubDate>
    </item>
    <item>
      <title>LangGraph로 챗봇 만들기 - Tool Calling Agent, Tavily, ToolNode, create_react_agent</title>
      <link>https://som-ethi-ng.tistory.com/626</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Tool Calling Agent는 자신이 가진 지식만 사용하는 것이 아니라, 필요하면 외부 도구(API, 웹 검색, DB, 코드 실행기 등)를 호출해 문제를 해결하는 에이전트다. 쉽게 말해, &amp;ldquo;대화만 하는 AI&amp;rdquo;가 아니라 &lt;b&gt;필요할 때 검색/계산/조회 같은 도구를 직접 쓰는 AI&lt;/b&gt;가 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 (1) Tavily로 웹 검색 도구를 붙이고, (2) LangChain에서 도구 바인딩을 하고, (3) LangGraph에서 ToolNode로 &amp;ldquo;LLM의 tool_calls를 실제 실행&amp;rdquo;까지 연결해 &lt;b&gt;웹 검색 챗봇&lt;/b&gt;을 만드는 흐름을 정리한다. 원문 흐름은 &lt;a href=&quot;https://ryuzyproject.tistory.com/194&quot;&gt;랭그래프를 이용한 간단한 챗봇&lt;/a&gt;을 바탕으로 재구성했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Tool Calling Agent란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Tool Calling Agent는 다음 순서로 움직인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1) 사용자 질문 이해&lt;br /&gt;2) &amp;ldquo;도구가 필요하다&amp;rdquo; 판단&lt;br /&gt;3) 어떤 도구를 어떤 입력으로 호출할지 결정(tool_calls 생성)&lt;br /&gt;4) 도구 실행 결과를 받아서&lt;br /&gt;5) 최종 답변 생성&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조를 잡아두면, 단순 Q&amp;amp;A를 넘어 &amp;ldquo;실제로 무언가를 처리하는 챗봇&amp;rdquo;으로 확장하기가 훨씬 쉬워진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원문 이미지:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdna%2FckV9NZ%2FbtsQrlsFwir%2FAAAAAAAAAAAAAAAAAAAAAAxIMYi9tK6eJ-KNQuSM2zqJOtMcskhXOI1uxBSHaUi6%2Fimg.png%3Fcredential%3DyqXZFxpELC7KVnFOS48ylbz2pIh7yKj8%26expires%3D1774969199%26allow_ip%3D%26allow_referer%3D%26signature%3DB7thyu6eb%252FyZtWpVGP%252Bfrpl%252FvMQ%253D&quot; /&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Tavily: 웹 검색을 도구로 붙이기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.tavily.com/&quot;&gt;Tavily&lt;/a&gt;는 웹을 검색해 AI가 최신/정확한 정보를 답변에 반영할 수 있게 돕는 검색 API다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-1. 설치와 키 설정&lt;/h3&gt;
&lt;pre class=&quot;cmake&quot;&gt;&lt;code&gt;!pip install -U tavily-python&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;swift&quot;&gt;&lt;code&gt;import getpass
import os


def _set_env(var: str):
    if not os.environ.get(var):
        os.environ[var] = getpass.getpass(f&quot;{var}: &quot;)


_set_env(&quot;OPENAI_API_KEY&quot;)
_set_env(&quot;TAVILY_API_KEY&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-2. 검색 호출&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;from tavily import TavilyClient

tavily_client = TavilyClient()
response = tavily_client.search(&quot;What is AI Agent?&quot;, max_results=3)
print(response[&quot;results&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;&lt;code&gt;max_results&lt;/code&gt;: 결과 최대 개수&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;검색 결과를 한 덩어리 컨텍스트로 뽑기&amp;rdquo;도 가능하다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;context = tavily_client.get_search_context(query=&quot;What is AI Agent?&quot;)
print(context)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;짧은 Q&amp;amp;A 스타일로 답만 받는 방식도 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;answer = tavily_client.qna_search(query=&quot;What is AI Agent?&quot;)
print(answer)&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. LangChain Tool로 TavilySearch 붙이기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LangChain Tool로 붙이면, LLM이 &amp;ldquo;도구 호출&amp;rdquo; 형태로 Tavily를 사용할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;cmake&quot;&gt;&lt;code&gt;!pip install langchain_tavily&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;from langchain_tavily import TavilySearch

tool = TavilySearch(max_results=3)
tool.invoke(&quot;What's a 'node' in LangGraph?&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;.invoke()&lt;/code&gt;는 문자열만 받을 수도 있고, Tool Call 이벤트처럼 딕셔너리 형태로 받을 수도 있다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;invoke_with_toolcall = tool.invoke(
    {
        &quot;args&quot;: {&quot;query&quot;: &quot;What's a 'node' in LangGraph?&quot;},
        &quot;type&quot;: &quot;tool_call&quot;,
        &quot;id&quot;: &quot;foo&quot;,
        &quot;name&quot;: &quot;tavily_search&quot;,
    }
)
print(invoke_with_toolcall.content)&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 도구 바인딩: LLM이 tools를 &amp;ldquo;호출&amp;rdquo;할 수 있게 만들기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도구 바인딩은 말 그대로 &amp;ldquo;LLM에게 쓸 수 있는 도구 목록을 쥐여주는 단계&amp;rdquo;다.&lt;/p&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;from langchain_core.tools import tool


@tool
def add(a: int, b: int) -&amp;gt; int:
    return a + b


@tool
def multiply(a: int, b: int) -&amp;gt; int:
    return a * b&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;cmake&quot;&gt;&lt;code&gt;!pip install langchain_openai&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;makefile&quot;&gt;&lt;code&gt;from langchain_openai import ChatOpenAI

llm = ChatOpenAI(model=&quot;gpt-5-nano&quot;)
llm_with_tools = llm.bind_tools([add, multiply])

resp = llm_with_tools.invoke(&quot;What is 3 * 12? Also, what is 11 + 49?&quot;)
print(resp.tool_calls)&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;LLM이 답변을 &amp;ldquo;그냥 텍스트로&amp;rdquo; 할 수도 있고&lt;/li&gt;
&lt;li&gt;tool_calls를 만들어서 &amp;ldquo;도구를 실행해줘&amp;rdquo;라고 요청할 수도 있다는 점이다&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. ToolNode: tool_calls를 실제 실행으로 연결하는 브리지&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LLM이 tool_calls를 만들었다고 해서 도구가 자동 실행되는 건 아니다.&lt;br /&gt;그 &amp;ldquo;실행&amp;rdquo;을 담당하는 게 ToolNode다.&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;LLM 노드 (tool_calls 생성)
  &amp;rarr; ToolNode (도구 실행)
  &amp;rarr; LLM 노드 (도구 결과를 보고 다음 행동 결정)
  &amp;rarr; (필요하면 반복)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원문에서는 ToolNode를 직접 흉내 낸 &lt;code&gt;BasicToolNode&lt;/code&gt; 예시도 있다. 핵심은:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;마지막 메시지의 tool_calls를 순회하고&lt;/li&gt;
&lt;li&gt;tool name으로 실제 도구를 찾아 invoke 한 뒤&lt;/li&gt;
&lt;li&gt;ToolMessage로 결과를 messages에 다시 넣는다는 점이다&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. (빠른 구성) create_react_agent로 ReAct 에이전트 만들기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;create_react_agent&lt;/code&gt;는 ReAct 패턴(Reason + Act)에 맞춰 에이전트를 빠르게 구성하는 팩토리다.&lt;br /&gt;LLM + tools만 넣으면 &amp;ldquo;생각 &amp;rarr; 도구 호출 &amp;rarr; 관찰 &amp;rarr; 답변&amp;rdquo; 루프를 기본 형태로 만들어준다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;from langgraph.prebuilt import create_react_agent
from langchain_openai import ChatOpenAI
from langchain_tavily import TavilySearch

tool = TavilySearch(max_results=2)
llm = ChatOpenAI(model=&quot;gpt-5-nano&quot;)

agent = create_react_agent(llm, tools=[tool])
response = agent.invoke({&quot;messages&quot;: [{&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: &quot;What is LangGraph?&quot;}]})
print(response)&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마치며&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Tool Calling Agent는 &amp;ldquo;대화형 모델 + 도구 실행&amp;rdquo;을 결합해, 챗봇을 실제 업무 시스템으로 확장하는 기본 패턴이다.&lt;/li&gt;
&lt;li&gt;Tavily 같은 검색 도구를 붙이면 &amp;ldquo;최신 정보&amp;rdquo;가 필요한 질문에서 품질이 확 달라진다.&lt;/li&gt;
&lt;li&gt;LangGraph에서는 ToolNode가 LLM의 tool_calls를 실제 실행으로 연결해주는 핵심 브리지다.&lt;/li&gt;
&lt;li&gt;빠르게 시작하려면 create_react_agent로 기본 루프를 만들고, 이후 State/조건 분기/재시도 등을 붙여서 점점 에이전틱 워크플로우로 키우는 방식이 좋다.&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://ryuzyproject.tistory.com/194&quot;&gt;랭그래프를 이용한 간단한 챗봇&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>현재/AI Agent</category>
      <author>수민 '-'</author>
      <guid isPermaLink="true">https://som-ethi-ng.tistory.com/626</guid>
      <comments>https://som-ethi-ng.tistory.com/626#entry626comment</comments>
      <pubDate>Tue, 24 Mar 2026 00:17:30 +0900</pubDate>
    </item>
    <item>
      <title>LangGraph 기초 문법 - State 업데이트, add_messages, invoke&amp;middot;stream, 조건&amp;middot;반복</title>
      <link>https://som-ethi-ng.tistory.com/623</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;LangGraph는 LangChain 생태계에서 에이전트나 RAG 시스템을 &lt;b&gt;그래프(노드/엣지)&lt;/b&gt; 로 설계하고 실행할 수 있게 해주는 오케스트레이션 프레임워크다. 직선형 파이프라인만 만드는 게 아니라 &lt;b&gt;분기(conditional), 반복(loop), 병렬(parallel), 스트리밍(stream)&lt;/b&gt; 같은 흐름을 &amp;ldquo;구조로&amp;rdquo; 표현할 수 있다는 점이 핵심이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글은 LangGraph를 처음 볼 때 가장 헷갈리는 기초 문법을 한 번에 정리한다. 특히 &lt;b&gt;State 업데이트&lt;/b&gt;, &lt;b&gt;메시지 누적 리듀서(add_messages)&lt;/b&gt;, &lt;b&gt;invoke/ainvoke/stream/astream&lt;/b&gt;, 그리고 &lt;b&gt;조건 분기/반복&lt;/b&gt;까지 &amp;ldquo;코드가 어디에서 어떻게 이어지는지&amp;rdquo; 기준으로 정리해본다. 원문 흐름은 &lt;a href=&quot;https://ryuzyproject.tistory.com/193&quot;&gt;랭그래프 기초 문법&lt;/a&gt;을 바탕으로 재구성했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 그래프의 상태(State) 업데이트: &amp;ldquo;return으로 상태를 갱신한다&amp;rdquo;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LangGraph의 노드는 보통 이런 형태다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;입력: &lt;code&gt;state&lt;/code&gt; (딕셔너리처럼 생긴 상태)&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;중요한 포인트는 &lt;b&gt;상태를 직접 mutate하는 게 아니라, 반환값으로 업데이트를 전달&lt;/b&gt;한다는 점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 가장 기본적인 &amp;ldquo;메시지에 AI 답변 하나 추가&amp;rdquo; 예시다.&lt;/p&gt;
&lt;pre class=&quot;cmake&quot;&gt;&lt;code&gt;!pip install langgraph&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;from langchain_core.messages import AnyMessage, AIMessage, HumanMessage
from typing_extensions import TypedDict
from langgraph.graph import StateGraph


class State(TypedDict):
    messages: list[AnyMessage]
    extra_field: int


def node(state: State):
    messages = state[&quot;messages&quot;]
    new_message = AIMessage(&quot;안녕하세요! 무엇을 도와드릴까요?&quot;)
    return {&quot;messages&quot;: messages + [new_message], &quot;extra_field&quot;: 10}


graph_builder = StateGraph(State)
graph_builder.add_node(&quot;node&quot;, node)
graph_builder.set_entry_point(&quot;node&quot;)  # START -&amp;gt; node
graph = graph_builder.compile()

result = graph.invoke({&quot;messages&quot;: [HumanMessage(&quot;안녕&quot;)]})
print(result[&quot;messages&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;기존 messages를 가져와서&lt;/li&gt;
&lt;li&gt;새 AIMessage를 붙이고&lt;/li&gt;
&lt;li&gt;그 리스트를 다시 return 한다&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 대화 메시지 누적 업데이트: add_messages 리듀서가 깔끔한 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대화형 에이전트에서 가장 자주 하는 일이 &amp;ldquo;messages를 계속 누적&amp;rdquo;하는 건데, 이걸 매번 &lt;code&gt;messages + [new]&lt;/code&gt;로 쓰면 코드가 금방 지저분해진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 등장하는 게 &lt;code&gt;add_messages&lt;/code&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;a href=&quot;https://reference.langchain.com/python/langgraph/graphs#langgraph.graph.message.add_messages&quot;&gt;add_messages&lt;/a&gt;는&lt;/li&gt;
&lt;li&gt;기존 messages에 새 messages를 &lt;b&gt;병합(merge)&lt;/b&gt; 하는 리듀서 역할을 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리듀서를 붙이면, 노드는 &amp;ldquo;메시지 하나만 return&amp;rdquo;해도 LangGraph가 알아서 누적해 준다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;from langchain_core.messages import AnyMessage, AIMessage
from typing_extensions import TypedDict, Annotated
from langgraph.graph import StateGraph
from langgraph.graph.message import add_messages


class State(TypedDict):
    messages: Annotated[list[AnyMessage], add_messages]
    extra_field: int


def node(state: State):
    new_message = AIMessage(&quot;안녕하세요! 무엇을 도와드릴까요?&quot;)
    return {&quot;messages&quot;: new_message, &quot;extra_field&quot;: 10}


graph_builder = StateGraph(State)
graph_builder.add_node(&quot;node&quot;, node)
graph_builder.set_entry_point(&quot;node&quot;)
graph = graph_builder.compile()&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;노드가 &amp;ldquo;내가 추가하고 싶은 메시지&amp;rdquo;만 반환하면 되고&lt;/li&gt;
&lt;li&gt;누적/병합 로직은 상태 스키마에서 통제되며&lt;/li&gt;
&lt;li&gt;여러 노드가 동시에 메시지를 내는 경우도 예측 가능해진다는 점이다&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. invoke / ainvoke / stream / astream: 실행 모드 네 가지&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LangGraph를 &amp;ldquo;앱처럼&amp;rdquo; 쓸 때 결국 필요한 건 실행 방식이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;invoke&lt;/b&gt;: 동기 실행. 결과가 나올 때까지 기다린다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;ainvoke&lt;/b&gt;: 비동기 실행. 여러 요청을 동시에 처리할 때 유리하다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;stream&lt;/b&gt;: 중간 결과를 실시간으로 받는다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;astream&lt;/b&gt;: 비동기 스트리밍.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-1. invoke&lt;/h3&gt;
&lt;pre class=&quot;makefile&quot;&gt;&lt;code&gt;input_message = {&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: &quot;안녕하세요.&quot;}
result = graph.invoke({&quot;messages&quot;: [input_message]})
print(result)&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-2. ainvoke&lt;/h3&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;result = await graph.ainvoke({&quot;messages&quot;: [input_message]})
print(result)&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-3. stream (values / updates / messages)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;stream_mode&lt;/code&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;code&gt;values&lt;/code&gt;: 각 단계의 &amp;ldquo;전체 상태&amp;rdquo;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;updates&lt;/code&gt;: 각 단계의 &amp;ldquo;변경분&amp;rdquo;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;messages&lt;/code&gt;: 메시지 단위 스트리밍&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;for chunk in graph.stream({&quot;messages&quot;: [input_message]}, stream_mode=&quot;values&quot;):
    print(chunk)&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;for chunk in graph.stream({&quot;messages&quot;: [input_message]}, stream_mode=&quot;updates&quot;):
    print(chunk)&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;for chunk_msg, metadata in graph.stream({&quot;messages&quot;: [input_message]}, stream_mode=&quot;messages&quot;):
    print(chunk_msg.content)
    print(metadata[&quot;langgraph_node&quot;])&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 노드와 엣지 연결: 순차, 시퀀스, 병렬&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-1. 노드와 엣지 순차 연결&lt;/h3&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;from typing_extensions import TypedDict
from langgraph.graph import START, StateGraph


class State(TypedDict):
    value_1: str
    value_2: int


def step_1(state: State):
    return {&quot;value_1&quot;: state[&quot;value_1&quot;]}


def step_2(state: State):
    current_value_1 = state[&quot;value_1&quot;]
    return {&quot;value_1&quot;: f&quot;{current_value_1} b&quot;}


def step_3(state: State):
    return {&quot;value_2&quot;: 10}


graph_builder = StateGraph(State)
graph_builder.add_node(step_1)
graph_builder.add_node(step_2)
graph_builder.add_node(step_3)

graph_builder.add_edge(START, &quot;step_1&quot;)
graph_builder.add_edge(&quot;step_1&quot;, &quot;step_2&quot;)
graph_builder.add_edge(&quot;step_2&quot;, &quot;step_3&quot;)

graph = graph_builder.compile()
print(graph.invoke({&quot;value_1&quot;: &quot;apple&quot;}))&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-2. add_sequence로 한 번에 연결&lt;/h3&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;graph_builder = StateGraph(State).add_sequence([step_1, step_2, step_3])
graph_builder.add_edge(START, &quot;step_1&quot;)
graph = graph_builder.compile()
print(graph.invoke({&quot;value_1&quot;: &quot;c&quot;}))&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-3. 병렬(Parallel) 연결&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원문에서 병렬 연결 구조를 그림으로 보면 이해가 빠르다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdna%2FbKsmXN%2FbtsQoa6BiQH%2FAAAAAAAAAAAAAAAAAAAAAPpgPBweh6r0kpol4tFqhOR7MdAaIfiH-Lj4HXxoNYBU%2Fimg.png%3Fcredential%3DyqXZFxpELC7KVnFOS48ylbz2pIh7yKj8%26expires%3D1774969199%26allow_ip%3D%26allow_referer%3D%26signature%3Det57M4W%252B1rxuWT1yQVw6r0BDHw4%253D&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;병렬 누적은 보통 &lt;code&gt;operator.add&lt;/code&gt; 같은 리듀서를 이용해 &amp;ldquo;결과를 리스트로 누적&amp;rdquo;하는 패턴을 쓴다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 조건 분기와 반복: 루프를 만들 때 어디가 핵심인가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조건부 엣지(conditional edges)는 &amp;ldquo;상태를 보고 다음 노드를 결정&amp;rdquo;하게 해준다. 루프는 &amp;ldquo;조건이 맞으면 다시 돌아가 반복&amp;rdquo;하게 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원문 예시처럼 그림으로 보면 구조가 잡힌다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdna%2FexNpm9%2FbtsQqoil95F%2FAAAAAAAAAAAAAAAAAAAAAIPncRoH-KNiTSy0fHLkuCYoGImVqZSYLqCJFGTP-wrG%2Fimg.png%3Fcredential%3DyqXZFxpELC7KVnFOS48ylbz2pIh7yKj8%26expires%3D1774969199%26allow_ip%3D%26allow_referer%3D%26signature%3D7XPcTIf3uYOEuTyJu5Y2j40yDEk%253D&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 반복이 너무 깊어지면 &lt;code&gt;GraphRecursionError&lt;/code&gt;가 날 수 있으니, &lt;code&gt;recursion_limit&lt;/code&gt;은 디버깅할 때 특히 중요하다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마치며&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;LangGraph의 핵심은 &amp;ldquo;노드/엣지/상태&amp;rdquo;를 코드로 정의해, 분기&amp;middot;반복&amp;middot;병렬 같은 워크플로우를 구조적으로 만드는 것이다.&lt;/li&gt;
&lt;li&gt;메시지 누적은 &lt;code&gt;add_messages&lt;/code&gt; 같은 리듀서를 붙이면 코드가 훨씬 단단해진다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;invoke/ainvoke/stream/astream&lt;/code&gt;은 운영&amp;middot;디버깅&amp;middot;UX에서 체감 차이가 큰 옵션이라, 초반에 같이 익혀 두는 게 좋다.&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://ryuzyproject.tistory.com/193&quot;&gt;랭그래프 기초 문법&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>현재/AI Agent</category>
      <author>수민 '-'</author>
      <guid isPermaLink="true">https://som-ethi-ng.tistory.com/623</guid>
      <comments>https://som-ethi-ng.tistory.com/623#entry623comment</comments>
      <pubDate>Fri, 20 Mar 2026 18:41:33 +0900</pubDate>
    </item>
    <item>
      <title>AI Agent - RAG&amp;middot;MCP&amp;middot;LangGraph로 보는 에이전트 워크플로우</title>
      <link>https://som-ethi-ng.tistory.com/622</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;AI Agent는 환경으로부터 정보를 지각(Perception)하고, 주어진 목표를 달성하기 위해 의사결정(Decision Making)을 거쳐 적절한 행동(Action)을 수행하는 지능형 주체다. 단순히 입력에 반응하는 프로그램과 달리, 에이전트는 데이터와 경험을 바탕으로 학습하며 상황에 맞게 적응한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근에는 텍스트&amp;middot;이미지&amp;middot;음성 등을 동시에 다루는 멀티모달 모델, 외부 지식을 검색해 활용하는 RAG, 그리고 도구&amp;middot;서비스&amp;middot;DB와 연결해 실행 능력을 확장하는 MCP 같은 기술이 결합되면서 &amp;ldquo;말만 잘하는 모델&amp;rdquo;에서 &amp;ldquo;일을 하는 시스템&amp;rdquo;으로 빠르게 진화하고 있다. (원문 흐름은 &lt;a href=&quot;https://ryuzyproject.tistory.com/191&quot;&gt;AI Agent&lt;/a&gt; 내용을 바탕으로 정리했다.)&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. AI Agent 한 줄 정의&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI Agent = &amp;ldquo;상황을 보고(지각) &amp;rarr; 판단하고(의사결정) &amp;rarr; 행동한다(실행)&amp;rdquo;&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;(웹, 문서, DB, 앱, 센서 등)과 상호작용하고&lt;/li&gt;
&lt;li&gt;&lt;b&gt;목표&lt;/b&gt;(예: 고객 문의 해결, 리포트 작성, 주문 실행)를 기준으로&lt;/li&gt;
&lt;li&gt;필요한 &lt;b&gt;도구 호출&lt;/b&gt;과 &lt;b&gt;반복 루프&lt;/b&gt;까지 포함하는 &amp;ldquo;작업 단위 시스템&amp;rdquo;이라는 점이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. RAG: 외부 지식을 끌고 와서 답하는 방식&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RAG(Retrieval-Augmented Generation)는 생성형 AI가 답변을 만들 때, 모델 파라미터에만 의존하지 않고 &lt;b&gt;관련 문서를 검색(Retrieval)&lt;/b&gt; 한 뒤 이를 입력 맥락에 포함시켜 &lt;b&gt;답변을 생성(Generation)&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;도메인 지식&lt;/b&gt;을 활용할 수 있다.&lt;/li&gt;
&lt;li&gt;환각(hallucination)을 줄이고 답변 신뢰도를 올릴 수 있다.&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;고객 지원(FAQ + 내부 정책 문서)&lt;/li&gt;
&lt;li&gt;법률/계약 문서 검색&lt;/li&gt;
&lt;li&gt;논문&amp;middot;기술 문서 요약&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원문 참고: &lt;a href=&quot;https://ryuzyproject.tistory.com/191&quot;&gt;AI Agent&lt;/a&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. MCP: 에이전트가 &amp;ldquo;도구&amp;rdquo;를 표준 인터페이스로 쓰게 해주는 연결고리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MCP(Model Context Protocol)는 에이전트가 외부 도구, 서비스, 데이터베이스를 &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;도구마다 API가 다르고&lt;/li&gt;
&lt;li&gt;인증/요청/응답 스키마가 제각각이라&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;MCP는 공통 인터페이스를 제공해:&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;DB 질의&lt;/li&gt;
&lt;li&gt;외부 애플리케이션 실행&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 작업을 더 일관된 구조로 다루게 해준다. 원문 참고: &lt;a href=&quot;https://ryuzyproject.tistory.com/191&quot;&gt;AI Agent&lt;/a&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. AI Agent 대표 사례&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원문에 나온 사례를 &amp;ldquo;지각 &amp;rarr; 의사결정 &amp;rarr; 행동&amp;rdquo; 관점으로 다시 보면 구조가 더 선명해진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1) &lt;b&gt;로봇청소기 (iRobot Roomba 등)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;지각: 센서/카메라로 집 구조 인식&lt;/li&gt;
&lt;li&gt;의사결정: 이동 경로 계획&lt;/li&gt;
&lt;li&gt;행동: 청소 수행&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2) &lt;b&gt;자율주행 자동차 (Tesla Autopilot, Waymo)&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;middot;라이다 등 센서로 환경 인식&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;3) &lt;b&gt;스마트 스피커 (Alexa, Google Assistant, Clova)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;지각: 음성 입력으로 요청 인식&lt;/li&gt;
&lt;li&gt;의사결정: 의도 분석&lt;/li&gt;
&lt;li&gt;행동: 음악 재생, 날씨 안내, IoT 제어 등 실행&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4) &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;/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;5) &lt;b&gt;게임 AI (AlphaGo, OpenAI Five, AlphaStar)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;지각: 게임 상태 인식&lt;/li&gt;
&lt;li&gt;의사결정: 수 선택/전략 판단&lt;/li&gt;
&lt;li&gt;행동: 실제 플레이 실행&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. AI Agent 대표 프레임워크(흐름 이미지)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원문에서 정리된 프레임워크 흐름 그림이다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdna%2Fcs5azj%2FbtsQobwGS03%2FAAAAAAAAAAAAAAAAAAAAACiJrAFkGAF_Rwe-L_fiV83H0VtarD-OmQM6fF6FTsid%2Fimg.png%3Fcredential%3DyqXZFxpELC7KVnFOS48ylbz2pIh7yKj8%26expires%3D1774969199%26allow_ip%3D%26allow_referer%3D%26signature%3DhoNK0K2qOv2ltbOh0qPuj%252BjQtqE%253D&quot; /&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 워크플로우(Workflow): 에이전트를 &amp;ldquo;일하는 시스템&amp;rdquo;으로 만드는 설계도&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;워크플로우(Workflow)는 어떤 작업을 달성하기 위해 사람이든 시스템이든 따라야 하는 단계적 절차나 흐름을 뜻한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI 에이전트 관점에서 워크플로우는 대략 이런 질문에 대한 답이다.&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;즉 &amp;ldquo;목표를 이루기 위해 어떤 노드들을 어떤 흐름으로 연결할지&amp;rdquo;를 정의하는 실행 계획이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. n8n: 워크플로우 자동화 레고 블록&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://n8n.io/&quot;&gt;n8n&lt;/a&gt;은 오픈소스 워크플로우 자동화 도구로, 노드를 이어 붙여 다양한 서비스와 AI 모델을 연결할 수 있게 해준다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이메일, DB, Slack 같은 협업 도구&lt;/li&gt;
&lt;li&gt;OpenAI, HuggingFace 같은 AI API&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;까지 연결해서 &amp;ldquo;데이터 수집 &amp;rarr; 전처리 &amp;rarr; AI 호출 &amp;rarr; 결과 전달&amp;rdquo; 같은 파이프라인을 빠르게 만들 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. RAG 워크플로우: 직선형 파이프라인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RAG 워크플로우는 보통 다음처럼 &lt;b&gt;고정된 순서&lt;/b&gt;로 움직이는 단일 경로 구조다.&lt;/p&gt;
&lt;pre class=&quot;excel&quot;&gt;&lt;code&gt;질문 입력
  &amp;rarr; 임베딩/쿼리 생성
  &amp;rarr; 문서 검색(Vector DB 등)
  &amp;rarr; 컨텍스트 합치기
  &amp;rarr; 답변 생성&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빠르고 단순해서 FAQ나 사내 문서 QA처럼 &amp;ldquo;질문-답&amp;rdquo;이 명확한 문제에 잘 맞는다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdna%2FbT2RP2%2FbtsQoQFLiyp%2FAAAAAAAAAAAAAAAAAAAAAMdWfay1v36JjVxHjFmcIEAObOYhq7g9lJL8BYOx0N01%2Fimg.webp%3Fcredential%3DyqXZFxpELC7KVnFOS48ylbz2pIh7yKj8%26expires%3D1774969199%26allow_ip%3D%26allow_referer%3D%26signature%3DUMgMPhRIEpQ9dz8eyBpH6VmsRFI%253D&quot; /&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 에이전틱 RAG 워크플로우: 분기/반성/재검색이 있는 반복형&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에이전틱 RAG(Agentic RAG)는 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;ldquo;검색이 필요한지&amp;rdquo; 판단&lt;/li&gt;
&lt;li&gt;쿼리를 재작성해 여러 번 검색&lt;/li&gt;
&lt;li&gt;검색 결과를 평가&amp;middot;반성하고 부족하면 재시도&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 반복 루프가 들어가면서, 복잡한 리서치/멀티홉 질문/장기 과제에 강해진다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdna%2FbZOb06%2FbtsQoHB42Lf%2FAAAAAAAAAAAAAAAAAAAAAOVajutW4TOc5hSEL6Yp1qhRV0cTbeIH_4W4ZDb5pKMq%2Fimg.webp%3Fcredential%3DyqXZFxpELC7KVnFOS48ylbz2pIh7yKj8%26expires%3D1774969199%26allow_ip%3D%26allow_referer%3D%26signature%3DixAfirbTBytMj7c5n3k%252BAURa57U%253D&quot; /&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 멀티홉 질의(Multi-hop Query)&lt;/h3&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;Single-hop&lt;/b&gt;: 질문 &amp;rarr; 한 문서/사실만 찾아도 답 가능&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Multi-hop&lt;/b&gt;: 여러 정보원을 찾아 순차적으로 이어야 답 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에이전틱 RAG가 강해지는 대표 케이스가 여기다. &amp;ldquo;한 번 검색으로 끝&amp;rdquo;이 아니라, 중간 결과를 보고 재검색/재정리가 필요한 경우가 많기 때문이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. LangChain과 LangGraph: &amp;ldquo;에이전트 워크플로우&amp;rdquo;를 코드로 만들기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원문 참고: &lt;a href=&quot;https://ryuzyproject.tistory.com/192&quot;&gt;랭그래프&lt;/a&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7-1. LangChain&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LangChain은 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;문서 검색(Retrieval)&lt;/li&gt;
&lt;li&gt;벡터 DB 연동&lt;/li&gt;
&lt;li&gt;체인(Chain) 구성&lt;/li&gt;
&lt;li&gt;외부 도구(Tool) 연결&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RAG나 에이전트 같은 시스템을 빠르게 구성할 때 유용하다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7-2. LangGraph&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LangGraph는 &amp;ldquo;에이전트나 RAG를 단계별로 구성하고 실행하는 그래프 기반 오케스트레이션&amp;rdquo;에 초점이 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기존 RAG가 직선형 파이프라인이라면&lt;/li&gt;
&lt;li&gt;LangGraph는 노드/엣지로 &lt;b&gt;분기, 반복, 조건 처리, 에이전트 루프&lt;/b&gt;를 명확하게 표현한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 &amp;ldquo;검색 &amp;rarr; 답변 생성 &amp;rarr; 자기평가 &amp;rarr; 재검색&amp;rdquo; 같은 흐름을 설계/디버깅하기 좋다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdna%2FdCO98Y%2FbtsQoC8BQio%2FAAAAAAAAAAAAAAAAAAAAAPoMHT7u3cW4k0Kyigd8GuwLoVUq-ucVBmmVCkkTe0M1%2Fimg.png%3Fcredential%3DyqXZFxpELC7KVnFOS48ylbz2pIh7yKj8%26expires%3D1774969199%26allow_ip%3D%26allow_referer%3D%26signature%3DMgxWn3U2yGBYImRERps5clJ0Eko%253D&quot; /&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. LangGraph의 필수 구성요소: Node, Edge, State, Flow Control&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LangGraph는 구성요소를 4개로 잡고 보면 이해가 쉽다. 원문 참고: &lt;a href=&quot;https://ryuzyproject.tistory.com/192&quot;&gt;랭그래프&lt;/a&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1) 노드(Node)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;노드는 그래프 안에서 실행되는 &amp;ldquo;작업 단위&amp;rdquo;다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;질문 임베딩하기&lt;/li&gt;
&lt;li&gt;벡터 DB에서 문서 검색하기&lt;/li&gt;
&lt;li&gt;LLM으로 답변 생성하기&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 단계가 각각 하나의 노드가 된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2) 엣지(Edge)&lt;/h3&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;rarr; 답변 생성&amp;rdquo;&lt;/li&gt;
&lt;li&gt;&amp;ldquo;답변 평가 &amp;rarr; 재검색&amp;rdquo;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 연결을 엣지로 표현한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3) 상태(State)&lt;/h3&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 질문&lt;/li&gt;
&lt;li&gt;검색 결과 문서&lt;/li&gt;
&lt;li&gt;답변 초안&lt;/li&gt;
&lt;li&gt;다음 행동(재검색할지 종료할지)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 값들이 state에 들어가고, 각 노드는 state를 읽고 업데이트한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4) 조건 분기와 루프(Flow Control)&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의 &amp;ldquo;재검색-재생성&amp;rdquo; 루프를 코드로 구현할 때 이 구조가 핵심이 된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;9. (실습 맛보기) StateGraph로 &amp;ldquo;한 노드짜리&amp;rdquo; 그래프 만들기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원문 예시 스타일을 최대한 유지해, 가장 작은 형태만 맛보기로 정리한다. 원문 참고: &lt;a href=&quot;https://ryuzyproject.tistory.com/192&quot;&gt;랭그래프&lt;/a&gt;&lt;/p&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;from typing_extensions import TypedDict
from langgraph.graph import StateGraph, START, END


class InputState(TypedDict):
    question: str


class OutputState(TypedDict):
    answer: str


class OverallState(InputState, OutputState):
    pass


def answer_node(state: InputState):
    # 실제로는 여기에서 LLM 호출이나 RAG 검색을 붙이는 형태로 확장된다.
    return {&quot;answer&quot;: &quot;bye&quot;, &quot;question&quot;: state[&quot;question&quot;]}


graph_builder = StateGraph(OverallState, input_schema=InputState, output_schema=OutputState)
graph_builder.add_node(answer_node)
graph_builder.add_edge(START, &quot;answer_node&quot;)
graph_builder.add_edge(&quot;answer_node&quot;, END)
graph = graph_builder.compile()

result = graph.invoke({&quot;question&quot;: &quot;hi&quot;})
print(result)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 작은 단위로 시작해, 노드를 &amp;ldquo;검색/생성/평가/재검색&amp;rdquo;으로 늘리면 Agentic RAG 워크플로우를 그래프로 구성할 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마치며&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;AI Agent는 지각 &amp;rarr; 의사결정 &amp;rarr; 행동의 구조를 가진 &amp;ldquo;목표 지향 시스템&amp;rdquo;이고, 최근에는 멀티모달/RAG/MCP 같은 기술과 결합하면서 실행 능력이 빠르게 확장되고 있다.&lt;/li&gt;
&lt;li&gt;RAG는 외부 지식을 검색해 컨텍스트로 붙여 답변 품질을 올리는 방식이고, MCP는 도구/서비스/DB 연동을 표준화해 에이전트를 더 쉽게 &amp;ldquo;일하는 시스템&amp;rdquo;으로 만든다.&lt;/li&gt;
&lt;li&gt;LangGraph는 노드/엣지/상태/루프로 워크플로우를 표현해, 직선형 RAG부터 반복형 Agentic RAG까지 구조적으로 설계&amp;middot;디버깅하기 좋다.&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://ryuzyproject.tistory.com/191&quot;&gt;AI Agent&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ryuzyproject.tistory.com/192&quot;&gt;랭그래프&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>현재/AI Agent</category>
      <author>수민 '-'</author>
      <guid isPermaLink="true">https://som-ethi-ng.tistory.com/622</guid>
      <comments>https://som-ethi-ng.tistory.com/622#entry622comment</comments>
      <pubDate>Thu, 19 Mar 2026 18:39:20 +0900</pubDate>
    </item>
    <item>
      <title>PPO - Proximal Policy Optimization 정책 업데이트 안정화</title>
      <link>https://som-ethi-ng.tistory.com/621</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;PPO(Proximal Policy Optimization)는 강화학습에서 &lt;b&gt;정책(Policy)&lt;/b&gt; 을 더 안정적으로 업데이트하기 위해 제안된 정책 기반 알고리즘이다.&lt;br /&gt;기존의 정책 경사(Policy Gradient) 방식은 정책을 한 번에 크게 바꾸면 학습이 흔들릴 수 있는데, PPO는 &lt;b&gt;이전 정책에서 너무 멀리 벗어나지 않도록&lt;/b&gt; 제한을 둔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 위해 &lt;b&gt;ratio&lt;/b&gt;(확률 비율)를 계산하고, 허용 범위를 벗어나면 업데이트 크기를 제한하는 &lt;b&gt;clipped objective&lt;/b&gt; 를 사용한다.&lt;br /&gt;그리고 Actor-Critic 구조를 기반으로 Actor는 행동 정책을 학습하고, Critic은 상태 가치(Value)를 추정해 Advantage를 계산한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PPO는 구현이 비교적 단순하면서도 안정성과 성능이 좋아서 OpenAI Baselines, Stable-Baselines, RLlib 같은 프레임워크에서 널리 쓰이는 대표적인 알고리즘 중 하나다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. PPO가 등장한 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정책 업데이트를 한 번 할 때 기존 정책과 너무 크게 달라지면, 학습이 불안정해지고 성능이 갑자기 떨어질 수 있다.&lt;br /&gt;그래서 &amp;ldquo;업데이트를 하되, 정책이 과도하게 바뀌지 않게 하자&amp;rdquo;는 문제의식이 생겼고, 그 해법 계열이 TRPO와 PPO로 이어진다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. TRPO&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TRPO는 정책이 이전 정책에서 너무 멀어지지 않도록 &lt;b&gt;KL Divergence&lt;/b&gt; 로 변화량을 제한한다.&lt;br /&gt;다만 다음과 같은 이유로 구현이 부담스럽다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;1) 구현이 매우 복잡함
2) 계산량이 많음
3) 헤시안 행렬(Hessian Matrix)을 사용&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. PPO 핵심 아이디어&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PPO는 &amp;ldquo;정책을 바꾸긴 바꾸되, 너무 멀리 가지는 말자&amp;rdquo;가 목표다.&lt;br /&gt;핵심은 &lt;b&gt;Policy(확률 비율), Advantage, Clip&lt;/b&gt; 이 세 덩어리로 정리된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. Policy&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&amp;pi;(a|s): 상태 s에서 행동 a를 할 확률&lt;/li&gt;
&lt;li&gt;&amp;pi;_old(a|s): 이전 정책이 같은 행동을 선택할 확률&lt;/li&gt;
&lt;li&gt;ratio: 현재 정책의 행동 확률 / 이전 정책의 행동 확률&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ratio 값은 보통 1이면 &amp;ldquo;변화 없음&amp;rdquo;으로 해석한다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdna%2FbehUHH%2FdJMcacJbdg7%2FAAAAAAAAAAAAAAAAAAAAAOu8G2vdloAnLdPR8Yau0X3JgrBlerxq9RUT_WEc-vYU%2Fimg.png%3Fcredential%3DyqXZFxpELC7KVnFOS48ylbz2pIh7yKj8%26expires%3D1774969199%26allow_ip%3D%26allow_referer%3D%26signature%3DVTLiM6yI9%252FNB0u9ob6tzsORdJ%252BM%253D&quot; /&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. Advantage&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PPO는 Advantage Actor-Critic 구조를 사용한다.&lt;br /&gt;여기서 Advantage는 &amp;ldquo;해당 행동이 평균보다 얼마나 좋은가&amp;rdquo;를 의미한다. 즉, 상태 가치 대비 행동 가치의 상대적인 차이다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdna%2FbriM8Q%2FdJMcaiiiuBD%2FAAAAAAAAAAAAAAAAAAAAANsoTR09mznzp1ziFFnD7T8KNy9T7EwQqgKiNdYIxI_6%2Fimg.png%3Fcredential%3DyqXZFxpELC7KVnFOS48ylbz2pIh7yKj8%26expires%3D1774969199%26allow_ip%3D%26allow_referer%3D%26signature%3DExbOevHcGtIbMcoLAjBAljp92YA%253D&quot; /&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. PPO Clipped Objective&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PPO의 목표 함수는 ratio와 Advantage를 묶고, ratio가 허용 범위를 벗어나면 clip으로 업데이트를 제한한다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdna%2FcgUSRV%2FdJMcaaEA25d%2FAAAAAAAAAAAAAAAAAAAAAJDLExiDeYXEitqro8aW_YkZbY66WNErYt8A7hH46Rsx%2Fimg.png%3Fcredential%3DyqXZFxpELC7KVnFOS48ylbz2pIh7yKj8%26expires%3D1774969199%26allow_ip%3D%26allow_referer%3D%26signature%3Ddi1hDt1LKyS%252BWLxJ1OmAQfo8Wgg%253D&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;epsilon;는 보통 0.1 ~ 0.2 범위에서 많이 시작한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. Clip&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PPO에서 ratio는 &amp;ldquo;이전 정책 대비 현재 정책이 해당 행동의 확률을 얼마나 바꿨는가&amp;rdquo;를 의미한다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdna%2FbRfigb%2FdJMcahwWK8i%2FAAAAAAAAAAAAAAAAAAAAACl6wFgrcA1xmgcCrCJKT44G7dNKVick0qZnIvL2_FRb%2Fimg.png%3Fcredential%3DyqXZFxpELC7KVnFOS48ylbz2pIh7yKj8%26expires%3D1774969199%26allow_ip%3D%26allow_referer%3D%26signature%3Dzs3qerfyrecIiHKM3LVREaO5VH0%253D&quot; /&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PPO의 목표는 &amp;ldquo;정책이 너무 많이 바뀌지 않게 하자!&amp;rdquo; 입니다.&lt;br /&gt;그래서 허용 범위를 정하고, 그 밖이면 업데이트를 제한합니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;&amp;epsilon; = 0.2 일 때, 허용 범위는
1 - &amp;epsilon; = 0.8
1 + &amp;epsilon; = 1.2
ratio는 0.8 ~ 1.2 사이까지만 허용&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;예를 들어
old policy = 0.4
new policy = 0.42
ratio = 0.42 / 0.4 = 1.05
그러면 허용 범위(0.8 ~ 1.2)이므로 정상 업데이트합니다.&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;만약 ratio가 1.5일 경우
PPO의 Clip은 정책이 이전 정책에서 너무 크게 변하지 않도록 업데이트 크기를 제한함
(ratio = 1.2로 강제로 제한)&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. PPO 전체 알고리즘&lt;/h2&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;초기 정책 &amp;pi;&amp;theta; 설정
반복:
    환경에서 데이터를 수집
        state
        action
        reward
    Advantage 계산
    여러 epoch 동안 반복
        ratio 계산
        clip 적용
        loss 계산
        gradient 업데이트&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 코드는 &lt;code&gt;CartPole-v1&lt;/code&gt; 같은 이산 행동 환경에서 PPO를 직접 구현한 예시다.&lt;/p&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;import gymnasium as gym
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torch.distributions import Categorical

# Hyperparameters
learning_rate = 0.0005
gamma = 0.98

# GAE: 여러 스텝의 TD 오차를 적당히 섞어서 안정적으로 advantage를 계산하기 위한 아이디어
lmbda = 0.95
eps_clip = 0.1
K_epoch = 3
T_horizon = 20


class PPO(nn.Module):
    def __init__(self):
        super(PPO, self).__init__()
        self.data = []

        self.fc1 = nn.Linear(4, 256)
        self.fc_pi = nn.Linear(256, 2)
        self.fc_v = nn.Linear(256, 1)
        self.optimizer = optim.Adam(self.parameters(), lr=learning_rate)

    def pi(self, x, softmax_dim=0):
        x = F.relu(self.fc1(x))
        x = self.fc_pi(x)
        prob = F.softmax(x, dim=softmax_dim)
        return prob

    def v(self, x):
        x = F.relu(self.fc1(x))
        v = self.fc_v(x)
        return v

    def put_data(self, transition):
        self.data.append(transition)

    def make_batch(self):
        s_lst, a_lst, r_lst, s_prime_lst, prob_a_lst, done_lst = [], [], [], [], [], []

        for transition in self.data:
            s, a, r, s_prime, prob_a, done = transition

            s_lst.append(s)
            a_lst.append([a])
            r_lst.append([r])
            s_prime_lst.append(s_prime)
            prob_a_lst.append([prob_a])

            done_mask = 0 if done else 1
            done_lst.append([done_mask])

        s = torch.tensor(np.array(s_lst), dtype=torch.float)
        a = torch.tensor(np.array(a_lst), dtype=torch.long)
        r = torch.tensor(np.array(r_lst), dtype=torch.float)
        s_prime = torch.tensor(np.array(s_prime_lst), dtype=torch.float)
        done_mask = torch.tensor(np.array(done_lst), dtype=torch.float)
        prob_a = torch.tensor(np.array(prob_a_lst), dtype=torch.float)

        self.data = []
        return s, a, r, s_prime, done_mask, prob_a

    def train_net(self):
        s, a, r, s_prime, done_mask, prob_a = self.make_batch()

        for _ in range(K_epoch):
            td_target = r + gamma * self.v(s_prime) * done_mask
            delta = td_target - self.v(s)
            delta = delta.detach().cpu().numpy()

            # advantage 계산 (delta를 뒤에서부터 누적)
            advantage_lst = []
            advantage = 0.0
            for delta_t in delta[::-1]:
                advantage = gamma * lmbda * advantage + delta_t[0]
                advantage_lst.append([advantage])

            advantage_lst.reverse()
            advantage = torch.tensor(advantage_lst, dtype=torch.float)

            # 현재 정책에서 행동 a의 확률
            pi = self.pi(s, softmax_dim=1)
            pi_a = pi.gather(1, a)

            # ratio = pi_new(a|s) / pi_old(a|s)
            ratio = torch.exp(torch.log(pi_a + 1e-8) - torch.log(prob_a + 1e-8))

            surr1 = ratio * advantage
            surr2 = torch.clamp(ratio, 1 - eps_clip, 1 + eps_clip) * advantage

            loss = -torch.min(surr1, surr2) + F.smooth_l1_loss(
                self.v(s), td_target.detach()
            )

            self.optimizer.zero_grad()
            loss.mean().backward()
            self.optimizer.step()


def main():
    env = gym.make('CartPole-v1')
    model = PPO()
    score = 0.0
    print_interval = 20

    for n_epi in range(10000):
        s, _ = env.reset()
        done = False

        while not done:
            for _ in range(T_horizon):
                prob = model.pi(torch.from_numpy(s).float())
                m = Categorical(prob)
                a = m.sample().item()

                s_prime, r, terminated, truncated, info = env.step(a)
                done = terminated or truncated

                model.put_data((s, a, r / 100.0, s_prime, prob[a].item(), done))
                s = s_prime
                score += r

                if done:
                    break

            model.train_net()

        if n_epi % print_interval == 0 and n_epi != 0:
            print(&quot;# of episode :{}, avg score : {:.1f}&quot;.format(n_epi, score / print_interval))
            score = 0.0

    env.close()


if __name__ == '__main__':
    main()&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 그 외 강화학습&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;World Model RL&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;머릿 속 시뮬레이션처럼 &amp;ldquo;가상의 미래&amp;rdquo;를 상상하면서 학습한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Transformer RL&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좋은 행동 패턴을 시퀀스로 보고, Transformer 기반 학습으로 정책을 만든다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Offline RL&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;환경에서 새 데이터를 모으지 않고, 이미 수집된 데이터셋만으로 학습한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;SAC&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SAC(Soft Actor-Critic)은 최대 엔트로피(Maximum Entropy) 강화학습 기반의 Off-policy Actor-Critic 알고리즘이다.&lt;br /&gt;보상(reward)을 최대화하는 것뿐 아니라 정책의 엔트로피(무작위성)도 함께 최대화해, 특정 행동에 과도하게 수렴하는 것을 막고 더 다양한 탐색을 유도한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;연속 행동 공간(continuous action space)에서 특히 강력한 성능을 보이며, 로봇 제어 같은 실제 환경에서도 널리 쓰인다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;MuZero&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MuZero는 DeepMind의 모델 기반 강화학습 알고리즘이다.&lt;br /&gt;환경의 정확한 규칙이나 모델을 미리 알지 못해도, 학습 과정에서 world model을 함께 학습해 의사결정을 수행할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MuZero는 representation network, dynamics network, prediction network를 함께 학습하고, 의사결정을 위해 MCTS를 사용한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;MCTS&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MCTS(Monte Carlo Tree Search)는 가능한 행동들을 트리로 확장하면서, 여러 번의 시뮬레이션 통계를 기반으로 가장 좋은 행동을 찾는 탐색 알고리즘이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Selection &amp;rarr; Expansion &amp;rarr; Simulation &amp;rarr; Backpropagation의 네 단계를 반복하며, 점점 더 유망한 가지에 탐색 자원을 집중한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Dreamer&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Dreamer는 실제 환경에서 상호작용하며 학습하기보다, 학습된 world model 내부에서 미래를 &amp;ldquo;상상&amp;rdquo;하며 정책을 학습하는 모델 기반 강화학습 계열이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;잠재 상태(latent state) 기반 동적 모델을 학습하고, 그 모델로 가상의 trajectory를 만들어 정책을 업데이트한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Decision Transformer&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Decision Transformer는 강화학습 문제를 시퀀스 모델링으로 바꾸는 접근이다.&lt;br /&gt;Transformer에 과거의 상태, 행동, 목표 보상(return)을 하나의 시퀀스로 넣고, 다음 행동을 예측한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존처럼 반복적으로 정책/가치를 갱신하는 대신, 오프라인 데이터셋을 활용해 지도학습 방식으로 학습한다. Return-to-Go를 조건으로 넣을 수 있어 &amp;ldquo;원하는 성능 수준에 맞춰 행동 생성&amp;rdquo;이 가능하다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마치며&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;PPO는 정책 업데이트를 너무 크게 만들지 않도록 ratio 기반으로 Clip을 적용하는 방식이다.&lt;/li&gt;
&lt;li&gt;Advantage Actor-Critic으로 학습 신호를 안정적으로 만들고, clipped objective로 정책 변화를 제한한다.&lt;/li&gt;
&lt;li&gt;구현이 어렵지 않은 편인데도 안정성과 성능이 좋아서 실무/프레임워크에서 기본 알고리즘으로 자주 사용된다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>현재/강화학습</category>
      <author>수민 '-'</author>
      <guid isPermaLink="true">https://som-ethi-ng.tistory.com/621</guid>
      <comments>https://som-ethi-ng.tistory.com/621#entry621comment</comments>
      <pubDate>Wed, 18 Mar 2026 18:37:18 +0900</pubDate>
    </item>
    <item>
      <title>A2C - Advantage Actor-Critic 동기식 병렬 학습</title>
      <link>https://som-ethi-ng.tistory.com/620</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;A3C를 동기식으로 단순화한 Actor-Critic&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;A2C(Advantage Actor-Critic)는 이름 그대로 &lt;b&gt;Advantage 기반 Actor-Critic&lt;/b&gt; 구조를 사용하면서, A3C처럼 완전히 비동기로 학습하지 않고 &lt;b&gt;동기식(synchronous)&lt;/b&gt; 으로 여러 환경에서 수집한 데이터를 한 번에 batch로 처리하는 강화학습 알고리즘이다.&lt;br /&gt;여러 worker 환경을 병렬로 돌리면서도, 일정 step마다 모든 경험을 모아 &lt;b&gt;한 번에 모델을 업데이트&lt;/b&gt;하기 때문에 학습이 안정적이고 GPU로 처리하기도 쉽다. 이 구조는 이후 PPO 등 많은 실전 알고리즘의 기반이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글에서는 A3C와의 차이, A2C 구조 및 장점, 그리고 &lt;code&gt;CartPole-v1&lt;/code&gt; 환경을 대상으로 한 &lt;b&gt;동기식 병렬 Actor-Critic 구현 코드&lt;/b&gt;까지 정리한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. A3C vs A2C 한눈에 비교&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 A3C와 A2C의 가장 큰 차이를 그림으로 정리해보자.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1-1. A3C &amp;ndash; 비동기 업데이트&lt;/h3&gt;
&lt;pre class=&quot;xl&quot;&gt;&lt;code&gt;Worker1 -&amp;gt; gradient -&amp;gt; Global
Worker2 -&amp;gt; gradient -&amp;gt; Global
Worker3 -&amp;gt; gradient -&amp;gt; Global
...&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;각 worker가 &lt;b&gt;자기 local 모델&lt;/b&gt; 기준으로 gradient를 계산하고,&lt;/li&gt;
&lt;li&gt;그 결과를 &lt;b&gt;전역 모델(global network)&lt;/b&gt; 에 바로 적용한다.&lt;/li&gt;
&lt;li&gt;서로를 기다리지 않기 때문에 &lt;b&gt;비동기(asynchronous)&lt;/b&gt; 이고, 그만큼 빠르지만 gradient가 서로 덮어쓰는 문제가 생길 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1-2. A2C &amp;ndash; 동기식 업데이트&lt;/h3&gt;
&lt;pre class=&quot;xl&quot;&gt;&lt;code&gt;Worker1 -&amp;gt; 환경 탐험
Worker2 -&amp;gt; 환경 탐험
Worker3 -&amp;gt; 환경 탐험
Worker4 -&amp;gt; 환경 탐험
        &amp;darr;
    데이터 모음 (batch)
        &amp;darr;
Actor + Critic 동기 업데이트&lt;/code&gt;&lt;/pre&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;여러 worker(환경)가 동시에 rollout을 수행해 데이터를 모은다.&lt;/li&gt;
&lt;li&gt;일정 step마다 &lt;b&gt;모든 worker의 데이터를 모아 하나의 batch&lt;/b&gt; 로 만든다.&lt;/li&gt;
&lt;li&gt;그 batch를 이용해 Actor와 Critic을 &lt;b&gt;한 번에 동기적으로&lt;/b&gt; 업데이트한다.&lt;/li&gt;
&lt;/ol&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;큰 batch를 만들어 GPU로 돌리기 좋으며,&lt;/li&gt;
&lt;li&gt;한 번의 업데이트가 더 많은 데이터를 보고 학습하므로 신호가 더 &lt;b&gt;안정적&lt;/b&gt;이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. A3C의 한계와 A2C가 필요한 이유&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-1. Gradient 충돌(Interference)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 worker가 동시에 글로벌 모델을 업데이트하면 다음과 같은 일이 생길 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;worker1이 막 gradient를 적용해 global 파라미터를 바꾼 직후,&lt;/li&gt;
&lt;li&gt;worker2가 &lt;b&gt;조금 전에 백업해둔 파라미터&lt;/b&gt; 기준으로 계산한 gradient를 그대로 덮어써 버린다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 현상을 &lt;b&gt;gradient interference(충돌)&lt;/b&gt; 라고 부르고, 이 때문에 학습이 비효율적이거나 불안정해질 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-2. 학습 불안정과 재현성 문제&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;가 계속 바뀐다.&lt;/li&gt;
&lt;li&gt;global 파라미터가 예측하기 어려운 타이밍에 바뀌기 때문에, 결과가 run마다 조금씩 달라지기 쉽다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;연구용 실험이라면 괜찮을 수 있지만, 실무에서는 &lt;b&gt;&amp;ldquo;동일 설정이면 비슷한 결과가 나와야 한다&amp;rdquo;&lt;/b&gt; 는 재현성이 중요하기 때문에 단점이 된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-3. GPU 활용 어려움&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;A3C는 구조적으로 &lt;b&gt;CPU 멀티프로세싱&lt;/b&gt; 에 초점을 맞춘 구조다.&lt;/li&gt;
&lt;li&gt;GPU는 큰 batch를 한 번에 처리할수록 효율이 좋은데,&lt;/li&gt;
&lt;li&gt;A3C처럼 작은 gradient 업데이트를 자주 보내는 패턴은 GPU를 제대로 활용하기 어렵다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 세 가지 이유 때문에, &amp;ldquo;A3C의 멀티 환경 병렬 수집&amp;rdquo;이라는 장점은 유지하면서도, &amp;ldquo;업데이트는 한 번에 동기식으로&amp;rdquo; 처리하는 A2C가 등장했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. A2C 구조: 동기식 Advantage Actor-Critic&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;A2C의 아이디어는 단순하다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;경험 수집은 여럿이 병렬로 하되,&lt;br /&gt;정책/가치 업데이트는 한 번에 동기식으로 하자.&lt;/p&gt;
&lt;/blockquote&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;여러 환경(worker)이 동시에 rollout을 진행한다.&lt;/li&gt;
&lt;li&gt;일정 step마다 &lt;code&gt;(state, action, reward, done, next_state)&lt;/code&gt; 를 모두 모아 큰 batch를 만든다.&lt;/li&gt;
&lt;li&gt;이 batch로 Actor와 Critic을 함께 업데이트한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Actor-Critic 구조 자체는 A3C와 동일하고, 변경되는 것은 &lt;b&gt;업데이트 타이밍과 방식(동기/비동기)&lt;/b&gt; 이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. A2C의 장점 정리&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-1. 학습 안정성&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모든 gradient가 &lt;b&gt;같은 시점의 파라미터&lt;/b&gt; 기준으로 계산된다.&lt;/li&gt;
&lt;li&gt;A3C처럼 worker 간 gradient가 서로 덮어쓰는 문제가 없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-2. GPU 활용 용이&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;여러 환경에서 모은 데이터를 하나의 큰 batch로 묶어 GPU에서 처리할 수 있다.&lt;/li&gt;
&lt;li&gt;대규모 네트워크, 복잡한 환경에서도 효율적인 학습이 가능하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-3. 구현 단순성&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;공유 메모리, 락(lock), 비동기 통신 같은 난이도 높은 부분을 많이 줄일 수 있다.&lt;/li&gt;
&lt;li&gt;&amp;ldquo;여러 환경을 하나의 벡터화된 환경처럼 다루는 패턴&amp;rdquo;만 구현하면 되기 때문에 코드 이해도 더 쉽다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-4. 재현성 증가&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;업데이트 순서가 고정된 동기식 방식이라, 같은 시드&amp;middot;같은 설정이라면 결과가 훨씬 더 잘 재현된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. CartPole-v1 A2C 구현 &amp;ndash; ParallelEnv와 메인 학습 루프&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 &lt;code&gt;CartPole-v1&lt;/code&gt; 환경을 대상으로, A2C 구조를 어떻게 코드로 옮기는지 살펴본다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;ParallelEnv&lt;/code&gt; 클래스로 &lt;b&gt;여러 환경을 한 번에 처리&lt;/b&gt;하고,&lt;/li&gt;
&lt;li&gt;메인 프로세스에서는 이를 하나의 벡터화된 환경처럼 다루면서,&lt;/li&gt;
&lt;li&gt;Actor-Critic 모델을 &lt;b&gt;동기식 batch 업데이트&lt;/b&gt;로 학습하는 구조다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5-1. Actor-Critic 모델 정의&lt;/h3&gt;
&lt;pre class=&quot;haskell&quot;&gt;&lt;code&gt;import gymnasium as gym
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torch.distributions import Categorical
import torch.multiprocessing as mp
import numpy as np

# Hyperparameters
n_train_processes = 3
learning_rate = 0.0002
update_interval = 5
gamma = 0.98
max_train_steps = 60000
PRINT_INTERVAL = update_interval * 100


class ActorCritic(nn.Module):
    def __init__(self):
        super().__init__()
        self.fc1 = nn.Linear(4, 256)
        self.fc_pi = nn.Linear(256, 2)
        self.fc_v = nn.Linear(256, 1)

    def pi(self, x, softmax_dim=1):
        x = F.relu(self.fc1(x))
        x = self.fc_pi(x)
        prob = F.softmax(x, dim=softmax_dim)
        return prob

    def v(self, x):
        x = F.relu(self.fc1(x))
        v = self.fc_v(x)
        return v&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;상태 차원 4(&lt;code&gt;CartPole&lt;/code&gt;) &amp;rarr; 은닉층 256 &amp;rarr;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;정책 네트워크: 행동 2개(왼쪽/오른쪽)에 대한 확률&lt;/li&gt;
&lt;li&gt;가치 네트워크: 스칼라 (V(s))&lt;/li&gt;
&lt;/ul&gt;
&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. worker 프로세스 &amp;ndash; 환경을 대신 step 해주기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 worker는 실제 &lt;code&gt;gym&lt;/code&gt; 환경을 가지고, 메인 프로세스로부터 받은 명령에 따라 step을 수행한다.&lt;/p&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;def worker(worker_id, master_end, worker_end):
    # worker 프로세스에서는 master_end를 사용하지 않음
    master_end.close()
    env = gym.make(&quot;CartPole-v1&quot;)

    # worker마다 다른 시드로 시작 (경험 다양성)
    obs, _ = env.reset(seed=worker_id)

    while True:
        # 메인 프로세스로부터 명령(step, reset, close 등) 수신
        cmd, data = worker_end.recv()

        if cmd == &quot;step&quot;:
            obs, reward, terminated, truncated, info = env.step(int(data))
            done = terminated or truncated

            if done:
                obs, _ = env.reset()

            # step 결과를 메인 프로세스로 전달
            worker_end.send((obs, reward, done, info))

        elif cmd == &quot;reset&quot;:
            obs, _ = env.reset()
            worker_end.send(obs)

        elif cmd == &quot;close&quot;:
            env.close()
            worker_end.close()
            break

        elif cmd == &quot;get_spaces&quot;:
            worker_end.send((env.observation_space, env.action_space))

        else:
            raise NotImplementedError(f&quot;Unknown command: {cmd}&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;메인 프로세스는 환경을 직접 건드리지 않고, &lt;b&gt;worker에게 명령을 보내는 역할&lt;/b&gt;만 한다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;step&lt;/code&gt;, &lt;code&gt;reset&lt;/code&gt;, &lt;code&gt;close&lt;/code&gt; 등을 파이프를 통해 전달하고, worker는 그 결과를 다시 돌려준다.&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. ParallelEnv &amp;ndash; 여러 환경을 한꺼번에 다루기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;ParallelEnv&lt;/code&gt;는 여러 개의 &lt;code&gt;worker&lt;/code&gt;를 통합해서, 마치 &lt;b&gt;하나의 벡터화된 환경&lt;/b&gt;처럼 사용할 수 있게 해준다.&lt;/p&gt;
&lt;pre class=&quot;ruby&quot;&gt;&lt;code&gt;class ParallelEnv:
    def __init__(self, n_train_processes):
        self.nenvs = n_train_processes
        self.waiting = False
        self.closed = False
        self.workers = []

        # 환경 개수만큼 파이프 생성
        master_ends, worker_ends = zip(*[mp.Pipe() for _ in range(self.nenvs)])
        self.master_ends = master_ends
        self.worker_ends = worker_ends

        # 각 환경마다 worker 프로세스 생성
        for worker_id, (master_end, worker_end) in enumerate(zip(master_ends, worker_ends)):
            p = mp.Process(target=worker, args=(worker_id, master_end, worker_end))
            p.daemon = True
            p.start()
            self.workers.append(p)

        # master는 worker_end를 사용하지 않으므로 닫음
        for worker_end in worker_ends:
            worker_end.close()&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;ruby&quot;&gt;&lt;code&gt;    def step_async(self, actions):
        # 여러 환경에 행동을 먼저 보내는 함수
        for master_end, action in zip(self.master_ends, actions):
            master_end.send((&quot;step&quot;, int(action)))
        self.waiting = True

    def step_wait(self):
        # 모든 worker의 step 결과를 받아옴
        results = [master_end.recv() for master_end in self.master_ends]
        self.waiting = False
        obs, rews, dones, infos = zip(*results)
        return (
            np.stack(obs).astype(np.float32),
            np.array(rews, dtype=np.float32),
            np.array(dones, dtype=np.bool_),
            infos,
        )

    def reset(self):
        for master_end in self.master_ends:
            master_end.send((&quot;reset&quot;, None))
        return np.stack([master_end.recv() for master_end in self.master_ends]).astype(np.float32)

    def step(self, actions):
        self.step_async(actions)
        return self.step_wait()&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;code&gt;step(actions)&lt;/code&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;모든 worker에서 결과를 받아,&lt;/li&gt;
&lt;li&gt;&lt;code&gt;(nenvs, obs_dim)&lt;/code&gt; 형태의 관측값 배열과 reward, done 벡터를 반환한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;메인 코드 입장에서는 여러 환경이 &lt;b&gt;벡터화된 하나의 환경&lt;/b&gt;처럼 보이게 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;ruby&quot;&gt;&lt;code&gt;    def close(self):
        if self.closed:
            return

        if self.waiting:
            _ = [master_end.recv() for master_end in self.master_ends]

        for master_end in self.master_ends:
            master_end.send((&quot;close&quot;, None))

        for worker in self.workers:
            worker.join()

        self.closed = True&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;학습이 끝난 뒤에는 모든 worker와 환경을 정리해 준다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. 테스트 함수 &amp;ndash; 현재 정책 성능 확인&lt;/h2&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;def test(step_idx, model):
    env = gym.make(&quot;CartPole-v1&quot;)
    score = 0.0
    num_test = 10

    for _ in range(num_test):
        s, _ = env.reset()
        done = False

        while not done:
            with torch.no_grad():
                prob = model.pi(torch.from_numpy(s).float(), softmax_dim=0)
            a = Categorical(prob).sample().item()

            s_prime, r, terminated, truncated, info = env.step(a)
            done = terminated or truncated

            s = s_prime
            score += r

    print(f&quot;Step # : {step_idx}, avg score : {score / num_test:.1f}&quot;)
    env.close()&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;일정 step마다 현재 Actor-Critic 모델의 성능을 테스트한다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;softmax_dim=0&lt;/code&gt; 으로 단일 상태에 대한 행동 확률을 구하고, 샘플링해서 episode를 진행한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;9. n-step TD target 계산 &amp;ndash; &lt;code&gt;compute_target&lt;/code&gt;&lt;/h2&gt;
&lt;pre class=&quot;ruby&quot;&gt;&lt;code&gt;def compute_target(v_final, r_lst, mask_lst):
    G = v_final.reshape(-1)  # 마지막 상태 가치 (배열)
    td_target = []

    # 보상과 마스크를 뒤에서부터 거꾸로 순회하면서 n-step return 계산
    for r, mask in zip(r_lst[::-1], mask_lst[::-1]):
        G = r + gamma * G * mask
        td_target.append(G)

    td_target.reverse()
    return torch.tensor(np.array(td_target), dtype=torch.float32)&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;code&gt;v_final&lt;/code&gt;: rollout 마지막 상태들의 가치 (V(s_{\text{final}}))&lt;/li&gt;
&lt;li&gt;&lt;code&gt;r_lst&lt;/code&gt;: 각 step마다의 보상 벡터&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mask_lst&lt;/code&gt;: done이면 0, 아니면 1&lt;/li&gt;
&lt;li&gt;뒤에서부터 누적하면서 (G&lt;i&gt;t = r_t + \gamma G&lt;/i&gt;{t+1}) 형태로 n-step Return을 만든 다음, 앞 방향 순서로 되돌린다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;10. 메인 학습 루프 &amp;ndash; A2C의 핵심&lt;/h2&gt;
&lt;pre class=&quot;maxima&quot;&gt;&lt;code&gt;def main():
    envs = ParallelEnv(n_train_processes)

    model = ActorCritic()
    optimizer = optim.Adam(model.parameters(), lr=learning_rate)

    step_idx = 0
    s = envs.reset()

    while step_idx &amp;lt; max_train_steps:
        s_lst, a_lst, r_lst, mask_lst = [], [], [], []

        for _ in range(update_interval):
            with torch.no_grad():
                prob = model.pi(torch.from_numpy(s).float(), softmax_dim=1)

            a = Categorical(prob).sample().numpy()
            s_prime, r, done, info = envs.step(a)

            s_lst.append(s.copy())
            a_lst.append(a.copy())
            r_lst.append(r / 100.0)
            mask_lst.append(1.0 - done.astype(np.float32))

            s = s_prime
            step_idx += 1&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;code&gt;(nenvs, obs_dim)&lt;/code&gt; 형태의 상태 &lt;code&gt;s&lt;/code&gt;에서 정책을 돌려, 각 환경마다 하나씩 행동을 샘플링한다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;envs.step(a)&lt;/code&gt; 로 모든 환경을 동시에 한 스텝 진행한다.&lt;/li&gt;
&lt;li&gt;reward를 &lt;code&gt;r / 100.0&lt;/code&gt; 으로 스케일링한다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mask = 1 - done&lt;/code&gt; 으로, episode가 끝난 환경에 대해서는 이후 가치가 0이 되도록 만든다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;        s_final = torch.from_numpy(s_prime).float()
        with torch.no_grad():
            # 마지막 상태의 가치 (n-step return 시작점)
            v_final = model.v(s_final).cpu().numpy()

        td_target = compute_target(v_final, r_lst, mask_lst)

        td_target_vec = td_target.reshape(-1)
        # 상태 리스트를 텐서로 변환 (전체 샘플 수, 4)
        s_vec = torch.tensor(np.array(s_lst), dtype=torch.float32).reshape(-1, 4)
        # 행동을 (N, 1) 형태로 변환
        a_vec = torch.tensor(np.array(a_lst), dtype=torch.long).reshape(-1).unsqueeze(1)

        values = model.v(s_vec).reshape(-1)
        advantage = td_target_vec - values

        pi = model.pi(s_vec, softmax_dim=1)
        pi_a = pi.gather(1, a_vec).reshape(-1)

        loss = -(torch.log(pi_a + 1e-8) * advantage.detach()).mean() \
               + F.smooth_l1_loss(values, td_target_vec)

        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

        if step_idx % PRINT_INTERVAL == 0:
            test(step_idx, model)

    envs.close()&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;code&gt;s_lst&lt;/code&gt;, &lt;code&gt;a_lst&lt;/code&gt;, &lt;code&gt;r_lst&lt;/code&gt; 등을 모두 펼쳐서 &lt;b&gt;하나의 큰 batch&lt;/b&gt; 로 만든다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;values = V(s)&lt;/code&gt;, &lt;code&gt;td_target_vec&lt;/code&gt; 을 이용해 Advantage를 계산한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;정책 손실&lt;/b&gt;: (-\log \pi(a|s) \cdot \text{Advantage}) 의 평균&lt;/li&gt;
&lt;li&gt;&lt;b&gt;가치 손실&lt;/b&gt;: (V(s))와 TD target 사이의 Huber loss&lt;/li&gt;
&lt;li&gt;두 손실을 합쳐 backward와 optimizer step을 수행한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;if __name__ == &quot;__main__&quot;:
    mp.set_start_method(&quot;spawn&quot;, force=True)
    main()&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;code&gt;spawn&lt;/code&gt; 모드는 맥OS&amp;middot;윈도우 등에서 멀티프로세싱을 안전하게 쓰기 위해 필수적인 설정이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;11. A3C vs A2C 요약 정리&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;A3C&lt;/th&gt;
&lt;th&gt;A2C&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;업데이트 방식&lt;/td&gt;
&lt;td&gt;비동기(asynchronous)&lt;/td&gt;
&lt;td&gt;동기(synchronous)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;경험 수집&lt;/td&gt;
&lt;td&gt;여러 worker가 환경 병렬 탐험&lt;/td&gt;
&lt;td&gt;여러 worker가 환경 병렬 탐험&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gradient 적용&lt;/td&gt;
&lt;td&gt;각 worker가 global에 바로 적용&lt;/td&gt;
&lt;td&gt;모든 worker 데이터를 모아서 한 번에 적용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;안정성&lt;/td&gt;
&lt;td&gt;gradient interference 가능&lt;/td&gt;
&lt;td&gt;gradient 충돌 없음, 더 안정적&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPU 활용&lt;/td&gt;
&lt;td&gt;CPU 멀티프로세싱 위주&lt;/td&gt;
&lt;td&gt;큰 batch로 GPU 학습에 적합&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;구현 난이도&lt;/td&gt;
&lt;td&gt;공유 메모리&amp;middot;비동기 처리 등으로 상대적 고난도&lt;/td&gt;
&lt;td&gt;상대적으로 단순, 코드 구조가 명확&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;후속 알고리즘&lt;/td&gt;
&lt;td&gt;A3C 자체 사용은 줄어드는 추세&lt;/td&gt;
&lt;td&gt;PPO 등 많은 알고리즘이 A2C 스타일을 기반으로 발전&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마치며&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;A3C&lt;/b&gt;는 여러 에이전트를 비동기로 돌려 global 모델을 업데이트하는 구조로, 데이터 효율과 탐험 성능을 크게 끌어올렸다.&lt;/li&gt;
&lt;li&gt;하지만 비동기 구조 특성상 gradient 충돌, 재현성, GPU 활용 측면에서 한계가 있었고, 이를 해결하기 위해 등장한 것이 &lt;b&gt;A2C&lt;/b&gt;다.&lt;/li&gt;
&lt;li&gt;A2C처럼 여러 환경에서 &lt;b&gt;동시에 병렬로 경험을 수집&lt;/b&gt;하면서도, &lt;b&gt;정책/가치 업데이트는 동기식 batch&lt;/b&gt; 로 처리하는 패턴은 이후 PPO, A2C 변형, Curiosity 기반 탐험 등 다양한 실전 RL 코드에서 거의 공통으로 등장하는 기본 템플릿이라고 보면 된다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>현재/강화학습</category>
      <author>수민 '-'</author>
      <guid isPermaLink="true">https://som-ethi-ng.tistory.com/620</guid>
      <comments>https://som-ethi-ng.tistory.com/620#entry620comment</comments>
      <pubDate>Tue, 17 Mar 2026 18:34:17 +0900</pubDate>
    </item>
    <item>
      <title>A3C - Asynchronous Advantage Actor-Critic</title>
      <link>https://som-ethi-ng.tistory.com/619</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;여러 에이전트를 비동기로 돌리는 Actor-Critic&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;A3C(Asynchronous Advantage Actor-Critic)는 여러 개의 에이전트(worker)가 각자 &lt;b&gt;별도의 환경&lt;/b&gt;에서 동시에 경험을 수집하고, 그 결과를 하나의 &lt;b&gt;전역 신경망(global network)&lt;/b&gt; 에 비동기적으로 반영하며 학습하는 강화학습 알고리즘이다.&lt;br /&gt;Actor는 어떤 행동을 할지 &lt;b&gt;확률적으로 결정하는 정책&lt;/b&gt;을 학습하고, Critic은 현재 상태가 얼마나 좋은지 나타내는 &lt;b&gt;가치 함수&lt;/b&gt;를 학습한다. 그리고 Advantage는 &amp;ldquo;실제로 얻은 결과가 현재 가치 예측보다 얼마나 더 좋거나 나빴는가&amp;rdquo;를 나타내어, 정책을 더 안정적으로 업데이트하도록 돕는 역할을 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;A3C의 핵심은 다음 세 가지 아이디어가 결합된 구조라는 점이다.&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;Actor-Critic 구조&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Advantage 기반 정책 업데이트&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;비동기 병렬 학습&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 worker가 병렬로 다양한 경험을 빠르게 모으기 때문에 데이터 효율과 탐험 성능이 좋아지고, 서로 다른 경로의 경험이 섞이면서 학습이 더 안정적으로 이루어진다. 이후에는 이 구조를 &lt;b&gt;동기식&lt;/b&gt;으로 단순화한 A2C가 등장해, PPO 등 최신 알고리즘의 기반이 되었다.&lt;br /&gt;(&lt;a href=&quot;https://arxiv.org/pdf/1602.01783&quot;&gt;[논문]&lt;/a&gt;)&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Actor-Critic와 Advantage 다시 정리&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1-1. Actor-Critic 구조&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;Actor&lt;/b&gt;: 상태 s를 보고 어떤 행동 a를 할지 &lt;b&gt;확률적으로&lt;/b&gt; 결정하는 정책 &amp;pi;(a|s)를 학습한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Critic&lt;/b&gt;: 상태 s가 얼마나 좋은지 나타내는 &lt;b&gt;가치 함수&lt;/b&gt; V(s) 또는 Q(s, a)를 학습한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, Actor는 &amp;ldquo;무엇을 할까?&amp;rdquo;를 담당하고, Critic은 &amp;ldquo;방금 행동이 얼마나 괜찮았는가?&amp;rdquo;를 평가해주는 구조다.&lt;br /&gt;정책 기반(Policy-Based)의 유연함과 가치 기반(Value-Based)의 평가 능력을 동시에 가져가는 셈이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1-2. Advantage란 무엇인가?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Advantage는 &lt;b&gt;&amp;ldquo;현재 행동이 예상보다 얼마나 더 좋았는가&amp;rdquo;&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;Advantage: A(s, a) = Target &amp;minus; V(s)&lt;/li&gt;
&lt;li&gt;Target(1-step TD 타깃 예시): Target = r + &amp;gamma; &amp;middot; V(s')&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;r + &amp;gamma; &amp;middot; V(s') : 실제로 한 번 움직여 보고 얻은 &amp;ldquo;새로운 평가&amp;rdquo;&lt;/li&gt;
&lt;li&gt;V(s) : Critic이 원래 생각하던 &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;&lt;b&gt;A(s, a) &amp;gt; 0&lt;/b&gt; 이면 &amp;rarr; 예상보다 좋았으니, 그 행동의 확률을 &lt;b&gt;늘려야&lt;/b&gt; 한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;A(s, a) &amp;lt; 0&lt;/b&gt; 이면 &amp;rarr; 예상보다 나빴으니, 그 행동의 확률을 &lt;b&gt;줄여야&lt;/b&gt; 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Actor는 이 Advantage를 이용해 &lt;b&gt;좋은 행동의 확률은 증가&lt;/b&gt;, &lt;b&gt;나쁜 행동의 확률은 감소&lt;/b&gt;하도록 정책을 업데이트한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. A3C가 등장한 이유&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-1. 문제 1 &amp;ndash; 데이터 상관성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초기 딥 RL(DQN 등)은 보통 &lt;b&gt;환경 하나 + 에이전트 하나&lt;/b&gt; 구조를 썼다. 이 경우 에이전트가 한 환경에서만 학습하기 때문에, 시간에 따라 얻는 경험들이 서로 &lt;b&gt;강하게 상관&lt;/b&gt; 되어 있다.&lt;br /&gt;DQN은 이를 완화하기 위해 Experience Replay로 샘플을 섞어 쓰지만, 어차피 하나의 환경 궤적에서 나온 데이터라는 한계가 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-2. 문제 2 &amp;ndash; 경험 수집 속도&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;환경이 하나뿐이면 경험 수집 자체가 느리다.&lt;br /&gt;특히 시뮬레이션이 무거운 환경이나 CPU 기반 환경에서는, &lt;b&gt;&amp;ldquo;경험을 모으는 속도&amp;rdquo;가 병목&lt;/b&gt; 이 되기 쉽다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. A3C의 핵심 아이디어&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-1. 여러 worker를 동시에 돌리기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아이디어 자체는 단순하다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;환경을 여러 개 띄우고, 에이전트도 여러 개 띄운 다음,&lt;br /&gt;각자 돌아가며 경험을 모아 공유된 하나의 글로벌 모델을 업데이트하자.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구조를 텍스트로 그리면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Worker 1 -&amp;gt; 환경 1
Worker 2 -&amp;gt; 환경 2
Worker 3 -&amp;gt; 환경 3
Worker 4 -&amp;gt; 환경 4
...&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;각 worker는 &lt;b&gt;독립적인 환경&lt;/b&gt;에서 탐험한다.&lt;/li&gt;
&lt;li&gt;동시에 여러 개의 trajectory를 수집하므로 경험 수집 속도가 빨라지고, &lt;b&gt;데이터 다양성&lt;/b&gt;도 올라간다.&lt;/li&gt;
&lt;li&gt;서로 다른 경로에서 온 경험이 섞이면서, 학습이 더 안정적으로 진행되는 효과도 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-2. 비동기 업데이트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;A3C의 포인트는 여러 worker가 &lt;b&gt;서로를 기다리지 않고(asynchronous)&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;각 worker는 글로벌 네트워크를 복사해 &lt;b&gt;local model&lt;/b&gt;을 만든다.&lt;/li&gt;
&lt;li&gt;환경에서 일정 step(또는 n-step) 동안 데이터를 모은다.&lt;/li&gt;
&lt;li&gt;이 데이터를 기반으로 &lt;b&gt;local model&lt;/b&gt;에서 그라디언트를 계산한다.&lt;/li&gt;
&lt;li&gt;계산된 그라디언트를 &lt;b&gt;전역 신경망(global network)&lt;/b&gt; 에 바로 반영해 파라미터를 업데이트한다.&lt;/li&gt;
&lt;li&gt;업데이트된 글로벌 파라미터를 다시 local model에 복사해서 동기화한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정을 각 worker가 &lt;b&gt;동시에, 비동기로&lt;/b&gt; 수행하기 때문에 A3C라는 이름이 붙었다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. A3C 전체 구조 한눈에 보기&lt;/h2&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Global Network (Actor + Critic)
                &amp;uarr;
                │
 ┌──────────────┼──────────────┐
 │              │              │
Worker 1      Worker 2      Worker 3
환경 탐험       환경 탐험       환경 탐험
데이터 수집     데이터 수집     데이터 수집
gradient 계산  gradient 계산  gradient 계산
 │              │              │
 └──────────────┴──────────────┘
        글로벌 모델 업데이트&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 worker는 다음 순서를 반복한다.&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;글로벌 모델 복사 &amp;rarr; local model 생성&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;환경에서 일정 step 동안 roll-out&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;target = r + &amp;gamma; &amp;middot; V(s')&lt;/li&gt;
&lt;li&gt;advantage = target &amp;minus; V(s)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;local model 기준으로 gradient 계산&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Actor Loss: log &amp;pi;(a|s) &amp;times; Advantage&lt;/li&gt;
&lt;li&gt;Critic Loss: (V(s) &amp;minus; target)&amp;sup2;&lt;/li&gt;
&lt;/ul&gt;
&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;local gradient를 글로벌 파라미터에 복사해 업데이트&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;업데이트된 글로벌 모델 &amp;rarr; 다시 local model에 복사&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;이 과정을 에피소드가 끝날 때까지 반복&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. A3C CartPole 구현 코드 분석&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 코드는 &lt;code&gt;CartPole-v1&lt;/code&gt; 환경에서 A3C 구조를 &lt;b&gt;멀티프로세싱&lt;/b&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;&lt;b&gt;전역 모델(global_model)&lt;/b&gt; 을 &lt;code&gt;share_memory()&lt;/code&gt;로 모든 프로세스가 공유&lt;/li&gt;
&lt;li&gt;각 worker 프로세스는 local 모델을 만든 뒤, global 모델을 업데이트&lt;/li&gt;
&lt;li&gt;별도의 테스트 프로세스는 같은 글로벌 모델을 읽어서 &lt;b&gt;성능을 모니터링&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;haskell&quot;&gt;&lt;code&gt;import gymnasium as gym
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torch.distributions import Categorical
import torch.multiprocessing as mp
import time

# Hyperparameters
n_train_processes = 3       # 학습용 프로세스를 3개 동작
learning_rate = 0.0002
update_interval = 5         # 한 번 업데이트 전에 최대 몇 step까지 데이터를 모을지
gamma = 0.98
max_train_ep = 300
max_test_ep = 100


class ActorCritic(nn.Module):
    def __init__(self):
        super().__init__()
        self.fc1 = nn.Linear(4, 256)
        self.fc_pi = nn.Linear(256, 2)
        self.fc_v = nn.Linear(256, 1)

    def pi(self, x, softmax_dim=0):
        x = F.relu(self.fc1(x))
        x = self.fc_pi(x)
        return F.softmax(x, dim=softmax_dim)

    def v(self, x):
        x = F.relu(self.fc1(x))
        return self.fc_v(x)&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;code&gt;CartPole-v1&lt;/code&gt;의 상태 차원이 4이므로 &lt;code&gt;nn.Linear(4, 256)&lt;/code&gt;으로 시작한다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fc_pi&lt;/code&gt;는 행동 2개(왼쪽, 오른쪽)에 대한 확률을 출력하고,&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fc_v&lt;/code&gt;는 해당 상태의 가치 (V(s)) 하나를 출력한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;def train(global_model, rank):
    local_model = ActorCritic()
    # global_model의 현재 가중치를 local_model에 복사
    local_model.load_state_dict(global_model.state_dict())

    # 실제 업데이트 대상은 global_model
    optimizer = optim.Adam(global_model.parameters(), lr=learning_rate)
    env = gym.make(&quot;CartPole-v1&quot;)

    for n_epi in range(max_train_ep):
        done = False
        s, _ = env.reset()

        while not done:
            s_lst, a_lst, r_lst = [], [], []

            for _ in range(update_interval):
                prob = local_model.pi(torch.from_numpy(s).float())
                m = Categorical(prob)
                a = m.sample().item()

                s_prime, r, terminated, truncated, info = env.step(a)
                done = terminated or truncated

                s_lst.append(s)
                a_lst.append([a])
                r_lst.append(r / 100.0)

                s = s_prime
                if done:
                    break&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;code&gt;update_interval&lt;/code&gt; 동안 rollout을 수행하며 &lt;code&gt;(s, a, r)&lt;/code&gt;를 저장한다.&lt;/li&gt;
&lt;li&gt;보상은 &lt;code&gt;r / 100.0&lt;/code&gt; 으로 스케일을 줄여 학습을 좀 더 안정적으로 만든다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;            s_final = torch.tensor(s_prime, dtype=torch.float)
            # 에피소드가 끝났으면 미래 가치가 없음
            R = 0.0 if done else local_model.v(s_final).item()

            td_target_lst = []
            # 보상 리스트를 뒤에서부터 순회하며 n-step Return 계산
            for reward in r_lst[::-1]:
                R = gamma * R + reward
                td_target_lst.append([R])
            td_target_lst.reverse()

            s_batch = torch.tensor(s_lst, dtype=torch.float)
            a_batch = torch.tensor(a_lst)
            td_target = torch.tensor(td_target_lst, dtype=torch.float)
            # Advantage = TD target - V(s)
            advantage = td_target - local_model.v(s_batch)

            pi = local_model.pi(s_batch, softmax_dim=1)  # 예: (5, 2)
            pi_a = pi.gather(1, a_batch)

            value_loss = F.smooth_l1_loss(local_model.v(s_batch), td_target.detach())
            policy_loss = -torch.log(pi_a) * advantage.detach()
            loss = policy_loss + value_loss

            optimizer.zero_grad()
            loss.mean().backward()

            for global_param, local_param in zip(global_model.parameters(), local_model.parameters()):
                global_param._grad = local_param.grad

            # 글로벌 모델 업데이트
            optimizer.step()
            # 최신 global_model 가중치를 다시 local_model에 복사
            local_model.load_state_dict(global_model.state_dict())

        if n_epi % 10 == 0:
            print(f&quot;[TRAIN {rank}] episode={n_epi}&quot;, flush=True)

    env.close()
    print(f&quot;Training process {rank} finished.&quot;, flush=True)&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;b&gt;Critic Loss&lt;/b&gt;: &lt;code&gt;value_loss = smooth_l1_loss(V(s), target)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Actor Loss&lt;/b&gt;: &lt;code&gt;policy_loss = -log &amp;pi;(a|s) * advantage&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;local 모델에서 backward를 돌리지만, optimizer는 &lt;b&gt;global_model&lt;/b&gt; 을 대상으로 한다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;global_param._grad = local_param.grad&lt;/code&gt; 로 그라디언트를 전역 파라미터에 복사한 뒤 &lt;code&gt;optimizer.step()&lt;/code&gt;으로 업데이트한다.&lt;/li&gt;
&lt;li&gt;이후 전역 파라미터를 다시 local 모델에 복사해 최신 상태로 맞춘다.&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. 테스트 프로세스와 메인 루프&lt;/h2&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;def test(global_model):
    env = gym.make(&quot;CartPole-v1&quot;)
    score = 0.0
    print_interval = 5

    for n_epi in range(max_test_ep):
        done = False
        s, _ = env.reset()

        while not done:
            prob = global_model.pi(torch.from_numpy(s).float())
            a = Categorical(prob).sample().item()

            s_prime, r, terminated, truncated, info = env.step(a)
            done = terminated or truncated

            s = s_prime
            score += r

        if n_epi % print_interval == 0 and n_epi != 0:
            print(f&quot;[TEST] episode={n_epi}, avg score={score / print_interval:.1f}&quot;, flush=True)
            score = 0.0
            time.sleep(0.5)

    env.close()&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;b&gt;하나의 프로세스&lt;/b&gt;에서 공유된 global 모델을 읽기만 한다.&lt;/li&gt;
&lt;li&gt;별도 업데이트 없이 현재 성능을 주기적으로 출력한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;if __name__ == &quot;__main__&quot;:
    mp.set_start_method(&quot;spawn&quot;, force=True)

    global_model = ActorCritic()
    # 전역 모델을 프로세스 간 공유 메모리에 올림
    global_model.share_memory()

    processes = []
    # 학습 프로세스 3개, 테스트 프로세스 1개
    for rank in range(n_train_processes + 1):
        if rank == 0:
            p = mp.Process(target=test, args=(global_model,))
        else:
            p = mp.Process(target=train, args=(global_model, rank))
        p.start()
        processes.append(p)

    for p in processes:
        p.join()
        print(&quot;process exitcode:&quot;, p.exitcode, flush=True)&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;code&gt;share_memory()&lt;/code&gt; 덕분에 모든 프로세스가 같은 &lt;code&gt;global_model&lt;/code&gt; 파라미터를 직접 공유한다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;rank == 0&lt;/code&gt; 은 테스트, 그 외는 학습 worker로 동작한다.&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. A3C 정리&lt;/h2&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Actor-Critic 구조 + Advantage + 멀티프로세싱 비동기 학습&lt;/li&gt;
&lt;/ul&gt;
&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;b&gt;데이터 효율&lt;/b&gt;과 &lt;b&gt;탐험 성능&lt;/b&gt;을 끌어올린다.&lt;/li&gt;
&lt;li&gt;서로 다른 trajectory가 섞이며 학습이 더 &lt;b&gt;안정적&lt;/b&gt;이 된다.&lt;/li&gt;
&lt;li&gt;CPU 여러 코어를 적극 활용할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&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;b&gt;gradient 충돌(gradient interference)&lt;/b&gt; 문제가 생길 수 있다.&lt;/li&gt;
&lt;li&gt;구현 난이도가 동기식 A2C에 비해 높고, GPU batch 학습과 궁합도 좋지 않다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 한계를 보완한 것이 바로 &lt;b&gt;A2C(Advantage Actor-Critic)&lt;/b&gt; 이고, PPO 등 실전에서 자주 쓰이는 알고리즘은 대부분 이 A2C 스타일의 &lt;b&gt;동기식 Advantage Actor-Critic&lt;/b&gt; 을 기반으로 발전해 나갔다.&lt;/p&gt;</description>
      <category>현재/강화학습</category>
      <author>수민 '-'</author>
      <guid isPermaLink="true">https://som-ethi-ng.tistory.com/619</guid>
      <comments>https://som-ethi-ng.tistory.com/619#entry619comment</comments>
      <pubDate>Mon, 16 Mar 2026 18:35:31 +0900</pubDate>
    </item>
    <item>
      <title>Policy 기반 에이전트 - REINFORCE, Actor-Critic, TD Actor-Critic</title>
      <link>https://som-ethi-ng.tistory.com/616</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;정책을 직접 학습하는 강화학습과 Policy Gradient Theorem&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강화학습에는 &lt;b&gt;가치 기반(Value-Based)&lt;/b&gt; 과 &lt;b&gt;정책 기반(Policy-Based)&lt;/b&gt; 이 있다. 가치 기반은 Q값을 구한 뒤 가장 좋은 행동을 고르고, 정책 기반은 &lt;b&gt;행동을 선택하는 규칙(정책) 자체&lt;/b&gt;를 직접 학습한다. &amp;ldquo;정책을 더 좋은 방향으로 어떻게 업데이트할지&amp;rdquo;를 수학적으로 설명하는 것이 &lt;b&gt;Policy Gradient Theorem&lt;/b&gt; 이고, 이를 구현한 대표 알고리즘이 &lt;b&gt;REINFORCE&lt;/b&gt; 와 &lt;b&gt;Actor-Critic&lt;/b&gt; 계열이다. 이 글에서는 정리 내용과 CartPole-v1 예제 코드까지 정리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(&lt;a href=&quot;https://ryuzyproject.tistory.com/272&quot;&gt;Policy 기반 에이전트 - 류지 프로젝트&lt;/a&gt;를 바탕으로 재구성했다.)&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Policy Gradient Theorem&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1-1. 왜 필요한가&lt;/h3&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;: 행동의 가치(Q값)를 계산해서 가장 큰 행동을 고르는 방식 (예: Q-learning, DQN)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;정책 기반&lt;/b&gt;: 행동 가치를 먼저 구하지 않고, &lt;b&gt;행동을 선택하는 규칙(정책)&lt;/b&gt; 을 직접 학습하는 방식 (예: REINFORCE, Policy Gradient, PPO)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정책 기반에서는 다음 질문이 생긴다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정책을 더 좋은 방향으로 바꾸려면&lt;br /&gt;정확히 어떻게 파라미터를 업데이트해야 할까?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이에 대한 답을 주는 것이 &lt;b&gt;Policy Gradient Theorem&lt;/b&gt; 이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1-2. 핵심 수식&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정책 파라미터 &amp;theta;에 대한 기대 보상 J(&amp;theta;)의 그래디언트는 다음 형태로 쓸 수 있다.&lt;/p&gt;
&lt;pre class=&quot;gcode&quot;&gt;&lt;code&gt;&amp;nabla;&amp;theta; J(&amp;theta;) &amp;prop; E[ &amp;nabla;&amp;theta; log &amp;pi;&amp;theta;(a|s) &amp;middot; Q^&amp;pi;(s,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;&lt;b&gt;&amp;nabla;&amp;theta; J(&amp;theta;)&lt;/b&gt;: 정책을 &amp;ldquo;어느 방향으로&amp;rdquo; 수정하면 보상이 커지는지를 나타내는 기울기.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;log &amp;pi;&amp;theta;(a|s)&lt;/b&gt;: 현재 상태에서 &lt;b&gt;실제로 선택한 행동&lt;/b&gt;의 로그 확률. 그 행동의 확률을 키우거나 줄이기 위한 도구.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&amp;nabla;&amp;theta; log &amp;pi;&amp;theta;(a|s)&lt;/b&gt;: &amp;theta;를 조금 바꿨을 때 그 행동의 선택 확률이 얼마나 변하는지(정책의 민감도).&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Q^&amp;pi;(s,a)&lt;/b&gt;: 상태 s에서 행동 a를 했을 때 앞으로 받을 것으로 기대되는 보상.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, &lt;b&gt;&amp;ldquo;선택한 행동의 로그 확률&amp;rdquo;에 &amp;ldquo;그 행동의 가치(Q 또는 Return)&amp;rdquo;를 곱한 방향으로 &amp;theta;를 조정&lt;/b&gt;하면, 높은 보상을 낸 행동의 확률은 올라가고 낮은 보상을 낸 행동의 확률은 내려간다. 환경 모델 없이 경험만으로 정책을 학습할 수 있으며, REINFORCE&amp;middot;Actor-Critic 등 정책 기반 알고리즘의 이론적 바탕이 된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1-3. 수식에 로그가 들어가는 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수학적으로 &lt;b&gt;미분을 쉽게 하기 위해서&lt;/b&gt;다. 확률의 곱을 다룰 때 로그를 취하면 곱이 &lt;b&gt;덧셈&lt;/b&gt;으로 바뀌어 계산이 단순해진다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. REINFORCE 알고리즘&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;REINFORCE는 &lt;b&gt;에피소드가 끝난 뒤&lt;/b&gt;, 그 에피소드에서 수집한 (상태, 행동, 보상)을 이용해 정책을 한 번에 업데이트하는 &lt;b&gt;Monte Carlo&lt;/b&gt; 형식의 정책 기반 알고리즘이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-1. 동작 과정&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;에이전트가 정책 &amp;pi;&amp;theta;에 따라 행동한다.&lt;/li&gt;
&lt;li&gt;에피소드가 끝날 때까지 환경과 상호작용한다.&lt;/li&gt;
&lt;li&gt;각 시점 t에서 &lt;b&gt;Return&lt;/b&gt; G_t(그 시점 이후 보상의 할인 합)를 계산한다.&lt;/li&gt;
&lt;li&gt;G_t를 이용해 정책 파라미터를 업데이트한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-2. Return&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Return&lt;/b&gt; G_t는 &amp;ldquo;그 시점 이후에 얻은 모든 보상의 할인 합&amp;rdquo;이다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;G_t = r_t + &amp;gamma;&amp;middot;r_{t+1} + &amp;gamma;&amp;sup2;&amp;middot;r_{t+2} + &amp;hellip;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에피소드가 끝난 뒤 &lt;b&gt;역순&lt;/b&gt;으로 누적하면 계산하기 편하다: R &amp;larr; r + &amp;gamma;&amp;middot;R.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-3. 업데이트 수식&lt;/h3&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;&amp;theta; &amp;larr; &amp;theta; + &amp;alpha; &amp;middot; &amp;nabla;&amp;theta; log &amp;pi;&amp;theta;(a_t|s_t) &amp;middot; G_t&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_t의 &lt;b&gt;로그 확률&lt;/b&gt;에 그 이후의 &lt;b&gt;총 보상(Return)&lt;/b&gt; G_t를 곱해 &amp;theta;를 갱신한다.&lt;/li&gt;
&lt;li&gt;보상이 큰 행동은 확률을 높이고, 보상이 작은 행동은 확률을 낮추는 방향이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-4. 의사코드&lt;/h3&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;정책 파라미터 &amp;theta; 초기화
반복:
    에피소드 실행 &amp;rarr; (s0,a0,r0), (s1,a1,r1), ... 수집
    각 시간 t에 대해:
        Return Gt 계산
        &amp;theta; &amp;larr; &amp;theta; + &amp;alpha; * &amp;nabla;&amp;theta; log &amp;pi;&amp;theta;(at|st) * Gt&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-5. CartPole-v1에서의 REINFORCE 구현&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;을 출력하고, 그 확률로 행동을 &lt;b&gt;샘플링&lt;/b&gt;한다.&lt;/li&gt;
&lt;li&gt;에피소드 동안 (보상, 선택한 행동의 확률)을 저장해 두었다가, 끝난 뒤 Return을 역순으로 계산하고 &lt;b&gt;한 번에&lt;/b&gt; 정책을 업데이트한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;ruby&quot;&gt;&lt;code&gt;import gymnasium as gym
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torch.distributions import Categorical

learning_rate = 0.0002
gamma = 0.98

class Policy(nn.Module):
    def __init__(self):
        super(Policy, self).__init__()
        self.data = []
        self.fc1 = nn.Linear(4, 128)
        self.fc2 = nn.Linear(128, 2)
        self.optimizer = optim.Adam(self.parameters(), lr=learning_rate)

    def forward(self, x):
        x = F.relu(self.fc1(x))
        x = F.softmax(self.fc2(x), dim=0)
        return x

    def put_data(self, item):
        self.data.append(item)

    def train_net(self):
        R = 0
        self.optimizer.zero_grad()
        for r, prob in self.data[::-1]:
            R = r + gamma * R
            loss = -torch.log(prob) * R
            loss.backward()
        self.optimizer.step()
        self.data = []

def main():
    env = gym.make('CartPole-v1')
    pi = Policy()
    score = 0.0
    print_interval = 20

    for n_epi in range(10000):
        s, _ = env.reset()
        done = False
        while not done:
            prob = pi(torch.from_numpy(s).float())
            m = Categorical(prob)
            a = m.sample().item()
            s_prime, r, done, truncated, info = env.step(a)
            pi.put_data((r, prob[a]))
            s = s_prime
            score += r
            if done or truncated:
                break
        pi.train_net()
        if n_epi % print_interval == 0 and n_epi != 0:
            print(&quot;n_episode: {}, avg score: {:.1f}&quot;.format(n_epi, score / print_interval))
            score = 0.0
    env.close()

if __name__ == '__main__':
    main()&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;code&gt;prob[a]&lt;/code&gt;: 실제로 선택한 행동의 확률. Return R이 크면 그 행동의 로그 확률을 키우는 쪽으로 학습한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. Actor-Critic 개요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Actor-Critic&lt;/b&gt;은 &lt;b&gt;정책(Actor)&lt;/b&gt; 과 &lt;b&gt;가치 평가(Critic)&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;Actor&lt;/b&gt;: &amp;ldquo;무엇을 할까?&amp;rdquo; &amp;mdash; 상태를 보고 행동을 선택하는 &lt;b&gt;정책&lt;/b&gt; &amp;pi;(a|s) 학습.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Critic&lt;/b&gt;: &amp;ldquo;방금 한 선택이 얼마나 좋았는가?&amp;rdquo; &amp;mdash; &lt;b&gt;가치 함수&lt;/b&gt; V(s) 또는 Q(s,a) 학습.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 정책 기반의 유연함과 가치 기반의 평가를 같이 써서, Actor는 Critic의 평가를 받아 정책을 고치고, Critic은 자신의 가치 추정을 더 정확하게 맞춘다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-1. 전체 흐름&lt;/h3&gt;
&lt;pre class=&quot;markdown&quot;&gt;&lt;code&gt;1. 상태 s 관찰
2. Actor가 행동 a 선택
3. 환경이 보상 r, 다음 상태 s' 반환
4. Critic이 평가(TD Error 등) 계산
5. Actor는 그 평가로 정책 수정, Critic은 가치 추정 수정&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-2. Critic이 필요한 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정책 기반에서는 &amp;ldquo;좋은 행동의 확률을 높이고 나쁜 행동의 확률을 낮추라&amp;rdquo;고 하는데, &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;REINFORCE는 에피소드가 끝난 뒤 &lt;b&gt;Return&lt;/b&gt;으로 판단한다. 에피소드가 길면 느리고, Return 분산이 커서 학습이 불안정할 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Critic&lt;/b&gt;은 현재 상태&amp;middot;행동의 가치를 추정해 Actor에게 &amp;ldquo;방금 행동은 예상보다 좋았다/나빴다&amp;rdquo; 같은 &lt;b&gt;즉각적인&lt;/b&gt; 신호를 준다. 그래서 매 스텝 또는 몇 스텝마다 업데이트할 수 있고, 더 안정적인 학습이 가능해진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-3. TD Error&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Critic이 &lt;b&gt;상태 가치&lt;/b&gt; V(s)를 학습할 때 많이 쓰는 것이 &lt;b&gt;TD Error&lt;/b&gt;다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;&amp;delta; = r + &amp;gamma;&amp;middot;V(s') &amp;minus; V(s)&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;b&gt;V(s)&lt;/b&gt;: 현재까지 예상하던 &amp;ldquo;현재 상태의 가치&amp;rdquo;.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;r + &amp;gamma;&amp;middot;V(s')&lt;/b&gt;: 방금 한 스텝을 경험해 본 &amp;ldquo;실제로 얻은 값&amp;rdquo;에 대한 추정.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, &lt;b&gt;실제로 관찰한 값 &amp;minus; 원래 예상한 값&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;&amp;delta; &amp;gt; 0&lt;/b&gt;: 예상보다 좋았다 &amp;rarr; 그 행동의 확률을 &lt;b&gt;높이는&lt;/b&gt; 방향으로 Actor 업데이트.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&amp;delta; &amp;lt; 0&lt;/b&gt;: 예상보다 나빴다 &amp;rarr; 그 행동의 확률을 &lt;b&gt;낮추는&lt;/b&gt; 방향으로 Actor 업데이트.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-4. Actor&amp;middot;Critic 학습 요약&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;Actor&lt;/b&gt;: &lt;code&gt;state &amp;rarr; neural network &amp;rarr; action probabilities&lt;/code&gt;. Critic이 &amp;ldquo;방금 행동이 좋았다&amp;rdquo;고 하면 해당 행동 확률을 키운다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Critic&lt;/b&gt;: &lt;code&gt;state &amp;rarr; neural network &amp;rarr; V(s)&lt;/code&gt;. 실제 경험 r, s'로 TD 타깃 r + &amp;gamma;&amp;middot;V(s')를 만들고, V(s)를 그 타깃에 가깝게 학습한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-5. REINFORCE vs Actor-Critic&lt;/h3&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;구분&lt;/th&gt;
&lt;th&gt;REINFORCE&lt;/th&gt;
&lt;th&gt;Actor-Critic&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&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&gt;신호&lt;/td&gt;
&lt;td&gt;Return G_t&lt;/td&gt;
&lt;td&gt;TD Error 등 Critic의 평가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;분산&lt;/td&gt;
&lt;td&gt;Return 사용으로 분산이 클 수 있음&lt;/td&gt;
&lt;td&gt;Critic으로 분산 감소 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;학습 속도&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;h3 data-ke-size=&quot;size23&quot;&gt;3-6. Actor-Critic의 장점&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;안정성&lt;/b&gt;: Critic이 즉각적인 평가를 해주어 REINFORCE보다 안정적인 경우가 많다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;연속 행동&lt;/b&gt;: 조향, 로봇 제어처럼 연속 행동 공간에 적합한 변형이 많다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;실제 사용&lt;/b&gt;: A2C, A3C, PPO, DDPG, SAC 등 현대 강화학습 알고리즘 상당수가 Actor-Critic 계열이다.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. Q Actor-Critic / Advantage Actor-Critic (A2C) / TD Actor-Critic&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Q Actor-Critic&lt;/b&gt;: Critic이 &lt;b&gt;Q(s,a)&lt;/b&gt;를 학습하고, Actor는 그 Q값을 이용해 &amp;ldquo;Q가 큰 행동&amp;rdquo;의 확률을 높인다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Advantage Actor-Critic (A2C)&lt;/b&gt;: &lt;b&gt;Advantage&lt;/b&gt; A(s,a) = Q(s,a) &amp;minus; V(s)를 사용해 정책을 업데이트한다. &amp;ldquo;평균보다 얼마나 나은지&amp;rdquo;만 반영해 분산을 줄인다. Critic은 V(s)를 학습해 Advantage 계산에 쓴다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;TD Actor-Critic&lt;/b&gt;: Critic이 &lt;b&gt;V(s)&lt;/b&gt;를 &lt;b&gt;TD 학습&lt;/b&gt;으로 업데이트하고, &lt;b&gt;TD Error&lt;/b&gt; &amp;delta; = r + &amp;gamma;&amp;middot;V(s') &amp;minus; V(s)를 Actor의 정책 그래디언트 신호로 쓴다. 에피소드 끝을 기다리지 않고 매 스텝(또는 n-step)마다 업데이트할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. TD Actor-Critic 구현 (CartPole-v1)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 &lt;b&gt;TD Actor-Critic&lt;/b&gt; 형태로, CartPole-v1에서 Actor(정책)와 Critic(V(s))을 같이 학습하는 예제다.&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;Actor&lt;/b&gt;: 상태 4차원 &amp;rarr; FC 256 &amp;rarr; 정책 출력 2 (왼쪽/오른쪽 확률).&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Critic&lt;/b&gt;: 같은 공유 레이어 후 V(s) 1개 출력.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;n_rollout&lt;/b&gt; 스텝만큼 (s, a, r, s', done)을 모은 뒤, TD 타깃 r + &amp;gamma;&amp;middot;V(s')&amp;middot;done_mask와 TD Error &amp;delta; = td_target &amp;minus; V(s)를 계산하고, Actor는 &amp;minus;log &amp;pi;(a|s)&amp;middot;&amp;delta;, Critic은 V(s)를 td_target에 맞추는 손실로 학습한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;ruby&quot;&gt;&lt;code&gt;import gymnasium as gym
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
import numpy as np
from torch.distributions import Categorical

learning_rate = 0.0002
gamma = 0.98
n_rollout = 10

class ActorCritic(nn.Module):
    def __init__(self):
        super(ActorCritic, self).__init__()
        self.data = []
        self.fc1 = nn.Linear(4, 256)
        self.fc_pi = nn.Linear(256, 2)
        self.fc_v = nn.Linear(256, 1)
        self.optimizer = optim.Adam(self.parameters(), lr=learning_rate)

    def pi(self, x, softmax_dim=0):
        x = F.relu(self.fc1(x))
        x = self.fc_pi(x)
        prob = F.softmax(x, dim=softmax_dim)
        return prob

    def v(self, x):
        x = F.relu(self.fc1(x))
        v = self.fc_v(x)
        return v

    def put_data(self, transition):
        self.data.append(transition)

    def make_batch(self):
        s_lst, a_lst, r_lst, s_prime_lst, done_lst = [], [], [], [], []
        for transition in self.data:
            s, a, r, s_prime, done = transition
            s_lst.append(s)
            a_lst.append([a])
            r_lst.append([r / 100.0])
            s_prime_lst.append(s_prime)
            done_mask = 0.0 if done else 1.0
            done_lst.append([done_mask])
        s_batch = torch.tensor(np.array(s_lst), dtype=torch.float)
        a_batch = torch.tensor(np.array(a_lst), dtype=torch.long)
        r_batch = torch.tensor(np.array(r_lst), dtype=torch.float)
        s_prime_batch = torch.tensor(np.array(s_prime_lst), dtype=torch.float)
        done_batch = torch.tensor(np.array(done_lst), dtype=torch.float)
        self.data = []
        return s_batch, a_batch, r_batch, s_prime_batch, done_batch

    def train_net(self):
        s, a, r, s_prime, done = self.make_batch()
        td_target = r + gamma * self.v(s_prime) * done
        delta = td_target - self.v(s)
        pi = self.pi(s, softmax_dim=1)
        pi_a = pi.gather(1, a)
        loss = -torch.log(pi_a) * delta.detach() + F.smooth_l1_loss(self.v(s), td_target.detach())
        self.optimizer.zero_grad()
        loss.mean().backward()
        self.optimizer.step()

def main():
    env = gym.make('CartPole-v1')
    model = ActorCritic()
    print_interval = 20
    score = 0.0
    for n_epi in range(10000):
        done = False
        s, _ = env.reset()
        while not done:
            for t in range(n_rollout):
                prob = model.pi(torch.from_numpy(s).float())
                m = Categorical(prob)
                a = m.sample().item()
                s_prime, r, done, truncated, info = env.step(a)
                model.put_data((s, a, r, s_prime, done))
                s = s_prime
                score += r
                if done or truncated:
                    break
            model.train_net()
        if n_epi % print_interval == 0 and n_epi != 0:
            print(&quot;# of episode: {}, avg score: {:.1f}&quot;.format(n_epi, score / print_interval))
            score = 0.0
    env.close()

if __name__ == '__main__':
    main()&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;code&gt;delta.detach()&lt;/code&gt;: Actor 쪽에서는 Critic이 준 &amp;ldquo;좋다/나쁘다&amp;rdquo; 신호만 쓰고, Critic 그래디언트는 Actor 손실을 타고 역전파되지 않게 한다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;done&lt;/code&gt;: 종료 시 다음 상태 가치를 0으로 두기 위한 마스크다.&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. 정리&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;주제&lt;/th&gt;
&lt;th&gt;핵심&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Policy Gradient Theorem&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&amp;nabla;&amp;theta; J &amp;prop; E[ &amp;nabla;&amp;theta; log &amp;pi;&amp;theta;(a&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;REINFORCE&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;에피소드 끝까지 수집 후 Return으로 &amp;theta; &amp;larr; &amp;theta; + &amp;alpha;&amp;middot;&amp;nabla;&amp;theta; log &amp;pi;&amp;theta;(a_t&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Actor-Critic&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Actor는 정책, Critic은 V(s) 또는 Q(s,a). Critic의 평가(TD Error 등)로 매 스텝에 가깝게 업데이트 가능.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;TD Error&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&amp;delta; = r + &amp;gamma;&amp;middot;V(s') &amp;minus; V(s). 양수면 그 행동 확률을 높이고, 음수면 낮춘다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Q / A2C / TD Actor-Critic&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Critic이 Q 학습, Advantage A = Q&amp;minus;V 사용, 또는 V(s)+TD 학습 등 변형.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&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;정책 기반 강화학습에서는 &lt;b&gt;Policy Gradient Theorem&lt;/b&gt;이 &amp;ldquo;정책을 어떻게 업데이트할지&amp;rdquo;의 기준을 준다. &lt;b&gt;REINFORCE&lt;/b&gt;는 이 정리를 Return을 써서 구현한 기본 형태이고, &lt;b&gt;Actor-Critic&lt;/b&gt;은 Critic으로 즉각적인 평가(TD Error 등)를 넣어 더 빠르고 안정적으로 학습한다. A2C, PPO 등 실전 알고리즘은 대부분 이 Actor-Critic 구조를 바탕으로 하므로, REINFORCE와 TD Actor-Critic 코드를 비교해 보면 정책 기반&amp;middot;가치 기반 결합의 차이가 잘 드러난다.&lt;/p&gt;</description>
      <category>현재/강화학습</category>
      <author>수민 '-'</author>
      <guid isPermaLink="true">https://som-ethi-ng.tistory.com/616</guid>
      <comments>https://som-ethi-ng.tistory.com/616#entry616comment</comments>
      <pubDate>Fri, 13 Mar 2026 18:45:20 +0900</pubDate>
    </item>
    <item>
      <title>Q-learning과 DQN - Q값 업데이트, Gym&amp;middot;CartPole 실습</title>
      <link>https://som-ethi-ng.tistory.com/615</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;행동의 점수(Q-value)를 배우는 가치 기반 강화학습&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Q-learning&lt;/b&gt; 은 각 상태에서 &lt;b&gt;어떤 행동이 얼마나 좋은지&lt;/b&gt; 를 점수(Q-value)로 학습하는 &lt;b&gt;가치 기반(Value-Based)&lt;/b&gt; 강화학습 알고리즘이다. 에이전트가 환경과 상호작용하며 보상을 받을 때마다 그 행동의 Q값을 조금씩 수정하고, 반복하면 좋은 행동의 점수는 올라가고 나쁜 행동의 점수는 내려가서, 결국 &lt;b&gt;가장 높은 Q값을 가진 행동&lt;/b&gt; 을 선택하게 된다. 이 글에서는 Q-learning의 핵심 수식과 간단한 1차원 예제, 그리고 &lt;b&gt;Gym(Gymnasium)&lt;/b&gt; 의 CartPole 환경에서 &lt;b&gt;DQN(Deep Q-Network)&lt;/b&gt; 으로 Q값을 신경망으로 근사하는 예제까지 정리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(&lt;a href=&quot;https://ryuzyproject.tistory.com/269&quot;&gt;Q-learning - 류지 프로젝트&lt;/a&gt;를 바탕으로 재구성했다.)&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Q값(Q-value)이란&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Q-learning의 중심 개념은 &lt;b&gt;Q값(Q-value)&lt;/b&gt; 이다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;Q(s, 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;(s): 현재 상태(State)&lt;/li&gt;
&lt;li&gt;(a): 행동(Action)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의미:&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 상태 (s)에서 행동 (a)를 했을 때, 앞으로 받을 것으로 기대되는 누적 보상&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, &amp;ldquo;이 상태에서 이 행동을 하면 얼마나 이득인가?&amp;rdquo;를 숫자로 나타낸 것이다. 에이전트는 각 상태에서 &lt;b&gt;Q값이 가장 큰 행동&lt;/b&gt; 을 선택하면 된다(탐험을 위해 &amp;epsilon;-greedy 등을 섞어 쓰는 경우가 많다).&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Q-learning이 작동하는 방식&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;&lt;b&gt;현재 상태&lt;/b&gt; 확인&lt;/li&gt;
&lt;li&gt;&lt;b&gt;가능한 행동 중 하나&lt;/b&gt; 선택 (예: &amp;epsilon;-greedy)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;행동 수행&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;보상(reward)&lt;/b&gt; 수령&lt;/li&gt;
&lt;li&gt;&lt;b&gt;다음 상태&lt;/b&gt; 로 이동&lt;/li&gt;
&lt;li&gt;&lt;b&gt;방금 한 행동의 Q값&lt;/b&gt; 을 업데이트&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정을 계속 반복하면서 Q값이 점점 &lt;b&gt;진짜 기대 보상&lt;/b&gt; 에 가까워진다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. Q-learning의 핵심 수식&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Q-learning은 &lt;b&gt;TD(0)&lt;/b&gt; 형태로, &amp;ldquo;현재 보상 + 다음 상태의 최대 미래 가치&amp;rdquo;를 이용해 Q값을 수정한다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;Q(s, a) &amp;larr; Q(s, a) + &amp;alpha; [ r + &amp;gamma; max_{a'} Q(s', a') &amp;minus; Q(s, 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;(&amp;alpha;): 학습률(learning rate)&lt;/li&gt;
&lt;li&gt;(&amp;gamma;): 할인율(discount factor)&lt;/li&gt;
&lt;li&gt;(r): 즉시 보상&lt;/li&gt;
&lt;li&gt;(s'): 다음 상태&lt;/li&gt;
&lt;li&gt;(\max_{a'} Q(s', a')): 다음 상태에서 &lt;b&gt;가장 큰 Q값&lt;/b&gt; (목표 상태이면 보통 0으로 둠)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉,&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 행동의 가치를 &amp;ldquo;현재 보상 + 할인된 다음 상태의 최대 Q값&amp;rdquo;으로 조금씩 보정하는 과정 이다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 간단한 1차원 환경 예제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같은 1차원 환경을 생각하자.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;S   .   .   .   G
0   1   2   3   4&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;b&gt;시작 상태&lt;/b&gt;: 0&lt;/li&gt;
&lt;li&gt;&lt;b&gt;목표 상태&lt;/b&gt;: 4 (Goal)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;목표 도착 보상&lt;/b&gt;: +10&lt;/li&gt;
&lt;li&gt;&lt;b&gt;한 칸 이동 보상&lt;/b&gt;: -1&lt;/li&gt;
&lt;li&gt;&lt;b&gt;행동&lt;/b&gt;: 왼쪽(0), 오른쪽(1)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초기에는 모든 Q값을 0으로 둔다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-1. 예제 1: 상태 3에서 오른쪽 이동&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;현재 상태: (s = 3)&lt;/li&gt;
&lt;li&gt;행동: (a =) 오른쪽&lt;/li&gt;
&lt;li&gt;이동 결과: 3 &amp;rarr; 4 (Goal)&lt;/li&gt;
&lt;li&gt;보상: (r = +10)&lt;/li&gt;
&lt;li&gt;다음 상태: (s' = 4)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Goal은 &lt;b&gt;종료 상태&lt;/b&gt; 이므로 더 이상 움직이지 않는다. 따라서 (\max Q(s', a') = 0) 으로 둔다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;(&amp;alpha; = 0.1), (&amp;gamma; = 0.9)&lt;/li&gt;
&lt;li&gt;목표값: (r + &amp;gamma; \times 0 = 10 + 0 = 10)&lt;/li&gt;
&lt;li&gt;업데이트: (Q(3, \text{오른쪽}) \leftarrow 0 + 0.1 \times (10 - 0) = 1)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; &lt;b&gt;상태 3에서 오른쪽 행동의 Q값이 1로 증가&lt;/b&gt; 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-2. 예제 2: 상태 2에서 오른쪽 이동&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;(s = 2), (a =) 오른쪽 &amp;rarr; (s' = 3), (r = -1)&lt;/li&gt;
&lt;li&gt;방금 구한 값: (Q(3, \text{오른쪽}) = 1) 이므로 (\max Q(3, a') = 1)&lt;/li&gt;
&lt;li&gt;목표값: (-1 + 0.9 \times 1 = -0.1)&lt;/li&gt;
&lt;li&gt;(Q(2, \text{오른쪽}) \leftarrow 0 + 0.1 \times (-0.1 - 0) = -0.01)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 &lt;b&gt;목표(Goal) 쪽 Q값이 먼저 올라가고, 그 값이 이전 상태로 전파&lt;/b&gt; 되면서, &amp;ldquo;목표로 가는 방향&amp;rdquo;의 Q값이 점점 학습된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. Q-learning 계산 과정 프로그램 구현&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 1차원 환경에서 &lt;b&gt;&amp;epsilon;-greedy&lt;/b&gt; 로 행동을 선택하고, Q-learning 수식으로 Q테이블을 업데이트하는 코드다. Q값이 어떻게 바뀌고 보상이 뒤쪽 상태로 전파되는지 확인할 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;학습률 (&amp;alpha; = 0.5), 할인율 (&amp;gamma; = 0.9), 탐험 확률 (&amp;epsilon; = 0.2)&lt;/li&gt;
&lt;li&gt;확률 (&amp;epsilon;): 랜덤 행동&lt;/li&gt;
&lt;li&gt;확률 (1-&amp;epsilon;): Q값이 가장 큰 행동 선택&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;import numpy as np
import random

num_states = 5
actions = [0, 1]  # 0: 왼쪽, 1: 오른쪽
learning_rate = 0.5
discount_factor = 0.9
epsilon = 0.2
episodes = 100

q_table = np.zeros((num_states, len(actions)))

def get_action(state):
    if random.random() &amp;lt; epsilon:
        return random.choice(actions)
    else:
        return np.argmax(q_table[state])

def step(state, action):
    if action == 1:
        next_state = min(state + 1, num_states - 1)
    else:
        next_state = max(state - 1, 0)

    if next_state == 4:
        reward = 10
        done = True
    else:
        reward = -1
        done = False
    return next_state, reward, done

for episode in range(episodes):
    state = 0
    done = False
    while not done:
        action = get_action(state)
        next_state, reward, done = step(state, action)

        old_value = q_table[state, action]
        next_max = np.max(q_table[next_state]) if not done else 0.0
        q_table[state, action] = old_value + learning_rate * (
            reward + discount_factor * next_max - old_value
        )
        state = next_state

print(&quot;최종 Q-테이블:&quot;)
print(q_table)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에피소드를 충분히 돌리면, &lt;b&gt;목표(4) 쪽으로 갈수록 오른쪽 행동의 Q값이 더 높아지는&lt;/b&gt; 패턴이 나온다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. Gym(Gymnasium)이란&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Gym&lt;/b&gt;(현재는 &lt;b&gt;Gymnasium&lt;/b&gt; 이 계승)은 강화학습 알고리즘을 실험하기 위한 &lt;b&gt;표준 환경(environment)&lt;/b&gt; 을 제공하는 파이썬 라이브러리다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;에이전트가 &lt;b&gt;상태&lt;/b&gt; 를 관찰하고 &lt;b&gt;행동&lt;/b&gt; 을 선택하면, 환경이 &lt;b&gt;다음 상태&lt;/b&gt; 와 &lt;b&gt;보상&lt;/b&gt; 을 반환한다.&lt;/li&gt;
&lt;li&gt;CartPole, MountainCar, Atari 등 다양한 환경을 제공해, 알고리즘 비교&amp;middot;실습에 널리 쓰인다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설치 예시:&lt;/p&gt;
&lt;pre class=&quot;cmake&quot;&gt;&lt;code&gt;pip install gymnasium&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. DQN: CartPole-v1에서 Q값을 신경망으로 근사&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;DQN(Deep Q-Network)&lt;/b&gt; 은 Q값 (Q(s,a)) 를 &lt;b&gt;테이블이 아니라 신경망&lt;/b&gt; 으로 근사하는 방법이다. &lt;a href=&quot;https://www.cs.toronto.edu/~vmnih/docs/dqn.pdf&quot;&gt;DQN 논문&lt;/a&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;CartPole-v1&lt;/b&gt;: 카트 위 막대가 쓰러지지 않도록 카트를 &lt;b&gt;왼쪽(0)&amp;middot;오른쪽(1)&lt;/b&gt; 으로 움직이는 환경.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;상태&lt;/b&gt;: 4차원 (카트 위치, 카트 속도, 막대 각도, 막대 각속도)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;행동&lt;/b&gt;: 0(왼쪽), 1(오른쪽)&lt;/li&gt;
&lt;li&gt;에이전트는 &lt;b&gt;상태 4개 값 &amp;rarr; 신경망 &amp;rarr; 행동 2개에 대한 Q값&lt;/b&gt; 을 출력하고, 그 Q값을 Q-learning 식으로 업데이트한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7-1. Replay Buffer&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DQN에서는 &lt;b&gt;경험 (s, a, r, s', done)&lt;/b&gt; 을 버퍼에 쌓아 두고, &lt;b&gt;랜덤하게 mini-batch&lt;/b&gt; 를 뽑아 학습한다. 이렇게 하면 연속된 샘플 간 상관을 줄이고 학습이 안정된다.&lt;/p&gt;
&lt;pre class=&quot;ruby&quot;&gt;&lt;code&gt;import collections
import random

buffer_limit = 50000
batch_size = 32

class ReplayBuffer:
    def __init__(self):
        self.buffer = collections.deque(maxlen=buffer_limit)

    def put(self, transition):
        self.buffer.append(transition)

    def sample(self, n):
        mini_batch = random.sample(self.buffer, n)
        s_lst, a_lst, r_lst, s_prime_lst, done_mask_lst = [], [], [], [], []
        for transition in mini_batch:
            s, a, r, s_prime, done_mask = transition
            s_lst.append(s)
            a_lst.append([a])
            r_lst.append([r])
            s_prime_lst.append(s_prime)
            done_mask_lst.append([done_mask])
        return (
            torch.tensor(s_lst, dtype=torch.float),
            torch.tensor(a_lst),
            torch.tensor(r_lst),
            torch.tensor(s_prime_lst, dtype=torch.float),
            torch.tensor(done_mask_lst),
        )

    def size(self):
        return len(self.buffer)&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7-2. Q 네트워크와 &amp;epsilon;-greedy 행동 선택&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상태 4차원 &amp;rarr; FC 128 &amp;rarr; ReLU &amp;rarr; FC 128 &amp;rarr; ReLU &amp;rarr; FC 2 (행동 2개 Q값).&lt;/p&gt;
&lt;pre class=&quot;haskell&quot;&gt;&lt;code&gt;import torch
import torch.nn as nn
import torch.nn.functional as F

class Qnet(nn.Module):
    def __init__(self):
        super(Qnet, self).__init__()
        self.fc1 = nn.Linear(4, 128)
        self.fc2 = nn.Linear(128, 128)
        self.fc3 = nn.Linear(128, 2)

    def forward(self, x):
        x = F.relu(self.fc1(x))
        x = F.relu(self.fc2(x))
        x = self.fc3(x)
        return x

    def sample_action(self, obs, epsilon):
        out = self.forward(obs)
        if random.random() &amp;lt; epsilon:
            return random.randint(0, 1)
        else:
            return out.argmax().item()&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7-3. 학습: TD 타깃과 Huber Loss&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Q-learning 수식을 그대로 쓰되, &lt;b&gt;타깃 네트워크 (q_{\text{target}})&lt;/b&gt; 로 다음 상태의 최대 Q값을 계산해 &lt;b&gt;타깃&lt;/b&gt; 을 만든다. 종료 상태면 미래 가치를 0으로 두기 위해 &lt;code&gt;done_mask&lt;/code&gt; 를 곱한다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;def train(q, q_target, memory, optimizer):
    for i in range(10):
        s, a, r, s_prime, done_mask = memory.sample(batch_size)
        q_out = q(s)
        q_a = q_out.gather(1, a)
        max_q_prime = q_target(s_prime).max(1)[0].unsqueeze(1)
        target = r + gamma * max_q_prime * done_mask
        loss = F.smooth_l1_loss(q_a, target)
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()&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;code&gt;q_out.gather(1, a)&lt;/code&gt;: 각 샘플에서 선택한 행동 (a) 에 해당하는 Q값만 뽑는다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;done_mask&lt;/code&gt;: done이면 0, 아니면 1 &amp;rarr; 종료 시 타깃이 (r) 만 남게 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7-4. 전체 학습 루프 (Gymnasium CartPole-v1)&lt;/h3&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;import gymnasium as gym
import torch.optim as optim

learning_rate = 0.0005
gamma = 0.98
print_interval = 20

def main():
    env = gym.make('CartPole-v1')
    q = Qnet()
    q_target = Qnet()
    q_target.load_state_dict(q.state_dict())
    memory = ReplayBuffer()
    score = 0.0
    optimizer = optim.Adam(q.parameters(), lr=learning_rate)

    for n_epi in range(10000):
        epsilon = max(0.01, 0.08 - 0.01 * (n_epi / 200))
        s, info = env.reset()
        done = False

        while not done:
            a = q.sample_action(torch.from_numpy(s).float(), epsilon)
            s_prime, r, done, truncated, info = env.step(a)
            done_mask = 0.0 if (done or truncated) else 1.0
            memory.put((s, a, r / 100.0, s_prime, done_mask))
            s = s_prime
            score += r
            if done or truncated:
                break

        if memory.size() &amp;gt; 2000:
            train(q, q_target, memory, optimizer)

        if n_epi % print_interval == 0 and n_epi != 0:
            q_target.load_state_dict(q.state_dict())
            print(&quot;n_episode: {}, score: {:.1f}, n_buffer: {}, eps: {:.1f}%&quot;.format(
                n_epi, score / print_interval, memory.size(), epsilon * 100
            ))
            score = 0.0
    env.close()

if __name__ == '__main__':
    main()&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;b&gt;&amp;epsilon;&lt;/b&gt;: 초반 8% 근처에서 시작해 점점 줄여 최소 1% 로 둔다 (탐험 &amp;rarr; 활용).&lt;/li&gt;
&lt;li&gt;&lt;b&gt;타깃 네트워크&lt;/b&gt;: 주기적으로 (q) 의 가중치를 (q_{\text{target}}) 에 복사해, 타깃이 너무 흔들리지 않게 한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Replay Buffer&lt;/b&gt;: 2000개 이상 쌓인 뒤부터 학습을 시작한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. 정리&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;주제&lt;/th&gt;
&lt;th&gt;핵심 포인트&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Q(s,a)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;상태 (s)에서 행동 (a)를 했을 때 기대되는 누적 보상.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Q-learning&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;(Q(s,a) \leftarrow Q(s,a) + &amp;alpha;[r + &amp;gamma; \max_{a'} Q(s',a') - Q(s,a)]). TD(0) 기반, off-policy.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;&amp;epsilon;-greedy&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;확률 (&amp;epsilon;)로 랜덤 행동(탐험), (1-&amp;epsilon;)로 최대 Q 행동(활용).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;DQN&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Q값을 신경망으로 근사. Replay Buffer + 타깃 네트워크로 안정화.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Gym/Gymnasium&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;강화학습용 표준 환경. CartPole 등으로 알고리즘 실험.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마치며&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Q-learning은 &lt;b&gt;행동의 점수(Q값)&lt;/b&gt; 를 배우고, 그 점수가 가장 높은 행동을 선택하는 &lt;b&gt;가치 기반&lt;/b&gt; 강화학습의 대표 알고리즘이다.&lt;/li&gt;
&lt;li&gt;1차원 같은 작은 환경에서는 &lt;b&gt;Q 테이블&lt;/b&gt; 만으로도 동작을 확인할 수 있고, &lt;b&gt;CartPole&lt;/b&gt; 처럼 연속 상태 공간에서는 &lt;b&gt;DQN&lt;/b&gt; 처럼 신경망으로 Q값을 근사해야 한다.&lt;/li&gt;
&lt;li&gt;Replay Buffer와 타깃 네트워크는 DQN 학습 안정성의 핵심 요소이므로, 코드에서 각각 &amp;ldquo;왜 쓰는지&amp;rdquo;를 이해해 두면 좋다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>현재/강화학습</category>
      <author>수민 '-'</author>
      <guid isPermaLink="true">https://som-ethi-ng.tistory.com/615</guid>
      <comments>https://som-ethi-ng.tistory.com/615#entry615comment</comments>
      <pubDate>Thu, 12 Mar 2026 18:31:19 +0900</pubDate>
    </item>
    <item>
      <title>Deep RL - 함수 근사, 신경망, 가치 기반&amp;middot;정책 기반 강화학습</title>
      <link>https://som-ethi-ng.tistory.com/614</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;테이블에서 신경망으로: Deep RL의 출발점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작은 강화학습 문제에서는 각 상태의 가치(Value)를 &lt;b&gt;테이블(table)&lt;/b&gt; 로 모두 저장할 수 있다. 예를 들어 상태가 A, B, C 세 개뿐이라면 다음처럼 쓸 수 있다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;V = {
    &quot;A&quot;: 0.5,
    &quot;B&quot;: 0.0,
    &quot;C&quot;: -0.5,
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 실제 문제(바둑, 체스, 자율주행, 로봇, 비디오 게임 등)에서는 상태&amp;middot;행동 수가 너무 많거나 &lt;b&gt;연속적&lt;/b&gt; 이라 이런 테이블 방식이 불가능하다. 이 한계를 넘기 위해 &lt;b&gt;함수 근사(Function Approximation)&lt;/b&gt; 와 &lt;b&gt;신경망(Deep Neural Network)&lt;/b&gt; 을 사용하는 것이 곧 &lt;b&gt;Deep RL&lt;/b&gt; 의 출발점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(&lt;a href=&quot;https://ryuzyproject.tistory.com/268&quot;&gt;Deep RL - 류지 프로젝트&lt;/a&gt;을 바탕으로 재구성했다.)&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 근사(Approximation)란 무엇인가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;근사(Approximation)&lt;/b&gt; 는 상태나 행동 수가 너무 많아 &lt;b&gt;모든 값을 정확히 계산&amp;middot;저장할 수 없을 때&lt;/b&gt;, 함수나 모델로 그 값을 &lt;b&gt;비슷하게 추정&lt;/b&gt; 하는 방법이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1-1. 상태 공간이 너무 큰 문제들&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;: 상태 수가 약 (10^{170}) 수준으로 추정된다. (19&amp;times;19 = 361, 각 위치에 흑/백/빈칸 3가지 &amp;rarr; 이론적으로 3^361 &amp;asymp; 10^172)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;체스&lt;/b&gt;: 상태 수가 약 (10^{43}) 정도로 추정.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;자율주행&lt;/b&gt;: 위치&amp;middot;속도&amp;middot;다른 차량&amp;middot;보행자&amp;middot;신호등&amp;middot;도로 상황 등 대부분이 &lt;b&gt;연속값&lt;/b&gt; &amp;rarr; 상태 공간이 사실상 무한대.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;로봇 제어&lt;/b&gt;: 관절 각도&amp;middot;속도&amp;middot;힘&amp;middot;센서 값 등 연속 상태.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;비디오 게임(Atari 등)&lt;/b&gt;: 화면(예: 84&amp;times;84 픽셀)이 상태 &amp;rarr; 픽셀 조합 수가 천문학적.&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;h3 data-ke-size=&quot;size23&quot;&gt;1-2. 함수 근사(Function Approximation)의 아이디어&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해결법:&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상태마다 값을 저장하는 대신,&lt;br /&gt;상태가 들어오면 가치를 계산하는 함수 를 학습한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 상태가 숫자 하나라면, 아주 단순하게:&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;V(s) = w &amp;times; s&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;(s): 상태&lt;/li&gt;
&lt;li&gt;(w): 학습되는 파라미터&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;ruby&quot;&gt;&lt;code&gt;w = 0.5

def V(s):
    return w * s&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상태가 들어올 때마다 &lt;b&gt;계산으로 값이 결정&lt;/b&gt; 된다.&lt;br /&gt;상태가 많아도, 저장해야 하는 것은 &lt;b&gt;함수의 파라미터 몇 개(예: w)&lt;/b&gt; 뿐이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 피팅, 언더피팅, 오버피팅&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-1. 피팅(Fitting)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;피팅(Fitting)&lt;/b&gt; 은 데이터에 맞도록 모델&amp;middot;함수를 조정하는 과정이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;입력과 출력 사이 관계를 가장 잘 설명하는 함수를 찾는다.&lt;/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;1차 함수(선형 회귀)&lt;/li&gt;
&lt;li&gt;2차&amp;middot;3차 다항식&lt;/li&gt;
&lt;li&gt;지수&amp;middot;로그 함수&lt;/li&gt;
&lt;li&gt;신경망(복잡한 비선형 모델)&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;h3 data-ke-size=&quot;size23&quot;&gt;2-2. 언더피팅(Underfitting)&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; 해서 데이터 패턴을 충분히 학습하지 못한 상태.&lt;/li&gt;
&lt;li&gt;예: 곡선 패턴 데이터를 직선으로만 설명하려 할 때.&lt;/li&gt;
&lt;li&gt;학습 데이터에서도 오차가 크고, 전반적으로 성능이 낮다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-3. 오버피팅(Overfitting)&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;.&lt;/li&gt;
&lt;li&gt;패턴뿐 아니라 &lt;b&gt;노이즈까지 외워버린&lt;/b&gt; 경우.&lt;/li&gt;
&lt;li&gt;학습 데이터에서는 정확하지만, 새 데이터에서 성능이 크게 떨어진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강화학습의 함수 근사에서도 &lt;b&gt;언더피팅&amp;middot;오버피팅&amp;middot;일반화&lt;/b&gt; 는 그대로 중요한 이슈다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-4. 무엇이 &amp;ldquo;저장&amp;rdquo; 되는가?&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;함수 자체에 값이 저장되는 것이 아니라,&lt;br /&gt;함수의 파라미터(parameter) 가 학습된다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강화학습에서 경험을 통해 바뀌는 것은 개별 상태 값이 아니라 &lt;b&gt;모델 파라미터(가중치)&lt;/b&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;테이블 방식: 상태 1,000개 &amp;rarr; 최소 1,000개의 값을 저장해야 한다.&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;함수 구조 + 파라미터 몇 개(예: w, 신경망 가중치)만 저장하면 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 일반화(Generalization)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;일반화&lt;/b&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;테이블 방식: 학습한 상태에 대해서만 값이 있다 &amp;rarr; 새로운 상태가 나오면 대응 불가.&lt;/li&gt;
&lt;li&gt;함수 근사/신경망: 여러 상태에서 공통 패턴을 학습 &amp;rarr; &lt;b&gt;비슷한 상태&lt;/b&gt; 에도 값&amp;middot;행동을 합리적으로 추론.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상태 공간이 큰 강화학습 문제에서 &lt;b&gt;일반화는 필수&lt;/b&gt; 이며, Deep RL은 이 일반화를 위해 &lt;b&gt;신경망&lt;/b&gt; 을 도입한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 강화학습에서의 신경망&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-1. 신경망의 역할&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강화학습에서 신경망(Neural Network)은 &lt;b&gt;상태(State)를 입력&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;상태 가치 (V(s))&lt;/li&gt;
&lt;li&gt;행동 가치 (Q(s,a))&lt;/li&gt;
&lt;li&gt;정책 (&amp;pi;(a|s))&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;pre class=&quot;gcode&quot;&gt;&lt;code&gt;상태(State) &amp;rarr; 신경망 &amp;rarr; 출력(Output)&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;b&gt;상태 가치 V(s)&lt;/b&gt;: 현재 상태가 얼마나 좋은지 &amp;rarr; 숫자 하나.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;행동 가치 Q(s,a)&lt;/b&gt;: 이 상태에서 각 행동이 얼마나 좋은지 &amp;rarr; 행동 개수만큼의 값.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;정책 &amp;pi;(a|s)&lt;/b&gt;: 각 행동을 선택할 확률 &amp;rarr; 확률 분포.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;신경망이 학습된다는 말은:&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;경험을 통해 신경망의 가중치가 바뀌면서,&lt;br /&gt;더 좋은 가치/행동 예측을 하게 된다는 뜻이다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-2. 신경망 학습 흐름&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강화학습에서는 에이전트가 환경과 상호작용하면서 경험을 쌓는다.&lt;/p&gt;
&lt;pre class=&quot;sml&quot;&gt;&lt;code&gt;상태 s 관찰
&amp;rarr; 신경망으로 행동/가치 계산
&amp;rarr; 행동 a 선택
&amp;rarr; 환경이 다음 상태 s'와 보상 r 반환
&amp;rarr; 이 경험 (s, a, r, s') 로 신경망 업데이트&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경험들이 누적되면서, 신경망은 점점 더 나은 의사결정 함수를 학습한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 가치 기반(Value-Based) 강화학습&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;가치 기반(Value-Based)&lt;/b&gt; 방법은 에이전트가 &lt;b&gt;상태나 행동의 가치(Value)&lt;/b&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;/li&gt;
&lt;li&gt;각 행동의 &lt;b&gt;점수(Q값)&lt;/b&gt; 를 계산한 뒤,&lt;/li&gt;
&lt;li&gt;가장 높은 값을 가진 행동을 선택한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5-1. 가치(Value)란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강화학습에서 &lt;b&gt;가치(Value)&lt;/b&gt; 는 어떤 상태&amp;middot;행동이 &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;가치가 높다 &amp;rarr; 좋은 상태/행동.&lt;/li&gt;
&lt;li&gt;가치가 낮다 &amp;rarr; 좋지 않은 상태/행동.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5-2. 행동 가치 함수 Q(s,a)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;행동 가치 함수(Q-value)는 다음을 의미한다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;Q(s, a) = 상태 s에서 행동 a를 선택했을 때,  
          앞으로 기대되는 누적 보상&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어:&lt;/p&gt;
&lt;pre class=&quot;gcode&quot;&gt;&lt;code&gt;Q(A, 오른쪽) &amp;gt; Q(A, 왼쪽)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이면, 에이전트는 &lt;b&gt;오른쪽 행동&lt;/b&gt; 을 선택하는 것이 합리적이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5-3. 가치 기반의 핵심 아이디어&lt;/h3&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;현재 상태 확인
&amp;darr;
각 행동의 Q값 계산
&amp;darr;
가장 큰 Q값을 가진 행동 선택&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;행동의 점수를 계산하고, 가장 높은 점수를 선택하는 방식.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5-4. 행동 선택 규칙&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 기본적인 규칙은:&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상태 s에서 가능한 행동 중 Q값이 가장 큰 행동 을 선택한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 그대로 쓰면 탐험이 부족해지므로, 실전에서는 (\epsilon)-greedy 등으로 탐험을 섞는다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5-5. 가치 기반 학습 과정&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1. 현재 상태 관찰
2. 행동 선택 (예: argmax Q or &amp;epsilon;-greedy)
3. 보상 받음
4. 다음 상태로 이동
5. Q값 또는 V값 업데이트&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에이전트는 이 과정을 반복하면서 행동 가치를 점점 더 정확하게 학습한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5-6. 대표 알고리즘&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;Q-Learning&lt;/b&gt;: 다음 상태의 최대 Q값을 이용해 현재 Q값을 업데이트.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;SARSA&lt;/b&gt;: 현재 정책이 실제 선택한 다음 행동의 Q값으로 업데이트(정책 온-폴리시).&lt;/li&gt;
&lt;li&gt;&lt;b&gt;DQN(Deep Q Network)&lt;/b&gt;: 신경망으로 Q값을 근사해, Atari 게임&amp;middot;로봇 제어&amp;middot;복잡한 환경 등에 적용.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5-7. 가치 기반 방법의 장단점&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;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;동작 원리가 직관적: 점수 계산 &amp;rarr; 최대값 선택.&lt;/li&gt;
&lt;li&gt;구현이 비교적 간단: Q-learning 계열.&lt;/li&gt;
&lt;li&gt;연구가 많이 되어 있고, 변형(DQN, Double DQN, Dueling DQN 등)이 풍부.&lt;/li&gt;
&lt;/ul&gt;
&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;행동 수가 매우 크거나 연속적인 경우, Q값 계산&amp;middot;저장이 어렵다.&lt;/li&gt;
&lt;/ul&gt;
&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. 정책 기반(Policy-Based) 강화학습&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;정책 기반(Policy-Based)&lt;/b&gt; 방법은 상태를 입력받았을 때 &lt;b&gt;어떤 행동을 할지에 대한 정책(Policy)&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;Q값을 계산해 argmax로 행동을 고르는 대신,&lt;/li&gt;
&lt;li&gt;각 행동을 선택할 &lt;b&gt;확률 분포 &amp;pi;(a|s)&lt;/b&gt; 를 직접 학습.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉,&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;각 행동의 점수를 계산&amp;rdquo; 하기보다,&lt;br /&gt;&amp;ldquo;행동을 뽑아내는 규칙 자체를 학습&amp;rdquo; 하는 접근이다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6-1. 정책(Policy)이란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정책은 &lt;b&gt;상태 &amp;rarr; 행동 선택 규칙&lt;/b&gt; 이다.&lt;/p&gt;
&lt;pre class=&quot;gcode&quot;&gt;&lt;code&gt;&amp;pi;(a|s) = 상태 s에서 행동 a를 선택할 확률&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어:&lt;/p&gt;
&lt;pre class=&quot;gcode&quot;&gt;&lt;code&gt;&amp;pi;(오른쪽 | 상태 A) = 0.7
&amp;pi;(왼쪽   | 상태 A) = 0.3&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이면, 상태 A에서 에이전트는 &lt;b&gt;70% 확률로 오른쪽&lt;/b&gt;, 30% 확률로 왼쪽을 선택한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6-2. 정책을 함수로 표현하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정책은 보통 파라미터 (&amp;theta;) 를 가진 함수 (&amp;pi;_&amp;theta;(a|s)) 로 표현한다.&lt;/p&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;state &amp;rarr; neural network(&amp;theta;) &amp;rarr; action probability&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 신경망 출력이 다음과 같을 수 있다.&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;[0.1, 0.6, 0.3]&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;행동1 선택 확률 10%&lt;/li&gt;
&lt;li&gt;행동2 선택 확률 60%&lt;/li&gt;
&lt;li&gt;행동3 선택 확률 30%&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6-3. 정책 기반 학습 방법&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;예:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;상태 A에서 오른쪽을 선택해 &lt;b&gt;+10 보상&lt;/b&gt; &amp;rarr; &lt;code&gt;&amp;pi;(오른쪽|A)&lt;/code&gt; 를 증가시키는 방향.&lt;/li&gt;
&lt;li&gt;상태 A에서 왼쪽 선택해 &lt;b&gt;-5 보상&lt;/b&gt; &amp;rarr; &lt;code&gt;&amp;pi;(왼쪽|A)&lt;/code&gt; 를 감소시키는 방향.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6-4. 정책 기반이 필요한 이유&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;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;로봇 제어, 드론 제어처럼 행동이 수천 개/연속인 경우, Q값 테이블&amp;middot;argmax가 비현실적.&lt;/li&gt;
&lt;/ul&gt;
&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;자동차 핸들 각도, 로봇 팔 관절 각도 등 행동이 연속값인 경우, Q-table 방식이 어렵다.&lt;/li&gt;
&lt;/ul&gt;
&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;h3 data-ke-size=&quot;size23&quot;&gt;6-5. 정책 기반 학습 과정&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1. 상태 관찰
2. 정책이 각 행동의 선택 확률 계산
3. 확률에 따라 행동 샘플링
4. 보상 받음
5. 정책 파라미터 업데이트 (Policy Gradient 등)&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6-6. 대표 알고리즘&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;Policy Gradient&lt;/b&gt;: 가장 기본적인 정책 기반 알고리즘.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;REINFORCE&lt;/b&gt;: Monte Carlo 기반 정책 학습.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;PPO (Proximal Policy Optimization)&lt;/b&gt;: 가장 널리 사용되는 정책 기반 알고리즘 중 하나.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Actor&amp;ndash;Critic 계열&lt;/b&gt;: 정책(Actor)과 가치 함수(Critic)를 동시에 학습.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6-7. 정책 기반의 장단점&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;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;연속 행동&lt;/b&gt; 문제 해결 가능: 로봇 제어 등.&lt;/li&gt;
&lt;li&gt;확률적 정책으로 &lt;b&gt;탐험(exploration)&lt;/b&gt; 이 자연스럽다.&lt;/li&gt;
&lt;li&gt;복잡하고 큰 행동 공간에도 적용 가능.&lt;/li&gt;
&lt;/ul&gt;
&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;/li&gt;
&lt;li&gt;샘플 효율이 낮을 수 있음: 많은 경험 데이터 필요.&lt;/li&gt;
&lt;/ul&gt;
&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Deep RL의 핵심은 &lt;b&gt;테이블 방식에서 함수 근사(특히 신경망)로 넘어가는 것&lt;/b&gt; 이다.&lt;/li&gt;
&lt;li&gt;함수 근사는 &lt;b&gt;근사&amp;middot;피팅&amp;middot;언더/오버피팅&amp;middot;일반화&lt;/b&gt; 같은 고전 ML 개념을 RL에 그대로 가져온다.&lt;/li&gt;
&lt;li&gt;신경망은 상태를 입력받아 가치나 정책을 출력하는 &lt;b&gt;근사 함수&lt;/b&gt; 로, 복잡한 상태 공간에서도 합리적인 의사결정을 가능하게 한다.&lt;/li&gt;
&lt;li&gt;그 위에 &lt;b&gt;가치 기반(Value-Based)&lt;/b&gt; 과 &lt;b&gt;정책 기반(Policy-Based)&lt;/b&gt; 이라는 두 축이 있고, Actor&amp;ndash;Critic&amp;middot;DQN&amp;middot;PPO 같은 알고리즘들이 이 위에서 구현된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글의 내용을 바탕으로, 이후에는 &lt;b&gt;Q-learning + 신경망(DQN)&lt;/b&gt; 이나 &lt;b&gt;정책 경사(Policy Gradient) + 신경망&lt;/b&gt; 을 직접 구현해 보면 Deep RL의 전체 그림이 한층 더 명확해질 것이다.&lt;/p&gt;</description>
      <category>현재/강화학습</category>
      <author>수민 '-'</author>
      <guid isPermaLink="true">https://som-ethi-ng.tistory.com/614</guid>
      <comments>https://som-ethi-ng.tistory.com/614#entry614comment</comments>
      <pubDate>Wed, 11 Mar 2026 18:30:32 +0900</pubDate>
    </item>
  </channel>
</rss>