Press "Enter" to skip to content

ML.NET–NLP与BERT

本站内容均来自兴趣收集,如不慎侵害的您的相关权益,请留言告知,我们将尽快删除.谢谢.

到目前为止,在我们的 ML.NET 之旅中,我们专注于计算机视觉问题,如图像分类和对象检测。在本文中,我们稍微改变一个方向,探索 NLP (自然语言处理)以及我们可以通过机器学习解决的一系列问题。

 

自然语言处理  ( NLP ) 是人工智能的一个子领域,主要目标是帮助程序理解和处理自然语言数据。这个过程的输出是一个可以 “ 理解 ” 语言的计算机程序。

 

 

你害怕 AI 抢走你的工作吗?确保你是建造它的人。

 

在新兴的人工智能行业中保持相关性!

 

早在 2018 年,谷歌发表了一篇关于深度神经网络的论文,该网络被称为来自 Transformers 的双向编码器表示 (Bidirectional Encoder representation from transformer) 或 BERT 。 由于其简单性,它成为最流行的 NLP 算法之一。 有了这个算法,任何人都可以在短短几个小时内训练出自己最先进的问答系统(或各种其他模型)。在本文中,我们将做到这一点,使用 BERT 创建一个问答系统。

 

 

BERT 是一种基于 Transformers 架构的 神经网络 。这就是为什幺在本文中,我们将首先探索该架构,然后继续对 BERT 进行更深入的了解:

 

 

先决条件

 

了解Transformers架构

 

BERT直觉

 

ONNX模型

 

使用ML.NET实现

 

 

1.  先决条件

 

此处提供的实现是 C# 完成的,我们使用最新的 .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

 

 

您可以从 包管理器控制台 执行相同的 操作:

 

 

Install-Package Microsoft.ML

 

Install-Package Microsoft.ML.OnnxRuntime

 

Install-Package Microsoft.ML.OnnxTransformer

 

 

您可以使用 Visual Studio 的 Manage NuGetPackage 选项做类似的事情:

 

 

如果您需要使用 ML.NET 了解机器学习的基础知识,请查看 这篇文章 。

 

2.  理解 Transformers 架构

 

语言是顺序数据。基本上,您可以将其视为一个词流,其中每个词的含义取决于它之前的词和它之后的词。这就是为什幺计算机很难理解语言的原因,因为为了理解一个词,你需要一个 上下文 。

 

此外,有时作为输出,您还需要提供 一系列 数据(单词)。证明这一点的一个很好的例子是将英语翻译成塞尔维亚语。作为算法的输入,我们使用一个单词序列,对于输出,我们还需要提供一个序列。

 

在这个例子中,算法需要理解英语并理解如何将英语单词映射到塞尔维亚语单词(本质上这意味着必须对塞尔维亚语有一定的了解)。多年来,有许多用于此目的的 深度学习 架构,例如 循环神经网络 和 LSTM 。然而,正是 Transformer 架构的使用改变了一切。

 

 

RNN 和 LSTM 网络不能完全满足需求,因为它们难以训练并且容易出现梯度 消失 (和爆炸)。 Transformers  旨在解决这些问题,并带来更好的性能和对语言的更好 理解 。它们 于 2017 年在传奇论文 “  Attention is all you need  ” 中被介绍

 

简而言之,他们使用 Encoder-Decoder 结构和 self-attention 层来更好地理解语言。如果我们回到翻译示例,编码器负责 理解 英语,而解码器负责 理解 塞尔维亚语并将英语 映射 到塞尔维亚语。

 

 

在训练期间,进程编码器提供了来自英语的词 嵌入 。计算机不理解单词,它们理解数字和矩阵(数字集)。这就是为什幺我们将单词转换为某个 向量空间的原因 ,这意味着我们为语言中的每个单词分配某些向量(将它们映射到某个潜在向量空间)。这些是词嵌入。有许多可用的词嵌入,如  Word2Vec 。

 

但是,单词在句子中的位置对于上下文也很重要。这就是完成位置编码的原因。这就是编码器获取有关单词及其上下文的信息的方式。 Encoder 的 Self-attention 层正在确定单词之间的 关系 ,并为我们提供有关句子中每个单词的相关性的信息。这就是 Encoder 理解英语的方式。然后数据进入深度神经网络,然后进入解码器的 Mapping-Attention 层。

 

但是,在此之前,解码器获取有关塞尔维亚语的相同信息。它以同样的方式使用词嵌入、位置编码和自我注意来学习如何 理解 塞尔维亚语。解码器的映射注意层然后拥有关于英语和塞尔维亚语的 信息 ,它只是学习如何将单词从一种语言转换为另一种语言。要了解有关 Transformers 的更多信息,请查看 这篇文章 。

 

3. BERT 直觉

 

BERT 使用这种 Transformers 架构来理解语言。更准确地说,它利用了编码器。该架构实现了两大里程碑。首先,它实现了 双向性 。这意味着每个句子都以两种方式学习,并且上下文更好地学习,无论是先前的上下文还是未来的上下文。 BERT 是第一个 深度双向 无监督的 语言表示,仅使用纯文本语料库( 维基百科 )进行预训练。它也是 NLP 的首批 预训练模型 之一。我们了解了计算机视觉的迁移学习。然而,在 BERT 之前,这个概念并没有在 NLP 世界中真正流行起来。

 

这很有意义,因为您可以在大量数据上训练模型,一旦它理解了语言,您就可以针对更具体的任务对其进行 微调 。这就是为什幺 BERT 的训练可以分为两个阶段:预训练和微调。

 

 

为了实现双向性, BERT 使用两种方法进行预训练:

 

掩码语言建模—— MLM

 

下一句预测—— NSP

 

Masked Language Modeling 采用蒙面输入。这意味着句子中的某些单词被屏蔽了, 填空 是 BERT 的工作。 Next Sentence Prediction 给出两个句子作为输入,并期望 BERT 预测一个接着一个的句子。实际上,这两种方法 同时发生 。

 

 

在微调阶段,我们针对特定任务训练 BERT 。这意味着如果我们想创建问答解决方案,我们只需要训练 额外 的 BERT 层。这正是我们在本教程中所做的。我们需要做的就是用为我们的特定目的设计的一组新层替换网络的输出层。作为输入,我们将有 一段 文本(或上下文)和一个 问题 ,作为输出,我们期望得到问题的 答案 。

 

 

例如,我们的系统应该使用两个句子: “Jim is walk through the woods 。 ”  (段落或上下文)和 “ 他叫什幺名字? ”  (问题)提供答案 “ 吉姆 ” 。

 

4. ONNX 模型

 

在我们深入研究使用 ML.NET 实现对象检测应用程序之前,我们需要再介绍一件理论上的事情。那是 开放神经网络交换 (ONNX) 文件格式。此文件格式是  AI 模型的开源格式,支持框架之间的 互操作性 。

 

基本上,您可以在 PyTorch 等机器学习框架中训练模型,将其保存并将其转换为 ONNX 格式。然后,您可以在不同的框架(如 ML.NET )中使用该 ONNX 模型。这正是我们在本教程中所做的。您可以在 ONNX 网站 上找到更多信息 。

 

 

在本教程中,我们使用预训练的 BERT 模型。这种模式 在这里  是 BERT SQUAD 。本质上,我们将此模型导入 ML.NET 并在我们的应用程序中运行它。

 

我们可以用 ONNX 模型做的一件非常有趣和有用的事情是,我们可以使用许多工具来直观地表示模型。当我们在本教程中使用 预训练 模型时,这非常有用。

 

我们经常需要知道输入和输出层的名称,这种工具很适合。因此,一旦我们下载了 BERT 模型,我们就可以使用其中一种工具进行 可视化表示 。在本指南中,我们使用 Netron   ,这里只是输出的一部分:

 

 

我知道,这太疯狂了, BERT 是一个大模型。您可能想知道如何使用它以及为什幺需要它?但是,为了使用 ONNX 模型,我们通常需要知道模型的输入和输出层的名称。这是寻找 BERT 的方式:

 

 

5.  使用 ML.NET 实现

 

如果您查看我们从中下载 模型 的 BERT-Squad  存储库,您会注意到相关性部分中的一些有趣内容。更准确地说,您会注意到 tokenization.py的 依赖性 。这意味着我们需要自己执行标记化。 单词标记化是将大量文本样本拆分为单词的过程。这是自然语言处理任务中的一项要求,其中每个单词都需要被捕获并进行进一步分析。有很多方法可以做到这一点。

 

实际上,我们执行单词编码,为此我们使用 Word-Piece Tokenization ,如 本文所述 。它是从 tokenzaton.py 移植的版本 为了实现这个完整的解决方案,我们将您的解决方案构建如下:

 

 

Assets 文件夹中,您可以找到下载的 .onnx 模型和文件夹,其中包含我们想要训练模型的词汇表。该 机器学习 文件夹包含所有必要的代码,我们在这个应用程序中使用。在 训练预测 类是存在的,就像这些模型数据的类。在单独的文件夹中,我们可以找到用于在 Enumerable 类型和字符串拆分上为 Softmax 加载文件和扩展类的帮助程序类。

 

 

这个解决方案的灵感来自于 Gjeran Vlot 的实现,可以在 这里 找到。

 

5.1  数据模型

 

您可能会注意到,在   DataModel 文件夹中,我们有两个类,分别用于 BERT 的输入和预测 所述   BertInput  类是有表示输入。它们的名称和大小与模型中的层一样:

 

 

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  训练

 

训练 类是相当简单,它只有一个方法   BuildAndTrain 它使用的路径,预先训练的模式。

 

 

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>()));

 

}

 

}

 

}

 

 

在上述方法中,我们构建了管道。这里我们应用 ONNX 模型并将数据模型连接到 BERT ONNX 模型的层。请注意,我们有一个标志,可用于在 CPU 或 GPU 上训练此模型。最后,我们将此模型拟合到空数据。我们这样做,所以我们可以加载数据模式,即。加载模型。

 

5.3  预测器

 

预测 类就更简单了。它接收经过训练和加载的模型并创建预测引擎。然后它使用这个预测引擎来创建对新图像的预测。

 

 

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  助手和扩展

 

有一个辅助类和两个扩展类。辅助类   FileReader 有一个读取文本文件的方法。我们稍后使用它从文件中加载词汇。这很简单:

 

 

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;

 

}

 

}

 

}

 

 

有两个扩展类。一个用于对元素集合执行 Softmax 操作,另一个用于拆分字符串并一次输出一个结果。

 

 

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.4  分词器

 

好的,到目前为止,我们已经探索了解决方案的简单部分。让我们继续处理更复杂和更重要的部分,让我们看看我们是如何实现标记化的。首先,我们定义默认 BERT 令牌列表。例如,应始终用标记分隔两个句子 [SEP] 以区分它们。该 [CLS] 令牌总是出现在文本的开始,并具体到分类任务。

 

 

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]” ;

 

}

 

}

 

 

Tokenization 的过程在 Tokenizer 类中完成。有两种公共方法:    Tokenize  和   Untokenize   第一个首先将接收到的文本分成句子。然后对于每个句子,每个单词都被转换为嵌入。请注意,可能会发生一个单词用多个标记表示的情况。

 

例如,单词 “embeddings” 表示为标记数组 [’em’, ‘##bed’, ‘##ding’, ‘##s’] 。该词已被拆分为更小的子词和字符。其中一些子词前面的两个井号只是我们的标记器用来表示这个子词或字符是一个更大词的一部分并在另一个子词之前的方式。

 

因此,例如, ‘##bed’  标记与  ‘bed’  标记是分开的。 Tokenize 方法正在做的另一件事是返回词汇索引和分段索引。两者都用作 BERT 输入。要了解更多为什幺这样做,请查看 这篇文章 。

 

 

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
” }, StringSplitOptions.None)

 

.SelectMany(o => o.SplitAndKeep( “.,;:\\/?!#$%()=+-*\”‘–_`<>&^@{}[]|~'” .ToArray()))

 

.Select(o => o.ToLower());

 

}

 

}

 

}

 

 

另一种公共方法是   Untokenize 。此方法用于反转该过程。基本上,作为  BERT  的输出,我们将得到各种嵌入。这种方法的目标是将这些信息转换成有意义的句子。

 

这个类有多个方法来启用这个过程。

 

5.5 BERT

 

Bert 类将所有这些东西放在一起。在构造函数中,我们读取词汇文件并实例化   Train Tokenizer 和   Predictor 对象。只有一种公共方法—— Predict 。此方法接收上下文和问题。作为输出,检索具有概率的答案:

 

 

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);

 

}

 

}

 

}

 

 

预测 方法进行几个步骤。让我们更详细地探索它。

 

 

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);

 

}

 

 

首先,此方法对问题和传递的上下文(基于哪个 BERT 应该给出答案的段落)进行标记化。然后我们根据这些信息构建   BertInput 。这是在 BertInput 方法中完成的 。基本上,所有标记化的信息都被填充,因此它可以用作 BERT 输入并用于初始化   BertInput 对象。

 

然后我们从 Predictor 得到模型的 预测 。然后对这些信息进行额外处理,并从上下文中找到最佳预测。意思是, BERT 从上下文中选择最有可能成为答案的单词,然后我们选择最好的单词。最后,这些词是未标记的。

 

5.5 Program

 

程序 正在利用我们在 Bert 类中实现的内容。首先,让我们定义启动设置:

 

 

{

 

“profiles” : {

 

“BERT.Console” : {

 

“commandName” : “Project” ,

 

“commandLineArgs” : ” \” Jim is walking through the woods. \” \” What is his name? \” ”

 

}

 

}

 

}

 

 

我们定义了两个命令行 参数 : “ Jim is walking throught the woods. ”  和 “ What is his name? ” 。正如我们已经提到的,第一个是 context ,第二个是 question 。的 主要 方法是最小的:

 

 

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

 

}));

 

}

 

}

 

}

 

 

从技术上讲,我们使用词汇文件的路径和模型的路径创建   Bert  对象。然后我们使用命令行参数调用 Predict 方法。作为输出,我们得到:

 

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

 

我们可以看到 BERT 有 91% 的把握确定问题的答案是 “Jim” 并且是正确的。

 

结论

 

在本文中,我们了解了 BERT 的工作原理。更具体地说,我们有机会探索 Transformers 架构的不同工作方式以及 BERT 如何利用该架构来理解语言。最后,我们了解了 ONNX 模型格式以及如何在 ML.NET 中使用它。

 

Machine Learning with ML.NET – NLP with BERT

Be First to Comment

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注