1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        如何使用 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)題,例如圖像分類和目標(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í)行類似操作:

        假如你想了解使用 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)梯度消失(和爆炸),因此不能完全滿足需求。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 模型,我們通常需要知道模型的輸入和輸出層的名稱。BERT 看起來(lái)是下面這樣的:

        5. 用ML.NET實(shí)現(xiàn)
        在我們下載模型的 BERT-Squad 倉(cāng)庫(kù)中,你會(huì)注意到關(guān)于依賴性的有趣部分。更為確切的說(shuō),你將注意到依賴于 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 類就在這里,就像為數(shù)據(jù)建模的類一樣。在單獨(dú)的文件夾中,我們可以找到用于加載文件的 helper 類和用于 Softmax 的 Enumerable 類型的 extension 類以及字符串的拆分。
        這個(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è)類。BertInput 類是用來(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 類使用 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)練器)類非常簡(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è)器)類甚至更加簡(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(助手)類和兩個(gè) extension(擴(kuò)展)類。helper 類 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 類。一個(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)頭,并特定于分類任務(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(詞法分析器)類中完成標(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)化成有意義的句子。

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

        5.6 BERT

        Bert 類將所有這些東西放在一起。在構(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 類中實(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)
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            一级特黄60分钟免费 | 暴操嫩逼 | 五月天激情小说 | 成人毛片18女人在线播放 | 橹撸视频日日操时时操 | 泷泽萝拉无删减avhd | 淫骚嫩亚| 啊啊啊好爽在线观看 | 91欧美精品成人AAA片 | 在线观看国产一区二区 |