본문 바로가기

NLP 연구실 업무/DSTC9 - SIMMC

코드 분석 - Task 1, 2

1. 코드 구조

1) shell argument

option.py : shell argument add

2) train & validation 진행

train_simmc_agent.py : train SIMMC baseline

eval_simmc_agent.py : evaluate 

3) dataloader

loader/ : DataLoader (SIMMC data)

1> loader_base.py : 추후 loader_simmc가 상속해서 활용

2> loader_simmc.py : simmc dataset의 dataloader 만든다.

4) Model

model/ : Model 파일들 존재

0> assistant.py : 아래 모델의 총합

1> encoders/ : Utterance & History Encoder

  • history_agnostic.py (HAE or T-HAE)
  • hierarchical_recurrent.py (HRE)
  • memory_network.py (MN)
  • tf_idf_encoder.py (과거 모델)

2> Multimodal Fusion

  • carousel_embedder.py : learn furniture data embeddding
  • user_memory_embedder.py : learn fashion data embeddding

3> Action Predictor : action_executor.py

4> Responce Generator : decoder.py  (LSTM 혹은 Transformer 사용)

5> 기타 모델

- self_attention.py : 일반 self_attention network

- positional_encoding.py : transformer에서 위치 정보를 주기 위한 encoding

6> json 파일

{DOMAIN}_model_metainfo.json : model의 action 별 attribute를 정리했다.

5) tools/

전처리 하는데 필요한 파일들 존재

6> scripts/

shell 파일들 존재

 

 


 

 

2. shell 파일 분석

1) preprocess_simmc.sh

json과 csv 형태의 data를 model에 넣을 수 있는 형태로 preprocess

0> variable & argument

#!/bin/bash

DOMAIN="furniture"
# DOMAIN="fashion"
ROOT="../data/simmc_${DOMAIN}/"

# Input files.
TRAIN_JSON_FILE="${ROOT}${DOMAIN}_train_dials.json"
DEV_JSON_FILE="${ROOT}${DOMAIN}_dev_dials.json"
DEVTEST_JSON_FILE="${ROOT}${DOMAIN}_devtest_dials.json"


if [ "$DOMAIN" == "furniture" ]; then
    METADATA_FILE="${ROOT}furniture_metadata.csv"
elif [ "$DOMAIN" == "fashion" ]; then
    METADATA_FILE="${ROOT}fashion_metadata.json"
else
    echo "Invalid domain!"
    exit 0
fi


# Output files.
VOCAB_FILE="${ROOT}${DOMAIN}_vocabulary.json"
METADATA_EMBEDS="${ROOT}${DOMAIN}_asset_embeds.npy"
ATTR_VOCAB_FILE="${ROOT}${DOMAIN}_attribute_vocabulary.json"

- DOMAIN은 "furniture"와 "fashion" 중에서 선택이 가능하다.

- input file 이름 : ${DOMAIN}_{dataset 종류}_dials.json

- metadata file 이름 : furniture_metadata.csv / fashion_metadata.json

- output file 이름 :

  • ${DOMAIN}_vocabulary.json
  • ${DOMAIN}_asset_embeds.npy
  • ${DOMAIN}_attribute_vocabulary.json

1> Step 1 : extract assistant API

# Step 1: Extract assistant API.
INPUT_FILES="${TRAIN_JSON_FILE} ${DEV_JSON_FILE} ${DEVTEST_JSON_FILE}"
# If statement.
if [ "$DOMAIN" == "furniture" ]; then
    python tools/extract_actions.py \
        --json_path="${INPUT_FILES}" \
        --save_root="${ROOT}" \
        --metadata_path="${METADATA_FILE}"
elif [ "$DOMAIN" == "fashion" ]; then
    python tools/extract_actions_fashion.py \
        --json_path="${INPUT_FILES}" \
        --save_root="${ROOT}" \
        --metadata_path="${METADATA_FILE}"
else
    echo "Invalid domain!"
    exit 0
fi

- domain에 따라 tools/extract_action.py 혹은 tools/extract_actions_fashion.py 파일 실행

=> action data를 extract

 

2> Step 2 : Extract vocabulary from train.

# Step 2: Extract vocabulary from train.
python tools/extract_vocabulary.py \
    --train_json_path="${TRAIN_JSON_FILE}" \
    --vocab_save_path="${VOCAB_FILE}" \
    --threshold_count=5

- tools/extract_vocabulary.py 파일 실행

=> SIMMC dataset(${DOMAIN}_{dataset 종류}_dials.json)에 있는 data를 tokenize하고 ${DOMAIN}_vocabulary.json 에 저장

 

3> Step 3: Read and embed shopping assets

# Step 3: Read and embed shopping assets.
if [ "$DOMAIN" == "furniture" ]; then
    python tools/embed_furniture_assets.py \
        --input_csv_file="${METADATA_FILE}" \
        --embed_path="${METADATA_EMBEDS}"
elif [ "$DOMAIN" == "fashion" ]; then
    python tools/embed_fashion_assets.py \
        --input_asset_file="${METADATA_FILE}" \
        --embed_path="${METADATA_EMBEDS}"
else
    echo "Invalid domain!"
    exit 0
fi

- tools/embed_{DOMAIN}_asset.py 파일 실행

=> furniture_metadata.csv / fashion_metadata.json data를 Glove Embedding을 적용하여 ${DOMAIN}_asset_embeds.npy 에 저장

=> { "asset_id": id_list, "embedding": embeddings, "asset_feature_size": feature_size} 형태로 저장

 

4> Step 4: Convert all the splits into npy files for dataloader

# Step 4: Convert all the splits into npy files for dataloader.
SPLIT_JSON_FILES=("${TRAIN_JSON_FILE}" "${DEV_JSON_FILE}" "${DEVTEST_JSON_FILE}")
for SPLIT_JSON_FILE in "${SPLIT_JSON_FILES[@]}" ; do
    python tools/build_multimodal_inputs.py \
        --json_path="${SPLIT_JSON_FILE}" \
        --vocab_file="${VOCAB_FILE}" \
        --save_path="$ROOT" \
        --action_json_path="${SPLIT_JSON_FILE/.json/_api_calls.json}" \
        --retrieval_candidate_file="${SPLIT_JSON_FILE/.json/_retrieval_candidates.json}" \
        --domain="${DOMAIN}"
done

- tools/build_multimodal_inputs.py 파일 실행

=> SIMMC dataset(${DOMAIN}_{dataset 종류}_dials.json)에 있는 data를 npy 형태로 저장

(아직 Step 2와 Step 4의 차이가 확실하게 구분이 되지는 않음)

 

5> Step 5: Extract vocabulary for attributes from train npy file

# Step 5: Extract vocabulary for attributes from train npy file.
python tools/extract_attribute_vocabulary.py \
    --train_npy_path="${TRAIN_JSON_FILE/.json/_mm_inputs.npy}" \
    --vocab_save_path="${ATTR_VOCAB_FILE}" \
    --domain="${DOMAIN}"

- tools/extract_attribute_vocabulary.py 파일 실행

=> attribute(object 간에 연결되는 속성들)를 추출하여 vocabulary 형태로 기록

 

2) train_simmc_model.sh

단일 모델 train 실행 파일

 

3) train_all_simmc_model.sh

모든 모델 train 실행 파일

 

0> variable 및 parameter 관리

#!/bin/bash
GPU_ID=0
# DOMAIN="furniture"
DOMAIN="fashion"
ROOT="../data/simmc_${DOMAIN}/"


# Input files.
TRAIN_JSON_FILE="${ROOT}${DOMAIN}_train_dials.json"
DEV_JSON_FILE="${ROOT}${DOMAIN}_dev_dials.json"
DEVTEST_JSON_FILE="${ROOT}${DOMAIN}_devtest_dials.json"
METADATA_FILE="${ROOT}${DOMAIN}_metadata.csv"
# Output files.
VOCAB_FILE="${ROOT}${DOMAIN}_vocabulary.json"
METADATA_EMBEDS="${ROOT}${DOMAIN}_asset_embeds.npy"
ATTR_VOCAB_FILE="${ROOT}${DOMAIN}_attribute_vocabulary.json"
MODEL_METAINFO="models/${DOMAIN}_model_metainfo.json"
CHECKPOINT_PATH="checkpoints"
LOG_PATH="logs/"


COMMON_FLAGS="
    --train_data_path=${TRAIN_JSON_FILE/.json/_mm_inputs.npy} \
    --eval_data_path=${DEV_JSON_FILE/.json/_mm_inputs.npy} \
    --asset_embed_path=${METADATA_EMBEDS} \
    --metainfo_path=${MODEL_METAINFO} \
    --attr_vocab_path=${ATTR_VOCAB_FILE} \
    --learning_rate=0.0001 --gpu_id=$GPU_ID --use_action_attention \
    --num_epochs=100 --eval_every_epoch=5 --batch_size=20 \
    --save_every_epoch=5 --word_embed_size=256 --num_layers=2 \
    --hidden_size=512 \
    --use_multimodal_state --use_action_output --use_bahdanau_attention \
    --skip_bleu_evaluation --domain=${DOMAIN}"

1> 5개 모델 지원 

# History-agnostic model.
function history_agnostic () {
    python -u train_simmc_agent.py $COMMON_FLAGS \
        --encoder="history_agnostic" --text_encoder="lstm" \
        --snapshot_path="${CHECKPOINT_PATH}/$1/hae/" &> "${LOG_PATH}/$1/hae.log" &
}
# Hierarchical recurrent encoder model.
function hierarchical_recurrent () {
    python -u train_simmc_agent.py $COMMON_FLAGS \
        --encoder="hierarchical_recurrent" --text_encoder="lstm" \
        --snapshot_path="${CHECKPOINT_PATH}/$1/hre/" &> "${LOG_PATH}/$1/hre.log" &
}
# Memory encoder model.
function memory_network () {
    python -u train_simmc_agent.py $COMMON_FLAGS \
        --encoder="memory_network" --text_encoder="lstm" \
        --snapshot_path="${CHECKPOINT_PATH}/$1/mn/" &> "${LOG_PATH}/$1/mn.log" &
}
# TF-IDF model.
function tf_idf () {
    python -u train_simmc_agent.py $COMMON_FLAGS \
        --encoder="tf_idf" --text_encoder="lstm" \
        --snapshot_path="${CHECKPOINT_PATH}/$1/tf_idf/" &> "${LOG_PATH}/$1/tf_idf.log" &
}
# Transformer model.
function transformer () {
    python -u train_simmc_agent.py $COMMON_FLAGS \
        --encoder="history_agnostic" \
        --text_encoder="transformer" \
        --num_heads_transformer=4 --num_layers_transformer=4 \
        --hidden_size_transformer=2048 --hidden_size=256\
        --snapshot_path="${CHECKPOINT_PATH}/$1/transf/" &> "${LOG_PATH}/$1/transf.log" &
}

- history_agonstic() : HAE (--text_encoder를 "transformer" 사용시,  T-HAE 모델 사용 가능 <마지막 모델에서 구현>)

- hierarchical_recurrent() : HRE 

- memory_network() : MN

- tf-idf() : tf-idf 기반 모델

- transformer() : T-HAE 

 

2> train all model

# Train all models on a domain Save checkpoints and logs with unique label.
UNIQ_LABEL="${DOMAIN}_dstc_split"
CUR_TIME=$(date +"_%m_%d_%Y_%H_%M_%S")
UNIQ_LABEL+=$CUR_TIME
mkdir "${LOG_PATH}${UNIQ_LABEL}"

history_agnostic "$UNIQ_LABEL"
hierarchical_recurrent "$UNIQ_LABEL"
memory_network "$UNIQ_LABEL"
tf_idf "$UNIQ_LABEL"
transformer "$UNIQ_LABEL

 


 

3. train_simmic_agent.py

from __future__ import absolute_import, division, print_function, unicode_literals

import json
import math
import time
import os
import torch

import loaders
import models
import options
import eval_simmc_agent as evaluation
from tools import support

# Arguments.
args = options.read_command_line()

1) options.py

read_command_line 함수에서 parameter들을 받는다.

0> 기본 배경지식

- class argparse.ArgumentParser : 새로운 ArgumentParser 객체 생성 (description : parameter 도움말 전에 표시할 텍스트)

- required=True : 필수 argument

- default= : default 값 설정

- help= : 해당 argument에 대한 설명

 

1> --train_data_path : training data의 경로 (required=True)

2> --eval_data_path :  val data의 경로 (default=None)

3> --snapshot_path : checkpoint save할 경로 (default="checkpoints/")

4> --metainfo_path : metainfo json 파일이 있는 경로 (default="data/furniture_metainfo.json")

5> --attr_vocab_path : attribule vocabulary가 있는 경로 (default="data/attr_vocab_file.json")

6> --domain : 사용할 data (furniture vs fashion)

7> --asset_embed_path : (default="data/furniture_asset_path.npy")

8> --encoder : Utterance & History Encoder (required=True)(choices=[ "history_agnostic", "history_aware", "pretrained_transformer", "hierarchical_recurrent", "memory_network", "tf_idf", ]

- parsed_args["encoder"] == "pretrained_transformer"인 경우 현재 사용한 모델 이외의 pretrained transformer를 사용 가능

- parsed_args["encoder"] == "tf_idf"인 경우 => parsed_args["use_action_output"] = False

9> --text_encoder : (required=True)(choices=["lstm", "transformer"])

10> --word_embed_size : embedding size (default=128)(type=int)

11> --hidden_size : LSTM/transformer의 hidden state 크기 (word_embed_size와 같아야 한다.) (default=128)(type=int)

(transformer 사용시 word_embed_size와 hidden_size는 같아야 한다.)

<transformer text encoder의 parameter>

12> --num_heads_transformer : transformer의 head 개수 (default=-1)(type=int)

13> --num_layers_transformer : transformer의 layer 개수 (default=-1)(type=int)

14> --hidden_size_transformer : transformer의 hidden size (default=2048)(type=int)

15> --num_layers : LSTM layer의 개수

16> --use_action_attention : 모든 encoder에서 attenttion을 사용하는지 (default=False)

17> --use_action_output : 

18> --use_multimodal_state : fashion task에서 multimodal state를 사용하는지 ?? (default=False)

19> --use_bahdanau_attention : bahdanau방식의 attention 사용 여부

- parsed_args["use_action_output"] == True

- parsed_args["text_encoder"] == "lstm"

20> --skip_bleu_evaluation : validation 과정에서 BLEU score를 사용하는지 (default=True)

21> --max_encoder_len : 문장의 최대 encoding 길이 (default=24)(type=int)

22> --max_history_len : history의 최대 encoding 길이 (default=100)(type=int)

23> --max_decoder_len : 문장의 chleo decoding 길이 (default=26)(type=int)

24> --max_rounds : 대화의 최대 round<한 번 왔다 갔다> (default=30)(type=int)

25> --share_embeddings : encoder와 decoder가 embedding을 share하는지 (default=True)

<optimization hyperparameter>

26> --batch_size : <왜 기본이 30인가?> (default=30)(type=int)

27> --learning_rate : (default=0.001)(type=float)

28> --dropout : drop_rate (default=0.2)(type=float)

29> --num_epochs : (default=20)(type=int)

30> --eval_every_epoch : epoch마다 validation 진행 여부 (default=1)(type=int)

31> --save_every_epoch : epoch마다 저장 여부 <-1이면 저장 X> (default=-1)(type=int)

32> --save_prudently : best score인 경우만 모델 저장 (default=False)

33> --gpu_id : 사용하는 gpu <CPU 사용시 -1로 설정> (default=-1)

 

2) dataloader

1> dataloader_args

dataloader_args = {
    "single_pass": False,
    "shuffle": True,
    "data_read_path": args["train_data_path"],
    "get_retrieval_candidates": False
}

2> 기존 argument (위의 options.py에서 가져온 것)도 추가

dataloader_args.update(args)

3> loader/loader_simmc.py의 DataloaderSIMMC(loader_base.py에 존재하는 LoaderParent를 상속받음)를 가져온다.

train_loader = loaders.DataloaderSIMMC(dataloader_args)

4> validation을 해야하는 경우(validation 경로 지정된 경우) 일부 argument update

if args["eval_data_path"]:
    dataloader_args = {
        "single_pass": True,
        "shuffle": False,
        "data_read_path": args["eval_data_path"],
        "get_retrieval_candidates": args["retrieval_evaluation"]
    }
    dataloader_args.update(args)
    val_loader = loaders.DataloaderSIMMC(dataloader_args)
else:
    val_loader = None

 

3) Model

1> Load Model : models/assistant.py에서 모델을 불러온다. (assistant.py 전체 모델을 합친 구조이다.)

wizard = models.Assistant(args)
wizard.train()

2> encoder가 tf-idf일 경우 _ship_helper함수를 통해 numpy array에서 tensor로 변환

if args["encoder"] == "tf_idf":
    wizard.encoder.IDF.data = train_loader._ship_helper(train_loader.IDF)

3> optimizer : Adam

optimizer = torch.optim.Adam(wizard.parameters(), args["learning_rate"])

 

4) Train

1> 용어 정리

smoother = support.ExponentialSmoothing()
num_iters_per_epoch = train_loader.num_instances / args["batch_size"]
print("Number of iterations per epoch: {:.2f}".format(num_iters_per_epoch))
eval_dict = {}
best_epoch = -1

iter_per_epoch = num_instances(dataloader에 존재하는 data 개수) / batch_size

num_iters_per_epoch : epoch당 train 횟수

train_loader.get_batch() : batch generator (loader_base.py의 LoaderParent 참고)

iter_ind : batch의 개수를 나타내는 index

=> 그래서 num_iters_per_epoch이 iter_ind와 같아질 때마다 1 epoch이 된다.

 

 

2> propagation & parameter update

smoother = support.ExponentialSmoothing()

for iter_ind, batch in enumerate(train_loader.get_batch()):
    epoch = iter_ind / num_iters_per_epoch
    batch_loss = wizard(batch)
    batch_loss_items = {key: val.item() for key, val in batch_loss.items()}
    losses = smoother.report(batch_loss_items)

    # Optimization steps.
    optimizer.zero_grad()
    batch_loss["total"].backward()
    torch.nn.utils.clip_grad_value_(wizard.parameters(), 1.0)
    optimizer.step()

- 모델은 4가지의 loss를 계산 (loss_token, loss_action, loss_action_attr, loss_total)

- ExponentialSmoothing

(exponentially weighted moving average를 기반으로 [이전 값에 0.95], [현재 loss에 0.05]의 가중치를 주며 loss를 축적)

(사용하는 이유에 대한 추측 : 논문 6.2 Task 1에서 밝히길 perplexity는 exp(loglikelihood mean)를 의미해서)

class ExponentialSmoothing:
    """Exponentially smooth and track losses.
    """

    def __init__(self):
        self.value = None
        self.blur = 0.95
        self.op = lambda x, y: self.blur * x + (1 - self.blur) * y

    def report(self, new_val):
        """Add a new score.

        Args:
            new_val: New value to record.
        """
        if self.value is None:
            self.value = new_val
        else:
            self.value = {
                key: self.op(value, new_val[key]) for key, value in self.value.items()
            }
        return self.value

3> iteration progress

    if iter_ind % 50 == 0:
        cur_time = time.strftime("%a %d%b%y %X", time.gmtime())
        print_str = (
            "[{}][Ep: {:.2f}][It: {:d}][A: {:.2f}][Aa: {:.2f}]" "[L: {:.2f}][T: {:.2f}]"
        )
        print_args = (
            cur_time,
            epoch,
            iter_ind,
            losses["action"],
            losses["action_attr"],
            losses["token"],
            losses["total"],
        )
        print(print_str.format(*print_args))

 

5) validation

    # Perform evaluation, every X number of epochs.
    if (
        val_loader
        and int(epoch) % args["eval_every_epoch"] == 0
        and (iter_ind == math.ceil(int(epoch) * num_iters_per_epoch))
    ):
        eval_dict[int(epoch)], eval_outputs = evaluation.evaluate_agent(
            wizard, val_loader, args
        )
        # Print the best epoch so far.
        best_epoch, best_epoch_dict = support.sort_eval_metrics(eval_dict)[0]
        print("\nBest Val Performance: Ep {}".format(best_epoch))
        for item in best_epoch_dict.items():
            print("\t{}: {:.2f}".format(*item))

 

evaluation.evaluate_agent(wizard, val_loader, args) : val_loader(DataLoader)와 wizard(Model)에 대한 validation 과정

beam이라는 개념?

1> BLEU score

(loader_simmc.py의 DataloaderSIMMC 클래스의 메서드인 stringify_beam_output, evaluate_response_generation)

- stringify_beam_output은 모델의 output을 string으로 바꿔주는 작업을 진행

- 그리고 evaluate_response_generation을 가지고 BLEU score를 계산

2> retrieval score

(정확한 retrieval score의 정의)

3> perplexity

4> action prediction

- tools의 action_evaluation.py에서 evaluate_action_prediction을 통해 action_accuracy, action_perplexity, attribute_accuracy 계산

 

※ 결론

eval_dict에는 BLEU score, retrieval score, perplexity, action_accuracy, action_perplexity, attribute_accuracy

eval_outputs에는 action_prediction과 model_responses가 있다.

 

6) Save the model

    if (
        args["save_every_epoch"] > 0
        and int(epoch) % args["save_every_epoch"] == 0
        and (iter_ind == math.ceil(int(epoch) * num_iters_per_epoch))
    ):
        # Create the folder if it does not exist.
        os.makedirs(args["snapshot_path"], exist_ok=True)
        # If prudent, save only if best model.
        checkpoint_dict = {
            "model_state": wizard.state_dict(),
            "args": args,
            "epoch": best_epoch,
        }
        if args["save_prudently"]:
            if best_epoch == int(epoch):
                save_path = os.path.join(args["snapshot_path"], "epoch_best.tar")
                print("Saving the model: {}".format(save_path))
                torch.save(checkpoint_dict, save_path)
        else:
            save_path = os.path.join(
                args["snapshot_path"], "epoch_{}.tar".format(int(epoch))
            )
            print("Saving the model: {}".format(save_path))
            torch.save(checkpoint_dict, save_path)
        # Save the file with evaluation metrics.
        eval_file = os.path.join(args["snapshot_path"], "eval_metrics.json")
        with open(eval_file, "w") as file_id:
            json.dump(eval_dict, file_id)

1> "save_every_epoch"인 경우

  • "save_prudently"이면 최고 성능인 model을 계속해서 덮어 쓰는 형태로 저장하고
  • 그렇지 않으면 모든 epoch의 모델을 저장한다.

2> 저장 파일

- 모델은 .tar 확장자로 저장하고

- validation metric은 json 형태로 저장 (원래도 dictionary 형태로 관리하고 있어서)

 


4. Model (Assistant)

1) __init__

import torch
import torch.nn as nn

from tools import weight_init, torch_support
import models
import models.encoders as encoders


class Assistant(nn.Module):
    """SIMMC Assistant Agent.
    """

    def __init__(self, params):
        super(Assistant, self).__init__()
        self.params = params

        self.encoder = encoders.ENCODER_REGISTRY[params["encoder"]](params)
        self.decoder = models.GenerativeDecoder(params)

        if params["encoder"] == "pretrained_transformer":
            self.decoder.word_embed_net = (
                self.encoder.models.decoder.bert.embeddings.word_embeddings
            )
            self.decoder.decoder_unit = self.encoder.models.decoder

        # Learn to predict and execute actions.
        self.action_executor = models.ActionExecutor(params)
        self.criterion = nn.CrossEntropyLoss(reduction="none")

        # Initialize weights.
        weight_init.weight_init(self)
        if params["use_gpu"]:
            self = self.cuda()
        # Sharing word embeddings across encoder and decoder.
        if self.params["share_embeddings"]:
            if hasattr(self.encoder, "word_embed_net") and hasattr(
                self.decoder, "word_embed_net"
            ):
                self.decoder.word_embed_net = self.encoder.word_embed_net

2) forward

1> 기본 forward propagation 과정

def forward(self, batch, mode=None):
        """Forward propagation.

        Args:
          batch: Dict of batch input variables.
          mode: None for training or teaching forcing evaluation;
                BEAMSEARCH / SAMPLE / MAX to generate text
        """
        outputs = self.encoder(batch)
        action_output = self.action_executor(batch, outputs)
        outputs.update(action_output)
        decoder_output = self.decoder(batch, outputs)

- outpus = self.encoder(batch) 

  • Utterance & History Encoder

- action_output = self.action_executor(batch, outputs)

(action_executor 함수 내부에서 Multimodal Fusion이 구현되어 있음에 주의)

  • Multimodal Fusion
  • Action Predictor

- decoder_output = self.decoder(batch, outputs)

  • Response Generator