エイエイレトリック

なぐりがき

# 市区町村名を冠する駅名がその市区町村に存在しない例を探す

タイトル通りです。

有名な例としては、品川駅は東京都品川区ではなく東京都港区にあります。

最近志木駅が埼玉県新座市にあることを知り、実際そういう駅がどれぐらいあるのか気になったので確認することにしました。

実行確認で利用した notebookは Gist にアップロードしています。 (コードの一部はChatGPTで生成した結果を使っています。ChatGPTを使いこなす練習も兼ねました)

データ

正確な情報が欲しいため、なるべく省庁から取得しました。

geocoding

国土交通省の鉄道データ (GeoJSON) は駅の位置 (geometry) しか含まれていないです。

別途市区町村名を取得する必要があります。

{
  "type": "Feature",
  "properties": {
    "N02_001": "12",
    "N02_002": "4",
    "N02_003": "けいはんな線",
    "N02_004": "近畿日本鉄道",
    "N02_005": "学研北生駒",
    "N02_005c": "006950",
    "N02_005g": "006950"
  },
  "geometry": {
    "type": "LineString",
    "coordinates": [
      [
        135.72281,
        34.72458
      ],
      [
        135.72403,
        34.72511
      ]
    ]
  }
}

国土数値情報ダウンロードサイトに位置参照情報はあるものの、全都道府県のデータをダウンロードするのも面倒なため、APIに頼ることにしました。

Geopyで Nominatimを呼び出します。 緯度経度 (coordinates) を渡すとaddressを返却します。

以下は 34.72458, 135.72281 を渡した場合の返却値 (address) です。

学研北生駒駅 の所在地は 奈良県生駒市上町 です。

neighbourhood が間違っていますが city生駒市で合っているので今回の利用目的としては問題なさそうです。

{
    "neighbourhood": "北大和一丁目", 
    "city": "生駒市", 
    "province": "奈良県", 
    "ISO3166-2-lvl4": "JP-29", 
    "postcode": "630-0131", 
    "country": "日本", 
    "country_code": "jp"
}

結果

駅名が全国の市区町村と一致するデータのみ、Nominatimから住所を取得しました。

異なる都道府県で同じ名前の市区町村が結構あるため、 今回は「Nominatimで取得した住所の都道府県にある市区町村と一致するかどうか」で「市区町村名を冠する駅名がその市区町村ではない」とみなしました。

結果としては22駅。 同じ駅名でも異なる路線の場合は別データ扱いになっていたので実際は12駅でした。

12駅なら人間でもチェック可能なのでWikipediaで正誤を確認しました。 確実に不一致なのは以下の7駅。

  • 品川: 東京都港区
  • 錦江: 鹿児島県姶良市
  • 厚木: 神奈川県海老名市
  • 中野 (長野県): 長野県上田市 (信州中野駅中野市)
  • 習志野: 千葉県船橋市
  • 目黒: 東京都品川区
  • 新宿: 東京都渋谷区

判定が微妙なのは以下の3駅。

冒頭で書いた志木駅が取得できなかったので他にも該当する駅がある気がします。

今後の課題:

  • 位置データの扱い: LineString の始点だけ使っているので、境界線上の住所になってしまっているのかも?
  • レスポンスを見る限りNominatimだと正しい住所を取得できないことが多そうなので別の取得方法を検討した方がよさそう

# 系列ラベリングの評価方法 in NLP: 日本語・形態素解析

前回は seqeval を使った系列ラベリングの評価をまとめた。

eieito.hatenablog.com

本題である日本語の系列ラベリングについてもまとめる。

といっても、日本語固有の評価指標があるわけではないので、 日本語特有のNLPタスクである形態素解析の評価方法について調べて、傾向を把握することにした。

日本語の系列ラベリング

conlleval のデータは 単語, ラベル を1行としたデータ。 単語が分割された状態になっている。

英語をはじめとした単語の間にスペースを含む言語では問題にならないが、日本語だとそういうわけにはいかない。 どのトークナイザで分かち書きしたかで分割単位が変わってしまうためだ。

とはいえ近年主流である LLM の tokenizer を使う場合は言語に関係なく 1token=1単語とは限らない。 前述の huggingface チュートリアルではトークナイズ結果とラベルのアライメントを前処理 で実施して学習している。

形態素解析の精度評価

話を戻して、日本語の特に形態素解析の場合は、以下の特徴がある。

  • 単語の分割も当てる必要がある
  • NER・Chunkingと比べてラベルの種類が多い
  • ラベルなし (IOB tagにおけるO-tag) がない
    • 未知語はあるものの、データに占める割合としては低い

なので、conlleval のコードをそのまま使うのは難しい。

評価指標はどうしているのかを論文で確認する。

MeCab

Applying Conditional Random Fields to Japanese Morphological Analysis (Kudo et al., EMNLP 2004)

In the evaluations of F-scores, three criteria of correctness are used: seg: (only the word segmentation is evaluated), top: (word segmentation and the top level of POS are evaluated), and all: (all information is used for evaluation).

単語分割のみ (seg)、 単語分割と最上位の品詞(top)、単語分割と品詞すべて(all) の三種類で評価。

最上位の品詞はおそらく Pos1 (名詞・動詞といった品詞の大分類) のこと。

Juman++

Jumanの論文はみつけられなかった。

Juman++: A Morphological Analysis Toolkit for Scriptio Continua (Tolmachev et al., EMNLP 2018)

Table 2: F1 scores of morphological analyzers on Jumandic-based corpora. Seg is segmentation; +Pos is correctly guessing the POS-tags after segmentation

詳しくは書かれていないが、おそらく MeCab の seg と all の二種類と同じ評価方法。

KyTea

http://www.phontron.com/kytea/

点予測のword segmentationモデル。

Pointwise Prediction for Robust, Adaptable Japanese Morphological Analysis (Neubig et al., ACL 2011)

As an evaluation measure, we follow Nagata (1994) and Kudo et al. (2004) and use Word/POS tag pair F-measure, so that both word boundaries and POS tags must be correct for a word to be considered correct.

単語分割と品詞すべての一致で判定。

引用している Nagata (1994) は A Stochastic Japanese Morphological Analyzer Using a Forward-DP Backward-A* N-Best Search Algorithm (Nagata, COLING 1994) のこと。

論文中のFigure4の例を見ると動詞は活用まで扱っており、名詞ではなく普通名詞扱いなので Meab における all の設定での評価だろう。

結論

形態素解析の結果は 単語分割が正解と一致しているかどうか + その品詞 (ラベル) が正しいか の2段階で評価する。

形態素解析器の評価ツール: Meval

日本語の形態素解析に限ると評価向けのコードは Meval しかない。

MevAL(メバル)は,形態素解析器の性能評価およびエラー分析を行うためのツールです. 実装はJAVAで行なっています.

MeCab の評価指標 seg, top, all の三種類で評価できる。

Java 実装なので自分は使ったことがない。

他の系列ラベリングタスクの評価

形態素解析は (一応) ツールがあるとして、他のタスクの場合はどのツールを使うべきか。

seqeval はIOB format以外のラベルに対応していない。

POSのような独自タグ向けとして huggingface evaluate の poseval がある。 実装としては sklean の classification_report を呼び出しているだけだったりする。 なので sklearn を直接呼び出すだけで同様の評価はできる。

sklearn.metrics.classification_report の 入力は 1d array-like である必要があるので、 list of list の場合は flatten することだけ注意が必要。

IOB は B からはじまり I が続く範囲からentityを取得して正解との一致を判定しているが、skleanをそのまま使う場合は単純なラベルの一致で評価する。

from sklearn.metrics import classification_report

y_true = ['B-LOC', 'O', 'B-LOC', 'O', 'O', 'O', 'O', 'O', 'B-ORG', 'O', 'O', 'O', 'B-PER', 'I-PER', 'I-PER', 'I-PER', 'O', 'B-PER', 'I-PER', 'O']
y_pred = ['B-LOC', 'O', 'B-LOC', 'O', 'O', 'O', 'O', 'O', 'B-ORG', 'O', 'O', 'O', 'B-PER', 'I-PER', 'I-PER', 'O', 'O', 'B-PER', 'B-PER', 'O']
print(classification_report(y_true, y_pred))

             precision    recall  f1-score   support

       B-LOC       1.00      1.00      1.00         2
       B-ORG       1.00      1.00      1.00         1
       B-PER       0.67      1.00      0.80         2
       I-PER       1.00      0.50      0.67         4
           O       0.92      1.00      0.96        11

    accuracy                           0.90        20
   macro avg       0.92      0.90      0.88        20
weighted avg       0.92      0.90      0.89        20

NER や情報抽出系のタスクは大人しくIOBタグに変換し、seqevalで評価するのがいいのかもしれない。

# 系列ラベリングの評価方法 in NLP: seqeval

Sequential Labeling (系列ラベリング) は 系列データの入力に対して対応するラベルを付与するタスクのこと。

NLPでは POS Tagging (品詞タグ付け)、NER (固有表現抽出) などの 単語や文字に対してラベルを付与するタスクが該当する。

NER の評価方法はコードが公開されているが、 形態素解析や POS tagging といった 他のタスクでも同じ評価方法でいいのか不安になったので調べてまとめる。

NER の評価

系列ラベリングで調べるとまず出てくるのが NER。 huggingface のチュートリアルでも Token Classification というセクションで WNUT 17 dataset を使っている。

このチュートリアルで評価に使われているのが seqeval なので、このパッケージが一般的に使われていると考えてよいだろう。

seqeval について深掘りする。

seqeval と conlleval

seqeval は conlleval に基づいて実装されている。 これは CoNLL 向けに作成された perl スクリプト (元のコード)。

タスク紹介のページ を見ると 2000年のChunkingタスク、2002年のNERタスクのいずれでも同じコードを使って評価している。

また、output.html の説明によると、 正しいスパンかつ正しいラベルのときに TruePositive となるので、完全一致の評価ロジックだということがわかる。

評価に用いるデータ形式も確認しておく。 以下は CoNLL2002 データセットesp.train.gz からの引用。

Melbourne B-LOC
( O
Australia B-LOC
) O
, O
25 O
may O
( O
EFE B-ORG
) O
. O

- O

El O
Abogado B-PER
General I-PER
del I-PER
Estado I-PER
, O
Daryl B-PER
Williams I-PER
, O

1行に単語とスペース、IOB format) のラベルが含まれる。

空行がデータの区切り、文末を意味する。

一方 seqeval は Python で書かれており、 入力は IOB のラベルだけ受け取る。

>>> from seqeval.metrics import f1_score
>>> y_true = [['O', 'O', 'O', 'B-MISC', 'I-MISC', 'I-MISC', 'O'], ['B-PER', 'I-PER', 'O']]
>>> y_pred = [['O', 'O', 'B-MISC', 'I-MISC', 'I-MISC', 'I-MISC', 'O'], ['B-PER', 'I-PER', 'O']]
>>> f1_score(y_true, y_pred)
0.50

ほかの seqeval の差分としては、CLI には対応していない点がある。

また、IOB 以外の形式 (IOE, IOBES, BILOU) にも対応しているので、 conlleval と厳密に同じロジックではない (と思われる) 。

seqeval の評価方法

seqeval のコードをみながら、評価方法を確認する。

最新 seqeval-1.2.2google colab 上で動作確認した。

評価には上記データのラベルを利用。 LOC, ORG, PERの3種類の entity を予測するタスク。

y_true= [
   ['B-LOC', 'O', 'B-LOC', 'O', 'O', 'O', 'O', 'O', 'B-ORG', 'O', 'O'],
   ['O'],
   ['O', 'B-PER', 'I-PER', 'I-PER', 'I-PER', 'O', 'B-PER', 'I-PER', 'O'],
]

完全一致の判定方法

conlleval と同様、seqeval の評価は完全一致を TruePositive としている。

完全一致かどうかの判定には、 get_entities() で sequence から BI 部分を 1つの entity として抽出する。

from seqeval.metrics.sequence_labeling import get_entities

for row in y_true:
  print(row, get_entities(row))
>>
['B-LOC', 'O', 'B-LOC', 'O', 'O', 'O', 'O', 'O', 'B-ORG', 'O', 'O'] [('LOC', 0, 0), ('LOC', 2, 2), ('ORG', 8, 8)]
['O'] []
['O', 'B-PER', 'I-PER', 'I-PER', 'I-PER', 'O', 'B-PER', 'I-PER', 'O'] [('PER', 1, 4), ('PER', 6, 7)]

上の例以外で entity の取得を確認してみる。

先頭の1文字目が BI 以外の場合は無視される。 (IOEとIOBESにも対応しているので正確には ES も扱える)

スタートが B でなく I の場合、形式としては 不正ではあるものの、 entity として扱われている。

y_invalid_samples = [
  [],
  ["O", "A-LOC", "B-LOC"],
  ["O", "I-LOC", "O"],
  ["B-ORG", "I-PER", "O"],
  ["B-ORG", "B-ORG", "O"],
  ["B-ORG", "B-PER", "O"],
  ["I-ORG", "I-ORG", "O"],
]

for row in y_invalid_samples:
  print(row, get_entities(row))

>>>
[] []
['O', 'A-LOC', 'B-LOC'] [('LOC', 2, 2)]
['O', 'I-LOC', 'O'] [('LOC', 1, 1)]
['B-ORG', 'I-PER', 'O'] [('ORG', 0, 0), ('PER', 1, 1)]
['B-ORG', 'B-ORG', 'O'] [('ORG', 0, 0), ('ORG', 1, 1)]
['B-ORG', 'B-PER', 'O'] [('ORG', 0, 0), ('PER', 1, 1)]
['I-ORG', 'I-ORG', 'O'] [('ORG', 0, 1)]

TruePositive のカウントは extract_tp_actual_correct の実装の通り。 entity の種類ごとに TruePositive をカウントしている。

https://github.com/chakki-works/seqeval/blob/6fc76acf89a0b7905d04e98ec517c0488cd25bce/seqeval/metrics/v1.py#L291-L308

entity の種類は指定しなければタグの - に続く文字列を使う。

e.g. I-LOC -> LOC , B-PER -> PER

スコアの算出方法: case study

適当に y_pred を作ってスコアがどうなるか確認。

case1: 1つ予測できない

最後のラベル 'B-PER', 'I-PER''O', 'O' に置き換え。

正解より予測の方が entity が少ない場合。

from seqeval.metrics import accuracy_score, f1_score, classification_report, precision_score, recall_score

y_pred_miss = [
    ['B-LOC', 'O', 'B-LOC', 'O', 'O', 'O', 'O', 'O', 'B-ORG', 'O', 'O'],
    ['O'],
    ['O', 'B-PER', 'I-PER', 'I-PER', 'I-PER', 'O', 'O', 'O', 'O']
]

print("accuracy:\t", accuracy_score(y_true, y_pred_miss))
print("precision:\t", precision_score(y_true, y_pred_miss))
print("recall:\t ", recall_score(y_true, y_pred_miss))
print("f1_score:\t", f1_score(y_true, y_pred_miss))
print(classification_report(y_true, y_pred_miss))
>>

accuracy:    0.9047619047619048
precision:   1.0
recall:   0.8
f1_score:    0.888888888888889
              precision    recall  f1-score   support

         LOC       1.00      1.00      1.00         2
         ORG       1.00      1.00      1.00         1
         PER       1.00      0.50      0.67         2

   micro avg       1.00      0.80      0.89         5
   macro avg       1.00      0.83      0.89         5
weighted avg       1.00      0.80      0.87         5

accuracy_score だけ get_entities() を使わずラベル単位での一致率を求めている。 ラベルが2つだけ異なるので 19/21 = 0.9047619047619048

precision_score, recall_score, f1_score はデフォルトで average = 'micro' のスコア。 全ての entity の TruePositive (TP) をまとめてカウントしてスコアを計算する。

この例の場合、一致していない entity は1つだけなので TP は 4。

  • precision
    • y_pred_miss で予測している entity は4つ
    • 4/4 = 1.0
  • recall
    • y_true に entity は5つ
    • 4/5 = 0.8
case2: 部分一致

最後のデータのラベル 'B-PER', 'I-PER', 'I-PER', 'I-PER''B-PER', 'I-PER', 'O', 'O' にして評価。

ラベルが部分一致している場合。

y_pred_partial = [
    ['B-LOC', 'O', 'B-LOC', 'O', 'O', 'O', 'O', 'O', 'B-ORG', 'O', 'O'],
    ['O'],
    ['O', 'B-PER', 'I-PER', 'O', 'O', 'O', 'B-PER', 'I-PER', 'O']
]

print("accuracy:\t", accuracy_score(y_true, y_pred_partial))
print("precision:\t", precision_score(y_true, y_pred_partial))
print("recall:\t ", recall_score(y_true, y_pred_partial))
print("f1_score:\t", f1_score(y_true, y_pred_partial))
print(classification_report(y_true, y_pred_partial))

>>>
accuracy:    0.9047619047619048
precision:   0.8
recall:   0.8
f1_score:    0.8000000000000002
              precision    recall  f1-score   support

         LOC       1.00      1.00      1.00         2
         ORG       1.00      1.00      1.00         1
         PER       0.50      0.50      0.50         2

   micro avg       0.80      0.80      0.80         5
   macro avg       0.83      0.83      0.83         5
weighted avg       0.80      0.80      0.80         5

完全一致のときのみ TP なので case1 と同じく TP は4つ。

  • precision
    • y_pred_partialで予測している entity は5つ
    • 4/5 = 0.8
  • recall
    • y_true に entity は5つ
    • 4/5 = 0.8 (case1 と同じ)
case3: 余計なラベル

最初のデータの 'O' の1つを 'I-ORG' に変更。

entity を正解より多く予測している場合。

y_pred_extra = [
    ['B-LOC', 'O', 'B-LOC', 'O', 'I-ORG', 'O', 'O', 'O', 'B-ORG', 'O', 'O'],
    ['O'],
    ['O', 'B-PER', 'I-PER', 'I-PER', 'I-PER', 'O', 'B-PER', 'I-PER', 'O']
]

print("accuracy:\t", accuracy_score(y_true, y_pred_extra))
print("precision:\t", precision_score(y_true, y_pred_extra))
print("recall:\t ", recall_score(y_true, y_pred_extra))
print("f1_score:\t", f1_score(y_true, y_pred_extra))
print(classification_report(y_true, y_pred_extra))

>> 
accuracy:    0.9523809523809523
precision:   0.8333333333333334
recall:   1.0
f1_score:    0.9090909090909091
              precision    recall  f1-score   support

         LOC       1.00      1.00      1.00         2
         ORG       0.50      1.00      0.67         1
         PER       1.00      1.00      1.00         2

   micro avg       0.83      1.00      0.91         5
   macro avg       0.83      1.00      0.89         5
weighted avg       0.90      1.00      0.93         5

1つ余計に予測しており、他は y_true と一致しているため TP は5。

  • precision
    • y_pred_extra で予測している entity は6つ
    • 5/6 = 0.8333333333333334
  • recall
    • y_true の entity は5つ
    • 5/5 = 1.0

seqeval の評価方法、注意すべき点

  • seqeval は entity 単位で評価
    • entity の種類と位置いずれも一致で TruePositive
  • O タグは評価に含めない (accuracy以外)
  • デフォルトで micro スコア
  • IOB 系の schema 以外は評価不可

続く

seqeval だけで長くなってしまった。

もともと形態素解析など、日本語の系列ラベリングについて調査するのが目的だったので 別の記事にこれらの情報をまとめる予定。

# Macbook のステッカー記録

今使っている Macbook を買い替えることにしたので、 下取りで引き渡す前にステッカーの記録をとっておく。

安いAndroidスマホで適当に撮ったので画質はあまりよくない。

研究室に配属されて以降、 色んな場面でもらったステッカーを使っている。 なので全部ノベルティ

使ったのは持っているもののほんの一部で、 未使用のステッカーは結構ある。

小学生の時にシール集めにハマっていたため、 子供の頃の趣味趣向は簡単には変わらないのだなと感じている。

ステッカーは主に学会やイベント、(修士課程で研究室に在籍時)企業の方が訪問したときにもらったもの。

イベントは具体的には

  • 就活イベント (ベンチャー中心)
  • PyCon のような言語のカンファレンス
  • 会社主催の勉強会
  • 技術書典

など。 そのほか、カンファレンスに参加した人から譲ってもらったものもある。

ステッカー欲しい人向けのメモ

ステッカーが一番簡単にかつ大量に集められるのはスポンサーブースだと個人的には思う。

学会やカンファレンスのスポンサーは広報活動のためにいろいろなノベルティを用意しているが、 だいたいの企業がステッカーやシールを用意している(気がする)。

自分は初対面の人とのコミュニケーションをとるは苦手だが、 空いている時に「ステッカーください」と声をかけてからもらうようにしている。

ブースが混んでいる時に行って何も言わずに持ち去るのは流石に申し訳ないと思うのと、 せっかくなので会社の説明をきいておきたいため。

逆にいうと会話のアイスブレイクでステッカーについて言及するのはいい選択肢だと思う。

ステッカーについての記憶

この Macbook を購入したのが社会人になってからだが、 使ったステッカーはなんだかんだ修士のときにもらったものがメインだった。

ChainerMN

個人的には Chainer のステッカーが気に入っていて、まずはこれから貼った。 いまは開発が止まっているが、もらった当時はもちろん現役だった。

最終的に自分は研究には Pytorch を使っていたが、 DeepLearning のライブラリの使い方は Chainer のチュートリアル で学んだので、Chainer には思い入れがある。

ChainerMN 含め Chainerシリーズのロゴはフラットデザインで統一されていて今見てもオシャレである。

もう手に入らないかと思うと名残惜しい。

最初にフラットデザインのステッカーを貼ったので、 増やす時は似たような傾向のステッカーを選んだと記憶している。

耐久性

貼ったのは3年以上前。 PyCon2019ステッカーがあるが、 貼ったのはそれより後なのでおそらく2020年。

さすがにフチからラミネート加工が多少剥がれ始めているが、 表面から剥がれることはなかった。

(GoogleAIの文字部分はラミネート加工がベロっと剥がれたので無理やりセロファンのシールで止めていた)

剥がれたら別のステッカーを貼ろうと思って貼っていたが、 結局そのままだった。

試しにステッカーを剥がしてみたら日焼けのように貼ってあった部分の跡が残った。

PyCon のステッカーは接着部分が残ってベタベタだったので消しゴムで剥がしている。 縁取り跡は接着のりではない。

写真ではわかりにくいが結構目立つ。

を読んだ限り、変色しているらしい。 元に戻らなそう。

使い続けるとしたら、別のステッカーを重ねるのが手っ取り早そうか? 自分はもう処分するので関係ないが。

とにかく、ステッカーを貼る場合は後々のことを考えた方がよい。

おわりに

積極的にイベント参加してステッカーをもらいたい。 次の Macbook に何を貼るかは考え中。

# spacy 日本語モデルにおける Token.morph

Spacy V3.0 から追加された Token の morph について日本語は他と仕様が違うようだったので調べました。

動作確認は google colab。 コードは Gist にアップロードしています。

spacy   3.7.5
en-core-web-sm  3.7.1
ja-core-news-sm 3.7.0

ドキュメント

Token のattributes には Morphological analysis. とだけ記載。

MorphAnalysis によると CoNLL-U Format の Morphological Annotation の情報が辞書で入っています。

英語モデルの morph

まずは英語のモデル en_core_web_sm で確認。

上記ドキュメントとdiscussions に書いてある通り、 morph には Universal features の情報が入っています。

実際に結果をみてみます。

import spacy

nlp = spacy.load("en_core_web_sm")
text = ("When Sebastian Thrun started working on self-driving cars at "
        # 省略
        "this week.")
doc = nlp(text)

for token in doc:
    print(token.text, token.pos_, token.tag_, token.morph.to_dict())

単語によって morph に含まれる情報は異なります。

たとえば動詞の場合は時制 (Tense)・動詞派生語 (VerbForm) を含むことが多い。

When {}
Sebastian {'Degree': 'Pos'}
Thrun {'Number': 'Sing'}
started {'Tense': 'Past', 'VerbForm': 'Fin'}
working {'Aspect': 'Prog', 'Tense': 'Pres', 'VerbForm': 'Part'}
on {}

名詞の場合、 格 (Case) や 数 (Number, 単数・複数などの情報) を含む。

I PRON PRP {'Case': 'Nom', 'Number': 'Sing', 'Person': '1', 'PronType': 'Prs'}
can AUX MD {'VerbForm': 'Fin'}
tell VERB VB {'VerbForm': 'Inf'}
you PRON PRP {'Person': '2', 'PronType': 'Prs'}

morph の特定の情報だけ取得したい場合は get を使います。

返却値は リスト なので扱いに注意が必要です。 該当の key がない場合は None ではなく空のリストを返却します。

doc[1].morph.get("Number"), doc[2].morph.get("Number")

([], ['Sing'])

日本語モデル

日本語の morph も v3.2 から対応しているようなので確認。

https://github.com/explosion/spaCy/releases/tag/v3.2.0

Japanese reading and inflection from sudachipy are annotated as Token.morph features.

ja_nlp = spacy.load("ja_core_news_sm")

# https://ja.wikipedia.org/wiki/SpaCy
ja_text = (
    "spaCyは高度な自然言語処理を行うためプログラミング言語"
    "PythonとCythonで書かれたオープンソースソフトウェア・ライブラリである。"
)
ja_doc = ja_nlp(ja_text)

for token in ja_doc:
    print(token.text, token.pos_, token.tag_, token.morph.to_dict())

日本語モデル ja_core_news_* では、 morph に形態素解析器 sudachipy の結果を格納しています。

Python NOUN 名詞-固有名詞-一般 {'Reading': 'パイソン'}
と ADP 助詞-格助詞 {'Reading': 'ト'}
Cython NOUN 名詞-普通名詞-一般 {'Reading': 'cython'}
で ADP 助詞-格助詞 {'Reading': 'デ'}
書か VERB 動詞-一般 {'Inflection': '五段-カ行;未然形-一般', 'Reading': 'カカ'}
れ AUX 助動詞 {'Inflection': '助動詞-レル;連用形-一般', 'Reading': 'レ'}
た AUX 助動詞 {'Inflection': '助動詞-タ;連体形-一般', 'Reading': 'タ'}
オープン NOUN 名詞-普通名詞-サ変形状詞可能 {'Reading': 'オープン'}
ソース NOUN 名詞-普通名詞-一般 {'Reading': 'ソース'}
ソフトウェア NOUN 名詞-普通名詞-一般 {'Reading': 'ソフトウェア'}
・ SYM 補助記号-一般 {'Reading': '・'}

基本的にはどの token にも読み (Reading) があります。

また、活用する単語 (動詞、助動詞など) は活用 (Inflection) が含まれています。

これらはそれぞれ sudachipy における Morpheme.reading()Morpheme.part_of_speech() と対応しているようです。

Morpheme.part_of_speech() の品詞 (pos) は tag_ に入っているので、 それ以外を morph に格納したのだと思われます。

活用

活用は Unidic における 活用型 (cType) と活用形 (cForm) を ; で連結しています。 どちらかだけ使いたい時は ; で split すればよいです。

inflection = ja_doc[3].morph.get("Inflection")
print(inflection)
if inflection:
  ctype, cform = inflection[0].split(";")
  print(f"ctype: {ctype}, cform: {cform}")

['助動詞-ダ;連体形-一般']
ctype: 助動詞-ダ, cform: 連体形-一般

活用型・活用形の定義については

に詳細が記載されています。ちなみにどちらもPDFです。

比較的細かく分類されているので、ルールベースで取得したいときは定義を確認することをおすすめします。

例えば活用形の終止形は 終止形-一般 以外に 終止形-撥音便終止形-促音便 などのケースを含め6種類あります。 (下記図参照)

# 終止形の判定方法
is_end = cform.startswith("終止形")
# or 
is_end = cform.split("-")[0] == "終止形"

『形態論情報規定集(下)』より

Gist

spacy_morph_ja.ipynb

NLP2024 読んだ・聴いた論文メモ

いつの間にか月が変わってましたが、2024年3月に言語処理学会第30回年次大会 に参加したので、論文紹介をします。 言語資源・評価手法関連が多いです。

サイトの上から順に選び、聴講の際も時系列に沿ってメモしていたので、それに従った順番に紹介します。 カテゴリは口頭発表に基づいてつけていますが、ポスター発表は自分の想像でつけています。

簡単な紹介しかしていないので、詳細を知りたい場合はリンク先の論文を読んでください。 間違いがないよう、なるべく論文の表現を引用しています。

P1-7 「昭和・平成書き言葉コーパス」の語彙統計情報の公開

https://www.anlp.jp/proceedings/annual_meeting/2024/pdf_dir/P1-7.pdf

言語資源・アノテーション

  • 公開データ: a1da4/shc-data
  • 「昭和・平成書き言葉コーパス」の n-gram 情報と共起情報を公開
  • データは雑誌・書籍・新聞
  • 共起情報で昭和から平成で意味が変化したかどうかの分析が可能

P1-10 日本語意味変化検出の評価セットの拡張と検出手法の評価

https://www.anlp.jp/proceedings/annual_meeting/2024/pdf_dir/P1-10.pdf

言語資源・アノテーション

  • 公開データ: tmu-nlp/JapaneseLSCDataset
  • 意味変化検出タスク向けの日本語の評価用単語リストを拡張
    • 先行研究を含めて合計20単語 (意味変化ありと意味変化なし)
  • 比較は 明治・大正、昭和・平成、平成 の3つのコーパスで行う

P2-8 計量テキスト分析のための文埋め込みによる探索的カテゴリ化

https://www.anlp.jp/proceedings/annual_meeting/2024/pdf_dir/P2-8.pdf

実社会応用

P4-25 文法誤り訂正の自動評価のための原文・参照文・訂正文間のN-gram F-score

https://www.anlp.jp/proceedings/annual_meeting/2024/pdf_dir/P4-25.pdf

文法誤り訂正

  • 自動評価尺度 GREEN の提案。計算が O(k) と高速で人手評価に近い評価を実現
  • 原文、参照文、訂正文を n-gram の多重集合として扱い、原文→参照文 と 原文→訂正文を比較する
    • 同じ操作・過剰な操作・操作不足
    • TruePositive, FalsePositive, FalseNegative が集計でき、F値を求めることができる

E6-2 意味変化の統計的法則は1000年成り立つ

https://www.anlp.jp/proceedings/annual_meeting/2024/pdf_dir/E6-2.pdf

言語学自然言語処理

  • 聖書をコーパスとして使い、長期間の意味変化を調査する
  • ラテン語ロマンス語 (フランス語・イタリア語など) で1000年以上の期間があっても「意味変化の統計的法則」(下記) が成立する
    • 高頻度語ほど意味変化の度合いが小さい
    • 多義語ほど意味変化の度合いが大きい

E6-4 意味の集中度に基づいた意味変化検出

https://www.anlp.jp/proceedings/annual_meeting/2024/pdf_dir/E6-4.pdf

言語学自然言語処理

  • 意味変化の検出に意味の集中度という指標を用いる
    • 意味の変化だけでなく、広がりも判定できる
  • 単語ベクトルが様々な方向を向いているほど多様な意味を持つ。常に同じ意味の場合は一点に集中する。

C7-1 音声認識を用いた青空文庫振り仮名注釈付き音声コーパスの構築の試み

https://www.anlp.jp/proceedings/annual_meeting/2024/pdf_dir/C7-1.pdf

言語資源・アノテーション

C7-5 J-UniMorph: 日本語の形態論における意味分類の体系化

https://www.anlp.jp/proceedings/annual_meeting/2024/pdf_dir/C7-5.pdf

言語資源・アノテーション

  • 公開データ: cl-tohoku/J-UniMorph
  • UniMorph という、「原形,語形,特徴ラベル」の3つのラベルを付与するプロジェクトがあり、その日本語版を作成する
    • 形態素解析の辞書定義と異なり、言語を横断して共通のラベルなので他の言語と対応付けられる
  • 基本的な動詞を使ってデータを構築

P9-22 英語中心の大規模言語モデルの言語横断汎化能力

https://www.anlp.jp/proceedings/annual_meeting/2024/pdf_dir/P9-22.pdf

LLM分析評価

  • 大規模言語モデルの事前学習データがほとんど英語でも、他の言語にも対応できる (言語横断汎化) のはなぜか
  • 実験1: 英語で instruction tuning したモデルを多言語で評価
    • instruction tuningした方が性能が向上する
  • 実験2: 英語とそれ以外の言語の対訳ペアの分埋め込み表現を獲得、instruction tuning前後の類似度を計算
    • 類似度の変化は小さい
    • → instruction tuning を通じて「事前学習時に既に獲得していた多言語表現に基づき,言語横断的なタスクを解く能力を学習した」と推測

P10-13 機密情報検知における生成AIを用いたデータ拡張

https://www.anlp.jp/proceedings/annual_meeting/2024/pdf_dir/P10-13.pdf

LLM分析評価

  • 個人・顧客の機密情報の漏えいを防ぐため、固有表現抽出 (NER) の考え方をベースに機密情報検知モデルを構築したい。学習 (主にfine-tuning) には高品質なデータセットが必要。
  • 生成 AI のハルシネーションを活用し、データ拡張する
    • 周辺文脈の拡張: 企業名を含む文をLLMが生成
    • エンティティの拡張: 架空の企業名をLLMが生成し、元データの企業名と置換する
  • 拡張したデータで学習することで精度が向上

D11-1 テキスト編集事例の編集操作への自動分解

https://www.anlp.jp/proceedings/annual_meeting/2024/pdf_dir/D11-1.pdf

人間と計算機のことばの評価

  • テキストの編集操作系列を生成する
    • source から target に変換するとき、最小のまとまりごと操作したと考える

Scientists who study the brain

→ Researchers who study the brain

→ Brain researchers

  • 編集操作を同定することでシステムの振る舞いを理解できる
  • 提案手法: ラティス生成 (中間文候補の生成とフィルタリングの繰り返し) とパスの探索

余談: 神戸の思い出

毎日ポートライナーに乗って会場まで行きました。 通勤時間帯は満員電車でヤバいと聞いていたのですが、東京都心の通勤時間帯に比べると余裕があった気がします。

ポートアイランドのような人工島がある港町で、横浜・京浜工業地帯っぽさがあるのですが、六甲山がすぐそばにあるのが景観的に大きな違いだなと思いました。 会場から遠くて行く余裕がなかったため、今度神戸周辺に行く機会があればチャレンジしたい。

ポーアイから三宮方面(1) by 神戸市 is licensed under CC BY-NC-SA 4.0

また、帰る前にメリケンパークにある 神戸港震災メモリアルパーク に立ち寄りました。 神戸もまた、震災から復興した都市であることを思い出しました。

ちょうど金ローの すずめの戸締まり で、がっつり神戸が出てきたので思い出しがてらの余談でした。

# Spacy + fastAPI に locust で負荷試験を実行する

前回の記事で、 Spacy のモデルがメモリリークすることを調べました。

fastAPI で Spacy を動かしたとき、メモリがどれぐらい増加するのか確認します。

コードは Github にあげています。設定を諸々変えたので現状プルリクのままマージしていません。

github.com

設定

fastAPI + Docker

fastAPI のコードは cookiecutter-spacy-fastapi をベースにしています。 Spacy の結果から固有表現 (Named entity) を返却するAPIです。

Spacyのモデルには日本語のなかで一番軽量な "ja_core_news_sm" を指定しています。

クラウド上にデプロイして動かすことを想定し、利用可能なメモリに制限をかけます。 今回はDocker コンテナの設定を利用します。

AWS で t3.small 相当の1G (mem_limit:1g) を設定しました。 mem_limit は version 3 で対応していないのでバージョンを下げています。

// docker-compose.yml
version: "2"
services:
  app:
    container_name: spacy_fastapi
    build: .
    volumes:
      - ./:/usr/src/
    ports:
      - "8080:8080"
    command: uvicorn main:app --reload --host 0.0.0.0 --port 8080
    mem_limit: 1g

コンテナごとのメモリの使用量は docker stats でわかるので、ログとして残しておきます。

% docker stats spacy_fastapi  --no-stream
CONTAINER ID   NAME            CPU %     MEM USAGE / LIMIT   MEM %     NET I/O     BLOCK I/O     PIDS
6be01b84084b   spacy_fastapi   0.49%     170MiB / 1GiB       16.60%    876B / 0B   8.16MB / 0B   12

ファイルの出力は こちらの記事 を参考にしました。 扱いやすいようにjson で出力します。

 while true; do docker stats --no-stream --format "{{ json . }}" |tee -a stats.txt; sleep 10; done

locust

負荷試験にはPython製の locust を使います。 固有表現一覧を返すエンドポイント /entities に対してリクエストするコードを作成しました。

locustのドキュメントにある負荷試験と異なり、API に POST で渡す文書データが必要です。

前回の記事で Spacy は Vocab をキャッシュすることがわかっており、同じデータを繰り返し渡すだけでは正しく検証できません。

大量のユニークデータで検証するために、Wikipediaの記事を使います。

今回は Wikipedia日英京都関連文書対訳コーパス を使います。 14,111ファイル と数時間のテストには十分な量です。 本来の用途とは異なりますが、コーパスの日本語部分のみ利用させてもらいました。

(Wikidump でも問題ないのですが、ストレージを圧迫しそうだったので……)

負荷試験

  • spacy_fastapi を動かす docker-compose up
  • メモリの使用量をログ出力する docker stats --no-stream --format "{{ json . }}"
  • locust を起動する poetry run locust -f locustfile.py

http://localhost:8089 で UI を起動し、実行します。

ユーザー数を5とし、1時間実行します。

locustのUI

試験の結果

1時間の結果は以下の通りです。1.71%のリクエストに失敗していることがわかります。

Type     Name         # reqs      # fails |    Avg     Min     Max    Med |   req/s  failures/s
--------|-----------|-------|-------------|-------|-------|-------|-------|--------|-----------
POST     /entities      7437   127(1.71%) |    411      18   10101    120 |    2.07        0.04

エラーとしては ReadTimeout (タイムアウト) と RemoteDisconnected の2つです。

5    POST    /entities   RemoteDisconnected('Remote end closed connection without response')
122 POST    /entities   ReadTimeout(ReadTimeoutError("HTTPConnectionPool(host='localhost', port=8080): Read timed out. (read timeout=10)"))

失敗しているタイミングをチャートで確認してみると、後半ずっと失敗していることがわかります。

また、 RPS は成功している時間帯で 2 程度になりました。 Wikipediaのページをまるごと使っているので、短い文だともう少し捌けるかもしれません。

locust report

参考: Docker のエラーログ

docker logs では FastAPI のログを遡れるのですが、 500 Internal Server Error 以外の表示がないため、どの部分でのエラーかわかりませんでした。

コード部分で例外時のログを設定する必要がありそうです。

INFO:     192.168.65.1:63904 - "POST /entities HTTP/1.1" 200 OK
INFO:     192.168.65.1:63914 - "POST /entities HTTP/1.1" 200 OK
INFO:     192.168.65.1:63915 - "POST /entities HTTP/1.1" 500 Internal Server Error
INFO:     192.168.65.1:63904 - "POST /entities HTTP/1.1" 200 OK
INFO:     192.168.65.1:64213 - "POST /entities HTTP/1.1" 200 OK

メモリ使用率

docker stats から使用率だけ抽出します。

出力が 171MiB / 1GiB と分母の数値を含むので、簡単にフォーマットを調整しました。

import json

mem_usage_list = []
with open("stats.txt") as f:
    for line in f:
        data = json.loads(line.strip())
        if data["Name"] == "spacy_fastapi":
            mem_usage = data["MemUsage"].split("/")[0].strip()[:-3]
            mem_usage_list.append(mem_usage)

with open("stats_memory.txt", "w")as f:
    f.writelines("\n".join(mem_usage_list))

スプレッドシートでグラフにしたものが以下の通りです。 時間が経つごとにメモリ使用量が増えていき、 メモリ制限の 1GiB 相当の 1000MiB のまま動きません。

メモリ使用量

本番運用する場合はメモリを解放するため Spacy モデルか API自体を定期的にリロードしたいです。

リロードに関しては Gunicorn やその他の設定できそうなので、詳しく調べようと思います。

参考資料

前回 eieito.hatenablog.com