文章目录
- 1. 相关信息
- 2. 论文内容
- 3. 论文模型
- 3.1 Glyph Embedding
- 3.2 Pinyin Embedding
- 4. 实验与结论
- 5. 模型使用方式
1. 相关信息
论文年份:2021
论文地址:https://aclanthology.org/2021.acl-long.161.pdf
论文代码(官方) : https://github.com/ShannonAI/ChineseBert
Hugging Face: ShannonAI/ChineseBERT-base (560M+), ShannonAI/ChineseBERT-large (1.4G)
处理好后代码(自己处理的,详情参考最后章节模型使用方式):ChineseBert(百度网盘)
纯模型链接:
- ChineseBert-base(Google Drive): https://drive.google.com/file/d/1h5GqfVK_soKi5pXjDCPcCiH-SrbhIhdW/view?usp=share_link
2. 论文内容
论文思路和背景:目前中文BERT的做法和英文BERT一样,都是使用MLM任务和NSP任务进行训练的。但是,中文和英文不同,中文的拼音和字形也能为句子和词的语义提供信息。目前传统的做法忽略了这两个重要信息。所以作者就针对这一点,对BERT进行了改进,增加了这两种信息。
论文内容:
3. 论文模型
论文模型和传统的BERT一样,只是增加了字形编码(Glyph embedding)和拼音编码(Pinyin embedding)。
模型描述:首先,将每个token获取其"char embedding"、“glyph embedding”和"pinyin embedding"。然后将其Concat到一起,然后通过一个Fusion Layer(全连接层)将三种embedding进行融合。 之后就和普通BERT一样,增加position embedding,然后经过多层Transformer。最终得到每个token的hidden state。
fusion层核心代码如下:
# init
self.map_fc = nn.Linear(config.hidden_size * 3, config.hidden_size)
# forward
concat_embeddings = torch.cat((word_embeddings, pinyin_embeddings, glyph_embeddings), 2)
inputs_embeds = self.map_fc(concat_embeddings)
3.1 Glyph Embedding
对于字形编码(Glyph Embedding)的获取如下图所示:
作者使用的是一个字的三种书写方式的24x24的灰度图像作为输入图片,然后将其concat后送入全连接层进行特征提取,最终得到该字的Glyph Embedding。
例如上例中的猫
字,将经历如下步骤:
- 将
猫
字分别用“仿宋”、“行楷”和“隶书”三种方式绘制成24x24的灰度图片。最终得到一个24x24x3的tensor - 然后将其进行flatten操作,得到一个1728(24243)的tensor。(在论文中作者说是2352,应该是写错了)
- 最后将其通过一个全连接层进行特征提取。
作者对于字形的提取并没有使用卷积网络,可能是因为图片并不大,所以没必要。
虽然上面这么说,但作者的源码实现略有不同,但本质是一样的,作者源码如下:
class GlyphEmbedding(nn.Module):
"""Glyph2Image Embedding"""
def __init__(self, font_npy_files: List[str]):
super(GlyphEmbedding, self).__init__()
# font_arrays[i]存储了这个字的字形。 font_arrays[i].shape为(23236, 24, 24),其中23236是字典大小。数字范围为[0,255]
font_arrays = [
np.load(np_file).astype(np.float32) for np_file in font_npy_files
]
self.vocab_size = font_arrays[0].shape[0] # 字典大小,也就是23236
self.font_num = len(font_arrays) # 字体数量,三种字体:“仿宋”、“行楷”和“隶书”
self.font_size = font_arrays[0].shape[-1] # 图片大小,24.
# N, C, H, W
font_array = np.stack(font_arrays, axis=1) # 将三种字体组合到一起,font_array.shape为(23236, 3, 24, 24)
self.embedding = nn.Embedding( # 定义全连接层(Embedding和Linear本质是一样的)
num_embeddings=self.vocab_size,
embedding_dim=self.font_size ** 2 * self.font_num, # 将字编码成24*24*3大小的tensor
_weight=torch.from_numpy(font_array.reshape([self.vocab_size, -1]))
)
def forward(self, input_ids):
"""
get glyph images for batch inputs
Args:
input_ids: [batch, sentence_length]
Returns:
images: [batch, sentence_length, self.font_num*self.font_size*self.font_size]
"""
# return self.embedding(input_ids).view([-1, self.font_num, self.font_size, self.font_size])
return self.embedding(input_ids)
在上述Fusion层还有这么两行代码:
# init中定义的全连接层
self.glyph_map = nn.Linear(1728, config.hidden_size)
# forward中将1728维的字形特征映射到768维
glyph_embeddings = self.glyph_map(self.glyph_embeddings(input_ids)) # [bs,l,hidden_size]
3.2 Pinyin Embedding
与直觉相反,作者在Pinyin Embedding过程使用了卷积层(1维卷积),但Glyph Embedding却没有使用。
作者的思路如下图:
作者首先将字转换成拼音+音调,由于不同的字拼音长度不同,所以作者将长度固定为8,不足补0。然后将获取到的token编码成8个128维的向量。之后使用1维卷积(kernal_size=2, stride=1)对这8个向量进行卷积操作,最终会得到7个768维的输出向量。最后使用max_pool选择出一个最终的特征向量作为该字的pinyin embedding。
之所以使用Conv来完成拼音特征的提取,作者说是因为拼音长度不固定,为了避免补0给特征提取带来影响,所以使用卷积。
代码如下:
class PinyinEmbedding(nn.Module):
def __init__(self, embedding_size: int, pinyin_out_dim: int, config_path):
"""
Pinyin Embedding Module
Args:
embedding_size: the size of each embedding vector
pinyin_out_dim: kernel number of conv
"""
super(PinyinEmbedding, self).__init__()
with open(os.path.join(config_path, 'pinyin_map.json')) as fin:
pinyin_dict = json.load(fin)
self.pinyin_out_dim = pinyin_out_dim # 要将token编码成的向量维度,例如768。
# Embedding(32, 128)。其中32为6+26:6种音调, 26个英文字母。128为将一个拼音中字母(或音调)编码成128维的向量
self.embedding = nn.Embedding(len(pinyin_dict['idx2char']), embedding_size)
# 卷积层,输入通道数为128,输出通道数为768。
self.conv = nn.Conv1d(in_channels=embedding_size, out_channels=self.pinyin_out_dim, kernel_size=2,
stride=1, padding=0)
def forward(self, pinyin_ids):
"""
Args:
pinyin_ids: (batch_size, sentence_length, 8), sentence_length包含101和102, 8是固定长度(拼音+音调+不足补0)。
Returns:
pinyin_embed: (bs,sentence_length,pinyin_out_dim)
"""
# 将每个字母(或音调或[PAD])编码成128维的向量。embed.shape为[bs,sentence_length,8,embed_size],例如(1, 6, 8, 128)。
embed = self.embedding(pinyin_ids) # [bs,sentence_length,pinyin_locs,embed_size]
bs, sentence_length, pinyin_locs, embed_size = embed.shape
# 为了进行后续卷积,将batch_size和sentence_length合并。然后embed_size提前。
view_embed = embed.view(-1, pinyin_locs, embed_size) # [(bs*sentence_length),pinyin_locs,embed_size]
input_embed = view_embed.permute(0, 2, 1) # [(bs*sentence_length), embed_size, pinyin_locs]
# conv + max_pooling # 卷积+max_pooling操作
pinyin_conv = self.conv(input_embed) # [(bs*sentence_length),pinyin_out_dim,H]
pinyin_embed = F.max_pool1d(pinyin_conv, pinyin_conv.shape[-1]) # [(bs*sentence_length),pinyin_out_dim,1]
return pinyin_embed.view(bs, sentence_length, self.pinyin_out_dim) # [bs,sentence_length,pinyin_out_dim]
4. 实验与结论
作者在6种任务上进行了实验,要么和其他BERT打平,要么就是赢,反正就是效果挺不错的。详情可以参考作者代码或原论文
5. 模型使用方式
ChineseBert并不能直接像其他Huggging Face的Model直接从使用transformers代码加载,需要一些特殊操作。
我的处理步骤如下:
- 首先在项目下新建
ChineseBert
目录用于存放作者代码。 - 将作者的代码从Github上下载下来,将
datasets
,models
,utils
三个目录放入ChineseBert
目录下 - 从HuggingFace上下载
ChineseBERT-base
模型,放入ChineseBert/model
目录下。(注意:不能使用作者提供的Google Drive下载,那个文件有问题) - 将左右的
from models.*
改为from ChineseBert.models.*
。因为我在作者的基础上外面包了一层ChineseBert
。 - 将
ChineseBert
与其下的所有的文件夹加入__init__.py
文件,让它们都由普通文件夹变成Python Package
- 将所有的
from transformers.modeling_bert.*
改为from transformers.models.bert.modeling_bert.*
,因为高版本的transformers改变了一些类的路径
可以直接使用我封装好的代码:ChineseBert(百度网盘)
当上面都做完后,就可以将ChineseBert作为一个第三方依赖进行调用了,样例代码如下:
from ChineseBert.datasets.bert_dataset import BertDataset
from ChineseBert.models.modeling_glycebert import GlyceBertForMaskedLM
tokenizer = BertDataset("./ChineseBert/model/ChineseBERT-base")
chinese_bert = GlyceBertForMaskedLM.from_pretrained("./ChineseBert/model/ChineseBERT-base")
sentence = '我喜欢猫'
input_ids, pinyin_ids = tokenizer.tokenize_sentence(sentence)
length = input_ids.shape[0]
input_ids = input_ids.view(1, length)
pinyin_ids = pinyin_ids.view(1, length, 8)
output_hidden = chinese_bert.forward(input_ids, pinyin_ids)[0]
print(output_hidden.size())
print(output_hidden)
输出为:
torch.Size([1, 6, 23236])
tensor([[[ -8.6706, -8.5349, -8.4511, ..., -9.3002, -10.3638, -9.6329],
[ -7.4224, -7.6068, -7.6662, ..., -9.5153, -8.1475, -9.8946],
[-11.5929, -10.2694, -11.1009, ..., -13.2361, -12.7843, -17.3548],
[-11.8149, -10.0489, -10.2216, ..., -12.4247, -14.1545, -18.0415],
[ -6.2827, -5.1745, -7.0772, ..., -6.9521, -7.4132, -5.3711],
[ -8.6707, -8.5348, -8.4511, ..., -9.3002, -10.3638, -9.6329]]],
grad_fn=<ViewBackward0>)