我试图用*TensorFlow 2.0 beta*在MNIST数据集上编写一个带有两个隐藏层的基本神经网络的自定义实现,但我不确定这里出了什么问题,但我的training lossaccuracy似乎分别停留在1.585左右.但是,如果我用Keras来构建这个模型,我的训练损失非常低,精度在95%以上,只有8-10个历元.

我相信也许我没有更新我的体重什么的?那么,我需要将我在backprop函数中计算的新权重分配给它们各自的权重/偏差变量吗?

如果有人能帮我解决这个问题,以及我在下面提到的其他几个问题,我真的很感激.

Few more Questions:

1) 如何在这个自定义实现中添加DropoutBatch Normalization层?(i.e使其在列车和测试时间都能工作)

2) 在这个代码中如何使用callbacks?i、 e(利用EarlyStoping和ModelCheckpoint回调)

3) 下面我的代码中还有什么可以进一步优化的地方吗,比如使用tensorflow 2.x@tf.功能decorator 等)

4) 我还需要提取绘制和判断其分布所获得的最终权重.调查梯度消失或爆炸等问题.(例如:可能是张力板)

5) 我还需要帮助,以一种更通用的方式编写这段代码,这样我就可以轻松地基于这段代码实现其他网络,如ConvNets(即Conv、MaxPool等).

这是我的完整代码,便于复制:

注:I know I can use high-level API like Keras to build the model much easier but that is not my goal here. Please understand.

import numpy as np
import os
import logging
logging.getLogger('tensorflow').setLevel(logging.ERROR)
import tensorflow as tf
import tensorflow_datasets as tfds

(x_train, y_train), (x_test, y_test) = tfds.load('mnist', split=['train', 'test'], 
                                                  batch_size=-1, as_supervised=True)

# reshaping
x_train = tf.reshape(x_train, shape=(x_train.shape[0], 784))
x_test  = tf.reshape(x_test, shape=(x_test.shape[0], 784))

ds_train = tf.data.Dataset.from_tensor_slices((x_train, y_train))
# rescaling
ds_train = ds_train.map(lambda x, y: (tf.cast(x, tf.float32)/255.0, y))

class Model(object):
    def __init__(self, hidden1_size, hidden2_size, device=None):
        # layer sizes along with input and output
        self.input_size, self.output_size, self.device = 784, 10, device
        self.hidden1_size, self.hidden2_size = hidden1_size, hidden2_size
        self.lr_rate = 1e-03

        # weights initializationg
        self.glorot_init = tf.initializers.glorot_uniform(seed=42)
        # weights b/w input to hidden1 --> 1
        self.w_h1 = tf.Variable(self.glorot_init((self.input_size, self.hidden1_size)))
        # weights b/w hidden1 to hidden2 ---> 2
        self.w_h2 = tf.Variable(self.glorot_init((self.hidden1_size, self.hidden2_size)))
        # weights b/w hidden2 to output ---> 3
        self.w_out = tf.Variable(self.glorot_init((self.hidden2_size, self.output_size)))

        # bias initialization
        self.b1 = tf.Variable(self.glorot_init((self.hidden1_size,)))
        self.b2 = tf.Variable(self.glorot_init((self.hidden2_size,)))
        self.b_out = tf.Variable(self.glorot_init((self.output_size,)))

        self.variables = [self.w_h1, self.b1, self.w_h2, self.b2, self.w_out, self.b_out]


    def feed_forward(self, x):
        if self.device is not None:
            with tf.device('gpu:0' if self.device=='gpu' else 'cpu'):
                # layer1
                self.layer1 = tf.nn.sigmoid(tf.add(tf.matmul(x, self.w_h1), self.b1))
                # layer2
                self.layer2 = tf.nn.sigmoid(tf.add(tf.matmul(self.layer1,
                                                             self.w_h2), self.b2))
                # output layer
                self.output = tf.nn.softmax(tf.add(tf.matmul(self.layer2,
                                                             self.w_out), self.b_out))
        return self.output

    def loss_fn(self, y_pred, y_true):
        self.loss = tf.nn.sparse_softmax_cross_entropy_with_logits(labels=y_true, 
                                                                  logits=y_pred)
        return tf.reduce_mean(self.loss)

    def acc_fn(self, y_pred, y_true):
        y_pred = tf.cast(tf.argmax(y_pred, axis=1), tf.int32)
        y_true = tf.cast(y_true, tf.int32)
        predictions = tf.cast(tf.equal(y_true, y_pred), tf.float32)
        return tf.reduce_mean(predictions)

    def backward_prop(self, batch_xs, batch_ys):
        optimizer = tf.keras.optimizers.Adam(learning_rate=self.lr_rate)
        with tf.GradientTape() as tape:
            predicted = self.feed_forward(batch_xs)
            step_loss = self.loss_fn(predicted, batch_ys)
        grads = tape.gradient(step_loss, self.variables)
        optimizer.apply_gradients(zip(grads, self.variables))

n_shape = x_train.shape[0]
epochs = 20
batch_size = 128

ds_train = ds_train.repeat().shuffle(n_shape).batch(batch_size).prefetch(batch_size)

neural_net = Model(512, 256, 'gpu')

for epoch in range(epochs):
    no_steps = n_shape//batch_size
    avg_loss = 0.
    avg_acc = 0.
    for (batch_xs, batch_ys) in ds_train.take(no_steps):
        preds = neural_net.feed_forward(batch_xs)
        avg_loss += float(neural_net.loss_fn(preds, batch_ys)/no_steps) 
        avg_acc += float(neural_net.acc_fn(preds, batch_ys) /no_steps)
        neural_net.backward_prop(batch_xs, batch_ys)
    print(f'Epoch: {epoch}, Training Loss: {avg_loss}, Training ACC: {avg_acc}')

# output for 10 epochs:
Epoch: 0, Training Loss: 1.7005115111824125, Training ACC: 0.7603832868262543
Epoch: 1, Training Loss: 1.6052448933478445, Training ACC: 0.8524806404020637
Epoch: 2, Training Loss: 1.5905528008006513, Training ACC: 0.8664196092868224
Epoch: 3, Training Loss: 1.584107405738905, Training ACC: 0.8727630912326276
Epoch: 4, Training Loss: 1.5792385798413306, Training ACC: 0.8773203844903037
Epoch: 5, Training Loss: 1.5759121985174716, Training ACC: 0.8804754322627559
Epoch: 6, Training Loss: 1.5739163148682564, Training ACC: 0.8826455712551251
Epoch: 7, Training Loss: 1.5722616605926305, Training ACC: 0.8840812018606812
Epoch: 8, Training Loss: 1.569699136307463, Training ACC: 0.8867688354803249
Epoch: 9, Training Loss: 1.5679460542742163, Training ACC: 0.8885049475356936

推荐答案

我不知道从哪里开始你的多重问题,我决定用一句话来回答:

Your code definitely should not look like that and is nowhere near current Tensorflow best practices

抱歉,一步一步地调试它是浪费每个人的时间,对我们双方都没有好处.

现在,转到第三点:

  1. 下面的代码中还有什么我可以进一步优化的吗

是的,你可以使用tensorflow2.0个功能,看起来你正在逃离这些功能(tf.function decorator实际上在这里没有用,暂时别管它).

以下新的指导原则也会缓解你的第五点问题,即:

  1. 我还需要帮助,以一种更通用的方式编写这段代码

因为它是专门为此设计的.在简单介绍之后,我将try 通过以下几个步骤向您介绍这些概念:

1. Divide your program into logical parts

在代码可读性方面,Tensorflow造成了很大的危害;tf1.x中的所有东西通常都在一个地方处理,全局的函数定义,然后是另一个全局的函数定义,或者可能是数据加载,所有这些都是一团糟.这并不是开发者的错,因为系统的设计鼓励了这些行为.

现在,在tf2.0中,程序员被鼓励以类似于pytorchchainer和其他更用户友好的框架的 struct 来划分他的工作.

1.1数据加载

你的成绩很好,有Tensorflow Datasets分,但你没有明显的理由就拒绝了.

以下是您的代码和注释:

# You already have tf.data.Dataset objects after load
(x_train, y_train), (x_test, y_test) = tfds.load('mnist', split=['train', 'test'], 
                                                  batch_size=-1, as_supervised=True)

# But you are reshaping them in a strange manner...
x_train = tf.reshape(x_train, shape=(x_train.shape[0], 784))
x_test  = tf.reshape(x_test, shape=(x_test.shape[0], 784))

# And building from slices...
ds_train = tf.data.Dataset.from_tensor_slices((x_train, y_train))
# Unreadable rescaling (there are built-ins for that)

你可以很容易地概括这个 idea for any dataset,把它放在单独的模块中,比如datasets.py:

import tensorflow as tf
import tensorflow_datasets as tfds


class ImageDatasetCreator:
    @classmethod
    # More portable and readable than dividing by 255
    def _convert_image_dtype(cls, dataset):
        return dataset.map(
            lambda image, label: (
                tf.image.convert_image_dtype(image, tf.float32),
                label,
            )
        )

    def __init__(self, name: str, batch: int, cache: bool = True, split=None):
        # Load dataset, every dataset has default train, test split
        dataset = tfds.load(name, as_supervised=True, split=split)
        # Convert to float range
        try:
            self.train = ImageDatasetCreator._convert_image_dtype(dataset["train"])
            self.test = ImageDatasetCreator._convert_image_dtype(dataset["test"])
        except KeyError as exception:
            raise ValueError(
                f"Dataset {name} does not have train and test, write your own custom dataset handler."
            ) from exception

        if cache:
            self.train = self.train.cache()  # speed things up considerably
            self.test = self.test.cache()

        self.batch: int = batch

    def get_train(self):
        return self.train.shuffle().batch(self.batch).repeat()

    def get_test(self):
        return self.test.batch(self.batch).repeat()

现在,您可以使用简单的命令加载mnist多个:

from datasets import ImageDatasetCreator

if __name__ == "__main__":
    dataloader = ImageDatasetCreator("mnist", batch=64, cache = True)
    train, test = dataloader.get_train(), dataloader.get_test()

从现在开始,你可以使用除mnist以外的任何名称来加载数据集.

Please, stop making everything deep learning related one hand-off scripts, you are a programmer as well

1.2模型创建

根据模型的复杂程度,有两种建议方法:

  • tensorflow.keras.models.Sequential-@Stewart_R显示了这种方式,无需重申他的观点.用于最简单的模型(您应该将此模型与前馈一起使用).
  • 继承tensorflow.keras.Model并编写自定义模型.当您的模块中有某种逻辑或更复杂时(如resnet、多路径网络等),应该使用这个选项.总之,它更具可读性和可定制性.

你们班的Model名学生试图模仿这样的东西,但它又南下了;backprop肯定不是模型本身的一部分,lossaccuracyseparate them into another module or function, defo not a member!也不是

也就是说,让我们使用第二种方法对网络进行编码(为了简洁起见,应该将此代码放在model.py中).在此之前,我将从tf.keras.Layers继承YourDense前馈层,从头开始编写YourDense前馈层(这个可能会进入layers.py模块):

import tensorflow as tf

class YourDense(tf.keras.layers.Layer):
    def __init__(self, units):
        # It's Python 3, you don't have to specify super parents explicitly
        super().__init__()
        self.units = units

    # Use build to create variables, as shape can be inferred from previous layers
    # If you were to create layers in __init__, one would have to provide input_shape
    # (same as it occurs in PyTorch for example)
    def build(self, input_shape):
        # You could use different initializers here as well
        self.kernel = self.add_weight(
            shape=(input_shape[-1], self.units),
            initializer="random_normal",
            trainable=True,
        )
        # You could define bias in __init__ as well as it's not input dependent
        self.bias = self.add_weight(shape=(self.units,), initializer="random_normal")
        # Oh, trainable=True is default

    def call(self, inputs):
        # Use overloaded operators instead of tf.add, better readability
        return tf.matmul(inputs, self.kernel) + self.bias

关于你的

  1. 如何在这个自定义窗口中添加一个退出和批处理规范化层

我想您应该创建这些层的自定义实现.

class CustomDropout(layers.Layer):
    def __init__(self, rate, **kwargs):
        super().__init__(**kwargs)
        self.rate = rate

    def call(self, inputs, training=None):
        if training:
            # You could simply create binary mask and multiply here
            return tf.nn.dropout(inputs, rate=self.rate)
        # You would need to multiply by dropout rate if you were to do that
        return inputs

from here层,经过修改,更适合展示目的.

现在您可以最终创建模型(简单的双前馈):

import tensorflow as tf

from layers import YourDense


class Model(tf.keras.Model):
    def __init__(self):
        super().__init__()
        # Use Sequential here for readability
        self.network = tf.keras.Sequential(
            [YourDense(100), tf.keras.layers.ReLU(), YourDense(10)]
        )

    def call(self, inputs):
        # You can use non-parametric layers inside call as well
        flattened = tf.keras.layers.Flatten()(inputs)
        return self.network(flattened)

对于Ofc,您应该在一般实现中尽可能多地使用内置功能.

This structure is pretty extensible, so generalization to convolutional nets, resnets, senets, whatever should be done via this module.你可以阅读更多关于它的信息.

我认为这满足了你的第五点:

  1. 我还需要帮助,以一种更通用的方式编写这段代码

最后一件事,你可能必须使用model.build(shape)来建立你的模型的图表.

model.build((None, 28, 28, 1))

这将是MNIST的28x28x1输入形状,其中None代表批次.

1.3培训

同样,培训可以通过两种不同的方式进行:

  • standard Keras 100-用于分类等简单任务
  • 101-更复杂的训练方案,最突出的例子是Generative Adversarial Networks,其中两个模型优化了玩最小-最大博弈的正交目标

正如@Leevo再次指出的,如果要使用第二种方法,就不能简单地使用Keras提供的回调,因此我建议尽可能使用第一种方法.

理论上,你可以手动调用回调函数,比如on_batch_begin()和其他需要的函数,但这会很麻烦,我不确定这是如何工作的.

说到第一个选项,您可以直接使用tf.data.Dataset个具有fit的对象.这是在另一个模块(最好是train.py)中介绍的:

def train(
    model: tf.keras.Model,
    path: str,
    train: tf.data.Dataset,
    epochs: int,
    steps_per_epoch: int,
    validation: tf.data.Dataset,
    steps_per_validation: int,
    stopping_epochs: int,
    optimizer=tf.optimizers.Adam(),
):
    model.compile(
        optimizer=optimizer,
        # I used logits as output from the last layer, hence this
        loss=tf.losses.SparseCategoricalCrossentropy(from_logits=True),
        metrics=[tf.metrics.SparseCategoricalAccuracy()],
    )

    model.fit(
        train,
        epochs=epochs,
        steps_per_epoch=steps_per_epoch,
        validation_data=validation,
        validation_steps=steps_per_validation,
        callbacks=[
            # Tensorboard logging
            tf.keras.callbacks.TensorBoard(
                pathlib.Path("logs")
                / pathlib.Path(datetime.datetime.now().strftime("%Y%m%d-%H%M%S")),
                histogram_freq=1,
            ),
            # Early stopping with best weights preserving
            tf.keras.callbacks.EarlyStopping(
                monitor="val_sparse_categorical_accuracy",
                patience=stopping_epochs,
                restore_best_weights=True,
            ),
        ],
    )
    model.save(path)

更复杂的方法与PyTorch个训练循环非常相似(几乎是复制和粘贴),所以如果您熟悉这些循环,它们应该不会带来太多问题.

你可以在tf2.0个文档中找到示例,例如herehere.

2. Other things

2.1未回答的问题

  1. 代码中还有什么我可以进一步优化的吗

上面已经将模型转换为图形,因此我认为在这种情况下调用它不会有什么好处.过早的优化是万恶之源,记住在做这件事之前要衡量一下你的代码.

通过适当的数据缓存(如#1.1开头所述)和良好的管道(而不是那些),您将获得更多.

  1. 我还需要一种方法来提取所有层的最终权重

正如上面@Leevo所指出的,

weights = model.get_weights()

可以帮你拿重量.你可以把它们转换成np.array,用seabornmatplotlib、分析、判断或任何你想要的东西来绘图.

2.2总的来说

总而言之,你的main.py(或入口点或类似的东西)将包括以下内容(或多或少):

from dataset import ImageDatasetCreator
from model import Model
from train import train

# You could use argparse for things like batch, epochs etc.
if __name__ == "__main__":
    dataloader = ImageDatasetCreator("mnist", batch=64, cache=True)
    train, test = dataloader.get_train(), dataloader.get_test()
    model = Model()
    model.build((None, 28, 28, 1))
    train(
        model, train, path epochs, test, len(train) // batch, len(test) // batch, ...
    )  # provide necessary arguments appropriately
    # Do whatever you want with those
    weights = model.get_weights()

哦,记住,上面的功能不是用于复制粘贴,应该更像是一个指南.如果你有任何问题,请打电话给我.

3. Questions from comments

3.1如何初始化自定义层和内置层

3.1.1 TLDR您将要阅读的内容

  • 自定义泊松初始化函数,但需要three
  • tf.keras.initalization API需要two个参数(见最后一点in their docs),因此一个是
  • 添加了层的可选偏移,可以使用

为什么它如此复杂?To show that in 100 you can finally use Python's functionality,没有更多的图形麻烦,if而不是tf.cond等等.

3.1.2从TLDR到实施

Keras初始值设定项可以在here和Tensorflow's flavor here中找到.

请注意API的不一致性(大写字母类似于类,小写字母带有下划线类似的函数),尤其是在tf2.0中,但这与重点无关.

可以通过传递字符串(如上面YourDense中所述)或在对象创建过程中使用它们.

为了允许在自定义层中进行自定义初始化,只需向构造函数添加额外的参数(tf.keras.Model类仍然是Python类,__init__类的用法应该与Python类相同).

在此之前,我将向您展示如何创建自定义初始化:

# Poisson custom initialization because why not.
def my_dumb_init(shape, lam, dtype=None):
    return tf.squeeze(tf.random.poisson(shape, lam, dtype=dtype))

注意,它的签名需要三个参数,而它应该只需要(shape, dtype)个参数.尽管如此,在创建自己的图层时,仍可以轻松地"修复",如下面的图层(扩展YourLinear):

import typing

import tensorflow as tf


class YourDense(tf.keras.layers.Layer):
    # It's still Python, use it as Python, that's the point of tf.2.0
    @classmethod
    def register_initialization(cls, initializer):
        # Set defaults if init not provided by user
        if initializer is None:
            # let's make the signature proper for init in tf.keras
            return lambda shape, dtype: my_dumb_init(shape, 1, dtype)
        return initializer

    def __init__(
        self,
        units: int,
        bias: bool = True,
        # can be string or callable, some typing info added as well...
        kernel_initializer: typing.Union[str, typing.Callable] = None,
        bias_initializer: typing.Union[str, typing.Callable] = None,
    ):
        super().__init__()
        self.units: int = units
        self.kernel_initializer = YourDense.register_initialization(kernel_initializer)
        if bias:
            self.bias_initializer = YourDense.register_initialization(bias_initializer)
        else:
            self.bias_initializer = None

    def build(self, input_shape):
        # Simply pass your init here
        self.kernel = self.add_weight(
            shape=(input_shape[-1], self.units),
            initializer=self.kernel_initializer,
            trainable=True,
        )
        if self.bias_initializer is not None:
            self.bias = self.add_weight(
                shape=(self.units,), initializer=self.bias_initializer
            )
        else:
            self.bias = None

    def call(self, inputs):
        weights = tf.matmul(inputs, self.kernel)
        if self.bias is not None:
            return weights + self.bias

我已经添加了my_dumb_initialization作为默认值(如果用户没有提供),并使用bias参数 Select 了偏差.注:只要if不依赖数据,就可以自由使用.如果它是(或以某种方式依赖于tf.Tensor),则必须使用@tf.function decorator,将Python的流更改为tensorflow对应的流(例如iftf.cond).

更多关于签名的信息,请参见here,这很容易理解.

如果你想将上述初始值设定项更改合并到你的模型中,你必须创建合适的对象,就这样.

... # Previous of code Model here
self.network = tf.keras.Sequential(
    [
        YourDense(100, bias=False, kernel_initializer="lecun_uniform"),
        tf.keras.layers.ReLU(),
        YourDense(10, bias_initializer=tf.initializers.Ones()),
    ]
)
... # and the same afterwards

对于内置的tf.keras.layers.Dense层,你也可以这样做(参数名称不同,但idea适用).

3.2 Automatic Differentiation using tf.GradientTape

3.2.1简介

tf.GradientTape点是为了让用户能够控制变量相对于另一个变量的流量和梯度计算.

示例取自here个,但被分成了不同的部分:

def f(x, y):
  output = 1.0
  for i in range(y):
    if i > 1 and i < 5:
      output = tf.multiply(output, x)
  return output

带有forif条流控制语句的常规python函数

def grad(x, y):
  with tf.GradientTape() as t:
    t.watch(x)
    out = f(x, y)
  return t.gradient(out, x)

使用渐变磁带,您可以记录Tensors上的所有操作(以及它们的中间状态),并向后"播放"(使用chaingrule执行自动向后微分).

tf.GradientTape()上下文管理器中的每Tensor个自动记录.如果某个张量超出了范围,可以使用watch()方法,如上图所示.

最后,相对于x的梯度为output(返回输入).

3.2.2与深度学习的联系

上面描述的是backpropagation算法.为网络中的每个 node (或者更确切地说, for each 层)计算梯度w.r.t(相对于)输出.然后,各种优化器会使用这些梯度进行修正,因此它会重复.

让我们继续,假设已经设置了tf.keras.Model、optimizer实例、tf.data.Dataset和loss函数.

我们可以定义一个Trainer级的课程来为我们提供培训.Please read comments in the code if in doubt:

class Trainer:
    def __init__(self, model, optimizer, loss_function):
        self.model = model
        self.loss_function = loss_function
        self.optimizer = optimizer
        # You could pass custom metrics in constructor
        # and adjust train_step and test_step accordingly
        self.train_loss = tf.keras.metrics.Mean(name="train_loss")
        self.test_loss = tf.keras.metrics.Mean(name="train_loss")

    def train_step(self, x, y):
        # Setup tape
        with tf.GradientTape() as tape:
            # Get current predictions of network
            y_pred = self.model(x)
            # Calculate loss generated by predictions
            loss = self.loss_function(y, y_pred)
        # Get gradients of loss w.r.t. EVERY trainable variable (iterable returned)
        gradients = tape.gradient(loss, self.model.trainable_variables)
        # Change trainable variable values according to gradient by applying optimizer policy
        self.optimizer.apply_gradients(zip(gradients, self.model.trainable_variables))
        # Record loss of current step
        self.train_loss(loss)

    def train(self, dataset):
        # For N epochs iterate over dataset and perform train steps each time
        for x, y in dataset:
            self.train_step(x, y)

    def test_step(self, x, y):
        # Record test loss separately
        self.test_loss(self.loss_function(y, self.model(x)))

    def test(self, dataset):
        # Iterate over whole dataset
        for x, y in dataset:
            self.test_step(x, y)

    def __str__(self):
        # You need Python 3.7 with f-string support
        # Just return metrics
        return f"Loss: {self.train_loss.result()}, Test Loss: {self.test_loss.result()}"

现在,您可以在代码中使用这个类,非常简单,如下所示:

EPOCHS = 5

# model, optimizer, loss defined beforehand
trainer = Trainer(model, optimizer, loss)
for _ in range(EPOCHS):
    trainer.train(train_dataset) # Same for training and test datasets
    trainer.test(test_dataset)
    print(f"Epoch {epoch}: {trainer})")

打印会告诉你每个时代的训练和测试损失.您可以按照自己的方式混合培训和测试(例如,5个培训阶段和1个测试阶段),您可以添加不同的指标等.

如果您想要非面向对象的方法,请参阅here(在我看来,可读性较差,但每个方法都有自己的特点).

Python-3.x相关问答推荐

类型注释:pathlib. Path vs importlib. resources. abc. Traversable

Pandas 数据帧断言等同于NaN

如何从拼图分区数据集中读取数据到Polar

Python避免捕获特定异常

如何使用regex将电话号码和姓名从文本字符串中分离出来

如何将参数/值从测试方法传递给pytest的fixture函数?

为什么我无法在django中按月筛选事件?

估计列表中连续对的数量

为什么不能用格式字符串 '-' 绘制点?

如何在两个矩阵的比较中允许任何列的符号差异,Python3?

为什么 return node.next 会返回整个链表?

通过附加/包含多个列表来创建 nDimensional 列表

ImportError:没有名为资源的模块

具有 2 个输入的 python 3 map/lambda 方法

为什么Pandas会在 NaN 上合并?

在 sklearn.decomposition.PCA 中,为什么 components_ 是负数?

Python:遍历子列表

如何判断一个字符串是否包含有效的 Python 代码

aiohttp+sqlalchemy:在回滚无效事务之前无法重新连接

在 PyCharm 中配置解释器:请使用不同的 SDK 名称