├── .gitignore ├── README.md ├── code ├── cate_dataset.py ├── cate_model.py ├── inference.py ├── preprocess.ipynb ├── preprocess.py ├── train.bat ├── train.py └── train.sh ├── doc ├── embedding.png └── model_block.png ├── input └── README.md └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # logs 7 | logs/ 8 | 9 | # packaging 10 | .Python 11 | build/ 12 | 13 | # models 14 | models/ 15 | 16 | # notebook 17 | .ipynb_checkpoints/ 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 상품 카테고리 분류기 2 | 3 | [카카오 아레나 데이터 경진대회 1등 노하우](https://wikibook.co.kr/kakao-arena) 서적의 **2장 - 쇼핑몰 상품 카테고리 분류 1등 솔루션**의 소스코드입니다. [카카오 아레나 - 쇼핑몰 상품 카테고리 분류 대회](https://arena.kakao.com/c/1)에 참여해 1등의 성적을 거뒀던 솔루션은 LSTM(Long short-term memory) 기반의 텍스트 인코더였으나 집필 과정에서 최신 기술인 트랜스포머(Transformer)로 수정했습니다. 인코더로 LSTM을 사용한 버전은 다음의 [링크](https://github.com/lime-robot/product-categories-classification)를 확인해 주세요. 4 | 5 | 본 분류기는 상품명(product 컬럼)과 이미지 특징(img_feat 컬럼)을 입력으로 사용하여 대/중/소/세 카테고리를 예측합니다. 6 | 7 | 8 | 9 | 10 | 11 | ### 참고 - 주요 성능 향상 방법 12 | 예측 성능을 향상 시키기 위해서 버트처럼 세그먼트 임베딩도 활용하였습니다. 13 | 14 | 15 | 16 | 17 | 18 | ## Requirements 19 | Ubuntu 18.04, Python 3.7, pytorch 1.6.0에서 실행됨을 확인하였습니다. 20 | - (추가) Ubuntu 18.04, Python 3.8, pytorch 1.7.1에서 실행됨을 확인하였습니다. 21 | 22 | CPU 코어 4개 / 메모리 16G / GTX1080 8GB / 저장공간 180GB의 최소 사양이 필요합니다. 23 | 24 | 필요한 패키지는 아래의 명령어로 설치할 수 있습니다. 25 | ```bash 26 | pip install -r requirements.txt 27 | ``` 28 | 29 | ## Getting Started 30 | 31 | ### Step 1: 소스코드 다운로드 32 | 33 | 임의의 작업 디렉터리 아래에 소스코드를 다운로드 받습니다. 34 | ``` 35 | ~/workspace$ git clone https://github.com/lime-robot/categories-prediction.git 36 | ``` 37 | 38 | ### Step 2: 데이터 다운로드 39 | 40 | 다운로드 받은 코드 하위 `input` 디렉터리에 `raw_data` 디렉터리를 생성하고, [카카오 아레나 - 쇼핑몰 상품 카테고리 분류 대회의 데이터](https://arena.kakao.com/c/1/data)를 다운로드 받습니다. 41 | 약 90GB 정도의 저장 공간이 필요합니다. 42 | 43 | ``` 44 | categories-prediction/ 45 | ├──input 46 | │ └── raw_data/ 47 | │ ├── train.chunk.01 48 | │ ├── train.chunk.02 49 | │ ├── train.chunk.03 50 | │ ├── ... 51 | │ └── test.chunk.01 52 | ``` 53 | 54 | ### Step 3: 데이터 전처리 55 | raw_data를 전처리하여 학습에 사용이 가능한 형태로 변환합니다. 컴퓨터 사양에 따라서 약 10~20분 정도의 시간이 소요됩니다. 56 | 대회에서 제공한 데이터셋인 학습셋(Train set), 데브셋(Dev set), 테스트셋(Test set)을 전처리하여 저장합니다. 57 | 약 90GB 정도의 저장 공간이 필요합니다. 58 | ``` 59 | ~/workspace/categories-prediction/code$ python preprocess.py 60 | ``` 61 | 62 | ### Step 4: 학습하기 63 | 전처리된 학습셋(Train sest)으로 모델을 학습 시키고 파일로 저장시킵니다. GTX1080 8G기준 약 7~8시간이 소요됩니다. 64 | 65 | ```bash 66 | ~/workspace/categories-prediction/code$ python train.py --fold 0 67 | ``` 68 | 69 | 성능 향상을 위해 5개의 모델을 학습시킬 수 있습니다. GTX1080 8G 기준 약 36시간이 소요됩니다. 70 | ```bash 71 | ~/workspace/categories-prediction/code$ bash train.sh 72 | ``` 73 | 74 | ### Step 5: 추론하기 75 | 학습된 모델을 불러와서 데브셋(Dev set)의 카테고리를 예측하고 제출을 위한 파일을 생성합니다. 76 | 제출 파일은 `categories-prediction/submissoin`에 저장됩니다. 77 | 78 | ```bash 79 | ~/workspace/categories-prediction/code$ python inference.py 80 | ``` 81 | -------------------------------------------------------------------------------- /code/cate_dataset.py: -------------------------------------------------------------------------------- 1 | import torch # 파이토치 패키지 임포트 2 | from torch.utils.data import Dataset # Dataset 클래스 임포트 3 | import h5py # h5py 패키지 임포트 4 | import re # 정규식표현식 모듈 임포트 5 | 6 | class CateDataset(Dataset): 7 | """데이터셋에서 학습에 필요한 형태로 변환된 샘플 하나를 반환 8 | """ 9 | def __init__(self, df_data, img_h5_path, token2id, tokens_max_len=64, type_vocab_size=30): 10 | """ 11 | 매개변수 12 | df_data: 상품타이틀, 카테고리 등의 정보를 가지는 데이터프레임 13 | img_h5_path: img_feat가 저장돼 있는 h5 파일의 경로 14 | token2id: token을 token_id로 변환하기 위한 맵핑 정보를 가진 딕셔너리 15 | tokens_max_len: tokens의 최대 길이. 상품명의 tokens가 이 이상이면 잘라서 버림 16 | type_vocab_size: 타입 사전의 크기 17 | """ 18 | self.tokens = df_data['tokens'].values # 전처리된 상품명 19 | self.img_indices = df_data['img_idx'].values # h5의 이미지 인덱스 20 | self.img_h5_path = img_h5_path 21 | self.tokens_max_len = tokens_max_len 22 | self.labels = df_data[['bcateid', 'mcateid', 'scateid', 'dcateid']].values 23 | self.token2id = token2id 24 | self.p = re.compile('▁[^▁]+') # ▁기호를 기준으로 나누기 위한 컴파일된 정규식 25 | self.type_vocab_size = type_vocab_size 26 | 27 | def __getitem__(self, idx): 28 | """ 29 | 데이터셋에서 idx에 대응되는 샘플을 변환하여 반환 30 | """ 31 | if idx >= len(self): 32 | raise StopIteration 33 | 34 | # idx에 해당하는 상품명 가져오기. 상품명은 문자열로 저장돼 있음 35 | tokens = self.tokens[idx] 36 | if not isinstance(tokens, str): 37 | tokens = '' 38 | 39 | # 상품명을 ▁기호를 기준으로 분리하여 파이썬 리스트로 저장 40 | # "▁직소퍼즐 ▁1000 조각 ▁바다 거북 의 ▁여행 ▁pl 12 75" => 41 | # ["▁직소퍼즐", "▁1000 조각", "▁바다 거북 의", "▁여행", "▁pl 12 75"] 42 | tokens = self.p.findall(tokens) 43 | 44 | # ▁ 기호 별 토큰타입 인덱스 부여 45 | # ["▁직소퍼즐", "▁1000 조각", "▁바다 거북 의", "▁여행", "▁pl 12 75"] => 46 | # [ 0 , 1 1 , 2 2 2 , 3 , 4 4 4 ] 47 | token_types = [type_id for type_id, word in enumerate(tokens) for _ in word.split()] 48 | tokens = " ".join(tokens) # ▁기호로 분리되기 전의 원래의 tokens으로 되돌림 49 | 50 | # 토큰을 토큰에 대응되는 인덱스로 변환 51 | # "▁직소퍼즐 ▁1000 조각 ▁바다 거북 의 ▁여행 ▁pl 12 75" => 52 | # [2291, 784, 2179, 3540, 17334, 30827, 1114, 282, 163, 444] 53 | # "▁직소퍼즐" => 2291 54 | # "▁1000" => 784 55 | # "조각" => 2179 56 | # ... 57 | token_ids = [self.token2id[tok] if tok in self.token2id else 0 for tok in tokens.split()] 58 | 59 | # token_ids의 길이가 max_len보다 길면 잘라서 버림 60 | if len(token_ids) > self.tokens_max_len: 61 | token_ids = token_ids[:self.tokens_max_len] 62 | token_types = token_types[:self.tokens_max_len] 63 | 64 | # token_ids의 길이가 max_len보다 짧으면 짧은만큼 PAD값 0 값으로 채워넣음 65 | # token_ids 중 값이 있는 곳은 1, 그 외는 0으로 채운 token_mask 생성 66 | token_mask = [1] * len(token_ids) 67 | token_pad = [0] * (self.tokens_max_len - len(token_ids)) 68 | token_ids += token_pad 69 | token_mask += token_pad 70 | token_types += token_pad # max_len 보다 짧은만큼 PAD 추가 71 | 72 | # h5파일에서 이미지 인덱스에 해당하는 img_feat를 가져옴 73 | # 파이토치의 데이터로더에 의해 동시 h5파일에 동시접근이 발생해도 74 | # 안정적으로 img_feat를 가져오려면 아래처럼 매번 h5py.File 호출필요 75 | with h5py.File(self.img_h5_path, 'r') as img_feats: 76 | img_feat = img_feats['img_feat'][self.img_indices[idx]] 77 | 78 | # 넘파이(numpy)나 파이썬 자료형을 파이토치의 자료형으로 변환 79 | token_ids = torch.LongTensor(token_ids) 80 | token_mask = torch.LongTensor(token_mask) 81 | token_types = torch.LongTensor(token_types) 82 | 83 | # token_types의 타입 인덱스의 숫자 크기가 type_vocab_size 보다 작도록 바꿈 84 | token_types[token_types >= self.type_vocab_size] = self.type_vocab_size-1 85 | img_feat = torch.FloatTensor(img_feat) 86 | 87 | # 대/중/소/세 라벨 준비 88 | label = self.labels[idx] 89 | label = torch.LongTensor(label) 90 | 91 | # 크게 3가지 텍스트 입력, 이미지 입력, 라벨을 반환한다. 92 | return token_ids, token_mask, token_types, img_feat, label 93 | 94 | def __len__(self): 95 | """ 96 | tokens의 개수를 반환한다. 즉, 상품명 문장의 개수를 반환한다. 97 | """ 98 | return len(self.tokens) 99 | -------------------------------------------------------------------------------- /code/cate_model.py: -------------------------------------------------------------------------------- 1 | import torch # 파이토치 패키지 임포트 2 | import torch.nn as nn # 자주 사용하는 torch.nn패키지를 별칭 nn으로 명명 3 | # 허깅페이스의 트랜스포머 패키지에서 BertConfig, BertModel 클래스 임포트 4 | from transformers import BertConfig, BertModel 5 | 6 | class CateClassifier(nn.Module): 7 | """상품정보를 받아서 대/중/소/세 카테고리를 예측하는 모델 8 | """ 9 | def __init__(self, cfg): 10 | """ 11 | 매개변수 12 | cfg: hidden_size, nlayers 등 설정값을 가지고 있는 변수 13 | """ 14 | super(CateClassifier, self).__init__() 15 | # 글로벌 설정값을 멤버 변수로 저장 16 | self.cfg = cfg 17 | # 버트모델의 설정값을 멤버 변수로 저장 18 | self.bert_cfg = BertConfig( 19 | cfg.vocab_size, # 사전 크기 20 | hidden_size=cfg.hidden_size, # 히든 크기 21 | num_hidden_layers=cfg.nlayers, # 레이어 층 수 22 | num_attention_heads=cfg.nheads, # 어텐션 헤드의 수 23 | intermediate_size=cfg.intermediate_size, # 인터미디어트 크기 24 | hidden_dropout_prob=cfg.dropout, # 히든 드롭아웃 확률 값 25 | attention_probs_dropout_prob=cfg.dropout, # 어텐션 드롭아웃 확률 값 26 | max_position_embeddings=cfg.seq_len, # 포지션 임베딩의 최대 길이 27 | type_vocab_size=cfg.type_vocab_size, # 타입 사전 크기 28 | ) 29 | # 텍스트 인코더로 버트모델 사용 30 | self.text_encoder = BertModel(self.bert_cfg) 31 | # 이미지 인코더로 선형모델 사용(대회에서 이미지가 아닌 img_feat를 제공) 32 | self.img_encoder = nn.Linear(cfg.img_feat_size, cfg.hidden_size) 33 | 34 | # 분류기(Classifier) 생성기 35 | def get_cls(target_size=1): 36 | return nn.Sequential( 37 | nn.Linear(cfg.hidden_size*2, cfg.hidden_size), 38 | nn.LayerNorm(cfg.hidden_size), 39 | nn.Dropout(cfg.dropout), 40 | nn.ReLU(), 41 | nn.Linear(cfg.hidden_size, target_size), 42 | ) 43 | 44 | # 대 카테고리 분류기 45 | self.b_cls = get_cls(cfg.n_b_cls) 46 | # 중 카테고리 분류기 47 | self.m_cls = get_cls(cfg.n_m_cls) 48 | # 소 카테고리 분류기 49 | self.s_cls = get_cls(cfg.n_s_cls) 50 | # 세 카테고리 분류기 51 | self.d_cls = get_cls(cfg.n_d_cls) 52 | 53 | def forward(self, token_ids, token_mask, token_types, img_feat, label=None): 54 | """ 55 | 매개변수 56 | token_ids: 전처리된 상품명을 인덱스로 변환하여 token_ids를 만들었음 57 | token_mask: 실제 token_ids의 개수만큼은 1, 나머지는 0으로 채움 58 | token_types: ▁ 문자를 기준으로 서로 다른 타입의 토큰임을 타입 인덱스로 저장 59 | img_feat: resnet50으로 인코딩된 이미지 피처 60 | label: 정답 대/중/소/세 카테고리 61 | """ 62 | 63 | # 전처리된 상품명을 하나의 텍스트벡터(text_vec)로 변환 64 | # 반환 튜플(시퀀스 아웃풋, 풀드(pooled) 아웃풋) 중 시퀀스 아웃풋만 사용 65 | text_output = self.text_encoder(token_ids, token_mask, token_type_ids=token_types)[0] 66 | 67 | # 시퀀스 중 첫 타임스탭의 hidden state만 사용. 68 | text_vec = text_output[:, 0] 69 | 70 | # img_feat를 텍스트벡터와 결합하기 앞서 선형변환 적용 71 | img_vec = self.img_encoder(img_feat) 72 | 73 | # 이미지벡터와 텍스트벡터를 직렬연결(concatenate)하여 결합벡터 생성 74 | comb_vec = torch.cat([text_vec, img_vec], 1) 75 | 76 | # 결합된 벡터로 대카테고리 확률분포 예측 77 | b_pred = self.b_cls(comb_vec) 78 | # 결합된 벡터로 중카테고리 확률분포 예측 79 | m_pred = self.m_cls(comb_vec) 80 | # 결합된 벡터로 소카테고리 확률분포 예측 81 | s_pred = self.s_cls(comb_vec) 82 | # 결합된 벡터로 세카테고리 확률분포 예측 83 | d_pred = self.d_cls(comb_vec) 84 | 85 | # 데이터 패러럴 학습에서 GPU 메모리를 효율적으로 사용하기 위해 86 | # loss를 모델 내에서 계산함. 87 | if label is not None: 88 | # 손실(loss) 함수로 CrossEntropyLoss를 사용 89 | # label의 값이 -1을 가지는 샘플은 loss계산에 사용 안 함 90 | loss_func = nn.CrossEntropyLoss(ignore_index=-1) 91 | # label은 batch_size x 4를 (batch_size x 1) 4개로 만듦 92 | b_label, m_label, s_label, d_label = label.split(1, 1) 93 | # 대카테고리의 예측된 확률분포와 정답확률 분포의 차이를 손실로 반환 94 | b_loss = loss_func(b_pred, b_label.view(-1)) 95 | # 중카테고리의 예측된 확률분포와 정답확률 분포의 차이를 손실로 반환 96 | m_loss = loss_func(m_pred, m_label.view(-1)) 97 | # 소카테고리의 예측된 확률분포와 정답확률 분포의 차이를 손실로 반환 98 | s_loss = loss_func(s_pred, s_label.view(-1)) 99 | # 세카테고리의 예측된 확률분포와 정답확률 분포의 차이를 손실로 반환 100 | d_loss = loss_func(d_pred, d_label.view(-1)) 101 | # 대/중/소/세 손실의 평균을 낼 때 실제 대회 평가방법과 일치하도록 함 102 | loss = (b_loss + 1.2*m_loss + 1.3*s_loss + 1.4*d_loss)/4 103 | else: # label이 없으면 loss로 0을 반환 104 | loss = b_pred.new(1).fill_(0) 105 | 106 | # 최종 계산된 손실과 예측된 대/중/소/세 각 확률분포를 반환 107 | return loss, [b_pred, m_pred, s_pred, d_pred] 108 | -------------------------------------------------------------------------------- /code/inference.py: -------------------------------------------------------------------------------- 1 | import os 2 | os.environ['OMP_NUM_THREADS'] = '24' 3 | os.environ['NUMEXPR_MAX_THREADS'] = '24' 4 | import math 5 | import glob 6 | import json 7 | import torch 8 | import cate_dataset 9 | import cate_model 10 | import time 11 | import random 12 | import numpy as np 13 | import pandas as pd 14 | from torch.utils.data import DataLoader 15 | import warnings 16 | warnings.filterwarnings(action='ignore') 17 | import argparse 18 | 19 | 20 | # 전처리된 데이터가 저장된 디렉터리 21 | DB_DIR = '../input/processed' 22 | 23 | # 토큰을 인덱스로 치환할 때 사용될 사전 파일이 저장된 디렉터리 24 | VOCAB_DIR = os.path.join(DB_DIR, 'vocab') 25 | 26 | # 학습된 모델의 파라미터가 저장될 디렉터리 27 | MODEL_DIR = '../model' 28 | 29 | # 제출할 예측결과가 저장될 디렉터리 30 | SUBMISSION_DIR = '../submission' 31 | 32 | 33 | # 미리 정의된 설정 값 34 | class CFG: 35 | batch_size=1024 # 배치 사이즈 36 | num_workers=4 # 워커의 개수 37 | print_freq=100 # 결과 출력 빈도 38 | warmup_steps=100 # lr을 서서히 증가시킬 step 수 39 | hidden_size=512 # 은닉 크기 40 | dropout=0.2 # dropout 확률 41 | intermediate_size=256 # TRANSFORMER셀의 intermediate 크기 42 | nlayers=2 # BERT의 층수 43 | nheads=8 # BERT의 head 개수 44 | seq_len=64 # 토큰의 최대 길이 45 | n_b_cls = 57 + 1 # 대카테고리 개수 46 | n_m_cls = 552 + 1 # 중카테고리 개수 47 | n_s_cls = 3190 + 1 # 소카테고리 개수 48 | n_d_cls = 404 + 1 # 세카테고리 개수 49 | vocab_size = 32000 # 토큰의 유니크 인덱스 개수 50 | img_feat_size = 2048 # 이미지 피처 벡터의 크기 51 | type_vocab_size = 30 # 타입의 유니크 인덱스 개수 52 | csv_path = os.path.join(DB_DIR, 'dev.csv') # 전처리 돼 저장된 dev 데이터셋 53 | h5_path = os.path.join(DB_DIR, 'dev_img_feat.h5') 54 | 55 | 56 | 57 | def main(): 58 | # 명령행에서 받을 키워드 인자를 설정합니다. 59 | parser = argparse.ArgumentParser("") 60 | parser.add_argument("--model_dir", type=str, default=MODEL_DIR) 61 | parser.add_argument("--batch_size", type=int, default=CFG.batch_size) 62 | parser.add_argument("--seq_len", type=int, default=CFG.seq_len) 63 | parser.add_argument("--nworkers", type=int, default=CFG.num_workers) 64 | parser.add_argument("--seed", type=int, default=7) 65 | parser.add_argument("--nlayers", type=int, default=CFG.nlayers) 66 | parser.add_argument("--nheads", type=int, default=CFG.nheads) 67 | parser.add_argument("--hidden_size", type=int, default=CFG.hidden_size) 68 | parser.add_argument("--k", type=int, default=0) 69 | args = parser.parse_args() 70 | print(args) 71 | 72 | CFG.batch_size=args.batch_size 73 | CFG.seed = args.seed 74 | CFG.nlayers = args.nlayers 75 | CFG.nheads = args.nheads 76 | CFG.hidden_size = args.hidden_size 77 | CFG.seq_len = args.seq_len 78 | CFG.num_workers=args.nworkers 79 | CFG.res_dir=f'res_dir_{args.k}' 80 | print(CFG.__dict__) 81 | 82 | # 랜덤 시드를 설정하여 매 코드를 실행할 때마다 동일한 결과를 얻게 함 83 | os.environ['PYTHONHASHSEED'] = str(CFG.seed) 84 | random.seed(CFG.seed) 85 | np.random.seed(CFG.seed) 86 | torch.manual_seed(CFG.seed) 87 | torch.cuda.manual_seed(CFG.seed) 88 | torch.backends.cudnn.deterministic = True 89 | 90 | # 전처리된 데이터를 읽어옵니다. 91 | print('loading ...') 92 | dev_df = pd.read_csv(CFG.csv_path, dtype={'tokens':str}) 93 | dev_df['img_idx'] = dev_df.index 94 | img_h5_path = CFG.h5_path 95 | 96 | vocab = [line.split('\t')[0] for line in open(os.path.join(VOCAB_DIR, 'spm.vocab'), encoding='utf-8').readlines()] 97 | token2id = dict([(w, i) for i, w in enumerate(vocab)]) 98 | print('loading ... done') 99 | 100 | # 찾아진 모델 파일의 개수만큼 모델을 만들어서 파이썬 리스트에 추가함 101 | model_list = [] 102 | # args.model_dir에 있는 확장자 .pt를 가지는 모든 모델 파일의 경로를 읽음 103 | model_path_list = glob.glob(os.path.join(args.model_dir, '*.pt')) 104 | # 모델 경로 개수만큼 모델을 생성하여 파이썬 리스트에 추가함 105 | for model_path in model_path_list: 106 | model = cate_model.CateClassifier(CFG) 107 | if model_path != "": 108 | print("=> loading checkpoint '{}'".format(model_path)) 109 | checkpoint = torch.load(model_path) 110 | state_dict = checkpoint['state_dict'] 111 | model.load_state_dict(state_dict, strict=True) 112 | print("=> loaded checkpoint '{}' (epoch {})" 113 | .format(model_path, checkpoint['epoch'])) 114 | model.cuda() 115 | n_gpu = torch.cuda.device_count() 116 | if n_gpu > 1: 117 | model = torch.nn.DataParallel(model) 118 | model_list.append(model) 119 | if len(model_list) == 0: 120 | print('Please check the model directory.') 121 | return 122 | 123 | # 모델의 파라미터 수를 출력합니다. 124 | def count_parameters(model): 125 | return sum(p.numel() for p in model.parameters() if p.requires_grad) 126 | print('parameters: ', count_parameters(model_list[0])) 127 | 128 | # 모델의 입력에 적합한 형태의 샘플을 가져오는 CateDataset의 인스턴스를 만듦 129 | dev_db = cate_dataset.CateDataset(dev_df, img_h5_path, token2id, CFG.seq_len, 130 | CFG.type_vocab_size) 131 | 132 | # 여러 개의 워커로 빠르게 배치(미니배치)를 생성하도록 DataLoader로 133 | # CateDataset 인스턴스를 감싸 줌 134 | dev_loader = DataLoader( 135 | dev_db, batch_size=CFG.batch_size, shuffle=False, 136 | num_workers=CFG.num_workers, pin_memory=True) 137 | 138 | # dev 데이터셋의 모든 상품명에 대해 예측된 카테고리 인덱스를 반환 139 | pred_idx = inference(dev_loader, model_list) 140 | 141 | # dev 데이터셋의 상품ID별 예측된 카테고리를 붙여서 제출 파일을 생성하여 저장 142 | cate_cols = ['bcateid', 'mcateid', 'scateid', 'dcateid'] 143 | dev_df[cate_cols] = pred_idx 144 | os.makedirs(SUBMISSION_DIR, exist_ok=True) 145 | submission_path = os.path.join(SUBMISSION_DIR, 'dev.tsv') 146 | dev_df[['pid'] + cate_cols].to_csv(submission_path, sep='\t', header=False, index=False) 147 | 148 | print('done') 149 | 150 | 151 | def inference(dev_loader, model_list): 152 | """ 153 | dev 데이터셋의 모든 상품명에 대해 여러 모델들의 예측한 결과를 앙상블 하여 정확도가 개선된 154 | 카테고리 인덱스를 반환 155 | 156 | 매개변수 157 | dev_loader: dev 데이터셋에서 배치(미니배치) 불러옴 158 | model_list: args.model_dir에서 불러온 모델 리스트 159 | """ 160 | batch_time = AverageMeter() 161 | data_time = AverageMeter() 162 | sent_count = AverageMeter() 163 | 164 | # 모딜 리스트의 모든 모델을 평가(evaluation) 모드로 작동하게 함 165 | for model in model_list: 166 | model.eval() 167 | 168 | start = end = time.time() 169 | 170 | # 배치별 예측한 대/중/소/세 카테고리의 인덱스를 리스트로 가짐 171 | pred_idx_list = [] 172 | 173 | # dev_loader에서 반복해서 배치 데이터를 받음 174 | # CateDataset의 __getitem__() 함수의 반환 값과 동일한 변수 반환 175 | for step, (token_ids, token_mask, token_types, img_feat, _) in enumerate(dev_loader): 176 | # 데이터 로딩 시간 기록 177 | data_time.update(time.time() - end) 178 | 179 | # 배치 데이터의 위치를 CPU메모리에서 GPU메모리로 이동 180 | token_ids, token_mask, token_types, img_feat = ( 181 | token_ids.cuda(), token_mask.cuda(), token_types.cuda(), img_feat.cuda()) 182 | 183 | batch_size = token_ids.size(0) 184 | 185 | # with문 내에서는 그래디언트 계산을 하지 않도록 함 186 | with torch.no_grad(): 187 | pred_list = [] 188 | # model 별 예측치를 pred_list에 추가합니다. 189 | for model in model_list: 190 | _, pred = model(token_ids, token_mask, token_types, img_feat) 191 | pred_list.append(pred) 192 | 193 | # 예측치 리스트를 앙상블 하여 하나의 예측치로 만듦 194 | pred = ensemble(pred_list) 195 | # 예측치에서 카테고리별 인덱스를 가져옴 196 | pred_idx = get_pred_idx(pred) 197 | # 현재 배치(미니배치)에서 얻어진 카테고리별 인덱스를 pred_idx_list에 추가 198 | pred_idx_list.append(pred_idx.cpu()) 199 | 200 | # 소요시간 측정 201 | batch_time.update(time.time() - end) 202 | end = time.time() 203 | 204 | sent_count.update(batch_size) 205 | 206 | if step % CFG.print_freq == 0 or step == (len(dev_loader)-1): 207 | print('TEST: {0}/{1}] ' 208 | 'Data {data_time.val:.3f} ({data_time.avg:.3f}) ' 209 | 'Elapsed {remain:s} ' 210 | 'sent/s {sent_s:.0f} ' 211 | .format( 212 | step+1, len(dev_loader), batch_time=batch_time, 213 | data_time=data_time, 214 | remain=timeSince(start, float(step+1)/len(dev_loader)), 215 | sent_s=sent_count.avg/batch_time.avg 216 | )) 217 | 218 | # 배치별로 얻어진 카테고리 인덱스 리스트를 직렬연결하여 하나의 카테고리 인덱스로 변환 219 | pred_idx = torch.cat(pred_idx_list).numpy() 220 | return pred_idx 221 | 222 | # 예측치의 각 카테고리 별로 가장 큰 값을 가지는 인덱스를 반환함 223 | def get_pred_idx(pred): 224 | b_pred, m_pred, s_pred, d_pred= pred # 대/중/소/세 예측치로 분리 225 | _, b_idx = b_pred.max(1) # 대카테고리 중 가장 큰 값을 가지는 인덱스를 변수에 할당 226 | _, m_idx = m_pred.max(1) # 중카테고리 중 가장 큰 값을 가지는 인덱스를 변수에 할당 227 | _, s_idx = s_pred.max(1) # 소카테고리 중 가장 큰 값을 가지는 인덱스를 변수에 할당 228 | _, d_idx = d_pred.max(1) # 세카테고리 중 가장 큰 값을 가지는 인덱스를 변수에 할당 229 | 230 | # 대/중/소/세 인덱스 반환 231 | pred_idx = torch.stack([b_idx, m_idx, s_idx, d_idx], 1) 232 | return pred_idx 233 | 234 | 235 | # 예측된 대/중/소/세 결과들을 앙상블함 236 | # 앙상블 방법으로 간단히 산술 평균을 사용 237 | def ensemble(pred_list): 238 | b_pred, m_pred, s_pred, d_pred = 0, 0, 0, 0 239 | for pred in pred_list: 240 | # softmax를 적용해 대/중/소/세 각 카테고리별 모든 클래스의 합이 1이 되도록 정규화 241 | # 참고로 정규화된 pred[0]은 대카테고리의 클래스별 확률값을 가지는 확률분포 함수라 볼 수 있음 242 | b_pred += torch.softmax(pred[0], 1) 243 | m_pred += torch.softmax(pred[1], 1) 244 | s_pred += torch.softmax(pred[2], 1) 245 | d_pred += torch.softmax(pred[3], 1) 246 | b_pred /= len(pred_list) # 모델별 '대카테고리의 정규화된 예측값'들의 평균 계산 247 | m_pred /= len(pred_list) # 모델별 '중카테고리의 정규화된 예측값'들의 평균 계산 248 | s_pred /= len(pred_list) # 모델별 '소카테고리의 정규화된 예측값'들의 평균 계산 249 | d_pred /= len(pred_list) # 모델별 '세카테고리의 정규화된 예측값'들의 평균 계산 250 | 251 | # 앙상블 결과 반환 252 | pred = [b_pred, m_pred, s_pred, d_pred] 253 | return pred 254 | 255 | 256 | class AverageMeter(object): 257 | """Computes and stores the average and current value""" 258 | def __init__(self): 259 | self.reset() 260 | 261 | def reset(self): 262 | self.val = 0 263 | self.avg = 0 264 | self.sum = 0 265 | self.count = 0 266 | 267 | def update(self, val, n=1): 268 | self.val = val 269 | self.sum += val * n 270 | self.count += n 271 | self.avg = self.sum / self.count 272 | 273 | 274 | def asMinutes(s): 275 | m = math.floor(s / 60) 276 | s -= m * 60 277 | return '%dm %ds' % (m, s) 278 | 279 | 280 | def timeSince(since, percent): 281 | now = time.time() 282 | s = now - since 283 | es = s / (percent) 284 | rs = es - s 285 | return '%s (remain %s)' % (asMinutes(s), asMinutes(rs)) 286 | 287 | 288 | if __name__ == '__main__': 289 | main() 290 | -------------------------------------------------------------------------------- /code/preprocess.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# 필요 변수 설정" 8 | ] 9 | }, 10 | { 11 | "cell_type": "code", 12 | "execution_count": 1, 13 | "metadata": {}, 14 | "outputs": [], 15 | "source": [ 16 | "import os\n", 17 | "import json\n", 18 | "import h5py\n", 19 | "import numpy as np\n", 20 | "import pandas as pd\n", 21 | "from tqdm.notebook import tqdm\n", 22 | "\n", 23 | "RAW_DATA_DIR = \"../input/raw_data\" # 카카오에서 다운로드 받은 데이터의 디렉터리\n", 24 | "PROCESSED_DATA_DIR = '../input/processed' # 전처리된 데이터가 저장될 디렉터리\n", 25 | "VOCAB_DIR = os.path.join(PROCESSED_DATA_DIR, 'vocab') # 전처리에 사용될 사전 파일이 저장될 디렉터리\n", 26 | "\n", 27 | "# 학습에 사용될 파일 리스트\n", 28 | "train_file_list = [\n", 29 | " \"train.chunk.01\",\n", 30 | " \"train.chunk.02\",\n", 31 | " \"train.chunk.03\",\n", 32 | " \"train.chunk.04\",\n", 33 | " \"train.chunk.05\",\n", 34 | " \"train.chunk.06\",\n", 35 | " \"train.chunk.07\",\n", 36 | " \"train.chunk.08\",\n", 37 | " \"train.chunk.09\"\n", 38 | "]\n", 39 | "\n", 40 | "# 개발에 사용될 파일 리스트. 공개 리더보드 점수를 내는데 사용된다.\n", 41 | "dev_file_list = [\n", 42 | " \"dev.chunk.01\" \n", 43 | "]\n", 44 | "\n", 45 | "# 테스트에 사용될 파일 리스트. 파이널 리더보드 점수를 내는데 사용된다.\n", 46 | "test_file_list = [\n", 47 | " \"test.chunk.01\",\n", 48 | " \"test.chunk.02\", \n", 49 | "]\n", 50 | "\n", 51 | "# 파일명과 실제 파일이 위치한 디렉토리를 결합한다.\n", 52 | "train_path_list = [os.path.join(RAW_DATA_DIR, fn) for fn in train_file_list]\n", 53 | "dev_path_list = [os.path.join(RAW_DATA_DIR, fn) for fn in dev_file_list]\n", 54 | "test_path_list = [os.path.join(RAW_DATA_DIR, fn) for fn in test_file_list]\n", 55 | "\n", 56 | "# PROCESSED_DATA_DIR과 VOCAB_DIR를 생성한다.\n", 57 | "os.makedirs(PROCESSED_DATA_DIR, exist_ok=True)\n", 58 | "os.makedirs(VOCAB_DIR, exist_ok=True)" 59 | ] 60 | }, 61 | { 62 | "cell_type": "markdown", 63 | "metadata": {}, 64 | "source": [ 65 | "# 대회 데이터를 pandas.DataFrame으로 만들기" 66 | ] 67 | }, 68 | { 69 | "cell_type": "code", 70 | "execution_count": 2, 71 | "metadata": {}, 72 | "outputs": [], 73 | "source": [ 74 | "# path_list의 파일에서 col 변수에 해당하는 컬럼 값들을 가져온다.\n", 75 | "def get_column_data(path_list, div, col):\n", 76 | " col_data = []\n", 77 | " for path in path_list:\n", 78 | " h = h5py.File(path, 'r')\n", 79 | " col_data.append(h[div][col][:])\n", 80 | " h.close()\n", 81 | " return np.concatenate(col_data)\n", 82 | "\n", 83 | "# path_list의 파일에서 학습에 필요한 컬럼들을 DataFrame 포맷으로 반환한다.\n", 84 | "def get_dataframe(path_list, div):\n", 85 | " pids = get_column_data(path_list, div, col='pid')\n", 86 | " products = get_column_data(path_list, div, col='product')\n", 87 | " brands = get_column_data(path_list, div, col='brand')\n", 88 | " makers = get_column_data(path_list, div, col='maker')\n", 89 | " # 16GB를 가진 PC에서 실행 가능하도록 메모리를 많이 사용하는 칼럼은 주석처리\n", 90 | " #models = get_column_data(path_list, div, col='model') \n", 91 | " prices = get_column_data(path_list, div, col='price')\n", 92 | " updttms = get_column_data(path_list, div, col='updttm')\n", 93 | " bcates = get_column_data(path_list, div, col='bcateid')\n", 94 | " mcates = get_column_data(path_list, div, col='mcateid')\n", 95 | " scates = get_column_data(path_list, div, col='scateid')\n", 96 | " dcates = get_column_data(path_list, div, col='dcateid')\n", 97 | " \n", 98 | " df = pd.DataFrame({'pid': pids, 'product':products, 'brand':brands, 'maker':makers, \n", 99 | " #'model':models, \n", 100 | " 'price':prices, 'updttm':updttms, \n", 101 | " 'bcateid':bcates, 'mcateid':mcates, 'scateid':scates, 'dcateid':dcates} )\n", 102 | " \n", 103 | " # 바이트 열로 인코딩 상품제목과 상품ID를 유니코드 변환한다.\n", 104 | " df['pid'] = df['pid'].map(lambda x: x.decode('utf-8'))\n", 105 | " df['product'] = df['product'].map(lambda x: x.decode('utf-8'))\n", 106 | " df['brand'] = df['brand'].map(lambda x: x.decode('utf-8'))\n", 107 | " df['maker'] = df['maker'].map(lambda x: x.decode('utf-8'))\n", 108 | " #df['model'] = df['model'].map(lambda x: x.decode('utf-8')) # 메모리 사용량을 줄이기 위해 주석처리\n", 109 | " df['updttm'] = df['updttm'].map(lambda x: x.decode('utf-8')) \n", 110 | " \n", 111 | " return df" 112 | ] 113 | }, 114 | { 115 | "cell_type": "code", 116 | "execution_count": 3, 117 | "metadata": { 118 | "scrolled": true 119 | }, 120 | "outputs": [], 121 | "source": [ 122 | "train_df = get_dataframe(train_path_list, 'train')\n", 123 | "dev_df = get_dataframe(dev_path_list, 'dev')\n", 124 | "test_df = get_dataframe(test_path_list, 'test')" 125 | ] 126 | }, 127 | { 128 | "cell_type": "code", 129 | "execution_count": 4, 130 | "metadata": {}, 131 | "outputs": [ 132 | { 133 | "data": { 134 | "text/html": [ 135 | "
\n", 136 | "\n", 149 | "\n", 150 | " \n", 151 | " \n", 152 | " \n", 153 | " \n", 154 | " \n", 155 | " \n", 156 | " \n", 157 | " \n", 158 | " \n", 159 | " \n", 160 | " \n", 161 | " \n", 162 | " \n", 163 | " \n", 164 | " \n", 165 | " \n", 166 | " \n", 167 | " \n", 168 | " \n", 169 | " \n", 170 | "
product
0직소퍼즐 - 1000조각 바다거북의 여행 (PL1275)
1[모리케이스]아이폰6S/6S+ tree farm101 - 다이어리케이스[바보사랑][무료배송]
2크리비아 기모 3부 속바지 GLG4314P
\n", 171 | "
" 172 | ], 173 | "text/plain": [ 174 | " product\n", 175 | "0 직소퍼즐 - 1000조각 바다거북의 여행 (PL1275)\n", 176 | "1 [모리케이스]아이폰6S/6S+ tree farm101 - 다이어리케이스[바보사랑][무료배송]\n", 177 | "2 크리비아 기모 3부 속바지 GLG4314P" 178 | ] 179 | }, 180 | "execution_count": 4, 181 | "metadata": {}, 182 | "output_type": "execute_result" 183 | } 184 | ], 185 | "source": [ 186 | "pd.set_option('display.max_columns', 100)\n", 187 | "pd.set_option('display.max_colwidth', 500)\n", 188 | "train_df[['product']].head(3)" 189 | ] 190 | }, 191 | { 192 | "cell_type": "markdown", 193 | "metadata": {}, 194 | "source": [ 195 | "# 학습에 불필요한 컬럼 제거" 196 | ] 197 | }, 198 | { 199 | "cell_type": "code", 200 | "execution_count": 5, 201 | "metadata": {}, 202 | "outputs": [], 203 | "source": [ 204 | "# Peter Norvig Quote: “More data beats clever algorithms, but better data beats more data.”" 205 | ] 206 | }, 207 | { 208 | "cell_type": "code", 209 | "execution_count": 6, 210 | "metadata": {}, 211 | "outputs": [], 212 | "source": [ 213 | "import json\n", 214 | "# 카테고리 이름과 ID의 매핑 정보를 불러온다.\n", 215 | "cate_json = json.load(open(os.path.join(RAW_DATA_DIR, 'cate1.json')))\n", 216 | "\n", 217 | "# (이름, ID) 순서를 (ID, 이름)으로 바꾼 후 dictionary로 만든다.\n", 218 | "bid2nm = dict([(cid, name) for name, cid in cate_json['b'].items()])\n", 219 | "mid2nm = dict([(cid, name) for name, cid in cate_json['m'].items()])\n", 220 | "sid2nm = dict([(cid, name) for name, cid in cate_json['s'].items()])\n", 221 | "did2nm = dict([(cid, name) for name, cid in cate_json['d'].items()])\n", 222 | "\n", 223 | "# dictionary를 활용해 카테고리 ID에 해당하는 카테고리 이름 컬럼을 추가한다.\n", 224 | "train_df['bcatenm'] = train_df['bcateid'].map(bid2nm)\n", 225 | "train_df['mcatenm'] = train_df['mcateid'].map(mid2nm)\n", 226 | "train_df['scatenm'] = train_df['scateid'].map(sid2nm)\n", 227 | "train_df['dcatenm'] = train_df['dcateid'].map(did2nm)" 228 | ] 229 | }, 230 | { 231 | "cell_type": "code", 232 | "execution_count": 7, 233 | "metadata": { 234 | "scrolled": true 235 | }, 236 | "outputs": [ 237 | { 238 | "data": { 239 | "text/html": [ 240 | "
\n", 241 | "\n", 254 | "\n", 255 | " \n", 256 | " \n", 257 | " \n", 258 | " \n", 259 | " \n", 260 | " \n", 261 | " \n", 262 | " \n", 263 | " \n", 264 | " \n", 265 | " \n", 266 | " \n", 267 | " \n", 268 | " \n", 269 | " \n", 270 | " \n", 271 | " \n", 272 | " \n", 273 | " \n", 274 | " \n", 275 | " \n", 276 | " \n", 277 | " \n", 278 | " \n", 279 | " \n", 280 | " \n", 281 | " \n", 282 | " \n", 283 | " \n", 284 | " \n", 285 | " \n", 286 | " \n", 287 | " \n", 288 | " \n", 289 | " \n", 290 | " \n", 291 | " \n", 292 | " \n", 293 | " \n", 294 | " \n", 295 | " \n", 296 | " \n", 297 | " \n", 298 | " \n", 299 | " \n", 300 | " \n", 301 | " \n", 302 | " \n", 303 | " \n", 304 | " \n", 305 | " \n", 306 | " \n", 307 | " \n", 308 | " \n", 309 | " \n", 310 | " \n", 311 | " \n", 312 | " \n", 313 | " \n", 314 | " \n", 315 | " \n", 316 | " \n", 317 | " \n", 318 | " \n", 319 | " \n", 320 | " \n", 321 | " \n", 322 | " \n", 323 | " \n", 324 | " \n", 325 | " \n", 326 | " \n", 327 | " \n", 328 | " \n", 329 | " \n", 330 | " \n", 331 | "
pidproductbrandmakerpricebcatenmmcatenmscatenmdcatenm
0O4486751463직소퍼즐 - 1000조각 바다거북의 여행 (PL1275)퍼즐라이프상품상세설명 참조16520악기/취미/만들기보드게임/퍼즐직소/퍼즐
1P3307178849[모리케이스]아이폰6S/6S+ tree farm101 - 다이어리케이스[바보사랑][무료배송]바보사랑MORY|해당없음20370휴대폰/액세서리휴대폰액세서리아이폰액세서리
2R4424255515크리비아 기모 3부 속바지 GLG4314P크리비아-1언더웨어보정언더웨어속바지/속치마
3F3334315393[하프클럽/잭앤질]남성 솔리드 절개라인 포인트 포켓 팬츠 31133PT002_NA잭앤질㈜크리스패션16280남성의류바지일자면바지
4N731678492코드프리혈당시험지50매/코드프리시험지/최장유효기간기타-1건강관리/실버용품건강측정용품혈당지
\n", 332 | "
" 333 | ], 334 | "text/plain": [ 335 | " pid product brand \\\n", 336 | "0 O4486751463 직소퍼즐 - 1000조각 바다거북의 여행 (PL1275) 퍼즐라이프 \n", 337 | "1 P3307178849 [모리케이스]아이폰6S/6S+ tree farm101 - 다이어리케이스[바보사랑][무료배송] 바보사랑 \n", 338 | "2 R4424255515 크리비아 기모 3부 속바지 GLG4314P 크리비아 \n", 339 | "3 F3334315393 [하프클럽/잭앤질]남성 솔리드 절개라인 포인트 포켓 팬츠 31133PT002_NA 잭앤질 \n", 340 | "4 N731678492 코드프리혈당시험지50매/코드프리시험지/최장유효기간 \n", 341 | "\n", 342 | " maker price bcatenm mcatenm scatenm dcatenm \n", 343 | "0 상품상세설명 참조 16520 악기/취미/만들기 보드게임/퍼즐 직소/퍼즐 \n", 344 | "1 MORY|해당없음 20370 휴대폰/액세서리 휴대폰액세서리 아이폰액세서리 \n", 345 | "2 -1 언더웨어 보정언더웨어 속바지/속치마 \n", 346 | "3 ㈜크리스패션 16280 남성의류 바지 일자면바지 \n", 347 | "4 기타 -1 건강관리/실버용품 건강측정용품 혈당지 " 348 | ] 349 | }, 350 | "execution_count": 7, 351 | "metadata": {}, 352 | "output_type": "execute_result" 353 | } 354 | ], 355 | "source": [ 356 | "#train_df[['pid', 'product', 'brand', 'maker', 'model', 'price', 'bcatenm', 'mcatenm', 'scatenm', 'dcatenm']].head(5)\n", 357 | "train_df[['pid', 'product', 'brand', 'maker','price', 'bcatenm', 'mcatenm', 'scatenm', 'dcatenm']].head(5)" 358 | ] 359 | }, 360 | { 361 | "cell_type": "markdown", 362 | "metadata": {}, 363 | "source": [ 364 | "## 데이터 분석" 365 | ] 366 | }, 367 | { 368 | "cell_type": "markdown", 369 | "metadata": {}, 370 | "source": [ 371 | "### brand 컬럼 분석" 372 | ] 373 | }, 374 | { 375 | "cell_type": "code", 376 | "execution_count": 8, 377 | "metadata": {}, 378 | "outputs": [], 379 | "source": [ 380 | "def get_vc_df(df, col): \n", 381 | " vc_df = df[col].value_counts().reset_index()\n", 382 | " vc_df.columns = [col, 'count']\n", 383 | " vc_df['percentage'] = (vc_df['count'] / vc_df['count'].sum())*100 \n", 384 | " return vc_df" 385 | ] 386 | }, 387 | { 388 | "cell_type": "code", 389 | "execution_count": 9, 390 | "metadata": {}, 391 | "outputs": [ 392 | { 393 | "data": { 394 | "text/html": [ 395 | "
\n", 396 | "\n", 409 | "\n", 410 | " \n", 411 | " \n", 412 | " \n", 413 | " \n", 414 | " \n", 415 | " \n", 416 | " \n", 417 | " \n", 418 | " \n", 419 | " \n", 420 | " \n", 421 | " \n", 422 | " \n", 423 | " \n", 424 | " \n", 425 | " \n", 426 | " \n", 427 | " \n", 428 | " \n", 429 | " \n", 430 | " \n", 431 | " \n", 432 | " \n", 433 | " \n", 434 | " \n", 435 | " \n", 436 | " \n", 437 | " \n", 438 | " \n", 439 | " \n", 440 | " \n", 441 | " \n", 442 | " \n", 443 | " \n", 444 | " \n", 445 | " \n", 446 | " \n", 447 | " \n", 448 | " \n", 449 | " \n", 450 | " \n", 451 | " \n", 452 | " \n", 453 | " \n", 454 | " \n", 455 | " \n", 456 | " \n", 457 | " \n", 458 | " \n", 459 | " \n", 460 | " \n", 461 | " \n", 462 | " \n", 463 | " \n", 464 | " \n", 465 | " \n", 466 | " \n", 467 | " \n", 468 | " \n", 469 | " \n", 470 | " \n", 471 | " \n", 472 | " \n", 473 | " \n", 474 | " \n", 475 | " \n", 476 | " \n", 477 | " \n", 478 | " \n", 479 | " \n", 480 | "
brandcountpercentage
0393011348.312243
1상품상세설명 참조1531561.882722
2바보사랑666450.819256
3기타641440.788512
4상세설명참조357950.440022
5없음336030.413076
6아디다스322920.396960
7나이키307850.378435
8아트박스285180.350567
9알수없음267680.329055
\n", 481 | "
" 482 | ], 483 | "text/plain": [ 484 | " brand count percentage\n", 485 | "0 3930113 48.312243\n", 486 | "1 상품상세설명 참조 153156 1.882722\n", 487 | "2 바보사랑 66645 0.819256\n", 488 | "3 기타 64144 0.788512\n", 489 | "4 상세설명참조 35795 0.440022\n", 490 | "5 없음 33603 0.413076\n", 491 | "6 아디다스 32292 0.396960\n", 492 | "7 나이키 30785 0.378435\n", 493 | "8 아트박스 28518 0.350567\n", 494 | "9 알수없음 26768 0.329055" 495 | ] 496 | }, 497 | "execution_count": 9, 498 | "metadata": {}, 499 | "output_type": "execute_result" 500 | } 501 | ], 502 | "source": [ 503 | "vc_df = get_vc_df(train_df, 'brand')\n", 504 | "vc_df.head(10)" 505 | ] 506 | }, 507 | { 508 | "cell_type": "markdown", 509 | "metadata": {}, 510 | "source": [ 511 | "### maker 컬럼 분석" 512 | ] 513 | }, 514 | { 515 | "cell_type": "code", 516 | "execution_count": 10, 517 | "metadata": {}, 518 | "outputs": [ 519 | { 520 | "data": { 521 | "text/html": [ 522 | "
\n", 523 | "\n", 536 | "\n", 537 | " \n", 538 | " \n", 539 | " \n", 540 | " \n", 541 | " \n", 542 | " \n", 543 | " \n", 544 | " \n", 545 | " \n", 546 | " \n", 547 | " \n", 548 | " \n", 549 | " \n", 550 | " \n", 551 | " \n", 552 | " \n", 553 | " \n", 554 | " \n", 555 | " \n", 556 | " \n", 557 | " \n", 558 | " \n", 559 | " \n", 560 | " \n", 561 | " \n", 562 | " \n", 563 | " \n", 564 | " \n", 565 | " \n", 566 | " \n", 567 | " \n", 568 | " \n", 569 | " \n", 570 | " \n", 571 | " \n", 572 | " \n", 573 | " \n", 574 | " \n", 575 | " \n", 576 | " \n", 577 | " \n", 578 | " \n", 579 | " \n", 580 | " \n", 581 | " \n", 582 | " \n", 583 | " \n", 584 | " \n", 585 | " \n", 586 | " \n", 587 | " \n", 588 | " \n", 589 | " \n", 590 | " \n", 591 | " \n", 592 | " \n", 593 | " \n", 594 | " \n", 595 | " \n", 596 | " \n", 597 | " \n", 598 | " \n", 599 | " \n", 600 | " \n", 601 | " \n", 602 | " \n", 603 | " \n", 604 | " \n", 605 | " \n", 606 | " \n", 607 | "
makercountpercentage
0219684627.005472
1기타200982824.706490
2상품상세설명 참조4422995.437110
3상세페이지 참조637920.784185
4상세설명참조378990.465886
5상품상세설명참조363890.447324
6아디다스254720.313123
7상세설명참조 / 상세설명참조218730.268881
8[불명]208360.256134
9상품상세정보 참조197860.243226
\n", 608 | "
" 609 | ], 610 | "text/plain": [ 611 | " maker count percentage\n", 612 | "0 2196846 27.005472\n", 613 | "1 기타 2009828 24.706490\n", 614 | "2 상품상세설명 참조 442299 5.437110\n", 615 | "3 상세페이지 참조 63792 0.784185\n", 616 | "4 상세설명참조 37899 0.465886\n", 617 | "5 상품상세설명참조 36389 0.447324\n", 618 | "6 아디다스 25472 0.313123\n", 619 | "7 상세설명참조 / 상세설명참조 21873 0.268881\n", 620 | "8 [불명] 20836 0.256134\n", 621 | "9 상품상세정보 참조 19786 0.243226" 622 | ] 623 | }, 624 | "execution_count": 10, 625 | "metadata": {}, 626 | "output_type": "execute_result" 627 | } 628 | ], 629 | "source": [ 630 | "vc_df = get_vc_df(train_df, 'maker')\n", 631 | "vc_df.head(10)" 632 | ] 633 | }, 634 | { 635 | "cell_type": "markdown", 636 | "metadata": {}, 637 | "source": [ 638 | "### model 컬럼 분석" 639 | ] 640 | }, 641 | { 642 | "cell_type": "code", 643 | "execution_count": 11, 644 | "metadata": {}, 645 | "outputs": [], 646 | "source": [ 647 | "# 메모리 사용량을 줄이기 위해 주석처리\n", 648 | "#vc_df = get_vc_df(train_df, 'model')\n", 649 | "#vc_df.head(10)" 650 | ] 651 | }, 652 | { 653 | "cell_type": "markdown", 654 | "metadata": {}, 655 | "source": [ 656 | "### price" 657 | ] 658 | }, 659 | { 660 | "cell_type": "code", 661 | "execution_count": 12, 662 | "metadata": {}, 663 | "outputs": [ 664 | { 665 | "data": { 666 | "text/html": [ 667 | "
\n", 668 | "\n", 681 | "\n", 682 | " \n", 683 | " \n", 684 | " \n", 685 | " \n", 686 | " \n", 687 | " \n", 688 | " \n", 689 | " \n", 690 | " \n", 691 | " \n", 692 | " \n", 693 | " \n", 694 | " \n", 695 | " \n", 696 | " \n", 697 | " \n", 698 | " \n", 699 | " \n", 700 | " \n", 701 | " \n", 702 | " \n", 703 | " \n", 704 | " \n", 705 | " \n", 706 | " \n", 707 | " \n", 708 | " \n", 709 | " \n", 710 | " \n", 711 | " \n", 712 | " \n", 713 | " \n", 714 | " \n", 715 | " \n", 716 | " \n", 717 | " \n", 718 | " \n", 719 | " \n", 720 | " \n", 721 | " \n", 722 | " \n", 723 | " \n", 724 | " \n", 725 | " \n", 726 | " \n", 727 | " \n", 728 | " \n", 729 | " \n", 730 | " \n", 731 | " \n", 732 | " \n", 733 | " \n", 734 | " \n", 735 | " \n", 736 | " \n", 737 | " \n", 738 | " \n", 739 | " \n", 740 | " \n", 741 | " \n", 742 | " \n", 743 | " \n", 744 | " \n", 745 | " \n", 746 | " \n", 747 | " \n", 748 | " \n", 749 | " \n", 750 | " \n", 751 | " \n", 752 | "
pricecountpercentage
0-1527082164.793349
18550088720.109062
21080065220.080174
3900065050.079965
41350058850.072343
5990057920.071200
69400050830.062484
71800048010.059018
8720046980.057752
98930042410.052134
\n", 753 | "
" 754 | ], 755 | "text/plain": [ 756 | " price count percentage\n", 757 | "0 -1 5270821 64.793349\n", 758 | "1 85500 8872 0.109062\n", 759 | "2 10800 6522 0.080174\n", 760 | "3 9000 6505 0.079965\n", 761 | "4 13500 5885 0.072343\n", 762 | "5 9900 5792 0.071200\n", 763 | "6 94000 5083 0.062484\n", 764 | "7 18000 4801 0.059018\n", 765 | "8 7200 4698 0.057752\n", 766 | "9 89300 4241 0.052134" 767 | ] 768 | }, 769 | "execution_count": 12, 770 | "metadata": {}, 771 | "output_type": "execute_result" 772 | } 773 | ], 774 | "source": [ 775 | "vc_df = get_vc_df(train_df, 'price')\n", 776 | "vc_df.head(10)" 777 | ] 778 | }, 779 | { 780 | "cell_type": "markdown", 781 | "metadata": {}, 782 | "source": [ 783 | "# 최종 결정된 DataFrame" 784 | ] 785 | }, 786 | { 787 | "cell_type": "code", 788 | "execution_count": 13, 789 | "metadata": {}, 790 | "outputs": [], 791 | "source": [ 792 | "# 불필요한 컬럼을 제거한 DataFrame 생성\n", 793 | "train_df = train_df[['pid', 'product', 'bcateid', 'mcateid', 'scateid', 'dcateid']]\n", 794 | "dev_df = dev_df[['pid', 'product', 'bcateid', 'mcateid', 'scateid', 'dcateid']]\n", 795 | "test_df = test_df[['pid', 'product', 'bcateid', 'mcateid', 'scateid', 'dcateid']]" 796 | ] 797 | }, 798 | { 799 | "cell_type": "code", 800 | "execution_count": 14, 801 | "metadata": { 802 | "scrolled": false 803 | }, 804 | "outputs": [ 805 | { 806 | "data": { 807 | "text/html": [ 808 | "
\n", 809 | "\n", 822 | "\n", 823 | " \n", 824 | " \n", 825 | " \n", 826 | " \n", 827 | " \n", 828 | " \n", 829 | " \n", 830 | " \n", 831 | " \n", 832 | " \n", 833 | " \n", 834 | " \n", 835 | " \n", 836 | " \n", 837 | " \n", 838 | " \n", 839 | " \n", 840 | " \n", 841 | " \n", 842 | " \n", 843 | " \n", 844 | " \n", 845 | " \n", 846 | " \n", 847 | " \n", 848 | " \n", 849 | " \n", 850 | " \n", 851 | " \n", 852 | " \n", 853 | " \n", 854 | " \n", 855 | " \n", 856 | " \n", 857 | " \n", 858 | " \n", 859 | " \n", 860 | " \n", 861 | " \n", 862 | " \n", 863 | " \n", 864 | " \n", 865 | " \n", 866 | " \n", 867 | " \n", 868 | " \n", 869 | " \n", 870 | " \n", 871 | " \n", 872 | " \n", 873 | " \n", 874 | " \n", 875 | " \n", 876 | " \n", 877 | " \n", 878 | " \n", 879 | " \n", 880 | " \n", 881 | "
pidproductbcateidmcateidscateiddcateid
0O4486751463직소퍼즐 - 1000조각 바다거북의 여행 (PL1275)112-1
1P3307178849[모리케이스]아이폰6S/6S+ tree farm101 - 다이어리케이스[바보사랑][무료배송]334-1
2R4424255515크리비아 기모 3부 속바지 GLG4314P556-1
3F3334315393[하프클럽/잭앤질]남성 솔리드 절개라인 포인트 포켓 팬츠 31133PT002_NA778-1
4N731678492코드프리혈당시험지50매/코드프리시험지/최장유효기간10911-1
\n", 882 | "
" 883 | ], 884 | "text/plain": [ 885 | " pid product bcateid \\\n", 886 | "0 O4486751463 직소퍼즐 - 1000조각 바다거북의 여행 (PL1275) 1 \n", 887 | "1 P3307178849 [모리케이스]아이폰6S/6S+ tree farm101 - 다이어리케이스[바보사랑][무료배송] 3 \n", 888 | "2 R4424255515 크리비아 기모 3부 속바지 GLG4314P 5 \n", 889 | "3 F3334315393 [하프클럽/잭앤질]남성 솔리드 절개라인 포인트 포켓 팬츠 31133PT002_NA 7 \n", 890 | "4 N731678492 코드프리혈당시험지50매/코드프리시험지/최장유효기간 10 \n", 891 | "\n", 892 | " mcateid scateid dcateid \n", 893 | "0 1 2 -1 \n", 894 | "1 3 4 -1 \n", 895 | "2 5 6 -1 \n", 896 | "3 7 8 -1 \n", 897 | "4 9 11 -1 " 898 | ] 899 | }, 900 | "execution_count": 14, 901 | "metadata": {}, 902 | "output_type": "execute_result" 903 | } 904 | ], 905 | "source": [ 906 | "train_df.head()" 907 | ] 908 | }, 909 | { 910 | "cell_type": "markdown", 911 | "metadata": {}, 912 | "source": [ 913 | "# product 칼럼 전처리 하기" 914 | ] 915 | }, 916 | { 917 | "cell_type": "markdown", 918 | "metadata": {}, 919 | "source": [ 920 | "### 센텐스피스 모델 학습" 921 | ] 922 | }, 923 | { 924 | "cell_type": "code", 925 | "execution_count": 15, 926 | "metadata": {}, 927 | "outputs": [ 928 | { 929 | "name": "stdout", 930 | "output_type": "stream", 931 | "text": [ 932 | "CPU times: user 16.7 s, sys: 268 ms, total: 16.9 s\n", 933 | "Wall time: 16.9 s\n" 934 | ] 935 | }, 936 | { 937 | "data": { 938 | "text/html": [ 939 | "
\n", 940 | "\n", 953 | "\n", 954 | " \n", 955 | " \n", 956 | " \n", 957 | " \n", 958 | " \n", 959 | " \n", 960 | " \n", 961 | " \n", 962 | " \n", 963 | " \n", 964 | " \n", 965 | " \n", 966 | " \n", 967 | " \n", 968 | " \n", 969 | " \n", 970 | " \n", 971 | " \n", 972 | " \n", 973 | " \n", 974 | " \n", 975 | " \n", 976 | " \n", 977 | " \n", 978 | " \n", 979 | " \n", 980 | " \n", 981 | " \n", 982 | " \n", 983 | " \n", 984 | " \n", 985 | " \n", 986 | " \n", 987 | " \n", 988 | " \n", 989 | " \n", 990 | " \n", 991 | " \n", 992 | " \n", 993 | " \n", 994 | " \n", 995 | " \n", 996 | " \n", 997 | " \n", 998 | " \n", 999 | " \n", 1000 | " \n", 1001 | " \n", 1002 | " \n", 1003 | " \n", 1004 | " \n", 1005 | " \n", 1006 | " \n", 1007 | " \n", 1008 | " \n", 1009 | " \n", 1010 | " \n", 1011 | " \n", 1012 | "
pidproductbcateidmcateidscateiddcateid
0O4486751463직소퍼즐 1000조각 바다거북의 여행 pl1275112-1
1P3307178849모리케이스 아이폰6s 6s tree farm101 다이어리케이스 바보사랑 무료배송334-1
2R4424255515크리비아 기모 3부 속바지 glg4314p556-1
3F3334315393하프클럽 잭앤질 남성 솔리드 절개라인 포인트 포켓 팬츠 31133pt002 na778-1
4N731678492코드프리혈당시험지50매 코드프리시험지 최장유효기간10911-1
\n", 1013 | "
" 1014 | ], 1015 | "text/plain": [ 1016 | " pid product bcateid \\\n", 1017 | "0 O4486751463 직소퍼즐 1000조각 바다거북의 여행 pl1275 1 \n", 1018 | "1 P3307178849 모리케이스 아이폰6s 6s tree farm101 다이어리케이스 바보사랑 무료배송 3 \n", 1019 | "2 R4424255515 크리비아 기모 3부 속바지 glg4314p 5 \n", 1020 | "3 F3334315393 하프클럽 잭앤질 남성 솔리드 절개라인 포인트 포켓 팬츠 31133pt002 na 7 \n", 1021 | "4 N731678492 코드프리혈당시험지50매 코드프리시험지 최장유효기간 10 \n", 1022 | "\n", 1023 | " mcateid scateid dcateid \n", 1024 | "0 1 2 -1 \n", 1025 | "1 3 4 -1 \n", 1026 | "2 5 6 -1 \n", 1027 | "3 7 8 -1 \n", 1028 | "4 9 11 -1 " 1029 | ] 1030 | }, 1031 | "execution_count": 15, 1032 | "metadata": {}, 1033 | "output_type": "execute_result" 1034 | } 1035 | ], 1036 | "source": [ 1037 | "%%time\n", 1038 | "import re\n", 1039 | "\n", 1040 | "# 특수기호를 나열한 패턴 문자열을 컴파일하여 패턴 객체를 얻는다.\n", 1041 | "p = re.compile('[\\!@#$%\\^&\\*\\(\\)\\-\\=\\[\\]\\{\\}\\.,/\\?~\\+\\'\"|_:;><`┃]')\n", 1042 | "\n", 1043 | "# 위의 패턴 문자열의 매칭되는 문자는 아래 코드를 통해서 빈공백으로 치환할 것이다.\n", 1044 | "\n", 1045 | "# 문장의 특수기호 제거 함수\n", 1046 | "def remove_special_characters(sentence, lower=True):\n", 1047 | " sentence = p.sub(' ', sentence) # 패턴 객체로 sentence 내의 특수기호를 공백문자로 치환한다.\n", 1048 | " sentence = ' '.join(sentence.split()) # sentence 내의 두개 이상 연속된 빈공백들을 하나의 빈공백으로 만든다.\n", 1049 | " if lower:\n", 1050 | " sentence = sentence.lower()\n", 1051 | " return sentence\n", 1052 | "\n", 1053 | "# product 칼럼에 특수기호를 제거하는 함수를 적용한 결과를 반환한다.\n", 1054 | "train_df['product'] = train_df['product'].map(remove_special_characters)\n", 1055 | "\n", 1056 | "train_df.head() # 특수기호가 제거된 train_df의 상단 5행만 출력" 1057 | ] 1058 | }, 1059 | { 1060 | "cell_type": "code", 1061 | "execution_count": 16, 1062 | "metadata": {}, 1063 | "outputs": [ 1064 | { 1065 | "name": "stdout", 1066 | "output_type": "stream", 1067 | "text": [ 1068 | "../input/processed/vocab/spm.vocab\n", 1069 | "../input/processed/vocab/spm.model\n", 1070 | "CPU times: user 2min 29s, sys: 1.72 s, total: 2min 30s\n", 1071 | "Wall time: 2min 27s\n" 1072 | ] 1073 | } 1074 | ], 1075 | "source": [ 1076 | "%%time\n", 1077 | "import sentencepiece as spm # sentencepiece 모듈을 가져온다.\n", 1078 | "\n", 1079 | "# product 칼럼의 상품명을 product.txt 파일명으로 저장한다.\n", 1080 | "with open(os.path.join(VOCAB_DIR, 'product.txt'), 'w', encoding='utf-8') as f:\n", 1081 | " f.write(train_df['product'].str.cat(sep='\\n'))\n", 1082 | "\n", 1083 | "# sentencepiece 모델을 학습시키는 함수이다.\n", 1084 | "def train_spm(txt_path, spm_path,\n", 1085 | " vocab_size=32000, input_sentence_size=1000000): \n", 1086 | " # input_sentence_size: 개수 만큼만 학습데이터로 사용된다.\n", 1087 | " # vocab_size: 사전 크기\n", 1088 | " spm.SentencePieceTrainer.Train(\n", 1089 | " f' --input={txt_path} --model_type=bpe'\n", 1090 | " f' --model_prefix={spm_path} --vocab_size={vocab_size}'\n", 1091 | " f' --input_sentence_size={input_sentence_size}'\n", 1092 | " f' --shuffle_input_sentence=true'\n", 1093 | " )\n", 1094 | "\n", 1095 | "# product.txt 파일로 sentencepiece 모델을 학습 시킨다. \n", 1096 | "# 학습이 완료되면 spm.model, spm.vocab 파일이 생성된다.\n", 1097 | "train_spm(txt_path=os.path.join(VOCAB_DIR, 'product.txt'), \n", 1098 | " spm_path=os.path.join(VOCAB_DIR, 'spm')) # spm 접두어\n", 1099 | "\n", 1100 | "# 센텐스피스 모델 학습이 완료되면 product.txt는 삭제\n", 1101 | "os.remove(os.path.join(VOCAB_DIR, 'product.txt'))\n", 1102 | "\n", 1103 | "# 필요한 파일이 제대로 생성됐는지 확인\n", 1104 | "for dirname, _, filenames in os.walk(VOCAB_DIR):\n", 1105 | " for filename in filenames:\n", 1106 | " print(os.path.join(dirname, filename))" 1107 | ] 1108 | }, 1109 | { 1110 | "cell_type": "markdown", 1111 | "metadata": {}, 1112 | "source": [ 1113 | "### train_df 전처리" 1114 | ] 1115 | }, 1116 | { 1117 | "cell_type": "code", 1118 | "execution_count": 17, 1119 | "metadata": {}, 1120 | "outputs": [ 1121 | { 1122 | "data": { 1123 | "text/html": [ 1124 | "
\n", 1125 | "\n", 1138 | "\n", 1139 | " \n", 1140 | " \n", 1141 | " \n", 1142 | " \n", 1143 | " \n", 1144 | " \n", 1145 | " \n", 1146 | " \n", 1147 | " \n", 1148 | " \n", 1149 | " \n", 1150 | " \n", 1151 | " \n", 1152 | " \n", 1153 | " \n", 1154 | " \n", 1155 | " \n", 1156 | " \n", 1157 | " \n", 1158 | " \n", 1159 | " \n", 1160 | " \n", 1161 | " \n", 1162 | " \n", 1163 | " \n", 1164 | " \n", 1165 | " \n", 1166 | " \n", 1167 | " \n", 1168 | " \n", 1169 | " \n", 1170 | " \n", 1171 | " \n", 1172 | " \n", 1173 | "
producttokens
0직소퍼즐 1000조각 바다거북의 여행 pl1275▁직소퍼즐 ▁1000 조각 ▁바다 거북 의 ▁여행 ▁pl 12 75
1모리케이스 아이폰6s 6s tree farm101 다이어리케이스 바보사랑 무료배송▁모리케이스 ▁아이폰 6 s ▁6 s ▁tree ▁farm 101 ▁다이어리케이스 ▁바보사랑 ▁무료배송
2크리비아 기모 3부 속바지 glg4314p▁크리비아 ▁기모 ▁3 부 ▁속바지 ▁gl g 43 14 p
3하프클럽 잭앤질 남성 솔리드 절개라인 포인트 포켓 팬츠 31133pt002 na▁하프클럽 ▁잭앤질 ▁남성 ▁솔리드 ▁절개라인 ▁포인트 ▁포켓 ▁팬츠 ▁311 33 pt 002 ▁na
4코드프리혈당시험지50매 코드프리시험지 최장유효기간▁코드 프리 혈 당 시험 지 50 매 ▁코드 프리 시험 지 ▁최 장 유 효 기간
\n", 1174 | "
" 1175 | ], 1176 | "text/plain": [ 1177 | " product \\\n", 1178 | "0 직소퍼즐 1000조각 바다거북의 여행 pl1275 \n", 1179 | "1 모리케이스 아이폰6s 6s tree farm101 다이어리케이스 바보사랑 무료배송 \n", 1180 | "2 크리비아 기모 3부 속바지 glg4314p \n", 1181 | "3 하프클럽 잭앤질 남성 솔리드 절개라인 포인트 포켓 팬츠 31133pt002 na \n", 1182 | "4 코드프리혈당시험지50매 코드프리시험지 최장유효기간 \n", 1183 | "\n", 1184 | " tokens \n", 1185 | "0 ▁직소퍼즐 ▁1000 조각 ▁바다 거북 의 ▁여행 ▁pl 12 75 \n", 1186 | "1 ▁모리케이스 ▁아이폰 6 s ▁6 s ▁tree ▁farm 101 ▁다이어리케이스 ▁바보사랑 ▁무료배송 \n", 1187 | "2 ▁크리비아 ▁기모 ▁3 부 ▁속바지 ▁gl g 43 14 p \n", 1188 | "3 ▁하프클럽 ▁잭앤질 ▁남성 ▁솔리드 ▁절개라인 ▁포인트 ▁포켓 ▁팬츠 ▁311 33 pt 002 ▁na \n", 1189 | "4 ▁코드 프리 혈 당 시험 지 50 매 ▁코드 프리 시험 지 ▁최 장 유 효 기간 " 1190 | ] 1191 | }, 1192 | "execution_count": 17, 1193 | "metadata": {}, 1194 | "output_type": "execute_result" 1195 | } 1196 | ], 1197 | "source": [ 1198 | "# 센텐스피스 모델을 로드한다.\n", 1199 | "sp = spm.SentencePieceProcessor()\n", 1200 | "sp.Load(os.path.join(VOCAB_DIR, 'spm.model'))\n", 1201 | "\n", 1202 | "# product 칼럼의 상품명을 분절한 결과를 tokenized_product 칼럼에 저장한다.\n", 1203 | "train_df['tokens'] = train_df['product'].map(lambda x: \" \".join(sp.EncodeAsPieces(x)) )\n", 1204 | "\n", 1205 | "train_df[['product', 'tokens']].head()" 1206 | ] 1207 | }, 1208 | { 1209 | "cell_type": "markdown", 1210 | "metadata": {}, 1211 | "source": [ 1212 | "### dev_df, test_df 전처리" 1213 | ] 1214 | }, 1215 | { 1216 | "cell_type": "code", 1217 | "execution_count": 18, 1218 | "metadata": {}, 1219 | "outputs": [], 1220 | "source": [ 1221 | "# 특수기호를 공백문자로 치환\n", 1222 | "dev_df['product'] = dev_df['product'].map(remove_special_characters) \n", 1223 | "# product 칼럼을 분절한 뒤 token_id로 치환\n", 1224 | "dev_df['tokens'] = dev_df['product'].map(lambda x: \" \".join([str(token_id) for token_id in sp.EncodeAsPieces(x)]))\n", 1225 | "\n", 1226 | "# 특수기호를 공백문자로 치환\n", 1227 | "test_df['product'] = test_df['product'].map(remove_special_characters) \n", 1228 | "# product 칼럼을 분절한 뒤 token_id로 치환\n", 1229 | "test_df['tokens'] = test_df['product'].map(lambda x: \" \".join([str(token_id) for token_id in sp.EncodeAsPieces(x)]))" 1230 | ] 1231 | }, 1232 | { 1233 | "cell_type": "markdown", 1234 | "metadata": {}, 1235 | "source": [ 1236 | "# 전처리된 대회 데이터를 파일로 저장한다." 1237 | ] 1238 | }, 1239 | { 1240 | "cell_type": "markdown", 1241 | "metadata": {}, 1242 | "source": [ 1243 | "### 전처리가 완료된 train_df, dev_df, test_df를 파일로 저장한다." 1244 | ] 1245 | }, 1246 | { 1247 | "cell_type": "code", 1248 | "execution_count": 19, 1249 | "metadata": {}, 1250 | "outputs": [], 1251 | "source": [ 1252 | "# product, tokenized_product 칼럼을 제외한 칼럼만을 남긴다.\n", 1253 | "columns = ['pid', 'tokens', 'bcateid', 'mcateid', 'scateid', 'dcateid']\n", 1254 | "train_df = train_df[columns] \n", 1255 | "dev_df = dev_df[columns] \n", 1256 | "test_df = test_df[columns] \n", 1257 | "\n", 1258 | "# csv 포맷으로 저장한다.\n", 1259 | "train_df.to_csv(os.path.join(PROCESSED_DATA_DIR, 'train.csv'), index=False) \n", 1260 | "dev_df.to_csv(os.path.join(PROCESSED_DATA_DIR, 'dev.csv'), index=False) \n", 1261 | "test_df.to_csv(os.path.join(PROCESSED_DATA_DIR, 'test.csv'), index=False) " 1262 | ] 1263 | }, 1264 | { 1265 | "cell_type": "markdown", 1266 | "metadata": {}, 1267 | "source": [ 1268 | "### 용량이 큰 img_feat 컬럼은 별도의 hdf5 포맷의 파일로 저장" 1269 | ] 1270 | }, 1271 | { 1272 | "cell_type": "code", 1273 | "execution_count": 20, 1274 | "metadata": { 1275 | "scrolled": true 1276 | }, 1277 | "outputs": [ 1278 | { 1279 | "data": { 1280 | "application/vnd.jupyter.widget-view+json": { 1281 | "model_id": "3e63c37d29d24bb1832da53300c98197", 1282 | "version_major": 2, 1283 | "version_minor": 0 1284 | }, 1285 | "text/plain": [ 1286 | "HBox(children=(FloatProgress(value=0.0, description='train,img_feat', max=9.0, style=ProgressStyle(description…" 1287 | ] 1288 | }, 1289 | "metadata": {}, 1290 | "output_type": "display_data" 1291 | }, 1292 | { 1293 | "name": "stdout", 1294 | "output_type": "stream", 1295 | "text": [ 1296 | "\n" 1297 | ] 1298 | }, 1299 | { 1300 | "data": { 1301 | "application/vnd.jupyter.widget-view+json": { 1302 | "model_id": "584bdeba62d74394aa2fa189e03fc0cc", 1303 | "version_major": 2, 1304 | "version_minor": 0 1305 | }, 1306 | "text/plain": [ 1307 | "HBox(children=(FloatProgress(value=0.0, description='dev,img_feat', max=1.0, style=ProgressStyle(description_w…" 1308 | ] 1309 | }, 1310 | "metadata": {}, 1311 | "output_type": "display_data" 1312 | }, 1313 | { 1314 | "name": "stdout", 1315 | "output_type": "stream", 1316 | "text": [ 1317 | "\n" 1318 | ] 1319 | }, 1320 | { 1321 | "data": { 1322 | "application/vnd.jupyter.widget-view+json": { 1323 | "model_id": "0cb50081b8c84d45b5b47a7d78d12f92", 1324 | "version_major": 2, 1325 | "version_minor": 0 1326 | }, 1327 | "text/plain": [ 1328 | "HBox(children=(FloatProgress(value=0.0, description='test,img_feat', max=2.0, style=ProgressStyle(description_…" 1329 | ] 1330 | }, 1331 | "metadata": {}, 1332 | "output_type": "display_data" 1333 | }, 1334 | { 1335 | "name": "stdout", 1336 | "output_type": "stream", 1337 | "text": [ 1338 | "\n", 1339 | "../input/processed/dev_img_feat.h5\n", 1340 | "../input/processed/train_img_feat.h5\n", 1341 | "../input/processed/test_img_feat.h5\n", 1342 | "../input/processed/dev.csv\n", 1343 | "../input/processed/train.csv\n", 1344 | "../input/processed/test.csv\n", 1345 | "../input/processed/vocab/spm.vocab\n", 1346 | "../input/processed/vocab/spm.model\n" 1347 | ] 1348 | } 1349 | ], 1350 | "source": [ 1351 | "# image_feature는 데이터의 크기가 크므로 처리함수를 별도로 분리하였다.\n", 1352 | "def save_column_data(input_path_list, div, col, n_img_rows, output_path):\n", 1353 | " # img_feat를 저장할 h5 파일을 생성\n", 1354 | " h_out = h5py.File(output_path, 'w') \n", 1355 | " # 대회데이터의 상품개수 x 2048(img_feat 크기)로 dataset을 할당한다.\n", 1356 | " h_out.create_dataset(col, (n_img_rows, 2048), dtype=np.float32)\n", 1357 | " \n", 1358 | " offset_out = 0\n", 1359 | " \n", 1360 | " # h5포맷의 대회데이터에서 img_feat 칼럼만 읽어서 h5포맷으로 다시 저장한다.\n", 1361 | " for in_path in tqdm(input_path_list, desc=f'{div},{col}'):\n", 1362 | " h_in = h5py.File(in_path, 'r')\n", 1363 | " sz = h_in[div][col].shape[0]\n", 1364 | " h_out[col][offset_out:offset_out+sz] = h_in[div][col][:]\n", 1365 | " offset_out += sz\n", 1366 | " h_in.close()\n", 1367 | " h_out.close()\n", 1368 | "\n", 1369 | "\n", 1370 | "save_column_data(train_path_list, div='train', col='img_feat', n_img_rows=len(train_df), \n", 1371 | " output_path=os.path.join(PROCESSED_DATA_DIR, 'train_img_feat.h5'))\n", 1372 | "save_column_data(dev_path_list, div='dev', col='img_feat', n_img_rows=len(dev_df), \n", 1373 | " output_path=os.path.join(PROCESSED_DATA_DIR, 'dev_img_feat.h5'))\n", 1374 | "save_column_data(test_path_list, div='test', col='img_feat', n_img_rows=len(test_df), \n", 1375 | " output_path=os.path.join(PROCESSED_DATA_DIR, 'test_img_feat.h5'))\n", 1376 | "\n", 1377 | "# 파일이 제대로 생성됐는지 확인\n", 1378 | "for dirname, _, filenames in os.walk(PROCESSED_DATA_DIR):\n", 1379 | " for filename in filenames:\n", 1380 | " print(os.path.join(dirname, filename))" 1381 | ] 1382 | } 1383 | ], 1384 | "metadata": { 1385 | "kernelspec": { 1386 | "display_name": "Python 3", 1387 | "language": "python", 1388 | "name": "python3" 1389 | }, 1390 | "language_info": { 1391 | "codemirror_mode": { 1392 | "name": "ipython", 1393 | "version": 3 1394 | }, 1395 | "file_extension": ".py", 1396 | "mimetype": "text/x-python", 1397 | "name": "python", 1398 | "nbconvert_exporter": "python", 1399 | "pygments_lexer": "ipython3", 1400 | "version": "3.7.7" 1401 | } 1402 | }, 1403 | "nbformat": 4, 1404 | "nbformat_minor": 4 1405 | } 1406 | -------------------------------------------------------------------------------- /code/preprocess.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import json 4 | import h5py 5 | import logging 6 | import numpy as np 7 | import pandas as pd 8 | import sentencepiece as spm 9 | from tqdm import tqdm 10 | 11 | 12 | RAW_DATA_DIR = "../input/raw_data" # 카카오에서 다운로드 받은 데이터의 디렉터리 13 | PROCESSED_DATA_DIR = '../input/processed' # 전처리된 데이터가 저장될 디렉터리 14 | VOCAB_DIR =os.path.join(PROCESSED_DATA_DIR, 'vocab') # 전처리에 사용될 사전 파일이 저장될 디렉터리 15 | 16 | # 학습에 사용될 파일 리스트 17 | TRAIN_FILE_LIST = [ 18 | "train.chunk.01", 19 | "train.chunk.02", 20 | "train.chunk.03", 21 | "train.chunk.04", 22 | "train.chunk.05", 23 | "train.chunk.06", 24 | "train.chunk.07", 25 | "train.chunk.08", 26 | "train.chunk.09" 27 | ] 28 | 29 | # 개발에 사용될 파일 리스트. 공개 리더보드 점수를 내는데 사용된다. 30 | DEV_FILE_LIST = [ 31 | "dev.chunk.01" 32 | ] 33 | 34 | # 테스트에 사용될 파일 리스트. 파이널 리더보드 점수를 내는데 사용된다. 35 | TEST_FILE_LIST = [ 36 | "test.chunk.01", 37 | "test.chunk.02", 38 | ] 39 | 40 | 41 | def get_logger(): 42 | FORMAT = '[%(levelname)s]%(asctime)s:%(name)s:%(message)s' 43 | logging.basicConfig(format=FORMAT, level=logging.INFO) 44 | logger = logging.getLogger('main') 45 | logger.setLevel(logging.DEBUG) 46 | return logger 47 | logger = get_logger() 48 | 49 | 50 | # 문장의 특수기호 제거 함수 51 | def remove_special_characters(sentence, lower=True): 52 | # 특수기호를 나열한 패턴 문자열을 컴파일하여 패턴 객체를 얻는다. 53 | p = re.compile('[\!@#$%\^&\*\(\)\-\=\[\]\{\}\.,/\?~\+\'"|_:;><`┃]') 54 | 55 | sentence = p.sub(' ', sentence) # 패턴 객체로 sentence 내의 특수기호를 공백문자로 치환한다. 56 | sentence = ' '.join(sentence.split()) # sentence 내의 두개 이상 연속된 빈공백들을 하나의 빈공백으로 만든다. 57 | if lower: 58 | sentence = sentence.lower() 59 | return sentence 60 | 61 | # path_list의 파일에서 col 변수에 해당하는 컬럼 값들을 가져온다. 62 | def get_column_data(path_list, div, col): 63 | col_data = [] 64 | for path in path_list: 65 | h = h5py.File(path, 'r') 66 | col_data.append(h[div][col][:]) 67 | h.close() 68 | return np.concatenate(col_data) 69 | 70 | # path_list의 파일에서 학습에 필요한 컬럼들을 DataFrame 포맷으로 반환한다. 71 | def get_dataframe(path_list, div): 72 | pids = get_column_data(path_list, div, col='pid') 73 | products = get_column_data(path_list, div, col='product') 74 | bcates = get_column_data(path_list, div, col='bcateid') 75 | mcates = get_column_data(path_list, div, col='mcateid') 76 | scates = get_column_data(path_list, div, col='scateid') 77 | dcates = get_column_data(path_list, div, col='dcateid') 78 | 79 | df = pd.DataFrame({'pid': pids, 'product':products, 80 | 'bcateid':bcates, 'mcateid':mcates, 'scateid':scates, 'dcateid':dcates} ) 81 | 82 | # 바이트 열로 인코딩 상품제목과 상품ID를 유니코드 변환한다. 83 | df['pid'] = df['pid'].map(lambda x: x.decode('utf-8')) 84 | df['product'] = df['product'].map(lambda x: x.decode('utf-8')) 85 | return df 86 | 87 | 88 | # sentencepiece 모델을 학습시키는 함수이다. 89 | def train_spm(txt_path, spm_path, 90 | vocab_size=32000, input_sentence_size=1000000): 91 | # input_sentence_size: 개수 만큼만 학습데이터로 사용된다. 92 | # vocab_size: 사전 크기 93 | spm.SentencePieceTrainer.Train( 94 | f' --input={txt_path} --model_type=bpe' 95 | f' --model_prefix={spm_path} --vocab_size={vocab_size}' 96 | f' --input_sentence_size={input_sentence_size}' 97 | f' --shuffle_input_sentence=true' 98 | f' --minloglevel=2' 99 | ) 100 | 101 | 102 | # image_feature는 데이터의 크기가 크므로 처리함수를 별도로 분리하였다. 103 | def save_column_data(input_path_list, div, col, n_img_rows, output_path): 104 | # img_feat를 저장할 h5 파일을 생성 105 | h_out = h5py.File(output_path, 'w') 106 | # 대회데이터의 상품개수 x 2048(img_feat 크기)로 dataset을 할당한다. 107 | h_out.create_dataset(col, (n_img_rows, 2048), dtype=np.float32) 108 | 109 | offset_out = 0 110 | 111 | # h5포맷의 대회데이터에서 img_feat 칼럼만 읽어서 h5포맷으로 다시 저장한다. 112 | for in_path in tqdm(input_path_list, desc=f'{div},{col}'): 113 | h_in = h5py.File(in_path, 'r') 114 | sz = h_in[div][col].shape[0] 115 | h_out[col][offset_out:offset_out+sz] = h_in[div][col][:] 116 | offset_out += sz 117 | h_in.close() 118 | h_out.close() 119 | 120 | 121 | def preprocess(): 122 | # 파일명과 실제 파일이 위치한 디렉토리를 결합한다. 123 | train_path_list = [os.path.join(RAW_DATA_DIR, fn) for fn in TRAIN_FILE_LIST] 124 | dev_path_list = [os.path.join(RAW_DATA_DIR, fn) for fn in DEV_FILE_LIST] 125 | test_path_list = [os.path.join(RAW_DATA_DIR, fn) for fn in TEST_FILE_LIST] 126 | 127 | # PROCESSED_DATA_DIR과 VOCAB_DIR를 생성한다. 128 | os.makedirs(PROCESSED_DATA_DIR, exist_ok=True) 129 | os.makedirs(VOCAB_DIR, exist_ok=True) 130 | 131 | logger.info('loading ...') 132 | train_df = get_dataframe(train_path_list, 'train') 133 | dev_df = get_dataframe(dev_path_list, 'dev') 134 | test_df = get_dataframe(test_path_list, 'test') 135 | 136 | # product 칼럼에 특수기호를 제거하는 함수를 적용한 결과를 반환한다. 137 | train_df['product'] = train_df['product'].map(remove_special_characters) 138 | 139 | # product 칼럼의 상품명을 product.txt 파일명으로 저장한다. 140 | with open(os.path.join(VOCAB_DIR, 'product.txt'), 'w', encoding='utf-8') as f: 141 | f.write(train_df['product'].str.cat(sep='\n')) 142 | 143 | # product.txt 파일로 sentencepiece 모델을 학습 시킨다. 144 | # 학습이 완료되면 spm.model, spm.vocab 파일이 생성된다. 145 | logger.info('training sentencepiece model ...') 146 | train_spm(txt_path=os.path.join(VOCAB_DIR, 'product.txt'), 147 | spm_path=os.path.join(VOCAB_DIR, 'spm')) # spm 접두어 148 | 149 | # 센텐스피스 모델 학습이 완료되면 product.txt는 삭제 150 | os.remove(os.path.join(VOCAB_DIR, 'product.txt')) 151 | 152 | # 필요한 파일이 제대로 생성됐는지 확인 153 | for dirname, _, filenames in os.walk(VOCAB_DIR): 154 | for filename in filenames: 155 | logger.info(os.path.join(dirname, filename)) 156 | 157 | logger.info('tokenizing product ...') 158 | # 센텐스피스 모델을 로드한다. 159 | sp = spm.SentencePieceProcessor() 160 | sp.Load(os.path.join(VOCAB_DIR, 'spm.model')) 161 | 162 | # product 칼럼의 상품명을 분절한 결과를 tokenized_product 칼럼에 저장한다. 163 | train_df['tokens'] = train_df['product'].map(lambda x: " ".join(sp.EncodeAsPieces(x)) ) 164 | 165 | # 특수기호를 공백문자로 치환 166 | dev_df['product'] = dev_df['product'].map(remove_special_characters) 167 | # product 칼럼을 분절한 뒤 token_id로 치환 168 | dev_df['tokens'] = dev_df['product'].map(lambda x: " ".join([str(token_id) for token_id in sp.EncodeAsPieces(x)])) 169 | 170 | # 특수기호를 공백문자로 치환 171 | test_df['product'] = test_df['product'].map(remove_special_characters) 172 | # product 칼럼을 분절한 뒤 token_id로 치환 173 | test_df['tokens'] = test_df['product'].map(lambda x: " ".join([str(token_id) for token_id in sp.EncodeAsPieces(x)])) 174 | 175 | 176 | # product, tokenized_product 칼럼을 제외한 칼럼만을 남긴다. 177 | columns = ['pid', 'tokens', 'bcateid', 'mcateid', 'scateid', 'dcateid'] 178 | train_df = train_df[columns] 179 | dev_df = dev_df[columns] 180 | test_df = test_df[columns] 181 | 182 | # csv 포맷으로 저장한다. 183 | train_df.to_csv(os.path.join(PROCESSED_DATA_DIR, 'train.csv'), index=False) 184 | dev_df.to_csv(os.path.join(PROCESSED_DATA_DIR, 'dev.csv'), index=False) 185 | test_df.to_csv(os.path.join(PROCESSED_DATA_DIR, 'test.csv'), index=False) 186 | 187 | logger.info('processing img_feat ...') 188 | save_column_data(train_path_list, div='train', col='img_feat', n_img_rows=len(train_df), 189 | output_path=os.path.join(PROCESSED_DATA_DIR, 'train_img_feat.h5')) 190 | save_column_data(dev_path_list, div='dev', col='img_feat', n_img_rows=len(dev_df), 191 | output_path=os.path.join(PROCESSED_DATA_DIR, 'dev_img_feat.h5')) 192 | save_column_data(test_path_list, div='test', col='img_feat', n_img_rows=len(test_df), 193 | output_path=os.path.join(PROCESSED_DATA_DIR, 'test_img_feat.h5')) 194 | # 파일이 제대로 생성됐는지 확인 195 | for dirname, _, filenames in os.walk(PROCESSED_DATA_DIR): 196 | for filename in filenames: 197 | logger.info(os.path.join(dirname, filename)) 198 | 199 | 200 | if __name__ == '__main__': 201 | preprocess() 202 | 203 | -------------------------------------------------------------------------------- /code/train.bat: -------------------------------------------------------------------------------- 1 | python train.py --stratified --fold 0 2 | python train.py --stratified --fold 1 3 | python train.py --stratified --fold 2 4 | python train.py --stratified --fold 3 5 | python train.py --stratified --fold 4 6 | -------------------------------------------------------------------------------- /code/train.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | import math 4 | import torch 5 | import random 6 | import argparse 7 | import cate_dataset 8 | import cate_model 9 | import numpy as np 10 | import pandas as pd 11 | from torch.utils.data import DataLoader 12 | from sklearn.model_selection import StratifiedKFold, KFold 13 | from transformers import AdamW, get_linear_schedule_with_warmup 14 | 15 | import warnings 16 | warnings.filterwarnings(action='ignore') 17 | 18 | 19 | # 전처리된 데이터가 저장된 디렉터리 20 | DB_PATH=f'../input/processed' 21 | 22 | # 토큰을 인덱스로 치환할 때 사용될 사전 파일이 저장된 디렉터리 23 | VOCAB_DIR=os.path.join(DB_PATH, 'vocab') 24 | 25 | # 학습된 모델의 파라미터가 저장될 디렉터리 26 | MODEL_PATH=f'../model' 27 | 28 | 29 | # 미리 정의된 설정 값 30 | class CFG: 31 | learning_rate=3.0e-4 # 러닝 레이트 32 | batch_size=1024 # 배치 사이즈 33 | num_workers=4 # 워커의 개수 34 | print_freq=100 # 결과 출력 빈도 35 | start_epoch=0 # 시작 에폭 36 | num_train_epochs=10 # 학습할 에폭수 37 | warmup_steps=100 # lr을 서서히 증가시킬 step 수 38 | max_grad_norm=10 # 그래디언트 클리핑에 사용 39 | weight_decay=0.01 40 | dropout=0.2 # dropout 확률 41 | hidden_size=512 # 은닉 크기 42 | intermediate_size=256 # TRANSFORMER셀의 intermediate 크기 43 | nlayers=2 # BERT의 층수 44 | nheads=8 # BERT의 head 개수 45 | seq_len=64 # 토큰의 최대 길이 46 | n_b_cls = 57 + 1 # 대카테고리 개수 47 | n_m_cls = 552 + 1 # 중카테고리 개수 48 | n_s_cls = 3190 + 1 # 소카테고리 개수 49 | n_d_cls = 404 + 1 # 세카테고리 개수 50 | vocab_size = 32000 # 토큰의 유니크 인덱스 개수 51 | img_feat_size = 2048 # 이미지 피처 벡터의 크기 52 | type_vocab_size = 30 # 타입의 유니크 인덱스 개수 53 | csv_path = os.path.join(DB_PATH, 'train.csv') 54 | h5_path = os.path.join(DB_PATH, 'train_img_feat.h5') 55 | 56 | 57 | def main(): 58 | # 명령행에서 받을 키워드 인자를 설정합니다. 59 | parser = argparse.ArgumentParser("") 60 | parser.add_argument("--model", type=str, default='') 61 | parser.add_argument("--batch_size", type=int, default=CFG.batch_size) 62 | parser.add_argument("--nepochs", type=int, default=CFG.num_train_epochs) 63 | parser.add_argument("--seq_len", type=int, default=CFG.seq_len) 64 | parser.add_argument("--nworkers", type=int, default=CFG.num_workers) 65 | parser.add_argument("--wsteps", type=int, default=CFG.warmup_steps) 66 | parser.add_argument("--seed", type=int, default=7) 67 | parser.add_argument("--nlayers", type=int, default=CFG.nlayers) 68 | parser.add_argument("--nheads", type=int, default=CFG.nheads) 69 | parser.add_argument("--hidden_size", type=int, default=CFG.hidden_size) 70 | parser.add_argument("--fold", type=int, default=0) 71 | parser.add_argument("--stratified", action='store_true') 72 | parser.add_argument("--lr", type=float, default=CFG.learning_rate) 73 | parser.add_argument("--dropout", type=float, default=CFG.dropout) 74 | args = parser.parse_args() 75 | 76 | # 키워드 인자로 받은 값을 CFG로 다시 저장합니다. 77 | CFG.batch_size=args.batch_size 78 | CFG.num_train_epochs=args.nepochs 79 | CFG.seq_len=args.seq_len 80 | CFG.num_workers=args.nworkers 81 | CFG.warmup_steps=args.wsteps 82 | CFG.learning_rate=args.lr 83 | CFG.dropout=args.dropout 84 | CFG.seed = args.seed 85 | CFG.nlayers = args.nlayers 86 | CFG.nheads = args.nheads 87 | CFG.hidden_size = args.hidden_size 88 | print(CFG.__dict__) 89 | 90 | # 랜덤 시드를 설정하여 매 코드를 실행할 때마다 동일한 결과를 얻게 합니다. 91 | os.environ['PYTHONHASHSEED'] = str(CFG.seed) 92 | random.seed(CFG.seed) 93 | np.random.seed(CFG.seed) 94 | torch.manual_seed(CFG.seed) 95 | torch.cuda.manual_seed(CFG.seed) 96 | torch.backends.cudnn.deterministic = True 97 | 98 | # 전처리된 데이터를 읽어옵니다. 99 | print('loading ...') 100 | train_df = pd.read_csv(CFG.csv_path, dtype={'tokens':str}) 101 | train_df['img_idx'] = train_df.index # 몇 번째 행인지 img_idx 칼럼에 기록 102 | 103 | # StratifiedKFold 사용 104 | if args.stratified: 105 | print('use StratifiedKFold ...') 106 | # 대/중/소/세 카테고리를 결합하여 유니크 카테고리를 만듭니다. 107 | train_df['unique_cateid'] = (train_df['bcateid'].astype('str') + 108 | train_df['mcateid'].astype('str') + 109 | train_df['scateid'].astype('str') + 110 | train_df['dcateid'].astype('str')).astype('category') 111 | train_df['unique_cateid'] = train_df['unique_cateid'].cat.codes 112 | 113 | # StratifiedKFold을 사용해 데이터셋을 학습셋(train_df)과 검증셋(valid_df)으로 나눕니다. 114 | folds = StratifiedKFold(n_splits=5, random_state=CFG.seed, shuffle=True) 115 | train_idx, valid_idx = list(folds.split(train_df.values, train_df['unique_cateid']))[args.fold] 116 | else: 117 | # KFold을 사용해 데이터셋을 학습셋(train_df)과 검증셋(valid_df)으로 나눕니다. 118 | folds = KFold(n_splits=5, random_state=CFG.seed, shuffle=True) 119 | train_idx, valid_idx = list(folds.split(train_df.values))[args.fold] 120 | valid_df = train_df.iloc[valid_idx] 121 | train_df = train_df.iloc[train_idx] 122 | 123 | # 토큰을 대응되는 인덱스로 치환할 때 사용될 딕셔너리를 로딩합니다. 124 | vocab = [line.split('\t')[0] for line in open(os.path.join(VOCAB_DIR, 'spm.vocab'), encoding='utf-8').readlines()] 125 | token2id = dict([(w, i) for i, w in enumerate(vocab)]) 126 | print('loading ... done') 127 | 128 | # 학습에 적합한 형태의 샘플을 가져오는 CateDataset의 인스턴스를 만듭니다. 129 | train_db = cate_dataset.CateDataset(train_df, CFG.h5_path, token2id, 130 | CFG.seq_len, CFG.type_vocab_size) 131 | valid_db = cate_dataset.CateDataset(valid_df, CFG.h5_path, token2id, 132 | CFG.seq_len, CFG.type_vocab_size) 133 | 134 | # 여러 개의 워커로 빠르게 배치(미니배치)를 생성하도록 DataLoader로 135 | # CateDataset 인스턴스를 감싸 줍니다. 136 | train_loader = DataLoader( 137 | train_db, batch_size=CFG.batch_size, shuffle=True, drop_last=True, 138 | num_workers=CFG.num_workers, pin_memory=True) 139 | 140 | valid_loader = DataLoader( 141 | valid_db, batch_size=CFG.batch_size, shuffle=False, 142 | num_workers=CFG.num_workers, pin_memory=False) 143 | 144 | # 카테고리 분류기 모델을 생성합니다. 145 | model = cate_model.CateClassifier(CFG) 146 | 147 | # 모델의 파라미터를 GPU메모리로 옮깁니다. 148 | model.cuda() 149 | 150 | # 모델의 파라미터 수를 출력합니다. 151 | def count_parameters(model): 152 | return sum(p.numel() for p in model.parameters() if p.requires_grad) 153 | print('parameters: ', count_parameters(model)) 154 | 155 | # GPU가 2개 이상이면 데이터패러럴로 학습 가능하게 만듭니다. 156 | n_gpu = torch.cuda.device_count() 157 | if n_gpu > 1: 158 | model = torch.nn.DataParallel(model) 159 | 160 | # 학습 동안 수행될 총 스텝 수 161 | # 데이터셋을 배치크기로 나눈 것이 1에폭 동안 스텝 수 162 | # 총 스텝 수 = 1에폭 스텝 수 * 총 에폭 수 163 | num_train_optimization_steps = int( 164 | len(train_db) / CFG.batch_size) * (CFG.num_train_epochs) 165 | print('num_train_optimization_steps', num_train_optimization_steps) 166 | 167 | # 파라미터 그룹핑 정보 생성 168 | # 가중치 감쇠(weight decay) 미적용 파라미터 그룹과 적용 파라미터로 나눔 169 | param_optimizer = list(model.named_parameters()) 170 | no_decay = ['bias', 'LayerNorm.bias', 'LayerNorm.weight'] 171 | optimizer_grouped_parameters = [ 172 | {'params':[p for n, p in param_optimizer if not any(nd in n for nd in no_decay)], 173 | 'weight_decay': 0.01}, 174 | {'params': [p for n, p in param_optimizer if any(nd in n for nd in no_decay)], 175 | 'weight_decay': 0.0} 176 | ] 177 | 178 | # AdamW 옵티마이저 생성 179 | optimizer = AdamW(optimizer_grouped_parameters, 180 | lr=CFG.learning_rate, 181 | weight_decay=CFG.weight_decay, 182 | ) 183 | 184 | # learning_rate가 선형적으로 감소하는 스케줄러 생성 185 | scheduler = get_linear_schedule_with_warmup(optimizer, 186 | num_warmup_steps=CFG.warmup_steps, 187 | num_training_steps=num_train_optimization_steps) 188 | print('use WarmupLinearSchedule ...') 189 | 190 | def get_lr(): 191 | return scheduler.get_lr()[0] 192 | 193 | log_df = pd.DataFrame() # 에폭 별 실험결과 로그를 저장할 데이터 프레임 194 | curr_lr = get_lr() 195 | print(f'initial learning rate:{curr_lr}') 196 | 197 | # (num_train_epochs - start_epoch) 횟수 만큼 학습을 진행합니다. 198 | for epoch in range(CFG.start_epoch, CFG.num_train_epochs): 199 | 200 | # 한 에폭의 결과가 집계된 한 행을 반환합니다. 201 | def get_log_row_df(epoch, lr, train_res, valid_res): 202 | log_row = {'EPOCH':epoch, 'LR':lr, 203 | 'TRAIN_LOSS':train_res[0], 'TRAIN_OACC':train_res[1], 204 | 'TRAIN_BACC':train_res[2], 'TRAIN_MACC':train_res[3], 205 | 'TRAIN_SACC':train_res[4], 'TRAIN_DACC':train_res[5], 206 | 'VALID_LOSS':valid_res[0], 'VALID_OACC':valid_res[1], 207 | 'VALID_BACC':valid_res[2], 'VALID_MACC':valid_res[3], 208 | 'VALID_SACC':valid_res[4], 'VALID_DACC':valid_res[5], 209 | } 210 | return pd.DataFrame(log_row, index=[0]) 211 | 212 | # 학습을 진행하고 loss나 accuracy와 같은 결과를 반환합니다. 213 | train_res = train(train_loader, model, optimizer, epoch, scheduler) 214 | # 검증을 진행하고 loss나 accuracy와 같은 결과를 반환합니다. 215 | valid_res = validate(valid_loader, model) 216 | curr_lr = get_lr() 217 | print(f'set the learning_rate: {curr_lr}') 218 | 219 | log_row_df = get_log_row_df(epoch, curr_lr, train_res, valid_res) 220 | # log_df에 결과가 집계된 한 행을 추가합니다. 221 | log_df = log_df.append(log_row_df, sort=False) 222 | print(log_df.tail(10)) # log_df의 최신 10개 행만 출력합니다. 223 | 224 | # 모델의 파라미터가 저장될 파일의 이름을 정합니다. 225 | curr_model_name = (f'b{CFG.batch_size}_h{CFG.hidden_size}_' 226 | f'd{CFG.dropout}_l{CFG.nlayers}_hd{CFG.nheads}_' 227 | f'ep{epoch}_s{CFG.seed}_fold{args.fold}.pt') 228 | # torch.nn.DataParallel로 감싸진 경우 원래의 model을 가져옵니다. 229 | model_to_save = model.module if hasattr(model, 'module') else model 230 | 231 | print('training done') 232 | 233 | # 모델의 파라미터를 저장합니다. 234 | save_checkpoint({ 235 | 'epoch': epoch + 1, 236 | 'arch': 'transformer', 237 | 'state_dict': model_to_save.state_dict(), 238 | 'log': log_df, 239 | }, 240 | MODEL_PATH, curr_model_name, 241 | ) 242 | 243 | 244 | def train(train_loader, model, optimizer, epoch, scheduler): 245 | """ 246 | 한 에폭 단위로 학습을 시킵니다. 247 | 248 | 매개변수 249 | train_loader: 학습 데이터셋에서 배치(미니배치) 불러옵니다. 250 | model: 학습될 파라미터를 가진 딥러닝 모델 251 | optimizer: 파라미터를 업데이트 시키는 역할 252 | scheduler: learning_rate를 감소시키는 역할 253 | """ 254 | # AverageMeter는 지금까지 입력 받은 전체 수의 평균 값 반환 용도 255 | batch_time = AverageMeter() # 한 배치처리 시간 집계 256 | data_time = AverageMeter() # 데이터 로딩 시간 집계 257 | losses = AverageMeter() # 손실 값 집계 258 | o_accuracies = AverageMeter() # 대회 평가 방법으로 집계 259 | b_accuracies = AverageMeter() # 대카테고리 정확도 집계 260 | m_accuracies = AverageMeter() # 중카테고리 정확도 집계 261 | s_accuracies = AverageMeter() # 소카테고리 정확도 집계 262 | d_accuracies = AverageMeter() # 세카테고리 정확도 집계 263 | 264 | sent_count = AverageMeter() # 문장 처리 개수 집계 265 | 266 | # 학습 모드로 교체 267 | model.train() 268 | 269 | start = end = time.time() 270 | 271 | # train_loader에서 반복해서 학습용 배치 데이터를 받아옵니다. 272 | # CateDataset의 __getitem__() 함수의 반환 값과 동일한 변수 반환 273 | for step, (token_ids, token_mask, token_types, img_feat, label) in enumerate(train_loader): 274 | # 데이터 로딩 시간 기록 275 | data_time.update(time.time() - end) 276 | 277 | # 배치 데이터의 위치를 CPU메모리에서 GPU메모리로 이동 278 | token_ids, token_mask, token_types, img_feat, label = ( 279 | token_ids.cuda(), token_mask.cuda(), token_types.cuda(), 280 | img_feat.cuda(), label.cuda()) 281 | 282 | batch_size = token_ids.size(0) 283 | 284 | # model은 배치 데이터를 입력 받아서 예측 결과 및 loss 반환 285 | # model은 인스턴스이나 __call__함수가 추가돼 함수처럼 호출이 가능합니다. 286 | # CateClassifier의 __call__ 함수 내에서 forward 함수가 호출됩니다. 287 | loss, pred = model(token_ids, token_mask, token_types, img_feat, label) 288 | loss = loss.mean() # Multi-GPU 학습의 경우 mean() 호출 필요 289 | 290 | # loss 값을 기록 291 | losses.update(loss.item(), batch_size) 292 | 293 | # 역전파 수행 294 | loss.backward() 295 | 296 | # CFG.max_grad_norm 이상의 값을 가지는 그래디언트 값 클리핑 297 | grad_norm = torch.nn.utils.clip_grad_norm_(model.parameters(), CFG.max_grad_norm) 298 | 299 | scheduler.step() # 스케쥴러로 learning_rate 조절 300 | optimizer.step() # 옵티마이저로 파라미터 업데이터 301 | optimizer.zero_grad() # 옵티마이저 내의 그래디언트 초기화 302 | 303 | # 소요시간 측정 304 | batch_time.update(time.time() - end) 305 | end = time.time() 306 | 307 | sent_count.update(batch_size) 308 | 309 | # CFG.print_freq 주기대로 결과 로그를 출력 310 | if step % CFG.print_freq == 0 or step == (len(train_loader)-1): 311 | # 대/중/소/세가 예측된 pred와 정답 label로 정확도 계산 및 집계 312 | o_acc, b_acc, m_acc, s_acc, d_acc = calc_cate_acc(pred, label) 313 | o_accuracies.update(o_acc, batch_size) 314 | b_accuracies.update(b_acc, batch_size) 315 | m_accuracies.update(m_acc, batch_size) 316 | s_accuracies.update(s_acc, batch_size) 317 | d_accuracies.update(d_acc, batch_size) 318 | 319 | print('Epoch: [{0}][{1}/{2}] ' 320 | 'Data {data_time.val:.3f} ({data_time.avg:.3f}) ' 321 | 'Elapsed {remain:s} ' 322 | 'Loss: {loss.val:.3f}({loss.avg:.3f}) ' 323 | 'OAcc: {o_acc.val:.3f}({o_acc.avg:.3f}) ' 324 | 'BAcc: {b_acc.val:.3f}({b_acc.avg:.3f}) ' 325 | 'MAcc: {m_acc.val:.4f}({m_acc.avg:.3f}) ' 326 | 'SAcc: {s_acc.val:.3f}({s_acc.avg:.3f}) ' 327 | 'DAcc: {d_acc.val:.3f}({d_acc.avg:.3f}) ' 328 | 'Grad: {grad_norm:.4f} ' 329 | 'LR: {lr:.6f} ' 330 | 'sent/s {sent_s:.0f} ' 331 | .format( 332 | epoch, step+1, len(train_loader), 333 | data_time=data_time, loss=losses, 334 | o_acc=o_accuracies, b_acc=b_accuracies, m_acc=m_accuracies, 335 | s_acc=s_accuracies, d_acc=d_accuracies, 336 | remain=timeSince(start, float(step+1)/len(train_loader)), 337 | grad_norm=grad_norm, 338 | lr=scheduler.get_lr()[0], 339 | sent_s=sent_count.avg/batch_time.avg 340 | )) 341 | # 학습 동안 집계된 결과 반환 342 | return (losses.avg, o_accuracies.avg, b_accuracies.avg, m_accuracies.avg, 343 | s_accuracies.avg, d_accuracies.avg) 344 | 345 | 346 | def validate(valid_loader, model): 347 | """ 348 | 한 에폭 단위로 검증합니다. 349 | 350 | 매개변수 351 | valid_loader: 검증 데이터셋에서 배치(미니배치) 불러옵니다. 352 | model: train 함수에서 학습된 딥러닝 모델 353 | """ 354 | batch_time = AverageMeter() # 한 배치처리 시간 집계 355 | data_time = AverageMeter() # 데이터 로딩 시간 집계 356 | losses = AverageMeter() # 손실 값 집계 357 | o_accuracies = AverageMeter() # 대회 평가 방법으로 집계 358 | b_accuracies = AverageMeter() # 대카테고리 정확도 집계 359 | m_accuracies = AverageMeter() # 중카테고리 정확도 집계 360 | s_accuracies = AverageMeter() # 소카테고리 정확도 집계 361 | d_accuracies = AverageMeter() # 세카테고리 정확도 집계 362 | 363 | sent_count = AverageMeter() # 문장 처리 개수 집계 364 | 365 | # 평가(evaluation) 모드로 교체 366 | # 드롭아웃이나 배치정규화가 일관된 값을 내도록 함 367 | model.eval() 368 | 369 | start = end = time.time() 370 | 371 | for step, (token_ids, token_mask, token_types, img_feat, label) in enumerate(valid_loader): 372 | # 데이터 로딩 시간 기록 373 | data_time.update(time.time() - end) 374 | 375 | # 배치 데이터의 위치를 CPU메모리에서 GPU메모리로 이동 376 | token_ids, token_mask, token_types, img_feat, label = ( 377 | token_ids.cuda(), token_mask.cuda(), token_types.cuda(), 378 | img_feat.cuda(), label.cuda()) 379 | 380 | batch_size = token_ids.size(0) 381 | 382 | # with문 내에서는 그래디언트 계산을 하지 않도록 함 383 | with torch.no_grad(): 384 | # model은 배치 데이터를 입력 받아서 예측 결과 및 loss 반환 385 | loss, pred = model(token_ids, token_mask, token_types, img_feat, label) 386 | loss = loss.mean() 387 | 388 | # loss 값을 기록 389 | losses.update(loss.item(), batch_size) 390 | 391 | # 소요시간 측정 392 | batch_time.update(time.time() - end) 393 | end = time.time() 394 | 395 | sent_count.update(batch_size) 396 | 397 | # CFG.print_freq 주기대로 결과 로그를 출력 398 | if step % CFG.print_freq == 0 or step == (len(valid_loader)-1): 399 | o_acc, b_acc, m_acc, s_acc, d_acc = calc_cate_acc(pred, label) 400 | o_accuracies.update(o_acc, batch_size) 401 | b_accuracies.update(b_acc, batch_size) 402 | m_accuracies.update(m_acc, batch_size) 403 | s_accuracies.update(s_acc, batch_size) 404 | d_accuracies.update(d_acc, batch_size) 405 | 406 | print('TEST: {0}/{1}] ' 407 | 'Data {data_time.val:.3f} ({data_time.avg:.3f}) ' 408 | 'Elapsed {remain:s} ' 409 | 'Loss: {loss.val:.4f}({loss.avg:.4f}) ' 410 | 'OAcc: {o_acc.val:.3f}({o_acc.avg:.3f}) ' 411 | 'BAcc: {b_acc.val:.3f}({b_acc.avg:.3f}) ' 412 | 'MAcc: {m_acc.val:.4f}({m_acc.avg:.3f}) ' 413 | 'SAcc: {s_acc.val:.3f}({s_acc.avg:.3f}) ' 414 | 'DAcc: {d_acc.val:.3f}({d_acc.avg:.3f}) ' 415 | 'sent/s {sent_s:.0f} ' 416 | .format( 417 | step+1, len(valid_loader), 418 | data_time=data_time, loss=losses, 419 | o_acc=o_accuracies, b_acc=b_accuracies, m_acc=m_accuracies, 420 | s_acc=s_accuracies, d_acc=d_accuracies, 421 | remain=timeSince(start, float(step+1)/len(valid_loader)), 422 | sent_s=sent_count.avg/batch_time.avg 423 | )) 424 | # 검증 동안 집계된 결과 반환 425 | return (losses.avg, o_accuracies.avg, b_accuracies.avg, m_accuracies.avg, 426 | s_accuracies.avg, d_accuracies.avg) 427 | 428 | 429 | def calc_cate_acc(pred, label): 430 | """ 431 | 대/중/소/세 카테고리별 정확도와 전체(overall) 정확도를 반환 432 | 전체 정확도는 대회 평가 방법과 동일한 가중치로 계산 433 | """ 434 | b_pred, m_pred, s_pred, d_pred= pred 435 | _, b_idx = b_pred.max(1) 436 | _, m_idx = m_pred.max(1) 437 | _, s_idx = s_pred.max(1) 438 | _, d_idx = d_pred.max(1) 439 | 440 | b_acc = (b_idx == label[:, 0]).sum().item() / (label[:, 0]>0).sum().item() 441 | m_acc = (m_idx == label[:, 1]).sum().item() / (label[:, 1]>0).sum().item() 442 | 443 | s_acc = (s_idx == label[:, 2]).sum().item() / ((label[:, 2]>0).sum().item()+1e-06) 444 | d_acc = (d_idx == label[:, 3]).sum().item() / ((label[:, 3]>0).sum().item()+1e-06) 445 | o_acc = (b_acc + 1.2*m_acc + 1.3*s_acc + 1.4*d_acc)/4 446 | return o_acc, b_acc, m_acc, s_acc, d_acc 447 | 448 | 449 | def save_checkpoint(state, model_path, model_filename, is_best=False): 450 | print('saving cust_model ...') 451 | if not os.path.exists(model_path): 452 | os.makedirs(model_path) 453 | torch.save(state, os.path.join(model_path, model_filename)) 454 | if is_best: 455 | torch.save(state, os.path.join(model_path, 'best_' + model_filename)) 456 | 457 | 458 | class AverageMeter(object): 459 | """Computes and stores the average and current value""" 460 | def __init__(self): 461 | self.reset() 462 | 463 | def reset(self): 464 | self.val = 0 465 | self.avg = 0 466 | self.sum = 0 467 | self.count = 0 468 | 469 | def update(self, val, n=1): 470 | self.val = val 471 | self.sum += val * n 472 | self.count += n 473 | self.avg = self.sum / self.count 474 | 475 | 476 | def asMinutes(s): 477 | m = math.floor(s / 60) 478 | s -= m * 60 479 | return '%dm %ds' % (m, s) 480 | 481 | 482 | def timeSince(since, percent): 483 | now = time.time() 484 | s = now - since 485 | es = s / (percent) 486 | rs = es - s 487 | return '%s (remain %s)' % (asMinutes(s), asMinutes(rs)) 488 | 489 | 490 | if __name__ == '__main__': 491 | main() 492 | -------------------------------------------------------------------------------- /code/train.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | date 5 | python train.py --stratified --fold 0 6 | python train.py --stratified --fold 1 7 | python train.py --stratified --fold 2 8 | python train.py --stratified --fold 3 9 | python train.py --stratified --fold 4 10 | date 11 | -------------------------------------------------------------------------------- /doc/embedding.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lime-robot/categories-prediction/1cdaf3797285bfc8b9dc1118adc6a5230a394b97/doc/embedding.png -------------------------------------------------------------------------------- /doc/model_block.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lime-robot/categories-prediction/1cdaf3797285bfc8b9dc1118adc6a5230a394b97/doc/model_block.png -------------------------------------------------------------------------------- /input/README.md: -------------------------------------------------------------------------------- 1 | 2 | # 쇼핑 상품 분류 대회의 데이터 다운로드 받기 3 | 4 | 여기 input/ 하위에 raw_data/ 를 생성하고 https://arena.kakao.com/c/5/data 에서 대회 데이터 파일을 다운로드합니다. 5 | 6 | 아래처럼 대회에서 다운로드 받은 파일이 위치해야 합니다. 7 | 8 | ``` 9 | categories-prediction/ 10 | └── input 11 | ├── README.md 12 | └── raw_data 13 | ├── cate1.json 14 | ├── dev.chunk.01 15 | ├── test.chunk.01 16 | ├── test.chunk.02 17 | ├── train.chunk.01 18 | ├── train.chunk.02 19 | ├── train.chunk.03 20 | ├── train.chunk.04 21 | ├── train.chunk.05 22 | ├── train.chunk.06 23 | ├── train.chunk.07 24 | ├── train.chunk.08 25 | └── train.chunk.09 26 | ``` 27 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | #torch==1.7.1 2 | transformers==4.0.0 3 | sentencepiece==0.1.94 4 | tokenizers==0.9.4 5 | --------------------------------------------------------------------------------