클론 코딩

[코드 리뷰] [Public_0.6674 ] No Fine-tune _ RAG

ParkS2 2024. 4. 6. 20:53

코드를 따라가며 코드의 흐름을 파악하고 이해하며 학습

코드역할 및 내용을 코드 각줄의 주석과 마크다운으로 설명

 

마지막부분에 코드 아이디어 와 코드에 대한 평가를 진행

 

코딩 아이디어

  1. 데이터 전처리 및 정제: 텍스트 데이터에서 불필요한 부분(예: 주석)을 제거하고, 데이터를 원하는 형태로 정제하는 과정이 포함된다. 이러한 전처리 과정은 텍스트 데이터를 분석하거나 모델에 입력하기 전에 깨끗하고 일관된 형태로 만들기 위해 필수적이다.
  2. 데이터셋 구성 및 로딩: CustomDataset 클래스를 통해 PyTorch의 Dataset을 상속받아 사용자 정의 데이터셋을 구성하고, 이를 DataLoader를 통해 배치 단위로 모델에 공급하는 방식이다. 이 방법은 다양한 머신러닝 및 딥러닝 작업에서 데이터를 효율적으로 관리하고 사용하기 위해 필요하다.
  3. 모델 학습 및 평가: 사전 학습된 모델(AutoModelForSequenceClassification)을 로드하고, 특정 태스크에 맞게 파인 튜닝하는 과정이다. 학습 과정에서는 손실 함수, 최적화 알고리즘, 학습률 스케줄러 등을 설정하고, 이를 바탕으로 모델을 학습시킨다. 이러한 접근 방식은 다양한 자연어 처리(NLP) 작업에 적용 가능하다.
  4. 앙상블 기법(소프트 보팅): 여러 모델의 예측 결과를 결합하여 최종 예측을 도출하는 방식이다. 각 모델의 예측 확률을 평균내어 가장 높은 확률을 가진 클래스를 최종 예측으로 선택한다. 이 방법은 모델의 일반화 성능을 향상시키고, 과적합을 방지하는 데 도움이 될 수 있다.

코드 평가

  1. 전처리와 데이터셋 준비: 코드는 데이터를 철저하게 전처리하고, 훈련과 테스트 데이터셋을 만드는 과정을 잘 구현했다. 특히, C++ 코드에서 불필요한 주석을 제거하고, 데이터를 적절한 형식으로 변환하여 모델 학습에 적합하게 만든다.
  2. 모델 활용과 학습: 사전 훈련된 모델(neulab/codebert-cpp)을 활용하고, 이를 특정 데이터셋에 맞게 미세 조정(fine-tuning)하는 과정이 잘 설계되어 있다. 이는 전이 학습의 효과를 적극적으로 활용하는 좋은 사례다.
  3. 다중 모델 앙상블: 여러 개의 모델로부터 예측을 수집하고, 이를 통합하여 최종 예측을 결정하는 소프트 보팅(soft voting) 방식을 사용했다. 이는 모델의 예측력을 향상시킬 수 있는 효과적인 전략이다.

개선점

1. 비슷한 데이터 로딩 및 모델 훈련 코드가 여러 번 반복되는데, 이를 함수나 반복문으로 더 효율적으로 처리할 수 있을 것이다.

2. 코드에서 예외 처리가 누락된 부분이 있다. 예를 들어, 파일 로딩이나 토큰화 과정에서 발생할 수 있는 에러를 적절히 처리하지 않고 있다.

!pip install pandas 
!pip install scikit-learn 
!pip install numba 
!pip install tqdm 
!pip install matplotlib 
!pip install transformers
!pip install rank-bm25

 

라이브러리 Import

In [ ]:
import pandas as pd
import numpy as np
from sklearn.metrics import accuracy_score, f1_score
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import LabelEncoder

import random
import os, re

import torch
from torch import nn
import torch.nn.functional as F
from torch.utils.data import Dataset, DataLoader
from torch.optim import AdamW
from torch.utils.data import TensorDataset, DataLoader, RandomSampler, SequentialSampler

from tqdm import tqdm

from transformers import logging
from transformers import AutoTokenizer, RobertaForSequenceClassification, AutoModelForSequenceClassification

import matplotlib as mpl
import matplotlib.pyplot as plt

logging.set_verbosity_error()
import warnings
warnings.filterwarnings(action='ignore')

from itertools import combinations
from rank_bm25 import BM25L

import time
import datetime
import pickle
import argparse

데이터 생성

데이터 불러오기

In [ ]:
# 데이터 자동으로 불러오기 위한 숫자 생성(001~500)
numbers = [str(i).zfill(3) for i in range(1, 501)]
all_code_list = []

# train_code(cpp) 불러오기
for i in numbers:
    cpp_code_list = []
    
    for file in os.listdir(f"./data/train_code/problem{i}"):
    
        if file.endswith(".cpp"):
            with open(os.path.join(f"./data/train_code/problem{i}", file), "r") as f:
                cpp_code_list.append(f.read())
    all_code_list.append(cpp_code_list)

-> 지정된 디렉토리 내의 모든 C++ 코드 파일의 내용이 all_code_list에 순서대로 저장

In [ ]:
# 전처리 함수 1_주석 처리 
def clean_data(text):

    #입력된 문자열 text의 앞과 뒤에서 공백을 제거
    text = text.strip()

    #정규 표현식을 사용하여 C++의 한 줄 주석(//로 시작하는 부분)을 찾아서 제거
    text = re.sub(r"//.*", "", text)

    #C++의 여러 줄 주석(/ *로 시작하고 * /로 끝나는 부분, 여기서 공백은 없어야 함)을 찾아서 제거
    text = re.sub(r'/\*.*?\*/', '', text, flags=re.DOTALL)

    #다시 한번 문자열의 앞뒤 공백을 제거
    text = text.strip()
    return text

# 전처리 함수 2_빈 줄 제거
def get_rid_of_empty(c):
    ret = []
    splitted = c.split('\n')
    for s in splitted:
        if len(s.strip()) > 0:
            ret.append(s)
    return '\n'.join(ret)

전처리 함수 1: clean_data 이 함수는 주어진 텍스트에서 특정 패턴의 문자열을 제거하는 역할을 한다.

전처리 함수 2: get_rid_of_empty 이 함수는 주어진 문자열에서 빈 줄을 제거하는 역할을 한다.

->C++ 코드에서 주석을 제거하고 빈 줄을 없애는 전처리 과정

In [ ]:
# 전처리 실행
all_code_list_clean = []

for i in range(500):
    cleans = []
    for j in range(500):
        clean = get_rid_of_empty(clean_data(all_code_list[i][j]))
        cleans.append(clean)
    all_code_list_clean.append(cleans)

-------------------------------------------------
all_code_list_clean=[]

for i in range(500):
    cleans=[]
    for j in range(500):
        clean=get_rid_of_empty(clean_data(all_code_list[i][j]))
        cleans.append(clean)
    all_code_list_clean.append(cleans)

all_code_list_clean=[]

for i in range(500):
    cleans=[]
    for j in range(500):
        clean=get_rid_of_empty(clean_data(all_code_list[i][j]))
        cleans.append(clean)
    all_code_list_clean.append(cleans)

for i in range(500):
    cleans=[]
    for j in range(500):
        clean=get_rid_of_empty(clean_data(all_code_list[i][j]))
        cleans.append(clean)
------------------------------------------------

두 개의 중첩 반복문 사용: 외부 반복문은 i를 0부터 499까지 반복하고, 내부 반복문은 j를 0부터 499까지 반복한다. 이는 총 250,000(500x500)회의 반복을 의미하며, all_code_list라는 2차원 리스트(또는 다른 형태의 중첩된 데이터 구조)의 각 원소에 접근하여 처리한다.

전처리 함수 호출: clean_data() 함수와 get_rid_of_empty() 함수를 순차적으로 호출한다. 이 함수들은 각각의 이름에서 유추할 수 있듯이, 데이터를 정제하고 빈 값을 처리하는 역할을 한다. 정확한 작업 내용은 함수의 구현에 따라 다르지만, 일반적으로 불필요한 공백 제거, 잘못된 데이터 형식 수정, 결측치 처리 등의 작업을 포함할 수 있다.

결과 저장: 각 j번째 반복에서 처리된 clean 데이터는 cleans라는 리스트에 추가된다. 이렇게 내부 반복문에서 생성된 cleans 리스트는 각 i번째 반복에서 all_code_list_clean에 추가된다. 최종적으로, all_code_list_clean는 원본 all_code_list의 각 원소를 전처리한 결과를 담은 2차원 리스트가 된다.

-> 모든 C++ 코드의 주석을 제거하고 빈 줄을 없애는 전처리를 수행

In [ ]:
# 데이터 셋 생성 config 및 함수

# config
#cfg 클래스를 정의하고, 생성자(__init__)에서 checkpoint_path 변수를 초기화합니다. 이 변수는 모델 체크포인트의 경로를 저장합니다.
class cfg():
    def __init__(self) :
        self.checkpoint_path = 'neulab/codebert-cpp'
        # self.learning_rate = 3e-4
        # self.epochs = 5
        # self.num_labels=2
        # self.batch_size=16
class cfg():
    def __init__(self):
        self.checkpoint_path='neulab/codebert-cpp'
args = cfg()

# 함수
#입력 데이터프레임(input_df)에서 'code' 열의 모든 값을 리스트로 추출합니다.
#고유한 'problem_num' 값을 찾아 리스트로 만들고, 이 리스트를 정렬합니다.

#######code를 리스트로 -> 리스트를 정렬
def get_pairs(input_df, tokenizer):
    codes = input_df['code'].to_list()
    problems = input_df['problem_num'].unique().tolist()
    problems.sort()

def get_pairs(input_df,tokenizer):
    codes=Winput_df['code'].to_list()
    problems=input_df['problem_num'].unique().tolist()
    problems.sort()
    
#입력 코드를 토큰화하고, 이 토큰화된 코드들을 사용하여 BM25L 객체를 초기화합니다. BM25L은 각 코드와 다른 코드 간의 유사성 점수를 계산하는 데 사용됩니다.
    
    ######## BM25L 생성
    tokenized_corpus = [tokenizer.tokenize(code) for code in codes]
    bm25 = BM25L(tokenized_corpus)

    tokenized_corpous=[tokenizer.tokenize(code) for code in codes]
    bm25=BM25L(tokenized_corpus)

#유사한 쌍과 유사하지 않은 쌍을 저장할 리스트를 초기화
    total_positive_pairs = []
    total_negative_pairs = []

#현재 문제 번호(problem)에 해당하는 모든 코드를 선택합니다. 선택된 코드들 사이에서 가능한 모든 유사한 쌍(positive_pairs)을 생성합니다
    
    ##### 유사한 쌍 생성
    for problem in tqdm(problems):
        solution_codes = input_df[input_df['problem_num'] == problem]['code']
        positive_pairs = list(combinations(solution_codes.to_list(),2))

    for problem in tqdm(problems):
        solution_codes= input_df[input_df['problem_num']==problem]['code']
        positive_pairs=list(combinations(solution_codes.to_list(),2))

    for problem in tqdm(problems):
        solution_codes=input_df[input_df['problem_num']==problem]['code']
        positive_codes=list(combinations(solution_codes.to_list(),2))

#선택된 코드의 인덱스를 저장하고, 유사하지 않은 쌍을 저장할 빈 리스트를 초기화합니다.
        solution_codes_indices = solution_codes.index.to_list()
        negative_pairs = []

#첫 번째 유사한 쌍의 첫 번째 코드를 토큰화하고, BM25 점수를 계산한 후, 이 점수를 기준으로 내림차순 정렬하여 가장 유사도가 높은 코드의 인덱스를 구합니다.
        first_tokenized_code = tokenizer.tokenize(positive_pairs[0][0])
        negative_code_scores = bm25.get_scores(first_tokenized_code)
        negative_code_ranking = negative_code_scores.argsort()[::-1] # 내림차순
        #유사도 순위에 사용할 인덱스 변수를 초기화합니다.
        ranking_idx = 0

        first_tokenized_code=tokenizer.tokenize(positive_pairs[0][0])
        negative_code_scores=bm25.get_scores(first_tokenized_code) # bm25점수 계산
        negative_code_ranking=negative_code_scores.argsort()[::-1]#내림차순

#현재 문제의 모든 해결책 코드에 대해 반복
        for solution_code in solution_codes:
            negative_solutions = []
            while len(negative_solutions) < len(positive_pairs) // len(solution_codes):
                high_score_idx = negative_code_ranking[ranking_idx]

                if high_score_idx not in solution_codes_indices:
                    negative_solutions.append(input_df['code'].iloc[high_score_idx])
                ranking_idx += 1

            for negative_solution in negative_solutions:
                negative_pairs.append((solution_code, negative_solution))

        total_positive_pairs.extend(positive_pairs)
        total_negative_pairs.extend(negative_pairs)

    pos_code1 = list(map(lambda x:x[0],total_positive_pairs))
    pos_code2 = list(map(lambda x:x[1],total_positive_pairs))

    neg_code1 = list(map(lambda x:x[0],total_negative_pairs))
    neg_code2 = list(map(lambda x:x[1],total_negative_pairs))

    pos_label = [1]*len(pos_code1)
    neg_label = [0]*len(neg_code1)

    pos_code1.extend(neg_code1)
    total_code1 = pos_code1
    pos_code2.extend(neg_code2)
    total_code2 = pos_code2
    pos_label.extend(neg_label)
    total_label = pos_label
    pair_data = pd.DataFrame(data={
        'code1':total_code1,
        'code2':total_code2,
        'similar':total_label
    })
    pair_data = pair_data.sample(frac=1).reset_index(drop=True)
    return pair_data

->입력 코드 샘플들 사이에서 유사한 쌍과 유사하지 않은 쌍을 찾아내어, 코드 유사도를 학습할 수 있는 데이터셋을 생성

  1. 설정(config) cfg 클래스를 통해 모델 학습과 데이터 처리에 필요한 설정값을 정의한다. 여기에는 checkpoint_path 등의 모델 설정이 포함되며, 주석 처리된 부분에는 학습률(learning_rate), 에폭(epochs), 레이블 수(num_labels), 배치 크기(batch_size) 등의 학습 관련 설정이 나타난다.
  2. 데이터 셋 생성 함수(get_pairs) get_pairs 함수는 입력 데이터프레임(input_df)과 토크나이저(tokenizer)를 받아, 코드 쌍과 해당 쌍이 유사한지(양성) 혹은 다른지(음성)를 나타내는 레이블을 가진 데이터셋을 생성한다.

코드 및 문제 처리: 입력 데이터에서 코드(code)와 문제 번호(problem_num)를 추출하여 리스트로 만든다. 문제 번호는 고유한 값을 추출하여 정렬한다.

토큰화 및 BM25 적용: 입력 코드를 토크나이저를 사용해 토큰화하고, 토큰화된 코드를 기반으로 BM25 알고리즘을 사용해 유사도 점수를 계산한다. BM25는 검색 엔진 등에서 사용되는 랭킹 함수로, 문서의 중요한 특성을 고려하여 유사도를 평가한다.

양성 및 음성 쌍 생성:

양성 쌍: 같은 문제에 대한 서로 다른 솔루션 코드를 쌍으로 묶어 양성 쌍을 생성한다. 음성 쌍: 양성 쌍을 구성하는 코드 중 하나와 다른 문제의 솔루션 코드를 쌍으로 묶어 음성 쌍을 생성한다. BM25 점수를 기반으로 유사도가 높은 코드를 선택하되, 동일한 문제의 솔루션은 제외한다. 데이터프레임 생성 및 셔플링: 양성 및 음성 쌍으로부터 추출한 코드와 레이블을 합쳐 최종 데이터프레임을 생성한다. 데이터프레임은 전체를 무작위로 섞어(sample(frac=1)) 모델 학습에 사용될 수 있도록 준비한다.

In [ ]:
# 학습 데이터 셋 만들기 (각 코드 : 문제 번호)
#초기화: 빈 리스트 preproc_scripts와 problem_nums를 생성합니다. 이 리스트들은 전처리된 스크립트와 각 스크립트에 해당하는 문제 번호를 저장하는 데 사용
preproc_scripts = []
problem_nums = []

#i는 문제 번호의 인덱스, k는 특정 문제 번호에 해당
for i in range(500):
    for k in range(500):

        for i in range(500):
            for k in range(500):
        ##all_code_list_clean[i][k]를 사용하여 전처리된 스크립트를 가져온 후, preproc_scripts 리스트에 추가합니다. 여기서 preprocessed_script는 i번째 문제의 k번째 코드를 전처리한 결과입니다.
        preprocessed_script = all_code_list_clean[i][k]
        preproc_scripts.append(preprocessed_script)

        preprocess_script=all_code_list_clean[i][k]
        preproc_scripts.append(preprocess_script)
        #문제 번호 저장
        problem_nums.append(numbers[i])

#preproc_scripts와 problem_nums 리스트를 사용하여 새로운 pandas 데이터프레임 data_df를 생성
data_df = pd.DataFrame(data={'code': preproc_scripts, 'problem_num': problem_nums})

-> 전처리된 스크립트와 해당 문제 번호를 포함하는 학습 데이터셋을 만드는 과정전처리된 스크립트와 해당 문제 번호를 포함하는 학습 데이터셋을 만드는 과정

In [ ]:
# 데이터 셋 생성 config 및 함수

# config
#cfg 클래스를 정의하고, 생성자(__init__)에서 checkpoint_path 변수를 초기화합니다. 이 변수는 모델 체크포인트의 경로를 저장합니다.
class cfg():
    def __init__(self) :
        self.checkpoint_path = 'neulab/codebert-cpp'
        # self.learning_rate = 3e-4
        # self.epochs = 5
        # self.num_labels=2
        # self.batch_size=16

args = cfg()

# 함수
#입력 데이터프레임(input_df)에서 'code' 열의 모든 값을 리스트로 추출합니다.
#고유한 'problem_num' 값을 찾아 리스트로 만들고, 이 리스트를 정렬합니다.
def get_pairs(input_df, tokenizer):
    codes = input_df['code'].to_list()
    problems = input_df['problem_num'].unique().tolist()
    problems.sort()

def get_pairs(input_df,tokenizer):
    codes=input_df['code'].to_list()
    problems=input_df['problem_num'].unique().tolist()
    
#입력 코드를 토큰화하고, 이 토큰화된 코드들을 사용하여 BM25L 객체를 초기화합니다. BM25L은 각 코드와 다른 코드 간의 유사성 점수를 계산하는 데 사용됩니다.
    tokenized_corpus = [tokenizer.tokenize(code) for code in codes]
    bm25 = BM25L(tokenized_corpus)

    tokenized_corpus = [tokenizer.tokenize(code) for code in codes]
    bm25=BM25L(tokenized_corpus)

#유사한 쌍과 유사하지 않은 쌍을 저장할 리스트를 초기화
    total_positive_pairs = []
    total_negative_pairs = []

    total_positive_pairs=[]
    total_negative_pairs=[]

#현재 문제 번호(problem)에 해당하는 모든 코드를 선택합니다. 선택된 코드들 사이에서 가능한 모든 유사한 쌍(positive_pairs)을 생성합니다
    for problem in tqdm(problems):
        solution_codes = input_df[input_df['problem_num'] == problem]['code']
        positive_pairs = list(combinations(solution_codes.to_list(),2))

    for problem in tqdm(problems):
        solution_codes=input_df[input_df['problem_num']==problem]['code']
        positive_paris=list(combinations(solution_codes.to_list(),2))

#선택된 코드의 인덱스를 저장하고, 유사하지 않은 쌍을 저장할 빈 리스트를 초기화합니다.
        solution_codes_indices = solution_codes.index.to_list()
        negative_pairs = []

        solution_codes_indices=solution_codes.index.to_list()
        negative_pairs=[]

#첫 번째 유사한 쌍의 첫 번째 코드를 토큰화하고, BM25 점수를 계산한 후, 이 점수를 기준으로 내림차순 정렬하여 가장 유사도가 높은 코드의 인덱스를 구합니다.
        first_tokenized_code = tokenizer.tokenize(positive_pairs[0][0])
        negative_code_scores = bm25.get_scores(first_tokenized_code)
        negative_code_ranking = negative_code_scores.argsort()[::-1] # 내림차순

        first_tokenized_code = tokenizer.tokenize(positive_pairs[0][0])
        negative_code_scores=bm25.get_scores(first_tokenized_code)
        negative_code_ranking=negative_code_scores.argsort()[::-1]
        #유사도 순위에 사용할 인덱스 변수를 초기화합니다.
        ranking_idx = 0

#현재 문제의 모든 해결책 코드에 대해 반복
        for solution_code in solution_codes:
            negative_solutions = []
            while len(negative_solutions) < len(positive_pairs) // len(solution_codes):
                high_score_idx = negative_code_ranking[ranking_idx]

                if high_score_idx not in solution_codes_indices:
                    negative_solutions.append(input_df['code'].iloc[high_score_idx])
                ranking_idx += 1

            for negative_solution in negative_solutions:
                negative_pairs.append((solution_code, negative_solution))

        total_positive_pairs.extend(positive_pairs)
        total_negative_pairs.extend(negative_pairs)

    pos_code1 = list(map(lambda x:x[0],total_positive_pairs))
    pos_code2 = list(map(lambda x:x[1],total_positive_pairs))

    neg_code1 = list(map(lambda x:x[0],total_negative_pairs))
    neg_code2 = list(map(lambda x:x[1],total_negative_pairs))

    pos_label = [1]*len(pos_code1)
    neg_label = [0]*len(neg_code1)

    pos_code1.extend(neg_code1)
    total_code1 = pos_code1
    pos_code2.extend(neg_code2)
    total_code2 = pos_code2
    pos_label.extend(neg_label)
    total_label = pos_label
    pair_data = pd.DataFrame(data={
        'code1':total_code1,
        'code2':total_code2,
        'similar':total_label
    })
    pair_data = pair_data.sample(frac=1).reset_index(drop=True)
    return pair_data

-> 입력 데이터프레임에서 코드 쌍을 생성하여 이들이 서로 유사한지 여부를 판단하는 데 필요한 데이터셋을 구성하는 과정

In [ ]:
#데이터 분리
train_code, valid_code, train_label, valid_label = train_test_split(
    data_df,
    data_df['problem_num'],
    random_state=42,
    test_size=0.1,
    stratify=data_df['problem_num']
)

train_code, valid_code, train_label, valid_label = train_test_split(
    data_df,
    data_df['problem_num'],
    random_state=42,
    test_size=0.1,
    stratify=data_df['problem_num']
)

#reset_index 메소드를 사용하여 학습 세트와 검증 세트의 인덱스를 재설정
train_code = train_code.reset_index(drop=True)
valid_code = valid_code.reset_index(drop=True)

train_code=train_code.reset_index(drop=True)
valide_code=valid_code.reset_index(drop=True)
#from_pretrained 메소드를 사용하여 사전에 학습된 토크나이저를 로드
tokenizer = AutoTokenizer.from_pretrained(args.checkpoint_path)
#(자르기) 방향을 설정
tokenizer.truncation_side = 'left'

#train_code 데이터프레임 내의 코드 쌍을 생성
final_train_df = get_pairs(train_code, tokenizer)
# final_valid_df = get_pairs(valid_code, tokenizer)

# 생성 데이터 저장
final_train_df.to_pickle("./data/train.pkl")
# final_valid_df.to_pickle("./data/val.pkl")

->사전 학습된 토크나이저를 사용하여 코드 쌍을 생성하고, 이를 훈련 및 검증 데이터셋으로 나누는 과정을 담당

In [ ]:
# 테스트 데이터 셋 만들기
test_df = pd.read_csv("./data/test.csv")

#code1'과 'code2'라는 열의 값을 numpy 배열로 추출
code1 = test_df['code1'].values
code2 = test_df['code2'].values

code1=test_df['code1'].values
code2=test_df['code2'].values
#처리된 코드를 저장할 두 개의 빈 리스트를 초기화
processed_code1 = []
processed_code2 = []

#code1' 배열의 길이만큼 반복문을 실행하여, 각 코드 쌍에 대해 전처리를 수행
for i in range(len(code1)):
for i in range(len(code1)):

        #code1[i]'와 'code2[i]'에 대해 clean_data 함수를 호출하여 데이터를 청소한 후, get_rid_of_empty 함수를 호출하여 비어 있는 부분을 제거
        processed_c1 = get_rid_of_empty(clean_data(code1[i]))
        processed_c2 = get_rid_of_empty(clean_data(code2[i]))
        
        processed_c1=get_rid_of_empty(clean_data(code1[i]))
        processed_c2=get_rid_of_empty(clean_data(code2[i]))

        #processed_code1과 processed_code2 리스트에 추가
        processed_code1.append(processed_c1)
        processed_code2.append(processed_c2)
        
#새 DataFrame을 만듭니다.
processed_test = pd.DataFrame(list(zip(processed_code1, processed_code2)), columns=["code1", "code2"])
processed_test.to_pickle("./data/processed_test.pkl")

-> 테스트 데이터의 빈 공간을 제거하여 전처리 하고 새로운 dataframe 을 만드는 코드

데이터 분할

In [ ]:
# 데이터 불러오기

train_data = pd.read_pickle("./data/train.pkl")
# val_data = pd.read_pickle("./data/val.pkl")
In [ ]:
# 데이터 추출(랜덤)
#data_splitter라는 함수를 정의합니다. 이 함수는 데이터프레임 df와 추출할 샘플 크기 size를 입력
def data_splitter(df, size):

def data_splitter(df,size):

    # 'similar' 열의 값이 0인 샘플들 중에서 무작위로 size // 2 만큼 추출
    label_0_df = df[df['similar'] == 0].sample(size // 2)
    label_0_idx = label_0_df.index

    label_0_df=df[df['similar']==0].sample(size//2)
    label_0_idx=label_0_df.index

    label_0_df=df[df['similar']==0].sample(size//2)
    label_0_idx=label_0_df.index

    #'similar' 값이 1인 샘플들을 무작위로 size // 2 만큼 추출
    label_1_df = df[df['similar'] == 1].sample(size // 2)
    label_1_idx = label_1_df.index

    #label_0_df와 label_1_df 데이터프레임을 하나로 합쳐 sampled_df라는 새로운 데이터프레임을 생성
    sampled_df = pd.concat([label_0_df, label_1_df], axis=0)

    sampled_df=pd.concat([label_0_df,label_1_df],axis=0)
    smpled_df=pd.concat([label_0_df,label_1_df],axis=0)
    
    #label_0_idx와 label_1_idx 리스트를 합쳐서 sampled_idx라는 새로운 리스트를 생성
    sampled_idx = list(label_0_idx) + list(label_1_idx)

    return sampled_idx, sampled_df

# 추출한 후 데이터 셋을 초기화
def splitted_original(origin, idx):
    origin = origin.drop(idx)
    return origin

-> 이 두 함수는 데이터셋을 레이블별로 균등하게 분할하고, 특정 부분을 추출한 후 원본 데이터셋에서 해당 부분을 제거하는 전처리 작업을 위해 사용

In [ ]:
train_size = 5000000

for i in range(1, 9):

    #train_size 만큼의 샘플을 랜덤하게 추출
    train_idx, train_df = data_splitter(train_data, train_size)
    
    train_idx,train_df=data_splitter(train_data,train_size)

    train_idx,traind_df=data_splitter(train_data,train_size)

    #train_idx에 해당하는 샘플을 제거
    train_data = splitted_original(train_data, train_idx)

    train_data=splitted_original(train_data,train_idx)
    train_df.to_pickle(f'./data/train{i}.pkl')

    train_data=splitted_orginal(tarin_data,train_idx)
    train_df.to_prickle(f'./data/train{i}.pkl')

-> 이 프로세스를 통해 큰 데이터셋을 여러 개의 작은 파일로 분할하여 저장

In [ ]:
# val_size = 100000

# valid_idx, valid_df = data_splitter(val_data, val_size)
# val_data = splitted_original(val_data, valid_idx)
# valid_df.to_pickle(f'./data/valid.pkl')

Train

  • 모델은 neulab/codebert-cp
  • 다른 데이터 8개로 모델 파인 튜닝
  • DataParallel 시 loss 부분이랑 모델 저장 부분에서 주의
In [ ]:
device = torch.device('cuda')if torch.cuda.is_available() else torch.device('cpu')
device

하이퍼 파라미터 설정

In [ ]:
import os
os.getcwd()
In [ ]:
class config():
    def __init__(self):

        self.source_len=512
        self.epochs = 1

        #학습률(learning rate)을 0.00002로 설정
        self.learning_rate=2e-5

        #배치 크기(batch size)를 32로 설정
        self.batch_size=32

        #데이터를 섞을지 여부를 결정하는 shuffle 플래그를 True로 설정
        self.shuffle = True

        #랜덤 시드를 2022로 설정
        self.seed=2022

        #모델의 출력 레이블 수를 2로 설정
        self.num_labels=2
        self.checkpoint_path = 'neulab/codebert-cpp'
        self.train_path1 = './data/train1.pkl'
        # self.train_path2 = './data/train2.pkl'
        # self.train_path3 = './data/train3.pkl'
        # self.train_path4 = './data/train4.pkl'
        # self.train_path5 = './data/train5.pkl'
        # self.train_path6 = './data/train6.pkl'
        # self.train_path7 = './data/train7.pkl'
        # self.train_path8 = './data/train8.pkl'
        
        # self.hf_data_path1= 'emaeon/train1'
        # self.hf_data_path2= 'emaeon/train2'
        # self.hf_data_path3= 'emaeon/train3'
        # self.hf_data_path4= 'emaeon/train4'
        # self.hf_data_path5= 'emaeon/train5'
        # self.hf_data_path6= 'emaeon/train6'
        # self.hf_data_path7= 'emaeon/train7'
        # self.hf_data_path8= 'emaeon/train8'

cfg = config()

-> 이 클래스 인스턴스는 머신 러닝 모델을 학습시킬 때 필요한 다양한 구성 옵션을 저장하는데 사용

랜덤 시드 고정

In [ ]:
def seed_everything(seed):
    random.seed(seed)
    os.environ['PYTHONHASHSEED']=str(seed)
    np.random.seed(seed)
    torch.manual_seed(seed)
    torch.cuda.manual_seed(seed)
    torch.backends.cudnn.deterministic=True
    torch.backends.cudnn.bechmark = True

seed_everything(cfg.seed) #seed 고정

모델, 토크나이저 호출

In [ ]:
# 허깅페이스에서 사전학습된 모델 불러옵니다

#이 모델은 시퀀스 분류 작업(예: 문장이나 문서의 분류)에 사용됩니다
model = AutoModelForSequenceClassification.from_pretrained(cfg.checkpoint_path, num_labels=cfg.num_labels, output_hidden_states=False).to(device)

model= AutoModelForSequenceClassification.from_pretrained(cfg.checkpoint_path,num_labels=cfg.num_labels,output_hidden_states=False).to(device)

#토크나이저는 텍스트를 모델이 처리할 수 있는 형태로 변환하는 데 사용
tokenizer = AutoTokenizer.from_pretrained(cfg.checkpoint_path,)

tokenizer=AutoTokenizer.from_pretrained(cfg.checkpoint_path)

#토크나이저의 트렁케이션(자르기) 방향을 "left"로 설정
tokenizer.truncation_side = "left"

tokenizer.truncation_side='left'

tokenizer.truncation_side='left'

#모델의 토큰 임베딩 크기를 토크나이저의 어휘 크기에 맞게 조정
model.resize_token_embeddings(len(tokenizer))

model.resize_token_embeddings(len(tokenizer))

model.resize_token_embeddings(len(tokenizer))
'''DataParallel이 필요할 경우에는 아래 코드를 실행해야 합니다'''
# model = nn.DataParallel(model).to(device) 

-> 사전 학습된 모델과 토크나이저를 로드하고, 모델을 현재 작업 환경에 맞게 조정

In [ ]:
model #모델 확인

Dataset 커스터마이징

In [ ]:
class CustomDataset(Dataset):
    
    #클래스의 생성자(__init__)는 데이터셋을 초기화할 때 필요한 파라미터들을 받습니다.
    def __init__(self, data_a,data_b, labels, tokenizer, source_len) :

    def __init__(self,data_a,data b, labels, tokenizer, source_len):

    # 초기화 과정에서 입력으로 받은 값들을 클래스 인스턴스의 속성으로 복사하고 저장
        self.data_a = data_a.copy()
        self.data_b = data_b.copy()
        self.labels = labels.copy()
        self.tokenizer = tokenizer
        self.source_len = source_len

        self.data_a= data_a.copy()
        self.data_b=data_b.copy()
        self.labels=labels.copy()
        self.tokenizer=tokenizer
        self.source_len=source_len

    def __getitem__(self, index) :
    # 데이터 셋에서 한 개의 데이터를 가져오는 함수 정의
        text1 = self.data_a[index]
        text2 = self.data_b[index]

        text1=self.data_a[index]
        text2= self.data_b[index]
        
        '''text_pair에 비교할 문장을 입력하면 알아서 sep토큰으로 문장 구분된 하나의 입력 셋이 형성됩니다.'''
        #tokenizer를 사용하여 두 텍스트(text1, text2)를 토크나이징
        inputs = self.tokenizer(text = text1,text_pair=text2,max_length=self.source_len,padding='max_length',truncation=True, return_tensors='pt') 
        label = self.labels[index]

        inputs= self.tokenizer(text=text, text_pair=text2, max_length=self.source_len,pedding='max_length',truncation=True,return_tensors='pt')
        label=self.labels[index]

        '''neulab/codebert-cpp는 input_ids와 attention_mask를 입력 받습니다'''
        #input_ids와 attention_mask를 추출하고, .squeeze() 메소드를 통해 불필요한 차원을 제거
        input_ids = inputs['input_ids'].squeeze()
        attention_mask = inputs['attention_mask'].squeeze()

        input_ids=inputs['input_ids'].squueze()
        attention_mask=inputs['input_ids'].squeeze()

        #input_ids와 attention_mask를 딕셔너리로 구성하고, .to(device)를 통해 해당 텐서들을 계산을 수행할 장치(CPU 또는 GPU)로 이동
        inputs_dict = {
            'input_ids' : input_ids.to(device, dtype = torch.long),
            'attention_mask' : attention_mask.to(device, dtype = torch.long),
        }
        label = torch.tensor(label).to(device, dtype = torch.long),
    
        return inputs_dict, label #

        inputs_dict = {
            'input_ids' : input_ids.to(device, dtype = torch.long),
            'attention_mask' : attention_mask.to(device, dtype = torch.long),
        }
        label = torch.tensor(label).to(device, dtype = torch.long),
    
        return inputs_dict, label #

    def __len__(self) :
    # 데이터 셋의 길이
        return len(self.labels)

-> 두텍스트 사이의 관계나 유사도를 파악하는 클래스를 만드는 코드

In [ ]:
traindf = pd.read_pickle(cfg.train_path1)
# traindf = pd.read_pickle(cfg.train_path2)
# traindf = pd.read_pickle(cfg.train_path3)
# traindf = pd.read_pickle(cfg.train_path4)
# traindf = pd.read_pickle(cfg.train_path5)
# traindf = pd.read_pickle(cfg.train_path6)
# traindf = pd.read_pickle(cfg.train_path7)
# traindf = pd.read_pickle(cfg.train_path8)
In [ ]:
"""허깅페이스에 올려놓은 데이터를 가져오려면 다음과 같이 해야 합니다"""
# from datasets import load_dataset
# dataset = load_dataset(cfg.hf_data_path1)
# traindf = dataset['train'].to_pandas()


# dataset = load_dataset(cfg.hf_data_path2)
# dataset = load_dataset(cfg.hf_data_path3)
# dataset = load_dataset(cfg.hf_data_path4)
# dataset = load_dataset(cfg.hf_data_path5)
# dataset = load_dataset(cfg.hf_data_path6)
# dataset = load_dataset(cfg.hf_data_path7)
# dataset = load_dataset(cfg.hf_data_path8)
In [ ]:
traindf
In [ ]:
train_data = CustomDataset(data_a=list(traindf['code1']),data_b=list(traindf['code2']),
                           labels=list(traindf['similar']),tokenizer=tokenizer,source_len=cfg.source_len)
train_loader = DataLoader(train_data, batch_size=cfg.batch_size, shuffle=cfg.shuffle,num_workers=0)

Train 함수 정의

In [ ]:
#elapsed라는 인자를 받는데, 이는 경과 시간을 초 단위로 나타낸 값
def format_time(elapsed):

    #입력된 경과 시간(elapsed)을 가장 가까운 정수로 반올림
    elapsed_rounded = int(round((elapsed)))

    #datetime.timedelta 객체는 일, 시간, 분, 초를 나타내는데 사용
    return str(datetime.timedelta(seconds=elapsed_rounded))

def format_time(elapsed):
    elapsed_rounded = int(round((elapsed))
    reuturn str(datetime.timedelta(seconds=elapsed_rounded)))
    
def format_time(elapsed):
    elapsed_rounded=int(round((elapsed)))
    return str(datetime.timedelta(seconds=elapsed_rounded))

-> 경과 시간(초 단위)을 받아서, 이를 HH:MM:SS 형태의 문자열로 포맷팅하여 반환

In [ ]:
def train(epoch, model, optimizer, loader):

    #모델을 학습 모드로 설정
    model.train()

    #총 손실(total_loss), 총 정확도(total_accuracy), 그리고 학습 스텝 수(nb_train_steps)를 초기화
    total_loss, total_accuracy = 0,0
    nb_train_steps = 0
    
    #tqdm은 진행 상황을 시각적으로 보여주는 라이브러리
    for _,(inputs, labels) in tqdm(enumerate(loader, 0)):
        
        #모델에 입력 데이터(inputs)와 레이블(labels)을 전달하여 포워드 패스를 실행
        outputs = model(**inputs, labels = labels)

        #outputs 딕셔너리에서 손실 값을 추출
        loss = outputs.loss
        '''DataParallel은 loss가 3개가 나오기 때문에 평균해야 합니다'''
        # loss = outputs.loss.mean()

        #모델 출력(outputs.logits)에서 예측된 클래스를 추출하고, 실제 레이블과 비교하여 정확도를 계산
        pred = [logit.argmax().cpu().detach().item() for logit in outputs.logits]
        true = [label for label in labels.cpu().numpy()]
        acc = accuracy_score(true,pred)

        #매 50번째 스텝마다 현재의 평균 손실과 경과 시간을 출력
        if _ % 50 == 0 and not _ == 0: #50iter마다 loss 확인하고자 넣었습니다.
            elapsed = format_time(time.time() - t0)
            print('  Batch {:>5,}  of  {:>5,}.    Elapsed: {:}.'.format(_, len(loader), elapsed))
            print('  current average loss = {}'.format(
                total_loss / _))
        if _%50==0  and not _ == 0 :
            print(f'Epoch : {epoch}, train_{_}_step_loss : {loss.item()}')
            psuedo_pred = [logit.argmax().item() for logit in outputs.logits]
            psuedo_acc = np.sum(np.array(labels.to('cpu'))==np.array(psuedo_pred))/len(labels)
            print(f'{epoch}_{_}_step_정확도 :{psuedo_acc}')
        if _%15625==0 and not _ == 0: #런타임 오류가 생길 경우 모델이 날라갈 것을 방지하고자 했습니다.
            torch.save(model.state_dict(), f'/data/{_}batch_trained_cppbert1.pt')
            '''DataParallel후 저장하는 방법이 약간 다릅니다'''
            # torch.save(model.module.state_dict(), f'/data/{_}batch_trained_cppbert.pt')


        loss.backward()
        optimizer.step()
        optimizer.zero_grad()

        #총 손실, 총 정확도를 업데이트하고, 학습 스텝 수를 증가
        total_loss += loss.item()
        total_accuracy += acc
        nb_train_steps += 1

    #전체 손실(total_loss)을 데이터 로더(loader)의 길이(즉, 총 배치 수)로 나누어 평균 손실(avg_loss)을 계산
    avg_loss = total_loss/len(loader)

    #총 정확도(total_accuracy)를 학습 스텝 수(nb_train_steps)로 나누어 평균 정확도(avg_acc)를 계산
    avg_acc = total_accuracy/nb_train_steps

    #총 정확도(total_accuracy)를 데이터 로더(loader)의 길이로 나누어 다른 형태의 평균 정확도(t_test_avg_acc)를 계산
    t_test_avg_acc = total_accuracy/len(loader)

    print(f'Epoch:{epoch}, train_{_}_stepLoss:{avg_loss}')
    print(f'Epoch:{epoch}, train_{_}_stepacc:{avg_acc}')
    print(f'Epoch:{epoch}, train_{_}_stepacc:{t_test_avg_acc}')
    loss_dic['train_loss'].append(avg_loss)
    loss_dic['train_acc'].append(avg_acc)

-> 모델 학습 과정에서 손실을 계산하고, 가중치를 업데이트하며, 진행 상황을 모니터링

RUN

In [ ]:
# AdamW 최적화 알고리즘의 인스턴스를 생성
optimizer = torch.optim.AdamW(params = model.parameters(), lr=cfg.learning_rate)

#코사인 애닐링 웜 리스타트(Cosine Annealing Warm Restarts) 학습률 스케줄러의 인스턴스를 생성
scheduler = torch.optim.lr_scheduler.CosineAnnealingWarmRestarts(optimizer, T_0=10, T_mult=2, eta_min=0.01, last_epoch=-1)

-> 고급 최적화 기법과 학습률 조정 전략을 적용

In [ ]:
loss_dic = {'epoch':[],'train_loss':[], 'validation_loss':[],'train_acc':[],'val_acc':[]}



for epoch in tqdm(range(1,cfg.epochs+1)):
    t0 = time.time()
    train(epoch, model, optimizer, train_loader)
    torch.save(model.state_dict(), './data/trained_cppbert1.pt')
    # torch.save(model.state_dict(), './data/trained_cppbert2.pt')
    # torch.save(model.state_dict(), './data/trained_cppbert3.pt')
    # torch.save(model.state_dict(), './data/trained_cppbert4.pt')
    # torch.save(model.state_dict(), './data/trained_cppbert5.pt')
    # torch.save(model.state_dict(), './data/trained_cppbert6.pt')
    # torch.save(model.state_dict(), './data/trained_cppbert7.pt')
    # torch.save(model.state_dict(), './data/trained_cppbert8.pt')
    
    '''DataParallel후 저장하는 방법이 약간 다릅니다'''
    # torch.save(model.module.state_dict(), './data/trained_cppbert1.pt')  

    scheduler.step()

-> 설정된 에폭 수만큼 모델을 학습시키고, 각 에폭마다 모델의 상태를 저장한 후, 학습률 스케줄러를 업데이트

Test

In [ ]:
device = torch.device('cuda')if torch.cuda.is_available() else torch.device('cpu')
device

#CUDA 장치(즉, GPU)를 설정

하이퍼 파라미터 설정

In [ ]:
class config():
    def __init__(self):

        #입력 데이터의 최대 길이를 512로 설정
        self.source_len=512

        #학습 또는 추론을 위해 한 번에 처리할 데이터 샘플의 수를 16으로 설정
        self.batch_size=16

        #데이터를 모델에 공급하기 전에 데이터를 섞을지 여부를 결정
        self.shuffle = True

        #실험의 재현성을 위해 난수 생성 시 사용되는 시드 값을 2022로 설정
        self.seed=2022

        #모델이 예측해야 하는 레이블의 수를 2로 설정
        self.num_labels=2
        
        self.load_path1= './data/trained_cppbert1.pt'
        # self.load_path2= './data/trained_cppbert2.pt'
        # self.load_path3= './data/trained_cppbert3.pt'
        # self.load_path4= './data/trained_cppbert4.pt'
        # self.load_path5= './data/trained_cppbert5.pt'
        # self.load_path6= './data/trained_cppbert6.pt'
        # self.load_path7= './data/trained_cppbert7.pt'
        # self.load_path8= './data/trained_cppbert8.pt'
        
        # self.hf_load_path1= 'emaeon/trained_cppbert1'
        # self.hf_load_path1= 'emaeon/trained_cppbert2'
        # self.hf_load_path1= 'emaeon/trained_cppbert3'
        # self.hf_load_path1= 'emaeon/trained_cppbert4'
        # self.hf_load_path1= 'emaeon/trained_cppbert5'
        # self.hf_load_path1= 'emaeon/trained_cppbert6'
        # self.hf_load_path1= 'emaeon/trained_cppbert7'
        # self.hf_load_path1= 'emaeon/trained_cppbert8'
        
        self.checkpoint_path = 'neulab/codebert-cpp'
        self.test_path = './data/processed_test.pkl'
        
cfg = config()

-> 설정 클래스를 사용함으로써, 학습 및 테스트 파이프라인을 구축할 때 필요한 모든 설정 값을 한 곳에서 관리

모델, 토크나이저 호출

In [ ]:
from transformers import AutoTokenizer, RobertaForSequenceClassification, AutoModelForSequenceClassification

model = AutoModelForSequenceClassification.from_pretrained(cfg.checkpoint_path, num_labels=cfg.num_labels, output_hidden_states=False,ignore_mismatched_sizes=True).to(device)
tokenizer = AutoTokenizer.from_pretrained(cfg.checkpoint_path)
tokenizer.truncation_side = "left"
model.resize_token_embeddings(len(tokenizer))

-> transformers 라이브러리를 사용하여 사전 훈련된 시퀀스 분류 모델을 로드하고, 해당 모델을 특정 작업에 맞게 조정하는 과정

In [ ]:
model.load_state_dict(torch.load(cfg.load_path1))
# model.load_state_dict(torch.load(cfg.load_path2))
# model.load_state_dict(torch.load(cfg.load_path3))
# model.load_state_dict(torch.load(cfg.load_path4))
# model.load_state_dict(torch.load(cfg.load_path5))
# model.load_state_dict(torch.load(cfg.load_path6))
# model.load_state_dict(torch.load(cfg.load_path7))
# model.load_state_dict(torch.load(cfg.load_path8))
model.eval()

-> 모델을 평가 모드로 전환하는 과정

In [ ]:
"""local이 아닌 허깅페이스에 저장된 모델 불러오려면 다음과 같이 해야 합니다"""
# model = AutoModelForSequenceClassification.from_pretrained(cfg.hf_load_path1, num_labels=cfg.num_labels, output_hidden_states=False).to(device)
# tokenizer = AutoTokenizer.from_pretrained(cfg.checkpoint_path,)

# tokenizer.truncation_side = "left"
# model.resize_token_embeddings(len(tokenizer))
'''DataParallel이 필요할 경우에는 아래 코드를 실행해야 합니다'''
# model = nn.DataParallel(model).to(device) 

Test data tokenizing

In [ ]:
test_data = pd.read_pickle(cfg.test_path)
In [ ]:
#code1과 code2 열의 값을 numpy 배열로 추출
c1 = test_data['code1'].values
c2 = test_data['code2'].values

#N은 테스트 데이터셋의 샘플 수를 나타내며, MAX_LEN은 모델 입력에 사용될 시퀀스의 최대 길이를 설정
N = test_data.shape[0]
MAX_LEN = 512

#test_input_ids와 test_attention_masks를 각각 N x MAX_LEN 크기의 정수형(numpy int) 0 행렬로 초기화
test_input_ids = np.zeros((N, MAX_LEN), dtype=int)
test_attention_masks = np.zeros((N, MAX_LEN), dtype=int)

#진행 상황을 보여주는 tqdm 라이브러리를 사용하여 N번 반복
for i in tqdm(range(N), position=0, leave=True):
    try:
        cur_c1 = str(c1[i])
        cur_c2 = str(c2[i])

        #토크나이저를 사용하여 cur_c1과 cur_c2 코드 조각을 토큰화하고, PyTorch 텐서로 변환된 입력을 생성
        encoded_input = tokenizer(cur_c1, cur_c2, return_tensors='pt', max_length=512, padding='max_length',
                                    truncation=True)
        
        #토큰화된 입력에서 input_ids와 attention_mask를 추출하여 각각 test_input_ids와 test_attention_masks의 i번째 행에 할
        test_input_ids[i,] = encoded_input['input_ids']
        test_attention_masks[i,] = encoded_input['attention_mask']

    except Exception as e:
        print(e)
        pass

test_input_ids = torch.tensor(test_input_ids, dtype=int)
test_attention_masks = torch.tensor(test_attention_masks, dtype=int)

-> 두 코드 조각(code1과 code2)을 포함하는 테스트 데이터셋에 대해 입력 ID와 주의(attention) 마스크를 생성하는 과정을 설명

In [ ]:
'''토큰화 작업에 시간이 오래걸려 토큰 파일만 따로 저장해두고 불러와서 추론했습니다'''
torch.save(test_input_ids, "./data/test_input_ids.pt")
torch.save(test_attention_masks, "./data/test_attention_masks.pt")
In [ ]:
test_input_ids=torch.load('./data/test_input_ids.pt')
test_attention_masks=torch.load('./data/test_attention_masks.pt')

Inference

In [ ]:
# model.cuda()

#TensorDataset 클래스를 사용하여 test_input_ids와 test_attention_masks로부터 텐서 데이터셋을 생성
test_tensor = TensorDataset(test_input_ids, test_attention_masks)

#SequentialSampler를 사용하여 테스트 데이터셋의 샘플링 방법을 순차적으로 설정
test_sampler = SequentialSampler(test_tensor)

#DataLoader를 사용하여 배치 크기가 16인 테스트 데이터 로더를 생성
test_dataloader = DataLoader(test_tensor, sampler=test_sampler, batch_size=16)

submission = pd.read_csv("./data/sample_submission.csv")

# 모델 출력(로짓)을 저장할 리스트를 초기
logits_list = [] #soft voting을 위한 리스트
preds = np.array([]) #최종 출력값(레이블)

#반복문을 통해 test_dataloader에서 배치를 순차적으로 가져와 모델로부터 예측을 수행
for step, batch in tqdm(enumerate(test_dataloader), desc="Iteration", smoothing=0.05):

    #각 배치를 설정된 디바이스(GPU 또는 CPU)로 이동
    batch = tuple(t.to(device) for t in batch)

    #배치에서 입력 ID와 주의 마스크를 추출
    b_input_ids, b_input_mask = batch

    #이 구문 내에서 모델의 예측을 수행할 때, PyTorch가 자동으로 계산 그래프를 생성하는 것을 방지하여 메모리 사용량을 감소시키고 계산 속도를 향상
    with torch.no_grad():

        #모델에 입력 ID와 주의 마스크를 제공하여 예측을 수행
        outputs = model(b_input_ids, attention_mask=b_input_mask)
    
    '''soft voting을 위한 logit값'''

    #모델 출력 중 첫 번째 요소를 로짓으로 추출
    logits = outputs[0]

    #로짓을 계산 그래프로부터 분리하고, CPU로 이동
    logits = logits.detach().cpu()

    #분리된 로짓을 logits_list에 추가
    logits_list.append(logits)
    
    '''최종 출력label(0 & 1)'''
    _pred = logits.numpy()
    pred = np.argmax(_pred, axis=1).flatten()
    preds = np.append(preds, pred)
    
submission['similar'] = preds
all_logits = torch.cat(logits_list, dim=0)

-> 테스트 데이터셋에 대한 모델의 예측을 수행하고, 그 결과를 제출 파일 형식으로 저장

In [ ]:
torch.save(all_logits, "./data/all_logits_model1.pt")
# torch.save(all_logits, "./data/all_logits_model2.pt")
# torch.save(all_logits, "./data/all_logits_model3.pt")
# torch.save(all_logits, "./data/all_logits_model4.pt")
# torch.save(all_logits, "./data/all_logits_model5.pt")
# torch.save(all_logits, "./data/all_logits_model6.pt")
# torch.save(all_logits, "./data/all_logits_model7.pt")
# torch.save(all_logits, "./data/all_logits_model8.pt")
In [ ]:
submission.to_csv('./data/submission' +'model1.csv', index=False)
# submission.to_csv('./data/submission' +'model2.csv', index=False)
# submission.to_csv('./data/submission' +'model3.csv', index=False)
# submission.to_csv('./data/submission' +'model4.csv', index=False)
# submission.to_csv('./data/submission' +'model5.csv', index=False)
# submission.to_csv('./data/submission' +'model6.csv', index=False)
# submission.to_csv('./data/submission' +'model7.csv', index=False)
# submission.to_csv('./data/submission' +'model8.csv', index=False)

Ensemble(Soft Voting)

  • Soft Voting 전 Logit 값들을 모두 Softmax 통과 시킵니다.
In [ ]:
import torch
import numpy as np
import pandas as pd

submission = pd.read_csv("./data/sample_submission.csv")

#logits_1부터 logits_8까지 각 줄은 모델 1부터 모델 8까지 각각의 로짓 파일을 불러온 후, PyTorch의 softmax 함수를 사용하여 로짓을 확률로 변환
logits_1 = torch.nn.functional.softmax(torch.load("./data/all_logits_model1.pt"))
logits_2 = torch.nn.functional.softmax(torch.load("./data/all_logits_model2.pt"))
logits_3 = torch.nn.functional.softmax(torch.load("./data/all_logits_model3.pt"))
logits_4 = torch.nn.functional.softmax(torch.load("./data/all_logits_model4.pt"))
logits_5 = torch.nn.functional.softmax(torch.load("./data/all_logits_model5.pt"))
logits_6 = torch.nn.functional.softmax(torch.load("./data/all_logits_model6.pt"))
logits_7 = torch.nn.functional.softmax(torch.load("./data/all_logits_model7.pt"))
logits_8 = torch.nn.functional.softmax(torch.load("./data/all_logits_model8.pt"))

->예측 확률을 불러오고, 이를 확률로 변환하여 나중에 이러한 확률을 기반으로 최종 예측을 결정하는 데 사용

In [ ]:
#여덟 개 모델의 소프트맥스 확률(logits_1부터 logits_8까지)을 모두 더한 후, 8로 나누어 평균을 계산
logits = (logits_1 + logits_2 + logits_3 + logits_4 + logits_5 + logits_6 + logits_7 + logits_8) / 8

# logits를 NumPy 배열로 변환
logits_np = logits.numpy()

#np.argmax 함수를 사용하여 변환된 NumPy 배열 logits_np의 각 행(각 예측 확률 벡터)에 대해 가장 높은 값을 가진 인덱스를 찾습니다.
pred = np.argmax(logits_np, axis=1).flatten()

-> 여러 모델의 예측을 통합하여 최종 예측을 생성

In [ ]:
submission['similar'] = pred
submission.to_csv('./data/final_soft_pred.csv', index=False)