Skip to main content

Keras Denseレイヤー:正しく使用する方法

この記事では、KerasのDenseレイヤーを見ていきます。これを十分に理解することは、Kerasでカスタムモデルを構築するうえで不可欠です。
Created on March 23|Last edited on May 30
KerasDenseレイヤー は、古き良き、完全に/密に結合されたニューラルネットワークです。それ以上言うことはありません。ただし、これを十分理解することは、Kerasでカスタムモデルを構築する時に役立ちます。
この記事では、必要なすべての知識を得られるように、Denseレイヤーとは何か、および実際にどのように機能するか探っていきます。
以下は、この記事で扱う内容です:

目次



Denseレイヤーとは?

機械学習では、全結合層は、すべての入力特徴をそのレイヤー内のすべてのニューロンに接続します。Denseレイヤーは大抵、特徴抽出ブロック(畳み込み、エンコーダーまたはデコーダーなど)、出力レイヤー(最後のレイヤー)後の、最後から2番目のレイヤーとして使用され、次元 d0のベクトルを新しい次元 d1に投影します。
1D入力特徴について考えてみましょう。
この入力は、4つのニューロンを含む全結合層を使って処理されます(分かりやすくするために、直線性とバイアスは無視します)。得られる接続の数は3 * 4 = 12です。すなわち、以下の図に示すように、1D特徴空間内のすべての値に4つの重みベクトル(色付きの線で表される)を乗じます。

図1:全結合のニューラルネットワーク。

コード例

Kerasを使って、全結合のニューラルネットワークを実装します(図1)。以下に示すように、 Denseレイヤーを使ってこれを達成できます:
import tensorflow as tf
from tensorflow.keras import layers

# batch_sizeが1の、ランダム入力特徴形状を作りましょう。
inputs = tf.random.uniform(shape=(1,3)) # (batch_size, num_features)

# 全結合層を初期化(分かりやすくするため、バイアスは使いません)。
dense_layer = layers.Dense(unit=4, use_bias=False)

# 出力として何を予測できるでしょうか?
print(dense_layer(inputs))
>>> tf.Tensor([[-0.07313907 0.31886736 -0.83136106 0.47411117]], shape=(1, 4), dtype=float32)
形状の入力 (1、3)には、形状の出力 (1、4)が得られます。これは、4つのニューロンが全結合層にあるためです。図1では、重みを表す12本の線が描かれています。全結合層の初期化された重みを調べてみましょう。
print(dense_layer.weights)
>>> [<tf.Variable 'dense_1/kernel:0' shape=(3, 4) dtype=float32, numpy=
array([[-0.18026423, 0.8457761 , 0.20618927, 0.34542954],
[-0.68638337, -0.09881872, -0.891773 , 0.7983763 ],
[ 0.84850013, -0.5968083 , -0.522443 , -0.62690055]],
dtype=float32)>]
予想どおり、これは形状 (3、4)(3*4 = 12)の行列です。

N次元入力についてはどうでしょうか?

形状の入力 (time/num_frame/arbitrary_feature、features)があると仮定しましょう。この入力はシーケンス(時系列または動画)または任意の特徴空間になる場合があります。ユースケースに応じて、全結合層を通して、複数の方法でこの入力を渡す(または処理する)ことができます。ここでいくつかの点について考えてみましょう。

1.これは任意の特徴空間である

入力は、形状の任意の特徴空間 (arbitrary_feature、features)になる場合があります。入力のフラット化を検討し、実験することができます。Kerasでは、入力のフラット化とはどのようなものでしょうか?確かめてみましょう。

1.1 入力のフラット化

Kerasでは、 Flatten()レイヤーを使って、入力を1Dベクトルにフラット化することができます。このレイヤーはバッチサイズに従ってフラット化を行いません。すなわち、入力に (32、2、3)(ここで32はバッチサイズ)の形状がある場合です。フラット化オペレーションでは、形状 (32、6)のベクトルが得られます。
# 形状(1、2、3)(ここで、1はバッチサイズ)の一定入力を初期化しましょう。
inputs = tf.constant(
[[[1, 1, 1], [2, 2, 2]]]
)
# フラット化レイヤーの初期化
flatten = layers.Flatten()
# 出力として何を予測できるでしょうか?
outputs = flatten(inputs)
print(outputs)
>>> tf.Tensor([[1 1 1 2 2 2]], shape=(1, 6), dtype=int32)
形状 (1、6)(ここで、1はバッチサイズ)のフラット化したベクトルが得られます。このレイヤーは通常、ディープニューラルネットワーク内の特徴抽出ブロックの後に使用されます。これは、 arbitrary_featureサイズが独立している場合の使用にも有効です。すなわち、特徴がシーケンスの一部ではないとみなすことができます。Denseレイヤーを通してこれを渡してみましょう。
dense_layer = layers.Dense(
units=4, # この全結合層に4つのニューロンがあります。
use_bias=False,
kernel_initializer=tf.keras.initializers.Constant(value=0.5) # 0.5の一定値で重みが初期化されます。
)

# 出力として何を予測できるでしょうか?
print(dense_layer(outputs))
>>> tf.Tensor([[4.5 4.5 4.5 4.5]], shape=(1, 4), dtype=float32)
この値はどのように得られたのでしょうか?全結合層の後に、各出力値(4、5)が、 1*0.5 + 1*0.5 + 1*0.5 + 2*0.5 + 2*0.5 + 2*0.5 = 4.5のように算出されます。

2.これはシーケンスである

形状の入力(時間、特徴)について考えてみましょう。ここで、各特徴は時間軸に沿って依存しており、フラット化されたベクトルはこの依存状態を失います。特徴の次元を新しい次元に投影する必要があるシナリオについて考えてみましょう。Keras Dense()レイヤーを使ってこれを実行できるでしょうか?
ドキュメントを見てみましょう:
レイヤーへの入力に2より大きいランクがある場合、Denseは、入力の最後の軸とカーネルの軸0に沿って、入力とカーネル間のドット積を算出します。たとえば、入力の次元が( batch_size、d0、d1)である場合、形状( d1、ユニット)でカーネルを作成し、形状( 1、1、d1)のすべてのサブテンソル上で( batch_size * d0のサブテンソルがある)、カーネルは入力の軸2に沿って動作します。このケースの出力には形状 (batch_size、d0、ユニット)があります。
これを分析して、各可動部をコードとともに理解しましょう。
# 形状(1、2、3)(ここで、1はバッチサイズ、2は時間軸)の入力。
inputs = tf.constant(
[[[1, 1, 1], [2, 2, 2]]]
)

# 入力のランクは2より大きいですか?
print(tf.rank(inputs))
>>> <tf.Tensor: shape=(), dtype=int32, numpy=3>
Denseレイヤーを使って、最後の次元を3から4に投影しましょう。
# 4つの出力ニューロンと一定重量0.5で、Denseレイヤーを初期化します。
dense_layer = layers.Dense(
units=4,
use_bias=False,
kernel_initializer=tf.keras.initializers.Constant(value=0.5)
)

# どのような出力が予測されるでしょうか?
print(dense_layer(inputs))
>>> tf.Tensor(
[[[1.5 1.5 1.5 1.5]
[3. 3. 3. 3. ]]], shape=(1, 2, 4), dtype=float32)
ご覧の通り、 Denseレイヤーは、形状 (1、2、3)の入力を (1、2、4)に投影しました。この計算から出力値1.5が得られました: 1*0.5 + 1*0.5 + 1*0.5 = 1.5。同様に、この計算から出力値3.0が得られました: 2*0.5 + 2*0.5 + 2*0.5 = 3。ドキュメントのとおり、重み行列の形状は (3、4)になるはずです。
print(dense_layer.weights)
>>> [<tf.Variable 'dense_5/kernel:0' shape=(3, 4) dtype=float32, numpy=
array([[0.5, 0.5, 0.5, 0.5],
[0.5, 0.5, 0.5, 0.5],
[0.5, 0.5, 0.5, 0.5]], dtype=float32)>]

2.1 TimeDistributedレイヤー

(time, features)入力をさまざまな次元に投影する観点から見てきましたが、 Denseオペレーション(ドット積)を各特徴に連続的に適用することについてはどうでしょうか?
この議論に特別な意味合いを持たせるために、形状 (num_frames, height, width, 3)の動画データサンプルを想像してみましょう。学習済み画像モデルを使って、各フレームから連続的に情報を抽出したいとします。 TimeDistributedレイヤーによって、レイヤー(ここでは特徴抽出)を、入力(ここでは動画)のすべての一時的なスライス(ここではフレーム)に適用することができます。
Denseオペレーションを、形状(time, features)の入力にどのように適用できるか見てみましょう。前の例で初期化された inputsを使用します。
# このdenseレイヤーは各「時間」に適用されます。
dense_layer = layers.Dense(
units=4,
use_bias=False,
kernel_initializer=tf.keras.initializers.Constant(value=0.5)
)
# 「TimeDistributed」レイヤーを初期化します。
timedistributed = layers.TimeDistributed(dense_layer)
# どのような出力が予測されるでしょうか?
print(timedistributed(inputs))
>>> tf.Tensor(
[[[1.5 1.5 1.5 1.5]
[3. 3. 3. 3. ]]], shape=(1, 2, 4), dtype=float32)
出力は、前のセクションの出力と異なりますか?このシナリオでは、これによって、 TimeDistributed(Dense(...))Dense(...)はお互いに等しくなります。

事例

素晴らしいですね!これで、Keras Dense レイヤーの正しい使用方法を理解できました。このレイヤーを使って実験し、より深く理解しましょう。物事をシンプルに保ち、Scikit Learnの make_classificationを使って作成された合成データセットを使用します。このデータセットには、特徴とクラスがそれぞれ10個あります。特徴は正規化せずに、ニューラルネットワークがサポートなしで特徴をどの程度適切に抽出できるか確認します。
データセットは一定して10,000個のサンプルのままです。100エポック、バッチサイズ256でモデルをトレーニングします。


Colabノートブックをチェック \rightarrow

全結合ニューラルネットワークの外観は以下のようになります:
def MLPModel():
inputs = layers.Input(shape=(10,))
x = layers.Dense(configs["hidden_units"], activation="relu")(inputs)
outputs = layers.Dense(10, activation="softmax")(x)

return models.Model(inputs, outputs)

パラメーターの効果 - units

Denseレイヤー内の最初の引数は、そのレイヤー内のニューロン数を制御できる unitsです。ユニット数は10、100または1000になります。以下のパネルは、さまざまなユニットを使用した結果です。
明らかに、パラメーター(units)の数を増やすと、モデルが改善されます。

Run set
3

トレーニングと検証損失を見てみると、1000ユニットのモデルはすぐに過学習し始める一方、わずか10ニューロンのモデルはダイバージェンスさえも開始していません。

Run set
3


カーネル正規化の効果

モデルの容量を減らさずに、この過学習を無効にできるでしょうか?カーネル正規化に入ると、最適化中にレイヤーパラメーターペナルティを適用することができます。詳細の議論や、実際の効果の確認は行いません。
正規化せずに1000個の隠れユニットでトレーニングされたモデルを、同じ構成を持ち、 kernel_regularizationでトレーニングされたモデルと比較します。 l2引数を取るL2正規化ペナルティを使用します。これは[0-1]範囲内の浮動小数点値です。0に近い値は「正規化なし」を意味します。
前のセクションで、正規化なしでトレーニングされたモデルはすぐに過学習を始めました。さまざまな l2値と、正規化の全般的な効果を見てみましょう。

Run set
4

no-reg runの lossval_lossの間にダイバージェンスがあります。ここでは過学習を示すダイバージェンスが最大である一方、 reg-0.1 runのダイバージェンスは最小です。正規化は、モデルの最終精度にどのような影響を与えるのでしょうか?

Run set
4

0.1の l2値を持つL2正規化は、過学習の無効化には役立ちましたが、モデルが同じレベルの val_accを達成するのには役立ちませんでした。しかし結果として、正規化されたモデルをより長時間トレーニングすることができます。実際、私たちは通常、1e-3の l2値で正規化を行います。

結論

この簡単なレポートでは、Kerasを使って全結合層の初期化と作成を行う方法が説明されています。使用方法をより良く理解するための比較研究も含まれています。
Denseレイヤーはディープラーニングにおける基盤層であり、通常は注意層、MLPブロック、プロジェクターなどで使用されます。密接続によってパラメーター数が多くなり、画像などの高次元入力の直接処理には、これは理想的ではなくなります。
より複雑なユースケースに取り組んでいる場合は、代わりにEinsumDenseレイヤーの使用を検討してください。これはeinsum式を使用し、 DenseEinsumDenseの特殊なケースです。これについては、まもなく別のレポートで扱います。
Iterate on AI agents and models faster. Try Weights & Biases today.