IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 人工智能 -> Tensorflow 2.0 使用流程详解 -> 正文阅读

[人工智能]Tensorflow 2.0 使用流程详解

Tensorflow 2.0 使用流程详解

前言 :明确神经网络搭建流程,列举了过程中所有实现方法。
絮叨几句
自己最初就是想借 tensorflow 架构一个简单网络,但看了网上诸多教程,依旧对 tensorflow 如何去实现感到糊涂,官方文档教程和指南也感觉逻辑搞得相当混乱和复杂,各种方法混用,看了反而更莫名其妙,获取到的知识碎片化严重,还记不牢。

更有些教程知识点反而集中到了感知机、线性回归、各类神经网络上。我只是想搞清架构网络的方法而已,比如怎么架构,最重要的有几种实现方法,毕竟tensorflow只是工具而已,没有整体概念,知识无法宏观掌控,就会觉得这玩意的学习乱七八糟、没有尽头似的。摸不清工具的底细是很难受的。

索性写笔记,记录获取到的知识点。也做总结归纳用。夹杂自己的理解,难免有错,仅供参考。

0. 写在前面

Tensorflow 模型架构比较成熟,系统复杂庞大,学习代价相对高些,但优点是稳定,可这一点就足以受到工业青睐。事实上,目前工业上大多都是使用这一架构部署人工智能网络。

至于其它的一些人工智能架构,一般特点是易用性强,易上手。多用于科研。如PyTorch,还有曾经的 Keras,但随着谷歌对Keras的收购,其单独的 Keras 库不再更新,取而代之的是,Keras 融入到了 tensorflow 中,变成其一个子模块tf.kerastf.keras 的所有实现都是以 tensorflow 为基础。(注:Kerastf.keras 有些地方会有稍微的差别)

自己感觉学习 tensorflow 比较好,因为它就是神经网络架构的大杂烩,啥都有,啥都不缺,底层的基础函数、中层API、高层API,应有尽有。实现某一功能,可以借助不同层级的 API ,从而达到多种实现,这可能也是看完诸多教程知识混乱的原因。

贴一个比较重要的图,Tensorflow 的系统架构,对tensorflow整体情况有个了解,如下

在这里插入图片描述

其中,作为新手,只把它当做人工智能研究的工具,只需关注客户端部分就可以了。我们用python编写 tensorflow 代码就是客户端层面。

客户端层面,编写代码本质上就是函数组合调用,或者说是使用tensorflow提供的 API。但这也分情况:

  • 一种是直接调用底层函数

    如变量常量声明、一些数学运算等。对应于tf.Variable,tf.constant,tf.nn.conv2d,tf.GradientTape 等等

  • 一种是调用 tensorflow 已经把底层函数组合封装好以实现某一常用功能的函数(其实自己用底层函数也可以实现)。

    如封装好一些卷积运算、激活函数的层,封装好的网络评估函数等。对应于 tf.keras.layers, tf.keras.losses, tf.keras.optimizers 等等,这些函数可能存在优化,可能比自己实现计算效率要高。

    在未收购 keras 前,tensorflow 也有 tf.layers, tf.losses 等,现在仍旧存在。应该跟tf.keras.layerstf.keras.losses 差别不到哪去。

事实上,还有第三种,更高层的 API。就是

  • 把第二种得到的如tf.keras.layers的网络层组合起来,直接组建一个神经网络模型,即 tf.keras.model

    该模型是作为类出现,使用者自定义模型时,可以基于这个基本类(继承)进行修改。添加自己想要的一些功能。

记得最初接触 tensorflow.v1 的时候,编写网络还都是青一色的自己手写各种层的实现(第一种API),手写网络性能评估。可能源于收购 Keras 的原因,现在更方便更普遍的是直接调用更高一层封装好的功能函数(第二种API)。Pytorch等其它网络的易用性也体现在此处,开发人员已经把常用功能函数封装,提供API接口,因此实现某个新网络搭建更快捷,使用者直接调用即可。

所以综上所述,可以把 tensorflow 提供的函数,或者说 API 分为三类:低阶API中阶API高阶API

用一张图表示更清晰明了些:

在这里插入图片描述

所以,搭建神经网络时,就不止一种方式可以实现之。

不仅如此,训练网络也不止一种方法可以实现之。

更有甚者,你想要用 tensorflow 实现某一功能,都总有很多种方法可以实现之。

1. 数据准备

先明确一个目标吧。因为自己多处理图像,就实现用一个简单神经网络实现一个简单功能。确切说是

用经典的 LeNet 网络实现 手写数字识别

1.1 数据获取

最通用方法就是下载数据集,现在热点人工智能方面的问题,网上都会有对应的公开数据集。当然也可以自己制作,比如借助网络和爬虫的力量 【使用爬虫构建新闻文本数据集】

至于手写数据集,神经网络的hello world,网络上有。当然,使用起来有两种方法,

现在也有个 Fasion-MNIST 数据集,其外在特征跟 MNIST 完全一致,只是内容变成了10类商品,克服手写数字集太简单问题,更复杂、具象化些。下载从这:【Fasion-MNIST 数据集】

  • 使用代码,自动下载

    from tensorflow.keras.datasets import mnist
    # 下载并加载数据
    (x_train, y_train), (x_test, y_test) = mnist.load_data()
    
  • 下载至本地,直接使用

    import numpy as np
    
    MNIST = np.load('./mnist.npz')
    x_train, y_train = MNIST['x_train'], MNIST['y_train']
    x_test, y_test = MNIST['x_test'], MNIST['x_test']
    
    # 或者使用 tensorflow,注:下面路径要填写绝对路径!不然依旧会从网络上下载!
    (x_train, y_train), (x_test, y_test) = mnist.load_data(r'd:\mnist-npz\mnist.npz')
    
    #------ tensorflow 2.0 中下面方法已不可用 --------#
    # from tensorflow.examples.tutorials.mnist import input_data
    # MNIST = input_data.read_data_sets('./MNIST_data', one_hot = True)
    
    # 可以自己解压,自己定义函数加载,这里牵扯到 *.gz 格式文件认知和解码。不想研究。
    

1.2 数据处理

本例数据比较简单,已经划分好训练集和测试集,数据量也小,不用怎么处理。

只要想办法载入就可以了,遵循五步走即可,直接上代码

# 自定义一些特殊处理
def preprocess(x, y):
    # 将图像归一化的 [0,1]
    x = tf.cast(x, tf.float32)/ 255
    # 将标签转成 one-hot 格式
    y = tf.cast(y, tf.int32)
    y = tf.one_hot(y, depth=10)
    return x, y

# 1.另样加载
train_data = tf.data.Dataset.from_tensor_slices((x_train, y_train))

# 2.特殊处理
train_data = train_data.map(preprocess)

# 3.复制几份
train_data = train_data.repeat(3)

# 4.打乱顺序
train_data = train_data.shuffle(5000)

# 5.分批训练
batch_size = 500 # 一次喂 1000 个数据
train_data = train_data.repeat(3).shuffle(5000).batch(batch_size)

# 说明:事实上,这些功能后面 model.fit() 就可以实现了,因此后面使用model搭建的模型进行训练时并没用到。

另外值得强调的是!真实项目中数据处理是极其极其极其重要的一部分,并且这一重要部分牵扯到至少三个重要知识点:

(1)数据集划分

包括训练集、验证集、测试集等。

听起来似乎很简单,但这是个技术活,好的数据集划分和使用可以大大提高网络性能。

(2)数据增广

当训练集中数据很少时,通过一些手段来增加训练数据数量。见【深度学习之数据增强方式和实现总结】

(3)数据读取

  • 当数据量很少时,可以直接读取,加载到内存中,导入到网络中,进行训练。很推荐这么做,这种方法很高效、快捷。

  • 当数据量很大时,不能一次性全部加载入内存,此时就需要一些手段来处理这些数据。此时会牵扯到 标准tensorflow数据格式(TFRecords) , **数据输入方法(tf.data API, Queue API, PreloadedData, Feeding)**等一系列”复杂“内容。

此外还可能包括数据归一化处理等,每个都是一个值得研究的知识点。现不详述,现也不深究。(曾痛苦地深究过,但都已忘记)

但先有概念、框架,了解其在整个流程的大概位置,具体内容后续再详细了解,再向框架中定点填充实物。

2. 网络搭建

经典 LeNet 网络,结构如下:

在这里插入图片描述

LayerDescriptionoutput_shapeparams
INPUTInput_data32 x 320
C1Conv 5x5 , strides= 1, filter_number = 66, 28 x 28156
S2Pooling 2x2, strides = 2 / sigmoid6, 14 x 1412
C3Conv 5x5, strides = 1, filter_number = 1616, 10 x 1024164
S4Pooling 2x2, strides = 2 / sigmoid16, 5 x 532
C5FC 12012048120
F6FC 84 /sigmoid8410164
OUTPUTFC 1010840

下面使用前述三种层次的 API 进行搭建。这里用到了四种方法,可能还会有别的方法。

提前说明

  • 为了保持原汁原味的 LeNet ,下面网络搭建的 padding 使用了 padding='VALID',而非'SAME'

  • 因为手写数字图像尺寸为 28x28,而 LeNet 网络输入为 32x32,为使网络适用,将 C1 层 padding 改为 padding='SAME',即第一层输出仍为 28x28,以与原网络保持一致。

2.1 方式 1 ------ 应用 tensorflow 底层函数搭建(低阶 API)

# note: shape=[Conv 5x5, channels=1, Number=6]
w1 = tf.Variable(tf.random.truncated_normal(shape = [5,5,1,6], stddev= 0.1))
b1 = tf.Variable(tf.constant(0.1, shape = [6]))
w2 = tf.Variable(tf.random.truncated_normal(shape = [5,5,6,16], stddev= 0.1))
b2 = tf.Variable(tf.constant(0.1, shape = [16]))
w3 = tf.Variable(tf.random.truncated_normal(shape = [5*5*16, 120], stddev= 0.1))
b3 = tf.Variable(tf.constant(0.1, shape = [120]))
w4 = tf.Variable(tf.random.truncated_normal(shape = [120, 84], stddev= 0.1))
b4 = tf.Variable(tf.constant(0.1, shape = [84]))
w5 = tf.Variable(tf.random.truncated_normal(shape = [84, 10], stddev= 0.1))
b5 = tf.Variable(tf.constant(0.1, shape = [10]))
# 放到一个列表里,后续指定要更新训练的参数时要用
w_b = [w1, b1, w2, b2, w3, b3, w4, b4, w5, b5]

def myNet(inputs):
    # 统一下输入数据的尺寸,以满足后续卷积操作。shape = [sample_num, width, height, channels]
    inputs = tf.reshape(inputs, [-1, 28, 28, 1])

    # C1层,S2层,激活函数
    # note: strides= [batch=1,stride,stride,channels=1]
    conv1 = tf.nn.bias_add(tf.nn.conv2d(inputs, w1, strides=[1,1,1,1], padding='SAME'), b1) # out_shape=[-1,28,28,6]
    # note: ksize=[batch=1, height, width, channels=1]
    pool1 = tf.nn.max_pool(conv1, ksize=[1,2,2,1], strides=[1,2,2,1],padding='VALID') # out_shape=[-1,14,14,6]
    o1 = tf.nn.relu(pool1)

    # C3层,S4层,激活函数
    conv2 = tf.nn.bias_add(tf.nn.conv2d(o1, w2, strides=[1,1,1,1], padding='VALID'), b2) # out_shape=[-1,10,10,16]
    pool2 = tf.nn.max_pool(conv2, ksize=[1,2,2,1], strides=[1,2,2,1],padding='VALID') # out_shape=[-1,5,5,16]
    o2 = tf.nn.relu(pool2)

    # 展平,便于后续全连接
    f = tf.reshape(o2, [-1, 5*5*16])

    # C5层
    # note: shape = [in_node_nums,out_node_nums]
    fc3 = tf.matmul(f, w3) + b3 # out_shape=[-1,120]
    o3 = tf.nn.relu(fc3)

    # F6层
    fc4 = tf.matmul(o3, w4) + b4 # out_shape=[-1,84]
    o4 = tf.nn.relu(fc4)

    # OUTPUT层
    outputs = tf.matmul(o4, w5) + b5 # out_shape=[-1,10]
    
    return outputs

说明1

tensorflow 2.0 好像不支持 feed_dict了,同样也不支持 Session() ,取而代之的是,Eager execution,使用另一种方式实现 gragh。

下面自己训练这部分模型时,没有使用相关技巧,所以运算相当慢。

说明2

因为 说明1 中的缘故:没有 feed 和 Seesion 了,自己把整个网络过程搭建到函数中了,以前 tensorflow1.0 是不需要的 。

tensorflow2.0 后面训练时要用到求梯度,其中就需要整个 y_pred = f (x_inputs) 过程,所以就把它写到一个函数myNet()里了。

说明3

这里需要注意记录下网络中的需要训练的参数,后续训练时,需要指定训练参数(更新参数) trainable_variable,即这里 w1, b1, w2 …。且!一定要 在网络函数外 提前定义好,不要放在函数里!
也可以使用字典定义。

2.2 方式 2 ------ 应用 Sequential 容器线性搭建(中阶 API)

代码

# 模型搭建
model = tf.keras.models.Sequential([
    # C1层,S2层,激活函数
    tf.keras.layers.Conv2D(filters = 6, kernel_size = 5, strides= 1, input_shape=(28,28,1), padding="SAME"),
    tf.keras.layers.MaxPooling2D(pool_size = 2, strides = 2),
    tf.keras.layers.ReLU(),
    # C3层,S4层,激活函数
    tf.keras.layers.Conv2D(filters = 16, kernel_size = 3, strides = 1, padding="VALID"),
    tf.keras.layers.MaxPooling2D(pool_size = 2, strides = 2),
    tf.keras.layers.ReLU(),
    # 展平,便于后续全连接
    tf.keras.layers.Flatten(),
    # C5层,F6层,OUTPUT层
    tf.keras.layers.Dense(units = 120, activation = 'relu'),
    tf.keras.layers.Dense(units = 84, activation = 'relu'),
    tf.keras.layers.Dense(10, activation = None)
])

model.summary()

说明1:

也可以使用tf.keras.Sequential(),俩一样。

另,在向sequential容器中加入网络相关层时,也可以使用函数.add()添加方式,如下

model = tf.keras.models.Sequential()
model.add(tf.keras.layers.Conv2D())
model.add()
......

效果一样

说明2:

tf.keras.layers包含的函数还有很多,具体可参见 【tensorflow 官方文档之 tf.keras.layers】,几个比较常用的函数罗列如下:

.Input():输入层。

.Flatten():展平层。将多维张量展开为一维。

.Dense():密集连接层,即线性层或全连接层。

.Conv2D():二维卷积层。

.MaxPool2D():二维最大池化层,或下采样层。无训练参数,用于降维。

.AveragePooling2D():二维平均池化层,或下采样层。无训练参数,用于降维。

.Activation():激活函数。也可以直接在 Dense, Conv2D 参数中指定。

.Dropout():随机置零层。正则化方式的一种。

.BatchNormalization():批量标准化。通过线性变换将输入缩放到稳定均值和标准差,一般在激活函数前使用。

具体参数设置和说明,同理参见上述官方文档。

说明3:

该种方式有局限性,即,只可构造线性模型,只可模型层层堆叠。而不能构建层间有交叉、跨越、反复的复杂网络,针对这些复杂网络可以使用上述的 函数API 或下面的 Model方法。

2.3 方式 3 ------ 应用继承 Model 类灵活搭建(高阶 API)

# 模型搭建
class myModel(tf.keras.Model):
    # 构建网络
    def __init__(self):
        super(myModel, self).__init__()
        self.conv1 = tf.keras.layers.Conv2D(filters = 6, kernel_size = 5, strides= 1, padding="SAME")
        self.pool1 = tf.keras.layers.MaxPooling2D(pool_size = 2, strides = 2)
        self.Relu = tf.keras.layers.ReLU()
        
        self.conv2 = tf.keras.layers.Conv2D(filters = 16, kernel_size = 3, strides = 1, padding="VALID")
        self.pool2 = tf.keras.layers.MaxPooling2D(pool_size = 2, strides = 2)
        
        self.f = tf.keras.layers.Flatten()
        
        self.d1 = tf.keras.layers.Dense(units = 120, activation = 'relu')
        self.d2 = tf.keras.layers.Dense(units = 84, activation = 'relu')
        self.d3 = tf.keras.layers.Dense(10, activation = None)
    # 前向传播
    def call(self, inputs, training = None, mask = None):
        x = self.conv1(inputs)
        x = self.pool1(x)
        x = self.Relu(x)
        
        x = self.conv2(x)
        x = self.pool2(x)
        x = self.Relu(x)
        
        x = self.f(x)
        
        x = self.d1(x)
        x = self.d2(x)
        outputs = self.d3(x)
        return outputs

model = myModel()

2.4 方式4 ------ 应用 Model() 函数灵活搭建(与方式3等价)

# 模型搭建
INPUT = tf.keras.Input(shape = (28, 28, 1))
# C1层,S2层,激活函数
x = tf.keras.layers.Conv2D(filters = 6, kernel_size = 5, strides= 1, padding="SAME")(INPUT)
x = tf.keras.layers.MaxPooling2D(pool_size = 2, strides = 2)(x)
x = tf.keras.layers.ReLU()(x)
# C3层,S4层,激活函数
x = tf.keras.layers.Conv2D(filters = 16, kernel_size = 3, strides = 1, padding="VALID")(x)
x = tf.keras.layers.MaxPooling2D(pool_size = 2, strides = 2)(x)
x = tf.keras.layers.ReLU()(x)
# 展平,便于后续全连接
x = tf.keras.layers.Flatten()(x)
# C5层,F6层,OUTPUT层
x = tf.keras.layers.Dense(units = 120, activation = 'relu')(x)
x = tf.keras.layers.Dense(units = 84, activation = 'relu')(x)
OUTPUT = tf.keras.layers.Dense(10, activation = None)(x)

model = tf.keras.Model(inputs = INPUT, outputs = OUTPUT)

3. 网络训练与性能评估

3.1 函数式

# 定义交叉熵损失函数
def cross_entropy(y_pred, y_true):
    # 将预测值限制在一个范围之内以避免log(0)错误
    y_pred = tf.clip_by_value(y_pred, 1e-9, 1.)
    # 计算交叉熵
    res = tf.reduce_mean(-tf.reduce_sum(y_true * tf.math.log(y_pred)))
    return res
# 或者直接使用自带函数 tf.keras.losses.CategoricalCrossentropy(from_logits=True)(y_pred, y_true)也行

#  准确率计算函数,用于评估
def accuracy(y_pred, y_true):
    correct_prediction = tf.equal(tf.argmax(y_pred, 1), tf.argmax(y_true, 1))
    res = tf.reduce_mean(tf.cast(correct_prediction, tf.float32), axis=-1)
    return res


# 定义优化器
learning_rate = 0.01  # 学习率
optimizer = tf.keras.optimizers.Adam(learning_rate)

epoch = 1 # 所有样本循环多少次
display_steps = 100 # 进行到多少次时展示下模型评估结果
for n in range(epoch):
    for steps,(x, y) in enumerate(train_data):
        with tf.GradientTape() as gt:
            # 计算y预测值
            y_pred = myNet(x)
            # 计算损失
            loss = cross_entropy(y_pred, y)
		
        # 注意:这里使用的trainable_variables就是前面2.1节提到的。
        trainable_variables = w_b
        # 如果用于model模型,则可以直接 model.trainable_variables 获得,极其方便。
        grad = gt.gradient(loss, trainable_variables)
        # 依据梯度,更新
        optimizer.apply_gradients(zip(grad, trainable_variables))
        
        if steps % display_steps == 0:
            acc = accuracy(y_pred, y)
            print("steps: %i, loss: %f, accuracy: %f" % (steps, loss, acc))

说明1

这种方法适用于上面搭建的所有模型。

3.2 fit() 方式

# 尺寸要与模型输入相符
x = tf.reshape(x_train, shape = [-1, 28, 28, 1])
y = tf.keras.utils.to_categorical(y_train) # 这里跟损失函数息息相关,不注意会报错
# 数据类型也很重要,转换成tensorflow能够辨别的,适合网络的
x = tf.cast(x, tf.float64)
y = tf.cast(y, tf.int32)

# 告知训练选择的优化器、损失函数、评测指标等
model.compile(
    optimizer = tf.keras.optimizers.SGD(lr = 0.01), #使用SGD优化器,学习率为0.1
    loss = tf.keras.losses.CategoricalCrossentropy(from_logits = False), # 如果不是 one-hot,则使用 SparseCategoricalCrossentropy!!!
    metrics = ['categorical_accuracy'] #标注网络评价指标,同样,one-hot,则使用 SparseCategoricalCrossentropy!!!
)

# 告知输入的特征、标签、batch大小、数据集迭代次数等
history = model.fit(x, y, batch_size = 1000, epochs = 2)

# 网络结构和参数统计
model.summary()

说明1

尤其需要注意的是,

  • 输入、输出要匹配搭建网络的输入输出,包括尺寸、数据类型等。
  • 损失函数要根据实际情况选择(比如上面 one-hot 格式与 int 格式的交叉熵函数就不一样)
  • 该种方法不适用于自编程实现的模型,如 2.1 中自编的网络。

4. 扩展内容

  • 网络可视化:TensorBoard

  • 高效率模型训练:多GPU训练

  • 模型保存与加载

  • 模型部署

  • Estimator

再深究时补充

5. 写在后面

(1)上面所有代码都是基于 tensorflow 2.0 编写,都是可直接运行通过的。

(2)再次强烈官方文档,有任何问题可以查这个 —> 【TensorFlow 2.0 docs】。不推荐官方教程、指南。

(3)上面的网络对于手写数字识别结果非常不好,但自己目的也不在于这,而在于理清如何走通整个流程,及弄清所有可用方法。如果想结果好些,可以考虑构建下面简单网络,用 Sequential()容器搭建方法,代码如下

# 一个简单手写识别模型(参考于网上;可以尝试用上述其它方法搭建网络)
model = tf.keras.models.Sequential([
    tf.keras.layers.Flatten(),
    tf.keras.layers.Dense(units = 128, activation = 'relu'),
    tf.keras.layers.Dense(10, activation = 'softmax')
])

(4)上文中有些名词是自己瞎起的,可意会就行。

花费了一天时间整理,完结!下班!
and, Have a nice weekend!

  人工智能 最新文章
2022吴恩达机器学习课程——第二课(神经网
第十五章 规则学习
FixMatch: Simplifying Semi-Supervised Le
数据挖掘Java——Kmeans算法的实现
大脑皮层的分割方法
【翻译】GPT-3是如何工作的
论文笔记:TEACHTEXT: CrossModal Generaliz
python从零学(六)
详解Python 3.x 导入(import)
【答读者问27】backtrader不支持最新版本的
上一篇文章      下一篇文章      查看所有文章
加:2021-08-14 14:01:52  更:2021-08-14 14:03:25 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/27 21:06:39-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码