背景
自2016年facebook开源 文本分类工具以来. 为实现工程上快速方便使用, 急需java版本的fasttext工具.
调研之后fasttext4j是最为方便和准确的选择, 但存在一个问题: 中文文本的预测部分出现偏差. 因此对fasttext4j进行改造.
编码
编码问题主要是在于数据在处理过程中,到底加载成什么样子,是不是期望的01长度序列,或者字节长度.
比如: 对于输入字符串(或者一句话)的处理,每一个字符到底占用多少个字节.
编码不一致导致问题出现的根源在于一下几个方面:
1 | 1. 输入的字符串本身的编码和读取时指定的编码(将byte数组转出String)不一致 |
比如在Java中,内部使用unicode统一处理,但外部文件的编码格式不一.
UNICODE、UTF8、GBK
说到unicode,详细解释下.
1 | Unicode 是「字符集」 |
广义的 Unicode 是一个标准,定义了一个字符集以及一系列的编码规则,即 Unicode 字符集和UTF-8,UTF-16等等编码.
2字节标准的Unicode怎么对世界上绝大多数字符进行编码? UCS-2 和 UCS-4 标准.
Unicode 只是一个符号集,它只规定了符号的二进制代码,却没有规定这个二进制代码应该如何存储.不存在编码和转码流程(对于识别二进制的过程-转码)
UTF-8 就是在互联网上使用最广的一种 Unicode 的实现方式.其他实现方式还包括 UTF-16(字符用两个字节或四个字节表示)和UTF-32(字符用四个字节表示),不过在互联网上基本不用。重复一遍,这里的关系是,UTF-8 是 Unicode 的实现方式之一。
1 | UTF-8 最大的一个特点,就是它是一种变长的编码方式。它可以使用1~4个字节表示一个符号,根据不同的符号而变化字节长度. |
UTF8的编码规则
1 | 1. 对于单字节的符号,字节的第一位设为0,后面7位为这个符号的 Unicode 码。因此对于英语字母,UTF-8 编码和 ASCII 码是相同的。 |
下表总结了编码规则,字母x表示可用编码的位。
1 | Unicode符号范围(十六进制) | UTF-8编码方式(二进制) |
跟据上表,解读 UTF-8 编码非常简单。如果一个字节的第一位是0,则这个字节单独就是一个字符;如果第一位是1,则连续有多少个1,就表示当前字符占用多少个字节。
下面,还是以汉字严为例,演示如何实现 UTF-8 编码。
严的 Unicode 是4E25(100111000100101),根据上表,可以发现4E25处在第三行的范围内(0000 0800 - 0000 FFFF),因此严的 UTF-8 编码需要三个字节,即格式是1110xxxx 10xxxxxx 10xxxxxx。然后,从严的最后一个二进制位开始,依次从后向前填入格式中的x,多出的位补0。这样就得到了,严的 UTF-8 编码是11100100 10111000 10100101,转换成十六进制就是E4B8A5
UTF-8编码有一个额外的好处,就是ASCII编码实际上可以被看成是UTF-8编码的一部分,所以,大量只支持ASCII编码的历史遗留软件可以在UTF-8编码下继续工作
在计算机内存中,统一使用Unicode编码,当需要保存到硬盘或者需要传输的时候,就转换为UTF-8编码。 确认都是这个样子?
Fasttext中的编码问题
此问题主要针对 Facebook的C++版本fasttext被翻译成Java版本之后的预测等计算误差问题,由于编码问题导致.
首先,通过上面的了解, 可以知道一个字符串在内存中待程序处理时,它的编码是什么?
从一个文件中读取数据内容,应该按照指定编码进行解码,比如文件时UTF-8的,那处理时应该按照UTF-8格式来处理.
那么问题在于. fasttext在取子字特征(computeSubwords)时,有如下代码:
1 | if ((word[i] & 0xC0) == 0x80) continue; |
CO 二进制为 11000000, 80 二进制为 10000000.
这段代码的意思时通过掩码CO,过滤掉10开头的单字节.
那为什么这样做呢?
通过UTF-8的编码规则,可以容易发现是过滤掉了不能成为一个字母的字节.(因为C++中的char-<这里的word[i]>是单个字节,所以,处理时是按字节遍历词语提取子字特征,但处理是针对单字母处理,如果遇到不能成为一个字的字节,就过滤掉)
然而:在java版本中,取子字特征时,是以字符遍历单词的,以下代码:
1 | // true: 表示 字节c 不构成字符. false: 表示字节c构成字符. 原理见UTF-8编码规则. 多字节字符的低位字节前两位都是 10. 所以通过掩码筛选. |
将会过滤掉一些中文字符,导致一些特征丢失,出现预测结果很大不一样.
ASCII,Unicode和UTF-8终于找到一个能完全搞清楚的文章了
总结一下
- c++版本的fasttext 针对多字节的字符 做了支持: c++ 中的char代表一个字节。 然后通过掩码与运算 跳过了不成字符的字节 来提取子字ngram特征(字符级ngram)
- 然而java版本的 char代表一个字符. 所以根本就不需要过滤,直接按照字符处理即可. java 中的char代表一个字符, 并且编译器默认使用Unicode编码. 所有有可能正好第一个字节就满足而charMatches. 这时, 这个字符就被当做不成字符的字节被丢掉.
子字概念
字母的 Ngram 实现, 或者叫 字符级ngram.
什么是字母的 Ngram? "字母Ngram"如何加入到模型中? 源码里面把这块叫做 Subwords
其实就是把它和"词语"一样对待,一起求和取平均.
中文词subwords的计算: 其实就是多字节字符的计算(包括init算子字特征,预测的子字合并)
以skipgram为例,输入的 vector 和所要预测的 vector 都是单个词语与subwords相加求和的结果
【手撕 - 自然语言处理】手撕 FastText 源码(02)基于字母的 Ngram 实现
fasttext模型架构
fastText模型架构和Word2Vec中的CBOW模型很类似,不同之处在于,fastText预测标签,而CBOW模型预测中间词
fastText使用到两个tricks,一是通过构建一个霍夫曼编码树来加速softmax layer的计算,从而降低算法的时间复杂度,所以它在分类特别多的时候效果会更加明显。二是通过加入N-gram features进行补充,然后用hashing来减少N-gram的存储。