iOS MachineLearning 系列(19)—— 分析文本中的问题答案 本篇文章将介绍Apple官方推荐的唯一的一个文本处理模型:BERT-SQuAD。此模型用来分析一段文本,并根据提供的问题在文本中寻找答案。需要注意,BERT模型不会生成新的句子,它会从提供的文本中找到最有可能的答案段落或句子。
BERT模型的使用比较复杂,大致可以分为如下几步:
将词汇表导入。
将问题和原文档分解为Token序列。
使用词汇表将Token序列转换成id序列。
将id序列转换成模型需要的多维数组进行输入。
根据分析的结果解析答案序列。
根据词汇表将答案序列转换为可读的字符串。
我们一步一步来进行介绍。
1 - 词汇表 词汇表没有过多需要讲的,其中定义了每个词汇对应的id,在本文末尾会有示例代码工程,工程中自带了需要使用的词汇表。此词汇表包含了3万余个词汇,每个词汇独占一行,其行号即表示当前词汇的id值。
加载词汇表的示例代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 var tokensDic = Dictionary <Substring , Int >()func readDictionary () { let fileName = "bert-base-uncased-vocab" guard let url = Bundle .main.url(forResource: fileName, withExtension: "txt" ) else { fatalError ("Vocabulary file is missing" ) } guard let rawVocabulary = try ? String (contentsOf: url) else { fatalError ("Vocabulary file has no contents." ) } let words = rawVocabulary.split (separator: "\n" ) let values = 0 ..<words.count tokensDic = Dictionary (uniqueKeysWithValues: zip (words, values)) }
2 - 将问题和原文档分解为Token序列 在本系列前面的文章中,有介绍过NaturalLanguage这个框架,其实用来进行自然语言处理的,当然也包含文本的Token分解功能。示例代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 var wordTokens = [Substring ]()let tagger = NLTagger (tagSchemes: [.tokenType])tagger.string = content.lowercased() tagger.enumerateTags(in : tagger.string!.startIndex ..< tagger.string!.endIndex, unit: .word, scheme: .tokenType, options: [.omitWhitespace]) { (_ , range) -> Bool in wordTokens.append(tagger.string![range]) return true } var questionTokens = [Substring ]()tagger.string = question.lowercased() tagger.enumerateTags(in : tagger.string!.startIndex ..< tagger.string!.endIndex, unit: .word, scheme: .tokenType, options: [.omitWhitespace]) { (_ , range) -> Bool in questionTokens.append(tagger.string![range]) return true }
3 - 将Token序列转换成ID序列 第三步,根据词汇表来将Token序列转换成ID序列,如下:
1 2 3 4 5 6 7 8 9 10 readDictionary() let questionTokenIds = questionTokens.compactMap { token in tokensDic[token] } let contentTokenIds = wordTokens.compactMap { token in tokensDic[token] }
4 - 将ID序列转换为模型的输入 这一步略微复杂,首先我们先看下BERT-SQuAD模式的输入输出:
需要注意,其输入有两个,wordIDs是ID序列二维数组,其中包含问题,源文档,使用特殊的分隔符进行分割。wordTypes也是一个二维数组,对应的标记wordIDs数组中每个元素的意义。示例代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 let startToken = 101 let separatorToken = 102 let padToken = 0 var inputTokens:[Int ] = []inputTokens.append(startToken) inputTokens.append(contentsOf: questionTokenIds) inputTokens.append(separatorToken) inputTokens.append(contentsOf: contentTokenIds) inputTokens.append(separatorToken) while inputTokens.count < 384 { inputTokens.append(padToken) } var inputTokenTypes:[Int ] = []inputTokenTypes.append(0 ) inputTokenTypes.append(contentsOf: Array (repeating: 1 , count : questionTokenIds.count )) inputTokenTypes.append(0 ) inputTokenTypes.append(contentsOf: Array (repeating: 1 , count : contentTokenIds.count )) inputTokenTypes.append(0 ) while inputTokenTypes.count < 384 { inputTokenTypes.append(0 ) } var tokenMultiArray = try ! MLMultiArray (shape: [1 , 384 ], dataType: .int32)for (index, inputToken) in inputTokens.enumerated() { tokenMultiArray[[0 , NSNumber (integerLiteral: index)]] = NSNumber (integerLiteral: inputToken) } var typesMultiArray = try ! MLMultiArray (shape: [1 , 384 ], dataType: .int32)for (index, inputToken) in inputTokenTypes.enumerated() { typesMultiArray[[0 , NSNumber (integerLiteral: index)]] = NSNumber (integerLiteral: inputToken) }
5 - 使用模型进行预测 准备好了输入数据,这一步就非常简单,示例如下:
1 2 3 4 let model = try ! BERTSQUADFP16 (configuration: MLModelConfiguration ())let input = BERTSQUADFP16Input (wordIDs: tokenMultiArray, wordTypes: typesMultiArray)let output = try ! model.prediction(input: input)handleOutput(output: output)
6 - 处理输出 BERT-SQuAD模型的输出为两个1*1*384*1的四位数组,指定了答案的起始位置与结束位置。虽然输出数据为4维的,但是其有3各维度都只有1个元素,因此我们可以将其提取为一维的,定义方法如下:
1 2 3 4 5 6 7 extension MLMultiArray { func doubleArray () -> [Double ] { let unsafeMutablePointer = dataPointer.bindMemory(to: Double .self , capacity: count ) let unsafeBufferPointer = UnsafeBufferPointer (start: unsafeMutablePointer, count : count ) return [Double ](unsafeBufferPointer) } }
处理输出数据如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 func handleOutput (output: BERTSQUADFP16Output) { var startIndex = 0 for p in startIndex ..< output.startLogits.doubleArray().count { if output.startLogits.doubleArray()[p] > output.startLogits.doubleArray()[startIndex] { startIndex = p } } var endIndex = startIndex for p in endIndex ..< startIndex + 5 { if output.endLogits.doubleArray()[p] > output.endLogits.doubleArray()[startIndex] { endIndex = p } } let subs = inputTokens[startIndex ..< endIndex] for i in subs { for item in tokensDic where item.value == i { print (item.key) } } }
代码运行效果如下图所示:
本中所涉及到的代码,都可以在如下 Demo 中找到:
https://github.com/ZYHshao/MachineLearnDemo
本系列文章到此已经将Apple官方所推荐的模型都做了介绍,当然这些模式的训练都是广泛的,不一定会适用于你的应用场景,CoreML框架也提供了更加强大的模型训练能力,我们可以根据自己的场景并提供有针对性的数据进行个性化的模型训练,在后续文章中会再详细讨论。