Skip to content

「智能计算系统」 编程框架实验

Published at:

这次的实验主要基于 Tensorflow框架进行,前三个部分与之前的实验有着比较强的关联,需要对VGG19网络和风格迁移训练网络结构有较好的理解,同时也要掌握 Tensorflow 的用法,最后一个算子添加实验与下一章节的实验有比较强的关系。因此,实验四整体起到承上启下的作用。

1 实验4-1-基于 VGG19 实现图像分类

1.1 实验目的

掌握 TensorFlow 编程框架的使用,能够使用 TensorFlow 编程框架实现利用 VGG19 网 络对输入图像进行分类,并在深度学习处理器 DLP 上实现图像分类。具体包括:

  1. 掌握使用 TensorFlow 编程框架处理深度学习任务的流程;
  2. 熟悉 TensorFlow 中几种常用数据结构的使用方法;
  3. 掌握 TensorFlow 中几种常用 API 的使用方法,包括卷积、激活等相关操作;
  4. 与第3章的实验比较,理解使用编程框架实现深度学习算法的便捷性及高效性。

1.2 实验要求

• 60 分标准:在 CPU 平台上正确实现读入输入图像、定义卷积层、池化层操作的过程。可以通过在会话中打印输入图像、卷积层计算结果和池化层计算结果来验证。 • 80 分标准:在 CPU 平台上完成网络模型的正确转换,以及网络参数的正确读取。 • 90 分标准:在 CPU 平台上正确实现对 VGG19 网络的定义,给定 VGG19 的网络参数值和输入图像,可以得到正确的 softmax 层输出结果和正确的图像分类结果。 • 100分标准:在云平台上正确实现对VGG19网络的pb格式转换及量化,给定VGG19的网络参数值和输入图像,可以得到正确的 softmax 层输出结果和正确的图像分类结果,处理时间相比 CPU 平台平均提升 10 倍以上。

1.3 实验步骤及心得收获

这次实验主要是利用Tensorflow和Scipy等框架和库重新实现VGG19的图像分类算法,实验指导手册也提供了Tensorflow一些重要的函数和使用方法:

函数名功能描述
tf.nn.conv2d( input, filter=None, strides=None, padding=None, use_cudnn_on_gpu=True, data_format=’NHWC’, dilations=[1, 1, 1, 1], name=None, filters=None)计算输入张量 input 和卷积核 filter 的卷积,返回卷积计算的结果张量。
tf.nn.bias_add( value, bias, data_format=None, name=None)对输入张量 value 加上偏置 bias,并返回一个与 value 相同类型的张量。
tf.nn.relu(features, name=None)对输入张量 features 计算 ReLU,返回一个与 features 相同数据 类型的张量。
tf.nn.softmax(logits, axis=None, name=None, dim=None )对输入张量 logits 执行 softmax 激活操作,返回一个与 logits 相同数据类型、形状的张量。
tf.nn.max_pool( value, ksize, strides, padding, data_format=’NHWC’, name=None, input=None)对输入张量 value 执行最大池化操作,返回操作的结果。
tf.nn.conv2d_transpose( value=None, filter=None, output_shape=None, strides=None, padding=’SAME’, data_format=’NHWC’, name=None, input=None, filters=None, dilations=None)计算输入张量 value和卷积核 filter 的转置积,返回计算的结果张量。

1)定义VGG19模型

按照实验三中VGG19的定义,利用Tensorflow构建VGG19的网络结构:

2)加载图片函数

这一部分主要使用了Scipy这个库,主要使用了如下的函数:

import scipy
"""
从文件 name 中读入一张图像,将其处理成 ndarray类型的数据并返回。
scipy.misc.imread(name,flatten=False, mode=None)

对图像 arr 的尺寸进行缩放,返回处理后的ndarray类型数据。
scipy.misc.imresize(arr, size,interp=’bilinear’, mode=None)
"""
def load_image(path):
   """
    使用 scipy.misc 模块读入输入图像,
    调用 preprocess 函数对图像进行预处理,
    并返回形状为(1,244,244,3)的数组 image
    """
    mean = np.array([123.68, 116.779, 103.939])
    image = scipy.misc.imresize(
        scipy.misc.imread(path), (224, 224, 3))
    image = np.expand_dims(preprocess(image, mean),axis=0)
    return image

3)在 DLP 平台运行

做这一部分的时候,我起初没有注意到模型量化的问题,怎么都运行不起来,后来经过仔细查看实验指导手册发现:

image-20230105233318829

因此,需要使用实验提供的 fppb_to_intpb 工具对原始的 pb 模型进行参数量化,生成INT8的新pb模型,这样子DLP平台才可以正常的使用模型。用下列的命令就可以调用转换。

python fppb_to_intpb.py ../vgg19_int8.ini

经过这样的处理,就终于可以在DLP平台运行代码了,通过运行实验进行对比,发现DLP的加速效果非常明显,达到了CPU速度的34倍。

1.4 实验结果

测试点测试结果
读入输入图像通过
定义卷积层和池化层通过
转换网络模型通过
CPU图像分类通过
MLU图像分类通过
CPU处理时间MLU处理时间加速比
0.25110.007234.0

2 实验4-2-实时风格迁移预测

2.1 实验目的

掌握如何使用 TensorFlow 实现实时风格迁移算法中的图像转换网络的推断模块,并进 行图像的风格迁移处理。具体包括:

  1. 掌握使用 TensorFlow 定义一个完整的网络结构的方法;
  2. 掌握使用 TensorFlow 恢复模型参数的方法;
  3. 以实时风格迁移算法为例,在 CPU 上掌握使用 TensorFlow 进行神经网络预测的方法;
  4. 理解 DLP 高性能算子库集成到 TensorFlow 框架的基本原理;
  5. 掌握在 DLP 上使用 TensorFlow 对模型进行量化并推理的能力;

2.2 实验要求

• 60 分标准:在CPU平台上正确实现实时风格迁移预测的全过程,给定输入图像、权重参数,可以实时计算并输出风格迁移后的图像。同时给出对图像进行实时风格迁移 预测的时间。 • 100分标准:在完成60分标准的基础上,在DLP平台上,给定输入图像、权重参数,能够实时输出风格迁移后的图像,同时给出基于 DLP 硬件对图像进行实时风格迁移 预测的时间并和 CPU 对比。

2.3 实验步骤及心得收获

实时风格迁移算法用一个图像转换网络和一个特征提取网络来做。大致结构如下:

2.3.1 读取图片

读取图片的步骤与实验三中的类似,同样是使用scipyimreadimresize:

def get_img(src, img_size=False):
    """
    使用 scipy.misc 模块读入输入图像 src 并
    转化成’RGB’ 模式,返回 ndarray 类型数组 img
    """
    img = scipy.misc.(src, mode="RGB")
    img = scipy.misc.imresize(img, img_size) 
    if img_size else img
    return img

2.3.2 CPU上执行

这一步主要需要完成实时风格迁移的函数定义,主要还是按照空缺处的TODO提示来完成,整体来说比较容易。主要是要完成一些与图像形式的转换相关的代码:

# TODO:如果 data_in 是保存输入图像的文件路径,即 is_paths 为 True,则读入第一张图像,由于 pb 模型的输入维度为 1 × 256 × 256 × 3, 因此需将输入图像的形状调整为 256 × 256,并传递给 img_shape;
# 如果 data_in 是已经读入图像并转化成数组形式的数据,即 is_paths 为 False,则直接获取图像的 shape 特征 img_shape
if is_paths:
    img_sp = get_img(data_in[0], (256, 256, 3)).shape
else:
    img_sp = data_in[0].shape
# .......    

# TODO:读入的输入图像的数据格式为 HWC,还需要将其转换成 NHWC
batch_shape = (batch_size, ) + img_sp
#.........

# TODO:读入的输入图像的数据格式为 HWC,还需要将其转换成 NHWC
batch_shape = (batch_size, ) + img_sp

2.3.3 DLP上执行

DLP上的模型推断代码与CPU上的几乎一致,很容易实现,但是经过实验4.1的教训后,知道了DLP平台上的模型必须经过量化后才能运行已经提前训练好的图像转换网络的数据类型为 Float32,需要经过量化后才可以在 DLP上运行。在 fppb_to_intpb 目录下运行以下命令,使用量化工具完成对模型的量化,生成新模型 udnie_int8.pb。

python fppb_to_intpb.py  udnie_int8.ini

2.4 实验结果

处理结果验证CPU处理时间MLU处理时间加速比
通过1.87360.061430.0
CPU输出图像MLU输出图像
imgimg

3 实验4-3-实时风格迁移的训练

3.1 实验目的

掌握如何使用 TensorFlow 实现实时风格迁移算法的训练。具体包括:

  1. 掌握使用 TensorFlow 定义损失函数的方法;
  2. 掌握使用 TensorFlow 存储网络模型的方法;
  3. 以实时风格迁移算法为例,掌握使用 TensorFlow 进行神经网络训练的方法。

3.2 实验要求

• 60 分标准:正确实现特征提取网络及损失函数的构建。给定输入的内容图像、风格图像,首先通过图像转换网络输出生成图像,再根据内容图像、生成图像以及风格图 像来计算损失函数值。正确实现实时风格迁移的训练过程,给定输入图像、风格图像,可以通过训练过程使得 loss 值逐渐减少。 • 80 分标准:在图像转换网络中使用实例归一化替代批归一化,正确实现实时风格迁移 的训练过程,给定输入图像、风格图像,可以通过训练过程使得 loss 值逐渐减少 • 100分标准:正确实现检查点文件的保存及恢复功能,使得每经过一定训练迭代次数 即将当前参数保存在特定检查点文件中,且图像转换网络可使用该参数生成图像,以验证训练效果。

3.3 实验步骤及心得收获

实时图像风格迁移算法的流程如下图所示:

特征提取网络的结构由 VGG16 替换成 VGG19,使得特征提取网络的网络深度更深,网络参数更多,这样网络的表达能力更强,特征提取的区分度更强,效果也更好。VGG19的结构已经在上一次的实验中有了非常充分的了解。

3.3.1 卷积层、残差块

1)卷积层

根据提示,比较容易完成卷积层的代码。

image-20230106171704843

2)残差块

图像转换网络中包含了五个残差块,其基本结构如图所示:输入 x 经过一个卷积层,再做 ReLU,然后经过另一个卷积层得到 F(x),再加上 x 得到输出 H(x) = F(x) + x,然后做 ReLU 得到基本块的最终输出 y。当输入 x 的维度与卷积输出 F(x) 的维度不同时,需要先对 x 做恒等变换使二者维度一致,然后再加和。 与常规的卷积神经网络相比,残差块增加了从输入到输出的直连,其卷积拟合的是残差。由于输入和输出都做了批归一化,符合正态分布,因此输入和输出可以做减法。残差网络的优点是对数据波动更灵敏,更容易求得最优解,因此能够改善深层网络的训练 。

3)转置卷积

转置卷积又可以称为小数步长卷积,输入矩阵 InputData 是 2×2 的矩阵,卷积核 Kernel 的大小为 3×3,卷积步长为 1,输出 OutputData 是 4×4的矩阵。 转置卷积的原理如下图所示:

实验指导手册已经给出了他的操作流程:

按照上述规则完成转置卷积层的代码:

def _conv_tranpose_layer(net, num_filters, filter_size, strides, type=0):
    # TODO:准备好权重的初值
    weights_init = _conv_init_vars(net, num_filters, filter_size, transpose=True)
    batch_size, rows, cols, in_channels = [i.value for i in net.get_shape()]
    new_rows, new_cols = int(rows * strides), int(cols * strides)
    # TODO:输入的 num_filters、strides 参数为标量,需将其处理成转置卷积函数能够使用的数据形式
    new_shape = [batch_size, new_rows, new_cols, num_filters]
    tf_shape = tf.stack(new_shape)
    strides_shape = [1,strides,strides,1]

    # TODO:进行转置卷积计算
    net = tf.nn.conv2d_transpose(net, weights_init, tf_shape, strides_shape, padding='SAME') 
    
    # 对卷积计算结果进行批归一化处理
    if type == 0:
        net = _batch_norm(net)
    elif type == 1:
        net = _instance_norm(net)
    
    # TODO:对归一化结果进行 ReLU 操作
    net = tf.nn.relu(net)

    return net

3.3.2 定义特征提取网络

特征提取网络采用与第3.1节相同的 VGG19 模型文件,使用与第4.1小节类似的定义方法。结合实验的提示和之前的经验,这一步很容易就能完成。

3.3.3 损失函数

1)内容损失

根据实验指导手册,内容损失的公式如下:

其中, C_jH_jW_j 分别表示第 j 层卷积输出特征图的通道数、高度和宽度, ϕ(y) 是损失网络中第 j 层卷积输出的特征图,实际中选择第 7 层卷积的特征计算特征重建损失。 转化成代码如下图所示:

2)风格损失

j 层卷积后的风格重建损失为输出图像和目标图像的格拉姆矩阵的差的 F-范数:

其中,格拉姆矩阵 Gj(x) 为 Cj × Cj 大小的矩阵,矩阵元素为:

根据这样的公式转化成对应的代码:

3)全变分正则化

本实验中,为了平滑输出图像,消除图像生成过程中可能带来的伪影,在损失函数中增加了全变分正则化部分。其计算方法为将图像水平和垂直方向各平移一个像素,分别与原图相减,然后计算两者 L2 范数的和。 实现方式如下:

# TODO:将图像 preds 向水平和垂直方向各平移一个像素,分别与原图相减,分别计算二者的 𝐿2 范数 x_tv 和 y_tv
# Hint: use tf.nn.l2_loss
y_tv = tf.nn.l2_loss(preds[:,1:,:,:] - preds[:,:batch_shape[1]-1,:,:])
x_tv = tf.nn.l2_loss(preds[:,:,1:,:] - preds[:,:,:batch_shape[2]-1,:])
tv_loss = tv_weight*2*(x_tv/tv_x_size + y_tv/tv_y_size)/batch_size

4)整体损失

整体损失是上面三项的和:

loss = content_loss + style_loss + tv_loss

3.4 实验结果

测试结果
批归一化网络训练实例归一化网络训练检查点文件的保存及恢复
通过通过通过
批归一化测试结果(Loss值变化率:-3846505.904762)
Iteration: 1Iteration: 2Iteration: 3Iteration: 4Iteration: 5Iteration: 6Iteration: 7Iteration: 8Iteration: 9Iteration: 10
239996000.0164150600.0146224460.0138804580.0137535950.0131331840.0133558776.0132089200.0127947990.0131223640.0
实例归一化测试结果(Loss值变化率:-3845418.857143)
Iteration: 1Iteration: 2Iteration: 3Iteration: 4Iteration: 5Iteration: 6Iteration: 7Iteration: 8Iteration: 9Iteration: 10
239996050.0164151490.0146226540.0138805710.0137533420.0131332456.0133564024.0132094550.0127953976.0131233030.0

4 实验4-4-自定义 TensorFlow CPU 算子

4.1 实验目的

掌握如何在 TensorFlow 中新增自定义的 PowerDifference 算子。具体包括:

  1. 熟悉 TensorFlow 整体设计机理;
  2. 通过对风格迁移 pb 模型的扩展,掌握对 TensorFlow pb 模型进行修改的方法,理解 TensorFlow 是如何以计算图的方式完成对深度学习算法的处理;
  3. 通过添加自定义的PowerDifference算子,加深对TensorFlow算子实现机制的理解,掌握在 TensorFlow 中添加自定义 CPU 算子的能力,为后续在 TensorFlow 中集成添加自定义的 DLP 算子奠定基础。

4.2 实验要求

• 60分标准:完成PowerDifferenceNumPy算子和C++算子的编写和注册工作,对于实验平台提供的测试数据可以做到精度正常。 • 80分标准:在60分基础上,对于实验平台提供的大规模测试数据(多种输入Shape) 可以做到精度正常。 • 100分标准:在此前的基础上,基于两种算子实现方式进行模型推理的精度正常,且 C++ 实现方式性能要优于 Numpy 实现方式。

4.3 实验步骤及心得收获

本实验加的算子叫做PowerDiffence算子,可以替代原有的SquareDifference,更通用。

此次实验的流程如下图所示:

4.3.1 替换现有模型中的op

PowerDifference算子显然是兼容SquareDifference算子的,替换方法需要利用课程提供的两个脚本,pb_to_pbtxt.pypbtxt_to_pb.py这两个工具。

1)pb→pbtxt

先用pb_to_pbtxt.py把训练好的模型转换成文本格式,用下列的命令实现

python pb_to_pbtxt.py models/pb_models/udnie.pb udnie.pbtxt

2)编辑文本格式中的节点

添加一个输入节点

SquaredDifference节点修改为PowerDifference节点,还要把以SquaredDifference作为输入的节点的input也替换成PowerDifference

3)pbtxt→pb

最后要重新利用pbtxt_to_pb.py将编辑后的模型txt输出为拓展后的pb模型

4.3.2 添加 numpy cpu算子

根据powerdifference的计算公式,可以很容易添加一个python代码实现的 cpu 算子。只需要实现一个函数就可以了,根据实验手册的提示完成空缺部分,核心的部分其实就是一个for循环:

for i in range(x_new_shape[0]):
    difference = x[i] - y
    power_difference = difference ** input_z
    output.append(power_difference)

<img src="https://gallery-of-jafari.oss-cn-beijing.aliyuncs.com/caleb.ink/2023/03/24/image-20230106135012445.png?lastModify=1679669827" zoom:60%" >

4.3.3 添加c++ cpu算子

1)C++编写Kernel函数

tensorflow/core/kernels/路径下创建*.cc.h文件用来保存算子kernel函数的c++代码。一个算子是一个Object,都继承自OpKernel类。我们要实现算子对象的构造函数Compute方法。Kernel函数的编写,需要遵循tensorflow的规则,包括很多宏定义的操作。

namespace tensorflow{    
template<typename T>
    class PowerDifferenceOp: public OpKernel {
    public:
        explicit PowerDifferenceOp(OpKernelConstruction* context):Opkernel(context){
            //do nothing
        }//end of contructor


        void Compute(OpKernelContext* context)override {
            //注:override关键字是提示编译器检查的。
            /*获取输入张量的引用*/
            const Tensor& input_x_tensor=context->input(0);
            const Tensor& input_y_tensor=context->input(1);
            const Tensor& input_z_tensor=context->input(2);

            const Eigen::ThreadPoolDevice& device=context->eigen_device(); //待理解

            /*分配输出张量,并写入context*/
            BCast bcast(BCast::FromShape(input_y_tensor.shape()), BCast::FromShape(input_x_tensor.shape()),true);
            Tensor* output_tensor=nullptr;
            TensorShape output_shape=BCast::ToShape(bcast.output_shape());
OP_REQUIRES_OK(context,
                           context->allocate_output(0, output_shape, &output_tensor));
            
            
            /*输入张量广播(创建一个中间张量然后根据原张量扩展)*/
            Tensor input_x_broad(input_x_tensor.dtype(), output_shape);
            Tensor input_y_broad(input_y_tensor.dtype(), output_shape);
            OP_REQUIRES_OK(context,
                    context->allocate_temp(input_x_tensor.dtype(),
                                           output_shape,
                                            &input_x_broad));
      OP_REQUIRES_OK(context,
                    context->allocate_temp(input_y_tensor.dtype(),
                                           output_shape,
                                            &input_y_broad));

            //扩展广播是依靠的Eigen这个库
            functor::BroadcastTo()(device, context, input_x_broad, output_shape,input_x_tensor, input_x_tensor.shape(), bcast);
            functor::BroadcastTo()(device, context, input_y_broad, output_shape,input_y_tensor, input_y_tensor.shape(), bcast);
            
            /*运算前整理形状*/
            auto input_x = input_x_broad.flat();
            auto input_y = input_y_broad.flat();
            auto input_pow = input_pow_tensor.flat();
            auto output = output_tensor->flat();
            const int N = input_x.size();
            const int POW = input_pow(0); 
            float tmp = 0;
      
            /**上面都是一系列的准备工作,下面是核心的计算过程***/
            for(int i=0; i
                tmp=input_x(i)-input_y(i);
                output(i)=tmp;
                for(int j=0; j-1; j++){
                    output(i)=output(i)*tmp;
                }
            }


        }//end of member function: Compute

    }; //end of class PowerDifferenceOp
    
REGISTER_KERNEL_BUILDER(Name("PowerDifference").Device(DEVICE_CPU), PowerDifferenceOp<float>);
}//end of namespace tensorflow

最后这个REGISTER_KERNEL_BUILDER是通过宏定义进行一串函数调用

//tensorflow/core/framework/op_kernel.h
#define REGISTER_KERNEL_BUILDER(kernel_builder, ...) \
  REGISTER_KERNEL_BUILDER_UNIQ_HELPER(__COUNTER__, kernel_builder, __VA_ARGS__)

#define REGISTER_KERNEL_BUILDER_UNIQ_HELPER(ctr, kernel_builder, ...) \
  REGISTER_KERNEL_BUILDER_UNIQ(ctr, kernel_builder, __VA_ARGS__)

#define REGISTER_KERNEL_BUILDER_UNIQ(ctr, kernel_builder, ...)        \
  constexpr bool should_register_##ctr##__flag =                      \
      SHOULD_REGISTER_OP_KERNEL(#__VA_ARGS__);                        \
  static ::tensorflow::kernel_factory::OpKernelRegistrar              \
      registrar__body__##ctr##__object(                               \
          should_register_##ctr##__flag                               \
              ? ::tensorflow::register_kernel::kernel_builder.Build() \
              : nullptr,                                              \
          #__VA_ARGS__,                                               \
          [](::tensorflow::OpKernelConstruction* context)             \
              -> ::tensorflow::OpKernel* {                            \
            return new __VA_ARGS__(context);                          \
          });

2)在C++文件中注册新Op

Op的注册和实现是两个独立的,注册是为了定义其名字、输入和输出。注册体现在两个地方。一个是在算子实现(编写kernel函数时)的旁边,本例是在tensorflow/core/kernels/cwise_op_power_difference.cc里。

REGISTER_KERNEL_BUILDER(
  Name("PowerDifference").Device(DEVICE_CPU), 
  PowerDifferenceOp<float>);

二个是在注册用到的文件在tensorflow/core/ops中,我们把PowerDifference算子的注册写在math_ops.cc中。

REGISTER_OP("PowerDifference")
    .Input("x: T")
    .Input("y: T")
    .Input("pow: T")
    .Output("z: T")
    .Attr(
        "T: {bfloat16, float, half, double, int32, int64, complex64, "
        "complex128}")
    .SetShapeFn([](::tensorflow::shape_inference::InferenceContext* c){
      c->set_output(0, c->input(0));
      c->set_output(0, c->input(1));
      c->set_output(0, c->input(2));
      return Status::OK();
    });

上面的注册过程实际上是通过宏定义来让过程更加清晰,它的背后是一串函数调用。部分宏定义如下:

//tensorflow/core/framework/op.h
#define REGISTER_OP(name) REGISTER_OP_UNIQ_HELPER(__COUNTER__, name)
#define REGISTER_OP_UNIQ_HELPER(ctr, name) REGISTER_OP_UNIQ(ctr, name)
#define REGISTER_OP_UNIQ(ctr, name)                                          \
  static ::tensorflow::register_op::OpDefBuilderReceiver register_op##ctr    \
      TF_ATTRIBUTE_UNUSED =                                                  \
          ::tensorflow::register_op::OpDefBuilderWrapper
              name)>(name)

3)算子编译

需要修改tensorflow/core/kernel/BUILD文件。BUILD文件遵循bazel的规范,因为tensorflow是采用bazel进行编译的。需要简单的了解bazel的编译规则,Bazel是一个构建工具,即一个可以运行编译和测试来组装软件的工具,跟Make、Ant、Gradle、Buck、Pants和Maven一样。

<img src="https://gallery-of-jafari.oss-cn-beijing.aliyuncs.com/caleb.ink/2023/03/24/image-20230106140113236.png?lastModify=1679669827" zoom:60%" >

执行编译命令,编译命令很复杂,不是两三行可以搞定的,因此将编译命令写成了一个shell脚本,通过执行脚本来完成编译。

# 查看主机bazel检查版本>=0.24
$ bazel version
# 激活环境
$ source env.sh
# 执行编译脚本
$ cd tensorflow/
$ ./build_tensorflow-v1.10_mlu.sh

4.3.4测试

集成到tensorflow之后就可以通过tensorflow来调用了

with tf.Session() as sess:
    output=tf.power_difference(input_x, input_y, input_pow)
    sess.run(output)

4.4 实验结果

小规模数据集测试结果
C++ 测试精度Numpy 测试精度测试结果
1.5449471333365215e-06%5.8291128344162815e-06%通过
大规模数据集测试结果
C++ 测试精度Numpy 测试精度测试结果
0.0%1.178727792681365e-05%通过
模型推理测试结果(ms)
C++ inference(CPU) origin pb timeC++ inference(CPU) timeNumpy inference(CPU) time
0.91274189949035640.75000095367431641.6945741176605225