https://github.com/lukysummer/Movie-Review-Sentiment-Analysis-LSTM-Pytorch

如有侵权立即删除。


数据集

一行一个样本,共25000行

标签:25000个

1. 读取训练文本

with open("data/reviews.txt") as f:
    reviews = f.read()
    
with open("data/labels.txt") as f:
    labels = f.read()


# 3行的小数据集
with open("./data/small_reviews.txt") as f:
    reviews = f.read()
print(reviews)
print(type(reviews))   #<class 'str'>

with open("./data/small_labels.txt") as f:
    labels = f.read()
print(labels)
print(type(labels)) # <class 'str'>

先用三行的小文本测试的

2.文本预处理

from string import punctuation

def preprocess(text):
    text = text.lower()
    text = "".join([ch for ch in text if ch not in punctuation]) # 3行
    all_reviews = text.split("\n")
    # text = " ".join(text)
    text = " ".join(all_reviews) #改动  把所有的内容连接 添加到了text中 1行
    # print(text) # bromwell high is a cartoon comedy  it ran at the same tim
    all_words = text.split()
    # print(all_words) # ['bromwell', 'high', 'is', 'a', 'cartoon', 'comedy', 'it', 'ran', 'at', 'the', 'same', 'time', 'as', 'some', 'other', 'programs', 'about', 'school', 'life', 'such',
    return all_reviews, all_words


all_reviews, all_words = preprocess(reviews)

 前面是读取了数据集的内容reviews 3行

然后将数据全部转为小写 3行

然后去除所有的标点符号 3行

将内容按行划分为列表。一维列表,每个元素是一个篇评论。

将内容连接成一行

将内容按照空格划分为 单词列表

返回   评论列表(小写且不包含标点)和单词列表

3.创建词典并对评论进行编码

from collections import Counter  # 用于计算可迭代对象中元素的频率。

word_counts = Counter(all_words)  # 这是一个频率字典  # 使用 Counter 计算 all_words 中每个单词的频率,将结果存储在 word_counts 中,它是一个字典,其中键是单词,值是对应的频率。
word_list = sorted(word_counts, key=word_counts.get, reverse=True) #这是单词列表 # 将 word_counts 中的单词按照它们的频率从高到低排序,并将排序后的结果存储在 word_list 中。key=word_counts.get 意味着按照每个单词在 word_counts 中的频率进行排序,reverse=True 表示降序排列。
print(word_list) # ['the', 'a', 'to', 'it', 'of', 's', 'on', 'is', 'who', 'or', 'br', 'as', 'that
vocab_to_int = {word:idx+1 for idx, word in enumerate(word_list)} # 整一个字典 键是词,值是标识
int_to_vocab = {idx:word for word, idx in vocab_to_int.items()} # 整一个字典  反过来  键是标识,值是词
# encoded_reviews = [[vocab_to_int[word] for word in review] for review in all_reviews]
### fixed by Ritian-Li ###
encoded_reviews = [[vocab_to_int.get(word) for word in review.split()] for review in all_reviews]
print(encoded_reviews)

 这是两个字典:vocab_to_int、int_to_vocab

 encoded_reviews 将评论转化为二维列表,每个元素代表一个评论,其中每个评论是将单词转化为对应索引。

# 将评论中得每个单词转化为对应的索引,得到一个二维列表,每个列表因为评论长度不同,列表长度也不同

 4.编码标签

all_labels = labels.split("\n")
encoded_labels = [1 if label == "positive" else 0 for label in all_labels] # 将标签转化为0/1 ,积极得就是1.消极的就是0
print(encoded_labels)
print(len(encoded_labels))
# 判断 评论的列表长度是否等于标签列表长度   如果不相同,将引发断言错误,显示错误消息, "# of encoded reivews & encoded labels must be the same!"
assert len(encoded_reviews) == len(encoded_labels), "# of encoded reivews & encoded labels must be the same!"

 

5.去掉长度为0的评论

import numpy as np
import torch

# 获得评论长度大于0的标签列表
encoded_labels = np.array( [label for idx, label in enumerate(encoded_labels) if len(encoded_reviews[idx]) > 0] )
# 获得评论长度大于0的评论列表
encoded_reviews = [review for review in encoded_reviews if len(review) > 0]

6.使所有评论的篇幅相同

# 使篇幅长度相同
def pad_text(encoded_reviews, seq_length):
    
    reviews = []
    
    for review in encoded_reviews:
        if len(review) >= seq_length:
            reviews.append(review[:seq_length])  # 直接去除长于200的那部分
        else:
            reviews.append([0]*(seq_length-len(review)) + review) # 如果 不到200,则在前面填充0,使长度达到200
            # print(reviews)
        
    return np.array(reviews)

padded_reviews = pad_text(encoded_reviews, seq_length=200) # 填充后的评论,现在评论的长度都相同,都是200
print(padded_reviews.shape) # (3, 200)

 reviews的样子 是一个列表

 padded_reviews 是numpy类型的数组

 7.划分数据并获取(评论、标签)数据加载器

 

train_ratio = 0.8
valid_ratio = (1 - train_ratio)/2
total = padded_reviews.shape[0] # 总共有多少条评论,也就是总共有多少样本
train_cutoff = int(total * train_ratio) # 训练个数
valid_cutoff = int(total * (1 - valid_ratio)) # 验证个数

train_x, train_y = padded_reviews[:train_cutoff], encoded_labels[:train_cutoff] # 训练 都是numpy类型的数组
valid_x, valid_y = padded_reviews[:train_cutoff : valid_cutoff], encoded_labels[train_cutoff : valid_cutoff] # 验证
test_x, test_y = padded_reviews[valid_cutoff:], encoded_labels[valid_cutoff:] # 测试

from torch.utils.data import TensorDataset, DataLoader

# 转化为tensor类型的数据集
train_data = TensorDataset(train_x, train_y)
valid_data = TensorDataset(valid_x, valid_y)
test_data = TensorDataset(test_x, test_y)

# 转化为dataloader
batch_size = 20  # 从50转为20
train_loader = DataLoader(train_data, batch_size = batch_size, shuffle = True)
valid_loader = DataLoader(valid_data, batch_size = batch_size, shuffle = True)
test_loader = DataLoader(test_data, batch_size = batch_size, shuffle = True)

 8.定义LSTM模型

from torch import nn

class SentimentLSTM(nn.Module):
    
    def __init__(self, n_vocab, n_embed, n_hidden, n_output, n_layers, drop_p = 0.5):
        super().__init__()
        # params: "n_" means dimension
        self.n_vocab = n_vocab     # number of unique words in vocabulary
        self.n_layers = n_layers   # number of LSTM layers 
        self.n_hidden = n_hidden   # number of hidden nodes in LSTM
        
        self.embedding = nn.Embedding(n_vocab, n_embed)
        self.lstm = nn.LSTM(n_embed, n_hidden, n_layers, batch_first = True, dropout = drop_p)
        self.dropout = nn.Dropout(drop_p)
        self.fc = nn.Linear(n_hidden, n_output)
        self.sigmoid = nn.Sigmoid()
        
        
    def forward (self, input_words):
                                             # INPUT   :  (batch_size, seq_length)
        embedded_words = self.embedding(input_words)    # (batch_size, seq_length, n_embed)
        lstm_out, h = self.lstm(embedded_words)         # (batch_size, seq_length, n_hidden)
        lstm_out = self.dropout(lstm_out)
        lstm_out = lstm_out.contiguous().view(-1, self.n_hidden) # (batch_size*seq_length, n_hidden)
        fc_out = self.fc(lstm_out)                      # (batch_size*seq_length, n_output)
        sigmoid_out = self.sigmoid(fc_out)              # (batch_size*seq_length, n_output)
        sigmoid_out = sigmoid_out.view(batch_size, -1)  # (batch_size, seq_length*n_output)
        
        # extract the output of ONLY the LAST output of the LAST element of the sequence
        sigmoid_last = sigmoid_out[:, -1]               # (batch_size, 1)
        
        return sigmoid_last, h
    
    
    def init_hidden (self, batch_size):  # initialize hidden weights (h,c) to 0
        
        device = "cuda" if torch.cuda.is_available() else "cpu"
        weights = next(self.parameters()).data
        h = (weights.new(self.n_layers, batch_size, self.n_hidden).zero_().to(device),
             weights.new(self.n_layers, batch_size, self.n_hidden).zero_().to(device))
        
        return h

初始化lstm,参数的意思

self.lstm = nn.LSTM(n_embed, n_hidden, n_layers, batch_first = True, dropout = drop_p) 
  • n_embed: 输入特征的维度,通常是词嵌入的维度。在这个情感分析模型中,n_embed 表示每个单词的嵌入维度。

  • n_hidden: LSTM 层中的隐藏节点数,表示模型学习的表示空间的维度。更多的隐藏节点可能允许模型学到更复杂的特征,但也会增加计算成本。

  • n_layers: LSTM 层的数量。多层 LSTM 允许模型学习更复杂的时序模式。

  • batch_first=True: 这个参数指定输入数据的形状。如果设置为 True,输入数据的形状应该是 (batch_size, seq_length, n_embed)其中 batch_size 表示每个批次的样本数,seq_length 表示序列的长度(在这个例子里,是一个评论的长度是200),n_embed 表示每个单词的嵌入维度。

  • dropout=drop_p: 这是一个可选的参数,用于指定在 LSTM 层中是否使用 dropout。dropout 是一种正则化技术,有助于防止过拟合。它指定在网络中随机丢弃输入单元的比例。

self.lstm = nn.LSTM(n_embed, n_hidden, n_layers, batch_first = True, dropout = drop_p) self.dropout = nn.Dropout(drop_p)

为什么上面lstm有一个dropout,下面还要加一个

        总体而言,这一行代码创建了一个具有指定参数的 LSTM 层,用于处理输入序列。在情感分析中,输入序列通常是单词序列,而 LSTM 层有助于模型理解文本中的时序信息。

        在上述代码中,nn.LSTMdropout 参数是关于输入的 dropout,而下面的 nn.Dropout 则是关于输出的 dropout。        

具体来说:

  1. self.lstm = nn.LSTM(n_embed, n_hidden, n_layers, batch_first=True, dropout=drop_p): 这里的 dropout 参数是用于控制输入层到LSTM层的 dropout。这表示在将输入序列传递到LSTM层之前,每个输入元素都有 drop_p 的概率被随机清零,这有助于防止过拟合。

  2. self.dropout = nn.Dropout(drop_p): 这里的 nn.Dropout 是一个独立的 dropout 层,通常用于模型的其他部分,比如在全连接层之后。这个 dropout 用于控制从 LSTM 层到全连接层的输出的 dropout。它有助于防止模型在训练时对于特定的输入过于依赖,同样也是为了防止过拟合。

这两者一起使用有助于提高模型的泛化能力,降低对于训练数据的过拟合风险。dropout 在训练时起到正则化的作用,而在模型评估或推断时,这些 dropout 层通常会被关闭。

9.实例化带有超参数的模型

n_vocab = len(vocab_to_int)
n_embed = 400
n_hidden = 512
n_output = 1   # 1 ("positive") or 0 ("negative")
n_layers = 2

net = SentimentLSTM(n_vocab, n_embed, n_hidden, n_output, n_layers)

10.定义损失和优化器

from torch import optim

criterion = nn.BCELoss()
optimizer = optim.Adam(net.parameters(), lr = 0.001)

11. 训练这个网络

12.在测试集上测试训练后的模型

13.在随机单次审查中测试训练后的模型

 

 一些知识点:

1.nn.Embedding(num_embeddings=vocab_size, embedding_dim=embed_dim)

参数是词表大小和嵌入维度

这个函数的作用是,对你的词表产生一个嵌入表,这个嵌入只保证唯一性,不保证相关性(所以和word2vec还是有区别的)。

在使用时,直接调用:

embedding = self.embedding(input)

这里的 input 应为一个下标列表,输出即为对应的嵌入。

Logo

瓜分20万奖金 获得内推名额 丰厚实物奖励 易参与易上手

更多推荐