FastText4j相关字符编码问题

fastText4j

Posted by Nova on 2018-09-15

背景

自2016年facebook开源 文本分类工具以来. 为实现工程上快速方便使用, 急需java版本的fasttext工具.

调研之后fasttext4j是最为方便和准确的选择, 但存在一个问题: 中文文本的预测部分出现偏差. 因此对fasttext4j进行改造.

编码

编码问题主要是在于数据在处理过程中,到底加载成什么样子,是不是期望的01长度序列,或者字节长度.

比如: 对于输入字符串(或者一句话)的处理,每一个字符到底占用多少个字节.

编码不一致导致问题出现的根源在于一下几个方面:

1
2
1. 输入的字符串本身的编码和读取时指定的编码(将byte数组转出String)不一致
2. 一些程序处理时用的统一编码方式,所以会进行转换,这里的规则和流程是否清楚

比如在Java中,内部使用unicode统一处理,但外部文件的编码格式不一.

UNICODE、UTF8、GBK

说到unicode,详细解释下.

1
2
Unicode 是「字符集」
UTF-8 是「编码规则」

广义的 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
2
1. 对于单字节的符号,字节的第一位设为0,后面7位为这个符号的 Unicode 码。因此对于英语字母,UTF-8 编码和 ASCII 码是相同的。
2. 对于n字节的符号(n > 1),第一个字节的前n位都设为1,第n + 1位设为0,后面字节的前两位一律设为10。剩下的没有提及的二进制位,全部为这个符号的 Unicode 码。

下表总结了编码规则,字母x表示可用编码的位。

1
2
3
4
5
Unicode符号范围(十六进制)    | UTF-8编码方式(二进制)
0000 0000 - 0000 007F | 0xxxxxxx
0000 0080 - 0000 07FF | 110xxxxx 10xxxxxx
0000 0800 - 0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx
0001 0000 - 0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx

跟据上表,解读 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
2
3
4
5
// true: 表示 字节c 不构成字符.  false: 表示字节c构成字符. 原理见UTF-8编码规则. 多字节字符的低位字节前两位都是 10. 所以通过掩码筛选. 
protected boolean charMatches(char c) {
return (c & 0xC0) == 0x80;
}
// 问题 在于 java 中 char 本身就是一个字符, 不需要筛选剔除掉不成字符的字节. 所以此方法返回false即可. 否则, 像 '安' (101101110001001)【Java编译器默认使用Unicode编码】 这样的字符丢失了子字特征.

将会过滤掉一些中文字符,导致一些特征丢失,出现预测结果很大不一样.

Unicode 和 UTF-8 有什么区别?

unicode编码是占用几个字节?

ASCII,Unicode和UTF-8终于找到一个能完全搞清楚的文章了

总结一下

  1. c++版本的fasttext 针对多字节的字符 做了支持: c++ 中的char代表一个字节。 然后通过掩码与运算 跳过了不成字符的字节 来提取子字ngram特征(字符级ngram)
  2. 然而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的存储。

使用fasttext进行文档分类

参考

Java基本类型占用的字节数

基于fastText的意图识别框架

短文本分类和长文本分类的模型如何进行选择?