테이블, 데이터베이스(DB), 지식 그래프(KG)와 같은 구조화된 지식은 법률 판결, 질병 진단, 투자 분석 등 다양한 지능형 애플리케이션의 핵심 기반을 이룹니다. 이러한 애플리케이션의 중심 기술인 구조화 지식 추론(SKR)은 테이블 질의 응답(TableQA), 텍스트-SQL 변환(Text-to-SQL), 지식 그래프 질의 응답(KGQA) 등의 형태로 자연어 처리(NLP) 연구의 중요한 분야로 자리 잡았습니다. 최근 대규모 언어 모델(LLM)의 강력한 생성 능력을 활용한 연구들은 단일 유형의 구조화 지식 추론에서 상당한 발전을 이루었습니다.
그러나 실제 세계의 복잡한 애플리케이션은 종종 여러 유형의 구조화 지식을 통합하여 사용합니다. 예를 들어, 의료 의사결정 지원 시스템은 환자 데이터베이스와 의약품 지식 그래프를 동시에 추론해야 할 수 있습니다. 이는 다양한 SKR 작업을 통합된 방식으로 처리할 수 있는 능력을 요구합니다. 하지만 기존의 대부분의 방법은 특정 작업에 맞춰 설계되어 다양한 SKR 작업 간의 간극을 메우는 데 어려움을 겪습니다.
LLM을 기반으로 하는 최근 연구들은 StructGPT, Readi, TrustUQA와 같은 여러 통합 SKR 프레임워크를 제안했습니다. 이 방법들은 작업별 전략에 의존하거나 사용자 정의된 표현 방식을 사용함으로써 통합을 달성했지만, 성능에 제약이 있었습니다. 특히, Readi와 TrustUQA는 데이터베이스 추론 커버리지 측면에서 부족함을 보였습니다.
이상적인 통합 SKR 프레임워크는 두 가지 핵심 특징을 가져야 합니다. 첫째, 다양한 구조화 지식 소스 간의 지식 전이를 촉진해야 합니다. 예를 들어, SQL과 SPARQL 쿼리가 외부 구문은 다르지만 동일한 의미를 가질 수 있습니다. 이러한 쿼리를 통일된 표현으로 변환하면 LLM이 다른 SKR 작업의 지식을 활용하여 특정 작업의 성능을 향상시키는 데 도움이 될 수 있습니다. 둘째, 다양한 구조화 지식을 LLM에 친숙한 통합된 형식으로 표현해야 합니다. 코드는 LLM이 프로그래밍 언어에 대한 광범위한 사전 훈련 덕분에 코드를 이해하고, 생성하고, 추론하는 데 탁월하므로, 구조적이고 조합 가능한 특성 때문에 이상적인 선택입니다. 다양한 지식을 코드로 변환하면 입력 표현과 LLM의 고유한 이해 사이의 간극을 줄일 수 있습니다.
본 논문에서는 PANDas cOde-dRiven Agent (PANDORA)라는 새로운 통합 SKR 프레임워크를 제안합니다. 이 프레임워크는 잘 정렬된 LLM, 추론 메모리, Python 인터프리터의 세 가지 핵심 구성 요소로 이루어져 있습니다. Pandora는 테이블, 데이터베이스, 지식 그래프를 Pandas 라이브러리 기반의 통합 표현인 "Pandora의 BOX"로 변환합니다. 각 자연어 질문(NLQ)에 대해 Pandora 에이전트는 LLM을 활용하여 먼저 자연어 추론 단계와 실행 가능한 Python 코드를 생성합니다. 생성된 코드는 BOX에서 답을 도출하기 위해 실행됩니다. 메모리는 훈련 예제를 통해 구축되며, LLM이 NLQ를 Pandas API에 매핑하는 방법을 학습할 수 있도록 컨텍스트 학습(ICL)을 위한 주석이 달린 데모를 제공합니다. 다양한 SKR 작업에 걸친 지식 전이를 활용하기 위해 데모는 모든 SKR 작업에서 수집될 수 있습니다. 또한, 인터프리터가 제공하는 코드 실행 피드백은 모델이 추론 단계를 개선하고 코드를 수정하도록 추가로 유도합니다. 우리는 Text-to-SQL, TableQA, KGQA 세 가지 구조화 지식 추론 작업을 포함하는 네 가지 널리 사용되는 데이터셋에 대해 광범위한 실험을 수행했습니다. 실험 결과는 우리의 방법이 기존의 모든 통합 구조화 지식 추론 프레임워크보다 우수하며, 작업별 방법과도 효과적으로 경쟁함을 보여주었습니다.
본 논문의 기여는 다음과 같습니다:
- LLM을 활용하여 다양한 구조화 지식에 적용 가능한 코드 기반 추론 단계를 생성하는 새로운 프레임워크를 제안합니다. 우리가 아는 한, 이는 SKR의 통합 메커니즘으로 코드를 활용하는 최초의 시도입니다.
- 공유 데모를 통해 다양한 구조화 지식 소스 간의 지식 전이를 촉진하여 통합 프레임워크의 성능을 향상시키는 방법을 제시합니다.
- 여러 주류 벤치마크에서 포괄적인 실험을 수행했으며, 우리의 방법은 통합 구조화 지식 추론에서 최신 성능을 달성합니다.
2. 사전 지식
2.1 구조화 지식
다음 세 가지 유형의 구조화 지식에 초점을 맞춥니다:
데이터 테이블: 테이블은 $T = (\{c_i\}_{i=1}^{C}, \{r_j\}_{j=1}^{R}, \{v_{i, j}\}_{i=1, j=1}^{C, R})$로 표현될 수 있으며, 여기서 $c_i$는 $i$번째 열 이름을, $r_j$는 열 인덱스에 따른 데이터 레코드를 나타냅니다. $v_{i, j}$는 $c_i$와 $r_j$의 교차점에 위치한 셀 내용입니다.
데이터베이스: 데이터베이스 $\mathcal{D}$는 여러 테이블을 포함하며, $\mathcal{D} = \{\mathcal{T}_1, \mathcal{T}_2, \ldots, \mathcal{T}_{\mathcal{T}}\}$로 표현됩니다. 열 이름 외에도, 모든 테이블 간의 외래 키가 두 테이블의 데이터를 연결하는 데 사용되며, $\{\langle c_i^p, c_j^q \rangle\}$로 표기합니다. 여기서 $c_i^p$와 $c_j^q$는 각각 $\mathcal{T}_p$와 $\mathcal{T}_q$의 $i$번째 열과 $j$번째 열을 나타냅니다.
지식 그래프: 지식 그래프(KG)는 일반적으로 주어-술어-목적어 삼중항의 집합으로, $\mathcal{K}=\{\langle s, p, o \rangle \mid s \in \mathcal{E}, p \in \mathcal{R}, o \in \mathcal{E} \cup \Gamma\}$로 표현됩니다. 여기서 $\mathcal{E}, \mathcal{R}$ 및 $\Gamma$는 각각 개체(entity) 집합, 관계(relation) 집합, 유형(type) 집합을 나타냅니다.
2.2 문제 공식화
자연어 질문 $Q$와 접근 가능한 구조화 지식 $\mathcal{S}$ (예: 테이블 $\mathcal{T}$, 데이터베이스 $\mathcal{D}$, 또는 지식 그래프 $\mathcal{K}$)가 주어졌을 때, 목표는 $\mathcal{S}$에서 필요한 답변 $\mathcal{A}$를 검색할 수 있는 실행 가능한 쿼리를 생성하는 것입니다.
2.3 BOX 및 Pandas 코드 표현
다양한 SKR 작업 간의 지식 전이를 촉진하기 위해, 우리는 BOX라는 통일된 지식 표현 구조를 제안합니다.
정의 1 (BOX): BOX는 데이터 구조로서 $B=(b, \Phi, \Psi)$로 표기됩니다. 여기서 $b$는 텍스트 이름을 나타내고, $\Phi=\{\phi_i\}_{i=1}^{N}$, 그리고 $\Psi=\{[\psi_j^{\phi_i}]_{j=1}^{M}\}_{i=1}^{N}$ 입니다. $\phi_i$는 테이블의 열 이름이나 KG 관계일 수 있는 필드를 나타냅니다. $\psi_j^{\phi_i}$는 필드 $\phi_i$와 관련된 $j$번째 값을 나타냅니다. 값 $\psi_j^{\phi_i}$는 테이블 셀 내용이나 KG 개체일 수 있습니다.
BOX는 Python으로 이해하고 조작할 수 있는 동적 테이블로 간주될 수 있습니다. 실험에서는 Pandas 라이브러리를 사용하여 BOX를 구현했습니다. 특히, BOX $B = (b, \Phi, \Psi)$는 Pandas 코드에서 다음과 같이 표현됩니다:
import pandas as pd
data_frame_name = pd.DataFrame({
'field_1': [value_1_1, ...],
'field_2': [value_2_1, ...],
...
})
여기서 field_1과 value_2_1은 각각 $\phi_1$과 $\psi_1^{\phi_2}$의 코드 표현입니다. Pandas는 BOX를 조작하는 다양한 메서드를 제공합니다. 예를 들어, BOX authors와 BOX books를 병합하여 새로운 BOX merged_book_data를 만든 후, 다음 코드와 같이 필터링 작업을 수행할 수 있습니다:
filtered_data = merged_book_data[
(merged_book_data['author_name'] == 'J.K. Rowling') &
(merged_book_data['publication_year'] > 2005)
]
Pandas는 그룹화, 정렬, 집계와 같은 추가적인 강력한 도구도 제공하여 NLQ에서 발견되는 다양한 쿼리 논리를 처리할 수 있습니다. 추가 예시는 부록 A를 참조하십시오.
3. 구조화 지식의 BOX 변환
구조화 지식 $\mathcal{S}$를 해당 BOX로 변환하는 과정을 설명합니다.
3.1 테이블을 BOX로 변환
데이터 테이블 $T=\left(\left\{c_{i}\right\}_{i=1}^{C},\left\{r_{j}\right\}_{j=1}^{R},\left\{v_{i, j}\right\}_{i=1, j=1}^{C, R}\right)$는 각 열 이름 $c_{i}$를 필드 이름 $\phi_{i}$로, 각 셀 내용 $v_{i, j}$를 필드 값 $\psi_{j}^{\phi_{i}}$로 간주하여, BOX $B=\left(b,\left\{\phi_{i}\right\}_{i=1}^{C},\left\{\left[\psi_{j}^{\phi_{i}}\right]_{j=1}^{R}\right\}_{i=1}^{C}\right)$로 원활하게 변환될 수 있습니다.
3.2 데이터베이스를 BOX로 변환
데이터베이스 $\mathcal{D}=\left\{\mathcal{T}_{i}\right\}_{i=1}^{T}$의 경우, 각 테이블 $\mathcal{T}_{i} \in \mathcal{D}$는 3.1절에 설명된 절차에 따라 BOX $B_{i}$로 변환됩니다. 동시에 외래 키 정보 $\left\{\left(\phi_{i}^{p}, \phi_{j}^{q}\right)\right\}$는 유지되며, 여기서 $\phi_{i}^{p}$와 $\phi_{j}^{q}$는 각각 $B_{p}$의 $i$번째 필드와 $B_{q}$의 $j$번째 필드를 나타냅니다.
3.3 지식 그래프를 BOX로 변환
지식 그래프 $\mathcal{K}=\{\langle s, p, o\rangle \mid s \in \mathcal{E}, p \in \mathcal{R}, o \in \mathcal{E} \cup \Gamma\}$는 규모가 매우 크기 때문에 각 NLQ $Q$에 대해 서브그래프를 추출해야 합니다. 구체적으로, 먼저 깊이 우선 탐색을 실행하여 $Q$에서 언급된 각 주제 개체의 $H$-홉 서브그래프 $\mathcal{K}^{*} \subset \mathcal{K}$를 추출합니다. 여기서 $\mathcal{E}^{*} \subset \mathcal{E}$와 $\Gamma^{*} \subset \Gamma$는 각각 $\mathcal{K}^{*}$의 개체 집합과 유형 집합을 나타냅니다. 검색 공간을 더 좁히기 위해, Xie 외(2022)의 데이터를 활용하여 $Q$와 높은 임베딩 유사성을 가진 관계 $\mathcal{R}^{*} \subseteq \mathcal{R}$만 유지합니다. 이어서, 각 개체 유형 $\gamma \in \Gamma^{*}$와 해당 개체 집합 $\mathcal{E}_{\gamma}=\left\{e \mid \exists\left\langle e, \mathrm{IsA}, \gamma\right\rangle \in \mathcal{K}^{*}\right\}$에 대해 BOX $\mathcal{B}_{\gamma}=\left(\gamma, \Phi_{\gamma}^{1: N}, \Psi_{\gamma}^{1: N}\right)$를 구성합니다. 구체적으로, 필드 이름 $\Phi_{\gamma}^{1: N}=\Phi_{\gamma}^{1} \cup \Phi_{\gamma}^{2: N}$, 여기서 $\Phi_{\gamma}^{1}=\{\gamma\}$이고, $\Phi_{\gamma}^{2: N}=\left\{\phi_{i} \mid \phi_{i} \in \mathcal{R}^{*}, \exists\left\langle s, \phi_{i}, o\right\rangle \in \mathcal{K}^{*}, s \in \mathcal{E}_{\gamma}\right\}_{i=2}^{N}$는 $\mathcal{E}_{\gamma}$ 개체에서 파생된 1-홉 관계를 포함합니다. 마찬가지로, 필드 값 $\Psi_{\gamma}=\Psi_{\gamma}^{1} \cup \Psi_{\gamma}^{2: N}$, 여기서 $\Psi_{\gamma}^{1}=\left\{\left[\psi_{1, j} \mid \psi_{1, j} \in \mathcal{E}_{\gamma}\right]_{j=1}^{M}\right\}_{i=1}^{1}$는 $\Phi_{\gamma}^{1}$에 해당하며 유형이 $\gamma$인 개체를 포함합니다. $\Psi_{\gamma}^{2: N}=\left\{\left[\psi_{i, j} \mid \exists\left\langle s, p, \psi_{i, j}\right\rangle \in \mathcal{K}^{*}, s \in \mathcal{E}_{\gamma}, p \in \mathcal{R}^{*}\right]_{j=1}^{M}\right\}_{i=2}^{N}$는 $\Phi_{\gamma}^{2: N}$에 해당하며 $\mathcal{R}^{*}$의 관계를 통해 $\mathcal{E}_{\gamma}$ 개체의 1-홉 이웃을 포함합니다. KG 관점에서, BOX $\mathcal{B}_{\gamma}$에 대해 $\psi_{1, j}$는 주어 역할을 하고, $\phi_{i}$는 술어를 나타내며, $\psi_{i, j}$는 목적어 역할을 합니다. 이 방식으로 Pandas의 병합(merge)을 사용하여 BOX들을 연결함으로써 KG에서 다중 홉 추론을 구현할 수 있습니다. 모든 BOX가 구성된 후, 외래 키 정보는 $\left\{\left(\phi_{i}^{p}, \phi_{j}^{q}\right)\right\}$로 정의되며, 여기서 $\phi_{i}^{p}$와 $\phi_{j}^{q}$는 적어도 하나의 공통 개체를 가집니다. 자세한 KG-to-BOX 알고리즘은 부록 B.3을 참조하십시오.
4. 판도라 (Pandora)
4.1 개요
Pandora는 잘 정렬된 LLM $f_{\theta}$ (코드 기반 추론 단계 생성 담당), NLQ와 유효한 추론 단계 쌍을 저장하는 메모리 $\mathcal{M}$ (컨텍스트 학습용), 그리고 생성된 코드를 실행하는 Python 인터프리터 $\mathcal{I}$의 세 가지 주요 구성 요소로 구성된 에이전트입니다. 일반적으로 Pandora는 코드 기반 추론과 코드 실행이라는 두 가지 주요 동작을 통해 작동하며, 이는 $\mathcal{I}$에 의해 정의된 Python의 BOX 환경과 상호작용합니다.
4.2 판도라 추론
Pandora의 추론 과정은 다음과 같습니다. 먼저, 구조화 지식 $\mathcal{S}$가 일련의 BOX $\mathcal{B}^{*}$로 변환됩니다. 다음으로, $\mathcal{B}^{*}$와 NLQ $Q$는 프롬프트 $\mathcal{X}$에 통합되어 $f_{\theta}$에 입력됩니다. $f_{\theta}$는 이후 자연어 추론 $R$과 실행 가능한 Python 코드 $C$를 포함하는 코드 기반 추론 단계 $\mathcal{Y}=R, C$를 생성합니다. 마지막으로, $\mathcal{I}$를 사용하여 $\mathcal{B}^{*}$에서 코드 $C$를 실행함으로써 답변 $\mathcal{A}$가 도출됩니다.
컨텍스트 추론: $f_{\theta}$가 NLQ에서 다양한 Pandas API로의 매핑을 이해하도록 돕기 위해 컨텍스트 학습(ICL)을 활용합니다. 특히, 프롬프트 $\mathcal{X}$의 구조는 다음과 같습니다:
$$ \mathcal{X}=\mathcal{P}, \mathcal{Q}_{1}, \mathcal{B}_{1}^{*}, \mathcal{Y}_{1}, \ldots \mathcal{Q}_{K}, \mathcal{B}_{K}^{*}, \mathcal{Y}_{K}, \mathcal{Q}, \mathcal{B}^{*} $$
여기서 $\mathcal{P}$는 $f_{\theta}$가 먼저 $R$을 생성한 다음 $C$를 생성하도록 지시하는 자연어 명령어입니다. 이는 CoT(Chain of Thought) 개념을 채택한 것입니다. 입력 길이 관리를 위해 모든 값 $\Psi$는 모든 프롬프트에서 제외됩니다. $(\mathcal{Q}_{k}, \mathcal{B}_{k}^{*}, \mathcal{Y}_{k})(1 \leq k \leq K)$는 메모리 $\mathcal{M}$에서 검색된 데모를 구성합니다. 전체 프롬프트는 부록 C.2를 참조하십시오. 그런 다음 $f_{\theta}$는 다음을 추정하여 $\mathcal{Y}$를 생성합니다:
$$ P(\mathcal{Y} \mid \mathcal{X}, \theta)=\prod_{j=1}^{|\mathcal{Y}|} P\left(y_{j} \mid \mathcal{X}, y_{<j}, \theta\right) $$
여기서 $y_{j}$는 $\mathcal{Y}$의 $j$번째 토큰을 나타냅니다.
공유 데모 검색: 통합 BOX 표현에서, 우리는 구조화 지식 $\mathcal{S}_a$에 대한 추론이 Pandas API를 공유하므로 $f_{\theta}$가 다른 유형의 구조화 지식 $\mathcal{S}_b$를 추론하는 데 잠재적으로 도움이 될 수 있다고 가정합니다. 따라서 $\mathcal{M}$에서 $(\mathcal{Q}_{k}, \mathcal{B}_{k}^{*}, \mathcal{Y}_{k})$를 검색할 때, $\mathcal{Q}_{k}$와 $\mathcal{Q}$가 동일한 SKR 작업에서 유래할 필요는 없습니다. $\mathcal{Q}$에 대한 $K$개의 데모는 가장 높은 의미론적 유사도에 따라 선택됩니다:
$$ s\left(\mathcal{Q}_{k}, \mathcal{Q}\right)=\cos \left(g_{\theta}\left(\mathcal{Q}_{k}\right), g_{\theta}(\mathcal{Q})\right) $$
여기서 $g(\mathcal{Q}) \in \mathbb{R}^{d}$는 전용 LLM 인코더 $g_{\theta}$를 통해 얻은 $\mathcal{Q}$의 임베딩을 나타냅니다.
실행 가이드: 생성된 코드 $C$의 환각 문제를 완화하기 위해, 우리는 코드 실행 결과를 피드백으로 활용하여 $f_{\theta}$가 $C$를 수정하도록 유도합니다. 특히, $C$가 인터프리터 $\mathcal{I}$에 의해 실행될 때, 결과 $\mathcal{A}$가 다음 두 조건 중 하나를 만족하면 유효하지 않다고 간주되고 $f_{\theta}$에 피드백됩니다: a) $C$의 실행이 오류를 발생시키는 경우. b) $\mathcal{A}$가 비어 있는 경우. $\mathcal{I}$로부터의 오류 메시지는 $\mathcal{F}$로 기록됩니다. 실행 가이드 프롬프트 템플릿은 다음과 같습니다:
$$ \mathcal{X}_{\mathcal{F}}=\mathcal{P}_{\mathrm{EG}}\left|\mathcal{Q}, \mathcal{B}^{*}, \mathcal{R}, \mathcal{C}\right| \mathcal{F} $$
여기서 $\mathcal{P}_{\text {EG }}$는 자연어 명령어를 나타냅니다. 이후, $\mathcal{X}_{\mathcal{F}}$는 $f_{\theta}$에 입력되어 수정된 $\mathcal{Y}_{\mathcal{F}}$를 제공합니다. 이 과정은 $\mathcal{Y}_{\mathcal{F}}$가 유효해지거나 우리가 설정한 상한 $L$을 초과할 때까지 계속됩니다.
4.3 판도라 학습
Pandora의 학습 과정은 주로 훈련 데이터의 NLQ를 Python 코드로 주석 처리하고 이를 메모리 $\mathcal{M}$에 저장하는 것을 포함합니다. 이는 두 단계로 나눌 수 있습니다.
4.3.1 추론 메모리 초기화
첫 번째 단계에서는 DB SKR 작업의 훈련 NLQ에 주석을 답니다. 일반적으로 Spider와 같은 DB SKR 작업은 신뢰할 수 있는 수동 작성 SQL 레이블을 제공하며, 이는 사용 가능한 데모가 없더라도 코드 주석의 어려움을 줄이는 데 도움이 됩니다. 특히, 훈련 예제 $(\tilde{\mathcal{Q}}, \tilde{\mathcal{S}}, \tilde{\mathcal{Z}}, \tilde{\mathcal{A}})$가 주어졌을 때, 여기서 $\tilde{\mathcal{Z}}$는 SQL 레이블을 나타내고 $\tilde{\mathcal{A}}$는 정답 세트이며, $f_{\theta}$는 코드 기반 레이블 $\tilde{\mathcal{Y}}=\tilde{\mathcal{R}}, \tilde{\mathcal{C}}$를 생성하는 데 사용됩니다. 프롬프트 형식 구조는 다음과 같습니다:
$$ \mathcal{X}=\mathcal{P}_{\text {train }} \mid \tilde{\mathcal{Q}}, \tilde{\mathcal{S}}, \tilde{\mathcal{Z}}, \tilde{\mathcal{B}}^{*} $$
여기서 $\mathcal{P}_{\text {train }}$는 명령어이고 $\tilde{\mathcal{B}}^{*}$는 $\tilde{\mathcal{S}}$에서 변환된 BOX 집합입니다. $\tilde{\mathcal{Y}}$의 품질을 보장하기 위해 실행 가이드(EG) 전략이 사용됩니다. 또한, $\tilde{\mathcal{C}}$ 실행을 통해 얻은 검색된 답변 $\mathcal{A}$는 $\tilde{\mathcal{A}}$와 비교됩니다. 이 비교 결과는 $f_{\theta}$에 피드백되어 추가적인 자체 수정을 수행합니다. 최종적으로, 모든 올바른 $\tilde{\mathcal{C}}$ 샘플은 $\mathcal{M}_{0}$을 형성하는 데 사용됩니다.
4.3.2 다중 작업 적응
두 번째 단계에서는 $\mathcal{M}_{0}$(DB SKR)의 예제를 데모로 활용하여 KG 및 테이블 SKR 작업의 훈련 NLQ에 주석을 답니다. 이는 그들의 특정 레이블을 사용하는 대신입니다. 주요 이유는 다음과 같습니다: a) 테이블 SKR 작업의 예제는 NLQ-답변 쌍만을 포함하며, 추론 단계를 설명하는 논리적 쿼리가 부족합니다. b) 우리의 실험 결과에 따르면 $f_{\theta}$는 SPARQL보다 SQL에 대한 이해가 더 좋습니다. 여기서도 EG 전략이 적용됩니다. 강조할 점은 세 가지 SKR 작업에 대해 소수의 데이터만 주석 처리되어 최종적으로 메모리 $\mathcal{M}$을 형성했다는 것입니다.
5. 실험
5.1 데이터셋 및 평가 지표
우리는 세 가지 SKR 작업에 대해 방법을 평가했습니다:
- DB SKR: 복잡하고 교차 도메인 텍스트-SQL 생성을 위해 수동으로 주석이 달린 데이터셋인 Spider를 사용합니다. 이 데이터셋은 다양한 데이터베이스와 복잡한 NLQ를 포함하며, 정확한 SQL을 구축하기 위해 다단계 추론과 데이터베이스 스키마에 대한 깊은 이해가 필요합니다.
- 테이블 SKR: 실제 세계 테이블 질의 응답에 특화된 데이터셋인 WikiTableQuestions(WikiTQ)를 사용합니다. 이 데이터셋은 집계, 비교, 필터링과 같은 작업을 수행해야 합니다.
- KG SKR: 최대 다중 홉 추론이 필요한 Freebase 지식 그래프의 NLQ를 포함하는 GrailQA 및 WebQSP를 사용합니다. 이 작업들은 개체, 관계 및 복잡한 논리적 구조를 포함합니다.
기존 연구를 따라 Spider와 WikiTQ는 실행 정확도(EX) 및 지칭 정확도(DA)로 평가합니다. GrailQA와 WebQSP는 Hit@1을 평가 지표로 사용합니다. 또한, 예측된 답변 집합과 정답 집합 간의 F1 점수를 계산합니다.
5.2 구현 세부 사항
우리는 $f_{\theta}$에 gpt-4o-mini-2024-07-18을, $g_{\theta}$에 bge-large-en-v1.5를 사용했습니다. 모든 컨텍스트 추론의 데모 수 $K$는 10으로 설정했습니다. KG 서브그래프의 홉 수 $H$는 GrailQA에 대해 3으로, WebQSP에 대해 2로 설정되었습니다. 각 NLQ에 대해 실행 가이드(EG)는 최대 $L=3$회 수행됩니다. 사용된 데이터셋과 우리 메모리 $\mathcal{M}$의 통계는 원본 논문의 그림 4에 제시되어 있습니다.
5.3 경쟁 방법
- 단일 유형 SKR 작업의 경쟁 기준선:
- KG SKR: RnG-KBQA, TIARA, DecAF, KB-Binder, KB-Coder.
- DB SKR: DIN-SQL, DAIL, CodeS, DTS-SQL.
- 테이블 SKR: TAPEX, Binder, DATER, STR.
- 순수 LLM 또는 통합 SKR 방법: CodeLlama, DeepSeek-Coder, UnifiedSKG, StructLM, StructGPT, Readi, TrustUQA.
($\boldsymbol{\Delta}$는 대상 데이터셋에 대한 미세 조정을 나타냅니다.)
5.4 DB SKR 결과
Spider 데이터셋에서 Pandora는 StructLM-7B를 1.7% 능가하며, LLM 사전 훈련과의 정렬 및 작업 간 지식 효과적인 전이 능력을 입증했습니다. DTS-SQL과 같은 작업별 미세 조정 방법이 여전히 성능에서 우위를 보이지만, Pandora는 이러한 격차를 좁히며 작업별 미세 조정 없이도 DB SKR 작업에서 우수한 능력을 보여주었습니다.
5.5 테이블 SKR 결과
WikiTQ 데이터셋에서 Pandora는 최고의 통합 미세 조정 방법인 Readi를 2.2% 앞서며 선두 통합 모델로 자리매김했습니다. 또한, Pandora는 DATER를 약간 상회하며 작업별 미세 조정 없이도 경쟁력 있는 결과를 달성할 수 있음을 입증했습니다.
5.6 KG SKR 결과
GrailQA 벤치마크에서 Pandora는 기존 통합 방법을 능가하고 미세 조정되지 않은 방법 중 최고 성능을 달성하며 KB-Coder를 16% 초과했습니다. WebQSP에서는 Pandora가 경쟁력 있는 결과를 제공하여, Hit@1 지표에서 기존 최고 통합 SKR 방법인 TrustUQA에 단 0.7% 뒤졌습니다.
5.7 다른 백본 LLM의 영향
gpt-4o와 gpt-4o-mini를 $f_{\theta}$로 사용했을 때의 Pandora 성능은 원본 논문의 그림 5에 제시되어 있습니다. gpt-4o 사용 비용을 최소화하기 위해, 각 데이터셋에서 동일한 200개의 NLQ를 무작위로 추출하여 두 설정에서 평가했습니다. gpt-4o는 특히 논리적 형식 레이블이 부족한 WikiTQ에서 gpt-4o-mini보다 지속적으로 우수한 성능을 보였습니다.
5.8 제거 연구
제안된 Pandora의 성능을 평가하기 위해 다음 구성 요소를 순차적으로 제거했습니다: a) - 실행 가이드(-EG): 인터프리터 $\mathcal{I}$로부터 피드백을 받지 않고 추론 단계 $R$과 코드 $C$를 생성합니다. b) - 공유 데모(-SD): 테스트 NLQ와 동일한 데이터셋의 예제만 데모로 사용합니다. c) - 유사성 검색(-SR): 원본 논문의 식 (2)에 설명된 과정을 무시하고 데모를 무작위로 선택합니다. d) - 컨텍스트 추론(-ICR): $R$과 $C$를 생성하기 위해 데모 없이 제로샷 추론을 사용합니다. e) - 코드 스타일(-CS): Pandas 코드를 생성하는 대신 원본 작업 레이블(예: SQL 또는 SPARQL)을 직접 생성합니다.
제거 연구 결과는 제안된 Pandora 프레임워크의 각 구성 요소의 기여도를 보여줍니다. EG 제거는 평균 성능을 5.5% 감소시켰으며, 이는 추론 및 실행 개선을 위한 인터프리터 피드백의 중요성을 보여줍니다. CS를 제외하면 성능이 17.2% 추가로 감소하여, 구조화된 추론 작업을 위한 통합 표현으로서 Pandora 코드의 효과를 입증했습니다. SD 제거는 5.4% 감소로 이어졌으며, 데이터셋 간 지식 전이의 중요성을 강조했습니다.
5.9 데모 수의 영향
데모 수 변경이 Pandora 성능에 미치는 영향을 연구했습니다. 실험 결과는 원본 논문의 그림 6에 제시되어 있습니다. 예제 수를 늘리면 다양한 설정에서 Pandora의 성능이 지속적으로 향상됩니다. Spider 및 WikiTQ 데이터셋에서 SD 및 SR 제거는 성능을 크게 저하시켰습니다. WebQSP에서 SD의 성능이 더 낮았던 것은 해당 데이터셋의 상대적으로 간단한 SPARQL 구조 때문일 수 있습니다. 다른 데이터셋에 존재하는 다양성이 노이즈를 유발하여 유효성을 떨어뜨렸을 수 있습니다.
5.10 오류 분석
제안된 방법의 한계를 구체적으로 평가하기 위해 세 가지 SKR 작업에서 각각 50개의 샘플을 무작위로 추출하여 오류 분석을 수행했습니다. 다음 유형의 오류를 요약했습니다:
- 실행 실패: 코드가 실행되지 못했습니다.
- BOX 오류: 코드의 BOX가 잘못되었거나 누락되었습니다.
- 필드 오류: 코드의 필드가 잘못되었거나 누락되었습니다.
- 추론 논리 오류: 관련 BOX와 필드는 정확하지만, 코드의 논리가 잘못되었습니다.
- 쿼리 의도 오류: 쿼리 의도를 오해하거나 잘못 구현했습니다.
- 출력 형식 오류: 이론적인 답변은 정확하지만, 반환된 형식이 주석이 달린 답변과 일치하지 않습니다.
각종 오류 유형의 분포는 원본 논문의 그림 7에 제시되어 있습니다. Pandas 추론 환경에서 필드(즉, 열 또는 KG 관계)를 식별하는 것은 여전히 주요 과제입니다. 또한, Python 출력 메서드의 광범위한 범위는 출력 형식 오류를 초래했습니다.
6. 관련 연구
DB SKR: 이 작업은 일반적으로 자연어 질문을 SQL 쿼리로 변환하는 텍스트-SQL을 통해 해결되며, 모델 아키텍처 및 중간 표현에 중점을 둡니다. 최근 방법은 LLM을 활용하여 작업 분해, CoT(Chain-of-Thought), 자기 일관성과 같은 기술을 결합하여 향상된 결과를 얻고 있습니다. 다른 연구는 오픈 소스 LLM을 미세 조정하여 독점 모델과 동등하거나 능가하는 데 중점을 둡니다.
KG SKR: 이 작업은 지식 그래프를 사용하여 자연어 질문에 답하는 KGQA를 통해 해결됩니다. 전통적인 방법은 일반적으로 실행 가능한 논리적 형식을 생성하기 위한 의미 분석 또는 쿼리 일치를 위한 임베딩 기반 기술을 포함합니다. 최근 LLM을 사용한 발전은 DecAF와 같이 논리적 형식과 직접적인 답변 생성을 결합하며, KB-BINDER는 BM25를 통해 성능을 향상시킵니다. 마찬가지로, KB-Coder는 코드 스타일 패러다임에서 ICL을 활용하여 더 나은 성능을 달성합니다.
테이블 SKR: 이 작업은 자연어 질문과 구조화된 테이블 데이터 모두에 대한 추론을 요구합니다. 전통적인 방법은 의미 분석 또는 테이블-쿼리 일치를 위한 임베딩 기반 방법에 의존합니다. TAPEX, Binder, DATER와 같은 현대 LLM 기반 모델은 복잡한 테이블과 자연어 질문을 분해하여 우수한 성능을 보여줍니다.
통합 구조화 지식 추론: UnifiedSKG 및 StructLM과 같은 초기 통합 프레임워크는 T5 및 CodeLlama와 같은 모델을 미세 조정하여 여러 구조화 지식 데이터셋을 통합함으로써 구조화 지식 이해를 향상시켰습니다. 최근의 통합 질의 응답 프레임워크는 다양한 구조화 데이터 유형을 처리합니다. 예를 들어, StructGPT는 증거를 검색하고 답변을 생성하기 위해 반복적인 읽기-추론 전략을 채택하고, Readi는 추론 경로를 반복적으로 정제하여 증거를 추출하고 답변을 생성합니다. 이러한 발전에도 불구하고, 이들은 특정 데이터 전략에 의존하여 통합성을 제한합니다. 가장 관련 있는 작업인 TrustUQA는 통일된 그래프 표현을 제안하고 해석 가능한 쿼리를 생성하지만, LLM의 사전 훈련된 지식과의 간극을 만드는 표현을 재정의했습니다. 대조적으로, 우리의 방법인 Pandora는 코드 기반 통합 표현을 채택하여 LLM의 이해와 더 잘 일치합니다.
7. 결론 및 한계
본 논문에서는 Pandas API를 구조화 지식의 표준 표현 형식으로 사용하는 통합 SKR 에이전트 Pandora를 제안했습니다. 자연어 추론과 실행 가능한 Python 코드를 결합함으로써 Pandora는 추론 단계의 반복적인 최적화, 재사용을 위한 메모리 저장, 그리고 효율적인 작업 간 지식 전이를 달성합니다. 포괄적인 실험은 Pandora가 기존의 통합 방법보다 우수하며, 작업별 방법과도 경쟁력을 유지함을 입증했습니다. 향후에는 이 프레임워크를 더 많은 구조화 지식으로 확장하고, 더 광범위한 SKR 작업을 처리하기 위해 더 고급 추론 능력을 탐색할 계획입니다.
한계: Pandas API를 사용하여 구조화 지식을 표현하는 것은 표준화되고 효율적인 방법이지만, 몇 가지 한계도 따릅니다. 첫째, Pandas API는 주로 테이블 데이터 조작을 위해 설계되었으므로, 그래프나 중첩 데이터셋과 같은 비테이블 또는 계층적 데이터 구조에 대한 추론이 필요한 작업에서의 적응성을 제한할 수 있습니다. 둘째, 이러한 API에 의존하면 Pandas의 범위를 넘어서는 특정 라이브러리 또는 기술이 관련된 도메인별 추론을 처리하기 어려워져, 고도로 전문화된 애플리케이션에서 프레임워크의 일반성을 저해할 수 있습니다.
부록 A: Pandas 코드 예시
A.1 Text-to-SQL 예시
다음은 SQL 쿼리를 동등한 Pandas 코드로 변환한 예시입니다. 필터링, 정렬, 그룹화, 중첩 쿼리 등을 다룹니다.
NLQ: 임시 담당자가 'Yes'이고 직원 수가 2명 이상인 부서의 이름과 직원 수를 보여주세요.
# 데이터: department (department_id, name, num_employees), management (department_id, temporary_acting)
# SQL:
# SELECT T1.name, T1.num_employees
# FROM department AS T1 JOIN management AS T2
# ON T1.department_id = T2.department_id
# WHERE T2.temporary_acting = 'Yes'
# HAVING count(*) >= 2
# Pandas 코드:
import pandas as pd
# 가정: department_df, management_df는 이미 로드된 Pandas DataFrame입니다.
# 두 DataFrame을 'department_id'를 기준으로 병합합니다.
merged_dept_mgmt = pd.merge(
department_df, management_df,
on='department_id',
how='inner'
)
# 'temporary_acting'이 'Yes'인 행을 필터링합니다.
filtered_temp_acting = merged_dept_mgmt[
merged_dept_mgmt['temporary_acting'] == 'Yes'
]
# 부서별로 그룹화하고, 각 부서의 레코드 수를 계산합니다.
dept_counts = filtered_temp_acting.groupby('department_id').size().reset_index(name='record_count')
# record_count가 2 이상인 부서만 필터링합니다.
multi_manager_depts = dept_counts[dept_counts['record_count'] >= 2]
# 원래 filtered_temp_acting 데이터프레임에서 해당 부서 정보를 추출합니다.
# 이름과 직원 수를 가져오기 위해 department_id를 다시 병합합니다.
result_df = pd.merge(multi_manager_depts, department_df, on='department_id', how='inner')
result_list = result_df[['name', 'num_employees']].drop_duplicates().values.tolist()
# 최종 결과는 다음 형식이어야 합니다.
result = [[str(item) for item in row] for row in result_list]
NLQ: 등록 과정 301의 가장 최근 학생 ID는 무엇입니까?
# 데이터: student_course_attendance (student_id, course_id, date_of_attendance)
# SQL:
# SELECT student_id FROM student_course_attendance
# WHERE course_id = 301
# ORDER BY date_of_attendance DESC LIMIT 1
# Pandas 코드:
import pandas as pd
# 가정: course_attendance_df는 이미 로드된 Pandas DataFrame입니다.
# 'course_id'가 301인 기록을 필터링합니다.
course_301_records = course_attendance_df[
course_attendance_df['course_id'] == 301
].copy() # SettingWithCopyWarning을 피하기 위해 copy() 사용
# 'date_of_attendance'를 datetime 형식으로 변환합니다.
course_301_records['date_of_attendance'] = pd.to_datetime(
course_301_records['date_of_attendance']
)
# 날짜를 기준으로 내림차순 정렬합니다.
sorted_attendance = course_301_records.sort_values(
by='date_of_attendance',
ascending=False
)
# 가장 최근 기록의 'student_id'를 가져옵니다.
if not sorted_attendance.empty:
latest_student_id = sorted_attendance.iloc[0]['student_id']
result = [[str(latest_student_id)]]
else:
result = [[]] # 결과가 없는 경우
NLQ: 각 우편번호에 대해 '0'으로 시작하는 모든 날짜의 평균 온도는 얼마입니까?
# 데이터: weather (zip_code, date, mean_temperature_f)
# SQL:
# SELECT zip_code, avg(mean_temperature_f)
# FROM weather
# WHERE date LIKE '0/%'
# GROUP BY zip_code
# Pandas 코드:
import pandas as pd
# 가정: weather_data_df는 이미 로드된 Pandas DataFrame입니다.
# 'date' 열의 값이 '0/'으로 시작하는 행을 필터링합니다.
filtered_by_date = weather_data_df[
weather_data_df['date'].astype(str).str.startswith('0/')
]
# 'zip_code'별로 그룹화하고 'mean_temperature_f'의 평균을 계산합니다.
avg_temp_by_zip = filtered_by_date.groupby('zip_code')[
'mean_temperature_f'
].mean().reset_index()
# 열 이름을 변경하여 SQL 쿼리 결과와 유사하게 만듭니다.
avg_temp_by_zip.columns = [
'zip_code',
'average_mean_temperature_f'
]
result = avg_temp_by_zip.values.tolist()
result = [[str(item) for item in row] for row in result]
NLQ: 어떤 부서가 동시에 두 명 이상의 담당자를 가집니까? ID, 이름, 담당자 수를 나열하세요.
# 데이터: department (department_id, name), management (department_id, manager_id)
# SQL:
# SELECT T1.department_id, T1.name, count(*)
# FROM management AS T2 JOIN department AS T1
# ON T1.department_id = T2.department_id
# GROUP BY T1.department_id
# HAVING count(*) > 1
# Pandas 코드:
import pandas as pd
# 가정: dept_info_df, manager_assignments_df는 이미 로드된 Pandas DataFrame입니다.
# 부서 정보와 관리자 할당을 'department_id'로 병합합니다.
combined_data = pd.merge(
dept_info_df, manager_assignments_df,
on='department_id',
how='inner'
)
# 'department_id'와 'name'으로 그룹화하고 각 그룹의 크기(담당자 수)를 계산합니다.
manager_counts = combined_data.groupby(
['department_id', 'name']
).size().reset_index(name='manager_count')
# 담당자 수가 1보다 큰 부서만 필터링합니다.
multiple_managers_depts = manager_counts[manager_counts['manager_count'] > 1]
# 결과 열을 추출하고 리스트로 변환합니다.
result = multiple_managers_depts[['department_id', 'name', 'manager_count']].values.tolist()
result = [[str(item) for item in row] for row in result]
NLQ: Palo Alto에 없는 역의 자전거 이용 가능 평균값은 얼마입니까?
# 데이터: status (station_id, bikes_available), station (id, city)
# SQL:
# SELECT avg(bikes_available)
# FROM status
# WHERE station_id NOT IN (
# SELECT id FROM station WHERE city = "Palo_Alto"
# )
# Pandas 코드:
import pandas as pd
# 가정: station_status_df, station_details_df는 이미 로드된 Pandas DataFrame입니다.
# Palo Alto에 있는 역의 ID를 찾습니다.
palo_alto_station_ids = station_details_df[
station_details_df['city'] == 'Palo_Alto'
]['id'].unique()
# Palo Alto에 없는 역의 상태를 필터링합니다.
non_palo_alto_status = station_status_df[
~station_status_df['station_id'].isin(palo_alto_station_ids)
]
# 필터링된 역들의 'bikes_available' 평균을 계산합니다.
average_bikes_available = non_palo_alto_status['bikes_available'].mean()
# 결과 형식에 맞게 변환합니다.
result = [[str(average_bikes_available)]]
A.2 KGQA 예시
다음은 SPARQL 쿼리를 동등한 Pandas 코드로 변환한 예시입니다. 다중 홉 쿼리, 카운트, 유형별 필터링, argmax/argmin 찾기 등을 다룹니다.
NLQ: Servigroup Papa Luna 호텔에 인증을 부여한 호텔 등급 기관은 어디입니까?
# 데이터: hotel_grade (hotel, awarded_by)
# SPARQL (원문에서 너무 길고 복잡하여 생략)
# Python 코드:
import pandas as pd
# 가정: hotel_grades_df는 이미 로드된 Pandas DataFrame입니다.
# 'hotel' 열에서 'Servigroup_Papa_Luna_Hotel'을 찾습니다.
# 해당 호텔에 등급을 부여한 기관들을 'awarded_by' 열에서 가져옵니다.
grading_authorities = hotel_grades_df[
hotel_grades_df['hotel'] == 'Servigroup_Papa_Luna_Hotel'
]['awarded_by'].unique().tolist()
# 결과를 리스트 오브 리스트 형식으로 변환합니다.
result = [[str(auth)] for auth in grading_authorities]
NLQ: 2011년 일본의 총리는 누구였습니까?
# 데이터: governmental_jurisdiction (governing_officials, jurisdiction_name),
# government_position_held (office_holder, basic_title, from, to)
# SPARQL (원문에서 너무 길고 복잡하여 생략)
# Python 코드:
import pandas as pd
# 가정: gov_jurisdiction_df, gov_position_df는 이미 로드된 Pandas DataFrame입니다.
# 두 DataFrame을 'governing_officials'와 'government_position_held' (가상의 연결 키)로 병합합니다.
# 실제 Freebase KG 스키마를 반영하여 논리적으로 연결.
# 여기서는 편의상 gov_jurisdiction_df가 'governing_official_id'를 가지고 있고,
# gov_position_df도 'governing_official_id'를 가진다고 가정합니다.
# 실제 스키마에 따라 병합 키는 달라질 수 있습니다.
combined_gov_data = pd.merge(
gov_jurisdiction_df,
gov_position_df,
left_on='governing_official_id', # 예시 키
right_on='governing_official_id', # 예시 키
how='inner'
)
# 2011년 기간에 해당하고, 'Japan'의 'Prime_minister'인 경우를 필터링합니다.
# 날짜 형식은 YYYY-MM-DD로 가정합니다.
filtered_prime_ministers = combined_gov_data.loc[
(combined_gov_data['jurisdiction_name'] == 'Japan') &
(combined_gov_data['basic_title'] == 'Prime_minister') &
(pd.to_datetime(combined_gov_data['from']) <= '2011-12-31') &
(pd.to_datetime(combined_gov_data['to']) >= '2011-01-01')
]
# 'office_holder'를 추출하고 중복을 제거한 후 리스트로 변환합니다.
prime_ministers_2011 = filtered_prime_ministers['office_holder'].unique().tolist()
# 결과를 리스트 오브 리스트 형식으로 변환합니다.
result = [[str(pm)] for pm in prime_ministers_2011]
NLQ: 러시아와 인접한 국가는 어디입니까?
# 데이터: location (location_name, adjoins_id), adjoining_relationship (adjoins_id, adjoins_country)
# SPARQL (원문에서 너무 길고 복잡하여 생략)
# Python 코드:
import pandas as pd
# 가정: locations_df, adj_relations_df는 이미 로드된 Pandas DataFrame입니다.
# 위치 정보와 인접 관계를 병합합니다.
# 여기서는 locations_df가 'adjoins_id'를 포함한다고 가정합니다.
merged_location_data = pd.merge(
locations_df,
adj_relations_df,
left_on='adjoins_id', # 예시 키
right_on='adjoins_id', # 예시 키
how='inner'
)
# 'location_name'이 'Russia'인 행을 필터링합니다.
russian_borders = merged_location_data.loc[
merged_location_data['location_name'] == 'Russia'
]
# 러시아가 인접한 국가 목록을 가져오고 중복을 제거합니다.
adjacent_countries = russian_borders['adjoins_country'].unique().tolist()
# 자기 자신(러시아)은 목록에서 제외합니다.
adjacent_countries = [
country for country in adjacent_countries if country != 'Russia'
]
# 결과를 리스트 오브 리스트 형식으로 변환합니다.
result = [[str(country)] for country in adjacent_countries]
NLQ: 유대인 민족인 종교 지도자는 몇 명입니까?
# 데이터: religious_leader (leader_id, ethnicity), person (person_id, ethnicity)
# SPARQL (원문에서 너무 길고 복잡하여 생략)
# Python 코드:
import pandas as pd
# 가정: religious_leaders_df, person_info_df는 이미 로드된 Pandas DataFrame입니다.
# 'ethnicity'가 'jew'인 사람들을 필터링합니다.
jewish_people = person_info_df[
person_info_df['ethnicity'] == 'jew'
]['person_id'].unique()
# 'religious_leaders_df'에서 'leader_id'가 jewish_people에 속하는 경우를 찾습니다.
# 여기서는 religious_leaders_df에 'person_id'와 같은 식별자가 있다고 가정합니다.
jewish_religious_leaders = religious_leaders_df[
religious_leaders_df['leader_id'].isin(jewish_people)
]
# 수를 계산합니다.
count = len(jewish_religious_leaders)
# 결과를 리스트 오브 리스트 형식으로 변환합니다.
result = [[str(count)]]
A.3 테이블 질의 응답 예시
NLQ: Brown이 어느 카운티에서 가장 많은 표를 얻었습니까?
# 데이터: df (County, Votes, Candidate)
# Python 코드:
import pandas as pd
# 가정: df_election_results는 이미 로드된 Pandas DataFrame입니다.
# 'Candidate'가 'Brown'인 행을 필터링합니다.
brown_candidate_results = df_election_results[
df_election_results['Candidate'] == 'Brown'
].copy()
# 'Votes' 열에서 쉼표를 제거하고 정수형으로 변환합니다.
brown_candidate_results['Clean_Votes'] = brown_candidate_results['Votes'].str.replace(',', '').astype(int)
# Clean_Votes가 가장 높은 행의 인덱스를 찾습니다.
max_votes_idx = brown_candidate_results['Clean_Votes'].idxmax()
# 해당 인덱스의 'County' 값을 가져옵니다.
county_with_max_votes = brown_candidate_results.loc[max_votes_idx, 'County']
# 결과를 리스트 오브 리스트 형식으로 변환합니다.
result = [[str(county_with_max_votes)]]
NLQ: 미국 팀에서 최고 순위의 드라이버는 누구입니까?
# 데이터: df (Pos, Driver, Team)
# Python 코드:
import pandas as pd
# 가정: df_race_results는 이미 로드된 Pandas DataFrame입니다.
# 'Pos' 열을 숫자형으로 변환하고, 오류 발생 시 NaN으로 처리합니다.
df_race_results['Pos_Numeric'] = pd.to_numeric(
df_race_results['Pos'], errors='coerce'
)
# 'Team'이 'USA'인 드라이버를 필터링하고, 유효한 순위를 가진 드라이버만 선택합니다.
usa_team_drivers = df_race_results[
(df_race_results['Team'] == 'USA') &
(df_race_results['Pos_Numeric'].notna())
]
# 'Pos_Numeric'을 기준으로 오름차순 정렬하여 가장 낮은 순위(최고 순위) 드라이버를 찾습니다.
top_usa_driver = usa_team_drivers.nsmallest(1, 'Pos_Numeric')['Driver'].tolist()
# 결과를 리스트 오브 리스트 형식으로 변환합니다.
result = [[str(driver_name)] for driver_name in top_usa_driver]
NLQ: 인도가 전체 대회에서 총 몇 점을 득점했습니까?
# 데이터: df (Results, Team) (Results 예시: '1-0', 'Draw', '0-0')
# Python 코드:
import pandas as pd
# 가정: df_competition_data는 이미 로드된 Pandas DataFrame입니다.
# 'Team' 열이 'India'인 행을 필터링합니다.
india_matches = df_competition_data[
df_competition_data['Team'] == 'India'
].copy()
# 'Results' 열에서 NaN 값을 제거합니다.
valid_results = india_matches['Results'].dropna()
# 각 결과 문자열에서 점수를 추출합니다 (예: 'X-Y'에서 X와 Y).
# 'Draw'와 같은 경우는 0-0으로 간주합니다.
scores_extracted = valid_results.str.extract(r'(\d+)\s*-\s*(\d+)')
# 추출된 점수가 없는(예: Draw) 경우를 0으로 채우고 정수형으로 변환합니다.
# 첫 번째 그룹이 자국 점수, 두 번째 그룹이 상대방 점수라고 가정합니다.
# 문제에서 "인도가 전체 대회에서 총 몇 점을 득점했습니까?"는 인도가 득점한 총 점수를 의미합니다.
# 'X-Y'에서 X가 인도의 점수라고 가정합니다.
india_score_column = scores_extracted[0].fillna(0).astype(int)
# 인도의 총 득점을 계산합니다.
total_points_for_india = india_score_column.sum()
# 결과를 리스트 오브 리스트 형식으로 변환합니다.
result = [[str(total_points_for_india)]]
NLQ: 그들은 경기에서 이기기 전에 얼마나 오랫동안 플레이했습니까?
# 데이터: df (Date, Result)
# Python 코드:
import pandas as pd
# 가정: df_game_log는 이미 로드된 Pandas DataFrame입니다.
# 'Date' 열을 datetime 형식으로 변환합니다.
df_game_log['Date'] = pd.to_datetime(df_game_log['Date'])
# 첫 경기의 날짜를 가져옵니다.
first_game_date = df_game_log['Date'].min()
# 첫 승리 경기를 찾습니다.
wins = df_game_log[df_game_log['Result'].astype(str).str.startswith('W')]
if not wins.empty:
# 첫 승리 경기의 날짜를 가져옵니다.
first_win_date = wins['Date'].min()
# 첫 승리까지의 일수 차이를 계산합니다.
days_to_first_win = (first_win_date - first_game_date).days
else:
# 승리가 없는 경우 0일로 간주합니다.
days_to_first_win = 0
# 결과를 리스트 오브 리스트 형식으로 변환합니다.
result = [[str(days_to_first_win)]]
NLQ: 5위와 10위 참가자 간의 시간 차이는 얼마입니까?
# 데이터: df (Rank, Time)
# Python 코드:
import pandas as pd
# 가정: df_competition_times는 이미 로드된 Pandas DataFrame입니다.
# 'Rank' 열을 정수형으로 변환합니다.
df_competition_times['Rank_Numeric'] = pd.to_numeric(
df_competition_times['Rank'], errors='coerce'
)
# 5위와 10위 참가자의 시간을 찾습니다.
time_5th_place = df_competition_times[
df_competition_times['Rank_Numeric'] == 5
]['Time'].iloc[0] if not df_competition_times[df_competition_times['Rank_Numeric'] == 5].empty else None
time_10th_place = df_competition_times[
df_competition_times['Rank_Numeric'] == 10
]['Time'].iloc[0] if not df_competition_times[df_competition_times['Rank_Numeric'] == 10].empty else None
def time_str_to_seconds(time_str):
"""'MM:SS.ms' 또는 'MM:SS' 형식의 시간 문자열을 초 단위로 변환합니다."""
if not isinstance(time_str, str):
return None
parts = time_str.split(':')
minutes = int(parts[0])
seconds = float(parts[1])
return minutes * 60 + seconds
if time_5th_place is None or time_10th_place is None:
result = [["Times not found for specified ranks."]]
else:
seconds_5th = time_str_to_seconds(time_5th_place)
seconds_10th = time_str_to_seconds(time_10th_place)
if seconds_5th is not None and seconds_10th is not None:
difference_seconds = abs(seconds_5th - seconds_10th)
result = [[str(difference_seconds)]]
else:
result = [["Error converting times to seconds."]]
NLQ: 카우보이즈는 1970년대에 NFC 챔피언십에서 몇 번 우승했습니까?
# 데이터: df (Team, Season, Championship_Type)
# Python 코드:
import pandas as pd
# 가정: df_championship_data는 이미 로드된 Pandas DataFrame입니다.
# 'Team'이 'Dallas_Cowboys'이고 'Championship_Type'이 'NFC'인 경우를 필터링합니다.
cowboys_nfc_championships = df_championship_data[
(df_championship_data['Team'] == 'Dallas_Cowboys') &
(df_championship_data['Championship_Type'] == 'NFC')
]
# 1970년대 (1970년부터 1979년까지) 시즌을 필터링합니다.
championships_70s = cowboys_nfc_championships[
(cowboys_nfc_championships['Season'] >= 1970) &
(cowboys_nfc_championships['Season'] < 1980)
]
# 우승 횟수를 계산합니다.
number_of_wins = championships_70s.shape[0]
# 결과를 리스트 오브 리스트 형식으로 변환합니다.
result = [[str(number_of_wins)]]
NLQ: 학사 학위만 가진 유일한 관리자는 누구입니까?
# 데이터: df (Name, Educational_Background)
# Python 코드:
import pandas as pd
# 가정: df_employees는 이미 로드된 Pandas DataFrame입니다.
# 'Educational_Background'에서 'B.S.'를 포함하지만, 'Ph.D', 'M.S', 'M.A', 'Master'를 포함하지 않는 행을 필터링합니다.
filtered_by_degree = df_employees[
df_employees['Educational_Background'].astype(str).str.contains('B.S.') &
~df_employees['Educational_Background'].astype(str).str.contains('Ph.D|M.S|M.A|Master')
]
# 필터링된 결과가 비어 있지 않으면, 첫 번째 'Name'을 가져옵니다.
if not filtered_by_degree.empty:
unique_bs_holder = filtered_by_degree['Name'].iloc[0]
result = [[str(unique_bs_holder)]]
else:
result = [[]] # 결과가 없는 경우
부록 B: BOX 구축 세부 사항
B.1 테이블을 BOX로 변환
알고리즘 1은 테이블을 BOX로 변환하는 과정을 요약합니다. 테이블 이름이 제공되지 않으면 Pandas 코드에서 BOX의 변수 이름으로 "Table"을 사용합니다.
알고리즘 1: 테이블-BOX 변환
입력: 데이터 테이블 $\mathcal{T} = (\{c_i\}_{i=1}^{C}, \{r_j\}_{j=1}^{R}, \{v_{i, j}\}_{i=1, j=1}^{C, R})$
여기서 각 행 $r_j$는 데이터 레코드를 나타내고, $v_{i, j}$는 내용을 나타냅니다.
출력: BOX $\mathcal{B}$
1: 함수 `TransformTableToBOX($\mathcal{T}$)`:
2: `box_fields` $\leftarrow \emptyset$ // BOX 필드 집합 초기화
3: `box_values` $\leftarrow \emptyset$ // BOX 값 집합 초기화
4:
5: // 각 열을 필드로 처리
6: For $i = 1$ to $C$ do:
7: `box_fields` $\leftarrow$ `box_fields` $\cup \{c_i\}$
8: End For
9:
10: // 각 셀 내용을 필드 값으로 처리
11: For $i = 1$ to $C$ do:
12: For $j = 1$ to $R$ do:
13: `box_values` $\leftarrow$ `box_values` $\cup \{v_{i, j}\}$
14: End For
15: End For
16:
17: // BOX 이름 설정
18: If $\mathcal{T}$에 테이블 이름 $t$가 있으면:
19: $\mathcal{B} \leftarrow (t, \text{box_fields}, \text{box_values})$
20: Else:
21: $\mathcal{B} \leftarrow (\text{Table}, \text{box_fields}, \text{box_values})$
22: End If
23:
24: Return $\mathcal{B}$
25: End 함수
26: $\mathcal{B} = \text{TransformTableToBOX}(\mathcal{T})$
27: Return $\mathcal{B}$
B.2 데이터베이스를 BOX로 변환
알고리즘 2는 데이터베이스를 BOX 집합으로 변환하는 과정을 요약합니다. 여기서 함수 TableToBOX는 알고리즘 1에 설명된 테이블을 단일 BOX로 변환하는 과정을 나타냅니다. 또한, 데이터베이스의 외래 키 정보가 유지됩니다.
알고리즘 2: 데이터베이스-BOX 변환
입력: 데이터베이스 $\mathcal{D} = \{\mathcal{T}_1, \mathcal{T}_2, \ldots, \mathcal{T}_T\}$, 여기서 $\mathcal{T}_i$는 테이블을 나타냅니다.
출력: BOX 집합 $\mathcal{B}^*$
1: `box_set` $\leftarrow \emptyset$ // BOX 집합 초기화
2:
3: For $i = 1$ to $T$ do:
4: $\mathcal{B}_i = \text{TransformTableToBOX}(\mathcal{T}_i)$ // 알고리즘 1에 따라 각 테이블에 대한 BOX 생성
5: `box_set` $\leftarrow$ `box_set` $\cup \{\mathcal{B}_i\}$
6: End For
7:
8: Return `box_set`
B.3 지식 그래프를 BOX로 변환
알고리즘 3은 KG를 BOX 집합으로 변환하는 과정을 상세히 설명합니다. `DEPTHFIRSTSEARCH` 함수는 주제 개체의 $H$-홉 서브그래프에서 필드 기록(즉, 관련 삼중항)을 검색하는 것을 목표로 합니다.
알고리즘 3: 지식 그래프-BOX 변환
입력: 지식 그래프 $\mathcal{K}=\{\langle s, p, o\rangle \mid s \in \mathcal{E}, p \in \mathcal{R}, o \in \mathcal{E} \cup \Gamma\}$,
여기서 $\mathcal{E}, \mathcal{R}, \Gamma$는 각각 개체, 관계, 유형 집합을 나타냅니다.
주제 개체 집합 $\mathcal{E}^* \subset \mathcal{E}$.
관련 관계 집합 $\mathcal{R}^* \subset \mathcal{R}$.
$H$: 홉 수.
출력: BOX 집합 $\mathcal{B}^*$
1: `result_boxes` $\leftarrow \emptyset$ // BOX 집합 초기화
2: `field_records_list` $\leftarrow []$ // 필드 기록 목록
3: `visited_entities` $\leftarrow \emptyset$ // 방문한 개체 집합
4: 함수 `PerformDFS($e_c$, current_path, hop_limit)`:
5: If `len(current_path)` $= 2 \times \text{hop_limit}$ then:
6: `field_records_list`.append(`current_path`)
7: Return
8: End If
9:
10: For $r \in \mathcal{R}^*$ do:
11: `neighbors_out` $\leftarrow$ `GetNeighborEntities`($e_c$, $r$, 'out') // $e_c$에서 $r$을 통해 나가는 개체
12: `neighbors_in` $\leftarrow$ `GetNeighborEntities`($e_c$, $r$, 'in') // $r$을 통해 $e_c$로 들어오는 개체
13:
14: For $e_{out} \in \text{neighbors_out}$ do: // $e_c$에서 $r$을 통해 시작하는 1-홉 이웃 개체 순회
15: If $e_{out} \notin \text{visited_entities}$ then: // 가지치기: 동일한 개체를 통해 순환 방지
16: `current_path`.append((`Type`($e_c$), `Type`($e_c$), $e_c$))
17: `current_path`.append((`Type`($e_c$), $r$, $e_{out}$))
18: `visited_entities` $\leftarrow$ `visited_entities` $\cup \{e_{out}\}$
19: `PerformDFS`($e_{out}$, `current_path`, `hop_limit`)
20: `current_path`.pop() // 백트래킹
21: `current_path`.pop() // 백트래킹
22: `visited_entities` $\leftarrow$ `visited_entities` $\setminus \{e_{out}\}$
23: End If
24: End For
25:
26: For $e_{in} \in \text{neighbors_in}$ do: // $r$을 통해 $e_c$에서 끝나는 1-홉 이웃 개체 순회
27: If $e_{in} \notin \text{visited_entities}$ then: // 가지치기: 동일한 개체를 통해 순환 방지
28: `current_path`.append((`Type`($e_{in}$), `Type`($e_{in}$), $e_{in}$))
29: `current_path`.append((`Type`($e_{in}$), $r$, $e_c$))
30: `visited_entities` $\leftarrow$ `visited_entities` $\cup \{e_{in}\}$
31: `PerformDFS`($e_{in}$, `current_path`, `hop_limit`)
32: `current_path`.pop() // 백트래킹
33: `current_path`.pop() // 백트래킹
34: `visited_entities` $\leftarrow$ `visited_entities` $\setminus \{e_{in}\}$
35: End If
36: End For
37: End For
38: End 함수
39: // 각 주제 개체에 대해 DFS 시작
40: For $e \in \mathcal{E}^*$ do:
41: `PerformDFS`($e$, [], $H$)
42: End For
43: // 필드 기록을 기반으로 BOX 구축 (먼저 필드만 추가)
44: `box_field_sets` $\leftarrow$ Empty Dictionary // {box_name: {field_name1, field_name2, ...}}
45: For `record_path` $\in$ `field_records_list` do:
46: For ($box_name$, $field_name$, $value$) $\in$ `record_path` do:
47: If $box_name \notin \text{box_field_sets}$ then:
48: `box_field_sets`[$box_name$] $\leftarrow \emptyset$
49: End If
50: `box_field_sets`[$box_name$] $\leftarrow$ `box_field_sets`[$box_name$] $\cup \{field_name\}$
51: End For
52: End For
53: // 필드 기록을 기반으로 BOX 구축 (값 추가 및 정렬)
54: `constructed_boxes` $\leftarrow$ Empty Dictionary // {box_name: {field_name: [value1, value2, ...]}, ...}
55: For $box_name, fields \in \text{box_field_sets.items()}$ do:
56: `constructed_boxes`[$box_name$] $\leftarrow$ {f: [] for f in fields}
57: End For
58: // 각 기록 경로를 순회하며 BOX에 값 채우기
59: For `record_path` $\in$ `field_records_list` do:
60: `current_record_values` $\leftarrow$ {f: "NA" for f in `box_field_sets`[`record_path`[0][0]]} // 현재 레코드의 모든 필드에 NA 초기화
61: For ($box_name$, $field_name$, $value$) $\in$ `record_path` do:
62: `current_record_values`[$field_name$] $\leftarrow value$
63: End For
64: // BOX에 현재 레코드 추가 (모든 필드가 같은 길이를 유지하도록)
65: For $field \in \text{box_field_sets}$[`record_path`[0][0]] do:
66: `constructed_boxes`[`record_path`[0][0]][$field$].append(`current_record_values`[$field$])
67: End For
68: End For
69: // 최종 BOX 객체 생성
70: For $box_name, field_values \in \text{constructed_boxes.items()}$ do:
71: `result_boxes`.append(BOX($box_name$, `field_values.keys()`, `field_values.values()`))
72: End For
73: Return `result_boxes`
부록 C: 프롬프트 템플릿
C.1 컨텍스트 추론 프롬프트
컨텍스트 추론 프롬프트는 LLM이 주어진 NLQ와 구조화 지식 스키마를 바탕으로 Pandas 코드를 생성하도록 안내합니다. 이 프롬프트는 LLM이 먼저 추론 단계를 자연어로 설명한 다음 실행 가능한 Python 코드를 생성하도록 유도하는 역할을 합니다. 여기에는 LLM이 Pandas API를 활용하여 복잡한 쿼리를 해결하는 방법을 학습할 수 있도록 몇 가지 훈련 예제(데모)가 포함됩니다. 프롬프트는 명확한 지침, 데이터베이스 스키마 정보, 외래 키 정보, 그리고 질문으로 구성됩니다. LLM은 단계별 추론 과정과 최종 Pandas 코드를 JSON 형식으로 출력하도록 요청받습니다. 이 JSON 출력은 추론 과정에 대한 설명과 함께 Pandas DataFrame 스키마를 제외한 순수 Python 코드만을 포함해야 합니다. 또한, 최종 결과는 항상 result: List[List[str]] 변수에 저장되어야 하며, 단일 개체, 여러 개체, 또는 숫자 값 등 다양한 유형의 답변을 처리할 수 있도록 각 답변 항목이 별도의 리스트 안에 포함된 중첩 리스트 형식이어야 합니다.
C.2 실행 가이드 프롬프트
실행 가이드 프롬프트는 이전 쿼리가 실패했거나 올바르지 않은 결과를 반환했을 때 사용됩니다. 이 프롬프트는 LLM에게 이전에 생성된 코드의 실행 결과와 발생한 오류 메시지(예: 구문 오류, 필드 참조 오류, 논리적 오류)를 제공합니다. LLM은 데이터베이스 스키마, 원래 질문, 그리고 실패한 실행의 세부 정보를 분석하여 오류의 근본 원인을 파악하고, 이를 기반으로 코드를 수정해야 합니다. 수정된 코드는 문제의 의도에 부합하며 올바른 데이터 형식을 따르고, 필요한 모든 정보를 정확히 검색해야 합니다. 응답은 JSON 형식으로 제공되며, 오류 분석, 수정된 추론 과정, 그리고 수정된 Python 코드를 포함합니다.