最近着手开发一个基于多模态大模型的泰国文档关键信息提取算法,在前期进行数据分析和tokenization实验的过程中遇到一些比较有趣的问题,这是在做中文或者英文相关任务中是没有遇到过的。
众所周知,像中文字符或者英文字母,无论笔画有多少、无论构成多么复杂,一个字符其长度就是1,但是泰文不一样,主要跟泰语的书写系统和字符的组合方式有关,所以记录一下。
事情的起因是这样的,当我在对比模型预测结果跟真实标注结果时(比如泰文中姓名或者地址),发现两个字符串一模一样,可是却又不相等,所以尝试一个个字符对比将不同的字符和索引打印出来,通过索引获取的字符好像原串中又看不到,所以引起极大的探索欲望。
比如下面这个例子,a和b看上去很像吧,但是却不相同,所以将不同的字符和所在索引打印出来,然后取得这个字符。
这是结果:
上面出现的那个字符很奇怪,我的a和b中没有啊,只是某个字符的组成部分,然后再通过索引和分片方式打印出来看看:
网上搜了才知道,泰文(Thai)与英语等拉丁字母语言不同,它是一种音节型语言,由多个组件构成单个字符或音节,具体来说,泰语是一种合成型(或称为字母组合型)语言,这意味着一些字符在视觉上是由多个符号或符号的组合构成的。
泰文的一个可见字符可能由多个 Unicode 码点(code points)组成,这就导致 len() 计算的长度可能大于 1,在 Python 中,len() 计算的是 Unicode 码点的数量,而泰文的一个可见字符可能由多个码点组成,泰文的元音和声调符号在 Unicode 中属于组合字符,它们不会单独占据一个字母位置,但在Python的 len() 计算中,每个 Unicode 码点都算一个单位。
例如:
len("ก") → 1(单个辅音)
len("กิ") → 2(辅音 ก + 元音 ิ)
len("กิ่") → 3(辅音 ก + 元音 ิ + 声调符号 ่)
所以进一步了解了泰文的书写系统和字符的组合方式。
泰语由 44 个辅音字母 和 15 个元音符号 组成,但元音在拼写时可以分布在辅音的上、下、左、右。因此,需要包括:
- 泰文辅音(เช่น ก, ข, ค, ง)
- 泰文元音(เช่น า, ิ, ึ, ื, ะ, เ, โ)
- 声调符号(่, ้, ๊, ๋)
- 独立元音(อ)
- 其他特殊字符(ๆ, ฯ, ี, ำ)
完整的常用泰文字符集可以参考 Unicode 范围:
- 辅音:U+0E01 - U+0E2E
- 元音 & 变音符号:U+0E30 - U+0E3A
- 声调符号:U+0E48 - U+0E4E
- 泰文数字:U+0E50 - U+0E59(如果需要识别泰文数字)
- 基本字母和附加符号
在泰文中,基础字母(例如:ม)可以与附加符号(如声调符号、元音符号、复合辅音)结合,形成一个复杂字符或音节。例如,ม ("m") 和附加的元音符号或声调符号(如 ู 和 ่)组合后仍然是同一个音节:มู่ 由两个符号组成:ม(字母 "m")和 ู่(由两部分组成的元音符号和声调符号) - 元音符号
泰语的元音通常不是独立字符,而是与辅音字符结合来表达音节。 元音可以出现在辅音字符的前面、后面、上方或下方,甚至嵌在字符内部。 例如:ม ("m") 字母可以与不同的元音符号(如 ู、ิ、ื 等)组合来形成不同的音节。 - 复合辅音
有时一个音节由多个辅音组合而成,这些辅音可以是并列的或结合在一起发音。 例如,泰语有一些复合辅音(例如: ทร),它是由两个字母并组成的。 - 声调符号
泰语是一个有声调的语言,不同的声调会改变单词的意思。声调通常通过附加符号表示,这些符号可以出现在基础字符的上方或下方。一个泰语字符可能需要与这些附加的声调符号结合,才能表达正确的发音和含义。
示例:มู่ ("mū") 是由以下部分构成的:
- ม:一个泰文辅音字母,代表 /m/ 音。
- ู่:由两部分组成的元音符号和声调符号,表示元音的发音和调调。
综上: 泰语是一个以音节为单位的语言,每个音节可以由辅音字母、元音符号和声调符号的组合构成,因此会出现一个字符由多个部分组成的现象。
所以,在构造泰文字符集时,辅音、元音、声调符号是必须的,另外,空格、标点、数字、英文字符可以根据实际需要添加。
这是一个泰文字符集:
thai_chars = [ "ก", "ข", "ฃ", "ค", "ฅ", "ฆ", "ง", "จ", "ฉ", "ช", "ซ", "ฌ", "ญ", "ฎ", "ฏ", "ฐ", "ฑ", "ฒ", "ณ", "ด", "ต", "ถ", "ท", "ธ", "น", "บ", "ป", "ผ", "ฝ", "พ", "ฟ", "ภ", "ม", "ย", "ร", "ล", "ว", "ศ", "ษ", "ส", "ห", "ฬ", "อ", "ฮ", "ะ", "ั", "า", "ำ", "ิ", "ี", "ึ", "ื", "ุ", "ู", "เ", "แ", "โ", "ใ", "ไ", "ๅ", "ๆ", "็", "่", "้", "๊", "๋", "์", "ํ", "ฺ", "ฯ", "๐", "๑", "๒", "๓", "๔", "๕", "๖", "๗", "๘", "๙"]
# 构造字符到索引的映射
char2idx = {char: idx for idx, char in enumerate(thai_chars)}
idx2char = {idx: char for char, idx in char2idx.items()}
# 可以英文字母和一些特殊字符,根据实际需要即可
thai_extended_chars = thai_chars + list("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ .,!?")
char2idx_extended = {char: idx for idx, char in numerate(thai_extended_chars)}
搞清楚了泰文的构成方法,还需要处理的问题是已有的tokenizer虽然号称支持100多种语言,但是某些泰文字符还是<unk>,也就是在训练集中没有,出现未知字符的问题,所以需要换一个能够处理泰文的tokenizer,这里试了四个:
google/byt5-smallgoogle/mt5-basesambanovasystems/SambaLingo-Thai-Baseairesearch/wangchanberta-base-att-spm-uncased
各有优劣吧。在测试字符串上面试了一下,这个测试字符串包括数字、英文字母大小写、泰文字符和一些特殊字符,基本能够覆盖文档中的字符集。
下面是分别进行的测试,注意不仅要能encode,还要能够decode回去。
1、google/byt5-small
这是Google开发的一种基于字符的 SentencePiece 模型,字符级 SentencePiece。
2、google/mt5-base
Google 的 mT5 训练了一个 支持100+种语言(包括泰文和英文)的 SentencePiece Tokenizer。
3、sambanovasystems/SambaLingo-Thai-Base
SambaLingo-Thai-Base 是由 SambaNova Systems 开发的双语(泰语和英语)预训练模型。该模型通过在 Cultura-X 数据集的泰语部分进行训练,处理了 380 亿个标记(tokens),将 Llama-2-7b 模型适配于泰语。SambaLingo-Thai-Base 可用于多种任务,包括文本生成、翻译和对话。
4、airesearch/wangchanberta-base-att-spm-uncased
WangchanBERTa 是由泰国 VISTEC-depa 人工智能研究所开发的泰语预训练语言模型,基于 RoBERTa 架构。其中,wangchanberta-base-att-spm-uncased 是该模型的主要版本,专为泰语自然语言处理任务设计。该模型在一个包含 78.5GB 未压缩文本的多样化泰语语料库上进行预训练,语料来源包括社交媒体帖子、新闻文章和其他公开可用的数据集。
使用 SentencePiece 子词分词器,词汇量为 25,000 个子词单元,适应泰语中词与词之间通常不以空格分隔的特点。由于泰语本身不区分大小写,模型在训练时未考虑大小写差异。
看到了吧,token--> string 和 id --> token --> string 的结果不一样,因为这个tokenizer不支持大写字母,会变成unk,这从tokens = tokenizer.tokenize(s)看不出来,在将id转换为token的时候就看出来了,也就是tokenizer.convert_ids_to_tokens(ids)。
所以得小心不是。