🧬

rinna GPT-2モデルの生成パラメータ

2021/09/08に公開

GPT-2のrinnaモデルにて、文章生成する際には以下のようなコードを利用します。

input_ids = tokenizer.encode("昔々あるところに、", return_tensors="pt",add_special_tokens=False)
output = model.generate(input_ids,max_length=50)
print(tokenizer.batch_decode(output,skip_special_tokens=True))
# ['昔々あるところに、おじいさんとおばあさんが住んでいました。 おばあさんは、おじいさんが作ったおにぎりを、おじいさんに食べさせました。 おばあ']

より品質のよい文書の生成をするにはmode.generate()のパラメータを適切に設定すると良さそうです。
パラメータにどんな物があるか分からなかったので調べました。

パラメータ解説記事を見る

以下の記事にてTransformersの文章生成時のパラメータについて解説があります。

https://huggingface.co/blog/how-to-generate#beam-search

記事で紹介されているパラメータをrinnnaモデルで試してみます。

欲張り検索(Greedy Search)

パラメータ無しの場合、これになります。冒頭のコードと同じです。
ある単語に続く確率が高いものを素朴に生成していきます。
文中に、繰り返し同じような単語が出てしまいます。

input_ids = tokenizer.encode("昔々あるところに、", return_tensors="pt",add_special_tokens=False)
output = model.generate(input_ids,max_length=50)
print(tokenizer.batch_decode(output,skip_special_tokens=True))
# ['昔々あるところに、おじいさんとおばあさんが住んでいました。 おばあさんは、おじいさんが作ったおにぎりを、おじいさんに食べさせました。 おばあ']

num_beamsを2以上に設定することで、Beam searchが行われます。
生成文書は、全体を考慮して、単語間の確率のスコアが高くなるようなものが選ばれます。
こちらも同様に文中に、繰り返し同じような単語が出ます。

beam_output = model.generate(input_ids,max_length=50,num_beams=5,early_stopping=True)
print(tokenizer.decode(beam_output[0],skip_special_tokens=True))
# 昔々あるところに、おじいさんとおばあさんが住んでいました。 ある日、おじいさんとおばあさんは、おじいさんとおばあさんの家に遊びに行きました。 おじいさん

繰り返しにペナルティno_repeat_ngram_sizeを与えると、単語の繰り返しがなくなります。
一見良さそうですが、同じ単語が使えなくなるので長文生成などでは使いにくいでしょう。

beam_output = model.generate(input_ids, max_length=50,  num_beams=5,  no_repeat_ngram_size=2, early_stopping=True)
print(tokenizer.decode(beam_output[0],skip_special_tokens=True))
# 昔々あるところに、おじいさんとおばあさんが住んでいました。 ある日、いつものようにお父さんとお母さんは畑仕事をしていたのですが、 すると、畑の真ん中に、大きな穴が空

num_return_sequencesでスコアが上位のものをリストで取得できます。

beam_outputs = model.generate(input_ids, max_length=50, num_beams=5,no_repeat_ngram_size=2, num_return_sequences=5,early_stopping=True)
for i, beam_output in enumerate(beam_outputs): 
  print("{}: {}".format(i,tokenizer.decode(beam_output, skip_special_tokens=True)))

# 0: 昔々あるところに、おじいさんとおばあさんが住んでいました。 ある日、いつものようにお父さんとお母さんは畑仕事をしていたのですが、 すると、畑の真ん中に、大きな穴が空
# 1: 昔々あるところに、おじいさんとおばあさんが住んでいました。 ある日、いつものようにお父さんとお母さんは畑仕事をしていたのですが、 すると、畑の真ん中に小さなお地蔵様が
# 2: 昔々あるところに、おじいさんとおばあさんが住んでいました。 ある日、いつものようにお父さんとお母さんは畑仕事をしていたのですが、 すると、畑の真ん中に、大きな木が生え
# 3: 昔々あるところに、おじいさんとおばあさんが住んでいました。 ある日、いつものようにお父さんとお母さんは畑仕事をしていたのですが、 すると、畑の真ん中に、大きな穴があい
# 4: 昔々あるところに、おじいさんとおばあさんが住んでいました。 ある日、いつものようにお父さんとお母さんは畑仕事をしていたのですが、 すると、畑の真ん中に、大きな穴が開

サンプリング

単語間の確率を、確率の高低に従いランダムにピックアップしたものをサンプリングと呼びます。
do_sampleで有効化されます。
生成文書はこれまでのものよりもダイナミックに変化します。
ただし、文書に一貫性が無くなります。

output = model.generate(input_ids,do_sample=True, max_length=50, top_k=0)
print(tokenizer.batch_decode(output,skip_special_tokens=True))
# ['昔々あるところに、貧乏な門弟だった男がいました。 父方のお宅に縁談が持ち上がって、お願いしてきました。 そうお手伝いをして縁談が打ち切られたどり着いたのが、この']

一貫性を上げるため、確率に制限を加えます。
temparatureで確率の高いものをより高く、低いものはより確率を低くします。
非サンプリング状態の結果との中間のような感じになります。
同じ単語が生じてしまう問題はまた出てきます。
temparatureが0で非サンプリング状態となります。

sample_output = model.generate(input_ids, do_sample=True, max_length=50, top_k=0, temperature=0.7)
print(tokenizer.batch_decode(sample_output,skip_special_tokens=True))
# ['昔々あるところに、おじいさんとおばあさんが住んでいました。おじいさんが、ゆうゆうと畑仕事をしていると、裏山の畑の草が伸びて、ゆうゆうと草を刈り取って']

Top-K sampling

サンプリング時には確率の上位候補に絞ってピックアップするという方式です。
top_kに絞り込み数を設定します。
程よくランダム性が増し、また違和感の少ない文章が生成されます。

sample_output = model.generate(input_ids,do_sample=True, max_length=50, top_k=50)
print(tokenizer.batch_decode(sample_output,skip_special_tokens=True))
# ['昔々あるところに、とてもいい顔した美しい娘さんがいらっしゃいました。 その娘さんは美人でしたが、心が優しく穏やかで、そして、芯の強い芯の女の人でした。 娘さんとはいつ']

Top-P sampling

サンプリング時に確率が一定以上の単語に絞ってピックアップするという方式です。
top_pに加減となる確率を0~1の範囲で設定します。
突然話題が変わっていますが、top_kと同様にある程度読める文書が出力されます。

sample_output = model.generate(input_ids,do_sample=True,max_length=50,top_p=0.92,top_k=0)
print(tokenizer.batch_decode(sample_output,skip_special_tokens=True))
# ['昔々あるところに、大学の合宿に来る学生がいました。 それを見て私は、その合宿に来る学生たちの購買を手伝いました。 名探偵コナンと言えば、悪魔と契約した魔導書を持つことが有名です。 しかし、それよりももっと']

Top-KとTop-Pは両方指定できます。一貫性を担保する時には両方を指定すると良いでしょう。
引き換えに多様性が失われると思います。

設定する数値がどれぐらいが適切なのかはイマイチ分かりません。
ここらへんのハイパーパラメータを設定する指標はあるのかな?🤔

複数の文章を出力する場合にはnum_return_sequencesを利用します。

sample_outputs = model.generate(input_ids,do_sample=True,max_length=50,top_k=50,top_p=0.95,num_return_sequences=3)
for i, sample_output in enumerate(sample_outputs):
  print("{}: {}".format(i, tokenizer.decode(sample_output, skip_special_tokens=True)))
# 0: 昔々あるところに、おじいちゃんとおじいちゃん(もしくはおばあちゃん) の家に 1 組の夫婦がいました。 1 組の夫婦の家には必ず、おじいちゃんとおばあちゃんが
# 1: 昔々あるところに、木の板を焼いて作った、おやきがあるのです。 昔の人は、こんな焼き菓子を焼いて、皆さんを喜ばせていたのでしょう。 私たちは皆さんが大好きな
# 2: 昔々あるところに、まだ見ぬ、美しい女性、桃源郷にひとりの美しい姫君がいました。その姫君が夢にうたわれ、お姫様からお告げを受けた時、あなたならどちらの姫君を選び

パラメータを見る

以下のHuggingFaceのドキュメントを参考にmodel.generate()パラメータを見ていきます。

https://huggingface.co/transformers/main_classes/model.html#transformers.generation_utils.GenerationMixin.generate

全般

  • input_ids
    • 種となるトークン化した開始文字列
  • num_return_sequences
    • 複数結果の出力設定
  • max_time
    • 処理時間への制限
  • use_cache
    • デコードの高速化設定
  • remove_invalid_values
    • nanおよびinf出力を削除してクラッシュを予防
    • 設定すると生成が遅くなる可能性あり
  • model_kwargs
    • モデル固有のkwargsを指定

出力形式の設定

  • return_dict_in_generate
    • 出力結果をタプルの代わりにdictで返す
    • 以下のoutput_**のデータを取得する際に設定しておくと便利
  • output_attentions
    • 全てのアテンションレイヤーのアテンションテンソルを返すかどうか
  • output_hidden_states
    • 全てのレイヤーの非表示状態を返すかどうか
  • output_scores
    • 予測スコアを返すかどうか
    • なんかに使えそうですが、テンソル形式なのでよくわかりませんでした…🤯
output = model.generate(input_ids,max_length=50,return_dict_in_generate=True,output_scores=True)
for key in output:
  print(key); 
print(tokenizer.decode(output.sequences[0],skip_special_tokens=True))

# sequences
# scores
# 昔々あるところに、おじいさんとおばあさんが住んでいました。 おばあさんは、おじいさんが作ったおにぎりを、おじいさんに食べさせました。 おばあ

生成文書の長さの調整

  • max_length
    • 生成されるテキストの最大長
  • max_new_tokens
    • 生成するトークンの最大数。max_lengthと同じような働き
  • min_length
    • 生成されるテキストの最小長
  • length_penalty
    • 生成する文書の長さへペナルティを設定。文書の長さを調整できる。

Special Token関連

  • pad_token_id/bos_token_id/eos_token_id
    • モデルがこれらのトークンを持っていない場合ユーザーが設定できる
  • bad_words_ids
    • 生成が許可されていないトークンIDのリスト
    • 禁止用語などの設定に使うと思われる
  • Attention_mask
    • Padding Tokenへのマスク
  • decoder_start_token_id
    • bosとは異なるトークンでデコードを開始する場合に指定
  • forced_bos_token_id
    • decoder_start_token_idの後に最初に生成されたトークンとして強制するトークンID
  • forced_eos_token_id
    • max_lengthに達したときに最後に生成されたトークンとして強制するトークンID

繰り返しへの調整

  • repetition_penalty
    • 繰り返しペナルティのパラメータ。1.0はペナルティがないことを意味する。
    • 繰り返しを防ぐのに非常に効果的だが、さまざまなモデルやユースケースに非常に敏感
  • no_repeat_ngram_size
    • 指定されたngramサイズの繰り替えしへの制約
    • 1回のみ発生
  • encoder_no_repeat_ngram_size
    • encoder_input_idsで発生するそのサイズのすべてのngramは、decoder_input_idsで発生することはできない

サンプリング

  • do_sample
    • サンプリングを使用するかどうか
  • top_k
    • top-kフィルタリング
  • top_p
    • top-pフィルタリング。0 < 1で設定。
  • temperature
    • 高い確率の単語の可能性を増加させ、低確率の単語の可能性を減少させる。
    • beam search用と思われるが、汎用?

beam search系

  • num_beams
    • ビーム検索用のビーム数。 1はビーム検索がないことを意味する。
  • Eearly_stopping
    • ビーム検索でEOSトークンに到達したときに生成が終了するように設定
  • num_beam_groups
    • num_beamをグループ化して多様性をもたせる設定
  • diversity_penalty
    • beam searchで他のグループと同じトークンを生成する場合、ビームのスコアから差し引かれる
  • prefix_allowed_tokens_fn
    • 各ステップでのみ許可されたトークンにビーム検索を制約
    • プレフィックスを条件とする制約付き生成に役立つ

Colab Notebook

参考にrinna GPT-2モデルを試したノートブックはこれです。

https://colab.research.google.com/drive/18OjecuAMjnoKRcwc4PPfmEnMT0erQ4xQ?usp=sharing

まとめ

GPT-2による文章生成時には、適切に制約をかけると出力内容の品質が良くなることが分かりました。
参考記事を見る限り、基本的にはbeam searchなしでtop-k,top-p指定で生成すればデフォルト出力よりも出力の品質が良くなりそうです。
ただハイパーパラメータの調整が大変そうですが…

Special Tokenの扱いがまだイマイチわかりませんでしたので、引き続き調べていきたいです。

Discussion