学习目标
- 学习FastText的使用和基础原理
- 学会使用验证集进行调参
深度学习可以用于文本表示,可以将词典其映射到一个低纬空间。其中比较典型的例子有:FastText、Word2Vec和Bert。在本章我们将介绍FastText,将在后面的内容介绍Word2Vec和Bert。
FastText
FastText是一种典型的深度学习词向量的表示方法,它非常简单通过Embedding层将单词映射到稠密空间,然后将句子中所有的单词在Embedding空间中进行平均,进而完成分类操作。
FastText是一个三层的神经网络,输入层、隐含层和输出层。在论文中,除了描述了网络的基础结构以外,FastText还用到了ngram和分层softmax的技巧,可以提高精度和加快运算速度。
FastText中还提到了subword,他讲英文中word也拆成不同的subword进行考虑,因为英文中的前缀和后缀都是有意义的,但是在中文中只能作为以字为单位进行考虑了。
pytorch版的代码:
代码已经上传至Github
我们通过Embedding层将单词映射到稠密空间,
tensor
的维度是(batchsize
,length_of_sentence
,embedding_dim
),再经过平均操作之后,
tensor
的维度变为(batchsize
,embedding_dim
),因为我们是对句子中的所有单词做平均,这时候的向量或者下一层的向量可以表示整个句子,我们可以用这些向量来做句子相似度的实验。
输出层则是分层softmax,tensor
的维度变为(batchsize
,class_num
)
我们针对这次的匿名数据集,使用torchtext
进行数据的预处理,构造了迭代器,注意里的'./data/train_torch.csv'
是被我重新保存过的csv,第一列是text
,第二列是label
。
class MyDataset:
def __init__(self, train_path='./data/train_torch.csv', test_path='./data/test_a.csv', fix_length=600):
self.train_path = train_path
self.test_path = test_path
self.fix_length = fix_length
def get_data_by_torchtext(self):
print("读取数据,需要花挺长时间")
def x_tokenize(x):
return [w for w in x.split()]
def y_tokenize(y):
return int(y)
TEXT = Field(sequential=True, tokenize=x_tokenize,
fix_length=self.fix_length, use_vocab=True,
init_token=None, eos_token=None,
include_lengths=True, batch_first=True)
LABEL = Field(sequential=False, tokenize=y_tokenize,
use_vocab=False, is_target=True)
fields_train = [('text', TEXT), ('label', LABEL)]
fields_test = [('text', TEXT)]
train = TabularDataset(
path=self.train_path, format='csv',
skip_header=True, fields=fields_train
)
test = TabularDataset(
path=self.test_path, format='csv',
skip_header=True, fields=fields_test
)
TEXT.build_vocab(train)
return TEXT.vocab, train, test
def split(self, train, split_ratio=0.9):
train_dataset, valid_dataset = train.split(split_ratio, stratified=True)
return train_dataset, valid_dataset
def get_iter(self, train, valid, test, train_batch=64, valid_batch=64, test_batch=128):
# 构造迭代器
train_iter, valid_iter = BucketIterator.splits(
(train, valid),
batch_sizes=(train_batch, valid_batch),
device=torch.device("cuda"),
sort_key=lambda x: len(x.text),
sort_within_batch=True
)
test_iter = Iterator(test, batch_size=test_batch, device=torch.device("cuda"), sort=False, repeat=False,
sort_within_batch=False,shuffle=False)
return train_iter, valid_iter, test_iter
我们搭建了一个最简单的FastText的模型,但它不包含ngram和分层softmax。
class Fasttext(nn.Module):
def __init__(self, vocab, embedding_dim=300,hidden_dim=64,out_dim=14):
super(Fasttext, self).__init__()
self.num_vocab=len(vocab)
self.embedding_dim=embedding_dim
self.hidden_dim=hidden_dim
self.out_dim=out_dim
self.embed = nn.Embedding(num_embeddings=self.num_vocab,embedding_dim=self.embedding_dim)
self.embed.weight.requires_grad = True
self.pred = nn.Sequential(
nn.Linear(self.embedding_dim, self.hidden_dim),
nn.Dropout(0.5),
nn.BatchNorm1d(self.hidden_dim),
nn.ReLU(inplace=True),
nn.Linear(self.hidden_dim, self.out_dim),
# nn.ReLU(inplace=True),
)
nn.init.xavier_uniform_(self.embed.weight)
self.xavier(self.pred)
def xavier(self,layers):
for index,net in enumerate(layers):
if index==0:
nn.init.xavier_uniform_(net.weight)
def forward(self, x):
x = self.embed(x)
out = self.pred(torch.mean(x, dim=1))
return out
这里所谓的句子向量即为 embedding 层之后的torch.mean(x, dim=1)
。
我们还构造了训练,验证,预测的整个迭代过程。
class TrainFunc():
def __init__(self, model, train_iter=None, valid_iter=None, test_iter=None):
self.model = model
self.best_model = model
self.best_score = 0
self.train_iter = train_iter
self.valid_iter = valid_iter
self.test_iter = test_iter
def train(self, epoch):
self.model.train()
for i in range(epoch):
train_acc = 0
train_loss = 0
for batch_idx, batch in enumerate(iter(self.train_iter)):
data, label = batch.text[0], batch.label
batchsize = data.shape[0]
output = self.model(data)
opt.zero_grad()
loss = criterion(output, label)
loss.backward()
opt.step()
train_loss += loss.item()
train_acc += (output.argmax(1) == label).sum().item()
if batch_idx % int(200 * (64 / batchsize)) == 0:
print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format(
i + 1, batch_idx * len(data), len(self.train_iter.dataset),
100. * batch_idx / len(self.train_iter), loss.item()))
# assert False
print(
f'\tLoss: {train_loss / len(self.train_iter):.4f}(train)\t|\tAcc: {train_acc / len(self.train_iter.dataset) * 100:.1f}%(train)')
score = self.valid_func()
if score > self.best_score:
self.best_score = score
self.best_model = deepcopy(self.model)
print("Now_best:{:.4f}".format(self.best_score))
# scheduler.step()
return self.best_model
def valid_func(self):
valid_acc = 0
valid_loss = 0
ans_box = []
label_box = []
self.model.eval()
with torch.no_grad():
for batch_idx, batch in enumerate(iter(self.valid_iter)):
data, label = batch.text[0], batch.label
output = self.model(data)
pred = output.argmax(1)
loss = criterion(output, label)
ans_box.extend(pred.cpu().tolist())
label_box.extend(label.cpu().tolist())
valid_loss += loss.item()
valid_acc += (pred == label).sum().item()
score1 = f1_score(ans_box, label_box, average='macro')
score2 = f1_score(ans_box, label_box, average='micro')
print(
f'\tLoss: {valid_loss / len(self.valid_iter):.4f}(valid)\t|\tAcc: {valid_acc / len(self.valid_iter.dataset) * 100:.1f}%(valid)')
print(f'\tMicro: {score2:.4f}(valid)\t|\tMacro: {score1:.4f}(valid)')
self.model.train()
return score1
def predict(self):
self.best_model.eval()
ans_box = []
with torch.no_grad():
for batch_idx, batch in enumerate(iter(self.test_iter)):
data = batch.text[0]
output = self.best_model(data)
pred = output.argmax(1)
ans_box.extend(pred.cpu().tolist())
return ans_box
我们给出一个参考的跑程序过程:
# 构造迭代器
my_data=MyDataset(train_path='./data/train_torch.csv',test_path='./data/test_a.csv',fix_length=600)
vocab,train,test=my_data.get_data_by_torchtext()
train_dataset, valid_dataset=my_data.split(train)
train_iter, valid_iter,test_iter=my_data.get_iter(train_dataset, valid_dataset,test,train_batch=64)
# 构造模型的参数
model=Fasttext(vocab)
model=model.cuda()
criterion = nn.NLLLoss()
lr = 1e-4
opt = torch.optim.Adam(model.parameters(), lr)
# 开始训练,使用验证集获得做好的参数,并进行预测
mytrain=TrainFunc(model,train_iter, valid_iter,test_iter)
best_model=mytrain.train(15)
ans=mytrain.predict() # 因为没有打乱过,所以这个ans是按顺序的。
df_sub=pd.read_csv(data_root["sub_path"])
df_sub.label=np.array(ans)
df_sub.to_csv('./fasttext_submission.csv', index=False)
官方开源的FastTex使用:
FastText可以快速的在CPU上进行训练,最好的实践方法就是官方开源的版本。
既然我们自己的实验搭建那么多代码,那我们尝试一下官方的FastText版本的愉快版本吧!
其实操作起来非常简单,分为两步,保存成为需要的格式,进行训练得到模型即可。
# 保存格式
train_df = pd.read_csv(data_root["train_path"])
train_df['label_ft'] = '__label__' + train_df['label'].astype(str)
train_df[['text','label_ft']].iloc[:200000].to_csv('./data/train_for_fast.csv', index=None, header=None, sep='\t')
# 训练
model = fasttext.train_supervised(data_root["fast_path"],lr=1.0,
dim=300,wordNgrams=2, minn=4,
minCount=2, epoch=25, loss="hs",thread=16)
# 预测
val_pred = [model.predict(x)[0][0].split('__')[-1] for x in test_df.text.values]
关于train_supervised
中的参数可以参考官方文档的说明
这里给出我定义的参数的意义:
- input: 训练数据文件路径
- lr: 学习率
- dim: 字向量维度
- minn: 构造subword时最小char个数
- epoch: 迭代次数
- loss: 损失函数类型, softmax, ns: 负采样, hs: 分层softmax
- minCountLabel: 类别阈值,类别小于该值初始化时会过滤掉
返回的是一个分类器,可以通过下面这些方法或者属性来进行查看你需要的东西:
- model.words
- model.labels
- model[‘king’]
- model.predict ()
本章作业
1.阅读FastText的文档,尝试修改参数,得到更好的分数
使用官方开源的FastText程序,在线上获得0.917的成绩。
2.基于验证集的结果调整超参数,使得模型性能更优
使用自己编写的FastText程序(pytorch),使用验证集进行调参,在线上获得了0.904的成绩。
于7月28日更改了损失函数,logsoftmax 之后应该接 NLLLoss(原来的CrossEntropy 我思考之后应该是错的),但是再跑一遍提交就算了。