首先我们使用几个经典的机器学习模型,使用机器学习模型作文本分类,主要分为两个阶段:
1)特征表示:将输入文本表示为特征向量,一般采用TF-IDF提取特征。可能会融入一些降维方法,降低特征向量的维度。
2)分类器:将提取的特征向量输入分类器进行分类。
单模型融合,就是对每个模型采用10折交叉验证,每个模型都会得到一个在训练集上的预测结果(train_samples,classes),和10个在测试集上的预测结果,直接取平均(test_samples,classes).
我们采用的机器学习模型主要包括:逻辑回归,朴素贝叶斯,支持向量机,SVD降维+支持向量机,SVD降维+LightGBM。其中LightGBM是一个主流的集成学习算法,和XGBoost齐名,是竞赛和刷结果的神器。后续有时间我会专门整理一个集成学习算法的专栏,如果不理解,可以简单把它看作是一个随机森林算法。
目录
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')