iOS
自然言語処理
Swift

NLP APIでいろいろ試してみた

More than 1 year has passed since last update.

WWDC2017のNatural Language Processing and your Appsで発表されたように、NLP APIが強化されているので試しにいろいろいじってみました。

NLP APIにおける自然言語処理の流れなどを知りたい場合はこちらの記事を御覧ください。

利用可能なタグスキームの取得

利用可能なタグスキームは以下のメソッドで取得することができます。languageのところにはjaなどの言語コードを指定します。

class func availableTagSchemes(for unit: NSLinguisticTaggerUnit, language: String)
-> [NSLinguisticTagScheme]

ちなみに利用可能なスキームをユニット単位で知りたいのではなく、言語単位で知りたい場合は以下のメソッドを使うことができます。これから例に挙げるサンプルコードではわかりやすいようにtagSchemesの引数にいちいち1個ずつスキームを設定していますが、上記メソッドを使えばある言語に対して利用可能なタグスキームを一気に設定することができます。

class func availableTagSchemes(forLanguage language: String) -> [NSLinguisticTagScheme]

言語判定

言語判定を行うことができるdominantLanguageはiOS 11から登場しました。言語が判定できれば言語コードのStringが、判定できなければundという文字列が返ってきます。

let tagger = NSLinguisticTagger(tagSchemes: [.language], options: 0)
tagger.string = "Die Kleinen haben friedlich zusammen gespielt"
let language = tagger.dominantLanguage // -> "de"
let locale = Locale(identifier: "ja_JP")
locale.localizedString(forLanguageCode: language!)! // -> "ドイツ語"

このdominantLanguage、返り値の型はString?なのでnilが返ってくることもあります。もはや言語とはいえない文字列をセットするとnilが返ってきました。

例:*;+を設定
→nilが返ってきた

ここで意地悪をしてみます。Helloという文字列は英語やドイツ語、その他の言語で使われている語です。これを単独で投入するとどのようなことが起きるでしょうか。

let tagger = NSLinguisticTagger(tagSchemes: [.language], options: 0)
tagger.string = "Hello"
let language = tagger.dominantLanguage // -> "it"

一番可能性が高いと判断された言語が返ってきています。2ヶ国語が混ざっている場合も、2つの言語が返ってくるわけではなく、可能性の高い1言語だけです。

例:NSLinguisticTagger provides text processing APIs.\n NSLinguisticTagger 是苹果的文字理处平台。を設定。
→"en"で返ってきた

このようなことがあるので、WWDCのセッションでは「入力するテキストの言語が予めわかってるんだったら明示的に指定してあげて」と言っています。

ちなみにdominantLanguageはクラスメソッドも存在しているので、taggerを作成しなくてもいい場合はこちらを使っても良いです。

let hoge = NSLinguisticTagger.dominantLanguage(for: "NSLinguisticTagger provides text processing APIs.")
// -> "en"

トークン化

テキストをトークン単位で分割するにはNSLinguisticTaggertagSchemestokenTypeを指定します。

import Foundation

let tagger = NSLinguisticTagger(tagSchemes: [.tokenType], options: 0)
let text = "NSLinguisticTagger provides text processing APIs.\n NSLinguisticTagger 是苹果的文字理处平台。"
tagger.string = text
let range = NSRange(location: 0, length: text.utf16.count)
let options: NSLinguisticTagger.Options = [.omitPunctuation, .omitWhitespace]

tagger.enumerateTags(in: range, unit: .word, scheme: .tokenType, options: options) { tag, tokenRange, stop in
    let token = (text as NSString).substring(with: tokenRange)
    print(token)
}
// -> NSLinguisticTagger
// -> provides
// -> text
// -> processing
// -> APIs
// -> NSLinguisticTagger
// -> 是
// -> 苹果
// -> 的
// -> 文字
// -> 理
// -> 处
// -> 平台

日本語でやってみましょう。2つの文を投げてみました。精度としてはまあまあなのかなあというかんじです。

// -> 太陽
// -> フレア
// -> の
// -> せい
// -> で
// -> やる気
// -> が
// -> 出
// -> ない
// -> 今日
// -> もやっ
// -> て
// -> いく

品詞タグ付け

WWDCセッション内では説明がなかったのですが、lexicalClassで品詞を抽出することができたのでその例を載せます。

let tagger = NSLinguisticTagger(tagSchemes: [.lexicalClass], options: 0)
let text = "Tim Cook is the CEO of Apple Inc. which is located in Cupertino, California"
tagger.string = text
let range = NSRange(location: 0, length: text.utf16.count)
tagger.enumerateTags(in: range, scheme: .lexicalClass, options: NSLinguisticTagger.Options(rawValue: 0))
{ tag, tokenRange, stop, _  in
    let token = (text as NSString).substring(with: tokenRange)
    print("\(tag!._rawValue)\t \(token)")
}

// -> Noun     Tim
// -> Whitespace
// -> Noun     Cook
// -> Whitespace
// -> Verb     is
// -> Whitespace
// -> Determiner     the
// -> Whitespace
// -> Noun     CEO
// -> …(省略)

特定の品詞の単語だけ取り出したいときはこのような書き方もできます。動詞と名詞を取り出してみます。

let text = "Mary spent wonderful afternoon in America."
let tagger = NSLinguisticTagger(tagSchemes: [.lexicalClass], options: 0)

tagger.string = text
let options: NSLinguisticTagger.Options = [.omitPunctuation, .omitWhitespace, .joinNames]
let range = NSRange(location: 0, length: text.utf16.count)
let tags: [NSLinguisticTag] = [.verb, .noun]

tagger.enumerateTags(in: range, unit: .word, scheme: .lexicalClass, options: options) { tag, tokenRange, _ in
    if let tag = tag, tags.contains(tag) {
        let token = (text as NSString).substring(with: tokenRange)
        print("token = \(token), tag = \(tag._rawValue)")
    }
}

// -> token = Mary, tag = Noun
// -> token = spent, tag = Verb
// -> token = afternoon, tag = Noun
// -> token = America, tag = Noun

ダメ元で今日はいい天気という日本語の文字列を入れてみたところ、出力はされましたが正しい結果ではありませんでした。

// -> OtherWord  今日
// -> OtherWord  は
// -> OtherWord  いい
// -> OtherWord  天気

レンマ化

テキストからレンマを抽出するにはNSLinguisticTaggertagSchemeslemmaを指定します。

import Foundation

let tagger = NSLinguisticTagger(tagSchemes: [.lemma], options: 0)
let text = "I spent wonderful afternoon in Marin Country."
tagger.string = text
let range = NSRange(location: 0, length: text.utf16.count)
let options: NSLinguisticTagger.Options = [.omitPunctuation, .omitWhitespace]
let tags: [NSLinguisticTag] = [.personalName, .placeName, .organizationName]

tagger.enumerateTags(in: range, unit: .word, scheme: .lemma, options: options) { tag, tokenRange, stop in
    if let lemma = tag?.rawValue {
        print(lemma)
    }
}
// -> I
// -> spend
// -> wonderful
// -> afternoon
// -> in
// -> Marin
// -> country

過去形spentが現在形spendになっており、見出し語がちゃんと抽出できています。また、文頭の大文字Wは小文字になっていて、固有名詞Marinの頭のMは大文字のままなところがちゃんと仕事してる感あります。

こちらも日本語で試してみたいところなのですが、レンマ化、品詞タグ付け、固有表現抽出に関しては英語、フランス語、イタリア語、ドイツ語、スペイン語、ポルトガル語、ロシア語、トルコ語の8ヶ国語までしか対応していません。日本語をセットしてPlaygroundで動かしてみようとしましたが何も出力されませんでした。

固有表現抽出

テキストから固有表現を抽出するにはNSLinguisticTaggertagSchemesnameTypeを指定します。

let text = "Tim Cook is the CEO of Apple Inc."
let tagger = NSLinguisticTagger(tagSchemes: [.nameType], options: 0)
tagger.string = text
let options: NSLinguisticTagger.Options = [.omitPunctuation, .omitWhitespace, .joinNames]
let range = NSRange(location: 0, length: text.utf16.count)
let tags: [NSLinguisticTag] = [.personalName, .placeName, .organizationName]

tagger.enumerateTags(in: range, unit: .word, scheme: .nameType, options: options) 
{ tag, tokenRange, _ in
    if let tag = tag, tags.contains(tag) {
        let token = (text as NSString).substring(with: tokenRange)
        print("token = \(token), tag = \(tag._rawValue)")
    }
}

// -> token = Tim Cook, tag = PersonalName
// -> token = CEO, tag = PersonalName
// -> token = Apple Inc., tag = OrganizationName

joinNamesというオプションを設定しているので複数単語の固有表現があった場合は1つの塊として抽出することができます。

こちらも懲りずに日本語を設定してみましたが、何も出力されませんでした。