云南网站设计外包,政务网站建设的功能模块,如何盗取网站,网页设计学习内容引言
本文我们采用比较聚合模型来实现文本匹配任务。
数据准备
数据准备包括
构建词表(Vocabulary)构建数据集(Dataset)
本次用的是LCQMC通用领域问题匹配数据集#xff0c;它已经分好了训练、验证和测试集。
我们通过pandas来加载一下。
import pandas as pdtrain_df …引言
本文我们采用比较聚合模型来实现文本匹配任务。
数据准备
数据准备包括
构建词表(Vocabulary)构建数据集(Dataset)
本次用的是LCQMC通用领域问题匹配数据集它已经分好了训练、验证和测试集。
我们通过pandas来加载一下。
import pandas as pdtrain_df pd.read_csv(data_path.format(train), sep\t, headerNone, names[sentence1, sentence2, label])train_df.head()数据是长这样子的有两个待匹配的句子标签是它们是否相似。
下面用jieba来处理每个句子。
def tokenize(sentence):return list(jieba.cut(sentence))train_df.sentence1 train_df.sentence1.apply(tokenize)
train_df.sentence2 train_df.sentence2.apply(tokenize)得到分好词的数据后我们就可以得到整个训练语料库中的所有token
train_sentences train_df.sentence1.to_list() train_df.sentence2.to_list()
train_sentences[0][喜欢, 打篮球, 的, 男生, 喜欢, 什么样, 的, 女生]现在就可以来构建词表了我们定义一个类
UNK_TOKEN UNK
PAD_TOKEN PADclass Vocabulary:Class to process text and extract vocabulary for mappingdef __init__(self, token_to_idx: dict None, tokens: list[str] None) - None:Args:token_to_idx (dict, optional): a pre-existing map of tokens to indices. Defaults to None.tokens (list[str], optional): a list of unique tokens with no duplicates. Defaults to None.assert any([tokens, token_to_idx]), At least one of these parameters should be set as not None.if token_to_idx:self._token_to_idx token_to_idxelse:self._token_to_idx {}if PAD_TOKEN not in tokens:tokens [PAD_TOKEN] tokensfor idx, token in enumerate(tokens):self._token_to_idx[token] idxself._idx_to_token {idx: token for token, idx in self._token_to_idx.items()}self.unk_index self._token_to_idx[UNK_TOKEN]self.pad_index self._token_to_idx[PAD_TOKEN]classmethoddef build(cls,sentences: list[list[str]],min_freq: int 2,reserved_tokens: list[str] None,) - Vocabulary:Construct the Vocabulary from sentencesArgs:sentences (list[list[str]]): a list of tokenized sequencesmin_freq (int, optional): the minimum word frequency to be saved. Defaults to 2.reserved_tokens (list[str], optional): the reserved tokens to add into the Vocabulary. Defaults to None.Returns:Vocabulary: a Vocubulary instanetoken_freqs defaultdict(int)for sentence in tqdm(sentences):for token in sentence:token_freqs[token] 1unique_tokens (reserved_tokens if reserved_tokens else []) [UNK_TOKEN]unique_tokens [tokenfor token, freq in token_freqs.items()if freq min_freq and token ! UNK_TOKEN]return cls(tokensunique_tokens)def __len__(self) - int:return len(self._idx_to_token)def __getitem__(self, tokens: list[str] | str) - list[int] | int:Retrieve the indices associated with the tokens or the index with the single tokenArgs:tokens (list[str] | str): a list of tokens or single tokenReturns:list[int] | int: the indices or the single indexif not isinstance(tokens, (list, tuple)):return self._token_to_idx.get(tokens, self.unk_index)return [self.__getitem__(token) for token in tokens]def lookup_token(self, indices: list[int] | int) - list[str] | str:Retrive the tokens associated with the indices or the token with the single indexArgs:indices (list[int] | int): a list of index or single indexReturns:list[str] | str: the corresponding tokens (or token)if not isinstance(indices, (list, tuple)):return self._idx_to_token[indices]return [self._idx_to_token[index] for index in indices]def to_serializable(self) - dict:Returns a dictionary that can be serializedreturn {token_to_idx: self._token_to_idx}classmethoddef from_serializable(cls, contents: dict) - Vocabulary:Instantiates the Vocabulary from a serialized dictionaryArgs:contents (dict): a dictionary generated by to_serializableReturns:Vocabulary: the Vocabulary instancereturn cls(**contents)def __repr__(self):return fVocabulary(size{len(self)})
可以通过build方法传入所有分好词的语句同时传入min_freq指定保存最少出现次数的单词。
这里实现了__getitem__来获取token对应的索引如果传入的是单个token就返回单个索引如果传入的是token列表就返回索引列表。类似地通过lookup_token来根据所以查找对应的token。
vocab Vocabulary.build(train_sentences)
vocab100%|██████████| 477532/477532 [00:0000:00, 651784.13it/s]
Vocabulary(size35925)我们的词表有35925个token。
有了词表之后我们就可以向量化句子了这里也通过一个类来实现。
class TMVectorizer:The Vectorizer which vectorizes the Vocabularydef __init__(self, vocab: Vocabulary, max_len: int) - None:Args:vocab (Vocabulary): maps characters to integersmax_len (int): the max length of the sequence in the datasetself.vocab vocabself.max_len max_lendef _vectorize(self, indices: list[int], vector_length: int -1, padding_index: int 0) - np.ndarray:Vectorize the provided indicesArgs:indices (list[int]): a list of integers that represent a sequencevector_length (int, optional): an arugment for forcing the length of index vector. Defaults to -1.padding_index (int, optional): the padding index to use. Defaults to 0.Returns:np.ndarray: the vectorized index arrayif vector_length 0:vector_length len(indices)vector np.zeros(vector_length, dtypenp.int64)if len(indices) vector_length:vector[:] indices[:vector_length]else:vector[: len(indices)] indicesvector[len(indices) :] padding_indexreturn vectordef _get_indices(self, sentence: list[str]) - list[int]:Return the vectorized sentenceArgs:sentence (list[str]): list of tokensReturns:indices (list[int]): list of integers representing the sentencereturn [self.vocab[token] for token in sentence]def vectorize(self, sentence: list[str], use_dataset_max_length: bool True) - np.ndarray:Return the vectorized sequenceArgs:sentence (list[str]): raw sentence from the datasetuse_dataset_max_length (bool): whether to use the global max vector lengthReturns:the vectorized sequence with paddingvector_length -1if use_dataset_max_length:vector_length self.max_lenindices self._get_indices(sentence)vector self._vectorize(indices, vector_lengthvector_length, padding_indexself.vocab.pad_index)return vectorclassmethoddef from_serializable(cls, contents: dict) - TMVectorizer:Instantiates the TMVectorizer from a serialized dictionaryArgs:contents (dict): a dictionary generated by to_serializableReturns:TMVectorizer:vocab Vocabulary.from_serializable(contents[vocab])max_len contents[max_len]return cls(vocabvocab, max_lenmax_len)def to_serializable(self) - dict:Returns a dictionary that can be serializedReturns:dict: a dict contains Vocabulary instance and max_len attributereturn {vocab: self.vocab.to_serializable(), max_len: self.max_len}def save_vectorizer(self, filepath: str) - None:Dump this TMVectorizer instance to fileArgs:filepath (str): the path to store the filewith open(filepath, w) as f:json.dump(self.to_serializable(), f)classmethoddef load_vectorizer(cls, filepath: str) - TMVectorizer:Load TMVectorizer from a fileArgs:filepath (str): the path stored the fileReturns:TMVectorizer:with open(filepath) as f:return TMVectorizer.from_serializable(json.load(f))命名为TMVectorizer表示是用于文本匹配(Text Matching)的专门类调用vectorize方法一次传入一个分好词的句子就可以得到向量化的表示支持填充Padding。
同时还支持保存功能主要是用于保存相关的词表以及TMVectorizer所需的max_len字段。
在本小节的最后通过继承Dataset来构建专门的数据集。
class TMDataset(Dataset):Dataset for text matchingdef __init__(self, text_df: pd.DataFrame, vectorizer: TMVectorizer) - None:Args:text_df (pd.DataFrame): a DataFrame which contains the processed data examplesvectorizer (TMVectorizer): a TMVectorizer instanceself.text_df text_dfself._vectorizer vectorizerdef __getitem__(self, index: int) - Tuple[np.ndarray, np.ndarray, int]:row self.text_df.iloc[index]return (self._vectorizer.vectorize(row.sentence1),self._vectorizer.vectorize(row.sentence2),row.label,)def get_vectorizer(self) - TMVectorizer:return self._vectorizerdef __len__(self) - int:return len(self.text_df)
构建函数所需的参数只有两个分别是处理好的DataFrame和TMVectorizer实例。
实现__getitem__方法因为这个方法会被DataLoader调用在该方法中对语句进行向量化。
max_len 50
vectorizer TMVectorizer(vocab, max_len)train_dataset TMDataset(train_df, vectorizer)batch_size 128
train_data_loader DataLoader(train_dataset, batch_sizebatch_size,shuffleTrue)for setence1, setence12, label in train_data_loader:print(setence1)print(setence12)print(label)break模型实现 该模型的整体架构如上图所示由以下四层组成
预处理层(Preprocessing) 使用一个预处理层(图中没有)来处理 Q \pmb Q Q和 A \pmb A A来获取两个新矩阵 Q ‾ ∈ R l × Q \overline{\pmb Q} \in \R^{l \times Q} Q∈Rl×Q和 A ‾ ∈ R l × A \overline{\pmb A} \in \R^{l \times A} A∈Rl×A。目的是为序列中每个单词获取一个新的嵌入向量来捕获一些上下文信息。注意力层(Attention) 在 Q ‾ \overline{\pmb Q} Q和 A ‾ \overline{\pmb A} A上应用标准的注意力机制以获取对于 A ‾ \overline{\pmb A} A中每个列向量(对应一个单词)在 Q ‾ \overline{\pmb Q} Q中所有列向量相应的注意力权重。基于这些注意力权重对于 A ‾ \overline{\pmb A} A中的每个列向量 a ‾ j \overline {\pmb a}_j aj计算一个相应的 h j \pmb h_j hj向量它是 Q ‾ \overline{\pmb Q} Q列向量的注意力加权和。比较层(Comparision) 使用一个比较函数 f f f来组合每个 a ‾ j \overline {\pmb a}_j aj和 h ‾ j \overline {\pmb h}_j hj对到一个向量 t j \pmb t_j tj。聚合层(Aggregation) 使用CNN层来聚合向量序列 t \pmb t t用于最后的分类。
预处理层
这里使用了一种简化的LSTM/GRU的门控结构对输入文本进行处理。 Q ‾ σ ( W i Q b i ⊗ e Q ) ⊙ tanh ( W u Q b u ⊗ e Q ) A ‾ σ ( W i A b i ⊗ e A ) ⊙ tanh ( W u A b u ⊗ e A ) \begin{aligned} \overline{\pmb Q} \sigma(W^i\pmb Q \pmb b^i \otimes \pmb e_Q) \odot \tanh(W^u \pmb Q \pmb b^u \otimes \pmb e_Q) \\ \overline{\pmb A} \sigma(W^i\pmb A \pmb b^i \otimes \pmb e_A) \odot \tanh(W^u \pmb A \pmb b^u \otimes \pmb e_A) \end{aligned} QAσ(WiQbi⊗eQ)⊙tanh(WuQbu⊗eQ)σ(WiAbi⊗eA)⊙tanh(WuAbu⊗eA) 相当于仅保留输入门来记住有意义的单词 σ ( ⋅ ) \sigma(\cdot) σ(⋅)部分代表是门控 tanh ( ⋅ ) \tanh(\cdot) tanh(⋅)代表具体的值。
其中 ⊙ \odot ⊙代表元素级乘法 W i , W u ∈ R l × d W^i,W^u \in \R^{l \times d} Wi,Wu∈Rl×d, b i , b u ∈ R l \pmb b^i,\pmb b^u \in \R^l bi,bu∈Rl是要学习的参数 ⊗ \otimes ⊗代表克罗内克积。具体为将列向量 b \pmb b b复制Q份拼接起来组成一个 l × Q l \times Q l×Q的矩阵与 W i Q W^i\pmb Q WiQ的结果矩阵维度保持一致但这在Pytorch中似乎利用广播机制就够了 l l l表示隐藏单元个数。
class Preprocess(nn.Module):Implements the preprocess layerdef __init__(self, embedding_dim: int, hidden_size: int) - None:Args:embedding_dim (int): embedding sizehidden_size (int): hidden sizesuper().__init__()self.Wi nn.Parameter(torch.randn(embedding_dim, hidden_size))self.bi nn.Parameter(torch.randn(hidden_size))self.Wu nn.Parameter(torch.randn(embedding_dim, hidden_size))self.bu nn.Parameter(torch.randn(hidden_size))def forward(self, x: torch.Tensor) - torch.Tensor:Args:x (torch.Tensor): the input sentence with shape (batch_size, seq_len, embedding_size)Returns:torch.Tensor:# e_xi (batch_size, seq_len, hidden_size)e_xi torch.matmul(x, self.Wi)# gate (batch_size, seq_len, hidden_size)gate torch.sigmoid(e_xi self.bi)# e_xu (batch_size, seq_len, hidden_size)e_xu torch.matmul(x, self.Wu)# value (batch_size, seq_len, hidden_size)value torch.tanh(e_xu self.bu)# x_bar (batch_size, seq_len, hidden_size)x_bar gate * valuereturn x_bar预处理层可以接收 Q Q Q和 A A A分别得到 Q ‾ \overline{\pmb Q} Q和 A ‾ \overline{\pmb A} A。这里实现上分别计算门控和具体的值然后将它们乘起来。
注意力层
注意力层构建在计算好的 Q ‾ \overline{\pmb Q} Q和 A ‾ \overline{\pmb A} A上 G softmax ( ( W g Q ‾ b g ⊗ e Q ) T A ‾ ) H Q ‾ G \begin{aligned} \pmb G \text{softmax}((W^g\overline{\pmb Q} \pmb b^g \otimes \pmb e_Q)^T \overline{\pmb A}) \\ \pmb H \overline{\pmb Q}\pmb G \end{aligned} GHsoftmax((WgQbg⊗eQ)TA)QG 其中 W g ∈ R l × l W^g \in \R^{l \times l} Wg∈Rl×l b g ∈ R l \pmb b^g \in \R ^l bg∈Rl是学习的参数 G ∈ R Q × A \pmb G \in \R^{Q \times A} G∈RQ×A是注意力矩阵 H ∈ R l × A \pmb H \in \R^{l \times A} H∈Rl×A是注意力加权的向量即注意力运算结果它的维度和 A \pmb A A一致。
具体地 h j \pmb h_j hj是 H \pmb H H的第 j j j列是通过 Q ‾ \overline{\pmb Q} Q的所有列的加权和计算而来表示最能匹配 A \pmb A A中地 j j j个单词的部分 Q \pmb Q Q。
class Attention(nn.Module):def __init__(self, hidden_size: int) - None:super().__init__()self.Wg nn.Parameter(torch.randn(hidden_size, hidden_size))self.bg nn.Parameter(torch.randn(hidden_size))def forward(self, q_bar: torch.Tensor, a_bar: torch.Tensor) - torch.Tensor:forward in attention layerArgs:q_bar (torch.Tensor): the question sentencce with shape (batch_size, q_seq_len, hidden_size)a_bar (torch.Tensor): the answer sentence with shape (batch_size, a_seq_len, hidden_size)Returns:torch.Tensor: weighted sum of q_bar# e_q_bar (batch_size, q_seq_len, hidden_size)e_q torch.matmul(q_bar, self.Wg)# transform (batch_size, q_seq_len, hidden_size)transform e_q self.bg# attention_matrix (batch_size, q_seq_len, a_seq_len)attention_matrix torch.softmax(torch.matmul(transform, a_bar.permute(0, 2, 1)))# h (batch_size, a_seq_len, hidden_size)h torch.matmul(attention_matrix.permute(0, 2, 1), a_bar)return h这里要注意这两个句子的长度不能搞混了。显示地将句子长度写出来不容易出错。
比如attention_matrix注意力矩阵得到的维度是(batch_size, q_seq_len, a_seq_len)对应原论文中说的 Q × A Q \times A Q×A Q Q Q表示句子 Q \pmb Q Q的长度 A A A表示句子 Q \pmb Q Q的长度。
最后计算出来的 H \pmb H H与 A \pmb A A一致。相当于是单向地计算了 A \pmb A A对 Q \pmb Q Q的注意力即 A A A的每个时间步都考虑了 Q \pmb Q Q的所有时间步。
比较层
作者觉得光注意力不够还添加了一个比较层。比较层的目的是匹配每个 a ‾ j \overline{\pmb a}_j aj它表示上下文 A \pmb A A中第 j j j个单词和 h j \pmb h_j hj它表示能最好匹配 a ‾ j \overline{\pmb a}_j aj的加权版 Q \pmb Q Q。 f f f表示一个比较函数转换 a ‾ j \overline{\pmb a}_j aj和 h j \pmb h_j hj到一个向量 t j \pmb t_j tj该向量表示比较的结果。
我们这里实现的是作者提出来的混合比较函数。
即组合了SUB和MULT接一个NN SUBMUTLNN : t j f ( a ‾ j , h j ) ReLU ( W [ ( a ‾ j − h j ) ⊙ ( a ‾ j − h j ) a ‾ j ⊙ h j ] b ) \text{SUBMUTLNN}: \quad \pmb t_j f(\overline{\pmb a}_j , \pmb h_j ) \text{ReLU}(W\begin{bmatrix} (\overline{\pmb a}_j -\pmb h_j) \odot (\overline{\pmb a}_j -\pmb h_j) \\ \overline{\pmb a}_j \odot \pmb h_j \\ \end{bmatrix} \pmb b) SUBMUTLNN:tjf(aj,hj)ReLU(W[(aj−hj)⊙(aj−hj)aj⊙hj]b) 如果用图片表示的话那就是上图(4)(5)(2)。
class Compare(nn.Module):def __init__(self, hidden_size: int) - None:super().__init__()self.W nn.Parameter(torch.randn(2 * hidden_size, hidden_size))self.b nn.Parameter(torch.randn(hidden_size))def forward(self, h: torch.Tensor, a_bar: torch.Tensor) - torch.Tensor:Args:h (torch.Tensor): the output of Attention layer (batch_size, a_seq_len, hidden_size)a_bar (torch.Tensor): proprecessed a (batch_size, a_seq_len, hidden_size)Returns:torch.Tensor:# sub (batch_size, a_seq_len, hidden_size)sub (h - a_bar) ** 2# mul (batch_size, a_seq_len, hidden_size)mul h * a_bar# t (batch_size, a_seq_len, hidden_size)t torch.relu(torch.matmul(torch.cat([sub, mul], dim-1), self.W) self.b)return t该比较层的输入接收上一层比较后的结果和预处理后的 A \pmb A A。
最后是聚合层以及整个模型架构的堆叠。
我们先来看聚合层。
聚合层
在通过上面的比较函数得到一系列 t j \pmb t_j tj向量之后使用一层(Text) CNN来聚合这些向量 r CNN ( [ t 1 , ⋯ , t A ] ) \pmb r \text{CNN}([\pmb t_1,\cdots, \pmb t_A]) rCNN([t1,⋯,tA]) 这里 r ∈ R n l \pmb r \in \R^{nl} r∈Rnl可以用于最终的分类 n n n是CNN中的窗口数(卷积核数)。
聚合层接收上一层的输出 t \pmb t t这是一个长度为 A A A的向量序列。得到的 r \pmb r r维度是 n × l n \times l n×l l l l我们知道是隐藏单元个数 n n n是卷积核数。
具体在实现这里的CNN之前我们先来回顾一下CNN的知识。
CNN
我们通过图片来直观理解一下 https://ezyang.github.io/convolution-visualizer/ 提供了一个很好地可视化页面。 假设初始时有一张大小6x6的输入图片卷积核大小为3即这个filter(上图中的Weight)为3x3filter中的权重是可学习的填充为0步长(stride)为1代表这个filter每次移动一步。上面的这个Dilation参数我们这里不需要关心。
具体地filter和它盖住的输入部分对应位置元素相乘再累加得到一个标量输出此时位于输出(output)矩阵的0,0处。 由于步长为1filter可以右移一步经过运算得到了Output中的0,1处的标量。以此类推 当它移动到输入的最右边时计算出来Output中第一行的最后一个元素0,3处。
下一次移动该filter就会从输入的第二行开始 计算出Output中的1,0处元素。这就是卷积操作。该输出Output也叫feature map这里只演示了一个filter实际上如果再来一个同样大小但参数不同的卷积核我们就可以得到2个4x4的feature map。
要注意的是filter不一定是方阵。
那我们要怎么知道输出的大小呢可以看出这是和输入大小以及filter大小有关的。
如果我们有一个 n × n n \times n n×n的图像用一个 f × f f \times f f×f 的filter做卷积那么得到的结果矩阵大小将是 ( n − f 1 ) × ( n − f 1 ) (n - f 1) \times (n - f 1) (n−f1)×(n−f1)。
我们带入算一下这里 n 6 , f 3 n6,f3 n6,f3结果矩阵大小是 6 − 3 1 4 6-314 6−314正确。
在CV中图片一般是用rgb来表示的分别表示三个通道(channel)输入就会变成三维的。那么filter也会变成三维的过滤器的通道数要和输入的通道数一致。 虽然是三维的但过滤器一步计算出来的结果还是一个标量之前是累加9个数现在变成了累加27个数。这样我们得到的输出大小还是4x4的。
下面用一张动图作为一个总结图片来自Lerner Zhang在参考3问题中的回答 这里有三个通道即图片是3D的有两个filter每个filter也是3D的我们就得到了2个输出。图中还画了一个偏置。
等等我们上面介绍的只是卷积操作其实还有池化操作。 这里输入是一个 4 × 4 4 \times 4 4×4的矩阵用到的池化类型是最大池化(max pooling)把输入分成了四组每组中取最大元素得到一个 2 × 2 2 \times 2 2×2的输出矩阵。
可以理解为应用了一个 2 × 2 2\times 2 2×2的filter步长为2。
除了最大池化比较常用的还有一种叫平均池化就是计算对应元素的平均值。
池化操作的输出大小计算为 n − f s 1 \frac{n-f}{s} 1 sn−f1 s s s表示步长 n n n是feature map的高度。
我们也计算一下 4 − 2 2 1 2 \frac{4-2}{2}12 24−212没错。
最后的最后一般还有一个激活函数比如可以是 ReLU \text{ReLU} ReLU应用在池化后的结果上。
以上是对CNN在图像应用的一个小回顾更详细的可以见参考文章。
下面我们来看下CNN如何应用在文本上。
TextCNN 个人感觉描述TextCNN最清晰的就是这张图片来自参考5的论文。
这张图片描述的内容有点多我们来逐一分析一下。
按照从左往右、从上往下的顺序①首先来看输入输入是一个 7 × 5 7 \times 5 7×5的矩阵也就是(seq_len, embedding_dim)句子长度为7词嵌入大小为5。
② 然后应用了三个不同的filter大小(用不同的颜色表示)分别为2,3,4。 每个大小分配两个不同的filter(用同一颜色不同深浅表示比如第二列矩阵第一个是棕红色第二个是亮红色都是大小为4的)即共6个filter。
这里要注意的是filter不再是方阵而是(filter_size, embedding_dim)。且一般步长都会设成1但不需要右移直接往下移即可这非常类似word2vec中n-gram的窗口大小(每次处理filter_size个单词)所以也称这个filter_size为window_size这个在看论文的时候要注意。
就以这个 4 × 5 4 \times 5 4×5的filter为例它得到的feature map大小是怎样的呢由于filter的宽度是固定的为词嵌入大小因此不管词嵌入大小有多大每次filter计算输出是一个标量。所以我们只要关心filter的高度也就是filter_size。在上面的图片中表示为region size。
所以这里的region size为4基于步长为1的情况下也可以通过上面小节介绍的公式 ( n − f 1 ) (n - f 1) (n−f1)来计算。 n n n是输入句子的长度 f f f就是filter_size。那么代入 7 − 4 1 4 7-414 7−414。即输出 4 × 1 4 \times 1 4×1的列向量(矩阵)对应上图第三列的第一个矩阵。
我们再来验证 f 2 f2 f2的情况输出应该为 7 − 2 1 6 7-216 7−216对应上图第三列黄色的矩阵数一下刚好也是 6 × 1 6 \times 1 6×1的。
由于共有6个filter因此共得到了6个列向量维度存在不一样的情况。对了这里卷积运算完毕后会经过激活函数不过不会改变维度。
③ 维度不一样没关系我们可以应用池化层。把这6个filter的输出应用对应大小的池化层就得到了6个标量。把这6个标量拼接在一起就得到了 6 × 1 6 \times 1 6×1的列向量。有多少个filter行的维度就是多少。所以这里是6这是很自然的。
这样我们得到了一个固定大小的输出如上图的倒数第二列。显然我们应用一个和filter输出的feature map同样大小的最大池化filter就可以得到一个标量。
④ 拿到这个定长向量可以把它理解为CNN作为特征提取器提取的句向量就可以应用到不同的任务。比如文本分类任务中可以喂给一个分类器。
好了所需要的知识就这些现在我们来实现这个由CNN构建的聚合层。
聚合层实现
class Aggregation(nn.Module):def __init__(self,embedding_dim: int,num_filter: int,filter_sizes: list[int],output_dim: int,conv_activation: str relu,dropout: float 0.1,) - None:_summary_Args:embedding_dim (int): embedding sizenum_filter (int): the output dim of each convolution layerfilter_sizes (list[int]): the size of the convolving kerneloutput_dim: (int) the number of classesconv_activation (str, optional): activation to use after the convolution layer. Defaults to relu.dropout (float): the dropout ratiosuper().__init__()if conv_activation.lower() relu:activation nn.ReLU()else:activation nn.Tanh()self.convs nn.ModuleList([nn.Sequential(nn.Conv2d(in_channels1,out_channelsnum_filter,kernel_size(fs, embedding_dim),),activation,)for fs in filter_sizes])pooled_output_dim num_filter * len(filter_sizes)self.linear nn.Linear(pooled_output_dim, output_dim)self.dropout nn.Dropout(dropout)def forward(self, t: torch.Tensor) - torch.Tensor:Args:t (torch.Tensor): the output of Compare (batch_size, a_seq_len, hidden_size)Returns:torch.Tensor:# t (batch_size, 1, a_seq_len, hidden_size)t t.unsqueeze(1)# the shape of convs_out(t) is (batch_size, num_filter, a_seq_len - filter_size 1, 1)# element in convs_out with shape (batch_size, num_filter, a_seq_len - filter_size 1)convs_out [self.dropout(conv(t).squeeze(-1)) for conv in self.convs]# adaptive_avg_pool1d applies a 1d adaptive max pooling over an input# adaptive_avg_pool1d(o, output_size1) returns an output with shape (batch_size, num_filter, 1)# so the elements in maxpool_out have a shape of (batch_size, num_filter)maxpool_out [F.adaptive_avg_pool1d(o, output_size1).squeeze(-1) for o in convs_out]# cat (batch_size, num_filter * len(filter_sizes))cat torch.cat(maxpool_out, dim1)# (batch_size, output_dim)return self.linear(cat)这就是聚合层的实现我们这里一次可以处理整个批次的数据。
我们这里用Conv2d来实现卷积它有几个必填参数
in_channels 输入的通道数对于图片来说的就是3对于文本来说可以简单的认为就是1out_channels 就是filter的个数对于同样大小的卷积核可以设定参数不同的filterkernel_size 卷积核大小这里不再是一个方阵而是(filter_size, hidden_size)的矩阵
原论文中用了不同的filter_sizehidden_size是经过前面层转换过的嵌入的维度和词嵌入维度可以不同所以用hidden_size来描述更准确。
此外这里应用了adaptive_max_pool1d方法来做最大池化操作它需要指定一个输出大小不管输入大小是怎样的都会转换成这样的输出大小我们就不需要关心其他东西。不然的话用max_pool1d来实现还要考虑它的参数。 也有不少人通过Conv1d来实现对文本的卷积实际上是一样的只不过参数不同Conv1d应该还简单些看个人的喜好。 整体实现
最后用一个模型把上面所有定义的模型封装起来
class Aggregation(nn.Module):def __init__(self,embedding_dim: int,num_filter: int,filter_sizes: list[int],output_dim: int,conv_activation: str relu,dropout: float 0.1,) - None:_summary_Args:embedding_dim (int): embedding sizenum_filter (int): the output dim of each convolution layerfilter_sizes (list[int]): the size of the convolving kerneloutput_dim: (int) the number of classesconv_activation (str, optional): activation to use after the convolution layer. Defaults to relu.dropout (float): the dropout ratiosuper().__init__()if conv_activation.lower() relu:activation nn.ReLU()else:activation nn.Tanh()self.convs nn.ModuleList([nn.Sequential(nn.Conv2d(in_channels1,out_channelsnum_filter,kernel_size(fs, embedding_dim),),activation,)for fs in filter_sizes])pooled_output_dim num_filter * len(filter_sizes)self.linear nn.Linear(pooled_output_dim, output_dim)self.dropout nn.Dropout(dropout)def forward(self, t: torch.Tensor) - torch.Tensor:Args:t (torch.Tensor): the output of Compare (batch_size, a_seq_len, hidden_size)Returns:torch.Tensor:# t (batch_size, 1, a_seq_len, hidden_size)t t.unsqueeze(1)# the shape of convs_out(t) is (batch_size, num_filter, a_seq_len - filter_size 1, 1)# element in convs_out with shape (batch_size, num_filter, a_seq_len - filter_size 1)convs_out [self.dropout(conv(t).squeeze(-1)) for conv in self.convs]# adaptive_avg_pool1d applies a 1d adaptive max pooling over an input# adaptive_avg_pool1d(o, output_size1) returns an output with shape (batch_size, num_filter, 1)# so the elements in maxpool_out have a shape of (batch_size, num_filter)maxpool_out [F.adaptive_avg_pool1d(o, output_size1).squeeze(-1) for o in convs_out]# cat (batch_size, num_filter * len(filter_sizes))cat torch.cat(maxpool_out, dim1)# (batch_size, output_dim)return self.linear(cat)class ComAgg(nn.Module):The Compare aggregate MODEL model implemention.def __init__(self, args) - None:super().__init__()self.embedding nn.Embedding(args.vocab_size, args.embedding_dim)self.preprocess Preprocess(args.embedding_dim, args.hidden_size)self.attention Attention(args.hidden_size)self.compare Compare(args.hidden_size)self.aggregate Aggregation(args.hidden_size,args.num_filter,args.filter_sizes,args.num_classes,args.conv_activation,args.dropout,)self.dropouts [nn.Dropout(args.dropout) for _ in range(4)]def forward(self, q: torch.Tensor, a: torch.Tensor) - torch.Tensor:_summary_Args:q (torch.Tensor): the inputs of q (batch_size, q_seq_len)a (torch.Tensor): the inputs of a (batch_size, a_seq_len)Returns:torch.Tensor: _description_q_embed self.dropouts[0](self.embedding(q))a_embed self.dropouts[0](self.embedding(a))q_bar self.dropouts[1](self.preprocess(q_embed))a_bar self.dropouts[1](self.preprocess(a_embed))h self.dropouts[2](self.attention(q_bar, a_bar))# t (batch_size, a_seq_len, hidden_size)t self.dropouts[3](self.compare(h, a_bar))# out (batch_size, num_filter * len(filter_sizes))out self.aggregate(t)return out
作者在附录透露了模型实现一些细节
词嵌入由GloVe初始化并且是 固定参数的这里我们直接用自己初始化词嵌入层向量隐藏层维度 l 150 l150 l150使用ADAMAX优化器我们使用Adam就够了批大小为 30 30 30有点小学习率为 0.002 0.002 0.002唯一需要的超参数是dropout中的丢弃率用于词嵌入层对于不同的任务使用了不同的卷积核最多同时使用了[1,2,3,4,5]。
训练模型
定义评估指标
def metrics(y: torch.Tensor, y_pred: torch.Tensor) - Tuple[float, float, float, float]:TP ((y_pred 1) (y 1)).sum().float() # True PositiveTN ((y_pred 0) (y 0)).sum().float() # True NegativeFN ((y_pred 0) (y 1)).sum().float() # False NegatvieFP ((y_pred 1) (y 0)).sum().float() # False Positivep TP / (TP FP).clamp(min1e-8) # Precisionr TP / (TP FN).clamp(min1e-8) # RecallF1 2 * r * p / (r p).clamp(min1e-8) # F1 scoreacc (TP TN) / (TP TN FP FN).clamp(min1e-8) # Accuraryreturn acc, p, r, F1定义评估函数
def evaluate(data_iter: DataLoader, model: nn.Module
) - Tuple[float, float, float, float]:y_list, y_pred_list [], []model.eval()for x1, x2, y in tqdm(data_iter):x1 x1.to(device).long()x2 x2.to(device).long()y torch.LongTensor(y).to(device)output model(x1, x2)pred torch.argmax(output, dim1).long()y_pred_list.append(pred)y_list.append(y)y_pred torch.cat(y_pred_list, 0)y torch.cat(y_list, 0)acc, p, r, f1 metrics(y, y_pred)return acc, p, r, f1定义训练函数
def evaluate(data_iter: DataLoader, model: nn.Module
) - Tuple[float, float, float, float]:y_list, y_pred_list [], []model.eval()for x1, x2, y in tqdm(data_iter):x1 x1.to(device).long()x2 x2.to(device).long()y torch.LongTensor(y).to(device)output model(x1, x2)pred torch.argmax(output, dim1).long()y_pred_list.append(pred)y_list.append(y)y_pred torch.cat(y_pred_list, 0)y torch.cat(y_list, 0)acc, p, r, f1 metrics(y, y_pred)return acc, p, r, f1def train(data_iter: DataLoader,model: nn.Module,criterion: nn.CrossEntropyLoss,optimizer: torch.optim.Optimizer,print_every: int 500,verboseTrue,
) - None:model.train()for step, (x1, x2, y) in enumerate(tqdm(data_iter)):x1 x1.to(device).long()x2 x2.to(device).long()y torch.LongTensor(y).to(device)output model(x1, x2)loss criterion(output, y)optimizer.zero_grad()loss.backward()optimizer.step()if verbose and (step 1) % print_every 0:pred torch.argmax(output, dim1).long()acc, p, r, f1 metrics(y, pred)print(f TRAIN iter{step1} loss{loss.item():.6f} accuracy{acc:.3f} precision{p:.3f} recal{r:.3f} f1 score{f1:.4f})
定义整体参数
args Namespace(dataset_csvtext_matching/data/lcqmc/{}.txt,vectorizer_filevectorizer.json,model_state_filemodel.pth,save_dirf{os.path.dirname(__file__)}/model_storage,reload_modelFalse,cudaTrue,learning_rate1e-3,batch_size128,num_epochs10,max_len50,embedding_dim200,hidden_size100,num_filter1,filter_sizes[1, 2, 3, 4, 5],conv_activationrelu,num_classes2,dropout0,min_freq2,print_every500,verboseTrue,
)开始训练
make_dirs(args.save_dir)if args.cuda:device torch.device(cuda:0 if torch.cuda.is_available() else cpu)
else:device torch.device(cpu)print(fUsing device: {device}.)vectorizer_path os.path.join(args.save_dir, args.vectorizer_file)train_df build_dataframe_from_csv(args.dataset_csv.format(train))
test_df build_dataframe_from_csv(args.dataset_csv.format(test))
dev_df build_dataframe_from_csv(args.dataset_csv.format(dev))if os.path.exists(vectorizer_path):print(Loading vectorizer file.)vectorizer TMVectorizer.load_vectorizer(vectorizer_path)args.vocab_size len(vectorizer.vocab)
else:print(Creating a new Vectorizer.)train_sentences train_df.sentence1.to_list() train_df.sentence2.to_list()vocab Vocabulary.build(train_sentences, args.min_freq)args.vocab_size len(vocab)print(fBuilds vocabulary : {vocab})vectorizer TMVectorizer(vocab, args.max_len)vectorizer.save_vectorizer(vectorizer_path)train_dataset TMDataset(train_df, vectorizer)
test_dataset TMDataset(test_df, vectorizer)
dev_dataset TMDataset(dev_df, vectorizer)train_data_loader DataLoader(train_dataset, batch_sizeargs.batch_size, shuffleTrue
)
dev_data_loader DataLoader(dev_dataset, batch_sizeargs.batch_size)
test_data_loader DataLoader(test_dataset, batch_sizeargs.batch_size)print(fArguments : {args})
model ComAgg(args)print(fModel: {model})model_saved_path os.path.join(args.save_dir, args.model_state_file)
if args.reload_model and os.path.exists(model_saved_path):model.load_state_dict(torch.load(args.model_saved_path))print(Reloaded model)
else:print(New model)model model.to(device)optimizer torch.optim.Adam(model.parameters(), lrargs.learning_rate)
criterion nn.CrossEntropyLoss()for epoch in range(args.num_epochs):train(train_data_loader,model,criterion,optimizer,print_everyargs.print_every,verboseargs.verbose,)print(Begin evalute on dev set.)with torch.no_grad():acc, p, r, f1 evaluate(dev_data_loader, model)print(fEVALUATE [{epoch1}/{args.num_epochs}] accuracy{acc:.3f} precision{p:.3f} recal{r:.3f} f1 score{f1:.4f})model.eval()acc, p, r, f1 evaluate(test_data_loader, model)
print(fTEST accuracy{acc:.3f} precision{p:.3f} recal{r:.3f} f1 score{f1:.4f})
Using device: cuda:0.
Building prefix dict from the default dictionary ...
Loading model from cache C:\Users\ADMINI~1\AppData\Local\Temp\jieba.cache
Loading model cost 0.531 seconds.
Prefix dict has been built successfully.
Loading vectorizer file.
Arguments : Namespace(dataset_csvtext_matching/data/lcqmc/{}.txt, vectorizer_filevectorizer.json, model_state_filemodel.pth, reload_modelFalse, cudaTrue, learning_rate0.001, batch_size128, num_epochs10, max_len50, embedding_dim200, hidden_size100, num_filter1, filter_sizes[1, 2, 3, 4, 5], conv_activationrelu, num_classes2, dropout0, min_freq2, print_every500, verboseTrue, vocab_size35925)
Model: ComAgg((embedding): Embedding(35925, 200)(preprocess): Preprocess()(attention): Attention()(compare): Compare()(aggregate): Aggregation((convs): ModuleList((0): Sequential((0): Conv2d(1, 1, kernel_size(1, 100), stride(1, 1))(1): ReLU())(1): Sequential((0): Conv2d(1, 1, kernel_size(2, 100), stride(1, 1))(1): ReLU())(2): Sequential((0): Conv2d(1, 1, kernel_size(3, 100), stride(1, 1))(1): ReLU())(3): Sequential((0): Conv2d(1, 1, kernel_size(4, 100), stride(1, 1))(1): ReLU())(4): Sequential((0): Conv2d(1, 1, kernel_size(5, 100), stride(1, 1))(1): ReLU()))(linear): Linear(in_features5, out_features2, biasTrue)(dropout): Dropout(p0, inplaceFalse))
)
New model
...
TRAIN iter500 loss0.427597 accuracy0.805 precision0.803 recal0.838 f1 score0.820153%|███████████████████████████████████████████████████████████████████████████████████████████████████▎ | 996/1866 [00:2900:25, 34.26it/s]
TRAIN iter1000 loss0.471204 accuracy0.789 precision0.759 recal0.900 f1 score0.823580%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▌ | 1499/1866 [00:4400:10, 34.42it/s]
TRAIN iter1500 loss0.446409 accuracy0.773 precision0.774 recal0.867 f1 score0.8176
100%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 1866/1866 [00:5500:00, 33.47it/s]
Begin evalute on dev set.
100%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 69/69 [00:0000:00, 80.59it/s]
EVALUATE [10/10] accuracy0.640 precision0.621 recal0.719 f1 score0.6666
100%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 98/98 [00:0100:00, 85.45it/s]
TEST accuracy0.678 precision0.628 recal0.871 f1 score0.7301参考
[论文笔记]A COMPARE-AGGREGATE MODEL FOR MATCHING TEXT SEQUENCES李宏毅机器学习——深度学习卷积神经网络https://stats.stackexchange.com/questions/295397/what-is-the-difference-between-conv1d-and-conv2d吴恩达深度学习——卷积神经网络基础A Sensitivity Analysis of (and Practitioners’ Guide to) Convolutional Neural Networks for Sentence Classification