

# 보상 함수 구현
<a name="nova-implementing-reward-functions"></a>

## 개요
<a name="nova-reward-overview"></a>

보상 함수(점수 계산기 또는 등급 계산기라고도 함)는 모델 응답을 평가하고 훈련을 위한 피드백 신호를 제공하는 핵심 구성 요소입니다. 이는 모델 응답을 수락하고 보상 점수를 반환하는 Lambda 함수로 구현해야 합니다.

## 인터페이스 형식
<a name="nova-reward-interface"></a>

보상 함수는 다음 형식으로 데이터를 수락하고 반환해야 합니다.

**훈련을 위한 샘플 입력 샘플**

```
{  
    "messages": [  
        {  
            "role": "user",  
            "content": "Do you have a dedicated security team?"  
        }  
    ],              
   "reference_answer": {  
       "compliant": "No",  
       "explanation": "As an AI developed by Company, I do not have a traditional security team..."  
    }  
}
```

**보상 Lambda용 샘플 페이로드**

컨테이너는 다음을 통해 데이터를 Lambda 함수로 전송하기 전에 데이터를 자동으로 변환합니다.

1. 각 프롬프트에 대한 모델 응답 생성

1. 메시지 배열에 어시스턴트 턴(생성된 응답) 추가

1. 추적을 위한 고유한 `id` 필드 추가

Lambda 함수는 이러한 변환된 형식으로 데이터를 수신합니다.

```
{    
   "id": "123",  
    "messages": [  
        {  
            "role": "user",  
            "content": "Do you have a dedicated security team?"  
        },  
        {  
            "role": "assistant",  
            "content": "As an AI developed by Amazon, I don not have a dedicated security team..."  
        }  
    ],              
    # Following section will be same as your training dataset sample  
    "reference_answer": {  
        "compliant": "No",  
        "explanation": "As an AI developed by Company, I do not have a traditional security team..."  
    }  
}
```

**보상 Lambda 계약**

```
def lambda_handler(event, context):  
   return lambda_grader(event)  
  
def lambda_grader(samples: list[dict]) -> list[dict]:  
    """  
    Args:  
        samples: List of dictionaries in OpenAI format  
          
        Example input:  
        {     
            "id": "123",  
            "messages": [  
                {  
                    "role": "user",  
                    "content": "Do you have a dedicated security team?"  
                },  
                {  
                    "role": "assistant",  
                    "content": "As an AI developed by Company, I don nott have a dedicated security team..."  
                }  
            ],              
            # This section will be same as your training dataset  
            "reference_answer": {  
                "compliant": "No",  
                "explanation": "As an AI developed by Company, I do not have a traditional security team..."  
            }  
        }  
      
    Returns:  
        List of dictionaries with reward scores:  
        {  
            "id": str,                              # Same id as input sample  
            "aggregate_reward_score": float,        # Overall score for the sample  
            "metrics_list": [                       # OPTIONAL: Component scores  
                {  
                    "name": str,                    # Name of the component score  
                    "value": float,                 # Value of the component score  
                    "type": str                     # "Reward" or "Metric"  
                }  
            ]  
        }  
    """
```

## 입력 및 출력 필드
<a name="nova-reward-fields"></a>

### 입력 필드
<a name="nova-reward-input-fields"></a>


| 필드 | 설명 | 추가 참고 사항 | 
| --- | --- | --- | 
| id | 샘플의 고유 식별자 | 출력에서 다시 반향됩니다. 문자열 형식 | 
| messages | 정렬된 채팅 기록(OpenAI 형식) | 메시지 객체 배열 | 
| messages[].role | 메시지의 화자 | 일반적인 값: "user", "assistant", "system" | 
| messages[].content | 메시지의 텍스트 콘텐츠 | 일반 문자열 | 
| \*\*metadata | 등급 지정에 도움이 되는 자유 양식 정보 | 객체, 훈련 데이터에서 전달되는 선택적 필드 | 

### 출력 필드
<a name="nova-reward-output-fields"></a>


| 필드 | 설명 | 추가 참고 사항 | 
| --- | --- | --- | 
| id | 입력 샘플과 동일한 식별자 | 입력과 일치해야 함 | 
| aggregate\_reward\_score | 샘플의 전체 점수 | 부동 소수점(예: 0.0\~1.0 또는 태스크 정의 범위) | 
| metrics\_list | 집계를 구성하는 구성 요소 점수 | 지표 객체의 배열 | 

## 기술적 제약 조건
<a name="nova-reward-constraints"></a>
+ **제한 시간** - Lambda 간접 호출당 최대 15분의 실행 시간
+ **동시성** - `rollout_worker_replicas * 64` 동시 요청을 처리해야 함
+ **신뢰성** - 적절한 오류 처리를 구현하고 유효한 점수를 일관되게 반환해야 함
+ **성능** - 빠른 실행(분이 아닌 초 단위)을 위해 최적화하여 효율적인 훈련을 지원함

**모범 사례**
+ 외부 API 직접 호출 최소화
+ 효율적인 알고리즘 및 데이터 구조 사용
+ 일시적인 장애에 대한 재시도 로직 구현
+ 캐시 재사용 가능 계산
+ 훈련 전에 철저히 테스트하여 버그 없는 실행 보장

## 사용자 지정 보상 함수 사용
<a name="nova-reward-using-custom"></a>

태스크별 평가 기준이 있는 경우 사용자 지정 보상 함수를 구현합니다.
+ **평가 기준 정의** - 태스크에 대해 좋은 응답을 만드는 요소 결정
+ **Lambda 함수 구현** - 인터페이스 형식에 따라 Lambda 함수 생성
+ **로컬에서 테스트** - 함수가 샘플 입력에 대해 올바른 점수를 반환하는지 검증
+ **AWS에 배포** - Lambda를 배포하고 ARN 기록
+ **레시피 구성** - Lambda ARN을 레시피의 `reward_lambda_arn` 필드에 추가
+ **소규모 데이터세트로 테스트** - 최소한의 데이터로 RFT를 실행하여 통합 확인

## IAM 권한
<a name="nova-reward-iam"></a>

### 필수 권한
<a name="nova-reward-required-permissions"></a>

SageMaker 실행 역할에는 Lambda 함수를 간접 호출할 권한이 있어야 합니다. SageMaker 실행 역할에 이 정책을 추가합니다.

```
{  
  "Version": "2012-10-17",		 	 	   
  "Statement": [  
    {  
      "Effect": "Allow",  
      "Action": [  
        "lambda:InvokeFunction"  
      ],  
      "Resource": "arn:aws:lambda:region:account-id:function:function-name"  
    }  
  ]  
}
```

### Lambda 실행 역할
<a name="nova-reward-lambda-role"></a>

Lambda 함수의 실행 역할에는 기본 Lambda 실행 권한이 필요합니다.

```
{  
  "Version": "2012-10-17",		 	 	   
  "Statement": [  
    {  
      "Effect": "Allow",  
      "Action": [  
        "logs:CreateLogGroup",  
        "logs:CreateLogStream",  
        "logs:PutLogEvents"  
      ],  
      "Resource": "arn:aws:logs:*:*:*"  
    }  
  ]  
}
```

추가 권한: Lambda 함수가 다른 AWS 서비스(예: 참조 데이터의 경우 S3, 로깅의 경우 DynamoDB)에 액세스하는 경우 해당 권한을 Lambda 실행 역할에 추가합니다.

## 예: 평가형 LLM 보상 함수
<a name="nova-reward-llm-judge-example"></a>

이 예제에서는 Amazon Bedrock 모델을 평가자로 사용하여 모델 응답을 참조 답변과 비교하여 평가하는 방법을 보여줍니다. 이 Lambda 템플릿은 고객이 평가자의 평가를 처리하기 위한 추론 요청에 대해 Amazon Bedrock에 대한 직접 호출을 구현할 수 있는 프레임워크를 제공합니다. Lambda 함수는 다른 보상 함수와 동일한 입력/출력 계약을 유지 관리합니다.

### 구현
<a name="nova-reward-llm-judge-implementation"></a>

이 Lambda 함수는 2단계 평가 프로세스를 구현합니다. 즉, `lambda_handler`는 수신 샘플에서 모델 응답과 참조 응답을 추출한 다음, `lambda_graded` 함수에서 Amazon Bedrock을 직접 호출하여 이들 간의 시맨틱 유사성을 평가합니다. 이 구현에서는 일시적인 장애에 대한 자동 재시도를 통한 강력한 오류 처리를 포함하며 유연한 참조 응답 형식(문자열 및 구조화된 사전 형식 모두)을 지원합니다.

**구현 세부 정보:**
+ **재시도 로직**: Bedrock API 사용량 제한을 처리하기 위해 스로틀링 예외에 대한 지수 백오프(1초, 2초, 4초) 구현
+ **오류 처리**: 예외를 발생시키는 대신, 실패한 평가에 대해 0.0의 점수 반환
+ **결정적 점수:** 온도=0.0을 사용하여 여러 평가에서 일관된 점수 보장
+ **유연한 참조 형식**: 문자열 및 사전 참조 답변 모두를 자동으로 처리
+ **점수 고정**: 모든 점수가 유효한 [0.0, 1.0] 범위 내에 있는지 확인
+ **모델 무관**: 모든 Amazon Bedrock 모델(Nova, Llama, Mistral 등)을 사용하도록 JUDGE\_MODEL\_ID 변경

```
"""  
LLM Judge Lambda POC - Working implementation using Amazon Bedrock  
"""  
  
import json  
import time  
import boto3  
  
bedrock_runtime = boto3.client('bedrock-runtime', region_name='us-east-1')  
JUDGE_MODEL_ID = "anthropic.claude-3-5-sonnet-20240620-v1:0"  
SYSTEM_PROMPT = "You must output ONLY a number between 0.0 and 1.0. No explanations, no text, just the number."  
  
JUDGE_PROMPT_TEMPLATE = """Compare the following two responses and rate how similar they are on a scale of 0.0 to 1.0, where:  
- 1.0 means the responses are semantically equivalent (same meaning, even if worded differently)  
- 0.5 means the responses are partially similar  
- 0.0 means the responses are completely different or contradictory  
  
Response A: {response_a}  
  
Response B: {response_b}  
  
Output ONLY a number between 0.0 and 1.0. No explanations."""  
  
  
def lambda_graded(response_a: str, response_b: str, max_retries: int = 3) -> float:  
    """Call Bedrock to compare responses and return similarity score."""  
    prompt = JUDGE_PROMPT_TEMPLATE.format(response_a=response_a, response_b=response_b)  
      
    for attempt in range(max_retries):  
        try:  
            response = bedrock_runtime.converse(  
                modelId=JUDGE_MODEL_ID,  
                messages=[{"role": "user", "content": [{"text": prompt}]}],  
                system=[{"text": SYSTEM_PROMPT}],  
                inferenceConfig={"temperature": 0.0, "maxTokens": 10}  
            )  
            print(f"Bedrock call successful: {response}")  
            output = response['output']['message']['content'][0]['text'].strip()  
            score = float(output)  
            print(f"Score parsed: {score}")  
            return max(0.0, min(1.0, score))  
                  
        except Exception as e:  
            if "ThrottlingException" in str(e) and attempt < max_retries - 1:  
                time.sleep(2 ** attempt)  
            else:  
                print(f"Bedrock call failed: {e}")  
                return None  
    return None  
  
  
def lambda_handler(event, context):  
    """AWS Lambda handler - processes samples from RFTEvalInvoker."""  
    try:  
        samples = event if isinstance(event, list) else [event]  
        results = []  
          
        for sample in samples:  
            sample_id = sample.get("id", "unknown")  
            messages = sample.get("messages", [])  
              
            # Extract assistant response (response A)  
            response_a = ""  
            for msg in messages:  
                if msg.get("role") in ["assistant", "nova_assistant"]:  
                    response_a = msg.get("content", "")  
                    break  
              
            # Extract reference answer from root level (no longer in metadata)  
            reference_answer = sample.get("reference_answer", "")  
              
            # Handle both string and dict reference_answer formats  
            if isinstance(reference_answer, dict):  
                # If reference_answer is a dict, extract the explanation or compliant field  
                response_b = reference_answer.get("explanation", reference_answer.get("compliant", ""))  
            else:  
                response_b = reference_answer  
              
            if not response_a or not response_b:  
                results.append({  
                    "id": sample_id,  
                    "aggregate_reward_score": 0.0,  
                    "metrics_list": [{"name": "similarity_score", "value": 0.0, "type": "Metric"}]  
                })  
                continue  
              
            # Get similarity score  
            score = lambda_graded(response_a, response_b)  
              
            results.append({  
                "id": sample_id,  
                "aggregate_reward_score": score,  
                "metrics_list": [  
                    {  
                        "name": "similarity_score",  
                        "value": score,  
                        "type": "Metric"  
                    }  
                ]  
            })  
          
        return {"statusCode": 200, "body": json.dumps(results)}  
          
    except Exception as e:  
        print(f"Error: {e}")  
        return {"statusCode": 500, "body": json.dumps({"error": str(e)})}
```

### 입력 형식
<a name="nova-reward-llm-judge-input"></a>

Lambda는 다른 보상 함수와 동일한 입력 형식을 수신합니다.

```
{  
    "id": "sample-001",  
    "messages": [  
        {  
            "role": "user",  
            "content": "Do you have a dedicated security team?"  
        },  
        {  
            "role": "assistant",  
            "content": "As an AI developed by Amazon, I don't have a dedicated security team..."  
        }  
    ],  
    "reference_answer": {  
        "compliant": "No",  
        "explanation": "As an AI developed by Company, I do not have a traditional security team..."  
    },  
    "my_custom_field": "custom_value"  
}
```

### 출력 형식
<a name="nova-reward-llm-judge-output"></a>

```
{  
    "id": "sample-001",  
    "aggregate_reward_score": 0.85,  
    "metrics_list": [  
        {  
            "name": "similarity_score",  
            "value": 0.85,  
            "type": "Metric"  
        }  
    ]  
}
```

### 배포 고려 사항
<a name="nova-reward-llm-judge-deployment"></a>

선택한 모델의 기능 및 API 형식에 따라 프롬프트 템플릿 및 추론 파라미터를 조정해야 할 수도 있습니다.
+ **IAM 권한**: Lambda 실행 역할은 선택한 모델에 대한 `bedrock:InvokeModel` 권한을 보유해야 함
+ **제한 시간**: Bedrock API 지연 시간 및 재시도를 수용할 수 있도록 Lambda 제한 시간을 최소 60초로 설정
+ **리전**: 선택한 Bedrock 모델이 사용 가능한 리전에 배포
+ **비용**: 각 평가가 샘플당 하나의 API를 직접 호출할 때 Bedrock API 사용량 모니터링
+ **처리량**: 대규모 평가의 경우 스로틀링을 방지하기 위해 Bedrock 할당량 증가 요청

**Bedrock 처리량 증가**

평가 중에 스로틀링이 발생하는 경우 Bedrock 모델 할당량을 늘립니다.
+ AWS Service Quotas 콘솔로 이동
+ 'Bedrock'을 검색하고 리전 선택
+ 선택한 모델의 할당량 찾기(예: 'Claude 3.5 Sonnet에 대한 분당 간접 호출 수')
+ '할당량 증가 요청'을 클릭하고 원하는 처리량 지정
+ 증가에 대한 근거 제공(예: 'RFT 평가 워크로드')

Lambda의 기본 제공 재시도 로직은 간헐적인 스로틀링을 처리하지만 지속적인 대규모 평가에는 적절한 할당량 증가가 필요합니다.

**필수 IAM 정책:**

```
{  
    "Version": "2012-10-17",		 	 	   
    "Statement": [  
        {  
            "Effect": "Allow",  
            "Action": [  
                "bedrock:InvokeModel"  
            ],  
            "Resource": "arn:aws:bedrock:*::foundation-model/*"  
        }  
    ]  
}
```