Python 智能项目(三)
八、面向客户服务的对话式人工智能聊天程序
会话聊天机器人最近产生了大量的炒作,因为它们在增强客户体验方面的作用。现代企业已经开始在几个不同的过程中使用聊天机器人的功能。由于会话式人工智能被广泛接受,通过互联网填写表格或发送信息的繁琐任务变得更加简单。对话聊天机器人的一个期望品质是它应该能够在当前环境中响应用户请求。对话聊天机器人系统中的玩家分别是用户和机器人。使用对话聊天机器人有很多优点,如下表所示:
- 个性化协助:为所有客户创造个性化体验可能是一项繁琐的任务,但不这样做可能会让企业遭受损失。对话聊天机器人是为每一位顾客提供个性化体验的便捷选择。
- 全天候支持 : 全天候使用客服代表费用昂贵。在非办公时间使用 chatbots 进行客户服务,无需雇佣额外的客户代表。
- 回答的一致性:聊天机器人提供的回答可能是一致的,而不同的客户服务代表对相同问题的回答可能会有所不同。这样,如果客户对客户服务代表提供的答案不满意,就无需多次致电。
- 耐心:虽然客服代表在接待客户时可能会失去耐心,但这对于聊天机器人来说是不可能的。
- 查询记录 : 聊天机器人查询记录的效率比人类客服代表要高得多。
聊天机器人不是最近才出现的东西,它的起源可以追溯到 20 世纪 50 年代。就在第二次世界大战之后,艾伦·图灵开发了图灵测试来观察一个人是否能区分人和机器。多年后的 1966 年,约瑟夫·韦森鲍姆开发了一些名为伊莱扎的软件,该软件模仿了一位心理治疗师的语言。工具仍可位于psych.fullerton.edu/mbirnbaum/p…。
聊天机器人可以执行各种各样的任务,下面的列表显示了其中一些任务,以强调它们的多功能性:
- 回答关于产品的问题
- 向客户提供建议
- 执行句子完成活动
- 对话聊天机器人
- 与客户谈判价格并参与投标
很多时候,企业很难弄清楚他们是否需要聊天机器人。企业是否需要聊天机器人可以通过图 8.1 中的流程图来确定:
Figure 8.1: Customer engagement model
作为本章的一部分,我们将涵盖以下主题:
- Chatbot 架构
- 聊天机器人的 LSTM 序列对序列模型
- 为推特支持的聊天机器人建立一个序列到序列的模型
技术要求
您将需要具备 Python 3、TensorFlow 和 Keras 的基本知识
本章代码文件可在 GitHub: https://GitHub . com/PacktPublishing/Intelligent-Projects-use-Python/tree/master/chapter 08上找到
查看以下视频,查看正在运行的代码: bit.ly/2G9AyoB
Chatbot 架构
chatbot 的核心组件是它的自然语言处理框架。聊天机器人通过一个通常被称为解析的过程,用自然语言处理呈现给它们的数据。解析后的用户输入随后被解释,并且根据用户想要的内容将适当的响应发送回用户,如从输入中解密的那样。chatbot 可能需要从知识库和历史交易数据存储中寻求帮助,以适当地处理用户的请求。
聊天机器人可以大致分为以下两类:
- 基于检索的模型:这些模型通常依赖于查找表或知识库从预定义的一组答案中选择一个答案。虽然这种方法看起来很幼稚,但生产中的大多数聊天机器人都是这种类型的。当然,在从查找表或知识库中选择最佳答案方面,可以有不同的复杂程度。
- 生成模型 : 生成模型动态生成响应,而不是采用基于查找的方法。它们大多是概率模型或基于机器学习的模型。直到最近,马尔可夫链大多被用作生成模型;然而,随着最近深度学习的成功,基于递归神经网络的方法得到了普及。一般来说,LSTM 版本的 RNNs 被用作聊天机器人的生成模型,因为它更擅长处理长序列。
基于检索的模型和生成模型都有各自的优缺点。由于基于检索的模型从一组固定的答案中回答,它们不能处理看不见的问题或没有适当的预定义响应的请求。生成模型更加复杂;他们可以理解用户输入中的实体,并生成类似人类的响应。然而,它们更难训练,并且通常需要更多的数据来训练。他们也容易犯语法错误,这是基于检索的模型所不能犯的。
使用 LSTM 的序列到序列模型
序列到序列模型架构非常适合捕获客户输入的上下文,然后基于此生成适当的响应。图 8.2 显示了一个序列到序列的模型框架,它可以像聊天机器人一样回答问题:
Figure 8.2: Sequence-to-sequence model using an LSTM
从上图(图 8.2 )我们可以看到,编码器 LSTM 将输入的单词序列编码成隐藏状态向量,和细胞状态向量
。向量
和
是 LSTM 编码器最后一步的隐藏和单元状态。它们将基本上捕捉整个输入句子的上下文。
和
形式的编码信息随后被馈送到解码器 LSTM 作为其初始隐藏和单元状态。每一步中的解码器 LSTM 试图根据当前单词预测下一个单词。这意味着,解码器 LSTM 每一步的输入都是当前字。
为了预测第一个单词,LSTM 将被提供一个虚拟的开始关键字 < BOS > ,它代表句子的开始。同样的, < EOS > dummy 关键字代表句子的结尾,一旦预测到这一点,输出生成就应该停止。
在为每个目标单词训练序列到序列模型的过程中,我们先验地知道是输入到解码器 LSTM 的前一个单词。然而,在推断过程中,我们不会有这些目标词,因此我们必须将前一步作为输入。
*# 构建序列对序列模型
我们将用于构建聊天机器人的序列到序列模型的架构将对先前在*图 8.2 中说明的基本序列到序列架构进行轻微修改。*修改后的架构如下图所示(图 8.3 ):
Figure 8.3: Sequence-to-sequence model
不是将编码器最后一步的隐藏状态和单元状态
馈送到解码器 LSTM 的初始隐藏和单元状态,而是在解码器的每个输入步骤馈送隐藏状态
。要在任意一步 t 预测目标词wtT8】,输入为前一个目标词, w t-1 ,任意一步, t-1 ,隐藏状态
。
推特上的客户支持
现在我们已经对如何使用递归神经网络构建聊天机器人有了一些想法,我们将使用 20 个大品牌对客户发布的推文的客户服务响应来构建聊天机器人。数据集twcs.zip可位于https://www . kaggle . com/thought vector/customer-support-on-Twitter。 每条推文由tweet_id标识,推文内容在text字段。客户发布的推文可以通过in_response_to_tweet_id字段识别。这应该包含客户推文的空值。对于客户服务推文,此in_response_to_tweet_id 字段应指向此推文指向的客户tweet_id。
为训练聊天机器人创建数据
要提取客户发布的所有入站推文,我们需要提取所有in_response_to_tweet_id 字段为null的推文。包含客服代表回复的出站文件可以通过推文过滤提取,其中in_response_to_tweet_id 字段不为空。一旦我们有了入站和出站文件,我们需要将它们合并在入站文件的tweet_id和出站文件的in_response_to_tweet_id 上。这将为我们提供客户在中的推文和客户服务代表在中的推文作为回应。数据创建功能可以编码如下:
def process_data(self,path):
data = pd.read_csv(path)
if self.mode == 'train':
data = pd.read_csv(path)
data['in_response_to_tweet_id'].fillna(-12345,inplace=True)
tweets_in = data[data['in_response_to_tweet_id'] == -12345]
tweets_in_out =
tweets_in.merge(data,left_on=['tweet_id'],right_on=
['in_response_to_tweet_id'])
return tweets_in_out[:self.num_train_records]
elif self.mode == 'inference':
return data
将文本标记为单词索引
这些推文需要被标记化并转换成数字,然后才能输入神经网络。计数向量器用于确定构成聊天机器人词汇空间的固定数量的频繁单词。我们还引入了三个新的标记,分别表示一个句子的开始(START)、一个句子的结束(PAD)以及任何未知的单词(UNK)。此处显示了标记推文的功能,以供参考:
def tokenize_text(self,in_text,out_text):
count_vectorizer = CountVectorizer(tokenizer=casual_tokenize, max_features=self.max_vocab_size - 3)
count_vectorizer.fit(in_text + out_text)
self.analyzer = count_vectorizer.build_analyzer()
self.vocabulary =
{key_: value_ + 3 for key_,value_ in count_vectorizer.vocabulary_.items()}
self.vocabulary['UNK'] = self.UNK
self.vocabulary['PAD'] = self.PAD
self.vocabulary['START'] = self.START
self.reverse_vocabulary =
{value_: key_ for key_, value_ in self.vocabulary.items()}
joblib.dump(self.vocabulary,self.outpath + 'vocabulary.pkl')
joblib.dump(self.reverse_vocabulary,self.outpath + 'reverse_vocabulary.pkl')
joblib.dump(count_vectorizer,self.outpath + 'count_vectorizer.pkl')
#pickle.dump(self.count_vectorizer,open(self.outpath +
'count_vectorizer.pkl',"wb"))
现在,需要将标记化的单词转换为单词索引,以便它们可以被馈送到递归神经网络,如以下代码所示:
def words_to_indices(self,sent):
word_indices =
[self.vocabulary.get(token,self.UNK) for token in self.analyzer(sent)] +
[self.PAD]*self.max_seq_len
word_indices = word_indices[:self.max_seq_len]
return word_indices
我们还希望将递归神经网络预测的单词索引转换成单词以形成句子。该功能可以编码如下:
def indices_to_words(self,indices):
return ' '.join(self.reverse_vocabulary[id] for id in indices if id != self.PAD).strip()
替换匿名屏幕名称
在我们对推文进行标记化之前,将推文中匿名化的屏幕名称替换为通用名称可能是值得的,这样响应会更好地概括。该功能可以编码如下:
def replace_anonymized_names(self,data):
def replace_name(match):
cname = match.group(2).lower()
if not cname.isnumeric():
return match.group(1) + match.group(2)
return '@__cname__'
re_pattern = re.compile('(\W@|^@)([a-zA-Z0-9_]+)')
if self.mode == 'train':
in_text = data['text_x'].apply(lambda txt:re_pattern.sub(replace_name,txt))
out_text = data['text_y'].apply(lambda
txt:re_pattern.sub(replace_name,txt))
return list(in_text.values),list(out_text.values)
else:
return map(lambda x:re_pattern.sub(replace_name,x),data)
定义模型
RNN 的 LSTM 版本被用来建立序列到序列模型。这是因为 LSTMs 在记忆长文本序列中的长期依赖关系方面要高效得多。LSTM 架构中的三个门使它能够有效地记住长期序列。一个基本的 RNN 无法记住长期的依赖关系,因为与其架构相关的渐变问题正在消失。
在这个模型中,我们使用了两个 LSTMs。第一个 LSTM 将把输入的推文编码成一个上下文向量。这个上下文向量只不过是编码器 LSTM 的最后一个隐藏状态, n 是隐藏状态向量的维数。输入的推文
作为单词索引序列输入到编码器 LSTM,其中 k 是输入推文的序列长度。在输入 LSTM 之前,这些单词索引被映射到单词嵌入。单词嵌入被封装在嵌入矩阵中, *[W ∈ R m x N ,*其中 N 表示词汇中单词的数量。
第二个 LSTM 作为解码器工作。它试图将编码器 LSTM 创建的上下文向量解码成有意义的响应。作为该方法的一部分,我们在每个时间步长将上下文向量与前一个单词一起馈送,以生成当前单词。在第一个时间步骤中,我们没有任何先前的单词来限制 LSTM,因此我们使用代理
START单词来开始从解码器 LSTM 生成单词序列的过程。在推理过程中,我们如何在当前时间步长输入前一个单词不同于我们在训练过程中使用的方法。在训练中,既然我们知道了前面的单词先验的,那么在每一个时间步,我们相应的喂养它们没有任何问题。然而,在推断过程中,由于我们在当前时间步长没有实际的前一个单词,所以前一个时间步长的预测单词被馈送。在最终的大软最大值 N 之前,每个时间步长 t 的隐藏状态通过几个完全连接的层传送。在这个 softmax 层中获得最大概率的单词是时间步长的预测单词。该字然后被馈送到下一步骤的输入端,即解码器 LSTM 的步骤 t + 1 。
Keras 中的TimeDistributed函数允许在解码器 LSTM 的每个时间步长有效实现预测,如以下代码所示:
def define_model(self):
# Embedding Layer
embedding = Embedding(
output_dim=self.embedding_dim,
input_dim=self.max_vocab_size,
input_length=self.max_seq_len,
name='embedding',
)
# Encoder input
encoder_input = Input(
shape=(self.max_seq_len,),
dtype='int32',
name='encoder_input',
)
embedded_input = embedding(encoder_input)
encoder_rnn = LSTM(
self.hidden_state_dim,
name='encoder',
dropout=self.dropout
)
# Context is repeated to the max sequence length so that the same context
# can be feed at each step of decoder
context = RepeatVector(self.max_seq_len)(encoder_rnn(embedded_input))
# Decoder
last_word_input = Input(
shape=(self.max_seq_len,),
dtype='int32',
name='last_word_input',
)
embedded_last_word = embedding(last_word_input)
# Combines the context produced by the encoder and the last word uttered as
inputs
# to the decoder.
decoder_input = concatenate([embedded_last_word, context],axis=2)
# return_sequences causes LSTM to produce one output per timestep instead of
one at the
# end of the intput, which is important for sequence producing models.
decoder_rnn = LSTM(
self.hidden_state_dim,
name='decoder',
return_sequences=True,
dropout=self.dropout
)
decoder_output = decoder_rnn(decoder_input)
# TimeDistributed allows the dense layer to be applied to each decoder output
per timestep
next_word_dense = TimeDistributed(
Dense(int(self.max_vocab_size/20),activation='relu'),
name='next_word_dense',
)(decoder_output)
next_word = TimeDistributed(
Dense(self.max_vocab_size,activation='softmax'),
name='next_word_softmax'
)(next_word_dense)
return Model(inputs=[encoder_input,last_word_input], outputs=[next_word])
用于训练模型的损失函数
该模型基于分类交叉熵损失进行训练,以预测解码器 LSTM 的每个时间步长中的目标词。任何步骤中的分类交叉熵损失都将超过词汇表中的所有单词,可以表示如下:
标签代表目标单词的一个热编码版本。只有与实际单词对应的标签才会是一;剩下的将是零点。术语 Pi 表示实际目标单词是由 i 索引的单词的概率。为了获得总损失, C ,对于每个输入/输出推文对,我们需要总结解码器 LSTM 的所有时间步长上的损失。由于词汇表的大小可能会变得非常大,因此在每个时间步骤中为目标标签创建一个热编码向量
将会非常昂贵。
sparse_categorical_crossentropy的丢失在这里变得非常有益,因为我们不需要将目标单词转换成一个热编码的向量,而是可以将目标单词的索引作为目标标签。
训练模型
模型可以用 Adam 优化器训练,因为它可靠地提供了稳定的收敛性。由于 rnn 容易出现爆炸式的梯度问题(尽管这对于 LSTMs 来说问题不大),如果梯度变得太大,最好对其进行裁剪。给定的模型可以用 Adam 优化器和sparse_categorical_crossentropy来定义和编译,如下面的代码块所示:
def create_model(self):
_model_ = self.define_model()
adam = Adam(lr=self.learning_rate,clipvalue=5.0)
_model_.compile(optimizer=adam,loss='sparse_categorical_crossentropy')
return _model_
现在我们已经了解了所有的基本功能,训练功能可以编码如下:
def train_model(self,model,X_train,X_test,y_train,y_test):
input_y_train = self.include_start_token(y_train)
print(input_y_train.shape)
input_y_test = self.include_start_token(y_test)
print(input_y_test.shape)
early = EarlyStopping(monitor='val_loss',patience=10,mode='auto')
checkpoint =
ModelCheckpoint(self.outpath + 's2s_model_' + str(self.version) +
'_.h5',monitor='val_loss',verbose=1,save_best_only=True,mode='auto')
lr_reduce =
ReduceLROnPlateau(monitor='val_loss',factor=0.5, patience=2, verbose=0,
mode='auto')
model.fit([X_train,input_y_train],y_train,
epochs=self.epochs,
batch_size=self.batch_size,
validation_data=[[X_test,input_y_test],y_test],
callbacks=[early,checkpoint,lr_reduce],
shuffle=True)
return model
在train_model函数的开始,我们创建input_y_train和input_y_test,它们分别是y_train和y_test的副本,并且从它们偏移一个时间步长,以便它们可以在解码器的每个时间步长充当前一个单词的输入。这些移位序列的第一个字是在解码器 LSTM 的第一时间步馈送的START关键字。include_start_token自定义实用功能如下:
def include_start_token(self,Y):
print(Y.shape)
Y = Y.reshape((Y.shape[0],Y.shape[1]))
Y = np.hstack((self.START*np.ones((Y.shape[0],1)),Y[:, :-1]))
return Y
回到训练功能train_model,我们看到如果损失在10时期没有减少,则使用EarlyStopping回拨功能启用提前停止。类似地,ReduceLROnPlateau回调会将现有的学习率减少一半(0.5),如果错误在两个时期内没有减少的话。只要误差在一个时期内减小,就通过ModelCheckpoint回调保存模型。
从模型生成输出响应
一旦模型被训练好,我们想用它来生成给定输入推文的响应。在以下步骤中也可以这样做:
- 用通用名称替换输入推文中的匿名屏幕名称。
- 将修改后的输入推文转换为单词索引。
- 将单词索引馈送到编码器 LSTM,将
START关键字馈送到解码器 LSTM,以生成第一个预测单词。从下一步开始,输入前一时间步的预测单词,而不是START关键字。 - 继续这样做,直到预测到句尾关键字。我们已经用
PAD表示了这一点。 - 查看逆词汇词典,从预测单词索引中获取单词。
下面的代码说明了respond_to_input功能,该功能可以在给定输入推文的情况下生成输出序列,以供参考:
def respond_to_input(self,model,input_sent):
input_y = self.include_start_token(self.PAD * np.ones((1,self.max_seq_len)))
ids = np.array(self.words_to_indices(input_sent)).reshape((1,self.max_seq_len))
for pos in range(self.max_seq_len -1):
pred = model.predict([ids, input_y]).argmax(axis=2)[0]
#pred = model.predict([ids, input_y])[0]
input_y[:,pos + 1] = pred[pos]
return self.indices_to_words(model.predict([ids,input_y]).argmax(axis=2)[0])
把它们放在一起
综上所述,main函数可以定义为有两个流程:一个用于训练,另一个用于推理。即使在训练函数中,我们也会对输入的推文序列生成一些响应,以检查我们训练模型的效果。以下代码显示了main功能供参考:
def main(self):
if self.mode == 'train':
X_train, X_test, y_train, y_test,test_sentences = self.data_creation()
print(X_train.shape,y_train.shape,X_test.shape,y_test.shape)
print('Data Creation completed')
model = self.create_model()
print("Model creation completed")
model = self.train_model(model,X_train,X_test,y_train,y_test)
test_responses = self.generate_response(model,test_sentences)
print(test_sentences)
print(test_responses)
pd.DataFrame(test_responses).to_csv(self.outpath +
'output_response.csv',index=False)
elif self.mode == 'inference':
model = load_model(self.load_model_from)
self.vocabulary = joblib.load(self.vocabulary_path)
self.reverse_vocabulary = joblib.load(self.reverse_vocabulary_path)
#nalyzer_file = open(self.analyzer_path,"rb")
count_vectorizer = joblib.load(self.count_vectorizer_path)
self.analyzer = count_vectorizer.build_analyzer()
data = self.process_data(self.data_path)
col = data.columns.tolist()[0]
test_sentences = list(data[col].values)
test_sentences = self.replace_anonymized_names(test_sentences)
responses = self.generate_response(model,test_sentences)
print(responses)
responses.to_csv(self.outpath + 'responses_' + str(self.version) +
'_.csv',index=False)
调用培训
可以通过运行带有多个参数的chatbot.py(参见本项目 GitHub 中的代码)模块来调用培训,如下命令所示:
python chatbot.py --max_vocab_size 50000 --max_seq_len 30 --embedding_dim 100 --hidden_state_dim 100 --epochs 80 --batch_size 128 --learning_rate 1e-4 --data_path /home/santanu/chatbot/data/twcs.csv --outpath /home/santanu/chatbot/ --dropout 0.3 --mode train --num_train_records 50000 --version v1
以下是一些重要的参数,以及它们的描述和用于调用 chatbot 序列到序列模型的训练的值:
| 参数 | 描述 | 用于训练的数值 |
| max_vocab_size | 要纳入词汇的单词数 | 50,000 |
| max_seq_len | 推特的最大长度将被限制为喂给 LSTM | 30 |
| hidden_state_dim | 隐藏状态维度的 LSTM |
100 |
| embedding_dim | 单词嵌入的维度 | 100 |
| learning rate | 用于优化器的起始学习速率 | 0.0001 |
| dropout | 用于正规化目的的辍学 | 0.3 |
| Mode | 训练/推理 | 训练使用train推理使用inference |
对一些输入推文的推断结果
训练后的序列到序列模型响应一些输入的推文生成一些相关的推文。以下命令显示了示例推理命令。data_path参数包含测试输入推文的位置:
python chatbot.py --max_vocab_size 50000 --max_seq_len 30 --embedding_dim 100 --hidden_state_dim 100 --data_path /home/santanu/chatbot/data/test.csv --outpath /home/santanu/chatbot/ --dropout 0.3 --mode inference --version v1 --load_model_from /home/santanu/chatbot/s2s_model_v1_.h5 --vocabulary_path /home/santanu/chatbot/vocabulary.pkl --reverse_vocabulary_path /home/santanu/chatbot/reverse_vocabulary.pkl --count_vectorizer_path /home/santanu/chatbot/count_vectorizer.pkl
推断的结果如下表所示:
| 在发推 | 发微博 |
| @sprintcare是最差的客服。 | 嘿,你好!你能告诉我们你账户的电子邮件地址吗?我们去后台看看/dnt.co/ldfdzrinat。 |
| 你们都谎称自己和 T2 有很好的关系。5 条 LTE,还是不会加载什么东西。Smh。 | @__cname__对于给您带来的麻烦,我们深表歉意。请给我们发送一封包含您的电子邮件地址的 dm,以便我们连接。 |
| @__cname__每当我联系客户支持时,他们都会告诉我我的帐户启用了短码,但在这 4 年中,我从未尝试过t.co/0G98RtNxPK。 | @__cname__你好,听到这个消息我们很难过。请给我们发一封包含您的电子邮件地址和电话号码的 dm,这样我们就可以对此进行调查。 |
| 其实那是你发给我的一个断掉的链接和不正确的信息t.co/V4yfrHR8VI。 | @__cname__嗨,听到这个消息我们很难过。请告诉我们你的联系方式,我们很乐意帮忙。千开 |
| 哟@Ask_Spectrum,你的客户服务代表是超级好的——但是如果你们没有得到我的服务,我就开始走神了! | @__cname__对于给您带来的麻烦,我们深表歉意。请给我们发送一封包含您的电子邮件地址的 dm,以便我们连接。 |
| 我的照片几乎每天都在@Ask_Spectrum上。我为什么要每月支付 171 美元?t.co/U6ptkQa5Ik。 | @__cname__嗨,听到这个消息我们很难过。请告诉我们你的联系方式,我们很乐意帮忙。千开 |
| 请从@VerizonSupport来的人帮助我,我在你们的客户服务方面运气最差。 | @__cname__对于给您带来的麻烦,我们深表歉意。请告诉我们您的联系方式和电话号码,以便我们提供帮助。 |
| @VerizonSupport我的朋友没有互联网,我们需要一起玩电子游戏,请不要有互联网,我们的技能每时每刻都在下降。 | 嘿,你好!你能告诉我们你账户的电子邮件地址吗?我们去后台看看/dnt.co/ldfdzrinat |
| @__cname__试图支付 60 天的账单。没有服务,粗鲁的 CS,和几次转移。查一下我的设备#给我一个账号#! | @__cname__嗨,听到这个消息我们很难过。请告诉我们你的联系方式,我们很乐意帮忙。千开 |
| @__cname__是我遇到过最差的 ISP。 | 嘿,你好!你能告诉我们你账户的电子邮件地址吗?我们去后台看看/dnt.co/ldfdzrinat |
| @__cname__修复你的应用程序,它甚至不会打开。 | 你好!我们没有关于这个问题的任何信息。请寄给我们一份 dm,上面有您的姓名和电话号码以及完整的服务地址。 |
摘要
现在,我们到了这一章的结尾。在浏览了本章中阐述的与 chatbot 和序列到序列模型相关的各种概念后,读者现在应该能够构建自己的 chat bot 实现,并以有趣的方式对其进行扩展。正如我们所知,序列到序列模型不仅适用于聊天机器人,还适用于一系列自然语言处理领域,如机器翻译。本章代码可在 GitHub 位置https://GitHub . com/PacktPublishing/Python-人工智能-项目/树/主/第 08 章找到。
在下一章中,我们将使用强化学习来让赛车学会自己驾驶。我们期待您的参与。*
九、基于强化学习的自动驾驶汽车
强化学习,即代理人通过与环境互动来学习做出决策,在过去几年中真正起飞了。它是当今人工智能和机器学习领域最热门的话题之一,该领域的研究进展迅速。在强化学习 ( RL )中,代理将他们的行动和经验转化为学习,以便在未来做出更好的决策。
强化学习不属于有监督或无监督的机器学习范式,因为它本身就是一个领域。在监督学习中,我们尝试学习一种映射 F: X → Y ,该映射将输入 X 映射为输出 Y ,而在强化学习中,代理通过试错学习采取最佳行动。当一个代理人做得好的时候,会得到奖励,而当代理人做得不好的时候,会得到惩罚。代理试图吸收这些信息,并学会在类似的情况下不重复这些错误。代理所面临的这些条件被称为状态。图 9.1 显示了强化学习框架中环境中代理的交互:
Figure 9.1: Illustration of agent and environment interaction
技术要求
你将需要具备 Python 3、TensorFlow、Keras 和 OpenCV 的基本知识。
本章代码文件可在 GitHub: https://GitHub . com/PacktPublishing/Intelligent-Projects-use-Python/tree/master/chapter 09上找到
查看以下视频,查看正在运行的代码: bit.ly/2WxfwpF
马尔可夫决策过程
任何强化学习问题都可以看作是一个马尔可夫决策过程,我们在第一章、基于人工智能系统的基础中简要介绍过。为了您的利益,我们将再次详细讨论这一点。在马尔可夫决策过程中,我们有一个与环境交互的代理。在任何给定的情况下, t 试剂暴露于多种状态之一: (s (t) = s) ∈ S 。基于代理的动作 (a (t) = a) ∈ A 在状态 s (t) 代理被呈现一个新的状态*(S(t+1)= S′**)∈S*。这里, S 表示代理可能暴露于的所有状态的集合,而 A 表示代理可能参与的动作。
你现在可能想知道代理如何采取行动。它应该是随机的还是基于启发式的?嗯,这要看代理和环境有多少互动。在初始阶段,代理可能会采取随机行动,因为他们对环境一无所知。然而,一旦代理与环境进行了足够的交互,基于奖励和惩罚,代理就会知道在给定的状态下应该采取什么样的适当行动。类似于人们倾向于采取有利于长期回报的行动,RL 代理也采取他的行动,这使长期回报最大化。
数学上,代理试图为每个状态动作对 (s ∈ S,a ∈ A) 学习一个 Q 值 Q(s,a) 。对于给定的状态*(t)*,RL 代理选择给出最大 Q 值的动作 a、。代理人采取的动作 a (t) 可以表示如下:
一旦代理人采取行动 a (t) 处于状态 s (t) ,一个新的状态 s (t+1) 被呈现给待处理的代理人。这个新状态 s (t+1) 一般不具有确定性,一般表示为当前状态 s (t) 和动作 a (t) 上的概率分布条件。这些概率被称为状态转移概率,可以表示如下:
每当代理在状态采取行动 a (t) 并到达新状态 s (t+1) 时,立即奖励给代理,可表示如下:
现在我们有了定义马尔可夫决策过程所需的一切。马尔可夫决策过程是由以下四个要素表征的系统:
- 一组状态 S
- 一套动作一套
- 一套奖励 R
- 状态转移概率P(s(t+1)= s′/s(t)= s,a(t)= a:
Figure 9.2: Illustration of a Markov decision process with three states
学习 Q 值函数
对于一个 RL 代理来说,学习 Q 值函数是很重要的。Q 值函数可以通过贝尔曼方程迭代学习。当代理开始与环境交互时,它以随机状态*(0)*和每个状态动作对的 Q 值的随机状态开始。代理的行为也会有些随机,因为它没有状态 Q 值来做出明智的决定。对于所采取的每一个动作,环境将根据哪个代理开始构建 Q 值表来返回奖励,并随着时间的推移而改进。
在任何暴露状态下 s (t) 在迭代 t 时,代理将采取一个动作 a (t) ,使其长期回报最大化。Q 表保存长期奖励值,因此选择的 a (t) 将基于以下启发:
Q 值表也是通过迭代 t 来索引的,因为代理目前只能查看 Q 表构建,随着代理与环境的交互越来越多,Q 表构建将会改进。
基于采取的动作 a (t) ,环境将向代理人呈现奖励 r (t) 和新状态 s (t+1) 。代理将更新 Q 表,使其总的长期预期回报最大化。长期奖励*r(t)*可以写成如下形式:
这里,是折扣因子。我们可以看到,长期奖励结合了即时奖励 r (t) 和基于下一个状态 s (t+1) 呈现的累积未来奖励。
基于计算出的长期奖励,状态动作对 (s (t) 、a (t) ) 的现有 Q 值更新如下:
深度学习
深度 Q 学习利用深度学习网络学习 Q 值函数。如下图所示,*图 9.3,*是深度 Q 学习网络的架构:
Figure 9.3: Illustration of a deep Q network
该图学习将每对状态 (s,a) 和动作映射成输出 Q 值输出 Q(s,a),而在右侧的图中,对于每个状态*,*我们学习与每个动作 a 相关的 Q 值。如果每个状态都有 n 个可能的动作,网络的输出产生 n 个输出 Q(s,a 1 )、Q(s,a 2 ),。。。。。Q(s,a n ) 。
深度 Q 学习网络是用一个非常简单的想法训练出来的,这个想法叫做经验重播。让 RL 代理与环境交互,将体验以 *(s,a,r,s’)*的元组形式存储在重放缓冲区中。可以从这个重放缓冲器中取样小批量来训练网络。开始时,重放缓冲区以随机体验存储。
制定成本函数
使用这种体系结构更容易,在这种体系结构中,我们可以得到网络在给定状态下所有动作的 Q 值。同样的情况在图 9.3 的右侧也有说明。我们会让代理与环境交互,收集状态和奖励,在此基础上我们将学习 Q 函数。事实上,网络会通过将给定状态下所有动作的预测 Q 值与目标 Q 值最小化来学习 Q 函数。每个训练记录都是一个元组 (s (t) 、a (t) 、r (t) 、s (t+1) ) 。
请记住,目标 Q 值是根据网络本身计算的。让我们考虑网络由 W ∈ R d 权重参数化的事实,并且我们学习从状态到给定状态的每个动作的 Q 值的映射。对于 n 组动作,网络将预测与每个动作相关的 i Q 值。映射函数可以表示如下:
这个映射用于预测给定状态的 Q 值 s (t) ,这个预测到达我们正在最小化的成本函数。这里需要考虑的唯一技术问题是,我们只需要取对应于在实例 t 处观察到的动作 a (t) 的预测 Q 值。
我们可以使用相同的映射,基于下一个状态 s (t+1) 来构建目标 Q 值。如前一节所示,Q 值的候选更新如下:
因此,目标 Q 值可以这样计算:
为了学习从状态到 Q 值的函数映射,我们相对于神经网络的权重最小化平方损失或其他相关损失:
双深度 Q 学习
深度 Q 学习的一个问题是,我们使用相同的网络权重 W 来估计目标和 Q 值。因此,我们预测的 Q 值和目标 Q 值之间有很大的相关性,因为它们都使用相同的变化权重。这使得预测的和目标的 Q 值在训练的每一步都移动,导致振荡。
为了稳定这一点,我们使用原始网络的副本来估计目标 Q 值,目标网络的权重在步骤中以特定的间隔从原始网络中复制。这种深度 Q 学习网络的变体称为双深度 Q 学习,一般会带来稳定的训练。双深度 Q 学习的工作机制如下图所示图 9.4A 和图 9.4B :
Figure 9.4A: Illustration of double deep Q learning
Figure 9.4B: Illustration of double deep Q learning
在上图中,我们可以看到两个网络:网络 A 学习预测给定状态下的实际 Q 值,网络 B 帮助计算目标 Q 值。网络 A 通过最小化目标和预测 Q 值的损失函数进行改善。由于 Q 值本质上通常是连续的,一些有效的损失函数是mean squared error、mean absolute error、Huber Loss、log-cosh loss等等。
网络 B 基本上是网络 A 的副本,因此它们共享相同的架构。来自网络 A 的权重通常以指定的时间间隔复制到网络 B 。这是为了确保不使用同一组网络权重来预测 Q 值,并且还制定目标 Q 值,因为这会导致不稳定的训练。给定单个训练元组 (s (t) = s,a (t) = a,r (t) *= r,s(t+1)= s′),网络 A**给定状态s(t)*给出 Q 值的预测既然知道了实际动作 a (t) = a ,我们就选择 Q 值Q(t)(s(t)= s,a (t) = a) 。这将作为我们的预测 Q 值。
计算目标现在有点困难,因为它涉及两个网络。我们知道在任何状态 s (t) 在步骤 t 的候选 Q 值是在时间 t 的即时奖励 r (t) 加上给定新状态 s (t+1) 的下一步最大 Q 值。候选 Q 值可以表示如下:
当是一个恒定的折扣因子时就是这种情况。奖励 r 已经是训练元组的一部分。因此,我们唯一需要计算的目标是动作a′,该动作给出最大的 Q 值,并将对应的 Q 值带到该对应的动作a′。这个计算问题
分为两部分:
- 网络 A 确定给定状态s′给出最大 Q 值的动作A′T5。但是我们不会从网络 A 取动作A′对应的 Q 值和状态s′。
- 网络 B 用于提取状态s和动作a对应的 Q 值Q(t-k)(s′、a′)。
与基础DQN相比,这导致了更稳定的训练。
实现自动驾驶汽车
我们现在将着眼于实现一种自动驾驶赛车,它可以使用深度 Q 网络在赛道上学会自己驾驶。驾驶员和汽车将充当代理,而赛道及其周围环境则充当环境。我们将使用 OpenAI 健身房CarRacing-v0框架作为环境。状态和奖励将由环境呈现给代理人,而代理人将通过采取适当的行动来执行这些。这些状态是从车前的摄像头拍摄的图像。环境接受的动作是三维向量 a ∈ R 3 的形式,其中第一个分量用于左转,第二个分量用于向前移动,第三个分量用于向右移动。代理将与环境交互,并将交互转换为形式的元组。这些交互元组将作为我们的训练数据。
该架构将类似于我们在图表右侧所展示的内容(图 9.4A 和图 9.4B )。
深度 Q 学习的离散化动作
离散化动作对于深度 Q 学习非常重要,因为三维连续动作空间可以有无限的 Q 值,并且不可能在深度 Q 网络的输出层中为每个动作都有单独的单元。动作空间的三个维度如下:
转向:∞[-1,1]
气:∏0,1】
突破:∏0,1】
我们将这个三维动作空间转换为我们感兴趣的四个动作,如下所示:
Brake : [0.0, 0.0, 0.0]
Sharp Left: [-0.6, 0.05, 0.0]
Sharp Right: [0.6, 0.05, 0.0]
Straight: [0.0, 0.3, 0.0]
实现双深度 Q 网络
双深 Q 网络的网络架构如下图所示。这些网络采用有线电视新闻网架构,将状态处理为图像,并为所有可能的动作输出 Q 值。详细代码(DQN.py)如下:
import keras
from keras import optimizers
from keras.layers import Convolution2D
from keras.layers import Dense, Flatten, Input, concatenate, Dropout
from keras.models import Model
from keras.utils import plot_model
from keras import backend as K
import numpy as np
'''
Double Deep Q Network Implementation
'''
learning_rate = 0.0001
BATCH_SIZE = 128
class DQN:
def __init__(self,num_states,num_actions,model_path):
self.num_states = num_states
print(num_states)
self.num_actions = num_actions
self.model = self.build_model() # Base Model
self.model_ = self.build_model()
# target Model (copy of Base Model)
self.model_chkpoint_1 = model_path +"CarRacing_DDQN_model_1.h5"
self.model_chkpoint_2 = model_path +"CarRacing_DDQN_model_2.h5"
save_best = keras.callbacks.ModelCheckpoint(self.model_chkpoint_1,
monitor='loss',
verbose=1,
save_best_only=True,
mode='min',
period=20)
save_per = keras.callbacks.ModelCheckpoint(self.model_chkpoint_2,
monitor='loss',
verbose=1,
save_best_only=False,
mode='min',
period=400)
self.callbacks_list = [save_best,save_per]
# Convolutional Neural Network that takes in the state and outputs the Q values for all the possible actions.
def build_model(self):
states_in = Input(shape=self.num_states,name='states_in')
x = Convolution2D(32,(8,8),strides=(4,4),activation='relu')(states_in)
x = Convolution2D(64,(4,4), strides=(2,2), activation='relu')(x)
x = Convolution2D(64,(3,3), strides=(1,1), activation='relu')(x)
x = Flatten(name='flattened')(x)
x = Dense(512,activation='relu')(x)
x = Dense(self.num_actions,activation="linear")(x)
model = Model(inputs=states_in, outputs=x)
self.opt = optimizers.Adam(lr=learning_rate, beta_1=0.9, beta_2=0.999, epsilon=None,decay=0.0, amsgrad=False)
model.compile(loss=keras.losses.mse,optimizer=self.opt)
plot_model(model,to_file='model_architecture.png',show_shapes=True)
return model
# Train function
def train(self,x,y,epochs=10,verbose=0):
self.model.fit(x,y,batch_size=(BATCH_SIZE), epochs=epochs, verbose=verbose, callbacks=self.callbacks_list)
#Predict function
def predict(self,state,target=False):
if target:
# Return the Q value for an action given a state from thr target Network
return self.model_.predict(state)
else:
# Return the Q value from the original Network
return self.model.predict(state)
# Predict for single state function
def predict_single_state(self,state,target=False):
x = state[np.newaxis,:,:,:]
return self.predict(x,target)
#Update the target Model with the Base Model weights
def target_model_update(self):
self.model_.set_weights(self.model.get_weights())
正如我们在前面的代码中看到的,我们有两个模型,其中一个是另一个的副本。基础模型和目标模型保存为CarRacing_DDQN_model_1.h5 和CarRacing_DDQN_model_2.h5。
通过调用target_model_update ,目标模型被更新为具有与基础模型相同的权重。
设计代理
代理将与环境交互,并且在给定的状态下,将尝试执行最佳操作。代理最初将执行随机动作,随着训练的进行,这些动作将更多地基于给定状态的 Q 值。ε参数的值决定了动作随机的概率。最初ε被设置为1以使动作随机。当代理已经收集了指定数量的训练样本时,在每个步骤中减少ε,使得动作随机的概率减少。这种基于ε值的方案被称为ε贪婪算法。我们定义两个代理类如下:
Agent:基于给定状态的 Q 值执行动作RandomAgent:执行随机动作
代理类有三个功能,具有以下功能:
act:代理根据状态决定要采取的动作observe:代理捕获状态和目标 Q 值replay:代理基于观察训练模型
代理(Agents.py)的详细代码如下所示:
import math
from Memory import Memory
from DQN import DQN
import numpy as np
import random
from helper_functions import sel_action,sel_action_index
# Agent and Random Agent implementations
max_reward = 10
grass_penalty = 0.4
action_repeat_num = 8
max_num_episodes = 1000
memory_size = 10000
max_num_steps = action_repeat_num * 100
gamma = 0.99
max_eps = 0.1
min_eps = 0.02
EXPLORATION_STOP = int(max_num_steps*10)
_lambda_ = - np.log(0.001) / EXPLORATION_STOP
UPDATE_TARGET_FREQUENCY = int(50)
batch_size = 128
class Agent:
steps = 0
epsilon = max_eps
memory = Memory(memory_size)
def __init__(self, num_states,num_actions,img_dim,model_path):
self.num_states = num_states
self.num_actions = num_actions
self.DQN = DQN(num_states,num_actions,model_path)
self.no_state = np.zeros(num_states)
self.x = np.zeros((batch_size,)+img_dim)
self.y = np.zeros([batch_size,num_actions])
self.errors = np.zeros(batch_size)
self.rand = False
self.agent_type = 'Learning'
self.maxEpsilone = max_eps
def act(self,s):
print(self.epsilon)
if random.random() < self.epsilon:
best_act = np.random.randint(self.num_actions)
self.rand=True
return sel_action(best_act), sel_action(best_act)
else:
act_soft = self.DQN.predict_single_state(s)
best_act = np.argmax(act_soft)
self.rand=False
return sel_action(best_act),act_soft
def compute_targets(self,batch):
# 0 -> Index for current state
# 1 -> Index for action
# 2 -> Index for reward
# 3 -> Index for next state
states = np.array([rec[1][0] for rec in batch])
states_ = np.array([(self.no_state if rec[1][3] is None else rec[1][3]) for rec in batch])
p = self.DQN.predict(states)
p_ = self.DQN.predict(states_,target=False)
p_t = self.DQN.predict(states_,target=True)
act_ctr = np.zeros(self.num_actions)
for i in range(len(batch)):
rec = batch[i][1]
s = rec[0]; a = rec[1]; r = rec[2]; s_ = rec[3]
a = sel_action_index(a)
t = p[i]
act_ctr[a] += 1
oldVal = t[a]
if s_ is None:
t[a] = r
else:
t[a] = r + gamma * p_t[i][ np.argmax(p_[i])] # DDQN
self.x[i] = s
self.y[i] = t
if self.steps % 20 == 0 and i == len(batch)-1:
print('t',t[a], 'r: %.4f' % r,'mean t',np.mean(t))
print ('act ctr: ', act_ctr)
self.errors[i] = abs(oldVal - t[a])
return (self.x, self.y,self.errors)
def observe(self,sample): # in (s, a, r, s_) format
_,_,errors = self.compute_targets([(0,sample)])
self.memory.add(errors[0], sample)
if self.steps % UPDATE_TARGET_FREQUENCY == 0:
self.DQN.target_model_update()
self.steps += 1
self.epsilon = min_eps + (self.maxEpsilone - min_eps) * np.exp(-1*_lambda_ * self.steps)
def replay(self):
batch = self.memory.sample(batch_size)
x, y,errors = self.compute_targets(batch)
for i in range(len(batch)):
idx = batch[i][0]
self.memory.update(idx, errors[i])
self.DQN.train(x,y)
class RandomAgent:
memory = Memory(memory_size)
exp = 0
steps = 0
def __init__(self, num_actions):
self.num_actions = num_actions
self.agent_type = 'Learning'
self.rand = True
def act(self, s):
best_act = np.random.randint(self.num_actions)
return sel_action(best_act), sel_action(best_act)
def observe(self, sample): # in (s, a, r, s_) format
error = abs(sample[2]) # reward
self.memory.add(error, sample)
self.exp += 1
self.steps += 1
def replay(self):
pass
自动驾驶汽车的环境
自动驾驶汽车的环境是**欧派健身房的CarRacing-v0。**在这个 OpenAI 环境中呈现给代理的状态是CarRacing-v0中模拟汽车前方的图像。环境也根据代理在给定状态下采取的行动返回奖励。如果汽车踩在草地上,我们会处罚奖励,并且会将奖励正常化到(-1,1)范围内进行稳定训练。环境的详细代码如下
import gym
from gym import envs
import numpy as np
from helper_functions import rgb2gray,action_list,sel_action,sel_action_index
from keras import backend as K
seed_gym = 3
action_repeat_num = 8
patience_count = 200
epsilon_greedy = True
max_reward = 10
grass_penalty = 0.8
max_num_steps = 200
max_num_episodes = action_repeat_num*100
'''
Enviroment to interact with the Agent
'''
class environment:
def __init__(self, environment_name,img_dim,num_stack,num_actions,render,lr):
self.environment_name = environment_name
print(self.environment_name)
self.env = gym.make(self.environment_name)
envs.box2d.car_racing.WINDOW_H = 500
envs.box2d.car_racing.WINDOW_W = 600
self.episode = 0
self.reward = []
self.step = 0
self.stuck_at_local_minima = 0
self.img_dim = img_dim
self.num_stack = num_stack
self.num_actions = num_actions
self.render = render
self.lr = lr
if self.render == True:
print("Rendering proeprly set")
else:
print("issue in Rendering")
# Agent performing its task
def run(self,agent):
self.env.seed(seed_gym)
img = self.env.reset()
img = rgb2gray(img, True)
s = np.zeros(self.img_dim)
#Collecting the state
for i in range(self.num_stack):
s[:,:,i] = img
s_ = s
R = 0
self.step = 0
a_soft = a_old = np.zeros(self.num_actions)
a = action_list[0]
#print(agent.agent_type)
while True:
if agent.agent_type == 'Learning' :
if self.render == True :
self.env.render("human")
if self.step % action_repeat_num == 0:
if agent.rand == False:
a_old = a_soft
#Agent outputs the action
a,a_soft = agent.act(s)
# Rescue Agent stuck at local minima
if epsilon_greedy:
if agent.rand == False:
if a_soft.argmax() == a_old.argmax():
self.stuck_at_local_minima += 1
if self.stuck_at_local_minima >= patience_count:
print('Stuck in local minimum, reset learning rate')
agent.steps = 0
K.set_value(agent.DQN.opt.lr,self.lr*10)
self.stuck_at_local_minima = 0
else:
self.stuck_at_local_minima =
max(self.stuck_at_local_minima -2, 0)
K.set_value(agent.DQN.opt.lr,self.lr)
#Perform the action on the environment
img_rgb, r,done,info = self.env.step(a)
if not done:
# Create the next state
img = rgb2gray(img_rgb, True)
for i in range(self.num_stack-1):
s_[:,:,i] = s_[:,:,i+1]
s_[:,:,self.num_stack-1] = img
else:
s_ = None
# Cumulative reward tracking
R += r
# Normalize reward given by the gym environment
r = (r/max_reward)
if np.mean(img_rgb[:,:,1]) > 185.0:
# Penalize if the car is on the grass
r -= grass_penalty
# Keeping the value of reward within -1 and 1
r = np.clip(r, -1 ,1)
#Agent has a whole state,action,reward,and next state to learn from
agent.observe( (s, a, r, s_) )
agent.replay()
s = s_
else:
img_rgb, r, done, info = self.env.step(a)
if not done:
img = rgb2gray(img_rgb, True)
for i in range(self.num_stack-1):
s_[:,:,i] = s_[:,:,i+1]
s_[:,:,self.num_stack-1] = img
else:
s_ = None
R += r
s = s_
if (self.step % (action_repeat_num * 5) == 0) and
(agent.agent_type=='Learning'):
print('step:', self.step, 'R: %.1f' % R, a, 'rand:', agent.rand)
self.step += 1
if done or (R <-5) or (self.step > max_num_steps) or
np.mean(img_rgb[:,:,1]) > 185.1:
self.episode += 1
self.reward.append(R)
print('Done:', done, 'R<-5:', (R<-5), 'Green
>185.1:',np.mean(img_rgb[:,:,1]))
break
print("Episode ",self.episode,"/", max_num_episodes,agent.agent_type)
print("Average Episode Reward:", R/self.step, "Total Reward:",
sum(self.reward))
def test(self,agent):
self.env.seed(seed_gym)
img= self.env.reset()
img = rgb2gray(img, True)
s = np.zeros(self.img_dim)
for i in range(self.num_stack):
s[:,:,i] = img
R = 0
self.step = 0
done = False
while True :
self.env.render('human')
if self.step % action_repeat_num == 0:
if(agent.agent_type == 'Learning'):
act1 = agent.DQN.predict_single_state(s)
act = sel_action(np.argmax(act1))
else:
act = agent.act(s)
if self.step <= 8:
act = sel_action(3)
img_rgb, r, done,info = self.env.step(act)
img = rgb2gray(img_rgb, True)
R += r
for i in range(self.num_stack-1):
s[:,:,i] = s[:,:,i+1]
s[:,:,self.num_stack-1] = img
if(self.step % 10) == 0:
print('Step:', self.step, 'action:',act, 'R: %.1f' % R)
print(np.mean(img_rgb[:,:,0]), np.mean(img_rgb[:,:,1]),
np.mean(img_rgb[:,:,2]))
self.step += 1
if done or (R< -5) or (agent.steps > max_num_steps) or
np.mean(img_rgb[:,:,1]) > 185.1:
R = 0
self.step = 0
print('Done:', done, 'R<-5:', (R<-5), 'Green>
185.1:',np.mean(img_rgb[:,:,1]))
break
上面代码中的run函数表示代理在环境上下文中的活动。
把它们放在一起
main.py脚本将环境的逻辑、DQN和agent适当地放在一起,使汽车能够通过强化学习来学习驾驶。详细代码如下:
import sys
#sys.path.append('/home/santanu/ML_DS_Catalog-/Python-Artificial-Intelligence-Projects_backup/Python-Artificial-Intelligence-Projects/Chapter09/Scripts/')
from gym import envs
from Agents import Agent,RandomAgent
from helper_functions import action_list,model_save
from environment import environment
import argparse
import numpy as np
import random
from sum_tree import sum_tree
from sklearn.externals import joblib
'''
This is the main module for training and testing the CarRacing Application from gym
'''
if __name__ == "__main__":
#Define the Parameters for training the Model
parser = argparse.ArgumentParser(description='arguments')
parser.add_argument('--environment_name',default='CarRacing-v0')
parser.add_argument('--model_path',help='model_path')
parser.add_argument('--train_mode',type=bool,default=True)
parser.add_argument('--test_mode',type=bool,default=False)
parser.add_argument('--epsilon_greedy',default=True)
parser.add_argument('--render',type=bool,default=True)
parser.add_argument('--width',type=int,default=96)
parser.add_argument('--height',type=int,default=96)
parser.add_argument('--num_stack',type=int,default=4)
parser.add_argument('--lr',type=float,default=1e-3)
parser.add_argument('--huber_loss_thresh',type=float,default=1.)
parser.add_argument('--dropout',type=float,default=1.)
parser.add_argument('--memory_size',type=int,default=10000)
parser.add_argument('--batch_size',type=int,default=128)
parser.add_argument('--max_num_episodes',type=int,default=500)
args = parser.parse_args()
environment_name = args.environment_name
model_path = args.model_path
test_mode = args.test_mode
train_mode = args.train_mode
epsilon_greedy = args.epsilon_greedy
render = args.render
width = args.width
height = args.height
num_stack = args.num_stack
lr = args.lr
huber_loss_thresh = args.huber_loss_thresh
dropout = args.dropout
memory_size = args.memory_size
dropout = args.dropout
batch_size = args.batch_size
max_num_episodes = args.max_num_episodes
max_eps = 1
min_eps = 0.02
seed_gym = 2 # Random state
img_dim = (width,height,num_stack)
num_actions = len(action_list)
if __name__ == '__main__':
environment_name = 'CarRacing-v0'
env = environment(environment_name,img_dim,num_stack,num_actions,render,lr)
num_states = img_dim
print(env.env.action_space.shape)
action_dim = env.env.action_space.shape[0]
assert action_list.shape[1] ==
action_dim,"length of Env action space does not match action buffer"
num_actions = action_list.shape[0]
# Setting random seeds with respect to python inbuilt random and numpy random
random.seed(901)
np.random.seed(1)
agent = Agent(num_states, num_actions,img_dim,model_path)
randomAgent = RandomAgent(num_actions)
print(test_mode,train_mode)
try:
#Train agent
if test_mode:
if train_mode:
print("Initialization with random agent. Fill memory")
while randomAgent.exp < memory_size:
env.run(randomAgent)
print(randomAgent.exp, "/", memory_size)
agent.memory = randomAgent.memory
randomAgent = None
print("Starts learning")
while env.episode < max_num_episodes:
env.run(agent)
model_save(model_path, "DDQN_model.h5", agent, env.reward)
else:
# Load train Model
print('Load pre-trained agent and learn')
agent.DQN.model.load_weights(model_path+"DDQN_model.h5")
agent.DQN.target_model_update()
try :
agent.memory = joblib.load(model_path+"DDQN_model.h5"+"Memory")
Params = joblib.load(model_path+"DDQN_model.h5"+"agent_param")
agent.epsilon = Params[0]
agent.steps = Params[1]
opt = Params[2]
agent.DQN.opt.decay.set_value(opt['decay'])
agent.DQN.opt.epsilon = opt['epsilon']
agent.DQN.opt.lr.set_value(opt['lr'])
agent.DQN.opt.rho.set_value(opt['rho'])
env.reward = joblib.load(model_path+"DDQN_model.h5"+"Rewards")
del Params, opt
except:
print("Invalid DDQL_Memory_.csv to load")
print("Initialization with random agent. Fill memory")
while randomAgent.exp < memory_size:
env.run(randomAgent)
print(randomAgent.exp, "/", memory_size)
agent.memory = randomAgent.memory
randomAgent = None
agent.maxEpsilone = max_eps/5
print("Starts learning")
while env.episode < max_num_episodes:
env.run(agent)
model_save(model_path, "DDQN_model.h5", agent, env.reward)
else:
print('Load agent and play')
agent.DQN.model.load_weights(model_path+"DDQN_model.h5")
done_ctr = 0
while done_ctr < 5 :
env.test(agent)
done_ctr += 1
env.env.close()
#Graceful exit
except KeyboardInterrupt:
print('User interrupt..gracefule exit')
env.env.close()
if test_mode == False:
# Prompt for Model save
print('Save model: Y or N?')
save = input()
if save.lower() == 'y':
model_save(model_path, "DDQN_model.h5", agent, env.reward)
else:
print('Model is not saved!')
助手函数
以下是该强化学习框架中使用的几个辅助函数,用于动作选择、存储用于训练的观察值、状态图像处理和保存训练模型的权重:
"""
Created on Thu Nov 2 16:03:46 2017
@author: Santanu Pattanayak
"""
from keras import backend as K
import numpy as np
import shutil, os
import numpy as np
import pandas as pd
from scipy import misc
import pickle
import matplotlib.pyplot as plt
from sklearn.externals import joblib
huber_loss_thresh = 1
action_list = np.array([
[0.0, 0.0, 0.0], #Brake
[-0.6, 0.05, 0.0], #Sharp left
[0.6, 0.05, 0.0], #Sharp right
[0.0, 0.3, 0.0]] ) #Staight
rgb_mode = True
num_actions = len(action_list)
def sel_action(action_index):
return action_list[action_index]
def sel_action_index(action):
for i in range(num_actions):
if np.all(action == action_list[i]):
return i
raise ValueError('Selected action not in list')
def huber_loss(y_true,y_pred):
error = (y_true - y_pred)
cond = K.abs(error) <= huber_loss_thresh
if cond == True:
loss = 0.5 * K.square(error)
else:
loss = 0.5 *huber_loss_thresh**2 + huber_loss_thresh*(K.abs(error) - huber_loss_thresh)
return K.mean(loss)
def rgb2gray(rgb,norm=True):
gray = np.dot(rgb[...,:3], [0.299, 0.587, 0.114])
if norm:
# normalize
gray = gray.astype('float32') / 128 - 1
return gray
def data_store(path,action,reward,state):
if not os.path.exists(path):
os.makedirs(path)
else:
shutil.rmtree(path)
os.makedirs(path)
df = pd.DataFrame(action, columns=["Steering", "Throttle", "Brake"])
df["Reward"] = reward
df.to_csv(path +'car_racing_actions_rewards.csv', index=False)
for i in range(len(state)):
if rgb_mode == False:
image = rgb2gray(state[i])
else:
image = state[i]
misc.imsave( path + "img" + str(i) +".png", image)
def model_save(path,name,agent,R):
''' Saves actions, rewards and states (images) in DataPath'''
if not os.path.exists(path):
os.makedirs(path)
agent.DQN.model.save(path + name)
print(name, "saved")
print('...')
joblib.dump(agent.memory,path+name+'Memory')
joblib.dump([agent.epsilon,agent.steps,agent.DQN.opt.get_config()], path+name+'AgentParam')
joblib.dump(R,path+name+'Rewards')
print('Memory pickle dumped')
自动驾驶汽车强化学习过程的培训可按如下方式调用
python main.py --environment_name 'CarRacing-v0' --model_path '/home/santanu/Autonomous Car/train/' --train_mode True --test_mode False --epsilon_greedy True --render True --width 96 --height 96 --num_stack 4 --huber_loss_thresh 1 --dropout 0.2 --memory_size 10000 --batch_size 128 --max_num_episodes 500
培训结果
最初,自动驾驶汽车会犯错,但经过一段时间后,汽车通过训练从错误中学习,因此变得更好。这张截图显示了汽车在训练的最初阶段的活动图像,然后是训练的后期,当它从以前的错误中吸取教训时的图像。这一点已经在下面的截图中说明了(图 9.5(A )和图 9.5(B) ):
Figure 9.5(A): The car making mistakes in the initial part of the training
以下结果显示汽车经过足够的训练后成功驾驶:
Figure 9.5(B): The car driving successfully after sufficient training
摘要
至此,我们结束了这一章。本章中讨论的主题将帮助您跟上强化学习范式,并使您能够构建智能 RL 系统。此外,读者应该将在本项目中学到的技术应用于其他基于 RL 的问题。
在下一章中,我们将从深度学习的角度来看验证码,并围绕它构建一些有趣的项目。期待您的参与。
十、深度学习视角下的验证码
术语验证码是完全自动化的公共图灵测试的首字母缩写,用来区分计算机和人类。这是一个旨在区分人类用户和机器或机器人的计算机程序,通常作为防止垃圾邮件和数据滥用的安全措施。验证码的概念早在 1997 年就被引入,当时互联网搜索公司 AltaVista 试图阻止自动向平台提交网址,这扭曲了他们的搜索引擎算法。为了解决这个问题,AltaVista 的首席科学家 Andrei Broder 想出了一种算法,可以随机生成文本图像,这些图像很容易被人类识别,但不会被机器人识别。后来,在 2003 年,路易斯·冯·安恩、曼努埃尔·布卢姆、尼古拉斯·J·霍珀和约翰·兰福德完善了这项技术,并将其称为验证码。最常见的验证码形式需要用户识别失真图像中的字母和数字。这项测试的目的是希望人类能够很容易地区分扭曲图像中的字符,而自动程序或机器人将无法区分它们。验证码测试有时被称为反向图灵测试,因为它是由计算机而不是人类执行的。
直到最近,验证码已经开始发挥比防止机器人欺诈更大的作用。例如,谷歌在将《纽约时报》的档案和谷歌图书中的一些书籍数字化时,使用了验证码及其变体之一 reCAPTCHA。这通常是通过要求用户正确输入多个验证码的字符来完成的。实际上只有一个验证码被标记并用于验证用户是否是人类。
其余的验证码由用户标记。目前,谷歌使用基于图像的验证码来帮助标记其自动驾驶汽车数据集,如下图所示:
Figure 10.1: Some common CAPTCHAs on various websites
在本章中,我们将涵盖以下主题:
- 什么是验证码
- 使用深度学习破解验证码,暴露其漏洞
- 利用对抗性学习生成验证码
技术要求
你将需要具备 Python 3、TensorFlow、Keras 和 OpenCV 的基本知识。
本章代码文件可在 GitHub: https://GitHub . com/PacktPublishing/Intelligent-Projects-use-Python/tree/master/chapter 10上找到
查看以下视频,查看正在运行的代码: bit.ly/2SgwR6P
用深度学习打破藩篱
随着卷积神经网络 ( CNNs )最近在计算机视觉任务中的成功,在几分钟内打破基本的验证码是一项相对容易的任务。因此,验证码需要比过去更加进化。在本章的第一部分,我们将通过深度学习框架,揭示使用僵尸工具自动检测基本验证码的漏洞。我们将通过利用 GAN 创建更难被机器人检测到的验证码来跟进此事。
生成基本验证码
验证码可以使用 Python 中的Claptcha包生成。我们用它来生成由数字和文本组成的四个字符的验证码图像。因此,每个字符可以是26字母和10数字中的任何一个。以下代码可用于生成随机选择字母和数字的验证码:
alphabets = 'abcdefghijklmnopqrstuvwxyz'
alphabets = alphabets.upper()
font = "/home/santanu/Android/Sdk/platforms/android-28/data/fonts/DancingScript-Regular.ttf"
# For each of the 4 characters determine randomly whether its a digit or alphabet
char_num_ind = list(np.random.randint(0,2,4))
text = ''
for ind in char_num_ind:
if ind == 1:
# for indicator 1 select character else number
loc = np.random.randint(0,26,1)
text = text + alphabets[np.random.randint(0,26,1)[0]]
else:
text = text + str(np.random.randint(0,10,1)[0])
c = Claptcha(text,font)
text,image = c.image
plt.imshow(image)
下面的截图(图 10.2 )是前面代码生成的随机验证码:
Figure 10.2: Random CAPTCHA with the characters 26UR
除了文本之外,Claptcha工具还要求输入打印文本的字体。正如我们所看到的,它以水平轴上有点扭曲的线条的形式给图像添加了噪声。
为训练验证码生成数据
在本节中,我们将使用Claptcha工具生成几个验证码来训练一个 CNN 模型。CNN 模型将通过监督训练来学习识别验证码中的字符。我们将生成一个训练和验证集,用于训练 CNN 模型。除此之外,我们还将生成一个单独的测试集来评估它概括未知数据的能力。CaptchaGenerator.py脚本可以编码如下,生成验证码数据:
from claptcha import Claptcha
import os
import numpy as np
import cv2
import fire
from elapsedtimer import ElasedTimer
def generate_captcha(outdir,font,num_captchas=20000):
alphabets = 'abcdefghijklmnopqrstuvwxyz'
alphabets = alphabets.upper()
try:
os.mkdir(outdir)
except:
'Directory already present,writing captchas to the same'
#rint(char_num_ind)
# select one alphabet if indicator 1 else number
for i in range(num_captchas):
char_num_ind = list(np.random.randint(0,2,4))
text = ''
for ind in char_num_ind:
if ind == 1:
loc = np.random.randint(0,26,1)
text = text + alphabets[np.random.randint(0,26,1)[0]]
else:
text = text + str(np.random.randint(0,10,1)[0])
c = Claptcha(text,font)
text,image = c.image
image.save(outdir + text + '.png')
def main_process(outdir_train,num_captchas_train,
outdir_val,num_captchas_val,
outdir_test,num_captchas_test,
font):
generate_captcha(outdir_train,font,num_captchas_train)
generate_captcha(outdir_val,font,num_captchas_val)
generate_captcha(outdir_test,font,num_captchas_test)
if __name__ == '__main__':
with ElasedTimer('main_process'):
fire.Fire(main_process)
需要注意的一点是,大多数验证码生成器使用ttf文件来获取验证码的字体模式。
我们可以使用CaptchaGenerator.py脚本生成大小为16000、4000和4000的训练集、验证集和测试集,如下所示:
python CaptchaGenerator.py --outdir_train '/home/santanu/Downloads/Captcha Generation/captcha_train/' --num_captchas_train 16000 --outdir_val '/home/santanu/Downloads/Captcha Generation/captcha_val/' --num_captchas_val 4000
--outdir_test '/home/santanu/Downloads/Captcha Generation/captcha_test/' --num_captchas_test 4000 --font "/home/santanu/Android/Sdk/platforms/android-28/data/fonts/DancingScript-Regular.ttf"
脚本用3.328 mins生成16000训练验证码、4000验证验证码、4000测试验证码,从下面的脚本日志可以看到:
3.328 min: main_process
在下一节中,我们将讨论验证码破解者的卷积神经网络架构。
验证码破解 CNN 架构
我们将使用美国有线电视新闻网的架构来识别验证码中的字符。美国有线电视新闻网在密集层之前会有两对卷积和汇集。我们不是将整个验证码输入网络,而是将验证码分成四个字符,并分别输入模型。这需要 CNN 的最终输出层预测属于26字母和10数字的36类之一。
通过函数_model_,可以定义如下代码所示的模型:
def _model_(n_classes):
# Build the neural network
input_ = Input(shape=(40,25,1))
# First convolutional layer with max pooling
x = Conv2D(20, (5, 5), padding="same",activation="relu")(input_)
x = MaxPooling2D(pool_size=(2, 2), strides=(2, 2))(x)
x = Dropout(0.2)(x)
# Second convolutional layer with max pooling
x = Conv2D(50, (5, 5), padding="same", activation="relu")(x)
x = MaxPooling2D(pool_size=(2, 2), strides=(2, 2))(x)
x = Dropout(0.2)(x)
# Hidden layer with 1024 nodes
x = Flatten()(x)
x = Dense(1024, activation="relu")(x)
# Output layer with 36 nodes (one for each possible alphabet/digit we predict)
out = Dense(n_classes,activation='softmax')(x)
model = Model(inputs=[input_],outputs=out)
model.compile(loss="sparse_categorical_crossentropy", optimizer="adam", metrics=
["accuracy"])
return model
验证码破解者 CNN 模型可以如下图所示进行图形化描述(图 10.3 ):
Figure 10.3: The CAPTCHA breaker CNN architecture
预处理验证码图像
图像的原始像素与美国有线电视新闻网的架构不太匹配。为了让 CNN 更快地收敛,对图像进行归一化总是一个好主意。通常用作归一化方案的两种方法是平均像素减法或通过将像素值除以255将像素缩放至位于[0,1]范围内。对于我们的美国有线电视新闻网,我们将正常化图像,使其位于[0,1]的范围内。我们还将处理验证码的灰度图像,这意味着我们将只处理一个颜色通道。load_img功能可用于加载和预处理验证码图片,如以下代码所示:
def load_img(path,dim=(100,40)):
img = cv2.imread(path,cv2.IMREAD_GRAYSCALE)
img = cv2.resize(img,dim)
img = img.reshape((dim[1],dim[0],1))
#print(img.shape)
return img/255.
将验证码字符转换为类
验证码的原始字符需要转换成数字类来进行训练。create_dict_char_to_index功能可用于将原始字符转换为类别标签:
def create_dict_char_to_index():
chars = 'abcdefghijklmnopqrstuvwxyz0123456789'.upper()
chars = list(chars)
index = np.arange(len(chars))
char_to_index_dict,index_to_char_dict = {},{}
for v,k in zip(index,chars):
char_to_index_dict[k] = v
index_to_char_dict[v] = k
return char_to_index_dict,index_to_char_dict
数据生成程序
动态生成成批的训练和验证数据对于以高效的方式训练 CNN 是很重要的。在训练开始之前将所有数据加载到内存中可能会导致数据存储问题,因此在训练期间动态读取验证码和构建批处理是有意义的。这导致资源的最佳利用。
我们将使用一个数据生成器,它可以用于构建训练和验证批次。生成器将在初始化期间存储验证码文件位置,并在每个时期动态构建批处理。文件的顺序在每个之后被随机打乱,使得验证码图像在每个时期不以相同的顺序遍历。这通常可以确保模型在训练过程中不会陷入糟糕的局部极小值。数据生成器类可以编码如下:
class DataGenerator(keras.utils.Sequence):
'Generates data for Keras'
def __init__(self,dest,char_to_index_dict,batch_size=32,n_classes=36,dim=(40,100,1),shuffle=True):
'Initialization'
self.dest = dest
self.files = os.listdir(self.dest)
self.char_to_index_dict = char_to_index_dict
self.batch_size = batch_size
self.n_classes = n_classes
self.dim = (40,100)
self.shuffle = shuffle
self.on_epoch_end()
def __len__(self):
'Denotes the number of batches per epoch'
return int(np.floor(len(self.files) / self.batch_size))
def __getitem__(self, index):
'Generate one batch of data'
# Generate indexes of the batch
indexes = self.indexes[index*self.batch_size:(index+1)*self.batch_size]
# Find list of files to be processed in the batch
list_files = [self.files[k] for k in indexes]
# Generate data
X, y = self.__data_generation(list_files)
return X, y
def on_epoch_end(self):
'Updates indexes after each epoch'
self.indexes = np.arange(len(self.files))
if self.shuffle == True:
np.random.shuffle(self.indexes)
def __data_generation(self,list_files):
'Generates data containing batch_size samples' # X :
(n_samples, *dim, n_channels)
# Initialization
dim_h = dim[0]
dim_w = dim[1]//4
channels = dim[2]
X = np.empty((4*len(list_files),dim_h,dim_w,channels))
y = np.empty((4*len(list_files)),dtype=int)
# print(X.shape,y.shape)
# Generate data
k = -1
for f in list_files:
target = list(f.split('.')[0])
target = [self.char_to_index_dict[c] for c in target]
img = load_img(self.dest + f)
img_h,img_w = img.shape[0],img.shape[1]
crop_w = img.shape[1]//4
for i in range(4):
img_crop = img[:,i*crop_w:(i+1)*crop_w]
k+=1
X[k,] = img_crop
y[k] = int(target[i])
return X,y
训练验证码破解者
验证码破解模型可以通过调用train函数进行训练,如下所示:
def train(dest_train,dest_val,outdir,batch_size,n_classes,dim,shuffle,epochs,lr):
char_to_index_dict,index_to_char_dict = create_dict_char_to_index()
model = _model_(n_classes)
train_generator = DataGenerator(dest_train,char_to_index_dict,batch_size,n_classes,dim,shuffle)
val_generator = DataGenerator(dest_val,char_to_index_dict,batch_size,n_classes,dim,shuffle)
model.fit_generator(train_generator,epochs=epochs,validation_data=val_generator)
model.save(outdir + 'captcha_breaker.h5')
对于批处理中的验证码,所有四个字符都被考虑用于训练。我们使用DataGenerator类定义train_generator和val_generator对象。这些数据生成器动态地为训练和验证提供批处理。
可以通过运行带有如下train参数的captcha_solver.py脚本来调用训练:
python captcha_solver.py train --dest_train '/home/santanu/Downloads/Captcha Generation/captcha_train/' --dest_val '/home/santanu/Downloads/Captcha Generation/captcha_val/' --outdir '/home/santanu/ML_DS_Catalog-/captcha/model/' --batch_size 16 --lr 1e-3 --epochs 20 --n_classes 36 --shuffle True --dim '(40,100,1)'
仅在20训练阶段,该模型就实现了验证码每个字符级别约 98.3%的验证准确率,如以下输出日志所示:
Epoch 17/20
1954/1954 [==============================] - 14s 7ms/step - loss: 0.0340 - acc: 0.9896 - val_loss: 0.0781 - val_acc: 0.9835
Epoch 18/20
1954/1954 [==============================] - 13s 7ms/step - loss: 0.0310 - acc: 0.9904 - val_loss: 0.0679 - val_acc: 0.9851
Epoch 19/20
1954/1954 [==============================] - 13s 7ms/step - loss: 0.0315 - acc: 0.9904 - val_loss: 0.0813 - val_acc: 0.9822
Epoch 20/20
1954/1954 [==============================] - 13s 7ms/step - loss: 0.0297 - acc: 0.9910 - val_loss: 0.0824 - val_acc: 0.9832
4.412 min: captcha_solver
The training time for 20 epochs with roughly 16000 98.3s (that is, 64000 CAPTCHA characters) is around 4.412 min using a GeForce GTX 1070 GPU. Readers are advised to use a GPU based machine for faster training.
测试数据集的准确性
测试数据的推断可以通过调用evaluate函数来运行。evaluate功能说明如下,供参考。请注意,评估应该从整体验证码的角度来看准确性,而不是验证码的字符级别。因此,只有当验证码目标的所有四个字符都与预测匹配时,我们才能将验证码标记为被美国有线电视新闻网正确识别。
在测试验证码上运行推理的evaluate功能可以编码如下:
def evaluate(model_path,eval_dest,outdir,fetch_target=True):
char_to_index_dict,index_to_char_dict = create_dict_char_to_index()
files = os.listdir(eval_dest)
model = keras.models.load_model(model_path)
predictions,targets = [],[]
for f in files:
if fetch_target == True:
target = list(f.split('.')[0])
targets.append(target)
pred = []
img = load_img(eval_dest + f)
img_h,img_w = img.shape[0],img.shape[1]
crop_w = img.shape[1]//4
for i in range(4):
img_crop = img[:,i*crop_w:(i+1)*crop_w]
img_crop = img_crop[np.newaxis,:]
pred_index = np.argmax(model.predict(img_crop),axis=1)
#print(pred_index)
pred_char = index_to_char_dict[pred_index[0]]
pred.append(pred_char)
predictions.append(pred)
df = pd.DataFrame()
df['files'] = files
df['predictions'] = predictions
if fetch_target == True:
match = []
df['targets'] = targets
accuracy_count = 0
for i in range(len(files)):
if targets[i] == predictions[i]:
accuracy_count+= 1
match.append(1)
else:
match.append(0)
print(f'Accuracy: {accuracy_count/float(len(files))} ')
eval_file = outdir + 'evaluation.csv'
df['match'] = match
df.to_csv(eval_file,index=False)
print(f'Evaluation file written at: {eval_file} ')
可以运行以下命令来调用captcha_solver.py脚本的evaluate功能进行推理:
python captcha_solver.py evaluate --model_path /home/santanu/ML_DS_Catalog-/captcha/model/captcha_breaker.h5 --eval_dest '/home/santanu/Downloads/Captcha Generation/captcha_test/' --outdir /home/santanu/ML_DS_Catalog-/captcha/ --fetch_target True
在4000验证码测试数据集上实现的准确率在 93%左右。运行evaluate功能的输出如下:
Accuracy: 0.9320972187421699
Evaluation file written at: /home/santanu/ML_DS_Catalog-/captcha/evaluation.csv
13.564 s: captcha_solver
我们还可以看到对那些4000验证码的推断花费了大约 14 秒,评估的输出被写入/home/santanu/ML_DS_Catalog-/captcha/evaluation.csv文件。
我们将在下面的截图中查看模型没有做好的一些目标和预测(图 10.4 ):
Figure 10.4: CAPTCHAs where the CAPTCHA solver model failed
通过对抗性学习生成验证码
在本节中,我们将通过生成性对抗网络创建验证码。我们将生成类似于街景门牌号数据集( SVHN 数据集)中的图像。这个想法是使用这些 GAN 生成的图像作为验证码。只有当我们训练了氮化镓,它们才容易从噪声分布中采样。这将减轻通过更复杂的方法创建验证码的需要。它还将为验证码中使用的 SVHN 街道号码提供一些变化。
SVHN 是一个真实世界的数据集,由于其在对象识别算法中的应用,在机器学习和深度学习领域非常受欢迎。顾名思义,该数据集包含从谷歌街景图像获得的门牌号真实图像。数据集可从以下链接下载:ufldl.stanford.edu/housenumber…。
我们将使用调整后的门牌号数据集,其中图像已经调整到尺寸(32,32)。我们感兴趣的数据集是train_32x32.mat。
通过这个生成对抗网络 ( GAN )我们将从随机噪声中生成门牌号图像,生成的图像将非常类似于 SVHN 数据集中的图像。
简单回顾一下,在一个 GAN 中,我们有一个生成器( G )和一个鉴别器( D ),它们根据损失函数相互进行零和极小极大游戏。随着时间的推移,发生器和鉴别器都在工作中变得更好,直到我们达到一个稳定的点,两者都不能进一步改善。这个稳定点是损失函数的鞍点。对于我们的应用,发电机 G 将把噪声 z 从给定的分布 P(z) 转换成门牌号图像 x ,使得 x = G(z) 。
该生成的图像通过鉴别器 D ,该鉴别器试图将该生成的图像 x 检测为假的,并且将来自 SVHN 数据集的真实门牌号图像检测为真实的。同时,生成器将尝试创建图像 x = G(z) ,使得鉴别器发现图像是真实的。如果我们将真实图像标记为1,将生成器生成的假图像标记为0,那么鉴别器将尝试最小化二进制交叉熵损失,作为给定两个类别的分类器网络。鉴频器 D 最小化的损耗可以写成:
在前面的表达式 *D(。)*是鉴别器函数,其输出表示将图像标记为真实的概率。 P z (z) 表示随机变量噪声 *z、*的分布,而 P X (x) 表示真实门牌号图像的分布。 *G(。)*和 *D(。)*分别表示发生器网络功能和鉴别器网络功能。这些将由网络的权重参数化,我们已经方便地跳过了杂乱的符号。如果我们用表示发电机网络权重的参数,用
表示鉴别器网络的参数,那么鉴别器将学会相对于
最小化 (1) 中的损耗,而发电机将致力于相对于
最大化 (1) 中的相同损耗。我们可以将 (1) 中优化的损耗称为发生器和鉴别器都在优化其参数的效用函数。效用函数 U 可以写成发生器和鉴别器参数的函数,如下所示:
从博弈论的角度来看,生成器 G 和鉴别器 D 用效用函数相互进行零和极小极大博弈,极小极大博弈的优化问题可以表示为:
在参数空间中的一个点上,如果一个函数对于某些参数是局部最大值,对于其余参数是局部最小值,那么这个点被称为鞍点。因此,给出的点将是效用函数
的鞍点。这个鞍点是极小极大零和博弈的纳什均衡,并且参数
对于生成器和鉴别器正在优化的效用来说是最优的。就眼前的问题而言,生成器 G 会产生最难识别的验证码,以
为参数进行检测。同样,鉴别器最适合检测以
为参数的假验证码。
有鞍点的函数中最简单的是 x 2 - y 2 ,鞍点就是原点:(0,0)。
优化氮化镓损耗
在前一节中,我们已经看到,发生器和鉴别器相对于其各自网络参数的最佳状态由下式给出:
为了使目标函数最大化,我们通常使用梯度上升,而为了使成本函数最小化,我们使用梯度下降。前面的优化问题可以分解为两部分:生成器和鉴别器分别通过梯度上升和梯度下降轮流优化效用函数。在优化期间的任何步骤 t 中,鉴别器将通过如下最小化效用来尝试移动到新的状态:
或者,发电机将尝试最大化相同的效用。由于鉴别器 D 没有发电机的任何参数,效用的第二项不影响发电机的优化。同样的情况可以表述如下:
我们已经将生成器和鉴别器优化目标转换为最小化问题。鉴别器和生成器的优化使用梯度下降执行,直到我们到达目标函数的鞍点。
发电机网络
生成器网络将接收随机噪声,并尝试输出类似于 SVHN 图像的图像作为输出。随机噪声是一个100维输入向量。每个维度都是遵循标准正态分布的随机变量,平均值为0,标准差为1。
最初的致密层有8192个单位,被重塑为一个 4×4×512 形状的三维张量。张量可以被认为是带有512滤镜的 4×4 图像。为了增加张量的空间维度,我们进行了一系列的转置 2D 卷积,步长为2,核滤波器大小为 5×5。步幅大小决定转置卷积的缩放比例。例如,2 倍于输入图像的每个空间维度的步幅,随后是转置卷积,通常伴随着批量归一化,以便更好地收敛。除激活层外,网络使用LeakyReLU作为激活功能。网络的最终输出是 32 x 32 x 3 维的图像。
在最终层中使用tanh激活,以便在[-1,1]范围内归一化图像像素值。
发生器可以编码如下:
def generator(input_dim,alpha=0.2):
model = Sequential()
model.add(Dense(input_dim=input_dim, output_dim=4*4*512))
model.add(Reshape(target_shape=(4,4,512)))
model.add(BatchNormalization())
model.add(LeakyReLU(alpha))
model.add(Conv2DTranspose(256, kernel_size=5, strides=2,
padding='same'))
model.add(BatchNormalization())
model.add(LeakyReLU(alpha))
model.add(Conv2DTranspose(128, kernel_size=5, strides=2,
padding='same'))
model.add(BatchNormalization())
model.add(LeakyReLU(alpha))
model.add(Conv2DTranspose(3, kernel_size=5, strides=2,
padding='same'))
model.add(Activation('tanh'))
return model
发电机的网络架构如下图所示(图 10.5 )供参考:
Figure 10.5: Generator network graph
鉴别网络
鉴别器将是一个经典的二进制分类卷积神经网络,它可以将生成器图像分类为假图像,并将实际的 SVHN 数据集图像分类为真实图像。
鉴别器网络可以编码如下:
def discriminator(img_dim,alpha=0.2):
model = Sequential()
model.add(
Conv2D(64, kernel_size=5,strides=2,
padding='same',
input_shape=img_dim)
)
model.add(LeakyReLU(alpha))
model.add(Conv2D(128,kernel_size=5,strides=2,padding='same'))
model.add(BatchNormalization())
model.add(LeakyReLU(alpha))
model.add(Conv2D(256,kernel_size=5,strides=2,padding='same'))
model.add(BatchNormalization())
model.add(LeakyReLU(alpha))
model.add(Flatten())
model.add(Dense(1))
model.add(Activation('sigmoid'))
return model
前一个代码块中定义的鉴别器网络将伪生成器图像和真实的 SVHN 图像作为输入,并在最终输出层之前通过3组 2D 卷积。该网络中的卷积之后不是池化,而是批量标准化和LeakyReLU激活。
鉴别器的网络架构如下图所示(图 10.6 ):
Figure 10.6: Discriminator network graph
鉴别器的输出激活函数是一个 sigmoid。这有助于从真实的 SVHN 图像中对伪生成图像进行二进制分类。
训练氮化镓
为生成性对抗网络建立训练流程并不简单,因为它需要大量的技术考虑。我们定义了如下三个培训网络:
- 发电机网络
g带参数 - 带参数的鉴别器网络
d - 由带有权重
和
的
g_d表示的组合发生器鉴别器网络
生成器g创建假图像,d鉴别器会评估这些图像,并试图将其标记为假图像。
在g_d网络中,g生成器创建假图像,然后试图欺骗d鉴别器相信它们是真实的。鉴别器网络是用二进制交叉熵损失编译的,并且损失是相对于鉴别器参数优化的,而
g_d网络是相对于g发生器的参数编译的,以便欺骗鉴别器。因此,
g_d网络损失是与鉴别器将所有假图像标记为真实图像相关的二进制交叉熵损失。在每个小批量中,发生器和鉴别器权重根据与g_d和d网络相关的损耗优化进行更新:
def train(dest_train,outdir,
gen_input_dim,gen_lr,gen_beta1,
dis_input_dim,dis_lr,dis_beta1,
epochs,batch_size,alpha=0.2,smooth_coef=0.1):
#X_train,X_test = read_data(dest_train),read_data(dest_test)
train_data = loadmat(dest_train + 'train_32x32.mat')
X_train, y_train = train_data['X'], train_data['y']
X_train = np.rollaxis(X_train, 3)
print(X_train.shape)
#Image pixels are normalized between -1 to +1 so that one can use the tanh activation function
#_train = (X_train.astype(np.float32) - 127.5)/127.5
X_train = (X_train/255)*2-1
g = generator(gen_input_dim,alpha)
plot_model(g,show_shapes=True, to_file='generator_model.png')
d = discriminator(dis_input_dim,alpha)
d_optim = Adam(lr=dis_lr,beta_1=dis_beta1)
d.compile(loss='binary_crossentropy',optimizer=d_optim)
plot_model(d,show_shapes=True, to_file='discriminator_model.png')
g_d = generator_discriminator(g, d)
g_optim = Adam(lr=gen_lr,beta_1=gen_beta1)
g_d.compile(loss='binary_crossentropy', optimizer=g_optim)
plot_model(g_d,show_shapes=True, to_file=
'generator_discriminator_model.png')
for epoch in range(epochs):
print("Epoch is", epoch)
print("Number of batches", int(X_train.shape[0]/batch_size))
for index in range(int(X_train.shape[0]/batch_size)):
noise =
np.random.normal(loc=0, scale=1, size=(batch_size,gen_input_dim))
image_batch = X_train[index*batch_size:(index+1)*batch_size,:]
generated_images = g.predict(noise, verbose=0)
if index % 20 == 0:
combine_images(generated_images,outdir,epoch,index)
# Images converted back to be within 0 to 255
print(image_batch.shape,generated_images.shape)
X = np.concatenate((image_batch, generated_images))
d1 = d.train_on_batch(image_batch,[1 - smooth_coef]*batch_size)
d2 = d.train_on_batch(generated_images,[0]*batch_size)
y = [1] * batch_size + [0] * batch_size
# Train the Discriminator on both real and fake images
make_trainable(d,True)
#_loss = d.train_on_batch(X, y)
d_loss = d1 + d2
print("batch %d d_loss : %f" % (index, d_loss))
noise =
np.random.normal(loc=0, scale=1, size=(batch_size,gen_input_dim))
make_trainable(d,False)
#d.trainable = False
# Train the generator on fake images from Noise
g_loss = g_d.train_on_batch(noise, [1] * batch_size)
print("batch %d g_loss : %f" % (index, g_loss))
if index % 10 == 9:
g.save_weights('generator', True)
d.save_weights('discriminator', True)
Adam优化器用于两个网络的优化。需要注意的一点是,需要编译网络g_d来优化发电机参数的损耗 G 。因此,我们需要在网络g_d中禁用鉴别器 D 参数的训练。
我们可以使用以下功能来禁用或启用网络参数的学习:
def make_trainable(model, trainable):
for layer in model.layers:
layer.trainable = trainable
我们可以通过将可训练设置为False来禁用参数的学习,而如果我们想要启用这些参数的训练,我们需要将其设置为True。
噪声分布
输入到 GAN 的噪声需要遵循特定的概率分布。一般均匀分布U[-1,1]或标准正态分布即均值为0且标准差为1的正态分布用于对噪声向量的每个维度进行采样。从经验上看,从标准正态分布中采样噪声似乎比从均匀分布中采样效果更好。在这个实现中,我们将使用标准正态分布对随机噪声进行采样。
数据预处理
如前所述,我们将使用维度32 x 32 x 3的 SVHN 数据集图像。
数据集图像很容易以矩阵数据形式获得。图像的原始像素在[-1,1]范围内归一化,以实现更快、更稳定的收敛。由于这种变换,发生器的最终激活被保持tanh以确保生成的图像具有在[-1,1]内的像素值。
read_data可用于处理输入数据。dir_flag用于确定我们是否有原始处理的数据矩阵文件或图像目录。例如,当我们使用 SVHN 数据集时,dir_flag应该设置为False,因为我们已经有了一个名为train_32x32.mat的预处理数据矩阵文件。
然而,最好保持read_data函数是通用的,因为这允许我们为其他数据集重用脚本。来自scipy.io的loadmat功能可用于读取train_32x32.mat。
如果输入的是放在目录中的原始图像,那么我们可以读取目录中可用的图像文件,并通过opencv读取它们。使用opencv可以使用load_img功能读取原始图像。
最后,为了更好地收敛网络,像素强度被归一化到[-1,1]的范围内:
def load_img(path,dim=(32,32)):
img = cv2.imread(path)
img = cv2.resize(img,dim)
img = img.reshape((dim[1],dim[0],3))
return img
def read_data(dest,dir_flag=False):
if dir_flag == True:
files = os.listdir(dest)
X = []
for f in files:
img = load_img(dest + f)
X.append(img)
return X
else:
train_data = loadmat(path)
X,y = train_data['X'], train_data['y']
X = np.rollaxis(X,3)
X = (X/255)*2-1
return X
调用培训
可以通过运行captcha_gan.py脚本的train功能调用 GAN 的训练,参数如下:
python captcha_gan.py train --dest_train '/home/santanu/Downloads/train_32x32.mat' --outdir '/home/santanu/ML_DS_Catalog-/captcha/SVHN/' --dir_flag False --batch_size 100 --gen_input_dim 100 --gen_beta1 0.5 --gen_lr 0.0001 --dis_input_dim '(32,32,3)' --dis_lr 0.001 --dis_beta1 0.5 --alpha 0.2 --epochs 100 --smooth_coef 0.1
前面的脚本使用fire Python 包来调用用户指定的函数,在我们的例子中是train。fire的好处在于,用户可以将函数的所有输入作为参数提供,正如我们从前面的命令中看到的那样。
众所周知,GANs 很难训练,因此需要调整这些参数,以便模型正常运行。以下是一些重要的参数:
| 参数 | 值 | comment |
| batch_size | 100 | 小批量随机梯度下降的批量。 |
| gen_input_dim | 100 | 输入随机噪声向量维数。 |
| gen_lr | 0.0001 | 发电机学习率。 |
| gen_beta1 | 0.5 | beta_1是生成器的 Adam 优化器的参数。 |
| dis_input_dim | (32,32,3) | 真假门牌号图像的形状鉴别器。 |
| dis_lr | 0.001 | 鉴别器网络的学习速率。 |
| dis_beta1 | 0.5 | beta_1是用于鉴别器的 Adam 优化器的参数。 |
| alpha | 0.2 | 这是LeakyReLU激活的泄漏因子。当activation函数的输入为负时,这有助于提供梯度(此处为0.2)。它有助于解决垂死的ReLU问题。如果输入小于或等于0,ReLU 函数的输出相对于其输入的梯度为0。来自后面层的反向传播误差乘以这个0,没有误差传递到前面的层,尽管与这个ReLU.相关的神经元据说已经死亡,许多这样的死亡ReLUs会影响训练。LeakyReLU通过即使对于负的输入值也提供小的梯度来克服这个问题,从而确保训练不会由于缺乏梯度而停止。 |
| epochs | 100 | 这是要运行的纪元数。 |
| smooth_coef | 0.1 | 这个平滑系数的设计是为了减少真实样本对鉴别器的损失。例如,0.1的smooth_coef会将归因于真实图像的损失减少到原始损失的 90%。这有助于 GANs 更好地收敛。 |
Training the GAN with these parameters takes around 3.12 hours, using a GeForce GTX 1070 GPU. Readers are advised to use a GPU for faster training.
培训期间验证码的质量
现在让我们研究一下在训练过程中不同时期生成的验证码的质量。以下为历元5(见图 10.7a )、历元51(见图 10.7b )和历元100(见图 10.7c 后的 CAPTCHAs 图像。我们可以看到验证码图像的质量随着训练的进行而提高。以下屏幕截图显示了在第 5 个时期生成的样本验证码的结果:
Figure 10.7a: Sample CAPTCHAs generated at epoch 5
以下屏幕截图显示了在第 51 个时期生成的样本验证码的结果:
Figure 10.7b: Sample CAPTCHAs generated at epoch 51
以下屏幕截图显示了在纪元 100 生成的样本验证码的结果:
Figure 10.7c: Sample CAPTCHAs generated at epoch 100
使用经过训练的生成器创建验证码以供使用
经过训练的 GAN 网络可以在运行时加载,以生成像验证码一样的街景房屋号码供使用。generate_captcha功能可以用来生成验证码使用,如下图所示:
def generate_captcha(gen_input_dim,alpha,
num_images,model_dir,outdir):
g = generator(gen_input_dim,alpha)
g.load_weights(model_dir + 'generator')
noise =
np.random.normal(loc=0, scale=1, size=(num_images,gen_input_dim))
generated_images = g.predict(noise, verbose=1)
for i in range(num_images):
img = generated_images[i,:]
img = np.uint8(((img+1)/2)*255)
img = Image.fromarray(img)
img.save(outdir + 'captcha_' + str(i) + '.png')
您可能想知道如何为这些生成的验证码创建标签,因为它们需要验证用户是人类还是机器人。这个想法很简单:发送未标记的验证码和一些标记的验证码,这样用户就不知道哪个验证码会被评估。一旦你有足够的标签生成验证码,把大多数标签作为实际标签,并使用它来评估。
通过调用以下命令,可以从captcha_gan.py脚本中调用generate_captcha功能:
python captcha_gan.py generate-captcha --gen_input_dim 100 --num_images 200 --model_dir '/home/santanu/ML_DS_Catalog-/captcha/' --outdir '/home/santanu/ML_DS_Catalog-/captcha/captcha_for_use/' --alpha 0.2
下面的截图(图 10.8 )描述了通过调用generate_captcha函数生成的一些验证码。我们可以看到这些图像足够体面,可以用作验证码:
Figure 10.8: Generated CAPTCHAs using the generator of the trained GAN network
摘要
至此,我们结束了这一章。与本章相关的所有代码都可以在 GitHub 链接中找到:https://GitHub . com/PacktPublishing/Intelligent-Projects-use-Python/tree/master/chapter 10。你现在会对深度学习如何影响验证码有一个公平的想法。在光谱的一端,我们可以看到带有深度学习人工智能应用程序的机器人可以多么容易地解决验证码。然而,在另一端,我们看到深度学习可以如何用于利用给定的数据集并从随机噪声中创建新的验证码。您可以利用深度学习,扩展本章中关于生成性对抗网络的技术知识,构建智能验证码生成系统。现在,我们来到这本书的结尾。我希望这九个基于人工智能的实际应用的旅程是丰富的。祝你一切顺利!