基础知识

Tensorflow

Bert4Keras 框架

bert4keras的总体架构由6个代码文件组成,主文件是models.py,其余的文件均为主文件服务,或者为下游任务的建模过程中服务。

  • models.py:主文件,主体Transformer类,其余的都是以Transformer为父类实现其算法,包括BERT,ALBERT,NEZHA,ELECTRA,GPT2_ML,T5的算法及其优化
  • layers.py: 实现各类功能的层,类似Keras中的layer,这里包括Embedding,MultiHeadAttention(多头注意力),LayerNormalization,PositionEmbedding(位置编码),RelativePositionEmbedding(相对位置编码),FeedForward等实现

bert4keras.backend

分离后端函数,主要是为了同时兼容原生keras和tf.keras

  • def set_gelu(version):设置gelu版本(由于gelu没有精确数学表达,只能近似拟合,gelu有两个拟合版本),根据传入的version,动态的选择gelu_tanh()和gelu_erf()。其中 assert version in [‘erf’, ‘tanh’]

bert4keras.layers

主要为定义自定义层,在model.py中使用

bert4keras.models

bert4keras.optimizers

bert4keras.snippets

bert4keras.tokenizers

tokenizers 为tokenizer相关工具,该文件内置了所有在生成token时可能使用到的工具。

1、load_vocab(dict_path, encoding=’utf-8’, simplified=False, startswith=None)

这个方法就是读取bert中的vovab.txt文件,返回一个字典,格式为{keyword:idx,keyword:idx}。

  • dict_path 为vovab.txt文件的路径。
  • encoding 为vovab.txt的编码方式。
  • simplified为True则开启精简词汇表模型,通过去除CJK类字符和标点符号,将之前的21128(bert-chinese)个词精简到13584个, 从而将词汇表变小(随之而来的就是embedding变小,通过将21128768切片成13584768),同时,返回token_dict和keep_tokens, token_dict负责生成token,keep_tokens则传入模型构建中,来对embedding进行切片。
  • startswith则为一个列表,在simplified为True时使用,用来保留一些特殊字符(比如[PAD], [UNK], [CLS], [SEP])。

example:

  1. # 加载并精简词表
  2. token_dict, keep_tokens = load_vocab(
  3. dict_path=dict_path,
  4. simplified=True,
  5. startswith=['[PAD]', '[UNK]', '[CLS]', '[SEP]'],
  6. )

2、save_vocab(dict_path, token_dict, encoding=’utf-8’)

用于保存词汇表(比如精简后的)。

参数 说明
dict_path 词汇表保存路径:str
token_dict 需要被保存的词汇表:dict
encoding 编码方式:str

3、class Tokenizer(TokenizerBase)

用于类Bert模型的tokenizer

3.1、def init( self, token_dict, do_lower_case=False, word_maxlen=200 token_start=’[CLS]’, token_end=’[SEP]’, pre_tokenize=None, token_translate=None, )

  • token_dict 可以是vacab.txt的路径,方法内调用load_vocab()获得词汇表;也可以是已加载好的字典 token_dict
  • do_lower_case是否全部转化为小写(bert case和uncase)。
  • word_maxlen单词最大长度,由于使用了细粒度拆次因此,词中词,比如北京大学中有北京 和 大学,定义最大长度,比如word_maxlen=3,则不对北京大学进行拆分。
  • token_starttoken开始标记(默认CLS)。
  • token_endtoken结束标记(默认SEP)。
  • pre_tokenize为一个预分词方法,可以是一个jieba分词等方法,用来先将句子分词然后做token。
  • token_translate为token替换字典,比如需要将所有的CLS替换为SEP({101:102})。

Tokenizer的两种初始化方式:

  1. dict_path = '/root/kg/bert/chinese_L-12_H-768_A-12/vocab.txt'
  2. # 第一种:直接传入一个vocab.txt的路径str
  3. tokenizer = Tokenizer(dict_path, do_lower_case=True)
  4. # 第二种
  5. token_dict, keep_tokens = load_vocab(dict_path=dict_path)
  6. tokenizer = Tokenizer(token_dict, do_lower_case=True)
  7. model = build_transformer_model(...,keep_tokens=keep_tokens,)

3.2、def encode( self, first_text, second_text=None, maxlen=None, pattern=’SEE’, truncate_from=’right’ )

该方法为Tokenizer的父类TokenizerBase中的方法,用于对文本进行编码(变成token id)

  • first_text 第一个文本(对应bert中nsp任务的两个句子,虽然并不一定是nsp任务)。
  • second_text第二个文本。
  • maxlen返回数据的最大长度,超过截断(比如bert base的512)。
  • pattern对于first text 和second text的拼接规则,S代表CLS,E代表SEP,代表两句话的拼接规则,比如默认的SEE则为CLS sentence 1 SEP sentence 2 SEP,而SESE 则为 CLS sentence 1 SEP CLS sentence 2 SEP。
  • truncate_from截断方向,对于超过最大长度的数据,截头部(left)还是截尾部(right)当然还支持通过一直删某个中间位置来截取(某一int值), 截取方法详见truncate_sequences

注意,由于bert4keras只提供了两句的分割,既“[CLS]sentence1[SEP]sentence2[SEP]”,如果用户有多句输入的需求,比如“[CLS]sentence1[SEP]sentence2[SEP]sentence3[SEP]”,原生bert4keras无法做到(因为encode的实现逻辑将“[SEP]”视为了多个字符),需要复写encode[支持更个性化的tokenize方式],或者只需要简单的复写tokenize即可
example:

  1. class CustomTokenizer(Tokenizer):
  2. def __init__(self, *args, **kwargs):
  3. super().__init__(*args, **kwargs)
  4. def tokenize(self, text, maxlen=None):
  5. """分词函数
  6. """
  7. tokens = [
  8. self._token_translate.get(token) or token
  9. for token in self._tokenize(text)
  10. ]
  11. for i in range(len(tokens)):
  12. # 将#转换为SEP,只有这里的SEP会被识别为分隔符
  13. if tokens[i] == '#':
  14. tokens[i] = '[SEP]'
  15. if self._token_start is not None:
  16. tokens.insert(0, self._token_start)
  17. if self._token_end is not None:
  18. tokens.append(self._token_end)
  19. if maxlen is not None:
  20. index = int(self._token_end is not None) + 1
  21. truncate_sequences(maxlen, -index, tokens)
  22. return tokens

3.3、def decode(self, ids, tokens=None)

解码器,将token变为可读的文本。

  • ids:encode之后(或模型输出)的token ids列表,例如[101,770,102],返回‘你’。
  • tokens:这个比较简单,当tokens不为None时,ids无效。方法变为将tokens的字符拼接、处理,作为可读文本返回。 例如[‘[CLS]’,’你’,’[SEP]’] 则会返回‘你’。

最后返回可读文本 text.

Bert4Keras 模型转onnx

https://blog.csdn.net/hailongzhang26/article/details/118937909

Bert4Keras 相关 issues 整理

如何正确地在vocab里人为增加一些special token? #408

苏神解答:

1、在vocab.txt后面追加你所需要的token,假设追加了16个; 2、build_transformer_model的时候,传入compound_tokens参数:compound_tokens是一个list,长度为16;list里边的每个item也是一个list,长度不定,里边的元素则是int,代表用这些int代表的token的embedding的平均来初始化新的token的embedding。 3、如果你的token跟字没有什么特殊关系,可以全部用[UNK]初始化就行。

修改源码方法示例 (参考自医学文本定向微调 wobert 权重):

  1. from bert4keras.tokenizers import Tokenizer, load_vocab
  2. from bert4keras.models import build_transformer_model
  3. # 建立分词器,添加特殊占位符
  4. token_dict = load_vocab(dict_path=dict_path)
  5. pure_tokenizer = Tokenizer(token_dict.copy(), do_lower_case=True) # 全小写
  6. user_dict = ["[pic]", "[know]", "[http]", "[num]"] # special token只能是小写的
  7. for special_token in user_dict:
  8. if special_token not in token_dict:
  9. token_dict[special_token] = len(token_dict)
  10. compound_tokens = [pure_tokenizer.encode(s)[0][1:-1] for s in user_dict]
  11. tokenizer = Tokenizer(token_dict, do_lower_case=True)
  12. model = build_transformer_model(
  13. config_path,
  14. checkpoint_path,
  15. ..
  16. compound_tokens=compound_tokens, # 增加词,用字平均来初始化
  17. )

tokenizer分词器无法将special token切开 #403 → 怎么做才可以把‘北京是[unused1]中国的首都’分成 ‘北 京 是 [unused1] 中 国 的 首 都’

苏神解答:

  1. from bert4keras.tokenizers import Tokenizer
  2. import re
  3. dict_path = '/root/kg/bert/chinese_L-12_H-768_A-12/vocab.txt'
  4. def pre_tokenize(text):
  5. """单独识别出[xxx]的片段
  6. """
  7. tokens, start = [], 0
  8. for r in re.finditer('\[[^\[]+\]', text):
  9. tokens.append(text[start:r.start()])
  10. tokens.append(text[r.start():r.end()])
  11. start = r.end()
  12. if text[start:]:
  13. tokens.append(text[start:])
  14. return tokens
  15. tokenizer = Tokenizer(
  16. dict_path, do_lower_case=True, pre_tokenize=pre_tokenize
  17. ) # 建立分词器

这样比较通用,但special token只能是小写的(因为do_lower_case=True了)