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.string和setLanguage把文本和语言信息交给 tagger。enumerateTags逐词遍历预测结果。tagHypotheses(at:unit:scheme:maximumCount:)在 token 位置返回候选标签和置信度。- Session 建议用应用自己的代表性数据校准阈值,并按类别分别设置阈值。
用词向量和句向量表达相似度
(12:16)NLEmbedding.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_city和to_city。怎么开始:定义标签,收集用户可能输入的样本句,用 Create ML 训练 word tagger,再用NLModel和NLTagger在 App 内运行。
关联 Session
- Control training in Create ML with Swift — 继续学习如何用代码控制 Create ML 训练、保存检查点和监控训练进度。
- Get models on device using Core ML Converters — 了解如何把外部机器学习模型转换成 Core ML,并接入设备端推理流程。
- Use model deployment and security with Core ML — 文本模型训练完成后,可以继续看模型部署、加密和远程更新策略。
- Build Image and Video Style Transfer models in Create ML — 通过另一类 Create ML 自定义模型,理解训练数据、参数和设备端模型交付的完整路径。
评论
GitHub Issues · utterances