我が家の息子が可愛すぎる。
可愛すぎるので、LINE Botを作成し布教したが、少し問題が・・・
はじめに
息子の可愛さを普及するために、AWS + LINEでBotを作った話の続編となります。元ソースや環境はこちらに記載してあります。
問題
「かわいい」と送信した際には、会話ができているが、「かわいいね」や「超!!かわいい!!!」など、「かわいい」という文字列にアドオンされた場合、該当するメッセージがないと判断され、「〜ってなんでちゅか?」と分からないフリをするかわいい。(親バカ)
原因
原因となる部分はこちら
const body = JSON.parse(event.body)
const events = body.events[0]
const message = events.message.text
const param = {
TableName: 'ReplyMapping',
Key: {
type: message ← ここ!!
}
}
const result = await dynamodb.get(param).promise()
let msg, img
if (result.Item) {
const getRandomList = (list) => list[Math.floor(Math.random() * list.length)]
msg = getRandomList(result.Item.msg)
img = getRandomList(result.Item.img)
} else {
msg = `${message}ってなんでちゅか?`
img = ''
}
メッセージとtypeが完全一致した時に、DynamoDBからレコードを取得している。
今回の事象は、「かわいい」というレコードは登録されているが、「かわいいね」や「超!!かわいい!!!」といったレコードが登録されていないため発生している。
対策
じゃあ、メッセージを形態素解析して、どういった種類のメッセージか。解析するようにしたら良いのでは?
実装
kuromoji.jsを使って実現してみよう。
※ kuromoji.jsとは、javaのオープンソース日本語形態素解析エンジンkuromojiのjs移植版である。
ラッパークラスを作ってみた。
'use strict'
const path = require('path')
const DIR = './node_modules/kuromoji/dict'
const kuromoji = require('kuromoji')
module.exports = class KuromojiWrapper {
constructor() {
}
async init() {
this.tokenizer = await new Promise((resolve, reject) => {
kuromoji
.builder({ dicPath: DIR })
.build((err, tokenizer) => {
if (err) reject(err)
resolve(tokenizer)
})
})
}
exec(text) {
return this.tokenizer.tokenize(text)
}
get(text) {
const res = this.exec(text)
// 第一優先: 感動詞
const prop1 = res.find((o) => o.pos === '感動詞')
if (prop1) return prop1.basic_form
// 第二優先: 形容詞
const prop2 = res.find((o) => o.pos === '形容詞')
if (prop2) return prop2.basic_form
return text
}
}
this.tokenizer.tokenize(text)
で形態素解析を行う。
例えば、超!かわいいね!
という文字列の場合、kuromojiからのレスポンスは下記となる。
[
{
word_id: 70940,
word_type: 'KNOWN',
word_position: 1,
surface_form: '超',
pos: '接頭詞',
pos_detail_1: '名詞接続',
pos_detail_2: '*',
pos_detail_3: '*',
conjugated_type: '*',
conjugated_form: '*',
basic_form: '超',
reading: 'チョウ',
pronunciation: 'チョー'
},
{
word_id: 91640,
word_type: 'KNOWN',
word_position: 2,
surface_form: '!',
pos: '記号',
pos_detail_1: '一般',
pos_detail_2: '*',
pos_detail_3: '*',
conjugated_type: '*',
conjugated_form: '*',
basic_form: '!',
reading: '!',
pronunciation: '!'
},
{
word_id: 1717990,
word_type: 'KNOWN',
word_position: 3,
surface_form: 'かわいい',
pos: '形容詞',
pos_detail_1: '自立',
pos_detail_2: '*',
pos_detail_3: '*',
conjugated_type: '形容詞・イ段',
conjugated_form: '基本形',
basic_form: 'かわいい',
reading: 'カワイイ',
pronunciation: 'カワイイ'
},
{
word_id: 92590,
word_type: 'KNOWN',
word_position: 7,
surface_form: 'ね',
pos: '助詞',
pos_detail_1: '終助詞',
pos_detail_2: '*',
pos_detail_3: '*',
conjugated_type: '*',
conjugated_form: '*',
basic_form: 'ね',
reading: 'ネ',
pronunciation: 'ネ'
},
{
word_id: 91640,
word_type: 'KNOWN',
word_position: 9,
surface_form: '!',
pos: '記号',
pos_detail_1: '一般',
pos_detail_2: '*',
pos_detail_3: '*',
conjugated_type: '*',
conjugated_form: '*',
basic_form: '!',
reading: '!',
pronunciation: '!'
}
]
この中から、pos
が感動詞となっているものを第一優先。形容詞となっているものを第二優先として、抜き出す。
※ ここは正直適当。優先順位を持たせることができたらとりあえずOK
呼び出し方としてはこう。
※ node.jsってconstructorでawaitできないのね・・・ちょっと強引に外部メソッドで実現。
const KuromojiWrapper = require('./KuromojiWrapper')
const main = async () => {
// イニシャライズ
const analysis = new KuromojiWrapper()
// 外部init
await analysis.init()
const res = analysis.get('超!かわいいね!!')
console.log(res)
}
main()
// 実行結果
// かわいい
既存の処理に組み込む
ソース全文
'use strict'
const line = require('@line/bot-sdk')
const client = new line.Client({ channelAccessToken: process.env.ACCESSTOKEN })
const crypto = require('crypto')
const AWS = require('aws-sdk')
const dynamodb = new AWS.DynamoDB.DocumentClient({
region: 'ap-northeast-1'
})
const KuromojiWrapper = require('./KuromojiWrapper')
exports.handler = async (event) => {
return new Promise(async (resolve, reject) => {
const signature = crypto.createHmac('sha256', process.env.CHANNELSECRET).update(event.body).digest('base64')
const checkHeader = (event.headers || {})['X-Line-Signature']
const body = JSON.parse(event.body)
if (signature !== checkHeader) {
reject('Authentication error')
}
const events = body.events[0]
const message = events.message.text
// ここから 追加
const analysis = new KuromojiWrapper()
await analysis.init()
const key = analysis.get(message)
// ここまで 追加
const param = {
TableName: 'ReplyMapping',
Key: {
type: key // ここもいじった
}
}
const result = await dynamodb.get(param).promise()
let msg, img
if (result.Item) {
const getRandomList = (list) => list[Math.floor(Math.random() * list.length)]
msg = getRandomList(result.Item.msg)
img = getRandomList(result.Item.img)
} else {
msg = `${message}ってなんでちゅか?`
img = ''
}
const replyText = {
type: 'text',
text: msg
}
const replyImage = {
type: 'image',
originalContentUrl: img,
previewImageUrl: img
}
client.replyMessage(events.replyToken, replyText)
.then((r) => {
return client.pushMessage(events.source.userId, replyImage)
})
.then((r) => {
resolve({
statusCode: 200,
headers: { 'X-Line-Status': 'OK' },
body: '{"result":"completed"}'
})
}).catch(reject)
})
}
結果
固定文言じゃなくても、単文レベルならそれっぽくできた!!!
ただ・・レスポンス遅くね?
まとめ
それっぽいロジックは組み込めたけど・・・
それっぽいロジックを組み込むことができたが、考えれば考えるほど様々な処理を入れたくなる。(例えば年齢ロジックとか、天気とか)
これらを詰め合わせると、とんでもないことになりそうな予感。
パフォーマンス劣化
パフォーマンスがかなり劣化。
ローカルで動かした時より圧倒的に劣化しているので、NW周りとか、Lambdaのメモリーとかが関係しているのかな。