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,多谢!
读取数据:
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]
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())
因为代码实现的话,很简单,add,add的就完事了 ,但是其中的细节与原理我认为才是最重要的。但是本人能力有限,有错误请及时批评指正。
1.从原理上讲,是这样的。因为在进入网络之前,我们虽然将数据处理成了整齐的(9040,219)形式,但是这219维向量彼此之间是没有关系的,也就是说你这样处理只是干巴巴的把文字变成了数字,但是前后文的关系被你丢失了,而lstm我们都知道它会考虑前后文的关系,而你这时候的数据已经缺失了上下文关系,所以需要经过一种手段,重新还原原来的上下文关系,这种手段就是embedding层。embedding层和word2vec是一样的,无论是Skip-gram 还是CBOW 模型,他们都是由上下文与当前互相推断,所以考虑了前后文关系。在此我们不展开讲,我之前整理过一些连接:
https://blog.csdn.net/ssswill/article/details/88319996
这个问题其实不难回答,因为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个。
从上面的分析,我们很容易看出来了,就是把(9040,219,1)变为了(9040,219,128)。也就是9040个句子,每个句子包含219个词,每个词的维度为128.。
https://blog.csdn.net/ssswill/article/details/88319996
我们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层?我们下面慢慢讲。
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维的。
(128+256)*256+256=98560。而lstm单元一共有4个权重矩阵,所以参数是4乘以98560=394240.至于原因:
https://blog.csdn.net/ssswill/article/details/88429794
因为输入是(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)。
很好理解,在初始版lstm中,我们每个时刻都会输出一个(32,256),而句子长度都是固定的219,所以219个时刻一共会输出219个(32,256),而没有加return_sequences=True参数之前,他只会保留最后一个时刻的输出,所以是(32,256),但是加了return_sequences=True参数之后,每个时刻的输出都会保留,那么输出就是219个(32,256),也就是(32,219,256)。但是每个时刻的输出不变,所以参数不变。
同时,保留多个时刻的输出,经过我的验证,效果是有的。
现在就很好理解了,就是为了方面后面全连接层的连接而已,把一个(219,256)的二维矩阵压扁成一维。
因为lstm的话,我这里原理没讲,所以如果没有理清,请到我上一篇博客找一些思路,相信你一定可以搞懂。连接:
https://blog.csdn.net/ssswill/article/details/88429794
同时,我觉得是由于数据量比较小,而且embedding层语料不丰富,所以lstm在此表现0.83左右,不是很理想。
很高兴我的博客帮助到了一些人,如果大家对于nlp有兴趣,或者不满足于本文提出的基础解法。欢迎到我第二个nlp大型博文。它同样是解决情感分析问题的,但是情况更复杂,我们也会用到更复杂的技术。由于时间关系我并没有更新完整。
对应的github在:
https://github.com/willinseu/kaggle-Jigsaw-Unintended-Bias-in-Toxicity-Classification-solution
里面我用本文的方法实现了对于另外一个数据集的情感分析。
至于提升版本的,还没有上传,但是其中一些关键性技术我已经写完了,但是没有给串成一个整体。可以先看一下:
在后面我一定会更新完的,可以watch一下我的github项目。保证干货满满。