BiMPM实战文本匹配【上】
引言
今天来实现BiMPM模型进行文本匹配,数据集采用的是中文文本匹配数据集。内容较长,分为上下两部分。
数据准备
数据准备这里和之前的模型有些区别,主要是因为它同时有字符词表和单词词表。
from collections import defaultdict
from tqdm import tqdm
import numpy as np
import json
from torch.utils.data import Dataset, DataLoader
import pandas as pd
from typing import TupleUNK_TOKEN = "<UNK>"
PAD_TOKEN = "<PAD>"class Vocabulary:"""Class to process text and extract vocabulary for mapping"""def __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 instane"""token_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(tokens=unique_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 index"""if 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 serialized"""return {"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_serializable`Returns:Vocabulary: the Vocabulary instance"""return cls(**contents)def __repr__(self):return f"<Vocabulary(size={len(self)})>"class TMVectorizer:"""The Vectorizer which vectorizes the Vocabulary"""def __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 dataset"""self.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 array"""if vector_length <= 0:vector_length = len(indices)vector = np.zeros(vector_length, dtype=np.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 sentence"""return [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 padding"""vector_length = -1if use_dataset_max_length:vector_length = self.max_lenindices = self._get_indices(sentence)vector = self._vectorize(indices, vector_length=vector_length, padding_index=self.vocab.pad_index)return vector@classmethoddef from_serializable(cls, contents: dict) -> "TMVectorizer":"""Instantiates the TMVectorizer from a serialized dictionaryArgs:contents (dict): a dictionary generated by `to_serializable`Returns:TMVectorizer:"""vocab = Vocabulary.from_serializable(contents["vocab"])max_len = contents["max_len"]return cls(vocab=vocab, max_len=max_len)def to_serializable(self) -> dict:"""Returns a dictionary that can be serializedReturns:dict: a dict contains Vocabulary instance and max_len attribute"""return {"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 file"""with 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))
先定义词表和向量化的类,然后在分词的时候去掉数字和字母。因为有些数字/字母连在一起非常长,为了防止得到过长的word len。
def tokenize(sentence: str):tokens = []for word in jieba.cut(sentence):if word.isdigit():tokens.extend(list(word))else:tokens.append(word)return tokens
同时移除了所有的标点:
def build_dataframe_from_csv(dataset_csv: str) -> pd.DataFrame:df = pd.read_csv(dataset_csv,sep="\t",header=None,names=["sentence1", "sentence2", "label"],)# remove all punctuationsdf.sentence1 = df.sentence1.str.replace(r'[^\u4e00-\u9fa50-9]', '', regex=True)df.sentence2 = df.sentence2.str.replace(r'[^\u4e00-\u9fa50-9]', '', regex=True)df = df.dropna()return dfdef tokenize_df(df):df.sentence1 = df.sentence1.apply(tokenize)df.sentence2 = df.sentence2.apply(tokenize)return df
我们来看下处理好的结果:
args = Namespace(dataset_csv="text_matching/data/lcqmc/{}.txt",vectorizer_file="vectorizer.json",model_state_file="model.pth",save_dir=f"{os.path.dirname(__file__)}/model_storage",reload_model=False,cuda=False,learning_rate=1e-3,batch_size=128,num_epochs=10,max_len=50,char_vocab_size=4699,word_embedding_dim=300,word_vocab_size=35092,max_word_len=8,char_embedding_dim=20,hidden_size=100,char_hidden_size=50,num_perspective=20,num_classes=2,dropout=0.2,epsilon=1e-8,min_word_freq=2,min_char_freq=1,print_every=500,verbose=True,)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"))train_df.head()
上图是处理后的数据。
接下来先估计字符词表:
train_chars = train_df.sentence1.to_list() + train_df.sentence2.to_list()char_vocab = Vocabulary.build(train_chars, args.min_char_freq)
# 将词表长度写到参数中
args.char_vocab_size = len(char_vocab)
再进行分词,分词的同时去掉子母和数字:
train_word_df = tokenize_df(train_df)
test_word_df = tokenize_df(test_df)
dev_word_df = tokenize_df(dev_df)train_sentences = train_df.sentence1.to_list() + train_df.sentence2.to_list()
train_sentences[:10]
[['喜欢', '打篮球', '的', '男生', '喜欢', '什么样', '的', '女生'],['我', '手机', '丢', '了', '我', '想', '换个', '手机'],['大家', '觉得', '她', '好看', '吗'],['求', '秋色', '之空', '漫画', '全集'],['晚上', '睡觉', '带', '着', '耳机', '听', '音乐', '有', '什么', '害处', '吗'],['学', '日语', '软件', '手机', '上', '的'],['打印机', '和', '电脑', '怎样', '连接', '该', '如何', '设置'],['侠盗', '飞车', '罪恶都市', '怎样', '改车'],['什么', '花', '一年四季', '都', '开'],['看图', '猜', '一', '电影', '名']]
接着生成单词词表:
word_vocab = Vocabulary.build(train_sentences, args.min_word_freq)args.word_vocab_size = len(word_vocab)args.word_vocab_size
然后找出最长的单词:
words = [word_vocab.lookup_token(idx) for idx in range(args.word_vocab_size)]longest_word = ''for word in words:if len(word) > len(longest_word):longest_word = wordlongest_word
'中南财经政法大学'
# 记录下最长单词长度
args.max_word_len = len(longest_word)char_vectorizer = TMVectorizer(char_vocab, len(longest_word))
word_vectorizer = TMVectorizer(word_vocab, args.max_len)
然后用最长单词长度和自定义的最长句子长度分别构建字符和单词向量化实例。
我们根据这两个TMVectorizer
重新设计TMDataset
:
from typing import Tupleclass TMDataset(Dataset):"""Dataset for text matching"""def __init__(self, text_df: pd.DataFrame, char_vectorizer: TMVectorizer, word_vectorizer: TMVectorizer) -> None:"""Args:text_df (pd.DataFrame): a DataFrame which contains the processed data examples (list of word list)vectorizer (TMVectorizer): a TMVectorizer instance"""self.text_df = text_dfself._char_vectorizer = char_vectorizerself._word_vectorizer = word_vectorizerdef __getitem__(self, index: int) -> Tuple[np.ndarray, np.ndarray, int]:row = self.text_df.iloc[index]def vectorize_character(sentence: list[str]) -> np.ndarray:# (seq_len, word_len)char_vectors = np.zeros(shape=(self._word_vectorizer.max_len, self._char_vectorizer.max_len))for idx, word in enumerate(sentence):char_vectors[idx] = self._char_vectorizer.vectorize(word)return char_vectorsself._char_vectorizer.vectorize(row.sentence1),self._char_vectorizer.vectorize(row.sentence2),return (self._word_vectorizer.vectorize(row.sentence1),self._word_vectorizer.vectorize(row.sentence2),vectorize_character(row.sentence1),vectorize_character(row.sentence2),row.label,)def get_vectorizer(self) -> Tuple[TMVectorizer, TMVectorizer]:return self._word_vectorizer, self._char_vectorizerdef __len__(self) -> int:return len(self.text_df)
多返回了两个char_vectors
,它们的形状都是(seq_len, word_len)
。
train_dataset = TMDataset(train_df, char_vectorizer, word_vectorizer)
test_dataset = TMDataset(test_df, char_vectorizer, word_vectorizer)
dev_dataset = TMDataset(dev_df, char_vectorizer, word_vectorizer)for v1, v2, c1, c2, l in train_dataset:print(v1.shape)print(v2.shape)print(c1.shape)print(c2.shape)print(l)break
(50,)
(50,)
(50, 8)
(50, 8)
1
看一下每个样本的形状。
模型实现
整个模型架构如上图所示,具体的理论部分可以参考引用或自己去读原论文。
我们从底向上依次实现。
单词表征层
该层的目标是用一个 d d d维度的向量来表示 P P P和 Q Q Q中的每个单词。该向量由两部分组成:一个单词级嵌入和一个字符级嵌入。这里我们的单词级嵌入使用Embedding
层来实现,随机初始化来训练。
字符嵌入也是随机初始化可训练的,通过将一个单词中的每个字符(由字符嵌入表示)输入到一个LSTM网络中,用最后一个字符的输出代表整个单词的字符级嵌入。然后和单词级嵌入拼接起来。就得到了该层的输出:两个词向量序列 P : [ p 1 , ⋯ , p M ] P:[\pmb p_1,\cdots,\pmb p_M] P:[p1,⋯,pM]和 Q : [ q 1 , ⋯ , q N ] Q:[\pmb q_1,\cdots,\pmb q_N] Q:[q1,⋯,qN]。
class WordRepresentation(nn.Module):def __init__(self, args: Namespace) -> None:super().__init__()self.char_embed = nn.Embedding(args.char_vocab_size, args.char_embedding_dim, padding_idx=0)self.char_lstm = nn.LSTM(input_size=args.char_embedding_dim,hidden_size=args.char_hidden_size,batch_first=True,)self.word_embed = nn.Embedding(args.word_vocab_size, args.word_embedding_dim)self.reset_parameters()def reset_parameters(self) -> None:nn.init.uniform_(self.char_embed.weight, -0.005, 0.005)# zere vectors for padding indexself.char_embed.weight.data[0].fill_(0)nn.init.uniform_(self.word_embed.weight, -0.005, 0.005)nn.init.kaiming_normal_(self.char_lstm.weight_ih_l0)nn.init.constant_(self.char_lstm.bias_ih_l0, val=0)nn.init.orthogonal_(self.char_lstm.weight_hh_l0)nn.init.constant_(self.char_lstm.bias_hh_l0, val=0)def forward(self, x: Tensor, x_char: Tensor) -> Tensor:"""Args:x (Tensor): word input sequence a with shape (batch_size, seq_len)x_char (Tensor): character input sequence a with shape (batch_size, seq_len, word_len)Returns:Tensor: concatenated word and char embedding (batch_size, seq_len, word_embedding_dim + char_hidden_size)"""batch_size, seq_len, word_len = x_char.shape# (batch_size, seq_len, word_len) -> (batch_size * seq_len, word_len)x_char = x_char.view(-1, word_len)# x_char_embed (batch_size * seq_len, word_len, char_embedding_dim)x_char_embed = self.char_embed(x_char)# x_char_hidden (1, batch_size * seq_len, char_hidden_size)_, (x_char_hidden, _) = self.char_lstm(x_char_embed)# x_char_hidden (batch_size, seq_len, char_hidden_size)x_char_hidden = x_char_hidden.view(batch_size, seq_len, -1)# x_embed (batch_size, seq_len, word_embedding_dim),x_embed = self.word_embed(x)# (batch_size, seq_len, word_embedding_dim + char_hidden_size)return torch.cat([x_embed, x_char_hidden], dim=-1)
字符嵌入需要一个Embedding
层和一个单向LSTM
层;单词嵌入只需要定义一个Embedding
层。
本模型的参数初始化挺重要的,对这些网络层不同的参数分别进行了初始化。其中正交初始化(nn.init.orthogonal_
)可以缓解LSTM中的梯度消失/爆炸问题、改善收敛性、提高模型的泛化能力、降低参数的冗余性。
注意WordRepresentation
层是单独作用于不同的语句的,每个语句进行单词级拆分和字符级拆分。
在计算字符级嵌入时,首先将x_char
的形状变为(batch_size * seq_len, word_len)
,可以理解增大了批大小,word_len
是最长单词长度;然后输入到char_embed
中得到字符嵌入;最后再喂给char_lstm
并用最后一个时间步(最后一个字符)对应的状态表示对应的单词的字符级嵌入。
然后把单词的字符级嵌入恢复成原来的形状,即batch_size, seq_len, char_hidden_size
。
这样就可以和单词嵌入(batch_size, seq_len, word_embedding_dim)
进行在最后一个维度上拼接。
即融合了单词和字符级信息的表征。
上下文表示层
上下文表示层(Context Representation Layer) 使用一个BiLSTM合并上下文信息到 P P P和 Q Q Q的每个时间步的表示中。
class ContextRepresentation(nn.Module):def __init__(self, args: Namespace) -> None:super().__init__()self.context_lstm = nn.LSTM(input_size=args.word_embedding_dim + args.char_hidden_size,hidden_size=args.hidden_size,batch_first=True,bidirectional=True,)self.reset_parameters()def reset_parameters(self) -> None:nn.init.kaiming_normal_(self.context_lstm.weight_ih_l0)nn.init.constant_(self.context_lstm.bias_ih_l0, val=0)nn.init.orthogonal_(self.context_lstm.weight_hh_l0)nn.init.constant_(self.context_lstm.bias_hh_l0, val=0)nn.init.kaiming_normal_(self.context_lstm.weight_ih_l0_reverse)nn.init.constant_(self.context_lstm.bias_ih_l0_reverse, val=0)nn.init.orthogonal_(self.context_lstm.weight_hh_l0_reverse)nn.init.constant_(self.context_lstm.bias_hh_l0_reverse, val=0)def forward(self, x: Tensor) -> Tensor:"""Compute the contextual information about input.Args:x (Tensor): (batch_size, seq_len, hidden_size)Returns:Tensor: (batch_size, seq_len, 2 * hidden_size)"""# (batch_size, seq_len, 2 * hidden_size)return self.context_lstm(x)[0]
该层的实现和简单,注意它的LSTM输入大小是低层的输出大小,也是独立应用于每个语句的。
匹配层
匹配层(Matching Layer) 这是该模型的核心层,也是最复杂的。
目标是用一个句子的每个上下文嵌入(时间步)和另一个句子的所有上下文嵌入(时间步)进行比较。如上图所示,我们会从两个方向匹配 P P P和 Q Q Q:对于 P P P来说, P P P的每个时间步都会和 Q Q Q所有时间步进行匹配,然后 Q Q Q的每个时间步也会和 P P P所有时间步进行匹配。
为了让一个句子的一个时间步与另一个的所有时间步进行匹配,作者设计了一个多视角匹配操作 ⊗ \otimes ⊗。该层的输出是两个匹配向量序列,每个序列为一个句子一个时间步与另一个所有时间步的匹配结果。
通过以下两步来定义多视角匹配操作 ⊗ \otimes ⊗:
① 定义一个多视角余弦匹配函数 f m f_m fm来比较两个向量:
m = f m ( v 1 , v 2 ; W ) (1) \pmb m = f_m(\pmb v_1,\pmb v_2;W) \tag 1 m=fm(v1,v2;W)(1)
这里 v 1 \pmb v_1 v1和 v 2 \pmb v_2 v2是两个 d d d维的向量; W ∈ R l × d W \in \R^{l \times d} W∈Rl×d是一个可训练的参数; l l l是视角数;返回的 m \pmb m m是一个 l l l维的向量 m = [ m 1 , ⋯ , m k , ⋯ , m l ] \pmb m= [m_1,\cdots,m_k,\cdots,m_l] m=[m1,⋯,mk,⋯,ml]。
其中每个元素 m k ∈ m m_k \in \pmb m mk∈m是第 k k k个视角的匹配值(标量),它是通过计算两个加权向量余弦相似度得到的:
m k = cos ( W k ∘ v 1 , W k ∘ v 2 ) (2) m_k = \cos(W_k \circ \pmb v_1,W_k \circ \pmb v_2) \tag 2 mk=cos(Wk∘v1,Wk∘v2)(2)
这里 ∘ \circ ∘是元素级乘法; W k W_k Wk是 W W W的第 k k k行,它控制了第 k k k个视角并且为 d d d维空间的不同维度分配了不同的权重。
把公式 ( 1 ) (1) (1)展开来就是:
m = f m ( v 1 , v 2 ; W ) = [ m 1 ⋯ m k ⋯ m l ] = [ cos ( W 1 ∘ v 1 , W 1 ∘ v 2 ) ⋯ cos ( W k ∘ v 1 , W k ∘ v 2 ) ⋯ cos ( W l ∘ v 1 , W l ∘ v 2 ) ] ∈ R l (3) m = f_m(\pmb v_1,\pmb v_2;W)=\begin{bmatrix} m_1 \\ \cdots \\ m_k \\ \cdots \\ m_l \end{bmatrix} = \begin{bmatrix} \cos(W_1 \circ \pmb v_1,W_1 \circ \pmb v_2) \\ \cdots \\ \cos(W_k \circ \pmb v_1,W_k \circ \pmb v_2) \\ \cdots \\ \cos(W_l \circ \pmb v_1,W_l \circ \pmb v_2) \\ \end{bmatrix} \in \R^l \tag 3 m=fm(v1,v2;W)= m1⋯mk⋯ml = cos(W1∘v1,W1∘v2)⋯cos(Wk∘v1,Wk∘v2)⋯cos(Wl∘v1,Wl∘v2) ∈Rl(3)
这里的 l l l是超参数,对应不同的权重。
简单来说就是计算两个向量的余弦相似度,但这两个向量是经过加权( W i ∘ v , i ∈ { 1 , ⋯ , l } W_i \circ v,\quad i \in \{1,\cdots,l\} Wi∘v,i∈{1,⋯,l})之后的结果。有多少个权重是由 l l l控制的,每次加权的参数不同,相当于不同的视角。总共有 l l l个视角。希望用权重(视角)去控制比较这两个向量不同的方面。
下面来看第二步。
②基于上面这个匹配函数,定义了四种匹配策略:
- 全匹配(Full-Matching)
- 最大池匹配(Maxpooling-Matching)
- 注意力匹配(Attentive-Matching)
- 最大注意力匹配(Max-Attentive-Matching)
全匹配
如下图所示,在该策略中,每个句子的正向(或反向)上下文嵌入 h i p → \overset{\rightarrow}{\pmb h_i^p} hip→(或 h i p ← \overset{\leftarrow}{\pmb h_i^p} hip←)都与另一句的正向(或反向)的最后一个时间步表示 h N q → \overset{\rightarrow}{\pmb h_N^q} hNq→(或 h 1 q ← \overset{\leftarrow}{\pmb h_1^q} h1q←)进行比较:
比如 Q Q Q句子正向上所有时间步的嵌入要比较的是 P P P正向上最后一个时间步。
公式如下:
根据公式实现全匹配如下:
def _full_matching(self, v1: Tensor, v2_last: Tensor, w: Tensor) -> Tensor:"""full matching operation.Args:v1 (Tensor): the full embedding vector sequence (batch_size, seq_len1, hidden_size)v2_last (Tensor): single embedding vector (batch_size, hidden_size)w (Tensor): weights of one direction (num_perspective, hidden_size)Returns:Tensor: (batch_size, seq_len1, num_perspective)"""# (batch_size, seq_len1, num_perspective, hidden_size)v1 = self._time_distributed_multiply(v1, w)# (batch_size, num_perspective, hidden_size)v2 = self._time_distributed_multiply(v2_last, w)# (batch_size, 1, num_perspective, hidden_size)v2 = v2.unsqueeze(1)# (batch_size, seq_len1, num_perspective)return self._cosine_similarity(v1, v2)
如函数所示,它接收两个参数,分别是某个方向上(正向或反向)一个包含所有时间步的完整序列向量,和另一个同方向上最后一个时刻的向量。
如公式 ( 3 ) (3) (3)所示,分别让这两个向量乘以多视角权重 w ∈ R l × d w \in \R^{l \times d} w∈Rl×d,这里调用_time_distributed_multiply
来实现。最后调用_cosine_similarity
计算它们之间的余弦相似度。
def _time_distributed_multiply(self, x: Tensor, w: Tensor) -> Tensor:"""element-wise multiply vector and weights.Args:x (Tensor): sequence vector (batch_size, seq_len, hidden_size) or singe vector (batch_size, hidden_size)w (Tensor): weights (num_perspective, hidden_size)Returns:Tensor: (batch_size, seq_len, num_perspective, hidden_size) or (batch_size, num_perspective, hidden_size)"""# dimension of xn_dim = x.dim()hidden_size = x.size(-1)# if n_dim == 3seq_len = x.size(1)# (batch_size * seq_len, hidden_size) for n_dim == 3# (batch_size, hidden_size) for n_dim == 2x = x.contiguous().view(-1, hidden_size)# (batch_size * seq_len, 1, hidden_size) for n_dim == 3# (batch_size, 1, hidden_size) for n_dim == 2x = x.unsqueeze(1)# (1, num_perspective, hidden_size)w = w.unsqueeze(0)# (batch_size * seq_len, num_perspective, hidden_size) for n_dim == 3# (batch_size, num_perspective, hidden_size) for n_dim == 2x = x * w# reshape to original shapeif n_dim == 3:# (batch_size, seq_len, num_perspective, hidden_size)x = x.view(-1, seq_len, self.l, hidden_size)elif n_dim == 2:# (batch_size, num_perspective, hidden_size)x = x.view(-1, self.l, hidden_size)# (batch_size, seq_len, num_perspective, hidden_size) for n_dim == 3# (batch_size, num_perspective, hidden_size) for n_dim == 2return x
_time_distributed_multiply
可以接收不同形状的向量,num_perspective
是视角数,也就是原文中的l
,为了和1
进行区分,这里用完整名称表示。
首先查看输入向量的x
形状,如果有3个维度,我们还要记录它的seq_len
大小。
然后转换成(-1, hidden_size)
的形状,接着变成(?, 1, hidden_size)
的维度,这里?
根据n_dim
有所区分,具体可以参考注释。
接着为了进行广播,在w
上也插入一个维度,变成 (1, num_perspective, hidden_size)
。
广播的时候x
会变成(?, num_perspective, hidden_size)
;w
会变成(?, num_perspective, hidden_size)
。
这样x * w
实际上是逐元素相乘。
最后还原成原来x
的形状(batch_size, seq_len, num_perspective, hidden_size)
或x.view(-1, self.l, hidden_size)
,但多了个视角数num_perspective
。
此时的这个待返回的x
已经是乘以不同视角权重后的结果。
我们把眼光回到_full_matching
中,
# (batch_size, seq_len1, num_perspective, hidden_size)
v1 = self._time_distributed_multiply(v1, w)v2 = self._time_distributed_multiply(v2_last, w)
# (batch_size, 1, num_perspective, hidden_size)
v2 = v2.unsqueeze(1)# (batch_size, seq_len1, num_perspective)
return self._cosine_similarity(v1, v2)
同样为了进行广播,对v2
插入相应的维度,变成了 (batch_size, 1, num_perspective, hidden_size)
,那个1
会被复制seq_len1
次。
最后调用_cosine_similarity
计算它们之间的余弦相似度。
def _cosine_similarity(self, v1: Tensor, v2: Tensor) -> Tensor:"""compute cosine similarity between v1 and v2.Args:v1 (Tensor): (..., hidden_size)v2 (Tensor): (..., hidden_size)Returns:Tensor: _description_"""# element-wise multiplycosine = v1 * v2# caculate on hidden_size dimenstaion# (batch_size, seq_len, l)cosine = cosine.sum(-1)# caculate on hidden_size dimenstaion# (batch_size, seq_len, l)v1_norm = torch.sqrt(torch.sum(v1**2, -1).clamp(min=self.epsilon))# (batch_size, seq_len, l)v2_norm = torch.sqrt(torch.sum(v2**2, -1).clamp(min=self.epsilon))# (batch_size, seq_len, l)return cosine / (v1_norm * v2_norm)
我们以上面的例子继续分析,这里v1
的形状是(batch_size, seq_len1, num_perspective, hidden_size)
,v2
也被会广播成(batch_size, seq_len1, num_perspective, hidden_size)
。
根据余弦相似度的公式,首先计算这两个向量的点积,然后除以这两个向量的模。
第一步是计算这两个向量的点积,首先进行逐元素乘法,得到(batch_size, seq_len1, num_perspective, hidden_size)
的结果,然后求和,得到(batch_size, seq_len1, num_perspective)
的结果。
可以看成是有batch_size * seq_len1 * num_perspective
个向量对进行点积运算,每个向量的维度是100维,cosine = cosine.sum(-1)
就得到batch_size * seq_len1 * num_perspective
个标量(点积结果)。
也可以理解为两个(batch_size, seq_len1, num_perspective, hidden_size)
的向量在hidden_size
维度上计算点积。
接下来分别计算这两个向量的模,可能结果非常小接近零,防止过小,设定了最小为sefl.epsilon
。
最后就是点积除以模,得到余弦相似度的值。
下面来看最大池化匹配。
最大池化匹配
如下图所示,在这种策略中,每个正向(或反向)上下文嵌入 h i p → \overset{\rightarrow}{\pmb h_i^p} hip→(或 h i p ← \overset{\leftarrow}{\pmb h_i^p} hip←)都与另一句的每个正向(或反向)上下文嵌入 h j q → \overset{\rightarrow}{\pmb h_j^q} hjq→(或 h j q ← \overset{\leftarrow}{\pmb h_j^q} hjq←, 其中 j ∈ ( 1 , ⋯ N ) j \in (1,\cdots N) j∈(1,⋯N)) 进行比较,然后只保留每个维度的最大值。
公式为:
理解了全匹配之后应该不难理解这个最大池化匹配。
这次在这两个向量序列之间互相计算,也是在最后一个维度hidden_size
上计算。
首先分别得到加权后的向量,然后这两个向量计算余弦相似度,最后在第二个向量的seq_len
维度上找到最大的值。没了。
def _max_pooling_matching(self, v1: Tensor, v2: Tensor, w: Tensor) -> Tensor:"""max pooling matching operation.Args:v1 (Tensor): (batch_size, seq_len1, hidden_size)v2 (Tensor): (batch_size, seq_len2, hidden_size)w (Tensor): (num_perspective, hidden_size)Returns:Tensor: (batch_size, seq_len1, num_perspective)"""# (batch_size, seq_len1, num_perspective, hidden_size)v1 = self._time_distributed_multiply(v1, w)# (batch_size, seq_len2, num_perspective, hidden_size)v2 = self._time_distributed_multiply(v2, w)# (batch_size, seq_len1, 1, num_perspective, hidden_size)v1 = v1.unsqueeze(2)# (batch_size, 1, seq_len2, num_perspective, hidden_size)v2 = v2.unsqueeze(1)# (batch_size, seq_len1, seq_len2, num_perspective)cosine = self._cosine_similarity(v1, v2)# (batch_size, seq_len1, num_perspective)return cosine.max(2)[0]
为了计算这两个向量之间的余弦相似度,分别需要插入新维度,变成(batch_size, seq_len1, seq_len2, num_perspective, hidden_size)
的形式。
调用_cosine_similarity
计算余弦相似度后,形状变成了 (batch_size, seq_len1, seq_len2, num_perspective)
。然后取第二个向量序列维度上的最大值,即cosine.max(2)
,它会返回一个value和index,通过[0]
取它的value。
相关文章:
BiMPM实战文本匹配【上】
引言 今天来实现BiMPM模型进行文本匹配,数据集采用的是中文文本匹配数据集。内容较长,分为上下两部分。 数据准备 数据准备这里和之前的模型有些区别,主要是因为它同时有字符词表和单词词表。 from collections import defaultdict from …...
【C++】构造函数和析构函数第二部分(拷贝构造函数)--- 2023.9.28
目录 什么是拷贝构造函数?编译器默认的拷贝构造函数构造函数的分类及调用结束语 什么是拷贝构造函数? 用一句话来描述为拷贝构造即 “用一个已知的对象去初始化另一个对象” 具体怎么使用我们直接看代码,代码如下: class Maker…...
现在学RPA,还有前途吗,会不会太卷?
RPA是机器人流程自动化的缩写,是一种通过软件机器人模拟人类操作计算机的技术。随着人工智能和自动化技术的不断发展,RPA已经成为了企业数字化转型的重要工具之一。那么,现在学习RPA还有前途吗?会不会太卷? 一、RPA的…...
Vue的详细教程--用Vue-cli搭建SPA项目
Vue的详细教程--用Vue-cli搭建SPA项目 1.Vue-cli是什么2.什么是SPA项目1.vue init webpack spa2.一问一答模式2:运行完上面的命令后,我们需要将当前路径改变到SPA这个文件夹内,然后安装需要的模块此步骤可理解成:maven的web项目创…...
openldap访问控制
系统:debian12 /etc/ldap/slapd.d/cnconfig目录下 包含以下三个数据库: dn: olcDatabase{-1}frontend,cnconfig dn: olcDatabase{0}config,cnconfig dn: olcDatabase{1}mdb,cnconfigolcDatabase: [{\<index\>}]\<type\>数据库条目必须具有…...
阿里云服务器技术创新、网络技术和数据中心技术说明
阿里云服务器技术创新、网络技术创新、数据中心技术创新和智能运维:云服务器方升架构、自研硬件、自研存储硬件AliFlash和异构计算加速平台,以及全自研网络系统技术创新和数据中心巴拿马电源、液冷技术等技术创新说明,阿里云百科分享阿里云服…...
华为智能高校出口安全解决方案(2)
本文承接: https://qiuhualin.blog.csdn.net/article/details/131475315?spm1001.2014.3001.5502 重点讲解华为智能高校出口安全解决方案的基础网络安全&业务部署与优化的部署流程。 华为智能高校出口安全解决方案(2) 课程地址基础网络…...
【AI绘画】Stable Diffusion WebUI
💝💝💝欢迎来到我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。 推荐:kuan 的首页,持续学…...
html、css学习记录【uniapp前奏】
Html 声明:该学习笔记源于菜鸟自学网站,特此记录笔记。很多示例源于此官网,若有侵权请联系删除。 文章目录 Html声明: CSS 全称 Cascading Style Sheets,层叠样式表。是一种用来为结构化文档(如 HTML 文档…...
Linux-正则三剑客
目录 一、正则简介 1.正则表达式分两类: 2.正则表达式的意义 二、Linux三剑客简介 1.文本处理工具,均支持正则表达式引擎 2.正则表达式分类 3.基本正则表达式BRE集合 4.扩展正则表达式ere集合 三、grep 1.简介 2.实践 3.贪婪匹配 四、sed …...
Zilliz@阿里云:大模型时代下Milvus Cloud向量数据库处理非结构化数据的最佳实践
大模型时代下的数据存储与分析该如何处理?有没有已经落地的应用实践? 为探讨这些问题,近日,阿里云联合 Zilliz 和 Doris 举办了一场以《大模型时代下的数据存储与分析》为主题的技术沙龙,其中,阿里云对象存储 OSS 上拥有海量的非结构化数据,Milvus(Zilliz)作为全球最有…...
解决 react 项目启动端口冲突
报错信息: Emitted error event on Server instance at:at emitErrorNT (net.js:1358:8)at processTicksAndRejections (internal/process/task_queues.js:82:21) {code: EADDRINUSE,errno: -4091,syscall: listen,address: 0.0.0.0,port: 8070 }解决方法ÿ…...
ChatGPT AIGC 总结Vlookup的20种不同用法
Vlookup是Excel中最常见的函数。接下来我们让ChatGPT,AIGC总结Vlookup函数的用法 。 1. 基本的VLOOKUP用法:=VLOOKUP("John", A2:B5, 2, FALSE)。在A2:B5范围中查找"John",返回与"John"在同一行的第2列的值。例如,查找员工姓名,返回员工ID。…...
Android Logcat 命令行工具
关于作者:CSDN内容合伙人、技术专家, 从零开始做日活千万级APP。 专注于分享各领域原创系列文章 ,擅长java后端、移动开发、商业变现、人工智能等,希望大家多多支持。 目录 一、导读二、概览三、日常用法3.1 面板介绍3.2 日志过滤…...
蓝桥等考Python组别八级004
第一部分:选择题 1、Python L8 (15分) 运行下面程序,输出的结果是( )。 i = 1 while i <= 3: print(i, end = ) i += 1 1 20 1 2 31 2 30 1 2正确答案:C 2、Python L8...
selenium-webdriver 阿里云ARMS 自动化巡检
很久没更新了,今天分享一篇关于做项目巡检的内容,这部分,前两天刚在公司做了部门分享,趁着劲还没过,发出来跟大家分享下。 一、本地巡检实现 1. Selenium Webdriver(SW) 简介 Selenium Webdriver(以下简称…...
【数据仓库设计基础(二)】维度数据模型
文章目录 一. 概述二. 维度数据模型建模过程三. 维度规范化四. 维度数据模型的特点五. 维度数据模型1. 星型模式1.1.事实表1.2.维度表1.3.优点1.4.缺点1.5.示例 2. 雪花模式2.1.数据规范化与存储2.2&#x…...
【数据结构】排序算法(一)—>插入排序、希尔排序、选择排序、堆排序
👀樊梓慕:个人主页 🎥个人专栏:《C语言》《数据结构》《蓝桥杯试题》《LeetCode刷题笔记》《实训项目》 🌝每一个不曾起舞的日子,都是对生命的辜负 目录 前言 1.直接插入排序 2.希尔排序 3.直接选择排…...
基于JAVA+SpringBoot的新闻发布平台
✌全网粉丝20W,csdn特邀作者、博客专家、CSDN新星计划导师、java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ 🍅文末获取项目下载方式🍅 一、项目背景介绍: 随着科技的飞速发展和…...
Java实现word excel ppt模板渲染与导出及预览 LibreOffice jodconverter
Java Office 一、文档格式转换 文档格式转换是office操作中经常需要进行一个操作,例如将docx文档转换成pdf格式。 java在这方面有许多的操作方式,大致可以分为内部调用(无需要安装额外软件),外部调用(需…...
【通意千问】大模型GitHub开源工程学习笔记(2)
使用Transformers来使用模型 如希望使用Qwen-chat进行推理,所需要写的只是如下所示的数行代码。请确保你使用的是最新代码,并指定正确的模型名称和路径,如Qwen/Qwen-7B-Chat和Qwen/Qwen-14B-Chat 这里给出了一段代码 from transformers import AutoModelForCausalLM, Aut…...
MQ - 35 四款MQ的架构设计与实现的对比
文章目录 导图概述RabbitMQ顺序消息定时和延时消息事务消息优先级队列死信队列WebSocketRocketMQ顺序消息定时和延时消息事务消息死信队列消息查询根据 Offset 查询消息根据时间戳查询消息据消息 ID 查询消息SchemaKafka顺序消息幂等事务消息消息查询...
spring6-IOC容器
IOC容器 1、IoC容器1.1、控制反转(IoC)1.2、依赖注入1.3、IoC容器在Spring的实现 2、基于XML管理Bean2.1、搭建子模块spring6-ioc-xml2.2、实验一:获取bean①方式一:根据id获取②方式二:根据类型获取③方式三ÿ…...
macOS - 使用 chromedriver
文章目录 下载对应的 chromedriver 下载 Chrome https://www.google.com/chrome/ 查看 版本 下载对应的 chromedriver http://chromedriver.storage.googleapis.com/index.html https://chromedriver.chromium.org/downloads 移动 sudo mv chromedriver /usr/local/bin/ $ c…...
项目进展(四)-双电机均可驱动,配置模拟SPI,调平仪功能初步实现!
一、前言 截止到今天,该项目也算实现基本功能了,后续继续更新有关32位ADC芯片相关的内容,今天对驱动芯片做一个总结,也对模拟SPI做一点总结吧 二、模拟SPI 由于模拟SPI还是得有四种模式(CPOL和CPHA组合为四种),下面…...
《学术小白学习之路13》基于DTM和主题共现网络——实现主题时序演化网络分析(数据代码在结尾)
《学术小白学习之路13》基于DTM和主题共现网络实现主题演化网络分析 一、数据导入二、数据预处理2.1分词2.2 向量化三、DTM建模3.1 主题一致性检验3.2主题建模四、计算主题的相似度4.1获取文档主题分布4.2 时期分组4.3相似度计算4.3.1第一时期和第二时期的对比4.3.2第二时期与第…...
实验三十三、三端稳压器 LM7805 稳压性能的研究
一、题目 LM7805 输出电压、电压调整率、电流调整率以及输出纹波电压的研究。 二、仿真电路 电路如图1所示。集成稳压芯片采用 LM7805CT。 三、仿真内容 (1)测量图1(a)LM7805CT 的电压调整率,测量条件为 I O 50…...
第三章 软件架构
固件框架由如下所示的构建块组成,如上图所示。 隔离边界。分区接口。分区。分区清单。分区管理器。以下各小节详细描述了这些构建块。 3.1 隔离边界 该框架定义了两种类型的隔离边界。 1、逻辑隔离边界,可用于以下情况: (1)通过一个由 IMPLEMENTATION DEFINED 机制定义…...
怎么保护苹果手机移动应用程序ipa中文件安全?
目录 前言 1. 对敏感文件进行文件名称混淆 2. 更改文件的MD5值 3. 增加不可见水印处理 3. 对html,js,css等资源进行压缩 5. 删除可执行文件中的调试信息 前言 ios应用程序存储一些图片,资源,配置信息,甚至敏感数…...
中秋节快乐
中秋节快乐,国庆节快乐...
哪里做网站最好网站/只要做好关键词优化
LeetCode 31 Next Permutation / 60 Permutation Sequence [Permutation] <c> LeetCode 31 Next Permutation 给出一个序列,求其下一个排列 STL中有std::next_permutation这个方法可以直接拿来用 也可以写一个实现程序: 从右往左遍历序列ÿ…...
手机做网站服务器吗/短视频获客系统
在子线程中new一个Handler为什么会报以下错误? java.lang.RuntimeException: Cant create handler inside thread that has not called Looper.prepare() 这是因为Handler对象与其调用者在同一线程中,如果在Handler中设置了延时操作,则调用…...
ps海报素材网/seo网站优化知识
写一个脚本/root/bin/yesorno.sh,提示用户输入yes或no,并判断用户输入的是yes还是no,或是其它信息#!/bin/bash read -p"put your answer(yes or no):" Answer case $Answer in y|Y|[yY][eE][sS]) //判断用户输入yes时的一切可能性echo &q…...
b2b网站大全 网址大全/泰州网站建设优化
本文翻译自:Getting error “No such module” using Xcode, but the framework is thereIm currently coding in Swift, and Ive got an error: 我目前正在使用Swift进行编码,但出现错误: No such module Social 没有这样的模块社交 But I…...
能发朋友圈的网站建设语/东莞网站推广方案
C语言入门视频教程_9天精通Linux C语言 - 创客学院www.makeru.com.cn良好习惯之规范在写C语言程序的时候为了书写清晰、便于阅读、便于理解、便于维护,在编写程序时应遵循以下规则:1、一个说明或一个语句占一行,例如:包含头文件…...
帝国cms 网站地图插件/上海网站推广广告
作者 | 杨秀璋责编 | 夕颜出品 | CSDN博客数学形态学(Mathematical morphology)是一门建立在格论和拓扑学基础之上的图像分析学科,是数学形态学图像处理的基本理论。其基本的运算包括:腐蚀和膨胀、开运算和闭运算、骨架抽取、极限腐蚀、击中击不中变换、…...