用lstm实现nlp情感分析(roman urdu小语种为例)代码+原理详解

10 篇文章 1 订阅

1 赛题描述

link: https://www.kesci.com/home/competition/5c77ab9c1ce0af002b55af86/content/1
本练习赛所用数据,是名为「Roman Urdu DataSet」的公开数据集。
这些数据,均为文本数据。原始数据的文本,对应三类情感标签:Positive, Negative, Netural。
本练习赛,移除了标签为Netural的数据样例。因此,练习赛中,所有数据样例的标签为Positive和Negative。
本练习赛的任务是「分类」。「分类目标」是用训练好的模型,对测试集中的文本情感进行预测,判断其情感为「Negative」或者「Positive」。

本文全部代码上传至我的github:
https://github.com/willinseu/kesci-urdu-sentiment-analysis
觉得对你有帮助的话,请帮我点一下star,多谢!

2.lstm解法

读取数据:

df_train = pd.read_csv('train.csv',lineterminator='\n')
df_test = pd.read_csv('test.csv',lineterminator='\n')
df_train['label'] = df_train['label'].map({'Negative':0,'Positive':1})
df_train.head(20)

在这里插入图片描述
由于是小语种,所以我们也看不懂上面的话是什么意思,但是标签我们还是看的懂的,并用map函数编码。

#test if nan exists
df_train.isnull().sum()

检查是否有缺失值:
在这里插入图片描述

df_test.head()

查看测试集,显然是没有标签的:
在这里插入图片描述

numpy_array = df_train.as_matrix()
numpy_array_test = df_test.as_matrix()

将训练集,测试集转为矩阵形式:
在这里插入图片描述
(其实没有必要这么做,直接对df表格操作即可。但是在此不做深究了。)
可以看到每一行被我们拆分成了id,text,label的形式。

#two commom ways to clean data
def cleaner(word):
  word = re.sub(r'\#\.', '', word)
  word = re.sub(r'\n', '', word)
  word = re.sub(r',', '', word)
  word = re.sub(r'\-', ' ', word)
  word = re.sub(r'\.', '', word)
  word = re.sub(r'\\', ' ', word)
  word = re.sub(r'\\x\.+', '', word)
  word = re.sub(r'\d', '', word)
  word = re.sub(r'^_.', '', word)
  word = re.sub(r'_', ' ', word)
  word = re.sub(r'^ ', '', word)
  word = re.sub(r' $', '', word)
  word = re.sub(r'\?', '', word)

  return word.lower()
def hashing(word):
  word = re.sub(r'ain$', r'ein', word)
  word = re.sub(r'ai', r'ae', word)
  word = re.sub(r'ay$', r'e', word)
  word = re.sub(r'ey$', r'e', word)
  word = re.sub(r'ie$', r'y', word)
  word = re.sub(r'^es', r'is', word)
  word = re.sub(r'a+', r'a', word)
  word = re.sub(r'j+', r'j', word)
  word = re.sub(r'd+', r'd', word)
  word = re.sub(r'u', r'o', word)
  word = re.sub(r'o+', r'o', word)
  word = re.sub(r'ee+', r'i', word)
  if not re.match(r'ar', word):
    word = re.sub(r'ar', r'r', word)
  word = re.sub(r'iy+', r'i', word)
  word = re.sub(r'ih+', r'eh', word)
  word = re.sub(r's+', r's', word)
  if re.search(r'[rst]y', 'word') and word[-1] != 'y':
    word = re.sub(r'y', r'i', word)
  if re.search(r'[bcdefghijklmnopqrtuvwxyz]i', word):
    word = re.sub(r'i$', r'y', word)
  if re.search(r'[acefghijlmnoqrstuvwxyz]h', word):
    word = re.sub(r'h', '', word)
  word = re.sub(r'k', r'q', word)
  return word

def array_cleaner(array):
  # X = array
  X = []
  for sentence in array:
    clean_sentence = ''
    words = sentence.split(' ')
    for word in words:
      clean_sentence = clean_sentence +' '+ cleaner(word)
    X.append(clean_sentence)
  return X

上面定义的是一个常用的nlp语句清洗的函数。同时会把一个array转为一个list,这个list我们下面会看到。

X_test = numpy_array_test[:,1]
X_train = numpy_array[:, 1]
# Clean X here
X_train = array_cleaner(X_train)
X_test = array_cleaner(X_test)
y_train = numpy_array[:, 2]

利用上面定义的函数进行清洗:
得到而list:
在这里插入图片描述
现在好像看不到这样清洗有什么作用,但需要说明的是,确实是有用的。会去除掉一些没用的符号,但是这里也没有很好的针对urdu特别设置,所以还是有瑕疵的。

print(len(X_train))
print(len(X_test))
print(len(y_train))

打印长度,确认中间没有出错。
在这里插入图片描述

y_train = np.array(y_train)
y_train = y_train.astype('int8')
y_train[:6]

转化y_train的格式。
在这里插入图片描述

X_all = X_train + X_test # Combine both to fit the tokenizer.
lentrain = len(X_train)

下面就开始编码了,我们采用的是keras.preprocessing.text.Tokenizer

tokenizer = Tokenizer(
    nb_words=2000, 
                      filters='!"#$%&()*+,-./:;<=>?@[\\]^_`{|}~\t\n',
                                   lower=True,split=' ')
tokenizer.fit_on_texts(X_all)

这时候tokenizer干的事其实很简单,就是把它看到的单词以空格划分,然后用数字来一一对应,然后我们取前2000个出现频率最高的词,其他的当做不认识。
如想很详细的了解,我整理了一些资料链接:
在这里插入图片描述
来自:https://blog.csdn.net/edogawachia/article/details/79446354
此时X_all没有变化。
在这里插入图片描述

X = tokenizer.texts_to_sequences(X_all)
# X = pad_sequences(X)
X[:2]

现在开始text–>sequence。
在这里插入图片描述
可以看到长短不一,我们需要pad填充。

X = pad_sequences(X)
X[:2]

在这里插入图片描述
被填充成了最大的长度。
在这里插入图片描述
可以看到到目前为止,我们的X变为了(9040,219)的数据维度。
下面就开始 了embedding以及lstm。

embed_dim = 128
lstm_out = 256
batch_size = 32

model = Sequential()
model.add(Embedding(2000,embed_dim, input_length=X.shape[1],dropout = 0.2))
model.add(LSTM(lstm_out, dropout_U = 0.2, dropout_W = 0.2,return_sequences=True))
model.add(Flatten())
model.add(Dense(2,activation='softmax'))
model.compile(loss = 'categorical_crossentropy', optimizer='adam',metrics = ['accuracy'])
# model.compile(loss = 'binary_crossentropy',optimizer='adam',metrics = ['accuracy'])
print(model.summary())

在这里插入图片描述

3.深究embedding层。

因为代码实现的话,很简单,add,add的就完事了 ,但是其中的细节与原理我认为才是最重要的。但是本人能力有限,有错误请及时批评指正。

3.1为什么需要embedding层?直接输入不行吗?

1.从原理上讲,是这样的。因为在进入网络之前,我们虽然将数据处理成了整齐的(9040,219)形式,但是这219维向量彼此之间是没有关系的,也就是说你这样处理只是干巴巴的把文字变成了数字,但是前后文的关系被你丢失了,而lstm我们都知道它会考虑前后文的关系,而你这时候的数据已经缺失了上下文关系,所以需要经过一种手段,重新还原原来的上下文关系,这种手段就是embedding层。embedding层和word2vec是一样的,无论是Skip-gram 还是CBOW 模型,他们都是由上下文与当前互相推断,所以考虑了前后文关系。在此我们不展开讲,我之前整理过一些连接:
https://blog.csdn.net/ssswill/article/details/88319996

3.2网络参数为什么是256000个?

这个问题其实不难回答,因为word2vec就是把one-hot向量转为了你指定的embedding层维度。
也就是说:你的数据流向是这样的:
(9040,219,1)–》(9040,219,2000),这是onehot表示。这一步不是embedding层做的事。
下面就是从(9040,219,2000)–》(9040,219,128),这个128是自己设置的维度,它代表一个词的维度。这就是embedding层做的事了。中间权重矩阵就很好写了:(2000,128)。所以参数个数为:2000*128=256000个。

3.3embedding层的输入输出?

从上面的分析,我们很容易看出来了,就是把(9040,219,1)变为了(9040,219,128)。也就是9040个句子,每个句子包含219个词,每个词的维度为128.。
在这里插入图片描述
https://blog.csdn.net/ssswill/article/details/88319996

4.深究lstm层。

4.0一些background

我们lstm层的实现也只有潇潇洒洒两句话:

model.add(LSTM(lstm_out, dropout_U = 0.2, dropout_W = 0.2,return_sequences=True))
model.add(Flatten())

return_sequences=True该参数声明为True之后,需要加一个flatten层。我尝试过,return_sequences=True之后更好。
至于它是干啥的,为啥加了它就需要flatten层?我们下面慢慢讲。

4.1从简化版的lstm讲起

embed_dim = 128
lstm_out = 256
batch_size = 32

model2 = Sequential()
model2.add(Embedding(2000,embed_dim, input_length=X.shape[1],dropout = 0.2))
model2.add(LSTM(lstm_out, dropout_U = 0.2, dropout_W = 0.2))
# model2.add(Flatten())
model2.add(Dense(2,activation='softmax'))
model2.compile(loss = 'categorical_crossentropy', optimizer='adam',metrics = ['accuracy'])
# model.compile(loss = 'binary_crossentropy',optimizer='adam',metrics = ['accuracy'])
print(model2.summary())

在这里插入图片描述
我们从embedding层的输出开始,它的输出是(32,219,128)。因为我们batch_size是32。同时我们的lstm_out=256,也就是units参数为256,这里可不是说有256个lstm的cell,而是指的是cell里隐藏层的神经元个数是4*256,也就是输出是256维的。

4.2参数为什么是394240?

(128+256)*256+256=98560。而lstm单元一共有4个权重矩阵,所以参数是4乘以98560=394240.至于原因:
https://blog.csdn.net/ssswill/article/details/88429794

4.3输出为什么是(None,256)?

因为输入是(32,219,128)。32是batch_size。219不仅是句子长度,也是一个timestep参数。即每219个时刻后更新一次参数。在时刻1,32个句子的第一个单词输入到lstm中,即输入是(32,128)。contact后变为(32,128+256),也就是(32,384)。经过权重矩阵(384+1,256),其中+1是bias。也就是每个时刻输出的是(32,256)。这样就解释了输出为什么是(None,256)。

4.4那么本代码中为啥输出是

在这里插入图片描述

一个三维向量?

很好理解,在初始版lstm中,我们每个时刻都会输出一个(32,256),而句子长度都是固定的219,所以219个时刻一共会输出219个(32,256),而没有加return_sequences=True参数之前,他只会保留最后一个时刻的输出,所以是(32,256),但是加了return_sequences=True参数之后,每个时刻的输出都会保留,那么输出就是219个(32,256),也就是(32,219,256)。但是每个时刻的输出不变,所以参数不变。

同时,保留多个时刻的输出,经过我的验证,效果是有的。

4.5flatten层作用?

现在就很好理解了,就是为了方面后面全连接层的连接而已,把一个(219,256)的二维矩阵压扁成一维。

4.6理解了吗?

因为lstm的话,我这里原理没讲,所以如果没有理清,请到我上一篇博客找一些思路,相信你一定可以搞懂。连接:
https://blog.csdn.net/ssswill/article/details/88429794

5.一些后续


后面的代码就是预测,提交了。我不再展开了,写的真的很累,可以去我的github里看一下,顺便点一下star~感谢 ,说明下,我在这里加了一个keras输出auc指标的模块,直接用就行。

同时,我觉得是由于数据量比较小,而且embedding层语料不丰富,所以lstm在此表现0.83左右,不是很理想。

6.2019年5月9日更新

很高兴我的博客帮助到了一些人,如果大家对于nlp有兴趣,或者不满足于本文提出的基础解法。欢迎到我第二个nlp大型博文。它同样是解决情感分析问题的,但是情况更复杂,我们也会用到更复杂的技术。由于时间关系我并没有更新完整。
对应的github在:
https://github.com/willinseu/kaggle-Jigsaw-Unintended-Bias-in-Toxicity-Classification-solution

里面我用本文的方法实现了对于另外一个数据集的情感分析。
至于提升版本的,还没有上传,但是其中一些关键性技术我已经写完了,但是没有给串成一个整体。可以先看一下:
在这里插入图片描述
在后面我一定会更新完的,可以watch一下我的github项目。保证干货满满。


  • 25
    点赞
  • 97
    收藏
    觉得还不错? 一键收藏
  • 9
    评论
以下是一个使用 LSTM 实现文本情感分析的案例代码: ``` import numpy as np from keras.models import Sequential from keras.layers import Dense, LSTM, Embedding from keras.preprocessing.text import Tokenizer from keras.preprocessing.sequence import pad_sequences from keras.utils import to_categorical # 定义数据集 texts = ['I love this movie', 'This movie is terrible', 'This is a great movie', 'I hate this movie'] labels = [1, 0, 1, 0] # 对文本进行分词和编码 tokenizer = Tokenizer(num_words=1000) tokenizer.fit_on_texts(texts) sequences = tokenizer.texts_to_sequences(texts) # 对序列进行填充 max_len = max([len(x) for x in sequences]) data = pad_sequences(sequences, maxlen=max_len) # 将标签进行 one-hot 编码 labels = to_categorical(np.asarray(labels)) # 构建 LSTM 模型 model = Sequential() model.add(Embedding(1000, 32, input_length=max_len)) model.add(LSTM(32)) model.add(Dense(2, activation='softmax')) model.compile(optimizer='rmsprop', loss='categorical_crossentropy', metrics=['acc']) # 训练模型 model.fit(data, labels, epochs=10, batch_size=32) # 对新数据进行预测 new_texts = ['This is a fantastic movie', 'I do not like this film'] new_sequences = tokenizer.texts_to_sequences(new_texts) new_data = pad_sequences(new_sequences, maxlen=max_len) predictions = model.predict(new_data) print(predictions) ``` 这个代码使用 LSTM 模型对文本进行情感分析,将文本分为两类:正面和负面。模型使用 Embedding 层将文本编码为向量,然后使用 LSTM 层对向量进行处理,最后使用全连接层输出分类结果。
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值