皆さんは「Nim」という言語をご存知でしょうか?
Nimは「Pythonをブラッシュアップして、秘伝の悪魔のタレをかけたような感じ」と比喩されるような言語です。
そしてNimはC言語などにコンパイルされ、C言語などのコンパイラを使ってバイナリにコンパイルされます。
そんなNim言語を触っていきましょうか。
Nim言語について
公式サイトは以下です。
https://nim-lang.org/
文法はPythonに影響を受けており、インデントが意味を持ついわゆる「オフサイドルール」というものを採用しています。
また静的型付けです。
とてもシンプルです。
echo "Hello World"
こちらも簡単です。
以下のような形になります。
$ nim c -r 保存ファイル名
nim c
で「C言語にコンパイル」を意味します。
-r
はコンパイル後即実行です。これを省略すると、バイナリコンパイルだけが行われます。
蛇足ですが、nim c
でC言語に、
nim cpp
でC++に、
nim objc
でObjective-Cに、
nim js
でJavaScriptにコンパイルされます。
遊んで見る
少し自然言語処理のようなことをして遊んでみましょうか。
n-gramを自作してみましょう。
n-gramのnは変数です。
例えば、uni-gram、bi-gram、tri-gram、4-gram…のような感じになります。
例を見てみましょう。
例文:「今日はいい天気」
uni-gram
uni-gramでは以下のように分解されます。
今
日
は
い
い
天
気
bi-gram
bi-gramでは以下のように分解されます。
今日
日は
はい
いい
い天
天気
このような感じになります。
n-gramを作成するプログラムを書いてみる
以下のコードをtest.nim
として保存します。
import unicode
proc createNGram*(n: int, text: string): seq[string] =
let runeText = text.toRunes()
var index = 0
var cnt = 0
var tmp: string
while true:
if n <= cnt:
result.add(tmp)
tmp = ""
cnt = 0
index = index - (n - 1)
if text.runeLen() <= index:
break
tmp = tmp & $runeText[index]
cnt = cnt + 1
index = index + 1
echo createNGram(2, "今日はいい天気")
コマンドプロンプトやターミナルで以下のコンパイルコマンドを実行すると、
「今日はいい天気」という文字列がn-gramに分解されて表示されると思います。
$ nim c -r test.nim
↓
@["今日", "日は", "はい", "いい", "い天", "天気"]
いい感じですね!
コメントにも書きましたが、このように分割したものを「コーパス」と呼んだりします。
さて、続きはこのコーパスをどのように利用していくかを見ていきましょう。
TF
Term Frequencyといえばなんかかっこよさげですが、意味は「その単語の出現回数」です。
「今日」という単語が何回出てきた?「天気」という単語が何回出てきた?
そんな意味です。
ではNimでそれを計算してみましょうか。
test.nimを書き換えます。
import unicode
import tables
proc createNGram*(n: int, text: string): seq[string] =
let runeText = text.toRunes()
var index = 0
var cnt = 0
var tmp: string
while true:
if n <= cnt:
result.add(tmp)
tmp = ""
cnt = 0
index = index - (n - 1)
if text.runeLen() <= index:
break
tmp = tmp & $runeText[index]
cnt = cnt + 1
index = index + 1
proc tf*(corpus: seq[string]): Table[string, int] =
for c in corpus:
if result.hasKey(c):
result[c] += 1
else:
result[c] = 1
let corpus = createNGram(2, "今日はいい天気")
let tfTable = tf(corpus)
echo tfTable
これを先ほどと同じようにnim c -r test.nim
でコンパイルすると以下のような出力を得ることができます。
{"いい": 1, "はい": 1, "い天": 1, "日は": 1, "天気": 1, "今日": 1}
各コーパスの出現回数が取得できました。
全部出現回数は1ですね。
そして順番は担保されていませんが、今回は順番は関係ないのでこのまま進めます。
文章の類似度を求める
2つの文章の類似度を求めてみましょう。
ここではメジャーなコサイン類似度を利用してみましょうか。
コサイン類似度の他にも、ユークリッド距離、マンハッタン距離などでも求めることができます。
コサイン類似度とは?
ざっくり説明します。本当にざっくりと。
コサイン類似度はその名の通りコサインを利用します。
コサインは、角度が0度に近づけば1、90度に近づけば0になるという特性があります。
要は2つの要素が近ければ(似ていれば)1に近づき、
離れていれば(似ていなければ)0に近づくといえます。
詳しくは数学系のサイトを見てみると良いでしょう。
コードを改変
以下の様にコードを改変していきます。
import unicode
import tables
import math
proc createNGram*(n: int, text: string): seq[string] =
let runeText = text.toRunes()
var index = 0
var cnt = 0
var tmp: string
while true:
if n <= cnt:
result.add(tmp)
tmp = ""
cnt = 0
index = index - (n - 1)
if text.runeLen() <= index:
break
tmp = tmp & $runeText[index]
cnt = cnt + 1
index = index + 1
proc tf*(corpus: seq[string]): Table[string, int] =
for c in corpus:
if result.hasKey(c):
result[c] += 1
else:
result[c] = 1
proc cosineSimilarity*(text1: string, text2: string, ngramNum: int): float =
let text1Copus = createNGram(ngramNum, text1)
let text2Copus = createNGram(ngramNum, text2)
let text2Tf = tf(text2Copus)
var c = 0.0
var m1 = 0.0
var m2 = 0.0
for t1c in text1Copus:
var n = 0.0
if text2Tf.hasKey(t1c):
n = 1.0
c += (1 * n)
m1 += 1 * 1
m2 += n * n
if m1 == 0 or m2 == 0:
return 0
result = c / round(sqrt(m1) * sqrt(m2))
echo cosineSimilarity("今日はいい天気ですね", "今日はいい天気ですね", 2)
echo cosineSimilarity("今日はいい天気ですね", "いい天気ですね今日は", 2)
echo cosineSimilarity("渋谷でお買い物", "新宿でお買い物", 2)
echo cosineSimilarity("スイカは果物", "ピーマンは嫌い", 2)
これで文章の類似性をざっくり出すことができました。
もっと丁寧に文章を解析する手法として「形態素解析」や「構文解析」などを用いる方法があります。
これをやると精度が上がります。
今回は入門ということでn-gramを使ってみましたが、
機会があればやってみたいですね。
終わりに
ちょっと詰め込みすぎましたが
- Nim言語は良いぞぉ!
- n-gramを使って文章の類似度を出す
ということを行いました。
これを機会に、よかったらNim言語に手を出してみてください!
それでは今回はこのへんで。
また次回!