2020-10-04 吴恩达-C4 卷积神经网络-w1 CNN(课后编程2-Convolution model - Application)

原文链接

卷积神经网络 Convolution model - Application

  • 1.0 Tensorflow模型
    • 1.1 创建占位符placeholders
    • 1.2 初始化参数
    • 1.3 前向传播
    • 1.4 计算成本
    • 1.5 模型

欢迎来到第四课的第二个练习,在这里你将:

  • 在Tensorflow中实现辅助函数
  • 在Tensorflow中实现一个完整的卷积神经网络ConvNet

完成练习之后,你将能够

  • 在TensorFlow中构筑和训练ConvNet来实现分类。

完成本作业必须熟悉Tensorflow。
如果你还不会,请参考2020-8-23 吴恩达-改善深层NN-w3 超参调整/批量正则化/编程框架(课后编程-TensorFlow Tutorial-手势辨认)

1.0 Tensorflow模型

在上一个编程作业中,你利用numpy来构筑辅助函数,以此来了解ConvNet背后的原理。但是今天大多数的深度学习实践应用都是利用程序框架来实现的,这些程序框架有很多内置函数方便你调用。

和往常一下,我们先导入库

import math
import numpy as np
import h5py
import matplotlib.pyplot as plt
import scipy
from PIL import Image
from scipy import ndimage
import tensorflow as tf
from tensorflow.python.framework import ops
from cnn_utils import *

#%matplotlib inline #Jupyter Notebook中使用
np.random.seed(1)

然后导入"SIGNS"数据集

# Loading the data (signs)
X_train_orig, Y_train_orig, X_test_orig, Y_test_orig, classes = load_dataset()

提醒一下,"SIGNS"数据集中收集了6个手势来代表数字0到5。
2020-10-04 吴恩达-C4 卷积神经网络-w1 CNN(课后编程2-Convolution model - Application)_第1张图片

你可以看一下数据集中带标签的图像样本。修改下面代码中的index,可以查看不同图像。

# Example of a picture
index = 16
plt.imshow(X_train_orig[index])
plt.show()
print ("y = " + str(np.squeeze(Y_train_orig[:, index])))

运行结果

y = 2

2020-10-04 吴恩达-C4 卷积神经网络-w1 CNN(课后编程2-Convolution model - Application)_第2张图片

其实这个数据集在前面的编程作业《2020-8-23 吴恩达-改善深层NN-w3 超参调整/批量正则化/编程框架(课后编程-TensorFlow Tutorial-手势辨认)》中已经用过一次了,你利用它实现了一个全连接网络。但是因为这是一个图像数据集,所以更自然的选择是利用ConvNet来实现分类。ConvNet最重要的应用就是在图像识别领域。

开始之前,先看一下你的数据的形状。

X_train = X_train_orig/255.
X_test = X_test_orig/255.
Y_train = convert_to_one_hot(Y_train_orig, 6).T
Y_test = convert_to_one_hot(Y_test_orig, 6).T
print ("number of training examples = " + str(X_train.shape[0]))
print ("number of test examples = " + str(X_test.shape[0]))
print ("X_train shape: " + str(X_train.shape))
print ("Y_train shape: " + str(Y_train.shape))
print ("X_test shape: " + str(X_test.shape))
print ("Y_test shape: " + str(Y_test.shape))
conv_layers = {
     }

运行结果

number of training examples = 1080
number of test examples = 120
X_train shape: (1080, 64, 64, 3)
Y_train shape: (1080, 6)
X_test shape: (120, 64, 64, 3)
Y_test shape: (120, 6)

1.1 创建占位符placeholders

TensorFlow要求你为运行会话session时将送入模型中的输入数据创建占位符。

练习:实现下面的函数,为图像输入 X 和输出 Y 创建占位符。现在你还不需要定义训练样本的数量。为了做到这点,你将使用"None"作为批量数据的大小,它可以让你更加灵活的在后面再赋值。因此,现在 X 的维度是 [None, n_H0, n_W0, n_C0] ,Y的维度是[None, n_y]。

实现代码

def create_placeholders(n_H0, n_W0, n_C0, n_y):
    """
    Creates the placeholders for the tensorflow session.
    为session创建占位符
    
    Arguments:
    n_H0 -- scalar, height of an input image 实数,输入图像的高度
    n_W0 -- scalar, width of an input image 宽度
    n_C0 -- scalar, number of channels of the input 通道数量
    n_y -- scalar, number of classes 分类数量
        
    Returns:
    输入数据的占位符,维度为[None, n_H0, n_W0, n_C0],类型为"float"
    X -- placeholder for the data input, of shape [None, n_H0, n_W0, n_C0] and dtype "float"

    输入数据的标签的占位符,维度为[None, n_y],维度为"float"
    Y -- placeholder for the input labels, of shape [None, n_y] and dtype "float"
    """

    ### START CODE HERE ### (≈2 lines)
    X = tf.placeholder(tf.float32, [None, n_H0, n_W0, n_C0])
    Y = tf.placeholder(tf.float32, [None, n_y])
    ### END CODE HERE ###
    
    return X, Y

测试一下

X, Y = create_placeholders(64, 64, 3, 6)
print ("X = " + str(X))
print ("Y = " + str(Y))

运行结果

X = Tensor("Placeholder:0", shape=(?, 64, 64, 3), dtype=float32)
Y = Tensor("Placeholder_1:0", shape=(?, 6), dtype=float32)

1.2 初始化参数

你将使用tf.contrib.layers.xavier_initializer(seed = 0)来初始化过滤器/权重 W 1 W1 W1 W 2 W2 W2 。你不需要考虑偏置,因为你很快会发现TensorFlow会考虑到的。需要注意的是你只需要初始化为2D卷积函数,全连接层TensorFlow会自动初始化的。

练习:实现函数initialize_parameters()。每组过滤器的维度按照以下方式提供。

W = tf.get_variable("W", [1,2,3,4], initializer = ...)

实现代码

def initialize_parameters():
    """
    初始化权值矩阵,这里我们把权值矩阵硬编码:
    Initializes weight parameters to build a neural network with tensorflow. The shapes are:
                        W1 : [4, 4, 3, 8]
                        W2 : [2, 2, 8, 16]
    Returns:
    parameters -- a dictionary of tensors containing W1, W2 包含了tensor类型的W1、W2的字典
    """
    
    tf.set_random_seed(1)                              # so that your "random" numbers match ours
        
    ### START CODE HERE ### (approx. 2 lines of code)
    W1 = tf.get_variable("W1", [4, 4, 3, 8], initializer=tf.contrib.layers.xavier_initializer(seed=0))
    W2 = tf.get_variable("W2", [2, 2, 8, 16], initializer=tf.contrib.layers.xavier_initializer(seed=0))
    ### END CODE HERE ###

    parameters = {
     "W1": W1,
                  "W2": W2}
    
    return parameters

测试一下

tf.reset_default_graph()
with tf.Session() as sess_test:
    parameters = initialize_parameters()
    init = tf.global_variables_initializer()
    sess_test.run(init)
    print("W1 = " + str(parameters["W1"].eval()[1,1,1]))
    print("W2 = " + str(parameters["W2"].eval()[1,1,1]))

结果如下

W1 = [ 0.00131723  0.1417614  -0.04434952  0.09197326  0.14984085 -0.03514394
 -0.06847463  0.05245192]
W2 = [-0.08566415  0.17750949  0.11974221  0.16773748 -0.0830943  -0.08058
 -0.00577033 -0.14643836  0.24162132 -0.05857408 -0.19055021  0.1345228
 -0.22779644 -0.1601823  -0.16117483 -0.10286498]

1.3 前向传播

在TensorFlow里面有一些可内置函数可以提供你构建卷积神经网络:

  • tf.nn.conv2d(X,W1, strides = [1,s,s,1], padding = ‘SAME’) : 给定输入 X X X 和一组过滤器 W 1 W1 W1 ,这个函数将会自动使用 W 1 W1 W1 来对 X X X 进行卷积,第三个输入参数是([1,f,f,1]) 是指对于输入 (m, n_H_prev, n_W_prev, n_C_prev)而言,每次滑动的步伐。
  • tf.nn.max_pool(A, ksize = [1,f,f,1], strides = [1,s,s,1], padding = ‘SAME’) : 给定输入 X X X ,该函数将会使用大小为(f,f)以及步伐为(s,s)的窗口对其进行滑动取最大值(最大池)。
  • tf.nn.relu(Z1) : 计算Z1的ReLU激活。
  • tf.contrib.layers.flatten§ : 给定一个输入P,此函数将会把每个样本转化成一维的向量,然后返回一个tensor变量,其维度为(batch_size,k)。
  • tf.contrib.layers.fully_connected(F, num_outputs) : 给定一个已经一维化了的输入F,此函数将会返回一个由全连接层计算过后的输出。

使用tf.contrib.layers.fully_connected(F, num_outputs)的时候,全连接层会自动初始化权值,同时在你训练模型的时候它也会一直参与。因此当你初始化参数的时候,你不需要专门去初始化它的权值。

练习:构筑函数forward_propagation,实现以下模型:CONV2D -> RELU -> MAXPOOL -> CONV2D -> RELU -> MAXPOOL -> FLATTEN -> FULLYCONNECTED。

具体实现的时候,我们需要使用以下的步骤和参数:

  • Conv2D: 步长 1, 填充方式 “SAME”
  • ReLU
  • Max pool: 过滤器大小 8 x 8,步长 8 x 8, 填充方式"SAME"
  • Conv2D: 步长 1, 填充方式"SAME"
  • ReLU
  • Max pool: 过滤器大小 4 x 4,步长 4 x 4, 填充方式"SAME"
  • 一维化上一层的输出
  • 全连接层(FC):使用没有非线性激活函数的全连接层。这里不要调用SoftMax。这将导致输出层中有6个神经元,然后再传递到softmax。 在TensorFlow中,softmax和成本函数被集中到一个函数中,在计算成本时你将调用不同的函数。

实现代码

# GRADED FUNCTION: forward_propagation

def forward_propagation(X, parameters):
    """
    实现前向传播
    Implements the forward propagation for the model:
    CONV2D -> RELU -> MAXPOOL -> CONV2D -> RELU -> MAXPOOL -> FLATTEN -> FULLYCONNECTED
    
    Arguments:
    输入数据的placeholder,维度为(输入节点数量,样本数量)
    X -- input dataset placeholder, of shape (input size, number of examples)

    包含了“W1”和“W2”的字典。
    parameters -- python dictionary containing your parameters "W1", "W2"
                  the shapes are given in initialize_parameters

    Returns:
    Z3 -- the output of the last LINEAR unit 最后一个LINEAR节点的输出
    """
    
    # Retrieve the parameters from the dictionary "parameters" 
    W1 = parameters['W1']
    W2 = parameters['W2']
    
    ### START CODE HERE ###
    # CONV2D: stride of 1, padding 'SAME'
    #CONV2D : 步伐:1,填充方式:“SAME”
    Z1 = tf.nn.conv2d(X, W1, strides=[1, 1, 1, 1], padding='SAME')
    
    # RELU
    A1 = tf.nn.relu(Z1)

    # MAXPOOL: window 8x8, stride 8, padding 'SAME'
    # 窗口大小:8x8,步伐:8x8,填充方式:“SAME”
    P1 = tf.nn.max_pool(A1, ksize = [1, 8, 8, 1], strides = [1, 8, 8, 1], padding='SAME')

    # CONV2D: filters W2, stride 1, padding 'SAME'
    # CONV2D: 步伐:1,填充方式:“SAME”
    Z2 = tf.nn.conv2d(P1, W2, strides=[1, 1, 1, 1], padding='SAME')

    # RELU
    A2 = tf.nn.relu(Z2)
    # MAXPOOL: window 4x4, stride 4, padding 'SAME'
    # 过滤器大小:4x4,步伐:4x4,填充方式:“SAME”
    P2 = tf.nn.max_pool(A2, ksize = [1, 4, 4, 1], strides = [1, 4, 4, 1], padding='SAME')

    # FLATTEN #一维化上一层的输出
    P = tf.contrib.layers.flatten(P2)

    # FULLY-CONNECTED without non-linear activation function (not not call softmax).
    #全连接层(FC):使用没有非线性激活函数的全连接层
    # 6 neurons in output layer. Hint: one of the arguments should be "activation_fn=None" 
    Z3 = tf.contrib.layers.fully_connected(P, 6, activation_fn=None)
    ### END CODE HERE ###

    return Z3

测试一下

tf.reset_default_graph()

with tf.Session() as sess_test:
    np.random.seed(1)
    X, Y = create_placeholders(64, 64, 3, 6)
    parameters = initialize_parameters()
    Z3 = forward_propagation(X, parameters)
    init = tf.global_variables_initializer()
    sess_test.run(init)
    a = sess_test.run(Z3, {
     X: np.random.randn(2,64,64,3), Y: np.random.randn(2,6)})
    print("Z3 = " + str(a))

运行结果

Z3 = [[ 1.4416984  -0.24909666  5.450499   -0.2618962  -0.20669907  1.3654671 ]
 [ 1.4070846  -0.02573211  5.08928    -0.48669922 -0.40940708  1.2624859 ]]

这个结果和原文中不一致。我的环境是py3.6+tensorflow 1.4。检查了代码,没有问题。

不得已,换了一个环境,使用Anaconda3,先安装tensorflow

C:\Users\toddc>conda install tensorflow
Fetching package metadata .............
Solving package specifications: .

Package plan for installation in environment C:\Users\toddc\Anaconda3:

The following NEW packages will be INSTALLED:

    backports.weakref: 1.0rc1-py36_0
    blas:              1.0-mkl
    bleach:            1.5.0-py36_0
    html5lib:          0.9999999-py36_0
    libprotobuf:       3.2.0-vc14_0
    markdown:          2.6.9-py36_0
    protobuf:          3.2.0-py36_0
    tensorflow:        1.2.1-py36_0
    vc:                14-0

The following packages will be UPDATED:

    conda:             4.3.8-py36_0     --> 4.3.30-py36h7e176b0_0
    vs2015_runtime:    14.0.25123-0     --> 14.0.25420-0

Proceed ([y]/n)? y

blas-1.0-mkl.t 100% |###############################| Time: 0:00:00   2.44 MB/s
libprotobuf-3. 100% |###############################| Time: 0:00:03   2.79 MB/s
markdown-2.6.9 100% |###############################| Time: 0:00:00   6.35 MB/s
backports.weak 100% |###############################| Time: 0:00:00   3.35 MB/s
html5lib-0.999 100% |###############################| Time: 0:00:00   8.44 MB/s
protobuf-3.2.0 100% |###############################| Time: 0:00:00   8.22 MB/s
bleach-1.5.0-p 100% |###############################| Time: 0:00:00   4.48 MB/s
tensorflow-1.2 100% |###############################| Time: 0:00:06   3.39 MB/s
conda-4.3.30-p 100% |###############################| Time: 0:00:00   1.38 MB/s

tensorflow版本低了,不过不影响执行代码。

使用IPython,执行结果

Z3 = [[-0.44670227 -1.57208765 -1.53049231 -2.31013036 -1.29104376  0.46852064]
 [-0.17601591 -1.57972014 -1.4737016  -2.61672091 -1.00810647  0.5747785 ]]

太神奇了,正确了,等空了再找原因把。

1.4 计算成本

你可以利用以下2个函数来完成成本计算

  • tf.nn.softmax_cross_entropy_with_logits(logits = Z3, labels = Y) : 计算softmax的熵损失。这个函数既计算softmax的激活,也计算其损失。
  • tf.reduce_mean: 计算的是平均值,使用它来计算所有样本的损失来得到总成本。

代码如下

# GRADED FUNCTION: compute_cost 

def compute_cost(Z3, Y):
    """
    Computes the cost
    
    Arguments:
    Z3 -- output of forward propagation (output of the last LINEAR unit), of shape (6, number of examples) 正向传播最后一个LINEAR节点的输出,维度为(6,样本数)。
    Y -- "true" labels vector placeholder, same shape as Z3
    
    Returns:
    cost - Tensor of the cost function
    """
    
    ### START CODE HERE ### (1 line of code)
    cost = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits=Z3, labels=Y))
    ### END CODE HERE ###
    
    return cost

测试一下

tf.reset_default_graph()

with tf.Session() as sess:
    np.random.seed(1)
    X, Y = create_placeholders(64, 64, 3, 6)
    parameters = initialize_parameters()
    Z3 = forward_propagation(X, parameters)
    cost = compute_cost(Z3, Y)
    init = tf.global_variables_initializer()
    sess.run(init)
    a = sess.run(cost, {
     X: np.random.randn(4,64,64,3), Y: np.random.randn(4,6)})
    print("cost = " + str(a))

结果

cost = 2.91034

1.5 模型

最终你需要把上面完成的函数合并构建模型。然后使用它训练SIGNS数据集。

你已经在《2020-8-15 吴恩达-改善深层NN-w2 优化算法(课后编程-Optimization Methods-3种算法:Mini-batch下降/带动量/Adam)》中实现了random_mini_batches()函数,它会返回mini-batches列表

练习:实现模型,包含

  • 创建占位符
  • 初始化参数
  • 前向传播
  • 计算成本
  • 创建优化器

最终你创建一个session执行num_epochs次for循环,获得mini-batches,对于每个mini-batches执行优化

# GRADED FUNCTION: model

def model(X_train, Y_train, X_test, Y_test, learning_rate=0.009,
          num_epochs=100, minibatch_size=64, print_cost=True):
    """
    Implements a three-layer ConvNet in Tensorflow:
    使用TensorFlow实现三层的卷积神经网络
    CONV2D -> RELU -> MAXPOOL -> CONV2D -> RELU -> MAXPOOL -> FLATTEN -> FULLYCONNECTED
    
    Arguments:
    X_train -- training set, of shape (None, 64, 64, 3) 训练数据,维度为(None, 64, 64, 3)
    Y_train -- test set, of shape (None, n_y = 6) 训练数据的标签,维度为(None, n_y = 6)
    X_test -- training set, of shape (None, 64, 64, 3) 测试数据,维度为(None, 64, 64, 3)
    Y_test -- test set, of shape (None, n_y = 6) 训练数据的标签,维度为(None, n_y = 6)
    learning_rate -- learning rate of the optimization 学习率
    num_epochs -- number of epochs of the optimization loop 遍历整个数据集的次数
    minibatch_size -- size of a minibatch 每个小批量数据块的大小
    print_cost -- True to print the cost every 100 epochs 是否打印成本值,每遍历100次整个数据集打印一次
    
    Returns:
    实数,训练集的准确度
    train_accuracy -- real number, accuracy on the train set (X_train)

    实数,测试集的准确度
    test_accuracy -- real number, testing accuracy on the test set (X_test)

    学习后的参数
    parameters -- parameters learnt by the model. They can then be used to predict.
    """
    
    #能够重新运行模型而不覆盖tf变量
    ops.reset_default_graph()                         # to be able to rerun the model without overwriting tf variables

    #确保你的数据和我一样
    tf.set_random_seed(1)                             # to keep results consistent (tensorflow seed)

   #指定numpy的随机种子
    seed = 3                                          # to keep results consistent (numpy seed)

    print(X_train.shape)
    (m, n_H0, n_W0, n_C0) = X_train.shape             
    n_y = Y_train.shape[1]                            
    costs = []                                        # To keep track of the cost
    
    # Create Placeholders of the correct shape #为当前维度创建占位符
    ### START CODE HERE ### (1 line)
    X, Y = create_placeholders(n_H0, n_W0, n_C0, n_y)
    ### END CODE HERE ###

    # Initialize parameters #初始化参数
    ### START CODE HERE ### (1 line)
    parameters = initialize_parameters()
    ### END CODE HERE ###
    
    # Forward propagation: Build the forward propagation in the tensorflow graph
    # 前向传播
    ### START CODE HERE ### (1 line)
    Z3 = forward_propagation(X, parameters)
    ### END CODE HERE ###
    
    # Cost function: Add cost function to tensorflow graph 计算成本
    ### START CODE HERE ### (1 line)
    cost = compute_cost(Z3, Y)
    ### END CODE HERE ###
    
    # Backpropagation: Define the tensorflow optimizer. Use an AdamOptimizer that minimizes the cost.
    #反向传播,由于框架已经实现了反向传播,我们只需要选择一个优化器就行了
    ### START CODE HERE ### (1 line)
    optimizer = tf.train.AdamOptimizer(learning_rate=learning_rate).minimize(cost)
    ### END CODE HERE ###
    
    # Initialize all the variables globally #全局初始化所有变量
    init = tf.global_variables_initializer()
     
    # Start the session to compute the tensorflow graph #开始运行
    with tf.Session() as sess:
        
        # Run the initialization #初始化参数
        sess.run(init)
        
        # Do the training loop #开始遍历数据集
        for epoch in range(num_epochs):

            minibatch_cost = 0.
            num_minibatches = int(m / minibatch_size) # number of minibatches of size minibatch_size in the train set
            seed = seed + 1
            minibatches = random_mini_batches(X_train, Y_train, minibatch_size, seed)

            for minibatch in minibatches: #对每个数据块进行处理

                # Select a minibatch 选择一个数据块
                (minibatch_X, minibatch_Y) = minibatch
                # IMPORTANT: The line that runs the graph on a minibatch.
                # Run the session to execute the optimizer and the cost, the feedict should contain a minibatch for (X,Y).
                ### START CODE HERE ### (1 line) 最小化这个数据块的成本
                _ , temp_cost = sess.run([optimizer, cost], feed_dict={
     X:minibatch_X, Y:minibatch_Y})
                ### END CODE HERE ###
                
                minibatch_cost += temp_cost / num_minibatches #累加数据块的成本值
                

            # Print the cost every epoch
            if print_cost == True and epoch % 5 == 0:
                print ("Cost after epoch %i: %f" % (epoch, minibatch_cost))
            if print_cost == True and epoch % 1 == 0:
                costs.append(minibatch_cost)
        
        
        # plot the cost
        plt.plot(np.squeeze(costs))
        plt.ylabel('cost')
        plt.xlabel('iterations (per tens)')
        plt.title("Learning rate =" + str(learning_rate))
        plt.show()
        plt.savefig('3.png')

        #开始预测数据
        # Calculate the correct predictions # 计算当前的预测情况
        predict_op = tf.argmax(Z3, 1)
        correct_prediction = tf.equal(predict_op, tf.argmax(Y, 1))
        
        # Calculate accuracy on the test set ##计算准确度
        accuracy = tf.reduce_mean(tf.cast(correct_prediction, "float"))
        print(accuracy)
        train_accuracy = accuracy.eval({
     X: X_train, Y: Y_train})
        test_accuracy = accuracy.eval({
     X: X_test, Y: Y_test})
        print("Train Accuracy:", train_accuracy)
        print("Test Accuracy:", test_accuracy)
                
        return train_accuracy, test_accuracy, parameters

迭代100次

_, _, parameters = model(X_train, Y_train, X_test, Y_test)

运行结果

Cost after epoch 0: 1.917920
Cost after epoch 5: 1.532475
Cost after epoch 10: 1.014804
Cost after epoch 15: 0.885137
Cost after epoch 20: 0.766963
Cost after epoch 25: 0.651208
Cost after epoch 30: 0.613356
Cost after epoch 35: 0.605931
Cost after epoch 40: 0.534713
Cost after epoch 45: 0.551402
Cost after epoch 50: 0.496976
Cost after epoch 55: 0.454438
Cost after epoch 60: 0.455496
Cost after epoch 65: 0.458359
Cost after epoch 70: 0.450040
Cost after epoch 75: 0.410687
Cost after epoch 80: 0.469005
Cost after epoch 85: 0.389253
Cost after epoch 90: 0.363808
Cost after epoch 95: 0.376132
Tensor("Mean_1:0", shape=(), dtype=float32)
Train Accuracy: 0.868519
Test Accuracy: 0.733333

成本曲线
2020-10-04 吴恩达-C4 卷积神经网络-w1 CNN(课后编程2-Convolution model - Application)_第3张图片
说明

  • plt.savefig(‘3.png’),这句代码是用于把成本曲线图保存到文件。
  • 最后模型训练结果:训练集的准确率是86%,测试集是73%
  • 原文中训练集准确率是94%,测试集准确率是78%,2者相差很大,说明训练集过拟合,这个模型的方差很高。可以通过调试超参,或者正则来进一步优化模型。
  • 在《2020-8-23 吴恩达-改善深层NN-w3 超参调整/批量正则化/编程框架(课后编程-TensorFlow Tutorial-手势辨认)》中利用tensorflow实现的一个3层NN,最终学习结果训练集准确率99.9%,测试集准确率72.5%,过拟合更加厉害。
  • 观察成本下降的过程,可以发现,从初始位置开始,多次迭代,它不是总朝向最小值靠近,它是更持续地靠近最小值的方向。这个是mini-batch算法成本函数的特点,具体参见链接。

你可能感兴趣的:(python,深度学习,IT,深度学习)