zoukankan      html  css  js  c++  java
  • Python机器学习笔记:深入理解Keras中序贯模型和函数模型

       先从sklearn说起吧,如果学习了sklearn的话,那么学习Keras相对来说比较容易。为什么这样说呢?

      我们首先比较一下sklearn的机器学习大致使用流程和Keras的大致使用流程:

    sklearn的机器学习使用流程:

    from  sklearn.模型簇    import  模型名
    from  sklearn.metrics   import  评价指标
    
    
    '''  数据预处理及训练测试集分离提取'''
    
    myModel = 模型名称()     # 对象初始化
    myModel.fit(训练集x ,  训练集y)     #模型训练
    y预测集  = myModel.predict(开发集x)  #模型预测
    评价指标 = 评价指标(y预测集,y测试集)   #模型效果评估
    

      

    Keras的机器学习使用流程:

    import keras
    ...根据具体需求引入keras的包...
    
    ...keras模型搭建...
    ...keras模型编译(可选择模型指标)...
    
    kerasModel.fit(训练集x,训练集y)#keras模型训练
    y预测集=myModel.predict(开发集x)#keras模型预测
    

      

    两者的区别

      由上面伪代码可知Keras和sklearn最大不同在于需要进行模型搭建,可是既然有了这么多模型为什么还要模型搭建?

      如果你了解过神经网络感知机就会比较理解这个过程,一个感知器相当于一个神经元,可根据输入信息反馈出需要的电信号,根据我们的世界观,一个细胞可以单独执行很多功能但是大量单纯的任务会让细胞只针对一个方向发展。用生物学的说话就是分化能力逐渐减弱,机器学习说法就是过拟合。因此,只有大量细胞通过不同的组合才能完成纷繁复杂的预测任务,因而有证明说神经网络理论上可拟合出任何曲线。

      那么话说回来,Keras需要自行搭建模型,搭建方法有两种:序贯模型和函数式模型。而我本次的笔记就是学习序贯模型和函数式模型。

    序贯模型

      序贯模型是多个网络蹭的线性堆叠,是函数式模型的简略版,为最简单的线性,从头到尾的结构顺序,不发生分叉。是多个网络层的线性堆叠。

      Keras实现了很多层,包括core核心层,Convolution卷积层,Pooling池化层等非常丰富有趣的网络结构。

    应用序贯模型的基本步骤

    • 1,model.add()           添加层
    • 2,model.compile()     模型训练的BP模式设置
    • 3,model.fit()         模型训练参数设置+训练
    • 4,model.evaluate()         模型评估
    • 5,model.predict()     模型预测

    序贯模型的创建

      1,可以通过向Sequential模型传递一个layer的list来构造Sequential模型:

    from keras.models import Sequential
    from keras.layers import Dense ,Activation
    
    model = Sequential([
        Dense(32,input_shape=(784,)),
        Activation('relu'),
        Dense(10),
        Activation('softmax')
    ])
    
    model.summary()
    

      

      2,也可以通过.add()方法一个个的将layer加入模型中:

    from keras.models import Sequential
    from keras.layers import Dense ,Activation
    
    model = Sequential()
    model.add(Dense(32,input_shape=(784,)))
    model.add(Activation('relu'))
    model.add(Dense(10))
    model.add(Activation('softmax'))
    
    model.summary()
    

      结果如下:

    Using TensorFlow backend.
    _________________________________________________________________
    Layer (type)                 Output Shape              Param #   
    =================================================================
    dense_1 (Dense)              (None, 32)                25120     
    _________________________________________________________________
    activation_1 (Activation)    (None, 32)                0         
    _________________________________________________________________
    dense_2 (Dense)              (None, 10)                330       
    _________________________________________________________________
    activation_2 (Activation)    (None, 10)                0         
    =================================================================
    Total params: 25,450
    Trainable params: 25,450
    Non-trainable params: 0
    _________________________________________________________________
    

      

    3,指定输入数据的shape

      模型需要知道输入数据的shape,因此,Sequential的第一层需要接受一个关于输入数据shape的参数,后面的各个层则可以自动的推导出中间数据的shape,因此不需要为每一层都指定这个参数。有几种方法来为第一层指定输入数据的shape

      1,传递一个input_shape的关键字参数给第一层,input_shape是一个tupel类型的数据(一个整数或者None的元祖,其中None表示可能为任何正整数)在input_shape中不包含数据的batch大小。

    model = Sequential()
    model.add(Dense(64,input_shape=(20,),activation='relu'))
    

      

      2,有些2D层,如Dense,支持通过指定其输入维度input_dim来隐含的指定输入数据shape,是一个Int类型的数据。一些3D的时域层支持通过参数input_dim和input_length来指定输入shape。

    model = Sequential()
    model.add(Dense(64, input_dim=20, activation='relu'))
    

      3,如果你需要为你的输入指定一个固定的batch大小(这对于statsful RNNs很有用),你可以传递一个batch_size参数给一个层。如果你同时将batch_size=32和input_shape = (6,8)传递给一个层,那么每一批输入的尺寸就是(32,6,8)。

      因此下面的代码是等价的:

    model = Sequential()
    model.add(Dense(32, input_shape=(784,)))
     
    model = Sequential()
    model.add(Dense(32, input_dim=784))
    

      下面三种方法也是严格等价的

    model = Sequential()
    model.add(LSTM(32, input_shape=(10, 64)))
     
     
    model = Sequential()
    model.add(LSTM(32, batch_input_shape=(None, 10, 64)))
     
     
    model = Sequential()
    model.add(LSTM(32, input_length=10, input_dim=64))
    

      

    4,编译

      在训练模型之前,我们需要通过compile来对学习过程进行配置,compile接收三个参数:优化器optimizer,损失函数loss,指标列表metrics。

    compile(self, optimizer, loss, metrics=None, sample_weight_mode=None)
    

    其中:

      optimizer:字符串(预定义优化器名)或者优化器对象,,如 rmsprop 或 adagrad,也可以是 Optimizer 类的实例。详见:optimizers

      loss:字符串(预定义损失函数名)或目标函数,模型试图最小化的目标函数,它可以是现有损失函数的字符串标识符,如categorical_crossentropy 或 mse,也可以是一个目标函数。详见:losses

      metrics:列表,包含评估模型在训练和测试时的网络性能的指标,典型用法是metrics=[‘accuracy’]。评估标准可以是现有的标准的字符串标识符,也可以是自定义的评估标准函数。

    注意:

      模型在使用前必须编译,否则在调用fit或者evaluate时会抛出异常。

    例子:

    # 多分类问题
    model.compile(optimizer='rmsprop',
                  loss='categorical_crossentropy',
                  metrics=['accuracy'])
     
    # 二分类问题
    model.compile(optimizer='rmsprop',
                  loss='binary_crossentropy',
                  metrics=['accuracy'])
     
    # 均方误差回归问题
    model.compile(optimizer='rmsprop',
                  loss='mse')
     
    # 自定义评估标准函数
    import keras.backend as K
     
    def mean_pred(y_true, y_pred):
        return K.mean(y_pred)
     
    model.compile(optimizer='rmsprop',
                  loss='binary_crossentropy',
                  metrics=['accuracy', mean_pred])
    

      

    5,训练

      Keras 模型在输入数据和标签的 Numpy 矩阵上进行训练。为了训练一个模型,你通常会使用 fit 函数。文档详见此处

    fit(self, x, y, batch_size=32, epochs=10, verbose=1, callbacks=None,
     validation_split=0.0, validation_data=None, shuffle=True,
     class_weight=None, sample_weight=None, initial_epoch=0)
    

      本函数将模型训练nb_epoch轮,其参数有:

      x:输入数据,如果模型只有一个输入,那么x的类型是numpy array,如果模型有多个输入,那么x的类型应当是list,list的元素是对应于各个输入的numpy array

      y:标签 ,numpy array

      batch_size:整数,指定进行梯度下降时每个batch包含的样本数,训练时一个batch的样本会被计算一次梯度下降,使目标函数优化一步。

      epochs:整数,训练的轮数,每个epoch会把训练集轮一遍。

      verbose:日志显示,0为不在标准输出流输出日志信息,1为输出进度条记录,2为每个epoch输出一行记录

      callbacks:list,,其中的元素是keras.callbacks.Callback的对象。这个list中的回调函数将会在训练过程中的适当时机被调用,参考回调函数。

      validation_split:0~1之间的浮点数,用来指定训练集的一定比例数据作为验证集。验证集将不参与训练,并在每个epoch结束后测试的模型的指标,如损失函数,精确度等。注意,validation_split的划分在shuffle之前,因此如果你的数据本身是有序的,需要先手工打乱再指定validation_split,否则可能会出现验证集样本不均匀。

      validation_data:形式为(X,y)的tuple,是指定的验证集,此参数将覆盖validation_spilt。

      shuffle:布尔值或者字符串,一般为布尔值,表示是否在训练过程中随机打乱输入样本的顺序。若为字符串“batch”,则用来处理HDF5数据大特殊情况,它将在batch内部将数据打乱。

      class_weight:字典,将不同的类别映射为不同的权重,该参数用来训练过程中调整损失函数(只能用于训练)

      sample_weight:权值的numpy array,用于在训练时调整损失(仅用于训练)。

    可以传递一个1D的与样本等长的向量用于对样本进行1对1的加权,或者在面对时序数据时,传递一个的形式为(samples,sequence_length)的矩阵来为每个时间步上的样本赋不同的权。这种情况下请确定在编译模型时添加了sample_weight_mode=‘temporal’。
      initial_epoch:从该参数指定的epoch开始训练,在继续之前的训练时候有用。

      fit函数返回一个History的对象,其History.history属性记录了损失函数和其他指标的数值随着epoch变化的情况,如果有验证集的话,也包含了验证集的这些指标变化情况。

     示例一:

    # 对于具有2个类的单输入模型(二进制分类):
     
    model = Sequential()
    model.add(Dense(32, activation='relu', input_dim=100))
    model.add(Dense(1, activation='sigmoid'))
    model.compile(optimizer='rmsprop',
                  loss='binary_crossentropy',
                  metrics=['accuracy'])
     
    # 生成虚拟数据
    import numpy as np
    data = np.random.random((1000, 100))
    labels = np.random.randint(2, size=(1000, 1))
     
    # 训练模型,以 32 个样本为一个 batch 进行迭代
    model.fit(data, labels, epochs=10, batch_size=32)
    

      

    示例二:

    # 对于具有10个类的单输入模型(多分类分类):
     
    model = Sequential()
    model.add(Dense(32, activation='relu', input_dim=100))
    model.add(Dense(10, activation='softmax'))
    model.compile(optimizer='rmsprop',
                  loss='categorical_crossentropy',
                  metrics=['accuracy'])
     
    # 生成虚拟数据
    import numpy as np
    data = np.random.random((1000, 100))
    labels = np.random.randint(10, size=(1000, 1))
     
    # 将标签转换为分类的 one-hot 编码
    one_hot_labels = keras.utils.to_categorical(labels, num_classes=10)
     
    # 训练模型,以 32 个样本为一个 batch 进行迭代
    model.fit(data, one_hot_labels, epochs=10, batch_size=32)
    

      

    6,评估

      根据验证集评估模型的好坏

    evaluate(self, x, y, batch_size=32, verbose=1, sample_weight=None)
    

      本函数按batch计算在某些输入数据上模型的误差,其参数有:

      x:输入数据,与fit一样,是numpy array 或者numpy array的list

      y:标签,numpy array

      batch_size:整数,含义同fit的同名参数

      verbose:含义同fit的同名参数,但是只能取0或1

      sample_weight:numpy array ,含义同fit的同名参数

      本函数返回一个测试误差的标量值(如果模型没有其他评价指标),或一个标量的list(如果模型还有其他的评价指标)。model.metrics_names将给出list中各个值的含义。

      如果没有特殊说明,以下函数的参数均保持与fit的同名参数相同的含义

      如果没有特殊说明,以下函数的verbose参数(如果有)均只能取0或者1

    score = model.evaluate(x_val , y_val ,batch_size = 128)
    print('val score:', score[0])
    print('val accuracy:', score[1])
    

      

    7,预测

      对已经训练完成的模型,输入特征值x会预测得到标签y。

    predict(self, x, batch_size=32, verbose=0)
    predict_classes(self, x, batch_size=32, verbose=1)
    predict_proba(self, x, batch_size=32, verbose=1)
    

      本函数按batch获得输入数据对应的输出,其参数有:

      函数的返回值是预测值的numpy array

      predict_classes:本函数按batch产生输入数据的类别预测结果

      predict_proba:本函数按batch产生输入数据属于各个类别的概率

    x = 1
    y = model.predict(x,verbose=0)
    print(y)
    

      

    8,on_batch,batch的结果,检查

    train_on_batch(self, x, y, class_weight=None, sample_weight=None)
    test_on_batch(self, x, y, sample_weight=None)
    predict_on_batch(self, x)
    

      train_on_batch:本函数在batch的数据上进行一次参数更新,函数返回训练误差的标量值或者标量值的list,与evaluate的情形相同。

      test_on_batch:本函数在一个batch的样本上对模型进行评估,函数的返回与evaluate的情形相同。

      predict_on_batch:本函数在一个batch的样本上对模型进行测试,函数返回模型在一个batch上的预测结果。

     

    9,完整代码及其一次结果

    代码:

    from keras.models import Sequential
    from keras.layers import Dense ,Activation,Dropout
    import numpy as np
    
    # 准备训练集和验证集
    x_train = np.random.random((1000,20))
    y_train = np.random.randint(2,size=(1000,1))
    x_val = np.random.random((100,20))
    y_val = np.random.randint(2,size=(100,1))
    
    model = Sequential()
    model.add(Dense(64,input_dim=20,activation='relu'))
    model.add(Dropout(0.5))
    model.add(Dense(64,activation='relu'))
    model.add(Dropout(0.5))
    model.add(Dense(1,activation='sigmoid'))
    
    model.compile(loss='binary_crossentropy',optimizer='rmsprop',metrics=['accuracy'])
    model.fit(x_train,y_train,epochs=20,batch_size=128)
    
    score = model.evaluate(x_val , y_val ,batch_size = 128)
    print('val score:', score[0])
    print('val accuracy:', score[1])
    
    # x = 1
    # y = model.predict(x,verbose=0)
    # print(y)
    

    结果:

    Using TensorFlow backend.
    Epoch 1/20
    
     128/1000 [==>...........................] - ETA: 1s - loss: 0.7093 - acc: 0.5469
    1000/1000 [==============================] - 0s 291us/step - loss: 0.7098 - acc: 0.5090
    Epoch 2/20
    
     128/1000 [==>...........................] - ETA: 0s - loss: 0.7191 - acc: 0.4766
    1000/1000 [==============================] - 0s 15us/step - loss: 0.7087 - acc: 0.5080
    Epoch 3/20
    
     128/1000 [==>...........................] - ETA: 0s - loss: 0.7009 - acc: 0.4766
    1000/1000 [==============================] - 0s 15us/step - loss: 0.6969 - acc: 0.5040
    Epoch 4/20
    
     128/1000 [==>...........................] - ETA: 0s - loss: 0.6946 - acc: 0.5312
    1000/1000 [==============================] - 0s 15us/step - loss: 0.6969 - acc: 0.5240
    Epoch 5/20
    
     128/1000 [==>...........................] - ETA: 0s - loss: 0.7029 - acc: 0.4609
    1000/1000 [==============================] - 0s 15us/step - loss: 0.7002 - acc: 0.4950
    Epoch 6/20
    
     128/1000 [==>...........................] - ETA: 0s - loss: 0.7027 - acc: 0.4531
    1000/1000 [==============================] - 0s 15us/step - loss: 0.6992 - acc: 0.5090
    Epoch 7/20
    
     128/1000 [==>...........................] - ETA: 0s - loss: 0.6907 - acc: 0.5312
    1000/1000 [==============================] - 0s 16us/step - loss: 0.6895 - acc: 0.5290
    Epoch 8/20
    
     128/1000 [==>...........................] - ETA: 0s - loss: 0.6906 - acc: 0.5000
    1000/1000 [==============================] - 0s 16us/step - loss: 0.6969 - acc: 0.5040
    Epoch 9/20
    
     128/1000 [==>...........................] - ETA: 0s - loss: 0.6860 - acc: 0.5078
    1000/1000 [==============================] - 0s 16us/step - loss: 0.6914 - acc: 0.5280
    Epoch 10/20
    
     128/1000 [==>...........................] - ETA: 0s - loss: 0.6784 - acc: 0.6016
    1000/1000 [==============================] - 0s 17us/step - loss: 0.6912 - acc: 0.5390
    Epoch 11/20
    
     128/1000 [==>...........................] - ETA: 0s - loss: 0.6812 - acc: 0.6406
    1000/1000 [==============================] - 0s 16us/step - loss: 0.6874 - acc: 0.5330
    Epoch 12/20
    
     128/1000 [==>...........................] - ETA: 0s - loss: 0.6997 - acc: 0.4766
    1000/1000 [==============================] - 0s 16us/step - loss: 0.6949 - acc: 0.5080
    Epoch 13/20
    
     128/1000 [==>...........................] - ETA: 0s - loss: 0.6843 - acc: 0.5781
    1000/1000 [==============================] - 0s 15us/step - loss: 0.6912 - acc: 0.5380
    Epoch 14/20
    
     128/1000 [==>...........................] - ETA: 0s - loss: 0.6746 - acc: 0.5703
    1000/1000 [==============================] - 0s 17us/step - loss: 0.6873 - acc: 0.5360
    Epoch 15/20
    
     128/1000 [==>...........................] - ETA: 0s - loss: 0.6959 - acc: 0.5000
    1000/1000 [==============================] - 0s 16us/step - loss: 0.6891 - acc: 0.5310
    Epoch 16/20
    
     128/1000 [==>...........................] - ETA: 0s - loss: 0.6780 - acc: 0.5938
    1000/1000 [==============================] - 0s 17us/step - loss: 0.6907 - acc: 0.5280
    Epoch 17/20
    
     128/1000 [==>...........................] - ETA: 0s - loss: 0.6835 - acc: 0.5938
    1000/1000 [==============================] - 0s 16us/step - loss: 0.6858 - acc: 0.5690
    Epoch 18/20
    
     128/1000 [==>...........................] - ETA: 0s - loss: 0.6845 - acc: 0.4922
    1000/1000 [==============================] - 0s 16us/step - loss: 0.6921 - acc: 0.5220
    Epoch 19/20
    
     128/1000 [==>...........................] - ETA: 0s - loss: 0.6861 - acc: 0.5625
    1000/1000 [==============================] - 0s 15us/step - loss: 0.6859 - acc: 0.5540
    Epoch 20/20
    
     128/1000 [==>...........................] - ETA: 0s - loss: 0.6959 - acc: 0.5312
    1000/1000 [==============================] - 0s 16us/step - loss: 0.6841 - acc: 0.5530
    
    100/100 [==============================] - 0s 440us/step
    val score: 0.6957631707191467
    val accuracy: 0.5099999904632568
    

    函数式模型

      比序贯模型要复杂,但是效果很好,可以同时/分阶段输入变量,分阶段输出想要的模型。

      所以说,只要你的模型不是类似VGG一样

    1,应用函数式模型的基本步骤

    1,model.layers()     添加层

    2,model.compile()  模型训练的BP模式设置

    3,model.fit()       模型训练参数设置+训练

    4,evaluate()             模型评估

    5,predict()                模型预测

     

    2,常用Model属性

    model.layers:组成模型图的各个层
    model.inputs:模型的输入张量列表
    model.outputs:模型的输出张量列表
    

      

    model = Model(inputs=, outputs = )
    

      

    3,指定输入数据的shape

    inputs = Input(shape = (20, ))
    

     

    4,编译,训练,评估,预测等步骤与序贯式模型相同(这里不再赘述)

    compile(self, optimizer, loss, metrics=None, loss_weights=None, sample_weight_mode=None)
    

      本函数按batch计算在某些输入数据上模型的误差,其参数有:

      x:输入数据,与fit一样,是numpy array或者numpy array的list

      y:标签,numpy array

      batch_size:整数,含义同fit的同名函数

      verbose:含义与fit的同名函数,但是只能取0或者1

      sample_weight:numpy array,含义同fit的同名函数

      本函数编译模型以供训练,参数有:

    evaluate(self, x, y, batch_size=32, verbose=1, sample_weight=None)
    

      

    5,示例一(基于上面序贯模型进行改造)

    import numpy as np
    from keras.models import Model
    from keras.layers import Dense , Dropout,Input
    
    # 准备训练集和验证集
    x_train = np.random.random((1000,20))
    y_train = np.random.randint(2,size=(1000,1))
    x_val = np.random.random((100,20))
    y_val = np.random.randint(2,size=(100,1))
    
    inputs = Input(shape = (20,))
    x = Dense(64,activation='relu')(inputs)
    x = Dropout(0.5)(x)
    x = Dense(64,activation='relu')(x)
    x = Dropout(0.5)(x)
    predictions = Dense(1,activation='sigmoid')(x)
    model=Model(inputs=inputs, outputs=predictions)
    model.compile(loss='binary_crossentropy', optimizer='rmsprop', metrics=['accuracy'])
    model.fit(x_train, y_train,epochs=20,batch_size=128)
    
    score = model.evaluate(x_val, y_val, batch_size=128)
    print('val score:', score[0])
    print('val accuracy:', score[1])
    

      

     

    序贯模型和函数模型共同的API

      model.summary():打印出模型的概况,它实际调用的是keras.utils.print_summary

      model.get_config():返回包含模型配置信息的Python字典,模型也可以从config中重构回去。

    config = model.get_config()
    model = Model.from_config(config)
    model = Sequential.from_config(config)
    

      上面是分别对序贯模型和函数式模型载入config

    model.get_layer():依据层名或下标获得层对象

    model.get_weights():返回模型权重张量的列表,类型为numpy.array

    model.set_weights():从numpy array里载入给模型,要求数组与model.get_weights()一样

    model.to_json():返回代表模型的JSON字符串,仅仅包含网络结构,不包含权重,可以从JSON字符串中重构模型

    Keras模型保存

      首先不推荐使用pickle或者cPickle来保存Keras模型

    1,保存结构

       可以使用model.save(filepath)将Keras模型和权重保存在一个HDF5文件中,该文件将包含:

    • 模型的结构,以便重构该模型
    • 模型的权重
    • 训练配置(损失函数,优化器等)
    • 优化器的状态,以便从上次训练中断的地方开始

      使用Keras.models.load_model(filepath)来重新实例化你的模型,如果文件中存储了训练配置的话,该函数还会同时完成模型的编译。例如:

    from keras.models  import load_model
    
    # create a HDF5 file 'my_load.h5'
    model.save('my_load.h5')
    
    # deletes the existing model
    del  model
    
    # returns a compiled model
    # indentical to the previous one
    model = load_model('my_load.h5')
    

      

    2,保存结构

      如果你只是希望保存模型的结构,而不包括其权重或者配置信息,可以使用:

    # save as JSON
    json_string = model.to_json()
    
    # save as YAML
    yaml_string = model.to_yaml()
    

      这项操作可以将模型序列化为json或者yaml文件,如果需要的话你甚至可以手动打开这些文件进行编辑。

      当然你也可以从保存好的json文件或者yaml文件中载入模型:

    # model reconstruction from JSON:
    from keras.models import model_from_json
    model = model_from_json(json_string)
    
    # model reconstruction from YAML
    model = model_from_yaml(yaml_string)
    

      

    3,保存模型的权重

      如果需要保存模型,可通过下面的代码利用HDF5进行保存。注意,在使用前需要确保你已经安装了HDF5 和Python的库h5py。

    model.save_weights('my_model_weights.h5')
    

      如果你需要在代码中初始化一个完全相同的模型,请使用:

    model.load_weights('my_model_weights.h5')
    

      

    4,加载权重到不同的网络结构

      如果你需要加载权重到不同的网络结构(有些层一样)中,例如fine-tune或transfer-learning,你可以通过层名字来加载模型。

    model.load_weights('my_model_weights.h5', by_name=True)
    

      例如:

    """
    假如原模型为:
        model = Sequential()
        model.add(Dense(2, input_dim=3, name="dense_1"))
        model.add(Dense(3, name="dense_2"))
        ...
        model.save_weights(fname)
    """
    # new model
    model = Sequential()
    model.add(Dense(2, input_dim=3, name="dense_1"))  # will be loaded
    model.add(Dense(10, name="new_dense"))  # will not be loaded
    
    # load weights from first model; will only affect the first layer, dense_1.
    model.load_weights(fname, by_name=True)
    

      加载权重到不同的网络结构上多数用于迁移学习。

    参考文献:https://zhuanlan.zhihu.com/p/37376691

    https://zhuanlan.zhihu.com/p/50543770

  • 相关阅读:
    Minecraft 1.12.2/1.14.4 Mod开发笔记——搭建环境
    Minecraft 1.12.2 Mod开发笔记
    浅谈莫比乌斯反演
    卡迈克尔数
    一些可能会有用的东西(持续更新)
    emacs配置
    CSPS 2020游记
    浅谈KMP
    Atcoder AGC052
    乌班图操作指令(持续更新)
  • 原文地址:https://www.cnblogs.com/wj-1314/p/9967480.html
Copyright © 2011-2022 走看看