Laravel + Amazon Comprehend で従業員の感情を日報から判別する

こんにちは❗フリーランス・エンジニアの 九保すこひ です。

さてさて、このところAWSのサービスをいろいろと体験しているので、今回もその流れで「あること」を実装してみることにしました。

それは・・・・・

文章から感情を読み取る

です。

そして、これを実現できるAmazon Comprehendで、

従業員の感情を日報から判別する

するというシステムを作ってみたいと思います。

目的としては、「従業さんのサポートが必要かどうかを数値化する」というものです。

どの世界でも「知らないことはできない」というのは当然のことですが、やはり自分からヘルプ・サインを出せない人が(特に日本人には)多いように感じますので、きっとAmazon Comprehendはこういった分野でも役に立つんじゃないかと考えました。

ということで、今回も楽しくプログラムしていきましょう❗

「どうやら千鳥温泉の
鏡広告、人気すぎて
年内はムリみたいです。😭」

開発環境: Laravel 8.x、Amazon Comprehend(バージョン:2017-11-27)

前提として

Laravelにログイン機能がインストール済みであることが前提です。
もしまだの方は以下から先にインストールしておいてください。

📝 参考URL

※なお、Jetstreamは高機能ですが改造が難しいので、Laravel Breezeをおすすめします👍

AWSへのアクセス情報を取得する

まずLaravelからAmazon ComprehendにアクセスするためにはAWSの「IAM」で取得できる以下2つが必要になります。

  • アクセスキー
  • シークレットアクセスキー

ということで、この作業は以下のページを参考にして取得しておいてください。

📝 参考ページ: アクセス情報(IAM)を取得する

なお、アクセス権限は「ComprehendFullAccess」にしました。

そして、取得できたら.envにセットしておいてください。

.env

AWS_ACCESS_KEY_ID="(アクセスキー)"
AWS_SECRET_ACCESS_KEY="(シークレットアクセスキー)"
AWS_DEFAULT_REGION=ap-northeast-1

パッケージをインストールする

AWSPHP用のパッケージを提供してくれていますので、以下のパッケージをインストールしておきます。

composer require aws/aws-sdk-php

業務日報システムをつくる

続いて、Laravelでシンプルな「業務日報システム」を作ります。(この部分はメインではないので、投稿だけできるようします)

モデル + マイグレーションをつくる

では、今回必要な「モデル + マイグレーション」を2ペア作ります。
内容は以下のとおりです。

  • work_reports: 日報のメインテーブル
  • work_report_sentiments: 感情分析した結果のテーブル

※つまり、work_reports : work_report_sentiments は「1:多」の関係になります。

以下のコマンドを実行してください。

php artisan make:model WorkReport -m
php artisan make:model WorkReportSentiment -m

すると、モデルとマイグレーションのファイルが作成されるのでそれぞれ以下のように変更してください。

database/migrations/****_**_**_******_create_work_reports_table.php

// 省略

public function up()
{
    Schema::create('work_reports', function (Blueprint $table) {
        $table->id();
        $table->unsignedBigInteger('user_id')->comment('ユーザーID');
        $table->text('description')->comment('内容');
        $table->timestamps();

        $table->foreign('user_id')->references('id')->on('users');
    });
}

database/migrations/****_**_**_******_create_work_report_sentiments_table.php

// 省略

public function up()
{
    Schema::create('work_report_sentiments', function (Blueprint $table) {
        $table->id();
        $table->unsignedBigInteger('work_report_id')->comment('日報のID');
        $table->string('type')->comment('感情タイプ'); // POSITIVE, NEUTRAL, MIXED, or NEGATIVE
        $table->unsignedTinyInteger('score')->comment('感情の度合い'); // 管理しやすいので 0〜100 で管理します
        $table->timestamps();

        $table->foreign('work_report_id')->references('id')->on('work_reports');
    });
}

そして、モデルにリレーションシップを追加します。

app/Models/WorkReport.php

<?php

namespace App\Models;

use App\Events\WorkReportSaved;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class WorkReport extends Model
{
    use HasFactory;

    // Relationship
    public function sentiments()
    {
        return $this->hasMany(WorkReportSentiment::class, 'work_report_id', 'id');
    }

    public function user() {

        return $this->belongsTo(User::class, 'user_id', 'id');

    }
}

変更が完了したら、マイグレーションを実行してテーブルを作成しましょう。

php artisan migrate

するとテーブルは以下のようになります。

コントローラーをつくる

次にコントローラーを作ります。
以下のコマンドを実行してください。

php artisan make:controller WorkReportController

するとファイルが作成されるので中身を以下のように変更します。

app/Http/Controllers/WorkReportController.php

<?php

namespace App\Http\Controllers;

use App\Models\WorkReport;
use Illuminate\Http\Request;

class WorkReportController extends Controller
{
    public function create()
    {
        return view('user.work_report.create');
    }

    public function store(Request $request)
    {
        // [ご注意] バリデーションは省略してます

        $word_report = new WorkReport();
        $word_report->user_id = auth()->id();
        $word_report->description = $request->description;
        $result = $word_report->save();

        return ['result' => $result];
    }
}

ビューをつくる

続いて先ほどのコントローラー内でセットしたビューを作成します。

resources/views/user/work_report/create.blade.php

<html>
<head>
    <link href="https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css" rel="stylesheet">
</head>
<body>
<div id="app" class="py-5 container mx-auto">
    <div class="text-center text-3xl font-bold mb-3">
        業務報告
    </div>
    <div class="bg-gray-200 p-2">業務報告</div>
    <div class="border-2">
        <textarea class="p-2 w-full" rows="10" v-model="params.description"></textarea>
    </div>
    <div class="text-right pt-5">
        <button type="button" class="bg-blue-700 text-blue-50 p-3 text-xl" @click="onSubmit">送信する</button>
    </div>
</div>
<script src="https://unpkg.com/vue@3.0.11/dist/vue.global.prod.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.21.1/axios.min.js"></script>
<script>

    Vue.createApp({
        data() {
            return {
                params: {
                    description: ''
                }
            }
        },
        methods: {
            onSubmit() {

                if(confirm('送信します。よろしいですか?')) {

                    const url = '{{ route('user.work_report.store') }}';
                    axios.post(url, this.params)
                        .then(response => {

                            if(response.data.result === true) {

                                alert('保存が完了しました。');
                                this.params = {
                                    description: '',
                                };

                            }

                        });

                }

            }
        }
    }).mount('#app');

</script>
</body>
</html>

ルートをつくる

最後にルートを書いていきます。

routes/web.php

use App\Http\Controllers\WorkReportController;

// 省略

Route::prefix('user')->middleware('auth')->name('user.')->group(function() {

    Route::resource('work_report', WorkReportController::class)->only([
        'create',
        'store'
    ]);

});

感情分析する部分をつくる

では、ここからがメインの日報から感情分析をする部分になります。

流れとしては以下になります。

  • 日報が保存される
  • イベントを自動的に起動する
  • イベント内でAWSから感情分析結果を取得
  • 結果を保存

イベントをつくる

では、日報が保存されたら必ずコードが実行できるようにWorkReportSavedイベントをつくってセットしていきましょう。

以下のコマンドを実行してください。

php artisan make:event WorkReportSaved

するとファイルが作成されるので、中身を以下のように変更します。

app/Events/WorkReportSaved.php

<?php

namespace App\Events;

use App\Mail\NegativeFeelingsDetected;
use App\Models\WorkReport;
use App\Models\WorkReportSentiment;

// 省略

class WorkReportSaved
{
    // 省略

    public function __construct(WorkReport $work_report)
    {
        $client = new ComprehendClient([
            'region' => 'ap-northeast-1', // 東京リージョン
            'version' => '2017-11-27'
        ]);

        try {

            $text = $work_report->description;
            $result = $client->detectSentiment([
                'LanguageCode' => 'ja',
                'Text' => $text // このテキストを感情分析する
            ]);
            $scores = $result['SentimentScore'];

            foreach ($scores as $type => $score) {

                $score = round($score * 100); // 操作しやすいように 0〜100 へ変換
                $sentiment = new WorkReportSentiment();
                $sentiment->work_report_id = $work_report->id;
                $sentiment->type = $type;
                $sentiment->score = $score;
                $sentiment->save();

            }

        } catch (\Exception $e) {

            // ここにうまくいかなかったときの処理

        }

    }
}

そして、イベントをつくっただけでは起動されませんので、WorkReportモデルにセットしておきます。

app/Models/WorkReport.php

<?php

// 省略

class WorkReport extends Model
{
    use HasFactory;

    protected $dispatchesEvents = [
        'saved' => WorkReportSaved::class
    ];

これで、日報が保存(作成、変更)されたら自動的にAmazon Comprehendにアクセスし、感情分析の結果を取得&保存できるようになりました。

ネガティブな感情を検知したら管理者へ通知するようにする

せっかく感情分析ができるようになったので、もし日報の文章から「この従業員はストレスを感じている」と判断されたら、管理者へメールで知らせるようにしてみましょう。

つまり、「投稿した従業員さんへのサポートが必要」であるというアラートということになります。

メール部分をつくる

では、ネガティブな感情が検知されたときにメール送信する部分をつくっていきます。

※なお、今回はいい機会ですのでMarkdown(≒簡単にHTML記述ができる書き方)を使ったメールを送信してみましょう。

以下のコマンドでMailableクラスをつくってください。

php artisan make:mail NegativeFeelingsDetected --markdown=emails.negative_feelings_detected

すると(Mailableクラスとテンプレートファイル)ファイルが作成されるので、中身を以下のように変更します。

app/Mail/NegativeFeelingsDetected.php

<?php

namespace App\Mail;

use App\Models\WorkReport;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;

class NegativeFeelingsDetected extends Mailable
{
    use Queueable, SerializesModels;

    private $work_report, $score;

    public function __construct(WorkReport $work_report, $score)
    {
        $this->work_report = $work_report;
        $this->score = $score;
    }

    public function build()
    {
        $to = 'admin@example.com';
        $from = 'no-reply@example.com';

        return $this
            ->to($to)
            ->from($from)
            ->markdown('emails.negative_feelings_detected')
            ->with([
                'work_report' => $this->work_report,
                'score' => $this->score
            ]);
    }
}

そして、メールテンプレート(Markdown)です。

resources/views/emails/negative_feelings_detected.blade.php

@component('mail::message')
# ネガティブな感情を検知

日報からネガティブな感情が検知されました。内容をチェックし、必要であればサポートを実施してください。

## 従業員

```
{{ $work_report->user->name }}
```

## 日報の内容

```
{{ $work_report->description }}
```

## ネガティブ指数(0 - 100|良い - 悪い)

```
{{ $score }}
```
@endcomponent

※なお、Markdownの書き方はGitHubがまとめてくれています。

送信部分をつくる

では、先ほどつくったメールを実際に送信する部分です。
これはすでに作成したモデルWorkReportSavedの中にコードを追加します。

app/Events/WorkReportSaved.php

// 省略

$scores = $result['SentimentScore'];

foreach ($scores as $type => $score) {

    // 省略

    if($type === 'Negative' && $score > 80) { // ネガティブな感情を検知した場合

        \Mail::send(new NegativeFeelingsDetected($work_report, $score));

    }

}

これで全て完了です!

テストしてみる

では実際にテストしてみましょう❗
ログインしてから「http://******/user/work_report/create」にアクセスします。

業務報告フォームが表示されました。

ポジティブな内容

では、はじめに以下のポジティブな内容を日報として保存してみましょう。

新システムのログイン機能(通常ログイン/ソーシャルログイン)の実装が予定より早く完了しました。
テストも完了しており、バグは発見されておりません。

また、株式会社山田の担当者様から「素晴らしいシステムです」とお褒めのメッセージをいただきました。

クレーム発生はありませんでした。

すると・・・・・・

まずwork_reportsテーブルデータが追加されていることが確認できました。
では、肝心の感情分析の方はどうでしょうか。

はい❗
うまくデータが追加されています。

Positiveが93点なので感情分析の結果としても正しいといっていいでしょう

ネガティブな内容

続いてネガティブな内容も試してみましょう。

新システムのログイン機能の開発取り掛かりましたが、ソーシャルログインの仕様が難しく、まだ実際の作業に入ることができませんでした。
(ちなみにアクセスキーは取得していますが、これであっているのかな?という状態です)

また、株式会社佐藤の担当者様からバグがあったので修正してほしいとの連絡がはいりました。

クレームは3件ありました。

結果はどうなるでしょうか。

はい❗
2つ目のデータが登録されています。

では、感情分析結果です。

こちらもきちんとデータが保存されましたが、Negative85となっているのでこの従業員さんは「ストレスを感じている」という判断になりました。

想定した動きをしています😊

そして、今回はネガティブな数値が設定された80よりも大きかったので通知メールが送信されているはずです。こちらもチェックしてみましょう。

すると・・・・・・

はい❗
通知メールが送信されていることを確認しました。

全て成功です😊👍✨

企業様へのご提案

今回は日報という形で従業員さんの感情分析を試みましたが、テキストデータさえあれば同じことが実現できます。

そのため、以下のような応用が考えられます。

  • 口コミの書き込みから感情を分析し、商品の改善に役立てる
  • メッセージサービス(例: Slack、ChatWork、LINEなど)やメールの内容から感情分析をし、チームの状態を把握する
  • お問い合わせ内容を感情分析し、よりネガティブなものはクレーム対応に回す
  • 入社面談が予定されている人のSNSで感情分析し、性格がポジティブなのかネガティブなのかを判断する

ぜひご興味がありましたらお問い合わせからご連絡ください。m(_ _)m

ちなみに:「ぱおん退職届」で試してみた

ふと、少し前に話題になった退職届のことが気になったのでAmazon Comprehendはどう判断するのかやってみました。

結果はこちら❗

うっすらPositiveだと判断されています😂
退職できて嬉しいという気持ちの現れでしょうか。。。

これを見ると、人間の感情って複雑だと感じますね。

参考にしたURL

おわりに

ということで今回はAmazon Comprehendを使って感情分析を試してみました。

なお、精度に関してですが、私の個人的なLINEのメッセージで試してみたところ、ほとんどが納得できる判別結果となっていましたが、ひとつだけ「うーん、それはネガティブには言ってないな…💦」というものがあったのも事実です。

また、婉曲的な表現や、実際の気持ちをテキストにしていない場合は流石にAmazonの機械学習でも判断することはできないので、テクノロジーに頼るだけでなく、「日報を正直に書くことができる」というような職場環境も必要になってくるといっていいでしょう。(パワハラ上司がいたら絶対にウソが多くなりますよね)

ということは、まだまだデータだけで何かを判断するのは難しいのかもしれません。

では、テストならAWSにログインするだけで感情分析を試すことができるので、皆さんもチャレンジしてみてくださいね。

ではでは〜❗

「そう考えると、パワハラ上司・検知システム
とか需要あるのかな??」

開発のご依頼お待ちしております 😊✨ お問い合わせ
また、こちらもお待ちしております。
  • 実案件の開発サポート: 詳細
  • ツイッターのフォロー: 詳細
どうぞよろしくお願いいたします!
このエントリーをはてなブックマークに追加       follow us in feedly