• 余弦定理实现新闻自动分类算法


    前言

    余弦定理,这个在初中课本中就出现过的公式,恐怕没有人不知道的吧。但是另外一个概念,可能不是很多的人会听说过,他叫空间向量,一般用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


    参考文献

    百度百科

    <<数学之美>>第二版.吴军博士

  • 相关阅读:
    控制element表格禁用选择
    深度拷贝
    VScode修复eslint报错,保存的时候自动格式修正
    关于route监听
    PAT 1030 完美数列
    PAT1029 旧键盘(C完全正确)
    PAT 1028 人口普查
    PAT 1016
    PAT:1013
    PAT :1012 数字分类
  • 原文地址:https://www.cnblogs.com/bianqi/p/12183901.html
Copyright © 2020-2023  润新知