译者:飞龙
本文来自【ApacheCN 深度学习 译文集】,采用译后编辑(MTPE)流程来尽可能提升效率。
不要担心自己的形象,只关心如何实现目标。——《原则》,生活原则 2.3.c
六、智能推荐系统
互联网上拥有大量的数字信息,这对用户有效地访问项目构成了挑战。 推荐系统是信息过滤系统,该系统处理数字数据过载的问题,以根据用户的喜好,兴趣和行为,从先前的活动中推断出项目或信息。
在本章中,我们将介绍以下主题:
- 推荐系统介绍
- 基于潜在分解的协同过滤
- 使用深度学习进行潜在因子协同过滤
- 使用受限玻尔兹曼机(RBM)构建推荐系统
- 训练 RBM 的对比差异
- 使用 RBM 的协同过滤
- 使用 RBM 实现协同过滤应用
技术要求
读者应具有 Python 3 和人工智能的基础知识,才能完成本章中的项目。
什么是推荐系统?
推荐系统在当今世界无处不在。 无论是 Netflix 上的电影推荐还是亚马逊上的产品推荐,推荐器系统都产生了重大影响。 推荐系统可以大致分为基于内容的过滤系统,协同过滤系统和基于潜在因子的过滤推荐系统。 基于内容的过滤依赖于基于项目内容的手工编码特征。 根据用户对现有商品的评分方式,创建用户个人资料,并将用户提供的排名赋予这些商品:
图 6.1:基于内容的过滤插图
如上图所示(“图 6.1”),用户A
购买了名为深度学习和神经网络的书籍。 由于书籍人工智能的内容与这两本书相似,因此基于内容的推荐系统已将书籍人工智能推荐给用户A
。 如我们所见,在基于内容的筛选中,根据用户的偏好向用户推荐项目。 这不涉及其他用户如何评价这本书。
协同过滤尝试识别属于给定用户的相似用户,然后推荐相似用户喜欢,购买或给予很高评价的用户项目。 这通常称为用户-用户协同过滤。 相反的是找到与给定项目相似的项目,并向也高度喜欢,购买或评价其他类似项目的用户推荐这些项目。 这就是项目-项目协同过滤的名称:
图 6.2:项目-项目协同过滤插图
在上图(“图 6.2”)中,用户A
和用户B
在购买书本方面非常相似。 用户A
最近购买了书籍深度学习和神经网络。 由于用户B
与用户A
非常相似,因此用户-用户协作推荐系统也将这些图书推荐给用户B
。
基于潜在分解的推荐系统
基于潜在因子分解的过滤器推荐方法尝试通过分解评分来发现潜在特征,以表示用户和项目资料。 与基于内容的过滤特征不同,这些潜在特征不可解释,可以表示复杂的特征。 例如,在电影推荐系统中,潜在特征之一可能以特定比例表示幽默,悬念和浪漫的线性组合。 通常,对于已经评分的商品,用户i
对商品j
的评分r[ij]
可以表示为r[ij] = u[i]^T v[j]
。 其中u[i]
是基于潜在因子的用户配置文件向量,而v[i]
是基于相同潜在因子的项目向量 :
图 6.3:基于潜在因子的过滤图
上图(“图 6.3”)中说明了一种基于潜在因子的推荐方法,其中评级矩阵R[mxn]
已分解为用户配置文件矩阵U[mxk]
和项目配置文件矩阵P[nxk]
的转置的乘积,其中k
是模型的潜在因子。 基于这些配置文件,我们可以通过计算用户配置文件和项目配置文件的内部产品来推荐用户迄今为止尚未购买的项目。 内部产品给出了用户购买该产品时可能给出的暂定评分。
创建这些用户和商品资料的一种方法是,在以某种形式的平均值填充缺失值之后,对评分矩阵执行奇异值分解(SVD) 用户和项目(视情况而定)。 根据 SVD,评级矩阵R
可以分解如下:
我们可以将用户个人资料矩阵作为US^(1/2)
,然后将项目个人资料矩阵转置为 S^(1/2) V^T
形成潜在因子模型。 当在分级矩阵中缺少与用户未分级的电影相对应的条目时,您可能会遇到有关如何执行 SVD 的问题。 常见的方法是在执行 SVD 之前,通过用户的平均评分或总体评分的平均值来估计缺失的评分。
用于潜在因子协同过滤的深度学习
除了使用 SVD,您还可以利用深度学习方法来导出给定大小的用户和商品资料向量。
对于每个用户i
,您可以通过嵌入层定义用户向量u[i] ∈ R^k
。 同样,对于每个项目j
,您可以通过另一个嵌入层定义项目向量v[j] ∈ R^k
。 然后,用户i
对项目j
的评分r[ij]
可以表示为u[i], v[j]
的点积,如下所示:
您可以修改神经网络以为用户和项目添加偏见。 假设我们想要k
潜在分量,则m
用户的嵌入矩阵U
的大小将是mxk
。 类似地,n
项的嵌入矩阵V
的大小将为nxk
。
在“基于深度学习的潜在因子模型”部分中,我们将使用这种嵌入方法基于100K Movie Lens
数据集创建推荐系统。 数据集可以从https://grouplens.org/datasets/movielens/
下载。
我们将使用u1.base
作为训练数据集,并使用u1.test
作为保持测试数据集。
基于深度学习的潜在因子模型
“潜在协同过滤的深度学习”部分中讨论的基于深度学习的潜在因子模型可以如图 6.4 所示进行设计:
图 6.4:电影镜头 100 K 数据集上基于深度学习的潜在因子模型
user_ID
和movie_ID
从其相应的嵌入矩阵中提取用户和电影嵌入向量。 在该图中,embedding_1
代表用户 ID 的嵌入层,而embedding_2
代表电影 ID 的嵌入层。 在dot_1
层中执行用户嵌入向量和电影嵌入向量的点积,以输出评分(一到五个)。 定义模型的代码如下所示:
def model(max_users,max_movies,latent_factors):
user_ID = Input(shape=(1,))
movie_ID = Input(shape=(1,))
x = Embedding(max_users,latent_factors, input_length=1)(user_ID)
y = Embedding(max_movies,latent_factors, input_length=1)(movie_ID)
out = dot([x,y],axes=2,normalize=False)
out= Reshape((1,))(out)
model = Model(inputs=[user_ID,movie_ID],outputs=out)
print(model.summary())
return model
在前面的model
函数中,max_users
和max_movies
分别确定用户的大小和电影嵌入矩阵。 该模型的参数不过是用户和电影嵌入矩阵的组成部分。 因此,如果我们有m
个用户和n
个电影,并且我们选择了k
的潜在维度,那么我们就有mxk + nxk = (m + n) * k
个待学习参数。
数据处理函数可以编码如下:
data_dir = Path('/home/santanu/ML_DS_Catalog-/Collaborating Filtering/ml-100k/')
outdir = Path('/home/santanu/ML_DS_Catalog-/Collaborating Filtering/ml-100k/')
#Function to read data
def create_data(rating,header_cols):
data = pd.read_csv(rating,header=None,sep='\t')
#print(data)
data.columns = header_cols
return data
#Movie ID to movie name dict
def create_movie_dict(movie_file):
print(movie_file)
df = pd.read_csv(movie_file,sep='|', encoding='latin-1',header=None)
movie_dict = {}
movie_ids = list(df[0].values)
movie_name = list(df[1].values)
for k,v in zip(movie_ids,movie_name):
movie_dict[k] = v
return movie_dict
# Function to create training validation and test data
def train_val(df,val_frac=None):
X,y = df[['userID','movieID']].values,df['rating'].values
#Offset the ids by 1 for the ids to start from zero
X = X - 1
if val_frac != None:
X_train, X_test, y_train, y_val = train_test_split(X, y, test_size=val_frac,random_state=0)
return X_train, X_val, y_train, y_val
else:
return X,y
要注意的一件事是,已经从user_ID
和movie_ID
中都减去了1
,以确保 ID 从0
而不是1
开始,以便嵌入层可以正确地引用它们。
调用数据处理和训练的代码如下:
#Data processing and model training
train_ratings_df = create_data(f'{data_dir}/u1.base',['userID','movieID','rating','timestamp'])
test_ratings_df = create_data(f'{data_dir}/u1.test',['userID','movieID','rating','timestamp'])
X_train, X_val,y_train, y_val = train_val(train_ratings_df,val_frac=0.2)
movie_dict = create_movie_dict(f'{data_dir}/u.item')
num_users = len(train_ratings_df['userID'].unique())
num_movies = len(train_ratings_df['movieID'].unique())
print(f'Number of users {num_users}')
print(f'Number of movies {num_movies}')
model = model(num_users,num_movies,40)
plot_model(model, to_file='model_plot.png', show_shapes=True, show_layer_names=True)
model.compile(loss='mse',optimizer='adam')
callbacks = [EarlyStopping('val_loss', patience=2),
ModelCheckpoint(f'{outdir}/nn_factor_model.h5', save_best_only=True)]
model.fit([X_train[:,0],X_train[:,1]], y_train, nb_epoch=30, validation_data=([X_val[:,0],X_val[:,1]], y_val), verbose=2, callbacks=callbacks)
该模型已设置为存储有关验证误差的最佳模型。 从训练日志中可以看出,该模型收敛于大约0.8872
的验证 RMSE,如下所示:
Train on 64000 samples, validate on 16000 samples
Epoch 1/30
- 4s - loss: 8.8970 - val_loss: 2.0422
Epoch 2/30
- 3s - loss: 1.3345 - val_loss: 1.0734
Epoch 3/30
- 3s - loss: 0.9656 - val_loss: 0.9704
Epoch 4/30
- 3s - loss: 0.8921 - val_loss: 0.9317
Epoch 5/30
- 3s - loss: 0.8452 - val_loss: 0.9097
Epoch 6/30
- 3s - loss: 0.8076 - val_loss: 0.8987
Epoch 7/30
- 3s - loss: 0.7686 - val_loss: 0.8872
Epoch 8/30
- 3s - loss: 0.7260 - val_loss: 0.8920
Epoch 9/30
- 3s - loss: 0.6842 - val_loss: 0.8959
现在,我们在看不见的测试数据集上评估模型的表现。 可以调用以下代码对测试数据集进行推理:
#Evaluate on the test dataset
model = load_model(f'{outdir}/nn_factor_model.h5')
X_test,y_test = train_val(test_ratings_df,val_frac=None)
pred = model.predict([X_test[:,0],X_test[:,1]])[:,0]
print('Hold out test set RMSE:',(np.mean((pred - y_test)**2)**0.5))
pred = np.round(pred)
test_ratings_df['predictions'] = pred
test_ratings_df['movie_name'] = test_ratings_df['movieID'].apply(lambda x:movie_dict[x])
从日志中可以看到,保持测试 RMSE 在0.95
附近:
Hold out test set RMSE: 0.9543926404313371
现在,我们通过调用以下代码行,为测试数据集中 ID 为1
的用户评估模型的表现:
#Check evaluation results for the UserID = 1
test_ratings_df[test_ratings_df['userID'] == 1].sort_values(['rating','predictions'],ascending=False)
从以下结果(“图 6.5”)可以看出,该模型在预测训练期间看不到的电影的收视率方面做得很好:
图 6.5:用户 ID 1 的评估结果
可以在这个页面上找到与深度学习方法潜在因子方法相关的代码。
SVD++
通常,SVD 不会捕获用户和数据中可能存在的项目偏差。 一种名为 SVD++ 的方法考虑了潜在分解因子方法中的用户和项目偏见,并且在诸如 Netflix Challenge 之类的比赛中非常流行。
进行基于潜在因子推荐的最常见方法是将用户配置文件定义为u[i] ∈ R^k
和b[i] ∈ R
,项目轮廓和偏差为v[i] ∈ R^k
和b[j] ∈ R
。 然后,将用户i
对项目j
提供的评分r_hat[ij]
定义如下:
µ
是所有评分的总体平均值。
然后,通过在预测由用户评分的所有物品的评分时,通过最小化误差平方和来确定用户概况和物品概况。 要优化的误差平方误差可以表示为:
I[ij]
是一种指标函数,如果用户i
具有额定项目j
,则该函数为 1;否则为零。
相对于用户参数和项目资料,成本最小化。 通常,这种优化会导致过拟合,因此,将用户的规范和物料配置文件用作成本函数的正则化,如下所示:
这里,λ[1]
和λ[2]
是正则化常数。 通常,一种流行的梯度下降技术称为交替最小二乘(ALS)用于优化,该技术通过保持项目参数固定来交替更新用户配置文件参数,反之亦然。
surprise
包具有 SVD++ 的良好实现。 在下一部分中,我们将在100K movie lens
数据集上使用 SVD++ 训练模型,并查看表现指标。
MovieLens 100k 数据集上的 SVD++ 训练模型
可以使用以下命令通过conda
下载surprise
包:
conda install -c conda-forge scikit-surprise
对应于 SVD++ 的算法在surprise
中被命名为SVDpp
。 我们可以按以下方式加载所有必需的包:
import numpy as np
from surprise import SVDpp # SVD++ algorithm
from surprise import Dataset
from surprise import accuracy
from surprise.model_selection import cross_validate
from surprise.model_selection import train_test_split
可以下载100K Movie lens
数据集,并使用surprise
中的Dataset.load_builtin
工具将其提供给代码。 我们将数据分为80
与20
比率的训练和保持测试集。 数据处理代码行如下:
# Load the movie lens 10k data and split the data into train test files(80:20)
data = Dataset.load_builtin('ml-100k')
trainset, testset = train_test_split(data, test_size=.2)
接下来,我们将对数据进行5
折叠交叉验证,并查看交叉验证结果。 我们为随机梯度下降选择了0.008
的学习率。 为了防止过拟合,我们为 L1 和 L2 正则化选择了正则化常数0.1
。 这些代码行的详细信息如下:
#Perform 5 fold cross validation with all data
algo = SVDpp(n_factors=40, n_epochs=40, lr_all=0.008, reg_all=0.1)
# Run 5-fold cross-validation and show results summary
cross_validate(algo,data, measures=['RMSE', 'MAE'], cv=5, verbose=True)
交叉验证的结果如下:
Evaluating RMSE, MAE of algorithm SVDpp on 5 split(s). Fold 1 Fold 2 Fold 3 Fold 4 Fold 5 Mean Std RMSE (testset) 0.9196 0.9051 0.9037 0.9066 0.9151 0.9100 0.0062 MAE (testset) 0.7273 0.7169 0.7115 0.7143 0.7228 0.7186 0.0058 Fit time 374.57 374.58 369.74 385.44 382.36 377.34 5.72 Test time 2.53 2.63 2.74 2.79 2.84 2.71 0.11
从前面的结果可以看出,模型的5 fold cv RMSE
是0.91
。 在Movie Lens 100K
数据集上,结果令人印象深刻。
现在,我们将仅在训练数据集trainset
上训练模型,然后在测试集上评估模型。 相关代码行如下:
model = SVDpp(n_factors=40, n_epochs=10, lr_all=0.008, reg_all=0.1)
model.fit(trainset)
训练完模型后,我们将在保留测试数据集测试集中评估模型。 相关代码行如下:
#validate the model on the testset
pred = model.test(testset)
print("SVD++ results on the Test Set")
accuracy.rmse(pred, verbose=True)
验证的输出如下:
SVD++ results on the test set
RMSE: 0.9320
从前面的结果可以看出,SVD++ 模型在 RMSE 为0.93
的测试数据集上确实表现良好。 结果与我们在此之前训练的基于深度学习的模型潜在因子模型(支持0.95
的 RMSE)相当。
在“用于推荐的受限玻尔兹曼机”部分中,我们将介绍用于构建推荐系统的受限玻尔兹曼机。 由于该方法可以扩展到大型数据集,因此在协同过滤中获得了很大的普及。 协同过滤域中的大多数数据集都很稀疏,从而导致困难的非凸优化问题。 与其他分解方法(例如 SVD)相比,RBM 在数据集中更不容易遭受此稀疏问题的困扰。
用于推荐的受限玻尔兹曼机
受限玻尔兹曼机是一类属于无监督学习技术的神经网络。 众所周知,受限玻尔兹曼机(RBM)试图通过将输入数据投影到隐藏层中来学习数据的隐藏结构。
隐藏层激活有望对输入信号进行编码并重新创建。 受限制的玻尔兹曼机通常可处理二进制数据:
图 6.6:用于二进制数据的受限玻尔兹曼机
只是为了刷新我们的记忆,上图(“图 6.6”)是一个 RBM,具有m
输入或可见单元。 这被投影到具有n
个单元的隐藏层。 给定可见层输入v[i]
,则隐藏单元彼此独立,因此可以如下进行采样,其中σ(·)
表示 Sigmoid 函数:
类似地,给定隐藏层激活h = {h[j]}, j = 1 -> n
,可见层单元是独立的,可以如下进行采样:
RBM 的参数是可见层单元i
与隐藏层单元之间的广义权重连接w[ij] ∈ w[mxn]
,在可见单元i
处的偏差c[i] ∈ b
和隐藏层单元j
处的偏差c[j] ∈ c
。
通过最大化可见输入数据的可能性来学习 RBM 的这些参数。 如果我们用θ = [W; b; c]
表示组合的参数集,并且有一组T
训练输入数据点,则在 RBM 中,我们尝试使似然函数最大化:
通常,我们不使用乘积形式,而是最大化似然对数,或最小化对数似然的负值,以使函数在数学上更加方便。 如果我们将对数似然的负数表示为成本函数C
,则:
通常通过梯度下降使成本函数最小化。 成本函数相对于参数的梯度由期望项组成,并表示为:
项E[.]
表示对隐藏和可见单元的联合概率分布的任何给定数量的期望。 另外,h_hat
表示给定可见单元v
的采样的隐藏层输出。 在梯度下降的每次迭代中计算联合概率分布的期望在计算上是棘手的。 我们将采用下一节中讨论的称为对比发散的智能方法来计算期望值。
对比散度
计算联合概率分布的期望值的一种方法是通过吉布斯采样从联合概率分布中生成很多样本,然后将样本的平均值作为期望值。 在吉布斯抽样中,可以以其余变量为条件对联合概率分布中的每个变量进行抽样。 由于可见单元是独立的,因此给定隐藏单元,反之亦然,因此您可以将隐藏单元采样为h_bar <- P(h/v)
,然后将可见单元激活给定隐藏单元为v_bar = P(v/h = h_bar)
。 然后我们可以将样本(v_bar, h_bar)
作为从联合概率分布中抽取的样本。 这样,我们可以生成大量样本,例如M
,并取其平均值来计算期望的期望值。 但是,在梯度下降的每个步骤中进行如此大量的采样将使训练过程变得令人无法接受的缓慢,因此,与其在梯度下降的每个步骤中计算许多样本的平均值,不如从联合概率中生成一个样本,它应当表示整个联合概率分布上的所需期望:
图 6.7:对比散度图
如上图所示(“图 6.7”),我们从可见的输入v^(t)
开始,并根据条件概率分布P(h / v = v^(t))
采样隐藏层激活h'
。 再次,使用条件概率分布P(v / h = h')
,我们对v'
进行采样。 根据条件概率分布P(h / v = v')
对隐藏单元的下一次采样给出h_bar
,然后使用P(v/h = h_bar)
对可见单元激活进行采样,将提供给我们v_bar
。 对于v
和h
,即P(v, h/Θ)
的整个联合概率分布,将样本(v_bar, h_bar)
取为代表性样本。 相同的用于计算包含v
和h
的任何表达式的期望。 这种采样过程称为对比散度。
从可见输入开始,然后从条件分布P(v / h)
和P(v / h)
连续采样构成吉布斯采样的一个步骤,并为我们提供了联合分布中的样本v / h
。 代替在吉布斯采样的每个步骤中选择样本v / h
,我们可以选择从条件概率分布中进行几次连续的采样迭代后再选择样本。 如果在吉布斯采样的k
个步骤之后,选择了代表元素,则将对比散度称为CD-k
。 “图 6.7”中所示的对比散度可以称为CD-2
,因为我们是在经过两步吉布斯采样后才选择样本的。
使用 RBM 的协同过滤
提出建议时,可以使用受限玻尔兹曼机进行协同过滤。 我们将使用这些 RBM 向用户推荐电影。 使用不同用户为不同电影提供的分级来训练他们。 用户不会观看或评价所有电影,因此可以使用此训练模型来向用户推荐未看过的电影。
我们应该首先遇到的一个问题是如何处理 RBM 中的评分,因为评分本质上是有序的,而 RBM 则针对二进制数据。 可以将评分视为二进制数据,表示评分的单元数量等于每个评分的唯一值的数量。 例如:在评级系统中,评分从 1 到 5 不等,并且将有 5 个二进制单元,其中与评分相对应的 1 个单元设置为 1,其余单元设置为 0。 RBM 可见的单元将是为用户提供给不同电影的评分。 如所讨论的,每个评分将以二进制表示,并且对于每个可见单元,来自所有二进制可见单元的权重连接都与电影评分相对应。 由于每个用户将为一组不同的电影评分,因此每个用户的输入将不同。 但是,从电影分级单元到隐藏单元的权重连接对于所有用户而言都是通用的。
下图所示(“图 6.8a”和“图 6.8b”)是用户A
和用户B
的 RBM 视图。 用户A
和用户B
为一组不同的电影评分。 但是,正如我们所看到的,每个用户到每部电影中隐藏单元的权重连接都相同。 关于用户A
的 RBM 评分如下:
图 6.8a:用于协同过滤的 RBM 用户 A 视图
相对于用户B
的 RBM 评分如下:
图 6.8b:用于协同过滤的 RBM 用户 B 视图
还有一点要注意的是,如果有M
个电影,并且每个电影都有k
评分,那么 RBM 的可见单元数是M * k
。 此外,如果二进制隐藏单元的数量为n
,则W
中的权重连接数等于M * k * n
。 给定可见层输入,每个隐藏单元h[j]
可以独立于其他隐藏单元进行采样,如下所示:
此处,m = M * k
。
与传统的 RBM 不同,在给定隐藏层激活的情况下,无法独立采样该网络可见层中的二进制单元。 相对于电影的评分,每个k
个二进制单元通过 k 路 softmax 激活函数进行绑定。 如果给定隐藏单元的特定电影的可见单元的输入为s[i1], s[i2], ..., s[il], ..., s[ik]
,则电影i
的评分l
的一般输入计算如下:
在这里,(i - 1) * k + 1
是电影i
的可见单元对评分l
的索引。 同样,可以根据 soft-max 函数给出的概率对任何特定电影的可见单元进行采样,如下所示:
在定义隐藏单元和可见单元的输出时,还有一件重要的事情是需要概率抽样,而不是将输出默认为具有最大概率的抽样。 如果给定可见单元的隐藏单元激活的概率为P
,则统一生成[0, 1]
范围内的随机数r
,并且如果P > r
,则隐藏单元激活设置为true
。 该方案将确保在很长的一段时间内以概率P
将激活设置为true
。 类似地,电影的可见单元是根据跨国公司发行给定隐藏单元的概率从跨国发行机构中采样的。 因此,如果对于特定电影,给定的隐藏单元激活为(p[1], p[2], p[3], p[4], p[5])
,然后可以从多项式分布中抽样选择五个评分中的评分值, 其概率质量函数如下:
这里:
现在,我们具备了创建用于协同过滤的受限玻尔兹曼机所需的所有技术知识。
使用 RBM 的协同过滤实现
在接下来的几节中,我们将使用前一部分介绍的技术原理,使用受限玻尔兹曼机实现协同过滤系统。 我们将使用的数据集是 MovieLens 100K 数据集,其中包含用户对不同电影提供的评分(从 1 到 5)。 可以从这里下载数据集。
此协同过滤系统的 TensorFlow 实现在接下来的几节中介绍。
处理输入
每行中的输入评级文件记录包含字段userId
,movieId
,rating
和timestamp
。 我们处理每条记录以创建numpy
数组形式的训练文件,其中三个维度分别与userId
,movieId
和rating
有关。 从 1 到 5 的评级是一热编码的,因此沿评级维度的长度为 5。 我们使用 80% 的输入记录创建训练数据,而其余 20% 保留用于测试。 用户已评分的电影数量为1682
。 训练文件包含943
用户,因此训练数据的维度为(943,1682,5)
。 训练文件中的每个用户都是 RBM 的训练记录,其中将包含用户已评分的几部电影和用户尚未评分的几部电影。 一些电影分级也已删除,将包含在测试文件中。 将对 RBM 进行可用分级的训练,在隐藏单元中捕获输入数据的隐藏结构,然后尝试从捕获的隐藏结构中为每个用户重建所有电影的输入分级。 我们还创建了两个字典,以将实际电影 ID 的交叉引用及其索引存储在训练/测试数据集中。 以下是用于创建训练和测试文件的详细代码:
"""
@author: santanu
"""
import numpy as np
import pandas as pd
import argparse
'''
Ratings file preprocessing script to create training and hold out test datasets
'''
def process_file(infile_path):
infile = pd.read_csv(infile_path,sep='\t',header=None)
infile.columns = ['userId','movieId','rating','timestamp']
users = list(np.unique(infile.userId.values))
movies = list(np.unique(infile.movieId.values))
test_data = []
ratings_matrix = np.zeros([len(users),len(movies),5])
count = 0
total_count = len(infile)
for i in range(len(infile)):
rec = infile[i:i+1]
user_index = int(rec['userId']-1)
movie_index = int(rec['movieId']-1)
rating_index = int(rec['rating']-1)
if np.random.uniform(0,1) < 0.2 :
test_data.append([user_index,movie_index,int(rec['rating'])])
else:
ratings_matrix[user_index,movie_index,rating_index] = 1
count +=1
if (count % 100000 == 0) & (count>= 100000):
print('Processed ' + str(count) + ' records out of ' + str(total_count))
np.save(path + 'train_data',ratings_matrix)
np.save(path + 'test_data',np.array(test_data))
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('--path',help='input data path')
parser.add_argument('--infile',help='input file name')
args = parser.parse_args()
path = args.path
infile = args.infile
process_file(path + infile)
训练文件是大小为mxnxk
的numpy
数组对象,其中m
是用户总数,n
是电影总数,并且k
是离散额定值的数量(一到五个)。 要构建测试集,我们从训练数据集中随机选择 20% 的m x n
评分条目。 因此,测试集评级样本的所有k
评级值在训练数据集中均标记为零。 在测试集中,我们不会将数据扩展为三维 numpy 数组格式,因此可以将其用于训练。 相反,我们只将userid
,movieid
和分配的评分保存在三列中。 请注意,存储在训练中的userid
和movieid
和测试文件不是原始评级数据文件u.data
中的实际 ID。 它们被1
偏移以适应从0
而非1
开始的 Python 和numpy
索引
以下命令可用于调用数据预处理脚本:
python preprocess_ratings.py --path '/home/santanu/ML_DS_Catalog-/Collaborating Filtering/ml-100k/' --infile 'u.data'
建立用于协同过滤的 RBM 网络
以下函数_network
为协同过滤创建所需的 RBM 结构。 首先,我们定义输入的权重,偏差和占位符。 然后定义sample_hidden
和sample_visible
函数,以根据概率分别对隐藏的激活和可见的激活进行采样。 隐藏的单元是从 Sigmoid 函数提供的概率从伯努利分布中采样的,而与每个电影有关的可见单元是根据 softmax 函数提供的概率从多项分布中采样的。 无需创建 softmax 概率,因为tf.multinomial
函数可以直接从对率采样,而不是实际概率。
接下来,我们根据吉布斯采样定义对比差异的逻辑。 gibbs_step
函数执行吉布斯采样的一个步骤,然后利用它来实现k
阶的对比散度。
现在我们拥有所有必需的函数,我们将创建 TensorFlow 操作,以在给定可见输入的情况下对隐藏状态self.h
进行采样,并在给定采样状态下对可见单元self.x
进行采样。 我们还使用对比散度从v
和h
的联合概率分布(即P(v,h/model)
)中抽取(self.x_s,self.h_s)
作为代表性样本,以计算梯度中的不同期望项。
_network
函数的最后一步基于梯度更新 RBM 模型的权重和偏差。 如我们先前所见,梯度基于给定可见层输入的隐藏层激活self.h
以及通过对比发散得出的联合概率分布P(v,h/model)
的代表性样本(self.x_s,self.h_s)
。
TensorFlow ops self.x_
指的是给定隐藏层激活self.h
的可见层激活,在推断过程中将有用以推导尚未被每个用户评级的电影的评级:
def __network(self):
self.x = tf.placeholder(tf.float32, [None,self.num_movies,self.num_ranks], name="x")
self.xr = tf.reshape(self.x, [-1,self.num_movies*self.num_ranks], name="xr")
self.W = tf.Variable(tf.random_normal([self.num_movies*self.num_ranks,self.num_hidden], 0.01), name="W")
self.b_h = tf.Variable(tf.zeros([1,self.num_hidden], tf.float32, name="b_h"))
self.b_v = tf.Variable(tf.zeros([1,self.num_movies*self.num_ranks],tf.float32, name="b_v"))
self.k = 2
## Converts the probability into discrete binary states i.e. 0 and 1
def sample_hidden(probs):
return tf.floor(probs + tf.random_uniform(tf.shape(probs), 0, 1))
def sample_visible(logits):
logits = tf.reshape(logits,[-1,self.num_ranks])
sampled_logits = tf.multinomial(logits,1)
sampled_logits = tf.one_hot(sampled_logits,depth = 5)
logits = tf.reshape(logits,[-1,self.num_movies*self.num_ranks])
print(logits)
return logits
## Gibbs sampling step
def gibbs_step(x_k):
# x_k = tf.reshape(x_k,[-1,self.num_movies*self.num_ranks])
h_k = sample_hidden(tf.sigmoid(tf.matmul(x_k,self.W) + self.b_h))
x_k = sample_visible(tf.add(tf.matmul(h_k,tf.transpose(self.W)),self.b_v))
return x_k
## Run multiple gives Sampling step starting from an initital point
def gibbs_sample(k,x_k):
for i in range(k):
x_k = gibbs_step(x_k)
# Returns the gibbs sample after k iterations
return x_k
# Constrastive Divergence algorithm
# 1\. Through Gibbs sampling locate a new visible state x_sample based on the current visible state x
# 2\. Based on the new x sample a new h as h_sample
self.x_s = gibbs_sample(self.k,self.xr)
self.h_s = sample_hidden(tf.sigmoid(tf.matmul(self.x_s,self.W) + self.b_h))
# Sample hidden states based given visible states
self.h = sample_hidden(tf.sigmoid(tf.matmul(self.xr,self.W) + self.b_h))
# Sample visible states based given hidden states
self.x_ = sample_visible(tf.matmul(self.h,tf.transpose(self.W)) + self.b_v)
# The weight updated based on gradient descent
#self.size_batch = tf.cast(tf.shape(x)[0], tf.float32)
self.W_add = tf.multiply(self.learning_rate/self.batch_size,tf.subtract(tf.matmul(tf.transpose(self.xr),self.h),tf.matmul(tf.transpose(self.x_s),self.h_s)))
self.bv_add = tf.multiply(self.learning_rate/self.batch_size, tf.reduce_sum(tf.subtract(self.xr,self.x_s), 0, True))
self.bh_add = tf.multiply(self.learning_rate/self.batch_size, tf.reduce_sum(tf.subtract(self.h,self.h_s), 0, True))
self.updt = [self.W.assign_add(self.W_add), self.b_v.assign_add(self.bv_add), self.b_h.assign_add(self.bh_add)]
可以使用如下所示的read_data
函数在训练和推理期间读取来自预处理步骤的数据:
def read_data(self):
if self.mode == 'train':
self.train_data = np.load(self.train_file)
self.num_ranks = self.train_data.shape[2]
self.num_movies = self.train_data.shape[1]
self.users = self.train_data.shape[0]
else:
self.train_df = pd.read_csv(self.train_file)
self.test_data = np.load(self.test_file)
self.test_df = pd.DataFrame(self.test_data,columns=['userid','movieid','rating'])
if self.user_info_file != None:
self.user_info_df = pd.read_csv(self.user_info_file,sep='|',header=None)
self.user_info_df.columns=['userid','age','gender','occupation','zipcode']
if self.movie_info_file != None:
self.movie_info_df = pd.read_csv(self.movie_info_file,sep='|',encoding='latin-1',header=None)
self.movie_info_df = self.movie_info_df[[0,1]]
self.movie_info_df.columns = ['movieid','movie Title']
同样,在推理过程中,我们将与所有测试文件和测试文件一起(所有代码和评分均被读入)读入预测文件 CSV(在先前代码的推理部分中为self.train_file
,在此处)。 一旦训练了模型,便执行预测。 由于我们已经在训练后预测了收视率,因此在推断时间内我们要做的就是将收视率预测信息与测试文件的实际收视率信息相结合(后面的train
和inference
部分中有更多详细信息)。 另外,我们从用户和电影元数据文件中读取信息以供以后使用。
训练 RBM
此处说明的_train
函数可用于训练 RBM。 在此函数中,我们首先调用_network
函数以构建 RBM 网络结构,然后在激活的 TensorFlow 会话中针对指定周期数训练模型。 使用 TensorFlow 的saver
函数以指定的时间间隔保存模型:
def _train(self):
self.__network()
# TensorFlow graph execution
with tf.Session() as sess:
self.saver = tf.train.Saver()
#saver = tf.train.Saver(write_version=tf.train.SaverDef.V2)
# Initialize the variables of the Model
init = tf.global_variables_initializer()
sess.run(init)
total_batches = self.train_data.shape[0]//self.batch_size
batch_gen = self.next_batch()
# Start the training
for epoch in range(self.epochs):
if epoch < 150:
self.k = 2
if (epoch > 150) & (epoch < 250):
self.k = 3
if (epoch > 250) & (epoch < 350):
self.k = 5
if (epoch > 350) & (epoch < 500):
self.k = 9
# Loop over all batches
for i in range(total_batches):
self.X_train = next(batch_gen)
# Run the weight update
#batch_xs = (batch_xs > 0)*1
_ = sess.run([self.updt],feed_dict={self.x:self.X_train})
# Display the running step
if epoch % self.display_step == 0:
print("Epoch:", '%04d' % (epoch+1))
print(self.outdir)
self.saver.save(sess,os.path.join(self.outdir,'model'),
global_step=epoch)
# Do the prediction for all users all items irrespective of whether they
have been rated
self.logits_pred = tf.reshape(self.x_,
[self.users,self.num_movies,self.num_ranks])
self.probs = tf.nn.softmax(self.logits_pred,axis=2)
out = sess.run(self.probs,feed_dict={self.x:self.train_data})
recs = []
for i in range(self.users):
for j in range(self.num_movies):
rec = [i,j,np.argmax(out[i,j,:]) +1]
recs.append(rec)
recs = np.array(recs)
df_pred = pd.DataFrame(recs,columns=
['userid','movieid','predicted_rating'])
df_pred.to_csv(self.outdir + 'pred_all_recs.csv',index=False)
print("RBM training Completed !")
在前面的函数中要强调的重要一点是使用自定义next_batch
函数创建随机批量。 该函数在下面的代码片段中定义,并且用于定义迭代器batch_gen
,该迭代器可以由next
方法调用以检索下一个小批量:
def next_batch(self):
while True:
ix = np.random.choice(np.arange(self.data.shape[0]),self.batch_size)
train_X = self.data[ix,:,:]
yield train_X
需要注意的一件事是,在训练结束时,我们会预测所有用户对所有电影的收视率,无论它们是否被评级。 具有最高可能性的评级,将从五个可能的评级(即从 1 到 5)中给出最终评级。 由于在 Python 中,索引从零开始,因此在使用argmax
获得最高概率的位置之后,我们加一以获得实际评分。 因此,在训练结束时,我们有一个pred_all_recs.csv
文件,其中包含所有训练和测试记录的预测评分。 请注意,测试记录已嵌入训练记录中,并且评分的所有索引(从 1 到 5)都设置为零。
但是,一旦我们从用户观看过的电影的隐藏表示中充分训练了模型,就可以学习从用户未看过的电影中生成评分。
可以通过调用以下命令来训练模型:
python rbm.py main_process --mode train --train_file '/home/santanu/ML_DS_Catalog-/Collaborating Filtering/ml-100k/train_data.npy' --outdir '/home/santanu/ML_DS_Catalog-/Collaborating Filtering/' --num_hidden 5 --epochs 1000
从日志中可以看到,仅使用5
隐藏层训练1000
周期的模型大约需要52
秒。
RBM training Completed !
52.012 s: process RBM
请注意,受限玻尔兹曼机网络已在配备 GeForce Zotac 1070 GPU 和 64 GB RAM 的 Ubuntu 机器上进行了训练。 训练时间可能会根据用于训练网络的系统而有所不同。
将训练后的 RBM 用于推理
鉴于我们已经在训练过程中生成了带有所有预测的文件pred_all_recs.csv
,因此针对 RBM 的推理非常简单。 我们要做的只是基于提供的测试文件从pred_all_recs.csv
中提取测试记录。 另外,我们通过将1
添加到它们的当前值来求助于原始的userid
和movieid
。 返回原始 ID 的目的是能够从u.user
和u.item
文件中添加用户和电影信息。
推理块如下:
def inference(self):
self.df_result = self.test_df.merge(self.train_df,on=['userid','movieid'])
# in order to get the original ids we just need to add 1
self.df_result['userid'] = self.df_result['userid'] + 1
self.df_result['movieid'] = self.df_result['movieid'] + 1
if self.user_info_file != None:
self.df_result.merge(self.user_info_df,on=['userid'])
if self.movie_info_file != None:
self.df_result.merge(self.movie_info_df,on=['movieid'])
self.df_result.to_csv(self.outdir + 'test_results.csv',index=False)
print(f'output written to {self.outdir}test_results.csv')
test_rmse = (np.mean((self.df_result['rating'].values -
self.df_result['predicted_rating'].values)**2))**0.5
print(f'test RMSE : {test_rmse}')
可以按以下方式调用推断:
python rbm.py main_process --mode test --train_file '/home/santanu/ML_DS_Catalog-/Collaborating Filtering/pred_all_recs.csv' --test_file '/home/santanu/ML_DS_Catalog-/Collaborating Filtering/ml-100k/test_data.npy' --outdir '/home/santanu/ML_DS_Catalog-/Collaborating Filtering/' --user_info_file '/home/santanu/ML_DS_Catalog-/Collaborating Filtering/ml-100k/u.user' --movie_info_file '/home/santanu/ML_DS_Catalog-/Collaborating Filtering/ml-100k/u.item'
通过在 RBM 中仅使用5
隐藏单元,我们获得了大约1.19
的测试均方根误差(RMSE),这是值得称赞的,因为我们选择了这样一个简单的网络。 以下代码块中提供了推理的输出日志,以供参考:
output written to /home/santanu/ML_DS_Catalog-/Collaborating Filtering/test_results.csv
test RMSE : 1.1999306704742303
458.058 ms: process RBM
我们从test_results.csv
观察userid 1
的推断结果,如下所示(请参见“图 6.9”):
图 6.9:用户 ID 1 的保留数据验证结果
从前面的屏幕快照(“图 6.9”)中的预测可以看出,RBM 在预测userid
1
的电影的保留集方面做得很好。
建议您将最终收视率预测作为对每个电影收视率预测的多项式概率分布的收视率的期望值,并与采用最高收视率的方法进行比较,了解多项式分布的概率的效果如何。 可以在这个页面上找到用于协同过滤的 RBM 论文。受限玻尔兹曼机有关的代码位于这里。
总结
阅读完本章后,您现在应该能够使用受限玻尔兹曼机构建智能推荐系统,并根据您的领域和要求以有趣的方式对其进行扩展。 有关本章中说明的项目的详细实现,请参考此项目的 GiHub 链接。
在下一章中,我们将处理移动应用的创建,以执行电影评论的情感分析。 我期待您的参与。
七、电影评论情感分析移动应用
在这个现代时代,将数据发送到云中基于 AI 的应用进行推理是司空见惯的。 例如,用户可以将手机拍摄的图像发送到 Amazon Rekognition API,该服务可以标记图像中存在的各种对象,人物,文本,场景等。 利用托管在云中的基于 AI 的应用的服务的优势在于其易用性。 移动应用只需要向基于 AI 的服务以及图像发出 HTTPS 请求,然后在几秒钟内,该服务将提供推理结果。 这些机器学习即服务供应商中的一些如下:
- 亚马逊 Rekognition
- 亚马逊 Polly
- 亚马逊 Lex
- Microsoft Azure 认知服务
- IBM Watson
- Google Cloud Vision
下图“图 7.1,”说明了这种应用在云上托管时的架构以及如何与移动设备交互:
图 7.1:移动应用与托管在云上的 AI 模型进行通信
如您在上图中所见,移动应用将图像本地化和分类请求与图像一起发送到托管在云上的模型,并且在对提供的图像进行推断之后,模型将结果发送回去。 在云上使用此类服务的优势如下:
- 无需收集数据来训练这种模型
- 将 AI 模型作为服务托管不会带来任何痛苦
- 无需担心重新训练模型
所有上述内容将由服务提供商负责。 但是,在云上使用这种 AI 应用确实也有一些缺点,其中包括:
- 用户无法在移动设备上本地运行推理。 所有推理都需要通过向托管 AI 应用的服务器发送网络请求来完成。 如果没有网络连接,该移动应用将无法运行。 同样,通过网络从模型中获取预测可能会有一些延迟。
- 如果不是免费托管的云应用,则用户通常需要为运行的推理次数付费。
- 云托管的模型非常通用,用户无法控制使用自己的数据训练这些模型。 如果数据是唯一的,则在通用数据上经过训练的这种应用可能不会提供很好的结果。
可以通过在移动设备本身上运行推理,而不是通过互联网将数据发送到 AI 应用来克服部署在云上的 AI 应用的上述缺点。
可以使用特定于移动应用所针对问题的训练数据在具有适当 CPU 和 GPU 的任何系统上训练该模型。 然后,可以将经过训练的模型转换为优化的文件格式,而只需要运行推理所需的权重和操作即可。 然后,可以将优化的模型与移动应用集成,并且可以将整个项目作为应用加载到移动设备上。 训练后的模型的优化文件应尽可能轻巧,因为模型将与其他移动应用代码一起存储在移动设备上。 在本章中,我们将使用 TensorFlow mobile 开发一个 Android 移动应用。
技术要求
您需要具备 Python 3,TensorFlow 和 Java
的基本知识。
使用 TensorFlow Mobile 构建 Android 移动应用
在这个项目中,我们将使用 TensorFlow 的移动功能来优化训练好的模型作为协议缓冲区对象。 然后,我们将模型与 Android 应用集成,该应用的逻辑将用 Java 编写。 我们需要执行以下步骤:
- 在 TensorFlow 中构建模型并使用相关数据进行训练。
- 模型在验证数据集上令人满意地执行后,将 TensorFlow 模型转换为优化的 protobuf 对象(例如,
optimized_model.pb
)。 - 下载 Android Studio 及其先决条件。 用 Java 开发核心应用逻辑,并使用 XML 开发接口页面。
- 将 TensorFlow 训练有素的模型 protobuf 对象及其相关的依赖项集成到项目内的 Assets 文件夹中。
- 生成项目并运行它。
下图说明了此 Android 应用的实现(“图 7.2”):
图 7.2:移动应用部署架构图
Android 应用中的电影评论评分
我们将构建一个 Android 应用,该应用将基于电影评论的情感分析,将电影评论作为输入,并提供从0
到5
的等级作为输出。 首先将训练循环神经网络的 LSTM 版本,以对电影的情感进行二分类。 训练数据将由基于文本的电影评论以及0
或1
的二进制标签组成。 标签1
代表评论带有正面情绪,而0
则代表电影带有负面情绪。 从模型中,我们将预测情绪为正的可能性,然后将可能性放大五倍,以将其转换为合理的等级。 将使用 TensorFlow 构建模型,然后将训练后的模型转换为优化的冻结 protobuf 对象,以与 Android 应用逻辑集成。 冻结对象的大小将比原始训练模型小得多,并且仅用于推理目的。
我们将使用以下论文中标题为《学习单词向量进行情感分析》的可用数据集:
@InProceedings{maas-EtAl:2011:ACL-HLT2011,
author = {Maas, Andrew L. and Daly, Raymond E. and Pham, Peter T. and Huang, Dan and Ng, Andrew Y. and Potts, Christopher},
title = {Learning Word Vectors for Sentiment Analysis},
booktitle = {Proceedings of the 49th Annual Meeting of the Association for Computational Linguistics: Human Language Technologies},
month = {June},
year = {2011},
address = {Portland, Oregon, USA},
publisher = {Association for Computational Linguistics},
pages = {142--150},
url = {http://www.aclweb.org/anthology/P11-1015}
}
预处理电影评论文本
电影评论文本需要进行预处理,并转换为数字标记,与语料库中的不同单词相对应。 通过使用第一个50000
常见单词,Keras 分词器将这些单词转换为数字索引或标记。 我们已限制电影评论最多包含1000
个单词标记。 如果电影评论的单词标记少于1000
,则该评论的开头会填充零。 预处理之后,数据将分为训练,验证和测试集。 保存 Keras Tokenizer
对象以在推理期间使用。
用于预处理电影评论的详细代码(preprocess.py
)如下:
# -*- coding: utf-8 -*-
"""
Created on Sun Jun 17 22:36:00 2018
@author: santanu
"""
import numpy as np
import pandas as pd
import os
import re
from keras.preprocessing.text import Tokenizer
from keras.preprocessing.sequence import pad_sequences
import pickle
import fire
from elapsedtimer import ElapsedTimer
# Function to clean the text and convert it into lower case
def text_clean(text):
letters = re.sub("[^a-zA-z0-9\s]", " ",text)
words = letters.lower().split()
text = " ".join(words)
return text
def process_train(path):
review_dest = []
reviews = []
train_review_files_pos = os.listdir(path + 'train/pos/')
review_dest.append(path + 'train/pos/')
train_review_files_neg = os.listdir(path + 'train/neg/')
review_dest.append(path + 'train/neg/')
test_review_files_pos = os.listdir(path + 'test/pos/')
review_dest.append(path + 'test/pos/')
test_review_files_neg = os.listdir(path + 'test/neg/')
review_dest.append(path + 'test/neg/')
sentiment_label = [1]*len(train_review_files_pos) + \
[0]*len(train_review_files_neg) + \
[1]*len(test_review_files_pos) + \
[0]*len(test_review_files_neg)
review_train_test = ['train']*len(train_review_files_pos) + \
['train']*len(train_review_files_neg) + \
['test']*len(test_review_files_pos) + \
['test']*len(test_review_files_neg)
reviews_count = 0
for dest in review_dest:
files = os.listdir(dest)
for f in files:
fl = open(dest + f,'r')
review = fl.readlines()
review_clean = text_clean(review[0])
reviews.append(review_clean)
reviews_count +=1
df = pd.DataFrame()
df['Train_test_ind'] = review_train_test
df['review'] = reviews
df['sentiment_label'] = sentiment_label
df.to_csv(path + 'processed_file.csv',index=False)
print ('records_processed',reviews_count)
return df
def process_main(path):
df = process_train(path)
# We will tokenize the text for the most common 50000 words.
max_fatures = 50000
tokenizer = Tokenizer(num_words=max_fatures, split=' ')
tokenizer.fit_on_texts(df['review'].values)
X = tokenizer.texts_to_sequences(df['review'].values)
X_ = []
for x in X:
x = x[:1000]
X_.append(x)
X_ = pad_sequences(X_)
y = df['sentiment_label'].values
index = list(range(X_.shape[0]))
np.random.shuffle(index)
train_record_count = int(len(index)*0.7)
validation_record_count = int(len(index)*0.15)
train_indices = index[:train_record_count]
validation_indices = index[train_record_count:train_record_count +
validation_record_count]
test_indices = index[train_record_count + validation_record_count:]
X_train,y_train = X_[train_indices],y[train_indices]
X_val,y_val = X_[validation_indices],y[validation_indices]
X_test,y_test = X_[test_indices],y[test_indices]
np.save(path + 'X_train',X_train)
np.save(path + 'y_train',y_train)
np.save(path + 'X_val',X_val)
np.save(path + 'y_val',y_val)
np.save(path + 'X_test',X_test)
np.save(path + 'y_test',y_test)
# saving the tokenizer oject for inference
with open(path + 'tokenizer.pickle', 'wb') as handle:
pickle.dump(tokenizer, handle, protocol=pickle.HIGHEST_PROTOCOL)
if __name__ == '__main__':
with ElapsedTimer('Process'):
fire.Fire(process_main)
代码preprocess.py
可以按以下方式调用:
python preprocess.py --path /home/santanu/Downloads/Mobile_App/aclImdb/
相同的输出日志如下:
Using TensorFlow backend.
records_processed 50000
24.949 s: Process
建立模型
我们将构建一个简单的 LSTM 版本的循环神经网络,在输入层之后有一个嵌入层。 使用预训练的大小为 100 的 Glove 向量初始化嵌入层字向量,并将该层定义为trainable
,以便字向量嵌入可以根据训练数据进行更新。 隐藏状态和单元状态的维数也保持为100
。 使用二进制交叉熵损失训练模型。 为避免过拟合,将脊正则化添加到损失函数中。 Adam 优化器用于训练模型。
以下代码段显示了用于在 TensorFlow 中构建模型的函数:
def _build_model(self):
with tf.variable_scope('inputs'):
self.X = tf.placeholder(shape=[None, self.sentence_length],dtype=tf.int32,name="X")
print (self.X)
self.y = tf.placeholder(shape=[None,1], dtype=tf.float32,name="y")
self.emd_placeholder = tf.placeholder(tf.float32,shape=[self.n_words,self.embedding_dim])
with tf.variable_scope('embedding'):
# create embedding variable
self.emb_W =tf.get_variable('word_embeddings',[self.n_words, self.embedding_dim],initializer=tf.random_uniform_initializer(-1, 1, 0),trainable=True,dtype=tf.float32)
self.assign_ops = tf.assign(self.emb_W,self.emd_placeholder)
# do embedding lookup
self.embedding_input = tf.nn.embedding_lookup(self.emb_W,self.X,"embedding_input")
print( self.embedding_input )
self.embedding_input = tf.unstack(self.embedding_input,self.sentence_length,1)
#rint( self.embedding_input)
# define the LSTM cell
with tf.variable_scope('LSTM_cell'):
self.cell = tf.nn.rnn_cell.BasicLSTMCell(self.hidden_states)
# define the LSTM operation
with tf.variable_scope('ops'):
self.output, self.state = tf.nn.static_rnn(self.cell,self.embedding_input,dtype=tf.float32)
with tf.variable_scope('classifier'):
self.w = tf.get_variable(name="W", shape=[self.hidden_states,1],dtype=tf.float32)
self.b = tf.get_variable(name="b", shape=[1], dtype=tf.float32)
self.l2_loss = tf.nn.l2_loss(self.w,name="l2_loss")
self.scores = tf.nn.xw_plus_b(self.output[-1],self.w,self.b,name="logits")
self.prediction_probability = tf.nn.sigmoid(self.scores,name='positive_sentiment_probability')
print (self.prediction_probability)
self.predictions = tf.round(self.prediction_probability,name='final_prediction')
self.losses = tf.nn.sigmoid_cross_entropy_with_logits(logits=self.scores,labels=self.y)
self.loss = tf.reduce_mean(self.losses) + self.lambda1*self.l2_loss
tf.summary.scalar('loss', self.loss)
self.optimizer = tf.train.AdamOptimizer(self.learning_rate).minimize(self.losses)
self.correct_predictions = tf.equal(self.predictions,tf.round(self.y))
print (self.correct_predictions)
self.accuracy = tf.reduce_mean(tf.cast(self.correct_predictions, "float"), name="accuracy")
tf.summary.scalar('accuracy', self.accuracy)
训练模型
在本部分中,我们将说明用于训练模型的 TensorFlow 代码。 训练模型时要适度10 epochs
,以避免过拟合。 用于优化器的学习率为0.001
,而训练批量大小和验证批量大小分别设置为250
和50
。 需要注意的一件事是,我们使用tf.train.write_graph
函数将模型图定义保存在model.pbtxt
文件中。 同样,一旦模型被训练,我们将使用tf.train.Saver
函数将模型权重保存在检查点文件model_ckpt
中。 model.pbtxt
和model_ckpt
文件将用于创建 protobuf 格式的 TensorFlow 模型的优化版本,该版本可以与 Android 应用集成:
def _train(self):
self.num_batches = int(self.X_train.shape[0]//self.batch_size)
self._build_model()
self.saver = tf.train.Saver()
with tf.Session() as sess:
init = tf.global_variables_initializer()
sess.run(init)
sess.run(self.assign_ops,feed_dict={self.emd_placeholder:self.embedding_matrix})
tf.train.write_graph(sess.graph_def, self.path, 'model.pbtxt')
print (self.batch_size,self.batch_size_val)
for epoch in range(self.epochs):
gen_batch = self.batch_gen(self.X_train,self.y_train,self.batch_size)
gen_batch_val = self.batch_gen(self.X_val,self.y_val,self.batch_size_val)
for batch in range(self.num_batches):
X_batch,y_batch = next(gen_batch)
X_batch_val,y_batch_val = next(gen_batch_val)
sess.run(self.optimizer,feed_dict={self.X:X_batch,self.y:y_batch})
c,a = sess.run([self.loss,self.accuracy],feed_dict={self.X:X_batch,self.y:y_batch})
print(" Epoch=",epoch," Batch=",batch," Training Loss: ","{:.9f}".format(c), " Training Accuracy=", "{:.9f}".format(a))
c1,a1 = sess.run([self.loss,self.accuracy],feed_dict={self.X:X_batch_val,self.y:y_batch_val})
print(" Epoch=",epoch," Validation Loss: ","{:.9f}".format(c1), " Validation Accuracy=", "{:.9f}".format(a1))
results = sess.run(self.prediction_probability,feed_dict={self.X:X_batch_val})
print(results)
if epoch % self.checkpoint_step == 0:
self.saver.save(sess, os.path.join(self.path,'model'), global_step=epoch)
self.saver.save(sess,self.path + 'model_ckpt')
results = sess.run(self.prediction_probability,feed_dict={self.X:X_batch_val})
print(results)
批量生成器
在train
函数中,我们将使用批量生成器根据传递的批量大小生成随机批量。 生成器函数可以定义如下。 请注意,这些函数使用yield
代替return
。 通过使用所需参数调用函数,将创建批量的迭代器对象。 可以通过将next
方法应用于迭代器对象来检索批量。 我们将在每个周期开始时调用生成器函数,以便在每个周期中批量都是随机的。
以下代码段说明了用于生成批量迭代器对象的函数:
def batch_gen(self,X,y,batch_size):
index = list(range(X.shape[0]))
np.random.shuffle(index)
batches = int(X.shape[0]//batch_size)
for b in range(batches):
X_train,y_train = X[index[b*batch_size: (b+1)*batch_size],:],
y[index[b*batch_size: (b+1)*batch_size]]
yield X_train,y_train
脚本**movie_review_model_train.py**
中提供了模型训练活动的详细代码。 可以通过以下方式调用相同的训练:
python movie_review_model_train.py process_main --path /home/santanu/Downloads/Mobile_App/ --epochs 10
训练的输出如下:
Using TensorFlow backend.
(35000, 1000) (35000, 1)
(7500, 1000) (7500, 1)
(7500, 1000) (7500, 1)
no of positive class in train: 17497
no of positive class in test: 3735
Tensor("inputs/X:0", shape=(?, 1000), dtype=int32)
Tensor("embedding/embedding_lookup:0", shape=(?, 1000, 100), dtype=float32)
Tensor("positive_sentiment_probability:0", shape=(?, 1), dtype=float32)
.....
25.043 min: Model train
将模型冻结为 protobuf 格式
以model.pbtxt
和model_ckpt
文件的形式保存,训练好的模型不能直接由 Android 应用使用。 我们需要将其转换为优化的 protobuf 格式(.pb
扩展文件),该格式可以与 Android 应用集成。 经过优化的 protobuf 格式的文件大小将比model.pbtxt
和model_ckpt
文件的总大小小得多。
以下代码(freeze_code.py
)将根据model.pbtxt
和model_ckpt
文件创建优化的 protobuf 模型:
# -*- coding: utf-8 -*-
import sys
import tensorflow as tf
from tensorflow.python.tools import freeze_graph
from tensorflow.python.tools import optimize_for_inference_lib
import fire
from elapsedtimer import ElapsedTimer
#path = '/home/santanu/Downloads/Mobile_App/'
#MODEL_NAME = 'model'
def model_freeze(path,MODEL_NAME='model'):
# Freeze the graph
input_graph_path = path + MODEL_NAME+'.pbtxt'
checkpoint_path = path + 'model_ckpt'
input_saver_def_path = ""
input_binary = False
output_node_names = 'positive_sentiment_probability'
restore_op_name = "save/restore_all"
filename_tensor_name = "save/Const:0"
output_frozen_graph_name = path + 'frozen_'+MODEL_NAME+'.pb'
output_optimized_graph_name = path + 'optimized_'+MODEL_NAME+'.pb'
clear_devices = True
freeze_graph.freeze_graph(input_graph_path, input_saver_def_path,
input_binary, checkpoint_path, output_node_names,
restore_op_name, filename_tensor_name,
output_frozen_graph_name, clear_devices, "")
input_graph_def = tf.GraphDef()
with tf.gfile.Open(output_frozen_graph_name, "rb") as f:
data = f.read()
input_graph_def.ParseFromString(data)
output_graph_def = optimize_for_inference_lib.optimize_for_inference(
input_graph_def,
["inputs/X" ],#an array of the input node(s)
["positive_sentiment_probability"],
tf.int32.as_datatype_enum # an array of output nodes
)
# Save the optimized graph
f = tf.gfile.FastGFile(output_optimized_graph_name, "w")
f.write(output_graph_def.SerializeToString())
if __name__ == '__main__':
with ElapsedTimer('Model Freeze'):
fire.Fire(model_freeze)
正如您在前面的代码中看到的那样,我们首先通过引用在声明模型时定义的名称来声明输入张量和输出张量。 使用输入和输出张量以及model.pbtxt
和model_ckpt
文件,通过使用来自tensorflow.python.tools
的freeze_graph
函数冻结模型。 下一步,使用tensorflow.python.tools
中的optimize_for_inference_lib
函数创建名为optimized_model.pb
的原型泡沫模型,进一步优化了冻结模型。 经过优化的 protobuf 模型optimized_model.pb
将与 Android 应用集成,以进行推理。
可以调用freeze_code.py
模型来创建 protobuf 格式文件,如下所示:
python freeze_code.py --path /home/santanu/Downloads/Mobile_App/ --MODEL_NAME model
前面命令的执行输出如下:
39.623 s: Model Freeze
创建用于推理的单词到标记的词典
在预处理过程中,我们训练了 Keras 标记器,用其数字单词索引替换单词,以便将处理后的电影评论输入 LSTM 模型进行训练。 我们还保留了具有最高单词频率的前50000
个单词,并将查看序列设置为1000
的最大长度。 尽管训练有素的 Keras 标记生成器已保存用于推断,但 Android 应用无法直接使用它。 我们可以还原 Keras 标记器,并将前50000
个单词及其对应的单词索引保存在文本文件中。 可以在 Android 应用中使用此文本文件,以构建词对索引词典,以将评论文本的词转换为其词索引。 重要的是要注意,可以通过参考tokenizer.word_index.
从加载的 Keras 标记生成器对象中检索单词到索引的映射,执行此活动tokenizer_2_txt.py
的详细代码如下:
import keras
import pickle
import fire
from elapsedtimer import ElapsedTimer
#path = '/home/santanu/Downloads/Mobile_App/aclImdb/tokenizer.pickle'
#path_out = '/home/santanu/Downloads/Mobile_App/word_ind.txt'
def tokenize(path,path_out):
with open(path, 'rb') as handle:
tokenizer = pickle.load(handle)
dict_ = tokenizer.word_index
keys = list(dict_.keys())[:50000]
values = list(dict_.values())[:50000]
total_words = len(keys)
f = open(path_out,'w')
for i in range(total_words):
line = str(keys[i]) + ',' + str(values[i]) + '\n'
f.write(line)
f.close()
if __name__ == '__main__':
with ElapsedTimer('Tokeize'):
fire.Fire(tokenize)
tokenizer_2_txt.py
可以如下运行:
python tokenizer_2_txt.py --path '/home/santanu/Downloads/Mobile_App/aclImdb/tokenizer.pickle' --path_out '/home/santanu/Downloads/Mobile_App/word_ind.txt'
上一条命令的输出日志如下:
Using TensorFlow backend.
165.235 ms: Tokenize
应用界面设计
可以使用 Android Studio 设计一个简单的移动应用界面,并将相关代码生成为 XML 文件。 正如您在以下屏幕截图(“图 7.3”)中所看到的那样,该应用包含一个简单的电影评论文本框,用户可以在其中输入他们的电影评论,并在完成后按SUBMIT
按钮。 按下SUBMIT
按钮后,评论将传递到核心应用逻辑,该逻辑将处理电影评论文本并将其传递给 TensorFlow 优化模型进行推理。
作为推理的一部分,将计算情感得分,该得分将显示在移动应用上,并同时以星级评分的形式展示:
图 7.3:移动应用用户界面页面格式
生成前面提到的移动应用视图所需的 XML 文件如下所示:
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity"
tools:layout_editor_absoluteY="81dp">
<TextView
android:id="@+id/desc"
android:layout_width="100dp"
android:layout_height="26dp"
android:layout_marginEnd="8dp"
android:layout_marginLeft="44dp"
android:layout_marginRight="8dp"
android:layout_marginStart="44dp"
android:layout_marginTop="36dp"
android:text="Movie Review"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.254"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="HardcodedText" />
<EditText
android:id="@+id/Review"
android:layout_width="319dp"
android:layout_height="191dp"
android:layout_marginEnd="8dp"
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="24dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/desc" />
<RatingBar
android:id="@+id/ratingBar"
android:layout_width="240dp"
android:layout_height="49dp"
android:layout_marginEnd="8dp"
android:layout_marginLeft="52dp"
android:layout_marginRight="8dp"
android:layout_marginStart="52dp"
android:layout_marginTop="28dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.238"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/score"
tools:ignore="MissingConstraints" />
<TextView
android:id="@+id/score"
android:layout_width="125dp"
android:layout_height="39dp"
android:layout_marginEnd="8dp"
android:layout_marginLeft="96dp"
android:layout_marginRight="8dp"
android:layout_marginStart="96dp"
android:layout_marginTop="32dp"
android:ems="10"
android:inputType="numberDecimal"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.135"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/submit" />
<Button
android:id="@+id/submit"
android:layout_width="wrap_content"
android:layout_height="35dp"
android:layout_marginEnd="8dp"
android:layout_marginLeft="136dp"
android:layout_marginRight="8dp"
android:layout_marginStart="136dp"
android:layout_marginTop="24dp"
android:text="SUBMIT"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/Review" />
</android.support.constraint.ConstraintLayout>
要注意的一件事是,在android:id
选项中,在 XML 文件中声明了用户和移动应用核心逻辑相互交互所通过的变量。 例如,用户提供的电影评论将由Review
变量处理,如下面所示的 XML 文件中所定义:
android:id="@+id/Review"
Android 应用的核心逻辑
Android 应用的核心逻辑是处理用户请求以及传递的数据,然后将结果发送回用户。 作为此移动应用的一部分,核心逻辑将接受用户提供的电影评论,处理原始数据,并将其转换为经过训练的 LSTM 模型可以进行推理的格式。 Java 中的OnClickListener
函数用于监视用户是否已提交处理请求。 在将输入直接输入经过优化的经过训练的 LSTM 模型进行推理之前,需要将提供的电影评论中的每个单词更改为其索引。 除了优化的 protobuf 模型之外,还为此目的存储了单词及其对应索引的字典。 TensorFlowInferenceInterface
方法用于对训练后的模型进行推理。 优化的 protobuf 模型和单词词典及其对应的索引存储在assets
文件夹中。 总而言之,应用的核心逻辑执行的任务如下:
- 将索引字典中的单词加载到
WordToInd
HashMap
中。 词间索引字典是在训练模型之前,在文本的预处理过程中从分词器派生的。 - 使用
OnClickListener
方法监视用户是否已提交电影评论以进行推断。 - 如果已提交电影评论,则将从与 XML 关联的
Review
变量中读取评论。 该评论会通过删除标点符号等内容进行清理,然后拆分为单词。 使用HashMap
函数WordToInd
将这些单词中的每个单词转换为相应的索引。 这些索引构成我们 TensorFlow 模型的InputVec
输入,以进行推断。 输入向量长度为1000
; 因此,如果评论的字数少于1000
,则向量的开头将填充零。 - 下一步,使用
TensorFlowInferenceInterface
函数创建mInferenceInterface
对象,将优化的 protobuf 模型(扩展名为.pb
)从assets
文件夹加载到内存中。 与原始模型一样,需要定义要参考的 TensorFlow 模型的输入节点和输出节点。 对于我们的模型,它们定义为INPUT_NODE
和OUTPUT_NODE
,它们分别包含 TensorFlow 输入占位符的名称和输出情感概率的操作。mInferenceInterface
对象的feed
方法用于将InputVec
值分配给的INPUT_NODE
模型,而mInferenceInterface
的run
方法执行OUTPUT_NODE
。 最后,使用mInferenceInterface
的fetch
方法来填充对浮点变量value_
的推断结果。 - 情感分数(情感为正的概率)乘以五将转换为等级。 然后通过
ratingBar
变量将其馈送到 Android 应用用户界面。
Java 中的移动应用的核心逻辑如下:
package com.example.santanu.abc;
import android.content.res.AssetManager;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.RatingBar;
import android.widget.TextView;
import android.widget.Button;
import android.widget.EditText;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.Map;
import org.tensorflow.contrib.android.TensorFlowInferenceInterface;
public class MainActivity extends AppCompatActivity {
private TensorFlowInferenceInterface mInferenceInterface;
private static final String MODEL_FILE = "file:///android_asset/optimized_model.pb";
private static final String INPUT_NODE = "inputs/X";
private static final String OUTPUT_NODE = "positive_sentiment_probability";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Create references to the widget variables
final TextView desc = (TextView) findViewById(R.id.desc);
final Button submit = (Button) findViewById(R.id.submit);
final EditText Review = (EditText) findViewById(R.id.Review);
final TextView score = (TextView) findViewById(R.id.score);
final RatingBar ratingBar = (RatingBar) findViewById(R.id.ratingBar);
//String filePath = "/home/santanu/Downloads/Mobile_App/word2ind.txt";
final Map<String,Integer> WordToInd = new HashMap<String,Integer>();
//String line;
//reader = new BufferedReader(new InputStreamReader(getAssets().open("word2ind.txt")));
BufferedReader reader = null;
try {
reader = new BufferedReader(
new InputStreamReader(getAssets().open("word_ind.txt")));
// do reading, usually loop until end of file reading
String line;
while ((line = reader.readLine()) != null)
{
String[] parts = line.split("\n")[0].split(",",2);
if (parts.length >= 2)
{
String key = parts[0];
//System.out.println(key);
int value = Integer.parseInt(parts[1]);
//System.out.println(value);
WordToInd.put(key,value);
} else
{
//System.out.println("ignoring line: " + line);
}
}
} catch (IOException e) {
//log the exception
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
//log the exception
}
}
}
//line = reader.readLine();
// Create Button Submit Listener
submit.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// Read Values
String reviewInput = Review.getText().toString().trim();
System.out.println(reviewInput);
String[] WordVec = reviewInput.replaceAll("[^a-zA-z0-9 ]", "").toLowerCase().split("\\s+");
System.out.println(WordVec.length);
int[] InputVec = new int[1000];
// Initialize the input
for (int i = 0; i < 1000; i++) {
InputVec[i] = 0;
}
// Convert the words by their indices
int i = 1000 - 1 ;
for (int k = WordVec.length -1 ; k > -1 ; k--) {
try {
InputVec[i] = WordToInd.get(WordVec[k]);
System.out.println(WordVec[k]);
System.out.println(InputVec[i]);
}
catch (Exception e) {
InputVec[i] = 0;
}
i = i-1;
}
if (mInferenceInterface == null) {
AssetManager assetManager = getAssets();
mInferenceInterface = new TensorFlowInferenceInterface(assetManager,MODEL_FILE);
}
float[] value_ = new float[1];
mInferenceInterface.feed(INPUT_NODE,InputVec,1,1000);
mInferenceInterface.run(new String[] {OUTPUT_NODE}, false);
System.out.println(Float.toString(value_[0]));
mInferenceInterface.fetch(OUTPUT_NODE, value_);
double scoreIn;
scoreIn = value_[0]*5;
double ratingIn = scoreIn;
String stringDouble = Double.toString(scoreIn);
score.setText(stringDouble);
ratingBar.setRating((float) ratingIn);
}
});
}
}
需要注意的要点之一是,为了将包添加到依赖项中,我们可能需要编辑该应用的build.gradle
文件:
org.tensorflow:tensorflow-android:1.7.0
测试移动应用
我们将使用以下两部电影的评论测试该移动应用: 《阿凡达》和《星际穿越》。 《阿凡达》电影评论来自这里,其内容如下:
“看着《阿凡达》,我感觉与 1977 年看到《星球大战》时的感觉差不多。那是另一部我充满不确定性的电影。詹姆斯·卡梅隆的电影一直是毫无疑问的超前嗡嗡声的主题,就像他的《泰坦尼克号》一样。再次,他只是通过制作一部非凡的电影而使怀疑者们保持沉默,好莱坞仍然至少有一个人知道如何花费 2.5 亿美元,或者明智地花费 3 亿美元。
“阿凡达》不仅是一种令人震撼的娱乐活动,而且还是一项技术突破。它具有鲜明的绿色和反战信息。它包含如此直观的细节, 像《指环王》一样,它发明了一种新的语言 Na'vi,尽管我很仁慈地怀疑这种语言可以被人类甚至青少年使用,它创造了新的电影明星。 在那些电影中,您觉得必须跟上对话的步伐。”
审阅者给电影评分为 4/5,而移动应用的评分为 4.8/5,如以下屏幕截图所示(“图 7.4”):
图 7.4。 电影 Avatar 的移动应用评论评分
同样,我们将评估应用为电影《星际穿越》提供的评分,并从这里获取评论。 评论如下:
“星际大片代表了导演兼导演克里斯托弗·诺兰(Christopher Nolan)所期待的更多激动人心,发人深省,视觉上灿烂的电影制作人,即使其知识渊博超出了人们的理解范围。”
该影片在烂番茄上的平均评分为 7/10,如果将其缩放为 5,则得分为 3.5/5,而移动应用预测的评分为 3.37,如下图所示(“图 7.5”):
图 7.5。 电影《星际穿越》的移动应用评论评分
正如您在前面的两个插图中看到的那样,移动电影评论分级应用在为电影评论提供合理的分级方面做得很好。
总结
完成本章后,读者应该对如何使用 TensorFlow 的移动功能在 Android 应用中部署深度学习模型有一个清晰的认识。 本章所涉及的技术和实现细节将对读者有所帮助,帮助他们构建智能 Android 移动应用并以有趣的方式扩展它们。 该项目的详细代码位于这里。
在下一章中,我们将构建一个用于客户服务的对话式 AI 聊天机器人。 我们非常期待你的参与。
八、用于客户服务的会话式 AI 聊天机器人
会话式聊天机器人最近因其在增强客户体验方面的作用而大肆宣传。 现代企业已经开始在几个不同的过程中使用聊天机器人的功能。 由于对话式 AI 的广泛接受,填写表格或通过互联网发送信息的繁琐任务变得更加简化。 对话型聊天机器人的理想品质之一是,它应该能够在当前上下文中响应用户请求。 对话型聊天机器人系统中的参与者分别是用户和机器人。 使用对话型聊天机器人有很多优点,如下表所示:
- 个性化帮助:为所有客户创建个性化体验可能是一项繁琐的任务,但如果不这样做,则会使企业蒙受损失。 对话型聊天机器人是向每个客户提供个性化体验的便捷替代方法。
- 全天候支持:使用客户服务代表 24/7 的费用很高。 在非工作时间使用聊天机器人提供客户服务消除了雇用额外客户代表的麻烦。
- 一致性响应:聊天机器人提供的响应可能是一致的,而不同客户服务代表对相同问题的响应可能会有所不同。 如果客户对客户服务代表提供的答案不满意,则无需多次拨打电话。
- 耐心:虽然客户服务代表在与客户打交道时可能会失去耐心,但这对于聊天机器人来说是不可能的。
- 查询记录:与人类客户服务代表相比,聊天机器人在查询记录方面效率更高。
聊天机器人不是最近才出现的东西,其起源可以追溯到 1950 年代。 在第二次世界大战之后,艾伦·图灵(Alan Turing)开发了图灵测试,以查看人是否可以将人与机器区分开。 多年后的 1966 年,约瑟夫·魏曾鲍姆(Joseph Weizenbaum)开发了一些名为 Eliza 的软件,该软件模仿了心理治疗师的语言。 该工具仍位于这个页面中。
聊天机器人可以执行各种各样的任务,下面的列表中显示了其中的一些任务,以强调其多函数式:
- 回答有关产品的查询
- 向客户提供建议
- 进行句子补全活动
- 会话聊天机器人
- 与客户协商价格并参与投标
很多时候,企业很难确定是否需要聊天机器人。 企业是否需要聊天机器人可以通过“图 8.1”中的流程图确定:
图 8.1:客户参与模型
作为本章的一部分,我们将涵盖以下主题:
- 聊天机器人架构
- 用于聊天机器人的 LSTM 序列到序列模型
- 为 Twitter 支持聊天机器人构建序列到序列模型
技术要求
您将需要具备 Python 3,TensorFlow 和 Keras 的基础知识
聊天机器人架构
聊天机器人的核心组件是其自然语言处理框架。 聊天机器人使用自然语言处理通过通常称为解析的过程来处理呈现给他们的数据。 然后解析解析的用户输入,并根据用户的需求(从输入中解密),将适当的响应发送回用户。 聊天机器人可能需要从知识库和历史交易数据存储中寻求帮助,以适当地处理用户的请求。
聊天机器人可以大致分为以下两类:
- 基于检索的模型:这些模型通常依赖于查找表或知识库来从预定义的答案集中选择答案。 尽管这种方法看起来很幼稚,但是生产中的大多数聊天机器人都是这种类型的。 当然,从查找表或知识库中选择最佳答案可能会有各种复杂程度。
- 生成模型:生成模型可即时生成响应,而不是采用基于查找的方法。 它们主要是概率模型或基于机器学习的模型。 直到最近,马尔可夫链大多被用作生成模型。 然而,随着深度学习的最新成功,基于循环神经网络的方法越来越受欢迎。 通常,RSTM 的 LSTM 版本被用作聊天机器人的生成模型,因为它更擅长处理长序列。
基于检索的模型和生成模型都具有各自的优缺点。 由于基于检索的模型从一组固定的答案中进行回答,因此它们无法处理看不见的问题或没有适当预定义响应的请求。 生成模型要复杂得多。 他们可以了解用户输入中的实体并生成类似人的响应。 但是,它们很难训练,并且通常需要更多的数据来训练。 他们还容易犯语法错误,而基于检索的模型则不会犯这些语法错误。
使用 LSTM 的序列到序列模型
序列到序列模型架构非常适合捕获客户输入的上下文,然后基于该上下文生成适当的响应。 “图 8.2”显示了一个序列到序列模型框架,该框架可以像聊天机器人那样回答问题:
图 8.2:使用 LSTM 的序列到序列模型
从上图(“图 8.2”)中可以看出,编码器 LSTM 接受单词的输入序列,并将其编码为隐藏状态向量h
和单元状态向量c
。 向量h
和c
是 LSTM 编码器最后一步的隐藏状态和单元状态。 它们本质上将捕获整个输入句子的上下文。
然后,以h
和c
形式的编码信息作为其初始隐藏状态和单元状态被馈送到解码器 LSTM 。 每个步骤中的解码器 LSTM 尝试预测以当前单词为条件的下一个单词。 这意味着,解码器 LSTM 的每个步骤的输入都是当前字。
为了预测第一个单词,LSTM 将提供一个虚拟的起始关键字<BOS>
,它代表句子的开头。 同样, <EOS>
虚拟关键字表示句子的结尾,并且一旦预测到该句,就应该停止输出生成。
在训练每个目标词的序列到序列模型的过程中,我们知道apriori
是先前的词,这是解码器 LSTM 的输入。 但是,在推理过程中,我们将没有这些目标词,因此我们必须将上一步作为输入。
建立序列到序列模型
我们将用于构建聊天机器人的序列到序列模型的架构将对先前在“图 8.2”中说明的基本序列到序列架构进行一些修改。修改后的架构可以在下图中看到(“图 8.3”):
图 8.3:序列到序列模型
与其将编码器最后一步的隐藏状态h
和单元状态c
馈送到解码器 LSTM 的初始隐藏状态和单元状态,我们将隐藏状态h
馈入解码器的每个输入步骤。 为了预测目标词w[t]
在任何步骤t
中,输入是先前的目标词w[t-1]
,t-1
和隐藏状态h
。
Twitter 上的客户支持
现在,我们对如何使用循环神经网络构建聊天机器人有了一些想法,我们将使用 20 个大品牌对客户发布的推文的客户服务响应来构建聊天机器人。 数据集twcs.zip
位于这个页面中。每个推文均由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
中。 作为回应,这将为我们提供客户发布的tweets in
以及客户服务代表在回复中发布tweets out
。 数据创建函数可以编码如下:
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 版本用于构建序列到序列模型。 这是因为 LSTM 在记住长文本序列中的长期依存关系方面效率更高。 LSTM 架构中的三个门使它能够有效地记住长期序列。 基本的 RNN 无法记住长期依赖关系,因为与其架构相关的梯度问题逐渐消失。
在此模型中,我们使用两个 LSTM。 第一个 LSTM 将输入推文编码为上下文向量。 该上下文向量不过是编码器 LSTM 的最后一个隐藏状态h ∈ R^n
,n
是隐藏状态向量的维。 输入推文x ∈ R^k
作为单词索引序列被馈送到编码器 LSTM,k
就是输入推文的序列长度。 这些单词索引在馈送到 LSTM 之前已映射到单词嵌入w ∈ R^m
。 单词嵌入包含在一个嵌入矩阵中[W ∈ R^(m x N)]
,其中N
表示词汇表中单词的数量。
第二个 LSTM 用作解码器。 它试图将编码器 LSTM 创建的上下文向量h
解码为有意义的响应。 作为此方法的一部分,我们在每个时间步中将上下文向量与前一个单词一起馈入以生成当前单词。 在第一步中,我们没有任何先前的词可用于条件 LSTM,因此我们使用智能体START
词开始从解码器 LSTM 生成词序列的过程。 在推理过程中,我们在当前时间步输入前一个单词的方式与训练过程中使用的方法不同。 在训练中,由于我们在每个时间步都知道apriori
之前的单词,因此相应地输入它们没有任何问题。 但是,在推理期间,由于我们在当前时间步上没有实际的前一个单词,因此会反馈前一个时间步上的预测单词。 每个时间步t
的隐藏状态h'[t]
在最终的最大 softmax N
之前通过几个全连接层馈送。 在此 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 的每个时间步中的目标单词。 任何步骤中的分类交叉熵损失都将遍及词汇表的所有单词,并且可以表示为:
标签[y[i]], i = 1 -> N
代表目标单词的单热编码版本。 仅对应于实际单词的标签为1
; 其余为0
。 项Pi
表示实际目标单词是由i
索引的单词的概率。 为了获得每个输入/输出推特对的总损失C
,我们需要对解码器 LSTM 的所有时间步长上的损失求和。 由于词汇量可能会变得很大,因此在每个时间步骤中为目标标签创建单热编码向量y[t] = [y[i]], i = 1 -> n
会很昂贵。 sparse_categorical_crossentropy
损失在这里变得非常有益,因为我们不需要将目标单词转换为单热编码向量,而只需输入目标单词的索引作为目标标签即可。
训练模型
该模型可以使用 Adam 优化器进行训练,因为它可靠地提供了稳定的收敛性。 由于 RNN 容易出现梯度问题(尽管这对于 LSTM 来说不是问题),因此,如果梯度太大,则最好将其剪裁。 可以使用 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
的副本,并从它们移了一个时间步,以便它们可以用作解码器每个时间步的前一个单词的输入。 这些移位序列的第一个单词是START
关键字,它在解码器 LSTM 的第一时间步输入。 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
以下是一些重要的论据,以及它们的描述和用于调用聊天机器人序列到序列模型的训练的使用值:
参数 | 说明 | 用于训练的值 |
| 词汇中的单词数 |
|
| 应被限制到 LSTM 的推文的最大长度 |
|
| LSTM 的隐藏状态 |
|
| 词嵌入的维度 |
|
| 用于优化程序的起始学习率 |
|
| 用于正则化目的的丢弃 |
|
| 训练/推理 | 对于训练,请使用 |
对某些输入推文的推断结果
训练后的序列到序列模型响应于某些输入推文生成了一些相关的推文。 以下命令显示了示例推断命令。 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
推断结果如下表所示:
Tweet in | Tweet out |
|
|
|
|
|
|
Actually that's a broken link you sent me and incorrect information https://t.co/V4yfrHR8VI. |
|
Yo |
|
My picture on |
|
Somebody from |
|
|
|
|
|
|
|
|
|
总结
现在,我们到本章的结尾。 看完本章中说明的与聊天机器人和序列到序列模型相关的各种概念之后,读者现在应该能够构建自己的聊天机器人实现并以有趣的方式对其进行扩展。 众所周知,序列到序列模型不仅适用于聊天机器人,还适用于整个自然语言处理领域,例如机器翻译。 本章的代码位于 GitHub。
在下一章中,我们将使用强化学习来使赛车学习如何独立驾驶。 我们非常期待你的参与。
九、使用强化学习的自主无人驾驶汽车
在过去的几年中,增强学习已经真正兴起,在增强学习中,智能体通过与环境的交互来学习决策。 这是当今人工智能和机器学习中最热门的主题之一,并且这一领域的研究正在快速发展。 在强化学习(RL)中,智能体将他们的行动和经验转化为学习,以便将来做出更好的决策。
增强学习不属于有监督或无监督的机器学习范式,因为它本身就是一个领域。 在监督学习中,我们尝试学习一个映射F: X → Y
,将输入X
映射到输出Y
,而在强化学习中,智能体学习通过反复试验采取最佳行动。 当业务代表执行任务出色时,将分配奖励,而当业务代表执行不好时,则要支付罚款。 智能体试图吸收这些信息,并学会在类似的情况下不重复这些错误。 智能体所处的这些条件称为状态。 “图 9.1”显示了强化学习框架中环境中智能体的交互作用:
图 9.1:智能体与环境交互的图示
技术要求
您将需要具备 Python 3,TensorFlow,Keras 和 OpenCV 的基础知识。
马尔可夫决策过程
任何强化学习问题都可以看作是马尔可夫决策过程,我们在第 1 章“基于人工智能的系统基础”中进行了简要介绍。 为了您的利益,我们将再次详细研究。 在马尔可夫决策过程中,我们有一个与环境交互的主体。 在任何给定的情况下,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)
。 对于给定状态s[t]
,RL 智能体选择动作a
,该动作给出最大 Q 值。 智能体采取的动作a[t]
可以表示如下:
一旦智能体在状态s[t]
采取行动a[t]
,新状态s[t + 1]
会呈现给智能体来处理。 这个新状态s[t + 1]
通常不是确定性的,通常表示为当前状态s[t]
和动作a[t]
的条件概率分布。这些概率称为状态转移概率,可以表示为:
每当智能体在状态s[t]
采取行动a[t]
并达到新状态s[t + 1]
时,即时奖励会奖励给智能体,可以表示为:
现在,我们拥有定义马尔可夫决策过程所需的一切。 马尔可夫决策过程是一个系统,其特征在于以下四个特征:
- 一组状态
S
- 一组动作
A
- 一组奖励
R
- 状态转移概率
P(s[t + 1] = s' | s[t] = s, a[t] = a)
:
图 9.2:具有三个状态的马尔可夫决策过程的图示
学习 Q 值函数
对于 RL 智能体做出决定,重要的是智能体学习 Q 值函数。 可以通过贝尔曼方程迭代地学习 Q 值函数。 当智能体开始与环境交互时,它以随机状态s[0]
和每个状态动作对的 Q 值的随机状态开始。 智能体的动作在某种程度上也是随机的,因为它没有状态 Q 值来做出明智的决策。 对于每个采取的行动,环境将根据哪个智能体开始建立 Q 值表并随着时间的推移而改善而返回奖励。
在任何暴露状态s[t]
处于迭代状态t
时,智能体会采取行动a[t]
,以最大化其长期回报。 Q 表保存长期奖励值,因此选择的a[t]
将基于以下启发式:
Q 值表也通过迭代t
进行索引,因为智能体只能查看到目前为止的 Q 表构建,随着智能体与环境的交互作用会越来越大。
根据动作a[t]
,环境呈现给智能体奖励r[t]
和新状态s[t + 1]
。 智能体将更新 Q 表,以使其长期预期总收益最大化。 长期奖励r'[t]
可以写成如下:
在这里,γ
是折扣因子。 如我们所见,长期奖励结合了即时奖励r[t]
和基于所展示的下一状态s[t + 1]
的累积未来奖励。
根据计算出的长期奖励,状态动作对(s[t], a[t])
的现有 Q 值更新如下:
深度 Q 学习
深度 Q 学习利用深度学习网络来学习 Q 值函数。 如下图所示,“图 9.3,”是深度 Q 学习网络的架构:
图 9.3:深度 Q 网络的图示
该图学习将每对状态(s, a)
和动作映射到输出 Q 值输出Q(s, a)
,而在右侧图中,对于每个状态s
,我们学习与每个动作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 函数。 实际上,网络会通过将给定状态s
的所有动作[a[i]], i = 1 -> n
的预测 Q 值与目标 Q 值的预测 Q 值最小化来学习 Q 函数。 每个训练记录都是一个元组s[t], a[t], r[t], s[t + 1]
。
请记住,要根据网络本身计算目标 Q 值。 让我们考虑一个事实,即网络是由W ∈ R^d
重参数化的,对于给定状态的每个动作,我们学习从状态到 Q 值的映射。 对于n
组动作[a[i]], i = 1 -> n
,网络将预测与每个动作有关的i
Q 值。 映射函数可以表示如下:
在给定状态s[t]
的情况下,此映射用于预测 Q 值,并且此预测p_hat(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”说明了双重深度 Q 学习的工作机制:
图 9.4A:双重深度 Q 学习的图示
图 9.4B:双重深度 Q 学习的图示
在上图中,我们可以看到两个网络:网络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[r] = r, s[t + 1] = s')
,网络A
对所有可能的动作给出状态s[t] = s
。 由于我们知道实际动作a[t] = a
,因此我们选择 Q 值Q[t](s[t] = a, a[t] = a)
。 这将充当我们的预测 Q 值y_hat
。
现在,计算目标要困难一些,因为它涉及到两个网络。 我们知道在步骤t
的任何状态s[t]
的候选 Q 值是时间t
的即时奖励r[t]
,加上给定新状态s[t + 1]
的在下一步t + 1
的最大 Q 值。 候选 Q 值可以表示为:
当γ
是恒定折扣因子时就是这种情况。 奖励r
已经是训练元组的一部分。 因此,我们唯一需要计算目标的是动作a'
,该动作给出最大 Q 值,并将相应的 Q 值赋予相应的动作a'
。 这个计算问题max[a'] Q[t](s', a)
分为两个部分:
- 网络
A
确定动作a'
,该动作在状态s'
下给出最大 Q 值。 但是,我们不会从网络A
中获取与a'
和状态s'
对应的 Q 值。 - 网络
B
用于提取 Q 值Q[tk](s', a')
对应于状态s'
和动作a'
。
与基本的DQN
相比,这导致稳定得多的训练。
实现自动驾驶汽车
现在,我们将研究实现一种自动驾驶的无人驾驶赛车,该赛车使用深度 Q 网络学习如何在赛道上自行驾驶。 驾驶员和汽车将充当智能体,赛车场及其周围环境将充当环境。 我们将使用 OpenAI Gym CarRacing-v0
框架作为环境。 状态和奖励将由环境呈现给智能体,而智能体将通过采取适当的行动对智能体采取行动。 这些状态采用从汽车前面的摄像头拍摄的图像的形式。 环境接受的动作为三维向量a ∈ R^3
的形式,其中第一个分量用于左移,第二个分量用于前移,第三部分用于右移。 该智能体将与环境交互并将交互转换为(s, a, r, s'), i = 1 -> m
形式的元组。 这些交互元组将用作我们的训练数据。
该架构将类似于我们在图右侧所示的架构(“图 9.4A”和“图 9.4B”)。
离散化深度 Q 学习的动作
离散化动作对于深度 Q 学习非常重要,因为三维连续动作空间可以具有无限的 Q 值,并且在深度 Q 网络的输出层中不可能为每个动作单独设置单元。 动作空间的三个维度如下:
Steering:[-1, 1]
Gas:[0, 1]
Brake:[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 网络的网络架构如下所示。 网络具有 CNN 架构,可将状态处理为图像并输出所有可能动作的 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 值。 epsilon
参数的值确定操作是随机的概率。 最初,将ε
设置为1
,以使操作随机。 当智能体已收集指定数量的训练样本时,在每个步骤中都会减少ε,从而减少了随机动作的可能性。 这种基于ε值的作用的方案称为epsilon
贪婪算法。 我们定义两个智能体类,如下所示:
-
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
自动驾驶汽车的环境
自动驾驶汽车的环境是 OpenAI Gym 的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.5A 和图 9.5B):
图 9.5(A):汽车在训练的最初阶段出现错误
以下结果显示经过足够的训练后汽车成功驾驶:
图 9.5(B):经过充分训练后,汽车成功驾驶
总结
至此,我们到了本章的结尾。 本章中讨论的主题将帮助您快速掌握强化学习范例,并使您能够构建智能 RL 系统。 另外,希望读者将在该项目中学到的技术应用于其他基于 RL 的问题。
在下一章中,我们将从深度学习的角度看待验证码,并围绕它构建一些有趣的项目。 期待您的参与。
十、深度学习视角的验证码
术语 CAPTCHA 是用于区分计算机和人类的完全自动化的公共图灵测试的缩写。 这是一种旨在区分人类用户与机器或机器人的计算机程序,通常是一种安全措施,可防止垃圾邮件和数据滥用。 早在 1997 年就引入了 CAPTCHA 的概念,当时互联网搜索公司 AltaVista 试图阻止向该平台歪曲其搜索引擎算法的自动 URL 提交。 为了解决这个问题,AltaVista 的首席科学家安德烈·布罗德(Andrei Broder)提出了一种算法,该算法可以随机生成文本图像,这些图像很容易被人识别,但不能被机器人识别。 后来,在 2003 年,Luis von Ahn,Manuel Blum,Nicholas J Hopper 和 John Langford 完善了这项技术,并将其称为 CAPTCHA。 验证码最常见的形式要求用户识别变形图像中的字母和数字。 进行此测试是为了希望人类能够轻松地区分变形图像中的字符,而自动化程序或漫游器将无法区分它们。 验证码测试有时称为反向图灵测试,因为它是由计算机而非人工执行的。
截至最近,CAPTCHA 已开始发挥更大的作用,而不仅仅是防止机器人欺诈。 例如,当 Google 数字化《纽约时报》的档案和 Google 图书中的某些图书时,他们使用了 CAPTCHA 及其变体之一 reCAPTCHA。 通常,通过要求用户正确输入多个验证码的字符来完成此操作。 实际上只有 CAPTCHA 之一被标记并用于验证用户是否为人类。
其余的验证码由用户标记。 目前,Google 使用基于图像的 CAPTCHA 来帮助标记其自动驾驶汽车数据集,如以下屏幕截图所示:
图 10.1:各个网站上的一些常见验证码
在本章中,我们将介绍以下主题:
- 什么是 CAPTCHA
- 使用深度学习打破 CAPTCHA 暴露其脆弱性
- 使用对抗学习生成验证码
技术要求
您将需要具备 Python 3,TensorFlow,Keras 和 OpenCV 的基础知识。
通过深度学习破解验证码
随着卷积神经网络(CNN)在计算机视觉任务中的最新成功,在几分钟内打破基本的验证码是相对容易的任务。 因此,CAPTCHA 需要比过去有更多的发展。 在本章的第一部分中,我们将介绍使用具有深度学习框架的机器人自动检测到的基本验证码的漏洞。 我们将通过利用 GAN 创建更难以被机器人检测到的 CAPTCHA 进行跟进。
生成基本的验证码
可以使用 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”)是上述代码生成的随机验证码:
图 10.2:字符为 26UR 的随机验证码
与文本一起,Claptcha
工具要求使用打印字体作为输入的字体。 如我们所见,它以横轴上有些扭曲的线的形式给图像增加了噪点。
生成数据来训练 CAPTCHA 破丨解丨器
在本节中,我们将使用Claptcha
工具生成多个验证码,以训练 CNN 模型。 CNN 模型将通过监督训练来学习识别 CAPTCHA 中的字符。 我们将生成用于训练 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)
需要注意的一件事是,大多数 CAPTCHA 生成器都使用ttf
文件来获取 CAPTCHA 的字体模式。
我们可以通过使用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
训练 CAPTCHA,4000
验证 CAPTCHA 和4000
测试 CAPTCHA,如我们从脚本的以下日志中所见:
3.328 min: main_process
在下一节中,我们将讨论 CAPTCHA 破丨解丨器的卷积神经网络架构。
验证码破丨解丨器 CNN 架构
我们将使用 CNN 架构来识别 CAPTCHA 中的字符。 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
可以如下图所示图示 CAPTCHA 破丨解丨器 CNN 模型(“图 10.3”):
图 10.3:CAPTCHA 破丨解丨器 CNN 架构
预处理 CAPTCHA 图像
图像的原始像素无法与 CNN 架构配合使用。 标准化图像以使 CNN 更快收敛始终是一个好主意。 通常用作规范化方案的两种方法是平均像素减法或通过将像素值除以255
将像素缩放到[0,1]
范围内。 对于我们的 CNN 网络,我们将图像标准化为[0,1]
。 我们还将处理 CAPTCHA 的灰度图像,这意味着我们将只处理一个颜色通道。 load_img
函数可用于加载和预处理 CAPTCHA 图像,如以下代码所示:
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 https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/intel-proj-py/img/255.
将验证码字符转换为类
为了训练目的,需要将 CAPTCHA 的原始字符转换为数字类。 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 至关重要。 在训练开始之前将所有数据加载到内存中可能会导致数据存储问题,因此在训练期间读取 CAPTCHA 并动态构建批量是有意义的。 这导致资源的最佳利用。
我们将使用一个可用于构建训练和验证批量的数据生成器。 生成器将在初始化期间存储 CAPTCHA 文件的位置,并在每个周期动态构建批量。 在每个文件之后,文件的顺序会随机打乱,以免在每个周期中都不能以相同的顺序遍历验证码图像。 这通常可以确保模型在训练期间不会卡在不良的局部最小值上。 数据生成器类可以如下编码:
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
训练 CAPTCHA 破丨解丨器
可以通过调用train
函数来训练 CAPTCHA 破丨解丨器模型,如下所示:
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')
对于批量中的 CAPTCHA,将考虑所有四个字符进行训练。 我们使用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
个训练周期内,该模型就可以使 CAPTCHA 的每个字符级别的验证准确率达到 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
使用 GeForce GTX 1070 GPU,大约16000
98.3s(即64000
CAPTCHA 字符)的20
周期的训练时间约为4.412 min
。 建议读者使用基于 GPU 的机器进行更快的训练。
测试数据集的准确率
可以通过调用evaluate
函数来运行测试数据的推断。 evaluate
函数如下所示,以供参考。 请注意,评估程序的设计应从整体验证码的角度看待准确率,而不是在验证码的字符级别上。 因此,只有当 CAPTCHA 目标的所有四个字符都与预测匹配时,我们才能将 CAPTCHA 标记为被 CNN 正确识别。
用于在测试验证码上运行推理的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
CAPTCHA 的测试数据集上实现的准确率约为 93%。 运行evaluate
函数的输出如下:
Accuracy: 0.9320972187421699
Evaluation file written at: /home/santanu/ML_DS_Catalog-/captcha/evaluation.csv
13.564 s: captcha_solver
我们还可以看到,对那些4000
CAPTCHA 的推断花费了大约 14 秒,并且评估的输出写入了/home/santanu/ML_DS_Catalog-/captcha/evaluation.csv
文件中。
在下面的屏幕快照中,我们将查看一些模型做得不好的目标和预测(“图 10.4”):
图 10.4:CAPTCHA 求解器模型失败的 CAPTCHA
通过对抗学习生成验证码
在本节中,我们将通过生成的对抗网络来创建验证码。 我们将生成类似于街景门牌号码数据集(SVHN 数据集)中的图像。 想法是将这些 GAN 生成的图像用作验证码。 仅当我们训练 GAN 时,它们才容易从噪声分布中采样。 这将减轻通过更复杂的方法创建验证码的需要。 它也将为验证码中使用的 SVHN 街道号提供一些变化。
SVHN 是一个现实世界的数据集,由于它在对象识别算法中的使用而在机器学习和深度学习领域中非常受欢迎。 顾名思义,该数据集包含从 Google Street View Images 获得的门牌号码的真实图像。 可以从以下链接下载数据集。
我们将使用调整后的门牌号数据集,其中图像已调整为大小(32,32)
。 我们感兴趣的数据集是train_32x32.mat
。
通过这个生成对抗网络(GAN),我们将根据随机噪声生成房屋编号图像,并且生成的图像将与 SVHN 数据集中的图像非常相似。
回顾一下,在 GAN 中,我们有一个生成器(G
)和一个判别器(D
),它们针对损失函数彼此玩零和极小极大游戏。 随着时间的流逝,生成器和判别器的工作都会越来越好,直到我们到达一个固定点为止,两者都无法进一步改善。 该固定点是相对于损失函数的鞍点。 对于我们的应用,生成器G
会将给定分布P(z)
的噪声z
转换为门牌号图像x
,以使x = G(z)
。
生成的图像通过判别器D
传递,判别器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
相互之间具有效用函数U(θ, φ)
的零和最小极大值游戏,并且最小极大值游戏的优化问题可以表示为:
在参数空间中的某个点上,如果某个函数相对于某些参数而言是局部最大值,而对于其余参数而言是局部最小值,则该点称为鞍点。 因此,(θ_hat, φ_hat)
给出的点将成为效用函数U(θ, φ)
的鞍点。 该鞍点是极小极大零和博弈的纳什均衡,对于生成器和判别器正在优化的效用,(θ_hat, φ_hat)
参数是最佳的。 就当前问题而言,生成器G
会产生最困难的验证码,供判别器以θ_hat
作为其参数进行检测。 同样,判别器最适合以φ
作为参数来检测伪造的验证码。
具有鞍点的最简单函数是x^2 - y^2
,鞍点是原点:(0,0)
。
优化 GAN 损失
在上一节中,我们已经看到,生成器和判别器相对于它们各自网络的参数的最佳状态由以下公式给出:
为了最大化目标函数,我们通常使用梯度上升,而为了最小化成本函数,我们使用梯度下降。 前面的优化问题可以分为两部分:生成器和判别器分别通过梯度上升和梯度下降依次优化效用函数。 在优化过程中的任何步骤t
上,判别器都将通过使工具最小化来尝试移至新状态,如下所示:
替代地,生成器将尝试最大化相同的效用。 由于判别器D
没有生成器的任何参数,因此工具的第二项不会影响生成器的优化。 可以这样表示:
我们已经将生成器和判别器优化目标都转换为最小化问题。 判别器和生成器的优化都是使用梯度下降进行的,直到我们达到目标函数的鞍点。
生成器网络
生成器网络将吸收随机噪声,并尝试输出类似于 SVHN 图像的图像作为输出。 随机噪声是100
维输入向量。 每个维度都是遵循标准正态分布的随机变量,平均值为0
,标准差为1
。
最初的密集层具有8192
单元,将其重塑为形状为4 x 4 x 512
的三维张量。 使用512
过滤器可以将张量视为4 x 4
图像。 为了增加张量的空间大小,我们进行了一系列转置 2D 卷积,步幅为2
,核过滤器大小为5 x5
。步幅大小决定了转置卷积的缩放比例。 例如,跨度为 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”)中描述了生成器的网络架构,以供参考:
图 10.5:生成器组网络图
判别器网络
判别器将是一个经典的二元分类卷积神经网络,可以将生成器图像分类为伪图像,将实际 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”):
图 10.6:判别器网络图
判别器的输出激活函数为 Sigmoid。 这有助于对来自真实 SVHN 图像的伪生成图像进行二分类。
训练 GAN
为生成对抗网络建立训练流程并非一帆风顺,因为这需要很多技术方面的考虑。 我们定义了以下三个训练网络:
- 带有参数
θ
的生成器网络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
读取它们。 load_img
函数可用于使用opencv
读取原始图像。
最后,为了更好地融合网络,将像素强度归一化为[-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
的好处是,函数的所有输入都可以由用户作为参数提供,正如我们从上一条命令中看到的那样。
众所周知,GAN 很难训练,因此需要调整这些参数,以使模型正常运行。 以下是一些重要参数:
参数 | 值 | 注释 |
|
| 小型批量随机梯度下降的批量大小。 |
|
| 输入随机噪声向量维。 |
|
| 生成器学习率。 |
|
|
|
|
| 辨别真假房屋号码图像的形状。 |
|
| 判别器网络的学习率。 |
|
|
|
|
| 这是 |
|
| 这是要运行的周期数。 |
|
| 设计该平滑系数的目的是减少真实样本对判别器的损失。 例如, |
使用 GeForce GTX 1070 GPU,用这些参数训练 GAN 大约需要 3.12 小时。 建议读者使用 GPU 进行更快的训练。
训练期间的验证码的质量
现在,让我们研究训练期间各个周期生成的验证码的质量。 以下是历时5
(请参阅“图 10.7a”),历时51
(请参阅“图 10.7b”)和历时100
之后的 CAPTCHA 图像。 “图 10.7c”)。 我们可以看到,随着训练的进行,CAPTCHA 图像的质量有所提高。 以下屏幕快照显示了在第 5 阶段生成的示例验证码的结果:
图 10.7a:在第 5 阶段生成的示例验证码
以下屏幕截图显示了在周期 51 生成的示例验证码的结果:
图 10.7b:在周期 51 生成的样本验证码
以下屏幕截图显示了在周期 100 生成的示例验证码的结果:
图 10.7c:在周期 100 生成的示例验证码
使用训练好的生成器来创建验证码供使用
可以在运行时加载经过训练的 GAN 网络,以生成街景房屋编号(例如 CAPTCHA)以供使用。 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。 一旦有足够的标签用于生成的验证码,则将多数标签作为实际标签,并从此用于评估。
可以通过调用以下命令从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
函数生成的一些验证码。 我们可以看到图像足够不错,可以用作验证码:
图 10.8:使用经过训练的 GAN 网络的生成器生成的验证码
总结
这样,我们到了本章的结尾。 与本章相关的所有代码都可以在 GitHub 链接中找到。 现在,您将对深度学习如何影响验证码有一个清晰的认识。 在频谱的一端,我们可以看到使用具有深度学习的 AI 应用的机器人如何轻松地解决 CAPTCHA。 但是,另一方面,我们看到了如何使用深度学习来利用给定的数据集并根据随机噪声创建新的验证码。 您可以在本章中扩展有关生成对抗网络的技术知识,以使用深度学习构建智能的验证码生成系统。 现在,我们到本书的结尾。 我希望通过九个基于人工智能的实用应用的旅程是一次充实的旅程。 祝一切顺利!