CNN+pytorch實現文字二分類

LuKaiNotFound發表於2021-07-07

引言

最近學習了卷積神經網路,想上手一個小專案實踐一下,該專案的資料集來自於github,內容為汽車售後正負面評價,藉助pytorch實現對模型的訓練並完成test集中對於某條評價的二分類。

原理:利用卷積提取區域性特徵的特性,捕捉類似於N-gram的關鍵資訊。

1.資料的預處理

在自然語言處理中,不可避開的話題就是詞向量,我藉助的是torchtext這個工具庫來實現詞向量的構建

分詞器

def tokenizer(text): # create a tokenizer function
    regex = re.compile(r'[^\u4e00-\u9fa5aA-Za-z0-9]')
    text = regex.sub(' ', text)
    return [word for word in jieba.cut(text) if word.strip()]
複製程式碼

分詞器藉助中文分詞工具jieba庫進行分詞,將分完的詞以列表形式返回。

去停用詞

def get_stop_words():
    file_object = open('D:\\MyStudy\\program\\text-classification-master\\text-cnn\\data\\stopwords.txt',encoding='UTF-8')
    stop_words = []
    for line in file_object.readlines():
        line = line[:-1]
        line = line.strip()
        stop_words.append(line)
    return stop_words
複製程式碼

事先下載停用詞表,將處理好之後停用詞以列表形式返回。

資料處理

def load_data(args):
    print('載入資料中...')
    stop_words = get_stop_words() # 載入停用詞表
    '''
    如果需要設定文字的長度,則設定fix_length,否則torchtext自動將文字長度處理為最大樣本長度
    text = data.Field(sequential=True, tokenize=tokenizer, fix_length=args.max_len, stop_words=stop_words)
    '''
    text = data.Field(sequential=True, lower=True, tokenize=tokenizer, stop_words=stop_words)
    label = data.Field(sequential=False)

    text.tokenize = tokenizer
    train, val = data.TabularDataset.splits(
            path='D:\\MyStudy\\program\\text-classification-master\\text-cnn\\data\\',
            skip_header=True,
            train='train.tsv',
            validation='validation.tsv',
            format='tsv',
            fields=[('index', None), ('label', label), ('text', text)],
        )

    if args.static:
        text.build_vocab(train, val, vectors=Vectors(name="data\\eco_article.vector")) # 此處改為你自己的詞向量
        args.embedding_dim = text.vocab.vectors.size()[-1]
        args.vectors = text.vocab.vectors

    else: text.build_vocab(train, val)

    label.build_vocab(train, val)

    train_iter, val_iter = data.Iterator.splits(
            (train, val),
            sort_key=lambda x: len(x.text),
            batch_sizes=(args.batch_size, len(val)), # 訓練集設定batch_size,驗證集整個集合用於測試
            device=-1
    )
    args.vocab_size = len(text.vocab)
    args.label_num = len(label.vocab)
    return train_iter, val_iter

複製程式碼

torchtext的使用步驟一般為: 1,利用data.Field()定義一個物件,並預設定引數,此處對text和label分別定義。 2,用 data.TabularDataset().spilts()來讀取檔案,得到train,val兩個部分。 3,構建詞向量,利用text.build_vocab(trian,val),label.build_vocab(trian,val)構建訓練文字和標籤的詞向量。 4,利用data.Iterator.splits()生成bacth。

自此,資料的預處理完成。

2.模型的建立

採用CNN架構。藉助pytorch. 總體網路架構是:嵌入層、維度處理、卷積層、啟用函式、池化層、多通道特徵提取,Dropout層,全連線層。

嵌入層

將構建的詞向量進行嵌入操作,嵌入層的引數有詞向量大小嵌入維度

卷積層

將嵌入層的輸出維度變換為適應卷積層輸入的維度,並用self.convs=nn.MoudleList(nn.conv2() for fsz in filter_sizes)將三個通道並行的卷積層儲存其中,返回一個卷積層的列表。

啟用函式

x = F.relu(conv(x) for conv in self.convs)植入非線性

池化,下采樣

多通道的特徵提取與合併

x = [x_item.view(x_item.size(0), -1) for x_item in x]將不同卷積核運算結果維度展平。

Dropout防止過擬合

全連線層輸出

模型建立部分程式碼如下

class TextCNN(nn.Module):
    # 多通道textcnn
    def __init__(self, args):
        super(TextCNN, self).__init__()
        self.args = args

        label_num = args.label_num # 標籤的個數
        filter_num = args.filter_num # 卷積核的個數
        filter_sizes = [int(fsz) for fsz in args.filter_sizes.split(',')]
        vocab_size = args.vocab_size
        embedding_dim = args.embedding_dim

        self.embedding = nn.Embedding(vocab_size, embedding_dim)
        if args.static: # 如果使用預訓練詞向量,則提前載入,當不需要微調時設定freeze為True
            self.embedding = self.embedding.from_pretrained(args.vectors, freeze=not args.fine_tune)

        self.convs = nn.ModuleList(
            [nn.Conv2d(1, filter_num, (fsz, embedding_dim)) for fsz in filter_sizes])
        self.dropout = nn.Dropout(args.dropout)
        self.linear = nn.Linear(len(filter_sizes)*filter_num, label_num)

    def forward(self, x):
        # 輸入x的維度為(batch_size, max_len), max_len可以通過torchtext設定或自動獲取為訓練樣本的最大=長度
        x = self.embedding(x) # 經過embedding,x的維度為(batch_size, max_len, embedding_dim)

        # 經過view函式x的維度變為(batch_size, input_chanel=1, w=max_len, h=embedding_dim)
        x = x.view(x.size(0), 1, x.size(1), self.args.embedding_dim)

        # 經過卷積運算,x中每個運算結果維度為(batch_size, out_chanel, w, h=1)
        x = [F.relu(conv(x)) for conv in self.convs]

        # 經過最大池化層,維度變為(batch_size, out_chanel, w=1, h=1)
        x = [F.max_pool2d(input=x_item, kernel_size=(x_item.size(2), x_item.size(3))) for x_item in x]

        # 將不同卷積核運算結果維度(batch,out_chanel,w,h=1)展平為(batch, outchanel*w*h)
        x = [x_item.view(x_item.size(0), -1) for x_item in x]

        # 將不同卷積核提取的特徵組合起來,維度變為(batch, sum:outchanel*w*h)
        x = torch.cat(x, 1)

        # dropout層
        x = self.dropout(x)

        # 全連線層
        logits = self.linear(x)
        return logits
複製程式碼

3,模型的訓練與優化

建立模型之後進入訓練過程,先進行超引數的設定。

parser = argparse.ArgumentParser(description='TextCNN text classifier')

parser.add_argument('-lr', type=float, default=0.001, help='學習率')
parser.add_argument('-batch-size', type=int, default=128)
parser.add_argument('-epoch', type=int, default=20)
parser.add_argument('-filter-num', type=int, default=200, help='卷積核的個數')
parser.add_argument('-filter-sizes', type=str, default='6,7,8', help='不同卷積核大小')
parser.add_argument('-embedding-dim', type=int, default=128, help='詞向量的維度')
parser.add_argument('-dropout', type=float, default=0.4)
parser.add_argument('-label-num', type=int, default=2, help='標籤個數')
parser.add_argument('-static', type=bool, default=False, help='是否使用預訓練詞向量')
parser.add_argument('-fine-tune', type=bool, default=True, help='預訓練詞向量是否要微調')
parser.add_argument('-cuda', type=bool, default=False)
parser.add_argument('-log-interval', type=int, default=1, help='經過多少iteration記錄一次訓練狀態')
parser.add_argument('-test-interval', type=int, default=100,help='經過多少iteration對驗證集進行測試')
parser.add_argument('-early-stopping', type=int, default=1000, help='早停時迭代的次數')
parser.add_argument('-save-best', type=bool, default=True, help='當得到更好的準確度是否要儲存')
parser.add_argument('-save-dir', type=str, default='model_dir', help='儲存訓練模型位置')

複製程式碼
def train(args):
    train_iter, dev_iter = data_processor.load_data(args) # 將資料分為訓練集和驗證集
    print('載入資料完成')
    model = TextCNN(args)
    if args.cuda: model.cuda()
    optimizer = torch.optim.Adam(model.parameters(), lr=args.lr)
    steps = 0
    best_acc = 0
    last_step = 0
    model.train()
    for epoch in range(1, args.epoch + 1):
        for batch in train_iter:
            feature, target = batch.text, batch.label
            # t_()函式表示將(max_len, batch_size)轉置為(batch_size, max_len)
            with torch.no_grad():
                feature.t_()
                target.sub_(1)
            if args.cuda:
                feature, target = feature.cuda(), target.cuda()
            optimizer.zero_grad()
            logits = model(feature)
            loss = F.cross_entropy(logits, target)
            loss.backward()
            optimizer.step()
            steps += 1
            if steps % args.log_interval == 0:
                # torch.max(logits, 1)函式:返回每一行中最大值的那個元素,且返回其索引(返回最大元素在這一行的列索引)
                corrects = (torch.max(logits, 1)[1] == target).sum()
                train_acc = 100.0 * corrects / batch.batch_size
                sys.stdout.write(
                    '\rBatch[{}] - loss: {:.6f}  acc: {:.4f}%({}/{})'.format(steps,
                                                                             loss.item(),
                                                                             train_acc,
                                                                             corrects,
                                                                             batch.batch_size))
            if steps % args.test_interval == 0:
                dev_acc = eval(dev_iter, model, args)
                if dev_acc > best_acc:
                    best_acc = dev_acc
                    last_step = steps
                    if args.save_best:
                        print('Saving best model, acc: {:.4f}%\n'.format(best_acc))
                        save(model, args.save_dir, 'best', steps)
                else:
                    if steps - last_step >= args.early_stopping:
                        print('\nearly stop by {} steps, acc: {:.4f}%'.format(args.early_stopping, best_acc))
                        raise KeyboardInterrupt

複製程式碼

訓練過程首先將模型例項化model,然後定義優化器,我採用的是Adam優化器,然後就是pytorch訓練的基本操作

for epoch in eopch_num:
	for bacth in batches:
		optimizer.zero_grad()#梯度清零
		logits = model(feature)
		loss = F.cross_entropy(logits,targets)#交叉熵函式
		loss.backward()#反向傳播
		optimizer.step()
		step + =1
複製程式碼

驗證集的測試過程同訓練過程相似

def eval(data_iter, model, args):
    corrects, avg_loss = 0, 0
    for batch in data_iter:
        feature, target = batch.text, batch.label
        with torch.no_grad():
            feature.t_()
            target.sub_(1)
        if args.cuda:
            feature, target = feature.cuda(), target.cuda()
        logits = model(feature)
        loss = F.cross_entropy(logits, target)
        avg_loss += loss.item()
        corrects += (torch.max(logits, 1)
                     [1].view(target.size()) == target).sum()
    size = len(data_iter.dataset)
    avg_loss /= size
    accuracy = 100.0 * corrects / size
    print('\nEvaluation - loss: {:.6f}  acc: {:.4f}%({}/{}) \n'.format(avg_loss,
                                                                       accuracy,
                                                                       corrects,
                                                                       size))
    return accuracy

def save(model, save_dir, save_prefix, steps):
    if not os.path.isdir(save_dir):
        os.makedirs(save_dir)
    save_prefix = os.path.join(save_dir, save_prefix)
    save_path = '{}_steps_{}.pt'.format(save_prefix, steps)
    torch.save(model.state_dict(), save_path)

train(args)


複製程式碼

訓練完畢後驗證集正確率可達90% 程式碼參考來自連結

相關文章