达观杯文本分类比赛 | (2) 单模型融合(机器学习模型)

首先我们使用几个经典的机器学习模型,使用机器学习模型作文本分类,主要分为两个阶段:

1)特征表示:将输入文本表示为特征向量,一般采用TF-IDF提取特征。可能会融入一些降维方法,降低特征向量的维度。

2)分类器:将提取的特征向量输入分类器进行分类。

单模型融合,就是对每个模型采用10折交叉验证,每个模型都会得到一个在训练集上的预测结果(train_samples,classes),和10个在测试集上的预测结果,直接取平均(test_samples,classes).

我们采用的机器学习模型主要包括:逻辑回归,朴素贝叶斯,支持向量机,SVD降维+支持向量机,SVD降维+LightGBM。其中LightGBM是一个主流的集成学习算法,和XGBoost齐名,是竞赛和刷结果的神器。后续有时间我会专门整理一个集成学习算法的专栏,如果不理解,可以简单把它看作是一个随机森林算法。

目录

1. 数据预处理

2. 逻辑回归(LR)

3. 支持向量机(SVM)

4. SVD+SVM

5. SVD+LightGBM

6. 朴素贝叶斯


1. 数据预处理

对于机器学习模型,我们使用基于词的表示(word-level,文本以词为间隔)。

def prepare_word_data(): #准备数据 word-level
    #如果有处理好的.pkl文件 直接读取 节省时间
    if os.path.exists('../../data/train_word.pkl'):
        with open('../../data/train_word.pkl', 'rb') as f:
            train_x = pkl.load(f)
        with open('../../data/label.pkl', 'rb') as f:
            train_y = pkl.load(f)
        with open('../../data/test_word.pkl', 'rb') as f:
            test_x = pkl.load(f)
    else:
        #否则 读取官方提供的原始.csv文件
        train_df = pd.read_csv('../../input/train_set.csv') #训练集
        test_df = pd.read_csv('../../input/test_set.csv') #测试集
        train_word = train_df['word_seg'].values #一维数组 数组中每个元素为字符串(表示一段文本,内部用空格以词为间隔分开并做了加密处理,每个数字代表一个词的编码)eg '123 456 789 ...'
        train_y = train_df['class'].values.astype(np.int) - 1 #类别 一维数组 从0开始
        test_word = test_df['word_seg'].values

        #提取TF-IDF特征
        vectorizer = TfidfVectorizer(ngram_range=(1, 2), max_df=0.9, min_df=3, sublinear_tf=True)
        train_x = vectorizer.fit_transform(train_word) #基于训练集进行统计(训练),计算TF-IDF。把每个字符串/文本转换为向量,向量维度为词典大小,向量(稀疏)中的值为该词对应的tf-idf值
        test_x = vectorizer.transform(test_word) #直接把相关计算结果运用在测试集上
        # 将提取好的TF-IDF特征 存储为.pkl文件,后续模型直接读取,不用再次处理
        with open('../../data/train_word.pkl', 'wb') as f:
            pkl.dump(train_x, f)
        print('Training word data saved...')
        with open('../../data/label.pkl', 'wb') as f:
            pkl.dump(train_y, f)
        print('Label saved...')
        with open('../../data/test_word.pkl', 'wb') as f:
            pkl.dump(test_x, f)
        print('Test word data saved...')
    return train_x, train_y, test_x
  • TF-IDF特征
TfidfVectorizer(stop_words=stpwrdlst,ngram_range=(1, 2), max_df=0.9, min_df=3, sublinear_tf=True)

关于参数:

1)stop_words:

    传入停用词,以后获得vocabulary_的时候,就会根据文本信息去掉停用词得到.(降低词典的大小,即特征向量的维数)

2)vocabulary:

    词典索引(构造好的词典)

3)sublinear_tf:

    计算tf值采用亚线性策略。比如,我们以前计算tf是词频,现在会用1+log(分子/(1+分母))的方式解决,默认是开启的

4)norm:

    归一化,我们计算TF-IDF的时候,使用TF*IDF,TF可以是归一化的,也可以是没有归一化的,一般都是采用归一化的方法,默认开启

5)max_df:

    有些词,它们的文档频率太高了(一个词如果每篇文档都出现,那就没有必要用它来区分文本类别了),所以,我们可以设定一个阈值,比如float类型0.5(取值范围[0.0 , 1.0]),表示这个词如果在整个数据集中超过50%的文本都出现了,那么我们也把它列为临时停用词。当然你也可以设定为int型,例如max_df=10,表示这个词如果在整个数据集中超过10的文本都出现了,那么我们也把它列为临时停用词

6)min_df:

    与max_df相反,虽然文档频率越低,似乎越能区分文本,可是如果太低,例如10000篇文本中只有一篇文本出现过这个词,仅仅因为这一篇文本,就增加了词向量空间的维度,不太划算。一般为整数,当一个词只在少于min_df篇文本中出现时,则把它列为临时停用词。

7)ngram_range

 有时候我们觉得单个的词语作为特征还不足够,能够加入一些词组更好,就可以设置这个参数,ngram_range=(1,2)允许词表使用1个词语(uni-gram),或者2个词语(bi-gram)的组合。

当然,max_df和min_df在给定vocabulary参数时,就失效了.

TF-IDF的原理,可以查看我的另一篇博客:https://blog.csdn.net/sdu_hao/article/details/86767832

TF-IDF默认对输入文本按空格作为切分,官方提供的数据已经按空格切分好了,所以不用我们自己处理。所以,对于一般的英文文本可以直接输入(或经过一些预处理手段,如去停止词、特殊符号等);对于一般的中文文本需要先分好词再以空格连接作为输入(或经过一些预处理手段,如去停止词、特殊符号等)。最好加一些预处理,这样可以降低词典的维度,而TF-IDF计算的文本特征向量维度等于词典维度,降低特征向量的维度分类器的效果会更好。

2. 逻辑回归(LR)

if __name__ == '__main__':

    train_data, train_label, test_x = prepare_word_data() #获取TF-IDF提取的特征和标签

    num_classes = len(set(train_label)) #类别数

    num_fold = 10  #10折
    fold_len = train_data.shape[0] // num_fold #每一折的数据量

    skf_indices = []
    skf = StratifiedKFold(n_splits=num_fold, shuffle=True, random_state=2018) #将训练集分为10折
    for i, (train_idx, valid_idx) in enumerate(skf.split(np.ones(train_data.shape[0]), train_label)):
        skf_indices.extend(valid_idx.tolist())

    train_pred = np.zeros((train_data.shape[0], num_classes)) #在训练集上的预测结果 (train_samples,classes)
    test_pred = np.zeros((test_x.shape[0], num_classes))#在测试集上的预测结果 (test_samples,classes)
    clf = LogisticRegression(C=4.0)    #声明逻辑回归分类器 C为正则化强度的倒数 C越大正则化强度越低 容易发生欠拟合。其他使用默认参数,可以基于数据的实际类别数,自动进行多分类。

    for fold in range(num_fold):

        print(f'Processing fold {fold}...')

        fold_start = fold * fold_len
        fold_end = (fold + 1) * fold_len
        if fold == num_fold - 1:
            fold_end = train_data.shape[0]
        #训练部分索引 9折
        train_indices = skf_indices[:fold_start] + skf_indices[fold_end:]
        # 验证部分索引 1折
        test_indices = skf_indices[fold_start:fold_end]

        #训练部分数据 9折
        train_x = train_data[train_indices]
        train_y = train_label[train_indices]
        #验证部分数据 1折
        cv_test_x = train_data[test_indices]

        clf.fit(train_x, train_y) #训练

        pred = clf.predict_proba(cv_test_x) #在验证部分数据上 进行验证
        train_pred[test_indices] = pred   #把预测结果 赋给验证部分对应的位置  循环结束将会得到整个训练集上的预测结果
        pred = clf.predict_proba(test_x)  #得到 当前训练的模型在测试集上的预测结果
        test_pred += pred / num_fold  #对每个模型在测试集上的预测结果直接取平均(10折将会有10个结果)

    y_pred = np.argmax(train_pred, axis=1) #对训练集上的预测结果按行取最大值 得到预测的标签
    score = f1_score(train_label, y_pred, average='macro') #和训练集对应的真实标签 计算macro-f1_score

    #保存逻辑回归模型在训练集和测试集上的预测结果
    np.save(f'../../oof_pred/lr_word_train_{score:.4f}', train_pred)
    np.save(f'../../oof_pred/lr_word_test_{score:.4f}', test_pred)

3. 支持向量机(SVM)

if __name__ == '__main__':

    train_data, train_label, test_x = prepare_word_data()#获取TF-IDF提取的特征和标签

    num_classes = len(set(train_label)) #类别数

    num_fold = 10 #10折交叉验证
    fold_len = train_data.shape[0] // num_fold #每一折的数据量

    skf_indices = []
    skf = StratifiedKFold(n_splits=num_fold, shuffle=True, random_state=2018)#将训练集分为10折
    for i, (train_idx, valid_idx) in enumerate(skf.split(np.ones(train_data.shape[0]), train_label)):
        skf_indices.extend(valid_idx.tolist())
    #skf_indices为打乱后的样本索引

    train_pred = np.zeros((train_data.shape[0], num_classes))
    test_pred = np.zeros((test_x.shape[0], num_classes))
    clf = LinearSVC() #声明SVM分类器 全部采用默认超参数 可以基于数据的实际类别数,自动进行多分类。

    for fold in range(num_fold):

        print(f'Processing fold {fold}...')

        fold_start = fold * fold_len
        fold_end = (fold + 1) * fold_len
        if fold == num_fold - 1: #最后一折
            fold_end = train_data.shape[0]
        # 训练部分索引 9折
        train_indices = skf_indices[:fold_start] + skf_indices[fold_end:] #num_fold-1折训练 剩余一折验证
        # 验证部分索引 1折
        test_indices = skf_indices[fold_start:fold_end]

        # 训练部分数据 9折
        train_x = train_data[train_indices] 
        train_y = train_label[train_indices]
        # 验证部分数据 1折
        cv_test_x = train_data[test_indices] 

        clf.fit(train_x, train_y) #训练

        pred = clf.decision_function(cv_test_x) #在验证部分数据上 进行验证
        train_pred[test_indices] = softmax(pred) #把预测结果先通过softmax转换为概率分布(归一化) 赋给验证部分对应的位置  循环结束将会得到整个训练集上的预测结果
        pred = clf.decision_function(test_x) #得到 当前训练的模型在测试集上的预测结果
        test_pred += softmax(pred) / num_fold#对每个模型在测试集上的预测结果先通过softmax转换为概率分布,再直接取平均(10折将会有10个结果)

    y_pred = np.argmax(train_pred, axis=1) #得到训练集样本的预测类别
    score = f1_score(train_label, y_pred, average='macro') #与真实类别计算macro f1-score

    # 保存SVM模型在训练集和测试集上的预测结果
    np.save(f'../../oof_pred/linearsvc_word_train_{score:.4f}', train_pred)
    np.save(f'../../oof_pred/linearsvc_word_test_{score:.4f}', test_pred)

4. SVD+SVM

  • 降维预处理
if __name__ == '__main__':
    #读取原始的.csv数据
    train_df = pd.read_csv('../../input/train_set.csv')
    test_df = pd.read_csv('../../input/test_set.csv')

    #article 以字为间隔;word_seg 以词为间隔
    train_char = train_df['article'].values.tolist()
    train_word = train_df['word_seg'].values.tolist()
    train_label = train_df['class'].values - 1
    test_char = test_df['article'].values.tolist()
    test_word = test_df['word_seg'].values.tolist()

    #提取TF-IDF特征 word-level
    word_vectorizer = TfidfVectorizer(ngram_range=(1, 2), min_df=3, max_df=0.9, sublinear_tf=True)
    train_word_feat = word_vectorizer.fit_transform(train_word)
    test_word_feat = word_vectorizer.transform(test_word)

    #使用svd进行降维
    #n_components主成分数 降至的维度  下面是降到100维
    svd = TruncatedSVD(n_components=100, n_iter=20, random_state=SEED) #降维
    train_svd_feat = svd.fit_transform(train_word_feat) #训练并降维
    print('Training set transformed..')
    
    #对训练集和测试集用TF-IDF提取的特征向量降到100维 对保存降维结果
    with open('../../data/train_svd_feat.pkl', 'wb') as f:
        pkl.dump(train_svd_feat, f)

    test_svd_feat = svd.transform(test_word_feat)#降维
    print('Test set transformed..')
    with open('../../data/test_svd_feat.pkl', 'wb') as f:
        pkl.dump(test_svd_feat, f)
  • 基于降维结果进行分类
if __name__ == '__main__':
    
    #读取降维后的特征向量
    with open('../../data/train_svd_feat.pkl', 'rb') as f:
        train_data = pkl.load(f)
    with open('../../data/test_svd_feat.pkl', 'rb') as f:
        test_x = pkl.load(f)
    train_label = np.load('../../data/label.npy')

    num_classes = len(set(train_label))#类别数

    num_fold = 10#10折交叉验证
    fold_len = train_data.shape[0] // num_fold#每一折的数据量

    skf_indices = []
    skf = StratifiedKFold(n_splits=num_fold, shuffle=True, random_state=2018)#将训练集分为10折
    for i, (train_idx, valid_idx) in enumerate(skf.split(np.ones(train_data.shape[0]), train_label)):
        skf_indices.extend(valid_idx.tolist())
    # skf_indices为打乱后的样本索引
    
    
    train_pred = np.zeros((train_data.shape[0], num_classes)) #在训练集上的预测结果 (train_samples,classes)
    test_pred = np.zeros((test_x.shape[0], num_classes))#在测试集上的预测结果 (test_samples,classes)
    clf = LinearSVC(C=10) #声明SVM分类器 C为正则化强度的倒数 C越大正则化强度越低 容易发生欠拟合。其他使用默认超参数,可以基于数据的实际类别数,自动进行多分类。
    
    for fold in range(num_fold):

        print(f'Processing fold {fold}...')

        fold_start = fold * fold_len
        fold_end = (fold + 1) * fold_len
        if fold == num_fold - 1: #最后一折
            fold_end = train_data.shape[0]
        # 训练部分索引 9折
        train_indices = skf_indices[:fold_start] + skf_indices[fold_end:] #num_fold-1折训练 剩余一折验证
        # 验证部分索引 1折
        test_indices = skf_indices[fold_start:fold_end]

        # 训练部分数据 9折
        train_x = train_data[train_indices]
        train_y = train_label[train_indices]
        # 验证部分数据 1折
        cv_test_x = train_data[test_indices]

        clf.fit(train_x, train_y) #训练

        pred = clf.decision_function(cv_test_x) #在验证部分数据上 进行验证
        train_pred[test_indices] = softmax(pred) #把预测结果先通过softmax转换为概率分布(归一化) 赋给验证部分对应的位置  循环结束将会得到整个训练集上的预测结果
        pred = clf.decision_function(test_x) #得到 当前训练的模型在测试集上的预测结果
        test_pred += softmax(pred) / num_fold#对每个模型在测试集上的预测结果先通过softmax转换为概率分布,再直接取平均(10折将会有10个结果)

    y_pred = np.argmax(train_pred, axis=1) #得到训练集样本的预测类别
    score = f1_score(train_label, y_pred, average='macro') #与真实类别计算macro f1-score

    # 保存SVD+SVM模型在训练集和测试集上的预测结果
    np.save(f'../../oof_pred/linearsvc_svd_train_{score:.4f}', train_pred)
    np.save(f'../../oof_pred/linearsvc_svd_test_{score:.4f}', test_pred)

5. SVD+LightGBM

if __name__ == '__main__':

    # 读取降维后的特征向量
    with open('../../data/train_svd_feat.pkl', 'rb') as f:
        train_data = pkl.load(f)
    with open('../../data/test_svd_feat.pkl', 'rb') as f:
        test_data = pkl.load(f)
    train_label = np.load('../../data/label.npy')

    num_classes = len(set(train_label))  # 类别数

    num_fold = 10  # 10折交叉验证
    fold_len = train_data.shape[0] // num_fold  # 每一折的数据量

    skf_indices = []
    skf = StratifiedKFold(n_splits=num_fold, shuffle=True, random_state=2018)  # 将训练集分为10折
    for i, (train_idx, valid_idx) in enumerate(skf.split(np.ones(train_data.shape[0]), train_label)):
        skf_indices.extend(valid_idx.tolist())
    # skf_indices为打乱后的样本索引

    train_pred = np.zeros((train_data.shape[0], num_classes))  # 在训练集上的预测结果 (train_samples,classes)
    test_pred = np.zeros((test_data.shape[0], num_classes))  # 在测试集上的预测结果 (test_samples,classes)

    for fold in range(num_fold):

        print(f'Processing fold {fold}...')

        fold_start = fold * fold_len
        fold_end = (fold + 1) * fold_len
        if fold == num_fold - 1:
            fold_end = len(skf_indices)

        train_indices = skf_indices[:fold_start] + skf_indices[fold_end:]
        test_indices = skf_indices[fold_start:fold_end]

        train_x, test_x = train_data[train_indices], train_data[test_indices]
        train_y = train_label[train_indices]

        clf = LGBMClassifier(n_estimators=1000) #1000棵树 其他使用默认超参数
        clf.fit(train_x, train_y) #训练
        
        pred = clf.predict_proba(test_x) #在验证部分数据上 进行验证
        train_pred[test_indices] = pred #把预测结果 赋给验证部分对应的位置  循环结束将会得到整个训练集上的预测结果
        pred = clf.predict_proba(test_data)#得到 当前训练的模型在测试集上的预测结果
        test_pred += pred / num_fold#对每个模型在测试集上的预测结果直接取平均(10折将会有10个结果)

    y_pred = np.argmax(train_pred, axis=1)#对训练集上的预测结果按行取最大值 得到预测的标签
    score = f1_score(train_label, y_pred, average='macro')#和训练集对应的真实标签 计算macro-f1_score
    print(score)

    # 保存SVD+LightGBM模型在训练集和测试集上的预测结果
    np.save(f'../../oof_pred/_lgbm_svd_train_{score:.4f}', train_pred)
    np.save(f'../../oof_pred/_lgbm_svd_test_{score:.4f}', test_pred)

可以把LightGBM暂时理解为随机森林,LightGBM的安装可以看我的另一篇博客:https://blog.csdn.net/sdu_hao/article/details/103737330

 

6. 朴素贝叶斯

上述机器学习模型有很多部分的代码都是重复的,所以我们可以先对其进行封装,这样就可以很方便地扩展新的模型。

def generate_oof_pred(clf, model_name, granularity='word', num_fold=10, seed=2018, silent=False):
    if granularity == 'word':
        train_data, train_label, test_x = prepare_word_data()
    else:
        train_data, train_label, test_x = prepare_char_data()
    
    
    num_classes = len(set(train_label))
    fold_len = train_data.shape[0] // num_fold
    skf_indices = []
    skf = StratifiedKFold(n_splits=num_fold, shuffle=True, random_state=seed)
    for i, (train_idx, valid_idx) in enumerate(skf.split(np.ones(train_data.shape[0]), train_label)):
        skf_indices.extend(valid_idx.tolist())
        
    train_pred = np.zeros((train_data.shape[0], num_classes))
    test_pred = np.zeros((test_x.shape[0], num_classes))
    
    for fold in range(num_fold):
        if not silent:
            print(f'Processing fold {fold}...')
        fold_start = fold * fold_len
        fold_end = (fold + 1) * fold_len
        if fold == num_fold - 1:
            fold_end = train_data.shape[0]
            
        train_indices = skf_indices[:fold_start] + skf_indices[fold_end:]
        test_indices = skf_indices[fold_start:fold_end]
        
        train_x = train_data[train_indices]
        train_y = train_label[train_indices]
        cv_test_x = train_data[test_indices]
        
        clf.fit(train_x, train_y)
        
        pred = clf.predict_proba(cv_test_x)
        train_pred[test_indices] = pred
        pred = clf.predict_proba(test_x)
        test_pred += pred / num_fold
        
    y_pred = np.argmax(train_pred, axis=1)
    score = f1_score(train_label, y_pred, average='macro')
    
    if not silent:
        np.save(f'../../oof_pred/{model_name}_{granularity}_train_{score:.4f}', train_pred)
        np.save(f'../../oof_pred/{model_name}_{granularity}_test_{score:.4f}', test_pred)
    return score
if __name__ == '__main__':
    #声明离散型朴素贝叶斯分类器 基于贝叶斯定理
    clf = MultinomialNB(alpha=0.0009)#alpha其实就是添加拉普拉斯平滑,即为贝叶斯定理中的λ ,如果这个参数设置为0,就是不添加平滑;
    #使用10折交叉验证 得到在训练集和测试集上的预测结果   基于实际数据的类别情况,自动进行多分类
    generate_oof_pred(clf, 'multinomialNB')
发布了365 篇原创文章 · 获赞 712 · 访问量 13万+

猜你喜欢

转载自blog.csdn.net/sdu_hao/article/details/104086326