├── .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 | " product | \n",
154 | "
\n",
155 | " \n",
156 | " \n",
157 | " \n",
158 | " 0 | \n",
159 | " 직소퍼즐 - 1000조각 바다거북의 여행 (PL1275) | \n",
160 | "
\n",
161 | " \n",
162 | " 1 | \n",
163 | " [모리케이스]아이폰6S/6S+ tree farm101 - 다이어리케이스[바보사랑][무료배송] | \n",
164 | "
\n",
165 | " \n",
166 | " 2 | \n",
167 | " 크리비아 기모 3부 속바지 GLG4314P | \n",
168 | "
\n",
169 | " \n",
170 | "
\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 | " pid | \n",
259 | " product | \n",
260 | " brand | \n",
261 | " maker | \n",
262 | " price | \n",
263 | " bcatenm | \n",
264 | " mcatenm | \n",
265 | " scatenm | \n",
266 | " dcatenm | \n",
267 | "
\n",
268 | " \n",
269 | " \n",
270 | " \n",
271 | " 0 | \n",
272 | " O4486751463 | \n",
273 | " 직소퍼즐 - 1000조각 바다거북의 여행 (PL1275) | \n",
274 | " 퍼즐라이프 | \n",
275 | " 상품상세설명 참조 | \n",
276 | " 16520 | \n",
277 | " 악기/취미/만들기 | \n",
278 | " 보드게임/퍼즐 | \n",
279 | " 직소/퍼즐 | \n",
280 | " | \n",
281 | "
\n",
282 | " \n",
283 | " 1 | \n",
284 | " P3307178849 | \n",
285 | " [모리케이스]아이폰6S/6S+ tree farm101 - 다이어리케이스[바보사랑][무료배송] | \n",
286 | " 바보사랑 | \n",
287 | " MORY|해당없음 | \n",
288 | " 20370 | \n",
289 | " 휴대폰/액세서리 | \n",
290 | " 휴대폰액세서리 | \n",
291 | " 아이폰액세서리 | \n",
292 | " | \n",
293 | "
\n",
294 | " \n",
295 | " 2 | \n",
296 | " R4424255515 | \n",
297 | " 크리비아 기모 3부 속바지 GLG4314P | \n",
298 | " 크리비아 | \n",
299 | " | \n",
300 | " -1 | \n",
301 | " 언더웨어 | \n",
302 | " 보정언더웨어 | \n",
303 | " 속바지/속치마 | \n",
304 | " | \n",
305 | "
\n",
306 | " \n",
307 | " 3 | \n",
308 | " F3334315393 | \n",
309 | " [하프클럽/잭앤질]남성 솔리드 절개라인 포인트 포켓 팬츠 31133PT002_NA | \n",
310 | " 잭앤질 | \n",
311 | " ㈜크리스패션 | \n",
312 | " 16280 | \n",
313 | " 남성의류 | \n",
314 | " 바지 | \n",
315 | " 일자면바지 | \n",
316 | " | \n",
317 | "
\n",
318 | " \n",
319 | " 4 | \n",
320 | " N731678492 | \n",
321 | " 코드프리혈당시험지50매/코드프리시험지/최장유효기간 | \n",
322 | " | \n",
323 | " 기타 | \n",
324 | " -1 | \n",
325 | " 건강관리/실버용품 | \n",
326 | " 건강측정용품 | \n",
327 | " 혈당지 | \n",
328 | " | \n",
329 | "
\n",
330 | " \n",
331 | "
\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 | " brand | \n",
414 | " count | \n",
415 | " percentage | \n",
416 | "
\n",
417 | " \n",
418 | " \n",
419 | " \n",
420 | " 0 | \n",
421 | " | \n",
422 | " 3930113 | \n",
423 | " 48.312243 | \n",
424 | "
\n",
425 | " \n",
426 | " 1 | \n",
427 | " 상품상세설명 참조 | \n",
428 | " 153156 | \n",
429 | " 1.882722 | \n",
430 | "
\n",
431 | " \n",
432 | " 2 | \n",
433 | " 바보사랑 | \n",
434 | " 66645 | \n",
435 | " 0.819256 | \n",
436 | "
\n",
437 | " \n",
438 | " 3 | \n",
439 | " 기타 | \n",
440 | " 64144 | \n",
441 | " 0.788512 | \n",
442 | "
\n",
443 | " \n",
444 | " 4 | \n",
445 | " 상세설명참조 | \n",
446 | " 35795 | \n",
447 | " 0.440022 | \n",
448 | "
\n",
449 | " \n",
450 | " 5 | \n",
451 | " 없음 | \n",
452 | " 33603 | \n",
453 | " 0.413076 | \n",
454 | "
\n",
455 | " \n",
456 | " 6 | \n",
457 | " 아디다스 | \n",
458 | " 32292 | \n",
459 | " 0.396960 | \n",
460 | "
\n",
461 | " \n",
462 | " 7 | \n",
463 | " 나이키 | \n",
464 | " 30785 | \n",
465 | " 0.378435 | \n",
466 | "
\n",
467 | " \n",
468 | " 8 | \n",
469 | " 아트박스 | \n",
470 | " 28518 | \n",
471 | " 0.350567 | \n",
472 | "
\n",
473 | " \n",
474 | " 9 | \n",
475 | " 알수없음 | \n",
476 | " 26768 | \n",
477 | " 0.329055 | \n",
478 | "
\n",
479 | " \n",
480 | "
\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 | " maker | \n",
541 | " count | \n",
542 | " percentage | \n",
543 | "
\n",
544 | " \n",
545 | " \n",
546 | " \n",
547 | " 0 | \n",
548 | " | \n",
549 | " 2196846 | \n",
550 | " 27.005472 | \n",
551 | "
\n",
552 | " \n",
553 | " 1 | \n",
554 | " 기타 | \n",
555 | " 2009828 | \n",
556 | " 24.706490 | \n",
557 | "
\n",
558 | " \n",
559 | " 2 | \n",
560 | " 상품상세설명 참조 | \n",
561 | " 442299 | \n",
562 | " 5.437110 | \n",
563 | "
\n",
564 | " \n",
565 | " 3 | \n",
566 | " 상세페이지 참조 | \n",
567 | " 63792 | \n",
568 | " 0.784185 | \n",
569 | "
\n",
570 | " \n",
571 | " 4 | \n",
572 | " 상세설명참조 | \n",
573 | " 37899 | \n",
574 | " 0.465886 | \n",
575 | "
\n",
576 | " \n",
577 | " 5 | \n",
578 | " 상품상세설명참조 | \n",
579 | " 36389 | \n",
580 | " 0.447324 | \n",
581 | "
\n",
582 | " \n",
583 | " 6 | \n",
584 | " 아디다스 | \n",
585 | " 25472 | \n",
586 | " 0.313123 | \n",
587 | "
\n",
588 | " \n",
589 | " 7 | \n",
590 | " 상세설명참조 / 상세설명참조 | \n",
591 | " 21873 | \n",
592 | " 0.268881 | \n",
593 | "
\n",
594 | " \n",
595 | " 8 | \n",
596 | " [불명] | \n",
597 | " 20836 | \n",
598 | " 0.256134 | \n",
599 | "
\n",
600 | " \n",
601 | " 9 | \n",
602 | " 상품상세정보 참조 | \n",
603 | " 19786 | \n",
604 | " 0.243226 | \n",
605 | "
\n",
606 | " \n",
607 | "
\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 | " price | \n",
686 | " count | \n",
687 | " percentage | \n",
688 | "
\n",
689 | " \n",
690 | " \n",
691 | " \n",
692 | " 0 | \n",
693 | " -1 | \n",
694 | " 5270821 | \n",
695 | " 64.793349 | \n",
696 | "
\n",
697 | " \n",
698 | " 1 | \n",
699 | " 85500 | \n",
700 | " 8872 | \n",
701 | " 0.109062 | \n",
702 | "
\n",
703 | " \n",
704 | " 2 | \n",
705 | " 10800 | \n",
706 | " 6522 | \n",
707 | " 0.080174 | \n",
708 | "
\n",
709 | " \n",
710 | " 3 | \n",
711 | " 9000 | \n",
712 | " 6505 | \n",
713 | " 0.079965 | \n",
714 | "
\n",
715 | " \n",
716 | " 4 | \n",
717 | " 13500 | \n",
718 | " 5885 | \n",
719 | " 0.072343 | \n",
720 | "
\n",
721 | " \n",
722 | " 5 | \n",
723 | " 9900 | \n",
724 | " 5792 | \n",
725 | " 0.071200 | \n",
726 | "
\n",
727 | " \n",
728 | " 6 | \n",
729 | " 94000 | \n",
730 | " 5083 | \n",
731 | " 0.062484 | \n",
732 | "
\n",
733 | " \n",
734 | " 7 | \n",
735 | " 18000 | \n",
736 | " 4801 | \n",
737 | " 0.059018 | \n",
738 | "
\n",
739 | " \n",
740 | " 8 | \n",
741 | " 7200 | \n",
742 | " 4698 | \n",
743 | " 0.057752 | \n",
744 | "
\n",
745 | " \n",
746 | " 9 | \n",
747 | " 89300 | \n",
748 | " 4241 | \n",
749 | " 0.052134 | \n",
750 | "
\n",
751 | " \n",
752 | "
\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 | " pid | \n",
827 | " product | \n",
828 | " bcateid | \n",
829 | " mcateid | \n",
830 | " scateid | \n",
831 | " dcateid | \n",
832 | "
\n",
833 | " \n",
834 | " \n",
835 | " \n",
836 | " 0 | \n",
837 | " O4486751463 | \n",
838 | " 직소퍼즐 - 1000조각 바다거북의 여행 (PL1275) | \n",
839 | " 1 | \n",
840 | " 1 | \n",
841 | " 2 | \n",
842 | " -1 | \n",
843 | "
\n",
844 | " \n",
845 | " 1 | \n",
846 | " P3307178849 | \n",
847 | " [모리케이스]아이폰6S/6S+ tree farm101 - 다이어리케이스[바보사랑][무료배송] | \n",
848 | " 3 | \n",
849 | " 3 | \n",
850 | " 4 | \n",
851 | " -1 | \n",
852 | "
\n",
853 | " \n",
854 | " 2 | \n",
855 | " R4424255515 | \n",
856 | " 크리비아 기모 3부 속바지 GLG4314P | \n",
857 | " 5 | \n",
858 | " 5 | \n",
859 | " 6 | \n",
860 | " -1 | \n",
861 | "
\n",
862 | " \n",
863 | " 3 | \n",
864 | " F3334315393 | \n",
865 | " [하프클럽/잭앤질]남성 솔리드 절개라인 포인트 포켓 팬츠 31133PT002_NA | \n",
866 | " 7 | \n",
867 | " 7 | \n",
868 | " 8 | \n",
869 | " -1 | \n",
870 | "
\n",
871 | " \n",
872 | " 4 | \n",
873 | " N731678492 | \n",
874 | " 코드프리혈당시험지50매/코드프리시험지/최장유효기간 | \n",
875 | " 10 | \n",
876 | " 9 | \n",
877 | " 11 | \n",
878 | " -1 | \n",
879 | "
\n",
880 | " \n",
881 | "
\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 | " pid | \n",
958 | " product | \n",
959 | " bcateid | \n",
960 | " mcateid | \n",
961 | " scateid | \n",
962 | " dcateid | \n",
963 | "
\n",
964 | " \n",
965 | " \n",
966 | " \n",
967 | " 0 | \n",
968 | " O4486751463 | \n",
969 | " 직소퍼즐 1000조각 바다거북의 여행 pl1275 | \n",
970 | " 1 | \n",
971 | " 1 | \n",
972 | " 2 | \n",
973 | " -1 | \n",
974 | "
\n",
975 | " \n",
976 | " 1 | \n",
977 | " P3307178849 | \n",
978 | " 모리케이스 아이폰6s 6s tree farm101 다이어리케이스 바보사랑 무료배송 | \n",
979 | " 3 | \n",
980 | " 3 | \n",
981 | " 4 | \n",
982 | " -1 | \n",
983 | "
\n",
984 | " \n",
985 | " 2 | \n",
986 | " R4424255515 | \n",
987 | " 크리비아 기모 3부 속바지 glg4314p | \n",
988 | " 5 | \n",
989 | " 5 | \n",
990 | " 6 | \n",
991 | " -1 | \n",
992 | "
\n",
993 | " \n",
994 | " 3 | \n",
995 | " F3334315393 | \n",
996 | " 하프클럽 잭앤질 남성 솔리드 절개라인 포인트 포켓 팬츠 31133pt002 na | \n",
997 | " 7 | \n",
998 | " 7 | \n",
999 | " 8 | \n",
1000 | " -1 | \n",
1001 | "
\n",
1002 | " \n",
1003 | " 4 | \n",
1004 | " N731678492 | \n",
1005 | " 코드프리혈당시험지50매 코드프리시험지 최장유효기간 | \n",
1006 | " 10 | \n",
1007 | " 9 | \n",
1008 | " 11 | \n",
1009 | " -1 | \n",
1010 | "
\n",
1011 | " \n",
1012 | "
\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 | " product | \n",
1143 | " tokens | \n",
1144 | "
\n",
1145 | " \n",
1146 | " \n",
1147 | " \n",
1148 | " 0 | \n",
1149 | " 직소퍼즐 1000조각 바다거북의 여행 pl1275 | \n",
1150 | " ▁직소퍼즐 ▁1000 조각 ▁바다 거북 의 ▁여행 ▁pl 12 75 | \n",
1151 | "
\n",
1152 | " \n",
1153 | " 1 | \n",
1154 | " 모리케이스 아이폰6s 6s tree farm101 다이어리케이스 바보사랑 무료배송 | \n",
1155 | " ▁모리케이스 ▁아이폰 6 s ▁6 s ▁tree ▁farm 101 ▁다이어리케이스 ▁바보사랑 ▁무료배송 | \n",
1156 | "
\n",
1157 | " \n",
1158 | " 2 | \n",
1159 | " 크리비아 기모 3부 속바지 glg4314p | \n",
1160 | " ▁크리비아 ▁기모 ▁3 부 ▁속바지 ▁gl g 43 14 p | \n",
1161 | "
\n",
1162 | " \n",
1163 | " 3 | \n",
1164 | " 하프클럽 잭앤질 남성 솔리드 절개라인 포인트 포켓 팬츠 31133pt002 na | \n",
1165 | " ▁하프클럽 ▁잭앤질 ▁남성 ▁솔리드 ▁절개라인 ▁포인트 ▁포켓 ▁팬츠 ▁311 33 pt 002 ▁na | \n",
1166 | "
\n",
1167 | " \n",
1168 | " 4 | \n",
1169 | " 코드프리혈당시험지50매 코드프리시험지 최장유효기간 | \n",
1170 | " ▁코드 프리 혈 당 시험 지 50 매 ▁코드 프리 시험 지 ▁최 장 유 효 기간 | \n",
1171 | "
\n",
1172 | " \n",
1173 | "
\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 |
--------------------------------------------------------------------------------