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 from sklearn import preprocessing, tree, ensemble, svm, metrics, calibration from sklearn.model_selection import cross_val_score, train_test_splitfrom sklearn.neighbors import KNeighborsClassifierfrom sklearn.metrics import auc, accuracy_scorefrom sklearn.preprocessing import StandardScalerfrom sklearn.feature_extraction import textfrom matplotlib import pyplot as pltfrom itertools import combinationsfrom wordcloud import WordCloud from collections import Counterfrom textblob import TextBlob from tqdm import tqdmimport pandas as pdimport numpy as npimport randomimport 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(" " ))) 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(" " ))) 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, treefrom sklearn.naive_bayes import MultinomialNBfrom sklearn.neighbors import KNeighborsClassifierfrom sklearn.metrics import accuracy_scorefrom 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 metricsimport matplotlib.pyplot as pltfrom sklearn.metrics import accuracy_scoredef averange (results ): return np.average(results,axis=1 )def weight_average (results, weights ): return np.dot(results, weights) / np.sum (weights)def vote (labels ): return np.average(labels, axis=1 )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 npimport randomimport mathfrom sklearn.metrics import accuracy_scoreimport randomdef 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_indicesdef 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 predictionsdef 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) 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' )
图比较大,只截取部分:
只输出准确率:
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 def evaluate_model (y_test, y_pred ): accuracy = accuracy_score(y_test, y_pred) print (f"Model accuracy: {accuracy} " ) 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 ) y_pred_bagging = np.average(results_bagging, axis=0 ) y_pred_bagging = (y_pred_bagging > 0.5 ).astype(int ) print (f"Evaluation of Bagging with {name} :" ) evaluate_model(y_test, y_pred_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 ) y_pred_adaboost = np.average(results_adaboost, axis=0 ) 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的效果在某些条件下相差不大,且目前来看权重投票方式应该是最好的一种。