⛓️

レシートの商品名にJANコードを紐付ける手法の紹介

2024/08/02に公開

はじめに

WED株式会社でMLエンジニアをしている catabon です。
WEDでは、ユーザーの方々からレシートを買い取るアプリ「ONE」を開発運用しています。他記事 (VertexAI Online Predectionを運用してみての話, レシート情報の抽出-LLM編-) で解説されているように、レシート画像からOCRによって文字列を取得し、それらをより有用な情報になるよう抽出・集積しています。

その過程の一つが、抽出した商品名にJANコードを紐付けするものです。販売されている商品の大部分にはJANコードが付与されており、商品自体によくバーコードとして印刷されていたりします。

"jan_code_example"
(画像は「バーコード篇(1) バーコードの仕組み 日本NCRコマース株式会社」より)

理論的にはそれらの商品名について一意的にJANコードが対応づけられるはずですが、実際にはレシートの商品名は例えば以下に示したように異なる形で省略されていることが多く、対応するJANコードを1対1で紐付けることが不可能なものも存在します。

商店名 レシートの表記
スーパーA WED ONE ゼリー 180G
スーパーB ウェッドワンゼリー18
量販店C ウエド ワン ゼリ
コンビニD ウェッドワンゼリー 180g
コンビニE WO ゼリー

一方でプライベートブランドの商品など、レシート上に印字されている商品名であっても対応するJANコードがない場合もあります。

"private_brand_foods"

このような状況下で、大量のデータを効率的に処理し、かつできる限り有用なJANコードの紐付けを商品名に対し行うことを目指しています。

JANコードデータ

商品名とJANコードの対応づけの情報を網羅的に得るためには、JANコード統合商品情報データベース(JICFS)のデータを用いています。これは商品名とJANコードの対応の他に、読みが半角カタカナで与えられ、また対応するカテゴリや会社名・会社コードも付与されています。但し登録された商品名・会社名などの情報は登録した会社に任されている部分があって、一貫性が保証されているとはいえません。例えば一つの会社コードに複数の会社名が対応付けられていたり、半角カタカナで表わされた読みにおいても、ヤユヨとャュョなど、片仮名の小文字が普通のものに変換されている場合といない場合があったり、といった問題があります。

紐付け手法

このようにレシート上の商品名とJANコードデータとの紐付けには、双方の表記の揺れを考慮した手法が必要となります。

手法1: レーベンシュタイン距離

一つの手法としては、レーベンシュタイン(Levenshtein)距離を用いた紐付けが考えられます。レーベンシュタイン距離とは、2つの文字列を比較する際に、それらの間の差異を解消できる編集操作の最小回数で表したものです。編集操作とは、一文字の挿入/削除/置換のうちのどれか1つを指し、例えば:

  • abcとabとの間の距離は1 (挿入または削除)
  • abcとadeの間の距離は2 (置換2回)

のように計算されます。

一般には、これをさらに双方の文字列の長さを考慮して[0.0, 1.0]の間の値になるよう標準化した値が用いられることが多いです。我々の実験でも、レーベンシュタイン距離とそれを標準化した距離のそれぞれを用いて、もっとも近い商品名をJICFSエントリから選択する手法を評価したところ、標準化したものとそうでないものとでは、標準化したものの方が若干評価値が高い結果が出ました。

標準化したレーベンシュタイン距離を用いた紐付け手法をGoogle Cloud Platform(GCP)のCloud Runを用いて実用環境にデプロイしました。実際の処理には、高速でレーベンシュタイン距離の計算・比較が行えるRapidFuzzを用いました。処理時間は、1000エントリを処理するのに15分程度でした。ただ実装の際に、使用メモリの制限など仕様に応じて、距離の値を整数で格納することで省メモリ化を行ったりして、その分精度が犠牲になってしまったりしています。

手法2: 単語ベクトル

より速度と精度の向上を求めて新たに用いたのは、文字列そのものでなくそれらを単語に切り分ける処理を行って(tokenization)各単語を数値ベクトルに変換し、そのベクトル同士を比較して最も類似したベクトルに対応するJANコードを付与する手法です。

既存の機械学習モデルを用いた単語ベクトルの生成

このような手法を用いる際に、Hugging Faceなどで公開されている大規模日本語コーパスを使った学習済みのモデルを使って、単語ベクトルを生成することが簡便であり主流だと思いますが、レシートに記載されている商品名についてそのまま適用したところ、望むような結果が得られませんでした。これは、一つにはモデルが一般的な日本語の文章をもとに学習されており、対象がレシート画像のOCRの結果得られる文字列とは性質が異なることが挙げられます。入力文字列では単語が途中で切れていたり、濁点が抜けたり、商品名の省略表記があったりして一般的な文章中の単語と異なっていること多く、単語分割が必ずしも適切に行われないことがあるのです。これに対処するために、形態素解析器に依らない単語分割を行い、その分割に基いた単語ベクトルの生成を行う手法を用いることにしました。

データに即したモデルの新規生成

1. サブワードによる分割

文法に基いた分割でなく、統計的に文字列を分割することで未知語に対処するのがサブワードと呼ばれる手法です。これにより分割は人間にとって不自然になる可能性はあるのですが、、既存の形態素解析システムが単語分割をせず未知語として一つの長いトークンにしてしまうような文字列であっても、それらがある程度の頻度を持つ部分文字列に分割されることが期待できます。実際のサブワード生成にはSentencePieceを用いました。
SentencePieceは学習済みの機械学習モデルの単語分割にも用いられているモジュールですが、ここでは単独でJICFSデータベースにある商品名を入力として、一からSentencePieceのモデルを作成しました。

2. ベクトルモデルの生成

新たに作成したサブワードのモデルによる単語分割は既存のモデルのものとは単位が異なるので、この分割に基づいて新たにベクトルモデルを生成する必要があります。そこで、ここではgensimのFastTextを用いて、SentencePieceモデルによる単語分割を基にしてベクトルモデルを生成し、このモデルを用いてJICFSデータベース中の各商品についてベクトルを生成し、格納しました。検索実行時には、与えられた商品名に対して作成したモデルを用いてサブワードへの分割、ベクトルの生成を行い、格納しておいた商品ベクトルとのコサイン類似度を求め、最も近いものを対応するJANコードとして出力しています。

"cosine_vectors"
コサイン類似度の解釈

これらのモデルによって、まだ不正確な所はありますが、ある程度納得のいくJANコードの紐付け結果を得ることができました。

処理の高速化

1. Faissライブラリ

紐付けを行う際には、与えられたレシート中の商品名と格納されている全ての商品ベクトルとのコサイン類似度を求めることになり、この処理のコストが大きくなります。これに対処するために、まずFaissライブラリを用いた高速化を行いました。Faissを用いた結果、処理速度をscikit-learn.cosine_similarityを用いた場合よりも10倍近く向上させることができました。更に、FaissではGPUを用いて更に高速化ができて(faiss-gpu)、例えば5000個の商品名に対してCPUのみ用いた場合では数分かかったものが、GPUを用いた場合では数秒で済むようになりました。

2. Vertex AI ベクトル検索

GPUを使える環境ではFaissライブラリによって実用上十分な速度が得られたのですが、デプロイのためにGPUを常に確保するにはコストがかかり、また確実にGPUを必要数確保できるか保証がない状況でもありましたので、別の手法を試すことにしました。現在我々がGCPを用いていることもあって、Vertex AI ベクトル検索を使ってみました。

Vertex AIベクトル検索を利用する際には、IDと対応するベクトルをjsonl形式で用意する必要があります。JANコードの紐付けに利用する際には、IDとしてJANコードを与えて、以下のようなデータを作成しました。

    {"id": "45xxxxxxxxxxx", "embedding": [-0.3855966031551361, -0.25235968828201294, -0.08109860867261887, ... ]}
    {"id": "49xxxxxxxxxxx", "embedding": [-0.3268126845359802, 0.11844168603420258, 0.097178153693676, ... ]}
    ...

インデックスのパラメータには、上記の作成したデータを格納したバケットの名前や次元数、距離関数などの情報を記述します。

	{
	  "contentsDeltaUri": "gs://BUCKET_NAME/path", 
	  "config": { 
		"dimensions": 100, 
		"approximateNeighborsCount": 150,
		"distanceMeasureType": "DOT_PRODUCT_DISTANCE",
		"shardSize": "SHARD_SIZE_MEDIUM",
		"algorithm_config": {
		  "treeAhConfig": {
			"leafNodeEmbeddingCount": 5000,
			"leafNodesToSearchPercent": 3
		  }
		}
	  }
	}

ベクトル検索のインデックスとインデックスポイントの作成は、公式文書にもありますが、gcloudコマンドを使って作ることができます。

    # make indexes
    gcloud ai indexes create \
  	--metadata-file=[index_metadata.json] \
  	--display-name=[your_index_display_name] \
    --region=[your_region_name]
  
    # make index endpoint
    gcloud ai index-endpoints create \
  	--display-name=[your_index_point_display_name] \
  	--network=[your_network_name] \
  	--project=[your_project_name] \
  	--region=[your_region_name]

実際に商品名に対して検索を行うためには、商品名をベクトルに変換してベクトル検索のAPIとやりとりをして検索結果を商品名のリストに変換するクライアントが別途必要になります。これを用意し、レーベンシュタイン距離を用いた処理と同様にCloud Runにデプロイして利用できるようにしました。精度はFaissライブラリを用いたものとほぼ同等ですが、より大量の入力データを処理できるようになりました。現在1日100万枚弱のレシートデータに対して40分程度で処理できており、1枚あたりの解析コストを抑えることができています。

おわりに

レシートに記載された商品名に対しJANコードの紐付けを行う処理について紹介しました。今後もさらなる精度向上や利用できる情報の追加など、やるべきこと・やりたいことに取り組んでいきたいと思っています。

WED Engineering Blog

Discussion