购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

1.2 文本特征表示方法

文本特征表示就是把文字表示成计算机能够运算的数字或者向量。在机器学习问题中,我们从训练数据集中学习到的其实就是一组模型的参数,然后通过学习得到的参数确定模型的表示,最后用这个模型进行后续的预测或分类等任务。在模型训练过程中,我们会对训练数据集进行抽象、抽取大量特征,其中既有离散型特征,也有连续型特征。针对不同类型的数据,常用的文本特征表示方法有离散型特征表示方法和分布型特征表示方法。

1.2.1 离散型特征表示方法

简单来看,离散型数据是可数的,其变量值可以按照一定的顺序一一列举。离散特征的数值只能用自然数表示,包括个数、人数、年龄、城市等。在机器学习中,对于离散型的数据,我们常使用独热编码、词袋模型和TF-IDF进行特征表示。

1.独热编码特征表示

独热编码(One-Hot Encoding)又称一位有效编码,其方法是使用 N 位状态寄存器对 N 个状态进行编码,每个状态都有独立的寄存器位,并且在任何时候,独热编码都只有一位有效,即只有一位是一,其余都是零。独热编码是机器学习中的“万金油”,任何非数值类型的数据都可以直接或间接地进行独热编码。下面结合具体例子对独热编码进行阐述。

对于性别特征,按照 N 位状态寄存器对 N 个状态进行编码后的结果为:男:[1, 0]女:[0, 1],这里 N =2。

对下面的语料进行独热编码:

针对上述语料,我们要确认该语料需要多少个状态寄存器,实际上就是统计语料中出现过的单词数。该案例的 N =10,该语料构造的词典为:

该语料的独热编码结果为:

具体可以采用sklearn(scikit-learn)中的OneHotEncoder实现,代码如下:

其结果如图1-1所示。

图1-1 独热编码结果

独热编码的优缺点分析如下。

优点如下:独热编码为分类器处理离散型数据提供了方法,且能在一定程度上扩充样本特征数。

缺点如下:

1)当类别数量很多时,特征空间会很大,如当整个语料库中含有1000万个单词时,其独热编码维度为1000万,这就导致了维度爆炸,在这种情况下,一般可以使用PCA方法来减少特征维度;

2)该编码方法无法衡量不同词之间的相似关系,比如“i”和“me”的语义十分相似,但在独热编码中表示为两个完全不同的向量;

3)该编码只能反映某个词是否在句子中出现,无法衡量不同词在句子中的重要程度;

4)独热编码不考虑词与词之间的顺序;

5)独热编码得到的结果是高维稀疏矩阵,会浪费大量的计算资源和存储空间。

2.词袋模型特征表示

词袋(Bag Of Word, BOW)模型是指假设文本中的每个词都是独立的,仅使用词在文章中出现的频率决定词表达,并将每个词的表达进行简单的组合后来表示整个文本。词袋模型也称为计数向量表示,它不考虑语序和词法信息。接下来我们通过一个例子来详细讲解词袋模型的原理。假设有如下3篇简短文章:

首先,我们需要构建词袋,即将文章中的所有词提取出来放在一个袋子中:

共得到包含8个词的词袋,所以把每篇文章的维度固定为8。接下来我们需要统计词频,对词袋中的每个词统计其在文章中出现的频率,并按顺序一一记录,如文章1中,“今天”出现1次,“唱歌”出现1次,“喜欢”出现0次,“小明”出现0次,“我们”出现2次,“打球”出现0次,“明天”出现1次,“爬山”出现1次。因此,3篇文章的编码结果为:

观察得到的结果:横向来看,我们将每条文本都表示成一个向量;纵向来看,不同文档中单词的个数可以构成某个单词的词向量。可以采用sklearn中的CountVectorizer函数实现,代码如下所示:

词袋模型表示方法的优缺点分析如下。

优点如下:词袋模型除了考虑都有哪些词在文本中出现外,还考虑了词语出现的频次,用出现词语的频次来突出文本主题,进而表示文本的语义,简单快捷,易于理解。

缺点如下:

1)向量稀疏度较高,当词袋较大时,容易产生维度爆炸现象。这主要是因为词袋模型表示方法是对整个文档进行编码,但大多数情况下,一个文档中所包含的单词数是远小于整个语料库中所含单词数的;

2)词袋模型表示方法忽略了词的位置信息,而位置信息可以体现不同词之间的前后逻辑性;

3)该表示方法只能统计词语在文本中出现的频次,不能衡量每个词的重要程度,如存在大量的语气助词、虚词等却对语义理解没有实质性的帮助;

4)词袋模型假设文本中词与词之间相互独立,上下文没有关联性,有悖人类语言规律。

3.TF-IDF特征表示

TF-IDF(词频-逆文档频率)是文本处理中常用的一种统计方法,该方法可以评估一个单词在文档中的重要程度。单词的重要性与它在当前文本中出现的频率成正比,与它在语料库的其他文本中出现的频率成反比。TF-IDF的分数代表了单词在当前文档和整个语料库中的相对重要性。TF-IDF由两部分组成:第一部分是词频(Term Frequency, TF),第二部分是逆文档频率(Inverse Document Frequency, IDF)。

T F:关键词 w 在文档 D i 中出现的频率,公式如下。

其中,count( w )为关键词 w 在文档 D i 中出现的次数,| D i |为文档 D i 中所有词的数量。

DF(Document Frequency,文档频率):表示关键词 w 在其他文档中出现的频率。

IDF:反映关键词的普遍程度,当一个词越普遍,也就是有大量文档包含这个词时,其IDF值就会越低,这就意味着该单词的重要性不高,反之,IDF值越高,该单词的重要性越高。

注意,在IDF计算公式中,由于对数函数的底数无论是多少均不影响数值的变化趋势,所以书中不再标注底数,统一用log形式。其中, N 为语料库中的文档总数, I w , D i )表示文档 D i 是否包含关键词,若包含则为1,不包含则为0。但在一些特殊情况下,IDF公式可能会有一些小问题,比如某一个生僻词在我们的语料库中没有出现过,那么IDF公式中的分母就为0,IDF就没有意义了。因此,需要对IDF进行平滑处理,在分母位置加1,保证其不为0:

关键词 w 在文档 D i 中的TF-IDF值为:

一个单词的TF-IDF值越大,意味着该单词越重要。接下来通过一个简单的例子演示如何计算TF-IDF。

上述语料库的词典为[今天,上,NLP, 课程,的,有,意思,数据,也],该词典的长度为9。由TF-IDF的公式求得每个句子的TF-IDF向量表示如下:

句子1:

句子2:

句子3:

当然,我们也可以直接借助sklearn库中的TfidfVectorizer实现TF-IDF的计算,其实现代码如下:

TF-IDF的优点如下:TF-IDF特征表示方法除了考虑有哪些词在文本中出现外,还考虑单词出现的频率以及单词在整个语料库上的频率倒数。单词出现的词频可以突出文本的主题,单词的逆文档频率可以突出文档的独特性,进而反映文本的语义。此外,TF-IDF简单快速,结果比较符合实际。

TF-IDF的缺点如下:TF-IDF只考虑了单词的词频,忽略了词与词的位置关系,例如在提取关键词时,词的位置信息起到至关重要的作用,如文本的标题、文本的首句和尾句等包含比较重要的内容,应该赋予较高的权重。TF-IDF假定词与词之间是相互独立的,忽略了文本的上下文信息。此外,TF-IDF严重依赖语料库的选取,很容易将生僻词的重要性放大。同时,TF-IDF得到的向量稀疏度较高,会浪费大量的计算资源和存储空间。

1.2.2 分布型特征表示方法

离散型特征表示方法虽然实现了文本的向量表示,但是离散型特征表示方法仅仅是对单词进行符号化,不包含任何语义信息。如何将语义信息融入词表示中呢?Harris和Firth先后对分布假说进行定义和完善,提出上下文相似的词的语义信息也是相似的,这也就是说单词的语义信息是由单词的上下文决定的。基于该假说,研究者提出了分布型特征表示方法。分布型表示是指一种稠密的、低维度向量化的单词表示方法。假设我们有苹果、香蕉和梨三种水果,我们希望将这三个词转化为程序可以识别的向量。离散型特征表示方法会有如下转换:苹果=[1, 0, 0],香蕉=[0, 1, 0],梨=[0, 0, 1]。可以看出每种水果对应了向量中的某一位,其余位是0。如果水果种类增加了,离散型特征表示方法需要增加向量的维度。而使用分布型特征表示方法表示三个词的向量为:苹果=[0.2, 0.3, 0.3],香蕉=[-0.9, 0.8, -0.2],梨=[0.9, 0.1, -0.1]。

在详细介绍词的分布型特征表示方法之前,我们需要先了解NLP中的一个关键概念:语言模型(Language Model, LM)。语言模型是一种基于概率的判别模型,它的输入是一句话(单词的顺序序列),输出是这句话出现的概率,即这些单词的联合概率。假设我们有一个由 n 个词组成的句子 S =( w 1, w 2, …, wn ),如何衡量它的概率呢?我们假设每个单词 wi 都依赖第一个单词 w 1到它之前的一个单词 wi -1的影响:

P ( S )= P ( w 1, w 2, …, wn )= P ( w 1) P ( w 2| w 1)… P ( wn | wn -1, …, w 2 w 1)

常见的分布型特征表示方法有N-gram、Word2Vec、GloVe、ELMo、BERT等,下面会进行重点讲解。

1.N-gram

N-gram是一种语言模型,也是一种生成型模型。N-gram是一种类似于联想的方法,它的特点是:某个词的出现依赖于其他若干个词,且我们获得的信息越多,预测越准确。

假定文本中的每个词 wi 和前面 N -1个词有关,而与更前面的词无关,这种假设被称为 N -1阶马尔可夫假设。N-gram模型假设第 n 个词的出现仅与前面的 n -1个词相关,而与其他任何词都不相关,整句的概率就是各个词出现概率的乘积。每个词的概率都可以通过从语料库中直接统计该词出现的次数而得到。

如果仅依赖前一个词,即 N =2,就是Bi-gram(也被称为一阶马尔可夫链):

P ( S )= P ( w 1 , w 2 ,…, w n )= P ( w 1 ) P ( w 2 | w 1 )… P ( w n | w n -1 )

如果仅依赖前两个词,即 N =3,就是Tri-gram(也被称为二阶马尔可夫链):

P ( S )= P ( w 1 , w 2 ,…, w n )= P ( w 1 ) P ( w 2 | w 1 ) P ( w 3 | w 2 , w 1 )… P ( w n | w n -1 , w n -2 )

还有Four-gram、Five-gram等,不过 N >5的应用很少见。常用的是Bi-gram( N =2)和Tri-gram( N =3)。

N-gram与词袋模型原理类似,词袋模型利用一个字或者词进行装袋,而N-gram利用滑动窗口的方式选择 N 个向量的字或者词进行装袋。Bi-gram将相邻两个单词编上索引,Tri-gram将相邻三个单词编上索引,N-gram将相邻 N 个单词编上索引,一般可以按照字符级别和词级别进行N-gram特征表示。

接下来说说它们的区别。举个例子。

中文中的字词与英语中的字词有些区别。具体来说,在中文中,“今天”“天气”“真不错”表示词,“今”“天”“气”表示字;在英语中,it、is、a、good、day表示词,i、t、s、a等字符表示中文中的字。

针对这个案例,按照字符级别列出Bi-gram,可以得到如下内容。

中文:今天、天天、天气、气真、真不、不错。英文:it、ti、is、sa、ag、go、oo、od、dd、da、ay。

按词级别列出Bi-gram,可以得到如下内容。

中文:今天天气、天气真不错。英文:it is、is a、a good、good day。

在实际应用中,结果可能会出现细微的差距,比如本案例得出的结果会将空格包括进去:it is。在字符级别上的Bi-gram表示结果就是:it、t空格、空格i、is。

N-gram特征表示可以借助sklearn库中的CountVectorizer函数实现。词级别如下:

输出如下:

注意,N-gram产生的特征只是作为文本特征的候选集,后面可能需要用到信息熵、卡方统计、IDF等文本特征选择方式筛选出比较重要的特征。

2.Word2Vec

语言模型不需要人工标注语料(属于自监督模型),所以语言模型能够从无限制的大规模语料中学习到丰富的语义知识。为了缓解N-gram模型估算概率时遇到的数据稀疏问题,研究者们提出了神经网络语言模型(Neural Network Language Model, NNLM)。

Word2Vec作为神经概率语言模型的输入,其本身其实是神经概率模型的副产品,是神经网络在学习语言模型时产生的中间结果。Word2Vec包含连续词袋(Continuous Bag Of Word, CBOW)和Skip-gram两种训练模型,如图1-2所示。其中CBOW是根据上下文预测当前值,相当于一句话随机删除一个词,让模型预测删掉的词是什么;而Skip-gram是根据当前词预测上下文,相当于给出一个词,预测该词的前后是什么词。

图1-2 Word2Vec的两种模型

Word2Vec的优势在于它会考虑到词语的上下文,可以学习到文本中的语义和语法信息,并且得到的词向量维度小,可以节省存储空间和计算资源。此外,Word2Vec的通用性强,可以应用到各种NLP任务中。

但Word2Vec也存在一定的不足。具体来说,Word2Vec的词和向量是一对一的关系,无法解决多义词的问题;且Word2Vec是一种静态模型,虽然通用性强,但无法针对特定任务做动态优化。Word2Vec可以借助Gensim库实现:

3.GloVe

GloVe(Global Vectors for word representation)是斯坦福大学的Jeffrey、Richard等提出的一种词向量表示算法。总体来看,GloVe模型是一种对“词-词”矩阵进行分解从而得到词表示的方法,它可以把一个单词表达成一个由实数组成的向量,该向量可以捕捉单词之间的语义和语法特性,如相似性、类比性等。

在使用GloVe模型时,首先需要基于语料库构建词的共现矩阵,然后基于共现矩阵和GloVe模型学习词向量。接下来,我们通过一个简单的例子说明如何构建共现矩阵。假设有语料库:

该语料库涉及7个单词:i、love、you、but、him、am、sad。如果我们采用窗口宽度为5(左右长度为2)的统计窗口,可以得到如表1-3所示的窗口内容。

表1-3 该语料窗口为5时的窗口内容

(续)

窗口0、1、8、9长度小于5,因为中心词左侧或右侧内容少于2个。以窗口5为例,如何构造共现矩阵呢?假设共现矩阵为 X ,其元素为 X i , j ,矩阵中的每一个元素 X ij 代表单词 i 和上下文单词 j 在特定大小的上下文窗口内共同出现的次数。中心词为love,上下文词为but、you、him、i,则执行 X love, but +=1, X love, you +=1, X love, him +=1, X love, i +=1,依次使用窗口将整个语料库遍历一遍,即可得到共现矩阵 X ,如表1-4所示。

表1-4 共现矩阵

GloVe算法可以借助Gesim库来实现,其代码实现如下:

GloVe与Word2Vec的关键区别在于,GloVe不只依赖于附近的单词,还会结合全局统计数据(即跨语料库的单词的出现情况)来获得词向量。与Word2Vec相比,GloVe更容易并行化,且在训练数据较大时速度更快。

4.ELMo

Word2Vec和GloVe模型得到的词向量都是静态词向量,静态词向量会对多义词的语义进行融合,训练结束之后不会根据上下文进行改变,无法解决多义词的问题。例如:“我今天买了7斤苹果”和“我今天买了苹果7”中的“苹果”就是一个多义词。而ELMo模型训练过的词向量可以解决多义词的问题。

ELMo是一种双向语言模型,如图1-3所示,该模型的特点是每一个词语的特征表示都是整个输入语句的函数。具体做法是先在大语料上以语言模型为目标训练出BiLSTM模型,然后利用LSTM模型产生词语的特征表示。关于LSTM模型和BiLSTM模型的更多内容,请参见本书第3章,这里不再详述。

图1-3 ELMo结构图

如图1-3所示,ELMo主要使用一个两层双向的LSTM模型。给定一个长度为 N 的词汇序列( t 1, t 2, t 3, …, tN ),在每个时间步,前向语言模型会根据前面的词汇预测当前词汇的概率,最终对每个时间步的输出概率进行累积,将累积结果作为整个序列的预测概率,并期望该概率越大越好,即:

前向语言模型可能会包含多层单向LSTM,但在进行概率预测时,我们利用最后一层LSTM的每个时间步的隐藏状态向量进行预测。

后向语言模型与前向语言模型相反,后向语言模型将词汇序列进行逆排序,每个时间步是根据后面的词汇信息预测之前的分词,具体如下:

双向语言模型将前向语言模型和后向语言模型进行结合,直接最大化前向和后向语言模型的对数概率,即:

ELMo是双向语言模型内部中间层的组合,对于每个词来说,一个 L 层的双向语言模型要计算出2 L +1个表示,为了应用到其他模型中,ELMo需要将所有层的输出结果整合为一个向量。

相比之前的模型,ELMo可以更好地捕捉文本中的语义和语法信息。此外,ELMo是基于词级别的特征表示,对词汇量没有限制,但相应的每个词的编码都需要语言模型计算得到,计算速度较慢。

ELMo该怎么使用呢?这里介绍3种可以使用预训练好的ELMo模型的方法:

1)ELMo官方allenNLP发布的基于PyTorch实现的版本;

2)ELMo官方发布的基于TensorFlow实现的版本;

3)TensorFlow-Hub发布的基于TensorFlow实现的ELMo版本。本节内容通过该版本实现。

在上述代码中,使用hub.Module第一次加载模型时会非常慢,因为要下载模型。该模型是训练好的模型,即LSTM中的参数都是固定的。

如果要将上面生成的embedding转换成Numpy向量,可以使用下面的代码实现。

5.BERT

从Word2Vec到ELMo,模型的性能得到了极大的提升。这说明预训练模型的潜力无限,不是只能为下游任务提供一份精准的词向量。那我们可不可以直接预训练一个“龙骨级”的模型呢?如果它里面已经充分地描述了字符级、词级、句子级甚至句间关系的特征,那么在不同的NLP任务中,只需要为特定任务设计一个轻量级的输出层(比如分类任务的一个分类层)。BERT是目前最强的预训练模型,其性能在NLP领域刷新了多个记录。

BERT(Bidirectional Encoder Representations from Transformer)是一种预训练的语言表示模型。它强调不再像以往一样采用传统的单向语言模型或者把两个单向语言模型进行浅层拼接的方法进行预训练,而是采用新的掩码语言模型(MLM)生成深层的双向语言表示。MLM是指,我们不是像传统的语言模型那样给定已经出现过的词,去预测下一个词,而是直接把整个句子的一部分词盖住,让模型去预测这些盖住的词是什么。这个任务其实最开始叫作cloze test(可以理解为“完形填空测验”)。该模型有以下主要优点。

1)采用MLM对双向的Transformer结构进行预训练,以生成深层的双向语言表示。

2)预训练后,只需要添加一个额外的输出层进行微调,就可以在各式各样的下游任务中取得最优的表现,且整个过程不需要对BERT结构进行修改。

BERT是如何实现的呢?BERT模型的大体结构如图1-4所示。

图1-4 BERT结构图

如图1-4所示,BERT由两层Transformer编码器(Encoder)组成,该编码器的结构如图1-5所示。

图1-5 Transformer结构,左侧是编码器,右侧是解码器

Transformer的核心思想是使用注意力(Attention)机制,在一个序列的不同位置之间建立距离为1的平行关系,从而解决循环神经网络的长距离依赖问题。Transformer与大多数Seq2Seq(序列到序列)模型一样由编码器和解码器(Decoder)两部分组成。编码器负责把自然语言序列映射为隐藏层的数学表达;而解码器负责把隐藏层映射回自然语言序列。

1)Transformer的编码器:编码器由 N 个相同的层(Layer)组成,Layer就是图1-5中左侧的单元,最左边有个 N x ,这里 x 是6。每个Layer由两个子层(Sublayer)组成,分别是多头自注意力机制(Multi-Head Self-Attention Mechanism)和全连接前馈网络(Fully Connected Feed-Forward Network)。其中每个子层都使用了残差连接(Residual Connection)和归一化(Normalisation),因此子层的输出可以表示为:

sub_layer_output=layerNorm( x +(sublayer( x )))

接下来我们按顺序解释一下编码器中的这两个子层:

多头自注意力机制可以表示为:

Attention_output=Attention( Q , K , V )

多头自注意力机制则是通过 h 个不同的线性变换对 Q K V 进行投影,之后将不同的注意力机制的结果拼接起来:

Transformer中的注意力机制采用的是缩放的点积(Scaled Dot-Product)运算,即:

位置前馈网络实际上是一个全连接前馈网络,每个位置的词都经过这个前馈网络进行运算。该层包含两个线性变换,即两个全连接层,第一个全连接层的激活函数为ReLU,该层可以表示为:

FFN( x )=max(0, xW 1 + b 1 ) W 2 + b 2

注意:每个编码器和解码器中的前馈网络结构是相同的,但它们不共享参数。

2)Transformer的解码器:如图1-5右侧部分所示,解码器除了包含与编码器中相同的多头自注意力层和前馈网络层外,还有一个编码器层和解码器层之间的注意力层,该注意力层用来关注输入信息的重要部分。

解码器的输入、输出和解码过程如下:

输出:对应 i 位置的输出词的概率分布。

输入:编码器的输出和对应 i -1位置解码器的输出。中间的注意力层不是自注意力层,它的 K V 来自编码器, Q 来自上一位置解码器的输出。

解码:训练时,将输出一次全部解码出来,用上一步的真实值(Ground Truth)来预测;如果预测过程中没有真实值,则需要一个一个预测。

3)位置编码(Positional Encoding):Transformer结构中除了最重要的编码器和解码器,还包含数据预处理部分。Transformer抛弃了循环神经网络(RNN),而RNN最大的优点就是在时间序列上对数据的抽象。为了捕获数据中序列的顺序,Transformer设计了位置编码,为每个输入的词嵌入添加了一个向量,这些向量遵循模型学习的特定模式,有助于模型确定每个词的位置,或序列中不同词之间的距离。位置编码的公式为:

至此,BERT的主体结构就介绍完了,接下来我们看一看BERT的输入和输出。

BERT的输入为每一个token对应的特征表示(图1-6下方的梯形块是token,中间的矩形块是token对应的特征表示),BERT的单词字典由WordPiece算法构建。为了实现具体的分类任务,除了单词的token之外,还需要在输入的每一个序列开头插入特定的分类token([CLS]),该token对应最后一个Transformer层的输出,起到聚集整个序列特征表示信息的作用。

图1-6 BERT预训练流程图

由于BERT是一个预训练模型,为了适应各种各样的自然语言任务,模型所输入的序列需要包含一句话(如文本情感分类、序列标注任务的数据)甚至两句话以上(如文本摘要、自然语言推断、问答任务的数据)。如何让模型去分辨哪个范围属于句子A,哪个范围属于句子B呢?BERT采用了两种解决方法。

1)在序列token中把分割token([SEP])插入每个句子,以分开不同的句子token。

2)为每一个token表征都添加一个可学习的嵌入(Embedding),来表示它是属于句子A还是属于句子B。

BERT的输入为每一个token对应的表示,但实际上该表示是由三部分组成的,分别是对应的token(Token Embedding)、分割嵌入(Segment Embedding)和位置嵌入(Position Embedding),如图1-7所示。

图1-7 token的组成

了解了BERT的输入,那它的输出是什么呢?Transformer的特点是有多少个输入就对应多少个输出,所以BERT的输出如图1-6上半部分圆角矩形所示。

C 为分类token([CLS])对应的最后一个Transformer的输出, Ti 则代表其他token对应最后一个Transformer的输出。对一些token级别的任务来说(如序列标注和问答任务),需要把 Ti 输入额外的输出层中进行预测。对一些句子级别的任务来说(如自然语言推断和情感分类任务),需要把 C 输入额外的输出层中,这里也就解释了为什么需要在每一个token序列前插入特定的分类token。

到此为止,BERT的结构和原理介绍完了,那如何使用BERT呢?

首先,需要安装server和client两个工具包:

然后,下载BERT预训练模型,比如我们下载中文版本BERT模型——BERT-Base,并解压到本地某个目录下。例如:/bert-base-chinese。

然后,打开终端,输入以下命令启动服务:

其中,参数model_dir为解压得到的BERT预训练模型路径,num_worker为进程数。需要说明的是,num_worker必须小于CPU的核心数或GPU设备数。

最后,编写客户端代码:

除了bert-as-service这种使用方式外,当然也可以利用TensorFlow、Keras等深度学习框架重建BERT预训练模型,然后利用重建的BERT模型去获取文本的向量表示。 8oVzPZ9Jl3+7/UpTj9iZ6pK7wk/tEb1QSZ4jc99suGpDiL+17Civ9uB2j01HqAt8

点击中间区域
呼出菜单
上一章
目录
下一章
×