用 Python 从零开始创建神经网络(十八):模型对象(Model Object)

模型对象(Model Object)

引言

我们构建了一个可以执行前向传播、反向传播以及精度测量等辅助任务的模型。通过编写相当多的代码并在一些较大的代码块中进行修改,我们实现了这些功能。此时,将模型本身转化为一个对象的做法开始显得更有意义,特别是当我们希望保存和加载这个对象以用于未来的预测任务时。此外,我们还可以利用这个对象减少一些常见代码行,使得与当前代码库的协作更加便捷,同时也更容易构建新的模型。为了完成模型对象的转换,我们将使用我们最近工作的模型,即使用正弦数据的回归模型:

from nnfs.datasets import sine_data

X, y = sine_data()

有了数据之后,我们制作模型类的第一步就是添加我们想要的各层。因此,我们可以通过以下操作来开始我们的模型类:

# Model class
class Model:
    def __init__(self):
        # Create a list of network objects
        self.layers = []
        
    # Add objects to the model
    def add(self, layer):
        self.layers.append(layer)

这样,我们就可以使用模型对象的添加方法来添加图层。仅这一点就能大大提高可读性。让我们添加一些图层:

# Instantiate the model
model = Model()

# Add layers
model.add(Layer_Dense(1, 64))
model.add(Activation_ReLU())
model.add(Layer_Dense(64, 64))
model.add(Activation_ReLU())
model.add(Layer_Dense(64, 1))
model.add(Activation_Linear())

我们现在也可以查询这个模型:

print(model.layers)
>>>
[<__main__.Layer_Dense object at 0x000001D1EB2A2900>, 
<__main__.Activation_ReLU object at 0x000001D1EB2A2180>, 
<__main__.Layer_Dense object at 0x000001D1EB2A3F20>, 
<__main__.Activation_ReLU object at 0x000001D1EB2B9220>, 
<__main__.Layer_Dense object at 0x000001D1EB2BB800>, 
<__main__.Activation_Linear object at 0x000001D1EB2BBA40>]

除了添加层,我们还想为模型设置损失函数和优化器。为此,我们将创建一个名为 set 的方法:

# Set loss and optimizer
def set(self, *, loss, optimizer):
    self.loss = loss
    self.optimizer = optimizer

在参数定义中使用星号(*)表示后续的参数(在本例中是lossoptimizer)为关键字参数。由于这些参数没有默认值,因此它们是必需的关键字参数,也就是说必须通过名称和值的形式传递,从而使代码更加易读。

现在,我们可以将一个调用此方法的语句添加到我们新创建的模型对象中,并传递loss和optimizer对象:

# Create dataset
X, y = sine_data()

# Instantiate the model
model = Model()

# Add layers
model.add(Layer_Dense(1, 64))
model.add(Activation_ReLU())
model.add(Layer_Dense(64, 64))
model.add(Activation_ReLU())
model.add(Layer_Dense(64, 1))
model.add(Activation_Linear())

# Set loss and optimizer objects
model.set(
    loss=Loss_MeanSquaredError(),
    optimizer=Optimizer_Adam(learning_rate=0.005, decay=1e-3),
    )

设置好模型的层、损失函数和优化器后,下一步就是训练了,因此我们要添加一个 train 方法。现在,我们先将其作为一个占位符,不久后再进行填充:

# Train the model
def train(self, X, y, *, epochs=1, print_every=1):
    # Main training loop
    for epoch in range(1, epochs+1):
        # Temporary
        pass

然后,我们可以在模型定义中添加对 train 方法的调用。我们将传递训练数据、epochs 的数量(10000,我们目前使用的是),以及打印训练摘要的频率。我们不需要或不希望每一步都打印,因此我们将对其进行配置:

# Create dataset
X, y = sine_data()

# Instantiate the model
model = Model()

# Add layers
model.add(Layer_Dense(1, 64))
model.add(Activation_ReLU())
model.add(Layer_Dense(64, 64))
model.add(Activation_ReLU())
model.add(Layer_Dense(64, 1))
model.add(Activation_Linear())

# Set loss and optimizer objects
model.set(
    loss=Loss_MeanSquaredError(),
    optimizer=Optimizer_Adam(learning_rate=0.005, decay=1e-3),
    )

model.train(X, y, epochs=10000, print_every=100)

要进行训练,我们需要执行前向传播。在对象中执行前向传播稍微复杂一些,因为我们需要在层的循环中完成此操作,并且需要知道前一层的输出以正确地传递数据。查询前一层的一个问题是,第一层没有“前一层”。我们定义的第一层是第一隐含层。因此,我们的一个选择是创建一个“输入层”。这被认为是神经网络中的一层,但没有与之相关的权重和偏置。输入层仅包含训练数据,我们仅在循环迭代层时将其用作第一层的“前一层”。我们将创建一个新类,并像调用Layer_Dense类一样调用它,称为Layer_Input

# Input "layer"
class Layer_Input:
    # Forward pass
    def forward(self, inputs):
        self.output = inputs

forward方法将训练样本设置为self.output。这一属性与其他层是通用的。这里没有必要实现反向传播方法,因为我们永远不会用到它。现在可能看起来创建这个类有点多余,但希望很快你就会明白我们将如何使用它。接下来,我们要为模型的每一层设置前一层和后一层的属性。我们将在Model类中创建一个名为finalize的方法:

	# Finalize the model
	def finalize(self):
	    # Create and set the input layer
	    self.input_layer = Layer_Input()
	    # Count all the objects
	    layer_count = len(self.layers)
	    # Iterate the objects
	    for i in range(layer_count):
	        # If it's the first layer,
	        # the previous layer object is the input layer
	        if i == 0:
	            self.layers[i].prev = self.input_layer
	            self.layers[i].next = self.layers[i+1]
	        # All layers except for the first and the last
	        elif i < layer_count - 1:
	            self.layers[i].prev = self.layers[i-1]
	            self.layers[i].next = self.layers[i+1]
	        # The last layer - the next object is the loss
	        else:
	            self.layers[i].prev = self.layers[i-1]
	            self.layers[i].next = self.loss

这段代码创建了一个输入层,并为模型对象的self.layers列表中的每一层设置了nextprev引用。我们创建了Layer_Input类,以便在循环中为第一隐藏层设置prev属性,因为我们将以统一的方式调用所有层。对于最后一层,其next层将是我们已经创建的损失函数。

现在,我们已经为模型对象执行前向传播所需的层信息准备就绪,让我们添加一个forward方法。我们将同时在训练时和之后仅进行预测(也称为模型推理)时使用这个forward方法。以下是在Model类中继续添加的代码:

# Forward pass
class Model:
	...
    # Performs forward pass
    def forward(self, X):
        # Call forward method on the input layer
        # this will set the output property that
        # the first layer in "prev" object is expecting
        self.input_layer.forward(X)
        # Call forward method of every object in a chain
        # Pass output of the previous object as a parameter
        for layer in self.layers:
            layer.forward(layer.prev.output)
        # "layer" is now the last object from the list,
        # return its output
        return layer.output

在这种情况下,我们传入输入数据 X X X,然后简单地通过 Model 对象中的 input_layer 处理该数据,这会在该对象中创建一个 output 属性。从这里开始,我们迭代 self.layers 中的层,这些层从第一个隐藏层开始。对于每一层,我们对上一层的输出数据 layer.prev.output 执行前向传播。对于第一个隐藏层,layer.prevself.input_layer。调用每一层的 forward 方法时会创建该层的 output 属性,然后该属性会作为输入传递到下一层的 forward 方法调用中。一旦我们遍历了所有层,就会返回最后一层的输出。

这就是一次前向传播。现在,让我们将这个前向传播方法调用添加到 Model 类的 train 方法中:

# Forward pass
class Model:
	...
    # Train the model
    def train(self, X, y, *, epochs=1, print_every=1):
        # Main training loop
        for epoch in range(1, epochs+1):
            # Perform the forward pass
            output = self.forward(X)
            # Temporary
            print(output)
            sys.exit()

到目前为止的完整Model类:

# Model class
class Model:
    def __init__(self):
        # Create a list of network objects
        self.layers = []
        
    # Add objects to the model
    def add(self, layer):
        self.layers.append(layer)
    
    # Set loss and optimizer
    def set(self, *, loss, optimizer):
        self.loss = loss
        self.optimizer = optimizer
    
    # Train the model
    def train(self, X, y, *, epochs=1, print_every=1):
        # Main training loop
        for epoch in range(1, epochs+1):
            # Perform the forward pass
            output = self.forward(X)
            # Temporary
            print(output)
            sys.exit()

    # Finalize the model
    def finalize(self):
        # Create and set the input layer
        self.input_layer = Layer_Input()
        # Count all the objects
        layer_count = len(self.layers)
        # Iterate the objects
        for i in range(layer_count):
            # If it's the first layer,
            # the previous layer object is the input layer
            if i == 0:
                self.layers[i].prev = self.input_layer
                self.layers[i].next = self.layers[i+1]
            # All layers except for the first and the last
            elif i < layer_count - 1:
                self.layers[i].prev = self.layers[i-1]
                self.layers[i].next = self.layers[i+1]
            # The last layer - the next object is the loss
            else:
                self.layers[i].prev = self.layers[i-1]
                self.layers[i].next = self.loss

    # Performs forward pass
    def forward(self, X):
        # Call forward method on the input layer
        # this will set the output property that
        # the first layer in "prev" object is expecting
        self.input_layer.forward(X)
        # Call forward method of every object in a chain
        # Pass output of the previous object as a parameter
        for layer in self.layers:
            layer.forward(layer.prev.output)
        # "layer" is now the last object from the list,
        # return its output
        return layer.output

最后,我们可以在主代码中添加 finalize 方法调用(请记住,除其他事项外,该方法还能让模型的图层知道它们的上一层和下一层)。

# Create dataset
X, y = sine_data()

# Instantiate the model
model = Model()

# Add layers
model.add(Layer_Dense(1, 64))
model.add(Activation_ReLU())
model.add(Layer_Dense(64, 64))
model.add(Activation_ReLU())
model.add(Layer_Dense(64, 1))
model.add(Activation_Linear())

# Set loss and optimizer objects
model.set(
    loss=Loss_MeanSquaredError(),
    optimizer=Optimizer_Adam(learning_rate=0.005, decay=1e-3),
    )

# Finalize the model
model.finalize()

model.train(X, y, epochs=10000, print_every=100)
>>>
[[ 0.00000000e+00]
[-1.13209149e-08]
[-2.26418297e-08]
...
[-1.12869511e-05]
[-1.12982725e-05]
[-1.13095930e-05]]

此时,我们已经在Model类中覆盖了模型的前向传播。我们仍需要计算损失和准确率,并进行反向传播。在此之前,我们需要知道哪些层是“可训练的”,也就是说这些层具有我们可以调整的权重和偏置。为此,我们需要检查层是否有weightsbiases属性。我们可以通过以下代码进行检查:

			# 如果层包含一个名为“weights”的属性,  
			# 那么它是一个可训练层 -  
			# 将其添加到可训练层列表中  
			# 我们不需要检查偏置 -  
			# 检查权重已经足够了  
			if hasattr(self.layers[i], 'weights'):
				self.trainable_layers.append(self.layers[i])

其中, i i i 是层列表中某一层的索引。我们将把这段代码添加到 finalize 方法中。以下是目前该方法的完整代码:

    # Finalize the model
    def finalize(self):
        # Create and set the input layer
        self.input_layer = Layer_Input()
        # Count all the objects
        layer_count = len(self.layers)
        # Initialize a list containing trainable layers:
        self.trainable_layers = []
        # Iterate the objects
        for i in range(layer_count):
            # If it's the first layer,
            # the previous layer object is the input layer
            if i == 0:
                self.layers[i].prev = self.input_layer
                self.layers[i].next = self.layers[i+1]
            # All layers except for the first and the last
            elif i < layer_count - 1:
                self.layers[i].prev = self.layers[i-1]
                self.layers[i].next = self.layers[i+1]
            # The last layer - the next object is the loss
            # Also let's save aside the reference to the last object
            # whose output is the model's output
            else:
                self.layers[i].prev = self.layers[i-1]
                self.layers[i].next = self.loss
                self.output_layer_activation = self.layers[i]
            
            # 如果层包含一个名为“weights”的属性,  
            # 那么它是一个可训练层 -  
            # 将其添加到可训练层列表中  
            # 我们不需要检查偏置 -  
            # 检查权重已经足够了  
            if hasattr(self.layers[i], 'weights'):
            	self.trainable_layers.append(self.layers[i])

接下来,我们将修改普通 Loss 类,使其包含以下内容:

# Common loss class
class Loss:
	...        
    # Calculates the data and regularization losses
    # given model output and ground truth values
    def calculate(self, output, y):
        # Calculate sample losses
        sample_losses = self.forward(output, y)
        # Calculate mean loss
        data_loss = np.mean(sample_losses)
        # Return the data and regularization losses
        return data_loss, self.regularization_loss()   
        
    # Set/remember trainable layers
    def remember_trainable_layers(self, trainable_layers):
        self.trainable_layers = trainable_layers

commonLoss 类中的 remember_trainable_layers 方法“告知”损失对象哪些是 Model 对象中的可训练层。在单次调用期间,calculate 方法已被修改为还会返回 self.regularization_loss() 的值。regularization_loss 方法目前需要一个层对象,但随着在 remember_trainable_layers 方法中设置了 self.trainable_layers 属性,我们现在可以迭代所有可训练层,以计算整个模型的正则化损失,而不是每次仅针对一个层进行计算:

# Common loss class
class Loss:
	...
	# Regularization loss calculation
    def regularization_loss(self):        
        # 0 by default
        regularization_loss = 0
        # Calculate regularization loss
        # iterate all trainable layers
        for layer in self.trainable_layers:
            # L1 regularization - weights
            # calculate only when factor greater than 0
            if layer.weight_regularizer_l1 > 0:
                regularization_loss += layer.weight_regularizer_l1 * np.sum(np.abs(layer.weights))
            # L2 regularization - weights
            if layer.weight_regularizer_l2 > 0:
                regularization_loss += layer.weight_regularizer_l2 * np.sum(layer.weights * layer.weights)
            # L1 regularization - biases
            # calculate only when factor greater than 0
            if layer.bias_regularizer_l1 > 0:
                regularization_loss += layer.bias_regularizer_l1 * np.sum(np.abs(layer.biases))
            # L2 regularization - biases
            if layer.bias_regularizer_l2 > 0:
                regularization_loss += layer.bias_regularizer_l2 * np.sum(layer.biases * layer.biases)
        return regularization_loss

为了计算准确率,我们需要预测结果。目前,根据模型的类型,预测需要不同的代码。例如,对于 softmax 分类器,我们使用 np.argmax(),但对于回归,由于输出层使用线性激活函数,预测结果直接为输出值。理想情况下,我们需要一个预测方法,该方法能够为我们的模型选择合适的预测方式。为此,我们将在每个激活函数类中添加一个 predictions 方法:

# Softmax activation
class Activation_Softmax:
	...            
    # Calculate predictions for outputs
    def predictions(self, outputs):
        return np.argmax(outputs, axis=1)
# Sigmoid activation
class Activation_Sigmoid:
	...
    # Calculate predictions for outputs
    def predictions(self, outputs):
        return (outputs > 0.5) * 1
# Linear activation
class Activation_Linear:
    ...
    # Calculate predictions for outputs
    def predictions(self, outputs):
        return outputs

predictions 函数内部进行的所有计算与之前章节中针对适当模型所执行的计算相同。尽管我们没有计划将 ReLU 激活函数用于输出层的激活函数,但我们为了完整性仍会在此处包含它:

# ReLU activation
class Activation_ReLU:  
	...
    # Calculate predictions for outputs
    def predictions(self, outputs):
        return outputs

我们仍然需要在 Model 对象中为最终层的激活函数设置一个引用。之后我们可以调用 predictions 方法,该方法将根据输出计算并返回预测值。我们将在 Model 类的 finalize 方法中设置这一引用。

# Model class
class Model:
	...
	# Finalize the model
    def finalize(self):
    	...
			# The last layer - the next object is the loss
            # Also let's save aside the reference to the last object
            # whose output is the model's output
            else:
                self.layers[i].prev = self.layers[i-1]
                self.layers[i].next = self.loss
                self.output_layer_activation = self.layers[i]

就像不同的预测方法一样,我们也需要以不同的方式计算准确率。我们将以类似于特定损失类对象实现的方式来实现这一功能——创建特定的准确率类及其对象,并将它们与模型关联。

首先,我们会编写一个通用的 Accuracy 类,该类目前只包含一个方法 calculate,用于返回根据比较结果计算的准确率。我们已经在代码中添加了对 self.compare 方法的调用,但这个方法目前还不存在。我们将在继承自 Accuracy 类的其他类中创建该方法。现在只需要知道这个方法会返回一个由 TrueFalse 值组成的列表,指示预测是否与真实值匹配。接下来,我们计算这些值的平均值(True 被视为1,False 被视为0),并将其作为准确率返回。代码如下:

# Common accuracy class
class Accuracy:
    # Calculates an accuracy
    # given predictions and ground truth values
    def calculate(self, predictions, y):
        # Get comparison results
        comparisons = self.compare(predictions, y)
        # Calculate an accuracy
        accuracy = np
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值