用Word Tagger进行实时文本分析:专业计算机视觉,第一部分

146 阅读14分钟

作者。 Maxim Skorynin, Evil Martians的iOS工程师和Travis Turner, Evil Martians的技术编辑

在这个系列中,我想分享我在计算机视觉方面的工作经验。为了做到这一点,我们将玩一个示例应用程序,它可以使用手机摄像头识别食谱的英文文本,然后可以将这些信息转换成一组类对象。而这一切都将在设备上实时发生!🕶️

在某些时候,你可能已经使用了利用计算机视觉来识别图像中的文本的应用程序。这最常见于在线翻译器或简单的文本扫描器中。2021年下半年,移动世界在这一领域获得了巨大的能量:随着iOS 15.0版本的发布,苹果在本地相机应用中加入了 实时文本到本地相机应用中,允许即时文本识别,并让用户与图像中出现的任何文本、链接和电话号码无缝互动。

这一基本功能--从图片中读取文字--不再让人感到惊讶。但是,如果我们想更进一步,能够将识别的单词和字符变成一组类对象呢?这将使我们能够以任何我们可能需要的方式对原始文本进行格式化:我们可以重新安排句子的部分内容,删除多余的单词,或者添加缺失的单词。

On the left is a recipe for sponge cake, in the middle an arrow indicates some magic process will happen, and on the right we see the elements from the sponge cake recipe converted into class objects

这可能看起来是一项复杂的任务,但不用担心,机器学习又来救场了!幸运的是,Create ML的模板包括Word Tagger模型,它允许我们在自然语言文本中标记单个单词和短语。对于实现我们的目标来说是完美的!

The word tagging template selected within Create ML

我们的目标

我们希望最后能有这样的效果。

正在运行的菜谱阅读器

为了实现我们的菜谱阅读器,我们将通过几个阶段进行。

  1. 实际训练模型
  2. 实现一个能够从视频流中识别文本的移动应用程序
  3. 使用Word Tagger模型将得到的文本转换为一个对象阵列

在本系列的第一部分,我们将介绍Word Tagger模型是如何工作的,准备原始数据,在Xcode Playground中编写一个程序,从原始数据中生成一个数据集,然后我们将训练我们的模型。

了解Word Tagger模型的要求

但是,在我们走得太远之前,重要的是要更好地了解Word Tagger是如何工作的以及它的数据要求。让我们举一个简单的例子,设想我们的任务是取一个句子,并在其中找到任何行星的名字。🪐

为了创建和训练模型,我们需要一个JSON文件,它的结构如下。

[	{	 "tokens" : ["Uranus", "and", "Neptune", "look", "alike", "."],
	 "labels" : ["planet", "none", "planet", "none", "none", "none"]
	},

	{
	 "tokens" : ["Earth", "comes", "third", "."],
	 "labels" : ["planet", "none", "none", "none"]
	},

	{
	 "tokens" : ["Welcome", "to", "Mars", "!"],
	 "labels" : ["none", "none", "planet", "none"]
	}
]

JSON由一个对象数组组成,每个对象有两个必要的字段。

  • tokens - 一个由单词、搭配和标点符号组成的数组
  • labels - 对应于每个tokens 数组元素的标签数组。

❗️重要的是两个数组的长度都要匹配!

因为我们只想找到行星的名字,所以标签集里的元素被赋予了 "行星 "标签,而所有其他元素都有 "无 "标签。

简单地说,要创建一个单词标签模型,我们需要一个带有现实生活中自然发音的例子的句子集,其中每个单词都会有自己相应的标签。听起来很容易,对吗?但是,有一个问题!在这种情况下,我们需要一个有真实生活、自然发音的句子集。在这种情况下,我们需要一个巨大的数据集,越大越好。我们在下面看到的菜谱是相当小的,有一些可识别的模式,但想象一下,如果我们有一个20个字的句子,其中 "土星 "这个词在第17个位置。

使这项任务复杂化的是,行星名称几乎可以放在句子的任何地方,以及一个特定句子中的字数不受限制的事实。此外,我们还需要在句子中找到这些词的所有可能的组合。😅

因此,为了演示的目的,我们现在先看看我们的--稍稍容易处理的任务。

处理主要任务

我们的演示项目的主要目标是识别和解析菜谱文本。我们需要弄清楚从头开始创建我们的数据集需要什么。为了得到最好的结果,我们需要仔细分析足够数量的现实生活中的例子。这个过程需要收集用于训练的数据要求,并有助于找到可识别文本的共同特征,以及边缘案例。

🗒️首先要注意的是,大多数菜谱都是以列表的形式写成的,其中每一行都包含一种成分的信息--为了我们的目的,这些行都可以被视为一个句子。这意味着所识别的文本将是一个元素(句子)的列表,我们将把它传递给我们的Word Tagger模型进行处理。

A recipe for delicious sponge cake

嗯...听起来很美味。🤤

稍微分析一下,似乎每一行总是由几个元素组成,按照一定的顺序书写,如下图所示。

The milk item on the recipe with each component underlined and labels as a value, measurement, or ingredient

但实际上,这具有误导性。我能够找到至少4种记录成分信息的记号。主要的区别与元素的数量和排列顺序有关。

Several recipe items with labels for values, measurement type, and ingredient type

关键点

  • 一个字符串只能由原料名称组成。
  • 一个给定的测量值可能是缺失的。
  • 数字值和计量单位可以合并(如上面例子中的0.5tbsp )。这种情况经常发生,我们将以一种特殊的方式处理。
  • 成分名称和测量值并不总是由一个词组成,而且,有时还表明了加工方法。(在这种情况下,我们将在原料名称中加入加工方法。

A labeled ingredient item with a multi-word ingredient emphasized

  • 措施可以有不同的符号选择。它们可以是缩写形式(有点和无点)或复数形式。所有这些在创建数据集时都必须考虑到。

A collection of various measurement notions used in recipes

  • 数值也可以这么说。例如,小数可以有多种写法:用点、斜线或逗号分开。

A collection of various value notations possible in recipes

寻找数据来训练我们的模型

乍一看,可能会觉得可能的数值种类太多,甚至无法处理!但是,这并不妨碍我们找到一个可以训练模型的数据。嗯,是的,世界上确实有大量的成分类型,但是,尽管如此,这个数据池仍然是有限的。不要惊慌--只要放松🧘现在,我们将一起处理原始数据的来源,并准备好创建我们的数据集所需的一切。

我设法在Kaggle上找到了一组成分名称。该资源是开放的,没有授权,所以我们可以在我们的项目中使用它。它是一个JSON文件,包含6714个独特的项目。

不可能找到一个现成的测量数据集。然而,这相当容易自己制作。为了做到这一点,我分析了一些描述可能的测量值的资源,并考虑到了本文上一节中描述的各种符号选项。

接下来,基于这些数据,看一下我用Xcode Playground创建的程序代码。特别是注意getMeasures方法,它生成了测量集。请注意,流体盎司的各种变化被放在另一个数组中,因为它们由两个词组成,与其他例子不同。

func getMeasures() -> [WordObject] {
    var measures = ["tbsp", "tbsp.", "tablespoon", "tablespoons", "tb.", "tb", "tbl.", "tbl", "tsp", "tsp.", "teaspoon", "teaspoons", "oz", "oz.", "ounce", "ounces", "c", "c.", "cup", "cups", "qt", "qt.", "quart", "pt", "pt.", "pint", "pints", "ml", "milliliter", "milliliters", "g", "gram", "grams", "kg", "kilogram", "kilograms", "l", "liter", "liters", "pinch", "pinches", "gal", "gal.", "gallons", "lb.", "lb", "pkg.", "pkg", "package", "packages","can", "cans", "box", "boxes", "stick", "sticks", "bag", "bags"]

    measures += ["fluid ounce", "fluid ounces", "fl. oz"].flatMap { Array(repeating: $0, count: 10) }
    return measures.map { WordObject(token: $0, label: .measure) }.shuffled()
}

我们在这里触及了一些重要的东西:模式的概念!在学习过程中,Word Tagger模型不仅要处理数值和标签,还要处理句子中单词的顺序和出现次数,因此,它可以对不在训练集中的例子做出正确的预测。不要担心一些现有的值会在最终的JSON中缺席。

让我们回到fluid ounces :这些是唯一由两个词组成的计量单位。最初,只有3个项目。fluid ounce,fluid ounces, 和fl. oz 。这些测量单位所遵循的模式(由两个词组成)在大多数单字测量单位中很少出现。如果不考虑这一点,那么在生成数据集时,含有fl.oz.的例子的数量会相对较少。出于这个原因,我将count ,即fl. oz ,增加到10 份。这将允许我们在JSON文件中添加更多这种类型的例子,因此,我们的ML模型将能够更好地处理这种情况。

measures += ["fluid ounce", "fluid ounces", "fl. oz"].flatMap { Array(repeating: $0, count: 10) }

让我们继续讨论getValues方法,它生成了一个数值集。我尽可能地采取在现实生活中可以找到的各种数值:不同符号的小数,从1到9的整数,以及从10到1000的整数,步骤为25。

func getValues() -> [WordObject] {
    var values: [String] = [
        "1/2", "1/3", "1/4", "1/5", "2/3", "3/4",
        "0,25", "0.25", "0,5", "1,5", "0.5", "1.5", "2.5", "2,5",
        "1", "2", "3", "4", "5", "6", "7", "8", "9"
    ]

    values += values.flatMap { Array(repeating: $0, count: 2) }
    values += (10 ... 1000).filter { $0 % 25 == 0 }.map { String($0) }

    return values.map { WordObject(token: $0, label: .value) }.shuffled()
}

我还增加了小数例子的数量,以配合整数例子的数量。

values += values.flatMap { Array(repeating: $0, count: 2) }

数据集准备

现在,让我们弄清楚如何将我们的原始数据集变成可以用来训练模型的JSON。我很有信心,你可以(或已经)自己搞清楚程序代码。💪

不过,下面我还是会试着描述你需要确保和实现的最重要的几点。这个程序的代码应该做到以下几点。

  1. 将带有原料名称的JSON转换成一组唯一的数值,并准备好测量单位和数值。
  2. 提出一套建议,考虑到本文处理主要任务部分中描述的所有细微差别。
  3. 将生成的JSON文件保存到磁盘。

注意getIngredients方法,它解析了我们在Kaggle上找到的JSON文件。最初,这个文件由一个对象列表组成,每个对象都是一个描述所需成分的食谱。有些位置可能会重复。这个文件中有39774个原料名称,但其中只有6714个是唯一的值。为了不费吹灰之力从整个列表中只获得唯一的原料名称,我们使用Set 数据结构。

return Set(jsonArray.flatMap { $0.ingredients }).map {
    return WordObject(token: $0, label: .ingredient)
}

接下来是generateSentences方法,它将3组数据(valuesmeasuresingredients )组合成真正的句子。看一下下面的代码。通过所有的成分名称,程序得出了4种不同类型的句子,我们在处理主要任务部分也提到了这一点。

func generateSentences(ingredients: [WordObject], measures: [WordObject], values: [WordObject]) -> Set<[WordObject]> {
    var measureIndex = 0
    var valueIndex = 0

    var isFirstWay = true

    return ingredients.reduce(into: Set<[WordObject]>()) { sentences, ingredient in
        if measureIndex == measures.count {
            measureIndex = 0
        }

        if valueIndex == values.count {
            valueIndex = 0
        }

        let measure = measures[measureIndex]
        let value = values[valueIndex]

        measureIndex += 1
        valueIndex += 1

        let token = value.token + measure.token
        let combination = WordObject(token: token, label: .combination)

        if isFirstWay {
            sentences.insert([value, ingredient]) // 10 sugar
            sentences.insert([combination, ingredient]) // 10tbsp sugar
        } else {
            sentences.insert([ingredient]) // sugar
        }

        sentences.insert([value, measure, ingredient]) // 10 tbsp sugar
        isFirstWay.toggle()
    }
}

我还想多说一点关于isFirstWay 这个变量。通过它,我决定平衡数据集,使由我们关注的三个主要部分(valuemeasure ,和ingredient )组成的句子数量占优势。在我看来,这是我们最常处理的句子类型。因此,在代码中,你可能会注意到,这样的例子是在每个reduce 方法的迭代中产生的,而不同类型的句子是在每个第二迭代中产生的。

而且,你还记得我是如何谈到成分名称或措施可能由几个词组成的情况吗?当然,我们可以通过整个复合字符串进行训练,对一个搭配中的所有单词使用一个measureingredient 标签,像这样。

A multi-word ingredient of sharp cheddar cheese, where the words are grouped as one label

但是,通过我自己的实验,我发现这并不是正确的方法。相反,一个更好的选择是分割元素,使每个词都有自己的标签。

A multi-word ingredient of sharp cheddar cheese where the ingredient value is split into 3 words

在一个JSON文件中,不同的变化看起来是这样的。

[  {   "tokens" : ["2", "fl. oz", "sharp cheddar cheese"],
   "labels" : ["value", "measure", "ingredient"]
  },
  {
   "tokens" : ["2", "fl.", "oz", "sharp", "cheddar", "cheese"],
   "labels" : ["value", "measure", "measure", "ingredient", "ingredient", "ingredient"]
  }
]

为了使之有效,我写了separateCollocations方法,它将搭配分割成独立的词,其中分隔元素是空格。

func separateCollocations(in sentences: Set<[WordObject]>) -> [SentenceObject] {
    return sentences.compactMap { sentence in
        let sentenceObject = SentenceObject()

        sentence.map { word in
            word.token.split(separator: " ").map { part in
                let newToken = String(part)

                sentenceObject.tokens.append(newToken)
                sentenceObject.labels.append(word.label)
            }
        }

        return sentenceObject
    }
}

这种搭配拆分提高了模型的准确性。为了证实这一理论,我在上述两种方法的基础上进行了两个实验。在下面的图片中,你可以看到,只要我们对整个短语使用一个标签,模型就不能区分测量和成分名称。

The preview tab results show that when using one label for an entire ingredient phrase, the model can't separate the measurement and the ingredient name itself

最后,为了确保我们已经完全吸收了训练JSON数据的算法,我提议再完整地看一遍。

A graph with a summation of the JSON training algorithm

训练模型

随着数据集准备的完成,让我们进入这个可爱过程的下一步:训练。对于训练,我们需要三个JSON文件:第一个文件用于训练(train),第二个用于验证(valid),第三个用于测试(test)。每个文件将由16785个元素组成。我们的脚本程序总是对初始值进行洗牌。这意味着,每次运行时,都会创建一个包含随机组合的文件。在程序执行结束时,新的JSON文件路径将显示在控制台。

如果你需要,这里有预先生成的数据

说得够多了,让我们继续训练我们的模型吧!🤫 首先,让我们在Create ML中使用Word Tagger模板创建一个新项目。

An empty Create ML project interface

在训练数据部分,使用名为Choose的上下文菜单(或(+)按钮,或拖放文件)并加载训练JSON文件。

A closeup of the Create ML interface, displaying how we add our JSON files for training

Create ML提示你为标签标记字段选择值。这可以通过屏幕中间相应的上下文菜单来完成。接下来,程序提示我们从训练集中抽取一部分数据来生成验证集。但是,在这种情况下,我们已经准备了两个特殊的集合。有效测试。在项目中加入验证和测试的数据。使用下面的上下文菜单选择英语。

The Create ML interface has been loaded with our JSON, here we choose the training algorithm, with Conditional Random Field currently selected

在开始Word Tagger训练之前,该模型要求我们在两种算法中选择一种。

选项1:条件随机场(CRF)是一种序列建模算法,用于识别文本中的实体或模式。这个模型不仅假设特征是相互依赖的,而且在学习模式时还考虑未来的观察。就性能而言,它被认为是序列识别的最佳方法。

方案二:我在《用Create ML进行物体检测》一文的第二部分谈到了转移学习法。当使用这种方法时,单词标记器模型的训练时间比使用CRF要长得多,而且文件大小可能会大3倍。

This chart shows the processing difference between the two algorithms, with CRF clearly the faster option

算法的最终选择可能取决于你的具体目标。例如,如果你需要为一个必须支持iOS 12.0以上版本的应用程序实现文本识别,那么你只有一个选择--CRF算法。而且,最好记住,没有一种可用的算法能保证100%的效率。

在我们的项目中,我们将使用一个用条件随机场算法创建的模型来支持iOS 13。此外,在用不同的数据集做了很多实验后,我发现CRF算法在处理那些不包括在原始数据集中的例子时表现得更好。

选择算法后,按下训练按钮,等待训练完成。如前所述,在预览标签上,你可以测试模型的性能。要导出一个模型文件,请到输出标签。然后点击获取按钮,将模型保存到磁盘。

总结

那么,让我们总结一下我们在这篇文章中所做的工作。我们已经揭示了创建单词标记器模型的数据要求,以及在哪里找到这些数据。我们了解了识别烹饪食谱这一任务所涉及的错综复杂的问题和边缘案例。之后,我们写了一个生成数据集的脚本,然后我们又训练了模型。

在本系列的下一部分中,我们将创建我们的项目。我们将整合第一阶段训练的模型,并使用两种不同的方法和两种不同的工具来实现相机和菜谱文本识别的功能。GoogleMLKit/TextRecognition(iOS 13.0)和本地Live Text工具(iOS 15.0)。请继续关注!🧑🍳


顺便说一下,如果你有一个问题,(无论是否与ML有关)邪恶的火星人已经准备好帮助检测它,分析它,并轻轻地把它送走请给我们留言!