![자체 라벨링과 학습 알고리즘: 다수결로 가짜 정답을 만들고 GRPO/ADPO로 가중치를 업데이트하다 [프롬프트로 읽어내는 AI 에이전트 #11] 대표 이미지](https://static.zenn.studio/images/drawing/discussion.png)
자체 라벨링과 학습 알고리즘: 다수결로 가짜 정답을 만들고 GRPO/ADPO로 가중치를 업데이트하다 [프롬프트로 읽어내는 AI 에이전트 #11]
요약
Agent0 프레임워크를 통해 인간의 주석 없이 자체 라벨링과 강화학습(GRPO/ADPO)으로 모델 가중치를 업데이트하는 메커니즘을 분석합니다. 자기 일관성(self-consistency) 기반의 다수결 방식을 활용하여 의사 정답을 생성하고 학습 루프를 구축하는 과정을 다룹니다.
핵심 포인트
- Agent0는 외부 데이터 없이 가중치를 직접 수정하는 학습형 프레임워크임
- 자기 일관성(self-consistency)을 이용해 다수결로 의사 정답을 생성함
- 출제자(Questioner)와 답변자(Executor)의 공진화 구조를 가짐
- GRPO 및 ADPO 알고리즘을 통해 모델의 가중치를 업데이트함
연재 「프롬프트로 읽어내는 AI 에이전트」 제11회(제9장).
실존하는 3개의 AI 에이전트 OSS를 실 코드와 실 프롬프트를 원전에서 인용하며 분석하고, 최종적으로 '스스로 AI 에이전트를 만들 수 있는' 상태를 목표로 하는 연재입니다.
본 기사는 제2부의 최종 장입니다. 제7장에서 동기를, 제8장에서 공진화와 보상 설계를 살펴보았습니다. 본 장에서는 학습 루프 자체를 열어봅니다.
이 장의 전제 (제7장・제8장 요약)
제2부에 들어선 이후의 3장을 본 장의 입구에서 한 단계씩 겹쳐서 설명하겠습니다.
제7장: Agent0 (aiming-lab/Agent0 @30e1882)은 '작동하는 애플리케이션'이 아니라, 강화학습(RL)으로 모델의 가중치 자체를 수정하는 프레임워크입니다. 출력은 대화가 아니라 훈련된 가중치의 체크포인트입니다. 실행형과 학습형은 경쟁하는 대체안이 아니라, 목적과 전제가 다른 공존하는 두 갈래 길입니다. -
제8장: 그 학습은 **출제 역할(Curriculum Agent)**과 **답변 역할(Executor Agent)**의 공진화로 돌아갑니다. 프롬프트는 추론 시의 지시문이 아니라, 보상 함수가 자동 채점할 수 있는 형태로 출력을 구속하는 계약 장치가 됩니다 (예: <question>...</question>와 oxed{final_answer}가 그대로 정규 표현식으로 추출되어 점수로 변환됨). -
본장(제9장): 그 공진화의 심장부인 (1) 자체 라벨링과 **(2) 학습 알고리즘 (GRPO와 ADPO)**를 원전 코드에 따라 열어봅니다. Agent0이 '제로 데이터'(인간 주석 제로)를 정말 어떻게 실현하고 있는지, 그리고 출제 역할/답변 역할이 각각 어떤 기울기(gradient)를 받아 가중치가 업데이트되는지—이것이 본 장에서 독자가 가져갈 두 가지 그림입니다.
그리고 본 장은 동시에 제6장(실행형의 자체 개선)으로부터 가장 날카로운 대비를 이루는 장이기도 합니다. 제6장 말미에서 예고했듯이, MEMORY.md나 behaviour.md, SKILL.md 같은 것을 텍스트로 수정하는 '실행형의 자체 개선'과, global_step_N/actor 디렉터리의 파라미터를 강화학습으로 수정하는 '학습형의 자체 개선'이 같은
Agent0의 주요 특징(Key Features) 제1항 Zero Data: 외부 데이터나 인간의 주석(human annotations)에 대한 의존성을 제거함.
(Agent0/README.md @ 30e1882, L13)은 제7장에서 동기로 읽었습니다. 본 장에서는 그 구현을 살펴보겠습니다.
'외부 데이터에 의존하지 않는다'라고 할 때, Agent0가 직면하는 벽은 명백합니다. 출제자(Questioner)가 문제를 만들어냅니다. 그 문제에는 정답 라벨(ground truth label)이 없습니다 (인간이 붙인 것이 아니기 때문입니다). 정답이 없다면, 해답자(Answerer)를 훈련하기 위한 보상(reward)도 계산할 수 없습니다.
Agent0의 해법은 self-consistency(자기 일관성)에 의한 다수결입니다. 출제자가 만든 문제에 대해, 해답자에게 같은 문제를 여러 번 독립적으로 풀게 하여, 가장 많은 후보가 도달한 답을 **의사 정답(pseudo ground truth)**으로 간주합니다. 일치하는 후보의 비율이 높으면 "이 문제는 명확한 답이 있으며, 그것은 아마 이것이다"라고 판정합니다. 비율이 낮으면 "답이 너무 흔들려서 훈련에 사용할 수 없다"라고 버립니다. 이 메커니즘만으로, 라벨이 붙은 데이터가 전혀 없는 상태에서 훈련 데이터를 만들어냅니다.
evaluate.py
A-1. 출제자 훈련 중에 실행되는 평가 (Agent0/curriculum_train/question_evaluate/evaluate.py는 출제자를 GRPO로 훈련할 때, 보상을 계산하기 위해 실행됩니다.)
출제자가 생성한 문제 1개당, 해답자 측에서 n=9개의 후보 답안을 생성하고, 이를 묶어서 의사 정답과 점수를 산출합니다.
샘플링 설정은 다음과 같습니다.
parser.add_argument("--num_samples", type=int, default=9, help="Number of candidate answers to generate per question (n).")
―― Agent0/curriculum_train/question_evaluate/evaluate.py @ 30e1882, L36
sample_params = vllm.SamplingParams(
max_tokens=4096,
temperature=1.0,
...
―― 동일, L91-98
temperature=1.0 · top_p=1.0 · top_k=40으로 다양성을 최대화한 샘플링을 n=9회 수행합니다. 동일한 온도 조건에서 9번 독립적으로 풀었을 때, 만약 문제가 명확한 정답을 가지고 있다면 9번 중 상당수가 같은 답으로 수렴할 것입니다. 반대로 문제가 모호하거나, 풀 수 없거나, 너무 자명하다면 답이 분산되거나, 아무도 풀지 못하거나, 혹은 모두가 똑같이 자명한 답으로 가게 됩니다. 이 성질을 라벨의 대용으로 사용하는 것이 Agent0의 첫 번째 고안입니다.
채점 측은 다음과 같이 작성되어 있습니다. 추출한 \boxed{} 안의 내용을 동치 판정(mathruler.grade_answer)으로 묶어서, 최빈값(most frequent) 답안을 의사 정답으로, 출현 비율을 점수로 만듭니다.
# Extract the boxed content from all generated samples
results = [extract_boxed_content(output.text) for output in response.outputs]
results = [res for res in results if res] # Filter out None/empty results
―― 동일, L117-119
answer_counts = {}
for result in results:
matched = False
...
―― 동일, L125-137 (...로 발췌 생략, 이어지는 L138-155에서 역방향 대조 및 타임아웃 처리)
여기서의 대조는 문자열이 완전 일치하거나, mathruler.grade_answer를 통해 수식으로서 동치라고 판정되면 같은 답으로 묶는 2단계 구조입니다 (\frac{1}{2}와 0.5를 서로 다른 것으로 계산하지 않도록 하기 위함). 묶기가 끝나면 최빈값 답안과 그 점수를 출력합니다.
# 다수결 답안과 그 점수를 결정합니다
majority_answer = max(answer_counts, key=answer_counts.get)
max_count = answer_counts[majority_answer]
...
―― 동일, L163-166
score = max_count / len(results)
―― 이 한 줄이 의사 라벨(pseudo-label)의 확실성에 대한 수치 표현입니다. n=9 중 6개가 같은 답이라면 score = 6/9 ≈ 0.67,
9개가 모두 제각각이라면 score ≈ 0.11,
9개가 모두 같다면 score = 1.0입니다.
Agent0는 이 수치를 의사 정답(pseudo-ground truth)의 신뢰도로서 후속 단계(답변 역할의 학습)까지 가져갑니다 (후술할 ADPO의 trust_weight가 이를 직접 사용합니다).
여기까지로 라벨링 과정은 완결됩니다. 출제 역할이 문제를 만들고, 답변 역할이 9번 풀고, 이를 묶어 다수결을 취하면 최빈값이 의사 정답이 됩니다. 인간은 한 번도 등장하지 않습니다.
보충: 출제 역할의 훈련 중에 실행되는 이 평가는, 후속 단계인
본격 학습 단계(답변 역할을 ADPO로 훈련하기 위한 데이터를 만들 때)에는 n=10으로 도구 실행(tool execution)을 동반하는 다회차(multi-turn) 버전으로 교체됩니다. 다음 절 A-3에서 살펴보겠습니다.
A-2. 큐레이션 (Curation) ―― 너무 쉬운 문제도 너무 어려운 문제도 버리기
의사 라벨만 만든다고 해서 바로 훈련 데이터로 쓸 수 있는 것은 아닙니다. Agent0는 score로 필터를 적용합니다.
parser.add_argument("--max_score", type=float, default=0.7)
parser.add_argument("--min_score", type=float, default=0.3)
―― Agent0/curriculum_train/question_evaluate/upload.py
@ 30e1882
, L17-18
filtered_datas = [
{'problem': data['question'], 'answer': data['answer'], 'score': data['score']}
for data in datas
...
―― 동일, L41-45
이 필터가 의미하는 바는 다음과 같습니다. 즉, 9개의 샘플에서 일치 비율이 0.30.7(README 예시에서는 0.30.8, 아래 참조) 범위에 들어오는 문제만 훈련 데이터로 사용한다는 것입니다. 비율이 너무 높으면(≥0.7 또는 ≥0.8) "답변 역할이 이미 여유롭게 풀 수 있음 = 훈련해도 새로 배울 것이 적음"이므로 버립니다. 비율이 너무 낮으면(≤0.3) "9개의 샘플을 뽑아도 대부분 제각각임 = 의사 정답 자체를 신뢰할 수 없음"이므로 버립니다. 너무 어렵지도, 너무 쉽지도 않은 대역(band)만을 남기는 큐레이션입니다.
실제로 README의 퀵스타트(Quick Start)에서는 본격 학습 데이터를 만드는 단계에서 0.3~0.8 대역을 채택하고 있습니다.
LOCAL_DATA_PATH=$(python question_evaluate/upload.py --max_score 0.8 --min_score 0.3 --experiment_name ${experiment_name})
―― Agent0/README.md
@ 30e1882
, L114
이 필터는 제7장에서 살펴본 Co-Evolution (공진화)의 핵심과 정확히 맞물립니다. 제7장에서 출제 역할이 "답변 역할에게 딱 풀 수 있을지 없을지의 경계에 있는 문제"를 만들도록 유도된다고 기술했는데, 그 "딱"을 측정하는 척도가 바로 이 score = max_count / len(results)입니다. 0.3~0.8 대역은 말 그대로 "딱 적당함"에 대한 조작적 정의입니다.
A-3. 본격 학습 데이터를 만들 때는 도구 실행 포함
본격 학습용 데이터 생성(README L107-115 단계)에서는 evaluate.py 계열의 단발 추론이 아니라, 코드 실행을 포함한 다회차(multi-turn) 추론으로 10개의 후보를 생성한 뒤 다수결을 취합니다.
def generate_with_tool_use(question: str, num_candidates: int = 10, max_turns: int = 4):
"""
다회차 대화 루프(최대 max_turns)를 사용하여 답변을 생성합니다.
...
―― Agent0/curriculum_train/vllm_service_init/start_vllm_server_tool.py
@ 30e1882
, L149-167(...에서 발췌 생략, 이어지는 L168-254에서 코드 블록 탐지·샌드박스 실행·이력 추가·중단 판정)
여기서 10개의 후보는 각각 다회차(multi-turn) 추론을 통해 <code>...</code> 블록을 작성하고 Python 샌드박스에서 실행하여 결과를 관측한 뒤, 최종적으로 \boxed{}를 출력하는――제8장에서 보았던 "Tool Integration"의 전형적인 흐름을 따릅니다. 10개 후보의 각 최종 출력을 모아 다수결을 취하는 방식은 evaluate.py와 동일합니다.
def consolidate_and_grade(question, golden_answer, assistant_messages):
'''단일 질문에 대한 LLM 출력들을 통합하고 채점합니다.'''
results = [extract_boxed_content(msg) for msg in assistant_messages]
...
―― 동일, L257-270(...에서 grade_answer_with_timeout의 양방향 대조 및 예외 처리를 생략, L271-292)
if not answer_counts:
majority_ans, max_count = '', 0
else:
...
―― 동일, L294-308
score = max_count / len(assistant_messages)
―― 여기서도 비율이 점수(score)입니다. 다 읽고 나서 깨닫게 되는 점은, Agent0의 모든 "정답 판정"이 수식 동치 판정 + 후보 집합 내에서의 다수결이라는 동일한 형태로 통일되어 있다는 것입니다. 제6장에서 보았던 실행형 자기 개선(self-improvement)이 MEMORY.md라는 텍스트 편집 행위로 통일되었던 것과 대조적으로, 학습형 자기 라벨링(self-labeling)은 수치(비율)의 계산 행위로 통일됩니다.
A-4. 자기 라벨링 최소 재현 코드
지금까지의 내용을 정리한 의사 코드(Pseudo-code, 독자가 재현할 수 있는 형태)를 제시합니다. 실행 환경은 필요하지 않습니다. call_llm(...)의 내부 구현은 OpenAI 호환 API나 자체 모델 등 원하는 것으로 자유롭게 교체하십시오.
# self_label.py ―― 자기 라벨링의 최소 구현
from collections import Counter
def grade_equiv(a: str, b: str) -> bool:
...
이 코드를 그대로 실행하면, 출제자가 만든 한 문제에 대해 "의사 정답(pseudo-ground truth)은 무엇인가", "자기 일치도는 어느 정도인가", "훈련 데이터에 남길 것인가 버릴 것인가"라는 세 가지 결과가 함께 반환됩니다. max_score를 0.7로 설정하면 evaluate.py의 기본값과 같고, 0.8로 설정하면 README의 실제 워크플로우 기본값과 같아지므로 원본에 맞춰 전환할 수 있습니다. 이 함수를 호출할 수 있다면, Zero Data의 핵심은 독자의 손안에서 동작합니다.
B. GRPO ―― 출제자 업데이트 알고리즘
자기 라벨과 점수가 준비되었다면, 다음은 그것을 사용하여 가중치(weights)를 업데이트하는 알고리즘 차례입니다. Agent0는 출제자(Curriculum Agent)의 업데이트에 GRPO (Group Relative Policy Optimization)를 채택합니다.
GRPO의 용어를 본 장의 첫 등장으로서 한 단계 정의해 두겠습니다.
- GRPO: 「동일한 프롬프트에 대해 여러 개의 샘플(rollout)을 뽑고, 그 그룹 내에서의 상대적인 우수성을 사용하여 그래디언트(gradient)를 계산하는」 강화학습 (RL) 알고리즘. 가치 함수(critic)를 별도로 학습할 필요가 없으며, 보상의 정규화(normalization)에 그룹 내 z-score(평균을 빼고 표준편차로 나눔)를 사용한다.
- 어드밴티지 (advantage): 특정 샘플의 보상이 동일한 프롬프트에 대한 다른 샘플들의 보상과 비교하여 얼마나 좋았는지를 나타내는 수치. 플러스(+)라면 평균보다 좋음, 마이너스(-)라면 평균보다 나쁨을 의미한다. 가중치 업데이트 방향은 이 어드밴티지에 의해 결정된다.
- 롤아웃 (rollout): 1회의 추론으로 생성된 출력 샘플 (제7장에서 처음 등장).
설정에서 GRPO를 선택하고 있는 부분은 여기입니다.
algorithm:
adv_estimator: grpo
disable_kl: false
...
―― Agent0/curriculum_train/examples/config.yaml
@ 30e1882
, L19-25
adv_estimator: grpo
―― 이 한 줄로, 출제자(문제 생성 역할)의 가중치 업데이트에 GRPO의 어드밴티지 계산이 사용됨이 결정됩니다.
구현은 compute_grpo_outcome_advantage
입니다.
# NOTE(sgm): this implementation only consider outcome supervision, where the reward is a scalar.
@torch.no_grad()
def compute_grpo_outcome_advantage(
...
―― Agent0/curriculum_train/verl/trainer/core_algos.py
@ 30e1882
, L136-175 (docstring의 Args / Returns 부분은 지면 관계상 ...으로 생략)
코드를 읽어보겠습니다.
scores = token_level_rewards.sum(dim=-1)
―― 각 롤아웃(rollout)에 대해, 토큰별 보상을 합산하여 **1개 샘플 = 1개 스칼라(scalar)**로 압축한다 (최종 답변의 정오를 0/1로 받는다면, 그것이 그대로 scores[i]가 된다).
id2score[index[i]].append(scores[i])
―― 롤아웃을 「동일한 프롬프트에서 나온 동료들」 단위로 그룹화한다. index[i]는 「이 샘플이 몇 번째 프롬프트에서 나왔는지」를 나타내는 ID이다.
assert len(id2score[idx]) > 1, "GRPO needs rollout.n > 1."
―― 동일한 프롬프트로부터 최소 2개의 샘플이 필요하다. 이는 GRPO의 구조적인 전제로, 그룹 내의 상대적 비교가 불가능하면 의미가 없기 때문이다.
id2mean[idx] = torch.mean(...) / id2std[idx] = torch.std(...)
―― 그룹 내의 평균과 표준편차를 계산한다.
scores[i] = (scores[i] - id2mean[index[i]]) / (id2std[index[i]] + eps)
―― z-score 정규화. 이것이 어드밴티지(advantage)이다. eps는 0으로 나누는 것을 방지하기 위함이다.
즉 GRPO의 어드밴티지는, 「동일한 문제에 대한 다른 샘플들과 비교하여, 이 샘플이 평균으로부터 얼마나 벗어나 좋았는가/나빴는가」를 표준편차 단위로 측정한 것입니다. 그룹 내에서 보상이 모두 같다면 전원 0(업데이트하지 않음), 자신만 좋았다면 플러스(그 방향의 행동을 강화), 자신만 나빴다면 마이너스(그 방향의 행동을 억제). 이것이 전부입니다.
출제자 훈련 스크립트 쪽에서는 GRPO에 필요한 「동일한 프롬프트로부터의 여러 샘플」을 rollout.n=4로 확보하고 있습니다.
CUDA_VISIBLE_DEVICES=0,1,2,3 python3 -m verl.trainer.main \
config=examples/config.yaml \
data.max_response_length=4096 \
...
―― Agent0/curriculum_train/scripts/curriculum_train.sh
@ 30e1882
, L20-36
worker.rollout.n=4를 통해 1개 프롬프트당 4개의 샘플을 생성하고, global_batch_size=128 크기의 배치로 trainer.max_steps=6번의 업데이트 스텝을 수행합니다. ―― 이것이 제8장에서 살펴보았던 Curriculum Agent의 1회 이터레이션 (Iteration) 규모입니다. GPU 4장, 스텝 6회, 그리고 매 스텝마다 save_freq=1로 체크포인트를 저장합니다. 이는 제6장의 MEMORY.md를 수정하는 것과는 차원이 다른 「수정 행위」입니다.
B-1. GRPO의 어드밴티지 (Advantage)를 수동으로 재현하기
compute_grpo_outcome_advantage의 본질만을 추출한 의사 코드(Pseudo-code) ―― 이것 또한 별도의 환경 없이 numpy만으로 동작합니다.
# grpo_advantage.py ―― GRPO의 어드밴티지 계산 최소 재현
import numpy as np
def grpo_outcome_advantage(scores: list[float], group_id: list[int],
...
이 스니펫을 직접 타이핑하여 실행해 보면 GRPO의 감각을 피부로 느낄 수 있습니다 ―― 보상(Reward)이 동일한 그룹 내에서 모두 같아지면 그래디언트(Gradient)가 발생하지 않으며(문제 C), 단 하나만 돌출되면 어드밴티지가 급격히 상승하고(문제 B), 반반으로 나뉘면 ±1 표준 편차 부근의 솔직한 시그널이 됩니다(문제 A). Agent0가 score = 0.3~0.8 대역에서 데이터를 큐레이션(Curation)하는 이유를 여기서 구체적으로 확인할 수 있습니다. 전원 정답(score=1.0)인 경우도, 전원 오답(score=0.0)인 경우도 GRPO 관점에서는 거의 가치가 없는 훈련 데이터이기 때문입니다.
C. ADPO ―― 해답 역할의 업데이트 알고리즘 (Agent0의 신규성)
이제 본 장의 마지막 고지인, Agent0가 새롭게 제안하는 ADPO(Advantage-Dynamic Policy Optimization으로 읽히는 명칭) 단계로 들어갑니다. 해답 역할(Executor Agent)의 가중치를 업데이트하는 알고리즘입니다. README에서 명시적으로 "ADPO를 executor_train/verl_tool/trainer/ppo/core_algos.py에 구현했다"라고 기술한 바로 그 실체입니다.
Lastly, we implement the ADPO algorithm in
executor_train/verl_tool/trainer/ppo/core_algos.py
and then update the executor agent with multi-turn RL based on VeRL-Tool.――
Agent0/README.md
@30e1882
, L120
ADPO는 GRPO의 단순한 확장처럼 보일 수 있지만, 사실 자기 라벨링(Self-labeling)과 맞물리는 두 가지 장치가 포함되어 있습니다 ―― (C-1) 의사 라벨(Pseudo-label)의 신뢰도에 따라 그래디언트를 동적으로 스케일링하는 trust_weight, 그리고 **(C-2) 어려운 문제일수록 탐색 폭을 넓히는 비대칭·동적 클리핑(Clipping)**입니다. 차례대로 살펴보겠습니다.
dynamic_epsilon_high
C-0. 그 전에 ―― 해답 역할이 받는 순수 보상은 ±1의 이진값
ADPO에 대해 논하기 전에, 해답 역할이 무엇을 보상으로 받는지를 확정하겠습니다. 실행형 자기 개선이 "다음번을 위해 MEMORY.md를 추가 작성한다"와 같은 모호한 신호였던 것과 달리, 해답 역할의 순수 보상은 **±1의 이진값(Binary value)**이라는 극한까지 단순화된 수치입니다.
torl_score = self.compute_score(
# data_source=data_source,
solution_str=response_str,
...
―― Agent0/executor_train/verl_tool/workers/reward_manager/torl.py
@ 30e1882
, L169-176
주석 # 1 or -1이 그대로 사양(Specification)입니다. 구현 본체는 다음과 같이 작성되어 있습니다.
boxed_pattern = re.compile(r"\\boxed\{((?:[^{}]|\\{|\\}|(?:\{(?:[^{}]|\\{|\\}|(?:\{(?:[^{}]|\\{|\\}|(?:\{[^{}]*\})*\}))*\}))*\}))"")
―― Agent0/executor_train/verl_tool/workers/reward_manager/reward_score/torl_math.py
@ 30e1882
, L38
def correctness_score_default(response, gt):
matches = boxed_pattern.findall(response)
if not matches: return -1.0
...
―― 동일 파일, L62-66
\boxed{...}
을 마지막에서 하나 추출하여, 가상 정답 gt와 동치이면 +1.0, 다르면 -1.0.
\boxed{}
자체적으로 발견되지 않아도 -1.0.
이것만 합니다. 제8장에서 본 '프롬프트로 보상을 만들기'의 가장 단순한 형태가 여기에 있습니다 ―― 프롬프트 측에서
\boxed{final_answer}
을 강제하는 출력 계약(output contract)을 붙이고 있기 때문에, 보상 측은 정규 표현식 하나만으로 스칼라화할 수 있습니다. 여기서 gt (ground_truth)
에 무엇이 들어가는지가 이번 장의 연결 고리입니다. 자가 라벨링(A절)의 majority_answer가 그대로 ground_truth로서 ADPO로 흘러들어갑니다. 데이터셋을 만드는
upload.py
이 {'problem': data['question'], 'answer': data['answer'], 'score': data['score']}
의 형식으로 parquet화하고(앞서 언급), 훈련 스크립트 train_qwen3_4b_adpo.sh
이 그것을 읽어 torl
reward manager에 전달합니다 ―― 이 일련의 과정이 '인간 라벨 제로로 강화 학습을 돌리는' 구체적인 경로입니다.
trust_weight
―― 가상 라벨의 신뢰도로 기울기(gradient)를 스케일링하다
AI 자동 생성 콘텐츠
본 콘텐츠는 Zenn AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기