Deeplearning4j 實戰 (21):Bert簡介及NLP問題應用

Eclipse Deeplearning4j GitChat課程https://gitbook.cn/gitchat/column/5bfb6741ae0e5f436e35cd9f
Eclipse Deeplearning4j 系列博客https://blog.csdn.net/wangongxi
Eclipse Deeplearning4j Githubhttps://github.com/eclipse/deeplearning4j

版權聲明:本文爲CSDN博主「wangongxi」的原創文章,遵循CC 4.0 BY-SA版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/wangongxi/article/details/106228218
在上一篇博客中,我們介紹了attention機制的基本原理以及如何基於Deeplearning4j中內置的attention layer對文本之類的序列數據進行建模的過程。這篇博客在上一篇的基礎上,介紹下2019年Google的研究成果,同樣也是和attention機制有關的Bert模型。在Google的論文中介紹,Bert模型在GLUE數據集上都達到的當時的歷史最佳。當然後續改進的一些工作也逐步的在指標上超越了Bert,其中就有國內百度公司的工作ERNIE,但Bert構建NLP語言模型的方式方法還是非常值得去學習和研究,包括後續精簡參數後的ALBert。這次的文章主要會分爲4個部分,在第一部分中結合Bert的論文介紹其基本原理,第二部分中我們嘗試將預訓練好的Bert模型通過Deeplearning4j中的SameDiff工具導入到DL4j中並打印一些網絡結構信息。在第二部分的基礎上,也就是基於導入到DL4j中的Bert模型,我們嘗試通過遷移學習的方式進行文本分類和序列標註任務的建模。最後我們對全文做下小結。

Bert簡介

Bert模型的論文題目叫做《BERT: Pre-training of Deep Bidirectional Transformers for
Language Understanding》
。從題目中可以很直接的看出,Bert其實是基於Transformer的一種網絡結構。Transformer結構本身也就是基於self-attention,最早實在論文《Attention Is All You Need》中提出的,分爲Encoder和Decoder兩個部分,我們看下論文中的結構圖。
在這裏插入圖片描述
Bert中用到的是transformer中的encoder部分,因此我們重點看下編碼這塊的網絡結構。輸入端和一般的網絡結構類似,是Embedding向量化的結果以及加上了位置信息的Embedding信息。接着就利用到了上一篇博客中提到的多頭自注意力機制(Multi-Head Attention)。接着,經過一個簡單的position-wise的全連接網絡與LayerNorm,也就同網絡層的歸一化就可以得到encoder部分的輸出。需要注意的是,對於整個encoder的部分,輸入和輸出的格式是[BN,SeqLen,EmbedDim]。Transformer是第一個不依賴於RNN或者CNN結構,完全由Attention結構和普通全連接神經網絡構成的。這也是爲什麼論文的題目叫做attention is all you need的原因。另外,爲了彌補attention機制相比於RNN網絡在時間維度的欠缺,transformer在輸入端將位置信息向量化以後疊加到原始輸入上作爲共同輸入信息進入到模型結構中。計算方式如下:
在這裏插入圖片描述
值得一提的是,transformer中也使用到了殘差機制。從encoder部分可以看到,embedding的聯合輸入有一個箭頭直接指向了Add&Norm,並沒有經過自注意力機制,這樣的設計其實借鑑了機器視覺中殘差網絡設計的思路,畢竟原始信息也非常重要,而且便於加深網絡測層次,優化梯度彌散的問題。以上就是transformer結構中encoder部分的大概情況,我們回到Bert模型。
Bert模型是通過堆疊transformer結構來實現,主要是堆疊transformer的encoder部分。根據原論文中的描述,官方預訓練的模型有堆疊12和24個transformer單元兩個版本。通過一層一層的編碼器,Bert模型可以獲取到每個token的分佈式表達,這個結果可以用來作爲語言模型用於後期的文本建模等相關工作。預訓練模型構建過程中,Bert採用了兩個任務聯合訓練來獲取句子以及token的語義信息,它們分別是Masked LM和Next Sentence Prediction (NSP)。對於Masked LM來說,在建模的時候會隨機掩蓋句子中的一些詞,然後通過上下文來預測這個詞。而對於NSP問題來說,在輸出端將上下文兩句話同時作爲輸入喂到模型中,結果做一個二分類就可以了。當然對於中文來說,有詞和字mask兩種方案,Google官方提供的中文版本是基於字的,而哈工大開源的是基於整詞的,有興趣的同學可以自己Google一下。對於英文來說,也不一定使用整個單詞,可以使用粒度更細的Word Piece。下面看下論文中給出的建模示意圖。
在這裏插入圖片描述
輸入序列通過[CLS]和[SEP]兩個特殊字符作爲開始和句子的間隔(如果是單句,就是句子結尾)。Token Embedding很好理解,就是詞向量,Segment Embedding是一個只含有0,1兩個元素的序列,用於表示第一句和第二句。Position Embedding就是之前提到的,可以通過餘弦變換得到的位置向量信息。將其累和後作爲輸入喂到transformer模型中。
在這裏插入圖片描述
以上就是官方給出的中文版本的預訓練的Bert模型。
在這裏插入圖片描述
這是哈工大做整詞mask的改進版Bert的github開源項目。
在第一部分的最後,我們嘗試通過tensorbard看一下預訓練好的Bert模型的結構。我們先看下導入到tensorboard後展示的結構。
在這裏插入圖片描述
從tensorboard可視化工具上面展示的預訓練模型結構中,bert有三個輸入placeholder。而bert這個大的模塊其中就包含了transformer結構。

Bert模型導入SameDiff

我們首先從官網上下載中文的預訓練模型。下面截圖是解壓縮後的預訓練。
在這裏插入圖片描述
然後通過以下腳本導出pb格式的模型。

import tensorflow as tf
from tensorflow.python.tools import freeze_graph
from tensorflow.tools.graph_transforms import TransformGraph
from tensorflow.summary import FileWriter
import argparse

def load_graph(checkpoint_path, mb, seq_len):
    init_all_op = tf.initialize_all_variables()
    graph2 = tf.Graph()
    with graph2.as_default():
        with tf.Session(graph=graph2) as sess:
            saver = tf.train.import_meta_graph(checkpoint_path + '.meta')
            saver.restore(sess, checkpoint_path)
            print("Restored structure...")
            saver.restore(sess, checkpoint_path)
            print("Restored params...")
            graph_def = graph2.as_graph_def()
            FileWriter("__tb-ch", graph2)

            input_names = ["Placeholder", "Placeholder_1", "Placeholder_2"]
            output_names = ["bert/pooler/dense/Tanh"]
            transforms = ['strip_unused_nodes(type=int32, shape="' + str(mb) + ',' + str(seq_len) + '")']
            graph2 = TransformGraph(graph2.as_graph_def(), inputs=input_names, outputs=output_names, transforms=transforms)

            return graph2


parser = argparse.ArgumentParser(description='Freeze BERT model')
parser.add_argument('--minibatch', help='Minibatch size', default=4)
parser.add_argument('--seq_length', help='Sequence length', default=128)
parser.add_argument('--input_dir', help='Input directory for model', default="D:/chinese_L-12_H-768_A-12/chinese_L-12_H-768_A-12/")
parser.add_argument('--ckpt', help='Checkpoint filename in input dir', default="bert_model.ckpt")

args = parser.parse_args()
mb = int(args.minibatch)
seq_len = int(args.seq_length)

print("minibatch: ", mb)
print("seq_length: ", seq_len)
print("input_dir: ", args.input_dir)
print("checkpoint: ", args.ckpt)

dirIn = args.input_dir
dirOut = dirIn + "frozen/"
ckpt = args.ckpt
graph = load_graph(dirIn + ckpt, mb, seq_len)
txtName = "bert_export_mb" + str(mb) + "_len" + str(seq_len) + ".pb.txt"
txtPath = dirOut + txtName
tf.train.write_graph(graph, dirOut, txtName, True)


output_graph = dirOut + "bert_frozen_mb" + str(mb) + "_len" + str(seq_len) + ".pb"
print("Freezing Graph...")
freeze_graph.freeze_graph(
    input_graph=txtPath,
    input_checkpoint=dirIn+ckpt,
    input_saver="",
    output_graph=output_graph,
    input_binary=False,
    output_node_names="bert/pooler/dense/Tanh",     #This is log(prob(x))
    # output_node_names="loss/Softmax",     #This is log(prob(x))
    restore_op_name="save/restore_all",
    filename_tensor_name="save/Const:0",
    clear_devices=True,
    initializer_nodes="")
print("Freezing graph complete...")

下面我們將保存好的pb模型文件通過SameDiff導入到DL4j中並打印模型的基本信息。

	File f = new File("E:/bert_frozen_ch", "bert_frozen_mb4_len128.pb");
    SameDiff sd = TFGraphMapper.importGraph(f);
    System.out.println(sd.summary());

這段邏輯比較簡單,我們直接看下summary打印出的結果。
在這裏插入圖片描述
在這裏插入圖片描述
從summary的截圖可以比較清楚地看到,encoder共有0~11個transformer單元模塊,並在最後一個block做了pooling,這主要方便做後續的遷移學習。需要注意的是,在Bert源碼中pooling的操作其實是拿出了第一個token的向量來作爲整個序列的語義向量,我們在基於預訓練模型做進一步的優化的時候,可以不完全按照這種方式,例如我們可以直接將每個token的序列做線性加和來達到獲取整個序列語義的目的。以下Bert是源碼中對pooling的解釋。
在這裏插入圖片描述
我們截圖出一個單獨的transformer單元,並對照上文中transformer的結構來看下。
在這裏插入圖片描述
從截圖中我們可以看到,紅框的部分是自注意力機制的計算部分,藍框的部分則是add&norm的部分。這和《Attention is all you need》論文中對於transformer的結構描述是一致的。開發人員可以根據和論文中的對照關係對導入的模型結構進行分析。該部分最後我們看下DL4j提供的用於Bert模型的數據迭代器。
Deeplearning4j主要爲Bert提供了BertWordPieceTokenizerFactory和BertIterator兩個組件。BertWordPieceTokenizerFactory支持加載官方提供的字典數據,主要維護token以及對應索引值的鍵值對關係。BertIterator主要用於構建Bert預訓練任務和遷移監督學習任務的訓練數據。對於預訓練模型這塊,目前只支持Mask LM的任務,暫不支持NSP。我們看下源碼實現的註釋中給出的例子。
在這裏插入圖片描述
從圖中我們可以看到,第一步我們需要加載字典到BertWordPieceTokenizerFactory的實例中,接着我們設置BertIterator對象的參數,包括分詞的工具、序列的長度、batch size、單句或者上下文序列預處理工具等等。對於預訓練模型的Mask LM任務,以下這幾個參數是需要指定的

.task(BertIterator.Task.UNSUPERVISED)
.masker(new BertMaskedLMMasker(new Random(12345), 0.2, 0.5, 0.5))
.unsupervisedLabelFormat(BertIterator.UnsupervisedLabelFormat.RANK2_IDX)
.maskToken("[MASK]")

task用於指定預訓練的任務類型,masker參數則是用於確定掩碼的token的比例,unsupervisedLabelFormat用於指定預訓練模式下的標註的格式,maskToken則是指定掩蓋token的特殊標識符。而對於非預訓練建模問題的配置上,需要設置以下參數

.featureArrays(BertIterator.FeatureArrays.INDICES_MASK)
.vocabMap(t.getVocab())
.task(BertIterator.Task.SEQ_CLASSIFICATION)

對於非預訓練任務,我們需要通過task參數設置任務的類型,示例中是序列分類。下面兩個部分將分別使用預訓練好的模型進行遷移學習。其中也會涉及內置Bert數據迭代器的使用。

基於Bert預訓練模型的文本分類

在上面的部分中,我們介紹瞭如何將預訓練好的中文Bert模型保存成pb格式並通過SameDiff工具導入到DL4j中。我們基於預訓練好的模型可以在此基礎上做進一步的遷移學習,這個部分我們首先介紹下如何通過添加部分網絡結構來實現文本分類的功能。
在已經導入pb模型的基礎上,我們添加以下結構

SDVariable labels = sd.placeHolder("label", DataType.FLOAT, 1, 2);
NameScope my_transfer = sd.withNameScope("loss");
SDVariable input = sd.getVariable("bert/pooler/dense/Tanh");
SDVariable my_flatten_weights = sd.var("flatten_weights", new XavierInitScheme('c', 768, 2), DataType.FLOAT, 768, 2);
SDVariable my_flatten_bias = sd.var("flatten_bias", new UniformInitScheme('c', 2),DataType.FLOAT, 2);
SDVariable linear_output = input.mmul(my_flatten_weights).add("linear_output",my_flatten_bias);
SDVariable softmax_output = sd.nn().softmax("softmax", linear_output);
SDVariable loss = sd.loss().logLoss("Loss", labels, softmax_output);
my_transfer.close();
//
sd.setLossVariables(loss);
sd.addListeners(new ScoreListener(1));

這段邏輯的核心在於我們將“bert/pooler/dense/Tanh”變量作爲整個預訓練模型的輸出,其實就是一個768維的向量,然後我們添加一個全連接網絡作爲整個網絡結構的輸出。這裏涉及到的算子比較簡單,通過SameDiff的mmul、add以及softmaxLoss就可以在預訓練的Bert模型上搭建起序列分類的模型結構。這裏需要提一點,就是我用了NameScope爲相關域範圍內的算子和變量添加的命名範圍,如果不使用也是可以的。通過setLossVariables來定義該網絡結構的損失函數。我們同樣可以輸出模型結構的summary來看下整個遷移後的模型結構。
在這裏插入圖片描述
由於name scope設置的名稱是loss,所以添加的網絡結構的前綴都會帶有loss。其餘未截圖出來的結構和之前Bert預訓練的模型結構是一致的,這裏不多描述了。下面介紹下訓練數據的準備。由於時間原因,這裏沒有準備非常翔實的語料數據,但實現邏輯是一致的,我們先來看下

private static MyBertIterator getSupervisedDataIterator(String bertModelPath) throws Exception {
	List<String> sentences = new ArrayList<String>() {{add("這個菜很好喫");
											add("那個商品質量太差了");
											add("差評!太垃圾了!");
											add("非常喜歡這個品類");}};
	List<String> sentencesR = new ArrayList<String>() {{add("");add("");add("");add("");}};
	List<String> label = new ArrayList<String>() {{add("pos");add("neg");add("neg");add("pos");}};
	CollectionLabeledPairSentenceProvider labeledPairSentenceProvider = new CollectionLabeledPairSentenceProvider(sentences, sentencesR, label, new Random(123L));
    File wordPieceTokens = new File("E:/bert_frozen_ch/vocab.txt");

    BertWordPieceTokenizerFactory t = new BertWordPieceTokenizerFactory(wordPieceTokens, true, true, StandardCharsets.UTF_8);
    MyBertIterator b = MyBertIterator.builder()
                  .tokenizer(t)
                  .lengthHandling(MyBertIterator.LengthHandling.FIXED_LENGTH, 128)
                  .minibatchSize(1)
                  .sentenceProvider(null)
                  .sentencePairProvider(labeledPairSentenceProvider)
                  .featureArrays(MyBertIterator.FeatureArrays.INDICES_MASK_SEGMENTID)
                  .vocabMap(t.getVocab())
                  .task(MyBertIterator.Task.SEQ_CLASSIFICATION)
                  .prependToken("[CLS]")
                  .appendToken("[SEP]")
                  .build();

    return b;
}

上面的邏輯中我們一共準備了4段短文本,主要是圍繞評論的。在label這個對象中,我們爲每段文本添加了一個標註,neg或者pos,也就是負面或者正面。而對於sentencesR這個對象,其實是用於sentence pair的輸入的第二句文本,這裏我們不需要所以將其置空。labeledPairSentenceProvider對象其實是處理原始語料和label的工具類,直接使用即可。這裏有一點需要注意,就是需要從Bert模型的字典文件,也就是wordPieceTokens指向的文件位置,需要通過BertWordPieceTokenizerFactory加載到內存中。字典文件可以直接用文本編輯器打開,裏面存儲的是一些中文單字和英文。
在這裏插入圖片描述
單字的索引在BertWordPieceTokenizerFactory實例化後會自動維護,這裏對用戶是透明的。最後就是聲明一個BertIterator迭代器了。這裏需要注意的是,BertIterator源碼中對於sentencePairProvider的reset操作有一個bug,因此我做一些修改,這裏直接用我自定義的MyBertIterator,絕大部分功能實現和BertIterator是一樣的。我們來看下訓練模型的主邏輯。

MyBertIterator datasetIter = getSupervisedDataIterator();
SameDiff sd = getBertModel();
TrainingConfig c = TrainingConfig.builder()
            .updater(new Adam(0.01))
            .l2(1e-5)
            .dataSetFeatureMapping("Placeholder", "Placeholder_1")
            .dataSetFeatureMaskMapping("Placeholder_2")
            .dataSetLabelMapping("label")
            .build();
sd.setTrainingConfig(c);
System.out.println("Start Training...");
long start = System.currentTimeMillis();
for( int i = 0; i < 50; ++i ){
	sd.fit(datasetIter, 1);
	datasetIter.reset();
}
long end = System.currentTimeMillis();
System.out.println("Total Time Cost: " + (end - start) + "ms");
System.out.println("End Training...");

這段邏輯首先我們構建了訓練語料並封裝在Bert數據迭代器中,接着獲取模型實例,當然這裏的模型已經是在預訓練模型基礎上添加了全連接網絡。我們通過TrainingConfig來配置訓練的參數,其中包含優化器和學習率、L2正則化項以及整個模型的輸入和輸出。我們一共訓練50個epoch並在每一輪訓練完畢後需要reset一下Bert數據迭代器。我們看下控制檯的部分輸出。
在這裏插入圖片描述
從日誌中可以看到每一輪訓練後的loss情況,由於語料的數量較少,因此不能反映實際工程的狀況,但可以作爲開發人員的參考。最後我們對這部分做下簡單的小結。
對於基於Bert預訓練好的中文模型,我們先通過SameDiff導入到DL4j中,然後在預訓練好的pooling層添加一個全連接網絡用於分類任務。語料的準備是比較簡單的構造了幾句正面和負面的評價,然後通過BertIterator解析成可以喂到Bert模型中的數據格式。在構建好訓練數據集以及遷移的模型後,我們設置一些訓練的參數,包括優化器和學習率等超參數,然後和以往的神經網絡一樣訓練若干個epoch即可完成基於Bert的分類建模任務。下面我們來看下Bert預訓練模型如果做NER問題的。

基於Bert預訓練模型的實體識別問題

基於Bert預訓練模型做NER問題和文本分類有一些區別,我們需要對序列中的每一個token進行打標,而不是對於整個序列進行識別,因此我們其實並不需要預訓練中pooling層的結果,我們需要的其實是12層transformer encoder結果的輸出,也就是每一個batch中每一個token的768維的encoder結果。我們先給出網絡結構的遷移部分的邏輯再做解釋。

private static SameDiff getBertModel() throws IOException {
	File f = new File("E:/bert_frozen_ch", "bert_frozen_mb4_len128.pb");
    SameDiff sd = TFGraphMapper.importGraph(f);
    //
    SDVariable labels = sd.placeHolder("label", DataType.INT, 1, 128);
    NameScope my_transfer = sd.withNameScope("loss");
    SDVariable input = sd.getVariable("bert/encoder/layer_11/output/LayerNorm/batchnorm/add_1");
    SDVariable my_flatten_weights = sd.var("flatten_weights", new XavierInitScheme('c', 768, 7), DataType.FLOAT, 768, 7);
    SDVariable my_flatten_bias = sd.var("flatten_bias", new UniformInitScheme('c', 7),DataType.FLOAT, 7);
    SDVariable linear_output = input.mmul(my_flatten_weights).add("linear_output",my_flatten_bias);
    SDVariable softmax_output = sd.nn().softmax("softmax", linear_output);
    SDVariable loss = sd.loss().sparseSoftmaxCrossEntropy("Loss", softmax_output, labels);
    my_transfer.close();
    //
    sd.setLossVariables(loss);
    sd.addListeners(new ScoreListener(1));
    //
    return sd;
}

首先我們基於SameDiff導入預訓練好的模型,這個和上一部分中的邏輯相同。我們重點看下NameScope中的部分邏輯。我們把encoder部分的output部分的輸出變量拿出來,然後我們用全連接網絡對輸出張量做線性變化並得到一個[Batch,SeqLen,LabelCount]的張量。這裏的LabelCount是指的包括特殊字符[CLS]和[SEP]都在內的所有標籤的數量,由於我們準備做BMSE標籤方法做一個分詞工具,因此除了預留的label=0之外,剩餘的標籤數量就是共有6個,總共是7個,這也是我們做全連接網絡線性變化的時候用的是768x7的結構的原因。需要注意的是我們這裏用的是sparseSoftmaxCrossEntropy作爲損失函數,而不是普通的softmax交差熵loss,sparse的loss可用於每個label獨立且互斥的情況。同時label數據的格式不再是one-hot的格式,而是一個整數序列。我們給出訓練數據準備的邏輯。
在這裏插入圖片描述

private static List<MultiDataSet> getDataIter(String fileName, Map<String,Integer> vocab) throws IOException{
	List<String> lines = FileUtils.readLines(new File(fileName), Charset.forName("utf-8"));
	List<MultiDataSet> datasets = new LinkedList<>();
	List<Integer> idxsLst = new ArrayList<>(128);
	List<Integer> maskLst = new ArrayList<>(128);
	List<Integer> labelLst = new ArrayList<>(128);
	//
	for( String line : lines ){
		String[] tokens = line.split("\t");
		idxsLst.add(101);//頭部增加[CLS]
		labelLst.add(1);//CLS的默認標註
		maskLst.add(1);
		for( String token : tokens ){
			String[] wordAndLabel = token.split("/");
			String word = wordAndLabel[0];
			String label = wordAndLabel[1];
			idxsLst.add(vocab.getOrDefault(word, 100));	//字典中查找每個字的索引,否則默認是UNK.
			maskLst.add(1);
			labelLst.add(labelMap.get(label));
		}
		idxsLst.add(102);//尾部增加[SEP]
		idxsLst.addAll(Collections.nCopies(128 - idxsLst.size(), 0));
		labelLst.add(2);//SEP的默認標註
		labelLst.addAll(Collections.nCopies(128 - labelLst.size(), 0));
		maskLst.add(1);
		maskLst.addAll(Collections.nCopies(128 - maskLst.size(), 0));
		//
		INDArray idxs = Nd4j.create(idxsLst);
		INDArray mask = Nd4j.create(maskLst);
		INDArray segmentIdxs = Nd4j.zeros(128);
		INDArray labelArr = Nd4j.create(labelLst);
		MultiDataSet mds = new org.nd4j.linalg.dataset.MultiDataSet(new INDArray[]{idxs, mask, segmentIdxs}, new INDArray[]{labelArr});
		datasets.add(mds);
		//
		idxsLst.clear();
		maskLst.clear();
		labelLst.clear();
	}
	return datasets;
}

截圖是語料的標註情況,當然爲了驗證模型我們也只是準備了非常少量的語料數據。下面的邏輯就是數據預處理的過程。當讀取一條記錄後,我們以tab來分割字符串並取出token以及token的label,從vocab這個字典對象中獲取該token的索引值否則用正整數100來代替,它的物理含義是UNK。需要注意的是,爲了兼容Bert模型的訓練數據格式,我們在每一個序列的開始需要添加“CLS”這個token,它對應的索引整數是101,同時在每個序列的結尾需要添加“SEP”這個token,它對應的索引整數值是102。至於label的數據方面,我們可以爲CLS、SEP以及UNK設定默認的label值,否則就按照語料中給label並給出相關的索引值。除了token序列以及標註序列,我們需要準備一個掩碼序列。這個掩碼序列的功能主要是標識哪些位置是有效,哪些是無效位置,用0-1二值表示即可。在構建完這三個List之後,我們就可以創建三個張量對象,然後逐個封裝在MultiDataSet對象中。下面看下完整的建模過程。

File wordPieceTokens = new File("E:/bert_frozen_ch/vocab.txt");
BertWordPieceTokenizerFactory t = new BertWordPieceTokenizerFactory(wordPieceTokens, true, true, StandardCharsets.UTF_8);
Map<String,Integer> vocab = t.getVocab();
List<MultiDataSet> iter = getDataIter("seg.corpus",vocab);
//
SameDiff sd = getBertModel();
TrainingConfig c = TrainingConfig.builder()
					      .updater(new Adam(0.01))
					      .l2(1e-5)
					      .dataSetFeatureMapping("Placeholder", "Placeholder_1","Placeholder_2")
					      .dataSetLabelMapping("label")
					      .build();
sd.setTrainingConfig(c);
//
long start = System.currentTimeMillis();
for( int numEpoch = 0; numEpoch < 10; numEpoch++ ){
     for( int i = 0; i < iter.size(); ++i ){
          sd.fit(iter.get(i));
     }
} 

該部分邏輯和上一部分文本分類的建模邏輯類似,首先我們需要從磁盤上讀取字典文件到內存中。並且結合上面介紹的訓練數據準備邏輯,將token序列和label都封裝在MultiDataSet的對象中。從getBertModel方法中我們可以按照之前介紹的邏輯獲取序列標註的遷移模型結構。然後和之前文本分類的邏輯一致,設置訓練的參數以及多輪的訓練。最後我們看下控制檯的日誌。
在這裏插入圖片描述

總結

我們對這篇文章的內容做一下總結。對於Bert模型,我們解釋了它內部的transformer結構並介紹了完整Bert預訓練模型是如何構建的,包括Mask LM和NSP兩個預訓練任務。對於中文預訓練Bert模型,我們介紹瞭如何先將其轉化爲pb模型文件以及如何藉助SameDiff導入到DL4j中。此外,在Deeplearning4j中,內置了一些工具類用於支持Bert模型的訓練和數據的ETL,我們也給出了對應的使用介紹。我們通過打印summary信息到控制檯,可以比較清晰地看到預訓練模型的結構,並對照論文進行進一步的理解。接着,在導入預訓練模型的基礎上,我們爲其添加一些網絡結構使其支持做文本分類和序列標註的遷移學習。Bert模型其實在遷移學習的過程中起到了提取特徵的作用,而我們基於這樣的特徵提取器可以更好地進行相關的NLP任務。由於時間有限,我們沒有準備太過豐富的語料信息,有興趣的同學可以自行準備並進行驗證。
Bert模型本身結構其實沒有給出太過原始的創新點,主要是基於transformer的encoder部分搭建整個網絡的架構。此外,由於attention機制本身對位置信息的缺失,在輸入層將token以及其位置的embedding信息進行線性疊加,這樣從理論上可以彌補一些時序信息的缺失。Bert本身更想做一個一個LM,這個語言模型可以遷移到其他的一些NLP任務當中,而不是像傳統網絡結構一樣每次都重新train一個LM。NLP任務確實是AI一個難點,畢竟文字這種符號化的東西是人類自身創造的,意義也是人賦予的,而不像CV的一些問題可以認爲是原始信息的一些採集。還是很期待以Bert爲代表的新一代NLP解決方案可以進一步推進AI的發展。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章