본문 바로가기
Engineering Log (실습 및 프로젝트)/Local AI Project

Local AI Project 두 번째 이야기 - Gemma 4의 기본 답변을 저장하고 비교 기준 만들기

by 온semi로 2026. 7. 3.
반응형

이번 목표는 QLoRA 학습 하기 전, 답변을 baseline으로 저장해서 나중에 학습 후 결과와 비교해보는게 목적이다.

즉 Gemma 4의 기본 답변을 받아본다.

이후에 내가 QLoRA 학습을 한 후 답변이 얼마나 달라지는지를 볼 것이다.

 

다시 우분투를 키는 것부터 시작한다. 터미널을 키고 우분투를 클릭하면 된다.

 

우분투를 켜고 나선, 내가 설정해놨던 폴더로 간다. cd ~/local-semicon-ai

이후에 pwd 라는 명령어를 쳐 보면 정상적으로 왔는지 확인 가능하다.

 

이제 다시 가상환경을 킬 것이다. source .venv/bin/activate 를 입력한다. (.venv)가 보이면 파이썬 가상환경이 켜진 것이다.

이제 질문 파일을 만들어 넣을 폴더(data, output, scripts) 3개를 생성한다. mkdir -p data outputs scripts

mkdir는 make directory로 폴더를 만드는 명령어이고 -p 명령어는 이미 같은 이름의 폴더가 있으면 그냥 조용히 넘어가고, 없으면 새로 만들라는 옵션이다. 혹여 같은 이름의 폴더가 이미 있을 떄 오류메세지를 내지 않게 만드는 것이다.

  • data: 모델에게 입력할 질문이나 학습 데이터 파일을 저장하는 폴더
  • outputs: 모델이 생성한 답변이나 실험 결과를 저장하는 폴더
  • scripts: Python 실행 코드를 저장하는 폴더

그런 다음 질문 파일들을 생성한다. 원하는 질문을 입력하도록 하자. 난 반도체 관련 질문들을 넣었다.

cat > data/baseline_questions.txt <<'EOF'
PVD 공정에서 step coverage가 나빠지는 이유를 설명해줘.
CVD 공정이 PVD보다 conformality 측면에서 유리한 이유를 설명해줘.
ALD 공정에서 self-limiting reaction이 중요한 이유를 설명해줘.
식각 공정에서 selectivity와 anisotropy의 차이를 설명해줘.
양산 공정에서 chamber pressure drift가 발생했을 때 어떤 데이터를 추가로 확인해야 하는지 설명해줘.
EOF

 

질문파일이 제대로 만들어졌는지에 대한 확인은 cat data/baseline_questions.txt 로 한다. 

적어놨던 질문들이 그대로 출력되면 정상이다.

 

이제 baseline 답변 저장 코드를 만들 것이다. Gemma 4 E4B-it 모델이 baseline 질문 파일을 읽고, 각 질문에 대한 답변을 outputs/baseline_answers.txt 파일에 저장하도록 Python 코드를 작성한다.

 

cat > scripts/save_baseline_answers.py <<'PY'
import torch
from transformers import AutoProcessor, AutoModelForCausalLM, BitsAndBytesConfig

MODEL_ID = "google/gemma-4-E4B-it"
QUESTION_FILE = "data/baseline_questions.txt"
OUTPUT_FILE = "outputs/baseline_answers.txt"

print("CUDA available:", torch.cuda.is_available())
print("GPU:", torch.cuda.get_device_name(0) if torch.cuda.is_available() else "CPU")

quantization_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch.bfloat16,
bnb_4bit_use_double_quant=True,
)

print("Loading processor...")
processor = AutoProcessor.from_pretrained(MODEL_ID)

print("Loading model in 4bit...")
model = AutoModelForCausalLM.from_pretrained(
MODEL_ID,
quantization_config=quantization_config,
device_map="auto",
dtype=torch.bfloat16,
)

print("Model loaded successfully.")

with open(QUESTION_FILE, "r", encoding="utf-8") as f:
questions = [line.strip() for line in f if line.strip()]

with open(OUTPUT_FILE, "w", encoding="utf-8") as out:
for idx, question in enumerate(questions, start=1):
print(f"Generating answer {idx}/{len(questions)}")

messages = [
{"role": "user", "content": question}
]

inputs = processor.apply_chat_template(
messages,
tokenize=True,
add_generation_prompt=True,
return_tensors="pt",
).to(model.device)

with torch.no_grad():
outputs = model.generate(
inputs,
max_new_tokens=350,
do_sample=False,
)

response = processor.decode(
outputs[0][inputs.shape[-1]:],
skip_special_tokens=True,
)

out.write(f"===== Question {idx} =====\n")
out.write(question + "\n\n")
out.write("===== Answer =====\n")
out.write(response.strip() + "\n\n\n")

print(f"Saved baseline answers to {OUTPUT_FILE}")
PY

 

이 코드는 크게 여섯 단계로 구성된다.

첫째, 필요한 라이브러리를 불러온다.

import torch
from transformers import AutoProcessor, AutoModelForCausalLM, BitsAndBytesConfig


torch는 GPU 사용 여부를 확인하고 모델 추론을 실행하기 위해 사용했다.
transformers는 Hugging Face의 Gemma 4 E4B-it 모델을 불러오기 위해 사용했다.

AutoProcessor는 사용자의 질문을 모델이 이해할 수 있는 입력 형식으로 바꿔주는 도구. 즉 processor를 통해 우리가 영어 또는 한국어 문장으로 입력 한 것을 모델이 읽을 수 있게 변환한다.
AutoModelForCausalLM텍스트 생성용 LLM을 불러오는 클래스. LM임에 주의하자.

BitsAndBytesConfig는 모델을 4bit 양자화 상태로 불러오기 위한 클래스이다.

둘째, 사용할 모델과 파일 경로를 지정한다.

MODEL_ID = "google/gemma-4-E4B-it"
QUESTION_FILE = "data/baseline_questions.txt"
OUTPUT_FILE = "outputs/baseline_answers.txt"


MODEL_ID는 허깅페이스로부터 사용할 모델인 google/gemma-4-E4B-it를 지정한다. 다른 모델을 사용하고 싶다면 이 라인만 바꾸면 된다.
QUESTION_FILE은 질문이 들어 있는 data/baseline_questions.txt 파일이다.
OUTPUT_FILE은 모델 답변이 저장될 outputs/baseline_answers.txt 파일이다.

셋째, CUDA와 GPU가 정상적으로 잡히는지 확인한다.
print("CUDA available:", torch.cuda.is_available())
print("GPU:", torch.cuda.get_device_name(0) if torch.cuda.is_available() else "CPU")

CUDA available 값이 True로 나오면 PyTorch가 GPU를 사용할 수 있다는 뜻이다.
GPU 이름으로 RTX 4080 SUPER가 출력되면 정상적으로 인식된 것이다. GPU가 안 잡히면 LLM CPU로 돌아가 시간이 오래 걸리기에 확인해준다. 

넷째, 4bit 양자화 설정을 적용한다.
quantization_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch.bfloat16,
bnb_4bit_use_double_quant=True,
)

4bit 양자화는 모델을 더 적은 VRAM으로 불러오기 위한 방법이다.
개인 GPU 환경에서는 대형 언어모델을 원본 그대로 올리기 어렵기 때문에, 4bit 양자화를 사용해 메모리 부담을 줄인다.

 

load_in_4bit=True  모델을 4bit 양자화 상태로 로딩한다. 원본 모델은 내 GPU에 무리이기 때문에 quantization한다.
bnb_4bit_quant_type="nf4"  NF4 방식의 4bit 양자화를 사용한다. QLoRA에서 자주 쓰이는 방식이다.
bnb_4bit_compute_dtype=torch.bfloat16  계산할 때는 bfloat16 형식을 사용한다.
bnb_4bit_use_double_quant=True  양자화 정보를 한 번 더 압축해 메모리를 절약한다.

다섯째, processor와 model을 불러온다.

print("Loading processor...")
processor = AutoProcessor.from_pretrained(MODEL_ID)

 

processor는 사용자의 질문을 모델이 이해할 수 있는 입력 형식으로 바꿔주는 역할을 한다. 먼저 Gemma가 요구하는 대화 형식으로 변환하고, token으로 변환한 뒤, PyTorch tensor로 변환한다.
model은 실제 Gemma 4 E4B-it 모델이다.

model = AutoModelForCausalLM.from_pretrained(
MODEL_ID,
quantization_config=quantization_config,
device_map="auto",
dtype=torch.bfloat16,
)

 

MODEL_ID  어떤 모델을 불러올지 지정한다.
quantization_config=quantization_config  앞에서 만든 4bit 양자화 설정을 적용한다.
device_map="auto"  모델을 어느 장치에 올릴지 자동 결정한다. GPU가 정상적으로 인식되어 있다면 가능한 범위에서 GPU를 사용한다.
dtype=torch.bfloat16  계산 데이터 형식을 bfloat16으로 지정한다.

 

정상적으로 끝나면 Model loaded successfully라고 출력된다.


여섯째, 질문 파일을 읽고 각 질문에 대한 답변을 생성한다.

 

모델 준비가 끝났으니 질문 파일을 익는다.


with open(QUESTION_FILE, "r", encoding="utf-8") as f:

QUESTION_FILE은 앞서 지정한 data/baseline_questions.txt 파일이다.

"r"은 read, 즉 읽기 모드이다.

encoding="utf-8"은 한국어가 깨지지 않게 하기 위한 설정이다. 

 

questions = [line.strip() for line in f if line.strip()]

를 통해서는 파일에서 한 줄씩 읽어서 질문 리스트로 만든다.

 

다음과 같은 파일이 있다면

 

PVD 공정에서 step coverage가 나빠지는 이유를 설명해줘.
CVD 공정이 PVD보다 conformality 측면에서 유리한 이유를 설명해줘.

 

questions = [
    "PVD 공정에서 step coverage가 나빠지는 이유를 설명해줘.",
    "CVD 공정이 PVD보다 conformality 측면에서 유리한 이유를 설명해줘."
]

의 형식으로 파이썬 내부에 저장된다. strip()으로 줄 끝의 엔터나 공백을 제거한다.

 

with open(OUTPUT_FILE, "w", encoding="utf-8") as out:

이제 답변을 저장할 파일을 연다. 파일 읽기와 거의 똑같다.

OUTPUT_FILE로 지정했던 outputs/baseline_answers.txt 를 불러온다.

"w"로 쓰기 모드를 정한다. 단 "w" 모드는 기존 파일이 있으면 덮어쓴다. 즉, 코드를 다시 실행하면 기존 baseline_answers.txt가 새 답변으로 바뀐다. 나중에 답변을 보존하고 싶으면 파일명을 날짜별로 바꾸는 게 좋다.

 


그다음 for문을 사용해 질문마다 답변을 생성한다.

for idx, question in enumerate(questions, start=1):
print(f"Generating answer {idx}/{len(questions)}")

 

이 코드는 질문 리스트를 하나씩 꺼낸다. enumerate는 번호를 붙여주는 함수다.
예를 들어 질문이 5개면 이렇게 진행된다.

 

Generating answer 1/5
Generating answer 2/5
Generating answer 3/5
Generating answer 4/5
Generating answer 5/5


이 출력이 있으면 현재 몇 번째 질문을 처리 중인지 알 수 있다.


각 질문은 chat 형식으로 변환된다.

messages = [
{"role": "user", "content": question}
]

 

요즘 LLM은 단순히 문장 하나를 던지는 것보다 대화 형식을 사용하기에 user 역할로 질문을 넣고, 모델이 assistant 역할로 답변을 생성하게 만든다.


이후 processor.apply_chat_template을 사용해 Gemma 모델이 요구하는 입력 형식으로 바꾸고, token으로 변환한다.

이 부분이 입력 변환의 핵심이다.

inputs = processor.apply_chat_template(
messages,
tokenize=True,
add_generation_prompt=True,
return_tensors="pt",
).to(model.device)

 

messages를 Gemma 모델이 요구하는 대화 형식으로 변환하고, 문장을 token으로 변환하고, PyTorch tensor로 변환한 후 모델이 올라간 장치로 이동한다.

 

구체적으로 코드를 보면 

tokenize=True  문장을 token 숫자로 바꾼다.
add_generation_prompt=True  이제 assistant가 답변할 차례라는 신호를 추가한다.
return_tensors="pt"  PyTorch tensor 형식으로 반환한다.
.to(model.device)  입력 데이터를 모델이 있는 장치로 보낸다.

 

마지막으로 model.generate()를 사용해 답변을 생성한다.

with torch.no_grad():
outputs = model.generate(
inputs,
max_new_tokens=350,
do_sample=False,
)

 

input은 모델에게 넣을 질문이다.
torch.no_grad()는 학습이 아니라 추론만 할 때 사용하는 설정이다. 학습할 때는 gradient 계산이 필요하지만, 지금은 답변만 생성하면 된다. 그래서 gradient 계산을 끄면 메모리 사용량이 줄어든다.
max_new_tokens=350은 답변을 최대 350토큰까지 생성하라는 의미이다.
do_sample=False는 랜덤성을 줄여 비교적 일정한 답변을 생성하도록 하는 설정이다.

 

생성된 답변은 processor.decode()를 통해 사람이 읽을 수 있는 문장으로 변환된다.

response = processor.decode(
outputs[0][inputs.shape[-1]:],
skip_special_tokens=True,
)

 

여기서 중요한 부분
outputs[0][inputs.shape[-1]:]
이다.
모델 출력에는 입력 질문 부분과 새로 생성된 답변 부분이 같이 들어 있을 수 있다. 그래서 입력 길이만큼 앞부분을 잘라내고, 모델이 새로 생성한 답변만 가져오는 코드다.

skip_special_tokens=True는 모델 내부에서 쓰는 특수 토큰을 출력하지 않게 하는 설정이다.


이후 질문과 답변을 outputs/baseline_answers.txt 파일에 저장한다.

out.write(f"===== Question {idx} =====\n")
out.write(question + "\n\n")
out.write("===== Answer =====\n")
out.write(response.strip() + "\n\n\n")

답변 형식을 지정한 것이다. 저장 형식은 다음과 같다.

===== Question 1 =====
사용자의 질문...

===== Answer =====
모델 답변 내용...

 

이렇게 저장해두면 나중에 학습 전후의 차이를 비교할 수 있다.

 

코드를 작성한 뒤 명령어를 통해 실행한다. python scripts/save_baseline_answers.py

 

이러면 이제 답변들이 outputs/baseline_answers.txt 에 저장되었다.

읽어보는건 cat outputs/baseline_answers.txt 를 이용해 연다

 

답변이 잘 생성되었다.

이렇게 굳이 data/baseline_questions.txtoutputs/baseline_answers.txt 를 이용해 Gemma 4 에게 던졌던 질문과 그에 대한 답변을 기록해 둔 것은 처음에 말한 대로 나중에 QLoRa 학습 후 답변이랑 비교하기 위해서이다. 

 

중간에 있었던, token, tensor, quantization에 대한 추가 설명...


 tokenization에 대해선

우리는 모델에게 다음과 같은 문장을 입력했다.

PVD 공정에서 step coverage가 나빠지는 이유를 설명해줘.

사람은 이 문장을 그대로 읽고 의미를 이해할 수 있다. 하지만 LLM은 문장을 사람처럼 직접 읽는 것이 아니라, 내부적으로 숫자 계산을 통해 다음 단어를 예측하는 모델이다. 따라서 문장을 먼저 모델이 처리할 수 있는 숫자 형태로 바꿔야 한다.

이때 문장을 잘게 쪼갠 단위를 token이라고 한다.

예를 들어 문장은 대략 다음과 같은 조각으로 나뉠 수 있다.

PVD / 공정 / 에서 / step / coverage / 가 / 나빠지는 / 이유

실제 token 분리는 모델의 tokenizer에 따라 달라질 수 있다. 한글은 한 글자나 여러 글자 단위로 쪼개질 수도 있고, 영어 단어는 단어 전체 또는 단어 일부로 쪼개질 수도 있다.

이렇게 쪼개진 token들은 다시 token ID라는 숫자로 변환된다.

예를 들어 개념적으로는 다음과 같은 흐름이다.

"PVD" → 58213
"공정" → 19384
"step" → 7284
"coverage" → 21938

이 숫자들은 예시일 뿐 실제 값은 다르다. 중요한 것은 LLM이 문장 자체를 직접 처리하는 것이 아니라, 문장을 token으로 쪼개고, 각 token을 숫자 ID로 바꾼 뒤 처리한다는 점이다.

숫자로 바꿨을 때 모델은 다음과 같이 이해한다.
모델 내부에는 embedding table이라는 것이 있다. token ID는 다시 embedding vector라는 긴 숫자 배열로 바뀐다. 즉, 단순한 숫자 ID가 바로 의미를 갖는 것이 아니라, 학습 과정에서 만들어진 벡터 표현으로 바뀌면서 의미 정보를 담게 된다.

전체 흐름은 다음과 같다.

  1. 사람 문장
  2. token으로 분리
  3. token ID 숫자로 변환
  4. embedding vector로 변환
  5. Transformer 구조에서 문맥 계산
  6. 다음 token 예측
  7. 답변 생성


즉, tokenization은 사람이 쓰는 자연어를 LLM이 계산할 수 있는 숫자 데이터로 바꾸는 과정이다.


여기서 tensor라는 개념도 등장한다.

코드에서는 다음 옵션을 사용했다.

return_tensors="pt"

여기서 tensor는 딥러닝에서 사용하는 숫자 배열이다. 쉽게 말하면 vector나 matrix를 더 일반화한 개념이다.

숫자 하나는 scalar
숫자 한 줄은 vector
숫자로 된 표는 matrix
그보다 더 복잡한 다차원 숫자 배열은 tensor라고 볼 수 있다.

LLM은 한 문장의 token뿐만 아니라, 여러 token의 embedding, batch, sequence length 등을 한꺼번에 계산해야 한다. 그래서 입력 데이터는 단순한 숫자 하나가 아니라 다차원 숫자 배열인 tensor 형태로 처리된다.

return_tensors="pt"는 이 결과를 PyTorch tensor 형태로 반환하라는 의미이다. 여기서 pt는 PyTorch를 뜻한다.

그리고 코드 마지막에 붙은 다음 부분은

.to(model.device)

이 코드는 입력 tensor를 모델이 올라가 있는 장치로 보내는 역할을 한다. 모델이 GPU에 올라가 있으면 입력 데이터도 GPU에 있어야 계산이 가능하다. 모델은 GPU에 있는데 입력은 CPU에 남아 있으면 계산이 제대로 이루어지지 않거나 오류가 발생할 수 있다.



다음 4bit quantization에 대해선

LLM 내부에는 매우 많은 숫자가 들어 있다. 이 숫자들을 weight, 즉 가중치라고 부른다. 모델은 이 weight들을 이용해 입력 문맥을 계산하고 다음 token을 예측한다.

원래 weight는 FP16, BF16, FP32 같은 형식으로 저장될 수 있다. 이런 형식은 비교적 정밀하게 숫자를 표현할 수 있지만, 모델 크기가 커질수록 GPU 메모리를 많이 사용한다.

4bit quantization은 이 weight들을 4bit 수준으로 압축해서 저장하는 방식이다.

4bit로 표현할 수 있는 값의 개수는 2^4, 즉 16개이다. 다시 말해 원래 다양한 실수 값을 매우 적은 수의 대표값으로 근사해서 저장하는 방식이라고 볼 수 있다.

개념적으로는 다음과 같다.

  1. 원래 weight 값
  2. 가장 가까운 4bit code로 매핑
  3. 4bit code로 저장
  4. 계산할 때 근사값으로 복원해서 사용


이 과정은 이번 학기에 전공수업인 통신이론에서 배운 quantization과 큰 개념은 비슷하다. 통신이론에서 quantization은 연속적인 아날로그 신호 값을 제한된 개수의 디지털 level로 근사하는 과정이다. LLM에서의 quantization도 연속적인 실수 weight 값을 제한된 개수의 discrete level로 근사한다는 점에서 비슷하다.

다만 차이는 대상과 목적이다.

통신이론에서는 주로 신호의 amplitude를 디지털로 표현하기 위해 quantization을 한다.
LLM에서는 모델 weight를 더 적은 메모리로 저장하고, 개인 GPU에서도 실행할 수 있게 만들기 위해 quantization을 한다.

공통점은 둘 다 정보 손실이 생길 수 있다는 점이다. bit 수가 줄어들수록 메모리는 절약되지만, 원래 값을 더 거칠게 근사하게 되므로 성능 저하 가능성이 있다. 그래서 어떤 방식으로 quantization하느냐가 중요하다.
통신이론 수업에서는 이러한 quantization은 원본값과의 차이로 인한 error를 피할 수 없고, 원 Signal에 따라 이를 최소화 할 수 있는 여러 방식의 non-uniforn quantization도 배웠다.

코드에서는 다음 설정을 사용했다.

bnb_4bit_quant_type="nf4"

여기서 NF4는 NormalFloat 4-bit를 의미한다. 일반적인 4bit quantization은 값을 균등한 간격으로 나눌 수도 있다. 하지만 LLM의 weight는 모든 구간에 균등하게 분포하지 않는 경우가 많다. 보통 0 근처에 많은 값이 몰려 있고, 큰 값은 상대적으로 적게 존재한다.

NF4는 이런 weight 분포를 고려한 4bit quantization 방식이다. 쉽게 말하면, weight가 많이 몰려 있는 구간은 더 세밀하게 표현하고, 값이 드문 구간은 상대적으로 덜 세밀하게 표현하는 방식이라고 이해하면 된다.

 

마치 통신이론에서 사람의 음성을 quantization할 때 확률 분포로 quantization하는 것과 같다.
통신이론을 학교애서 배운 사람이라면 통신이론에 대입해서 이해하면 굉장히 편할 것 같다.

통신이론에서 quantization은 입력 신호가 특정 decision interval J_k에 들어오면, 해당 구간을 대표하는 reconstruction level v_k로 근사하는 과정이다. 이때 원래 값과 대표값 사이의 차이가 quantization error가 되고, D인 평균제곱오차(MSE)를 줄이도록 구간과 대표값을 설계한다.

LLM의 4bit quantization도 큰 흐름은 비슷하다. 다만 대상이 음성 신호의 amplitude가 아니라 모델 내부의 weight라는 점이 다르다. 모델 weight를 4bit code로 저장하고, 계산 시 해당 code에 대응되는 대표값으로 복원한다. 이 과정에서 원래 weight와 복원 weight 사이의 오차가 발생하며, 이 오차가 커지면 모델 성능이 저하될 수 있다.

특히 NF4는 LLM weight가 대체로 정규분포에 가까운 형태를 가진다는 점을 고려해 설계된 4bit 표현 방식이다. uniform quantization처럼 전체 구간을 균등하게 나누는 것이 아니라, weight 분포를 고려해 4bit level을 배치한다는 점에서 non-uniform quantization과 유사한 사고방식을 갖는다.

통신이론의 μ-law, A-law가 음성 신호의 분포와 청각 특성을 고려해 작은 amplitude 구간을 더 세밀하게 표현하는 것처럼, NF4는 LLM weight 분포를 고려해 제한된 4bit 표현력을 더 효율적으로 사용한다.

다만 두 방식이 완전히 같은 것은 아니다. 통신이론에서는 주로 신호 복원 품질이나 SQNR, MSE가 중요한 지표가 되지만, LLM에서는 weight 복원 오차뿐 아니라 perplexity, benchmark 성능, 실제 답변 품질까지 함께 고려해야 한다. 따라서 NF4는 통신이론의 non-uniform quantization 개념을 LLM weight 압축 문제에 적용한 사례로 이해할 수 있다.

정리하면 다음과 같다.

4bit quantization: 모델 weight를 16개 수준의 값으로 근사해 메모리를 줄이는 방식
NF4: LLM weight 분포를 고려해 4bit 대표값을 더 효율적으로 배치하는 방식

그래서 QLoRA에서는 NF4 방식이 자주 사용된다.


다음은 bfloat16이다.
코드에서는 다음 설정이 있었다.

bnb_4bit_compute_dtype=torch.bfloat16
dtype=torch.bfloat16

bfloat16은 딥러닝에서 많이 사용되는 16bit 부동소수점 형식이다. 부동소수점은 0.001, 3.14, -2.5 같은 실수 값을 컴퓨터가 표현하는 방식이다.

대표적인 형식에는 FP32, FP16, BF16이 있다.

  • FP32는 32bit를 사용하므로 비교적 정밀하지만 메모리를 많이 사용한다.
  • FP16은 16bit를 사용하므로 메모리는 적게 쓰지만 표현 가능한 숫자 범위가 좁아질 수 있다.
  • BF16, 즉 bfloat16은 16bit를 사용하면서도 FP32와 비슷한 exponent 범위를 갖는다.


쉽게 말하면 bfloat16은 FP16처럼 가볍지만, 큰 수와 작은 수를 표현하는 범위는 비교적 안정적인 형식이다. 정밀한 소수점 표현은 FP32보다 떨어지지만, 딥러닝 연산에서는 메모리 효율과 안정성의 균형이 좋아 자주 사용된다.


또 하나의 설정은 다음과 같다.

bnb_4bit_use_double_quant=True

이 설정은 double quantization을 사용하겠다는 의미이다.

4bit quantization을 하면 weight 자체만 저장하는 것이 아니라, 그 4bit code를 실제 값에 가깝게 복원하기 위한 scale 정보도 필요하다. 예를 들어 어떤 weight 묶음을 4bit로 저장했다면, 이 값들이 원래 어느 정도 크기 범위에 있었는지 알려주는 보조 정보가 필

4bit로 압축된 weight code + 그 code를 실제 값으로 복원하기 위한 scale 정보

double quantization은 이 scale 정보까지 다시 quantization해서 압축하는 방식이다.
이렇게 하면 모델을 불러올 때 필요한 메모리를 조금 더 줄일 수 있다.


다음으로 모델을 불러오는 부분에서

model = AutoModelForCausalLM.from_pretrained(
    MODEL_ID,
    quantization_config=quantization_config,
    device_map="auto",
    dtype=torch.bfloat16,
)

이 코드에서 AutoModelForCausalLM은 Hugging Face Transformers에서 제공하는 자동 모델 로더이다.

AutoModel은 모델 종류를 자동으로 파악해서 불러온다는 의미이고, CausalLM은 Causal Language Model을 뜻한다. Causal Language Model은 앞의 문맥을 보고 다음 token을 예측하는 방식의 언어모델이다.

Gemma 같은 LLM은 기본적으로 다음 token을 하나씩 예측하면서 문장을 생성한다.

예를 들어 입력이 다음과 같다면,

PVD 공정에서 step coverage가

모델은 다음에 올 가능성이 높은 token을 예측한다.

나빠지는

그다음 다시 다음 token을 예측한다.


이 과정을 반복하면서 답변 문장이 생성된다. 그래서 Gemma 같은 텍스트 생성 모델은 AutoModelForCausalLM으로 불러온다.

from_pretrained는 이미 학습된 모델을 불러오겠다는 의미이다. 우리는 모델을 처음부터 학습시키는 것이 아니라, 이미 대규모 데이터로 사전학습된 Gemma 4 모델을 가져와 사용하는 것이다.

마지막으로 special token에 대해 정리할 필요가 있다.

코드에는 다음 부분이 있었다.

skip_special_tokens=True

special token은 모델이 대화 구조를 이해하기 위해 사용하는 내부 표식이다. 사람이 읽기 위한 단어라기보다, 모델에게 “여기서부터 사용자 질문이다”, “여기서부터 assistant 답변이다”, “문장이 끝났다” 같은 정보를 알려주는 역할을 한다.

예를 들어 모델 내부에서는 대화가 개념적으로 다음과 같은 형태로 구성될 수 있다.

<start_of_turn>user
PVD 공정에서 step coverage가 나빠지는 이유를 설명해줘.
<end_of_turn>
<start_of_turn>assistant

이런 표식이 있어야 모델은 어디까지가 user의 질문이고, 어디서부터 assistant가 답변해야 하는지 구분할 수 있다.

하지만 최종적으로 사람이 읽을 답변에는 이런 내부 표식이 필요 없다. 우리가 원하는 출력은 다음과 같다.

PVD는 방향성이 강한 물리적 증착 방식이기 때문에 고종횡비 구조에서 step coverage가 나빠질 수 있다.

반면 special token이 그대로 출력되면 다음처럼 불필요한 내부 표식이 섞일 수 있다.

<start_of_turn>assistant
PVD는 방향성이 강한 물리적 증착 방식이기 때문에...

그래서 decode할 때 skip_special_tokens=True를 사용해 special token은 제거하고, 사람이 읽을 답변 내용만 출력하도록 했다.

 

 

앞으로는 다음 항목을 기준으로 답변을 비교할 예정이다.

  1. 답변 구조가 일정한가
  2. 공정 변수에 대한 언급이 있는가
  3. 가능한 원인 가설을 제시하는가
  4. 추가로 확인해야 할 데이터를 제안하는가
  5. 개선 실험 방향을 제안하는가
  6. 공정 엔지니어 관점의 결론이 포함되는가

결국 이번 baseline 저장 단계는 QLoRA 효과를 확인하기 위한 출발점이다.

현재까지는 로컬 환경에서 Gemma 4 E4B-it 모델을 4bit로 불러와 기본 답변을 저장하는 데까지 진행했다. 다음 단계에서는 이 baseline 답변을 기준으로, 모델이 공정 엔지니어형 답변 구조를 더 잘 따르도록 QLoRA 학습 데이터셋을 구성할 예정이다.