WWDC Quick Look 💓 By SwiftGGTeam
Make apps smarter with Natural Language

Make apps smarter with Natural Language

观看原视频

Highlight

Natural Language 在 WWDC 2020 为 NLTagger 增加 tagHypotheses 置信度,为 NLEmbedding 增加句子级向量,并把 Create ML 的 transfer learning 扩展到自定义词序列标注器。

核心内容

一个新闻 App 想从文章里提取地点、人物和组织,过去能拿到 NLTagger 的预测结果,却不知道这个结果有多可信。Session 里的 Buzz 示例遇到的就是这种问题:Do Not Disturb while driving 被当成组织名,界面右侧出现了无用实体。

WWDC 2020 的 Natural Language 给这类预测补上了置信度。tagHypotheses 会返回候选标签及其 confidence score。开发者可以用代表自己业务的数据校准阈值,并按类别设置不同阈值,减少命名实体识别里的误报。

另一个痛点是搜索。Nosh 这个外卖 App 的 FAQ 只能靠用户自己滚动查找答案。词向量能表达词之间的距离,但平均每个词的向量会丢掉句子结构。新的 sentence embedding(句子嵌入)直接把一句话编码成 512 维向量,让「Where do you deliver?」这类查询能匹配到语义接近的 FAQ。

当文本规模继续变大,线性遍历就会变慢。Verse 示例把每行诗的句向量压缩成 custom embedding(自定义嵌入),再用 nearest neighbor search(最近邻搜索)找到语义最接近的诗句。最后,Nosh 还用 custom word tagger(自定义词序列标注器)从用户输入里抽取 food、restaurant、from_city、to_city 这些字段,把自由文本变成可执行的业务参数。

详细内容

用置信度过滤命名实体误判

03:46)Natural Language 的基础能力包括语言识别、分词、词性标注、词形还原和命名实体识别。过去这些 API 返回预测标签。2020 年新增的 tagHypotheses 返回候选标签和对应置信度,Buzz 示例用它过滤低置信度的组织名误判。

import NaturalLanguage

let tagger = NLTagger(tagSchemes: [.nameType])
let str = "Tim Cook is very popular in Spain."
let strRange = Range(uncheckedBounds: (str.startIndex, str.endIndex))
tagger.string = str
tagger.setLanguage(.english, range: strRange)

tagger.enumerateTags(in: str.startIndex..<str.endIndex, unit: .word, scheme: .nameType, options: .omitWhitespace) { (tag, tokenRange) -> Bool in
    let (hypotheses, _) = tagger.tagHypotheses(at: tokenRange.lowerBound, unit: .word,
                                               scheme: .nameType, maximumCount: 1)
    print(hypotheses)
    return true
}

关键点:

  • NLTagger(tagSchemes: [.nameType]) 使用命名实体识别。
  • tagger.stringsetLanguage 把文本和语言信息交给 tagger。
  • enumerateTags 逐词遍历预测结果。
  • tagHypotheses(at:unit:scheme:maximumCount:) 在 token 位置返回候选标签和置信度。
  • Session 建议用应用自己的代表性数据校准阈值,并按类别分别设置阈值。

用词向量和句向量表达相似度

12:16NLEmbedding.wordEmbedding(for:) 提供静态词嵌入。给定一个词,可以取向量、计算两个词的距离,也可以枚举最近邻词。它适合处理词级别的相似度。

import NaturalLanguage

if let embedding = NLEmbedding.wordEmbedding(for: .english) {
    let word = "bicycle"

    if let vector = embedding.vector(for: word) {
        print(vector)
    }

    let dist = embedding.distance(between:word, and: "motorcycle")
    print(dist)

    embedding.enumerateNeighbors(for: word, maximumCount: 5) { neighbor, distance in
        print("\(neighbor): \(distance.description)")
        return true
    }
}

关键点:

  • wordEmbedding(for:) 按语言取得系统内置的词嵌入。
  • vector(for:) 返回词在向量空间中的表示。
  • distance(between:and:) 计算两个词的距离。
  • enumerateNeighbors 枚举向量空间里的近邻词。

16:30)FAQ 搜索需要句子级语义。Session 说明 sentence embedding 会把一句话编码成 512 维向量,并可计算两个句子的距离,适合处理用户查询和 FAQ 问题之间的匹配。

import NaturalLanguage

if let embedding = NLEmbedding.sentenceEmbedding(for: .english) {
    let sentence = "This is a sentence."

    if let vector = sentenceEmbedding.vector(for: sentence) {
        print(vector)
    }

    let dist = sentenceEmbedding.distance(between: sentence, and: "That is a sentence.")
    print(dist)
}

关键点:

  • sentenceEmbedding(for:) 是句子级入口。
  • vector(for:) 把完整句子编码成固定维度向量。
  • distance(between:and:) 比较两句话在语义空间中的距离。
  • Transcript 明确说明这种能力适合单句、几句话或短段落,长文本应拆成句子后处理。

用句向量做最近邻搜索

18:31)Nosh 的 FAQ 搜索预先为每个答案准备两三个示例 query,并保存这些 query 的句向量。运行时,用户输入也被转成句向量,再与表里的向量计算距离,距离最小的 key 对应要展示的答案。

func answerKey(for string: String) -> String? {
        guard let embedding = NLEmbedding.sentenceEmbedding(for: .english) else { return nil }
        guard let queryVector = embedding.vector(for: string) else { return nil }

        var answerKey: String? = nil
        var answerDistance = 2.0

        for (key, vectors) in self.faqEmbeddings {
            for (vector) in vectors {
                let distance = self.cosineDistance(vector, queryVector)
                if (distance < answerDistance) {
                    answerDistance = distance
                    answerKey = key
                }
            }
        }

        return answerKey
    }

关键点:

  • 方法先取得英语 sentence embedding,再把用户输入转换为 queryVector
  • faqEmbeddings 存的是预计算好的示例 query 向量。
  • cosineDistance 用来比较用户 query 和每个示例 query 的距离。
  • answerDistance 记录当前最小距离,answerKey 记录对应答案。

22:16)当数据量变成上千行诗句时,线性搜索会变慢。Verse 示例把每行诗的句向量放进 dictionary,再通过 Create ML 生成一个 Core ML custom embedding。这个模型既压缩向量表,也保留用于最近邻搜索的几何信息。

import NaturalLanguage
import CreateML


let embedding = try MLWordEmbedding(dictionary: sentenceVectors)

try embedding.write(to: URL(fileURLWithPath: "/tmp/Verse.mlmodel"))

关键点:

  • sentenceVectors 的 key 可以是自定义字符串,示例使用能反查诗和行号的 key。
  • MLWordEmbedding(dictionary:) 把向量字典转成 Create ML 可写出的 embedding。
  • write(to:) 输出的是 Core ML 模型文件。
  • Session 说明这个 custom embedding 可以让最近邻查询避免遍历整个表。

22:43)App 运行时仍然用 sentence embedding 计算用户 query 向量,但最近邻检索交给 custom embedding 完成。

func answerKeyCustom(for string: String) -> String? {
        guard let embedding = NLEmbedding.sentenceEmbedding(for: .english) else { return nil }
        guard let queryVector = embedding.vector(for: string) else { return nil }

        guard let (nearestLineKey, _) = self.customEmbedding.neighbors(for: queryVector, maximumCount: 1).first else { return nil }

        return self.poemKeyFromLineKey(nearestLineKey)
    }

关键点:

  • queryVector 仍来自 NLEmbedding.sentenceEmbedding(for:)
  • customEmbedding.neighbors(for:maximumCount:) 直接返回最近邻。
  • nearestLineKey 是写入 custom embedding 时保留的 key。
  • poemKeyFromLineKey 把行级 key 转回诗级结果。

训练和使用自定义词序列标注器

29:03)Custom word tagger 的任务,是给一句话里的每个 token 打标签。Nosh 示例要从「Do you deliver pizza from Cupertino」里提取 food、from_city、to_city、restaurant 等字段。Transcript 提到,简单词表可以用 NLGazetteer,但词表无法覆盖所有值,也不理解上下文;word tagger 可以用上下文区分出发城市和送达城市。

33:21)训练阶段先定义标签,收集并标注样本句。Session 展示了 JSON 形式的 token/label 平行序列,并说明 2020 年把 transfer learning 用到了 word tagging。Create ML 会把训练数据交给 Natural Language,最后产出 Core ML 模型。

36:45)部署阶段用 NLModel 加载编译后的模型,再把它挂到 NLTagger 的自定义 tag scheme 上。NLTagger 负责分词和逐词应用模型。

func findTags(for string: String) {
        let model = try! NLModel(contentsOf: Bundle.main.url(forResource: "Nosh", withExtension: "mlmodelc")!)
        let tagger = NLTagger(tagSchemes: [NoshTags])

        tagger.setModels([model], forTagScheme: NoshTags)
        tagger.string = string

        tagger.enumerateTags(in: string.startIndex..<string.endIndex, unit: .word, scheme: NoshTags, options: .omitWhitespace) { (tag, tokenRange) -> Bool in

            let name = String(string[tokenRange])

            switch tag {
                case NoshTagRestaurant:
                    self.noteRestaurant(name)
                case NoshTagFood:
                    self.noteFood(name)
                case NoshTagFromCity:
                    self.noteFromCity(name)
                case NoshTagToCity:
                    self.noteToCity(name)
                default:
                    break
            }
            return true
        }
    }

关键点:

  • NLModel(contentsOf:) 加载已经编译进 bundle 的 mlmodelc
  • NLTagger(tagSchemes: [NoshTags]) 创建自定义标签体系。
  • setModels([model], forTagScheme:) 把模型绑定到这个标签体系。
  • enumerateTags 逐词返回模型预测的标签。
  • switch tag 把 restaurant、food、from_city、to_city 分别交给业务处理函数。

核心启发

  • 做一个本地 FAQ 语义搜索。做什么:让用户输入自然语言问题,直接跳到最接近的 FAQ 答案。为什么值得做:Session 展示了用 sentence embedding 比较用户 query 和预计算 FAQ query 的流程。怎么开始:为每个答案准备几条示例问法,预计算向量,运行时用 NLEmbedding.sentenceEmbedding(for:) 取 query 向量并计算距离。

  • 做新闻实体抽取的置信度面板。做什么:在文章页右侧显示人物、地点和组织名,并隐藏低置信度实体。为什么值得做:Buzz 示例说明命名实体识别会把短语误判成组织名,tagHypotheses 能提供过滤依据。怎么开始:记录每个实体的 confidence score,用真实文章样本为 person、place、organization 分别校准阈值。

  • 做图片或文档的自然语言检索。做什么:用用户输入的句子查找 caption、摘要或段落最相近的图片和文档。为什么值得做:Transcript 提到 FindMyShot 可用图片 caption 的 sentence embedding 做检索,也提到文档、消息、评论和问题报告可以聚类。怎么开始:为每条 caption 或摘要计算句向量,小数据集先线性搜索,大数据集再转成 custom embedding。

  • 做外卖或旅行输入解析器。做什么:从一句自由文本里抽出食物、餐厅、出发城市和送达城市。为什么值得做:Nosh 示例展示了 custom word tagger 如何用上下文区分 from_cityto_city。怎么开始:定义标签,收集用户可能输入的样本句,用 Create ML 训练 word tagger,再用 NLModelNLTagger 在 App 内运行。

关联 Session

评论

GitHub Issues · utterances