kntty.hateblo.jp

ここに何か書く。

100 / (100+α) みたいな割り算の手計算を、近似でラクする

100や1000などのキリの良い数を、「それプラスちょっと」の数で割る、という手計算を迫られる場面が、 少なからず(少なからず?)ある。

ぱっと浮かぶ例としては、税込金額における税別金額の比率を求める、とか。

100 / 108 = ? (※食品テイクアウトは、いまも、8 %のはず。)

電卓なければ筆算なりをする訳だが、 108という桁多き数に9を掛けて、引き算して…と、初手から気が進まない。
しかも、分母分子が逆なら一瞬て答えが出るだけに、なんか癪に思う。

で、それなら割り算しないで近似すれば良いじゃん、という 気づきがあったので、ここに紹介する。

計算方法

  1. キリのいい数字を$p$、加える"ちょっと"の数を$α$ とし、元の分数を $\frac{p}{p + α}$ で表す。
    ここで、$x = \frac{α}{p}$ を計算する。
    これはすなわち、$\frac{p}{p + α} = \frac{1}{1 + x}$を満たすxを求めていることに他ならない。

      # 先ほどの例(p=100、α=8)。
      100 / 108 = 100 / (100 + 8) = 1 / (1 + 0.08)
      x = 0.08
    
  2. 近似式 $\frac{1}{1 + x} ≃ 1 - x + x ^ 2$ に基づいて、右辺を計算する。以上。

     1 - x + x^2 = 1 - 0.08 + 0.08^2
                 = 1.0000 - 0.0800 + 0.0064
                 = 0.9264
     よって、1/(1 + x) ≒ 0.9264
    

電卓で求めると 100 / 108 = 0.925925925... となって、誤差は 0.00048 ぐらいに収まる。

理屈

$f(x) = \frac{1}{1 + x}$のマクローリン展開による。

$\frac{1}{1 + x} = 1 - x + x ^ 2 - x ^ 3 + ...$

xのべき乗が並び、符号が交互に変わることに注意。 2乗の項までで近似したのが、先ほどの近似式である。

誤差$E$を計算すると、

$E = \left| \frac{1}{1 + x} - (1 - x + x ^ 2) \right| $
$\quad = \left| \frac{1}{1 + x} \{ 1 - (1 - x ^ 3) \} \right| = \left| \frac{x ^ 3}{1 + x} \right| $

となって、おおよそxの3乗のレベルの誤差となることが分かる。 $1 \gg x$なら、十分近似になる。

もちろん、精度を求めるなら、更に$x ^ 3$乗を引けば良い。αが1桁なら暗算でも何とかなるはず。

発展

p / (p - α) のパターンにも応用できる(例:1000/997)。
符号を変えた場合($x → -x$)でも当然成り立つから、

$\frac{p}{p - α} = \frac{1}{1 - x} ≃ 1 + x + x ^ 2$

を使えば良い。

また、α / (p + α) のパターン(例:8 / 108)であれば、 1から引く形に式変形すれば良い。

8 / 108 = 1 - 100/108 ≃ 1 - 0.9264 = 0.0736 (近似)
8 / 108 = 0.0740740... (真値)

tf.Kerasの事前学習済みモデルに、正則化を加える正しい方法

TensorFlow(2系)の、 tf.keras.applicationsの学習済みモデルに、L2等の正規化を加える方法。 次の記事[1]で解決した。簡単にその要約を記す。

[1] Silva TS. How to Add Regularization to Keras Pre-trained Models the Right Way. 2019.

sthalles.github.io

  • Step 1. 学習済みモデルmodelが持つのlayerを走査する。 もしlayerがkernel_regularizerプロパティを持っていれば、そこにregularizerオブジェクトを代入する。
     (ここで加えた変更は、まだ、モデルそのものには反映されていない。)
  • Step 2. 一旦、config情報をjsonに書き出す。
  • Step 3. 重みについても、一時ファイルに書き出す。
  • Step 4. Step 3で書き出したconfig情報(json)を読み戻す。
    (regularizerは反映されるが、重みがリセットされてしまう。)
  • Step 5. Step 4で書き出した重みを、一時ファイルから読み戻す。

https://gist.github.com/sthalles/d4e0c4691dc2be2497ba1cdbfe3bc2eb から引用)

import os
import tempfile

def add_regularization(model, regularizer=tf.keras.regularizers.l2(0.0001)):

    if not isinstance(regularizer, tf.keras.regularizers.Regularizer):
      print("Regularizer must be a subclass of tf.keras.regularizers.Regularizer")
      return model

    for layer in model.layers:
        for attr in ['kernel_regularizer']:
            if hasattr(layer, attr):
              setattr(layer, attr, regularizer)

    # When we change the layers attributes, the change only happens in the model config file
    model_json = model.to_json()

    # Save the weights before reloading the model.
    tmp_weights_path = os.path.join(tempfile.gettempdir(), 'tmp_weights.h5')
    model.save_weights(tmp_weights_path)

    # load the model from the config
    model = tf.keras.models.model_from_json(model_json)

    # Reload the model weights
    model.load_weights(tmp_weights_path, by_name=True)
    return model

動作確認として、model.lossesにアクセスすれば、正則化の値をリストで取得することができる。 正則化を正しく反映できていれば、リストは空にならないはずである。

なお、プロパティとして、kernel_regularizerだけでなく bias_regularizeractivity_regularizerも加えることが可能である。

Transformerを理解するまでに私が陥った、3つの勘違い

自然言語処理でお馴染み、他の分野も席巻しつつあるTransformerについて、 やっと自分の理解が追いついてきた。

Transformerとは何か、については、良い記事がたくさんあるのでそちらを参照されたい。

念のため、最小限の説明をすると、次のような感じ。

  • 入力列から、辞書的に情報を拾い上げて出力列に伝える仕組み、もしくは、それを使ったモデル構造のこと。
  • Attentionと呼ばれる機構が、「辞書的に情報を拾い上げ」る役割を実現。

以前に調べたときは、上辺だけをさらっと知ることを優先していたために、 結構勘違いが多かったことに気づかされた。そのことについて、ここにメモしておく。

(#上辺じゃなくて最初からちゃんと理解せえ、という自分への戒めも込めて。)

(2021.7.16 5月頃にメモとして書いていたものを、タイトル含め書き直して再投稿)

Transformerモデルについて

1. 「Transformer」という語が指す範囲は、文脈による

特に、TransformerとBERTをいっぺんに理解しようとしてはまった点。

私の理解の上では、(Original)Transformerの1層とBERTの1層が、 図のように対応する。

f:id:kntty:20210713092013p:plain
Transformer[1]とBERT[2]の図の関係

Attention Is All You Needの論文の解説記事だと、そもそも左の図全体を (Originalの) Transformerと呼ぶことが多い。一方で、その応用技術では、図の1層分をTransformerと捉えている印象がある。

なお、上の左右の図は、そもそも見比べるべきではないことに注意が必要である。

右図は、先行研究のELMo等の概念と比べるために書かれた図であって、 BERTの図が間違っている、とか、そういう批判をしたい訳ではないことを補足しておく。

2. 「BERT」は「(単なる)双方向Transformer」ではない

特に双方向RNN等を知っている上で、"Bidirectional Transformer"と言われると、 何か計算上の改良が加わっているのではないか、と勘違いが働きやすい(私だけ?)がそうではない。

では、BERTの何が双方向性を持つのか、というと、「学習対象のタスクが」である。

  • 従来よく行われていた、「文章の n-1 語めまでを入力して、n 語めを推測する」タスクでは、前向き方向にしか推定が行えない (*1)。
  • 対して、BERTで設定されたタスク(2つのうち)の1つは、「文章の n 語めを隠して入力し、隠された n 語めを推測する」というタスクである。これなら、前向きにも後ろ向きにも単語情報を活用する、という恩恵が得られる。

つまり、双方向の予測が働くように設定したタスクによって、Transformerが双方向性の予測にも強くなった、ということである。

(*1) 補足:

  • 文をひっくり返して、後ろ向きも予測すればいいという考え方であれば、双方向RNNで導入されており、これは前身のELMoで採用されていた方法である。

3. Decoderは、再帰せずに文書生成を行える訳ではない

「TransformerはRNNでないから、並列処理できる」的な説明だけを見て これも誤認してしまった。これは「学習時」にのみ、当てはまる。

例として、原文「彼/は/あの/通り/沿い/に/住んでいる」とその訳文「He/lives/along/that/street」を考える。 この場合、Transformer翻訳モデルの入力と出力の関係は、次のようになる。

(Encoderへの入力/Decoderへの入力 → /Decoderに期待される出力)

  • [彼][は][あの][通り][沿い][に][住んでいる][<終>] / [<空>] → [He]
  • [彼][は][あの][通り][沿い][に][住んでいる][<終>] / [<空>][He] → [He][lives]
  • [彼][は][あの][通り][沿い][に][住んでいる][<終>] / [<空>][He][lives] → [He][lives][along]
  • [彼][は][あの][通り][沿い][に][住んでいる][<終>] / [<空>][He][lives][along] → [He][lives][along][that]
  • [彼][は][あの][通り][沿い][に][住んでいる][<終>] / [<空>][He][lives][along][that] → [He][lives][along][that][street]
  • [彼][は][あの][通り][沿い][に][住んでいる][<終>] / [<空>][He][lives][along][that][street] → [He][lives][along][that][street][<終>]

学習時は、途中までの予測が完璧だと仮定して、次の1単語の予測を行う。 RNNみたいに内部状態が要るわけではないので、6つ分をいっぺんに並列計算できる。

一方、テスト時は、当然「途中までの予測」なんてものは手元にないので、 6つ分をいっぺんに、ではなく、RNNのDecoderと同じようなイメージで、 1つずつ単語を増やして予測していく。

参考文献

Attentionと、全結合・畳み込みとの関係

Transformerに使われるAttentionと、FFN(Position-wise Dense)、全結合(Dense)、畳み込み(Convolution)の 関係を俯瞰するために、お絵描きを試みたので、ここに載せる。

もしかしたら不正確な表現があるかもしれないが、ご容赦いただきたい。

なお、図では、下側を入力、上側を出力としている。

(2021/6/30: 図のインデックスの誤りを修正)

1. Dense (全結合)

全結合は、基本的に重み行列$ \bm W $の乗算で表される。

なお、通常はもうひとつ別の重みベクトル(バイアス)$\bm b$を用意して、 $\bm y = \bm W \bm x + \bm b$とするが、本記事では省略する。

f:id:kntty:20210630144745p:plain
Dense (no bias)

2. Convolution (畳み込み)

簡単のため、1次元の畳み込み演算(カーネルサイズ3、入出力の次元は同じ)とする。

畳み込み演算は、一部の重み(1次元畳み込みの場合は、行列の対角方向の要素)が共有され、かつ、 重みの外側がゼロである行列を用意することで、全結合と同様に行列演算として表すことができる。

バイアスを組み入れることの方が多いが、その場合は$\bm y = \bm C \bm x + \bm b$とする。

f:id:kntty:20210630144909p:plain
Convolution (1D)

3. Position-wise Dense

ここから2次元(Depth軸とPosition軸)。

Transformerブロックでも使われるPosition-wise Denseでは、 Depth軸だけを入力とした全結合(重み$\tilde \bm W$)を、 全てのPosition位置で行う。このとき、重み$\tilde \bm W$は、Position位置に依らず共有とする。

f:id:kntty:20210630144959p:plain
Position-wise Dense

4. Depth-wise Convolution

Transformerとは関係ないが、比較対象として。

Depth-wise Convolutionでは、全てのDepth位置で、Position方向に広がった畳み込みを行う。

畳み込む対象がスカラーからベクトルになった、と考えた方が分かりやすいかも。

なお、Depth-wise Convolutionの2次元版の応用例としては、 フルのConvolutionをDepth-wiseとPoint-wiseに分割しすることによる パラメータ数削減(モデル軽量化)が挙げられる。

f:id:kntty:20210630145033p:plain
Depth-wise Convolution (1D)

5. Self Attention / Source-Target Attention

以上を踏まえて、Attentionの基本的な部分を図にすると、このような感じ。

ValueもKeyもQueryも、Position-wise Denseに相当する方法で生成する。

Depth-wise Convolutionのような、ベクトルを重み付けして集約する機能があり、 その重み付けは、QueryとKeyの内積ベースで決まる。

完全に全結合として行数×列数分の要素を重みパラメータとするよりも、 行(q)と列(k)に分けて用意する方が、(少なくとも)重みパラメータは少なくて済むことが多いはず。

f:id:kntty:20210630145614p:plain
Attention

6. Multi-headed Attention

ついでに、Multi-headの場合。

図では全てを示せていないが、Valueだけではなく、KeyとQueryも同じように分割した後、 Attentionの計算をして、得られる結果を単純に結合する。

f:id:kntty:20210630145156p:plain
Multi-head Attention

<s>Transformerを理解するために、早く知っておきたかったこと(メモ)</s>

2021.7.16 書き直しました。新しい記事の方を参照ください。