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

WWDC2017の[Natural Language Processing and your Apps](https://developer.apple.com/videos/play/wwdc2017/208/)で発表されたように、NLP APIが強化されているので試しにいろいろいじってみました。

NLP APIにおける自然言語処理の流れなどを知りたい場合は[こちらの記事](http://qiita.com/akatsuki174/items/9f273ecfd29245a9f39d)を御覧ください。

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

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

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

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

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

# 言語判定

言語判定を行うことができる[dominantLanguage](https://developer.apple.com/documentation/foundation/nslinguistictagger/2875112-dominantlanguage)はiOS 11から登場しました。言語が判定できれば言語コードのStringが、判定できなければ`und`という文字列が返ってきます。

```swift
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`という文字列は英語やドイツ語、その他の言語で使われている語です。これを単独で投入するとどのようなことが起きるでしょうか。

```swift
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を作成しなくてもいい場合はこちらを使っても良いです。

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

# トークン化

テキストをトークン単位で分割するには`NSLinguisticTagger`の`tagSchemes`に`tokenType`を指定します。

```swift
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つの文を投げてみました。精度としてはまあまあなのかなあというかんじです。

```swift
// -> 太陽
// -> フレア
// -> の
// -> せい
// -> で
// -> やる気
// -> が
// -> 出
// -> ない
```

```swift
// -> 今日
// -> もやっ
// -> て
// -> いく
```

# 品詞タグ付け

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

```swift
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
// -> …（省略）
```

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

```swift
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
```

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

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

# レンマ化

テキストからレンマを抽出するには`NSLinguisticTagger`の`tagSchemes`に`lemma`を指定します。

```swift
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で動かしてみようとしましたが何も出力されませんでした。

# 固有表現抽出

テキストから固有表現を抽出するには`NSLinguisticTagger`の`tagSchemes`に`nameType`を指定します。

```swift
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つの塊として抽出することができます。


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