/

AI 역량 평가 사후 Q&A는 지원자가 과제를 마친 뒤 자신의 결과물과 판단 근거를 직접 설명하고, 그 설명이 실제 구현과 일치하는지 확인하는 단계입니다. SK하이닉스 뉴스룸 공지에 따르면 SK하이닉스 AI 해커톤 2026 온라인 예선에서도 크래프톤의 AI 네이티브 채용·평가 솔루션인 'Cofa-Probe' 등 다양한 AI 활용 역량 평가 도구로 문제 정의, AI 활용, 결과 검증 과정 등을 종합적으로 살펴볼 예정입니다. 이런 방식의 평가는 채용과 대회에서도 쓰이고 있습니다.

핵심 요약

사후 Q&A는 내 제출물과 작업 기록을 바탕으로 질문하고, 내 설명이 실제 구현과 맞는지 대조합니다. 주로 작동 원리와 실패 가능성을 설명할 수 있는지(인지)를 봅니다. 대비는 '왜'를 기록해 두고, 약점→위험→다음 검증 순서로 답하는 연습입니다.

AI 역량 평가 사후 Q&A란 무엇입니까?

한 줄 답: 과제 제출물과 프롬프트·대화·검증 실행 기록을 바탕으로 결과물과 판단 근거를 묻고, 지원자의 설명이 실제 구현 및 코드 위치와 일치하는지 대조하는 단계입니다.

Cofa 공식 Probe 소개 페이지에 따르면 평가는 네 단계로 이어집니다. 먼저 실전 환경에서 과제를 시작하고(01), 작업하는 동안 "산출물과 함께 프롬프트, 도구 설정, AI 대화와 검증 실행 기록을 자동 수집합니다"(02). 이어지는 사후 Q&A 단계(03)에서는 지원자가 "자신의 결과물과 판단 근거를 설명합니다. 답변이 실제 구현과 일치하는지 확인합니다." 마지막으로 평가 리포트·면접 지원 단계(04)로 연결됩니다.

같은 공식 페이지는 리포트 단계에서 "후보자의 설명을 실제 구현과 대조합니다" 그리고 "판단의 근거가 된 코드와 해당 위치를 직접 확인합니다"라고 설명합니다. 또한 자주 묻는 질문 항목을 통해 "도구 사용법뿐 아니라 문제를 정의하고, AI에 일을 맡기고, 결과를 검증·설명하는 역량을 함께 평가합니다"라고 밝히고 있습니다.

머니투데이 김평화 기자의 기사 역시 "과제 종료 후에는 지원자가 결과물과 의사결정 과정을 설명하는 사후 Q&A를 진행하며, 시스템은 이를 종합해 평가 결과와 면접 참고 리포트를 만든다"고 보도했습니다.

기술·의도·인지 중 사후 Q&A는 어느 축과 가장 가깝습니까?

한 줄 답: 세 평가 축 가운데 결과물의 작동 원리와 실패 가능성을 본인 산출물 기준으로 설명할 수 있는지를 보는 인지 관리 축과 가장 가깝습니다.

Cofa 공식 Probe 페이지는 "산출물과 소스코드, 프롬프트와 작업 기록, 사후 Q&A를 함께 확인합니다. 기술·의도·인지의 기준으로 판단하고 그 근거를 리포트에 담습니다"라고 안내합니다. 머니투데이 기사에 따르면 이 솔루션은 7월 30일 펍지성수에서 크래프톤과 CJ올리브영이 공동 개최한 'Cofathon: AI Native Battlegrounds'에 적용되었으며, 평가 기준인 기술 관리(Technique), 의도 관리(Intent), 인지 관리(Cognition) 세 축을 통해 "AI에 목표와 제약을 정확하게 전달했는지, AI가 만든 결과를 직접 검증하고 수정했는지, 결과의 작동 원리와 실패 가능성을 설명할 수 있는지를 함께 살펴본다"고 설명합니다.

즉, 세 축 가운데 결과의 작동 원리와 실패 가능성을 설명할 수 있는지를 살피는 축이 바로 인지 관리입니다. 해당 행사의 공개 후기인 브런치 글쓴이 Luka의 「크래프톤 FDE 코파톤 후기」에서도 인지 관리를 "왜 이 구조인지·어떻게 동작하는지·어디서 깨지는지를 본인 산출물 기준으로 설명할 수 있는가?"로 정리하며, 글쓴이에 따르면 그 행사에서 "참가자 전원이 가장 낮았던 축"이었다고 전합니다.

실제로 어떤 질문이 나왔습니까?

한 줄 답: 코파톤 공개 후기 기준으로 특정 부분을 왜 그렇게 구현했는지와 장애가 발생하면 어떻게 대응할 것인지 등을 산출물 기반으로 즉석에서 물었습니다.

브런치 글쓴이 Luka가 작성한 그 행사의 공개 후기(「크래프톤 FDE 코파톤 후기」)에 따르면, 해당 행사에서는 과제를 제출한 뒤 산출물을 보고 즉석에서 생성된 확인 문제에 직접 답변하는 절차가 진행되었습니다.

후기 글쓴이 Luka가 소개한 질문 예시는 "특정 부분을 왜 그렇게 만들었나", "장애가 발생하면 어떻게 대응하나"와 같은 형태였습니다. 글쓴이는 직접 겪은 소감으로 "조리 있게 설명하기가 참 어렵더군요."라고 남겼습니다. AI 코딩 도구의 도움을 받아 기능을 빠르게 구현하더라도, 본인 산출물의 세부 구조와 예외 대응 방식을 스스로 소화하지 못하면 즉석에서 논리적으로 답하기 어렵다는 점을 보여 줍니다.

사후 Q&A는 어떻게 대비하면 좋습니까?

한 줄 답: 아래는 제 의견이며, 작업 중 결정 이유를 한 줄로 남기고 약점·위험·다음 검증 순서로 답하며 대체값 표시와 테스트·커밋 기록을 남기는 다섯 가지 습관이 도움이 됩니다.

아래는 제 의견입니다. 평소 과제를 수행할 때 다음 다섯 가지 습관을 들여 두면 사후 Q&A에서 내 구현과 일치하는 설명을 조리 있게 제시하는 데 도움이 됩니다. 작업 중 생기는 부채를 줄이는 배경이 궁금하다면 AI 코딩 에이전트에 일을 맡길 때 생기는 세 가지 부채 글도 함께 참고하시기 바랍니다.

  1. 단계마다 '왜 이렇게 했나'를 DECISIONS.md에 한 줄 기록하기: 구현 단계마다 무엇을 선택했고 왜 그렇게 판단했는지를 DECISIONS.md 파일에 한 줄씩 적어 둡니다. 과제가 끝난 뒤 기억에 의존하지 않고도 내 의사결정 근거를 바로 떠올릴 수 있습니다.
  2. 약점 → 위험 → 다음 검증 순서로 답하기: 브런치 후기 글쓴이 Luka는 확인 문제의 모범 답안이 "내 코드의 구체적인 약점 지점을 지목하고, 그게 왜 위험한지 파급을 설명하고, 다음에 뭘 검증·개선할지 계획을 제시하는 순서"였다고 관찰했습니다. 한계를 숨기기보다 약점과 파급 위험, 후속 검증 계획을 순서대로 짚는 연습이 유효합니다.
  3. 대체값·추정값을 실제 결과와 구분해 표시하기(예: 플래그 열): 결측치나 오류를 메우기 위해 넣은 대체값·추정값은 별도의 플래그 열 등으로 실제 정상 결과와 명확히 구분해 표시합니다. 어떤 데이터를 제외하거나 대체했는지 투명하게 드러나므로 설명이 쉬워집니다.
  4. 검증을 테스트 실행과 커밋 로그로 남기기: Cofa 공식 Probe 페이지에서 밝히듯 평가 과정에서는 "검증 실행 기록"을 자동 수집합니다. 눈으로만 대충 확인하고 넘어가기보다 테스트 실행과 정돈된 커밋 로그로 검증 흔적을 남겨 두면 내 설명을 뒷받침하는 근거가 됩니다.
  5. 질문의 전제가 내 구현과 다르면 그렇다고 먼저 말하기: 공식 페이지 안내처럼 사후 Q&A는 "답변이 실제 구현과 일치하는지 확인"하고 "후보자의 설명을 실제 구현과 대조합니다." 만약 질문이 내 실제 코드와 다른 전제를 담고 있다면 질문에 끌려가며 말을 맞추지 말고, 실제로는 어떻게 구현했는지부터 먼저 바로잡아 설명해야 합니다.

답변 예시는 어떤 모습입니까?

한 줄 답: 판단과 근거를 먼저 밝힌 뒤, 내 구현의 한계(약점)와 그로 인한 파급(위험), 이어서 확인·개선할 계획을 순서대로 제시하는 구조입니다.

아래는 제 연습용 예시입니다. 앞서 살펴본 원칙을 적용해 가상의 질문과 답변을 구성하면 다음과 같습니다.

질문(예시): "센서 온도 값 -9999를 왜 그렇게 처리했습니까?"

"-9999는 실제 온도가 아니라 센서가 값을 못 읽었을 때 들어가는 결측 표시로 판단했습니다. 그래서 평균 계산에서는 빼고, 해당 행에는 is_missing 플래그를 붙여 보고서에 몇 행이 빠졌는지 함께 적었습니다. 약점은 -9999가 아닌 다른 이상값(예: 순간적으로 튄 값)은 걸러지지 않는다는 점입니다. 이 값이 평균을 끌어올리면 보고서 결론이 틀어질 수 있습니다. 다음에는 정상 범위를 담당자에게 확인하고, 범위 밖 값에 대한 테스트를 추가하겠습니다."

이 답변은 먼저 왜 그렇게 처리했는지 판단과 근거(-9999를 결측 표시로 판단해 평균에서 제외하고 is_missing 플래그와 누락 행 수를 기록함)를 밝히고, 이어서 현 구현의 약점(다른 이상값은 걸러지지 않음)과 위험(평균이 왜곡되어 보고서 결론이 틀어질 수 있음), 그리고 다음 검증(정상 범위 확인 및 범위 밖 값 테스트 추가) 순서로 짜여 있습니다.

사후 Q&A 대비의 핵심을 정리하면 무엇입니까?

한 줄 답: 그럴듯한 결과물 자체보다 내가 왜 그렇게 판단하고 검증했는지를 코드와 기록으로 설명할 수 있는지가 핵심입니다.

AI 코딩 도구를 쓰면 돌아가는 결과물은 빠르게 만들 수 있지만, 평가가 확인하는 지점은 후보자가 자기 코드의 선택 이유와 한계를 정확히 알고 있는지입니다. 따라서 완성도 높은 화면이나 코드만 남기는 데 그치지 않고, 작업하는 순간마다 '왜 그렇게 했는지'를 설명할 수 있는 기록과 검증 흔적을 함께 남기는 습관이 가장 확실한 대비책입니다.

자주 묻는 질문은 무엇입니까?

한 줄 답: 사후 Q&A 중 도구 사용 규칙 확인, 기록 분량 조절, 모르는 질문이 나왔을 때의 대처법을 정리합니다.

사후 Q&A 답변에 AI 코딩 도구를 써도 됩니까?

일반 원칙으로 말씀드리면 평가마다 허용 규칙이 다르므로, 응시하는 평가의 공식 안내를 반드시 확인하고 그 규칙을 따르시기 바랍니다.

기록을 너무 많이 남기면 오히려 손해입니까?

제 의견으로는 긴 줄글 문서를 쓰느라 구현 시간을 뺏기면 손해입니다. DECISIONS.md에는 각 단계에서 무엇을 왜 골랐는지 한 줄 메모만 남겨 두어도 사후 Q&A에서 기억을 되살리기에 충분합니다.

모르는 질문이 나오면 어떻게 합니까?

제 의견으로는 아는 척하며 추측성 답변을 지어내지 않는 것이 좋습니다. 사후 Q&A는 답변을 실제 구현 및 코드와 대조하므로, 아직 확인하지 못한 부분이라면 그 사실을 솔직히 밝히고 어떤 위험이 있을지, 다음에 어떤 방법으로 확인할 것인지를 말씀하시기 바랍니다.

함께 보면 좋은 글은 무엇입니까?

한 줄 답: AI 코딩 에이전트 협업 시 쌓이는 세 가지 부채와 프롬프트·컨텍스트·하네스 엔지니어링 개념 정리 글을 함께 살펴보시기 바랍니다.

출처는 어디입니까?

한 줄 답: Cofa-Probe 공식 소개 페이지, 머니투데이 기사, 코파톤 참가 공개 후기, SK하이닉스 뉴스룸 공지를 참고했습니다.

multiple definition 에러는 헤더 파일에 일반 함수의 본문(정의)을 작성하고 여러 .cpp 파일에서 그 헤더를 포함했을 때 링크 단계에서 발생하는 대표적인 C++ 링크 에러입니다. 이 글에서는 2026-10-25(KST)에 g++ (Debian 14.2.0-19) 14.2.0으로 재현한 실제 코드와 링커 출력을 바탕으로 ODR(One Definition Rule) 위반 원인과 inline을 비롯한 해결 방법을 정리합니다.

핵심 요약

이 에러는 같은 함수의 정의가 여러 .cpp 파일에 하나씩 들어가 링커가 정의를 두 개 이상 발견할 때 납니다. 헤더에 함수 본문을 쓰면, 그 헤더를 포함한 .cpp마다 정의가 생기기 때문입니다. 헤더 쪽 함수에 inline을 붙이거나, 헤더에는 선언만 두고 정의는 .cpp 한 곳에 두면 해결됩니다.

multiple definition 에러란 무엇입니까?

한 줄 답: multiple definition 에러는 개별 소스 파일을 컴파일할 때가 아니라, 여러 목적 파일(.o)을 하나로 합치는 링크 단계에서 동일한 심볼 정의가 중복될 때 링커(ld/collect2)가 내는 링크 에러입니다.

C++의 ODR(One Definition Rule)에 따르면, 하나의 번역 단위(translation unit) 안에는 변수·함수·클래스 등의 정의가 하나만 있어야 하며, 프로그램 전체에서도 odr-used되는 모든 비-inline 함수나 변수는 오직 하나의 정의만 존재해야 합니다. 헤더에 비-inline 함수 본문을 정의하고 두 개의 .cpp 파일에서 포함하면 프로그램 전체에 정의가 하나만 있어야 한다는 규칙이 깨집니다.

아래는 문제를 재현하는 세 개의 파일입니다.

util.h:

#pragma once
int helper() { return 1; }

a.cpp:

#include "util.h"
int a() { return helper(); }

main.cpp:

#include "util.h"
int a();
int main() { return a() + helper(); }

이 파일들을 g++ -std=c++17 a.cpp main.cpp -o odr_bad 명령으로 빌드하면 종료 상태 1(exit status 1)과 함께 다음 출력이 나옵니다(임시 목적 파일 이름은 ccXXXX로 표기했습니다).

/usr/bin/ld: /tmp/ccXXXX.o: in function `helper()':
main.cpp:(.text+0x0): multiple definition of `helper()'; /tmp/ccXXXX.o:a.cpp:(.text+0x0): first defined here
collect2: error: ld returned 1 exit status

메시지 앞부분이 컴파일러가 아니라 /usr/bin/ld와 collect2로 시작하는 점에서 링크 에러임을 알 수 있습니다. 전처리기나 문법 단계에서 나는 컴파일 에러는 형태가 다르며, 예를 들어 매크로 이름 충돌로 발생하는 컴파일 에러는 expected unqualified-id before numeric constant 에러란? 매크로와 이름이 겹칠 때 C++ 해결 방법 글에서 확인할 수 있습니다.

헤더 가드가 있는데 왜 생깁니까?

한 줄 답: #pragma once나 헤더 가드는 하나의 번역 단위(.cpp 파일 하나) 안에서 같은 헤더가 두 번 포함되는 것만 막아 주므로, 서로 다른 .cpp 파일이 각자 헤더를 포함하는 것까지 막지는 못합니다.

각 .cpp 파일은 서로 독립된 번역 단위로 컴파일됩니다. 따라서 util.h 맨 위에 #pragma once가 있더라도 다음과 같이 처리됩니다.

  • a.cpp를 컴파일할 때 util.h가 한 번 포함되어 목적 파일에 helper()의 정의가 들어갑니다.
  • main.cpp를 컴파일할 때도 util.h가 한 번 포함되어 목적 파일에 helper()의 정의가 또 들어갑니다.
  • 각 번역 단위 안에서는 정의가 하나뿐이므로 컴파일은 통과하지만, 두 목적 파일을 합치는 링커 입장에서는 helper() 정의가 두 개가 되어 충돌합니다.

multiple definition 에러는 어떻게 고칩니까?

한 줄 답: 헤더의 함수 정의에 inline을 붙이거나, 헤더에는 선언만 남기고 정의를 하나의 .cpp 파일로 분리하거나, 파일 내부 전용 복사본이 필요하면 static 또는 익명 이름공간을 사용합니다.

1. 헤더의 함수에 inline 붙이기

헤더에 함수 본문을 그대로 두어야 한다면 함수 선언 앞에 inline 지정자를 붙입니다. 다음은 수정한 util_inline.h입니다.

#pragma once
inline int helper() { return 1; }

a_inline.cpp와 main_inline.cpp는 각각 a.cpp, main.cpp와 내용이 같고 포함하는 헤더만 "util_inline.h"로 바꾼 파일입니다. g++ -std=c++17 a_inline.cpp main_inline.cpp -o odr_inline 명령을 실행하면 에러 없이 링크되며, ./odr_inline; echo $? 실행 결과는 다음과 같습니다.

2

종료 코드 2는 a()가 반환한 1과 helper()가 반환한 1이 더해진 값입니다.

2. 헤더에는 선언만 두고 하나의 .cpp에 정의 분리하기

본문이 없는 함수 선언은 정의가 아니라 선언(declaration)입니다. 헤더에는 선언만 두고 실제 본문(정의)은 단 하나의 .cpp 파일에만 작성하면 프로그램 전체에서 정의가 하나만 존재하게 됩니다(아래 예시는 설명용 코드 조각이며 별도로 실행하지 않았습니다).

// util.h (설명용 예시 — 별도 실행 안 함)
#pragma once
int helper();

// util.cpp (설명용 예시 — 별도 실행 안 함)
#include "util.h"
int helper() { return 1; }

3. static 함수 또는 익명 이름공간 사용하기

헤더의 함수를 static으로 선언하거나 익명 이름공간(anonymous namespace) 안에 두면 내부 연결(internal linkage)을 갖게 됩니다. 이 경우 각 .cpp 파일마다 자기만의 별도 복사본이 생겨 링크 충돌은 나지 않지만, 포함된 파일 수만큼 함수 복사본이 만들어진다는 차이가 있습니다(아래 예시는 설명용 코드 조각이며 별도로 실행하지 않았습니다).

// util.h (설명용 예시 — 별도 실행 안 함)
#pragma once
static int helper() { return 1; }

한편 CMake로 빌드할 때는 같은 .cpp 소스 파일이 타깃에 두 번 추가되지 않았는지, 소스 목록에 한 번만 들어갔는지도 함께 확인해야 하며, 기본적인 CMake 빌드 설정 구성은 aarch64 크로스 컴파일 CMake, toolchain 파일은 어떻게 쓰나 글을 참고할 수 있습니다.

inline은 "인라인 확장"과 같은 뜻입니까?

한 줄 답: 원래 의도는 인라인 치환 힌트였으나, C++98부터 함수의 inline은 "여러 번역 단위에 동일한 정의가 나타나는 것을 허용한다"는 의미가 핵심입니다.

cppreference의 inline 지정자 문서에 따르면, 컴파일러는 inline이 붙지 않은 함수도 인라인 치환할 수 있고 반대로 inline이 붙은 함수를 일반 함수 호출로 처리할 수도 있습니다. 즉 현대 C++에서 inline의 실질적인 역할은 외부 연결(external linkage)을 가진 함수나 변수(C++17부터)가 서로 다른 번역 단위에 여러 번 정의되더라도 모든 정의의 토큰 시퀀스가 동일하다면(예: 같은 헤더를 여러 소스 파일에서 포함) ODR 위반으로 보지 않도록 허용하는 것입니다.

또한 C++17에서는 이 규칙이 변수로 확장되어 inline 변수가 도입되었으며, 이로써 헤더 전용(header-only) 라이브러리를 만들 때 가장 큰 걸림돌이었던 변수 정의 문제가 해결되었습니다. 컴파일러가 실제로 어떤 코드를 만들어 내는지 직접 확인하고 싶다면 C++ Insights란? 컴파일러가 숨겨 둔 코드(람다·range-for)를 직접 보는 법 글에서 소개한 도구를 활용할 수 있습니다.

자주 묻는 질문은 무엇입니까?

한 줄 답: 함수 템플릿, 클래스 정의 내부의 멤버 함수, 헤더의 전역 변수에서도 동일한 ODR 및 inline 규칙이 적용됩니다.

템플릿은 왜 헤더에 정의해도 됩니까?

ODR 규칙상 템플릿 엔티티(함수 템플릿, 클래스 템플릿 등)는 클래스 타입이나 inline 함수·변수와 마찬가지로, 서로 다른 번역 단위에 정의가 나타나고 토큰 시퀀스가 동일하기만 하면 프로그램 안에 여러 정의가 존재하는 것이 허용되기 때문입니다.

클래스 안에서 정의한 멤버 함수는 괜찮습니까?

괜찮습니다. 클래스 정의 내부에서 본문까지 완전히 정의된 함수와 constexpr 함수는 별도로 inline 키워드를 쓰지 않아도 암묵적으로 inline(implicitly inline)으로 취급됩니다.

전역 변수에서도 생깁니까?

생깁니다. 헤더에 비-inline 전역 변수를 정의하면 여러 .cpp에서 포함할 때 똑같이 중복 정의 에러가 발생하므로, 헤더에는 초기값 없는 extern 선언만 두고(초기값 없는 extern은 정의가 아닌 선언입니다) 하나의 .cpp 파일에서 정의하거나, C++17 이상이라면 inline 변수를 사용합니다.

함께 보면 좋은 글은 무엇입니까?

한 줄 답: C++ 컴파일 에러 해결, CMake 빌드 설정, 컴파일러 생성 코드 확인을 다룬 관련 글입니다.

출처는 어디입니까?

한 줄 답: cppreference의 정의·ODR 문서 및 inline 지정자 문서와 직접 실행한 GCC 결과입니다.

역전파(Backpropagation)는 다층 신경망이 내놓은 답이 목표값과 다를 때, 각 층의 가중치를 어느 방향으로 얼마나 조정해야 할지 계산하는 핵심 절차입니다. 이 글에서는 신경망 학습의 네 단계 흐름부터 기울기의 의미, 연쇄 법칙을 이용한 작은 계산 예제, 그리고 경사 하강법과의 역할 차이를 차례대로 정리합니다.

핵심 요약

역전파는 신경망이 낸 답과 정답의 차이(손실)를 출력층에서 입력층 쪽으로 거꾸로 전달하면서, 각 가중치가 손실에 얼마나 책임이 있는지(기울기)를 연쇄 법칙으로 계산하는 방법입니다. 이렇게 구한 기울기의 반대 방향으로 가중치를 조금씩 옮기는 것이 경사 하강법이고, 둘을 반복하는 것이 학습입니다.

역전파란 무엇입니까?

한 줄 답: 순전파로 구한 출력과 정답의 차이(손실)를 출력 쪽에서 입력 쪽으로 거꾸로 전달하며 각 가중치의 기울기를 구하는 절차입니다.

신경망의 학습 한 단계는 다음 네 과정을 반복하며 이루어집니다.

  1. 순전파(Forward pass): 입력값을 넣어 신경망의 출력을 계산합니다.
  2. 손실(Loss) 계산: 신경망이 낸 실제 출력과 목표 출력(정답) 사이의 차이를 측정합니다.
  3. 역전파(Backward pass, Backpropagation): 출력 쪽에서 입력 쪽으로 거꾸로 거슬러 올라가며, 연쇄 법칙(chain rule)을 사용해 각 가중치의 기울기(gradient)를 계산합니다.
  4. 가중치 갱신(Update): 구한 기울기의 반대 방향으로 각 가중치를 조금씩 이동시킵니다(경사 하강법). 이 네 과정을 반복합니다.

Rumelhart, Hinton, Williams가 1986년에 발표한 논문(Learning representations by back-propagating errors, Nature 323, 533–536, 1986년 10월 9일 발행)은 다층 신경망에서 이 역전파 절차를 널리 알린 논문입니다. 이 논문은 뉴런과 유사한 단위들로 이루어진 연결망에서 실제 출력 벡터와 원하는 출력 벡터의 차이를 줄이도록 연결 가중치를 반복 조정하는 학습 절차를 설명하며, 그 결과 내부의 은닉 단위(hidden units)들이 과제의 중요한 특징을 표현하게 된다고 기술합니다.

학습 단계(사전학습·파인튜닝 등) 구분은 사전학습·파인튜닝·RLHF 차이: LLM은 어떤 단계로 만들어지나?에서 다룹니다.

기울기는 무엇을 알려줍니까?

한 줄 답: 가중치를 조금 늘렸을 때 손실이 커지는지 작아지는지(부호)와 얼마나 민감하게 변하는지(크기)를 알려줍니다.

역전파를 통해 각 가중치마다 구하는 기울기(gradient)에는 두 가지 정보가 담겨 있습니다.

  • 부호(Sign): 해당 가중치를 조금 늘렸을 때 손실이 늘어나는지 줄어드는지를 알려줍니다.
  • 크기(Size): 가중치 변화에 대해 손실이 얼마나 민감하게 반응하는지를 나타냅니다.

이를 안개 낀 산에서 내려가는 상황에 비유할 수 있습니다. 앞이 보이지 않는 안개 속에서 서 있을 때 발바닥으로 느껴지는 경사(기울기)를 확인하고, 오르막의 반대쪽인 내리막 방향으로 한 걸음씩 내딛는 것과 같습니다.

연쇄 법칙은 역전파에서 어떻게 쓰입니까?

한 줄 답: 출력 쪽에서 구한 기울기에 각 연산 노드의 국소 기울기를 차례로 곱해 앞쪽 가중치의 기울기를 구합니다.

곱셈 노드 u = w × x와 덧셈 노드 z = u + b, 그리고 손실 L = (z − t)²으로 이루어진 작은 계산 그래프를 예로 들어 보겠습니다. 입력과 가중치, 목표값은 각각 x = 2, w = 3, b = 1, t = 5입니다.

u = w * x
z = u + b
L = (z - t)^2

x = 2, w = 3, b = 1, t = 5
Forward:  u = 3 * 2 = 6,  z = 6 + 1 = 7,  L = (7 - 5)^2 = 4
Backward: dL/dz = 2 * (z - t) = 2 * 2 = 4
          dL/du = 4,  dL/db = 4
          dL/dw = dL/du * x = 4 * 2 = 8
          dL/dx = dL/du * w = 4 * 3 = 12

순전파와 역전파 계산을 단계별로 풀면 다음과 같습니다.

  • 순전파(Forward): u = 3 × 2 = 6이고, z = 6 + 1 = 7이며, 손실 L = (7 − 5)² = 4입니다.
  • 출력에 대한 손실의 기울기: ∂L/∂z = 2 × (z − t) = 2 × 2 = 4입니다.
  • 덧셈 노드(z = u + b) 역전파: 덧셈 노드는 전달받은 기울기를 두 입력 쪽으로 그대로 넘겨줍니다. 따라서 ∂L/∂u = 4, ∂L/∂b = 4입니다.
  • 곱셈 노드(u = w × x) 역전파: 곱셈 노드는 전달받은 기울기에 상대쪽 입력값을 곱합니다. 따라서 ∂L/∂w = ∂L/∂u × x = 4 × 2 = 8이고, ∂L/∂x = ∂L/∂u × w = 4 × 3 = 12입니다.

결과를 해석해 보면 ∂L/∂w = 8로 양수이므로, w를 조금 늘리면 손실 L이 커진다는 뜻입니다. 따라서 손실을 줄이려는 경사 하강법은 w를 감소시키는 방향으로 움직입니다.

역전파와 경사 하강법은 무엇이 다릅니까?

한 줄 답: 역전파는 각 가중치의 기울기를 계산하는 단계이고, 경사 하강법은 그 기울기를 이용해 가중치를 실제로 갱신하는 단계입니다.

두 용어는 함께 쓰이지만 역할이 구분됩니다.

  • 역전파(Backpropagation): 연쇄 법칙을 사용해 출력 쪽에서 입력 쪽으로 거슬러 올라가며 각 가중치의 기울기(∂L/∂w)를 계산하는 절차입니다.
  • 경사 하강법(Gradient descent): 역전파로 구한 기울기의 반대 방향으로 각 가중치를 조금씩 옮겨 손실을 줄이는 갱신 절차입니다.

이때 한 걸음에 가중치를 얼마나 옮길지 결정하는 비율은 별도 글인 「학습률(Learning Rate)이란?」에서 다룹니다.

자주 묻는 질문은 무엇입니까?

한 줄 답: 기울기 소실 현상, 프레임워크의 자동 미분 지원 여부, 추론 시 역전파 실행 여부 등 세 가지 질문을 정리했습니다.

기울기 소실이란 무엇입니까?

깊은 신경망에서 역전파를 할 때 작은 국소 기울기들이 사슬을 따라 여러 번 곱해지면서, 입력층에 가까운 쪽의 기울기가 매우 작아져 해당 가중치들이 거의 바뀌지 않는 현상입니다. 활성화 함수 선택이 한 가지 요인이며, 활성화 함수는 별도 글인 「활성화 함수(Activation Function)란?」에서 다룹니다.

프레임워크가 역전파를 자동으로 해 줍니까?

그렇습니다. 딥러닝 프레임워크는 자동 미분(automatic differentiation)을 통해 각 가중치의 기울기를 자동으로 계산합니다.

추론할 때도 역전파를 합니까?

하지 않습니다. 학습이 끝난 모델을 사용하는 추론(inference) 단계에서는 순전파만 실행하며, 역전파나 가중치 갱신은 수행하지 않습니다.

함께 보면 좋은 글은 무엇입니까?

한 줄 답: 언어 모델의 학습 단계 구분과 과적합, AI 기초 개념 정리 글을 함께 참고하실 수 있습니다.

출처는 어디입니까?

한 줄 답: 다층 신경망의 역전파 학습 절차를 널리 알린 1986년 Nature 논문을 참고했습니다.

  • Rumelhart, D. E., Hinton, G. E., & Williams, R. J., "Learning representations by back-propagating errors", Nature 323, 533–536 (published 9 October 1986), https://doi.org/10.1038/323533a0 (확인 2026-10-25)

엔디안(Endianness)은 임베디드 면접에서 바이트 순서의 정의, C 코드로 호스트 바이트 순서를 확인하는 방법, 네트워크 및 이진 데이터 송수신 시 주의점을 물을 때 자주 나오는 주제입니다. 이 글의 실행 환경은 x86_64 리눅스(리틀엔디안)입니다.

핵심 요약

엔디안은 여러 바이트로 된 값을 메모리에 어떤 순서로 놓는지를 말합니다. 리틀엔디안은 가장 낮은 자리 바이트(LSB)를 낮은 주소에, 빅엔디안은 가장 높은 자리 바이트(MSB)를 낮은 주소에 둡니다. 리틀엔디안 호스트에서 0x12345678을 바이트로 읽으면 78 56 34 12가 나오고, 네트워크로 보낼 때는 htonl로 빅엔디안(네트워크 바이트 순서)으로 바꿉니다.

엔디안이란 무엇입니까?

한 줄 답: 엔디안(Endianness)은 여러 바이트로 이루어진 값을 메모리에 저장할 때 바이트를 배치하는 순서입니다.

엔디안은 한 바이트 내부의 비트(bit) 순서가 아니라, 멀티바이트 값의 바이트(byte)가 메모리에 놓이는 순서를 뜻합니다.

다른 아키텍처용으로 빌드할 때는 타깃 환경의 바이트 순서 같은 특성을 함께 고려해야 하며, 크로스 컴파일 설정 방법은 aarch64 크로스 컴파일 CMake, toolchain 파일은 어떻게 쓰나 글에서 다룹니다.

빅엔디안과 리틀엔디안은 어떻게 다릅니까?

한 줄 답: 가장 높은 자리 바이트(MSB)를 가장 낮은 메모리 주소에 두면 빅엔디안이고, 가장 낮은 자리 바이트(LSB)를 가장 낮은 메모리 주소에 두면 리틀엔디안입니다.

0x12345678에서 가장 높은 자리 바이트(Most Significant Byte, MSB)는 0x12이고, 가장 낮은 자리 바이트(Least Significant Byte, LSB)는 0x78입니다. 이 값을 메모리 주소 0x00부터 0x03까지 저장할 때 리틀엔디안은 78 56 34 12 순서로 배치하고, 개념상 빅엔디안은 12 34 56 78 순서로 배치합니다.

구분주소 0x00주소 0x01주소 0x02주소 0x03
리틀엔디안 (Little-endian)78 (LSB)563412 (MSB)
빅엔디안 (Big-endian, 개념)12 (MSB)345678 (LSB)

내 시스템의 엔디안은 어떻게 확인합니까?

한 줄 답: C 코드에서 멀티바이트 정수의 주소를 unsigned char *로 읽어 첫 바이트 값을 확인하거나, 리눅스 셸에서 lscpu 명령을 실행해 확인합니다.

C 언어에서 값의 바이트 배치를 확인할 때는 unsigned char * 포인터로 각 바이트를 읽는 방식을 일반적으로 사용합니다. 다음은 gcc (Debian 14.2.0-19) 14.2.0 환경에서 gcc -std=c11 -Wall endian.c -o endian 명령으로 경고 없이 컴파일한 뒤 ./endian을 실행한 코드입니다.

#include <stdio.h>
#include <stdint.h>
#include <arpa/inet.h>
int main(void) {
    uint32_t v = 0x12345678;
    const unsigned char *p = (const unsigned char *)&v;   /* 바이트 단위로 읽기 */
    printf("memory: %02x %02x %02x %02x\n", p[0], p[1], p[2], p[3]);
    printf("%s-endian\n", p[0] == 0x78 ? "little" : "big");
    uint32_t n = htonl(v);                                 /* 네트워크 바이트 순서(빅엔디안) */
    const unsigned char *q = (const unsigned char *)&n;
    printf("htonl:  %02x %02x %02x %02x\n", q[0], q[1], q[2], q[3]);
    printf("ntohl(htonl(v)) == v: %s\n", ntohl(n) == v ? "yes" : "no");
    return 0;
}

실행 결과는 다음과 같습니다.

memory: 78 56 34 12
little-endian
htonl:  12 34 56 78
ntohl(htonl(v)) == v: yes

같은 호스트에서 lscpu | grep -i "byte order" 명령을 실행하면 바이트 순서를 바로 확인할 수 있습니다.

Byte Order:                              Little Endian

또한 디버거인 gdb에서는 x 명령으로 메모리의 바이트를 직접 확인할 수 있으며, 자세한 사용법은 임베디드 리눅스에서 gdb로 크래시 원인은 어떻게 찾나? core dump·gdbserver 실전 글에서 확인할 수 있습니다.

통신·파일에서는 왜 문제가 됩니까?

한 줄 답: 바이트 순서가 다른 장치끼리 멀티바이트 이진 데이터를 변환 없이 주고받으면 바이트가 반대로 읽혀 값이 달라지기 때문입니다.

리눅스 매뉴얼 byteorder(3)(POSIX.1-2008 표준)에 따르면 인터넷에서 쓰이는 네트워크 바이트 순서는 가장 높은 자리 바이트가 먼저 오는 빅엔디안(Most Significant Byte first)인 반면, 리틀엔디안 호스트의 바이트 순서는 가장 낮은 자리 바이트가 먼저 오는 방식(Least Significant Byte first)입니다. <arpa/inet.h>에 선언된 htonl과 htons는 호스트 바이트 순서를 네트워크 바이트 순서로 변환하고, ntohl과 ntohs는 다시 호스트 바이트 순서로 되돌립니다. 위 실행 결과에서 htonl: 12 34 56 78과 ntohl(htonl(v)) == v: yes가 출력된 것처럼, 바이트 순서가 다른 장치 사이에 이진 데이터나 파일을 교환할 때는 바이트 순서를 미리 정하고 변환 함수를 거쳐야 합니다.

임베디드 통신 인터페이스 비교는 I2C vs SPI vs UART 차이: 임베디드 면접에서 어떻게 비교하나? 글에서 다룹니다.

면접에서 자주 나오는 엔디안 질문은 무엇입니까?

한 줄 답: 문자열(char 배열)의 바이트 순서, 구조체 전송 시 패딩 문제, 비트 필드 배치, 공용체(union)를 이용한 확인 시 C와 C++의 차이를 자주 묻습니다.
문자열(char 배열)에도 엔디안이 있습니까?

char 배열이나 문자열은 각 원소가 단일 바이트이므로 바이트 순서에 따라 글자 순서가 바뀌지 않으며, 엔디안은 여러 바이트로 이루어진 값에서 문제가 됩니다.

구조체를 그대로 전송하면 왜 안 됩니까?

바이트 순서 차이뿐만 아니라 컴파일러가 정렬(alignment)을 위해 멤버 사이에 패딩(padding)을 넣을 수 있어 컴파일러나 아키텍처마다 구조체의 실제 바이트 배치가 달라질 수 있기 때문입니다.

비트 필드는 무엇을 조심해야 합니까?

비트 필드(bit-field)가 저장 단위 안에 어떤 순서로 배치되는지는 구현 정의(implementation-defined) 사항이므로, 서로 다른 시스템 사이에 비트 필드 구조체를 원시 바이트 그대로 주고받지 않아야 합니다.

공용체(union)로 엔디안을 확인해도 됩니까?

C(C99 TC3 이후)에서는 마지막으로 기록한 멤버가 아닌 다른 공용체 멤버를 읽으면 저장된 객체 표현을 재해석(type punning)하지만, C++에서는 마지막으로 기록하지 않은 공용체 멤버를 읽는 것이 미정의 동작(undefined behavior)이고 많은 컴파일러가 비표준 확장으로만 허용하므로 C++에서는 unsigned char *로 바이트를 읽는 편이 안전합니다.

함께 보면 좋은 글은 무엇입니까?

한 줄 답: 크로스 컴파일, 임베디드 리눅스 부팅 과정, gdb 메모리 디버깅, 통신 인터페이스 비교 글을 함께 참고하시기 바랍니다.

출처는 어디입니까?

한 줄 답: Linux man-pages byteorder(3) 문서와 cppreference의 C·C++ 공용체(union) 문서를 참고했습니다.

C++ 람다 캡처는 람다식이 바깥 스코프의 변수를 값([=])이나 참조([&])로 가져오는 문법입니다. 람다를 함수 밖으로 반환하거나 저장할 때 캡처 방식을 잘못 고르면 수명이 끝난 변수를 가리키는 댕글링 참조가 생깁니다.

핵심 요약

[=]는 람다를 만들 때 변수를 복사해 두고, [&]는 원래 변수를 참조합니다. 그래서 람다를 만든 뒤 값을 바꾸면 [=] 쪽은 예전 값, [&] 쪽은 바뀐 값을 봅니다. 람다가 변수보다 오래 살면(반환, 저장, 다른 스레드로 넘기기) 참조 캡처는 댕글링이 되므로 값으로 캡처해야 합니다.

C++ 람다 캡처란 무엇입니까?

한 줄 답: C++ 람다 캡처는 람다식이 만들어내는 이름 없는 함수 객체(클로저)가 바깥 스코프의 변수를 복사본이나 참조로 붙잡아 두는 문법입니다.

cppreference의 "Lambda expressions" 문서에 따르면, 람다식은 스코프 안의 변수를 캡처할 수 있는 이름 없는 함수 객체인 클로저(closure)를 생성합니다. 대괄호로 적는 캡처 리스트는 어떤 변수를 어떤 방식으로 가져올지 지정합니다.

  • []: 아무 변수도 캡처하지 않습니다.
  • [=]: 람다 본문에서 쓰는 바깥 변수를 기본적으로 값(복사)으로 캡처합니다.
  • [&]: 람다 본문에서 쓰는 바깥 변수를 기본적으로 참조로 캡처합니다.
  • [x]: 변수 x만 값(복사)으로 캡처합니다.
  • [&x]: 변수 x만 참조로 캡처합니다.
  • [=, &x]: 기본적으로 값으로 복사하되, x만 참조로 캡처합니다.

컴파일러가 람다를 내부적으로 어떤 클래스로 바꾸는지는 C++ Insights란? 컴파일러가 숨겨 둔 코드(람다·range-for)를 직접 보는 법 글에서 확인할 수 있습니다.

[=]와 [&]는 무엇이 다릅니까?

한 줄 답: [=]는 람다식이 평가될 때 변수를 클로저 멤버로 복사하고, [&]는 원본 객체에 접근하는 참조를 바인딩합니다.

값으로 캡처([=] 또는 & 없는 변수 이름)하면 클로저 안에 복사본을 담는 멤버가 생기고, 람다식이 평가되는 시점에 초기화됩니다. 반면 참조로 캡처([&] 또는 &이름)하면 람다 본문이 원본 객체에 직접 접근합니다. 또한 mutable 키워드가 없으면 클로저의 operator()가 const 멤버 함수이므로 값으로 복사한 변수를 본문 안에서 수정할 수 없습니다. mutable을 붙이면 복사본을 수정할 수 있지만 원본 변수는 바뀌지 않습니다.

다음은 g++ (Debian 14.2.0-19) 14.2.0 환경에서 g++ -std=c++17 -Wall lambda_capture.cpp로 경고 없이 컴파일한 lambda_capture.cpp입니다.

#include <cstdio>
int main() {
    int x = 1;
    auto by_value = [=] { return x; };   // 만들 때 x를 복사
    auto by_ref   = [&] { return x; };   // x를 참조
    x = 42;
    std::printf("by_value=%d by_ref=%d\n", by_value(), by_ref());
    int counter = 0;
    auto inc = [counter]() mutable { return ++counter; };  // 복사본만 바뀜
    inc(); inc();
    std::printf("inc()=%d counter=%d\n", inc(), counter);
}

실행 결과는 다음과 같습니다.

by_value=1 by_ref=42
inc()=3 counter=0
  • by_value는 람다가 만들어질 때의 x 값인 1을 복사해 두었으므로, 이후 x = 42;로 바꾸어도 1을 반환합니다.
  • by_ref는 원본 x를 참조하므로 바뀐 값인 42를 반환합니다.
  • inc는 [counter]() mutable로 선언되어 내부 복사본을 세 번 증가시켜 3을 반환하지만, 바깥의 원본 counter는 그대로 0입니다.

댕글링 참조는 언제 생깁니까?

한 줄 답: 참조로 캡처한 대상의 수명이 끝난 뒤에 람다의 operator()를 호출하면 댕글링 참조가 되어 정의되지 않은 동작(UB)이 발생합니다.

cppreference에 정리된 규칙처럼, 클로저는 참조로 캡처한 객체의 수명을 연장하지 않습니다(this를 통해 캡처된 *this도 마찬가지입니다). 따라서 지역 변수를 참조로 캡처한 람다를 함수 밖으로 반환하면, 함수가 끝나는 순간 지역 변수가 사라져 참조가 빈자리를 가리키게 됩니다.

다음은 지역 변수 count를 참조로 캡처해 반환하는 lambda_dangling.cpp입니다.

#include <cstdio>
#include <functional>
std::function<int()> make_counter() {
    int count = 10;
    return [&count] { return ++count; };   // 지역 변수를 참조로 캡처 -> 함수가 끝나면 댕글링
}
int main() {
    auto f = make_counter();
    std::printf("%d\n", f());   // 정의되지 않은 동작(UB)
}

g++ -std=c++17 -Wall -Wextra -O2 lambda_dangling.cpp로 컴파일하면 다음과 같은 경고가 출력됩니다(중간의 인라인 경로 안내는 ...으로 생략했습니다).

In lambda function,
...
lambda_dangling.cpp:5:32: warning: ‘count’ is used uninitialized [-Wuninitialized]

이 실행 파일을 세 번 실행했을 때 각각 32572, 32608, 32723이 출력되었습니다(종료 코드 0). 댕글링 참조를 읽는 것은 정의되지 않은 동작입니다. 위 숫자는 이 환경에서 우연히 나온 값이며, 다른 컴파일러·옵션에서는 다른 값이 나오거나 프로그램이 죽을 수 있고, 아무 일 없이 맞는 값처럼 보일 수도 있습니다.

댕글링 캡처는 어떻게 찾고 고칩니까?

한 줄 답: AddressSanitizer(-fsanitize=address)로 실행 중 stack-use-after-return을 검출하고, 람다가 스코프보다 오래 살아남을 때는 값 캡처나 [*this]를 사용합니다.

컴파일 옵션에 AddressSanitizer, 메모리 오류를 실행 중에 찾는 법에서 다룬 -fsanitize=address를 넣고 g++ -std=c++17 -g -O0 -fsanitize=address lambda_dangling.cpp로 빌드해 실행하면, 종료 코드 1과 함께 다음과 같은 에러가 출력됩니다(실행마다 달라지는 PID와 주소는 가렸습니다).

==PID==ERROR: AddressSanitizer: stack-use-after-return on address 0x... at pc 0x... bp 0x... sp 0x...
READ of size 4 at 0x... thread T0
    #0 0x... in operator() .../lambda_dangling.cpp:5
...
SUMMARY: AddressSanitizer: stack-use-after-return .../lambda_dangling.cpp:5 in operator()

함수가 반환된 뒤 스택 메모리를 읽었다는 stack-use-after-return이 lambda_dangling.cpp:5에서 정확히 잡힙니다. 람다가 현재 스코프보다 오래 살아남는다면 참조 대신 값으로 캡처해야 합니다. 다음은 값 캡처로 고친 lambda_fixed.cpp이며, g++ -std=c++17 -Wall -Wextra로 경고 없이 컴파일됩니다.

#include <cstdio>
#include <functional>
std::function<int()> make_counter() {
    int count = 10;
    return [count]() mutable { return ++count; };   // 값으로 캡처: 람다가 복사본을 가짐
}
int main() {
    auto f = make_counter();
    std::printf("%d\n", f());
    std::printf("%d\n", f());
}

실행 결과는 다음과 같습니다.

11
12

실무에서 기억해 둘 기준은 두 가지입니다. 첫째, 람다를 반환하거나 컨테이너·콜백·다른 스레드로 넘겨 스코프 밖에서 실행한다면 값으로 캡처합니다. 둘째, 클래스 멤버 함수 안에서 멤버 변수에 접근할 때는 this를 거치는데, [=]를 쓰면 객체 자체가 복사되는 것이 아니라 this 포인터가 복사되어 결과적으로 *this가 참조로 암묵 캡처됩니다([=]를 통한 this 암묵 캡처는 C++20부터 비권장입니다). 객체가 먼저 소멸할 수 있다면 C++17에 도입된 [*this]를 사용해 객체 전체를 복사본으로 캡처합니다.

자주 묻는 질문은 무엇입니까?

한 줄 답: 이동 캡처 문법(C++14), [=] 사용 시 포인터와 this 주의점, 스마트 포인터 캡처의 수명 규칙을 정리합니다.

이동 캡처는 어떻게 합니까?

C++14부터 도입된 초기화 캡처(init capture) 문법인 [v = std::move(v)]를 사용하면 복사할 수 없는 이동 전용(move-only) 타입도 람다 안으로 이동시킬 수 있습니다(위 한 줄 문법 예시는 별도로 실행하지 않은 코드 조각입니다). 복사와 이동의 차이는 복사 생성자 vs 이동 생성자 차이: 언제 무엇이 호출되나? 글을 참고하시기 바랍니다.

[=]만 쓰면 항상 안전합니까?

항상 안전하지는 않습니다. 포인터 변수를 값으로 캡처하면 주소값만 복사되므로 가리키던 대상이 먼저 소멸하면 여전히 댕글링 포인터가 되며, 멤버 함수 안에서는 [=]가 this 포인터만 복사하므로 객체 수명이 끝나면 *this 접근이 정의되지 않은 동작이 됩니다.

스마트 포인터를 캡처하면 어떻습니까?

std::shared_ptr를 값으로 복사해 캡처하면 클로저가 살아 있는 동안 참조 카운트가 유지되어 대상 객체의 수명도 함께 유지됩니다(C++ unique_ptr와 shared_ptr, 면접에서 고르는 기준 참고).

함께 보면 좋은 글은 무엇입니까?

한 줄 답: 람다의 클래스 변환, AddressSanitizer 사용법, 스마트 포인터, 복사·이동 생성자를 다룬 글입니다.

출처는 어디입니까?

한 줄 답: cppreference의 Lambda expressions 문서와 GCC 14.2.0 실측 환경입니다.

LLM 신뢰도 점수(confidence score)는 질문에 답하는 에이전트를 만들 때 답변을 그대로 내보낼지, 사람에게 넘길지 결정하기 위해 흔히 찾는 값입니다. 하지만 모델에게 직접 확신도를 물어봐서 얻은 숫자는 그 자체로 믿기 어렵습니다. 왜 그런지, 그리고 보정(calibration)이 무엇인지 정리합니다.

핵심 요약

LLM에게 "얼마나 확신해?"라고 물어서 나온 숫자는 측정값이 아니라, 다른 글자처럼 토큰으로 이어 쓴 문장입니다. 같은 질문을 다시 하거나 말투를 바꾸면 숫자도 바뀝니다. 믿을 수 있는 신뢰도는 "이 점수라고 했을 때 실제로 얼마나 맞았나"를 기록해 맞춰 보는 보정(calibration)을 거쳐야 합니다.

LLM 신뢰도 점수란 무엇이고, "보정됐다"는 무슨 뜻입니까?

한 줄 답: 예측에 붙인 확신도와 실제로 맞은 비율이 여러 사례에서 일치할 때 그 신뢰도 점수가 보정(calibration)되었다고 합니다.

예보관의 확신도는 오늘 관측한 정보와 과거 기록에서 나옵니다. 같은 정보에 같은 규칙을 적용하면 언제나 같은 확신도가 나와야 합니다. 어떤 확신도가 믿을 만한지는 한 번의 예측만으로는 알 수 없고, 여러 사례를 모아 확인해야 합니다. 같은 확신도를 매긴 사례들을 한데 묶었을 때 실제로 그 일이 일어난 빈도와 비교하는 방식입니다. 말한 확신도와 실제 빈도가 일치하면 보정(calibration)되었다고 하며, 말한 것보다 덜 일어났다면 과신(overconfidence), 더 자주 일어났다면 과소확신(underconfidence) 상태입니다.

다만 보정되었다는 것만으로는 충분하지 않습니다. 날마다 계절 평균치만 똑같이 말하는 예보관도 장기 비율을 따져 보면 보정된 상태이지만, 특정한 날에 대해 아무런 정보도 주지 못합니다. 실제로 쓸모 있는 신뢰도 점수가 되려면 관찰된 정보를 활용하면서도 보정되어 있어야 하며, 누군가 모든 예측과 실제 결과를 빠짐없이 기록해야만 그 확신도가 정확한지 확인할 수 있습니다.

신경망 보정을 다룬 Guo 등의 논문(ICML 2017)에서도 보정을 예측에 부여된 확률이 실제 정답일 가능성을 반영하는 성질로 정의합니다. 완벽하게 보정된 모델이라면 확신도 p를 부여한 예측들 가운데 실제로 맞은 비율이 정확히 p와 같습니다. 이를 시각적으로 나타낸 것이 신뢰도 다이어그램(reliability diagram)입니다. 예측들을 확신도 구간별로 묶어 가로축에 확신도, 세로축에 실제 정확도를 그린 그래프로, 완벽하게 보정된 모델은 대각선 위에 놓이고 대각선에서 벗어난 정도가 곧 보정 오차가 됩니다. 해당 논문은 신경망이 깊고 커질수록 실제 정확도보다 높은 확신도를 내놓는 과신(overconfidence) 경향이 나타난다고 보고합니다.

LLM이 말한 확신도는 왜 믿기 어렵습니까?

한 줄 답: LLM은 이전 토큰에 이어 다음 토큰을 예측해 글자를 쓸 뿐, 내부 계기판에서 확신도를 읽어 숫자를 내놓는 것이 아니기 때문입니다.

LLM은 앞서 나온 토큰들을 바탕으로 다음 토큰을 하나씩 이어 쓰는 자기회귀(autoregressive) 방식으로 문장을 만듭니다. 토큰 단위로 글자를 생성하는 기본 원리는 토큰과 컨텍스트 윈도우란? LLM이 글자를 세는 방식과 길이 제한이 생기는 이유에서 다룹니다. 모델이 문장 끝에 적는 확신도 숫자 역시 내부 계기판에서 읽어 온 측정값이 아니라, 다른 일반 텍스트와 똑같은 방식으로 이어 쓴 그럴듯한 다음 토큰일 뿐입니다. 사전학습 과정은 텍스트의 다음 토큰을 잘 맞히도록 모델을 평가할 뿐, 모델이 말한 확신도가 실제 정답 빈도와 일치하는지를 평가하지 않습니다.

그 결과 실제로 관찰할 수 있는 세 가지 현상이 나타납니다.

  • 같은 질문을 다시 물어보면 숫자가 바뀔 수 있습니다.
  • "정말 확실합니까?"처럼 묻는 말투나 표현을 바꾸면 숫자가 달라질 수 있습니다.
  • 숫자를 쓰기 전에 이미 생성한 문장(예컨대 답이 명백히 맞다고 단정하는 문장)이 뒤이어 나올 확신도 숫자를 그에 맞춰 끌고 갑니다.

이러한 한계는 "Asking LLM for confidence" 블로그 글에서 정리되었고, 해커 뉴스(Hacker News)의 "Using LLM for confidence scoring is a bad idea" 토론에서도 논의되었습니다. 토론 참가자들은 루브릭이나 좁은 척도를 쓰자는 의견, 혹은 토큰 확률을 보자는 의견을 내놓기도 했으나, 다른 참가자들은 그런 방식 역시 결국 보정 과정을 거쳐야 한다고 지적했습니다.

그럼 믿을 수 있는 신뢰도는 어떻게 만듭니까?

한 줄 답: 모델 바깥에서 관찰 가능한 정보를 고르고, 모든 답변과 실제 정답 여부를 기록해 그룹별 실제 정답률로 규칙을 갱신해야 합니다.

모델에게 얼마나 확신하는지 직접 물어보는 프롬프트에는 이러한 구조적 한계가 있으며, 프롬프트 설계 전반의 내용은 프롬프트 엔지니어링 정리 글에서 확인할 수 있습니다. 원문 블로그는 질문에 답하는 에이전트를 예로 들어, LLM 바깥에서 다음 다섯 단계로 신뢰도 규칙을 만드는 방법을 제시합니다.

  1. 관찰 가능한 정보 선택: 검색에서 관련 문서를 찾았는지, 이전에 답해 본 유형의 질문인지, 같은 질문에 대한 두 번의 답변이 서로 일치하는지처럼 모델 바깥에서 확인할 수 있는 정보를 고릅니다.
  2. 과거 기본 비율에서 출발: 처음에는 과거 전체 기록의 기본 정답 비율(base rate)에서 시작합니다.
  3. 모든 답변과 결과 기록: 모든 답변마다 그때 관찰된 정보, 부여한 신뢰도, 그리고 실제로 답변이 맞았는지를 빠짐없이 기록합니다.
  4. 그룹별 실제 정답률로 규칙 갱신: 같은 조건으로 묶인 그룹별로 실제 맞은 비율을 집계하여 신뢰도를 매기는 규칙을 갱신합니다.
  5. 주기적 재확인과 수정: 시간이 지나면서 특정 그룹이 과신이나 과소확신으로 치우치지 않는지 주기적으로 다시 확인하고 규칙을 고칩니다.

결국 이 규칙이 얼마나 믿을 만한지는 얼마나 충실하게 기록을 쌓았는지에 달려 있습니다.

토큰 확률(logprob)과는 무엇이 다릅니까?

한 줄 답: 토큰 확률은 모델이 다음 토큰에 매긴 내부 확률이라 말로 쓴 확신도와 다르지만, 실제 정답률과 맞는지 보정 확인을 거쳐야 한다는 점은 같습니다.

모델이 텍스트로 직접 적어 낸 확신도 숫자와, 모델이 다음 토큰을 고를 때 내부에서 계산하는 토큰 확률(logprob)은 구분해야 합니다. 토큰 확률은 말로 이어 쓴 문장이 아니라 모델 내부의 계산값이라는 점에서 측정값에 더 가깝습니다. 하지만 다음 토큰이 나올 확률이 곧 전체 답변이 사실로서 맞을 확률을 뜻하지는 않으므로, 토큰 확률을 신뢰도 점수에 활용하더라도 기록된 실제 결과와 대조해 보정(calibration)을 확인해야 합니다.

이처럼 모델 바깥의 기록을 보는 대신 모델 내부 작동 방식을 들여다보는 다른 접근은 기계적 해석가능성(Mechanistic Interpretability)이란?에서 다루며, 토큰을 고를 때 쓰는 샘플링 설정은 Temperature와 Top-p란? LLM 답변이 매번 달라지는 이유에서 다룹니다.

자주 묻는 질문은 무엇입니까?

한 줄 답: 프롬프트 변경만으로는 확신도를 보증할 수 없으며, 보정 여부와 함께 개별 사례의 정보를 반영하는지, 기록이 쌓이고 있는지를 확인해야 합니다.

프롬프트를 바꾸면 확신도를 믿을 수 있게 됩니까?

프롬프트만 바꿔서는 믿을 수 없습니다. 묻는 말투나 척도를 바꾸더라도, 그 점수가 나왔을 때 실제로 얼마나 맞았는지를 기록과 대조해 확인하지 않으면 보정 여부를 알 수 없기 때문입니다.

보정만 되면 좋은 신뢰도입니까?

아닙니다. 날마다 계절 평균만 말하는 예보관도 전체 비율로만 보면 보정된 상태이지만, 오늘의 구체적인 판단에는 아무 도움이 되지 않습니다. 좋은 신뢰도는 관찰된 정보를 활용해 사례를 구분하면서도 보정되어 있어야 합니다.

기록이 적을 때는 어떻게 합니까?

처음에는 과거의 기본 비율(base rate)에서 출발합니다. 초기에는 그룹 구분이 거칠 수밖에 없지만, 답변과 실제 정답 여부를 계속 기록하면서 데이터가 쌓이면 그룹별 규칙을 점차 정교하게 다듬을 수 있습니다.

함께 보면 좋은 글은 무엇입니까?

한 줄 답: 토큰 생성 원리, 프롬프트 설계, 기계적 해석가능성, 샘플링 설정을 다룬 글입니다.

출처는 어디입니까?

한 줄 답: 원문 블로그 글과 해커 뉴스 토론, 그리고 신경망 보정 정의를 다룬 ICML 논문입니다.

+ Recent posts