本篇内容是
datawhale
2023第三期夏令营机器学习笔记-第一篇
一 baseline流程跑通
1 加载必要的库
import pandas as pd
import numpy as np
from sklearn.tree import DecisionTreeClassifier
from sklearn.metrics import f1_score
2 读取数据
train_data = pd.read_csv('data/train.csv')
test_data = pd.read_csv('data/test.csv')
3 特征工程
3.1 变换时间戳
这里将以毫秒表示的时间转为常见的时间格式。
train_data['common_ts'] = pd.to_datetime(train_data['common_ts'], unit='ms')
test_data['common_ts'] = pd.to_datetime(test_data['common_ts'], unit='ms')
例如:将1689673468244
转为时间2023-07-18 09:44:28.244000
。
3.2 udmap
特征处理
由于原始的udmap
特征是非结构化的形式存储,机器学习不支持该类数据的输入,因此需要转化为结构化的特征。
def udmap_onethot(d):
v = np.zeros(9)
if d == 'unknown':
return v
d = eval(d) #将字符形式的数据解析为字典
for i in range(1, 10):
if 'key' + str(i) in d:
v[i-1] = d['key' + str(i)]
return v
train_udmap_df = pd.DataFrame(np.vstack(train_data['udmap'].apply(udmap_onethot)))
test_udmap_df = pd.DataFrame(np.vstack(test_data['udmap'].apply(udmap_onethot)))
在 python 中, 形如 {'key1': 3, 'key2': 2}
格式的为字典类型对象, 通过key-value
键值对的方式存储。而在本数据集中, udmap
实际是以字符的形式存储, 所以处理时需要先用eval
函数将udmap
解析为字典。
将经过处理的udmap
特征重命名,并与原始的数据进行拼接。
train_udmap_df.columns = ['key' + str(i) for i in range(1, 10)]
test_udmap_df.columns = ['key' + str(i) for i in range(1, 10)]
train_data = pd.concat([train_data, train_udmap_df], axis=1)
test_data = pd.concat([test_data, test_udmap_df], axis=1)
3.3 构建访问频率特征
根据不同的访问行为,得到各自的频率。
train_data['eid_freq'] = train_data['eid'].map(train_data['eid'].value_counts())
test_data['eid_freq'] = test_data['eid'].map(train_data['eid'].value_counts())
3.4 不同访问行为目标值的均值特征
根据不同的访问行为进行数据分组,并将同一组的目标值计算均值。
train_data['eid_mean'] = train_data['eid'].map(train_data.groupby('eid')['target'].mean())
test_data['eid_mean'] = test_data['eid'].map(train_data.groupby('eid')['target'].mean())
3.5 行为属性为unknown
的记为1
原始的unknown
不好表达,因此转为数值型的数据。
train_data['udmap_isunknown'] = (train_data['udmap'] == 'unknown').astype(int)
test_data['udmap_isunknown'] = (test_data['udmap'] == 'unknown').astype(int)
3.6 提取小时特征
train_data['common_ts_hour'] = train_data['common_ts'].dt.hour
test_data['common_ts_hour'] = test_data['common_ts'].dt.hour
4 模型训练
clf = DecisionTreeClassifier()
clf.fit(
train_data.drop(['udmap', 'common_ts', 'uuid', 'target'], axis=1),
train_data['target']
)
5 结果预测
predict = pd.DataFrame({
'uuid': test_data['uuid'],
'target': model.predict(test_data.drop(['udmap', 'common_ts', 'uuid'], axis=1))
})
predict.to_csv('predict/submit_20230813_01.csv', index=None)
6 本地结果验证
6.1 所有数据本地验证
根据4的模型训练,计算在本地训练的情况下,f1_score
的得分。
pred = clf.predict(train_data.drop(['udmap', 'common_ts', 'uuid', 'target'], axis=1))
accuracy = f1_score(train_data['target'],pred, average='macro')
print('在训练集上的精确度: %.4f'%accuracy)
#结果为:0.9521
6.2 切分数据为训练集与验证集
这里只用训练集进行数据的训练,查看训练集与验证集的差距。
#random_state:保证结果复现
train, val = train_test_split(train_data, test_size=0.2, random_state=2023, shuffle=True)
clf1 = DecisionTreeClassifier()
clf1.fit(
train.drop(['udmap', 'common_ts', 'uuid', 'target'], axis=1),
train['target']
)
pred1 = clf1.predict(train.drop(['udmap', 'common_ts', 'uuid', 'target'], axis=1))
accuracy1 = f1_score(train['target'],pred1, average='macro')
print('在训练集上的精确度: %.4f'%accuracy1)
pred2 = clf1.predict(val.drop(['udmap', 'common_ts', 'uuid', 'target'], axis=1))
accuracy2 = f1_score(val['target'],pred2, average='macro')
print('在测试集上的精确度: %.4f'%accuracy2)
''' 结果如下:
在训练集上的精确度: 0.9555
在测试集上的精确度: 0.7715
'''
这里可以看出来在训练集表现良好,验证集的结果很差,说明模型过拟合了。
6.3 采用交叉验证查看结果
'''***--- 增加交叉验证在默认参数情况下 ---***'''
# 原始训练集
scores = cross_val_score(clf,train_data.drop(['udmap', 'common_ts', 'uuid', 'target'], axis=1),
train_data['target'],cv=5, scoring='f1_macro')
print("训练集上的精确度: %0.2f (+/- %0.2f)" % (scores.mean(), scores.std() * 2))
# 切分后的训练集
scores1 = cross_val_score(clf1,train.drop(['udmap', 'common_ts', 'uuid', 'target'], axis=1),
train['target'],cv=5, scoring='f1_macro')
print("训练集上的精确度: %0.2f (+/- %0.2f)" % (scores1.mean(), scores1.std() * 2))
scores2 = cross_val_score(clf1,val.drop(['udmap', 'common_ts', 'uuid', 'target'], axis=1),
val['target'],cv=5, scoring='f1_macro')
print("测试集上的平均精确度: %0.2f (+/- %0.2f)" % (scores2.mean(), scores2.std() * 2))
'''
结果为:
训练集上的精确度: 0.77 (+/- 0.00)
训练集上的精确度: 0.76 (+/- 0.00)
测试集上的平均精确度: 0.68 (+/- 0.01)
'''
6.4 线上提交结果
线上结果为:0.62687。
7 结论
以上是baseline的整个流程,由最后的本地结果验证可以知道,模型存在过拟合的情况。
以下是采用lightgbm
模型进行训练,如果采用默认参数,效果很差,线上分数大概为0.18左右(我已经尝试过了,小伙伴们可以不用尝试了),因此这里采用了optuna
进行了模型的粗调。
二 lightgbm + optuna
1 将baseline的1-4步骤封装至Load_data.py
# -*- coding: utf-8 -*-
"""
将baseline提供的特征工程封装为一个函数,并返回训练集与测试集
"""
import pandas as pd
import numpy as np
def load_data():
# 1.加载数据
train_data = pd.read_csv('data/train.csv')
test_data = pd.read_csv('data/test.csv')
# 2.将时间戳转为常用的时间格式
train_data['common_ts'] = pd.to_datetime(train_data['common_ts'], unit='ms')
test_data['common_ts'] = pd.to_datetime(test_data['common_ts'], unit='ms')
# 3.对其中一列变量进行one-hot编码
def udmap_onethot(d):
v = np.zeros(9)
if d == 'unknown':
return v
d = eval(d)
for i in range(1, 10):
if 'key' + str(i) in d:
v[i-1] = d['key' + str(i)]
return v
train_udmap_df = pd.DataFrame(np.vstack(train_data['udmap'].apply(udmap_onethot)))
test_udmap_df = pd.DataFrame(np.vstack(test_data['udmap'].apply(udmap_onethot)))
train_udmap_df.columns = ['key' + str(i) for i in range(1, 10)]
test_udmap_df.columns = ['key' + str(i) for i in range(1, 10)]
train_data = pd.concat([train_data, train_udmap_df], axis=1)
test_data = pd.concat([test_data, test_udmap_df], axis=1)
# 4.访问行为特征得到访问频率
train_data['eid_freq'] = train_data['eid'].map(train_data['eid'].value_counts())
test_data['eid_freq'] = test_data['eid'].map(train_data['eid'].value_counts())
# 5.不同访问行为目标值的均值
train_data['eid_mean'] = train_data['eid'].map(train_data.groupby('eid')['target'].mean())
test_data['eid_mean'] = test_data['eid'].map(train_data.groupby('eid')['target'].mean())
# 6.行为属性为unknown的记为1
train_data['udmap_isunknown'] = (train_data['udmap'] == 'unknown').astype(int)
test_data['udmap_isunknown'] = (test_data['udmap'] == 'unknown').astype(int)
# 7.提取小时特征
train_data['common_ts_hour'] = train_data['common_ts'].dt.hour
test_data['common_ts_hour'] = test_data['common_ts'].dt.hour
return train_data,test_data
2 加载必要的库
import numpy as np
from Load_data import load_data #加载1中的数据。
import optuna
import lightgbm as lgb
from sklearn.model_selection import train_test_split
from sklearn.metrics import f1_score
from optuna.visualization import plot_optimization_history
from optuna.visualization import plot_param_importances
import time
3 读取数据
train_data, test_data = load_data()
# 在训练集将原始的udmap、时间戳、uuid、target四列数据去掉,得到特征X与标签y
X = train_data.drop(['udmap', 'common_ts', 'uuid', 'target'], axis=1)
y = train_data['target']
# 将训练集分为训练集与验证集,验证集用于模型的评估
train_x, test_x, train_y, test_y = train_test_split(X, y, test_size=0.2,random_state=2023)
# 将训练集与验证集转换为lightgbm可以加载的数据格式
train_data = lgb.Dataset(data = train_x, label=train_y)
val_data = lgb.Dataset(data = test_x, label=test_y)
4 参数调整
4.1 自定义评估函数f1_score
由于lightgbm没有内置该函数,因此自定义该评估函数
'''
自定义的评估函数要求返回三个参数
name:评估函数名字,字符串格式
score: 计算出的分数
is_higher: 分数是否越高越好(True / False)
'''
def f1_eval(preds, true):
true = true.get_field('label')
preds = np.where(preds >= 0.5, 1, 0)
f_score = f1_score(true , preds, average = 'macro')
return 'f1_score', f_score, True
4.2 定义优化目标
def objective(trial):
params_naive={
"learning_rate":trial.suggest_categorical('learning_rate', [0.01, 0.02, 0.05, 0.005, 0.1]),
'max_bin':63,
'num_leaves':trial.suggest_int('num_leaves', 11, 333),
"max_depth":trial.suggest_int('max_depth', 5, 64),
'num_boost_round': trial.suggest_int('num_boost_round', 2000, 8000),
"lambda_l1":trial.suggest_float('lambda_l1', 0.001, 10.0),
"lambda_l2":trial.suggest_float('lambda_l2', 0.001, 10.0),
'random_state': 42,
'boosting_type': 'gbdt',
'metric': 'f1_score',
'device': 'cpu'
# 安装了gpu版本的可以采用gpu训练
#'gpu_platform_id': 0,
#'gpu_device_id': 0
}
bst = lgb.train(params_naive, train_data, valid_sets=[val_data], callbacks=[lgb.early_stopping(stopping_rounds=300)]
,verbose_eval = 50, feval=f1_eval)
preds = bst.predict(test_x)
preds = np.where(preds >= 0.5, 1, 0)
f1 = f1_score(test_y, preds, average = 'macro')
trial.set_user_attr("f1_score_val", f1)
return f1
4.3 开始优化
study = optuna.create_study(direction='maximize', sampler=optuna.samplers.TPESampler())
study.optimize(objective, n_trials=20)
5 保存最好的模型
这里训练集可以采用全部训练集,也可以采用切分后的训练集。
result = lgb.train(params=study.best_params, train_set=train_data)
result.save_model("model/light_20230816_01.txt")
6 加载模型并查看特征重要性
gbm = lgb.Booster(model_file='model/light_20230816_01.txt')
df_feature_importance = pd.DataFrame({
'importance':gbm.feature_importance(),
'name':gbm.feature_name()
})
df_feature_importance.sort_values(by='importance',ascending=False, inplace = True,ignore_index=True)
plt.barh(df_feature_importance['name'], df_feature_importance['importance'])
由上图可知,小时特征、x5、x4、key3、x7
这5个特征占比比较重要,可用于后续的特征构建。
7 本地最好的结果
7.1 得到optuna
调整的最好得分
df = study.trials_dataframe()
df.to_csv('optuna/optuna_light_20230816_01.csv',index = None)
study.best_score
在测试集上的f1
得分为:0.8087764989879106。
其中的csv
文件包含训练的参数,这里就不展示了。
7.2 线上提交结果
线上结果为:0.68696。
总结
第一步是baseline
的跑通,采用了决策树的算法,第二步是采用了lightgbm
+optuna
进行粗略的模型参数调整。最终只比baseline
的模型提高了一点点的分数,因此下一步注重对特征的构建。
7 本地最好的结果
7.1 得到optuna
调整的最好得分
df = study.trials_dataframe()
df.to_csv('optuna/optuna_light_20230816_01.csv',index = None)
study.best_score
在测试集上的f1
得分为:0.8087764989879106。
其中的csv
文件包含训练的参数,这里就不展示了。
7.2 线上提交结果
线上结果为:0.68696。
总结
第一步是baseline
的跑通,采用了决策树的算法,第二步是采用了lightgbm
+optuna
进行粗略的模型参数调整。最终只比baseline
的模型提高了一点点的分数,因此下一步注重对特征的构建。