前言
余弦定理,这个在初中课本中就出现过的公式,恐怕没有人不知道的吧。但是另外一个概念,可能不是很多的人会听说过,他叫空间向量,一般用e表示,高中课本中有专门讲过这个东西,有了余弦定理和向量空间,我们就可以做许多有意思的事情了,利用余弦定理计算文本相似度的算法就是其中一个很典型的例子。当然这个话题太老,说的人太多,没有什么新意,恰巧周末阅读了吴军博士的<<数学之美>>这门书,书中讲到了利用余弦定理实现新闻分类,于是就索性完成这个算法的初步模型。感兴趣的可以继续往下看。
算法背景
在以往,如果对一则新闻进行归类,一般使用的都是人工分类的办法,大体上看一下标题和首尾两段文字,就能知道新闻是属于财经的,体育的又或者是健康类的。但是在当今信息爆炸的时代,这显然是不可能完成的任务,所以我们急切的相用机器自己帮我们”分类“。最好的形式是我给计算机提供大量的已分类好的数据,等强大的计算机大脑训练好了这个分类模型,后边的事情就是他来完成了。看起来这好像很高深,很困难的样子,但是其实我们自己也可以写一个,只是效果可能不会那么好。
分类器实现原理
新闻自动分类器实现的本质也是利用余弦定理比较文本的相似度,于是这个问题的难点就在于这个特征向量哪里来,怎么去获得。特征向量,特征向量,关键两个字在于特征,新闻的特征就在于他的关键词,我的简单理解就是专业性的词语,换句话说,就是属于某类新闻特有的词语,比如金融类的新闻,关键词一般就是股票啊,公司啊,上市啊等等词语。这些词的寻找可以通过统计词频的方式实现,最后统计出来的关键词,进行降序排列,一个关键词就代表一个新的维度。 那么新的问题又来了,我要统计词频,那么就得首先进行分词,要把每个新闻句子的主谓宾统统挖掘出来啊,好像这个工作比我整个算法还要复杂的样子。OK,其实已经有人已经帮我们把这个问题解决了,在这个算法中我使用的是中科大的ICTCLAS分词系统,效果非常棒,举个例子,下面是我原始的新闻内容:
教育部副部长:教育公平是社会公平重要基础 7月23日,教育部党组副书记、副部长杜玉波为全国学联全体代表作《教育综合改革与青年学生成长成才》的专题报告。 中国青年网记者 张炎良 摄 人民网北京7月24日电(记者 贺迎春 实习生 王斯慧
经过分词系统处理后的分词效果:
教育部/nt 副/b 部长/n :/wm 教育/v 公平/an 是/vshi 社会/n 公平/a 重要/a 基础/n 7月/t 23日/t ,/wd 教育部/nt 党组/n 副/b 书记/n 、/wn 副/b 部长/n 杜玉波/nr 为/p 全国学联/nt 全体/n 代表作/n 《/wkz 教育/vn 综合/vn 改革/vn 与/cc 青年/n 学生/n 成长/vi 成才/vi 》/wky 的/ude1 专题/n 报告/n 。/wj 中国/ns 青年/n 网/n 记者/n 张/q 炎/ng 良/d 摄/vg 人民/n 网/n 北京/ns 7月/t 24日/t 电/n (/wkz 记者/n 贺/vg 迎春/n 实习生/n 王斯慧/nr )/wky 昨日/t ,/wd 教育部/nt 副/b 部长
OK,有了这个分词的结果之后,后面的事情就水到渠成了。
算法的实现步骤
1、给定训练的新闻数据集。
2、通过分词系统统计词频的方式,统计词频最高的N位作为特征词,即特征向量
3、输入测试数据,同样统计词频,并于训练数据的进行商的操作,得到特征向量值
4、最后利用余弦定理计算相似度,并与最小阈值做比较。
算法的代码实现
ICTCLAS工具类ICTCLAS.java:
package NewsClassify; import java.io.File; import java.io.FileOutputStream; import java.io.InputStream; import java.util.StringTokenizer; public class ICTCLAS50 { static { try { String libpath = System.getProperty("user.dir") + "\\lib"; String path = null; StringTokenizer st = new StringTokenizer(libpath, System.getProperty("path.separator")); if (st.hasMoreElements()) { path = st.nextToken(); } // copy all dll files to java lib path File dllFile = null; InputStream inputStream = null; FileOutputStream outputStream = null; byte[] array = null; dllFile = new File(new File(path), "ICTCLAS50.dll"); if (!dllFile.exists()) { inputStream = ICTCLAS50.class.getResource("/lib/ICTCLAS50.dll") .openStream(); outputStream = new FileOutputStream(dllFile); array = new byte[1024]; for (int i = inputStream.read(array); i != -1; i = inputStream .read(array)) { outputStream.write(array, 0, i); } outputStream.close(); } } catch (Exception e) { e.printStackTrace(); } try { // load JniCall.dll System.loadLibrary("ICTCLAS50"); System.out.println("4444"); } catch (Error e) { e.printStackTrace(); } } public native boolean ICTCLAS_Init(byte[] sPath); public native boolean ICTCLAS_Exit(); public native int ICTCLAS_ImportUserDictFile(byte[] sPath, int eCodeType); public native int ICTCLAS_SaveTheUsrDic(); public native int ICTCLAS_SetPOSmap(int nPOSmap); public native boolean ICTCLAS_FileProcess(byte[] sSrcFilename, int eCodeType, int bPOSTagged, byte[] sDestFilename); public native byte[] ICTCLAS_ParagraphProcess(byte[] sSrc, int eCodeType, int bPOSTagged); public native byte[] nativeProcAPara(byte[] sSrc, int eCodeType, int bPOStagged); }
新闻实体类New.java
package NewsClassify; /** * 词语实体类 * * @author lyq * */ public class Word implements Comparable<Word> { // 词语名称 String name; // 词频 Integer count; public Word(String name, Integer count) { this.name = name; this.count = count; } @Override public int compareTo(Word o) { // TODO Auto-generated method stub return o.count.compareTo(this.count); } }
分类算法类NewsClassify.java:
package NewsClassify; import java.io.BufferedReader; import java.io.File; import java.io.FileReader; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; /** * 分类算法模型 * * @author lyq * */ public class NewsClassifyTool { // 余弦向量空间维数 private int vectorNum; // 余弦相似度最小满足阈值 private double minSupportValue; // 当前训练数据的新闻类别 private String newsType; // 训练新闻数据文件地址 private ArrayList<String> trainDataPaths; public NewsClassifyTool(ArrayList<String> trainDataPaths, String newsType, int vectorNum, double minSupportValue) { this.trainDataPaths = trainDataPaths; this.newsType = newsType; this.vectorNum = vectorNum; this.minSupportValue = minSupportValue; } /** * 从文件中读取数据 */ private String readDataFile(String filePath) { File file = new File(filePath); StringBuilder strBuilder = null; try { BufferedReader in = new BufferedReader(new FileReader(file)); String str; strBuilder = new StringBuilder(); while ((str = in.readLine()) != null) { strBuilder.append(str); } in.close(); } catch (IOException e) { e.getStackTrace(); } return strBuilder.toString(); } /** * 计算测试数据的特征向量 */ private double[] calCharacterVectors(String filePath) { int index; double[] vectorDimensions; double[] temp; News news; News testNews; String newsCotent; String testContent; String parseContent; // 高频词汇 ArrayList<Word> frequentWords; ArrayList<Word> wordList; testContent = readDataFile(filePath); testNews = new News(testContent); parseNewsContent(filePath); index = filePath.indexOf(‘.‘); parseContent = readDataFile(filePath.substring(0, index) + "-split.txt"); testNews.statWords(parseContent); vectorDimensions = new double[vectorNum]; // 计算训练数据集的类别的特征向量 for (String path : this.trainDataPaths) { newsCotent = readDataFile(path); news = new News(newsCotent); // 进行分词操作 index = path.indexOf(‘.‘); parseNewsContent(path); parseContent = readDataFile(path.substring(0, index) + "-split.txt"); news.statWords(parseContent); wordList = news.wordDatas; // 将词频统计结果降序排列 Collections.sort(wordList); frequentWords = new ArrayList<Word>(); // 截取出前vectorDimens的词语 for (int i = 0; i < vectorNum; i++) { frequentWords.add(wordList.get(i)); } temp = testNews.calVectorDimension(frequentWords); // 将特征向量值进行累加 for (int i = 0; i < vectorDimensions.length; i++) { vectorDimensions[i] += temp[i]; } } // 最后取平均向量值作为最终的特征向量值 for (int i = 0; i < vectorDimensions.length; i++) { vectorDimensions[i] /= trainDataPaths.size(); } return vectorDimensions; } /** * 根据求得的向量空间计算余弦相似度值 * * @param vectorDimension * 已求得的测试数据的特征向量值 * @return */ private double calCosValue(double[] vectorDimension) { double result; double num1; double num2; double temp1; double temp2; // 标准的特征向量,每个维度上都为1 double[] standardVector; standardVector = new double[vectorNum]; for (int i = 0; i < vectorNum; i++) { standardVector[i] = 1; } temp1 = 0; temp2 = 0; num1 = 0; for (int i = 0; i < vectorNum; i++) { // 累加分子的值 num1 += vectorDimension[i] * standardVector[i]; // 累加分母的值 temp1 += vectorDimension[i] * vectorDimension[i]; temp2 += standardVector[i] * standardVector[i]; } num2 = Math.sqrt(temp1) * Math.sqrt(temp2); // 套用余弦定理公式进行计算 result = num1 / num2; return result; } /** * 进行新闻分类 * * @param filePath * 测试新闻数据文件地址 */ public void newsClassify(String filePath) { double result; double[] vectorDimension; vectorDimension = calCharacterVectors(filePath); result = calCosValue(vectorDimension); // 如果余弦相似度值满足最小阈值要求,则属于目标分类 if (result >= minSupportValue) { System.out.println(String.format("最终相似度结果为%s,大于阈值%s,所以此新闻属于%s类新闻", result, minSupportValue, newsType)); } else { System.out.println(String.format("最终相似度结果为%s,小于阈值%s,所以此新闻不属于%s类新闻", result, minSupportValue, newsType)); } } /** * 利用分词系统进行新闻内容的分词 * * @param srcPath * 新闻文件路径 */ private void parseNewsContent(String srcPath) { // TODO Auto-generated method stub int index; String dirApi; String desPath; dirApi = System.getProperty("user.dir") + "\\lib"; // 组装输出路径值 index = srcPath.indexOf(‘.‘); desPath = srcPath.substring(0, index) + "-split.txt"; try { ICTCLAS50 testICTCLAS50 = new ICTCLAS50(); // 分词所需库的路径、初始化 if (testICTCLAS50.ICTCLAS_Init(dirApi.getBytes("GB2312")) == false) { System.out.println("Init Fail!"); return; } // 将文件名string类型转为byte类型 byte[] Inputfilenameb = srcPath.getBytes(); // 分词处理后输出文件名、将文件名string类型转为byte类型 byte[] Outputfilenameb = desPath.getBytes(); // 文件分词(第一个参数为输入文件的名,第二个参数为文件编码类型,第三个参数为是否标记词性集1 yes,0 // no,第四个参数为输出文件名) testICTCLAS50.ICTCLAS_FileProcess(Inputfilenameb, 0, 1, Outputfilenameb); // 退出分词器 testICTCLAS50.ICTCLAS_Exit(); } catch (Exception ex) { ex.printStackTrace(); } } }
场景测试了Client.java:
package NewsClassify; import java.util.ArrayList; /** * 新闻分类算法测试类 * @author lyq * */ public class Client { public static void main(String[] args){ String testFilePath1; String testFilePath2; String testFilePath3; String path; String newsType; int vectorNum; double minSupportValue; ArrayList<String> trainDataPaths; NewsClassifyTool classifyTool; //添加测试以及训练集数据文件路径 testFilePath1 = "C:\\Users\\lyq\\Desktop\\icon\\test\\testNews1.txt"; testFilePath2 = "C:\\Users\\lyq\\Desktop\\icon\\test\\testNews2.txt"; testFilePath3 = "C:\\Users\\lyq\\Desktop\\icon\\test\\testNews3.txt"; trainDataPaths = new ArrayList<String>(); path = "C:\\Users\\lyq\\Desktop\\icon\\test\\trainNews1.txt"; trainDataPaths.add(path); path = "C:\\Users\\lyq\\Desktop\\icon\\test\\trainNews2.txt"; trainDataPaths.add(path); newsType = "金融"; vectorNum = 10; minSupportValue = 0.45; classifyTool = new NewsClassifyTool(trainDataPaths, newsType, vectorNum, minSupportValue); classifyTool.newsClassify(testFilePath1); classifyTool.newsClassify(testFilePath2); classifyTool.newsClassify(testFilePath3); } }
结果输出:
最终相似度结果为0.39999999999999997,小于阈值0.45,所以此新闻不属于金融类新闻 最终相似度结果为0.4635393084189425,大于阈值0.45,所以此新闻属于金融类新闻 最终相似度结果为0.661835948543857,大于阈值0.45,所以此新闻属于金融类新闻
测试数据以及全部代码,链接在此:https://github.com/linyiqun/news-classifier
参考文献
百度百科
<<数学之美>>第二版.吴军博士
版权声明:本文为博主原创文章,未经博主允许不得转载。