튜토리얼: 에이전트 파인튜닝

튜토리얼: 에이전트 파인튜닝 (Fine-tuning Agents)

50단계 작업으로 게임을 플레이하는 ReAct 에이전트를 나타내는 DSPy 모듈 내부의 언어 모델 가중치(즉, 파인튜닝)를 최적화하는 quick example을 함께 살펴볼게요.

출처: 문서

본문

의존성 설치 및 데이터 다운로드 (Install dependencies and download data)

최신 DSPy를 pip install -U dspy로 설치하고 따라와 보세요. 이 튜토리얼은 DSPy 2.6.0에 의존하는 AlfWorld 데이터셋을 사용해요.

또한 다음 의존성도 필요해요:

> pip install -U alfworld==0.3.5 multiprocess
> alfworld-download

언어 모델 설정 (Set up the language models)

우리의 목표는 문자열 프롬프트나 예제 궤적을 손으로 조작하지 않고 gpt-4o-mini가 AlfWorld 가정 게임을 능숙하게 플레이하게 하는 것입니다.

엄밀히 필수는 아니지만, 프롬프트 최적화와 파인튜닝에 더 큰 gpt-4o를 사용해 작은 gpt-4o-mini 에이전트를 구축함으로써 작업을 조금 더 쉽게 만들 거예요.

import dspy

gpt4o_mini = dspy.LM('gpt-4o-mini-2024-07-18')
gpt4o = dspy.LM('openai/gpt-4o')
dspy.configure(experimental=True)

먼저 이 작업의 예를 살펴볼게요.

from dspy.datasets.alfworld import AlfWorld

alfworld = AlfWorld()
trainset, devset = alfworld.trainset[:200], alfworld.devset[-200:]
len(trainset), len(devset)
example = trainset[0]

with alfworld.POOL.session() as env:
    task, info = env.init(**example.inputs())

print(task)
-= Welcome to TextWorld, ALFRED! =-

You are in the middle of a room. Looking quickly around you, you see a countertop 1, a drawer 8, a drawer 7, a drawer 6, a drawer 5, a drawer 4, a drawer 3, a drawer 2, a drawer 1, a garbagecan 1, a handtowelholder 1, a sinkbasin 2, a sinkbasin 1, a toilet 1, a toiletpaperhanger 1, and a towelholder 1.

Your task is to: put a clean soapbar in garbagecan.

이제 에이전트를 정의해 볼게요. ReAct 에이전트는 주어진 작업, 지금까지의 궤적, 가능한 행동 목록으로부터 다음 행동을 예측합니다:

class Agent(dspy.Module):
    def __init__(self, max_iters=50, verbose=False):
        self.max_iters = max_iters
        self.verbose = verbose
        self.react = dspy.Predict("task, trajectory, possible_actions: list[str] -> action")

    def forward(self, idx):
        with alfworld.POOL.session() as env:
            trajectory = []
            task, info = env.init(idx)
            if self.verbose:
                print(f"Task: {task}")

            for _ in range(self.max_iters):
                trajectory_ = "\n".join(trajectory)
                possible_actions = info["admissible_commands"][0] + ["think: ${...thoughts...}"]
                prediction = self.react(task=task, trajectory=trajectory_, possible_actions=possible_actions)
                trajectory.append(f"> {prediction.action}")

                if prediction.action.startswith("think:"):
                    trajectory.append("OK.")
                    continue

                obs, reward, done, info = env.step(prediction.action)
                obs, reward, done = obs[0], reward[0], done[0]
                trajectory.append(obs)

                if self.verbose:
                    print("\n".join(trajectory[-2:]))

                if done:
                    break

        assert reward == int(info["won"][0]), (reward, info["won"][0])
        return dspy.Prediction(trajectory=trajectory, success=reward)

참고: 에이전트에 지침을 포함하고 싶다면... (Aside: If you wanted to include instructions for your agent...)

위에서 우리는 에이전트를 매우 단순하게 유지하기로 했고, 작업을 설명하는 짧은 지침조차 제공하지 않았어요.

원칙적으로는 AlfWorld 작업의 짧은 정의(Yao et al., 2022 기반)를 복사해 에이전트의 지침으로 사용할 수 있어요. 이것이 본질적으로 필수는 아니지만, DSPy에서 지침의 역할을 설명하는 데 도움이 됩니다: 지침은 모델이 특정 행동을 하도록 강요하기 위한 것이 아니라, 작업의 기본을 간단하고 사람이 읽을 수 있는 방식으로 설명하기 위해 존재해요.

그렇게 하고 싶다면 다음을:

self.react = dspy.Predict("task, trajectory, possible_actions: list[str] -> action")

이것으로 바꾸면 됩니다:

INSTRUCTIONS = """
Interact with a simulated household to achieve a high-level goal. Make sure to plan, track subgoals,
determine likely locations for common household items (e.g. desklamps will likely be on desks, shelfs, or dressers),
and explore systematically (e.g. check all desks one by one for desklamp).
""".strip()

self.react = dspy.Predict(dspy.Signature("task, trajectory, possible_actions: list[str] -> action", INSTRUCTIONS))

이제, 최적화 작업 전에 이 간단한 프로그램을 시도해 볼게요.

agent_4o = Agent()
agent_4o.set_lm(gpt4o)
agent_4o.verbose = True

agent_4o(**example.inputs())
Task: -= Welcome to TextWorld, ALFRED! =-
...
> go to countertop 1
You arrive at countertop 1. On the countertop 1, you see a candle 1, a soapbar 1, a soapbottle 2, a soapbottle 1, and a spraybottle 1.
> take soapbar 1 from countertop 1
You pick up the soapbar 1 from the countertop 1.
> go to garbagecan 1
You arrive at garbagecan 1. On the garbagecan 1, you see nothing.
> move soapbar 1 to garbagecan 1
You move the soapbar 1 to the garbagecan 1.
...

이제 평가 지표와 평가자를 정의해 볼게요. 지표는 에이전트가 성공(success)했는지를 반환합니다:

metric = lambda x, y, trace=None: y.success
evaluate = dspy.Evaluate(devset=devset, metric=metric, display_progress=True, num_threads=16)

최적화 (Optimization)

먼저 gpt-4o 에이전트를 평가한 다음 gpt-4o-mini 에이전트를 평가해 큰 모델과 작은 모델의 차이를 확인해 볼게요:

agent_4o.verbose = False
evaluate(agent_4o)
agent_4o_mini = Agent()
agent_4o_mini.set_lm(gpt4o_mini)

evaluate(agent_4o_mini)

이제 dspy.MIPROv2로 gpt-4o 에이전트의 프롬프트를 최적화해 볼게요:

optimizer = dspy.MIPROv2(metric=metric, auto="light", num_threads=16, prompt_model=gpt4o)

config = dict(max_bootstrapped_demos=1, max_labeled_demos=0, minibatch_size=40)
optimized_4o = optimizer.compile(agent_4o, trainset=trainset, **config)

그리고 dspy.BootstrapFinetune으로 gpt-4o-mini 학생 에이전트를 파인튜닝해 볼게요. 선생님으로 최적화된 gpt-4o 에이전트를 사용합니다:

student_4o_mini = optimized_4o.deepcopy()
student_4o_mini.set_lm(gpt4o_mini)
# student_4o_mini.react.demos = []  # you can optionally reset the demos
optimizer = dspy.BootstrapFinetune(metric=metric, num_threads=16)
finetuned_4o_mini = optimizer.compile(student_4o_mini, teacher=optimized_4o, trainset=trainset)
evaluate(finetuned_4o_mini)

저장 및 로드 (Save and Load)

파인튜닝된 에이전트를 저장하고 다시 로드할 수 있어요:

finetuned_4o_mini.save('finetuned_4o_mini_001.pkl')
finetuned_4o_mini.verbose = True
finetuned_4o_mini(**devset[0].inputs())
loaded = Agent()
loaded.load('finetuned_4o_mini_001.pkl', allow_pickle=True)

더 알아보기 (Learn more)