서론
지난 포스팅에서는 데이터셋을 어떻게 수집하고, 데이터를 어떻게 분석하는 지 그 과정에 대해서 알아보았다. 이번 포스팅에서는 이렇게 분석한 데이터를 어떻게 프롬프트에 반영하는지에 대해서 알아보고자 한다. 이렇게 만든 프롬프트를 기반으로 prompt를 만든 다음 나중에 Fine-tuning에 사용할 예정이다.
생성자 설정 __init__()
우선 초기 상태 설정인 init 함수를 적어준다. 받는 값들은 미리 넣어줄 것이고, 나중에 판단하거나 값을 새로 할당해야되는 것들은 None, 0 내지 False로 지정해줬다. detail을 바로 data['detail']로 받지 않은 이유는 디테일에 불필요한 요소가 너무 많기에 따로 받아서 정제를 해줘야되기 때문이다. 우선 데이터를 받으면 파싱하는 함수를 만들겠다.
def __init__(self, data, price):
self.title = data['title']
self.price = price
self.details = None
self.prompt = None
self.token_count = 0
self.include = False
self.videos = data.get('videos', {})
self.store = None
self.store_in_title = False
self.parse(data)
파싱 함수
우선 파싱함수는 prompt를 바꾸고 토큰 수를 세는 함수다. 단순히 셀 뿐 아니라 내가 지정한 토큰 범위까지 제안해서 프롬프트를 작성한다. 토큰수를 제한하는 이유는 너무 적어도, 너무 많아도 llm 학습에 방해가 되기 때문이다. 이렇게 파싱된 프롬프트를 llm으로 전달하기 위함이다. 그리고 이 때 llm model을 사용하기 때문에 비용이 든다. 해당 함수 큰 그림은 다음과 같다. 1) 원하는 정보를 prompt에 넣기 2) 일부 불필요한 정보가 많다면 정제해서 넣기 3) 2)까지 정보를 다 넣은 prompt 길이가 최소 길이를 넘으면 컨텐츠를 미리 끊어 놓기 4) 정제된 prompt를 토크나이저로 encoding하기 5) 인코딩된 후 토큰의 길이가 최소 토큰수를 넘으면 max token만큼 자른뒤 다시 디코딩해서 최종 프롬프트 작성. 참고로 contents는 최종 프롬프트가 되기 전에 담은 정보들이고 prompt는 1부터 5과정을 다 거친(=최종 인코딩 된) 것을 의미한다. 한줄로 요약하면 아래에 과정은 컨텐츠를 작성하고 이 컨텐츠가 곧 프롬프트가 되는 과정이라고 생각하면 되겠다.
1) 원하는 정보를 프롬프트에 넣기
이전 포스팅에서 얘기했듯 나는 store 이름과 판매 제품의 브랜드가 같다면 official 이라고 하기로 했다. 그 과정이 아래 코드 블록이다.
self.store = data.get('store')
self.store_in_title = False
if self.store and self.store.lower()[:5] in self.title.lower():
self.store_in_title = True
contents += "official sale\n"
지난 포스팅에서 채택된 두번째 가정은 비디오가 있는 것이 비싸다는 가정이었다. 그래서 아래와 같이 video dictionary(dictionary 값으로 데이터가 있었음. 내가 설정한게 아님.)에 url이나 타이틀이 있다면 -> video_exists contents에 넣었다. 이는 아래 코드블록으로 구현했다.
video_data = data.get("videos", {})
if isinstance(video_data, dict):
titles = video_data.get("title", [])
urls = video_data.get("url", [])
if titles or urls:
contents += "video_exists \n"
이 후 description, features, details를 차례차례 넣었다. 여기서 내가 video와 official여부를 먼저 넣은 것은, 뒤에 배치하면 3번 혹은 4번에서 같이 씻겨 나가게 된다. 그걸 방지하고자 앞에다가 넣었다.
2) 불필요한 정보 정제하기
detail에서 필요없는 문구를 제외하기 위해 아래 scrub_details 함수를 parse 함수 위에다가 선언했다.
def scrub_details(self):
REMOVALS = ['"Batteries Included?": "No"', '"Batteries Included?": "Yes"', '"Batteries Required?": "No"', '"Batteries Required?": "Yes"', "By Manufacturer", "Item", "Date First", "Package", ":", "Number of", "Best Sellers", "Number", "Product "]
details = self.details
for remove in REMOVALS:
details = details.replace(remove, "")
return details
그리고 추가로 scrub 함수를 작성한다. 불필요한 정보를 없애는 함수다. detail 한정 말고 최종 contents를 만들고 데이터를 토크나이징 하기 전에 최종적으로 정제할 때 사용한다.
def scrub(self, stuff):
stuff = re.sub(r'[:\[\]"{}【】\s]+', ' ', stuff).strip()
stuff = stuff.replace(" ,", ",").replace(",,,",",").replace(",,",",")
words = stuff.split(' ')
select = [word for word in words if len(word)<7 or not any(char.isdigit() for char in word)]
return " ".join(select)
3) 정제된 contents 를 인코딩 하기 ~ 5) 최종 프롬프트 작성
세번쨰 과정이다. 위에서 작성된 컨텐츠 길이에 맞춰서 일정 길이를 넘으면 max_token를 넘지 않기 위해 글자 개수를 사전에 제거한다. 이 함수에서는 목표 max token이 160이다. 보통 1토큰이 4글자인걸 생각하면 160*4 = 640글자인데 2000자로 잘랐다. 이건 본인 마음임. 어처피 토크나이저로 줄일건데 어떤 컨텐츠를 토크나이저에 집어넣는건 본인 몫이다. 아무튼 난 저렇게 설정한뒤, 위에서 했던 scrub함수로 정제해줬다. 그리고 정제된걸 encoding했다. encoding된 토큰이 150이상이면 160으로 자른다. 이게 이 파싱 함수의 목표이기 때문이다. 이후 decoding해서 prompt를 넘겨준다. encoding기껏 해놓고 왜 decoding하나요? -> 토큰 수보고 일정 토큰만큼 자르는게 목적이기 때문입니다. 아무튼 이렇게 디코딩된 text를 최종 prompt 작성을 위해 make_prompt 함수에 넘긴다. 이건 별거 아니고 그냥 위 아래로 텍스트랑 얼마인지 알려달라는 문구 넣는거임. 그리고 test용으로 하나 더 작성한다. test 용은 얼마인지 가격을 빼놓은 거임. 정답지를 안 준 prompt라고 보면 된다.
if len(contents) > 300:
contents = contents[:2000]
text = f"{self.scrub(self.title)}\n{self.scrub(contents)}"
tokens = self.tokenizer.encode(text, add_special_tokens=False)
if len(tokens) > 150:
tokens = tokens[:160]
text = self.tokenizer.decode(tokens)
self.make_prompt(text)
self.include = True
def make_prompt(self, text): # prompt 보는 기
self.prompt = f"How much does this cost to the nearest dollar? \n\n{text}\n\n"
self.prompt += f"Price is $,{str(round(self.price))}.00"
self.token_count = len(self.tokenizer.encode(self.prompt, add_special_tokens=False))
def test_prompt(self):
return self.prompt.split("Price is $")[0] + "Price is $"
최종 Item Class 코드
from typing import Optional
from transformers import AutoTokenizer
import re
class Item:
tokenizer = AutoTokenizer.from_pretrained("meta-llama/Meta-Llama-3.1-8B", trust_remote_code=True)
title: str
price: float
category: str
token_count: int = 0
details: Optional[str]
include = False
videos: str
store: Optional[str]
store_in_title: bool = False
prompt: Optional[str] = None
def scrub_details(self):
details = self.details
for remove in self.REMOVALS:
details = details.replace(remove, "")
return details
def scrub(self, stuff):
stuff = re.sub(r'[:\[\]"{}【】\s]+', ' ', stuff).strip()
stuff = stuff.replace(" ,", ",").replace(",,,",",").replace(",,",",")
words = stuff.split(' ')
select = [word for word in words if len(word)<7 or not any(char.isdigit() for char in word)]
return " ".join(select)
def parse(self, data):
contents = ''
# add store name
self.store = data.get('store')
self.store_in_title = False
if self.store and self.store.lower()[:5] in self.title.lower():
self.store_in_title = True
contents += "official sale\n"
# compare store name and product name
video_data = data.get("videos", {})
if isinstance(video_data, dict):
titles = video_data.get("title", [])
urls = video_data.get("url", [])
if titles or urls:
contents += "video_exists \n"
description_text = '\n'.join(data['description'])
if description_text:
contents += description_text + '\n'
features = '\n'.join(data['features'])
if features:
contents += features + '\n'
self.details = data['details']
if self.details:
contents += self.scrub_details() + '\n'
if len(contents) > 300:
contents = contents[:2000]
text = f"{self.scrub(self.title)}\n{self.scrub(contents)}"
tokens = self.tokenizer.encode(text, add_special_tokens=False)
if len(tokens) > 150:
tokens = tokens[:160]
text = self.tokenizer.decode(tokens)
self.make_prompt(text)
self.include = True
def make_prompt(self, text):
self.prompt = f"How much does this cost to the nearest dollar? \n\n{text}\n\n"
self.prompt += f"Price is $,{str(round(self.price))}.00"
self.token_count = len(self.tokenizer.encode(self.prompt, add_special_tokens=False))
def test_prompt(self):
return self.prompt.split("Price is $")[0] + "Price is $"
def __repr__(self):
return f"<{self.title} = ${self.price}>"
종 코드는 위와 같다. 이 함수는 나중에 ItemLoader 함수에서 데이터를 부른 뒤 프롬프트를 만드는데 사용할 것이다. ItemLoader도 작성됐다는 전제하에 어떻게 사용되냐면, items = ItemLoader("Appliances").load() 이렇게 선언하면 items에 리스트 형식으로 각각 제품들이 담긴다. 그래서 item[0].prompt를 선언하면 위에서 적은 프롬프트 대로 아래와 같이 나온다. 왜 비디오 여부랑 official여부가 없죠? -> 얜 비디오도 없고 제품명에 store명이 안들어간 경우이기 때문입니다.
How much does this cost to the nearest dollar?
Cardone Service Plus Remanufactured Caliper Bracket (Renewed)
At Cardone, quality is the foundation of our corporate culture and the focus of each and every part we sell. We remanufacture all Caliper Brackets to the highest standards to meet or exceed OEM performance. All bolt threads have been thoroughly inspected and tested to ensure trouble-free installation. Feel confident each and every time you apply your brakes, knowing you have Cardone high quality products installed. Meets or exceeds OE performance and original OE design flaws are corrected Application-specific brackets ensure proper fit and function Brackets are coated with rust protection to extend product life Grease pack is included for easy installation Industry-leading bracket coverage offers the most complete list of applications to choose from Manufacturer Cardone Service Plus, Brand Cardone, Model
Price is $36.00
이 코드를 끝으로 다음 포스팅에서는 ItemLoader 클래스를 작성하도록 하겠다.
'IT, Digital' 카테고리의 다른 글
Fine-tuning LLM (2) Dataset investigation (0) | 2025.06.05 |
---|---|
Fine-tuning LLM (1) Data Curation과 데이터 수집 및 로드 (1) | 2025.06.03 |
[RAG] Chroma와 FAISS 차이, 장단점 간단 정리 (0) | 2025.05.28 |
티스토리 하위 도메인으로 애드센스 승인 (2) 하위 도메인 블로그 에드센스 연결 및 주의사항 (1) | 2025.05.27 |
티스토리 하위 도메인으로 애드센스 승인 (1) 티스토리 새 블로그 및 하위(3차) 도메인 개설 방법 (0) | 2025.05.26 |