使用pytorch进行IMDB情感分析
建议:将代码整合到main()函数中。
1. 配置
1.1 设置cuda和随机种子
# 设置cuda
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
SEED = 1234
torch.manual_seed(SEED) # 为cpu设置随机种子
torch.cuda.manual_seed(SEED) # 为gpu设置随机种子
torch.backends.cudnn.deterministic = True # 提升一点训练速度,没有额外开销
1.2 将控制台保存
class Logger(object):
def __init__(self, filename='default.log', stream=sys.stdout):
self.terminal = stream
self.log = open(filename, 'w')
def write(self, message):
self.terminal.write(message)
self.log.write(message)
def flush(self):
pass
sys.stdout = Logger('EmbeddingBag_IMDB.log', sys.stdout)
2. 部分超参数定义
Batch_Size = 64
N_epochs = 10
Dropout = 0.2
LR = 0.1
Emb_Dim = 100
Hidden_Dim = 768
Output_dim = 1
3. 加载数据
主要使用torchtext进行数据的加载。数据最终的形式为可以直接放到神经网络的iterator数字序列。这里使用成熟的spacy作为tokenizer,其他的berttokenizer等都可以使用。
def load_data():
"""
加载IMDB数据,
:return: 返回 data_iter, valid_iter, test_iter格式的数据。
"""
# 使用en_core_web_sm作为tokenizer的语言,可以使用en_core_web_trf或者large来提升token序列化,从而提升模型acc。
# 按照“常理”,将batch放在第一个纬度。
TEXT = data.Field(tokenize=tokenizer, tokenizer_language='en_core_web_sm',
batch_first=True)
LABEL = data.LabelField(dtype=torch.float)
# load和split数据。这个数据需要下载。
train_data, test_data = datasets.IMDB.splits(root='../.data',
text_field=TEXT,
label_field=LABEL)
# 再将train_data分割为train和valid的常规操作。
train_data, valid_data = train_data.split(split_ratio=0.7,
random_state=random.seed(SEED))
print('build vocab...')
# 定义build_vocab使用的向量,这个就相当于是别人训练好的非常精炼的词向量,直接用在我们的数据的单词,这样我们就不需要从头训练。
# 对了,这条解释是我猜的。。。:)。
# 这行代码的作用就是使用之前下载的glove数据,因为glove数据很大,每次下载很不方便。
# 如果使用缓存数据,直接将下一行的 vectors='glove.6B.100d'替换为vectors=vectors。
vectors = torchtext.vocab.Vectors(name='glove.6B.100d.txt', cache='../.vector_cache/')
# traindata已经有数据的所有内容,所以直接用traindata构建vocabulary。这里的maxsize可选可不选。
TEXT.build_vocab(train_data,
max_size=25000,
vectors='glove.6B.100d',
unk_init=torch.Tensor.normal_)
LABEL.build_vocab(train_data)
# 创建torch需要的iterator形式。将数据在gpu上训练。
train_iter, valid_iter, test_iter = data.BucketIterator.splits(
datasets=(train_data, valid_data, test_data),
batch_size=Batch_Size,
device=device)
return train_iter, valid_iter, test_iter, TEXT, LABEL
tokenizer = 'spacy'
train_iter, valid_iter, test_iter, TEXT, LABEL = load_data()
3. 创建模型
定义和创建模型。模型由一个EmbeddingBag层和全连接层组成。可以根据个人口味添加一些其他层。
class EmbeddingBag(nn.Module):
"""
使用一个embedding bag层对每个batch的数据进行embedding和平均操作,
nn.embedding bag 类似于 nn.embedding + mean(),
最后添加两个全连接层。
"""
def __init__(self, vocab_size, emb_dim, hidden_dim, output_dim, pad_idx, dropout=0):
super(EmbeddingBag, self).__init__()
# 这里需要注意的一个是,padding_idx=pad_idx,因为使用的词向量不同,每个vocab的映射就有区别,对于padding的编号可能会不同,使用的哪个tokenizer,就要使用那个tokenizer的 pad_id/unk_id/cls_id/sep_id。否则映射的id错误,会直接影响到模型的拟合能力。
self.embedding = nn.EmbeddingBag(vocab_size, emb_dim, mode='mean', padding_idx=pad_idx)
self.linear = nn.Linear(emb_dim, output_dim)
self.dropout = nn.Dropout(dropout)
def forward(self, text):
# x直接输出使用。使用的embeddingbag()函数,所以这里不需要对emb的数据进行squeeze(1)纬度合并操作。
x = self.embedding(text)
out = self.linear(x)
return out
# 剩余初始化参数,将vectors glove的词向量作为初始化参数
vocab_size = len(TEXT.vocab)
UNK_IDX = TEXT.vocab.stoi[TEXT.unk_token]
PAD_IDX = TEXT.vocab.stoi[TEXT.pad_token]
model = EmbeddingBag(vocab_size, Emb_Dim, Hidden_Dim, Output_dim, PAD_IDX, Dropout)
criterion = nn.BCEWithLogitsLoss()
optimizer = optim.Adam(model.parameters())
4. 准备训练模型的函数
包括train(), evaluate(), binary_acc(), epoch_time()函数。
def binary_acc(predictions, label):
# predictions是一整个batch的预测
# 首先对prediction进行sigmoid,
# 然后通过round()函数对sigmoid的结果进行四舍五入,得到0、1.
# 这个函数很简单,如果不懂可以使用torch.tensor创建一个一维的数据的predictions和label模拟一下。
predictions = torch.round(torch.sigmoid(predictions))
corrects = (predictions == label).float()
acc = sum(corrects) / len(corrects)
return acc
def train(model: nn.Module,
iterator: data.BucketIterator,
optimizer: optim.Adam,
criterion: nn.BCEWithLogitsLoss):
epoch_loss = 0
epoch_acc = 0
# total_len:保存当前iterator的所有样本数,等价 len(iterator) * batch_size,
# 但是有时候iterator的最后一个batch不等于batch_size,有的最后一个batch等于30,batch_size等于64,
# 所以len(iterator) * batch_size不准确,但是对于loss影响很小,所以两种方法均可。
total_len = 0
# 将模型调整为model.train()模式。具体可搜索model.train()和model.eval()的区别。
model.train()
for batch in iterator:
optimizer.zero_grad()
# [batch_size, output_dim] --> [batch_size]。具体可以在这里写一个print(predictions),看一下shape,是可以压缩的,而且必须压缩,不然和label的纬度不同没办法比较。
predictions = model(batch.text).squeeze(1)
loss = criterion(predictions, batch.label)
acc = binary_acc(predictions, batch.label)
loss.backward()
optimizer.step()
# pytorch新版本使用item(),而item()必须乘以batch才是一整个batch的loss。acc同理。
# 这里有点绕,可以将loss.item()和loss都print()到控制台,便于理解。
epoch_loss += loss.item() * len(batch.label)
epoch_acc += acc.item() * len(batch.label)
total_len += len(batch.label)
return epoch_loss / total_len, epoch_acc / total_len
def evaluate(model: nn.Module,
iterator: data.BucketIterator,
criterion: nn.BCEWithLogitsLoss):
epoch_loss = 0
epoch_acc = 0
total_len = 0
model.eval()
# with torch.no_grad():我也不知道为什么写这句,大家都写,我也写。
with torch.no_grad():
for batch in iterator:
predictions = model(batch.text).squeeze(1)
loss = criterion(predictions, batch.label)
acc = binary_acc(predictions, batch.label)
epoch_loss += loss * len(batch.label)
epoch_acc += acc * len(batch.label)
total_len += len(batch.label)
return epoch_loss / total_len, epoch_acc / total_len
def epoch_time(start_time, end_time):
inter_time = end_time - start_time
inter_mins = int(inter_time / 60)
inter_secs = int(inter_time % 60)
return inter_mins, inter_secs
5. 训练函数for loop
best_valid_loss = float('inf') # 定义源loss为无限大
for epoch in range(N_epochs):
start_time = time.time()
print('第', epoch, '次循环:')
train_loss, train_acc = train(model, train_iter, optimizer, criterion)
valid_loss, valid_acc = evaluate(model, valid_iter, criterion)
end_time = time.time()
epoch_mins, epoch_secs = epoch_time(start_time, end_time)
if valid_loss < best_valid_loss:
best_valid_loss = valid_loss
torch.save(model.state_dict(), 'EmbeddingBag_IMDB.pt')
print(f'Epoch{epoch:02}, time: {epoch_mins}m {epoch_secs}s')
print(f'\ttrain loss: {train_loss:.3f}, train acc: {train_acc * 100:.2f}%')
print(f'\t valid loss: {valid_loss:.3f}, valid acc: {valid_acc * 100:.2f}%')
6. 测试
model_test = EmbeddingBag(vocab_size, Emb_Dim, Hidden_Dim, Output_dim, PAD_IDX, Dropout)
model_test.load_state_dict(torch.load('EmbeddingBag_IMDB.pt'))
model_test.eval()
test_loss, test_acc = evaluate(model_test, test_iter, criterion)
print(f'\ttest loss: {test_loss:.3f}, test acc: {test_acc * 100:.2f}%')