OptunaでFlair NERのパラメーターチューニング
とある仕事で,固有表現抽出器の性能をぎりぎりまで向上させる必要があったため,自作のコーパスに対してFlairでNERを学習し,Optunaでハイパーパラメータチューニングを行なった.本記事ではそのときのコードを示す.
Embeddingについては事前にどれが有効かわかっていたので固定にし,Learning rateなど細かい設定をチューニングするつくりにした.
学習部の関数(Objective)では,micro F1スコアを返すようになっているため,optuna.create_study(direction=’maximize’)と,directionにmaximizeを設定.
評価関数(スクリプト内ではoptimizerと定義)もチューニングしようとするが,デフォルトのもの以外で性能がガタ落ちしたため,取りやめ.私の設定が甘かったのか?
また,optunaのパラメーター設定で必要な関数は,私の場合,trial.suggest_intとtrial.suggest_floatとtrial.suggest_categoricalの3つだけで十分だと感じた.設定の方法は下記スクリプトに記載しているのでどうぞ参考にされたし.
本記事の最下部では,このチューニング用スクリプトを実行して出力されるcsvの例を示す.
flair_ner_optimization.py
# このスクリプトはflair==0.4.3で動作確認 from pathlib import Path from argparse import ArgumentParser from flair.models import SequenceTagger from flair.data import Dictionary, Corpus from flair.datasets import ColumnCorpus from flair.embeddings import ( # TransformerWordEmbeddings, StackedEmbeddings, BytePairEmbeddings, CharacterEmbeddings, ELMoEmbeddings, ) from flair.trainers import ModelTrainer # import allennlp import torch import optuna def objective(trial): parser = ArgumentParser() parser.add_argument("corpus", type=Path) parser.add_argument("output", type=Path) parser.add_argument("--transformer", type=str) parser.add_argument("--fine_tune", action="store_true") parser.add_argument("--device", type=str, default="cpu") parser.add_argument("--elmo", type=str) parser.add_argument("--bpe", action='store_true') parser.add_argument("--char", action='store_true') parser.add_argument("--electra_small", action='store_true') parser.add_argument("--electra_base", action='store_true') parser.add_argument("--electra_large", action='store_true') args = parser.parse_args() lr = trial.suggest_float('lr', 0.05, 0.3, step=0.05) dropout = trial.suggest_float('dropout', 0.3, 0.6, step=0.1) locked_dropout = trial.suggest_float('locked_dropout', 0.3, 0.6, step=0.1) hidden_size = trial.suggest_categorical('hidden_size', [32, 64, 128, 256]) mini_batch_size = trial.suggest_categorical('mini_batch_size', [16, 32, 64]) rnn_layers = trial.suggest_int('rnn_layers', 1, 3, step=1) # anneal_factor = trial.suggest_float('anneal_factor', 0.1, 0.6, step=0.1) word_dropout= trial.suggest_float('word_dropout', 0.05, 0.15, step=0.05) beta = trial.suggest_float('beta', 0.7, 1.0, step=0.1) weight_decay = trial.suggest_float('weight_decay', 0.0001, 0.0005, step=0.0001) # optimizer = trial.suggest_categorical('optimizer', ['Default', 'AdamP', 'AdaBelief', 'DiffGrad', 'SGDP']) corpus_dir = args.corpus output_dir = args.output labels_txt = args.corpus / "labels.txt" fine_tune = args.fine_tune device = args.device transformer = args.transformer elmo = args.elmo char = args.char bpe = args.bpe electra_small = args.electra_small electra_base = args.electra_base electra_large = args.electra_large # embeddings embedding_types = [] if transformer: embedding_types.append(TransformerWordEmbeddings(transformer, from_tf=Path(transformer).exists(), fine_tune=fine_tune)) if elmo: # ELMoをEmbeddingに使う場合は,指定するパス(フォルダ)に,elmo_options.jsonとelmo_weights.hdf5が入っている必要がある. embedding_types.append(ELMoEmbeddings("custom",options_file=str(Path(elmo)/'elmo_options.json'), weight_file=str(Path(elmo)/'elmo_weights.hdf5'))) if bpe: embedding_types.append(BytePairEmbeddings("en")) if char: embedding_types.append(CharacterEmbeddings()) if electra_small: embedding_types.append(TransformerWordEmbeddings("google/electra-small-discriminator", fine_tune=fine_tune)) if electra_base: embedding_types.append(TransformerWordEmbeddings("google/electra-base-discriminator", fine_tune=fine_tune)) if electra_large: embedding_types.append(TransformerWordEmbeddings("google/electra-large-discriminator", fine_tune=fine_tune)) embeddings = StackedEmbeddings(embedding_types) # prepare corpus columns = {0: "text", 1: "ner"} corpus = ColumnCorpus( corpus_dir, columns, train_file="train.tsv", test_file="test.tsv", dev_file="devel.tsv", # column_delimiter="\t" # corpus_dir, columns, train_file="dev.txt", test_file="test.txt", dev_file="dev.txt", column_delimiter="\t" ) # create label dictionary # labels = labels_txt.read_text().split("\n") # tag_dictionary = Dictionary() # for lbl in labels: # tag_dictionary.add_item(lbl) tag_type = "ner" tag_dictionary = corpus.make_tag_dictionary(tag_type=tag_type) print(tag_dictionary) # define model # tagger = SequenceTagger(hidden_size=512, embeddings=embeddings, tag_dictionary=tag_dictionary, tag_type=tag_type) tagger = SequenceTagger( hidden_size=hidden_size, rnn_layers=rnn_layers, embeddings=embeddings, tag_dictionary=tag_dictionary, dropout=dropout, locked_dropout=locked_dropout, word_dropout=word_dropout, beta=beta, tag_type=tag_type, use_crf=True ) # 参考のため, flairのリポジトリからSequenceTaggerの引数を取得 # def __init__( # self, # hidden_size: int, # embeddings: TokenEmbeddings, # tag_dictionary: Dictionary, # tag_type: str, # use_crf: bool = True, # use_rnn: bool = True, # rnn_layers: int = 1, # dropout: float = 0.0, # word_dropout: float = 0.05, # locked_dropout: float = 0.5, # reproject_embeddings: Union[bool, int] = True, # train_initial_hidden_state: bool = False, # rnn_type: str = "LSTM", # pickle_module: str = "pickle", # beta: float = 1.0, # loss_weights: Dict[str, float] = None, # ): # if optimizer == 'AdamP': # from torch_optimizer import AdamP as opt # elif optimizer == 'AdaBelief': # from torch_optimizer import AdaBelief as opt # elif optimizer == 'DiffGrad': # from torch_optimizer import DiffGrad as opt # elif optimizer == 'SGDP': # from torch_optimizer import SGDP as opt # if optimizer == 'Default': # trainer: ModelTrainer = ModelTrainer(tagger, corpus) # else: # trainer: ModelTrainer = ModelTrainer(tagger, corpus, optimizer=opt) trainer: ModelTrainer = ModelTrainer(tagger, corpus) result = trainer.train( base_path=output_dir, learning_rate=lr, mini_batch_size=mini_batch_size, max_epochs=25, # patience=5, monitor_test=True, embeddings_storage_mode=device, save_final_model=False, # パラメータ探索が目的のため # anneal_factor=anneal_factor, weight_decay=weight_decay, ) # 参考のため, flairのリポジトリからtrainの引数を取得 # TRAINING # trainer = ModelTrainer(tagger, corpus, use_tensorboard=True, optimizer=torch.optim.AdamW) # trainer.train( # base_path=output_dir, # learning_rate=1e-3, # mini_batch_size=16, # mini_batch_chunk_size=None, # max_epochs=100, # # scheduler=AnnealOnPlateau, # # cycle_momentum=False, # # anneal_factor=0.5, # patience=3, # initial_extra_patience=0, # min_learning_rate=1e-8, # train_with_dev=False, # monitor_train=False, # monitor_test=False, # embeddings_storage_mode=device, # checkpoint=True, # save_final_model=True, # # anneal_with_restarts=False, # # anneal_with_prestarts=False, # # batch_growth_annealing=False, # shuffle=True, # param_selection_mode=False, # write_weights=False, # num_workers=6, # sampler=None, # use_amp=False, # amp_opt_level="O1", # eval_on_train_fraction=0.0, # eval_on_train_shuffle=False, # ) return result['test_score'] if __name__ == "__main__": study = optuna.create_study(direction='maximize') study.optimize(objective, n_trials=50) print('params:', study.best_params) print('value:', study.best_value) study_df = study.trials_dataframe() study_df.to_csv('./flair_ner_optimization.csv')
上のスクリプトを実行し,得られる結果(csv).
これをみると,各ステップで探索されたパラメーターが一目瞭然である.結果の中から最もF1スコア(ここでいうvalue)の高いパラメータの組み合わせを本番の学習で設定することによって,性能改善の効果が期待できる.
筆者の場合は,0.2~0.4くらいしか性能が上がっていないが,チューニング前よりは確かに上がっているため,やらないよりはましだろう.また,深層学習ベースの学習器の場合,ある1つの設定でたまたま高スコアがでることがよくあるため,こうして何度も学習を行い,どのステップにおいても同じくらいのスコアがでるのを確認することは,モデルのデバッグを行うことにおいても有効なのではないかと思う.
number value datetime_start datetime_complete duration params_beta params_dropout params_hidden_size params_locked_dropout params_lr params_mini_batch_size params_rnn_layers params_weight_decay params_word_dropout state 0 0 0.763867827 10:59.9 37:11.1 0 days 00:26:11.182141000 0.8 0.6 256 0.6 0.2 32 1 0.0005 0.15 COMPLETE 1 1 0.800172265 37:11.1 14:16.9 0 days 00:37:05.824783000 1 0.4 256 0.3 0.25 16 2 0.0003 0.15 COMPLETE 2 2 0.793049793 14:16.9 34:33.8 0 days 00:20:16.894379000 1 0.4 128 0.3 0.2 64 1 0.0002 0.15 COMPLETE 3 3 0.774522723 34:33.8 55:13.7 0 days 00:20:39.848606000 0.7 0.4 64 0.4 0.15 64 2 0.0003 0.15 COMPLETE 4 4 0.759173047 55:13.7 15:23.5 0 days 00:20:09.795247000 0.8 0.6 64 0.4 0.05 64 1 0.0002 0.15 COMPLETE 5 5 0.724664384 15:23.5 41:44.8 0 days 00:26:21.303128000 0.7 0.6 32 0.4 0.15 32 2 0.0002 0.1 COMPLETE 6 6 0.786737225 41:44.8 08:52.6 0 days 00:27:07.824726000 0.8 0.3 64 0.4 0.2 32 3 0.0002 0.15 COMPLETE 7 7 0.783839779 08:52.6 44:54.4 0 days 00:36:01.805369000 1 0.6 256 0.4 0.2 16 1 0.0004 0.15 COMPLETE 8 8 0.773110651 44:54.4 05:12.4 0 days 00:20:17.958787000 0.7 0.4 128 0.4 0.05 64 1 0.0005 0.15 COMPLETE 9 9 0.76408377 05:12.4 41:33.8 0 days 00:36:21.409957000 0.8 0.5 64 0.6 0.1 16 2 0.0005 0.05 COMPLETE
ディスカッション
コメント一覧
まだ、コメントがありません