<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          如何使用 BERT 進(jìn)行自然語(yǔ)言處理?

          共 20524字,需瀏覽 42分鐘

           ·

          2021-05-24 01:52

          點(diǎn)擊上方視學(xué)算法”,選擇加"星標(biāo)"或“置頂

          重磅干貨,第一時(shí)間送達(dá)

          來(lái)源:AI前線

          本文大約7510字,閱讀時(shí)間約10分鐘。
          本文介紹并探索了基于 Transformer 架構(gòu)的神經(jīng)網(wǎng)絡(luò)BERT,并介紹了進(jìn)一步了解BERT的若干方法。

          迄今為止,在我們的 ML.NET 之旅中,我們主要關(guān)注計(jì)算機(jī)視覺(jué)問(wèn)題,例如圖像分類(lèi)和目標(biāo)檢測(cè)。在本文中,我們將轉(zhuǎn)向自然語(yǔ)言處理,并探索一些我們可以用機(jī)器學(xué)習(xí)來(lái)解決的問(wèn)題。

          自然語(yǔ)言處理(Natural language processing,NLP)是人工智能的一個(gè)子領(lǐng)域,其主要目的是幫助程序理解和處理自然語(yǔ)言數(shù)據(jù)。這一過(guò)程的輸出是一個(gè)計(jì)算機(jī)程序,它可以“理解”語(yǔ)言。


          如果你擔(dān)心人工智能會(huì)奪走你的飯碗,那么一定要成為它的創(chuàng)造者,并與不斷上升的人工智能產(chǎn)業(yè)保持緊密聯(lián)系。

          追溯到 2018 年,谷歌發(fā)表了一篇論文,其中有一個(gè)深度神經(jīng)網(wǎng)絡(luò)叫做 Bidirectional Encoder Representations from Transformers 或 BERT。因?yàn)樗暮?jiǎn)單性,它成為目前最流行的一種自然語(yǔ)言處理算法。使用這種算法,任何人都能在短短的幾個(gè)小時(shí)內(nèi)訓(xùn)練自己最先進(jìn)的問(wèn)答系統(tǒng)(或其他各種模型)。在本文中,我們將使用 BERT 來(lái)創(chuàng)建一個(gè)問(wèn)答系統(tǒng)。

          BERT 是基于 Transformer 架構(gòu)的神經(jīng)網(wǎng)絡(luò)。正因?yàn)槿绱耍诒疚闹校覀儗⑹紫忍剿鬟@個(gè)架構(gòu),然后再進(jìn)一步了解 BERT:
          1. 前提
          2. 理解 Transformer 架構(gòu)
          3. BERT 直覺(jué)
          4. ONNX 模型
          5. 用 ML.NET 實(shí)現(xiàn)
          1. 前提
          本文的實(shí)現(xiàn)用 C# 語(yǔ)言完成,我們使用最新的 .NET 5。因此要確保你已安裝此 SDK。若你正在使用 Visual Studio,則隨附 16.8.3 版本。此外,確保你已安裝下列軟件包:
          $ dotnet add package Microsoft.ML
          $ dotnet add package Microsoft.ML.OnnxRuntime
          $ dotnet add package Microsoft.ML.OnnxTransformer

          你可以在 Package Manager Console 中執(zhí)行相同操作:

          Install-Package Microsoft.ML
          Install-Package Microsoft.ML.OnnxRuntime
          Install-Package Microsoft.ML.OnnxTransformer

          你可以使用 Visual Studio 的 Manage NuGetPackage 選項(xiàng)來(lái)執(zhí)行類(lèi)似操作:

          假如你想了解使用 ML.NET 進(jìn)行機(jī)器學(xué)習(xí)的基本知識(shí),請(qǐng)看這篇文章:《使用 ML.NET 進(jìn)行機(jī)器學(xué)習(xí):簡(jiǎn)介》(Machine Learning with ML.NET – Introduction)(https://rubikscode.net/2021/01/04/machine-learning-with-ml-net-introduction/)。

          2. 理解Transformer架構(gòu)

          語(yǔ)言是順序數(shù)據(jù)。從根本上說(shuō),你可以把它看成是一個(gè)詞流,每個(gè)詞的含義都取決于它前面的詞和后面的詞。因此,計(jì)算機(jī)理解語(yǔ)言非常困難,因?yàn)橐肜斫庖粋€(gè)詞,你需要一個(gè) 上下文。

          此外,有時(shí)候作為輸出,還需要提供數(shù)據(jù) 序列(詞)。把英語(yǔ)翻譯成塞爾維亞語(yǔ)就是一個(gè)好例子。我們將詞序列作為算法的輸入,同時(shí)對(duì)輸出也需要提供一個(gè)序列。

          本例中,一種算法要求我們理解英語(yǔ),并理解如何將英語(yǔ)單詞映射到塞爾維亞語(yǔ)單詞(實(shí)質(zhì)上,這意味著對(duì)塞爾維亞語(yǔ)也有某種程度的理解)。在過(guò)去的幾年里,已經(jīng)有很多深度學(xué)習(xí)的架構(gòu)用于這種目的,例如遞歸神經(jīng)網(wǎng)絡(luò)(Recurrent Neural Network,RNN)和長(zhǎng)短期記憶網(wǎng)絡(luò)(LSTM)。但是,Transformer 架構(gòu)的使用改變了一切。

          由于 RNN 和 LSTM 難以訓(xùn)練,且已出現(xiàn)梯度消失(和爆炸),因此不能完全滿(mǎn)足需求。Transformer 的目的就是解決這些問(wèn)題,帶來(lái)更好的性能和更好的語(yǔ)言理解。它們于 2017 年推出,并被發(fā)表在一篇名為《注意力就是你所需要的一切》(Attention is all you need)(https://arxiv.org/pdf/1706.03762.pdf)的傳奇性論文上。

          簡(jiǎn)而言之,他們使用編碼器 - 解碼器結(jié)構(gòu)和自注意力層來(lái)更好地理解語(yǔ)言。如果我們回到翻譯的例子,編碼器負(fù)責(zé)理解英語(yǔ),解碼器負(fù)責(zé)理解塞爾維亞語(yǔ),并將英語(yǔ)映射到塞爾維亞語(yǔ)。
          在訓(xùn)練過(guò)程中,使用過(guò)程編碼器從英語(yǔ)語(yǔ)言中提取詞嵌入。計(jì)算機(jī)并不理解單詞,它們理解的是數(shù)字和矩陣(一組數(shù)字)。這就是為什么我們要將詞轉(zhuǎn)換成向量空間,也就是說(shuō),我們?yōu)檎Z(yǔ)言中的每個(gè)詞分配某些向量(將它們映射到某些潛在的向量空間)。這些就是詞嵌入。有許多可用的詞嵌入,如 Word2Vec。
          但是,該詞在句子中的位置也是影響上下文的重要因素,所以才會(huì)有位置編碼。編碼器就是這樣獲取關(guān)于單詞和它的上下文信息的。編碼器的自注意力層確定了詞之間的關(guān)系,并為我們提供了句子中每一個(gè)詞相互關(guān)系的信息。編碼器就是這樣理解英語(yǔ)的。接著,數(shù)據(jù)進(jìn)入深度神經(jīng)網(wǎng)絡(luò),再進(jìn)入解碼器的映射 - 注意力層。
          不過(guò),在此之前,解碼器已獲取有關(guān)塞爾維亞語(yǔ)的同樣信息。用同樣的方法學(xué)習(xí)如何理解塞爾維亞語(yǔ),使用詞嵌入、位置編碼和自注意力。解碼器的映射 - 注意力層既有英語(yǔ)也有塞爾維亞語(yǔ)的信息,它只是學(xué)習(xí)如何從一種語(yǔ)言轉(zhuǎn)換到另一種語(yǔ)言的詞。如需有關(guān) Transformer 的更多信息,請(qǐng)參閱這篇文章《Transformer 架構(gòu)介紹》(Introduction to Transformers Architecture)(https://rubikscode.net/2019/07/29/introduction-to-transformers-architecture/)。

          3. BERT直覺(jué)

          BERT 使用這種 Transformer 架構(gòu)來(lái)理解語(yǔ)言。更為確切的是,它使用了編碼器。

          這個(gè)架構(gòu)有兩大里程碑。首先,它實(shí)現(xiàn)了雙向性。也就是說(shuō),每個(gè)句子都是雙向?qū)W習(xí)的,并且更好地學(xué)習(xí)上下文,包括之前的上下文和將來(lái)的上下文。BERT 是首個(gè)采用純文本語(yǔ)料進(jìn)行訓(xùn)練的深度雙向、無(wú)監(jiān)督的語(yǔ)言表示。這也是最早應(yīng)用于自然語(yǔ)言處理的一種預(yù)訓(xùn)練模型。在計(jì)算機(jī)視覺(jué)中,我們了解了遷移學(xué)習(xí)。但是,在 BERT 出現(xiàn)之前,這一概念就沒(méi)有在自然語(yǔ)言處理領(lǐng)域得到重視。

          這有很大的意義,因?yàn)槟憧梢栽诖罅康臄?shù)據(jù)上訓(xùn)練模型,并且一旦模型理解了語(yǔ)言,你就可以根據(jù)更具體的任務(wù)對(duì)它進(jìn)行微調(diào)。因此,BERT 的訓(xùn)練分為兩個(gè)階段:預(yù)訓(xùn)練和微調(diào)。

          BERT 預(yù)訓(xùn)練采用兩種方法實(shí)現(xiàn)雙向性:

          掩碼語(yǔ)言建模:MLM(Masked Language Modeling)

          下一句預(yù)測(cè):NSP(Next Sentence Prediction)

          掩碼語(yǔ)言建模使用掩碼輸入。這意味著句子中的一些詞被掩碼,BERT 的工作就是填補(bǔ)這些空白。下一句預(yù)測(cè)是給出兩個(gè)句子作為輸入,并期望 BERT 預(yù)測(cè)是一個(gè)句子接著另一個(gè)句子。在現(xiàn)實(shí)中,這兩種方法都是同時(shí)發(fā)生的。

          在微調(diào)階段,我們?yōu)樘囟ǖ娜蝿?wù)訓(xùn)練 BERT。這就是說(shuō),如果我們想要?jiǎng)?chuàng)建一個(gè)問(wèn)答系統(tǒng)的解決方案,我們只需要訓(xùn)練 BERT 的額外層。這正是我們?cè)诒窘坛讨兴龅摹K形覀冃枰龅木褪菍⒕W(wǎng)絡(luò)的輸出層替換為為我們特定目的設(shè)計(jì)的新層集。我們有文本段(或上下文)和問(wèn)題作為輸入,而作為輸出,我們想要問(wèn)題的答案。

          舉例來(lái)說(shuō),我們的系統(tǒng),應(yīng)該使用兩個(gè)句子。為了提供答案“Jim”,可以使用“Jim is walking through the woods.”(段落或上下文)和“What is his name?” (問(wèn)題)。

          4. ONNX模型

          在進(jìn)一步探討利用 ML.NET 實(shí)現(xiàn)對(duì)象檢測(cè)應(yīng)用之前,我們還需要介紹一個(gè)理論上的內(nèi)容。那就是 開(kāi)放神經(jīng)網(wǎng)絡(luò)交換( Open Neural Network Exchange,ONNX)文件格式。這種文件格式是人工智能模型的一種開(kāi)源格式,它支持框架之間的 互操作性。

          你可以用機(jī)器學(xué)習(xí)的框架(比如 PyTorch)來(lái)訓(xùn)練模型,保存模型,并將其轉(zhuǎn)換為 ONNX 格式。那么你就可以將 ONNX 模型用于另一個(gè)框架,比如 ML.NET。這正是我們?cè)诒窘坛讨兴龅膬?nèi)容。你可以在 ONNX 網(wǎng)站(https://onnx.ai/)上找到詳細(xì)信息。

          在本教程中,我們使用了預(yù)訓(xùn)練 BERT 模型,在這里(https://github.com/onnx/models/tree/master/text/machine_comprehension/bert-squad)可以找到該模型,即 BERT SQUAD。簡(jiǎn)而言之就是,我們將這個(gè)模型導(dǎo)入到 ML.NET 中,并在應(yīng)用中運(yùn)行它。

          在 ONNX 模型中,有一件非常有趣且有用的事情,那就是我們可以使用一系列工具來(lái)對(duì)模型進(jìn)行可視化表示。這在像本教程一樣使用預(yù)訓(xùn)練模型的情況下很有用。

          我們常常需要知道輸入層和輸出層的名字,而這個(gè)工具在這方面很有優(yōu)勢(shì)。所以,下載 BERT 模型之后,我們就可以使用這些工具中的一種來(lái)加載它,并進(jìn)行 可視化表示。我們?cè)谶@個(gè)指南中使用 Netron,這里只有一部分輸出:

          我知道,這太瘋狂了,BERT 是個(gè)大模型。你可能會(huì)想,我怎么能用這個(gè),為什么我需要它?但是,為了使用 ONNX 模型,我們通常需要知道模型的輸入和輸出層的名稱(chēng)。BERT 看起來(lái)是下面這樣的:

          5. 用ML.NET實(shí)現(xiàn)
          在我們下載模型的 BERT-Squad 倉(cāng)庫(kù)中,你會(huì)注意到關(guān)于依賴(lài)性的有趣部分。更為確切的說(shuō),你將注意到依賴(lài)于 tokenization.py。這意味著我們需要自己進(jìn)行標(biāo)記化處理。詞標(biāo)記化是將大量的文本樣本分割成詞的一個(gè)過(guò)程。在自然語(yǔ)言處理中,每一個(gè)詞都需要捕捉,并進(jìn)行進(jìn)一步分析。做這件事的方法有很多。
          實(shí)際上,我們進(jìn)行詞編碼,并為此使用 Word-Piece Tokenization,正如這篇論文所描述的那樣。該版本由 tokenzaton.py 移植。為實(shí)現(xiàn)這一復(fù)雜的解決方案,我們構(gòu)建了這樣的解決方案:
          在 Assets 文件夾中,你可以找到下載的 .onnx 模型和包含詞匯的文件夾,我們要在這些詞匯上訓(xùn)練我們的模型。Machine Learning 文件夾包含我們?cè)谶@個(gè)應(yīng)用程序中所需要的代碼。Trainer 和 Predictor 類(lèi)就在這里,就像為數(shù)據(jù)建模的類(lèi)一樣。在單獨(dú)的文件夾中,我們可以找到用于加載文件的 helper 類(lèi)和用于 Softmax 的 Enumerable 類(lèi)型的 extension 類(lèi)以及字符串的拆分。
          這個(gè)解決方案的靈感來(lái)源于 Gjeran Vlot 的實(shí)現(xiàn),你可以在這里(https://github.com/GerjanVlot/BERT-ML.NET)找到。
          5.1 數(shù)據(jù)模型
          你可能注意到,在 DataModel 文件夾中,我們?yōu)?BERT 的輸入和預(yù)測(cè)提供了兩個(gè)類(lèi)。BertInput 類(lèi)是用來(lái)表示輸入的。它們的名稱(chēng)和大小與模型中的層類(lèi)似:
          using Microsoft.ML.Data;

          namespace BertMlNet.MachineLearning.DataModel
          {
          public class BertInput
          {
          [VectorType(1)]
          [ColumnName("unique_ids_raw_output___9:0")]
          public long[] UniqueIds { get; set; }

          [VectorType(1, 256)]
          [ColumnName("segment_ids:0")]
          public long[] SegmentIds { get; set; }

          [VectorType(1, 256)]
          [ColumnName("input_mask:0")]
          public long[] InputMask { get; set; }

          [VectorType(1, 256)]
          [ColumnName("input_ids:0")]
          public long[] InputIds { get; set; }
          }
          }

          Bertpredictions 類(lèi)使用 BERT 輸出層:

          using Microsoft.ML.Data;

          namespace BertMlNet.MachineLearning.DataModel
          {
          public class BertPredictions
          {
          [VectorType(1, 256)]
          [ColumnName("unstack:1")]
          public float[] EndLogits { get; set; }

          [VectorType(1, 256)]
          [ColumnName("unstack:0")]
          public float[] StartLogits { get; set; }

          [VectorType(1)]
          [ColumnName("unique_ids:0")]
          public long[] UniqueIds { get; set; }
          }
          }

          5.2訓(xùn)練器

          Trainer(訓(xùn)練器)類(lèi)非常簡(jiǎn)單,它只有一個(gè)方法 BuildAndTrain,使用預(yù)訓(xùn)練模型的路徑。

          using BertMlNet.MachineLearning.DataModel;
          using Microsoft.ML;
          using System.Collections.Generic;

          namespace BertMlNet.MachineLearning
          {
          public class Trainer
          {
          private readonly MLContext _mlContext;


          public Trainer()
          {
          _mlContext = new MLContext(11);
          }

          public ITransformer BuidAndTrain(string bertModelPath, bool useGpu)
          {
          var pipeline = _mlContext.Transforms
          .ApplyOnnxModel(modelFile: bertModelPath,
          outputColumnNames: new[] { "unstack:1",
          "unstack:0",
          "unique_ids:0" },
          inputColumnNames: new[] {"unique_ids_raw_output___9:0",
          "segment_ids:0",
          "input_mask:0",
          "input_ids:0" },
          gpuDeviceId: useGpu ? 0 : (int?)null);


          return pipeline.Fit(_mlContext.Data.LoadFromEnumerable(new List<BertInput>()));
          }
          }

          在上述方法中,我們建立了管道。在這里,我們應(yīng)用 ONNX 模型并將數(shù)據(jù)模型與 BERT ONNX 模型的各個(gè)層連接起來(lái)。請(qǐng)注意,我們有一個(gè)標(biāo)志,可以用來(lái)在 CPU 或 GPU 上訓(xùn)練這個(gè)模型。最后,我們將該模型與空白數(shù)據(jù)進(jìn)行擬合。這么做的目的是加載數(shù)據(jù)模式,即加載模型。

          5.3 預(yù)測(cè)器

          Predictor(預(yù)測(cè)器)類(lèi)甚至更加簡(jiǎn)單。它接收一個(gè)經(jīng)過(guò)訓(xùn)練和加載的模型,并創(chuàng)建一個(gè)預(yù)測(cè)引擎。然后它使用這個(gè)預(yù)測(cè)引擎為新圖像創(chuàng)建預(yù)測(cè)。

              using BertMlNet.MachineLearning.DataModel;
          using Microsoft.ML;

          namespace BertMlNet.MachineLearning
          {
          public class Predictor
          {
          private MLContext _mLContext;
          private PredictionEngine<BertInput, BertPredictions> _predictionEngine;

          public Predictor(ITransformer trainedModel)
          {
          _mLContext = new MLContext();
          _predictionEngine = _mLContext.Model
          .CreatePredictionEngine<BertInput, BertPredictions>(trainedModel);
          }

          public BertPredictions Predict(BertInput encodedInput)
          {
          return _predictionEngine.Predict(encodedInput);
          }
          }
          }


          5.4 助手與擴(kuò)展

          有一個(gè) helper(助手)類(lèi)和兩個(gè) extension(擴(kuò)展)類(lèi)。helper 類(lèi) FileReader 有一個(gè)讀取文本文件的方法。我們稍后用它來(lái)從文件中加載詞匯表。它非常簡(jiǎn)單:

                  using System.Collections.Generic;
          using System.IO;

          namespace BertMlNet.Helpers
          {
          public static class FileReader
          {
          public static List<string> ReadFile(string filename)
          {
          var result = new List<string>();

          using (var reader = new StreamReader(filename))
          {
          string line;

          while ((line = reader.ReadLine()) != null)
          {
          if (!string.IsNullOrWhiteSpace(line))
          {
          result.Add(line);
          }
          }
          }

          return result;
          }
          }
          }

          有兩個(gè) extension 類(lèi)。一個(gè)用于對(duì)元素集合進(jìn)行 Softmax 操作,另一個(gè)用于分割字符串并一次處理一個(gè)結(jié)果。

          using System;
          using System.Collections.Generic;
          using System.Linq;

          namespace BertMlNet.Extensions
          {
          public static class SoftmaxEnumerableExtension
          {
          public static IEnumerable<(T Item, float Probability)> Softmax<T>(
          this IEnumerable<T> collection,
          Func<T, float> scoreSelector)
          {
          var maxScore = collection.Max(scoreSelector);
          var sum = collection.Sum(r => Math.Exp(scoreSelector(r) - maxScore));

          return collection.Select(r => (r, (float)(Math.Exp(scoreSelector(r) - maxScore) / sum)));
          }
          }
          }
          using System.Collections.Generic;

          namespace BertMlNet.Extensions
          {
          static class StringExtension
          {
          public static IEnumerable<string> SplitAndKeep(
          this string inputString, params char[] delimiters)
          {
          int start = 0, index;

          while ((index = inputString.IndexOfAny(delimiters, start)) != -1)
          {
          if (index - start > 0)
          yield return inputString.Substring(start, index - start);

          yield return inputString.Substring(index, 1);

          start = index + 1;
          }

          if (start < inputString.Length)
          {
          yield return inputString.Substring(start);
          }
          }
          }
          }

          5.5 詞法分析器

          到目前為止,我們已經(jīng)探索過(guò)解決方案的簡(jiǎn)單部分。接下來(lái),我們來(lái)看一看如何實(shí)現(xiàn)標(biāo)記化,從而了解更復(fù)雜和重要的部分。先定義一個(gè)默認(rèn)的 BERT 標(biāo)記列表。舉例來(lái)說(shuō),兩個(gè)句子都應(yīng)該使用 [SEP] 標(biāo)記來(lái)區(qū)分。[CLS] 標(biāo)記總是出現(xiàn)在文本的開(kāi)頭,并特定于分類(lèi)任務(wù)。

          namespace BertMlNet.Tokenizers
          {
          public class Tokens
          {
          public const string Padding = "";
          public const string Unknown = "[UNK]";
          public const string Classification = "[CLS]";
          public const string Separation = "[SEP]";
          public const string Mask = "[MASK]";
          }
          }

          在 Tokenizer(詞法分析器)類(lèi)中完成標(biāo)記化的過(guò)程。有兩個(gè)公共方法:Tokenize 和 Untokenize。第一個(gè)方法首先將接收的的文本分割成若干句子,然后對(duì)于每個(gè)句子,每個(gè)詞都被轉(zhuǎn)換為嵌入。需要注意的是,一個(gè)詞可能會(huì)出現(xiàn)用多個(gè)標(biāo)記表示的情況。

          舉例來(lái)說(shuō),單詞“embeddings”表示為標(biāo)記數(shù)組:['em', '##bed', '##ding', '##s']。這個(gè)詞已經(jīng)被分割成更小的子詞和字符,其中一些子詞前面有兩個(gè) # 號(hào),這只是我們的詞法分析器的方式,表示這個(gè)子詞或字符是一個(gè)大詞的一部分,前面是另一個(gè)子詞。

          因此,例如,'##bed' 標(biāo)記與 'bed' 標(biāo)記是分開(kāi)的。標(biāo)記方法所做的另一件事是返回詞匯索引和分割索引。這兩個(gè)都是 BERT 輸入。如果想知道更多的原因,請(qǐng)查閱這篇文章《BERT 詞嵌入教程》(BERT Word Embeddings Tutorial)(https://mccormickml.com/2019/05/14/BERT-word-embeddings-tutorial/)。

          using BertMlNet.Extensions;
          using System;
          using System.Collections.Generic;
          using System.Linq;

          namespace BertMlNet.Tokenizers
          {
          public class Tokenizer
          {
          private readonly List<string> _vocabulary;

          public Tokenizer(List<string> vocabulary)
          {
          _vocabulary = vocabulary;
          }

          public List<(string Token, int VocabularyIndex, long SegmentIndex)> Tokenize(params string[] texts)
          {
          IEnumerable<string> tokens = new string[] { Tokens.Classification };

          foreach (var text in texts)
          {
          tokens = tokens.Concat(TokenizeSentence(text));
          tokens = tokens.Concat(new string[] { Tokens.Separation });
          }

          var tokenAndIndex = tokens
          .SelectMany(TokenizeSubwords)
          .ToList();

          var segmentIndexes = SegmentIndex(tokenAndIndex);

          return tokenAndIndex.Zip(segmentIndexes, (tokenindex, segmentindex)
          => (tokenindex.Token, tokenindex.VocabularyIndex, segmentindex)).ToList();
          }

          public List<string> Untokenize(List<string> tokens)
          {
          var currentToken = string.Empty;
          var untokens = new List<string>();
          tokens.Reverse();

          tokens.ForEach(token =>
          {
          if (token.StartsWith("##"))
          {
          currentToken = token.Replace("##", "") + currentToken;
          }
          else
          {
          currentToken = token + currentToken;
          untokens.Add(currentToken);
          currentToken = string.Empty;
          }
          });

          untokens.Reverse();

          return untokens;
          }

          public IEnumerable<long> SegmentIndex(List<(string token, int index)> tokens)
          {
          var segmentIndex = 0;
          var segmentIndexes = new List<long>();

          foreach (var (token, index) in tokens)
          {
          segmentIndexes.Add(segmentIndex);

          if (token == Tokens.Separation)
          {
          segmentIndex++;
          }
          }

          return segmentIndexes;
          }

          private IEnumerable<(string Token, int VocabularyIndex)> TokenizeSubwords(string word)
          {
          if (_vocabulary.Contains(word))
          {
          return new (string, int)[] { (word, _vocabulary.IndexOf(word)) };
          }

          var tokens = new List<(string, int)>();
          var remaining = word;

          while (!string.IsNullOrEmpty(remaining) && remaining.Length > 2)
          {
          var prefix = _vocabulary.Where(remaining.StartsWith)
          .OrderByDescending(o => o.Count())
          .FirstOrDefault();

          if (prefix == null)
          {
          tokens.Add((Tokens.Unknown, _vocabulary.IndexOf(Tokens.Unknown)));

          return tokens;
          }

          remaining = remaining.Replace(prefix, "##");

          tokens.Add((prefix, _vocabulary.IndexOf(prefix)));
          }

          if (!string.IsNullOrWhiteSpace(word) && !tokens.Any())
          {
          tokens.Add((Tokens.Unknown, _vocabulary.IndexOf(Tokens.Unknown)));
          }

          return tokens;
          }

          private IEnumerable<string> TokenizeSentence(string text)
          {
          // remove spaces and split the , . : ; etc..
          return text.Split(new string[] { " ", " ", "\r\n" }, StringSplitOptions.None)
          .SelectMany(o => o.SplitAndKeep(".,;:\\/?!#$%()=+-*\"'–_`<>&^@{}[]|~'".ToArray()))
          .Select(o => o.ToLower());
          }
          }
          }

          另一個(gè)公共方法是 Untokenize。這個(gè)方法被用于逆轉(zhuǎn)這一過(guò)程。從根本上說(shuō),BERT 的輸出會(huì)產(chǎn)生大量的嵌入信息。這個(gè)方法的目的是把這些信息轉(zhuǎn)化成有意義的句子。

          該類(lèi)具有使該過(guò)程成為現(xiàn)實(shí)的多種方法。

          5.6 BERT

          Bert 類(lèi)將所有這些東西放在一起。在構(gòu)造函數(shù)中,我們讀取詞匯文件并實(shí)例化 Train、Tokenizer 和 Predictor 對(duì)象。這里只有一個(gè)公共方法:Predict。這個(gè)方法接收上下文和問(wèn)題。作為輸出,將檢索出具有概率的答案:

          using BertMlNet.Extensions;
          using BertMlNet.Helpers;
          using BertMlNet.MachineLearning;
          using BertMlNet.MachineLearning.DataModel;
          using BertMlNet.Tokenizers;
          using System.Collections.Generic;
          using System.Linq;

          namespace BertMlNet
          {
          public class Bert
          {
          private List<string> _vocabulary;

          private readonly Tokenizer _tokenizer;
          private Predictor _predictor;

          public Bert(string vocabularyFilePath, string bertModelPath)
          {
          _vocabulary = FileReader.ReadFile(vocabularyFilePath);
          _tokenizer = new Tokenizer(_vocabulary);

          var trainer = new Trainer();
          var trainedModel = trainer.BuidAndTrain(bertModelPath, false);
          _predictor = new Predictor(trainedModel);
          }

          public (List<string> tokens, float probability) Predict(string context, string question)
          {
          var tokens = _tokenizer.Tokenize(question, context);
          var input = BuildInput(tokens);

          var predictions = _predictor.Predict(input);

          var contextStart = tokens.FindIndex(o => o.Token == Tokens.Separation);

          var (startIndex, endIndex, probability) = GetBestPrediction(predictions, contextStart, 20, 30);

          var predictedTokens = input.InputIds
          .Skip(startIndex)
          .Take(endIndex + 1 - startIndex)
          .Select(o => _vocabulary[(int)o])
          .ToList();

          var connectedTokens = _tokenizer.Untokenize(predictedTokens);

          return (connectedTokens, probability);
          }

          private BertInput BuildInput(List<(string Token, int Index, long SegmentIndex)> tokens)
          {
          var padding = Enumerable.Repeat(0L, 256 - tokens.Count).ToList();

          var tokenIndexes = tokens.Select(token => (long)token.Index).Concat(padding).ToArray();
          var segmentIndexes = tokens.Select(token => token.SegmentIndex).Concat(padding).ToArray();
          var inputMask = tokens.Select(o => 1L).Concat(padding).ToArray();

          return new BertInput()
          {
          InputIds = tokenIndexes,
          SegmentIds = segmentIndexes,
          InputMask = inputMask,
          UniqueIds = new long[] { 0 }
          };
          }

          private (int StartIndex, int EndIndex, float Probability) GetBestPrediction(BertPredictions result, int minIndex, int topN, int maxLength)
          {
          var bestStartLogits = result.StartLogits
          .Select((logit, index) => (Logit: logit, Index: index))
          .OrderByDescending(o => o.Logit)
          .Take(topN);

          var bestEndLogits = result.EndLogits
          .Select((logit, index) => (Logit: logit, Index: index))
          .OrderByDescending(o => o.Logit)
          .Take(topN);

          var bestResultsWithScore = bestStartLogits
          .SelectMany(startLogit =>
          bestEndLogits
          .Select(endLogit =>
          (
          StartLogit: startLogit.Index,
          EndLogit: endLogit.Index,
          Score: startLogit.Logit + endLogit.Logit
          )
          )
          )
          .Where(entry => !(entry.EndLogit < entry.StartLogit || entry.EndLogit - entry.StartLogit > maxLength || entry.StartLogit == 0 && entry.EndLogit == 0 || entry.StartLogit < minIndex))
          .Take(topN);

          var (item, probability) = bestResultsWithScore
          .Softmax(o => o.Score)
          .OrderByDescending(o => o.Probability)
          .FirstOrDefault();

          return (StartIndex: item.StartLogit, EndIndex: item.EndLogit, probability);
          }
          }
          }

          Predict 方法會(huì)執(zhí)行一些步驟。讓我們來(lái)詳細(xì)討論一下。

          public (List<string> tokens, float probability) Predict(string context, string question)
          {
          var tokens = _tokenizer.Tokenize(question, context);
          var input = BuildInput(tokens);

          var predictions = _predictor.Predict(input);

          var contextStart = tokens.FindIndex(o => o.Token == Tokens.Separation);

          var (startIndex, endIndex, probability) = GetBestPrediction(predictions,
          contextStart,
          20,
          30);

          var predictedTokens = input.InputIds
          .Skip(startIndex)
          .Take(endIndex + 1 - startIndex)
          .Select(o => _vocabulary[(int)o])
          .ToList();

          var connectedTokens = _tokenizer.Untokenize(predictedTokens);

          return (connectedTokens, probability);
          }

          首先,該方法對(duì)問(wèn)題和傳遞的上下文(基于 BERT 應(yīng)該給出答案的段落)進(jìn)行標(biāo)記化。基于這些信息,我們建立了 BertInput。這是在 BertInput 方法中完成的。基本上,所有標(biāo)記化的信息都被填充了,因此可以將其作為 BERT 的輸入,并用于初始化 BertInput 對(duì)象。

          然后我們從 Predictor 獲得模型的預(yù)測(cè)結(jié)果。這些信息會(huì)得到額外的處理,并且根據(jù)上下文找到最佳預(yù)測(cè)。也就是說(shuō),BERT 從上下文中選出最有可能是答案的詞,然后我們選出最好的詞。最后,這些詞都是未標(biāo)記的。

          5.7 程序

          Program(程序)是利用了我們?cè)?Bert 類(lèi)中實(shí)現(xiàn)的內(nèi)容。首先,讓我們定義啟動(dòng)設(shè)置:

          {
          "profiles": {
          "BERT.Console": {
          "commandName": "Project",
          "commandLineArgs": "\"Jim is walking through the woods.\" \"What is his name?\""
          }
          }
          }

          我們定義了兩個(gè)命令行參數(shù):“Jim is walking throught the woods.”和“What is his name?”。正如我們已經(jīng)提到的,第一個(gè)參數(shù)是上下文,第二個(gè)參數(shù)是問(wèn)題。Main 方法是最小的。

          using System;
          using System.Text.Json;


          namespace BertMlNet
          {
          class Program
          {
          static void Main(string[] args)
          {
          var model = new Bert("..\\BertMlNet\\Assets\\Vocabulary\\vocab.txt",
          "..\\BertMlNet\\Assets\\Model\\bertsquad-10.onnx");


          var (tokens, probability) = model.Predict(args[0], args[1]);


          Console.WriteLine(JsonSerializer.Serialize(new
          {
          Probability = probability,
          Tokens = tokens
          }));
          }
          }

          在技術(shù)上,我們用詞匯表文件的路徑和模型的路徑創(chuàng)建 Bert 對(duì)象。然后我們用命令行參數(shù)調(diào)用 Predict 方法。我們得到的輸出是這樣的:

          {"Probability":0.9111285,"Tokens":["jim"]}

          我們可以看到,BERT 有 91% 的把握認(rèn)為問(wèn)題的答案是“Jim”,而且是正確的。

          結(jié)語(yǔ)

          通過(guò)這篇文章,我們了解了 BERT 的工作原理。更具體地說(shuō),我們有機(jī)會(huì)探索 Transformer 架構(gòu)的工作原理,并了解 BERT 如何利用該架構(gòu)來(lái)理解語(yǔ)言。最后,我們學(xué)習(xí)了 ONNX 模型格式,以及如何將它用于 ML.NET。

          作者介紹

          Nikola M. Zivkovic 是 Rubik's Code 的首席人工智能官,也是《Deep Learning for Programmers》(尚無(wú)中譯本)一書(shū)的作者。熱愛(ài)知識(shí)分享,是一位經(jīng)驗(yàn)豐富的演講者,也是塞爾維亞諾維薩德大學(xué)的客座講師。

          原文鏈接:

          https://rubikscode.net/2021/04/19/machine-learning-with-ml-net-nlp-with-bert/


          ——END——


          點(diǎn)個(gè)在看 paper不斷!

          瀏覽 53
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  欧美一级欧美三级 | 顶级毛片高清免费精品视频 | 中文字幕成人在线观看 | 久久99人妻精品 | 亚洲国产成人Av片在线观看 |