チケットの見える化にチャレンジしてみる(2-3: 準備編 文書ベクトル〜コサイン類似度)
こんばんは!株式会社スマレジ のmasaです。
4連休はいかがお過ごしでしたか? masaは前後にお休みをいただけたので、古い友人にあったりして、久しぶりにゆっくり過ごせました。
さて今回は、計量テキスト分析でとてもよく使われる尺度をご紹介します。 これも形態素解析と同じく、今回のredmineチケットの見える化で利用します。 ただ、今日紹介するのは前提知識で、これをベースにした高次の尺度を実際は使用します。
文書ベクトル
前回、形態素解析を使うことで欲しい単語を抽出することができることを確認しました。 次は、抽出した単語たちを使って、二つの文書が似ているかどうかを比較することを考えます。
二つの文書A,Bがあったとします。この時、それぞれの文書から抽出した単語リストのことを「文書ベクトル」と呼びます。 ベクトルといえば、高校の数学や物理で「量(スカラー)に方向を持ったもの」と習ったかと思います。 例えば、xy平面上の原点を始点とし、(x,y)=(1,2)の点を終点とするベクトルは
みたいに書いたと思います。 平面ベクトルなら1とか2などの数字が入りますが、文書ベクトルの場合は、その単語があったら1、なかったら0が入ります。 この0,1の値が単語ごとに存在するのが文書ベクトルになります。 そのため、いろいろな単語があるととても長い(=次元の大きい)ベクトルになります。
例えばAの単語リストが「りんご」「みかん」「バナナ」、Bの単語リストが「スイカ」「りんご」「いちご」だったとし、 それぞれの文書ベクトルを,とすると、
というふうになります。 (これは例えで記載しているもので、記述方法は正しくはないです。専門書を見ると別の書き方をしていますのでご注意ください。)
コサイン類似度
サイン・コサイン・タンジェント、今聴くと懐かしい気持ちになる高校時代に習った三角比を実はここで使います。 コサイン類似度はその名の通り、文書の比較として、コサイン(余弦)の値を使用するものです。
さっき説明した文書ベクトルが2本あれば、二つのベクトルが角度を作るので、その角度θのコサインを求めます。
で、高校の数1では直角三角形の底辺/斜辺からcos(θ)を求めたと思いますが、 ここではベクトルの単元で出てきた方法でcos(θ)を出します。(実はこっちがcos(θ)の定義だったりします)
ここの分母のはの大きさです。大きさはベクトルの各成分を二乗して全部足して平方根を取ったものですが、 文書ベクトルの場合は成分が0 or 1なので、例えばであれば
となります。
また、分子はベクトルの内積です。これは文書ベクトルの場合だと、ダブっている単語の種類の合計になります。 今回は「りんご」だけがダブっているので
となります。
そのためコサイン類似度は
となりました。
コサイン類似度の分子はダブっている単語の合計になるので、ダブっている数が多ければ多いほど、コサイン類似度も大きくなります。 また、文書ベクトルが0,1しか取らないため、分母、分子ともにマイナスになることはありません。最低値は一切被りがない場合なので、0になり、 最高値は全てダブった場合で1になります。なぜ全てダブった場合が1になるのかというと、分母が全部ダブっているので、出現する単語の種類数になり、分子もそれぞれのベクトルが出現する単語の種類数の平方根になるためです。
要は、1に近いほど、単語のダブりが多いということです。つまり1に近いほど文書同士が似ている可能性が高くなるわけです。
次回はpythonでコサイン類似度を計算するコードを書いてみます。
チケットの見える化にチャレンジしてみる(2-2: 準備編 Janomeを使った形態素解析)
こんばんは!株式会社スマレジ 、開発部のmasaです。
前回、今回は数学のお話になりそうと言っていましたが、理論をする前に少し実装のイメージを記載した方が、あとで自分が読み返すときにわかりやすいかと思い直したので、今回は形態素解析の具体例として、Janomeを使ってredmine チケットの単語の頻度抽出をやってみます。
Janomeの概要については前回のブログをご覧ください。
まずはソースコード
実装をまず上げて、そのあとで説明を加えていきます。
from janome.tokenizer import Tokenizer from janome.analyzer import Analyzer from janome.charfilter import * from janome.tokenfilter import * import json import requests char_filters = [UnicodeNormalizeCharFilter(), RegexReplaceCharFilter('<("[^"]*"|\'[^\']*\'|[^\'">])*>', u'')] tokenizer = Tokenizer() token_filters = [CompoundNounFilter(), POSKeepFilter(['名詞']), LowerCaseFilter(), TokenCountFilter(sorted=True)] headers = {'X-Redmine-API-Key': "<キーの値>"} params = {'project_id': 7} response = requests.get("<Redmine APIのURL>", headers=headers, params=params) text = response.text resJson = json.loads(text) issues = resJson["issues"] sentence = "" for issue in issues: sentence += issue["description"] analyzeInfo = Analyzer(char_filters=char_filters, tokenizer=tokenizer, token_filters=token_filters) surfaceList = [] for result in analyzeInfo.analyze(sentence): print(str(result))
ブロックワードについて
形態素解析を使って品詞の抜き出しをする際、頻繁に使われるのが名詞です。これはいわゆる「固有名詞」がその文章を特徴づけるデータとなることが多いためです。ところが、日本語の形態素解析の場合、半角の記号などは言語体系として存在しないため、名詞に分類されてしまうことが多いです。(特にhtmlを解析するときはhtmlタグが軒並み名刺扱いされることも)
こういった意図しないワードのヒットを避けるための概念として存在するのが「ブロックワード」、つまり単語のブラックリストになります。
上記の「文字」に関するフィルタリングはソースコード上の
char_filters = [UnicodeNormalizeCharFilter(), RegexReplaceCharFilter('<("[^"]*"|\'[^\']*\'|[^\'">])*>', u'')]
ここが該当します。UnicodeNormalizeCharFilter()で文字コードを指定して、RegexReplaceCharFilter('<("[^"]"|\'[^\']\'|[^\'">])*>', u'')でhtml形式のフィルタをかけています。
また、例えば弊社であれば「スマレジ 」という言葉は日常的に使われている言葉であり、Redmineチケットや車内の文書でこの言葉が頻繁に使われているからと言って、それが特別な意味を持つこともなく、逆に分析の上でノイズになってしまいます。
また、ブロックワードの概念とは少しズレますが、分析の目的に応じて、品詞を指定して抽出することは重要です。
まず、助詞・感嘆詞などは文書上極めて頻繁に使われるため、さっきの「スマレジ 」の例と同じくノイズになってしまうため、計量テキスト解析をする際は除外することが多いです。
こういった、文字に関するフィルタリングは下記の部分が該当します。また、この部分で頻度を計測する指定もしています。
token_filters = [CompoundNounFilter(), POSKeepFilter(['名詞']), LowerCaseFilter(), TokenCountFilter(sorted=True)]
ここでそれぞれのフィルタの意味についてはAPIリファレンスを見てください。
[補足]ブロックワードは分析の目的によってチューニングする
「動詞・形容詞・形容動詞」などの述部で使用される品詞は文書上の意味を断定する役割を持つため、分析の目的によって抽出対象にするかどうかを判断する必要があります。例えば「花が綺麗だ」と「花が醜い」では同じ「花」という名詞が主語に来ていても、意味が真逆になります。「花」に関する話題の量を計測したいなら、同じ重みでいいですが、「花」に向けられた意味を含めた計量をしたい場合、これらは別々に計量しなければいけません。(当たり前のことなんですが、何度も実施していると分析麻痺になることもあるので、意識は常に持っておくのが良いと思います。)
これは、個人的な意見になりますが、業務で実施するのであればまずは名詞だけに限定して、分析対象を広く取って、 固有名詞の分布状況の把握から行うのが良いと思います。最も手軽ですし、計算負荷も軽量であることが多いためです。 名刺のみの分析結果から導き出される結論は、人間の認識からずれることも少なく、新たな発見にはつながりづらいですが、 プログラムのバグを潰すためのある種の教師データにもなり得ますし、本当にズレがあったのならそれはチケット状のやり取りと人間の認識が大きくずれていることを示すため、重大な事象を示している可能性もあるためです。
Redmine APIをコールする
これは過去のブログでも扱ったことがあるので、該当コードの部分だけ示します。
過去のブログは↓
該当するソースはここ。今回はproject_idが7のチケットを全て取ってくるように指定しています。 その上で、渡ってきたテキスト情報をjsonにデコードして辞書型で取り込んで、issueのみ取り出しています。 そのあとで、チケットの本文部分を全てつなげてとってきています。
headers = {'X-Redmine-API-Key': "<キーの値>"} params = {'project_id': 7} response = requests.get("<Redmine APIのURL>", headers=headers, params=params) text = response.text resJson = json.loads(text) issues = resJson["issues"] sentence = "" for issue in issues: sentence += issue["description"]
ブロックワードを適応させ、分析を実行する
ここも対応するソースコードだけ。詳しく知りたい人は上記のAPIリファレンスを見てみて下さい。
analyzeInfo = Analyzer(char_filters=char_filters, tokenizer=tokenizer, token_filters=token_filters) for result in analyzeInfo.analyze(sentence): print(str(result))
チケットの見える化にチャレンジしてみる(2-1: 準備編 形態素解析)
こんにちは!株式会社スマレジ 開発部のmasaです。 緊急事態宣言が明けて、大阪支社のある本町〜心斎橋は少し活気が戻ってきた感じがあります。 ワクチン摂取も始まったので、コロナ禍が収束することをただただ願うばかりです。
さて、今回は前回の続きで、まずはpythonを使って形態素解析をしていみます。
形態素解析とは
平たく言えば、文章を品詞(形態素)で分解する自然言語の解析・・・というかデータの前処理です。
私たちがこうして普段使っている日本語は、国文法で定められた品詞に分解することができます。 例えば、「masaはamazonでpythonの入門書を購入した」という文章は、 「masa/は/amazon/で/python/の/入門書/を/購入し/た」という風に分けることができ、それぞれが
- masa ... 名詞
- は ... 助詞
- amazon ... 名詞
- で ... 助詞
- python ... 名詞
- の ... 助詞
- 入門書 ... 名詞
- を ... 助詞
- 購入し ... 動詞
- た ... 助詞
という品詞に分解できます。小〜中学校の国語の時間に勉強したという方が多いのではないでしょうか。
余談ですが、この「国文法」は実は何人かの言語学者が文法を挙げており、学校で教えるのは橋本進吉という言語学者が作った橋本文法がベースになっています。 興味のある方はググってみてください。
形態素解析ライブラリ Janome
形態素解析を人間ではなく、ソフトウェアで行う場合は使用する言語の辞書をあらかじめソフトウェアにインストールしておく必要があります。辞書と解析を別々にした解析ソフトはmecabやchasenが有名で、これらは実績もあり多くのAIや自然言語を扱うソフトウェアで利用されています。
しかし、現代では次々に新しい言葉が生まれてきており、辞書自体も定期的にアップデートしてあげる必要があります。 それはつまり、ソフトウェアの保守作業に「辞書のアップデート」が追加されることを意味し、保守手順を1つ増やすことを意味します。
出来る限りそういった手間はかけたくないので、今回は辞書と解析ツールが一体となっているライブラリであるjanomeを利用します。
こうすることで、pythonライブラリのアップデートのみで辞書の更新も対応できるようになります。 ただし、辞書一体型は基本ライブラリ提供側がライブラリの更新をしない限り、いつまでも同じ辞書を使うことになります。 そのため、商用のソフトを開発するのであれば、辞書分離型のライブラリを利用する方が良いとmasa的には思います。 よく使われる辞書分離型形態素解析ツールであるMeCabは下記からダウンロードできます。
このほかにもAPIで形態素解析を行うサービスやJavaScriptで作成されている解析ツールもあるので、 用途と環境に応じて適切な解析ツールを選ぶことが大事になります。
また、Janomeを利用した形態素解析の実施方法については、下記の記事が参考になりました。
今回もコーディングがないですが、次回も多分ないです汗 (次回は多分、数学のお話)
チケットの見える化にチャレンジしてみる(1: やりたいこと編)
こんばんは!株式会社スマレジ 、開発部のmasaです!
今週の大阪は平日はスカッと晴れていたんですが、土曜日から曇り空で湿気を感じる休日でした。。。
さて、長いことスマレジ プラットフォームAPIのお話をしてきましたが、今回から別のことをやっていきます。
大量のRedmine チケットを整理したい
弊社では開発業務にRedmineを利用しており、もう運用し始めて数年経ちます。 結果、数十万近くのチケットが生成され、さらに年を追うに連れてチケット起票のペースも上がっています。
チケットの数が増えると、類似のチケットを探すのにも一苦労。「このバグ、チケット化してたっけ?」と思って検索して、フリーワードで検索すると検索結果が100件overだったり・・・。フリーワード検索の後、進行状況などでさらに絞り込むので、チケットを探しきれなくてめちゃ困るという事例は(ほぼ)発生していないのですが、今後、チケット量が増えてくると問題になってしまいそうではあります。
そこで、もうちょっと感覚的にチケットの全体像を見える化できないかな?と思い、色々と試行してみた内容と結果をつらつらと書いていこうかなと思っています。
注意点として、うちのチケットの分析結果をそのまま載せることはできないので、ある程度マスクしたり、サンプルデータなどで紹介するので、あらかじめご了承ください。
ざっくり作りたいもの
めちゃめちゃざっくりですが上の図のように、チケットを1つの点として、下記の尺度で平面にマッピングするというものです。
- 類似したチケットが「近く」表示
- redmineのステータスを元に、関連性を「矢印」で表示
あまり手の込んだ分析を作るのは大変なので、まずはこれだけを表示できるようにします。 とは言ってもこの図を書くだけでも、
- 何をもって近い・遠いとするの?
- この画面の上下左右にはどういう意味があるの?
- 矢印は何を基準に引いて、向きはどうやって決まるの?
などなど、決めていくことはかなり沢山ありますし、正直作ったとして私たちの感覚に沿う、あるいはヒントを与えてくれる分析結果が出るかどうかもビミョーです。
ぶっちゃけますと...
解決したい課題はあげたのですが、今回は本音は「面白そうなんでやってみよう」くらいしか考えてないです笑。(先述のように直近で困ってませんし)
まあでも、面白い結果が取れるかもしれませんし、上図のような重み付き有向グラフの形式で取れれば、離散グラフの分析に転用することもできますから、期待せずに楽しむことを意識してやっていきますー。
プラットフォームAPI(POS)で取引情報を大量に取得する際の注意点
こんにちは!株式会社スマレジ 、開発部のmasaです。 投稿間隔が空いてしまいすいません🙇♂️ 風邪をひいてしまい、ダウンしておりました。 季節の変わり目なので、皆様も体調管理にはお気をつけください。
さて、今回はよくプラットフォームAPI周りでいただく質問の一つを掘り下げようと思います。
取引情報の同期をしたいが、コール数が足りない!
現在スマレジ プラットフォームAPIには、下記のリンクから確認できるように、プラン価格毎のコール数の制限を将来的に実施することを掲示しています。
(ディベロッパー管理画面の中にある説明なので、ログインしてご確認ください)
コール数関連の質問について、このブログでお答えできることはないのですが、他社APIと同じく弊社APIについても無計画な大量アクセスを許容してしまうとAPIサーバーやデータベースへのアクセスが不安定になり、サービス品質を落としてしまうことになります。
特にPOSのAPIのように、取引履歴情報を大量に抱えるデータベースへのアクセスがある場合、この安定性の問題がより大事になってきます。 しかし、「なんとか全ての取引情報を取ってきたい!」「取引同期が行えないと作りたいアプリが作れない!」というケースもあるかと思うので、今回は現段階で実装できる取引の同期方法についてご紹介します。
取引情報の構成
スマレジ の取引は大きく3つの情報から成り立っています。 例えば、「渋谷店でAさんが税込み800円のコップを3つと一つ1000円の皿を2枚、現金3000円とPayPay1400円で購入した。」という取引があったとすると、下記のように分類されます。
- 取引ヘッダ
- 「どの店」で「どの会員」が購入したか、といった1つのレシートに対して、一つ情報が存在するもの
- 上記の例だと「渋谷店で会員Aさんが現金3000円とPayPay1400円で購入」が該当
- 決済情報もこっちに含まれる。
- 取置き販売や取置きの引き取り状況、その他決済情報などもこっちに含まれる
- 取引一覧取得APIで大量に取得可能
- 取引明細
- 「どの商品を」「いくつ」「一つあたりいくらで」購入したのか、といった購入したものに関連する情報
- 上記の例だと「税込み800円のコップを3つと一つ1000円の皿を2枚購入」が該当
- 部門販売などもこっちに含まれる
- 取引明細CSV作成APIで大量取得可能
一度のコールで大量の取引ヘッダと取引明細を取ってくることは現状不可能
上述のように、取引ヘッダ・取引明細をそれぞれで大量取得することは可能ですが、1度にまとめてセットで取ってくることは現状できません。下記の取引取得APIではセットで取得することはできますが、これは取引IDを指定して取得するため、大量取得処理には不向きです。
取引情報については、一度連携側のマスタに差分更新で取り込んでから処理する
上述のように、現状まとめて取引を取ってくる手段がなく、また大量の取引情報をずっとメモリ上に持っておくことも非効率的なことがほとんどなので、取得した取引情報はそのままテーブルに登録することをお勧めします。 また上記で紹介した、取引一覧取得APIと取引明細CSV作成APIは「取引日時」や「更新日時」をリクエストに指定することができますので、「直近30分ごとに新たに生成/更新された取引を取得」という処理を30分間隔で実施するようにすれば、コール数を節約して、大量のデータを取得することができます。
Nuxt.jsからプラットフォームAPIをコールする(expressで認証編)
こんにちは!株式会社スマレジ 、開発部のmasaです。
緊急事態宣言が延長になり、masaもリモートワークの日が増えました。
僕自身は会社の近くに住んでいるので、通勤のリスクは少ないのですが、知らないうちに移してしまうリスクもあるので、たとえ体調が悪くなくても、毎朝の検温の結果やその他体の異常を少しでも感じたらリモートワークに切り替えるようにしています。
ワクチン摂取が進むまで、もうしばらく頑張りましょう!
今回は、前々回にNuxt.jsのサーバミドルウェアにExpressを導入したので、スマレジ プラットフォームAPIの認証を行います。
プラットフォームAPIの認証
認証の流れについては最新の仕様書を確認していただければと思います。
今回は、認証コードの取得が完了して、ユーザアクセストークンの取得を実施するところからの実装となります。
ソースコード(Node.js + Express)
いつの間にかはてなブログでIDEのスタイルそのままでコピペができるようになっているので、使ってみました。
今回はサンプルコードのため、共通化やバリデーションなどは行っていませんのでご了承ください。
想定は、
POST http;//(連携アプリのURL)/auth
でRequestとして、
{ "code": "認証コード" }
がフロントエンドから送られてくる想定です。
他の設定値については仕様書を確認しながらお読みください。
const bodyParser = require('body-parser')
const app = require('express')()
const request = require('request');
let contractId = null;
app.use(bodyParser.json())
app.post('/auth', (req, res) => {
const smaregiVerificationCode = req.query.code;
if (typeof smaregiVerificationCode === "undefined" || smaregiVerificationCode.length === 0) {
console.log("empty error");
res.json({data: null, message: "スマレジ のとの認証に失敗しました" });
return;
}
const URL = 'https://id.smaregi.dev/authorize/token';
const BASIC_CODE = 'クライアントID:クライアントシークレットをbase64エンコーディングしたもの';
//オプションを定義
const options = {
url: URL,
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': 'Basic ' + BASIC_CODE
},
json: true,
form: {
"grant_type": "authorization_code",
"code": smaregiVerificationCode,
"redirect_uri": "環境設定で指定したリダイレクトURI"
}
}
//リクエスト送信(ユーザアクセストークン取得 -> 契約ID取得 -> 部門一覧API実行の順)
request(URL, options, function (error, response, body) {
//ユーザアクセストークン取得
const userInfoUrl = "https://id.smaregi.dev/userinfo";
const userInfoOptions = {
headers: {
Authorization: "Bearer " + body.access_token
}
}
console.log(body);
request(userInfoUrl, userInfoOptions, function (error, response, body) {
//契約ID取得
const bodyObj = JSON.parse(body);
console.log(bodyObj);
const contractInfo = bodyObj.contract;
if (typeof contractInfo === "undefined") {
return;
}
contractId = contractInfo.id;
if (typeof contractId === "undefined") {
return;
}
const accessTokenUrl = "https://id.smaregi.dev/app/" + contractId + "/token";
const accessTokenOptions = {
url: accessTokenUrl,
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': 'Basic ' + BASIC_CODE
},
json: true,
form: {
"grant_type": "client_credentials",
"scope": "pos.products:read pos.products:write" // 必要なスコープを半角スペース区切りで指定
}
}
request(accessTokenUrl, accessTokenOptions, function (error, response, body) {
//部門一覧API実行
const accessToken = body.access_token;
if (typeof accessToken === "undefined") {
return;
}
const smaregiCategoryUrl = "https://api.smaregi.dev/" + contractId + "/pos/categories";
const smaregiCategoryOptions = {
url: smaregiCategoryUrl,
method: 'GET',
headers: {
'Authorization': 'Bearer ' + accessToken
},
json: true,
qs: {
"limit": "1000"
}
}
request(smaregiCategoryUrl, smaregiCategoryOptions, function (error, response, body) {
...
});
});
res.json({contract_id:contractId});
});
});
});
module.exports = app
16期が終わって
こんにちは! 株式会社スマレジ、開発部のmasaです。
今日は開発ブログではなくふりかえりです。 株式会社スマレジは4月決算で来月より17期に入ります。 なので、今回は16期のふりかえりをしていきます。
16期のキーワードは「チームを支える」と「インフラ」
チームを作る
実は16期から、スマレジサービスの加速に伴い、POSチームのメンバー追加が発生し、新生チームになりました。 僕が1年を通して技術面以外で一番学んだことは「チーム作りの難しさ」です。
POSサーバーチームに入ってきた方は新入社員さんのみでした。 しかし、もtもとIT業界でエンジニアとして長くやってらした経験豊富な方が多く、 IT業界はまだ3年僕ですがちょっとの僕ですが、僕の目には持っている技術力やマネジメント力は非常に高い方ばかりに見えましたし、実際そうだと思います。
ただ、そういったハイレベルな方であっても、SaaSエンジニアの経験は少ない方がほとんどで、 新生チームの僕の最初の仕事は「スマレジ」のエンジニアの考え方を共有することでした。
SaaSエンジニアの考え方
ITエンジニアにとって一番大事なのはコミュニケーション力と技術力で、SaaSエンジニアもそこは変わらないと思います。 ただ、SaaSエンジニアはそれに加えて、SIerとはまたちがう仕様を把握し再設計する力がものを言います。
SIはその規模にもよりますが、スクラッチであれば真っ白なところから新たに作る分、「産みの苦しみ」はありますが すでにあるプロダクトを強化していく「育ての苦しみ」はありません。 うちのチームにJOINした方はまずこの「育ての苦しみ」に悩みます。
極端な話、ボタンの配置一つ変えるだけで大騒ぎになるのがSaaSだと僕は思っています。 常に現行の仕様を考慮し、「使っている人が混乱しないような仕様変更」を意識しながら実装していく・・・。 僕個人は、この作業は使っている人をイメージしながら仕事ができるので楽しいんですが、苦痛な人にとっては苦痛みたいですね。 今同じチームにいてくれる方はこの作業の理解をしてくれながら、お仕事をしていただけているので本当に助かります。
巨大なデータとインフラの知識
特に既存の変更の中で一番神経を使うのが、取引系の機能の改修です。 スマレジはサービスインしてからずっと取引情報をため続けているので、取引データのI/Oを新たに増やすときなどは非常に苦悩します。 今年度であれば、プラットフォームAPIの「取引会員更新API」や「取引明細CSV作成API」などですね。 これらの実装は、上記のベテランの新メンバーさんやインフラチームと連携して、どうにか実装にこぎつけました。
そして、これらを通してインフラ、特に大量データを扱う際に注意すべき点や回避策について、かなり学ぶことができました。
ふりかえってみて
振り返ってみると、非常に学びと成長の多い1年でした。自分自身のスキルも少しずつ成長して、できることが広がってきている段階なんでしょうか? 来期は好奇心旺盛に、でも慎重により自分の強みを伸ばしていければと思います。