HanseulJo / cuisine-prediction

0 stars 1 forks source link

[Expr] Set Transformer 기반 Classification+Completion model #27

Open HanseulJo opened 2 years ago

HanseulJo commented 2 years ago

Dataset

일단 이전에 만든 데이터셋 (Hanseul/Container/*)을 활용했습니다. binary/integer형 data와 label을 train/validation(classification/completion)/test(classification/completion)에서 모두 지원하도록 만들어놓았습니다. 다만 찬호님이 먼저 데이터셋 만들어 놓으신 게 있기 때문에, 필요하다면 찬호님 데이터를 활용하는 쪽으로 바꾸는 것도 가능합니다.

Model

model

Classification 실험 결과

1차적인 debugging을 마치고 classification만 돌려보았습니다. lr은 1e-3 -> 1e-4, embedding dimension 300, dropout 0.5 썼을 때 23 epoch 정도에서 아래와 같은 결과가 나왔습니다. 결과만 요약하자면,

Completion Training 실험 결과

이번엔 Completion만 수행했을 때 결과입니다. 매우 느리고 (20epochs만 돌리려고 해도 4시간), 결과도 별로 좋지 않습니다(epoch 3까지 돌아도 accuracy가 0.05 언저리를 못 벗어남).

label이 한쪽으로 쏠리는 현상이 심합니다. 특히 937번('salt') 재료가 prediction에 엄청 많이 등장합니다. 아마 ingredient의 분포가 imbalance가 심해서 나타나는 현상 같습니다. 또 추측이긴 하지만 model의 softmax output의 dimension이 6714로 엄청 커서 loss가 잘 감소하지 않는 문제도 보입니다. loss function을 다른 것을 써보거나 network 구조를 조금 바꾸던가 해야 할 것 같습니다. 혹은, classification에서 train된 embedding을 그대로 가져와 freeze해서 사용하는 방법도 있겠습니다. (어떤 게 나을까요....)

Edit 3090으로 돌리니까 completion도 (batchsize 16 -> 32, 25epochs) 18분 정도로 금방하긴 하네요 ㅋㅋㅋ 역시 좋은 gpu를 써야..

HanseulJo commented 2 years ago

Completion 모델 training이 잘 안 되는 이유는 아마 training 방식 때문인 것 같습니다.

ingredient마다 recipe에 등장하는 횟수가 만 번이 넘는 재료(937, 'salt')가 있는 반면 1번 내지는 아예 등장하는 재료는 상당히 많습니다. 또 recipe마다 재료의 수도 1개부터 65개까지 천차만별입니다. 그런데 training 시에 매 recipe마다 모두 하나씩만 뽑아서 label로 학습시키는 방식을 쓰게 되면 상당히 class-imbalanced된 ingredient classification을 하는 것이 될 뿐만 아니라 ingredient distribution을 잘못 해석하는 것이 될 수도 있습니다.

completion을 training하는 좋은 방식이 무엇일지는 더 고민해보아야 할 문제인 듯합니다.

HanseulJo commented 2 years ago

Completion 방식 변경

기존 방식:

  1. Loss computation: Completion을 6714-(주어진 recipe 재료 수) 가지 label이 있는 Multi-Class classification으로 해석해서, completion model의 output(6714차원 벡터)에서 주어진 recipe에 해당하는 index는 masking하고 나머지에 대한 softmax값을 통해 CrossEntropyLoss 구함. 이때 label은 새로 넣어야 할 재료 index. (단, 실제로는 softmax가 loss 안에 이미 내장되어 있으므로 별도의 softmax layer 필요 없음)
  2. Training: classification용 dataset에서 recipe 가운데 random하게 한 재료를 뽑아 그것을 label로 삼아서 training.

문제점: class 수가 너무 많아서 softmax 값이 다 엇비슷함. 또한, recipe마다 재료수의 차이가 있어서 dataset에서 등장하던 빈도만큼 training에 쓰이지 못하는 재료들 발생. 다르게 말하면 ingredient의 distribution을 잘못 학습시킴. 결과: accuracy가 0.06을 넘은 적이 없음. (train, validation 모두) loss의 하강도 별로 뚜렷하지 않음.

새로운 방식:

  1. Loss computation: Completion을 6714 가지 label이 있는 Multi-Label classification으로 해석해서, completion model의 output(6714차원 벡터)에 sigmoid를 취한뒤 각 재료 index별로 BCELoss 구함. 이때 label은 기존 재료 + 빠진 재료, 즉 completed recipe에 대한 binary vector.
  2. Training: completion을 위한 별도의 dataset을 생성. 모든 recipe에서 각각 한 재료씩을 뺀 것을 전부 별개의 data로 여겨서 만듦. 기존 dataset 크기는 23547였지만, 새로운 dataset은 이의 10배 정도로 늘어남: 253419. 기존처럼 training 도중에 data가 들어올 때마다 random하게 재료를 빼는 일을 할 필요 없음.

문제점: 재료수가 많은 경우와 적은 경우에 그 recipe를 볼 수 있는 빈도차이가 극심함. (최대 65배 차이) 한편, 같은 recipe에서 나온 data라 해도 다른 data처럼 인식하게 되어서 불필요하게 epoch당 computation 횟수를 10배 이상으로 늘림. (실제로는 한 epoch당 6분 넘게 걸릴 정도.) 결과: validation set에서는 큰 차이 없으나 train set에 대해서는 4epoch 정도 만에 accuracy 0.1정도에 도달. 다만 이것은 그만큼 training time도 늘어나서 가능한 결과일 수 있음. 그래도 loss는 꾸준히 감소.

HanseulJo commented 2 years ago

Loss functions

아래와 같은 다양한 loss function을 도입하고 서로 엮음.

Classification

  1. CrossEntropyLoss: 제일 기본.
  2. FocalLoss: Class imbalance에 강하다고 잘 알려져있음. (논문 링크)
  3. F1Loss: classification 평가 기준이 macro/micro-f1 score이므로, 이를 응용한 loss를 사용하는 것도 좋을 것이라고 기대됨. 주로 macro-F1 loss를 이용.
  4. ASL(Asymmetric loss) (single-label multi-class version): 원래는 multi-label classification용으로 고안된 Loss인데 2021년에 single-label version도 업데이트 되었음. 설명은 아래에.

F1 + ASL 또는 F1 + Focal loss를 이용하려고 함.

Completion:

  1. BCEWithLogitLoss: 제일 기본. 위 comment에서 설명한 binary cross entropy loss. sigmoid까지 이미 포함하고 있음.
  2. ASL(Asymmetric loss): 쉽게말하면 focal loss의 2020년 확장판. (논문 링크)

여기서도 F1, Focal loss 등을 이용할수 있음. 일단 F1 + BCE 또는 F1 + ASL을 이용하려고 함.

HanseulJo commented 2 years ago

더 생각해볼 것

  1. completion model이 거의 학습되지 않는 이유는 무엇일까? completion task 수행 방식을 바꾸었음에도 불구하고, training dataset에서조차 accuracy는 늘어날 조짐이 보이지 않는다.
  2. 어떤 dataset에서는 나타나고 어디에서는 나타나지 않는 재료는 어떻게 처리할 것인가? 가령, 표로 정리해보면 다음과 같다.
    train | valid |  test | how many
    O   |   O   |   O   |   2568
    O   |   O   |   -   |   1073
    O   |   -   |   O   |    386
    O   |   -   |   -   |   1832
    -   |   O   |   O   |     44
    -   |   O   |   -   |    401
    -   |   -   |   O   |    218
    -   |   -   |   -   |    192

    ingredient embedding은 unseen ingredient에 대해서는 훈련되지 않으므로, embedding space 상에서의 nearest neighbor 등을 이용해야 할지도 모름.

  3. overfitting 문제. 안그래도 training도 잘 안되는데, validation accuracy는 가면 갈수록 감소하는 문제 발생. 와중에 training loss는 정말 꾸준히 감소하지만, valIdation loss는 잘 감소하지 않는다. 이건 아마 dataset 간의 괴리 문제 때문이거나 hyperparameter를 잘못 정했기 때문일지도 모름.

더 구현해볼 것

  1. Recall @ 10 같이 좀 다양한 metric으로 model 성능 판별하기. 지금은 주어진 recipe를 제외한 나머지 recipe 중 가장 score가 높은 것으로만 Prediction을 평가하고 있지만, model이 이미 주어진 recipe도 잘 회복?하는지도 평가할 필요 있음.
  2. 동일한 이름의 재료 처리하기. 해당하는 두 개 혹은 세 개의 index에 대한 embedding vector가 항상 동일하도록 세팅해놓고 training하기. 아니면 dataset에서부터 같은 재료는 같은 index로 매핑해도 됨(찬호님 modified data처럼).
  3. Logging. WandB나 Tensorboard 등으로 실험 결과 디스플레이하기.
HanseulJo commented 2 years ago

Completion 방식 변경

기존 방식:

  1. Loss computation: Completion을 6714-(주어진 recipe 재료 수) 가지 label이 있는 Multi-Class classification으로 해석해서, completion model의 output(6714차원 벡터)에서 주어진 recipe에 해당하는 index는 masking하고 나머지에 대한 softmax값을 통해 CrossEntropyLoss 구함. 이때 label은 새로 넣어야 할 재료 index. (단, 실제로는 softmax가 loss 안에 이미 내장되어 있으므로 별도의 softmax layer 필요 없음)
  2. Training: classification용 dataset에서 recipe 가운데 random하게 한 재료를 뽑아 그것을 label로 삼아서 training.

문제점: class 수가 너무 많아서 softmax 값이 다 엇비슷함. 또한, recipe마다 재료수의 차이가 있어서 dataset에서 등장하던 빈도만큼 training에 쓰이지 못하는 재료들 발생. 다르게 말하면 ingredient의 distribution을 잘못 학습시킴. 결과: accuracy가 0.06을 넘은 적이 없음. (train, validation 모두) loss의 하강도 별로 뚜렷하지 않음.

Debug: multi class classification 기반 completion에 관해

일단 학습이 잘 되지 않는 이유는 class 수가 너무 많은 것이 문제이기는 하다(softmax 값들이 너무 작아서). 그러나 한 가지 착각한 지점은, input에 있던 재료 인덱스들을 무시해서는 안 된다는 것. 이 인덱스들이 절대 답이 되지 않은 것을 학습해야 했다. 게다가, 이미 답이 될 가능성이 적다는 걸 알고 있는 인덱스들은 무시해도 되지만, 답이 아닌데도 답을 착각하는 소수의 인덱스들에 대해서는 꼭 학습해야 한다. 따라서 6714 dim 전부에 대한 CE loss를 구할 필요 없이, (1) input에 있던 index, (2) label이 되는 index, (3) label이 아닌데 logit 값이 이상하게 높은 상위 100개 정도의 index에 대한 logit만 가져와서 CE loss 구해도 충분할 것 같다.

또 한가지 간과한 점은 model output(=logit)을 -inf로 가리면 안된다는 것. 그러면 CE 구할 때 log(0)을 계산하게 되어서 nan loss가 생긴다... softmax 값을 0으로 만들려고 -inf를 집어넣는 일은 Attention layer에서나 하기.

HanseulJo commented 2 years ago

30 : 최근 업데이트로 데이터셋부터 모델까지 쭉 수정 및 보강을 진행하고 몇 가지 기능을 추가했습니다.

1) 기존 attention 기반의 completion 모델은 버리고 Residual Block 기반 모델로 대체했습니다. 또 기존의 set transformer encoder를 이용하던 encoder 부분에도 역시 fully connected layer 기반 모델을 추가해두었습니다. (정확히는 Deep Sets 기반, arXiv:1703.06114) 실험해보니 DeepSets 기반 모델이 훨씬 빠르고 성능도 비슷하지만, dropout의 영향을 크게 받습니다. 아마 DeepSets와 SetTransformer가 적당히 섞인 모델을 지향하게 되지 않을까 싶습니다.

2) 잠깐 생각했었던 multilabel classification 기반의 Completion은 아예 버리고, cuisine classification과 recipe completion 둘다 single-label multi-class classification으로 풀려고 합니다.

3) Python 파일로 만들어 command line으로 실행 가능하게 했습니다. 지금은 주피터 노트북 파일이나 파이썬 파일이나 모두 up-to-date이지만 자잘한 업데이트는 노트북 파일에 먼저 반영되는 편입니다.

예시:

python3 ./run.py -cls -bs 128 -lr 0.001 -encmode deep_sets -poolmode set_transformer -decmode simple -numenc 4 -numdec 4 -drop 0.2

4) WandB,... 그냥 제가 했습니다. 생각보다 많이 간단하네요. 지금 Classification 모델을 모델 종류 및 dropout rate에 대해 tuning 중인데, 실험 내용을 아래 링크에서 보실 수 있습니다. 굳이 dropout에 대해 tuning하는 이유는 모델 성능이 dropout 정도에 따라 크게 다르게 나오는 것을 관찰했기 때문입니다. https://wandb.ai/hanseuljo/Cuisine%20Classification?workspace=user-hanseuljo

HanseulJo commented 2 years ago

Experiment.ipynb

Classification Only

WandB link

run(classify=True, complete=False, batch_size=64, encoder_mode='HYBRID', pooler_mode='ATT', lr=5e-4,
    num_enc_layers=3, num_dec_layers=2, num_outputs_cpl=1, dropout=0.1, weight_decay=0.1, wandb_log=True, )

Training complete in 19m 27s ==== Best Result ==== clf_Loss: 1.57776213 _clfAcc: 0.73547401 clf_Topk: 0.93756371 _clfF1macro: 0.64558903 clf_F1micro: 0.73547401 bestEpoch: 30

Completion Only

WandB link

run(classify=False, complete=True, batch_size=64, encoder_mode='HYBRID', pooler_mode='ATT', lr=5e-4,
    num_enc_layers=3, num_dec_layers=2, num_outputs_cpl=1, dropout=0.1, weight_decay=0.1, wandb_log=True, )

Training complete in 21m 4s ==== Best Result ==== cpl_Loss: 5.89179945 _cplAcc: 0.09569317 cpl_Topk: 0.30287971 bestEpoch: 49

Classification + Completion

WandB link

run(classify=True, complete=True, batch_size=64, encoder_mode='HYBRID', pooler_mode='ATT', lr=5e-4,
    num_enc_layers=3, num_dec_layers=2, num_outputs_cpl=1, dropout=0.1, weight_decay=0.1, wandb_log=True, )

Training complete in 30m 30s ==== Best Result ==== clf_Loss: 1.52818406 clf_Acc: 0.72872069 clf_Topk: 0.93845566 clf_F1macro: 0.63214452 clf_F1micro: 0.72872069 cpl_Loss: 5.80203962 _cplAcc: 0.10665138 cpl_Topk: 0.32071865 bestEpoch: 50