TFRecord格式

TFRecord格式是TensorFlow首选的格式,用于储存大量数据并有效读取数据。这是一种非常简单的二进制格式,只包含大小不同的二进制记录序列(每个记录由一个长度、一个用于检查长度是否受损的CRC校验和、实际数据
以及最后一个CRC校验和组成)。可以使用tf.io.TFRecordWriter 类轻松创建TFRecord文件

import tensorflow as tf

with tf.io.TFRecordWriter('my_data.tfrecord') as f:
    f.write(b'This is the first tfrecord')
    f.write(b'And this is the second record')

然后可以使用tf.data.TFRecordDataset读取一个或多个TFRecord文件:

filepaths = ['my_data.tfrecord']
dataset = tf.data.TFRecordDataset(filepaths)
for item in dataset:
    print(item)
tf.Tensor(b'This is the first tfrecord', shape=(), dtype=string)
tf.Tensor(b'And this is the second record', shape=(), dtype=string)

默认情况下,TFRecordDataset将一个接一个地读取文件,但是可以用过num_parallel_reads使其并行读取多个文件并交织记录。另外,可以使用list_files()和interleave()得到与前面读取多个csv文件相同的结果

压缩的TFRecord文件

有时压缩TFRecord文件可能很有用,尤其是在需要通过网络连接加载它们的情况下。可以通过设置options参数来创建压缩的TFRecord文件:

options = tf.io.TFRecordOptions(compression_type='GZIP')
with tf.io.TFRecordWriter('my_compressed.tfrecord', options) as f:
    f.write(b'This is the first tfrecord')
    f.write(b'And this is the second record')
    filepaths = ['my_compressed.tfrecord']
dataset = tf.data.TFRecordDataset(filepaths, compression_type='GZIP')
for item in dataset:
    print(item)
tf.Tensor(b'This is the first tfrecord', shape=(), dtype=string)
tf.Tensor(b'And this is the second record', shape=(), dtype=string)

协议缓冲区简介

即使每个记录可以使用想要的任何二进制格式,TFRecord文件通常包含序列化地协议缓冲区(也成为protobufs)。这是一种可移植
、可拓展且高效的二进制格式,在2001年由Google开发,并于2008年开源。protobufs现在被广泛使用,尤其是在Google的远程过程调用系统gRPC中

def cube(x):
    return x**3
cube(2)
8
import tensorflow as tf
cube(tf.constant(2.0))
<tf.Tensor: shape=(), dtype=float32, numpy=8.0>

现在用tf.function()函数将此Python函数转化为TensorFlow函数

tf_cube=tf.function(cube)
tf_cube
<tensorflow.python.eager.def_function.Function at 0x23900209dc0>

现在可以像原Python函数一样使用此TF函数,并且会返回相同的结果(但作为张量)

tf_cube(2)
<tf.Tensor: shape=(), dtype=int32, numpy=8>
tf_cube(tf.constant(2.0))
<tf.Tensor: shape=(), dtype=float32, numpy=8.0>

在后台,tf.function()分析了cube()函数执行的计算,并生成等效的计算图。另外可以使用tf.function()作为装饰器。

@tf.function
def tf_cube(x):
    return x**3

如果有需要可以通过TF函数的python_function属性使用原Python函数

tf_cube.python_function(2)
8

TensorFlow 可以优化计算图,修建未使用的节点,简化表达式(例如将1+2替换为),等等。准备好优化的图,TF函数会以适当的顺序(并在可能时并行执行)有效地执行图中的操作。因此TF通道函数通常比原始的Python函数运行得更快,尤其是在执行复杂计算的情况下。大多数情况下,不用真正了解很多:
当想要增强Python函数时,只需要将其转换为TF函数即可。

此外,编写的自定义函数、自定义损失、自定义层或任何其他自定义函数,并在Keras模型中使用它,Keras会自动将自定义函数转换为TF函数——不需要使用tf.function()。因此大多数情况,这些处理都是100%透明的。

也可以在创建自定义层或自定义模型时设置dynamic=True来告诉Keras不要将Python函数转换为TF函数。或者可以在调用模型的compile()方法时设置run_eagerly=True

默认情况下,TF函数会为每个不同的输入形状和数据类型集生成一个新图形,并将其缓存以供后续调用。例如,如果调用tf_cube(tf.constant(10))),将形状为[]的int32张量生成图形。如果调用tf_cube(tf.constant(20)),则会重用相同的图。但是如果随后调用tf_cube(tf.constant([10,20])),则会为形状为[2]的int32张量生成一个新图。这就是TF函数处理多态(即变换的参数类型和形状)的方式。但是这仅适用于张量参数:如果将Python数值传递给TF函数,则将为每个不同的值生成一个新图:例如调用tf_cube(10)和tf_cube(20)将生成两个图

如果不同的Python数值多次调用TF函数,则会生成许多图,这会降低程序的运行速度并消耗大量RAM(必须删除TF函数才能释放它)。Python值应该保留给很少有唯一值的参数,例如每层神经元的数量那样的超参数,这使TensorFlow可以更好地优化模型的每个变体。

TF函数规则

大多数情况下,将实行TensorFlow操作的Python函数转换为TF函数很简单,用@tf.function修饰它或让Keras处理。但是有一些规则需要遵守:

  • 如果调用任何外部库,包括NumPy库甚至标准库,此调用将仅在跟踪过程中运行。它不会成为图表的一部分,实际上,TensorFlow图只能包含TensorFlow构造(张量、运算、变量、数据集等)。因此需要使用tf.reduce_sum()函数代替np.sum(),使用tf.sort()代替内置的sorted()函数,以此类推。

    • 如果定义了一个返回np.random.rand()函数的TF函数f(x),则仅在跟踪该函数时才会生成随机数,因此f(tf.constant(2.))和f(tf.constant(3.))将返回相同的随机数,但f(tf.constant([2.,3.]))将返回不同的随机数。如果把np.random.rand()替换为tf.random.uniform([]),则每次操作都会生成一个新的随机数,因为该操作将成为图形的一部分
    • 如果非TensorFlow代码具有副作用(例如记录某些内容或更新Python计数器),那么每次调用TF函数不会发生这些副作用,因为它们只会在跟踪该函数时发生
    • 可以在tf.py_function()操作中包装任何Python代码,但这样会降低性能,因为TensorFlow无法对此代码进行任何图优化。这也会降低可移植性,因为该图仅可在安装了Python(并且安装了正确的库)的平台上运行
  • 可以调用其他Python函数或TF函数,但它们应遵循相同的规则,因为TensorFlow会在计算图中捕捉它们的操作。这些函数不需要@tf.function修饰。
  • 如果该函数创建了一个TensorFlow变量(或任何其他有状态的TensorFlow对象,例如数据集或队列),则必须在第一次调用时这样做,否则会得到一个异常。通常最好在TF函数(例如在自定义层的build()方法中)外部创建变量。如果要为变量分配一个新值,确保调用它的assign方法,而不要使用=运算符
  • Python函数的源代码应可用于TensorFlow。如果源代码不可用(如果,如果在Python shell中定义函数,而该函数不提供对源代码的访问权,或者仅将已编译的*.pyc Python文件部署到环境中),则生成图的过程会失败或功能受限。
  • TensorFlow只能捕获在张量或数据集上迭代的for循环,。因此确保使用for i in tf.range(x),而不是for i in range(x),否则这个循环不会在图中被捕捉。相反它会在跟踪过程中运行。
  • 出于性能原因,应尽可能使用向量化实现,而不是使用循环。

自定义损失函数

当训练回归模型时,训练集有噪声,均方误差MSE可能会对大误差惩罚太多而导致模型不精确。平均绝对误差MAE不会对异常值惩罚太多,但是训练可能要一段时间才能收敛,并且训练后的模型可能不太精确。这就是使用Huber损失而不是旧的MSE损失的好时机。

$$ L_\delta(y,f(x)) = \left\{ \begin{aligned} \frac{1}{2}(y-f(x))^2, \quad &|y-f(x)|\le\delta\\ \delta|y-f(x)|-\frac{1}{2}\delta^2, \quad &|y-f(x)|>\delta \end{aligned} \right. $$

import tensorflow as tf
from tensorflow import keras
# 当delta=1时的huber损失定义


def huber_fn(y_true, y_pred):
    error = y_true-y_pred
    is_small_error = tf.abs(error) < 1
    square_loss = tf.square(error)/2
    linear_loss = tf.abs(error)-.5
    return tf.where(is_small_error, squared_loss, linear_loss)

可以在编译Keras模型时候使用此损失,然后训练模型:

model.compile(loss=huber_fn, optimizer='nadam')
model.fit(X_train, y_train)

保存和加载包含自定义组件的模型

保存包含自定义损失函数的模型效果很好。因为Keras会保存函数的名称。每次加载时都需要提供一个字典,将函数名称映射到实际函数。一般而言,当加载包含自定义对象的模型是需要将名称映射到对象:

model = keras.models.load_model(
    'my_model_with_a_custom_loss.h5', custom_objects={'huber_fn': huber_fn})

对于Huber损失需要一个不同的delta值可以创建一个函数,该函数创建已配置的损失函数:

def create_huber(threshold=1.0):
    def huber_fn(y_true, y_pred):
        error = y_true-y_pred
        is_small_error = tf.abs(error) < threshold
        squared_loss = tf.quare(error)/2
        linear_loss = threshold*tf.abs(error)-threshold**2/2
        return tf.where(is_small_error, squared_loss, linear_loss)
    return huber_fn

当保存模型时,阈值不会被保存。这意味着在加载模型时必须指定阈值。使用的名称是"huber_fn",这是为Keras命名的的函数的名称,而不是创建函数时的名称:

model = keras.models.load_model('my_model_with_custom_loss_threshold_2.h5',
                                custom_objects={'huber_fn': create_huber(2.0)})

也可以通过创建keras.losses.Loss类的子类,然后是实现其get_config()方法来解决此问题

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
        is_small_error = tf.abs(error) < self.threshold
        squared_loss = tf.square(error)/2
        linear_loss = self.threshold*tf.abs(error)-self.threshold**2/2
        return tf.where(is_small_error, squared_loss, linear_loss)

    def get_config(self):
        base_config = super().get_config()
        return {**base_config, 'threshold': self.threshold}
  • 构造函数接受**kwargs并把它们传递给父类构建函数,该父类构造函数处理标准超参数:超参数:损失的name和用于聚合单个实例损失的reduction算法。默认情况下,它是"sum_over_batch_size",这意味着损失将是实例损失的总和,由样本权重(如果有)加权,再除以批量大小(而不是权重之和,因此不是权重的平均)。其中可能的值为"sum"和"none"
  • call()方法获取标签和预测,计算所有实例损失,然后将其返回
  • get_config()方法返回一个字典,将每个超参数名称映射到其值。它首先调用父类的get_config()方法,然后将新的超参数添加到此字典中
# 可以在编译模型时使用此类的任何实例
model.compile(loss=HuberLoss(2.), optimizer='nadam')
# 当保存模型时,阈值会同时一起保存。在加载模型时,只需要将类名称映射到类本身即可:
model = keras.models.load_model(
    'my_model_with_a_custom_loss_class.h5', custom_objects={'HuberLoss': HuberLoss})

自定义激活函数、初始化、正则化和约束

def my_softplus(z):  # 自定义softplus激活函数,相当于keras.activations.softplus()
    return tf.math.log(tf.exp(z)+1.0)


# 自定义Glorot初始化,相当于keras.initializers.glorot_normal()
def my_glorot_initializer(shape, dtype=tf.float32):
    # 计算方差,fan_{avg}=(fan_{in}+fan{out}/2,/sigma^2=1/fan_{avg}
    stddev = tf.sqrt(2./(shape[0]+shape[1]))
    return tf.random.normal(shape, stddev=stddev, dtype=dtype)


def my_l1_regularizer(weights):  # 自定义l1正则化,相当于keras.regularizers.l1(0.01)
    return tf.reduce_sum(tf.abs(0.01*weights))


def my_positive_weights(weights):  # 确保权重为正的自定义约束,等同于keras.contraints.nonneg()
    # 权重为负则返回0,非负则不
    return tf.where(weights < 0., tf.zeros_like(weights), weights)


# 参数取决于自定义函数的类型,然后可以正常使用这些自定义函数
layer = keras.layers.Dense(30, activation=my_softplus,
                           kernel_initializer=my_glorot_initializer,
                           kernel_regularizer=my_l1_regularizer,
                           kernel_constraint=my_positive_weights)
'''激活函数将应用于此Dense层的输出,将结果传递到下一层。层的权重将使用初始化程序返回的值初始化。
在每个训练步骤中,权重将传递给正则化函数以计算正则化损失,并将其添加到主要损失中以得到用于训练的最终损失。
最后在每个步骤之后将调用约束函数,并将层的权重替换为约束权重。'''
'激活函数将应用于此Dense层的输出,将结果传递到下一层。层的权重将使用初始化程序返回的值初始化。\n在每个训练步骤中,权重将传递给正则化函数以计算正则化损失,并将其添加到主要损失中以得到用于训练的最终损失。\n最后在每个步骤之后将调用约束函数,并将层的权重替换为约束权重。'


如果函数具有需要和模型一起保存的超参数,需要继承适当的类,例如keras.regularizers.Regularizer,keras.constraints.Constraint,keras.initializers.Initializer或keras.layers.Layer(适用于任何层,包含激活函数)。就像为自定义损失一样,这是一个用于$\ell_1$正则化的简单类,它保存了其factor超参数(这次不用调用父类构造函数或get_config()方法,因为它不是由父类定义的):

class MylL1Regularizer(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}

必须为损失、层(包括激活函数)和模型实现call()方法,或者为正则化、初始化和约束实现__call__()方法。

自定义指标

损失和指标在概念上不是一回事:损失(例如交叉熵)被梯度下降用来训练模型,因此它们必须是可微的(至少是在求值的地方),并且梯度在任何地方都不应该为0。另外,如果人类不容易解释它们也没有问题。相反,指标(例如准确率)用于评估模型,它们必须更容易被解释,并且可以是不可微的或在各处具有0梯度。

也就是说,在大多数情况下,定义一个自定义指标函数与定义一个自定义损失函数完全相同。实际上,甚至可以将之前创建的Huber损失函数用作指标。它可以很好地工作(持久性也可以以相同的方式工作,在这种情况下,仅保存函数名"huber_fn"):

model.compile(loss='mse', optimizer='nadam', metrics=[create_huber(2.0)])

对于训练期间的每一批次,Keras都会计算该指标并跟踪自轮次开始以来的均值。大多数时候,这是你想要的,但不总是!例如,考虑一个二分类器的精度。精度是真正的数量除以正预测的数量(包括真正和假正)。假设该模型在第一批次中做出了5个正预测,其中4个是正确的,即80%的精度。然后假设该模型在第二批次中做出了3个正预测,但它们都是不正确的,即第二批次的精度为0%。如果仅计算这两个精度的均值,则可以得到40%。但是,这不是模型在这两个批次上的精度。实际上,在8个正预测(5+3)中,总共有4个真正(4+0),因此总体精度为50%,而不是40%。需要的是一个对象,该对象可以跟踪真正的数量和假正的数量,并且可以在请求时计算其比率。这真是keras.metrics.Precision类要做的事情:

precision = keras.metrics.Precision()
precision([0, 1, 1, 1, 0, 1, 0, 1], [1, 1, 0, 1, 0, 1, 0, 1])
<tf.Tensor: shape=(), dtype=float32, numpy=0.8>



precision([0, 1, 0, 0, 1, 0, 1, 1], [1, 0, 1, 1, 0, 0, 0, 0])
<tf.Tensor: shape=(), dtype=float32, numpy=0.5>



precision.result()
<tf.Tensor: shape=(), dtype=float32, numpy=0.5>



precision.variables
[<tf.Variable 'true_positives:0' shape=(1,) dtype=float32, numpy=array([4.], dtype=float32)>,
 <tf.Variable 'false_positives:0' shape=(1,) dtype=float32, numpy=array([4.], dtype=float32)>]



precision.reset_states() # 所有预测值重置为0
class HuberMetrics(keras.metrics.Metric):
    def __init__(self, threshold=1.0, **kwargs):
        super().__init__(**kwargs)
        self.threshold = threshold
        self.huber_fn = create_huber(threshold)
        self.total = seld.add_weight('total', initializer='zeros')
        self.count = self.add_weight('count', initializer='zeros')

    def update_state(self, y_true, y_pred, sample_weight=None):
        metric = self.huber_fn(y_true, y_pred)
        self.total.assign_add(tf.reduce_sum(metric))
        self.count.assign_add(tf.cast(tf.size(y_true), 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}
  • 构造函数使用add_weight()方法创建用于跟踪多个批次和度量状态所需的变量,在这种情况下,该变量包括Huber损失的总和(总计)以及到目前为止看到的实例数(计数)。如果愿意,你可以手动创建变量。Keras会跟踪任何设置为属性的tf.Variable(更一般而言,是任何“可跟踪”的对象,例如层或模型)
  • 使用此类的实例作为函数时,将调用update_state()方法。给定一个批次的标签和预测值(以及采样权重,但这个示例中忽略它们),它会更新变量
  • result()方法计算并返回最终结果,在这种情况下为所有实例的平均Huber度量。当使用度量作为函数时,首先调用update_state()方法,然后调用result()方法,并返回其输出
  • 这里还实现了get_config()方法来确保threshold与模型一起被保存
  • reset_states()方法的默认实现将所有变量重置为0.0(但是可以根据要求覆盖它)

自定义层

某些层没有权重,如Keras.layers.Flatten或keras.layers.ReLU。如果要创建不带任何权重的自定义层,最简单的选择是编写一个函数并将其包装在keras.layers.Lambda层中。例如,以下层是对它的输入指数函数:

exponential_layer = keras.layers.Lambda(lambda x: tf.exp(x))

然后可以像使用顺序API、函数API或子类API等其他任何层一样使用此自定义层。也可以将其当作激活函数(或者可以使用activation=tf.exp、activation=keras.activations.exponential或仅使用activation='exponential')。当要预测的值具有非常不同的标度(例如0.01、10、1000)时,有时会在回归模型的输出层中使用指数层

要构建自定义的有状态层(即具有权重的层),需要创建keras.layers.Layer类的子类。例如以下类实现了Dense层的简化版本

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='bias', shape=[batch_input_shape[-1], self.units],
            initializer='glorot_normal')
        self.bias = self.add_weight(
            name='bias', shape=[self.units], initializer='zeros')
        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)}
  • 构造函数将所有超参数作为参数(在此示例中为units和activation),重要的是它还接受**kwargs参数。它调用父类构造函数,并将其传递给kwargs:这负责处理标准参数例如input_shape、trainable和name。然后,它将超参数保存为属性,使用keras.activations.get()函数将激活函数转换为适当的激活函数(它接受函数、标准字符串(如"relu"或"selu",或None))
  • build()方法的作用是通过为每个权重调用add_weight()方法来创建层的变量。首次使用该层时,将调用build()方法,这对于创建某些权重通常是必需的。例如,需要知道上一层中神经元的数量,以便创建连接权重矩阵(即"kernel"):这对应输出的最后维度的大小。在build()方法的最后(并且在最后),必须调用父类的build()方法:这告诉Keras这一层被构建了(它只是设置了self.built=True)
  • call()方法执行所需操作。在这种情况下,计算输出X与层内核的矩阵乘积,添加偏执向量,并将结果应用于激活函数,从而获得层的输出
  • compute_output_shape()方法仅返回该层的输出形状。在这种情况下,它的形状与输入的形状相同,只是最后一个维度被替换为该层中的神经元的数量。在tf.keras中,形状是tf.TensorShape类的实例,可以通过as_list()将其转换为Python列表
  • get_config()方法就像之前的自定义类一样。通过调用keras.actiavtions.serialize()保存激活函数的完整配置

要创建一个具有多个输如(例如Concatenate)的层,call()方法的参数应该是包含所有输入的元组,而同样,compute_output_shape()方法的参数应该是一个包含每个输入的批处理形状的元组。要创建具有多个输出的层,call()方法应返回输出列表,而compute_output_shape()应返回批处理输出形状的列表(每个输出一个)。例如,一下这个层需要两个输入并返回三个输出:

class MyMultiLayer(keras.layers.Layer):
    def call(self, X):
        X1, X2 = X
        return [X1+X2, X1*X2, X1/X2]

    def compute_output_shape(self, batch_input_shape):
        b1, b2 = batch_input_shape
        return [b1, b1, b1]

如果层在训练期间和测试期间需要具有不用的行为(例如使用Dorpout或BatchNormalization层),则必须将训练参数添加到call()方法并使用此参数来决定要做什么。下面创建一个在训练期间(用于正则化)添加高斯噪声但在测试期间不执行任何操作的层(Keras具有相同功能的层:keras.layers.GaussianNoise):

class MyGaussianNoise(keras.layers.Layer):
    def __init__(self, stddev, **kwargs):
        super().__init__(**kwargs)
        self.stddev = stddev

    def call(self, x, training=None):
        if training:
            noise = tf.random.normal(tf.shape(x), stddev=self.stddev)
            return x+noise
        else:
            return x

    def compute_output_shape(x, batch_input_shape):
        return batch_input_shape

自定义模型

定义一个输入经过第一个密集层,然后经过由两个密集层组成的残差快并执行加法运算,然后经过相同的残差块3次或者更多次,然后通过第二个残差块,最终结果经过密集输出层。要实现此模型,首先最好创建一个ResidualBolck层,因为将创建几个相同的块

class ResidualBlock(keras.layers.Layer):
    def __init__(self, n_layers, n_neurons, **kwargs):
        super().__init__(**kwargs)
        self.hidden = [keras.layers.Dense(
            n_neurons, 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

该层有点特殊,因为它包含其他层。这由Keras透明地处理:它会自动检查到隐藏的属性,该属性包含可跟踪的对昂,因此它们的变量会自动添加到该层的变量列表中。接下来使用子类API定义模型本身

class ResidualRegressor(keras.Model):
    def __init__(self, output_dim, **kwargs):
        super().__init__(**kwargs)
        self.hidden = 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(output_dim)

    def call(self, inputs):
        Z = self.hidden1(inputs)
        for _ in range(1+3):
            Z = self.block1(Z)
        Z = self.block2(Z)
        return self.out(Z)

在构造函数中创建层,并在call()方法中使用它们。然后就可以像使用任何其他模型一样使用此模型(对其进行编译、拟合、评估和预测)。如果希望能够使用save()方法保存模型并使用keras.models.load_model()函数加载模型,则必须在两个ResidualBlock类和ResidualRegressor类中都实现get_config()方法。另外可以使用save_weights()和load_weights()方法保存和加载权重

Model类是Layer类的子类,因此可以像定义层一样定义和使用模型。但是模型具有一些额外的功能,包括其compile()、fit()、evaluate()和predict()方法(以及一些变体)以及get_layers()方法(可以按名称或按索引返回任何模型的层)和save()方法(支持keras.models.load_model()和keras.models.clone_model())

基于模型内部的损失和指标

有时候需要根据模型的其他部分来定义损失,例如权重或隐藏层的激活。这对于正则化或监视模型的某些内部方面可能很有用

要基于模型内部定义自定义损失,根据所需模型的任何部分进行计算,然后将结果传递给add_loss()方法。例如,构建一个自定义回归MLP模型,该模型由5个隐藏层和1个输出层的堆栈组成。此自定义模型还将在上部隐藏层的顶部有辅助输出。与该辅助输出相关的损失称为重建损失:它是重建与输入之间的均方差。通过将这种重建损失添加到主要损失中,鼓励模型通过隐藏层保留尽可能多的信息,即使对于回归任务本身没有直接用处的信息。实际上,这种损失有时会提高泛化性(这是正则化损失)。以下是带有自定义损失的自定义模型的代码:

class ReconstructingRegressor(keras.Model):
    def __init__(self, out_dim, **kwargs):
        super().__init__(**kwargs)
        self.hidden = [keras.layers.Dense(
            30, activation='elu', kernel_initialzer='lecun_normal')for _ in range(5)]
        self.out = keras.layers.Dense(output_dim)

    def build(self, batch_input_shape):
        n_inputs = batch_input_shape[-1]
        self.recontruct = keras.layers.Dense(n_inputs)
        super().build(batch_input_shape)

    def call(self, inputs):
        Z = inputs
        for layer in self.hidden:
            Z = layer(Z)
        reconstruction = self.reconstruct(Z)
        recon_loss = tf.reduce_mean(tf.square(rescontruction-inputs))
        self.add_loss(0.05*recon_loss)
        return self.out(Z)
  • 构建函数创建具有5个密集隐藏层和一个密集输出层的DNN
  • build()方法创建一个额外的密集层,该层用于重建模型的输入。必须在此处创建它,因为它的单元数必须等于输入数,并且在调用build()方法之前,此数是未知的
  • call()方法处理所有5个隐藏层的输入,然后将结果传递到重建层,从而产生重构
  • 然后call()方法计算重建损失(重建与输入之间的均方差),并使用add_loss()方法将其添加到模型的损失列表中。通过将其乘以0.05(可以调整的超参数)按比例缩小的重建。这确保了重建损失不会在主要损失中占大部分。
  • 最后,call()方法将隐藏层的输出传递到输出层并返回其输出

使用自动微分计算梯度

# 首先考虑一个简单的函数
def f(w1, w2):
    return 3*w1**2+2*w1*w2
# 该函数关于w1的偏导数为6*w1+2*w2
# 该函数关于w2的偏导数为2*w1
# 例如在点(w1,w2)=(5,3)处,这些偏导数分别等于36和10,因此此点的梯度向量为(36,10)
# 一种解决方案是通过在调整相应参数时测量函数输出的变化来计算每个导数的偏导数的近似值


w1, w2 = 5, 3
eps = 1e-6
print((f(w1+eps, w2)-f(w1, w2))/eps)
print((f(w1, w2+eps)-f(w1, w2))/eps)
36.000003007075065
10.000000003174137

这工作得很好并且易于实现,但这只是一个近似值,重要的是每个参数至少要调用一次f()(不是两次,因为只计算一次f(w1,w2))。每个参数至少需要调用f()一次,这种方法对于大型神经网络来说很棘手。因此,应该使用自动微分。TensorFlow使这个变得非常简单

import tensorflow as  tf
w1, w2 = tf.Variable(5.), tf.Variable(3.)
with tf.GradientTape() as tape:
    z = f(w1, w2)
gradients = tape.gradient(z, [w1, w2])
gradients
[<tf.Tensor: shape=(), dtype=float32, numpy=36.0>,
 <tf.Tensor: shape=(), dtype=float32, numpy=10.0>]


结果不仅是准确的(精度仅受浮点误差限制),而且无论有多少变量,gradient()方法都只经历以此计算(以相反的顺序),而且它非常有效。就像魔术一样

为了节省内存,仅将严格的最小值放入tf.GradientTape()块中。或者通过在tf.GradientTape()块中使用tape.stop_recording()块来暂停记录

调用tape的gradient()方法后,tape会立即被自动擦除,因此如果尝试两次调用gradient(),则会出现异常

with tf.GradientTape() as tape:
    z=f(w1,w2)
dz_w1=tape.gradient(z,w1)
dz_w2=tape.gradient(z,w2)
---------------------------------------------------------------------------

RuntimeError                              Traceback (most recent call last)

~\AppData\Local\Temp/ipykernel_4932/1289662103.py in <module>
      2     z=f(w1,w2)
      3 dz_w1=tape.gradient(z,w1)
----> 4 dz_w2=tape.gradient(z,w2)


C:\ProgramData\Miniconda3\envs\tf2\lib\site-packages\tensorflow\python\eager\backprop.py in gradient(self, target, sources, output_gradients, unconnected_gradients)
   1030     """
   1031     if self._tape is None:
-> 1032       raise RuntimeError("A non-persistent GradientTape can only be used to "
   1033                          "compute one set of gradients (or jacobians)")
   1034     if self._recording:


RuntimeError: A non-persistent GradientTape can only be used to compute one set of gradients (or jacobians)

如果需要多次调用gradient(),则必须使该tape具有持久性,并在每次使用完该tape后将其删除以释放资源

with tf.GradientTape(persistent=True) as tape:
    z=f(w1,w2)
dz_dw1=tape.gradient(z,w1) # => tensor 36.0
dz_dw2=tape.gradient(z,w2) # => tensor 10.0
del tape

默认情况下,tape仅跟踪设计变量的操作,因此如果尝试针对变量以外的任何其他变量计算z的梯度,则结果将为None

c1, c2 = tf.constant(5.), tf.constant(3.)
with tf.GradientTape() as tape:
    z = f(c1, c2)
gradient = tape.gradient(z, [c1, c2])
gradient
[None, None]


但是,可以强制tape观察你喜欢的任何张量,来记录设计它们的所有操作,然后可以针对这些张量计算梯度,就好像它们是变量一样

with tf.GradientTape() as tape:
    tape.watch(c1)
    tape.watch(c2)
    z = f(c1, c2)
gradient = tape.gradient(z, [c1, c2])
gradient
[<tf.Tensor: shape=(), dtype=float32, numpy=36.0>,
 <tf.Tensor: shape=(), dtype=float32, numpy=10.0>]


在某些情况下,这可能有用1,如果要实现正则化损失,从而在输出变化不大时惩罚那些变化很大的激活:损失将基于激活相对于输入的梯度而定。由于输入不是变量,因此你需要告诉tape观察它们

大多数情况下,一个梯度tape是用来计算单个值(通常是损失)相对于一组值(通常是模型参数)的梯度。这就是反向模式自动微分有用的地方,因为它只需要执行一次正向传播和一次反向传播即可获得所有梯度。如果尝试计算向量的梯度,例如包含多个损失的向量,那么Tensor Flow将计算向量和梯度。因此如果需要获取单独的梯度(例如每种损失相对于模型的梯度),则必须调用tape的jacobian()方法:它对向量中的每个损失执行一次反向模式自动微分(默认情况下全部并行)。它甚至可以计算二阶偏导数(Hessian,即偏导数的偏导数),但实际上很少需要

在某些情况下,可能希望阻止梯度在神经网络的某些部分反向传播。为此必须使用tf.stop_gradient()函数。该函数在向前传递过程中返回其输入(如tf.identity()),但在反向传播期间不让梯度通过(它的作用类似于常量)

def f(w1,w2):
    return 3*w1**2+tf.stop_gradient(2*w1*w2)
with tf.GradientTape() as tape:
    z=f(w1,w2)
gradients=tape.gradient(z,[w1,w2])
gradients
[<tf.Tensor: shape=(), dtype=float32, numpy=30.0>, None]


最后在计算梯度时,可能会遇到一些数值问题。如果用大数值输入来计算my_softplus()函数的梯度,则结果为NaN:

x = tf.Variable([100.])
with tf.GradientTape() as tape:
    z = my_softplus(x)
tape.gradient(z, [x])
[<tf.Tensor: shape=(1,), dtype=float32, numpy=array([nan], dtype=float32)>]


这是因为使用自动微分计算此函数的梯度值会导致一些数值上的困难:由于浮点精度误差,自动微分最终导致无法计算除以无穷(返回NaN)。幸运的是,可以分析发现softplus的导数为$\frac{1}{1+\frac{1}{e^{x}}}$,在数值上是稳定的。接下来可以告诉TensorFlow在计算my_softplus()函数的梯度时使用@tf.custom_gradient来修饰它并使它既返回正常输出又返回计算导数的函数(它将接收到目前为止反向传播的梯度,直到softplus函数。根据链式法则,应该将它们乘以该函数的梯度):

@tf.custom_gradient
def my_better_softplus(z):
    exp=tf.exp(z)
    def my_softplus_gradient(grad):
        return grad/(1+1/exp)
    return tf.math.log(exp+1),my_softplus_gradients

现在计算my_better_softplus()函数的梯度时,即使对于较大的输入值,我们也可以获得正确的结果(但是由于指数的关系,主要输出仍然会爆炸。一种解决方法是使用tf.where()在输入较大时返回输入)

自定义训练循环

在极少数情况等下,fit()方法可能不够灵活而无法满足需要。例如在前面讨论的使用两种不同的优化器:一种用于宽路径,另一种用于深路径。由于fit()方法只是用一个优化器(编译模型时指定的优化器),因此实现该论文需要编写自定义循环

编写自定义训练循环会使得代码更长、更容易出错并且更难以维护

除非真的需要额外的灵活性,否则应该更倾向于使用fit()方法,而不是实现自己的训练循环,尤其是在团队合作中

首先,建立一个简单的模型,无需编译,因为将手动处理训练循环:

l2_reg = keras.regularizers.l2(.05)
model = keras.models.Sequential([keras.layers.Dense(30, activation='elu', kernel_initializer='he_normal', kernel_regularizer=l2_reg),
                                keras.layers.Dense(1, kernel_regularizer=l2_reg)])

接下来,创建一个小函数,从训练集中随机采样一批实例:

def random_batch(X, y, batch_size=32):
    idx = np.ranodm.randint(len(X), size=batch_size)
    return X[idx], y[idx]

定义一个函数,显示训练状态,包括步数、步总数、从轮次开始以来的平均损失(即使用Mean指标来计算),和其他指标:

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(iteration, total)+metrics, end=end)

首先需要自定义一些超参数,然后选择优化器、损失函数和指标(这里使用MAE)

n_epochs=5
batch_size=32
n_steps=len(X_train)//batch_size
optimizer=keras.optimizers.Nadam(lr=.01)
loss_fn=keras.losses.mean_squared_error
mean_loss=keras.metrics.Mean()
metrics=[keras.metrics.MeanAbsoluteError()]

构建自定义循环:

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,training=True)
            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))
        mean_loss(loss)
        for metric in metrics:
            metric(y_batch,y_pred)
        print_status_bar(step*batch_size,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针对每个可训练变量计算损失的梯度,然后用优化器来执行梯度下降步骤
  • 然后,更新平均损失和指标(在当前轮次),并显示状态栏
  • 在每个轮次结束时,再次显示状态栏使其看起来完整并打印换行符,然后重置平均损失和指标的状态

如果设置优化器的超参数clipnorm或clipvalue,它会解决这一问题。如果要对梯度应用任何其他变换,只需调用apply_gradients()方法之前进行即可

如果要对模型添加权重约束(例如在创建层时设置kernel_constraint或bias_constraint),则应更新训练循环以在apply_gradients()之后应用这些约束:

for variable in model.variables:
    if variable.constraint is not None:
        variable.assign(variable.constraint(variable))

最重要的时,此训练循环不会处理在训练期间和测试期间行为不同的层(例如BatchNormalization或Dropout)。需要处理这些问题,需要使用training=True调用模型,确保将其传播到需要它的层

张量和操作

可以使用tf.constant()创建张量

from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity='all'
import tensorflow as tf
tf.constant([[1., 2., 3.], [4., 5., 6.]])
<tf.Tensor: shape=(2, 3), dtype=float32, numpy=
array([[1., 2., 3.],
       [4., 5., 6.]], dtype=float32)>

像 ndarray 一样,tf.Tensor 具有形状和数据类型(dtype):

t = tf.constant([[1., 2., 3.], [4., 5., 6.]])
t.shape
TensorShape([2, 3])
t.dtype
tf.float32

索引工作的方式也类似于NumPy

t[:, 1:]
<tf.Tensor: shape=(2, 2), dtype=float32, numpy=
array([[2., 3.],
       [5., 6.]], dtype=float32)>
t[..., 1]
<tf.Tensor: shape=(2,), dtype=float32, numpy=array([2., 5.], dtype=float32)>
t[..., 1, tf.newaxis]  # 提取所有行,第一列的数据并增加一个维度 所以形状是(2,1),上面没有增加新的维度形状就是(2,)
<tf.Tensor: shape=(2, 1), dtype=float32, numpy=
array([[2.],
       [5.]], dtype=float32)>

可以使用各种张量操作

t+10
<tf.Tensor: shape=(2, 3), dtype=float32, numpy=
array([[11., 12., 13.],
       [14., 15., 16.]], dtype=float32)>
tf.square(t)
<tf.Tensor: shape=(2, 3), dtype=float32, numpy=
array([[ 1.,  4.,  9.],
       [16., 25., 36.]], dtype=float32)>
t@tf.transpose(t) # @运算符是矩阵乘法,相当于tf.matmul()函数
<tf.Tensor: shape=(2, 2), dtype=float32, numpy=
array([[14., 32.],
       [32., 77.]], dtype=float32)>

张量和 NumPy

张量可以与NumPy配合使用:可以使用NumPy数组创建张量,也可以将TensorFlow操作应用于NumPy数组,将NumPy操作应用于张量:

import numpy as np
a=np.array([2.,4.,5.])
tf.constant(a)
<tf.Tensor: shape=(3,), dtype=float64, numpy=array([2., 4., 5.])>
t.numpy()
array([[1., 2., 3.],
       [4., 5., 6.]], dtype=float32)
tf.square(a)
<tf.Tensor: shape=(3,), dtype=float64, numpy=array([ 4., 16., 25.])>
np.square(a)
array([ 4., 16., 25.])

类型转换

类型转换会严重影响性能,并且自动完成转换很容易被忽视。为了避免这种情况TensorFlow不会自动执行任何类型转换:如果对不兼容类型的张量执行操作,会引发异常。例如:不能把浮点张量和整数张量相加,甚至不能相加32位浮点和64位浮点

tf.constant(2.0)+tf.constant(40)
---------------------------------------------------------------------------

InvalidArgumentError                      Traceback (most recent call last)

~\AppData\Local\Temp/ipykernel_12252/3002758296.py in <module>
----> 1 tf.constant(2.0)+tf.constant(40)


InvalidArgumentError: cannot compute AddV2 as input #1(zero-based) was expected to be a float tensor but is a int32 tensor [Op:AddV2]


tf.constant(2.0)+tf.constant(40,dtype=tf.float64)
---------------------------------------------------------------------------

InvalidArgumentError                      Traceback (most recent call last)

~\AppData\Local\Temp/ipykernel_12252/2939420824.py in <module>
----> 1 tf.constant(2.0)+tf.constant(40,dtype=tf.float64)
InvalidArgumentError: cannot compute AddV2 as input #1(zero-based) was expected to be a float tensor but is a double tensor [Op:AddV2]

需要转换类型时,可以使用tf.cast():

t2=tf.constant(40.,dtype=tf.float64)
tf.constant(2.)+tf.cast(t2,tf.float32)
<tf.Tensor: shape=(), dtype=float32, numpy=42.0>

变量

目前看到的tf.Tensor值是不变的,无法修改它们。不能使用常规张量在神经网络中实现权重,因为它们需要通过反向传播进行调整。另外还可能需要随时间改变其他参数(例如动量优化器跟踪过去的梯度)。这时就需要使用tf.Variable:

v=tf.Variable([[1.,2.,3.],[4.,5.,6.]])
v
<tf.Variable 'Variable:0' shape=(2, 3) dtype=float32, numpy=
array([[1., 2., 3.],
       [4., 5., 6.]], dtype=float32)>

tf.Variable的行为和tf.Tensor的行为非常相似:可以使用它执行相同的操作,它在NumPy中也可以很好地发挥作用,并且对类型也很挑剔。但是可以使用assign()方法(或assign_add()或assign_sub(),给变量增加或减少给定值)进行修改。还可以使用切片的的assign()方法或使用scatter_update()或scatter_nd_update()方法来修改单个切片

v.assign(2*v)
v[0,1].assign(42)
v[:,2].assign([0.,1.])
v.scatter_nd_update(indices=[[0,0],[1,2]],updates=[100.,200.])
<tf.Variable 'UnreadVariable' shape=(2, 3) dtype=float32, numpy=
array([[ 2.,  4.,  6.],
       [ 8., 10., 12.]], dtype=float32)>
<tf.Variable 'UnreadVariable' shape=(2, 3) dtype=float32, numpy=
array([[ 2., 42.,  6.],
       [ 8., 10., 12.]], dtype=float32)>
<tf.Variable 'UnreadVariable' shape=(2, 3) dtype=float32, numpy=
array([[ 2., 42.,  0.],
       [ 8., 10.,  1.]], dtype=float32)>
<tf.Variable 'UnreadVariable' shape=(2, 3) dtype=float32, numpy=
array([[100.,  42.,   0.],
       [  8.,  10., 200.]], dtype=float32)>

其他数据结构

  • 稀疏张量(tf.SparseTensor)
    有效地表示主要包含零的张量。tf.sparse程序包包含稀疏张量的操作
  • 张量数组(tf.TensorArray)
    张量的列表。默认情况下,它们的大小是固定的,但可以选择动态设置。它们包含的所有张量必须具有相同的形状和数据类型
  • 不规则张量(tf.RaggedTensor)
    表示张量列表的静态列表,其中每个张量具有相同的形状和数据类型。tf.ragged程序包包含用于不规则的张量的操作
  • 字符串张量
    tf.string类型的常规张量。它表示字节字符串,而不是Unicode字符串,因此如果使用Unicode字符串创建字符串张量,则它将被自动编码为UTF-8。或者,可以使用类型为tf.int32的张量来表示Unicode字符串,其中每个项都表示一个Unicode代码点。tf.strings包包含用于字节字符串和Unicode字符串的操作(并将转换为另一个)。重要的是要注意,tf.string是原子级的,这意味着它的长度不会出现在张量的形状当中。一旦将其转换为Unicode张量(即包含Unicode代码点的tf.int32l类型的张量)后,长度就会显示在形状中
  • 集合
    表示为常见张量(或稀疏张量)。例如,tf.constant([[1,2.],[3.4]])代表两个集合{1,2}和{3,4}。通常,每个集合由张量的最后一个轴上的向量表示。可以使用tf.sets包中的操作来操作集。
  • 队列
    跨多个步骤存储的张量。TenorFlow提供了各种队列:简单的先进先出(FIFO)队列(FIFOQueue),可以区分某些元素优先级的队列(Priority Queue),将其元素(RandomShuffleQueue)随机排列,通过填充(PaddingFIFOQueue)批量处理具有不同形状的元素。这些类都在tf.queue包中。

梯度下降

线性模型

$\hat{y}=x*w$来拟合学习时间$x$与考试分数$y$

$$ loss=(\hat{y}-y)^2=(x*w-y)^2\to cost=\frac1N\sum^N_{n=1}(\hat{y_n}-y_n)^2 $$

由图可知损失函数在$w=2.0$时,取得最小值。记损失函数在$w^*$处取得最小值

$$ w^*=\arg_w min\ cost(w) $$

假定w的初始位置在Initial Guess处,在这点处的梯度为 $\frac{\partial cost}{\partial w}$,此时梯度为正数,沿着梯度方向$cost$变化最快,可以通过沿梯度方向变化最快方向来更新$w$,通过公式:
$$w = w - \alpha \frac{\partial \text{cost}}{\partial w} $$
来更新 $w$。$\alpha$ 为学习率,也可以理解成沿梯度下降方向迈出步伐的大小。
$$ \frac{\partial \text{cost}(\omega)}{\partial\omega} = \frac{\partial}{\partial\omega}\frac{1}{N}\sum^N_{n=1}(x_n\omega-y_n)^2 $$

$$ = \frac{1}{N}\sum^N_{n=1}\frac{\partial}{\partial\omega}(x_n\omega-y_n)^2 $$

$$ = \frac{1}{N}\sum^N_{n=1}2(x_n\omega-y_n)\frac{\partial(x_n\omega-y_n)}{\partial\omega} $$

$$ = \frac{1}{N}\sum^N_{n=1}2x_n(x_n\omega-y_n) $$

x_data = [1.0, 2.0, 3.0] # 准备数据
y_data = [2.0, 4.0, 6.0]
 
w = 1.0 # 初始化权重w为1.0


def forward(x):
    return x*w # 定义线性模型y=x*w


def cost(xs, ys): # 定义损失函数
    cost = 0
    for x, y in zip(xs, ys):
        y_pred = forward(x)
        cost += (y_pred-y)**2 # 损失等于每个数据的和
    return cost/len(xs) # 除以数据个数来获得均方误差差


def gradient(xs, ys): # 定义梯度函数
    grad = 0
    for x, y in zip(xs, ys):
        grad += 2*x*(x*w-y) # 根据前面的公式计算在该点的梯度
    return grad/len(xs)


print('Predict (before training', 4, forward(4)) # 输出训练之前的x=4和y_pred
for epoch in range(100): # 训练100伦
    cost_val = cost(x_data, y_data) # 计算损失函数
    grad_val = gradient(x_data, y_data) # 计算梯度
    w -= 0.01*grad_val # 通过梯度和学习率来调整权重
    print('Epoch', epoch, 'w=', w, 'loss=', cost_val)
print('Predict (after training)', 4, forward(4)) # 输出训练之后的x=4和y_pred
Epoch 90 w= 1.9998658050763347 loss= 1.0223124683409346e-07
Epoch 91 w= 1.9998783299358769 loss= 8.403862850836479e-08
Epoch 92 w= 1.9998896858085284 loss= 6.908348768398496e-08
Epoch 93 w= 1.9998999817997325 loss= 5.678969725349543e-08
Epoch 94 w= 1.9999093168317574 loss= 4.66836551287917e-08
Epoch 95 w= 1.9999177805941268 loss= 3.8376039345125727e-08
Epoch 96 w= 1.9999254544053418 loss= 3.154680994333735e-08
Epoch 97 w= 1.9999324119941766 loss= 2.593287985380858e-08
Epoch 98 w= 1.9999387202080534 loss= 2.131797981222471e-08
Epoch 99 w= 1.9999444396553017 loss= 1.752432687141379e-08
Predict (after training) 4 7.999777758621207

随机梯度下降(SGD)Stochastic Gradient Descent

$$ w=w-\alpha\frac{\partial cost}{\partial w}\to w=w-\alpha\frac{\partial loss}{\partial w} $$

$$ \frac{\partial cost}{\partial w}=\frac1N\sum_{n=1}^N2\cdot x_n\cdot(x_n\cdot w-y_n)\to\frac{\partial loss_n}{\partial w}=2\cdot x_n\cdot(x_n\cdot w-y_n) $$

def loss(x,y):
    y_pred=forward(x)
    return (y_pred-y)**2

$loss=(\hat{y}-y)^2=(x*w-y)^2$

def graident(x,y):
    return 2*x*(x*w-y)

$\frac{\partial loss_n}{\partial w}=2\cdot x_n\cdot(x_n\cdot w-y_n)$

for epoch in range(100):
    for x,y in zip(x_data,y_data):
        grad=gradient(x,y)
        w=-=0.01*grad
        print('\tgrad:',x,y,grad)
        l=loss(x,y)

更新权重使用训练集每个样本的梯度,而不是梯度的平均