原理简介
1.训练模型
首先要获得训练数据,训练数据就是一些网页样本,但是原始的网页并不能直接来作为训练集合, 因为原始网页具有复杂无规律的信息,我们需要根据原始网页提取有用的特征,如内容特征,网站特征,url 特征等等。由于网页信息的庞大,过多的特征等于提升了训练样本的维度,这对训练模型来说有很大的负面影响。
信息含量较低的特征, 尽量保留具有区分度的特征集合。良好的特征直接影响到分类的质量,所以特征提取和筛选是非常重要的一步。通过一次训练并不能保证模型的良好性,这有几方面的原因,一来训练数据自身存在偏差, 比如会存在标记错误的现象, 二来训练数据的正负例比例不协调, 导致模型无法学习到好的参数,还有一种情况就是训练数据覆盖的种类太少,对某型特殊页面不能很好分类。对于这些情况,我们需要反复迭代训练数据,通过添加语料,调整正负例比例,完善训练语料覆盖面等等,让训练数据力争达到覆盖全面、无差错的效果。
2. 网页预处理
网页主要是由html语言书写,它与纯文本之间存在很大差别,主要体现在 一下几方面:
(1)网页包含大量的结构化标签,比如<head>,<title>等,它比纯文本更具表现力,有更多的信息能够被利用,比如通过<head>标签我们很容易得到标签中对应的文本就是网页的标题,再比如不同的字体也有相应标签, 通过字体大小我们可以得到对应文本的重要性,一般标题会用大号的字 体,而正文就是普通字体。
(2)网页中存在大量的超链接。超链接将互联网上的网页连成了一张巨大的 网络,网页上的超链接代表着这个网页到另一个网页的路径,通过超链接我们可以获得网页的一些特征,比如索引页就存在大量的超链接。
(3)网页中包含着大量噪音,包括各种广告、导航、注释以及版权申明等一 些和内容无关的东西,在分类之前需要去除这些噪音,否则这些噪音会 影响分类性能。但有时候也可利用这些噪音,比如网页顶部的导航块和 底部的版权块,如果能确定这两个块的位置,那么位于这连个块之间的 我们可以认为是网页的主体部分。
由于网页和纯文本的这些区别,在进行网页分类时我们需要对其进行预处理 工作。
3.中文分词
中文分词指的是将一个汉字序列切分成一个一个单独的词。分词就是将连续的字序列按照一定的规范重新组合成词序列的过程。
IKAnalyzer是一个开源的,基于java语言开发的轻量级的中文分词工具包。从2006年12月推出1.0版开始,IKAnalyzer已经推出 了3个大版本。最初,它是以开源项目Luence为应用主体的,结合词典分词和文法分析算法的中文分词组件。新版本的IKAnalyzer3.0则发展为 面向Java的公用分词组件,独立于Lucene项目,同时提供了对Lucene的默认优化实现。
采用了特有的“正向迭代最细粒度切分算法“,具有60万字/秒的高速处理能力。 采用了多子处理器分析模式,支持:英文字母(IP地址、Email、URL)、数字(日期,常用中文数量词,罗马数字,科学计数法),中文词汇(姓名、地名处理)等分词处理。对中英联合支持不是很好,在这方面的处理比较麻烦,需再做一次查询,同时是支持个人词条的 。优化的词典存储,更小的内存占用。
4.页面特征提取
网页分类的质量很大程度上取决于特征提取的好坏,网页本身具有复杂无规律的很多特征,而我们需要对这些特征进行提取筛选,选取那些具有区分度的特征,特征的选取主要是通过对网页的预处理提取出网页的文本信息和结构信息,然后利用一些成熟的特征筛选方法进行筛选,包括特征频率(TF)、文档频率 (DF)、信息增益(IG)、互信息(MI)、卡方拟和检验(CHI)以及期望交叉熵(ECE),主要是因为原始网页特征维度太大,其中包含很多噪声,区分度不明显,无法对后序 SVM 学习提供帮助,而且这些未经处理的特征甚至会给分类算法的计算带来巨大的开销, 同时也会对分类效果产生负面影响,因此我们有必要在分类前对这些特征进行提取和筛选,对数据维度进行压缩,保证准确度的前提下尽量减少训练数据的空间维度。
需求描述
完成网页分类必不可少的就是要有训练集,此实验重点讲解我们如何通过编写的程序,将已将采集的大量网页一步步变成训练集的过程。我们采集的网页分成10类,并且分类存储在10个不同的文件夹中,即分别是体育、军事、财经、娱乐、文化、健康、汽车、教育、房产和其它。
实验环境
Linux Ubuntu 16.04
Eclipse 4.2
文件下载
打开终端模拟器
1.切换到data目录下,下载文本集,里面包含有5000篇左右的文档,几乎平均分配了10个不同的新闻网页(也可以用自己采集的新闻网页,但是注意保存路径应与程序中设置的路径相符)
cd /data
wget http://10.51.46.104:32600/allfiles/classify/classifydata.tar.gz
2.解压classifydata.tar.gz包到当前目录
tar zxvf classifydata.tar.gz
3.在/data目录下用wget命令下载该项目的配置文件,jar包等
wget http://10.51.46.104:32600/allfiles/classify/pzwj.tar.gz
4.解压pzwj.tar.gz包到当前目录
tar zxvf pzwj.tar.gz
项目搭建
打开Eclipse,创建一个Java Project项目,命名为createtraintxts。
选中该项目中的src,右键新建一个包,命名为com.yxcx.gettrain
选中该项目名,右键,新建一个Folder,命名为lib
将/data/pzwj下的jar包复制到该项目的lib目录下中,然后选中这两个jar包,右键选中Build Path,点击Add to Build Path
将/date/pzwj下的IKAnalyzer.cfg.xml配置文件和chinese_stopword.dic停用词文件复制到src下面
这时我们就完成了整个项目的搭建,项目结构如下图所示:
程序编写
1.接下来选择com.yxcx.gettrain,右键,创建类文件
在Name处,填写类名
2.第一步,我们创建的类的作用是需要对网页进行预处理,去除网页中大量的标签、超链接和script代码等,然后进行中文分词,并通过IKAnalyzer.cfg.xml加载chinese_stopword.dic停用词文件,去掉停用词,完成文本的预处理。
由此我们新建一个类,命名为DataPreProcess,完整代码如下,代码详细解释可以参照代码中注释。
package com.yxcx.gettrain;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.StringReader;
import org.apache.lucene.analysis.TokenStream;
import org.apache.lucene.analysis.tokenattributes.CharTermAttribute;
import org.wltea.analyzer.lucene.IKAnalyzer;
public class DataPreProcess {
public void doProcess(String strDir) throws IOException {
// strDir 要进行分词的网页的路径
System.out.println(strDir);
File fileDir = new File(strDir);
if (!fileDir.exists()) {
System.out.println("File not exist:" + strDir);
return;
}
File[] srcFiles = fileDir.listFiles();
String[] stemFileNames = new String[srcFiles.length];
for (int i = 0; i < srcFiles.length; i++) {
String fileFullName = srcFiles[i].getCanonicalPath();
// 文件名称
String fileShortName = srcFiles[i].getName();
if (!new File(fileFullName).isDirectory()) {
System.out.println("Begin preprocess:" + fileFullName);
StringBuilder stringBuilder = new StringBuilder();
// 路径/名称
stringBuilder.append(strDir + "/" + fileShortName);
createProcessFile(fileFullName, stringBuilder.toString());
stemFileNames[i] = stringBuilder.toString();
} else {
doProcess(fileFullName);
}
}
}
public static void createProcessFile(String fileFullName, String toFile) {
BufferedWriter bw = null;
BufferedReader br = null;
FileWriter fileWriter = null;
FileReader fileReader = null;
String line, resLine = null;
try {
StringBuilder sb = new StringBuilder();
fileReader = new FileReader(fileFullName);
br = new BufferedReader(fileReader);
// readLine方法:一行一行的读取文件
// 去掉a标签和html标签
while ((line = br.readLine()) != null) {
String conts = "";
line = line.replaceAll("<a(.*?)</a>.", "")
.replaceAll("</?[^>]+>", "")
.replaceAll("[^\\u4e00-\\u9fa5]", "");
conts += line;
// 开始进行中文分词,用/n分割
if (!conts.isEmpty()) {
IKAnalyzer analyzer = new IKAnalyzer(true);
// 通过分析器Analyzer将一个字符串创建成Taken流,第一个参数是一个名字没有实际作用
TokenStream tokenStream = analyzer.tokenStream("content",
new StringReader(conts));
while (tokenStream.incrementToken()) {
// 保存相应词汇
CharTermAttribute charTermAttribute = tokenStream
.getAttribute(CharTermAttribute.class);
sb.append(charTermAttribute.toString());
sb.append("\n");
}
resLine = sb.toString();
analyzer.close();
}
}
fileWriter = new FileWriter(toFile);
bw = new BufferedWriter(fileWriter);
bw.write(resLine);
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
bw.close();
br.close();
fileWriter.close();
fileReader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* @param args
* @throws IOException
*/
public void BPPMain(String[] args) throws IOException {
// TODO Auto-generated method stub
DataPreProcess dataPrePro = new DataPreProcess();
dataPrePro.doProcess("/data/classifydata/traindata/");
}
}
第二步,在进行文本预处理以后,需要提取特征词,为下一步生成训练集做准备。由此我们新建一个类,命名为ComputeWordsVector,在这个类中编写生成特征词的方法,用于最后一步的CreateTrainAndTestSample.java中生成训练集时调用。完整代码如下,代码详细解释可以参照代码注释。
package com.yxcx.gettrain;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.SortedMap;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.Iterator;
public class ComputeWordsVector {
// 统计每个词的总的出现次数,返回出现次数大于3词的词语的词汇构成最终的属性词典
public SortedMap<String, Double> countWords(String strDir,
Map<String, Double> wordMap) throws IOException {
File sampleFile = new File(strDir);// strDir 预处理好的文件的路径
File[] sample = sampleFile.listFiles();
String word;
for (int i = 0; i < sample.length; i++) {
if (!sample[i].isDirectory()) {
FileReader samReader = new FileReader(sample[i]);
BufferedReader samBR = new BufferedReader(samReader);
while ((word = samBR.readLine()) != null) {
if (!word.isEmpty() && wordMap.containsKey(word)) {
double count = wordMap.get(word) + 1;
wordMap.put(word, count);
} else {
wordMap.put(word, 1.0);
}
}
} else
countWords(sample[i].getCanonicalPath(), wordMap);
}
// 只返回出现次数大于3的词语
SortedMap<String, Double> newWordMap = new TreeMap<String, Double>();
Set<Map.Entry<String, Double>> allWords = wordMap.entrySet();
for (Iterator<Map.Entry<String, Double>> it = allWords.iterator(); it
.hasNext();) {
Map.Entry<String, Double> me = it.next();
if (me.getValue() >= 3) {
newWordMap.put(me.getKey(), me.getValue());
}
}
return newWordMap;
}
// 打印属性词典
void printWordMap(Map<String, Double> wordMap) throws IOException {
// TODO Auto-generated method stub
int countLine = 0;
File outPutFile = new File(
"/data/classifydata/docVector/allDicWordCountMap.txt");
FileWriter outPutFileWriter = new FileWriter(outPutFile);
Set<Map.Entry<String, Double>> allWords = wordMap.entrySet();
for (Iterator<Map.Entry<String, Double>> it = allWords.iterator(); it
.hasNext();) {
Map.Entry<String, Double> me = it.next();
outPutFileWriter.write(me.getKey() + " " + me.getValue() + "\n");
countLine++;
}
System.out.println("WordMap size" + countLine);
}
}
最后一步,生成训练集,新建一个类,命名为CreateTrainAndTestSample,完整代码如下,代码详细解释可以参照代码注释。
package com.yxcx.gettrain;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.SortedMap;
import java.util.TreeMap;
//创建训练集合与测试集合
public class CreateTrainAndTestSample {
// 根据包含非特征词的文档集生成只包含特征词的文档集到trainSpecial目录下
ComputeWordsVector cwv = new ComputeWordsVector();
void trainSpecialWords() throws IOException {
File file2 = new File("/data/classifydata/trainSpecial/" );
if(!file2.exists()){
file2.mkdir();
}
File file3 = new File("/data/classifydata/docVector/" );
if(!file3.exists()){
file3.mkdir();
}
// TODO Auto-generated method stub
String word;
String fileDir = "/data/classifydata/traindata/";
SortedMap<String, Double> wordMap = new TreeMap<String, Double>();
wordMap = cwv.countWords(fileDir, wordMap);
// 把wordMap输出到文件
cwv.printWordMap(wordMap);
File[] sampleDir = new File(fileDir).listFiles();
for (int i = 0; i < sampleDir.length; i++) {
File[] sample = sampleDir[i].listFiles();
String targetDir = "/data/classifydata/trainSpecial/"
+ sampleDir[i].getName();
File targetDirFile = new File(targetDir);
if (!targetDirFile.exists()) {
targetDirFile.mkdir();
}
for (int j = 0; j < sample.length; j++) {
String fileShortName = sample[j].getName();
targetDir = "/data/classifydata/trainSpecial/"
+ sampleDir[i].getName() + "/" + fileShortName;
FileWriter tgWriter = new FileWriter(targetDir);
FileReader samReader = new FileReader(sample[j]);
BufferedReader samBR = new BufferedReader(samReader);
while ((word = samBR.readLine()) != null) {
if (wordMap.containsKey(word)) {
tgWriter.append(word + "\n");
}
}
tgWriter.flush();
tgWriter.close();
}
}
}
public void NaiveBayesianClassifierMain(String[] args) throws Exception {
CreateTrainAndTestSample ctt = new CreateTrainAndTestSample();
ctt.trainSpecialWords();
}
public static void main(String[] args) throws Exception {
DataPreProcess DataPP = new DataPreProcess();
DataPP.BPPMain(args);
CreateTrainAndTestSample ctt = new CreateTrainAndTestSample();
ctt.trainSpecialWords();
}
}
程序编写完成,执行Run As,运行项目
至此,我们已经得到训练集了,打开文件系统
进入到/data/classifydata/目录中,traindata中存放的是将网页预处理完后的文本集,trainSpecial目录下存放的是我们下一步实验所需要的训练集,docVector目录下存放的是属性词典。