CUEBiC TEC BLOG

キュービックTECチームの技術ネタを投稿しております。

MeCabで形態素解析すれば品名の表記が異なっても一向にかまわんッッ

はじめまして。キュービックでWebエンジニアを担当しているthと申します。
本年(2022年)は梅雨をすっ飛ばして突然真夏に突入してしまいましたが、みなさんいかがお過ごしでしょうか。

背景・概要

さて、早速ですが今回のお話の概要です。
複数のECサイトにて販売されている商品を一つのサイトにまとめて掲載する、いわゆる「価格比較サイト」のようなサービスの展開を検討しました。
このときに、各ECサイトにてJANコードのような商品が一意に識別できるIDが付与(掲載)されていればそれを元に「同一商品である」と簡単にみなすことができますが、サイトによって付与されているIDがまちまち、またはそもそも掲載されていない場合がありました。困っちゃいますね。
ならば、商品名の一致をもって同一商品であることを識別すればよさそうです。しかし、掲載されている商品名もサイトによってバラバラで、完全一致や部分一致では識別は難しいようでした。
この問題を解決するべく、商品名を形態素解析し単語に分解することで同一商品を識別することを検討しました。

※本記事ではPythonによる実装例を示します。

課題点

各サイトの商品名を観察してみると、商品名+各サイトごとに商品のオススメポイントやカテゴリを追記されていることが多いようでした。例えば、

という商品が

のような商品名にて掲載されているということです。このような場合だと、不要な文字列(上記例の場合【】内の文字列)を除去すれば完全一致にて識別できそうですね。
しかし、【】内(任意の記号で囲まれた)に商品識別のために必要な文字列が含まれている場合もあるため、どのような文字列が不要で除去していいかの判断は難しそうでした。

では、部分一致だとどうでしょうか?上記例の場合は短い方の商品名が長い方の商品名に含まれているか?で識別可能そうです。ですが、

のように単語の並び順が異なる場合だと、部分一致でも識別することができません。

解決案

さらに複数ECサイトの商品名を眺め続けたところ、どのサイトの商品名に関しても、順序が違ったり識別に不要な文字列が含まれていたりするが、ほとんどの場合どのサイトの商品名にも識別に必要な同じ単語が含まれていることがわかりました。例えば、

のような具合です。この2つが同一商品であることを識別するにはどうすればよいでしょうか?

人間の目からすれば明らかですよね。このとき、人間は「あきたこまち」「なまはげライス」「特選米」「5kg」と単語に分解し認識することで、どう並び替えようが、いくら不要な文字列が追加されようが、同じ商品だと認識できています。
つまり、商品名を単語に分解し、単語数が少ない方の商品名に含まれている単語すべてが単語数が多い方の商品名に含まれていれば同じ商品だとみなすことができるのではないかと考えました。*1

前処理

ECサイトが提供しているAPI経由またはスクレイピング等によって取得した商品名には、商品の識別には不必要な記号が含まれていたり、英数字の半角・全角がバラバラだったりします。これを極力綺麗にしておくと後々うれしいことが多いです。

ノイズになる記号等の削除

どのような文字が不要であるかは要件によって異なりますが、参考程度に一般的に用いられる記号を概ね網羅した正規表現を示します。以下にマッチしたものを削除する処理を挟んでおきました。

'[!"#$%&\'\\\\()*+,-./:;<=>?@[\\]^_`{|}~「」〔〕“”〈〉『』【】&*・()$#@。、?!`+¥%  ]'

さらに、意図せず謎の改行コードが混入していてエラーを吐くケースがありました(完全な異物混入……)。これも削除しておきましょう。

'(\r?\n)|(\r\n?)'

全角・半角の統一

「5kg」と「5kg」のような英数字、さらに「プリキュア」と「プリキュア」のようなカタカナにも全角半角問題がつきまといます。
正規表現が得意な方であれば力技で解決できそうですが、筆者は心が折れたので便利そうなライブラリを探しました。Python では、mojimoji というライブラリが願いを叶えてくれそうだったのでこちらを採用します。
mojimoji - PyPI
全角→半角、または半角→全角への変換をワンパンしてくれます。*2

import mojimoji

product_name_1 = '129.3kgのカマキリ'
product_name_2 = '129.3kgのカマキリ'

print(mojimoji.zen_to_han(product_name_1))
print(mojimoji.zen_to_han(product_name_2))
>>> 129.3kgのカマキリ
>>> 129.3kgのカマキリ

以上をまとめた関数を用意しておくときっといつか役に立つでしょう。

import re
import mojimoji

def clean_japanese_string(jp_string):
    cleaned_string = re.sub('[!"#$%&\'\\\\()*+,-./:;<=>?@[\\]^_`{|}~「」〔〕“”〈〉『』【】&*・()$#@。、?!`+¥%  ]', '', jp_string)
    cleaned_string = re.sub('(\r?\n)|(\r\n?)','', cleaned_string)
    cleaned_string = mojimoji.zen_to_han(cleaned_string)
    return cleaned_string

形態素解析

形態素解析とは、自然言語で書かれた文章を言語として意味を持つ最小単位(形態素)に分割し、かつそれぞれの形態素の品詞や活用形を判別する技術のことです。 この技術を用いれば、先述した人間が行っている商品を単語に分割することが可能になるはずです。今回の場合、品詞や活用形を分析する必要はなく、形態素に分割するためだけに使います。これを 分かち書き といいます。

MeCab

形態素解析エンジンにはMeCabを採用しました。
MeCab - ウィキペディア日本語版

MeCabオープンソース形態素解析エンジンで、奈良先端科学技術大学院大学出身、現GoogleソフトウェアエンジニアでGoogle 日本語入力開発者の一人である工藤拓によって開発されている。名称は開発者の好物「和布蕪(めかぶ)」から取られた。

(多分)もっとも広汎に使われている形態素解析エンジンで、PHP, Ruby, Python など様々な言語にてラッパーが提供されています。

mecab-ipadic-NEologd

neologd / mecab-ipadic-neologd - GitHub

MeCab形態素解析を行うためには、辞書が必要です。IPA辞書と呼ばれる形態素解析用の辞書を用いるのが標準的ですが、mecab-ipadic-NEologdはIPA辞書に比べ新語や固有名詞に強いという特徴を持つ辞書です。
今回の場合、商品名には固有名詞が多く含まれていることが想定されたため、こちらの辞書を採用しました。*3

試してみる

Python + MeCab + Neologd の動作環境の構築は、利用OSなどにより手順が異なるため割愛します。*4

辞書の違いを検証するため、意地悪な文章を用意します。ここでは、範馬刃牙はゆめぴりかが好き」 という文章を試してみましょう。

IPA辞書の場合

import MeCab

tagger = MeCab.Tagger()
parse = tagger.parse('範馬刃牙はゆめぴりかが好き。')
print(parse)
範      名詞,一般,*,*,*,*,範,ハン,ハン
馬      名詞,接尾,一般,*,*,*,馬,バ,バ
刃      名詞,一般,*,*,*,*,刃,ハ,ハ
牙      名詞,一般,*,*,*,*,牙,キバ,キバ
は      助詞,係助詞,*,*,*,*,は,ハ,ワ
ゆめ    副詞,一般,*,*,*,*,ゆめ,ユメ,ユメ
ぴりかが        名詞,一般,*,*,*,*,*
好き    名詞,接尾,形容動詞語幹,*,*,*,好き,スキ,スキ
。      記号,句点,*,*,*,*,。,。,。
EOS

mecab-ipadic-NEologdの場合

import MeCab

# Use -d option to specify a dictionary
tagger = MeCab.Tagger('-d/usr/local/lib/mecab/dic/mecab-ipadic-neologd')
parse = tagger.parse('範馬刃牙はゆめぴりかが好き。')
print(parse)
範馬刃牙        名詞,固有名詞,人名,一般,*,*,範馬刃牙,ハンマバキ,ハンマバキ
は      助詞,係助詞,*,*,*,*,は,ハ,ワ
ゆめぴりか      名詞,固有名詞,一般,*,*,*,ゆめぴりか,ユメピリカ,ユメピリカ
が      助詞,格助詞,一般,*,*,*,が,ガ,ガ
好き    名詞,形容動詞語幹,*,*,*,*,好き,スキ,スキ
。      記号,句点,*,*,*,*,。,。,。
EOS

mecab-ipadic-NEologdを導入することで「範馬刃牙」と「ゆめぴりか」という固有名詞が識別可能となったことが確認できました。やりましたね!

実装

各商品名を分かち書きし、短い方の商品名に含まれる形態素(単語)が長い方の商品にすべて含まれていれば、同一の商品とみなす。という処理の実装例を下記に示します。

import MeCab
import re
import mojimoji

def clean_japanese_string(jp_string):
    cleaned_string = re.sub('[!"#$%&\'\\\\()*+,-./:;<=>?@[\\]^_`{|}~「」〔〕“”〈〉『』【】&*・()$#@。、?!`+¥%  ]', '', jp_string)
    cleaned_string = re.sub('(\r?\n)|(\r\n?)','', cleaned_string)
    cleaned_string = mojimoji.zen_to_han(cleaned_string)
    return cleaned_string

def contains_same_morphemes(original, compare):
    # FIXME: Specify your dictionary directory
    tagger = MeCab.Tagger('-d/usr/local/lib/mecab/dic/mecab-ipadic-neologd')
    
    # decompose into morphemes and store in each array.
    original_morphemes_set = set([line.split()[0] for line in tagger.parse(original).splitlines()])
    compare_morphemes_set = set([line.split()[0] for line in tagger.parse(compare).splitlines()])
    
    # If the length of the product set of two sets is the same as the length of the longer set, 
    # this means that the longer set contains all the morphemes of the shorter set.
    contains = True if max(len(original_morphemes_set), len(compare_morphemes_set)) == len(original_morphemes_set | compare_morphemes_set) else False
    
    return contains

product_1 = '【ふるさと納税】【あきたこまち】なまはげライス特選米5kg 【精米・お米・あきたこまち・米・秋田県産】'
product_2 = 'なまはげライス特選米あきたこまち5kg'

product_1 = clean_japanese_string(product_1)
product_2 = clean_japanese_string(product_2)

print(contains_same_morphemes(product_1, product_2))
>>> True

応用の検討

今回は、分析対象の商品名に「並び順が違ったり余計な文字列が入ってたりするけど、なんやかんやで含まれている単語は同じ」という特徴があったのでこのような実装でそこそこの精度で識別することができました。
しかし、場合によっては例えば「ぼたん鍋」と「猪鍋」を同じ商品であると識別しなければならないケースもあるでしょう。単純なタイポがある場合だとどうでしょうか?このような場合は今回のような単純な分かち書きでは対応しきれず、単語の意味の類推などを行う必要がありそうです。
そもそも商品名だけに頼らず、商品に対する説明文もほとんどのECサイトで与えられるのだから、深層学習を用いて説明文から商品名を特定するモデルを生成してしまえばいいのでは?のようなアプローチも考えられます。うーん、難しそうです……

このように応用や発展はまだまだ検討の余地がありますが、なんらかの自然言語処理を行いたくなったとき、形態素解析は基本となる技術なので学習するいい機会となりました。

いかがだったでしょうか

梅雨でジメジメしているかと思ったら突然の猛暑。祝日はない。有休もない。*5の三拍子が揃った最低最悪な6月でしたが、新しい技術に触れると楽しくて晴れやかな気持ちになりますね。
筆者は7月になり有休が付与された瞬間に昼から寿司を貪り食うためだけに有休を取得する予定です。対戦よろしくお願いします!

*1:すべて含まれている必要はないのでは?とも考えましたが、例えばサイズ違い(Sが含まれているかLが含まれているか等)など、そこそこ厳密に一致しないと同一商品として括ることができません。

*2:全角と半角どちらに統一するかはさして問題にならないはずなので、お好みで。

*3:新語や流行語にも対応していることが強みですが、残念ながら2020年9月を最後に更新が止まっているようです。

*4:Amazon Linux 2 で MeCab + NEologdが参考になりました。

*5:筆者は入社2ヶ月目であるためです。