0%

【PHP-ML】解决NLP中文文本分类问题

php-ml

php-ml是波兰开发者Arkadiusz Kondas的作品,前段时间刚翻译了他关于PHP在机器学习领域的看法「3 Reasons Why PHP is Not Yet Perfect for Machine Learning」。php-ml的出现丰富了PHP生态,让PHP开发者也能写机器学习应用,这篇文章讲一讲文本分类问题在php-ml中是怎么解决的。

本文的实践例子已经放在Github:php-text-classification

数据集

本文采用头条新闻数据集

  • 数据规模:共382688条,分布于15个分类中。
  • 采集时间:2018年05月

分类code与名称:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
100 民生 故事 news_story
101 文化 文化 news_culture
102 娱乐 娱乐 news_entertainment
103 体育 体育 news_sports
104 财经 财经 news_finance
106 房产 房产 news_house
107 汽车 汽车 news_car
108 教育 教育 news_edu
109 科技 科技 news_tech
110 军事 军事 news_military
112 旅游 旅游 news_travel
113 国际 国际 news_world
114 证券 股票 stock
115 农业 三农 news_agriculture
116 电竞 游戏 news_game

Classification

php-ml有多种文本分类模型

  • SVM(依赖libsvm库)
  • KNN
  • NavieBayes
  • MLPClassifier

数据预处理

php-ml对分类模型保持高度一致的接口,定义了统一的输入和输出。下面的代码贯穿全文,只需要选取合适的模型,把样本数据集Samples和类别对象Labels灌入训练API,即可进行训练。

1
2
3
4
5
$samples = [[5, 1, 1], [1, 5, 1], [1, 1, 5]];
$labels = ['a', 'b', 'c'];

$classifier = new ClassificationModel();
$classifier->train($samples, $labels);

在这里,ClassificationModel可以是SVM,也可以KNN或者其他分类模型。区别在于,各模型存在算法、核函数或超参差异。当进行深度拟合数据、提高模型分类效果时,可进行调整。然而,php-ml没有交叉验证或者网格搜索等方法,需要自己设计程序进行调参。

回到主题,我们需要把不同类别的中文文本数据进行训练,中文文本的形式化处理是关键。

先看原始数据形式,截取第一条数据出来

1
6551700932705387022_!_101_!_news_culture_!_京城最值得你来场文化之旅的博物馆_!_保利集团,马未都,中国科学技术馆,博物馆,新中国

数据以_!_分隔,对本文来说,需要获取类别ID、类别名称和句子,如下:

  • category_id: 101
  • category_name: news_culture
  • text: 京城最值得你来场文化之旅的博物馆

读取文件,进行分词和停用词过滤后,得到如下的数据结构

1
2
3
4
5
6
7
8
[
[
category_id: 101,
category_name: news_culture,
text: 京城最值得你来场文化之旅的博物馆
]
// 此处省略382687行
]

特征提取

分词

分词方式有多种:

  1. 直接调用php jieba分词库fukuball/jieba-php
  2. 直接调用php jieba分词扩展jonnywang/phpjieba
  3. 使用swoole+jieba分词,提供一个http服务。参考之前写的这篇文章Swoole加速结巴分词
  4. 使用python+aiohttp+jieba

过滤

  1. 停用词过滤,可以使用goto456/stopwords
  2. 对于英文文本,单个字符会被过滤。对于中文同样适用,单个词没有太大意义

语料库(词袋)

进行分词和过滤后,将获得特征。

1
['京城', '值得',  '来场' , '文化', '之旅', '博物馆']

将特征以空格分隔,合并成句子

1
京城 值得 来场 文化 之旅 博物馆

使用WhitespaceTokenizer进行文本样本集的向量化

1
2
3
$vectorizer = new TokenCountVectorizer(new WhitespaceTokenizer());
$vectorizer->fit($trainX);
$vectorizer->transform($trainX);

词袋

1
2
$vectorizer->getVocabulary();
// ['之旅', '京城', '值得', '博物馆', '文化', '来场']

语料库

1
[[1 1 1 1 1 1]]

TF-IDF

tf-idf是一种统计方法,用以评估一字词对于一个文件集或一个语料库中的其中一份文件的重要程度。字词的重要性随着它在文件中出现的次数成正比增加,但同时会随着它在语料库中出现的频率成反比下降。

1
2
$transformer = new TfIdfTransformer($trainX);
$transformer->transform($trainX);

在这里,由于只有一句话,且这句话里面所有的字词都只有一个,所以权重是一样的。

1
[[0.40824829 0.40824829 0.40824829 0.40824829 0.40824829 0.40824829]]

训练

进行特征处理后的特征集类别对象传入模型的构造方法,即可进行训练。注意,这里可以对比未使用tfidf使用tfidf前后的效果。

这里使用朴素贝叶斯举例:

1
2
$model = new NaiveBayes($trainX, $trainY),
$classifier = $model->train();

预测

准备新的样本文本,并进行分词、过滤和特征提取后,传入即可

1
$classifier->predict($testSample);

持久化

如果想要保存训练结果,避免多次训练,可以将模型持久化到本地。需要使用时,将模型重新导入内存即可使用。

1
2
3
4
5
6
7
8
// 模型导出
$filepath = '/path/to/store/the/model';
$modelManager = new ModelManager();
$modelManager->saveToFile($classifier, $filepath);

// 模型导入
$restoredClassifier = $modelManager->restoreFromFile($filepath);
$restoredClassifier->predict([3, 2]);

评估指标(Metric)

仓库代码中,我将样本通过StratifiedRandomSplit划分为训练集测试集,用于评估模型效果。

1
$split = new StratifiedRandomSplit($dataset, 0.2);

对测试集进行预测

1
2
3
4
5
6
7
8
$predictY = [];
foreach ($testX as $test) {
$testSampleText = [$test];

$vectorizer->transform($testSampleText);

$predictY[] = current($classifier->predict($testSampleText));
}

Score

通过Accuracy得到预测结果的正确率

1
Accuracy::score($testY, $predictY)

Confusion Matrix

通过ConfusionMatrix得到预测结果的错误情况

1
2
3
$text = new Text($textFile);
$categoryIds = $text->getCategoryIds();
ConfusionMatrix::compute($testY, $predictY, $categoryIds);

Classification Report

通过ClassificationReport得到整体分类报告(score、f1、recall)

1
2
$report = new ClassificationReport($testY, $predictY);
$report->getAverage();

Pipeline

还可以使用Pipeline来管线化工作流,有两个好处:

  1. 代码少很多,阅读更清晰
  2. 内存占用更低
1
2
3
4
5
6
7
8
9
$transformers = [
new TokenCountVectorizer(new WhitespaceTokenizer()),
new TfIdfTransformer()
];

$pipeline = new Pipeline($transformers, new NaiveBayes());
$pipeline->train($trainX, $trainY);
$predictY = $pipeline->predict($testX);
Accuracy::score($testY, $predictY);
不为赚钱,只为交个朋友!干杯~

Welcome to my other publishing channels