人工知能を作りたい③
こんにちは、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つの部分に分割するために使われます:
- 感情変化値(オプション):
(-?\d+)##
の部分 - 感情変化値のない場合:
?
で感情変化値がオプションであることを示す - 残りの文字列:
(.*)
このパターンは、感情変化値があってもなくても対応できるように設計されています。例えば、「5##こんにちは
」 や 「こんにちは
」などの形式の文字列を解析できます。
__init__メソッド
1 def __init__(self, pattern, phrases):
2 self.init_modifypattern(pattern)
3 self.init_phrases(phrases)
このクラスは、ユーザーの入力パターンとそれに対応する応答を保持します。コンストラクタは、pattern
とphrases
という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)
コンストラクタで、名前、パターンファイル、レスポンダー、感情オブジェクトを初期化します。RandomResponder
とPatternResponder
を持ち、どちらかをランダムに選びます。
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の応答を表示します。感情の変化も表示されるようになっています。ユーザーが入力を停止するまで対話を続けます。
まとめ
このChatBotは、ユーザーの入力に対して、パターン辞書を用いた応答を生成するだけでなく、感情スコアを管理し、応答の内容に応じて感情の変化を反映します。これにより、より人間らしい対話が可能となります。
最後まで読んでいただきありがとうございました!