AI・機械学習

人工知能を作りたい③

palm

こんにちは、Palmです!

今回は、前回作成したChatBotに「Emotion」機能を追加して、感情を持ったChatBot「Mirai」をご紹介します。前回は、単なる応答の繰り返しでボットと話している感覚が強かったですが、今回は感情を数値で管理することで、より人間らしい自然な応答ができるようにしました。これにより、ChatBotとの会話がさらに楽しくなります!

合わせて読みたい
人工知能を作りたい②
人工知能を作りたい②

感情を持ったChatBotの仕組み

Miraiは、入力されたテキストに基づいて応答を生成し、その際に感情スコアを変化させます。感情スコアは、応答内容に応じて増減し、ユーザーに現在の感情状態を伝えます。例えば、ポジティブな応答には感情スコアが上がり、ネガティブな応答には感情スコアが下がる仕組みです。

以下は、Miraiの主な機能を実装するPythonコードです。

1import os
2import re
3import random
4
5class PatternItem:
6    SEPARATOR = '^((-?\d+)##)?(.*)$'
7
8    def __init__(self, pattern, phrases):
9        self.init_modifypattern(pattern)
10        self.init_phrases(phrases)
11
12    def init_modifypattern(self, pattern):
13        m = re.findall(PatternItem.SEPARATOR, pattern)
14        self.modify = 0
15        if m[0][1]:
16            self.modify = int(m[0][1])
17        self.pattern = m[0][2]
18
19    def init_phrases(self, phrases):
20        self.phrases = []
21        dic = {}
22        for phrase in phrases.split('|'):
23            m = re.findall(PatternItem.SEPARATOR, phrase)
24            dic['need'] = 0
25            if m[0][1]:
26                dic['need'] = int(m[0][1])
27            dic['phrase'] = m[0][2]
28            self.phrases.append(dic.copy())
29
30    def match(self, text):
31        return re.search(self.pattern, text)
32
33    def choice(self, mood):
34        choices = [p['phrase'] for p in self.phrases if self.suitable(p['need'], mood)]
35        if choices:
36            return random.choice(choices), self.modify
37        return None, 0
38
39    def suitable(self, need, mood):
40        if need == 0:
41            return True
42        elif need > 0:
43            return mood > need
44        else:
45            return mood < need
46
47class Dictionary:
48    def __init__(self, pattern_file):
49        self.patterns = self.load_patterns(pattern_file)
50
51    def load_patterns(self, pattern_file):
52        patterns = []
53        with open(pattern_file, 'r', encoding='utf-8') as f:
54            for line in f:
55                line = line.strip()
56                if not line or '\t' not in line:
57                    continue
58                pattern, responses = line.split('\t', 1)
59                patterns.append(PatternItem(pattern, responses))
60        return patterns
61
62    def response(self, input_text):
63        for pattern in self.patterns:
64            if pattern.match(input_text):
65                response, emotion_change = pattern.choice(0)
66                if response:
67                    return response, emotion_change
68        return None, 0
69
70class PatternResponder:
71    def __init__(self, pattern_file):
72        self.patterns = self.load_patterns(pattern_file)
73
74    def load_patterns(self, pattern_file):
75        patterns = []
76        with open(pattern_file, 'r', encoding='utf-8') as f:
77            for line in f:
78                line = line.strip()
79                if not line or '\t' not in line:
80                    continue
81                pattern, responses = line.split('\t', 1)
82                response_list = []
83                for response in responses.split('|'):
84                    emotion_change, response_text = response.split('##', 1)
85                    response_list.append((int(emotion_change), response_text))
86                patterns.append((re.compile(pattern), response_list))
87        return patterns
88
89    def response(self, input_text):
90        for pattern, responses in self.patterns:
91            if pattern.search(input_text):
92                emotion_change, response = random.choice(responses)
93                return response, emotion_change
94        return None, 0
95
96class RandomResponder:
97    def response(self, input_text):
98        responses = [
99            "そうなんですね。",
100            "なるほど。",
101            "わかりました。",
102            "そうですか。",
103            "おっしゃる通りです。",
104            "その通りですね。",
105            "いいですね。"
106        ]
107        return random.choice(responses), 0
108
109class Emotion:
110    def __init__(self):
111        self.emotion_score = 0
112
113    def update_emotion(self, change):
114        self.emotion_score += change
115        self.show_emotion()
116
117    def show_emotion(self):
118        if self.emotion_score > 5:
119            print(f"現在の感情スコア: {self.emotion_score} -> とても良い気分です!")
120        elif self.emotion_score > 0:
121            print(f"現在の感情スコア: {self.emotion_score} -> 良い気分です。")
122        elif self.emotion_score == 0:
123            print(f"現在の感情スコア: {self.emotion_score} -> 普通の気分です。")
124        elif self.emotion_score > -5:
125            print(f"現在の感情スコア: {self.emotion_score} -> 少し悪い気分です。")
126        else:
127            print(f"現在の感情スコア: {self.emotion_score} -> とても悪い気分です。")
128
129class Mirai:
130    def __init__(self, name, pattern_file):
131        self.name = name
132        self.random_responder = RandomResponder()
133        self.pattern_responder = PatternResponder(pattern_file)
134        self.emotion = Emotion()
135        self.dictionary = Dictionary(pattern_file)
136        self.responders = [self.random_responder, self.pattern_responder]
137        self.current_responder = random.choice(self.responders)
138
139    def dialogue(self, input_text):
140        # 通常の応答
141        self.current_responder = random.choice(self.responders)
142        response, emotion_change = self.current_responder.response(input_text)
143        self.emotion.update_emotion(emotion_change)
144        #print(f"現在の感情スコア: {self.emotion.emotion_score}")
145        return response
146
147    def get_name(self):
148        return self.name
149
150def prompt(obj):
151    return obj.get_name() + '> '
152
153print("感情の変化を使った Miraiとの会話を始めます。")
154mirai = Mirai('Mirai', 'dics/pattern.txt')
155
156while True:
157    inputs = input(' > ')
158    if not inputs:
159        print('会話を終了します')
160        break
161    response = mirai.dialogue(inputs)
162    print(prompt(mirai), response)

パターンファイル(dics/pattern.txt)の内容

pattern.txtには、ユーザーの入力と、それに対する応答が記述されています。さらに、応答ごとに感情の変化が数値で表現されています。
その一部の例です!

1ありがとう|感謝	5##どういたしまして!|5##こちらこそありがとう!|5##嬉しいな!
2疲れた	-2##ゆっくり休んでね。|-2##無理しないでね。|-2##少し休憩しようよ。
3おやすみ	0##おやすみなさい、良い夢を!|0##おやすみ、また明日ね!
4こんにちは	0##こんにちは!今日はどんな日?|0##やあ、元気?
5寂しい	-5##いつでも話を聞くよ。|-5##大丈夫、ここにいるからね。|-5##一緒にいてあげるよ。
6お疲れ様	3##お疲れ様!今日も頑張ったね。|3##よく頑張ったね!
7何してるの	0##いろいろやってるよ。|0##君は何してるの?
8さようなら	-3##さようなら、またね!|-3##元気でね!


解説

このChatBotは、以下の特徴を持っています。

PatternItemクラス

1class PatternItem:

このクラスは、正規表現パターンと、それに対応する応答フレーズと感情変化を保持します。パターンがユーザーの入力と一致すると、適切な応答を選びます。

正規表現パターン

1SEPARATOR = '^((-?\d+)##)?(.*)$'

正規表現のパターンを定義しています。このパターンは、文字列を3つの部分に分割するために使われます:

  1. 感情変化値(オプション): (-?\d+)## の部分
  2. 感情変化値のない場合: ? で感情変化値がオプションであることを示す
  3. 残りの文字列: (.*)

このパターンは、感情変化値があってもなくても対応できるように設計されています。例えば、「5##こんにちは」 や 「こんにちは」などの形式の文字列を解析できます。

__init__メソッド

1    def __init__(self, pattern, phrases):
2        self.init_modifypattern(pattern)
3        self.init_phrases(phrases)

このクラスは、ユーザーの入力パターンとそれに対応する応答を保持します。コンストラクタは、patternphrasesという2つの引数を受け取り、それぞれの初期化を行います。

init_modifypatternメソッド

1    def init_modifypattern(self, pattern):
2        m = re.findall(PatternItem.SEPARATOR, pattern)
3        self.modify = 0
4        if m[0][1]:
5            self.modify = int(m[0][1])
6        self.pattern = m[0][2]

このメソッドは、パターン文字列を解析し、感情変化値と正規表現パターンに分けて保持します。PatternItem.SEPARATORを使って正規表現でパターン文字列を分解します。

init_phrasesメソッド

1    def init_phrases(self, phrases):
2        self.phrases = []
3        dic = {}
4        for phrase in phrases.split('|'):
5            m = re.findall(PatternItem.SEPARATOR, phrase)
6            dic['need'] = 0
7            if m[0][1]:
8                dic['need'] = int(m[0][1])
9            dic['phrase'] = m[0][2]
10            self.phrases.append(dic.copy())

応答フレーズを解析し、感情変化値とフレーズに分けて保持します。phrases|で分割し、それぞれのフレーズに対して正規表現を適用します。

matchメソッド

1    def match(self, text):
2        return re.search(self.pattern, text)

このメソッドは、入力テキストが正規表現パターンに一致するかをチェックします。re.searchを使って一致を確認します。

choiceメソッド

1    def choice(self, mood):
2        choices = [p['phrase'] for p in self.phrases if self.suitable(p['need'], mood)]
3        if choices:
4            return random.choice(choices), self.modify
5        return None, 0

現在の感情スコアに適した応答フレーズを選びます。一致するフレーズがあればランダムに選択し、感情変化値と共に返します。

suitableメソッド

1    def suitable(self, need, mood):
2        if need == 0:
3            return True
4        elif need > 0:
5            return mood > need
6        else:
7            return mood < need

フレーズの必要感情スコアと現在の感情スコアを比較し、適しているかどうかを判断します。このメソッドで、必要な感情スコアと現在の感情スコアを比較し、条件を満たす場合のみ応答フレーズを選びます。

Dictionaryクラス

1class Dictionary:

ユーザーの入力に対する応答と感情変化を取得します。

__init__メソッド

1    def __init__(self, pattern_file):
2        self.patterns = self.load_patterns(pattern_file)

コンストラクタで、パターンファイルを読み込みます。load_patternsメソッドを呼び出して、パターンを読み込みます。

load_patternsメソッド

1    def load_patterns(self, pattern_file):
2        patterns = []
3        with open(pattern_file, 'r', encoding='utf-8') as f:
4            for line in f:
5                line = line.strip()
6                if not line or '\t' not in line:
7                    continue
8                pattern, responses = line.split('\t', 1)
9                patterns.append(PatternItem(pattern, responses))
10        return patterns

このメソッドは、パターンファイルから各行を読み込み、PatternItemオブジェクトとしてリストに保持します。ファイルを開き、各行を読み取り、パターンと応答フレーズに分割してPatternItemに渡します。

responseメソッド

1    def response(self, input_text):
2        for pattern in self.patterns:
3            if pattern.match(input_text):
4                response, emotion_change = pattern.choice(0)
5                if response:
6                    return response, emotion_change
7        return None, 0

ユーザーの入力に対して一致するパターンを検索し、適切な応答と感情変化を返します。パターンリストを順番にチェックし、一致するパターンがあればその応答と感情変化値を返します。

PatternResponderクラス

1class PatternResponder:

パターンを元に応答を生成します。

__init__メソッド

1    def __init__(self, pattern_file):
2        self.patterns = self.load_patterns(pattern_file)

コンストラクタで、パターンファイルを読み込みます。load_patternsメソッドを呼び出して、パターンを読み込みます。

load_patternsメソッド

1    def load_patterns(self, pattern_file):
2        patterns = []
3        with open(pattern_file, 'r', encoding='utf-8') as f:
4            for line in f:
5                line = line.strip()
6                if not line or '\t' not in line:
7                    continue
8                pattern, responses = line.split('\t', 1)
9                response_list = []
10                for response in responses.split('|'):
11                    emotion_change, response_text = response.split('##', 1)
12                    response_list.append((int(emotion_change), response_text))
13                patterns.append((re.compile(pattern), response_list))
14        return patterns

パターンファイルから各行を読み込み、正規表現パターンと応答リストとして保持します。ファイルを開き、各行を読み取り、パターンと応答フレーズに分割して保持します。

responseメソッド

1    def response(self, input_text):
2        for pattern, responses in self.patterns:
3            if pattern.search(input_text):
4                emotion_change, response = random.choice(responses)
5                return response, emotion_change
6        return None, 0

ユーザーの入力をパターンと照合し、一致する場合に適切な応答と感情変化を返します。各パターンを順番にチェックし、一致するパターンがあればその応答と感情変化値を返します。

RandomResponderクラス

1class RandomResponder:

ランダムな定型応答を返します。

responseメソッド

1    def response(self, input_text):
2        responses = [
3            "そうなんですね。",
4            "なるほど。",
5            "わかりました。",
6            "そうですか。",
7            "おっしゃる通りです。",
8            "その通りですね。",
9            "いいですね。"
10        ]
11        return random.choice(responses), 0

定型文のリストからランダムに応答を選んで返します。感情変化はないため、感情変化値は0です。

Emotionクラス

1class Emotion:

感情スコアを保持し、更新する機能を持ちます。感情スコアに応じてメッセージを表示します。

__init__メソッド

1    def __init__(self):
2        self.emotion_score = 0

初期化時に感情スコアを0に設定します。

update_emotionメソッド

1    def update_emotion(self, change):
2        self.emotion_score += change
3        self.show_emotion()

感情スコアを変化させ、変化後の感情スコアを表示します。

show_emotionメソッド

1    def show_emotion(self):
2        if self.emotion_score > 5:
3            print(f"現在の感情スコア: {self.emotion_score} -> とても良い気分です!")
4        elif self.emotion_score > 0:
5            print(f"現在の感情スコア: {self.emotion_score} -> 良い気分です。")
6        elif self.emotion_score == 0:
7            print(f"現在の感情スコア: {self.emotion_score} -> 普通の気分です。")
8        elif self.emotion_score > -5:
9            print(f"現在の感情スコア: {self.emotion_score} -> 少し悪い気分です。")
10        else:
11            print(f"現在の感情スコア: {self.emotion_score} -> とても悪い気分です。")

現在の感情スコアに応じたメッセージを表示します。感情スコアの範囲に基づいてメッセージを変えます。

Miraiクラス

1class Mirai:

ChatBotの本体です。ユーザーの入力に対してPatternResponderまたはRandomResponderを使って応答します。感情スコアを更新します。

__init__メソッド

1   def __init__(self, name, pattern_file):
2        self.name = name
3        self.random_responder = RandomResponder()
4        self.pattern_responder = PatternResponder(pattern_file)
5        self.emotion = Emotion()
6        self.dictionary = Dictionary(pattern_file)
7        self.responders = [self.random_responder, self.pattern_responder]
8        self.current_responder = random.choice(self.responders)

コンストラクタで、名前、パターンファイル、レスポンダー、感情オブジェクトを初期化します。RandomResponderPatternResponderを持ち、どちらかをランダムに選びます。

dialogueメソッド

1    def dialogue(self, input_text):
2        self.current_responder = random.choice(self.responders)
3        response, emotion_change = self.current_responder.response(input_text)
4        self.emotion.update_emotion(emotion_change)
5        return response

ユーザーの入力に対してランダムにレスポンダーを選び、応答と感情変化を取得します。感情スコアを更新し、応答を返します。

get_nameメソッド

1    def get_name(self):
2        return self.name

ChatBotの名前を返します。これはプロンプト表示のために使われます。

プログラムの実行部分

prompt 関数

1def prompt(obj):
2    return obj.get_name() + '> '

prompt 関数は、Mirai インスタンスから名前と応答者の名前を取得して、プロンプト文字列を生成します。

この関数により、次のようなプロンプト文字列が生成されます。

1Mirai>
 

メインの処理(実行)

1print("感情の変化を使った Miraiとの会話を始めます。")
2mirai = Mirai('Mirai', 'dics/pattern.txt')
3
4while True:
5    inputs = input(' > ')
6    if not inputs:
7        print('会話を終了します')
8        break
9    response = mirai.dialogue(inputs)
10    print(prompt(mirai), response)

プログラムの実行部分です。ユーザーからの入力を受け取り、ChatBotの応答を表示します。感情の変化も表示されるようになっています。ユーザーが入力を停止するまで対話を続けます。

画像に alt 属性が指定されていません。ファイル名: -2024-05-26-16.05.02.png

まとめ

このChatBotは、ユーザーの入力に対して、パターン辞書を用いた応答を生成するだけでなく、感情スコアを管理し、応答の内容に応じて感情の変化を反映します。これにより、より人間らしい対話が可能となります。

最後まで読んでいただきありがとうございました!

スポンサーリンク
ABOUT ME
Palm
Palm
東京通信大学3年生
私はpalm(ぱるむ)です。お花や自然が大好きです。専門学校でWeb開発を学び、東京通信大学に編入しました。得意分野は、ウェブ開発(フロントエンド、バックエンド)や機械学習(自然言語、データ分析)です。趣味で色々デモ開発をしています。
記事URLをコピーしました