VisasQ Dev Blog

ビザスク開発ブログ

Sudachiを使った同義語辞書の拡充

検索チームの tanker です。

弊社のフルサポート形式の場合、スタッフがクライアントからどんな人に話を聞きたいか要望をヒアリングし、登録されているアドバイザーを専用の検索システムを使って探しています。

ただし、クライアントから出てくる単語とアドバイザーが登録している情報の単語は必ずしも一致していることはなく、漏れなく探し出すために表記ゆれや周辺単語を加えて検索することがリサーチマネージャー (RM) の方々の腕の見せ所とはなっています。一方で、そのスキルはどうしても経験を積んでいく必要があり、その支援として検索システム側でフォローするために同義語辞書の導入を検討しました。

蓄積されたアドバイザー情報や依頼情報から共起をとって弊社のサービスドメインに合わせた同義語辞書を作る方法もありますが、今回は一般公開されているデータを使ってみたいと思います。

ワークスアプリケーションズさんの SudachiJava / Python / Elasticsearch 向けにライブラリーを提供しており、商用利用可能で開発も盛んにおこなわれているということで今回使ってみました。

Python 版は同義語辞書に完全に対応していなかった

2021/1 現在、 SudachiPy だと、 同義語辞書に対応した ID の出力には対応しているものの、それがどういう単語かというのは対応していませんでした。 Sudachi の Slack (上記 Github参照) で中の人に聞いてみたところ 現在 Python版 Chikkar を開発中ということで今後に期待したいですね。

ユーザ辞書に「ビザスク」「visasq」を登録するところまではコードを書いたので貼っておきます。 (Python 3.7 で挙動確認)

辞書も含めて pip で入れられるのは楽でいいですね。

$ pip install sudachipy sudachidict_core

19カラム目にスラッシュ区切で同義語グループID を追加します。サンプルなので ID は適当です。 SudachiPy の ubuild コマンドでバイナリーファイルに変換します。 (使い方は公式ドキュメント参照)

$ cat sudachi_userdict.csv
visasq,4786,4786,5000,visasq,名詞,固有名詞,組織名,*,*,*,ビザスク,visasq,*,*,*,*,*,1/2

sudachi.json に userDict を追加しました。

{
  "userDict": ["/home/tanker/synonym_sudachi/user2.dic","/home/tanker/synonym_sudachi/user.dic"],
  "characterDefinitionFile": "char.def",
(以下略)
from sudachipy import config, dictionary, tokenizer
import os

config_file_path = os.path.join(os.getcwd(), 'sudachi_config.json')
# config_path を設定すると基本的に resource_dir も追従してしまうが、char.def 等はデフォルトの場所にあるのでそちらを使うようにする
# そのため、userDict の path は絶対パスにする
resource_dir = config.DEFAULT_RESOURCEDIR
tokenizer_obj = dictionary.Dictionary(config_path=config_file_path, resource_dir=config.DEFAULT_RESOURCEDIR).create()

def print_morpheme(text):
  mode = tokenizer.Tokenizer.SplitMode.C
  for m in tokenizer_obj.tokenize(text, mode):
    print('========')
    print('surface:\t\t{}'.format(m.surface()))  # 表層形 (入力した形)
    print('normalized_form:\t{}'.format(m.normalized_form()))  # 正規化形
    print('dictionary_form:\t{}'.format(m.dictionary_form()))  # 辞書形
    print('reading_form:\t\t{}'.format(m.reading_form()))  # 読み (カタカナ)
    print('part_of_speech:\t\t{}'.format(m.part_of_speech()))  # 品詞情報
    print('word_id:\t\t{}'.format(m.word_id()))  # 単語ID
    print('dictionary_id:\t\t{}'.format(m.dictionary_id()))  # 0: sudachi辞書, 1: ユーザ辞書, -1:oov (-1だけど is_oov は False なことがあるっぽい)
    print('synonym_group_ids:\t{}'.format(m.synonym_group_ids()))  # 同義語グループID列
    print('is_oov:\t\t\t{}'.format(m.is_oov()))  # 未知語かどうか out of vocabulary

print_morpheme('VisasQはビザスクliteです')
$ python extract.py
========
surface:                VisasQ
normalized_form:        visasq
dictionary_form:        visasq
reading_form:           ビザスク
part_of_speech:         ['名詞', '固有名詞', '組織名', '*', '*', '*']
word_id:                536870912
dictionary_id:          2
synonym_group_ids:      [1, 2]
is_oov:                 False
========
surface:                は
normalized_form:        は
dictionary_form:        は
reading_form:           ハ
part_of_speech:         ['助詞', '係助詞', '*', '*', '*', '*']
word_id:                122109
dictionary_id:          0
synonym_group_ids:      []
is_oov:                 False
========
surface:                ビザスク
normalized_form:        ビザスク
dictionary_form:        ビザスク
reading_form:           ビザスク
part_of_speech:         ['名詞', '固有名詞', '組織名', '*', '*', '*']
word_id:                268435456
dictionary_id:          1
synonym_group_ids:      [1, 2]
is_oov:                 False
========
surface:                lite
normalized_form:        lite
dictionary_form:        lite
reading_form:
part_of_speech:         ['名詞', '普通名詞', '一般', '*', '*', '*']
word_id:                0
dictionary_id:          -1
synonym_group_ids:      []
is_oov:                 True
========
surface:                です
normalized_form:        です
dictionary_form:        です
reading_form:           デス
part_of_speech:         ['助動詞', '*', '*', '*', '助動詞-デス', '終止形-一般']
word_id:                102139
dictionary_id:          0
synonym_group_ids:      []
is_oov:                 False

Java版 Sudachi + Chikkar で同義語抽出

Scala 環境構築構築 + Hello world

WSL2 の Ubuntu なので Scalaの公式ドキュメントに沿ってインストールします。 ( scalaJDK 8系か 11系推奨とのことなので 11系の最新に)

$ sudo apt install zip
$ curl -s get.sdkman.io | bash
All done!

Please open a new terminal, or run the following in the existing one:

    source "/home/tanker/.sdkman/bin/sdkman-init.sh"

Then issue the following command:

    sdk help

Enjoy!!!
$ source .bashrc
$ sdk list java
$ sdk install java 11.0.9.hs-adpt
$ sdk install sbt

Scalatest を最新にしたら、 Scala の公式ドキュメントに載っているコードだと動かなくなったので微修正しています。 ( 3.2 以降で変わったらしいです)

ThisBuild / scalaVersion := "2.13.4"
ThisBuild / organization := "com.visasq"

lazy val synonym = (project in file("."))
  .settings(
    name := "Synonym",
    libraryDependencies += "org.scalatest" %% "scalatest" % "3.2.2" % Test,
  )
package example

object Hello extends App {
  println("Hello")
}
import org.scalatest.funsuite.AnyFunSuite
import org.scalatest.diagrams.Diagrams

class HelloSpec extends AnyFunSuite with Diagrams {
  test("Hello should start with H") {
    // Hello, as opposed to hello
    assert("Hello".startsWith("H"))
  }
}

Chikkar を使って 同義語バイナリーファイル生成

Sudachi 本体は maven で配布されているのですが、Chikkar はそのようなことがなさそうなので、手元でビルドしたうえで、同義語バイナリーファイルを生成します。

$ sdk install gradle
$ git clone git@github.com:WorksApplications/chikkar.git
$ cd chikkar
$ gradle build
$ ll build/libs/
total 476
-rw-r--r-- 1 tanker tanker 436060 Jan  8 18:20 chikkar-0.1.0-SNAPSHOT-javadoc.jar
-rw-r--r-- 1 tanker tanker  19791 Jan  8 18:20 chikkar-0.1.0-SNAPSHOT-sources.jar
-rw-r--r-- 1 tanker tanker  26796 Jan  8 18:20 chikkar-0.1.0-SNAPSHOT.jar

Chikkar の jar ファイルと DL したその他の jar ファイルおよび 同義語辞書のテキストデータを 同じ階層に置いて以下のコマンドでバイナリーファイルを生成します。

$ java -cp sudachi-0.5.1.jar:jdartsclone-1.2.0.jar:javax.json-1.1.jar:chikkar-0.1.0-SNAPSHOT.jar com.worksap.nlp.chikkar.dictionary.DictionaryBuilder -o system_syn.dic synonyms.txt
$ ll system_syn.dic
-rw-r--r-- 1 tanker tanker 3857381 Jan  8 18:39 system_syn.dic

Java (Scala) のコードで同義語辞書を使う

ファイルは以下のように設置しました。Sudachi の形態素解析用の辞書 (system_core.dic) は こちらでバイナリー済みのデータが配布されているので利用します。

ThisBuild / scalaVersion := "2.13.4"
ThisBuild / organization := "com.visasq"

unmanagedBase := baseDirectory.value / "libs"

lazy val synonym = (project in file("."))
  .settings(
    name := "Synonym",
    libraryDependencies += "org.scalatest" %% "scalatest" % "3.2.2" % Test,
    libraryDependencies += "com.worksap.nlp" % "sudachi" % "0.5.1",
    libraryDependencies += "com.worksap.nlp" % "chikkar" % "0.1.0-SNAPSHOT",
  )

設定 jsonsudachi.json を参考に systemDict 部分を修正しました。

{
  "systemDict" : "src/main/resources/system_core.dic",
(以下略)
package example
import com.worksap.nlp.chikkar._
import com.worksap.nlp.chikkar.dictionary.{Dictionary => ChikkarDictionary}
import com.worksap.nlp.sudachi._
import com.worksap.nlp.sudachi.Tokenizer.SplitMode
import scala.io.Source
import scala.collection.JavaConverters._

object Sudachi extends App {
  val settings: String = scala.io.Source.fromFile("src/main/resources/sudachi_settings.json").mkString
  val dict: Dictionary = new DictionaryFactory().create(settings)
  val tokenizer: Tokenizer = dict.create()
  val chikkar: Chikkar = new Chikkar();
  chikkar.addDictionary(new ChikkarDictionary("system_syn.dic", true))

  def printInfo(text: String) = {
    val morphemes: Seq[Morpheme] = tokenizer.tokenize(SplitMode.C, text).asScala.toSeq
    for (m <- morphemes) {
      println(s"=======================")
      println(s"surface:\t${m.surface()}")
      println(s"normalizedForm:\t${m.normalizedForm()}")
      println(s"dictionaryForm:\t${m.dictionaryForm()}")
      println(s"readingForm:\t${m.readingForm()}")
      println(s"partOfSpeech:\t${m.partOfSpeech().asScala.toSeq.mkString(",")}")
      println(s"wordId:\t\t${m.getWordId()}")
      println(s"dictionaryId:\t${m.getDictionaryId()}")
      // println(s"synonymGroupIds:\t${m.getSynonymGroupIds()}")
      println(s"isOOV:\t\t${m.isOOV()}")
      println(s"synonym:\t${chikkar.find(m.normalizedForm()).asScala.toSeq.mkString(",")}")
    }
  }

  printInfo("VisasQはビザスクを開発しています")
}

「開発」が、同義語展開できましたね。

=======================
surface:    VisasQ
normalizedForm: visasq
dictionaryForm: visasq
readingForm:
partOfSpeech:   名詞,普通名詞,一般,*,*,*
wordId:     0
dictionaryId:   -1
isOOV:      true
synonym:
=======================
surface:    は
normalizedForm: は
dictionaryForm: は
readingForm:    ハ
partOfSpeech:   助詞,係助詞,*,*,*,*
wordId:     122109
dictionaryId:   0
isOOV:      false
synonym:
=======================
surface:    ビザスク
normalizedForm: ビザスク
dictionaryForm: ビザスク
readingForm:
partOfSpeech:   名詞,普通名詞,一般,*,*,*
wordId:     0
dictionaryId:   -1
isOOV:      true
synonym:
=======================
surface:    を
normalizedForm: を
dictionaryForm: を
readingForm:    ヲ
partOfSpeech:   助詞,格助詞,*,*,*,*
wordId:     171753
dictionaryId:   0
isOOV:      false
synonym:
=======================
surface:    開発
normalizedForm: 開発
dictionaryForm: 開発
readingForm:    カイハツ
partOfSpeech:   名詞,普通名詞,サ変可能,*,*,*
wordId:     734761
dictionaryId:   0
isOOV:      false
synonym:    ディベロップメント,デベロップメント,development

同義語を追加する

形態素解析辞書に「visasq」、同義語辞書に「visasq = ビザスク」を登録してみます。

公式ドキュメント を見ながら synonyms.txt の末尾に以下のように追加します。グループID は被らないように 9から始まるようにしてみました。

900001,1,0,1,0,0,0,(企業名),ビザスク,,
900001,1,0,1,2,0,1,(企業名),visasq,,

形態素解析用のユーザ辞書も今回追加した 9から始まるグループID に差し替えます。

$ cat sudachi_userdict.csv
ビザスク,4786,4786,5000,ビザスク,名詞,固有名詞,組織名,*,*,*,ビザスク,ビザスク,*,*,*,*,*,900001
visasq,4786,4786,5000,visasq,名詞,固有名詞,組織名,*,*,*,ビザスク,visasq,*,*,*,*,*,900001

形態素解析用のユーザ辞書は user_v3.dic として resources 以下に置いています。また、同義語辞書は既存のものに追加しただけなのでファイル名はそのままで再生成しています。

{
  "systemDict" : "src/main/resources/system_core.dic",
  "userDict" : ["src/main/resources/user_v3.dic"],

以下のように、単語の意味と、同義語を取得できることが確認できました。

=======================
surface:    VisasQ
normalizedForm: visasq
dictionaryForm: visasq
readingForm:    ビザスク
partOfSpeech:   名詞,固有名詞,組織名,*,*,*
wordId:     268435457
dictionaryId:   1
isOOV:      false
synonym:    ビザスク
=======================
surface:    は
normalizedForm: は
dictionaryForm: は
readingForm:    ハ
partOfSpeech:   助詞,係助詞,*,*,*,*
wordId:     122109
dictionaryId:   0
isOOV:      false
synonym:    
=======================
surface:    ビザスク
normalizedForm: ビザスク
dictionaryForm: ビザスク
readingForm:    ビザスク
partOfSpeech:   名詞,固有名詞,組織名,*,*,*
wordId:     268435456
dictionaryId:   1
isOOV:      false
synonym:    visasq

Sudachi 同義語辞書のボキャブラリー充実度の検証

過去案件で、RM の方が実際に検索した際のキーワードと、案件情報を今回のコードで取得できた同義語を比較しながら評価します。

なお、形態素解析用のデータは 2020/12/23 版 同義語辞書は 2020/8/5 版 を使っています。

  • ドローン配送の課題
    • 実際の検索ワード: 「ドローン」「drone」「配送」
    • ドローンの英語表記はまだ対応していないようでした。
=======================
surface:    ドローン
normalizedForm: ドローン
dictionaryForm: ドローン
readingForm:    ドローン
partOfSpeech:   名詞,普通名詞,一般,*,*,*
wordId:     227309
dictionaryId:   0
isOOV:      false
synonym:    
=======================
surface:    配送
normalizedForm: 配送
dictionaryForm: 配送
readingForm:    ハイソウ
partOfSpeech:   名詞,普通名詞,サ変可能,*,*,*
wordId:     725806
dictionaryId:   0
isOOV:      false
synonym:    配達,デリバリー,デリバリ,delivery
=======================
surface:    課題
normalizedForm: 課題
dictionaryForm: 課題
readingForm:    カダイ
partOfSpeech:   名詞,普通名詞,一般,*,*,*
wordId:     686560
dictionaryId:   0
isOOV:      false
synonym:    イシュー,issue,問題
  • 子ども向け教育商材
    • 実際の検索ワード: 「 教材 」
    • そもそも形態素解析の段階で失敗していますね。
=======================
surface:    子ども
normalizedForm: 子供
dictionaryForm: 子ども
readingForm:    コドモ
partOfSpeech:   名詞,普通名詞,一般,*,*,*
wordId:     383282
dictionaryId:   0
isOOV:      false
synonym:    子ども,キッド,kid,キッズ,kids,チャイルド,child,子供達,子供たち,子ども達
=======================
surface:    教育
normalizedForm: 教育
dictionaryForm: 教育
readingForm:    キョウイク
partOfSpeech:   名詞,普通名詞,サ変可能,*,*,*
wordId:     494305
dictionaryId:   0
isOOV:      false
synonym:    エデュケーション,education
=======================
surface:    商材
normalizedForm: 商材
dictionaryForm: 商材
readingForm:    ショウザイ
partOfSpeech:   名詞,普通名詞,一般,*,*,*
wordId:     356388
dictionaryId:   0
isOOV:      false
synonym:    
  • パン製造工場
    • 実際の検索ワード: 「 工場 」「パン」「ブレッド」
    • パンからブレッドの同義語辞書は出ませんでしたね
=======================
surface:    パン
normalizedForm: パン
dictionaryForm: パン
readingForm:    パン
partOfSpeech:   名詞,普通名詞,一般,*,*,*
wordId:     236613
dictionaryId:   0
isOOV:      false
synonym:    
=======================
surface:    製造
normalizedForm: 製造
dictionaryForm: 製造
readingForm:    セイゾウ
partOfSpeech:   名詞,普通名詞,サ変可能,*,*,*
wordId:     665667
dictionaryId:   0
isOOV:      false
synonym:    
=======================
surface:    工場
normalizedForm: 工場
dictionaryForm: 工場
readingForm:    コウジョウ
partOfSpeech:   名詞,普通名詞,一般,*,*,*
wordId:     402361
dictionaryId:   0
isOOV:      false
synonym:    ファクトリー,factory
  • がん患者の職場復帰
    • 実際の検索ワード: 「がん患者」 「癌」
    • 漢字の癌はでませんでしたが、英単語の方はでましたね。また、職場復帰という単語が登録されており復職やリワークという同義語が取れるのは有用だと感じました。
=======================
surface:    がん
normalizedForm: 癌
dictionaryForm: がん
readingForm:    ガン
partOfSpeech:   名詞,普通名詞,一般,*,*,*
wordId:     46084
dictionaryId:   0
isOOV:      false
synonym:    がん,ガン,キャンサー,cancer
=======================
surface:    患者
normalizedForm: 患者
dictionaryForm: 患者
readingForm:    カンジャ
partOfSpeech:   名詞,普通名詞,一般,*,*,*
wordId:     437399
dictionaryId:   0
isOOV:      false
synonym:    
=======================
surface:    職場復帰
normalizedForm: 職場復帰
dictionaryForm: 職場復帰
readingForm:    ショクバフッキ
partOfSpeech:   名詞,普通名詞,サ変可能,*,*,*
wordId:     1480117
dictionaryId:   0
isOOV:      false
synonym:    復職,リワーク,rework
=======================
surface:    新型コロナウイルス
normalizedForm: 新型コロナウイルス
dictionaryForm: 新型コロナウイルス
readingForm:    シンガタコロナウイルス
partOfSpeech:   名詞,普通名詞,一般,*,*,*
wordId:     1249339
dictionaryId:   0
isOOV:      false
synonym:    
=======================
surface:    経済産業省
normalizedForm: 経済産業省
dictionaryForm: 経済産業省
readingForm:    ケイザイサンギョウショウ
partOfSpeech:   名詞,固有名詞,一般,*,*,*
wordId:     1465494
dictionaryId:   0
isOOV:      false
synonym:    経産省
=======================
surface:    国土交通省
normalizedForm: 国土交通省
dictionaryForm: 国土交通省
readingForm:    コクドコウツウショウ
partOfSpeech:   名詞,固有名詞,一般,*,*,*
wordId:     1042803
dictionaryId:   0
isOOV:      false
synonym:    国交省,建設省

総評

英単語への展開は、 Sudachi の同義語辞書を使うことでだいぶ楽になりそうだと感じました。一方で、まだまだ日本語の同義語で弱いところがあるので、社内に蓄積されたデータを使いつつ充実させていく必要がありそうだとも感じました。また、本旨とは少しズレますがRMの方が文脈を読み取って同義語も導き出しているケースもあり、やはり単語ベースだと限界がある部分も多いとも感じました。