CoreML (2)
Natural Language Framework로 언어 감지하기
Natural Language framework는 텍스트를 자연스럽게 분석하는 기능을 제공한다. Hidden Markov Model이나 word embedding, deep neural network 같은 개념을 몰라도, 이런 자연어 처리 기능을 앱에 그대로 가져다 쓸 수 있다.
이 섹션에서 다룰 내용은 이렇다. 먼저 언어 인식기(language recognizer)를 만들어서 “불 없이는 연기 없다” 같은 문장이 어떤 언어인지 판별한다. 이어서 문자열 tokenizer를 만들어 텍스트를 의미 단위로 쪼개보고, 명사/동사/형용사 같은 품사를 식별하는 것도 해본다. 마지막으로 텍스트에서 이름, 장소, 조직 같은 개체를 인식하는 데모까지 구현한다.
Playground 만들기
Xcode를 열고 Playground를 새로 만든다.
기본으로 들어있는 boilerplate 코드는 지우고, Natural Language framework를 import한다.
1
import NaturalLanguage
dominantLanguage(for:)로 언어 감지하기
텍스트의 언어를 식별할 때는 NLLanguageRecognizer 클래스를 쓴다. 가장 간단한 방법은 dominantLanguage(for:) type method를 쓰는 것이다.
1
2
3
4
5
6
7
let string = "Ich wünsche dir einen guten Morgen!"
if let language = NLLanguageRecognizer.dominantLanguage(for: string) {
print("Detected \(language.rawValue.uppercased()) as dominant language for: \n\"\(string)\"")
} else {
print("Could not recognize the language of the string.")
}
이 메서드는 텍스트를 입력받아 optional NLLanguage 인스턴스를 반환한다. optional이니 if let으로 값을 꺼내서, 있으면 결과를 출력하고 없으면 인식 실패 메시지를 출력한다.
Playground를 실행하면 “Ich wünsche dir einen guten Morgen!”의 지배 언어가 정확히 DE(독일어)로 인식된다.
1
2
Detected DE as dominant language for:
"Ich wünsche dir einen guten Morgen!"
히브리어 문장을 넣어도 올바르게 식별되고, ZH-Hant(중국어 번체)처럼 세부 variant까지 구분해서 반환한다.
1
2
3
4
Detected HE as dominant language for:
"שלום!"
Detected ZH-HANT as dominant language for:
"妳好。很高興見到你"
언어 코드 전체 목록은 BCP 47에서 확인할 수 있다.
언어 후보와 확률 확인하기
지배 언어 하나만 뽑는 대신, 가능성 있는 언어 후보들과 그 확률을 함께 확인할 수도 있다.
1
2
3
4
5
6
7
8
9
10
let languageRecognizer = NLLanguageRecognizer()
languageRecognizer.processString(text)
let languageProbabilities = languageRecognizer.languageHypotheses(withMaximum: 3)
for (language, probability) in languageProbabilities {
print("\(language.rawValue): \(probability)")
}
languageRecognizer.reset()
먼저 NLLanguageRecognizer 인스턴스를 만들고 processString(_:)으로 텍스트를 전달한다. languageHypotheses(withMaximum:)는 언어와 확률이 짝지어진 dictionary를 반환하는데, withMaximum: 3으로 상위 3개만 가져오도록 제한했다. 반환된 dictionary를 순회하면서 각 언어와 확률을 출력한다.
한 문장을 넣어보면, 중국어 번체가 거의 100% 확률로 가장 높게 나오고, 그다음이 일본어, 중국어 간체는 확률이 거의 0에 가깝게 나온다.
1
2
3
4
5
Detected ZH-HANT as dominant language for:
"空穴來風, 未必無因"
zh-Hant: 0.981166422367096
zh-Hans: 5.321910134803431e-10
ja: 0.01883360557258129
마지막으로 reset()을 호출해서 recognizer를 초기 상태로 되돌려야, 이 인스턴스를 다른 텍스트 분석에 다시 재사용할 수 있다.
감지 언어 제한하기
languageConstraints 프로퍼티를 쓰면 인식 대상 언어 자체를 제한할 수 있다. 예를 들어 영어, 스페인어, 중국어 간체만 감지하고 싶다면 이렇게 설정한다.
1
2
3
4
5
6
7
8
9
10
11
12
let languageRecognizer = NLLanguageRecognizer()
languageRecognizer.processString(string)
languageRecognizer.languageConstraints = [.english, .spanish, .simplifiedChinese] // new
let languageProbabilities = languageRecognizer.languageHypotheses(withMaximum: 3)
for (language, probability) in languageProbabilities {
print("\(language.rawValue): \(probability)")
}
languageRecognizer.reset()
이렇게 후보군을 좁혀두면, 감지기가 이 세 언어 중에서만 가장 가능성 높은 걸 골라 반환한다. 실제로 중국어 간체 텍스트를 넣었을 때, 제약을 걸지 않았을 땐 다른 언어가 더 높게 나올 수 있는 상황에서도, 이 제약 덕분에 중국어 간체가 가장 높은 확률로 반환됐다.
1
2
3
4
5
Detected ZH-HANT as dominant language for:
"空穴來風, 未必無因"
zh-Hans: 1.0
ja: 0.0
zh-Hant: 0.0
NLLanguageRecognizer는 이렇게 최소한의 코드만으로 텍스트 조각의 언어를 감지할 수 있는 간단한 인터페이스를 제공한다.
NLTagger로 문자열 토큰화하기
이번엔 문자열 tokenizer를 만든다.
1
2
3
import NaturalLanguage
let text = "Knowledge will give you power, but character respect"
NLTagger로 토큰 찾기
문자열을 토큰화하려면 NLTagger 인스턴스가 필요하다. NLTagger는 여러 언어를 지원하는 클래스로, 텍스트를 단어/문장/단락 같은 단위로 구분하고, 각 단위의 어휘 클래스 같은 속성까지 식별할 수 있다.
1
2
3
4
5
6
7
let tagger = NLTagger(tagSchemes: [.tokenType])
tagger.string = text
tagger.enumerateTags(in: text.startIndex..<text.endIndex, unit: NLTokenUnit.word, scheme: NLTagScheme.tokenType, options: [.omitPunctuation, .omitWhitespace]) { (tag, range) -> Bool in
print(text[range])
return true
}
단계별로 짚어보면 이렇다.
NLTagger는 관심 있는 tag scheme 목록으로 초기화한다. 단순히 토큰만 찾고 싶다면 배열에.tokenType하나만 넣으면 된다tagger.string에 분석할 텍스트를 지정한다enumerateTags(in:unit:scheme:options:using:)로 주어진 범위에서 토큰을 순회한다- 첫 번째 인자는 분석할 범위다. 전체 문자열을 다 보고 싶으면
text.startIndex..<text.endIndex로 지정한다 unit은 어떤 단위로 나눌지를 뜻한다. 여기서는 단어 단위인NLTokenUnit.word를 쓴다scheme은NLTagScheme.tokenType으로 지정한다options에.omitPunctuation,.omitWhitespace를 넣어서 구두점과 공백은 결과에서 제외한다- 마지막 클로저는
(tag, range) -> Bool형태로,tag(언어 태그)와range(토큰의 범위) 두 인자를 받는다. 여기서는range만 이용해서 해당 위치의 단어를 출력한다 - 클로저는
Bool을 반환하는데,true를 반환하면 순회를 계속하고false를 반환하면enumerateTags가 그 시점에서 처리를 멈춘다. 여기서는 매번 토큰을 출력하고true를 반환해서 끝까지 순회한다
- 첫 번째 인자는 분석할 범위다. 전체 문자열을 다 보고 싶으면
Playground를 실행해보면 문장이 단어 단위로 하나씩 출력된다. 다른 문장을 넣어봐도 예상대로 잘 동작한다.
NLTagger는 여러 언어를 지원하므로, 인터넷에서 히브리어나 일본어 속담을 검색해서 넣어보는 식으로 다국어 텍스트에 대한 토큰화도 테스트해볼 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Knowledge
will
give
you
power
but
character
respect
读书
须
用意
一
字
值
千金
NLTagger로 품사 인식하기
문자열 토큰화도 중요하지만, NLTagger는 더 흥미로운 기능들을 갖고 있다.
tag scheme 바꾸기
기존에 NLTagger(tagSchemes: [.tokenType])로 초기화했던 부분을 여러 scheme을 담은 배열로 바꾼다.
1
let tagger = NLTagger(tagSchemes: [.lexicalClass, .language, .script])
tagSchemes는 배열이라 여러 값을 동시에 넣을 수 있다.
.lexicalClass: 토큰이 품사(명사, 동사 등)인지, 문장부호인지, 공백인지를 알려준다.language: 언어를 알려준다.script: 토큰의 script(문자 체계) 식별자를 알려준다. 예를 들어 라틴 문자는 “Latn”, 중국어 간체는 “Hans”로 표시된다
enumerateTags 클로저 및 scheme 파라미터 수정
enumerateTags를 호출할 때 scheme 인자도 .tokenType에서 .lexicalClass로 바꾸고, 클로저 안에서 tag 값도 같이 출력하도록 고친다.
1
2
3
4
5
6
7
tagger.string = text
tagger.enumerateTags(in: text.startIndex..<text.endIndex, unit: NLTokenUnit.word, scheme: NLTagScheme.lexicalClass, options: [.omitPunctuation, .omitWhitespace]) { (tag, range) -> Bool in
print(text[range])
print(text[range], tag?.rawValue ?? "Unknown")
return true
}
tag는 optional이라 tag?.rawValue로 꺼내고, nil이면 “Unknown”을 대신 출력하도록 처리한다.
이 코드를 실행해보면, “knowledge is power to give you” 같은 영어 문장에서 NLTagger가 각 단어의 품사를 정확히 구분해낸다.
“knowledge”는 명사(Noun), “give”는 동사(Verb), “you”는 대명사(Pronoun)로 인식되는 식이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
knowledge
knowledge Noun
is
is Verb
power
power Noun
to
to Particle
give
give Verb
you
you Pronoun
but
but Conjunction
character
character Noun
respect
respect Noun
读书
读书 Verb
须
须 Verb
用意
用意 Noun
一
一 OtherWord
字
字 Noun
值
值 Verb
千金
千金 Noun
연습: language와 script scheme으로 바꿔보기
enumerateTags의 scheme 파라미터를 NLTagScheme.language나 NLTagScheme.script로 바꿔서 실행해보면, 같은 방식으로 각 토큰의 언어나 문자 체계를 확인할 수 있다. 일본어나 히브리어처럼 다른 언어로 작성된 텍스트로도 실험해볼 만하다. NLTagger는 텍스트나 언어에 관계없이 대체로 정확하게 언어와 script를 식별해낸다.
이렇게 NLTagger를 활용하면 자연어 텍스트를 분류하고, 앱 안에서 품사를 식별하는 기능을 만들 수 있다.
NLTagger로 개체명 인식하기
이번엔 자연스러운 텍스트에서 사람 이름, 조직, 장소를 식별하는 데모를 만든다.
nameType scheme으로 전환하기
텍스트 상수를 사람 이름/조직/장소가 포함된 문장으로 바꾸고, tagger 초기화와 enumerateTags의 scheme 파라미터를 .nameType으로 바꾼다.
1
2
3
4
5
6
7
8
9
10
let text = "Steve Jobs, Steve Wozniak, and Ronald Wayne founded Apple Computer in the garage of Steve Jobs's Los Altos home."
let tagger = NLTagger(tagSchemes: [.nameType])
tagger.string = text
tagger.enumerateTags(in: text.startIndex..<text.endIndex, unit: NLTokenUnit.word, scheme: NLTagScheme.nameType, options: [.omitPunctuation, .omitWhitespace]) { (tag, range) -> Bool in
print(text[range])
print(text[range], tag?.rawValue ?? "Unknown")
return true
}
.lexicalClass 대신 .nameType을 tag scheme으로 지정하기만 하면, 같은 enumerateTags 흐름 그대로 개체명 인식으로 용도가 바뀐다.
Playground를 실행해보면 NLTagger가 문장 안에서 사람 이름(PersonalName), 조직(OrganizationName), 장소(PlaceName)를 각각 정확하게 구분해낸다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
Steve
Steve PersonalName
Jobs
Jobs PersonalName
Steve
Steve PersonalName
Wozniak
Wozniak PersonalName
and
and OtherWord
Ronald
Ronald PersonalName
Wayne
Wayne PersonalName
founded
founded OtherWord
Apple
Apple OrganizationName
Computer
Computer OrganizationName
in
in OtherWord
the
the OtherWord
garage
garage OtherWord
of
of OtherWord
Steve
Steve PersonalName
Jobs
Jobs PersonalName
's
's OtherWord
Los
Los PlaceName
Altos
Altos PlaceName
home
home OtherWord
연습: joinNames 옵션 추가하기
enumerateTags의 options 배열에 .joinNames를 추가하고 다시 실행해보면 결과가 달라진다. 이 옵션 없이는 “Tim”과 “Cook”이 각각 별도의 토큰으로 인식되지만, .joinNames를 추가하면 여러 단어로 이루어진 이름(“Tim Cook”)이 하나의 토큰으로 합쳐져서 인식된다.
정리
Natural Language framework를 통해 언어 감지, 문자열 토큰화, 품사 인식, 개체명 인식까지 둘러봤다. 자연어 처리는 내부적으로 복잡한 알고리즘에 의존하지만, 그 복잡함이 단순하고 쓰기 편한 API 뒤에 잘 가려져 있어서, 몇 줄의 코드만으로도 상당히 정교한 텍스트 분석 기능을 앱에 붙일 수 있다.