【论文解读】(拼音+字形+字三种信息的中文BERT)ChineseBERT: Chinese Pretraining Enhanced by Glyph and Pinyin Information

文章目录

  • 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)ChineseBERT: Chinese Pretraining Enhanced by Glyph and Pinyin Information_第1张图片

论文模型和传统的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)的获取如下图所示:

【论文解读】(拼音+字形+字三种信息的中文BERT)ChineseBERT: Chinese Pretraining Enhanced by Glyph and Pinyin Information_第2张图片

作者使用的是一个字的三种书写方式的24x24的灰度图像作为输入图片,然后将其concat后送入全连接层进行特征提取,最终得到该字的Glyph Embedding。

例如上例中的字,将经历如下步骤:

  1. 字分别用“仿宋”、“行楷”和“隶书”三种方式绘制成24x24的灰度图片。最终得到一个24x24x3的tensor
  2. 然后将其进行flatten操作,得到一个1728(24243)的tensor。(在论文中作者说是2352,应该是写错了)
  3. 最后将其通过一个全连接层进行特征提取。

作者对于字形的提取并没有使用卷积网络,可能是因为图片并不大,所以没必要。

虽然上面这么说,但作者的源码实现略有不同,但本质是一样的,作者源码如下:

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却没有使用。

作者的思路如下图:

【论文解读】(拼音+字形+字三种信息的中文BERT)ChineseBERT: Chinese Pretraining Enhanced by Glyph and Pinyin Information_第3张图片

作者首先将字转换成拼音+音调,由于不同的字拼音长度不同,所以作者将长度固定为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代码加载,需要一些特殊操作。

我的处理步骤如下:

  1. 首先在项目下新建ChineseBert目录用于存放作者代码。
  2. 将作者的代码从Github上下载下来,将datasetsmodelsutils 三个目录放入ChineseBert目录下
  3. 从HuggingFace上下载ChineseBERT-base模型,放入ChineseBert/model目录下。(注意:不能使用作者提供的Google Drive下载,那个文件有问题)
  4. 将左右的from models.* 改为 from ChineseBert.models.*。因为我在作者的基础上外面包了一层ChineseBert
  5. ChineseBert与其下的所有的文件夹加入__init__.py文件,让它们都由普通文件夹变成Python Package
  6. 将所有的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=)

你可能感兴趣的:(机器学习,bert,python,深度学习)