大數據文摘出品
來源:Google Colab
編譯:武帥、曹培信
2018年10月,Google AI團隊推出了Bert,可以說Bert一出生就自帶光環。
在史丹福大學機器閱讀理解水平測試SQuAD1.1中,Bert在全部兩個衡量指標上,全面超越人類表現。並且在另外11種不同NLP測試中均創造了歷史以來最好成績,將GLUE基準提升7.6%,將MultiNLI的準確率提提升5.6%。
然而這個擁有12層神經網絡的「多頭怪」(這裡指BERT-Base,BERT-Large有24層),在4個 Cloud TPU 上需要訓練 4 天(BERT-Large需要16個Cloud TPU),如此高的訓練成本讓許多想嘗試的同學望而卻步。
不過,谷歌也給廣大程式設計師帶來了福音!我們可以藉助谷歌雲TPU訓練Bert了!並且只需要花費1美元,在Google Colab上還出了完整的教程!
快跟文摘菌一起薅谷歌羊毛!
在本次實驗中,我們將藉助谷歌雲,在任意文本數據上預訓練當下最先進的NLP模型—BERT。
BERT模型起源:
https://arxiv.org/abs/1810.04805?source=post_page
本指南包含了模型預訓練的所有階段,包括:
搭建訓練環境下載原始文本數據文本數據預處理學習新詞彙表切分預訓練數據將數據和模型存儲到谷歌雲在雲TPU上訓練模型
先回答幾個問題
這份指南有什麼用?
藉助本指南,你可以在任意文本數據上訓練BERT模型。特別是當開源社區沒有你需要的語言或示例的預訓練模型時,它會幫助到你。
誰需要這份指南?
這份指南適用於對BERT感興趣但對當前可用的開源模型的性能並不滿意的NLP研究人員。
我該如何開始?
要想長時間地保存訓練數據和模型,你需要一個谷歌雲端存儲分區(Google Cloud Storage Bucket,GCSB)。請按照這份谷歌雲TPU快速入門指南創建一個谷歌雲平臺帳戶和谷歌雲存儲分區。
谷歌雲TPU快速入門指南:
https://cloud.google.com/tpu/docs/quickstart?source=post_page
每一個谷歌雲的新用戶都可獲得300美元的免費金額。
連結:
https://cloud.google.com/free/?source=post_page
本教程的1到5步出於演示目的,在沒有谷歌雲存儲的情況下也能進行。但是,在這種情況下,你將無法訓練模型。
需要什麼?
在第二代TPU(TPUv2)上預訓練一個BERT模型大約需要54小時。Google Colab並不是為執行此類需要長時間運行的任務而設計的,它每隔8小時便會中斷訓練過程。因此,為了訓練過程不被中斷,你需要使用付費的搶佔式的TPUv2。
譯者註:Google Colab,谷歌免費提供的用於機器學習的平臺
相關連結:
https://www.jianshu.com/p/000d2a9d36a0
譯者註:搶佔式,一種進程調度方式,允許將邏輯上可繼續運行的進程暫停,適合通用系統。
相關連結:
https://blog.csdn.net/qq_34173549/article/details/79936219
也就是說,在撰寫本文時(2019年5月9日),通過Google Colab提供的一塊TPU,花費大約1美元,就可以在谷歌雲上存儲所需要的數據和模型,並預訓練一個BERT模型。
我該如何遵循指南?
下面的代碼是Python和Bash的組合。它在Colab Jupyter環境中運行。因此,它可以很方便地在那裡運行。
然而,除了實際的模型訓練部分之外,本指南列出的其他步驟都可以在單獨的機器上運行。特別是當你的數據集過大或者十分私密而無法在Colab環境中進行預處理時,這就顯得十分有用了。
好的,給我看看代碼
代碼連結:
https://colab.research.google.com/drive/1nVn6AFpQSzXBt8_ywfx6XR8ZfQXlKGAz?source=post_page#scrollTo=ODimOhBR05yR
我需要修改代碼嗎?
代碼中唯一需要你修改的地方就是谷歌雲存儲的帳戶名。其他的地方默認就好。
還有別的麼?
說句題外話,除了這個程序,我還發布了一個訓練好的俄羅斯語BERT模型。
下載連結:
https://storage.googleapis.com/bert_resourses/russian_uncased_L-12_H-768_A-12.zip?source=post_page
我希望相關研究人員可以發布其他語言的預訓練模型。這樣就可以改善我們每個人的NLP環境。現在,讓我們進入正題吧!
第1步:搭建訓練環境
首先,我們導入需要用到的包。
在Jupyter Notebook中可以通過使用一個感嘆號『!』直接執行bash命令。如下所示:
!pip install sentencepiece!git clone https://github.com/google-research/bert
整個演示過程我將會用同樣的方法使用幾個bash命令。
現在,讓我們導入包並在谷歌雲中自行授權。
import osimport sysimport jsonimport nltkimport randomimport loggingimport tensorflow as tfimport sentencepiece as spmfrom glob import globfrom google.colab import auth, drivefrom tensorflow.keras.utils import Progbarsys.path.append("bert")from bert import modeling, optimization, tokenizationfrom bert.run_pretraining import input_fn_builder, model_fn_builderauth.authenticate_user()# configure logginglog = logging.getLogger('tensorflow')log.setLevel(logging.INFO)# create formatter and add it to the handlersformatter = logging.Formatter('%(asctime)s : %(message)s')sh = logging.StreamHandler()sh.setLevel(logging.INFO)sh.setFormatter(formatter)log.handlers = [sh]if'COLAB_TPU_ADDR'in os.environ: log.info("Using TPU runtime") USE_TPU = True TPU_ADDRESS = 'grpc://' + os.environ['COLAB_TPU_ADDR']with tf.Session(TPU_ADDRESS) as session: log.info('TPU address is ' + TPU_ADDRESS)# Upload credentials to TPU.with open('/content/adc.json', 'r') as f: auth_info = json.load(f) tf.contrib.cloud.configure_gcs(session, credentials=auth_info)else: log.warning('Not connected to TPU runtime') USE_TPU = FalseSetting up BERT training environment
搭建BERT訓練環境
第2步:獲取數據
接下來我們獲取文本數據語料庫。這次實驗我們將採用OpenSubtitles 數據集。
連結:
https://www.opensubtitles.org/en/?source=post_page
該數據集有65種語言可以使用。
連結:
http://opus.nlpl.eu/OpenSubtitles-v2016.php?source=post_page
與更常用的文本數據集(如維基百科)不同,該數據集並不需要進行任何複雜的數據預處理。它也預先格式化了,每行一個句子,便於後續處理。
你也可以通過設置相應的語言代碼來使用該數據集。
AVAILABLE = {'af','ar','bg','bn','br','bs','ca','cs','da','de','el','en','eo','es','et','eu','fa','fi','fr','gl','he','hi','hr','hu','hy','id','is','it','ja','ka','kk','ko','lt','lv','mk','ml','ms','nl','no','pl','pt','pt_br','ro','ru','si','sk','sl','sq','sr','sv','ta','te','th','tl','tr','uk','ur','vi','ze_en','ze_zh','zh','zh_cn','zh_en','zh_tw','zh_zh'}LANG_CODE = "en" #@param {type:"string"}assert LANG_CODE in AVAILABLE, "Invalid language code selected"!wget http://opus.nlpl.eu/download.php?f=OpenSubtitles/v2016/mono/OpenSubtitles.raw.'$LANG_CODE'.gz -O dataset.txt.gz!gzip -d dataset.txt.gz!tail dataset.txt
下載OPUS數據
出於演示目的,我們默認只使用語料庫的一小部分。
在實際訓練模型時,請務必取消選中DEMO_MODE複選框以使用大100倍的數據集。
請放心,一億行語句足以訓練出一個相當不錯的BERT模型。
DEMO_MODE = True #@param {type:"boolean"}if DEMO_MODE: CORPUS_SIZE = 1000000else: CORPUS_SIZE = 100000000 #@param {type: "integer"}!(head -n $CORPUS_SIZE dataset.txt) > subdataset.txt!mv subdataset.txt dataset.txt
拆分數據集
第3步:文本預處理
我們下載的原始文本數據包含了標點符號,大寫字母以及非UTF編碼的符號,這些都需要提前刪除。在模型推斷時,我們也需要對新數據集採取同樣的做法。
如果你的用例需要不同的預處理方法(例如在模型推斷時大寫字母或者標點符號是需要保留的),那麼就修改代碼中的函數以滿足你的需求。
regex_tokenizer = nltk.RegexpTokenizer("\w+")defnormalize_text(text):# lowercase text text = str(text).lower()# remove non-UTF text = text.encode("utf-8", "ignore").decode()# remove punktuation symbols text = " ".join(regex_tokenizer.tokenize(text))return textdefcount_lines(filename): count = 0with open(filename) as fi:for line in fi: count += 1return count
定義預處理例程
現在讓我們對整個數據集進行預處理吧。
RAW_DATA_FPATH = "dataset.txt"#@param {type: "string"}PRC_DATA_FPATH = "proc_dataset.txt"#@param {type: "string"}# apply normalization to the dataset# this will take a minute or twototal_lines = count_lines(RAW_DATA_FPATH)bar = Progbar(total_lines)with open(RAW_DATA_FPATH,encoding="utf-8") as fi: with open(PRC_DATA_FPATH, "w",encoding="utf-8") as fo: for l in fi: fo.write(normalize_text(l)+"\n") bar.add(1)
應用預處理
第4步:構建詞彙表
下一步,我們將學習一個新的詞彙表,用於表示我們的數據集。
因為 BERT 論文中使用了谷歌內部未開源的 WordPiece 分詞器,因此,這裡我們只能使用一元文法模式(unigram mode)下開源的 SentencePiece 分詞器了。
連結:
https://github.com/google/sentencepiece?source=post_page
雖然它與BERT並不直接兼容,但我們可以通過一個小技巧讓它工作。
SentencePiece需要相當多的運行內存(RAM),因此在Colab上運行整個數據集會導致內核崩潰。為避免這一情況發生,我們將隨機地對數據集的一小部分進行子採樣,從而構建詞彙表。當然,也可以使用運行內存更大的計算機來執行此步驟。這完全取決於你。
此外,SentencePiece默認將BOS和EOS控制符號添加到詞彙表中。我們可以通過手動地把它們的詞id設為-1來禁用它們。
VOC_SIZE的典型值介於32000到128000之間。我們將保留NUM_PLACEHOLDERS標記,以防有人想在訓練前的階段完成後更新詞彙和微調模型。
MODEL_PREFIX = "tokenizer" #@param {type: "string"}VOC_SIZE = 32000 #@param {type:"integer"}SUBSAMPLE_SIZE = 12800000 #@param {type:"integer"}NUM_PLACEHOLDERS = 256 #@param {type:"integer"}SPM_COMMAND = ('--input={} --model_prefix={} ''--vocab_size={} --input_sentence_size={} ''--shuffle_input_sentence=true ''--bos_id=-1 --eos_id=-1').format( PRC_DATA_FPATH, MODEL_PREFIX, VOC_SIZE - NUM_PLACEHOLDERS, SUBSAMPLE_SIZE)spm.SentencePieceTrainer.Train(SPM_COMMAND)
學習SentencePiece詞彙表
現在,看看我們是如何使SentencePiece在BERT模型中工作的。
下面是官方倉庫的一個英語BERT預訓練模型中通過WordPiece詞彙表標記後的語句。
連結:
https://github.com/google-research/bert?source=post_page
模型下載:
https://storage.googleapis.com/bert_models/2018_10_18/uncased_L-12_H-768_A-12.zip?source=post_page
>>> wordpiece.tokenize("Colorless geothermal substations are generating furiously")['color','##less','geo','##thermal','sub','##station','##s','are','generating','furiously']
我們看到,WordPiece分詞器在兩個單詞中間以「##」在前的形式預設了子詞。單詞開頭的子詞並沒有發生變化。如果子詞出現在開頭和單詞的中間,則兩個版本(帶或不帶『##』)都會添加到詞彙表中。
SentencePiece創建了兩個文件:tokenizer.model and tokenizer.vocab。讓我們來看看學到的詞彙:
defread_sentencepiece_vocab(filepath): voc = []with open(filepath, encoding='utf-8') as fi:for line in fi: voc.append(line.split("\t")[0])# skip the first <unk> token voc = voc[1:]return vocsnt_vocab = read_sentencepiece_vocab("{}.vocab".format(MODEL_PREFIX))print("Learnt vocab size: {}".format(len(snt_vocab)))print("Sample tokens: {}".format(random.sample(snt_vocab, 10)))
讀取學習後的SentencePiece詞彙表
給出結果:
Learnt vocab size: 31743Sample tokens: ['▁cafe', '▁slippery', 'xious', '▁resonate', '▁terrier', '▁feat', '▁frequencies', 'ainty', '▁punning', 'modern']
我們觀察到,SentencePiece和WordPiece給出的結果完全相反。從這篇文檔中可以看出SentencePiece首先用元符號「_」(UnicodeMath編碼字符:U+2581)替換掉空格,如 「Hello World」被替換為成:
Hello▁World.
連結:
https://github.com/google/sentencepiece/blob/master/README.md?source=post_page
然後,此文本被分割為小塊,如下所示:
[Hello] [▁Wor] [ld] [.]
在空格之後出現的子詞(也是大多數單詞的開頭)通常前面加上了 『_』,而其它的並沒有變化。這不包括那些僅出現在句子開頭而不是其他地方的子詞。然而,這些情況很少發生。
因此,為了獲得類似於WordPiece的詞彙表,我們需要進行一個簡單的轉換,將那些『_』符號刪除並將『##』符號添加到不含它的標記中。
我們還添加了一些BERT架構所需要的特殊控制符號。按照慣例,我們把它們放在了詞彙表的開頭。
此外,我們也在詞彙表中添加了一些佔位符標記。
如果某人希望用新的特定任務的標記來更新模型時,以上那些做法就十分有用了。此時,將原先的佔位符標記替換為新的標記,預訓練數據就會重新生成,模型也會在新數據上進行微調。
defparse_sentencepiece_token(token):if token.startswith("▁"):return token[1:]else:return"##" + tokenbert_vocab = list(map(parse_sentencepiece_token, snt_vocab))ctrl_symbols = ["[PAD]","[UNK]","[CLS]","[SEP]","[MASK]"]bert_vocab = ctrl_symbols + bert_vocabbert_vocab += ["[UNUSED_{}]".format(i) for i in range(VOC_SIZE - len(bert_vocab))]print(len(bert_vocab))
轉換詞彙表以用於BERT
最後,我們將獲得的詞彙表寫入文件。
VOC_FNAME = "vocab.txt" #@param {type:"string"}with open(VOC_FNAME, "w") as fo:for token in bert_vocab: fo.write(token+"\n")
將詞彙表寫入文件
現在,讓我們看看新詞彙表在實踐中是如何運作的:
>>> testcase = "Colorless geothermal substations are generating furiously">>> bert_tokenizer = tokenization.FullTokenizer(VOC_FNAME)>>> bert_tokenizer.tokenize(testcase)['color','##less','geo','##ther','##mal','sub','##station','##s','are','generat','##ing','furious','##ly']
第5步:生成預訓練數據
藉助於手頭的詞彙表,我們已經可以生成BERT模型的預訓練數據了。因為我們的數據集可能非常大,所以我們將其切分:
mkdir ./shardssplit -a 4 -l 256000 -d $PRC_DATA_FPATH ./shards/shard_
切分數據集
現在,對於每個切片,我們需要從BERT倉庫中調用create_pretraining_data.py 腳本。為此,我們使用xargs命令。
在我們開始生成數據前,我們需要設置一些參數並傳遞給腳本。你可以在README文檔中找到有關它們含義的更多信息。
連結:
https://github.com/google-research/bert/blob/master/README.md?source=post_page
MAX_SEQ_LENGTH = 128 #@param {type:"integer"}MASKED_LM_PROB = 0.15 #@paramMAX_PREDICTIONS = 20 #@param {type:"integer"}DO_LOWER_CASE = True #@param {type:"boolean"}PRETRAINING_DIR = "pretraining_data" #@param {type:"string"}# controls how many parallel processes xargs can createPROCESSES = 2 #@param {type:"integer"}
定義預訓練數據的參數
運行這一步可能需要相當長的時間,具體取決於數據集的大小。
XARGS_CMD = ("ls ./shards/ | ""xargs -n 1 -P {} -I{} ""python3 bert/create_pretraining_data.py ""--input_file=./shards/{} ""--output_file={}/{}.tfrecord ""--vocab_file={} ""--do_lower_case={} ""--max_predictions_per_seq={} ""--max_seq_length={} ""--masked_lm_prob={} ""--random_seed=34 ""--dupe_factor=5")XARGS_CMD = XARGS_CMD.format(PROCESSES, '{}', '{}', PRETRAINING_DIR, '{}', VOC_FNAME, DO_LOWER_CASE, MAX_PREDICTIONS, MAX_SEQ_LENGTH, MASKED_LM_PROB)tf.gfile.MkDir(PRETRAINING_DIR)!$XARGS_CMD
創建預訓練數據
第6步:創建持久化存儲
為了保存我們來之不易的財產,我們將其保存在谷歌雲存儲上。如果你已經創建了谷歌雲存儲分區,那麼這是很容易實現的。
我們將在谷歌雲存儲上設置兩個目錄:一個用於數據,一個用於模型。在模型目錄下,我們將放置模型詞彙表和配置文件。
在繼續操作之前,請在此配置你的BUCKET_NAME變量,否則你將無法訓練模型。
BUCKET_NAME = "bert_resourses" #@param {type:"string"}MODEL_DIR = "bert_model" #@param {type:"string"}tf.gfile.MkDir(MODEL_DIR)if not BUCKET_NAME: log.warning("WARNING: BUCKET_NAME is not set. ""You will not be able to train the model.")
配置GCS bucket名稱
下面是BERT的超參數配置示例。若更改將自擔風險!
# use thisfor BERT-basebert_base_config = {"attention_probs_dropout_prob": 0.1, "directionality": "bidi", "hidden_act": "gelu", "hidden_dropout_prob": 0.1, "hidden_size": 768, "initializer_range": 0.02, "intermediate_size": 3072, "max_position_embeddings": 512, "num_attention_heads": 12, "num_hidden_layers": 12, "pooler_fc_size": 768, "pooler_num_attention_heads": 12, "pooler_num_fc_layers": 3, "pooler_size_per_head": 128, "pooler_type": "first_token_transform", "type_vocab_size": 2, "vocab_size": VOC_SIZE}with open("{}/bert_config.json".format(MODEL_DIR), "w") as fo: json.dump(bert_base_config, fo, indent=2)with open("{}/{}".format(MODEL_DIR, VOC_FNAME), "w") as fo:for token in bert_vocab: fo.write(token+"\n")
配置BERT的超參數並保存到磁碟
現在,我們將我們的成果存放到谷歌雲存儲。
if BUCKET_NAME: !gsutil -m cp -r $MODEL_DIR$PRETRAINING_DIR gs://$BUCKET_NAME
上傳成果到谷歌雲存儲
第7步:訓練模型
我們已準備好開始訓練我們的模型了。
建議過去步驟中的一些參數不要更改,以便於快速重啟訓練過程。
確保整個實驗所設置的參數完全相同。
BUCKET_NAME = "bert_resourses"#@param {type:"string"}MODEL_DIR = "bert_model"#@param {type:"string"}PRETRAINING_DIR = "pretraining_data"#@param {type:"string"}VOC_FNAME = "vocab.txt"#@param {type:"string"}# Input data pipeline configTRAIN_BATCH_SIZE = 128 #@param {type:"integer"}MAX_PREDICTIONS = 20 #@param {type:"integer"}MAX_SEQ_LENGTH = 128 #@param {type:"integer"}MASKED_LM_PROB = 0.15 #@param# Training procedure configEVAL_BATCH_SIZE = 64LEARNING_RATE = 2e-5TRAIN_STEPS = 1000000 #@param {type:"integer"}SAVE_CHECKPOINTS_STEPS = 2500 #@param {type:"integer"}NUM_TPU_CORES = 8if BUCKET_NAME: BUCKET_PATH = "gs://{}".format(BUCKET_NAME)else: BUCKET_PATH = "."BERT_GCS_DIR = "{}/{}".format(BUCKET_PATH, MODEL_DIR)DATA_GCS_DIR = "{}/{}".format(BUCKET_PATH, PRETRAINING_DIR)VOCAB_FILE = os.path.join(BERT_GCS_DIR, VOC_FNAME)CONFIG_FILE = os.path.join(BERT_GCS_DIR, "bert_config.json")INIT_CHECKPOINT = tf.train.latest_checkpoint(BERT_GCS_DIR)bert_config = modeling.BertConfig.from_json_file(CONFIG_FILE)input_files = tf.gfile.Glob(os.path.join(DATA_GCS_DIR,'*tfrecord'))log.info("Using checkpoint: {}".format(INIT_CHECKPOINT))log.info("Using {} data shards".format(len(input_files)))
配置訓練過程
準備訓練運行配置,建立評估器和輸入函數,啟動BERT。
model_fn = model_fn_builder(bert_config=bert_config,init_checkpoint=INIT_CHECKPOINT,learning_rate=LEARNING_RATE,num_train_steps=TRAIN_STEPS,num_warmup_steps=10,use_tpu=USE_TPU,use_one_hot_embeddings=True)tpu_cluster_resolver = tf.contrib.cluster_resolver.TPUClusterResolver(TPU_ADDRESS)run_config = tf.contrib.tpu.RunConfig(cluster=tpu_cluster_resolver,model_dir=BERT_GCS_DIR,save_checkpoints_steps=SAVE_CHECKPOINTS_STEPS,tpu_config=tf.contrib.tpu.TPUConfig(iterations_per_loop=SAVE_CHECKPOINTS_STEPS,num_shards=NUM_TPU_CORES,per_host_input_for_training=tf.contrib.tpu.InputPipelineConfig.PER_HOST_V2))estimator = tf.contrib.tpu.TPUEstimator(use_tpu=USE_TPU,model_fn=model_fn,config=run_config,train_batch_size=TRAIN_BATCH_SIZE,eval_batch_size=EVAL_BATCH_SIZE)train_input_fn = input_fn_builder(input_files=input_files,max_seq_length=MAX_SEQ_LENGTH,max_predictions_per_seq=MAX_PREDICTIONS,is_training=True)
建立評估模型和輸入函數
啟動!
estimator.train(input_fn=train_input_fn, max_steps=TRAIN_STEPS)
啟動BERT
使用默認參數訓練模型100萬步大約需要53個小時。如果內核出於某種原因重新啟動,你可以從最新的檢查點繼續訓練。
以上就是在雲TPU上從頭開始預訓練BERT模型的指南。
還能做點什麼?
我們已經訓練好了模型,那接下來呢?
這是一個全新的話題。你可以做如下幾件事:
將預訓練模型作為通用NLU模塊針對某些特定的分類任務微調模型使用BERT作為構件塊創建另一個深度學習模型
連結:
https://towardsdatascience.com/building-a-search-engine-with-bert-and-tensorflow-c6fdc0186c8a
真正有趣的東西還在後面,所以睜大你的眼睛。同時,看看bear-as-a-service這個很棒的項目,將你剛訓好的模型部署到線上去吧!
項目連結:
https://github.com/hanxiao/bert-as-service?source=post_page
最最重要的,不斷學習!
參考資料:
https://www.jianshu.com/p/7e13a498bd63
https://www.jianshu.com/p/d110d0c13063
https://colab.research.google.com/drive/1nVn6AFpQSzXBt8_ywfx6XR8ZfQXlKGAz?source=post_page