前言

本文是我对在公司参加的“ChatGPT 生成文本检测器”比赛。数据集为中文作文样本,其中从互联网上采集得到了真实作文,并且 ChatGLM-6B 生成了部分作文。参赛选手的任务是根据文本内容,区分作文的来源。但是,文本不是以内容呈现,而是一堆数字字符串,形如:[0 43 2 66]。可以推测出,每个数字代表一个汉字在语料库中的索引。

文本分类任务的四步:

  • 准备数据集:包括加载数据集和执行基本预处理,然后把数据集分为训练集和验证集。
  • 特征工程:将原始数据集被转换为用于训练机器学习模型的平坦特征(flat features)。
  • 模型训练
  • 进一步提高分类器性能

下面按照前面三步(省略第四步)介绍我的做法。

准备数据集

这一步的主要工作是读取原始文件,并划分训练集和验证集,用到的库 pandas 和 sklearn。

import pandas as pd
from sklearn import model_selection, preprocessing, naive_bayes, metrics, linear_model
from sklearn.feature_extraction.text import TfidfVectorizer, CountVectorizer

df = pd.read_csv('train.csv')

# print(df['content'])

#将数据集分为训练集和验证集
train_x, valid_x, train_y, valid_y = model_selection.train_test_split(df['content'], df['label'])

# label编码为目标变量
encoder = preprocessing.LabelEncoder()
train_y = encoder.fit_transform(train_y)
valid_y = encoder.fit_transform(valid_y)

特征工程

计数向量是数据集的矩阵表示,其中每行代表来自语料库的文档,每列表示来自语料库的术语,并且每个单元格表示特定文档中特定术语的频率计数:

#创建一个向量计数器对象
count_vect = CountVectorizer(analyzer='word', token_pattern=r'\w{1,}')
count_vect.fit(df['content'])

#使用向量计数器对象转换训练集和验证集
xtrain_count =  count_vect.transform(train_x)
xvalid_count =  count_vect.transform(valid_x)

模型训练

线性分类器用于训练。

## 训练函数
def train_model(classifier, feature_vector_train, label, feature_vector_valid, is_neural_net=False):
    # fit the training dataset on the classifier
    classifier.fit(feature_vector_train, label)


    # predict the labels on validation dataset
    predictions = classifier.predict(feature_vector_valid)


    if is_neural_net:
        predictions = predictions.argmax(axis=-1)


    return classifier, metrics.accuracy_score(predictions, valid_y)

if __name__ == "__main__":
    # classifier, accuracy = train_model(naive_bayes.MultinomialNB(), xtrain_count, train_y, xvalid_count)
    # print("NB, Count Vectors: ", accuracy)

    classifier, accuracy = train_model(linear_model.LogisticRegression(), xtrain_count, train_y, xvalid_count)
    print("LR, Count Vectors: ", accuracy)

    # csv out

    testDF = pd.read_csv('test.csv')
    test_x = testDF['content']

    #使用向量计数器对象转换测试集
    xtest_count =  count_vect.transform(test_x)

    predictions = classifier.predict(xtest_count)

    submissionDF = pd.DataFrame()
    submissionDF['name'] = testDF['name']
    submissionDF['label'] = predictions

    submissionDF.to_csv('LR_submission.csv', index=False)

总结

这次是我在 NLP 领域的一次小试牛刀,主要用到了 sklearn 框架,使得代码的编写变得简单。在不需要数据清洗等步骤的情况下,通过简单的计数向量和逻辑回归就使得最终的测试集结果达到了 0.99 以上,说明本次比赛的数据还是比较简单的。

另外,这次比赛也改变了我对于机器学习的认知。一开始我认为这是一个有难度的任务,想去 huggingface 上找一些文本分类的模型,践行“拿来主义”。但是怎么也找不到合适的。后来,在网上搜索文本分类的解决方案,看到一个相似的任务,且用了 sklearn,很短的代码就能达到不错的效果。也许,传统机器学习的能力比我想象的强很多。

更新

过了一段时间后,在接触更多 sklearn 知识后,我用 pipeline 来以更简单的方式实现:

import pandas as pd
from sklearn import model_selection, preprocessing, naive_bayes, metrics, linear_model
from sklearn.feature_extraction.text import TfidfVectorizer, CountVectorizer
from sklearn.decomposition import PCA, TruncatedSVD
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier
from sklearn.svm import SVC
from sklearn.ensemble import VotingClassifier
from sklearn.neighbors import KNeighborsClassifier  # 用于分类任务
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
from sklearn.metrics import confusion_matrix, precision_score, recall_score, f1_score
from sklearn.metrics import roc_curve, roc_auc_score
import matplotlib.pyplot as plt

df = pd.read_csv('train.csv')
X = df['content'].values
y = df['label'].values
le = preprocessing.LabelEncoder()
y = le.fit_transform(y)


#将数据集分为训练集和验证集
train_x, valid_x, train_y, valid_y = model_selection.train_test_split(X, y, random_state=43)

pipe_lr = Pipeline([('cv', CountVectorizer(analyzer='word', token_pattern=r'\w{1,}')),
                    ('scl', StandardScaler(with_mean=False)),
                    ('clf', linear_model.LogisticRegression(max_iter=50000))])

pipe_lr.fit(train_x, train_y)
print('Test Accuracy: %.4f' %pipe_lr.score(valid_x, valid_y))

## more metrics
valid_y_pred = pipe_lr.predict(valid_x)
confmat = confusion_matrix(y_true=valid_y, y_pred=valid_y_pred)
print(confmat)

print('Precision: %.3f' % precision_score(y_true=valid_y, y_pred=valid_y_pred))
print('Recall: %.3f' % recall_score(y_true=valid_y, y_pred=valid_y_pred))
print('F1: %.3f' % f1_score(y_true=valid_y, y_pred=valid_y_pred))

## draw roc
y_probs = pipe_lr.predict_proba(valid_x)
fpr, tpr, thresholds = roc_curve(valid_y, y_probs[:, 1], pos_label=1)
roc_auc = roc_auc_score(valid_y, y_probs[:, 1])
print('auc: %.4f' % roc_auc)
# Plot the ROC curve
plt.figure(figsize=(8, 6))
plt.plot(fpr, tpr, color='darkorange', lw=2, label='ROC curve (area = %.4f)' % roc_auc)
plt.plot([0, 1], [0, 1], color='navy', lw=2, linestyle='--')
plt.xlim([0.0, 1.0])
plt.ylim([0.0, 1.05])
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate')
plt.title('Receiver Operating Characteristic')
plt.legend(loc='lower right')
plt.show()


## testset result

testDF = pd.read_csv('test.csv')
test_x = testDF['content'].values

pipe_lr.fit(X, y)
predictions = pipe_lr.predict(test_x)

submissionDF = pd.DataFrame()
submissionDF['name'] = testDF['name']
submissionDF['label'] = predictions

submissionDF.to_csv('LR_submission.csv', index=False)

在新版本中,加入了不少指标用于衡量模型性能。更重要的是,之前在输出测试结果时,用的是训练集上训练的模型,而不是整个数据集上重新训练一次。在重新训练后,发现指标从 0.9901 来到了 0.9912,提示非常显著。这提示了我以后一定要注意这个问题。