基于集成学习的亚马逊用户评论预测

1 简介

集成学习(ensemble learning),并不是一个单独的机器学习算法,而是通过构建并结合多个机器学习器(基学习器,Base learner)来完成学习任务。

一般来说,集成学习的构建方法可以分为两类:

  • 平行方法:

    • 构建多个独立的学习器,取他们的预测结果的平均
    • 个体学习器之间不存在强依赖关系,一系列个体学习器可以并行生成
    • 通常是同质的弱学习器
    • 代表算法是Bagging随机森林(Random Forest)系列算法。
  • 顺序化方法:

    • 多个学习器是依次构建的
    • 个体学习器之间存在强依赖关系,因为一系列个体学习器需要串行生成
    • 通常是异质的学习器
    • 代表算法是Boosting系列算法,比如AdaBoost梯度提升树等。
  • 所有可运行代码可在代码仓库中下载:https://github.com/Guoxn1/ai。

  • 原出处:https://gitlab.diantouedu.cn/QY/test1/tree/master/%E4%BA%BA%E5%B7%A5%E6%99%BA%E8%83%BD%E7%B3%BB%E7%BB%9F%E5%AE%9E%E6%88%98%E7%AC%AC%E4%B8%89%E6%9C%9F/%E5%AE%9E%E6%88%98%E4%BB%A3%E7%A0%81/%E6%9C%BA%E5%99%A8%E5%AD%A6%E4%B9%A0%E9%A1%B9%E7%9B%AE%E5%AE%9E%E6%88%98/%E5%9F%BA%E4%BA%8E%E9%9B%86%E6%88%90%E5%AD%A6%E4%B9%A0%E7%9A%84Amazon%E7%94%A8%E6%88%B7%E8%AF%84%E8%AE%BA%E8%B4%A8%E9%87%8F%E9%A2%84%E6%B5%8B。

2 数据预处理

2.1 数据简介

本次案例所采用的数据是Amazon上面的用户评论,我们需要对评论质量进行评估。

image-20231107132826625

其中test.csv和train.csv分别是测试数据集和训练数据集,pre_l.csv是测试数据集的标签,main.ipynb是主要的运行程序。

train.csv中一共有七列:

image-20231107142546446

reviewID是用户ID

asin是商品ID

reviewText是评论内容

overall是用户对商品的打分

votes_up是认为评论有用的点赞数

votes_all是该评论得到的总点赞数

label是标签

test.csv:

image-20231107143011497

测试集只有评论内容和用户对商品的打分有可能作为X,label在另一个文件中。

观察数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from sklearn.naive_bayes import MultinomialNB, BernoulliNB, ComplementNB, GaussianNB # 导入不同类型的朴素贝叶斯分类器
from sklearn.feature_extraction.text import CountVectorizer, TfidfVectorizer # 导入文本特征提取工具:词频和TF-IDF向量化器
from sklearn import preprocessing, tree, ensemble, svm, metrics, calibration # 导入预处理、决策树、集成方法、支持向量机、评价指标等模块
from sklearn.model_selection import cross_val_score, train_test_split
from sklearn.neighbors import KNeighborsClassifier
from sklearn.metrics import auc, accuracy_score
from sklearn.preprocessing import StandardScaler
from sklearn.feature_extraction import text
from matplotlib import pyplot as plt
from itertools import combinations
from wordcloud import WordCloud # 导入生成词云的工具
from collections import Counter
from textblob import TextBlob # 导入文本情感分析工具
from tqdm import tqdm
import pandas as pd
import numpy as np
import random
import math

train_df = pd.read_csv("train.csv",sep="\t")
test_df = pd.read_csv("test.csv",sep="\t",index_col=False)
train_df.describe()

2.2 为测试集增加标签列

1
2
labels=pd.read_csv("pre_l.csv")
labels._append(labels)

2.3 去除测试集id列

id列对于我们没什么用,当然这里去不去都行,因为最后我们选择的时候不选择就可以了。

2.4 建立训练和测试集的评论的长度及情感极性列

这里主要利用到了已经有的库,TextbBlob,来判断情感极性,位于0到1之间的值,帮助我们赋值判断。

1
2
3
4
5
6
7
train_df["length"] = train_df["reviewText"].map(lambda x: len(x.split(" ")))
# 使用了TextBlob库中的sentiment方法,该方法返回一个元组,第一个元素是极性值,第二个元素是主观性值
train_df["polarity"] = train_df["reviewText"].map(lambda x : TextBlob(str(x)).sentiment[0])

test_df["length"] = test_df["reviewText"].map(lambda x: len(str(x).split(" ")))
# 使用了TextBlob库中的sentiment方法,该方法返回一个元组,第一个元素是极性值,第二个元素是主观性值
test_df["polarity"] = test_df["reviewText"].map(lambda x : TextBlob(str(x)).sentiment[0])

2.5 转评论词为向量表示

评论是个比较珍贵的资源,单单用库还是不够的,况且我们这次的目标就是集成学习。

所以把词转为词向量的形式,然后,再将其作为训练数据训练。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 将文本数据向量化
def get_vectorizer(method,min_df,max_df,max_features,stop_words):
if method=="count":
return CountVectorizer(min_df=min_df,max_df=max_df,max_features=max_features,stop_words=stop_words)
if method =="tfidf":
return TfidfVectorizer(min_df=min_df,max_df=max_df,max_features=max_features,stop_words=stop_words,ngram_range=(1,1))

def tranfrom_data(vectorizer,data):
return vectorizer.transform(data).toarray()

def vectorize(train_data,test_data=None,min_df = 0.019,max_df = 1.0,max_features=1000,stop_words="english",method="tfidf"):
vectorizer = get_vectorizer(method,min_df,max_df,max_features,stop_words)
vectorizer_model = vectorizer.fit(train_data)
features = tranfrom_data(vectorizer,train_data)
if not test_data.empty:
test_features = tranfrom_data(vectorizer, test_data)
else:
test_features = None
return features, test_features
1
2
3
4
5
features, test_features = vectorize(train_df['reviewText'], test_df['reviewText'].apply(lambda x: np.str_(x)))
print('features.shape:')
print(features.shape, test_features.shape if test_features is not None else None)


1
2
3
4
5
#去除一些空值和异常值
test_df.fillna(0.0,inplace=True)
test_df.head()
test_df[test_df['overall']=='overall']
test_df['overall'].iloc[11209]=0.0

2.6 合并训练集和测试集

合并我们的数据集,最后我们需要知道,训练的x是需要有词向量这一列,然后还有Textblob给出的评分以及词的长度,词的长度越长可能会表达的情感越强烈,所以也纳入标准,总评当然也纳入。合并后要对其进行最小最大归一化。

1
2
3
4
5
6
7
8
X_train = np.concatenate(
[features, train_df[['overall', 'length', 'polarity']]], axis=1)
X_train = preprocessing.MinMaxScaler().fit_transform(X_train)

X_test = np.concatenate(
[test_features, test_df[['overall', 'length', 'polarity']]], axis=1)
X_test = preprocessing.MinMaxScaler().fit_transform(X_test)

3 数据处理

3.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
from sklearn import calibration, svm, tree
from sklearn.naive_bayes import MultinomialNB
from sklearn.neighbors import KNeighborsClassifier
from sklearn.metrics import accuracy_score
from tqdm import tqdm

def get_classifiers():
# 返回一个字典,包含了不同分类器的名称和对应的分类器对象
return {
'LinearSVM': calibration.CalibratedClassifierCV(svm.LinearSVC(loss='squared_hinge', dual=False)),
'DecisionTree': tree.DecisionTreeClassifier(criterion='gini', max_depth=5, splitter='random'),
'MultBayes': MultinomialNB(alpha=1, fit_prior=True, class_prior=[0.8, 0.2]),
'Knn': KNeighborsClassifier(n_neighbors=3)
}

def train_and_evaluate(clf, X_train, y_train):
# 训练分类器并计算准确率
clf.fit(X_train[:10000], y_train[:10000])
predictions = clf.predict(X_train)
accuracy = accuracy_score(predictions, y_train)
return accuracy

# 获取不同分类器的名称和对应的分类器对象
classifiers = get_classifiers()

# 对每个分类器进行训练和评估
for classifier_name, classifier in classifiers.items():
accuracy = train_and_evaluate(classifier, X_train, train_df['label'])
print(f"Accuracy of {classifier_name}: {accuracy}")

image-20231108104126950

准确率大概在78%左右徘徊。

3.2 bagging(并行)

在集成学习中,每个模型通常是基于相同的算法或模型类型,但在训练过程中可能会使用不同的训练数据或随机初始化来产生多个模型实例。这样做的目的是在模型之间引入多样性,以便更好地捕捉数据中的不同方面和模式,从而提高整体的预测性能和鲁棒性。

也就是说对于并行和串行,所使用的基分类器都是一样的。

定义一些平均函数(硬投票)和投票平均(软投票),一个是拿结果来平均,结果一般是0到1的概率值;一个是拿labels来平均,这个labels是根据结果升序排序后和阈值来确定的,比如result大于阈值labels就为1,小于阈值就是labels是0。阈值又是由整个数据集决定的,训练数据中0的个数占77%,测试集中占90%,所以阈值设定为85%比较合适。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from sklearn import metrics
import matplotlib.pyplot as plt
from sklearn.metrics import accuracy_score

def averange(results):
return np.average(results,axis=1)

# 定义函数weight_average,计算加权结果的平均值
def weight_average(results, weights):
return np.dot(results, weights) / np.sum(weights)

# 定义函数vote,计算标签的平均值
def vote(labels):
return np.average(labels, axis=1)

# 定义函数weight_vote,计算加权标签的平均值
def weight_vote(labels, weights):
return np.dot(labels, weights) / np.sum(weights)

需要一些绘图函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def plot_results(y,pres,methods,cls_name):
fig = plt.figure(figsize=(10,10))
for idx,method in enumerate(methods):
pre = pres[idx]
fpr,tpr,thresholds = metrics.roc_curve(y,pre,pos_label=1)
plt.subplot(2, 2, idx+1)
plt.plot(fpr, tpr, lw=2)
auc = metrics.auc(fpr, tpr)

pre[pre>=0.5],pre[pre<0.5] = 1,0
acc = accuracy_score(pre,y)
plt.title('Method: %s Auc: %.2f Acc: %.2f' % (method, auc, acc))
plt.suptitle('Classifier: '+str(cls_name))

def weight(results,labels,weights,y,cls_name):
results,labels,weights = np.array(results).T,np.array(labels).T, np.array(weights)
methods = ['average', 'weight-average', 'vote', 'weight-vote']
pres = [averange(results), weight_average(results, weights), vote(labels), weight_vote(labels, weights)]
plot_results(y,pres,methods,cls_name)

不管在串行还是并行中,都要进行随机部分取样。对于串行(boosting)的来说,取样的种类一定全部的种类,但是每个基分类器的到的样本应当是不一样的,是全集的一部分。在 Boosting 中,每个基分类器都倾向于关注那些在前一个分类器中分类错误的样本。因此,每个基分类器需要使用全部特征来尽可能准确地纠正错误。对于并行(bagging)来说,每个基分类器的特征数目通常是部分的,而不是全部的特征。在 Bagging 中,每个基分类器只使用部分特征的目的是增加基分类器之间的差异性,从而提高集成模型的多样性和泛化能力。

所以定义随机抽样函数,上面提到的创建labels的函数以及训练函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import numpy as np
import random
import math
from sklearn.metrics import accuracy_score

import random

def draw_samples(datas,sample_size,feature_size):
sample_indices = random.sample(range(datas[0].shape[0]),sample_size)
feature_indices = random.sample(range(datas[0].shape[1]),feature_size)
return sample_indices,feature_indices

def fit_and_predict(clf,datas,sample_indecs,feature_indices):

clf.fit(datas[0][sample_indecs][:,feature_indices],datas[1][sample_indecs])
predictions = np.array([p[1] for p in clf.predict_proba(datas[2][:,feature_indices])])
return predictions

def create_labels(predictions,threshold):
labels = predictions.copy()
labels[labels>threshold],labels[labels<threshold] = 1,0
return labels

定义bagging方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def bagging(base_estimators,datas,n_estimators=10,max_samples = 0.1,max_features = 0.5):
assert(n_estimators>0 and max_samples>0 and max_features>0)
result,labels,weights = [],[],[]
sample_size,feature_size = int(max_samples*datas[0].shape[0]),int(max_features*datas[0].shape[1])
for clf in base_estimators:
for _ in range(n_estimators):
sample_indices,feature_indices = draw_samples(datas,sample_size,feature_size)

predictions = fit_and_predict(clf,datas,sample_indices,feature_indices)
result.append(predictions)
# 取阈值为0.85,在0.77-0.90之间 没问题
labels.append(create_labels(predictions,np.sort(predictions)[int(0.85*len(predictions))]))
labels[-1] = labels[-1].astype(int)
weights.append([accuracy_score(labels[-1],(datas[3]))])

return result,labels,weights


这里的方法中base_estimators在本次实验中每轮只会传一个,代表基分类器的种类只使用一种,但是n_estimators给了循环次数,对应到理论中就是一共10个相同的基分类器并行判断。

3.3 boosting(串行)

定义boosting

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
def adaboost(base_estimator,datas,n_estimators=30,learning_rate=1.0,max_samples=0.05,max_features=1.0):
assert (n_estimators > 0 and max_samples > 0 and max_features > 0)
results,labels,weights = [],[],[]
sample_size,feature_size = int(max_samples*datas[0].shape[0]),int(max_features*datas[0].shape[1])
clf = base_estimator
sample_weigths = np.array([1/datas[0].shape[0] for _ in range(datas[0].shape[0])])

for _ in range(n_estimators):
sample_indices,feature_indices = draw_samples(datas,sample_size,feature_size)

predictions = fit_and_predict(clf,datas,sample_indices,feature_indices)
# 计算错误分类的情况,并为其调整权重参数
# 异或操作
misclassified = np.array(clf.predict(datas[0][sample_indices,:][:,feature_indices])) ^ datas[1][sample_indices]
error = np.sum(sample_weigths[sample_indices][misclassified==1])
if error > 0.5:
print('ERROR more than half.')
break
sample_weigths[sample_indices][misclassified==1] *= learning_rate*error/(1-error)

sample_weigths /= np.sum(sample_weigths)
results.append(predictions)
labels.append(create_labels(predictions, np.sort(predictions)[int(0.8 * len(predictions))]))
weights.append(1/2 * math.log((1-error)/error))
return results,labels,weights

其中base_estimator是一个基分类器,但是由于n_estimators=30,代表我们有30个这样的基分类器在串行判断。为什么这里的循环就是串行而上面的是并行呢,主要是这里每次循环都受到上次循环的影响(权重),所以这里是串行性质,而上面的bagging就是并行特征。

这里面的参数更新规则可以看下面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
在 AdaBoost(自适应增强)算法中,权重的更新公式如下:

初始化样本权重:对于一个包含 N 个样本的训练集,初始时,每个样本的权重相等,即 w_i = 1/N,其中 i 表示样本的索引。

对于每个弱分类器(例如决策树),执行以下步骤:
a. 使用当前样本权重训练一个弱分类器。
b. 计算弱分类器在训练集上的错误率(误差),表示为 err。
c. 计算弱分类器的权重 alpha,根据以下公式计算:alpha = 0.5 * ln((1 - err) / err)。
d. 更新样本的权重:

对于被正确分类的样本,乘以 e^(-alpha)。
对于被错误分类的样本,乘以 e^(alpha)。
具体公式为:w_i = w_i * e^(alpha * I(y_i ≠ h(x_i))),其中 y_i 是样本的真实标签,h(x_i) 是弱分类器的预测结果,I() 是指示函数,当 y_i ≠ h(x_i) 时取值为 1,否则为 0
e. 标准化样本权重,使其总和为 1:w_i = w_i / sum(w),其中 sum(w) 表示所有样本权重的总和。
对于下一个弱分类器,重复步骤 2,直到达到预定的弱分类器数量或错误率满足要求。

代码里的^是表示异或。

err -> error

alpha -> weights

w_i以及I()函数 -> sample_weigths[sample_indices][misclassified==1]

训练:

1
2
3
4
5
6
7
8
9
10
11
12
13
y_train = train_df['label']
y_test = test_df['label'].astype(int)

base_estimators = get_classifiers()

for name,clf in base_estimators.items():
datas = [X_train, y_train, X_test, y_test]

results_bagging, labels_bagging, weights_bagging = bagging([clf], datas, n_estimators=10, max_samples=0.1, max_features=0.5)
weight(results_bagging, labels_bagging, weights_bagging, y_test, name + ' Bagging')

results_adaboost, labels_adaboost, weights_adaboost = adaboost(clf, datas, n_estimators=30, learning_rate=1.0, max_samples=0.05, max_features=1.0)
weight(results_adaboost, labels_adaboost, weights_adaboost, y_test, name + ' AdaBoost')

图比较大,只截取部分:

image-20231108110225433

image-20231108110236811

只输出准确率:

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
# Define a function to evaluate the model
def evaluate_model(y_test, y_pred):
accuracy = accuracy_score(y_test, y_pred)
print(f"Model accuracy: {accuracy}")

# Base estimators
base_estimators = get_classifiers()

# Train and evaluate ensemble models
for name, clf in base_estimators.items():
# Data format: [X_train, y_train, X_test, y_test]
datas = [X_train, y_train, X_test, y_test]

# Bagging
results_bagging, labels_bagging, weights_bagging = bagging([clf], datas, n_estimators=10, max_samples=0.1, max_features=0.5)
y_pred_bagging = np.average(results_bagging, axis=0) # average the results of all the bagging classifiers
y_pred_bagging = (y_pred_bagging > 0.5).astype(int)
print(f"Evaluation of Bagging with {name}:")
evaluate_model(y_test, y_pred_bagging)

# AdaBoost
results_adaboost, labels_adaboost, weights_adaboost = adaboost(clf, datas, n_estimators=30, learning_rate=1.0, max_samples=0.05, max_features=1.0)
y_pred_adaboost = np.average(results_adaboost, axis=0) # average the results of all the adaboost classifiers
y_pred_adaboost = (y_pred_adaboost > 0.5).astype(int)
print(f"Evaluation of AdaBoost with {name}:")
evaluate_model(y_test, y_pred_adaboost)

输出如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Evaluation of Bagging with LinearSVM:
Model accuracy: 0.8949058792042109
Evaluation of AdaBoost with LinearSVM:
Model accuracy: 0.9018645731108931
Evaluation of Bagging with DecisionTree:
Model accuracy: 0.9032027834775627
Evaluation of AdaBoost with DecisionTree:
Model accuracy: 0.9037380676242305
Evaluation of Bagging with MultBayes:
Model accuracy: 0.9040057096975644
Evaluation of AdaBoost with MultBayes:
Model accuracy: 0.8916049602997591
Evaluation of Bagging with Knn:
Model accuracy: 0.9035596395753412
Evaluation of AdaBoost with Knn:
Model accuracy: 0.8963333035953251

可见集成学习提高了基分类器的准确率,且不同的基分类器对于bagging和boosting的效果在某些条件下相差不大,且目前来看权重投票方式应该是最好的一种。


基于集成学习的亚马逊用户评论预测
http://example.com/2023/11/07/基于集成学习的亚马逊用户评论预测/
作者
Guoxin
发布于
2023年11月7日
许可协议