认识Python的“类”和它们在Keras中的使用

204 阅读9分钟

类是Python语言的基本构件之一,可以应用于机器学习应用的开发。正如我们将要看到的,开发类的Python语法很简单,可以应用于实现Keras的回调。

在本教程中,你将发现Python类和它们的功能。

完成本教程后,你将知道。

  • 为什么Python类很重要。
  • 如何定义和实例化一个类,并设置其属性。
  • 如何创建方法和传递参数。
  • 什么是类的继承。
  • 如何使用类来实现Keras中的回调。

教程概述

本教程分为六个部分;它们是:

  • 类的介绍
  • 定义一个类
  • 实例化和属性引用
  • 创建方法和传递参数
  • 类的继承
  • 在Keras中使用类

类的介绍

在面向对象的语言中,例如Python,类是基本的构建模块之一。

它们可以被比作对象的蓝图,因为它们定义了一个对象应该有哪些属性和方法/行为。

-Python基础知识,2018。

创建一个新的类会创建一个新的对象,其中每个类的实例都可以通过其属性来保持其状态,并通过方法来修改其状态。

定义一个类

class关键字允许创建一个新的类定义,紧接着就是类的名称。

class MyClass:
    <statements>

通过这种方式,一个与指定的类名*(在这个特定的例子中是MyClass*)绑定的新的类对象被创建。每个类对象都可以支持实例化和属性引用,我们很快就会看到。

实例化和属性引用

实例化是创建一个新的类的实例。

为了创建一个新的类的实例,我们可以用它的类名来调用它,并把它赋给一个变量。这将创建一个新的、空的类对象。

x = MyClass()

在创建一个新的类实例时,Python 调用它的对象构造方法,__init()__,该方法通常需要参数,用来设置实例化对象的属性。

我们可以在我们的类中定义这个构造方法,就像一个函数一样,并指定在实例化对象时需要传递的属性。

-Python基础知识,2018。

比方说,我们想定义一个新的类,名为 ""。

class Dog:
	family = "Canine"

	def __init__(self, name, breed):
		self.name = name
		self.breed = breed

在这里,构造方法需要两个参数,名字品种,这些参数可以在实例化对象时传递给它。

dog1 = Dog("Lassie", "Rough Collie")

在我们考虑的例子中,名字品种被称为实例变量(或属性),因为它们被绑定到一个特定的实例。这意味着这些属性属于它们被设置的对象,而不属于从同一类别实例化的任何其他对象。

另一方面,family是一个类变量(或属性),因为它被同一类的所有实例所共享。

你可能还注意到,构造函数方法(或任何其他方法)的第一个参数通常被称为self。这个参数指的是我们正在创建的对象。遵循将第一个参数设置为self的惯例是很好的做法,这样可以保证你的代码对其他程序员的可读性。

一旦我们设置了对象的属性,就可以使用点运算符来访问它们。例如,再次考虑狗类dog1实例,它的name属性可以被访问,如下所示。

print(dog1.name)

产生以下输出。

Lassie

创建方法和传递参数

除了有一个构造方法外,一个类对象还可以有其他几个方法来修改其状态。

定义一个实例方法的语法是很熟悉的。我们传递参数self......它总是一个实例方法的第一个参数。

-Python基础知识,2018。

与构造方法类似,每个实例方法可以接受几个参数,第一个是参数self,让我们设置和访问对象的属性。

class Dog:
	family = "Canine"

	def __init__(self, name, breed):
		self.name = name
		self.breed = breed

	def info(self):
		print(self.name, "is a female", self.breed)

同一对象的不同方法也可以使用self参数来相互调用。

class Dog:
	family = "Canine"

	def __init__(self, name, breed):
		self.name = name
		self.breed = breed
		self.tricks = []

	def add_tricks(self, x):
		self.tricks.append(x)

	def info(self, x):
		self.add_tricks(x)
		print(self.name, "is a female", self.breed, "that", self.tricks[0])

然后可以生成一个输出字符串,如下所示。

dog1 = Dog("Lassie", "Rough Collie")
dog1.info("barks on command")

我们发现,在这样做的时候,当info() 方法调用add_tricks()方法时,命令输入的叫声被追加到技巧列表中。产生的输出结果如下。

Lassie is a female Rough Collie that barks on command

类的继承

Python支持的另一个特性是类的继承

继承是一种机制,它允许一个子类(也称为派生类子类) 访问一个超类(也称为基类父类) 的所有属性和方法。

使用子类的语法如下。

class SubClass(BaseClass):
    <statements>

也有可能一个子类也继承了多个基类。在这种情况下,语法将如下。

class SubClass(BaseClass1, BaseClass2, BaseClass3):
    <statements>

类的属性和方法在基类中被搜索,在多继承的情况下也在后续的基类中被搜索。

Python 进一步允许子类中的一个方法覆盖基类中另一个同名的方法。子类中的覆盖方法可能是替换基类的方法,或者只是扩展它的功能。当一个覆盖的子类方法可用时,调用时执行的是这个方法,而不是基类中同名的方法。

在Keras中使用类

Keras中类的一个实际用途是编写自己的回调。

回调是Keras中一个强大的工具,它允许我们在训练、测试和预测的不同阶段查看我们模型的行为。

事实上,我们可以将一个回调列表传递给以下任何一个。

  • keras.Model.fit()
  • keras.Model.evaluation()
  • keras.Model.predict()

Keras的API带有几个内置的回调。尽管如此,我们可能希望编写自己的回调,为此,我们将看到如何建立一个自定义的回调类。为了做到这一点,我们可以从回调基类中继承几个方法,这些方法可以为我们提供何时的信息。

  • 训练、测试和预测的开始和结束。
  • 一个epoch开始和结束。
  • 一个训练、测试和预测批次的开始和结束。

让我们首先考虑一个简单的例子,即一个自定义回调类,它可以在每一个纪元开始和结束时报告。我们将把这个自定义回调类命名为EpochCallback,并从基类keras.callbacks.Callback中重写历时级方法on_epoch_begin()on_epoch_end()

import tensorflow.keras as keras

class EpochCallback(keras.callbacks.Callback):
    def on_epoch_begin(self, epoch, logs=None):
        print("Starting epoch {}".format(epoch + 1))

    def on_epoch_end(self, epoch, logs=None):
        print("Finished epoch {}".format(epoch + 1))

为了测试我们刚刚定义的自定义回调,我们需要一个模型来训练。为此,让我们定义一个简单的Keras模型。

from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense, Flatten

def simple_model():
    model = Sequential()
    model.add(Flatten(input_shape=(28, 28)))
    model.add(Dense(128, activation="relu"))
    model.add(Dense(10, activation="softmax"))

    model.compile(loss="categorical_crossentropy",
                  optimizer="sgd",
                  metrics=["accuracy"])
    return model

我们还需要一个数据集来训练,为此我们将使用MNIST数据集。

from tensorflow.keras.datasets import mnist
from tensorflow.keras.utils import to_categorical

# Loading the MNIST training and testing data splits
(x_train, y_train), (x_test, y_test) = mnist.load_data()

# Pre-processing the training data
x_train = x_train / 255.0
x_train = x_train.reshape(60000, 28, 28, 1)
y_train_cat = to_categorical(y_train, 10)

现在,让我们试试这个自定义回调,把它添加到我们作为输入传递给*keras.Model.fit()*方法的回调列表里。

model = simple_model()

model.fit(x_train,
          y_train_cat,
          batch_size=32,
          epochs=5,
          callbacks=[EpochCallback()],
          verbose=0)

我们刚刚创建的回调会产生以下输出。

Starting epoch 1
Finished epoch 1
Starting epoch 2
Finished epoch 2
Starting epoch 3
Finished epoch 3
Starting epoch 4
Finished epoch 4
Starting epoch 5
Finished epoch 5

我们可以创建另一个自定义回调,在每个epoch结束时监测损失值,并仅在损失减少时存储模型权重。为此,我们将从logdict中读取损失值,它在每个批次和历时结束时存储指标。我们还将通过self.model来访问对应于当前一轮训练、测试或预测的模型。

让我们把这个自定义回调称为CheckpointCallback

import numpy as np

class CheckpointCallback(keras.callbacks.Callback):

    def __init__(self):
        super(CheckpointCallback, self).__init__()
        self.best_weights = None

    def on_train_begin(self, logs=None):
        self.best_loss = np.Inf

    def on_epoch_end(self, epoch, logs=None):
        current_loss = logs.get("loss")
        print("Current loss is {}".format(current_loss))
        if np.less(current_loss, self.best_loss):
            self.best_loss = current_loss
            self.best_weights = self.model.get_weights()
            print("Storing the model weights at epoch {} \n".format(epoch + 1))

我们可以再试一次,这次把CheckpointCallback纳入回调列表中。

model = simple_model()

model.fit(x_train,
          y_train_cat,
          batch_size=32,
          epochs=5,
          callbacks=[EpochCallback(), CheckpointCallback()],
          verbose=0)

现在,两个回调一起产生了以下的输出。

Starting epoch 1
Finished epoch 1
Current loss is 0.6327750086784363
Storing the model weights at epoch 1

Starting epoch 2
Finished epoch 2
Current loss is 0.3391888439655304
Storing the model weights at epoch 2

Starting epoch 3
Finished epoch 3
Current loss is 0.29216915369033813
Storing the model weights at epoch 3

Starting epoch 4
Finished epoch 4
Current loss is 0.2625095248222351
Storing the model weights at epoch 4

Starting epoch 5
Finished epoch 5
Current loss is 0.23906977474689484
Storing the model weights at epoch 5

Keras中的其他类

除了回调,我们还可以在Keras中为自定义度量(源自keras.metrics.Metrics )、自定义层(源自keras.layers.Layer )、自定义正则器(源自keras.regularizers.Regularizer )甚至自定义模型(源自keras.Model ,用于改变调用模型的行为)制作衍生类。你所要做的就是遵循准则来改变一个类的成员函数。你必须在成员函数中使用完全相同的名称和参数。

下面是Keras文档中的一个例子。

class BinaryTruePositives(tf.keras.metrics.Metric):

  def __init__(self, name='binary_true_positives', **kwargs):
    super(BinaryTruePositives, self).__init__(name=name, **kwargs)
    self.true_positives = self.add_weight(name='tp', initializer='zeros')

  def update_state(self, y_true, y_pred, sample_weight=None):
    y_true = tf.cast(y_true, tf.bool)
    y_pred = tf.cast(y_pred, tf.bool)

    values = tf.logical_and(tf.equal(y_true, True), tf.equal(y_pred, True))
    values = tf.cast(values, self.dtype)
    if sample_weight is not None:
      sample_weight = tf.cast(sample_weight, self.dtype)
      values = tf.multiply(values, sample_weight)
    self.true_positives.assign_add(tf.reduce_sum(values))

  def result(self):
    return self.true_positives

  def reset_states(self):
    self.true_positives.assign(0)

m = BinaryTruePositives()
m.update_state([0, 1, 1, 1], [0, 1, 0, 0])
print('Intermediate result:', float(m.result()))

m.update_state([1, 1, 1, 1], [0, 1, 1, 0])
print('Final result:', float(m.result()))

这揭示了为什么我们需要一个用于自定义度量的类。公制不仅仅是一个函数,而是一个增量计算其值的函数,在训练周期内每批训练数据计算一次。最终,在一个历时结束时,在result() 函数中报告结果,并使用reset_state() 函数重置其内存,这样你就可以在下一个历时中重新开始。

关于具体要得出的细节,你应该参考Keras的文档。

总结

在本教程中,你发现了Python类和它们的功能。

具体来说,你学到了:

  • 为什么 Python 类很重要。
  • 如何定义和实例化一个类,并设置其属性。
  • 如何创建方法和传递参数。
  • 什么是类的继承。
  • 如何使用类来实现Keras中的回调。