<jupyter_start><jupyter_text>简介在机器学习训练完模型之后我们要考虑模型的效率问题，在这篇文章中，主要介绍针对模型融合提升模型效率的实践内容，即对多个baseline按一定的方法进行“融合”以期达到性能的提升。常用的提升模型效率分析手段有：1. 特征工程- 主要包括数据清洗、单个多个特征的选择、处理（归一化、离散化、降维）等内容2. 参数调整- 研究模型学习曲线，判断模型是否过拟合或者欠拟合，并做出相应的调整；- 对于模型权重参数进行分析，对于权重绝对值高/低的特征，可以对特征进行更细化的工作，也可以进行特征组合；- 进行bad-case分析，对错误的例子分析是否还有什么可以修改挖掘3. 模型融合- 模型融合就是训练多个模型，然后按照一定的方法集成过个模型，应为它容易理解、实现也简单，同时效果也很好，在工业界的很多应用，在天池、kaggle比赛中也经常拿来做模型集成。 参考文献https://zhuanlan.zhihu.com/p/212210886 https://zhuanlan.zhihu.com/p/25836678 https://blog.csdn.net/u014248127/article/details/78993753 https://www.jianshu.com/p/07b05127ac5c<jupyter_code>1 概念简介
所谓模型融合，其实就是字面意思，通过融合多个不同的模型来提升性能。最容易理解的模型融合有针对分类问题的Voting和回归问题的Average。在其基础上有改进或稍微复杂的有Bagging，Boosting，Stacking等。

Voting
用多个模型对样本进行分类，以“投票”的形式，投票最多者为最终的分类。

Average
对不同模型得出的结果取平均或加权平均。

Bagging
先利用多次有放回抽样生成不同的训练集训练出不同的模型，将这些模型的输出结果通过上述两种方法综合得到最终的结果。随机森林就是基于Bagging算法的一个典型例子。

Boosting
一种迭代的方法，每一次训练的时候都更加关心分类错误的样例，给这些分类错误的样例增加更大的权重，下一次迭代的目标就是能够更容易辨别出上一轮分类错误的样例。最终将这些弱分类器进行加权相加。

Stacking
本质上是分层的结构。第一层是k折交叉的训练集，针对于每一折，由除自己以外的其他的训练数据训练出模型，并以此模型对这一折进行预测。按照此流程进行k次之后，我们得到了用不同模型预测训练集标签的结果，按顺序罗列之后作为第二层的训练集。再用k次训练的模型预测测试集标签，将得到的结果除以k取平均，作为第二层的测试集。随后，我们再用另一个模型去训练第二层的训练集并预测第二层的测试集，获取对原始的测试集的预测结果进行评估。


在本次实践中，主要应用的是Voting和Stacking两种模型融合的方法，也将在下文中分别展开。
# Voting实现

在此之前，我们先需要有几个预先存好的模型。
最好是奇数个，这样不会出现特别多的平票的情况。我使用了5个模型进行voting，分别是TextCNN，以及5个和10个epochs分别训练出来的Word2Vec模型下，各自的SVM和神经网络分类器。
#模型融合
import joblib
import h5py
from tensorflow.keras.models import load_model

#导入五个提前保存好的模型
cnnmodel = load_model('textcnn.h5')
svm1model = joblib.load("download_w2v_svm.m")
svm2model = joblib.load("download_w2v_10epochs_svm.m")
nn1model = joblib.load("download_w2v_nn.m")
nn2model = joblib.load("download_w2v_10epochs_nn.m")

#由于CNN模型和其他分类器模型所产生的标签格式是不一致的
#因此需要应用不同的测试集特征格式，并将预测结果进行统一
y_cnnpred = cnnmodel.predict(X_testcnn, batch_size=64,
                              verbose=0, steps=None,
                              callbacks=None, max_queue_size=10,
                              workers=1, use_multiprocessing=False)
y_svm1pred = svm1model.predict_proba(X_testclf)
y_svm2pred = svm2model.predict_proba(X_testclf)
y_nn1pred = nn1model.predict_proba(X_testclf)
y_nn2pred = nn2model.predict_proba(X_testclf)

#获取one-hot标签形式
y_cnnpred = np.rint(y_cnnpred)
y_nn1pred = np.rint(y_nn1pred)
y_nn2pred = np.rint(y_nn2pred)
y_svm1pred = np.rint(y_svm1pred)
y_svm2pred = np.rint(y_svm2pred)

#加和投票
y_ensemble = y_cnnpred + y_svm1pred + y_svm2pred + y_nn1pred + y_nn2pred
y_pred_ensemble = y_ensemble.argmax(axis=1)

#Confusion Matrix and report
print(confusion_matrix(y_test, y_pred_ensemble))
print(classification_report(y_test, y_pred_ensemble, digits=4))
最终，该融合模型可以达到了90.77%的准确率和88.57%的macro f1，这在我目前的所有模型中达到最好的效果，也比上一节只是用SVM分类器多个指标提高了1%以上。
# Stacking实现

拿下了目前为止的最优模型表现，我们一鼓作气向Stacking方法前进。
Stacking在原理上其实没有很难理解，实现上也不是特别复杂。当然，我个人对这个方法所能达到的效果还是有些怀疑，虽然是应用交叉验证进行了模型融合，但是双层的训练结构其实也会一定程度上会造成错误的累加。虽然初衷是降低泛化的误差，但是略显复杂的模型结构也容易造成过拟合。简单进行了一些思考和预估之后，开始实践。
def get_stacking(clf,X_train,y_train,X_test,n_folds):
    """
    x_train, y_train, x_test 的值应该为numpy里面的数组类型 numpy.ndarray .
    """
    #初始化
    train_num, test_num = X_train.shape[0], X_test.shape[0]
    second_level_train_set = np.zeros((train_num,))
    second_level_test_set = np.zeros((test_num,))
    test_nfolds_sets = np.zeros((test_num, n_folds))
    #创建k折数据
    skf = StratifiedKFold(n_splits=n_folds)

    for i,(train_index, test_index) in enumerate(skf.split(X_train,y_train)):
        X_tra, y_tra = X_train[train_index], y_train[train_index]
        X_tst, y_tst =  X_train[test_index], y_train[test_index]

        clf.fit(X_tra, y_tra)
        #获得第二层训练集
        second_level_train_set[test_index] = clf.predict(X_tst)
        test_nfolds_sets[:,i] = clf.predict(X_test)
    #获得第二层测试集，取平均
    second_level_test_set[:] = test_nfolds_sets.mean(axis=1)
    return second_level_train_set, second_level_test_set
get_stacking()是针对一个模型进行k折交叉验证。和刚才的Voting方法相同，我们同样导入所需要的的模型。
import joblib

rfmodel = joblib.load("download_w2v_rf.m")
svm1model = joblib.load("download_w2v_svm.m")
svm2model = joblib.load("download_w2v_10epochs_svm.m")
nn1model = joblib.load("download_w2v_nn.m")
nn2model = joblib.load("download_w2v_10epochs_nn.m")

train_sets = []
test_sets = []
for clf in [rfmodel, svm1model, svm2model, nn1model, nn2model]:
    train_set, test_set = get_stacking(clf, X_train, y_train, X_test,n_folds=5)
    train_sets.append(train_set)
    test_sets.append(test_set)

meta_train = np.concatenate([result_set.reshape(-1,1) for result_set in train_sets], axis=1)
meta_test = np.concatenate([y_test_set.reshape(-1,1) for y_test_set in test_sets], axis=1)
这时我们已经获取了第二层的训练集和测试集，接下来我们再用一个新的模型去进行训练和预测。
from sklearn import svm
from sklearn.model_selection import GridSearchCV
from sklearn.metrics import f1_score
#Find the svm(rbf) model with highest f1 score
clfsecond = svm.SVC()
grid_values = {'gamma': [0.001, 0.01, 0.05, 0.1, 1, 10],
               'C':[0.01, 0.1, 1, 10, 100]}

grid_clf = GridSearchCV(clfsecond, param_grid = grid_values,scoring = 'f1_macro')
grid_clf.fit(meta_train, y_train)
y_grid_pred = grid_clf.predict(meta_test)

print('Test set F1: ', f1_score(y_test,y_grid_pred,average='macro'))
print('Grid best parameter (max. f1): ', grid_clf.best_params_)
print('Grid best score (accuracy): ', grid_clf.best_score_)
# Badcase分析

为了更好地发现模型对于某些有共性案例的错误判断，我们往往会对预测错误的badcase进行分析。
#badcase 预测标签和实际标签不一样的案例
test = pd.read_csv('article_features_test.csv')
test['predict'] = pd.Series(y_pred_ensemble.tolist())
badcase = test[test.label != test.predict]
truecase = test[test.label == test.predict]
之前已经发现，模型对历史类别的分类表现较差，因此我们着重去观察历史类的错误分类文档，发现这些文章一部分是文物的出土，可能是因为这部分词的词频过低，因此在Word2Vec模型中就没有得到很好的训练，不巧这些词又在文章中扮演着重要作用，因此造成了判定错误的结论。当然，更多的历史文章都是讲述历史中的一个课题，比如过去某个时段的事件或名人，某种文化的发展进程等等，我们很难单独把历史这个类别割裂出来，这也造成了其准确率较差的表现。

当然，以上仅是我对一些表现的原因分析，至于如何去完善或修正这些badcases，我在Word2Vec模型上的改进尝试没有取得理想的效果，或许添加一些预知的规则会有用，但是其实还是没有明确的思路。如果各位有什么经验或想法的话欢迎多多交流。

5 小结
通过模型融合，我们确实提升了模型的性能，尽管没有特别高的突破，但也是有所收获。至此，我们已经完整的完成了中文文本分类的全过程，包括文件提取，中文分词，词向量表达，模型构建和模型融合。当然，每一个部分都是以实践为主，而实践的方法肯定也会有不同之处。比如，将文本转化为向量会有其他方法，而模型的构建也是多种多样。只是我在学习过程中，发现每一个部分的知识都比较碎片，少有把整个过程整合起来的文章，所以既然自己也已经完成了一次中文文本分类的实践，就把自己所用到的方法略作总结，算是对知识的回顾也希望能为有需要的人提供帮助。如果有不足和错误也欢迎大家在评论区指出，很希望和大家交流心得！<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.10325386597938144, 'fraction_numerical': 0.02657860824742268, 'mean_word_length': 7.908177905308465, 'pattern_counts': {'":': 0, '<': 5, '<?xml version=': 0, '>': 5, 'https://': 4, 'lorem ipsum': 0, 'www.': 1, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 9, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '1', 'dir': 'jupyter-structured-clean-dedup', 'id': '92729', 'license': 'MIT', 'n_tokens_mistral': 3922, 'n_tokens_neox': 4292, 'n_words': 274, 'path': 'src/Texts_Classification/文本分类----模型融合.ipynb', 'repo_name': 'wynshiter/NLP_DEMO', 'size': '6208'}