본문 바로가기
LLM

[LLM] LLM으로 Tabular Data 학습해보기 - 1. GPT4o Finetuning (경정데이터분석)

by Tiabet 2024. 9. 20.

https://www.kboat.or.kr/contents/customPlaza/noticeView.do?seqId=21256&cPage=1

 

KBOAT 경정

2024 전국 대학생 경정 데이터 분석 경진대회 개최 안내 작성자 관리자 작성일 2024.07.24 조회 4191 첨부파일 파일 첨부됨 [양식]개인정보 수집·활용 동의서.pdf  국민체육진흥공단 경륜경정총괄본

www.kboat.or.kr

친구들과 함께 이 대회에 참가하고 있다. 정형데이터 (Tabular) 분석할 때는 아무래도 머신러닝 알고리즘인 XGB, LGBM, CatBoost, Random Forest 등을 사용하는 것이 정설이다. 하지만 최근 LLM이 워낙 발전하면서 이런 정형데이터들도 LLM으로 풀어보려는 시도들이 여러 공모전에서 활발히 이루어지고 있음을 알 수 있었다.

 

https://dacon.io/competitions/official/236214/codeshare/9591?page=2&dtype=recent

 

LLM ( LLama2 )를 이용한 정답 구하기

고객 대출등급 분류 AI 해커톤

dacon.io

이건 올해 초 데이콘에서 열린 대출등급 분류 해커톤이었는데, 마찬가지로 정형데이터 분류 문제였다. LLM을 활용하여 정형데이터에 대해 예측을 진행할 때는, 정형데이터를 적당히 버무려서 프롬프트를 구성하여 학습을 시키고, 그 뒤 테스트 데이터에 추론을 시키는 방식으로 진행된다.

 

그래서 나도 한 번 위 코드를 흉내내서 경정데이터를 LLM을 활용하여 분석해보는 시도를 해봤다. 

 

결론부터 미리 말하자면 결국에 이 방법은 나는 실패했다.

 

경정이란

출처 : KBoat 공식 홈페이지

사실 대회에 참가하기 전까진 경정대회라는 거의 존재 자체를 몰랐다. 경정이란 한마디로 정리하면 배를 타고 하는 경마대회이다. 한국에서 열리는 경정대회는 총 6명의 선수가 배를 타고 경주를 하여 순위를 매기는 방식으로 열린다.

 

사용한 데이터

https://www.kboat.or.kr/contents/information/fixedChuljuPage.do

 

KBOAT 경정

선수명, 모터번호, 보트번호를 클릭하시면 해당 정보가 팝업으로 나타납니다. 금일 출주경주란의 경주번호를 클릭하시면 해당 경주의 출주표로 이동합니다.

www.kboat.or.kr

친숙하지 않은 대회라서 낯설었는데 생각보다 홈페이지도 잘 되어있고 업데이트도 실시간으로 이루어져서 관리가 잘 되고 있음이 느껴졌다. 나는 다른 팀원이 위 사이트에서 크롤링한 데이터를 사용했고, 마찬가지로 홈페이지에 올라와있는 모터와 보트 데이터까지 수집해서 사용했다.

 

https://www.data.go.kr/data/845467/linkedData.do

 

공공데이터 포털

국가에서 보유하고 있는 다양한 데이터를『공공데이터의 제공 및 이용 활성화에 관한 법률(제11956호)』에 따라 개방하여 국민들이 보다 쉽고 용이하게 공유•활용할 수 있도록 공공데이터(Datase

www.data.go.kr

굳이 크롤링을 하지 않아도 공공데이터 포털에 선수, 모터, 보트 정보와 매 경기 출주표가 잘 수집되어 있어서 그대로 사용해도 된다. 위 데이터들을 잘 엮어서 하나로 합쳐진 데이터프레임을 사용했다.

전처리를 다 하고 나면 이런 식으로 데이터프레임이 나왔다. 총 70000개에 결과 포함 35개의 피쳐가 나왔다. 나 말고 팀원들은 머신러닝 모델을 사용해서 위처럼 데이터가 나왔고, 나는 이 데이터로  LLM 학습을 진행해야 했다. 한 행에는 선수 하나의 정보가 담겨있으며, 경정은 한 경기에 6명의 선수가 참가하므로 상위에서 6명씩 자르면 그게 한 경기가 된다.

 

내가 시도한 방법은 다음과 같다.

 

1. 6명의 선수를 하나의 프롬프트로 묶어서 누가 우승을 했는지 선수 번호로 학습을 시킨다.

2. 하나의 프롬프트에 한 선수만 넣어서 이 선수가 우승을 했는지 이진 분류 문제로 학습을 시킨다.

 

사용한 모델은 총 2개로, OPENAI의 API로 GPT4o와 Facebook의 Roberta 를 사용했으며, 우선 1번 방식으로 접근했을 때의 결과를 공유하고자 한다.

 

 

프롬프트 구성

# Adjust the function to correctly determine the winning player index within each group
def create_jsonl_format_grouped(group):
    # Constructing the user input prompt with relevant player information for a group of 6
    user_input = " ".join([
        f"선수{i+1}: 등급: {row['등급']}, 성별: {row['성별']}, 나이: {row['나이']}, 체중: {row['체중']}, "
        f"최근6회차_평균착순점: {row['최근6회차_평균착순점']}, 최근6회차_평균득점: {row['최근6회차_평균득점']}, "
        f"최근6회차_승률: {row['최근6회차_승률']}, 최근6회차_연대율2: {row['최근6회차_연대율2']}, "
        f"최근6회차_연대율3: {row['최근6회차_연대율3']}, 최근6회차_평균ST: {row['최근6회차_평균ST']}, "
        f"연간성적_평균착순점: {row['연간성적_평균착순점']}, 연간성적_연대율: {row['연간성적_연대율']}, "
        f"FL: {row['FL']}, 평균사고점: {row['평균사고점']}, 출주횟수: {row['출주횟수']}, "
        f"모터_평균착순점: {row['모터_평균착순점']}, 모터_연대율2: {row['모터_연대율2']}, "
        f"모터_연대율3: {row['모터_연대율3']}, 보트_평균착순점: {row['보트_평균착순점']}, "
        f"보트_연대율: {row['보트_연대율']}, 1코스_성적: {row['1코스_성적']:.2f}, 2코스_성적: {row['2코스_성적']:.2f}, "
        f"3코스_성적: {row['3코스_성적']:.2f}, 4코스_성적: {row['4코스_성적']:.2f}, 5코스_성적: {row['5코스_성적']:.2f}, "
        f"6코스_성적: {row['6코스_성적']:.2f}, 최근1경기_착순: {row['최근1경기_착순']}, 최근2경기_착순: {row['최근2경기_착순']}, "
        f"최근3경기_착순: {row['최근3경기_착순']}, 최근4경기_착순: {row['최근4경기_착순']}, "
        f"최근5경기_착순: {row['최근5경기_착순']}, 최근6경기_착순: {row['최근6경기_착순']}, "
        f"최근7경기_착순: {row['최근7경기_착순']}, 최근8경기_착순: {row['최근8경기_착순']}"
        for i, (_, row) in enumerate(group.iterrows())
    ])

    # Find the index of the row with 'result' == 1 within the group
    winning_player_index = group[group['result'] == 1].index[0] % 6 + 1 if not group[group['result'] == 1].empty else None
    assistant_output = f"우승한 선수의 번호는 {winning_player_index}번 입니다."
    
    jsonl_entry = {
        "messages": [
            {"role": "system", "content": "당신은 유능한 순위예측 전문가입니다."},
            {"role": "user", "content": user_input.strip()},
            {"role": "assistant", "content": assistant_output.strip()}
        ]
    }
    return jsonl_entry

GPT의 도움을 받아서 위와 같이 프롬프트를 짰다. API에서 학습을 진행하려면 파일을 json 형식으로 넣어주어야 해서 위처럼 진행했다. 사실 지금 보니 칼럼 이름에 _을 다 빼고 더 잘 토큰화될 수 있도록 처리를 해줬어야 했는데 하는 아쉬움이 남는다. 답변으로는 "우슨한 선수의 번호는 1번입니다." 처럼 나오게 했고, 페르소나 튜닝 기법을 사용하기 위해 "당신은 유능한 순위예측 전문가입니다."라는 역할을 시스템에 주었다. 그리고 6명을 한 번에 묶는 작업을 실행하기 위해 group이라는 리스트를 넣어주는 식으로 구성했다.

 

import json 

grouped_data = [data.iloc[i:i+6] for i in range(60000, len(data), 6)]

jsonl_data_grouped = [create_jsonl_format_grouped(group) for group in grouped_data if len(group) == 6]

output_file_path_grouped_corrected = 'data/train_single_smaller.jsonl'
with open(output_file_path_grouped_corrected, 'w', encoding='utf-8') as f:
    for entry in jsonl_data_grouped:
        json.dump(entry, f, ensure_ascii=False)
        f.write('\n')

 

그리고 위처럼 그룹화를 Train Dataset에 진행해주고, jsonl 파일로 저장한 다음 OPENAI의 플랫폼으로 넘어갔다.

 

프롬프트 결과 예시는 아래와 같다.

 

OPENAI Platform에서 학습하기

OPENAI Platform이라고 구글에 검색하면 ChatGPT 사이트와는 별개의 사이트가 나온다.

https://platform.openai.com/docs/overview

여기엔 모델 설명도 되어있고 사용법들도 설명해주는 문서들이 존재한다. 추가로 별개의 코딩 없이 모델을 학습시킬 수 있는 기능이 오픈되어 있다. 모든 모델을 지원하는 것은 아니고, 얼마 전 gpt4o와 mini의 파인튜닝을 일부 무료로 오픈해주어서 사용해봤다. 

 

https://openai.com/index/gpt-4o-fine-tuning/

(무료로 열어줬다는 글)

 

https://platform.openai.com/assistants

그리고 대쉬보드로 넘어가면 파인튜닝을 할 수 있는 창이 나온다. 들어가서 create 버튼을 누르면

 

이렇게 Train과 Validation data를 넣고 파인튜닝을 진행할 수 있게 된다. 튜닝할 수 있는 파라미터는 batch, learning rate multiplier, epochs로 총 3개밖에 없다. 나는 요금적인 한계로 인하여 총 12000개의 데이터 중 2000개만을 학습을 진행했다. OPENAI의 설명에 따르면 양질의 데이터로 500개에서 1000개 정도만 있으면 파인튜닝이 완벽하게 된다고 주장하지만, 역시 오차가 있을 수 있음도 설명하고 있다.

 

 

파인튜닝 결과는 위와 같았다. 파인튜닝 과정에서 보여주는 정보는 loss 뿐이다. 사실 이렇게 loss가 수렴하지 않고 계속 튀는 것이 이상해서 학습이 잘 안된 거 같은 느낌이 오긴 했었다. 학습에는 총 1시간 30분 정도가 소요됐다.

 

결과 확인

OPENAI에선 파인튜닝된 모델의 결과까지 확인할 수 있는 기능은 Dashboard에 만들어놓지 않았다. 그래서 API로 추론을 직접 진행해줬다.

 

import re
from tqdm import tqdm 
from dotenv import load_dotenv 

load_dotenv()
SECRET_KEY = os.getenv("SECRET_KEY")

client = OpenAI(api_key = SECRET_KEY)

y_pred_1 = []

for i in tqdm(range(len(jsonl_data_grouped_test))):
    completion = client.chat.completions.create(
    model="ft:gpt-4o-mini-2024-07-18:personal:--",
    messages= jsonl_data_grouped_test[i]['messages']
    )
    message = completion.choices[0].message.content
    
    number = re.search(r'\d+', message)

    if number:
        y_pred_1.append(int(number.group()))

print("prediction1 complete")

 

openai의 API는 그 사용방법이 완벽히 설명되어 있지는 않지만 꽤 쉽게 가져다가 사용할 수 있게 되어있다. 리턴 형식 또한 json 형식으로 나오기 때문에 그것만 확인해가면서 진행했다. "우승한 선수의 번호는 1번입니다." 형식으로 답이 나오기 때문에 re.search로 정수를 찾아주었다. 총 추론은 3번 진행하여 결과를 보팅 앙상블하였다. 이제 결과를 확인해보자.

 

indices = []

for i in range(0, len(y_test), 6):
    chunk = y_test[i:i+6]
    index_of_one = np.where(chunk == 1)[0]
    if len(index_of_one) > 0:
        indices.append(index_of_one[0]+1)

y_test_real = indices

from sklearn.metrics import accuracy_score

y_pred_combined = [round((y1 + y2 + y3) / 3) for y1, y2, y3 in zip(y_pred_1, y_pred_2, y_pred_3)]

print(accuracy_score(y_test_real,y_pred_combined))

 

이렇게 진행했을 때 정확도가 0.23~0.25가 나왔다. 6명의 선수 중 우승자(1위)를 맞추는 단승 형식으로 진행했기에 순수하게 랜덤으로 찍는다면 16퍼센트 정도가 나와야 하는데, 이보다 근소하게 높게 나오는 것을 보면 학습이 어느 정도 진행된 거 같긴 해도 만족스러운 결과는 아니다.

 

하지만 내가 이 과정을 진행하면서 느낀 점은 다음과 같다.

 

1. 재현성이 없다. 같은 경기의 프롬프트를 줘도 다른 선수가 우승했다고 답변하는 적이 많았다.

2. 부분 무료긴 해도 일정량이 초과하면 유료다 보니 무한정 학습시킬 수고, 추론 또한 유료이기 때문에 제한적이다.

3. 6명에 대한 정보가 들어가기 때문에 같은 단어가 반복되고 있고, 숫자의 의미를 잘 모르는 LLM의 특성상 잘 훈련시키기가 어려울 것 같다.

 

위의 이유에 근거해서 2번 방식인 이진 분류 문제로 해석하여 다시 접근해봤다. 2번 방식에 대한 내용은 다음 포스팅에서 쓰겠다.