2020CCF大数据与计算智能大赛--路况状态时空预测赛题解决方案

1 前言

该文为我们团队参加CCF大数据与计算智能大赛2020路况状态时空预测赛题的解决方案,采用双向LSTM + LGBM融合的思路。
最终成绩:
A榜:线上分数 0.48092653 分,排名 23/2482
B榜:线上分数 0.47989942 分,排名 26/2482

仅供大家参考,第一次参加比赛,部分代码参考了OTTO Data Lab,如有错误恳请大家指出。

2 题目分析

该赛题链接为https://www.datafountain.cn/competitions/466,题目大致为官方给定7月1日-30日的道路时空特征以及对应的拥堵程度(用1,2,3表示,标签中出现的4当做3处理),预测7月31日(后来由于数据泄露改为8月1日)的道路拥堵程度。
官方给定的数据格式如下:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

第一张表大致为某段路需要预测的时间片和当前时间片,当前及历史7天、14天、21天、28天的时间特征(如速度等),第二张表则是描述所有出现的道路的空间特征(路有多宽,车道数量等),第三张表表示道路之间的关系(表示某段路的下游是哪段路),这三种数据下载后均为txt格式,打开如下:

在这里插入图片描述
第二张

在这里插入图片描述
由于能力有限,我们并没能利用到这个topo图,只对时间和空间特征表做了处理。

3数据处理

3.1 预处理

首先,我们要做的就是把数据从txt文档中提取出来,特别是第一张表中的数据,这里我们使用pands,利用分隔符将数据提取成dataframe表格,并对一些时间特征进行提取,这里参考了OTTO Data Lab的方案:我们提取了每个时间周期时间片速度,eta速度的最大、最小、平均、差值等等,最多的拥堵情况等

def get_base_info(x):
    return [i.split(':')[-1] for i in x.split(' ')]


def get_speed(x):
    return np.array([i.split(',')[0] for i in x], dtype='float16')


def get_eta(x):
    return np.array([i.split(',')[1] for i in x], dtype='float16')


def get_state(x):
    return [int(i.split(',')[2]) for i in x]


def get_cnt(x):
    return np.array([i.split(',')[3] for i in x], dtype='int16')


def gen_feats(path, mode='is_train'):
    df = pd.read_csv(path, sep=';', header=None) #sep分隔符,以;分割
    df['link'] = df[0].apply(lambda x: x.split(' ')[0]) #df[0]是开头4个数据
    if mode == 'is_train':
        df['label'] = df[0].apply(lambda x: int(x.split(' ')[1]))
        df['label'] = df['label'].apply(lambda x: 3 if x > 3 else x)
        df['label'] -= 1 #标签从1,2,3变成0,1,2
        df['current_slice_id'] = df[0].apply(lambda x: int(x.split(' ')[2]))
        df['future_slice_id'] = df[0].apply(lambda x: int(x.split(' ')[3]))
    else:
        df['label'] = -1
        df['current_slice_id'] = df[0].apply(lambda x: int(x.split(' ')[2]))
        df['future_slice_id'] = df[0].apply(lambda x: int(x.split(' ')[3]))

    df['time_diff'] = df['future_slice_id'] - df['current_slice_id']

    df['curr_state'] = df[1].apply(lambda x: x.split(' ')[-1].split(':')[-1]) #当前时间片的特征
    df['curr_speed'] = df['curr_state'].apply(lambda x: x.split(',')[0])
    df['curr_eta'] = df['curr_state'].apply(lambda x: x.split(',')[1])
    df['curr_cnt'] = df['curr_state'].apply(lambda x: x.split(',')[3])
    df['curr_state'] = df['curr_state'].apply(lambda x: x.split(',')[2]) #当前时间片的状态label
    del df[0]

    for i in tqdm(range(1, 6)): #tqdm 显示一个加载进度条
        df['his_info'] = df[i].apply(get_base_info)
        if i == 1:
            flg = 'current'
        else:
            flg = f'his_{(6 - i) * 7}'

        #提取每一段时间片的数据特征
        df['his_speed'] = df['his_info'].apply(get_speed)
        df[f'{flg}_speed_min'] = df['his_speed'].apply(lambda x: x.min())
        df[f'{flg}_speed_max'] = df['his_speed'].apply(lambda x: x.max())
        df[f'{flg}_speed_mean'] = df['his_speed'].apply(lambda x: x.mean())
        df[f'{flg}_speed_std'] = df['his_speed'].apply(lambda x: x.std())

        df['his_eta'] = df['his_info'].apply(get_eta)
        df[f'{flg}_eta_min'] = df['his_eta'].apply(lambda x: x.min())
        df[f'{flg}_eta_max'] = df['his_eta'].apply(lambda x: x.max())
        df[f'{flg}_eta_mean'] = df['his_eta'].apply(lambda x: x.mean())
        df[f'{flg}_eta_std'] = df['his_eta'].apply(lambda x: x.std())

        df['his_cnt'] = df['his_info'].apply(get_cnt)
        df[f'{flg}_cnt_min'] = df['his_cnt'].apply(lambda x: x.min())
        df[f'{flg}_cnt_max'] = df['his_cnt'].apply(lambda x: x.max())
        df[f'{flg}_cnt_mean'] = df['his_cnt'].apply(lambda x: x.mean())
        df[f'{flg}_cnt_std'] = df['his_cnt'].apply(lambda x: x.std())

        df['his_state'] = df['his_info'].apply(get_state)
        #counter()函数返回的是一个类似于字典的counter计数器
        #Counter类中的most_common(n)函数:传进去一个可选参数n(代表获取数量最多的前n个元素,如果不传参数,代表返回所有结果)
        df[f'{flg}_state'] = df['his_state'].apply(lambda x: Counter(x).most_common()[0][0])
        df.drop([i, 'his_info', 'his_speed', 'his_eta', 'his_cnt', 'his_state'], axis=1, inplace=True)
    if mode == 'is_train':
        df.to_csv(f"{mode}_{path.split('/')[-1]}", index=False)
    else:
        df.to_csv(f"is_test.csv", index=False)

提取后的表格如下:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
由于表格太长,有74多列,这里只截取了一部分,大部分都是重复的操作,截取每个时间周期的特征

3.2 转化成符合LSTM输入的格式

LSTM希望能接受(数据长度,时间步个数,特征个数)格式的数据,因此我们需要将预处理的数据进一步处理,这里我们先使用merge操作将时间特征和空间特征连接起来,然后提取6个时间步:

#合并时间特征和空间特征
df = pd.read_csv('D:/CCF/train/merge_26_30.txt')
attr = pd.read_csv(r'D:\jupyter_space\CCF\attr_topo_num_length.csv')
df = df.merge(attr, on='link', how='left')

#转化成LSTM能接受的格式
def get_lstm_data(df):
    df['the_null'] = 0
    lstm_data = np.zeros((df.shape[0],6,22))
    lstm_data[:,0,:] = df[['his_7_speed_min','his_7_speed_max','his_7_speed_mean','his_7_speed_std','his_7_eta_min',
                       'his_7_eta_max','his_7_eta_mean','his_7_eta_std','his_7_cnt_min','his_7_cnt_max','his_7_cnt_mean',
                       'his_7_cnt_std','his_7_state','time_diff','length', 'direction', 'path_class', 'speed_class', 'LaneNum', 'speed_limit',
                               'level', 'width']]
    lstm_data[:,1,:] = df[['his_14_speed_min','his_14_speed_max','his_14_speed_mean','his_14_speed_std','his_14_eta_min',
                       'his_14_eta_max','his_14_eta_mean','his_14_eta_std','his_14_cnt_min','his_14_cnt_max','his_14_cnt_mean',
                       'his_14_cnt_std','his_14_state','time_diff','length', 'direction', 'path_class', 'speed_class', 'LaneNum', 'speed_limit',
                               'level', 'width']]
    lstm_data[:,2,:] = df[['his_21_speed_min','his_21_speed_max','his_21_speed_mean','his_21_speed_std','his_21_eta_min',
                       'his_21_eta_max','his_21_eta_mean','his_21_eta_std','his_21_cnt_min','his_21_cnt_max','his_21_cnt_mean',
                       'his_21_cnt_std','his_21_state','time_diff','length', 'direction', 'path_class', 'speed_class', 'LaneNum', 'speed_limit',
                               'level', 'width']]
    lstm_data[:,3,:] = df[['his_28_speed_min','his_28_speed_max','his_28_speed_mean','his_28_speed_std','his_28_eta_min',
                       'his_28_eta_max','his_28_eta_mean','his_28_eta_std','his_28_cnt_min','his_28_cnt_max','his_28_cnt_mean',
                       'his_28_cnt_std','his_28_state','time_diff','length', 'direction', 'path_class', 'speed_class', 'LaneNum', 'speed_limit',
                               'level', 'width']]
    lstm_data[:,4,:] = df[['current_speed_min','current_speed_max','current_speed_mean','current_speed_std','current_eta_min',
                       'current_eta_max','current_eta_mean','current_eta_std','current_cnt_min','current_cnt_max','current_cnt_mean',
                       'current_cnt_std','current_state','time_diff','length', 'direction', 'path_class', 'speed_class', 'LaneNum', 'speed_limit',
                               'level', 'width']]
    lstm_data[:,5,:] = df[['curr_speed','curr_speed','curr_speed','the_null','curr_eta','curr_eta','curr_eta','the_null','curr_cnt','curr_cnt','curr_cnt',
                           'the_null' ,'curr_state','time_diff','length', 'direction', 'path_class', 'speed_class', 'LaneNum', 'speed_limit',
                               'level', 'width']]
    return lstm_data

通过上述转化得到的数据,就可以送入LSTM网络进行训练了

4 LSTM训练及改进

这里我们使用Tensorflow的keras模块实现,并通过kflod方法进行交叉验证:

model = tf.keras.Sequential([
    tf.keras.layers.BatchNormalization(),
    tf.keras.layers.Bidirectional(tf.keras.layers.LSTM(32,return_sequences = True)),
    tf.keras.layers.Dropout(0.2),
    tf.keras.layers.Bidirectional(tf.keras.layers.LSTM(64)),

    tf.keras.layers.Flatten(),
    tf.keras.layers.Dense(128,kernel_regularizer=tf.keras.regularizers.l2(0.01),activation = 'relu'),
    tf.keras.layers.Dropout(0.2),
    tf.keras.layers.Dense(3,activation='softmax')
])


model.compile(optimizer=tf.keras.optimizers.Adam(lr = 0.001),loss = tf.keras.losses.SparseCategoricalCrossentropy(),
              metrics = ['sparse_categorical_accuracy'])
weights = {0:1,1:1,2:2}
folds = KFold(n_splits=5, shuffle=True, random_state=2020)
for n_fold, (train_idx, valid_idx) in enumerate(folds.split(train_x), start=1):
    print('----------------------\n'
          'fold '+str(n_fold)+'开始了\n'+'本轮共有'+str(len(train_idx))+'条数据参与训练')
    train_x2, train_y2 = train_x[train_idx], train_y[train_idx]
    valid_x2, valid_y2 = train_x[valid_idx], train_y[valid_idx]
    model.fit(train_x2, train_y2,batch_size = 2048, epochs=3, validation_data=(valid_x2,valid_y2),
              validation_freq=1,class_weight = weights)

    predict_valid = model.predict(valid_x2)
    predict_valid_result = np.argmax(predict_valid, axis=1).reshape(-1)
    report_valid = f1_score(valid_y2, predict_valid_result, average=None)  # 计算分数
    print('Valid_Score: ', report_valid[0] * 0.2 + report_valid[1] * 0.2 + report_valid[2] * 0.6)

    test_pred+= model.predict(test)/5

在训练过程中,我们发现训练集中数据严重失衡,在50万的训练数据中,标签1有40余万,而2,3非常少,如下图(7.1日的数据):
在这里插入图片描述
因此训练得到的结果很不乐观,我们查阅资料后决定使用增加数据和修改权重的方法来平衡数据,首先我们训练的是7.30的数据,将7.30的数据全部采纳,然后补充26.27.28中标签仅为2,3的数据,补充后的数据如下:
在这里插入图片描述
补充后测试集线上分为0.455左右。
然后在训练中进一步提高不同标签的权重,采用1:1:2的权重进行训练,最后得到测试集的F1分数为0.4738

总结一下,LSTM的方法大致分为:

  1. 把预处理的数据处理成符合LSTM输入的格式
  2. 构建网络结构进行训练
  3. 修改网络结构达到最优(摸奖调参)
  4. 针对数据严重失衡的问题进行调整,把最近几天的2,3标签集合起来
  5. 修改训练权值

5 LGBM训练及改进

LGBM是我们采用的第二种办法,参考了OTTO Data Lab 的Baseline,这里采用kfold交叉验证,值得注意的是,为了数据拟合,kfold对道路id的唯一值进行kflod,而不是对所有的道路。

def lgb_train(train_: pd.DataFrame, test_: pd.DataFrame, use_train_feats: list, id_col: str, label: str,
              n_splits: int, split_rs: int, is_shuffle=True, use_cart=False, cate_cols=None) -> pd.DataFrame:
    if not cate_cols:
        cate_cols = []
    print('data shape:\ntrain--{}\ntest--{}'.format(train_.shape, test_.shape)) #数据维度
    print('Use {} features ...'.format(len(use_train_feats))) #有几个特征
    print('Use lightgbm to train ...')
    n_class = train_[label].nunique() #unique()是以 数组形式(numpy.ndarray)返回列的所有唯一值(特征的所有唯一值)
                                      #nunique() 返回的是唯一值的个数
                                      #这里n_class代表有几个分类,1,2,3即三种
    train_[f'{label}_pred'] = 0
    test_pred = np.zeros((test_.shape[0], n_class)) #预测先置为0
    fold_importance_df = pd.DataFrame()
    fold_importance_df["Feature"] = use_train_feats

    folds = KFold(n_splits=n_splits, shuffle=is_shuffle, random_state=split_rs) #n_split:要划分的折数
                                                                                #shuffle: 每次都进行shuffle,测试集中折数的总和就是训练集的个数
                                                                                #random_state:随机状态

    train_user_id = train_[id_col].unique() #返回所有id的唯一值
    params = {
        'learning_rate': 0.005, #学习率
        'boosting_type': 'rf', #gbdt模型为基础
        'objective': 'multiclass', #多分类
        'metric': 'None',
        'num_leaves': 80, #单棵树的最大叶子数

        'num_class': n_class, #共有多少类
        'feature_fraction': 0.8,# 如果 feature_fraction 小于 1.0, LightGBM 将会在每次迭代中随机选择部分特征. 例如, 如果设置为 0.8, 将会在每棵树训练之前选择 80% 的特征
                                 # 可以用来加速训练
                                 # 可以用来处理过拟合
        'bagging_fraction': 0.8, # 类似于 feature_fraction, 但是它将在不进行重采样的情况下随机选择部分数据
                                  # 可以用来加速训练
                                  # 可以用来处理过拟合
                                  # Note: 为了启用 bagging, bagging_freq 应该设置为非零值
        'bagging_freq': 5,       #bagging 的频率, 0 意味着禁用 bagging. k 意味着每 k 次迭代执行bagging
                                 #Note: 为了启用 bagging, bagging_fraction 设置适当
        'seed': 1,
        'bagging_seed': 1,
        'feature_fraction_seed': 7,
        'min_data_in_leaf': 20, #一个叶子上数据的最小数量. 可以用来处理过拟合.
        'nthread': -1,
        'verbose': -1,
    #      'device':'gpu',
    #     'gpu_platform_id':0,
    # 'gpu_device_id':0
    }


    for n_fold, (train_idx, valid_idx) in enumerate(folds.split(train_user_id), start=1): #把数据分成几折
        print('the {} training start ...'.format(n_fold))
        train_x, train_y = train_.loc[train_[id_col].isin(train_user_id[train_idx]), use_train_feats], train_.loc[
            train_[id_col].isin(train_user_id[train_idx]), label]
        valid_x, valid_y = train_.loc[train_[id_col].isin(train_user_id[valid_idx]), use_train_feats], train_.loc[
            train_[id_col].isin(train_user_id[valid_idx]), label]

        print(f'for train user:{len(train_idx)}\nfor valid user:{len(valid_idx)}')
        print('本轮共有'+str(len(train_x))+'条数据参与训练')

        if use_cart:
            dtrain = lgb.Dataset(train_x, label=train_y, categorical_feature=cate_cols)
            #lightgbm可以处理标称型(类别)数据。通过指定'categorical_feature' 这一参数告诉它哪些feature是标称型的。
            # 它不需要将数据展开成独热码(one-hot),其原理是对特征的所有取值,做一个one-vs-others,从而找出最佳分割的那一个特征取值
            dvalid = lgb.Dataset(valid_x, label=valid_y, categorical_feature=cate_cols)
        else:
            dtrain = lgb.Dataset(train_x, label=train_y)
            dvalid = lgb.Dataset(valid_x, label=valid_y)

        #训练
        clf = lgb.train(
            params=params,
            train_set=dtrain,
            num_boost_round=5000,
            valid_sets=[dvalid],
            early_stopping_rounds=100,
            verbose_eval=100,
            feval=f1_score_eval
        )
        fold_importance_df[f'fold_{n_fold}_imp'] = clf.feature_importance(importance_type='gain') #  统计某种特征在整个.py文件中使用的次数
        train_.loc[train_[id_col].isin(train_user_id[valid_idx]), f'{label}_pred'] = np.argmax(
            clf.predict(valid_x, num_iteration=clf.best_iteration), axis=1)
        test_pred += clf.predict(test_[use_train_feats], num_iteration=clf.best_iteration) / folds.n_splits

    report = f1_score(train_[label], train_[f'{label}_pred'], average=None) #计算分数
    print(classification_report(train_[label], train_[f'{label}_pred'], digits=4)) #评估报告
    print('Score: ', report[0] * 0.2 + report[1] * 0.2 + report[2] * 0.6)
    test_[f'{label}_pred'] = np.argmax(test_pred, axis=1)
    test_[label] = np.argmax(test_pred, axis=1)+1 #测试集标签


    # test_[label] = np.argmax(test_pred, axis=1)
    #统计数据
    five_folds = [f'fold_{f}_imp' for f in range(1, n_splits + 1)]
    fold_importance_df['avg_imp'] = fold_importance_df[five_folds].mean(axis=1)
    fold_importance_df.sort_values(by='avg_imp', ascending=False, inplace=True)
    print(fold_importance_df[['Feature', 'avg_imp']].head(20))
    return test_[[id_col, 'current_slice_id', 'future_slice_id', label]]

if __name__ == "__main__":
    # train_path = './20190701.txt'
    # test_path = 'test.txt'
    # gen_feats(train_path, mode='is_train')
    # gen_feats(test_path, mode='is_test')

    #道路特征
    # attr = pd.read_csv('20201012150828attr.txt', sep='\t',
    #                    names=['link', 'length', 'direction', 'path_class', 'speed_class', 'LaneNum', 'speed_limit',
    #                           'level', 'width'], header=None)
    attr = pd.read_csv(r'D:\jupyter_space\CCF\attr_topo_num_length.csv')
    #读取训练文件、测试文件
    train = pd.read_csv('./train/merge_26_30.txt')
    # test = pd.read_csv('./start_data/final_test_0801.csv')
    test = pd.read_csv('./start_data/final_test_0801.csv')
    #在训练文件、测试文件中添加道路特征
    train = train.merge(attr, on='link', how='left')
    test = test.merge(attr, on='link',how = 'left')
    train = train.loc[train['link'].isin(test['link']),:]
    use_cols = [i for i in train.columns if i not in ['link', 'label', 'current_slice_id', 'future_slice_id', 'label_pred']]

    sub = lgb_train(train, test, use_cols, 'link', 'label', 5, 2020)
    # sub.to_csv('./csv/11.20_1.csv', index=False, encoding='utf8')

最初LGBM只训练7.30日的数据,根据调参的结果,线上分大概可以达到0.39,加入26,27,28几天的2,3标签后,可以达到0.455左右,最后根据采样(摸奖)调整样本标签比例,可以达到0.466左右

6 融合

由于做融合已经是比赛的最后一天,我们决定用比较简单的方法把两种模型结合起来:采用双向LSTM输出的文件中分数最高且有一定行为差异的两个文件,以及LGBM中输出文件中分数最高且有一定行为差异的两个文件,以它们的线上分作为票权,进行投票,概括起来如下:
有4个输出结果,分别是A,B,C,D,每个输出结果都包含测试集的17万个标签,对应着4个网络的预测结果。
它们的线上成绩也就是票权,分别为:
A: 0.473
B: 0.472
C: 0.466
D: 0.455

对于第一条数据,若A,B输出结果为1,C,D输出结果为2,则对于第一条数据,标签1得分为0.473+0.472 = 0.945,标签2得分为0.466+0.455 = 0.921,标签3得分为0 ,横向比较,则最后结果为标签1。按这种方法循环整个测试集,就可以得到投票后的所有标签。

这种方法使得我们的分数从0.473上升到了0.480

由于最后几个小时才做完,代码写的比较仓促,大致思路如下:

import pandas as pd
import numpy as np
from tqdm import tqdm

# 读取
y_4662 = pd.read_csv('merge/y46624198223.csv')
y_4687 = pd.read_csv('merge/y46876205418.csv')
z_4729 = pd.read_csv('merge/z47295097868.csv')
z_4738 = pd.read_csv('merge/z47382782775.csv')
# 创建空数组
df = pd.DataFrame(np.zeros(y_4687.shape[0]*8)
                  .reshape((y_4687.shape[0],8))
                  ,columns=['y_4662','y_4687','z_4729','z_4738','1','2','3','最终label'])


df['y_4662'] = y_4662['label']
df['y_4687'] = y_4687['label']
df['z_4729'] = z_4729['label']
df['z_4738'] = z_4738['label']
df.to_csv('merge.csv')

for i in tqdm(range(0,df.shape[0])):
    if df['y_4662'][i] == 1:
        df['1'][i] = df['1'][i]+0.4662
    elif df['y_4662'][i] == 2:
        df['2'][i] = df['2'][i]+0.4662
    elif df['y_4662'][i] == 3:
        df['3'][i] = df['3'][i]+0.4662

    if df['y_4687'][i] == 1:
        df['1'][i] = df['1'][i]+0.4687
    elif df['y_4687'][i] == 2:
        df['2'][i] = df['2'][i]+0.4687
    elif df['y_4687'][i] == 3:
        df['3'][i] = df['3'][i]+0.4687

    if df['z_4729'][i] == 1:
        df['1'][i] = df['1'][i]+0.4729
    elif df['z_4729'][i] == 2:
        df['2'][i] = df['2'][i]+0.4729
    elif df['z_4729'][i] == 3:
        df['3'][i] = df['3'][i]+0.4729

    if df['z_4738'][i] == 1:
        df['1'][i] = df['1'][i]+0.4738
    elif df['z_4738'][i] == 2:
        df['2'][i] = df['2'][i]+0.4738
    elif df['z_4738'][i] == 3:
        df['3'][i] = df['3'][i]+0.4738
df.to_csv('merge.csv',index=False)
df = pd.read_csv('merge.csv')
df_3 = df.loc[:,list(df.columns)[4:7]]
nump = df_3.values
df['最终label'] = np.argmax(nump,axis=1)+1
print(np.argmax(nump,axis=1)+1)
df.to_csv('merge.csv',index=False)

7 别的方法以及遇到的一些问题

  1. CatBoost虽然简单,但是初步调参的效果不如LGBM;
  2. 单向LSTM的验证集分数更高,但是双向LSTM的线上分更高;
  3. 双向LSTM和LGBM可以尝试stacking融合,时间不允许故没有尝试 ;
  4. 官方8.1的测试集与 之前给出的7.1-7.30的训练集分布有一定差异,所以普遍验证集分数都比测试集高很多;
  5. 还有其他模型比如XGBM值得尝试;
  6. 拓扑图我们始终没有利用起来,导致双向LSTM卡在了0.473,LGBM卡在了0.466,如果能把拓扑图好好利用,做一些有用的特征,也许能更好;
  7. 按照前几名的说法,LGBM比LSTM更适合做这个比赛;
  8. 滴滴官方的视频里给出的方案为GCN和seq2seq,但我们没有学到这方面的知识,没有能成功实现
  9. 对于时空特征我们挖掘的不够,这个赛题特征工程尤为重要。
  10. 道路id似乎可以进行编码然后当做一个特征送入双向LSTM中,这点值得商榷。

8 写在最后

第一次参加比赛,能走到这里已经算是满足,感谢我的队友和我的老师,以及发布baseline帮助过我们的同学,希望以后能再接再厉,谨以此文纪念这些天的努力。
在这里插入图片描述

在这里插入图片描述

有问题欢迎联系我:
1759412770@qq.com
zn1759412770@163.com

  • 11
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

锌a

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值