1. 내가 준비할 것을 찾아봐야한다

스크린샷 2024-10-27 01.08.24.png

  1. Training data (학습 시킬 데이터)
  2. Validation data (모델이 평가 받을 데이터)

2. 나머지? 난수형태 + 솔직히 노가다로 찾아봐야한다.

스크린샷 2024-10-27 01.08.53.png

  1. seed
  2. Batch size
  3. Learning rate
  4. epoch

경험상 seed는 내가 조정할 수 있는 영역이 아니다. 물론 최적의 결과가 나오면 그 seed로 설정해야 하겠지만 내가 seed 값을 처음에 어떤게 나올지 모르는 난수영역이니 일단 건들지 않기!

  1. batch size: 이 아이는 진짜 하나도 모르겠다. 10에서 최적의 결과가 나올 수 있고, 100에서 나올수도 있고 갑자기 생뚱맞게 21에서도 나오는 친구다.. 노가다로 일단 해보자
  2. Learning rate: 하..진짜 얘도 과적합만 안뜨게끔 잘하면 되는데.. 너무 조심스럽게하면 우리가 원하는 정확도도 안나온다.일단 0.05부터 조심스럽게 시작해보고 0.1, 0.5 이런식으로 조금씩 늘려가보는걸로
  3. epoch: 위에 학습률 조정하는 앤데 얘도 너무 많이 설정하면 또 과적합이 뜬다..그렇다고 줄이면? 학습 못한 애처럼 나온다.

3. 그래서 결론? 할 수 있는거부터 해보자

내가 원하는건 어떤 값인지는 알잖아?

일단 플랜을 어떻게 프론트한테 주는지 다시한번 보자.

  1. entity는 대략 이렇다

    public class Plan extends BaseEntity {
    
        @ManyToOne(fetch = FetchType.LAZY)
        @JoinColumn(name = "user_id", nullable = false)
        @NotNull(message = "유저는 필수입니다.")
        private User user;
    
        @NotBlank(message = "제목은 공백일 수 없습니다.")
        @Size(max = 100, message = "제목은 100자를 넘을 수 없습니다.")
        @Column(length = 100, nullable = false)
        private String title;
    
        @Size(max = 1000, message = "설명은 1000자를 넘을 수 없습니다.")
        @Column(columnDefinition = "TEXT")
        private String description;
    
        @Column(nullable = false)
        private LocalDateTime startDate;
    
        @Column(nullable = false)
        private LocalDateTime endDate;
    
        @Builder.Default
        @Column(nullable = true)
        private Boolean accessibility = true;
    
        @Builder.Default
        @Column(nullable = false)
        private Boolean isCompleted = false;
    }
    

    그래서 이를 바탕으로 학습 데이터를 생각해보면,

    {
        "prompt": "이번 주 내로 토익 공부하고 싶어",
        "completion": [
            {
                "title": "토익 공부 - 대안 1",
                "description": "이번 주 금요일 저녁 7시부터 2시간 동안 집중적으로 토익 공부",
                "startDate": "<발화 시점 이후 금요일 저녁 7시>",
                "endDate": "<금요일 저녁 9시>",
                "accessibility": true,
                "isCompleted": false
            },
            {
                "title": "토익 공부 - 대안 2",
                "description": "토요일 오전 10시부터 3시간 동안 학습 계획",
                "startDate": "<발화 시점 이후 토요일 오전 10시>",
                "endDate": "<토요일 오후 1시>",
                "accessibility": true,
                "isCompleted": false
            },
            {
                "title": "토익 공부 - 대안 3",
                "description": "일요일 오후 2시부터 4시간 동안 집중 학습",
                "startDate": "<발화 시점 이후 일요일 오후 2시>",
                "endDate": "<일요일 오후 6시>",
                "accessibility": true,
                "isCompleted": false
            }
        ]
    }
    

    이런 느낌일 것이다

    물론 startDate와 endDate 의 < > 안은 LocalDateTime 형식이니 바꿀 것이다.

4. jsonl? json이랑 차이가 뭔데?

  1. GPT API 에서는 jsonl 이라는 파일을 요구하고 있다.

    jsonl을 달라고 요구하고 있다.

    jsonl을 달라고 요구하고 있다.

    json 이랑 별 차이 없겠지 뭐 라고 생각한 순간.. 그래도 알아보자라는 생각으로 찾아보았더니

    json + line이라고 한다.

    그러니까, 각 라인이 Json객체로 이루어진 파일 형식이다.

    쉽게 생각하면 한줄에 사전하나씩 여러개가 같이 있는 것.

    이 파일에 조건이 또 있다. 차라리 그냥 한줄에 사전하나씩 여러개가 있는것으로 끝나면 그냥 한줄에 쓰면 되지뭐~ 했지만..

    json line을 정의하기위해서 몇가지 규칙이있다.(http://jsonlines.org/)

    1. UTF-8 인코딩일 것.

    2. 각 줄은 유효한 json형식이어야 함.

    3. 줄 바꾸기 문자 "\n"으로 각 객체를 구분.

    너 진짜 뭐 돼?

    좋은 파인 튜닝을 위해 꼭 써야하는거니 변환? 해봐야지

    스크린샷 2024-10-27 04.54.43.png

    일단 json을 만들어봤다.

    이제 우리가 학습이나 평가 파일을 만들때마다 어떠한 웹사이트에 의존할 수 없으니 팀원들이 쓸 파이썬 파일을 만든다.

    import json
    
    def convert_json_to_jsonl_with_alternatives(input_json_path, output_jsonl_path):
        with open(input_json_path, 'r', encoding='utf-8') as json_file:
            data = json.load(json_file)
            
        with open(output_jsonl_path, 'w', encoding='utf-8') as jsonl_file:
            jsonl_item = {
                "prompt": data["prompt"],
                "completion": data["completion"]  # completion 리스트에 대안 3개 포함
            }
            jsonl_file.write(json.dumps(jsonl_item, ensure_ascii=False) + '\\n')
        
        print(f"JSONL file saved to {output_jsonl_path}")
    
    # 사용 예시
    input_json_path = './test.json' 
    output_jsonl_path = './test_output.jsonl'
    
    convert_json_to_jsonl_with_alternatives(input_json_path, output_jsonl_path)
    

    이렇게 하면..

    {"prompt": "이번 주 내로 토익 공부하고 싶어", "completion": [{"title": "토익 공부 - 대안 1", "description": "이번 주 금요일 저녁 7시부터 2시간 동안 집중적으로 토익 공부", "startDate": "2024-10-25T19:00:00", "endDate": "2024-10-25T21:00:00", "accessibility": true, "isCompleted": false}, {"title": "토익 공부 - 대안 2", "description": "토요일 오전 10시부터 3시간 동안 학습 계획", "startDate": "2024-10-26T10:00:00", "endDate": "2024-10-26T13:00:00", "accessibility": true, "isCompleted": false}, {"title": "토익 공부 - 대안 3", "description": "일요일 오후 2시부터 4시간 동안 집중 학습", "startDate": "2024-10-27T14:00:00", "endDate": "2024-10-27T18:00:00", "accessibility": true, "isCompleted": false}]}
    
    

    스크린샷 2024-10-27 05.15.43.png

    이런식으로 한개의 json이 한줄로 작성이 된다.

    5. 이게 끝이 아닌건 알고 있죠?

    이제 prompt 부분을 손봐야한다. 유저들에게는 보이지 않을 그 부분을 손봐야한다.

    예시로는 “이번 주 내로 토익 공부하고 싶어” 라고 했을때, (다른 일정이 있으면 겹치지 않게, 내가 말한 시간 이후로) 이런 느낌으로 가야한다.

    그래서 이걸 어떻게 해야할까…

    그냥 이걸 하드코딩 해버려야할까? 저걸 그대로 구현할 수 있을까? 의문…

    6. 형식이 중요한데 이걸 생각 못했네

    1. 데이터 자체가 중요한데 어떻게 들어오는지, 어떻게 출력할지 알아야지…
    2. 그리고 여러 계획이 혼재하면 어떻게함? 예시로, “이번 주 내로 토익 공부하고 싶어, 그리고 코딩 테스트 준비하고 싶어” 이런거

    자 뭐 그까짓거 해봅시다.

    일단 json 형식을

    {
        "deviceId": "abc123",
        "planGroups": [
            {
                "groupId": 1,
                "planCards": [
                    {
                        "title": "토익 공부",
                        "description": "금요일 저녁 7시부터 2시간 동안 집중적으로 토익 공부",
                        "startDate": "2024-10-25T19:00:00",
                        "endDate": "2024-10-25T21:00:00",
                        "accessibility": true,
                        "isCompleted": false
                    },
                    {
                        "title": "코딩 테스트 준비",
                        "description": "토요일 오후 4시부터 2시간 동안 문제 풀이",
                        "startDate": "2024-10-26T16:00:00",
                        "endDate": "2024-10-26T18:00:00",
                        "accessibility": true,
                        "isCompleted": false
                    }
                ]
            },
            {
                "groupId": 2,
                "planCards": [
                    {
                        "title": "토익 공부",
                        "description": "토요일 오전 10시부터 3시간 동안 학습 계획",
                        "startDate": "2024-10-26T10:00:00",
                        "endDate": "2024-10-26T13:00:00",
                        "accessibility": true,
                        "isCompleted": false
                    },
                    {
                        "title": "코딩 테스트 준비",
                        "description": "일요일 오전 9시부터 3시간 동안 문제 풀이",
                        "startDate": "2024-10-27T09:00:00",
                        "endDate": "2024-10-27T12:00:00",
                        "accessibility": true,
                        "isCompleted": false
                    }
                ]
            },
            {
                "groupId": 3,
                "planCards": [
                    {
                        "title": "토익 공부",
                        "description": "일요일 오후 2시부터 4시간 동안 집중 학습",
                        "startDate": "2024-10-27T14:00:00",
                        "endDate": "2024-10-27T18:00:00",
                        "accessibility": true,
                        "isCompleted": false
                    },
                    {
                        "title": "코딩 테스트 준비",
                        "description": "일요일 오후 1시부터 2시간 동안 코딩 문제 풀이",
                        "startDate": "2024-10-27T13:00:00",
                        "endDate": "2024-10-27T15:00:00",
                        "accessibility": true,
                        "isCompleted": false
                    }
                ]
            }
        ]
    }
    

    이런식으로 하면 여러 플랜을 한번에 받을 수 있다.

    아 코드 그럼 다시 짜야하잖아

    또 gpt가 알아듣게끔 현재 한국 시간기준, 내가 입력한 시간 이후로 해줘 이런 식으로 해야한다.

    복잡하네 정말^_^

    그걸 토대로한 condition.json

    [
        {
            "key": "avoid_schedule_conflicts",
            "condition": "다른 일정이 있으면 겹치지 않게 해줘",
            "required": true
        },
        {
            "key": "start_after_user_input_time",
            "condition": "내가 말한 시간 이후로 계획을 세워줘",
            "required": true
        },
        {
            "key": "use_korean_timezone",
            "condition": "일정은 한국 시간(UTC+9)을 기준으로 설정해줘",
            "required": true
        }
    ]
    

    이 condition.json이 유저 입력에 녹아들어서 4o mini가 알아서 일정을 짜줄거다.

    그래 이걸 바탕으로 파이썬 파일을 다시 짜보자.

    import json
    from datetime import datetime
    
    def load_conditions(condition_file_path):
        with open(condition_file_path, 'r', encoding='utf-8') as condition_file:
            conditions = json.load(condition_file)
            current_time = datetime.now().strftime('%Y-%m-%dT%H:%M:%S')
            for condition in conditions:
                condition["condition"] = condition["condition"].replace("${current_time}", current_time)
        return conditions
    
    def load_prompt(prompt_file_path):
        with open(prompt_file_path, 'r', encoding='utf-8') as prompt_file:
            prompt = prompt_file.read().strip()
        return prompt
    
    def create_prompt_with_conditions(user_input, conditions):
        prompt = user_input
        condition_statements = [condition["condition"] for condition in conditions if condition.get("required", False)]
        if condition_statements:
            prompt += " (" + ", ".join(condition_statements) + ")"
        return prompt
    
    def convert_json_file_to_jsonl(input_json_path, condition_file_path, prompt_file_path, output_jsonl_path):
        with open(input_json_path, 'r', encoding='utf-8') as json_file:
            input_data = json.load(json_file)
            
            if not isinstance(input_data, list):
                raise TypeError("input_json 파일이 리스트 형식이 아닙니다.")
        
        conditions = load_conditions(condition_file_path)
        user_input = load_prompt(prompt_file_path)
        prompt = create_prompt_with_conditions(user_input, conditions)
        
        with open(output_jsonl_path, 'w', encoding='utf-8') as jsonl_file:
            jsonl_file.write(json.dumps({"prompt": prompt}, ensure_ascii=False) + '\\n')
            
            for item in input_data:
                jsonl_file.write(json.dumps(item, ensure_ascii=False) + '\\n')
        
        print(f"JSONL file saved to {output_jsonl_path}")
    
    # 사용 예시
    input_json_path = './input_data.json'
    condition_file_path = './conditions.json'
    prompt_file_path = './prompt.txt'
    output_jsonl_path = './test_output.jsonl'
    
    convert_json_file_to_jsonl(input_json_path, condition_file_path, prompt_file_path, output_jsonl_path)
    

    일단 해봐야지

    prompt.txt는 일단 테스트용이니 뭐..

    진짜 다행히도 잘나온다

    json에서 jsonl로 변환된 파일:

    {"prompt": "\\"이번 주 내로 토익 공부하고 싶어, 그리고 코딩 테스트 준비하고 싶어\\" (다른 일정이 있으면 겹치지 않게 해줘, 현재 시간 이후로 계획을 세워줘 (2024-10-27T17:33:05 기준), 일정은 한국 시간(UTC+9)을 기준으로 설정해줘)"}
    {"completion": {"deviceId": "abc123", "groupId": 1, "planCards": [{"title": "토익 공부", "description": "10월 27일 일요일 오후 6시부터 2시간 동안 집중 학습", "startDate": "2024-10-27T18:00:00", "endDate": "2024-10-27T20:00:00", "accessibility": true, "isCompleted": false}, {"title": "코딩 테스트 준비", "description": "10월 28일 월요일 오전 9시부터 2시간 동안 문제 풀이", "startDate": "2024-10-28T09:00:00", "endDate": "2024-10-28T11:00:00", "accessibility": true, "isCompleted": false}]}}
    {"completion": {"deviceId": "abc123", "groupId": 2, "planCards": [{"title": "토익 공부", "description": "10월 28일 월요일 오전 11시부터 3시간 동안 학습 계획", "startDate": "2024-10-28T11:00:00", "endDate": "2024-10-28T14:00:00", "accessibility": true, "isCompleted": false}, {"title": "코딩 테스트 준비", "description": "10월 28일 월요일 오후 3시부터 2시간 동안 문제 풀이", "startDate": "2024-10-28T15:00:00", "endDate": "2024-10-28T17:00:00", "accessibility": true, "isCompleted": false}]}}
    {"completion": {"deviceId": "abc123", "groupId": 3, "planCards": [{"title": "토익 공부", "description": "10월 29일 화요일 오전 10시부터 2시간 동안 집중 학습", "startDate": "2024-10-29T10:00:00", "endDate": "2024-10-29T12:00:00", "accessibility": true, "isCompleted": false}, {"title": "코딩 테스트 준비", "description": "10월 29일 화요일 오후 1시부터 3시간 동안 문제 풀이", "startDate": "2024-10-29T13:00:00", "endDate": "2024-10-29T16:00:00", "accessibility": true, "isCompleted": false}]}}
    

    위 python파일을 실행하기 위해서는 일단 노가다가 필요하다.

    7. 끝인줄 알았지? 파인튜닝의 세계는 그리 녹록지 않단다.

    저렇게 하면 된다며… 공식문서도 그렇다며,,,

    {"prompt": "<prompt text>", "completion": "<ideal generated text>"}
    

    공식문서가 이렇게 하라는데.. 그래 그렇게 해보자!

    스크린샷 2024-10-30 02.39.38.png

    그냥 접을까? 파인튜닝 그게 뭐라고^^

    그래.. 그래도 해야지. 다시 알아보자.

    제대로 다시 찾아보자.

    OpenAI의 chat 모델(gpt-4-turbo 등)로 파인튜닝을 할 때, 요구되는 데이터 포맷이 일반 prompt-completion 형식과 다르기 때문에 발생한 문제입니다. chat 모델은 ‘메시지’ 형식이 필요합니다. 각 레코드는 단순한 prompt와 completion으로 구분되는 대신, message 형식의 JSON 객체로 변환해야 합니다.

    데이터는 다음처럼 {"messages": [{"role": "user", "content": "질문 내용"}, {"role": "assistant", "content": "응답 내용"}]} 형식을 유지해야 합니다.

    그래서 진짜 해야하는건 어떤 형식일까요

    {"messages": [{"role": "user", "content": "질문 내용"}, {"role": "assistant", "content": "응답 내용"}]}
    

    네. 이런 형식입니다…

    아이고 두야….

    그럼 이 형식을 바탕으로 다시 jsonl을 작성해보자.

    {"messages": [{"role": "user", "content": "\\"다음 주에 있을 프로젝트 발표 준비하고 싶어. 그리고 이번 주말에는 가족 모임이 있어\\" (다른 일정이 있으면 겹치지 않게 해줘, 기존일정:[{\\"title\\": \\"데이터베이스 시험 준비\\", \\"description\\": \\"10월 30일 수요일 오후 1시부터 3시간 동안 개념 정리\\", \\"startDate\\": \\"2024-10-30T13:00:00\\", \\"endDate\\": \\"2024-10-30T16:00:00\\", \\"accessibility\\": true, \\"isCompleted\\": false}, {\\"title\\": \\"데이터베이스 시험 준비\\", \\"description\\": \\"10월 31일 목요일 오후 3시부터 4시간 동안 심화 문제 풀이\\", \\"startDate\\": \\"2024-10-31T15:00:00\\", \\"endDate\\": \\"2024-10-31T19:00:00\\", \\"accessibility\\": true, \\"isCompleted\\": false}, {\\"title\\": \\"영화 관람\\", \\"description\\": \\"11월 2일 토요일 오후 2시부터 3시간 동안 친구들과 영화 관람\\", \\"startDate\\": \\"2024-11-02T14:00:00\\", \\"endDate\\": \\"2024-11-02T17:00:00\\", \\"accessibility\\": true, \\"isCompleted\\": false}, {\\"title\\": \\"저녁 약속\\", \\"description\\": \\"11월 2일 토요일 저녁 6시 30분부터 2시간 동안 친구들과 저녁 식사\\", \\"startDate\\": \\"2024-11-02T18:30:00\\", \\"endDate\\": \\"2024-11-02T20:30:00\\", \\"accessibility\\": true, \\"isCompleted\\": false}, {\\"title\\": \\"병원 방문\\", \\"description\\": \\"11월 4일 월요일 오전 9시부터 1시간 동안 병원 방문\\", \\"startDate\\": \\"2024-11-04T09:00:00\\", \\"endDate\\": \\"2024-11-04T10:00:00\\", \\"accessibility\\": true, \\"isCompleted\\": false}, {\\"title\\": \\"데이터베이스 시험 준비\\", \\"description\\": \\"11월 4일 월요일 오후 1시부터 5시간 동안 종합 복습\\", \\"startDate\\": \\"2024-11-04T13:00:00\\", \\"endDate\\": \\"2024-11-04T18:00:00\\", \\"accessibility\\": true, \\"isCompleted\\": false}, {\\"title\\": \\"데이터베이스 시험 준비\\", \\"description\\": \\"11월 5일 화요일 오전 10시부터 6시간 동안 최종 마무리\\", \\"startDate\\": \\"2024-11-05T10:00:00\\", \\"endDate\\": \\"2024-11-05T16:00:00\\", \\"accessibility\\": true, \\"isCompleted\\": false}] 현재 시간 이후로 계획을 세워줘 (2024-10-29T20:41:00 기준), 일정은 한국 시간(UTC+9)을 기준으로 설정해줘)\\""}, {"role": "assistant", "content": "{\\"deviceId\\": \\"xyz987\\", \\"groupId\\": 3, \\"planCards\\": [{\\"title\\": \\"프로젝트 발표 준비\\", \\"description\\": \\"발표 준비 - 주제 탐색 및 기초 자료 수집\\", \\"startDate\\": \\"2024-10-30T10:00:00\\", \\"endDate\\": \\"2024-10-30T12:00:00\\", \\"accessibility\\": true, \\"isCompleted\\": false}, {\\"title\\": \\"프로젝트 발표 준비\\", \\"description\\": \\"발표 준비 - 발표 연습\\", \\"startDate\\": \\"2024-11-01T13:00:00\\", \\"endDate\\": \\"2024-11-01T15:00:00\\", \\"accessibility\\": true, \\"isCompleted\\": false}, {\\"title\\": \\"가족 모임\\", \\"description\\": \\"주말 점심 가족 모임\\", \\"startDate\\": \\"2024-11-03T12:00:00\\", \\"endDate\\": \\"2024-11-03T14:00:00\\", \\"accessibility\\": true, \\"isCompleted\\": false}]}"}]}
    {"messages": [{"role": "user", "content": "Hello"}, {"role": "assistant", "content": "올바른 입력을 해주세요. 형식: '이번 주 영어 공부와 수학 시험을 준비하고 싶어'와 같이 입력할 수 있습니다."}]}
    

    대충 이런식 x 100개 정도..? 솔직히 이것도 최소수치라 학습 데이터로 잘 써야한다.

    또, 프롬프팅문도 진짜 잘 작성해야한다. user는 정말 대충 말할 수도 있어서, 뒤에서 알아서 프롬프팅을 해줘야한다. 그게 프롬프팅+파인튜닝의 매력이 아닐까?

    ok 일단 테스트용으로 학습 돌려 봅시다!

    스크린샷 2024-10-30 02.49.56.png

    하..진짜 이거 하나보자고 너무 매달렸다..진짜…

    image.png

    이것마저 잘되다니 진짜 너무 좋다…

    또, 이상한 요청에 대한 학습도 했었는데, 그건?

    image.png

    거의 뭐 이제 MVP 된 느낌..

    이제 gpt가 뱉어내는 값중 deviceId만 가변 값(예시:<USER_DEVICE_ID>)으로 바꾸면 될 것 같다.

    이제 프롬프팅을 잘 바꿔보자!

    일정이 없는 경우: