preprocessing, datasets 베이스라인 탐구
베이스라인이나 공부해보자.
순서는 아래로 하나하나씩 한줄씩 공부해보자.
preprocessing.py 부터 보는 것이 좋겠습니다.
데이터가 어떻게 전처리되는지 이해하는 것이 첫 단계입니다 메타데이터(감독, 장르, 작가 등)가 어떻게 처리되는지 볼 수 있습니다
datasets.py
전처리된 데이터가 어떤 형태로 모델에 입력되는지 이해할 수 있습니다 데이터 로딩 및 배치 생성 과정을 볼 수 있습니다
modules.py & models.py
모델의 기본 구성 요소와 전체 아키텍처를 이해할 수 있습니다
trainers.py & run_train.py
실제 학습이 어떻게 이루어지는지 볼 수 있습니다
inference.py
학습된 모델이 어떻게 추천을 생성하는지 이해할 수 있습니다
utils.py
보조함수
preprocessing.py
import pandas as pd
def main():
genres_df = pd.read_csv("../data/train/genres.tsv", sep="\t")
# genres_df의 "genre" 컬럼을 수치형으로 변환합니다
# pd.factorize는 문자열 카테고리를 0부터 시작하는 정수로 인코딩합니다
# 예: 'Action' -> 0, 'Drama' -> 1, 'Comedy' -> 2 등
# array는 변환된 숫자값들의 배열, index는 원래 장르명들의 리스트입니다
array, index = pd.factorize(genres_df["genre"])
# 변환된 숫자값들을 다시 genres_df의 "genre" 컬럼에 할당합니다
genres_df["genre"] = array
# groupby를 사용하여 각 영화(item)별로 장르 리스트를 만들고
# 이를 JSON 형태로 저장합니다
# 예: {"1": [0,1,2], "2": [1,3]} -> 영화 1번은 0,1,2번 장르를, 영화 2번은 1,3번 장르를 가짐
genres_df.groupby("item")["genre"].apply(list).to_json(
"data/Ml_item2attributes.json"
)
if __name__ == "__main__":
main()
우선은 뭐 별거 없긴하다. 그냥 genres파일을 숫자값으로 인코딩해서 json형식으로 변환한건데
인코딩해서 수치 데이터로 만들어서 딥러닝 모델에 처리가능하게 한건가? Embedding 레이어를 위하여? 근데 이게 인코딩이 순서에 따른 인코딩아닌가? 각 장르별 상관관계가 가까운 순서대로 인코딩하는게 낫지 않을까?
one-hot encoding으로 바꾸는게 낫지않을까? 근데 그러면 희소 벡터 많이 생겨서 연산량 많아질 거 같긴한데 좀 더 나으려나?
그리고 다른 director나 title같은건 인코딩 못할거같은데 어카지? 굳이 인코딩을 해야하나? 좀 자주 나오는 얘들로만 전처리해서 인코딩해버릴까?
해볼거
- genres를 원-핫인코딩으로 변경
- 다른 파일들 차원의 저주 안나도록 전처리해서 인코딩해보기
datasets.py
얜 좀 코드가 길다. 다는 못 쓸거 같고 아래와 같이 두개의 class가 있는데
# PyTorch의 Dataset 클래스를 상속받는 사전학습용 데이터셋 클래스
class PretrainDataset(Dataset):
# SASRec 모델을 위한 데이터셋 클래스
class SASRecDataset(Dataset):
PretrainDataset 클래스의 주요 특징:
- 데이터 준비
- 시퀀스 마스킹 (일부 아이템을 마스크로 대체)
- 세그먼트 예측을 위한 데이터 준비
- 부정 샘플링 (학습에 사용할 부정 예제 생성)
- 데이터 처리
- 패딩 (모든 시퀀스를 동일한 길이로 맞춤)
- 속성 정보 변환 (원-핫 인코딩)
- 텐서 변환 (PyTorch 모델 입력용)
- 검증
- 모든 시퀀스 길이가 올바른지 확인
- 데이터 형식이 올바른지 검증
SASRecDataset 클래스의 주요 특징:
- 데이터 타입별(train/valid/test/submission) 다른 처리 방식 적용
- 시계열 특성을 고려한 입력/타겟 분리
- 부정 샘플링을 통한 학습 데이터 보강
- 테스트 시 별도의 부정 샘플 사용 가능
사실 위에가 뭔 소린지 모르겠음. ㅋㅋ
PretrainDataset class가 실제로 어떻게 동작하는가?
user,item,time
11,4643,1230782529
11,170,1230782534
11,531,1230782539
11,616,1230782542
11,2140,1230782563
11,2722,1230782583
...
이런 데이터가 들어왔다고 치면
- 시퀀스 데이터 구성
User 11의 시청 시퀀스:
[4643 → 170 → 531 → 616 → 2140 → 2722 → 2313 → 2688 → 2428 → 3113 → 1591 → 2600 → 8169 → 2572]
- split_sequence() -> 시퀀스를 부분 시퀀스로 분할합니다. 예를 들어 max_len이 5라고 가정하면:
첫번째 부분: [4643]
두번째 부분: [4643, 170]
세번째 부분: [4643, 170, 531]
네번째 부분: [4643, 170, 531, 616]
다섯번째 부분: [4643, 170, 531, 616, 2140]
...
- getitem() -> 부분 시퀀스 [4643, 170, 531, 616, 2140]를
# A) Masked Item Prediction
# 원본
sequence = [4643, 170, 531, 616, 2140]
# 마스킹 확률(mask_p)이 0.2라고 가정
masked_sequence = [4643, MASK, 531, MASK, MASK]
# 부정 샘플링 (사용자가 보지 않은 다른 영화 ID)
neg_items = [4643, 999, 531, 888, 777]
# B) Segment Prediction
# 랜덤하게 길이 2의 세그먼트를 선택했다고 가정
원본: [4643, 170, 531, 616, 2140]
start_id = 1 (랜덤 선택)
masked_segment = [4643, MASK, MASK, 616, 2140]
pos_segment = [MASK, 170, 531, MASK, MASK] # 실제 세그먼트
neg_segment = [MASK, 999, 888, MASK, MASK] # 다른 무작위 세그먼트
# C) Attribute Prediction
# 각 영화의 속성을 원-핫 인코딩으로 변환
# 속성 크기가 5라고 가정
4643의 속성: [1, 0, 1, 0, 1] # 예: 액션(1), SF(3), 스릴러(5) 장르
170의 속성: [0, 1, 1, 0, 0] # 예: 드라마(2), SF(3) 장르
531의 속성: [1, 1, 0, 0, 1]
...
attributes = [
[1, 0, 1, 0, 1], # 4643 영화의 속성
[0, 1, 1, 0, 0], # 170 영화의 속성
[1, 1, 0, 0, 1], # 531 영화의 속성
...
]
- 모든 시퀀스의 길이를 동일하게 맞추기 위해 패딩처리를 해줍니다.
# 사용자 A의 시청 기록
[4643, 170, 531] # 길이 3
# 사용자 B의 시청 기록
[4643, 170, 531, 616, 2140] # 길이 5
# 사용자 C의 시청 기록
[4643, 170, 531, 616, 2140, 2722, 2313] # 길이 7
위와 같은 시청 기록을
# 사용자 A의 시청 기록 (패딩 후)
[0, 0, 0, 0, 4643, 170, 531] # 앞쪽에 0으로 패딩
# 사용자 B의 시청 기록 (패딩 후)
[0, 0, 4643, 170, 531, 616, 2140] # 앞쪽에 0으로 패딩
# 사용자 C의 시청 기록 (패딩 필요 없음)
[4643, 170, 531, 616, 2140, 2722, 2313] # 이미 길이가 7
다른 길이의 시퀀스들을 딥러닝 모델을 위해 같은 크기로 맞춰줍니다.
-
데이터를 최종적으로 반환해줍니다.
return ( torch.tensor(attributes), # 영화 속성 정보 torch.tensor(masked_sequence), # 마스킹된 시퀀스 torch.tensor(pos_items), # 원본 시퀀스 torch.tensor(neg_items), # 부정 샘플 시퀀스 torch.tensor(masked_segment), # 마스킹된 세그먼트 torch.tensor(pos_segment), # 실제 세그먼트 torch.tensor(neg_segment) # 부정 세그먼트 )
SASRecDataset class가 어떻게 동작하는가?
user_id=11의 시청 기록:
[4643, 170, 531, 616, 2140, 2722, 2313, 2688, 2428, 3113, 1591, 2600, 8169, 2572]
이 데이터가 각 데이터 타입(“train”, “valid”, “test”, “submission”)별로 어떻게 처리되는지 살펴보겠습니다.
- “train” 모드
# data_type="train"일 때
input_ids = [4643, 170, 531, 616, 2140, 2722, 2313, 2688, 2428, 3113, 1591] # 마지막 3개 제외
target_pos = [170, 531, 616, 2140, 2722, 2313, 2688, 2428, 3113, 1591, 2600] # input_ids에서 한 칸씩 밀린 것
target_neg = [999, 888, 777, 666, 555, 444, 333, 222, 111, 000, 123] # 랜덤 샘플링된 부정 예시
answer = [0] # 학습시에는 사용안함
- “valid” 모드
# data_type="valid"일 때
input_ids = [4643, 170, 531, 616, 2140, 2722, 2313, 2688, 2428, 3113, 1591, 2600] # 마지막 2개 제외
target_pos = [170, 531, 616, 2140, 2722, 2313, 2688, 2428, 3113, 1591, 2600, 8169] # input_ids에서 한 칸씩 밀린 것
target_neg = [999, 888, 777, 666, 555, 444, 333, 222, 111, 000, 123, 456] # 랜덤 샘플링된 부정 예시
answer = [8169] # 검증용 정답
-
“test” 모드
# data_type="test"일 때 input_ids = [4643, 170, 531, 616, 2140, 2722, 2313, 2688, 2428, 3113, 1591, 2600, 8169] # 마지막 1개 제외 target_pos = [170, 531, 616, 2140, 2722, 2313, 2688, 2428, 3113, 1591, 2600, 8169, 2572] # input_ids에서 한 칸씩 밀린 것 target_neg = [999, 888, 777, 666, 555, 444, 333, 222, 111, 000, 123, 456, 789] # 랜덤 샘플링된 부정 예시 answer = [2572] # 테스트용 정답
-
“submission” 모드
# data_type="submission"일 때
input_ids = [4643, 170, 531, 616, 2140, 2722, 2313, 2688, 2428, 3113, 1591, 2600, 8169, 2572] # 전체 시퀀스
target_pos = [4643, 170, 531, 616, 2140, 2722, 2313, 2688, 2428, 3113, 1591, 2600, 8169, 2572] # 사용 안함
target_neg = [999, 888, 777, 666, 555, 444, 333, 222, 111, 000, 123, 456, 789, 321] # 랜덤 샘플링
answer = [] # 답 없음
패딩 처리 (max_len=7 가정)
# 예: train 모드의 경우
원본 input_ids = [4643, 170, 531, 616, 2140, 2722, 2313, 2688, 2428, 3113, 1591]
# 1. 패딩 추가
input_ids = [0, 0, 0, 0, 4643, 170, 531, 616, 2140, 2722, 2313, 2688, 2428, 3113, 1591]
target_pos = [0, 0, 0, 0, 170, 531, 616, 2140, 2722, 2313, 2688, 2428, 3113, 1591, 2600]
target_neg = [0, 0, 0, 0, 999, 888, 777, 666, 555, 444, 333, 222, 111, 000, 123]
# 2. 최대 길이(7)로 자르기 (뒤에서부터 7개만 유지)
input_ids = [2688, 2428, 3113, 1591]
target_pos = [2428, 3113, 1591, 2600]
target_neg = [222, 111, 000, 123]
최종 반환
return (
torch.tensor(11), # user_id
torch.tensor(input_ids), # 입력 시퀀스
torch.tensor(target_pos), # 긍정 타겟
torch.tensor(target_neg), # 부정 타겟
torch.tensor(answer), # 정답
)
- 데이터 타입에 따라 다르게 시퀀스를 자릅니다:
- train: 마지막 3개제외, validation과 test를 위해 제외
- valid: 마지막 2개 제외, 모델 성능 검증에 사용. 8169를 맞추는지 확인
- test: 마지막 1개 제외, 최종 모델 테스트에 사용. 2572를 맞추는지 확인
- submission: 실제 예측할 때는 모든 데이터를 사용하여 다음 영화를 추천
- 각 입력 아이템에 대해 다음 아이템을 예측하도록 구성됩니다 (target_pos)
- 부정적 예시(target_neg)를 각 위치마다 생성합니다
PretrainDataset 클래스
# 사전학습(Pre-training)을 위한 데이터 처리
목적: BERT처럼 마스킹과 세그먼트 예측을 통한 사전학습
return (
torch.tensor(attributes), # 영화 속성 정보
torch.tensor(masked_sequence), # 마스킹된 시퀀스
torch.tensor(pos_items), # 원본 시퀀스
torch.tensor(neg_items), # 부정 샘플 시퀀스
torch.tensor(masked_segment), # 마스킹된 세그먼트
torch.tensor(pos_segment), # 실제 세그먼트
torch.tensor(neg_segment) # 부정 세그먼트
)
SASRecDataset 클래스
# 실제 추천을 위한 데이터 처리
목적: 다음 영화 추천을 위한 순차적 학습
return (
torch.tensor(user_id), # 사용자 ID
torch.tensor(input_ids), # 입력 시퀀스
torch.tensor(target_pos), # 실제 다음 아이템
torch.tensor(target_neg), # 부정 샘플 아이템
torch.tensor(answer) # 정답 (valid/test용)
)
주요 차이점:
- PretrainDataset:
- 영화의 속성 정보도 포함
- BERT 스타일의 마스킹 사용
- 세그먼트 단위 예측도 수행
- run_pretrain.py에서 사용
- SASRecDataset:
- 사용자 ID 포함
- train/valid/test/submission 모드 구분
- 순차적 예측에 초점
- run_train.py에서 사용