本文是使用tensorflow對神經網絡模型中激活函數,連接權重,層,指標,損失函數以及優化器,學習率,包括模型的訓練過程經行創建。
首先我們先定義一個基本的神經網絡模型,分析相應常見的參數以及訓練過程,然后對相關參數,函數(如)以及訓練過程經行自定義創建。
1、創建常見模型
model = keras.models.Sequential([
keras.layers.Dense(5, activation='selu', kernel_initializer='lecun_normal',
kernel_regularizer= keras.regularizers.l1_l2(.2), kernel_constraint= keras.constraints.MaxNorm(1.)),
keras.layers.Dropout(.3),
keras.layers.BatchNormalization(),
keras.layers.Dense(1)
])
model.compile(loss = keras.losses.mse, optimizer=keras.optimizers.SGD(lr=1e-3), metrics =['mae'])
history = model.fit(X_train_scaled, y_train , epochs=10, validation_data=(X_valid_scaled, y_valid))
2、自定義隱藏層
keras.layers.Dense(5, activation='selu', kernel_initializer='lecun_normal',
kernel_regularizer= keras.regularizers.l1_l2(.2), kernel_constraint= keras.constraints.MaxNorm(1.)),
從這句代碼中不難看出 在定義隱層時,首先我們應該將隱層中的相關函數定義了,如激活函數,權重初始化函數等。他們的定義如下:
2.1、自定義激活函數,權重初始化,正則化,約束
2.1.1 方法一 通過函數定義
def my_softplus(z):
return tf.math.log(tf.exp(z)+1.)
def my_glort_initializer(shape, dtype=tf.float32):
stddev = tf.sqrt(2./(shape[0]+shape[1]))
return tf.random.normal(shape, stddev, dtype= dtype)
def my_l1_regularizer(weight):
return tf.reduce_sum(tf.abs(0.01*weight))
def my_position_weight(weight):
return tf.where(weight<0, tf.zeros_like(weight), weight)
layer = keras.layers.Dense(1, activation=my_softplus,
kernel_initializer = my_glort_initializer,
kernel_regularizer = my_l1_regularizer,
kernel_constraint = my_position_weight)
我們需要注意的是,在使用自定義函數保存模型后將模型取出來時要加載自定義對象名稱映射到字典中。如下所示:
model.save('model_with_custom.h5')
model = keras.models.load_model('model_with_custom.h5',
custom_objects={'my_softplus':my_softplus,
'my_glort_initializer':my_glort_initializer,
'my_l1_regularizer':my_l1_regularizer,
'my_position_weight':my_position_weight})
2.1.2 通過繼承類
如果函數具有需要與模型一起保存的超參數,你需要繼承適當的類,例如keras.regularizers.Regularizer,keras.constraints.Constraint,keras.initializers.Initializer或keras.layers.Layer(適用于任何層,包括激活函數)。就像我們為自定義損失所做的一樣,這是一個用于1正則化的簡單類,它保存了其factor超參數(這一次我們不需要調用父類構造函數或get_config()方法,因為它們不是由父類定義的)。方法如下:
class MyL1Regularizer(keras.regularizers.Regularizer):
def __init__(self, factor):
self.factor = factor
def __call__(self, weights):
return tf.reduce_sum(tf.abs(self.factor * weights))
def get_config(self):
return {"factor": self.factor}
model = keras.models.Sequential([
keras.layers.Dense(30, activation="selu", kernel_initializer="lecun_normal",
input_shape=input_shape),
keras.layers.Dense(1, activation=my_softplus,
kernel_regularizer=MyL1Regularizer(0.01),
kernel_constraint=my_positive_weights,
kernel_initializer=my_glorot_initializer),
])
model.compile(loss="mse", optimizer="nadam", metrics=["mae"])
需要注意的是由于類中get_config()的方法將值包含了進去,所以在加載對象時不需要再填入值。
model.save("my_model_with_many_custom_parts.h5")
model = keras.models.load_model(
"my_model_with_many_custom_parts.h5",
custom_objects={
"MyL1Regularizer": MyL1Regularizer,
"my_positive_weights": my_positive_weights,
"my_glorot_initializer": my_glorot_initializer,
"my_softplus": my_softplus,
})
2.2 自定義層
方法一:函數定義
exponential_layer = keras.layers.Lambda(lambda x : tf.exp(x))
model = keras.models.Sequential([
keras.layers.Dense(30, activation='relu'),
keras.layers.Dense(1),
exponential_layer
])
model.compile(loss="mse", optimizer="sgd")
model.fit(X_train_scaled, y_train, epochs=5,
validation_data=(X_valid_scaled, y_valid))
model.evaluate(X_test_scaled, y_test)
方法二:繼承類
class MyDense(keras.layers.Layer):
def __init__(self, units, activation=None, **kwargs):
super().__init__(**kwargs)
self.units = units
self.activation = keras.activations.get(activation)
def build(self, batch_input_shape):
self.kernel = self.add_weight(name='kernel', shape=[batch_input_shape[-1], self.units],
initializer='glorot_normal'),
self.bias = self.add_weight(name='bias', shape=[self.units], initializer='zero')
super().build(batch_input_shape)
def call(self, X):
return self.activation(X @ self.kernel + self*bias)
def compute_output_shape(self, batch_input_shape):
return tf.TensorShape(batch_input_shape.as_list()[:-1] + [self.units])
def get_config(self):
base_config = super().get_config()
return {**base_config, 'units': self.units, 'activation':keras.activations.serialize(self.activation)}
modle = keras.models.Sequential([
MyDense(30, activation='relu'),
MyDense(1)
])
model.compile(loss="mse", optimizer="nadam")
model.fit(X_train_scaled, y_train, epochs=2,
validation_data=(X_valid_scaled, y_valid))
model.evaluate(X_test_scaled, y_test)
構造函數將所有超參數用作參數(在此示例中為units和activation),重要的是它還接受**kwargs參數。它調用父類構造函數,并將其傳遞給kwargs:這負責處理標準參數,例如input_shape、trainable和name。然后,它將超參數保存為屬性,使用keras.activations.get()函數將激活參數轉換為適當的激活函數(它接受函數、標準字符串(如"relu"或"selu",或None)?!uild()方法的作用是通過為每個權重調用add_weight()方法來創建層的變量。首次使用該層時,將調用build()方法。在這一點上,Keras知道該層的輸入形狀,并將其傳遞給build()方法,這對于創建某些權重通常是必需的。例如,我們需要知道上一層中神經元的數量,以便創建連接權重矩陣(即"Kernel"):這對應于輸入的最后維度的大小。在build()方法的最后(并且僅在最后),你必須調用父類的build()方法:這告訴Keras這一層被構建了(它只是設置了self.built=True)。call()方法執行所需的操作。在這種情況下,我們計算輸入X與層內核的矩陣乘積,添加偏置向量,并對結果應用激活函數,從而獲得層的輸出。
現在可以像其他任何層一樣使用此層,但是當然只能使用函數式和子類API,而不能使用順序API(僅接受具有一個輸入和一個輸出的層)。如果你的層在訓練期間和測試期間需要具有不同的行為(例如如果使用Dropout或BatchNormalization層),則必須將訓練參數添加到call()方法并使用此參數來決定要做什么。讓我們創建一個在訓練期間(用于正則化)添加高斯噪聲但在測試期間不執行任何操作的層(Keras具有相同功能的層:keras.layers.GaussianNoise):
class ResidualBlock(keras.layers.Layer):
def __init__(self, n_layers, n_neurous, **kwargs):
super().__init__(**kwargs)
self.hidden = [keras.layers.Dense(n_neurous, activation='elu' ,
kernel_initializer='he_normal') for _ in range(n_layers)]
def call(self, inputs):
Z = inputs
for layer in self.hidden:
Z = layer(Z)
return inputs + Z
3、 定義模型
model.compile(loss = keras.losses.mse, optimizer=keras.optimizers.SGD(lr=1e-3), metrics =['mae'])
從上述代碼中我們不難看出,我們應該首先將,損失函數,優化函數和指標函數定義了
3.1 損失函數的定義
方法一:函數定義
def create_huber(threshold = 1.):
def huber_fn(y_true, y_pred):
error = y_true - y_pred
is_small_erroe = tf.abs(error)<threshold
squared_loss = tf.square(error)/2
linear_loss = threshold* tf.abs(error) - threshold**2/2
return tf.where(is_small_erroe, squared_loss, linear_loss)
return huber_fn
model = keras.models.Sequential([
keras.layers.Dense(30, activation='selu', kernel_initializer='lecun_normal'),
keras.layers.Dense(10, activation='selu', kernel_initializer='lecun_normal'),
keras.layers.Dense(5, activation='selu', kernel_initializer='lecun_normal'),
keras.layers.Dense(1)
])
huber_fn = create_huber()
model.compile(loss=huber_fn, optimizer="nadam", metrics=["accuracy"])
model.fit(X_train_scaled, y_train, epochs=2,
validation_data=(X_valid_scaled, y_valid))
我們需要注意的是再保存后加載該模型時需要再字典中加載自定義。
model.save('my_model_with_custom.h5')
model_a = keras.models.load_model('my_model_with_custom.h5', custom_objects={'huber_fn':huber_fn(.2)})
model_a.fit(X_train_scaled, y_train, epochs=2,
validation_data=(X_valid_scaled, y_valid))
方法二:繼承類
class HuberLoss(keras.losses.Loss):
def __init__(self, threshold=1.0, **kwargs):
self.threshold = threshold
super().__init__(**kwargs)
def call(self, y_true, y_pred):
error = y_true - y_pred
threshold = self.threshold
is_small_erroe = tf.abs(error)<threshold
squared_loss = tf.square(error)/2
linear_loss = threshold* tf.abs(error) - threshold**2/2
return tf.where(is_small_erroe, squared_loss, linear_loss)
def get_config(self):
base_config = super().get_config()
return { **base_config, 'threshold':self.threshold}
model = keras.models.Sequential([
keras.layers.Dense(30, activation='selu', kernel_initializer = 'lecun_normal'),
keras.layers.Dense(1)
])
model.compile(loss=HuberLoss(2.), optimizer='nadam', metrics='mae')
需要注意的是由于使用繼承類中get_config()方法所以我們不需要再定義一次值,保存后讀取如下所示:
model.save('my_model_with_loss.h5')
model = keras.models.load_model('my_model_with_loss.h5', custom_objects={'HuberLoss':HuberLoss})
3.2 自定義指標
損失和指標在概念上不是一回事:損失(例如交叉熵)被梯度下降用來訓練模型,因此它們必須是可微的(至少是在求值的地方),并且梯度在任何地方都不應為0。另外,如果人類不容易解釋它們也沒有問題。相反,指標(例如準確率)用于評估模型,它們必須更容易被解釋,并且可以是不可微的或在各處具有0梯度。也就是說,在大多數情況下,定義一個自定義指標函數與定義一個自定義損失函數完全相同。實際上,我們甚至可以將之前創建的Huber損失函數用作指標。
如下所示
方法一:同上面定義的損失函數一樣
model.compile(loss='mse', optimizer="nadam", metrics=[create_huber(2.)])
方法二:繼承類
class HuberMetric(keras.metrics.Metric):
def __init__(self, threshold=1.0, **kwargs):
super().__init__(**kwargs)
self.threshold = threshold
self.huber_fn = create_huber(threshold)
self.total = self.add_weight('total', initializer='zeros')
self.count = self.add_weight('count', initializer='zeros')
def update_state(self, y_true, y_pred, sample_weight=None):
metrics = self.huber_fn(y_true, y_pred)
self.total.assign_add(tf.reduce_sum(metrics))
self.count.assign_add(tf.cast(tf.size(y_pred), tf.float32))
def result(self):
return self.total/self.count
def get_config(self):
base_config = super().get_config()
return {**base_config, 'threshold':self.threshold}
model = keras.models.Sequential([
keras.layers.Dense(30, activation='selu',kernel_initializer='lecun_normal',
input_shape=[8]),
keras.layers.Dense(1)
])
model.compile(loss='mse', optimizer="nadam", metrics=[HuberMetric(2.0)])
3.3 自定義模型
class ResidualRegressor(keras.models.Model):
def __init__(self, ouput, **kwargs):
super().__init__()
self.hidden1 = keras.layers.Dense(30, activation= 'elu',kernel_initializer='he_normal')
self.block1 = ResidualBlock(2,30)
self.block2 = ResidualBlock(2,30)
self.out = keras.layers.Dense(ouput)
def call(self, inputs):
print(type(inputs))
Z = self.hidden1(inputs)
print('z:',type(Z))
for _ in range(2):
Z = self.block1(Z)
Z = self.block2(Z)
return self.out(Z)
基于模型內部的損失和指標
def __init__(self, output_dim, **kwargs):
super().__init__(**kwargs)
self.hidden = [keras.layers.Dense(30, activation="selu",
kernel_initializer="lecun_normal")
for _ in range(5)]
self.out = keras.layers.Dense(output_dim)
self.reconstruct = keras.layers.Dense(8) # workaround for TF issue #46858
self.reconstruction_mean = keras.metrics.Mean(name="reconstruction_error")
def call(self, inputs, training=None):
Z = inputs
for layer in self.hidden:
Z = layer(Z)
reconstruction = self.reconstruct(Z)
recon_loss = tf.reduce_mean(tf.square(reconstruction - inputs))
self.add_loss(0.05 * recon_loss)
if training:
result = self.reconstruction_mean(recon_loss)
self.add_metric(result)
return self.out(Z)
構造函數創建具有5個密集隱藏層和一個密集輸出層的DNN。
build()方法創建一個額外的密集層,該層用于重建模型的輸入。必須在此處創建它,因為它的單元數必須等于輸入數,并且在調用build()方法之前,此數是未知的。
call()方法處理所有5個隱藏層的輸入,然后將結果傳遞到重建層,從而產生重構?!と缓骳all()方法計算重建損失(重建與輸入之間的均方差),并使用add_loss()方法將其添加到模型的損失列表中。
請注意,我們通過將其乘以0.05(這是你可以調整的超參數)按比例縮小了重建。這確保了重建損失不會在主要損失中占大部分。
最后,call()方法將隱藏層的輸出傳遞到輸出層并返回其輸出。
4、訓練循環
1、創建一個隨機抽取實例的函數,訓練時模型時按照小批量送入
def random_batch(X, y ,batch_size=32):
idx = np.random.randint(len(X),size = batch_size)
return X[idx], y[idx]
2、編寫進度條和損失值
def progress_bar(iteration, total, size=30):
running = iteration < total
c = ">" if running else "="
#縮放到當前迭代次數
p = (size - 1) * iteration // total
#fmt = "{{:-{}d}}/{{}} [{{}}]".format(len(str(total)))
fmt = '{}/{} [{}]'
params = [iteration, total, "=" * p + c + "." * (size - p - 1)]
return fmt.format(*params)
def print_status_bar(iteration, total, loss, metrics=None):
metrics = '-'.join(['{}: {:.4f}'.format(m.name, m.result()) for m in [loss] + (metrics or [])])
end = '' if iteration <total else '\n'
print('\r {}/{} -'.format(progress_bar(iteration, total) ,metrics), end=end)
3、設置基本參數
n_epochs = 5
batch_size = 32
n_steps = len(X_train) // batch_size
optimizer = keras.optimizers.Nadam(lr=0.01)
loss_fn = keras.losses.mean_squared_error
mean_loss = keras.metrics.Mean()
metrics = [keras.metrics.MeanAbsoluteError()]
4、
for epoch in range(1, n_epochs + 1):
print("Epoch {}/{}".format(epoch, n_epochs))
for step in range(1, n_steps + 1):
X_batch, y_batch = random_batch(X_train_scaled, y_train)
with tf.GradientTape() as tape:
y_pred = model(X_batch)
main_loss = tf.reduce_mean(loss_fn(y_batch, y_pred))
loss = tf.add_n([main_loss] + model.losses)
gradients = tape.gradient(loss, model.trainable_variables)
optimizer.apply_gradients(zip(gradients, model.trainable_variables))
for variable in model.variables:
if variable.constraint is not None:
variable.assign(variable.constraint(variable))
mean_loss(loss)
for metric in metrics:
metric(y_batch, y_pred)
print_status_bar(step * batch_size, len(y_train), mean_loss, metrics)
print_status_bar(len(y_train), len(y_train), mean_loss, metrics)
for metric in [mean_loss] + metrics:
metric.reset_states()
我們創建了兩個嵌套循環:一個用于輪次,另一個用于輪次內的批處理。·然后從訓練集中抽取一個隨機批次。
在tf.GradientTape()塊中,我們對一個批次進行了預測(使用模型作為函數),并計算了損失:它等于主損失加其他損失(在此模型中,每層都有一個正則化損失)。由于mean_squared_error()函數每個實例返回一個損失,因此我們使用tf.reduce_mean()計算批次中的均值(如果要對每個實例應用不同的權重,則可以在這里進行操作)。
正則化損失已經歸約到單個標量,因此我們只需要對它們進行求和(使用tf.add_n()即可對具有相同形狀和數據類型的多個張量求和)。
接下來,我們要求tape針對每個可訓練變量(不是所有變量?。┯嬎銚p失的梯度,然后用優化器來執行“梯度下降”步驟。
然后,我們更新平均損失和指標(在當前輪次內),并顯示狀態欄。
在每個輪次結束時,我們再次顯示狀態欄以使其看起來完整并打印換行符,然后重置平均損失和指標的狀態。