雑記 in hibernation

頭の整理と備忘録

ビジネスメールから受けるダメージをテキスト感情分析で和らげる試み

労働が人間の精神に及ぼす悪影響については今更言及の余地はないでしょう。業務・責任に見合わない給与や長時間の拘束などなど、労働のネガティブな要素を挙げ始めるとキリがありませんが、僕個人としては、ビジネスコミュニケーション上で発生する精神的負担は負の要素の最たるものに思われます。

特にメールでのやりとりは僕のようなメンタルクソ雑魚くんにはなかなか辛いものがあります。リモートワークの普及により、以前に増してテキストベースでのやりとりが増加している昨今、無機質なメールの文面から相手の感情を邪推しては勝手に精神をすり減らす毎日を送る方も多くいらっしゃるのではないでしょうか。

この「無機質なメールの文面から相手の感情を邪推しては勝手に精神をすり減らす」 a.k.a.「ビジネスコミュニケーション独り相撲横綱」問題、ひょっとするとメールの文面を「書き手の感情を推定して」「ポップに装飾」することで解決できるのではないか。そんな発想のもと、テキスト感情分析のパッケージを利用してメールの文面から感情を推定し、感情に適した顔文字を付加することで受け手の精神的ダメージを緩和するコードを書いていきたいと思います。


やりたいこと

入力テキストデータの各行に対して感情分析を行い、その結果を元にテキストの各行に顔文字を追加したテキストを出力します。


全体の流れ

まあ対した処理ではないですが、全体の流れをおさらいしておきます。

以下のような感じです。

  1. 入力テキストを読み込む

  2. 入力テキストの各行に対して、以下の処理を実施

    1. 感情分析にかけて、positive/negative/neutral を判別
    2. 顔文字リストから判別した感情に相当する顔文字をランダムに抽出し、原文の文末に付加した文字列を出力用テキストに追加


実装

以降、具体的な実装について説明します。言語はPythonです。

なお、実行環境はgoogle colaboratoryです。必要なライブラリをpipでインストールするところからやっていきます。


1. ライブラリのインストール・インポート

今回の感情分析は、感情分析パッケージの"asari"を利用していきます。これはtf-idf によるベクトル表現とSVMにより学習されたポジティブ・ネガティブ判定のモデルです。学習元のデータが不明であるため、今回の課題に対して適切である保証はありませんが、まあ、初めの一歩としてとりあえずこれを使わせていただきましょう。

hironsan.hatenablog.com


余談ですが、感情分析のためにいの一番に検討したパッケージは"ML-Ask"でした。理由は、{喜, 怒, 哀, 怖, 恥, 好, 厭, 昂, 安, 驚}の10種類の感情を推定できる点です。今回は最初の一歩として「ネガ/ポジ」の一次元での感情評価になりますが、ML-askで実装しておけば今後の機能拡張としてより多彩な感情を判別して顔文字に反映する余地がありそうだと考えていました。

arakilab.media.eng.hokudai.ac.jp


しかし、いざ実装してみたところ、分析結果が"neutoral"となることが多く、ポジティブ・ネガティブの顔文字が付与できないケースが多く見られました。一方でasariでは分析結果がnegativeとpositiveの確信度の形で得られるため、無理矢理にでも正負のどちらかの感情に解釈できて使い勝手が良かったため、asariを採用するに至りました。ML-askの実装の話は気が向いたら別の記事に書きます。ちなみに、同じ方が作成されているらしいパッケージ"oseti"はasariと同様に連続値で結果が得られますが、neutoral(0)に判別されることが多い点では同傾向でした。

asariの導入については、こちらを参考にしています。 qiita.com


なお、導入時に引っかかるポイントとして、asariはPython3.6のみ対応とのことで、Python3.7だとエラーが出ます。こちらによると、対策としてJanomeを0.3.7にダウングレードするとしのげるとのことです。


# ライブラリのインストール
!pip install Janome==0.3.7 # https://teratail.com/questions/293005
!pip install asari
# ライブラリのインポート
import numpy as np
import pandas as pd
import re
import sklearn
import asari
import random


2. 入力ファイルの読み込み

入力テキストや顔文字リストなど、処理に利用するデータを読み込んでいきます。

2.1. パラメータ設定

あとで使うパラメータとか諸々を設定しときます。

# 顔文字リストのパス
kaomojilist_path = "/path/"
kaomojilist_filename = "顔文字リスト.csv"

# 入力する文章
# ここから精神的にダメージを負いやすい文章をピックアップ https://business-mail.jp/example
input_path = "/path/"
input_filename = "test_催促.txt"

# 感情閾値
neutral_abs = 0.5 # 中心値
neutral_range = 0.1 # 中心値からニュートラルとみなす範囲


2.2. 顔文字リストの読み込み

こちらから拝借してあらかじめ作成しておいた顔文字リストをpandasのデータフレームとして読み込みます。 顔文字リストは、「喜/怒/哀/楽/その他」のラベルが振られた顔文字のリストです。ちなみに、ラベルは引用元の顔文字の分類を参考にして手動でふっています。

# 顔文字リスト の読み込み
df_kaomojilist = pd.read_csv(kaomojilist_path+kaomojilist_filename)
df_kaomojilist
icon emotion
0 (^_^)
1 (⌒▽⌒
2 (^0_0^)
3 ^O^アーーーン
4 (^Q^)アーーン
... ... ...
182 (*`д´)b全然OK その他
183 (●´∀`)b そぅ☆ その他
184 (`・∀+´・)b そうだ!! その他
185 d(´∀` )了解 その他
186 (*´∀`)ゞラジャ! その他


2.3. 入力テキストの読み込み

入力テキストを読み込み、改行・句読点ごとにリストに分割します。

# テキストファイルの読み込み
f = open(input_path + input_filename)
input_text = f.read()

# 句読点と改行をセパレータとしてlistに分割
sentences = re.split("[。,\n]", input_text)
sentences
['社員各位',
 '',
 'お疲れ様です',
 '',
 '経理部の△△です',
 '',
 '',
 '先日交通費精算の期限について、メールをお送りしました',
 '',
 'まだ提出していない方は、大至急提出をお願いします',
 '',
 '',
 '既に期限は過ぎておりますが、',
 '',
 '○月○日(○)17:30まで',
 '',
 'この分まで、特別に受け付けます',
 '',
 '',
 '最近、期限後に提出される方が多くなっておりますが、',
 '処理が翌月にまわってしまう可能性があります',
 '',
 '',
 'またギリギリに提出されると、確認作業に時間を要するため、',
 'なるべく余裕をもって提出してくださると助かります',
 '',
 '',
 '年度末でお忙しい時期とは思いますが、',
 '期限日の厳守にご理解とご協力をお願いいたします',
 '']


ちなみに、元の文面は以下の通りです。

社員各位

お疲れ様です。 経理部の△△です。

先日交通費精算の期限について、メールをお送りしました。 まだ提出していない方は、大至急提出をお願いします。

既に期限は過ぎておりますが、

○月○日(○)17:30まで

この分まで、特別に受け付けます。

最近、期限後に提出される方が多くなっておりますが、 処理が翌月にまわってしまう可能性があります。

またギリギリに提出されると、確認作業に時間を要するため、 なるべく余裕をもって提出してくださると助かります。

年度末でお忙しい時期とは思いますが、 期限日の厳守にご理解とご協力をお願いいたします。


どうやら提出期限をすっぽかしていたようです。100%こちらが悪い状況にあって「大至急提出をお願いします。」「ギリギリに提出されると、確認作業に時間を要するため、 」など実害を事務的に伝えてくる文面がこちらのメンタルを容赦なく削ってきますね。いやぁ、もう、本当に申し訳ない。


3. 感情分析

さて、ここからが本番の感情分析です。

3.1. 関数定義

0~1の感情評価値(感情分析の出力で、高いほどポジティブ)と、ニュートラル判定の基準値(中心値とレンジ)、顔文字リストを引数として、評価値に対応した顔文字をランダムで返す関数を用意しておきます。

今回は、感情評価値と顔文字は以下のように対応させています。

  • 0.0~0.4:「怒」or「哀」(ネガティブ)
  • 0.4~0.6:「その他」(ニュートラル)
  • 0.6~1.0:「楽」(ポジティブ)

※ちなみに、今回は顔文字リストに「喜」に相当するラベルを振っておらず、ポジティブな顔文字には全部「楽」を振ってます。理由は、作業の途中でめんどくさくなったからです。

# 感情の評価値を受けて絵文字を返す関数
def chooseRandomKaomoji(emotion_value, neutral_abs, neutral_range, df_list):
    # 感情ラベルの割り当て
    emotion_label = "その他"
    if emotion_value > neutral_abs + neutral_range :
        emotion_label = "楽"
    # 今回は怒と哀の区別はないので、とりあえずランダムにどっちかを割り当てる
    if emotion_value < neutral_abs - neutral_range:
        if random.random() > 0.5:
            emotion_label = "怒"
        else: emotion_label = "哀"

    # ラベルに該当する感情の絵文字をランダムに取得
    kaomoji = df_list[df_list["emotion"]==emotion_label].sample()["icon"].tolist()[0]
    #print(emotion_value, emotion_label, kaomoji)

    return kaomoji


3.2. 感情分析と顔文字付与

入力テキストの各文章ごとに、感情分析して顔文字を付与していきます。

感情分析sonar.ping(sentence)の結果、ポジティブ・ネガティブ感情それぞれ確信度と最終結果のラベルが辞書型で出力されます。今回はポジティブの確信度を感情評価値として取得し、これを用いて付与する顔文字の選定を行います。

# https://qiita.com/hnishi/items/0d32a778e375a99aff13
from asari.api import Sonar
sonar = Sonar()

result_text = ""

# 変換対象の文面を読み込む
for sentence in sentences:

    # 感情分析
    analyze_result = sonar.ping(sentence)

    # 感情に応じた顔文字を取得
    emotion_value = analyze_result["classes"][1]["confidence"] # ポジティブ感情の確信度
    kaomoji = chooseRandomKaomoji(emotion_value, neutral_abs, neutral_range, df_kaomojilist)

    # 文字列が空白の場合、顔文字も空白にする。
    if sentence == "" : 
        kaomoji = ""
    else:
        # 動作確認用の出力
        print(sentence, kaomoji, '{:.2f}'.format(emotion_value))

    # テキストに出力を追加
    result_text += sentence +" "+ kaomoji + "\n"

/usr/local/lib/python3.7/dist-packages/sklearn/base.py:318: UserWarning: Trying to unpickle estimator LinearSVC from version 0.20.2 when using version 0.22.2.post1. This might lead to breaking code or invalid results. Use at your own risk.
  UserWarning)
/usr/local/lib/python3.7/dist-packages/sklearn/base.py:318: UserWarning: Trying to unpickle estimator LabelEncoder from version 0.20.2 when using version 0.22.2.post1. This might lead to breaking code or invalid results. Use at your own risk.
  UserWarning)
/usr/local/lib/python3.7/dist-packages/sklearn/base.py:318: UserWarning: Trying to unpickle estimator _SigmoidCalibration from version 0.20.2 when using version 0.22.2.post1. This might lead to breaking code or invalid results. Use at your own risk.
  UserWarning)
/usr/local/lib/python3.7/dist-packages/sklearn/base.py:318: UserWarning: Trying to unpickle estimator CalibratedClassifierCV from version 0.20.2 when using version 0.22.2.post1. This might lead to breaking code or invalid results. Use at your own risk.
  UserWarning)
/usr/local/lib/python3.7/dist-packages/sklearn/base.py:318: UserWarning: Trying to unpickle estimator TfidfTransformer from version 0.20.2 when using version 0.22.2.post1. This might lead to breaking code or invalid results. Use at your own risk.
  UserWarning)
/usr/local/lib/python3.7/dist-packages/sklearn/base.py:318: UserWarning: Trying to unpickle estimator TfidfVectorizer from version 0.20.2 when using version 0.22.2.post1. This might lead to breaking code or invalid results. Use at your own risk.
  UserWarning)

社員各位 (^o^)\(^^ )ヨシヨシ 0.73
お疲れ様です (⌒^⌒)b 0.75
経理部の△△です (●´▽`)ナハハ 0.82
先日交通費精算の期限について、メールをお送りしました (๑´ლ๑)フフ♡ 0.70
まだ提出していない方は、大至急提出をお願いします (#・∀・)ムカムカ 0.23
既に期限は過ぎておりますが、 ポリポリf(^_^; 0.64
○月○日(○)1730まで ヘヘ(^^; 0.72
この分まで、特別に受け付けます (^_^) 0.86
最近、期限後に提出される方が多くなっておりますが、 (´ ε `。・。)クスン 0.25
処理が翌月にまわってしまう可能性があります ^O^アーーーン 0.90
またギリギリに提出されると、確認作業に時間を要するため、 (๑´ლ๑)フフ♡ 0.82
なるべく余裕をもって提出してくださると助かります キタ━━━━(゚∀゚)━━━━!! 0.97
年度末でお忙しい時期とは思いますが、 (笑´w`) 0.96
期限日の厳守にご理解とご協力をお願いいたします )))))‘‘)/~~~ バイバ~イ! 0.57


上のテキストの出力結果は、動作確認のために改行のみの行を除去し、その他の行については顔文字・評価値と共に文面を出力しています。 「まだ提出していない方は、大至急提出をお願いします」や「最近、期限後に提出される方が多くなっておりますが、」といった文章がネガティブに評価されていて、なかなかいい感じに感情を推定してくれています。


ところで、めっちゃwarning出てんだけど何ですかねこれ、、、、

深追いできてないですが、asariがPython3.6のみに対応しているところを、3.7に無理矢理動かしていることに起因している気がします。動作としては狙い通りではあるので、一旦スルーします。


4. 出力結果の確認

で、出力はこんな感じ。

# 結果の確認
print(result_text)
# 結果の確認
社員各位 (^o^)\(^^ )ヨシヨシ
 
お疲れ様です (⌒^⌒)b
 
経理部の△△です (●´▽`)ナハハ
 
 
先日交通費精算の期限について、メールをお送りしました (๑´ლ๑)フフ♡
 
まだ提出していない方は、大至急提出をお願いします (#・∀・)ムカムカ
 
 
既に期限は過ぎておりますが、 ポリポリf(^_^;
 
○月○日(○)1730まで ヘヘ(^^;
 
この分まで、特別に受け付けます (^_^)
 
 
最近、期限後に提出される方が多くなっておりますが、 (´ ε `。・。)クスン
処理が翌月にまわってしまう可能性があります ^O^アーーーン
 
 
またギリギリに提出されると、確認作業に時間を要するため、 (๑´ლ๑)フフ♡
なるべく余裕をもって提出してくださると助かります キタ━━━━(゚∀゚)━━━━!!
 
 
年度末でお忙しい時期とは思いますが、 (笑´w`)
期限日の厳守にご理解とご協力をお願いいたします )))))‘‘)/~~~ バイバ~イ!


おお、いい感じに感情的かつポップな文面が出来上がってますね。


他の例文でも試してみる

アウトプットの挙動をもう少し見てみたいので、先に紹介したメール例文サイトからさらに追加で心にクる文面をピックアップしてきました。

以降、心臓の弱い方は閲覧注意です。


さて、まずは、初っ端から割と攻撃力の高いこちら。メルマガ停止の要望です。

お世話になっております。 一般社団法人日本ビジネスメール協会の山田太郎です。

現在、メールアドレス(●●)宛に ●●という件名のメルマガが届いております。

当方としては、登録をした記憶がまったくないため なぜ届いているのか、理由が分からない状況です。

また解除しようとしても、解除の方法が分かりません。

メルマガの配信をストップしてください。

また、配信停止の手続きが終わりましたら、 ご一報ください。

それでは、よろしくお願いいたします。


メルマガ送ってくんじゃねーよ、しかも止め方わかりにくいんだよ、的なメールですね。直球の「メルマガの配信をストップしてください。」が怖すぎるし、こんな豪速球は僕のメンタルでは受け切れません。

ということで、先程のコードで顔文字を追加してみたのが以下の文面です。

お世話になっております (^-^)vイエイ!
 
一般社団法人日本ビジネスメール協会の山田太郎です (⌒▽⌒
 
 
現在、メールアドレス(●●)宛に (*´∇*)
●●という件名のメルマガが届いております ( ̄ー ̄)ニヤリッ
 
 
当方としては、登録をした記憶がまったくないため (ノω=;)。。。
なぜ届いているのか、理由が分からない状況です (´ノω・`*)ナケル-
 
 
また解除しようとしても、解除の方法が分かりません | `Д´|ノこらぁ!
 
 
メルマガの配信をストップしてください キタ━━━━(゚∀゚)━━━━!!
 
 
また、配信停止の手続きが終わりましたら、 |´▽`●)ノ |Ю  | イッテキマース
ご一報ください ハンセイ□\(.. )
 
 
それでは、よろしくお願いいたします *\(^o^)/*


中盤の「なぜ届いているのか、理由が分からない状況です (´ノω・`*)ナケル-」「また解除しようとしても、解除の方法が分かりません | `Д´|ノこらぁ!」など、被害状況を訴えつつも過度にトゲを感じさせない文面に仕上がっています。

一方、肝心の「メルマガの配信をストップしてください」ではなぜか「キタ━━━━(゚∀゚)━━━━!!」とテンションが爆上がりしていたり、こちらに非があるにも関わらず後半で急に「ご一報ください ハンセイ□\(.. )」と下手に出たり、やや情緒不安定な部分も垣間見えます。ポジ/ネガの感情評価が狙いからズレてしまっているようなので、やはり厳密には感情分析モデルを問題に合わせてチューニングしていく必要があるのかもしれません。また、ML-Askなどに存在する(積極的/受動的)の評価も反映させると、テンションにあった顔文字が付与できるようになるかもしれません。


次のメールは、個人的にはちょっと他人事とは思えないこちら。システム不具合の報告です。

システム部 鈴木さん

お疲れ様です。営業部の山田です。

受注システムが、9月5日(月)15時頃から不具合を起こしています。 お手数ですが、調査をお願いできますでしょうか。

不具合項目は以下の2点です。

(1)受注入力画面で「発注書印刷」のボタンが使えない

(2)受注入力で「納品区分・納品先・希望納期・回収区分」を空白にすると

登録時にエラーが出る

現在、受注処理ができない状態です。 お客さまへの発送に時間がかかり、クレームにつながる可能性があります。

以上2点について、解消までにどのくらい時間がかかりそうか 本日の17時までに、ご連絡いただけますでしょうか。

よろしくお願いいたします。


つら。自分の受信ボックスかと見てるのかと思ったわ。淡々と状況だけを伝えてくるメールで、想像力が(悪い方に)無限に掻き立てられますね。

ということで、顔文字を追加してみたのが以下です。

システム部 (*゚▽゚*)ワクワク
鈴木さん ^O^アーーーン
 
お疲れ様です (^o^)\(^^ )ヨシヨシ
営業部の山田です ウインク(^_-)-☆
 
 
受注システムが、95日(月)15時頃から不具合を起こしています ヽ(●´3`)ノ゛
 
お手数ですが、調査をお願いできますでしょうか キャ━━━━(゚∀゚)━━━━!!
 
 
不具合項目は以下の2点です ヘヘ(^^;

(1)受注入力画面で「発注書印刷」のボタンが使えない 号(┳Д┳)泣
 
(2)受注入力で「納品区分・納品先・希望納期・回収区分」を空白にすると (^_^)
登録時にエラーが出る (# ゚Д゚)ゴルァ!!

 
現在、受注処理ができない状態です (`-´)モウ!
 
お客さまへの発送に時間がかかり、クレームにつながる可能性があります リョウカイ!(^-^ゝ
 
 
以上2点について、解消までにどのくらい時間がかかりそうか (*´▽*)❀
本日の17時までに、ご連絡いただけますでしょうか (^з^)-☆Chu!!
 
 
よろしくお願いいたします (*n’∀’)n バンザーイ


「登録時にエラーが出る (# ゚Д゚)ゴルァ!!」「現在、受注処理ができない状態です (`-´)モウ!」など、怒りが程よくライトに表現されています。「受注システムが、9月5日(月)15時頃から不具合を起こしています ヽ(●´3`)ノ゛」「お客さまへの発送に時間がかかり、クレームにつながる可能性があります リョウカイ!(^-^ゝ」のあたりも本当はネガティブな顔文字が欲しかったところです。ここもポジ/ネガの評価のズレが効いているようです。


最後にお口直しということで、平和なメールでも試してみます。お礼の文面です。

●●さん

お疲れ様です。山田太郎です。

宿泊の手配、ありがとうございます。

急な依頼にもかかわらず迅速に対応していただき 本当に助かりました。

次回は、もう少し早く依頼が出来るようにしますね。

それでは、よろしくお願いいたします。

●●さん (^^ゞテレテレ
 
お疲れ様です (^Q^)
山田太郎です \( ˆoˆ )/
 
 
宿泊の手配、ありがとうございます ワーイ!(^O^)
 
 
急な依頼にもかかわらず迅速に対応していただき | `Д´|ノこらぁ!
本当に助かりました ポリポリf(^_^;
 
 
次回は、もう少し早く依頼が出来るようにしますね (*^o^*)
 
 
それでは、よろしくお願いいたします (o*゚ー゚)oワクワク


こちとら急な依頼に対応してあげたにも関わらず、「急な依頼にもかかわらず迅速に対応していただき | `Д´|ノこらぁ!」と意味なくキレられてしまいました。感情分析が狙い通りでない、という点に加え、ネガティブな感情に対しても「怒」「哀」で表現が分かれていれば、きっと無意味にキレられることもなかったでしょう。

あとどうでもいい点ですが、「次回は、もう少し早く依頼が出来るようにしますね (^o^)」は、これ、完全に煽ってるでしょ。反省の色が見えません。顔文字をつけない方がいい場合もある、ということかもしれません。もしくは、顔文字がついたところで所詮は受け手の塩梅。邪推する奴は顔文字からだって邪推するのだ、ということかも。


おわりに

ということで、思っていたよりいい感じに顔文字をつけることができました。

職場でのメンタルケアに有効活用してください。