自然言語処理を用いて気になるあの子との距離を定量的に測ってみた

# はじめに
## 経緯
本記事は[【Qiita x COTOHA APIプレゼント企画】](https://zine.qiita.com/event/collaboration-cotoha-api/?utm_source=qiita&utm_medium=top_banner)への挑戦もかねて、私が昔作りたい考えていたシステムをCOTOHAを用いて作成したため、紹介させていただく記事となっています。
コードを随所に記載していますが難しいことは特に何もないため、さらっと流し読んでいただければ十分だと思います。

全ソースコード:[githubで公開しています。](https://github.com/SoraY677/HeartDistance)

# COTOHAとは
COTOHAはNTTCommunications様の作成した自然言語処理のできるWebAPIです。一口に自然言語処理といってもめちゃくちゃたくさんの機能に分かれており、様々な用途に利用することができます。
以下が、使える機能一覧です。

- 構文解析
- 固有表現抽出
- 固有名詞(企業名)補正
- 照応解析
- キーワード抽出
- 類似度算出
- 文タイプ判定
- ユーザ属性推定(β)
- 言い淀み除去(β)
- 音声認識誤り検知(β)
- 感情分析
- 音声認識
- 音声合成
- 要約(β)

すごい多いですよね。自然言語処理に関することならほとんどなんでもござれという感じです。

その中で、今回私が目を付けたのは**感情分析の機能**です。この機能のすごいところは文章の中の感情にまつわる言葉の抽出に加え文章全体のポジティブ/ネガティブ/普通さに点数をつけてくれるのです。面白い。

そこで私は点数化してくれる機能を利用して「気になるあの子との心の距離を測れないか」と考えたわけです。

# 気になるあの子との距離を知りたい...

皆さん、気になるあの子が自分のことをどう思っているかを考えてしまいますよね？そしてこう考えたことがありませんか？**恋愛を点数化できたらいいのに...**。
点数化できれば今の自分がとりうる行動は変わってくるでしょう。自分が見直すべき点が見えてきて、気になるあの子との距離を近づけることができるはずです。

そんな夢をかなえるシステムを作りました。

[作成したシステムは公開しているので良ければ遊んでみてください！](https://soray677.github.io/HeartDistance/)
**※[COTOHAの登録が必要になります。](https://api.ce-cotoha.com/contents/index.html)**
**※Google Chromeのみの対応となります。**

# システムの紹介
ここからは私の作成したシステムについて説明していきます。

作成したシステムは

 1. 気になるあの子との会話を録音する。
 2. 録音した会話を診断する(テキスト解析)。
 3. 心の距離が数値化され表示される。

という段階を踏むことで会話から心の距離を測ります。

## STEP1 録音する

まずは気になっている子との会話を録音します。(**録音の際はしっかり許可を取りましょう。**)
録音には「Speech Recognition API」を用いており、ウェブ上でできるようにしています。

```js:録音部分のコード
const SpeechRecognition = webkitSpeechRecognition || SpeechRecognition;
const recognition = new SpeechRecognition();

recognition.lang = "ja-JP"; //言語指定：日本語
recognition.interimResults = true; //認識途中のデータ取得
recognition.continuous = true; //認識し続ける

let talkResult = ""; //既に終わった会話のまとめ
let talkCount = 0;
recognition.onresult = event => {
	let currentTalk = "";
	for (
		let eventi = event.resultIndex;
		eventi < event.results.length;
		eventi++
	) {
		//DOM用に会話をパース
		let talkParse = '<p class="talk-section ';
		talkParse += talkCount % 2 == 0 ? "left" : "right"; //偶数は左寄り、奇数は右寄りにする
		talkParse += '">';
		talkParse += event.results[eventi][0].transcript + "</p>"; //DOM向けにパースしたもの

		if (event.results[eventi].isFinal) {
			//会話が終了していたら会話を記録
			recText += event.results[eventi][0].transcript + "。";
			talkResult = talkParse + talkResult;
			talkCount++;
		} else {
			//途中なら会話の経過を変数に記録
			currentTalk = talkParse;
		}
	}
	//DOMに反映
	document.getElementById("rec_text").innerHTML = currentTalk + talkResult;
};
```

参考にさせていただいたサイト：
[Webページでブラウザの音声認識機能を使おう - Web Speech API Speech Recognition](https://qiita.com/hmmrjn/items/4b77a86030ed0071f548)

## STEP2 解析する
解析してもらうにあたり、COTOHAでは解析を要求するためのトークンの取得が必要になります。なので順序としては、
COTOHAに登録した際のClientIDとClientSecretをもとにトークンを取得。
→録音データとトークンをCOTOHAにリクエストして、感情の分類と点数をつける。
となっています。

ちなみにCOTOHAにリクエストする際には、会話の分を「。」で結んでひとつの文章としています。

```js:トークンの取得と解析部分 
//Tokenを取得するためのHTTPリクエスト
const apikeyXhr = new XMLHttpRequest();

function requestAPIKey() {
	apikeyXhr.open("POST", "https://api.ce-cotoha.com/v1/oauth/accesstokens");
	apikeyXhr.setRequestHeader("Content-Type", "application/json");
	let requestJson = {};
	requestJson.grantType = "client_credentials";
	requestJson.clientId = clientId.value;
	requestJson.clientSecret = clientSecret.value;
	apikeyXhr.send(JSON.stringify(requestJson));
}

apikeyXhr.onreadystatechange = function() {
	//レスポンス取得完了後
	if (this.readyState == 4) {
		const responseResult = JSON.parse(apikeyXhr.responseText || false);
		{
			//responseが正常ならば
			if (responseResult != false) {
				requestAnalisys(responseResult.access_token);
			}
		}
	}
};

//感情分析するためのHTTPリクエスト
const analisysXhr = new XMLHttpRequest();

function requestAnalisys(token) {
	analisysXhr.open(
		"POST",
		"https://api.ce-cotoha.com/api/dev/nlp/v1/sentiment"
	);
	analisysXhr.setRequestHeader(
		"Content-Type",
		"application/json;charset=UTF-8"
	);
	analisysXhr.setRequestHeader("Authorization", "Bearer " + token);
	let requestJson = {};
	requestJson.sentence = recText;
	analisysXhr.send(JSON.stringify(requestJson));
}

//レスポンスが返ってきたら
analisysXhr.onreadystatechange = function() {
	if (this.readyState == 4) {
		const responseResult = JSON.parse(analisysXhr.responseText || false);
		if (responseResult != false) {
			console.log(responseResult);
			createDiagResult(
				responseResult.result.sentiment,
				responseResult.result.score,
				responseResult.result.emotional_phrase
			);
		}
	}
};
```

## STEP3 解析結果の表示
最後に解析結果を表示します。
解析結果で表示するのは、COTOHAからのレスポンスとして帰ってきた会話全体のPositive/Negative/Neutralのいずれかの分類・点数・会話内に存在する感情に関わる言葉・アドバイスです。
解析結果におけるアドバイスは私の独断で決定しています。

```js:会話の分析結果の表示部分
function createDiagResult(category, point, phraseArray) {
	//各点数と感情の分類に適したアドバイス
	//感情の分類[Positive/Negative/Neutral]をキーとする
	//点数による分類は20点ごとの5段階(各[0,20,40,60,80]以上かどうか)
	let advice = {
		Positive: [
			"会話に多少盛り上がりが見えます。ですが、まだまだ距離は遠いようです。もっと自分に素直に発言してみるといいかもしれません",
			"会話に盛り上がりがしっかりと見れます！気になるあの子と距離は近いです。あと一歩！",
			"会話になかなかの盛り上がりが見えます。気になるあの子との距離がかなり近いです！",
			"かなり会話が盛り上がっており、かなり近い距離にあります！素晴らしい！",
			"会話の盛り上がりが素晴らしいです！心の距離はもはやとなり合わせといっても過言ではないでしょう！"
		],
		Neutral: [
			"会話にあまり盛り上がりが見えず、業務連絡のようになってしまっていますね...。",
			"会話に心がないように思えます。もっと相手が楽しくなるような話題を積極的に振ってみましょう！",
			"会話への感情移入があまりなく、とても残念です。相手に合わせて話題を振ってみましょう。",
			"もっと自分を開放していいように感じます。趣味などの話を恐れずにだして相手の警戒心を解くことを心がけましょう。",
			"会話の内容が普通すぎて、測定が不可能です...。"
		],
		Negative: [
			"ちょっと改善したほうがいいかもしれないです。会話においてマイナスとなる部分が少し見られます。",
			"このままだと嫌われかねないです！もっと相手の事を考えて発言してみましょう。",
			"もしかしたら相手に嫌われているかもしれません。距離を自分から少しとってみてはいかがでしょうか。",
			"むしろ嫌われに行ってるのではと疑っているほど会話からマイナスの感情が伺えました。",
			"率直に申し上げますと...気になるあの子はあきらめた方がいいかもしれません...。"
		]
	};

	let pi = 4; //5段階目から始まる(配列の関係により5-1である4)
	for (let pointi = 1.0; pointi > 0.0; pointi -= 0.2) {
		//解析結果のpointが分類に当てはまれば、点数による分類添え字(pi)を決定
		//ダメなら段階を一つ下げる
		if (point > pointi) {
			break;
		}
		pi--;
	}

	// カテゴリー分けと点数から結果の画像と分類を決定
	let imgNum = null;
	let parseCategory = "";
	switch (category) {
		case "Positive":
			parseCategory = "いい感じ！";
			if (point >= 0.8) {
				imgNum = 1;
			} else if (point >= 0.6) {
				imgNum = 2;
			} else {
				imgNum = 3;
			}
			break;
		case "Neutral":
			parseCategory = "微妙かな";
			imgNum = 4;
			break;
		case "Negative":
			parseCategory = "うーん...";
			imgNum = 5;
			break;
	}

	//感情にまつわる配列を表示用にパース
	let parsePhraseArray = [];
	for (let pi = 0; pi < phraseArray.length; pi++) {
		parsePhraseArray.push(phraseArray[pi].form);
	}

	/*アドバイスをDOMに反映*/
	//
	document.getElementById("result_img").src = "img/grade/" + imgNum + ".png";
	document.getElementById("result_category").innerText = parseCategory;
	document.getElementById("result_point").innerText =
		parseCategory + "度：" + Math.round(point * 100) + "点";
	document.getElementById("result_advice").innerText = advice[category][pi];
	document.getElementById("result_phrase").innerText =
		"抽出された感情にまつわる単語:" + parsePhraseArray.join(" , ");
	//デフォルト文を消し、結果文を表示
	document.getElementById("result_default").classList.add("exit-erase");
	document.getElementById("result_response").classList.remove("exit-erase");
}
```

# 実際に遊んでみた。

実際に僕の友人Y君に協力してもらい、システムで遊んでみました。

シチュエーションとして会話が、
- 盛り上がったもの
- 盛り上がりの大きくないもの
- 盛り上がらなかったもの
をそれぞれを想定しています。

以下がその結果です。

## 会話が盛り上がったという想定の場合

会話の盛り上がりが高いことを想定した場合です。COTOHAからのレスポンスとして分類がPositiveであり、得点としても高いものを期待しました。
### 会話内容
![1.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/405090/8aaa9b43-b18a-482d-2ed0-d7ce5341fcdf.png)


### 結果
![12.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/405090/712193d9-b286-cac7-f9e3-88fb259aaea2.png)

なかなか想定通り動いた気がしますかね？感情的に好きなことについて互いに話し合っている会話であるため、感情に関する単語も多いですね。

## 会話の盛り上がりが大きくない場合

会話の盛り上がりがそこまで大きくなく、COTOHAからのレスポンスである分類としてNeutralが返ってくることを期待しました。

### 会話内容
![2.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/405090/d5770b28-07d3-75cd-3b5c-5640bd0bc922.png)


### 結果
![データ22.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/405090/f4d67e43-14db-0503-9c17-50a534721f96.png)

Neutralを想定しましたがPositiveに分類されましたね。「面白い」という単語に焦点があてられたようです。
どちらかが一方的に好きなことに関して感情的になっているとPositiveに分類されることが考えるので、今後の課題ではありますね...


## 会話が盛り上がらなかった場合

気になるあの子が望んでいるような会話ができておらず、分類がNegativeとレスポンスされる想定をした場合です。

### 会話内容
![キャプチャ3.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/405090/363d006a-6393-9a5e-d83f-b1c35b2dbb44.png)

### 結果
![キャプチャ32.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/405090/5509caf5-87a7-4f97-33ce-3ba372e4fbf6.png)

想定通りの結果が返ってきました！**てっきり「好き」という単語に反応するかと思ったのですが、「あんまり好きじゃない」という単語に反応できているのは素晴らしいですね！**

# まとめ
会話から心の距離を定量的に測ることに挑戦しましたが、意外と想定した通りの動きをしてくれていることが多かったので図ることができたといってもいいのでは？
システムを使うことを口実に気なるあの子と会話することも...ぐへへ...

まぁ、私が気になるあの子と距離を縮められたかはまた別のお話ということで。


ここまで読んでいただきありがとうございました。
システムに対して「もっと良くしたい！」・「こういう改善案がある」という方はソースコードを公開しているので、ぜひ自分色に作りなおしてみてください！

締めとしては、みんないろんなAPI使って~~ふざけた~~世の中を盛り上げるシステム作りましょう！ってことです。

## 謝辞・参考

- システムの動作への協力：Y君
- システム制作への助力：Fさん
- システム内の画像提供：[いらすとや](https://www.irasutoya.com/) 様
- 参考：[Webページでブラウザの音声認識機能を使おう - Web Speech API Speech Recognition](https://qiita.com/hmmrjn/items/4b77a86030ed0071f548)

この場を借りてお礼申し上げます。本当にありがとうございます。