用sklearn判断ins内容是否能上热门
背景
假设现在要使用爬虫从ins抓取内容,在ins的网页版上每个#标签下都有’热门’和’最新’两部分tab页,'热门’tab下只有9条内容,而’最新’tab页里页包含所有的热门内容,现在需要找到一个算法,将’最新’tab下的热门内容筛选出来。
那么现在我来介绍一下如何使用机器学习来实现这个算法,本文主要介绍从零开始使用机器学习并解决问题的过程,不会介绍细节。
开始之前要了解
- python:一种蛇
- instagram: 著名被墙网站之一
- pandas:数据处理类库
- matplotlib&seaborn: 画图类库
- 机器学习: 一种对计算机算法的研究方式,算法会根据经验自动优化效果
- 分类问题: 把一组数据分成两类或者多类,要划分类型已提前定义
- sklearn: 封装了多种机器学习算法的类库,开箱即用
数据处理
ins内容有很多字段信息,首先选择出可能影响内容上热门的字段,包含点赞/曝光/评论/发布时间/抓取时间/标题包含的tag数和当前页面tag下的内容总数等,这些内容用json格式保存,示例如下:
{
"id":"CAA-r0SjtF3",
"sourceTag":"dharmaproductions",
"publishTime":1589130910000,
"addTime":1589134125000,
"likes":4,
"comments":1,
"views":18,
"tagNumber":15,
"description":"",
"totalMedia":57149,
"hot":1
}
数据探索
这一步是为了进一步了解原始数据,检查数据真实性和字段的分布
- 用seaborn画直方图,查看热门/非热门内容的比例:
self.df = pd.read_json('debug/train_video.jl', lines=True)
sns.countplot(self.df['hot'], label="Count")
图中热门内容占比还是比较高的,有一点脱离真实情况,实际对于ins上被活跃使用的tag,本身内容数量就很多又更新频繁,所以只有很少一部分内容能上热门。
- 用seaborn画热力图,这一步是为了查看字段之间的关系:
self.df = pd.read_json('debug/train_video.jl', lines=True)
corr = self.df.corr()
plt.figure(figsize=(14, 14))
sns.heatmap(corr, annot=True)
corr()函数会计算数据集df中各字段的相关关系,图中的颜色越浅代表越相关,在左上角可以看到like/view之前相关性比较高,如果两个字段之间相关性接近1,可以考虑去掉其中一个字段。
- 用matplotlib画频率分布直方图,查看字段的分布类型
def histogram(self, items=None, index=None, field=None, y_label='Probability'):
"""
画频率直方图(带正态分布曲线)
:param index: 图片位置
:param field: 字段名
:param y_label:
:return:
"""
title = field + ' distribution'
if items is None:
items = self.df[field]
try:
plt.subplot(index)
mean = items.mean()
std = items.std()
x = np.arange(items.min(), items.max())
y = self.normfun(x=x, mu=mean, sigma=std)
plt.plot(x, y)
plt.hist(items, bins='auto', density=True, rwidth=0.9, stacked=True)
plt.title(title)
plt.xlabel(field)
plt.ylabel(y_label)
plt.tight_layout()
except Exception as e:
print(e)
图中蓝色的线是正态分布函数曲线,很明显like/view/comment的分布不符合正态分布,更像是长尾分布
数据清洗&特征选择
因为原始数据中并没有缺失值或异常值得情况,所以忽略数据清洗的步骤,直接进行特征选择.
首先我们不需要具体的时间戳,把发布时间publistTime和抓取时间addTime的间隔计算出来,单位是天和小时(ins热门tab中通常是最近发布的内容)
df['days'] = (pd.to_datetime(df['addTime'], unit='ms') - pd.to_datetime(df['publishTime'], unit='ms')).dt.days
# 不满一天的用1天替换
df['days'] = df['days'].replace(0, 1)
df['hours'] = ((pd.to_datetime(df['addTime'], unit='ms') - pd.to_datetime(df['publishTime'], unit='ms'))
.dt.total_seconds() / 3600).astype(int)
df['hours'] = df['hours'].replace(0, 1)
然后因为一般内容发布越早曝光也越高,所以添加like_per_hour/view_per_hour两个特征,减弱时间影响
df['like_per_hour'] = (df['likes'] / df['hours']).astype(int)
df['view_per_hour'] = (df['views'] / df['hours']).astype(int)
最后选择的特征如下:
features = ['views', 'likes', 'comments', 'tagNumber', 'totalMedia', 'days', 'like_per_hour', 'view_per_hour']
评分标准
在选择分类算法前,先来了解一下评估分类算法的常用标准
准确率,精确率,召回率
假设你开发了一款检测新冠病毒的试剂盒,那么每次检测结果有一下四种(阳性表示被检测人携带病毒):
TP(True Prosivite): 真阳性,说明正确检测出病毒
FP(False Prosivite): 假阳性
TN(True Negative): 真阴性
FN(False Negative): 假阴性,携带病毒却没有检测出来
对于病毒检测来说FN的危害显然要比FP更大,而根据这几种情况的样本数量就可以计算出准确率,精确率和召回率:
准确率(accuracy): T P + T N T P + F P + T N + F N {TP+TN}\over{TP+FP+TN+FN} TP+FP+TN+FNTP+TN,代表全部样本的正确率
精确率(precision): T P T P + F P {TP}\over{TP+FP} TP+FPTP,代表检测为阳性时的正确率
召回率(recall): T P T P + F N {TP}\over{TP+FN} TP+FNTP,代表所有病毒携带者被检测为阳性的覆盖率,也叫查全率
有三个样本,检测结果为y_predict,而实际值为y_true,1代表阳性,sklearn的score函数默认返回的是阳性分类的分数
from sklearn.metrics import accuracy_score,precision_score,recall_score
if __name__ == '__main__':
y_predict = [1,0,1]
y_true = [0,0,1]
print(f'accuracy:{accuracy_score(y_true,y_predict)}')
print(f'precision:{precision_score(y_true,y_predict)}')
print(f'recall:{recall_score(y_true,y_predict)}')
输出结果为:
accuracy:0.6666666666666666
precision:0.5
recall:1.0
对于判断ins是否属于热门内容的算法,可以允许ta把非热门内容分到热门但是要尽量不遗漏热门内容,即FN越小越好,FP 可以大一些,所以我们要求召回率越高越好,精确率次之
宏平均和微平均
对于求准确率还有宏平均和微平均两种方式,宏平均是对直接每个分类的准确率求平均值,而微平均要先对所有分类的预测结果求和再计算平均值,举个例子:
Class A: 1 TP and 1 FP
Class B: 10 TP and 90 FP
Class C: 1 TP and 1 FP
Class D: 1 TP and 1 FP
对于上面的分类结果:
准确率 p A = p C = p D = 0.5 pA=pC=pD=0.5 pA=pC=pD=0.5, p B = 0.1 pB=0.1 pB=0.1
宏 平 均 准 确 率 ( m a c r o − a v g ) = 宏平均准确率(macro-avg)= 宏平均准确率(macro−avg)= 0.5 + 0.1 + 0.5 + 0.5 4 {0.5+0.1+0.5+0.5}\over{4} 40.5+0.1+0.5+0.5 = 0.4 =0.4 =0.4
微 平 均 准 确 率 ( m i c r o − a v g ) = 微平均准确率(micro-avg)= 微平均准确率(micro−avg)= 1 + 10 + 1 + 1 2 + 100 + 2 + 2 {1+10+1+1}\over{2+100+2+2} 2+100+2+21+10+1+1 = 0.123 =0.123 =0.123
这个例子体现了宏平均把所有分类的权重都视为1的问题,在进行C分类时只有0.1的准确率,并且分类C的样本数占整体的90%以上,却没有影响宏平均的结果,所以一般把宏平均改进为加权的宏平均(权重是分类样本占总数的比例):
m
a
c
r
o
−
w
e
i
g
h
t
−
a
v
g
=
macro-weight-avg=
macro−weight−avg=
0.0189
×
0.5
+
0.943
×
0.1
+
0.0189
×
0.5
+
0.0189
×
0.5
=
0.123
0.0189\times0.5+0.943\times0.1+0.0189\times0.5+0.0189\times0.5=0.123
0.0189×0.5+0.943×0.1+0.0189×0.5+0.0189×0.5=0.123
算法选择
sklearn常用分类算法
决策树: 使用树形结构,把特征作为决策树上的节点,叶节点就是分类结果,构造决策树时追求最纯净的分类结果(越纯净则分类的不确定性越低)
朴素贝叶斯: 在你不知道事件全貌的情况下,先根据一点人生经验得到一个主观判断,然后根据后续观察结果进行修正.根据概率大小判断最后的分类,常用于文本分类
SVM: 几何解法,先把所有样本用多维空间的向量表示,然后求一个平面将不同分类的样本分隔开
KNN: K-Nearest Neighbor,几何解法,因为"近朱者赤,近墨者黑",所以对于节点A,相邻最近的K个节点是什么类型,A大概就是什么类型
集成算法: 本着"人多力量大"的原则,使用多个分类器一起工作,按分类器的协作方式分为两种,bagging:分类器一起投票,看哪个分类票多;boosting:再学习,通过多次迭代强化整体的分类效果
效果对比
准备了训练数据24w条,测试数据2.7w条,训练数据来自上百个tag,非热门内容占比较少,测试来自两个tag,非热门数据占比更接近整体比例.
import pandas as pd
from sklearn import naive_bayes
from sklearn.ensemble import AdaBoostClassifier, RandomForestClassifier
from sklearn.metrics import classification_report
from sklearn.neighbors import KNeighborsClassifier
from sklearn.tree import DecisionTreeClassifier
from sklearn.utils import shuffle
clf_list = {
'cart': DecisionTreeClassifier(max_depth=5),
# 'svm': svm.SVC(), # 太慢了,放弃
# 'nb_gauss': naive_bayes.GaussianNB(), # 太差了,放弃
'nb_multi': naive_bayes.MultinomialNB(),
'k_neighbors': KNeighborsClassifier(),
'adaptive_boost': AdaBoostClassifier(),
'random_forest': RandomForestClassifier(max_depth=2)
}
def train():
train_data = pd.read_json('debug/train_video.jl', lines=True)
train_data = shuffle(train_data)
# 特征选择
features = ['views', 'likes', 'comments', 'tagNumber', 'totalMedia', 'days', 'like_per_hour', 'view_per_hour']
train_features = train_data[features]
train_labels = train_data['hot']
# 测试数据
test_data = pd.read_json('debug/test_video.jl', lines=True)
test_data = shuffle(test_data)
test_features = test_data[features]
test_labels = test_data['hot']
for k, clf in clf_list.items():
print(f'-----{k} result-------')
# 决策树训练
clf.fit(train_features, train_labels)
target_names = ['normal', 'hot']
test_predict = clf.predict(test_features)
sample_weight = test_labels.replace(1, 100).replace(0, 1)
# 打印测试报告
print(classification_report(test_labels, test_predict, target_names=target_names, sample_weight=sample_weight))
在测试报告中,f1是precision/recall的综合平均分,support是分类的样本权重(默认按样本数量计算权重)。因为ins内容热门数量比较少,所以我用sample_weight参数把hot分类的权重调整为normal分类的100倍,macro/weighted avg就是上面说的宏平均和加权宏平均。
观察测试报告可以发现,CART决策树的对热门内容的召回率最高,达到92%,朴素贝叶斯次之
-----cart result-------
precision recall f1-score support
normal 0.44 0.90 0.59 24222.0
hot 0.99 0.92 0.95 344800.0
accuracy 0.92 369022.0
macro avg 0.72 0.91 0.77 369022.0
weighted avg 0.96 0.92 0.93 369022.0
-----nb_multi result-------
precision recall f1-score support
normal 0.17 0.63 0.27 24222.0
hot 0.97 0.79 0.87 344800.0
accuracy 0.78 369022.0
macro avg 0.57 0.71 0.57 369022.0
weighted avg 0.92 0.78 0.83 369022.0
-----k_neighbors result-------
precision recall f1-score support
normal 0.11 0.69 0.20 24222.0
hot 0.97 0.63 0.76 344800.0
accuracy 0.63 369022.0
macro avg 0.54 0.66 0.48 369022.0
weighted avg 0.91 0.63 0.72 369022.0
-----adaptive_boost result-------
precision recall f1-score support
normal 0.08 0.99 0.15 24222.0
hot 1.00 0.20 0.34 344800.0
accuracy 0.25 369022.0
macro avg 0.54 0.60 0.24 369022.0
weighted avg 0.94 0.25 0.32 369022.0
-----random_forest result-------
precision recall f1-score support
normal 0.10 0.96 0.18 24222.0
hot 0.99 0.37 0.54 344800.0
accuracy 0.41 369022.0
macro avg 0.54 0.67 0.36 369022.0
weighted avg 0.93 0.41 0.52 369022.0
为了进一步验证,将测试集分为14个大小2000的子集,然后分别记录每次的测试结果,画出折线图,选择的标准是热门内容的召回率&精确率和整体的准确率,在14次测试中CART算法都表现最好。
重复测试的代码如下:
def train():
train_data = pd.read_json('debug/train_video.jl', lines=True)
train_data = shuffle(train_data)
# 特征选择
features = ['views', 'likes', 'comments', 'tagNumber', 'totalMedia', 'days', 'like_per_hour', 'view_per_hour']
train_features = train_data[features]
train_labels = train_data['hot']
# 测试数据
test_data = pd.read_json('debug/test_video.jl', lines=True)
test_data = shuffle(test_data)
# 切分为每组大小为2000的集合
test_size = 2000
test_list = [test_data[i:i + test_size] for i in range(0, test_data.shape[0], test_size)]
reports = {}
for k, clf in clf_list.items():
print(f'-----{k} result-------')
# 决策树训练
clf.fit(train_features, train_labels)
target_names = ['normal', 'hot']
for test_set in test_list:
test_features = test_set[features]
test_labels = test_set['hot']
test_predict = clf.predict(test_features)
sample_weight = test_labels.replace(1, 100).replace(0, 1)
report = classification_report(test_labels, test_predict, target_names=target_names,
sample_weight=sample_weight, output_dict=True)
reports.setdefault(f'{k}_hot_recall', []).append(report['hot']['recall'])
reports.setdefault(f'{k}_hot_precision', []).append(report['hot']['precision'])
reports.setdefault(f'{k}_accuracy', []).append(report['accuracy'])
df = pd.DataFrame(reports)
metrics = ['hot_recall', 'hot_precision', '_accuracy']
fig, axes = plt.subplots(nrows=3, figsize=(16, 20))
for i, m in enumerate(metrics):
m_df = df.filter(regex=m)
m_df.plot(ax=axes[i], title=m, xticks=range(1, len(test_list) + 1))
fig.savefig(f"debug/report.png")
未解决的问题
这里我们缺失了机器学习最关键的一步,那就是调参(手动狗头),总感觉使用默认参数的模型没有灵魂。因为按直觉来说随机森林和AdaBoost应该优于决策树才对,结果准确率和召回率相差都很大,还需要优化。
另外测试结果容易受测试集影响,当我使用只有39个样本,其中只有一个非热门内容的测试集时,决策树的召回率反而最低,只有79%,所以选择测试集时要注意样本数量和真实性。
-----cart result-------
precision recall f1-score support
normal 0.00 1.00 0.00 1.0
hot 1.00 0.79 0.88 3800.0
accuracy 0.79 3801.0
macro avg 0.50 0.89 0.44 3801.0
weighted avg 1.00 0.79 0.88 3801.0
保存分类器
把表现最好的CART分类器模型保存到代码文件中,然后当需要对ins内容进行分类就可以直接用模型进行判断了.保存模型时我使用的是cPickle(
除了cPickle还可以使用joblib,但joblib还需要安装)
保存非常简单,调用dump()即可
import _pickle as cPickle
# 保存
with open('ins_hot_model.clf','wb') as f:
cPickle.dump(clf,f)
使用时需要从文件中加载分类器:
def use_model():
# 未分类的ins文档信息
doc = {"likes": 107, "comments": 2, "views": 435.0, "tagNumber": 25, "totalMedia": 22000, "days": 1343,
"like_per_hour": 0, "view_per_hour": 0}
# 读取分类器
clf = cPickle.load(open('ins_hot.clf', "rb"))
# 把文档转换为DataFrame
source = pd.DataFrame([doc], columns=list(doc.keys()))
features = ['views', 'likes', 'comments', 'tagNumber', 'totalMedia', 'days', 'like_per_hour', 'view_per_hour']
df = source[features]
# 为文档分类
print(clf.predict(df))
总结
进行数据分析时,需要经过数据采集、清洗、特征选择、模型训练几个过程,借助sklearn库可以让我们轻松地使用各种机器学习算法,而对于在生产环境中用机器学习解决实际问题,还是需要丰富的经验和大量的优化验证才可以,看来要成为码农里最高薪的调参工程师也不是那么简单。
最后,想进一步了解机器学习和调参技巧的盆友们,现在扫码即可8折购买参加极客时间的数据分析课程哦😯
关注公众号,第一时间更新