GIST/Artificial General Intelligence

[AGI] Program Synthesis - 4

bengal3636 2025. 4. 3. 19:03

프로그램 작성자는 specification과 프로그램을 연결하는 역할이다. 이번에는 어떻게 LLM이 다른 분야에서 보편적인 프로그램 작성자가 되는지를 설명할 것이다.

a menagerie of synthesizers

synthesizer는 매우 어려운 일을 수행한다. 그것은 프로그램이 무엇을 해야하는지(semantics)를 입력으로 받아 프로그램이 어떻게 생겨야 하는지(syntax)를 출력으로 만드는 작업이다. 즉, 인터프리터를 거꾸로 실행하는 작업이다.

어려운 문제가 주어졌을 때, 모든 문제 도메인마다 도메인 맞춤 synthesizer가 있어야하는 것은 당연하다고 생각이 들 수 있습니다. 실제로 그런 경우가 있는데 아래는 글 작성자의 작업물의 구조들입니다.

저것들은 좋아보이지만, 아래와 같은 상당한 수작업 엔지니어링 노력과 도메인 전문 지식이 필요했다:

  1. 작업은 간결하게 프로그래밍 DSL로 표현되어야 한다
  2. synthesizer는 specification을 올바르게 해석해야 한다
  3. synthesizer는 유효한 프로그램을 안정적으로 생성해야 한다
  4. synthesizer는 훈련을 통해 meaning matrix M의 구조를 파악하고 일반화할 수 있어야 한다

결국 이 모든 것은 DSL, interpreter, specification, synthesizer간의 공동 진화를 유발한다. 다들 연결되어 있기 때문이다.


Large Language Models

large language model(llm)은 문자열의 분포를 제공한다. 어마한 양의 텍스트 데이터를 학습한 후 놀라운 조건부 생성이 가능하다. 아래는 openai-codex 모델의 예시이다.

llm의 기능과 한계는 (2022-8월 기준) 여전히 연구되는 중이다. 


llm for synthesis?

이것은 llm이 왜 program synthesis에 유용한지에 대한 글 작성자의 평가이다. 이를 뒷받침하기 위해 openai codex 모델을 사용한 프롬프트 기반 상호작용 예시를 보여줄 것이다.

 

llm can pick-up programmatic patterns

프로그램은 생성과 해석에 명확한 규칙이 적용되는, 형식적이고 패턴화된 텍스트이다. LLM은 이러한 프로그래밍적 패턴을 자연스럽게 학습하고 파악하는 성향을 보이는 듯하다.

llm has basic conventional knowledge of human language

llm은 인터넷의 방대한 양의 텍스트를 통해 사람의 관습에 대한 일정 수준의 지식을 가지고 있다.

llm are reliable in generating syntactically complex programs

program synthesis의 실무자들은 일관성 없는 자유 텍스트 생성을 피해왔다. 그러나 llm은 문체가 일관된 텍스트를 생성하는데에도 매우 능숙하다.

llm operates over text - a universal datatype

specification과 프로그램은 텍스트로 쉽게 표현되며, 이로 인하여 기존의 프로그래밍 시스템과 program synthesis를 쉽게 통합할 수 있고 여러 DSL과 specification language를 실험적으로 바꿔가며 반복할 수 있다(별도의 parser가 필요 없다).


where prompting falls short

우리의 사각형 예시를 보자. 프롬프트만으로는 주어진 spec에 대해 유효한 사각형을 생성할 수 없다. 사실상 우리가 해결하고자 하는 문제에 llm + prompting 조합은 실패할 때가 매우 많다. llm이 학습한 인터넷 텍스트 데이터가 우리의 과제와 너무나도 다르기 때문이다.


fine-tuning large language models for synthesis

모델을 fine-tuning하는 것은 이미 합리적인 가중치(reasonable weights)를 가진 기존 모델을 바탕으로 특정한 데이터셋에 대해 학습을 이어가는 것을 의미한다. synthesis 문제에서는 영어와 파이썬에 대해 사전 학습된 llm을 기반으로 우리 도메인(ex. 사각형 문제)의 meaning matrix M에서 샘플링된 데이터셋 D에 대해 추가학습을 진행한다.


the model and the tokenizer

모델은 id의 시퀀스로 운영되며 문자열과 토큰을 번역해주는 tokenizer를 필요로 한다.

우리는 가벼운 모델 byt5-base 모델을 fine-tuning 할 것이다. 이 모델은 각각의 문자를 하나의 토큰으로 처리하며 총 256개의 토큰 ID를 사용한다. 이번 작업에서는 충분한 양의 in-domain data를 제공하기 때문에 거대한 모델이 반드시 필요한 것은 아니다. 빠르게 모델과 tokenizer를 불러오는 것은 간단하게 수행할 수 있다.

!pip install transformers datasets
from transformers import T5ForConditionalGeneration, AutoTokenizer, TrainingArguments
tokenizer = AutoTokenizer.from_pretrained('google/byt5-base')
model = T5ForConditionalGeneration.from_pretrained('google/byt5-base')

data

3장이랑 똑같은 데이터셋 D를 사용할 것이다.

# upload the following 2 files onto the colab vm instance
from rectangle import *
from rectangle_lm import *
import random

D_train = sample_D(5000)
D_test = sample_D(1000)

encoding the spec and prog

인코딩은 독창성을 요구한다. 너는 모델이 올바른 결정을 내릴 수 있도록 도와주는 정보를 강조하고 싶을 것이다. transfer 알고리즘의 시간 복잡도는 O(n^2)이고 문자열의 길이에 많은 영향을 받는다. 그렇기에 너는 spec 표현의 길이를 최소화하고 싶다.

 

이것을 마음에 가지고 토큰의 이름을 변경할 때 드는 비용을 생각해보라. 만약 너가 'blue'를 '+'로 바꾸었다고 생각해보자. 이렇게 하면 모델이 해석하는 능력은 떨어질 수 있다. 물론, fine-tuning 예제가 충분히 많다면 이러한 변경은 큰 문제가 되지 않는다.(program synthesis는 너가 원하는 만큼 (prog, spec) 쌍을 만들 수 있어서 괜찮다).

def spec_to_str(spec):
    return repr(spec).replace('True','+').replace('False','-').replace(')','').replace('(','').replace(' ','').replace(',','')

# before, length 261
[((1, 1), True), ((5, 0), False), ((3, 4), True), ((4, 5), False), ((0, 4), True), ((2, 3), True), ((3, 3), True), ((4, 5), False), ((0, 3), True), ((4, 3), True), ((0, 0), True), ((5, 2), False), ((4, 1), True), ((1, 2), True), ((5, 5), False), ((0, 3), True)]
# after, length 50
[11+50-34+45-04+23+33+45-03+43+00+52-41+12+55-03+]

minibatch to tensor with collator

collator는 서로 길이가 다른 입력-출력 쌍의 batch를 받아서 모델 학습에 사용할 수 있도록 직사각형 형태의 tensor로 패딩하는 역할을 한다.

class Collator:
  def __init__(self, tokenizer):
    self.tokenizer = tokenizer

  def __call__(self, batch):
    # entry[1] is the spec, need to call repr to turn it into a string. entry[0] is the prog_str already
    ret = {"input_ids": self.tokenizer([spec_to_str(entry[1]) for entry in batch], padding=True, return_tensors='pt').input_ids, 
            "labels": self.tokenizer([entry[0] for entry in batch], padding=True, return_tensors='pt').input_ids}
    return ret

training, and interpreting the training loss

huggingface API를 이용하여 훈련하는 것은 굉장히 간단하며, Seq2Seq 훈련 클래스를 호출하면 된다.

trainer = Seq2SeqTrainer(model=model,args = training_args,train_dataset=dataset,eval_dataset=None,tokenizer=tokenizer,compute_metrics=None,data_collator=Collator(tokenizer))
trainer.train()

 

모델의 학습이 끝나면 loss를 확인할 수 있다.

이 loss는 얼마나 모델의 분포가 훈련 데이터에 유사한지를 나타내며, 작은 loss는 더 유사하다는 뜻이다. 정량적으로 loss는 per-token-perplexity로 해석될 수 있다. 이것에 대해서는 예시를 통해 설명할 것이다.

 

우리가 훈련 data-point ("11+33+04-41-", "[1,3,1,4]")와 500번 반복에서 1.18의 loss가 있다고 가정하자. "11+33+04-41-"를 입력으로 주었을 때, 각 토큰에 있는 [1, 3, 1, 3]가 e^1.18 = 3.25의 perplexity를 가지고 있다는 것이다. 즉, 각 토큰이 1/3.25 확률로 올바른 생성을 한다는 것이다. [1, 3, 1, 4]는 총 9개의 토큰이고 우리의 모델은 한 번에 1 개의 토큰을 만들기 때문에 총 시퀀스는 1 / 3.25^9 = 1 / 40453의 확률로 만들어질 것이다.

 

1000번을 반복했을 때는 0.5의 loss를 가지고 있고, 각 토큰의 복잡도는 e^0.5 = 1.64이다. 그러므로 우리의 모델은 1 / 1.64^9 = 1 / 86의 확률로 완전히 올바른 정답을 생성한다. 하지만 주의할 점은 주어진 spec에 대해 가능한 정답 프로그램은 [1, 3, 1, 4] 하나가 아닐 수도 있다. 모델이 다른 정답을 생성할 수도 있기 때문에 1 / 86이라는 수치는 단 하나의 정답을 맞출 확률이며, 전체적으로 보면 정답을 맞출 확률의 lower bound라고 볼 수 있다.


inference

D에서 훈련이 끝난 후(fine-tuning), 우리는 specification을 문자로 바꿔주는 프로그램을 샘플링할 수 있다. 샘플링에 중요한 파라미터는 temperature이며 일반적으로 1.0이 좋은 기준값으로 사용된다. 너의 모델이 ground-truth distribution을 완벽히 학습했다고 믿으면 1.0의 temperature는 그 분포를 잘 반영한 샘플링을 수행한다.  너무 낮은 temperature는 다양성이 줄어들고, 너무 높은 temperature는 결과가 혼란스러워질 수 있다.

def generate_samples_with_temp(txt, n_samples, temp):
    to_tokenizer = [txt for i in range(n_samples)]
    outputs = model.generate(tokenizer(to_tokenizer, return_tensors='pt', padding=True).input_ids.to('cuda'), do_sample=True, max_length=128, temperature = temp)
    results = tokenizer.batch_decode(outputs, skip_special_tokens=True)
    return results

generate_samples_with_temp(repr(D_test[0][1]), 5, 1.0)
# ['[4,6,1,6]', '[1,5,3,5]', '[2,3,4,6]','[4,5,0,2]','[1,4,4,5]']
generate_samples_with_temp(repr(D_test[0][1]), 5, 3.0)
# ['[Tb2,2,6}', 'ee[>1,6,265,741F4]', '[0o9ѕdr_a|4,80,7]6ٓ5]і-$4732"r-H,', '[2˴в[4,4,3', '[A518,2/8žrev0B;D']

 

위의 결과는 좀 놀라울 것이다. fine-tuning 이후 정답처럼 보이는 사각형을 생성하고 있기 때문이다.


making a synthesizer with fine-tuned llm

우리는 단순히 llm_writer를 synthesizer에 통합하면 된다. 학습 과정에서 정답 프로그램을 복원할 확률이 1/86이었다는 것을 고려하면 search budget을 20정도로 설정하는 것은 합리적이다. 물론 현재 기준으로는 llm을 활용한 추론 속도가 다소 느리며, 최신의 program synthesizer들이 요구하는 수만 개의 샘플링에 비해 상대적으로 느리게 느껴질 수 있다. 그러나 이 문제는 몇 년 내에는 크게 걱정하지 않아도 될 것이다. 그냥 조금 기다리면 된다.

def llm_writer(spec):
    # in practice please batch this instead of 1 at a time lol
    return generate_samples_with_temp(spec_to_str(spec), 1, 1.0)[0]
synthesizer6 = get_synthesizer(llm_writer, is_correct, 20)

results

그래프에서 보이듯, 다른 방법들을 가뿐히 이겨버리는 모습을 볼 수 있다. 

https://evanthebouncy.github.io/program-synthesis-minimal/generation-with-llm/

 

fine-tuned llm as program writers

A program-writer is a conduit between specifications and programs. In this post, we will explain how large language models (llm) can be an universal program-writer for different domains.

evanthebouncy.github.io