• 一文了解深度学习实战——分类篇


    本文将从两个案例 MNIST手写数字识别狗的品种识别 入手,让童鞋们从实战角度快速入门深度学习的分类部分!

    MNIST手写数字识别

    TensorFlow搭建MLP

    import numpy as np
    import tensorflow as tf
    import matplotlib.pyplot as plt
    
    # 下载数据集
    from tensorflow.examples.tutorials.mnist import input_data
    mnist = input_data.read_data_sets("MNIST_data/", one_hot=True)
    
    print("训练集图像大小:{}".format(mnist.train.images.shape))
    print("训练集标签大小:{}".format(mnist.train.labels.shape))
    print("验证集图像大小:{}".format(mnist.validation.images.shape))
    print("验证集标签大小:{}".format(mnist.validation.labels.shape))
    print("测试集图像大小:{}".format(mnist.test.images.shape))
    print("测试集标签大小:{}".format(mnist.test.labels.shape))
    
    # 为了便于读取,我们把数据集先各自使用一个变量指向它们
    x_train, y_train = mnist.train.images, mnist.train.labels
    x_valid, y_valid = mnist.validation.images, mnist.validation.labels
    x_test, y_test = mnist.test.images, mnist.test.labels
    
    # 绘制和显示前5个训练集的图像 
    fig = plt.figure(figsize=(10, 10))
    for i in range(5):
        ax = fig.add_subplot(1, 5, i+1, xticks=[], yticks=[])
        ax.imshow(np.reshape(x_train[i:i+1], (28, 28)), cmap='gray')
    # 绘制和显示前(2*12)之后的五个训练集的图像 
    fig = plt.figure(figsize=(10, 10))
    for i in range(5):
        ax = fig.add_subplot(1, 5, i+1, xticks=[], yticks=[])
        ax.imshow(np.reshape(x_train[i+2*12:i+1+2*12], (28, 28)), cmap='gray')
    
    # 定义可视化图像的函数,传入一个图像向量和figure对象
    def visualize_input(img, ax):
        # 绘制并输出图像
        ax.imshow(img, cmap='gray')
        
        # 对于该图像的宽和高,我们输出它们的具体的数值,
        # 以便于我们更清晰的知道计算机是如何看待一张图像的
        width, height = img.shape
        
        # 将图像中的具体数值转换成0-1之间的值
        thresh = img.max()/2.5 
        # 遍历行
        for x in range(width):
            # 遍历列
            for y in range(height):
                # 将图像的数值在它对应的位置上标出,且水平垂直居中
                ax.annotate(str(round(img[x][y],2)), xy=(y,x),
                            horizontalalignment='center',
                            verticalalignment='center',
                            color='white' if img[x][y]<thresh else 'black')
    
    fig = plt.figure(figsize=(10, 10)) 
    ax = fig.add_subplot(111)
    # 假设我们就取出下标为5的样本来作为例子
    visualize_input(np.reshape(x_train[5:6], (28, 28)), ax)
    
    
    import math
    #模型搭建和训练
    # 参数准备
    img_size = 28 * 28
    num_classes = 10
    learning_rate = 0.1
    epochs = 100
    batch_size = 128
    
    # 创建模型
    # x表示输入,创建输入占位符,该占位符会在训练时,会对每次迭代的数据进行填充上
    x = tf.placeholder(tf.float32, [None, img_size])
    
    # W表示weight,创建权重,初始化时都是为0,它的大小是(图像的向量大小,图像的总类别)
    W = tf.Variable(tf.zeros([img_size, num_classes]))
    
    # b表示bias,创建偏移项
    b = tf.Variable(tf.zeros([num_classes]))
    
    # y表示计算输出结果,softmax表示激活函数是多类别分类的输出
    # 感知器的计算公式就是:(x * W) + b
    y = tf.nn.softmax(tf.matmul(x, W) + b)
    
    # 定义输出预测占位符y_
    y_ = tf.placeholder(tf.float32, [None, 10])
    
    valid_feed_dict = { x: x_valid, y_: y_valid  }
    test_feed_dict = { x: x_test, y_: y_test }
    
    # 通过激活函数softmax的交叉熵来定义损失函数
    cost = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(labels=y_, logits=y))
    # 定义梯度下降优化器
    optimizer = tf.train.GradientDescentOptimizer(learning_rate).minimize(cost)
    
    # 比较正确的预测结果
    correct_prediction = tf.equal(tf.argmax(y, 1), tf.argmax(y_, 1))
    # 计算预测准确率
    accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
        
    iteration = 0
    # 定义训练时的检查点
    saver = tf.train.Saver()
    
    # 创建一个TensorFlow的会话
    with tf.Session() as sess:
      
        # 初始化全局变量
        sess.run(tf.global_variables_initializer())
            
        # 根据每批次训练128个样本,计算出一共需要迭代多少次
        batch_count = int(math.ceil(mnist.train.labels.shape[0] / 128.0))
        
        # 开始迭代训练样本
        for e in range(epochs):
            
            # 每个样本都需要在TensorFlow的会话里进行运算,训练
            for batch_i in range(batch_count):
              
                # 样本的索引,间隔是128个
                batch_start = batch_i * batch_size
                # 取出图像样本
                batch_x = mnist.train.images[batch_start:batch_start+batch_size]
                # 取出图像对应的标签
                batch_y = mnist.train.labels[batch_start:batch_start+batch_size]
                # 训练模型
                loss, _ = sess.run([cost, optimizer], feed_dict={x: batch_x, y_: batch_y})
                
                # 每20个批次时输出一次训练损失等日志信息
                if batch_i % 20 == 0:
                    print("Epoch: {}/{}".format(e+1, epochs), 
                          "Iteration: {}".format(iteration), 
                          "Training loss: {:.5f}".format(loss))
                iteration += 1
    
                # 每128个样本时,验证一下训练的效果如何,并输出日志信息
                if iteration % batch_size == 0:
                    valid_acc = sess.run(accuracy, feed_dict=valid_feed_dict)
                    print("Epoch: {}/{}".format(e, epochs),
                          "Iteration: {}".format(iteration),
                          "Validation Accuracy: {:.5f}".format(valid_acc))
        
        # 保存训练模型的检查点
        saver.save(sess, "checkpoints/mnist_mlp_tf.ckpt")
    
    # 预测测试数据集精确度
    saver = tf.train.Saver()
    with tf.Session() as sess:
        # 从训练模型的检查点恢复
        saver.restore(sess, tf.train.latest_checkpoint('checkpoints'))
        
        # 预测测试集精确度
        test_acc = sess.run(accuracy, feed_dict=test_feed_dict)
        print("test accuracy: {:.5f}".format(test_acc))
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152

    TensorFlow搭建CNN

    import tensorflow as tf
    from tensorflow.examples.tutorials.mnist import input_data
    
    # 下载并加载数据集
    mnist = input_data.read_data_sets('MNIST_data/', one_hot=True)
    
    # 为了便于读取,我们把数据集先各自使用一个变量指向它们
    x_train, y_train = mnist.train.images, mnist.train.labels
    x_valid, y_valid = mnist.validation.images, mnist.validation.labels
    x_test, y_test = mnist.test.images, mnist.test.labels
    
    print("训练集图像大小:{}".format(x_train.shape))
    print("训练集标签大小:{}".format(y_train.shape))
    print("验证集图像大小:{}".format(x_valid.shape))
    print("验证集标签大小:{}".format(y_valid.shape))
    print("测试集图像大小:{}".format(x_test.shape))
    print("测试集标签大小:{}".format(y_test.shape))
    
    # 参数准备
    img_size = 28 * 28
    num_classes = 10
    learning_rate = 1e-4
    epochs = 10
    batch_size = 50
    
    # 定义输入占位符
    x = tf.placeholder(tf.float32, shape=[None, img_size])
    x_shaped = tf.reshape(x, [-1, 28, 28, 1])
    
    # 定义输出占位符
    y = tf.placeholder(tf.float32, shape=[None, num_classes])
    
    # 定义卷积函数
    def create_conv2d(input_data, num_input_channels, num_filters, filter_shape, pool_shape, name):
        # 卷积的过滤器大小结构是[filter_height, filter_width, in_channels, out_channels]
        conv_filter_shape = [filter_shape[0], filter_shape[1], num_input_channels, num_filters]
        
        # 定义权重Tensor变量,初始化时是截断正态分布,标准差是0.03
        weights = tf.Variable(tf.truncated_normal(conv_filter_shape, stddev=0.03), name=name+"_W")
        
        # 定义偏移项Tensor变量,初始化时是截断正态分布
        bias = tf.Variable(tf.truncated_normal([num_filters]), name=name+"_b")
        
        # 定义卷积层
        out_layer = tf.nn.conv2d(input_data, weights, (1, 1, 1, 1), padding="SAME")
        out_layer += bias
        # 通过激活函数ReLU来计算输出
        out_layer = tf.nn.relu(out_layer)
        # 添加最大池化层
        out_layer = tf.nn.max_pool(out_layer, ksize=(1, pool_shape[0], pool_shape[1], 1), strides=(1, 2, 2, 1), padding="SAME")
        return out_layer
    
    # 添加第一层卷积层
    layer1 = create_conv2d(x_shaped, 1, 32, (5, 5), (2, 2), name="layer1")
    # 添加第二层卷积层
    layer2 = create_conv2d(layer1, 32, 64, (5, 5), (2, 2), name="layer2")
    # 添加扁平化层
    flattened = tf.reshape(layer2, (-1, 7 * 7 * 64))
    
    # 添加全连接层
    wd1 = tf.Variable(tf.truncated_normal((7 * 7 * 64, 1000), stddev=0.03), name="wd1")
    bd1 = tf.Variable(tf.truncated_normal([1000], stddev=0.01), name="bd1")
    dense_layer1 = tf.add(tf.matmul(flattened, wd1), bd1)
    dense_layer1 = tf.nn.relu(dense_layer1)
    
    # 添加输出全连接层
    wd2 = tf.Variable(tf.truncated_normal((1000, num_classes), stddev=0.03), name="wd2")
    bd2 = tf.Variable(tf.truncated_normal([num_classes], stddev=0.01), name="bd2")
    dense_layer2 = tf.add(tf.matmul(dense_layer1, wd2), bd2)
    
    # 添加激活函数的softmax输出层
    y_ = tf.nn.softmax(dense_layer2)
    
    # 通过softmax交叉熵定义计算损失值
    cost = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits=y_, labels=y))
    # 定义优化器是Adam
    optimizer = tf.train.AdamOptimizer(learning_rate=learning_rate).minimize(cost)
    
    # 定义预测结果的比较
    correct_prediction = tf.equal(tf.argmax(y, 1), tf.argmax(y_, 1))
    # 定义预测的精确度
    accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
    
    iteration = 0
    
    
    import math
    
    # 定义要保存训练模型的变量
    saver = tf.train.Saver()
    
    # 创建TensorFlow会话
    with tf.Session() as sess:
      
        # 初始化TensorFlow的全局变量
        sess.run(tf.global_variables_initializer())
        
        # 计算所有的训练集需要被训练多少次,当每批次是batch_size个时
        batch_count = int(math.ceil(x_train.shape[0] / float(batch_size)))
        
        # 要迭代epochs次训练
        for e in range(epochs):
            # 对每张图像进行训练
            for batch_i in range(batch_count):
                # 每次取出batch_size张图像
                batch_x, batch_y = mnist.train.next_batch(batch_size=batch_size)
                # 训练模型
                _, loss = sess.run([optimizer, cost], feed_dict={x: batch_x, y: batch_y})
                
                # 每训练20次图像时打印一次日志信息,也就是20次乘以batch_size个图像已经被训练了
                if batch_i % 20 == 0:
                    print("Epoch: {}/{}".format(e+1, epochs), 
                          "Iteration: {}".format(iteration), 
                          "Training loss: {:.5f}".format(loss))
                iteration += 1
                
                # 每迭代一次时,做一次验证,并打印日志信息
                if iteration % batch_size == 0:
                    valid_acc = sess.run(accuracy, feed_dict={x: x_valid, y: y_valid})
                    print("Epoch: {}/{}".format(e, epochs),
                          "Iteration: {}".format(iteration),
                          "Validation Accuracy: {:.5f}".format(valid_acc))
    
        # 保存模型的检查点
        saver.save(sess, "checkpoints/mnist_cnn_tf.ckpt")
    
    # 预测测试数据集
    saver = tf.train.Saver()
    with tf.Session() as sess:
        # 从TensorFlow会话中恢复之前保存的模型检查点
        saver.restore(sess, tf.train.latest_checkpoint('checkpoints/'))
        
        # 通过测试集预测精确度
        test_acc = sess.run(accuracy, feed_dict={x: x_test, y: y_test})
        print("test accuracy: {:.5f}".format(test_acc))
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136

    Keras搭建MLP

    import keras
    from keras.datasets import mnist
    from keras.models import Sequential
    from keras.layers import Dense, Dropout, Activation
    from keras.optimizers import RMSprop
    
    # 参数准备
    batch_size = 128
    num_classes = 10
    epochs = 20
    img_size = 28 * 28
    
    # 下载并读取MNIST数据集数据
    (x_train, y_train), (x_test, y_test) = mnist.load_data()
    
    # 分割验证集数据
    valid_len = 5000
    x_len = x_train.shape[0]
    train_len = x_len-valid_len
    
    # 验证集数据
    x_valid = x_train[train_len:]
    y_valid = y_train[train_len:]
    
    # 训练集数据
    x_train = x_train[:train_len]
    y_train = y_train[:train_len]
    
    # 将训练集、验证集和测试集数据进行图像向量转换
    x_train = x_train.reshape(x_train.shape[0], img_size)
    x_valid = x_valid.reshape(x_valid.shape[0], img_size)
    x_test = x_test.reshape(x_test.shape[0], img_size)
    
    # 将训练集、验证集和测试集数据都转换成float32类型
    x_train = x_train.astype('float32')
    x_valid = x_valid.astype('float32')
    x_test = x_test.astype('float32')
    
    # 将训练集、验证集和测试集数据都转换成0到1之间的数值,就是归一化处理
    x_train /= 255
    x_valid /= 255
    x_test /= 255
    
    # 通过to_categorical()函数将训练集标签、验证集标签和测试集标签独热编码(one-hot encoding)
    y_train = keras.utils.to_categorical(y_train, num_classes)
    y_valid = keras.utils.to_categorical(y_valid, num_classes)
    y_test = keras.utils.to_categorical(y_test, num_classes)
    
    # 创建模型
    model = Sequential()
    model.add(Dense(512, activation='relu', input_shape=(img_size,)))
    model.add(Dropout(0.2))
    model.add(Dense(512, activation='relu'))
    model.add(Dropout(0.2))
    model.add(Dense(num_classes, activation='softmax'))
    # 模型架构预览
    model.summary()
    
    # 编译模型
    model.compile(loss='categorical_crossentropy', optimizer=RMSprop(), metrics=['accuracy'])
    # 训练模型
    model.fit(x_train, y_train, epochs=epochs, batch_size=batch_size, verbose=1, validation_data=(x_valid, y_valid))
    
    # 评估模型
    score = model.evaluate(x_test, y_test, verbose=0)
    print('Test accuracy:{}, Test loss: {}, {}'.format(score[1], score[0], score))
    
    
    #模型预测,画图
    import matplotlib.pyplot as plt
    import numpy as np
    x_img = x_test[7:8]
    # 预测单张图像的概率
    prediction = model.predict(x_img)
    x_coordinates = np.arange(prediction.shape[1])
    plt.bar(x_coordinates, prediction[0][:])
    plt.xticks(x_coordinates, np.arange(10))
    plt.show()
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79

    Keras搭建CNN

    import numpy as np
    import keras
    from keras.datasets import mnist
    from keras.models import Sequential
    from keras.layers import Dense, Dropout, Flatten, Conv2D, MaxPooling2D
    from keras import utils
    
    # 参数准备
    batch_size = 128
    epochs = 15
    num_classes = 10
    
    img_width = 28
    img_height = 28
    img_channels = 1
    
    # 下载并读取MNIST数据集数据
    (x_train, y_train), (x_test, y_test) = mnist.load_data()
    
    # 分割验证集数据
    valid_len = 5000
    x_len = x_train.shape[0]
    train_len = x_len-valid_len
    
    # 验证集数据
    x_valid = x_train[train_len:]
    y_valid = y_train[train_len:]
    
    # 训练集数据
    x_train = x_train[:train_len]
    y_train = y_train[:train_len]
    
    # 将训练集、验证集和测试集数据进行图像转换,
    # 图像的形状大小是 [batch, height, width, channels]
    x_train = x_train.reshape(x_train.shape[0], img_height, img_width, img_channels)
    x_valid = x_valid.reshape(x_valid.shape[0], img_height, img_width, img_channels)
    x_test = x_test.reshape(x_test.shape[0], img_height, img_width, img_channels)
    
    # 将训练集、验证集和测试集数据都转换成float32类型
    x_train = x_train.astype(np.float32)
    x_valid = x_valid.astype(np.float32)
    x_test = x_test.astype(np.float32)
    
    # 将训练集、验证集和测试集数据都转换成0到1之间的数值,就是归一化处理
    x_train /= 255
    x_valid /= 255
    x_test /= 255
    
    # 通过to_categorical()函数将训练集标签、验证集标签和测试集标签独热编码(one-hot encoding)
    y_train = keras.utils.to_categorical(y_train, num_classes)
    y_valid = keras.utils.to_categorical(y_valid, num_classes)
    y_test = keras.utils.to_categorical(y_test, num_classes)
    
    # 创建模型
    model = Sequential()
    model.add(Conv2D(filters=32, kernel_size=(3, 3), activation='relu', input_shape=(img_width, img_height, img_channels)))
    model.add(Conv2D(filters=64, kernel_size=(3, 3), activation='relu'))
    model.add(MaxPooling2D(pool_size=(2, 2)))
    model.add(Dropout(0.25))
    model.add(Flatten())
    model.add(Dense(128, activation='relu'))
    model.add(Dropout(0.5))
    model.add(Dense(num_classes, activation='softmax'))
    # 模型架构预览
    model.summary()
    
    # 编译模型
    model.compile(loss=keras.losses.categorical_crossentropy, 
                  optimizer=keras.optimizers.Adadelta(), metrics=['accuracy'])
    
    # 训练模型
    model.fit(x_train, y_train, batch_size=batch_size, epochs=epochs, 
              verbose=1, validation_data=(x_valid, y_valid))
    
    # 评估模型
    score = model.evaluate(x_test, y_test, verbose=0)
    print("Test Loss: {:.5f}, Test Accuracy: {:.5f}".format(score[0], score[1]))
    
    # 单张图像预测
    import matplotlib.pyplot as plt
    
    # 取出第一张图像
    x_img = x_test[0:1]
    # 通过模型预测
    prediction = model.predict(x_img)
    
    # 绘制图展示
    x_coordinate = np.arange(prediction.shape[1])
    plt.bar(x_coordinate, prediction[0][:])
    plt.xticks(x_coordinate, np.arange(10))
    plt.show()
    
    print("预测的图中的数字是{}。".format(y_test[0:1]))
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93

     

    狗的品种识别

    狗狗图片数据:
    链接:https://pan.baidu.com/s/1cEgg2aqXvAvI58M8EAS9CQ 密码:8ahu

    Keras搭建CNN

    from sklearn.datasets import load_files       
    from sklearn.model_selection import train_test_split
    from keras.utils import np_utils
    from keras.preprocessing import image   
    import numpy as np
    from glob import glob
    import matplotlib.pyplot as plt
    from matplotlib import image
    import tqdm
    
    # 共有120种狗狗的品种
    num_classes = 120
    
    # 定义加载数据集的函数
    def load_dataset(path):
        # 通过sklearn提供的load_files()方法加载文件
        # 返回一个类字典对象,包含文件相对路径和文件所属编号
        data = load_files(path)
        # 将文件路径转变成NumPy对象
        dog_files = np.array(data['filenames'])
        # 狗狗的每张图片都按照顺序排成列表
        raw_targets = np.array(data['target'])
        # 通过to_categorical()方法将文件所属编号转换成二进制类别矩阵(就是one-hot encoding)
        dog_targets = np_utils.to_categorical(raw_targets, num_classes)
        # 返回所有图片文件路径,图片文件编号和图片文件的二进制类别矩阵
        return dog_files, raw_targets, dog_targets
      
    # 加载数据集
    dog_filepaths, dog_raw_targets, dog_targets = load_dataset('Images/')
    
    # 加载狗狗的品种名称列表
    # glob是一个文件操作相关的模块,通过指定的匹配模式,返回相应的文件或文件夹路径
    # 这里的操作就是返回Images目录下的所有文件夹
    # 最后通过列表推导式遍历每个文件路径字符串,并截取狗狗类别名称那段字符串
    dogpath_prefix_len = len('Images/n02085620-')
    dog_names = [item[dogpath_prefix_len:] for item in sorted(glob("Images/*"))]
    
    print('狗狗的品种有{}种。'.format(len(dog_names)))
    print('狗狗的图片一共有{}张。\n'.format(len(dog_filepaths)))
    
    
    # 为了训练更快些,也考虑到一些读者的本地机器性能不高,我们就用前9000张狗狗的图片吧
    # 如果读者的机器性能还不错,那就注释这两行,直接训练所有的图片数据
    dog_filepaths = dog_filepaths[:9000]
    dog_targets = dog_targets[:9000]
    
    # 分割训练数据集和测试数据集
    X_train, X_test, y_train, y_test = train_test_split(dog_filepaths, dog_targets, test_size=0.2)
    
    # 将测试集数据分割一半给验证集
    half_test_count = int(len(X_test) / 2)
    X_valid = X_test[:half_test_count]
    y_valid = y_test[:half_test_count]
    
    X_test = X_test[half_test_count:]
    y_test = y_test[half_test_count:]
    
    print("X_train.shape={}, y_train.shape={}.".format(X_train.shape, y_train.shape))
    print("X_valid.shape={}, y_valid.shape={}.".format(X_valid.shape, y_valid.shape))
    print("X_test.shape={}, y_test.shape={}.".format(X_test.shape, y_test.shape))
    
    # 设置matplotlib在绘图时的默认样式
    plt.style.use('default')
    
    
    # 查看随机9张狗狗的图像
    def draw_random_9_dog_images():
        # 创建9个绘图对象,3行3列
        fig, axes = plt.subplots(nrows=3, ncols=3)
        # 设置绘图的总容器大小
        fig.set_size_inches(10, 9)
    
        # 随机选择9个数,也就是9个品种的狗(可能重复,且每次都不一样)
        random_9_nums = np.random.choice(len(X_train), 9)
        # 从训练集中选出9张图
        random_9_imgs = X_train[random_9_nums]
        print(random_9_imgs)
    
        # 根据这随机的9张图片路径,截取取得相应的狗狗品种名称
        imgname_list = []
        for imgpath in random_9_imgs:
            imgname = imgpath[dogpath_prefix_len:] 
            imgname = imgname[:imgname.find('/')]
            imgname_list.append(imgname)
    
        index = 0
        for row_index in range(3): # 行
            for col_index in range(3): # 列
                # 读取图片的数值内容
                img = image.imread(random_9_imgs[index])
                # 获取绘图Axes对象,根据[行索引, 列索引]
                ax = axes[row_index, col_index]
                # 在Axes对象上显示图像
                ax.imshow(img)
                # 在绘图对象上设置狗狗品种名称
                ax.set_xlabel(imgname_list[index])
                # 索引加1
                index += 1
                
    draw_random_9_dog_images()
    
    # 对数据集进行遍历,读取每张图片,并获取它的大小,
    # 最后返回的图片shape存储在变量dogs_shape_list列表里
    dogs_shape_list = []
    for filepath in dog_filepaths:
        shape = image.imread(filepath).shape
        if len(shape) == 3:
            dogs_shape_list.append(shape)
                 
    dogs_shapes = np.asarray(dogs_shape_list)
    
    print("总共{}张。".format(len(dogs_shapes)))
    print("随机抽取三张图片的维度是{}。".format(dogs_shapes[np.random.choice(len(dogs_shapes), 3)]))
    
    dogs_mean_width = np.mean(dogs_shapes[:,0])
    dogs_mean_height = np.mean(dogs_shapes[:,1])
    print("狗狗的图片的平均宽:{:.1f} * 平均高:{:.1f}。".format(dogs_mean_width, dogs_mean_height))
    
    # 定义一个函数,将每张图片都转换成标准大小(1, 224, 224, 3)
    def path_to_tensor(img_path):
        # 加载图片
        # 图片对象的加载用的是PIL库,通过load_img()方法返回的就是一个PIL对象
        img = image.load_img(img_path, target_size=(224, 224, 3))
        # 将PIL图片对象类型转化为格式(224, 224, 3)的3维张量
        x = image.img_to_array(img)
        # 将3维张量转化格式为(1, 224, 224, 3)的4维张量并返回
        return np.expand_dims(x, axis=0)
    
    # 定义一个函数,将数组里的所有路径的图片都转换成图像数值类型并返回
    def paths_to_tensor(img_paths):
        # tqdm模块表示使用进度条显示,传入一个所有图片的数组对象
        # 将所有图片的对象一个个都转换成numpy数值对象张量后,并返回成数组
        list_of_tensors = [path_to_tensor(img_path) for img_path in tqdm(img_paths)]
        # 将对象垂直堆砌排序摆放
        return np.vstack(list_of_tensors)
    
    
    from PIL import ImageFile 
    # 为了防止PIL读取图片对象时出现IO错误,则设置截断图片为True
    ImageFile.LOAD_TRUNCATED_IMAGES = True                 
    
    # 将所有图片都转换成标准大小的数值图像对象,然后除以255,进行归一化处理
    # RGB的颜色值,最大为255,最小为0
    # 对训练集数据进行处理
    train_tensors = paths_to_tensor(X_train).astype(np.float32) / 255
    # 对验证集数据进行处理
    valid_tensors = paths_to_tensor(X_valid).astype(np.float32) / 255
    # 对测试集数据进行处理
    test_tensors = paths_to_tensor(X_test).astype(np.float32) / 255
    
    
    from keras.layers import Conv2D, MaxPooling2D, GlobalAveragePooling2D
    from keras.layers import Dropout, Flatten, Dense
    from keras.models import Sequential
    
    # 创建Sequential模型
    model = Sequential()
    
    # 创建输入层,输入层必须传入input_shape参数以表示图像大小,深度是16
    model.add(Conv2D(filters=16, kernel_size=(2, 2), strides=(1, 1), padding='same', 
                     activation='relu', input_shape=train_tensors.shape[1:]))
    # 添加最大池化层,大小为2x2,有效范围默认是valid,就是说,不够2x2的大小的空间数据就丢弃了
    model.add(MaxPooling2D(pool_size=(2, 2)))
    # 添加Dropout层,每次丢弃20%的网络节点,防止过拟合
    model.add(Dropout(0.2))
    
    # 添加卷积层,深度是32,内核大小是2x2,跨步是1x1,有效范围是same则表示不够数据范围的就用0填充
    model.add(Conv2D(filters=32, kernel_size=(2, 2), strides=(1, 1), padding='same', activation='relu'))
    # 添加最大池化层,大小为2x2,有效范围默认是valid,就是说,不够2x2的大小的空间数据就丢弃了
    model.add(MaxPooling2D(pool_size=(2, 2)))
    # 添加Dropout层,每次丢弃20%的网络节点,防止过拟合
    model.add(Dropout(0.2))
    
    # 添加卷积层,深度是64
    model.add(Conv2D(filters=64, kernel_size=(2, 2), strides=(1, 1), padding='same', activation='relu'))
    model.add(MaxPooling2D(pool_size=(2, 2)))
    model.add(Dropout(0.2))
    
    # 添加全局平均池化层
    model.add(GlobalAveragePooling2D())
    # 添加Dropout,每次丢弃50%
    model.add(Dropout(0.5))
    # 添加输出层,120个类别输出
    model.add(Dense(num_classes, activation="softmax"))
                     
    # 打印输出网络模型架构
    model.summary()
    
    # 编译模型
    model.compile(optimizer='rmsprop', loss='categorical_crossentropy', metrics=['accuracy'])
    
    from keras.callbacks import ModelCheckpoint 
    
    epochs = 20
    checkpointer = ModelCheckpoint(filepath='saved_models/weights.best.from_scratch.hdf5', 
                                   verbose=1, 
                                   save_best_only=True)
    
    model.fit(train_tensors, 
              y_train, 
              validation_data=(valid_tensors, y_valid),
              epochs=epochs, 
              batch_size=20, 
              callbacks=[checkpointer], 
              verbose=1)
    
    ## 加载具有最好验证权重的模型
    model.load_weights('saved_models/weights.best.from_scratch.hdf5')
    
    # 获取测试数据集中每一个图像所预测的狗品种的index
    dog_breed_predictions = [np.argmax(model.predict(np.expand_dims(tensor, axis=0))) for tensor in test_tensors]
    
    # 测试准确率
    test_accuracy = 100*np.sum(np.array(dog_breed_predictions)==np.argmax(y_test, axis=1))/len(dog_breed_predictions)
    print('Test Accuracy: {:.4f}'.format(test_accuracy))
    
    #结果发现准确率很低很低,这是我们需要迁移学习
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168
    • 169
    • 170
    • 171
    • 172
    • 173
    • 174
    • 175
    • 176
    • 177
    • 178
    • 179
    • 180
    • 181
    • 182
    • 183
    • 184
    • 185
    • 186
    • 187
    • 188
    • 189
    • 190
    • 191
    • 192
    • 193
    • 194
    • 195
    • 196
    • 197
    • 198
    • 199
    • 200
    • 201
    • 202
    • 203
    • 204
    • 205
    • 206
    • 207
    • 208
    • 209
    • 210
    • 211
    • 212
    • 213
    • 214
    • 215
    • 216
    • 217

     

    迁移学习(InceptionV3)

    
    # 导入InceptionV3预训练模型和数据处理模块
    from keras.applications.inception_v3 import InceptionV3, preprocess_input, decode_predictions
    # 导入构建Keras的Model所需模块
    from keras.models import Model
    from keras.layers import Dense, GlobalAveragePooling2D, Dropout
    from keras.preprocessing import image
    from keras.optimizers import SGD
    from keras.callbacks import ModelCheckpoint  
    # 导入图片数据增强生成器
    from keras.preprocessing.image import ImageDataGenerator
    import matplotlib.pyplot as plt
    
    
    class InceptionV3Retrained:
        """
        定义一个类,用来在预训练模型上去训练新的数据
        """
        
    
        def add_new_last_layers(self, base_model, num_classes):
            """
            添加新的全连接层
            """
            # 添加一个全局空间平均池化层
            x = base_model.output
            x = GlobalAveragePooling2D()(x)
    
            # 添加1024个全连接层
            x = Dense(1024, activation='relu')(x)
    
            # 添加全连接输出层,有num_classes个类别输出,使用softmax多类别分类激活函数
            predictions = Dense(num_classes, activation='softmax')(x)
    
            # 通过上面定义的base_model对象和它的输出层
            # 我们自定义创建一个新的Keras的Model模型对象
            model = Model(input=base_model.input, output=predictions)
            return model
    
    
        def freeze_previous_layers(self, model, base_model):
            """
            冻结预训练模型之前的层
            """
            # 冻结InceptionV3模型的所有卷积层,因为我们迁移学习就是对顶部的几个层进行训练
            for layer in base_model.layers:
                layer.trainable = False
    
            # 编译模型
            # 优化器rmsprop,参数使用默认值即可
            # 分类交叉熵使用多类别的
            model.compile(optimizer='rmsprop', loss='categorical_crossentropy', metrics=['accuracy'])
    
    
        def fine_tune_model(self, model):
            """
            微调模型
            """
            # 我们冻结模型的前面172层,然后把剩下的层数都解冻
            for layer in model.layers[:172]:
                layer.trainable = False
            for layer in model.layers[172:]:
                layer.trainable = True
    
            # 再编译模型
            # 优化器使用随机梯度下降,学习率我们调小点0.0001
            # 分类交叉熵依旧使用多类别的
            model.compile(optimizer=SGD(lr=0.0001, momentum=0.9), loss='categorical_crossentropy', metrics=['accuracy'])
      
      
        def plot_training(self, history):
            """
            绘制训练模型时的损失值和精确度
            """
            # 取出训练时的精确度
            acc = history.history['acc']
            # 取出验证时的精确度
            val_acc = history.history['val_acc']
            # 取出训练时的损失值
            loss = history.history['loss']
            # 取出验证时的损失值
            val_loss = history.history['val_loss']
            # 根据精确度的个数,就可以得知训练了多少次
            epochs = range(len(acc))
    
            # 绘制训练精确度和验证精确度
            plt.plot(epochs, acc, 'r.')
            plt.plot(epochs, val_acc, 'r')
            plt.title('Training and validation accuracy')
    
            # 绘制训练损失和验证损失
            plt.figure()
            plt.plot(epochs, loss, 'r.')
            plt.plot(epochs, val_loss, 'r-')
            plt.title('Training and validation loss')
            plt.show()
    
    
        def train(self, num_classes, batch_size, epochs):
            """
            训练模型
            """
    
            # 定义训练数据增强生成器
            # 参数preprocessing_function表示每次输入都进行预处理
            # 参数rotation_range表示图像随机旋转的度数范围
            # 参数width_shift_range表示图像的宽度可移动范围
            # 参数height_shift_range表示图像的高度可移动范围
            # 参数shear_range表示逆时针方向剪切角度
            # 参数zoom_range表示随机缩放的角度值
            # 参数horizontal_flip表示是否水平翻转
            train_datagen = ImageDataGenerator(
              preprocessing_function=preprocess_input,
              rotation_range=20,
              width_shift_range=0.2,
              height_shift_range=0.2,
              shear_range=0.2,
              zoom_range=0.2,
              horizontal_flip=True
            )
            
            # 定义验证数据增强生成器
            valid_datagen = ImageDataGenerator(
              preprocessing_function=preprocess_input,
              rotation_range=20,
              width_shift_range=0.2,
              height_shift_range=0.2,
              shear_range=0.2,
              zoom_range=0.2,
              horizontal_flip=True
            )
    
            # 训练数据增强
            train_generator = train_datagen.flow(train_tensors, y_train, batch_size=batch_size)
            # 验证数据增强
            validation_generator = valid_datagen.flow(valid_tensors, y_valid, batch_size=batch_size)
    
            # 初始化InceptionV3模型
            # include_top=False表示初始化模型时不包含InceptionV3网络结构层中的最后的全连接层
            base_model = InceptionV3(weights='imagenet', include_top=False)  
            
            # 添加新的全连接层
            model = self.add_new_last_layers(base_model, num_classes)
    
            # 冻结刚创建的InceptionV3的模型的所有卷积层
            self.freeze_previous_layers(model, base_model)
            
            # 定义模型检查点,只保存最佳的
            checkpointer = ModelCheckpoint(filepath='inception_v3.dogs.133.best.weights.h5', 
                                           verbose=1, 
                                           save_best_only=True)
    
            print("首次训练模型")
            # 在新数据集上训练模型
            history_tl = model.fit_generator(train_generator, 
                              steps_per_epoch=train_tensors.shape[0] / batch_size, 
                              validation_steps=valid_tensors.shape[0] / batch_size, 
                              epochs=epochs,
                              verbose=1, 
                              callbacks=[checkpointer], 
                              validation_data=validation_generator)
    
            # 微调模型
            self.fine_tune_model(model)
    
            print("微调模型后,再次训练模型")
            # 我们再次训练模型
            history_ft = model.fit_generator(train_generator, 
                              steps_per_epoch=train_tensors.shape[0] / batch_size, 
                              validation_steps=valid_tensors.shape[0] / batch_size,
                              epochs=epochs,
                              verbose=1, 
                              callbacks=[checkpointer], 
                              validation_data=validation_generator)
    
            # 绘制模型的损失值和精确度
            self.plot_training(history_ft)
    
    # 每批次大小是128
    batch_size = 128
    # 训练5个批次
    epochs = 5
    
    incepV3_model = InceptionV3Retrained()
    incepV3_model.train(num_classes, batch_size, epochs)
    
    
    # 测试模型的精确度
    # 创建一个不带全连接层的InceptionV3模型
    test_model = InceptionV3(weights='imagenet', include_top=False, input_shape=test_tensors.shape[1:]) 
    
    # 添加全连接层输出层
    incepV3_model = InceptionV3Retrained()
    trained_model = incepV3_model.add_new_last_layers(test_model, num_classes)
    
    # 加载刚才训练的权重到模型中
    trained_model.load_weights("inception_v3.dogs.133.best.weights.h5") 
    
    # 编译模型
    trained_model.compile(loss='categorical_crossentropy', optimizer='sgd', metrics=['accuracy'])
    
    # 通过summary()方法,可以看到完整的InceptionV3的神经网络模型架构
    # trained_model.summary()
    
    # 评估模型
    score = trained_model.evaluate(test_tensors, y_test, verbose=1)
    print("Test {}: {:.2f}. Test {}: {:.2f}.".format(trained_model.metrics_names[0], 
                                                     score[0]*100, 
                                                     trained_model.metrics_names[1], 
                                                     score[1]*100))
    
    # 预测狗狗品种
    def predict_dog_breed(model, img_path):
        # 加载图像
        x = load_img(img_path)
        # 图片预处理
        x = preprocess_input(x)
        # 模型预测
        predictions = model.predict(x)
        # 取出预测数值
        prediction_list = predictions[0]
    
        # 取出最大值索引和最大值
        def get_max_arg_value(prediction_list):
            arg_max = np.argmax(prediction_list)
            max_val = prediction_list[arg_max]
            preds = np.delete(prediction_list, arg_max)
            return preds, arg_max, max_val
    
        # 取出前3个预测值的最大值索引和最大值
        def get_list_of_max_arg_value(prediction_list):
            preds, argmax1, max1val = get_max_arg_value(prediction_list)
            preds, argmax2, max2val = get_max_arg_value(preds)
            preds, argmax3, max3val = get_max_arg_value(preds)
    
            top_3_argmax = np.array([argmax1, argmax2, argmax3])
            top_3_max_val = np.array([max1val, max2val, max3val])
            return top_3_argmax, top_3_max_val
    
        top_3_argmax, top_3_max_val = get_list_of_max_arg_value(prediction_list)
        dog_titles = [dog_names[index] for index in top_3_argmax]
    
        print('前3个最大值: {}'.format(top_3_max_val))
    
    #     # 如果希望显示直方图,可以取消注释这三行代码
    #     plt.barh(np.arange(3), top_3_max_val)
    #     plt.yticks(np.arange(3), dog_titles)
    #     plt.show()
        
        # 创建绘图对象
        fig, ax = plt.subplots()
        # 设置绘图的总容器大小
        fig.set_size_inches(5, 5)
        # 将最大值乘以100就是百分比
        top_3_max_val *= 100
        # 拼接前三个最大值的字符串
        dog_title = "{}: {:.2f}%\n".format(dog_titles[0], top_3_max_val[0]) + \
                    "{}: {:.2f}%\n".format(dog_titles[1], top_3_max_val[1]) + \
                    "{}: {:.2f}%\n".format(dog_titles[2], top_3_max_val[2])
        # 在绘图的右上角显示加上识别的值字符串
        ax.text(1.01, 0.8, 
                dog_title, 
                horizontalalignment='left', 
                verticalalignment='bottom',
                transform=ax.transAxes)
        # 读取图片的数值内容
        img = matplotlib.image.imread(img_path)
        # 在Axes对象上显示图像
        ax.imshow(img)
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168
    • 169
    • 170
    • 171
    • 172
    • 173
    • 174
    • 175
    • 176
    • 177
    • 178
    • 179
    • 180
    • 181
    • 182
    • 183
    • 184
    • 185
    • 186
    • 187
    • 188
    • 189
    • 190
    • 191
    • 192
    • 193
    • 194
    • 195
    • 196
    • 197
    • 198
    • 199
    • 200
    • 201
    • 202
    • 203
    • 204
    • 205
    • 206
    • 207
    • 208
    • 209
    • 210
    • 211
    • 212
    • 213
    • 214
    • 215
    • 216
    • 217
    • 218
    • 219
    • 220
    • 221
    • 222
    • 223
    • 224
    • 225
    • 226
    • 227
    • 228
    • 229
    • 230
    • 231
    • 232
    • 233
    • 234
    • 235
    • 236
    • 237
    • 238
    • 239
    • 240
    • 241
    • 242
    • 243
    • 244
    • 245
    • 246
    • 247
    • 248
    • 249
    • 250
    • 251
    • 252
    • 253
    • 254
    • 255
    • 256
    • 257
    • 258
    • 259
    • 260
    • 261
    • 262
    • 263
    • 264
    • 265
    • 266
    • 267
    • 268
    • 269
    • 270
  • 相关阅读:
    代码随想录 | Day 57 - LeetCode 647. 回文子串、516. 最长回文子序列
    Map 和 WeakMap:JavaScript 中的键值对集合
    [附源码]计算机毕业设计springboot水果管理系统
    吃透Redis(九):缓存淘汰篇-LFU算法
    Mysql中delete from table、truncate table、drop table的区别
    OJ练习第172题——可以攻击国王的皇后
    DAY 12 结构体(重点) 共用体 枚举01
    oringin的x轴(按x轴规定值)绘制不规律的横坐标
    【java】java 内存分配之 指针碰撞 创建对象
    ELK学习(一)
  • 原文地址:https://blog.csdn.net/weixin_45116099/article/details/127723607