Keras
源碼分析
此文檔中续室,凡代碼里用pass
栋烤,均系省略源碼以便閱讀,起“本枝百世”之用猎贴。此注明者班缎,乃pass
非源碼所有蝴光,勿叫讀者疑心不解也。
[TOC]
Keras
概覽
我們從一個(gè)簡(jiǎn)單的全連接分類器來看Keras
的設(shè)計(jì)原則和閱讀源代碼达址。在Keras
的官網(wǎng)上有這樣一個(gè)簡(jiǎn)單全連接網(wǎng)絡(luò)的示例The Sequential model API:
import keras
from keras.models import Sequential
from keras.layers import Dense, Activation
from keras import backend as K
model = Sequential()
model.add(Dense(32, input_shape=(500,)))
model.add(Dense(10, activation='softmax'))
model.compile(optimizer='rmsprop',
loss='categorical_crossentropy',
metrics=['accuracy'])
model.fit(train['data'], train['label'], batch_size=32,
nb_epoch=10, verbose=1)
其中蔑祟,Sequential
模型的代碼在keras/models.py
中。
后端backend的代碼在keras/backend
里沉唠。
網(wǎng)絡(luò)的核心概念——層(Layer
)的核心源碼則在keras/engine/topology.py
中疆虚,Dense
網(wǎng)絡(luò)只是Layer
類的一個(gè)繼承,其他其他所有的層都是這樣的一種繼承满葛,所以developer可以通過繼承Layer
類來實(shí)現(xiàn)自己需要的層径簿。
從整體上看,Keras
源碼的組織和功能是這樣的:
.
│ activations.py
│ callbacks.py
│ constraints.py
│ initializations.py
│ metrics.py
│ models.py
│ objectives.py
│ optimizers.py
│ regularizers.py
│ __init__.py
│
├─applications
| # 一些典型的應(yīng)用
│ ...
│
├─backend
| # Theano, Tensorflow 后端
| # tensorflow_backend.py 和 theano_backend.py 有一些同名的函數(shù)
| # 這樣 import backend as K 以后應(yīng)用時(shí)嘀韧,就不需要考慮 Tensorflow 和 Theano 的具體差別了
│ common.py
│ tensorflow_backend.py
│ theano_backend.py
│ __init__.py
│
├─datasets
| # 下載數(shù)據(jù)集的腳本
│ ...
|
├─engine
│ topology.py # Keras Layer, Input, Merge, Container的基礎(chǔ)
│ training.py
│ __init__.py
│
├─layers
| # 相當(dāng)于 engine 的應(yīng)用
| # 通過繼承 engine/topology.py 的 Layer 來實(shí)現(xiàn)不同的層
│ convolutional.py
│ __init__.py
│ ...
│
├─preprocessing
│ image.py
│ sequence.py
│ text.py
│ __init__.py
│
├─utils
│ data_utils.py
│ generic_utils.py
│ io_utils.py
│ layer_utils.py
│ np_utils.py
│ test_utils.py
│ visualize_util.py
│ __init__.py
│
└─wrappers
scikit_learn.py
__init__.py
backend 設(shè)計(jì)
首先要說一下后端設(shè)計(jì)篇亭。Keras
最初后端只有Theano
,現(xiàn)在可以支持Tensorflow
锄贷。
Keras
之所以易于擴(kuò)展backend译蒂,是因?yàn)?em>后端采用的函數(shù)名都一樣。這等于說是在Tensorflow
和Theano
基礎(chǔ)上又向上封裝了一層谊却。
在backend/theano_backend.py
和tensorflow_backend.py
兩個(gè)文件中柔昼,封裝到backend中的函數(shù)有:Backend functions
這些同名函數(shù)的功能基本上如字面意思所述,例如:
def maximum(x, y):
return tf.maximum(x, y)
def maximum(x, y):
return T.maximum(x, y)
Tensorflow
和Theano
的差別基本就如上例所示炎辨。由于存在大量同名的函數(shù)捕透,所以在調(diào)用后端時(shí)只需要:
import backend as K
K.function_name(args)
我們可以通過這些重名的函數(shù)看到,哪些構(gòu)件對(duì)于深度學(xué)習(xí)而言是基本的碴萧、必需的乙嘀,這對(duì)于芯片設(shè)計(jì)會(huì)有一定的啟發(fā)。
class Layer(object)
設(shè)計(jì)
class Sequential(Model)
繼承了keras/engine/training.py
中的Model
類勿决,而Model
類則繼承了同目錄下的keras/engine/topology.py
中的Container
類乒躺,Container
類繼承了同文件中的Layer
類。
也就是說低缩,Sequential
模型實(shí)際上是泛型模型(functional API)的一個(gè)特殊情況嘉冒。而泛型模型又是容器(Container
)、是層(Layer
)的特殊情況咆繁,因此有必要先搞清楚Layer
的設(shè)計(jì)原理讳推。
class Node(object)
Layer
類和Node
類很有關(guān)系。兩個(gè)Layer
之間用Node
連接玩般。Layer
有inbound_nodes
和outbound_nodes
兩種list
银觅,他們的元素都是Node
,用來綁定輸入與輸出的張量坏为。
每當(dāng)一個(gè)Layer
接收新的輸入張量時(shí)究驴,就在layer.inbound_nodes
中增加一個(gè)Node
镊绪。同理,當(dāng)一個(gè)輸出張量被另一層Layer
調(diào)用時(shí)洒忧,在layer.outbound_nodes
增加新的節(jié)點(diǎn)蝴韭。Node
的作用類似于函數(shù)之間的參數(shù)傳遞。
class Node(object):
def __init__(self, outbound_layer,
inbound_layers, node_indices, tensor_indices,
input_tensors, output_tensors,
input_masks, output_masks,
input_shapes, output_shapes):
'''
構(gòu)造函數(shù)
outbound_layer
此 Node 綁定的輸出 Layer 熙侍,也就是說當(dāng)前 Node 在 outbound_layer 的 inbound_nodes 中榄鉴;
inbound_layers
輸入 Layer,當(dāng)前 Node 作為其 outbound_nodes 的元素
下面的循環(huán)將 Node 加入到所有要綁定的輸入 Layer 中蛉抓。
同時(shí)庆尘,也綁定了要輸出的 Layer 的 Node。
'''
for layer in inbound_layers:
if layer is not None:
layer.outbound_nodes.append(self)
outbound_layer.inbound_nodes.append(self)
@classmethod # 指定函數(shù) create_node 為類方法而非實(shí)例方法巷送,因此可以直接進(jìn)行類調(diào)用 Node.create_node()
def create_node(cls, outbound_layer,
inbound_layers, node_indices=None, tensor_indices=None):
'''
inbound_layers
從 inbound_layers.inbound_nodes 中讀取所有的輸入 Node 信息驶忌,包括數(shù)據(jù)、mask笑跛、shape位岔。
outbound_layer
根據(jù)從 inbound_layers 讀到的足夠多的信息來確定新建一個(gè) Node 傳遞給 outbound_layers 。
函數(shù)返還一個(gè)outbound_layers 的 Node 類節(jié)點(diǎn)堡牡。
'''
return cls(outbound_layer,
inbound_layers, node_indices, tensor_indices,
input_tensors, output_tensors,
input_masks, output_masks,
input_shapes, output_shapes)
def get_config(self):
'''
返還輸入輸出層的名字、節(jié)點(diǎn)與張量的索引
'''
return {'outbound_layer': self.outbound_layer.name if self.outbound_layer else None,
'inbound_layers': inbound_names,
'node_indices': self.node_indices,
'tensor_indices': self.tensor_indices}
Node
和Layer
互為成員變量杨刨,所以在Layer
創(chuàng)建的時(shí)候就已經(jīng)創(chuàng)建了晤柄,不需要單獨(dú)創(chuàng)建。
class Layer(object)
至于Layer
類妖胀,它主要包括這些成員變量(properties)芥颈、實(shí)例方法(methods)和類方法(class methods)
主要的 Properties
-
input_spec
class InputSpec
的list
。每一個(gè)元素描述了對(duì)于輸入的要求赚抡,例如維度ndim
和數(shù)據(jù)類型dtype
爬坑。 -
trainable
用來標(biāo)志這個(gè)
Layer
在訓(xùn)練中權(quán)重是否更新(訓(xùn)練)的bool
值 input_shape, output_shape
-
inbound_nodes
,outbound_nodes
Layer
之間存放的Node
的list
-
input, output
輸入輸出的張量(
tensor
) trainable_weights, non_trainable_weights, weights
可以訓(xùn)練、不可以訓(xùn)練的變量list
涂臣,weights
是他們的串接盾计。它們是以函數(shù)形式存在的property,返回list
赁遗。
關(guān)鍵的 Methods
-
I/O
相關(guān)的 Methods-
def create_input_layer(self, batch_input_shape, input_dtype=None, name=None)
這個(gè)函數(shù)會(huì)按照輸入?yún)?shù)修改當(dāng)前
Layer
的batch_input_shape
和input_dtype
署辉,并且調(diào)用函數(shù)def Input(shape=None, batch_shape=None, name=None, dtype=K.floatx(), sparse=False, tensor=None)
得到一個(gè)Keras tensor
,x
岩四。x = Input(batch_shape=batch_input_shape, dtype=input_dtype, name=name) self(x)
Keras tensor
是在backend的 tensor 基礎(chǔ)之上增加內(nèi)容的張量哭尝。用返還的Keras tensor
將自身實(shí)例化為Layer
,這是為了創(chuàng)造當(dāng)前Layer
與剛剛創(chuàng)造的輸入Layer
之間的連接Node
剖煌。
Keras tensor
實(shí)際上是InputLayer
輸入Node
的輸出張量:input_layer = InputLayer(batch_input_shape=batch_shape, name=name, input_dtype=dtype, sparse=sparse, input_tensor=tensor) outputs = input_layer.inbound_nodes[0].output_tensors if len(outputs) == 1: return outputs[0] else: return outputs
因?yàn)樵谧宇?code>InputLayer中并沒有調(diào)用該函數(shù)材鹦,所以沒有矛盾的地方逝淹。
-
-
與
Losses
,Update
有關(guān)的 Methods-
def add_loss(self, losses, inputs=None)
這個(gè)函數(shù)會(huì)不斷添加
self.losses
列表,參數(shù)losses
會(huì)被轉(zhuǎn)化為list
然后被加到self.losses
后面桶唐。
然后根據(jù)參數(shù)inputs
栅葡,獲得它的用戶級(jí)編號(hào)uid
作為hash
值。uid
是根據(jù)python
的id()
函數(shù)得到的莽红,某種意義上類似于C
的內(nèi)存地址妥畏。inputs_hash = object_list_uid(inputs)
然后將
losses
列表加入對(duì)應(yīng)的hash
值位置:self._per_input_losses[inputs_hash] += losses
-
def get_losses_for(self, inputs)
將
add_loss
函數(shù)設(shè)定的inputs
位置的losses
取出來 -
update
類的函數(shù)基本上和
losses
都一樣,只是將關(guān)鍵字losses
改成updates
安吁。
-
-
Weight
相關(guān)的 Methods-
def weights(self)
串接可訓(xùn)練與不可訓(xùn)練的權(quán)重:
return self.trainable_weights + self.non_trainable_weights
-
def set_weights(self, weights)
將
self.weights
和參數(shù)weights
的張量載入到[numpy.array]
形式的weight_value_tuples
-
def get_weights(self)
以
[numpy.array]
的形式返回當(dāng)前Layer
的張量
-
以 Dense
層為例
Dense
層是Keras
中最簡(jiǎn)單的一個(gè)全連接的網(wǎng)絡(luò)醉蚁。整個(gè)Dense
層的代碼大致如下:
class Dense(Layer):
def __init__(self, output_dim, init='glorot_uniform',
activation=None, weights=None,
W_regularizer=None, b_regularizer=None, activity_regularizer=None,
W_constraint=None, b_constraint=None,
bias=True, input_dim=None, **kwargs):
pass
super(Dense, self).__init__(**kwargs)
def build(self, input_shape):
pass
def call(self, x, mask=None):
output = K.dot(x, self.W)
if self.bias:
output += self.b
return self.activation(output)
def get_output_shape_for(self, input_shape):
pass
def get_config(self):
pass
首先,Dense
是對(duì)父類Layer
的繼承鬼店,但是覆蓋了
-
build
定義權(quán)重网棍。可以訓(xùn)練的加入self.trainable_weights
妇智,不可以訓(xùn)練的加入self.non_trainabe_weights
滥玷,需要更新的以(old_tensor, new_tensor)
的形式加入self.updates
。 -
call
定義功能巍棱,具體的數(shù)學(xué)運(yùn)算惑畴。 -
get_output_shape_for
給Keras
指明shape的變化。 -
get_config
給出Layer
的確認(rèn)信息航徙,包括output_dim
,W_constraint
等如贷。
這四個(gè)函數(shù),這四個(gè)函數(shù)具有“多態(tài)”的特點(diǎn)到踏。
在Dense
實(shí)例化時(shí)杠袱,在構(gòu)造函數(shù)__init__
的結(jié)尾調(diào)用父類Layer
的構(gòu)造函數(shù),這時(shí)候Layer
調(diào)用的多態(tài)函數(shù)就被子類覆蓋了窝稿,實(shí)現(xiàn)了子類的特有功能楣富。
在官方手冊(cè)Writing your own Keras layers中,并不需要用戶實(shí)現(xiàn)get_config
伴榔,只需要自己編寫另外三個(gè)多態(tài)函數(shù)即可纹蝴。
數(shù)學(xué)運(yùn)算
可見Layer
的計(jì)算功能集中在call
函數(shù)。
Dense
的call
如上面的代碼所示潮梯,它實(shí)際上還是按照輸入的activation
關(guān)鍵字調(diào)用了keras/activations.py
中的激活函數(shù)骗灶。
keras/activations.py
提供了如下這些激活類型,
- def softmax(x)
- def elu(x, alpha=1.0)
- def softplus(x)
- def softsign(x)
- def relu(x, alpha=0., max_value=None)
- def tanh(x)
- def sigmoid(x)
- def hard_sigmoid(x)
- def linear(x)
在不指定activation
參數(shù)的情況下秉馏,參數(shù)傳遞為None
耙旦,默認(rèn)調(diào)用linear
。
由此可見,Layer
只具有前向傳播的計(jì)算能力免都,不具備反向傳播的計(jì)算能力锉罐。
class Container(Layer)
設(shè)計(jì)
Container
是由Layer
組成的有向無環(huán)的計(jì)算圖(a directed acyclic graph of layers),實(shí)際上是一個(gè)Model
的拓?fù)浣Y(jié)構(gòu)绕娘。Container
和Model
之間的差別在于訓(xùn)練脓规,所以在構(gòu)造時(shí),Model
是對(duì)Container
的繼承险领。
__init__()
函數(shù)
構(gòu)造函數(shù)__init__()
通過一種“自頂向下”的方法構(gòu)造了計(jì)算圖模型侨舆。在__init__()
中,首先會(huì)將參數(shù)輸入的input
, output
這兩張層的張量處理好:
def __init__(self, input, output, name=None):
# 先將輸入的張量`input`, `output`
# 處理成`Container`專用的`tensor list`
# Container-specific properties.
if isinstance(input, (list, tuple)):
self.inputs = list(input) # Tensor or list of tensors.
else:
self.inputs = [input]
if isinstance(output, (list, tuple)):
self.outputs = list(output)
else:
self.outputs = [output]
# Build self.output_layers:
for x in self.outputs:
layer, node_index, tensor_index = x._keras_history
# 添加輸出*層*
self.output_layers.append(layer)
# 添加輸出層的*結(jié)點(diǎn)*
self.output_layers_node_indices.append(node_index)
# 添加*張量*
self.output_layers_tensor_indices.append(tensor_index)
# Build self.input_layers:
for x in self.inputs:
layer, node_index, tensor_index = x._keras_history
# It's supposed to be an input layer, so only one node
# and one tensor output.
assert node_index == 0
assert tensor_index == 0
self.input_layers.append(layer)
self.input_layers_node_indices.append(node_index)
self.input_layers_tensor_indices.append(tensor_index)
# `output_layers`, `input_layers` 的 cache 處理
Graph 構(gòu)建
接下來會(huì)通過下面這個(gè)__init__
內(nèi)部定義的函數(shù)來遞歸地構(gòu)造計(jì)算圖
def build_map_of_graph(tensor, seen_nodes=set(), depth=0,
layer=None, node_index=None, tensor_index=None):
構(gòu)造出來的計(jì)算圖大致是這樣的:
output layer
|
#------------#------------#
| | |
node ... node ... node
|
#------------#------------#
| | |
layer ... layer ... layer
|
#------------#------------#
| | |
node ... node ... node
... ... ...
#
|
input layer
“自頂向下”的構(gòu)造從output
layer 開始绢陌,逐個(gè)檢查它的inbound_nodes
列表中的結(jié)點(diǎn)挨下,將結(jié)點(diǎn)加入“圖可見結(jié)點(diǎn)”(seen_nodes
)中。因?yàn)?code>output layer 到 input
layer 之間并不是所有結(jié)點(diǎn)都有用的脐湾,只有在seen_nodes
中的才是計(jì)算圖模型所需要的臭笆。
最后再遍歷當(dāng)前結(jié)點(diǎn)的inbound_layers
。對(duì)于每一個(gè)layer
秤掌,需要繼續(xù)向下添加seen_nodes
愁铺。這樣,就遞歸地構(gòu)建了計(jì)算圖闻鉴。
同時(shí)茵乱,對(duì)于一個(gè)新構(gòu)造的Container
而言,作為不帶有訓(xùn)練功能的拓?fù)浣Y(jié)構(gòu)孟岛,它的inbound node
只有一個(gè)似将,并且沒有outbound node
。
Depth 環(huán)路避免
為了在有向圖中防止出現(xiàn)環(huán)蚀苛,所以采用depth
(深度)對(duì)Node
和Layer
進(jìn)行描述。按照depth
的順序玷氏,獲得經(jīng)過排序的self.layers_by_depth
和self.nodes_by_depth
堵未。
在有向圖中利用depth
來避免出現(xiàn)環(huán)是很容易理解的,因?yàn)槿绻霈F(xiàn)有向環(huán)的話盏触,那么Node
和Layer
的深度就會(huì)不斷增加以至于無窮大渗蟹,也就是所謂的“無窮計(jì)數(shù)問題”。
有關(guān)訓(xùn)練的 Property Methods
Update
在方法def updates(self)
中赞辩,確定某一個(gè)Layer
是否需要更新的方法就是檢查'updates'
屬性:
@property
def updates(self):
updates = []
for layer in self.layers:
if hasattr(layer, 'updates'):
# 根據(jù)`layer.inbound_nodes`進(jìn)行更新
pass
return updates
Loss
損失函數(shù)也一樣雌芽,通過檢查'losses'
屬性和檢查layer.inbound_nodes
來完成。
Weights 相關(guān)的 Methods
@property
def trainable_weights(self):
pass
@property
def non_trainable_weights(self):
pass
這兩個(gè)函數(shù)中的權(quán)重選取就根據(jù)Layer.trainable
這個(gè)屬性來進(jìn)行選擇辨嗽。在model.summary()
中世落,可以查看可訓(xùn)練與不可訓(xùn)練的參數(shù)數(shù)量,就是通過這兩個(gè)函數(shù)實(shí)現(xiàn)的糟需。
前向傳播計(jì)算
output
計(jì)算
在這里最重要的函數(shù)是
def run_internal_graph(self, inputs, masks=None):
# Computes output tensors for new inputs.
pass
因?yàn)橹霸诮▓D的時(shí)候有記錄depth
信息屉佳,所以數(shù)據(jù)的流動(dòng)谷朝、計(jì)算可以逐層進(jìn)行:
depth_keys = list(self.nodes_by_depth.keys())
depth_keys.sort(reverse=True)
for depth in depth_keys:
nodes = self.nodes_by_depth[depth]
for node in nodes:
# This is always a single layer, never a list.
pass
因?yàn)槭怯邢驘o環(huán)圖,而且圖又自有深度學(xué)習(xí)任務(wù)的特點(diǎn)武花,所以必然在每一層都只有一個(gè)Layer
圆凰。這是上面這段大循環(huán)的基礎(chǔ),這段循環(huán)將從深depth
到淺depth
遍歷層体箕,也就是從input layer
到output layer
遍歷专钉。
對(duì)于每一個(gè)層而言,前向傳播計(jì)算就是將從input
層來的累铅、已經(jīng)算過的張量拿過來計(jì)算跃须,并且傳給下一層。具體的計(jì)算就要通過Layer
的call
方法實(shí)現(xiàn):
output_tensors = to_list(layer.call(computed_tensors,
computed_masks))
output_masks = to_list(layer.compute_mask(computed_tensors,
computed_masks))
每完成一張層的前向計(jì)算争群,要添加一下update
回怜,loss
以及緩沖池的記錄,并且更新_keras_shape
换薄。這樣方便將來的訓(xùn)練玉雾。當(dāng)上面的大循環(huán)走完時(shí),數(shù)據(jù)流也就從input layer
流到了output layer
轻要。此時(shí)再將數(shù)據(jù)收集起來即可:
for x in self.outputs:
tensor, mask = tensor_map[str(id(x))]
pass
output_tensors.append(tensor)
output_masks.append(mask)
Method call
與 cache 策略
call
函數(shù)沒有承擔(dān)主要計(jì)算任務(wù)复旬,計(jì)算任務(wù)主要還是由run_internal_graph
方法實(shí)現(xiàn)的。
但是call
利用了一個(gè)巧妙的緩沖策略降低了調(diào)用run_internal_graph
的次數(shù)(顯然冲泥,這個(gè)函數(shù)要進(jìn)行一次圖的全局計(jì)算驹碍,代價(jià)相對(duì)比較高)。在Container
的構(gòu)造函數(shù)__init__
中凡恍,特別預(yù)置了三個(gè)dict
:
self._output_mask_cache = {}
self._output_tensor_cache = {}
self._output_shape_cache = {}
就是為了起到緩沖作用志秃,降低調(diào)用run_internal_graph
的次數(shù)。在call
中嚼酝,可以清晰地看到它們的作用:
def call(self, input, mask=None):
pass
if cache_key in self._output_tensor_cache:
return self._output_tensor_cache[cache_key]
else:
output_tensors, output_masks, output_shapes = self.run_internal_graph(inputs, masks)
return output_tensors
這個(gè)緩沖的設(shè)計(jì)浮还,或許也可以作為芯片設(shè)計(jì)的參考。
到此為止闽巩,keras/engine/topology.py
中的主要三個(gè)類:
Node
Layer
Container
已經(jīng)解釋過了钧舌,其實(shí)還有諸如Merge
之類的內(nèi)容也是很重要的。但如果只是Sequential
模型涎跨,就未必需要在這里添加說明以起探微Keras
代碼之功效洼冻,讀者可以自去讀源碼。
class Model(Container)
設(shè)計(jì)
Model
類相比于Container
類而言隅很,最大的特點(diǎn)就是它具有了反向傳播的能力撞牢,換而言之也就是說Model
可以進(jìn)行訓(xùn)練,這一點(diǎn)落在fit
函數(shù)上。至于其他的方法普泡,諸如predict
之類播掷,對(duì)于理解深度學(xué)習(xí)框架而言并不十分重要。因此撼班,主要需要理解的就是compile
和fit
兩個(gè)函數(shù)歧匈,實(shí)際上用戶在進(jìn)行訓(xùn)練時(shí),也是這兩個(gè)函數(shù)最重要砰嘁。
Method compile
compile
函數(shù)對(duì)輸入進(jìn)行了一些確認(rèn)
def compile(self, optimizer, loss, metrics=None, loss_weights=None,
sample_weight_mode=None, **kwargs):
pass
注意到用戶在調(diào)用compile
時(shí)件炉,實(shí)際上還沒有填充訓(xùn)練集與樣本標(biāo)簽,例如我們最開始使用的例子:
model.compile(optimizer='rmsprop',
loss='categorical_crossentropy',
metrics=['accuracy'])
化歸格式
compile
會(huì)對(duì)用來設(shè)定模型的參數(shù)進(jìn)行檢查矮湘,包括數(shù)據(jù)類型檢查等等斟冕,以化歸到恰當(dāng)?shù)男问健@?code>loss可能有dict
和list
的表達(dá)缅阳,那么compile
要分別處理兩種表達(dá)的輸入磕蛇。
準(zhǔn)備預(yù)測(cè)
預(yù)測(cè)結(jié)果用self.targets
來儲(chǔ)存:
self.targets = []
for i in range(len(self.outputs)):
shape = self.internal_output_shapes[i]
name = self.output_names[i]
self.targets.append(K.placeholder(ndim=len(shape),
name=name + '_target',
sparse=K.is_sparse(self.outputs[i]),
dtype=K.dtype(self.outputs[i])))
用Tensorflow
和Theano
后端產(chǎn)生一個(gè)占位符。
loss
計(jì)算
誤差計(jì)算用函數(shù)參數(shù)loss_weights
計(jì)算權(quán)重十办,加權(quán)計(jì)算總的誤差:
total_loss = None
for i in range(len(self.outputs)):
y_true = self.targets[i]
y_pred = self.outputs[i]
weighted_loss = weighted_losses[i]
sample_weight = sample_weights[i]
mask = masks[i]
loss_weight = loss_weights_list[i]
output_loss = weighted_loss(y_true, y_pred,
sample_weight, mask)
if len(self.outputs) > 1:
self.metrics_tensors.append(output_loss)
self.metrics_names.append(self.output_names[i] + '_loss')
if total_loss is None:
total_loss = loss_weight * output_loss
else:
total_loss += loss_weight * output_loss
最后秀撇,對(duì)于每一組真實(shí)值與預(yù)測(cè)值,都要確定計(jì)算他們之間loss
的目標(biāo)函數(shù):
for i in range(len(self.outputs)):
y_true = self.targets[i]
y_pred = self.outputs[i]
output_metrics = nested_metrics[i]
for metric in output_metrics:
if metric == 'accuracy' or metric == 'acc':
output_shape = self.internal_output_shapes[i]
acc_fn = None
# 選用不同的目標(biāo)函數(shù)
if output_shape[-1] == 1 or self.loss_functions[i] == objectives.binary_crossentropy:
acc_fn = metrics_module.binary_accuracy
elif self.loss_functions[i] == objectives.sparse_categorical_crossentropy:
acc_fn = metrics_module.sparse_categorical_accuracy
else:
acc_fn = metrics_module.categorical_accuracy
append_metric(i, 'acc', acc_fn(y_true, y_pred))
else:
pass
目標(biāo)函數(shù)本身是在keras/metrics.py
中向族,在keras/engine/training.py
中被調(diào)用為metrics_module
from .. import metrics as metrics_module
這部分代碼是用backend寫成的呵燕。以(0, 1)二值標(biāo)簽為例,目標(biāo)函數(shù)為:
def binary_accuracy(y_true, y_pred):
return K.mean(K.equal(y_true, K.round(y_pred)))
Method fit
fit
函數(shù)是具有批量反向傳播訓(xùn)練能力的函數(shù):
def fit(self, x, y, batch_size=32, nb_epoch=10, verbose=1, callbacks=None,
validation_split=0., validation_data=None, shuffle=True,
class_weight=None, sample_weight=None, initial_epoch=0):
pass
數(shù)據(jù)標(biāo)準(zhǔn)化
在fit
函數(shù)中件相,首先會(huì)調(diào)用類方法_standardize_user_data
以進(jìn)行數(shù)據(jù)處理:
x, y, sample_weights = self._standardize_user_data(
x, y,
sample_weight=sample_weight,
class_weight=class_weight,
check_batch_dim=False,
batch_size=batch_size)
這樣得到標(biāo)準(zhǔn)化的數(shù)據(jù)再扭。
訓(xùn)練函數(shù)
關(guān)于驗(yàn)證數(shù)據(jù)validation_data
這部分,因?yàn)橛袝r(shí)候用戶不會(huì)使用夜矗,所以就不在這里說明了泛范。接下來就是要準(zhǔn)備輸入數(shù)據(jù)和訓(xùn)練函數(shù)(目標(biāo)函數(shù)):
# prepare input arrays and training function
if self.uses_learning_phase and not isinstance(K.learning_phase(), int):
ins = x + y + sample_weights + [1.]
else:
ins = x + y + sample_weights
# 準(zhǔn)備算子`self.train_function`
self._make_train_function()
# 給出`self.train_function`算子,供`_fit_loop`使用
f = self.train_function
其中屬性uses_learning_phase
是從Layer
繼承來的紊撕,經(jīng)過Container
和Model
的封裝敦跌。它的本意是用來標(biāo)志Layer
是否會(huì)用到后端函數(shù)K.in_training_phase()
或K.in_test_phase()
。
調(diào)用_make_train_function
以準(zhǔn)備數(shù)據(jù)逛揩,以及準(zhǔn)備好目標(biāo)函數(shù)的算子:
def _make_train_function(self):
pass
if self.train_function is None:
# 準(zhǔn)備`inputs`
if self.uses_learning_phase and not isinstance(K.learning_phase(), int):
inputs = self.inputs + self.targets + self.sample_weights + [K.learning_phase()]
else:
inputs = self.inputs + self.targets + self.sample_weights
# 準(zhǔn)備`updates`
training_updates = self.optimizer.get_updates(self._collected_trainable_weights,
self.constraints,
self.total_loss)
updates = self.updates + training_updates
# 調(diào)用backend,準(zhǔn)備好目標(biāo)函數(shù)的算子
self.train_function = K.function(inputs,
[self.total_loss] + self.metrics_tensors,
updates=updates,
**self._function_kwargs)
Theano
后端的 Function
后端函數(shù)function
用傳遞來的參數(shù)實(shí)例化一個(gè)Keras
的Function
類返回:
def function(inputs, outputs, updates=[], **kwargs):
pass
return Function(inputs, outputs, updates=updates, **kwargs)
Function
類只有兩個(gè)函數(shù)麸俘,除了__init__
以外還有一個(gè)__call__
辩稽,使其成為“可調(diào)用的類”。這相當(dāng)于Function
的對(duì)象當(dāng)作函數(shù)來使用从媚,相當(dāng)于重載了括號(hào)運(yùn)算符逞泄。這樣就可以通過下面的代碼直接求出outputs
:
outputs = self.train_function(ins)
Function
的構(gòu)造函數(shù)主要完成變量的更新:
class Function(object):
def __init__(self, inputs, outputs, updates=[], **kwargs):
unique_variables_to_update = {}
for v, nv in updates:
if v not in unique_variables_to_update:
unique_variables_to_update[v] = nv
updates = unique_variables_to_update.items()
self.function = theano.function(inputs, outputs, updates=updates,
allow_input_downcast=True,
on_unused_input='ignore',
**kwargs)
同時(shí),定義好了形式上的輸入輸出函數(shù)self.function
。這是通過theano.function
實(shí)現(xiàn)的喷众,關(guān)于這個(gè)有用的函數(shù)可以去看Theano
的官方手冊(cè)function - defines theano.function
__call__
函數(shù)的主要任務(wù)則是進(jìn)行數(shù)據(jù)計(jì)算各谚,給出outputs
表達(dá)式的數(shù)值:
def __call__(self, inputs):
assert isinstance(inputs, (list, tuple))
return self.function(*inputs)
_fit_loop
函數(shù)fit
最終就是返回_fit_loop
的結(jié)果,這是訓(xùn)練過程中的一切歷史記錄信息到千。此時(shí)原始輸入的訓(xùn)練集已經(jīng)被改造成ins
昌渤,樣本標(biāo)簽也成為out_labels
,其他的參數(shù)都傳遞給訓(xùn)練函數(shù)_fit_loop
了:
return self._fit_loop(f, ins, out_labels=out_labels,
batch_size=batch_size, nb_epoch=nb_epoch,
verbose=verbose, callbacks=callbacks,
val_f=val_f, val_ins=val_ins, shuffle=shuffle,
callback_metrics=callback_metrics,
initial_epoch=initial_epoch)
_fit_loop
是一個(gè)抽象的函數(shù)f(ins)
憔四,這里f
是從后端構(gòu)建來的算子self.train_function
膀息,ins
是輸入的訓(xùn)練集。
歷史記錄是通過keras/callbacks.py
搜集的:
self.history = cbks.History()
撇開數(shù)據(jù)的處理了赵、準(zhǔn)備潜支,訓(xùn)練的主要代碼是這段循環(huán),非常關(guān)鍵:
for epoch in range(initial_epoch, nb_epoch):
# 記錄本回epoch的歷史信息
callbacks.on_epoch_begin(epoch)
# 按照batch批次打混索引
if shuffle == 'batch':
index_array = batch_shuffle(index_array, batch_size)
elif shuffle:
np.random.shuffle(index_array)
# 得到一個(gè)批次的索引
batches = make_batches(nb_train_sample, batch_size)
epoch_logs = {}
以下的循環(huán)是批量對(duì)訓(xùn)練集進(jìn)行訓(xùn)練柿汛,首先是準(zhǔn)備訓(xùn)練集的數(shù)據(jù)切片冗酿,切片大小自然是按照批次設(shè)定的:
for batch_index, (batch_start, batch_end) in enumerate(batches):
batch_ids = index_array[batch_start:batch_end]
try:
if isinstance(ins[-1], float):
# do not slice the training phase flag
ins_batch = slice_X(ins[:-1], batch_ids) + [ins[-1]]
else:
ins_batch = slice_X(ins, batch_ids)
except TypeError:
raise TypeError('TypeError while preparing batch. '
'If using HDF5 input data, '
'pass shuffle="batch".')
這里調(diào)用了函數(shù)slice_X
,這個(gè)函數(shù)是用來截取python
的list
和numpy
的array
兩種格式的列表的络断。如此獲得的ins_batch
自然就是此回epoch
裁替、此batch
批次的輸入ins
。
batch_logs = {}
batch_logs['batch'] = batch_index
batch_logs['size'] = len(batch_ids)
callbacks.on_batch_begin(batch_index, batch_logs)
outs = f(ins_batch)
if not isinstance(outs, list):
outs = [outs]
for l, o in zip(out_labels, outs):
batch_logs[l] = o
callbacks.on_batch_end(batch_index, batch_logs)
if batch_index == len(batches) - 1: # last batch
# validation
if do_validation:
# replace with self._evaluate
val_outs = self._test_loop(val_f, val_ins,
batch_size=batch_size,
verbose=0)
if not isinstance(val_outs, list):
val_outs = [val_outs]
# same labels assumed
for l, o in zip(out_labels, val_outs):
epoch_logs['val_' + l] = o
callbacks.on_epoch_end(epoch, epoch_logs)
if callback_model.stop_training:
break