ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 고등학생도 하는 GPT Fine-Tuning (AI-Hub 방언 데이터셋 활용해서 fine-tuning해보기)
    논문 후기와 구현 2024. 9. 2. 23:33

    Prerequisite

    • Visual Studio Code 설치 (데이터셋 용량이 커서 Google Colab에서 구현 힘듭니다)
    • 10기가 정도의 충분한 여유공간
    • OpenAI 가입 및 금액 충전 (ChatGPT 구독과 별개)

     

    GPT Fine-Tuning

    작년(2023년) 하반기에 독일에서 일할 때만 하더라도, 굉장히 어렵게 코딩를 짰어야지 겨우 fine-tuning을 할 수 있었는데, 2024년 상반기부터 굉장히 편해졌다. 데이터셋만 있으면, 누구나 fine-tuning을 할 수 있다. 늘 생각하지만 GPT의 fine-tuning이 가장 쉽기를 바라는 곳이 다름아닌 OpenAI이기 때문에, 그런 걸 할 줄 안다는 것에 자신의 가치를 두어서는 안 된다.

     

    Fine-tuning의 역할과 한계

    인공지능을 과대평가하는 것은 싫어하지만, 그렇다고 해서 LLM을 단순히 다음에 나올 가장 그럴듯한 단어(token)를 예측하는 확률 계산기로 폄하해서는 안 된다. 설사 그렇다고 하더라도, 중국어의 방 사고실험이 보여주듯 LLM은 이미 본질적인 지식을 갖춘 고등지적 생명체와 차이가 없다. 그런데 일반적인 도메인 말고 전문적인 도메인(의료, 법률 등)에 대해 질문하게 된다면, 만족스럽지 못한 답변을 들을 때가 많다. 그것을 Alignment의 문제일 때가 많다. 즉, 모델이 최종적으로 프로세스를 마치고 fully-connected layer(FC layer)를 통해 어떤 단어(token)를 내뱉을지 순차적으로 계산하는데, 그 fc layer가 해당 도메인에 맞추어지지 않아 일반적이고 피상적인 내용만을 답변하는 것이다.

     

    그래서 "보통은" fine-tuning은 fc layer를 포함한 상위 레이어단에서 이루어질 때가 많다. (아예 fc layer만 건드리면 그것은 전이학습이다.) 또한 LLM에 대한 fine-tuning으로는 주로 Instrucion fine-tuning이 사용된다(제일 쉬워서). 새로 tokenize할 필요조차 없이, 몇가지 응답을 종용하면 되기 때문이다. 그러므로 Instruction fine-tuning으로는 새로운 지식을 본질적으로 학습할 수 없다는 지적이 있다(이에 대한 반론도 있긴 함). 즉, 새로운 지식을 아예 학습시킬 목적으로는 적당하지는 않다. 다만, 이미 알고 있는 지식 사이를 연결시키고, 특정 답변 형식을 맞추도록 하기 위해서는 Instrucion fine-tuning만한 것이 없다.

     

    이해 못하겠어도 상관 없다. 일단 몇 번 해보면 감이 잡힐 것이니, 차근차근 따라와보자.

     

    Fine-Tuning해보기

    GPT를 fine-tuning하기 위해서는 다음이 필요하다:

    • Train set jsonl 파일
    • (Optional) Validation set jsonl 파일
    • OpenAI API 금액 충전

    이때 우리는 모든 jsonl 파일을 다음과 같은 형식으로 만들 것이다:

    {"messages": [{"role": "system", "content": "다음은 한국어를 소리 나는 대로 적은 것입니다. 만약 이것이 방언이라면 표준어로 바꾸세요."}, {"role": "user", "content": <방언>}, {"role": "assistant", "content": <표준어>}]}

     

    이 뜻은, <방언>이 input으로 들어왔을 때, <표준어>가 나오도록 (즉, 사투리를 번역하도록) 종용하는 것이다.

     

    1. 데이터셋 다운로드

    백문이 불여일견이므로 한번 fine-tuning을 해보자. 어떤 때에는 이것이 유용하고, 반면 어느 때에는 무용할지 감이 잡힐 것이다. 우선 AI Hub에서 방언 데이터셋을 받아야 한다. (로그인 필수)

     

    이렇게 많은 데이터셋 중, 여기에서는 그냥 아무거나 골라서 [중노년층 한국어 방언 데이터 (충청도, 전라도, 제주도)]를 활용한다. 이 데이터셋에는 방언 텍스트와 오디오 데이터셋이 나누어져 있다.

     

     

    라벨링데이터는 텍스트데이터이고, 원천데이터는 오디오데이터이다. 그러므로 원천데이터는 한국어 방언 인식이 가능한 Audio Encoder 등을 훈련시킬 때면 몰라도, 지금은 필요 없다. 용량도 지나치게 크므로 라벨링데이터만 다운받도록 하자. 라벨링데이터 중에서도 2인발화가 아닌 1인발화 데이터셋만 활용할 것이다.

     

    2. 데이터 전처리

    다운받아서 보면 여러개의 .json 파일로 이루어진 것을 확인할 수 있다. json 파일은 쉽게 말해서, 딕셔너리나 리스트 형식으로 쓰여진 텍스트 데이터이다. 즉, 하나의 방언 데이터가 딕셔너리에 담겨져있는 것이다. 예를 들어 다음과 같은 형식이다. AI Hub 약관에 따라 민감 내용은 가림.

    {
    	"fileName": "",
    	"speaker": [
    		{
    			"speakerId": "",
    			"gender": "",
    			"birthYear": null,
    			"residenceProvince": "",
    			"residenceCity": null,
    			"residencePeriod": null,
    			"job": "",
    			"academicBackground": null,
    			"healthCondition": null
    		}
    	],
    	"collector": {
    		"collectorId": "",
    		"residenceProvince": "",
    		"residenceCity": null,
    		"residencePeriod": null
    	},
    	"script": {
    		"scriptId": null,
    		"value": "",
    		"domain": "",
    		"speechType": ""
    	},
    	"audio": {
    		"bitsPerSample": null,
    		"samplingFrequency": null,
    		"channel": "",
    		"recordDate": "",
    		"speechStartTime": "",
    		"speechEndTime": "",
    		"recordDuration": null
    	},
    	"transcription": {
    		"segments": [
    			{
    				"orderInFile": null,
    				"startTime": "",
    				"endTime": "",
    				"dialect": "",
    				"pronunciation": null,
    				"standard": null,
    				"voiceType": ""
    			}
    		],
    		"sentences": [
    			{
    				"speakerId": "",
    				"intonations": [],
    				"startTime": "",
    				"endTime": "",
    				"dialect": "",
    				"pronunciation": "",
    				"standard": "",
    				"sentenceId": null
    			}
    		],
    		"dialect": "",
    		"pronunciation": "",
    		"standard": ""
    	},
    	"annotation": {
    		"standards": [
    			{
    				"value": "",
    				"transcriptionBeginInFile": null,
    				"transcriptionEndInFile": null
    			}
    		],
    		"intonations": [],
    		"transcriptionAnnotations": [
    			{
    				"transcriptionBeginInFile": null,
    				"transcriptionEndInFile": null,
    				"tagType": ""
    			}
    		],
    		"intents": [
    			{
    				"sentenceId": null,
    				"tagType": ""
    			}
    		],
    		"emotions": [
    			{
    				"sentenceId": null,
    				"tagType": ""
    			}
    		],
    		"grammarTypes": [
    			{
    				"sentenceId": null,
    				"tagType": ""
    			}
    		]
    	},
    	"stt": {
    		"recognizer": "",
    		"responseDate": "",
    		"speakerIds": [
    			""
    		],
    		"segments": [
    			{
    				"orderInFile": null,
    				"startTime": "",
    				"endTime": "",
    				"value": ""
    			}
    		]
    	}
    }

     

     

    이 구조를 자세히 분석하면 다음과 같이 전처리할 수 있다.

    import os
    import json
    from tqdm import tqdm
    
    def process_json_file(file_path):
        """JSON 파일을 읽고 필요한 데이터를 추출하여 포맷을 맞춘 후 반환합니다."""
        with open(file_path, 'r', encoding='utf-8') as f:
            data = json.load(f)
        
        pronunciation = data.get('transcription', {}).get('pronunciation', '')
        standard = data.get('transcription', {}).get('standard', '')
        
        formatted_data = {
            "messages": [
                {"role": "system", "content": "다음은 한국어를 소리 나는 대로 적은 것입니다. 만약 이것이 방언이라면 표준어로 바꾸세요."},
                {"role": "user", "content": pronunciation},
                {"role": "assistant", "content": standard}
            ]
        }
        
        return formatted_data
    
    def process_folder(folder_path):
        """단일 폴더 내의 모든 JSON 파일을 처리하고 결과를 리스트로 반환합니다."""
        files = [f for f in os.listdir(folder_path) if f.endswith('.json')]
        results = []
        
        for file_name in tqdm(files, desc=f"Processing folder {os.path.basename(folder_path)}"):
            file_path = os.path.join(folder_path, file_name)
            
            try:
                result = process_json_file(file_path)
                results.append(result)  
            except Exception as e:
                print(f"Error processing file {file_name}: {e}")
        
        return results
    
    def main(folders, output_file):
        """여러 폴더를 순회하여 JSON 파일을 처리하고 결과를 JSONL 파일에 저장합니다."""
        all_results = []
        for folder_path in folders:
            print(f"Processing folder: {folder_path}")
            folder_results = process_folder(folder_path)
            all_results.extend(folder_results)
        
        # 모든 결과를 한 번에 JSONL 파일에 저장
        with open(output_file, 'w', encoding='utf-8') as out_file:
            for item in tqdm(all_results, desc="Writing results to file"):
                out_file.write(json.dumps(item, ensure_ascii=False) + '\n')
    
    if __name__ == "__main__":
        folders = [] # JSON 파일이 위치한 폴더 경로들을 리스트에 넣어줘야 함
        output_file = #결과를 저장할 JSONL 파일 경로 (확장자 .jsonl까지 적어줘야 함)
        main(folders, output_file)

     

    이 코드만으로도 우리는 우리가 원했던 아래의 형식으로 데이터셋을 구축할 수 있다.

    {"messages": [{"role": "system", "content": "다음은 한국어를 소리 나는 대로 적은 것입니다. 만약 이것이 방언이라면 표준어로 바꾸세요."}, {"role": "user", "content": "삼춘도 초신 멘드라 봐쑤꽈"}, {"role": "assistant", "content": "할아버지도 짚신 만들어 봤습니까"}]}

     

     

    3. Fine-tuning

    이제 거의 다 왔다. 앞서 말했다시피 2023년까지만 해도 코드를 짜야 GPT를 fine-tuning할 수 있었는데, 이제는 OpenAI 사이트에서 GUI로 편하게 할 수 있다.

     

    1. 우선 OpenAI에 접속한다.

     

    2. Dashboard(우상단)에 접속한다.

    3. 좌측에 Fine-tuning 탭을 선택한다.

     

    4. Create를 눌러서 Training data와 Validation data(선택적)을 업로드한다. Seed와 Config는 그냥 기본값으로 두고 돌려도 괜찮다. 돈이 엄청 많은 게 아니라면 모델은 그냥 GPT-4o-mini를 선택하자.

     

    이렇게 하고 하루 정도 기다리면 Fine-tuning이 끝나있을 것이다!

     

    주의1: 20만 건의 사투리에 대해 fine-tuning을 진행하면 20만원 정도 소요될 수 있다. 시범적으로 4만 건 정도만 해도 충분히 성능은 괜찮으니 너무 욕심 안 부려도 된다.

    주의2: fine-tuning 도중 잔액이 다 떨어지면 training 자체가 중단되어 말짱 도루묵이 될 수 있으므로 잔액을 충분히 충전하고 진행해야 한다.

     

    성능

    이렇게 해서 사투리를 이해하도록 GPT-4o-mini를 튜닝하면, 기존 GPT-4o-mini보다 BLEU는 7배 이상, METEOR 및 ROUGE는 2배 가량 향상되는 것을 확인할 수 있었다. 

Designed by Tistory.