dak ブログ

python、rubyなどのプログラミング、MySQL、サーバーの設定などの備忘録。レゴの写真も。

CRF の algorithm を変更して名詞句の判定精度を比較

2022-02-19 12:59:25 | 自然言語処理
先日の「CRF で名詞句の判定をしてみた」で、
CRF の alogorithm を変更した場合の精度を比較してみました。

変更箇所は sklearn_crfsuite.CRF() のパラメータのみです。
今回つかったデータでは algorithm=lbfgs、l2sgd、ap、pa で同程度、
algorithm=arow が精度が低いという結果になりました。
■algorithm=lbfgs, c1=0.1, c2=0.1
    crf = sklearn_crfsuite.CRF(
        algorithm='lbfgs',
        c1=0.1,
        c2=0.1,
        max_iterations=100,
        all_possible_transitions=True
    )

0.9441077916505949

■algorithm=lbfgs, c1=0.5, c2=0.5
    crf = sklearn_crfsuite.CRF(
        algorithm='lbfgs',
        c1=0.5,
        c2=0.5,
        max_iterations=100,
        all_possible_transitions=True
    )

0.945220761760303

■algorithm=l2sgd, c2=0.1
    crf = sklearn_crfsuite.CRF(
        algorithm='l2sgd',
        c2=0.1,
        max_iterations=100,
        all_possible_transitions=True
    )

0.9426298984809508

■algorithm=l2sgd, c2=0.5
    crf = sklearn_crfsuite.CRF(
        algorithm='l2sgd',
        c2=0.5,
        max_iterations=100,
        all_possible_transitions=True
    )

0.9449389892604881

■algorithm=ap
    crf = sklearn_crfsuite.CRF(
        algorithm='ap',
        max_iterations=100,
        all_possible_transitions=True
    )

0.9418937778719487

■algorithm=pa
    crf = sklearn_crfsuite.CRF(
        algorithm='pa',
        max_iterations=100,
        all_possible_transitions=True
    )

0.9407907098905913

■algorithm=arow
    crf = sklearn_crfsuite.CRF(
        algorithm='arow',
        max_iterations=100,
        all_possible_transitions=True
    )

0.8690576483344261

CRF での名詞句の判定で特徴量を変更してみた

2022-02-17 23:53:55 | 自然言語処理
前回の「CRF で名詞句の判定をしてみた」の改良版として、
前後の単語情報を特徴量に追加してみました。

変更箇所は単語の特徴量を返却するメソッドのみです。
# 単語の特徴量
def word_featureh(sent, i):
    word = sent[i]
    tkn = word[0]
    pos = word[1]

    if i > 0:
        word_p1 = sent[i-1]
        tkn_p1 = word_p1[0]
        pos_p1 = word_p1[1]
    else:
        tkn_p1 = ''
        pos_p1 = ''

    if i >= len(sent)-1:
        tkn_n1 = '<e>'
        pos_n1 = '<e>'
    else:
        word_n1 = sent[i+1]
        tkn_n1 = word_n1[0]
        pos_n1 = word_n1[1]

    feath = {
        'bias': 1.0,
        'token': tkn.lower(),
        'token.isuppser()': tkn.isupper(),
        'token.istitle()': tkn.istitle(),
        'token.isdigit()': tkn.isdigit(),
        'pos': pos,

        'token_p1': tkn_p1.lower(),
        'token_p1.isuppser()': tkn_p1.isupper(),
        'token_p1.istitle()': tkn_p1.istitle(),
        'token_p1.isdigit()': tkn_p1.isdigit(),
        'pos_p1': pos_p1,

        'token_n1': tkn_n1.lower(),
        'token_n1.isuppser()': tkn_n1.isupper(),
        'token_n1.istitle()': tkn_n1.istitle(),
        'token_n1.isdigit()': tkn_n1.isdigit(),
        'pos_n1': pos_n1,
    }

    return feath

■実行結果
0.9618864951695193

前後の単語情報を含まない場合は 0.94 程度でしたので、前後の単語情報を追加することで精度が上がっていることがわかります。

CRF で名詞句の判定を試してみた

2022-02-16 23:49:49 | 自然言語処理
conll2000 のデータで、CRF で名詞句の判定をやってみたメモ。

データは以下のように [(表記, 品詞, 名詞句ラベル), ...] の形式です。
[('Confidence', 'NN', 'B-NP'), ('in', 'IN', 'O'), ('the', 'DT', 'B-NP'), ('pound', 'NN', 'I-NP'), ('is', 'VBZ', 'O'), ('widely', 'RB', 'O'), ('expected', 'VBN', 'O'), ('to', 'TO', 'O'), ('take', 'VB', 'O'), ('another', 'DT', 'B-NP'), ('sharp', 'JJ', 'I-NP'), ('dive', 'NN', 'I-NP'), ('if', 'IN', 'O'), ('trade', 'NN', 'B-NP'), ('figures', 'NNS', 'I-NP'), ('for', 'IN', 'O'), ('September', 'NNP', 'B-NP'), (',', ',', 'O'), ('due', 'JJ', 'O'), ('for', 'IN', 'O'), ('release', 'NN', 'B-NP'), ('tomorrow', 'NN', 'B-NP'), (',', ',', 'O'), ('fail', 'VB', 'O'), ('to', 'TO', 'O'), ('show', 'VB', 'O'), ('a', 'DT', 'B-NP'), ('substantial', 'JJ', 'I-NP'), ('improvement', 'NN', 'I-NP'), ('from', 'IN', 'O'), ('July', 'NNP', 'B-NP'), ('and', 'CC', 'I-NP'), ('August', 'NNP', 'I-NP'), ("'s", 'POS', 'B-NP'), ('near-record', 'JJ', 'I-NP'), ('deficits', 'NNS', 'I-NP'), ('.', '.', 'O')]

学習データ(train.txt)で学習を行い、テストデータ(test.txt)で精度を評価します。
プログラムは以下の通り。
import nltk
from nltk.corpus import conll2000
import sklearn
import sklearn_crfsuite
from sklearn_crfsuite import metrics

# [(token, pos, label), ...]
def create_data(file):
    sents = conll2000.chunked_sents(file, chunk_types=['NP'])
    sents = [nltk.chunk.tree2conlltags(s) for s in sents]
    return sents

# 単語の特徴量
def word_featureh(sent, i):
    word = sent[i]
    tkn = word[0]
    pos = word[1]

    feath = {
        'bias': 1.0,
        'token': tkn.lower(),
        'token.isuppser()': tkn.isupper(),
        'token.istitle()': tkn.istitle(),
        'token.isdigit()': tkn.isdigit(),
        'pos': pos,
    }

    return feath

# 文の特徴量
def sent_features(sent):
    return [word_featureh(sent, i) for i in range(len(sent))]

# 文のラベル
def sent_labels(sent):
    return [word[2] for word in sent]

def main():
    train_sents = create_data('train.txt')
    X_train = [sent_features(s) for s in train_sents]
    y_train = [sent_labels(s) for s in train_sents]

    test_sents = create_data('test.txt')
    X_test =  [sent_features(s) for s in test_sents]
    y_test =  [sent_labels(s) for s in test_sents]

    crf = sklearn_crfsuite.CRF(
        algorithm='lbfgs',
        c1=0.1,
        c2=0.1,
        max_iterations=100,
        all_possible_transitions=True
    )
    crf.fit(X_train, y_train)

    labels = list(crf.classes_)
    labels.remove('O')

    y_pred = crf.predict(X_test)
    res = metrics.flat_f1_score(y_test, y_pred,
                                average='weighted', labels=labels)
    print(res)

    return 0

if __name__ == '__main__':
    res = main()
    exit(res)

実行結果は以下の通り。
0.9441077916505949

単純な特徴量でそれなりの結果がでています。

python からの kuromojji の実行速度を改善

2021-11-13 20:11:33 | 自然言語処理
以前「python から kuromoji を実行」という記事を書きました。
この記事の方法で kuromoji である程度の量の文書を解析すると、実行時間がかなり遅いことに気づきました。

以前の方法では、以下のように token 毎に getSurfaceForm() などの kuromoji のメソッドを呼び出していました。
gw = JavaGateway()
tokenizer = gw.jvm.org.atilika.kuromoji.Tokenizer.builder().build()
jtkns = tokenizer.tokenize('日本語の文字列を解析します。')
tkns = []
for i in range(len(jtkns)):
    jtkn = jtkns[i]
    tkn = {
        'form': jtkn.getSurfaceForm(),
        'base': jtkn.getBaseForm(),
        'pos': jtkn.getBaseForm(),
    }
    tkns.append(tkn)    

この方法だと、java サーバとの通信が 1 token につき形態素解析結果の項目分の通信が発生しているのではないかと考えました。
そこで、java サーバは形態素解析結果を json 文字列を返却するようにすることで実行速度を改善してみました。

java サーバ側では以下のように json 文字列を返すメソッドを用意します。
json ライブラリとして jackson を使用しています。
import py4j.GatewayServer;
import java.util.List;
import org.atilika.kuromoji.Tokenizer;
import org.atilika.kuromoji.Token;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.node.ArrayNode;

public class KuromojiJsonGateway {
    public String tokenize(Tokenizer tknzr, String str) {
        List<Token> tkns = tknzr.tokenize(str);

        ObjectMapper mapper = new ObjectMapper();
        ArrayNode arr = mapper.createArrayNode();
        for (int i = 0; i < tkns.size(); i++) {
            Token tkn = (Token)(tkns.get(i));
            ObjectNode obj = mapper.createObjectNode();
            obj.put("form", tkn.getSurfaceForm());
            obj.put("base", tkn.getBaseForm());
            obj.put("read", tkn.getReading());
            obj.put("pos", tkn.getAllFeatures());
            arr.add(obj);
        }

        try {
            String json = mapper.writeValueAsString(arr);
            return json;
        }
        catch (Exception e) {
            return "";
        }
    }

    public static void main(String[] args) {
        KuromojiJsonGateway app = new KuromojiJsonGateway();
        GatewayServer server = new GatewayServer(app);

        server.start();
    }
}

上記のプログラムを以下のようにコンパイルして実行します。
javac -classpath py4j0.10.9.2.jar:kuromoji-0.7.7.jar:jackson-core-2.13.0.jar:jackson-databind-2.13.0.jar:jackson-annotations-2.13.0.jar KuromojiJsonGateway.java
java -classpath py4j0.10.9.2.jar:kuromoji-0.7.7.jar:jackson-core-2.13.0.jar:jackson-databind-2.13.0.jar:jackson-annotations-2.13.0.jar KuromojiJsonGateway

python では、前回と同様 java のメソッドを呼び出す方法と、上記の json を返すメソッドを使う方法とで
100文ずつ解析を行い、実行速度を比較します。
import sys
import time
import json
from py4j.java_gateway import JavaGateway

def convert(jtkns):
    tkns = []
    for i in range(len(jtkns)):
	jtkn = jtkns[i]
	tkn = {
            'form': jtkn.getSurfaceForm(),
            'base': jtkn.getBaseForm(),
            'read': jtkn.getReading(),
            'pos': jtkn.getAllFeatures(),
	}
    return tkns

def tokenize1(gw, str, num):
    tknzr = gw.jvm.org.atilika.kuromoji.Tokenizer.builder().build()
    from_time = time.time()

    for i in range(num):
	jtkns = tknzr.tokenize(str)
        tkns = convert(jtkns)
    to_time = time.time()
    print("tokenize1(): %s" % (to_time - from_time))

def tokenize2(gw, str, num):
    app = gw.entry_point
    tknzr = gw.jvm.org.atilika.kuromoji.Tokenizer.builder().build()
    from_time = time.time()

    for i in range(num):
        tkns_str = app.tokenize(tknzr, str)
        tkns = json.loads(tkns_str)
    to_time = time.time()
    print("tokenize2(): %s" % (to_time - from_time))

def main():
    gw = JavaGateway()
    num = 100
    str = '形態素解析を実行します'
    tokenize1(gw, str, num)
    tokenize2(gw, str, num)
    return 0

if __name__ == '__main__':
    res = main()
    exit(res)

実行結果は以下の通りです。
tokenize1(): 0.7767603397369385
tokenize2(): 0.13006877899169922

java のメソッド呼び出し回数を削減した分がそのまま実行速度に反映されているようです。

文字列を文単位で区切る

2021-11-08 22:36:57 | 自然言語処理
文字列を文単位で区切る方法のメモ。

「。」、「?」、「!」を文の区切り文字として、文字列を文毎に区切ります。
import re;

str1 = '文です。文です。。。文です??文です?!文です'
sents = re.findall('(?:[^。?!]+[。?!]*|[^。?!]*[。?!]+)', str1)
print(sents)

実行結果
['文です。', '文です。。。', '文です??', '文です?!', '文です']



word2vec の Out-Of-Vocabulary 対策の一案

2020-09-13 00:29:26 | 自然言語処理
word2vec の Out-Of-Vocabulary 対策の一案」をQiitaに投稿しました。

COTOHA API で空耳アワー

2020-04-07 22:32:10 | 自然言語処理
Qiitaに記事を投稿してみました。
COTOHA API で空耳アワー