原文:TensorFlow 2.0 Quick Start Guide

协议:CC BY-NC-SA 4.0

译者:飞龙

本文来自【ApacheCN 深度学习 译文集】,采用译后编辑(MTPE)流程来尽可能提升效率。

不要担心自己的形象,只关心如何实现目标。——《原则》,生活原则 2.3.c

第 2 部分:TensorFlow 2.00 Alpha 中的监督和无监督学习

在本节中,我们将首先看到 TensorFlow 在监督机器学习中的许多应用,包括线性回归,逻辑回归和聚类。 然后,我们将研究无监督学习,特别是应用于数据压缩和去噪的自编码。

本节包含以下章节:

  • 第 4 章“TensorFlow 2 和监督机器学习”
  • 第 5 章“Tensorflow 2 和无监督学习”

四、TensorFlow 2 和监督机器学习

在本章中,我们将讨论并举例说明 TensorFlow 2 在以下情况下的监督机器学习问题中的使用:线性回归,逻辑回归和 K 最近邻KNN) 。

在本章中,我们将研究以下主题:

  • 监督学习
  • 线性回归
  • 我们的第一个线性回归示例
  • 波士顿住房数据集
  • 逻辑回归(分类)
  • K 最近邻KNN

监督学习

监督学习是一种机器学习场景,其中一组数据点中的一个或多个数据点与标签关联。 然后,模型学习,以预测看不见的数据点的标签。 为了我们的目的,每个数据点通常都是张量,并与一个标签关联。 在计算机视觉中,有很多受监督的学习问题; 例如,算法显示了许多成熟和未成熟的西红柿的图片,以及表明它们是否成熟的分类标签,并且在训练结束后,该模型能够根据训练集预测未成熟的西红柿的状态。 这可能在番茄的物理分拣机制中有非常直接的应用。 或一种算法,该算法可以在显示许多示例以及它们的性别和年龄之后,学会预测新面孔的性别和年龄。 此外,如果模型已经在许多树图像及其类型标签上进行了训练,则可以学习根据树图像来预测树的类型可能是有益的。

线性回归

线性回归问题是在给定一个或多个其他变量(数据点)的值的情况下,您必须预测一个连续变量的值的问题。 例如,根据房屋的占地面积,预测房屋的售价。 在这些示例中,您可以将已知特征及其关联的标签绘制在简单的线性图上,如熟悉的x, y散点图,并绘制最适合数据的线 。 这就是最适合的系列。 然后,您可以读取对应于该图的x范围内的任何特征值的标签。

但是,线性回归问题可能涉及几个特征,其中使用了术语多个多元线性回归。 在这种情况下,不是最适合数据的线,而是一个平面(两个特征)或一个超平面(两个以上特征)。 在房价示例中,我们可以将房间数量和花园的长度添加到特征中。 有一个著名的数据集,称为波士顿住房数据集,涉及 13 个特征。 考虑到这 13 个特征,此处的回归问题是预测波士顿郊区的房屋中位数。

术语:特征也称为预测变量或自变量。 标签也称为响应变量或因变量。

我们的第一个线性回归示例

我们将从一个简单的,人为的,线性回归问题开始设置场景。 在此问题中,我们构建了一个人工数据集,首先在其中创建,因此知道了我们要拟合的线,但是随后我们将使用 TensorFlow 查找这条线。

我们执行以下操作-在导入和初始化之后,我们进入一个循环。 在此循环内,我们计算总损失(定义为点的数据集y的均方误差)。 然后,我们根据我们的权重和偏置来得出这种损失的导数。 这将产生可用于调整权重和偏差以降低损失的值; 这就是所谓的梯度下降。 通过多次重复此循环(技术上称为周期),我们可以将损失降低到尽可能低的程度,并且可以使用训练有素的模型进行预测。

首先,我们导入所需的模块(回想一下,急切执行是默认的):

 import tensorflow as tf import numpy as np

接下来,我们初始化重要的常量,如下所示:

n_examples = 1000 # number of training examplestraining_steps = 1000 # number of steps we are going to train fordisplay_step = 100 # after multiples of this, we display the losslearning_rate = 0.01 # multiplying factor on gradientsm, c = 6, -5 # gradient and y-intercept of our line, edit these for a different linear problem

给定weightbiasmc)的函数,用于计算预测的y

def train_data(n, m, c):    x = tf.random.normal([n]) # n values taken from a normal distribution,    noise = tf.random.normal([n])# n values taken from a normal distribution    y = m*x + c + noise # our scatter plot    return x, ydef prediction(x, weight, bias):    return weight*x + bias # our predicted (learned) m and c, expression is like y = m*x + c

用于获取初始或预测的权重和偏差并根据y计算均方损失(偏差)的函数:

def loss(x, y, weights, biases):     error = prediction(x, weights, biases) - y # how 'wrong' our predicted (learned) y is    squared_error = tf.square(error)    return tf.reduce_mean(input_tensor=squared_error) # overall mean of squared error, scalar value.

这就是 TensorFlow 发挥作用的地方。 使用名为GradientTape()的类,我们可以编写一个函数来计算相对于weightsbias的损失的导数(梯度):

def grad(x, y, weights, biases):    with tf.GradientTape() as tape:         loss_ = loss(x, y, weights, biases)    return tape.gradient(loss, [weights, bias]) # direction and value of the gradient of our weights and biases

为训练循环设置回归器,并显示初始损失,如下所示:

x, y = train_data(n_examples,m,c) # our training values x and yplt.scatter(x,y)plt.xlabel("x")plt.ylabel("y")plt.title("Figure 1: Training Data")W = tf.Variable(np.random.randn()) # initial, random, value for predicted weight (m)B = tf.Variable(np.random.randn()) # initial, random, value for predicted bias (c)print("Initial loss: {:.3f}".format(loss(x, y, W, B)))

输出如下所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-drCwMCWO-1681568158341)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/tf-20-quick-start-guide/img/99a9971b-03c1-4f4d-b0ee-b3877ad300ab.png)]

接下来,我们的主要训练循环。 这里的想法是根据我们的learning_rate来少量调整weightsbias,以将损失依次降低到我们最适合的线上收敛的点:

for step in range(training_steps): #iterate for each training step     deltaW, deltaB = grad(x, y, W, B) # direction(sign) and value of the gradients of our loss    # with respect to our weights and bias     change_W = deltaW * learning_rate # adjustment amount for weight     change_B = deltaB * learning_rate # adjustment amount for bias     W.assign_sub(change_W) # subract change_W from W     B.assign_sub(change_B) # subract change_B from B     if step==0 or step % display_step == 0:   # print(deltaW.numpy(), deltaB.numpy()) # uncomment if you want to see the gradients  print("Loss at step {:02d}: {:.6f}".format(step, loss(x, y, W, B)))

最终结果如下:

print("Final loss: {:.3f}".format(loss(x, y, W, B)))print("W = {}, B = {}".format(W.numpy(), B.numpy()))print("Compared with m = {:.3f}, c = {:.3f}".format(m, c)," of the original line")xs = np.linspace(-3, 4, 50)ys = W.numpy()*xs + B.numpy()plt.scatter(xs,ys)plt.xlabel("x")plt.ylabel("y")plt.title("Figure 2: Line of Best Fit")

您应该看到,发现WB的值非常接近我们用于mc的值,这是可以预期的:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QZAdxDGR-1681568158342)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/tf-20-quick-start-guide/img/f3445ea2-cea1-4ce6-9028-c8404eed2b8e.png)]

波士顿住房数据集

接下来,我们将类似的回归技术应用于波士顿房屋数据集。

此模型与我们之前的仅具有一个特征的人工数据集之间的主要区别在于,波士顿房屋数据集是真实数据,具有 13 个特征。 这是一个回归问题,因为我们认为房价(即标签)被不断估价。

同样,我们从导入开始,如下所示:

import tensorflow as tffrom sklearn.datasets import load_bostonfrom sklearn.preprocessing import scaleimport numpy as np

我们的重要常数如下所示:

learning_rate = 0.01epochs = 10000display_epoch = epochs//20n_train = 300n_valid = 100

接下来,我们加载数据集并将其分为训练,验证和测试集。 我们在训练集上进行训练,并在验证集上检查和微调我们的训练模型,以确保例如没有过拟合。 然后,我们使用测试集进行最终精度测量,并查看我们的模型在完全看不见的数据上的表现如何。

注意scale方法。 这用于将数据转换为均值为零且单位标准差为零的集合。 sklearn.preprocessing方法scale通过从特征集中的每个数据点减去平均值,然后将每个特征除以该特征集的标准差来实现此目的。

这样做是因为它有助于我们模型的收敛。 所有特征也都转换为float32数据类型:

features, prices = load_boston(True) n_test = len(features) - n_train - n_valid# Keep n_train samples for training train_features = tf.cast(scale(features[:n_train]), dtype=tf.float32)  train_prices = prices[:n_train]# Keep n_valid samples for validation valid_features = tf.cast(scale(features[n_train:n_train+n_valid]), dtype=tf.float32) valid_prices = prices[n_train:n_train+n_valid]# Keep remaining n_test data points as test set) test_features = tf.cast(scale(features[n_train+n_valid:n_train+n_valid+n_test]), dtype=tf.float32)test_prices = prices[n_train + n_valid : n_train + n_valid + n_test]

接下来,我们具有与上一个示例相似的函数。 首先,请注意我们现在使用的是更流行的路径,均方误差:

# A loss function using root mean-squared errordef loss(x, y, weights, bias):  error = prediction(x, weights, bias) - y # how 'wrong' our predicted (learned) y is  squared_error = tf.square(error)  return tf.sqrt(tf.reduce_mean(input_tensor=squared_error)) # squre root of overall mean of squared error.

接下来,我们找到相对于weightsbias的损失梯度的方向和值:

# Find the derivative of loss with respect to weight and biasdef gradient(x, y, weights, bias):  with tf.GradientTape() as tape:    loss_value = loss(x, y, weights, bias)  return tape.gradient(loss_value, [weights, bias])# direction and value of the gradient of our weight and bias

然后,我们查询设备,将初始权重设置为随机值,将bias设置为0,然后打印初始损失。

请注意,W现在是1向量的13,如下所示:

# Start with random values for W and B on the same batch of dataW = tf.Variable(tf.random.normal([13, 1],mean=0.0, stddev=1.0, dtype=tf.float32))B = tf.Variable(tf.zeros(1) , dtype = tf.float32)print(W,B)print("Initial loss: {:.3f}".format(loss(train_features, train_prices,W, B)))

现在,进入我们的主要训练循环。 这里的想法是根据我们的learning_rateweightsbias进行少量调整,以将损失逐步降低至我们已经收敛到最佳拟合线的程度。 如前所述,此技术称为梯度下降

for e in range(epochs): #iterate for each training epoch    deltaW, deltaB = gradient(train_features, train_prices, W, B) # direction (sign) and value of the gradient of our weight and bias    change_W = deltaW * learning_rate # adjustment amount for weight    change_B = deltaB * learning_rate # adjustment amount for bias    W.assign_sub(change_W) # subract from W    B.assign_sub(change_B) # subract from B    if e==0 or e % display_epoch == 0:        # print(deltaW.numpy(), deltaB.numpy()) # uncomment if you want to see the gradients        print("Validation loss after epoch {:02d}: {:.3f}".format(e, loss(valid_features, valid_prices, W, B)))

最后,让我们将实际房价与其预测值进行比较,如下所示:

example_house = 69y = test_prices[example_house]y_pred = prediction(test_features,W.numpy(),B.numpy())[example_house]print("Actual median house value",y," in $10K")print("Predicted median house value ",y_pred.numpy()," in $10K")

逻辑回归(分类)

这类问题的名称令人迷惑,因为正如我们所看到的,回归意味着连续值标签,例如房屋的中位数价格或树的高度。

逻辑回归并非如此。 当您遇到需要逻辑回归的问题时,这意味着标签为categorical; 例如,零或一,TrueFalse,是或否,猫或狗,或者它可以是两个以上的分类值; 例如,红色,蓝色或绿色,或一,二,三,四或五,或给定花的类型。 标签通常具有与之相关的概率; 例如,P(cat = 0.92)P(dog = 0.08)。 因此,逻辑回归也称为分类

在下一个示例中,我们将使用fashion_mnist数据集使用逻辑回归来预测时尚商品的类别。

这里有一些例子:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yNzIft7b-1681568158343)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/tf-20-quick-start-guide/img/b77a5c70-b3e3-4f5c-8495-ac934a9f0da1.png)]

逻辑回归以预测项目类别

我们可以在 50,000 张图像上训练模型,在 10,000 张图像上进行验证,并在另外 10,000 张图像上进行测试。

首先,我们导入建立初始模型和对其进行训练所需的模块,并启用急切的执行:

import numpy as npimport tensorflow as tfimport kerasfrom tensorflow.python.keras.datasets import fashion_mnist #this is our dataset from keras.callbacks import ModelCheckpointtf.enable_eager_execution()

接下来,我们初始化重要的常量,如下所示:

# important constantsbatch_size = 128epochs = 20n_classes = 10learning_rate = 0.1width = 28 # of our imagesheight = 28 # of our images

然后,我们将我们训练的时尚标签的indices与它们的标签相关联,以便稍后以图形方式打印出结果:

fashion_labels =["Shirt/top","Trousers","Pullover","Dress","Coat","Sandal","Shirt","Sneaker","Bag","Ankle boot"] #indices 0       1         2          3      4         5       6       7       8        9# Next, we load our fashion data set, # load the dataset (x_train, y_train), (x_test, y_test) = fashion_mnist.load_data()

然后,我们将每个图像中的每个整数值像素转换为float32并除以 255 以对其进行归一化:

# normalize the features for better training x_train = x_train.astype('float32') / 255. x_test = x_test.astype('float32') / 255.

x_train现在由60000float32值组成,并且x_test保持10000相似的值。

然后,我们展平特征集,准备进行训练:

# flatten the feature set for use by the training algorithm x_train = x_train.reshape((60000, width * height)) x_test = x_test.reshape((10000, width * height))

然后,我们将训练集x_trainy_train进一步分为训练集和验证集:

split = 50000 #split training sets into training and validation sets (x_train, x_valid) = x_train[:split], x_train[split:] (y_train, y_valid) = y_train[:split], y_train[split:]

如果标签是单热编码的,那么许多机器学习算法效果最好,因此我们接下来要做。 但请注意,我们会将产生的一束热张量转换回(单热)NumPy 数组,以备稍后由 Keras 使用:

# one hot encode the labels using TensorFLow. # then convert back to numpy as we cannot combine numpy # and tensors as input to keras later y_train_ohe = tf.one_hot(y_train, depth=n_classes).numpy() y_valid_ohe = tf.one_hot(y_valid, depth=n_classes).numpy() y_test_ohe = tf.one_hot(y_test, depth=n_classes).numpy() #or use tf.keras.utils.to_categorical(y_train,10)

这是一段代码,其中显示了一个介于零到九之间的值以及其单热编码版本:

# show difference between original label and one-hot-encoded labeli=5print(y_train[i]) # 'ordinairy' number value of label at index iprint (tf.one_hot(y_train[i], depth=n_classes))# same value as a 1\. in correct position in an length 10 1D tensorprint(y_train_ohe[i]) # same value as a 1\. in correct position in an length 10 1D numpy array

在这里重要的是要注意索引i和存储在索引i的标签之间的差异。 这是另一段代码,显示y_train中的前 10 个时尚项目:

# print sample fashion images.# we have to reshape the image held in x_train back to width by height# as we flattened it for training into width*heightimport matplotlib.pyplot as plt%matplotlib inline_,image = plt.subplots(1,10,figsize=(8,1))for i in range(10):    image[i].imshow(np.reshape(x_train[i],(width, height)), cmap="Greys")    print(fashion_labels[y_train[i]],sep='', end='')

现在,我们进入代码的重要且可概括的部分。 Google 建议,对于创建任何类型的机器学习模型,都可以通过将其分类为tf.keras.Model来创建模型。

这具有直接的优势,即我们可以在我们的子类化模型中使用tf.keras.Model的所有功能,包括编译和训练例程以及层功能,在后续的章节中,我们将详细介绍。

对于我们的逻辑回归示例,我们需要在子类中编写两个方法。 首先,我们需要编写一个构造器,该构造器调用超类的构造器,以便正确创建模型。 在这里,我们传入正在使用的类数(10),并在实例化模型以创建单个层时使用此构造器。 我们还必须声明call方法,并使用该方法来编程在模型训练的正向传递过程中发生的情况。

稍后,当我们考虑具有前向和后向传递的神经网络时,我们将对这种情况进行更多说明。 对于我们当前的目的,我们只需要知道在call方法中,我们采用输入的softmax来产生输出。 softmax函数的作用是获取一个向量(或张量),然后在其元素具有该向量最大值的位置上用几乎为 1 的值覆盖,在所有其他位置上使用几乎为零的值覆盖。 这与单热编码很相似。 请注意,在此方法中,由于softmax未为 GPU 实现,因此我们必须在 CPU 上强制执行:

# model definition (the canonical Google way)class LogisticRegression(tf.keras.Model):    def __init__(self, num_classes):        super(LogisticRegression, self).__init__() # call the constructor of the parent class (Model)        self.dense = tf.keras.layers.Dense(num_classes) #create an empty layer called dense with 10 elements.    def call(self, inputs, training=None, mask=None): # required for our forward pass        output = self.dense(inputs) # copy training inputs into our layer        # softmax op does not exist on the gpu, so force execution on the CPU        with tf.device('/cpu:0'):            output = tf.nn.softmax(output) # softmax is near one for maximum value in output                                           # and near zero for the other values.        return output

现在,我们准备编译和训练我们的模型。

首先,我们确定可用的设备,然后使用它。 然后,使用我们开发的类声明模型。 声明要使用的优化程序后,我们将编译模型。 我们使用的损失,分类交叉熵(也称为对数损失),通常用于逻辑回归,因为要求预测是概率。

优化器是一个选择和有效性的问题,有很多可用的方法。 接下来是带有三个参数的model.compile调用。 我们将很快看到,它为我们的训练模型做准备。

在撰写本文时,优化器的选择是有限的。 categorical_crossentropy是多标签逻辑回归问题的正态损失函数,'accuracy'度量是通常用于分类问题的度量。

请注意,接下来,我们必须使用样本大小仅为输入图像之一的model.call方法进行虚拟调用,否则model.fit调用将尝试将整个数据集加载到内存中以确定输入特征的大小 。

接下来,我们建立一个ModelCheckpoint实例,该实例用于保存训练期间的最佳模型,然后使用model.fit调用训练模型。

找出model.compilemodel.fit(以及所有其他 Python 或 TensorFlow 类或方法)的所有不同参数的最简单方法是在 Jupyter 笔记本中工作,然后按Shift + TAB + TAB,当光标位于相关类或方法调用上时。

从代码中可以看到,model.fit在训练时使用callbacks方法(由验证准确率确定)保存最佳模型,然后加载最佳模型。 最后,我们在测试集上评估模型,如下所示:

# build the modelmodel = LogisticRegression(n_classes)# compile the model#optimiser = tf.train.GradientDescentOptimizer(learning_rate)optimiser =tf.keras.optimizers.Adam() #not supported in eager execution mode.model.compile(optimizer=optimiser, loss='categorical_crossentropy', metrics=['accuracy'], )# TF Keras tries to use the entire dataset to determine the shape without this step when using .fit()# So, use one sample of the provided input dataset size to determine input/output shapes for the modeldummy_x = tf.zeros((1, width * height))model.call(dummy_x)checkpointer = ModelCheckpoint(filepath="./model.weights.best.hdf5", verbose=2, save_best_only=True, save_weights_only=True)    # train the modelmodel.fit(x_train, y_train_ohe, batch_size=batch_size, epochs=epochs,              validation_data=(x_valid, y_valid_ohe), callbacks=[checkpointer], verbose=2)    #load model with the best validation accuracymodel.load_weights("./model.weights.best.hdf5")    # evaluate the model on the test setscores = model.evaluate(x_test, y_test_ohe, batch_size, verbose=2)print("Final test loss and accuracy :", scores)y_predictions = model.predict(x_test)

最后,对于我们的逻辑回归示例,我们有一些代码可以检查一个时尚的测试项目,以查看其预测是否准确:

    # example of one predicted versus one true fashion labelindex = 42index_predicted = np.argmax(y_predictions[index]) # largest label probabilityindex_true = np.argmax(y_test_ohe[index]) # pick out index of element with a 1 in itprint("When prediction is ",index_predicted)print("ie. predicted label is", fashion_labels[index_predicted])print("True label is ",fashion_labels[index_true])print ("\n\nPredicted V (True) fashion labels, green is correct, red is wrong")size = 12 # i.e. 12 random numbers chosen out of x_test.shape[0] =1000, we do not replace themfig = plt.figure(figsize=(15,3))rows = 3cols = 4

检查 12 个预测的随机样本,如下所示:

for i, index in enumerate(np.random.choice(x_test.shape[0], size = size, replace = False)):          axis = fig.add_subplot(rows,cols,i+1, xticks=[], yticks=[]) # position i+1 in grid with rows rows and cols columns          axis.imshow(x_test[index].reshape(width,height), cmap="Greys")          index_predicted = np.argmax(y_predictions[index])          index_true = np.argmax(y_test_ohe[index])          axis.set_title(("{} ({})").format(fashion_labels[index_predicted],fashion_labels[index_true]),                                                  color=("green" if index_predicted==index_true else "red"))

以下屏幕快照显示了真实与(预测)时尚标签:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vj5BUmHm-1681568158343)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/tf-20-quick-start-guide/img/e81c9e90-26ff-4fb8-974d-b4e4eaffb6ae.png)]

时尚标签

到此结束我们对逻辑回归的研究。 现在,我们将看看另一种非常强大的监督学习技术,即 K 最近邻。

K 最近邻(KNN)

KNN 背后的想法相对简单。 给定新的特定数据点的值,请查看该点的 KNN,并根据该 k 个邻居的标签为该点分配标签,其中k是算法的参数。

在这种情况下,没有这样构造的模型。 该算法仅查看数据集中新点与所有其他数据点之间的所有距离,接下来,我们将使用由三种类型的鸢尾花组成的著名数据集:iris setosairis virginicairis versicolor。 对于这些标签中的每一个,特征都是花瓣长度,花瓣宽度,萼片长度和萼片宽度。 有关显示此数据集的图表,请参见这里。

有 150 个数据点(每个数据点都包含前面提到的四个测量值)和 150 个相关标签。 我们将它们分为 120 个训练数据点和 30 个测试数据点。

首先,我们有通常的导入,如下所示:

import numpy as npfrom sklearn import datasetsimport tensorflow as tf# and we next load our data:iris = datasets.load_iris()x = np.array([i for i in iris.data])y = np.array(iris.target)x.shape, y.shape

然后,我们将花标签放在列表中以备后用,如下所示:

flower_labels = ["iris setosa", "iris virginica", "iris versicolor"]

现在是时候对标签进行一次热编码了。 np.eye返回一个二维数组,在对角线上有一个,默认为主对角线。 然后用y进行索引为我们提供了所需的y单热编码:

#one hot encoding, another methody = np.eye(len(set(y)))[y]y[0:10]

接下来,我们将特征规格化为零到一,如下所示:

x = (x - x.min(axis=0)) / (x.max(axis=0) - x.min(axis=0))

为了使算法正常工作,我们必须使用一组随机的训练特征。 接下来,我们还要通过从数据集的整个范围中删除训练指标来设置测试指标:

# create indices for the train-test splitnp.random.seed(42)split = 0.8 # this makes 120 train and 30 test featurestrain_indices = np.random.choice(len(x), round(len(x) * split), replace=False)test_indices =np.array(list(set(range(len(x))) - set(train_indices)))

我们现在可以创建我们的训练和测试特征,以及它们的相关标签:

# the train-test split train_x = x[train_indices] test_x = x[test_indices] train_y = y[train_indices] test_y = y[test_indices]

现在,我们将k的值设置为5,如下所示:

k = 5

接下来,在 Jupyter 笔记本中,我们具有预测测试数据点类别的函数。 我们将逐行对此进行细分。

首先是我们的distance函数。 执行此函数后,可变距离包含我们 120 个训练点与 30 个测试点之间的所有(曼哈顿)距离; 也就是说,由 30 行乘 120 列组成的数组-曼哈顿距离,有时也称为城市街区距离,是x[1], x[2]的两个数据点向量的值之差的绝对值; 即|x[1] - x[2]|。 如果需要的话(如本例所示),将使用各个特征差异的总和。

tf.expandtest_x上增加了一个额外的维数,以便在减法发生之前,可以通过广播使两个数组扩展以使其与减法兼容。 由于x具有四个特征,并且reduce_sum超过axis=2,因此结果是我们 30 个测试点和 120 个训练点之间的距离的 30 行。 所以我们的prediction函数是:

def prediction(train_x, test_x, train_y,k):    print(test_x)    d0 = tf.expand_dims(test_x, axis =1)    d1 = tf.subtract(train_x, d0)    d2 = tf.abs(d1)    distances = tf.reduce_sum(input_tensor=d2, axis=2)    print(distances)    # or    # distances = tf.reduce_sum(tf.abs(tf.subtract(train_x, tf.expand_dims(test_x, axis =1))), axis=2)

然后,我们使用tf.nn.top_k返回 KNN 的索引作为其第二个返回值。 请注意,此函数的第一个返回值是距离本身的值,我们不需要这些距离,因此我们将其“扔掉”(带下划线):

_, top_k_indices = tf.nn.top_k(tf.negative(distances), k=k)

接下来,我们gather,即使用索引作为切片,找到并返回与我们最近的邻居的索引相关联的所有训练标签:

top_k_labels = tf.gather(train_y, top_k_indices)

之后,我们对预测进行汇总,如下所示:

predictions_sum = tf.reduce_sum(input_tensor=top_k_labels, axis=1)

最后,我们通过找到最大值的索引来返回预测的标签:

pred = tf.argmax(input=predictions_sum, axis=1)

返回结果预测pred。 作为参考,下面是一个完整的函数:

def prediction(train_x, test_x, train_y,k):     distances = tf.reduce_sum(tf.abs(tf.subtract(train_x, tf.expand_dims(test_x, axis =1))), axis=2)     _, top_k_indices = tf.nn.top_k(tf.negative(distances), k=k)     top_k_labels = tf.gather(train_y, top_k_indices)     predictions_sum = tf.reduce_sum(top_k_labels, axis=1)     pred = tf.argmax(predictions_sum, axis=1)     return pred

打印在此函数中出现的各种张量的形状可能非常有启发性。

代码的最后一部分很简单。 我们将花朵标签的预测与实际标签压缩(连接)在一起,然后我们可以遍历它们,打印出来并求出正确性总计,然后将精度打印为测试集中数据点数量的百分比 :

i, total = 0 , 0results = zip(prediction(train_x, test_x, train_y,k), test_y) #concatenate predicted label with actual labelprint("Predicted Actual")print("--------- ------")for pred, actual in results:    print(i, flower_labels[pred.numpy()],"\t",flower_labels[np.argmax(actual)] )    if pred.numpy() == np.argmax(actual):        total += 1    i += 1accuracy = round(total/len(test_x),3)*100print("Accuracy = ",accuracy,"%")

如果您自己输入代码,或运行提供的笔记本电脑,则将看到准确率为 96.7%,只有一个iris versicolor被误分类为iris virginica(测试索引为 25)。

总结

在本章中,我们看到了在涉及线性回归的两种情况下使用 TensorFlow 的示例。 其中将特征映射到具有连续值的已知标签,从而可以对看不见的特征进行预测。 我们还看到了逻辑回归的一个示例,更好地描述为分类,其中将特征映射到分类标签,再次允许对看不见的特征进行预测。 最后,我们研究了用于分类的 KNN 算法。

我们现在将在第 5 章“将 TensorFlow 2 用于无监督学习”,继续进行无监督学习,在该过程中,特征和标签之间没有初始映射,并且 TensorFlow 的任务是发现特征之​​间的关系。

五、TensorFlow 2 和无监督学习

在本章中,我们将研究使用 TensorFlow 2 进行无监督学习。无监督学习的目的是在数据中发现以前未标记数据点的模式或关系; 因此,我们只有特征。 这与监督式学习形成对比,在监督式学习中,我们既提供了特征及其标签,又希望预测以前未见过的新特征的标签。 在无监督学习中,我们想找出我们的数据是否存在基础结构。 例如,可以在不事先了解其结构的情况下以任何方式对其进行分组或组织吗? 这被称为聚类。 例如,亚马逊在其推荐系统中使用无监督学习来建议您以书本方式可能购买的商品,例如,通过识别以前购买的商品类别来提出建议。

无监督学习的另一种用途是在数据压缩技术中,其中数据中的模式可以用更少的内存表示,而不会损害数据的结构或完整性。 在本章中,我们将研究两个自编码器,以及如何将它们用于压缩数据以及如何消除图像中的噪声。

在本章中,我们将深入探讨自编码器。

自编码器

自编码是一种使用 ANN 实现的数据压缩和解压缩算法。 由于它是学习算法的无监督形式,因此我们知道只需要未标记的数据。 它的工作方式是通过强制输入通过瓶颈(即,宽度小于原始输入的一层或多层)来生成输入的压缩版本。 要重建输入(即解压缩),我们可以逆向处理。 我们使用反向传播在中间层中创建输入的表示形式,并重新创建输入作为表示形式的输出。

自编码是有损的,也就是说,与原始输入相比,解压缩后的输出将变差。 这与 MP3 和 JPEG 压缩格式相似。

自编码是特定于数据的,也就是说,只有与它们经过训练的数据相似的数据才可以正确压缩。 例如,训练有素的自编码器在汽车图片上的表现会很差,这是因为其学习到的特征将是汽车特有的。

一个简单的自编码器

让我们编写一个非常简单的自编码器,该编码器仅使用一层 ANN。 首先,像往常一样,让我们​​从导入开始,如下所示:

from tensorflow.keras.layers import Input, Densefrom tensorflow.keras.models import Modelfrom tensorflow.keras.datasets import fashion_mnistfrom tensorflow.keras.callbacks import ModelCheckpoint, EarlyStoppingfrom tensorflow.keras import regularizersimport numpy as npimport matplotlib.pyplot as plt%matplotlib inline

预处理数据

然后,我们加载数据。 对于此应用,我们将使用fashion_mnist数据集,该数据集旨在替代著名的 MNIST 数据集。 本节末尾有这些图像的示例。 每个数据项(图像中的像素)都是 0 到 255 之间的无符号整数,因此我们首先将其转换为float32,然后将其缩放为零至一的范围,以使其适合以后的学习过程:

(x_train, _), (x_test, _) = fashion_mnist.load_data() # we don't need the labelsx_train = x_train.astype('float32') / 255\. # normalizex_test = x_test.astype('float32') / 255.print(x_train.shape) # shape of inputprint(x_test.shape)

这将给出形状,如以下代码所示:

(60000, 28, 28)(10000, 28, 28)

接下来,我们将图像展平,因为我们要将其馈送到一维的密集层:

x_train = x_train.reshape(( x_train.shape[0], np.prod(x_train.shape[1:]))) #flattenx_test = x_test.reshape((x_test.shape[0], np.prod(x_test.shape[1:])))print(x_train.shape)print(x_test.shape)

现在的形状如下:

(60000, 784)(10000, 784)

分配所需的尺寸,如以下代码所示:

image_dim = 784 # this is the size of our input image, 784encoding_dim = 32 # this is the length of our encoded items.Compression of factor=784/32=24.5

接下来,我们构建单层编码器和自编码器模型,如下所示:

input_image = Input(shape=(image_dim, )) # the input placeholderencoded_image = Dense(encoding_dim, activation='relu', activity_regularizer=regularizers.l1(10e-5))(input_image)# "encoded" is the encoded representation of the inputencoder = Model(input_image, encoded_image)decoded_image = Dense(image_dim, activation='sigmoid')(encoded_image)# "decoded" is the lossy reconstruction of the inputautoencoder = Model(input_image, decoded_image) # this model maps an input to its reconstruction

然后,我们构造解码器模型,如下所示:

encoded_input = Input(shape=(encoding_dim,))# create a placeholder for an encoded (32-dimensional) inputdecoder_layer = autoencoder.layers[-1]# retrieve the last layer of the autoencoder modeldecoder = Model(encoded_input, decoder_layer(encoded_input))# create the decoder model

接下来,我们可以编译我们的自编码器。 由于数据几乎是二元的,因此选择了binary_crossentropy损失,因此,我们可以最小化每个像素的二元交叉熵:

autoencoder.compile(optimizer='adadelta', loss='binary_crossentropy')

我们可以定义两个有用的检查点。 第一个在每个周期后保存模型。 如果save_best_only=True,根据监视的数量(验证损失),最新的最佳模型将不会被覆盖。

其签名如下:

keras.callbacks.ModelCheckpoint(filepath, monitor='val_loss', verbose=0, save_best_only=False, save_weights_only=False, mode='auto', period=1)

我们声明如下:

checkpointer1 = ModelCheckpoint(filepath= 'model.weights.best.hdf5' , verbose =2, save_best_only = True)

当监视器中的更改(验证损失)小于min_delta时,即小于min_delta的更改不算改善时,第二个检查点停止训练。 这对于patience周期必定会发生,然后停止训练。 其签名如下:

EarlyStopping(monitor='val_loss', min_delta=0, patience=0, verbose=0, mode='auto', baseline=None)

我们声明如下:

checkpointer2 = EarlyStopping(monitor='val_loss', min_delta=0.0005, patience=2, verbose=2, mode='auto')

训练

训练运行使用.fit方法,其签名如下:

autoencoder.fit(x=None, y=None, batch_size=None, epochs=1, verbose=1, callbacks=None, validation_split=0.0, validation_data=None, shuffle=True, class_weight=None, sample_weight=None, initial_epoch=0, steps_per_epoch=None, validation_steps=None, max_queue_size=10, workers=1, use_multiprocessing=False, **kwargs)

香草训练运行如下。 注意,我们如何传递xyx_train,因为我们要使用x输入并尝试在输出(y=x)上再现它。 请注意以下代码:

epochs = 50autoencoder.fit(x_train, x_train, epochs=epochs, batch_size=256, verbose=2, shuffle=True, validation_data=(x_test, x_test))

这之后是一些代码,用于压缩和解压缩(编码和解码)test数据。 请记住,encoderdecoder都是模型,所以我们可以调用该方法。 在它们上使用predict方法生成其输出:

encoded_images = encoder.predict(x_test) #compressdecoded_images = decoder.predict(encoded_images) #decompress

我们还可以使用ModelCheckpoint检查点,在这种情况下,我们的.fit调用如下:

epochs = 50autoencoder.fit(x_train, x_train, epochs=epochs, batch_size=256, verbose=2, callbacks=[checkpointer1], shuffle=True, validation_data=(x_test, x_test))

我们还需要按如下方式加载保存的权重,以获取最佳模型:

autoencoder.load_weights('model.weights.best.hdf5' )encoded_images = encoder.predict(x_test)decoded_images = decoder.predict(encoded_images)

以类似的方式,我们可以使用EarlyStopping,在这种情况下,.fit调用如下:

epochs = 50autoencoder.fit(x_train, x_train, epochs=epochs, batch_size=256, verbose=2, callbacks=[checkpointer2], shuffle=True, validation_data=(x_test, x_test))

显示结果

下面是一些代码,可以在屏幕上前后打印一些内容。 我们正在使用以下代码:

plt.subplot(nrows, ncols, index, **kwargs)

子图在具有nrows行和ncols列的网格上的index位置处,index位置从左上角的一个位置开始,并向右增加以定位时尚项目:

number_of_items = 12 # how many items we will displayplt.figure(figsize=(20, 4))for i in range(number_of_items):    # display items before compression     graph = plt.subplot(2, number_of_items, i + 1)    plt.imshow(x_test[i].reshape(28, 28))    plt.gray()    graph.get_xaxis().set_visible(False)    graph.get_yaxis().set_visible(False)    # display items after decompression    graph = plt.subplot(2, number_of_items, i + 1 + number_of_items)    plt.imshow(decoded_images[i].reshape(28, 28))    plt.gray()    graph.get_xaxis().set_visible(False)    graph.get_yaxis().set_visible(False)plt.show()

压缩前的结果如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rPYTBLTL-1681568158344)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/tf-20-quick-start-guide/img/9758ae58-a6e9-401b-8c2a-cc46c6d4a00c.png)]

减压后,结果如下所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wmFnmvF0-1681568158344)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/tf-20-quick-start-guide/img/e6499d2a-350c-4aff-bf67-72f8dd0338a8.png)]

因此,压缩/解压缩的有损性很明显。 作为一种可能的健全性检查,如果我们使用encoding_dim = 768(与输入相同数量的隐藏层节点),我们将得到以下结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Xj5DZC8t-1681568158344)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/tf-20-quick-start-guide/img/a271f6c6-dc36-446e-a527-44d5edf655be.png)]

这可能与原始版本略有不同。 接下来,我们将看一下自编码的应用。

自编码器应用–去噪

自编码器的一个很好的应用是去噪:去除图像(噪声)中小的随机伪像的过程。 我们将用多层卷积码代替简单的一层自编码器。

我们将人造噪声添加到我们的时装中,然后将其消除。 我们还将借此机会研究使用 TensorBoard 来检查一些网络训练指标。

构建模型

我们最初的导入包括我们的卷积网络的导入。

注意,我们不必显式地使用 Keras,因为它是 TensorFlow 本身的模块,如以下代码所示:

from tensorflow.keras.layers import Input, Dense, Conv2D, MaxPooling2D, UpSampling2Dfrom tensorflow.keras.models import Modelfrom tensorflow.keras.datasets import fashion_mnistfrom tensorflow.keras.callbacks import TensorBoardimport numpy as npimport matplotlib.pyplot as plt%matplotlib inline

预处理数据

首先,加载图像数据; 我们不需要标签,因为我们只关注图像本身:

(train_x, _), (test_x, _) = fashion_mnist.load_data()

接下来,像以前一样,将图像数据点转换为零至一范围内的float32值:

train_x = train_x.astype('float32') / 255.test_x = test_x.astype('float32') / 255.

检查形状,如以下代码所示:

print(train_x.shape)print(test_x.shape)

它给出以下结果:

(60000, 28, 28) (10000, 28, 28)

输入卷积层需要以下形状:

train_x = np.reshape(train_x, (len(train_x), 28, 28, 1)) test_x = np.reshape(test_x, (len(test_x), 28, 28, 1))

在这里,形状中的一个是用于灰度通道; 以下是形状的完整性检查:

print(train_x.shape)print(test_x.shape)

得到以下结果:

(60000, 28, 28, 1) (10000, 28, 28, 1)

为了在图像中引入一些随机噪声,我们在训练和测试集中添加了np.random.normal(即高斯)值数组。 所需的签名如下:

numpy.random.normal(loc=0.0, scale=1.0, size=None)

在这里,loc是分布的中心,scale是标准差,size是输出形状。 因此,我们使用以下代码:

noise = 0.5train_x_noisy = train_x + noise * np.random.normal(loc=0.0, scale=1.0, size=train_x.shape) test_x_noisy = test_x + noise * np.random.normal(loc=0.0, scale=1.0, size=test_x.shape) 

由于这可能会使我们的值超出零至一的范围,因此我们将值裁剪到该范围:

train_x_noisy = np.clip(train_x_noisy, 0., 1.)test_x_noisy = np.clip(test_x_noisy, 0., 1.)

噪声图像

下面的代码从测试集中打印出一些嘈杂的图像。 注意如何调整图像的显示形状:

plt.figure(figsize=(20, 2))for i in range(number_of_items):    display = plt.subplot(1, number_of_items,i+1)    plt.imshow(test_x_noisy[i].reshape(28, 28))    plt.gray()    display.get_xaxis().set_visible(False)    display.get_yaxis().set_visible(False)plt.show()

这是结果,如以下屏幕快照所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Upbm4Ii6-1681568158345)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/tf-20-quick-start-guide/img/4456e23d-5c7b-4978-a348-ab3a9be3e662.png)]

因此很明显,原始图像与噪点几乎没有区别。

创建编码层

接下来,我们创建编码和解码层。 我们将使用 Keras 函数式 API 风格来设置模型。 我们从一个占位符开始,以(下一个)卷积层所需的格式输入:

input_image = Input(shape=(28, 28, 1))

接下来,我们有一个卷积层。 回忆卷积层的签名:

Conv2D(filters, kernel_size, strides=(1, 1), padding='valid', data_format=None, dilation_rate=(1, 1), activation=None, use_bias=True, kernel_initializer='glorot_uniform', bias_initializer='zeros', kernel_regularizer=None, bias_regularizer=None, activity_regularizer=None, kernel_constraint=None, bias_constraint=None, **kwargs)

我们将主要使用默认值; 接下来是我们的第一个Conv2D。 注意(3,3)的内核大小; 这是 Keras 应用于输入图像的滑动窗口的大小。 还记得padding='same'表示图像用 0 左右填充,因此卷积的输入和输出层是内核(过滤器)以其中心“面板”开始于图像中第一个像素时的大小。 。 默认步幅(1, 1)表示滑动窗口一次从图像的左侧到末尾水平移动一个像素,然后向下移动一个像素,依此类推。 接下来,我们将研究每个层的形状,如下所示:

im = Conv2D(filters=32, kernel_size=(3, 3), activation='relu', padding='same')(input_image)print(x.shape)

得到以下结果:

(?, 28, 28, 32)

?代表输入项目的数量。

接下来,我们有一个MaxPooling2D层。 回想一下,在此情况下,此操作将在图像上移动(2, 2)大小的滑动窗口,并采用在每个窗口中找到的最大值。 其签名如下:

MaxPooling2D(pool_size=(2, 2), strides=None, padding='valid', data_format=None, **kwargs)

这是下采样的示例,因为生成的图像尺寸减小了。 我们将使用以下代码:

im = MaxPooling2D((2, 2), padding='same')(im)print(im.shape)

得到以下结果:

(?, 14, 14, 32)

其余的编码层如下:

im = Conv2D(32, (3, 3), activation='relu', padding='same')(im)print(im.shape)encoded = MaxPooling2D((2, 2), padding='same')(im)print(encoded.shape)

所有这些都结束了编码。

创建解码层

为了进行解码,我们反转了该过程,并使用上采样层UpSampling2D代替了最大池化层。 上采样层分别按大小[0]和大小[1]复制数据的行和列。

因此,在这种情况下,会取消最大合并层的效果,尽管会损失细粒度。 签名如下:

 UpSampling2D(size=(2, 2), data_format=None, **kwargs)

我们使用以下内容:

im = UpSampling2D((2, 2))(im)

以下是解码层:

im = Conv2D(32, (3, 3), activation='relu', padding='same')(encoded)print(im.shape)im = UpSampling2D((2, 2))(im)print(im.shape)im = Conv2D(32, (3, 3), activation='relu', padding='same')(im)print(im.shape)im = UpSampling2D((2, 2))(im)print(im.shape)decoded = Conv2D(1, (3, 3), activation='sigmoid', padding='same')(im)print(decoded.shape)

得到以下结果:

(?, 7, 7, 32) (?, 14, 14, 32) (?, 14, 14, 32) (?, 28, 28, 32) (?, 28, 28, 1)

因此,您可以看到解码层如何逆转编码层的过程。

模型摘要

这是我们模型的摘要:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UQru7WYs-1681568158345)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/tf-20-quick-start-guide/img/b33ae38e-dc32-4e06-bb11-b04215f913c1.png)]

看看我们如何得出参数数字很有启发性。

公式是参数数量 = 过滤器数量 x 内核大小 x 上一层的深度 + 过滤器数量(用于偏差):

  • input_1:这是一个占位符,没有可训练的参数
  • conv2d:过滤器数量= 32,内核大小= 3 * 3 = 9,上一层的深度= 1,因此32 * 9 + 32 = 320
  • max_pooling2d:最大池化层没有可训练的参数。
  • conv2d_1:过滤器数= 32,内核大小= 3 * 3 = 9,上一层的深度= 14,因此32 * 9 * 32 + 32 = 9,248
  • conv_2d_2conv2d_3:与conv2d_1相同
  • conv2d_41 * 9 * 32 + 1 = 289

模型实例化,编译和训练

接下来,我们用输入层和输出层实例化模型,然后使用.compile方法设置模型以进行训练:

autoencoder = Model(inputs=input_img, outputs=decoded)autoencoder.compile(optimizer='adadelta', loss='binary_crossentropy')

现在,我们准备训练模型以尝试恢复时尚商品的图像。 请注意,我们已经为 TensorBoard 提供了回调,因此我们可以看一下一些训练指标。 Keras TensorBoard 签名如下:

keras.callbacks.TensorBoard(    ["log_dir='./logs'", 'histogram_freq=0', 'batch_size=32', 'write_graph=True', 'write_grads=False', 'write_images=False', 'embeddings_freq=0', 'embeddings_layer_names=None', 'embeddings_metadata=None', 'embeddings_data=None', "update_freq='epoch'"],)

我们将主要使用默认值,如下所示:

tb = [TensorBoard(log_dir='./tmp/tb', write_graph=True)]

接下来,我们使用.fit()方法训练自编码器。 以下代码是其签名:

fit(x=None, y=None, batch_size=None, epochs=1, verbose=1, callbacks=None, validation_split=0.0, validation_data=None, shuffle=True, class_weight=None, sample_weight=None, initial_epoch=0, steps_per_epoch=None, validation_steps=None, validation_freq=1)

注意我们如何将x_train_noisy用于特征(输入),并将x_train用于标签(输出):

epochs=100batch_size=128autoencoder.fit(x_train_noisy, x_train, epochs=epochs,batch_size=batch_size, shuffle=True, validation_data=(x_test_noisy, x_test), callbacks=tb)

去噪图像

现在,通过解码以下第一行中的所有测试集,然后循环遍历一个固定数字(number_of_items)并显示它们,来对测试集中的一些噪点图像进行去噪。 请注意,在显示每个图像(im)之前,需要对其进行重塑:

decoded_images = autoencoder.predict(test_noisy_x)number_of_items = 10plt.figure(figsize=(20, 2))for item in range(number_of_items):    display = plt.subplot(1, number_of_items,item+1)    im = decoded_images[item].reshape(28, 28)   plt.imshow(im, cmap="gray")    display.get_xaxis().set_visible(False)    display.get_yaxis().set_visible(False)plt.show()

我们得到以下结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PkVPSeyg-1681568158345)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/tf-20-quick-start-guide/img/5fd8eb0d-a384-485f-a54a-d05fb4780f67.png)]

考虑到图像最初模糊的程度,降噪器已经做了合理的尝试来恢复图像。

TensorBoard 输出

要查看 TensorBoard 输出,请在命令行上使用以下命令:

tensorboard  --logdir=./tmp/tb

然后,您需要将浏览器指向http://localhost:6006

下图显示了作为训练和验证时间的函数(x轴)的损失(y轴):

下图显示了训练损失:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LhWKvjmG-1681568158346)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/tf-20-quick-start-guide/img/1e1ff356-312f-4c90-8f53-75798f678bc4.png)]

验证损失如下图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-I3LpTcg9-1681568158346)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/tf-20-quick-start-guide/img/beed13e6-2d68-4db6-bd11-1d4fe2ea089d.png)]

到此结束我们对自编码器的研究。

总结

在本章中,我们研究了自编码器在无监督学习中的两种应用:首先用于压缩数据,其次用于降噪,这意味着从图像中去除噪声。

在下一章中,我们将研究如何在图像处理和识别中使用神经网络。