传感器融合第 1 部分:卡尔曼滤波基础知识
在本系列中,我将尝试解释卡尔曼滤波算法以及一个在多个传感器输入的帮助下跟踪车辆的实现示例,这通常称为传感器融合。
最基本形式的卡尔曼滤波器由 3 个步骤组成。a)预测——基于车辆位置和运动学方程的先前知识,我们预测时间 t+1 后车辆的位置。b)测量-从传感器获得关于车辆位置的读数,并将其与预测进行比较 C)更新-基于我们的预测和传感器读数更新我们关于车辆位置(或状态)的知识。仅此而已。卡尔曼滤波器的所有变体只是上述 3 个步骤的不同变体,这取决于你想要使用的不同运动学方程和你想要并入算法的不同种类的传感器读数。但是不管变型如何,所有卡尔曼滤波器的实现都包括上述 3 个步骤。因此,让我们使用车辆跟踪示例来分析它们。
A.预测
使用速度、距离和时间的基本方程,我们知道如果我的车辆在时间 t 的位置在 x 位置,那么在时间 t+1 车辆将在位置 x+((t+1)-t)* v,其中 v 是车辆的速度。进一步挖掘,如果我们有可用的加速度值,我们可以将它添加到上面的等式中,并将车辆的新位置更新为 x+((t+1)-t)* v+ 0.5 * a *(t+1)-t)2。公式化上述方程,让表示车辆的当前状态,其由作为向量 x 的车辆的当前位置和速度组成。因此向量 x 将具有 4 个元素,即 px、py、vx、vy;分别表示 x 和 y 方向的位置和速度。
为了让我们的例子尽可能简单(但不要比它需要的更简单),我们将在二维空间跟踪车辆,考虑到我们还没有会飞的汽车,这是一个公平的估计😊。位置 x 和 y 距离是相对于我们的车辆本身的位置。所以如果我们说一辆车在位置 px 和 py,我们的意思是它离我们的车有 px 和 py 的距离。
继续,让我们将速度和加速度定义为分别具有 x 和 y 方向上的速度和加速度数据的速度和加速度矢量。我们将时间变化表示为 delta_t,这样,我们跟踪位置和速度的单独方程就变成:
1.Px(t+1)= Px+delta _ t * VX+0.5 * ax * delta _ t 2
2.Py(t+1)= Py+delta _ t * vy+0.5 * ay * delta _ T2
3.Vx(t+1) = Vx + ax * delta_t
4.Vy(t+1) = Vy + ay * delta_t
不用深入细节,我们应该明白的一件事是,执行矩阵运算在计算上比求解单个方程有效得多。一个简单的理由是,如果我们考虑上述 4 个等式,控制器(将执行 1 和 0 操作的实际微控制器)将需要执行以下动作 4 次。I)创建一个变量 Px(t+ 1),ii)。获取 Px iii 的值)。获取 delta_t iv 的值)。获取 vx v 的值)。获取 ax vi 的值)。执行数学运算 vii)。存储结果。对于每个等式,所有这些操作都必须执行一次。为了更好地解释这个类比,假设你妈妈让你去杂货店买些洋葱(类似于从内存中获取 Px,vx,ax 值)。作为一个听话的孩子,你马上去把它带来。然后她让你切开它们(类似于数学运算),你照做了,然后就在你要看足球比赛的时候,她让你从商店里带西红柿回来。再次,作为听话的孩子,你回去带西红柿,收集所有的设备(刀,切割片和一个碗来保存它们),切西红柿并为她储存。然后她让你带些水果来……还有更多。你在这里得到了要点,足以说,那天晚上你不会是一个快乐的孩子。如果你的妈妈给了你所有需要带的东西的清单,并指示你在开始工作前把它们剪下来,这不是更好吗?电脑也是如此。(必须承认,我一直在等着写这个类比😊)
回到 1 和 0,让我们把 4 个独立的方程转换成下面的矩阵形式。现在我们的 4 个方程组合在一起,它们看起来像这样。
x_new = A * x + B * u 其中
从上面的矩阵方程中取出第一行
px _ new =[(1 * px)+(0 * py)+(delta _ t * VX)+(0 * vy)]+[(0.5 * delta _ t * delta _ t * acceleration _ x)+(0 * acceleration _ y)]
px_new = px + delta_t * vx + 0.5 *加速度 _x * delta_t 2
这和我们在把单个方程转换成矩阵形式之前想出来的是一样的。这里矩阵 A 和 B 只是代表位置、速度和加速度的运动学方程的矩阵。
哦,但是等等…嗯…妈妈在问…去……去…去杂货店(这次她道歉了)…所以你回说嘿,妈妈,我要去看足球比赛,我现在不能去。而且妈妈说所以告诉我( 预测 )你什么时候能走。和你回复我就进去 关于 *2 hrs。*那么,你问的这个对话有趣的部分是什么……嗯,是“关于”的部分。正如你能感觉到的,你有点不确定你什么时候能回去。你知道它将在 2 小时内,但有一点点± 10(或 15 或选择一个数字)分钟的不确定性,你不确定。也许在比赛中增加了很多额外的时间,也许比赛变得无聊了(当你的球队正在输球时阅读它),你决定不等到结束,或者也许这是一场德比比赛中的伟大胜利,你想在比赛庆祝和专家分析后等待和观看。
这个不确定因素对计算机也适用。一个想法可能会进入你的脑海,卡尔曼滤波器有什么了不起的,上面提到的运动学方程已经知道几十年了,如果你喜欢,我们可以用它的矩阵形式,并开始预测。但是就像本杰明·富兰克林说的那样,生活中只有两件事是确定的:死亡和纳税。因此,当我们要求计算机预测车辆新状态的价值时,我们也需要询问它的不确定性。在卡尔曼滤波语言中,这种不确定性被表示为协方差矩阵。让我们将状态协方差矩阵表示为
p =[方差 _px,0,0,0],
[0,方差 _py,0,0],
[0,0,方差 _vx,0],
[0,0,0,方差 _vy]
其中,方差 _px、方差 _py、方差 _vx、方差 _vy 分别表示预测的 x 位置、y 位置、x 速度和 y 速度的不确定性。所有剩下的 0 项是说一个值的不确定性独立于另一个值,意思是说我的位置 x 不确定性与我在 y 方向的不确定性无关。这不需要在每种情况下都是正确的。在某些情况下,你所追踪的变量实际上可能是相互依赖的。但是我们将在这里使用简单的方法,并将其他值设为 0。这实际上使它成为一个“方差”矩阵,但协方差是更广泛使用的术语。
冒着让我们的方程变得有点复杂的风险,我想引入过程噪声协方差矩阵 q。在任何协方差矩阵计算中(在我们的例子中是矩阵 P),都可能有一定量的噪声需要加入其中,以便在估计中得出新的预测协方差。
由于卡尔曼滤波是一个连续的迭代过程,我们需要在每次从传感器获得新的读数时预测状态向量及其协方差矩阵,这样我们就可以将预测值(步骤 a)与传感器值(步骤 b)进行比较,并更新我们正在跟踪的车辆的信息(步骤 c)。我们已经有了更新状态向量的方程,更新协方差矩阵的方程如下所示。这样,我们用下面两个方程结束了预测的步骤 a。
状态向量:x = A * x + B * u
协方差矩阵:P = A * P * AT * Q
B.尺寸
现在,假设我们接收到一个传感器读数,显示我们正在跟踪的车辆的位置。实际上,操作顺序是,只有当我们收到传感器读数时,才触发卡尔曼滤波计算。传感器读数通常具有与每个读数相关联的时间戳。我们读取时间戳,计算最后一次读取时间戳和这次读取的时间戳之间的差异。使用 delta_t =两个读数之间的时间戳差值,按照上述说明进行预测,然后进入传感器输入的测量和更新部分。像激光雷达和飞行时间传感器这样常用传感器会给你关于车辆位置的读数,但不是它的速度。因此,我们的传感器读数将是矢量 z = [px,py]的形式。如前所述,我们需要将该测量值与步骤 a 中预测的值进行比较。但是查看两个向量(预测的状态向量 x 和测量向量 z),我们会发现两者的阶数不同。因此,为了将状态矩阵转换为与 z 相同的大小,我们引入了矩阵“H”。进入矩阵基础,要将一个 4 x 1 ’ x’ 矩阵转换成 2 x 1 ’ z’ 矩阵(同时保持前两个值不变并移除速度变量),我们使用一个 2 x 4 矩阵 h。因此,比较预测值和测量值的等式变为 y = z-Hx,这给出了两个值之间的差异。
在这一步,我们还将计算卡尔曼增益。该增益系数基本上用于确定车辆状态的最终值,在预测值和测量值之间选择一个合适的值。我们已经将预测值中的不确定性视为状态协方差矩阵 p。测量读数的相应不确定性矩阵是测量协方差矩阵,用字母 r 表示。我们的状态变量有 4 个变量(x,y 位置和速度),因此其协方差矩阵为 4 x 4 矩阵。如前所述,让我们考虑我们的传感器只给我们 x 和 y 方向的位置读数。在这种情况下,我们的测量协方差矩阵将是 2 x 2 的形状,如下所示。
r =[[方差 _ 输入 _ 位置 _ x _ 读数,0]
[0,方差 _ 输入 _ 位置 _ y _ 读数]
显然,在决定最终值时,我们希望更倾向于具有较少不确定性的值。在卡尔曼增益值中,我们通过计算预测值中的不确定度占总不确定度的百分比来比较预测和测量方法中的不确定度。换句话说
**卡尔曼增益=预测状态的不确定性/(预测状态的不确定性+测量读数的不确定性)。**我们知道预测状态中的不确定性是矩阵 P,测量值中的不确定性是矩阵 r。但正如您所见,两者大小不同。因此,我们使用 H 矩阵将 P 矩阵转换为正确的大小。这样,卡尔曼增益方程就变成= >
K = ( P * HT ) / ( ( H * P * HT ) + R)
用 4 x 4 P 矩阵和 2 x 2 R 矩阵计算卡尔曼增益所需的 h 矩阵如下所示
H = [ [1,0,0,0],
[0, 1, 0, 0] ]
还是那句话,P 矩阵周围 H 矩阵的填充只是为了矩阵运算,请不要让它吓到你。卡尔曼增益只是一个简单的百分比公式。
好了,有了预测值、测量值和卡尔曼增益值,是时候移动到更新状态了,在这里我们对状态向量 x 及其相应的协方差矩阵 p 进行最终更新。
C.更新
即使我想,我也不能让这一步变得复杂,所以我会给你最终状态向量的方程–> x = x+Ky。假设我们对预测值非常有信心,那么我们的协方差 P 将会有非常小的方差值。这反过来将使卡尔曼增益(即预测状态的不确定性/预测状态的不确定性+测量读数的不确定性)。)作为最终等式的小数字将是*小数字/(小数字+大数字)的形式。在这种情况下,如果我们考虑最终的 x 方程,我们取预测值,只将它的一小部分差值(即预测值和测量值之间的差值)加到测量值上。因为根据上述理论,卡尔曼增益将是一个小值,所以 x 的方程将是:x = x +(小数字 y)
如您所见,这将导致最终 x 更倾向于预测值,这与我们最初假设一致,即与测量值相比,我们对预测值有更高信心。类似地,更新协方差矩阵的等式为 P = (I-KH) P。同样,H 矩阵用于矩阵操作。
这样,我们就有了实现完整卡尔曼滤波器所需的所有工具,公式如下
A.预测:
a.X = A * X + B * u
b.P = A * P * AT * Q
B.尺寸
a.Y = Z — H * X
b.K = ( P * HT ) / ( ( H * P * HT ) + R)
C.更新
a.X = X + K * Y
b.P = ( I — K * H ) * P
与我的其他文章类似,我也想在这篇文章中加入玩具代码,但是这篇文章读起来很长。我肯定会在这个系列的后续部分中添加它,我计划使用卡尔曼滤波器完成传感器融合。
像往常一样,如果你喜欢我的文章,用喜欢和评论来表达你的欣赏。你也可以在 twitter 上找到我和我的其他文章
直到下一次…干杯!!
传感器融合第 2 部分:卡尔曼滤波器代码
在第一部分中,我们在推导了卡尔曼滤波算法的基本方程后离开。为便于参考,在此再次陈述。
A.预测:
A . X = A * X+B * u
B . P = A * P * AT * Q
B .测量
A . Y = Z—H * X
B . K =(P * HT)/((H * P * HT)+R
c .更新
a. X = X + K * Y
b. P = ( I — K * H ) * P
在这篇文章中,我们将使用 Udacity 的 github handle 上免费提供的文本文件中的传感器读数,对上述等式进行编码。该文本文件(obj _ pose-Laser-Radar-synthetic-input . txt)包含来自激光和雷达的传感器读数,以及读数的时间戳和地面真实值。到目前为止,我们只介绍了基本的卡尔曼滤波算法,因此在这个编码练习中,我们将只使用上述输入文件中的激光读数。一旦我们在以后的文章中谈到“扩展卡尔曼滤波”,我们也将开始使用雷达读数。但是以我们目前对卡尔曼滤波方程的理解,仅仅使用激光读数就可以作为一个完美的例子,在编码的帮助下巩固我们的概念。
输入文本文件中捕获的传感器读数格式如下。
对于包含雷达数据的行,列有:sensor_type ®、rho_measured、phi_measured、rhodot_measured、timestamp、x_groundtruth、y_groundtruth、vx_groundtruth、vy_groundtruth、yaw_groundtruth、yawrate_groundtruth。
对于包含激光雷达数据的行,列包括:传感器类型(L)、x 测量值、y 测量值、时间戳、x 地面真实值、y 地面真实值、vx 地面真实值、vy 地面真实值、yaw 地面真实值、yawrate 地面真实值。
有了这些信息,让我们开始编码吧,不要再拖延了。与任何 Python 文件一样,让我们首先导入所有需要的库
#**************Importing Required Libraries*************
import numpy as np
import pandas as pd
from numpy.linalg import inv
接下来,读取包含传感器读数的输入文本文件。
#*************Declare Variables**************************
#Read Input File
measurements = pd.read_csv('obj_pose-laser-radar-synthetic-input.txt', header=None, delim_whitespace = True, skiprows=1)
我将“skiprows”参数设为 1,因为当我们开始实施 KF 算法时,我们没有任何关于车辆状态的先验知识。在这种情况下,我们通常将状态默认为传感器输出的第一个读数。这正是我们将要做的。在下面显示的代码中,我们将通过从输入文件的第一行读取来初始化我们的状态 X 。由于这个读数已经被使用,我们在读取输入文件时简单地跳过它。除了状态向量 X 的初始值,我们还将使用输入文件中的第一个时间戳作为我们的 先前时间 。正如上一篇文章所解释的,我们需要当前和以前的时间戳来计算 delta_t 。提供的时间戳以微秒为单位,我们将除以 10⁶.这有两个原因,第一,较小的数量更容易维护。第二,速度地面实况读数(以及我们代码中的速度值)是以秒为单位的。
# Manualy copy initial readings from first row of input file.
prv_time = 1477010443000000/1000000.0
x = np.array([
[0.312242],
[0.5803398],
[0],
[0]
])
接下来,我们初始化变量来存储地面真相和 RMSE 值。RMSE(均方根误差)用于相对于地面真实值来判断我们的算法的性能。
#Initialize variables to store ground truth and RMSE values
ground_truth = np.zeros([4, 1])
rmse = np.zeros([4, 1])
我们初始化矩阵 P 和 A 。关于 P 和 A 矩阵的详细结构,请参考第 1 部分中更深入的解释。基本上,矩阵 A 用于实现距离、速度和时间的运动学方程,矩阵 P 是状态协方差矩阵,具有作为其对角元素的 x、y、vx 和 vy 的方差。这个初始 P 值的意思是,我们对我们的位置值具有高置信度(这是有意义的,因为我们已经从实际传感器读数中得到它),由相对低的方差值表示,而对速度值具有低置信度(这也是有意义的,因为我们不知道速度),由相对大的方差值表示。
#Initialize matrices P and A
P = np.array([
[1, 0, 0, 0],
[0, 1, 0, 0],
[0, 0, 1000, 0],
[0, 0, 0, 1000]
])
A = np.array([
[1.0, 0, 1.0, 0],
[0, 1.0, 0, 1.0],
[0, 0, 1.0, 0],
[0, 0, 0, 1.0]
])
接下来我们定义 H 和 I 矩阵,正如我在上一篇文章中解释的,它们将分别是 4 x 2 和 4 x 4 矩阵。我们定义矢量 Z ,由于我们的激光雷达读数将由 2 个位置读数(x 和 y)组成,因此它将是一个 2 x 1 矢量。
H = np.array([
[1.0, 0, 0, 0],
[0, 1.0, 0, 0]
])
I = np.identity(4)
z_lidar = np.zeros([2, 1])
我们定义测量协方差矩阵 R ,根据上一篇文章,它也将是一个 2×2 的矩阵。我们将在以后的文章中详细讨论如何获取 R 矩阵和 noise_ax 和 noise_ay 的值。
R = np.array([
[0.0225, 0],
[0, 0.0225]
])
接下来我们定义 noise_ax , noise_ay 和 matrix Q 。
noise_ax = 5
noise_ay = 5
Q = np.zeros([4, 4])
让我们花点时间来理解它们。如果我们重温本系列第一部分中定义的运动学方程,你可以看到在位置和速度项中有一个加速度因子。为了方便起见,在这里对它们进行了重写。
- Px(t+1)= Px+δ_ t * VX+0.5 * ax *δ_ t
- Py(t+1)= Py+δ_ t * vy+0.5 * ay *δ_ t
- Vx(t+1) = Vx + ax * delta_t
- Vy(t+1) = Vy + ay * delta_t
由于加速度未知,我们可以将其添加到噪声成分中,这种随机噪声可以解析地表示为上面推导的等式中的最后一项。因此,我们有一个随机加速度向量 v ,它由一个零均值和一个协方差矩阵 Q. 描述
向量 v 可以分解成两个分量:不包含随机变量的 4 乘 2 矩阵 G 和包含随机加速度分量的 2 乘 1 矩阵 a :
在卡尔曼滤波器的每次迭代中计算 delta_t,并且由于我们没有任何加速度数据,我们将加速度定义为具有零均值和标准差 noise_ax 和 noise_ay 的随机向量。基于我们的噪声向量,我们现在可以定义新的协方差矩阵 q。协方差矩阵被定义为噪声向量 v 乘以噪声向量 v 转置的期望值。所以让我们把这个写下来:
要了解更多关于“期望值”的信息,请观看汗学院的视频。由于 G 不包含随机变量,我们可以把它放在期望计算之外
ax 和 ay 被假定为不相关的噪声过程。这意味着 Q 中的协方差 sigma_axy 为零。
因此,在将所有内容合并到一个矩阵中后,我们获得了 4x 4 Q 矩阵:
在卡尔曼滤波器的每次迭代中,我们将按照上面的公式计算矩阵 Q。定义好所有变量后,让我们开始遍历传感器数据,并对它们应用卡尔曼滤波器。运行 for 循环直到测量长度,读取测量线,检查是否是激光雷达(‘L’)读数。
**#**********************Iterate through main loop********************
#Begin iterating through sensor data
for i in range (len(measurements)):
new_measurement = measurements.iloc[i, :].values
if new_measurement[0] == 'L':**
从当前读数中获取时间戳,通过与先前时间戳进行比较来计算时间变化,然后在下一次迭代中将当前时间戳替换为先前时间戳。
**#Calculate Timestamp and its power variables
cur_time = new_measurement[3]/1000000.0
dt = cur_time - prv_time
prv_time = cur_time**
计算 delta_t 的平方、立方和 delta_t 的四次方(代码中的“dt ”),这是计算 Q 矩阵所需的。
**dt_2 = dt * dt
dt_3 = dt_2 * dt
dt_4 = dt_3 * dt**
用 delta_t 值更新矩阵 A。Delta_t 将乘以速度以得出位置值。
**#Updating matrix A with dt value
A[0][2] = dt
A[1][3] = dt**
更新 Q 矩阵。如果你回头看看上面导出的 Q 矩阵方程,你可以很容易地找到下面提供的代码行。
**#Updating Q matrix
Q[0][0] = dt_4/4*noise_ax
Q[0][2] = dt_3/2*noise_ax
Q[1][1] = dt_4/4*noise_ay
Q[1][3] = dt_3/2*noise_ay
Q[2][0] = dt_3/2*noise_ax
Q[2][2] = dt_2*noise_ax
Q[3][1] = dt_3/2*noise_ay
Q[3][3] = dt_2*noise_ay #Updating sensor readings
z_lidar[0][0] = new_measurement[1]
z_lidar[1][0] = new_measurement[2] #Collecting ground truths
ground_truth[0] = new_measurement[4]
ground_truth[1] = new_measurement[5]
ground_truth[2] = new_measurement[6]
ground_truth[3] = new_measurement[7]**
最后调用预测和更新函数。
**predict()
update(z_lidar)**
现在让我们看看我们的 predict()函数,它与我们在本系列中使用的以下预测方程非常相似。在代码部分没有太多要解释的,它实际上只是派生公式的直接复制。
A .预测
a. X = A * X + B * u
b. P = A * P * AT * Q
**#**********************Define Functions*****************************
def predict():
# Predict Step
global x, P, Q
x = np.matmul(A, x)
At = np.transpose(A)
P = np.add(np.matmul(A, np.matmul(P, At)), Q)**
继续定义 update()函数。我们将在该功能中实施“测量”和“更新”步骤。
B .测量
a . Y = Z—H * X
b . K =(P * HT)/((H * P * HT)+R
c .更新
a. X = X + K * Y
b. P = ( I — K * H ) * P
**def update(z):
global x, P
# Measurement update step
Y = np.subtract(z_lidar, np.matmul(H, x))
Ht = np.transpose(H)
S = np.add(np.matmul(H, np.matmul(P, Ht)), R)
K = np.matmul(P, Ht)
Si = inv(S)
K = np.matmul(K, Si)
# New state
x = np.add(x, np.matmul(K, Y))
P = np.matmul(np.subtract(I ,np.matmul(K, H)), P)**
…至此,您已经完成了卡尔曼滤波算法的完整代码。尽管这看起来像是一小步,但它是传感器融合技术的许多高级版本的基础算法。如前所述,卡尔曼滤波器的所有变体都由我们在本系列中定义的相同预测、测量和更新状态组成。在更高级的版本中,唯一的区别是它们使用不同的运动学和传感器方程。在本系列中,我们也将一步一步地介绍它们。但此时此刻,让我们击掌庆祝完成经典卡尔曼滤波算法的基础步骤。你可以在我的 github repo 这里找到完整的代码和输入文件。
像往常一样,如果你喜欢我的文章,用喜欢和评论来表达你的欣赏。你也可以在 twitter 找到我和我的其他文章
下次再见,干杯!!
句子嵌入
文献综述:
首先,让我们从单词嵌入开始,这些是单词在 n 维向量空间中的表示,以便语义相似(例如“船”-船只)或语义相关(例如“船”-水)的单词根据训练方法变得更接近。培训方法大致分为两部分:
- 使用文档作为上下文。( LSA ,话题模特)。他们捕捉语义关联。
- 使用单词作为上下文。(神经语言模型,分布式语义模型)。他们捕捉语义的相似性。
在平行线上,我们希望开发一些东西,可以捕捉句子之间的语义相似性或相关性,然后是段落,然后是文档。
现在暂时假设我们有单词向量。
获得句子表示的一种方法是将句子中包含的所有单词向量的表示相加,这被称为单词质心。并且两个句子之间的相似度可以通过 c 熵距离来计算。同样的事情可以扩展到段落和文档。但是这种方法忽略了很多信息,比如序列,并且可能给出错误的结果。比如:
- 你要去那里教书,而不是玩。
- 你要去那里玩而不是教书
这些将有相同的最终表现,但你可以看到,意义是完全不同的。
然后,通过引入编辑距离方法对其进行了一点微调,这被称为字移动器的距离。它来自于发表在 em NLP’14 上的论文“从单词嵌入到文档距离”。这里我们取句子 1 到句子 2 中每个单词的最小距离,并将它们相加。比如:
- 奥巴马在伊利诺伊州对媒体讲话
- 总统在芝加哥迎接媒体
Image taken from original paper
- 奥巴马匹配总统
- 向问候者说话
- 媒体报道
- 伊利诺伊到芝加哥
这种方法正在产生令人鼓舞的结果。论文在问答中使用单词嵌入的质心和单词移动器的距离进行生物医学文献检索,使用质心距离进行初始剪枝,然后使用 WMD 得到更好的结果。(注意 WMD 慢。是O(n*m)
,其中n
是sentence1
的长度,m
是sentence2
的长度
但是我们看到,在这两种方法中,我们没有使用来自序列的信息。所以在这个领域也有很多研究。
论文中, 卷积神经网络用于句子分类其中曾提出 CNN 用于同样的事情。它在句子中使用了填充,使它们具有相同的维度- >使用单词嵌入来映射这些填充句子中的单词- >应用 CNN - >使用最大超时池- >馈送到全连接层- >获取表示。他们写道,使用 word2vec 初始化单词嵌入,然后通过反向传播进行微调,得到了比“不调整”和“随机初始化”更好的结果。
然后是微软研究院 2015 年发表的关于深度语义相似度模型的论文,本文使用不同类型的词向量,这里是基于 n-grams。例:对于 hello 这个词,他们会加上起始和结束标记,并分解成 n 个字母组,即 hello - > #hello# - > {#he,hel,ell,llo,lo#}。并以向量形式表示它,向量的大小为(272627(如果不使用哈希))(请注意,它与词汇大小无关,并概括了看不见的单词,对拼写错误具有鲁棒性)。他们将这些向量的序列(用于表示句子)放入他们的模型,该模型给他们一个语义向量,并且基于已知的这些句子的相似性或不相似性,他们被训练(弱监督)。在该模型中,他们使用了具有最大池的 CNN。CNN 提取局部特征,max pooling 层从中生成全局特征。我已经在 short science这里写了一篇关于这篇论文的小总结。
因此,在训练期间,句子与具有相似性(肯定)和不相似性(否定)的句子一起提供。该模型可以被训练用于最大化(文件,阳性的余弦相似性)-文件,阴性的余弦相似性)。他们使用了随机梯度下降法。
但是我们不能在这里使用 rnn 吗?在这里,它们将拥有所有文档的内存,并在查询时返回所需的文档。根据 MetaMind 的这篇论文,用于视觉和文本问题回答的动态记忆网络 ,其中他们使用 GRU 将数据编码为事实,并将问题编码为向量,然后使用问题向量并循环事实以生成上下文向量并更新记忆。基于记忆,它能够回答。我们可以用同样的方法来寻找文档的相似性。我已经在 short science 这里写了一篇关于这篇论文的小总结。
本文中提出的进一步的观点是:使用双向** GRUs(在本文中有更好的解释, " 具有再次读取和复制机制的有效摘要 "),以及使用句子嵌入而不是单词嵌入。**
然后还有一篇论文在这个领域有很强的基础, 句子和文档的分布式表示 s ,它来自谷歌,发表于 2014 年。它提出以与单词向量相同的方式学习段落向量,并将其用于无数任务,包括情感分析和信息检索。
如有任何疑问,请发邮件至 nishantiam@gmail.com给我。
用 Seq2Seq 简化句子
作为一名机器学习的学生,我喜欢阅读最近发表的 Arxiv 论文,并试图理解它们。在这里,我将尝试解释一篇给我留下深刻印象的论文,它使用一种新颖的序列到序列(Seq2Seq)模型来简化句子。
[## [1704.02312v1]用于句子简化的受限序列到序列神经模型
摘要:句子简化降低了语义复杂性,有利于有语言障碍的人。上一个…
arxiv.org](https://arxiv.org/abs/1704.02312v1)
在这篇论文中,中国北京大学的研究人员利用 seq2seq RNN 模型和注意方法来简化句子。他们将此与单词级替换相结合,产生了最先进的简化结果。
顺便问一下,你喜欢阅读最近的 ML 研究论文的简短摘要吗?查看arxiv . email,那可是个 tl 的每周简讯;本周最佳出版研究博士。
句子简化的任务对于提高可读性很重要。这有利于非母语人士、儿童以及患有自闭症和阅读障碍等语言障碍的个人。我的许多团队都接触过这个任务,大多数团队都使用基于规则的系统来尝试简化任务。有的用词级替换,简单的把复杂的词换成简单的词。本文试图利用神经机器翻译的最新发展来简化句子。
“序列到序列”递归神经网络(RNN)模型是最近提出的,并证明了神经机器翻译(NMT)的优秀结果。这个模型接受一个输入序列(一种语言的句子),并训练一个模型生成一个不同语言的输出序列。事实证明,这种模式比 NMT 大学以前的方法更有效,显然现在谷歌翻译也在使用这种模式。
研究人员使用了从维基百科上搜集的句子匹配数据集。这是由 David Kauchek 创建的,包括来自英语维基百科和简单英语维基百科的 137,000 对匹配句子。
建模过程通常可总结如下:
- 用一个复杂的单词代替原句中的一个简单的单词。
- 把句子分成两部分,从“简单”替换词处开始。例如:
- 就拿“他成为了一个有名的作文老师,有很多有名的学生”这句话来说。
- 用作曲代替音乐。它变成了“…一个著名的音乐老师…”。
- 将替换词处的句子分成两部分。第一部分颠倒了。
- 第一部分成为“众所周知的一个成了他”
- 第二部分是“老师,有很多有名的学生”
然后,将每个单独的句子片段通过以下步骤:
- 取原句子部分,预处理成整数向量。他们使用一本 60000 字的词典。每个单词代表字典中的一个数字。未知单词由通用的“UNK”单词代替。
- 将输入句子的每个单词通过编码层:
- 将矢量化的单词通过单词嵌入层。
- 将嵌入层通过具有门控循环单位(GRU)细胞的双向 RNN (BiRNN)。
最后,将 BiRNN 的最终输出作为编码层的输出。
- 通过解码层传递最终的解码器状态,这是一个具有注意机制的 GRU RNN。
- 对于解码层中的每一步,通过 softmax 层传递 RNN 输出,以获得预测的最终字。
- 将两个生成的句子片段与替换单词组合,以创建最终生成的句子。
上述过程只是用一个复杂的单词代替一个更简单的单词。如果有多个需要替换的复杂单词,研究人员将对每个单词替换进行一次上述过程。所以如果有三个复杂的单词,他们会经历上述过程三次。他们会用第一个单词做一次,然后在替换了一个不同的单词后,获取输出并再次做整个过程。
Examples of outputs for different sentence simplification models. This paper’s approach is at the bottom.
上图显示了他们的最终模型(多约束 Seq2Seq)以及其他类似模型的输出。你可以看到许多模型根本没有简化句子,即使是一个简单的 Seq2Seq 模型。词汇模型只是用复杂的词代替简单的词,而不改变句子结构。词汇模型也只能将“一个键”变成“一个重要的”,而它应该是“一个重要的”。他们最终的模型不仅可以替换多个单词,而且可以重新排列句子,删除不必要的片段,语法正确,仍然保留原句的意思。
感谢阅读!最后一个不要脸的塞:check outarxiv . email,那可是个 tl 的周刊快讯;本周最佳出版研究博士。
情感分析:概念、分析和应用
情感分析是对文本进行上下文挖掘,识别和提取源材料中的主观信息,并帮助企业在监控在线对话的同时了解其品牌、产品或服务的社会情感。然而,社交媒体流的分析通常仅限于基本的情感分析和基于计数的指标。这类似于只触及表面,错过了那些等待被发现的高价值见解。那么,一个品牌应该如何抓住这个唾手可得的果实呢?
随着深度学习的最新进展,算法分析文本的能力有了很大提高。创造性地使用先进的人工智能技术可以成为进行深入研究的有效工具。我们认为,根据以下线索对客户关于某个品牌的对话进行分类非常重要:
- 顾客关心的品牌产品和服务的关键方面。
- 用户对这些方面的潜在意图和反应。
这些基本概念结合使用时,会成为一个非常重要的工具,用于以人类水平的准确度分析数百万次品牌对话。在这篇文章中,我们以优步为例,展示这是如何运作的。请继续阅读!
文本分类器——基本构件
情感分析 情感分析是最常见的文本分类工具,它分析收到的消息,并判断潜在的情感是积极的、消极的还是中性的。你可以输入一个你选择的句子,并通过这里的演示来判断潜在的情绪。
意图分析 意图分析通过分析消息背后的用户意图,并识别它是否与意见、新闻、营销、投诉、建议、赞赏或查询有关,从而加快游戏的进程。
Analyzing intent of textual data
上下文语义搜索(CSS)
这就是事情变得真正有趣的地方。为了获得可行的见解,了解用户讨论的是品牌的哪个方面是很重要的。例如:Amazon 想要隔离与以下内容相关的消息:延迟交付、账单问题、促销相关的查询、产品评论等。另一方面,星巴克希望根据信息是否与员工行为、新咖啡口味、卫生反馈、在线订单、商店名称和位置等相关来对信息进行分类。但是怎样才能做到呢?
我们引入了一种智能的智能搜索算法,称为上下文语义搜索(又名 CSS) 。CSS 的工作方式是,它将数千条消息和一个概念(如价格)作为输入,并过滤所有与给定概念紧密匹配的消息。下图展示了 CSS 如何代表了对业界使用的现有方法的重大改进。
Existing approach vs Contextual Semantic Search
过滤所有与价格相关的消息的传统方法是对价格和其他密切相关的词进行关键字搜索,如(定价、收费、$、已付)。然而,这种方法不是很有效,因为几乎不可能想到所有相关的关键字及其代表特定概念的变体。另一方面,CSS 只是将概念的名称( Price )作为输入,并过滤所有上下文相似的内容,即使没有提到概念关键字的明显变体。
对于好奇的人,我们想让他们看一下这是如何工作的。人工智能技术用于将每个单词转换为超空间中的特定点,这些点之间的距离用于识别上下文与我们正在探索的概念相似的消息。下面可以看到引擎盖下的可视化效果:
Visualizing contextually related Tweets
在下面的例子中,我们来看看 CSS 是如何工作的,以及它是如何处理与优步相关的评论的:
同样,看看这条推文:
在上述两种情况下,该算法将这些消息归类为与名为 P rice 的概念上下文相关,即使在这些消息中没有提到单词 Price 。
优步:深潜分析
全球估值最高的初创企业优步一直是共享经济的先驱。优步在全球 500 多个城市开展业务,为庞大的用户群提供服务,收到了大量用户的反馈、建议和投诉。通常,社交媒体是登记此类问题的最受欢迎的媒体。海量的传入数据使得分析、分类和生成见解成为一项具有挑战性的任务。
我们分析了数字媒体上发生的关于几个产品主题的在线对话:取消、支付、价格、安全和服务。
为了广泛覆盖数据来源,我们从优步官方脸书页面上的最新评论、提到优步的推文以及优步的最新新闻文章中获取数据。以下是所有渠道的数据点分布:
- 脸书:34173评论
- 推特:21603 条推特
- 新闻:第 4245 篇
分析用户对话的情感可以让你对整体品牌认知有所了解。但是,为了更深入地挖掘,借助上下文语义搜索进一步对数据进行分类是很重要的。
我们在同一个数据集上运行了上下文语义搜索算法,考虑了前面提到的类别(取消、支付、价格、安全和服务)。
脸谱网
情绪分析
Breakdown of Sentiment for Categories
值得注意的是,除了一个类别,所有类别的评论都有负面情绪。与价格相关的正面评论数量已经超过负面评论。为了更深入地挖掘,我们分析了这些评论的意图。作为一个社交平台,脸书的评论充斥着随机内容、新闻分享、营销和推广内容以及垃圾邮件/垃圾/无关内容。看看脸书评论上的意图分析:
Intent analysis of Facebook comments
脸书评论的意图分析
因此,我们删除了所有这些不相关的意图类别,并重现了结果:
Filtered Sentiment Analysis
每个类别的情感都有明显的变化。尤其是在价格相关评论中,正面评论数量从 46%下降到 29%。
这让我们看到了 CSS 如何从数字媒体中产生深入的见解。因此,一个品牌可以分析这样的推文,并从它们的积极点或从消极点获得反馈。
推特
情感分析
对抓取的推文也做了类似的分析。在最初的分析付款和安全相关的推文中有一种复杂的情绪。
Category wise sentiment analysis
为了了解真实的用户意见、投诉和建议,我们必须再次过滤不相关的推文(垃圾邮件、垃圾广告、营销、新闻和随机消息):
Filtered sentiment
与付款相关的推文数量明显减少。此外,针对类别安全(以及相关关键词)的正面推文数量大幅下降。)
此外,取消、支付和服务(以及相关词汇)是推特上评论中谈论最多的话题。似乎人们谈论最多的是司机取消乘车和向他们收取的取消费。看看这条推文:
像优步这样的品牌可以依靠这种洞察力,并根据最关键的话题采取行动。例如,服务相关推文的正面推文比例最低,负面推文比例最高。因此,优步可以分析这些推文,并根据它们采取行动,以提高服务质量。
新闻ˌ消息
Sentiment Analysis for News headlines
可以理解的是,安全一直是新闻中谈论最多的话题。有趣的是,新闻情绪总体上是积极的,在每个类别中也是如此。
我们也根据新闻的受欢迎程度进行分类。流行度得分归因于文章在不同社交媒体渠道上的分享计数。以下是热门新闻文章列表:
- 优步首席执行官因批评离开特朗普顾问委员会
- #DeleteUber:用户对川普穆斯林禁令废 app 感到愤怒
- 优步员工也讨厌他们自己的企业文化
- 每次我们获得优步,我们就在传播它的社会毒素
- 司机在抗议和罢工期间前往 JFK 机场后,愤怒的消费者删除了优步的应用程序
结论
随着技术的进步,从社交媒体数据中获得有意义见解的时代已经到来。优步的案例研究让你一瞥上下文语义搜索的威力。您的组织是时候超越总体情绪和基于计数的指标了。公司最近一直在利用数据的力量,但要获得最深层的信息,你必须利用人工智能、深度学习和智能分类器的力量,如上下文语义搜索和情感分析。在 Karna ,您可以联系我们以获得我们技术的许可,或者获得定制的仪表板,以便从数字媒体中生成有意义的见解。你可以点击查看演示。
parallel dots AI API,是由 ParallelDots Inc 提供的深度学习支持的 web 服务,可以理解大量的非结构化文本和视觉内容,为您的产品提供支持。你可以查看我们的一些文本分析API并通过填写此处的表格联系我们或者给我们 apis@paralleldots.com 写信。
R——好与不好——处理否定中的情感分析
Image credit: https://pixabay.com/en/eggs-colors-easter-holiday-spring-3398664/
情感分析是拥有未标记文本数据(没有分数或评级)的数据分析师最终试图从中提取一些见解的最明显的事情之一,同样的情感分析也是任何自然语言处理(NLP)爱好者的潜在研究领域之一。
对于分析师来说,同样的情感分析是一件痛苦的事情,因为大多数处理情感分析的原始包/库执行简单的字典查找,并根据正面和负面单词的出现次数计算最终的综合得分。但这往往会导致许多假阳性,一个非常明显的例子是“好”对“不好”——否定,通常是价移。
考虑一下这句话:‘我不太好’。任何原始的情感分析算法只会将这句话标记为肯定的,因为单词“good”显然会出现在肯定词典中。但是读这句话我们知道这不是一个肯定句。
虽然我们可以构建自己的方式来处理这些否定,但有几个新的 R-package 可以轻松做到这一点。泰勒·林克开发的[sentimentr](https://github.com/trinker/sentimentr)
就是这样一个软件包。
视频教程
安装软件包
sentimentr 可以从 CRAN 安装,也可以从 github 安装开发版。
install.packages('sentimentr')
#or
library(devtools)
install_github('trinker/sentimentr')
为什么是 sentimentr?
这个包的作者自己解释了sentimentr
做了什么而其他包没有做,为什么这很重要?
sentimentr 试图在保持速度的同时考虑变价词(即否定词、增强词、去增强词和转折连词)。简单地说,sentimentr 是一个扩充的字典查找。接下来的问题是为什么它很重要。”
情感评分:
sentimentr 提供了两个情感分析功能:1 .sentiment_by()
2。sentiment()
使用sentiment_by
的给定文本的聚合(平均)情感分数
sentiment_by('I am not very good', by = NULL)element_id sentence_id word_count sentiment
1: 1 1 5 -0.06708204
但是当我们有多个不同极性的句子时,这可能没有多大帮助,因此用sentiment
进行句子级评分会有所帮助。
sentiment('I am not very good. He is very good')element_id sentence_id word_count sentiment
1: 1 1 5 -0.06708204
2: 1 2 4 0.67500000
这两个函数都返回一个包含四列的数据帧:
1.element_id
–给定文本的 ID /序列号
2。sentence_id
–句子的 ID /序列号,在sentiment_by
3 的情况下等于 element_id。word_count
–给定句子的字数
4。sentiment
–给定句子的情感得分
提取情感关键词
extract_sentiment_terms()
功能帮助我们提取关键词——正面的和负面的,这是情感得分计算的一部分。sentimentr 还支持管道操作符%>%
,这使得编写多行代码变得更容易,赋值更少,代码也更简洁。
'My life has become terrible since I met you and lost money' %>% extract_sentiment_terms()
element_id sentence_id negative positive
1: 1 1 terrible,lost money
情绪突出:
最后,highight()
函数与sentiment_by()
相结合,给出一个 html 输出,其中部分句子用绿色和红色突出显示,以显示其极性。相信我,这可能看起来微不足道,但它确实有助于在做演示时分享结果,讨论假阳性,并确定准确性方面的改进空间。
'My life has become terrible since I met you and lost money. But I still have got a little hope left in me' %>%
sentiment_by(by = NULL) %>%
highlight()
输出截图:
Sentiment Highlight
尝试将sentimentr
用于您的情感分析和文本分析项目,并在评论中分享您的反馈。此处使用的完整代码可在 my github 上获得。欲了解更多信息,请查看本数据营课程——R 中的情绪分析——整洁的方式
情绪分析:贾斯廷·特鲁多的推文
我在网上到处看到对特朗普推文的情绪分析。这是有道理的——没有一位总统、政治人物或任何人真正像特朗普那样使用 Twitter。阅读这些对特朗普推文的分析,他的情绪到处都是。但是你不需要一个文本处理工具来使用它,你只要听听他的演讲或者读读新闻就能明白。然而,这让我想到,我的总理贾斯廷·特鲁多的推特看起来像什么?他的推文在情绪上更平衡吗?他用英语发的微博和用法语发的微博有区别吗?我们来看看吧!
为了这个分析,我会看看一些基本的推文特征:来源,“转发”和“收藏”计数,以及英语和法语推文的总体情绪。虽然推文的来源和观点让我们深入了解特鲁多,但转发和最受欢迎的计数代表加拿大人说话。通过这一分析,我们将对我们的总理有更多的了解,对我们自己也有更多的了解。
加拿大不一样
我们先来看看加拿大人在 2016 年 12 月至 2017 年 10 月期间对特鲁多推文的反应。这当然考虑了所有以转发或收藏的形式与@JustinTrudeau 互动的 Twitter 用户,但我现在假设这些用户中的大多数是加拿大人。
这是一个非常有趣的结果。到目前为止,转发和收藏最多的英文推文是:
毫无疑问,这条推文获得了大量关注。这可能有很多原因。首先,加拿大可能是不同的。我们欢迎,加拿大人喜欢我们的总理宣传这一点。第二,响应可能来自非加拿大公民(这将使我上面的假设无效),也许来自难民自己。第三,这种反应可能主要是由美国人组成的。随着(当时)唐纳德·特朗普(Donald Trump)成为新的@POTUS,美国人可能会转发并喜欢这条推文,以此表示“看看加拿大有多不同。”
同样的法语推文获得了最多的转发,但另一条推文获得了最多的收藏。
最受欢迎的推文是:
事实上,这条推特比英文版获得了更多的转发。似乎法裔加拿大人,或者说法语的人,热衷于支持 LGBTQ 群体,并且喜欢从总理那里听到这些。这个帖子的收藏数量和转发数量之间的差距很奇怪——也许法裔加拿大人不喜欢转发这种事情?
来源和长度
接下来,我们来看看特鲁多的推文来源,以及它们的长度。
因此,特鲁多似乎使用 iPhone,但更喜欢在电脑上发推文。这与特朗普截然不同,他似乎更喜欢在手机上发推文。
现在,对于特鲁多的推文长度。
情感分析
完成基本的探索后,让我们继续进行情感分析。在这个分析中,我使用了 VADER 和 TextBlob,因为 VADER 针对社交媒体进行了优化,但是 TextBlob 针对法语进行了优化。我不仅想比较英文和法文的感悟,也想比较这两个包。
让我们先来看看使用 VADER 和 TextBlob 的结果。
这里的一个主要区别是,TextBlob 认为这两种语言的推文更加中性,而 VADER 认为特鲁多的英语推文更加积极。让我们将分析分为英语和法语,并再次比较 VADER 和 TextBlob,以便更加清晰。
在英语中,看起来 TextBlob 更经常分配正面的推文,但当推文对 VADER 是正面的时,它们是非常正面的。在法语中,这两个软件包产生了更多相似的结果。
最后我们来看看特鲁多推文的情绪,逐月来看。
有趣的是,与法语推文相比,使用 VADER 和 TextBlob 的英语推文给人的感觉更积极。法语是一门很美的语言,但英语只是更热情吗?或者这只是特鲁多倾向于发推特的方式,或者这可以归因于他对每种语言的掌握以及他更习惯的东西?很高兴看到这种情绪在两种语言和两种文本处理工具中都遵循相似的模式。看到 10 月份情绪的下降也很有趣——考虑到过去两天在埃德蒙顿和拉斯维加斯发生的可怕事件,这是有道理的。我们必须等着看这种下降趋势是否会继续。但如果是这样,这可能说明两件事:这可能是世界事件的反映,或者这可能意味着特鲁多作为总理感到疲惫。
感谢阅读!
这个分析的代码可以在 my github 上的一个 Jupyter 笔记本里找到。
原发布于 我的个人网站 。
印地语和马拉地语音译文本的情感分析
这个博客是关于我在自然语言处理领域的论文的陈述。情感分析对英语来说很容易。对于其他语言,如印地语或马拉地语来说,这有点困难,但不是很难。当用于其他语言的文字是罗马文字时,这就变得困难了。最困难的是当音译文本是多种语言的混合时。在我们的例子中,我们考虑英语-印地语和英语-马拉地语的两种组合。这种语言的混合被称为语码混合。通常其中一种语言(通常是罗马文字)用于文本表示。如何分析这种情况下的情绪?
普通情感分析
通常分析中涉及的步骤如下:
- 规范化单词标记
- 执行词性标签分析,以获取被标记为名词、形容词、副词和动词的单词。
- 使用 wordnet 获取标识符,使用 senti-wordnet 获取与这些单词相关的情感,以及我们在 POS 标签分析中找到的相应标签
- 将中的单词转换为特征:
- 删除停用词
- 去掉不重要的词,如除了形容词和副词之外的一切,因为它们在情感方面是最重要的
- 创建一个 ngram 特性,将单词与其上下文联系起来
- 将其输入分类器进行训练,然后对主题进行测试以计算准确度
- 交叉验证用于不过度训练
- 找到一个阈值来平衡过度训练和分析看不见的数据的一般性
混合代码通用方法的问题
虽然这些步骤比较复杂,我也错过了一些,但是一般的方法已经包含在上面的步骤中了。音译混合代码的唯一问题是它不能与上述步骤一起工作。为什么?因为音译的文字没有固定的语法,单词拼写也不正确。句子结构不遵循单一语言的特定规则。如果把每种语言的语块都拿出来,那么很多情况下;他们没有形成各自语言的正确语法。最后一部分由于语言规则的混合而发生。一个很好的例子是:缅因州 tujhe 祝你好运因为 tumse na hoga。在前面的陈述中,英语的用法遵循印地语的上下文语法,因此在这种情况下,它们的词性标注及其相关的情感是非常不明确的。由此得出结论,混合代码脚本需要一种不同的方法来实现这一目的,而这正是我的论文所要讨论的。
提议的方法
因为数字说明了千言万语,所以我让下面的数字来说明我为了进行情感分析而采取的方法。
上图给出了这种方法所需的所有重要步骤,下面再次列出,供那些对花时间在图上不感兴趣的人参考。
- 看看单词 tokens 是一个表情符号还是一个众所周知的俚语,比如 lol 等等,然后用合适的语言作品来替换它们,比如😊会变成微笑,lol 会变成放声大笑。
- 音译北印度语/马拉地语脚本(梵文)中的剩余单词,并在北印度语/马拉地语词典中查找其存在。检查拼写变化。如果找到了,就贴上标签。
- 在英语词典中查找所有单词,并标记它们。如果与另一种语言存在平局,那么使用词频概率来打破平局。
- 根据每种语言(印地语/英语)的语言标签,从 sentiwordnet 中循环得出每种语言的情感分数。除了马拉地语,目前还没有 sentiwordnet,在这种情况下,使用双语词典从印地语和英语中获取马拉地语单词的含义,然后按照这些语言的过程合并分数。
- 最后,使用 ngrams 将单词转换为具有相应情感得分的特征,并对训练和测试数据进行交叉验证,以查看结果的准确性。
这些步骤实现起来要复杂得多,在使用 python Scikit-learn 实现这些步骤后,我能够得到如下图所示的结果。
最糟糕的是,即使是这些也不如针对单一语言的情感分析有效。对于印地语和英语,我最多能做到 50%以上的准确率,对于马拉地语和英语的组合,我能做到 80%的准确率。现在 80%看起来不错,但有一个警告。马拉地语/英语组合的问题是,固定单词的存在通常会扭曲数据。我使用的数据来自 youtube 评论,在积极和消极的群体中都有一些词,这往往是情绪极性的绝对指标。这使得系统工作,但事实上它真的不工作。我对结果并不感到自豪,但我很高兴我从中学到了很多。下次见。
原载于 2017 年 8 月 3 日【www.3371aspectz.co】。
基于多项式朴素贝叶斯的微博情感分析
Based on illustration from http://eppsnet.com/2018/05/reflecting-on-mathematical-techniques/.
Web 2.0 的普及显著增加了在线交流。因此,它引发了自然语言处理领域,特别是情感分析领域的快速发展研究。信息过载以及评论和消息数量的增长促进了对高性能自动处理方法的需求。
本文致力于使用具有多项式分布的朴素贝叶斯分类器进行二元情感分析。我们简要概述了从概率模型构建分类器,然后进入数据预处理、训练和超参数优化阶段。我们将使用 Jupyter Notebook 用 Python 编写我们的脚本。
它是如何工作的
多项式朴素贝叶斯分类算法趋向于情感分析任务的一个基线解。朴素贝叶斯技术的基本思想是通过使用词和类别的联合概率来找到分配给文本的类别的概率。让我们简单看一下数学。
Bayes’ theorem spelt out in blue neon at the offices of Autonomy in Cambridge.
给定从属特征向量 (x₁,…,xn) 和类 Ck 。贝叶斯定理在数学上表述为以下关系:
根据“天真”的条件独立性假设,对于给定的类 Ck 矢量xi 的每个特征都是有条件独立于其他每个特征XJforI≠j…
因此,该关系可以简化为
由于***【p(x₁,…,xn)*** 是常数,如果特征变量的值是已知的,可以使用下面的分类规则:
为了避免下溢,可以使用对数概率。
各种朴素贝叶斯分类器之间的主要区别在于它们对 P(xi|Ck) 的分布所做的假设,而 P(Ck) 通常被定义为训练数据集中类 Ck 的相对频率。
多项式分布由针对每个类别***【Ck】***的向量 θk=(θk1,…,θkn) 来参数化,其中 n 是特征的数量(即,词汇的大小),并且 θki 是特征 的概率 P(xi|Ck)
参数 θk 通过最大似然的平滑版本,即相对频率计数来估计:
其中 Nki 是特征 i 出现在训练集 T 中类 k 的样本中的次数,而 Ny 是类 Ck 的所有特征的总数。平滑先验 α≥0 考虑了学习样本中不存在的特征,并防止了进一步计算中的零概率。设置 α=1 称为拉普拉斯平滑,而 α < 1 称为李德斯通平滑。
因此,最终决策规则定义如下:
加载情感数据
俄语推文的情感数据集[1]可在http://study.mokoron.com/获得。文件 positive.csv 和 negative.csv 分别包含带正标签和带负标签的推文。
import pandas as pd
import numpy as np# Read the data from CSV files
n = ['id', 'date','name','text','typr','rep','rtw','faw','stcount','foll','frien','listcount']
data_positive = pd.read_csv('positive.csv', sep=';',error_bad_lines=False, names=n, usecols=['text'])
data_negative = pd.read_csv('negative.csv', sep=';',error_bad_lines=False, names=n, usecols=['text'])# Create balanced dataset
sample_size = min(data_positive.shape[0], data_negative.shape[0])
raw_data = np.concatenate((data_positive['text'].values[:sample_size],
data_negative['text'].values[:sample_size]), axis=0)
labels = [1]*sample_size + [0]*sample_size
预处理数据
社交媒体网站中由人类生成的文本包含大量噪声,这些噪声会显著影响情感分类过程的结果。此外,根据特征生成方法的不同,每个新术语似乎都给特征空间增加了至少一个新的维度。这使得特征空间更加稀疏和高维。因此,分类器的任务变得更加复杂。
为了准备消息,在该程序中使用了文本预处理技术,如用关键字替换 URL 和用户名、删除标点符号以及转换成小写字母。
import redef preprocess_text(text):
text = re.sub('((www\.[^\s]+)|(https?://[^\s]+))','URL', text)
text = re.sub('@[^\s]+','USER', text)
text = text.lower().replace("ё", "е")
text = re.sub('[^a-zA-Zа-яА-Я1-9]+', ' ', text)
text = re.sub(' +',' ', text)
return text.strip()data = [preprocess_text(t) for t in raw_data]cc
训练多项式朴素贝叶斯
管道类用于使矢量器= >转换器= >分类器更容易使用。诸如 n-grams 范围、IDF 使用、TF-IDF 规范化类型和朴素贝叶斯 alpha 之类的超参数使用网格搜索来调整。所选超参数的性能是在模型训练步骤中未使用的测试集上测量的。
from sklearn.pipeline import Pipeline
from sklearn.naive_bayes import MultinomialNB
from sklearn.feature_extraction.text import CountVectorizer, TfidfTransformer
from sklearn.model_selection import train_test_split, GridSearchCVtext_clf = Pipeline([('vect', CountVectorizer()),
('tfidf', TfidfTransformer()),
('clf', MultinomialNB())])tuned_parameters = {
'vect__ngram_range': [(1, 1), (1, 2), (2, 2)],
'tfidf__use_idf': (True, False),
'tfidf__norm': ('l1', 'l2'),
'clf__alpha': [1, 1e-1, 1e-2]
}
数据集被分成训练和测试子集。
x_train, x_test, y_train, y_test = train_test_split(data, labels, test_size=0.33, random_state=42)
最后启动了 10 重交叉验证的网格搜索。
from sklearn.metrics import classification_reportclf = GridSearchCV(text_clf, tuned_parameters, cv=10, scoring=score)
clf.fit(x_train, y_train)
print(classification_report(y_test, clf.predict(x_test), digits=4))
测试数据集的分类报告如下所示。
precision recall f1-score support
0 0.7397 0.7941 0.7659 37078
1 0.7759 0.7183 0.7460 36792
avg / total 0.7577 0.7564 0.7560 73870
根据网格搜索结果,在开发集上找到的最佳参数集如下:clf__alpha=1
、tfidf__norm=l2
、tfidf__use_idf=True
、vect__ngram_range=(1, 2)
。
结果
在开发集上训练的模型在评估集上展示了 F₁=0.765 。通过使用词干化、规范化、句法和语义特征等技术,分类度量可能会得到更大的提高。
Github 提供了源代码。
使用嵌入 Word2Vec 的卷积神经网络(CNN)对俄语推文进行情感分析。…
github.com](https://github.com/sismetanin/sentiment-analysis-of-tweets-in-russian/blob/master/Sentiment%20Analysis%20of%20Tweets%20in%20Russian%20using%20Multinomial%20Naive%20Bayes.ipynb)
参考
- Y.Rubtsova,“为情感分类训练构建语料库”,《软件与系统》,第 109 卷,第 1 期,第 72–78 页,2015 年。
- 《朴素贝叶斯》,scikit-learn.org,2018 年。【在线】。可用:http://scikit-learn.org/stable/modules/naive_bayes.html。[访问日期:2018 年 8 月 26 日]。
- “使用文本数据”,scikit-learn.org,2018 年。【在线】。可用:http://sci kit-learn . org/stable/tutorial/text _ analytics/working _ with _ text _ data . html。[访问日期:2018 年 8 月 26 日]。
NBA 顶级球员推特账户的情感分析——第二部分推特数据清理
<本文其他部分的快速链接>
Part2 Tweets 数据清理
摘要
深入研究社交网络上的文本处理
在这一部分,我们将清理推文,因为它们包含不必要的文本,我们也将处理特殊情况。
由于人们经常使用特殊的字符、标点符号或表情符号来表达他们的感受,因此删除除字母以外的所有内容并不是处理推文的最佳方式。例如:
“在中国,据说‘吃了金汤面,便得了金牌’!!明天,如果我一顿饭吃 9 个,也许我能拿到那份财富里的 MVP:)。🍜😋🏀”
上面的句子显然是一个强烈的积极反应,不仅仅是因为词汇,还因为像“!!"、“😃”和😋。如果抹去这些符号,整个表达可能会变得比以前弱得多,或者在最坏的情况下变成完全相反的意思。此外,许多推文只包含特殊字符,因为它们使用起来很方便。简单地把它们拿出来会导致一个巨大的问题。
第 2 部分的先决条件
文本处理
因此,我们只删除一些看起来确实不必要的单词。@ '提及:删除“@ username”(Twitter 中的网名)无论是来自转推还是原创推文。我们知道标记某人是分享强烈情感的一种方式,但我们不在这里讨论。
2。URL:删除所有可能的 URL 链接,因为我们不打算深入链接
3。停用词:去掉没有太大意义的常用词。标点符号:删除标点符号仍然很重要,但我们必须确保我们首先保留了重要的单词或符号,否则它们会被无意识地删除。
此外,我们将以下内容保留在原始格式中,因为我们相信它们需要纳入分析中。表情符号:😋,😢…等等
2。特殊字符:“😦”、“><”…等
3。标签的标签:(#) behappy…etc
4。一些标点:“!!"、“~”、“…”…等等
接下来,我们将开始处理我们的文本分为两部分,第一部分只是标记和删除不必要的单词。第二个是应用两种词干方法,这样我们可以减少实际上具有相同含义的独特单词。这是我做的清理函数:
如果 tweets_cleaner() 的输出是这样的。这里我们只是删除了@ '提及、URL 和一些很少有特殊意义的标点符号。注意,停用词处理在下一个函数中,因为我们需要在移除停用词之前对其进行标记。
如果 tokenization_and_stem() 的输出如下所示。您可以看到每种标记化和词干化方法之间的区别。现在我们可能会看到一些标记看起来毫无意义,例如“😃”变成了“:”和“)”。但是请记住,我们将再次把这个放回句子结构中!
现在,把记号列表放回句子结构中
最后,我们得到了干净的句子!back_to_clean_sent()只是将单词连接成一个句子。
- sentence_tokenized:删除了停用词的已处理句子。
- sentence_snowstemmeed:用 SnowballStemmer 方法处理去掉停用词的句子和词干处理。
这是第一个播放器的输出,去掉了一些停用词、标点符号和最有可能没有意义的特殊符号。请注意,在 NLP 中很难获得准确的干净句子,尤其是当我们不针对特定主题或领域时。
以上是对 tweets 的数据清理,第三章我们将实现情感分析和数据聚类。
NBA 顶级球员推特账户的情感分析(三)
情感分析和聚类
<本文其他部分的快速链接>
Part3 情感分析&聚类
摘要
终于到了主菜的时间了。让我们回顾一下上一部分的一些结果:
- sentence_tokenized:处理去掉停用词的句子。
- sentence_snowstemmeed:使用 SnowballStemmer 方法处理去掉停用词的句子和词干处理。
我们将对原始 tweets、sentence _ tokenized 和 sentence _ snowstemmeed 实施情感分析,以观察这些方法之间的差异并检查它们的效果。当谈到情感分析时,我们使用三种常见的类型,本章将选择基于规则的方法,因为它更容易应用,并且不需要实际的训练集来训练数据。
- 基于规则的:基于一套预定义的规则和情感评分进行情感分析。
- 自动:利用机器学习技术从数据中学习。
- 混合的:结合基于规则和自动的方法。
稍后,我们尝试对玩家进行聚类,以查看每组玩家在 K-means 和潜在狄利克雷分配中是否有相似的词语使用行为。
第 3 部分的先决条件
我们使用一个名为Vader perspectivity的包,这是一个基于词汇和规则的情绪分析工具,专门用于社交媒体中的情绪分析,因为它在特殊字符上工作得很好,如表情符号和一些标点符号的组合。我们可以认为 lexicon approach 只是一个情感词典,我们只是根据每个单词搜索相应的情感得分,并且还考虑得分应该基于句子的每个长度和特征有多大的权重。
现在,你可能会想,每个人的情绪可能会非常不同。人们对同一个词有不同的表达。为了解决这个问题,软件包的创建者收集了大量的样本,并对每个单词的评分进行平均。
情感分析
然后,我们创建一个情感分析(),这只是一个使我们能够分析大量句子的方法:
以下是第一位玩家基于简单的标记化方法对每条推文的情感评分结果:
这里的复合得分是通过将词典中每个词的化合价得分相加,根据规则进行调整,然后归一化到-1(负得分的最大值)和+1(正得分的最大值)之间来计算的。按照惯例,复合得分≥ 0.05 视为积极情绪;≤ -0.05 是负面情绪;介于-0.05 和 0.05 之间的分数被视为中性。
现在让我们仔细看看原始推文、标记推文和词干推文的情感得分。下图显示了原始推文以及基于不同方法的评分。看出什么不同了吗?被处理过的句子现在往往有更强烈的情绪!
现在,我们根据前面的定义将复合得分转换为“正”、“中性”、“负”,并放入数据框中。
使聚集
在开始聚类之前,我们必须首先创建 TF-IDF 矩阵和模型。TF-IDF 指的是词频-逆文档频率,它是两个统计量的乘积,这两个统计量反映了一个词对集合或语料库中的文档有多重要:
为了简单起见,这里我们只使用带词干的句子并生成 TF-IDF 矩阵。 Tweets_mix 只是一个收集所有词干句子的列表,只有这样我们才能生成 TF-IDF 矩阵。
k 均值聚类
首先,我们实现了聚类的 K-means,在本文中我们选择了 3 个聚类:
接下来,输出玩家及其对应的集群:
请记住,我们试图将玩家分组,看看每组玩家在选择词汇时是否有相似的行为。换句话说,我们需要知道每个聚类的单词集。然而,根据前面的过程,我们使用词干化的单词来生成 TF-IDF 矩阵,这意味着每个聚类的单词集也是词干化的单词。
例如,如果聚类 1 显示“famli”和“happi”是他们最常用的词,我们知道“famli”等于“家庭”,“happi”等于“快乐”;虽然对我们来说并不总是那么简单。
为了解决这个问题,我们必须翻译回我们非常熟悉的单词。为此,我们创建了一个字典,比如{“happi” : “happy”},这样当我们计算频繁出现的词干时,就可以立即追溯到原来的单词。 docs_tokenized 和 docs_snowstemmed 是包含根据每个玩家的标记化和词干化的单词的列表,我们将使用它们进行我们的原始单词翻译。
然后,我们输出每个玩家对应的聚类和他们经常使用的单词:
在输出中,我们可以看到像斯蒂芬·库里或安东尼·戴维斯这样的球员倾向于在推特上多次使用相同的特定词语。另一方面,像拉塞尔·维斯特布鲁克这样的球员通常会标记他所属的州。
请注意,当每个集群中的集群数量分布不均匀时,您可能会得到一个坏的集群。此外,这里的集群似乎没有太多的信息,但我们所说的只是为了证明我们也可以在 NLP 上实现集群。
潜在的狄利克雷分配(主题建模)
潜在狄利克雷分配 ( LDA )是一种生成模型,它使样本集由未观察到的组来解释,并显示为什么数据的某些部分彼此相似。在 LDA 中,每个文档可以被视为各种主题的混合,其中每个文档被认为具有通过 LDA 分配给它的一组主题。
假设观察值是 LDA 文档中的单词,那么我们说该文档是各种小主题的混合,每个单词的创建可归因于文档的一个主题,如下图所示:
source: Failure Prognosis of High Voltage Circuit Breakers with Temporal Latent Dirichlet Allocation
从数学的角度来看,基本的直觉是这样的:
在本文中,我们设置了 3 个组件,因为我们在 K-means 方法中也有 3 个聚类。 n_stop_words 表示我们希望看到每个主题中的前 3 个常用词。数字设置为 4 是因为索引问题,所以我们必须为 3 的结果加上 1。LDA 方法如下:
同样,下面是每个玩家对应集群的输出以及他们经常使用的单词:
对情感的分析和聚类就是这样。第四章我们要去检验选手的成绩之间是否存在相关性,在相关性测试之前做一些 EDA。
NBA 顶级球员推特账户的情感分析——第四部分
绩效和情绪之间的相关性测试
<本文其他部分的快速链接>
Part4 绩效之间的相关性测试&情绪
摘要
经过漫长的过程,我们终于要看到球员在球场上的表现与他们的推特之间是否有任何统计相关性。现在让我们看看会发生什么!
第 2 部分的先决条件
在我们开始之前,可视化和相关性测试需要这些包:
数据可视化
在这里,我们将绘制一些图表,以便对每个玩家的情绪得分有一个简单的了解。可惜由于数据本身的限制,只有两个图看起来有点用。
首先,我们来看看 2016-2018 年选手的平均情绪得分。很明显,所有的玩家通常都发布积极的推文,这在现实中是很符合逻辑的,对吗?这里,我们在标记化方法和词干化方法中实现情感评分。
来自标记化方法的情感分数越浅,来自词干化方法的情感分数越深。词干法中的分数都较低,但不会对结果产生太大影响。
此外,我们绘制出每个玩家的每月情绪得分。下面的图有两条蓝色水平线,表示积极、中性和消极情绪。在-0.05 和 0.05 线之间为中性;以上为正;下方是负数。
绩效和情绪之间的相关性测试
最后,还记得我们的终极目标吗?我们很好奇球员赛前的情绪会不会和他们的表现有什么关联。
**可悲的是,我们的文章指出在皮尔逊相关性检验下它们之间没有相关性。**至少我们知道 NBA 球员在场上的时候都很专业。他们能够忘记除了游戏之外的一切!
这就是本文的全部任务。谢谢,喜欢就鼓掌!!
美国 Twitter 航空公司数据集上的情感分析—第 1 页,共 2 页
我想探索一些情感分析的概念,并尝试一些可以在数据分析和情感分析方面提供帮助的库。使用的数据集是“ *Twitter 美国航空公司情绪”*在 ka ggle:https://www . ka ggle . com/crowd flower/Twitter-Airline-sensation上很容易找到
这个数据集非常好,包含了 2015 年 2 月美国航空公司的推文,分为正面、负面和中性推文。负面推文也根据负面原因进行分类。
本帖分为两部分:
- 第一部分:对数据集进行数据分析,找出最好和最差的航空公司,并了解在糟糕的航班情况下最常见的问题是什么
- 第二部分:训练两个朴素贝叶斯分类器:首先将推文分类为正面和负面,第二个分类器根据原因将负面推文分类。这样就有可能用新的推文来更新第一点的统计数据。
这项工作有助于航空公司了解需要解决的问题。
对于这项工作的 Python 与:熊猫,jupyter 笔记本和 NLTK 将被使用。
Pandas 是一个开源库,为 Python 提供了高性能、易于使用的数据结构和分析工具。我将使用这个库来加载数据集并进行一些分析。
Jupyter notebook 对数据科学家非常有用,因为它是一个 web 应用程序,允许创建和共享包含实时代码、等式、可视化和说明性文本的文档。这样,共享工作就很容易了。
NLTK 是一个平台,用于构建使用人类语言的程序。用这个库来训练计算语言学的分类器是很简单的。
数据分析
好了,先从数据分析开始。观察数据集,我们可以发现许多列,但最重要的是:
- 航空公司
- 航空公司 _ 情绪
- 消极原因
这个数据集不需要任何清理操作,但是对于我要回答的问题,一些转换是必要的。首先,有必要将推文分为三组:正面、负面和中性。然后我想找到 2015 年 2 月最好和最差的航空公司。之后,我想找出导致糟糕飞行的常见问题。为此,只考虑负面推文(显然!).
最差航空公司的结果如下:
最好的结果是:
从数据集中,不良飞行问题的等级是:
结论
所以糟糕的飞行的最重要的原因是由于客户服务…对公司来说不是很好…
在下一篇文章中,我将训练贝叶斯分类器来分析新的推文。
原载于 2017 年 9 月 28 日【devklaus.wordpress.com】。
美国 Twitter 航空数据集上的情感分析—第 2 页,共 2 页
在数据集上可以找到在最后一篇文章中所做的分析结果。但是现在,我的目标是让这些统计数据在每条微博上更新,或者每小时更新一次。因此,首先,有必要训练一个分类器,能够将新推文分为正面和负面。
选择的分类器是朴素贝叶斯分类器。该分类器是最简单的监督机器学习方法之一,并且结果是可接受的。
用于训练分类器的步骤如下:
- 将 NLTK 与 punkt 一起使用
- 定义了一组无用词(用 nltk 停用词和字符串标点符号)来正确标记推文
- 将负面和正面的推文符号化,并定义特征
- 用训练集制作了朴素贝叶斯情感分类器
- 测试分类器并找到准确性
正如我在上一篇文章中所写的,NLTK 是一个非常强大的库,所以创建分类器很容易。转换成代码的步骤如下:
正如我们所见,分类器的准确率约为 86%,这个结果取决于用于训练的少量 tweets。我只用了 2000 条推文,因为这个数据集的正面推文只有大约 2400 条。负面推文的数量更高,但我不能使用所有这些推文。有必要用相同数量的正面和负面推文训练分类器,以避免引入偏差。
这项工作的最后一步是使用负面推文来训练一个分类器,以找出负面推文的原因。定义分类器的步骤与第一个分类器相同,但现在的子集只有负面推文。
在这项工作中,我试图只对负面推文的前两个原因进行分类,因为其他原因没有足够的数据。所有其他推文被归类为其他。
对于这个分类器,达到的准确率是 69%,不太好。但是使用的推文数量只有 1000 和 400 来验证分类器。
结论
准确度为 86%的第一分类器是可接受的,因为人类的准确度约为 80%。但是第二种的精度是不能接受的,因为太低了。
如果我们考虑组合准确度(负面和正面的分类,以及负面推文的原因分类),它下降到 59%。
这些结果的主要原因是因为数据集几乎没有正面的推文,也没有针对每个负面原因的推文。不排除使用更复杂的分类器可以达到更好的结果。
原载于 2017 年 10 月 3 日【devklaus.wordpress.com】。
通过 LSTMs 进行情感分析
用不到 50 行代码构建您自己的模型
自然语言处理简介
语言将人类联系在一起。语言是我们向另一个人传达思想和感情的工具,在这个工具的帮助下,我们也能够理解他们的思想和感情。我们大多数人从 18 个月大到 2 岁左右开始说话。人们还没有完全理解人脑是如何在如此幼小的年龄掌握如此大量的知识的。但是,已经发现大多数语言处理功能发生在大脑的大脑皮层内。
情感分析背后的动机
人类自己无法理解我们的大脑到底是如何处理语言的。那么,我们有没有可能教会机器学习我们的语言呢??是的,通过广泛的研究,已经开发了许多方法来帮助机器理解我们的语言。NLP 或自然语言处理是关注人类语言和计算机之间交互的研究领域。自然语言处理的一个子问题是情感分析,即将一个陈述分类为正面或负面。将一个陈述分为正面或负面有什么用??我们以亚马逊网站为例。在亚马逊上,它的用户可以对产品发表评论,说明它是好是坏,甚至可以是中性的。现在,使用人工阅读所有的评论并获得客户对产品的总体反馈将是昂贵且耗时的。进入我们的机器学习模型。机器学习模型可以在大量数据中搅动,做出推论并对评论进行分类。使用这种 ML 模型,亚马逊可以通过客户评论来改进产品,从而为公司带来更多收入。
情感分析并不像看起来那么简单。如果你认为包含“好”、“棒极了”等词语的评论可以被归类为正面评论,而包含“糟糕”、“悲惨”等词语的评论可以被归类为负面评论,请再想想。E.x:“完全缺乏良好的品味”和“对快餐很好,但没什么特别的”分别代表负面和中性的反馈,即使它们含有“好”这个词。因此,正如我提到的,这项任务可能不像看起来那么容易。让我们继续我们将要处理的数据。
资料组
我们将查看来自亚马逊产品评论、IMDB 电影评论和 Yelp 评论的评论,以建立我们的情感分析模型。可以从这个链接下载数据。所有数据都已经标注,0 代表负反馈,1 代表正反馈。几行亚马逊数据看起来与下图相似。
密码
我们写点代码吧!
数据存在于不同的文本文件中。我们打开每个文件,阅读所有文本行,包括每个文本的标签。然后,我们将它们存储在一个名为“lines”的列表中。
在数据集中的每一行,我们有文本和后面的四个字符空间,我们有该文本的标签(0 或 1)。因此,我们取包含文本的第一部分,并将其添加到我们的特征(x)中,然后我们取标签,其末尾有’ \n '。因此,它被删除,然后添加到我们的标签列表(y)。
Keras 有一个内置的 API,使得为计算准备文本变得更加容易。Tokenizer 类有 4 个属性,可用于准备要素。看一下下面的例子来理解 tokenizer 实际上做了什么。
## CODE
tokenizer = Tokenizer()texts = ["The sun is shining in June!","September is grey.","Life is beautiful in August.","I like it","This and other things?"]tokenizer.fit_on_texts(texts)print(tokenizer.word_index)tokenizer.texts_to_sequences(["June is beautiful and I like it!"])## OUPUT{'sun': 3, 'september': 4, 'june': 5, 'other': 6, 'the': 7, 'and': 8, 'like': 9, 'in': 2, 'beautiful': 11, 'grey': 12, 'life': 17, 'it': 16, 'i': 14, 'is': 1, 'august': 15, 'things': 10, 'shining': 13, 'this': 18}[[5, 1, 11, 8, 14, 9, 16]]
Tokenizer 为句子中的每个单词分配索引值,并且可以使用该索引值来表示新的句子。由于我们使用的文本语料库包含大量不同的单词,我们设置了一个上限,只使用最经常出现的 2500 个单词。
我们现在将文本转换成如上所示的数字序列,并填充数字序列。由于句子可以有不同的长度,它们的序列长度也会不同。因此,pad_sequences 查找最长的句子,并用 0 填充所有其他语句以匹配该长度。
## Pad Sequences Example
pad_sequences([[1, 2, 3], [3, 4, 5, 6], [7, 8]])
array([[0, 1, 2, 3],
[3, 4, 5, 6],
[0, 0, 7, 8]], dtype=int32)
我们将标签转换成一个热编码。这有助于 LSTM 网络预测文本的标签。现在,我们已经准备好了文本数据,我们可以将它分成训练样本和测试样本。80%的数据用于训练,20%的数据用于测试模型。
我们现在建立模型,编译它,训练它,测试它。该模型具有嵌入层。输入序列是文本的稀疏表示,因为词汇表将是巨大的,并且给定的单词将由大的向量来表示。如果我们能够建立一些序列的密集表示,网络就更容易预测。2500 个单词的单词嵌入/密集表示是通过嵌入层训练模型而得到的。然后,我们添加 LSTM 和密集层的模型。LSTM 细胞负责进行上下文推理,并帮助预测一个句子是否是肯定句。密集层输出每个类别的概率。我没有给出太多关于 LSTMs 的细节。想了解更多,请参考这个博客。
输出
Training
Testing
您刚刚用 50 行代码构建了一个情感分类器。
使用 RNNs 的情感分析(LSTM)
这里,我们使用评论的例子来预测情绪(尽管它可以更普遍地应用于其他领域,例如对推文、评论、客户反馈等的情绪分析)。这里的整体思想是,电影评论是由单词序列和单词顺序组成的,这些单词编码了许多对预测情感有用的信息。第一步是将单词映射到单词嵌入(参见帖子 1 和 2 了解更多关于单词嵌入的上下文)。步骤 2 是 RNN,它接收向量序列作为输入,并考虑向量的顺序来生成预测。
该网络的架构如下所示。
这里,我们将把单词传递给一个嵌入层。您实际上可以用 word2vec 训练一个嵌入,并在这里使用它。但是只要有一个嵌入层并让网络自己学习嵌入表就足够了。
从嵌入层,新的表现将被传递到 LSTM 细胞。这将增加网络的循环连接,这样我们就可以在数据中包含单词序列的信息。最后,LSTM 细胞将进入一个 sigmoid 输出层。我们使用 sigmoid 是因为我们试图预测这篇文章是否有积极或消极的情绪。输出层将只是一个具有 s 形激活函数的单一单元。
除了最后一个,我们不关心 sigmoid 输出,其他的可以忽略。我们将根据上一步的输出和训练标签来计算成本。
架构摘要:
将固定长度的评论编码为整数,然后转换为嵌入向量,以循环方式传递给 LSTM 层,并挑选最后的预测作为输出情感。
陷阱:
在我的实验中,我无法解释的一件事是,当我将单词编码为整数时,如果我随机为单词分配唯一的整数,我得到的最佳准确度是 50–55%(基本上,该模型并不比随机猜测好多少)。但是,如果对单词进行编码,使得最高频率的单词得到最低的数字,那么在 3-5 个时期内,模型精度为 80%。我的猜测是,这对于训练嵌入层是必要的,但是在任何地方都找不到原因的解释。
代码:
https://github . com/mchablani/deep-learning/blob/master/情操-rnn/情操 _RNN.ipynb
数据预处理:
将评论中的所有单词用整数编码。现在每个评论都是一个有序的整数数组。让每个评论固定大小(比如 200),这样较短的评论前面会填充 0,而较长的评论会被截断到 200。因为我们用 0 填充,所以单词到 int 的映射从 1 开始。标签被编码为“1”和“0”,分别代表“正”和“负”。
构建图表
lstm_size = 256
lstm_layers = 2
batch_size = 500
learning_rate = 0.001
embed_size = 300n_words = len(vocab_to_int) + 1 # Add 1 for 0 added to vocab# Create the graph object
tf.reset_default_graph()
with tf.name_scope('inputs'):
inputs_ = tf.placeholder(tf.int32, [None, None], name="inputs")
labels_ = tf.placeholder(tf.int32, [None, None], name="labels")
keep_prob = tf.placeholder(tf.float32, name="keep_prob")# Sizeof embedding vectors (number of units in the embedding layer)
with tf.name_scope("Embeddings"):
embedding = tf.Variable(tf.random_uniform((n_words, embed_size), -1, 1))
embed = tf.nn.embedding_lookup(embedding, inputs_)def lstm_cell():
# Your basic LSTM cell
lstm = tf.contrib.rnn.BasicLSTMCell(lstm_size, reuse=tf.get_variable_scope().reuse)
# Add dropout to the cell
return tf.contrib.rnn.DropoutWrapper(lstm, output_keep_prob=keep_prob)with tf.name_scope("RNN_layers"):
# Stack up multiple LSTM layers, for deep learning
cell = tf.contrib.rnn.MultiRNNCell([lstm_cell() for _ in range(lstm_layers)])
# Getting an initial state of all zeros
initial_state = cell.zero_state(batch_size, tf.float32)with tf.name_scope("RNN_forward"):
outputs, final_state = tf.nn.dynamic_rnn(cell, embed, initial_state=initial_state)with tf.name_scope('predictions'):
predictions = tf.contrib.layers.fully_connected(outputs[:, -1], 1, activation_fn=tf.sigmoid)
tf.summary.histogram('predictions', predictions)
with tf.name_scope('cost'):
cost = tf.losses.mean_squared_error(labels_, predictions)
tf.summary.scalar('cost', cost)with tf.name_scope('train'):
optimizer = tf.train.AdamOptimizer(learning_rate).minimize(cost)
配料和培训
def get_batches(x, y, batch_size=100):
n_batches = len(x)//batch_size
x, y = x[:n_batches*batch_size], y[:n_batches*batch_size]
for ii in range(0, len(x), batch_size):
yield x[ii:ii+batch_size], y[ii:ii+batch_size]epochs = 10# with graph.as_default():
saver = tf.train.Saver()with tf.Session() as sess:
sess.run(tf.global_variables_initializer())
train_writer = tf.summary.FileWriter('./logs/tb/train', sess.graph)
test_writer = tf.summary.FileWriter('./logs/tb/test', sess.graph)
iteration = 1
for e in range(epochs):
state = sess.run(initial_state)
for ii, (x, y) in enumerate(get_batches(train_x, train_y, batch_size), 1):
feed = {inputs_: x,
labels_: y[:, None],
keep_prob: 0.5,
initial_state: state}
summary, loss, state, _ = sess.run([merged, cost, final_state, optimizer], feed_dict=feed)
# loss, state, _ = sess.run([cost, final_state, optimizer], feed_dict=feed)train_writer.add_summary(summary, iteration)
if iteration%5==0:
print("Epoch: {}/{}".format(e, epochs),
"Iteration: {}".format(iteration),
"Train loss: {:.3f}".format(loss))if iteration%25==0:
val_acc = []
val_state = sess.run(cell.zero_state(batch_size, tf.float32))
for x, y in get_batches(val_x, val_y, batch_size):
feed = {inputs_: x,
labels_: y[:, None],
keep_prob: 1,
initial_state: val_state}
# batch_acc, val_state = sess.run([accuracy, final_state], feed_dict=feed)
summary, batch_acc, val_state = sess.run([merged, accuracy, final_state], feed_dict=feed)
val_acc.append(batch_acc)
print("Val acc: {:.3f}".format(np.mean(val_acc)))
iteration +=1
test_writer.add_summary(summary, iteration)
saver.save(sess, "checkpoints/sentiment_manish.ckpt")
saver.save(sess, "checkpoints/sentiment_manish.ckpt")
学分:来自课堂讲稿:https://classroom.udacity.com/nanodegrees/nd101/syllabus
围棋中基于简单朴素贝叶斯分类器的情感分析
使用 Go 通过情感分析对文本进行分类
(credits: Chang Sau Sheong)
我最近在读 Pedro Domingos 的大师算法。这是一本引人入胜的读物,有一些有趣的想法。在书中,多明戈斯提出,机器学习算法可以归入 5 个部落之一——符号主义者、连接主义者、进化论者、贝叶斯主义者和类比主义者。每个部落都有自己的算法。符号主义者是逆向演绎(决策树),连接主义者是反向传播(神经网络),进化者是遗传编程(遗传算法),贝叶斯主义者是贝叶斯定理,类比主义者是支持向量机(SVM)。
当我在读关于贝叶斯人的那一章时,我记得大约 10 年前我在我的旧博客中写过一篇关于朴素贝叶斯分类器的博文。所以我把它挖了出来(我的旧博客现在处于休眠状态,已经很多年没碰过了),掸去灰尘,决定刷新和重温这个话题。
我想做一些事情。首先,我将在 Go 中重写它。Ruby 代码工作得非常好,但是重温旧代码并看看如何改进它总是感觉很好。第二,我将把它用于一个非常流行的目的——情感分析(稍后我会谈到这一点)。最后,我将使用适当的训练和测试数据集对其进行训练和测试(我之前的博客文章中没有这一项)。
贝叶斯定理
在我们开始学习贝叶斯定理之前,让我们回到基础知识,谈谈概率。概率是某件事情发生的可能性,在数学中我们将其表示为 0 到 1 之间的一个数字,其中 0 表示它永远不会发生,1 表示它永远会发生。
条件概率是一种特殊的概率,它受某些条件或某些背景信息的影响。例如,你可能会也可能不会和你的朋友出去(一种可能性),但这受天气影响——如果雨下得很大,你可能不想出去。所以你出去的概率是一个条件概率,基于天气。
Going out in the rain with your friends (credits: https://commons.wikimedia.org/wiki/File:Singin%27_in_the_Rain_trailer.jpg)
为了用数学来表示,我们假设不管发生什么,你出门的概率是A
,坏天气的概率是B
,你出门的条件概率,取决于天气是p(A|B)
,或者大声读出是“给定 B,A 的概率”。
一个联合概率是两个事件都实现的概率。在我们上面的例子中,你在恶劣天气下外出的概率是p(A and B)
。你可能已经学过(或者从中学数学中有一些模糊的回忆)如果两个概率彼此独立,那么p(A and B) = p(A)p(B)
,即A
的概率和B
是A
的独立概率和B
的独立概率的倍数。
然而我们刚刚了解到A
实际上并不独立于B
,所以p(A)
实际上是p(A|B)
的一个特例。如果下雨,会减少你外出的可能性。换句话说,更一般的数学描述是:
p(A and B) = p(A|B)p(B)
由于A
和B
可以是一般化的任何事件,因此A
和B
的合取概率是可交换的:
p(A and B) = p(B and A)
如果我们代入方程:
p(A|B)p(B) = p(B|A)p(A)
然后得到p(A|B)
的条件概率:
Bayes’ Theorem
这就是所谓的贝叶斯定理(或贝叶斯定律或贝叶斯法则)。
情感分析
现在让我们看看我们想要解决的问题,然后再回头看看贝叶斯定理是如何被用来解决它的。情感分析是一种根据说话者或作者所说或写下的内容来判断其心理状态的技术。它通常用于挖掘社交媒体(推文、评论、评论等)对品牌、产品或服务的情感,因为手动挖掘太困难、太昂贵或太慢。情感分析也被用于政治,在竞选活动中衡量公众对某些话题的看法。
这是一个相当复杂的问题,有许多不同类型的算法可以使用,其中一些可能非常复杂。在我们的例子中,我们希望分析来自亚马逊、IMDB 和 Yelp 的不同文本评论,并了解情绪是积极的还是消极的。换句话说,这是一个分类问题,我们将基于贝叶斯定理构建一个分类器。
基于贝叶斯定理的文档分类
分类器就是对其他事物进行分类的东西。分类器是一个函数,它接收一组数据,并告诉我们数据属于哪个类别或分类。要对一个文本文档进行分类,我们要问——给定一个特定的文档,它属于这个类别的概率是多少?当我们找到给定文档在所有类别中的概率时,分类器挑选概率最高的类别,并宣布它为获胜者,也就是说,该文档最有可能属于该类别。
让我们将前面的数学公式转换成一个可用于文档分类的公式:
Bayes Theorem for document classification
在上面的公式中,p(category|document)
就是我们要找的东西——给定一个文档,它属于这个类别的概率是多少?
类似地,p(document|category)
是该文档存在于该类别中的概率,p(category)
是该类别与任何文档无关的概率,p(document)
是该文档与任何类别无关的概率。
我们真正需要的只是p(document|category)
和p(category)
。我们可以去掉p(document)
,因为它对每个类别都是一样的。
那么我们如何找到p(document|category)
?文档是由一串单词组成的,因此文档的概率是文档中所有单词的联合概率。给定一个类别的文档的概率是该文档中所有单词在一个类别中的联合概率。
一个词出现在一个类别中的概率很简单,那只是这个词在类别中出现的次数。连接部分非常棘手,因为单词不会随机出现在文档中,单词的顺序和外观取决于文档中的其他单词。那么我们如何解决这个问题呢?这就是朴素贝叶斯分类器的朴素部分的用武之地。我们简单地忽略单词的条件概率,并假设每个单词都是相互独立的。换句话说(双关语),我们假设单词随机出现在文档中。做出这样的假设似乎非常愚蠢,但是让我们看看它是如何实现的。
概率p(category)
相对容易,它只是一个类别中文档的数量除以所有类别中文档的总数。
这很简单。让我们来看看代码。
围棋中的朴素贝叶斯分类器
创建分类器
我们将开始在一个名为classifier.go
的文件中创建一个通用的朴素贝叶斯文本分类器。
首先,我们创建了Classifier
结构。
// Classifier is what we use to classify documents
type Classifier struct {
words map[string]map[string]int
totalWords int
categoriesDocuments map[string]int
totalDocuments int
categoriesWords map[string]int
threshold float64
}
// create and initialize the classifier
func createClassifier(categories []string, threshold float64) (c Classifier) {
c = Classifier{
words: make(map[string]map[string]int),
totalWords: 0,
categoriesDocuments: make(map[string]int),
totalDocuments: 0,
categoriesWords: make(map[string]int),
threshold: threshold,
}
for _, category := range categories {
c.words[category] = make(map[string]int)
c.categoriesDocuments[category] = 0
c.categoriesWords[category] = 0
}
return
}
在该结构中,words
是表示已经由分类器训练的单词的映射图。大概是这样的(不完全是):
{
"1": {
"good": 10,
"wonderful": 5,
"amazing": 7,
},
"0": {
"awful": 6,
"loud": 4,
}
}
totalWords
字段是分类器中单词的总数,而totalDocuments
是分类器中文档的总数。
categoriesDocuments
字段是给出每个类别中文档数量的图:
{
"1": 13,
"0": 16,
}
categoriesWords
字段是给出每个类别中单词数量的图:
{
"1": 35,
"0": 44,
}
稍后我会描述threshold
。
计数单词
分类器的核心实际上是对单词进行计数,所以接下来让我们来看看。我们有一个函数countWords
可以做到这一点,它传入一个文档,并返回每个单词出现次数的地图。
var cleaner = regexp.MustCompile(`[^\w\s]`)
// truncated list
var stopWords = map[string]bool{"a": true, "able": true, "about": true, ..., "you've": true, "z": true, "zero": true}
// clean up and split words in document, then stem each word and count the occurrence
func countWords(document string) (wordCount map[string]int) {
cleaned := cleaner.ReplaceAllString(document, "")
words := strings.Split(cleaned, " ")
wordCount = make(map[string]int)
for _, word := range words {
if !stopWords[word] {
key := stem(strings.ToLower(word))
wordCount[key]++
}
}
return
}
// stem a word using the Snowball algorithm
func stem(word string) string {
stemmed, err := snowball.Stem(word, "english", true)
if err == nil {
return stemmed
}
fmt.Println("Cannot stem word:", word)
return word
}
首先,我们使用正则表达式清理文档,删除所有不是单词的内容(包括标点符号等)。然后我们将文档拆分成单词。
我们不需要文档中的所有单词,我们只需要关键词,所以我们删除了文档中任何常见的单词,例如,我们将忽略冠词,如 a 、 the ,代词,如 he 、 she 等等。所以我们用一个停用词列表,过滤掉那些常用词。其余的将被放入小写,使关键一致。
很多词都有不同的变体,比如名词可以是复数(猫和猫要一起算),动词可以有时态(吃,吃和吃要一起算)等等。为了不重复计算单词变化,我们使用一种叫做词干的技术。在我们的分类器中,我使用了一个词干分析器库,基于雪球算法的雪球。
最后,将单词计数相加并返回。
训练分类器
训练分类器实际上就是对训练数据集文档中的单词进行计数,繁重的工作由countWords
函数完成。分类器中的Train
方法简单地使用了countWords
函数,并根据类别分配计数。
// Train the classifier
func (c *Classifier) Train(category string, document string) {
for word, count := range countWords(document) {
c.words[category][word] += count
c.categoriesWords[category] += count
c.totalWords += count
}
c.categoriesDocuments[category]++
c.totalDocuments++
}
文档分类
这是实际行动开始的地方。在进入Classify
方法之前,让我们再看一下等式:
p(category|document) = p(document|category)p(category)
我们将创建一个方法来计算每个概率。先说p(category)
。
// p (category)
func (c *Classifier) pCategory(category string) float64 {
return float64(c.categoriesDocuments[category]) / float64(c.totalDocuments)
}
这是不言自明的——我们将类别中的文档数除以文档总数,得到该类别的概率。
接下来我们来看p(document|category)
。
// p (document | category)
func (c *Classifier) pDocumentCategory(category string, document string) (p float64) {
p = 1.0
for word := range countWords(document) {
p = p * c.pWordCategory(category, word)
}
return p
}
func (c *Classifier) pWordCategory(category string, word string) float64 {
return float64(c.words[category][stem(word)]+1) / float64(c.categoriesWords[category])
}
首先,我们使用countWords
给出文档中的字数。这里我们实际上并不关心字数,我们只是想要文档中的关键词列表。对于每一个关键词,我们在类别中找到它的概率。这就是该关键字在类别中出现的次数除以类别中的总字数。例如,在训练分类器之后,假设对于类别1
(它是肯定的),我们有 10 个单词“good”。而我们在类别1
中总共有 100 个单词。这意味着该词在类别中的概率是0.1
。
我们对文档中的每个关键词都这样做,然后将所有这些概率相乘,这将是p(document|category)
。
最后,我们找到了p(category|document)
,这是相当琐碎的。
// p (category | document)
func (c *Classifier) pCategoryDocument(category string, document string) float64 {
return c.pDocumentCategory(category, document) * c.pCategory(category)
}
现在我们有了每个类别的条件概率,我们把它们放在一个单一的地图中。
// Probabilities of each category
func (c *Classifier) Probabilities(document string) (p map[string]float64) {
p = make(map[string]float64)
for category := range c.words {
p[category] = c.pCategoryDocument(category, document)
}
return
}
这将由我们的Classify
方法使用。
// Classify a document
func (c *Classifier) Classify(document string) (category string) {
// get all the probabilities of each category
prob := c.Probabilities(document)
// sort the categories according to probabilities
var sp []sorted
for c, p := range prob {
sp = append(sp, sorted{c, p})
}
sort.Slice(sp, func(i, j int) bool {
return sp[i].probability > sp[j].probability
})
// if the highest probability is above threshold select that
if sp[0].probability/sp[1].probability > c.threshold {
category = sp[0].category
} else {
category = "unknown"
}
return
}
我们的Classify
方法根据概率对类别进行排序,并找出最重要的类别。但这还不是结束。顶级和二级之间的差别可能非常小。例如,让我们以将电子邮件分类为垃圾邮件和非垃圾邮件为例,假设概率是这样的-垃圾邮件是 51%,非垃圾邮件是 49%。该文档是否应归类为垃圾邮件?这取决于你希望分类器有多严格。
这就是threshold
字段的原因,它是用于分离类别的阈值比率。例如,如果threshold
是1.5
,这意味着具有最高概率的类别需要比第二高的概率高 1.5 倍。如果顶级类别没有达到阈值,我们将把它归类为unknown
。
我们已经完成了分类器,接下来让我们看看如何使用它。
使用朴素贝叶斯分类器的情感分析
在这篇博文中,我使用了 Dimitrios Kotzias 等人为论文“使用深度特征从群体到个体标签”创建的情感标签句子数据集。艾尔。KDD 2015。该数据集包含来自亚马逊、IMDB 和 Yelp 网站的 1000 条评论,标签为正面的1
或负面的0
。这些评论分别摘自产品、电影和餐馆的评论。
首先,让我们看看数据是如何设置的。
设置数据
我将每个数据集分为训练数据集和测试数据集,并使用训练数据集来训练分类器,使用测试数据集来验证分类器。这是通过setupData
功能完成的。
// datasets
type document struct {
sentiment string
text string
}
var train []document
var test []document
// set up data for training and testing
func setupData(file string) {
rand.Seed(time.Now().UTC().UnixNano())
data, err := readLines(file)
if err != nil {
fmt.Println("Cannot read file", err)
os.Exit(1)
}
for _, line := range data {
s := strings.Split(line, "|")
doc, sentiment := s[0], s[1]
if rand.Float64() > testPercentage {
train = append(train, document{sentiment, doc})
} else {
test = append(test, document{sentiment, doc})
}
}
}
// read the file line by line
func readLines(path string) ([]string, error) {
file, err := os.Open(path)
if err != nil {
return nil, err
}
defer file.Close()
var lines []string
scanner := bufio.NewScanner(file)
for scanner.Scan() {
lines = append(lines, scanner.Text())
}
return lines, scanner.Err()
}
我使用变量testPercentage
来获取整个数据集作为测试数据集的百分比。这又被用来随机选择测试数据集中的一些记录。
创建文档分类器
一旦我们建立了数据集,我们开始用我们的参数创建分类器。
// parameters
var (
testPercentage = 0.1
datafile = "amazon.txt"
threshold = 1.1
)
var categories = []string{"1", "0"}
func main() {
setupData(datafile)
fmt.Println("Data file used:", datafile)
fmt.Println("no of docs in TRAIN dataset:", len(train))
fmt.Println("no of docs in TEST dataset:", len(test))
c := createClassifier(categories, threshold)
...
用训练数据集训练分类器
给定分类器,我们开始使用训练数据集来训练它。
...
// train on train dataset
for _, doc := range train {
c.Train(doc.sentiment, doc.text)
}
...
测试数据集上的测试分类器
在训练分类器之后,我们使用它在测试数据集上进行测试。
...
// validate on test dataset
count, accurates, unknowns := 0, 0, 0
for _, doc := range test {
count++
sentiment := c.Classify(doc.text)
if sentiment == doc.sentiment {
accurates++
}
if sentiment == "unknown" {
unknowns++
}
}
fmt.Printf("Accuracy on TEST dataset is %2.1f%% with %2.1f%% unknowns", float64(accurates)*100/float64(count), float64(unknowns)*100/float64(count))
...
我们计算精确分类的数量,以及未知分类的数量。
在训练数据集上测试分类器
我们还对一些训练数据集进行了测试,以进行健全性检查。
..
// validate on the first 100 docs in the train dataset
count, accurates, unknowns = 0, 0, 0
for _, doc := range train[0:100] {
count++
sentiment := c.Classify(doc.text)
if sentiment == doc.sentiment {
accurates++
}
if sentiment == "unknown" {
unknowns++
}
}
fmt.Printf("\nAccuracy on TRAIN dataset is %2.1f%% with %2.1f%% unknowns", float64(accurates)*100/float64(count), float64(unknowns)*100/float64(count))
...
我们来看看结果。
成绩还不算太差!你可能会意识到,如果你运行多次,你会得到不同的结果,这是因为在培训中使用的顺序和文件实际上很重要。您也可以尝试调整不同的参数,以查看分类器的行为。
结论
你可能会很惊讶(如果你还不知道的话)这样一个简单的算法实际上是如何表现得相对较好的。然而,这种特殊的做法有一个警告。首先,对使用的数据集进行清洗,选择明确的阳性或阴性。在现实世界中,情感分析充满了许多其他问题,真的不是一个简单的问题。然而,这一点只是为了表明贝叶斯定理可以成为你的工具箱中一个非常强大的工具。
源代码
你可以在 https://github.com/sausheong/gonb 的 Github 中找到这段代码
参考
- 我最初的博文大量引用了托比·塞格兰的《T2 编程集体智慧》一书
- 佩德罗·多明戈斯引人入胜的大师算法一书引发了这篇文章,值得再次提及
- 艾伦·唐尼的书认为贝叶斯很好地描述了贝叶斯定律,我在这篇博客文章中描述它时从书中得到了一些启示
- 我使用了 Kotzias 等人的论文“使用深度特征从组到个体标签”中的数据集。艾尔。KDD 2015
使用 PySpark 进行情感分析
Photo by Chris J. Davis on Unsplash
Apache Spark 是我非常感兴趣但没有太多机会探索的工具之一。大多数时候,Pandas 和 Scikit-Learn 足以处理我试图建立模型的数据量。但这也意味着我还没有机会处理数 Pb 的数据,我希望为我面临真正大数据的情况做好准备。
我以前尝试过用 PySpark 进行一些基本的数据操作,但只达到非常基本的水平。我想在使用 PySpark 的过程中了解更多信息,更加得心应手。这篇文章是我为了更好地理解 PySpark 所做的努力。
Python 非常适合数据科学建模,这要归功于它众多的帮助实现数据科学目标的模块和包。但是,如果您处理的数据无法放入一台机器中,该怎么办呢?也许您可以在单台机器上实现仔细的采样来进行分析,但是使用像 PySpark 这样的分布式计算框架,您可以高效地实现大型数据集的任务。
Spark API 支持多种编程语言(Scala、Java、Python 和 R)。关于 Spark 的性能如何随运行它的语言而变化,存在争议,但是因为我一直使用的主要语言是 Python,所以我将重点讨论 PySpark,而不会过多地讨论我应该为 Apache Spark 选择什么语言。
Image courtesy of DataFlair
Spark 通过其 API 提供了三种不同的数据结构:RDD、Dataframe(这不同于 Pandas data frame)、Dataset。对于这篇文章,我将使用 Dataframe 和相应的机器学习库 SparkML。我首先决定了我想要使用的数据结构,这是基于来自 Analytics Vidhya 的帖子的建议。Dataframe 比 RDD 快得多,因为它有关联的元数据(一些关于数据的信息),这允许 Spark 优化查询计划可以从原帖中找到全面的介绍。
在 Databricks 上也有一个信息丰富的帖子,比较了 Apache Spark 的不同数据结构:“三个 Apache Spark APIs 的故事:rdd、DataFrames 和 Datasets ”。
然后我发现,如果我想处理 Dataframe,我需要使用 SparkML 而不是 SparkMLLib。SparkMLLib 与 RDD 一起使用,而 SparkML 支持 Dataframe。
还有一点需要注意的是,我将使用笔记本电脑在本地模式下工作。本地模式通常用于原型、开发、调试和测试。然而,由于 Spark 的本地模式与集群模式完全兼容,本地编写的代码只需几个额外的步骤就可以在集群上运行。
为了在 Jupyter 笔记本中使用 PySpark,您应该配置 PySpark 驱动程序,或者使用一个名为 Findspark 的包,使 Spark 上下文在您的 Jupyter 笔记本中可用。您可以通过命令行上的“pip install findspark”轻松安装 Findspark。让我们首先加载一些我们需要的基本依赖项。
除了我将附上的简短代码块,你可以在这篇文章的末尾找到整个 Jupyter 笔记本的链接。
import findspark
findspark.init()
import pyspark as ps
import warnings
from pyspark.sql import SQLContext
任何 Apache 编程的第一步都是创建 SparkContext。当我们想要在集群中执行操作时,需要 SparkContext。SparkContext 告诉 Spark 如何以及在哪里访问集群。这是连接 Apache 集群的第一步。
try:
# create SparkContext on all CPUs available: in my case I have 4 CPUs on my laptop
sc = ps.SparkContext('local[4]')
sqlContext = SQLContext(sc)
print("Just created a SparkContext")
except ValueError:
warnings.warn("SparkContext already exists in this scope")
我将在这篇文章中使用的数据集是来自“感知 140 ”的带注释的推文。它源于斯坦福大学的一个研究项目,我在之前的 Twitter 情感分析系列中使用了这个数据集。由于我已经在我之前的项目过程中清理了推文,我将使用预先清理的推文。如果你想更详细地了解我采取的清理过程,你可以查看我以前的帖子:“用 Python 进行的另一次 Twitter 情绪分析——第二部分”。
df = sqlContext.read.format('com.databricks.spark.csv').options(header='true', inferschema='true').load('project-capstone/Twitter_sentiment_analysis/clean_tweet.csv')
type(df)
df.show(5)
df = df.dropna()
df.count()
在成功地将数据装载为 Spark Dataframe 之后,我们可以通过调用。show(),相当于熊猫。头部()。去掉 NA 之后,我们的 Tweets 只有不到 160 万条。我将把它分成三部分:培训、验证、测试。因为我有大约 160 万个条目,验证和测试集各有 1%就足够测试模型了。
(train_set, val_set, test_set) = df.randomSplit([0.98, 0.01, 0.01], seed = 2000)
HashingTF + IDF +逻辑回归
通过我之前使用 Pandas 和 Scikit-Learn 进行情感分析的尝试,我了解到 TF-IDF 与 Logistic 回归是一个相当强的组合,并且表现出稳健的性能,与 Word2Vec +卷积神经网络模型一样高。所以在本帖中,我将尝试用 PySpark 实现 TF-IDF + Logistic 回归模型。
顺便说一下,如果你想知道更多关于 TF-IDF 是如何计算的细节,请查看我以前的帖子:“用 Python 进行的另一个 Twitter 情感分析—第五部分(Tfidf 矢量器,模型比较,词法方法)”
from pyspark.ml.classification import LogisticRegression
lr = LogisticRegression(maxIter=100)
lrModel = lr.fit(train_df)
predictions = lrModel.transform(val_df)from pyspark.ml.evaluation import BinaryClassificationEvaluator
evaluator = BinaryClassificationEvaluator(rawPredictionCol="rawPrediction")
evaluator.evaluate(predictions)
0.86!那看起来不错,也许太好了。因为我已经在 Pandas 和 SKLearn 中使用相同的数据尝试了相同的技术组合,所以我知道使用逻辑回归的 unigram TF-IDF 的结果大约有 80%的准确性。由于详细的模型参数,可能会有一些细微的差异,但这看起来太好了。
通过查看 Spark 文档,我意识到 BinaryClassificationEvaluator 所评估的默认情况下是 areaUnderROC。
对于二进制分类,Spark 不支持将准确性作为度量标准。但是我仍然可以通过计算匹配标签的预测数并除以总条目数来计算准确性。
accuracy = predictions.filter(predictions.label == predictions.prediction).count() / float(val_set.count())
accuracy
现在看起来似乎更合理,实际上,精确度比我从 SKLearn 的结果中看到的略低。
计数矢量器+ IDF +逻辑回归
还有另一种方法可以获得 IDF(逆文档频率)计算的词频。它是 SparkML 中的 CountVectorizer。除了特性(词汇表)的可逆性之外,它们在过滤顶级特性的方式上也有很大的不同。在 HashingTF 的情况下,这是由于可能的冲突而导致的维数减少。CountVectorizer 会丢弃不常用的标记。
让我们看看如果使用 CountVectorizer 而不是 HashingTF,性能是否会发生变化。
看起来使用 CountVectorizer 稍微提高了一点性能。
n 元语法实现
在 Scikit-Learn 中,n-gram 的实现相当容易。当调用 TfIdf 矢量器时,可以定义 n 元语法的范围。但是对于 Spark 来说,就有点复杂了。它不会自动组合来自不同 n-gram 的特征,所以我不得不在管道中使用 VectorAssembler 来组合我从每个 n-gram 获得的特征。
我首先试图从一元、二元、三元模型中提取大约 16,000 个特征。这意味着我总共将获得大约 48,000 个特征。然后,我实现了卡方特征选择,将特征总数减少到 16,000 个。
现在我准备运行上面定义的函数。
%%time
trigram_pipelineFit = build_trigrams().fit(train_set)
predictions = trigram_pipelineFit.transform(val_set)
accuracy = predictions.filter(predictions.label == predictions.prediction).count() / float(dev_set.count())
roc_auc = evaluator.evaluate(predictions)# print accuracy, roc_auc
print "Accuracy Score: {0:.4f}".format(accuracy)
print "ROC-AUC: {0:.4f}".format(roc_auc)
精确度有所提高,但是您可能已经注意到了,拟合模型花了 4 个小时!这主要是因为 ChiSqSelector。
如果我首先从 unigram、bigram、trigram 中分别提取 5460 个特征,最终总共有大约 16000 个特征,而没有卡方特征选择,会怎么样?
%%timetrigramwocs_pipelineFit = build_ngrams_wocs().fit(train_set)
predictions_wocs = trigramwocs_pipelineFit.transform(val_set)
accuracy_wocs = predictions_wocs.filter(predictions_wocs.label == predictions_wocs.prediction).count() / float(val_set.count())
roc_auc_wocs = evaluator.evaluate(predictions_wocs)# print accuracy, roc_auc
print "Accuracy Score: {0:.4f}".format(accuracy_wocs)
print "ROC-AUC: {0:.4f}".format(roc_auc_wocs)
这给了我几乎相同的结果,略低,但差异在第四位。考虑到它只需要 6 分钟,我肯定会选择没有 ChiSqSelector 的模型。
最后,让我们在最终的测试集上尝试这个模型。
test_predictions = trigramwocs_pipelineFit.transform(test_set)
test_accuracy = test_predictions.filter(test_predictions.label == test_predictions.prediction).count() / float(test_set.count())
test_roc_auc = evaluator.evaluate(test_predictions)# print accuracy, roc_auc
print "Accuracy Score: {0:.4f}".format(test_accuracy)
print "ROC-AUC: {0:.4f}".format(test_roc_auc)
最终测试集的准确率为 81.22%,ROC-AUC 为 0.8862。
通过这篇文章,我用 PySpark 实现了一个简单的情感分析模型。尽管这可能不是 PySpark 的高级应用,但我相信让自己不断接触新环境和新挑战是很重要的。探索 PySpark 的一些基本功能确实引发了我的兴趣(没有双关的意思)。
我明天(2018 年 13 月 3 日)将参加 Spark 伦敦会议,主题是“Apache Spark:深度学习管道、PySpark MLLib 和流中的模型”。我迫不及待地想深入探索 PySpark 世界!!
感谢您的阅读,您可以通过以下链接找到 Jupyter 笔记本:
用 Python 进行情感分析(第 1 部分)
IMDb 电影评论分类
Photo by Denise Jans on Unsplash
情感分析是数据科学家需要执行的一项常见 NLP 任务。这是一个用 Python 创建准系统电影评论分类器的简单指南。本系列的后续部分将重点改进分类器。
本系列中使用的所有代码以及补充材料都可以在这个 GitHub 资源库中找到。
数据概述
在这项分析中,我们将使用来自 IMDb 的 50,000 条电影评论的数据集。这些数据由安德鲁·马斯汇编,可以在这里找到: IMDb 评论。
数据被平均分割,25k 条评论用于训练,25k 条用于测试你的分类器。而且每套都有 12.5k 的正面和 12.5k 的负面评论。
IMDb 让用户从 1 到 10 给电影打分。为了给这些评论贴上标签,数据管理员将≤ 4 星的评论标为负面,将≥ 7 星的评论标为正面。5 星或 6 星的评论被排除在外。
第一步:下载并合并电影评论
如果你还没有,去 IMDb 评论点击“大电影评论数据集 1.0 版”。一旦完成,在你的下载文件夹中就会有一个名为aclImdb_v1.tar.gz
的文件。
**快捷方式:**如果您想直接进行数据分析和/或对终端不太熟悉,我在这里放了一个这个步骤创建的最终目录的 tar 文件:合并的电影数据。双击这个文件应该足以解压它(至少在 Mac 上),否则终端中的gunzip -c movie_data.tar.gz | tar xopf —
就会这么做。
拆包和合并
遵循这些步骤或者在这里运行 shell 脚本:预处理脚本
- 将 tar 文件移动到您希望存储这些数据的目录中。
- 打开一个终端窗口,
cd
到你放aclImdb_v1.tar.gz
的目录。 gunzip -c aclImdb_v1.tar.gz | tar xopf -
cd aclImdb && mkdir movie_data
for split in train test; do for sentiment in pos neg; do for file in $split/$sentiment/*; do cat $file >> movie_data/full_${split}.txt; echo >> movie_data/full_${split}.txt; done; done; done;
步骤 2:读入 Python
对于本演练中我们想要做的大多数事情,我们只需要我们的评论在 Python list
中。确保将open
指向存放电影数据的目录。
步骤 3:清理和预处理
这些评论的原始文本相当混乱,所以在我们进行任何分析之前,我们需要清理一下。这里有一个例子:
"This isn't the comedic Robin Williams, nor is it the quirky/insane Robin Williams of recent thriller fame. This is a hybrid of the classic drama without over-dramatization, mixed with Robin's new love of the thriller. But this isn't a thriller, per se. This is more a mystery/suspense vehicle through which Williams attempts to locate a sick boy and his keeper.<br /><br />Also starring Sandra Oh and Rory Culkin, this Suspense Drama plays pretty much like a news report, until William's character gets close to achieving his goal.<br /><br />I must say that I was highly entertained, though this movie fails to teach, guide, inspect, or amuse. It felt more like I was watching a guy (Williams), as he was actually performing the actions, from a third person perspective. In other words, it felt real, and I was able to subscribe to the premise of the story.<br /><br />All in all, it's worth a watch, though it's definitely not Friday/Saturday night fare.<br /><br />It rates a 7.7/10 from...<br /><br />the Fiend :."
**注意:**理解并能够使用正则表达式是执行任何自然语言处理任务的先决条件。如果你不熟悉它们,也许可以从这里开始:正则表达式教程
这是同样的评论现在的样子:
"this isnt the comedic robin williams nor is it the quirky insane robin williams of recent thriller fame this is a hybrid of the classic drama without over dramatization mixed with robins new love of the thriller but this isnt a thriller per se this is more a mystery suspense vehicle through which williams attempts to locate a sick boy and his keeper also starring sandra oh and rory culkin this suspense drama plays pretty much like a news report until williams character gets close to achieving his goal i must say that i was highly entertained though this movie fails to teach guide inspect or amuse it felt more like i was watching a guy williams as he was actually performing the actions from a third person perspective in other words it felt real and i was able to subscribe to the premise of the story all in all its worth a watch though its definitely not friday saturday night fare it rates a from the fiend"
**注意:**有许多不同的、更复杂的方法来清理文本数据,它们可能会产生比我在这里所做的更好的结果。我希望本教程的第 1 部分尽可能简单。此外,我通常认为,在花费时间进行可能不必要的转换之前,最好用最简单的可能解决方案获得基线预测。
…向量化…
为了让这些数据对我们的机器学习算法有意义,我们需要将每个评论转换成数字表示,我们称之为矢量化。
最简单的形式是创建一个非常大的矩阵,用一列来表示语料库中的每个独特的单词(在我们的例子中,语料库是所有 50k 篇评论)。然后我们将每个评论转换成包含 0 和 1 的一行,其中 1 表示与该列对应的语料库中的单词出现在那个评论中。也就是说,矩阵的每一行都非常稀疏(大部分是零)。这个过程也被称为一次热编码。
步骤 4:构建分类器
既然我们已经将数据集转换成适合建模的格式,我们就可以开始构建分类器了。逻辑回归是我们使用的一个很好的基线模型,原因有几个:(1)它们易于解释,(2)线性模型往往在像这样的稀疏数据集上表现良好,以及(3)与其他算法相比,它们学习起来非常快。
为了简单起见,我只担心超参数C
,它调整正则化。
**注意:**我们用于训练和测试的目标/标签将是相同的,因为两个数据集的结构相同,其中第一个 12.5k 是正的,最后一个 12.5k 是负的。
看起来给我们最高精度的 C 值是0.05
。
列车最终模型
现在我们已经找到了 C 的最佳值,我们应该使用整个训练集训练一个模型,并评估我们在 25k 测试评论上的准确性。
作为一个理智的检查,让我们来看看正面和负面评论中最有区别的 5 个词。我们将通过分别查看最大和最小系数来做到这一点。
这就是了。一个非常简单的分类器,开箱后具有相当不错的准确性。
下次
在本系列的下一部分中,我们将研究更复杂的方法来提高分类器的性能。
- 文本处理:词干化/词条化,将每个单词的不同形式转化为一个。
- n-grams :除了单词标记(1-gram/unigram),我们还可以包含单词对。
- 表示:代替简单的二进制向量,我们可以使用字数或 TF-IDF 来转换这些计数。
- 算法:除了逻辑回归,我们还会看看支持向量机的表现。
点击这里查看:
改进电影评论情感分类器
towardsdatascience.com](/sentiment-analysis-with-python-part-2-4f71e7bde59a)
感谢阅读!这是我在 Medium 上的第一篇帖子,所以请评论你的任何问题或建议。
基于文本挖掘的情感分析
了解如何准备文本数据并运行两个不同的分类器来预测推文的情绪。
Photo by Romain Vignes on Unsplash
在本教程中,我将探索一些用于情感分析的文本挖掘技术。首先,我们将花一些时间准备文本数据。这将涉及清理文本数据,删除停用词和词干。为此,Kaggle 上的 Twitter 美国航空公司情绪数据集非常适合合作。它包含 tweet 的文本和一个具有三个可能情感值的变量。
为了推断推文的情感,我们使用两个分类器:逻辑回归和多项式朴素贝叶斯。我们将使用网格搜索来调整两个分类器的超参数。
我们将用三个指标来比较性能:精确度、召回率和 F1 分数。
我们从导入包和配置一些设置开始。
import numpy as np
import pandas as pd
pd.set_option('display.max_colwidth', -1)
from time import time
import re
import string
import os
import emoji
from pprint import pprint
import collectionsimport matplotlib.pyplot as plt
import seaborn as sns
sns.set(style="darkgrid")
sns.set(font_scale=1.3)from sklearn.base import BaseEstimator, TransformerMixin
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.model_selection import GridSearchCV
from sklearn.model_selection import train_test_split
from sklearn.pipeline import Pipeline, FeatureUnion
from sklearn.metrics import classification_reportfrom sklearn.naive_bayes import MultinomialNB
from sklearn.linear_model import LogisticRegression
from sklearn.externals import joblibimport gensimfrom nltk.corpus import stopwords
from nltk.stem import PorterStemmer
from nltk.tokenize import word_tokenizeimport warnings
warnings.filterwarnings('ignore')np.random.seed(37)
加载数据
我们读取了从 Kaggle 数据集下载的逗号分隔文件。我们打乱了数据帧,以防类被排序。对原始指数的permutation
应用reindex
方法有利于此。在这本笔记本中,我们将使用text
变量和airline_sentiment
变量。
df = pd.read_csv('../input/Tweets.csv')
df = df.reindex(np.random.permutation(df.index))
df = df[['text', 'airline_sentiment']]
探索性数据分析
目标变量
我们将预测三个类别标签:负面、中性或正面。
正如我们在下面的图表中看到的,分类标签是不平衡的。这是我们在模型训练阶段应该记住的事情。有了 seaborn 包的factorplot
,我们可以可视化目标变量的分布。
sns.factorplot(x="airline_sentiment", data=df, kind="count", size=6, aspect=1.5, palette="PuBuGn_d")
plt.show();
Imbalanced distribution of the target class labels
输入变量
为了分析text
变量,我们创建了一个类TextCounts
。在这个类中,我们计算文本变量的一些基本统计数据。
count_words
:推文字数count_mentions
:其他 Twitter 账户的推荐以@开头count_hashtags
:标签字数,以#开头count_capital_words
:一些大写单词有时被用来“叫喊”和表达(负面)情绪count_excl_quest_marks
:问号或感叹号的个数count_urls
:推文中的链接数量,以 http(s)开头count_emojis
:表情符号的数量,这可能是情绪的一个好迹象
class TextCounts(BaseEstimator, TransformerMixin):
def count_regex(self, pattern, tweet):
return len(re.findall(pattern, tweet))
def fit(self, X, y=None, **fit_params):
# fit method is used when specific operations need to be done on the train data, but not on the test data
return self
def transform(self, X, **transform_params):
count_words = X.apply(lambda x: self.count_regex(r'\w+', x))
count_mentions = X.apply(lambda x: self.count_regex(r'@\w+', x))
count_hashtags = X.apply(lambda x: self.count_regex(r'#\w+', x))
count_capital_words = X.apply(lambda x: self.count_regex(r'\b[A-Z]{2,}\b', x))
count_excl_quest_marks = X.apply(lambda x: self.count_regex(r'!|\?', x))
count_urls = X.apply(lambda x: self.count_regex(r'http.?://[^\s]+[\s]?', x))
# We will replace the emoji symbols with a description, which makes using a regex for counting easier
# Moreover, it will result in having more words in the tweet
count_emojis = X.apply(lambda x: emoji.demojize(x)).apply(lambda x: self.count_regex(r':[a-z_&]+:', x))
df = pd.DataFrame({'count_words': count_words
, 'count_mentions': count_mentions
, 'count_hashtags': count_hashtags
, 'count_capital_words': count_capital_words
, 'count_excl_quest_marks': count_excl_quest_marks
, 'count_urls': count_urls
, 'count_emojis': count_emojis
})
return df
tc = TextCounts()
df_eda = tc.fit_transform(df.text)
df_eda['airline_sentiment'] = df.airline_sentiment
查看 TextStats 变量与 class 变量的关系可能会很有趣。因此我们编写了一个函数show_dist
,它为每个目标类提供了描述性的统计数据和图表。
def show_dist(df, col):
print('Descriptive stats for {}'.format(col))
print('-'*(len(col)+22))
print(df.groupby('airline_sentiment')[col].describe())
bins = np.arange(df[col].min(), df[col].max() + 1)
g = sns.FacetGrid(df, col='airline_sentiment', size=5, hue='airline_sentiment', palette="PuBuGn_d")
g = g.map(sns.distplot, col, kde=False, norm_hist=True, bins=bins)
plt.show()
下面你可以找到每个目标类别的 tweet 字数分布。为简洁起见,我们将仅限于这个变量。所有 TextCounts 变量的图表都在 Github 的笔记本里。
- 推文中使用的字数相当低。最大的字数是 36 个,甚至有只有 2 个字的推文。所以在数据清理过程中,我们必须小心,不要删除太多的单词。但是文字处理会更快。负面推文比中性或正面推文包含更多单词。
- 所有推文至少有一次提及。这是基于 Twitter 数据中的提及提取推文的结果。就情感而言,提及次数似乎没有什么不同。
- 大多数推文不包含哈希标签。所以这个变量在模型训练期间不会被保留。同样,对于情感,散列标签的数量没有差别。
- 大多数推文不包含大写单词,我们看不到情绪分布的差异。
- 积极的推文似乎使用了更多的感叹或问号。
- 大多数推文不包含网址。
- 大多数推文不使用表情符号。
文本清理
在我们开始使用 tweets 的文本之前,我们需要清理它。我们将在CleanText
**课上做这件事。**在这个课程中,我们将执行以下操作:
- 删除提及,因为我们也想推广到其他航空公司的推文。
- 删除散列标签符号(#),但不要删除实际的标签,因为这可能包含信息
- 将所有单词设为小写
- 删除所有标点符号,包括问号和感叹号
- 删除网址,因为它们不包含有用的信息。我们没有注意到情感类别之间使用的 URL 数量的差异
- 确保将表情符号转换成一个单词。
- 删除数字
- 删除停用词
- 应用
PorterStemmer
保留单词的词干
class CleanText(BaseEstimator, TransformerMixin):
def remove_mentions(self, input_text):
return re.sub(r'@\w+', '', input_text)
def remove_urls(self, input_text):
return re.sub(r'http.?://[^\s]+[\s]?', '', input_text)
def emoji_oneword(self, input_text):
# By compressing the underscore, the emoji is kept as one word
return input_text.replace('_','')
def remove_punctuation(self, input_text):
# Make translation table
punct = string.punctuation
trantab = str.maketrans(punct, len(punct)*' ') # Every punctuation symbol will be replaced by a space
return input_text.translate(trantab) def remove_digits(self, input_text):
return re.sub('\d+', '', input_text)
def to_lower(self, input_text):
return input_text.lower()
def remove_stopwords(self, input_text):
stopwords_list = stopwords.words('english')
# Some words which might indicate a certain sentiment are kept via a whitelist
whitelist = ["n't", "not", "no"]
words = input_text.split()
clean_words = [word for word in words if (word not in stopwords_list or word in whitelist) and len(word) > 1]
return " ".join(clean_words)
def stemming(self, input_text):
porter = PorterStemmer()
words = input_text.split()
stemmed_words = [porter.stem(word) for word in words]
return " ".join(stemmed_words)
def fit(self, X, y=None, **fit_params):
return self
def transform(self, X, **transform_params):
clean_X = X.apply(self.remove_mentions).apply(self.remove_urls).apply(self.emoji_oneword).apply(self.remove_punctuation).apply(self.remove_digits).apply(self.to_lower).apply(self.remove_stopwords).apply(self.stemming)
return clean_X
为了展示清理后的文本变量的外观,这里有一个示例。
ct = CleanText()
sr_clean = ct.fit_transform(df.text)
sr_clean.sample(5)
高兴 rt 打赌鸟愿飞南方冬天
点 upc 代码检查 baggag 告诉 luggag vacat day tri 泳装
vx jfk la dirti 飞机不标准
告诉意味着工作需要估计时间到达请需要笔记本电脑工作感谢
当然业务去 els 航空旅行姓名凯瑟琳索特罗
文本清理的一个副作用是一些行的文本中没有任何单词。对于CountVectorizer
和TfIdfVectorizer
来说,这不成问题。然而,对于Word2Vec
算法来说,这会导致一个错误。有不同的策略来处理这些缺失的价值观。
- 删除整行,但在生产环境中这是不可取的。
- 用类似*[no_text]*的占位符文本估算缺失值
- 当应用 Word2Vec 时:使用所有向量的平均值
这里我们将使用占位符文本进行估算。
empty_clean = sr_clean == ''
print('{} records have no words left after text cleaning'.format(sr_clean[empty_clean].count()))
sr_clean.loc[empty_clean] = '[no_text]'
现在,我们已经清理了推文的文本,我们可以看看最常用的词是什么。下面我们将展示前 20 个单词。出现频率最高的词是“逃”。
cv = CountVectorizer()
bow = cv.fit_transform(sr_clean)
word_freq = dict(zip(cv.get_feature_names(), np.asarray(bow.sum(axis=0)).ravel()))
word_counter = collections.Counter(word_freq)
word_counter_df = pd.DataFrame(word_counter.most_common(20), columns = ['word', 'freq'])fig, ax = plt.subplots(figsize=(12, 10))
sns.barplot(x="word", y="freq", data=word_counter_df, palette="PuBuGn_d", ax=ax)
plt.show();
创建测试数据
为了检查模型的性能,我们需要一套测试设备。对训练数据的评估是不正确的。您不应该在用于训练模型的相同数据上进行测试。
首先,我们将TextCounts
变量与CleanText
变量结合起来。最初,我在GridSearchCV
中错误地执行了 TextCounts 和 CleanText。这花费了太长时间,因为每次运行 GridSearch 都要应用这些函数。只运行一次就足够了。
df_model = df_eda
df_model['clean_text'] = sr_clean
df_model.columns.tolist()
所以df_model
现在包含了几个变量。但是我们的矢量器(见下文)将只需要clean_text
变量。可以添加TextCounts
变量。为了选择列,我编写了下面的类ColumnExtractor
。
class ColumnExtractor(TransformerMixin, BaseEstimator):
def __init__(self, cols):
self.cols = cols def transform(self, X, **transform_params):
return X[self.cols] def fit(self, X, y=None, **fit_params):
return selfX_train, X_test, y_train, y_test = train_test_split(df_model.drop('airline_sentiment', axis=1), df_model.airline_sentiment, test_size=0.1, random_state=37)
超参数调整和交叉验证
正如我们将在下面看到的,矢量器和分类器都有可配置的参数。为了选择最佳参数,我们需要在单独的验证集上进行测试。培训期间没有使用该验证集。然而,仅使用一个验证集可能不会产生可靠的验证结果。由于偶然的机会,您可能在验证集上有一个好的模型性能。如果您以其他方式分割数据,您可能会得到其他结果。为了得到更准确的估计,我们进行交叉验证。
通过交叉验证,我们可以多次将数据分成训练集和验证集。然后在不同的折叠上对评估度量进行平均。幸运的是,GridSearchCV 应用了现成的交叉验证。
为了找到矢量器和分类器的最佳参数,我们创建了一个Pipeline
。
评估指标
默认情况下,GridSearchCV 使用默认计分器来计算best_score_
。对于MultiNomialNb
和LogisticRegression
来说,这个默认的评分标准是准确性。
在我们的函数grid_vect
中,我们额外生成了测试数据的classification_report
。这为每个目标类提供了一些有趣的度量。这在这里可能更合适。这些指标是精确度、召回率和 F1 分数**。**
- Precision : 在我们预测为某个类的所有行中,我们正确预测了多少?
- 回想一下 : 在某个类的所有行中,我们正确预测了多少行?
- F1 得分 : 精确度和召回率的调和平均值。
利用混淆矩阵的元素,我们可以计算精确度和召回率。
# Based on [http://scikit-learn.org/stable/auto_examples/model_selection/grid_search_text_feature_extraction.html](http://scikit-learn.org/stable/auto_examples/model_selection/grid_search_text_feature_extraction.html)
def grid_vect(clf, parameters_clf, X_train, X_test, parameters_text=None, vect=None, is_w2v=False):
textcountscols = ['count_capital_words','count_emojis','count_excl_quest_marks','count_hashtags'
,'count_mentions','count_urls','count_words']
if is_w2v:
w2vcols = []
for i in range(SIZE):
w2vcols.append(i)
features = FeatureUnion([('textcounts', ColumnExtractor(cols=textcountscols))
, ('w2v', ColumnExtractor(cols=w2vcols))]
, n_jobs=-1)
else:
features = FeatureUnion([('textcounts', ColumnExtractor(cols=textcountscols))
, ('pipe', Pipeline([('cleantext', ColumnExtractor(cols='clean_text')), ('vect', vect)]))]
, n_jobs=-1) pipeline = Pipeline([
('features', features)
, ('clf', clf)
])
# Join the parameters dictionaries together
parameters = dict()
if parameters_text:
parameters.update(parameters_text)
parameters.update(parameters_clf) # Make sure you have scikit-learn version 0.19 or higher to use multiple scoring metrics
grid_search = GridSearchCV(pipeline, parameters, n_jobs=-1, verbose=1, cv=5)
print("Performing grid search...")
print("pipeline:", [name for name, _ in pipeline.steps])
print("parameters:")
pprint(parameters) t0 = time()
grid_search.fit(X_train, y_train)
print("done in %0.3fs" % (time() - t0))
print() print("Best CV score: %0.3f" % grid_search.best_score_)
print("Best parameters set:")
best_parameters = grid_search.best_estimator_.get_params()
for param_name in sorted(parameters.keys()):
print("\t%s: %r" % (param_name, best_parameters[param_name]))
print("Test score with best_estimator_: %0.3f" % grid_search.best_estimator_.score(X_test, y_test))
print("\n")
print("Classification Report Test Data")
print(classification_report(y_test, grid_search.best_estimator_.predict(X_test)))
return grid_search
GridSearchCV 的参数网格
在网格搜索中,我们将研究分类器的性能。用于测试性能的一组参数如下所示。
# Parameter grid settings for the vectorizers (Count and TFIDF)
parameters_vect = {
'features__pipe__vect__max_df': (0.25, 0.5, 0.75),
'features__pipe__vect__ngram_range': ((1, 1), (1, 2)),
'features__pipe__vect__min_df': (1,2)
} # Parameter grid settings for MultinomialNB
parameters_mnb = {
'clf__alpha': (0.25, 0.5, 0.75)
} # Parameter grid settings for LogisticRegression
parameters_logreg = {
'clf__C': (0.25, 0.5, 1.0),
'clf__penalty': ('l1', 'l2')
}
分类器
这里我们将比较一下MultinomialNB
和LogisticRegression
的性能。
mnb = MultinomialNB()
logreg = LogisticRegression()
计数矢量器
为了在分类器中使用单词,我们需要将单词转换成数字。Sklearn 的CountVectorizer
获取所有推文中的所有单词,分配一个 ID,并统计每个推文中该单词的出现频率。然后,我们使用这个单词包作为分类器的输入。这一袋单词是一个稀疏的数据集。这意味着每条记录都将有许多零,代表没有在 tweet 中出现的单词。
countvect = CountVectorizer()# MultinomialNB
best_mnb_countvect = grid_vect(mnb, parameters_mnb, X_train, X_test, parameters_text=parameters_vect, vect=countvect)
joblib.dump(best_mnb_countvect, '../output/best_mnb_countvect.pkl')# LogisticRegression
best_logreg_countvect = grid_vect(logreg, parameters_logreg, X_train, X_test, parameters_text=parameters_vect, vect=countvect)
joblib.dump(best_logreg_countvect, '../output/best_logreg_countvect.pkl')
TF-IDF 矢量器
CountVectorizer 的一个问题是可能会有频繁出现的单词。这些词可能没有歧视性信息。因此它们可以被移除。 TF-IDF(词频—逆文档频率)可以用来对这些频繁出现的词进行降权。
tfidfvect = TfidfVectorizer()# MultinomialNB
best_mnb_tfidf = grid_vect(mnb, parameters_mnb, X_train, X_test, parameters_text=parameters_vect, vect=tfidfvect)
joblib.dump(best_mnb_tfidf, '../output/best_mnb_tfidf.pkl')# LogisticRegression
best_logreg_tfidf = grid_vect(logreg, parameters_mnb, X_train, X_test, parameters_text=parameters_vect, vect=tfidfvect)
joblib.dump(best_logreg_tfidf, '../output/best_logreg_tfidf.pkl')
Word2Vec
将单词转换成数值的另一种方法是使用Word2Vec
。Word2Vec 将每个单词映射到多维空间中。它通过考虑一个词在推文中出现的上下文来做到这一点。结果,相似的单词在多维空间中也彼此接近。
Word2Vec 算法是 gensim 包的一部分。
Word2Vec 算法使用单词列表作为输入。为此,我们使用了nltk
包的word_tokenize
方法。
SIZE = 50X_train['clean_text_wordlist'] = X_train.clean_text.apply(lambda x : word_tokenize(x))
X_test['clean_text_wordlist'] = X_test.clean_text.apply(lambda x : word_tokenize(x))model = gensim.models.Word2Vec(X_train.clean_text_wordlist
, min_count=1
, size=SIZE
, window=5
, workers=4)model.most_similar('plane', topn=3)
Word2Vec 模型提供了所有 tweets 中的词汇。对于每个单词,你也有它的向量值。向量值的数量等于所选的大小。这些是每个单词在多维空间中映射的维度。出现次数少于min_count
的单词不会保留在词汇表中。
min_count 参数的一个副作用是一些 tweets 可能没有向量值。当 tweet 中的单词在少于 min_count 的 tweet 中出现时,就会出现这种情况。由于 tweets 的语料库很小,在我们的案例中有发生这种情况的风险。因此,我们将 min_count 值设置为 1。
推文可以有不同数量的向量,这取决于它包含的字数。为了使用这个输出进行建模,我们将计算每条 tweet 的所有向量的平均值。因此,我们将拥有相同数量(即大小)的输入变量。
我们用函数compute_avg_w2v_vector
来做这件事。在这个函数中,我们还检查 tweet 中的单词是否出现在 Word2Vec 模型的词汇表中。如果不是,则返回一个用 0.0 填充的列表。否则是单词向量的平均值。
def compute_avg_w2v_vector(w2v_dict, tweet):
list_of_word_vectors = [w2v_dict[w] for w in tweet if w in w2v_dict.vocab.keys()]
if len(list_of_word_vectors) == 0:
result = [0.0]*SIZE
else:
result = np.sum(list_of_word_vectors, axis=0) / len(list_of_word_vectors)
return resultX_train_w2v = X_train['clean_text_wordlist'].apply(lambda x: compute_avg_w2v_vector(model.wv, x))
X_test_w2v = X_test['clean_text_wordlist'].apply(lambda x: compute_avg_w2v_vector(model.wv, x))
这给了我们一个向量维数等于SIZE
的序列。现在我们将分割这个向量并创建一个数据帧,每个向量值在单独的列中。这样,我们可以将 Word2Vec 变量连接到其他 TextCounts 变量。我们需要重用X_train
和X_test
的索引。否则,这将在以后的连接中产生问题(重复)。
X_train_w2v = pd.DataFrame(X_train_w2v.values.tolist(), index= X_train.index)
X_test_w2v = pd.DataFrame(X_test_w2v.values.tolist(), index= X_test.index)# Concatenate with the TextCounts variables
X_train_w2v = pd.concat([X_train_w2v, X_train.drop(['clean_text', 'clean_text_wordlist'], axis=1)], axis=1)
X_test_w2v = pd.concat([X_test_w2v, X_test.drop(['clean_text', 'clean_text_wordlist'], axis=1)], axis=1)
我们只考虑逻辑回归,因为我们在 Word2Vec 向量中有负值。多项式 lNB 假设变量具有多项式分布。因此它们不能包含负值。
best_logreg_w2v = grid_vect(logreg, parameters_logreg, X_train_w2v, X_test_w2v, is_w2v=True)
joblib.dump(best_logreg_w2v, '../output/best_logreg_w2v.pkl')
结论
- 当使用计数矢量器的特性时,这两种分类器都能获得最佳结果
- 逻辑回归优于多项式朴素贝叶斯分类器
- 测试集上的最佳性能来自带有 CountVectorizer 特性的 LogisticRegression。
最佳参数:
- c 值为 1
- L2 正则化
- max_df: 0.5 或最大文档频率 50%。
- min_df: 1 或者这些词需要出现在至少两条推文中
- ngram_range: (1,2),两个单词都作为二元语法使用
评估指标:
- 测试准确率为 81.3%。这优于预测所有观察的多数类(这里是负面情绪)的基线性能。基线会给出 63%的准确度。
- 这三个类别的精度都相当高。例如,在我们预测为负面的所有案例中,80%是负面的。
- 中性类的召回率很低。在我们测试数据的所有中性案例中,我们只预测 48%是中性的。
对新推文应用最佳模型
为了好玩,我们将使用最佳模型,并将其应用于一些包含“@VirginAmerica”的新推文。我手动选择了 3 条负面和 3 条正面的推文。
多亏了 GridSearchCV,我们现在知道了什么是最好的超参数。因此,现在我们可以在所有训练数据上训练最佳模型,包括我们之前分离的测试数据。
textcountscols = ['count_capital_words','count_emojis','count_excl_quest_marks','count_hashtags'
,'count_mentions','count_urls','count_words']features = FeatureUnion([('textcounts', ColumnExtractor(cols=textcountscols))
, ('pipe', Pipeline([('cleantext', ColumnExtractor(cols='clean_text'))
, ('vect', CountVectorizer(max_df=0.5, min_df=1, ngram_range=(1,2)))]))]
, n_jobs=-1)pipeline = Pipeline([
('features', features)
, ('clf', LogisticRegression(C=1.0, penalty='l2'))
])best_model = pipeline.fit(df_model.drop('airline_sentiment', axis=1), df_model.airline_sentiment)# Applying on new positive tweets
new_positive_tweets = pd.Series(["Thank you @VirginAmerica for you amazing customer support team on Tuesday 11/28 at @EWRairport and returning my lost bag in less than 24h! #efficiencyiskey #virginamerica"
,"Love flying with you guys ask these years. Sad that this will be the last trip 😂 @VirginAmerica #LuxuryTravel"
,"Wow @VirginAmerica main cabin select is the way to fly!! This plane is nice and clean & I have tons of legroom! Wahoo! NYC bound! ✈️"])df_counts_pos = tc.transform(new_positive_tweets)
df_clean_pos = ct.transform(new_positive_tweets)
df_model_pos = df_counts_pos
df_model_pos['clean_text'] = df_clean_posbest_model.predict(df_model_pos).tolist()# Applying on new negative tweets
new_negative_tweets = pd.Series(["@VirginAmerica shocked my initially with the service, but then went on to shock me further with no response to what my complaint was. #unacceptable @Delta @richardbranson"
,"@VirginAmerica this morning I was forced to repack a suitcase w a medical device because it was barely overweight - wasn't even given an option to pay extra. My spouses suitcase then burst at the seam with the added device and had to be taped shut. Awful experience so far!"
,"Board airplane home. Computer issue. Get off plane, traverse airport to gate on opp side. Get on new plane hour later. Plane too heavy. 8 volunteers get off plane. Ohhh the adventure of travel ✈️ @VirginAmerica"])df_counts_neg = tc.transform(new_negative_tweets)
df_clean_neg = ct.transform(new_negative_tweets)
df_model_neg = df_counts_neg
df_model_neg['clean_text'] = df_clean_negbest_model.predict(df_model_neg).tolist()
该模型对所有推文进行了正确分类。应该使用更大的测试集来评估模型的性能。但是在这个小数据集上,它做了我们想要做的事情。
我希望你喜欢读这个故事。如果你做到了,请鼓掌。