文本向量化前言

 
pip install jieba
    

 


文本与向量

 
n个 数 排列一起形成的一组数就是向量
向量有两个核心要素:大小,方向 

多个维数相同的向量放在一起就是矩阵 

线性代数的基本思想是什么? 是用矩阵去描述解决问题
虽然线性代数全是在讲矩阵,但若说矩阵就是线性代数就显得狭隘了,

思维再打开一些,所有的知识都是在描述这个世界的规律,
就像字符类语言可以描述解决问题一样,由数字组成的矩阵也可以 

更重要的是,计算机中全是数字,我们要用计算机,就得把事物转成数字,
不管它是具体的还是抽象的,是静态的,还是运行变化的

而向量使用数字组来描述事物,向量也是矩阵的组成,或者说是基本单位
同时,单个向量也叫列矩阵 / 行矩阵 

所以,文本向量化,实际上是文本矩阵化
一个单词对应一个数字 
一个句子对应一个向量
多个句子对应一个矩阵

 

    

 


 

  

 


文本向量化技巧

文本向量化大方向

 
文本向量化,实际上是文本矩阵化
多个句子对应一个矩阵,即用一段文本用矩阵表示,这一点是确定的

有可灵活设计的地方在于:
一个单词对应一个数字 
单个向量可以表示单个事物,那么就可以使用向量表示单词 
并且单词个数多,含义也有差异,我们希望表示单词的向量也能体现这种差异
至于怎么体现,就是八仙过海,各显神通,意思就是看你算法怎么设计


 
是用一个数字表示一个单词,还是用向量表示单词以及怎么用向量表示单词,
是文本向量化算法的角逐的点

就像汉语可以翻译成外语一样,也能翻译成矩阵,
只是现在还没有哪家公司用矩阵组建一套国际通用语法结构

如果用一个向量表示一个单词,那么一个句子对应一个2维矩阵
    

 

    

汉语中的字与词

 
汉字约3000多个,而单词的量级在万级,几十万... 
一个字对应一个向量,经实践证明在某些场合,效果并不比一个单词对应一个向量效果差

一句话或一段文本
[1, seq_len, 单词的维度]

多段文本,即一个批次的文本
[Batch_size, seq_len, 单词的维度]

单词的维度也是AI中的特征维度,feature dim 
而序列长度,则是AI中的特征个数feature nums (本网站叫feature shape)

单个样本就是一句话[1, seq_len, 单词的维度],或者[seq_len, 单词的维度]

 

    

word2index单词转索引

 
import jieba
ss = "团结一切可以团结的力量"
sentence = jieba.lcut(ss)
print(sentence)
"""
['团结', '一切', '可以', '团结', '的', '力量']
"""

word2id = {word:index for index,word in enumerate(set(sentence))}
word2id["UNK"] = len(word2id)
word2id["PAD"] = len(word2id)  # 补长度 是为了批次处理

print(word2id)
"""
{'可以': 0, '团结': 1, '一切': 2, '力量': 3, '的': 4, 'UNK': 5, 'PAD': 6}
"""

"""
字典长度,向量长度为7,即使每个元素限定取0或1 -- one hot
"""
print(2**7)  # 128
print(2**3)  # 8

代码分析
---------------------------------------------
词典,key:汉字,value:索引编号
为每个单词编号,文本中所有不重复的单词及编号形成一个词典

在预测过程中遇到的新单词转为UNK

一句话的单词个数不定,而在批次处理中,要求向量维度一致,
此时向量维度通常选句子长度的平均值,多的舍去,不足补为PAD

使用索引表示向量仅仅是给不同的单词一个不同的编号,
如果使用one hot编码,则向量长度为7,每个向量中索引所在位置表示单词,
向量的两个核心要素:大小与方向,

 
one hot编码,向量的模为1(1是一个非常特殊的值),所以,单词之间的区别就剩下方向了,
如果单词个数少,这种表示法就是完美
如果单词个数多,这种表示法会占用太多的空间,
单词个数一旦过万,那么向量维度就过万,
计算机算不过来,解决方式就是降维

依然使用0与1二进制来区分单词,7个位数的二进制可表示128个不同的单词,
这里的需求是7个单词,使用3位二进制就可以了
如此,向量的维数从7降为3
2**20=1048576,20位二进制可表示100万不同的单词,向量位数为20在计算机中并不算长

 

  

torch nn.Embedding

 
单词转索引后,可以由Embedding转为指定维度的向量
向量维度变小,同时,还要一个向量唯一表示一个单词

import jieba
ss = "团结一切可以团结的力量"
sentence = jieba.lcut(ss)
word2id = {word:index for index,word in enumerate(set(sentence))}
word2id["UNK"] = len(word2id)
word2id["PAD"] = len(word2id)  # 补长度 是为了批次处理

dict_len = len(word2id)

 

import torch
from torch import nn
embed = nn.Embedding(num_embeddings=dict_len, embedding_dim=3, padding_idx=word2id["PAD"])   # __call__

sentence_index = [word2id[word] for word in sentence]
print(sentence_index)
"""
[0, 3, 1, 0, 2, 4]
对应的单词列表
['团结', '一切', '可以', '团结', '的', '力量']
"""
sentence_index = torch.Tensor(sentence_index).long()
sen_vec = embed(sentence_index)
print(sen_vec)
"""
tensor([[-0.1450,  0.2220, -0.2267],
        [-0.5169, -1.7362, -0.2435],
        [ 0.4777,  0.2449,  0.0158],
        [-0.1450,  0.2220, -0.2267],
        [-0.8421,  0.0353, -0.3633],
        [ 1.9878,  0.9668,  0.1843]], grad_fn=EmbeddingBackward0)
"""

embed的结果是随机的,通常放入神经网络起训练,损失函数梯度下降时会优化参数

  

 


index2word

 
import jieba
ss = "团结一切可以团结的力量"
sentence = jieba.lcut(ss)

word2idx = {}
word2idx["PAD"] = len(word2idx)  # 补长度 是为了批次处理
word2idx["UNK"] = len(word2idx)  
word2idx.update({word:(index+2) for index,word in enumerate(set(sentence))})
idx2word = {index:word for word,index in word2idx.items()}

idx2word
{0: 'PAD', 1: 'UNK', 2: '的', 3: '团结', 4: '一切', 5: '力量', 6: '可以'}

 


单词转one hot向量

np.eye单位矩阵

 
import numpy as np 
np.eye(3)
array([[1., 0., 0.],
       [0., 1., 0.],
       [0., 0., 1.]])

a=np.eye(3)
a[0]
array([1., 0., 0.])

单位矩阵是方阵,每个向量中只有一个元素为1,其他为0,并且行向量与列向量相等
这与One Hot 向量恰好一致,故可以使用np.eye来打one hot标签 
import numpy as np 
import jieba
ss = "团结一切可以团结的力量"
sentence = jieba.lcut(ss)

word2idx = {}
word2idx["PAD"] = len(word2idx)  # 补长度 是为了批次处理
word2idx["UNK"] = len(word2idx)  
word2idx.update({word:(index+2) for index,word in enumerate(set(sentence))})
print(word2idx) # {'PAD': 0, 'UNK': 1, '一切': 2, '力量': 3, '团结': 4, '的': 5, '可以': 6}

def one_hot_vec(word2idx, word):
    template_vec = np.eye(len(word2idx))
    return template_vec[word2idx[word]]

one_hot_vec(word2idx=word2idx,word="团结")  # array([0., 0., 0., 0., 1., 0., 0.])

情感识别

酒店评估

 
原数据集是一段话一个文本,数万个小文件,太占空间,压缩存放以下目录
/wks/datasets/hotel_reader/data_pkl
    

原始数据集:已做单词切片,未转索引,是单词列表

 
import os 
from tpf import pkl_save,pkl_load 
BASE_DIR = "/wks/datasets/hotel_reader"
file_path = os.path.join(BASE_DIR,'data_pkl/word.pkl')
pkl_save((X_train,y_train,X_test,y_test,words_set,word2idx,idx2word),file_path=file_path)

加载

 
import os 
from tpf import pkl_save,pkl_load 
BASE_DIR = "/wks/datasets/hotel_reader"
file_path = os.path.join(BASE_DIR,'data_pkl/word.pkl')
X_train,y_train,X_test,y_test,words_set,word2idx,idx2word = pkl_load(file_path)
    

查看,不定长单词列表

 
s = ""
for i in X_train[0]:
    s += " " + i
s
' 房间 还 可以 , 早餐 不大好 , 周围 比较 偏僻 。 服务态度 一般 。'


 
s = ""
for i in X_train[1]:
    s += " " + i
s
' 这次 入住 感到 服务 人员 的 工作 态度 不如 以前 , 整体 工作 热情 也 不象 其他 喜来登 那么 专业 。 因为 我定 的 房间 满 了 于是 给 我 升级 到 行政 套房 , 这个 本来 是 不错 的 安排 , 但是 不 知道 为什么 偏偏 这个 房间 里 早晚 都 有 一股 股 噪音 ; 而 我 朋友 住 在 我 下面 一层 , 一清早 就 被 行政 酒廊 准备 早餐 的 声音 吵醒 。 酒店 2 楼 西餐厅 服务 还是 很 好 的 。'

第二版:one-hot编码

 
import os 
import numpy as np
import torch
from torch import nn
from torch.nn import functional as F
from torch.utils.data import Dataset
from torch.utils.data import DataLoader
from tpf import pkl_save,pkl_load 

class MyDataSet(Dataset):
    """
    构建数据集
    """
    def __init__(self, X, y):
        self.X = X
        self.y = y
    
    def __len__(self):
        return len(self.X)
    
    def __getitem__(self, idx):
        x = self.X[idx]
        y = self.y[idx]
        
        return torch.tensor(data=x).long(), torch.tensor(data=y).long()


BASE_DIR = os.path.dirname(os.path.abspath(__file__))
BASE_DIR = "/wks/datasets/hotel_reader"

"""
读取原始数据
"""
dict_file = os.path.join(BASE_DIR,"data_pkl/wordict.pkl")
file_train = os.path.join(BASE_DIR,"data_pkl/train.pkl")
file_test = os.path.join(BASE_DIR, "data_pkl/test.pkl")

words_set,word2idx = pkl_load(file_path=dict_file)   # 字典
X_train,y_train = pkl_load(file_path=file_train)     # 训练集
X_test,y_test = pkl_load(file_path=file_test)        # 测试集

    
    

 

    

 

    

第一版:对接深度学习

 

import os 
import numpy as np
import torch
from torch import nn
from torch.nn import functional as F
from torch.utils.data import Dataset
from torch.utils.data import DataLoader
from tpf import pkl_save,pkl_load 

class MyDataSet(Dataset):
    """
    构建数据集
    """
    def __init__(self, X, y):
        self.X = X
        self.y = y
    
    def __len__(self):
        return len(self.X)
    
    def __getitem__(self, idx):
        x = self.X[idx]
        y = self.y[idx]
        
        return torch.tensor(data=x).long(), torch.tensor(data=y).long()


BASE_DIR = os.path.dirname(os.path.abspath(__file__))
BASE_DIR = "/wks/datasets/hotel_reader"

"""
读取原始数据
"""
dict_file = os.path.join(BASE_DIR,"data_pkl/wordict.pkl")
file_train = os.path.join(BASE_DIR,"data_pkl/train.pkl")
file_test = os.path.join(BASE_DIR, "data_pkl/test.pkl")

words_set,word2idx = pkl_load(file_path=dict_file)   # 字典
X_train,y_train = pkl_load(file_path=file_train)     # 训练集
X_test,y_test = pkl_load(file_path=file_test)        # 测试集
    
    

 
train_dataset = MyDataSet(X=X_train, y=y_train)
test_dataset = MyDataSet(X=X_test,y=y_test)

print(train_dataset[0],len(train_dataset))
print("test len=",len(test_dataset))
print(f"words_set len={len(words_set)}")

 
$ python read_test2.py 
(tensor([ 6426, 21219, 13836, 12268,  6804, 11523, 12268, 12177, 19919,  3728,
            3689,  7275, 11758,  3689,     0,     0,     0,     0,     0,     0,
            0,     0,     0,     0,     0,     0,     0,     0,     0,     0,
            0,     0,     0,     0,     0,     0,     0,     0,     0,     0,
            0,     0,     0,     0,     0,     0,     0,     0,     0,     0,
            0,     0,     0,     0,     0,     0,     0,     0,     0,     0,
            0,     0,     0,     0,     0,     0,     0,     0,     0,     0,
            0,     0,     0,     0,     0,     0,     0,     0,     0,     0,
            0,     0,     0,     0,     0]), tensor(1)) 4800
test len= 1200
words_set len=21437


 
import numpy as np
from sklearn.model_selection import train_test_split
import os
import jieba
    

读取原始数据

 
train_file = "./train.csv"
test_file = "./test.csv"

# 训练集文本和标签
X_train = []
y_train = []

# 构建词典
# UNK:未知词
# PAD:填充词
words_set = {"", ""}

# 训练集读取
with open(file=train_file, mode="r", encoding="utf8") as f:
    for line in f.readlines():
        file_name, label = line.strip().split(",")
        y_train.append(int(label))
        with open(file=file_name, mode="r", encoding="gbk", errors="ignore") as f1:
            txt = f1.read().replace(" ", "").replace("\n", "").replace("\t", "")
            txt_cut = jieba.lcut(txt)
            words_set = words_set.union(set(txt_cut))
            X_train.append(txt_cut)


# 测试集文本和标签
X_test = []
y_test = []

# 测试集读取
with open(file=test_file, mode="r", encoding="utf8") as f:
    for line in f.readlines():
        file_name, label = line.strip().split(",")
        y_test.append(int(label))
        with open(file=file_name, mode="r", encoding="gbk", errors="ignore") as f1:
            txt = f1.read().replace(" ", "").replace("\n", "").replace("\t", "")
            txt_cut = jieba.lcut(txt)
            X_test.append(txt_cut)
    

 
# words_list = list(words_set)
word2idx = {word:idx for idx, word in enumerate(words_set)}
idx2word = {idx:word for idx, word in enumerate(words_set)}
    

 
# 字典长度
dict_len = len(words_set)  #21437
    

文本向量化·索引编码

 
# 训练集的向量化
X_train1 = []

for x in X_train:
    # 临时编码向量
    temp = [0] * dict_len
    for word in x:
        word_idx = word2idx[word] if word in words_set else word2idx[""]
        temp[word_idx] += 1
    X_train1.append(temp)


# 测试集的向量化
X_test1 = []

for x in X_test:
    # 临时编码向量
    temp = [0] * dict_len
    for word in x:
        word_idx = word2idx[word] if word in words_set else word2idx[""]
        temp[word_idx] = 1
    X_test1.append(temp)
    

数据转 NumPy向量

 

X_train1 = np.array(X_train1)
y_train1 = np.array(y_train)

X_test1 = np.array(X_test1)
y_test1 = np.array(y_test)
    

保存

 
from tpf import pkl_save,pkl_load 
BASE_DIR = "/wks/datasets/hotel_reader"
dict_file = os.path.join(BASE_DIR,"data_pkl/wordict_2.pkl")
file_train = os.path.join(BASE_DIR,"data_pkl/train_2.pkl")
file_test = os.path.join(BASE_DIR, "data_pkl/test_2.pkl")
pkl_save((words_set,word2idx),dict_file)
pkl_save((X_train1,y_train1),file_train)
pkl_save((X_test1,y_test1),file_test)
    

 

    

 

    

逻辑回归

 

import os 
import numpy as np
from tpf import pkl_save,pkl_load 


BASE_DIR = os.path.dirname(os.path.abspath(__file__))
BASE_DIR = "/wks/datasets/hotel_reader"

"""
读取原始数据
"""
dict_file = os.path.join(BASE_DIR,"data_pkl/wordict_2.pkl")
file_train = os.path.join(BASE_DIR,"data_pkl/train_2.pkl")
file_test = os.path.join(BASE_DIR, "data_pkl/test_2.pkl")

words_set,word2idx = pkl_load(file_path=dict_file)   # 字典
X_train,y_train = pkl_load(file_path=file_train)     # 训练集
X_test,y_test = pkl_load(file_path=file_test)        # 测试集


X_train1 = np.array(X_train)
y_train1 = np.array(y_train)

X_test1 = np.array(X_test)
y_test1 = np.array(y_test)

from sklearn.linear_model import LogisticRegression
lr = LogisticRegression(max_iter=10000)
lr.fit(X=X_train1, y=y_train1)
print(lr.score(X=X_test1, y=y_test1))

 
0.8933333333333333

 
然而,若是将数据集更换为最早之前保存的版本的话,精度下降明显 
$ python alg_lr.py 
0.6075

个人怀疑是数据集并不充分,按不同的方式split时,会导致学习到的规律出现大的波动
- 版本2的数据集是按指定的excel处理的,训练集与测试集的划分是提前分好的

然而,经过确认,下面的数值完全一致
print(len(y_test1),y_test1[:7])
1200 [1 0 1 0 0 1 1]

数据一致,那么不同的就是编码方式了 
- 也有可能是去除空格的原因
- 也有可能是数据不同了,虽然它们标签一致

idx2word = {idx:word for idx, word in enumerate(words_set)}
for i in X_train1[0]:
    if i > 0:
        print(idx2word[i])



 
第一版数据  
专业
市委
疏漏
单床
游戏规则
黄同金
单床
章
虫
本周
总台
下楼
散客
总台

print(len(X_train1[0]))
85

第二版数据 
大开
大开
大开
手势
大开
大开
大开
大开
大开
手势
大开
大开

len(X_train1[0])  # 21437

 
至此可以看出,最大的不同是向量的设计,第二版向量被设计为字典长度 

"""
    文本向量化
    Count 计算法
"""
# 训练集的向量化
X_train1 = []

for x in X_train:
    # 临时编码向量
    temp = [0] * dict_len
    for word in x:
        word_idx = word2idx[word] if word in words_set else word2idx[""]
        temp[word_idx] += 1
    X_train1.append(temp)

  

 
temp = [0] * dict_len
X_train1.append(temp)
  
第1版的向量只有85的长度,肯定是截断了
第2版,何止是没有截断,这长度都让人无语了,但效果
>>> 0.8933-0.6075
0.28579999999999994
提升了将近30% 

 

  

 

  

 
数据集在使用one-hot编码时,即使使用逻辑回归,也有不错的精度 
这种方法在卷积网络中,很多时间不会动,计算不动

在个人计算机上,不得不采用索引编码再转向量的方式
  

 

  

 

  

 

  

 


情感识别·卷积·1维

 
文本处理,若使用onehot编码,效果可能会好,但向量长度过长,个人电脑算不动 

因此这里使用索引编码的方式作为输入,由模型进行embedding 转向量

需要维度好字典信息 
    

 
#加载数据集
import os 
from tpf import pkl_save,pkl_load 
BASE_DIR = "/wks/datasets/hotel_reader"
file_path = os.path.join(BASE_DIR,'data_pkl/word.pkl')
X_train,y_train,X_test,y_test,words_set,word2idx,idx2word = pkl_load(file_path)

# 字典长度
dict_len = len(words_set)
    
    

 
ll_max = 0
for s in X_train:
    ll = len(s)
    if ll > ll_max:
        ll_max = ll 

ll_max  
1755
    

 
# 序列长度
seq_len = 512
    

 
# 训练集
X_train1 = []
for x in X_train:
    temp = x + ["<PAD>"] * seq_len
    X_train1.append(temp[:seq_len])

# 测试集
X_test1 = []
for x in X_test:
    temp = x + ["<PAD>"] * seq_len
    X_test1.append(temp[:seq_len])
    

 

    

 

    

 
"""
    索引向量化
"""
# 训练集向量化

X_train2 = []
for x in X_train1:
    temp = []
    for word in x:
        idx = word2idx[word] if word in word2idx else word2idx["<UNK>"]
        temp.append(idx)
    X_train2.append(temp)
    

# 测试集向量化

X_test2 = []
for x in X_test1:
    temp = []
    for word in x:
        idx = word2idx[word] if word in word2idx else word2idx["<UNK>"]
        temp.append(idx)
    X_test2.append(temp)
    

 
len(X_test2[0])
512

    

 

    

 
"""
    构建数据集
"""

from torch.utils.data import Dataset
from torch.utils.data import DataLoader
import torch
from torch import nn
from torch.nn import functional as F

class MyDataSet(Dataset):
    
    def __init__(self, X=X_train2, y=y_train):
        self.X = X
        self.y = y
    
    def __len__(self):
        return len(self.X)
    
    def __getitem__(self, idx):
        x = self.X[idx]
        y = self.y[idx]
        
        return torch.tensor(data=x).long(), torch.tensor(data=y).long()


 
"""
    定义数据加载器
"""
train_dataset = MyDataSet(X=X_train2, y=y_train)
train_dataloader = DataLoader(dataset=train_dataset, shuffle=True, batch_size=128)

test_dataset = MyDataSet(X=X_test2, y=y_test)
test_dataloader = DataLoader(dataset=test_dataset, shuffle=False, batch_size=256)

 
train_dataset[0][0][:7]
tensor([ 5004, 10135, 20586,  9530, 16808, 14513,  9530])


 


 


 
import numpy as np
import torch
from torch import nn
from torch.nn import functional as F
class TextCNN3(nn.Module):
    """
        TextCNN优化,
    """
    def __init__(self,num_embeddings, embedding_dim,padding_idx,seq_len):
        super().__init__()
        
        self.embed = nn.Embedding(num_embeddings=num_embeddings,
                                    embedding_dim=embedding_dim,
                                    padding_idx=padding_idx)
        
        # [N, C, seq_len] -- [N, C, seq_len-1],[N, C, seq_len-kernel_size+1]
        self.gram_2 = nn.Sequential(
            nn.Conv1d(in_channels=embedding_dim, out_channels=256, kernel_size=2),
            # nn.BatchNorm1d(num_features=256),
            nn.ReLU(),
            nn.MaxPool1d(kernel_size=seq_len-1)  # [N, C, 1]
        )

        # [N, C, seq_len, 1] -- [N, C, seq_len-2, 1]
        self.gram_3 = nn.Sequential(
            nn.Conv1d(in_channels=embedding_dim, out_channels=256, kernel_size=3),
            # nn.BatchNorm1d(num_features=256),
            nn.ReLU(),
            nn.MaxPool1d(kernel_size=seq_len-2) # [N, C, 1]
        )
        # [N, C, seq_len, 1] -- [N, C, seq_len-3, 1]
        self.gram_4 = nn.Sequential(
            nn.Conv1d(in_channels=embedding_dim, out_channels=256, kernel_size=4),
            # nn.BatchNorm1d(num_features=256),
            nn.ReLU(),
            nn.MaxPool1d(kernel_size=seq_len-3) # [N, C, 1]
        )

        self.dropout1 = nn.Dropout(p=0.2)
        self.fc1 = nn.Linear(in_features=256*3, out_features=2)
        

    def forward(self,X):
        # [B,seq_len,embedding_dim]
        x = self.embed(X)
        # [B, seq_len, embedding_dim] -- [B, embedding_dim, seq_len] 
        x = torch.permute(input=x, dims=(0, 2, 1))
        # print(x.shape)  # torch.Size([128, 256, 87])

        x1 = self.gram_2(x)
        # print(f"x1.shape={x1.shape}")  #torch.Size([128, 256, 1])
        x2 = self.gram_3(x)
        x3 = self.gram_4(x)

        x = torch.concat(tensors=(x1,x2,x3),dim=1)
        # print(x.shape)  # torch.Size([128, 768, 1])
        x = torch.squeeze(x)

        x = self.dropout1(x)
        x = self.fc1(x)

        return x 
  

 
device = "cuda:0" if torch.cuda.is_available() else "cpu"

model = TextCNN3(num_embeddings=dict_len, embedding_dim=256, padding_idx=word2idx[""], seq_len=seq_len)

# 定义优化器
optimizer = torch.optim.SGD(params=model.parameters(), lr=1e-3)

# 定义损失函数
loss_fn = nn.CrossEntropyLoss()
  

 

  

 

  

 

  

 
# 定义训练轮次
epochs = 200

 
# 定义过程监控函数

def get_acc(dataloader=train_dataloader, model=model):
    accs = []
    model.to(device=device)
    model.eval()
    with torch.no_grad():
        for X,y in dataloader:
            X=X.to(device=device)
            y=y.to(device=device)
            y_pred = model(X)
            y_pred = y_pred.argmax(dim=1)
            acc = (y_pred == y).float().mean().item()
            accs.append(acc)
    return np.array(accs).mean()
    

 
# 定义训练过程
def train(model=model, 
            optimizer=optimizer, 
            loss_fn=loss_fn, 
            epochs=epochs, 
            train_dataloader=train_dataloader,
            test_dataloader=test_dataloader):
    model.to(device=device)
    for epoch in range(1, epochs+1):
        print(f"正在进行第 {epoch} 轮训练:")

        model.train()
        for X,y in train_dataloader:
            X=X.to(device=device)
            y=y.to(device=device)
            # 正向传播
            y_pred = model(X)

            # 清空梯度
            optimizer.zero_grad()

            # 计算损失
            loss = loss_fn(y_pred, y)

            # 梯度下降
            loss.backward()

            # 优化一步
            optimizer.step()

        print(f"train_acc: {get_acc(dataloader=train_dataloader)}, test_acc: {get_acc(dataloader=test_dataloader)}")
        
train()

 
train_acc: 0.9389391447368421, test_acc: 0.8470170497894287
正在进行第 190 轮训练:
train_acc: 0.9436677631578947, test_acc: 0.8477982997894287
正在进行第 191 轮训练:
train_acc: 0.9451069078947368, test_acc: 0.8504971623420715
正在进行第 192 轮训练:
train_acc: 0.9395559210526315, test_acc: 0.8450284123420715
正在进行第 193 轮训练:
train_acc: 0.9459292763157895, test_acc: 0.8504971623420715
正在进行第 194 轮训练:
train_acc: 0.9469572368421053, test_acc: 0.8493607997894287
正在进行第 195 轮训练:
train_acc: 0.9467516447368421, test_acc: 0.8512784123420716
正在进行第 196 轮训练:
train_acc: 0.9494243421052632, test_acc: 0.8512784123420716
正在进行第 197 轮训练:
train_acc: 0.9488075657894737, test_acc: 0.8512784123420716
正在进行第 198 轮训练:
train_acc: 0.9483963815789473, test_acc: 0.8477982997894287
正在进行第 199 轮训练:
train_acc: 0.9525082236842105, test_acc: 0.8531960248947144
正在进行第 200 轮训练:
train_acc: 0.9523026315789473, test_acc: 0.8539772748947143


 
优化方向:
- 模型定义复杂一些,这网络过于简单
- 可以序列设置更长一些,如果耐心/细心一点,可以分析一下出错的句子,是否不够长的因素
- 训练轮次可以现多一些

 


 
这里出了一个问题,
就是在一维卷积中增加BN组件
nn.BatchNorm1d(num_features=256),
梯度不会下降了

输出的精度稳定在一个数上,不变化,不知道是什么原因

 
对于文本处理,还有一个归一化,叫层归一化,后可以转为层归一化处理
    

 

    

 

    
情感识别·卷积·2维

 
[B,C,L] -- [B,C,L,1]

K = (3,1)
s = (1,1)
P = (1,0)

这样就可以应用2维卷积的方法了 

增加的这个1维,属于多维数据中的空维,是个空的维度,
这个维度上没有数据...那增加的是什么 ?
- 是 观察/看 数据的一个角度  
    

示例

 
import torch
from torch import nn

a = torch.randn(64,512) #[B,L]
a = a.unsqueeze(dim=1)  #[64,1,512],  [B,C,L]
a = a.unsqueeze(dim=3)  #[64,1,512,1],[B,C,L,1]

conv_func = nn.Conv2d(in_channels=1, 
                out_channels=512,
                kernel_size=(3, 1),
                stride=(1, 1),
                padding=(1, 0)
                )


conv_func(a).shape
torch.Size([64, 512, 512, 1])
    

 
C从1到512,而L的维度没有变化
    

 
2维卷积中BN不会导致结果不变了 
    

 

    

 
import os 
from tpf import pkl_save,pkl_load 
BASE_DIR = "/wks/datasets/hotel_reader"
file_path = os.path.join(BASE_DIR,'data_pkl/word.pkl')
X_train,y_train,X_test,y_test,words_set,word2idx,idx2word = pkl_load(file_path)

# 字典长度
dict_len = len(words_set)

# 序列长度
seq_len = 512

# 训练集
X_train1 = []
for x in X_train:
    temp = x + ["<PAD>"] * seq_len
    X_train1.append(temp[:seq_len])

# 测试集
X_test1 = []
for x in X_test:
    temp = x + ["<PAD>"] * seq_len
    X_test1.append(temp[:seq_len])


"""
    索引向量化
"""
# 训练集向量化

X_train2 = []
for x in X_train1:
    temp = []
    for word in x:
        idx = word2idx[word] if word in word2idx else word2idx["<UNK>"]
        temp.append(idx)
    X_train2.append(temp)
    

# 测试集向量化

X_test2 = []
for x in X_test1:
    temp = []
    for word in x:
        idx = word2idx[word] if word in word2idx else word2idx["<UNK>"]
        temp.append(idx)
    X_test2.append(temp)


"""
    构建数据集
"""
import numpy as np
from torch.utils.data import Dataset
from torch.utils.data import DataLoader
import torch
from torch import nn
from torch.nn import functional as F

class MyDataSet(Dataset):
    
    def __init__(self, X=X_train2, y=y_train):
        self.X = X
        self.y = y
    
    def __len__(self):
        return len(self.X)
    
    def __getitem__(self, idx):
        x = self.X[idx]
        y = self.y[idx]
        
        return torch.tensor(data=x).long(), torch.tensor(data=y).long()

train_dataset = MyDataSet(X=X_train2, y=y_train)
train_dataloader = DataLoader(dataset=train_dataset, shuffle=True, batch_size=128)

test_dataset = MyDataSet(X=X_test2, y=y_test)
test_dataloader = DataLoader(dataset=test_dataset, shuffle=False, batch_size=256)
train_dataset[0][0][:7]
    

 
tensor([ 5004, 10135, 20586,  9530, 16808, 14513,  9530])
    

 

    

 
之前的1维使用的是TextCNN,多尺度的概念 

这里的2维,就是普通的卷积提取特征,
- 3层卷积提取特征
- 全连接分类 

 
class TextCNN2D(nn.Module):
    """
        使用二维卷积来处理文本分类
    """
    def __init__(self, num_embeddings, embedding_dim=256,padding_idx=word2idx["<PAD>"]):
        super(TextCNN2D, self).__init__()
        # 嵌入层
        self.embed = nn.Embedding(num_embeddings=num_embeddings, 
                                  embedding_dim=embedding_dim,
                                  padding_idx=padding_idx)
        
        # 第一组
        self.conv2d1 = nn.Conv2d(in_channels=embedding_dim, 
                                out_channels=512,
                                kernel_size=(3, 1),
                                stride=(1, 1),
                                padding=(1, 0)
                              )
        self.bn1 = nn.BatchNorm2d(num_features=512)
        self.maxpool1 = nn.MaxPool2d(kernel_size=(2, 1), stride=(2, 1), padding=(0, 0))
        
        
        # 第二组
        self.conv2d2 = nn.Conv2d(in_channels=512, 
                                out_channels=512,
                                kernel_size=(3, 1),
                                stride=(1, 1),
                                padding=(1, 0)
                              )
        self.bn2 = nn.BatchNorm2d(num_features=512)
        self.maxpool2 = nn.MaxPool2d(kernel_size=(2, 1), stride=(2, 1), padding=(0, 0))
        
        
        # 第三组
        self.conv2d3 = nn.Conv2d(in_channels=512, 
                                out_channels=1024,
                                kernel_size=(3, 1),
                                stride=(1, 1),
                                padding=(1, 0)
                              )
        self.bn3 = nn.BatchNorm2d(num_features=1024)
        self.maxpool3 = nn.MaxPool2d(kernel_size=(2, 1), stride=(2, 1), padding=(0, 0))
        
        
        # 全连接层
        self.fc1 = nn.Linear(in_features=65536, out_features=512)
        self.dropout = nn.Dropout(p=0.2)
        self.fc2 = nn.Linear(in_features=512, out_features=2)
        
    def forward(self, x):
        # [B, 86] -- [B, 86, 256]
        x = self.embed(x)
        # [B, 86, 256] -- [B, 256, 86]
        x = x.permute(0, 2, 1)
        # [B, 256, 86] -- [B, 256, 86, 1]
        x = torch.unsqueeze(input=x, dim=-1)
        
        # 第一组卷积
        x = self.conv2d1(x)
        x = self.bn1(x)
        x = F.relu(x)
        
        # 第一个池化
        x = self.maxpool1(x)
        
        
        # 第二组卷积
        x = self.conv2d2(x)
        x = self.bn2(x)
        x = F.relu(x)
        
        # 第二个池化
        x = self.maxpool2(x)
        
        # 第三组卷积
        x = self.conv2d3(x)
        x = self.bn3(x)
        x = F.relu(x)
        
        # 第二个池化
        x = self.maxpool3(x)
        
        # 展平层
        x = x.view(x.size(0), -1)
        # print(x.shape)
        x = self.fc1(x)
        x = self.dropout(x)
        x =self.fc2(x)
        return x


model = TextCNN2D(num_embeddings=dict_len, embedding_dim=256, padding_idx=word2idx["<PAD>"])

# 定义优化器
optimizer = torch.optim.SGD(params=model.parameters(), lr=1e-3)

# 定义损失函数
loss_fn = nn.CrossEntropyLoss()


 
a = torch.randint(0,dict_len-1,(64,seq_len))
# model(a)

 


 
# 定义训练轮次
epochs = 200
device = "cuda:0" if torch.cuda.is_available() else "cpu"

# 定义过程监控函数
def get_acc(dataloader=train_dataloader, model=model):
    accs = []
    model.to(device=device)
    model.eval()
    with torch.no_grad():
        for X,y in dataloader:
            X=X.to(device=device)
            y=y.to(device=device)
            y_pred = model(X)
            y_pred = y_pred.argmax(dim=1)
            acc = (y_pred == y).float().mean().item()
            accs.append(acc)
    return np.array(accs).mean()

# 定义训练过程
def train(model=model, 
            optimizer=optimizer, 
            loss_fn=loss_fn, 
            epochs=epochs, 
            train_dataloader=train_dataloader,
            test_dataloader=test_dataloader):
    model.to(device=device)
    for epoch in range(1, epochs+1):
        print(f"正在进行第 {epoch} 轮训练:")

        model.train()
        for X,y in train_dataloader:
            X=X.to(device=device)
            y=y.to(device=device)
            # 正向传播
            y_pred = model(X)

            # 清空梯度
            optimizer.zero_grad()

            # 计算损失
            loss = loss_fn(y_pred, y)

            # 梯度下降
            loss.backward()

            # 优化一步
            optimizer.step()

        print(f"train_acc: {get_acc(dataloader=train_dataloader)}, test_acc: {get_acc(dataloader=test_dataloader)}")
        
train()

  

 
精度相当1维没有变化,85% 
感觉计算量稍微大了一点 

 


 
由于之前BN影响了训练,因为这里注释三层BN中的第二层 
# self.bn2 = nn.BatchNorm2d(num_features=512)

出现了过拟合

 
正在进行第 30 轮训练:
train_acc: 0.96484375, test_acc: 0.8110085248947143
正在进行第 31 轮训练:
train_acc: 0.9849917763157895, test_acc: 0.8337357997894287
正在进行第 32 轮训练:
train_acc: 0.984375, test_acc: 0.8305397748947143
正在进行第 33 轮训练:
train_acc: 0.98046875, test_acc: 0.825994324684143

 
加bn2,依然出现过拟合,说明BN与过拟合没有太多关系,再次注释掉,毕竟少了一点计算

正在进行第 16 轮训练:
train_acc: 0.91796875, test_acc: 0.7944602370262146
正在进行第 17 轮训练:
train_acc: 0.9342105263157895, test_acc: 0.7936789870262146
正在进行第 18 轮训练:
train_acc: 0.9397615131578947, test_acc: 0.8111505746841431

基于前面OneHot编码89%的精度,这里seq_len=1024,全连接分类输入达到了131072

 
# 全连接层
self.fc1 = nn.Linear(in_features=131072, out_features=512)
self.dropout = nn.Dropout(p=0.5)
self.fc2 = nn.Linear(in_features=512, out_features=2)

可能向量过长,增加Dropout的概述,随机舍弃一半,再次训练  


依然出现了过拟合,并且波动较大

 
正在进行第 23 轮训练:
train_acc: 0.9506578947368421, test_acc: 0.8325994372367859
正在进行第 24 轮训练:
train_acc: 0.8122944078947368, test_acc: 0.71015625
正在进行第 25 轮训练:
train_acc: 0.9619654605263158, test_acc: 0.8423295497894288
正在进行第 26 轮训练:
train_acc: 0.9592927631578947, test_acc: 0.8441761374473572


训练轮次增加精度有所提升,也可能是seq_len设置长包含更多信息的原因

 
在进行第 70 轮训练:
train_acc: 0.9997944078947368, test_acc: 0.8718039870262146
正在进行第 71 轮训练:
train_acc: 1.0, test_acc: 0.8713778495788574
正在进行第 72 轮训练:
train_acc: 1.0, test_acc: 0.86796875
正在进行第 73 轮训练:
train_acc: 1.0, test_acc: 0.8705965995788574
正在进行第 74 轮训练:
train_acc: 1.0, test_acc: 0.8730113744735718
正在进行第 75 轮训练:
train_acc: 1.0, test_acc: 0.8722301244735717
正在进行第 76 轮训练:
train_acc: 1.0, test_acc: 0.8725852370262146
正在进行第 77 轮训练:
train_acc: 1.0, test_acc: 0.8733664870262146
    
    

 

    
参考