hanlp源码解析word2vec词向量算法

2023-10-14 18:30

本文主要是介绍hanlp源码解析word2vec词向量算法,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

欢迎关注鄙人公众号,技术干货随时看!
在这里插入图片描述

one-hot表示法

词向量就是把一个词用向量的形式表示,以前的经典表示法是one-hot,这种表示法向量的维度是词汇量的大小。它的处理方式简单粗暴,一般就是统计词库包含的所有V个词,然后将这V个词固定好顺序,然后每个词就可以用一个V维的稀疏向量来表示,向量中只有在该词出现的位置的元素才为1,其它元素全为0。比如下面这几个词,第一个元素为1的表示中国,第六个元素为1的表示美国,第五个元素为1的表示日本。

中国[1,0,0,0,0,0,0,0,0,……,0,0,0,0,0,0,0]

美国[0,0,0,0,0,1,0,0,0,……,0,0,0,0,0,0,0]

日本[0,0,0,0,1,0,0,0,0,……,0,0,0,0,0,0,0]

按照目前汉语的词汇量20万左右,那么一个词就是20万维度的向量来表示,这对内存和计算效率都是灾难性的,优点是非常简单,只需过扫描一遍语料库好即可。

word2vec

google开源的word2vec得到的词的向量形式则可以自由控制维度,一般是100左右。google开源的是用c语言开发的,hanlp的作者移植了这套c代码并合并到了word2vec中。据说,每个线程每秒训练的词语稳定在180-190K,比原版C程序要快2.5倍左右;训练速度比C程序要快的原因是,原版C程序读取单词后需要去char数组里遍历查找id;而我的Java实现直接读取缓存文件中的id,当然开始训练前要先进行词->id的转换并输出到缓存文件,这个过程大约多花一两分钟时间,相较于训练时间,无疑是值得的。这样改进之后还可以直接读取类似text8那样的变态语料,一举多得。效率与c语言版的没有差别。

下面开始正式讨论hanlp中word2vec的源码。关计word2vec中用到的神经网络的模型和算法,这里不再赘述,请参考作者的文章http://www.hankcs.com/nlp/word2vec.html
 
 语料库

训库词向量当然需要一个相对完整的语料库,目有可以采用人民日报、Sighan05分词语料 http://sighan.cs.uchicago.edu/bakeoff2005/,一般情况下首先对语料库分词,这里不再讨论分词,为了讨论源码的方更的,我们采用的语料库如下(生产环境语料库越大越向量模型越准确):

帕勒莫 VS 梅西纳 已经 无关紧要 初盘 显示 格局 
雷吉纳 VS 尤文图斯 初盘 显示 客队 强大 关系到 客队 夺冠 问题 尤文图斯 任胆 
特雷维索 VS 乌迪内斯 乌迪内斯 客场 连续 拿下 状态 开出 平手 想必 乌鸡 势头 就此 中断 足彩 王智 德甲 解盘 
科隆 VS 比勒菲尔德 初盘 高开 意图 明显 庄家 筹码 上盘 嫌疑 极大 科隆 有望 不败 
拜仁 慕尼黑 VS 多特蒙德 多特蒙德 客场 至多 连赢 极限 盘也 有意 冷落 主队 
汉堡 VS 不莱梅 半球盘 本赛季 尚无 平局 记录 适合 选择 
杜伊斯堡 VS 美因兹 初盘 极为 不符 庄家 利用 主队 已经 降级 题材 美因兹 嫌疑 
沙尔克 VS 斯图加特 初盘 主队 水位 偏高 目前 斯图加特 客场 路有 反弹 迹象 排除 客队 可能 

训练完成的词向量文件如下所示:第一行是词向量的条数和维度。

15 20
VS 0.020013 0.022097 -0.019151 -0.016390 0.006833 0.015105 0.004704 0.001057 -0.018018 0.011092 -0.021782 0.006248 -0.003757 -0.004786 -0.016579 -0.009411 0.012897 0.015127 0.014845 0.007987
初盘 0.007693 -0.018967 -0.020466 0.024825 0.019040 0.015461 -0.003025 0.020149 -0.002462 0.003626 -0.000768 -0.014950 0.006504 -0.006674 -0.019058 0.023742 0.021883 -0.005529 -0.001090 0.002513
客队 -0.018188 -0.020036 0.022774 0.000315 -0.012912 -0.015211 -0.015382 0.008485 0.001007 0.006655 -0.021068 -0.019039 -0.000650 0.005718 0.012749 -0.015850 0.020398 0.004635 0.005598 -0.003042
客场 0.014932 -0.011439 -0.010487 0.010792 -0.003766 0.005154 0.009023 -0.020443 -0.009915 0.014568 0.021159 0.019660 -0.015234 -0.010538 -0.004546 0.010007 -0.018942 0.014989 0.013939 -0.007995
主队 -0.007750 -0.011236 0.021236 0.019609 -0.005778 0.021135 0.024224 0.009164 0.024857 -0.015614 -0.007675 -0.010631 -0.014663 0.014050 0.008034 0.002098 -0.011031 0.007467 0.015391 0.000876
已经 0.011419 -0.024740 0.021474 0.002454 -0.009068 -0.010289 -0.003746 -0.014546 -0.021767 -0.014196 0.021319 -0.008875 -0.013376 0.011613 -0.008489 0.023771 -0.007968 -0.022923 0.013644 -0.000344
显示 -0.017444 0.004879 0.007210 -0.002407 0.009122 -0.019788 0.004405 0.009083 -0.015045 0.000710 -0.000304 -0.011996 -0.014163 -0.023469 0.000114 0.000764 0.000049 -0.019669 -0.024809 -0.023733
尤文图斯 -0.021628 -0.002735 0.006956 0.005921 -0.015912 0.024990 -0.010057 -0.006368 -0.007022 0.023663 -0.018819 0.005805 -0.006677 0.015939 0.000203 0.021348 -0.014096 -0.013026 -0.020961 -0.018334
乌迪内斯 -0.015997 -0.015574 0.012221 -0.009335 0.013400 0.018450 0.006779 0.014753 0.012378 0.011703 -0.017754 -0.017165 -0.018283 -0.000660 0.020653 -0.013683 -0.015302 -0.020982 0.016530 -0.020895
科隆 -0.002896 0.010894 -0.023091 -0.022393 0.007214 0.017623 0.021321 0.010728 0.015811 -0.015638 -0.018202 0.019874 -0.013824 0.008767 0.002870 0.008952 -0.005911 0.000994 -0.008430 -0.005633
庄家 -0.022181 -0.016407 0.017515 0.005170 -0.011805 -0.007914 0.012580 -0.017677 -0.011669 0.023722 0.021310 -0.019916 0.004310 -0.011295 0.000681 0.015143 0.024270 0.009833 0.020564 0.015712
嫌疑 -0.001188 0.022385 0.012687 0.023688 -0.008521 0.023697 0.021633 -0.007123 -0.022107 0.024156 0.004487 -0.010408 -0.003606 -0.003700 -0.019260 -0.007152 -0.002211 -0.024650 -0.017598 -0.006039
多特蒙德 -0.004566 0.008701 0.004184 -0.002898 0.014885 0.007425 0.002952 0.018191 -0.005542 -0.007308 -0.002137 -0.013698 -0.015125 0.001091 0.021833 -0.006802 -0.000246 -0.020732 0.018738 -0.007649
美因兹 0.014931 -0.012285 -0.004318 0.000548 0.018110 -0.000446 0.022263 -0.003785 0.010456 -0.000800 -0.019446 0.021221 0.005188 -0.015099 0.015043 -0.020609 -0.021879 0.016274 -0.017079 -0.013266
斯图加特 0.017639 0.016427 0.003426 0.022617 0.024283 0.020275 0.008824 -0.021752 0.011444 -0.024461 -0.006432 0.007560 0.004904 -0.007624 0.014690 -0.023040 -0.007056 -0.006559 -0.008281 0.015354

模型训练第一步:加载语料库,统计词频,(只保留词频大于等于2的词)

 /*** 此函数功能完成词频统计* 每个词的相关信息存储在VocabWord中,word是对应的词,cn是在语料库中出现的次数,codelen是huffman编码的长度,code是huffman编码,point是对应的前置结点** @throws IOException*/public void learnVocab() throws IOException{vocab = new VocabWord[vocabMaxSize];vocabIndexMap = new TreeMap<String, Integer>();//此处作者是用TreeMap和数组相结合的形式来存储的,vocabIndexMap的key是word,value是对应vocab中的下标vocabSize = 0;final File trainFile = new File(config.getInputFile());BufferedReader raf = null;FileInputStream fileInputStream = null;cache = null;vocabSize = 0;TrainingCallback callback = config.getCallback();try{fileInputStream = new FileInputStream(trainFile);raf = new BufferedReader(new InputStreamReader(fileInputStream, encoding));cacheFile = File.createTempFile(String.format("corpus_%d", System.currentTimeMillis()), ".bin");cache = new DataOutputStream(new FileOutputStream(cacheFile));while (true){String word = readWord(raf);//读取一个词if (word == null && eoc) break;trainWords++;if (trainWords % 100000 == 0)//这段代码是打印进度的,不用管{if (callback == null){System.err.printf("=======%c%.2f%% %dK", 13,(1.f - fileInputStream.available() / (float) trainFile.length()) * 100.f,trainWords / 1000);System.err.flush();}else{callback.corpusLoading((1.f - fileInputStream.available() / (float) trainFile.length()) * 100.f);}}int idx = searchVocab(word);//查询下vocabIndexMap中有没有出现过,出现过就词步加1,没有出现过就加到map里面if (idx == -1){idx = addWordToVocab(word);vocab[idx].cn = 1;} else {vocab[idx].cn++;}if (vocabSize > VOCAB_MAX_SIZE * 0.7)//这段代码是当此词汇非常大时,移除低词频的词{reduceVocab();idx = searchVocab(word);}cache.writeInt(idx);}}finally{Utility.closeQuietly(fileInputStream);Utility.closeQuietly(raf);Utility.closeQuietly(cache);System.err.println();}if (callback == null){System.err.printf("%c100%% %dK", 13, trainWords / 1000);System.err.flush();}else{callback.corpusLoading(100);callback.corpusLoaded(vocabSize, trainWords, trainWords);}}

模型训练第二步:对词频排序,从大到小,实现原理很简单就是实现comparable接口

@Overridepublic int compareTo(VocabWord that){return that.cn - this.cn;}

模型训练第三步:构建huffman树

 void createBinaryTree(){int[] point = new int[VocabWord.MAX_CODE_LENGTH];char[] code = new char[VocabWord.MAX_CODE_LENGTH];int[] count = new int[vocabSize * 2 + 1];char[] binary = new char[vocabSize * 2 + 1];//存储huffman编码int[] parentNode = new int[vocabSize * 2 + 1];//一组数组存储huffman树for (int i = 0; i < vocabSize; i++)count[i] = vocab[i].cn;for (int i = vocabSize; i < vocabSize * 2; i++)count[i] = Integer.MAX_VALUE;int pos1 = vocabSize - 1;int pos2 = vocabSize;// Following algorithm constructs the Huffman tree by adding one node at a timeint min1i, min2i;for (int i = 0; i < vocabSize - 1; i++){// First, find two smallest nodes 'min1, min2'if (pos1 >= 0){if (count[pos1] < count[pos2]){min1i = pos1;pos1--;}else{min1i = pos2;pos2++;}}else{min1i = pos2;pos2++;}if (pos1 >= 0){if (count[pos1] < count[pos2]){min2i = pos1;pos1--;}else{min2i = pos2;pos2++;}}else{min2i = pos2;pos2++;}count[vocabSize + i] = count[min1i] + count[min2i];parentNode[min1i] = vocabSize + i;parentNode[min2i] = vocabSize + i;binary[min2i] = 1;}System.out.println(Arrays.toString(count));System.out.println(Arrays.toString(parentNode));System.out.println(Arrays.toString(binary));// Now assign binary code to each vocabulary wordfor (int j = 0; j < vocabSize; j++){int k = j;int i = 0;while (true){code[i] = binary[k];point[i] = k;i++;k = parentNode[k];if (k == vocabSize * 2 - 2) break;}vocab[j].codelen = i;vocab[j].point[0] = vocabSize - 2;for (k = 0; k < i; k++){vocab[j].code[i - k - 1] = code[k];vocab[j].point[i - k] = point[k] - vocabSize;}}System.out.println(Arrays.toString(vocab));}

这段代码看起来有点晕,因为词汇量很大,我画了张图,最终构建的huffman如下所示,下面所有的模型训练都是基于这张huffman图

这里写图片描述

最终的词汇表如下所示:

这里写图片描述

cbow模型训练:
待续!

这篇关于hanlp源码解析word2vec词向量算法的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/212330

相关文章

深度解析Python中递归下降解析器的原理与实现

《深度解析Python中递归下降解析器的原理与实现》在编译器设计、配置文件处理和数据转换领域,递归下降解析器是最常用且最直观的解析技术,本文将详细介绍递归下降解析器的原理与实现,感兴趣的小伙伴可以跟随... 目录引言:解析器的核心价值一、递归下降解析器基础1.1 核心概念解析1.2 基本架构二、简单算术表达

深度解析Java @Serial 注解及常见错误案例

《深度解析Java@Serial注解及常见错误案例》Java14引入@Serial注解,用于编译时校验序列化成员,替代传统方式解决运行时错误,适用于Serializable类的方法/字段,需注意签... 目录Java @Serial 注解深度解析1. 注解本质2. 核心作用(1) 主要用途(2) 适用位置3

Java MCP 的鉴权深度解析

《JavaMCP的鉴权深度解析》文章介绍JavaMCP鉴权的实现方式,指出客户端可通过queryString、header或env传递鉴权信息,服务器端支持工具单独鉴权、过滤器集中鉴权及启动时鉴权... 目录一、MCP Client 侧(负责传递,比较简单)(1)常见的 mcpServers json 配置

从原理到实战解析Java Stream 的并行流性能优化

《从原理到实战解析JavaStream的并行流性能优化》本文给大家介绍JavaStream的并行流性能优化:从原理到实战的全攻略,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的... 目录一、并行流的核心原理与适用场景二、性能优化的核心策略1. 合理设置并行度:打破默认阈值2. 避免装箱

Maven中生命周期深度解析与实战指南

《Maven中生命周期深度解析与实战指南》这篇文章主要为大家详细介绍了Maven生命周期实战指南,包含核心概念、阶段详解、SpringBoot特化场景及企业级实践建议,希望对大家有一定的帮助... 目录一、Maven 生命周期哲学二、default生命周期核心阶段详解(高频使用)三、clean生命周期核心阶

深入解析C++ 中std::map内存管理

《深入解析C++中std::map内存管理》文章详解C++std::map内存管理,指出clear()仅删除元素可能不释放底层内存,建议用swap()与空map交换以彻底释放,针对指针类型需手动de... 目录1️、基本清空std::map2️、使用 swap 彻底释放内存3️、map 中存储指针类型的对象

Java Scanner类解析与实战教程

《JavaScanner类解析与实战教程》JavaScanner类(java.util包)是文本输入解析工具,支持基本类型和字符串读取,基于Readable接口与正则分隔符实现,适用于控制台、文件输... 目录一、核心设计与工作原理1.底层依赖2.解析机制A.核心逻辑基于分隔符(delimiter)和模式匹

Java+AI驱动实现PDF文件数据提取与解析

《Java+AI驱动实现PDF文件数据提取与解析》本文将和大家分享一套基于AI的体检报告智能评估方案,详细介绍从PDF上传、内容提取到AI分析、数据存储的全流程自动化实现方法,感兴趣的可以了解下... 目录一、核心流程:从上传到评估的完整链路二、第一步:解析 PDF,提取体检报告内容1. 引入依赖2. 封装

深度解析Python yfinance的核心功能和高级用法

《深度解析Pythonyfinance的核心功能和高级用法》yfinance是一个功能强大且易于使用的Python库,用于从YahooFinance获取金融数据,本教程将深入探讨yfinance的核... 目录yfinance 深度解析教程 (python)1. 简介与安装1.1 什么是 yfinance?

99%的人都选错了! 路由器WiFi双频合一还是分开好的专业解析与适用场景探讨

《99%的人都选错了!路由器WiFi双频合一还是分开好的专业解析与适用场景探讨》关于双频路由器的“双频合一”与“分开使用”两种模式,用户往往存在诸多疑问,本文将从多个维度深入探讨这两种模式的优缺点,... 在如今“没有WiFi就等于与世隔绝”的时代,越来越多家庭、办公室都开始配置双频无线路由器。但你有没有注